From 1846d6c15f3c1524d97dd8455104b2a0e19b3da8 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Thu, 6 Jun 2024 19:05:40 +0200 Subject: [PATCH 001/979] allow custom time in clock tool --- lib/src/model/clock/clock_controller.dart | 57 ++++-- lib/src/view/clock/clock_settings.dart | 7 +- .../create_custom_time_control_screen.dart | 192 ++++++++++++++++++ lib/src/view/play/time_control_modal.dart | 20 ++ 4 files changed, 262 insertions(+), 14 deletions(-) create mode 100644 lib/src/view/play/create_custom_time_control_screen.dart diff --git a/lib/src/model/clock/clock_controller.dart b/lib/src/model/clock/clock_controller.dart index 5dea10a871..619a4a4dcc 100644 --- a/lib/src/model/clock/clock_controller.dart +++ b/lib/src/model/clock/clock_controller.dart @@ -10,8 +10,15 @@ part 'clock_controller.g.dart'; class ClockController extends _$ClockController { @override ClockState build() { + const time = Duration(minutes: 10); + const increment = Duration.zero; return ClockState.fromOptions( - const ClockOptions(time: Duration(minutes: 10), increment: Duration.zero), + const ClockOptions( + timePlayerTop: time, + timePlayerBottom: time, + incrementPlayerTop: increment, + incrementPlayerBottom: increment, + ), ); } @@ -39,16 +46,22 @@ class ClockController extends _$ClockController { } if (playerType == ClockPlayerType.top) { - state = state.copyWith(playerTopTime: duration + state.options.increment); + state = state.copyWith( + playerTopTime: duration + state.options.incrementPlayerTop, + ); } else { - state = - state.copyWith(playerBottomTime: duration + state.options.increment); + state = state.copyWith( + playerBottomTime: duration + state.options.incrementPlayerBottom, + ); } } void updateOptions(TimeIncrement timeIncrement) => state = ClockState.fromTimeIncrement(timeIncrement); + void updateOptionsCustom(TimeIncrement playerTop, TimeIncrement playerBottom) => + state = ClockState.fromSeparateTimeIncrements(playerTop, playerBottom); + void setLoser(ClockPlayerType playerType) => state = state.copyWith(currentPlayer: null, loser: playerType); @@ -66,8 +79,10 @@ class ClockOptions with _$ClockOptions { const ClockOptions._(); const factory ClockOptions({ - required Duration time, - required Duration increment, + required Duration timePlayerTop, + required Duration timePlayerBottom, + required Duration incrementPlayerTop, + required Duration incrementPlayerBottom, }) = _ClockOptions; } @@ -90,15 +105,33 @@ class ClockState with _$ClockState { factory ClockState.fromTimeIncrement(TimeIncrement timeIncrement) { final options = ClockOptions( - time: Duration(seconds: timeIncrement.time), - increment: Duration(seconds: timeIncrement.increment), + timePlayerTop: Duration(seconds: timeIncrement.time), + timePlayerBottom: Duration(seconds: timeIncrement.time), + incrementPlayerTop: Duration(seconds: timeIncrement.increment), + incrementPlayerBottom: Duration(seconds: timeIncrement.increment), ); return ClockState( id: DateTime.now().millisecondsSinceEpoch, options: options, - playerTopTime: options.time, - playerBottomTime: options.time, + playerTopTime: options.timePlayerTop, + playerBottomTime: options.timePlayerBottom, + ); + } + + factory ClockState.fromSeparateTimeIncrements( + TimeIncrement playerTop, TimeIncrement playerBottom,) { + final options = ClockOptions( + timePlayerTop: Duration(seconds: playerTop.time), + timePlayerBottom: Duration(seconds: playerBottom.time), + incrementPlayerTop: Duration(seconds: playerTop.increment), + incrementPlayerBottom: Duration(seconds: playerBottom.increment), + ); + return ClockState( + id: DateTime.now().millisecondsSinceEpoch, + options: options, + playerTopTime: options.timePlayerTop, + playerBottomTime: options.timePlayerBottom, ); } @@ -106,8 +139,8 @@ class ClockState with _$ClockState { return ClockState( id: DateTime.now().millisecondsSinceEpoch, options: options, - playerTopTime: options.time, - playerBottomTime: options.time, + playerTopTime: options.timePlayerTop, + playerBottomTime: options.timePlayerBottom, ); } diff --git a/lib/src/view/clock/clock_settings.dart b/lib/src/view/clock/clock_settings.dart index 022f6bdcd5..a9f6bdead0 100644 --- a/lib/src/view/clock/clock_settings.dart +++ b/lib/src/view/clock/clock_settings.dart @@ -54,12 +54,15 @@ class ClockSettings extends ConsumerWidget { return TimeControlModal( excludeUltraBullet: true, value: TimeIncrement( - options.time.inSeconds, - options.increment.inSeconds, + options.timePlayerTop.inSeconds, + options.incrementPlayerTop.inSeconds, ), onSelected: (choice) { controller.updateOptions(choice); }, + onSelectedCustom: (playerTop, playerBottom) { + controller.updateOptionsCustom(playerTop, playerBottom); + }, ); }, ); diff --git a/lib/src/view/play/create_custom_time_control_screen.dart b/lib/src/view/play/create_custom_time_control_screen.dart new file mode 100644 index 0000000000..64a3b3b362 --- /dev/null +++ b/lib/src/view/play/create_custom_time_control_screen.dart @@ -0,0 +1,192 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:lichess_mobile/src/model/common/time_increment.dart'; +import 'package:lichess_mobile/src/model/lobby/game_setup.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/list.dart'; +import 'package:lichess_mobile/src/widgets/non_linear_slider.dart'; +import 'package:lichess_mobile/src/widgets/platform.dart'; + +class CreateCustomTimeControlScreen extends StatelessWidget { + final void Function(TimeIncrement playerTop, TimeIncrement playerBottom) onSubmit; + final TimeIncrement defaultTime; + + const CreateCustomTimeControlScreen({ + required this.onSubmit, + required this.defaultTime, + }); + + @override + Widget build(BuildContext context) { + return PlatformWidget( + androidBuilder: _androidBuilder, + iosBuilder: _iosBuilder, + ); + } + + Widget _androidBuilder(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text(context.l10n.custom)), + body: _Body(onSubmit: onSubmit, defaultTime: defaultTime), + ); + } + + Widget _iosBuilder(BuildContext context) { + return CupertinoPageScaffold( + navigationBar: const CupertinoNavigationBar(), + child: _Body(onSubmit: onSubmit, defaultTime: defaultTime), + ); + } +} + +class _Body extends StatefulWidget { + final void Function(TimeIncrement timeTopPlayer, TimeIncrement timeBottomPlayer) onSubmit; + final TimeIncrement defaultTime; + + const _Body({ + required this.onSubmit, + required this.defaultTime, + }); + + @override + State<_Body> createState() => _BodyState(); +} + +class _BodyState extends State<_Body> { + late int timeTopPlayer; + late int incrementTopPlayer; + late int timeBottomPlayer; + late int incrementBottomPlayer; + + @override + void initState() { + timeTopPlayer = timeBottomPlayer = widget.defaultTime.time; + incrementTopPlayer = incrementBottomPlayer = widget.defaultTime.increment; + super.initState(); + } + + @override + Widget build(BuildContext context) { + void onSubmit(TimeIncrement topPlayer, TimeIncrement bottomPlayer) { + Navigator.pop(context); + Navigator.pop(context); + widget.onSubmit(topPlayer, bottomPlayer); + } + + return Padding( + padding: Styles.bodyPadding, + child: ListView( + children: [ + _PlayerTimeSlider( + playerNr: 1, + time: timeTopPlayer, + increment: incrementTopPlayer, + updateTime: (int time) => setState(() => timeTopPlayer = time), + updateIncrement: (int increment) => setState(() => incrementTopPlayer = increment), + ), + _PlayerTimeSlider( + playerNr: 2, + time: timeBottomPlayer, + increment: incrementBottomPlayer, + updateTime: (int time) => setState(() => timeBottomPlayer = time), + updateIncrement: (int increment) => setState(() => incrementBottomPlayer = increment), + ), + FatButton( + semanticsLabel: context.l10n.apply, + child: Text(context.l10n.apply), + onPressed: () => onSubmit( + TimeIncrement(timeTopPlayer, incrementTopPlayer), + TimeIncrement(timeBottomPlayer, incrementBottomPlayer), + ), + ), + ], + ), + ); + } +} + +class _PlayerTimeSlider extends StatelessWidget { + const _PlayerTimeSlider({ + required this.playerNr, + required this.time, + required this.increment, + required this.updateTime, + required this.updateIncrement, + }); + + final int playerNr; + final int time; + final int increment; + final void Function(int time) updateTime; + final void Function(int time) updateIncrement; + + @override + Widget build(BuildContext context) { + return ListView( + shrinkWrap: true, + children: [ + Text( + '${context.l10n.player} $playerNr', + style: Styles.title, + ), + PlatformListTile( + padding: EdgeInsets.zero, + title: Text( + '${context.l10n.time}: ${context.l10n.nbMinutes(secToMin(time))}', + ), + subtitle: NonLinearSlider( + value: time, + values: kAvailableTimesInSeconds, + labelBuilder: _clockTimeLabel, + onChange: Theme.of(context).platform == TargetPlatform.iOS + ? (num value) { + updateTime(value.toInt()); + } + : null, + onChangeEnd: (num value) { + updateTime(value.toInt()); + }, + ), + ), + PlatformListTile( + padding: EdgeInsets.zero, + title: Text( + '${context.l10n.increment}: ${context.l10n.nbSeconds(increment)}', + ), + subtitle: NonLinearSlider( + value: increment, + values: kAvailableIncrementsInSeconds, + labelBuilder: (num sec) => sec.toString(), + onChange: Theme.of(context).platform == TargetPlatform.iOS + ? (num value) { + updateIncrement(value.toInt()); + } + : null, + onChangeEnd: (num value) { + updateIncrement(value.toInt()); + }, + ), + ), + ], + ); + } +} + +int secToMin(num sec) => sec ~/ 60; + +String _clockTimeLabel(num seconds) { + switch (seconds) { + case 0: + return '0'; + case 45: + return '¾'; + case 30: + return '½'; + case 15: + return '¼'; + default: + return secToMin(seconds).toString(); + } +} diff --git a/lib/src/view/play/time_control_modal.dart b/lib/src/view/play/time_control_modal.dart index ec301ca72b..f109115e5e 100644 --- a/lib/src/view/play/time_control_modal.dart +++ b/lib/src/view/play/time_control_modal.dart @@ -6,15 +6,19 @@ import 'package:lichess_mobile/src/model/common/time_increment.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/view/play/create_custom_time_control_screen.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; class TimeControlModal extends ConsumerWidget { final ValueSetter onSelected; + final void Function(TimeIncrement playerTop, TimeIncrement playerBottom)? onSelectedCustom; final TimeIncrement value; final bool excludeUltraBullet; const TimeControlModal({ required this.onSelected, + this.onSelectedCustom, required this.value, this.excludeUltraBullet = false, super.key, @@ -38,6 +42,22 @@ class TimeControlModal extends ConsumerWidget { style: Styles.title, ), const SizedBox(height: 26.0), + if (onSelectedCustom != null) + FatButton( + semanticsLabel: context.l10n.custom, + onPressed: () => { + pushPlatformRoute( + context, + title: context.l10n.custom, + builder: (_) => CreateCustomTimeControlScreen( + onSubmit: onSelectedCustom!, + defaultTime: value, + ), + ), + }, + child: Text(context.l10n.custom), + ), + const SizedBox(height: 20.0), _SectionChoices( value, choices: [ From 726467d054a3f071b184d844473555bde26432ed Mon Sep 17 00:00:00 2001 From: Mauritz Date: Thu, 6 Jun 2024 19:10:30 +0200 Subject: [PATCH 002/979] separate default time for top and bottom player --- lib/src/view/clock/clock_settings.dart | 6 ++- lib/src/view/home/quick_game_button.dart | 3 +- .../create_custom_time_control_screen.dart | 50 +++++++++++-------- lib/src/view/play/time_control_modal.dart | 17 ++++--- 4 files changed, 45 insertions(+), 31 deletions(-) diff --git a/lib/src/view/clock/clock_settings.dart b/lib/src/view/clock/clock_settings.dart index a9f6bdead0..392e210921 100644 --- a/lib/src/view/clock/clock_settings.dart +++ b/lib/src/view/clock/clock_settings.dart @@ -53,10 +53,14 @@ class ClockSettings extends ConsumerWidget { ); return TimeControlModal( excludeUltraBullet: true, - value: TimeIncrement( + topPlayer: TimeIncrement( options.timePlayerTop.inSeconds, options.incrementPlayerTop.inSeconds, ), + bottomPlayer: TimeIncrement( + options.timePlayerBottom.inSeconds, + options.incrementPlayerBottom.inSeconds, + ), onSelected: (choice) { controller.updateOptions(choice); }, diff --git a/lib/src/view/home/quick_game_button.dart b/lib/src/view/home/quick_game_button.dart index 5fbdc1a065..2f9f918f99 100644 --- a/lib/src/view/home/quick_game_button.dart +++ b/lib/src/view/home/quick_game_button.dart @@ -53,7 +53,8 @@ class QuickGameButton extends ConsumerWidget { ), builder: (BuildContext context) { return TimeControlModal( - value: playPrefs.timeIncrement, + topPlayer: playPrefs.timeIncrement, + bottomPlayer: playPrefs.timeIncrement, onSelected: (choice) { ref .read(gameSetupPreferencesProvider.notifier) diff --git a/lib/src/view/play/create_custom_time_control_screen.dart b/lib/src/view/play/create_custom_time_control_screen.dart index 64a3b3b362..ccd6146e44 100644 --- a/lib/src/view/play/create_custom_time_control_screen.dart +++ b/lib/src/view/play/create_custom_time_control_screen.dart @@ -11,11 +11,13 @@ import 'package:lichess_mobile/src/widgets/platform.dart'; class CreateCustomTimeControlScreen extends StatelessWidget { final void Function(TimeIncrement playerTop, TimeIncrement playerBottom) onSubmit; - final TimeIncrement defaultTime; + final TimeIncrement topPlayer; + final TimeIncrement bottomPlayer; const CreateCustomTimeControlScreen({ required this.onSubmit, - required this.defaultTime, + required this.topPlayer, + required this.bottomPlayer, }); @override @@ -29,25 +31,27 @@ class CreateCustomTimeControlScreen extends StatelessWidget { Widget _androidBuilder(BuildContext context) { return Scaffold( appBar: AppBar(title: Text(context.l10n.custom)), - body: _Body(onSubmit: onSubmit, defaultTime: defaultTime), + body: _Body(onSubmit: onSubmit, topPlayer: topPlayer, bottomPlayer: bottomPlayer), ); } Widget _iosBuilder(BuildContext context) { return CupertinoPageScaffold( navigationBar: const CupertinoNavigationBar(), - child: _Body(onSubmit: onSubmit, defaultTime: defaultTime), + child: _Body(onSubmit: onSubmit, topPlayer: topPlayer, bottomPlayer: bottomPlayer), ); } } class _Body extends StatefulWidget { final void Function(TimeIncrement timeTopPlayer, TimeIncrement timeBottomPlayer) onSubmit; - final TimeIncrement defaultTime; + final TimeIncrement topPlayer; + final TimeIncrement bottomPlayer; const _Body({ required this.onSubmit, - required this.defaultTime, + required this.topPlayer, + required this.bottomPlayer, }); @override @@ -62,8 +66,10 @@ class _BodyState extends State<_Body> { @override void initState() { - timeTopPlayer = timeBottomPlayer = widget.defaultTime.time; - incrementTopPlayer = incrementBottomPlayer = widget.defaultTime.increment; + timeTopPlayer = widget.topPlayer.time; + timeBottomPlayer = widget.bottomPlayer.time; + incrementTopPlayer = widget.topPlayer.increment; + incrementBottomPlayer = widget.bottomPlayer.increment; super.initState(); } @@ -81,15 +87,15 @@ class _BodyState extends State<_Body> { children: [ _PlayerTimeSlider( playerNr: 1, - time: timeTopPlayer, - increment: incrementTopPlayer, + timeSec: timeTopPlayer, + incrementSec: incrementTopPlayer, updateTime: (int time) => setState(() => timeTopPlayer = time), updateIncrement: (int increment) => setState(() => incrementTopPlayer = increment), ), _PlayerTimeSlider( playerNr: 2, - time: timeBottomPlayer, - increment: incrementBottomPlayer, + timeSec: timeBottomPlayer, + incrementSec: incrementBottomPlayer, updateTime: (int time) => setState(() => timeBottomPlayer = time), updateIncrement: (int increment) => setState(() => incrementBottomPlayer = increment), ), @@ -110,15 +116,15 @@ class _BodyState extends State<_Body> { class _PlayerTimeSlider extends StatelessWidget { const _PlayerTimeSlider({ required this.playerNr, - required this.time, - required this.increment, + required this.timeSec, + required this.incrementSec, required this.updateTime, required this.updateIncrement, }); final int playerNr; - final int time; - final int increment; + final int timeSec; + final int incrementSec; final void Function(int time) updateTime; final void Function(int time) updateIncrement; @@ -134,10 +140,10 @@ class _PlayerTimeSlider extends StatelessWidget { PlatformListTile( padding: EdgeInsets.zero, title: Text( - '${context.l10n.time}: ${context.l10n.nbMinutes(secToMin(time))}', + '${context.l10n.time}: ${timeSec < 60 ? context.l10n.nbSeconds(timeSec) : context.l10n.nbMinutes(_secToMin(timeSec))}', ), subtitle: NonLinearSlider( - value: time, + value: timeSec, values: kAvailableTimesInSeconds, labelBuilder: _clockTimeLabel, onChange: Theme.of(context).platform == TargetPlatform.iOS @@ -153,10 +159,10 @@ class _PlayerTimeSlider extends StatelessWidget { PlatformListTile( padding: EdgeInsets.zero, title: Text( - '${context.l10n.increment}: ${context.l10n.nbSeconds(increment)}', + '${context.l10n.increment}: ${context.l10n.nbSeconds(incrementSec)}', ), subtitle: NonLinearSlider( - value: increment, + value: incrementSec, values: kAvailableIncrementsInSeconds, labelBuilder: (num sec) => sec.toString(), onChange: Theme.of(context).platform == TargetPlatform.iOS @@ -174,7 +180,7 @@ class _PlayerTimeSlider extends StatelessWidget { } } -int secToMin(num sec) => sec ~/ 60; +int _secToMin(num sec) => sec ~/ 60; String _clockTimeLabel(num seconds) { switch (seconds) { @@ -187,6 +193,6 @@ String _clockTimeLabel(num seconds) { case 15: return '¼'; default: - return secToMin(seconds).toString(); + return _secToMin(seconds).toString(); } } diff --git a/lib/src/view/play/time_control_modal.dart b/lib/src/view/play/time_control_modal.dart index f109115e5e..30788feb31 100644 --- a/lib/src/view/play/time_control_modal.dart +++ b/lib/src/view/play/time_control_modal.dart @@ -13,13 +13,15 @@ import 'package:lichess_mobile/src/widgets/buttons.dart'; class TimeControlModal extends ConsumerWidget { final ValueSetter onSelected; final void Function(TimeIncrement playerTop, TimeIncrement playerBottom)? onSelectedCustom; - final TimeIncrement value; + final TimeIncrement topPlayer; + final TimeIncrement bottomPlayer; final bool excludeUltraBullet; const TimeControlModal({ required this.onSelected, this.onSelectedCustom, - required this.value, + required this.topPlayer, + required this.bottomPlayer, this.excludeUltraBullet = false, super.key, }); @@ -51,7 +53,8 @@ class TimeControlModal extends ConsumerWidget { title: context.l10n.custom, builder: (_) => CreateCustomTimeControlScreen( onSubmit: onSelectedCustom!, - defaultTime: value, + topPlayer: topPlayer, + bottomPlayer: bottomPlayer, ), ), }, @@ -59,7 +62,7 @@ class TimeControlModal extends ConsumerWidget { ), const SizedBox(height: 20.0), _SectionChoices( - value, + topPlayer, choices: [ if (!excludeUltraBullet) const TimeIncrement(0, 1), const TimeIncrement(60, 0), @@ -74,7 +77,7 @@ class TimeControlModal extends ConsumerWidget { ), const SizedBox(height: 20.0), _SectionChoices( - value, + topPlayer, choices: const [ TimeIncrement(180, 0), TimeIncrement(180, 2), @@ -89,7 +92,7 @@ class TimeControlModal extends ConsumerWidget { ), const SizedBox(height: 20.0), _SectionChoices( - value, + topPlayer, choices: const [ TimeIncrement(600, 0), TimeIncrement(600, 5), @@ -104,7 +107,7 @@ class TimeControlModal extends ConsumerWidget { ), const SizedBox(height: 20.0), _SectionChoices( - value, + topPlayer, choices: const [ TimeIncrement(1500, 0), TimeIncrement(1800, 0), From 0ee821f10cc0c9575d84e4a62771909e51cb8f11 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Thu, 6 Jun 2024 19:13:12 +0200 Subject: [PATCH 003/979] fix: formatting --- lib/src/model/clock/clock_controller.dart | 9 +++-- lib/src/view/clock/clock_settings.dart | 5 ++- .../create_custom_time_control_screen.dart | 26 ++++++++++---- lib/src/view/play/time_control_modal.dart | 36 +++++++++++-------- 4 files changed, 52 insertions(+), 24 deletions(-) diff --git a/lib/src/model/clock/clock_controller.dart b/lib/src/model/clock/clock_controller.dart index 619a4a4dcc..47b0a35b96 100644 --- a/lib/src/model/clock/clock_controller.dart +++ b/lib/src/model/clock/clock_controller.dart @@ -59,7 +59,10 @@ class ClockController extends _$ClockController { void updateOptions(TimeIncrement timeIncrement) => state = ClockState.fromTimeIncrement(timeIncrement); - void updateOptionsCustom(TimeIncrement playerTop, TimeIncrement playerBottom) => + void updateOptionsCustom( + TimeIncrement playerTop, + TimeIncrement playerBottom, + ) => state = ClockState.fromSeparateTimeIncrements(playerTop, playerBottom); void setLoser(ClockPlayerType playerType) => @@ -120,7 +123,9 @@ class ClockState with _$ClockState { } factory ClockState.fromSeparateTimeIncrements( - TimeIncrement playerTop, TimeIncrement playerBottom,) { + TimeIncrement playerTop, + TimeIncrement playerBottom, + ) { final options = ClockOptions( timePlayerTop: Duration(seconds: playerTop.time), timePlayerBottom: Duration(seconds: playerBottom.time), diff --git a/lib/src/view/clock/clock_settings.dart b/lib/src/view/clock/clock_settings.dart index 392e210921..cbd4ba627f 100644 --- a/lib/src/view/clock/clock_settings.dart +++ b/lib/src/view/clock/clock_settings.dart @@ -65,7 +65,10 @@ class ClockSettings extends ConsumerWidget { controller.updateOptions(choice); }, onSelectedCustom: (playerTop, playerBottom) { - controller.updateOptionsCustom(playerTop, playerBottom); + controller.updateOptionsCustom( + playerTop, + playerBottom, + ); }, ); }, diff --git a/lib/src/view/play/create_custom_time_control_screen.dart b/lib/src/view/play/create_custom_time_control_screen.dart index ccd6146e44..68d43f75a9 100644 --- a/lib/src/view/play/create_custom_time_control_screen.dart +++ b/lib/src/view/play/create_custom_time_control_screen.dart @@ -10,7 +10,8 @@ import 'package:lichess_mobile/src/widgets/non_linear_slider.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; class CreateCustomTimeControlScreen extends StatelessWidget { - final void Function(TimeIncrement playerTop, TimeIncrement playerBottom) onSubmit; + final void Function(TimeIncrement playerTop, TimeIncrement playerBottom) + onSubmit; final TimeIncrement topPlayer; final TimeIncrement bottomPlayer; @@ -31,20 +32,31 @@ class CreateCustomTimeControlScreen extends StatelessWidget { Widget _androidBuilder(BuildContext context) { return Scaffold( appBar: AppBar(title: Text(context.l10n.custom)), - body: _Body(onSubmit: onSubmit, topPlayer: topPlayer, bottomPlayer: bottomPlayer), + body: _Body( + onSubmit: onSubmit, + topPlayer: topPlayer, + bottomPlayer: bottomPlayer, + ), ); } Widget _iosBuilder(BuildContext context) { return CupertinoPageScaffold( navigationBar: const CupertinoNavigationBar(), - child: _Body(onSubmit: onSubmit, topPlayer: topPlayer, bottomPlayer: bottomPlayer), + child: _Body( + onSubmit: onSubmit, + topPlayer: topPlayer, + bottomPlayer: bottomPlayer, + ), ); } } class _Body extends StatefulWidget { - final void Function(TimeIncrement timeTopPlayer, TimeIncrement timeBottomPlayer) onSubmit; + final void Function( + TimeIncrement timeTopPlayer, + TimeIncrement timeBottomPlayer, + ) onSubmit; final TimeIncrement topPlayer; final TimeIncrement bottomPlayer; @@ -90,14 +102,16 @@ class _BodyState extends State<_Body> { timeSec: timeTopPlayer, incrementSec: incrementTopPlayer, updateTime: (int time) => setState(() => timeTopPlayer = time), - updateIncrement: (int increment) => setState(() => incrementTopPlayer = increment), + updateIncrement: (int increment) => + setState(() => incrementTopPlayer = increment), ), _PlayerTimeSlider( playerNr: 2, timeSec: timeBottomPlayer, incrementSec: incrementBottomPlayer, updateTime: (int time) => setState(() => timeBottomPlayer = time), - updateIncrement: (int increment) => setState(() => incrementBottomPlayer = increment), + updateIncrement: (int increment) => + setState(() => incrementBottomPlayer = increment), ), FatButton( semanticsLabel: context.l10n.apply, diff --git a/lib/src/view/play/time_control_modal.dart b/lib/src/view/play/time_control_modal.dart index 30788feb31..0e0f20eb65 100644 --- a/lib/src/view/play/time_control_modal.dart +++ b/lib/src/view/play/time_control_modal.dart @@ -12,7 +12,8 @@ import 'package:lichess_mobile/src/widgets/buttons.dart'; class TimeControlModal extends ConsumerWidget { final ValueSetter onSelected; - final void Function(TimeIncrement playerTop, TimeIncrement playerBottom)? onSelectedCustom; + final void Function(TimeIncrement playerTop, TimeIncrement playerBottom)? + onSelectedCustom; final TimeIncrement topPlayer; final TimeIncrement bottomPlayer; final bool excludeUltraBullet; @@ -45,22 +46,27 @@ class TimeControlModal extends ConsumerWidget { ), const SizedBox(height: 26.0), if (onSelectedCustom != null) - FatButton( - semanticsLabel: context.l10n.custom, - onPressed: () => { - pushPlatformRoute( - context, - title: context.l10n.custom, - builder: (_) => CreateCustomTimeControlScreen( - onSubmit: onSelectedCustom!, - topPlayer: topPlayer, - bottomPlayer: bottomPlayer, - ), + ListView( + shrinkWrap: true, + children: [ + FatButton( + semanticsLabel: context.l10n.custom, + onPressed: () => { + pushPlatformRoute( + context, + title: context.l10n.custom, + builder: (_) => CreateCustomTimeControlScreen( + onSubmit: onSelectedCustom!, + topPlayer: topPlayer, + bottomPlayer: bottomPlayer, + ), + ), + }, + child: Text(context.l10n.custom), ), - }, - child: Text(context.l10n.custom), + const SizedBox(height: 20.0), + ], ), - const SizedBox(height: 20.0), _SectionChoices( topPlayer, choices: [ From 03700ae43ed75fb4feece3885600fe463d2a5aa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Nowak?= Date: Wed, 3 Jul 2024 14:32:15 +0200 Subject: [PATCH 004/979] feat: display db size in MB and add button to delete local db --- lib/src/db/database.dart | 55 +++++++-- lib/src/view/settings/settings_screen.dart | 125 ++++++++++----------- 2 files changed, 102 insertions(+), 78 deletions(-) diff --git a/lib/src/db/database.dart b/lib/src/db/database.dart index bfbd957253..20c235e2a2 100644 --- a/lib/src/db/database.dart +++ b/lib/src/db/database.dart @@ -1,4 +1,7 @@ +import 'dart:io'; + import 'package:lichess_mobile/src/app_initialization.dart'; +import 'package:path/path.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:sqflite/sqflite.dart'; @@ -27,19 +30,34 @@ Database database(DatabaseRef ref) { Future sqliteVersion(SqliteVersionRef ref) async { final db = ref.read(databaseProvider); try { - final versionStr = (await db.rawQuery('SELECT sqlite_version()')) - .first - .values - .first - .toString(); - final versionCells = - versionStr.split('.').map((i) => int.parse(i)).toList(); + final versionStr = (await db.rawQuery('SELECT sqlite_version()')).first.values.first.toString(); + final versionCells = versionStr.split('.').map((i) => int.parse(i)).toList(); return versionCells[0] * 100000 + versionCells[1] * 1000 + versionCells[2]; } catch (_) { return null; } } +@Riverpod(keepAlive: true) +Future getDbSizeInBytes(GetDbSizeInBytesRef ref) async { + final dbPath = join(await getDatabasesPath(), kLichessDatabaseName); + final dbFile = File(dbPath); + + return dbFile.length(); +} + +/// Clears all database rows regardless of TTL. +Future clearDatabase(Database db) async { + await Future.wait([ + _deleteEntry(db, 'puzzle_batchs'), + _deleteEntry(db, 'puzzle'), + _deleteEntry(db, 'correspondence_game'), + _deleteEntry(db, 'game'), + _deleteEntry(db, 'chat_read_messages'), + ]); + await db.execute('VACUUM'); +} + Future openDb(DatabaseFactory dbFactory, String path) async { return dbFactory.openDatabase( path, @@ -142,15 +160,30 @@ void _createChatReadMessagesTableV1(Batch batch) { Future _deleteOldEntries(Database db, String table, Duration ttl) async { final date = DateTime.now().subtract(ttl); - final tableExists = await db.rawQuery( - "SELECT name FROM sqlite_master WHERE type='table' AND name='$table'", - ); - if (tableExists.isEmpty) { + + if (!await _doesTableExist(db, table)) { return; } + await db.delete( table, where: 'lastModified < ?', whereArgs: [date.toIso8601String()], ); } + +Future _deleteEntry(Database db, String table) async { + if (!await _doesTableExist(db, table)) { + return; + } + + await db.delete(table); +} + +Future _doesTableExist(Database db, String table) async { + final tableExists = await db.rawQuery( + "SELECT name FROM sqlite_master WHERE type='table' AND name='$table'", + ); + + return tableExists.isNotEmpty; +} diff --git a/lib/src/view/settings/settings_screen.dart b/lib/src/view/settings/settings_screen.dart index 1d556c48f1..d6453732f6 100644 --- a/lib/src/view/settings/settings_screen.dart +++ b/lib/src/view/settings/settings_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/db/database.dart'; import 'package:lichess_mobile/src/model/auth/auth_controller.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; @@ -99,33 +100,29 @@ class _Body extends ConsumerWidget { final boardPrefs = ref.watch(boardPreferencesProvider); final androidVersionAsync = ref.watch(androidVersionProvider); + final dbSize = ref.watch(getDbSizeInBytesProvider); - final Widget? donateButton = - userSession == null || userSession.user.isPatron != true - ? PlatformListTile( - leading: Icon( - LichessIcons.patron, - semanticLabel: context.l10n.patronLichessPatron, - color: context.lichessColors.brag, - ), - title: Text( - context.l10n.patronDonate, - style: TextStyle(color: context.lichessColors.brag), - ), - trailing: Theme.of(context).platform == TargetPlatform.iOS - ? const CupertinoListTileChevron() - : null, - onTap: () { - launchUrl(Uri.parse('https://lichess.org/patron')); - }, - ) - : null; + final Widget? donateButton = userSession == null || userSession.user.isPatron != true + ? PlatformListTile( + leading: Icon( + LichessIcons.patron, + semanticLabel: context.l10n.patronLichessPatron, + color: context.lichessColors.brag, + ), + title: Text( + context.l10n.patronDonate, + style: TextStyle(color: context.lichessColors.brag), + ), + trailing: Theme.of(context).platform == TargetPlatform.iOS ? const CupertinoListTileChevron() : null, + onTap: () { + launchUrl(Uri.parse('https://lichess.org/patron')); + }, + ) + : null; final List content = [ ListSection( - header: userSession != null - ? UserFullNameWidget(user: userSession.user) - : null, + header: userSession != null ? UserFullNameWidget(user: userSession.user) : null, hasLeading: true, showDivider: true, children: [ @@ -133,9 +130,7 @@ class _Body extends ConsumerWidget { PlatformListTile( leading: const Icon(Icons.person), title: Text(context.l10n.profile), - trailing: Theme.of(context).platform == TargetPlatform.iOS - ? const CupertinoListTileChevron() - : null, + trailing: Theme.of(context).platform == TargetPlatform.iOS ? const CupertinoListTileChevron() : null, onTap: () { pushPlatformRoute( context, @@ -147,9 +142,7 @@ class _Body extends ConsumerWidget { PlatformListTile( leading: const Icon(Icons.manage_accounts), title: Text(context.l10n.preferencesPreferences), - trailing: Theme.of(context).platform == TargetPlatform.iOS - ? const CupertinoListTileChevron() - : null, + trailing: Theme.of(context).platform == TargetPlatform.iOS ? const CupertinoListTileChevron() : null, onTap: () { pushPlatformRoute( context, @@ -186,9 +179,7 @@ class _Body extends ConsumerWidget { }, ), ], - if (Theme.of(context).platform == TargetPlatform.android && - donateButton != null) - donateButton, + if (Theme.of(context).platform == TargetPlatform.android && donateButton != null) donateButton, ], ), ListSection( @@ -207,9 +198,7 @@ class _Body extends ConsumerWidget { selectedItem: soundTheme, labelBuilder: (t) => Text(soundThemeL10n(context, t)), onSelectedItemChanged: (SoundTheme? value) { - ref - .read(generalPreferencesProvider.notifier) - .setSoundTheme(value ?? SoundTheme.standard); + ref.read(generalPreferencesProvider.notifier).setSoundTheme(value ?? SoundTheme.standard); ref.read(soundServiceProvider).changeTheme( value ?? SoundTheme.standard, playSound: true, @@ -233,9 +222,7 @@ class _Body extends ConsumerWidget { title: const Text('System colors'), value: hasSystemColors, onChanged: (value) { - ref - .read(generalPreferencesProvider.notifier) - .toggleSystemColors(); + ref.read(generalPreferencesProvider.notifier).toggleSystemColors(); }, ) : const SizedBox.shrink(), @@ -251,11 +238,9 @@ class _Body extends ConsumerWidget { context, choices: ThemeMode.values, selectedItem: themeMode, - labelBuilder: (t) => - Text(ThemeModeScreen.themeTitle(context, t)), - onSelectedItemChanged: (ThemeMode? value) => ref - .read(generalPreferencesProvider.notifier) - .setThemeMode(value ?? ThemeMode.system), + labelBuilder: (t) => Text(ThemeModeScreen.themeTitle(context, t)), + onSelectedItemChanged: (ThemeMode? value) => + ref.read(generalPreferencesProvider.notifier).setThemeMode(value ?? ThemeMode.system), ); } else { pushPlatformRoute( @@ -293,9 +278,7 @@ class _Body extends ConsumerWidget { PlatformListTile( leading: const Icon(Icons.gamepad), title: Text(context.l10n.preferencesGameBehavior), - trailing: Theme.of(context).platform == TargetPlatform.iOS - ? const CupertinoListTileChevron() - : null, + trailing: Theme.of(context).platform == TargetPlatform.iOS ? const CupertinoListTileChevron() : null, onTap: () { pushPlatformRoute( context, @@ -313,9 +296,7 @@ class _Body extends ConsumerWidget { PlatformListTile( leading: const Icon(Icons.info), title: Text(context.l10n.aboutX('Lichess')), - trailing: Theme.of(context).platform == TargetPlatform.iOS - ? const CupertinoListTileChevron() - : null, + trailing: Theme.of(context).platform == TargetPlatform.iOS ? const CupertinoListTileChevron() : null, onTap: () { launchUrl(Uri.parse('https://lichess.org/about')); }, @@ -323,9 +304,7 @@ class _Body extends ConsumerWidget { PlatformListTile( leading: const Icon(Icons.feedback), title: const Text('Feedback'), - trailing: Theme.of(context).platform == TargetPlatform.iOS - ? const CupertinoListTileChevron() - : null, + trailing: Theme.of(context).platform == TargetPlatform.iOS ? const CupertinoListTileChevron() : null, onTap: () { launchUrl(Uri.parse('https://lichess.org/contact')); }, @@ -333,9 +312,7 @@ class _Body extends ConsumerWidget { PlatformListTile( leading: const Icon(Icons.article), title: Text(context.l10n.termsOfService), - trailing: Theme.of(context).platform == TargetPlatform.iOS - ? const CupertinoListTileChevron() - : null, + trailing: Theme.of(context).platform == TargetPlatform.iOS ? const CupertinoListTileChevron() : null, onTap: () { launchUrl(Uri.parse('https://lichess.org/terms-of-service')); }, @@ -343,9 +320,7 @@ class _Body extends ConsumerWidget { PlatformListTile( leading: const Icon(Icons.privacy_tip), title: Text(context.l10n.privacyPolicy), - trailing: Theme.of(context).platform == TargetPlatform.iOS - ? const CupertinoListTileChevron() - : null, + trailing: Theme.of(context).platform == TargetPlatform.iOS ? const CupertinoListTileChevron() : null, onTap: () { launchUrl(Uri.parse('https://lichess.org/privacy')); }, @@ -359,9 +334,7 @@ class _Body extends ConsumerWidget { PlatformListTile( leading: const Icon(Icons.code), title: Text(context.l10n.sourceCode), - trailing: Theme.of(context).platform == TargetPlatform.iOS - ? const CupertinoListTileChevron() - : null, + trailing: Theme.of(context).platform == TargetPlatform.iOS ? const CupertinoListTileChevron() : null, onTap: () { launchUrl(Uri.parse('https://lichess.org/source')); }, @@ -369,9 +342,7 @@ class _Body extends ConsumerWidget { PlatformListTile( leading: const Icon(Icons.bug_report), title: Text(context.l10n.contribute), - trailing: Theme.of(context).platform == TargetPlatform.iOS - ? const CupertinoListTileChevron() - : null, + trailing: Theme.of(context).platform == TargetPlatform.iOS ? const CupertinoListTileChevron() : null, onTap: () { launchUrl(Uri.parse('https://lichess.org/help/contribute')); }, @@ -379,13 +350,25 @@ class _Body extends ConsumerWidget { PlatformListTile( leading: const Icon(Icons.star), title: Text(context.l10n.thankYou), - trailing: Theme.of(context).platform == TargetPlatform.iOS - ? const CupertinoListTileChevron() - : null, + trailing: Theme.of(context).platform == TargetPlatform.iOS ? const CupertinoListTileChevron() : null, onTap: () { launchUrl(Uri.parse('https://lichess.org/thanks')); }, ), + PlatformListTile( + leading: const Icon(Icons.storage), + title: const Text('Delete local database'), + additionalInfo: dbSize.hasValue + ? Text('${_bytesToMB(dbSize.value ?? (0)).toStringAsFixed(2)}MB') + : const SizedBox.shrink(), + trailing: Theme.of(context).platform == TargetPlatform.iOS ? const CupertinoListTileChevron() : null, + onTap: () => showConfirmDialog( + context, + title: const Text('Delete local database'), + onConfirm: (_) => _deleteDatabase(ref), + isDestructiveAction: true, + ), + ), ], ), Padding( @@ -455,4 +438,12 @@ class _Body extends ConsumerWidget { ); } } + + double _bytesToMB(int bytes) => bytes * 0.000001; + + Future _deleteDatabase(WidgetRef ref) async { + final db = ref.read(databaseProvider); + await clearDatabase(db); + ref.invalidate(getDbSizeInBytesProvider); + } } From be94e6fa9a93d0b6c0caf080295f000b36f22e7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Nowak?= Date: Wed, 3 Jul 2024 14:38:46 +0200 Subject: [PATCH 005/979] fix: use correct formatting --- lib/src/db/database.dart | 9 +- lib/src/view/settings/settings_screen.dart | 108 ++++++++++++++------- 2 files changed, 79 insertions(+), 38 deletions(-) diff --git a/lib/src/db/database.dart b/lib/src/db/database.dart index 20c235e2a2..90e42d0781 100644 --- a/lib/src/db/database.dart +++ b/lib/src/db/database.dart @@ -30,8 +30,13 @@ Database database(DatabaseRef ref) { Future sqliteVersion(SqliteVersionRef ref) async { final db = ref.read(databaseProvider); try { - final versionStr = (await db.rawQuery('SELECT sqlite_version()')).first.values.first.toString(); - final versionCells = versionStr.split('.').map((i) => int.parse(i)).toList(); + final versionStr = (await db.rawQuery('SELECT sqlite_version()')) + .first + .values + .first + .toString(); + final versionCells = + versionStr.split('.').map((i) => int.parse(i)).toList(); return versionCells[0] * 100000 + versionCells[1] * 1000 + versionCells[2]; } catch (_) { return null; diff --git a/lib/src/view/settings/settings_screen.dart b/lib/src/view/settings/settings_screen.dart index d6453732f6..9ab696a04a 100644 --- a/lib/src/view/settings/settings_screen.dart +++ b/lib/src/view/settings/settings_screen.dart @@ -102,27 +102,32 @@ class _Body extends ConsumerWidget { final androidVersionAsync = ref.watch(androidVersionProvider); final dbSize = ref.watch(getDbSizeInBytesProvider); - final Widget? donateButton = userSession == null || userSession.user.isPatron != true - ? PlatformListTile( - leading: Icon( - LichessIcons.patron, - semanticLabel: context.l10n.patronLichessPatron, - color: context.lichessColors.brag, - ), - title: Text( - context.l10n.patronDonate, - style: TextStyle(color: context.lichessColors.brag), - ), - trailing: Theme.of(context).platform == TargetPlatform.iOS ? const CupertinoListTileChevron() : null, - onTap: () { - launchUrl(Uri.parse('https://lichess.org/patron')); - }, - ) - : null; + final Widget? donateButton = + userSession == null || userSession.user.isPatron != true + ? PlatformListTile( + leading: Icon( + LichessIcons.patron, + semanticLabel: context.l10n.patronLichessPatron, + color: context.lichessColors.brag, + ), + title: Text( + context.l10n.patronDonate, + style: TextStyle(color: context.lichessColors.brag), + ), + trailing: Theme.of(context).platform == TargetPlatform.iOS + ? const CupertinoListTileChevron() + : null, + onTap: () { + launchUrl(Uri.parse('https://lichess.org/patron')); + }, + ) + : null; final List content = [ ListSection( - header: userSession != null ? UserFullNameWidget(user: userSession.user) : null, + header: userSession != null + ? UserFullNameWidget(user: userSession.user) + : null, hasLeading: true, showDivider: true, children: [ @@ -130,7 +135,9 @@ class _Body extends ConsumerWidget { PlatformListTile( leading: const Icon(Icons.person), title: Text(context.l10n.profile), - trailing: Theme.of(context).platform == TargetPlatform.iOS ? const CupertinoListTileChevron() : null, + trailing: Theme.of(context).platform == TargetPlatform.iOS + ? const CupertinoListTileChevron() + : null, onTap: () { pushPlatformRoute( context, @@ -142,7 +149,9 @@ class _Body extends ConsumerWidget { PlatformListTile( leading: const Icon(Icons.manage_accounts), title: Text(context.l10n.preferencesPreferences), - trailing: Theme.of(context).platform == TargetPlatform.iOS ? const CupertinoListTileChevron() : null, + trailing: Theme.of(context).platform == TargetPlatform.iOS + ? const CupertinoListTileChevron() + : null, onTap: () { pushPlatformRoute( context, @@ -179,7 +188,9 @@ class _Body extends ConsumerWidget { }, ), ], - if (Theme.of(context).platform == TargetPlatform.android && donateButton != null) donateButton, + if (Theme.of(context).platform == TargetPlatform.android && + donateButton != null) + donateButton, ], ), ListSection( @@ -198,7 +209,9 @@ class _Body extends ConsumerWidget { selectedItem: soundTheme, labelBuilder: (t) => Text(soundThemeL10n(context, t)), onSelectedItemChanged: (SoundTheme? value) { - ref.read(generalPreferencesProvider.notifier).setSoundTheme(value ?? SoundTheme.standard); + ref + .read(generalPreferencesProvider.notifier) + .setSoundTheme(value ?? SoundTheme.standard); ref.read(soundServiceProvider).changeTheme( value ?? SoundTheme.standard, playSound: true, @@ -222,7 +235,9 @@ class _Body extends ConsumerWidget { title: const Text('System colors'), value: hasSystemColors, onChanged: (value) { - ref.read(generalPreferencesProvider.notifier).toggleSystemColors(); + ref + .read(generalPreferencesProvider.notifier) + .toggleSystemColors(); }, ) : const SizedBox.shrink(), @@ -238,9 +253,11 @@ class _Body extends ConsumerWidget { context, choices: ThemeMode.values, selectedItem: themeMode, - labelBuilder: (t) => Text(ThemeModeScreen.themeTitle(context, t)), - onSelectedItemChanged: (ThemeMode? value) => - ref.read(generalPreferencesProvider.notifier).setThemeMode(value ?? ThemeMode.system), + labelBuilder: (t) => + Text(ThemeModeScreen.themeTitle(context, t)), + onSelectedItemChanged: (ThemeMode? value) => ref + .read(generalPreferencesProvider.notifier) + .setThemeMode(value ?? ThemeMode.system), ); } else { pushPlatformRoute( @@ -278,7 +295,9 @@ class _Body extends ConsumerWidget { PlatformListTile( leading: const Icon(Icons.gamepad), title: Text(context.l10n.preferencesGameBehavior), - trailing: Theme.of(context).platform == TargetPlatform.iOS ? const CupertinoListTileChevron() : null, + trailing: Theme.of(context).platform == TargetPlatform.iOS + ? const CupertinoListTileChevron() + : null, onTap: () { pushPlatformRoute( context, @@ -296,7 +315,9 @@ class _Body extends ConsumerWidget { PlatformListTile( leading: const Icon(Icons.info), title: Text(context.l10n.aboutX('Lichess')), - trailing: Theme.of(context).platform == TargetPlatform.iOS ? const CupertinoListTileChevron() : null, + trailing: Theme.of(context).platform == TargetPlatform.iOS + ? const CupertinoListTileChevron() + : null, onTap: () { launchUrl(Uri.parse('https://lichess.org/about')); }, @@ -304,7 +325,9 @@ class _Body extends ConsumerWidget { PlatformListTile( leading: const Icon(Icons.feedback), title: const Text('Feedback'), - trailing: Theme.of(context).platform == TargetPlatform.iOS ? const CupertinoListTileChevron() : null, + trailing: Theme.of(context).platform == TargetPlatform.iOS + ? const CupertinoListTileChevron() + : null, onTap: () { launchUrl(Uri.parse('https://lichess.org/contact')); }, @@ -312,7 +335,9 @@ class _Body extends ConsumerWidget { PlatformListTile( leading: const Icon(Icons.article), title: Text(context.l10n.termsOfService), - trailing: Theme.of(context).platform == TargetPlatform.iOS ? const CupertinoListTileChevron() : null, + trailing: Theme.of(context).platform == TargetPlatform.iOS + ? const CupertinoListTileChevron() + : null, onTap: () { launchUrl(Uri.parse('https://lichess.org/terms-of-service')); }, @@ -320,7 +345,9 @@ class _Body extends ConsumerWidget { PlatformListTile( leading: const Icon(Icons.privacy_tip), title: Text(context.l10n.privacyPolicy), - trailing: Theme.of(context).platform == TargetPlatform.iOS ? const CupertinoListTileChevron() : null, + trailing: Theme.of(context).platform == TargetPlatform.iOS + ? const CupertinoListTileChevron() + : null, onTap: () { launchUrl(Uri.parse('https://lichess.org/privacy')); }, @@ -334,7 +361,9 @@ class _Body extends ConsumerWidget { PlatformListTile( leading: const Icon(Icons.code), title: Text(context.l10n.sourceCode), - trailing: Theme.of(context).platform == TargetPlatform.iOS ? const CupertinoListTileChevron() : null, + trailing: Theme.of(context).platform == TargetPlatform.iOS + ? const CupertinoListTileChevron() + : null, onTap: () { launchUrl(Uri.parse('https://lichess.org/source')); }, @@ -342,7 +371,9 @@ class _Body extends ConsumerWidget { PlatformListTile( leading: const Icon(Icons.bug_report), title: Text(context.l10n.contribute), - trailing: Theme.of(context).platform == TargetPlatform.iOS ? const CupertinoListTileChevron() : null, + trailing: Theme.of(context).platform == TargetPlatform.iOS + ? const CupertinoListTileChevron() + : null, onTap: () { launchUrl(Uri.parse('https://lichess.org/help/contribute')); }, @@ -350,7 +381,9 @@ class _Body extends ConsumerWidget { PlatformListTile( leading: const Icon(Icons.star), title: Text(context.l10n.thankYou), - trailing: Theme.of(context).platform == TargetPlatform.iOS ? const CupertinoListTileChevron() : null, + trailing: Theme.of(context).platform == TargetPlatform.iOS + ? const CupertinoListTileChevron() + : null, onTap: () { launchUrl(Uri.parse('https://lichess.org/thanks')); }, @@ -359,9 +392,12 @@ class _Body extends ConsumerWidget { leading: const Icon(Icons.storage), title: const Text('Delete local database'), additionalInfo: dbSize.hasValue - ? Text('${_bytesToMB(dbSize.value ?? (0)).toStringAsFixed(2)}MB') + ? Text( + '${_bytesToMB(dbSize.value ?? (0)).toStringAsFixed(2)}MB') : const SizedBox.shrink(), - trailing: Theme.of(context).platform == TargetPlatform.iOS ? const CupertinoListTileChevron() : null, + trailing: Theme.of(context).platform == TargetPlatform.iOS + ? const CupertinoListTileChevron() + : null, onTap: () => showConfirmDialog( context, title: const Text('Delete local database'), From f3712e052538812546b9dc69191a3d577a92025b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Nowak?= Date: Wed, 3 Jul 2024 14:40:05 +0200 Subject: [PATCH 006/979] fix: use null instead of SizedBox.shrink() when can --- lib/src/view/settings/settings_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/view/settings/settings_screen.dart b/lib/src/view/settings/settings_screen.dart index 9ab696a04a..652de43797 100644 --- a/lib/src/view/settings/settings_screen.dart +++ b/lib/src/view/settings/settings_screen.dart @@ -394,7 +394,7 @@ class _Body extends ConsumerWidget { additionalInfo: dbSize.hasValue ? Text( '${_bytesToMB(dbSize.value ?? (0)).toStringAsFixed(2)}MB') - : const SizedBox.shrink(), + : null, trailing: Theme.of(context).platform == TargetPlatform.iOS ? const CupertinoListTileChevron() : null, From 959b87fad7873506586a405f91cb498570640e6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Nowak?= Date: Wed, 3 Jul 2024 14:49:01 +0200 Subject: [PATCH 007/979] fix: add trailing comma --- lib/src/view/settings/settings_screen.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/view/settings/settings_screen.dart b/lib/src/view/settings/settings_screen.dart index 652de43797..65a5c392ea 100644 --- a/lib/src/view/settings/settings_screen.dart +++ b/lib/src/view/settings/settings_screen.dart @@ -393,7 +393,8 @@ class _Body extends ConsumerWidget { title: const Text('Delete local database'), additionalInfo: dbSize.hasValue ? Text( - '${_bytesToMB(dbSize.value ?? (0)).toStringAsFixed(2)}MB') + '${_bytesToMB(dbSize.value ?? (0)).toStringAsFixed(2)}MB', + ) : null, trailing: Theme.of(context).platform == TargetPlatform.iOS ? const CupertinoListTileChevron() From 6f600f5844de70f4bc540dd215a80394087d7962 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Nowak?= Date: Wed, 3 Jul 2024 15:03:26 +0200 Subject: [PATCH 008/979] feat: add trailing text on android since additionalInfo is displayed only on iOS --- lib/src/view/settings/settings_screen.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/src/view/settings/settings_screen.dart b/lib/src/view/settings/settings_screen.dart index 65a5c392ea..b2944408cb 100644 --- a/lib/src/view/settings/settings_screen.dart +++ b/lib/src/view/settings/settings_screen.dart @@ -398,7 +398,9 @@ class _Body extends ConsumerWidget { : null, trailing: Theme.of(context).platform == TargetPlatform.iOS ? const CupertinoListTileChevron() - : null, + : Text( + '${_bytesToMB(dbSize.value ?? (0)).toStringAsFixed(2)}MB', + ), onTap: () => showConfirmDialog( context, title: const Text('Delete local database'), From 9e59a953119b842c8f74faaad26acd04409cf3f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Nowak?= Date: Wed, 3 Jul 2024 15:08:08 +0200 Subject: [PATCH 009/979] fix: add method to get size (MB) in string --- lib/src/view/settings/settings_screen.dart | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/lib/src/view/settings/settings_screen.dart b/lib/src/view/settings/settings_screen.dart index b2944408cb..68b3f968d8 100644 --- a/lib/src/view/settings/settings_screen.dart +++ b/lib/src/view/settings/settings_screen.dart @@ -391,16 +391,11 @@ class _Body extends ConsumerWidget { PlatformListTile( leading: const Icon(Icons.storage), title: const Text('Delete local database'), - additionalInfo: dbSize.hasValue - ? Text( - '${_bytesToMB(dbSize.value ?? (0)).toStringAsFixed(2)}MB', - ) - : null, + additionalInfo: + dbSize.hasValue ? Text(_getSizeString(dbSize.value)) : null, trailing: Theme.of(context).platform == TargetPlatform.iOS ? const CupertinoListTileChevron() - : Text( - '${_bytesToMB(dbSize.value ?? (0)).toStringAsFixed(2)}MB', - ), + : Text(_getSizeString(dbSize.value)), onTap: () => showConfirmDialog( context, title: const Text('Delete local database'), @@ -478,6 +473,9 @@ class _Body extends ConsumerWidget { } } + String _getSizeString(int? bytes) => + '${_bytesToMB(bytes ?? (0)).toStringAsFixed(2)}MB'; + double _bytesToMB(int bytes) => bytes * 0.000001; Future _deleteDatabase(WidgetRef ref) async { From 1fe7372e29a619418ca6868571f03338e816cec6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Nowak?= Date: Wed, 3 Jul 2024 15:32:23 +0200 Subject: [PATCH 010/979] fix: mock database size in tests --- test/view/settings/settings_screen_test.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/view/settings/settings_screen_test.dart b/test/view/settings/settings_screen_test.dart index 4f63a24d4b..c844a44e2d 100644 --- a/test/view/settings/settings_screen_test.dart +++ b/test/view/settings/settings_screen_test.dart @@ -3,6 +3,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; +import 'package:lichess_mobile/src/db/database.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/view/settings/settings_screen.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; @@ -67,6 +68,7 @@ void main() { overrides: [ lichessClientProvider .overrideWith((ref) => LichessClient(client, ref)), + getDbSizeInBytesProvider.overrideWith((_) => 1000), ], ); From 9f258f9dc378c67ecb3bdf89be86bf0da123b1c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Nowak?= Date: Thu, 4 Jul 2024 12:48:53 +0200 Subject: [PATCH 011/979] feat: add translations to delete local database --- lib/l10n/app_en.arb | 1 + lib/l10n/l10n.dart | 40 +- lib/l10n/l10n_af.dart | 18 + lib/l10n/l10n_ar.dart | 18 + lib/l10n/l10n_az.dart | 18 + lib/l10n/l10n_be.dart | 18 + lib/l10n/l10n_bg.dart | 18 + lib/l10n/l10n_bn.dart | 18 + lib/l10n/l10n_br.dart | 18 + lib/l10n/l10n_bs.dart | 18 + lib/l10n/l10n_ca.dart | 18 + lib/l10n/l10n_cs.dart | 18 + lib/l10n/l10n_da.dart | 18 + lib/l10n/l10n_de.dart | 18 + lib/l10n/l10n_el.dart | 18 + lib/l10n/l10n_en.dart | 18 + lib/l10n/l10n_eo.dart | 18 + lib/l10n/l10n_es.dart | 18 + lib/l10n/l10n_et.dart | 18 + lib/l10n/l10n_eu.dart | 18 + lib/l10n/l10n_fa.dart | 28 +- lib/l10n/l10n_fi.dart | 18 + lib/l10n/l10n_fo.dart | 18 + lib/l10n/l10n_fr.dart | 18 + lib/l10n/l10n_ga.dart | 18 + lib/l10n/l10n_gl.dart | 18 + lib/l10n/l10n_he.dart | 18 + lib/l10n/l10n_hi.dart | 18 + lib/l10n/l10n_hr.dart | 18 + lib/l10n/l10n_hu.dart | 18 + lib/l10n/l10n_hy.dart | 18 + lib/l10n/l10n_id.dart | 18 + lib/l10n/l10n_it.dart | 18 + lib/l10n/l10n_ja.dart | 18 + lib/l10n/l10n_kk.dart | 18 + lib/l10n/l10n_ko.dart | 18 + lib/l10n/l10n_lb.dart | 18 + lib/l10n/l10n_lt.dart | 18 + lib/l10n/l10n_lv.dart | 18 + lib/l10n/l10n_mk.dart | 18 + lib/l10n/l10n_nb.dart | 18 + lib/l10n/l10n_nl.dart | 18 + lib/l10n/l10n_nn.dart | 18 + lib/l10n/l10n_pl.dart | 18 + lib/l10n/l10n_pt.dart | 3564 ++++++++++---------- lib/l10n/l10n_ro.dart | 18 + lib/l10n/l10n_ru.dart | 18 + lib/l10n/l10n_sk.dart | 18 + lib/l10n/l10n_sl.dart | 18 + lib/l10n/l10n_sq.dart | 18 + lib/l10n/l10n_sr.dart | 18 + lib/l10n/l10n_sv.dart | 18 + lib/l10n/l10n_tr.dart | 18 + lib/l10n/l10n_tt.dart | 18 + lib/l10n/l10n_uk.dart | 18 + lib/l10n/l10n_vi.dart | 18 + lib/l10n/l10n_zh.dart | 18 + lib/src/view/settings/settings_screen.dart | 4 +- translation/source/mobile.xml | 1 + 59 files changed, 2810 insertions(+), 1782 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 1207f55484..0b5862665c 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -4,6 +4,7 @@ "mobileToolsTab": "Tools", "mobileWatchTab": "Watch", "mobileSettingsTab": "Settings", + "mobileDeleteLocalDatabase": "Delete local database", "activityActivity": "Activity", "activityHostedALiveStream": "Hosted a live stream", "activityRankedInSwissTournament": "Ranked #{param1} in {param2}", diff --git a/lib/l10n/l10n.dart b/lib/l10n/l10n.dart index d4663c6774..439a991418 100644 --- a/lib/l10n/l10n.dart +++ b/lib/l10n/l10n.dart @@ -186,7 +186,7 @@ abstract class AppLocalizations { Locale('nn'), Locale('pl'), Locale('pt'), - Locale('pt', 'BR'), + Locale('pt', 'PT'), Locale('ro'), Locale('ru'), Locale('sk'), @@ -202,6 +202,42 @@ abstract class AppLocalizations { Locale('zh', 'TW') ]; + /// No description provided for @mobileHomeTab. + /// + /// In en, this message translates to: + /// **'Home'** + String get mobileHomeTab; + + /// No description provided for @mobilePuzzlesTab. + /// + /// In en, this message translates to: + /// **'Puzzles'** + String get mobilePuzzlesTab; + + /// No description provided for @mobileToolsTab. + /// + /// In en, this message translates to: + /// **'Tools'** + String get mobileToolsTab; + + /// No description provided for @mobileWatchTab. + /// + /// In en, this message translates to: + /// **'Watch'** + String get mobileWatchTab; + + /// No description provided for @mobileSettingsTab. + /// + /// In en, this message translates to: + /// **'Settings'** + String get mobileSettingsTab; + + /// No description provided for @mobileDeleteLocalDatabase. + /// + /// In en, this message translates to: + /// **'Delete local database'** + String get mobileDeleteLocalDatabase; + /// No description provided for @activityActivity. /// /// In en, this message translates to: @@ -7850,7 +7886,7 @@ AppLocalizations lookupAppLocalizations(Locale locale) { } case 'pt': { switch (locale.countryCode) { - case 'BR': return AppLocalizationsPtBr(); + case 'PT': return AppLocalizationsPtPt(); } break; } diff --git a/lib/l10n/l10n_af.dart b/lib/l10n/l10n_af.dart index 2d969087c1..4a1a235445 100644 --- a/lib/l10n/l10n_af.dart +++ b/lib/l10n/l10n_af.dart @@ -6,6 +6,24 @@ import 'l10n.dart'; class AppLocalizationsAf extends AppLocalizations { AppLocalizationsAf([String locale = 'af']) : super(locale); + @override + String get mobileHomeTab => 'Tuis'; + + @override + String get mobilePuzzlesTab => 'Kopkrappers'; + + @override + String get mobileToolsTab => 'Hulpmiddels'; + + @override + String get mobileWatchTab => 'Hou dop'; + + @override + String get mobileSettingsTab => 'Instellings'; + + @override + String get mobileDeleteLocalDatabase => 'Delete local database'; + @override String get activityActivity => 'Aktiwiteite'; diff --git a/lib/l10n/l10n_ar.dart b/lib/l10n/l10n_ar.dart index c66571ecc6..0df12f6495 100644 --- a/lib/l10n/l10n_ar.dart +++ b/lib/l10n/l10n_ar.dart @@ -6,6 +6,24 @@ import 'l10n.dart'; class AppLocalizationsAr extends AppLocalizations { AppLocalizationsAr([String locale = 'ar']) : super(locale); + @override + String get mobileHomeTab => 'Home'; + + @override + String get mobilePuzzlesTab => 'Puzzles'; + + @override + String get mobileToolsTab => 'Tools'; + + @override + String get mobileWatchTab => 'Watch'; + + @override + String get mobileSettingsTab => 'Settings'; + + @override + String get mobileDeleteLocalDatabase => 'Delete local database'; + @override String get activityActivity => 'الأنشطة'; diff --git a/lib/l10n/l10n_az.dart b/lib/l10n/l10n_az.dart index d47430ec53..d0ad26338b 100644 --- a/lib/l10n/l10n_az.dart +++ b/lib/l10n/l10n_az.dart @@ -6,6 +6,24 @@ import 'l10n.dart'; class AppLocalizationsAz extends AppLocalizations { AppLocalizationsAz([String locale = 'az']) : super(locale); + @override + String get mobileHomeTab => 'Home'; + + @override + String get mobilePuzzlesTab => 'Puzzles'; + + @override + String get mobileToolsTab => 'Tools'; + + @override + String get mobileWatchTab => 'Watch'; + + @override + String get mobileSettingsTab => 'Settings'; + + @override + String get mobileDeleteLocalDatabase => 'Delete local database'; + @override String get activityActivity => 'Aktivlik'; diff --git a/lib/l10n/l10n_be.dart b/lib/l10n/l10n_be.dart index 796136203d..4fbd6a9112 100644 --- a/lib/l10n/l10n_be.dart +++ b/lib/l10n/l10n_be.dart @@ -6,6 +6,24 @@ import 'l10n.dart'; class AppLocalizationsBe extends AppLocalizations { AppLocalizationsBe([String locale = 'be']) : super(locale); + @override + String get mobileHomeTab => 'Home'; + + @override + String get mobilePuzzlesTab => 'Puzzles'; + + @override + String get mobileToolsTab => 'Tools'; + + @override + String get mobileWatchTab => 'Watch'; + + @override + String get mobileSettingsTab => 'Settings'; + + @override + String get mobileDeleteLocalDatabase => 'Delete local database'; + @override String get activityActivity => 'Актыўнасць'; diff --git a/lib/l10n/l10n_bg.dart b/lib/l10n/l10n_bg.dart index 892dadd159..8a8101a37e 100644 --- a/lib/l10n/l10n_bg.dart +++ b/lib/l10n/l10n_bg.dart @@ -6,6 +6,24 @@ import 'l10n.dart'; class AppLocalizationsBg extends AppLocalizations { AppLocalizationsBg([String locale = 'bg']) : super(locale); + @override + String get mobileHomeTab => 'Начало'; + + @override + String get mobilePuzzlesTab => 'Задачи'; + + @override + String get mobileToolsTab => 'Инструменти'; + + @override + String get mobileWatchTab => 'Гледай'; + + @override + String get mobileSettingsTab => 'Настройки'; + + @override + String get mobileDeleteLocalDatabase => 'Delete local database'; + @override String get activityActivity => 'Дейност'; diff --git a/lib/l10n/l10n_bn.dart b/lib/l10n/l10n_bn.dart index 0b5b3a0a26..37527c8839 100644 --- a/lib/l10n/l10n_bn.dart +++ b/lib/l10n/l10n_bn.dart @@ -6,6 +6,24 @@ import 'l10n.dart'; class AppLocalizationsBn extends AppLocalizations { AppLocalizationsBn([String locale = 'bn']) : super(locale); + @override + String get mobileHomeTab => 'Home'; + + @override + String get mobilePuzzlesTab => 'Puzzles'; + + @override + String get mobileToolsTab => 'Tools'; + + @override + String get mobileWatchTab => 'Watch'; + + @override + String get mobileSettingsTab => 'Settings'; + + @override + String get mobileDeleteLocalDatabase => 'Delete local database'; + @override String get activityActivity => 'কার্যকলাপ'; diff --git a/lib/l10n/l10n_br.dart b/lib/l10n/l10n_br.dart index 57273b3816..7f0ea67630 100644 --- a/lib/l10n/l10n_br.dart +++ b/lib/l10n/l10n_br.dart @@ -6,6 +6,24 @@ import 'l10n.dart'; class AppLocalizationsBr extends AppLocalizations { AppLocalizationsBr([String locale = 'br']) : super(locale); + @override + String get mobileHomeTab => 'Home'; + + @override + String get mobilePuzzlesTab => 'Puzzles'; + + @override + String get mobileToolsTab => 'Tools'; + + @override + String get mobileWatchTab => 'Watch'; + + @override + String get mobileSettingsTab => 'Settings'; + + @override + String get mobileDeleteLocalDatabase => 'Delete local database'; + @override String get activityActivity => 'Obererezhioù diwezhañ'; diff --git a/lib/l10n/l10n_bs.dart b/lib/l10n/l10n_bs.dart index b9060dda04..60143929ad 100644 --- a/lib/l10n/l10n_bs.dart +++ b/lib/l10n/l10n_bs.dart @@ -6,6 +6,24 @@ import 'l10n.dart'; class AppLocalizationsBs extends AppLocalizations { AppLocalizationsBs([String locale = 'bs']) : super(locale); + @override + String get mobileHomeTab => 'Home'; + + @override + String get mobilePuzzlesTab => 'Puzzles'; + + @override + String get mobileToolsTab => 'Tools'; + + @override + String get mobileWatchTab => 'Watch'; + + @override + String get mobileSettingsTab => 'Settings'; + + @override + String get mobileDeleteLocalDatabase => 'Delete local database'; + @override String get activityActivity => 'Aktivnost'; diff --git a/lib/l10n/l10n_ca.dart b/lib/l10n/l10n_ca.dart index 98f95f2ab5..f7fb2af68b 100644 --- a/lib/l10n/l10n_ca.dart +++ b/lib/l10n/l10n_ca.dart @@ -6,6 +6,24 @@ import 'l10n.dart'; class AppLocalizationsCa extends AppLocalizations { AppLocalizationsCa([String locale = 'ca']) : super(locale); + @override + String get mobileHomeTab => 'Inici'; + + @override + String get mobilePuzzlesTab => 'Problemes'; + + @override + String get mobileToolsTab => 'Eines'; + + @override + String get mobileWatchTab => 'Visualitza'; + + @override + String get mobileSettingsTab => 'Configuració'; + + @override + String get mobileDeleteLocalDatabase => 'Delete local database'; + @override String get activityActivity => 'Activitat'; diff --git a/lib/l10n/l10n_cs.dart b/lib/l10n/l10n_cs.dart index a1ea83d7a6..aacbd7e71f 100644 --- a/lib/l10n/l10n_cs.dart +++ b/lib/l10n/l10n_cs.dart @@ -6,6 +6,24 @@ import 'l10n.dart'; class AppLocalizationsCs extends AppLocalizations { AppLocalizationsCs([String locale = 'cs']) : super(locale); + @override + String get mobileHomeTab => 'Home'; + + @override + String get mobilePuzzlesTab => 'Puzzles'; + + @override + String get mobileToolsTab => 'Tools'; + + @override + String get mobileWatchTab => 'Watch'; + + @override + String get mobileSettingsTab => 'Settings'; + + @override + String get mobileDeleteLocalDatabase => 'Delete local database'; + @override String get activityActivity => 'Aktivita'; diff --git a/lib/l10n/l10n_da.dart b/lib/l10n/l10n_da.dart index 199e531603..06a13a7f26 100644 --- a/lib/l10n/l10n_da.dart +++ b/lib/l10n/l10n_da.dart @@ -6,6 +6,24 @@ import 'l10n.dart'; class AppLocalizationsDa extends AppLocalizations { AppLocalizationsDa([String locale = 'da']) : super(locale); + @override + String get mobileHomeTab => 'Hjem'; + + @override + String get mobilePuzzlesTab => 'Opgaver'; + + @override + String get mobileToolsTab => 'Værktøjer'; + + @override + String get mobileWatchTab => 'Se'; + + @override + String get mobileSettingsTab => 'Indstillinger'; + + @override + String get mobileDeleteLocalDatabase => 'Delete local database'; + @override String get activityActivity => 'Aktivitet'; diff --git a/lib/l10n/l10n_de.dart b/lib/l10n/l10n_de.dart index 0eb46b8558..7b5d0929d8 100644 --- a/lib/l10n/l10n_de.dart +++ b/lib/l10n/l10n_de.dart @@ -6,6 +6,24 @@ import 'l10n.dart'; class AppLocalizationsDe extends AppLocalizations { AppLocalizationsDe([String locale = 'de']) : super(locale); + @override + String get mobileHomeTab => 'Startseite'; + + @override + String get mobilePuzzlesTab => 'Aufgaben'; + + @override + String get mobileToolsTab => 'Werkzeuge'; + + @override + String get mobileWatchTab => 'Zuschauen'; + + @override + String get mobileSettingsTab => 'Einstellungen'; + + @override + String get mobileDeleteLocalDatabase => 'Delete local database'; + @override String get activityActivity => 'Verlauf'; diff --git a/lib/l10n/l10n_el.dart b/lib/l10n/l10n_el.dart index bb5b49a2bd..63cb1baa0e 100644 --- a/lib/l10n/l10n_el.dart +++ b/lib/l10n/l10n_el.dart @@ -6,6 +6,24 @@ import 'l10n.dart'; class AppLocalizationsEl extends AppLocalizations { AppLocalizationsEl([String locale = 'el']) : super(locale); + @override + String get mobileHomeTab => 'Home'; + + @override + String get mobilePuzzlesTab => 'Puzzles'; + + @override + String get mobileToolsTab => 'Tools'; + + @override + String get mobileWatchTab => 'Watch'; + + @override + String get mobileSettingsTab => 'Settings'; + + @override + String get mobileDeleteLocalDatabase => 'Delete local database'; + @override String get activityActivity => 'Δραστηριότητα'; diff --git a/lib/l10n/l10n_en.dart b/lib/l10n/l10n_en.dart index 46c133ccb4..65cd63b142 100644 --- a/lib/l10n/l10n_en.dart +++ b/lib/l10n/l10n_en.dart @@ -6,6 +6,24 @@ import 'l10n.dart'; class AppLocalizationsEn extends AppLocalizations { AppLocalizationsEn([String locale = 'en']) : super(locale); + @override + String get mobileHomeTab => 'Home'; + + @override + String get mobilePuzzlesTab => 'Puzzles'; + + @override + String get mobileToolsTab => 'Tools'; + + @override + String get mobileWatchTab => 'Watch'; + + @override + String get mobileSettingsTab => 'Settings'; + + @override + String get mobileDeleteLocalDatabase => 'Delete local database'; + @override String get activityActivity => 'Activity'; diff --git a/lib/l10n/l10n_eo.dart b/lib/l10n/l10n_eo.dart index 1260b33238..d77f4525b8 100644 --- a/lib/l10n/l10n_eo.dart +++ b/lib/l10n/l10n_eo.dart @@ -6,6 +6,24 @@ import 'l10n.dart'; class AppLocalizationsEo extends AppLocalizations { AppLocalizationsEo([String locale = 'eo']) : super(locale); + @override + String get mobileHomeTab => 'Home'; + + @override + String get mobilePuzzlesTab => 'Puzzles'; + + @override + String get mobileToolsTab => 'Tools'; + + @override + String get mobileWatchTab => 'Watch'; + + @override + String get mobileSettingsTab => 'Settings'; + + @override + String get mobileDeleteLocalDatabase => 'Delete local database'; + @override String get activityActivity => 'Aktiveco'; diff --git a/lib/l10n/l10n_es.dart b/lib/l10n/l10n_es.dart index 59885a6fb3..2cac47acc5 100644 --- a/lib/l10n/l10n_es.dart +++ b/lib/l10n/l10n_es.dart @@ -6,6 +6,24 @@ import 'l10n.dart'; class AppLocalizationsEs extends AppLocalizations { AppLocalizationsEs([String locale = 'es']) : super(locale); + @override + String get mobileHomeTab => 'Inicio'; + + @override + String get mobilePuzzlesTab => 'Ejercicios'; + + @override + String get mobileToolsTab => 'Herramientas'; + + @override + String get mobileWatchTab => 'Ver'; + + @override + String get mobileSettingsTab => 'Preferencias'; + + @override + String get mobileDeleteLocalDatabase => 'Delete local database'; + @override String get activityActivity => 'Actividad'; diff --git a/lib/l10n/l10n_et.dart b/lib/l10n/l10n_et.dart index 8e0471668c..53673fa026 100644 --- a/lib/l10n/l10n_et.dart +++ b/lib/l10n/l10n_et.dart @@ -6,6 +6,24 @@ import 'l10n.dart'; class AppLocalizationsEt extends AppLocalizations { AppLocalizationsEt([String locale = 'et']) : super(locale); + @override + String get mobileHomeTab => 'Home'; + + @override + String get mobilePuzzlesTab => 'Puzzles'; + + @override + String get mobileToolsTab => 'Tools'; + + @override + String get mobileWatchTab => 'Watch'; + + @override + String get mobileSettingsTab => 'Settings'; + + @override + String get mobileDeleteLocalDatabase => 'Delete local database'; + @override String get activityActivity => 'Aktiivsus'; diff --git a/lib/l10n/l10n_eu.dart b/lib/l10n/l10n_eu.dart index f59619ca09..64a0e65260 100644 --- a/lib/l10n/l10n_eu.dart +++ b/lib/l10n/l10n_eu.dart @@ -6,6 +6,24 @@ import 'l10n.dart'; class AppLocalizationsEu extends AppLocalizations { AppLocalizationsEu([String locale = 'eu']) : super(locale); + @override + String get mobileHomeTab => 'Home'; + + @override + String get mobilePuzzlesTab => 'Puzzles'; + + @override + String get mobileToolsTab => 'Tools'; + + @override + String get mobileWatchTab => 'Watch'; + + @override + String get mobileSettingsTab => 'Settings'; + + @override + String get mobileDeleteLocalDatabase => 'Delete local database'; + @override String get activityActivity => 'Jarduera'; diff --git a/lib/l10n/l10n_fa.dart b/lib/l10n/l10n_fa.dart index d9265f3490..2476878fed 100644 --- a/lib/l10n/l10n_fa.dart +++ b/lib/l10n/l10n_fa.dart @@ -6,6 +6,24 @@ import 'l10n.dart'; class AppLocalizationsFa extends AppLocalizations { AppLocalizationsFa([String locale = 'fa']) : super(locale); + @override + String get mobileHomeTab => 'خانه'; + + @override + String get mobilePuzzlesTab => 'معماها'; + + @override + String get mobileToolsTab => 'ابزارها'; + + @override + String get mobileWatchTab => 'تماشا'; + + @override + String get mobileSettingsTab => 'تنظیمات'; + + @override + String get mobileDeleteLocalDatabase => 'Delete local database'; + @override String get activityActivity => 'فعالیت'; @@ -2900,7 +2918,7 @@ class AppLocalizationsFa extends AppLocalizations { String get streamersMenu => 'بَرخَط-محتواسازها'; @override - String get mobileApp => 'برنامه ی موبایل'; + String get mobileApp => 'گوشی‌افزار'; @override String get webmasters => 'وبداران'; @@ -3238,7 +3256,7 @@ class AppLocalizationsFa extends AppLocalizations { String get playChessEverywhere => 'همه جا شطرنج بازی کنید'; @override - String get asFreeAsLichess => 'رایگان به مانند لیچس'; + String get asFreeAsLichess => 'کاملا رایگان'; @override String get builtForTheLoveOfChessNotMoney => 'ساخته شده با عشق به شطرنج نه پول'; @@ -3262,7 +3280,7 @@ class AppLocalizationsFa extends AppLocalizations { String get correspondenceChess => 'شطرنج مکاتبه ای'; @override - String get onlineAndOfflinePlay => 'بازی کردن بَرخط و بُرون‌خط'; + String get onlineAndOfflinePlay => 'بازی بَرخط و بُرون‌خط'; @override String get viewTheSolution => 'دیدن راهِ حل'; @@ -4353,8 +4371,8 @@ class AppLocalizationsFa extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'در $count زبان‌ها موجود است', - one: 'در $count زبان‌ها موجود است', + other: 'در $count زبان موجود است!', + one: 'در $count زبان موجود است!', ); return '$_temp0'; } diff --git a/lib/l10n/l10n_fi.dart b/lib/l10n/l10n_fi.dart index 8a300124c6..a07b1368e6 100644 --- a/lib/l10n/l10n_fi.dart +++ b/lib/l10n/l10n_fi.dart @@ -6,6 +6,24 @@ import 'l10n.dart'; class AppLocalizationsFi extends AppLocalizations { AppLocalizationsFi([String locale = 'fi']) : super(locale); + @override + String get mobileHomeTab => 'Home'; + + @override + String get mobilePuzzlesTab => 'Puzzles'; + + @override + String get mobileToolsTab => 'Tools'; + + @override + String get mobileWatchTab => 'Watch'; + + @override + String get mobileSettingsTab => 'Settings'; + + @override + String get mobileDeleteLocalDatabase => 'Delete local database'; + @override String get activityActivity => 'Toiminta'; diff --git a/lib/l10n/l10n_fo.dart b/lib/l10n/l10n_fo.dart index 3fc29bf8ec..109d898f68 100644 --- a/lib/l10n/l10n_fo.dart +++ b/lib/l10n/l10n_fo.dart @@ -6,6 +6,24 @@ import 'l10n.dart'; class AppLocalizationsFo extends AppLocalizations { AppLocalizationsFo([String locale = 'fo']) : super(locale); + @override + String get mobileHomeTab => 'Home'; + + @override + String get mobilePuzzlesTab => 'Puzzles'; + + @override + String get mobileToolsTab => 'Tools'; + + @override + String get mobileWatchTab => 'Watch'; + + @override + String get mobileSettingsTab => 'Settings'; + + @override + String get mobileDeleteLocalDatabase => 'Delete local database'; + @override String get activityActivity => 'Virkni'; diff --git a/lib/l10n/l10n_fr.dart b/lib/l10n/l10n_fr.dart index 1ea3f06c9f..17f90b4ed3 100644 --- a/lib/l10n/l10n_fr.dart +++ b/lib/l10n/l10n_fr.dart @@ -6,6 +6,24 @@ import 'l10n.dart'; class AppLocalizationsFr extends AppLocalizations { AppLocalizationsFr([String locale = 'fr']) : super(locale); + @override + String get mobileHomeTab => 'Accueil'; + + @override + String get mobilePuzzlesTab => 'Problèmes'; + + @override + String get mobileToolsTab => 'Outils'; + + @override + String get mobileWatchTab => 'Regarder'; + + @override + String get mobileSettingsTab => 'Paramètres'; + + @override + String get mobileDeleteLocalDatabase => 'Delete local database'; + @override String get activityActivity => 'Activité'; diff --git a/lib/l10n/l10n_ga.dart b/lib/l10n/l10n_ga.dart index d921fd5ee8..96d6d22226 100644 --- a/lib/l10n/l10n_ga.dart +++ b/lib/l10n/l10n_ga.dart @@ -6,6 +6,24 @@ import 'l10n.dart'; class AppLocalizationsGa extends AppLocalizations { AppLocalizationsGa([String locale = 'ga']) : super(locale); + @override + String get mobileHomeTab => 'Home'; + + @override + String get mobilePuzzlesTab => 'Puzzles'; + + @override + String get mobileToolsTab => 'Tools'; + + @override + String get mobileWatchTab => 'Watch'; + + @override + String get mobileSettingsTab => 'Settings'; + + @override + String get mobileDeleteLocalDatabase => 'Delete local database'; + @override String get activityActivity => 'Gníomhaíocht'; diff --git a/lib/l10n/l10n_gl.dart b/lib/l10n/l10n_gl.dart index 281fe27957..8bdf3b4c1e 100644 --- a/lib/l10n/l10n_gl.dart +++ b/lib/l10n/l10n_gl.dart @@ -6,6 +6,24 @@ import 'l10n.dart'; class AppLocalizationsGl extends AppLocalizations { AppLocalizationsGl([String locale = 'gl']) : super(locale); + @override + String get mobileHomeTab => 'Inicio'; + + @override + String get mobilePuzzlesTab => 'Crebacabezas'; + + @override + String get mobileToolsTab => 'Ferramentas'; + + @override + String get mobileWatchTab => 'Ver'; + + @override + String get mobileSettingsTab => 'Axustes'; + + @override + String get mobileDeleteLocalDatabase => 'Delete local database'; + @override String get activityActivity => 'Actividade'; diff --git a/lib/l10n/l10n_he.dart b/lib/l10n/l10n_he.dart index 4d014e38da..050156d2ca 100644 --- a/lib/l10n/l10n_he.dart +++ b/lib/l10n/l10n_he.dart @@ -6,6 +6,24 @@ import 'l10n.dart'; class AppLocalizationsHe extends AppLocalizations { AppLocalizationsHe([String locale = 'he']) : super(locale); + @override + String get mobileHomeTab => 'בית'; + + @override + String get mobilePuzzlesTab => 'חידות'; + + @override + String get mobileToolsTab => 'כלים'; + + @override + String get mobileWatchTab => 'צפייה'; + + @override + String get mobileSettingsTab => 'הגדרות'; + + @override + String get mobileDeleteLocalDatabase => 'Delete local database'; + @override String get activityActivity => 'פעילות'; diff --git a/lib/l10n/l10n_hi.dart b/lib/l10n/l10n_hi.dart index e3082c7091..3ecf89bfe3 100644 --- a/lib/l10n/l10n_hi.dart +++ b/lib/l10n/l10n_hi.dart @@ -6,6 +6,24 @@ import 'l10n.dart'; class AppLocalizationsHi extends AppLocalizations { AppLocalizationsHi([String locale = 'hi']) : super(locale); + @override + String get mobileHomeTab => 'Home'; + + @override + String get mobilePuzzlesTab => 'Puzzles'; + + @override + String get mobileToolsTab => 'Tools'; + + @override + String get mobileWatchTab => 'Watch'; + + @override + String get mobileSettingsTab => 'Settings'; + + @override + String get mobileDeleteLocalDatabase => 'Delete local database'; + @override String get activityActivity => 'कार्यकलाप'; diff --git a/lib/l10n/l10n_hr.dart b/lib/l10n/l10n_hr.dart index e0bff3ebd3..db5dfb0b15 100644 --- a/lib/l10n/l10n_hr.dart +++ b/lib/l10n/l10n_hr.dart @@ -6,6 +6,24 @@ import 'l10n.dart'; class AppLocalizationsHr extends AppLocalizations { AppLocalizationsHr([String locale = 'hr']) : super(locale); + @override + String get mobileHomeTab => 'Home'; + + @override + String get mobilePuzzlesTab => 'Puzzles'; + + @override + String get mobileToolsTab => 'Tools'; + + @override + String get mobileWatchTab => 'Watch'; + + @override + String get mobileSettingsTab => 'Settings'; + + @override + String get mobileDeleteLocalDatabase => 'Delete local database'; + @override String get activityActivity => 'Aktivnost'; diff --git a/lib/l10n/l10n_hu.dart b/lib/l10n/l10n_hu.dart index e02ed54fc6..3d8c190111 100644 --- a/lib/l10n/l10n_hu.dart +++ b/lib/l10n/l10n_hu.dart @@ -6,6 +6,24 @@ import 'l10n.dart'; class AppLocalizationsHu extends AppLocalizations { AppLocalizationsHu([String locale = 'hu']) : super(locale); + @override + String get mobileHomeTab => 'Home'; + + @override + String get mobilePuzzlesTab => 'Puzzles'; + + @override + String get mobileToolsTab => 'Tools'; + + @override + String get mobileWatchTab => 'Watch'; + + @override + String get mobileSettingsTab => 'Settings'; + + @override + String get mobileDeleteLocalDatabase => 'Delete local database'; + @override String get activityActivity => 'Aktivitás'; diff --git a/lib/l10n/l10n_hy.dart b/lib/l10n/l10n_hy.dart index 5a501de7b8..16d5df9a42 100644 --- a/lib/l10n/l10n_hy.dart +++ b/lib/l10n/l10n_hy.dart @@ -6,6 +6,24 @@ import 'l10n.dart'; class AppLocalizationsHy extends AppLocalizations { AppLocalizationsHy([String locale = 'hy']) : super(locale); + @override + String get mobileHomeTab => 'Home'; + + @override + String get mobilePuzzlesTab => 'Puzzles'; + + @override + String get mobileToolsTab => 'Tools'; + + @override + String get mobileWatchTab => 'Watch'; + + @override + String get mobileSettingsTab => 'Settings'; + + @override + String get mobileDeleteLocalDatabase => 'Delete local database'; + @override String get activityActivity => 'Գործունեություն'; diff --git a/lib/l10n/l10n_id.dart b/lib/l10n/l10n_id.dart index b0c2c4391b..686bc5c838 100644 --- a/lib/l10n/l10n_id.dart +++ b/lib/l10n/l10n_id.dart @@ -6,6 +6,24 @@ import 'l10n.dart'; class AppLocalizationsId extends AppLocalizations { AppLocalizationsId([String locale = 'id']) : super(locale); + @override + String get mobileHomeTab => 'Home'; + + @override + String get mobilePuzzlesTab => 'Puzzles'; + + @override + String get mobileToolsTab => 'Tools'; + + @override + String get mobileWatchTab => 'Watch'; + + @override + String get mobileSettingsTab => 'Settings'; + + @override + String get mobileDeleteLocalDatabase => 'Delete local database'; + @override String get activityActivity => 'Aktivitas'; diff --git a/lib/l10n/l10n_it.dart b/lib/l10n/l10n_it.dart index 0992736853..cc69b2c4ff 100644 --- a/lib/l10n/l10n_it.dart +++ b/lib/l10n/l10n_it.dart @@ -6,6 +6,24 @@ import 'l10n.dart'; class AppLocalizationsIt extends AppLocalizations { AppLocalizationsIt([String locale = 'it']) : super(locale); + @override + String get mobileHomeTab => 'Home'; + + @override + String get mobilePuzzlesTab => 'Puzzles'; + + @override + String get mobileToolsTab => 'Tools'; + + @override + String get mobileWatchTab => 'Watch'; + + @override + String get mobileSettingsTab => 'Settings'; + + @override + String get mobileDeleteLocalDatabase => 'Delete local database'; + @override String get activityActivity => 'Attività'; diff --git a/lib/l10n/l10n_ja.dart b/lib/l10n/l10n_ja.dart index bf1673ab43..5c90cc5573 100644 --- a/lib/l10n/l10n_ja.dart +++ b/lib/l10n/l10n_ja.dart @@ -6,6 +6,24 @@ import 'l10n.dart'; class AppLocalizationsJa extends AppLocalizations { AppLocalizationsJa([String locale = 'ja']) : super(locale); + @override + String get mobileHomeTab => 'Home'; + + @override + String get mobilePuzzlesTab => 'Puzzles'; + + @override + String get mobileToolsTab => 'Tools'; + + @override + String get mobileWatchTab => 'Watch'; + + @override + String get mobileSettingsTab => 'Settings'; + + @override + String get mobileDeleteLocalDatabase => 'Delete local database'; + @override String get activityActivity => '活動'; diff --git a/lib/l10n/l10n_kk.dart b/lib/l10n/l10n_kk.dart index a5a1901e11..09d095a3d4 100644 --- a/lib/l10n/l10n_kk.dart +++ b/lib/l10n/l10n_kk.dart @@ -6,6 +6,24 @@ import 'l10n.dart'; class AppLocalizationsKk extends AppLocalizations { AppLocalizationsKk([String locale = 'kk']) : super(locale); + @override + String get mobileHomeTab => 'Home'; + + @override + String get mobilePuzzlesTab => 'Puzzles'; + + @override + String get mobileToolsTab => 'Tools'; + + @override + String get mobileWatchTab => 'Watch'; + + @override + String get mobileSettingsTab => 'Settings'; + + @override + String get mobileDeleteLocalDatabase => 'Delete local database'; + @override String get activityActivity => 'Белсенділігі'; diff --git a/lib/l10n/l10n_ko.dart b/lib/l10n/l10n_ko.dart index 2fa95bba2c..e32897f122 100644 --- a/lib/l10n/l10n_ko.dart +++ b/lib/l10n/l10n_ko.dart @@ -6,6 +6,24 @@ import 'l10n.dart'; class AppLocalizationsKo extends AppLocalizations { AppLocalizationsKo([String locale = 'ko']) : super(locale); + @override + String get mobileHomeTab => 'Home'; + + @override + String get mobilePuzzlesTab => 'Puzzles'; + + @override + String get mobileToolsTab => 'Tools'; + + @override + String get mobileWatchTab => 'Watch'; + + @override + String get mobileSettingsTab => 'Settings'; + + @override + String get mobileDeleteLocalDatabase => 'Delete local database'; + @override String get activityActivity => '활동'; diff --git a/lib/l10n/l10n_lb.dart b/lib/l10n/l10n_lb.dart index 7b08ab8538..a9345c1a62 100644 --- a/lib/l10n/l10n_lb.dart +++ b/lib/l10n/l10n_lb.dart @@ -6,6 +6,24 @@ import 'l10n.dart'; class AppLocalizationsLb extends AppLocalizations { AppLocalizationsLb([String locale = 'lb']) : super(locale); + @override + String get mobileHomeTab => 'Home'; + + @override + String get mobilePuzzlesTab => 'Puzzles'; + + @override + String get mobileToolsTab => 'Tools'; + + @override + String get mobileWatchTab => 'Watch'; + + @override + String get mobileSettingsTab => 'Settings'; + + @override + String get mobileDeleteLocalDatabase => 'Delete local database'; + @override String get activityActivity => 'Verlaf'; diff --git a/lib/l10n/l10n_lt.dart b/lib/l10n/l10n_lt.dart index 91301e21ac..2037f4c0b4 100644 --- a/lib/l10n/l10n_lt.dart +++ b/lib/l10n/l10n_lt.dart @@ -6,6 +6,24 @@ import 'l10n.dart'; class AppLocalizationsLt extends AppLocalizations { AppLocalizationsLt([String locale = 'lt']) : super(locale); + @override + String get mobileHomeTab => 'Home'; + + @override + String get mobilePuzzlesTab => 'Puzzles'; + + @override + String get mobileToolsTab => 'Tools'; + + @override + String get mobileWatchTab => 'Watch'; + + @override + String get mobileSettingsTab => 'Settings'; + + @override + String get mobileDeleteLocalDatabase => 'Delete local database'; + @override String get activityActivity => 'Veikla'; diff --git a/lib/l10n/l10n_lv.dart b/lib/l10n/l10n_lv.dart index 1b7d1c8f25..a0915afab5 100644 --- a/lib/l10n/l10n_lv.dart +++ b/lib/l10n/l10n_lv.dart @@ -6,6 +6,24 @@ import 'l10n.dart'; class AppLocalizationsLv extends AppLocalizations { AppLocalizationsLv([String locale = 'lv']) : super(locale); + @override + String get mobileHomeTab => 'Home'; + + @override + String get mobilePuzzlesTab => 'Puzzles'; + + @override + String get mobileToolsTab => 'Tools'; + + @override + String get mobileWatchTab => 'Watch'; + + @override + String get mobileSettingsTab => 'Settings'; + + @override + String get mobileDeleteLocalDatabase => 'Delete local database'; + @override String get activityActivity => 'Aktivitāte'; diff --git a/lib/l10n/l10n_mk.dart b/lib/l10n/l10n_mk.dart index cf98bd684e..c0ba4d36c1 100644 --- a/lib/l10n/l10n_mk.dart +++ b/lib/l10n/l10n_mk.dart @@ -6,6 +6,24 @@ import 'l10n.dart'; class AppLocalizationsMk extends AppLocalizations { AppLocalizationsMk([String locale = 'mk']) : super(locale); + @override + String get mobileHomeTab => 'Home'; + + @override + String get mobilePuzzlesTab => 'Puzzles'; + + @override + String get mobileToolsTab => 'Tools'; + + @override + String get mobileWatchTab => 'Watch'; + + @override + String get mobileSettingsTab => 'Settings'; + + @override + String get mobileDeleteLocalDatabase => 'Delete local database'; + @override String get activityActivity => 'Активност'; diff --git a/lib/l10n/l10n_nb.dart b/lib/l10n/l10n_nb.dart index 4a34912c59..2628344d2d 100644 --- a/lib/l10n/l10n_nb.dart +++ b/lib/l10n/l10n_nb.dart @@ -6,6 +6,24 @@ import 'l10n.dart'; class AppLocalizationsNb extends AppLocalizations { AppLocalizationsNb([String locale = 'nb']) : super(locale); + @override + String get mobileHomeTab => 'Home'; + + @override + String get mobilePuzzlesTab => 'Puzzles'; + + @override + String get mobileToolsTab => 'Tools'; + + @override + String get mobileWatchTab => 'Watch'; + + @override + String get mobileSettingsTab => 'Settings'; + + @override + String get mobileDeleteLocalDatabase => 'Delete local database'; + @override String get activityActivity => 'Aktivitet'; diff --git a/lib/l10n/l10n_nl.dart b/lib/l10n/l10n_nl.dart index beb6ed59e5..b240a9d029 100644 --- a/lib/l10n/l10n_nl.dart +++ b/lib/l10n/l10n_nl.dart @@ -6,6 +6,24 @@ import 'l10n.dart'; class AppLocalizationsNl extends AppLocalizations { AppLocalizationsNl([String locale = 'nl']) : super(locale); + @override + String get mobileHomeTab => 'Startscherm'; + + @override + String get mobilePuzzlesTab => 'Puzzels'; + + @override + String get mobileToolsTab => 'Gereedschap'; + + @override + String get mobileWatchTab => 'Kijken'; + + @override + String get mobileSettingsTab => 'Instellingen'; + + @override + String get mobileDeleteLocalDatabase => 'Delete local database'; + @override String get activityActivity => 'Activiteit'; diff --git a/lib/l10n/l10n_nn.dart b/lib/l10n/l10n_nn.dart index ed5e705d45..ba1fdcbef1 100644 --- a/lib/l10n/l10n_nn.dart +++ b/lib/l10n/l10n_nn.dart @@ -6,6 +6,24 @@ import 'l10n.dart'; class AppLocalizationsNn extends AppLocalizations { AppLocalizationsNn([String locale = 'nn']) : super(locale); + @override + String get mobileHomeTab => 'Startside'; + + @override + String get mobilePuzzlesTab => 'Oppgåver'; + + @override + String get mobileToolsTab => 'Verktøy'; + + @override + String get mobileWatchTab => 'Sjå'; + + @override + String get mobileSettingsTab => 'Innstillingar'; + + @override + String get mobileDeleteLocalDatabase => 'Delete local database'; + @override String get activityActivity => 'Aktivitet'; diff --git a/lib/l10n/l10n_pl.dart b/lib/l10n/l10n_pl.dart index eed4b7510a..e25a5e0f15 100644 --- a/lib/l10n/l10n_pl.dart +++ b/lib/l10n/l10n_pl.dart @@ -6,6 +6,24 @@ import 'l10n.dart'; class AppLocalizationsPl extends AppLocalizations { AppLocalizationsPl([String locale = 'pl']) : super(locale); + @override + String get mobileHomeTab => 'Home'; + + @override + String get mobilePuzzlesTab => 'Puzzles'; + + @override + String get mobileToolsTab => 'Tools'; + + @override + String get mobileWatchTab => 'Watch'; + + @override + String get mobileSettingsTab => 'Settings'; + + @override + String get mobileDeleteLocalDatabase => 'Delete local database'; + @override String get activityActivity => 'Aktywność'; diff --git a/lib/l10n/l10n_pt.dart b/lib/l10n/l10n_pt.dart index 84228f7a1e..e12d8010a8 100644 --- a/lib/l10n/l10n_pt.dart +++ b/lib/l10n/l10n_pt.dart @@ -6,27 +6,45 @@ import 'l10n.dart'; class AppLocalizationsPt extends AppLocalizations { AppLocalizationsPt([String locale = 'pt']) : super(locale); + @override + String get mobileHomeTab => 'Início'; + + @override + String get mobilePuzzlesTab => 'Problemas'; + + @override + String get mobileToolsTab => 'Ferramentas'; + + @override + String get mobileWatchTab => 'Assistir'; + + @override + String get mobileSettingsTab => 'Configurações'; + + @override + String get mobileDeleteLocalDatabase => 'Delete local database'; + @override String get activityActivity => 'Atividade'; @override - String get activityHostedALiveStream => 'Criou uma livestream'; + String get activityHostedALiveStream => 'Iniciou uma transmissão ao vivo'; @override String activityRankedInSwissTournament(String param1, String param2) { - return 'Classificado #$param1 em $param2'; + return 'Classificado #$param1 entre $param2'; } @override - String get activitySignedUp => 'Registou-se no lichess.org'; + String get activitySignedUp => 'Registrou-se no lichess'; @override String activitySupportedNbMonths(int count, String param2) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Apoiou o lichess.org durante $count meses como $param2', - one: 'Apoiou o lichess.org durante $count mês como $param2', + other: 'Contribuiu para o lichess.org por $count meses como $param2', + one: 'Contribuiu para o lichess.org por $count mês como $param2', ); return '$_temp0'; } @@ -47,8 +65,8 @@ class AppLocalizationsPt extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Resolveu $count problemas', - one: 'Resolveu $count problema', + other: 'Resolveu $count quebra-cabeças táticos', + one: 'Resolveu $count quebra-cabeça tático', ); return '$_temp0'; } @@ -58,8 +76,8 @@ class AppLocalizationsPt extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Jogou $count jogos de $param2', - one: 'Jogou $count jogo de $param2', + other: 'Jogou $count partidas de $param2', + one: 'Jogou $count partida de $param2', ); return '$_temp0'; } @@ -80,8 +98,8 @@ class AppLocalizationsPt extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Fez $count jogadas', - one: 'Fez $count jogada', + other: 'Jogou $count movimentos', + one: 'Jogou $count movimento', ); return '$_temp0'; } @@ -135,8 +153,8 @@ class AppLocalizationsPt extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Criou $count exibições simultâneas', - one: 'Criou $count exibição simultânea', + other: 'Hospedou $count exibições simultâneas', + one: 'Hospedou $count exibição simultânea', ); return '$_temp0'; } @@ -168,8 +186,8 @@ class AppLocalizationsPt extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Competiu em $count torneios', - one: 'Competiu em $count torneio', + other: 'Competiu em $count torneios arena', + one: 'Competiu em $count torneio arena', ); return '$_temp0'; } @@ -179,8 +197,8 @@ class AppLocalizationsPt extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Qualificado #$count (nos $param2% melhores) com $param3 jogos em $param4', - one: 'Qualificado #$count (nos $param2% melhores) com $param3 jogo em $param4', + other: 'Classificado #$count (top $param2%) com $param3 jogos em $param4', + one: 'Classificado #$count (top $param2%) com $param3 jogo em $param4', ); return '$_temp0'; } @@ -201,8 +219,8 @@ class AppLocalizationsPt extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Entrou em $count equipas', - one: 'Entrou em $count equipa', + other: 'Entrou nas $count equipes', + one: 'Entrou na $count equipe', ); return '$_temp0'; } @@ -211,7 +229,7 @@ class AppLocalizationsPt extends AppLocalizations { String get broadcastBroadcasts => 'Transmissões'; @override - String get broadcastStartDate => 'Data de início no teu fuso horário'; + String get broadcastStartDate => 'Data de início em seu próprio fuso horário'; @override String challengeChallengesX(String param1) { @@ -225,17 +243,17 @@ class AppLocalizationsPt extends AppLocalizations { String get challengeChallengeDeclined => 'Desafio recusado'; @override - String get challengeChallengeAccepted => 'Desafio aceite!'; + String get challengeChallengeAccepted => 'Desafio aceito!'; @override String get challengeChallengeCanceled => 'Desafio cancelado.'; @override - String get challengeRegisterToSendChallenges => 'Por favor regista-te para enviar desafios.'; + String get challengeRegisterToSendChallenges => 'Por favor, registre-se para enviar desafios.'; @override String challengeYouCannotChallengeX(String param) { - return 'Não podes desafiar $param.'; + return 'Você não pode desafiar $param.'; } @override @@ -245,12 +263,12 @@ class AppLocalizationsPt extends AppLocalizations { @override String challengeYourXRatingIsTooFarFromY(String param1, String param2) { - return 'O teu ranking $param1 esta muito distante de $param2.'; + return 'O seu rating $param1 é muito diferente de $param2.'; } @override String challengeCannotChallengeDueToProvisionalXRating(String param) { - return 'Não podes desafiar devido a ranking provisório $param.'; + return 'Não pode desafiar devido ao rating provisório de $param.'; } @override @@ -259,52 +277,52 @@ class AppLocalizationsPt extends AppLocalizations { } @override - String get challengeDeclineGeneric => 'Não estou a aceitar desafios no momento.'; + String get challengeDeclineGeneric => 'Não estou aceitando desafios no momento.'; @override String get challengeDeclineLater => 'Este não é o momento certo para mim, por favor pergunte novamente mais tarde.'; @override - String get challengeDeclineTooFast => 'Este controlo de tempo é muito rápido para mim, por favor, desafie-me novamente com um jogo mais lento.'; + String get challengeDeclineTooFast => 'Este controle de tempo é muito rápido para mim, por favor, desafie novamente com um jogo mais lento.'; @override - String get challengeDeclineTooSlow => 'Este controlo de tempo é muito lento para mim, por favor, desafie-me novamente com um jogo mais rápido.'; + String get challengeDeclineTooSlow => 'Este controle de tempo é muito lento para mim, por favor, desafie novamente com um jogo mais rápido.'; @override - String get challengeDeclineTimeControl => 'Não estou a aceitar desafios com este controlo de tempo.'; + String get challengeDeclineTimeControl => 'Não estou aceitando desafios com estes controles de tempo.'; @override - String get challengeDeclineRated => 'Por favor, envie-me um desafio a valer para a classificação.'; + String get challengeDeclineRated => 'Por favor, envie-me um desafio ranqueado.'; @override String get challengeDeclineCasual => 'Por favor, envie-me um desafio amigável.'; @override - String get challengeDeclineStandard => 'Não estou a aceitar desafios de variante, de momento.'; + String get challengeDeclineStandard => 'Não estou aceitando desafios de variantes no momento.'; @override - String get challengeDeclineVariant => 'Não estou disposto a jogar essa variante, de momento.'; + String get challengeDeclineVariant => 'Não estou a fim de jogar esta variante no momento.'; @override - String get challengeDeclineNoBot => 'Não estou a aceitar desafios de bots.'; + String get challengeDeclineNoBot => 'Não estou aceitando desafios de robôs.'; @override - String get challengeDeclineOnlyBot => 'Apenas aceito desafios de bots.'; + String get challengeDeclineOnlyBot => 'Estou aceitando apenas desafios de robôs.'; @override - String get challengeInviteLichessUser => 'Ou convide um utilizador Lichess:'; + String get challengeInviteLichessUser => 'Ou convide um usuário Lichess:'; @override - String get contactContact => 'Contacto'; + String get contactContact => 'Contato'; @override - String get contactContactLichess => 'Contactar o Lichess'; + String get contactContactLichess => 'Entrar em contato com Lichess'; @override - String get patronDonate => 'Doar'; + String get patronDonate => 'Doação'; @override - String get patronLichessPatron => 'Patrono do Lichess'; + String get patronLichessPatron => 'Apoie o Lichess'; @override String perfStatPerfStats(String param) { @@ -312,46 +330,46 @@ class AppLocalizationsPt extends AppLocalizations { } @override - String get perfStatViewTheGames => 'Ver as partidas'; + String get perfStatViewTheGames => 'Ver os jogos'; @override String get perfStatProvisional => 'provisório'; @override - String get perfStatNotEnoughRatedGames => 'Não foi jogado um número suficiente de partidas a pontuar para estabelecer uma pontuação de confiança.'; + String get perfStatNotEnoughRatedGames => 'Não foram jogadas partidas suficientes valendo rating para estabelecer uma classificação confiável.'; @override String perfStatProgressOverLastXGames(String param) { - return 'Progresso nas últimas $param partidas:'; + return 'Progresso nos últimos $param jogos:'; } @override String perfStatRatingDeviation(String param) { - return 'Desvio da pontuação: $param.'; + return 'Desvio de pontuação: $param.'; } @override String perfStatRatingDeviationTooltip(String param1, String param2, String param3) { - return 'Um valor inferior significa que a classificação é mais estável. Acima de $param1, a classificação é considerada provisória. Para ser incluído nas classificações, esse valor deve estar abaixo de $param2 (xadrez padrão) ou $param3 (variantes).'; + return 'Um valor inferior indica que a pontuação é mais estável. Superior a $param1, a pontuação é classificada como provisória. Para ser incluída nas classificações, esse valor deve ser inferior a $param2 (xadrez padrão) ou $param3 (variantes).'; } @override String get perfStatTotalGames => 'Total de partidas'; @override - String get perfStatRatedGames => 'Total de partidas a pontuar'; + String get perfStatRatedGames => 'Partidas valendo pontos'; @override - String get perfStatTournamentGames => 'Partidas em torneios'; + String get perfStatTournamentGames => 'Jogos de torneio'; @override - String get perfStatBerserkedGames => 'Partidas no modo frenético'; + String get perfStatBerserkedGames => 'Partidas Berserked'; @override - String get perfStatTimeSpentPlaying => 'Tempo passado a jogar'; + String get perfStatTimeSpentPlaying => 'Tempo jogando'; @override - String get perfStatAverageOpponent => 'Pontuação média dos adversários'; + String get perfStatAverageOpponent => 'Pontuação média do adversário'; @override String get perfStatVictories => 'Vitórias'; @@ -363,7 +381,7 @@ class AppLocalizationsPt extends AppLocalizations { String get perfStatDisconnections => 'Desconexões'; @override - String get perfStatNotEnoughGames => 'Não foram jogadas partidas suficientes'; + String get perfStatNotEnoughGames => 'Jogos insuficientes jogados'; @override String perfStatHighestRating(String param) { @@ -372,19 +390,19 @@ class AppLocalizationsPt extends AppLocalizations { @override String perfStatLowestRating(String param) { - return 'Pontuação mais baixa: $param'; + return 'Rating mais baixo: $param'; } @override String perfStatFromXToY(String param1, String param2) { - return 'de $param1 a $param2'; + return 'de $param1 para $param2'; } @override - String get perfStatWinningStreak => 'Vitórias consecutivas'; + String get perfStatWinningStreak => 'Série de Vitórias'; @override - String get perfStatLosingStreak => 'Derrotas consecutivas'; + String get perfStatLosingStreak => 'Série de derrotas'; @override String perfStatLongestStreak(String param) { @@ -397,16 +415,16 @@ class AppLocalizationsPt extends AppLocalizations { } @override - String get perfStatBestRated => 'Melhores vitórias a pontuar'; + String get perfStatBestRated => 'Melhores vitórias valendo pontuação'; @override - String get perfStatGamesInARow => 'Partidas jogadas de seguida'; + String get perfStatGamesInARow => 'Partidas jogadas seguidas'; @override String get perfStatLessThanOneHour => 'Menos de uma hora entre partidas'; @override - String get perfStatMaxTimePlaying => 'Tempo máximo passado a jogar'; + String get perfStatMaxTimePlaying => 'Tempo máximo jogando'; @override String get perfStatNow => 'agora'; @@ -415,7 +433,7 @@ class AppLocalizationsPt extends AppLocalizations { String get preferencesPreferences => 'Preferências'; @override - String get preferencesDisplay => 'Mostrar'; + String get preferencesDisplay => 'Exibição'; @override String get preferencesPrivacy => 'Privacidade'; @@ -427,58 +445,58 @@ class AppLocalizationsPt extends AppLocalizations { String get preferencesPieceAnimation => 'Animação das peças'; @override - String get preferencesMaterialDifference => 'Diferença de material'; + String get preferencesMaterialDifference => 'Diferença material'; @override - String get preferencesBoardHighlights => 'Destacar as casas do tabuleiro (último movimento e xeque)'; + String get preferencesBoardHighlights => 'Destacar casas do tabuleiro (último movimento e xeque)'; @override - String get preferencesPieceDestinations => 'Destino das peças (movimentos válidos e antecipados)'; + String get preferencesPieceDestinations => 'Destino das peças (movimentos válidos e pré-movimentos)'; @override String get preferencesBoardCoordinates => 'Coordenadas do tabuleiro (A-H, 1-8)'; @override - String get preferencesMoveListWhilePlaying => 'Lista de movimentos'; + String get preferencesMoveListWhilePlaying => 'Lista de movimentos durante a partida'; @override - String get preferencesPgnPieceNotation => 'Anotação de movimentos'; + String get preferencesPgnPieceNotation => 'Modo de notação das jogadas'; @override - String get preferencesChessPieceSymbol => 'Usar símbolo das peças'; + String get preferencesChessPieceSymbol => 'Símbolo da peça'; @override - String get preferencesPgnLetter => 'Usar letras (K, Q, R, B, N)'; + String get preferencesPgnLetter => 'Letra (K, Q, R, B, N)'; @override - String get preferencesZenMode => 'Modo zen'; + String get preferencesZenMode => 'Modo Zen'; @override - String get preferencesShowPlayerRatings => 'Mostrar classificações dos jogadores'; + String get preferencesShowPlayerRatings => 'Mostrar rating dos jogadores'; @override - String get preferencesShowFlairs => 'Mostrar os estilos do jogadores'; + String get preferencesShowFlairs => 'Mostrar emotes de usuário'; @override - String get preferencesExplainShowPlayerRatings => 'Isto permite ocultar todas as avaliações do site, para o ajudar a concentrar-se no xadrez. Os jogos continuam a poder ser avaliados, trata-se apenas do que poderá ver.'; + String get preferencesExplainShowPlayerRatings => 'Permite ocultar todas os ratings do site, para ajudar a se concentrar no jogo. As partidas continuam valendo rating.'; @override - String get preferencesDisplayBoardResizeHandle => 'Mostrar o cursor de redimensionamento do tabuleiro'; + String get preferencesDisplayBoardResizeHandle => 'Mostrar cursor de redimensionamento do tabuleiro'; @override String get preferencesOnlyOnInitialPosition => 'Apenas na posição inicial'; @override - String get preferencesInGameOnly => 'Apenas em Jogo'; + String get preferencesInGameOnly => 'Durante partidas'; @override - String get preferencesChessClock => 'Relógio de xadrez'; + String get preferencesChessClock => 'Relógio'; @override String get preferencesTenthsOfSeconds => 'Décimos de segundo'; @override - String get preferencesWhenTimeRemainingLessThanTenSeconds => 'Quando o tempo restante for < 10 segundos'; + String get preferencesWhenTimeRemainingLessThanTenSeconds => 'Quando o tempo restante < 10 segundos'; @override String get preferencesHorizontalGreenProgressBars => 'Barras de progresso verdes horizontais'; @@ -490,25 +508,25 @@ class AppLocalizationsPt extends AppLocalizations { String get preferencesGiveMoreTime => 'Dar mais tempo'; @override - String get preferencesGameBehavior => 'Funcionamento do jogo'; + String get preferencesGameBehavior => 'Comportamento do jogo'; @override - String get preferencesHowDoYouMovePieces => 'Como queres mexer as peças?'; + String get preferencesHowDoYouMovePieces => 'Como você move as peças?'; @override String get preferencesClickTwoSquares => 'Clicar em duas casas'; @override - String get preferencesDragPiece => 'Arrastar uma peça'; + String get preferencesDragPiece => 'Arrastar a peça'; @override - String get preferencesBothClicksAndDrag => 'Qualquer'; + String get preferencesBothClicksAndDrag => 'Ambas'; @override - String get preferencesPremovesPlayingDuringOpponentTurn => 'Jogadas antecipadas (jogadas durante a vez do adversário)'; + String get preferencesPremovesPlayingDuringOpponentTurn => 'Pré-movimentos (jogadas durante o turno do oponente)'; @override - String get preferencesTakebacksWithOpponentApproval => 'Voltar jogadas atrás (com aprovação do adversário)'; + String get preferencesTakebacksWithOpponentApproval => 'Voltar jogada (com aprovação do oponente)'; @override String get preferencesInCasualGamesOnly => 'Somente em jogos casuais'; @@ -520,58 +538,58 @@ class AppLocalizationsPt extends AppLocalizations { String get preferencesExplainPromoteToQueenAutomatically => 'Mantenha a tecla pressionada enquanto promove para desativar temporariamente a autopromoção'; @override - String get preferencesWhenPremoving => 'Quando mover antecipadamente'; + String get preferencesWhenPremoving => 'Quando pré-mover'; @override - String get preferencesClaimDrawOnThreefoldRepetitionAutomatically => 'Reivindicar empate automaticamente após uma repetição tripla'; + String get preferencesClaimDrawOnThreefoldRepetitionAutomatically => 'Reivindicar empate sobre a repetição tripla automaticamente'; @override - String get preferencesWhenTimeRemainingLessThanThirtySeconds => 'Quando o tempo restante for < 30 segundos'; + String get preferencesWhenTimeRemainingLessThanThirtySeconds => 'Quando o tempo restante < 30 segundos'; @override String get preferencesMoveConfirmation => 'Confirmação de movimento'; @override - String get preferencesExplainCanThenBeTemporarilyDisabled => 'Pode ser desativado durante um jogo com o menu do tabuleiro'; + String get preferencesExplainCanThenBeTemporarilyDisabled => 'Pode ser desativado durante a partida no menu do tabuleiro'; @override String get preferencesInCorrespondenceGames => 'Jogos por correspondência'; @override - String get preferencesCorrespondenceAndUnlimited => 'Por correspondência e ilimitado'; + String get preferencesCorrespondenceAndUnlimited => 'Por correspondência e sem limites'; @override - String get preferencesConfirmResignationAndDrawOffers => 'Confirmar desistências e propostas de empate'; + String get preferencesConfirmResignationAndDrawOffers => 'Confirmar abandono e oferta de empate'; @override - String get preferencesCastleByMovingTheKingTwoSquaresOrOntoTheRook => 'Método de roque'; + String get preferencesCastleByMovingTheKingTwoSquaresOrOntoTheRook => 'Maneira de rocar'; @override String get preferencesCastleByMovingTwoSquares => 'Mover o rei duas casas'; @override - String get preferencesCastleByMovingOntoTheRook => 'Mover o rei até à torre'; + String get preferencesCastleByMovingOntoTheRook => 'Mover o rei em direção à torre'; @override - String get preferencesInputMovesWithTheKeyboard => 'Introduzir movimentos com o teclado'; + String get preferencesInputMovesWithTheKeyboard => 'Fazer lances com escrita do teclado'; @override - String get preferencesInputMovesWithVoice => 'Insira movimentos com a sua voz'; + String get preferencesInputMovesWithVoice => 'Mova as peças com sua voz'; @override - String get preferencesSnapArrowsToValidMoves => 'Alinhar as setas para sítios para onde as peças se podem mover'; + String get preferencesSnapArrowsToValidMoves => 'Insira setas para movimentos válidos'; @override - String get preferencesSayGgWpAfterLosingOrDrawing => 'Dizer \"Good game, well played\" (Bom jogo, bem jogado) após uma derrota ou empate'; + String get preferencesSayGgWpAfterLosingOrDrawing => 'Diga \"Bom jogo, bem jogado\" após a derrota ou empate'; @override - String get preferencesYourPreferencesHaveBeenSaved => 'As tuas preferências foram guardadas.'; + String get preferencesYourPreferencesHaveBeenSaved => 'Suas preferências foram salvas.'; @override - String get preferencesScrollOnTheBoardToReplayMoves => 'Rolar no tabuleiro para repetir os movimentos'; + String get preferencesScrollOnTheBoardToReplayMoves => 'Use o scroll do mouse no tabuleiro para ir passando as jogadas'; @override - String get preferencesCorrespondenceEmailNotification => 'Notificações diárias por email listando seus jogos por correspondência'; + String get preferencesCorrespondenceEmailNotification => 'Email diário listando seus jogos por correspondência'; @override String get preferencesNotifyStreamStart => 'Streamer começou uma transmissão ao vivo'; @@ -580,28 +598,28 @@ class AppLocalizationsPt extends AppLocalizations { String get preferencesNotifyInboxMsg => 'Nova mensagem na caixa de entrada'; @override - String get preferencesNotifyForumMention => 'Um comentário do fórum menciona-o'; + String get preferencesNotifyForumMention => 'Você foi mencionado em um comentário do fórum'; @override - String get preferencesNotifyInvitedStudy => 'Convite para estudo'; + String get preferencesNotifyInvitedStudy => 'Convite para um estudo'; @override - String get preferencesNotifyGameEvent => 'Atualizações dos jogos por correspondência'; + String get preferencesNotifyGameEvent => 'Jogo por correspondência atualizado'; @override String get preferencesNotifyChallenge => 'Desafios'; @override - String get preferencesNotifyTournamentSoon => 'O torneio começará em breve'; + String get preferencesNotifyTournamentSoon => 'O torneio vai começar em breve'; @override - String get preferencesNotifyTimeAlarm => 'Está a acabar o tempo no jogo por correspondência'; + String get preferencesNotifyTimeAlarm => 'Está acabando o tempo no jogo por correspondência'; @override - String get preferencesNotifyBell => 'Notificação do sino no Lichess'; + String get preferencesNotifyBell => 'Notificação no Lichess'; @override - String get preferencesNotifyPush => 'Notificação do dispositivo quando não está no Lichess'; + String get preferencesNotifyPush => 'Notificação no dispositivo fora do Lichess'; @override String get preferencesNotifyWeb => 'Navegador'; @@ -613,10 +631,10 @@ class AppLocalizationsPt extends AppLocalizations { String get preferencesBellNotificationSound => 'Som da notificação'; @override - String get puzzlePuzzles => 'Problemas'; + String get puzzlePuzzles => 'Quebra-cabeças'; @override - String get puzzlePuzzleThemes => 'Temas de problemas'; + String get puzzlePuzzleThemes => 'Temas de quebra-cabeça'; @override String get puzzleRecommended => 'Recomendado'; @@ -625,13 +643,13 @@ class AppLocalizationsPt extends AppLocalizations { String get puzzlePhases => 'Fases'; @override - String get puzzleMotifs => 'Temas'; + String get puzzleMotifs => 'Motivos táticos'; @override String get puzzleAdvanced => 'Avançado'; @override - String get puzzleLengths => 'Comprimentos'; + String get puzzleLengths => 'Distância'; @override String get puzzleMates => 'Xeque-mates'; @@ -646,42 +664,42 @@ class AppLocalizationsPt extends AppLocalizations { String get puzzleSpecialMoves => 'Movimentos especiais'; @override - String get puzzleDidYouLikeThisPuzzle => 'Gostaste deste problema?'; + String get puzzleDidYouLikeThisPuzzle => 'Você gostou deste quebra-cabeças?'; @override - String get puzzleVoteToLoadNextOne => 'Vota para carregares o próximo!'; + String get puzzleVoteToLoadNextOne => 'Vote para carregar o próximo!'; @override - String get puzzleUpVote => 'Aprove o puzzle'; + String get puzzleUpVote => 'Votar a favor do quebra-cabeça'; @override - String get puzzleDownVote => 'Desaprove o puzzle'; + String get puzzleDownVote => 'Votar contra o quebra-cabeça'; @override - String get puzzleYourPuzzleRatingWillNotChange => 'A tua classificação de problemas não será alterada. Nota que os problemas não são uma competição. A classificação ajuda a selecionar os melhores problemas para o teu nível atual.'; + String get puzzleYourPuzzleRatingWillNotChange => 'Sua pontuação de quebra-cabeças não mudará. Note que os quebra-cabeças não são uma competição. A pontuação indica os quebra-cabeças que se adequam às suas habilidades.'; @override - String get puzzleFindTheBestMoveForWhite => 'Encontra a melhor jogada para as brancas.'; + String get puzzleFindTheBestMoveForWhite => 'Encontre o melhor lance para as brancas.'; @override - String get puzzleFindTheBestMoveForBlack => 'Encontra a melhor jogada para as pretas.'; + String get puzzleFindTheBestMoveForBlack => 'Encontre a melhor jogada para as pretas.'; @override String get puzzleToGetPersonalizedPuzzles => 'Para obter desafios personalizados:'; @override String puzzlePuzzleId(String param) { - return 'Problema $param'; + return 'Quebra-cabeça $param'; } @override - String get puzzlePuzzleOfTheDay => 'Problema do dia'; + String get puzzlePuzzleOfTheDay => 'Quebra-cabeça do dia'; @override - String get puzzleDailyPuzzle => 'Problema diário'; + String get puzzleDailyPuzzle => 'Quebra-cabeça diário'; @override - String get puzzleClickToSolve => 'Clica para resolveres'; + String get puzzleClickToSolve => 'Clique para resolver'; @override String get puzzleGoodMove => 'Boa jogada'; @@ -690,42 +708,42 @@ class AppLocalizationsPt extends AppLocalizations { String get puzzleBestMove => 'Melhor jogada!'; @override - String get puzzleKeepGoing => 'Continua…'; + String get puzzleKeepGoing => 'Continue…'; @override String get puzzlePuzzleSuccess => 'Sucesso!'; @override - String get puzzlePuzzleComplete => 'Problema resolvido!'; + String get puzzlePuzzleComplete => 'Quebra-cabeças concluído!'; @override String get puzzleByOpenings => 'Por abertura'; @override - String get puzzlePuzzlesByOpenings => 'Problemas por abertura'; + String get puzzlePuzzlesByOpenings => 'Quebra-cabeças por abertura'; @override - String get puzzleOpeningsYouPlayedTheMost => 'Aberturas que jogou mais vezes em partidas com rating'; + String get puzzleOpeningsYouPlayedTheMost => 'Aberturas que você mais jogou em partidas valendo pontos'; @override - String get puzzleUseFindInPage => 'Usar \"Localizar na página\" no menu do navegador para encontrar a sua abertura favorita!'; + String get puzzleUseFindInPage => 'Use a ferramenta \"Encontrar na página\" do navegador para encontrar sua abertura favorita!'; @override - String get puzzleUseCtrlF => 'Usar Ctrl+f para encontrar a sua abertura favorita!'; + String get puzzleUseCtrlF => 'Aperte Ctrl + F para encontrar sua abertura favorita!'; @override - String get puzzleNotTheMove => 'Não é esse movimento!'; + String get puzzleNotTheMove => 'O movimento não é este!'; @override - String get puzzleTrySomethingElse => 'Tenta outra coisa.'; + String get puzzleTrySomethingElse => 'Tente algo diferente.'; @override String puzzleRatingX(String param) { - return 'Pontuação: $param'; + return 'Rating: $param'; } @override - String get puzzleHidden => 'oculta'; + String get puzzleHidden => 'oculto'; @override String puzzleFromGameLink(String param) { @@ -733,7 +751,7 @@ class AppLocalizationsPt extends AppLocalizations { } @override - String get puzzleContinueTraining => 'Continuar o treino'; + String get puzzleContinueTraining => 'Continue treinando'; @override String get puzzleDifficultyLevel => 'Nível de dificuldade'; @@ -742,88 +760,88 @@ class AppLocalizationsPt extends AppLocalizations { String get puzzleNormal => 'Normal'; @override - String get puzzleEasier => 'Mais fáceis'; + String get puzzleEasier => 'Fácil'; @override - String get puzzleEasiest => 'Mais fáceis'; + String get puzzleEasiest => 'Muito fácil'; @override - String get puzzleHarder => 'Mais difíceis'; + String get puzzleHarder => 'Difícil'; @override - String get puzzleHardest => 'Mais difíceis'; + String get puzzleHardest => 'Muito difícil'; @override String get puzzleExample => 'Exemplo'; @override - String get puzzleAddAnotherTheme => 'Adicionar outro tema'; + String get puzzleAddAnotherTheme => 'Adicionar um outro tema'; @override - String get puzzleNextPuzzle => 'Próximo desafio'; + String get puzzleNextPuzzle => 'Próximo quebra-cabeça'; @override - String get puzzleJumpToNextPuzzleImmediately => 'Saltar imediatamente para o próximo problema'; + String get puzzleJumpToNextPuzzleImmediately => 'Ir para o próximo problema automaticamente'; @override - String get puzzlePuzzleDashboard => 'Painel de controlo dos problemas'; + String get puzzlePuzzleDashboard => 'Painel do quebra-cabeças'; @override - String get puzzleImprovementAreas => 'Áreas a melhorar'; + String get puzzleImprovementAreas => 'Áreas de aprimoramento'; @override String get puzzleStrengths => 'Pontos fortes'; @override - String get puzzleHistory => 'Histórico de problemas'; + String get puzzleHistory => 'Histórico de quebra-cabeças'; @override String get puzzleSolved => 'resolvido'; @override - String get puzzleFailed => 'incorreto'; + String get puzzleFailed => 'falhou'; @override - String get puzzleStreakDescription => 'Resolve puzzles progressivamente mais difíceis e estabelece uma sequência de vitórias. Não há relógio, demora o teu tempo. Um movimento errado e o jogo acaba! No entanto, podes saltar um movimento por sessão.'; + String get puzzleStreakDescription => 'Resolva quebra-cabeças progressivamente mais difíceis e construa uma sequência de vitórias. Não há relógio, então tome seu tempo. Um movimento errado e o jogo acaba! Porém, você pode pular um movimento por sessão.'; @override String puzzleYourStreakX(String param) { - return 'Vitórias consecutivas: $param'; + return 'Sua sequência: $param'; } @override - String get puzzleStreakSkipExplanation => 'Salta este movimento para preservar a tua sequência! Apenas funciona uma vez por sessão.'; + String get puzzleStreakSkipExplanation => 'Pule este lance para preservar a sua sequência! Funciona apenas uma vez por corrida.'; @override - String get puzzleContinueTheStreak => 'Continua a sequência'; + String get puzzleContinueTheStreak => 'Continuar a sequência'; @override - String get puzzleNewStreak => 'Nova sequência de vitórias'; + String get puzzleNewStreak => 'Nova sequência'; @override String get puzzleFromMyGames => 'Dos meus jogos'; @override - String get puzzleLookupOfPlayer => 'Pesquise problemas de jogos de um jogador'; + String get puzzleLookupOfPlayer => 'Pesquise quebra-cabeças de um jogador específico'; @override String puzzleFromXGames(String param) { - return 'Puzzles dos jogos de $param'; + return 'Problemas de $param\' jogos'; } @override - String get puzzleSearchPuzzles => 'Pesquisar desafios'; + String get puzzleSearchPuzzles => 'Procurar quebra-cabeças'; @override - String get puzzleFromMyGamesNone => 'Não tens problemas na base de dados, mas Lichess adora-te muito.\n\nJoga partidas semi-rápidas e clássicas para aumentares a probabilidade de teres um problema adicionado!'; + String get puzzleFromMyGamesNone => 'Você não tem nenhum quebra-cabeça no banco de dados, mas o Lichess ainda te ama muito.\nJogue partidas rápidas e clássicas para aumentar suas chances de ter um desafio seu adicionado!'; @override String puzzleFromXGamesFound(String param1, String param2) { - return '$param1 problemas encontrados em $param2 partidas'; + return '$param1 quebra-cabeças encontrados em $param2 partidas'; } @override - String get puzzlePuzzleDashboardDescription => 'Treinar, analisar, melhorar'; + String get puzzlePuzzleDashboardDescription => 'Treine, analise, melhore'; @override String puzzlePercentSolved(String param) { @@ -831,13 +849,13 @@ class AppLocalizationsPt extends AppLocalizations { } @override - String get puzzleNoPuzzlesToShow => 'Nada para mostrar, joga alguns problemas primeiro!'; + String get puzzleNoPuzzlesToShow => 'Não há nada para mostrar aqui, jogue alguns quebra-cabeças primeiro!'; @override String get puzzleImprovementAreasDescription => 'Treine estes para otimizar o seu progresso!'; @override - String get puzzleStrengthDescription => 'Você tem melhor desempenho nestes temas'; + String get puzzleStrengthDescription => 'Sua perfomance é melhor nesses temas'; @override String puzzlePlayedXTimes(int count) { @@ -845,7 +863,7 @@ class AppLocalizationsPt extends AppLocalizations { count, locale: localeName, other: 'Jogado $count vezes', - one: 'Jogado $count vez', + one: 'Jogado $count vezes', ); return '$_temp0'; } @@ -855,8 +873,8 @@ class AppLocalizationsPt extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count pontos abaixo da sua pontuação de problemas', - one: 'Um ponto abaixo da sua pontuação de problemas', + other: '$count pontos abaixo da sua classificação de quebra-cabeças', + one: 'Um ponto abaixo da sua classificação de quebra-cabeças', ); return '$_temp0'; } @@ -866,8 +884,8 @@ class AppLocalizationsPt extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count pontos acima da sua pontuação de problemas', - one: 'Um ponto acima da sua pontuação de problemas', + other: '$count pontos acima da sua classificação de quebra-cabeças', + one: 'Um ponto acima da sua classificação de quebra-cabeças', ); return '$_temp0'; } @@ -877,8 +895,8 @@ class AppLocalizationsPt extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count problemas feitos', - one: '$count problema feito', + other: '$count jogados', + one: '$count jogado', ); return '$_temp0'; } @@ -888,8 +906,8 @@ class AppLocalizationsPt extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count para repetir', - one: '$count para repetir', + other: '$count a serem repetidos', + one: '$count a ser repetido', ); return '$_temp0'; } @@ -898,136 +916,136 @@ class AppLocalizationsPt extends AppLocalizations { String get puzzleThemeAdvancedPawn => 'Peão avançado'; @override - String get puzzleThemeAdvancedPawnDescription => 'A chave do tático é um peão a promover ou a ameaçar promover.'; + String get puzzleThemeAdvancedPawnDescription => 'Um peão prestes a ser promovido ou à beira da promoção é um tema tático.'; @override String get puzzleThemeAdvantage => 'Vantagem'; @override - String get puzzleThemeAdvantageDescription => 'Aproveita a oportunidade de obter uma vantagem decisiva. (200cp ≤ aval ≤ 600cp)'; + String get puzzleThemeAdvantageDescription => 'Aproveite a sua chance de ter uma vantagem decisiva. (200cp ≤ eval ≤ 600cp)'; @override String get puzzleThemeAnastasiaMate => 'Mate Anastasia'; @override - String get puzzleThemeAnastasiaMateDescription => 'Um cavalo e uma torre ou dama cooperam para prender o rei inimigo entre um lado do tabuleiro e outra peça inimiga.'; + String get puzzleThemeAnastasiaMateDescription => 'Um cavalo e uma torre se unem para prender o rei do oponente entre a lateral do tabuleiro e uma peça amiga.'; @override - String get puzzleThemeArabianMate => 'Mate Árabe'; + String get puzzleThemeArabianMate => 'Mate árabe'; @override - String get puzzleThemeArabianMateDescription => 'Um cavalo e uma torre cooperam para prenderem o rei inimigo no canto do tabuleiro.'; + String get puzzleThemeArabianMateDescription => 'Um cavalo e uma torre se unem para prender o rei inimigo em um canto do tabuleiro.'; @override - String get puzzleThemeAttackingF2F7 => 'Atacar f2 ou f7'; + String get puzzleThemeAttackingF2F7 => 'Atacando f2 ou f7'; @override - String get puzzleThemeAttackingF2F7Description => 'Um ataque ao peão de f2 ou f7, como a abertura \"Fried Liver\".'; + String get puzzleThemeAttackingF2F7Description => 'Um ataque focado no peão de f2 e no peão de f7, como na abertura frango frito.'; @override String get puzzleThemeAttraction => 'Atração'; @override - String get puzzleThemeAttractionDescription => 'Uma troca ou sacrifício que encoraja ou força uma peça adversária a ir para uma casa que permite um tático.'; + String get puzzleThemeAttractionDescription => 'Uma troca ou sacrifício encorajando ou forçando uma peça do oponente a uma casa que permite uma sequência tática.'; @override - String get puzzleThemeBackRankMate => 'Mate de corredor'; + String get puzzleThemeBackRankMate => 'Mate do corredor'; @override - String get puzzleThemeBackRankMateDescription => 'Dá mate ao rei na fila inicial, quando está preso pelas suas próprias peças.'; + String get puzzleThemeBackRankMateDescription => 'Dê o xeque-mate no rei na última fileira, quando ele estiver bloqueado pelas próprias peças.'; @override - String get puzzleThemeBishopEndgame => 'Final de bispos'; + String get puzzleThemeBishopEndgame => 'Finais de bispo'; @override - String get puzzleThemeBishopEndgameDescription => 'Um final apenas com bispos e peões.'; + String get puzzleThemeBishopEndgameDescription => 'Final com somente bispos e peões.'; @override - String get puzzleThemeBodenMate => 'Mate Boden'; + String get puzzleThemeBodenMate => 'Mate de Boden'; @override - String get puzzleThemeBodenMateDescription => 'Dois bispos em diagonais perpendiculares dão mate ao rei inimigo obstruído por peças aliadas.'; + String get puzzleThemeBodenMateDescription => 'Dois bispos atacantes em diagonais cruzadas dão um mate em um rei obstruído por peças amigas.'; @override String get puzzleThemeCastling => 'Roque'; @override - String get puzzleThemeCastlingDescription => 'Proteger o rei e trazer a torre para o ataque.'; + String get puzzleThemeCastlingDescription => 'Traga o seu rei para a segurança, e prepare sua torre para o ataque.'; @override - String get puzzleThemeCapturingDefender => 'Capturar o defensor'; + String get puzzleThemeCapturingDefender => 'Capture o defensor'; @override - String get puzzleThemeCapturingDefenderDescription => 'Remove uma peça que seja crítica para a defesa de outra peça, permitindo que esta seja capturada na próxima jogada.'; + String get puzzleThemeCapturingDefenderDescription => 'Remover uma peça que seja importante na defesa de outra, permitindo que agora a peça indefesa seja capturada na jogada seguinte.'; @override - String get puzzleThemeCrushing => 'Esmagar'; + String get puzzleThemeCrushing => 'Punindo'; @override - String get puzzleThemeCrushingDescription => 'Descobre um erro grave do oponente e obtém uma vantagem esmagadora. (avaliação ≥ 600cp)'; + String get puzzleThemeCrushingDescription => 'Perceba a capivarada do oponente para obter uma vantagem decisiva. (vantagem ≥ 600cp)'; @override - String get puzzleThemeDoubleBishopMate => 'Mate com dois bispos'; + String get puzzleThemeDoubleBishopMate => 'Mate de dois bispos'; @override - String get puzzleThemeDoubleBishopMateDescription => 'Dois bispos em diagonais adjacentes dão mate ao rei inimigo obstruído por peças aliadas.'; + String get puzzleThemeDoubleBishopMateDescription => 'Dois bispos atacantes em diagonais adjacentes dão um mate em um rei obstruído por peças amigas.'; @override - String get puzzleThemeDovetailMate => 'Mate cauda-de-andorinha'; + String get puzzleThemeDovetailMate => 'Mate da cauda de andorinha'; @override - String get puzzleThemeDovetailMateDescription => 'Uma dama dá mate ao rei inimigo cujas jogadas de escape estão bloqueadas por peças aliadas.'; + String get puzzleThemeDovetailMateDescription => 'Uma dama dá um mate em um rei adjacente, cujos únicos dois quadrados de fuga estão obstruídos por peças amigas.'; @override String get puzzleThemeEquality => 'Igualdade'; @override - String get puzzleThemeEqualityDescription => 'Recupera de uma posição perdedora e garante um empate ou uma posição de equilíbrio. (avaliação ≤ 200cp)'; + String get puzzleThemeEqualityDescription => 'Saia de uma posição perdida, e assegure um empate ou uma posição equilibrada. (aval ≤ 200cp)'; @override - String get puzzleThemeKingsideAttack => 'Ataque no lado do rei'; + String get puzzleThemeKingsideAttack => 'Ataque na ala do Rei'; @override - String get puzzleThemeKingsideAttackDescription => 'Um ataque ao rei do adversário, após este ter feito roque menor (para o lado do rei).'; + String get puzzleThemeKingsideAttackDescription => 'Um ataque ao rei do oponente, após ele ter efetuado o roque curto.'; @override - String get puzzleThemeClearance => 'Limpeza'; + String get puzzleThemeClearance => 'Lance útil'; @override - String get puzzleThemeClearanceDescription => 'Uma jogada, com tempo, que limpa uma casa, fila, coluna ou diagonal para uma ideia tática subsequente.'; + String get puzzleThemeClearanceDescription => 'Um lance, às vezes consumindo tempos, que libera uma casa, fileira ou diagonal para uma ideia tática em seguida.'; @override String get puzzleThemeDefensiveMove => 'Movimento defensivo'; @override - String get puzzleThemeDefensiveMoveDescription => 'Um movimento ou sequência de movimentos precisos, necessários para evitar uma desvantagem, como por exemplo perda de material.'; + String get puzzleThemeDefensiveMoveDescription => 'Um movimento preciso ou sequência de movimentos que são necessários para evitar perda de material ou outra vantagem.'; @override String get puzzleThemeDeflection => 'Desvio'; @override - String get puzzleThemeDeflectionDescription => 'Uma jogada que distrai uma peça do adversário de outra função, como por exemplo, proteger uma casa chave. Às vezes também é chamado de sobrecarga.'; + String get puzzleThemeDeflectionDescription => 'Um movimento que desvia a peça do oponente da sua função, por exemplo a de defesa de outra peça ou a defesa de uma casa importante.'; @override String get puzzleThemeDiscoveredAttack => 'Ataque descoberto'; @override - String get puzzleThemeDiscoveredAttackDescription => 'Mover uma peça que estava a bloquear um ataque de uma peça de longo alcance, como por exemplo um cavalo que sai da frente de uma torre.'; + String get puzzleThemeDiscoveredAttackDescription => 'Mover uma peça que anteriormente bloqueava um ataque de uma peça de longo alcance, como por exemplo um cavalo liberando a coluna de uma torre.'; @override String get puzzleThemeDoubleCheck => 'Xeque duplo'; @override - String get puzzleThemeDoubleCheckDescription => 'Fazer xeque com duas peças ao mesmo tempo, como consequência de um ataque descoberto em que tanto a peça que move como a peça que é descoberta atacam o rei do adversário.'; + String get puzzleThemeDoubleCheckDescription => 'Dar Xeque com duas peças ao mesmo tempo, como resultado de um ataque descoberto onde tanto a peça que se move quanto a peça que estava sendo obstruída atacam o rei do oponente.'; @override - String get puzzleThemeEndgame => 'Final de jogo'; + String get puzzleThemeEndgame => 'Finais'; @override - String get puzzleThemeEndgameDescription => 'Uma tática durante a última fase do jogo.'; + String get puzzleThemeEndgameDescription => 'Tática durante a última fase do jogo.'; @override - String get puzzleThemeEnPassantDescription => 'Uma tática que envolve a regra de \"en passant\", onde um peão pode capturar um peão adversário que o ignorou usando o seu primeiro movimento de duas casas.'; + String get puzzleThemeEnPassantDescription => 'Uma tática envolvendo a regra do en passant, onde um peão pode capturar um peão do oponente que passou por ele usando seu movimento inicial de duas casas.'; @override String get puzzleThemeExposedKing => 'Rei exposto'; @@ -1036,273 +1054,273 @@ class AppLocalizationsPt extends AppLocalizations { String get puzzleThemeExposedKingDescription => 'Uma tática que envolve um rei com poucos defensores ao seu redor, muitas vezes levando a xeque-mate.'; @override - String get puzzleThemeFork => 'Garfo'; + String get puzzleThemeFork => 'Garfo (ou duplo)'; @override - String get puzzleThemeForkDescription => 'Uma jogada em que uma peça ataca duas peças do adversário simultaneamente.'; + String get puzzleThemeForkDescription => 'Um movimento onde a peça movida ataca duas peças de oponente de uma só vez.'; @override - String get puzzleThemeHangingPiece => 'Peça desprotegida'; + String get puzzleThemeHangingPiece => 'Peça pendurada'; @override - String get puzzleThemeHangingPieceDescription => 'Uma tática que envolve uma peça do adversário que não está suficientemente defendida e por isso pode ser capturada.'; + String get puzzleThemeHangingPieceDescription => 'Uma táctica que envolve uma peça indefesa do oponente ou insuficientemente defendida e livre para ser capturada.'; @override - String get puzzleThemeHookMate => 'Mate gancho'; + String get puzzleThemeHookMate => 'Xeque gancho'; @override - String get puzzleThemeHookMateDescription => 'Mate com uma torre, cavalo e peão em que o rei inimigo tem as jogadas de escape bloqueadas por um peão aliado.'; + String get puzzleThemeHookMateDescription => 'Xeque-mate com uma torre, um cavalo e um peão, juntamente com um peão inimigo, para limitar a fuga do rei.'; @override String get puzzleThemeInterference => 'Interferência'; @override - String get puzzleThemeInterferenceDescription => 'Jogar uma peça para uma casa entre duas peças do adversário deixando pelo menos uma delas desprotegia, como por exemplo um cavalo numa casa defendida entre duas torres.'; + String get puzzleThemeInterferenceDescription => 'Mover uma peça entre duas peças do oponente para deixar uma ou duas peças do oponente indefesas, como um cavalo em uma casa defendida por duas torres.'; @override - String get puzzleThemeIntermezzo => 'Intermezzo'; + String get puzzleThemeIntermezzo => 'Lance intermediário'; @override - String get puzzleThemeIntermezzoDescription => 'Em vez de jogares o movimento esperado, primeiro interpõe outro movimento colocando uma ameaça imediata à qual o oponente deve responder. Também conhecido como \"Zwischenzug\" ou jogada intermédia.'; + String get puzzleThemeIntermezzoDescription => 'Em vez de jogar o movimento esperado, primeiro realiza outro movimento criando uma ameaça imediata a que o oponente deve responder. Também conhecido como \"Zwischenzug\" ou \"In between\".'; @override - String get puzzleThemeKnightEndgame => 'Final de cavalo'; + String get puzzleThemeKnightEndgame => 'Finais de Cavalo'; @override - String get puzzleThemeKnightEndgameDescription => 'Um final de jogo com apenas cavalos e peões.'; + String get puzzleThemeKnightEndgameDescription => 'Um final jogado apenas com cavalos e peões.'; @override - String get puzzleThemeLong => 'Problema longo'; + String get puzzleThemeLong => 'Quebra-cabeças longo'; @override - String get puzzleThemeLongDescription => 'Três movimentos para ganhar.'; + String get puzzleThemeLongDescription => 'Vitória em três movimentos.'; @override - String get puzzleThemeMaster => 'Jogos de mestres'; + String get puzzleThemeMaster => 'Partidas de mestres'; @override - String get puzzleThemeMasterDescription => 'Problemas de partidas jogadas por jogadores titulados.'; + String get puzzleThemeMasterDescription => 'Quebra-cabeças de partidas jogadas por jogadores titulados.'; @override - String get puzzleThemeMasterVsMaster => 'Jogos de Mestre vs Mestre'; + String get puzzleThemeMasterVsMaster => 'Partidas de Mestre vs Mestre'; @override - String get puzzleThemeMasterVsMasterDescription => 'Partidas jogadas entre dois jogadores titulados.'; + String get puzzleThemeMasterVsMasterDescription => 'Quebra-cabeças de partidas entre dois jogadores titulados.'; @override String get puzzleThemeMate => 'Xeque-mate'; @override - String get puzzleThemeMateDescription => 'Vence a partida com estilo.'; + String get puzzleThemeMateDescription => 'Vença o jogo com estilo.'; @override String get puzzleThemeMateIn1 => 'Mate em 1'; @override - String get puzzleThemeMateIn1Description => 'Faz xeque-mate num movimento.'; + String get puzzleThemeMateIn1Description => 'Dar xeque-mate em um movimento.'; @override String get puzzleThemeMateIn2 => 'Mate em 2'; @override - String get puzzleThemeMateIn2Description => 'Faz xeque-mate em dois movimentos.'; + String get puzzleThemeMateIn2Description => 'Dar xeque-mate em dois movimentos.'; @override String get puzzleThemeMateIn3 => 'Mate em 3'; @override - String get puzzleThemeMateIn3Description => 'Faz xeque-mate em três movimentos.'; + String get puzzleThemeMateIn3Description => 'Dar xeque-mate em três movimentos.'; @override String get puzzleThemeMateIn4 => 'Mate em 4'; @override - String get puzzleThemeMateIn4Description => 'Faz xeque-mate em quatro movimentos.'; + String get puzzleThemeMateIn4Description => 'Dar xeque-mate em 4 movimentos.'; @override String get puzzleThemeMateIn5 => 'Mate em 5 ou mais'; @override - String get puzzleThemeMateIn5Description => 'Descobre uma longa sequência que leva ao xeque-mate.'; + String get puzzleThemeMateIn5Description => 'Descubra uma longa sequência de mate.'; @override String get puzzleThemeMiddlegame => 'Meio-jogo'; @override - String get puzzleThemeMiddlegameDescription => 'Uma tática durante a segunda fase do jogo.'; + String get puzzleThemeMiddlegameDescription => 'Tática durante a segunda fase do jogo.'; @override - String get puzzleThemeOneMove => 'Problema de um movimento'; + String get puzzleThemeOneMove => 'Quebra-cabeças de um movimento'; @override - String get puzzleThemeOneMoveDescription => 'Um problema que é resolvido com apenas um movimento.'; + String get puzzleThemeOneMoveDescription => 'Quebra-cabeças de um movimento.'; @override String get puzzleThemeOpening => 'Abertura'; @override - String get puzzleThemeOpeningDescription => 'Uma tática durante a primeira fase do jogo.'; + String get puzzleThemeOpeningDescription => 'Tática durante a primeira fase do jogo.'; @override - String get puzzleThemePawnEndgame => 'Final de peões'; + String get puzzleThemePawnEndgame => 'Finais de peões'; @override - String get puzzleThemePawnEndgameDescription => 'Um final de jogo só com peões.'; + String get puzzleThemePawnEndgameDescription => 'Um final apenas com peões.'; @override String get puzzleThemePin => 'Cravada'; @override - String get puzzleThemePinDescription => 'Uma tática que envolve cravadas, onde uma peça é incapaz de se mover sem revelar um ataque a uma peça de valor superior.'; + String get puzzleThemePinDescription => 'Uma tática envolvendo cravada, onde uma peça é incapaz de mover-se sem abrir um descoberto em uma peça de maior valor.'; @override String get puzzleThemePromotion => 'Promoção'; @override - String get puzzleThemePromotionDescription => 'Promova o teu peão a uma dama ou numa peça menor.'; + String get puzzleThemePromotionDescription => 'Promova um peão para uma dama ou a uma peça menor.'; @override - String get puzzleThemeQueenEndgame => 'Final de dama'; + String get puzzleThemeQueenEndgame => 'Finais de Dama'; @override String get puzzleThemeQueenEndgameDescription => 'Um final com apenas damas e peões.'; @override - String get puzzleThemeQueenRookEndgame => 'Dama e torre'; + String get puzzleThemeQueenRookEndgame => 'Finais de Dama e Torre'; @override - String get puzzleThemeQueenRookEndgameDescription => 'Um final de jogo só com damas, torres e peões.'; + String get puzzleThemeQueenRookEndgameDescription => 'Finais com apenas Dama, Torre e Peões.'; @override - String get puzzleThemeQueensideAttack => 'Ataque no lado da dama'; + String get puzzleThemeQueensideAttack => 'Ataque na ala da dama'; @override - String get puzzleThemeQueensideAttackDescription => 'Um ataque ao rei do adversário, após este ter feito roque grande (para o lado da dama).'; + String get puzzleThemeQueensideAttackDescription => 'Um ataque ao rei adversário, após ter efetuado o roque na ala da Dama.'; @override - String get puzzleThemeQuietMove => 'Jogada subtil'; + String get puzzleThemeQuietMove => 'Lance de preparação'; @override - String get puzzleThemeQuietMoveDescription => 'Um movimento que não faz uma cheque nem captura, mas prepara uma ameaça inevitável.'; + String get puzzleThemeQuietMoveDescription => 'Um lance que não dá xeque nem realiza uma captura, mas prepara uma ameaça inevitável para a jogada seguinte.'; @override - String get puzzleThemeRookEndgame => 'Final de torre'; + String get puzzleThemeRookEndgame => 'Finais de Torres'; @override - String get puzzleThemeRookEndgameDescription => 'Um final de jogo com apenas torres e peões.'; + String get puzzleThemeRookEndgameDescription => 'Um final com apenas torres e peões.'; @override String get puzzleThemeSacrifice => 'Sacrifício'; @override - String get puzzleThemeSacrificeDescription => 'Uma tática que involve abdicar de material a curto prazo, para ganhar uma vantagem após uma sequência forçada de movimentos.'; + String get puzzleThemeSacrificeDescription => 'Uma tática envolvendo a entrega de material no curto prazo, com o objetivo de se obter uma vantagem após uma sequência forçada de movimentos.'; @override - String get puzzleThemeShort => 'Problema curto'; + String get puzzleThemeShort => 'Quebra-cabeças curto'; @override - String get puzzleThemeShortDescription => 'Duas jogadas para ganhar.'; + String get puzzleThemeShortDescription => 'Vitória em dois lances.'; @override - String get puzzleThemeSkewer => 'Cravada inversa'; + String get puzzleThemeSkewer => 'Raio X'; @override - String get puzzleThemeSkewerDescription => 'Uma tática que envolve uma peça de alto valor que está ser atacada, mas ao afastar-se, permite que uma peça de menor valor, que estava atrás dela, seja capturada ou atacada. É o inverso da cravada.'; + String get puzzleThemeSkewerDescription => 'Um movimento que envolve uma peça de alto valor sendo atacada fugindo do ataque e permitindo que uma peça de menor valor seja capturada ou atacada, o inverso de cravada.'; @override - String get puzzleThemeSmotheredMate => 'Mate de Philidor'; + String get puzzleThemeSmotheredMate => 'Mate de Philidor (mate sufocado)'; @override - String get puzzleThemeSmotheredMateDescription => 'Uma xeque-mate feito por um cavalo em que o rei não se pode mover porque está rodeado pelas suas próprias peças. Também conhecido como mate sufocado.'; + String get puzzleThemeSmotheredMateDescription => 'Um xeque-mate dado por um cavalo onde o rei é incapaz de mover-se porque está cercado (ou sufocado) pelas próprias peças.'; @override - String get puzzleThemeSuperGM => 'Jogos de Super GM'; + String get puzzleThemeSuperGM => 'Super partidas de GMs'; @override - String get puzzleThemeSuperGMDescription => 'Problemas de partidas jogadas pelos melhores jogadores do mundo.'; + String get puzzleThemeSuperGMDescription => 'Quebra-cabeças de partidas jogadas pelos melhores jogadores do mundo.'; @override - String get puzzleThemeTrappedPiece => 'Peça encurralada'; + String get puzzleThemeTrappedPiece => 'Peça presa'; @override - String get puzzleThemeTrappedPieceDescription => 'Uma peça não consegue escapar à captura, pois tem movimentos limitados.'; + String get puzzleThemeTrappedPieceDescription => 'Uma peça é incapaz de escapar da captura, pois tem movimentos limitados.'; @override String get puzzleThemeUnderPromotion => 'Subpromoção'; @override - String get puzzleThemeUnderPromotionDescription => 'Promoção para um cavalo, bispo ou torre.'; + String get puzzleThemeUnderPromotionDescription => 'Promover para cavalo, bispo ou torre.'; @override - String get puzzleThemeVeryLong => 'Problema muito longo'; + String get puzzleThemeVeryLong => 'Quebra-cabeças muito longo'; @override - String get puzzleThemeVeryLongDescription => 'Quatro jogadas para ganhar.'; + String get puzzleThemeVeryLongDescription => 'Quatro movimentos ou mais para vencer.'; @override - String get puzzleThemeXRayAttack => 'Ataque raio-X'; + String get puzzleThemeXRayAttack => 'Ataque em raio X'; @override - String get puzzleThemeXRayAttackDescription => 'Uma peça ataque ou defende uma casa através de uma peça inimiga.'; + String get puzzleThemeXRayAttackDescription => 'Uma peça ataca ou defende uma casa indiretamente, através de uma peça adversária.'; @override String get puzzleThemeZugzwang => 'Zugzwang'; @override - String get puzzleThemeZugzwangDescription => 'O adversário está limitado quanto aos seus movimentos, e todas as jogadas pioram a sua posição.'; + String get puzzleThemeZugzwangDescription => 'O adversário tem os seus movimentos limitados, e qualquer movimento que ele faça vai enfraquecer sua própria posição.'; @override - String get puzzleThemeHealthyMix => 'Mistura saudável'; + String get puzzleThemeHealthyMix => 'Combinação saudável'; @override - String get puzzleThemeHealthyMixDescription => 'Um pouco de tudo. Não sabes o que esperar, então ficas pronto para qualquer coisa! Exatamente como em jogos de verdade.'; + String get puzzleThemeHealthyMixDescription => 'Um pouco de tudo. Você nunca sabe o que vai encontrar, então esteja pronto para tudo! Igualzinho aos jogos em tabuleiros reais.'; @override - String get puzzleThemePlayerGames => 'Jogos de jogadores'; + String get puzzleThemePlayerGames => 'Partidas de jogadores'; @override - String get puzzleThemePlayerGamesDescription => 'Procura problemas gerados a partir dos teus jogos ou de jogos de outro jogador.'; + String get puzzleThemePlayerGamesDescription => 'Procure quebra-cabeças gerados a partir de suas partidas ou das de outro jogador.'; @override String puzzleThemePuzzleDownloadInformation(String param) { - return 'Esses problemas são do domínio público e podem ser obtidos em $param.'; + return 'Esses quebra-cabeças estão em domínio público, e você pode baixá-los em $param.'; } @override - String get searchSearch => 'Procurar'; + String get searchSearch => 'Buscar'; @override String get settingsSettings => 'Configurações'; @override - String get settingsCloseAccount => 'Encerrar a conta'; + String get settingsCloseAccount => 'Encerrar conta'; @override - String get settingsManagedAccountCannotBeClosed => 'A sua conta é gerida e não pode ser encerrada.'; + String get settingsManagedAccountCannotBeClosed => 'Sua conta é gerenciada, e não pode ser encerrada.'; @override - String get settingsClosingIsDefinitive => 'O encerramento é definitivo. Não podes voltar atrás. Tens a certeza?'; + String get settingsClosingIsDefinitive => 'O encerramento é definitivo. Não há como desfazer. Tem certeza?'; @override - String get settingsCantOpenSimilarAccount => 'Não poderá criar uma nova conta com o mesmo nome, mesmo que as maiúsculas ou minúsculas sejam diferentes.'; + String get settingsCantOpenSimilarAccount => 'Você não poderá abrir uma nova conta com o mesmo nome, mesmo que alterne entre maiúsculas e minúsculas.'; @override - String get settingsChangedMindDoNotCloseAccount => 'Mudei de ideias, não encerrem a minha conta'; + String get settingsChangedMindDoNotCloseAccount => 'Eu mudei de ideia, não encerre minha conta'; @override - String get settingsCloseAccountExplanation => 'Tens a certeza que queres encerrar sua conta? Encerrar a tua conta é uma decisão permanente. Tu NUNCA MAIS serás capaz de iniciar sessão nesta conta.'; + String get settingsCloseAccountExplanation => 'Tem certeza de que deseja encerrar sua conta? Encerrar sua conta é uma decisão permanente. Você NUNCA MAIS será capaz de entrar com ela novamente.'; @override String get settingsThisAccountIsClosed => 'Esta conta foi encerrada.'; @override - String get playWithAFriend => 'Jogar com um amigo'; + String get playWithAFriend => 'Jogar contra um amigo'; @override String get playWithTheMachine => 'Jogar contra o computador'; @override - String get toInviteSomeoneToPlayGiveThisUrl => 'Para convidares alguém para jogar, envia este URL'; + String get toInviteSomeoneToPlayGiveThisUrl => 'Para convidar alguém para jogar, envie este URL'; @override String get gameOver => 'Fim da partida'; @@ -1311,13 +1329,13 @@ class AppLocalizationsPt extends AppLocalizations { String get waitingForOpponent => 'Aguardando oponente'; @override - String get orLetYourOpponentScanQrCode => 'Ou deixa o teu oponente ler este código QR'; + String get orLetYourOpponentScanQrCode => 'Ou deixe seu oponente ler este QR Code'; @override - String get waiting => 'A aguardar'; + String get waiting => 'Aguardando'; @override - String get yourTurn => 'É a tua vez'; + String get yourTurn => 'Sua vez'; @override String aiNameLevelAiLevel(String param1, String param2) { @@ -1331,7 +1349,7 @@ class AppLocalizationsPt extends AppLocalizations { String get strength => 'Nível'; @override - String get toggleTheChat => 'Ativar/Desativar o chat'; + String get toggleTheChat => 'Ativar/Desativar chat'; @override String get chat => 'Chat'; @@ -1352,10 +1370,10 @@ class AppLocalizationsPt extends AppLocalizations { String get black => 'Pretas'; @override - String get asWhite => 'com as brancas'; + String get asWhite => 'de brancas'; @override - String get asBlack => 'com as pretas'; + String get asBlack => 'de pretas'; @override String get randomColor => 'Cor aleatória'; @@ -1370,16 +1388,16 @@ class AppLocalizationsPt extends AppLocalizations { String get blackIsVictorious => 'Pretas vencem'; @override - String get youPlayTheWhitePieces => 'Tu jogas com as peças brancas'; + String get youPlayTheWhitePieces => 'Você joga com as peças brancas'; @override - String get youPlayTheBlackPieces => 'Tu jogas com as peças pretas'; + String get youPlayTheBlackPieces => 'Você joga com as peças pretas'; @override - String get itsYourTurn => 'É a tua vez!'; + String get itsYourTurn => 'É a sua vez!'; @override - String get cheatDetected => 'Fraude detetada'; + String get cheatDetected => 'Trapaça Detectada'; @override String get kingInTheCenter => 'Rei no centro'; @@ -1394,22 +1412,22 @@ class AppLocalizationsPt extends AppLocalizations { String get variantEnding => 'Fim da variante'; @override - String get newOpponent => 'Novo adversário'; + String get newOpponent => 'Novo oponente'; @override - String get yourOpponentWantsToPlayANewGameWithYou => 'O teu adversário quer jogar outra vez contra ti'; + String get yourOpponentWantsToPlayANewGameWithYou => 'Seu oponente quer jogar uma nova partida contra você'; @override String get joinTheGame => 'Entrar no jogo'; @override - String get whitePlays => 'Jogam as brancas'; + String get whitePlays => 'Brancas jogam'; @override - String get blackPlays => 'Jogam as pretas'; + String get blackPlays => 'Pretas jogam'; @override - String get opponentLeftChoices => 'O teu adversário deixou a partida. Podes reivindicar vitória, declarar empate ou aguardar.'; + String get opponentLeftChoices => 'O seu oponente deixou a partida. Você pode reivindicar vitória, declarar empate ou aguardar.'; @override String get forceResignation => 'Reivindicar vitória'; @@ -1418,22 +1436,22 @@ class AppLocalizationsPt extends AppLocalizations { String get forceDraw => 'Reivindicar empate'; @override - String get talkInChat => 'Por favor, sê gentil na conversa!'; + String get talkInChat => 'Por favor, seja gentil no chat!'; @override - String get theFirstPersonToComeOnThisUrlWillPlayWithYou => 'A primeira pessoa que aceder a este link jogará contra ti.'; + String get theFirstPersonToComeOnThisUrlWillPlayWithYou => 'A primeira pessoa que acessar esta URL jogará contigo.'; @override - String get whiteResigned => 'As brancas desistiram'; + String get whiteResigned => 'Brancas desistiram'; @override - String get blackResigned => 'As pretas desistiram'; + String get blackResigned => 'Pretas desistiram'; @override - String get whiteLeftTheGame => 'As brancas deixaram a partida'; + String get whiteLeftTheGame => 'Brancas deixaram a partida'; @override - String get blackLeftTheGame => 'As pretas deixaram a partida'; + String get blackLeftTheGame => 'Pretas deixaram a partida'; @override String get whiteDidntMove => 'As brancas não se moveram'; @@ -1442,10 +1460,10 @@ class AppLocalizationsPt extends AppLocalizations { String get blackDidntMove => 'As pretas não se moveram'; @override - String get requestAComputerAnalysis => 'Solicitar uma análise de computador'; + String get requestAComputerAnalysis => 'Solicitar uma análise do computador'; @override - String get computerAnalysis => 'Análise de computador'; + String get computerAnalysis => 'Análise do computador'; @override String get computerAnalysisAvailable => 'Análise de computador disponível'; @@ -1462,22 +1480,22 @@ class AppLocalizationsPt extends AppLocalizations { } @override - String get usingServerAnalysis => 'A usar a análise do servidor'; + String get usingServerAnalysis => 'Análise de servidor em uso'; @override - String get loadingEngine => 'A carregar o motor de jogo...'; + String get loadingEngine => 'Carregando ...'; @override - String get calculatingMoves => 'A calcular as jogadas...'; + String get calculatingMoves => 'Calculando jogadas...'; @override - String get engineFailed => 'Erro ao carregar o motor'; + String get engineFailed => 'Erro ao carregar o engine'; @override String get cloudAnalysis => 'Análise na nuvem'; @override - String get goDeeper => 'Aprofundar'; + String get goDeeper => 'Detalhar'; @override String get showThreat => 'Mostrar ameaça'; @@ -1486,37 +1504,37 @@ class AppLocalizationsPt extends AppLocalizations { String get inLocalBrowser => 'no navegador local'; @override - String get toggleLocalEvaluation => 'Ligar/desligar a avaliação local'; + String get toggleLocalEvaluation => 'Ativar/Desativar análise local'; @override String get promoteVariation => 'Promover variante'; @override - String get makeMainLine => 'Tornar variante principal'; + String get makeMainLine => 'Transformar em linha principal'; @override - String get deleteFromHere => 'Eliminar a partir de aqui'; + String get deleteFromHere => 'Excluir a partir daqui'; @override - String get collapseVariations => 'Recolher variações'; + String get collapseVariations => 'Esconder variantes'; @override - String get expandVariations => 'Expandir variações'; + String get expandVariations => 'Mostrar variantes'; @override - String get forceVariation => 'Forçar variante'; + String get forceVariation => 'Variante forçada'; @override - String get copyVariationPgn => 'Copiar variação PGN'; + String get copyVariationPgn => 'Copiar PGN da variante'; @override - String get move => 'Jogada'; + String get move => 'Movimentos'; @override - String get variantLoss => 'Variante perdida'; + String get variantLoss => 'Derrota da variante'; @override - String get variantWin => 'Variante ganha'; + String get variantWin => 'Vitória da variante'; @override String get insufficientMaterial => 'Material insuficiente'; @@ -1531,26 +1549,26 @@ class AppLocalizationsPt extends AppLocalizations { String get close => 'Fechar'; @override - String get winning => 'Ganhas'; + String get winning => 'Vencendo'; @override - String get losing => 'Perdidas'; + String get losing => 'Perdendo'; @override - String get drawn => 'Empatado'; + String get drawn => 'Empate'; @override - String get unknown => 'Desconhecidos'; + String get unknown => 'Posição desconhecida'; @override - String get database => 'Base de dados'; + String get database => 'Banco de Dados'; @override String get whiteDrawBlack => 'Brancas / Empate / Pretas'; @override String averageRatingX(String param) { - return 'Pontuação média: $param'; + return 'Classificação média: $param'; } @override @@ -1561,20 +1579,20 @@ class AppLocalizationsPt extends AppLocalizations { @override String masterDbExplanation(String param1, String param2, String param3) { - return 'de partidas OTB de jogadores com +$param1 rating FIDE de $param2 a $param3'; + return 'Duas milhões de partidas de jogadores com pontuação FIDE acima de $param1, desde $param2 a $param3'; } @override - String get dtzWithRounding => 'DTZ50\'\' com arredondamento, baseado no número de meios-movimentos até à próxima captura ou movimento de peão'; + String get dtzWithRounding => 'DTZ50\" com arredondamento, baseado no número de meias-jogadas até a próxima captura ou jogada de peão'; @override - String get noGameFound => 'Nenhum jogo encontrado'; + String get noGameFound => 'Nenhuma partida encontrada'; @override - String get maxDepthReached => 'Nível máximo alcançado!'; + String get maxDepthReached => 'Profundidade máxima alcançada!'; @override - String get maybeIncludeMoreGamesFromThePreferencesMenu => 'Talvez incluir mais jogos no menu de preferências?'; + String get maybeIncludeMoreGamesFromThePreferencesMenu => 'Talvez você queira incluir mais jogos a partir do menu de preferências'; @override String get openings => 'Aberturas'; @@ -1587,47 +1605,47 @@ class AppLocalizationsPt extends AppLocalizations { @override String xOpeningExplorer(String param) { - return 'Explorador de aberturas de $param'; + return '$param Explorador de aberturas'; } @override - String get playFirstOpeningEndgameExplorerMove => 'Jogar o primeiro lance do explorador de aberturas/finais'; + String get playFirstOpeningEndgameExplorerMove => 'Jogue o primeiro lance do explorador de aberturas/finais'; @override String get winPreventedBy50MoveRule => 'Vitória impedida pela regra dos 50 movimentos'; @override - String get lossSavedBy50MoveRule => 'Derrota evitada pela regra dos 50 movimentos'; + String get lossSavedBy50MoveRule => 'Derrota impedida pela regra dos 50 movimentos'; @override - String get winOr50MovesByPriorMistake => 'Vitória ou 50 movimentos por engano anterior'; + String get winOr50MovesByPriorMistake => 'Vitória ou 50 movimentos por erro anterior'; @override - String get lossOr50MovesByPriorMistake => 'Vitória ou 50 movimentos por engano anterior'; + String get lossOr50MovesByPriorMistake => 'Derrota ou 50 movimentos por erro anterior'; @override - String get unknownDueToRounding => 'Vitória/derrota garantida apenas se a linha da tabela recomendada tiver sido seguida desde a última captura ou movimento de peão, devido a possível arredondamento.'; + String get unknownDueToRounding => 'Vitória/derrota garantida somente se a variante recomendada tiver sido seguida desde o último movimento de captura ou de peão, devido ao possível arredondamento.'; @override - String get allSet => 'Tudo a postos!'; + String get allSet => 'Tudo pronto!'; @override String get importPgn => 'Importar PGN'; @override - String get delete => 'Eliminar'; + String get delete => 'Excluir'; @override - String get deleteThisImportedGame => 'Eliminar este jogo importado?'; + String get deleteThisImportedGame => 'Excluir este jogo importado?'; @override - String get replayMode => 'Modo de repetição'; + String get replayMode => 'Rever a partida'; @override - String get realtimeReplay => 'Tempo real'; + String get realtimeReplay => 'Tempo Real'; @override - String get byCPL => 'Por CPL'; + String get byCPL => 'Por erros'; @override String get openStudy => 'Abrir estudo'; @@ -1639,13 +1657,13 @@ class AppLocalizationsPt extends AppLocalizations { String get bestMoveArrow => 'Seta de melhor movimento'; @override - String get showVariationArrows => 'Ver setas de variação'; + String get showVariationArrows => 'Mostrar setas das variantes'; @override - String get evaluationGauge => 'Medidor da avaliação'; + String get evaluationGauge => 'Escala de avaliação'; @override - String get multipleLines => 'Múltiplas continuações'; + String get multipleLines => 'Linhas de análise'; @override String get cpus => 'CPUs'; @@ -1657,13 +1675,13 @@ class AppLocalizationsPt extends AppLocalizations { String get infiniteAnalysis => 'Análise infinita'; @override - String get removesTheDepthLimit => 'Remove o limite de profundidade e mantém o teu computador quente'; + String get removesTheDepthLimit => 'Remove o limite de profundidade, o que aquece seu computador'; @override - String get engineManager => 'Gestão do motor'; + String get engineManager => 'Gerenciador de engine'; @override - String get blunder => 'Erro grave'; + String get blunder => 'Capivarada'; @override String get mistake => 'Erro'; @@ -1672,16 +1690,16 @@ class AppLocalizationsPt extends AppLocalizations { String get inaccuracy => 'Imprecisão'; @override - String get moveTimes => 'Tempo das jogadas'; + String get moveTimes => 'Tempo por movimento'; @override - String get flipBoard => 'Inverter o tabuleiro'; + String get flipBoard => 'Girar o tabuleiro'; @override - String get threefoldRepetition => 'Repetição tripla'; + String get threefoldRepetition => 'Tripla repetição'; @override - String get claimADraw => 'Declarar empate'; + String get claimADraw => 'Reivindicar empate'; @override String get offerDraw => 'Propor empate'; @@ -1696,7 +1714,7 @@ class AppLocalizationsPt extends AppLocalizations { String get fiftyMovesWithoutProgress => 'Cinquenta jogadas sem progresso'; @override - String get currentGames => 'Partidas a decorrer'; + String get currentGames => 'Partidas atuais'; @override String get viewInFullSize => 'Ver em tela cheia'; @@ -1708,16 +1726,16 @@ class AppLocalizationsPt extends AppLocalizations { String get signIn => 'Entrar'; @override - String get rememberMe => 'Lembrar-me'; + String get rememberMe => 'Lembrar de mim'; @override - String get youNeedAnAccountToDoThat => 'Precisas de uma conta para fazeres isso'; + String get youNeedAnAccountToDoThat => 'Você precisa de uma conta para fazer isso'; @override - String get signUp => 'Registar-se'; + String get signUp => 'Registrar'; @override - String get computersAreNotAllowedToPlay => 'Computadores ou jogadores assistidos por computador não estão autorizados a jogar. Por favor não utilizes assistência de programas de xadrez, bases de dados ou outros jogadores enquanto estiveres a jogar. Além disso, a criação de contas múltiplas é fortemente desencorajada e a sua prática excessiva acarretará banimento.'; + String get computersAreNotAllowedToPlay => 'A ajuda de software não é permitida. Por favor, não utilize programas de xadrez, bancos de dados ou o auxilio de outros jogadores durante a partida. Além disso, a criação de múltiplas contas é fortemente desaconselhada e sua prática excessiva acarretará em banimento.'; @override String get games => 'Partidas'; @@ -1740,7 +1758,7 @@ class AppLocalizationsPt extends AppLocalizations { String get friends => 'Amigos'; @override - String get discussions => 'Conversas'; + String get discussions => 'Discussões'; @override String get today => 'Hoje'; @@ -1758,10 +1776,10 @@ class AppLocalizationsPt extends AppLocalizations { String get variants => 'Variantes'; @override - String get timeControl => 'Ritmo de jogo'; + String get timeControl => 'Ritmo'; @override - String get realTime => 'Em tempo real'; + String get realTime => 'Tempo real'; @override String get correspondence => 'Correspondência'; @@ -1776,37 +1794,37 @@ class AppLocalizationsPt extends AppLocalizations { String get time => 'Tempo'; @override - String get rating => 'Pontuação'; + String get rating => 'Rating'; @override - String get ratingStats => 'Estatísticas de pontuação'; + String get ratingStats => 'Estatísticas de classificação'; @override - String get username => 'Nome de utilizador'; + String get username => 'Nome de usuário'; @override - String get usernameOrEmail => 'Nome ou e-mail do utilizador'; + String get usernameOrEmail => 'Nome ou email do usuário'; @override - String get changeUsername => 'Alterar o nome de utilizador'; + String get changeUsername => 'Alterar nome de usuário'; @override - String get changeUsernameNotSame => 'Só te é permitido trocar as letras de minúscula para maiúscula e vice-versa. Por exemplo, \"johndoe\" para \"JohnDoe\".'; + String get changeUsernameNotSame => 'Pode-se apenas trocar as letras de minúscula para maiúscula e vice-versa. Por exemplo, \"fulanodetal\" para \"FulanoDeTal\".'; @override - String get changeUsernameDescription => 'Altera o teu nome de utilizador. Isso só pode ser feito uma vez e só poderás trocar as letras de minúscula para maiúscula e vice-versa.'; + String get changeUsernameDescription => 'Altere seu nome de usuário. Isso só pode ser feito uma vez e você poderá apenas trocar as letras de minúscula para maiúscula e vice-versa.'; @override - String get signupUsernameHint => 'Certifique-se que escolhe um nome de utilizador decoroso. Não poderá alterá-lo mais tarde e quaisquer contas com nomes de utilizador inapropriados serão fechadas!'; + String get signupUsernameHint => 'Escolha um nome de usuário apropriado. Não será possível mudá-lo, e qualquer conta que tiver um nome ofensivo ou inapropriado será excluída!'; @override - String get signupEmailHint => 'Só o usaremos para redefinir a palavra-passe.'; + String get signupEmailHint => 'Vamos usar apenas para redefinir a sua senha.'; @override - String get password => 'Palavra-passe'; + String get password => 'Senha'; @override - String get changePassword => 'Alterar a palavra-passe'; + String get changePassword => 'Alterar senha'; @override String get changeEmail => 'Alterar email'; @@ -1815,45 +1833,45 @@ class AppLocalizationsPt extends AppLocalizations { String get email => 'E-mail'; @override - String get passwordReset => 'Redefinir a palavra-passe'; + String get passwordReset => 'Redefinição de senha'; @override - String get forgotPassword => 'Esqueceste-te da tua palavra-passe?'; + String get forgotPassword => 'Esqueceu sua senha?'; @override - String get error_weakPassword => 'Esta senha é extremamente comum, e muito fácil de adivinhar.'; + String get error_weakPassword => 'A senha é extremamente comum e fácil de adivinhar.'; @override - String get error_namePassword => 'Por favor, não usa o teu nome de utilizador como senha.'; + String get error_namePassword => 'Não utilize seu nome de usuário como senha.'; @override - String get blankedPassword => 'Utilizou a mesma palavra-passe noutro site, e esse site foi comprometido. Para garantir a segurança da sua conta Lichess, precisamos que redefina a palavra-passe. Obrigado pela compreensão.'; + String get blankedPassword => 'Você usou a mesma senha em outro site, e esse site foi comprometido. Para garantir a segurança da sua conta no Lichess, você precisa criar uma nova senha. Agradecemos sua compreensão.'; @override - String get youAreLeavingLichess => 'Você está a sair do Lichess'; + String get youAreLeavingLichess => 'Você está saindo do Lichess'; @override - String get neverTypeYourPassword => 'Nunca escrevas a tua senha Lichess em outro site!'; + String get neverTypeYourPassword => 'Nunca digite sua senha do Lichess em outro site!'; @override String proceedToX(String param) { - return 'Continuar para $param'; + return 'Ir para $param'; } @override - String get passwordSuggestion => 'Não uses uma senha sugerida por outra pessoa. Eles vão utilizar-la para roubar a tua conta.'; + String get passwordSuggestion => 'Não coloque uma senha sugerida por outra pessoa, porque ela poderá roubar sua conta.'; @override - String get emailSuggestion => 'Não uses um email sugerida por outra pessoa. Eles vão utilizar-la para roubar a tua conta.'; + String get emailSuggestion => 'Não coloque um endereço de email sugerido por outra pessoa, porque ela poderá roubar sua conta.'; @override - String get emailConfirmHelp => 'Ajuda com a confirmação do endereço eletrónico'; + String get emailConfirmHelp => 'Ajuda com confirmação por e-mail'; @override - String get emailConfirmNotReceived => 'Não recebeu no seu correio eletrónico uma mensagem de confirmação após o registo?'; + String get emailConfirmNotReceived => 'Não recebeu seu e-mail de confirmação após o registro?'; @override - String get whatSignupUsername => 'Que nome de utilizador usou para se registar?'; + String get whatSignupUsername => 'Qual nome de usuário você usou para se registrar?'; @override String usernameNotFound(String param) { @@ -1861,24 +1879,24 @@ class AppLocalizationsPt extends AppLocalizations { } @override - String get usernameCanBeUsedForNewAccount => 'Pode usar esse nome de utilizador para criar uma conta'; + String get usernameCanBeUsedForNewAccount => 'Você pode usar esse nome de usuário para criar uma nova conta'; @override String emailSent(String param) { - return 'Enviámos um correio eletrónico para $param.'; + return 'Enviamos um e-mail para $param.'; } @override - String get emailCanTakeSomeTime => 'Pode demorar algum tempo a chegar.'; + String get emailCanTakeSomeTime => 'Pode levar algum tempo para chegar.'; @override - String get refreshInboxAfterFiveMinutes => 'Aguarde 5 minutos e atualize a sua caixa de entrada de correio eletrónico.'; + String get refreshInboxAfterFiveMinutes => 'Aguarde 5 minutos e atualize sua caixa de entrada.'; @override - String get checkSpamFolder => 'Verifique também a sua pasta de “spam”, pode estar lá. Se sim, assinale como não “spam”.'; + String get checkSpamFolder => 'Verifique também a sua caixa de spam. Caso esteja lá, marque como não é spam.'; @override - String get emailForSignupHelp => 'Se tudo falhar, então envie-nos este correio eletrónico:'; + String get emailForSignupHelp => 'Se todo o resto falhar, envie-nos este e-mail:'; @override String copyTextToEmail(String param) { @@ -1886,20 +1904,20 @@ class AppLocalizationsPt extends AppLocalizations { } @override - String get waitForSignupHelp => 'Nós entraremos brevemente em contacto para ajudá-lo a completar a inscrição.'; + String get waitForSignupHelp => 'Entraremos em contato em breve para ajudá-lo a completar seu registro.'; @override String accountConfirmed(String param) { - return 'O utilizador $param foi confirmado com sucesso.'; + return 'O usuário $param foi confirmado com sucesso.'; } @override String accountCanLogin(String param) { - return 'Pode agora aceder como $param.'; + return 'Você pode acessar agora como $param.'; } @override - String get accountConfirmationEmailNotNeeded => 'Não precisa de um endereço eletrónico de confirmação.'; + String get accountConfirmationEmailNotNeeded => 'Você não precisa de um e-mail de confirmação.'; @override String accountClosed(String param) { @@ -1908,52 +1926,52 @@ class AppLocalizationsPt extends AppLocalizations { @override String accountRegisteredWithoutEmail(String param) { - return 'A conta $param foi registada sem um endereço eletrónico.'; + return 'A conta $param foi registrada sem um e-mail.'; } @override - String get rank => 'Classificação'; + String get rank => 'Rank'; @override String rankX(String param) { - return 'Posição: $param'; + return 'Classificação: $param'; } @override - String get gamesPlayed => 'Partidas jogadas'; + String get gamesPlayed => 'Partidas realizadas'; @override String get cancel => 'Cancelar'; @override - String get whiteTimeOut => 'Acabou o tempo das brancas'; + String get whiteTimeOut => 'Tempo das brancas esgotado'; @override - String get blackTimeOut => 'Acabou o tempo das pretas'; + String get blackTimeOut => 'Tempo das pretas esgotado'; @override String get drawOfferSent => 'Proposta de empate enviada'; @override - String get drawOfferAccepted => 'Proposta de empate aceite'; + String get drawOfferAccepted => 'Proposta de empate aceita'; @override String get drawOfferCanceled => 'Proposta de empate cancelada'; @override - String get whiteOffersDraw => 'As brancas propõem empate'; + String get whiteOffersDraw => 'Brancas oferecem empate'; @override - String get blackOffersDraw => 'As pretas propõem empate'; + String get blackOffersDraw => 'Pretas oferecem empate'; @override - String get whiteDeclinesDraw => 'As brancas recusam o empate'; + String get whiteDeclinesDraw => 'Brancas recusam empate'; @override - String get blackDeclinesDraw => 'As pretas recusam o empate'; + String get blackDeclinesDraw => 'Pretas recusam empate'; @override - String get yourOpponentOffersADraw => 'O teu adversário propõe empate'; + String get yourOpponentOffersADraw => 'Seu adversário oferece empate'; @override String get accept => 'Aceitar'; @@ -1962,16 +1980,16 @@ class AppLocalizationsPt extends AppLocalizations { String get decline => 'Recusar'; @override - String get playingRightNow => 'A jogar agora'; + String get playingRightNow => 'Jogando agora'; @override - String get eventInProgress => 'A decorrer agora'; + String get eventInProgress => 'Jogando agora'; @override String get finished => 'Terminado'; @override - String get abortGame => 'Cancelar a partida'; + String get abortGame => 'Cancelar partida'; @override String get gameAborted => 'Partida cancelada'; @@ -1989,64 +2007,64 @@ class AppLocalizationsPt extends AppLocalizations { String get mode => 'Modo'; @override - String get casual => 'Amigável'; + String get casual => 'Amistosa'; @override - String get rated => 'A valer pontos'; + String get rated => 'Ranqueada'; @override - String get casualTournament => 'Amigável'; + String get casualTournament => 'Amistoso'; @override - String get ratedTournament => 'A valer pontos'; + String get ratedTournament => 'Valendo pontos'; @override String get thisGameIsRated => 'Esta partida vale pontos'; @override - String get rematch => 'Desforra'; + String get rematch => 'Revanche'; @override - String get rematchOfferSent => 'Pedido de desforra enviado'; + String get rematchOfferSent => 'Oferta de revanche enviada'; @override - String get rematchOfferAccepted => 'Pedido de desforra aceite'; + String get rematchOfferAccepted => 'Oferta de revanche aceita'; @override - String get rematchOfferCanceled => 'Pedido de desforra cancelado'; + String get rematchOfferCanceled => 'Oferta de revanche cancelada'; @override - String get rematchOfferDeclined => 'Pedido de desforra recusado'; + String get rematchOfferDeclined => 'Oferta de revanche recusada'; @override - String get cancelRematchOffer => 'Cancelar o pedido de desforra'; + String get cancelRematchOffer => 'Cancelar oferta de revanche'; @override - String get viewRematch => 'Ver a desforra'; + String get viewRematch => 'Ver revanche'; @override - String get confirmMove => 'Confirmar o lance'; + String get confirmMove => 'Confirmar lance'; @override String get play => 'Jogar'; @override - String get inbox => 'Caixa de entrada'; + String get inbox => 'Mensagens'; @override String get chatRoom => 'Sala de chat'; @override - String get loginToChat => 'Inicia sessão para poderes conversar'; + String get loginToChat => 'Faça login para conversar'; @override - String get youHaveBeenTimedOut => 'Foste impedido de conversar por agora.'; + String get youHaveBeenTimedOut => 'Sua sessão expirou.'; @override - String get spectatorRoom => 'Sala dos espectadores'; + String get spectatorRoom => 'Sala do espectador'; @override - String get composeMessage => 'Escrever uma mensagem'; + String get composeMessage => 'Escrever mensagem'; @override String get subject => 'Assunto'; @@ -2055,7 +2073,7 @@ class AppLocalizationsPt extends AppLocalizations { String get send => 'Enviar'; @override - String get incrementInSeconds => 'Incremento em segundos'; + String get incrementInSeconds => 'Acréscimo em segundos'; @override String get freeOnlineChess => 'Xadrez Online Gratuito'; @@ -2064,34 +2082,34 @@ class AppLocalizationsPt extends AppLocalizations { String get exportGames => 'Exportar partidas'; @override - String get ratingRange => 'Pontuação entre'; + String get ratingRange => 'Rating entre'; @override - String get thisAccountViolatedTos => 'Esta conta violou os termos de serviço do Lichess'; + String get thisAccountViolatedTos => 'Esta conta violou os Termos de Serviço do Lichess'; @override - String get openingExplorerAndTablebase => 'Explorador de aberturas & tabelas de finais'; + String get openingExplorerAndTablebase => 'Explorador de abertura & tabela de finais'; @override - String get takeback => 'Voltar uma jogada atrás'; + String get takeback => 'Voltar jogada'; @override - String get proposeATakeback => 'Propor voltar uma jogada atrás'; + String get proposeATakeback => 'Propor voltar jogada'; @override - String get takebackPropositionSent => 'Proposta de voltar uma jogada atrás enviada'; + String get takebackPropositionSent => 'Proposta de voltar jogada enviada'; @override - String get takebackPropositionDeclined => 'Proposta de voltar uma jogada atrás recusada'; + String get takebackPropositionDeclined => 'Proposta de voltar jogada recusada'; @override - String get takebackPropositionAccepted => 'Proposta de voltar uma jogada atrás aceite'; + String get takebackPropositionAccepted => 'Proposta de voltar jogada aceita'; @override - String get takebackPropositionCanceled => 'Proposta de voltar uma jogada atrás cancelada'; + String get takebackPropositionCanceled => 'Proposta de voltar jogada cancelada'; @override - String get yourOpponentProposesATakeback => 'O teu adversário propõe voltar uma jogada atrás'; + String get yourOpponentProposesATakeback => 'Seu oponente propõe voltar jogada'; @override String get bookmarkThisGame => 'Adicionar esta partida às favoritas'; @@ -2103,38 +2121,38 @@ class AppLocalizationsPt extends AppLocalizations { String get tournaments => 'Torneios'; @override - String get tournamentPoints => 'Pontos de torneio'; + String get tournamentPoints => 'Pontos de torneios'; @override - String get viewTournament => 'Ver o torneio'; + String get viewTournament => 'Ver torneio'; @override String get backToTournament => 'Voltar ao torneio'; @override - String get noDrawBeforeSwissLimit => 'Num torneio suíço não pode empatar antes de 30 jogadas.'; + String get noDrawBeforeSwissLimit => 'Não é possível empatar antes de 30 lances em um torneio suíço.'; @override String get thematic => 'Temático'; @override String yourPerfRatingIsProvisional(String param) { - return 'A tua pontuação em $param é provisória'; + return 'Seu rating $param é provisório'; } @override String yourPerfRatingIsTooHigh(String param1, String param2) { - return 'A tua pontuação em $param1 ($param2) é demasiado alta'; + return 'Seu $param1 rating ($param2) é muito alta'; } @override String yourTopWeeklyPerfRatingIsTooHigh(String param1, String param2) { - return 'A tua pontuação máxima nesta semana em $param1 ($param2) é demasiado alta'; + return 'Seu melhor rating $param1 da semana ($param2) é muito alto'; } @override String yourPerfRatingIsTooLow(String param1, String param2) { - return 'A tua pontuação em $param1 ($param2) é demasiado baixa'; + return 'Sua $param1 pontuação ($param2) é muito baixa'; } @override @@ -2149,40 +2167,40 @@ class AppLocalizationsPt extends AppLocalizations { @override String mustBeInTeam(String param) { - return 'Tens de pertencer à equipa $param'; + return 'Precisa estar na equipe $param'; } @override String youAreNotInTeam(String param) { - return 'Não estás na equipa $param'; + return 'Você não está na equipe $param'; } @override - String get backToGame => 'Voltar à partida'; + String get backToGame => 'Retorne à partida'; @override - String get siteDescription => 'Xadrez online gratuito. Joga xadrez numa interface simples. Sem registos, sem anúncios, sem plugins. Joga xadrez com o computador, amigos ou adversários aleatórios.'; + String get siteDescription => 'Xadrez online gratuito. Jogue xadrez agora numa interface simples. Sem registro, sem anúncios, sem plugins. Jogue xadrez contra computador, amigos ou adversários aleatórios.'; @override String xJoinedTeamY(String param1, String param2) { - return '$param1 juntou-se à equipa $param2'; + return '$param1 juntou-se à equipe $param2'; } @override String xCreatedTeamY(String param1, String param2) { - return '$param1 criou a equipa $param2'; + return '$param1 criou a equipe $param2'; } @override - String get startedStreaming => 'começou uma stream'; + String get startedStreaming => 'começou uma transmissão ao vivo'; @override String xStartedStreaming(String param) { - return '$param começou uma stream'; + return '$param começou a transmitir'; } @override - String get averageElo => 'Pontuação média'; + String get averageElo => 'Média de rating'; @override String get location => 'Localização'; @@ -2191,70 +2209,70 @@ class AppLocalizationsPt extends AppLocalizations { String get filterGames => 'Filtrar partidas'; @override - String get reset => 'Voltar ao original'; + String get reset => 'Reiniciar'; @override String get apply => 'Aplicar'; @override - String get save => 'Guardar'; + String get save => 'Salvar'; @override - String get leaderboard => 'Tabela de liderança'; + String get leaderboard => 'Classificação'; @override - String get screenshotCurrentPosition => 'Posição atual da captura de ecrã'; + String get screenshotCurrentPosition => 'Captura de tela da posição atual'; @override - String get gameAsGIF => 'Jogo como GIF'; + String get gameAsGIF => 'Salvar a partida como GIF'; @override - String get pasteTheFenStringHere => 'Coloca a notação FEN aqui'; + String get pasteTheFenStringHere => 'Cole a notação FEN aqui'; @override - String get pasteThePgnStringHere => 'Coloca a notação PGN aqui'; + String get pasteThePgnStringHere => 'Cole a notação PGN aqui'; @override - String get orUploadPgnFile => 'Ou enviar um ficheiro PGN'; + String get orUploadPgnFile => 'Ou carregue um arquivo PGN'; @override - String get fromPosition => 'A partir de uma posição'; + String get fromPosition => 'A partir da posição'; @override - String get continueFromHere => 'Continuar a partir daqui'; + String get continueFromHere => 'Continuar daqui'; @override String get toStudy => 'Estudo'; @override - String get importGame => 'Importar uma partida'; + String get importGame => 'Importar partida'; @override - String get importGameExplanation => 'Coloca aqui o PGN de um jogo, para teres acesso a navegar pela repetição,\nanálise de computador, sala de chat do jogo e link de partilha.'; + String get importGameExplanation => 'Após colar uma partida em PGN você poderá revisá-la interativamente, consultar uma análise de computador, utilizar o chat e compartilhar um link.'; @override - String get importGameCaveat => 'As variações serão apagadas. Para mantê-las, importe o PGN através de um estudo.'; + String get importGameCaveat => 'As variantes serão apagadas. Para salvá-las, importe o PGN em um estudo.'; @override - String get importGameDataPrivacyWarning => 'Este PGN pode ser acessada pelo público. Para importar um jogo de forma privada, use um estudo.'; + String get importGameDataPrivacyWarning => 'Este PGN pode ser acessado publicamente. Use um estudo para importar um jogo privado.'; @override - String get thisIsAChessCaptcha => 'Este é um \"CAPTCHA\" de xadrez.'; + String get thisIsAChessCaptcha => 'Este é um CAPTCHA enxadrístico.'; @override - String get clickOnTheBoardToMakeYourMove => 'Clica no tabuleiro para fazeres a tua jogada, provando que és humano.'; + String get clickOnTheBoardToMakeYourMove => 'Clique no tabuleiro para fazer seu lance, provando que é humano.'; @override - String get captcha_fail => 'Por favor, resolve o captcha.'; + String get captcha_fail => 'Por favor, resolva o captcha enxadrístico.'; @override - String get notACheckmate => 'Não é xeque-mate.'; + String get notACheckmate => 'Não é xeque-mate'; @override - String get whiteCheckmatesInOneMove => 'As brancas dão mate em um movimento'; + String get whiteCheckmatesInOneMove => 'As brancas dão mate em um lance'; @override - String get blackCheckmatesInOneMove => 'As pretas dão mate em um movimento'; + String get blackCheckmatesInOneMove => 'As pretas dão mate em um lance'; @override String get retry => 'Tentar novamente'; @@ -2263,7 +2281,7 @@ class AppLocalizationsPt extends AppLocalizations { String get reconnecting => 'Reconectando'; @override - String get noNetwork => 'Desligado'; + String get noNetwork => 'Sem conexão'; @override String get favoriteOpponents => 'Adversários favoritos'; @@ -2272,10 +2290,10 @@ class AppLocalizationsPt extends AppLocalizations { String get follow => 'Seguir'; @override - String get following => 'A seguir'; + String get following => 'Seguindo'; @override - String get unfollow => 'Deixar de seguir'; + String get unfollow => 'Parar de seguir'; @override String followX(String param) { @@ -2297,7 +2315,7 @@ class AppLocalizationsPt extends AppLocalizations { String get unblock => 'Desbloquear'; @override - String get followsYou => 'Segue-te'; + String get followsYou => 'Segue você'; @override String xStartedFollowingY(String param1, String param2) { @@ -2337,13 +2355,13 @@ class AppLocalizationsPt extends AppLocalizations { String get winner => 'Vencedor'; @override - String get standing => 'Classificação'; + String get standing => 'Colocação'; @override - String get createANewTournament => 'Criar um torneio'; + String get createANewTournament => 'Criar novo torneio'; @override - String get tournamentCalendar => 'Calendário de torneios'; + String get tournamentCalendar => 'Calendário do torneio'; @override String get conditionOfEntry => 'Condições de participação:'; @@ -2352,16 +2370,16 @@ class AppLocalizationsPt extends AppLocalizations { String get advancedSettings => 'Configurações avançadas'; @override - String get safeTournamentName => 'Escolhe um nome totalmente seguro para o torneio.'; + String get safeTournamentName => 'Escolha um nome seguro para o torneio.'; @override - String get inappropriateNameWarning => 'Até uma linguagem ligeiramente inadequada pode levar ao encerramento da tua conta.'; + String get inappropriateNameWarning => 'Até mesmo a menor indecência poderia ensejar o encerramento de sua conta.'; @override - String get emptyTournamentName => 'Deixe em branco e será atribuído um nome aleatório de um jogador notável ao torneio.'; + String get emptyTournamentName => 'Deixe em branco para dar ao torneio o nome de um grande mestre aleatório.'; @override - String get makePrivateTournament => 'Torna o torneio privado e restrinje o acesso com uma palavra-passe'; + String get makePrivateTournament => 'Faça o torneio privado e restrinja o acesso com uma senha'; @override String get join => 'Entrar'; @@ -2382,71 +2400,71 @@ class AppLocalizationsPt extends AppLocalizations { String get createdBy => 'Criado por'; @override - String get tournamentIsStarting => 'O torneio está a começar'; + String get tournamentIsStarting => 'O torneio está começando'; @override - String get tournamentPairingsAreNowClosed => 'Os emparelhamentos no torneio já estão fechados.'; + String get tournamentPairingsAreNowClosed => 'Os pareamentos do torneio estão fechados agora.'; @override String standByX(String param) { - return 'Aguarda $param, estamos a emparelhar jogadores, prepara-te!'; + return '$param, aguarde: o pareamento está em andamento, prepare-se!'; } @override - String get pause => 'Pausa'; + String get pause => 'Pausar'; @override String get resume => 'Continuar'; @override - String get youArePlaying => 'Estás a jogar!'; + String get youArePlaying => 'Você está participando!'; @override String get winRate => 'Taxa de vitórias'; @override - String get berserkRate => 'Taxa de partidas no modo frenético'; + String get berserkRate => 'Taxa Berserk'; @override String get performance => 'Desempenho'; @override - String get tournamentComplete => 'Torneio terminado'; + String get tournamentComplete => 'Torneio completo'; @override - String get movesPlayed => 'Movimentos feitos'; + String get movesPlayed => 'Movimentos realizados'; @override - String get whiteWins => 'Vitórias com as brancas'; + String get whiteWins => 'Brancas venceram'; @override - String get blackWins => 'Vitórias com as pretas'; + String get blackWins => 'Pretas venceram'; @override - String get drawRate => 'Taxa de empate'; + String get drawRate => 'Taxa de empates'; @override String get draws => 'Empates'; @override String nextXTournament(String param) { - return 'Próximo torneio de $param:'; + return 'Próximo torneio $param:'; } @override - String get averageOpponent => 'Pontuação média dos adversários'; + String get averageOpponent => 'Pontuação média adversários'; @override String get boardEditor => 'Editor de tabuleiro'; @override - String get setTheBoard => 'Partilha o tabuleiro'; + String get setTheBoard => 'Defina a posição'; @override String get popularOpenings => 'Aberturas populares'; @override - String get endgamePositions => 'Posições finais'; + String get endgamePositions => 'Posições de final'; @override String chess960StartPosition(String param) { @@ -2457,10 +2475,10 @@ class AppLocalizationsPt extends AppLocalizations { String get startPosition => 'Posição inicial'; @override - String get clearBoard => 'Limpar o tabuleiro'; + String get clearBoard => 'Limpar tabuleiro'; @override - String get loadPosition => 'Carregar uma posição'; + String get loadPosition => 'Carregar posição'; @override String get isPrivate => 'Privado'; @@ -2472,34 +2490,34 @@ class AppLocalizationsPt extends AppLocalizations { @override String profileCompletion(String param) { - return 'Perfil completo: $param'; + return 'Conclusão do perfil: $param'; } @override String xRating(String param) { - return 'Pontuação $param'; + return 'Rating $param'; } @override - String get ifNoneLeaveEmpty => 'Se não existir, deixa em branco'; + String get ifNoneLeaveEmpty => 'Se nenhuma, deixe vazio'; @override String get profile => 'Perfil'; @override - String get editProfile => 'Editar o perfil'; + String get editProfile => 'Editar perfil'; @override - String get realName => 'Nome Real'; + String get realName => 'Nome real'; @override - String get setFlair => 'Defina o teu estilo'; + String get setFlair => 'Escolha seu emote'; @override String get flair => 'Estilo'; @override - String get youCanHideFlair => 'Há uma opção para ocultar todos os estilos dos utilizadores em todo o site.'; + String get youCanHideFlair => 'Você pode esconder todos os emotes de usuário no site.'; @override String get biography => 'Biografia'; @@ -2511,22 +2529,22 @@ class AppLocalizationsPt extends AppLocalizations { String get thankYou => 'Obrigado!'; @override - String get socialMediaLinks => 'Links das redes sociais'; + String get socialMediaLinks => 'Links de mídia social'; @override - String get oneUrlPerLine => 'Um URL por linha.'; + String get oneUrlPerLine => 'Uma URL por linha.'; @override - String get inlineNotation => 'Anotações em linha'; + String get inlineNotation => 'Notação em linha'; @override - String get makeAStudy => 'Para guardar e partilhar, considere fazer um estudo.'; + String get makeAStudy => 'Para salvar e compartilhar uma análise, crie um estudo.'; @override - String get clearSavedMoves => 'Limpar jogadas'; + String get clearSavedMoves => 'Limpar lances'; @override - String get previouslyOnLichessTV => 'Anteriormente na TV Lichess'; + String get previouslyOnLichessTV => 'Anteriormente em Lichess TV'; @override String get onlinePlayers => 'Jogadores online'; @@ -2535,19 +2553,19 @@ class AppLocalizationsPt extends AppLocalizations { String get activePlayers => 'Jogadores ativos'; @override - String get bewareTheGameIsRatedButHasNoClock => 'Cuidado, o jogo vale pontos, mas não há limite de tempo!'; + String get bewareTheGameIsRatedButHasNoClock => 'Cuidado, o jogo vale rating, mas não há controle de tempo!'; @override String get success => 'Sucesso'; @override - String get automaticallyProceedToNextGameAfterMoving => 'Passar automaticamente ao jogo seguinte após a jogada'; + String get automaticallyProceedToNextGameAfterMoving => 'Passar automaticamente ao jogo seguinte após o lance'; @override String get autoSwitch => 'Alternar automaticamente'; @override - String get puzzles => 'Problemas'; + String get puzzles => 'Quebra-cabeças'; @override String get onlineBots => 'Bots online'; @@ -2562,7 +2580,7 @@ class AppLocalizationsPt extends AppLocalizations { String get descPrivate => 'Descrição privada'; @override - String get descPrivateHelp => 'Texto que apenas está visível para os membros da equipa. Se definido, substitui a descrição pública dos membros da equipa.'; + String get descPrivateHelp => 'Texto que apenas os membros da equipe verão. Se definido, substitui a descrição pública para os membros da equipe.'; @override String get no => 'Não'; @@ -2574,7 +2592,7 @@ class AppLocalizationsPt extends AppLocalizations { String get help => 'Ajuda:'; @override - String get createANewTopic => 'Criar um novo tópico'; + String get createANewTopic => 'Criar novo tópico'; @override String get topics => 'Tópicos'; @@ -2583,7 +2601,7 @@ class AppLocalizationsPt extends AppLocalizations { String get posts => 'Publicações'; @override - String get lastPost => 'Última publicação'; + String get lastPost => 'Última postagem'; @override String get views => 'Visualizações'; @@ -2601,13 +2619,13 @@ class AppLocalizationsPt extends AppLocalizations { String get message => 'Mensagem'; @override - String get createTheTopic => 'Criar o tópico'; + String get createTheTopic => 'Criar tópico'; @override - String get reportAUser => 'Denunciar um utilizador'; + String get reportAUser => 'Reportar um usuário'; @override - String get user => 'Utilizador'; + String get user => 'Usuário'; @override String get reason => 'Motivo'; @@ -2616,7 +2634,7 @@ class AppLocalizationsPt extends AppLocalizations { String get whatIsIheMatter => 'Qual é o motivo?'; @override - String get cheat => 'Batota'; + String get cheat => 'Trapaça'; @override String get troll => 'Troll'; @@ -2625,10 +2643,10 @@ class AppLocalizationsPt extends AppLocalizations { String get other => 'Outro'; @override - String get reportDescriptionHelp => 'Inclui o link do(s) jogo(s) e explica o que há de errado com o comportamento deste utilizador. Não digas apenas \"ele faz batota\"; informa-nos como chegaste a essa conclusão. A tua denúncia será processada mais rapidamente se for escrita em inglês.'; + String get reportDescriptionHelp => 'Cole o link do(s) jogo(s) e explique o que há de errado com o comportamento do usuário. Não diga apenas \"ele trapaceia\", informe-nos como chegou a esta conclusão. Sua denúncia será processada mais rapidamente se escrita em inglês.'; @override - String get error_provideOneCheatedGameLink => 'Por favor, fornece-nos pelo menos um link para um jogo onde tenha havido batota.'; + String get error_provideOneCheatedGameLink => 'Por favor forneça ao menos um link para um jogo com suspeita de trapaça.'; @override String by(String param) { @@ -2641,7 +2659,7 @@ class AppLocalizationsPt extends AppLocalizations { } @override - String get thisTopicIsNowClosed => 'Este tópico foi fechado.'; + String get thisTopicIsNowClosed => 'O tópico foi fechado.'; @override String get blog => 'Blog'; @@ -2650,46 +2668,46 @@ class AppLocalizationsPt extends AppLocalizations { String get notes => 'Notas'; @override - String get typePrivateNotesHere => 'Escreve notas privadas aqui'; + String get typePrivateNotesHere => 'Digite notas pessoais aqui'; @override - String get writeAPrivateNoteAboutThisUser => 'Escreva uma nota privada sobre este utilizador'; + String get writeAPrivateNoteAboutThisUser => 'Escreva uma nota pessoal sobre este usuário'; @override - String get noNoteYet => 'Ainda sem notas'; + String get noNoteYet => 'Nenhuma nota'; @override - String get invalidUsernameOrPassword => 'Nome de utilizador ou palavra-passe incorretos'; + String get invalidUsernameOrPassword => 'Nome de usuário ou senha incorretos'; @override - String get incorrectPassword => 'Palavra-passe incorreta'; + String get incorrectPassword => 'Senha incorreta'; @override - String get invalidAuthenticationCode => 'Código de autenticação inválido'; + String get invalidAuthenticationCode => 'Código de verificação inválido'; @override - String get emailMeALink => 'Envie-me um link por e-mail'; + String get emailMeALink => 'Me envie um link'; @override - String get currentPassword => 'Palavra-passe atual'; + String get currentPassword => 'Senha atual'; @override - String get newPassword => 'Nova palavra-chave'; + String get newPassword => 'Nova senha'; @override - String get newPasswordAgain => 'Nova palavra-passe (novamente)'; + String get newPasswordAgain => 'Nova senha (novamente)'; @override - String get newPasswordsDontMatch => 'As novas palavras-passe não coincidem'; + String get newPasswordsDontMatch => 'As novas senhas não correspondem'; @override - String get newPasswordStrength => 'Força da palavra-passe'; + String get newPasswordStrength => 'Senha forte'; @override - String get clockInitialTime => 'Tempo inicial no relógio'; + String get clockInitialTime => 'Tempo de relógio'; @override - String get clockIncrement => 'Incremento no relógio'; + String get clockIncrement => 'Incremento do relógio'; @override String get privacy => 'Privacidade'; @@ -2698,13 +2716,13 @@ class AppLocalizationsPt extends AppLocalizations { String get privacyPolicy => 'Política de privacidade'; @override - String get letOtherPlayersFollowYou => 'Permitir que outros jogadores te sigam'; + String get letOtherPlayersFollowYou => 'Permitir que outros jogadores sigam você'; @override - String get letOtherPlayersChallengeYou => 'Permitir que outros jogadores te desafiem'; + String get letOtherPlayersChallengeYou => 'Permitir que outros jogadores desafiem você'; @override - String get letOtherPlayersInviteYouToStudy => 'Permitir que outros jogadores te convidem para estudos'; + String get letOtherPlayersInviteYouToStudy => 'Deixe outros jogadores convidá-lo para um estudo'; @override String get sound => 'Som'; @@ -2731,7 +2749,7 @@ class AppLocalizationsPt extends AppLocalizations { String get allSquaresOfTheBoard => 'Todas as casas do tabuleiro'; @override - String get onSlowGames => 'Em jogos lentos'; + String get onSlowGames => 'Em partidas lentas'; @override String get always => 'Sempre'; @@ -2752,33 +2770,33 @@ class AppLocalizationsPt extends AppLocalizations { @override String victoryVsYInZ(String param1, String param2, String param3) { - return '$param1 contra $param2 em $param3'; + return '$param1 vs $param2 em $param3'; } @override String defeatVsYInZ(String param1, String param2, String param3) { - return '$param1 contra $param2 em $param3'; + return '$param1 vs $param2 em $param3'; } @override String drawVsYInZ(String param1, String param2, String param3) { - return '$param1 contra $param2 em $param3'; + return '$param1 vs $param2 em $param3'; } @override - String get timeline => 'Cronologia'; + String get timeline => 'Linha do tempo'; @override - String get starting => 'Começa às:'; + String get starting => 'Iniciando:'; @override String get allInformationIsPublicAndOptional => 'Todas as informações são públicas e opcionais.'; @override - String get biographyDescription => 'Fala de ti, do que gostas no xadrez, das tuas aberturas favoritas, jogos, jogadores...'; + String get biographyDescription => 'Fale sobre você, seus interesses, o que você gosta no xadrez, suas aberturas favoritas, jogadores...'; @override - String get listBlockedPlayers => 'Lista os jogadores que bloqueaste'; + String get listBlockedPlayers => 'Sua lista de jogadores bloqueados'; @override String get human => 'Humano'; @@ -2799,7 +2817,7 @@ class AppLocalizationsPt extends AppLocalizations { String get learnMenu => 'Aprender'; @override - String get studyMenu => 'Estudos'; + String get studyMenu => 'Estudar'; @override String get practice => 'Praticar'; @@ -2817,50 +2835,50 @@ class AppLocalizationsPt extends AppLocalizations { String get error_unknown => 'Valor inválido'; @override - String get error_required => 'Este campo tem de ser preenchido'; + String get error_required => 'Este campo deve ser preenchido'; @override String get error_email => 'Este endereço de e-mail é inválido'; @override - String get error_email_acceptable => 'Este endereço de e-mail não é aceitável. Por favor verifica-o e tenta outra vez.'; + String get error_email_acceptable => 'Este endereço de e-mail não é válido. Verifique e tente novamente.'; @override - String get error_email_unique => 'Endereço de e-mail inválido ou já utilizado'; + String get error_email_unique => 'Endereço de e-mail é inválido ou já está sendo utilizado'; @override - String get error_email_different => 'Este já é o teu endereço de e-mail'; + String get error_email_different => 'Este já é o seu endereço de e-mail'; @override String error_minLength(String param) { - return 'Deve conter pelo menos $param caracteres'; + return 'O mínimo de caracteres é $param'; } @override String error_maxLength(String param) { - return 'Deve conter no máximo $param caracteres'; + return 'O máximo de caracteres é $param'; } @override String error_min(String param) { - return 'Deve ser pelo menos $param'; + return 'Deve ser maior ou igual a $param'; } @override String error_max(String param) { - return 'Deve ser no máximo $param'; + return 'Deve ser menor ou igual a $param'; } @override String ifRatingIsPlusMinusX(String param) { - return 'Se a pontuação for ± $param'; + return 'Se o rating for ± $param'; } @override - String get ifRegistered => 'Se registado'; + String get ifRegistered => 'Se registrado'; @override - String get onlyExistingConversations => 'Apenas conversas existentes'; + String get onlyExistingConversations => 'Apenas conversas iniciadas'; @override String get onlyFriends => 'Apenas amigos'; @@ -2872,35 +2890,35 @@ class AppLocalizationsPt extends AppLocalizations { String get castling => 'Roque'; @override - String get whiteCastlingKingside => 'Brancas O-O'; + String get whiteCastlingKingside => 'O-O das brancas'; @override - String get blackCastlingKingside => 'Pretas O-O'; + String get blackCastlingKingside => 'O-O das pretas'; @override String tpTimeSpentPlaying(String param) { - return 'Tempo passado a jogar: $param'; + return 'Tempo jogando: $param'; } @override - String get watchGames => 'Ver jogos'; + String get watchGames => 'Assistir partidas'; @override String tpTimeSpentOnTV(String param) { - return 'Tempo a ser transmitido na TV: $param'; + return 'Tempo na TV: $param'; } @override - String get watch => 'Observar'; + String get watch => 'Assistir'; @override - String get videoLibrary => 'Videoteca'; + String get videoLibrary => 'Vídeos'; @override String get streamersMenu => 'Streamers'; @override - String get mobileApp => 'Aplicação móvel'; + String get mobileApp => 'Aplicativo Móvel'; @override String get webmasters => 'Webmasters'; @@ -2915,11 +2933,11 @@ class AppLocalizationsPt extends AppLocalizations { @override String xIsAFreeYLibreOpenSourceChessServer(String param1, String param2) { - return 'O $param1 é um servidor de xadrez grátis ($param2), sem publicidades e open-source.'; + return '$param1 é um servidor de xadrez gratuito ($param2), livre, sem anúncios e código aberto.'; } @override - String get really => 'a sério'; + String get really => 'realmente'; @override String get contribute => 'Contribuir'; @@ -2928,17 +2946,17 @@ class AppLocalizationsPt extends AppLocalizations { String get termsOfService => 'Termos de serviço'; @override - String get sourceCode => 'Código fonte'; + String get sourceCode => 'Código-fonte'; @override - String get simultaneousExhibitions => 'Exibições simultâneas'; + String get simultaneousExhibitions => 'Exibição simultânea'; @override - String get host => 'Anfitrião'; + String get host => 'Simultanista'; @override String hostColorX(String param) { - return 'Cor do anfitrião: $param'; + return 'Cor do simultanista: $param'; } @override @@ -2948,10 +2966,10 @@ class AppLocalizationsPt extends AppLocalizations { String get createdSimuls => 'Simultâneas criadas recentemente'; @override - String get hostANewSimul => 'Iniciar uma simultânea'; + String get hostANewSimul => 'Iniciar nova simultânea'; @override - String get signUpToHostOrJoinASimul => 'Registra-te para hospedar ou juntar a uma simultânea'; + String get signUpToHostOrJoinASimul => 'Entre em uma ou crie uma conta para hospedar'; @override String get noSimulFound => 'Simultânea não encontrada'; @@ -2960,88 +2978,88 @@ class AppLocalizationsPt extends AppLocalizations { String get noSimulExplanation => 'Esta exibição simultânea não existe.'; @override - String get returnToSimulHomepage => 'Voltar à página inicial da simultânea'; + String get returnToSimulHomepage => 'Retornar à página inicial da simultânea'; @override - String get aboutSimul => 'As simultâneas envolvem um único jogador contra vários adversários ao mesmo tempo.'; + String get aboutSimul => 'A simultânea envolve um único jogador contra vários oponentes ao mesmo tempo.'; @override - String get aboutSimulImage => 'Contra 50 adversários, Fischer ganhou 47 jogos, empatou 2 e perdeu 1.'; + String get aboutSimulImage => 'Contra 50 oponentes, Fischer ganhou 47 jogos, empatou 2 e perdeu 1.'; @override - String get aboutSimulRealLife => 'O conceito provém de eventos reais, nos quais o simultanista se move de mesa em mesa, executando um movimento de cada vez.'; + String get aboutSimulRealLife => 'O conceito provém de eventos reais, nos quais o simultanista se move de mesa em mesa, executando um movimento por vez.'; @override - String get aboutSimulRules => 'Quando a simultânea começa, cada jogador começa sua partida contra o anfitrião, que joga sempre com as peças brancas. A simultânea termina quando todas as partidas tiverem acabado.'; + String get aboutSimulRules => 'Quando a simultânea começa, cada jogador começa sua partida contra o simultanista, o qual sempre tem as brancas. A simultânea termina quando todas as partidas são finalizadas.'; @override - String get aboutSimulSettings => 'As simultâneas são sempre partidas amigáveis. Desforras, voltar jogadas atrás e dar mais tempo estão desativados.'; + String get aboutSimulSettings => 'As simultâneas sempre são partidas amigáveis. Revanches, voltar jogadas e tempo adicional estão desativados.'; @override String get create => 'Criar'; @override - String get whenCreateSimul => 'Quando crias uma simultânea, podes jogar com vários adversários ao mesmo tempo.'; + String get whenCreateSimul => 'Quando cria uma simultânea, você joga com vários adversários ao mesmo tempo.'; @override - String get simulVariantsHint => 'Se selecionares diversas variantes, cada jogador poderá escolher qual delas jogar.'; + String get simulVariantsHint => 'Se você selecionar diversas variantes, cada jogador poderá escolher qual delas jogar.'; @override - String get simulClockHint => 'Configuração de incrementos no relógio. Quanto mais jogadores admitires, mais tempo poderás necessitar.'; + String get simulClockHint => 'Configuração de acréscimos no relógio. Quanto mais jogadores admitir, mais tempo pode necessitar.'; @override - String get simulAddExtraTime => 'Podes acrescentar tempo adicional ao teu relógio, para te ajudar a lidar com a simultânea.'; + String get simulAddExtraTime => 'Você pode acrescentar tempo adicional a seu relógio, para ajudá-lo a lidar com a simultânea.'; @override - String get simulHostExtraTime => 'Tempo adicional do anfitrião'; + String get simulHostExtraTime => 'Tempo adicional do simultanista'; @override - String get simulAddExtraTimePerPlayer => 'Adicione tempo inicial ao seu relógio para cada jogador que entra na simulação.'; + String get simulAddExtraTimePerPlayer => 'Adicionar tempo inicial ao seu relógio por cada jogador adversário que entrar na simultânea.'; @override - String get simulHostExtraTimePerPlayer => 'Tempo extra de relógio por jogador'; + String get simulHostExtraTimePerPlayer => 'Tempo adicional do simultanista por jogador'; @override String get lichessTournaments => 'Torneios do Lichess'; @override - String get tournamentFAQ => 'Perguntas frequentes sobre torneios em arena'; + String get tournamentFAQ => 'Perguntas Frequentes sobre torneios no estilo Arena'; @override - String get timeBeforeTournamentStarts => 'Contagem decrescente para o início do torneio'; + String get timeBeforeTournamentStarts => 'Contagem regressiva para início do torneio'; @override - String get averageCentipawnLoss => 'Perda média de centésimos de peão'; + String get averageCentipawnLoss => 'Perda média em centipeões'; @override String get accuracy => 'Precisão'; @override - String get keyboardShortcuts => 'Atalhos do teclado'; + String get keyboardShortcuts => 'Atalhos de teclado'; @override - String get keyMoveBackwardOrForward => 'retroceder/avançar jogada'; + String get keyMoveBackwardOrForward => 'retroceder/avançar lance'; @override String get keyGoToStartOrEnd => 'ir para início/fim'; @override - String get keyCycleSelectedVariation => 'Ciclo da variante selecionada'; + String get keyCycleSelectedVariation => 'Alternar entre as variantes'; @override - String get keyShowOrHideComments => 'mostrar/ocultar os comentários'; + String get keyShowOrHideComments => 'mostrar/ocultar comentários'; @override String get keyEnterOrExitVariation => 'entrar/sair da variante'; @override - String get keyRequestComputerAnalysis => 'Solicite análise do computador, Aprenda com seus erros'; + String get keyRequestComputerAnalysis => 'Solicite análise do computador, aprenda com seus erros'; @override - String get keyNextLearnFromYourMistakes => 'Seguinte (Aprenda com os seus erros)'; + String get keyNextLearnFromYourMistakes => 'Próximo (Aprenda com seus erros)'; @override - String get keyNextBlunder => 'Próxima gafe'; + String get keyNextBlunder => 'Próximo erro grave'; @override String get keyNextMistake => 'Próximo erro'; @@ -3050,19 +3068,19 @@ class AppLocalizationsPt extends AppLocalizations { String get keyNextInaccuracy => 'Próxima imprecisão'; @override - String get keyPreviousBranch => 'Ramo anterior'; + String get keyPreviousBranch => 'Branch anterior'; @override - String get keyNextBranch => 'Próximo ramo'; + String get keyNextBranch => 'Próximo branch'; @override - String get toggleVariationArrows => 'Ativar/desactivar seta da variante'; + String get toggleVariationArrows => 'Ativar/desativar setas'; @override - String get cyclePreviousOrNextVariation => 'Ciclo anterior/próxima variante'; + String get cyclePreviousOrNextVariation => 'Variante seguinte/anterior'; @override - String get toggleGlyphAnnotations => 'Ativar/desativar anotações com símbolos'; + String get toggleGlyphAnnotations => 'Ativar/desativar anotações'; @override String get togglePositionAnnotations => 'Ativar/desativar anotações de posição'; @@ -3071,16 +3089,16 @@ class AppLocalizationsPt extends AppLocalizations { String get variationArrowsInfo => 'Setas de variação permitem navegar sem usar a lista de movimentos.'; @override - String get playSelectedMove => 'jogar o movimento selecionado'; + String get playSelectedMove => 'jogar movimento selecionado'; @override String get newTournament => 'Novo torneio'; @override - String get tournamentHomeTitle => 'Torneios de xadrez com diversos ritmos de jogo e variantes'; + String get tournamentHomeTitle => 'Torneios de xadrez com diversos controles de tempo e variantes'; @override - String get tournamentHomeDescription => 'Joga xadrez em ritmo acelerado! Entra num torneio oficial agendado ou cria o teu próprio. Bullet, Rápida, Clássica, Chess960, Rei da Colina, Três Xeques e mais opções disponíveis para uma diversão ilimitada.'; + String get tournamentHomeDescription => 'Jogue xadrez em ritmo acelerado! Entre em um torneio oficial agendado ou crie seu próprio. Bullet, Blitz, Clássico, Chess960, King of the Hill, Três Xeques e outras modalidades disponíveis para uma ilimitada diversão enxadrística.'; @override String get tournamentNotFound => 'Torneio não encontrado'; @@ -3089,109 +3107,109 @@ class AppLocalizationsPt extends AppLocalizations { String get tournamentDoesNotExist => 'Este torneio não existe.'; @override - String get tournamentMayHaveBeenCanceled => 'O torneio pode ter sido cancelado, se todos os jogadores tiverem saíram antes do seu começo.'; + String get tournamentMayHaveBeenCanceled => 'O evento pode ter sido cancelado, se todos os jogadores saíram antes de seu início.'; @override - String get returnToTournamentsHomepage => 'Voltar à página inicial de torneios'; + String get returnToTournamentsHomepage => 'Volte à página inicial de torneios'; @override String weeklyPerfTypeRatingDistribution(String param) { - return 'Distribuição semanal de pontuação em $param'; + return 'Distribuição mensal de rating em $param'; } @override String yourPerfTypeRatingIsRating(String param1, String param2) { - return 'A tua pontuação em $param1 é $param2.'; + return 'Seu rating em $param1 é $param2.'; } @override String youAreBetterThanPercentOfPerfTypePlayers(String param1, String param2) { - return 'És melhor que $param1 dos jogadores de $param2.'; + return 'Você é melhor que $param1 dos jogadores de $param2.'; } @override String userIsBetterThanPercentOfPerfTypePlayers(String param1, String param2, String param3) { - return '$param1 é melhor que $param2 dos jogadores de $param3.'; + return '$param1 é melhor que $param2 dos $param3 jogadores.'; } @override String betterThanPercentPlayers(String param1, String param2) { - return 'Melhor que $param1 de $param2 jogadores'; + return 'Melhor que $param1 dos jogadores de $param2'; } @override String youDoNotHaveAnEstablishedPerfTypeRating(String param) { - return 'Não tens uma pontuação estabelecida em $param.'; + return 'Você não tem rating definido em $param.'; } @override - String get yourRating => 'A tua pontuação'; + String get yourRating => 'Seu rating'; @override - String get cumulative => 'Acumulativo'; + String get cumulative => 'Cumulativo'; @override - String get glicko2Rating => 'Pontuação Glicko-2'; + String get glicko2Rating => 'Rating Glicko-2'; @override - String get checkYourEmail => 'Verifica o teu e-mail'; + String get checkYourEmail => 'Verifique seu e-mail'; @override - String get weHaveSentYouAnEmailClickTheLink => 'Enviámos-te um e-mail. Clica no link nesse e-mail para ativares a tua conta.'; + String get weHaveSentYouAnEmailClickTheLink => 'Enviamos um e-mail. Clique no link do e-mail para ativar sua conta.'; @override - String get ifYouDoNotSeeTheEmailCheckOtherPlaces => 'Se você não vires o e-mail, verifica outros locais onde este possa estar, como pastas de lixo, spam, social ou outras.'; + String get ifYouDoNotSeeTheEmailCheckOtherPlaces => 'Se você não vir o e-mail, verifique outros locais onde possa estar, como lixeira, spam ou outras pastas.'; @override String weHaveSentYouAnEmailTo(String param) { - return 'Enviámos-te um e-mail para $param. Clica no link nesse e-mail para redefinires a tua palavra-passe.'; + return 'Enviamos um e-mail para $param. Clique no link do e-mail para redefinir sua senha.'; } @override String byRegisteringYouAgreeToBeBoundByOur(String param) { - return 'Ao criares uma conta, concordas comprometeres-te com os nossos $param.'; + return 'Ao registrar, você concorda em se comprometer com nossa $param.'; } @override String readAboutOur(String param) { - return 'Lê sobre a nossa $param.'; + return 'Leia sobre a nossa $param.'; } @override - String get networkLagBetweenYouAndLichess => 'Atraso na rede entre ti e o Lichess'; + String get networkLagBetweenYouAndLichess => 'Atraso na rede'; @override String get timeToProcessAMoveOnLichessServer => 'Tempo para processar um movimento no servidor do Lichess'; @override - String get downloadAnnotated => 'Transferir anotação'; + String get downloadAnnotated => 'Baixar anotação'; @override - String get downloadRaw => 'Transferir texto'; + String get downloadRaw => 'Baixar texto'; @override - String get downloadImported => 'Transferir a partida importada'; + String get downloadImported => 'Baixar partida importada'; @override String get crosstable => 'Tabela'; @override - String get youCanAlsoScrollOverTheBoardToMoveInTheGame => 'Também podes rodar a rodinha do rato sobre o tabuleiro para percorreres as jogadas na partida.'; + String get youCanAlsoScrollOverTheBoardToMoveInTheGame => 'Você também pode rolar sobre o tabuleiro para percorrer as jogadas.'; @override - String get scrollOverComputerVariationsToPreviewThem => 'Passe o rato sobre as variantes do computador para visualizá-las.'; + String get scrollOverComputerVariationsToPreviewThem => 'Passe o mouse pelas variações do computador para visualizá-las.'; @override - String get analysisShapesHowTo => 'Pressiona shift+clique ou clica com o botão direito do rato para desenhares círculos e setas no tabuleiro.'; + String get analysisShapesHowTo => 'Pressione Shift+Clique ou clique com o botão direito do mouse para desenhar círculos e setas no tabuleiro.'; @override - String get letOtherPlayersMessageYou => 'Permitir que outros jogadores te enviem mensagens'; + String get letOtherPlayersMessageYou => 'Permitir que outros jogadores lhe enviem mensagem'; @override - String get receiveForumNotifications => 'Olá aOlá a todos'; + String get receiveForumNotifications => 'Receba notificações quando você for mencionado no fórum'; @override - String get shareYourInsightsData => 'Compartilhar os teus dados de \"insights\" de xadrez'; + String get shareYourInsightsData => 'Compartilhe seus dados da análise'; @override String get withNobody => 'Com ninguém'; @@ -3206,24 +3224,24 @@ class AppLocalizationsPt extends AppLocalizations { String get kidMode => 'Modo infantil'; @override - String get kidModeIsEnabled => 'Modo infantil está ativado.'; + String get kidModeIsEnabled => 'O modo infantil está ativado.'; @override - String get kidModeExplanation => 'Iso é sobre segurança. No modo infantil, todas as comunicações do site ficam desactivadas. Activa esta opção para os teus filhos ou alunos, para protegê-los de outros utilizadores da internet.'; + String get kidModeExplanation => 'Isto diz respeito à segurança. No modo infantil, todas as comunicações do site são desabilitadas. Habilite isso para seus filhos e alunos, para protegê-los de outros usuários da Internet.'; @override String inKidModeTheLichessLogoGetsIconX(String param) { - return 'No modo criança, o logótipo do Lichess fica com um ícone $param para que saibas que as tuas crianças estão seguras.'; + return 'No modo infantil, a logo do lichess tem um ícone $param, para que você saiba que suas crianças estão seguras.'; } @override - String get askYourChessTeacherAboutLiftingKidMode => 'A sua conta é gerida. Peça ao seu professor de xadrez para retirar o modo infantil.'; + String get askYourChessTeacherAboutLiftingKidMode => 'Sua conta é gerenciada. Para desativar o modo infantil, peça ao seu professor.'; @override - String get enableKidMode => 'Ativar o modo infantil'; + String get enableKidMode => 'Habilitar o modo infantil'; @override - String get disableKidMode => 'Desativar o modo infantil'; + String get disableKidMode => 'Desabilitar o modo infantil'; @override String get security => 'Segurança'; @@ -3232,10 +3250,10 @@ class AppLocalizationsPt extends AppLocalizations { String get sessions => 'Sessões'; @override - String get revokeAllSessions => 'desativar todas as sessões'; + String get revokeAllSessions => 'revogar todas as sessões'; @override - String get playChessEverywhere => 'Joga xadrez em qualquer lugar'; + String get playChessEverywhere => 'Jogue xadrez em qualquer lugar'; @override String get asFreeAsLichess => 'Tão gratuito quanto o Lichess'; @@ -3244,7 +3262,7 @@ class AppLocalizationsPt extends AppLocalizations { String get builtForTheLoveOfChessNotMoney => 'Desenvolvido pelo amor ao xadrez, não pelo dinheiro'; @override - String get everybodyGetsAllFeaturesForFree => 'Todos têm todos os recursos gratuitamente'; + String get everybodyGetsAllFeaturesForFree => 'Todos têm todos os recursos de graça'; @override String get zeroAdvertisement => 'Zero anúncios'; @@ -3253,7 +3271,7 @@ class AppLocalizationsPt extends AppLocalizations { String get fullFeatured => 'Cheio de recursos'; @override - String get phoneAndTablet => 'Telemóvel e tablet'; + String get phoneAndTablet => 'Celular e tablet'; @override String get bulletBlitzClassical => 'Bullet, blitz, clássico'; @@ -3262,20 +3280,20 @@ class AppLocalizationsPt extends AppLocalizations { String get correspondenceChess => 'Xadrez por correspondência'; @override - String get onlineAndOfflinePlay => 'Jogar online e offline'; + String get onlineAndOfflinePlay => 'Jogue online e offline'; @override - String get viewTheSolution => 'Ver a solução'; + String get viewTheSolution => 'Ver solução'; @override - String get followAndChallengeFriends => 'Joga e desafia amigos'; + String get followAndChallengeFriends => 'Siga e desafie amigos'; @override String get gameAnalysis => 'Análise da partida'; @override String xHostsY(String param1, String param2) { - return '$param1 criou a exibição simultânea $param2'; + return '$param1 criou $param2'; } @override @@ -3289,24 +3307,24 @@ class AppLocalizationsPt extends AppLocalizations { } @override - String get quickPairing => 'Emparelhamento rápido'; + String get quickPairing => 'Pareamento rápido'; @override - String get lobby => 'Sala de espera'; + String get lobby => 'Salão'; @override String get anonymous => 'Anônimo'; @override String yourScore(String param) { - return 'O teu resultado: $param'; + return 'Sua pontuação:$param'; } @override - String get language => 'Lingua'; + String get language => 'Idioma'; @override - String get background => 'Fundo'; + String get background => 'Cor tema'; @override String get light => 'Claro'; @@ -3336,37 +3354,37 @@ class AppLocalizationsPt extends AppLocalizations { String get brightness => 'Brilho'; @override - String get hue => 'Tonalidade'; + String get hue => 'Tom'; @override - String get boardReset => 'Redefinir cores para o padrão'; + String get boardReset => 'Restaurar as cores padrão'; @override - String get pieceSet => 'Peças'; + String get pieceSet => 'Estilo das peças'; @override - String get embedInYourWebsite => 'Incorporar no teu site'; + String get embedInYourWebsite => 'Incorporar no seu site'; @override - String get usernameAlreadyUsed => 'Este nome de utilizador já existe, por favor escolhe outro.'; + String get usernameAlreadyUsed => 'Este nome de usuário já está registado, por favor, escolha outro.'; @override - String get usernamePrefixInvalid => 'O nome do utilizador tem começar com uma letra.'; + String get usernamePrefixInvalid => 'O nome de usuário deve começar com uma letra.'; @override - String get usernameSuffixInvalid => 'O nome do utilizador tem de acabar com uma letra ou número.'; + String get usernameSuffixInvalid => 'O nome de usuário deve terminar com uma letra ou um número.'; @override - String get usernameCharsInvalid => 'O nome do utilizador só pode conter letras, números, underscores ou hífenes.'; + String get usernameCharsInvalid => 'Nomes de usuário só podem conter letras, números, sublinhados e hifens.'; @override - String get usernameUnacceptable => 'Este nome de utilizador não é aceitável.'; + String get usernameUnacceptable => 'Este nome de usuário não é aceitável.'; @override - String get playChessInStyle => 'Jogar xadrez com estilo'; + String get playChessInStyle => 'Jogue xadrez com estilo'; @override - String get chessBasics => 'O básico do xadrez'; + String get chessBasics => 'Básicos do xadrez'; @override String get coaches => 'Treinadores'; @@ -3390,66 +3408,66 @@ class AppLocalizationsPt extends AppLocalizations { @override String perfRatingX(String param) { - return 'Pontuação: $param'; + return 'Rating: $param'; } @override - String get practiceWithComputer => 'Praticar com o computador'; + String get practiceWithComputer => 'Pratique com o computador'; @override String anotherWasX(String param) { - return 'Outro seria $param'; + return 'Um outro lance seria $param'; } @override String bestWasX(String param) { - return '$param seria melhor'; + return 'Melhor seria $param'; } @override - String get youBrowsedAway => 'Saíste'; + String get youBrowsedAway => 'Você navegou para longe'; @override - String get resumePractice => 'Continuar a prática'; + String get resumePractice => 'Retornar à prática'; @override - String get drawByFiftyMoves => 'O jogo foi empatado de acordo com a regra dos cinquenta lances.'; + String get drawByFiftyMoves => 'O jogo empatou pela regra dos cinquenta movimentos.'; @override - String get theGameIsADraw => 'O jogo é um empate.'; + String get theGameIsADraw => 'A partida terminou em empate.'; @override - String get computerThinking => 'Computador a pensar...'; + String get computerThinking => 'Computador pensando ...'; @override - String get seeBestMove => 'Ver o melhor movimento'; + String get seeBestMove => 'Veja o melhor lance'; @override - String get hideBestMove => 'Ocultar o melhor movimento'; + String get hideBestMove => 'Esconder o melhor lance'; @override String get getAHint => 'Obter uma dica'; @override - String get evaluatingYourMove => 'A analisar o teu movimento ...'; + String get evaluatingYourMove => 'Avaliando o seu movimento ...'; @override - String get whiteWinsGame => 'As brancas ganham'; + String get whiteWinsGame => 'Brancas vencem'; @override - String get blackWinsGame => 'As pretas ganham'; + String get blackWinsGame => 'Pretas vencem'; @override - String get learnFromYourMistakes => 'Aprende com os teus erros'; + String get learnFromYourMistakes => 'Aprenda com seus erros'; @override - String get learnFromThisMistake => 'Aprende com este erro'; + String get learnFromThisMistake => 'Aprenda com este erro'; @override - String get skipThisMove => 'Saltar este movimento'; + String get skipThisMove => 'Pular esse lance'; @override - String get next => 'Seguinte'; + String get next => 'Próximo'; @override String xWasPlayed(String param) { @@ -3457,49 +3475,49 @@ class AppLocalizationsPt extends AppLocalizations { } @override - String get findBetterMoveForWhite => 'Encontra um melhor movimento para as brancas'; + String get findBetterMoveForWhite => 'Encontrar o melhor lance para as Brancas'; @override - String get findBetterMoveForBlack => 'Encontra um melhor movimento para as pretas'; + String get findBetterMoveForBlack => 'Encontre o melhor lance para as Pretas'; @override String get resumeLearning => 'Continuar a aprendizagem'; @override - String get youCanDoBetter => 'Podes fazer melhor'; + String get youCanDoBetter => 'Você pode fazer melhor'; @override - String get tryAnotherMoveForWhite => 'Tenta outro movimento para as brancas'; + String get tryAnotherMoveForWhite => 'Tente um outro lance para as Brancas'; @override - String get tryAnotherMoveForBlack => 'Tenta outro movimento para as pretas'; + String get tryAnotherMoveForBlack => 'Tente um outro lance para as Pretas'; @override String get solution => 'Solução'; @override - String get waitingForAnalysis => 'A aguardar pela análise'; + String get waitingForAnalysis => 'Aguardando análise'; @override - String get noMistakesFoundForWhite => 'Não foram encontrados erros das brancas'; + String get noMistakesFoundForWhite => 'Nenhum erro encontrado para as Brancas'; @override - String get noMistakesFoundForBlack => 'Não foram encontrados erros das pretas'; + String get noMistakesFoundForBlack => 'Nenhum erro encontrado para as Pretas'; @override - String get doneReviewingWhiteMistakes => 'Terminada a revisão de erros das brancas'; + String get doneReviewingWhiteMistakes => 'Erros das brancas já revistos'; @override - String get doneReviewingBlackMistakes => 'Terminada a revisão de erros das pretas'; + String get doneReviewingBlackMistakes => 'Erros das pretas já revistos'; @override - String get doItAgain => 'Repetir'; + String get doItAgain => 'Faça novamente'; @override - String get reviewWhiteMistakes => 'Rever os erros das brancas'; + String get reviewWhiteMistakes => 'Rever erros das Brancas'; @override - String get reviewBlackMistakes => 'Rever os erros das pretas'; + String get reviewBlackMistakes => 'Rever erros das Pretas'; @override String get advantage => 'Vantagem'; @@ -3508,95 +3526,95 @@ class AppLocalizationsPt extends AppLocalizations { String get opening => 'Abertura'; @override - String get middlegame => 'Meio jogo'; + String get middlegame => 'Meio-jogo'; @override - String get endgame => 'Final de jogo'; + String get endgame => 'Finais'; @override - String get conditionalPremoves => 'Movimentos antecipados condicionais'; + String get conditionalPremoves => 'Pré-lances condicionais'; @override - String get addCurrentVariation => 'Adicionar a variante atual'; + String get addCurrentVariation => 'Adicionar a variação atual'; @override - String get playVariationToCreateConditionalPremoves => 'Joga uma variante para criares movimentos antecipados condicionais'; + String get playVariationToCreateConditionalPremoves => 'Jogar uma variação para criar pré-lances condicionais'; @override - String get noConditionalPremoves => 'Sem movimentos antecipados condicionais'; + String get noConditionalPremoves => 'Sem pré-lances condicionais'; @override String playX(String param) { - return 'Joga $param'; + return 'Jogar $param'; } @override - String get showUnreadLichessMessage => 'Recebestes uma mensagem privada do Lichess.'; + String get showUnreadLichessMessage => 'Você recebeu uma mensagem privada do Lichess.'; @override - String get clickHereToReadIt => 'Clica aqui para ler'; + String get clickHereToReadIt => 'Clique aqui para ler'; @override String get sorry => 'Desculpa :('; @override - String get weHadToTimeYouOutForAWhile => 'Tivemos de te banir por algum tempo.'; + String get weHadToTimeYouOutForAWhile => 'Tivemos de bloqueá-lo por um tempo.'; @override - String get why => 'Porquê?'; + String get why => 'Por quê?'; @override - String get pleasantChessExperience => 'Tencionamos proporcionar uma experiência de xadrez agradável a todos.'; + String get pleasantChessExperience => 'Buscamos oferecer uma experiência agradável de xadrez para todos.'; @override - String get goodPractice => 'Para isso, temos de nos assegurar que todos os jogadores seguem boas práticas.'; + String get goodPractice => 'Para isso, precisamos assegurar que nossos jogadores sigam boas práticas.'; @override - String get potentialProblem => 'Quando um potencial problema é detetado, exibimos esta mensagem.'; + String get potentialProblem => 'Quando um problema em potencial é detectado, nós mostramos esta mensagem.'; @override - String get howToAvoidThis => 'Como evitar isto?'; + String get howToAvoidThis => 'Como evitar isso?'; @override - String get playEveryGame => 'Joga todas as partidas que começares.'; + String get playEveryGame => 'Jogue todos os jogos que inicia.'; @override - String get tryToWin => 'Tenta ganhar (ou pelo menos empatar) todas as partida que jogares.'; + String get tryToWin => 'Tente vencer (ou pelo menos empatar) todos os jogos que jogar.'; @override - String get resignLostGames => 'Desiste em partidas perdidas (não deixes o tempo no relógio acabar).'; + String get resignLostGames => 'Conceda partidas perdidas (não deixe o relógio ir até ao fim).'; @override - String get temporaryInconvenience => 'Pedimos desculpa pelo incómodo temporário,'; + String get temporaryInconvenience => 'Pedimos desculpa pelo incômodo temporário,'; @override - String get wishYouGreatGames => 'e desejamos-te grandes jogos no lichess.org.'; + String get wishYouGreatGames => 'e desejamos-lhe grandes jogos em lichess.org.'; @override String get thankYouForReading => 'Obrigado pela leitura!'; @override - String get lifetimeScore => 'Pontuação desde sempre'; + String get lifetimeScore => 'Pontuação de todo o período'; @override - String get currentMatchScore => 'Pontuação atual'; + String get currentMatchScore => 'Pontuação da partida atual'; @override - String get agreementAssistance => 'Concordo que nunca recorrerei a assistência durante as minhas partidas (de um computador de xadrez, livro, base de dados ou outra pessoa).'; + String get agreementAssistance => 'Eu concordo que em momento algum receberei assistência durante os meus jogos (seja de um computador, livro, banco de dados ou outra pessoa).'; @override - String get agreementNice => 'Concordo que serei sempre repeitoso para os outros jogadores.'; + String get agreementNice => 'Eu concordo que serei sempre cortês com outros jogadores.'; @override String agreementMultipleAccounts(String param) { - return 'Concordo que não criarei várias contas (exceto pelas razões indicadas em $param).'; + return 'Eu concordo que não criarei múltiplas contas (exceto pelas razões indicadas em $param).'; } @override - String get agreementPolicy => 'Concordo que seguirei todas as políticas do Lichess.'; + String get agreementPolicy => 'Eu concordo que seguirei todas as normas do Lichess.'; @override - String get searchOrStartNewDiscussion => 'Pesquisa ou começa uma nova conversa'; + String get searchOrStartNewDiscussion => 'Procurar ou iniciar nova conversa'; @override String get edit => 'Editar'; @@ -3605,31 +3623,31 @@ class AppLocalizationsPt extends AppLocalizations { String get bullet => 'Bullet'; @override - String get blitz => 'Rápidas'; + String get blitz => 'Blitz'; @override - String get rapid => 'Semi-rápidas'; + String get rapid => 'Rápida'; @override - String get classical => 'Clássicas'; + String get classical => 'Clássico'; @override - String get ultraBulletDesc => 'Partidas incrivelmente rápidas: menos de 30 segundos'; + String get ultraBulletDesc => 'Jogos insanamente rápidos: menos de 30 segundos'; @override - String get bulletDesc => 'Partidas muito rápidas: menos de 3 minutos'; + String get bulletDesc => 'Jogos muito rápidos: menos de 3 minutos'; @override - String get blitzDesc => 'Partidas rápidas: 3 a 8 minutos'; + String get blitzDesc => 'Jogos rápidos: 3 a 8 minutos'; @override - String get rapidDesc => 'Partidas semi-rápidas: 8 a 25 minutos'; + String get rapidDesc => 'Jogos rápidos: 8 a 25 minutos'; @override - String get classicalDesc => 'Partidas clássicas: 25 minutos ou mais'; + String get classicalDesc => 'Jogos clássicos: 25 minutos ou mais'; @override - String get correspondenceDesc => 'Partidas por correspondência: um ou vários dias por lance'; + String get correspondenceDesc => 'Jogos por correspondência: um ou vários dias por lance'; @override String get puzzleDesc => 'Treinador de táticas de xadrez'; @@ -3639,91 +3657,91 @@ class AppLocalizationsPt extends AppLocalizations { @override String yourQuestionMayHaveBeenAnswered(String param1) { - return 'A tua pergunta pode já ter uma resposta $param1'; + return 'A sua pergunta pode já ter sido respondida $param1'; } @override - String get inTheFAQ => 'no F.A.Q. (perguntas frequentes).'; + String get inTheFAQ => 'no F.A.Q.'; @override String toReportSomeoneForCheatingOrBadBehavior(String param1) { - return 'Para reportares um utilizador por fazer batota ou por mau comportamento, $param1.'; + return 'Para denunciar um usuário por trapaças ou mau comportamento, $param1'; } @override - String get useTheReportForm => 'usa a ficha própria para o fazeres'; + String get useTheReportForm => 'use o formulário de denúncia'; @override String toRequestSupport(String param1) { - return 'Para solicitares suporte, $param1.'; + return 'Para solicitar ajuda, $param1'; } @override - String get tryTheContactPage => 'tenta a página de contacto'; + String get tryTheContactPage => 'tente a página de contato'; @override String makeSureToRead(String param1) { - return 'Certifique-se que lê $param1'; + return 'Certifique-se de ler $param1'; } @override - String get theForumEtiquette => 'a etiqueta do fórum'; + String get theForumEtiquette => 'as regras do fórum'; @override - String get thisTopicIsArchived => 'Este tópico foi arquivado e já não pode ser respondido.'; + String get thisTopicIsArchived => 'Este tópico foi arquivado e não pode mais ser respondido.'; @override String joinTheTeamXToPost(String param1) { - return 'Junta-te a $param1, para publicares neste fórum'; + return 'Junte-se a $param1 para publicar neste fórum'; } @override String teamNamedX(String param1) { - return 'equipa $param1'; + return 'Equipe $param1'; } @override - String get youCannotPostYetPlaySomeGames => 'Ainda não podes publicar nos fóruns. Joga alguns jogos!'; + String get youCannotPostYetPlaySomeGames => 'Você não pode publicar nos fóruns ainda. Jogue algumas partidas!'; @override - String get subscribe => 'Subscrever-se'; + String get subscribe => 'Seguir publicações'; @override - String get unsubscribe => 'Cancelar a subscrição'; + String get unsubscribe => 'Deixar de seguir publicações'; @override String mentionedYouInX(String param1) { - return 'foste mencionado em \"$param1\".'; + return 'mencionou você em \"$param1\".'; } @override String xMentionedYouInY(String param1, String param2) { - return '$param1 mencionou-te em \"$param2\".'; + return '$param1 mencionou você em \"$param2\".'; } @override String invitedYouToX(String param1) { - return 'convidou-te para \"$param1\".'; + return 'convidou você para \"$param1\".'; } @override String xInvitedYouToY(String param1, String param2) { - return '$param1 convidou-te para \"$param2\".'; + return '$param1 convidou você para \"$param2\".'; } @override - String get youAreNowPartOfTeam => 'Já fazes parte da equipa.'; + String get youAreNowPartOfTeam => 'Você agora faz parte da equipe.'; @override String youHaveJoinedTeamX(String param1) { - return 'Juntaste-te a \"$param1\".'; + return 'Você ingressou em \"$param1\".'; } @override - String get someoneYouReportedWasBanned => 'Alguém que denunciaste foi banido'; + String get someoneYouReportedWasBanned => 'Alguém que você denunciou foi banido'; @override - String get congratsYouWon => 'Parabéns! Ganhaste!'; + String get congratsYouWon => 'Parabéns, você venceu!'; @override String gameVsX(String param1) { @@ -3736,27 +3754,27 @@ class AppLocalizationsPt extends AppLocalizations { } @override - String get lostAgainstTOSViolator => 'Perdes-te contra alguém que violou as regras do Lichess'; + String get lostAgainstTOSViolator => 'Você perdeu rating para alguém que violou os termos de serviço do Lichess'; @override String refundXpointsTimeControlY(String param1, String param2) { - return 'Devolução: $param1 $param2 pontos de Elo.'; + return 'Reembolso: $param1 $param2 pontos de rating.'; } @override - String get timeAlmostUp => 'O tempo está quase a terminar!'; + String get timeAlmostUp => 'O tempo está quase acabando!'; @override String get clickToRevealEmailAddress => '[Clique para revelar o endereço de e-mail]'; @override - String get download => 'Transferir'; + String get download => 'Baixar'; @override - String get coachManager => 'Gestor de treinadores'; + String get coachManager => 'Configurações para professores'; @override - String get streamerManager => 'Gestor do streamer'; + String get streamerManager => 'Configurações para streamers'; @override String get cancelTournament => 'Cancelar o torneio'; @@ -3765,66 +3783,66 @@ class AppLocalizationsPt extends AppLocalizations { String get tournDescription => 'Descrição do torneio'; @override - String get tournDescriptionHelp => 'Quer dizer alguma coisa em especial aos participantes? Seja breve. Estão disponíveis ligações de Markdown: [name](https://url)'; + String get tournDescriptionHelp => 'Algo especial que você queira dizer aos participantes? Tente ser breve. Links em Markdown disponíveis: [name](https://url)'; @override - String get ratedFormHelp => 'Os jogos são classificados\ne afetam as avaliações dos jogadores'; + String get ratedFormHelp => 'Os jogos valem classificação\ne afetam o rating dos jogadores'; @override - String get onlyMembersOfTeam => 'Apenas membros da equipa'; + String get onlyMembersOfTeam => 'Apenas membros da equipe'; @override - String get noRestriction => 'Sem restrições'; + String get noRestriction => 'Sem restrição'; @override - String get minimumRatedGames => 'Jogos com classificação mínima'; + String get minimumRatedGames => 'Mínimo de partidas ranqueadas'; @override - String get minimumRating => 'Classificação mínima'; + String get minimumRating => 'Rating mínimo'; @override - String get maximumWeeklyRating => 'Avaliação semanal máxima'; + String get maximumWeeklyRating => 'Rating máxima da semana'; @override String positionInputHelp(String param) { - return 'Cole um FEN válido para iniciar todos os jogos a partir de uma determinada posição.\nSó funciona para os jogos padrão, não com variantes.\nVocê pode usar o $param para gerar uma posição FEN e, em seguida, colá-lo aqui.\nDeixe em branco para iniciar jogos da posição inicial normal.'; + return 'Cole um FEN válido para iniciar as partidas a partir de uma posição específica.\nSó funciona com jogos padrão, e não com variantes.\nUse o $param para gerar uma posição FEN, e depois cole-a aqui.\nDeixe em branco para começar as partidas na posição inicial padrão.'; } @override String get cancelSimul => 'Cancelar a simultânea'; @override - String get simulHostcolor => 'Cor do anfitrião para cada jogo'; + String get simulHostcolor => 'Cor do simultanista em cada jogo'; @override - String get estimatedStart => 'Hora de início prevista'; + String get estimatedStart => 'Tempo de início estimado'; @override String simulFeatured(String param) { - return 'Em destaque em $param'; + return 'Compartilhar em $param'; } @override String simulFeaturedHelp(String param) { - return 'Mostre a sua simultânea a todos em $param. Desativar para simultâneas privadas.'; + return 'Compartilhar a simultânia com todos em $param. Desative para jogos privados.'; } @override String get simulDescription => 'Descrição da simultânea'; @override - String get simulDescriptionHelp => 'Quer dizer alguma coisa aos participantes?'; + String get simulDescriptionHelp => 'Você gostaria de dizer algo aos participantes?'; @override String markdownAvailable(String param) { - return '$param está disponível para sintaxe mais avançada.'; + return '$param está disponível para opções de formatação adicionais.'; } @override - String get embedsAvailable => 'Cole o URL de um jogo ou um URL de um capítulo de estudo para integrá-lo.'; + String get embedsAvailable => 'Cole a URL de uma partida ou de um capítulo de estudo para incorporá-lo.'; @override - String get inYourLocalTimezone => 'No seu próprio fuso horário local'; + String get inYourLocalTimezone => 'No seu próprio fuso horário'; @override String get tournChat => 'Chat do torneio'; @@ -3833,72 +3851,72 @@ class AppLocalizationsPt extends AppLocalizations { String get noChat => 'Sem chat'; @override - String get onlyTeamLeaders => 'Apenas líderes da equipa'; + String get onlyTeamLeaders => 'Apenas líderes de equipe'; @override - String get onlyTeamMembers => 'Apenas membros da equipa'; + String get onlyTeamMembers => 'Apenas membros da equipe'; @override - String get navigateMoveTree => 'Navegar pela árvore de movimentos'; + String get navigateMoveTree => 'Navegar pela notação de movimentos'; @override - String get mouseTricks => 'Movimentos do rato'; + String get mouseTricks => 'Funcionalidades do mouse'; @override - String get toggleLocalAnalysis => 'Ativar/desativar análise local no computador'; + String get toggleLocalAnalysis => 'Ativar/desativar análise local do computador'; @override - String get toggleAllAnalysis => 'Alternar todas as análises no computador'; + String get toggleAllAnalysis => 'Ativar/desativar todas análises locais do computador'; @override - String get playComputerMove => 'Jogar o melhor lance do computador'; + String get playComputerMove => 'Jogar o melhor lance de computador'; @override String get analysisOptions => 'Opções de análise'; @override - String get focusChat => 'Focar no bate-papo'; + String get focusChat => 'Focar texto'; @override String get showHelpDialog => 'Mostrar esta mensagem de ajuda'; @override - String get reopenYourAccount => 'Reabrir a sua conta'; + String get reopenYourAccount => 'Reabra sua conta'; @override - String get closedAccountChangedMind => 'Se fechou a sua conta mas desde então mudou de ideias, terá uma oportunidade de recuperar a sua conta.'; + String get closedAccountChangedMind => 'Caso você tenha encerrado sua conta, mas mudou de opinião, você tem ainda uma chance de recuperá-la.'; @override - String get onlyWorksOnce => 'Isto só vai funcionar uma única vez.'; + String get onlyWorksOnce => 'Isso só vai funcionar uma vez.'; @override - String get cantDoThisTwice => 'Se fechar a conta uma segunda vez, não haverá forma de a recuperar.'; + String get cantDoThisTwice => 'Caso você encerre sua conta pela segunda vez, será impossível recuperá-la.'; @override - String get emailAssociatedToaccount => 'Endereço de email associado à conta'; + String get emailAssociatedToaccount => 'Endereço de e-mail associado à conta'; @override - String get sentEmailWithLink => 'Enviámos-lhe um e-mail com um link.'; + String get sentEmailWithLink => 'Enviamos um e-mail pra você com um link.'; @override String get tournamentEntryCode => 'Código de entrada do torneio'; @override - String get hangOn => 'Aguarde!'; + String get hangOn => 'Espere!'; @override String gameInProgress(String param) { - return 'Tem um jogo em curso com $param.'; + return 'Você tem uma partida em andamento com $param.'; } @override - String get abortTheGame => 'Cancelar o jogo'; + String get abortTheGame => 'Cancelar a partida'; @override - String get resignTheGame => 'Abandonar o jogo'; + String get resignTheGame => 'Abandonar a partida'; @override - String get youCantStartNewGame => 'Não pode iniciar um novo jogo antes de este estar terminado.'; + String get youCantStartNewGame => 'Você não pode iniciar um novo jogo até que este acabe.'; @override String get since => 'Desde'; @@ -3907,25 +3925,25 @@ class AppLocalizationsPt extends AppLocalizations { String get until => 'Até'; @override - String get lichessDbExplanation => 'Jogos avaliados por amostragem de todos os jogadores Lichess'; + String get lichessDbExplanation => 'Amostra de partidas rankeadas de todos os jogadores do Lichess'; @override String get switchSides => 'Trocar de lado'; @override - String get closingAccountWithdrawAppeal => 'Encerrar a tua conta anula o teu apelo'; + String get closingAccountWithdrawAppeal => 'Encerrar sua conta anulará seu apelo'; @override - String get ourEventTips => 'Os nossos conselhos para organizar eventos'; + String get ourEventTips => 'Nossas dicas para organização de eventos'; @override String get instructions => 'Instruções'; @override - String get showMeEverything => 'Mostra-me tudo'; + String get showMeEverything => 'Mostrar tudo'; @override - String get lichessPatronInfo => 'Lichess é uma instituição de caridade e software de código aberto totalmente livre.\nTodos os custos operacionais, de desenvolvimento e conteúdo são financiados exclusivamente por doações de usuários.'; + String get lichessPatronInfo => 'Lichess é um software de código aberto, totalmente grátis e sem fins lucrativos. Todos os custos operacionais, de desenvolvimento, e os conteúdos são financiados unicamente através de doações de usuários.'; @override String get nothingToSeeHere => 'Nada para ver aqui no momento.'; @@ -3935,8 +3953,8 @@ class AppLocalizationsPt extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'O teu adversário deixou a partida. Podes reivindicar vitória em $count segundos.', - one: 'O teu adversário deixou a partida. Podes reivindicar vitória em $count segundo.', + other: 'O seu adversário deixou a partida. Você pode reivindicar vitória em $count segundos.', + one: 'O seu adversário deixou a partida. Você pode reivindicar vitória em $count segundo.', ); return '$_temp0'; } @@ -3946,8 +3964,8 @@ class AppLocalizationsPt extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Xeque-mate em $count meio-movimentos', - one: 'Xeque-mate em $count meio-movimento', + other: 'Mate em $count lances', + one: 'Mate em $count lance', ); return '$_temp0'; } @@ -3957,8 +3975,8 @@ class AppLocalizationsPt extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count erros graves', - one: '$count erro grave', + other: '$count capivaradas', + one: '$count capivarada', ); return '$_temp0'; } @@ -3990,8 +4008,8 @@ class AppLocalizationsPt extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count jogadores', - one: '$count jogador', + other: '$count jogadores conectados', + one: '$count jogadores conectados', ); return '$_temp0'; } @@ -4001,8 +4019,8 @@ class AppLocalizationsPt extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count jogos', - one: '$count jogo', + other: '$count partidas', + one: '$count partida', ); return '$_temp0'; } @@ -4012,8 +4030,8 @@ class AppLocalizationsPt extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$param2 partidas $count avaliadas', - one: '$param2 partida $count avaliada', + other: 'Rating $count após $param2 partidas', + one: 'Rating $count em $param2 jogo', ); return '$_temp0'; } @@ -4023,8 +4041,8 @@ class AppLocalizationsPt extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count favoritos', - one: '$count favorito', + other: '$count Favoritos', + one: '$count Favoritos', ); return '$_temp0'; } @@ -4035,7 +4053,7 @@ class AppLocalizationsPt extends AppLocalizations { count, locale: localeName, other: '$count dias', - one: '$count dia', + one: '$count dias', ); return '$_temp0'; } @@ -4046,7 +4064,7 @@ class AppLocalizationsPt extends AppLocalizations { count, locale: localeName, other: '$count horas', - one: '$count hora', + one: '$count horas', ); return '$_temp0'; } @@ -4067,8 +4085,8 @@ class AppLocalizationsPt extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'As posições são atualizadas a cada $count minutos', - one: 'As posições são atualizadas a cada minuto', + other: 'O ranking é atualizado a cada $count minutos', + one: 'O ranking é atualizado a cada $count minutos', ); return '$_temp0'; } @@ -4079,7 +4097,7 @@ class AppLocalizationsPt extends AppLocalizations { count, locale: localeName, other: '$count problemas', - one: '$count problema', + one: '$count quebra-cabeça', ); return '$_temp0'; } @@ -4089,8 +4107,8 @@ class AppLocalizationsPt extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count jogos contigo', - one: '$count jogo contigo', + other: '$count partidas contra você', + one: '$count partidas contra você', ); return '$_temp0'; } @@ -4100,8 +4118,8 @@ class AppLocalizationsPt extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count partidas a valer pontos', - one: '$count partida a valer pontos', + other: '$count valendo pontos', + one: '$count valendo pontos', ); return '$_temp0'; } @@ -4134,7 +4152,7 @@ class AppLocalizationsPt extends AppLocalizations { count, locale: localeName, other: '$count empates', - one: '$count empate', + one: '$count empates', ); return '$_temp0'; } @@ -4144,8 +4162,8 @@ class AppLocalizationsPt extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count a jogar', - one: '$count a jogar', + other: '$count jogando', + one: '$count jogando', ); return '$_temp0'; } @@ -4156,7 +4174,7 @@ class AppLocalizationsPt extends AppLocalizations { count, locale: localeName, other: 'Dar $count segundos', - one: 'Dar $count segundo', + one: 'Dar $count segundos', ); return '$_temp0'; } @@ -4199,8 +4217,8 @@ class AppLocalizationsPt extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '≥ $count partidas a valer pontos', - one: '≥ $count partida a valer pontos', + other: '≥ $count jogos valendo pontos', + one: '≥ $count jogos valendo pontos', ); return '$_temp0'; } @@ -4210,8 +4228,8 @@ class AppLocalizationsPt extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '≥ $count partidas de $param2 a valer pontos', - one: '≥ $count partida de $param2 a valer pontos', + other: '≥ $count $param2 partidas valendo pontos', + one: '≥ $count partida $param2 valendo pontos', ); return '$_temp0'; } @@ -4221,8 +4239,8 @@ class AppLocalizationsPt extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Precisa de jogar mais $count jogos de $param2 a valer pontos', - one: 'Precisas de jogar mais $count jogo de $param2 a valer pontos', + other: 'Você precisa jogar mais $count partidas de $param2 valendo pontos', + one: 'Você precisa jogar mais $count partida de $param2 valendo pontos', ); return '$_temp0'; } @@ -4232,8 +4250,8 @@ class AppLocalizationsPt extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Tens de jogar mais $count jogos a valer pontos', - one: 'Tens de jogar mais $count jogo a valer pontos', + other: 'Você precisa jogar ainda $count partidas valendo pontos', + one: 'Você precisa jogar ainda $count partidas valendo pontos', ); return '$_temp0'; } @@ -4243,8 +4261,8 @@ class AppLocalizationsPt extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count partidas importadas', - one: '$count partida importada', + other: '$count de partidas importadas', + one: '$count de partidas importadas', ); return '$_temp0'; } @@ -4266,7 +4284,7 @@ class AppLocalizationsPt extends AppLocalizations { count, locale: localeName, other: '$count seguidores', - one: '$count seguidor', + one: '$count seguidores', ); return '$_temp0'; } @@ -4276,8 +4294,8 @@ class AppLocalizationsPt extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'a seguir $count jogadores', - one: 'a seguir $count jogador', + other: '$count seguidos', + one: '$count seguidos', ); return '$_temp0'; } @@ -4287,8 +4305,8 @@ class AppLocalizationsPt extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Menos de $count minutos', - one: 'Menos de $count minuto', + other: 'Menos que $count minutos', + one: 'Menos que $count minutos', ); return '$_temp0'; } @@ -4298,8 +4316,8 @@ class AppLocalizationsPt extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count jogos a decorrer', - one: '$count jogo a decorrer', + other: '$count partidas em andamento', + one: '$count partidas em andamento', ); return '$_temp0'; } @@ -4310,7 +4328,7 @@ class AppLocalizationsPt extends AppLocalizations { count, locale: localeName, other: 'Máximo: $count caracteres.', - one: 'Máximo: $count carácter.', + one: 'Máximo: $count caractere.', ); return '$_temp0'; } @@ -4342,8 +4360,8 @@ class AppLocalizationsPt extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count jogadores ativos esta semana em $param2.', - one: '$count jogador ativo esta semana em $param2.', + other: '$count $param2 jogadores nesta semana.', + one: '$count $param2 jogador nesta semana.', ); return '$_temp0'; } @@ -4353,8 +4371,8 @@ class AppLocalizationsPt extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Disponível em $count línguas!', - one: 'Disponível em $count língua!', + other: 'Disponível em $count idiomas!', + one: 'Disponível em $count idiomas!', ); return '$_temp0'; } @@ -4364,8 +4382,8 @@ class AppLocalizationsPt extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count segundos para jogar o primeiro movimento', - one: '$count segundo para jogar o primeiro movimento', + other: '$count segundos para fazer o primeiro lance', + one: '$count segundo para fazer o primeiro lance', ); return '$_temp0'; } @@ -4386,23 +4404,23 @@ class AppLocalizationsPt extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'e guarda $count variantes de movimentos antecipados', - one: 'e guarda $count variante de movimentos antecipados', + other: 'e salvar as linhas de pré-lance de $count', + one: 'e salvar a linha de pré-lance de $count', ); return '$_temp0'; } @override - String get stormMoveToStart => 'Faz um lance para começar'; + String get stormMoveToStart => 'Mova para começar'; @override - String get stormYouPlayTheWhitePiecesInAllPuzzles => 'Jogas com as peças brancas em todos os problemas'; + String get stormYouPlayTheWhitePiecesInAllPuzzles => 'Você joga com as peças brancas em todos os quebra-cabeças'; @override - String get stormYouPlayTheBlackPiecesInAllPuzzles => 'Jogas com as peças pretas em todos os problemas'; + String get stormYouPlayTheBlackPiecesInAllPuzzles => 'Você joga com as peças pretas em todos os quebra-cabeças'; @override - String get stormPuzzlesSolved => 'problemas resolvidos'; + String get stormPuzzlesSolved => 'quebra-cabeças resolvidos'; @override String get stormNewDailyHighscore => 'Novo recorde diário!'; @@ -4414,11 +4432,11 @@ class AppLocalizationsPt extends AppLocalizations { String get stormNewMonthlyHighscore => 'Novo recorde mensal!'; @override - String get stormNewAllTimeHighscore => 'Novo recorde!'; + String get stormNewAllTimeHighscore => 'Novo recorde de todos os tempos!'; @override String stormPreviousHighscoreWasX(String param) { - return 'O recorde anterior era $param'; + return 'Recorde anterior era $param'; } @override @@ -4433,7 +4451,7 @@ class AppLocalizationsPt extends AppLocalizations { String get stormScore => 'Pontuação'; @override - String get stormMoves => 'Total de lances'; + String get stormMoves => 'Lances'; @override String get stormAccuracy => 'Precisão'; @@ -4445,119 +4463,119 @@ class AppLocalizationsPt extends AppLocalizations { String get stormTime => 'Tempo'; @override - String get stormTimePerMove => 'Tempo por jogada'; + String get stormTimePerMove => 'Tempo por lance'; @override - String get stormHighestSolved => 'Problema mais difícil resolvido'; + String get stormHighestSolved => 'Classificação mais alta'; @override - String get stormPuzzlesPlayed => 'Problemas jogados'; + String get stormPuzzlesPlayed => 'Quebra-cabeças jogados'; @override - String get stormNewRun => 'Nova partida (tecla: espaço)'; + String get stormNewRun => 'Nova série (tecla de atalho: espaço)'; @override - String get stormEndRun => 'Terminar partida (tecla: Enter)'; + String get stormEndRun => 'Finalizar série (tecla de atalho: Enter)'; @override - String get stormHighscores => 'Recorde'; + String get stormHighscores => 'Melhores pontuações'; @override - String get stormViewBestRuns => 'Ver as melhores partidas'; + String get stormViewBestRuns => 'Ver melhores séries'; @override - String get stormBestRunOfDay => 'Melhor partida do dia'; + String get stormBestRunOfDay => 'Melhor série do dia'; @override - String get stormRuns => 'Partidas'; + String get stormRuns => 'Séries'; @override - String get stormGetReady => 'Preparar!'; + String get stormGetReady => 'Prepare-se!'; @override - String get stormWaitingForMorePlayers => 'À espera de mais jogadores...'; + String get stormWaitingForMorePlayers => 'Esperando mais jogadores entrarem...'; @override - String get stormRaceComplete => 'Race concluída!'; + String get stormRaceComplete => 'Corrida concluída!'; @override - String get stormSpectating => 'A assistir'; + String get stormSpectating => 'Espectando'; @override - String get stormJoinTheRace => 'Junta-te à corrida!'; + String get stormJoinTheRace => 'Entre na corrida!'; @override String get stormStartTheRace => 'Começar a corrida'; @override String stormYourRankX(String param) { - return 'A tua pontuação: $param'; + return 'Sua classificação: $param'; } @override - String get stormWaitForRematch => 'Espera pela desforra'; + String get stormWaitForRematch => 'Esperando por revanche'; @override String get stormNextRace => 'Próxima corrida'; @override - String get stormJoinRematch => 'Juntar-se à desforra'; + String get stormJoinRematch => 'Junte-se a revanche'; @override - String get stormWaitingToStart => 'À espera de começar'; + String get stormWaitingToStart => 'Esperando para começar'; @override String get stormCreateNewGame => 'Criar um novo jogo'; @override - String get stormJoinPublicRace => 'Junta-te a uma corrida pública'; + String get stormJoinPublicRace => 'Junte-se a uma corrida pública'; @override - String get stormRaceYourFriends => 'Corre contra os teus amigos'; + String get stormRaceYourFriends => 'Corra contra seus amigos'; @override - String get stormSkip => 'ignorar'; + String get stormSkip => 'pular'; @override - String get stormSkipHelp => 'Pode pular um movimento por corrida:'; + String get stormSkipHelp => 'Você pode pular um movimento por corrida:'; @override - String get stormSkipExplanation => 'Passa à frente esta jogada para preservares o teu combo! Só podes fazê-lo apenas uma vez por Race.'; + String get stormSkipExplanation => 'Pule este lance para preservar o seu combo! Funciona apenas uma vez por corrida.'; @override - String get stormFailedPuzzles => 'Desafios falhados'; + String get stormFailedPuzzles => 'Quebra-cabeças falhados'; @override - String get stormSlowPuzzles => 'Desafios lentos'; + String get stormSlowPuzzles => 'Quebra-cabeças lentos'; @override - String get stormSkippedPuzzle => 'Desafios saltados'; + String get stormSkippedPuzzle => 'Quebra-cabeça pulado'; @override - String get stormThisWeek => 'Esta semana'; + String get stormThisWeek => 'Essa semana'; @override - String get stormThisMonth => 'Este mês'; + String get stormThisMonth => 'Esse mês'; @override - String get stormAllTime => 'Desde sempre'; + String get stormAllTime => 'Desde o início'; @override String get stormClickToReload => 'Clique para recarregar'; @override - String get stormThisRunHasExpired => 'Esta sessão expirou!'; + String get stormThisRunHasExpired => 'Esta corrida acabou!'; @override - String get stormThisRunWasOpenedInAnotherTab => 'Esta sessão foi aberta noutra aba!'; + String get stormThisRunWasOpenedInAnotherTab => 'Esta corrida foi aberta em outra aba!'; @override String stormXRuns(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count tentativas', - one: '1 partida', + other: '$count séries', + one: '1 tentativa', ); return '$_temp0'; } @@ -4567,47 +4585,47 @@ class AppLocalizationsPt extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Jogou $count partidas de $param2', - one: 'Jogou uma partida de $param2', + other: 'Jogou $count tentativas de $param2', + one: 'Jogou uma tentativa de $param2', ); return '$_temp0'; } @override - String get streamerLichessStreamers => 'Streamers no Lichess'; + String get streamerLichessStreamers => 'Streamers do Lichess'; @override - String get studyShareAndExport => 'Partilhar & exportar'; + String get studyShareAndExport => 'Compartilhar & exportar'; @override String get studyStart => 'Iniciar'; } -/// The translations for Portuguese, as used in Brazil (`pt_BR`). -class AppLocalizationsPtBr extends AppLocalizationsPt { - AppLocalizationsPtBr(): super('pt_BR'); +/// The translations for Portuguese, as used in Portugal (`pt_PT`). +class AppLocalizationsPtPt extends AppLocalizationsPt { + AppLocalizationsPtPt(): super('pt_PT'); @override String get activityActivity => 'Atividade'; @override - String get activityHostedALiveStream => 'Iniciou uma transmissão ao vivo'; + String get activityHostedALiveStream => 'Criou uma livestream'; @override String activityRankedInSwissTournament(String param1, String param2) { - return 'Classificado #$param1 entre $param2'; + return 'Classificado #$param1 em $param2'; } @override - String get activitySignedUp => 'Registrou-se no lichess'; + String get activitySignedUp => 'Registou-se no lichess.org'; @override String activitySupportedNbMonths(int count, String param2) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Contribuiu para o lichess.org por $count meses como $param2', - one: 'Contribuiu para o lichess.org por $count mês como $param2', + other: 'Apoiou o lichess.org durante $count meses como $param2', + one: 'Apoiou o lichess.org durante $count mês como $param2', ); return '$_temp0'; } @@ -4628,8 +4646,8 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Resolveu $count quebra-cabeças táticos', - one: 'Resolveu $count quebra-cabeça tático', + other: 'Resolveu $count problemas', + one: 'Resolveu $count problema', ); return '$_temp0'; } @@ -4639,8 +4657,8 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Jogou $count partidas de $param2', - one: 'Jogou $count partida de $param2', + other: 'Jogou $count jogos de $param2', + one: 'Jogou $count jogo de $param2', ); return '$_temp0'; } @@ -4661,8 +4679,8 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Jogou $count movimentos', - one: 'Jogou $count movimento', + other: 'Fez $count jogadas', + one: 'Fez $count jogada', ); return '$_temp0'; } @@ -4716,8 +4734,8 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Hospedou $count exibições simultâneas', - one: 'Hospedou $count exibição simultânea', + other: 'Criou $count exibições simultâneas', + one: 'Criou $count exibição simultânea', ); return '$_temp0'; } @@ -4749,8 +4767,8 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Competiu em $count torneios arena', - one: 'Competiu em $count torneio arena', + other: 'Competiu em $count torneios', + one: 'Competiu em $count torneio', ); return '$_temp0'; } @@ -4760,8 +4778,8 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Classificado #$count (top $param2%) com $param3 jogos em $param4', - one: 'Classificado #$count (top $param2%) com $param3 jogo em $param4', + other: 'Qualificado #$count (nos $param2% melhores) com $param3 jogos em $param4', + one: 'Qualificado #$count (nos $param2% melhores) com $param3 jogo em $param4', ); return '$_temp0'; } @@ -4782,8 +4800,8 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Entrou nas $count equipes', - one: 'Entrou na $count equipe', + other: 'Entrou em $count equipas', + one: 'Entrou em $count equipa', ); return '$_temp0'; } @@ -4792,7 +4810,7 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get broadcastBroadcasts => 'Transmissões'; @override - String get broadcastStartDate => 'Data de início em seu próprio fuso horário'; + String get broadcastStartDate => 'Data de início no teu fuso horário'; @override String challengeChallengesX(String param1) { @@ -4806,17 +4824,17 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get challengeChallengeDeclined => 'Desafio recusado'; @override - String get challengeChallengeAccepted => 'Desafio aceito!'; + String get challengeChallengeAccepted => 'Desafio aceite!'; @override String get challengeChallengeCanceled => 'Desafio cancelado.'; @override - String get challengeRegisterToSendChallenges => 'Por favor, registre-se para enviar desafios.'; + String get challengeRegisterToSendChallenges => 'Por favor regista-te para enviar desafios.'; @override String challengeYouCannotChallengeX(String param) { - return 'Você não pode desafiar $param.'; + return 'Não podes desafiar $param.'; } @override @@ -4826,12 +4844,12 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { @override String challengeYourXRatingIsTooFarFromY(String param1, String param2) { - return 'O seu rating $param1 é muito diferente de $param2.'; + return 'O teu ranking $param1 esta muito distante de $param2.'; } @override String challengeCannotChallengeDueToProvisionalXRating(String param) { - return 'Não pode desafiar devido ao rating provisório de $param.'; + return 'Não podes desafiar devido a ranking provisório $param.'; } @override @@ -4840,52 +4858,52 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { } @override - String get challengeDeclineGeneric => 'Não estou aceitando desafios no momento.'; + String get challengeDeclineGeneric => 'Não estou a aceitar desafios no momento.'; @override String get challengeDeclineLater => 'Este não é o momento certo para mim, por favor pergunte novamente mais tarde.'; @override - String get challengeDeclineTooFast => 'Este controle de tempo é muito rápido para mim, por favor, desafie novamente com um jogo mais lento.'; + String get challengeDeclineTooFast => 'Este controlo de tempo é muito rápido para mim, por favor, desafie-me novamente com um jogo mais lento.'; @override - String get challengeDeclineTooSlow => 'Este controle de tempo é muito lento para mim, por favor, desafie novamente com um jogo mais rápido.'; + String get challengeDeclineTooSlow => 'Este controlo de tempo é muito lento para mim, por favor, desafie-me novamente com um jogo mais rápido.'; @override - String get challengeDeclineTimeControl => 'Não estou aceitando desafios com estes controles de tempo.'; + String get challengeDeclineTimeControl => 'Não estou a aceitar desafios com este controlo de tempo.'; @override - String get challengeDeclineRated => 'Por favor, envie-me um desafio ranqueado.'; + String get challengeDeclineRated => 'Por favor, envie-me um desafio a valer para a classificação.'; @override String get challengeDeclineCasual => 'Por favor, envie-me um desafio amigável.'; @override - String get challengeDeclineStandard => 'Não estou aceitando desafios de variantes no momento.'; + String get challengeDeclineStandard => 'Não estou a aceitar desafios de variante, de momento.'; @override - String get challengeDeclineVariant => 'Não estou a fim de jogar esta variante no momento.'; + String get challengeDeclineVariant => 'Não estou disposto a jogar essa variante, de momento.'; @override - String get challengeDeclineNoBot => 'Não estou aceitando desafios de robôs.'; + String get challengeDeclineNoBot => 'Não estou a aceitar desafios de bots.'; @override - String get challengeDeclineOnlyBot => 'Estou aceitando apenas desafios de robôs.'; + String get challengeDeclineOnlyBot => 'Apenas aceito desafios de bots.'; @override - String get challengeInviteLichessUser => 'Ou convide um usuário Lichess:'; + String get challengeInviteLichessUser => 'Ou convide um utilizador Lichess:'; @override - String get contactContact => 'Contato'; + String get contactContact => 'Contacto'; @override - String get contactContactLichess => 'Entrar em contato com Lichess'; + String get contactContactLichess => 'Contactar o Lichess'; @override - String get patronDonate => 'Doação'; + String get patronDonate => 'Doar'; @override - String get patronLichessPatron => 'Apoie o Lichess'; + String get patronLichessPatron => 'Patrono do Lichess'; @override String perfStatPerfStats(String param) { @@ -4893,46 +4911,46 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { } @override - String get perfStatViewTheGames => 'Ver os jogos'; + String get perfStatViewTheGames => 'Ver as partidas'; @override String get perfStatProvisional => 'provisório'; @override - String get perfStatNotEnoughRatedGames => 'Não foram jogadas partidas suficientes valendo rating para estabelecer uma classificação confiável.'; + String get perfStatNotEnoughRatedGames => 'Não foi jogado um número suficiente de partidas a pontuar para estabelecer uma pontuação de confiança.'; @override String perfStatProgressOverLastXGames(String param) { - return 'Progresso nos últimos $param jogos:'; + return 'Progresso nas últimas $param partidas:'; } @override String perfStatRatingDeviation(String param) { - return 'Desvio de pontuação: $param.'; + return 'Desvio da pontuação: $param.'; } @override String perfStatRatingDeviationTooltip(String param1, String param2, String param3) { - return 'Um valor inferior indica que a pontuação é mais estável. Superior a $param1, a pontuação é classificada como provisória. Para ser incluída nas classificações, esse valor deve ser inferior a $param2 (xadrez padrão) ou $param3 (variantes).'; + return 'Um valor inferior significa que a classificação é mais estável. Acima de $param1, a classificação é considerada provisória. Para ser incluído nas classificações, esse valor deve estar abaixo de $param2 (xadrez padrão) ou $param3 (variantes).'; } @override String get perfStatTotalGames => 'Total de partidas'; @override - String get perfStatRatedGames => 'Partidas valendo pontos'; + String get perfStatRatedGames => 'Total de partidas a pontuar'; @override - String get perfStatTournamentGames => 'Jogos de torneio'; + String get perfStatTournamentGames => 'Partidas em torneios'; @override - String get perfStatBerserkedGames => 'Partidas Berserked'; + String get perfStatBerserkedGames => 'Partidas no modo frenético'; @override - String get perfStatTimeSpentPlaying => 'Tempo jogando'; + String get perfStatTimeSpentPlaying => 'Tempo passado a jogar'; @override - String get perfStatAverageOpponent => 'Pontuação média do adversário'; + String get perfStatAverageOpponent => 'Pontuação média dos adversários'; @override String get perfStatVictories => 'Vitórias'; @@ -4944,7 +4962,7 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get perfStatDisconnections => 'Desconexões'; @override - String get perfStatNotEnoughGames => 'Jogos insuficientes jogados'; + String get perfStatNotEnoughGames => 'Não foram jogadas partidas suficientes'; @override String perfStatHighestRating(String param) { @@ -4953,19 +4971,19 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { @override String perfStatLowestRating(String param) { - return 'Rating mais baixo: $param'; + return 'Pontuação mais baixa: $param'; } @override String perfStatFromXToY(String param1, String param2) { - return 'de $param1 para $param2'; + return 'de $param1 a $param2'; } @override - String get perfStatWinningStreak => 'Série de Vitórias'; + String get perfStatWinningStreak => 'Vitórias consecutivas'; @override - String get perfStatLosingStreak => 'Série de derrotas'; + String get perfStatLosingStreak => 'Derrotas consecutivas'; @override String perfStatLongestStreak(String param) { @@ -4978,16 +4996,16 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { } @override - String get perfStatBestRated => 'Melhores vitórias valendo pontuação'; + String get perfStatBestRated => 'Melhores vitórias a pontuar'; @override - String get perfStatGamesInARow => 'Partidas jogadas seguidas'; + String get perfStatGamesInARow => 'Partidas jogadas de seguida'; @override String get perfStatLessThanOneHour => 'Menos de uma hora entre partidas'; @override - String get perfStatMaxTimePlaying => 'Tempo máximo jogando'; + String get perfStatMaxTimePlaying => 'Tempo máximo passado a jogar'; @override String get perfStatNow => 'agora'; @@ -4996,7 +5014,7 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get preferencesPreferences => 'Preferências'; @override - String get preferencesDisplay => 'Exibição'; + String get preferencesDisplay => 'Mostrar'; @override String get preferencesPrivacy => 'Privacidade'; @@ -5008,58 +5026,58 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get preferencesPieceAnimation => 'Animação das peças'; @override - String get preferencesMaterialDifference => 'Diferença material'; + String get preferencesMaterialDifference => 'Diferença de material'; @override - String get preferencesBoardHighlights => 'Destacar casas do tabuleiro (último movimento e xeque)'; + String get preferencesBoardHighlights => 'Destacar as casas do tabuleiro (último movimento e xeque)'; @override - String get preferencesPieceDestinations => 'Destino das peças (movimentos válidos e pré-movimentos)'; + String get preferencesPieceDestinations => 'Destino das peças (movimentos válidos e antecipados)'; @override String get preferencesBoardCoordinates => 'Coordenadas do tabuleiro (A-H, 1-8)'; @override - String get preferencesMoveListWhilePlaying => 'Lista de movimentos durante a partida'; + String get preferencesMoveListWhilePlaying => 'Lista de movimentos'; @override - String get preferencesPgnPieceNotation => 'Modo de notação das jogadas'; + String get preferencesPgnPieceNotation => 'Anotação de movimentos'; @override - String get preferencesChessPieceSymbol => 'Símbolo da peça'; + String get preferencesChessPieceSymbol => 'Usar símbolo das peças'; @override - String get preferencesPgnLetter => 'Letra (K, Q, R, B, N)'; + String get preferencesPgnLetter => 'Usar letras (K, Q, R, B, N)'; @override - String get preferencesZenMode => 'Modo Zen'; + String get preferencesZenMode => 'Modo zen'; @override - String get preferencesShowPlayerRatings => 'Mostrar rating dos jogadores'; + String get preferencesShowPlayerRatings => 'Mostrar classificações dos jogadores'; @override - String get preferencesShowFlairs => 'Mostrar emotes de usuário'; + String get preferencesShowFlairs => 'Mostrar os estilos do jogadores'; @override - String get preferencesExplainShowPlayerRatings => 'Permite ocultar todas os ratings do site, para ajudar a se concentrar no jogo. As partidas continuam valendo rating.'; + String get preferencesExplainShowPlayerRatings => 'Isto permite ocultar todas as avaliações do site, para o ajudar a concentrar-se no xadrez. Os jogos continuam a poder ser avaliados, trata-se apenas do que poderá ver.'; @override - String get preferencesDisplayBoardResizeHandle => 'Mostrar cursor de redimensionamento do tabuleiro'; + String get preferencesDisplayBoardResizeHandle => 'Mostrar o cursor de redimensionamento do tabuleiro'; @override String get preferencesOnlyOnInitialPosition => 'Apenas na posição inicial'; @override - String get preferencesInGameOnly => 'Durante partidas'; + String get preferencesInGameOnly => 'Apenas em Jogo'; @override - String get preferencesChessClock => 'Relógio'; + String get preferencesChessClock => 'Relógio de xadrez'; @override String get preferencesTenthsOfSeconds => 'Décimos de segundo'; @override - String get preferencesWhenTimeRemainingLessThanTenSeconds => 'Quando o tempo restante < 10 segundos'; + String get preferencesWhenTimeRemainingLessThanTenSeconds => 'Quando o tempo restante for < 10 segundos'; @override String get preferencesHorizontalGreenProgressBars => 'Barras de progresso verdes horizontais'; @@ -5071,25 +5089,25 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get preferencesGiveMoreTime => 'Dar mais tempo'; @override - String get preferencesGameBehavior => 'Comportamento do jogo'; + String get preferencesGameBehavior => 'Funcionamento do jogo'; @override - String get preferencesHowDoYouMovePieces => 'Como você move as peças?'; + String get preferencesHowDoYouMovePieces => 'Como queres mexer as peças?'; @override String get preferencesClickTwoSquares => 'Clicar em duas casas'; @override - String get preferencesDragPiece => 'Arrastar a peça'; + String get preferencesDragPiece => 'Arrastar uma peça'; @override - String get preferencesBothClicksAndDrag => 'Ambas'; + String get preferencesBothClicksAndDrag => 'Qualquer'; @override - String get preferencesPremovesPlayingDuringOpponentTurn => 'Pré-movimentos (jogadas durante o turno do oponente)'; + String get preferencesPremovesPlayingDuringOpponentTurn => 'Jogadas antecipadas (jogadas durante a vez do adversário)'; @override - String get preferencesTakebacksWithOpponentApproval => 'Voltar jogada (com aprovação do oponente)'; + String get preferencesTakebacksWithOpponentApproval => 'Voltar jogadas atrás (com aprovação do adversário)'; @override String get preferencesInCasualGamesOnly => 'Somente em jogos casuais'; @@ -5101,58 +5119,58 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get preferencesExplainPromoteToQueenAutomatically => 'Mantenha a tecla pressionada enquanto promove para desativar temporariamente a autopromoção'; @override - String get preferencesWhenPremoving => 'Quando pré-mover'; + String get preferencesWhenPremoving => 'Quando mover antecipadamente'; @override - String get preferencesClaimDrawOnThreefoldRepetitionAutomatically => 'Reivindicar empate sobre a repetição tripla automaticamente'; + String get preferencesClaimDrawOnThreefoldRepetitionAutomatically => 'Reivindicar empate automaticamente após uma repetição tripla'; @override - String get preferencesWhenTimeRemainingLessThanThirtySeconds => 'Quando o tempo restante < 30 segundos'; + String get preferencesWhenTimeRemainingLessThanThirtySeconds => 'Quando o tempo restante for < 30 segundos'; @override String get preferencesMoveConfirmation => 'Confirmação de movimento'; @override - String get preferencesExplainCanThenBeTemporarilyDisabled => 'Pode ser desativado durante a partida no menu do tabuleiro'; + String get preferencesExplainCanThenBeTemporarilyDisabled => 'Pode ser desativado durante um jogo com o menu do tabuleiro'; @override String get preferencesInCorrespondenceGames => 'Jogos por correspondência'; @override - String get preferencesCorrespondenceAndUnlimited => 'Por correspondência e sem limites'; + String get preferencesCorrespondenceAndUnlimited => 'Por correspondência e ilimitado'; @override - String get preferencesConfirmResignationAndDrawOffers => 'Confirmar abandono e oferta de empate'; + String get preferencesConfirmResignationAndDrawOffers => 'Confirmar desistências e propostas de empate'; @override - String get preferencesCastleByMovingTheKingTwoSquaresOrOntoTheRook => 'Maneira de rocar'; + String get preferencesCastleByMovingTheKingTwoSquaresOrOntoTheRook => 'Método de roque'; @override String get preferencesCastleByMovingTwoSquares => 'Mover o rei duas casas'; @override - String get preferencesCastleByMovingOntoTheRook => 'Mover o rei em direção à torre'; + String get preferencesCastleByMovingOntoTheRook => 'Mover o rei até à torre'; @override - String get preferencesInputMovesWithTheKeyboard => 'Fazer lances com escrita do teclado'; + String get preferencesInputMovesWithTheKeyboard => 'Introduzir movimentos com o teclado'; @override - String get preferencesInputMovesWithVoice => 'Mova as peças com sua voz'; + String get preferencesInputMovesWithVoice => 'Insira movimentos com a sua voz'; @override - String get preferencesSnapArrowsToValidMoves => 'Insira setas para movimentos válidos'; + String get preferencesSnapArrowsToValidMoves => 'Alinhar as setas para sítios para onde as peças se podem mover'; @override - String get preferencesSayGgWpAfterLosingOrDrawing => 'Diga \"Bom jogo, bem jogado\" após a derrota ou empate'; + String get preferencesSayGgWpAfterLosingOrDrawing => 'Dizer \"Good game, well played\" (Bom jogo, bem jogado) após uma derrota ou empate'; @override - String get preferencesYourPreferencesHaveBeenSaved => 'Suas preferências foram salvas.'; + String get preferencesYourPreferencesHaveBeenSaved => 'As tuas preferências foram guardadas.'; @override - String get preferencesScrollOnTheBoardToReplayMoves => 'Use o scroll do mouse no tabuleiro para ir passando as jogadas'; + String get preferencesScrollOnTheBoardToReplayMoves => 'Rolar no tabuleiro para repetir os movimentos'; @override - String get preferencesCorrespondenceEmailNotification => 'Email diário listando seus jogos por correspondência'; + String get preferencesCorrespondenceEmailNotification => 'Notificações diárias por email listando seus jogos por correspondência'; @override String get preferencesNotifyStreamStart => 'Streamer começou uma transmissão ao vivo'; @@ -5161,28 +5179,28 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get preferencesNotifyInboxMsg => 'Nova mensagem na caixa de entrada'; @override - String get preferencesNotifyForumMention => 'Você foi mencionado em um comentário do fórum'; + String get preferencesNotifyForumMention => 'Um comentário do fórum menciona-o'; @override - String get preferencesNotifyInvitedStudy => 'Convite para um estudo'; + String get preferencesNotifyInvitedStudy => 'Convite para estudo'; @override - String get preferencesNotifyGameEvent => 'Jogo por correspondência atualizado'; + String get preferencesNotifyGameEvent => 'Atualizações dos jogos por correspondência'; @override String get preferencesNotifyChallenge => 'Desafios'; @override - String get preferencesNotifyTournamentSoon => 'O torneio vai começar em breve'; + String get preferencesNotifyTournamentSoon => 'O torneio começará em breve'; @override - String get preferencesNotifyTimeAlarm => 'Está acabando o tempo no jogo por correspondência'; + String get preferencesNotifyTimeAlarm => 'Está a acabar o tempo no jogo por correspondência'; @override - String get preferencesNotifyBell => 'Notificação no Lichess'; + String get preferencesNotifyBell => 'Notificação do sino no Lichess'; @override - String get preferencesNotifyPush => 'Notificação no dispositivo fora do Lichess'; + String get preferencesNotifyPush => 'Notificação do dispositivo quando não está no Lichess'; @override String get preferencesNotifyWeb => 'Navegador'; @@ -5194,10 +5212,10 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get preferencesBellNotificationSound => 'Som da notificação'; @override - String get puzzlePuzzles => 'Quebra-cabeças'; + String get puzzlePuzzles => 'Problemas'; @override - String get puzzlePuzzleThemes => 'Temas de quebra-cabeça'; + String get puzzlePuzzleThemes => 'Temas de problemas'; @override String get puzzleRecommended => 'Recomendado'; @@ -5206,13 +5224,13 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get puzzlePhases => 'Fases'; @override - String get puzzleMotifs => 'Motivos táticos'; + String get puzzleMotifs => 'Temas'; @override String get puzzleAdvanced => 'Avançado'; @override - String get puzzleLengths => 'Distância'; + String get puzzleLengths => 'Comprimentos'; @override String get puzzleMates => 'Xeque-mates'; @@ -5227,42 +5245,42 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get puzzleSpecialMoves => 'Movimentos especiais'; @override - String get puzzleDidYouLikeThisPuzzle => 'Você gostou deste quebra-cabeças?'; + String get puzzleDidYouLikeThisPuzzle => 'Gostaste deste problema?'; @override - String get puzzleVoteToLoadNextOne => 'Vote para carregar o próximo!'; + String get puzzleVoteToLoadNextOne => 'Vota para carregares o próximo!'; @override - String get puzzleUpVote => 'Votar a favor do quebra-cabeça'; + String get puzzleUpVote => 'Aprove o puzzle'; @override - String get puzzleDownVote => 'Votar contra o quebra-cabeça'; + String get puzzleDownVote => 'Desaprove o puzzle'; @override - String get puzzleYourPuzzleRatingWillNotChange => 'Sua pontuação de quebra-cabeças não mudará. Note que os quebra-cabeças não são uma competição. A pontuação indica os quebra-cabeças que se adequam às suas habilidades.'; + String get puzzleYourPuzzleRatingWillNotChange => 'A tua classificação de problemas não será alterada. Nota que os problemas não são uma competição. A classificação ajuda a selecionar os melhores problemas para o teu nível atual.'; @override - String get puzzleFindTheBestMoveForWhite => 'Encontre o melhor lance para as brancas.'; + String get puzzleFindTheBestMoveForWhite => 'Encontra a melhor jogada para as brancas.'; @override - String get puzzleFindTheBestMoveForBlack => 'Encontre a melhor jogada para as pretas.'; + String get puzzleFindTheBestMoveForBlack => 'Encontra a melhor jogada para as pretas.'; @override String get puzzleToGetPersonalizedPuzzles => 'Para obter desafios personalizados:'; @override String puzzlePuzzleId(String param) { - return 'Quebra-cabeça $param'; + return 'Problema $param'; } @override - String get puzzlePuzzleOfTheDay => 'Quebra-cabeça do dia'; + String get puzzlePuzzleOfTheDay => 'Problema do dia'; @override - String get puzzleDailyPuzzle => 'Quebra-cabeça diário'; + String get puzzleDailyPuzzle => 'Problema diário'; @override - String get puzzleClickToSolve => 'Clique para resolver'; + String get puzzleClickToSolve => 'Clica para resolveres'; @override String get puzzleGoodMove => 'Boa jogada'; @@ -5271,42 +5289,42 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get puzzleBestMove => 'Melhor jogada!'; @override - String get puzzleKeepGoing => 'Continue…'; + String get puzzleKeepGoing => 'Continua…'; @override String get puzzlePuzzleSuccess => 'Sucesso!'; @override - String get puzzlePuzzleComplete => 'Quebra-cabeças concluído!'; + String get puzzlePuzzleComplete => 'Problema resolvido!'; @override String get puzzleByOpenings => 'Por abertura'; @override - String get puzzlePuzzlesByOpenings => 'Quebra-cabeças por abertura'; + String get puzzlePuzzlesByOpenings => 'Problemas por abertura'; @override - String get puzzleOpeningsYouPlayedTheMost => 'Aberturas que você mais jogou em partidas valendo pontos'; + String get puzzleOpeningsYouPlayedTheMost => 'Aberturas que jogou mais vezes em partidas com rating'; @override - String get puzzleUseFindInPage => 'Use a ferramenta \"Encontrar na página\" do navegador para encontrar sua abertura favorita!'; + String get puzzleUseFindInPage => 'Usar \"Localizar na página\" no menu do navegador para encontrar a sua abertura favorita!'; @override - String get puzzleUseCtrlF => 'Aperte Ctrl + F para encontrar sua abertura favorita!'; + String get puzzleUseCtrlF => 'Usar Ctrl+f para encontrar a sua abertura favorita!'; @override - String get puzzleNotTheMove => 'O movimento não é este!'; + String get puzzleNotTheMove => 'Não é esse movimento!'; @override - String get puzzleTrySomethingElse => 'Tente algo diferente.'; + String get puzzleTrySomethingElse => 'Tenta outra coisa.'; @override String puzzleRatingX(String param) { - return 'Rating: $param'; + return 'Pontuação: $param'; } @override - String get puzzleHidden => 'oculto'; + String get puzzleHidden => 'oculta'; @override String puzzleFromGameLink(String param) { @@ -5314,7 +5332,7 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { } @override - String get puzzleContinueTraining => 'Continue treinando'; + String get puzzleContinueTraining => 'Continuar o treino'; @override String get puzzleDifficultyLevel => 'Nível de dificuldade'; @@ -5323,88 +5341,88 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get puzzleNormal => 'Normal'; @override - String get puzzleEasier => 'Fácil'; + String get puzzleEasier => 'Mais fáceis'; @override - String get puzzleEasiest => 'Muito fácil'; + String get puzzleEasiest => 'Mais fáceis'; @override - String get puzzleHarder => 'Difícil'; + String get puzzleHarder => 'Mais difíceis'; @override - String get puzzleHardest => 'Muito difícil'; + String get puzzleHardest => 'Mais difíceis'; @override String get puzzleExample => 'Exemplo'; @override - String get puzzleAddAnotherTheme => 'Adicionar um outro tema'; + String get puzzleAddAnotherTheme => 'Adicionar outro tema'; @override - String get puzzleNextPuzzle => 'Próximo quebra-cabeça'; + String get puzzleNextPuzzle => 'Próximo desafio'; @override - String get puzzleJumpToNextPuzzleImmediately => 'Ir para o próximo problema automaticamente'; + String get puzzleJumpToNextPuzzleImmediately => 'Saltar imediatamente para o próximo problema'; @override - String get puzzlePuzzleDashboard => 'Painel do quebra-cabeças'; + String get puzzlePuzzleDashboard => 'Painel de controlo dos problemas'; @override - String get puzzleImprovementAreas => 'Áreas de aprimoramento'; + String get puzzleImprovementAreas => 'Áreas a melhorar'; @override String get puzzleStrengths => 'Pontos fortes'; @override - String get puzzleHistory => 'Histórico de quebra-cabeças'; + String get puzzleHistory => 'Histórico de problemas'; @override String get puzzleSolved => 'resolvido'; @override - String get puzzleFailed => 'falhou'; + String get puzzleFailed => 'incorreto'; @override - String get puzzleStreakDescription => 'Resolva quebra-cabeças progressivamente mais difíceis e construa uma sequência de vitórias. Não há relógio, então tome seu tempo. Um movimento errado e o jogo acaba! Porém, você pode pular um movimento por sessão.'; + String get puzzleStreakDescription => 'Resolve puzzles progressivamente mais difíceis e estabelece uma sequência de vitórias. Não há relógio, demora o teu tempo. Um movimento errado e o jogo acaba! No entanto, podes saltar um movimento por sessão.'; @override String puzzleYourStreakX(String param) { - return 'Sua sequência: $param'; + return 'Vitórias consecutivas: $param'; } @override - String get puzzleStreakSkipExplanation => 'Pule este lance para preservar a sua sequência! Funciona apenas uma vez por corrida.'; + String get puzzleStreakSkipExplanation => 'Salta este movimento para preservar a tua sequência! Apenas funciona uma vez por sessão.'; @override - String get puzzleContinueTheStreak => 'Continuar a sequência'; + String get puzzleContinueTheStreak => 'Continua a sequência'; @override - String get puzzleNewStreak => 'Nova sequência'; + String get puzzleNewStreak => 'Nova sequência de vitórias'; @override String get puzzleFromMyGames => 'Dos meus jogos'; @override - String get puzzleLookupOfPlayer => 'Pesquise quebra-cabeças de um jogador específico'; + String get puzzleLookupOfPlayer => 'Pesquise problemas de jogos de um jogador'; @override String puzzleFromXGames(String param) { - return 'Problemas de $param\' jogos'; + return 'Puzzles dos jogos de $param'; } @override - String get puzzleSearchPuzzles => 'Procurar quebra-cabeças'; + String get puzzleSearchPuzzles => 'Pesquisar desafios'; @override - String get puzzleFromMyGamesNone => 'Você não tem nenhum quebra-cabeça no banco de dados, mas o Lichess ainda te ama muito.\nJogue partidas rápidas e clássicas para aumentar suas chances de ter um desafio seu adicionado!'; + String get puzzleFromMyGamesNone => 'Não tens problemas na base de dados, mas Lichess adora-te muito.\n\nJoga partidas semi-rápidas e clássicas para aumentares a probabilidade de teres um problema adicionado!'; @override String puzzleFromXGamesFound(String param1, String param2) { - return '$param1 quebra-cabeças encontrados em $param2 partidas'; + return '$param1 problemas encontrados em $param2 partidas'; } @override - String get puzzlePuzzleDashboardDescription => 'Treine, analise, melhore'; + String get puzzlePuzzleDashboardDescription => 'Treinar, analisar, melhorar'; @override String puzzlePercentSolved(String param) { @@ -5412,13 +5430,13 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { } @override - String get puzzleNoPuzzlesToShow => 'Não há nada para mostrar aqui, jogue alguns quebra-cabeças primeiro!'; + String get puzzleNoPuzzlesToShow => 'Nada para mostrar, joga alguns problemas primeiro!'; @override String get puzzleImprovementAreasDescription => 'Treine estes para otimizar o seu progresso!'; @override - String get puzzleStrengthDescription => 'Sua perfomance é melhor nesses temas'; + String get puzzleStrengthDescription => 'Você tem melhor desempenho nestes temas'; @override String puzzlePlayedXTimes(int count) { @@ -5426,7 +5444,7 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { count, locale: localeName, other: 'Jogado $count vezes', - one: 'Jogado $count vezes', + one: 'Jogado $count vez', ); return '$_temp0'; } @@ -5436,8 +5454,8 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count pontos abaixo da sua classificação de quebra-cabeças', - one: 'Um ponto abaixo da sua classificação de quebra-cabeças', + other: '$count pontos abaixo da sua pontuação de problemas', + one: 'Um ponto abaixo da sua pontuação de problemas', ); return '$_temp0'; } @@ -5447,8 +5465,8 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count pontos acima da sua classificação de quebra-cabeças', - one: 'Um ponto acima da sua classificação de quebra-cabeças', + other: '$count pontos acima da sua pontuação de problemas', + one: 'Um ponto acima da sua pontuação de problemas', ); return '$_temp0'; } @@ -5458,8 +5476,8 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count jogados', - one: '$count jogado', + other: '$count problemas feitos', + one: '$count problema feito', ); return '$_temp0'; } @@ -5469,8 +5487,8 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count a serem repetidos', - one: '$count a ser repetido', + other: '$count para repetir', + one: '$count para repetir', ); return '$_temp0'; } @@ -5479,136 +5497,136 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get puzzleThemeAdvancedPawn => 'Peão avançado'; @override - String get puzzleThemeAdvancedPawnDescription => 'Um peão prestes a ser promovido ou à beira da promoção é um tema tático.'; + String get puzzleThemeAdvancedPawnDescription => 'A chave do tático é um peão a promover ou a ameaçar promover.'; @override String get puzzleThemeAdvantage => 'Vantagem'; @override - String get puzzleThemeAdvantageDescription => 'Aproveite a sua chance de ter uma vantagem decisiva. (200cp ≤ eval ≤ 600cp)'; + String get puzzleThemeAdvantageDescription => 'Aproveita a oportunidade de obter uma vantagem decisiva. (200cp ≤ aval ≤ 600cp)'; @override String get puzzleThemeAnastasiaMate => 'Mate Anastasia'; @override - String get puzzleThemeAnastasiaMateDescription => 'Um cavalo e uma torre se unem para prender o rei do oponente entre a lateral do tabuleiro e uma peça amiga.'; + String get puzzleThemeAnastasiaMateDescription => 'Um cavalo e uma torre ou dama cooperam para prender o rei inimigo entre um lado do tabuleiro e outra peça inimiga.'; @override - String get puzzleThemeArabianMate => 'Mate árabe'; + String get puzzleThemeArabianMate => 'Mate Árabe'; @override - String get puzzleThemeArabianMateDescription => 'Um cavalo e uma torre se unem para prender o rei inimigo em um canto do tabuleiro.'; + String get puzzleThemeArabianMateDescription => 'Um cavalo e uma torre cooperam para prenderem o rei inimigo no canto do tabuleiro.'; @override - String get puzzleThemeAttackingF2F7 => 'Atacando f2 ou f7'; + String get puzzleThemeAttackingF2F7 => 'Atacar f2 ou f7'; @override - String get puzzleThemeAttackingF2F7Description => 'Um ataque focado no peão de f2 e no peão de f7, como na abertura frango frito.'; + String get puzzleThemeAttackingF2F7Description => 'Um ataque ao peão de f2 ou f7, como a abertura \"Fried Liver\".'; @override String get puzzleThemeAttraction => 'Atração'; @override - String get puzzleThemeAttractionDescription => 'Uma troca ou sacrifício encorajando ou forçando uma peça do oponente a uma casa que permite uma sequência tática.'; + String get puzzleThemeAttractionDescription => 'Uma troca ou sacrifício que encoraja ou força uma peça adversária a ir para uma casa que permite um tático.'; @override - String get puzzleThemeBackRankMate => 'Mate do corredor'; + String get puzzleThemeBackRankMate => 'Mate de corredor'; @override - String get puzzleThemeBackRankMateDescription => 'Dê o xeque-mate no rei na última fileira, quando ele estiver bloqueado pelas próprias peças.'; + String get puzzleThemeBackRankMateDescription => 'Dá mate ao rei na fila inicial, quando está preso pelas suas próprias peças.'; @override - String get puzzleThemeBishopEndgame => 'Finais de bispo'; + String get puzzleThemeBishopEndgame => 'Final de bispos'; @override - String get puzzleThemeBishopEndgameDescription => 'Final com somente bispos e peões.'; + String get puzzleThemeBishopEndgameDescription => 'Um final apenas com bispos e peões.'; @override - String get puzzleThemeBodenMate => 'Mate de Boden'; + String get puzzleThemeBodenMate => 'Mate Boden'; @override - String get puzzleThemeBodenMateDescription => 'Dois bispos atacantes em diagonais cruzadas dão um mate em um rei obstruído por peças amigas.'; + String get puzzleThemeBodenMateDescription => 'Dois bispos em diagonais perpendiculares dão mate ao rei inimigo obstruído por peças aliadas.'; @override String get puzzleThemeCastling => 'Roque'; @override - String get puzzleThemeCastlingDescription => 'Traga o seu rei para a segurança, e prepare sua torre para o ataque.'; + String get puzzleThemeCastlingDescription => 'Proteger o rei e trazer a torre para o ataque.'; @override - String get puzzleThemeCapturingDefender => 'Capture o defensor'; + String get puzzleThemeCapturingDefender => 'Capturar o defensor'; @override - String get puzzleThemeCapturingDefenderDescription => 'Remover uma peça que seja importante na defesa de outra, permitindo que agora a peça indefesa seja capturada na jogada seguinte.'; + String get puzzleThemeCapturingDefenderDescription => 'Remove uma peça que seja crítica para a defesa de outra peça, permitindo que esta seja capturada na próxima jogada.'; @override - String get puzzleThemeCrushing => 'Punindo'; + String get puzzleThemeCrushing => 'Esmagar'; @override - String get puzzleThemeCrushingDescription => 'Perceba a capivarada do oponente para obter uma vantagem decisiva. (vantagem ≥ 600cp)'; + String get puzzleThemeCrushingDescription => 'Descobre um erro grave do oponente e obtém uma vantagem esmagadora. (avaliação ≥ 600cp)'; @override - String get puzzleThemeDoubleBishopMate => 'Mate de dois bispos'; + String get puzzleThemeDoubleBishopMate => 'Mate com dois bispos'; @override - String get puzzleThemeDoubleBishopMateDescription => 'Dois bispos atacantes em diagonais adjacentes dão um mate em um rei obstruído por peças amigas.'; + String get puzzleThemeDoubleBishopMateDescription => 'Dois bispos em diagonais adjacentes dão mate ao rei inimigo obstruído por peças aliadas.'; @override - String get puzzleThemeDovetailMate => 'Mate da cauda de andorinha'; + String get puzzleThemeDovetailMate => 'Mate cauda-de-andorinha'; @override - String get puzzleThemeDovetailMateDescription => 'Uma dama dá um mate em um rei adjacente, cujos únicos dois quadrados de fuga estão obstruídos por peças amigas.'; + String get puzzleThemeDovetailMateDescription => 'Uma dama dá mate ao rei inimigo cujas jogadas de escape estão bloqueadas por peças aliadas.'; @override String get puzzleThemeEquality => 'Igualdade'; @override - String get puzzleThemeEqualityDescription => 'Saia de uma posição perdida, e assegure um empate ou uma posição equilibrada. (aval ≤ 200cp)'; + String get puzzleThemeEqualityDescription => 'Recupera de uma posição perdedora e garante um empate ou uma posição de equilíbrio. (avaliação ≤ 200cp)'; @override - String get puzzleThemeKingsideAttack => 'Ataque na ala do Rei'; + String get puzzleThemeKingsideAttack => 'Ataque no lado do rei'; @override - String get puzzleThemeKingsideAttackDescription => 'Um ataque ao rei do oponente, após ele ter efetuado o roque curto.'; + String get puzzleThemeKingsideAttackDescription => 'Um ataque ao rei do adversário, após este ter feito roque menor (para o lado do rei).'; @override - String get puzzleThemeClearance => 'Lance útil'; + String get puzzleThemeClearance => 'Limpeza'; @override - String get puzzleThemeClearanceDescription => 'Um lance, às vezes consumindo tempos, que libera uma casa, fileira ou diagonal para uma ideia tática em seguida.'; + String get puzzleThemeClearanceDescription => 'Uma jogada, com tempo, que limpa uma casa, fila, coluna ou diagonal para uma ideia tática subsequente.'; @override String get puzzleThemeDefensiveMove => 'Movimento defensivo'; @override - String get puzzleThemeDefensiveMoveDescription => 'Um movimento preciso ou sequência de movimentos que são necessários para evitar perda de material ou outra vantagem.'; + String get puzzleThemeDefensiveMoveDescription => 'Um movimento ou sequência de movimentos precisos, necessários para evitar uma desvantagem, como por exemplo perda de material.'; @override String get puzzleThemeDeflection => 'Desvio'; @override - String get puzzleThemeDeflectionDescription => 'Um movimento que desvia a peça do oponente da sua função, por exemplo a de defesa de outra peça ou a defesa de uma casa importante.'; + String get puzzleThemeDeflectionDescription => 'Uma jogada que distrai uma peça do adversário de outra função, como por exemplo, proteger uma casa chave. Às vezes também é chamado de sobrecarga.'; @override String get puzzleThemeDiscoveredAttack => 'Ataque descoberto'; @override - String get puzzleThemeDiscoveredAttackDescription => 'Mover uma peça que anteriormente bloqueava um ataque de uma peça de longo alcance, como por exemplo um cavalo liberando a coluna de uma torre.'; + String get puzzleThemeDiscoveredAttackDescription => 'Mover uma peça que estava a bloquear um ataque de uma peça de longo alcance, como por exemplo um cavalo que sai da frente de uma torre.'; @override String get puzzleThemeDoubleCheck => 'Xeque duplo'; @override - String get puzzleThemeDoubleCheckDescription => 'Dar Xeque com duas peças ao mesmo tempo, como resultado de um ataque descoberto onde tanto a peça que se move quanto a peça que estava sendo obstruída atacam o rei do oponente.'; + String get puzzleThemeDoubleCheckDescription => 'Fazer xeque com duas peças ao mesmo tempo, como consequência de um ataque descoberto em que tanto a peça que move como a peça que é descoberta atacam o rei do adversário.'; @override - String get puzzleThemeEndgame => 'Finais'; + String get puzzleThemeEndgame => 'Final de jogo'; @override - String get puzzleThemeEndgameDescription => 'Tática durante a última fase do jogo.'; + String get puzzleThemeEndgameDescription => 'Uma tática durante a última fase do jogo.'; @override - String get puzzleThemeEnPassantDescription => 'Uma tática envolvendo a regra do en passant, onde um peão pode capturar um peão do oponente que passou por ele usando seu movimento inicial de duas casas.'; + String get puzzleThemeEnPassantDescription => 'Uma tática que envolve a regra de \"en passant\", onde um peão pode capturar um peão adversário que o ignorou usando o seu primeiro movimento de duas casas.'; @override String get puzzleThemeExposedKing => 'Rei exposto'; @@ -5617,273 +5635,273 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get puzzleThemeExposedKingDescription => 'Uma tática que envolve um rei com poucos defensores ao seu redor, muitas vezes levando a xeque-mate.'; @override - String get puzzleThemeFork => 'Garfo (ou duplo)'; + String get puzzleThemeFork => 'Garfo'; @override - String get puzzleThemeForkDescription => 'Um movimento onde a peça movida ataca duas peças de oponente de uma só vez.'; + String get puzzleThemeForkDescription => 'Uma jogada em que uma peça ataca duas peças do adversário simultaneamente.'; @override - String get puzzleThemeHangingPiece => 'Peça pendurada'; + String get puzzleThemeHangingPiece => 'Peça desprotegida'; @override - String get puzzleThemeHangingPieceDescription => 'Uma táctica que envolve uma peça indefesa do oponente ou insuficientemente defendida e livre para ser capturada.'; + String get puzzleThemeHangingPieceDescription => 'Uma tática que envolve uma peça do adversário que não está suficientemente defendida e por isso pode ser capturada.'; @override - String get puzzleThemeHookMate => 'Xeque gancho'; + String get puzzleThemeHookMate => 'Mate gancho'; @override - String get puzzleThemeHookMateDescription => 'Xeque-mate com uma torre, um cavalo e um peão, juntamente com um peão inimigo, para limitar a fuga do rei.'; + String get puzzleThemeHookMateDescription => 'Mate com uma torre, cavalo e peão em que o rei inimigo tem as jogadas de escape bloqueadas por um peão aliado.'; @override String get puzzleThemeInterference => 'Interferência'; @override - String get puzzleThemeInterferenceDescription => 'Mover uma peça entre duas peças do oponente para deixar uma ou duas peças do oponente indefesas, como um cavalo em uma casa defendida por duas torres.'; + String get puzzleThemeInterferenceDescription => 'Jogar uma peça para uma casa entre duas peças do adversário deixando pelo menos uma delas desprotegia, como por exemplo um cavalo numa casa defendida entre duas torres.'; @override - String get puzzleThemeIntermezzo => 'Lance intermediário'; + String get puzzleThemeIntermezzo => 'Intermezzo'; @override - String get puzzleThemeIntermezzoDescription => 'Em vez de jogar o movimento esperado, primeiro realiza outro movimento criando uma ameaça imediata a que o oponente deve responder. Também conhecido como \"Zwischenzug\" ou \"In between\".'; + String get puzzleThemeIntermezzoDescription => 'Em vez de jogares o movimento esperado, primeiro interpõe outro movimento colocando uma ameaça imediata à qual o oponente deve responder. Também conhecido como \"Zwischenzug\" ou jogada intermédia.'; @override - String get puzzleThemeKnightEndgame => 'Finais de Cavalo'; + String get puzzleThemeKnightEndgame => 'Final de cavalo'; @override - String get puzzleThemeKnightEndgameDescription => 'Um final jogado apenas com cavalos e peões.'; + String get puzzleThemeKnightEndgameDescription => 'Um final de jogo com apenas cavalos e peões.'; @override - String get puzzleThemeLong => 'Quebra-cabeças longo'; + String get puzzleThemeLong => 'Problema longo'; @override - String get puzzleThemeLongDescription => 'Vitória em três movimentos.'; + String get puzzleThemeLongDescription => 'Três movimentos para ganhar.'; @override - String get puzzleThemeMaster => 'Partidas de mestres'; + String get puzzleThemeMaster => 'Jogos de mestres'; @override - String get puzzleThemeMasterDescription => 'Quebra-cabeças de partidas jogadas por jogadores titulados.'; + String get puzzleThemeMasterDescription => 'Problemas de partidas jogadas por jogadores titulados.'; @override - String get puzzleThemeMasterVsMaster => 'Partidas de Mestre vs Mestre'; + String get puzzleThemeMasterVsMaster => 'Jogos de Mestre vs Mestre'; @override - String get puzzleThemeMasterVsMasterDescription => 'Quebra-cabeças de partidas entre dois jogadores titulados.'; + String get puzzleThemeMasterVsMasterDescription => 'Partidas jogadas entre dois jogadores titulados.'; @override String get puzzleThemeMate => 'Xeque-mate'; @override - String get puzzleThemeMateDescription => 'Vença o jogo com estilo.'; + String get puzzleThemeMateDescription => 'Vence a partida com estilo.'; @override String get puzzleThemeMateIn1 => 'Mate em 1'; @override - String get puzzleThemeMateIn1Description => 'Dar xeque-mate em um movimento.'; + String get puzzleThemeMateIn1Description => 'Faz xeque-mate num movimento.'; @override String get puzzleThemeMateIn2 => 'Mate em 2'; @override - String get puzzleThemeMateIn2Description => 'Dar xeque-mate em dois movimentos.'; + String get puzzleThemeMateIn2Description => 'Faz xeque-mate em dois movimentos.'; @override String get puzzleThemeMateIn3 => 'Mate em 3'; @override - String get puzzleThemeMateIn3Description => 'Dar xeque-mate em três movimentos.'; + String get puzzleThemeMateIn3Description => 'Faz xeque-mate em três movimentos.'; @override String get puzzleThemeMateIn4 => 'Mate em 4'; @override - String get puzzleThemeMateIn4Description => 'Dar xeque-mate em 4 movimentos.'; + String get puzzleThemeMateIn4Description => 'Faz xeque-mate em quatro movimentos.'; @override String get puzzleThemeMateIn5 => 'Mate em 5 ou mais'; @override - String get puzzleThemeMateIn5Description => 'Descubra uma longa sequência de mate.'; + String get puzzleThemeMateIn5Description => 'Descobre uma longa sequência que leva ao xeque-mate.'; @override String get puzzleThemeMiddlegame => 'Meio-jogo'; @override - String get puzzleThemeMiddlegameDescription => 'Tática durante a segunda fase do jogo.'; + String get puzzleThemeMiddlegameDescription => 'Uma tática durante a segunda fase do jogo.'; @override - String get puzzleThemeOneMove => 'Quebra-cabeças de um movimento'; + String get puzzleThemeOneMove => 'Problema de um movimento'; @override - String get puzzleThemeOneMoveDescription => 'Quebra-cabeças de um movimento.'; + String get puzzleThemeOneMoveDescription => 'Um problema que é resolvido com apenas um movimento.'; @override String get puzzleThemeOpening => 'Abertura'; @override - String get puzzleThemeOpeningDescription => 'Tática durante a primeira fase do jogo.'; + String get puzzleThemeOpeningDescription => 'Uma tática durante a primeira fase do jogo.'; @override - String get puzzleThemePawnEndgame => 'Finais de peões'; + String get puzzleThemePawnEndgame => 'Final de peões'; @override - String get puzzleThemePawnEndgameDescription => 'Um final apenas com peões.'; + String get puzzleThemePawnEndgameDescription => 'Um final de jogo só com peões.'; @override String get puzzleThemePin => 'Cravada'; @override - String get puzzleThemePinDescription => 'Uma tática envolvendo cravada, onde uma peça é incapaz de mover-se sem abrir um descoberto em uma peça de maior valor.'; + String get puzzleThemePinDescription => 'Uma tática que envolve cravadas, onde uma peça é incapaz de se mover sem revelar um ataque a uma peça de valor superior.'; @override String get puzzleThemePromotion => 'Promoção'; @override - String get puzzleThemePromotionDescription => 'Promova um peão para uma dama ou a uma peça menor.'; + String get puzzleThemePromotionDescription => 'Promova o teu peão a uma dama ou numa peça menor.'; @override - String get puzzleThemeQueenEndgame => 'Finais de Dama'; + String get puzzleThemeQueenEndgame => 'Final de dama'; @override String get puzzleThemeQueenEndgameDescription => 'Um final com apenas damas e peões.'; @override - String get puzzleThemeQueenRookEndgame => 'Finais de Dama e Torre'; + String get puzzleThemeQueenRookEndgame => 'Dama e torre'; @override - String get puzzleThemeQueenRookEndgameDescription => 'Finais com apenas Dama, Torre e Peões.'; + String get puzzleThemeQueenRookEndgameDescription => 'Um final de jogo só com damas, torres e peões.'; @override - String get puzzleThemeQueensideAttack => 'Ataque na ala da dama'; + String get puzzleThemeQueensideAttack => 'Ataque no lado da dama'; @override - String get puzzleThemeQueensideAttackDescription => 'Um ataque ao rei adversário, após ter efetuado o roque na ala da Dama.'; + String get puzzleThemeQueensideAttackDescription => 'Um ataque ao rei do adversário, após este ter feito roque grande (para o lado da dama).'; @override - String get puzzleThemeQuietMove => 'Lance de preparação'; + String get puzzleThemeQuietMove => 'Jogada subtil'; @override - String get puzzleThemeQuietMoveDescription => 'Um lance que não dá xeque nem realiza uma captura, mas prepara uma ameaça inevitável para a jogada seguinte.'; + String get puzzleThemeQuietMoveDescription => 'Um movimento que não faz uma cheque nem captura, mas prepara uma ameaça inevitável.'; @override - String get puzzleThemeRookEndgame => 'Finais de Torres'; + String get puzzleThemeRookEndgame => 'Final de torre'; @override - String get puzzleThemeRookEndgameDescription => 'Um final com apenas torres e peões.'; + String get puzzleThemeRookEndgameDescription => 'Um final de jogo com apenas torres e peões.'; @override String get puzzleThemeSacrifice => 'Sacrifício'; @override - String get puzzleThemeSacrificeDescription => 'Uma tática envolvendo a entrega de material no curto prazo, com o objetivo de se obter uma vantagem após uma sequência forçada de movimentos.'; + String get puzzleThemeSacrificeDescription => 'Uma tática que involve abdicar de material a curto prazo, para ganhar uma vantagem após uma sequência forçada de movimentos.'; @override - String get puzzleThemeShort => 'Quebra-cabeças curto'; + String get puzzleThemeShort => 'Problema curto'; @override - String get puzzleThemeShortDescription => 'Vitória em dois lances.'; + String get puzzleThemeShortDescription => 'Duas jogadas para ganhar.'; @override - String get puzzleThemeSkewer => 'Raio X'; + String get puzzleThemeSkewer => 'Cravada inversa'; @override - String get puzzleThemeSkewerDescription => 'Um movimento que envolve uma peça de alto valor sendo atacada fugindo do ataque e permitindo que uma peça de menor valor seja capturada ou atacada, o inverso de cravada.'; + String get puzzleThemeSkewerDescription => 'Uma tática que envolve uma peça de alto valor que está ser atacada, mas ao afastar-se, permite que uma peça de menor valor, que estava atrás dela, seja capturada ou atacada. É o inverso da cravada.'; @override - String get puzzleThemeSmotheredMate => 'Mate de Philidor (mate sufocado)'; + String get puzzleThemeSmotheredMate => 'Mate de Philidor'; @override - String get puzzleThemeSmotheredMateDescription => 'Um xeque-mate dado por um cavalo onde o rei é incapaz de mover-se porque está cercado (ou sufocado) pelas próprias peças.'; + String get puzzleThemeSmotheredMateDescription => 'Uma xeque-mate feito por um cavalo em que o rei não se pode mover porque está rodeado pelas suas próprias peças. Também conhecido como mate sufocado.'; @override - String get puzzleThemeSuperGM => 'Super partidas de GMs'; + String get puzzleThemeSuperGM => 'Jogos de Super GM'; @override - String get puzzleThemeSuperGMDescription => 'Quebra-cabeças de partidas jogadas pelos melhores jogadores do mundo.'; + String get puzzleThemeSuperGMDescription => 'Problemas de partidas jogadas pelos melhores jogadores do mundo.'; @override - String get puzzleThemeTrappedPiece => 'Peça presa'; + String get puzzleThemeTrappedPiece => 'Peça encurralada'; @override - String get puzzleThemeTrappedPieceDescription => 'Uma peça é incapaz de escapar da captura, pois tem movimentos limitados.'; + String get puzzleThemeTrappedPieceDescription => 'Uma peça não consegue escapar à captura, pois tem movimentos limitados.'; @override String get puzzleThemeUnderPromotion => 'Subpromoção'; @override - String get puzzleThemeUnderPromotionDescription => 'Promover para cavalo, bispo ou torre.'; + String get puzzleThemeUnderPromotionDescription => 'Promoção para um cavalo, bispo ou torre.'; @override - String get puzzleThemeVeryLong => 'Quebra-cabeças muito longo'; + String get puzzleThemeVeryLong => 'Problema muito longo'; @override - String get puzzleThemeVeryLongDescription => 'Quatro movimentos ou mais para vencer.'; + String get puzzleThemeVeryLongDescription => 'Quatro jogadas para ganhar.'; @override - String get puzzleThemeXRayAttack => 'Ataque em raio X'; + String get puzzleThemeXRayAttack => 'Ataque raio-X'; @override - String get puzzleThemeXRayAttackDescription => 'Uma peça ataca ou defende uma casa indiretamente, através de uma peça adversária.'; + String get puzzleThemeXRayAttackDescription => 'Uma peça ataque ou defende uma casa através de uma peça inimiga.'; @override String get puzzleThemeZugzwang => 'Zugzwang'; @override - String get puzzleThemeZugzwangDescription => 'O adversário tem os seus movimentos limitados, e qualquer movimento que ele faça vai enfraquecer sua própria posição.'; + String get puzzleThemeZugzwangDescription => 'O adversário está limitado quanto aos seus movimentos, e todas as jogadas pioram a sua posição.'; @override - String get puzzleThemeHealthyMix => 'Combinação saudável'; + String get puzzleThemeHealthyMix => 'Mistura saudável'; @override - String get puzzleThemeHealthyMixDescription => 'Um pouco de tudo. Você nunca sabe o que vai encontrar, então esteja pronto para tudo! Igualzinho aos jogos em tabuleiros reais.'; + String get puzzleThemeHealthyMixDescription => 'Um pouco de tudo. Não sabes o que esperar, então ficas pronto para qualquer coisa! Exatamente como em jogos de verdade.'; @override - String get puzzleThemePlayerGames => 'Partidas de jogadores'; + String get puzzleThemePlayerGames => 'Jogos de jogadores'; @override - String get puzzleThemePlayerGamesDescription => 'Procure quebra-cabeças gerados a partir de suas partidas ou das de outro jogador.'; + String get puzzleThemePlayerGamesDescription => 'Procura problemas gerados a partir dos teus jogos ou de jogos de outro jogador.'; @override String puzzleThemePuzzleDownloadInformation(String param) { - return 'Esses quebra-cabeças estão em domínio público, e você pode baixá-los em $param.'; + return 'Esses problemas são do domínio público e podem ser obtidos em $param.'; } @override - String get searchSearch => 'Buscar'; + String get searchSearch => 'Procurar'; @override String get settingsSettings => 'Configurações'; @override - String get settingsCloseAccount => 'Encerrar conta'; + String get settingsCloseAccount => 'Encerrar a conta'; @override - String get settingsManagedAccountCannotBeClosed => 'Sua conta é gerenciada, e não pode ser encerrada.'; + String get settingsManagedAccountCannotBeClosed => 'A sua conta é gerida e não pode ser encerrada.'; @override - String get settingsClosingIsDefinitive => 'O encerramento é definitivo. Não há como desfazer. Tem certeza?'; + String get settingsClosingIsDefinitive => 'O encerramento é definitivo. Não podes voltar atrás. Tens a certeza?'; @override - String get settingsCantOpenSimilarAccount => 'Você não poderá abrir uma nova conta com o mesmo nome, mesmo que alterne entre maiúsculas e minúsculas.'; + String get settingsCantOpenSimilarAccount => 'Não poderá criar uma nova conta com o mesmo nome, mesmo que as maiúsculas ou minúsculas sejam diferentes.'; @override - String get settingsChangedMindDoNotCloseAccount => 'Eu mudei de ideia, não encerre minha conta'; + String get settingsChangedMindDoNotCloseAccount => 'Mudei de ideias, não encerrem a minha conta'; @override - String get settingsCloseAccountExplanation => 'Tem certeza de que deseja encerrar sua conta? Encerrar sua conta é uma decisão permanente. Você NUNCA MAIS será capaz de entrar com ela novamente.'; + String get settingsCloseAccountExplanation => 'Tens a certeza que queres encerrar sua conta? Encerrar a tua conta é uma decisão permanente. Tu NUNCA MAIS serás capaz de iniciar sessão nesta conta.'; @override String get settingsThisAccountIsClosed => 'Esta conta foi encerrada.'; @override - String get playWithAFriend => 'Jogar contra um amigo'; + String get playWithAFriend => 'Jogar com um amigo'; @override String get playWithTheMachine => 'Jogar contra o computador'; @override - String get toInviteSomeoneToPlayGiveThisUrl => 'Para convidar alguém para jogar, envie este URL'; + String get toInviteSomeoneToPlayGiveThisUrl => 'Para convidares alguém para jogar, envia este URL'; @override String get gameOver => 'Fim da partida'; @@ -5892,13 +5910,13 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get waitingForOpponent => 'Aguardando oponente'; @override - String get orLetYourOpponentScanQrCode => 'Ou deixe seu oponente ler este QR Code'; + String get orLetYourOpponentScanQrCode => 'Ou deixa o teu oponente ler este código QR'; @override - String get waiting => 'Aguardando'; + String get waiting => 'A aguardar'; @override - String get yourTurn => 'Sua vez'; + String get yourTurn => 'É a tua vez'; @override String aiNameLevelAiLevel(String param1, String param2) { @@ -5912,7 +5930,7 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get strength => 'Nível'; @override - String get toggleTheChat => 'Ativar/Desativar chat'; + String get toggleTheChat => 'Ativar/Desativar o chat'; @override String get chat => 'Chat'; @@ -5933,10 +5951,10 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get black => 'Pretas'; @override - String get asWhite => 'de brancas'; + String get asWhite => 'com as brancas'; @override - String get asBlack => 'de pretas'; + String get asBlack => 'com as pretas'; @override String get randomColor => 'Cor aleatória'; @@ -5951,16 +5969,16 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get blackIsVictorious => 'Pretas vencem'; @override - String get youPlayTheWhitePieces => 'Você joga com as peças brancas'; + String get youPlayTheWhitePieces => 'Tu jogas com as peças brancas'; @override - String get youPlayTheBlackPieces => 'Você joga com as peças pretas'; + String get youPlayTheBlackPieces => 'Tu jogas com as peças pretas'; @override - String get itsYourTurn => 'É a sua vez!'; + String get itsYourTurn => 'É a tua vez!'; @override - String get cheatDetected => 'Trapaça Detectada'; + String get cheatDetected => 'Fraude detetada'; @override String get kingInTheCenter => 'Rei no centro'; @@ -5975,22 +5993,22 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get variantEnding => 'Fim da variante'; @override - String get newOpponent => 'Novo oponente'; + String get newOpponent => 'Novo adversário'; @override - String get yourOpponentWantsToPlayANewGameWithYou => 'Seu oponente quer jogar uma nova partida contra você'; + String get yourOpponentWantsToPlayANewGameWithYou => 'O teu adversário quer jogar outra vez contra ti'; @override String get joinTheGame => 'Entrar no jogo'; @override - String get whitePlays => 'Brancas jogam'; + String get whitePlays => 'Jogam as brancas'; @override - String get blackPlays => 'Pretas jogam'; + String get blackPlays => 'Jogam as pretas'; @override - String get opponentLeftChoices => 'O seu oponente deixou a partida. Você pode reivindicar vitória, declarar empate ou aguardar.'; + String get opponentLeftChoices => 'O teu adversário deixou a partida. Podes reivindicar vitória, declarar empate ou aguardar.'; @override String get forceResignation => 'Reivindicar vitória'; @@ -5999,22 +6017,22 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get forceDraw => 'Reivindicar empate'; @override - String get talkInChat => 'Por favor, seja gentil no chat!'; + String get talkInChat => 'Por favor, sê gentil na conversa!'; @override - String get theFirstPersonToComeOnThisUrlWillPlayWithYou => 'A primeira pessoa que acessar esta URL jogará contigo.'; + String get theFirstPersonToComeOnThisUrlWillPlayWithYou => 'A primeira pessoa que aceder a este link jogará contra ti.'; @override - String get whiteResigned => 'Brancas desistiram'; + String get whiteResigned => 'As brancas desistiram'; @override - String get blackResigned => 'Pretas desistiram'; + String get blackResigned => 'As pretas desistiram'; @override - String get whiteLeftTheGame => 'Brancas deixaram a partida'; + String get whiteLeftTheGame => 'As brancas deixaram a partida'; @override - String get blackLeftTheGame => 'Pretas deixaram a partida'; + String get blackLeftTheGame => 'As pretas deixaram a partida'; @override String get whiteDidntMove => 'As brancas não se moveram'; @@ -6023,10 +6041,10 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get blackDidntMove => 'As pretas não se moveram'; @override - String get requestAComputerAnalysis => 'Solicitar uma análise do computador'; + String get requestAComputerAnalysis => 'Solicitar uma análise de computador'; @override - String get computerAnalysis => 'Análise do computador'; + String get computerAnalysis => 'Análise de computador'; @override String get computerAnalysisAvailable => 'Análise de computador disponível'; @@ -6043,22 +6061,22 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { } @override - String get usingServerAnalysis => 'Análise de servidor em uso'; + String get usingServerAnalysis => 'A usar a análise do servidor'; @override - String get loadingEngine => 'Carregando ...'; + String get loadingEngine => 'A carregar o motor de jogo...'; @override - String get calculatingMoves => 'Calculando jogadas...'; + String get calculatingMoves => 'A calcular as jogadas...'; @override - String get engineFailed => 'Erro ao carregar o engine'; + String get engineFailed => 'Erro ao carregar o motor'; @override String get cloudAnalysis => 'Análise na nuvem'; @override - String get goDeeper => 'Detalhar'; + String get goDeeper => 'Aprofundar'; @override String get showThreat => 'Mostrar ameaça'; @@ -6067,37 +6085,37 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get inLocalBrowser => 'no navegador local'; @override - String get toggleLocalEvaluation => 'Ativar/Desativar análise local'; + String get toggleLocalEvaluation => 'Ligar/desligar a avaliação local'; @override String get promoteVariation => 'Promover variante'; @override - String get makeMainLine => 'Transformar em linha principal'; + String get makeMainLine => 'Tornar variante principal'; @override - String get deleteFromHere => 'Excluir a partir daqui'; + String get deleteFromHere => 'Eliminar a partir de aqui'; @override - String get collapseVariations => 'Esconder variantes'; + String get collapseVariations => 'Recolher variações'; @override - String get expandVariations => 'Mostrar variantes'; + String get expandVariations => 'Expandir variações'; @override - String get forceVariation => 'Variante forçada'; + String get forceVariation => 'Forçar variante'; @override - String get copyVariationPgn => 'Copiar PGN da variante'; + String get copyVariationPgn => 'Copiar variação PGN'; @override - String get move => 'Movimentos'; + String get move => 'Jogada'; @override - String get variantLoss => 'Derrota da variante'; + String get variantLoss => 'Variante perdida'; @override - String get variantWin => 'Vitória da variante'; + String get variantWin => 'Variante ganha'; @override String get insufficientMaterial => 'Material insuficiente'; @@ -6112,26 +6130,26 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get close => 'Fechar'; @override - String get winning => 'Vencendo'; + String get winning => 'Ganhas'; @override - String get losing => 'Perdendo'; + String get losing => 'Perdidas'; @override - String get drawn => 'Empate'; + String get drawn => 'Empatado'; @override - String get unknown => 'Posição desconhecida'; + String get unknown => 'Desconhecidos'; @override - String get database => 'Banco de Dados'; + String get database => 'Base de dados'; @override String get whiteDrawBlack => 'Brancas / Empate / Pretas'; @override String averageRatingX(String param) { - return 'Classificação média: $param'; + return 'Pontuação média: $param'; } @override @@ -6142,20 +6160,20 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { @override String masterDbExplanation(String param1, String param2, String param3) { - return 'Duas milhões de partidas de jogadores com pontuação FIDE acima de $param1, desde $param2 a $param3'; + return 'de partidas OTB de jogadores com +$param1 rating FIDE de $param2 a $param3'; } @override - String get dtzWithRounding => 'DTZ50\" com arredondamento, baseado no número de meias-jogadas até a próxima captura ou jogada de peão'; + String get dtzWithRounding => 'DTZ50\'\' com arredondamento, baseado no número de meios-movimentos até à próxima captura ou movimento de peão'; @override - String get noGameFound => 'Nenhuma partida encontrada'; + String get noGameFound => 'Nenhum jogo encontrado'; @override - String get maxDepthReached => 'Profundidade máxima alcançada!'; + String get maxDepthReached => 'Nível máximo alcançado!'; @override - String get maybeIncludeMoreGamesFromThePreferencesMenu => 'Talvez você queira incluir mais jogos a partir do menu de preferências'; + String get maybeIncludeMoreGamesFromThePreferencesMenu => 'Talvez incluir mais jogos no menu de preferências?'; @override String get openings => 'Aberturas'; @@ -6168,47 +6186,47 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { @override String xOpeningExplorer(String param) { - return '$param Explorador de aberturas'; + return 'Explorador de aberturas de $param'; } @override - String get playFirstOpeningEndgameExplorerMove => 'Jogue o primeiro lance do explorador de aberturas/finais'; + String get playFirstOpeningEndgameExplorerMove => 'Jogar o primeiro lance do explorador de aberturas/finais'; @override String get winPreventedBy50MoveRule => 'Vitória impedida pela regra dos 50 movimentos'; @override - String get lossSavedBy50MoveRule => 'Derrota impedida pela regra dos 50 movimentos'; + String get lossSavedBy50MoveRule => 'Derrota evitada pela regra dos 50 movimentos'; @override - String get winOr50MovesByPriorMistake => 'Vitória ou 50 movimentos por erro anterior'; + String get winOr50MovesByPriorMistake => 'Vitória ou 50 movimentos por engano anterior'; @override - String get lossOr50MovesByPriorMistake => 'Derrota ou 50 movimentos por erro anterior'; + String get lossOr50MovesByPriorMistake => 'Vitória ou 50 movimentos por engano anterior'; @override - String get unknownDueToRounding => 'Vitória/derrota garantida somente se a variante recomendada tiver sido seguida desde o último movimento de captura ou de peão, devido ao possível arredondamento.'; + String get unknownDueToRounding => 'Vitória/derrota garantida apenas se a linha da tabela recomendada tiver sido seguida desde a última captura ou movimento de peão, devido a possível arredondamento.'; @override - String get allSet => 'Tudo pronto!'; + String get allSet => 'Tudo a postos!'; @override String get importPgn => 'Importar PGN'; @override - String get delete => 'Excluir'; + String get delete => 'Eliminar'; @override - String get deleteThisImportedGame => 'Excluir este jogo importado?'; + String get deleteThisImportedGame => 'Eliminar este jogo importado?'; @override - String get replayMode => 'Rever a partida'; + String get replayMode => 'Modo de repetição'; @override - String get realtimeReplay => 'Tempo Real'; + String get realtimeReplay => 'Tempo real'; @override - String get byCPL => 'Por erros'; + String get byCPL => 'Por CPL'; @override String get openStudy => 'Abrir estudo'; @@ -6220,13 +6238,13 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get bestMoveArrow => 'Seta de melhor movimento'; @override - String get showVariationArrows => 'Mostrar setas das variantes'; + String get showVariationArrows => 'Ver setas de variação'; @override - String get evaluationGauge => 'Escala de avaliação'; + String get evaluationGauge => 'Medidor da avaliação'; @override - String get multipleLines => 'Linhas de análise'; + String get multipleLines => 'Múltiplas continuações'; @override String get cpus => 'CPUs'; @@ -6238,13 +6256,13 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get infiniteAnalysis => 'Análise infinita'; @override - String get removesTheDepthLimit => 'Remove o limite de profundidade, o que aquece seu computador'; + String get removesTheDepthLimit => 'Remove o limite de profundidade e mantém o teu computador quente'; @override - String get engineManager => 'Gerenciador de engine'; + String get engineManager => 'Gestão do motor'; @override - String get blunder => 'Capivarada'; + String get blunder => 'Erro grave'; @override String get mistake => 'Erro'; @@ -6253,16 +6271,16 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get inaccuracy => 'Imprecisão'; @override - String get moveTimes => 'Tempo por movimento'; + String get moveTimes => 'Tempo das jogadas'; @override - String get flipBoard => 'Girar o tabuleiro'; + String get flipBoard => 'Inverter o tabuleiro'; @override - String get threefoldRepetition => 'Tripla repetição'; + String get threefoldRepetition => 'Repetição tripla'; @override - String get claimADraw => 'Reivindicar empate'; + String get claimADraw => 'Declarar empate'; @override String get offerDraw => 'Propor empate'; @@ -6277,7 +6295,7 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get fiftyMovesWithoutProgress => 'Cinquenta jogadas sem progresso'; @override - String get currentGames => 'Partidas atuais'; + String get currentGames => 'Partidas a decorrer'; @override String get viewInFullSize => 'Ver em tela cheia'; @@ -6289,16 +6307,16 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get signIn => 'Entrar'; @override - String get rememberMe => 'Lembrar de mim'; + String get rememberMe => 'Lembrar-me'; @override - String get youNeedAnAccountToDoThat => 'Você precisa de uma conta para fazer isso'; + String get youNeedAnAccountToDoThat => 'Precisas de uma conta para fazeres isso'; @override - String get signUp => 'Registrar'; + String get signUp => 'Registar-se'; @override - String get computersAreNotAllowedToPlay => 'A ajuda de software não é permitida. Por favor, não utilize programas de xadrez, bancos de dados ou o auxilio de outros jogadores durante a partida. Além disso, a criação de múltiplas contas é fortemente desaconselhada e sua prática excessiva acarretará em banimento.'; + String get computersAreNotAllowedToPlay => 'Computadores ou jogadores assistidos por computador não estão autorizados a jogar. Por favor não utilizes assistência de programas de xadrez, bases de dados ou outros jogadores enquanto estiveres a jogar. Além disso, a criação de contas múltiplas é fortemente desencorajada e a sua prática excessiva acarretará banimento.'; @override String get games => 'Partidas'; @@ -6321,7 +6339,7 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get friends => 'Amigos'; @override - String get discussions => 'Discussões'; + String get discussions => 'Conversas'; @override String get today => 'Hoje'; @@ -6339,10 +6357,10 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get variants => 'Variantes'; @override - String get timeControl => 'Ritmo'; + String get timeControl => 'Ritmo de jogo'; @override - String get realTime => 'Tempo real'; + String get realTime => 'Em tempo real'; @override String get correspondence => 'Correspondência'; @@ -6357,37 +6375,37 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get time => 'Tempo'; @override - String get rating => 'Rating'; + String get rating => 'Pontuação'; @override - String get ratingStats => 'Estatísticas de classificação'; + String get ratingStats => 'Estatísticas de pontuação'; @override - String get username => 'Nome de usuário'; + String get username => 'Nome de utilizador'; @override - String get usernameOrEmail => 'Nome ou email do usuário'; + String get usernameOrEmail => 'Nome ou e-mail do utilizador'; @override - String get changeUsername => 'Alterar nome de usuário'; + String get changeUsername => 'Alterar o nome de utilizador'; @override - String get changeUsernameNotSame => 'Pode-se apenas trocar as letras de minúscula para maiúscula e vice-versa. Por exemplo, \"fulanodetal\" para \"FulanoDeTal\".'; + String get changeUsernameNotSame => 'Só te é permitido trocar as letras de minúscula para maiúscula e vice-versa. Por exemplo, \"johndoe\" para \"JohnDoe\".'; @override - String get changeUsernameDescription => 'Altere seu nome de usuário. Isso só pode ser feito uma vez e você poderá apenas trocar as letras de minúscula para maiúscula e vice-versa.'; + String get changeUsernameDescription => 'Altera o teu nome de utilizador. Isso só pode ser feito uma vez e só poderás trocar as letras de minúscula para maiúscula e vice-versa.'; @override - String get signupUsernameHint => 'Escolha um nome de usuário apropriado. Não será possível mudá-lo, e qualquer conta que tiver um nome ofensivo ou inapropriado será excluída!'; + String get signupUsernameHint => 'Certifique-se que escolhe um nome de utilizador decoroso. Não poderá alterá-lo mais tarde e quaisquer contas com nomes de utilizador inapropriados serão fechadas!'; @override - String get signupEmailHint => 'Vamos usar apenas para redefinir a sua senha.'; + String get signupEmailHint => 'Só o usaremos para redefinir a palavra-passe.'; @override - String get password => 'Senha'; + String get password => 'Palavra-passe'; @override - String get changePassword => 'Alterar senha'; + String get changePassword => 'Alterar a palavra-passe'; @override String get changeEmail => 'Alterar email'; @@ -6396,45 +6414,45 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get email => 'E-mail'; @override - String get passwordReset => 'Redefinição de senha'; + String get passwordReset => 'Redefinir a palavra-passe'; @override - String get forgotPassword => 'Esqueceu sua senha?'; + String get forgotPassword => 'Esqueceste-te da tua palavra-passe?'; @override - String get error_weakPassword => 'A senha é extremamente comum e fácil de adivinhar.'; + String get error_weakPassword => 'Esta senha é extremamente comum, e muito fácil de adivinhar.'; @override - String get error_namePassword => 'Não utilize seu nome de usuário como senha.'; + String get error_namePassword => 'Por favor, não usa o teu nome de utilizador como senha.'; @override - String get blankedPassword => 'Você usou a mesma senha em outro site, e esse site foi comprometido. Para garantir a segurança da sua conta no Lichess, você precisa criar uma nova senha. Agradecemos sua compreensão.'; + String get blankedPassword => 'Utilizou a mesma palavra-passe noutro site, e esse site foi comprometido. Para garantir a segurança da sua conta Lichess, precisamos que redefina a palavra-passe. Obrigado pela compreensão.'; @override - String get youAreLeavingLichess => 'Você está saindo do Lichess'; + String get youAreLeavingLichess => 'Você está a sair do Lichess'; @override - String get neverTypeYourPassword => 'Nunca digite sua senha do Lichess em outro site!'; + String get neverTypeYourPassword => 'Nunca escrevas a tua senha Lichess em outro site!'; @override String proceedToX(String param) { - return 'Ir para $param'; + return 'Continuar para $param'; } @override - String get passwordSuggestion => 'Não coloque uma senha sugerida por outra pessoa, porque ela poderá roubar sua conta.'; + String get passwordSuggestion => 'Não uses uma senha sugerida por outra pessoa. Eles vão utilizar-la para roubar a tua conta.'; @override - String get emailSuggestion => 'Não coloque um endereço de email sugerido por outra pessoa, porque ela poderá roubar sua conta.'; + String get emailSuggestion => 'Não uses um email sugerida por outra pessoa. Eles vão utilizar-la para roubar a tua conta.'; @override - String get emailConfirmHelp => 'Ajuda com confirmação por e-mail'; + String get emailConfirmHelp => 'Ajuda com a confirmação do endereço eletrónico'; @override - String get emailConfirmNotReceived => 'Não recebeu seu e-mail de confirmação após o registro?'; + String get emailConfirmNotReceived => 'Não recebeu no seu correio eletrónico uma mensagem de confirmação após o registo?'; @override - String get whatSignupUsername => 'Qual nome de usuário você usou para se registrar?'; + String get whatSignupUsername => 'Que nome de utilizador usou para se registar?'; @override String usernameNotFound(String param) { @@ -6442,24 +6460,24 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { } @override - String get usernameCanBeUsedForNewAccount => 'Você pode usar esse nome de usuário para criar uma nova conta'; + String get usernameCanBeUsedForNewAccount => 'Pode usar esse nome de utilizador para criar uma conta'; @override String emailSent(String param) { - return 'Enviamos um e-mail para $param.'; + return 'Enviámos um correio eletrónico para $param.'; } @override - String get emailCanTakeSomeTime => 'Pode levar algum tempo para chegar.'; + String get emailCanTakeSomeTime => 'Pode demorar algum tempo a chegar.'; @override - String get refreshInboxAfterFiveMinutes => 'Aguarde 5 minutos e atualize sua caixa de entrada.'; + String get refreshInboxAfterFiveMinutes => 'Aguarde 5 minutos e atualize a sua caixa de entrada de correio eletrónico.'; @override - String get checkSpamFolder => 'Verifique também a sua caixa de spam. Caso esteja lá, marque como não é spam.'; + String get checkSpamFolder => 'Verifique também a sua pasta de “spam”, pode estar lá. Se sim, assinale como não “spam”.'; @override - String get emailForSignupHelp => 'Se todo o resto falhar, envie-nos este e-mail:'; + String get emailForSignupHelp => 'Se tudo falhar, então envie-nos este correio eletrónico:'; @override String copyTextToEmail(String param) { @@ -6467,20 +6485,20 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { } @override - String get waitForSignupHelp => 'Entraremos em contato em breve para ajudá-lo a completar seu registro.'; + String get waitForSignupHelp => 'Nós entraremos brevemente em contacto para ajudá-lo a completar a inscrição.'; @override String accountConfirmed(String param) { - return 'O usuário $param foi confirmado com sucesso.'; + return 'O utilizador $param foi confirmado com sucesso.'; } @override String accountCanLogin(String param) { - return 'Você pode acessar agora como $param.'; + return 'Pode agora aceder como $param.'; } @override - String get accountConfirmationEmailNotNeeded => 'Você não precisa de um e-mail de confirmação.'; + String get accountConfirmationEmailNotNeeded => 'Não precisa de um endereço eletrónico de confirmação.'; @override String accountClosed(String param) { @@ -6489,52 +6507,52 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { @override String accountRegisteredWithoutEmail(String param) { - return 'A conta $param foi registrada sem um e-mail.'; + return 'A conta $param foi registada sem um endereço eletrónico.'; } @override - String get rank => 'Rank'; + String get rank => 'Classificação'; @override String rankX(String param) { - return 'Classificação: $param'; + return 'Posição: $param'; } @override - String get gamesPlayed => 'Partidas realizadas'; + String get gamesPlayed => 'Partidas jogadas'; @override String get cancel => 'Cancelar'; @override - String get whiteTimeOut => 'Tempo das brancas esgotado'; + String get whiteTimeOut => 'Acabou o tempo das brancas'; @override - String get blackTimeOut => 'Tempo das pretas esgotado'; + String get blackTimeOut => 'Acabou o tempo das pretas'; @override String get drawOfferSent => 'Proposta de empate enviada'; @override - String get drawOfferAccepted => 'Proposta de empate aceita'; + String get drawOfferAccepted => 'Proposta de empate aceite'; @override String get drawOfferCanceled => 'Proposta de empate cancelada'; @override - String get whiteOffersDraw => 'Brancas oferecem empate'; + String get whiteOffersDraw => 'As brancas propõem empate'; @override - String get blackOffersDraw => 'Pretas oferecem empate'; + String get blackOffersDraw => 'As pretas propõem empate'; @override - String get whiteDeclinesDraw => 'Brancas recusam empate'; + String get whiteDeclinesDraw => 'As brancas recusam o empate'; @override - String get blackDeclinesDraw => 'Pretas recusam empate'; + String get blackDeclinesDraw => 'As pretas recusam o empate'; @override - String get yourOpponentOffersADraw => 'Seu adversário oferece empate'; + String get yourOpponentOffersADraw => 'O teu adversário propõe empate'; @override String get accept => 'Aceitar'; @@ -6543,16 +6561,16 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get decline => 'Recusar'; @override - String get playingRightNow => 'Jogando agora'; + String get playingRightNow => 'A jogar agora'; @override - String get eventInProgress => 'Jogando agora'; + String get eventInProgress => 'A decorrer agora'; @override String get finished => 'Terminado'; @override - String get abortGame => 'Cancelar partida'; + String get abortGame => 'Cancelar a partida'; @override String get gameAborted => 'Partida cancelada'; @@ -6570,64 +6588,64 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get mode => 'Modo'; @override - String get casual => 'Amistosa'; + String get casual => 'Amigável'; @override - String get rated => 'Ranqueada'; + String get rated => 'A valer pontos'; @override - String get casualTournament => 'Amistoso'; + String get casualTournament => 'Amigável'; @override - String get ratedTournament => 'Valendo pontos'; + String get ratedTournament => 'A valer pontos'; @override String get thisGameIsRated => 'Esta partida vale pontos'; @override - String get rematch => 'Revanche'; + String get rematch => 'Desforra'; @override - String get rematchOfferSent => 'Oferta de revanche enviada'; + String get rematchOfferSent => 'Pedido de desforra enviado'; @override - String get rematchOfferAccepted => 'Oferta de revanche aceita'; + String get rematchOfferAccepted => 'Pedido de desforra aceite'; @override - String get rematchOfferCanceled => 'Oferta de revanche cancelada'; + String get rematchOfferCanceled => 'Pedido de desforra cancelado'; @override - String get rematchOfferDeclined => 'Oferta de revanche recusada'; + String get rematchOfferDeclined => 'Pedido de desforra recusado'; @override - String get cancelRematchOffer => 'Cancelar oferta de revanche'; + String get cancelRematchOffer => 'Cancelar o pedido de desforra'; @override - String get viewRematch => 'Ver revanche'; + String get viewRematch => 'Ver a desforra'; @override - String get confirmMove => 'Confirmar lance'; + String get confirmMove => 'Confirmar o lance'; @override String get play => 'Jogar'; @override - String get inbox => 'Mensagens'; + String get inbox => 'Caixa de entrada'; @override String get chatRoom => 'Sala de chat'; @override - String get loginToChat => 'Faça login para conversar'; + String get loginToChat => 'Inicia sessão para poderes conversar'; @override - String get youHaveBeenTimedOut => 'Sua sessão expirou.'; + String get youHaveBeenTimedOut => 'Foste impedido de conversar por agora.'; @override - String get spectatorRoom => 'Sala do espectador'; + String get spectatorRoom => 'Sala dos espectadores'; @override - String get composeMessage => 'Escrever mensagem'; + String get composeMessage => 'Escrever uma mensagem'; @override String get subject => 'Assunto'; @@ -6636,7 +6654,7 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get send => 'Enviar'; @override - String get incrementInSeconds => 'Acréscimo em segundos'; + String get incrementInSeconds => 'Incremento em segundos'; @override String get freeOnlineChess => 'Xadrez Online Gratuito'; @@ -6645,34 +6663,34 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get exportGames => 'Exportar partidas'; @override - String get ratingRange => 'Rating entre'; + String get ratingRange => 'Pontuação entre'; @override - String get thisAccountViolatedTos => 'Esta conta violou os Termos de Serviço do Lichess'; + String get thisAccountViolatedTos => 'Esta conta violou os termos de serviço do Lichess'; @override - String get openingExplorerAndTablebase => 'Explorador de abertura & tabela de finais'; + String get openingExplorerAndTablebase => 'Explorador de aberturas & tabelas de finais'; @override - String get takeback => 'Voltar jogada'; + String get takeback => 'Voltar uma jogada atrás'; @override - String get proposeATakeback => 'Propor voltar jogada'; + String get proposeATakeback => 'Propor voltar uma jogada atrás'; @override - String get takebackPropositionSent => 'Proposta de voltar jogada enviada'; + String get takebackPropositionSent => 'Proposta de voltar uma jogada atrás enviada'; @override - String get takebackPropositionDeclined => 'Proposta de voltar jogada recusada'; + String get takebackPropositionDeclined => 'Proposta de voltar uma jogada atrás recusada'; @override - String get takebackPropositionAccepted => 'Proposta de voltar jogada aceita'; + String get takebackPropositionAccepted => 'Proposta de voltar uma jogada atrás aceite'; @override - String get takebackPropositionCanceled => 'Proposta de voltar jogada cancelada'; + String get takebackPropositionCanceled => 'Proposta de voltar uma jogada atrás cancelada'; @override - String get yourOpponentProposesATakeback => 'Seu oponente propõe voltar jogada'; + String get yourOpponentProposesATakeback => 'O teu adversário propõe voltar uma jogada atrás'; @override String get bookmarkThisGame => 'Adicionar esta partida às favoritas'; @@ -6684,38 +6702,38 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get tournaments => 'Torneios'; @override - String get tournamentPoints => 'Pontos de torneios'; + String get tournamentPoints => 'Pontos de torneio'; @override - String get viewTournament => 'Ver torneio'; + String get viewTournament => 'Ver o torneio'; @override String get backToTournament => 'Voltar ao torneio'; @override - String get noDrawBeforeSwissLimit => 'Não é possível empatar antes de 30 lances em um torneio suíço.'; + String get noDrawBeforeSwissLimit => 'Num torneio suíço não pode empatar antes de 30 jogadas.'; @override String get thematic => 'Temático'; @override String yourPerfRatingIsProvisional(String param) { - return 'Seu rating $param é provisório'; + return 'A tua pontuação em $param é provisória'; } @override String yourPerfRatingIsTooHigh(String param1, String param2) { - return 'Seu $param1 rating ($param2) é muito alta'; + return 'A tua pontuação em $param1 ($param2) é demasiado alta'; } @override String yourTopWeeklyPerfRatingIsTooHigh(String param1, String param2) { - return 'Seu melhor rating $param1 da semana ($param2) é muito alto'; + return 'A tua pontuação máxima nesta semana em $param1 ($param2) é demasiado alta'; } @override String yourPerfRatingIsTooLow(String param1, String param2) { - return 'Sua $param1 pontuação ($param2) é muito baixa'; + return 'A tua pontuação em $param1 ($param2) é demasiado baixa'; } @override @@ -6730,40 +6748,40 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { @override String mustBeInTeam(String param) { - return 'Precisa estar na equipe $param'; + return 'Tens de pertencer à equipa $param'; } @override String youAreNotInTeam(String param) { - return 'Você não está na equipe $param'; + return 'Não estás na equipa $param'; } @override - String get backToGame => 'Retorne à partida'; + String get backToGame => 'Voltar à partida'; @override - String get siteDescription => 'Xadrez online gratuito. Jogue xadrez agora numa interface simples. Sem registro, sem anúncios, sem plugins. Jogue xadrez contra computador, amigos ou adversários aleatórios.'; + String get siteDescription => 'Xadrez online gratuito. Joga xadrez numa interface simples. Sem registos, sem anúncios, sem plugins. Joga xadrez com o computador, amigos ou adversários aleatórios.'; @override String xJoinedTeamY(String param1, String param2) { - return '$param1 juntou-se à equipe $param2'; + return '$param1 juntou-se à equipa $param2'; } @override String xCreatedTeamY(String param1, String param2) { - return '$param1 criou a equipe $param2'; + return '$param1 criou a equipa $param2'; } @override - String get startedStreaming => 'começou uma transmissão ao vivo'; + String get startedStreaming => 'começou uma stream'; @override String xStartedStreaming(String param) { - return '$param começou a transmitir'; + return '$param começou uma stream'; } @override - String get averageElo => 'Média de rating'; + String get averageElo => 'Pontuação média'; @override String get location => 'Localização'; @@ -6772,70 +6790,70 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get filterGames => 'Filtrar partidas'; @override - String get reset => 'Reiniciar'; + String get reset => 'Voltar ao original'; @override String get apply => 'Aplicar'; @override - String get save => 'Salvar'; + String get save => 'Guardar'; @override - String get leaderboard => 'Classificação'; + String get leaderboard => 'Tabela de liderança'; @override - String get screenshotCurrentPosition => 'Captura de tela da posição atual'; + String get screenshotCurrentPosition => 'Posição atual da captura de ecrã'; @override - String get gameAsGIF => 'Salvar a partida como GIF'; + String get gameAsGIF => 'Jogo como GIF'; @override - String get pasteTheFenStringHere => 'Cole a notação FEN aqui'; + String get pasteTheFenStringHere => 'Coloca a notação FEN aqui'; @override - String get pasteThePgnStringHere => 'Cole a notação PGN aqui'; + String get pasteThePgnStringHere => 'Coloca a notação PGN aqui'; @override - String get orUploadPgnFile => 'Ou carregue um arquivo PGN'; + String get orUploadPgnFile => 'Ou enviar um ficheiro PGN'; @override - String get fromPosition => 'A partir da posição'; + String get fromPosition => 'A partir de uma posição'; @override - String get continueFromHere => 'Continuar daqui'; + String get continueFromHere => 'Continuar a partir daqui'; @override String get toStudy => 'Estudo'; @override - String get importGame => 'Importar partida'; + String get importGame => 'Importar uma partida'; @override - String get importGameExplanation => 'Após colar uma partida em PGN você poderá revisá-la interativamente, consultar uma análise de computador, utilizar o chat e compartilhar um link.'; + String get importGameExplanation => 'Coloca aqui o PGN de um jogo, para teres acesso a navegar pela repetição,\nanálise de computador, sala de chat do jogo e link de partilha.'; @override - String get importGameCaveat => 'As variantes serão apagadas. Para salvá-las, importe o PGN em um estudo.'; + String get importGameCaveat => 'As variações serão apagadas. Para mantê-las, importe o PGN através de um estudo.'; @override - String get importGameDataPrivacyWarning => 'Este PGN pode ser acessado publicamente. Use um estudo para importar um jogo privado.'; + String get importGameDataPrivacyWarning => 'Este PGN pode ser acessada pelo público. Para importar um jogo de forma privada, use um estudo.'; @override - String get thisIsAChessCaptcha => 'Este é um CAPTCHA enxadrístico.'; + String get thisIsAChessCaptcha => 'Este é um \"CAPTCHA\" de xadrez.'; @override - String get clickOnTheBoardToMakeYourMove => 'Clique no tabuleiro para fazer seu lance, provando que é humano.'; + String get clickOnTheBoardToMakeYourMove => 'Clica no tabuleiro para fazeres a tua jogada, provando que és humano.'; @override - String get captcha_fail => 'Por favor, resolva o captcha enxadrístico.'; + String get captcha_fail => 'Por favor, resolve o captcha.'; @override - String get notACheckmate => 'Não é xeque-mate'; + String get notACheckmate => 'Não é xeque-mate.'; @override - String get whiteCheckmatesInOneMove => 'As brancas dão mate em um lance'; + String get whiteCheckmatesInOneMove => 'As brancas dão mate em um movimento'; @override - String get blackCheckmatesInOneMove => 'As pretas dão mate em um lance'; + String get blackCheckmatesInOneMove => 'As pretas dão mate em um movimento'; @override String get retry => 'Tentar novamente'; @@ -6844,7 +6862,7 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get reconnecting => 'Reconectando'; @override - String get noNetwork => 'Sem conexão'; + String get noNetwork => 'Desligado'; @override String get favoriteOpponents => 'Adversários favoritos'; @@ -6853,10 +6871,10 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get follow => 'Seguir'; @override - String get following => 'Seguindo'; + String get following => 'A seguir'; @override - String get unfollow => 'Parar de seguir'; + String get unfollow => 'Deixar de seguir'; @override String followX(String param) { @@ -6878,7 +6896,7 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get unblock => 'Desbloquear'; @override - String get followsYou => 'Segue você'; + String get followsYou => 'Segue-te'; @override String xStartedFollowingY(String param1, String param2) { @@ -6918,13 +6936,13 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get winner => 'Vencedor'; @override - String get standing => 'Colocação'; + String get standing => 'Classificação'; @override - String get createANewTournament => 'Criar novo torneio'; + String get createANewTournament => 'Criar um torneio'; @override - String get tournamentCalendar => 'Calendário do torneio'; + String get tournamentCalendar => 'Calendário de torneios'; @override String get conditionOfEntry => 'Condições de participação:'; @@ -6933,16 +6951,16 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get advancedSettings => 'Configurações avançadas'; @override - String get safeTournamentName => 'Escolha um nome seguro para o torneio.'; + String get safeTournamentName => 'Escolhe um nome totalmente seguro para o torneio.'; @override - String get inappropriateNameWarning => 'Até mesmo a menor indecência poderia ensejar o encerramento de sua conta.'; + String get inappropriateNameWarning => 'Até uma linguagem ligeiramente inadequada pode levar ao encerramento da tua conta.'; @override - String get emptyTournamentName => 'Deixe em branco para dar ao torneio o nome de um grande mestre aleatório.'; + String get emptyTournamentName => 'Deixe em branco e será atribuído um nome aleatório de um jogador notável ao torneio.'; @override - String get makePrivateTournament => 'Faça o torneio privado e restrinja o acesso com uma senha'; + String get makePrivateTournament => 'Torna o torneio privado e restrinje o acesso com uma palavra-passe'; @override String get join => 'Entrar'; @@ -6963,71 +6981,71 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get createdBy => 'Criado por'; @override - String get tournamentIsStarting => 'O torneio está começando'; + String get tournamentIsStarting => 'O torneio está a começar'; @override - String get tournamentPairingsAreNowClosed => 'Os pareamentos do torneio estão fechados agora.'; + String get tournamentPairingsAreNowClosed => 'Os emparelhamentos no torneio já estão fechados.'; @override String standByX(String param) { - return '$param, aguarde: o pareamento está em andamento, prepare-se!'; + return 'Aguarda $param, estamos a emparelhar jogadores, prepara-te!'; } @override - String get pause => 'Pausar'; + String get pause => 'Pausa'; @override String get resume => 'Continuar'; @override - String get youArePlaying => 'Você está participando!'; + String get youArePlaying => 'Estás a jogar!'; @override String get winRate => 'Taxa de vitórias'; @override - String get berserkRate => 'Taxa Berserk'; + String get berserkRate => 'Taxa de partidas no modo frenético'; @override String get performance => 'Desempenho'; @override - String get tournamentComplete => 'Torneio completo'; + String get tournamentComplete => 'Torneio terminado'; @override - String get movesPlayed => 'Movimentos realizados'; + String get movesPlayed => 'Movimentos feitos'; @override - String get whiteWins => 'Brancas venceram'; + String get whiteWins => 'Vitórias com as brancas'; @override - String get blackWins => 'Pretas venceram'; + String get blackWins => 'Vitórias com as pretas'; @override - String get drawRate => 'Taxa de empates'; + String get drawRate => 'Taxa de empate'; @override String get draws => 'Empates'; @override String nextXTournament(String param) { - return 'Próximo torneio $param:'; + return 'Próximo torneio de $param:'; } @override - String get averageOpponent => 'Pontuação média adversários'; + String get averageOpponent => 'Pontuação média dos adversários'; @override String get boardEditor => 'Editor de tabuleiro'; @override - String get setTheBoard => 'Defina a posição'; + String get setTheBoard => 'Partilha o tabuleiro'; @override String get popularOpenings => 'Aberturas populares'; @override - String get endgamePositions => 'Posições de final'; + String get endgamePositions => 'Posições finais'; @override String chess960StartPosition(String param) { @@ -7038,10 +7056,10 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get startPosition => 'Posição inicial'; @override - String get clearBoard => 'Limpar tabuleiro'; + String get clearBoard => 'Limpar o tabuleiro'; @override - String get loadPosition => 'Carregar posição'; + String get loadPosition => 'Carregar uma posição'; @override String get isPrivate => 'Privado'; @@ -7053,34 +7071,34 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { @override String profileCompletion(String param) { - return 'Conclusão do perfil: $param'; + return 'Perfil completo: $param'; } @override String xRating(String param) { - return 'Rating $param'; + return 'Pontuação $param'; } @override - String get ifNoneLeaveEmpty => 'Se nenhuma, deixe vazio'; + String get ifNoneLeaveEmpty => 'Se não existir, deixa em branco'; @override String get profile => 'Perfil'; @override - String get editProfile => 'Editar perfil'; + String get editProfile => 'Editar o perfil'; @override - String get realName => 'Nome real'; + String get realName => 'Nome Real'; @override - String get setFlair => 'Escolha seu emote'; + String get setFlair => 'Defina o teu estilo'; @override String get flair => 'Estilo'; @override - String get youCanHideFlair => 'Você pode esconder todos os emotes de usuário no site.'; + String get youCanHideFlair => 'Há uma opção para ocultar todos os estilos dos utilizadores em todo o site.'; @override String get biography => 'Biografia'; @@ -7092,22 +7110,22 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get thankYou => 'Obrigado!'; @override - String get socialMediaLinks => 'Links de mídia social'; + String get socialMediaLinks => 'Links das redes sociais'; @override - String get oneUrlPerLine => 'Uma URL por linha.'; + String get oneUrlPerLine => 'Um URL por linha.'; @override - String get inlineNotation => 'Notação em linha'; + String get inlineNotation => 'Anotações em linha'; @override - String get makeAStudy => 'Para salvar e compartilhar uma análise, crie um estudo.'; + String get makeAStudy => 'Para guardar e partilhar, considere fazer um estudo.'; @override - String get clearSavedMoves => 'Limpar lances'; + String get clearSavedMoves => 'Limpar jogadas'; @override - String get previouslyOnLichessTV => 'Anteriormente em Lichess TV'; + String get previouslyOnLichessTV => 'Anteriormente na TV Lichess'; @override String get onlinePlayers => 'Jogadores online'; @@ -7116,19 +7134,19 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get activePlayers => 'Jogadores ativos'; @override - String get bewareTheGameIsRatedButHasNoClock => 'Cuidado, o jogo vale rating, mas não há controle de tempo!'; + String get bewareTheGameIsRatedButHasNoClock => 'Cuidado, o jogo vale pontos, mas não há limite de tempo!'; @override String get success => 'Sucesso'; @override - String get automaticallyProceedToNextGameAfterMoving => 'Passar automaticamente ao jogo seguinte após o lance'; + String get automaticallyProceedToNextGameAfterMoving => 'Passar automaticamente ao jogo seguinte após a jogada'; @override String get autoSwitch => 'Alternar automaticamente'; @override - String get puzzles => 'Quebra-cabeças'; + String get puzzles => 'Problemas'; @override String get onlineBots => 'Bots online'; @@ -7143,7 +7161,7 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get descPrivate => 'Descrição privada'; @override - String get descPrivateHelp => 'Texto que apenas os membros da equipe verão. Se definido, substitui a descrição pública para os membros da equipe.'; + String get descPrivateHelp => 'Texto que apenas está visível para os membros da equipa. Se definido, substitui a descrição pública dos membros da equipa.'; @override String get no => 'Não'; @@ -7155,7 +7173,7 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get help => 'Ajuda:'; @override - String get createANewTopic => 'Criar novo tópico'; + String get createANewTopic => 'Criar um novo tópico'; @override String get topics => 'Tópicos'; @@ -7164,7 +7182,7 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get posts => 'Publicações'; @override - String get lastPost => 'Última postagem'; + String get lastPost => 'Última publicação'; @override String get views => 'Visualizações'; @@ -7182,13 +7200,13 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get message => 'Mensagem'; @override - String get createTheTopic => 'Criar tópico'; + String get createTheTopic => 'Criar o tópico'; @override - String get reportAUser => 'Reportar um usuário'; + String get reportAUser => 'Denunciar um utilizador'; @override - String get user => 'Usuário'; + String get user => 'Utilizador'; @override String get reason => 'Motivo'; @@ -7197,7 +7215,7 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get whatIsIheMatter => 'Qual é o motivo?'; @override - String get cheat => 'Trapaça'; + String get cheat => 'Batota'; @override String get troll => 'Troll'; @@ -7206,10 +7224,10 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get other => 'Outro'; @override - String get reportDescriptionHelp => 'Cole o link do(s) jogo(s) e explique o que há de errado com o comportamento do usuário. Não diga apenas \"ele trapaceia\", informe-nos como chegou a esta conclusão. Sua denúncia será processada mais rapidamente se escrita em inglês.'; + String get reportDescriptionHelp => 'Inclui o link do(s) jogo(s) e explica o que há de errado com o comportamento deste utilizador. Não digas apenas \"ele faz batota\"; informa-nos como chegaste a essa conclusão. A tua denúncia será processada mais rapidamente se for escrita em inglês.'; @override - String get error_provideOneCheatedGameLink => 'Por favor forneça ao menos um link para um jogo com suspeita de trapaça.'; + String get error_provideOneCheatedGameLink => 'Por favor, fornece-nos pelo menos um link para um jogo onde tenha havido batota.'; @override String by(String param) { @@ -7222,7 +7240,7 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { } @override - String get thisTopicIsNowClosed => 'O tópico foi fechado.'; + String get thisTopicIsNowClosed => 'Este tópico foi fechado.'; @override String get blog => 'Blog'; @@ -7231,46 +7249,46 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get notes => 'Notas'; @override - String get typePrivateNotesHere => 'Digite notas pessoais aqui'; + String get typePrivateNotesHere => 'Escreve notas privadas aqui'; @override - String get writeAPrivateNoteAboutThisUser => 'Escreva uma nota pessoal sobre este usuário'; + String get writeAPrivateNoteAboutThisUser => 'Escreva uma nota privada sobre este utilizador'; @override - String get noNoteYet => 'Nenhuma nota'; + String get noNoteYet => 'Ainda sem notas'; @override - String get invalidUsernameOrPassword => 'Nome de usuário ou senha incorretos'; + String get invalidUsernameOrPassword => 'Nome de utilizador ou palavra-passe incorretos'; @override - String get incorrectPassword => 'Senha incorreta'; + String get incorrectPassword => 'Palavra-passe incorreta'; @override - String get invalidAuthenticationCode => 'Código de verificação inválido'; + String get invalidAuthenticationCode => 'Código de autenticação inválido'; @override - String get emailMeALink => 'Me envie um link'; + String get emailMeALink => 'Envie-me um link por e-mail'; @override - String get currentPassword => 'Senha atual'; + String get currentPassword => 'Palavra-passe atual'; @override - String get newPassword => 'Nova senha'; + String get newPassword => 'Nova palavra-chave'; @override - String get newPasswordAgain => 'Nova senha (novamente)'; + String get newPasswordAgain => 'Nova palavra-passe (novamente)'; @override - String get newPasswordsDontMatch => 'As novas senhas não correspondem'; + String get newPasswordsDontMatch => 'As novas palavras-passe não coincidem'; @override - String get newPasswordStrength => 'Senha forte'; + String get newPasswordStrength => 'Força da palavra-passe'; @override - String get clockInitialTime => 'Tempo de relógio'; + String get clockInitialTime => 'Tempo inicial no relógio'; @override - String get clockIncrement => 'Incremento do relógio'; + String get clockIncrement => 'Incremento no relógio'; @override String get privacy => 'Privacidade'; @@ -7279,13 +7297,13 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get privacyPolicy => 'Política de privacidade'; @override - String get letOtherPlayersFollowYou => 'Permitir que outros jogadores sigam você'; + String get letOtherPlayersFollowYou => 'Permitir que outros jogadores te sigam'; @override - String get letOtherPlayersChallengeYou => 'Permitir que outros jogadores desafiem você'; + String get letOtherPlayersChallengeYou => 'Permitir que outros jogadores te desafiem'; @override - String get letOtherPlayersInviteYouToStudy => 'Deixe outros jogadores convidá-lo para um estudo'; + String get letOtherPlayersInviteYouToStudy => 'Permitir que outros jogadores te convidem para estudos'; @override String get sound => 'Som'; @@ -7312,7 +7330,7 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get allSquaresOfTheBoard => 'Todas as casas do tabuleiro'; @override - String get onSlowGames => 'Em partidas lentas'; + String get onSlowGames => 'Em jogos lentos'; @override String get always => 'Sempre'; @@ -7333,33 +7351,33 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { @override String victoryVsYInZ(String param1, String param2, String param3) { - return '$param1 vs $param2 em $param3'; + return '$param1 contra $param2 em $param3'; } @override String defeatVsYInZ(String param1, String param2, String param3) { - return '$param1 vs $param2 em $param3'; + return '$param1 contra $param2 em $param3'; } @override String drawVsYInZ(String param1, String param2, String param3) { - return '$param1 vs $param2 em $param3'; + return '$param1 contra $param2 em $param3'; } @override - String get timeline => 'Linha do tempo'; + String get timeline => 'Cronologia'; @override - String get starting => 'Iniciando:'; + String get starting => 'Começa às:'; @override String get allInformationIsPublicAndOptional => 'Todas as informações são públicas e opcionais.'; @override - String get biographyDescription => 'Fale sobre você, seus interesses, o que você gosta no xadrez, suas aberturas favoritas, jogadores...'; + String get biographyDescription => 'Fala de ti, do que gostas no xadrez, das tuas aberturas favoritas, jogos, jogadores...'; @override - String get listBlockedPlayers => 'Sua lista de jogadores bloqueados'; + String get listBlockedPlayers => 'Lista os jogadores que bloqueaste'; @override String get human => 'Humano'; @@ -7380,7 +7398,7 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get learnMenu => 'Aprender'; @override - String get studyMenu => 'Estudar'; + String get studyMenu => 'Estudos'; @override String get practice => 'Praticar'; @@ -7398,50 +7416,50 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get error_unknown => 'Valor inválido'; @override - String get error_required => 'Este campo deve ser preenchido'; + String get error_required => 'Este campo tem de ser preenchido'; @override String get error_email => 'Este endereço de e-mail é inválido'; @override - String get error_email_acceptable => 'Este endereço de e-mail não é válido. Verifique e tente novamente.'; + String get error_email_acceptable => 'Este endereço de e-mail não é aceitável. Por favor verifica-o e tenta outra vez.'; @override - String get error_email_unique => 'Endereço de e-mail é inválido ou já está sendo utilizado'; + String get error_email_unique => 'Endereço de e-mail inválido ou já utilizado'; @override - String get error_email_different => 'Este já é o seu endereço de e-mail'; + String get error_email_different => 'Este já é o teu endereço de e-mail'; @override String error_minLength(String param) { - return 'O mínimo de caracteres é $param'; + return 'Deve conter pelo menos $param caracteres'; } @override String error_maxLength(String param) { - return 'O máximo de caracteres é $param'; + return 'Deve conter no máximo $param caracteres'; } @override String error_min(String param) { - return 'Deve ser maior ou igual a $param'; + return 'Deve ser pelo menos $param'; } @override String error_max(String param) { - return 'Deve ser menor ou igual a $param'; + return 'Deve ser no máximo $param'; } @override String ifRatingIsPlusMinusX(String param) { - return 'Se o rating for ± $param'; + return 'Se a pontuação for ± $param'; } @override - String get ifRegistered => 'Se registrado'; + String get ifRegistered => 'Se registado'; @override - String get onlyExistingConversations => 'Apenas conversas iniciadas'; + String get onlyExistingConversations => 'Apenas conversas existentes'; @override String get onlyFriends => 'Apenas amigos'; @@ -7453,35 +7471,35 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get castling => 'Roque'; @override - String get whiteCastlingKingside => 'O-O das brancas'; + String get whiteCastlingKingside => 'Brancas O-O'; @override - String get blackCastlingKingside => 'O-O das pretas'; + String get blackCastlingKingside => 'Pretas O-O'; @override String tpTimeSpentPlaying(String param) { - return 'Tempo jogando: $param'; + return 'Tempo passado a jogar: $param'; } @override - String get watchGames => 'Assistir partidas'; + String get watchGames => 'Ver jogos'; @override String tpTimeSpentOnTV(String param) { - return 'Tempo na TV: $param'; + return 'Tempo a ser transmitido na TV: $param'; } @override - String get watch => 'Assistir'; + String get watch => 'Observar'; @override - String get videoLibrary => 'Vídeos'; + String get videoLibrary => 'Videoteca'; @override String get streamersMenu => 'Streamers'; @override - String get mobileApp => 'Aplicativo Móvel'; + String get mobileApp => 'Aplicação móvel'; @override String get webmasters => 'Webmasters'; @@ -7496,11 +7514,11 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { @override String xIsAFreeYLibreOpenSourceChessServer(String param1, String param2) { - return '$param1 é um servidor de xadrez gratuito ($param2), livre, sem anúncios e código aberto.'; + return 'O $param1 é um servidor de xadrez grátis ($param2), sem publicidades e open-source.'; } @override - String get really => 'realmente'; + String get really => 'a sério'; @override String get contribute => 'Contribuir'; @@ -7509,17 +7527,17 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get termsOfService => 'Termos de serviço'; @override - String get sourceCode => 'Código-fonte'; + String get sourceCode => 'Código fonte'; @override - String get simultaneousExhibitions => 'Exibição simultânea'; + String get simultaneousExhibitions => 'Exibições simultâneas'; @override - String get host => 'Simultanista'; + String get host => 'Anfitrião'; @override String hostColorX(String param) { - return 'Cor do simultanista: $param'; + return 'Cor do anfitrião: $param'; } @override @@ -7529,10 +7547,10 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get createdSimuls => 'Simultâneas criadas recentemente'; @override - String get hostANewSimul => 'Iniciar nova simultânea'; + String get hostANewSimul => 'Iniciar uma simultânea'; @override - String get signUpToHostOrJoinASimul => 'Entre em uma ou crie uma conta para hospedar'; + String get signUpToHostOrJoinASimul => 'Registra-te para hospedar ou juntar a uma simultânea'; @override String get noSimulFound => 'Simultânea não encontrada'; @@ -7541,88 +7559,88 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get noSimulExplanation => 'Esta exibição simultânea não existe.'; @override - String get returnToSimulHomepage => 'Retornar à página inicial da simultânea'; + String get returnToSimulHomepage => 'Voltar à página inicial da simultânea'; @override - String get aboutSimul => 'A simultânea envolve um único jogador contra vários oponentes ao mesmo tempo.'; + String get aboutSimul => 'As simultâneas envolvem um único jogador contra vários adversários ao mesmo tempo.'; @override - String get aboutSimulImage => 'Contra 50 oponentes, Fischer ganhou 47 jogos, empatou 2 e perdeu 1.'; + String get aboutSimulImage => 'Contra 50 adversários, Fischer ganhou 47 jogos, empatou 2 e perdeu 1.'; @override - String get aboutSimulRealLife => 'O conceito provém de eventos reais, nos quais o simultanista se move de mesa em mesa, executando um movimento por vez.'; + String get aboutSimulRealLife => 'O conceito provém de eventos reais, nos quais o simultanista se move de mesa em mesa, executando um movimento de cada vez.'; @override - String get aboutSimulRules => 'Quando a simultânea começa, cada jogador começa sua partida contra o simultanista, o qual sempre tem as brancas. A simultânea termina quando todas as partidas são finalizadas.'; + String get aboutSimulRules => 'Quando a simultânea começa, cada jogador começa sua partida contra o anfitrião, que joga sempre com as peças brancas. A simultânea termina quando todas as partidas tiverem acabado.'; @override - String get aboutSimulSettings => 'As simultâneas sempre são partidas amigáveis. Revanches, voltar jogadas e tempo adicional estão desativados.'; + String get aboutSimulSettings => 'As simultâneas são sempre partidas amigáveis. Desforras, voltar jogadas atrás e dar mais tempo estão desativados.'; @override String get create => 'Criar'; @override - String get whenCreateSimul => 'Quando cria uma simultânea, você joga com vários adversários ao mesmo tempo.'; + String get whenCreateSimul => 'Quando crias uma simultânea, podes jogar com vários adversários ao mesmo tempo.'; @override - String get simulVariantsHint => 'Se você selecionar diversas variantes, cada jogador poderá escolher qual delas jogar.'; + String get simulVariantsHint => 'Se selecionares diversas variantes, cada jogador poderá escolher qual delas jogar.'; @override - String get simulClockHint => 'Configuração de acréscimos no relógio. Quanto mais jogadores admitir, mais tempo pode necessitar.'; + String get simulClockHint => 'Configuração de incrementos no relógio. Quanto mais jogadores admitires, mais tempo poderás necessitar.'; @override - String get simulAddExtraTime => 'Você pode acrescentar tempo adicional a seu relógio, para ajudá-lo a lidar com a simultânea.'; + String get simulAddExtraTime => 'Podes acrescentar tempo adicional ao teu relógio, para te ajudar a lidar com a simultânea.'; @override - String get simulHostExtraTime => 'Tempo adicional do simultanista'; + String get simulHostExtraTime => 'Tempo adicional do anfitrião'; @override - String get simulAddExtraTimePerPlayer => 'Adicionar tempo inicial ao seu relógio por cada jogador adversário que entrar na simultânea.'; + String get simulAddExtraTimePerPlayer => 'Adicione tempo inicial ao seu relógio para cada jogador que entra na simulação.'; @override - String get simulHostExtraTimePerPlayer => 'Tempo adicional do simultanista por jogador'; + String get simulHostExtraTimePerPlayer => 'Tempo extra de relógio por jogador'; @override String get lichessTournaments => 'Torneios do Lichess'; @override - String get tournamentFAQ => 'Perguntas Frequentes sobre torneios no estilo Arena'; + String get tournamentFAQ => 'Perguntas frequentes sobre torneios em arena'; @override - String get timeBeforeTournamentStarts => 'Contagem regressiva para início do torneio'; + String get timeBeforeTournamentStarts => 'Contagem decrescente para o início do torneio'; @override - String get averageCentipawnLoss => 'Perda média em centipeões'; + String get averageCentipawnLoss => 'Perda média de centésimos de peão'; @override String get accuracy => 'Precisão'; @override - String get keyboardShortcuts => 'Atalhos de teclado'; + String get keyboardShortcuts => 'Atalhos do teclado'; @override - String get keyMoveBackwardOrForward => 'retroceder/avançar lance'; + String get keyMoveBackwardOrForward => 'retroceder/avançar jogada'; @override String get keyGoToStartOrEnd => 'ir para início/fim'; @override - String get keyCycleSelectedVariation => 'Alternar entre as variantes'; + String get keyCycleSelectedVariation => 'Ciclo da variante selecionada'; @override - String get keyShowOrHideComments => 'mostrar/ocultar comentários'; + String get keyShowOrHideComments => 'mostrar/ocultar os comentários'; @override String get keyEnterOrExitVariation => 'entrar/sair da variante'; @override - String get keyRequestComputerAnalysis => 'Solicite análise do computador, aprenda com seus erros'; + String get keyRequestComputerAnalysis => 'Solicite análise do computador, Aprenda com seus erros'; @override - String get keyNextLearnFromYourMistakes => 'Próximo (Aprenda com seus erros)'; + String get keyNextLearnFromYourMistakes => 'Seguinte (Aprenda com os seus erros)'; @override - String get keyNextBlunder => 'Próximo erro grave'; + String get keyNextBlunder => 'Próxima gafe'; @override String get keyNextMistake => 'Próximo erro'; @@ -7631,19 +7649,19 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get keyNextInaccuracy => 'Próxima imprecisão'; @override - String get keyPreviousBranch => 'Branch anterior'; + String get keyPreviousBranch => 'Ramo anterior'; @override - String get keyNextBranch => 'Próximo branch'; + String get keyNextBranch => 'Próximo ramo'; @override - String get toggleVariationArrows => 'Ativar/desativar setas'; + String get toggleVariationArrows => 'Ativar/desactivar seta da variante'; @override - String get cyclePreviousOrNextVariation => 'Variante seguinte/anterior'; + String get cyclePreviousOrNextVariation => 'Ciclo anterior/próxima variante'; @override - String get toggleGlyphAnnotations => 'Ativar/desativar anotações'; + String get toggleGlyphAnnotations => 'Ativar/desativar anotações com símbolos'; @override String get togglePositionAnnotations => 'Ativar/desativar anotações de posição'; @@ -7652,16 +7670,16 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get variationArrowsInfo => 'Setas de variação permitem navegar sem usar a lista de movimentos.'; @override - String get playSelectedMove => 'jogar movimento selecionado'; + String get playSelectedMove => 'jogar o movimento selecionado'; @override String get newTournament => 'Novo torneio'; @override - String get tournamentHomeTitle => 'Torneios de xadrez com diversos controles de tempo e variantes'; + String get tournamentHomeTitle => 'Torneios de xadrez com diversos ritmos de jogo e variantes'; @override - String get tournamentHomeDescription => 'Jogue xadrez em ritmo acelerado! Entre em um torneio oficial agendado ou crie seu próprio. Bullet, Blitz, Clássico, Chess960, King of the Hill, Três Xeques e outras modalidades disponíveis para uma ilimitada diversão enxadrística.'; + String get tournamentHomeDescription => 'Joga xadrez em ritmo acelerado! Entra num torneio oficial agendado ou cria o teu próprio. Bullet, Rápida, Clássica, Chess960, Rei da Colina, Três Xeques e mais opções disponíveis para uma diversão ilimitada.'; @override String get tournamentNotFound => 'Torneio não encontrado'; @@ -7670,109 +7688,109 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get tournamentDoesNotExist => 'Este torneio não existe.'; @override - String get tournamentMayHaveBeenCanceled => 'O evento pode ter sido cancelado, se todos os jogadores saíram antes de seu início.'; + String get tournamentMayHaveBeenCanceled => 'O torneio pode ter sido cancelado, se todos os jogadores tiverem saíram antes do seu começo.'; @override - String get returnToTournamentsHomepage => 'Volte à página inicial de torneios'; + String get returnToTournamentsHomepage => 'Voltar à página inicial de torneios'; @override String weeklyPerfTypeRatingDistribution(String param) { - return 'Distribuição mensal de rating em $param'; + return 'Distribuição semanal de pontuação em $param'; } @override String yourPerfTypeRatingIsRating(String param1, String param2) { - return 'Seu rating em $param1 é $param2.'; + return 'A tua pontuação em $param1 é $param2.'; } @override String youAreBetterThanPercentOfPerfTypePlayers(String param1, String param2) { - return 'Você é melhor que $param1 dos jogadores de $param2.'; + return 'És melhor que $param1 dos jogadores de $param2.'; } @override String userIsBetterThanPercentOfPerfTypePlayers(String param1, String param2, String param3) { - return '$param1 é melhor que $param2 dos $param3 jogadores.'; + return '$param1 é melhor que $param2 dos jogadores de $param3.'; } @override String betterThanPercentPlayers(String param1, String param2) { - return 'Melhor que $param1 dos jogadores de $param2'; + return 'Melhor que $param1 de $param2 jogadores'; } @override String youDoNotHaveAnEstablishedPerfTypeRating(String param) { - return 'Você não tem rating definido em $param.'; + return 'Não tens uma pontuação estabelecida em $param.'; } @override - String get yourRating => 'Seu rating'; + String get yourRating => 'A tua pontuação'; @override - String get cumulative => 'Cumulativo'; + String get cumulative => 'Acumulativo'; @override - String get glicko2Rating => 'Rating Glicko-2'; + String get glicko2Rating => 'Pontuação Glicko-2'; @override - String get checkYourEmail => 'Verifique seu e-mail'; + String get checkYourEmail => 'Verifica o teu e-mail'; @override - String get weHaveSentYouAnEmailClickTheLink => 'Enviamos um e-mail. Clique no link do e-mail para ativar sua conta.'; + String get weHaveSentYouAnEmailClickTheLink => 'Enviámos-te um e-mail. Clica no link nesse e-mail para ativares a tua conta.'; @override - String get ifYouDoNotSeeTheEmailCheckOtherPlaces => 'Se você não vir o e-mail, verifique outros locais onde possa estar, como lixeira, spam ou outras pastas.'; + String get ifYouDoNotSeeTheEmailCheckOtherPlaces => 'Se você não vires o e-mail, verifica outros locais onde este possa estar, como pastas de lixo, spam, social ou outras.'; @override String weHaveSentYouAnEmailTo(String param) { - return 'Enviamos um e-mail para $param. Clique no link do e-mail para redefinir sua senha.'; + return 'Enviámos-te um e-mail para $param. Clica no link nesse e-mail para redefinires a tua palavra-passe.'; } @override String byRegisteringYouAgreeToBeBoundByOur(String param) { - return 'Ao registrar, você concorda em se comprometer com nossa $param.'; + return 'Ao criares uma conta, concordas comprometeres-te com os nossos $param.'; } @override String readAboutOur(String param) { - return 'Leia sobre a nossa $param.'; + return 'Lê sobre a nossa $param.'; } @override - String get networkLagBetweenYouAndLichess => 'Atraso na rede'; + String get networkLagBetweenYouAndLichess => 'Atraso na rede entre ti e o Lichess'; @override String get timeToProcessAMoveOnLichessServer => 'Tempo para processar um movimento no servidor do Lichess'; @override - String get downloadAnnotated => 'Baixar anotação'; + String get downloadAnnotated => 'Transferir anotação'; @override - String get downloadRaw => 'Baixar texto'; + String get downloadRaw => 'Transferir texto'; @override - String get downloadImported => 'Baixar partida importada'; + String get downloadImported => 'Transferir a partida importada'; @override String get crosstable => 'Tabela'; @override - String get youCanAlsoScrollOverTheBoardToMoveInTheGame => 'Você também pode rolar sobre o tabuleiro para percorrer as jogadas.'; + String get youCanAlsoScrollOverTheBoardToMoveInTheGame => 'Também podes rodar a rodinha do rato sobre o tabuleiro para percorreres as jogadas na partida.'; @override - String get scrollOverComputerVariationsToPreviewThem => 'Passe o mouse pelas variações do computador para visualizá-las.'; + String get scrollOverComputerVariationsToPreviewThem => 'Passe o rato sobre as variantes do computador para visualizá-las.'; @override - String get analysisShapesHowTo => 'Pressione Shift+Clique ou clique com o botão direito do mouse para desenhar círculos e setas no tabuleiro.'; + String get analysisShapesHowTo => 'Pressiona shift+clique ou clica com o botão direito do rato para desenhares círculos e setas no tabuleiro.'; @override - String get letOtherPlayersMessageYou => 'Permitir que outros jogadores lhe enviem mensagem'; + String get letOtherPlayersMessageYou => 'Permitir que outros jogadores te enviem mensagens'; @override - String get receiveForumNotifications => 'Receba notificações quando você for mencionado no fórum'; + String get receiveForumNotifications => 'Olá aOlá a todos'; @override - String get shareYourInsightsData => 'Compartilhe seus dados da análise'; + String get shareYourInsightsData => 'Compartilhar os teus dados de \"insights\" de xadrez'; @override String get withNobody => 'Com ninguém'; @@ -7787,24 +7805,24 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get kidMode => 'Modo infantil'; @override - String get kidModeIsEnabled => 'O modo infantil está ativado.'; + String get kidModeIsEnabled => 'Modo infantil está ativado.'; @override - String get kidModeExplanation => 'Isto diz respeito à segurança. No modo infantil, todas as comunicações do site são desabilitadas. Habilite isso para seus filhos e alunos, para protegê-los de outros usuários da Internet.'; + String get kidModeExplanation => 'Iso é sobre segurança. No modo infantil, todas as comunicações do site ficam desactivadas. Activa esta opção para os teus filhos ou alunos, para protegê-los de outros utilizadores da internet.'; @override String inKidModeTheLichessLogoGetsIconX(String param) { - return 'No modo infantil, a logo do lichess tem um ícone $param, para que você saiba que suas crianças estão seguras.'; + return 'No modo criança, o logótipo do Lichess fica com um ícone $param para que saibas que as tuas crianças estão seguras.'; } @override - String get askYourChessTeacherAboutLiftingKidMode => 'Sua conta é gerenciada. Para desativar o modo infantil, peça ao seu professor.'; + String get askYourChessTeacherAboutLiftingKidMode => 'A sua conta é gerida. Peça ao seu professor de xadrez para retirar o modo infantil.'; @override - String get enableKidMode => 'Habilitar o modo infantil'; + String get enableKidMode => 'Ativar o modo infantil'; @override - String get disableKidMode => 'Desabilitar o modo infantil'; + String get disableKidMode => 'Desativar o modo infantil'; @override String get security => 'Segurança'; @@ -7813,10 +7831,10 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get sessions => 'Sessões'; @override - String get revokeAllSessions => 'revogar todas as sessões'; + String get revokeAllSessions => 'desativar todas as sessões'; @override - String get playChessEverywhere => 'Jogue xadrez em qualquer lugar'; + String get playChessEverywhere => 'Joga xadrez em qualquer lugar'; @override String get asFreeAsLichess => 'Tão gratuito quanto o Lichess'; @@ -7825,7 +7843,7 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get builtForTheLoveOfChessNotMoney => 'Desenvolvido pelo amor ao xadrez, não pelo dinheiro'; @override - String get everybodyGetsAllFeaturesForFree => 'Todos têm todos os recursos de graça'; + String get everybodyGetsAllFeaturesForFree => 'Todos têm todos os recursos gratuitamente'; @override String get zeroAdvertisement => 'Zero anúncios'; @@ -7834,7 +7852,7 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get fullFeatured => 'Cheio de recursos'; @override - String get phoneAndTablet => 'Celular e tablet'; + String get phoneAndTablet => 'Telemóvel e tablet'; @override String get bulletBlitzClassical => 'Bullet, blitz, clássico'; @@ -7843,20 +7861,20 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get correspondenceChess => 'Xadrez por correspondência'; @override - String get onlineAndOfflinePlay => 'Jogue online e offline'; + String get onlineAndOfflinePlay => 'Jogar online e offline'; @override - String get viewTheSolution => 'Ver solução'; + String get viewTheSolution => 'Ver a solução'; @override - String get followAndChallengeFriends => 'Siga e desafie amigos'; + String get followAndChallengeFriends => 'Joga e desafia amigos'; @override String get gameAnalysis => 'Análise da partida'; @override String xHostsY(String param1, String param2) { - return '$param1 criou $param2'; + return '$param1 criou a exibição simultânea $param2'; } @override @@ -7870,24 +7888,24 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { } @override - String get quickPairing => 'Pareamento rápido'; + String get quickPairing => 'Emparelhamento rápido'; @override - String get lobby => 'Salão'; + String get lobby => 'Sala de espera'; @override String get anonymous => 'Anônimo'; @override String yourScore(String param) { - return 'Sua pontuação:$param'; + return 'O teu resultado: $param'; } @override - String get language => 'Idioma'; + String get language => 'Lingua'; @override - String get background => 'Cor tema'; + String get background => 'Fundo'; @override String get light => 'Claro'; @@ -7917,37 +7935,37 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get brightness => 'Brilho'; @override - String get hue => 'Tom'; + String get hue => 'Tonalidade'; @override - String get boardReset => 'Restaurar as cores padrão'; + String get boardReset => 'Redefinir cores para o padrão'; @override - String get pieceSet => 'Estilo das peças'; + String get pieceSet => 'Peças'; @override - String get embedInYourWebsite => 'Incorporar no seu site'; + String get embedInYourWebsite => 'Incorporar no teu site'; @override - String get usernameAlreadyUsed => 'Este nome de usuário já está registado, por favor, escolha outro.'; + String get usernameAlreadyUsed => 'Este nome de utilizador já existe, por favor escolhe outro.'; @override - String get usernamePrefixInvalid => 'O nome de usuário deve começar com uma letra.'; + String get usernamePrefixInvalid => 'O nome do utilizador tem começar com uma letra.'; @override - String get usernameSuffixInvalid => 'O nome de usuário deve terminar com uma letra ou um número.'; + String get usernameSuffixInvalid => 'O nome do utilizador tem de acabar com uma letra ou número.'; @override - String get usernameCharsInvalid => 'Nomes de usuário só podem conter letras, números, sublinhados e hifens.'; + String get usernameCharsInvalid => 'O nome do utilizador só pode conter letras, números, underscores ou hífenes.'; @override - String get usernameUnacceptable => 'Este nome de usuário não é aceitável.'; + String get usernameUnacceptable => 'Este nome de utilizador não é aceitável.'; @override - String get playChessInStyle => 'Jogue xadrez com estilo'; + String get playChessInStyle => 'Jogar xadrez com estilo'; @override - String get chessBasics => 'Básicos do xadrez'; + String get chessBasics => 'O básico do xadrez'; @override String get coaches => 'Treinadores'; @@ -7971,66 +7989,66 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { @override String perfRatingX(String param) { - return 'Rating: $param'; + return 'Pontuação: $param'; } @override - String get practiceWithComputer => 'Pratique com o computador'; + String get practiceWithComputer => 'Praticar com o computador'; @override String anotherWasX(String param) { - return 'Um outro lance seria $param'; + return 'Outro seria $param'; } @override String bestWasX(String param) { - return 'Melhor seria $param'; + return '$param seria melhor'; } @override - String get youBrowsedAway => 'Você navegou para longe'; + String get youBrowsedAway => 'Saíste'; @override - String get resumePractice => 'Retornar à prática'; + String get resumePractice => 'Continuar a prática'; @override - String get drawByFiftyMoves => 'O jogo empatou pela regra dos cinquenta movimentos.'; + String get drawByFiftyMoves => 'O jogo foi empatado de acordo com a regra dos cinquenta lances.'; @override - String get theGameIsADraw => 'A partida terminou em empate.'; + String get theGameIsADraw => 'O jogo é um empate.'; @override - String get computerThinking => 'Computador pensando ...'; + String get computerThinking => 'Computador a pensar...'; @override - String get seeBestMove => 'Veja o melhor lance'; + String get seeBestMove => 'Ver o melhor movimento'; @override - String get hideBestMove => 'Esconder o melhor lance'; + String get hideBestMove => 'Ocultar o melhor movimento'; @override String get getAHint => 'Obter uma dica'; @override - String get evaluatingYourMove => 'Avaliando o seu movimento ...'; + String get evaluatingYourMove => 'A analisar o teu movimento ...'; @override - String get whiteWinsGame => 'Brancas vencem'; + String get whiteWinsGame => 'As brancas ganham'; @override - String get blackWinsGame => 'Pretas vencem'; + String get blackWinsGame => 'As pretas ganham'; @override - String get learnFromYourMistakes => 'Aprenda com seus erros'; + String get learnFromYourMistakes => 'Aprende com os teus erros'; @override - String get learnFromThisMistake => 'Aprenda com este erro'; + String get learnFromThisMistake => 'Aprende com este erro'; @override - String get skipThisMove => 'Pular esse lance'; + String get skipThisMove => 'Saltar este movimento'; @override - String get next => 'Próximo'; + String get next => 'Seguinte'; @override String xWasPlayed(String param) { @@ -8038,49 +8056,49 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { } @override - String get findBetterMoveForWhite => 'Encontrar o melhor lance para as Brancas'; + String get findBetterMoveForWhite => 'Encontra um melhor movimento para as brancas'; @override - String get findBetterMoveForBlack => 'Encontre o melhor lance para as Pretas'; + String get findBetterMoveForBlack => 'Encontra um melhor movimento para as pretas'; @override String get resumeLearning => 'Continuar a aprendizagem'; @override - String get youCanDoBetter => 'Você pode fazer melhor'; + String get youCanDoBetter => 'Podes fazer melhor'; @override - String get tryAnotherMoveForWhite => 'Tente um outro lance para as Brancas'; + String get tryAnotherMoveForWhite => 'Tenta outro movimento para as brancas'; @override - String get tryAnotherMoveForBlack => 'Tente um outro lance para as Pretas'; + String get tryAnotherMoveForBlack => 'Tenta outro movimento para as pretas'; @override String get solution => 'Solução'; @override - String get waitingForAnalysis => 'Aguardando análise'; + String get waitingForAnalysis => 'A aguardar pela análise'; @override - String get noMistakesFoundForWhite => 'Nenhum erro encontrado para as Brancas'; + String get noMistakesFoundForWhite => 'Não foram encontrados erros das brancas'; @override - String get noMistakesFoundForBlack => 'Nenhum erro encontrado para as Pretas'; + String get noMistakesFoundForBlack => 'Não foram encontrados erros das pretas'; @override - String get doneReviewingWhiteMistakes => 'Erros das brancas já revistos'; + String get doneReviewingWhiteMistakes => 'Terminada a revisão de erros das brancas'; @override - String get doneReviewingBlackMistakes => 'Erros das pretas já revistos'; + String get doneReviewingBlackMistakes => 'Terminada a revisão de erros das pretas'; @override - String get doItAgain => 'Faça novamente'; + String get doItAgain => 'Repetir'; @override - String get reviewWhiteMistakes => 'Rever erros das Brancas'; + String get reviewWhiteMistakes => 'Rever os erros das brancas'; @override - String get reviewBlackMistakes => 'Rever erros das Pretas'; + String get reviewBlackMistakes => 'Rever os erros das pretas'; @override String get advantage => 'Vantagem'; @@ -8089,95 +8107,95 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get opening => 'Abertura'; @override - String get middlegame => 'Meio-jogo'; + String get middlegame => 'Meio jogo'; @override - String get endgame => 'Finais'; + String get endgame => 'Final de jogo'; @override - String get conditionalPremoves => 'Pré-lances condicionais'; + String get conditionalPremoves => 'Movimentos antecipados condicionais'; @override - String get addCurrentVariation => 'Adicionar a variação atual'; + String get addCurrentVariation => 'Adicionar a variante atual'; @override - String get playVariationToCreateConditionalPremoves => 'Jogar uma variação para criar pré-lances condicionais'; + String get playVariationToCreateConditionalPremoves => 'Joga uma variante para criares movimentos antecipados condicionais'; @override - String get noConditionalPremoves => 'Sem pré-lances condicionais'; + String get noConditionalPremoves => 'Sem movimentos antecipados condicionais'; @override String playX(String param) { - return 'Jogar $param'; + return 'Joga $param'; } @override - String get showUnreadLichessMessage => 'Você recebeu uma mensagem privada do Lichess.'; + String get showUnreadLichessMessage => 'Recebestes uma mensagem privada do Lichess.'; @override - String get clickHereToReadIt => 'Clique aqui para ler'; + String get clickHereToReadIt => 'Clica aqui para ler'; @override String get sorry => 'Desculpa :('; @override - String get weHadToTimeYouOutForAWhile => 'Tivemos de bloqueá-lo por um tempo.'; + String get weHadToTimeYouOutForAWhile => 'Tivemos de te banir por algum tempo.'; @override - String get why => 'Por quê?'; + String get why => 'Porquê?'; @override - String get pleasantChessExperience => 'Buscamos oferecer uma experiência agradável de xadrez para todos.'; + String get pleasantChessExperience => 'Tencionamos proporcionar uma experiência de xadrez agradável a todos.'; @override - String get goodPractice => 'Para isso, precisamos assegurar que nossos jogadores sigam boas práticas.'; + String get goodPractice => 'Para isso, temos de nos assegurar que todos os jogadores seguem boas práticas.'; @override - String get potentialProblem => 'Quando um problema em potencial é detectado, nós mostramos esta mensagem.'; + String get potentialProblem => 'Quando um potencial problema é detetado, exibimos esta mensagem.'; @override - String get howToAvoidThis => 'Como evitar isso?'; + String get howToAvoidThis => 'Como evitar isto?'; @override - String get playEveryGame => 'Jogue todos os jogos que inicia.'; + String get playEveryGame => 'Joga todas as partidas que começares.'; @override - String get tryToWin => 'Tente vencer (ou pelo menos empatar) todos os jogos que jogar.'; + String get tryToWin => 'Tenta ganhar (ou pelo menos empatar) todas as partida que jogares.'; @override - String get resignLostGames => 'Conceda partidas perdidas (não deixe o relógio ir até ao fim).'; + String get resignLostGames => 'Desiste em partidas perdidas (não deixes o tempo no relógio acabar).'; @override - String get temporaryInconvenience => 'Pedimos desculpa pelo incômodo temporário,'; + String get temporaryInconvenience => 'Pedimos desculpa pelo incómodo temporário,'; @override - String get wishYouGreatGames => 'e desejamos-lhe grandes jogos em lichess.org.'; + String get wishYouGreatGames => 'e desejamos-te grandes jogos no lichess.org.'; @override String get thankYouForReading => 'Obrigado pela leitura!'; @override - String get lifetimeScore => 'Pontuação de todo o período'; + String get lifetimeScore => 'Pontuação desde sempre'; @override - String get currentMatchScore => 'Pontuação da partida atual'; + String get currentMatchScore => 'Pontuação atual'; @override - String get agreementAssistance => 'Eu concordo que em momento algum receberei assistência durante os meus jogos (seja de um computador, livro, banco de dados ou outra pessoa).'; + String get agreementAssistance => 'Concordo que nunca recorrerei a assistência durante as minhas partidas (de um computador de xadrez, livro, base de dados ou outra pessoa).'; @override - String get agreementNice => 'Eu concordo que serei sempre cortês com outros jogadores.'; + String get agreementNice => 'Concordo que serei sempre repeitoso para os outros jogadores.'; @override String agreementMultipleAccounts(String param) { - return 'Eu concordo que não criarei múltiplas contas (exceto pelas razões indicadas em $param).'; + return 'Concordo que não criarei várias contas (exceto pelas razões indicadas em $param).'; } @override - String get agreementPolicy => 'Eu concordo que seguirei todas as normas do Lichess.'; + String get agreementPolicy => 'Concordo que seguirei todas as políticas do Lichess.'; @override - String get searchOrStartNewDiscussion => 'Procurar ou iniciar nova conversa'; + String get searchOrStartNewDiscussion => 'Pesquisa ou começa uma nova conversa'; @override String get edit => 'Editar'; @@ -8186,31 +8204,31 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get bullet => 'Bullet'; @override - String get blitz => 'Blitz'; + String get blitz => 'Rápidas'; @override - String get rapid => 'Rápida'; + String get rapid => 'Semi-rápidas'; @override - String get classical => 'Clássico'; + String get classical => 'Clássicas'; @override - String get ultraBulletDesc => 'Jogos insanamente rápidos: menos de 30 segundos'; + String get ultraBulletDesc => 'Partidas incrivelmente rápidas: menos de 30 segundos'; @override - String get bulletDesc => 'Jogos muito rápidos: menos de 3 minutos'; + String get bulletDesc => 'Partidas muito rápidas: menos de 3 minutos'; @override - String get blitzDesc => 'Jogos rápidos: 3 a 8 minutos'; + String get blitzDesc => 'Partidas rápidas: 3 a 8 minutos'; @override - String get rapidDesc => 'Jogos rápidos: 8 a 25 minutos'; + String get rapidDesc => 'Partidas semi-rápidas: 8 a 25 minutos'; @override - String get classicalDesc => 'Jogos clássicos: 25 minutos ou mais'; + String get classicalDesc => 'Partidas clássicas: 25 minutos ou mais'; @override - String get correspondenceDesc => 'Jogos por correspondência: um ou vários dias por lance'; + String get correspondenceDesc => 'Partidas por correspondência: um ou vários dias por lance'; @override String get puzzleDesc => 'Treinador de táticas de xadrez'; @@ -8220,91 +8238,91 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { @override String yourQuestionMayHaveBeenAnswered(String param1) { - return 'A sua pergunta pode já ter sido respondida $param1'; + return 'A tua pergunta pode já ter uma resposta $param1'; } @override - String get inTheFAQ => 'no F.A.Q.'; + String get inTheFAQ => 'no F.A.Q. (perguntas frequentes).'; @override String toReportSomeoneForCheatingOrBadBehavior(String param1) { - return 'Para denunciar um usuário por trapaças ou mau comportamento, $param1'; + return 'Para reportares um utilizador por fazer batota ou por mau comportamento, $param1.'; } @override - String get useTheReportForm => 'use o formulário de denúncia'; + String get useTheReportForm => 'usa a ficha própria para o fazeres'; @override String toRequestSupport(String param1) { - return 'Para solicitar ajuda, $param1'; + return 'Para solicitares suporte, $param1.'; } @override - String get tryTheContactPage => 'tente a página de contato'; + String get tryTheContactPage => 'tenta a página de contacto'; @override String makeSureToRead(String param1) { - return 'Certifique-se de ler $param1'; + return 'Certifique-se que lê $param1'; } @override - String get theForumEtiquette => 'as regras do fórum'; + String get theForumEtiquette => 'a etiqueta do fórum'; @override - String get thisTopicIsArchived => 'Este tópico foi arquivado e não pode mais ser respondido.'; + String get thisTopicIsArchived => 'Este tópico foi arquivado e já não pode ser respondido.'; @override String joinTheTeamXToPost(String param1) { - return 'Junte-se a $param1 para publicar neste fórum'; + return 'Junta-te a $param1, para publicares neste fórum'; } @override String teamNamedX(String param1) { - return 'Equipe $param1'; + return 'equipa $param1'; } @override - String get youCannotPostYetPlaySomeGames => 'Você não pode publicar nos fóruns ainda. Jogue algumas partidas!'; + String get youCannotPostYetPlaySomeGames => 'Ainda não podes publicar nos fóruns. Joga alguns jogos!'; @override - String get subscribe => 'Seguir publicações'; + String get subscribe => 'Subscrever-se'; @override - String get unsubscribe => 'Deixar de seguir publicações'; + String get unsubscribe => 'Cancelar a subscrição'; @override String mentionedYouInX(String param1) { - return 'mencionou você em \"$param1\".'; + return 'foste mencionado em \"$param1\".'; } @override String xMentionedYouInY(String param1, String param2) { - return '$param1 mencionou você em \"$param2\".'; + return '$param1 mencionou-te em \"$param2\".'; } @override String invitedYouToX(String param1) { - return 'convidou você para \"$param1\".'; + return 'convidou-te para \"$param1\".'; } @override String xInvitedYouToY(String param1, String param2) { - return '$param1 convidou você para \"$param2\".'; + return '$param1 convidou-te para \"$param2\".'; } @override - String get youAreNowPartOfTeam => 'Você agora faz parte da equipe.'; + String get youAreNowPartOfTeam => 'Já fazes parte da equipa.'; @override String youHaveJoinedTeamX(String param1) { - return 'Você ingressou em \"$param1\".'; + return 'Juntaste-te a \"$param1\".'; } @override - String get someoneYouReportedWasBanned => 'Alguém que você denunciou foi banido'; + String get someoneYouReportedWasBanned => 'Alguém que denunciaste foi banido'; @override - String get congratsYouWon => 'Parabéns, você venceu!'; + String get congratsYouWon => 'Parabéns! Ganhaste!'; @override String gameVsX(String param1) { @@ -8317,27 +8335,27 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { } @override - String get lostAgainstTOSViolator => 'Você perdeu rating para alguém que violou os termos de serviço do Lichess'; + String get lostAgainstTOSViolator => 'Perdes-te contra alguém que violou as regras do Lichess'; @override String refundXpointsTimeControlY(String param1, String param2) { - return 'Reembolso: $param1 $param2 pontos de rating.'; + return 'Devolução: $param1 $param2 pontos de Elo.'; } @override - String get timeAlmostUp => 'O tempo está quase acabando!'; + String get timeAlmostUp => 'O tempo está quase a terminar!'; @override String get clickToRevealEmailAddress => '[Clique para revelar o endereço de e-mail]'; @override - String get download => 'Baixar'; + String get download => 'Transferir'; @override - String get coachManager => 'Configurações para professores'; + String get coachManager => 'Gestor de treinadores'; @override - String get streamerManager => 'Configurações para streamers'; + String get streamerManager => 'Gestor do streamer'; @override String get cancelTournament => 'Cancelar o torneio'; @@ -8346,66 +8364,66 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get tournDescription => 'Descrição do torneio'; @override - String get tournDescriptionHelp => 'Algo especial que você queira dizer aos participantes? Tente ser breve. Links em Markdown disponíveis: [name](https://url)'; + String get tournDescriptionHelp => 'Quer dizer alguma coisa em especial aos participantes? Seja breve. Estão disponíveis ligações de Markdown: [name](https://url)'; @override - String get ratedFormHelp => 'Os jogos valem classificação\ne afetam o rating dos jogadores'; + String get ratedFormHelp => 'Os jogos são classificados\ne afetam as avaliações dos jogadores'; @override - String get onlyMembersOfTeam => 'Apenas membros da equipe'; + String get onlyMembersOfTeam => 'Apenas membros da equipa'; @override - String get noRestriction => 'Sem restrição'; + String get noRestriction => 'Sem restrições'; @override - String get minimumRatedGames => 'Mínimo de partidas ranqueadas'; + String get minimumRatedGames => 'Jogos com classificação mínima'; @override - String get minimumRating => 'Rating mínimo'; + String get minimumRating => 'Classificação mínima'; @override - String get maximumWeeklyRating => 'Rating máxima da semana'; + String get maximumWeeklyRating => 'Avaliação semanal máxima'; @override String positionInputHelp(String param) { - return 'Cole um FEN válido para iniciar as partidas a partir de uma posição específica.\nSó funciona com jogos padrão, e não com variantes.\nUse o $param para gerar uma posição FEN, e depois cole-a aqui.\nDeixe em branco para começar as partidas na posição inicial padrão.'; + return 'Cole um FEN válido para iniciar todos os jogos a partir de uma determinada posição.\nSó funciona para os jogos padrão, não com variantes.\nVocê pode usar o $param para gerar uma posição FEN e, em seguida, colá-lo aqui.\nDeixe em branco para iniciar jogos da posição inicial normal.'; } @override String get cancelSimul => 'Cancelar a simultânea'; @override - String get simulHostcolor => 'Cor do simultanista em cada jogo'; + String get simulHostcolor => 'Cor do anfitrião para cada jogo'; @override - String get estimatedStart => 'Tempo de início estimado'; + String get estimatedStart => 'Hora de início prevista'; @override String simulFeatured(String param) { - return 'Compartilhar em $param'; + return 'Em destaque em $param'; } @override String simulFeaturedHelp(String param) { - return 'Compartilhar a simultânia com todos em $param. Desative para jogos privados.'; + return 'Mostre a sua simultânea a todos em $param. Desativar para simultâneas privadas.'; } @override String get simulDescription => 'Descrição da simultânea'; @override - String get simulDescriptionHelp => 'Você gostaria de dizer algo aos participantes?'; + String get simulDescriptionHelp => 'Quer dizer alguma coisa aos participantes?'; @override String markdownAvailable(String param) { - return '$param está disponível para opções de formatação adicionais.'; + return '$param está disponível para sintaxe mais avançada.'; } @override - String get embedsAvailable => 'Cole a URL de uma partida ou de um capítulo de estudo para incorporá-lo.'; + String get embedsAvailable => 'Cole o URL de um jogo ou um URL de um capítulo de estudo para integrá-lo.'; @override - String get inYourLocalTimezone => 'No seu próprio fuso horário'; + String get inYourLocalTimezone => 'No seu próprio fuso horário local'; @override String get tournChat => 'Chat do torneio'; @@ -8414,72 +8432,72 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get noChat => 'Sem chat'; @override - String get onlyTeamLeaders => 'Apenas líderes de equipe'; + String get onlyTeamLeaders => 'Apenas líderes da equipa'; @override - String get onlyTeamMembers => 'Apenas membros da equipe'; + String get onlyTeamMembers => 'Apenas membros da equipa'; @override - String get navigateMoveTree => 'Navegar pela notação de movimentos'; + String get navigateMoveTree => 'Navegar pela árvore de movimentos'; @override - String get mouseTricks => 'Funcionalidades do mouse'; + String get mouseTricks => 'Movimentos do rato'; @override - String get toggleLocalAnalysis => 'Ativar/desativar análise local do computador'; + String get toggleLocalAnalysis => 'Ativar/desativar análise local no computador'; @override - String get toggleAllAnalysis => 'Ativar/desativar todas análises locais do computador'; + String get toggleAllAnalysis => 'Alternar todas as análises no computador'; @override - String get playComputerMove => 'Jogar o melhor lance de computador'; + String get playComputerMove => 'Jogar o melhor lance do computador'; @override String get analysisOptions => 'Opções de análise'; @override - String get focusChat => 'Focar texto'; + String get focusChat => 'Focar no bate-papo'; @override String get showHelpDialog => 'Mostrar esta mensagem de ajuda'; @override - String get reopenYourAccount => 'Reabra sua conta'; + String get reopenYourAccount => 'Reabrir a sua conta'; @override - String get closedAccountChangedMind => 'Caso você tenha encerrado sua conta, mas mudou de opinião, você tem ainda uma chance de recuperá-la.'; + String get closedAccountChangedMind => 'Se fechou a sua conta mas desde então mudou de ideias, terá uma oportunidade de recuperar a sua conta.'; @override - String get onlyWorksOnce => 'Isso só vai funcionar uma vez.'; + String get onlyWorksOnce => 'Isto só vai funcionar uma única vez.'; @override - String get cantDoThisTwice => 'Caso você encerre sua conta pela segunda vez, será impossível recuperá-la.'; + String get cantDoThisTwice => 'Se fechar a conta uma segunda vez, não haverá forma de a recuperar.'; @override - String get emailAssociatedToaccount => 'Endereço de e-mail associado à conta'; + String get emailAssociatedToaccount => 'Endereço de email associado à conta'; @override - String get sentEmailWithLink => 'Enviamos um e-mail pra você com um link.'; + String get sentEmailWithLink => 'Enviámos-lhe um e-mail com um link.'; @override String get tournamentEntryCode => 'Código de entrada do torneio'; @override - String get hangOn => 'Espere!'; + String get hangOn => 'Aguarde!'; @override String gameInProgress(String param) { - return 'Você tem uma partida em andamento com $param.'; + return 'Tem um jogo em curso com $param.'; } @override - String get abortTheGame => 'Cancelar a partida'; + String get abortTheGame => 'Cancelar o jogo'; @override - String get resignTheGame => 'Abandonar a partida'; + String get resignTheGame => 'Abandonar o jogo'; @override - String get youCantStartNewGame => 'Você não pode iniciar um novo jogo até que este acabe.'; + String get youCantStartNewGame => 'Não pode iniciar um novo jogo antes de este estar terminado.'; @override String get since => 'Desde'; @@ -8488,25 +8506,25 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get until => 'Até'; @override - String get lichessDbExplanation => 'Amostra de partidas rankeadas de todos os jogadores do Lichess'; + String get lichessDbExplanation => 'Jogos avaliados por amostragem de todos os jogadores Lichess'; @override String get switchSides => 'Trocar de lado'; @override - String get closingAccountWithdrawAppeal => 'Encerrar sua conta anulará seu apelo'; + String get closingAccountWithdrawAppeal => 'Encerrar a tua conta anula o teu apelo'; @override - String get ourEventTips => 'Nossas dicas para organização de eventos'; + String get ourEventTips => 'Os nossos conselhos para organizar eventos'; @override String get instructions => 'Instruções'; @override - String get showMeEverything => 'Mostrar tudo'; + String get showMeEverything => 'Mostra-me tudo'; @override - String get lichessPatronInfo => 'Lichess é um software de código aberto, totalmente grátis e sem fins lucrativos. Todos os custos operacionais, de desenvolvimento, e os conteúdos são financiados unicamente através de doações de usuários.'; + String get lichessPatronInfo => 'Lichess é uma instituição de caridade e software de código aberto totalmente livre.\nTodos os custos operacionais, de desenvolvimento e conteúdo são financiados exclusivamente por doações de usuários.'; @override String get nothingToSeeHere => 'Nada para ver aqui no momento.'; @@ -8516,8 +8534,8 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'O seu adversário deixou a partida. Você pode reivindicar vitória em $count segundos.', - one: 'O seu adversário deixou a partida. Você pode reivindicar vitória em $count segundo.', + other: 'O teu adversário deixou a partida. Podes reivindicar vitória em $count segundos.', + one: 'O teu adversário deixou a partida. Podes reivindicar vitória em $count segundo.', ); return '$_temp0'; } @@ -8527,8 +8545,8 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Mate em $count lances', - one: 'Mate em $count lance', + other: 'Xeque-mate em $count meio-movimentos', + one: 'Xeque-mate em $count meio-movimento', ); return '$_temp0'; } @@ -8538,8 +8556,8 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count capivaradas', - one: '$count capivarada', + other: '$count erros graves', + one: '$count erro grave', ); return '$_temp0'; } @@ -8571,8 +8589,8 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count jogadores conectados', - one: '$count jogadores conectados', + other: '$count jogadores', + one: '$count jogador', ); return '$_temp0'; } @@ -8582,8 +8600,8 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count partidas', - one: '$count partida', + other: '$count jogos', + one: '$count jogo', ); return '$_temp0'; } @@ -8593,8 +8611,8 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Rating $count após $param2 partidas', - one: 'Rating $count em $param2 jogo', + other: '$param2 partidas $count avaliadas', + one: '$param2 partida $count avaliada', ); return '$_temp0'; } @@ -8604,8 +8622,8 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count Favoritos', - one: '$count Favoritos', + other: '$count favoritos', + one: '$count favorito', ); return '$_temp0'; } @@ -8616,7 +8634,7 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { count, locale: localeName, other: '$count dias', - one: '$count dias', + one: '$count dia', ); return '$_temp0'; } @@ -8627,7 +8645,7 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { count, locale: localeName, other: '$count horas', - one: '$count horas', + one: '$count hora', ); return '$_temp0'; } @@ -8648,8 +8666,8 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'O ranking é atualizado a cada $count minutos', - one: 'O ranking é atualizado a cada $count minutos', + other: 'As posições são atualizadas a cada $count minutos', + one: 'As posições são atualizadas a cada minuto', ); return '$_temp0'; } @@ -8660,7 +8678,7 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { count, locale: localeName, other: '$count problemas', - one: '$count quebra-cabeça', + one: '$count problema', ); return '$_temp0'; } @@ -8670,8 +8688,8 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count partidas contra você', - one: '$count partidas contra você', + other: '$count jogos contigo', + one: '$count jogo contigo', ); return '$_temp0'; } @@ -8681,8 +8699,8 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count valendo pontos', - one: '$count valendo pontos', + other: '$count partidas a valer pontos', + one: '$count partida a valer pontos', ); return '$_temp0'; } @@ -8715,7 +8733,7 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { count, locale: localeName, other: '$count empates', - one: '$count empates', + one: '$count empate', ); return '$_temp0'; } @@ -8725,8 +8743,8 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count jogando', - one: '$count jogando', + other: '$count a jogar', + one: '$count a jogar', ); return '$_temp0'; } @@ -8737,7 +8755,7 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { count, locale: localeName, other: 'Dar $count segundos', - one: 'Dar $count segundos', + one: 'Dar $count segundo', ); return '$_temp0'; } @@ -8780,8 +8798,8 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '≥ $count jogos valendo pontos', - one: '≥ $count jogos valendo pontos', + other: '≥ $count partidas a valer pontos', + one: '≥ $count partida a valer pontos', ); return '$_temp0'; } @@ -8791,8 +8809,8 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '≥ $count $param2 partidas valendo pontos', - one: '≥ $count partida $param2 valendo pontos', + other: '≥ $count partidas de $param2 a valer pontos', + one: '≥ $count partida de $param2 a valer pontos', ); return '$_temp0'; } @@ -8802,8 +8820,8 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Você precisa jogar mais $count partidas de $param2 valendo pontos', - one: 'Você precisa jogar mais $count partida de $param2 valendo pontos', + other: 'Precisa de jogar mais $count jogos de $param2 a valer pontos', + one: 'Precisas de jogar mais $count jogo de $param2 a valer pontos', ); return '$_temp0'; } @@ -8813,8 +8831,8 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Você precisa jogar ainda $count partidas valendo pontos', - one: 'Você precisa jogar ainda $count partidas valendo pontos', + other: 'Tens de jogar mais $count jogos a valer pontos', + one: 'Tens de jogar mais $count jogo a valer pontos', ); return '$_temp0'; } @@ -8824,8 +8842,8 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count de partidas importadas', - one: '$count de partidas importadas', + other: '$count partidas importadas', + one: '$count partida importada', ); return '$_temp0'; } @@ -8847,7 +8865,7 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { count, locale: localeName, other: '$count seguidores', - one: '$count seguidores', + one: '$count seguidor', ); return '$_temp0'; } @@ -8857,8 +8875,8 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count seguidos', - one: '$count seguidos', + other: 'a seguir $count jogadores', + one: 'a seguir $count jogador', ); return '$_temp0'; } @@ -8868,8 +8886,8 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Menos que $count minutos', - one: 'Menos que $count minutos', + other: 'Menos de $count minutos', + one: 'Menos de $count minuto', ); return '$_temp0'; } @@ -8879,8 +8897,8 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count partidas em andamento', - one: '$count partidas em andamento', + other: '$count jogos a decorrer', + one: '$count jogo a decorrer', ); return '$_temp0'; } @@ -8891,7 +8909,7 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { count, locale: localeName, other: 'Máximo: $count caracteres.', - one: 'Máximo: $count caractere.', + one: 'Máximo: $count carácter.', ); return '$_temp0'; } @@ -8923,8 +8941,8 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count $param2 jogadores nesta semana.', - one: '$count $param2 jogador nesta semana.', + other: '$count jogadores ativos esta semana em $param2.', + one: '$count jogador ativo esta semana em $param2.', ); return '$_temp0'; } @@ -8934,8 +8952,8 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Disponível em $count idiomas!', - one: 'Disponível em $count idiomas!', + other: 'Disponível em $count línguas!', + one: 'Disponível em $count língua!', ); return '$_temp0'; } @@ -8945,8 +8963,8 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count segundos para fazer o primeiro lance', - one: '$count segundo para fazer o primeiro lance', + other: '$count segundos para jogar o primeiro movimento', + one: '$count segundo para jogar o primeiro movimento', ); return '$_temp0'; } @@ -8967,23 +8985,23 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'e salvar as linhas de pré-lance de $count', - one: 'e salvar a linha de pré-lance de $count', + other: 'e guarda $count variantes de movimentos antecipados', + one: 'e guarda $count variante de movimentos antecipados', ); return '$_temp0'; } @override - String get stormMoveToStart => 'Mova para começar'; + String get stormMoveToStart => 'Faz um lance para começar'; @override - String get stormYouPlayTheWhitePiecesInAllPuzzles => 'Você joga com as peças brancas em todos os quebra-cabeças'; + String get stormYouPlayTheWhitePiecesInAllPuzzles => 'Jogas com as peças brancas em todos os problemas'; @override - String get stormYouPlayTheBlackPiecesInAllPuzzles => 'Você joga com as peças pretas em todos os quebra-cabeças'; + String get stormYouPlayTheBlackPiecesInAllPuzzles => 'Jogas com as peças pretas em todos os problemas'; @override - String get stormPuzzlesSolved => 'quebra-cabeças resolvidos'; + String get stormPuzzlesSolved => 'problemas resolvidos'; @override String get stormNewDailyHighscore => 'Novo recorde diário!'; @@ -8995,11 +9013,11 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get stormNewMonthlyHighscore => 'Novo recorde mensal!'; @override - String get stormNewAllTimeHighscore => 'Novo recorde de todos os tempos!'; + String get stormNewAllTimeHighscore => 'Novo recorde!'; @override String stormPreviousHighscoreWasX(String param) { - return 'Recorde anterior era $param'; + return 'O recorde anterior era $param'; } @override @@ -9014,7 +9032,7 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get stormScore => 'Pontuação'; @override - String get stormMoves => 'Lances'; + String get stormMoves => 'Total de lances'; @override String get stormAccuracy => 'Precisão'; @@ -9026,119 +9044,119 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get stormTime => 'Tempo'; @override - String get stormTimePerMove => 'Tempo por lance'; + String get stormTimePerMove => 'Tempo por jogada'; @override - String get stormHighestSolved => 'Classificação mais alta'; + String get stormHighestSolved => 'Problema mais difícil resolvido'; @override - String get stormPuzzlesPlayed => 'Quebra-cabeças jogados'; + String get stormPuzzlesPlayed => 'Problemas jogados'; @override - String get stormNewRun => 'Nova série (tecla de atalho: espaço)'; + String get stormNewRun => 'Nova partida (tecla: espaço)'; @override - String get stormEndRun => 'Finalizar série (tecla de atalho: Enter)'; + String get stormEndRun => 'Terminar partida (tecla: Enter)'; @override - String get stormHighscores => 'Melhores pontuações'; + String get stormHighscores => 'Recorde'; @override - String get stormViewBestRuns => 'Ver melhores séries'; + String get stormViewBestRuns => 'Ver as melhores partidas'; @override - String get stormBestRunOfDay => 'Melhor série do dia'; + String get stormBestRunOfDay => 'Melhor partida do dia'; @override - String get stormRuns => 'Séries'; + String get stormRuns => 'Partidas'; @override - String get stormGetReady => 'Prepare-se!'; + String get stormGetReady => 'Preparar!'; @override - String get stormWaitingForMorePlayers => 'Esperando mais jogadores entrarem...'; + String get stormWaitingForMorePlayers => 'À espera de mais jogadores...'; @override - String get stormRaceComplete => 'Corrida concluída!'; + String get stormRaceComplete => 'Race concluída!'; @override - String get stormSpectating => 'Espectando'; + String get stormSpectating => 'A assistir'; @override - String get stormJoinTheRace => 'Entre na corrida!'; + String get stormJoinTheRace => 'Junta-te à corrida!'; @override String get stormStartTheRace => 'Começar a corrida'; @override String stormYourRankX(String param) { - return 'Sua classificação: $param'; + return 'A tua pontuação: $param'; } @override - String get stormWaitForRematch => 'Esperando por revanche'; + String get stormWaitForRematch => 'Espera pela desforra'; @override String get stormNextRace => 'Próxima corrida'; @override - String get stormJoinRematch => 'Junte-se a revanche'; + String get stormJoinRematch => 'Juntar-se à desforra'; @override - String get stormWaitingToStart => 'Esperando para começar'; + String get stormWaitingToStart => 'À espera de começar'; @override String get stormCreateNewGame => 'Criar um novo jogo'; @override - String get stormJoinPublicRace => 'Junte-se a uma corrida pública'; + String get stormJoinPublicRace => 'Junta-te a uma corrida pública'; @override - String get stormRaceYourFriends => 'Corra contra seus amigos'; + String get stormRaceYourFriends => 'Corre contra os teus amigos'; @override - String get stormSkip => 'pular'; + String get stormSkip => 'ignorar'; @override - String get stormSkipHelp => 'Você pode pular um movimento por corrida:'; + String get stormSkipHelp => 'Pode pular um movimento por corrida:'; @override - String get stormSkipExplanation => 'Pule este lance para preservar o seu combo! Funciona apenas uma vez por corrida.'; + String get stormSkipExplanation => 'Passa à frente esta jogada para preservares o teu combo! Só podes fazê-lo apenas uma vez por Race.'; @override - String get stormFailedPuzzles => 'Quebra-cabeças falhados'; + String get stormFailedPuzzles => 'Desafios falhados'; @override - String get stormSlowPuzzles => 'Quebra-cabeças lentos'; + String get stormSlowPuzzles => 'Desafios lentos'; @override - String get stormSkippedPuzzle => 'Quebra-cabeça pulado'; + String get stormSkippedPuzzle => 'Desafios saltados'; @override - String get stormThisWeek => 'Essa semana'; + String get stormThisWeek => 'Esta semana'; @override - String get stormThisMonth => 'Esse mês'; + String get stormThisMonth => 'Este mês'; @override - String get stormAllTime => 'Desde o início'; + String get stormAllTime => 'Desde sempre'; @override String get stormClickToReload => 'Clique para recarregar'; @override - String get stormThisRunHasExpired => 'Esta corrida acabou!'; + String get stormThisRunHasExpired => 'Esta sessão expirou!'; @override - String get stormThisRunWasOpenedInAnotherTab => 'Esta corrida foi aberta em outra aba!'; + String get stormThisRunWasOpenedInAnotherTab => 'Esta sessão foi aberta noutra aba!'; @override String stormXRuns(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count séries', - one: '1 tentativa', + other: '$count tentativas', + one: '1 partida', ); return '$_temp0'; } @@ -9148,17 +9166,17 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Jogou $count tentativas de $param2', - one: 'Jogou uma tentativa de $param2', + other: 'Jogou $count partidas de $param2', + one: 'Jogou uma partida de $param2', ); return '$_temp0'; } @override - String get streamerLichessStreamers => 'Streamers do Lichess'; + String get streamerLichessStreamers => 'Streamers no Lichess'; @override - String get studyShareAndExport => 'Compartilhar & exportar'; + String get studyShareAndExport => 'Partilhar & exportar'; @override String get studyStart => 'Iniciar'; diff --git a/lib/l10n/l10n_ro.dart b/lib/l10n/l10n_ro.dart index c4a507ccc8..c400c97529 100644 --- a/lib/l10n/l10n_ro.dart +++ b/lib/l10n/l10n_ro.dart @@ -6,6 +6,24 @@ import 'l10n.dart'; class AppLocalizationsRo extends AppLocalizations { AppLocalizationsRo([String locale = 'ro']) : super(locale); + @override + String get mobileHomeTab => 'Home'; + + @override + String get mobilePuzzlesTab => 'Puzzles'; + + @override + String get mobileToolsTab => 'Tools'; + + @override + String get mobileWatchTab => 'Watch'; + + @override + String get mobileSettingsTab => 'Settings'; + + @override + String get mobileDeleteLocalDatabase => 'Delete local database'; + @override String get activityActivity => 'Activitate'; diff --git a/lib/l10n/l10n_ru.dart b/lib/l10n/l10n_ru.dart index db6a389526..2c7fc0e9f7 100644 --- a/lib/l10n/l10n_ru.dart +++ b/lib/l10n/l10n_ru.dart @@ -6,6 +6,24 @@ import 'l10n.dart'; class AppLocalizationsRu extends AppLocalizations { AppLocalizationsRu([String locale = 'ru']) : super(locale); + @override + String get mobileHomeTab => 'Home'; + + @override + String get mobilePuzzlesTab => 'Puzzles'; + + @override + String get mobileToolsTab => 'Tools'; + + @override + String get mobileWatchTab => 'Watch'; + + @override + String get mobileSettingsTab => 'Settings'; + + @override + String get mobileDeleteLocalDatabase => 'Delete local database'; + @override String get activityActivity => 'Активность'; diff --git a/lib/l10n/l10n_sk.dart b/lib/l10n/l10n_sk.dart index cbe7260a5f..d1837009e7 100644 --- a/lib/l10n/l10n_sk.dart +++ b/lib/l10n/l10n_sk.dart @@ -6,6 +6,24 @@ import 'l10n.dart'; class AppLocalizationsSk extends AppLocalizations { AppLocalizationsSk([String locale = 'sk']) : super(locale); + @override + String get mobileHomeTab => 'Home'; + + @override + String get mobilePuzzlesTab => 'Puzzles'; + + @override + String get mobileToolsTab => 'Tools'; + + @override + String get mobileWatchTab => 'Watch'; + + @override + String get mobileSettingsTab => 'Settings'; + + @override + String get mobileDeleteLocalDatabase => 'Delete local database'; + @override String get activityActivity => 'Aktivita'; diff --git a/lib/l10n/l10n_sl.dart b/lib/l10n/l10n_sl.dart index 1b9d01ed22..1609cd87ca 100644 --- a/lib/l10n/l10n_sl.dart +++ b/lib/l10n/l10n_sl.dart @@ -6,6 +6,24 @@ import 'l10n.dart'; class AppLocalizationsSl extends AppLocalizations { AppLocalizationsSl([String locale = 'sl']) : super(locale); + @override + String get mobileHomeTab => 'Home'; + + @override + String get mobilePuzzlesTab => 'Puzzles'; + + @override + String get mobileToolsTab => 'Tools'; + + @override + String get mobileWatchTab => 'Watch'; + + @override + String get mobileSettingsTab => 'Settings'; + + @override + String get mobileDeleteLocalDatabase => 'Delete local database'; + @override String get activityActivity => 'Aktivnost'; diff --git a/lib/l10n/l10n_sq.dart b/lib/l10n/l10n_sq.dart index bff2acf258..42740a9ba1 100644 --- a/lib/l10n/l10n_sq.dart +++ b/lib/l10n/l10n_sq.dart @@ -6,6 +6,24 @@ import 'l10n.dart'; class AppLocalizationsSq extends AppLocalizations { AppLocalizationsSq([String locale = 'sq']) : super(locale); + @override + String get mobileHomeTab => 'Home'; + + @override + String get mobilePuzzlesTab => 'Puzzles'; + + @override + String get mobileToolsTab => 'Tools'; + + @override + String get mobileWatchTab => 'Watch'; + + @override + String get mobileSettingsTab => 'Settings'; + + @override + String get mobileDeleteLocalDatabase => 'Delete local database'; + @override String get activityActivity => 'Aktiviteti'; diff --git a/lib/l10n/l10n_sr.dart b/lib/l10n/l10n_sr.dart index 8c286ddb4a..7b8a05d441 100644 --- a/lib/l10n/l10n_sr.dart +++ b/lib/l10n/l10n_sr.dart @@ -6,6 +6,24 @@ import 'l10n.dart'; class AppLocalizationsSr extends AppLocalizations { AppLocalizationsSr([String locale = 'sr']) : super(locale); + @override + String get mobileHomeTab => 'Home'; + + @override + String get mobilePuzzlesTab => 'Puzzles'; + + @override + String get mobileToolsTab => 'Tools'; + + @override + String get mobileWatchTab => 'Watch'; + + @override + String get mobileSettingsTab => 'Settings'; + + @override + String get mobileDeleteLocalDatabase => 'Delete local database'; + @override String get activityActivity => 'Активност'; diff --git a/lib/l10n/l10n_sv.dart b/lib/l10n/l10n_sv.dart index af5484acca..58aedf6fff 100644 --- a/lib/l10n/l10n_sv.dart +++ b/lib/l10n/l10n_sv.dart @@ -6,6 +6,24 @@ import 'l10n.dart'; class AppLocalizationsSv extends AppLocalizations { AppLocalizationsSv([String locale = 'sv']) : super(locale); + @override + String get mobileHomeTab => 'Home'; + + @override + String get mobilePuzzlesTab => 'Puzzles'; + + @override + String get mobileToolsTab => 'Tools'; + + @override + String get mobileWatchTab => 'Watch'; + + @override + String get mobileSettingsTab => 'Settings'; + + @override + String get mobileDeleteLocalDatabase => 'Delete local database'; + @override String get activityActivity => 'Aktivitet'; diff --git a/lib/l10n/l10n_tr.dart b/lib/l10n/l10n_tr.dart index 1027b0e8c9..6be117c7e5 100644 --- a/lib/l10n/l10n_tr.dart +++ b/lib/l10n/l10n_tr.dart @@ -6,6 +6,24 @@ import 'l10n.dart'; class AppLocalizationsTr extends AppLocalizations { AppLocalizationsTr([String locale = 'tr']) : super(locale); + @override + String get mobileHomeTab => 'Home'; + + @override + String get mobilePuzzlesTab => 'Puzzles'; + + @override + String get mobileToolsTab => 'Tools'; + + @override + String get mobileWatchTab => 'Watch'; + + @override + String get mobileSettingsTab => 'Settings'; + + @override + String get mobileDeleteLocalDatabase => 'Delete local database'; + @override String get activityActivity => 'Son Etkinlikler'; diff --git a/lib/l10n/l10n_tt.dart b/lib/l10n/l10n_tt.dart index e815e499a7..b93339cf78 100644 --- a/lib/l10n/l10n_tt.dart +++ b/lib/l10n/l10n_tt.dart @@ -6,6 +6,24 @@ import 'l10n.dart'; class AppLocalizationsTt extends AppLocalizations { AppLocalizationsTt([String locale = 'tt']) : super(locale); + @override + String get mobileHomeTab => 'Home'; + + @override + String get mobilePuzzlesTab => 'Puzzles'; + + @override + String get mobileToolsTab => 'Tools'; + + @override + String get mobileWatchTab => 'Watch'; + + @override + String get mobileSettingsTab => 'Settings'; + + @override + String get mobileDeleteLocalDatabase => 'Delete local database'; + @override String get activityActivity => 'Эшчәнлек'; diff --git a/lib/l10n/l10n_uk.dart b/lib/l10n/l10n_uk.dart index e45f1b2f4d..c425fd5a74 100644 --- a/lib/l10n/l10n_uk.dart +++ b/lib/l10n/l10n_uk.dart @@ -6,6 +6,24 @@ import 'l10n.dart'; class AppLocalizationsUk extends AppLocalizations { AppLocalizationsUk([String locale = 'uk']) : super(locale); + @override + String get mobileHomeTab => 'Home'; + + @override + String get mobilePuzzlesTab => 'Puzzles'; + + @override + String get mobileToolsTab => 'Tools'; + + @override + String get mobileWatchTab => 'Watch'; + + @override + String get mobileSettingsTab => 'Settings'; + + @override + String get mobileDeleteLocalDatabase => 'Delete local database'; + @override String get activityActivity => 'Активність'; diff --git a/lib/l10n/l10n_vi.dart b/lib/l10n/l10n_vi.dart index f42a4a65ab..355f356ae7 100644 --- a/lib/l10n/l10n_vi.dart +++ b/lib/l10n/l10n_vi.dart @@ -6,6 +6,24 @@ import 'l10n.dart'; class AppLocalizationsVi extends AppLocalizations { AppLocalizationsVi([String locale = 'vi']) : super(locale); + @override + String get mobileHomeTab => 'Home'; + + @override + String get mobilePuzzlesTab => 'Puzzles'; + + @override + String get mobileToolsTab => 'Tools'; + + @override + String get mobileWatchTab => 'Watch'; + + @override + String get mobileSettingsTab => 'Settings'; + + @override + String get mobileDeleteLocalDatabase => 'Delete local database'; + @override String get activityActivity => 'Hoạt động'; diff --git a/lib/l10n/l10n_zh.dart b/lib/l10n/l10n_zh.dart index 7292fbc8c7..fb6e3eab5e 100644 --- a/lib/l10n/l10n_zh.dart +++ b/lib/l10n/l10n_zh.dart @@ -6,6 +6,24 @@ import 'l10n.dart'; class AppLocalizationsZh extends AppLocalizations { AppLocalizationsZh([String locale = 'zh']) : super(locale); + @override + String get mobileHomeTab => 'Home'; + + @override + String get mobilePuzzlesTab => 'Puzzles'; + + @override + String get mobileToolsTab => 'Tools'; + + @override + String get mobileWatchTab => 'Watch'; + + @override + String get mobileSettingsTab => 'Settings'; + + @override + String get mobileDeleteLocalDatabase => 'Delete local database'; + @override String get activityActivity => '动态'; diff --git a/lib/src/view/settings/settings_screen.dart b/lib/src/view/settings/settings_screen.dart index 68b3f968d8..e104ac5953 100644 --- a/lib/src/view/settings/settings_screen.dart +++ b/lib/src/view/settings/settings_screen.dart @@ -390,7 +390,7 @@ class _Body extends ConsumerWidget { ), PlatformListTile( leading: const Icon(Icons.storage), - title: const Text('Delete local database'), + title: Text(context.l10n.mobileDeleteLocalDatabase), additionalInfo: dbSize.hasValue ? Text(_getSizeString(dbSize.value)) : null, trailing: Theme.of(context).platform == TargetPlatform.iOS @@ -398,7 +398,7 @@ class _Body extends ConsumerWidget { : Text(_getSizeString(dbSize.value)), onTap: () => showConfirmDialog( context, - title: const Text('Delete local database'), + title: Text(context.l10n.mobileDeleteLocalDatabase), onConfirm: (_) => _deleteDatabase(ref), isDestructiveAction: true, ), diff --git a/translation/source/mobile.xml b/translation/source/mobile.xml index 20a8b980ae..3caa8f0d99 100644 --- a/translation/source/mobile.xml +++ b/translation/source/mobile.xml @@ -5,4 +5,5 @@ Tools Watch Settings + Delete local database From f5ace8cbfb06cd677ec352f36d1a8b1dafd6cf93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Nowak?= Date: Mon, 8 Jul 2024 09:33:06 +0200 Subject: [PATCH 012/979] fix: delete translation string --- lib/l10n/app_en.arb | 1 - lib/l10n/l10n.dart | 6 ------ lib/l10n/l10n_af.dart | 3 --- lib/l10n/l10n_ar.dart | 3 --- lib/l10n/l10n_az.dart | 3 --- lib/l10n/l10n_be.dart | 3 --- lib/l10n/l10n_bg.dart | 3 --- lib/l10n/l10n_bn.dart | 3 --- lib/l10n/l10n_br.dart | 3 --- lib/l10n/l10n_bs.dart | 3 --- lib/l10n/l10n_ca.dart | 3 --- lib/l10n/l10n_cs.dart | 3 --- lib/l10n/l10n_da.dart | 3 --- lib/l10n/l10n_de.dart | 3 --- lib/l10n/l10n_el.dart | 3 --- lib/l10n/l10n_en.dart | 3 --- lib/l10n/l10n_eo.dart | 3 --- lib/l10n/l10n_es.dart | 3 --- lib/l10n/l10n_et.dart | 3 --- lib/l10n/l10n_eu.dart | 3 --- lib/l10n/l10n_fa.dart | 3 --- lib/l10n/l10n_fi.dart | 3 --- lib/l10n/l10n_fo.dart | 3 --- lib/l10n/l10n_fr.dart | 3 --- lib/l10n/l10n_ga.dart | 3 --- lib/l10n/l10n_gl.dart | 3 --- lib/l10n/l10n_he.dart | 3 --- lib/l10n/l10n_hi.dart | 3 --- lib/l10n/l10n_hr.dart | 3 --- lib/l10n/l10n_hu.dart | 3 --- lib/l10n/l10n_hy.dart | 3 --- lib/l10n/l10n_id.dart | 3 --- lib/l10n/l10n_it.dart | 3 --- lib/l10n/l10n_ja.dart | 3 --- lib/l10n/l10n_kk.dart | 3 --- lib/l10n/l10n_ko.dart | 3 --- lib/l10n/l10n_lb.dart | 3 --- lib/l10n/l10n_lt.dart | 3 --- lib/l10n/l10n_lv.dart | 3 --- lib/l10n/l10n_mk.dart | 3 --- lib/l10n/l10n_nb.dart | 3 --- lib/l10n/l10n_nl.dart | 3 --- lib/l10n/l10n_nn.dart | 3 --- lib/l10n/l10n_pl.dart | 3 --- lib/l10n/l10n_pt.dart | 3 --- lib/l10n/l10n_ro.dart | 3 --- lib/l10n/l10n_ru.dart | 3 --- lib/l10n/l10n_sk.dart | 3 --- lib/l10n/l10n_sl.dart | 3 --- lib/l10n/l10n_sq.dart | 3 --- lib/l10n/l10n_sr.dart | 3 --- lib/l10n/l10n_sv.dart | 3 --- lib/l10n/l10n_tr.dart | 3 --- lib/l10n/l10n_tt.dart | 3 --- lib/l10n/l10n_uk.dart | 3 --- lib/l10n/l10n_vi.dart | 3 --- lib/l10n/l10n_zh.dart | 3 --- lib/src/view/settings/settings_screen.dart | 4 ++-- translation/source/mobile.xml | 1 - 59 files changed, 2 insertions(+), 175 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index cff2e65a90..5fb111c60e 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -44,7 +44,6 @@ "mobileCustomGameJoinAGame": "Join a game", "mobileCorrespondenceClearSavedMove": "Clear saved move", "mobileSomethingWentWrong": "Something went wrong.", - "mobileDeleteLocalDatabase": "Delete local database", "activityActivity": "Activity", "activityHostedALiveStream": "Hosted a live stream", "activityRankedInSwissTournament": "Ranked #{param1} in {param2}", diff --git a/lib/l10n/l10n.dart b/lib/l10n/l10n.dart index c7df0fb12d..ac48322794 100644 --- a/lib/l10n/l10n.dart +++ b/lib/l10n/l10n.dart @@ -430,12 +430,6 @@ abstract class AppLocalizations { /// **'Something went wrong.'** String get mobileSomethingWentWrong; - /// No description provided for @mobileDeleteLocalDatabase. - /// - /// In en, this message translates to: - /// **'Delete local database'** - String get mobileDeleteLocalDatabase; - /// No description provided for @activityActivity. /// /// In en, this message translates to: diff --git a/lib/l10n/l10n_af.dart b/lib/l10n/l10n_af.dart index 791e415f90..7f04ec8028 100644 --- a/lib/l10n/l10n_af.dart +++ b/lib/l10n/l10n_af.dart @@ -122,9 +122,6 @@ class AppLocalizationsAf extends AppLocalizations { @override String get mobileSomethingWentWrong => 'Something went wrong.'; - @override - String get mobileDeleteLocalDatabase => 'Delete local database'; - @override String get activityActivity => 'Aktiwiteite'; diff --git a/lib/l10n/l10n_ar.dart b/lib/l10n/l10n_ar.dart index 2f6ea7169c..b974992d51 100644 --- a/lib/l10n/l10n_ar.dart +++ b/lib/l10n/l10n_ar.dart @@ -122,9 +122,6 @@ class AppLocalizationsAr extends AppLocalizations { @override String get mobileSomethingWentWrong => 'Something went wrong.'; - @override - String get mobileDeleteLocalDatabase => 'Delete local database'; - @override String get activityActivity => 'الأنشطة'; diff --git a/lib/l10n/l10n_az.dart b/lib/l10n/l10n_az.dart index edc5871ac2..337a7ca617 100644 --- a/lib/l10n/l10n_az.dart +++ b/lib/l10n/l10n_az.dart @@ -122,9 +122,6 @@ class AppLocalizationsAz extends AppLocalizations { @override String get mobileSomethingWentWrong => 'Something went wrong.'; - @override - String get mobileDeleteLocalDatabase => 'Delete local database'; - @override String get activityActivity => 'Aktivlik'; diff --git a/lib/l10n/l10n_be.dart b/lib/l10n/l10n_be.dart index f2d7f7b72d..07a42a19ed 100644 --- a/lib/l10n/l10n_be.dart +++ b/lib/l10n/l10n_be.dart @@ -122,9 +122,6 @@ class AppLocalizationsBe extends AppLocalizations { @override String get mobileSomethingWentWrong => 'Something went wrong.'; - @override - String get mobileDeleteLocalDatabase => 'Delete local database'; - @override String get activityActivity => 'Актыўнасць'; diff --git a/lib/l10n/l10n_bg.dart b/lib/l10n/l10n_bg.dart index e5277c9f31..986bde81db 100644 --- a/lib/l10n/l10n_bg.dart +++ b/lib/l10n/l10n_bg.dart @@ -122,9 +122,6 @@ class AppLocalizationsBg extends AppLocalizations { @override String get mobileSomethingWentWrong => 'Something went wrong.'; - @override - String get mobileDeleteLocalDatabase => 'Delete local database'; - @override String get activityActivity => 'Дейност'; diff --git a/lib/l10n/l10n_bn.dart b/lib/l10n/l10n_bn.dart index 336294dd3a..ca60c567e7 100644 --- a/lib/l10n/l10n_bn.dart +++ b/lib/l10n/l10n_bn.dart @@ -122,9 +122,6 @@ class AppLocalizationsBn extends AppLocalizations { @override String get mobileSomethingWentWrong => 'Something went wrong.'; - @override - String get mobileDeleteLocalDatabase => 'Delete local database'; - @override String get activityActivity => 'কার্যকলাপ'; diff --git a/lib/l10n/l10n_br.dart b/lib/l10n/l10n_br.dart index d992b8e519..e75591fd62 100644 --- a/lib/l10n/l10n_br.dart +++ b/lib/l10n/l10n_br.dart @@ -122,9 +122,6 @@ class AppLocalizationsBr extends AppLocalizations { @override String get mobileSomethingWentWrong => 'Something went wrong.'; - @override - String get mobileDeleteLocalDatabase => 'Delete local database'; - @override String get activityActivity => 'Obererezhioù diwezhañ'; diff --git a/lib/l10n/l10n_bs.dart b/lib/l10n/l10n_bs.dart index 875a52d047..b193253dc7 100644 --- a/lib/l10n/l10n_bs.dart +++ b/lib/l10n/l10n_bs.dart @@ -122,9 +122,6 @@ class AppLocalizationsBs extends AppLocalizations { @override String get mobileSomethingWentWrong => 'Something went wrong.'; - @override - String get mobileDeleteLocalDatabase => 'Delete local database'; - @override String get activityActivity => 'Aktivnost'; diff --git a/lib/l10n/l10n_ca.dart b/lib/l10n/l10n_ca.dart index e1a37c1e48..f58ee31dbd 100644 --- a/lib/l10n/l10n_ca.dart +++ b/lib/l10n/l10n_ca.dart @@ -122,9 +122,6 @@ class AppLocalizationsCa extends AppLocalizations { @override String get mobileSomethingWentWrong => 'Something went wrong.'; - @override - String get mobileDeleteLocalDatabase => 'Delete local database'; - @override String get activityActivity => 'Activitat'; diff --git a/lib/l10n/l10n_cs.dart b/lib/l10n/l10n_cs.dart index 393886aca0..112c0cffa6 100644 --- a/lib/l10n/l10n_cs.dart +++ b/lib/l10n/l10n_cs.dart @@ -122,9 +122,6 @@ class AppLocalizationsCs extends AppLocalizations { @override String get mobileSomethingWentWrong => 'Something went wrong.'; - @override - String get mobileDeleteLocalDatabase => 'Delete local database'; - @override String get activityActivity => 'Aktivita'; diff --git a/lib/l10n/l10n_da.dart b/lib/l10n/l10n_da.dart index 3fc5ecbe57..e048509022 100644 --- a/lib/l10n/l10n_da.dart +++ b/lib/l10n/l10n_da.dart @@ -122,9 +122,6 @@ class AppLocalizationsDa extends AppLocalizations { @override String get mobileSomethingWentWrong => 'Something went wrong.'; - @override - String get mobileDeleteLocalDatabase => 'Delete local database'; - @override String get activityActivity => 'Aktivitet'; diff --git a/lib/l10n/l10n_de.dart b/lib/l10n/l10n_de.dart index f819a937ff..f696db2df8 100644 --- a/lib/l10n/l10n_de.dart +++ b/lib/l10n/l10n_de.dart @@ -122,9 +122,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get mobileSomethingWentWrong => 'Something went wrong.'; - @override - String get mobileDeleteLocalDatabase => 'Delete local database'; - @override String get activityActivity => 'Verlauf'; diff --git a/lib/l10n/l10n_el.dart b/lib/l10n/l10n_el.dart index ef6e29d48e..00d86c0cf1 100644 --- a/lib/l10n/l10n_el.dart +++ b/lib/l10n/l10n_el.dart @@ -122,9 +122,6 @@ class AppLocalizationsEl extends AppLocalizations { @override String get mobileSomethingWentWrong => 'Something went wrong.'; - @override - String get mobileDeleteLocalDatabase => 'Delete local database'; - @override String get activityActivity => 'Δραστηριότητα'; diff --git a/lib/l10n/l10n_en.dart b/lib/l10n/l10n_en.dart index e24171b812..be73104256 100644 --- a/lib/l10n/l10n_en.dart +++ b/lib/l10n/l10n_en.dart @@ -122,9 +122,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get mobileSomethingWentWrong => 'Something went wrong.'; - @override - String get mobileDeleteLocalDatabase => 'Delete local database'; - @override String get activityActivity => 'Activity'; diff --git a/lib/l10n/l10n_eo.dart b/lib/l10n/l10n_eo.dart index 225b537871..9ca1bd0348 100644 --- a/lib/l10n/l10n_eo.dart +++ b/lib/l10n/l10n_eo.dart @@ -122,9 +122,6 @@ class AppLocalizationsEo extends AppLocalizations { @override String get mobileSomethingWentWrong => 'Something went wrong.'; - @override - String get mobileDeleteLocalDatabase => 'Delete local database'; - @override String get activityActivity => 'Aktiveco'; diff --git a/lib/l10n/l10n_es.dart b/lib/l10n/l10n_es.dart index 2f5cd05e53..7921a628ba 100644 --- a/lib/l10n/l10n_es.dart +++ b/lib/l10n/l10n_es.dart @@ -122,9 +122,6 @@ class AppLocalizationsEs extends AppLocalizations { @override String get mobileSomethingWentWrong => 'Something went wrong.'; - @override - String get mobileDeleteLocalDatabase => 'Delete local database'; - @override String get activityActivity => 'Actividad'; diff --git a/lib/l10n/l10n_et.dart b/lib/l10n/l10n_et.dart index 6341183fb3..eb41a65488 100644 --- a/lib/l10n/l10n_et.dart +++ b/lib/l10n/l10n_et.dart @@ -122,9 +122,6 @@ class AppLocalizationsEt extends AppLocalizations { @override String get mobileSomethingWentWrong => 'Something went wrong.'; - @override - String get mobileDeleteLocalDatabase => 'Delete local database'; - @override String get activityActivity => 'Aktiivsus'; diff --git a/lib/l10n/l10n_eu.dart b/lib/l10n/l10n_eu.dart index a20fb52b07..aa04bd42b6 100644 --- a/lib/l10n/l10n_eu.dart +++ b/lib/l10n/l10n_eu.dart @@ -122,9 +122,6 @@ class AppLocalizationsEu extends AppLocalizations { @override String get mobileSomethingWentWrong => 'Something went wrong.'; - @override - String get mobileDeleteLocalDatabase => 'Delete local database'; - @override String get activityActivity => 'Jarduera'; diff --git a/lib/l10n/l10n_fa.dart b/lib/l10n/l10n_fa.dart index 5f24cc04b9..ed93b85453 100644 --- a/lib/l10n/l10n_fa.dart +++ b/lib/l10n/l10n_fa.dart @@ -122,9 +122,6 @@ class AppLocalizationsFa extends AppLocalizations { @override String get mobileSomethingWentWrong => 'Something went wrong.'; - @override - String get mobileDeleteLocalDatabase => 'Delete local database'; - @override String get activityActivity => 'فعالیت'; diff --git a/lib/l10n/l10n_fi.dart b/lib/l10n/l10n_fi.dart index ff9084fafc..8ccaeffb25 100644 --- a/lib/l10n/l10n_fi.dart +++ b/lib/l10n/l10n_fi.dart @@ -122,9 +122,6 @@ class AppLocalizationsFi extends AppLocalizations { @override String get mobileSomethingWentWrong => 'Something went wrong.'; - @override - String get mobileDeleteLocalDatabase => 'Delete local database'; - @override String get activityActivity => 'Toiminta'; diff --git a/lib/l10n/l10n_fo.dart b/lib/l10n/l10n_fo.dart index bef8b93a96..233b0ac0ac 100644 --- a/lib/l10n/l10n_fo.dart +++ b/lib/l10n/l10n_fo.dart @@ -122,9 +122,6 @@ class AppLocalizationsFo extends AppLocalizations { @override String get mobileSomethingWentWrong => 'Something went wrong.'; - @override - String get mobileDeleteLocalDatabase => 'Delete local database'; - @override String get activityActivity => 'Virkni'; diff --git a/lib/l10n/l10n_fr.dart b/lib/l10n/l10n_fr.dart index b4ac3f5809..2b2002a59e 100644 --- a/lib/l10n/l10n_fr.dart +++ b/lib/l10n/l10n_fr.dart @@ -122,9 +122,6 @@ class AppLocalizationsFr extends AppLocalizations { @override String get mobileSomethingWentWrong => 'Something went wrong.'; - @override - String get mobileDeleteLocalDatabase => 'Delete local database'; - @override String get activityActivity => 'Activité'; diff --git a/lib/l10n/l10n_ga.dart b/lib/l10n/l10n_ga.dart index 7459928b73..e1e9b3936b 100644 --- a/lib/l10n/l10n_ga.dart +++ b/lib/l10n/l10n_ga.dart @@ -122,9 +122,6 @@ class AppLocalizationsGa extends AppLocalizations { @override String get mobileSomethingWentWrong => 'Something went wrong.'; - @override - String get mobileDeleteLocalDatabase => 'Delete local database'; - @override String get activityActivity => 'Gníomhaíocht'; diff --git a/lib/l10n/l10n_gl.dart b/lib/l10n/l10n_gl.dart index b62b744a8b..1887c64a66 100644 --- a/lib/l10n/l10n_gl.dart +++ b/lib/l10n/l10n_gl.dart @@ -122,9 +122,6 @@ class AppLocalizationsGl extends AppLocalizations { @override String get mobileSomethingWentWrong => 'Something went wrong.'; - @override - String get mobileDeleteLocalDatabase => 'Delete local database'; - @override String get activityActivity => 'Actividade'; diff --git a/lib/l10n/l10n_he.dart b/lib/l10n/l10n_he.dart index 9066304422..f06e3a5e7b 100644 --- a/lib/l10n/l10n_he.dart +++ b/lib/l10n/l10n_he.dart @@ -122,9 +122,6 @@ class AppLocalizationsHe extends AppLocalizations { @override String get mobileSomethingWentWrong => 'Something went wrong.'; - @override - String get mobileDeleteLocalDatabase => 'Delete local database'; - @override String get activityActivity => 'פעילות'; diff --git a/lib/l10n/l10n_hi.dart b/lib/l10n/l10n_hi.dart index 9f2f816f20..0d8ba8aa68 100644 --- a/lib/l10n/l10n_hi.dart +++ b/lib/l10n/l10n_hi.dart @@ -122,9 +122,6 @@ class AppLocalizationsHi extends AppLocalizations { @override String get mobileSomethingWentWrong => 'Something went wrong.'; - @override - String get mobileDeleteLocalDatabase => 'Delete local database'; - @override String get activityActivity => 'कार्यकलाप'; diff --git a/lib/l10n/l10n_hr.dart b/lib/l10n/l10n_hr.dart index 7f0fa48ca9..63c5dc9999 100644 --- a/lib/l10n/l10n_hr.dart +++ b/lib/l10n/l10n_hr.dart @@ -122,9 +122,6 @@ class AppLocalizationsHr extends AppLocalizations { @override String get mobileSomethingWentWrong => 'Something went wrong.'; - @override - String get mobileDeleteLocalDatabase => 'Delete local database'; - @override String get activityActivity => 'Aktivnost'; diff --git a/lib/l10n/l10n_hu.dart b/lib/l10n/l10n_hu.dart index 0ff447a638..2694220eb6 100644 --- a/lib/l10n/l10n_hu.dart +++ b/lib/l10n/l10n_hu.dart @@ -122,9 +122,6 @@ class AppLocalizationsHu extends AppLocalizations { @override String get mobileSomethingWentWrong => 'Something went wrong.'; - @override - String get mobileDeleteLocalDatabase => 'Delete local database'; - @override String get activityActivity => 'Aktivitás'; diff --git a/lib/l10n/l10n_hy.dart b/lib/l10n/l10n_hy.dart index eecdfb784a..d3b541a169 100644 --- a/lib/l10n/l10n_hy.dart +++ b/lib/l10n/l10n_hy.dart @@ -122,9 +122,6 @@ class AppLocalizationsHy extends AppLocalizations { @override String get mobileSomethingWentWrong => 'Something went wrong.'; - @override - String get mobileDeleteLocalDatabase => 'Delete local database'; - @override String get activityActivity => 'Գործունեություն'; diff --git a/lib/l10n/l10n_id.dart b/lib/l10n/l10n_id.dart index ca016e69df..87d1e6e5b5 100644 --- a/lib/l10n/l10n_id.dart +++ b/lib/l10n/l10n_id.dart @@ -122,9 +122,6 @@ class AppLocalizationsId extends AppLocalizations { @override String get mobileSomethingWentWrong => 'Something went wrong.'; - @override - String get mobileDeleteLocalDatabase => 'Delete local database'; - @override String get activityActivity => 'Aktivitas'; diff --git a/lib/l10n/l10n_it.dart b/lib/l10n/l10n_it.dart index 8af99d9b83..395a608b47 100644 --- a/lib/l10n/l10n_it.dart +++ b/lib/l10n/l10n_it.dart @@ -122,9 +122,6 @@ class AppLocalizationsIt extends AppLocalizations { @override String get mobileSomethingWentWrong => 'Something went wrong.'; - @override - String get mobileDeleteLocalDatabase => 'Delete local database'; - @override String get activityActivity => 'Attività'; diff --git a/lib/l10n/l10n_ja.dart b/lib/l10n/l10n_ja.dart index 77dcfc78b2..0e5ad9177a 100644 --- a/lib/l10n/l10n_ja.dart +++ b/lib/l10n/l10n_ja.dart @@ -122,9 +122,6 @@ class AppLocalizationsJa extends AppLocalizations { @override String get mobileSomethingWentWrong => 'Something went wrong.'; - @override - String get mobileDeleteLocalDatabase => 'Delete local database'; - @override String get activityActivity => '活動'; diff --git a/lib/l10n/l10n_kk.dart b/lib/l10n/l10n_kk.dart index c9ff93a442..50980cd609 100644 --- a/lib/l10n/l10n_kk.dart +++ b/lib/l10n/l10n_kk.dart @@ -122,9 +122,6 @@ class AppLocalizationsKk extends AppLocalizations { @override String get mobileSomethingWentWrong => 'Something went wrong.'; - @override - String get mobileDeleteLocalDatabase => 'Delete local database'; - @override String get activityActivity => 'Белсенділігі'; diff --git a/lib/l10n/l10n_ko.dart b/lib/l10n/l10n_ko.dart index 024fd0fa95..05322255b4 100644 --- a/lib/l10n/l10n_ko.dart +++ b/lib/l10n/l10n_ko.dart @@ -122,9 +122,6 @@ class AppLocalizationsKo extends AppLocalizations { @override String get mobileSomethingWentWrong => 'Something went wrong.'; - @override - String get mobileDeleteLocalDatabase => 'Delete local database'; - @override String get activityActivity => '활동'; diff --git a/lib/l10n/l10n_lb.dart b/lib/l10n/l10n_lb.dart index 867fb88f08..cc91e8bc81 100644 --- a/lib/l10n/l10n_lb.dart +++ b/lib/l10n/l10n_lb.dart @@ -122,9 +122,6 @@ class AppLocalizationsLb extends AppLocalizations { @override String get mobileSomethingWentWrong => 'Something went wrong.'; - @override - String get mobileDeleteLocalDatabase => 'Delete local database'; - @override String get activityActivity => 'Verlaf'; diff --git a/lib/l10n/l10n_lt.dart b/lib/l10n/l10n_lt.dart index de5e0d4dc1..41f04ae623 100644 --- a/lib/l10n/l10n_lt.dart +++ b/lib/l10n/l10n_lt.dart @@ -122,9 +122,6 @@ class AppLocalizationsLt extends AppLocalizations { @override String get mobileSomethingWentWrong => 'Something went wrong.'; - @override - String get mobileDeleteLocalDatabase => 'Delete local database'; - @override String get activityActivity => 'Veikla'; diff --git a/lib/l10n/l10n_lv.dart b/lib/l10n/l10n_lv.dart index 59f772603f..5934d46c5e 100644 --- a/lib/l10n/l10n_lv.dart +++ b/lib/l10n/l10n_lv.dart @@ -122,9 +122,6 @@ class AppLocalizationsLv extends AppLocalizations { @override String get mobileSomethingWentWrong => 'Something went wrong.'; - @override - String get mobileDeleteLocalDatabase => 'Delete local database'; - @override String get activityActivity => 'Aktivitāte'; diff --git a/lib/l10n/l10n_mk.dart b/lib/l10n/l10n_mk.dart index 2df72b480e..bc58b55bb4 100644 --- a/lib/l10n/l10n_mk.dart +++ b/lib/l10n/l10n_mk.dart @@ -122,9 +122,6 @@ class AppLocalizationsMk extends AppLocalizations { @override String get mobileSomethingWentWrong => 'Something went wrong.'; - @override - String get mobileDeleteLocalDatabase => 'Delete local database'; - @override String get activityActivity => 'Активност'; diff --git a/lib/l10n/l10n_nb.dart b/lib/l10n/l10n_nb.dart index d967c44fc5..7aafa5746c 100644 --- a/lib/l10n/l10n_nb.dart +++ b/lib/l10n/l10n_nb.dart @@ -122,9 +122,6 @@ class AppLocalizationsNb extends AppLocalizations { @override String get mobileSomethingWentWrong => 'Something went wrong.'; - @override - String get mobileDeleteLocalDatabase => 'Delete local database'; - @override String get activityActivity => 'Aktivitet'; diff --git a/lib/l10n/l10n_nl.dart b/lib/l10n/l10n_nl.dart index 753ec28f10..4ffcc237ee 100644 --- a/lib/l10n/l10n_nl.dart +++ b/lib/l10n/l10n_nl.dart @@ -122,9 +122,6 @@ class AppLocalizationsNl extends AppLocalizations { @override String get mobileSomethingWentWrong => 'Something went wrong.'; - @override - String get mobileDeleteLocalDatabase => 'Delete local database'; - @override String get activityActivity => 'Activiteit'; diff --git a/lib/l10n/l10n_nn.dart b/lib/l10n/l10n_nn.dart index 0d9d9a2be8..e39d286569 100644 --- a/lib/l10n/l10n_nn.dart +++ b/lib/l10n/l10n_nn.dart @@ -122,9 +122,6 @@ class AppLocalizationsNn extends AppLocalizations { @override String get mobileSomethingWentWrong => 'Something went wrong.'; - @override - String get mobileDeleteLocalDatabase => 'Delete local database'; - @override String get activityActivity => 'Aktivitet'; diff --git a/lib/l10n/l10n_pl.dart b/lib/l10n/l10n_pl.dart index 7e62a4f654..d5fbf3b0d1 100644 --- a/lib/l10n/l10n_pl.dart +++ b/lib/l10n/l10n_pl.dart @@ -122,9 +122,6 @@ class AppLocalizationsPl extends AppLocalizations { @override String get mobileSomethingWentWrong => 'Something went wrong.'; - @override - String get mobileDeleteLocalDatabase => 'Delete local database'; - @override String get activityActivity => 'Aktywność'; diff --git a/lib/l10n/l10n_pt.dart b/lib/l10n/l10n_pt.dart index 80701f58aa..1baec9e403 100644 --- a/lib/l10n/l10n_pt.dart +++ b/lib/l10n/l10n_pt.dart @@ -122,9 +122,6 @@ class AppLocalizationsPt extends AppLocalizations { @override String get mobileSomethingWentWrong => 'Something went wrong.'; - @override - String get mobileDeleteLocalDatabase => 'Delete local database'; - @override String get activityActivity => 'Atividade'; diff --git a/lib/l10n/l10n_ro.dart b/lib/l10n/l10n_ro.dart index 3c8ecc8100..0b62c1aec5 100644 --- a/lib/l10n/l10n_ro.dart +++ b/lib/l10n/l10n_ro.dart @@ -122,9 +122,6 @@ class AppLocalizationsRo extends AppLocalizations { @override String get mobileSomethingWentWrong => 'Something went wrong.'; - @override - String get mobileDeleteLocalDatabase => 'Delete local database'; - @override String get activityActivity => 'Activitate'; diff --git a/lib/l10n/l10n_ru.dart b/lib/l10n/l10n_ru.dart index fb2a1dfe84..6d86950057 100644 --- a/lib/l10n/l10n_ru.dart +++ b/lib/l10n/l10n_ru.dart @@ -122,9 +122,6 @@ class AppLocalizationsRu extends AppLocalizations { @override String get mobileSomethingWentWrong => 'Something went wrong.'; - @override - String get mobileDeleteLocalDatabase => 'Delete local database'; - @override String get activityActivity => 'Активность'; diff --git a/lib/l10n/l10n_sk.dart b/lib/l10n/l10n_sk.dart index 26bb66fa3c..4df78d2454 100644 --- a/lib/l10n/l10n_sk.dart +++ b/lib/l10n/l10n_sk.dart @@ -122,9 +122,6 @@ class AppLocalizationsSk extends AppLocalizations { @override String get mobileSomethingWentWrong => 'Something went wrong.'; - @override - String get mobileDeleteLocalDatabase => 'Delete local database'; - @override String get activityActivity => 'Aktivita'; diff --git a/lib/l10n/l10n_sl.dart b/lib/l10n/l10n_sl.dart index c0c65ab8f0..9a0df945df 100644 --- a/lib/l10n/l10n_sl.dart +++ b/lib/l10n/l10n_sl.dart @@ -122,9 +122,6 @@ class AppLocalizationsSl extends AppLocalizations { @override String get mobileSomethingWentWrong => 'Something went wrong.'; - @override - String get mobileDeleteLocalDatabase => 'Delete local database'; - @override String get activityActivity => 'Aktivnost'; diff --git a/lib/l10n/l10n_sq.dart b/lib/l10n/l10n_sq.dart index 4a14d8c695..87b51ffca4 100644 --- a/lib/l10n/l10n_sq.dart +++ b/lib/l10n/l10n_sq.dart @@ -122,9 +122,6 @@ class AppLocalizationsSq extends AppLocalizations { @override String get mobileSomethingWentWrong => 'Something went wrong.'; - @override - String get mobileDeleteLocalDatabase => 'Delete local database'; - @override String get activityActivity => 'Aktiviteti'; diff --git a/lib/l10n/l10n_sr.dart b/lib/l10n/l10n_sr.dart index cef3531560..0747a942e4 100644 --- a/lib/l10n/l10n_sr.dart +++ b/lib/l10n/l10n_sr.dart @@ -122,9 +122,6 @@ class AppLocalizationsSr extends AppLocalizations { @override String get mobileSomethingWentWrong => 'Something went wrong.'; - @override - String get mobileDeleteLocalDatabase => 'Delete local database'; - @override String get activityActivity => 'Активност'; diff --git a/lib/l10n/l10n_sv.dart b/lib/l10n/l10n_sv.dart index 61a113cdc4..af9c1c6b05 100644 --- a/lib/l10n/l10n_sv.dart +++ b/lib/l10n/l10n_sv.dart @@ -122,9 +122,6 @@ class AppLocalizationsSv extends AppLocalizations { @override String get mobileSomethingWentWrong => 'Something went wrong.'; - @override - String get mobileDeleteLocalDatabase => 'Delete local database'; - @override String get activityActivity => 'Aktivitet'; diff --git a/lib/l10n/l10n_tr.dart b/lib/l10n/l10n_tr.dart index 963cedd15a..26b79019ba 100644 --- a/lib/l10n/l10n_tr.dart +++ b/lib/l10n/l10n_tr.dart @@ -122,9 +122,6 @@ class AppLocalizationsTr extends AppLocalizations { @override String get mobileSomethingWentWrong => 'Something went wrong.'; - @override - String get mobileDeleteLocalDatabase => 'Delete local database'; - @override String get activityActivity => 'Son Etkinlikler'; diff --git a/lib/l10n/l10n_tt.dart b/lib/l10n/l10n_tt.dart index 2c13f39c9a..7b5783909f 100644 --- a/lib/l10n/l10n_tt.dart +++ b/lib/l10n/l10n_tt.dart @@ -122,9 +122,6 @@ class AppLocalizationsTt extends AppLocalizations { @override String get mobileSomethingWentWrong => 'Something went wrong.'; - @override - String get mobileDeleteLocalDatabase => 'Delete local database'; - @override String get activityActivity => 'Эшчәнлек'; diff --git a/lib/l10n/l10n_uk.dart b/lib/l10n/l10n_uk.dart index b3d299a1a6..789cf508fa 100644 --- a/lib/l10n/l10n_uk.dart +++ b/lib/l10n/l10n_uk.dart @@ -122,9 +122,6 @@ class AppLocalizationsUk extends AppLocalizations { @override String get mobileSomethingWentWrong => 'Something went wrong.'; - @override - String get mobileDeleteLocalDatabase => 'Delete local database'; - @override String get activityActivity => 'Активність'; diff --git a/lib/l10n/l10n_vi.dart b/lib/l10n/l10n_vi.dart index cb00376f96..6ec73ca710 100644 --- a/lib/l10n/l10n_vi.dart +++ b/lib/l10n/l10n_vi.dart @@ -122,9 +122,6 @@ class AppLocalizationsVi extends AppLocalizations { @override String get mobileSomethingWentWrong => 'Something went wrong.'; - @override - String get mobileDeleteLocalDatabase => 'Delete local database'; - @override String get activityActivity => 'Hoạt động'; diff --git a/lib/l10n/l10n_zh.dart b/lib/l10n/l10n_zh.dart index aa5eafba1c..d95ef44fb4 100644 --- a/lib/l10n/l10n_zh.dart +++ b/lib/l10n/l10n_zh.dart @@ -122,9 +122,6 @@ class AppLocalizationsZh extends AppLocalizations { @override String get mobileSomethingWentWrong => 'Something went wrong.'; - @override - String get mobileDeleteLocalDatabase => 'Delete local database'; - @override String get activityActivity => '动态'; diff --git a/lib/src/view/settings/settings_screen.dart b/lib/src/view/settings/settings_screen.dart index a59c5beafa..6e238c8361 100644 --- a/lib/src/view/settings/settings_screen.dart +++ b/lib/src/view/settings/settings_screen.dart @@ -390,7 +390,7 @@ class _Body extends ConsumerWidget { ), PlatformListTile( leading: const Icon(Icons.storage), - title: Text(context.l10n.mobileDeleteLocalDatabase), + title: const Text('Delete local database'), additionalInfo: dbSize.hasValue ? Text(_getSizeString(dbSize.value)) : null, trailing: Theme.of(context).platform == TargetPlatform.iOS @@ -398,7 +398,7 @@ class _Body extends ConsumerWidget { : Text(_getSizeString(dbSize.value)), onTap: () => showConfirmDialog( context, - title: Text(context.l10n.mobileDeleteLocalDatabase), + title: const Text('Delete local database'), onConfirm: (_) => _deleteDatabase(ref), isDestructiveAction: true, ), diff --git a/translation/source/mobile.xml b/translation/source/mobile.xml index 8fb06521b5..8c07da5c41 100644 --- a/translation/source/mobile.xml +++ b/translation/source/mobile.xml @@ -38,5 +38,4 @@ Join a game Clear saved move Something went wrong. - Delete local database From 5624bcfc161ffa4537637af9f0900a43927a62b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Nowak?= Date: Wed, 10 Jul 2024 12:14:19 +0200 Subject: [PATCH 013/979] feat: refresh db size when opening settings screen --- lib/src/view/settings/settings_screen.dart | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/src/view/settings/settings_screen.dart b/lib/src/view/settings/settings_screen.dart index 6e238c8361..de8ea9054a 100644 --- a/lib/src/view/settings/settings_screen.dart +++ b/lib/src/view/settings/settings_screen.dart @@ -82,6 +82,12 @@ class SettingsScreen extends ConsumerWidget { class _Body extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + ref.listen(currentBottomTabProvider, (prev, current) { + if (prev != BottomTab.settings && current == BottomTab.settings) { + _refreshData(ref); + } + }); + final themeMode = ref.watch( generalPreferencesProvider.select((state) => state.themeMode), ); @@ -483,4 +489,8 @@ class _Body extends ConsumerWidget { await clearDatabase(db); ref.invalidate(getDbSizeInBytesProvider); } + + void _refreshData(WidgetRef ref) { + ref.invalidate(getDbSizeInBytesProvider); + } } From 2b46014b52737c0dc00ee383156712f4229d8e59 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Sat, 20 Jul 2024 23:58:29 +0200 Subject: [PATCH 014/979] feat: create provider for game filters --- lib/src/model/game/game_filter.dart | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 lib/src/model/game/game_filter.dart diff --git a/lib/src/model/game/game_filter.dart b/lib/src/model/game/game_filter.dart new file mode 100644 index 0000000000..efc9cad9d3 --- /dev/null +++ b/lib/src/model/game/game_filter.dart @@ -0,0 +1,22 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:lichess_mobile/src/model/common/perf.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'game_filter.freezed.dart'; +part 'game_filter.g.dart'; + +@riverpod +class GameFilter extends _$GameFilter { + + @override + GameFilterState build() { + return const GameFilterState(); + } +} + +@freezed +class GameFilterState with _$GameFilterState { + const factory GameFilterState({ + Perf? perf, + }) = _GameFilterState; +} From 6f6e18a147c54012bb33db3c6aa2199903b59b07 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Sun, 21 Jul 2024 00:53:01 +0200 Subject: [PATCH 015/979] feat: create filter in ui --- lib/src/model/game/game_filter.dart | 3 +- lib/src/view/user/game_history_screen.dart | 88 +++++++++++++++------- 2 files changed, 63 insertions(+), 28 deletions(-) diff --git a/lib/src/model/game/game_filter.dart b/lib/src/model/game/game_filter.dart index efc9cad9d3..af1d6b52b2 100644 --- a/lib/src/model/game/game_filter.dart +++ b/lib/src/model/game/game_filter.dart @@ -7,11 +7,12 @@ part 'game_filter.g.dart'; @riverpod class GameFilter extends _$GameFilter { - @override GameFilterState build() { return const GameFilterState(); } + + void setPerf(Perf? perf) => state = state.copyWith(perf: perf); } @freezed diff --git a/lib/src/view/user/game_history_screen.dart b/lib/src/view/user/game_history_screen.dart index 6d12043b48..17cdc8d8fa 100644 --- a/lib/src/view/user/game_history_screen.dart +++ b/lib/src/view/user/game_history_screen.dart @@ -2,10 +2,14 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; +import 'package:lichess_mobile/src/model/game/game_filter.dart'; import 'package:lichess_mobile/src/model/game/game_history.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/view/game/game_list_tile.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_choice_picker.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; @@ -137,39 +141,69 @@ class _BodyState extends ConsumerState<_Body> { ), ); + final gameFilterState = ref.watch(gameFilterProvider); + final perfFilterLabel = gameFilterState.perf?.title ?? + '${context.l10n.timeControl} / ${context.l10n.variant}'; + return gameListState.when( data: (state) { final list = state.gameList; return SafeArea( - child: ListView.builder( - controller: _scrollController, - itemCount: list.length + (state.isLoading ? 1 : 0), - itemBuilder: (context, index) { - if (state.isLoading && index == list.length) { - return const Padding( - padding: EdgeInsets.symmetric(vertical: 32.0), - child: CenterLoadingIndicator(), - ); - } else if (state.hasError && - state.hasMore && - index == list.length) { - // TODO: add a retry button - return const Padding( - padding: EdgeInsets.symmetric(vertical: 32.0), - child: Center( - child: Text( - 'Could not load more games', + child: Column( + children: [ + Container( + padding: Styles.bodyPadding, + child: Row( + children: [ + FatButton( + semanticsLabel: perfFilterLabel, + onPressed: () => showChoicePicker( + context, + choices: Perf.values, + selectedItem: gameFilterState.perf, + labelBuilder: (t) => Text(t!.title), + onSelectedItemChanged: (Perf? value) => ref + .read(gameFilterProvider.notifier) + .setPerf(value), + ), + child: Text(perfFilterLabel), ), - ), - ); - } - - return ExtendedGameListTile( - item: list[index], - userId: widget.user?.id, - ); - }, + ], + ), + ), + Expanded( + child: ListView.builder( + controller: _scrollController, + itemCount: list.length + (state.isLoading ? 1 : 0), + itemBuilder: (context, index) { + if (state.isLoading && index == list.length) { + return const Padding( + padding: EdgeInsets.symmetric(vertical: 32.0), + child: CenterLoadingIndicator(), + ); + } else if (state.hasError && + state.hasMore && + index == list.length) { + // TODO: add a retry button + return const Padding( + padding: EdgeInsets.symmetric(vertical: 32.0), + child: Center( + child: Text( + 'Could not load more games', + ), + ), + ); + } + + return ExtendedGameListTile( + item: list[index], + userId: widget.user?.id, + ); + }, + ), + ), + ], ), ); }, From af2bcee75f49a21453d6c0ede0846399a555e71a Mon Sep 17 00:00:00 2001 From: Mauritz Date: Sun, 21 Jul 2024 02:54:40 +0200 Subject: [PATCH 016/979] feat: apply filter to search --- lib/src/model/game/game_filter.dart | 4 ++-- lib/src/model/game/game_history.dart | 14 +++++++++----- lib/src/view/home/home_tab_screen.dart | 4 ++-- lib/src/view/user/game_history_screen.dart | 8 +++----- lib/src/view/user/recent_games.dart | 2 +- 5 files changed, 17 insertions(+), 15 deletions(-) diff --git a/lib/src/model/game/game_filter.dart b/lib/src/model/game/game_filter.dart index af1d6b52b2..bf589840ee 100644 --- a/lib/src/model/game/game_filter.dart +++ b/lib/src/model/game/game_filter.dart @@ -8,8 +8,8 @@ part 'game_filter.g.dart'; @riverpod class GameFilter extends _$GameFilter { @override - GameFilterState build() { - return const GameFilterState(); + GameFilterState build({Perf? perf}) { + return GameFilterState(perf: perf); } void setPerf(Perf? perf) => state = state.copyWith(perf: perf); diff --git a/lib/src/model/game/game_history.dart b/lib/src/model/game/game_history.dart index 940bd99edd..73d7d43246 100644 --- a/lib/src/model/game/game_history.dart +++ b/lib/src/model/game/game_history.dart @@ -33,15 +33,19 @@ const _nbPerPage = 20; /// stored locally are fetched instead. @riverpod Future> myRecentGames( - MyRecentGamesRef ref, -) async { + MyRecentGamesRef ref, { + Perf? perf, +}) async { final online = await ref .watch(connectivityChangesProvider.selectAsync((c) => c.isOnline)); final session = ref.watch(authSessionProvider); if (session != null && online) { return ref.withClientCacheFor( - (client) => GameRepository(client) - .getUserGames(session.user.id, max: kNumberOfRecentGames), + (client) => GameRepository(client).getUserGames( + session.user.id, + max: kNumberOfRecentGames, + perfType: perf, + ), const Duration(hours: 1), ); } else { @@ -132,7 +136,7 @@ class UserGameHistory extends _$UserGameHistory { perf: perf, ).future, ) - : ref.read(myRecentGamesProvider.future); + : ref.read(myRecentGamesProvider(perf: perf).future); _list.addAll(await recentGames); diff --git a/lib/src/view/home/home_tab_screen.dart b/lib/src/view/home/home_tab_screen.dart index 6298778c27..2b27803a77 100644 --- a/lib/src/view/home/home_tab_screen.dart +++ b/lib/src/view/home/home_tab_screen.dart @@ -179,7 +179,7 @@ class _HomeScreenState extends ConsumerState with RouteAware { Future _refreshData() { return Future.wait([ ref.refresh(accountProvider.future), - ref.refresh(myRecentGamesProvider.future), + ref.refresh(myRecentGamesProvider().future), ref.refresh(ongoingGamesProvider.future), ]); } @@ -214,7 +214,7 @@ class _HomeBody extends ConsumerWidget { data: (status) { final session = ref.watch(authSessionProvider); final isTablet = isTabletOrLarger(context); - final emptyRecent = ref.watch(myRecentGamesProvider).maybeWhen( + final emptyRecent = ref.watch(myRecentGamesProvider()).maybeWhen( data: (data) => data.isEmpty, orElse: () => false, ); diff --git a/lib/src/view/user/game_history_screen.dart b/lib/src/view/user/game_history_screen.dart index 17cdc8d8fa..593d69c52c 100644 --- a/lib/src/view/user/game_history_screen.dart +++ b/lib/src/view/user/game_history_screen.dart @@ -106,7 +106,6 @@ class _BodyState extends ConsumerState<_Body> { userGameHistoryProvider( widget.user?.id, isOnline: widget.isOnline, - perf: widget.perf, ), ); @@ -123,7 +122,6 @@ class _BodyState extends ConsumerState<_Body> { userGameHistoryProvider( widget.user?.id, isOnline: widget.isOnline, - perf: widget.perf, ).notifier, ) .getNext(); @@ -133,15 +131,15 @@ class _BodyState extends ConsumerState<_Body> { @override Widget build(BuildContext context) { + final gameFilterState = ref.watch(gameFilterProvider(perf: widget.perf)); final gameListState = ref.watch( userGameHistoryProvider( widget.user?.id, isOnline: widget.isOnline, - perf: widget.perf, + perf: gameFilterState.perf, ), ); - final gameFilterState = ref.watch(gameFilterProvider); final perfFilterLabel = gameFilterState.perf?.title ?? '${context.l10n.timeControl} / ${context.l10n.variant}'; @@ -164,7 +162,7 @@ class _BodyState extends ConsumerState<_Body> { selectedItem: gameFilterState.perf, labelBuilder: (t) => Text(t!.title), onSelectedItemChanged: (Perf? value) => ref - .read(gameFilterProvider.notifier) + .read(gameFilterProvider().notifier) .setPerf(value), ), child: Text(perfFilterLabel), diff --git a/lib/src/view/user/recent_games.dart b/lib/src/view/user/recent_games.dart index 599e23746a..53618dc26e 100644 --- a/lib/src/view/user/recent_games.dart +++ b/lib/src/view/user/recent_games.dart @@ -30,7 +30,7 @@ class RecentGamesWidget extends ConsumerWidget { final recentGames = user != null ? ref.watch(userRecentGamesProvider(userId: user!.id)) - : ref.watch(myRecentGamesProvider); + : ref.watch(myRecentGamesProvider()); final nbOfGames = ref .watch( From b7d536e3230c0307375622af5e1570840b1f1eec Mon Sep 17 00:00:00 2001 From: Mauritz Date: Sun, 21 Jul 2024 09:39:19 +0200 Subject: [PATCH 017/979] refactor: pass game filters object instead of individual params --- lib/src/model/game/game_history.dart | 24 ++++++++++++---------- lib/src/view/user/game_history_screen.dart | 23 +++++++++++++-------- lib/src/view/user/perf_stats_screen.dart | 3 ++- 3 files changed, 29 insertions(+), 21 deletions(-) diff --git a/lib/src/model/game/game_history.dart b/lib/src/model/game/game_history.dart index 73d7d43246..da5bbbdf0c 100644 --- a/lib/src/model/game/game_history.dart +++ b/lib/src/model/game/game_history.dart @@ -10,6 +10,7 @@ import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; import 'package:lichess_mobile/src/model/game/archived_game.dart'; +import 'package:lichess_mobile/src/model/game/game_filter.dart'; import 'package:lichess_mobile/src/model/game/game_repository.dart'; import 'package:lichess_mobile/src/model/game/game_storage.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; @@ -34,7 +35,7 @@ const _nbPerPage = 20; @riverpod Future> myRecentGames( MyRecentGamesRef ref, { - Perf? perf, + GameFilterState filters = const GameFilterState(), }) async { final online = await ref .watch(connectivityChangesProvider.selectAsync((c) => c.isOnline)); @@ -44,7 +45,7 @@ Future> myRecentGames( (client) => GameRepository(client).getUserGames( session.user.id, max: kNumberOfRecentGames, - perfType: perf, + perfType: filters.perf, ), const Duration(hours: 1), ); @@ -68,10 +69,11 @@ Future> myRecentGames( Future> userRecentGames( UserRecentGamesRef ref, { required UserId userId, - Perf? perf, + GameFilterState filters = const GameFilterState(), }) { return ref.withClientCacheFor( - (client) => GameRepository(client).getUserGames(userId, perfType: perf), + (client) => + GameRepository(client).getUserGames(userId, perfType: filters.perf), // cache is important because the associated widget is in a [ListView] and // the provider may be instanciated multiple times in a short period of time // (e.g. when scrolling) @@ -120,7 +122,7 @@ class UserGameHistory extends _$UserGameHistory { /// server. If this is false, the provider will fetch the games from the /// local storage. required bool isOnline, - Perf? perf, + GameFilterState filters = const GameFilterState(), }) async { ref.cacheFor(const Duration(minutes: 5)); ref.onDispose(() { @@ -133,10 +135,10 @@ class UserGameHistory extends _$UserGameHistory { ? ref.read( userRecentGamesProvider( userId: userId, - perf: perf, + filters: filters, ).future, ) - : ref.read(myRecentGamesProvider(perf: perf).future); + : ref.read(myRecentGamesProvider(filters: filters).future); _list.addAll(await recentGames); @@ -146,7 +148,7 @@ class UserGameHistory extends _$UserGameHistory { hasMore: true, hasError: false, online: isOnline, - perfType: perf, + filters: filters, session: session, ); } @@ -164,7 +166,7 @@ class UserGameHistory extends _$UserGameHistory { userId!, max: _nbPerPage, until: _list.last.game.createdAt, - perfType: currentVal.perfType, + perfType: currentVal.filters.perf, ), ) : currentVal.online && currentVal.session != null @@ -173,7 +175,7 @@ class UserGameHistory extends _$UserGameHistory { currentVal.session!.user.id, max: _nbPerPage, until: _list.last.game.createdAt, - perfType: currentVal.perfType, + perfType: currentVal.filters.perf, ), ) : ref @@ -223,7 +225,7 @@ class UserGameHistoryState with _$UserGameHistoryState { const factory UserGameHistoryState({ required IList gameList, required bool isLoading, - Perf? perfType, + required GameFilterState filters, required bool hasMore, required bool hasError, required bool online, diff --git a/lib/src/view/user/game_history_screen.dart b/lib/src/view/user/game_history_screen.dart index 593d69c52c..02abd83a67 100644 --- a/lib/src/view/user/game_history_screen.dart +++ b/lib/src/view/user/game_history_screen.dart @@ -17,13 +17,13 @@ class GameHistoryScreen extends ConsumerWidget { const GameHistoryScreen({ required this.user, required this.isOnline, - this.perf, + this.gameFilters = const GameFilterState(), this.games, super.key, }); final LightUser? user; final bool isOnline; - final Perf? perf; + final GameFilterState gameFilters; final int? games; @override @@ -47,7 +47,7 @@ class GameHistoryScreen extends ConsumerWidget { error: (e, s) => Text(context.l10n.mobileAllGames), ), ), - child: _Body(user: user, isOnline: isOnline, perf: perf), + child: _Body(user: user, isOnline: isOnline, gameFilters: gameFilters), ); } @@ -63,7 +63,7 @@ class GameHistoryScreen extends ConsumerWidget { error: (e, s) => Text(context.l10n.mobileAllGames), ), ), - body: _Body(user: user, isOnline: isOnline, perf: perf), + body: _Body(user: user, isOnline: isOnline, gameFilters: gameFilters), ); } } @@ -72,12 +72,12 @@ class _Body extends ConsumerStatefulWidget { const _Body({ required this.user, required this.isOnline, - required this.perf, + required this.gameFilters, }); final LightUser? user; final bool isOnline; - final Perf? perf; + final GameFilterState gameFilters; @override ConsumerState<_Body> createState() => _BodyState(); @@ -131,12 +131,13 @@ class _BodyState extends ConsumerState<_Body> { @override Widget build(BuildContext context) { - final gameFilterState = ref.watch(gameFilterProvider(perf: widget.perf)); + final gameFilterState = + ref.watch(gameFilterProvider(perf: widget.gameFilters.perf)); final gameListState = ref.watch( userGameHistoryProvider( widget.user?.id, isOnline: widget.isOnline, - perf: gameFilterState.perf, + filters: gameFilterState, ), ); @@ -162,7 +163,11 @@ class _BodyState extends ConsumerState<_Body> { selectedItem: gameFilterState.perf, labelBuilder: (t) => Text(t!.title), onSelectedItemChanged: (Perf? value) => ref - .read(gameFilterProvider().notifier) + .read( + gameFilterProvider( + perf: widget.gameFilters.perf, + ).notifier, + ) .setPerf(value), ), child: Text(perfFilterLabel), diff --git a/lib/src/view/user/perf_stats_screen.dart b/lib/src/view/user/perf_stats_screen.dart index cad1bf787e..214bb7e18c 100644 --- a/lib/src/view/user/perf_stats_screen.dart +++ b/lib/src/view/user/perf_stats_screen.dart @@ -13,6 +13,7 @@ import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; +import 'package:lichess_mobile/src/model/game/game_filter.dart'; import 'package:lichess_mobile/src/model/game/game_repository.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/model/user/user_repository_providers.dart'; @@ -282,7 +283,7 @@ class _Body extends ConsumerWidget { builder: (context) => GameHistoryScreen( user: user.lightUser, isOnline: true, - perf: perf, + gameFilters: GameFilterState(perf: perf), games: data.totalGames, ), ); From 2d9fd9c2a53f4c6018943080a6fd6ab558ce7fb5 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Sun, 21 Jul 2024 11:08:50 +0200 Subject: [PATCH 018/979] feat: filter with multiple perfs --- lib/src/model/game/game_filter.dart | 8 +++--- lib/src/model/game/game_history.dart | 9 +++--- lib/src/model/game/game_repository.dart | 13 +++++---- lib/src/view/user/game_history_screen.dart | 32 +++++++++++++--------- lib/src/view/user/perf_stats_screen.dart | 2 +- 5 files changed, 35 insertions(+), 29 deletions(-) diff --git a/lib/src/model/game/game_filter.dart b/lib/src/model/game/game_filter.dart index bf589840ee..8d011a0ae6 100644 --- a/lib/src/model/game/game_filter.dart +++ b/lib/src/model/game/game_filter.dart @@ -8,16 +8,16 @@ part 'game_filter.g.dart'; @riverpod class GameFilter extends _$GameFilter { @override - GameFilterState build({Perf? perf}) { - return GameFilterState(perf: perf); + GameFilterState build({Set? perfs}) { + return GameFilterState(perfs: perfs ?? {}); } - void setPerf(Perf? perf) => state = state.copyWith(perf: perf); + void setPerfs(Set? perfs) => state = state.copyWith(perfs: perfs ?? {}); } @freezed class GameFilterState with _$GameFilterState { const factory GameFilterState({ - Perf? perf, + @Default({}) Set perfs, }) = _GameFilterState; } diff --git a/lib/src/model/game/game_history.dart b/lib/src/model/game/game_history.dart index da5bbbdf0c..44ccb5ebea 100644 --- a/lib/src/model/game/game_history.dart +++ b/lib/src/model/game/game_history.dart @@ -8,7 +8,6 @@ import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/model/common/perf.dart'; import 'package:lichess_mobile/src/model/game/archived_game.dart'; import 'package:lichess_mobile/src/model/game/game_filter.dart'; import 'package:lichess_mobile/src/model/game/game_repository.dart'; @@ -45,7 +44,7 @@ Future> myRecentGames( (client) => GameRepository(client).getUserGames( session.user.id, max: kNumberOfRecentGames, - perfType: filters.perf, + perfs: filters.perfs, ), const Duration(hours: 1), ); @@ -73,7 +72,7 @@ Future> userRecentGames( }) { return ref.withClientCacheFor( (client) => - GameRepository(client).getUserGames(userId, perfType: filters.perf), + GameRepository(client).getUserGames(userId, perfs: filters.perfs), // cache is important because the associated widget is in a [ListView] and // the provider may be instanciated multiple times in a short period of time // (e.g. when scrolling) @@ -166,7 +165,7 @@ class UserGameHistory extends _$UserGameHistory { userId!, max: _nbPerPage, until: _list.last.game.createdAt, - perfType: currentVal.filters.perf, + perfs: currentVal.filters.perfs, ), ) : currentVal.online && currentVal.session != null @@ -175,7 +174,7 @@ class UserGameHistory extends _$UserGameHistory { currentVal.session!.user.id, max: _nbPerPage, until: _list.last.game.createdAt, - perfType: currentVal.filters.perf, + perfs: currentVal.filters.perfs, ), ) : ref diff --git a/lib/src/model/game/game_repository.dart b/lib/src/model/game/game_repository.dart index b6041eaca2..d5b6a8990b 100644 --- a/lib/src/model/game/game_repository.dart +++ b/lib/src/model/game/game_repository.dart @@ -40,12 +40,12 @@ class GameRepository { UserId userId, { int max = 20, DateTime? until, - Perf? perfType, + Set perfs = const {}, }) { - assert( - ![Perf.fromPosition, Perf.puzzle, Perf.storm, Perf.streak] - .contains(perfType), - ); + assert(!perfs.contains(Perf.fromPosition)); + assert(!perfs.contains(Perf.puzzle)); + assert(!perfs.contains(Perf.storm)); + assert(!perfs.contains(Perf.streak)); return client .readNdJsonList( Uri( @@ -54,7 +54,8 @@ class GameRepository { 'max': max.toString(), if (until != null) 'until': until.millisecondsSinceEpoch.toString(), - if (perfType != null) 'perfType': perfType.name, + if (perfs.isNotEmpty) + 'perfType': perfs.map((perf) => perf.name).join(','), 'moves': 'false', 'lastFen': 'true', 'accuracy': 'true', diff --git a/lib/src/view/user/game_history_screen.dart b/lib/src/view/user/game_history_screen.dart index 02abd83a67..5ad46179df 100644 --- a/lib/src/view/user/game_history_screen.dart +++ b/lib/src/view/user/game_history_screen.dart @@ -132,7 +132,7 @@ class _BodyState extends ConsumerState<_Body> { @override Widget build(BuildContext context) { final gameFilterState = - ref.watch(gameFilterProvider(perf: widget.gameFilters.perf)); + ref.watch(gameFilterProvider(perfs: widget.gameFilters.perfs)); final gameListState = ref.watch( userGameHistoryProvider( widget.user?.id, @@ -141,8 +141,11 @@ class _BodyState extends ConsumerState<_Body> { ), ); - final perfFilterLabel = gameFilterState.perf?.title ?? - '${context.l10n.timeControl} / ${context.l10n.variant}'; + final perfFilterLabel = gameFilterState.perfs.isEmpty + ? '${context.l10n.timeControl} / ${context.l10n.variant}' + : gameFilterState.perfs.length == 1 + ? gameFilterState.perfs.first.title + : '${gameFilterState.perfs.length}'; return gameListState.when( data: (state) { @@ -157,18 +160,21 @@ class _BodyState extends ConsumerState<_Body> { children: [ FatButton( semanticsLabel: perfFilterLabel, - onPressed: () => showChoicePicker( + onPressed: () => showMultipleChoicesPicker( context, choices: Perf.values, - selectedItem: gameFilterState.perf, - labelBuilder: (t) => Text(t!.title), - onSelectedItemChanged: (Perf? value) => ref - .read( - gameFilterProvider( - perf: widget.gameFilters.perf, - ).notifier, - ) - .setPerf(value), + selectedItems: gameFilterState.perfs, + labelBuilder: (t) => Text(t.title), + ).then( + (value) => value != null + ? ref + .read( + gameFilterProvider( + perfs: widget.gameFilters.perfs, + ).notifier, + ) + .setPerfs(value) + : null, ), child: Text(perfFilterLabel), ), diff --git a/lib/src/view/user/perf_stats_screen.dart b/lib/src/view/user/perf_stats_screen.dart index 214bb7e18c..6c2a09c330 100644 --- a/lib/src/view/user/perf_stats_screen.dart +++ b/lib/src/view/user/perf_stats_screen.dart @@ -283,7 +283,7 @@ class _Body extends ConsumerWidget { builder: (context) => GameHistoryScreen( user: user.lightUser, isOnline: true, - gameFilters: GameFilterState(perf: perf), + gameFilters: GameFilterState(perfs: {perf}), games: data.totalGames, ), ); From 6452a4f27ad9537dfd9804f516b842bf51863b2b Mon Sep 17 00:00:00 2001 From: Mauritz Date: Sun, 21 Jul 2024 12:12:30 +0200 Subject: [PATCH 019/979] feat: indicate how many filters in use --- lib/src/view/user/game_history_screen.dart | 26 +++++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/lib/src/view/user/game_history_screen.dart b/lib/src/view/user/game_history_screen.dart index 5ad46179df..6ad0d9a00e 100644 --- a/lib/src/view/user/game_history_screen.dart +++ b/lib/src/view/user/game_history_screen.dart @@ -141,11 +141,9 @@ class _BodyState extends ConsumerState<_Body> { ), ); - final perfFilterLabel = gameFilterState.perfs.isEmpty - ? '${context.l10n.timeControl} / ${context.l10n.variant}' - : gameFilterState.perfs.length == 1 - ? gameFilterState.perfs.first.title - : '${gameFilterState.perfs.length}'; + final perfFilterLabel = gameFilterState.perfs.length == 1 + ? gameFilterState.perfs.first.title + : '${context.l10n.timeControl} / ${context.l10n.variant}'; return gameListState.when( data: (state) { @@ -176,7 +174,23 @@ class _BodyState extends ConsumerState<_Body> { .setPerfs(value) : null, ), - child: Text(perfFilterLabel), + child: Row( + children: [ + if (gameFilterState.perfs.length > 1) + Container( + padding: const EdgeInsets.all(8), + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + child: Text( + '${gameFilterState.perfs.length}', + textAlign: TextAlign.center, + ), + ), + Text(' $perfFilterLabel'), + ], + ), ), ], ), From fa1bfc0d068f277fe42f49f1730d8fd858465db6 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Sun, 21 Jul 2024 12:32:32 +0200 Subject: [PATCH 020/979] refactor: separate widget for multiple choice filter --- lib/src/view/user/game_history_screen.dart | 95 ++++++++++++++-------- 1 file changed, 60 insertions(+), 35 deletions(-) diff --git a/lib/src/view/user/game_history_screen.dart b/lib/src/view/user/game_history_screen.dart index 6ad0d9a00e..2c659beb5d 100644 --- a/lib/src/view/user/game_history_screen.dart +++ b/lib/src/view/user/game_history_screen.dart @@ -156,41 +156,20 @@ class _BodyState extends ConsumerState<_Body> { padding: Styles.bodyPadding, child: Row( children: [ - FatButton( - semanticsLabel: perfFilterLabel, - onPressed: () => showMultipleChoicesPicker( - context, - choices: Perf.values, - selectedItems: gameFilterState.perfs, - labelBuilder: (t) => Text(t.title), - ).then( - (value) => value != null - ? ref - .read( - gameFilterProvider( - perfs: widget.gameFilters.perfs, - ).notifier, - ) - .setPerfs(value) - : null, - ), - child: Row( - children: [ - if (gameFilterState.perfs.length > 1) - Container( - padding: const EdgeInsets.all(8), - decoration: const BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, - ), - child: Text( - '${gameFilterState.perfs.length}', - textAlign: TextAlign.center, - ), - ), - Text(' $perfFilterLabel'), - ], - ), + _MultipleChoiceFilter( + filterLabel: perfFilterLabel, + choices: Perf.values, + selectedItems: gameFilterState.perfs, + choiceLabelBuilder: (t) => Text(t.title), + onChanged: (value) => value != null + ? ref + .read( + gameFilterProvider( + perfs: widget.gameFilters.perfs, + ).notifier, + ) + .setPerfs(value) + : null, ), ], ), @@ -240,3 +219,49 @@ class _BodyState extends ConsumerState<_Body> { ); } } + +class _MultipleChoiceFilter extends StatelessWidget { + const _MultipleChoiceFilter({ + required this.filterLabel, + required this.choices, + required this.selectedItems, + required this.choiceLabelBuilder, + required this.onChanged, + }); + + final String filterLabel; + final Iterable choices; + final Set selectedItems; + final Widget Function(T choice) choiceLabelBuilder; + final void Function(Set? value) onChanged; + + @override + Widget build(BuildContext context) { + return FatButton( + semanticsLabel: filterLabel, + onPressed: () => showMultipleChoicesPicker( + context, + choices: choices, + selectedItems: selectedItems, + labelBuilder: choiceLabelBuilder, + ).then(onChanged), + child: Row( + children: [ + if (selectedItems.length > 1) + Container( + padding: const EdgeInsets.all(8), + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + child: Text( + '${selectedItems.length}', + textAlign: TextAlign.center, + ), + ), + Text(' $filterLabel'), + ], + ), + ); + } +} From 9b6d1046346452f3bb8b933b0bc89da79cf47502 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Sun, 21 Jul 2024 12:43:01 +0200 Subject: [PATCH 021/979] fix: do not show amount of games found Since it's not possible to count games when complex filter in use --- lib/src/view/user/game_history_screen.dart | 24 ++-------------------- lib/src/view/user/perf_stats_screen.dart | 1 - 2 files changed, 2 insertions(+), 23 deletions(-) diff --git a/lib/src/view/user/game_history_screen.dart b/lib/src/view/user/game_history_screen.dart index 2c659beb5d..678f13dc05 100644 --- a/lib/src/view/user/game_history_screen.dart +++ b/lib/src/view/user/game_history_screen.dart @@ -18,13 +18,11 @@ class GameHistoryScreen extends ConsumerWidget { required this.user, required this.isOnline, this.gameFilters = const GameFilterState(), - this.games, super.key, }); final LightUser? user; final bool isOnline; final GameFilterState gameFilters; - final int? games; @override Widget build(BuildContext context, WidgetRef ref) { @@ -36,33 +34,15 @@ class GameHistoryScreen extends ConsumerWidget { } Widget _buildIos(BuildContext context, WidgetRef ref) { - final nbGamesAsync = ref.watch( - userNumberOfGamesProvider(user, isOnline: isOnline), - ); return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - middle: nbGamesAsync.when( - data: (nbGames) => Text(context.l10n.nbGames(games ?? nbGames)), - loading: () => const CupertinoActivityIndicator(), - error: (e, s) => Text(context.l10n.mobileAllGames), - ), - ), + navigationBar: CupertinoNavigationBar(middle: Text(context.l10n.games)), child: _Body(user: user, isOnline: isOnline, gameFilters: gameFilters), ); } Widget _buildAndroid(BuildContext context, WidgetRef ref) { - final nbGamesAsync = ref.watch( - userNumberOfGamesProvider(user, isOnline: isOnline), - ); return Scaffold( - appBar: AppBar( - title: nbGamesAsync.when( - data: (nbGames) => Text(context.l10n.nbGames(games ?? nbGames)), - loading: () => const ButtonLoadingIndicator(), - error: (e, s) => Text(context.l10n.mobileAllGames), - ), - ), + appBar: AppBar(title: Text(context.l10n.games)), body: _Body(user: user, isOnline: isOnline, gameFilters: gameFilters), ); } diff --git a/lib/src/view/user/perf_stats_screen.dart b/lib/src/view/user/perf_stats_screen.dart index 6c2a09c330..b66427cb25 100644 --- a/lib/src/view/user/perf_stats_screen.dart +++ b/lib/src/view/user/perf_stats_screen.dart @@ -284,7 +284,6 @@ class _Body extends ConsumerWidget { user: user.lightUser, isOnline: true, gameFilters: GameFilterState(perfs: {perf}), - games: data.totalGames, ), ); }, From 35f4c981d65b09b198201703a0c8a2044dd1d7ae Mon Sep 17 00:00:00 2001 From: Mauritz Date: Sun, 21 Jul 2024 12:51:32 +0200 Subject: [PATCH 022/979] feat: show message if no games found --- lib/src/view/user/game_history_screen.dart | 66 +++++++++++++--------- 1 file changed, 38 insertions(+), 28 deletions(-) diff --git a/lib/src/view/user/game_history_screen.dart b/lib/src/view/user/game_history_screen.dart index 678f13dc05..d1e204c75e 100644 --- a/lib/src/view/user/game_history_screen.dart +++ b/lib/src/view/user/game_history_screen.dart @@ -154,37 +154,47 @@ class _BodyState extends ConsumerState<_Body> { ], ), ), - Expanded( - child: ListView.builder( - controller: _scrollController, - itemCount: list.length + (state.isLoading ? 1 : 0), - itemBuilder: (context, index) { - if (state.isLoading && index == list.length) { - return const Padding( - padding: EdgeInsets.symmetric(vertical: 32.0), - child: CenterLoadingIndicator(), - ); - } else if (state.hasError && - state.hasMore && - index == list.length) { - // TODO: add a retry button - return const Padding( - padding: EdgeInsets.symmetric(vertical: 32.0), - child: Center( - child: Text( - 'Could not load more games', + if (list.isEmpty) + const Padding( + padding: EdgeInsets.symmetric(vertical: 32.0), + child: Center( + child: Text( + 'No games found', + ), + ), + ) + else + Expanded( + child: ListView.builder( + controller: _scrollController, + itemCount: list.length + (state.isLoading ? 1 : 0), + itemBuilder: (context, index) { + if (state.isLoading && index == list.length) { + return const Padding( + padding: EdgeInsets.symmetric(vertical: 32.0), + child: CenterLoadingIndicator(), + ); + } else if (state.hasError && + state.hasMore && + index == list.length) { + // TODO: add a retry button + return const Padding( + padding: EdgeInsets.symmetric(vertical: 32.0), + child: Center( + child: Text( + 'Could not load more games', + ), ), - ), - ); - } + ); + } - return ExtendedGameListTile( - item: list[index], - userId: widget.user?.id, - ); - }, + return ExtendedGameListTile( + item: list[index], + userId: widget.user?.id, + ); + }, + ), ), - ), ], ), ); From 304b0c3baebb6c414f62debb55374b731470c4c6 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Sun, 21 Jul 2024 12:57:54 +0200 Subject: [PATCH 023/979] fix: only allow valid Perf enum values --- lib/src/view/user/game_history_screen.dart | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/lib/src/view/user/game_history_screen.dart b/lib/src/view/user/game_history_screen.dart index d1e204c75e..f8a911677d 100644 --- a/lib/src/view/user/game_history_screen.dart +++ b/lib/src/view/user/game_history_screen.dart @@ -138,7 +138,22 @@ class _BodyState extends ConsumerState<_Body> { children: [ _MultipleChoiceFilter( filterLabel: perfFilterLabel, - choices: Perf.values, + choices: const [ + Perf.ultraBullet, + Perf.bullet, + Perf.blitz, + Perf.rapid, + Perf.classical, + Perf.correspondence, + Perf.chess960, + Perf.antichess, + Perf.kingOfTheHill, + Perf.threeCheck, + Perf.atomic, + Perf.horde, + Perf.racingKings, + Perf.crazyhouse, + ], selectedItems: gameFilterState.perfs, choiceLabelBuilder: (t) => Text(t.title), onChanged: (value) => value != null From d97afa9bfcfafce4fa8e376935868569d5c3b42b Mon Sep 17 00:00:00 2001 From: Mauritz Date: Sun, 21 Jul 2024 14:39:44 +0200 Subject: [PATCH 024/979] feat: highlight button if filter is used --- lib/src/view/user/game_history_screen.dart | 58 +++++++++++++--------- 1 file changed, 34 insertions(+), 24 deletions(-) diff --git a/lib/src/view/user/game_history_screen.dart b/lib/src/view/user/game_history_screen.dart index f8a911677d..8292260949 100644 --- a/lib/src/view/user/game_history_screen.dart +++ b/lib/src/view/user/game_history_screen.dart @@ -242,31 +242,41 @@ class _MultipleChoiceFilter extends StatelessWidget { @override Widget build(BuildContext context) { - return FatButton( - semanticsLabel: filterLabel, - onPressed: () => showMultipleChoicesPicker( - context, - choices: choices, - selectedItems: selectedItems, - labelBuilder: choiceLabelBuilder, - ).then(onChanged), - child: Row( - children: [ - if (selectedItems.length > 1) - Container( - padding: const EdgeInsets.all(8), - decoration: const BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, - ), - child: Text( - '${selectedItems.length}', - textAlign: TextAlign.center, - ), + void onPressed() => showMultipleChoicesPicker( + context, + choices: choices, + selectedItems: selectedItems, + labelBuilder: choiceLabelBuilder, + ).then(onChanged); + + final Widget child = Row( + children: [ + if (selectedItems.length > 1) + Container( + padding: const EdgeInsets.all(8), + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, ), - Text(' $filterLabel'), - ], - ), + child: Text( + '${selectedItems.length}', + textAlign: TextAlign.center, + ), + ), + Text(' $filterLabel'), + ], ); + + return selectedItems.isEmpty + ? SecondaryButton( + semanticsLabel: filterLabel, + onPressed: onPressed, + child: child, + ) + : FatButton( + semanticsLabel: filterLabel, + onPressed: onPressed, + child: child, + ); } } From 4dcc8eb99c8519b6194ba11db9d4221570e6f551 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Sun, 21 Jul 2024 14:40:20 +0200 Subject: [PATCH 025/979] fix: make text readable on all themes --- lib/src/view/user/game_history_screen.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/src/view/user/game_history_screen.dart b/lib/src/view/user/game_history_screen.dart index 8292260949..8aca8edea6 100644 --- a/lib/src/view/user/game_history_screen.dart +++ b/lib/src/view/user/game_history_screen.dart @@ -254,13 +254,14 @@ class _MultipleChoiceFilter extends StatelessWidget { if (selectedItems.length > 1) Container( padding: const EdgeInsets.all(8), - decoration: const BoxDecoration( - color: Colors.white, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.onPrimary, shape: BoxShape.circle, ), child: Text( '${selectedItems.length}', textAlign: TextAlign.center, + style: TextStyle(color: Theme.of(context).colorScheme.primary), ), ), Text(' $filterLabel'), From af1c3b035cc65fac85185675a55af0979ba2cd8c Mon Sep 17 00:00:00 2001 From: Mauritz Date: Sun, 21 Jul 2024 14:58:57 +0200 Subject: [PATCH 026/979] fix: sync paginator with filters --- lib/src/view/user/game_history_screen.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/src/view/user/game_history_screen.dart b/lib/src/view/user/game_history_screen.dart index 8aca8edea6..4399f60720 100644 --- a/lib/src/view/user/game_history_screen.dart +++ b/lib/src/view/user/game_history_screen.dart @@ -86,6 +86,8 @@ class _BodyState extends ConsumerState<_Body> { userGameHistoryProvider( widget.user?.id, isOnline: widget.isOnline, + filters: + ref.read(gameFilterProvider(perfs: widget.gameFilters.perfs)), ), ); @@ -102,6 +104,8 @@ class _BodyState extends ConsumerState<_Body> { userGameHistoryProvider( widget.user?.id, isOnline: widget.isOnline, + filters: ref + .read(gameFilterProvider(perfs: widget.gameFilters.perfs)), ).notifier, ) .getNext(); From 9026b1011dd7e6ae1d79bfc00393cad54ac55413 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Mon, 22 Jul 2024 20:45:13 +0200 Subject: [PATCH 027/979] feat: use chips from material3 --- lib/src/model/game/game_filter.dart | 2 +- lib/src/view/user/game_history_screen.dart | 115 ++++++++++++--------- 2 files changed, 68 insertions(+), 49 deletions(-) diff --git a/lib/src/model/game/game_filter.dart b/lib/src/model/game/game_filter.dart index 8d011a0ae6..e6a613d515 100644 --- a/lib/src/model/game/game_filter.dart +++ b/lib/src/model/game/game_filter.dart @@ -12,7 +12,7 @@ class GameFilter extends _$GameFilter { return GameFilterState(perfs: perfs ?? {}); } - void setPerfs(Set? perfs) => state = state.copyWith(perfs: perfs ?? {}); + void setPerfs(Set perfs) => state = state.copyWith(perfs: perfs); } @freezed diff --git a/lib/src/view/user/game_history_screen.dart b/lib/src/view/user/game_history_screen.dart index 4399f60720..8d56240a71 100644 --- a/lib/src/view/user/game_history_screen.dart +++ b/lib/src/view/user/game_history_screen.dart @@ -8,8 +8,6 @@ import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/view/game/game_list_tile.dart'; -import 'package:lichess_mobile/src/widgets/adaptive_choice_picker.dart'; -import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; @@ -160,15 +158,13 @@ class _BodyState extends ConsumerState<_Body> { ], selectedItems: gameFilterState.perfs, choiceLabelBuilder: (t) => Text(t.title), - onChanged: (value) => value != null - ? ref - .read( - gameFilterProvider( - perfs: widget.gameFilters.perfs, - ).notifier, - ) - .setPerfs(value) - : null, + onChanged: (value) => ref + .read( + gameFilterProvider( + perfs: widget.gameFilters.perfs, + ).notifier, + ) + .setPerfs(value), ), ], ), @@ -229,7 +225,7 @@ class _BodyState extends ConsumerState<_Body> { } } -class _MultipleChoiceFilter extends StatelessWidget { +class _MultipleChoiceFilter extends StatefulWidget { const _MultipleChoiceFilter({ required this.filterLabel, required this.choices, @@ -242,46 +238,69 @@ class _MultipleChoiceFilter extends StatelessWidget { final Iterable choices; final Set selectedItems; final Widget Function(T choice) choiceLabelBuilder; - final void Function(Set? value) onChanged; + final void Function(Set value) onChanged; @override - Widget build(BuildContext context) { - void onPressed() => showMultipleChoicesPicker( - context, - choices: choices, - selectedItems: selectedItems, - labelBuilder: choiceLabelBuilder, - ).then(onChanged); + State<_MultipleChoiceFilter> createState() => _MultipleChoiceFilterState(); +} - final Widget child = Row( - children: [ - if (selectedItems.length > 1) - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.onPrimary, - shape: BoxShape.circle, - ), - child: Text( - '${selectedItems.length}', - textAlign: TextAlign.center, - style: TextStyle(color: Theme.of(context).colorScheme.primary), - ), - ), - Text(' $filterLabel'), - ], - ); +class _MultipleChoiceFilterState + extends State<_MultipleChoiceFilter> { + late Set items; + + @override + void initState() { + super.initState(); + items = {...widget.selectedItems}; + } - return selectedItems.isEmpty - ? SecondaryButton( - semanticsLabel: filterLabel, - onPressed: onPressed, - child: child, + @override + Widget build(BuildContext context) { + return MenuAnchor( + onClose: () => widget.onChanged(items), + menuChildren: widget.choices + .map( + (choice) => FilterChip( + label: widget.choiceLabelBuilder(choice), + selected: items.contains(choice), + onSelected: (value) { + setState(() { + items = value + ? items.union({choice}) + : items.difference({choice}); + }); + }, + ), ) - : FatButton( - semanticsLabel: filterLabel, - onPressed: onPressed, - child: child, - ); + .toList(growable: false), + builder: ( + BuildContext context, + MenuController controller, + Widget? child, + ) => + TextButton( + onPressed: () => + controller.isOpen ? controller.close() : controller.open(), + child: Row( + children: [ + if (items.length > 1) + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.onPrimary, + shape: BoxShape.circle, + ), + child: Text( + '${items.length}', + textAlign: TextAlign.center, + style: + TextStyle(color: Theme.of(context).colorScheme.primary), + ), + ), + Text(' ${widget.filterLabel}'), + ], + ), + ), + ); } } From c830b5769971018d49bc116e422c42b3125e77d0 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Mon, 22 Jul 2024 20:50:33 +0200 Subject: [PATCH 028/979] refactor: use ISet instead of Set --- lib/src/model/game/game_filter.dart | 9 +++++---- lib/src/model/game/game_repository.dart | 2 +- lib/src/view/user/game_history_screen.dart | 9 +++++---- lib/src/view/user/perf_stats_screen.dart | 2 +- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/lib/src/model/game/game_filter.dart b/lib/src/model/game/game_filter.dart index e6a613d515..7eff5f6f52 100644 --- a/lib/src/model/game/game_filter.dart +++ b/lib/src/model/game/game_filter.dart @@ -1,3 +1,4 @@ +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -8,16 +9,16 @@ part 'game_filter.g.dart'; @riverpod class GameFilter extends _$GameFilter { @override - GameFilterState build({Set? perfs}) { - return GameFilterState(perfs: perfs ?? {}); + GameFilterState build({ISet? perfs}) { + return GameFilterState(perfs: perfs ?? const ISet.empty()); } - void setPerfs(Set perfs) => state = state.copyWith(perfs: perfs); + void setPerfs(ISet perfs) => state = state.copyWith(perfs: perfs); } @freezed class GameFilterState with _$GameFilterState { const factory GameFilterState({ - @Default({}) Set perfs, + @Default(ISet.empty()) ISet perfs, }) = _GameFilterState; } diff --git a/lib/src/model/game/game_repository.dart b/lib/src/model/game/game_repository.dart index d5b6a8990b..d4bfd63d65 100644 --- a/lib/src/model/game/game_repository.dart +++ b/lib/src/model/game/game_repository.dart @@ -40,7 +40,7 @@ class GameRepository { UserId userId, { int max = 20, DateTime? until, - Set perfs = const {}, + ISet perfs = const ISet.empty(), }) { assert(!perfs.contains(Perf.fromPosition)); assert(!perfs.contains(Perf.puzzle)); diff --git a/lib/src/view/user/game_history_screen.dart b/lib/src/view/user/game_history_screen.dart index 8d56240a71..d530df4bf3 100644 --- a/lib/src/view/user/game_history_screen.dart +++ b/lib/src/view/user/game_history_screen.dart @@ -1,3 +1,4 @@ +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -236,9 +237,9 @@ class _MultipleChoiceFilter extends StatefulWidget { final String filterLabel; final Iterable choices; - final Set selectedItems; + final ISet selectedItems; final Widget Function(T choice) choiceLabelBuilder; - final void Function(Set value) onChanged; + final void Function(ISet value) onChanged; @override State<_MultipleChoiceFilter> createState() => _MultipleChoiceFilterState(); @@ -246,12 +247,12 @@ class _MultipleChoiceFilter extends StatefulWidget { class _MultipleChoiceFilterState extends State<_MultipleChoiceFilter> { - late Set items; + late ISet items; @override void initState() { super.initState(); - items = {...widget.selectedItems}; + items = widget.selectedItems; } @override diff --git a/lib/src/view/user/perf_stats_screen.dart b/lib/src/view/user/perf_stats_screen.dart index b66427cb25..2fd6e4b6e4 100644 --- a/lib/src/view/user/perf_stats_screen.dart +++ b/lib/src/view/user/perf_stats_screen.dart @@ -283,7 +283,7 @@ class _Body extends ConsumerWidget { builder: (context) => GameHistoryScreen( user: user.lightUser, isOnline: true, - gameFilters: GameFilterState(perfs: {perf}), + gameFilters: GameFilterState(perfs: ISet({perf})), ), ); }, From 1febfbfd83cf4442207317666479e277439afcc6 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Mon, 22 Jul 2024 22:07:04 +0200 Subject: [PATCH 029/979] feat: add btn colors depending on state --- lib/src/view/user/game_history_screen.dart | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/src/view/user/game_history_screen.dart b/lib/src/view/user/game_history_screen.dart index d530df4bf3..cf3ba13f46 100644 --- a/lib/src/view/user/game_history_screen.dart +++ b/lib/src/view/user/game_history_screen.dart @@ -282,11 +282,19 @@ class _MultipleChoiceFilterState TextButton( onPressed: () => controller.isOpen ? controller.close() : controller.open(), + style: TextButton.styleFrom( + backgroundColor: items.isEmpty + ? Theme.of(context).colorScheme.secondary + : Theme.of(context).colorScheme.primary, + foregroundColor: items.isEmpty + ? Theme.of(context).colorScheme.onSecondary + : Theme.of(context).colorScheme.onPrimary, + ), child: Row( children: [ if (items.length > 1) Container( - padding: const EdgeInsets.all(8), + padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: Theme.of(context).colorScheme.onPrimary, shape: BoxShape.circle, From 43ade6e9eb12bb141e440fc064211920ccefbc97 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Thu, 25 Jul 2024 13:21:17 +0200 Subject: [PATCH 030/979] refactor: rename variable --- lib/src/view/user/game_history_screen.dart | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/src/view/user/game_history_screen.dart b/lib/src/view/user/game_history_screen.dart index cf3ba13f46..3d7336a418 100644 --- a/lib/src/view/user/game_history_screen.dart +++ b/lib/src/view/user/game_history_screen.dart @@ -247,28 +247,28 @@ class _MultipleChoiceFilter extends StatefulWidget { class _MultipleChoiceFilterState extends State<_MultipleChoiceFilter> { - late ISet items; + late ISet selectedItems; @override void initState() { super.initState(); - items = widget.selectedItems; + selectedItems = widget.selectedItems; } @override Widget build(BuildContext context) { return MenuAnchor( - onClose: () => widget.onChanged(items), + onClose: () => widget.onChanged(selectedItems), menuChildren: widget.choices .map( (choice) => FilterChip( label: widget.choiceLabelBuilder(choice), - selected: items.contains(choice), + selected: selectedItems.contains(choice), onSelected: (value) { setState(() { - items = value - ? items.union({choice}) - : items.difference({choice}); + selectedItems = value + ? selectedItems.add(choice) + : selectedItems.remove(choice); }); }, ), @@ -283,16 +283,16 @@ class _MultipleChoiceFilterState onPressed: () => controller.isOpen ? controller.close() : controller.open(), style: TextButton.styleFrom( - backgroundColor: items.isEmpty + backgroundColor: selectedItems.isEmpty ? Theme.of(context).colorScheme.secondary : Theme.of(context).colorScheme.primary, - foregroundColor: items.isEmpty + foregroundColor: selectedItems.isEmpty ? Theme.of(context).colorScheme.onSecondary : Theme.of(context).colorScheme.onPrimary, ), child: Row( children: [ - if (items.length > 1) + if (selectedItems.length > 1) Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration( @@ -300,7 +300,7 @@ class _MultipleChoiceFilterState shape: BoxShape.circle, ), child: Text( - '${items.length}', + '${selectedItems.length}', textAlign: TextAlign.center, style: TextStyle(color: Theme.of(context).colorScheme.primary), From 19be3a6b810d9c72498cbd4cf52bdf90e6003f96 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Thu, 25 Jul 2024 13:32:23 +0200 Subject: [PATCH 031/979] feat: update menu button label immediately when changed --- lib/src/view/user/game_history_screen.dart | 25 +++++++++++----------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/lib/src/view/user/game_history_screen.dart b/lib/src/view/user/game_history_screen.dart index 3d7336a418..ed73be15c1 100644 --- a/lib/src/view/user/game_history_screen.dart +++ b/lib/src/view/user/game_history_screen.dart @@ -124,10 +124,6 @@ class _BodyState extends ConsumerState<_Body> { ), ); - final perfFilterLabel = gameFilterState.perfs.length == 1 - ? gameFilterState.perfs.first.title - : '${context.l10n.timeControl} / ${context.l10n.variant}'; - return gameListState.when( data: (state) { final list = state.gameList; @@ -140,7 +136,8 @@ class _BodyState extends ConsumerState<_Body> { child: Row( children: [ _MultipleChoiceFilter( - filterLabel: perfFilterLabel, + filterName: + '${context.l10n.timeControl} / ${context.l10n.variant}', choices: const [ Perf.ultraBullet, Perf.bullet, @@ -158,7 +155,7 @@ class _BodyState extends ConsumerState<_Body> { Perf.crazyhouse, ], selectedItems: gameFilterState.perfs, - choiceLabelBuilder: (t) => Text(t.title), + choiceLabel: (t) => t.title, onChanged: (value) => ref .read( gameFilterProvider( @@ -228,17 +225,17 @@ class _BodyState extends ConsumerState<_Body> { class _MultipleChoiceFilter extends StatefulWidget { const _MultipleChoiceFilter({ - required this.filterLabel, + required this.filterName, required this.choices, required this.selectedItems, - required this.choiceLabelBuilder, + required this.choiceLabel, required this.onChanged, }); - final String filterLabel; + final String filterName; final Iterable choices; final ISet selectedItems; - final Widget Function(T choice) choiceLabelBuilder; + final String Function(T choice) choiceLabel; final void Function(ISet value) onChanged; @override @@ -262,7 +259,7 @@ class _MultipleChoiceFilterState menuChildren: widget.choices .map( (choice) => FilterChip( - label: widget.choiceLabelBuilder(choice), + label: Text(widget.choiceLabel(choice)), selected: selectedItems.contains(choice), onSelected: (value) { setState(() { @@ -306,7 +303,11 @@ class _MultipleChoiceFilterState TextStyle(color: Theme.of(context).colorScheme.primary), ), ), - Text(' ${widget.filterLabel}'), + Text( + selectedItems.length == 1 + ? widget.choiceLabel(selectedItems.first) + : widget.filterName, + ), ], ), ), From ae8de7963667534da00138f0ed5a7046dacc8e9a Mon Sep 17 00:00:00 2001 From: Mauritz Date: Thu, 25 Jul 2024 14:42:53 +0200 Subject: [PATCH 032/979] fix: minor style fixes --- lib/src/view/user/game_history_screen.dart | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/src/view/user/game_history_screen.dart b/lib/src/view/user/game_history_screen.dart index ed73be15c1..d4b0e62d80 100644 --- a/lib/src/view/user/game_history_screen.dart +++ b/lib/src/view/user/game_history_screen.dart @@ -11,6 +11,7 @@ import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/view/game/game_list_tile.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:material_color_utilities/utils/math_utils.dart'; class GameHistoryScreen extends ConsumerWidget { const GameHistoryScreen({ @@ -239,7 +240,8 @@ class _MultipleChoiceFilter extends StatefulWidget { final void Function(ISet value) onChanged; @override - State<_MultipleChoiceFilter> createState() => _MultipleChoiceFilterState(); + State<_MultipleChoiceFilter> createState() => + _MultipleChoiceFilterState(); } class _MultipleChoiceFilterState @@ -255,6 +257,13 @@ class _MultipleChoiceFilterState @override Widget build(BuildContext context) { return MenuAnchor( + style: MenuStyle( + maximumSize: WidgetStatePropertyAll( + Size.fromHeight( + MediaQuery.sizeOf(context).height * 0.7, + ), + ), + ), onClose: () => widget.onChanged(selectedItems), menuChildren: widget.choices .map( @@ -292,6 +301,7 @@ class _MultipleChoiceFilterState if (selectedItems.length > 1) Container( padding: const EdgeInsets.all(4), + margin: const EdgeInsets.only(right: 5), decoration: BoxDecoration( color: Theme.of(context).colorScheme.onPrimary, shape: BoxShape.circle, From 8d5a7a7d1baf2a09738adbfd574bca18426093ee Mon Sep 17 00:00:00 2001 From: Mauritz Date: Thu, 25 Jul 2024 16:14:21 +0200 Subject: [PATCH 033/979] feat: use gridview instead of menu for layout --- lib/src/view/user/game_history_screen.dart | 189 ++++++++++++--------- 1 file changed, 112 insertions(+), 77 deletions(-) diff --git a/lib/src/view/user/game_history_screen.dart b/lib/src/view/user/game_history_screen.dart index d4b0e62d80..dc6e9a76a2 100644 --- a/lib/src/view/user/game_history_screen.dart +++ b/lib/src/view/user/game_history_screen.dart @@ -224,7 +224,7 @@ class _BodyState extends ConsumerState<_Body> { } } -class _MultipleChoiceFilter extends StatefulWidget { +class _MultipleChoiceFilter extends StatelessWidget { const _MultipleChoiceFilter({ required this.filterName, required this.choices, @@ -239,88 +239,123 @@ class _MultipleChoiceFilter extends StatefulWidget { final String Function(T choice) choiceLabel; final void Function(ISet value) onChanged; - @override - State<_MultipleChoiceFilter> createState() => - _MultipleChoiceFilterState(); -} - -class _MultipleChoiceFilterState - extends State<_MultipleChoiceFilter> { - late ISet selectedItems; - - @override - void initState() { - super.initState(); - selectedItems = widget.selectedItems; - } - @override Widget build(BuildContext context) { - return MenuAnchor( - style: MenuStyle( - maximumSize: WidgetStatePropertyAll( - Size.fromHeight( - MediaQuery.sizeOf(context).height * 0.7, - ), - ), + return TextButton( + onPressed: () => showMultipleChoiceFilter( + context, + filterName: filterName, + choices: choices, + selectedItems: selectedItems, + choiceLabel: choiceLabel, + onChanged: onChanged, ), - onClose: () => widget.onChanged(selectedItems), - menuChildren: widget.choices - .map( - (choice) => FilterChip( - label: Text(widget.choiceLabel(choice)), - selected: selectedItems.contains(choice), - onSelected: (value) { - setState(() { - selectedItems = value - ? selectedItems.add(choice) - : selectedItems.remove(choice); - }); - }, - ), - ) - .toList(growable: false), - builder: ( - BuildContext context, - MenuController controller, - Widget? child, - ) => - TextButton( - onPressed: () => - controller.isOpen ? controller.close() : controller.open(), - style: TextButton.styleFrom( - backgroundColor: selectedItems.isEmpty - ? Theme.of(context).colorScheme.secondary - : Theme.of(context).colorScheme.primary, - foregroundColor: selectedItems.isEmpty - ? Theme.of(context).colorScheme.onSecondary - : Theme.of(context).colorScheme.onPrimary, - ), - child: Row( - children: [ - if (selectedItems.length > 1) - Container( - padding: const EdgeInsets.all(4), - margin: const EdgeInsets.only(right: 5), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.onPrimary, - shape: BoxShape.circle, - ), - child: Text( - '${selectedItems.length}', - textAlign: TextAlign.center, - style: - TextStyle(color: Theme.of(context).colorScheme.primary), - ), + style: TextButton.styleFrom( + backgroundColor: selectedItems.isEmpty + ? Theme.of(context).colorScheme.secondary + : Theme.of(context).colorScheme.primary, + foregroundColor: selectedItems.isEmpty + ? Theme.of(context).colorScheme.onSecondary + : Theme.of(context).colorScheme.onPrimary, + ), + child: Row( + children: [ + if (selectedItems.length > 1) + Container( + padding: const EdgeInsets.all(4), + margin: const EdgeInsets.only(right: 5), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.onPrimary, + shape: BoxShape.circle, + ), + child: Text( + '${selectedItems.length}', + textAlign: TextAlign.center, + style: TextStyle(color: Theme.of(context).colorScheme.primary), ), - Text( - selectedItems.length == 1 - ? widget.choiceLabel(selectedItems.first) - : widget.filterName, ), - ], - ), + Text( + selectedItems.length == 1 + ? choiceLabel(selectedItems.first) + : filterName, + ), + ], ), ); } } + +Future?> showMultipleChoiceFilter( + BuildContext context, { + required String filterName, + required Iterable choices, + required ISet selectedItems, + required String Function(T choice) choiceLabel, + required void Function(ISet value) onChanged, +}) { + return showAdaptiveDialog>( + context: context, + builder: (context) { + ISet items = selectedItems; + return AlertDialog.adaptive( + contentPadding: const EdgeInsets.only(top: 12), + scrollable: true, + content: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + final size = MediaQuery.sizeOf(context); + return SizedBox( + width: size.width, + height: size.height / 2, + child: GridView.count( + crossAxisCount: 2, + children: choices + .map( + (choice) => FilterChip( + label: Text(choiceLabel(choice)), + selected: items.contains(choice), + onSelected: (value) { + setState(() { + items = value + ? items.add(choice) + : items.remove(choice); + }); + }, + ), + ) + .toList(growable: false), + ), + ); + }, + ), + actions: Theme.of(context).platform == TargetPlatform.iOS + ? [ + CupertinoDialogAction( + onPressed: () => Navigator.of(context).pop(), + child: Text(context.l10n.cancel), + ), + CupertinoDialogAction( + isDefaultAction: true, + child: Text(context.l10n.mobileOkButton), + onPressed: () { + onChanged(items); + Navigator.of(context).pop(items); + }, + ), + ] + : [ + TextButton( + child: Text(context.l10n.cancel), + onPressed: () => Navigator.of(context).pop(), + ), + TextButton( + child: Text(context.l10n.mobileOkButton), + onPressed: () { + onChanged(items); + Navigator.of(context).pop(items); + }, + ), + ], + ); + }, + ); +} From 10b9f5631756b01478d1c2a42128007f0284d70d Mon Sep 17 00:00:00 2001 From: Mauritz Date: Thu, 25 Jul 2024 17:34:06 +0200 Subject: [PATCH 034/979] feat: calculate height of dialog --- lib/src/view/user/game_history_screen.dart | 42 +++++++++++++--------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/lib/src/view/user/game_history_screen.dart b/lib/src/view/user/game_history_screen.dart index dc6e9a76a2..7cc684e938 100644 --- a/lib/src/view/user/game_history_screen.dart +++ b/lib/src/view/user/game_history_screen.dart @@ -11,7 +11,6 @@ import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/view/game/game_list_tile.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; -import 'package:material_color_utilities/utils/math_utils.dart'; class GameHistoryScreen extends ConsumerWidget { const GameHistoryScreen({ @@ -248,8 +247,11 @@ class _MultipleChoiceFilter extends StatelessWidget { choices: choices, selectedItems: selectedItems, choiceLabel: choiceLabel, - onChanged: onChanged, - ), + ).then((value) { + if (value != null) { + onChanged(value); + } + }), style: TextButton.styleFrom( backgroundColor: selectedItems.isEmpty ? Theme.of(context).colorScheme.secondary @@ -291,23 +293,35 @@ Future?> showMultipleChoiceFilter( required Iterable choices, required ISet selectedItems, required String Function(T choice) choiceLabel, - required void Function(ISet value) onChanged, }) { return showAdaptiveDialog>( context: context, builder: (context) { ISet items = selectedItems; + const paddingTop = 12.0; return AlertDialog.adaptive( - contentPadding: const EdgeInsets.only(top: 12), + contentPadding: const EdgeInsets.only(top: paddingTop), scrollable: true, content: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { - final size = MediaQuery.sizeOf(context); + final width = MediaQuery.sizeOf(context).width; + const aspectRatio = 4.0; + const itemsPerRow = 2; + const verticalSpacing = 8.0; + const horizontalSpacing = 8.0; + + final chipWidth = width / itemsPerRow - horizontalSpacing; + final chipHeight = chipWidth / aspectRatio + verticalSpacing; + final rows = (choices.length / itemsPerRow).ceil(); + final height = chipHeight * rows - verticalSpacing - paddingTop; return SizedBox( - width: size.width, - height: size.height / 2, + width: width, + height: height, child: GridView.count( - crossAxisCount: 2, + childAspectRatio: aspectRatio, + crossAxisCount: itemsPerRow, + mainAxisSpacing: verticalSpacing, + crossAxisSpacing: horizontalSpacing, children: choices .map( (choice) => FilterChip( @@ -336,10 +350,7 @@ Future?> showMultipleChoiceFilter( CupertinoDialogAction( isDefaultAction: true, child: Text(context.l10n.mobileOkButton), - onPressed: () { - onChanged(items); - Navigator.of(context).pop(items); - }, + onPressed: () => Navigator.of(context).pop(items), ), ] : [ @@ -349,10 +360,7 @@ Future?> showMultipleChoiceFilter( ), TextButton( child: Text(context.l10n.mobileOkButton), - onPressed: () { - onChanged(items); - Navigator.of(context).pop(items); - }, + onPressed: () => Navigator.of(context).pop(items), ), ], ); From faf03618cc9b404de1a19e02150b431050462d37 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Thu, 25 Jul 2024 18:19:49 +0200 Subject: [PATCH 035/979] feat: change layout on tablet --- lib/src/view/user/game_history_screen.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/view/user/game_history_screen.dart b/lib/src/view/user/game_history_screen.dart index 7cc684e938..64ece0beaf 100644 --- a/lib/src/view/user/game_history_screen.dart +++ b/lib/src/view/user/game_history_screen.dart @@ -8,6 +8,7 @@ import 'package:lichess_mobile/src/model/game/game_history.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/view/game/game_list_tile.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; @@ -306,7 +307,7 @@ Future?> showMultipleChoiceFilter( builder: (BuildContext context, StateSetter setState) { final width = MediaQuery.sizeOf(context).width; const aspectRatio = 4.0; - const itemsPerRow = 2; + final itemsPerRow = isTabletOrLarger(context) ? 4 : 2; const verticalSpacing = 8.0; const horizontalSpacing = 8.0; From e2f97f3d8c8c05fce9b519340c9b34bceced444a Mon Sep 17 00:00:00 2001 From: Mauritz Date: Fri, 26 Jul 2024 05:49:46 +0200 Subject: [PATCH 036/979] feat: change layout use wrap instead of gridview --- lib/src/view/user/game_history_screen.dart | 54 ++++++++-------------- 1 file changed, 18 insertions(+), 36 deletions(-) diff --git a/lib/src/view/user/game_history_screen.dart b/lib/src/view/user/game_history_screen.dart index 64ece0beaf..8aceec9839 100644 --- a/lib/src/view/user/game_history_screen.dart +++ b/lib/src/view/user/game_history_screen.dart @@ -299,46 +299,28 @@ Future?> showMultipleChoiceFilter( context: context, builder: (context) { ISet items = selectedItems; - const paddingTop = 12.0; return AlertDialog.adaptive( - contentPadding: const EdgeInsets.only(top: paddingTop), + contentPadding: const EdgeInsets.all(16.0), scrollable: true, content: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { - final width = MediaQuery.sizeOf(context).width; - const aspectRatio = 4.0; - final itemsPerRow = isTabletOrLarger(context) ? 4 : 2; - const verticalSpacing = 8.0; - const horizontalSpacing = 8.0; - - final chipWidth = width / itemsPerRow - horizontalSpacing; - final chipHeight = chipWidth / aspectRatio + verticalSpacing; - final rows = (choices.length / itemsPerRow).ceil(); - final height = chipHeight * rows - verticalSpacing - paddingTop; - return SizedBox( - width: width, - height: height, - child: GridView.count( - childAspectRatio: aspectRatio, - crossAxisCount: itemsPerRow, - mainAxisSpacing: verticalSpacing, - crossAxisSpacing: horizontalSpacing, - children: choices - .map( - (choice) => FilterChip( - label: Text(choiceLabel(choice)), - selected: items.contains(choice), - onSelected: (value) { - setState(() { - items = value - ? items.add(choice) - : items.remove(choice); - }); - }, - ), - ) - .toList(growable: false), - ), + return Wrap( + spacing: 8.0, + runSpacing: 8.0, + children: choices + .map( + (choice) => FilterChip( + label: Text(choiceLabel(choice)), + selected: items.contains(choice), + onSelected: (value) { + setState(() { + items = + value ? items.add(choice) : items.remove(choice); + }); + }, + ), + ) + .toList(growable: false), ); }, ), From b98f4a6568304b93e763c432372ef0fe7a3a7495 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Fri, 26 Jul 2024 05:52:47 +0200 Subject: [PATCH 037/979] fix: shorter filter name --- lib/src/view/user/game_history_screen.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/src/view/user/game_history_screen.dart b/lib/src/view/user/game_history_screen.dart index 8aceec9839..84cf917e2d 100644 --- a/lib/src/view/user/game_history_screen.dart +++ b/lib/src/view/user/game_history_screen.dart @@ -137,8 +137,7 @@ class _BodyState extends ConsumerState<_Body> { child: Row( children: [ _MultipleChoiceFilter( - filterName: - '${context.l10n.timeControl} / ${context.l10n.variant}', + filterName: context.l10n.variant, choices: const [ Perf.ultraBullet, Perf.bullet, From 0af623b54bee52232b0a435bcae322bd54ea12f0 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Fri, 26 Jul 2024 06:34:29 +0200 Subject: [PATCH 038/979] feat: move filter to app bar --- lib/src/view/user/game_history_screen.dart | 119 +++++++++++---------- 1 file changed, 64 insertions(+), 55 deletions(-) diff --git a/lib/src/view/user/game_history_screen.dart b/lib/src/view/user/game_history_screen.dart index 84cf917e2d..310bdbe657 100644 --- a/lib/src/view/user/game_history_screen.dart +++ b/lib/src/view/user/game_history_screen.dart @@ -6,12 +6,9 @@ import 'package:lichess_mobile/src/model/common/perf.dart'; import 'package:lichess_mobile/src/model/game/game_filter.dart'; import 'package:lichess_mobile/src/model/game/game_history.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; -import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; -import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/view/game/game_list_tile.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; class GameHistoryScreen extends ConsumerWidget { const GameHistoryScreen({ @@ -26,23 +23,75 @@ class GameHistoryScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return ConsumerPlatformWidget( - ref: ref, - androidBuilder: _buildAndroid, - iosBuilder: _buildIos, - ); + final gameFilterState = + ref.watch(gameFilterProvider(perfs: gameFilters.perfs)); + + final perfFilter = _MultipleChoiceFilter( + filterName: context.l10n.variant, + choices: const [ + Perf.ultraBullet, + Perf.bullet, + Perf.blitz, + Perf.rapid, + Perf.classical, + Perf.correspondence, + Perf.chess960, + Perf.antichess, + Perf.kingOfTheHill, + Perf.threeCheck, + Perf.atomic, + Perf.horde, + Perf.racingKings, + Perf.crazyhouse, + ], + selectedItems: gameFilterState.perfs, + choiceLabel: (t) => t.title, + onChanged: (value) => ref + .read( + gameFilterProvider( + perfs: gameFilters.perfs, + ).notifier, + ) + .setPerfs(value), + ); + + switch (Theme.of(context).platform) { + case TargetPlatform.android: + return _buildAndroid(context, ref, perfFilter: perfFilter); + case TargetPlatform.iOS: + return _buildIos(context, ref, perfFilter: perfFilter); + default: + assert(false, 'Unexpected platform ${Theme.of(context).platform}'); + return const SizedBox.shrink(); + } } - Widget _buildIos(BuildContext context, WidgetRef ref) { + Widget _buildIos(BuildContext context, WidgetRef ref, { + required Widget perfFilter, + }) { return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar(middle: Text(context.l10n.games)), + navigationBar: CupertinoNavigationBar( + middle: Row( + children: [ + perfFilter, + ], + ), + ), child: _Body(user: user, isOnline: isOnline, gameFilters: gameFilters), ); } - Widget _buildAndroid(BuildContext context, WidgetRef ref) { + Widget _buildAndroid(BuildContext context, WidgetRef ref, { + required Widget perfFilter, + }) { return Scaffold( - appBar: AppBar(title: Text(context.l10n.games)), + appBar: AppBar( + title: Row( + children: [ + perfFilter, + ], + ), + ), body: _Body(user: user, isOnline: isOnline, gameFilters: gameFilters), ); } @@ -130,45 +179,8 @@ class _BodyState extends ConsumerState<_Body> { final list = state.gameList; return SafeArea( - child: Column( - children: [ - Container( - padding: Styles.bodyPadding, - child: Row( - children: [ - _MultipleChoiceFilter( - filterName: context.l10n.variant, - choices: const [ - Perf.ultraBullet, - Perf.bullet, - Perf.blitz, - Perf.rapid, - Perf.classical, - Perf.correspondence, - Perf.chess960, - Perf.antichess, - Perf.kingOfTheHill, - Perf.threeCheck, - Perf.atomic, - Perf.horde, - Perf.racingKings, - Perf.crazyhouse, - ], - selectedItems: gameFilterState.perfs, - choiceLabel: (t) => t.title, - onChanged: (value) => ref - .read( - gameFilterProvider( - perfs: widget.gameFilters.perfs, - ).notifier, - ) - .setPerfs(value), - ), - ], - ), - ), - if (list.isEmpty) - const Padding( + child: list.isEmpty + ? const Padding( padding: EdgeInsets.symmetric(vertical: 32.0), child: Center( child: Text( @@ -176,8 +188,7 @@ class _BodyState extends ConsumerState<_Body> { ), ), ) - else - Expanded( + : Expanded( child: ListView.builder( controller: _scrollController, itemCount: list.length + (state.isLoading ? 1 : 0), @@ -208,8 +219,6 @@ class _BodyState extends ConsumerState<_Body> { }, ), ), - ], - ), ); }, error: (e, s) { From 7e99865cec3763c85853f13d29c99213e1c55f64 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Fri, 26 Jul 2024 07:43:01 +0200 Subject: [PATCH 039/979] feat: show line separator between groups --- lib/src/view/user/game_history_screen.dart | 115 ++++++++++++--------- 1 file changed, 69 insertions(+), 46 deletions(-) diff --git a/lib/src/view/user/game_history_screen.dart b/lib/src/view/user/game_history_screen.dart index 310bdbe657..ce965bcf1a 100644 --- a/lib/src/view/user/game_history_screen.dart +++ b/lib/src/view/user/game_history_screen.dart @@ -27,33 +27,37 @@ class GameHistoryScreen extends ConsumerWidget { ref.watch(gameFilterProvider(perfs: gameFilters.perfs)); final perfFilter = _MultipleChoiceFilter( - filterName: context.l10n.variant, - choices: const [ - Perf.ultraBullet, - Perf.bullet, - Perf.blitz, - Perf.rapid, - Perf.classical, - Perf.correspondence, - Perf.chess960, - Perf.antichess, - Perf.kingOfTheHill, - Perf.threeCheck, - Perf.atomic, - Perf.horde, - Perf.racingKings, - Perf.crazyhouse, - ], - selectedItems: gameFilterState.perfs, - choiceLabel: (t) => t.title, - onChanged: (value) => ref - .read( - gameFilterProvider( - perfs: gameFilters.perfs, - ).notifier, - ) - .setPerfs(value), - ); + filterName: context.l10n.variant, + choicesGroups: const [ + [ + Perf.ultraBullet, + Perf.bullet, + Perf.blitz, + Perf.rapid, + Perf.classical, + Perf.correspondence, + ], + [ + Perf.chess960, + Perf.antichess, + Perf.kingOfTheHill, + Perf.threeCheck, + Perf.atomic, + Perf.horde, + Perf.racingKings, + Perf.crazyhouse, + ], + ], + selectedItems: gameFilterState.perfs, + choiceLabel: (t) => t.title, + onChanged: (value) => ref + .read( + gameFilterProvider( + perfs: gameFilters.perfs, + ).notifier, + ) + .setPerfs(value), + ); switch (Theme.of(context).platform) { case TargetPlatform.android: @@ -66,7 +70,9 @@ class GameHistoryScreen extends ConsumerWidget { } } - Widget _buildIos(BuildContext context, WidgetRef ref, { + Widget _buildIos( + BuildContext context, + WidgetRef ref, { required Widget perfFilter, }) { return CupertinoPageScaffold( @@ -81,7 +87,9 @@ class GameHistoryScreen extends ConsumerWidget { ); } - Widget _buildAndroid(BuildContext context, WidgetRef ref, { + Widget _buildAndroid( + BuildContext context, + WidgetRef ref, { required Widget perfFilter, }) { return Scaffold( @@ -235,14 +243,14 @@ class _BodyState extends ConsumerState<_Body> { class _MultipleChoiceFilter extends StatelessWidget { const _MultipleChoiceFilter({ required this.filterName, - required this.choices, + required this.choicesGroups, required this.selectedItems, required this.choiceLabel, required this.onChanged, }); final String filterName; - final Iterable choices; + final Iterable> choicesGroups; final ISet selectedItems; final String Function(T choice) choiceLabel; final void Function(ISet value) onChanged; @@ -253,7 +261,7 @@ class _MultipleChoiceFilter extends StatelessWidget { onPressed: () => showMultipleChoiceFilter( context, filterName: filterName, - choices: choices, + choicesGroups: choicesGroups, selectedItems: selectedItems, choiceLabel: choiceLabel, ).then((value) { @@ -299,7 +307,7 @@ class _MultipleChoiceFilter extends StatelessWidget { Future?> showMultipleChoiceFilter( BuildContext context, { required String filterName, - required Iterable choices, + required Iterable> choicesGroups, required ISet selectedItems, required String Function(T choice) choiceLabel, }) { @@ -312,20 +320,35 @@ Future?> showMultipleChoiceFilter( scrollable: true, content: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { - return Wrap( - spacing: 8.0, - runSpacing: 8.0, - children: choices + return Column( + children: choicesGroups .map( - (choice) => FilterChip( - label: Text(choiceLabel(choice)), - selected: items.contains(choice), - onSelected: (value) { - setState(() { - items = - value ? items.add(choice) : items.remove(choice); - }); - }, + (group) => Column( + children: [ + SizedBox( + width: double.infinity, + child: Wrap( + spacing: 8.0, + runSpacing: 8.0, + children: group + .map( + (choice) => FilterChip( + label: Text(choiceLabel(choice)), + selected: items.contains(choice), + onSelected: (value) { + setState(() { + items = value + ? items.add(choice) + : items.remove(choice); + }); + }, + ), + ) + .toList(growable: false), + ), + ), + if (choicesGroups.isNotLast(group)) const Divider(), + ], ), ) .toList(growable: false), From b296a522ff407997de1445e37f3b5f3792264f41 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Sat, 27 Jul 2024 12:35:55 +0200 Subject: [PATCH 040/979] feat: btn opens all filters in a bottom sheet --- lib/src/model/game/game_filter.dart | 4 +- lib/src/view/user/game_history_screen.dart | 338 +++++++++------------ 2 files changed, 154 insertions(+), 188 deletions(-) diff --git a/lib/src/model/game/game_filter.dart b/lib/src/model/game/game_filter.dart index 7eff5f6f52..d6b3da3fd5 100644 --- a/lib/src/model/game/game_filter.dart +++ b/lib/src/model/game/game_filter.dart @@ -13,7 +13,9 @@ class GameFilter extends _$GameFilter { return GameFilterState(perfs: perfs ?? const ISet.empty()); } - void setPerfs(ISet perfs) => state = state.copyWith(perfs: perfs); + void setFilter(GameFilterState filter) => state = state.copyWith( + perfs: filter.perfs, + ); } @freezed diff --git a/lib/src/view/user/game_history_screen.dart b/lib/src/view/user/game_history_screen.dart index ce965bcf1a..a360a3cd53 100644 --- a/lib/src/view/user/game_history_screen.dart +++ b/lib/src/view/user/game_history_screen.dart @@ -8,7 +8,9 @@ import 'package:lichess_mobile/src/model/game/game_history.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/view/game/game_list_tile.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; +import 'package:lichess_mobile/src/widgets/platform.dart'; class GameHistoryScreen extends ConsumerWidget { const GameHistoryScreen({ @@ -23,82 +25,68 @@ class GameHistoryScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final gameFilterState = - ref.watch(gameFilterProvider(perfs: gameFilters.perfs)); - - final perfFilter = _MultipleChoiceFilter( - filterName: context.l10n.variant, - choicesGroups: const [ - [ - Perf.ultraBullet, - Perf.bullet, - Perf.blitz, - Perf.rapid, - Perf.classical, - Perf.correspondence, - ], - [ - Perf.chess960, - Perf.antichess, - Perf.kingOfTheHill, - Perf.threeCheck, - Perf.atomic, - Perf.horde, - Perf.racingKings, - Perf.crazyhouse, - ], - ], - selectedItems: gameFilterState.perfs, - choiceLabel: (t) => t.title, - onChanged: (value) => ref - .read( - gameFilterProvider( - perfs: gameFilters.perfs, - ).notifier, - ) - .setPerfs(value), + return ConsumerPlatformWidget( + ref: ref, + androidBuilder: _buildAndroid, + iosBuilder: _buildIos, ); - - switch (Theme.of(context).platform) { - case TargetPlatform.android: - return _buildAndroid(context, ref, perfFilter: perfFilter); - case TargetPlatform.iOS: - return _buildIos(context, ref, perfFilter: perfFilter); - default: - assert(false, 'Unexpected platform ${Theme.of(context).platform}'); - return const SizedBox.shrink(); - } } - Widget _buildIos( - BuildContext context, - WidgetRef ref, { - required Widget perfFilter, - }) { + Widget _buildIos(BuildContext context, WidgetRef ref) { return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( - middle: Row( - children: [ - perfFilter, - ], + middle: Text(context.l10n.games), + trailing: IconButton( + icon: const Icon(Icons.tune), + tooltip: context.l10n.filterGames, + onPressed: () => showAdaptiveBottomSheet( + context: context, + builder: (_) => _FilterGames( + filter: ref.read( + gameFilterProvider(perfs: gameFilters.perfs), + ), + ), + ).then((value) { + if (value != null) { + ref + .read( + gameFilterProvider(perfs: gameFilters.perfs).notifier, + ) + .setFilter(value); + } + }), ), ), child: _Body(user: user, isOnline: isOnline, gameFilters: gameFilters), ); } - Widget _buildAndroid( - BuildContext context, - WidgetRef ref, { - required Widget perfFilter, - }) { + Widget _buildAndroid(BuildContext context, WidgetRef ref) { return Scaffold( appBar: AppBar( - title: Row( - children: [ - perfFilter, - ], - ), + title: Text(context.l10n.games), + actions: [ + IconButton( + icon: const Icon(Icons.tune), + tooltip: context.l10n.filterGames, + onPressed: () => showAdaptiveBottomSheet( + context: context, + builder: (_) => _FilterGames( + filter: ref.read( + gameFilterProvider(perfs: gameFilters.perfs), + ), + ), + ).then((value) { + if (value != null) { + ref + .read( + gameFilterProvider(perfs: gameFilters.perfs).notifier, + ) + .setFilter(value); + } + }), + ), + ], ), body: _Body(user: user, isOnline: isOnline, gameFilters: gameFilters), ); @@ -240,63 +228,75 @@ class _BodyState extends ConsumerState<_Body> { } } -class _MultipleChoiceFilter extends StatelessWidget { - const _MultipleChoiceFilter({ - required this.filterName, - required this.choicesGroups, - required this.selectedItems, - required this.choiceLabel, - required this.onChanged, +class _FilterGames extends StatefulWidget { + const _FilterGames({ + required this.filter, }); - final String filterName; - final Iterable> choicesGroups; - final ISet selectedItems; - final String Function(T choice) choiceLabel; - final void Function(ISet value) onChanged; + final GameFilterState filter; + + @override + State<_FilterGames> createState() => _FilterGamesState(); +} + +class _FilterGamesState extends State<_FilterGames> { + late GameFilterState filter; + + @override + void initState() { + super.initState(); + filter = widget.filter; + } @override Widget build(BuildContext context) { - return TextButton( - onPressed: () => showMultipleChoiceFilter( - context, - filterName: filterName, - choicesGroups: choicesGroups, - selectedItems: selectedItems, - choiceLabel: choiceLabel, - ).then((value) { - if (value != null) { - onChanged(value); - } - }), - style: TextButton.styleFrom( - backgroundColor: selectedItems.isEmpty - ? Theme.of(context).colorScheme.secondary - : Theme.of(context).colorScheme.primary, - foregroundColor: selectedItems.isEmpty - ? Theme.of(context).colorScheme.onSecondary - : Theme.of(context).colorScheme.onPrimary, - ), - child: Row( + return Container( + padding: const EdgeInsets.all(16), + child: Column( children: [ - if (selectedItems.length > 1) - Container( - padding: const EdgeInsets.all(4), - margin: const EdgeInsets.only(right: 5), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.onPrimary, - shape: BoxShape.circle, + _MultipleChoiceFilter( + filterName: context.l10n.variant, + choices: const [ + Perf.ultraBullet, + Perf.bullet, + Perf.blitz, + Perf.rapid, + Perf.classical, + Perf.correspondence, + Perf.chess960, + Perf.antichess, + Perf.kingOfTheHill, + Perf.threeCheck, + Perf.atomic, + Perf.horde, + Perf.racingKings, + Perf.crazyhouse, + ], + selectedItems: filter.perfs, + choiceLabel: (t) => t.title, + onSelected: (value, selected) => setState( + () { + filter = filter.copyWith( + perfs: selected + ? filter.perfs.add(value) + : filter.perfs.remove(value), + ); + }, + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Cancel'), ), - child: Text( - '${selectedItems.length}', - textAlign: TextAlign.center, - style: TextStyle(color: Theme.of(context).colorScheme.primary), + TextButton( + onPressed: () => Navigator.of(context).pop(filter), + child: const Text('OK'), ), - ), - Text( - selectedItems.length == 1 - ? choiceLabel(selectedItems.first) - : filterName, + ], ), ], ), @@ -304,80 +304,44 @@ class _MultipleChoiceFilter extends StatelessWidget { } } -Future?> showMultipleChoiceFilter( - BuildContext context, { - required String filterName, - required Iterable> choicesGroups, - required ISet selectedItems, - required String Function(T choice) choiceLabel, -}) { - return showAdaptiveDialog>( - context: context, - builder: (context) { - ISet items = selectedItems; - return AlertDialog.adaptive( - contentPadding: const EdgeInsets.all(16.0), - scrollable: true, - content: StatefulBuilder( - builder: (BuildContext context, StateSetter setState) { - return Column( - children: choicesGroups - .map( - (group) => Column( - children: [ - SizedBox( - width: double.infinity, - child: Wrap( - spacing: 8.0, - runSpacing: 8.0, - children: group - .map( - (choice) => FilterChip( - label: Text(choiceLabel(choice)), - selected: items.contains(choice), - onSelected: (value) { - setState(() { - items = value - ? items.add(choice) - : items.remove(choice); - }); - }, - ), - ) - .toList(growable: false), - ), - ), - if (choicesGroups.isNotLast(group)) const Divider(), - ], - ), - ) - .toList(growable: false), - ); - }, +class _MultipleChoiceFilter extends StatelessWidget { + const _MultipleChoiceFilter({ + required this.filterName, + required this.choices, + required this.selectedItems, + required this.choiceLabel, + required this.onSelected, + }); + + final String filterName; + final Iterable choices; + final ISet selectedItems; + final String Function(T choice) choiceLabel; + final void Function(T value, bool selected) onSelected; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(filterName, style: const TextStyle(fontSize: 18)), + SizedBox( + width: double.infinity, + child: Wrap( + spacing: 8.0, + runSpacing: 8.0, + children: choices + .map( + (choice) => FilterChip( + label: Text(choiceLabel(choice)), + selected: selectedItems.contains(choice), + onSelected: (value) => onSelected(choice, value), + ), + ) + .toList(growable: false), + ), ), - actions: Theme.of(context).platform == TargetPlatform.iOS - ? [ - CupertinoDialogAction( - onPressed: () => Navigator.of(context).pop(), - child: Text(context.l10n.cancel), - ), - CupertinoDialogAction( - isDefaultAction: true, - child: Text(context.l10n.mobileOkButton), - onPressed: () => Navigator.of(context).pop(items), - ), - ] - : [ - TextButton( - child: Text(context.l10n.cancel), - onPressed: () => Navigator.of(context).pop(), - ), - TextButton( - child: Text(context.l10n.mobileOkButton), - onPressed: () => Navigator.of(context).pop(items), - ), - ], - ); - }, - ); + ], + ); + } } From f13f007b2e5631843adde0d4c3d627b6b6378cdc Mon Sep 17 00:00:00 2001 From: Mauritz Date: Sat, 27 Jul 2024 12:53:32 +0200 Subject: [PATCH 041/979] fix: remove incorrect expanded widget --- lib/src/view/user/game_history_screen.dart | 54 +++++++++++----------- 1 file changed, 26 insertions(+), 28 deletions(-) diff --git a/lib/src/view/user/game_history_screen.dart b/lib/src/view/user/game_history_screen.dart index a360a3cd53..1570b4e268 100644 --- a/lib/src/view/user/game_history_screen.dart +++ b/lib/src/view/user/game_history_screen.dart @@ -184,36 +184,34 @@ class _BodyState extends ConsumerState<_Body> { ), ), ) - : Expanded( - child: ListView.builder( - controller: _scrollController, - itemCount: list.length + (state.isLoading ? 1 : 0), - itemBuilder: (context, index) { - if (state.isLoading && index == list.length) { - return const Padding( - padding: EdgeInsets.symmetric(vertical: 32.0), - child: CenterLoadingIndicator(), - ); - } else if (state.hasError && - state.hasMore && - index == list.length) { - // TODO: add a retry button - return const Padding( - padding: EdgeInsets.symmetric(vertical: 32.0), - child: Center( - child: Text( - 'Could not load more games', - ), + : ListView.builder( + controller: _scrollController, + itemCount: list.length + (state.isLoading ? 1 : 0), + itemBuilder: (context, index) { + if (state.isLoading && index == list.length) { + return const Padding( + padding: EdgeInsets.symmetric(vertical: 32.0), + child: CenterLoadingIndicator(), + ); + } else if (state.hasError && + state.hasMore && + index == list.length) { + // TODO: add a retry button + return const Padding( + padding: EdgeInsets.symmetric(vertical: 32.0), + child: Center( + child: Text( + 'Could not load more games', ), - ); - } - - return ExtendedGameListTile( - item: list[index], - userId: widget.user?.id, + ), ); - }, - ), + } + + return ExtendedGameListTile( + item: list[index], + userId: widget.user?.id, + ); + }, ), ); }, From 69c4c4d42bd252ed3419e8e67d198a3e413b2a0e Mon Sep 17 00:00:00 2001 From: Mauritz Date: Sat, 27 Jul 2024 14:11:01 +0200 Subject: [PATCH 042/979] feat: add filter for color --- lib/src/model/game/game_filter.dart | 7 +- lib/src/model/game/game_history.dart | 23 +++--- lib/src/model/game/game_repository.dart | 16 ++-- lib/src/view/user/game_history_screen.dart | 94 +++++++++++++--------- lib/src/view/user/perf_stats_screen.dart | 2 +- 5 files changed, 84 insertions(+), 58 deletions(-) diff --git a/lib/src/model/game/game_filter.dart b/lib/src/model/game/game_filter.dart index d6b3da3fd5..d9edbbcb8a 100644 --- a/lib/src/model/game/game_filter.dart +++ b/lib/src/model/game/game_filter.dart @@ -1,3 +1,4 @@ +import 'package:chessground/chessground.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; @@ -9,12 +10,13 @@ part 'game_filter.g.dart'; @riverpod class GameFilter extends _$GameFilter { @override - GameFilterState build({ISet? perfs}) { - return GameFilterState(perfs: perfs ?? const ISet.empty()); + GameFilterState build({GameFilterState? filter}) { + return filter ?? const GameFilterState(); } void setFilter(GameFilterState filter) => state = state.copyWith( perfs: filter.perfs, + side: filter.side, ); } @@ -22,5 +24,6 @@ class GameFilter extends _$GameFilter { class GameFilterState with _$GameFilterState { const factory GameFilterState({ @Default(ISet.empty()) ISet perfs, + Side? side, }) = _GameFilterState; } diff --git a/lib/src/model/game/game_history.dart b/lib/src/model/game/game_history.dart index 44ccb5ebea..c4e0fed031 100644 --- a/lib/src/model/game/game_history.dart +++ b/lib/src/model/game/game_history.dart @@ -34,7 +34,7 @@ const _nbPerPage = 20; @riverpod Future> myRecentGames( MyRecentGamesRef ref, { - GameFilterState filters = const GameFilterState(), + GameFilterState filter = const GameFilterState(), }) async { final online = await ref .watch(connectivityChangesProvider.selectAsync((c) => c.isOnline)); @@ -44,7 +44,7 @@ Future> myRecentGames( (client) => GameRepository(client).getUserGames( session.user.id, max: kNumberOfRecentGames, - perfs: filters.perfs, + filter: filter, ), const Duration(hours: 1), ); @@ -68,11 +68,10 @@ Future> myRecentGames( Future> userRecentGames( UserRecentGamesRef ref, { required UserId userId, - GameFilterState filters = const GameFilterState(), + GameFilterState filter = const GameFilterState(), }) { return ref.withClientCacheFor( - (client) => - GameRepository(client).getUserGames(userId, perfs: filters.perfs), + (client) => GameRepository(client).getUserGames(userId, filter: filter), // cache is important because the associated widget is in a [ListView] and // the provider may be instanciated multiple times in a short period of time // (e.g. when scrolling) @@ -121,7 +120,7 @@ class UserGameHistory extends _$UserGameHistory { /// server. If this is false, the provider will fetch the games from the /// local storage. required bool isOnline, - GameFilterState filters = const GameFilterState(), + GameFilterState filter = const GameFilterState(), }) async { ref.cacheFor(const Duration(minutes: 5)); ref.onDispose(() { @@ -134,10 +133,10 @@ class UserGameHistory extends _$UserGameHistory { ? ref.read( userRecentGamesProvider( userId: userId, - filters: filters, + filter: filter, ).future, ) - : ref.read(myRecentGamesProvider(filters: filters).future); + : ref.read(myRecentGamesProvider(filter: filter).future); _list.addAll(await recentGames); @@ -147,7 +146,7 @@ class UserGameHistory extends _$UserGameHistory { hasMore: true, hasError: false, online: isOnline, - filters: filters, + filter: filter, session: session, ); } @@ -165,7 +164,7 @@ class UserGameHistory extends _$UserGameHistory { userId!, max: _nbPerPage, until: _list.last.game.createdAt, - perfs: currentVal.filters.perfs, + filter: currentVal.filter, ), ) : currentVal.online && currentVal.session != null @@ -174,7 +173,7 @@ class UserGameHistory extends _$UserGameHistory { currentVal.session!.user.id, max: _nbPerPage, until: _list.last.game.createdAt, - perfs: currentVal.filters.perfs, + filter: currentVal.filter, ), ) : ref @@ -224,7 +223,7 @@ class UserGameHistoryState with _$UserGameHistoryState { const factory UserGameHistoryState({ required IList gameList, required bool isLoading, - required GameFilterState filters, + required GameFilterState filter, required bool hasMore, required bool hasError, required bool online, diff --git a/lib/src/model/game/game_repository.dart b/lib/src/model/game/game_repository.dart index d4bfd63d65..7d342ef6c6 100644 --- a/lib/src/model/game/game_repository.dart +++ b/lib/src/model/game/game_repository.dart @@ -5,6 +5,7 @@ import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; import 'package:lichess_mobile/src/model/game/archived_game.dart'; +import 'package:lichess_mobile/src/model/game/game_filter.dart'; import 'package:lichess_mobile/src/model/game/playable_game.dart'; class GameRepository { @@ -40,12 +41,12 @@ class GameRepository { UserId userId, { int max = 20, DateTime? until, - ISet perfs = const ISet.empty(), + GameFilterState filter = const GameFilterState(), }) { - assert(!perfs.contains(Perf.fromPosition)); - assert(!perfs.contains(Perf.puzzle)); - assert(!perfs.contains(Perf.storm)); - assert(!perfs.contains(Perf.streak)); + assert(!filter.perfs.contains(Perf.fromPosition)); + assert(!filter.perfs.contains(Perf.puzzle)); + assert(!filter.perfs.contains(Perf.storm)); + assert(!filter.perfs.contains(Perf.streak)); return client .readNdJsonList( Uri( @@ -54,12 +55,13 @@ class GameRepository { 'max': max.toString(), if (until != null) 'until': until.millisecondsSinceEpoch.toString(), - if (perfs.isNotEmpty) - 'perfType': perfs.map((perf) => perf.name).join(','), 'moves': 'false', 'lastFen': 'true', 'accuracy': 'true', 'opening': 'true', + if (filter.perfs.isNotEmpty) + 'perfType': filter.perfs.map((perf) => perf.name).join(','), + if (filter.side != null) 'color': filter.side!.name, }, ), headers: {'Accept': 'application/x-ndjson'}, diff --git a/lib/src/view/user/game_history_screen.dart b/lib/src/view/user/game_history_screen.dart index 1570b4e268..860bb15c63 100644 --- a/lib/src/view/user/game_history_screen.dart +++ b/lib/src/view/user/game_history_screen.dart @@ -1,4 +1,4 @@ -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:chessground/chessground.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -16,12 +16,12 @@ class GameHistoryScreen extends ConsumerWidget { const GameHistoryScreen({ required this.user, required this.isOnline, - this.gameFilters = const GameFilterState(), + this.gameFilter = const GameFilterState(), super.key, }); final LightUser? user; final bool isOnline; - final GameFilterState gameFilters; + final GameFilterState gameFilter; @override Widget build(BuildContext context, WidgetRef ref) { @@ -42,22 +42,18 @@ class GameHistoryScreen extends ConsumerWidget { onPressed: () => showAdaptiveBottomSheet( context: context, builder: (_) => _FilterGames( - filter: ref.read( - gameFilterProvider(perfs: gameFilters.perfs), - ), + filter: ref.read(gameFilterProvider(filter: gameFilter)), ), ).then((value) { if (value != null) { ref - .read( - gameFilterProvider(perfs: gameFilters.perfs).notifier, - ) + .read(gameFilterProvider(filter: gameFilter).notifier) .setFilter(value); } }), ), ), - child: _Body(user: user, isOnline: isOnline, gameFilters: gameFilters), + child: _Body(user: user, isOnline: isOnline, gameFilter: gameFilter), ); } @@ -72,23 +68,19 @@ class GameHistoryScreen extends ConsumerWidget { onPressed: () => showAdaptiveBottomSheet( context: context, builder: (_) => _FilterGames( - filter: ref.read( - gameFilterProvider(perfs: gameFilters.perfs), - ), + filter: ref.read(gameFilterProvider(filter: gameFilter)), ), ).then((value) { if (value != null) { ref - .read( - gameFilterProvider(perfs: gameFilters.perfs).notifier, - ) + .read(gameFilterProvider(filter: gameFilter).notifier) .setFilter(value); } }), ), ], ), - body: _Body(user: user, isOnline: isOnline, gameFilters: gameFilters), + body: _Body(user: user, isOnline: isOnline, gameFilter: gameFilter), ); } } @@ -97,12 +89,12 @@ class _Body extends ConsumerStatefulWidget { const _Body({ required this.user, required this.isOnline, - required this.gameFilters, + required this.gameFilter, }); final LightUser? user; final bool isOnline; - final GameFilterState gameFilters; + final GameFilterState gameFilter; @override ConsumerState<_Body> createState() => _BodyState(); @@ -131,8 +123,7 @@ class _BodyState extends ConsumerState<_Body> { userGameHistoryProvider( widget.user?.id, isOnline: widget.isOnline, - filters: - ref.read(gameFilterProvider(perfs: widget.gameFilters.perfs)), + filter: ref.read(gameFilterProvider(filter: widget.gameFilter)), ), ); @@ -149,8 +140,7 @@ class _BodyState extends ConsumerState<_Body> { userGameHistoryProvider( widget.user?.id, isOnline: widget.isOnline, - filters: ref - .read(gameFilterProvider(perfs: widget.gameFilters.perfs)), + filter: ref.read(gameFilterProvider(filter: widget.gameFilter)), ).notifier, ) .getNext(); @@ -161,12 +151,12 @@ class _BodyState extends ConsumerState<_Body> { @override Widget build(BuildContext context) { final gameFilterState = - ref.watch(gameFilterProvider(perfs: widget.gameFilters.perfs)); + ref.watch(gameFilterProvider(filter: widget.gameFilter)); final gameListState = ref.watch( userGameHistoryProvider( widget.user?.id, isOnline: widget.isOnline, - filters: gameFilterState, + filter: gameFilterState, ), ); @@ -252,8 +242,9 @@ class _FilterGamesState extends State<_FilterGames> { padding: const EdgeInsets.all(16), child: Column( children: [ - _MultipleChoiceFilter( + _Filter( filterName: context.l10n.variant, + filterType: FilterType.multipleChoice, choices: const [ Perf.ultraBullet, Perf.bullet, @@ -270,7 +261,7 @@ class _FilterGamesState extends State<_FilterGames> { Perf.racingKings, Perf.crazyhouse, ], - selectedItems: filter.perfs, + choiceSelected: (choice) => filter.perfs.contains(choice), choiceLabel: (t) => t.title, onSelected: (value, selected) => setState( () { @@ -282,6 +273,23 @@ class _FilterGamesState extends State<_FilterGames> { }, ), ), + const Divider(), + const SizedBox(height: 10.0), + _Filter( + filterName: context.l10n.side, + filterType: FilterType.singleChoice, + choices: Side.values, + choiceSelected: (choice) => filter.side == choice, + choiceLabel: (t) => switch (t) { + Side.white => context.l10n.white, + Side.black => context.l10n.black, + }, + onSelected: (value, selected) => setState( + () { + filter = filter.copyWith(side: selected ? value : null); + }, + ), + ), Row( mainAxisAlignment: MainAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end, @@ -302,18 +310,25 @@ class _FilterGamesState extends State<_FilterGames> { } } -class _MultipleChoiceFilter extends StatelessWidget { - const _MultipleChoiceFilter({ +enum FilterType { + singleChoice, + multipleChoice, +} + +class _Filter extends StatelessWidget { + const _Filter({ required this.filterName, + required this.filterType, required this.choices, - required this.selectedItems, + required this.choiceSelected, required this.choiceLabel, required this.onSelected, }); final String filterName; + final FilterType filterType; final Iterable choices; - final ISet selectedItems; + final bool Function(T choice) choiceSelected; final String Function(T choice) choiceLabel; final void Function(T value, bool selected) onSelected; @@ -330,11 +345,18 @@ class _MultipleChoiceFilter extends StatelessWidget { runSpacing: 8.0, children: choices .map( - (choice) => FilterChip( - label: Text(choiceLabel(choice)), - selected: selectedItems.contains(choice), - onSelected: (value) => onSelected(choice, value), - ), + (choice) => switch (filterType) { + FilterType.singleChoice => ChoiceChip( + label: Text(choiceLabel(choice)), + selected: choiceSelected(choice), + onSelected: (value) => onSelected(choice, value), + ), + FilterType.multipleChoice => FilterChip( + label: Text(choiceLabel(choice)), + selected: choiceSelected(choice), + onSelected: (value) => onSelected(choice, value), + ), + }, ) .toList(growable: false), ), diff --git a/lib/src/view/user/perf_stats_screen.dart b/lib/src/view/user/perf_stats_screen.dart index 2fd6e4b6e4..8b447d669c 100644 --- a/lib/src/view/user/perf_stats_screen.dart +++ b/lib/src/view/user/perf_stats_screen.dart @@ -283,7 +283,7 @@ class _Body extends ConsumerWidget { builder: (context) => GameHistoryScreen( user: user.lightUser, isOnline: true, - gameFilters: GameFilterState(perfs: ISet({perf})), + gameFilter: GameFilterState(perfs: ISet({perf})), ), ); }, From e428e01f2f7dd5bbbaa9e25074e206cfd59c1612 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Sat, 27 Jul 2024 14:40:23 +0200 Subject: [PATCH 043/979] feat: make bottom sheet scrollable --- lib/src/view/user/game_history_screen.dart | 135 +++++++++++---------- 1 file changed, 73 insertions(+), 62 deletions(-) diff --git a/lib/src/view/user/game_history_screen.dart b/lib/src/view/user/game_history_screen.dart index 860bb15c63..0bae8698a0 100644 --- a/lib/src/view/user/game_history_screen.dart +++ b/lib/src/view/user/game_history_screen.dart @@ -41,6 +41,8 @@ class GameHistoryScreen extends ConsumerWidget { tooltip: context.l10n.filterGames, onPressed: () => showAdaptiveBottomSheet( context: context, + isScrollControlled: true, + showDragHandle: true, builder: (_) => _FilterGames( filter: ref.read(gameFilterProvider(filter: gameFilter)), ), @@ -67,6 +69,8 @@ class GameHistoryScreen extends ConsumerWidget { tooltip: context.l10n.filterGames, onPressed: () => showAdaptiveBottomSheet( context: context, + isScrollControlled: true, + showDragHandle: true, builder: (_) => _FilterGames( filter: ref.read(gameFilterProvider(filter: gameFilter)), ), @@ -240,71 +244,78 @@ class _FilterGamesState extends State<_FilterGames> { Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(16), - child: Column( - children: [ - _Filter( - filterName: context.l10n.variant, - filterType: FilterType.multipleChoice, - choices: const [ - Perf.ultraBullet, - Perf.bullet, - Perf.blitz, - Perf.rapid, - Perf.classical, - Perf.correspondence, - Perf.chess960, - Perf.antichess, - Perf.kingOfTheHill, - Perf.threeCheck, - Perf.atomic, - Perf.horde, - Perf.racingKings, - Perf.crazyhouse, - ], - choiceSelected: (choice) => filter.perfs.contains(choice), - choiceLabel: (t) => t.title, - onSelected: (value, selected) => setState( - () { - filter = filter.copyWith( - perfs: selected - ? filter.perfs.add(value) - : filter.perfs.remove(value), - ); - }, + child: DraggableScrollableSheet( + initialChildSize: .7, + expand: false, + snap: true, + snapSizes: const [.7], + builder: (context, scrollController) => ListView( + controller: scrollController, + children: [ + _Filter( + filterName: context.l10n.variant, + filterType: FilterType.multipleChoice, + choices: const [ + Perf.ultraBullet, + Perf.bullet, + Perf.blitz, + Perf.rapid, + Perf.classical, + Perf.correspondence, + Perf.chess960, + Perf.antichess, + Perf.kingOfTheHill, + Perf.threeCheck, + Perf.atomic, + Perf.horde, + Perf.racingKings, + Perf.crazyhouse, + ], + choiceSelected: (choice) => filter.perfs.contains(choice), + choiceLabel: (t) => t.title, + onSelected: (value, selected) => setState( + () { + filter = filter.copyWith( + perfs: selected + ? filter.perfs.add(value) + : filter.perfs.remove(value), + ); + }, + ), ), - ), - const Divider(), - const SizedBox(height: 10.0), - _Filter( - filterName: context.l10n.side, - filterType: FilterType.singleChoice, - choices: Side.values, - choiceSelected: (choice) => filter.side == choice, - choiceLabel: (t) => switch (t) { - Side.white => context.l10n.white, - Side.black => context.l10n.black, - }, - onSelected: (value, selected) => setState( - () { - filter = filter.copyWith(side: selected ? value : null); + const Divider(), + const SizedBox(height: 10.0), + _Filter( + filterName: context.l10n.side, + filterType: FilterType.singleChoice, + choices: Side.values, + choiceSelected: (choice) => filter.side == choice, + choiceLabel: (t) => switch (t) { + Side.white => context.l10n.white, + Side.black => context.l10n.black, }, - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('Cancel'), - ), - TextButton( - onPressed: () => Navigator.of(context).pop(filter), - child: const Text('OK'), + onSelected: (value, selected) => setState( + () { + filter = filter.copyWith(side: selected ? value : null); + }, ), - ], - ), - ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(filter), + child: const Text('OK'), + ), + ], + ), + ], + ), ), ); } From e5e966af63632d37d037a8b5084694e536562924 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Sun, 28 Jul 2024 07:54:03 +0200 Subject: [PATCH 044/979] feat: add a chip indicating number of used filters --- lib/src/model/game/game_filter.dart | 7 ++ lib/src/view/user/game_history_screen.dart | 90 ++++++++++++++-------- 2 files changed, 64 insertions(+), 33 deletions(-) diff --git a/lib/src/model/game/game_filter.dart b/lib/src/model/game/game_filter.dart index d9edbbcb8a..e88bef9641 100644 --- a/lib/src/model/game/game_filter.dart +++ b/lib/src/model/game/game_filter.dart @@ -18,6 +18,13 @@ class GameFilter extends _$GameFilter { perfs: filter.perfs, side: filter.side, ); + + int countFiltersInUse() { + final fields = [state.perfs, state.side]; + return fields + .where((field) => field is Iterable ? field.isNotEmpty : field != null) + .length; + } } @freezed diff --git a/lib/src/view/user/game_history_screen.dart b/lib/src/view/user/game_history_screen.dart index 0bae8698a0..764c88c0a5 100644 --- a/lib/src/view/user/game_history_screen.dart +++ b/lib/src/view/user/game_history_screen.dart @@ -10,7 +10,6 @@ import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/view/game/game_list_tile.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; class GameHistoryScreen extends ConsumerWidget { const GameHistoryScreen({ @@ -25,18 +24,14 @@ class GameHistoryScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return ConsumerPlatformWidget( - ref: ref, - androidBuilder: _buildAndroid, - iosBuilder: _buildIos, - ); - } - Widget _buildIos(BuildContext context, WidgetRef ref) { - return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - middle: Text(context.l10n.games), - trailing: IconButton( + final filtersInUse = ref + .read(gameFilterProvider(filter: gameFilter).notifier) + .countFiltersInUse(); + final filterBtn = Stack( + alignment: Alignment.center, + children: [ + IconButton( icon: const Icon(Icons.tune), tooltip: context.l10n.filterGames, onPressed: () => showAdaptiveBottomSheet( @@ -54,35 +49,64 @@ class GameHistoryScreen extends ConsumerWidget { } }), ), + if (filtersInUse > 0) + Positioned( + top: 2.0, + right: 2.0, + child: Stack( + alignment: Alignment.center, + children: [ + Icon( + Icons.brightness_1, + size: 20.0, + color: Theme.of(context).colorScheme.secondary, + ), + FittedBox( + fit: BoxFit.contain, + child: DefaultTextStyle.merge( + style: TextStyle( + color: Theme.of(context).colorScheme.onSecondary, + fontWeight: FontWeight.bold, + ), + child: Text(filtersInUse.toString()), + ), + ), + ], + ), + ), + ], + ); + + switch (Theme.of(context).platform) { + case TargetPlatform.android: + return _buildAndroid(context, ref, filterBtn: filterBtn); + case TargetPlatform.iOS: + return _buildIos(context, ref, filterBtn: filterBtn); + default: + assert(false, 'Unexpected platform ${Theme.of(context).platform}'); + return const SizedBox.shrink(); + } + } + + Widget _buildIos(BuildContext context, WidgetRef ref, { + required Widget filterBtn, + }) { + return CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + middle: Text(context.l10n.games), + trailing: filterBtn, ), child: _Body(user: user, isOnline: isOnline, gameFilter: gameFilter), ); } - Widget _buildAndroid(BuildContext context, WidgetRef ref) { + Widget _buildAndroid(BuildContext context, WidgetRef ref, { + required Widget filterBtn, + }) { return Scaffold( appBar: AppBar( title: Text(context.l10n.games), - actions: [ - IconButton( - icon: const Icon(Icons.tune), - tooltip: context.l10n.filterGames, - onPressed: () => showAdaptiveBottomSheet( - context: context, - isScrollControlled: true, - showDragHandle: true, - builder: (_) => _FilterGames( - filter: ref.read(gameFilterProvider(filter: gameFilter)), - ), - ).then((value) { - if (value != null) { - ref - .read(gameFilterProvider(filter: gameFilter).notifier) - .setFilter(value); - } - }), - ), - ], + actions: [filterBtn], ), body: _Body(user: user, isOnline: isOnline, gameFilter: gameFilter), ); From 3801986a9c70af0cdfaf8a21d4e4bcf4500f97df Mon Sep 17 00:00:00 2001 From: Mauritz Date: Sun, 28 Jul 2024 08:16:28 +0200 Subject: [PATCH 045/979] feat: icons before filter name --- lib/src/view/user/game_history_screen.dart | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/lib/src/view/user/game_history_screen.dart b/lib/src/view/user/game_history_screen.dart index 764c88c0a5..b49ef82417 100644 --- a/lib/src/view/user/game_history_screen.dart +++ b/lib/src/view/user/game_history_screen.dart @@ -6,6 +6,7 @@ import 'package:lichess_mobile/src/model/common/perf.dart'; import 'package:lichess_mobile/src/model/game/game_filter.dart'; import 'package:lichess_mobile/src/model/game/game_history.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; +import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/view/game/game_list_tile.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; @@ -278,6 +279,7 @@ class _FilterGamesState extends State<_FilterGames> { children: [ _Filter( filterName: context.l10n.variant, + icon: const Icon(LichessIcons.classical), filterType: FilterType.multipleChoice, choices: const [ Perf.ultraBullet, @@ -311,6 +313,7 @@ class _FilterGamesState extends State<_FilterGames> { const SizedBox(height: 10.0), _Filter( filterName: context.l10n.side, + icon: const Icon(LichessIcons.chess_pawn), filterType: FilterType.singleChoice, choices: Side.values, choiceSelected: (choice) => filter.side == choice, @@ -353,6 +356,7 @@ enum FilterType { class _Filter extends StatelessWidget { const _Filter({ required this.filterName, + required this.icon, required this.filterType, required this.choices, required this.choiceSelected, @@ -361,6 +365,7 @@ class _Filter extends StatelessWidget { }); final String filterName; + final Icon icon; final FilterType filterType; final Iterable choices; final bool Function(T choice) choiceSelected; @@ -372,7 +377,15 @@ class _Filter extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(filterName, style: const TextStyle(fontSize: 18)), + Row( + children: [ + Container( + margin: const EdgeInsets.only(right: 10), + child: icon, + ), + Text(filterName, style: const TextStyle(fontSize: 18)), + ], + ), SizedBox( width: double.infinity, child: Wrap( From 2fa7ee3dd87f9ca9aaa0d374ba91558388a5d5f9 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Sun, 28 Jul 2024 10:35:14 +0200 Subject: [PATCH 046/979] feat: include username in appbar title --- lib/src/view/user/game_history_screen.dart | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/lib/src/view/user/game_history_screen.dart b/lib/src/view/user/game_history_screen.dart index b49ef82417..b5ee6f5b22 100644 --- a/lib/src/view/user/game_history_screen.dart +++ b/lib/src/view/user/game_history_screen.dart @@ -2,6 +2,7 @@ import 'package:chessground/chessground.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; import 'package:lichess_mobile/src/model/game/game_filter.dart'; import 'package:lichess_mobile/src/model/game/game_history.dart'; @@ -26,6 +27,13 @@ class GameHistoryScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final session = ref.read(authSessionProvider); + final username = user?.name ?? session?.user.name; + final title = Text( + username != null + ? '$username ${context.l10n.games.toLowerCase()}' + : context.l10n.games, + ); final filtersInUse = ref .read(gameFilterProvider(filter: gameFilter).notifier) .countFiltersInUse(); @@ -80,9 +88,9 @@ class GameHistoryScreen extends ConsumerWidget { switch (Theme.of(context).platform) { case TargetPlatform.android: - return _buildAndroid(context, ref, filterBtn: filterBtn); + return _buildAndroid(context, ref, title: title, filterBtn: filterBtn); case TargetPlatform.iOS: - return _buildIos(context, ref, filterBtn: filterBtn); + return _buildIos(context, ref, title: title, filterBtn: filterBtn); default: assert(false, 'Unexpected platform ${Theme.of(context).platform}'); return const SizedBox.shrink(); @@ -90,11 +98,12 @@ class GameHistoryScreen extends ConsumerWidget { } Widget _buildIos(BuildContext context, WidgetRef ref, { + required Widget title, required Widget filterBtn, }) { return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( - middle: Text(context.l10n.games), + middle: title, trailing: filterBtn, ), child: _Body(user: user, isOnline: isOnline, gameFilter: gameFilter), @@ -102,11 +111,12 @@ class GameHistoryScreen extends ConsumerWidget { } Widget _buildAndroid(BuildContext context, WidgetRef ref, { + required Widget title, required Widget filterBtn, }) { return Scaffold( appBar: AppBar( - title: Text(context.l10n.games), + title: title, actions: [filterBtn], ), body: _Body(user: user, isOnline: isOnline, gameFilter: gameFilter), From b590d9c26c6e74cac9eee2a8b95542ff8631974b Mon Sep 17 00:00:00 2001 From: Mauritz Date: Sun, 28 Jul 2024 10:36:01 +0200 Subject: [PATCH 047/979] feat: sort variants and remove unplayed ones --- lib/src/view/user/game_history_screen.dart | 102 ++++++++++++++------- 1 file changed, 67 insertions(+), 35 deletions(-) diff --git a/lib/src/view/user/game_history_screen.dart b/lib/src/view/user/game_history_screen.dart index b5ee6f5b22..f72b93c5ed 100644 --- a/lib/src/view/user/game_history_screen.dart +++ b/lib/src/view/user/game_history_screen.dart @@ -7,6 +7,7 @@ import 'package:lichess_mobile/src/model/common/perf.dart'; import 'package:lichess_mobile/src/model/game/game_filter.dart'; import 'package:lichess_mobile/src/model/game/game_history.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; +import 'package:lichess_mobile/src/model/user/user_repository_providers.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/view/game/game_list_tile.dart'; @@ -49,6 +50,7 @@ class GameHistoryScreen extends ConsumerWidget { showDragHandle: true, builder: (_) => _FilterGames( filter: ref.read(gameFilterProvider(filter: gameFilter)), + user: user, ), ).then((value) { if (value != null) { @@ -255,18 +257,20 @@ class _BodyState extends ConsumerState<_Body> { } } -class _FilterGames extends StatefulWidget { +class _FilterGames extends ConsumerStatefulWidget { const _FilterGames({ required this.filter, + required this.user, }); final GameFilterState filter; + final LightUser? user; @override - State<_FilterGames> createState() => _FilterGamesState(); + ConsumerState<_FilterGames> createState() => _FilterGamesState(); } -class _FilterGamesState extends State<_FilterGames> { +class _FilterGamesState extends ConsumerState<_FilterGames> { late GameFilterState filter; @override @@ -277,6 +281,56 @@ class _FilterGamesState extends State<_FilterGames> { @override Widget build(BuildContext context) { + const gamePerfs = [ + Perf.ultraBullet, + Perf.bullet, + Perf.blitz, + Perf.rapid, + Perf.classical, + Perf.correspondence, + Perf.chess960, + Perf.antichess, + Perf.kingOfTheHill, + Perf.threeCheck, + Perf.atomic, + Perf.horde, + Perf.racingKings, + Perf.crazyhouse, + ]; + + final session = ref.read(authSessionProvider); + final userId = widget.user?.id ?? session?.user.id; + + List availablePerfs(User user) { + final perfs = gamePerfs.where((perf) { + final p = user.perfs[perf]; + return p != null && p.numberOfGamesOrRuns > 0; + }).toList(growable: false); + perfs.sort( + (p1, p2) => user.perfs[p2]!.numberOfGamesOrRuns + .compareTo(user.perfs[p1]!.numberOfGamesOrRuns), + ); + return perfs; + } + + Widget perfFilter(List choices) => _Filter( + filterName: context.l10n.variant, + icon: const Icon(LichessIcons.classical), + filterType: FilterType.multipleChoice, + choices: choices, + choiceSelected: (choice) => filter.perfs.contains(choice), + choiceLabel: (t) => t.title, + onSelected: (value, selected) => setState( + () { + filter = filter.copyWith( + perfs: selected + ? filter.perfs.add(value) + : filter.perfs.remove(value), + ); + }, + ), + ); + return Container( padding: const EdgeInsets.all(16), child: DraggableScrollableSheet( @@ -287,38 +341,16 @@ class _FilterGamesState extends State<_FilterGames> { builder: (context, scrollController) => ListView( controller: scrollController, children: [ - _Filter( - filterName: context.l10n.variant, - icon: const Icon(LichessIcons.classical), - filterType: FilterType.multipleChoice, - choices: const [ - Perf.ultraBullet, - Perf.bullet, - Perf.blitz, - Perf.rapid, - Perf.classical, - Perf.correspondence, - Perf.chess960, - Perf.antichess, - Perf.kingOfTheHill, - Perf.threeCheck, - Perf.atomic, - Perf.horde, - Perf.racingKings, - Perf.crazyhouse, - ], - choiceSelected: (choice) => filter.perfs.contains(choice), - choiceLabel: (t) => t.title, - onSelected: (value, selected) => setState( - () { - filter = filter.copyWith( - perfs: selected - ? filter.perfs.add(value) - : filter.perfs.remove(value), - ); - }, - ), - ), + if (userId != null) + ref.watch(userProvider(id: userId)).when( + data: (user) => perfFilter(availablePerfs(user)), + loading: () => const Center( + child: CircularProgressIndicator.adaptive(), + ), + error: (_, __) => perfFilter(gamePerfs), + ) + else + perfFilter(gamePerfs), const Divider(), const SizedBox(height: 10.0), _Filter( From 477807d75120ec43174aa08cdb78f6c67ab7fd72 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Sun, 28 Jul 2024 11:01:39 +0200 Subject: [PATCH 048/979] fix: ui adjustments + extract variable + format --- lib/src/view/user/game_history_screen.dart | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/src/view/user/game_history_screen.dart b/lib/src/view/user/game_history_screen.dart index f72b93c5ed..8906d2e46f 100644 --- a/lib/src/view/user/game_history_screen.dart +++ b/lib/src/view/user/game_history_screen.dart @@ -27,7 +27,6 @@ class GameHistoryScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final session = ref.read(authSessionProvider); final username = user?.name ?? session?.user.name; final title = Text( @@ -99,7 +98,9 @@ class GameHistoryScreen extends ConsumerWidget { } } - Widget _buildIos(BuildContext context, WidgetRef ref, { + Widget _buildIos( + BuildContext context, + WidgetRef ref, { required Widget title, required Widget filterBtn, }) { @@ -112,7 +113,9 @@ class GameHistoryScreen extends ConsumerWidget { ); } - Widget _buildAndroid(BuildContext context, WidgetRef ref, { + Widget _buildAndroid( + BuildContext context, + WidgetRef ref, { required Widget title, required Widget filterBtn, }) { @@ -297,6 +300,7 @@ class _FilterGamesState extends ConsumerState<_FilterGames> { Perf.racingKings, Perf.crazyhouse, ]; + const filterGroupSpace = SizedBox(height: 10.0); final session = ref.read(authSessionProvider); final userId = widget.user?.id ?? session?.user.id; @@ -311,7 +315,7 @@ class _FilterGamesState extends ConsumerState<_FilterGames> { .compareTo(user.perfs[p1]!.numberOfGamesOrRuns), ); return perfs; - } + } Widget perfFilter(List choices) => _Filter( filterName: context.l10n.variant, @@ -352,7 +356,7 @@ class _FilterGamesState extends ConsumerState<_FilterGames> { else perfFilter(gamePerfs), const Divider(), - const SizedBox(height: 10.0), + filterGroupSpace, _Filter( filterName: context.l10n.side, icon: const Icon(LichessIcons.chess_pawn), @@ -428,11 +432,11 @@ class _Filter extends StatelessWidget { Text(filterName, style: const TextStyle(fontSize: 18)), ], ), + const SizedBox(height: 10), SizedBox( width: double.infinity, child: Wrap( spacing: 8.0, - runSpacing: 8.0, children: choices .map( (choice) => switch (filterType) { From 9f0679b3039037acfa3eadacb60372f250a05098 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Fri, 5 Jul 2024 17:25:32 +0200 Subject: [PATCH 049/979] feat: add row for opening line --- lib/src/view/analysis/analysis_screen.dart | 86 ++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index aff75712a2..041685aaea 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -347,6 +347,7 @@ class _Body extends ConsumerWidget { Orientation.portrait, ), ), + _OpeningExplorer(), ], ); }, @@ -359,6 +360,91 @@ class _Body extends ConsumerWidget { } } +class _OpeningExplorer extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), + child: const Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text('e4'), + SizedBox(width: 16), + Text('44%'), + SizedBox(width: 8), + Text('308,469'), + SizedBox(width: 8), + ], + ), + Expanded( + child: _WinPercentageChart( + whitePercent: 30, + drawPercent: 48, + blackPercent: 23, + ), + ), + ], + ), + ); + } +} + +class _WinPercentageChart extends StatelessWidget { + const _WinPercentageChart({ + required this.whitePercent, + required this.drawPercent, + required this.blackPercent, + }); + + final int whitePercent; + final int drawPercent; + final int blackPercent; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + flex: whitePercent, + child: ColoredBox( + color: Colors.white, + child: Text( + '$whitePercent', + textAlign: TextAlign.center, + style: const TextStyle(color: Colors.black), + ), + ), + ), + Expanded( + flex: drawPercent, + child: ColoredBox( + color: Colors.grey, + child: Text( + '$drawPercent', + textAlign: TextAlign.center, + style: const TextStyle(color: Colors.white), + ), + ), + ), + Expanded( + flex: blackPercent, + child: ColoredBox( + color: Colors.black, + child: Text( + '$blackPercent', + textAlign: TextAlign.center, + style: const TextStyle(color: Colors.white), + ), + ), + ), + ], + ); + } +} + class _Board extends ConsumerStatefulWidget { const _Board( this.pgn, From 3e07c99a01171dd29ca2179834012ab6bf0acb6e Mon Sep 17 00:00:00 2001 From: Mauritz Date: Fri, 5 Jul 2024 17:39:00 +0200 Subject: [PATCH 050/979] feat: add opening explorer toggle --- lib/src/model/analysis/analysis_preferences.dart | 10 ++++++++++ lib/src/view/analysis/analysis_screen.dart | 6 +++++- lib/src/view/analysis/analysis_settings.dart | 7 +++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/lib/src/model/analysis/analysis_preferences.dart b/lib/src/model/analysis/analysis_preferences.dart index 4cfc0e1daa..cd2724e9b9 100644 --- a/lib/src/model/analysis/analysis_preferences.dart +++ b/lib/src/model/analysis/analysis_preferences.dart @@ -56,6 +56,14 @@ class AnalysisPreferences extends _$AnalysisPreferences { ); } + Future toggleOpeningExplorer() { + return _save( + state.copyWith( + showOpeningExplorer: !state.showOpeningExplorer, + ), + ); + } + Future toggleShowBestMoveArrow() { return _save( state.copyWith( @@ -102,6 +110,7 @@ class AnalysisPrefState with _$AnalysisPrefState { required bool showBestMoveArrow, required bool showAnnotations, required bool showPgnComments, + required bool showOpeningExplorer, @Assert('numEvalLines >= 1 && numEvalLines <= 3') required int numEvalLines, @Assert('numEngineCores >= 1 && numEngineCores <= maxEngineCores') required int numEngineCores, @@ -113,6 +122,7 @@ class AnalysisPrefState with _$AnalysisPrefState { showBestMoveArrow: true, showAnnotations: true, showPgnComments: true, + showOpeningExplorer: true, numEvalLines: 2, numEngineCores: defaultEngineCores, ); diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index 041685aaea..b270d7dad8 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -230,6 +230,9 @@ class _Body extends ConsumerWidget { final showEvaluationGauge = ref.watch( analysisPreferencesProvider.select((value) => value.showEvaluationGauge), ); + final showOpeningExplorer = ref.watch( + analysisPreferencesProvider.select((value) => value.showOpeningExplorer), + ); final isEngineAvailable = ref.watch( ctrlProvider.select( @@ -347,7 +350,8 @@ class _Body extends ConsumerWidget { Orientation.portrait, ), ), - _OpeningExplorer(), + if (showOpeningExplorer) + _OpeningExplorer(), ], ); }, diff --git a/lib/src/view/analysis/analysis_settings.dart b/lib/src/view/analysis/analysis_settings.dart index 33a8ea3749..574b844ed2 100644 --- a/lib/src/view/analysis/analysis_settings.dart +++ b/lib/src/view/analysis/analysis_settings.dart @@ -140,6 +140,13 @@ class AnalysisSettings extends ConsumerWidget { .read(analysisPreferencesProvider.notifier) .togglePgnComments(), ), + SwitchSettingTile( + title: Text(context.l10n.openingExplorer), + value: prefs.showOpeningExplorer, + onChanged: (_) => ref + .read(analysisPreferencesProvider.notifier) + .toggleOpeningExplorer(), + ), SwitchSettingTile( title: Text(context.l10n.sound), value: isSoundEnabled, From 94ea0b7b1079abd6b8ee5230b0c6f48f4a27e424 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Fri, 5 Jul 2024 20:49:34 +0200 Subject: [PATCH 051/979] feat: multiple moves --- lib/src/view/analysis/analysis_screen.dart | 73 +++++++++++++++------- 1 file changed, 50 insertions(+), 23 deletions(-) diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index b270d7dad8..81cdc21a4c 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -351,7 +351,12 @@ class _Body extends ConsumerWidget { ), ), if (showOpeningExplorer) - _OpeningExplorer(), + _OpeningExplorer( + moves: [ + MoveOpening('e4', 100, 200, 50), + MoveOpening('d4', 130, 110, 80), + ].toIList(), + ), ], ); }, @@ -364,33 +369,55 @@ class _Body extends ConsumerWidget { } } +class MoveOpening { + String san; + int white; + int draws; + int black; + + MoveOpening(this.san, this.white, this.draws, this.black); +} + class _OpeningExplorer extends StatelessWidget { + const _OpeningExplorer({ + required this.moves, + }); + + final IList moves; + @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), - child: const Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Text('e4'), - SizedBox(width: 16), - Text('44%'), - SizedBox(width: 8), - Text('308,469'), - SizedBox(width: 8), - ], - ), - Expanded( - child: _WinPercentageChart( - whitePercent: 30, - drawPercent: 48, - blackPercent: 23, - ), - ), - ], + child: ListView( + shrinkWrap: true, + children: moves + .map( + (move) => Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text(move.san), + const SizedBox(width: 16), + const Text('44%'), + const SizedBox(width: 8), + const Text('308,469'), + const SizedBox(width: 8), + ], + ), + Expanded( + child: _WinPercentageChart( + whitePercent: move.white, + drawPercent: move.draws, + blackPercent: move.black, + ), + ), + ], + ), + ) + .toList(), ), ); } From 1048e31612db44f99d5fb9214237eb9d1c6e36c3 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Sat, 6 Jul 2024 00:31:01 +0200 Subject: [PATCH 052/979] feat: add opening explorer api class --- lib/src/model/analysis/opening_explorer.dart | 78 +++++++++++++++++++ .../analysis/opening_explorer_repository.dart | 37 +++++++++ 2 files changed, 115 insertions(+) create mode 100644 lib/src/model/analysis/opening_explorer.dart create mode 100644 lib/src/model/analysis/opening_explorer_repository.dart diff --git a/lib/src/model/analysis/opening_explorer.dart b/lib/src/model/analysis/opening_explorer.dart new file mode 100644 index 0000000000..a07829661a --- /dev/null +++ b/lib/src/model/analysis/opening_explorer.dart @@ -0,0 +1,78 @@ +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:lichess_mobile/src/model/common/chess.dart'; + +part 'opening_explorer.freezed.dart'; + +@freezed +class OpeningExplorer with _$OpeningExplorer { + const factory OpeningExplorer({ + required LightOpening opening, + required int white, + required int draws, + required int black, + required IList moves, + required IList topGames, + required IList recentGames, + required IList history, + }) = _OpeningExplorer; +} + +@freezed +class OpeningMove with _$OpeningMove { + const factory OpeningMove({ + required String uci, + required String san, + required int averageRating, + required int white, + required int draws, + required int black, + Game? game, + }) = _OpeningMove; +} + +@freezed +class Game with _$Game { + const Game._(); + + factory Game({ + required String uci, + required String id, + required String winner, + required MasterPlayer white, + required MasterPlayer black, + required int year, + required String month, + }) = _Game; +} + +@freezed +class GameWithMove with _$GameWithMove { + factory GameWithMove({ + required String uci, + required Game game, + }) = _GameWithMove; +} + + +@freezed +class MasterPlayer with _$MasterPlayer { + const MasterPlayer._(); + + const factory MasterPlayer({ + required String name, + required int rating, + }) = _MasterPlayer; +} + +@freezed +class HistoryStat with _$HistoryStat { + const HistoryStat._(); + + const factory HistoryStat({ + required String month, + required int white, + required int draws, + required int black, + }) = _HistoryStat; +} diff --git a/lib/src/model/analysis/opening_explorer_repository.dart b/lib/src/model/analysis/opening_explorer_repository.dart new file mode 100644 index 0000000000..1abef311a3 --- /dev/null +++ b/lib/src/model/analysis/opening_explorer_repository.dart @@ -0,0 +1,37 @@ +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:lichess_mobile/src/model/analysis/opening_explorer.dart'; +import 'package:lichess_mobile/src/model/common/chess.dart'; + +class OpeningExplorerRepository { + OpeningExplorer getOpeningExplorer(String fen) { + return OpeningExplorer( + opening: const LightOpening(eco: 'D10', name: 'Slav Defense'), + white: 1443, + draws: 3787, + black: 1156, + moves: const [ + OpeningMove( + uci: 'c6d5', + san: 'cxd5', + averageRating: 2423, + white: 1443, + draws: 3787, + black: 1155, + game: null, + ), + OpeningMove( + uci: 'g8f6', + san: 'Nf6', + averageRating: 2515, + white: 0, + draws: 0, + black: 1, + game: null, + ), + ].toIList(), + topGames: const IList.empty(), + recentGames: const IList.empty(), + history: const IList.empty(), + ); + } +} From 4d9dc696b9b7419d2ccf6dcc22a31cd6df55af66 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Sat, 6 Jul 2024 00:55:48 +0200 Subject: [PATCH 053/979] feat: use the new class in screen --- lib/src/model/analysis/opening_explorer.dart | 18 +++-- lib/src/view/analysis/analysis_screen.dart | 81 +++++++++----------- 2 files changed, 50 insertions(+), 49 deletions(-) diff --git a/lib/src/model/analysis/opening_explorer.dart b/lib/src/model/analysis/opening_explorer.dart index a07829661a..37bcbc4af9 100644 --- a/lib/src/model/analysis/opening_explorer.dart +++ b/lib/src/model/analysis/opening_explorer.dart @@ -6,6 +6,8 @@ part 'opening_explorer.freezed.dart'; @freezed class OpeningExplorer with _$OpeningExplorer { + const OpeningExplorer._(); + const factory OpeningExplorer({ required LightOpening opening, required int white, @@ -16,10 +18,16 @@ class OpeningExplorer with _$OpeningExplorer { required IList recentGames, required IList history, }) = _OpeningExplorer; + + int get games { + return white + draws + black; + } } @freezed class OpeningMove with _$OpeningMove { + const OpeningMove._(); + const factory OpeningMove({ required String uci, required String san, @@ -29,12 +37,14 @@ class OpeningMove with _$OpeningMove { required int black, Game? game, }) = _OpeningMove; + + int get games { + return white + draws + black; + } } @freezed class Game with _$Game { - const Game._(); - factory Game({ required String uci, required String id, @@ -57,8 +67,6 @@ class GameWithMove with _$GameWithMove { @freezed class MasterPlayer with _$MasterPlayer { - const MasterPlayer._(); - const factory MasterPlayer({ required String name, required int rating, @@ -67,8 +75,6 @@ class MasterPlayer with _$MasterPlayer { @freezed class HistoryStat with _$HistoryStat { - const HistoryStat._(); - const factory HistoryStat({ required String month, required int white, diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index 81cdc21a4c..059d609739 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -13,6 +13,7 @@ import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/account/account_preferences.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; +import 'package:lichess_mobile/src/model/analysis/opening_explorer_repository.dart'; import 'package:lichess_mobile/src/model/analysis/server_analysis_service.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; @@ -351,12 +352,7 @@ class _Body extends ConsumerWidget { ), ), if (showOpeningExplorer) - _OpeningExplorer( - moves: [ - MoveOpening('e4', 100, 200, 50), - MoveOpening('d4', 130, 110, 80), - ].toIList(), - ), + const _OpeningExplorer(fen: ''), ], ); }, @@ -369,49 +365,48 @@ class _Body extends ConsumerWidget { } } -class MoveOpening { - String san; - int white; - int draws; - int black; - - MoveOpening(this.san, this.white, this.draws, this.black); -} - class _OpeningExplorer extends StatelessWidget { const _OpeningExplorer({ - required this.moves, + required this.fen, }); - final IList moves; + final String fen; @override Widget build(BuildContext context) { + final explorer = OpeningExplorerRepository().getOpeningExplorer(fen); + return Container( padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), child: ListView( shrinkWrap: true, - children: moves + children: explorer.moves .map( (move) => Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Text(move.san), - const SizedBox(width: 16), - const Text('44%'), - const SizedBox(width: 8), - const Text('308,469'), - const SizedBox(width: 8), - ], + Expanded( + flex: 4, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text(move.san), + const SizedBox(width: 16), + Text( + '${((move.games / explorer.games) * 100).round()}%', + ), + const SizedBox(width: 8), + Text('${move.games}'), + const SizedBox(width: 8), + ], + ), ), Expanded( + flex: 6, child: _WinPercentageChart( - whitePercent: move.white, - drawPercent: move.draws, - blackPercent: move.black, + white: move.white, + draws: move.draws, + black: move.black, ), ), ], @@ -425,47 +420,47 @@ class _OpeningExplorer extends StatelessWidget { class _WinPercentageChart extends StatelessWidget { const _WinPercentageChart({ - required this.whitePercent, - required this.drawPercent, - required this.blackPercent, + required this.white, + required this.draws, + required this.black, }); - final int whitePercent; - final int drawPercent; - final int blackPercent; + final int white; + final int draws; + final int black; @override Widget build(BuildContext context) { return Row( children: [ Expanded( - flex: whitePercent, + flex: white, child: ColoredBox( color: Colors.white, child: Text( - '$whitePercent', + '$white', textAlign: TextAlign.center, style: const TextStyle(color: Colors.black), ), ), ), Expanded( - flex: drawPercent, + flex: draws, child: ColoredBox( color: Colors.grey, child: Text( - '$drawPercent', + '$draws', textAlign: TextAlign.center, style: const TextStyle(color: Colors.white), ), ), ), Expanded( - flex: blackPercent, + flex: black, child: ColoredBox( color: Colors.black, child: Text( - '$blackPercent', + '$black', textAlign: TextAlign.center, style: const TextStyle(color: Colors.white), ), From 64700a1d723a9fcee202d8c5f1bb12169a3393c2 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Sat, 6 Jul 2024 01:42:51 +0200 Subject: [PATCH 054/979] feat: use datatable instead of listview --- lib/src/view/analysis/analysis_screen.dart | 43 ++++++++++------------ 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index 059d609739..0c3154f6d4 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -377,33 +377,28 @@ class _OpeningExplorer extends StatelessWidget { final explorer = OpeningExplorerRepository().getOpeningExplorer(fen); return Container( + width: MediaQuery.of(context).size.width, padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), - child: ListView( - shrinkWrap: true, - children: explorer.moves + child: DataTable( + columnSpacing: 0, + horizontalMargin: 0, + columns: const [ + DataColumn(label: Text('Move')), + DataColumn(label: Text('Games')), + DataColumn(label: Text('')), + DataColumn(label: Text('White / Draw / Black')), + ], + rows: explorer.moves .map( - (move) => Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - flex: 4, - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Text(move.san), - const SizedBox(width: 16), - Text( - '${((move.games / explorer.games) * 100).round()}%', - ), - const SizedBox(width: 8), - Text('${move.games}'), - const SizedBox(width: 8), - ], - ), + (move) => DataRow( + cells: [ + DataCell(Text(move.san)), + DataCell( + Text('${((move.games / explorer.games) * 100).round()}%'), ), - Expanded( - flex: 6, - child: _WinPercentageChart( + DataCell(Text('${move.games}')), + DataCell( + _WinPercentageChart( white: move.white, draws: move.draws, black: move.black, From aa32bc4591f2a8f398ac49f25f671f3dae8aa1e0 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Sat, 6 Jul 2024 01:57:48 +0200 Subject: [PATCH 055/979] feat: show percentage in winrate graph --- lib/src/view/analysis/analysis_screen.dart | 64 +++++++++++++--------- 1 file changed, 37 insertions(+), 27 deletions(-) diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index 0c3154f6d4..63b6f1e0b5 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -426,41 +426,51 @@ class _WinPercentageChart extends StatelessWidget { @override Widget build(BuildContext context) { + int percentGames(int games) => + ((games / (white + draws + black)) * 100).round(); + + final percentWhite = percentGames(white); + final percentDraws = percentGames(draws); + final percentBlack = percentGames(black); + return Row( children: [ - Expanded( - flex: white, - child: ColoredBox( - color: Colors.white, - child: Text( - '$white', - textAlign: TextAlign.center, - style: const TextStyle(color: Colors.black), + if (percentWhite != 0) + Expanded( + flex: white, + child: ColoredBox( + color: Colors.white, + child: Text( + percentWhite < 5 ? '' : '${percentGames(white)}%', + textAlign: TextAlign.center, + style: const TextStyle(color: Colors.black), + ), ), ), - ), - Expanded( - flex: draws, - child: ColoredBox( - color: Colors.grey, - child: Text( - '$draws', - textAlign: TextAlign.center, - style: const TextStyle(color: Colors.white), + if (percentDraws != 0) + Expanded( + flex: draws, + child: ColoredBox( + color: Colors.grey, + child: Text( + percentDraws < 5 ? '' : '${percentGames(draws)}%', + textAlign: TextAlign.center, + style: const TextStyle(color: Colors.white), + ), ), ), - ), - Expanded( - flex: black, - child: ColoredBox( - color: Colors.black, - child: Text( - '$black', - textAlign: TextAlign.center, - style: const TextStyle(color: Colors.white), + if (percentBlack != 0) + Expanded( + flex: black, + child: ColoredBox( + color: Colors.black, + child: Text( + percentBlack < 5 ? '' : '${percentGames(black)}%', + textAlign: TextAlign.center, + style: const TextStyle(color: Colors.white), + ), ), ), - ), ], ); } From a6341b77b7dfcad036fe1684fc133fdcecebf9bf Mon Sep 17 00:00:00 2001 From: Mauritz Date: Sat, 6 Jul 2024 02:04:55 +0200 Subject: [PATCH 056/979] feat: merge games columns into one --- lib/src/view/analysis/analysis_screen.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index 63b6f1e0b5..acdab70ca1 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -385,7 +385,6 @@ class _OpeningExplorer extends StatelessWidget { columns: const [ DataColumn(label: Text('Move')), DataColumn(label: Text('Games')), - DataColumn(label: Text('')), DataColumn(label: Text('White / Draw / Black')), ], rows: explorer.moves @@ -394,9 +393,10 @@ class _OpeningExplorer extends StatelessWidget { cells: [ DataCell(Text(move.san)), DataCell( - Text('${((move.games / explorer.games) * 100).round()}%'), + Text( + '${((move.games / explorer.games) * 100).round()}% / ${move.games}', + ), ), - DataCell(Text('${move.games}')), DataCell( _WinPercentageChart( white: move.white, From 1272ca99a24cfbaee439107e44229d767fca75bb Mon Sep 17 00:00:00 2001 From: Mauritz Date: Sat, 6 Jul 2024 13:57:00 +0200 Subject: [PATCH 057/979] feat: fetch data from opening explorer api --- lib/src/constants.dart | 5 ++ lib/src/model/analysis/opening_explorer.dart | 51 ++++++++---- .../analysis/opening_explorer_repository.dart | 53 +++++------- lib/src/model/common/chess.dart | 10 +++ lib/src/model/common/http.dart | 15 +++- lib/src/view/analysis/analysis_screen.dart | 82 +++++++++++-------- 6 files changed, 132 insertions(+), 84 deletions(-) diff --git a/lib/src/constants.dart b/lib/src/constants.dart index 691f9b6c24..06729ce396 100644 --- a/lib/src/constants.dart +++ b/lib/src/constants.dart @@ -21,6 +21,11 @@ const kLichessCDNHost = String.fromEnvironment( defaultValue: 'https://lichess1.org', ); +const kLichessOpeningExplorerHost = String.fromEnvironment( + 'LICHESS_OPENING_EXPLORER_HOST', + defaultValue: 'explorer.lichess.ovh', +); + const kLichessDevUser = String.fromEnvironment('LICHESS_DEV_USER', defaultValue: 'lichess'); const kLichessDevPassword = String.fromEnvironment('LICHESS_DEV_PASSWORD'); diff --git a/lib/src/model/analysis/opening_explorer.dart b/lib/src/model/analysis/opening_explorer.dart index 37bcbc4af9..c27ae23137 100644 --- a/lib/src/model/analysis/opening_explorer.dart +++ b/lib/src/model/analysis/opening_explorer.dart @@ -3,28 +3,32 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; part 'opening_explorer.freezed.dart'; +part 'opening_explorer.g.dart'; -@freezed +@Freezed(fromJson: true) class OpeningExplorer with _$OpeningExplorer { const OpeningExplorer._(); const factory OpeningExplorer({ - required LightOpening opening, + LightOpening? opening, required int white, required int draws, required int black, - required IList moves, - required IList topGames, - required IList recentGames, - required IList history, + IList? moves, + IList? topGames, + IList? recentGames, + IList? history, }) = _OpeningExplorer; + factory OpeningExplorer.fromJson(Map json) => + _$OpeningExplorerFromJson(json); + int get games { return white + draws + black; } } -@freezed +@Freezed(fromJson: true) class OpeningMove with _$OpeningMove { const OpeningMove._(); @@ -38,42 +42,58 @@ class OpeningMove with _$OpeningMove { Game? game, }) = _OpeningMove; + factory OpeningMove.fromJson(Map json) => + _$OpeningMoveFromJson(json); + int get games { return white + draws + black; } } -@freezed +@Freezed(fromJson: true) class Game with _$Game { factory Game({ - required String uci, required String id, - required String winner, + String? winner, required MasterPlayer white, required MasterPlayer black, required int year, required String month, }) = _Game; + + factory Game.fromJson(Map json) => + _$GameFromJson(json); } -@freezed +@Freezed(fromJson: true) class GameWithMove with _$GameWithMove { factory GameWithMove({ required String uci, - required Game game, + required String id, + String? winner, + required MasterPlayer white, + required MasterPlayer black, + required int year, + required String month, }) = _GameWithMove; + + factory GameWithMove.fromJson(Map json) => + _$GameWithMoveFromJson(json); } -@freezed +@Freezed(fromJson: true) class MasterPlayer with _$MasterPlayer { const factory MasterPlayer({ required String name, required int rating, }) = _MasterPlayer; + + factory MasterPlayer.fromJson(Map json) => + _$MasterPlayerFromJson(json); } -@freezed +@Freezed(fromJson: true) class HistoryStat with _$HistoryStat { const factory HistoryStat({ required String month, @@ -81,4 +101,7 @@ class HistoryStat with _$HistoryStat { required int draws, required int black, }) = _HistoryStat; + + factory HistoryStat.fromJson(Map json) => + _$HistoryStatFromJson(json); } diff --git a/lib/src/model/analysis/opening_explorer_repository.dart b/lib/src/model/analysis/opening_explorer_repository.dart index 1abef311a3..8d558b1900 100644 --- a/lib/src/model/analysis/opening_explorer_repository.dart +++ b/lib/src/model/analysis/opening_explorer_repository.dart @@ -1,37 +1,28 @@ -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:lichess_mobile/src/model/analysis/opening_explorer.dart'; -import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/common/http.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'opening_explorer_repository.g.dart'; + +@riverpod +Future openingExplorer( + OpeningExplorerRef ref, { + required String fen, +}) async { + return ref.withClient( + (client) => OpeningExplorerRepository(client).getOpeningExplorer(fen), + ); +} class OpeningExplorerRepository { - OpeningExplorer getOpeningExplorer(String fen) { - return OpeningExplorer( - opening: const LightOpening(eco: 'D10', name: 'Slav Defense'), - white: 1443, - draws: 3787, - black: 1156, - moves: const [ - OpeningMove( - uci: 'c6d5', - san: 'cxd5', - averageRating: 2423, - white: 1443, - draws: 3787, - black: 1155, - game: null, - ), - OpeningMove( - uci: 'g8f6', - san: 'Nf6', - averageRating: 2515, - white: 0, - draws: 0, - black: 1, - game: null, - ), - ].toIList(), - topGames: const IList.empty(), - recentGames: const IList.empty(), - history: const IList.empty(), + const OpeningExplorerRepository(this.client); + + final LichessClient client; + + Future getOpeningExplorer(String fen) { + return client.readJson( + Uri(path: '/masters'), + mapper: OpeningExplorer.fromJson, ); } } diff --git a/lib/src/model/common/chess.dart b/lib/src/model/common/chess.dart index a327310f1c..89dc6e1589 100644 --- a/lib/src/model/common/chess.dart +++ b/lib/src/model/common/chess.dart @@ -176,6 +176,16 @@ class LightOpening with _$LightOpening implements Opening { factory LightOpening.fromJson(Map json) => _$LightOpeningFromJson(json); + + factory LightOpening.fromServerJson(Map json) => + LightOpening.fromPick(pick(json).required()); + + factory LightOpening.fromPick(RequiredPick pick) { + return LightOpening( + eco: pick('eco').asStringOrThrow(), + name: pick('name').asStringOrThrow(), + ); + } } @Freezed(fromJson: true, toJson: true) diff --git a/lib/src/model/common/http.dart b/lib/src/model/common/http.dart index f1e275352b..5c2128338f 100644 --- a/lib/src/model/common/http.dart +++ b/lib/src/model/common/http.dart @@ -37,11 +37,18 @@ final _logger = Logger('HttpClient'); const _maxCacheSize = 2 * 1024 * 1024; +const _openingExplorerEndpoints = ['/masters', '/lichess', '/player']; + /// Creates a Uri pointing to lichess server with the given unencoded path and query parameters. -Uri lichessUri(String unencodedPath, [Map? queryParameters]) => - kLichessHost.startsWith('localhost') - ? Uri.http(kLichessHost, unencodedPath, queryParameters) - : Uri.https(kLichessHost, unencodedPath, queryParameters); +Uri lichessUri(String unencodedPath, [Map? queryParameters]) { + final host = _openingExplorerEndpoints.contains(unencodedPath) + ? kLichessOpeningExplorerHost + : kLichessHost; + final val = host.startsWith('localhost') + ? Uri.http(host, unencodedPath, queryParameters) + : Uri.https(host, unencodedPath, queryParameters); + return val; +} /// Creates the appropriate http client for the platform. Client httpClientFactory() { diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index acdab70ca1..103aef4e60 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -365,7 +365,7 @@ class _Body extends ConsumerWidget { } } -class _OpeningExplorer extends StatelessWidget { +class _OpeningExplorer extends ConsumerWidget { const _OpeningExplorer({ required this.fen, }); @@ -373,41 +373,53 @@ class _OpeningExplorer extends StatelessWidget { final String fen; @override - Widget build(BuildContext context) { - final explorer = OpeningExplorerRepository().getOpeningExplorer(fen); - - return Container( - width: MediaQuery.of(context).size.width, - padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), - child: DataTable( - columnSpacing: 0, - horizontalMargin: 0, - columns: const [ - DataColumn(label: Text('Move')), - DataColumn(label: Text('Games')), - DataColumn(label: Text('White / Draw / Black')), - ], - rows: explorer.moves - .map( - (move) => DataRow( - cells: [ - DataCell(Text(move.san)), - DataCell( - Text( - '${((move.games / explorer.games) * 100).round()}% / ${move.games}', - ), - ), - DataCell( - _WinPercentageChart( - white: move.white, - draws: move.draws, - black: move.black, - ), + Widget build(BuildContext context, WidgetRef ref) { + final explorerAsync = ref.watch(openingExplorerProvider(fen: '')); + + return explorerAsync.when( + data: (explorer) { + return Container( + width: MediaQuery.of(context).size.width, + padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), + child: DataTable( + columnSpacing: 0, + horizontalMargin: 0, + columns: const [ + DataColumn(label: Text('Move')), + DataColumn(label: Text('Games')), + DataColumn(label: Text('White / Draw / Black')), + ], + rows: explorer.moves == null + ? [] + : explorer.moves! + .map( + (move) => DataRow( + cells: [ + DataCell(Text(move.san)), + DataCell( + Text( + '${((move.games / explorer.games) * 100).round()}% / ${move.games}', + ), + ), + DataCell( + _WinPercentageChart( + white: move.white, + draws: move.draws, + black: move.black, + ), + ), + ], ), - ], - ), - ) - .toList(), + ) + .toList(), + ), + ); + }, + loading: () => const Center( + child: CircularProgressIndicator(), + ), + error: (error, stackTrace) => Center( + child: Text(error.toString()), ), ); } From cb13d924159ecf21573f24decc6efd501405d204 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Sat, 6 Jul 2024 14:23:57 +0200 Subject: [PATCH 058/979] feat: make explorer table scrollable --- lib/src/view/analysis/analysis_screen.dart | 74 ++++++++++++---------- 1 file changed, 40 insertions(+), 34 deletions(-) diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index 103aef4e60..1e0381e0e5 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -378,40 +378,46 @@ class _OpeningExplorer extends ConsumerWidget { return explorerAsync.when( data: (explorer) { - return Container( - width: MediaQuery.of(context).size.width, - padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), - child: DataTable( - columnSpacing: 0, - horizontalMargin: 0, - columns: const [ - DataColumn(label: Text('Move')), - DataColumn(label: Text('Games')), - DataColumn(label: Text('White / Draw / Black')), - ], - rows: explorer.moves == null - ? [] - : explorer.moves! - .map( - (move) => DataRow( - cells: [ - DataCell(Text(move.san)), - DataCell( - Text( - '${((move.games / explorer.games) * 100).round()}% / ${move.games}', - ), - ), - DataCell( - _WinPercentageChart( - white: move.white, - draws: move.draws, - black: move.black, - ), - ), - ], - ), - ) - .toList(), + return Expanded( + child: Container( + width: MediaQuery.of(context).size.width, + padding: + const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), + child: SingleChildScrollView( + scrollDirection: Axis.vertical, + child: DataTable( + columnSpacing: 0, + horizontalMargin: 0, + columns: const [ + DataColumn(label: Text('Move')), + DataColumn(label: Text('Games')), + DataColumn(label: Text('White / Draw / Black')), + ], + rows: explorer.moves == null + ? [] + : explorer.moves! + .map( + (move) => DataRow( + cells: [ + DataCell(Text(move.san)), + DataCell( + Text( + '${((move.games / explorer.games) * 100).round()}% / ${move.games}', + ), + ), + DataCell( + _WinPercentageChart( + white: move.white, + draws: move.draws, + black: move.black, + ), + ), + ], + ), + ) + .toList(), + ), + ), ), ); }, From 3d085ea2df1713af030b30e528f31d60509bf3cf Mon Sep 17 00:00:00 2001 From: Mauritz Date: Sat, 6 Jul 2024 14:26:29 +0200 Subject: [PATCH 059/979] fix: remove unused code --- lib/src/model/common/chess.dart | 10 ---------- lib/src/model/common/http.dart | 3 +-- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/lib/src/model/common/chess.dart b/lib/src/model/common/chess.dart index 89dc6e1589..a327310f1c 100644 --- a/lib/src/model/common/chess.dart +++ b/lib/src/model/common/chess.dart @@ -176,16 +176,6 @@ class LightOpening with _$LightOpening implements Opening { factory LightOpening.fromJson(Map json) => _$LightOpeningFromJson(json); - - factory LightOpening.fromServerJson(Map json) => - LightOpening.fromPick(pick(json).required()); - - factory LightOpening.fromPick(RequiredPick pick) { - return LightOpening( - eco: pick('eco').asStringOrThrow(), - name: pick('name').asStringOrThrow(), - ); - } } @Freezed(fromJson: true, toJson: true) diff --git a/lib/src/model/common/http.dart b/lib/src/model/common/http.dart index 5c2128338f..87f740d37a 100644 --- a/lib/src/model/common/http.dart +++ b/lib/src/model/common/http.dart @@ -44,10 +44,9 @@ Uri lichessUri(String unencodedPath, [Map? queryParameters]) { final host = _openingExplorerEndpoints.contains(unencodedPath) ? kLichessOpeningExplorerHost : kLichessHost; - final val = host.startsWith('localhost') + return host.startsWith('localhost') ? Uri.http(host, unencodedPath, queryParameters) : Uri.https(host, unencodedPath, queryParameters); - return val; } /// Creates the appropriate http client for the platform. From b1aa13d4167f98f3109274d7268c08c3e7ad6816 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Sat, 6 Jul 2024 15:30:21 +0200 Subject: [PATCH 060/979] feat: fetch data for current position --- .../analysis/opening_explorer_repository.dart | 10 +++++----- lib/src/view/analysis/analysis_screen.dart | 16 +++++++++------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/lib/src/model/analysis/opening_explorer_repository.dart b/lib/src/model/analysis/opening_explorer_repository.dart index 8d558b1900..c2ec1ed965 100644 --- a/lib/src/model/analysis/opening_explorer_repository.dart +++ b/lib/src/model/analysis/opening_explorer_repository.dart @@ -5,12 +5,12 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'opening_explorer_repository.g.dart'; @riverpod -Future openingExplorer( - OpeningExplorerRef ref, { +Future masterDatabase( + MasterDatabaseRef ref, { required String fen, }) async { return ref.withClient( - (client) => OpeningExplorerRepository(client).getOpeningExplorer(fen), + (client) => OpeningExplorerRepository(client).getMasterDatabase(fen), ); } @@ -19,9 +19,9 @@ class OpeningExplorerRepository { final LichessClient client; - Future getOpeningExplorer(String fen) { + Future getMasterDatabase(String fen) { return client.readJson( - Uri(path: '/masters'), + Uri(path: '/masters', queryParameters: {'fen': fen}), mapper: OpeningExplorer.fromJson, ); } diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index 1e0381e0e5..910c333d10 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -248,6 +248,8 @@ class _Body extends ConsumerWidget { ctrlProvider.select((value) => value.displayMode == DisplayMode.summary), ); + final position = ref.watch(ctrlProvider.select((value) => value.position)); + return Column( children: [ Expanded( @@ -352,7 +354,7 @@ class _Body extends ConsumerWidget { ), ), if (showOpeningExplorer) - const _OpeningExplorer(fen: ''), + _OpeningExplorer(fen: position.fen), ], ); }, @@ -374,10 +376,10 @@ class _OpeningExplorer extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final explorerAsync = ref.watch(openingExplorerProvider(fen: '')); + final masterDatabaseAsync = ref.watch(masterDatabaseProvider(fen: fen)); - return explorerAsync.when( - data: (explorer) { + return masterDatabaseAsync.when( + data: (masterDatabase) { return Expanded( child: Container( width: MediaQuery.of(context).size.width, @@ -393,16 +395,16 @@ class _OpeningExplorer extends ConsumerWidget { DataColumn(label: Text('Games')), DataColumn(label: Text('White / Draw / Black')), ], - rows: explorer.moves == null + rows: masterDatabase.moves == null ? [] - : explorer.moves! + : masterDatabase.moves! .map( (move) => DataRow( cells: [ DataCell(Text(move.san)), DataCell( Text( - '${((move.games / explorer.games) * 100).round()}% / ${move.games}', + '${((move.games / masterDatabase.games) * 100).round()}% / ${move.games}', ), ), DataCell( From 4f228f9bc049bed9b4c0cd508e87cf0dde062b09 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Sat, 6 Jul 2024 15:53:21 +0200 Subject: [PATCH 061/979] fix: make month nullable --- lib/src/model/analysis/opening_explorer.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/model/analysis/opening_explorer.dart b/lib/src/model/analysis/opening_explorer.dart index c27ae23137..361edb5324 100644 --- a/lib/src/model/analysis/opening_explorer.dart +++ b/lib/src/model/analysis/opening_explorer.dart @@ -58,7 +58,7 @@ class Game with _$Game { required MasterPlayer white, required MasterPlayer black, required int year, - required String month, + String? month, }) = _Game; factory Game.fromJson(Map json) => @@ -74,7 +74,7 @@ class GameWithMove with _$GameWithMove { required MasterPlayer white, required MasterPlayer black, required int year, - required String month, + String? month, }) = _GameWithMove; factory GameWithMove.fromJson(Map json) => From 328acbe554c683b3b223735db4562d4048316512 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Sat, 6 Jul 2024 17:19:33 +0200 Subject: [PATCH 062/979] fix: remove percentage graph dynamic width It causes weird display issues, most likely because it's in a table --- lib/src/view/analysis/analysis_screen.dart | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index 910c333d10..cdfb6c06a0 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -388,8 +388,7 @@ class _OpeningExplorer extends ConsumerWidget { child: SingleChildScrollView( scrollDirection: Axis.vertical, child: DataTable( - columnSpacing: 0, - horizontalMargin: 0, + columnSpacing: 5, columns: const [ DataColumn(label: Text('Move')), DataColumn(label: Text('Games')), @@ -457,11 +456,10 @@ class _WinPercentageChart extends StatelessWidget { children: [ if (percentWhite != 0) Expanded( - flex: white, child: ColoredBox( color: Colors.white, child: Text( - percentWhite < 5 ? '' : '${percentGames(white)}%', + percentWhite < 5 ? '' : '$percentWhite%', textAlign: TextAlign.center, style: const TextStyle(color: Colors.black), ), @@ -469,11 +467,10 @@ class _WinPercentageChart extends StatelessWidget { ), if (percentDraws != 0) Expanded( - flex: draws, child: ColoredBox( color: Colors.grey, child: Text( - percentDraws < 5 ? '' : '${percentGames(draws)}%', + percentDraws < 5 ? '' : '$percentDraws%', textAlign: TextAlign.center, style: const TextStyle(color: Colors.white), ), @@ -481,11 +478,10 @@ class _WinPercentageChart extends StatelessWidget { ), if (percentBlack != 0) Expanded( - flex: black, child: ColoredBox( color: Colors.black, child: Text( - percentBlack < 5 ? '' : '${percentGames(black)}%', + percentBlack < 5 ? '' : '$percentBlack%', textAlign: TextAlign.center, style: const TextStyle(color: Colors.white), ), From 795c6fdf5bc57dafe66f14ecfa94b9574c60f450 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Sat, 6 Jul 2024 21:04:47 +0200 Subject: [PATCH 063/979] feat: show message if no moves found --- lib/src/model/analysis/opening_explorer.dart | 4 +- lib/src/view/analysis/analysis_screen.dart | 87 +++++++++++--------- 2 files changed, 50 insertions(+), 41 deletions(-) diff --git a/lib/src/model/analysis/opening_explorer.dart b/lib/src/model/analysis/opening_explorer.dart index 361edb5324..3019d10b4a 100644 --- a/lib/src/model/analysis/opening_explorer.dart +++ b/lib/src/model/analysis/opening_explorer.dart @@ -14,8 +14,8 @@ class OpeningExplorer with _$OpeningExplorer { required int white, required int draws, required int black, - IList? moves, - IList? topGames, + required IList moves, + required IList topGames, IList? recentGames, IList? history, }) = _OpeningExplorer; diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index cdfb6c06a0..8dee5fc7df 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -380,47 +380,56 @@ class _OpeningExplorer extends ConsumerWidget { return masterDatabaseAsync.when( data: (masterDatabase) { - return Expanded( - child: Container( - width: MediaQuery.of(context).size.width, - padding: - const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), - child: SingleChildScrollView( - scrollDirection: Axis.vertical, - child: DataTable( - columnSpacing: 5, - columns: const [ - DataColumn(label: Text('Move')), - DataColumn(label: Text('Games')), - DataColumn(label: Text('White / Draw / Black')), - ], - rows: masterDatabase.moves == null - ? [] - : masterDatabase.moves! - .map( - (move) => DataRow( - cells: [ - DataCell(Text(move.san)), - DataCell( - Text( - '${((move.games / masterDatabase.games) * 100).round()}% / ${move.games}', + return masterDatabase.moves.isEmpty + ? const Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('No game found'), + ], + ), + ) + : Expanded( + child: Container( + width: MediaQuery.of(context).size.width, + padding: const EdgeInsets.symmetric( + vertical: 8.0, + horizontal: 16.0, + ), + child: SingleChildScrollView( + scrollDirection: Axis.vertical, + child: DataTable( + columnSpacing: 5, + columns: const [ + DataColumn(label: Text('Move')), + DataColumn(label: Text('Games')), + DataColumn(label: Text('White / Draw / Black')), + ], + rows: masterDatabase.moves + .map( + (move) => DataRow( + cells: [ + DataCell(Text(move.san)), + DataCell( + Text( + '${((move.games / masterDatabase.games) * 100).round()}% / ${move.games}', + ), ), - ), - DataCell( - _WinPercentageChart( - white: move.white, - draws: move.draws, - black: move.black, + DataCell( + _WinPercentageChart( + white: move.white, + draws: move.draws, + black: move.black, + ), ), - ), - ], - ), - ) - .toList(), - ), - ), - ), - ); + ], + ), + ) + .toList(), + ), + ), + ), + ); }, loading: () => const Center( child: CircularProgressIndicator(), From 99959d2d2eef9e4ec6619529c14604ed77592417 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Sat, 6 Jul 2024 21:12:40 +0200 Subject: [PATCH 064/979] feat: do not make request if game long past opening --- lib/src/view/analysis/analysis_screen.dart | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index 8dee5fc7df..868ceaa967 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -354,7 +354,7 @@ class _Body extends ConsumerWidget { ), ), if (showOpeningExplorer) - _OpeningExplorer(fen: position.fen), + _OpeningExplorer(position: position), ], ); }, @@ -369,14 +369,26 @@ class _Body extends ConsumerWidget { class _OpeningExplorer extends ConsumerWidget { const _OpeningExplorer({ - required this.fen, + required this.position, }); - final String fen; + final Position position; @override Widget build(BuildContext context, WidgetRef ref) { - final masterDatabaseAsync = ref.watch(masterDatabaseProvider(fen: fen)); + if (position.fullmoves > 24) { + return const Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('Max depth reached'), + ], + ), + ); + } + + final masterDatabaseAsync = + ref.watch(masterDatabaseProvider(fen: position.fen)); return masterDatabaseAsync.when( data: (masterDatabase) { From b1ce902d9c06f9612c773cffec04efd79c5722f0 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Sat, 6 Jul 2024 21:23:52 +0200 Subject: [PATCH 065/979] feat: play move on tap row --- lib/src/view/analysis/analysis_screen.dart | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index 868ceaa967..cb38758a42 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -354,7 +354,10 @@ class _Body extends ConsumerWidget { ), ), if (showOpeningExplorer) - _OpeningExplorer(position: position), + _OpeningExplorer( + position: position, + ctrlProvider: ctrlProvider, + ), ], ); }, @@ -370,9 +373,11 @@ class _Body extends ConsumerWidget { class _OpeningExplorer extends ConsumerWidget { const _OpeningExplorer({ required this.position, + required this.ctrlProvider, }); final Position position; + final AnalysisControllerProvider ctrlProvider; @override Widget build(BuildContext context, WidgetRef ref) { @@ -411,6 +416,7 @@ class _OpeningExplorer extends ConsumerWidget { child: SingleChildScrollView( scrollDirection: Axis.vertical, child: DataTable( + showCheckboxColumn: false, columnSpacing: 5, columns: const [ DataColumn(label: Text('Move')), @@ -420,6 +426,9 @@ class _OpeningExplorer extends ConsumerWidget { rows: masterDatabase.moves .map( (move) => DataRow( + onSelectChanged: (_) => ref + .read(ctrlProvider.notifier) + .onUserMove(Move.fromUci(move.uci)!), cells: [ DataCell(Text(move.san)), DataCell( From 7e3ec6bdd251c296ec6579cbdd3c710feb74d8fc Mon Sep 17 00:00:00 2001 From: Mauritz Date: Sat, 6 Jul 2024 21:41:52 +0200 Subject: [PATCH 066/979] feat: add summary row at the bottom --- lib/src/view/analysis/analysis_screen.dart | 59 ++++++++++++++-------- 1 file changed, 38 insertions(+), 21 deletions(-) diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index cb38758a42..e54c849e00 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -423,30 +423,47 @@ class _OpeningExplorer extends ConsumerWidget { DataColumn(label: Text('Games')), DataColumn(label: Text('White / Draw / Black')), ], - rows: masterDatabase.moves - .map( - (move) => DataRow( - onSelectChanged: (_) => ref - .read(ctrlProvider.notifier) - .onUserMove(Move.fromUci(move.uci)!), - cells: [ - DataCell(Text(move.san)), - DataCell( - Text( - '${((move.games / masterDatabase.games) * 100).round()}% / ${move.games}', - ), + rows: [ + ...masterDatabase.moves.map( + (move) => DataRow( + onSelectChanged: (_) => ref + .read(ctrlProvider.notifier) + .onUserMove(Move.fromUci(move.uci)!), + cells: [ + DataCell(Text(move.san)), + DataCell( + Text( + '${((move.games / masterDatabase.games) * 100).round()}% / ${move.games}', ), - DataCell( - _WinPercentageChart( - white: move.white, - draws: move.draws, - black: move.black, - ), + ), + DataCell( + _WinPercentageChart( + white: move.white, + draws: move.draws, + black: move.black, ), - ], + ), + ], + ), + ), + DataRow( + cells: [ + const DataCell(Icon(Icons.functions)), + DataCell( + Text( + '100% / ${masterDatabase.games}', + ), + ), + DataCell( + _WinPercentageChart( + white: masterDatabase.white, + draws: masterDatabase.draws, + black: masterDatabase.black, + ), ), - ) - .toList(), + ], + ), + ], ), ), ), From 837781a3bbe98d1a55c5c1a57afbabba7bc7b146 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Sat, 6 Jul 2024 21:54:00 +0200 Subject: [PATCH 067/979] feat: format numbers --- lib/src/view/analysis/analysis_screen.dart | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index e54c849e00..467331f15f 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -9,6 +9,7 @@ import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/account/account_preferences.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; @@ -379,6 +380,8 @@ class _OpeningExplorer extends ConsumerWidget { final Position position; final AnalysisControllerProvider ctrlProvider; + String formatNum(int num) => NumberFormat.decimalPatternDigits().format(num); + @override Widget build(BuildContext context, WidgetRef ref) { if (position.fullmoves > 24) { @@ -433,7 +436,7 @@ class _OpeningExplorer extends ConsumerWidget { DataCell(Text(move.san)), DataCell( Text( - '${((move.games / masterDatabase.games) * 100).round()}% / ${move.games}', + '${((move.games / masterDatabase.games) * 100).round()}% / ${formatNum(move.games)}', ), ), DataCell( @@ -451,7 +454,7 @@ class _OpeningExplorer extends ConsumerWidget { const DataCell(Icon(Icons.functions)), DataCell( Text( - '100% / ${masterDatabase.games}', + '100% / ${formatNum(masterDatabase.games)}', ), ), DataCell( From 3b874ac879151e134e19da2f4d08b265c8b9a583 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Sat, 6 Jul 2024 21:57:16 +0200 Subject: [PATCH 068/979] feat: no horizontal margin in table --- lib/src/view/analysis/analysis_screen.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index 467331f15f..cbcad01889 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -421,6 +421,7 @@ class _OpeningExplorer extends ConsumerWidget { child: DataTable( showCheckboxColumn: false, columnSpacing: 5, + horizontalMargin: 0, columns: const [ DataColumn(label: Text('Move')), DataColumn(label: Text('Games')), From 88a2b12231e129a6847e8c357d116af35ec6a8e9 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Sat, 6 Jul 2024 22:27:33 +0200 Subject: [PATCH 069/979] fix: do not show by default --- lib/src/model/analysis/analysis_preferences.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/model/analysis/analysis_preferences.dart b/lib/src/model/analysis/analysis_preferences.dart index cd2724e9b9..8e1e380284 100644 --- a/lib/src/model/analysis/analysis_preferences.dart +++ b/lib/src/model/analysis/analysis_preferences.dart @@ -122,7 +122,7 @@ class AnalysisPrefState with _$AnalysisPrefState { showBestMoveArrow: true, showAnnotations: true, showPgnComments: true, - showOpeningExplorer: true, + showOpeningExplorer: false, numEvalLines: 2, numEngineCores: defaultEngineCores, ); From 40e91060e2c8e09a212859ef63773eba23fc3381 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Sat, 6 Jul 2024 22:33:26 +0200 Subject: [PATCH 070/979] fix: format --- lib/src/model/analysis/opening_explorer.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/src/model/analysis/opening_explorer.dart b/lib/src/model/analysis/opening_explorer.dart index 3019d10b4a..48f63e8982 100644 --- a/lib/src/model/analysis/opening_explorer.dart +++ b/lib/src/model/analysis/opening_explorer.dart @@ -61,8 +61,7 @@ class Game with _$Game { String? month, }) = _Game; - factory Game.fromJson(Map json) => - _$GameFromJson(json); + factory Game.fromJson(Map json) => _$GameFromJson(json); } @Freezed(fromJson: true) @@ -81,7 +80,6 @@ class GameWithMove with _$GameWithMove { _$GameWithMoveFromJson(json); } - @Freezed(fromJson: true) class MasterPlayer with _$MasterPlayer { const factory MasterPlayer({ From fd03f8f476af962de7a756021d4e464ac902e9b1 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Sat, 27 Jul 2024 10:02:28 +0200 Subject: [PATCH 071/979] feat: separate screen for opening explorer --- .../analysis/opening_explorer_screen.dart | 136 ++++++++++++++++++ lib/src/view/tools/tools_tab_screen.dart | 35 +++++ 2 files changed, 171 insertions(+) create mode 100644 lib/src/view/analysis/opening_explorer_screen.dart diff --git a/lib/src/view/analysis/opening_explorer_screen.dart b/lib/src/view/analysis/opening_explorer_screen.dart new file mode 100644 index 0000000000..d12a05b1c7 --- /dev/null +++ b/lib/src/view/analysis/opening_explorer_screen.dart @@ -0,0 +1,136 @@ +import 'package:chessground/chessground.dart' as cg; +import 'package:dartchess/dartchess.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/constants.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; +import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/utils/chessground_compat.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/screen.dart'; +import 'package:lichess_mobile/src/widgets/platform.dart'; + +class OpeningExplorerScreen extends StatelessWidget { + const OpeningExplorerScreen({required this.pgn, required this.options}); + + final String pgn; + final AnalysisOptions options; + + @override + Widget build(BuildContext context) { + return PlatformWidget( + androidBuilder: _androidBuilder, + iosBuilder: _iosBuilder, + ); + } + + Widget _androidBuilder(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(context.l10n.openingExplorer), + ), + body: _Body(pgn: pgn, options: options), + ); + } + + Widget _iosBuilder(BuildContext context) { + return CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + backgroundColor: Styles.cupertinoScaffoldColor.resolveFrom(context), + border: null, + middle: Text(context.l10n.openingExplorer), + ), + child: _Body(pgn: pgn, options: options), + ); + } +} + +class _Body extends ConsumerWidget { + const _Body({ + required this.pgn, + required this.options, + }); + + final String pgn; + final AnalysisOptions options; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return SafeArea( + bottom: false, + child: LayoutBuilder( + builder: (context, constraints) { + final defaultBoardSize = constraints.biggest.shortestSide; + final isTablet = isTabletOrLarger(context); + final remainingHeight = constraints.maxHeight - defaultBoardSize; + final isSmallScreen = + remainingHeight < kSmallRemainingHeightLeftBoardThreshold; + final boardSize = isTablet || isSmallScreen + ? defaultBoardSize - kTabletBoardTableSidePadding * 2 + : defaultBoardSize; + return _Board(pgn, options, boardSize, isTablet: isTablet); + }, + ), + ); + } +} + +class _Board extends ConsumerStatefulWidget { + const _Board( + this.pgn, + this.options, + this.boardSize, { + required this.isTablet, + }); + + final String pgn; + final AnalysisOptions options; + final double boardSize; + final bool isTablet; + + @override + ConsumerState<_Board> createState() => _BoardState(); +} + +class _BoardState extends ConsumerState<_Board> { + @override + Widget build(BuildContext context) { + final ctrlProvider = analysisControllerProvider(widget.pgn, widget.options); + final analysisState = ref.watch(ctrlProvider); + final boardPrefs = ref.watch(boardPreferencesProvider); + + return cg.Board( + size: widget.boardSize, + onMove: (move, {isDrop, isPremove}) => + ref.read(ctrlProvider.notifier).onUserMove(Move.fromUci(move.uci)!), + data: cg.BoardData( + orientation: analysisState.pov.cg, + interactableSide: analysisState.position.isGameOver + ? cg.InteractableSide.none + : analysisState.position.turn == Side.white + ? cg.InteractableSide.white + : cg.InteractableSide.black, + fen: analysisState.position.fen, + isCheck: boardPrefs.boardHighlights && analysisState.position.isCheck, + lastMove: analysisState.lastMove?.cg, + sideToMove: analysisState.position.turn.cg, + validMoves: analysisState.validMoves, + ), + settings: cg.BoardSettings( + pieceAssets: boardPrefs.pieceSet.assets, + colorScheme: boardPrefs.boardTheme.colors, + showValidMoves: boardPrefs.showLegalMoves, + showLastMove: boardPrefs.boardHighlights, + enableCoordinates: boardPrefs.coordinates, + animationDuration: boardPrefs.pieceAnimationDuration, + borderRadius: widget.isTablet + ? const BorderRadius.all(Radius.circular(4.0)) + : BorderRadius.zero, + boxShadow: widget.isTablet ? boardShadows : const [], + pieceShiftMethod: boardPrefs.pieceShiftMethod, + ), + ); + } +} diff --git a/lib/src/view/tools/tools_tab_screen.dart b/lib/src/view/tools/tools_tab_screen.dart index 689f3bd60c..effd6f79f8 100644 --- a/lib/src/view/tools/tools_tab_screen.dart +++ b/lib/src/view/tools/tools_tab_screen.dart @@ -10,6 +10,7 @@ import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_position_choice_screen.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; +import 'package:lichess_mobile/src/view/analysis/opening_explorer_screen.dart'; import 'package:lichess_mobile/src/view/clock/clock_screen.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; @@ -107,6 +108,40 @@ class _Body extends StatelessWidget { ), ), ), + Padding( + padding: Theme.of(context).platform == TargetPlatform.android + ? const EdgeInsets.only(bottom: 16.0) + : EdgeInsets.zero, + child: PlatformListTile( + leading: Icon( + Icons.explore, + size: Styles.mainListTileIconSize, + color: Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoTheme.of(context).primaryColor + : Theme.of(context).colorScheme.primary, + ), + title: Padding( + padding: tilePadding, + child: Text(context.l10n.openingExplorer, style: Styles.callout), + ), + trailing: Theme.of(context).platform == TargetPlatform.iOS + ? const CupertinoListTileChevron() + : null, + onTap: () => pushPlatformRoute( + context, + rootNavigator: true, + builder: (context) => const OpeningExplorerScreen( + pgn: '', + options: AnalysisOptions( + isLocalEvaluationAllowed: false, + variant: Variant.standard, + orientation: Side.white, + id: standaloneAnalysisId, + ), + ), + ), + ), + ), Padding( padding: Theme.of(context).platform == TargetPlatform.android ? const EdgeInsets.only(bottom: 16.0) From a116f735176c423fd068030f870c2105f31f4ae0 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Sun, 28 Jul 2024 15:25:58 +0200 Subject: [PATCH 072/979] feat: use same board widget for analysis screen and opening explorer --- .../model/analysis/analysis_preferences.dart | 10 - lib/src/view/analysis/analysis_board.dart | 199 ++ lib/src/view/analysis/analysis_screen.dart | 2127 +++++++---------- lib/src/view/analysis/analysis_settings.dart | 7 - .../analysis/opening_explorer_screen.dart | 322 ++- 5 files changed, 1333 insertions(+), 1332 deletions(-) create mode 100644 lib/src/view/analysis/analysis_board.dart diff --git a/lib/src/model/analysis/analysis_preferences.dart b/lib/src/model/analysis/analysis_preferences.dart index 8e1e380284..4cfc0e1daa 100644 --- a/lib/src/model/analysis/analysis_preferences.dart +++ b/lib/src/model/analysis/analysis_preferences.dart @@ -56,14 +56,6 @@ class AnalysisPreferences extends _$AnalysisPreferences { ); } - Future toggleOpeningExplorer() { - return _save( - state.copyWith( - showOpeningExplorer: !state.showOpeningExplorer, - ), - ); - } - Future toggleShowBestMoveArrow() { return _save( state.copyWith( @@ -110,7 +102,6 @@ class AnalysisPrefState with _$AnalysisPrefState { required bool showBestMoveArrow, required bool showAnnotations, required bool showPgnComments, - required bool showOpeningExplorer, @Assert('numEvalLines >= 1 && numEvalLines <= 3') required int numEvalLines, @Assert('numEngineCores >= 1 && numEngineCores <= maxEngineCores') required int numEngineCores, @@ -122,7 +113,6 @@ class AnalysisPrefState with _$AnalysisPrefState { showBestMoveArrow: true, showAnnotations: true, showPgnComments: true, - showOpeningExplorer: false, numEvalLines: 2, numEngineCores: defaultEngineCores, ); diff --git a/lib/src/view/analysis/analysis_board.dart b/lib/src/view/analysis/analysis_board.dart new file mode 100644 index 0000000000..c7592d6dbe --- /dev/null +++ b/lib/src/view/analysis/analysis_board.dart @@ -0,0 +1,199 @@ +import 'dart:math' as math; +import 'dart:ui'; + +import 'package:chessground/chessground.dart' as cg; +import 'package:collection/collection.dart'; +import 'package:dartchess/dartchess.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/constants.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; +import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/common/eval.dart'; +import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; +import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; +import 'package:lichess_mobile/src/utils/chessground_compat.dart'; +import 'package:lichess_mobile/src/view/analysis/annotations.dart'; + +class AnalysisBoard extends ConsumerStatefulWidget { + final String pgn; + + final AnalysisOptions options; + final double boardSize; + final bool isTablet; + const AnalysisBoard( + this.pgn, + this.options, + this.boardSize, { + required this.isTablet, + }); + + @override + ConsumerState createState() => _BoardState(); +} + +class _BoardState extends ConsumerState { + ISet userShapes = ISet(); + + @override + Widget build(BuildContext context) { + final ctrlProvider = analysisControllerProvider(widget.pgn, widget.options); + final analysisState = ref.watch(ctrlProvider); + final boardPrefs = ref.watch(boardPreferencesProvider); + final showBestMoveArrow = ref.watch( + analysisPreferencesProvider.select( + (value) => value.showBestMoveArrow, + ), + ); + final showAnnotationsOnBoard = ref.watch( + analysisPreferencesProvider.select((value) => value.showAnnotations), + ); + + final evalBestMoves = ref.watch( + engineEvaluationProvider.select((s) => s.eval?.bestMoves), + ); + + final currentNode = analysisState.currentNode; + final annotation = makeAnnotation(currentNode.nags); + + final bestMoves = evalBestMoves ?? currentNode.eval?.bestMoves; + + final sanMove = currentNode.sanMove; + + final ISet bestMoveShapes = showBestMoveArrow && + analysisState.isEngineAvailable && + bestMoves != null + ? _computeBestMoveShapes(bestMoves) + : ISet(); + + return cg.Board( + size: widget.boardSize, + onMove: (move, {isDrop, isPremove}) => + ref.read(ctrlProvider.notifier).onUserMove(Move.fromUci(move.uci)!), + data: cg.BoardData( + orientation: analysisState.pov.cg, + interactableSide: analysisState.position.isGameOver + ? cg.InteractableSide.none + : analysisState.position.turn == Side.white + ? cg.InteractableSide.white + : cg.InteractableSide.black, + fen: analysisState.position.fen, + isCheck: boardPrefs.boardHighlights && analysisState.position.isCheck, + lastMove: analysisState.lastMove?.cg, + sideToMove: analysisState.position.turn.cg, + validMoves: analysisState.validMoves, + shapes: userShapes.union(bestMoveShapes), + annotations: + showAnnotationsOnBoard && sanMove != null && annotation != null + ? altCastles.containsKey(sanMove.move.uci) + ? IMap({ + Move.fromUci(altCastles[sanMove.move.uci]!)!.cg.to: + annotation, + }) + : IMap({sanMove.move.cg.to: annotation}) + : null, + ), + settings: cg.BoardSettings( + pieceAssets: boardPrefs.pieceSet.assets, + colorScheme: boardPrefs.boardTheme.colors, + showValidMoves: boardPrefs.showLegalMoves, + showLastMove: boardPrefs.boardHighlights, + enableCoordinates: boardPrefs.coordinates, + animationDuration: boardPrefs.pieceAnimationDuration, + borderRadius: widget.isTablet + ? const BorderRadius.all(Radius.circular(4.0)) + : BorderRadius.zero, + boxShadow: widget.isTablet ? boardShadows : const [], + drawShape: cg.DrawShapeOptions( + enable: true, + onCompleteShape: _onCompleteShape, + onClearShapes: _onClearShapes, + ), + pieceShiftMethod: boardPrefs.pieceShiftMethod, + ), + ); + } + + ISet _computeBestMoveShapes(IList moves) { + // Scale down all moves with index > 0 based on how much worse their winning chances are compared to the best move + // (assume moves are ordered by their winning chances, so index==0 is the best move) + double scaleArrowAgainstBestMove(int index) { + const minScale = 0.15; + const maxScale = 1.0; + const winningDiffScaleFactor = 2.5; + + final bestMove = moves[0]; + final winningDiffComparedToBestMove = + bestMove.winningChances - moves[index].winningChances; + // Force minimum scale if the best move is significantly better than this move + if (winningDiffComparedToBestMove > 0.3) { + return minScale; + } + return clampDouble( + math.max( + minScale, + maxScale - winningDiffScaleFactor * winningDiffComparedToBestMove, + ), + 0, + 1, + ); + } + + return ISet( + moves.mapIndexed( + (i, m) { + final move = m.move; + // Same colors as in the Web UI with a slightly different opacity + // The best move has a different color than the other moves + final color = Color((i == 0) ? 0x66003088 : 0x664A4A4A); + switch (move) { + case NormalMove(from: _, to: _, promotion: final promRole): + return [ + cg.Arrow( + color: color, + orig: move.cg.from, + dest: move.cg.to, + scale: scaleArrowAgainstBestMove(i), + ), + if (promRole != null) + cg.PieceShape( + color: color, + orig: move.cg.to, + role: promRole.cg, + ), + ]; + case DropMove(role: final role, to: _): + return [ + cg.PieceShape( + color: color, + orig: move.cg.to, + role: role.cg, + ), + ]; + } + }, + ).expand((e) => e), + ); + } + + void _onClearShapes() { + setState(() { + userShapes = ISet(); + }); + } + + void _onCompleteShape(cg.Shape shape) { + if (userShapes.any((element) => element == shape)) { + setState(() { + userShapes = userShapes.remove(shape); + }); + return; + } else { + setState(() { + userShapes = userShapes.add(shape); + }); + } + } +} diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index cbcad01889..ac5fde5e11 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -1,7 +1,5 @@ import 'dart:math' as math; -import 'dart:ui'; -import 'package:chessground/chessground.dart' as cg; import 'package:collection/collection.dart'; import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; @@ -9,12 +7,10 @@ import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/account/account_preferences.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; -import 'package:lichess_mobile/src/model/analysis/opening_explorer_repository.dart'; import 'package:lichess_mobile/src/model/analysis/server_analysis_service.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; @@ -25,11 +21,9 @@ import 'package:lichess_mobile/src/model/engine/engine.dart'; import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; import 'package:lichess_mobile/src/model/game/game_repository_providers.dart'; import 'package:lichess_mobile/src/model/game/game_share_service.dart'; -import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/model/settings/brightness.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/chessground_compat.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; @@ -45,18 +39,192 @@ import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:popover/popover.dart'; import '../../utils/share.dart'; +import 'analysis_board.dart'; import 'analysis_settings.dart'; import 'analysis_share_screen.dart'; -import 'annotations.dart'; import 'tree_view.dart'; -class AnalysisScreen extends StatelessWidget { - const AnalysisScreen({ - required this.options, - required this.pgnOrId, - this.title, - }); +class AcplChart extends ConsumerWidget { + final String pgn; + + final AnalysisOptions options; + const AcplChart(this.pgn, this.options); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final mainLineColor = Theme.of(context).colorScheme.secondary; + // yes it looks like below/above are inverted in fl_chart + final brightness = Theme.of(context).brightness; + final white = Theme.of(context).colorScheme.surfaceContainerHighest; + final black = Theme.of(context).colorScheme.outline; + // yes it looks like below/above are inverted in fl_chart + final belowLineColor = brightness == Brightness.light ? white : black; + final aboveLineColor = brightness == Brightness.light ? black : white; + + VerticalLine phaseVerticalBar(double x, String label) => VerticalLine( + x: x, + color: const Color(0xFF707070), + strokeWidth: 0.5, + label: VerticalLineLabel( + style: TextStyle( + fontSize: 10, + color: Theme.of(context) + .textTheme + .labelMedium + ?.color + ?.withOpacity(0.3), + ), + labelResolver: (line) => label, + padding: const EdgeInsets.only(right: 1), + alignment: Alignment.topRight, + direction: LabelDirection.vertical, + show: true, + ), + ); + + final data = ref.watch( + analysisControllerProvider(pgn, options) + .select((value) => value.acplChartData), + ); + + final rootPly = ref.watch( + analysisControllerProvider(pgn, options) + .select((value) => value.root.position.ply), + ); + + final currentNode = ref.watch( + analysisControllerProvider(pgn, options) + .select((value) => value.currentNode), + ); + + final isOnMainline = ref.watch( + analysisControllerProvider(pgn, options) + .select((value) => value.isOnMainline), + ); + + if (data == null) { + return const SizedBox.shrink(); + } + + final spots = data + .mapIndexed( + (i, e) => FlSpot(i.toDouble(), e.winningChances(Side.white)), + ) + .toList(growable: false); + + final divisionLines = []; + + if (options.division?.middlegame != null) { + if (options.division!.middlegame! > 0) { + divisionLines.add(phaseVerticalBar(0.0, context.l10n.opening)); + divisionLines.add( + phaseVerticalBar( + options.division!.middlegame! - 1, + context.l10n.middlegame, + ), + ); + } else { + divisionLines.add(phaseVerticalBar(0.0, context.l10n.middlegame)); + } + } + + if (options.division?.endgame != null) { + if (options.division!.endgame! > 0) { + divisionLines.add( + phaseVerticalBar( + options.division!.endgame! - 1, + context.l10n.endgame, + ), + ); + } else { + divisionLines.add( + phaseVerticalBar( + 0.0, + context.l10n.endgame, + ), + ); + } + } + return Center( + child: AspectRatio( + aspectRatio: 2.5, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: LineChart( + LineChartData( + lineTouchData: LineTouchData( + enabled: false, + touchCallback: + (FlTouchEvent event, LineTouchResponse? touchResponse) { + if (event is FlTapDownEvent || + event is FlPanUpdateEvent || + event is FlLongPressMoveUpdate) { + final touchX = event.localPosition!.dx; + final chartWidth = context.size!.width - + 32; // Insets on both sides of the chart of 16 + final minX = spots.first.x; + final maxX = spots.last.x; + final touchXDataValue = + minX + (touchX / chartWidth) * (maxX - minX); + final closestSpot = spots.reduce( + (a, b) => (a.x - touchXDataValue).abs() < + (b.x - touchXDataValue).abs() + ? a + : b, + ); + final closestNodeIndex = closestSpot.x.round(); + ref + .read(analysisControllerProvider(pgn, options).notifier) + .jumpToNthNodeOnMainline(closestNodeIndex); + } + }, + ), + minY: -1.0, + maxY: 1.0, + lineBarsData: [ + LineChartBarData( + spots: spots, + isCurved: false, + barWidth: 1, + color: mainLineColor.withOpacity(0.7), + aboveBarData: BarAreaData( + show: true, + color: aboveLineColor, + applyCutOffY: true, + ), + belowBarData: BarAreaData( + show: true, + color: belowLineColor, + applyCutOffY: true, + ), + dotData: const FlDotData( + show: false, + ), + ), + ], + extraLinesData: ExtraLinesData( + verticalLines: [ + if (isOnMainline) + VerticalLine( + x: (currentNode.position.ply - 1 - rootPly).toDouble(), + color: mainLineColor, + strokeWidth: 1.0, + ), + ...divisionLines, + ], + ), + gridData: const FlGridData(show: false), + borderData: FlBorderData(show: false), + titlesData: const FlTitlesData(show: false), + ), + ), + ), + ), + ); + } +} +class AnalysisScreen extends StatelessWidget { /// The analysis options. final AnalysisOptions options; @@ -65,6 +233,12 @@ class AnalysisScreen extends StatelessWidget { final String? title; + const AnalysisScreen({ + required this.options, + required this.pgnOrId, + this.title, + }); + @override Widget build(BuildContext context) { return pgnOrId.length == 8 && GameId(pgnOrId).isValid @@ -77,870 +251,397 @@ class AnalysisScreen extends StatelessWidget { } } -class _LoadGame extends ConsumerWidget { - const _LoadGame(this.gameId, this.options, this.title); - - final AnalysisOptions options; - final GameId gameId; - final String? title; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final gameAsync = ref.watch(archivedGameProvider(id: gameId)); - - return gameAsync.when( - data: (game) { - final serverAnalysis = - game.white.analysis != null && game.black.analysis != null - ? (white: game.white.analysis!, black: game.black.analysis!) - : null; - return _LoadedAnalysisScreen( - options: options.copyWith( - id: game.id, - opening: game.meta.opening, - division: game.meta.division, - serverAnalysis: serverAnalysis, - ), - pgn: game.makePgn(), - title: title, - ); - }, - loading: () => const Center(child: CircularProgressIndicator.adaptive()), - error: (error, _) { - return Center( - child: Text('Cannot load game analysis: $error'), - ); - }, - ); - } -} - -class _LoadedAnalysisScreen extends ConsumerWidget { - const _LoadedAnalysisScreen({ - required this.options, - required this.pgn, - this.title, - }); - - final AnalysisOptions options; +class ServerAnalysisSummary extends ConsumerWidget { final String pgn; - final String? title; + final AnalysisOptions options; + const ServerAnalysisSummary(this.pgn, this.options); @override Widget build(BuildContext context, WidgetRef ref) { - return ConsumerPlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, - ref: ref, - ); - } - - Widget _androidBuilder(BuildContext context, WidgetRef ref) { final ctrlProvider = analysisControllerProvider(pgn, options); + final playersAnalysis = + ref.watch(ctrlProvider.select((value) => value.playersAnalysis)); + final pgnHeaders = + ref.watch(ctrlProvider.select((value) => value.pgnHeaders)); + final currentGameAnalysis = ref.watch(currentAnalysisProvider); - return Scaffold( - resizeToAvoidBottomInset: false, - appBar: AppBar( - title: _Title(options: options, title: title), - actions: [ - _EngineDepth(ctrlProvider), - AppBarIconButton( - onPressed: () => showAdaptiveBottomSheet( - context: context, - isScrollControlled: true, - showDragHandle: true, - isDismissible: true, - builder: (_) => AnalysisSettings(pgn, options), - ), - semanticsLabel: context.l10n.settingsSettings, - icon: const Icon(Icons.settings), - ), - ], - ), - body: _Body(pgn: pgn, options: options), - ); - } - - Widget _iosBuilder(BuildContext context, WidgetRef ref) { - final ctrlProvider = analysisControllerProvider(pgn, options); - - return CupertinoPageScaffold( - resizeToAvoidBottomInset: false, - navigationBar: CupertinoNavigationBar( - backgroundColor: Styles.cupertinoScaffoldColor.resolveFrom(context), - border: null, - padding: Styles.cupertinoAppBarTrailingWidgetPadding, - middle: _Title(options: options, title: title), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - _EngineDepth(ctrlProvider), - AppBarIconButton( - onPressed: () => showAdaptiveBottomSheet( - context: context, - isScrollControlled: true, - showDragHandle: true, - isDismissible: true, - builder: (_) => AnalysisSettings(pgn, options), - ), - semanticsLabel: context.l10n.settingsSettings, - icon: const Icon(Icons.settings), - ), - ], - ), - ), - child: _Body(pgn: pgn, options: options), - ); - } -} - -class _Title extends StatelessWidget { - const _Title({ - required this.options, - this.title, - }); - final AnalysisOptions options; - final String? title; - - @override - Widget build(BuildContext context) { - return title != null - ? Text(title!) - : Row( - mainAxisSize: MainAxisSize.min, + return playersAnalysis != null + ? ListView( children: [ - if (options.variant != Variant.standard) ...[ - Icon(options.variant.icon), - const SizedBox(width: 5.0), - ], - Text(context.l10n.analysis), - ], - ); - } -} - -class _Body extends ConsumerWidget { - const _Body({required this.pgn, required this.options}); - - final String pgn; - final AnalysisOptions options; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final ctrlProvider = analysisControllerProvider(pgn, options); - final showEvaluationGauge = ref.watch( - analysisPreferencesProvider.select((value) => value.showEvaluationGauge), - ); - final showOpeningExplorer = ref.watch( - analysisPreferencesProvider.select((value) => value.showOpeningExplorer), - ); - - final isEngineAvailable = ref.watch( - ctrlProvider.select( - (value) => value.isEngineAvailable, - ), - ); - - final hasEval = - ref.watch(ctrlProvider.select((value) => value.hasAvailableEval)); - - final showAnalysisSummary = ref.watch( - ctrlProvider.select((value) => value.displayMode == DisplayMode.summary), - ); - - final position = ref.watch(ctrlProvider.select((value) => value.position)); - - return Column( - children: [ - Expanded( - child: SafeArea( - bottom: false, - child: LayoutBuilder( - builder: (context, constraints) { - final aspectRatio = constraints.biggest.aspectRatio; - final defaultBoardSize = constraints.biggest.shortestSide; - final isTablet = isTabletOrLarger(context); - final remainingHeight = - constraints.maxHeight - defaultBoardSize; - final isSmallScreen = - remainingHeight < kSmallRemainingHeightLeftBoardThreshold; - final boardSize = isTablet || isSmallScreen - ? defaultBoardSize - kTabletBoardTableSidePadding * 2 - : defaultBoardSize; - - return aspectRatio > 1 - ? Row( - mainAxisSize: MainAxisSize.max, - children: [ - Padding( - padding: const EdgeInsets.only( - left: kTabletBoardTableSidePadding, - top: kTabletBoardTableSidePadding, - bottom: kTabletBoardTableSidePadding, - ), - child: Row( - children: [ - _Board( - pgn, - options, - boardSize, - isTablet: isTablet, - ), - if (hasEval && showEvaluationGauge) ...[ - const SizedBox(width: 4.0), - _EngineGaugeVertical(ctrlProvider), - ], - ], + if (currentGameAnalysis == options.gameAnyId?.gameId) + const Padding( + padding: EdgeInsets.only(top: 16.0), + child: WaitingForServerAnalysis(), + ), + AcplChart(pgn, options), + Center( + child: SizedBox( + width: math.min(MediaQuery.sizeOf(context).width, 500), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Table( + defaultVerticalAlignment: + TableCellVerticalAlignment.middle, + columnWidths: const { + 0: FlexColumnWidth(1), + 1: FlexColumnWidth(1), + 2: FlexColumnWidth(1), + }, + children: [ + TableRow( + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide(color: Colors.grey), ), ), - Flexible( - fit: FlexFit.loose, - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - if (isEngineAvailable) - _EngineLines( - ctrlProvider, - isLandscape: true, - ), - Expanded( - child: PlatformCard( - margin: const EdgeInsets.all( - kTabletBoardTableSidePadding, - ), - semanticContainer: false, - child: showAnalysisSummary - ? ServerAnalysisSummary(pgn, options) - : AnalysisTreeView( - pgn, - options, - Orientation.landscape, - ), - ), + children: [ + _SummaryPlayerName(Side.white, pgnHeaders), + Center( + child: Text( + pgnHeaders.get('Result') ?? '', + style: const TextStyle( + fontWeight: FontWeight.bold, ), - ], + ), ), - ), - ], - ) - : Column( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - _ColumnTopTable(ctrlProvider), - if (isTablet) - Padding( - padding: const EdgeInsets.all( - kTabletBoardTableSidePadding, + _SummaryPlayerName(Side.black, pgnHeaders), + ], + ), + if (playersAnalysis.white.accuracy != null && + playersAnalysis.black.accuracy != null) + TableRow( + children: [ + _SummaryNumber( + '${playersAnalysis.white.accuracy}%', ), - child: _Board( - pgn, - options, - boardSize, - isTablet: isTablet, + Center( + heightFactor: 1.8, + child: Text( + context.l10n.accuracy, + softWrap: true, + ), ), - ) - else - _Board(pgn, options, boardSize, isTablet: isTablet), - if (showAnalysisSummary) - Expanded(child: ServerAnalysisSummary(pgn, options)) - else - Expanded( - child: AnalysisTreeView( - pgn, - options, - Orientation.portrait, + _SummaryNumber( + '${playersAnalysis.black.accuracy}%', ), - ), - if (showOpeningExplorer) - _OpeningExplorer( - position: position, - ctrlProvider: ctrlProvider, - ), - ], - ); - }, - ), - ), - ), - _BottomBar(pgn: pgn, options: options), - ], - ); - } -} - -class _OpeningExplorer extends ConsumerWidget { - const _OpeningExplorer({ - required this.position, - required this.ctrlProvider, - }); - - final Position position; - final AnalysisControllerProvider ctrlProvider; - - String formatNum(int num) => NumberFormat.decimalPatternDigits().format(num); - - @override - Widget build(BuildContext context, WidgetRef ref) { - if (position.fullmoves > 24) { - return const Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text('Max depth reached'), - ], - ), - ); - } - - final masterDatabaseAsync = - ref.watch(masterDatabaseProvider(fen: position.fen)); - - return masterDatabaseAsync.when( - data: (masterDatabase) { - return masterDatabase.moves.isEmpty - ? const Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text('No game found'), - ], - ), - ) - : Expanded( - child: Container( - width: MediaQuery.of(context).size.width, - padding: const EdgeInsets.symmetric( - vertical: 8.0, - horizontal: 16.0, - ), - child: SingleChildScrollView( - scrollDirection: Axis.vertical, - child: DataTable( - showCheckboxColumn: false, - columnSpacing: 5, - horizontalMargin: 0, - columns: const [ - DataColumn(label: Text('Move')), - DataColumn(label: Text('Games')), - DataColumn(label: Text('White / Draw / Black')), - ], - rows: [ - ...masterDatabase.moves.map( - (move) => DataRow( - onSelectChanged: (_) => ref - .read(ctrlProvider.notifier) - .onUserMove(Move.fromUci(move.uci)!), - cells: [ - DataCell(Text(move.san)), - DataCell( - Text( - '${((move.games / masterDatabase.games) * 100).round()}% / ${formatNum(move.games)}', + ], + ), + for (final item in [ + ( + playersAnalysis.white.inaccuracies.toString(), + context.l10n + .nbInaccuracies(2) + .replaceAll('2', '') + .trim() + .capitalize(), + playersAnalysis.black.inaccuracies.toString() + ), + ( + playersAnalysis.white.mistakes.toString(), + context.l10n + .nbMistakes(2) + .replaceAll('2', '') + .trim() + .capitalize(), + playersAnalysis.black.mistakes.toString() + ), + ( + playersAnalysis.white.blunders.toString(), + context.l10n + .nbBlunders(2) + .replaceAll('2', '') + .trim() + .capitalize(), + playersAnalysis.black.blunders.toString() + ), + ]) + TableRow( + children: [ + _SummaryNumber(item.$1), + Center( + heightFactor: 1.2, + child: Text( + item.$2, + softWrap: true, ), ), - DataCell( - _WinPercentageChart( - white: move.white, - draws: move.draws, - black: move.black, + _SummaryNumber(item.$3), + ], + ), + if (playersAnalysis.white.acpl != null && + playersAnalysis.black.acpl != null) + TableRow( + children: [ + _SummaryNumber( + playersAnalysis.white.acpl.toString(), + ), + Center( + heightFactor: 1.5, + child: Text( + context.l10n.averageCentipawnLoss, + softWrap: true, + textAlign: TextAlign.center, ), ), + _SummaryNumber( + playersAnalysis.black.acpl.toString(), + ), ], ), - ), - DataRow( - cells: [ - const DataCell(Icon(Icons.functions)), - DataCell( - Text( - '100% / ${formatNum(masterDatabase.games)}', - ), - ), - DataCell( - _WinPercentageChart( - white: masterDatabase.white, - draws: masterDatabase.draws, - black: masterDatabase.black, - ), - ), - ], - ), ], ), ), ), - ); - }, - loading: () => const Center( - child: CircularProgressIndicator(), - ), - error: (error, stackTrace) => Center( - child: Text(error.toString()), - ), - ); - } -} - -class _WinPercentageChart extends StatelessWidget { - const _WinPercentageChart({ - required this.white, - required this.draws, - required this.black, - }); - - final int white; - final int draws; - final int black; - - @override - Widget build(BuildContext context) { - int percentGames(int games) => - ((games / (white + draws + black)) * 100).round(); - - final percentWhite = percentGames(white); - final percentDraws = percentGames(draws); - final percentBlack = percentGames(black); - - return Row( - children: [ - if (percentWhite != 0) - Expanded( - child: ColoredBox( - color: Colors.white, - child: Text( - percentWhite < 5 ? '' : '$percentWhite%', - textAlign: TextAlign.center, - style: const TextStyle(color: Colors.black), - ), - ), - ), - if (percentDraws != 0) - Expanded( - child: ColoredBox( - color: Colors.grey, - child: Text( - percentDraws < 5 ? '' : '$percentDraws%', - textAlign: TextAlign.center, - style: const TextStyle(color: Colors.white), - ), - ), - ), - if (percentBlack != 0) - Expanded( - child: ColoredBox( - color: Colors.black, - child: Text( - percentBlack < 5 ? '' : '$percentBlack%', - textAlign: TextAlign.center, - style: const TextStyle(color: Colors.white), ), - ), - ), - ], - ); - } -} - -class _Board extends ConsumerStatefulWidget { - const _Board( - this.pgn, - this.options, - this.boardSize, { - required this.isTablet, - }); - - final String pgn; - final AnalysisOptions options; - final double boardSize; - final bool isTablet; - - @override - ConsumerState<_Board> createState() => _BoardState(); -} - -class _BoardState extends ConsumerState<_Board> { - ISet userShapes = ISet(); - - ISet _computeBestMoveShapes(IList moves) { - // Scale down all moves with index > 0 based on how much worse their winning chances are compared to the best move - // (assume moves are ordered by their winning chances, so index==0 is the best move) - double scaleArrowAgainstBestMove(int index) { - const minScale = 0.15; - const maxScale = 1.0; - const winningDiffScaleFactor = 2.5; - - final bestMove = moves[0]; - final winningDiffComparedToBestMove = - bestMove.winningChances - moves[index].winningChances; - // Force minimum scale if the best move is significantly better than this move - if (winningDiffComparedToBestMove > 0.3) { - return minScale; - } - return clampDouble( - math.max( - minScale, - maxScale - winningDiffScaleFactor * winningDiffComparedToBestMove, - ), - 0, - 1, - ); - } - - return ISet( - moves.mapIndexed( - (i, m) { - final move = m.move; - // Same colors as in the Web UI with a slightly different opacity - // The best move has a different color than the other moves - final color = Color((i == 0) ? 0x66003088 : 0x664A4A4A); - switch (move) { - case NormalMove(from: _, to: _, promotion: final promRole): - return [ - cg.Arrow( - color: color, - orig: move.cg.from, - dest: move.cg.to, - scale: scaleArrowAgainstBestMove(i), - ), - if (promRole != null) - cg.PieceShape( - color: color, - orig: move.cg.to, - role: promRole.cg, - ), - ]; - case DropMove(role: final role, to: _): - return [ - cg.PieceShape( - color: color, - orig: move.cg.to, - role: role.cg, - ), - ]; - } - }, - ).expand((e) => e), - ); - } - - @override - Widget build(BuildContext context) { - final ctrlProvider = analysisControllerProvider(widget.pgn, widget.options); - final analysisState = ref.watch(ctrlProvider); - final boardPrefs = ref.watch(boardPreferencesProvider); - final showBestMoveArrow = ref.watch( - analysisPreferencesProvider.select( - (value) => value.showBestMoveArrow, - ), - ); - final showAnnotationsOnBoard = ref.watch( - analysisPreferencesProvider.select((value) => value.showAnnotations), - ); - - final evalBestMoves = ref.watch( - engineEvaluationProvider.select((s) => s.eval?.bestMoves), - ); - - final currentNode = analysisState.currentNode; - final annotation = makeAnnotation(currentNode.nags); - - final bestMoves = evalBestMoves ?? currentNode.eval?.bestMoves; - - final sanMove = currentNode.sanMove; - - final ISet bestMoveShapes = showBestMoveArrow && - analysisState.isEngineAvailable && - bestMoves != null - ? _computeBestMoveShapes(bestMoves) - : ISet(); - - return cg.Board( - size: widget.boardSize, - onMove: (move, {isDrop, isPremove}) => - ref.read(ctrlProvider.notifier).onUserMove(Move.fromUci(move.uci)!), - data: cg.BoardData( - orientation: analysisState.pov.cg, - interactableSide: analysisState.position.isGameOver - ? cg.InteractableSide.none - : analysisState.position.turn == Side.white - ? cg.InteractableSide.white - : cg.InteractableSide.black, - fen: analysisState.position.fen, - isCheck: boardPrefs.boardHighlights && analysisState.position.isCheck, - lastMove: analysisState.lastMove?.cg, - sideToMove: analysisState.position.turn.cg, - validMoves: analysisState.validMoves, - shapes: userShapes.union(bestMoveShapes), - annotations: - showAnnotationsOnBoard && sanMove != null && annotation != null - ? altCastles.containsKey(sanMove.move.uci) - ? IMap({ - Move.fromUci(altCastles[sanMove.move.uci]!)!.cg.to: - annotation, - }) - : IMap({sanMove.move.cg.to: annotation}) - : null, - ), - settings: cg.BoardSettings( - pieceAssets: boardPrefs.pieceSet.assets, - colorScheme: boardPrefs.boardTheme.colors, - showValidMoves: boardPrefs.showLegalMoves, - showLastMove: boardPrefs.boardHighlights, - enableCoordinates: boardPrefs.coordinates, - animationDuration: boardPrefs.pieceAnimationDuration, - borderRadius: widget.isTablet - ? const BorderRadius.all(Radius.circular(4.0)) - : BorderRadius.zero, - boxShadow: widget.isTablet ? boardShadows : const [], - drawShape: cg.DrawShapeOptions( - enable: true, - onCompleteShape: _onCompleteShape, - onClearShapes: _onClearShapes, - ), - pieceShiftMethod: boardPrefs.pieceShiftMethod, - ), - ); - } - - void _onCompleteShape(cg.Shape shape) { - if (userShapes.any((element) => element == shape)) { - setState(() { - userShapes = userShapes.remove(shape); - }); - return; - } else { - setState(() { - userShapes = userShapes.add(shape); - }); - } - } - - void _onClearShapes() { - setState(() { - userShapes = ISet(); - }); - } -} - -class _EngineGaugeVertical extends ConsumerWidget { - const _EngineGaugeVertical(this.ctrlProvider); - - final AnalysisControllerProvider ctrlProvider; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final analysisState = ref.watch(ctrlProvider); - - return Container( - clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4.0), - ), - child: EngineGauge( - displayMode: EngineGaugeDisplayMode.vertical, - params: analysisState.engineGaugeParams, - ), - ); - } -} - -class _ColumnTopTable extends ConsumerWidget { - const _ColumnTopTable(this.ctrlProvider); - - final AnalysisControllerProvider ctrlProvider; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final analysisState = ref.watch(ctrlProvider); - final showEvaluationGauge = ref.watch( - analysisPreferencesProvider.select((p) => p.showEvaluationGauge), - ); - - return analysisState.hasAvailableEval - ? Column( + ], + ) + : Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (showEvaluationGauge) - EngineGauge( - displayMode: EngineGaugeDisplayMode.horizontal, - params: analysisState.engineGaugeParams, + const Spacer(), + if (currentGameAnalysis == options.gameAnyId?.gameId) + const Center( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 16.0), + child: WaitingForServerAnalysis(), + ), + ) + else + Center( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Builder( + builder: (context) { + Future? pendingRequest; + return StatefulBuilder( + builder: (context, setState) { + return FutureBuilder( + future: pendingRequest, + builder: (context, snapshot) { + return SecondaryButton( + semanticsLabel: + context.l10n.requestAComputerAnalysis, + onPressed: ref.watch(authSessionProvider) == + null + ? () { + showPlatformSnackbar( + context, + context + .l10n.youNeedAnAccountToDoThat, + ); + } + : snapshot.connectionState == + ConnectionState.waiting + ? null + : () { + setState(() { + pendingRequest = ref + .read(ctrlProvider.notifier) + .requestServerAnalysis() + .catchError((Object e) { + if (context.mounted) { + showPlatformSnackbar( + context, + e.toString(), + type: SnackBarType.error, + ); + } + }); + }); + }, + child: Text( + context.l10n.requestAComputerAnalysis, + ), + ); + }, + ); + }, + ); + }, + ), + ), ), - if (analysisState.isEngineAvailable) - _EngineLines(ctrlProvider, isLandscape: false), + const Spacer(), ], - ) - : kEmptyWidget; + ); } } - -class _EngineLines extends ConsumerWidget { - const _EngineLines(this.ctrlProvider, {required this.isLandscape}); - final AnalysisControllerProvider ctrlProvider; - final bool isLandscape; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final analysisState = ref.watch(ctrlProvider); - final numEvalLines = ref.watch( - analysisPreferencesProvider.select( - (p) => p.numEvalLines, - ), - ); - final engineEval = ref.watch(engineEvaluationProvider).eval; - final eval = engineEval ?? analysisState.currentNode.eval; - - final emptyLines = List.filled( - numEvalLines, - _Engineline.empty(ctrlProvider), - ); - - final content = !analysisState.position.isGameOver - ? (eval != null - ? eval.pvs - .take(numEvalLines) - .map( - (pv) => _Engineline(ctrlProvider, eval.position, pv), - ) - .toList() - : emptyLines) - : emptyLines; - - if (content.length < numEvalLines) { - final padding = List.filled( - numEvalLines - content.length, - _Engineline.empty(ctrlProvider), - ); - content.addAll(padding); - } - - return Padding( - padding: EdgeInsets.symmetric( - vertical: isLandscape ? kTabletBoardTableSidePadding : 0.0, - horizontal: isLandscape ? kTabletBoardTableSidePadding : 0.0, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: content, - ), + +class WaitingForServerAnalysis extends StatelessWidget { + const WaitingForServerAnalysis({super.key}); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ + Image.asset( + 'assets/images/stockfish/icon.png', + width: 30, + height: 30, + ), + const SizedBox(width: 8.0), + Text(context.l10n.waitingForAnalysis), + const SizedBox(width: 8.0), + const CircularProgressIndicator.adaptive(), + ], ); } } -class _Engineline extends ConsumerWidget { - const _Engineline( - this.ctrlProvider, - this.fromPosition, - this.pvData, - ); - - const _Engineline.empty(this.ctrlProvider) - : pvData = const PvData(moves: IListConst([])), - fromPosition = Chess.initial; +class _Body extends ConsumerWidget { + final String pgn; - final AnalysisControllerProvider ctrlProvider; - final Position fromPosition; - final PvData pvData; + final AnalysisOptions options; + const _Body({required this.pgn, required this.options}); @override Widget build(BuildContext context, WidgetRef ref) { - if (pvData.moves.isEmpty) { - return const SizedBox( - height: kEvalGaugeSize, - child: SizedBox.shrink(), - ); - } + final ctrlProvider = analysisControllerProvider(pgn, options); + final showEvaluationGauge = ref.watch( + analysisPreferencesProvider.select((value) => value.showEvaluationGauge), + ); - final pieceNotation = ref.watch(pieceNotationProvider).maybeWhen( - data: (value) => value, - orElse: () => defaultAccountPreferences.pieceNotation, - ); + final isEngineAvailable = ref.watch( + ctrlProvider.select( + (value) => value.isEngineAvailable, + ), + ); - final lineBuffer = StringBuffer(); - int ply = fromPosition.ply + 1; - pvData.sanMoves(fromPosition).forEachIndexed((i, s) { - lineBuffer.write( - ply.isOdd - ? '${(ply / 2).ceil()}. $s ' - : i == 0 - ? '${(ply / 2).ceil()}... $s ' - : '$s ', - ); - ply += 1; - }); + final hasEval = + ref.watch(ctrlProvider.select((value) => value.hasAvailableEval)); - final brightness = ref.watch(currentBrightnessProvider); + final showAnalysisSummary = ref.watch( + ctrlProvider.select((value) => value.displayMode == DisplayMode.summary), + ); - final evalString = pvData.evalString; - return AdaptiveInkWell( - onTap: () => ref - .read(ctrlProvider.notifier) - .onUserMove(Move.fromUci(pvData.moves[0])!), - child: SizedBox( - height: kEvalGaugeSize, - child: Padding( - padding: const EdgeInsets.all(2.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Container( - decoration: BoxDecoration( - color: pvData.winningSide == Side.black - ? EngineGauge.backgroundColor(context, brightness) - : EngineGauge.valueColor(context, brightness), - borderRadius: BorderRadius.circular(4.0), - ), - padding: const EdgeInsets.symmetric( - horizontal: 4.0, - vertical: 2.0, - ), - child: Text( - evalString, - style: TextStyle( - color: pvData.winningSide == Side.black - ? Colors.white - : Colors.black, - fontSize: kEvalGaugeFontSize, - fontWeight: FontWeight.w600, - ), - ), - ), - const SizedBox(width: 8.0), - Expanded( - child: Text( - lineBuffer.toString(), - maxLines: 1, - softWrap: false, - style: TextStyle( - fontFamily: pieceNotation == PieceNotation.symbol - ? 'ChessFont' - : null, - ), - overflow: TextOverflow.ellipsis, - ), - ), - ], + return Column( + children: [ + Expanded( + child: SafeArea( + bottom: false, + child: LayoutBuilder( + builder: (context, constraints) { + final aspectRatio = constraints.biggest.aspectRatio; + final defaultBoardSize = constraints.biggest.shortestSide; + final isTablet = isTabletOrLarger(context); + final remainingHeight = + constraints.maxHeight - defaultBoardSize; + final isSmallScreen = + remainingHeight < kSmallRemainingHeightLeftBoardThreshold; + final boardSize = isTablet || isSmallScreen + ? defaultBoardSize - kTabletBoardTableSidePadding * 2 + : defaultBoardSize; + + return aspectRatio > 1 + ? Row( + mainAxisSize: MainAxisSize.max, + children: [ + Padding( + padding: const EdgeInsets.only( + left: kTabletBoardTableSidePadding, + top: kTabletBoardTableSidePadding, + bottom: kTabletBoardTableSidePadding, + ), + child: Row( + children: [ + AnalysisBoard( + pgn, + options, + boardSize, + isTablet: isTablet, + ), + if (hasEval && showEvaluationGauge) ...[ + const SizedBox(width: 4.0), + _EngineGaugeVertical(ctrlProvider), + ], + ], + ), + ), + Flexible( + fit: FlexFit.loose, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + if (isEngineAvailable) + _EngineLines( + ctrlProvider, + isLandscape: true, + ), + Expanded( + child: PlatformCard( + margin: const EdgeInsets.all( + kTabletBoardTableSidePadding, + ), + semanticContainer: false, + child: showAnalysisSummary + ? ServerAnalysisSummary(pgn, options) + : AnalysisTreeView( + pgn, + options, + Orientation.landscape, + ), + ), + ), + ], + ), + ), + ], + ) + : Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + _ColumnTopTable(ctrlProvider), + if (isTablet) + Padding( + padding: const EdgeInsets.all( + kTabletBoardTableSidePadding, + ), + child: AnalysisBoard( + pgn, + options, + boardSize, + isTablet: isTablet, + ), + ) + else + AnalysisBoard(pgn, options, boardSize, isTablet: isTablet), + if (showAnalysisSummary) + Expanded(child: ServerAnalysisSummary(pgn, options)) + else + Expanded( + child: AnalysisTreeView( + pgn, + options, + Orientation.portrait, + ), + ), + ], + ); + }, + ), ), ), - ), + _BottomBar(pgn: pgn, options: options), + ], ); } } class _BottomBar extends ConsumerWidget { + final String pgn; + + final AnalysisOptions options; const _BottomBar({ required this.pgn, required this.options, }); - final String pgn; - final AnalysisOptions options; - @override Widget build(BuildContext context, WidgetRef ref) { final ctrlProvider = analysisControllerProvider(pgn, options); @@ -1018,11 +719,11 @@ class _BottomBar extends ConsumerWidget { ); } - void _moveForward(WidgetRef ref) => - ref.read(analysisControllerProvider(pgn, options).notifier).userNext(); void _moveBackward(WidgetRef ref) => ref .read(analysisControllerProvider(pgn, options).notifier) .userPrevious(); + void _moveForward(WidgetRef ref) => + ref.read(analysisControllerProvider(pgn, options).notifier).userNext(); Future _showAnalysisMenu(BuildContext context, WidgetRef ref) { return showAdaptiveActionSheet( @@ -1098,11 +799,41 @@ class _BottomBar extends ConsumerWidget { } } +class _ColumnTopTable extends ConsumerWidget { + final AnalysisControllerProvider ctrlProvider; + + const _ColumnTopTable(this.ctrlProvider); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final analysisState = ref.watch(ctrlProvider); + final showEvaluationGauge = ref.watch( + analysisPreferencesProvider.select((p) => p.showEvaluationGauge), + ); + + return analysisState.hasAvailableEval + ? Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (showEvaluationGauge) + EngineGauge( + displayMode: EngineGaugeDisplayMode.horizontal, + params: analysisState.engineGaugeParams, + ), + if (analysisState.isEngineAvailable) + _EngineLines(ctrlProvider, isLandscape: false), + ], + ) + : kEmptyWidget; + } +} + class _EngineDepth extends ConsumerWidget { - const _EngineDepth(this.ctrlProvider); - final AnalysisControllerProvider ctrlProvider; + const _EngineDepth(this.ctrlProvider); + @override Widget build(BuildContext context, WidgetRef ref) { final isEngineAvailable = ref.watch( @@ -1172,289 +903,344 @@ class _EngineDepth extends ConsumerWidget { } } -class _StockfishInfo extends ConsumerWidget { - const _StockfishInfo(this.currentNode); +class _EngineGaugeVertical extends ConsumerWidget { + final AnalysisControllerProvider ctrlProvider; - final AnalysisCurrentNode currentNode; + const _EngineGaugeVertical(this.ctrlProvider); @override Widget build(BuildContext context, WidgetRef ref) { - final (engineName: engineName, eval: eval, state: engineState) = - ref.watch(engineEvaluationProvider); + final analysisState = ref.watch(ctrlProvider); - final currentEval = eval ?? currentNode.eval; + return Container( + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4.0), + ), + child: EngineGauge( + displayMode: EngineGaugeDisplayMode.vertical, + params: analysisState.engineGaugeParams, + ), + ); + } +} - final knps = engineState == EngineState.computing - ? ', ${eval?.knps.round()}kn/s' - : ''; - final depth = currentEval?.depth ?? 0; - final maxDepth = math.max(depth, kMaxEngineDepth); +class _Engineline extends ConsumerWidget { + final AnalysisControllerProvider ctrlProvider; - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - PlatformListTile( - leading: Image.asset( - 'assets/images/stockfish/icon.png', - width: 44, - height: 44, + final Position fromPosition; + + final PvData pvData; + const _Engineline( + this.ctrlProvider, + this.fromPosition, + this.pvData, + ); + const _Engineline.empty(this.ctrlProvider) + : pvData = const PvData(moves: IListConst([])), + fromPosition = Chess.initial; + + @override + Widget build(BuildContext context, WidgetRef ref) { + if (pvData.moves.isEmpty) { + return const SizedBox( + height: kEvalGaugeSize, + child: SizedBox.shrink(), + ); + } + + final pieceNotation = ref.watch(pieceNotationProvider).maybeWhen( + data: (value) => value, + orElse: () => defaultAccountPreferences.pieceNotation, + ); + + final lineBuffer = StringBuffer(); + int ply = fromPosition.ply + 1; + pvData.sanMoves(fromPosition).forEachIndexed((i, s) { + lineBuffer.write( + ply.isOdd + ? '${(ply / 2).ceil()}. $s ' + : i == 0 + ? '${(ply / 2).ceil()}... $s ' + : '$s ', + ); + ply += 1; + }); + + final brightness = ref.watch(currentBrightnessProvider); + + final evalString = pvData.evalString; + return AdaptiveInkWell( + onTap: () => ref + .read(ctrlProvider.notifier) + .onUserMove(Move.fromUci(pvData.moves[0])!), + child: SizedBox( + height: kEvalGaugeSize, + child: Padding( + padding: const EdgeInsets.all(2.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + decoration: BoxDecoration( + color: pvData.winningSide == Side.black + ? EngineGauge.backgroundColor(context, brightness) + : EngineGauge.valueColor(context, brightness), + borderRadius: BorderRadius.circular(4.0), + ), + padding: const EdgeInsets.symmetric( + horizontal: 4.0, + vertical: 2.0, + ), + child: Text( + evalString, + style: TextStyle( + color: pvData.winningSide == Side.black + ? Colors.white + : Colors.black, + fontSize: kEvalGaugeFontSize, + fontWeight: FontWeight.w600, + ), + ), + ), + const SizedBox(width: 8.0), + Expanded( + child: Text( + lineBuffer.toString(), + maxLines: 1, + softWrap: false, + style: TextStyle( + fontFamily: pieceNotation == PieceNotation.symbol + ? 'ChessFont' + : null, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], ), - title: Text(engineName), - subtitle: Text( - context.l10n.depthX( - '$depth/$maxDepth$knps', + ), + ), + ); + } +} + +class _EngineLines extends ConsumerWidget { + final AnalysisControllerProvider ctrlProvider; + final bool isLandscape; + const _EngineLines(this.ctrlProvider, {required this.isLandscape}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final analysisState = ref.watch(ctrlProvider); + final numEvalLines = ref.watch( + analysisPreferencesProvider.select( + (p) => p.numEvalLines, + ), + ); + final engineEval = ref.watch(engineEvaluationProvider).eval; + final eval = engineEval ?? analysisState.currentNode.eval; + + final emptyLines = List.filled( + numEvalLines, + _Engineline.empty(ctrlProvider), + ); + + final content = !analysisState.position.isGameOver + ? (eval != null + ? eval.pvs + .take(numEvalLines) + .map( + (pv) => _Engineline(ctrlProvider, eval.position, pv), + ) + .toList() + : emptyLines) + : emptyLines; + + if (content.length < numEvalLines) { + final padding = List.filled( + numEvalLines - content.length, + _Engineline.empty(ctrlProvider), + ); + content.addAll(padding); + } + + return Padding( + padding: EdgeInsets.symmetric( + vertical: isLandscape ? kTabletBoardTableSidePadding : 0.0, + horizontal: isLandscape ? kTabletBoardTableSidePadding : 0.0, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: content, + ), + ); + } +} + +class _LoadedAnalysisScreen extends ConsumerWidget { + final AnalysisOptions options; + + final String pgn; + final String? title; + + const _LoadedAnalysisScreen({ + required this.options, + required this.pgn, + this.title, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return ConsumerPlatformWidget( + androidBuilder: _androidBuilder, + iosBuilder: _iosBuilder, + ref: ref, + ); + } + + Widget _androidBuilder(BuildContext context, WidgetRef ref) { + final ctrlProvider = analysisControllerProvider(pgn, options); + + return Scaffold( + resizeToAvoidBottomInset: false, + appBar: AppBar( + title: _Title(options: options, title: title), + actions: [ + _EngineDepth(ctrlProvider), + AppBarIconButton( + onPressed: () => showAdaptiveBottomSheet( + context: context, + isScrollControlled: true, + showDragHandle: true, + isDismissible: true, + builder: (_) => AnalysisSettings(pgn, options), + ), + semanticsLabel: context.l10n.settingsSettings, + icon: const Icon(Icons.settings), + ), + ], + ), + body: _Body(pgn: pgn, options: options), + ); + } + + Widget _iosBuilder(BuildContext context, WidgetRef ref) { + final ctrlProvider = analysisControllerProvider(pgn, options); + + return CupertinoPageScaffold( + resizeToAvoidBottomInset: false, + navigationBar: CupertinoNavigationBar( + backgroundColor: Styles.cupertinoScaffoldColor.resolveFrom(context), + border: null, + padding: Styles.cupertinoAppBarTrailingWidgetPadding, + middle: _Title(options: options, title: title), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + _EngineDepth(ctrlProvider), + AppBarIconButton( + onPressed: () => showAdaptiveBottomSheet( + context: context, + isScrollControlled: true, + showDragHandle: true, + isDismissible: true, + builder: (_) => AnalysisSettings(pgn, options), + ), + semanticsLabel: context.l10n.settingsSettings, + icon: const Icon(Icons.settings), ), - ), + ], ), - ], + ), + child: _Body(pgn: pgn, options: options), ); } } -class ServerAnalysisSummary extends ConsumerWidget { - const ServerAnalysisSummary(this.pgn, this.options); - - final String pgn; +class _LoadGame extends ConsumerWidget { final AnalysisOptions options; + final GameId gameId; + final String? title; + const _LoadGame(this.gameId, this.options, this.title); + @override Widget build(BuildContext context, WidgetRef ref) { - final ctrlProvider = analysisControllerProvider(pgn, options); - final playersAnalysis = - ref.watch(ctrlProvider.select((value) => value.playersAnalysis)); - final pgnHeaders = - ref.watch(ctrlProvider.select((value) => value.pgnHeaders)); - final currentGameAnalysis = ref.watch(currentAnalysisProvider); + final gameAsync = ref.watch(archivedGameProvider(id: gameId)); - return playersAnalysis != null - ? ListView( - children: [ - if (currentGameAnalysis == options.gameAnyId?.gameId) - const Padding( - padding: EdgeInsets.only(top: 16.0), - child: WaitingForServerAnalysis(), - ), - AcplChart(pgn, options), - Center( - child: SizedBox( - width: math.min(MediaQuery.sizeOf(context).width, 500), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Table( - defaultVerticalAlignment: - TableCellVerticalAlignment.middle, - columnWidths: const { - 0: FlexColumnWidth(1), - 1: FlexColumnWidth(1), - 2: FlexColumnWidth(1), - }, - children: [ - TableRow( - decoration: const BoxDecoration( - border: Border( - bottom: BorderSide(color: Colors.grey), - ), - ), - children: [ - _SummaryPlayerName(Side.white, pgnHeaders), - Center( - child: Text( - pgnHeaders.get('Result') ?? '', - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - ), - ), - _SummaryPlayerName(Side.black, pgnHeaders), - ], - ), - if (playersAnalysis.white.accuracy != null && - playersAnalysis.black.accuracy != null) - TableRow( - children: [ - _SummaryNumber( - '${playersAnalysis.white.accuracy}%', - ), - Center( - heightFactor: 1.8, - child: Text( - context.l10n.accuracy, - softWrap: true, - ), - ), - _SummaryNumber( - '${playersAnalysis.black.accuracy}%', - ), - ], - ), - for (final item in [ - ( - playersAnalysis.white.inaccuracies.toString(), - context.l10n - .nbInaccuracies(2) - .replaceAll('2', '') - .trim() - .capitalize(), - playersAnalysis.black.inaccuracies.toString() - ), - ( - playersAnalysis.white.mistakes.toString(), - context.l10n - .nbMistakes(2) - .replaceAll('2', '') - .trim() - .capitalize(), - playersAnalysis.black.mistakes.toString() - ), - ( - playersAnalysis.white.blunders.toString(), - context.l10n - .nbBlunders(2) - .replaceAll('2', '') - .trim() - .capitalize(), - playersAnalysis.black.blunders.toString() - ), - ]) - TableRow( - children: [ - _SummaryNumber(item.$1), - Center( - heightFactor: 1.2, - child: Text( - item.$2, - softWrap: true, - ), - ), - _SummaryNumber(item.$3), - ], - ), - if (playersAnalysis.white.acpl != null && - playersAnalysis.black.acpl != null) - TableRow( - children: [ - _SummaryNumber( - playersAnalysis.white.acpl.toString(), - ), - Center( - heightFactor: 1.5, - child: Text( - context.l10n.averageCentipawnLoss, - softWrap: true, - textAlign: TextAlign.center, - ), - ), - _SummaryNumber( - playersAnalysis.black.acpl.toString(), - ), - ], - ), - ], - ), - ), - ), - ), - ], - ) - : Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Spacer(), - if (currentGameAnalysis == options.gameAnyId?.gameId) - const Center( - child: Padding( - padding: EdgeInsets.symmetric(vertical: 16.0), - child: WaitingForServerAnalysis(), - ), - ) - else - Center( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0), - child: Builder( - builder: (context) { - Future? pendingRequest; - return StatefulBuilder( - builder: (context, setState) { - return FutureBuilder( - future: pendingRequest, - builder: (context, snapshot) { - return SecondaryButton( - semanticsLabel: - context.l10n.requestAComputerAnalysis, - onPressed: ref.watch(authSessionProvider) == - null - ? () { - showPlatformSnackbar( - context, - context - .l10n.youNeedAnAccountToDoThat, - ); - } - : snapshot.connectionState == - ConnectionState.waiting - ? null - : () { - setState(() { - pendingRequest = ref - .read(ctrlProvider.notifier) - .requestServerAnalysis() - .catchError((Object e) { - if (context.mounted) { - showPlatformSnackbar( - context, - e.toString(), - type: SnackBarType.error, - ); - } - }); - }); - }, - child: Text( - context.l10n.requestAComputerAnalysis, - ), - ); - }, - ); - }, - ); - }, - ), - ), - ), - const Spacer(), - ], - ); + return gameAsync.when( + data: (game) { + final serverAnalysis = + game.white.analysis != null && game.black.analysis != null + ? (white: game.white.analysis!, black: game.black.analysis!) + : null; + return _LoadedAnalysisScreen( + options: options.copyWith( + id: game.id, + opening: game.meta.opening, + division: game.meta.division, + serverAnalysis: serverAnalysis, + ), + pgn: game.makePgn(), + title: title, + ); + }, + loading: () => const Center(child: CircularProgressIndicator.adaptive()), + error: (error, _) { + return Center( + child: Text('Cannot load game analysis: $error'), + ); + }, + ); } } -class WaitingForServerAnalysis extends StatelessWidget { - const WaitingForServerAnalysis({super.key}); +class _StockfishInfo extends ConsumerWidget { + final AnalysisCurrentNode currentNode; + + const _StockfishInfo(this.currentNode); @override - Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.max, + Widget build(BuildContext context, WidgetRef ref) { + final (engineName: engineName, eval: eval, state: engineState) = + ref.watch(engineEvaluationProvider); + + final currentEval = eval ?? currentNode.eval; + + final knps = engineState == EngineState.computing + ? ', ${eval?.knps.round()}kn/s' + : ''; + final depth = currentEval?.depth ?? 0; + final maxDepth = math.max(depth, kMaxEngineDepth); + + return Column( + mainAxisSize: MainAxisSize.min, children: [ - Image.asset( - 'assets/images/stockfish/icon.png', - width: 30, - height: 30, + PlatformListTile( + leading: Image.asset( + 'assets/images/stockfish/icon.png', + width: 44, + height: 44, + ), + title: Text(engineName), + subtitle: Text( + context.l10n.depthX( + '$depth/$maxDepth$knps', + ), + ), ), - const SizedBox(width: 8.0), - Text(context.l10n.waitingForAnalysis), - const SizedBox(width: 8.0), - const CircularProgressIndicator.adaptive(), ], ); } } class _SummaryNumber extends StatelessWidget { - const _SummaryNumber(this.data); final String data; + const _SummaryNumber(this.data); @override Widget build(BuildContext context) { @@ -1468,9 +1254,9 @@ class _SummaryNumber extends StatelessWidget { } class _SummaryPlayerName extends StatelessWidget { - const _SummaryPlayerName(this.side, this.pgnHeaders); final Side side; final IMap pgnHeaders; + const _SummaryPlayerName(this.side, this.pgnHeaders); @override Widget build(BuildContext context) { @@ -1516,182 +1302,27 @@ class _SummaryPlayerName extends StatelessWidget { } } -class AcplChart extends ConsumerWidget { - const AcplChart(this.pgn, this.options); - - final String pgn; +class _Title extends StatelessWidget { final AnalysisOptions options; + final String? title; + const _Title({ + required this.options, + this.title, + }); @override - Widget build(BuildContext context, WidgetRef ref) { - final mainLineColor = Theme.of(context).colorScheme.secondary; - // yes it looks like below/above are inverted in fl_chart - final brightness = Theme.of(context).brightness; - final white = Theme.of(context).colorScheme.surfaceContainerHighest; - final black = Theme.of(context).colorScheme.outline; - // yes it looks like below/above are inverted in fl_chart - final belowLineColor = brightness == Brightness.light ? white : black; - final aboveLineColor = brightness == Brightness.light ? black : white; - - VerticalLine phaseVerticalBar(double x, String label) => VerticalLine( - x: x, - color: const Color(0xFF707070), - strokeWidth: 0.5, - label: VerticalLineLabel( - style: TextStyle( - fontSize: 10, - color: Theme.of(context) - .textTheme - .labelMedium - ?.color - ?.withOpacity(0.3), - ), - labelResolver: (line) => label, - padding: const EdgeInsets.only(right: 1), - alignment: Alignment.topRight, - direction: LabelDirection.vertical, - show: true, - ), - ); - - final data = ref.watch( - analysisControllerProvider(pgn, options) - .select((value) => value.acplChartData), - ); - - final rootPly = ref.watch( - analysisControllerProvider(pgn, options) - .select((value) => value.root.position.ply), - ); - - final currentNode = ref.watch( - analysisControllerProvider(pgn, options) - .select((value) => value.currentNode), - ); - - final isOnMainline = ref.watch( - analysisControllerProvider(pgn, options) - .select((value) => value.isOnMainline), - ); - - if (data == null) { - return const SizedBox.shrink(); - } - - final spots = data - .mapIndexed( - (i, e) => FlSpot(i.toDouble(), e.winningChances(Side.white)), - ) - .toList(growable: false); - - final divisionLines = []; - - if (options.division?.middlegame != null) { - if (options.division!.middlegame! > 0) { - divisionLines.add(phaseVerticalBar(0.0, context.l10n.opening)); - divisionLines.add( - phaseVerticalBar( - options.division!.middlegame! - 1, - context.l10n.middlegame, - ), - ); - } else { - divisionLines.add(phaseVerticalBar(0.0, context.l10n.middlegame)); - } - } - - if (options.division?.endgame != null) { - if (options.division!.endgame! > 0) { - divisionLines.add( - phaseVerticalBar( - options.division!.endgame! - 1, - context.l10n.endgame, - ), - ); - } else { - divisionLines.add( - phaseVerticalBar( - 0.0, - context.l10n.endgame, - ), - ); - } - } - return Center( - child: AspectRatio( - aspectRatio: 2.5, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: LineChart( - LineChartData( - lineTouchData: LineTouchData( - enabled: false, - touchCallback: - (FlTouchEvent event, LineTouchResponse? touchResponse) { - if (event is FlTapDownEvent || - event is FlPanUpdateEvent || - event is FlLongPressMoveUpdate) { - final touchX = event.localPosition!.dx; - final chartWidth = context.size!.width - - 32; // Insets on both sides of the chart of 16 - final minX = spots.first.x; - final maxX = spots.last.x; - final touchXDataValue = - minX + (touchX / chartWidth) * (maxX - minX); - final closestSpot = spots.reduce( - (a, b) => (a.x - touchXDataValue).abs() < - (b.x - touchXDataValue).abs() - ? a - : b, - ); - final closestNodeIndex = closestSpot.x.round(); - ref - .read(analysisControllerProvider(pgn, options).notifier) - .jumpToNthNodeOnMainline(closestNodeIndex); - } - }, - ), - minY: -1.0, - maxY: 1.0, - lineBarsData: [ - LineChartBarData( - spots: spots, - isCurved: false, - barWidth: 1, - color: mainLineColor.withOpacity(0.7), - aboveBarData: BarAreaData( - show: true, - color: aboveLineColor, - applyCutOffY: true, - ), - belowBarData: BarAreaData( - show: true, - color: belowLineColor, - applyCutOffY: true, - ), - dotData: const FlDotData( - show: false, - ), - ), + Widget build(BuildContext context) { + return title != null + ? Text(title!) + : Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (options.variant != Variant.standard) ...[ + Icon(options.variant.icon), + const SizedBox(width: 5.0), ], - extraLinesData: ExtraLinesData( - verticalLines: [ - if (isOnMainline) - VerticalLine( - x: (currentNode.position.ply - 1 - rootPly).toDouble(), - color: mainLineColor, - strokeWidth: 1.0, - ), - ...divisionLines, - ], - ), - gridData: const FlGridData(show: false), - borderData: FlBorderData(show: false), - titlesData: const FlTitlesData(show: false), - ), - ), - ), - ), - ); + Text(context.l10n.analysis), + ], + ); } } diff --git a/lib/src/view/analysis/analysis_settings.dart b/lib/src/view/analysis/analysis_settings.dart index 574b844ed2..33a8ea3749 100644 --- a/lib/src/view/analysis/analysis_settings.dart +++ b/lib/src/view/analysis/analysis_settings.dart @@ -140,13 +140,6 @@ class AnalysisSettings extends ConsumerWidget { .read(analysisPreferencesProvider.notifier) .togglePgnComments(), ), - SwitchSettingTile( - title: Text(context.l10n.openingExplorer), - value: prefs.showOpeningExplorer, - onChanged: (_) => ref - .read(analysisPreferencesProvider.notifier) - .toggleOpeningExplorer(), - ), SwitchSettingTile( title: Text(context.l10n.sound), value: isSoundEnabled, diff --git a/lib/src/view/analysis/opening_explorer_screen.dart b/lib/src/view/analysis/opening_explorer_screen.dart index d12a05b1c7..cb693a194a 100644 --- a/lib/src/view/analysis/opening_explorer_screen.dart +++ b/lib/src/view/analysis/opening_explorer_screen.dart @@ -1,17 +1,18 @@ -import 'package:chessground/chessground.dart' as cg; import 'package:dartchess/dartchess.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; -import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; +import 'package:lichess_mobile/src/model/analysis/opening_explorer_repository.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/chessground_compat.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'analysis_board.dart'; + class OpeningExplorerScreen extends StatelessWidget { const OpeningExplorerScreen({required this.pgn, required this.options}); @@ -48,89 +49,276 @@ class OpeningExplorerScreen extends StatelessWidget { } class _Body extends ConsumerWidget { - const _Body({ - required this.pgn, - required this.options, - }); - final String pgn; + final AnalysisOptions options; + const _Body({required this.pgn, required this.options}); @override Widget build(BuildContext context, WidgetRef ref) { - return SafeArea( - bottom: false, - child: LayoutBuilder( - builder: (context, constraints) { - final defaultBoardSize = constraints.biggest.shortestSide; - final isTablet = isTabletOrLarger(context); - final remainingHeight = constraints.maxHeight - defaultBoardSize; - final isSmallScreen = - remainingHeight < kSmallRemainingHeightLeftBoardThreshold; - final boardSize = isTablet || isSmallScreen - ? defaultBoardSize - kTabletBoardTableSidePadding * 2 - : defaultBoardSize; - return _Board(pgn, options, boardSize, isTablet: isTablet); - }, - ), + return Column( + children: [ + Expanded( + child: SafeArea( + bottom: false, + child: LayoutBuilder( + builder: (context, constraints) { + final aspectRatio = constraints.biggest.aspectRatio; + final defaultBoardSize = constraints.biggest.shortestSide; + final isTablet = isTabletOrLarger(context); + final remainingHeight = + constraints.maxHeight - defaultBoardSize; + final isSmallScreen = + remainingHeight < kSmallRemainingHeightLeftBoardThreshold; + final boardSize = isTablet || isSmallScreen + ? defaultBoardSize - kTabletBoardTableSidePadding * 2 + : defaultBoardSize; + + return aspectRatio > 1 + ? Row( + mainAxisSize: MainAxisSize.max, + children: [ + Padding( + padding: const EdgeInsets.only( + left: kTabletBoardTableSidePadding, + top: kTabletBoardTableSidePadding, + bottom: kTabletBoardTableSidePadding, + ), + child: Row( + children: [ + AnalysisBoard( + pgn, + options, + boardSize, + isTablet: isTablet, + ), + ], + ), + ), + Flexible( + fit: FlexFit.loose, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Expanded( + child: PlatformCard( + margin: const EdgeInsets.all( + kTabletBoardTableSidePadding, + ), + semanticContainer: false, + child: _OpeningExplorer( + pgn: pgn, + options: options, + ), + ), + ), + ], + ), + ), + ], + ) + : Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (isTablet) + Padding( + padding: const EdgeInsets.all( + kTabletBoardTableSidePadding, + ), + child: AnalysisBoard( + pgn, + options, + boardSize, + isTablet: isTablet, + ), + ) + else + AnalysisBoard(pgn, options, boardSize, isTablet: isTablet), + _OpeningExplorer(pgn: pgn, options: options), + ], + ); + }, + ), + ), + ), + ], ); } } -class _Board extends ConsumerStatefulWidget { - const _Board( - this.pgn, - this.options, - this.boardSize, { - required this.isTablet, +class _OpeningExplorer extends ConsumerWidget { + const _OpeningExplorer({ + required this.pgn, + required this.options, }); final String pgn; final AnalysisOptions options; - final double boardSize; - final bool isTablet; @override - ConsumerState<_Board> createState() => _BoardState(); + Widget build(BuildContext context, WidgetRef ref) { + final ctrlProvider = analysisControllerProvider(pgn, options); + final position = ref.watch(ctrlProvider.select((value) => value.position)); + + if (position.fullmoves > 24) { + return const Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('Max depth reached'), + ], + ), + ); + } + + final masterDatabaseAsync = + ref.watch(masterDatabaseProvider(fen: position.fen)); + + return masterDatabaseAsync.when( + data: (masterDatabase) { + return masterDatabase.moves.isEmpty + ? const Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('No game found'), + ], + ), + ) + : Expanded( + child: Container( + width: MediaQuery.of(context).size.width, + padding: const EdgeInsets.symmetric( + vertical: 8.0, + horizontal: 16.0, + ), + child: SingleChildScrollView( + scrollDirection: Axis.vertical, + child: DataTable( + showCheckboxColumn: false, + columnSpacing: 5, + horizontalMargin: 0, + columns: const [ + DataColumn(label: Text('Move')), + DataColumn(label: Text('Games')), + DataColumn(label: Text('White / Draw / Black')), + ], + rows: [ + ...masterDatabase.moves.map( + (move) => DataRow( + onSelectChanged: (_) => ref + .read(ctrlProvider.notifier) + .onUserMove(Move.fromUci(move.uci)!), + cells: [ + DataCell(Text(move.san)), + DataCell( + Text( + '${((move.games / masterDatabase.games) * 100).round()}% / ${formatNum(move.games)}', + ), + ), + DataCell( + _WinPercentageChart( + white: move.white, + draws: move.draws, + black: move.black, + ), + ), + ], + ), + ), + DataRow( + cells: [ + const DataCell(Icon(Icons.functions)), + DataCell( + Text( + '100% / ${formatNum(masterDatabase.games)}', + ), + ), + DataCell( + _WinPercentageChart( + white: masterDatabase.white, + draws: masterDatabase.draws, + black: masterDatabase.black, + ), + ), + ], + ), + ], + ), + ), + ), + ); + }, + loading: () => const Center( + child: CircularProgressIndicator(), + ), + error: (error, stackTrace) => Center( + child: Text(error.toString()), + ), + ); + } + + String formatNum(int num) => NumberFormat.decimalPatternDigits().format(num); } -class _BoardState extends ConsumerState<_Board> { +class _WinPercentageChart extends StatelessWidget { + final int white; + + final int draws; + final int black; + const _WinPercentageChart({ + required this.white, + required this.draws, + required this.black, + }); + @override Widget build(BuildContext context) { - final ctrlProvider = analysisControllerProvider(widget.pgn, widget.options); - final analysisState = ref.watch(ctrlProvider); - final boardPrefs = ref.watch(boardPreferencesProvider); - - return cg.Board( - size: widget.boardSize, - onMove: (move, {isDrop, isPremove}) => - ref.read(ctrlProvider.notifier).onUserMove(Move.fromUci(move.uci)!), - data: cg.BoardData( - orientation: analysisState.pov.cg, - interactableSide: analysisState.position.isGameOver - ? cg.InteractableSide.none - : analysisState.position.turn == Side.white - ? cg.InteractableSide.white - : cg.InteractableSide.black, - fen: analysisState.position.fen, - isCheck: boardPrefs.boardHighlights && analysisState.position.isCheck, - lastMove: analysisState.lastMove?.cg, - sideToMove: analysisState.position.turn.cg, - validMoves: analysisState.validMoves, - ), - settings: cg.BoardSettings( - pieceAssets: boardPrefs.pieceSet.assets, - colorScheme: boardPrefs.boardTheme.colors, - showValidMoves: boardPrefs.showLegalMoves, - showLastMove: boardPrefs.boardHighlights, - enableCoordinates: boardPrefs.coordinates, - animationDuration: boardPrefs.pieceAnimationDuration, - borderRadius: widget.isTablet - ? const BorderRadius.all(Radius.circular(4.0)) - : BorderRadius.zero, - boxShadow: widget.isTablet ? boardShadows : const [], - pieceShiftMethod: boardPrefs.pieceShiftMethod, - ), + int percentGames(int games) => + ((games / (white + draws + black)) * 100).round(); + + final percentWhite = percentGames(white); + final percentDraws = percentGames(draws); + final percentBlack = percentGames(black); + + return Row( + children: [ + if (percentWhite != 0) + Expanded( + child: ColoredBox( + color: Colors.white, + child: Text( + percentWhite < 5 ? '' : '$percentWhite%', + textAlign: TextAlign.center, + style: const TextStyle(color: Colors.black), + ), + ), + ), + if (percentDraws != 0) + Expanded( + child: ColoredBox( + color: Colors.grey, + child: Text( + percentDraws < 5 ? '' : '$percentDraws%', + textAlign: TextAlign.center, + style: const TextStyle(color: Colors.white), + ), + ), + ), + if (percentBlack != 0) + Expanded( + child: ColoredBox( + color: Colors.black, + child: Text( + percentBlack < 5 ? '' : '$percentBlack%', + textAlign: TextAlign.center, + style: const TextStyle(color: Colors.white), + ), + ), + ), + ], ); } } From f0a8eb7b95080378f3a182396b7445182f580ea0 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Sun, 28 Jul 2024 15:46:40 +0200 Subject: [PATCH 073/979] fix: remove double expanded widget --- .../analysis/opening_explorer_screen.dart | 114 +++++++++--------- 1 file changed, 55 insertions(+), 59 deletions(-) diff --git a/lib/src/view/analysis/opening_explorer_screen.dart b/lib/src/view/analysis/opening_explorer_screen.dart index cb693a194a..0868bb32b6 100644 --- a/lib/src/view/analysis/opening_explorer_screen.dart +++ b/lib/src/view/analysis/opening_explorer_screen.dart @@ -119,8 +119,6 @@ class _Body extends ConsumerWidget { ) : Column( mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.center, children: [ if (isTablet) Padding( @@ -136,7 +134,9 @@ class _Body extends ConsumerWidget { ) else AnalysisBoard(pgn, options, boardSize, isTablet: isTablet), - _OpeningExplorer(pgn: pgn, options: options), + Expanded( + child: _OpeningExplorer(pgn: pgn, options: options), + ), ], ); }, @@ -179,74 +179,70 @@ class _OpeningExplorer extends ConsumerWidget { return masterDatabaseAsync.when( data: (masterDatabase) { return masterDatabase.moves.isEmpty - ? const Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text('No game found'), - ], - ), + ? const Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('No game found'), + ], ) - : Expanded( - child: Container( - width: MediaQuery.of(context).size.width, - padding: const EdgeInsets.symmetric( - vertical: 8.0, - horizontal: 16.0, - ), - child: SingleChildScrollView( - scrollDirection: Axis.vertical, - child: DataTable( - showCheckboxColumn: false, - columnSpacing: 5, - horizontalMargin: 0, - columns: const [ - DataColumn(label: Text('Move')), - DataColumn(label: Text('Games')), - DataColumn(label: Text('White / Draw / Black')), - ], - rows: [ - ...masterDatabase.moves.map( - (move) => DataRow( - onSelectChanged: (_) => ref - .read(ctrlProvider.notifier) - .onUserMove(Move.fromUci(move.uci)!), - cells: [ - DataCell(Text(move.san)), - DataCell( - Text( - '${((move.games / masterDatabase.games) * 100).round()}% / ${formatNum(move.games)}', - ), - ), - DataCell( - _WinPercentageChart( - white: move.white, - draws: move.draws, - black: move.black, - ), - ), - ], - ), - ), - DataRow( + : Container( + width: MediaQuery.of(context).size.width, + padding: const EdgeInsets.symmetric( + vertical: 8.0, + horizontal: 16.0, + ), + child: SingleChildScrollView( + scrollDirection: Axis.vertical, + child: DataTable( + showCheckboxColumn: false, + columnSpacing: 5, + horizontalMargin: 0, + columns: const [ + DataColumn(label: Text('Move')), + DataColumn(label: Text('Games')), + DataColumn(label: Text('White / Draw / Black')), + ], + rows: [ + ...masterDatabase.moves.map( + (move) => DataRow( + onSelectChanged: (_) => ref + .read(ctrlProvider.notifier) + .onUserMove(Move.fromUci(move.uci)!), cells: [ - const DataCell(Icon(Icons.functions)), + DataCell(Text(move.san)), DataCell( Text( - '100% / ${formatNum(masterDatabase.games)}', + '${((move.games / masterDatabase.games) * 100).round()}% / ${formatNum(move.games)}', ), ), DataCell( _WinPercentageChart( - white: masterDatabase.white, - draws: masterDatabase.draws, - black: masterDatabase.black, + white: move.white, + draws: move.draws, + black: move.black, ), ), ], ), - ], - ), + ), + DataRow( + cells: [ + const DataCell(Icon(Icons.functions)), + DataCell( + Text( + '100% / ${formatNum(masterDatabase.games)}', + ), + ), + DataCell( + _WinPercentageChart( + white: masterDatabase.white, + draws: masterDatabase.draws, + black: masterDatabase.black, + ), + ), + ], + ), + ], ), ), ); From 738145f175d59dc860c171e890bda9a34da997b5 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Sun, 28 Jul 2024 16:15:35 +0200 Subject: [PATCH 074/979] feat: add bottom bar --- lib/src/view/analysis/analysis_screen.dart | 185 +---------------- ...lysis_board.dart => analysis_widgets.dart} | 195 +++++++++++++++++- .../analysis/opening_explorer_screen.dart | 10 +- lib/src/view/tools/tools_tab_screen.dart | 3 +- 4 files changed, 205 insertions(+), 188 deletions(-) rename lib/src/view/analysis/{analysis_board.dart => analysis_widgets.dart} (50%) diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index ac5fde5e11..8fcd56435c 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -15,33 +15,25 @@ import 'package:lichess_mobile/src/model/analysis/server_analysis_service.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/eval.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/engine/engine.dart'; import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; import 'package:lichess_mobile/src/model/game/game_repository_providers.dart'; -import 'package:lichess_mobile/src/model/game/game_share_service.dart'; import 'package:lichess_mobile/src/model/settings/brightness.dart'; -import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; -import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/utils/string.dart'; import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; -import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; -import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:popover/popover.dart'; -import '../../utils/share.dart'; -import 'analysis_board.dart'; import 'analysis_settings.dart'; -import 'analysis_share_screen.dart'; +import 'analysis_widgets.dart'; import 'tree_view.dart'; class AcplChart extends ConsumerWidget { @@ -610,7 +602,12 @@ class _Body extends ConsumerWidget { ), ) else - AnalysisBoard(pgn, options, boardSize, isTablet: isTablet), + AnalysisBoard( + pgn, + options, + boardSize, + isTablet: isTablet, + ), if (showAnalysisSummary) Expanded(child: ServerAnalysisSummary(pgn, options)) else @@ -627,173 +624,7 @@ class _Body extends ConsumerWidget { ), ), ), - _BottomBar(pgn: pgn, options: options), - ], - ); - } -} - -class _BottomBar extends ConsumerWidget { - final String pgn; - - final AnalysisOptions options; - const _BottomBar({ - required this.pgn, - required this.options, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final ctrlProvider = analysisControllerProvider(pgn, options); - final canGoBack = - ref.watch(ctrlProvider.select((value) => value.canGoBack)); - final canGoNext = - ref.watch(ctrlProvider.select((value) => value.canGoNext)); - final displayMode = - ref.watch(ctrlProvider.select((value) => value.displayMode)); - final canShowGameSummary = - ref.watch(ctrlProvider.select((value) => value.canShowGameSummary)); - - return Container( - color: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoTheme.of(context).barBackgroundColor - : Theme.of(context).bottomAppBarTheme.color, - child: SafeArea( - top: false, - child: SizedBox( - height: kBottomBarHeight, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - Expanded( - child: BottomBarButton( - label: context.l10n.menu, - onTap: () { - _showAnalysisMenu(context, ref); - }, - icon: Icons.menu, - ), - ), - if (canShowGameSummary) - Expanded( - child: BottomBarButton( - label: displayMode == DisplayMode.summary - ? 'Moves' - : 'Summary', - onTap: () { - ref.read(ctrlProvider.notifier).toggleDisplayMode(); - }, - icon: displayMode == DisplayMode.summary - ? LichessIcons.flow_cascade - : Icons.area_chart, - ), - ), - Expanded( - child: RepeatButton( - onLongPress: canGoBack ? () => _moveBackward(ref) : null, - child: BottomBarButton( - key: const ValueKey('goto-previous'), - onTap: canGoBack ? () => _moveBackward(ref) : null, - label: 'Previous', - icon: CupertinoIcons.chevron_back, - showTooltip: false, - ), - ), - ), - Expanded( - child: RepeatButton( - onLongPress: canGoNext ? () => _moveForward(ref) : null, - child: BottomBarButton( - key: const ValueKey('goto-next'), - icon: CupertinoIcons.chevron_forward, - label: context.l10n.next, - onTap: canGoNext ? () => _moveForward(ref) : null, - showTooltip: false, - ), - ), - ), - ], - ), - ), - ), - ); - } - - void _moveBackward(WidgetRef ref) => ref - .read(analysisControllerProvider(pgn, options).notifier) - .userPrevious(); - void _moveForward(WidgetRef ref) => - ref.read(analysisControllerProvider(pgn, options).notifier).userNext(); - - Future _showAnalysisMenu(BuildContext context, WidgetRef ref) { - return showAdaptiveActionSheet( - context: context, - actions: [ - BottomSheetAction( - makeLabel: (context) => Text(context.l10n.flipBoard), - onPressed: (context) { - ref - .read(analysisControllerProvider(pgn, options).notifier) - .toggleBoard(); - }, - ), - BottomSheetAction( - makeLabel: (context) => Text(context.l10n.mobileShareGamePGN), - onPressed: (_) { - pushPlatformRoute( - context, - title: context.l10n.studyShareAndExport, - builder: (_) => AnalysisShareScreen(pgn: pgn, options: options), - ); - }, - ), - BottomSheetAction( - makeLabel: (context) => Text(context.l10n.mobileSharePositionAsFEN), - onPressed: (_) { - launchShareDialog( - context, - text: ref - .read(analysisControllerProvider(pgn, options)) - .position - .fen, - ); - }, - ), - if (options.gameAnyId != null) - BottomSheetAction( - makeLabel: (context) => - Text(context.l10n.screenshotCurrentPosition), - onPressed: (_) async { - final gameId = options.gameAnyId!.gameId; - final state = ref.read(analysisControllerProvider(pgn, options)); - try { - final image = - await ref.read(gameShareServiceProvider).screenshotPosition( - gameId, - options.orientation, - state.position.fen, - state.lastMove, - ); - if (context.mounted) { - launchShareDialog( - context, - files: [image], - subject: context.l10n.puzzleFromGameLink( - lichessUri('/$gameId').toString(), - ), - ); - } - } catch (e) { - if (context.mounted) { - showPlatformSnackbar( - context, - 'Failed to get GIF', - type: SnackBarType.error, - ); - } - } - }, - ), + BottomBar(pgn: pgn, options: options), ], ); } diff --git a/lib/src/view/analysis/analysis_board.dart b/lib/src/view/analysis/analysis_widgets.dart similarity index 50% rename from lib/src/view/analysis/analysis_board.dart rename to lib/src/view/analysis/analysis_widgets.dart index c7592d6dbe..25d4abd636 100644 --- a/lib/src/view/analysis/analysis_board.dart +++ b/lib/src/view/analysis/analysis_widgets.dart @@ -5,24 +5,32 @@ import 'package:chessground/chessground.dart' as cg; import 'package:collection/collection.dart'; import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:flutter/widgets.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/eval.dart'; +import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; +import 'package:lichess_mobile/src/model/game/game_share_service.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; +import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/utils/chessground_compat.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/utils/share.dart'; import 'package:lichess_mobile/src/view/analysis/annotations.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; +import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/feedback.dart'; -class AnalysisBoard extends ConsumerStatefulWidget { - final String pgn; +import 'analysis_share_screen.dart'; - final AnalysisOptions options; - final double boardSize; - final bool isTablet; +class AnalysisBoard extends ConsumerStatefulWidget { const AnalysisBoard( this.pgn, this.options, @@ -30,11 +38,16 @@ class AnalysisBoard extends ConsumerStatefulWidget { required this.isTablet, }); + final String pgn; + final AnalysisOptions options; + final double boardSize; + final bool isTablet; + @override - ConsumerState createState() => _BoardState(); + ConsumerState createState() => AnalysisBoardState(); } -class _BoardState extends ConsumerState { +class AnalysisBoardState extends ConsumerState { ISet userShapes = ISet(); @override @@ -197,3 +210,169 @@ class _BoardState extends ConsumerState { } } } + +class BottomBar extends ConsumerWidget { + const BottomBar({ + required this.pgn, + required this.options, + }); + + final String pgn; + final AnalysisOptions options; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final ctrlProvider = analysisControllerProvider(pgn, options); + final canGoBack = + ref.watch(ctrlProvider.select((value) => value.canGoBack)); + final canGoNext = + ref.watch(ctrlProvider.select((value) => value.canGoNext)); + final displayMode = + ref.watch(ctrlProvider.select((value) => value.displayMode)); + final canShowGameSummary = + ref.watch(ctrlProvider.select((value) => value.canShowGameSummary)); + + return Container( + color: Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoTheme.of(context).barBackgroundColor + : Theme.of(context).bottomAppBarTheme.color, + child: SafeArea( + top: false, + child: SizedBox( + height: kBottomBarHeight, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Expanded( + child: BottomBarButton( + label: context.l10n.menu, + onTap: () { + _showAnalysisMenu(context, ref); + }, + icon: Icons.menu, + ), + ), + if (canShowGameSummary) + Expanded( + child: BottomBarButton( + label: displayMode == DisplayMode.summary + ? 'Moves' + : 'Summary', + onTap: () { + ref.read(ctrlProvider.notifier).toggleDisplayMode(); + }, + icon: displayMode == DisplayMode.summary + ? LichessIcons.flow_cascade + : Icons.area_chart, + ), + ), + Expanded( + child: RepeatButton( + onLongPress: canGoBack ? () => _moveBackward(ref) : null, + child: BottomBarButton( + key: const ValueKey('goto-previous'), + onTap: canGoBack ? () => _moveBackward(ref) : null, + label: 'Previous', + icon: CupertinoIcons.chevron_back, + showTooltip: false, + ), + ), + ), + Expanded( + child: RepeatButton( + onLongPress: canGoNext ? () => _moveForward(ref) : null, + child: BottomBarButton( + key: const ValueKey('goto-next'), + icon: CupertinoIcons.chevron_forward, + label: context.l10n.next, + onTap: canGoNext ? () => _moveForward(ref) : null, + showTooltip: false, + ), + ), + ), + ], + ), + ), + ), + ); + } + + void _moveBackward(WidgetRef ref) => ref + .read(analysisControllerProvider(pgn, options).notifier) + .userPrevious(); + void _moveForward(WidgetRef ref) => + ref.read(analysisControllerProvider(pgn, options).notifier).userNext(); + + Future _showAnalysisMenu(BuildContext context, WidgetRef ref) { + return showAdaptiveActionSheet( + context: context, + actions: [ + BottomSheetAction( + makeLabel: (context) => Text(context.l10n.flipBoard), + onPressed: (context) { + ref + .read(analysisControllerProvider(pgn, options).notifier) + .toggleBoard(); + }, + ), + BottomSheetAction( + makeLabel: (context) => Text(context.l10n.mobileShareGamePGN), + onPressed: (_) { + pushPlatformRoute( + context, + title: context.l10n.studyShareAndExport, + builder: (_) => AnalysisShareScreen(pgn: pgn, options: options), + ); + }, + ), + BottomSheetAction( + makeLabel: (context) => Text(context.l10n.mobileSharePositionAsFEN), + onPressed: (_) { + launchShareDialog( + context, + text: ref + .read(analysisControllerProvider(pgn, options)) + .position + .fen, + ); + }, + ), + if (options.gameAnyId != null) + BottomSheetAction( + makeLabel: (context) => + Text(context.l10n.screenshotCurrentPosition), + onPressed: (_) async { + final gameId = options.gameAnyId!.gameId; + final state = ref.read(analysisControllerProvider(pgn, options)); + try { + final image = + await ref.read(gameShareServiceProvider).screenshotPosition( + gameId, + options.orientation, + state.position.fen, + state.lastMove, + ); + if (context.mounted) { + launchShareDialog( + context, + files: [image], + subject: context.l10n.puzzleFromGameLink( + lichessUri('/$gameId').toString(), + ), + ); + } + } catch (e) { + if (context.mounted) { + showPlatformSnackbar( + context, + 'Failed to get GIF', + type: SnackBarType.error, + ); + } + } + }, + ), + ], + ); + } +} diff --git a/lib/src/view/analysis/opening_explorer_screen.dart b/lib/src/view/analysis/opening_explorer_screen.dart index 0868bb32b6..c440887de9 100644 --- a/lib/src/view/analysis/opening_explorer_screen.dart +++ b/lib/src/view/analysis/opening_explorer_screen.dart @@ -11,7 +11,7 @@ import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; -import 'analysis_board.dart'; +import 'analysis_widgets.dart'; class OpeningExplorerScreen extends StatelessWidget { const OpeningExplorerScreen({required this.pgn, required this.options}); @@ -133,7 +133,12 @@ class _Body extends ConsumerWidget { ), ) else - AnalysisBoard(pgn, options, boardSize, isTablet: isTablet), + AnalysisBoard( + pgn, + options, + boardSize, + isTablet: isTablet, + ), Expanded( child: _OpeningExplorer(pgn: pgn, options: options), ), @@ -143,6 +148,7 @@ class _Body extends ConsumerWidget { ), ), ), + BottomBar(pgn: pgn, options: options), ], ); } diff --git a/lib/src/view/tools/tools_tab_screen.dart b/lib/src/view/tools/tools_tab_screen.dart index effd6f79f8..b185c04422 100644 --- a/lib/src/view/tools/tools_tab_screen.dart +++ b/lib/src/view/tools/tools_tab_screen.dart @@ -122,7 +122,8 @@ class _Body extends StatelessWidget { ), title: Padding( padding: tilePadding, - child: Text(context.l10n.openingExplorer, style: Styles.callout), + child: + Text(context.l10n.openingExplorer, style: Styles.callout), ), trailing: Theme.of(context).platform == TargetPlatform.iOS ? const CupertinoListTileChevron() From 3984bc6c6b10de9809edda6991f84a75ea686328 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Mon, 29 Jul 2024 07:15:44 +0200 Subject: [PATCH 075/979] fix: revert formatter going crazy and remove bottom bar --- lib/src/view/analysis/analysis_screen.dart | 1548 +++++++++-------- lib/src/view/analysis/analysis_widgets.dart | 178 -- .../analysis/opening_explorer_screen.dart | 1 - 3 files changed, 861 insertions(+), 866 deletions(-) diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index 8fcd56435c..dad1b1ae48 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -15,208 +15,42 @@ import 'package:lichess_mobile/src/model/analysis/server_analysis_service.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/eval.dart'; +import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/engine/engine.dart'; import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; import 'package:lichess_mobile/src/model/game/game_repository_providers.dart'; +import 'package:lichess_mobile/src/model/game/game_share_service.dart'; import 'package:lichess_mobile/src/model/settings/brightness.dart'; +import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/utils/string.dart'; +import 'package:lichess_mobile/src/view/analysis/analysis_share_screen.dart'; import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; +import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:popover/popover.dart'; +import '../../utils/share.dart'; import 'analysis_settings.dart'; import 'analysis_widgets.dart'; import 'tree_view.dart'; -class AcplChart extends ConsumerWidget { - final String pgn; - - final AnalysisOptions options; - const AcplChart(this.pgn, this.options); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final mainLineColor = Theme.of(context).colorScheme.secondary; - // yes it looks like below/above are inverted in fl_chart - final brightness = Theme.of(context).brightness; - final white = Theme.of(context).colorScheme.surfaceContainerHighest; - final black = Theme.of(context).colorScheme.outline; - // yes it looks like below/above are inverted in fl_chart - final belowLineColor = brightness == Brightness.light ? white : black; - final aboveLineColor = brightness == Brightness.light ? black : white; - - VerticalLine phaseVerticalBar(double x, String label) => VerticalLine( - x: x, - color: const Color(0xFF707070), - strokeWidth: 0.5, - label: VerticalLineLabel( - style: TextStyle( - fontSize: 10, - color: Theme.of(context) - .textTheme - .labelMedium - ?.color - ?.withOpacity(0.3), - ), - labelResolver: (line) => label, - padding: const EdgeInsets.only(right: 1), - alignment: Alignment.topRight, - direction: LabelDirection.vertical, - show: true, - ), - ); - - final data = ref.watch( - analysisControllerProvider(pgn, options) - .select((value) => value.acplChartData), - ); - - final rootPly = ref.watch( - analysisControllerProvider(pgn, options) - .select((value) => value.root.position.ply), - ); - - final currentNode = ref.watch( - analysisControllerProvider(pgn, options) - .select((value) => value.currentNode), - ); - - final isOnMainline = ref.watch( - analysisControllerProvider(pgn, options) - .select((value) => value.isOnMainline), - ); - - if (data == null) { - return const SizedBox.shrink(); - } - - final spots = data - .mapIndexed( - (i, e) => FlSpot(i.toDouble(), e.winningChances(Side.white)), - ) - .toList(growable: false); - - final divisionLines = []; - - if (options.division?.middlegame != null) { - if (options.division!.middlegame! > 0) { - divisionLines.add(phaseVerticalBar(0.0, context.l10n.opening)); - divisionLines.add( - phaseVerticalBar( - options.division!.middlegame! - 1, - context.l10n.middlegame, - ), - ); - } else { - divisionLines.add(phaseVerticalBar(0.0, context.l10n.middlegame)); - } - } - - if (options.division?.endgame != null) { - if (options.division!.endgame! > 0) { - divisionLines.add( - phaseVerticalBar( - options.division!.endgame! - 1, - context.l10n.endgame, - ), - ); - } else { - divisionLines.add( - phaseVerticalBar( - 0.0, - context.l10n.endgame, - ), - ); - } - } - return Center( - child: AspectRatio( - aspectRatio: 2.5, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: LineChart( - LineChartData( - lineTouchData: LineTouchData( - enabled: false, - touchCallback: - (FlTouchEvent event, LineTouchResponse? touchResponse) { - if (event is FlTapDownEvent || - event is FlPanUpdateEvent || - event is FlLongPressMoveUpdate) { - final touchX = event.localPosition!.dx; - final chartWidth = context.size!.width - - 32; // Insets on both sides of the chart of 16 - final minX = spots.first.x; - final maxX = spots.last.x; - final touchXDataValue = - minX + (touchX / chartWidth) * (maxX - minX); - final closestSpot = spots.reduce( - (a, b) => (a.x - touchXDataValue).abs() < - (b.x - touchXDataValue).abs() - ? a - : b, - ); - final closestNodeIndex = closestSpot.x.round(); - ref - .read(analysisControllerProvider(pgn, options).notifier) - .jumpToNthNodeOnMainline(closestNodeIndex); - } - }, - ), - minY: -1.0, - maxY: 1.0, - lineBarsData: [ - LineChartBarData( - spots: spots, - isCurved: false, - barWidth: 1, - color: mainLineColor.withOpacity(0.7), - aboveBarData: BarAreaData( - show: true, - color: aboveLineColor, - applyCutOffY: true, - ), - belowBarData: BarAreaData( - show: true, - color: belowLineColor, - applyCutOffY: true, - ), - dotData: const FlDotData( - show: false, - ), - ), - ], - extraLinesData: ExtraLinesData( - verticalLines: [ - if (isOnMainline) - VerticalLine( - x: (currentNode.position.ply - 1 - rootPly).toDouble(), - color: mainLineColor, - strokeWidth: 1.0, - ), - ...divisionLines, - ], - ), - gridData: const FlGridData(show: false), - borderData: FlBorderData(show: false), - titlesData: const FlTitlesData(show: false), - ), - ), - ), - ), - ); - } -} - class AnalysisScreen extends StatelessWidget { + const AnalysisScreen({ + required this.options, + required this.pgnOrId, + this.title, + }); + /// The analysis options. final AnalysisOptions options; @@ -225,12 +59,6 @@ class AnalysisScreen extends StatelessWidget { final String? title; - const AnalysisScreen({ - required this.options, - required this.pgnOrId, - this.title, - }); - @override Widget build(BuildContext context) { return pgnOrId.length == 8 && GameId(pgnOrId).isValid @@ -243,252 +71,154 @@ class AnalysisScreen extends StatelessWidget { } } -class ServerAnalysisSummary extends ConsumerWidget { - final String pgn; +class _LoadGame extends ConsumerWidget { + const _LoadGame(this.gameId, this.options, this.title); final AnalysisOptions options; - const ServerAnalysisSummary(this.pgn, this.options); + final GameId gameId; + final String? title; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final gameAsync = ref.watch(archivedGameProvider(id: gameId)); + + return gameAsync.when( + data: (game) { + final serverAnalysis = + game.white.analysis != null && game.black.analysis != null + ? (white: game.white.analysis!, black: game.black.analysis!) + : null; + return _LoadedAnalysisScreen( + options: options.copyWith( + id: game.id, + opening: game.meta.opening, + division: game.meta.division, + serverAnalysis: serverAnalysis, + ), + pgn: game.makePgn(), + title: title, + ); + }, + loading: () => const Center(child: CircularProgressIndicator.adaptive()), + error: (error, _) { + return Center( + child: Text('Cannot load game analysis: $error'), + ); + }, + ); + } +} + +class _LoadedAnalysisScreen extends ConsumerWidget { + const _LoadedAnalysisScreen({ + required this.options, + required this.pgn, + this.title, + }); + + final AnalysisOptions options; + final String pgn; + + final String? title; @override Widget build(BuildContext context, WidgetRef ref) { + return ConsumerPlatformWidget( + androidBuilder: _androidBuilder, + iosBuilder: _iosBuilder, + ref: ref, + ); + } + + Widget _androidBuilder(BuildContext context, WidgetRef ref) { final ctrlProvider = analysisControllerProvider(pgn, options); - final playersAnalysis = - ref.watch(ctrlProvider.select((value) => value.playersAnalysis)); - final pgnHeaders = - ref.watch(ctrlProvider.select((value) => value.pgnHeaders)); - final currentGameAnalysis = ref.watch(currentAnalysisProvider); - return playersAnalysis != null - ? ListView( - children: [ - if (currentGameAnalysis == options.gameAnyId?.gameId) - const Padding( - padding: EdgeInsets.only(top: 16.0), - child: WaitingForServerAnalysis(), - ), - AcplChart(pgn, options), - Center( - child: SizedBox( - width: math.min(MediaQuery.sizeOf(context).width, 500), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Table( - defaultVerticalAlignment: - TableCellVerticalAlignment.middle, - columnWidths: const { - 0: FlexColumnWidth(1), - 1: FlexColumnWidth(1), - 2: FlexColumnWidth(1), - }, - children: [ - TableRow( - decoration: const BoxDecoration( - border: Border( - bottom: BorderSide(color: Colors.grey), - ), - ), - children: [ - _SummaryPlayerName(Side.white, pgnHeaders), - Center( - child: Text( - pgnHeaders.get('Result') ?? '', - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - ), - ), - _SummaryPlayerName(Side.black, pgnHeaders), - ], - ), - if (playersAnalysis.white.accuracy != null && - playersAnalysis.black.accuracy != null) - TableRow( - children: [ - _SummaryNumber( - '${playersAnalysis.white.accuracy}%', - ), - Center( - heightFactor: 1.8, - child: Text( - context.l10n.accuracy, - softWrap: true, - ), - ), - _SummaryNumber( - '${playersAnalysis.black.accuracy}%', - ), - ], - ), - for (final item in [ - ( - playersAnalysis.white.inaccuracies.toString(), - context.l10n - .nbInaccuracies(2) - .replaceAll('2', '') - .trim() - .capitalize(), - playersAnalysis.black.inaccuracies.toString() - ), - ( - playersAnalysis.white.mistakes.toString(), - context.l10n - .nbMistakes(2) - .replaceAll('2', '') - .trim() - .capitalize(), - playersAnalysis.black.mistakes.toString() - ), - ( - playersAnalysis.white.blunders.toString(), - context.l10n - .nbBlunders(2) - .replaceAll('2', '') - .trim() - .capitalize(), - playersAnalysis.black.blunders.toString() - ), - ]) - TableRow( - children: [ - _SummaryNumber(item.$1), - Center( - heightFactor: 1.2, - child: Text( - item.$2, - softWrap: true, - ), - ), - _SummaryNumber(item.$3), - ], - ), - if (playersAnalysis.white.acpl != null && - playersAnalysis.black.acpl != null) - TableRow( - children: [ - _SummaryNumber( - playersAnalysis.white.acpl.toString(), - ), - Center( - heightFactor: 1.5, - child: Text( - context.l10n.averageCentipawnLoss, - softWrap: true, - textAlign: TextAlign.center, - ), - ), - _SummaryNumber( - playersAnalysis.black.acpl.toString(), - ), - ], - ), - ], - ), - ), - ), + return Scaffold( + resizeToAvoidBottomInset: false, + appBar: AppBar( + title: _Title(options: options, title: title), + actions: [ + _EngineDepth(ctrlProvider), + AppBarIconButton( + onPressed: () => showAdaptiveBottomSheet( + context: context, + isScrollControlled: true, + showDragHandle: true, + isDismissible: true, + builder: (_) => AnalysisSettings(pgn, options), + ), + semanticsLabel: context.l10n.settingsSettings, + icon: const Icon(Icons.settings), + ), + ], + ), + body: _Body(pgn: pgn, options: options), + ); + } + + Widget _iosBuilder(BuildContext context, WidgetRef ref) { + final ctrlProvider = analysisControllerProvider(pgn, options); + + return CupertinoPageScaffold( + resizeToAvoidBottomInset: false, + navigationBar: CupertinoNavigationBar( + backgroundColor: Styles.cupertinoScaffoldColor.resolveFrom(context), + border: null, + padding: Styles.cupertinoAppBarTrailingWidgetPadding, + middle: _Title(options: options, title: title), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + _EngineDepth(ctrlProvider), + AppBarIconButton( + onPressed: () => showAdaptiveBottomSheet( + context: context, + isScrollControlled: true, + showDragHandle: true, + isDismissible: true, + builder: (_) => AnalysisSettings(pgn, options), ), - ], - ) - : Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Spacer(), - if (currentGameAnalysis == options.gameAnyId?.gameId) - const Center( - child: Padding( - padding: EdgeInsets.symmetric(vertical: 16.0), - child: WaitingForServerAnalysis(), - ), - ) - else - Center( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0), - child: Builder( - builder: (context) { - Future? pendingRequest; - return StatefulBuilder( - builder: (context, setState) { - return FutureBuilder( - future: pendingRequest, - builder: (context, snapshot) { - return SecondaryButton( - semanticsLabel: - context.l10n.requestAComputerAnalysis, - onPressed: ref.watch(authSessionProvider) == - null - ? () { - showPlatformSnackbar( - context, - context - .l10n.youNeedAnAccountToDoThat, - ); - } - : snapshot.connectionState == - ConnectionState.waiting - ? null - : () { - setState(() { - pendingRequest = ref - .read(ctrlProvider.notifier) - .requestServerAnalysis() - .catchError((Object e) { - if (context.mounted) { - showPlatformSnackbar( - context, - e.toString(), - type: SnackBarType.error, - ); - } - }); - }); - }, - child: Text( - context.l10n.requestAComputerAnalysis, - ), - ); - }, - ); - }, - ); - }, - ), - ), - ), - const Spacer(), - ], - ); + semanticsLabel: context.l10n.settingsSettings, + icon: const Icon(Icons.settings), + ), + ], + ), + ), + child: _Body(pgn: pgn, options: options), + ); } } -class WaitingForServerAnalysis extends StatelessWidget { - const WaitingForServerAnalysis({super.key}); +class _Title extends StatelessWidget { + const _Title({ + required this.options, + this.title, + }); + final AnalysisOptions options; + final String? title; @override Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.max, - children: [ - Image.asset( - 'assets/images/stockfish/icon.png', - width: 30, - height: 30, - ), - const SizedBox(width: 8.0), - Text(context.l10n.waitingForAnalysis), - const SizedBox(width: 8.0), - const CircularProgressIndicator.adaptive(), - ], - ); + return title != null + ? Text(title!) + : Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (options.variant != Variant.standard) ...[ + Icon(options.variant.icon), + const SizedBox(width: 5.0), + ], + Text(context.l10n.analysis), + ], + ); } } class _Body extends ConsumerWidget { - final String pgn; + const _Body({required this.pgn, required this.options}); + final String pgn; final AnalysisOptions options; - const _Body({required this.pgn, required this.options}); @override Widget build(BuildContext context, WidgetRef ref) { @@ -624,17 +354,39 @@ class _Body extends ConsumerWidget { ), ), ), - BottomBar(pgn: pgn, options: options), + _BottomBar(pgn: pgn, options: options), ], ); } } -class _ColumnTopTable extends ConsumerWidget { +class _EngineGaugeVertical extends ConsumerWidget { + const _EngineGaugeVertical(this.ctrlProvider); + final AnalysisControllerProvider ctrlProvider; + @override + Widget build(BuildContext context, WidgetRef ref) { + final analysisState = ref.watch(ctrlProvider); + + return Container( + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4.0), + ), + child: EngineGauge( + displayMode: EngineGaugeDisplayMode.vertical, + params: analysisState.engineGaugeParams, + ), + ); + } +} + +class _ColumnTopTable extends ConsumerWidget { const _ColumnTopTable(this.ctrlProvider); + final AnalysisControllerProvider ctrlProvider; + @override Widget build(BuildContext context, WidgetRef ref) { final analysisState = ref.watch(ctrlProvider); @@ -660,117 +412,75 @@ class _ColumnTopTable extends ConsumerWidget { } } -class _EngineDepth extends ConsumerWidget { +class _EngineLines extends ConsumerWidget { + const _EngineLines(this.ctrlProvider, {required this.isLandscape}); final AnalysisControllerProvider ctrlProvider; - - const _EngineDepth(this.ctrlProvider); + final bool isLandscape; @override Widget build(BuildContext context, WidgetRef ref) { - final isEngineAvailable = ref.watch( - ctrlProvider.select( - (value) => value.isEngineAvailable, + final analysisState = ref.watch(ctrlProvider); + final numEvalLines = ref.watch( + analysisPreferencesProvider.select( + (p) => p.numEvalLines, ), ); - final currentNode = ref.watch( - ctrlProvider.select((value) => value.currentNode), - ); - final depth = ref.watch( - engineEvaluationProvider.select((value) => value.eval?.depth), - ) ?? - currentNode.eval?.depth; - - return isEngineAvailable && depth != null - ? AppBarTextButton( - onPressed: () { - showPopover( - context: context, - bodyBuilder: (context) { - return _StockfishInfo(currentNode); - }, - direction: PopoverDirection.top, - width: 240, - backgroundColor: - Theme.of(context).platform == TargetPlatform.android - ? Theme.of(context).dialogBackgroundColor - : CupertinoDynamicColor.resolve( - CupertinoColors.tertiarySystemBackground, - context, - ), - transitionDuration: Duration.zero, - popoverTransitionBuilder: (_, child) => child, - ); - }, - child: RepaintBoundary( - child: Container( - width: 20.0, - height: 20.0, - padding: const EdgeInsets.all(2.0), - decoration: BoxDecoration( - color: Theme.of(context).platform == TargetPlatform.android - ? Theme.of(context).colorScheme.secondary - : CupertinoTheme.of(context).primaryColor, - borderRadius: BorderRadius.circular(4.0), - ), - child: FittedBox( - fit: BoxFit.contain, - child: Text( - '${math.min(99, depth)}', - style: TextStyle( - color: Theme.of(context).platform == - TargetPlatform.android - ? Theme.of(context).colorScheme.onSecondary - : CupertinoTheme.of(context).primaryContrastingColor, - fontFeatures: const [ - FontFeature.tabularFigures(), - ], - ), - ), - ), - ), - ), - ) - : const SizedBox.shrink(); - } -} + final engineEval = ref.watch(engineEvaluationProvider).eval; + final eval = engineEval ?? analysisState.currentNode.eval; -class _EngineGaugeVertical extends ConsumerWidget { - final AnalysisControllerProvider ctrlProvider; + final emptyLines = List.filled( + numEvalLines, + _Engineline.empty(ctrlProvider), + ); - const _EngineGaugeVertical(this.ctrlProvider); + final content = !analysisState.position.isGameOver + ? (eval != null + ? eval.pvs + .take(numEvalLines) + .map( + (pv) => _Engineline(ctrlProvider, eval.position, pv), + ) + .toList() + : emptyLines) + : emptyLines; - @override - Widget build(BuildContext context, WidgetRef ref) { - final analysisState = ref.watch(ctrlProvider); + if (content.length < numEvalLines) { + final padding = List.filled( + numEvalLines - content.length, + _Engineline.empty(ctrlProvider), + ); + content.addAll(padding); + } - return Container( - clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4.0), + return Padding( + padding: EdgeInsets.symmetric( + vertical: isLandscape ? kTabletBoardTableSidePadding : 0.0, + horizontal: isLandscape ? kTabletBoardTableSidePadding : 0.0, ), - child: EngineGauge( - displayMode: EngineGaugeDisplayMode.vertical, - params: analysisState.engineGaugeParams, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: content, ), ); } } class _Engineline extends ConsumerWidget { - final AnalysisControllerProvider ctrlProvider; - - final Position fromPosition; - - final PvData pvData; const _Engineline( this.ctrlProvider, this.fromPosition, this.pvData, ); + const _Engineline.empty(this.ctrlProvider) : pvData = const PvData(moves: IListConst([])), fromPosition = Chess.initial; + final AnalysisControllerProvider ctrlProvider; + final Position fromPosition; + final PvData pvData; + @override Widget build(BuildContext context, WidgetRef ref) { if (pvData.moves.isEmpty) { @@ -858,220 +568,529 @@ class _Engineline extends ConsumerWidget { } } -class _EngineLines extends ConsumerWidget { - final AnalysisControllerProvider ctrlProvider; - final bool isLandscape; - const _EngineLines(this.ctrlProvider, {required this.isLandscape}); +class _BottomBar extends ConsumerWidget { + const _BottomBar({ + required this.pgn, + required this.options, + }); + + final String pgn; + final AnalysisOptions options; @override Widget build(BuildContext context, WidgetRef ref) { - final analysisState = ref.watch(ctrlProvider); - final numEvalLines = ref.watch( - analysisPreferencesProvider.select( - (p) => p.numEvalLines, - ), - ); - final engineEval = ref.watch(engineEvaluationProvider).eval; - final eval = engineEval ?? analysisState.currentNode.eval; + final ctrlProvider = analysisControllerProvider(pgn, options); + final canGoBack = + ref.watch(ctrlProvider.select((value) => value.canGoBack)); + final canGoNext = + ref.watch(ctrlProvider.select((value) => value.canGoNext)); + final displayMode = + ref.watch(ctrlProvider.select((value) => value.displayMode)); + final canShowGameSummary = + ref.watch(ctrlProvider.select((value) => value.canShowGameSummary)); - final emptyLines = List.filled( - numEvalLines, - _Engineline.empty(ctrlProvider), + return Container( + color: Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoTheme.of(context).barBackgroundColor + : Theme.of(context).bottomAppBarTheme.color, + child: SafeArea( + top: false, + child: SizedBox( + height: kBottomBarHeight, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Expanded( + child: BottomBarButton( + label: context.l10n.menu, + onTap: () { + _showAnalysisMenu(context, ref); + }, + icon: Icons.menu, + ), + ), + if (canShowGameSummary) + Expanded( + child: BottomBarButton( + label: displayMode == DisplayMode.summary + ? 'Moves' + : 'Summary', + onTap: () { + ref.read(ctrlProvider.notifier).toggleDisplayMode(); + }, + icon: displayMode == DisplayMode.summary + ? LichessIcons.flow_cascade + : Icons.area_chart, + ), + ), + Expanded( + child: RepeatButton( + onLongPress: canGoBack ? () => _moveBackward(ref) : null, + child: BottomBarButton( + key: const ValueKey('goto-previous'), + onTap: canGoBack ? () => _moveBackward(ref) : null, + label: 'Previous', + icon: CupertinoIcons.chevron_back, + showTooltip: false, + ), + ), + ), + Expanded( + child: RepeatButton( + onLongPress: canGoNext ? () => _moveForward(ref) : null, + child: BottomBarButton( + key: const ValueKey('goto-next'), + icon: CupertinoIcons.chevron_forward, + label: context.l10n.next, + onTap: canGoNext ? () => _moveForward(ref) : null, + showTooltip: false, + ), + ), + ), + ], + ), + ), + ), ); + } - final content = !analysisState.position.isGameOver - ? (eval != null - ? eval.pvs - .take(numEvalLines) - .map( - (pv) => _Engineline(ctrlProvider, eval.position, pv), - ) - .toList() - : emptyLines) - : emptyLines; - - if (content.length < numEvalLines) { - final padding = List.filled( - numEvalLines - content.length, - _Engineline.empty(ctrlProvider), - ); - content.addAll(padding); - } - - return Padding( - padding: EdgeInsets.symmetric( - vertical: isLandscape ? kTabletBoardTableSidePadding : 0.0, - horizontal: isLandscape ? kTabletBoardTableSidePadding : 0.0, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: content, - ), + void _moveForward(WidgetRef ref) => + ref.read(analysisControllerProvider(pgn, options).notifier).userNext(); + void _moveBackward(WidgetRef ref) => ref + .read(analysisControllerProvider(pgn, options).notifier) + .userPrevious(); + + Future _showAnalysisMenu(BuildContext context, WidgetRef ref) { + return showAdaptiveActionSheet( + context: context, + actions: [ + BottomSheetAction( + makeLabel: (context) => Text(context.l10n.flipBoard), + onPressed: (context) { + ref + .read(analysisControllerProvider(pgn, options).notifier) + .toggleBoard(); + }, + ), + BottomSheetAction( + makeLabel: (context) => Text(context.l10n.mobileShareGamePGN), + onPressed: (_) { + pushPlatformRoute( + context, + title: context.l10n.studyShareAndExport, + builder: (_) => AnalysisShareScreen(pgn: pgn, options: options), + ); + }, + ), + BottomSheetAction( + makeLabel: (context) => Text(context.l10n.mobileSharePositionAsFEN), + onPressed: (_) { + launchShareDialog( + context, + text: ref + .read(analysisControllerProvider(pgn, options)) + .position + .fen, + ); + }, + ), + if (options.gameAnyId != null) + BottomSheetAction( + makeLabel: (context) => + Text(context.l10n.screenshotCurrentPosition), + onPressed: (_) async { + final gameId = options.gameAnyId!.gameId; + final state = ref.read(analysisControllerProvider(pgn, options)); + try { + final image = + await ref.read(gameShareServiceProvider).screenshotPosition( + gameId, + options.orientation, + state.position.fen, + state.lastMove, + ); + if (context.mounted) { + launchShareDialog( + context, + files: [image], + subject: context.l10n.puzzleFromGameLink( + lichessUri('/$gameId').toString(), + ), + ); + } + } catch (e) { + if (context.mounted) { + showPlatformSnackbar( + context, + 'Failed to get GIF', + type: SnackBarType.error, + ); + } + } + }, + ), + ], ); } } -class _LoadedAnalysisScreen extends ConsumerWidget { - final AnalysisOptions options; - - final String pgn; - final String? title; +class _EngineDepth extends ConsumerWidget { + const _EngineDepth(this.ctrlProvider); - const _LoadedAnalysisScreen({ - required this.options, - required this.pgn, - this.title, - }); + final AnalysisControllerProvider ctrlProvider; @override Widget build(BuildContext context, WidgetRef ref) { - return ConsumerPlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, - ref: ref, + final isEngineAvailable = ref.watch( + ctrlProvider.select( + (value) => value.isEngineAvailable, + ), ); - } - - Widget _androidBuilder(BuildContext context, WidgetRef ref) { - final ctrlProvider = analysisControllerProvider(pgn, options); + final currentNode = ref.watch( + ctrlProvider.select((value) => value.currentNode), + ); + final depth = ref.watch( + engineEvaluationProvider.select((value) => value.eval?.depth), + ) ?? + currentNode.eval?.depth; - return Scaffold( - resizeToAvoidBottomInset: false, - appBar: AppBar( - title: _Title(options: options, title: title), - actions: [ - _EngineDepth(ctrlProvider), - AppBarIconButton( - onPressed: () => showAdaptiveBottomSheet( - context: context, - isScrollControlled: true, - showDragHandle: true, - isDismissible: true, - builder: (_) => AnalysisSettings(pgn, options), + return isEngineAvailable && depth != null + ? AppBarTextButton( + onPressed: () { + showPopover( + context: context, + bodyBuilder: (context) { + return _StockfishInfo(currentNode); + }, + direction: PopoverDirection.top, + width: 240, + backgroundColor: + Theme.of(context).platform == TargetPlatform.android + ? Theme.of(context).dialogBackgroundColor + : CupertinoDynamicColor.resolve( + CupertinoColors.tertiarySystemBackground, + context, + ), + transitionDuration: Duration.zero, + popoverTransitionBuilder: (_, child) => child, + ); + }, + child: RepaintBoundary( + child: Container( + width: 20.0, + height: 20.0, + padding: const EdgeInsets.all(2.0), + decoration: BoxDecoration( + color: Theme.of(context).platform == TargetPlatform.android + ? Theme.of(context).colorScheme.secondary + : CupertinoTheme.of(context).primaryColor, + borderRadius: BorderRadius.circular(4.0), + ), + child: FittedBox( + fit: BoxFit.contain, + child: Text( + '${math.min(99, depth)}', + style: TextStyle( + color: Theme.of(context).platform == + TargetPlatform.android + ? Theme.of(context).colorScheme.onSecondary + : CupertinoTheme.of(context).primaryContrastingColor, + fontFeatures: const [ + FontFeature.tabularFigures(), + ], + ), + ), + ), + ), ), - semanticsLabel: context.l10n.settingsSettings, - icon: const Icon(Icons.settings), - ), - ], - ), - body: _Body(pgn: pgn, options: options), - ); + ) + : const SizedBox.shrink(); } +} - Widget _iosBuilder(BuildContext context, WidgetRef ref) { - final ctrlProvider = analysisControllerProvider(pgn, options); +class _StockfishInfo extends ConsumerWidget { + const _StockfishInfo(this.currentNode); - return CupertinoPageScaffold( - resizeToAvoidBottomInset: false, - navigationBar: CupertinoNavigationBar( - backgroundColor: Styles.cupertinoScaffoldColor.resolveFrom(context), - border: null, - padding: Styles.cupertinoAppBarTrailingWidgetPadding, - middle: _Title(options: options, title: title), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - _EngineDepth(ctrlProvider), - AppBarIconButton( - onPressed: () => showAdaptiveBottomSheet( - context: context, - isScrollControlled: true, - showDragHandle: true, - isDismissible: true, - builder: (_) => AnalysisSettings(pgn, options), - ), - semanticsLabel: context.l10n.settingsSettings, - icon: const Icon(Icons.settings), + final AnalysisCurrentNode currentNode; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final (engineName: engineName, eval: eval, state: engineState) = + ref.watch(engineEvaluationProvider); + + final currentEval = eval ?? currentNode.eval; + + final knps = engineState == EngineState.computing + ? ', ${eval?.knps.round()}kn/s' + : ''; + final depth = currentEval?.depth ?? 0; + final maxDepth = math.max(depth, kMaxEngineDepth); + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + PlatformListTile( + leading: Image.asset( + 'assets/images/stockfish/icon.png', + width: 44, + height: 44, + ), + title: Text(engineName), + subtitle: Text( + context.l10n.depthX( + '$depth/$maxDepth$knps', ), - ], + ), ), - ), - child: _Body(pgn: pgn, options: options), + ], ); } } -class _LoadGame extends ConsumerWidget { - final AnalysisOptions options; +class ServerAnalysisSummary extends ConsumerWidget { + const ServerAnalysisSummary(this.pgn, this.options); - final GameId gameId; - final String? title; - const _LoadGame(this.gameId, this.options, this.title); + final String pgn; + final AnalysisOptions options; @override Widget build(BuildContext context, WidgetRef ref) { - final gameAsync = ref.watch(archivedGameProvider(id: gameId)); + final ctrlProvider = analysisControllerProvider(pgn, options); + final playersAnalysis = + ref.watch(ctrlProvider.select((value) => value.playersAnalysis)); + final pgnHeaders = + ref.watch(ctrlProvider.select((value) => value.pgnHeaders)); + final currentGameAnalysis = ref.watch(currentAnalysisProvider); - return gameAsync.when( - data: (game) { - final serverAnalysis = - game.white.analysis != null && game.black.analysis != null - ? (white: game.white.analysis!, black: game.black.analysis!) - : null; - return _LoadedAnalysisScreen( - options: options.copyWith( - id: game.id, - opening: game.meta.opening, - division: game.meta.division, - serverAnalysis: serverAnalysis, - ), - pgn: game.makePgn(), - title: title, - ); - }, - loading: () => const Center(child: CircularProgressIndicator.adaptive()), - error: (error, _) { - return Center( - child: Text('Cannot load game analysis: $error'), - ); - }, - ); + return playersAnalysis != null + ? ListView( + children: [ + if (currentGameAnalysis == options.gameAnyId?.gameId) + const Padding( + padding: EdgeInsets.only(top: 16.0), + child: WaitingForServerAnalysis(), + ), + AcplChart(pgn, options), + Center( + child: SizedBox( + width: math.min(MediaQuery.sizeOf(context).width, 500), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Table( + defaultVerticalAlignment: + TableCellVerticalAlignment.middle, + columnWidths: const { + 0: FlexColumnWidth(1), + 1: FlexColumnWidth(1), + 2: FlexColumnWidth(1), + }, + children: [ + TableRow( + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide(color: Colors.grey), + ), + ), + children: [ + _SummaryPlayerName(Side.white, pgnHeaders), + Center( + child: Text( + pgnHeaders.get('Result') ?? '', + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + ), + _SummaryPlayerName(Side.black, pgnHeaders), + ], + ), + if (playersAnalysis.white.accuracy != null && + playersAnalysis.black.accuracy != null) + TableRow( + children: [ + _SummaryNumber( + '${playersAnalysis.white.accuracy}%', + ), + Center( + heightFactor: 1.8, + child: Text( + context.l10n.accuracy, + softWrap: true, + ), + ), + _SummaryNumber( + '${playersAnalysis.black.accuracy}%', + ), + ], + ), + for (final item in [ + ( + playersAnalysis.white.inaccuracies.toString(), + context.l10n + .nbInaccuracies(2) + .replaceAll('2', '') + .trim() + .capitalize(), + playersAnalysis.black.inaccuracies.toString() + ), + ( + playersAnalysis.white.mistakes.toString(), + context.l10n + .nbMistakes(2) + .replaceAll('2', '') + .trim() + .capitalize(), + playersAnalysis.black.mistakes.toString() + ), + ( + playersAnalysis.white.blunders.toString(), + context.l10n + .nbBlunders(2) + .replaceAll('2', '') + .trim() + .capitalize(), + playersAnalysis.black.blunders.toString() + ), + ]) + TableRow( + children: [ + _SummaryNumber(item.$1), + Center( + heightFactor: 1.2, + child: Text( + item.$2, + softWrap: true, + ), + ), + _SummaryNumber(item.$3), + ], + ), + if (playersAnalysis.white.acpl != null && + playersAnalysis.black.acpl != null) + TableRow( + children: [ + _SummaryNumber( + playersAnalysis.white.acpl.toString(), + ), + Center( + heightFactor: 1.5, + child: Text( + context.l10n.averageCentipawnLoss, + softWrap: true, + textAlign: TextAlign.center, + ), + ), + _SummaryNumber( + playersAnalysis.black.acpl.toString(), + ), + ], + ), + ], + ), + ), + ), + ), + ], + ) + : Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Spacer(), + if (currentGameAnalysis == options.gameAnyId?.gameId) + const Center( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 16.0), + child: WaitingForServerAnalysis(), + ), + ) + else + Center( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Builder( + builder: (context) { + Future? pendingRequest; + return StatefulBuilder( + builder: (context, setState) { + return FutureBuilder( + future: pendingRequest, + builder: (context, snapshot) { + return SecondaryButton( + semanticsLabel: + context.l10n.requestAComputerAnalysis, + onPressed: ref.watch(authSessionProvider) == + null + ? () { + showPlatformSnackbar( + context, + context + .l10n.youNeedAnAccountToDoThat, + ); + } + : snapshot.connectionState == + ConnectionState.waiting + ? null + : () { + setState(() { + pendingRequest = ref + .read(ctrlProvider.notifier) + .requestServerAnalysis() + .catchError((Object e) { + if (context.mounted) { + showPlatformSnackbar( + context, + e.toString(), + type: SnackBarType.error, + ); + } + }); + }); + }, + child: Text( + context.l10n.requestAComputerAnalysis, + ), + ); + }, + ); + }, + ); + }, + ), + ), + ), + const Spacer(), + ], + ); } } -class _StockfishInfo extends ConsumerWidget { - final AnalysisCurrentNode currentNode; - - const _StockfishInfo(this.currentNode); +class WaitingForServerAnalysis extends StatelessWidget { + const WaitingForServerAnalysis({super.key}); @override - Widget build(BuildContext context, WidgetRef ref) { - final (engineName: engineName, eval: eval, state: engineState) = - ref.watch(engineEvaluationProvider); - - final currentEval = eval ?? currentNode.eval; - - final knps = engineState == EngineState.computing - ? ', ${eval?.knps.round()}kn/s' - : ''; - final depth = currentEval?.depth ?? 0; - final maxDepth = math.max(depth, kMaxEngineDepth); - - return Column( - mainAxisSize: MainAxisSize.min, + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, children: [ - PlatformListTile( - leading: Image.asset( - 'assets/images/stockfish/icon.png', - width: 44, - height: 44, - ), - title: Text(engineName), - subtitle: Text( - context.l10n.depthX( - '$depth/$maxDepth$knps', - ), - ), + Image.asset( + 'assets/images/stockfish/icon.png', + width: 30, + height: 30, ), + const SizedBox(width: 8.0), + Text(context.l10n.waitingForAnalysis), + const SizedBox(width: 8.0), + const CircularProgressIndicator.adaptive(), ], ); } } class _SummaryNumber extends StatelessWidget { - final String data; const _SummaryNumber(this.data); + final String data; @override Widget build(BuildContext context) { @@ -1085,9 +1104,9 @@ class _SummaryNumber extends StatelessWidget { } class _SummaryPlayerName extends StatelessWidget { + const _SummaryPlayerName(this.side, this.pgnHeaders); final Side side; final IMap pgnHeaders; - const _SummaryPlayerName(this.side, this.pgnHeaders); @override Widget build(BuildContext context) { @@ -1133,27 +1152,182 @@ class _SummaryPlayerName extends StatelessWidget { } } -class _Title extends StatelessWidget { +class AcplChart extends ConsumerWidget { + const AcplChart(this.pgn, this.options); + + final String pgn; final AnalysisOptions options; - final String? title; - const _Title({ - required this.options, - this.title, - }); @override - Widget build(BuildContext context) { - return title != null - ? Text(title!) - : Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (options.variant != Variant.standard) ...[ - Icon(options.variant.icon), - const SizedBox(width: 5.0), + Widget build(BuildContext context, WidgetRef ref) { + final mainLineColor = Theme.of(context).colorScheme.secondary; + // yes it looks like below/above are inverted in fl_chart + final brightness = Theme.of(context).brightness; + final white = Theme.of(context).colorScheme.surfaceContainerHighest; + final black = Theme.of(context).colorScheme.outline; + // yes it looks like below/above are inverted in fl_chart + final belowLineColor = brightness == Brightness.light ? white : black; + final aboveLineColor = brightness == Brightness.light ? black : white; + + VerticalLine phaseVerticalBar(double x, String label) => VerticalLine( + x: x, + color: const Color(0xFF707070), + strokeWidth: 0.5, + label: VerticalLineLabel( + style: TextStyle( + fontSize: 10, + color: Theme.of(context) + .textTheme + .labelMedium + ?.color + ?.withOpacity(0.3), + ), + labelResolver: (line) => label, + padding: const EdgeInsets.only(right: 1), + alignment: Alignment.topRight, + direction: LabelDirection.vertical, + show: true, + ), + ); + + final data = ref.watch( + analysisControllerProvider(pgn, options) + .select((value) => value.acplChartData), + ); + + final rootPly = ref.watch( + analysisControllerProvider(pgn, options) + .select((value) => value.root.position.ply), + ); + + final currentNode = ref.watch( + analysisControllerProvider(pgn, options) + .select((value) => value.currentNode), + ); + + final isOnMainline = ref.watch( + analysisControllerProvider(pgn, options) + .select((value) => value.isOnMainline), + ); + + if (data == null) { + return const SizedBox.shrink(); + } + + final spots = data + .mapIndexed( + (i, e) => FlSpot(i.toDouble(), e.winningChances(Side.white)), + ) + .toList(growable: false); + + final divisionLines = []; + + if (options.division?.middlegame != null) { + if (options.division!.middlegame! > 0) { + divisionLines.add(phaseVerticalBar(0.0, context.l10n.opening)); + divisionLines.add( + phaseVerticalBar( + options.division!.middlegame! - 1, + context.l10n.middlegame, + ), + ); + } else { + divisionLines.add(phaseVerticalBar(0.0, context.l10n.middlegame)); + } + } + + if (options.division?.endgame != null) { + if (options.division!.endgame! > 0) { + divisionLines.add( + phaseVerticalBar( + options.division!.endgame! - 1, + context.l10n.endgame, + ), + ); + } else { + divisionLines.add( + phaseVerticalBar( + 0.0, + context.l10n.endgame, + ), + ); + } + } + return Center( + child: AspectRatio( + aspectRatio: 2.5, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: LineChart( + LineChartData( + lineTouchData: LineTouchData( + enabled: false, + touchCallback: + (FlTouchEvent event, LineTouchResponse? touchResponse) { + if (event is FlTapDownEvent || + event is FlPanUpdateEvent || + event is FlLongPressMoveUpdate) { + final touchX = event.localPosition!.dx; + final chartWidth = context.size!.width - + 32; // Insets on both sides of the chart of 16 + final minX = spots.first.x; + final maxX = spots.last.x; + final touchXDataValue = + minX + (touchX / chartWidth) * (maxX - minX); + final closestSpot = spots.reduce( + (a, b) => (a.x - touchXDataValue).abs() < + (b.x - touchXDataValue).abs() + ? a + : b, + ); + final closestNodeIndex = closestSpot.x.round(); + ref + .read(analysisControllerProvider(pgn, options).notifier) + .jumpToNthNodeOnMainline(closestNodeIndex); + } + }, + ), + minY: -1.0, + maxY: 1.0, + lineBarsData: [ + LineChartBarData( + spots: spots, + isCurved: false, + barWidth: 1, + color: mainLineColor.withOpacity(0.7), + aboveBarData: BarAreaData( + show: true, + color: aboveLineColor, + applyCutOffY: true, + ), + belowBarData: BarAreaData( + show: true, + color: belowLineColor, + applyCutOffY: true, + ), + dotData: const FlDotData( + show: false, + ), + ), ], - Text(context.l10n.analysis), - ], - ); + extraLinesData: ExtraLinesData( + verticalLines: [ + if (isOnMainline) + VerticalLine( + x: (currentNode.position.ply - 1 - rootPly).toDouble(), + color: mainLineColor, + strokeWidth: 1.0, + ), + ...divisionLines, + ], + ), + gridData: const FlGridData(show: false), + borderData: FlBorderData(show: false), + titlesData: const FlTitlesData(show: false), + ), + ), + ), + ), + ); } } diff --git a/lib/src/view/analysis/analysis_widgets.dart b/lib/src/view/analysis/analysis_widgets.dart index 25d4abd636..85cf524abd 100644 --- a/lib/src/view/analysis/analysis_widgets.dart +++ b/lib/src/view/analysis/analysis_widgets.dart @@ -13,22 +13,10 @@ import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/eval.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; -import 'package:lichess_mobile/src/model/game/game_share_service.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; -import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/utils/chessground_compat.dart'; -import 'package:lichess_mobile/src/utils/l10n_context.dart'; -import 'package:lichess_mobile/src/utils/navigation.dart'; -import 'package:lichess_mobile/src/utils/share.dart'; import 'package:lichess_mobile/src/view/analysis/annotations.dart'; -import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; -import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; -import 'package:lichess_mobile/src/widgets/buttons.dart'; -import 'package:lichess_mobile/src/widgets/feedback.dart'; - -import 'analysis_share_screen.dart'; class AnalysisBoard extends ConsumerStatefulWidget { const AnalysisBoard( @@ -210,169 +198,3 @@ class AnalysisBoardState extends ConsumerState { } } } - -class BottomBar extends ConsumerWidget { - const BottomBar({ - required this.pgn, - required this.options, - }); - - final String pgn; - final AnalysisOptions options; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final ctrlProvider = analysisControllerProvider(pgn, options); - final canGoBack = - ref.watch(ctrlProvider.select((value) => value.canGoBack)); - final canGoNext = - ref.watch(ctrlProvider.select((value) => value.canGoNext)); - final displayMode = - ref.watch(ctrlProvider.select((value) => value.displayMode)); - final canShowGameSummary = - ref.watch(ctrlProvider.select((value) => value.canShowGameSummary)); - - return Container( - color: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoTheme.of(context).barBackgroundColor - : Theme.of(context).bottomAppBarTheme.color, - child: SafeArea( - top: false, - child: SizedBox( - height: kBottomBarHeight, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - Expanded( - child: BottomBarButton( - label: context.l10n.menu, - onTap: () { - _showAnalysisMenu(context, ref); - }, - icon: Icons.menu, - ), - ), - if (canShowGameSummary) - Expanded( - child: BottomBarButton( - label: displayMode == DisplayMode.summary - ? 'Moves' - : 'Summary', - onTap: () { - ref.read(ctrlProvider.notifier).toggleDisplayMode(); - }, - icon: displayMode == DisplayMode.summary - ? LichessIcons.flow_cascade - : Icons.area_chart, - ), - ), - Expanded( - child: RepeatButton( - onLongPress: canGoBack ? () => _moveBackward(ref) : null, - child: BottomBarButton( - key: const ValueKey('goto-previous'), - onTap: canGoBack ? () => _moveBackward(ref) : null, - label: 'Previous', - icon: CupertinoIcons.chevron_back, - showTooltip: false, - ), - ), - ), - Expanded( - child: RepeatButton( - onLongPress: canGoNext ? () => _moveForward(ref) : null, - child: BottomBarButton( - key: const ValueKey('goto-next'), - icon: CupertinoIcons.chevron_forward, - label: context.l10n.next, - onTap: canGoNext ? () => _moveForward(ref) : null, - showTooltip: false, - ), - ), - ), - ], - ), - ), - ), - ); - } - - void _moveBackward(WidgetRef ref) => ref - .read(analysisControllerProvider(pgn, options).notifier) - .userPrevious(); - void _moveForward(WidgetRef ref) => - ref.read(analysisControllerProvider(pgn, options).notifier).userNext(); - - Future _showAnalysisMenu(BuildContext context, WidgetRef ref) { - return showAdaptiveActionSheet( - context: context, - actions: [ - BottomSheetAction( - makeLabel: (context) => Text(context.l10n.flipBoard), - onPressed: (context) { - ref - .read(analysisControllerProvider(pgn, options).notifier) - .toggleBoard(); - }, - ), - BottomSheetAction( - makeLabel: (context) => Text(context.l10n.mobileShareGamePGN), - onPressed: (_) { - pushPlatformRoute( - context, - title: context.l10n.studyShareAndExport, - builder: (_) => AnalysisShareScreen(pgn: pgn, options: options), - ); - }, - ), - BottomSheetAction( - makeLabel: (context) => Text(context.l10n.mobileSharePositionAsFEN), - onPressed: (_) { - launchShareDialog( - context, - text: ref - .read(analysisControllerProvider(pgn, options)) - .position - .fen, - ); - }, - ), - if (options.gameAnyId != null) - BottomSheetAction( - makeLabel: (context) => - Text(context.l10n.screenshotCurrentPosition), - onPressed: (_) async { - final gameId = options.gameAnyId!.gameId; - final state = ref.read(analysisControllerProvider(pgn, options)); - try { - final image = - await ref.read(gameShareServiceProvider).screenshotPosition( - gameId, - options.orientation, - state.position.fen, - state.lastMove, - ); - if (context.mounted) { - launchShareDialog( - context, - files: [image], - subject: context.l10n.puzzleFromGameLink( - lichessUri('/$gameId').toString(), - ), - ); - } - } catch (e) { - if (context.mounted) { - showPlatformSnackbar( - context, - 'Failed to get GIF', - type: SnackBarType.error, - ); - } - } - }, - ), - ], - ); - } -} diff --git a/lib/src/view/analysis/opening_explorer_screen.dart b/lib/src/view/analysis/opening_explorer_screen.dart index c440887de9..adcef0a736 100644 --- a/lib/src/view/analysis/opening_explorer_screen.dart +++ b/lib/src/view/analysis/opening_explorer_screen.dart @@ -148,7 +148,6 @@ class _Body extends ConsumerWidget { ), ), ), - BottomBar(pgn: pgn, options: options), ], ); } From 743bb5338bbddcbe4d736eff081be4f82406ca2e Mon Sep 17 00:00:00 2001 From: Mauritz Date: Mon, 29 Jul 2024 07:44:10 +0200 Subject: [PATCH 076/979] feat: bottom bar icons to switch between analysis and explorer --- lib/src/view/analysis/analysis_screen.dart | 14 ++ .../analysis/opening_explorer_screen.dart | 172 ++++++++++++++++++ 2 files changed, 186 insertions(+) diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index dad1b1ae48..4312d56336 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -42,6 +42,7 @@ import 'package:popover/popover.dart'; import '../../utils/share.dart'; import 'analysis_settings.dart'; import 'analysis_widgets.dart'; +import 'opening_explorer_screen.dart'; import 'tree_view.dart'; class AnalysisScreen extends StatelessWidget { @@ -623,6 +624,19 @@ class _BottomBar extends ConsumerWidget { : Icons.area_chart, ), ), + Expanded( + child: BottomBarButton( + label: context.l10n.openingExplorer, + onTap: () => pushReplacementPlatformRoute( + context, + builder: (_) => OpeningExplorerScreen( + pgn: pgn, + options: options, + ), + ), + icon: Icons.explore, + ), + ), Expanded( child: RepeatButton( onLongPress: canGoBack ? () => _moveBackward(ref) : null, diff --git a/lib/src/view/analysis/opening_explorer_screen.dart b/lib/src/view/analysis/opening_explorer_screen.dart index adcef0a736..bcfc2776f8 100644 --- a/lib/src/view/analysis/opening_explorer_screen.dart +++ b/lib/src/view/analysis/opening_explorer_screen.dart @@ -6,9 +6,19 @@ import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/analysis/opening_explorer_repository.dart'; +import 'package:lichess_mobile/src/model/common/http.dart'; +import 'package:lichess_mobile/src/model/game/game_share_service.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; +import 'package:lichess_mobile/src/utils/share.dart'; +import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; +import 'package:lichess_mobile/src/view/analysis/analysis_share_screen.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; +import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; import 'analysis_widgets.dart'; @@ -148,6 +158,7 @@ class _Body extends ConsumerWidget { ), ), ), + _BottomBar(pgn: pgn, options: options), ], ); } @@ -323,3 +334,164 @@ class _WinPercentageChart extends StatelessWidget { ); } } + +class _BottomBar extends ConsumerWidget { + const _BottomBar({ + required this.pgn, + required this.options, + }); + + final String pgn; + final AnalysisOptions options; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final ctrlProvider = analysisControllerProvider(pgn, options); + final canGoBack = + ref.watch(ctrlProvider.select((value) => value.canGoBack)); + final canGoNext = + ref.watch(ctrlProvider.select((value) => value.canGoNext)); + + return Container( + color: Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoTheme.of(context).barBackgroundColor + : Theme.of(context).bottomAppBarTheme.color, + child: SafeArea( + top: false, + child: SizedBox( + height: kBottomBarHeight, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Expanded( + child: BottomBarButton( + label: context.l10n.menu, + onTap: () { + _showAnalysisMenu(context, ref); + }, + icon: Icons.menu, + ), + ), + Expanded( + child: BottomBarButton( + label: context.l10n.analysis, + onTap: () => pushReplacementPlatformRoute( + context, + builder: (_) => AnalysisScreen( + pgnOrId: pgn, + options: options, + ), + ), + icon: Icons.biotech, + ), + ), + Expanded( + child: RepeatButton( + onLongPress: canGoBack ? () => _moveBackward(ref) : null, + child: BottomBarButton( + key: const ValueKey('goto-previous'), + onTap: canGoBack ? () => _moveBackward(ref) : null, + label: 'Previous', + icon: CupertinoIcons.chevron_back, + showTooltip: false, + ), + ), + ), + Expanded( + child: RepeatButton( + onLongPress: canGoNext ? () => _moveForward(ref) : null, + child: BottomBarButton( + key: const ValueKey('goto-next'), + icon: CupertinoIcons.chevron_forward, + label: context.l10n.next, + onTap: canGoNext ? () => _moveForward(ref) : null, + showTooltip: false, + ), + ), + ), + ], + ), + ), + ), + ); + } + + void _moveForward(WidgetRef ref) => + ref.read(analysisControllerProvider(pgn, options).notifier).userNext(); + void _moveBackward(WidgetRef ref) => ref + .read(analysisControllerProvider(pgn, options).notifier) + .userPrevious(); + + Future _showAnalysisMenu(BuildContext context, WidgetRef ref) { + return showAdaptiveActionSheet( + context: context, + actions: [ + BottomSheetAction( + makeLabel: (context) => Text(context.l10n.flipBoard), + onPressed: (context) { + ref + .read(analysisControllerProvider(pgn, options).notifier) + .toggleBoard(); + }, + ), + BottomSheetAction( + makeLabel: (context) => Text(context.l10n.mobileShareGamePGN), + onPressed: (_) { + pushPlatformRoute( + context, + title: context.l10n.studyShareAndExport, + builder: (_) => AnalysisShareScreen(pgn: pgn, options: options), + ); + }, + ), + BottomSheetAction( + makeLabel: (context) => Text(context.l10n.mobileSharePositionAsFEN), + onPressed: (_) { + launchShareDialog( + context, + text: ref + .read(analysisControllerProvider(pgn, options)) + .position + .fen, + ); + }, + ), + if (options.gameAnyId != null) + BottomSheetAction( + makeLabel: (context) => + Text(context.l10n.screenshotCurrentPosition), + onPressed: (_) async { + final gameId = options.gameAnyId!.gameId; + final state = ref.read(analysisControllerProvider(pgn, options)); + try { + final image = + await ref.read(gameShareServiceProvider).screenshotPosition( + gameId, + options.orientation, + state.position.fen, + state.lastMove, + ); + if (context.mounted) { + launchShareDialog( + context, + files: [image], + subject: context.l10n.puzzleFromGameLink( + lichessUri('/$gameId').toString(), + ), + ); + } + } catch (e) { + if (context.mounted) { + showPlatformSnackbar( + context, + 'Failed to get GIF', + type: SnackBarType.error, + ); + } + } + }, + ), + ], + ); + } +} From 4c3640c86f8381a3d85b64272175b22b6774ce57 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Mon, 29 Jul 2024 08:16:56 +0200 Subject: [PATCH 077/979] feat: add settings (empty for now) --- .../analysis/opening_explorer_screen.dart | 26 ++++++++++++++ .../analysis/opening_explorer_settings.dart | 35 +++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 lib/src/view/analysis/opening_explorer_settings.dart diff --git a/lib/src/view/analysis/opening_explorer_screen.dart b/lib/src/view/analysis/opening_explorer_screen.dart index bcfc2776f8..8e758f2857 100644 --- a/lib/src/view/analysis/opening_explorer_screen.dart +++ b/lib/src/view/analysis/opening_explorer_screen.dart @@ -16,12 +16,14 @@ import 'package:lichess_mobile/src/utils/share.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_share_screen.dart'; import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; import 'analysis_widgets.dart'; +import 'opening_explorer_settings.dart'; class OpeningExplorerScreen extends StatelessWidget { const OpeningExplorerScreen({required this.pgn, required this.options}); @@ -41,6 +43,19 @@ class OpeningExplorerScreen extends StatelessWidget { return Scaffold( appBar: AppBar( title: Text(context.l10n.openingExplorer), + actions: [ + AppBarIconButton( + onPressed: () => showAdaptiveBottomSheet( + context: context, + isScrollControlled: true, + showDragHandle: true, + isDismissible: true, + builder: (_) => OpeningExplorerSettings(pgn, options), + ), + semanticsLabel: context.l10n.settingsSettings, + icon: const Icon(Icons.settings), + ), + ], ), body: _Body(pgn: pgn, options: options), ); @@ -52,6 +67,17 @@ class OpeningExplorerScreen extends StatelessWidget { backgroundColor: Styles.cupertinoScaffoldColor.resolveFrom(context), border: null, middle: Text(context.l10n.openingExplorer), + trailing: AppBarIconButton( + onPressed: () => showAdaptiveBottomSheet( + context: context, + isScrollControlled: true, + showDragHandle: true, + isDismissible: true, + builder: (_) => OpeningExplorerSettings(pgn, options), + ), + semanticsLabel: context.l10n.settingsSettings, + icon: const Icon(Icons.settings), + ), ), child: _Body(pgn: pgn, options: options), ); diff --git a/lib/src/view/analysis/opening_explorer_settings.dart b/lib/src/view/analysis/opening_explorer_settings.dart new file mode 100644 index 0000000000..df38a6ee84 --- /dev/null +++ b/lib/src/view/analysis/opening_explorer_settings.dart @@ -0,0 +1,35 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/widgets/list.dart'; + +class OpeningExplorerSettings extends ConsumerWidget { + const OpeningExplorerSettings(this.pgn, this.options); + + final String pgn; + final AnalysisOptions options; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return DraggableScrollableSheet( + initialChildSize: .7, + expand: false, + snap: true, + snapSizes: const [.7], + builder: (context, scrollController) => ListView( + controller: scrollController, + children: [ + PlatformListTile( + title: + Text(context.l10n.settingsSettings, style: Styles.sectionTitle), + subtitle: const SizedBox.shrink(), + ), + const SizedBox(height: 8.0), + + ], + ), + ); + } +} From 1449de1502d722d2de558e0731d4a9e725969e36 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Mon, 29 Jul 2024 08:48:15 +0200 Subject: [PATCH 078/979] feat: show opening name --- .../analysis/opening_explorer_screen.dart | 119 +++++++++++------- 1 file changed, 71 insertions(+), 48 deletions(-) diff --git a/lib/src/view/analysis/opening_explorer_screen.dart b/lib/src/view/analysis/opening_explorer_screen.dart index 8e758f2857..a999ee12ee 100644 --- a/lib/src/view/analysis/opening_explorer_screen.dart +++ b/lib/src/view/analysis/opening_explorer_screen.dart @@ -6,6 +6,7 @@ import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/analysis/opening_explorer_repository.dart'; +import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/game/game_share_service.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; @@ -204,6 +205,22 @@ class _OpeningExplorer extends ConsumerWidget { final ctrlProvider = analysisControllerProvider(pgn, options); final position = ref.watch(ctrlProvider.select((value) => value.position)); + final isRootNode = ref.watch( + ctrlProvider.select((s) => s.currentNode.isRoot), + ); + final nodeOpening = + ref.watch(ctrlProvider.select((s) => s.currentNode.opening)); + final branchOpening = + ref.watch(ctrlProvider.select((s) => s.currentBranchOpening)); + final contextOpening = + ref.watch(ctrlProvider.select((s) => s.contextOpening)); + final opening = isRootNode + ? LightOpening( + eco: '', + name: context.l10n.startPosition, + ) + : nodeOpening ?? branchOpening ?? contextOpening; + if (position.fullmoves > 24) { return const Expanded( child: Column( @@ -228,62 +245,68 @@ class _OpeningExplorer extends ConsumerWidget { ], ) : Container( - width: MediaQuery.of(context).size.width, - padding: const EdgeInsets.symmetric( - vertical: 8.0, - horizontal: 16.0, - ), + padding: Styles.bodyPadding, child: SingleChildScrollView( scrollDirection: Axis.vertical, - child: DataTable( - showCheckboxColumn: false, - columnSpacing: 5, - horizontalMargin: 0, - columns: const [ - DataColumn(label: Text('Move')), - DataColumn(label: Text('Games')), - DataColumn(label: Text('White / Draw / Black')), - ], - rows: [ - ...masterDatabase.moves.map( - (move) => DataRow( - onSelectChanged: (_) => ref - .read(ctrlProvider.notifier) - .onUserMove(Move.fromUci(move.uci)!), - cells: [ - DataCell(Text(move.san)), - DataCell( - Text( - '${((move.games / masterDatabase.games) * 100).round()}% / ${formatNum(move.games)}', + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (opening != null) + Text('${opening.eco} ${opening.name}'), + SizedBox( + width: MediaQuery.sizeOf(context).width, + child: DataTable( + showCheckboxColumn: false, + columnSpacing: 5, + horizontalMargin: 0, + columns: const [ + DataColumn(label: Text('Move')), + DataColumn(label: Text('Games')), + DataColumn(label: Text('White / Draw / Black')), + ], + rows: [ + ...masterDatabase.moves.map( + (move) => DataRow( + onSelectChanged: (_) => ref + .read(ctrlProvider.notifier) + .onUserMove(Move.fromUci(move.uci)!), + cells: [ + DataCell(Text(move.san)), + DataCell( + Text( + '${((move.games / masterDatabase.games) * 100).round()}% / ${formatNum(move.games)}', + ), + ), + DataCell( + _WinPercentageChart( + white: move.white, + draws: move.draws, + black: move.black, + ), + ), + ], ), ), - DataCell( - _WinPercentageChart( - white: move.white, - draws: move.draws, - black: move.black, - ), + DataRow( + cells: [ + const DataCell(Icon(Icons.functions)), + DataCell( + Text( + '100% / ${formatNum(masterDatabase.games)}', + ), + ), + DataCell( + _WinPercentageChart( + white: masterDatabase.white, + draws: masterDatabase.draws, + black: masterDatabase.black, + ), + ), + ], ), ], ), ), - DataRow( - cells: [ - const DataCell(Icon(Icons.functions)), - DataCell( - Text( - '100% / ${formatNum(masterDatabase.games)}', - ), - ), - DataCell( - _WinPercentageChart( - white: masterDatabase.white, - draws: masterDatabase.draws, - black: masterDatabase.black, - ), - ), - ], - ), ], ), ), From b3321216079e79e49198c8c152c5a72c196d3e3d Mon Sep 17 00:00:00 2001 From: Mauritz Date: Mon, 29 Jul 2024 10:51:01 +0200 Subject: [PATCH 079/979] feat: recreate table from scratch --- .../analysis/opening_explorer_screen.dart | 216 ++++++++++++------ .../analysis/opening_explorer_settings.dart | 1 - 2 files changed, 149 insertions(+), 68 deletions(-) diff --git a/lib/src/view/analysis/opening_explorer_screen.dart b/lib/src/view/analysis/opening_explorer_screen.dart index a999ee12ee..b6d79a2064 100644 --- a/lib/src/view/analysis/opening_explorer_screen.dart +++ b/lib/src/view/analysis/opening_explorer_screen.dart @@ -205,6 +205,30 @@ class _OpeningExplorer extends ConsumerWidget { final ctrlProvider = analysisControllerProvider(pgn, options); final position = ref.watch(ctrlProvider.select((value) => value.position)); + if (position.fullmoves > 24) { + return const Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('Max depth reached'), + ], + ), + ); + } + + final primaryColor = Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoDynamicColor.resolve( + CupertinoColors.systemGrey5, + context, + ) + : Theme.of(context).colorScheme.secondaryContainer; + const rowVerticalPadding = 6.0; + const rowHorizontalPadding = 6.0; + const tableRowPadding = EdgeInsets.symmetric( + vertical: rowVerticalPadding, + horizontal: rowHorizontalPadding, + ); + final isRootNode = ref.watch( ctrlProvider.select((s) => s.currentNode.isRoot), ); @@ -221,17 +245,6 @@ class _OpeningExplorer extends ConsumerWidget { ) : nodeOpening ?? branchOpening ?? contextOpening; - if (position.fullmoves > 24) { - return const Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text('Max depth reached'), - ], - ), - ); - } - final masterDatabaseAsync = ref.watch(masterDatabaseProvider(fen: position.fen)); @@ -244,71 +257,140 @@ class _OpeningExplorer extends ConsumerWidget { Text('No game found'), ], ) - : Container( - padding: Styles.bodyPadding, - child: SingleChildScrollView( - scrollDirection: Axis.vertical, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (opening != null) - Text('${opening.eco} ${opening.name}'), - SizedBox( - width: MediaQuery.sizeOf(context).width, - child: DataTable( - showCheckboxColumn: false, - columnSpacing: 5, - horizontalMargin: 0, - columns: const [ - DataColumn(label: Text('Move')), - DataColumn(label: Text('Games')), - DataColumn(label: Text('White / Draw / Black')), - ], - rows: [ - ...masterDatabase.moves.map( - (move) => DataRow( - onSelectChanged: (_) => ref - .read(ctrlProvider.notifier) - .onUserMove(Move.fromUci(move.uci)!), - cells: [ - DataCell(Text(move.san)), - DataCell( - Text( - '${((move.games / masterDatabase.games) * 100).round()}% / ${formatNum(move.games)}', + : SingleChildScrollView( + scrollDirection: Axis.vertical, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (opening != null) + Container( + padding: + const EdgeInsets.only(left: rowHorizontalPadding), + color: primaryColor, + child: Expanded( + child: Row( + children: [ + if (opening.eco.isEmpty) + Text(opening.name) + else + Text('${opening.eco} ${opening.name}'), + ], + ), + ), + ), + SizedBox( + child: Table( + children: [ + TableRow( + decoration: BoxDecoration( + color: primaryColor, + ), + children: [ + Container( + padding: tableRowPadding, + child: Text(context.l10n.move), + ), + Container( + padding: tableRowPadding, + child: Text(context.l10n.games), + ), + Container( + padding: tableRowPadding, + child: Text(context.l10n.whiteDrawBlack), + ), + ], + ), + ...List.generate( + masterDatabase.moves.length, + (int index) { + final move = masterDatabase.moves.get(index); + final percentGames = + ((move.games / masterDatabase.games) * 100) + .round(); + return TableRow( + decoration: BoxDecoration( + color: index.isEven + ? Theme.of(context) + .colorScheme + .surfaceContainerLow + : Theme.of(context) + .colorScheme + .surfaceContainerHigh, + ), + children: [ + TableRowInkWell( + onTap: () => ref + .read(ctrlProvider.notifier) + .onUserMove(Move.fromUci(move.uci)!), + child: Container( + padding: tableRowPadding, + child: Text(move.san), + ), + ), + TableRowInkWell( + onTap: () => ref + .read(ctrlProvider.notifier) + .onUserMove(Move.fromUci(move.uci)!), + child: Container( + padding: tableRowPadding, + child: Text( + '$percentGames% / ${formatNum(move.games)}', + ), ), ), - DataCell( - _WinPercentageChart( - white: move.white, - draws: move.draws, - black: move.black, + TableRowInkWell( + onTap: () => ref + .read(ctrlProvider.notifier) + .onUserMove(Move.fromUci(move.uci)!), + child: Container( + padding: tableRowPadding, + child: _WinPercentageChart( + white: move.white, + draws: move.draws, + black: move.black, + ), ), ), ], - ), + ); + }, + ), + TableRow( + decoration: BoxDecoration( + color: masterDatabase.moves.length.isEven + ? Theme.of(context) + .colorScheme + .surfaceContainerLow + : Theme.of(context) + .colorScheme + .surfaceContainerHigh, ), - DataRow( - cells: [ - const DataCell(Icon(Icons.functions)), - DataCell( - Text( - '100% / ${formatNum(masterDatabase.games)}', - ), + children: [ + Container( + padding: tableRowPadding, + alignment: Alignment.centerLeft, + child: const Icon(Icons.functions), + ), + Container( + padding: tableRowPadding, + child: Text( + '100% / ${formatNum(masterDatabase.games)}', ), - DataCell( - _WinPercentageChart( - white: masterDatabase.white, - draws: masterDatabase.draws, - black: masterDatabase.black, - ), + ), + Container( + padding: tableRowPadding, + child: _WinPercentageChart( + white: masterDatabase.white, + draws: masterDatabase.draws, + black: masterDatabase.black, ), - ], - ), - ], - ), + ), + ], + ), + ], ), - ], - ), + ), + ], ), ); }, diff --git a/lib/src/view/analysis/opening_explorer_settings.dart b/lib/src/view/analysis/opening_explorer_settings.dart index df38a6ee84..829986cfb1 100644 --- a/lib/src/view/analysis/opening_explorer_settings.dart +++ b/lib/src/view/analysis/opening_explorer_settings.dart @@ -27,7 +27,6 @@ class OpeningExplorerSettings extends ConsumerWidget { subtitle: const SizedBox.shrink(), ), const SizedBox(height: 8.0), - ], ), ); From 1824a77f887f0bde11fd6ca4b1b486f2b650c26c Mon Sep 17 00:00:00 2001 From: Mauritz Date: Mon, 29 Jul 2024 15:06:57 +0200 Subject: [PATCH 080/979] feat: set table column ratios --- lib/src/view/analysis/opening_explorer_screen.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/src/view/analysis/opening_explorer_screen.dart b/lib/src/view/analysis/opening_explorer_screen.dart index b6d79a2064..0ad94e3c78 100644 --- a/lib/src/view/analysis/opening_explorer_screen.dart +++ b/lib/src/view/analysis/opening_explorer_screen.dart @@ -280,6 +280,11 @@ class _OpeningExplorer extends ConsumerWidget { ), SizedBox( child: Table( + columnWidths: const { + 0: FractionColumnWidth(0.2), + 1: FractionColumnWidth(0.3), + 2: FractionColumnWidth(0.5), + }, children: [ TableRow( decoration: BoxDecoration( From d71873bfc05b702e042bca5044f4719bd9281bf9 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Mon, 29 Jul 2024 15:16:25 +0200 Subject: [PATCH 081/979] fix: enable stockfish when going to analysis board from opening explorer --- lib/src/view/tools/tools_tab_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/view/tools/tools_tab_screen.dart b/lib/src/view/tools/tools_tab_screen.dart index b185c04422..1030dbfdd6 100644 --- a/lib/src/view/tools/tools_tab_screen.dart +++ b/lib/src/view/tools/tools_tab_screen.dart @@ -134,7 +134,7 @@ class _Body extends StatelessWidget { builder: (context) => const OpeningExplorerScreen( pgn: '', options: AnalysisOptions( - isLocalEvaluationAllowed: false, + isLocalEvaluationAllowed: true, variant: Variant.standard, orientation: Side.white, id: standaloneAnalysisId, From 18a8bd8a64cf0233cb3c42b45206f61a44133736 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Mon, 29 Jul 2024 16:43:56 +0200 Subject: [PATCH 082/979] feat: graph improvements, including width by percentage --- .../analysis/opening_explorer_screen.dart | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/lib/src/view/analysis/opening_explorer_screen.dart b/lib/src/view/analysis/opening_explorer_screen.dart index 0ad94e3c78..0a40adc0dd 100644 --- a/lib/src/view/analysis/opening_explorer_screen.dart +++ b/lib/src/view/analysis/opening_explorer_screen.dart @@ -413,9 +413,9 @@ class _OpeningExplorer extends ConsumerWidget { class _WinPercentageChart extends StatelessWidget { final int white; - final int draws; final int black; + const _WinPercentageChart({ required this.white, required this.draws, @@ -430,43 +430,47 @@ class _WinPercentageChart extends StatelessWidget { final percentWhite = percentGames(white); final percentDraws = percentGames(draws); final percentBlack = percentGames(black); + String label(int percent) => percent < 20 ? '' : '$percent%'; - return Row( - children: [ - if (percentWhite != 0) + return ClipRRect( + borderRadius: BorderRadius.circular(5), + child: Row( + children: [ Expanded( + flex: percentWhite, child: ColoredBox( color: Colors.white, child: Text( - percentWhite < 5 ? '' : '$percentWhite%', + label(percentWhite), textAlign: TextAlign.center, style: const TextStyle(color: Colors.black), ), ), ), - if (percentDraws != 0) Expanded( + flex: percentDraws, child: ColoredBox( color: Colors.grey, child: Text( - percentDraws < 5 ? '' : '$percentDraws%', + label(percentDraws), textAlign: TextAlign.center, style: const TextStyle(color: Colors.white), ), ), ), - if (percentBlack != 0) Expanded( + flex: percentBlack, child: ColoredBox( color: Colors.black, child: Text( - percentBlack < 5 ? '' : '$percentBlack%', + label(percentBlack), textAlign: TextAlign.center, style: const TextStyle(color: Colors.white), ), ), ), - ], + ], + ), ); } } From f66ab5f0cebd1b5a2c1b152c80a60371e450e7df Mon Sep 17 00:00:00 2001 From: Mauritz Date: Mon, 29 Jul 2024 16:52:43 +0200 Subject: [PATCH 083/979] fix: remove unneeded widgets --- .../analysis/opening_explorer_screen.dart | 224 +++++++++--------- 1 file changed, 110 insertions(+), 114 deletions(-) diff --git a/lib/src/view/analysis/opening_explorer_screen.dart b/lib/src/view/analysis/opening_explorer_screen.dart index 0a40adc0dd..45ea97cacb 100644 --- a/lib/src/view/analysis/opening_explorer_screen.dart +++ b/lib/src/view/analysis/opening_explorer_screen.dart @@ -267,133 +267,129 @@ class _OpeningExplorer extends ConsumerWidget { padding: const EdgeInsets.only(left: rowHorizontalPadding), color: primaryColor, - child: Expanded( - child: Row( - children: [ - if (opening.eco.isEmpty) - Text(opening.name) - else - Text('${opening.eco} ${opening.name}'), - ], - ), + child: Row( + children: [ + if (opening.eco.isEmpty) + Text(opening.name) + else + Text('${opening.eco} ${opening.name}'), + ], ), ), - SizedBox( - child: Table( - columnWidths: const { - 0: FractionColumnWidth(0.2), - 1: FractionColumnWidth(0.3), - 2: FractionColumnWidth(0.5), - }, - children: [ - TableRow( - decoration: BoxDecoration( - color: primaryColor, + Table( + columnWidths: const { + 0: FractionColumnWidth(0.2), + 1: FractionColumnWidth(0.3), + 2: FractionColumnWidth(0.5), + }, + children: [ + TableRow( + decoration: BoxDecoration( + color: primaryColor, + ), + children: [ + Container( + padding: tableRowPadding, + child: Text(context.l10n.move), ), - children: [ - Container( - padding: tableRowPadding, - child: Text(context.l10n.move), - ), - Container( - padding: tableRowPadding, - child: Text(context.l10n.games), - ), - Container( - padding: tableRowPadding, - child: Text(context.l10n.whiteDrawBlack), + Container( + padding: tableRowPadding, + child: Text(context.l10n.games), + ), + Container( + padding: tableRowPadding, + child: Text(context.l10n.whiteDrawBlack), + ), + ], + ), + ...List.generate( + masterDatabase.moves.length, + (int index) { + final move = masterDatabase.moves.get(index); + final percentGames = + ((move.games / masterDatabase.games) * 100) + .round(); + return TableRow( + decoration: BoxDecoration( + color: index.isEven + ? Theme.of(context) + .colorScheme + .surfaceContainerLow + : Theme.of(context) + .colorScheme + .surfaceContainerHigh, ), - ], - ), - ...List.generate( - masterDatabase.moves.length, - (int index) { - final move = masterDatabase.moves.get(index); - final percentGames = - ((move.games / masterDatabase.games) * 100) - .round(); - return TableRow( - decoration: BoxDecoration( - color: index.isEven - ? Theme.of(context) - .colorScheme - .surfaceContainerLow - : Theme.of(context) - .colorScheme - .surfaceContainerHigh, - ), - children: [ - TableRowInkWell( - onTap: () => ref - .read(ctrlProvider.notifier) - .onUserMove(Move.fromUci(move.uci)!), - child: Container( - padding: tableRowPadding, - child: Text(move.san), - ), + children: [ + TableRowInkWell( + onTap: () => ref + .read(ctrlProvider.notifier) + .onUserMove(Move.fromUci(move.uci)!), + child: Container( + padding: tableRowPadding, + child: Text(move.san), ), - TableRowInkWell( - onTap: () => ref - .read(ctrlProvider.notifier) - .onUserMove(Move.fromUci(move.uci)!), - child: Container( - padding: tableRowPadding, - child: Text( - '$percentGames% / ${formatNum(move.games)}', - ), + ), + TableRowInkWell( + onTap: () => ref + .read(ctrlProvider.notifier) + .onUserMove(Move.fromUci(move.uci)!), + child: Container( + padding: tableRowPadding, + child: Text( + '$percentGames% / ${formatNum(move.games)}', ), ), - TableRowInkWell( - onTap: () => ref - .read(ctrlProvider.notifier) - .onUserMove(Move.fromUci(move.uci)!), - child: Container( - padding: tableRowPadding, - child: _WinPercentageChart( - white: move.white, - draws: move.draws, - black: move.black, - ), + ), + TableRowInkWell( + onTap: () => ref + .read(ctrlProvider.notifier) + .onUserMove(Move.fromUci(move.uci)!), + child: Container( + padding: tableRowPadding, + child: _WinPercentageChart( + white: move.white, + draws: move.draws, + black: move.black, ), ), - ], - ); - }, + ), + ], + ); + }, + ), + TableRow( + decoration: BoxDecoration( + color: masterDatabase.moves.length.isEven + ? Theme.of(context) + .colorScheme + .surfaceContainerLow + : Theme.of(context) + .colorScheme + .surfaceContainerHigh, ), - TableRow( - decoration: BoxDecoration( - color: masterDatabase.moves.length.isEven - ? Theme.of(context) - .colorScheme - .surfaceContainerLow - : Theme.of(context) - .colorScheme - .surfaceContainerHigh, + children: [ + Container( + padding: tableRowPadding, + alignment: Alignment.centerLeft, + child: const Icon(Icons.functions), ), - children: [ - Container( - padding: tableRowPadding, - alignment: Alignment.centerLeft, - child: const Icon(Icons.functions), + Container( + padding: tableRowPadding, + child: Text( + '100% / ${formatNum(masterDatabase.games)}', ), - Container( - padding: tableRowPadding, - child: Text( - '100% / ${formatNum(masterDatabase.games)}', - ), - ), - Container( - padding: tableRowPadding, - child: _WinPercentageChart( - white: masterDatabase.white, - draws: masterDatabase.draws, - black: masterDatabase.black, - ), + ), + Container( + padding: tableRowPadding, + child: _WinPercentageChart( + white: masterDatabase.white, + draws: masterDatabase.draws, + black: masterDatabase.black, ), - ], - ), - ], - ), + ), + ], + ), + ], ), ], ), From 06f2041083330f17eecff1f91cce215b2b78de58 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Mon, 29 Jul 2024 18:04:27 +0200 Subject: [PATCH 084/979] feat: show top games --- .../analysis/opening_explorer_screen.dart | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/lib/src/view/analysis/opening_explorer_screen.dart b/lib/src/view/analysis/opening_explorer_screen.dart index 45ea97cacb..89804d6a20 100644 --- a/lib/src/view/analysis/opening_explorer_screen.dart +++ b/lib/src/view/analysis/opening_explorer_screen.dart @@ -8,6 +8,8 @@ import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/analysis/opening_explorer_repository.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/game/game_repository_providers.dart'; import 'package:lichess_mobile/src/model/game/game_share_service.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; @@ -16,6 +18,7 @@ import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/utils/share.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_share_screen.dart'; +import 'package:lichess_mobile/src/view/game/archived_game_screen.dart'; import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; @@ -391,6 +394,58 @@ class _OpeningExplorer extends ConsumerWidget { ), ], ), + Container( + padding: tableRowPadding, + color: primaryColor, + child: Row( + children: [ + Text(context.l10n.topGames), + ], + ), + ), + ...masterDatabase.topGames.map( + (game) => AdaptiveInkWell( + onTap: () async { + final archivedGame = await ref.read( + archivedGameProvider(id: GameId(game.id)).future, + ); + if (context.mounted) { + pushPlatformRoute( + context, + builder: (_) => ArchivedGameScreen( + gameData: archivedGame.data, + orientation: Side.white, + ), + ); + } + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Column( + children: [ + Text(game.white.rating.toString()), + Text(game.black.rating.toString()), + ], + ), + Column( + children: [ + Text(game.white.name), + Text(game.black.name), + ], + ), + Text( + game.winner == 'white' + ? '1-0' + : game.winner == 'black' + ? '0-1' + : '1/2-1/2', + ), + if (game.month != null) Text(game.month!), + ], + ), + ), + ), ], ), ); From 6d5607963c56cbf3d578c46f0c1a058ffb483964 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Mon, 29 Jul 2024 18:27:18 +0200 Subject: [PATCH 085/979] feat: top games list ui enhancements --- .../analysis/opening_explorer_screen.dart | 118 ++++++++++++------ 1 file changed, 79 insertions(+), 39 deletions(-) diff --git a/lib/src/view/analysis/opening_explorer_screen.dart b/lib/src/view/analysis/opening_explorer_screen.dart index 89804d6a20..0623eff315 100644 --- a/lib/src/view/analysis/opening_explorer_screen.dart +++ b/lib/src/view/analysis/opening_explorer_screen.dart @@ -227,7 +227,7 @@ class _OpeningExplorer extends ConsumerWidget { : Theme.of(context).colorScheme.secondaryContainer; const rowVerticalPadding = 6.0; const rowHorizontalPadding = 6.0; - const tableRowPadding = EdgeInsets.symmetric( + const rowPadding = EdgeInsets.symmetric( vertical: rowVerticalPadding, horizontal: rowHorizontalPadding, ); @@ -292,15 +292,15 @@ class _OpeningExplorer extends ConsumerWidget { ), children: [ Container( - padding: tableRowPadding, + padding: rowPadding, child: Text(context.l10n.move), ), Container( - padding: tableRowPadding, + padding: rowPadding, child: Text(context.l10n.games), ), Container( - padding: tableRowPadding, + padding: rowPadding, child: Text(context.l10n.whiteDrawBlack), ), ], @@ -328,7 +328,7 @@ class _OpeningExplorer extends ConsumerWidget { .read(ctrlProvider.notifier) .onUserMove(Move.fromUci(move.uci)!), child: Container( - padding: tableRowPadding, + padding: rowPadding, child: Text(move.san), ), ), @@ -337,7 +337,7 @@ class _OpeningExplorer extends ConsumerWidget { .read(ctrlProvider.notifier) .onUserMove(Move.fromUci(move.uci)!), child: Container( - padding: tableRowPadding, + padding: rowPadding, child: Text( '$percentGames% / ${formatNum(move.games)}', ), @@ -348,7 +348,7 @@ class _OpeningExplorer extends ConsumerWidget { .read(ctrlProvider.notifier) .onUserMove(Move.fromUci(move.uci)!), child: Container( - padding: tableRowPadding, + padding: rowPadding, child: _WinPercentageChart( white: move.white, draws: move.draws, @@ -372,18 +372,18 @@ class _OpeningExplorer extends ConsumerWidget { ), children: [ Container( - padding: tableRowPadding, + padding: rowPadding, alignment: Alignment.centerLeft, child: const Icon(Icons.functions), ), Container( - padding: tableRowPadding, + padding: rowPadding, child: Text( '100% / ${formatNum(masterDatabase.games)}', ), ), Container( - padding: tableRowPadding, + padding: rowPadding, child: _WinPercentageChart( white: masterDatabase.white, draws: masterDatabase.draws, @@ -395,7 +395,7 @@ class _OpeningExplorer extends ConsumerWidget { ], ), Container( - padding: tableRowPadding, + padding: rowPadding, color: primaryColor, child: Row( children: [ @@ -403,8 +403,11 @@ class _OpeningExplorer extends ConsumerWidget { ], ), ), - ...masterDatabase.topGames.map( - (game) => AdaptiveInkWell( + ...List.generate(masterDatabase.topGames.length, + (int index) { + final game = masterDatabase.topGames.get(index); + const paddingResultContainer = EdgeInsets.all(5); + return AdaptiveInkWell( onTap: () async { final archivedGame = await ref.read( archivedGameProvider(id: GameId(game.id)).future, @@ -419,33 +422,70 @@ class _OpeningExplorer extends ConsumerWidget { ); } }, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Column( - children: [ - Text(game.white.rating.toString()), - Text(game.black.rating.toString()), - ], - ), - Column( - children: [ - Text(game.white.name), - Text(game.black.name), - ], - ), - Text( - game.winner == 'white' - ? '1-0' - : game.winner == 'black' - ? '0-1' - : '1/2-1/2', - ), - if (game.month != null) Text(game.month!), - ], + child: Container( + padding: rowPadding, + color: index.isEven + ? Theme.of(context) + .colorScheme + .surfaceContainerLow + : Theme.of(context) + .colorScheme + .surfaceContainerHigh, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Column( + children: [ + Text(game.white.rating.toString()), + Text(game.black.rating.toString()), + ], + ), + Column( + children: [ + Text(game.white.name), + Text(game.black.name), + ], + ), + if (game.winner == 'white') + Container( + padding: paddingResultContainer, + color: Colors.white, + child: const Text( + '1-0', + style: TextStyle( + color: Colors.black, + ), + ), + ) + else if (game.winner == 'black') + Container( + padding: paddingResultContainer, + color: Colors.black, + child: const Text( + '0-1', + style: TextStyle( + color: Colors.white, + ), + ), + ) + else + Container( + padding: paddingResultContainer, + color: Colors.grey, + child: const Text( + '½-½', + style: TextStyle( + color: Colors.white, + ), + ), + ), + + if (game.month != null) Text(game.month!), + ], + ), ), - ), - ), + ); + }), ], ), ); From 13d9f9e2a58e225d697c89b92fbe7a5ff112de26 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Mon, 29 Jul 2024 19:35:50 +0200 Subject: [PATCH 086/979] fix: remove link to master game since it's not supported yet --- .../analysis/opening_explorer_screen.dart | 125 ++++++++---------- 1 file changed, 53 insertions(+), 72 deletions(-) diff --git a/lib/src/view/analysis/opening_explorer_screen.dart b/lib/src/view/analysis/opening_explorer_screen.dart index 0623eff315..b108956a12 100644 --- a/lib/src/view/analysis/opening_explorer_screen.dart +++ b/lib/src/view/analysis/opening_explorer_screen.dart @@ -407,82 +407,63 @@ class _OpeningExplorer extends ConsumerWidget { (int index) { final game = masterDatabase.topGames.get(index); const paddingResultContainer = EdgeInsets.all(5); - return AdaptiveInkWell( - onTap: () async { - final archivedGame = await ref.read( - archivedGameProvider(id: GameId(game.id)).future, - ); - if (context.mounted) { - pushPlatformRoute( - context, - builder: (_) => ArchivedGameScreen( - gameData: archivedGame.data, - orientation: Side.white, - ), - ); - } - }, - child: Container( - padding: rowPadding, - color: index.isEven - ? Theme.of(context) - .colorScheme - .surfaceContainerLow - : Theme.of(context) - .colorScheme - .surfaceContainerHigh, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Column( - children: [ - Text(game.white.rating.toString()), - Text(game.black.rating.toString()), - ], - ), - Column( - children: [ - Text(game.white.name), - Text(game.black.name), - ], - ), - if (game.winner == 'white') - Container( - padding: paddingResultContainer, - color: Colors.white, - child: const Text( - '1-0', - style: TextStyle( - color: Colors.black, - ), + return Container( + padding: rowPadding, + color: index.isEven + ? Theme.of(context).colorScheme.surfaceContainerLow + : Theme.of(context) + .colorScheme + .surfaceContainerHigh, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Column( + children: [ + Text(game.white.rating.toString()), + Text(game.black.rating.toString()), + ], + ), + Column( + children: [ + Text(game.white.name), + Text(game.black.name), + ], + ), + if (game.winner == 'white') + Container( + padding: paddingResultContainer, + color: Colors.white, + child: const Text( + '1-0', + style: TextStyle( + color: Colors.black, ), - ) - else if (game.winner == 'black') - Container( - padding: paddingResultContainer, - color: Colors.black, - child: const Text( - '0-1', - style: TextStyle( - color: Colors.white, - ), + ), + ) + else if (game.winner == 'black') + Container( + padding: paddingResultContainer, + color: Colors.black, + child: const Text( + '0-1', + style: TextStyle( + color: Colors.white, ), - ) - else - Container( - padding: paddingResultContainer, - color: Colors.grey, - child: const Text( - '½-½', - style: TextStyle( - color: Colors.white, - ), + ), + ) + else + Container( + padding: paddingResultContainer, + color: Colors.grey, + child: const Text( + '½-½', + style: TextStyle( + color: Colors.white, ), ), - - if (game.month != null) Text(game.month!), - ], - ), + ), + if (game.month != null) Text(game.month!), + ], ), ); }), From 1c07d26d980476d98515a14cabaaa2b8f4f986d1 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Tue, 30 Jul 2024 08:03:40 +0200 Subject: [PATCH 087/979] feat: improve layout for top game row --- .../analysis/opening_explorer_screen.dart | 105 +++++++++++------- 1 file changed, 63 insertions(+), 42 deletions(-) diff --git a/lib/src/view/analysis/opening_explorer_screen.dart b/lib/src/view/analysis/opening_explorer_screen.dart index b108956a12..8104a6167d 100644 --- a/lib/src/view/analysis/opening_explorer_screen.dart +++ b/lib/src/view/analysis/opening_explorer_screen.dart @@ -406,7 +406,8 @@ class _OpeningExplorer extends ConsumerWidget { ...List.generate(masterDatabase.topGames.length, (int index) { final game = masterDatabase.topGames.get(index); - const paddingResultContainer = EdgeInsets.all(5); + const widthResultBox = 50.0; + const paddingResultBox = EdgeInsets.all(5); return Container( padding: rowPadding, color: index.isEven @@ -415,54 +416,74 @@ class _OpeningExplorer extends ConsumerWidget { .colorScheme .surfaceContainerHigh, child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Column( + Row( children: [ - Text(game.white.rating.toString()), - Text(game.black.rating.toString()), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(game.white.rating.toString()), + Text(game.black.rating.toString()), + ], + ), + const SizedBox(width: 10), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(game.white.name), + Text(game.black.name), + ], + ), ], ), - Column( + Row( children: [ - Text(game.white.name), - Text(game.black.name), - ], - ), - if (game.winner == 'white') - Container( - padding: paddingResultContainer, - color: Colors.white, - child: const Text( - '1-0', - style: TextStyle( - color: Colors.black, - ), - ), - ) - else if (game.winner == 'black') - Container( - padding: paddingResultContainer, - color: Colors.black, - child: const Text( - '0-1', - style: TextStyle( - color: Colors.white, - ), - ), - ) - else - Container( - padding: paddingResultContainer, - color: Colors.grey, - child: const Text( - '½-½', - style: TextStyle( + if (game.winner == 'white') + Container( + width: widthResultBox, + padding: paddingResultBox, color: Colors.white, + child: const Text( + '1-0', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.black, + ), + ), + ) + else if (game.winner == 'black') + Container( + width: widthResultBox, + padding: paddingResultBox, + color: Colors.black, + child: const Text( + '0-1', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + ), + ), + ) + else + Container( + width: widthResultBox, + padding: paddingResultBox, + color: Colors.grey, + child: const Text( + '½-½', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + ), + ), ), - ), - ), - if (game.month != null) Text(game.month!), + if (game.month != null) ...[ + const SizedBox(width: 5.0), + Text(game.month!), + ], + ], + ), ], ), ); From ad50a45deabaedbe898757f564e701a2ee465f01 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Tue, 30 Jul 2024 08:21:04 +0200 Subject: [PATCH 088/979] feat: only show number of games, show percent in tooltip --- .../analysis/opening_explorer_screen.dart | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/lib/src/view/analysis/opening_explorer_screen.dart b/lib/src/view/analysis/opening_explorer_screen.dart index 8104a6167d..0a1ea1b0ad 100644 --- a/lib/src/view/analysis/opening_explorer_screen.dart +++ b/lib/src/view/analysis/opening_explorer_screen.dart @@ -203,6 +203,8 @@ class _OpeningExplorer extends ConsumerWidget { final String pgn; final AnalysisOptions options; + String formatNum(int num) => NumberFormat.decimalPatternDigits().format(num); + @override Widget build(BuildContext context, WidgetRef ref) { final ctrlProvider = analysisControllerProvider(pgn, options); @@ -338,8 +340,9 @@ class _OpeningExplorer extends ConsumerWidget { .onUserMove(Move.fromUci(move.uci)!), child: Container( padding: rowPadding, - child: Text( - '$percentGames% / ${formatNum(move.games)}', + child: Tooltip( + message: '$percentGames%', + child: Text(formatNum(move.games)), ), ), ), @@ -378,8 +381,9 @@ class _OpeningExplorer extends ConsumerWidget { ), Container( padding: rowPadding, - child: Text( - '100% / ${formatNum(masterDatabase.games)}', + child: Tooltip( + message: '100%', + child: Text(formatNum(masterDatabase.games)), ), ), Container( @@ -500,21 +504,21 @@ class _OpeningExplorer extends ConsumerWidget { ), ); } - - String formatNum(int num) => NumberFormat.decimalPatternDigits().format(num); } class _WinPercentageChart extends StatelessWidget { - final int white; - final int draws; - final int black; - const _WinPercentageChart({ required this.white, required this.draws, required this.black, }); + final int white; + final int draws; + final int black; + + String label(int percent) => percent < 20 ? '' : '$percent%'; + @override Widget build(BuildContext context) { int percentGames(int games) => @@ -523,7 +527,6 @@ class _WinPercentageChart extends StatelessWidget { final percentWhite = percentGames(white); final percentDraws = percentGames(draws); final percentBlack = percentGames(black); - String label(int percent) => percent < 20 ? '' : '$percent%'; return ClipRRect( borderRadius: BorderRadius.circular(5), From 927238bf1ade3c73498825bd506373a57884750a Mon Sep 17 00:00:00 2001 From: Mauritz Date: Tue, 30 Jul 2024 08:41:05 +0200 Subject: [PATCH 089/979] feat: show opening name even when no game found --- .../analysis/opening_explorer_screen.dart | 454 +++++++++--------- 1 file changed, 232 insertions(+), 222 deletions(-) diff --git a/lib/src/view/analysis/opening_explorer_screen.dart b/lib/src/view/analysis/opening_explorer_screen.dart index 0a1ea1b0ad..c80a77926c 100644 --- a/lib/src/view/analysis/opening_explorer_screen.dart +++ b/lib/src/view/analysis/opening_explorer_screen.dart @@ -212,11 +212,9 @@ class _OpeningExplorer extends ConsumerWidget { if (position.fullmoves > 24) { return const Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text('Max depth reached'), - ], + child: Align( + alignment: Alignment.center, + child: Text('Max depth reached'), ), ); } @@ -255,246 +253,258 @@ class _OpeningExplorer extends ConsumerWidget { return masterDatabaseAsync.when( data: (masterDatabase) { - return masterDatabase.moves.isEmpty - ? const Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text('No game found'), - ], - ) - : SingleChildScrollView( - scrollDirection: Axis.vertical, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + return Column( + children: [ + if (opening != null) + Container( + padding: + const EdgeInsets.only(left: rowHorizontalPadding), + color: primaryColor, + child: Row( children: [ - if (opening != null) - Container( - padding: - const EdgeInsets.only(left: rowHorizontalPadding), - color: primaryColor, - child: Row( - children: [ - if (opening.eco.isEmpty) - Text(opening.name) - else - Text('${opening.eco} ${opening.name}'), - ], - ), - ), - Table( - columnWidths: const { - 0: FractionColumnWidth(0.2), - 1: FractionColumnWidth(0.3), - 2: FractionColumnWidth(0.5), - }, - children: [ - TableRow( - decoration: BoxDecoration( - color: primaryColor, - ), - children: [ - Container( - padding: rowPadding, - child: Text(context.l10n.move), - ), - Container( - padding: rowPadding, - child: Text(context.l10n.games), - ), - Container( - padding: rowPadding, - child: Text(context.l10n.whiteDrawBlack), + if (opening.eco.isEmpty) + Text(opening.name) + else + Text('${opening.eco} ${opening.name}'), + ], + ), + ), + + if (masterDatabase.moves.isEmpty) + const Expanded( + child: Align( + alignment: Alignment.center, + child: Text('No game found'), + ), + ) + else + Expanded( + child: SingleChildScrollView( + scrollDirection: Axis.vertical, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Table( + columnWidths: const { + 0: FractionColumnWidth(0.2), + 1: FractionColumnWidth(0.3), + 2: FractionColumnWidth(0.5), + }, + children: [ + TableRow( + decoration: BoxDecoration( + color: primaryColor, ), - ], - ), - ...List.generate( - masterDatabase.moves.length, - (int index) { - final move = masterDatabase.moves.get(index); - final percentGames = - ((move.games / masterDatabase.games) * 100) - .round(); - return TableRow( - decoration: BoxDecoration( - color: index.isEven - ? Theme.of(context) - .colorScheme - .surfaceContainerLow - : Theme.of(context) - .colorScheme - .surfaceContainerHigh, + children: [ + Container( + padding: rowPadding, + child: Text(context.l10n.move), ), - children: [ - TableRowInkWell( - onTap: () => ref - .read(ctrlProvider.notifier) - .onUserMove(Move.fromUci(move.uci)!), - child: Container( - padding: rowPadding, - child: Text(move.san), - ), + Container( + padding: rowPadding, + child: Text(context.l10n.games), + ), + Container( + padding: rowPadding, + child: Text(context.l10n.whiteDrawBlack), + ), + ], + ), + ...List.generate( + masterDatabase.moves.length, + (int index) { + final move = masterDatabase.moves.get(index); + final percentGames = + ((move.games / masterDatabase.games) * 100) + .round(); + return TableRow( + decoration: BoxDecoration( + color: index.isEven + ? Theme.of(context) + .colorScheme + .surfaceContainerLow + : Theme.of(context) + .colorScheme + .surfaceContainerHigh, ), - TableRowInkWell( - onTap: () => ref - .read(ctrlProvider.notifier) - .onUserMove(Move.fromUci(move.uci)!), - child: Container( - padding: rowPadding, - child: Tooltip( - message: '$percentGames%', - child: Text(formatNum(move.games)), + children: [ + TableRowInkWell( + onTap: () => ref + .read(ctrlProvider.notifier) + .onUserMove(Move.fromUci(move.uci)!), + child: Container( + padding: rowPadding, + child: Text(move.san), ), ), - ), - TableRowInkWell( - onTap: () => ref - .read(ctrlProvider.notifier) - .onUserMove(Move.fromUci(move.uci)!), - child: Container( - padding: rowPadding, - child: _WinPercentageChart( - white: move.white, - draws: move.draws, - black: move.black, + TableRowInkWell( + onTap: () => ref + .read(ctrlProvider.notifier) + .onUserMove(Move.fromUci(move.uci)!), + child: Container( + padding: rowPadding, + child: Tooltip( + message: '$percentGames%', + child: Text(formatNum(move.games)), + ), ), ), - ), - ], - ); - }, - ), - TableRow( - decoration: BoxDecoration( - color: masterDatabase.moves.length.isEven - ? Theme.of(context) - .colorScheme - .surfaceContainerLow - : Theme.of(context) - .colorScheme - .surfaceContainerHigh, + TableRowInkWell( + onTap: () => ref + .read(ctrlProvider.notifier) + .onUserMove(Move.fromUci(move.uci)!), + child: Container( + padding: rowPadding, + child: _WinPercentageChart( + white: move.white, + draws: move.draws, + black: move.black, + ), + ), + ), + ], + ); + }, ), - children: [ - Container( - padding: rowPadding, - alignment: Alignment.centerLeft, - child: const Icon(Icons.functions), + TableRow( + decoration: BoxDecoration( + color: masterDatabase.moves.length.isEven + ? Theme.of(context) + .colorScheme + .surfaceContainerLow + : Theme.of(context) + .colorScheme + .surfaceContainerHigh, ), - Container( - padding: rowPadding, - child: Tooltip( - message: '100%', - child: Text(formatNum(masterDatabase.games)), + children: [ + Container( + padding: rowPadding, + alignment: Alignment.centerLeft, + child: const Icon(Icons.functions), ), - ), - Container( - padding: rowPadding, - child: _WinPercentageChart( - white: masterDatabase.white, - draws: masterDatabase.draws, - black: masterDatabase.black, + Container( + padding: rowPadding, + child: Tooltip( + message: '100%', + child: Text(formatNum(masterDatabase.games)), + ), ), - ), - ], - ), - ], - ), - Container( - padding: rowPadding, - color: primaryColor, - child: Row( - children: [ - Text(context.l10n.topGames), + Container( + padding: rowPadding, + child: _WinPercentageChart( + white: masterDatabase.white, + draws: masterDatabase.draws, + black: masterDatabase.black, + ), + ), + ], + ), ], ), - ), - ...List.generate(masterDatabase.topGames.length, - (int index) { - final game = masterDatabase.topGames.get(index); - const widthResultBox = 50.0; - const paddingResultBox = EdgeInsets.all(5); - return Container( + Container( padding: rowPadding, - color: index.isEven - ? Theme.of(context).colorScheme.surfaceContainerLow - : Theme.of(context) - .colorScheme - .surfaceContainerHigh, + color: primaryColor, child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(game.white.rating.toString()), - Text(game.black.rating.toString()), - ], - ), - const SizedBox(width: 10), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(game.white.name), - Text(game.black.name), - ], - ), - ], - ), - Row( - children: [ - if (game.winner == 'white') - Container( - width: widthResultBox, - padding: paddingResultBox, - color: Colors.white, - child: const Text( - '1-0', - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.black, + Text(context.l10n.topGames), + ], + ), + ), + ...List.generate(masterDatabase.topGames.length, + (int index) { + final game = masterDatabase.topGames.get(index); + const widthResultBox = 50.0; + const paddingResultBox = EdgeInsets.all(5); + return Container( + padding: rowPadding, + color: index.isEven + ? Theme.of(context) + .colorScheme + .surfaceContainerLow + : Theme.of(context) + .colorScheme + .surfaceContainerHigh, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text(game.white.rating.toString()), + Text(game.black.rating.toString()), + ], + ), + const SizedBox(width: 10), + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text(game.white.name), + Text(game.black.name), + ], + ), + ], + ), + Row( + children: [ + if (game.winner == 'white') + Container( + width: widthResultBox, + padding: paddingResultBox, + color: Colors.white, + child: const Text( + '1-0', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.black, + ), ), - ), - ) - else if (game.winner == 'black') - Container( - width: widthResultBox, - padding: paddingResultBox, - color: Colors.black, - child: const Text( - '0-1', - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white, + ) + else if (game.winner == 'black') + Container( + width: widthResultBox, + padding: paddingResultBox, + color: Colors.black, + child: const Text( + '0-1', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + ), ), - ), - ) - else - Container( - width: widthResultBox, - padding: paddingResultBox, - color: Colors.grey, - child: const Text( - '½-½', - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white, + ) + else + Container( + width: widthResultBox, + padding: paddingResultBox, + color: Colors.grey, + child: const Text( + '½-½', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + ), ), ), - ), - if (game.month != null) ...[ - const SizedBox(width: 5.0), - Text(game.month!), + if (game.month != null) ...[ + const SizedBox(width: 5.0), + Text(game.month!), + ], ], - ], - ), - ], - ), - ); - }), - ], + ), + ], + ), + ); + }), + ], + ), ), - ); + ), + ], + ); }, loading: () => const Center( child: CircularProgressIndicator(), From 54d3fc7fe9fd45fc81107e6f68901d23ca0cef49 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Tue, 30 Jul 2024 09:55:23 +0200 Subject: [PATCH 090/979] feat: select opening database in settings --- .../opening_explorer_preferences.dart | 65 +++++++++++++++++++ .../analysis/opening_explorer_screen.dart | 7 +- .../analysis/opening_explorer_settings.dart | 38 ++++++++++- 3 files changed, 103 insertions(+), 7 deletions(-) create mode 100644 lib/src/model/analysis/opening_explorer_preferences.dart diff --git a/lib/src/model/analysis/opening_explorer_preferences.dart b/lib/src/model/analysis/opening_explorer_preferences.dart new file mode 100644 index 0000000000..0851b860db --- /dev/null +++ b/lib/src/model/analysis/opening_explorer_preferences.dart @@ -0,0 +1,65 @@ +import 'dart:convert'; + +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:lichess_mobile/src/db/shared_preferences.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'opening_explorer_preferences.freezed.dart'; +part 'opening_explorer_preferences.g.dart'; + +@Riverpod(keepAlive: true) +class OpeningExplorerPreferences extends _$OpeningExplorerPreferences { + static const prefKey = 'preferences.opening_explorer'; + + @override + OpeningExplorerPrefState build() { + final prefs = ref.watch(sharedPreferencesProvider); + + final stored = prefs.getString(prefKey); + return stored != null + ? OpeningExplorerPrefState.fromJson( + jsonDecode(stored) as Map, + ) + : OpeningExplorerPrefState.defaults; + } + + Future setDatabase(OpeningDatabase db) => _save( + state.copyWith(db: db), + ); + + Future _save(OpeningExplorerPrefState newState) async { + final prefs = ref.read(sharedPreferencesProvider); + await prefs.setString( + prefKey, + jsonEncode(newState.toJson()), + ); + state = newState; + } +} + +enum OpeningDatabase { + master, + lichess, + player, +} + +@Freezed(fromJson: true, toJson: true) +class OpeningExplorerPrefState with _$OpeningExplorerPrefState { + const OpeningExplorerPrefState._(); + + const factory OpeningExplorerPrefState({ + required OpeningDatabase db, + }) = _OpeningExplorerPrefState; + + static const defaults = OpeningExplorerPrefState( + db: OpeningDatabase.master, + ); + + factory OpeningExplorerPrefState.fromJson(Map json) { + try { + return _$OpeningExplorerPrefStateFromJson(json); + } catch (_) { + return defaults; + } + } +} diff --git a/lib/src/view/analysis/opening_explorer_screen.dart b/lib/src/view/analysis/opening_explorer_screen.dart index c80a77926c..db4116b2a2 100644 --- a/lib/src/view/analysis/opening_explorer_screen.dart +++ b/lib/src/view/analysis/opening_explorer_screen.dart @@ -8,8 +8,6 @@ import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/analysis/opening_explorer_repository.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; -import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/model/game/game_repository_providers.dart'; import 'package:lichess_mobile/src/model/game/game_share_service.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; @@ -18,7 +16,6 @@ import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/utils/share.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_share_screen.dart'; -import 'package:lichess_mobile/src/view/game/archived_game_screen.dart'; import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; @@ -257,8 +254,7 @@ class _OpeningExplorer extends ConsumerWidget { children: [ if (opening != null) Container( - padding: - const EdgeInsets.only(left: rowHorizontalPadding), + padding: const EdgeInsets.only(left: rowHorizontalPadding), color: primaryColor, child: Row( children: [ @@ -269,7 +265,6 @@ class _OpeningExplorer extends ConsumerWidget { ], ), ), - if (masterDatabase.moves.isEmpty) const Expanded( child: Align( diff --git a/lib/src/view/analysis/opening_explorer_settings.dart b/lib/src/view/analysis/opening_explorer_settings.dart index 829986cfb1..f7cff7df79 100644 --- a/lib/src/view/analysis/opening_explorer_settings.dart +++ b/lib/src/view/analysis/opening_explorer_settings.dart @@ -1,6 +1,7 @@ -import 'package:flutter/widgets.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; +import 'package:lichess_mobile/src/model/analysis/opening_explorer_preferences.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; @@ -13,6 +14,12 @@ class OpeningExplorerSettings extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final openingDatabase = ref.watch( + openingExplorerPreferencesProvider.select( + (state) => state.db, + ), + ); + return DraggableScrollableSheet( initialChildSize: .7, expand: false, @@ -27,6 +34,35 @@ class OpeningExplorerSettings extends ConsumerWidget { subtitle: const SizedBox.shrink(), ), const SizedBox(height: 8.0), + PlatformListTile( + title: Text(context.l10n.database), + subtitle: Wrap( + spacing: 5, + children: [ + ChoiceChip( + label: const Text('Masters'), + selected: openingDatabase == OpeningDatabase.master, + onSelected: (value) => ref + .read(openingExplorerPreferencesProvider.notifier) + .setDatabase(OpeningDatabase.master), + ), + ChoiceChip( + label: const Text('Lichess'), + selected: openingDatabase == OpeningDatabase.lichess, + onSelected: (value) => ref + .read(openingExplorerPreferencesProvider.notifier) + .setDatabase(OpeningDatabase.lichess), + ), + ChoiceChip( + label: Text(context.l10n.player), + selected: openingDatabase == OpeningDatabase.player, + onSelected: (value) => ref + .read(openingExplorerPreferencesProvider.notifier) + .setDatabase(OpeningDatabase.player), + ), + ], + ), + ), ], ), ); From 15691e7d15c6532c718de9bae85c2a97a5182ffe Mon Sep 17 00:00:00 2001 From: Mauritz Date: Tue, 30 Jul 2024 11:19:32 +0200 Subject: [PATCH 091/979] feat: add settings for master database --- .../opening_explorer_preferences.dart | 33 ++++++++- .../analysis/opening_explorer_repository.dart | 20 +++++- .../analysis/opening_explorer_screen.dart | 11 ++- .../analysis/opening_explorer_settings.dart | 70 ++++++++++++++++--- 4 files changed, 120 insertions(+), 14 deletions(-) diff --git a/lib/src/model/analysis/opening_explorer_preferences.dart b/lib/src/model/analysis/opening_explorer_preferences.dart index 0851b860db..82043ab02a 100644 --- a/lib/src/model/analysis/opening_explorer_preferences.dart +++ b/lib/src/model/analysis/opening_explorer_preferences.dart @@ -27,6 +27,12 @@ class OpeningExplorerPreferences extends _$OpeningExplorerPreferences { state.copyWith(db: db), ); + Future setMasterDbSince(int year) => + _save(state.copyWith(masterDb: state.masterDb.copyWith(sinceYear: year))); + + Future setMasterDbUntil(int year) => + _save(state.copyWith(masterDb: state.masterDb.copyWith(untilYear: year))); + Future _save(OpeningExplorerPrefState newState) async { final prefs = ref.read(sharedPreferencesProvider); await prefs.setString( @@ -49,10 +55,12 @@ class OpeningExplorerPrefState with _$OpeningExplorerPrefState { const factory OpeningExplorerPrefState({ required OpeningDatabase db, + required MasterDbPrefState masterDb, }) = _OpeningExplorerPrefState; - static const defaults = OpeningExplorerPrefState( + static final defaults = OpeningExplorerPrefState( db: OpeningDatabase.master, + masterDb: MasterDbPrefState.defaults, ); factory OpeningExplorerPrefState.fromJson(Map json) { @@ -63,3 +71,26 @@ class OpeningExplorerPrefState with _$OpeningExplorerPrefState { } } } + +@Freezed(fromJson: true, toJson: true) +class MasterDbPrefState with _$MasterDbPrefState { + const MasterDbPrefState._(); + + const factory MasterDbPrefState({ + required int sinceYear, + required int untilYear, + }) = _MasterDbPrefState; + + static final defaults = MasterDbPrefState( + sinceYear: 1952, + untilYear: DateTime.now().year, + ); + + factory MasterDbPrefState.fromJson(Map json) { + try { + return _$MasterDbPrefStateFromJson(json); + } catch (_) { + return defaults; + } + } +} diff --git a/lib/src/model/analysis/opening_explorer_repository.dart b/lib/src/model/analysis/opening_explorer_repository.dart index c2ec1ed965..afed7834d6 100644 --- a/lib/src/model/analysis/opening_explorer_repository.dart +++ b/lib/src/model/analysis/opening_explorer_repository.dart @@ -8,9 +8,12 @@ part 'opening_explorer_repository.g.dart'; Future masterDatabase( MasterDatabaseRef ref, { required String fen, + int? sinceYear, + int? untilYear, }) async { return ref.withClient( - (client) => OpeningExplorerRepository(client).getMasterDatabase(fen), + (client) => OpeningExplorerRepository(client) + .getMasterDatabase(fen, since: sinceYear, until: untilYear), ); } @@ -19,9 +22,20 @@ class OpeningExplorerRepository { final LichessClient client; - Future getMasterDatabase(String fen) { + Future getMasterDatabase( + String fen, { + int? since, + int? until, + }) { return client.readJson( - Uri(path: '/masters', queryParameters: {'fen': fen}), + Uri( + path: '/masters', + queryParameters: { + 'fen': fen, + if (since != null) 'since': since.toString(), + if (until != null) 'until': until.toString(), + }, + ), mapper: OpeningExplorer.fromJson, ); } diff --git a/lib/src/view/analysis/opening_explorer_screen.dart b/lib/src/view/analysis/opening_explorer_screen.dart index db4116b2a2..54e8c32136 100644 --- a/lib/src/view/analysis/opening_explorer_screen.dart +++ b/lib/src/view/analysis/opening_explorer_screen.dart @@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; +import 'package:lichess_mobile/src/model/analysis/opening_explorer_preferences.dart'; import 'package:lichess_mobile/src/model/analysis/opening_explorer_repository.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; @@ -245,8 +246,14 @@ class _OpeningExplorer extends ConsumerWidget { ) : nodeOpening ?? branchOpening ?? contextOpening; - final masterDatabaseAsync = - ref.watch(masterDatabaseProvider(fen: position.fen)); + final prefs = ref.watch(openingExplorerPreferencesProvider); + final masterDatabaseAsync = ref.watch( + masterDatabaseProvider( + fen: position.fen, + sinceYear: prefs.masterDb.sinceYear, + untilYear: prefs.masterDb.untilYear, + ), + ); return masterDatabaseAsync.when( data: (masterDatabase) { diff --git a/lib/src/view/analysis/opening_explorer_settings.dart b/lib/src/view/analysis/opening_explorer_settings.dart index f7cff7df79..47bfb19e1e 100644 --- a/lib/src/view/analysis/opening_explorer_settings.dart +++ b/lib/src/view/analysis/opening_explorer_settings.dart @@ -5,6 +5,7 @@ import 'package:lichess_mobile/src/model/analysis/opening_explorer_preferences.d import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; +import 'package:lichess_mobile/src/widgets/non_linear_slider.dart'; class OpeningExplorerSettings extends ConsumerWidget { const OpeningExplorerSettings(this.pgn, this.options); @@ -14,11 +15,10 @@ class OpeningExplorerSettings extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final openingDatabase = ref.watch( - openingExplorerPreferencesProvider.select( - (state) => state.db, - ), - ); + final prefs = ref.watch(openingExplorerPreferencesProvider); + + const earliestYear = 1952; + final years = DateTime.now().year - earliestYear + 1; return DraggableScrollableSheet( initialChildSize: .7, @@ -41,21 +41,21 @@ class OpeningExplorerSettings extends ConsumerWidget { children: [ ChoiceChip( label: const Text('Masters'), - selected: openingDatabase == OpeningDatabase.master, + selected: prefs.db == OpeningDatabase.master, onSelected: (value) => ref .read(openingExplorerPreferencesProvider.notifier) .setDatabase(OpeningDatabase.master), ), ChoiceChip( label: const Text('Lichess'), - selected: openingDatabase == OpeningDatabase.lichess, + selected: prefs.db == OpeningDatabase.lichess, onSelected: (value) => ref .read(openingExplorerPreferencesProvider.notifier) .setDatabase(OpeningDatabase.lichess), ), ChoiceChip( label: Text(context.l10n.player), - selected: openingDatabase == OpeningDatabase.player, + selected: prefs.db == OpeningDatabase.player, onSelected: (value) => ref .read(openingExplorerPreferencesProvider.notifier) .setDatabase(OpeningDatabase.player), @@ -63,6 +63,60 @@ class OpeningExplorerSettings extends ConsumerWidget { ], ), ), + if (prefs.db == OpeningDatabase.master) ...[ + PlatformListTile( + title: Text.rich( + TextSpan( + text: '${context.l10n.since}: ', + style: const TextStyle( + fontWeight: FontWeight.normal, + ), + children: [ + TextSpan( + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + text: prefs.masterDb.sinceYear.toString(), + ), + ], + ), + ), + subtitle: NonLinearSlider( + value: prefs.masterDb.sinceYear, + values: List.generate(years, (index) => earliestYear + index), + onChangeEnd: (value) => ref + .read(openingExplorerPreferencesProvider.notifier) + .setMasterDbSince(value.toInt()), + ), + ), + PlatformListTile( + title: Text.rich( + TextSpan( + text: '${context.l10n.until}: ', + style: const TextStyle( + fontWeight: FontWeight.normal, + ), + children: [ + TextSpan( + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + text: prefs.masterDb.untilYear.toString(), + ), + ], + ), + ), + subtitle: NonLinearSlider( + value: prefs.masterDb.untilYear, + values: List.generate(years, (index) => earliestYear + index), + onChangeEnd: (value) => ref + .read(openingExplorerPreferencesProvider.notifier) + .setMasterDbUntil(value.toInt()), + ), + ), + ], ], ), ); From 16308fbd5c02dea79172d2589e74d44215c092a0 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Tue, 30 Jul 2024 20:07:29 +0200 Subject: [PATCH 092/979] feat: add lichess games opening explorer --- lib/src/model/analysis/opening_explorer.dart | 174 +++-- .../opening_explorer_preferences.dart | 71 +- .../analysis/opening_explorer_repository.dart | 67 +- .../analysis/opening_explorer_screen.dart | 665 +++++++++++------- .../analysis/opening_explorer_settings.dart | 48 +- 5 files changed, 729 insertions(+), 296 deletions(-) diff --git a/lib/src/model/analysis/opening_explorer.dart b/lib/src/model/analysis/opening_explorer.dart index 48f63e8982..a7db52f317 100644 --- a/lib/src/model/analysis/opening_explorer.dart +++ b/lib/src/model/analysis/opening_explorer.dart @@ -1,27 +1,48 @@ +import 'package:deep_pick/deep_pick.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/common/perf.dart'; part 'opening_explorer.freezed.dart'; part 'opening_explorer.g.dart'; @Freezed(fromJson: true) -class OpeningExplorer with _$OpeningExplorer { - const OpeningExplorer._(); +class MasterOpeningExplorer with _$MasterOpeningExplorer { + const MasterOpeningExplorer._(); - const factory OpeningExplorer({ + const factory MasterOpeningExplorer({ LightOpening? opening, required int white, required int draws, required int black, required IList moves, - required IList topGames, - IList? recentGames, - IList? history, - }) = _OpeningExplorer; + required IList topGames, + }) = _MasterOpeningExplorer; - factory OpeningExplorer.fromJson(Map json) => - _$OpeningExplorerFromJson(json); + factory MasterOpeningExplorer.fromJson(Map json) => + _$MasterOpeningExplorerFromJson(json); + + int get games { + return white + draws + black; + } +} + +@Freezed(fromJson: true) +class LichessOpeningExplorer with _$LichessOpeningExplorer { + const LichessOpeningExplorer._(); + + const factory LichessOpeningExplorer({ + LightOpening? opening, + required int white, + required int draws, + required int black, + required IList moves, + required IList recentGames, + }) = _LichessOpeningExplorer; + + factory LichessOpeningExplorer.fromJson(Map json) => + _$LichessOpeningExplorerFromJson(json); int get games { return white + draws + black; @@ -39,7 +60,6 @@ class OpeningMove with _$OpeningMove { required int white, required int draws, required int black, - Game? game, }) = _OpeningMove; factory OpeningMove.fromJson(Map json) => @@ -50,56 +70,130 @@ class OpeningMove with _$OpeningMove { } } +class Game { + const Game({ + required this.uci, + required this.id, + this.speed, + this.mode, + required this.white, + required this.black, + this.winner, + required this.year, + this.month, + }); + + final String uci; + final String id; + final Perf? speed; + final Mode? mode; + final Player white; + final Player black; + final String? winner; + final int year; + final String? month; + + factory Game.fromTopGame(TopGame topGame) => Game( + uci: topGame.uci, + id: topGame.id, + white: topGame.white, + black: topGame.black, + winner: topGame.winner, + year: topGame.year, + month: topGame.month, + ); + + factory Game.fromRecentGame(RecentGame recentGame) => Game( + uci: recentGame.uci, + id: recentGame.id, + speed: recentGame.speed, + mode: recentGame.mode, + white: recentGame.white, + black: recentGame.black, + winner: recentGame.winner, + year: recentGame.year, + month: recentGame.month, + ); +} + @Freezed(fromJson: true) -class Game with _$Game { - factory Game({ +class TopGame with _$TopGame { + factory TopGame({ + required String uci, required String id, String? winner, - required MasterPlayer white, - required MasterPlayer black, + required Player white, + required Player black, required int year, String? month, - }) = _Game; + }) = _TopGame; - factory Game.fromJson(Map json) => _$GameFromJson(json); + factory TopGame.fromJson(Map json) => + _$TopGameFromJson(json); } @Freezed(fromJson: true) -class GameWithMove with _$GameWithMove { - factory GameWithMove({ +class RecentGame with _$RecentGame { + factory RecentGame({ required String uci, required String id, String? winner, - required MasterPlayer white, - required MasterPlayer black, + required Perf speed, + required Mode mode, + required Player white, + required Player black, required int year, String? month, - }) = _GameWithMove; + }) = _RecentGame; + + factory RecentGame.fromJson(Map json) => + RecentGame.fromPick(pick(json).required()); + + factory RecentGame.fromPick(RequiredPick pick) { + return RecentGame( + uci: pick('uci').asStringOrThrow(), + id: pick('id').asStringOrThrow(), + winner: pick('winner').asStringOrNull(), + speed: pick('speed').asPerfOrThrow(), + mode: pick('mode').asModeOrThrow(), + white: pick('white').letOrThrow(Player.fromPick), + black: pick('black').letOrThrow(Player.fromPick), + year: pick('year').asIntOrThrow(), + month: pick('month').asStringOrNull(), + ); + } +} - factory GameWithMove.fromJson(Map json) => - _$GameWithMoveFromJson(json); +enum Mode { casual, rated } + +extension ModeExtension on Pick { + Mode asModeOrThrow() { + switch (this.required().value) { + case 'casual': + return Mode.casual; + case 'rated': + return Mode.rated; + default: + throw PickException( + "value $value at $debugParsingExit can't be casted to Mode", + ); + } + } } @Freezed(fromJson: true) -class MasterPlayer with _$MasterPlayer { - const factory MasterPlayer({ +class Player with _$Player { + const factory Player({ required String name, required int rating, - }) = _MasterPlayer; + }) = _Player; - factory MasterPlayer.fromJson(Map json) => - _$MasterPlayerFromJson(json); -} + factory Player.fromJson(Map json) => _$PlayerFromJson(json); -@Freezed(fromJson: true) -class HistoryStat with _$HistoryStat { - const factory HistoryStat({ - required String month, - required int white, - required int draws, - required int black, - }) = _HistoryStat; - - factory HistoryStat.fromJson(Map json) => - _$HistoryStatFromJson(json); + factory Player.fromPick(RequiredPick pick) { + return Player( + name: pick('name').asStringOrThrow(), + rating: pick('rating').asIntOrThrow(), + ); + } } diff --git a/lib/src/model/analysis/opening_explorer_preferences.dart b/lib/src/model/analysis/opening_explorer_preferences.dart index 82043ab02a..369e3b27fa 100644 --- a/lib/src/model/analysis/opening_explorer_preferences.dart +++ b/lib/src/model/analysis/opening_explorer_preferences.dart @@ -1,7 +1,9 @@ import 'dart:convert'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/db/shared_preferences.dart'; +import 'package:lichess_mobile/src/model/common/perf.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'opening_explorer_preferences.freezed.dart'; @@ -33,6 +35,26 @@ class OpeningExplorerPreferences extends _$OpeningExplorerPreferences { Future setMasterDbUntil(int year) => _save(state.copyWith(masterDb: state.masterDb.copyWith(untilYear: year))); + Future toggleLichessDbSpeed(Perf speed) => _save( + state.copyWith( + lichessDb: state.lichessDb.copyWith( + speeds: state.lichessDb.speeds.contains(speed) + ? state.lichessDb.speeds.remove(speed) + : state.lichessDb.speeds.add(speed), + ), + ), + ); + + Future toggleLichessDbRating(int rating) => _save( + state.copyWith( + lichessDb: state.lichessDb.copyWith( + ratings: state.lichessDb.ratings.contains(rating) + ? state.lichessDb.ratings.remove(rating) + : state.lichessDb.ratings.add(rating), + ), + ), + ); + Future _save(OpeningExplorerPrefState newState) async { final prefs = ref.read(sharedPreferencesProvider); await prefs.setString( @@ -46,7 +68,6 @@ class OpeningExplorerPreferences extends _$OpeningExplorerPreferences { enum OpeningDatabase { master, lichess, - player, } @Freezed(fromJson: true, toJson: true) @@ -56,11 +77,13 @@ class OpeningExplorerPrefState with _$OpeningExplorerPrefState { const factory OpeningExplorerPrefState({ required OpeningDatabase db, required MasterDbPrefState masterDb, + required LichessDbPrefState lichessDb, }) = _OpeningExplorerPrefState; static final defaults = OpeningExplorerPrefState( db: OpeningDatabase.master, masterDb: MasterDbPrefState.defaults, + lichessDb: LichessDbPrefState.defaults, ); factory OpeningExplorerPrefState.fromJson(Map json) { @@ -94,3 +117,49 @@ class MasterDbPrefState with _$MasterDbPrefState { } } } + +@Freezed(fromJson: true, toJson: true) +class LichessDbPrefState with _$LichessDbPrefState { + const LichessDbPrefState._(); + + const factory LichessDbPrefState({ + required ISet speeds, + required ISet ratings, + required String since, + required String until, + }) = _LichessDbPrefState; + + static const availableSpeeds = ISetConst({ + Perf.ultraBullet, + Perf.bullet, + Perf.blitz, + Perf.rapid, + Perf.classical, + Perf.correspondence, + }); + static const availableRatings = ISetConst({ + 0, + 1000, + 1200, + 1400, + 1600, + 1800, + 2000, + 2200, + 2500, + }); + static final defaults = LichessDbPrefState( + speeds: availableSpeeds, + ratings: availableRatings, + since: '1952-01', + until: '${DateTime.now().year}-${DateTime.now().month}', + ); + + factory LichessDbPrefState.fromJson(Map json) { + try { + return _$LichessDbPrefStateFromJson(json); + } catch (_) { + return defaults; + } + } +} diff --git a/lib/src/model/analysis/opening_explorer_repository.dart b/lib/src/model/analysis/opening_explorer_repository.dart index afed7834d6..159888833c 100644 --- a/lib/src/model/analysis/opening_explorer_repository.dart +++ b/lib/src/model/analysis/opening_explorer_repository.dart @@ -1,19 +1,47 @@ +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:lichess_mobile/src/model/analysis/opening_explorer.dart'; +import 'package:lichess_mobile/src/model/analysis/opening_explorer_preferences.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; +import 'package:lichess_mobile/src/model/common/perf.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'opening_explorer_repository.g.dart'; @riverpod -Future masterDatabase( - MasterDatabaseRef ref, { +Future masterOpeningDatabase( + MasterOpeningDatabaseRef ref, { required String fen, - int? sinceYear, - int? untilYear, }) async { + final prefs = ref.watch( + openingExplorerPreferencesProvider.select( + (state) => state.masterDb, + ), + ); + return ref.withClient( + (client) => OpeningExplorerRepository(client).getMasterDatabase( + fen, + since: prefs.sinceYear, + until: prefs.untilYear, + ), + ); +} + +@riverpod +Future lichessOpeningDatabase( + LichessOpeningDatabaseRef ref, { + required String fen, +}) async { + final prefs = ref.watch( + openingExplorerPreferencesProvider.select( + (state) => state.lichessDb, + ), + ); return ref.withClient( - (client) => OpeningExplorerRepository(client) - .getMasterDatabase(fen, since: sinceYear, until: untilYear), + (client) => OpeningExplorerRepository(client).getLichessDatabase( + fen, + speeds: prefs.speeds, + ratings: prefs.ratings, + ), ); } @@ -22,7 +50,7 @@ class OpeningExplorerRepository { final LichessClient client; - Future getMasterDatabase( + Future getMasterDatabase( String fen, { int? since, int? until, @@ -36,7 +64,30 @@ class OpeningExplorerRepository { if (until != null) 'until': until.toString(), }, ), - mapper: OpeningExplorer.fromJson, + mapper: MasterOpeningExplorer.fromJson, + ); + } + + Future getLichessDatabase( + String fen, { + required ISet speeds, + required ISet ratings, + String? since, + String? until, + }) { + return client.readJson( + Uri( + path: '/lichess', + queryParameters: { + 'fen': fen, + if (speeds.isNotEmpty) + 'speeds': speeds.map((speed) => speed.name).join(','), + if (ratings.isNotEmpty) 'ratings': ratings.join(','), + if (since != null) 'since': since, + if (until != null) 'until': until, + }, + ), + mapper: LichessOpeningExplorer.fromJson, ); } } diff --git a/lib/src/view/analysis/opening_explorer_screen.dart b/lib/src/view/analysis/opening_explorer_screen.dart index 54e8c32136..644a526d6e 100644 --- a/lib/src/view/analysis/opening_explorer_screen.dart +++ b/lib/src/view/analysis/opening_explorer_screen.dart @@ -1,10 +1,12 @@ import 'package:dartchess/dartchess.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; +import 'package:lichess_mobile/src/model/analysis/opening_explorer.dart'; import 'package:lichess_mobile/src/model/analysis/opening_explorer_preferences.dart'; import 'package:lichess_mobile/src/model/analysis/opening_explorer_repository.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; @@ -201,8 +203,6 @@ class _OpeningExplorer extends ConsumerWidget { final String pgn; final AnalysisOptions options; - String formatNum(int num) => NumberFormat.decimalPatternDigits().format(num); - @override Widget build(BuildContext context, WidgetRef ref) { final ctrlProvider = analysisControllerProvider(pgn, options); @@ -217,19 +217,6 @@ class _OpeningExplorer extends ConsumerWidget { ); } - final primaryColor = Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoDynamicColor.resolve( - CupertinoColors.systemGrey5, - context, - ) - : Theme.of(context).colorScheme.secondaryContainer; - const rowVerticalPadding = 6.0; - const rowHorizontalPadding = 6.0; - const rowPadding = EdgeInsets.symmetric( - vertical: rowVerticalPadding, - horizontal: rowHorizontalPadding, - ); - final isRootNode = ref.watch( ctrlProvider.select((s) => s.currentNode.isRoot), ); @@ -246,33 +233,69 @@ class _OpeningExplorer extends ConsumerWidget { ) : nodeOpening ?? branchOpening ?? contextOpening; - final prefs = ref.watch(openingExplorerPreferencesProvider); - final masterDatabaseAsync = ref.watch( - masterDatabaseProvider( + final openingDb = ref.watch( + openingExplorerPreferencesProvider.select( + (state) => state.db, + ), + ); + + return switch (openingDb) { + OpeningDatabase.master => _MasterOpeningExplorer( + ctrlProvider: ctrlProvider, + opening: opening, + ), + OpeningDatabase.lichess => _LichessOpeningExplorer( + ctrlProvider: ctrlProvider, + opening: opening, + ), + }; + } +} + +class _MasterOpeningExplorer extends ConsumerWidget { + const _MasterOpeningExplorer({ + required this.ctrlProvider, + required this.opening, + }); + + final AnalysisControllerProvider ctrlProvider; + final Opening? opening; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final primaryColor = Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoDynamicColor.resolve( + CupertinoColors.systemGrey5, + context, + ) + : Theme.of(context).colorScheme.secondaryContainer; + + final position = ref.watch(ctrlProvider.select((value) => value.position)); + + final masterDbAsync = ref.watch( + masterOpeningDatabaseProvider( fen: position.fen, - sinceYear: prefs.masterDb.sinceYear, - untilYear: prefs.masterDb.untilYear, ), ); - return masterDatabaseAsync.when( - data: (masterDatabase) { + return masterDbAsync.when( + data: (masterDb) { return Column( children: [ if (opening != null) Container( - padding: const EdgeInsets.only(left: rowHorizontalPadding), + padding: const EdgeInsets.only(left: 6.0), color: primaryColor, child: Row( children: [ - if (opening.eco.isEmpty) - Text(opening.name) + if (opening!.eco.isEmpty) + Text(opening!.name) else - Text('${opening.eco} ${opening.name}'), + Text('${opening!.eco} ${opening!.name}'), ], ), ), - if (masterDatabase.moves.isEmpty) + if (masterDb.moves.isEmpty) const Expanded( child: Align( alignment: Alignment.center, @@ -286,221 +309,106 @@ class _OpeningExplorer extends ConsumerWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Table( - columnWidths: const { - 0: FractionColumnWidth(0.2), - 1: FractionColumnWidth(0.3), - 2: FractionColumnWidth(0.5), - }, - children: [ - TableRow( - decoration: BoxDecoration( - color: primaryColor, - ), - children: [ - Container( - padding: rowPadding, - child: Text(context.l10n.move), - ), - Container( - padding: rowPadding, - child: Text(context.l10n.games), - ), - Container( - padding: rowPadding, - child: Text(context.l10n.whiteDrawBlack), - ), - ], - ), - ...List.generate( - masterDatabase.moves.length, - (int index) { - final move = masterDatabase.moves.get(index); - final percentGames = - ((move.games / masterDatabase.games) * 100) - .round(); - return TableRow( - decoration: BoxDecoration( - color: index.isEven - ? Theme.of(context) - .colorScheme - .surfaceContainerLow - : Theme.of(context) - .colorScheme - .surfaceContainerHigh, - ), - children: [ - TableRowInkWell( - onTap: () => ref - .read(ctrlProvider.notifier) - .onUserMove(Move.fromUci(move.uci)!), - child: Container( - padding: rowPadding, - child: Text(move.san), - ), - ), - TableRowInkWell( - onTap: () => ref - .read(ctrlProvider.notifier) - .onUserMove(Move.fromUci(move.uci)!), - child: Container( - padding: rowPadding, - child: Tooltip( - message: '$percentGames%', - child: Text(formatNum(move.games)), - ), - ), - ), - TableRowInkWell( - onTap: () => ref - .read(ctrlProvider.notifier) - .onUserMove(Move.fromUci(move.uci)!), - child: Container( - padding: rowPadding, - child: _WinPercentageChart( - white: move.white, - draws: move.draws, - black: move.black, - ), - ), - ), - ], - ); - }, - ), - TableRow( - decoration: BoxDecoration( - color: masterDatabase.moves.length.isEven - ? Theme.of(context) - .colorScheme - .surfaceContainerLow - : Theme.of(context) - .colorScheme - .surfaceContainerHigh, - ), - children: [ - Container( - padding: rowPadding, - alignment: Alignment.centerLeft, - child: const Icon(Icons.functions), - ), - Container( - padding: rowPadding, - child: Tooltip( - message: '100%', - child: Text(formatNum(masterDatabase.games)), - ), - ), - Container( - padding: rowPadding, - child: _WinPercentageChart( - white: masterDatabase.white, - draws: masterDatabase.draws, - black: masterDatabase.black, - ), - ), - ], - ), - ], + _MoveTable( + moves: masterDb.moves, + whiteWins: masterDb.white, + draws: masterDb.draws, + blackWins: masterDb.black, + ctrlProvider: ctrlProvider, ), - Container( - padding: rowPadding, - color: primaryColor, - child: Row( - children: [ - Text(context.l10n.topGames), - ], - ), + _GameList( + title: context.l10n.topGames, + games: masterDb.topGames + .map((g) => Game.fromTopGame(g)) + .toIList(), + ), + ], + ), + ), + ), + ], + ); + }, + loading: () => const Center( + child: CircularProgressIndicator(), + ), + error: (error, stackTrace) => Center( + child: Text(error.toString()), + ), + ); + } +} + +class _LichessOpeningExplorer extends ConsumerWidget { + const _LichessOpeningExplorer({ + required this.ctrlProvider, + required this.opening, + }); + + final AnalysisControllerProvider ctrlProvider; + final Opening? opening; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final primaryColor = Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoDynamicColor.resolve( + CupertinoColors.systemGrey5, + context, + ) + : Theme.of(context).colorScheme.secondaryContainer; + + final position = ref.watch(ctrlProvider.select((value) => value.position)); + + final lichessDbAsync = ref.watch( + lichessOpeningDatabaseProvider( + fen: position.fen, + ), + ); + + return lichessDbAsync.when( + data: (lichessDb) { + return Column( + children: [ + if (opening != null) + Container( + padding: const EdgeInsets.only(left: 6.0), + color: primaryColor, + child: Row( + children: [ + if (opening!.eco.isEmpty) + Text(opening!.name) + else + Text('${opening!.eco} ${opening!.name}'), + ], + ), + ), + if (lichessDb.moves.isEmpty) + const Expanded( + child: Align( + alignment: Alignment.center, + child: Text('No game found'), + ), + ) + else + Expanded( + child: SingleChildScrollView( + scrollDirection: Axis.vertical, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _MoveTable( + moves: lichessDb.moves, + whiteWins: lichessDb.white, + draws: lichessDb.draws, + blackWins: lichessDb.black, + ctrlProvider: ctrlProvider, + ), + _GameList( + title: context.l10n.recentGames, + games: lichessDb.recentGames + .map((g) => Game.fromRecentGame(g)) + .toIList(), ), - ...List.generate(masterDatabase.topGames.length, - (int index) { - final game = masterDatabase.topGames.get(index); - const widthResultBox = 50.0; - const paddingResultBox = EdgeInsets.all(5); - return Container( - padding: rowPadding, - color: index.isEven - ? Theme.of(context) - .colorScheme - .surfaceContainerLow - : Theme.of(context) - .colorScheme - .surfaceContainerHigh, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text(game.white.rating.toString()), - Text(game.black.rating.toString()), - ], - ), - const SizedBox(width: 10), - Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text(game.white.name), - Text(game.black.name), - ], - ), - ], - ), - Row( - children: [ - if (game.winner == 'white') - Container( - width: widthResultBox, - padding: paddingResultBox, - color: Colors.white, - child: const Text( - '1-0', - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.black, - ), - ), - ) - else if (game.winner == 'black') - Container( - width: widthResultBox, - padding: paddingResultBox, - color: Colors.black, - child: const Text( - '0-1', - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white, - ), - ), - ) - else - Container( - width: widthResultBox, - padding: paddingResultBox, - color: Colors.grey, - child: const Text( - '½-½', - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white, - ), - ), - ), - if (game.month != null) ...[ - const SizedBox(width: 5.0), - Text(game.month!), - ], - ], - ), - ], - ), - ); - }), ], ), ), @@ -518,6 +426,283 @@ class _OpeningExplorer extends ConsumerWidget { } } +class _MoveTable extends ConsumerWidget { + const _MoveTable({ + required this.moves, + required this.whiteWins, + required this.draws, + required this.blackWins, + required this.ctrlProvider, + }); + + final IList moves; + final int whiteWins; + final int draws; + final int blackWins; + final AnalysisControllerProvider ctrlProvider; + + String formatNum(int num) => NumberFormat.decimalPatternDigits().format(num); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final primaryColor = Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoDynamicColor.resolve( + CupertinoColors.systemGrey5, + context, + ) + : Theme.of(context).colorScheme.secondaryContainer; + const rowPadding = EdgeInsets.all(6.0); + + final games = whiteWins + draws + blackWins; + + return Table( + columnWidths: const { + 0: FractionColumnWidth(0.2), + 1: FractionColumnWidth(0.3), + 2: FractionColumnWidth(0.5), + }, + children: [ + TableRow( + decoration: BoxDecoration( + color: primaryColor, + ), + children: [ + Container( + padding: rowPadding, + child: Text(context.l10n.move), + ), + Container( + padding: rowPadding, + child: Text(context.l10n.games), + ), + Container( + padding: rowPadding, + child: Text(context.l10n.whiteDrawBlack), + ), + ], + ), + ...List.generate( + moves.length, + (int index) { + final move = moves.get(index); + final percentGames = ((move.games / games) * 100).round(); + return TableRow( + decoration: BoxDecoration( + color: index.isEven + ? Theme.of(context).colorScheme.surfaceContainerLow + : Theme.of(context).colorScheme.surfaceContainerHigh, + ), + children: [ + TableRowInkWell( + onTap: () => ref + .read(ctrlProvider.notifier) + .onUserMove(Move.fromUci(move.uci)!), + child: Container( + padding: rowPadding, + child: Text(move.san), + ), + ), + TableRowInkWell( + onTap: () => ref + .read(ctrlProvider.notifier) + .onUserMove(Move.fromUci(move.uci)!), + child: Container( + padding: rowPadding, + child: Tooltip( + message: '$percentGames%', + child: Text(formatNum(move.games)), + ), + ), + ), + TableRowInkWell( + onTap: () => ref + .read(ctrlProvider.notifier) + .onUserMove(Move.fromUci(move.uci)!), + child: Container( + padding: rowPadding, + child: _WinPercentageChart( + white: move.white, + draws: move.draws, + black: move.black, + ), + ), + ), + ], + ); + }, + ), + TableRow( + decoration: BoxDecoration( + color: moves.length.isEven + ? Theme.of(context).colorScheme.surfaceContainerLow + : Theme.of(context).colorScheme.surfaceContainerHigh, + ), + children: [ + Container( + padding: rowPadding, + alignment: Alignment.centerLeft, + child: const Icon(Icons.functions), + ), + Container( + padding: rowPadding, + child: Tooltip( + message: '100%', + child: Text(formatNum(games)), + ), + ), + Container( + padding: rowPadding, + child: _WinPercentageChart( + white: whiteWins, + draws: draws, + black: blackWins, + ), + ), + ], + ), + ], + ); + } +} + +class _GameList extends StatelessWidget { + const _GameList({ + required this.title, + required this.games, + }); + + final String title; + final IList games; + + @override + Widget build(BuildContext context) { + final primaryColor = Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoDynamicColor.resolve( + CupertinoColors.systemGrey5, + context, + ) + : Theme.of(context).colorScheme.secondaryContainer; + + return Column( + children: [ + Container( + padding: const EdgeInsets.all(6.0), + color: primaryColor, + child: Row( + children: [ + Text(title), + ], + ), + ), + ...List.generate(games.length, (int index) { + return _Game( + game: games.get(index), + color: index.isEven + ? Theme.of(context).colorScheme.surfaceContainerLow + : Theme.of(context).colorScheme.surfaceContainerHigh, + ); + }), + ], + ); + } +} + +class _Game extends StatelessWidget { + const _Game({ + required this.game, + required this.color, + }); + + final Game game; + final Color color; + + @override + Widget build(BuildContext context) { + const widthResultBox = 50.0; + const paddingResultBox = EdgeInsets.all(5); + + return Container( + padding: const EdgeInsets.all(6.0), + color: color, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(game.white.rating.toString()), + Text(game.black.rating.toString()), + ], + ), + const SizedBox(width: 10), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(game.white.name), + Text(game.black.name), + ], + ), + ], + ), + Row( + children: [ + if (game.winner == 'white') + Container( + width: widthResultBox, + padding: paddingResultBox, + color: Colors.white, + child: const Text( + '1-0', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.black, + ), + ), + ) + else if (game.winner == 'black') + Container( + width: widthResultBox, + padding: paddingResultBox, + color: Colors.black, + child: const Text( + '0-1', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + ), + ), + ) + else + Container( + width: widthResultBox, + padding: paddingResultBox, + color: Colors.grey, + child: const Text( + '½-½', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + ), + ), + ), + if (game.month != null) ...[ + const SizedBox(width: 10.0), + Text(game.month!), + ], + if (game.speed != null) ...[ + const SizedBox(width: 10.0), + Icon(game.speed!.icon), + ], + ], + ), + ], + ), + ); + } +} + class _WinPercentageChart extends StatelessWidget { const _WinPercentageChart({ required this.white, diff --git a/lib/src/view/analysis/opening_explorer_settings.dart b/lib/src/view/analysis/opening_explorer_settings.dart index 47bfb19e1e..9e1f67eb05 100644 --- a/lib/src/view/analysis/opening_explorer_settings.dart +++ b/lib/src/view/analysis/opening_explorer_settings.dart @@ -53,13 +53,6 @@ class OpeningExplorerSettings extends ConsumerWidget { .read(openingExplorerPreferencesProvider.notifier) .setDatabase(OpeningDatabase.lichess), ), - ChoiceChip( - label: Text(context.l10n.player), - selected: prefs.db == OpeningDatabase.player, - onSelected: (value) => ref - .read(openingExplorerPreferencesProvider.notifier) - .setDatabase(OpeningDatabase.player), - ), ], ), ), @@ -116,6 +109,47 @@ class OpeningExplorerSettings extends ConsumerWidget { .setMasterDbUntil(value.toInt()), ), ), + ] else if (prefs.db == OpeningDatabase.lichess) ...[ + PlatformListTile( + title: Text(context.l10n.timeControl), + subtitle: Wrap( + spacing: 5, + children: LichessDbPrefState.availableSpeeds + .map( + (speed) => FilterChip( + label: Icon(speed.icon), + tooltip: speed.title, + selected: prefs.lichessDb.speeds.contains(speed), + onSelected: (value) => ref + .read(openingExplorerPreferencesProvider.notifier) + .toggleLichessDbSpeed(speed), + ), + ) + .toList(growable: false), + ), + ), + PlatformListTile( + title: Text(context.l10n.rating), + subtitle: Wrap( + spacing: 5, + children: LichessDbPrefState.availableRatings + .map( + (rating) => FilterChip( + label: Text(rating.toString()), + tooltip: rating == 0 + ? '0-1000' + : rating == 2500 + ? '2500+' + : '$rating-${rating + 200}', + selected: prefs.lichessDb.ratings.contains(rating), + onSelected: (value) => ref + .read(openingExplorerPreferencesProvider.notifier) + .toggleLichessDbRating(rating), + ), + ) + .toList(growable: false), + ), + ), ], ], ), From e17613fa70f40f2a5ffba2b7482217604d35f13d Mon Sep 17 00:00:00 2001 From: Mauritz Date: Wed, 31 Jul 2024 05:47:11 +0200 Subject: [PATCH 093/979] refactor: clean things up after last commit --- lib/src/model/analysis/opening_explorer.dart | 105 ++++------------ .../analysis/opening_explorer_screen.dart | 118 ++++++------------ 2 files changed, 63 insertions(+), 160 deletions(-) diff --git a/lib/src/model/analysis/opening_explorer.dart b/lib/src/model/analysis/opening_explorer.dart index a7db52f317..34776f7bb7 100644 --- a/lib/src/model/analysis/opening_explorer.dart +++ b/lib/src/model/analysis/opening_explorer.dart @@ -17,15 +17,11 @@ class MasterOpeningExplorer with _$MasterOpeningExplorer { required int draws, required int black, required IList moves, - required IList topGames, + required IList topGames, }) = _MasterOpeningExplorer; factory MasterOpeningExplorer.fromJson(Map json) => _$MasterOpeningExplorerFromJson(json); - - int get games { - return white + draws + black; - } } @Freezed(fromJson: true) @@ -38,15 +34,11 @@ class LichessOpeningExplorer with _$LichessOpeningExplorer { required int draws, required int black, required IList moves, - required IList recentGames, + required IList recentGames, }) = _LichessOpeningExplorer; factory LichessOpeningExplorer.fromJson(Map json) => _$LichessOpeningExplorerFromJson(json); - - int get games { - return white + draws + black; - } } @Freezed(fromJson: true) @@ -70,92 +62,30 @@ class OpeningMove with _$OpeningMove { } } -class Game { - const Game({ - required this.uci, - required this.id, - this.speed, - this.mode, - required this.white, - required this.black, - this.winner, - required this.year, - this.month, - }); - - final String uci; - final String id; - final Perf? speed; - final Mode? mode; - final Player white; - final Player black; - final String? winner; - final int year; - final String? month; - - factory Game.fromTopGame(TopGame topGame) => Game( - uci: topGame.uci, - id: topGame.id, - white: topGame.white, - black: topGame.black, - winner: topGame.winner, - year: topGame.year, - month: topGame.month, - ); - - factory Game.fromRecentGame(RecentGame recentGame) => Game( - uci: recentGame.uci, - id: recentGame.id, - speed: recentGame.speed, - mode: recentGame.mode, - white: recentGame.white, - black: recentGame.black, - winner: recentGame.winner, - year: recentGame.year, - month: recentGame.month, - ); -} - -@Freezed(fromJson: true) -class TopGame with _$TopGame { - factory TopGame({ - required String uci, - required String id, - String? winner, - required Player white, - required Player black, - required int year, - String? month, - }) = _TopGame; - - factory TopGame.fromJson(Map json) => - _$TopGameFromJson(json); -} - @Freezed(fromJson: true) -class RecentGame with _$RecentGame { - factory RecentGame({ +class Game with _$Game { + factory Game({ required String uci, required String id, String? winner, - required Perf speed, - required Mode mode, + Perf? speed, + Mode? mode, required Player white, required Player black, required int year, String? month, - }) = _RecentGame; + }) = _Game; - factory RecentGame.fromJson(Map json) => - RecentGame.fromPick(pick(json).required()); + factory Game.fromJson(Map json) => + Game.fromPick(pick(json).required()); - factory RecentGame.fromPick(RequiredPick pick) { - return RecentGame( + factory Game.fromPick(RequiredPick pick) { + return Game( uci: pick('uci').asStringOrThrow(), id: pick('id').asStringOrThrow(), winner: pick('winner').asStringOrNull(), - speed: pick('speed').asPerfOrThrow(), - mode: pick('mode').asModeOrThrow(), + speed: pick('speed').asPerfOrNull(), + mode: pick('mode').asModeOrNull(), white: pick('white').letOrThrow(Player.fromPick), black: pick('black').letOrThrow(Player.fromPick), year: pick('year').asIntOrThrow(), @@ -179,6 +109,15 @@ extension ModeExtension on Pick { ); } } + + Mode? asModeOrNull() { + if (value == null) return null; + try { + return asModeOrThrow(); + } catch (_) { + return null; + } + } } @Freezed(fromJson: true) diff --git a/lib/src/view/analysis/opening_explorer_screen.dart b/lib/src/view/analysis/opening_explorer_screen.dart index 644a526d6e..c013b07f61 100644 --- a/lib/src/view/analysis/opening_explorer_screen.dart +++ b/lib/src/view/analysis/opening_explorer_screen.dart @@ -263,15 +263,7 @@ class _MasterOpeningExplorer extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final primaryColor = Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoDynamicColor.resolve( - CupertinoColors.systemGrey5, - context, - ) - : Theme.of(context).colorScheme.secondaryContainer; - final position = ref.watch(ctrlProvider.select((value) => value.position)); - final masterDbAsync = ref.watch( masterOpeningDatabaseProvider( fen: position.fen, @@ -282,19 +274,6 @@ class _MasterOpeningExplorer extends ConsumerWidget { data: (masterDb) { return Column( children: [ - if (opening != null) - Container( - padding: const EdgeInsets.only(left: 6.0), - color: primaryColor, - child: Row( - children: [ - if (opening!.eco.isEmpty) - Text(opening!.name) - else - Text('${opening!.eco} ${opening!.name}'), - ], - ), - ), if (masterDb.moves.isEmpty) const Expanded( child: Align( @@ -309,6 +288,8 @@ class _MasterOpeningExplorer extends ConsumerWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + if (opening != null) + _Opening(opening: opening!), _MoveTable( moves: masterDb.moves, whiteWins: masterDb.white, @@ -318,9 +299,7 @@ class _MasterOpeningExplorer extends ConsumerWidget { ), _GameList( title: context.l10n.topGames, - games: masterDb.topGames - .map((g) => Game.fromTopGame(g)) - .toIList(), + games: masterDb.topGames, ), ], ), @@ -350,15 +329,7 @@ class _LichessOpeningExplorer extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final primaryColor = Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoDynamicColor.resolve( - CupertinoColors.systemGrey5, - context, - ) - : Theme.of(context).colorScheme.secondaryContainer; - final position = ref.watch(ctrlProvider.select((value) => value.position)); - final lichessDbAsync = ref.watch( lichessOpeningDatabaseProvider( fen: position.fen, @@ -369,19 +340,6 @@ class _LichessOpeningExplorer extends ConsumerWidget { data: (lichessDb) { return Column( children: [ - if (opening != null) - Container( - padding: const EdgeInsets.only(left: 6.0), - color: primaryColor, - child: Row( - children: [ - if (opening!.eco.isEmpty) - Text(opening!.name) - else - Text('${opening!.eco} ${opening!.name}'), - ], - ), - ), if (lichessDb.moves.isEmpty) const Expanded( child: Align( @@ -396,6 +354,8 @@ class _LichessOpeningExplorer extends ConsumerWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + if (opening != null) + _Opening(opening: opening!), _MoveTable( moves: lichessDb.moves, whiteWins: lichessDb.white, @@ -405,9 +365,7 @@ class _LichessOpeningExplorer extends ConsumerWidget { ), _GameList( title: context.l10n.recentGames, - games: lichessDb.recentGames - .map((g) => Game.fromRecentGame(g)) - .toIList(), + games: lichessDb.recentGames, ), ], ), @@ -426,6 +384,27 @@ class _LichessOpeningExplorer extends ConsumerWidget { } } +class _Opening extends StatelessWidget { + const _Opening({required this.opening}); + + final Opening opening; + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.only(left: 6.0), + color: Theme.of(context).colorScheme.primaryContainer, + child: Row( + children: [ + if (opening.eco.isEmpty) + Text(opening.name) + else + Text('${opening.eco} ${opening.name}'), + ], + ), + ); + } +} + class _MoveTable extends ConsumerWidget { const _MoveTable({ required this.moves, @@ -445,14 +424,7 @@ class _MoveTable extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final primaryColor = Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoDynamicColor.resolve( - CupertinoColors.systemGrey5, - context, - ) - : Theme.of(context).colorScheme.secondaryContainer; const rowPadding = EdgeInsets.all(6.0); - final games = whiteWins + draws + blackWins; return Table( @@ -464,7 +436,7 @@ class _MoveTable extends ConsumerWidget { children: [ TableRow( decoration: BoxDecoration( - color: primaryColor, + color: Theme.of(context).colorScheme.primaryContainer, ), children: [ Container( @@ -521,9 +493,9 @@ class _MoveTable extends ConsumerWidget { child: Container( padding: rowPadding, child: _WinPercentageChart( - white: move.white, + whiteWins: move.white, draws: move.draws, - black: move.black, + blackWins: move.black, ), ), ), @@ -553,9 +525,9 @@ class _MoveTable extends ConsumerWidget { Container( padding: rowPadding, child: _WinPercentageChart( - white: whiteWins, + whiteWins: whiteWins, draws: draws, - black: blackWins, + blackWins: blackWins, ), ), ], @@ -576,18 +548,11 @@ class _GameList extends StatelessWidget { @override Widget build(BuildContext context) { - final primaryColor = Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoDynamicColor.resolve( - CupertinoColors.systemGrey5, - context, - ) - : Theme.of(context).colorScheme.secondaryContainer; - return Column( children: [ Container( padding: const EdgeInsets.all(6.0), - color: primaryColor, + color: Theme.of(context).colorScheme.primaryContainer, child: Row( children: [ Text(title), @@ -705,25 +670,24 @@ class _Game extends StatelessWidget { class _WinPercentageChart extends StatelessWidget { const _WinPercentageChart({ - required this.white, + required this.whiteWins, required this.draws, - required this.black, + required this.blackWins, }); - final int white; + final int whiteWins; final int draws; - final int black; + final int blackWins; + int percentGames(int games) => + ((games / (whiteWins + draws + blackWins)) * 100).round(); String label(int percent) => percent < 20 ? '' : '$percent%'; @override Widget build(BuildContext context) { - int percentGames(int games) => - ((games / (white + draws + black)) * 100).round(); - - final percentWhite = percentGames(white); + final percentWhite = percentGames(whiteWins); final percentDraws = percentGames(draws); - final percentBlack = percentGames(black); + final percentBlack = percentGames(blackWins); return ClipRRect( borderRadius: BorderRadius.circular(5), From c984dbb2f214fd1a9a03fabf27f8ec72d1986c7d Mon Sep 17 00:00:00 2001 From: Mauritz Date: Wed, 31 Jul 2024 06:26:14 +0200 Subject: [PATCH 094/979] refactor: a bunch more refactors --- .../opening_explorer.dart | 0 .../opening_explorer_preferences.dart | 0 .../opening_explorer_repository.dart | 4 +- ...lysis_widgets.dart => analysis_board.dart} | 0 lib/src/view/analysis/analysis_screen.dart | 4 +- .../analysis/opening_explorer_settings.dart | 158 ----------------- .../opening_explorer_screen.dart | 20 +-- .../opening_explorer_settings.dart | 165 ++++++++++++++++++ lib/src/view/tools/tools_tab_screen.dart | 2 +- 9 files changed, 179 insertions(+), 174 deletions(-) rename lib/src/model/{analysis => opening_explorer}/opening_explorer.dart (100%) rename lib/src/model/{analysis => opening_explorer}/opening_explorer_preferences.dart (100%) rename lib/src/model/{analysis => opening_explorer}/opening_explorer_repository.dart (92%) rename lib/src/view/analysis/{analysis_widgets.dart => analysis_board.dart} (100%) delete mode 100644 lib/src/view/analysis/opening_explorer_settings.dart rename lib/src/view/{analysis => opening_explorer}/opening_explorer_screen.dart (97%) create mode 100644 lib/src/view/opening_explorer/opening_explorer_settings.dart diff --git a/lib/src/model/analysis/opening_explorer.dart b/lib/src/model/opening_explorer/opening_explorer.dart similarity index 100% rename from lib/src/model/analysis/opening_explorer.dart rename to lib/src/model/opening_explorer/opening_explorer.dart diff --git a/lib/src/model/analysis/opening_explorer_preferences.dart b/lib/src/model/opening_explorer/opening_explorer_preferences.dart similarity index 100% rename from lib/src/model/analysis/opening_explorer_preferences.dart rename to lib/src/model/opening_explorer/opening_explorer_preferences.dart diff --git a/lib/src/model/analysis/opening_explorer_repository.dart b/lib/src/model/opening_explorer/opening_explorer_repository.dart similarity index 92% rename from lib/src/model/analysis/opening_explorer_repository.dart rename to lib/src/model/opening_explorer/opening_explorer_repository.dart index 159888833c..d58fea5aa2 100644 --- a/lib/src/model/analysis/opening_explorer_repository.dart +++ b/lib/src/model/opening_explorer/opening_explorer_repository.dart @@ -1,8 +1,8 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:lichess_mobile/src/model/analysis/opening_explorer.dart'; -import 'package:lichess_mobile/src/model/analysis/opening_explorer_preferences.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; +import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; +import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_preferences.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'opening_explorer_repository.g.dart'; diff --git a/lib/src/view/analysis/analysis_widgets.dart b/lib/src/view/analysis/analysis_board.dart similarity index 100% rename from lib/src/view/analysis/analysis_widgets.dart rename to lib/src/view/analysis/analysis_board.dart diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index 4312d56336..983fba01c7 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -30,6 +30,7 @@ import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/utils/string.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_share_screen.dart'; import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; +import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_screen.dart'; import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; @@ -40,9 +41,8 @@ import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:popover/popover.dart'; import '../../utils/share.dart'; +import 'analysis_board.dart'; import 'analysis_settings.dart'; -import 'analysis_widgets.dart'; -import 'opening_explorer_screen.dart'; import 'tree_view.dart'; class AnalysisScreen extends StatelessWidget { diff --git a/lib/src/view/analysis/opening_explorer_settings.dart b/lib/src/view/analysis/opening_explorer_settings.dart deleted file mode 100644 index 9e1f67eb05..0000000000 --- a/lib/src/view/analysis/opening_explorer_settings.dart +++ /dev/null @@ -1,158 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; -import 'package:lichess_mobile/src/model/analysis/opening_explorer_preferences.dart'; -import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/l10n_context.dart'; -import 'package:lichess_mobile/src/widgets/list.dart'; -import 'package:lichess_mobile/src/widgets/non_linear_slider.dart'; - -class OpeningExplorerSettings extends ConsumerWidget { - const OpeningExplorerSettings(this.pgn, this.options); - - final String pgn; - final AnalysisOptions options; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final prefs = ref.watch(openingExplorerPreferencesProvider); - - const earliestYear = 1952; - final years = DateTime.now().year - earliestYear + 1; - - return DraggableScrollableSheet( - initialChildSize: .7, - expand: false, - snap: true, - snapSizes: const [.7], - builder: (context, scrollController) => ListView( - controller: scrollController, - children: [ - PlatformListTile( - title: - Text(context.l10n.settingsSettings, style: Styles.sectionTitle), - subtitle: const SizedBox.shrink(), - ), - const SizedBox(height: 8.0), - PlatformListTile( - title: Text(context.l10n.database), - subtitle: Wrap( - spacing: 5, - children: [ - ChoiceChip( - label: const Text('Masters'), - selected: prefs.db == OpeningDatabase.master, - onSelected: (value) => ref - .read(openingExplorerPreferencesProvider.notifier) - .setDatabase(OpeningDatabase.master), - ), - ChoiceChip( - label: const Text('Lichess'), - selected: prefs.db == OpeningDatabase.lichess, - onSelected: (value) => ref - .read(openingExplorerPreferencesProvider.notifier) - .setDatabase(OpeningDatabase.lichess), - ), - ], - ), - ), - if (prefs.db == OpeningDatabase.master) ...[ - PlatformListTile( - title: Text.rich( - TextSpan( - text: '${context.l10n.since}: ', - style: const TextStyle( - fontWeight: FontWeight.normal, - ), - children: [ - TextSpan( - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - ), - text: prefs.masterDb.sinceYear.toString(), - ), - ], - ), - ), - subtitle: NonLinearSlider( - value: prefs.masterDb.sinceYear, - values: List.generate(years, (index) => earliestYear + index), - onChangeEnd: (value) => ref - .read(openingExplorerPreferencesProvider.notifier) - .setMasterDbSince(value.toInt()), - ), - ), - PlatformListTile( - title: Text.rich( - TextSpan( - text: '${context.l10n.until}: ', - style: const TextStyle( - fontWeight: FontWeight.normal, - ), - children: [ - TextSpan( - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - ), - text: prefs.masterDb.untilYear.toString(), - ), - ], - ), - ), - subtitle: NonLinearSlider( - value: prefs.masterDb.untilYear, - values: List.generate(years, (index) => earliestYear + index), - onChangeEnd: (value) => ref - .read(openingExplorerPreferencesProvider.notifier) - .setMasterDbUntil(value.toInt()), - ), - ), - ] else if (prefs.db == OpeningDatabase.lichess) ...[ - PlatformListTile( - title: Text(context.l10n.timeControl), - subtitle: Wrap( - spacing: 5, - children: LichessDbPrefState.availableSpeeds - .map( - (speed) => FilterChip( - label: Icon(speed.icon), - tooltip: speed.title, - selected: prefs.lichessDb.speeds.contains(speed), - onSelected: (value) => ref - .read(openingExplorerPreferencesProvider.notifier) - .toggleLichessDbSpeed(speed), - ), - ) - .toList(growable: false), - ), - ), - PlatformListTile( - title: Text(context.l10n.rating), - subtitle: Wrap( - spacing: 5, - children: LichessDbPrefState.availableRatings - .map( - (rating) => FilterChip( - label: Text(rating.toString()), - tooltip: rating == 0 - ? '0-1000' - : rating == 2500 - ? '2500+' - : '$rating-${rating + 200}', - selected: prefs.lichessDb.ratings.contains(rating), - onSelected: (value) => ref - .read(openingExplorerPreferencesProvider.notifier) - .toggleLichessDbRating(rating), - ), - ) - .toList(growable: false), - ), - ), - ], - ], - ), - ); - } -} diff --git a/lib/src/view/analysis/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart similarity index 97% rename from lib/src/view/analysis/opening_explorer_screen.dart rename to lib/src/view/opening_explorer/opening_explorer_screen.dart index c013b07f61..669472f091 100644 --- a/lib/src/view/analysis/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -6,17 +6,18 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; -import 'package:lichess_mobile/src/model/analysis/opening_explorer.dart'; -import 'package:lichess_mobile/src/model/analysis/opening_explorer_preferences.dart'; -import 'package:lichess_mobile/src/model/analysis/opening_explorer_repository.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/game/game_share_service.dart'; +import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; +import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_preferences.dart'; +import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_repository.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/utils/share.dart'; +import 'package:lichess_mobile/src/view/analysis/analysis_board.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_share_screen.dart'; import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; @@ -26,7 +27,6 @@ import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; -import 'analysis_widgets.dart'; import 'opening_explorer_settings.dart'; class OpeningExplorerScreen extends StatelessWidget { @@ -288,8 +288,7 @@ class _MasterOpeningExplorer extends ConsumerWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (opening != null) - _Opening(opening: opening!), + if (opening != null) _Opening(opening: opening!), _MoveTable( moves: masterDb.moves, whiteWins: masterDb.white, @@ -354,8 +353,7 @@ class _LichessOpeningExplorer extends ConsumerWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (opening != null) - _Opening(opening: opening!), + if (opening != null) _Opening(opening: opening!), _MoveTable( moves: lichessDb.moves, whiteWins: lichessDb.white, @@ -560,7 +558,7 @@ class _GameList extends StatelessWidget { ), ), ...List.generate(games.length, (int index) { - return _Game( + return _GameTile( game: games.get(index), color: index.isEven ? Theme.of(context).colorScheme.surfaceContainerLow @@ -572,8 +570,8 @@ class _GameList extends StatelessWidget { } } -class _Game extends StatelessWidget { - const _Game({ +class _GameTile extends StatelessWidget { + const _GameTile({ required this.game, required this.color, }); diff --git a/lib/src/view/opening_explorer/opening_explorer_settings.dart b/lib/src/view/opening_explorer/opening_explorer_settings.dart new file mode 100644 index 0000000000..4d1e2a62cb --- /dev/null +++ b/lib/src/view/opening_explorer/opening_explorer_settings.dart @@ -0,0 +1,165 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; +import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_preferences.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/widgets/list.dart'; +import 'package:lichess_mobile/src/widgets/non_linear_slider.dart'; + +class OpeningExplorerSettings extends ConsumerWidget { + const OpeningExplorerSettings(this.pgn, this.options); + + final String pgn; + final AnalysisOptions options; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final prefs = ref.watch(openingExplorerPreferencesProvider); + + const earliestYear = 1952; + final years = DateTime.now().year - earliestYear + 1; + + final List masterDbSettings = [ + PlatformListTile( + title: Text.rich( + TextSpan( + text: '${context.l10n.since}: ', + style: const TextStyle( + fontWeight: FontWeight.normal, + ), + children: [ + TextSpan( + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + text: prefs.masterDb.sinceYear.toString(), + ), + ], + ), + ), + subtitle: NonLinearSlider( + value: prefs.masterDb.sinceYear, + values: List.generate(years, (index) => earliestYear + index), + onChangeEnd: (value) => ref + .read(openingExplorerPreferencesProvider.notifier) + .setMasterDbSince(value.toInt()), + ), + ), + PlatformListTile( + title: Text.rich( + TextSpan( + text: '${context.l10n.until}: ', + style: const TextStyle( + fontWeight: FontWeight.normal, + ), + children: [ + TextSpan( + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + text: prefs.masterDb.untilYear.toString(), + ), + ], + ), + ), + subtitle: NonLinearSlider( + value: prefs.masterDb.untilYear, + values: List.generate(years, (index) => earliestYear + index), + onChangeEnd: (value) => ref + .read(openingExplorerPreferencesProvider.notifier) + .setMasterDbUntil(value.toInt()), + ), + ), + ]; + + final List lichessDbSettings = [ + PlatformListTile( + title: Text(context.l10n.timeControl), + subtitle: Wrap( + spacing: 5, + children: LichessDbPrefState.availableSpeeds + .map( + (speed) => FilterChip( + label: Icon(speed.icon), + tooltip: speed.title, + selected: prefs.lichessDb.speeds.contains(speed), + onSelected: (value) => ref + .read(openingExplorerPreferencesProvider.notifier) + .toggleLichessDbSpeed(speed), + ), + ) + .toList(growable: false), + ), + ), + PlatformListTile( + title: Text(context.l10n.rating), + subtitle: Wrap( + spacing: 5, + children: LichessDbPrefState.availableRatings + .map( + (rating) => FilterChip( + label: Text(rating.toString()), + tooltip: rating == 0 + ? '0-1000' + : rating == 2500 + ? '2500+' + : '$rating-${rating + 200}', + selected: prefs.lichessDb.ratings.contains(rating), + onSelected: (value) => ref + .read(openingExplorerPreferencesProvider.notifier) + .toggleLichessDbRating(rating), + ), + ) + .toList(growable: false), + ), + ), + ]; + + return DraggableScrollableSheet( + initialChildSize: .7, + expand: false, + snap: true, + snapSizes: const [.7], + builder: (context, scrollController) => ListView( + controller: scrollController, + children: [ + PlatformListTile( + title: + Text(context.l10n.settingsSettings, style: Styles.sectionTitle), + subtitle: const SizedBox.shrink(), + ), + const SizedBox(height: 8.0), + PlatformListTile( + title: Text(context.l10n.database), + subtitle: Wrap( + spacing: 5, + children: [ + ChoiceChip( + label: const Text('Masters'), + selected: prefs.db == OpeningDatabase.master, + onSelected: (value) => ref + .read(openingExplorerPreferencesProvider.notifier) + .setDatabase(OpeningDatabase.master), + ), + ChoiceChip( + label: const Text('Lichess'), + selected: prefs.db == OpeningDatabase.lichess, + onSelected: (value) => ref + .read(openingExplorerPreferencesProvider.notifier) + .setDatabase(OpeningDatabase.lichess), + ), + ], + ), + ), + if (prefs.db == OpeningDatabase.master) + ...masterDbSettings + else if (prefs.db == OpeningDatabase.lichess) + ...lichessDbSettings, + ], + ), + ); + } +} diff --git a/lib/src/view/tools/tools_tab_screen.dart b/lib/src/view/tools/tools_tab_screen.dart index 1030dbfdd6..ae8a0a8857 100644 --- a/lib/src/view/tools/tools_tab_screen.dart +++ b/lib/src/view/tools/tools_tab_screen.dart @@ -10,8 +10,8 @@ import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_position_choice_screen.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; -import 'package:lichess_mobile/src/view/analysis/opening_explorer_screen.dart'; import 'package:lichess_mobile/src/view/clock/clock_screen.dart'; +import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_screen.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; From 4bb69f09e9c60389e802456cb0305030cea04df0 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Wed, 31 Jul 2024 08:20:22 +0200 Subject: [PATCH 095/979] feat: pick since and until dates for lichess db --- .../opening_explorer_preferences.dart | 20 +++++-- .../opening_explorer_repository.dart | 10 ++-- .../opening_explorer_settings.dart | 34 +++++++++-- lib/src/widgets/settings.dart | 60 +++++++++++++++++++ 4 files changed, 109 insertions(+), 15 deletions(-) diff --git a/lib/src/model/opening_explorer/opening_explorer_preferences.dart b/lib/src/model/opening_explorer/opening_explorer_preferences.dart index 369e3b27fa..4622b3815e 100644 --- a/lib/src/model/opening_explorer/opening_explorer_preferences.dart +++ b/lib/src/model/opening_explorer/opening_explorer_preferences.dart @@ -55,6 +55,14 @@ class OpeningExplorerPreferences extends _$OpeningExplorerPreferences { ), ); + Future setLichessDbSince(DateTime since) => _save( + state.copyWith(lichessDb: state.lichessDb.copyWith(since: since)), + ); + + Future setLichessDbUntil(DateTime until) => _save( + state.copyWith(lichessDb: state.lichessDb.copyWith(until: until)), + ); + Future _save(OpeningExplorerPrefState newState) async { final prefs = ref.read(sharedPreferencesProvider); await prefs.setString( @@ -104,8 +112,9 @@ class MasterDbPrefState with _$MasterDbPrefState { required int untilYear, }) = _MasterDbPrefState; + static const earliestYear = 1952; static final defaults = MasterDbPrefState( - sinceYear: 1952, + sinceYear: earliestYear, untilYear: DateTime.now().year, ); @@ -125,8 +134,8 @@ class LichessDbPrefState with _$LichessDbPrefState { const factory LichessDbPrefState({ required ISet speeds, required ISet ratings, - required String since, - required String until, + required DateTime since, + required DateTime until, }) = _LichessDbPrefState; static const availableSpeeds = ISetConst({ @@ -148,11 +157,12 @@ class LichessDbPrefState with _$LichessDbPrefState { 2200, 2500, }); + static final earliestDate = DateTime.parse('2012-12-01'); static final defaults = LichessDbPrefState( speeds: availableSpeeds, ratings: availableRatings, - since: '1952-01', - until: '${DateTime.now().year}-${DateTime.now().month}', + since: earliestDate, + until: DateTime.now(), ); factory LichessDbPrefState.fromJson(Map json) { diff --git a/lib/src/model/opening_explorer/opening_explorer_repository.dart b/lib/src/model/opening_explorer/opening_explorer_repository.dart index d58fea5aa2..de14b6b09f 100644 --- a/lib/src/model/opening_explorer/opening_explorer_repository.dart +++ b/lib/src/model/opening_explorer/opening_explorer_repository.dart @@ -41,6 +41,8 @@ Future lichessOpeningDatabase( fen, speeds: prefs.speeds, ratings: prefs.ratings, + since: prefs.since, + until: prefs.until, ), ); } @@ -72,8 +74,8 @@ class OpeningExplorerRepository { String fen, { required ISet speeds, required ISet ratings, - String? since, - String? until, + DateTime? since, + DateTime? until, }) { return client.readJson( Uri( @@ -83,8 +85,8 @@ class OpeningExplorerRepository { if (speeds.isNotEmpty) 'speeds': speeds.map((speed) => speed.name).join(','), if (ratings.isNotEmpty) 'ratings': ratings.join(','), - if (since != null) 'since': since, - if (until != null) 'until': until, + if (since != null) 'since': '${since.year}-${since.month}', + if (until != null) 'until': '${until.year}-${until.month}', }, ), mapper: LichessOpeningExplorer.fromJson, diff --git a/lib/src/view/opening_explorer/opening_explorer_settings.dart b/lib/src/view/opening_explorer/opening_explorer_settings.dart index 4d1e2a62cb..02054220f1 100644 --- a/lib/src/view/opening_explorer/opening_explorer_settings.dart +++ b/lib/src/view/opening_explorer/opening_explorer_settings.dart @@ -6,6 +6,7 @@ import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/non_linear_slider.dart'; +import 'package:lichess_mobile/src/widgets/settings.dart'; class OpeningExplorerSettings extends ConsumerWidget { const OpeningExplorerSettings(this.pgn, this.options); @@ -17,8 +18,7 @@ class OpeningExplorerSettings extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final prefs = ref.watch(openingExplorerPreferencesProvider); - const earliestYear = 1952; - final years = DateTime.now().year - earliestYear + 1; + final years = DateTime.now().year - MasterDbPrefState.earliestYear + 1; final List masterDbSettings = [ PlatformListTile( @@ -41,7 +41,10 @@ class OpeningExplorerSettings extends ConsumerWidget { ), subtitle: NonLinearSlider( value: prefs.masterDb.sinceYear, - values: List.generate(years, (index) => earliestYear + index), + values: List.generate( + years, + (index) => MasterDbPrefState.earliestYear + index, + ), onChangeEnd: (value) => ref .read(openingExplorerPreferencesProvider.notifier) .setMasterDbSince(value.toInt()), @@ -67,7 +70,10 @@ class OpeningExplorerSettings extends ConsumerWidget { ), subtitle: NonLinearSlider( value: prefs.masterDb.untilYear, - values: List.generate(years, (index) => earliestYear + index), + values: List.generate( + years, + (index) => MasterDbPrefState.earliestYear + index, + ), onChangeEnd: (value) => ref .read(openingExplorerPreferencesProvider.notifier) .setMasterDbUntil(value.toInt()), @@ -116,13 +122,29 @@ class OpeningExplorerSettings extends ConsumerWidget { .toList(growable: false), ), ), + DatePickerSettingsTile( + title: context.l10n.since, + value: prefs.lichessDb.since, + firstDate: LichessDbPrefState.earliestDate, + onChanged: (value) => ref + .read(openingExplorerPreferencesProvider.notifier) + .setLichessDbSince(value), + ), + DatePickerSettingsTile( + title: context.l10n.until, + value: prefs.lichessDb.until, + firstDate: LichessDbPrefState.earliestDate, + onChanged: (value) => ref + .read(openingExplorerPreferencesProvider.notifier) + .setLichessDbUntil(value), + ), ]; return DraggableScrollableSheet( - initialChildSize: .7, + initialChildSize: .8, expand: false, snap: true, - snapSizes: const [.7], + snapSizes: const [.8], builder: (context, scrollController) => ListView( controller: scrollController, children: [ diff --git a/lib/src/widgets/settings.dart b/lib/src/widgets/settings.dart index 9e322568d7..1a06abc599 100644 --- a/lib/src/widgets/settings.dart +++ b/lib/src/widgets/settings.dart @@ -324,3 +324,63 @@ class ChoicePicker extends StatelessWidget { } } } + +class DatePickerSettingsTile extends StatelessWidget { + const DatePickerSettingsTile({ + required this.title, + required this.value, + required this.firstDate, + required this.onChanged, + this.selectableDayPredicate, + }); + + final String title; + final DateTime value; + final DateTime firstDate; + final void Function(DateTime) onChanged; + final bool Function(DateTime)? selectableDayPredicate; + + @override + Widget build(BuildContext context) { + return PlatformListTile( + title: Text.rich( + TextSpan( + text: '$title: ', + style: const TextStyle( + fontWeight: FontWeight.normal, + ), + children: [ + TextSpan( + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + text: value.toString().split(' ').first, + ), + WidgetSpan( + alignment: PlaceholderAlignment.baseline, + baseline: TextBaseline.ideographic, + child: IconButton( + icon: const Icon(Icons.date_range), + padding: EdgeInsets.zero, + onPressed: () => showDatePicker( + context: context, + initialDate: value, + firstDate: firstDate, + lastDate: DateTime.now(), + selectableDayPredicate: selectableDayPredicate, + ).then( + (value) { + if (value != null) { + onChanged(value); + } + }, + ), + ), + ), + ], + ), + ), + ); + } +} From b0defae45f1c7a087131909ebce0c274cdb939a7 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Wed, 31 Jul 2024 08:54:37 +0200 Subject: [PATCH 096/979] refactor: use same object for all opening explorer types instead of a different one for each, use a single one with nullable fields. --- .../opening_explorer/opening_explorer.dart | 32 ++--- .../opening_explorer_repository.dart | 60 +++----- .../opening_explorer_screen.dart | 133 +++--------------- lib/src/widgets/settings.dart | 2 +- 4 files changed, 54 insertions(+), 173 deletions(-) diff --git a/lib/src/model/opening_explorer/opening_explorer.dart b/lib/src/model/opening_explorer/opening_explorer.dart index 34776f7bb7..7fdc4117dd 100644 --- a/lib/src/model/opening_explorer/opening_explorer.dart +++ b/lib/src/model/opening_explorer/opening_explorer.dart @@ -8,37 +8,21 @@ part 'opening_explorer.freezed.dart'; part 'opening_explorer.g.dart'; @Freezed(fromJson: true) -class MasterOpeningExplorer with _$MasterOpeningExplorer { - const MasterOpeningExplorer._(); +class OpeningExplorer with _$OpeningExplorer { + const OpeningExplorer._(); - const factory MasterOpeningExplorer({ + const factory OpeningExplorer({ LightOpening? opening, required int white, required int draws, required int black, required IList moves, - required IList topGames, - }) = _MasterOpeningExplorer; + IList? topGames, + IList? recentGames, + }) = _OpeningExplorer; - factory MasterOpeningExplorer.fromJson(Map json) => - _$MasterOpeningExplorerFromJson(json); -} - -@Freezed(fromJson: true) -class LichessOpeningExplorer with _$LichessOpeningExplorer { - const LichessOpeningExplorer._(); - - const factory LichessOpeningExplorer({ - LightOpening? opening, - required int white, - required int draws, - required int black, - required IList moves, - required IList recentGames, - }) = _LichessOpeningExplorer; - - factory LichessOpeningExplorer.fromJson(Map json) => - _$LichessOpeningExplorerFromJson(json); + factory OpeningExplorer.fromJson(Map json) => + _$OpeningExplorerFromJson(json); } @Freezed(fromJson: true) diff --git a/lib/src/model/opening_explorer/opening_explorer_repository.dart b/lib/src/model/opening_explorer/opening_explorer_repository.dart index de14b6b09f..b87fce4f72 100644 --- a/lib/src/model/opening_explorer/opening_explorer_repository.dart +++ b/lib/src/model/opening_explorer/opening_explorer_repository.dart @@ -8,42 +8,28 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'opening_explorer_repository.g.dart'; @riverpod -Future masterOpeningDatabase( - MasterOpeningDatabaseRef ref, { +Future openingExplorer( + OpeningExplorerRef ref, { required String fen, }) async { - final prefs = ref.watch( - openingExplorerPreferencesProvider.select( - (state) => state.masterDb, - ), - ); - return ref.withClient( - (client) => OpeningExplorerRepository(client).getMasterDatabase( - fen, - since: prefs.sinceYear, - until: prefs.untilYear, - ), - ); -} - -@riverpod -Future lichessOpeningDatabase( - LichessOpeningDatabaseRef ref, { - required String fen, -}) async { - final prefs = ref.watch( - openingExplorerPreferencesProvider.select( - (state) => state.lichessDb, - ), - ); + final prefs = ref.watch(openingExplorerPreferencesProvider); return ref.withClient( - (client) => OpeningExplorerRepository(client).getLichessDatabase( - fen, - speeds: prefs.speeds, - ratings: prefs.ratings, - since: prefs.since, - until: prefs.until, - ), + (client) => switch (prefs.db) { + OpeningDatabase.master => + OpeningExplorerRepository(client).getMasterDatabase( + fen, + since: prefs.masterDb.sinceYear, + until: prefs.masterDb.untilYear, + ), + OpeningDatabase.lichess => + OpeningExplorerRepository(client).getLichessDatabase( + fen, + speeds: prefs.lichessDb.speeds, + ratings: prefs.lichessDb.ratings, + since: prefs.lichessDb.since, + until: prefs.lichessDb.until, + ), + }, ); } @@ -52,7 +38,7 @@ class OpeningExplorerRepository { final LichessClient client; - Future getMasterDatabase( + Future getMasterDatabase( String fen, { int? since, int? until, @@ -66,11 +52,11 @@ class OpeningExplorerRepository { if (until != null) 'until': until.toString(), }, ), - mapper: MasterOpeningExplorer.fromJson, + mapper: OpeningExplorer.fromJson, ); } - Future getLichessDatabase( + Future getLichessDatabase( String fen, { required ISet speeds, required ISet ratings, @@ -89,7 +75,7 @@ class OpeningExplorerRepository { if (until != null) 'until': '${until.year}-${until.month}', }, ), - mapper: LichessOpeningExplorer.fromJson, + mapper: OpeningExplorer.fromJson, ); } } diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index 669472f091..df7685b9fa 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -10,7 +10,6 @@ import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/game/game_share_service.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; -import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_preferences.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_repository.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; @@ -233,48 +232,17 @@ class _OpeningExplorer extends ConsumerWidget { ) : nodeOpening ?? branchOpening ?? contextOpening; - final openingDb = ref.watch( - openingExplorerPreferencesProvider.select( - (state) => state.db, - ), - ); - - return switch (openingDb) { - OpeningDatabase.master => _MasterOpeningExplorer( - ctrlProvider: ctrlProvider, - opening: opening, - ), - OpeningDatabase.lichess => _LichessOpeningExplorer( - ctrlProvider: ctrlProvider, - opening: opening, - ), - }; - } -} - -class _MasterOpeningExplorer extends ConsumerWidget { - const _MasterOpeningExplorer({ - required this.ctrlProvider, - required this.opening, - }); - - final AnalysisControllerProvider ctrlProvider; - final Opening? opening; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final position = ref.watch(ctrlProvider.select((value) => value.position)); - final masterDbAsync = ref.watch( - masterOpeningDatabaseProvider( + final openingExplorerAsync = ref.watch( + openingExplorerProvider( fen: position.fen, ), ); - return masterDbAsync.when( - data: (masterDb) { + return openingExplorerAsync.when( + data: (openingExplorer) { return Column( children: [ - if (masterDb.moves.isEmpty) + if (openingExplorer.moves.isEmpty) const Expanded( child: Align( alignment: Alignment.center, @@ -288,83 +256,26 @@ class _MasterOpeningExplorer extends ConsumerWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (opening != null) _Opening(opening: opening!), + if (opening != null) _Opening(opening: opening), _MoveTable( - moves: masterDb.moves, - whiteWins: masterDb.white, - draws: masterDb.draws, - blackWins: masterDb.black, + moves: openingExplorer.moves, + whiteWins: openingExplorer.white, + draws: openingExplorer.draws, + blackWins: openingExplorer.black, ctrlProvider: ctrlProvider, ), - _GameList( - title: context.l10n.topGames, - games: masterDb.topGames, - ), - ], - ), - ), - ), - ], - ); - }, - loading: () => const Center( - child: CircularProgressIndicator(), - ), - error: (error, stackTrace) => Center( - child: Text(error.toString()), - ), - ); - } -} - -class _LichessOpeningExplorer extends ConsumerWidget { - const _LichessOpeningExplorer({ - required this.ctrlProvider, - required this.opening, - }); - - final AnalysisControllerProvider ctrlProvider; - final Opening? opening; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final position = ref.watch(ctrlProvider.select((value) => value.position)); - final lichessDbAsync = ref.watch( - lichessOpeningDatabaseProvider( - fen: position.fen, - ), - ); - - return lichessDbAsync.when( - data: (lichessDb) { - return Column( - children: [ - if (lichessDb.moves.isEmpty) - const Expanded( - child: Align( - alignment: Alignment.center, - child: Text('No game found'), - ), - ) - else - Expanded( - child: SingleChildScrollView( - scrollDirection: Axis.vertical, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (opening != null) _Opening(opening: opening!), - _MoveTable( - moves: lichessDb.moves, - whiteWins: lichessDb.white, - draws: lichessDb.draws, - blackWins: lichessDb.black, - ctrlProvider: ctrlProvider, - ), - _GameList( - title: context.l10n.recentGames, - games: lichessDb.recentGames, - ), + if (openingExplorer.topGames != null && + openingExplorer.topGames!.isNotEmpty) + _GameList( + title: context.l10n.topGames, + games: openingExplorer.topGames!, + ), + if (openingExplorer.recentGames != null && + openingExplorer.recentGames!.isNotEmpty) + _GameList( + title: context.l10n.recentGames, + games: openingExplorer.recentGames!, + ), ], ), ), diff --git a/lib/src/widgets/settings.dart b/lib/src/widgets/settings.dart index 1a06abc599..969f4163ec 100644 --- a/lib/src/widgets/settings.dart +++ b/lib/src/widgets/settings.dart @@ -339,7 +339,7 @@ class DatePickerSettingsTile extends StatelessWidget { final DateTime firstDate; final void Function(DateTime) onChanged; final bool Function(DateTime)? selectableDayPredicate; - + @override Widget build(BuildContext context) { return PlatformListTile( From a04d593979f3f4f53b75276fb62faa29dde250be Mon Sep 17 00:00:00 2001 From: Mauritz Date: Wed, 31 Jul 2024 09:18:00 +0200 Subject: [PATCH 097/979] feat: view lichess game ontap --- .../opening_explorer_screen.dart | 169 ++++++++++-------- 1 file changed, 98 insertions(+), 71 deletions(-) diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index df7685b9fa..0ce2be3521 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -8,8 +8,11 @@ import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/game/game_repository_providers.dart'; import 'package:lichess_mobile/src/model/game/game_share_service.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; +import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_preferences.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_repository.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; @@ -19,6 +22,7 @@ import 'package:lichess_mobile/src/utils/share.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_board.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_share_screen.dart'; +import 'package:lichess_mobile/src/view/game/archived_game_screen.dart'; import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; @@ -481,7 +485,7 @@ class _GameList extends StatelessWidget { } } -class _GameTile extends StatelessWidget { +class _GameTile extends ConsumerWidget { const _GameTile({ required this.game, required this.color, @@ -491,87 +495,110 @@ class _GameTile extends StatelessWidget { final Color color; @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { const widthResultBox = 50.0; const paddingResultBox = EdgeInsets.all(5); + final openingDb = ref.watch(openingExplorerPreferencesProvider).db; + return Container( padding: const EdgeInsets.all(6.0), color: color, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(game.white.rating.toString()), - Text(game.black.rating.toString()), - ], - ), - const SizedBox(width: 10), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(game.white.name), - Text(game.black.name), - ], - ), - ], - ), - Row( - children: [ - if (game.winner == 'white') - Container( - width: widthResultBox, - padding: paddingResultBox, - color: Colors.white, - child: const Text( - '1-0', - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.black, - ), + child: AdaptiveInkWell( + onTap: () async { + switch (openingDb) { + case OpeningDatabase.master: + return; + case OpeningDatabase.lichess: + final archivedGame = await ref.read( + archivedGameProvider(id: GameId(game.id)).future, + ); + if (context.mounted) { + pushPlatformRoute( + context, + builder: (_) => ArchivedGameScreen( + gameData: archivedGame.data, + orientation: Side.white, ), - ) - else if (game.winner == 'black') - Container( - width: widthResultBox, - padding: paddingResultBox, - color: Colors.black, - child: const Text( - '0-1', - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white, + ); + } + } + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(game.white.rating.toString()), + Text(game.black.rating.toString()), + ], + ), + const SizedBox(width: 10), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(game.white.name), + Text(game.black.name), + ], + ), + ], + ), + Row( + children: [ + if (game.winner == 'white') + Container( + width: widthResultBox, + padding: paddingResultBox, + color: Colors.white, + child: const Text( + '1-0', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.black, + ), ), - ), - ) - else - Container( - width: widthResultBox, - padding: paddingResultBox, - color: Colors.grey, - child: const Text( - '½-½', - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white, + ) + else if (game.winner == 'black') + Container( + width: widthResultBox, + padding: paddingResultBox, + color: Colors.black, + child: const Text( + '0-1', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + ), + ), + ) + else + Container( + width: widthResultBox, + padding: paddingResultBox, + color: Colors.grey, + child: const Text( + '½-½', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + ), ), ), - ), - if (game.month != null) ...[ - const SizedBox(width: 10.0), - Text(game.month!), - ], - if (game.speed != null) ...[ - const SizedBox(width: 10.0), - Icon(game.speed!.icon), + if (game.month != null) ...[ + const SizedBox(width: 10.0), + Text(game.month!), + ], + if (game.speed != null) ...[ + const SizedBox(width: 10.0), + Icon(game.speed!.icon), + ], ], - ], - ), - ], + ), + ], + ), ), ); } From c8103c23f8a1ddde048ef58af489c3b940934cac Mon Sep 17 00:00:00 2001 From: Mauritz Date: Wed, 31 Jul 2024 09:34:18 +0200 Subject: [PATCH 098/979] fix: align game result box correctly when date not present --- .../opening_explorer_screen.dart | 132 +++++++++--------- 1 file changed, 69 insertions(+), 63 deletions(-) diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index 0ce2be3521..b75360292f 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -527,75 +527,81 @@ class _GameTile extends ConsumerWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(game.white.rating.toString()), - Text(game.black.rating.toString()), - ], - ), - const SizedBox(width: 10), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(game.white.name), - Text(game.black.name), - ], - ), - ], + Expanded( + flex: 6, + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(game.white.rating.toString()), + Text(game.black.rating.toString()), + ], + ), + const SizedBox(width: 10), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(game.white.name), + Text(game.black.name), + ], + ), + ], + ), ), - Row( - children: [ - if (game.winner == 'white') - Container( - width: widthResultBox, - padding: paddingResultBox, - color: Colors.white, - child: const Text( - '1-0', - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.black, + Expanded( + flex: 4, + child: Row( + children: [ + if (game.winner == 'white') + Container( + width: widthResultBox, + padding: paddingResultBox, + color: Colors.white, + child: const Text( + '1-0', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.black, + ), ), - ), - ) - else if (game.winner == 'black') - Container( - width: widthResultBox, - padding: paddingResultBox, - color: Colors.black, - child: const Text( - '0-1', - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white, + ) + else if (game.winner == 'black') + Container( + width: widthResultBox, + padding: paddingResultBox, + color: Colors.black, + child: const Text( + '0-1', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + ), ), - ), - ) - else - Container( - width: widthResultBox, - padding: paddingResultBox, - color: Colors.grey, - child: const Text( - '½-½', - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white, + ) + else + Container( + width: widthResultBox, + padding: paddingResultBox, + color: Colors.grey, + child: const Text( + '½-½', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + ), ), ), - ), - if (game.month != null) ...[ - const SizedBox(width: 10.0), - Text(game.month!), - ], - if (game.speed != null) ...[ - const SizedBox(width: 10.0), - Icon(game.speed!.icon), + if (game.month != null) ...[ + const SizedBox(width: 10.0), + Text(game.month!), + ], + if (game.speed != null) ...[ + const SizedBox(width: 10.0), + Icon(game.speed!.icon), + ], ], - ], + ), ), ], ), From 061cf975ec394df5d6ec539d707558e5cdd75191 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Sat, 3 Aug 2024 08:21:47 +0200 Subject: [PATCH 099/979] feat: add specific player opening explorer --- .../opening_explorer/opening_explorer.dart | 13 +- .../opening_explorer_preferences.dart | 84 ++++++++++ .../opening_explorer_repository.dart | 40 +++++ .../opening_explorer_screen.dart | 12 ++ .../opening_explorer_settings.dart | 150 +++++++++++++++++- lib/src/view/user/player_screen.dart | 9 +- lib/src/view/user/search_screen.dart | 30 ++-- 7 files changed, 317 insertions(+), 21 deletions(-) diff --git a/lib/src/model/opening_explorer/opening_explorer.dart b/lib/src/model/opening_explorer/opening_explorer.dart index 7fdc4117dd..db680eb325 100644 --- a/lib/src/model/opening_explorer/opening_explorer.dart +++ b/lib/src/model/opening_explorer/opening_explorer.dart @@ -32,7 +32,9 @@ class OpeningMove with _$OpeningMove { const factory OpeningMove({ required String uci, required String san, - required int averageRating, + int? averageRating, + int? averageOpponentRating, + int? performance, required int white, required int draws, required int black, @@ -78,7 +80,14 @@ class Game with _$Game { } } -enum Mode { casual, rated } +enum Mode { + casual('Casual'), + rated('Rated'); + + const Mode(this.title); + + final String title; +} extension ModeExtension on Pick { Mode asModeOrThrow() { diff --git a/lib/src/model/opening_explorer/opening_explorer_preferences.dart b/lib/src/model/opening_explorer/opening_explorer_preferences.dart index 4622b3815e..eb48d9378b 100644 --- a/lib/src/model/opening_explorer/opening_explorer_preferences.dart +++ b/lib/src/model/opening_explorer/opening_explorer_preferences.dart @@ -1,9 +1,11 @@ import 'dart:convert'; +import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/db/shared_preferences.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; +import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'opening_explorer_preferences.freezed.dart'; @@ -63,6 +65,46 @@ class OpeningExplorerPreferences extends _$OpeningExplorerPreferences { state.copyWith(lichessDb: state.lichessDb.copyWith(until: until)), ); + Future setPlayerDbUsernameOrId(String usernameOrId) => _save( + state.copyWith( + playerDb: state.playerDb.copyWith( + usernameOrId: usernameOrId, + ), + ), + ); + + Future setPlayerDbSide(Side side) => _save( + state.copyWith(playerDb: state.playerDb.copyWith(side: side)), + ); + + Future togglePlayerDbSpeed(Perf speed) => _save( + state.copyWith( + playerDb: state.playerDb.copyWith( + speeds: state.playerDb.speeds.contains(speed) + ? state.playerDb.speeds.remove(speed) + : state.playerDb.speeds.add(speed), + ), + ), + ); + + Future togglePlayerDbMode(Mode mode) => _save( + state.copyWith( + playerDb: state.playerDb.copyWith( + modes: state.playerDb.modes.contains(mode) + ? state.playerDb.modes.remove(mode) + : state.playerDb.modes.add(mode), + ), + ), + ); + + Future setPlayerDbSince(DateTime since) => _save( + state.copyWith(playerDb: state.playerDb.copyWith(since: since)), + ); + + Future setPlayerDbUntil(DateTime until) => _save( + state.copyWith(playerDb: state.playerDb.copyWith(until: until)), + ); + Future _save(OpeningExplorerPrefState newState) async { final prefs = ref.read(sharedPreferencesProvider); await prefs.setString( @@ -76,6 +118,7 @@ class OpeningExplorerPreferences extends _$OpeningExplorerPreferences { enum OpeningDatabase { master, lichess, + player, } @Freezed(fromJson: true, toJson: true) @@ -86,12 +129,14 @@ class OpeningExplorerPrefState with _$OpeningExplorerPrefState { required OpeningDatabase db, required MasterDbPrefState masterDb, required LichessDbPrefState lichessDb, + required PlayerDbPrefState playerDb, }) = _OpeningExplorerPrefState; static final defaults = OpeningExplorerPrefState( db: OpeningDatabase.master, masterDb: MasterDbPrefState.defaults, lichessDb: LichessDbPrefState.defaults, + playerDb: PlayerDbPrefState.defaults, ); factory OpeningExplorerPrefState.fromJson(Map json) { @@ -173,3 +218,42 @@ class LichessDbPrefState with _$LichessDbPrefState { } } } + +@Freezed(fromJson: true, toJson: true) +class PlayerDbPrefState with _$PlayerDbPrefState { + const PlayerDbPrefState._(); + + const factory PlayerDbPrefState({ + String? usernameOrId, + required Side side, + required ISet speeds, + required ISet modes, + required DateTime since, + required DateTime until, + }) = _PlayerDbPrefState; + + static const availableSpeeds = ISetConst({ + Perf.ultraBullet, + Perf.bullet, + Perf.blitz, + Perf.rapid, + Perf.classical, + Perf.correspondence, + }); + static final earliestDate = DateTime.parse('2012-12-01'); + static final defaults = PlayerDbPrefState( + side: Side.white, + speeds: availableSpeeds, + modes: Mode.values.toISet(), + since: earliestDate, + until: DateTime.now(), + ); + + factory PlayerDbPrefState.fromJson(Map json) { + try { + return _$PlayerDbPrefStateFromJson(json); + } catch (_) { + return defaults; + } + } +} diff --git a/lib/src/model/opening_explorer/opening_explorer_repository.dart b/lib/src/model/opening_explorer/opening_explorer_repository.dart index b87fce4f72..fabb9d5f83 100644 --- a/lib/src/model/opening_explorer/opening_explorer_repository.dart +++ b/lib/src/model/opening_explorer/opening_explorer_repository.dart @@ -1,3 +1,4 @@ +import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; @@ -29,6 +30,17 @@ Future openingExplorer( since: prefs.lichessDb.since, until: prefs.lichessDb.until, ), + OpeningDatabase.player => + OpeningExplorerRepository(client).getPlayerDatabase( + fen, + // null check handled by widget + usernameOrId: prefs.playerDb.usernameOrId!, + color: prefs.playerDb.side, + speeds: prefs.playerDb.speeds, + modes: prefs.playerDb.modes, + since: prefs.playerDb.since, + until: prefs.playerDb.until, + ), }, ); } @@ -78,4 +90,32 @@ class OpeningExplorerRepository { mapper: OpeningExplorer.fromJson, ); } + + Future getPlayerDatabase( + String fen, { + required String usernameOrId, + required Side color, + required ISet speeds, + required ISet modes, + DateTime? since, + DateTime? until, + }) { + return client.readJson( + Uri( + path: '/player', + queryParameters: { + 'fen': fen, + 'player': usernameOrId, + 'color': color.name, + if (speeds.isNotEmpty) + 'speeds': speeds.map((speed) => speed.name).join(','), + if (modes.isNotEmpty) + 'modes': modes.map((mode) => mode.name).join(','), + if (since != null) 'since': '${since.year}-${since.month}', + if (until != null) 'until': '${until.year}-${until.month}', + }, + ), + mapper: OpeningExplorer.fromJson, + ); + } } diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index b75360292f..0d5a87e5b5 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -220,6 +220,17 @@ class _OpeningExplorer extends ConsumerWidget { ); } + final prefs = ref.read(openingExplorerPreferencesProvider); + if (prefs.db == OpeningDatabase.player && + prefs.playerDb.usernameOrId == null) { + return const Expanded( + child: Align( + alignment: Alignment.center, + child: Text('Select a lichess player in the settings'), + ), + ); + } + final isRootNode = ref.watch( ctrlProvider.select((s) => s.currentNode.isRoot), ); @@ -510,6 +521,7 @@ class _GameTile extends ConsumerWidget { case OpeningDatabase.master: return; case OpeningDatabase.lichess: + case OpeningDatabase.player: final archivedGame = await ref.read( archivedGameProvider(id: GameId(game.id)).future, ); diff --git a/lib/src/view/opening_explorer/opening_explorer_settings.dart b/lib/src/view/opening_explorer/opening_explorer_settings.dart index 02054220f1..dc05a1b135 100644 --- a/lib/src/view/opening_explorer/opening_explorer_settings.dart +++ b/lib/src/view/opening_explorer/opening_explorer_settings.dart @@ -1,11 +1,18 @@ +import 'package:dartchess/dartchess.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; +import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_preferences.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/view/user/player_screen.dart'; +import 'package:lichess_mobile/src/view/user/search_screen.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/non_linear_slider.dart'; +import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/settings.dart'; class OpeningExplorerSettings extends ConsumerWidget { @@ -139,6 +146,133 @@ class OpeningExplorerSettings extends ConsumerWidget { .setLichessDbUntil(value), ), ]; + final List playerDbSettings = [ + PlatformListTile( + title: Text.rich( + TextSpan( + text: '${context.l10n.player}: ', + style: const TextStyle( + fontWeight: FontWeight.normal, + ), + children: [ + TextSpan( + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + text: prefs.playerDb.usernameOrId, + ), + ], + ), + ), + subtitle: PlatformWidget( + androidBuilder: (context) => SearchBar( + leading: const Icon(Icons.search), + hintText: context.l10n.searchSearch, + focusNode: AlwaysDisabledFocusNode(), + onTap: () => pushPlatformRoute( + context, + fullscreenDialog: true, + builder: (_) => SearchScreen( + onUserTap: (user) => { + ref + .read(openingExplorerPreferencesProvider.notifier) + .setPlayerDbUsernameOrId(user.name), + Navigator.of(context).pop(), + }, + ), + ), + ), + iosBuilder: (context) => CupertinoSearchTextField( + placeholder: context.l10n.searchSearch, + focusNode: AlwaysDisabledFocusNode(), + onTap: () => pushPlatformRoute( + context, + fullscreenDialog: true, + builder: (_) => SearchScreen( + onUserTap: (user) => { + ref + .read(openingExplorerPreferencesProvider.notifier) + .setPlayerDbUsernameOrId(user.name), + Navigator.of(context).pop(), + }, + ), + ), + ), + ), + ), + PlatformListTile( + title: Text(context.l10n.side), + subtitle: Wrap( + spacing: 5, + children: Side.values + .map( + (side) => ChoiceChip( + label: switch (side) { + Side.white => const Text('White'), + Side.black => const Text('Black'), + }, + selected: prefs.playerDb.side == side, + onSelected: (value) => ref + .read(openingExplorerPreferencesProvider.notifier) + .setPlayerDbSide(side), + ), + ) + .toList(growable: false), + ), + ), + PlatformListTile( + title: Text(context.l10n.timeControl), + subtitle: Wrap( + spacing: 5, + children: PlayerDbPrefState.availableSpeeds + .map( + (speed) => FilterChip( + label: Icon(speed.icon), + tooltip: speed.title, + selected: prefs.playerDb.speeds.contains(speed), + onSelected: (value) => ref + .read(openingExplorerPreferencesProvider.notifier) + .togglePlayerDbSpeed(speed), + ), + ) + .toList(growable: false), + ), + ), + PlatformListTile( + title: Text(context.l10n.mode), + subtitle: Wrap( + spacing: 5, + children: Mode.values + .map( + (mode) => FilterChip( + label: Text(mode.title), + selected: prefs.playerDb.modes.contains(mode), + onSelected: (value) => ref + .read(openingExplorerPreferencesProvider.notifier) + .togglePlayerDbMode(mode), + ), + ) + .toList(growable: false), + ), + ), + DatePickerSettingsTile( + title: context.l10n.since, + value: prefs.playerDb.since, + firstDate: PlayerDbPrefState.earliestDate, + onChanged: (value) => ref + .read(openingExplorerPreferencesProvider.notifier) + .setPlayerDbSince(value), + ), + DatePickerSettingsTile( + title: context.l10n.until, + value: prefs.playerDb.until, + firstDate: PlayerDbPrefState.earliestDate, + onChanged: (value) => ref + .read(openingExplorerPreferencesProvider.notifier) + .setPlayerDbUntil(value), + ), + ]; return DraggableScrollableSheet( initialChildSize: .8, @@ -173,13 +307,21 @@ class OpeningExplorerSettings extends ConsumerWidget { .read(openingExplorerPreferencesProvider.notifier) .setDatabase(OpeningDatabase.lichess), ), + ChoiceChip( + label: Text(context.l10n.player), + selected: prefs.db == OpeningDatabase.player, + onSelected: (value) => ref + .read(openingExplorerPreferencesProvider.notifier) + .setDatabase(OpeningDatabase.player), + ), ], ), ), - if (prefs.db == OpeningDatabase.master) - ...masterDbSettings - else if (prefs.db == OpeningDatabase.lichess) - ...lichessDbSettings, + ...switch (prefs.db) { + OpeningDatabase.master => masterDbSettings, + OpeningDatabase.lichess => lichessDbSettings, + OpeningDatabase.player => playerDbSettings, + }, ], ), ); diff --git a/lib/src/view/user/player_screen.dart b/lib/src/view/user/player_screen.dart index 5d5a9c4855..06e69f63c8 100644 --- a/lib/src/view/user/player_screen.dart +++ b/lib/src/view/user/player_screen.dart @@ -88,6 +88,11 @@ class _SearchButton extends StatelessWidget { @override Widget build(BuildContext context) { + void onTapUser(LightUser user) => pushPlatformRoute( + context, + builder: (ctx) => UserScreen(user: user), + ); + return PlatformWidget( androidBuilder: (context) => SearchBar( leading: const Icon(Icons.search), @@ -96,7 +101,7 @@ class _SearchButton extends StatelessWidget { onTap: () => pushPlatformRoute( context, fullscreenDialog: true, - builder: (_) => const SearchScreen(), + builder: (_) => SearchScreen(onUserTap: onTapUser), ), ), iosBuilder: (context) => CupertinoSearchTextField( @@ -105,7 +110,7 @@ class _SearchButton extends StatelessWidget { onTap: () => pushPlatformRoute( context, fullscreenDialog: true, - builder: (_) => const SearchScreen(), + builder: (_) => SearchScreen(onUserTap: onTapUser), ), ), ); diff --git a/lib/src/view/user/search_screen.dart b/lib/src/view/user/search_screen.dart index 3a415e2886..efd0f2d828 100644 --- a/lib/src/view/user/search_screen.dart +++ b/lib/src/view/user/search_screen.dart @@ -3,11 +3,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/user/search_history.dart'; +import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/model/user/user_repository_providers.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; -import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/rate_limit.dart'; -import 'package:lichess_mobile/src/view/user/user_screen.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; @@ -17,7 +16,11 @@ import 'package:lichess_mobile/src/widgets/user_list_tile.dart'; const _kSaveHistoryDebouncTimer = Duration(seconds: 2); class SearchScreen extends ConsumerStatefulWidget { - const SearchScreen(); + const SearchScreen({ + this.onUserTap, + }); + + final void Function(LightUser)? onUserTap; @override ConsumerState createState() => _SearchScreenState(); @@ -91,7 +94,7 @@ class _SearchScreenState extends ConsumerState { autoFocus: true, ), ), - body: _Body(_term, setSearchText), + body: _Body(_term, setSearchText, widget.onUserTap), ); } @@ -112,21 +115,22 @@ class _SearchScreenState extends ConsumerState { onPressed: () => Navigator.pop(context), ), ), - child: _Body(_term, setSearchText), + child: _Body(_term, setSearchText, widget.onUserTap), ); } } class _Body extends ConsumerWidget { - const _Body(this.term, this.onRecentSearchTap); + const _Body(this.term, this.onRecentSearchTap, this.onUserTap); final String? term; final void Function(String) onRecentSearchTap; + final void Function(LightUser)? onUserTap; @override Widget build(BuildContext context, WidgetRef ref) { if (term != null) { return SafeArea( - child: _UserList(term!), + child: _UserList(term!, onUserTap), ); } else { final searchHistory = ref.watch(searchHistoryProvider).history; @@ -160,9 +164,10 @@ class _Body extends ConsumerWidget { } class _UserList extends ConsumerWidget { - const _UserList(this.term); + const _UserList(this.term, this.onUserTap); final String term; + final void Function(LightUser)? onUserTap; @override Widget build(BuildContext context, WidgetRef ref) { @@ -184,11 +189,10 @@ class _UserList extends ConsumerWidget { .map( (user) => UserListTile.fromLightUser( user, - onTap: () => { - pushPlatformRoute( - context, - builder: (ctx) => UserScreen(user: user), - ), + onTap: () { + if (onUserTap != null) { + onUserTap!.call(user); + } }, ), ) From f6744df03d38f3f792a556e46eacfde224dae380 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Sat, 3 Aug 2024 08:51:06 +0200 Subject: [PATCH 100/979] refactor: prefix constants names with k --- .../opening_explorer_preferences.dart | 18 +++++++++--------- .../opening_explorer_settings.dart | 12 ++++++------ 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/lib/src/model/opening_explorer/opening_explorer_preferences.dart b/lib/src/model/opening_explorer/opening_explorer_preferences.dart index eb48d9378b..89069c401e 100644 --- a/lib/src/model/opening_explorer/opening_explorer_preferences.dart +++ b/lib/src/model/opening_explorer/opening_explorer_preferences.dart @@ -157,9 +157,9 @@ class MasterDbPrefState with _$MasterDbPrefState { required int untilYear, }) = _MasterDbPrefState; - static const earliestYear = 1952; + static const kEarliestYear = 1952; static final defaults = MasterDbPrefState( - sinceYear: earliestYear, + sinceYear: kEarliestYear, untilYear: DateTime.now().year, ); @@ -183,7 +183,7 @@ class LichessDbPrefState with _$LichessDbPrefState { required DateTime until, }) = _LichessDbPrefState; - static const availableSpeeds = ISetConst({ + static const kAvailableSpeeds = ISetConst({ Perf.ultraBullet, Perf.bullet, Perf.blitz, @@ -191,7 +191,7 @@ class LichessDbPrefState with _$LichessDbPrefState { Perf.classical, Perf.correspondence, }); - static const availableRatings = ISetConst({ + static const kAvailableRatings = ISetConst({ 0, 1000, 1200, @@ -204,8 +204,8 @@ class LichessDbPrefState with _$LichessDbPrefState { }); static final earliestDate = DateTime.parse('2012-12-01'); static final defaults = LichessDbPrefState( - speeds: availableSpeeds, - ratings: availableRatings, + speeds: kAvailableSpeeds, + ratings: kAvailableRatings, since: earliestDate, until: DateTime.now(), ); @@ -232,7 +232,7 @@ class PlayerDbPrefState with _$PlayerDbPrefState { required DateTime until, }) = _PlayerDbPrefState; - static const availableSpeeds = ISetConst({ + static const kAvailableSpeeds = ISetConst({ Perf.ultraBullet, Perf.bullet, Perf.blitz, @@ -240,10 +240,10 @@ class PlayerDbPrefState with _$PlayerDbPrefState { Perf.classical, Perf.correspondence, }); - static final earliestDate = DateTime.parse('2012-12-01'); + static final earliestDate = DateTime.utc(2012, 12, 1); static final defaults = PlayerDbPrefState( side: Side.white, - speeds: availableSpeeds, + speeds: kAvailableSpeeds, modes: Mode.values.toISet(), since: earliestDate, until: DateTime.now(), diff --git a/lib/src/view/opening_explorer/opening_explorer_settings.dart b/lib/src/view/opening_explorer/opening_explorer_settings.dart index dc05a1b135..730e0591d5 100644 --- a/lib/src/view/opening_explorer/opening_explorer_settings.dart +++ b/lib/src/view/opening_explorer/opening_explorer_settings.dart @@ -25,7 +25,7 @@ class OpeningExplorerSettings extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final prefs = ref.watch(openingExplorerPreferencesProvider); - final years = DateTime.now().year - MasterDbPrefState.earliestYear + 1; + final years = DateTime.now().year - MasterDbPrefState.kEarliestYear + 1; final List masterDbSettings = [ PlatformListTile( @@ -50,7 +50,7 @@ class OpeningExplorerSettings extends ConsumerWidget { value: prefs.masterDb.sinceYear, values: List.generate( years, - (index) => MasterDbPrefState.earliestYear + index, + (index) => MasterDbPrefState.kEarliestYear + index, ), onChangeEnd: (value) => ref .read(openingExplorerPreferencesProvider.notifier) @@ -79,7 +79,7 @@ class OpeningExplorerSettings extends ConsumerWidget { value: prefs.masterDb.untilYear, values: List.generate( years, - (index) => MasterDbPrefState.earliestYear + index, + (index) => MasterDbPrefState.kEarliestYear + index, ), onChangeEnd: (value) => ref .read(openingExplorerPreferencesProvider.notifier) @@ -93,7 +93,7 @@ class OpeningExplorerSettings extends ConsumerWidget { title: Text(context.l10n.timeControl), subtitle: Wrap( spacing: 5, - children: LichessDbPrefState.availableSpeeds + children: LichessDbPrefState.kAvailableSpeeds .map( (speed) => FilterChip( label: Icon(speed.icon), @@ -111,7 +111,7 @@ class OpeningExplorerSettings extends ConsumerWidget { title: Text(context.l10n.rating), subtitle: Wrap( spacing: 5, - children: LichessDbPrefState.availableRatings + children: LichessDbPrefState.kAvailableRatings .map( (rating) => FilterChip( label: Text(rating.toString()), @@ -225,7 +225,7 @@ class OpeningExplorerSettings extends ConsumerWidget { title: Text(context.l10n.timeControl), subtitle: Wrap( spacing: 5, - children: PlayerDbPrefState.availableSpeeds + children: PlayerDbPrefState.kAvailableSpeeds .map( (speed) => FilterChip( label: Icon(speed.icon), From 8a87a10402303a590f7bdd9f977a2586a680e05f Mon Sep 17 00:00:00 2001 From: Mauritz Date: Sat, 3 Aug 2024 11:14:05 +0200 Subject: [PATCH 101/979] feat: select timespan by predefined choices --- .../opening_explorer_preferences.dart | 47 +++--- .../opening_explorer_repository.dart | 9 -- .../opening_explorer_screen.dart | 18 +-- .../opening_explorer_settings.dart | 152 +++++++----------- lib/src/widgets/settings.dart | 60 ------- 5 files changed, 88 insertions(+), 198 deletions(-) diff --git a/lib/src/model/opening_explorer/opening_explorer_preferences.dart b/lib/src/model/opening_explorer/opening_explorer_preferences.dart index 89069c401e..1e5352cbe1 100644 --- a/lib/src/model/opening_explorer/opening_explorer_preferences.dart +++ b/lib/src/model/opening_explorer/opening_explorer_preferences.dart @@ -34,9 +34,6 @@ class OpeningExplorerPreferences extends _$OpeningExplorerPreferences { Future setMasterDbSince(int year) => _save(state.copyWith(masterDb: state.masterDb.copyWith(sinceYear: year))); - Future setMasterDbUntil(int year) => - _save(state.copyWith(masterDb: state.masterDb.copyWith(untilYear: year))); - Future toggleLichessDbSpeed(Perf speed) => _save( state.copyWith( lichessDb: state.lichessDb.copyWith( @@ -61,10 +58,6 @@ class OpeningExplorerPreferences extends _$OpeningExplorerPreferences { state.copyWith(lichessDb: state.lichessDb.copyWith(since: since)), ); - Future setLichessDbUntil(DateTime until) => _save( - state.copyWith(lichessDb: state.lichessDb.copyWith(until: until)), - ); - Future setPlayerDbUsernameOrId(String usernameOrId) => _save( state.copyWith( playerDb: state.playerDb.copyWith( @@ -101,10 +94,6 @@ class OpeningExplorerPreferences extends _$OpeningExplorerPreferences { state.copyWith(playerDb: state.playerDb.copyWith(since: since)), ); - Future setPlayerDbUntil(DateTime until) => _save( - state.copyWith(playerDb: state.playerDb.copyWith(until: until)), - ); - Future _save(OpeningExplorerPrefState newState) async { final prefs = ref.read(sharedPreferencesProvider); await prefs.setString( @@ -154,14 +143,17 @@ class MasterDbPrefState with _$MasterDbPrefState { const factory MasterDbPrefState({ required int sinceYear, - required int untilYear, }) = _MasterDbPrefState; static const kEarliestYear = 1952; - static final defaults = MasterDbPrefState( - sinceYear: kEarliestYear, - untilYear: DateTime.now().year, - ); + static final now = DateTime.now(); + static final datesMap = { + 'Last 3 years': now.year - 3, + 'Last 10 years': now.year - 10, + 'Last 20 years': now.year - 20, + 'All time': kEarliestYear, + }; + static const defaults = MasterDbPrefState(sinceYear: kEarliestYear); factory MasterDbPrefState.fromJson(Map json) { try { @@ -180,7 +172,6 @@ class LichessDbPrefState with _$LichessDbPrefState { required ISet speeds, required ISet ratings, required DateTime since, - required DateTime until, }) = _LichessDbPrefState; static const kAvailableSpeeds = ISetConst({ @@ -202,12 +193,18 @@ class LichessDbPrefState with _$LichessDbPrefState { 2200, 2500, }); - static final earliestDate = DateTime.parse('2012-12-01'); + static final earliestDate = DateTime.utc(2012, 12); + static final now = DateTime.now(); + static const kDaysInAYear = 365; + static final datesMap = { + 'Last year': now.subtract(const Duration(days: kDaysInAYear)), + 'Last 5 years': now.subtract(const Duration(days: kDaysInAYear * 5)), + 'All time': earliestDate, + }; static final defaults = LichessDbPrefState( speeds: kAvailableSpeeds, ratings: kAvailableRatings, since: earliestDate, - until: DateTime.now(), ); factory LichessDbPrefState.fromJson(Map json) { @@ -229,7 +226,6 @@ class PlayerDbPrefState with _$PlayerDbPrefState { required ISet speeds, required ISet modes, required DateTime since, - required DateTime until, }) = _PlayerDbPrefState; static const kAvailableSpeeds = ISetConst({ @@ -240,13 +236,20 @@ class PlayerDbPrefState with _$PlayerDbPrefState { Perf.classical, Perf.correspondence, }); - static final earliestDate = DateTime.utc(2012, 12, 1); + static final earliestDate = DateTime.utc(2012, 12); + static final now = DateTime.now(); + static final datesMap = { + 'This month': now, + 'Last month': now.subtract(const Duration(days: 32)), + 'Last 6 months': now.subtract(const Duration(days: 183)), + 'Last year': now.subtract(const Duration(days: 365)), + 'All time': earliestDate, + }; static final defaults = PlayerDbPrefState( side: Side.white, speeds: kAvailableSpeeds, modes: Mode.values.toISet(), since: earliestDate, - until: DateTime.now(), ); factory PlayerDbPrefState.fromJson(Map json) { diff --git a/lib/src/model/opening_explorer/opening_explorer_repository.dart b/lib/src/model/opening_explorer/opening_explorer_repository.dart index fabb9d5f83..4d277887d1 100644 --- a/lib/src/model/opening_explorer/opening_explorer_repository.dart +++ b/lib/src/model/opening_explorer/opening_explorer_repository.dart @@ -20,7 +20,6 @@ Future openingExplorer( OpeningExplorerRepository(client).getMasterDatabase( fen, since: prefs.masterDb.sinceYear, - until: prefs.masterDb.untilYear, ), OpeningDatabase.lichess => OpeningExplorerRepository(client).getLichessDatabase( @@ -28,7 +27,6 @@ Future openingExplorer( speeds: prefs.lichessDb.speeds, ratings: prefs.lichessDb.ratings, since: prefs.lichessDb.since, - until: prefs.lichessDb.until, ), OpeningDatabase.player => OpeningExplorerRepository(client).getPlayerDatabase( @@ -39,7 +37,6 @@ Future openingExplorer( speeds: prefs.playerDb.speeds, modes: prefs.playerDb.modes, since: prefs.playerDb.since, - until: prefs.playerDb.until, ), }, ); @@ -53,7 +50,6 @@ class OpeningExplorerRepository { Future getMasterDatabase( String fen, { int? since, - int? until, }) { return client.readJson( Uri( @@ -61,7 +57,6 @@ class OpeningExplorerRepository { queryParameters: { 'fen': fen, if (since != null) 'since': since.toString(), - if (until != null) 'until': until.toString(), }, ), mapper: OpeningExplorer.fromJson, @@ -73,7 +68,6 @@ class OpeningExplorerRepository { required ISet speeds, required ISet ratings, DateTime? since, - DateTime? until, }) { return client.readJson( Uri( @@ -84,7 +78,6 @@ class OpeningExplorerRepository { 'speeds': speeds.map((speed) => speed.name).join(','), if (ratings.isNotEmpty) 'ratings': ratings.join(','), if (since != null) 'since': '${since.year}-${since.month}', - if (until != null) 'until': '${until.year}-${until.month}', }, ), mapper: OpeningExplorer.fromJson, @@ -98,7 +91,6 @@ class OpeningExplorerRepository { required ISet speeds, required ISet modes, DateTime? since, - DateTime? until, }) { return client.readJson( Uri( @@ -112,7 +104,6 @@ class OpeningExplorerRepository { if (modes.isNotEmpty) 'modes': modes.map((mode) => mode.name).join(','), if (since != null) 'since': '${since.year}-${since.month}', - if (until != null) 'until': '${until.year}-${until.month}', }, ), mapper: OpeningExplorer.fromJson, diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index 0d5a87e5b5..b1abe61dc0 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -212,22 +212,18 @@ class _OpeningExplorer extends ConsumerWidget { final position = ref.watch(ctrlProvider.select((value) => value.position)); if (position.fullmoves > 24) { - return const Expanded( - child: Align( - alignment: Alignment.center, - child: Text('Max depth reached'), - ), + return const Align( + alignment: Alignment.center, + child: Text('Max depth reached'), ); } - final prefs = ref.read(openingExplorerPreferencesProvider); + final prefs = ref.watch(openingExplorerPreferencesProvider); if (prefs.db == OpeningDatabase.player && prefs.playerDb.usernameOrId == null) { - return const Expanded( - child: Align( - alignment: Alignment.center, - child: Text('Select a lichess player in the settings'), - ), + return const Align( + alignment: Alignment.center, + child: Text('Select a lichess player in the settings'), ); } diff --git a/lib/src/view/opening_explorer/opening_explorer_settings.dart b/lib/src/view/opening_explorer/opening_explorer_settings.dart index 730e0591d5..a903a2903a 100644 --- a/lib/src/view/opening_explorer/opening_explorer_settings.dart +++ b/lib/src/view/opening_explorer/opening_explorer_settings.dart @@ -11,9 +11,7 @@ import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/user/player_screen.dart'; import 'package:lichess_mobile/src/view/user/search_screen.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; -import 'package:lichess_mobile/src/widgets/non_linear_slider.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; -import 'package:lichess_mobile/src/widgets/settings.dart'; class OpeningExplorerSettings extends ConsumerWidget { const OpeningExplorerSettings(this.pgn, this.options); @@ -25,65 +23,23 @@ class OpeningExplorerSettings extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final prefs = ref.watch(openingExplorerPreferencesProvider); - final years = DateTime.now().year - MasterDbPrefState.kEarliestYear + 1; - final List masterDbSettings = [ PlatformListTile( - title: Text.rich( - TextSpan( - text: '${context.l10n.since}: ', - style: const TextStyle( - fontWeight: FontWeight.normal, - ), - children: [ - TextSpan( - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - ), - text: prefs.masterDb.sinceYear.toString(), - ), - ], - ), - ), - subtitle: NonLinearSlider( - value: prefs.masterDb.sinceYear, - values: List.generate( - years, - (index) => MasterDbPrefState.kEarliestYear + index, - ), - onChangeEnd: (value) => ref - .read(openingExplorerPreferencesProvider.notifier) - .setMasterDbSince(value.toInt()), - ), - ), - PlatformListTile( - title: Text.rich( - TextSpan( - text: '${context.l10n.until}: ', - style: const TextStyle( - fontWeight: FontWeight.normal, - ), - children: [ - TextSpan( - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, + title: const Text('Timespan'), + subtitle: Wrap( + spacing: 5, + children: MasterDbPrefState.datesMap.keys + .map( + (key) => ChoiceChip( + label: Text(key), + selected: prefs.masterDb.sinceYear == + MasterDbPrefState.datesMap[key], + onSelected: (_) => ref + .read(openingExplorerPreferencesProvider.notifier) + .setMasterDbSince(MasterDbPrefState.datesMap[key]!), ), - text: prefs.masterDb.untilYear.toString(), - ), - ], - ), - ), - subtitle: NonLinearSlider( - value: prefs.masterDb.untilYear, - values: List.generate( - years, - (index) => MasterDbPrefState.kEarliestYear + index, - ), - onChangeEnd: (value) => ref - .read(openingExplorerPreferencesProvider.notifier) - .setMasterDbUntil(value.toInt()), + ) + .toList(growable: false), ), ), ]; @@ -99,7 +55,7 @@ class OpeningExplorerSettings extends ConsumerWidget { label: Icon(speed.icon), tooltip: speed.title, selected: prefs.lichessDb.speeds.contains(speed), - onSelected: (value) => ref + onSelected: (_) => ref .read(openingExplorerPreferencesProvider.notifier) .toggleLichessDbSpeed(speed), ), @@ -121,7 +77,7 @@ class OpeningExplorerSettings extends ConsumerWidget { ? '2500+' : '$rating-${rating + 200}', selected: prefs.lichessDb.ratings.contains(rating), - onSelected: (value) => ref + onSelected: (_) => ref .read(openingExplorerPreferencesProvider.notifier) .toggleLichessDbRating(rating), ), @@ -129,21 +85,23 @@ class OpeningExplorerSettings extends ConsumerWidget { .toList(growable: false), ), ), - DatePickerSettingsTile( - title: context.l10n.since, - value: prefs.lichessDb.since, - firstDate: LichessDbPrefState.earliestDate, - onChanged: (value) => ref - .read(openingExplorerPreferencesProvider.notifier) - .setLichessDbSince(value), - ), - DatePickerSettingsTile( - title: context.l10n.until, - value: prefs.lichessDb.until, - firstDate: LichessDbPrefState.earliestDate, - onChanged: (value) => ref - .read(openingExplorerPreferencesProvider.notifier) - .setLichessDbUntil(value), + PlatformListTile( + title: const Text('Timespan'), + subtitle: Wrap( + spacing: 5, + children: LichessDbPrefState.datesMap.keys + .map( + (key) => ChoiceChip( + label: Text(key), + selected: + prefs.lichessDb.since == LichessDbPrefState.datesMap[key], + onSelected: (_) => ref + .read(openingExplorerPreferencesProvider.notifier) + .setLichessDbSince(LichessDbPrefState.datesMap[key]!), + ), + ) + .toList(growable: false), + ), ), ]; final List playerDbSettings = [ @@ -213,7 +171,7 @@ class OpeningExplorerSettings extends ConsumerWidget { Side.black => const Text('Black'), }, selected: prefs.playerDb.side == side, - onSelected: (value) => ref + onSelected: (_) => ref .read(openingExplorerPreferencesProvider.notifier) .setPlayerDbSide(side), ), @@ -231,7 +189,7 @@ class OpeningExplorerSettings extends ConsumerWidget { label: Icon(speed.icon), tooltip: speed.title, selected: prefs.playerDb.speeds.contains(speed), - onSelected: (value) => ref + onSelected: (_) => ref .read(openingExplorerPreferencesProvider.notifier) .togglePlayerDbSpeed(speed), ), @@ -248,7 +206,7 @@ class OpeningExplorerSettings extends ConsumerWidget { (mode) => FilterChip( label: Text(mode.title), selected: prefs.playerDb.modes.contains(mode), - onSelected: (value) => ref + onSelected: (_) => ref .read(openingExplorerPreferencesProvider.notifier) .togglePlayerDbMode(mode), ), @@ -256,21 +214,23 @@ class OpeningExplorerSettings extends ConsumerWidget { .toList(growable: false), ), ), - DatePickerSettingsTile( - title: context.l10n.since, - value: prefs.playerDb.since, - firstDate: PlayerDbPrefState.earliestDate, - onChanged: (value) => ref - .read(openingExplorerPreferencesProvider.notifier) - .setPlayerDbSince(value), - ), - DatePickerSettingsTile( - title: context.l10n.until, - value: prefs.playerDb.until, - firstDate: PlayerDbPrefState.earliestDate, - onChanged: (value) => ref - .read(openingExplorerPreferencesProvider.notifier) - .setPlayerDbUntil(value), + PlatformListTile( + title: const Text('Timespan'), + subtitle: Wrap( + spacing: 5, + children: PlayerDbPrefState.datesMap.keys + .map( + (key) => ChoiceChip( + label: Text(key), + selected: + prefs.playerDb.since == PlayerDbPrefState.datesMap[key], + onSelected: (_) => ref + .read(openingExplorerPreferencesProvider.notifier) + .setPlayerDbSince(PlayerDbPrefState.datesMap[key]!), + ), + ) + .toList(growable: false), + ), ), ]; @@ -296,21 +256,21 @@ class OpeningExplorerSettings extends ConsumerWidget { ChoiceChip( label: const Text('Masters'), selected: prefs.db == OpeningDatabase.master, - onSelected: (value) => ref + onSelected: (_) => ref .read(openingExplorerPreferencesProvider.notifier) .setDatabase(OpeningDatabase.master), ), ChoiceChip( label: const Text('Lichess'), selected: prefs.db == OpeningDatabase.lichess, - onSelected: (value) => ref + onSelected: (_) => ref .read(openingExplorerPreferencesProvider.notifier) .setDatabase(OpeningDatabase.lichess), ), ChoiceChip( label: Text(context.l10n.player), selected: prefs.db == OpeningDatabase.player, - onSelected: (value) => ref + onSelected: (_) => ref .read(openingExplorerPreferencesProvider.notifier) .setDatabase(OpeningDatabase.player), ), diff --git a/lib/src/widgets/settings.dart b/lib/src/widgets/settings.dart index 969f4163ec..9e322568d7 100644 --- a/lib/src/widgets/settings.dart +++ b/lib/src/widgets/settings.dart @@ -324,63 +324,3 @@ class ChoicePicker extends StatelessWidget { } } } - -class DatePickerSettingsTile extends StatelessWidget { - const DatePickerSettingsTile({ - required this.title, - required this.value, - required this.firstDate, - required this.onChanged, - this.selectableDayPredicate, - }); - - final String title; - final DateTime value; - final DateTime firstDate; - final void Function(DateTime) onChanged; - final bool Function(DateTime)? selectableDayPredicate; - - @override - Widget build(BuildContext context) { - return PlatformListTile( - title: Text.rich( - TextSpan( - text: '$title: ', - style: const TextStyle( - fontWeight: FontWeight.normal, - ), - children: [ - TextSpan( - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - ), - text: value.toString().split(' ').first, - ), - WidgetSpan( - alignment: PlaceholderAlignment.baseline, - baseline: TextBaseline.ideographic, - child: IconButton( - icon: const Icon(Icons.date_range), - padding: EdgeInsets.zero, - onPressed: () => showDatePicker( - context: context, - initialDate: value, - firstDate: firstDate, - lastDate: DateTime.now(), - selectableDayPredicate: selectableDayPredicate, - ).then( - (value) { - if (value != null) { - onChanged(value); - } - }, - ), - ), - ), - ], - ), - ), - ); - } -} From 061fb36c540326bcfaaed5b9f4927e7dc54eb690 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Sat, 3 Aug 2024 11:40:48 +0200 Subject: [PATCH 102/979] feat: make name link to search screen instead of having search bar it took to much space --- .../opening_explorer_screen.dart | 2 +- .../opening_explorer_settings.dart | 58 +++++++------------ 2 files changed, 21 insertions(+), 39 deletions(-) diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index b1abe61dc0..d5bb84bc0b 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -223,7 +223,7 @@ class _OpeningExplorer extends ConsumerWidget { prefs.playerDb.usernameOrId == null) { return const Align( alignment: Alignment.center, - child: Text('Select a lichess player in the settings'), + child: Text('Select a Lichess player in the settings'), ); } diff --git a/lib/src/view/opening_explorer/opening_explorer_settings.dart b/lib/src/view/opening_explorer/opening_explorer_settings.dart index a903a2903a..6a43935922 100644 --- a/lib/src/view/opening_explorer/opening_explorer_settings.dart +++ b/lib/src/view/opening_explorer/opening_explorer_settings.dart @@ -1,5 +1,6 @@ import 'package:dartchess/dartchess.dart'; import 'package:flutter/cupertino.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; @@ -114,50 +115,31 @@ class OpeningExplorerSettings extends ConsumerWidget { ), children: [ TextSpan( + recognizer: TapGestureRecognizer() + ..onTap = () => pushPlatformRoute( + context, + fullscreenDialog: true, + builder: (_) => SearchScreen( + onUserTap: (user) => { + ref + .read( + openingExplorerPreferencesProvider.notifier, + ) + .setPlayerDbUsernameOrId(user.name), + Navigator.of(context).pop(), + }, + ), + ), style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 18, + decoration: TextDecoration.underline, ), - text: prefs.playerDb.usernameOrId, + text: prefs.playerDb.usernameOrId ?? 'Select a Lichess player', ), ], ), ), - subtitle: PlatformWidget( - androidBuilder: (context) => SearchBar( - leading: const Icon(Icons.search), - hintText: context.l10n.searchSearch, - focusNode: AlwaysDisabledFocusNode(), - onTap: () => pushPlatformRoute( - context, - fullscreenDialog: true, - builder: (_) => SearchScreen( - onUserTap: (user) => { - ref - .read(openingExplorerPreferencesProvider.notifier) - .setPlayerDbUsernameOrId(user.name), - Navigator.of(context).pop(), - }, - ), - ), - ), - iosBuilder: (context) => CupertinoSearchTextField( - placeholder: context.l10n.searchSearch, - focusNode: AlwaysDisabledFocusNode(), - onTap: () => pushPlatformRoute( - context, - fullscreenDialog: true, - builder: (_) => SearchScreen( - onUserTap: (user) => { - ref - .read(openingExplorerPreferencesProvider.notifier) - .setPlayerDbUsernameOrId(user.name), - Navigator.of(context).pop(), - }, - ), - ), - ), - ), ), PlatformListTile( title: Text(context.l10n.side), @@ -235,10 +217,10 @@ class OpeningExplorerSettings extends ConsumerWidget { ]; return DraggableScrollableSheet( - initialChildSize: .8, + initialChildSize: .85, expand: false, snap: true, - snapSizes: const [.8], + snapSizes: const [.85], builder: (context, scrollController) => ListView( controller: scrollController, children: [ From fb5a442f5baf1ea3bdfd7201db9513d1c75ea2f9 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Fri, 26 Jul 2024 14:45:58 +0200 Subject: [PATCH 103/979] feat: upgrade to chessground 4.0.0 --- lib/src/constants.dart | 8 +++ .../model/analysis/analysis_controller.dart | 6 +- lib/src/model/common/chess.dart | 4 +- lib/src/model/common/eval.dart | 6 +- lib/src/model/common/node.dart | 2 +- lib/src/model/common/uci.dart | 4 +- lib/src/model/game/game.dart | 2 +- lib/src/model/game/game_controller.dart | 7 +- lib/src/model/puzzle/puzzle.dart | 2 +- lib/src/model/puzzle/puzzle_controller.dart | 10 +-- lib/src/model/puzzle/storm_controller.dart | 11 ++- lib/src/model/settings/board_preferences.dart | 56 +++++++-------- lib/src/model/tv/tv_controller.dart | 2 +- lib/src/styles/lichess_colors.dart | 2 +- lib/src/utils/chessground_compat.dart | 41 ----------- lib/src/utils/color_palette.dart | 13 ++-- lib/src/view/analysis/analysis_screen.dart | 69 +++++++++---------- .../broadcast/broadcast_round_screen.dart | 12 ++-- .../offline_correspondence_game_screen.dart | 21 +++--- lib/src/view/game/archived_game_screen.dart | 20 +++--- lib/src/view/game/game_body.dart | 26 ++++--- lib/src/view/game/game_list_tile.dart | 5 +- lib/src/view/game/game_loading_board.dart | 35 ++++------ .../offline_correspondence_games_screen.dart | 5 +- lib/src/view/home/home_tab_screen.dart | 5 +- lib/src/view/play/challenge_screen.dart | 5 +- lib/src/view/play/ongoing_games_screen.dart | 5 +- .../view/puzzle/puzzle_feedback_widget.dart | 5 +- .../view/puzzle/puzzle_history_screen.dart | 10 ++- lib/src/view/puzzle/puzzle_screen.dart | 49 +++++-------- lib/src/view/puzzle/puzzle_tab_screen.dart | 11 ++- lib/src/view/puzzle/storm_screen.dart | 27 +++----- lib/src/view/puzzle/streak_screen.dart | 27 +++----- lib/src/view/settings/piece_set_screen.dart | 13 ++-- lib/src/view/settings/theme_screen.dart | 7 +- .../view/watch/live_tv_channels_screen.dart | 5 +- lib/src/view/watch/tv_screen.dart | 27 +++----- lib/src/widgets/board_carousel_item.dart | 9 +-- lib/src/widgets/board_preview.dart | 9 +-- lib/src/widgets/board_table.dart | 13 ++-- lib/src/widgets/board_thumbnail.dart | 12 ++-- pubspec.lock | 8 +-- pubspec.yaml | 4 +- test/model/common/node_test.dart | 38 +++++----- test/model/common/uci_test.dart | 16 ++--- test/test_utils.dart | 29 +++++--- test/view/analysis/analysis_screen_test.dart | 6 +- test/view/game/archived_game_screen_test.dart | 17 +++-- test/view/puzzle/puzzle_screen_test.dart | 50 +++++++------- test/view/puzzle/storm_screen_test.dart | 35 +++++----- 50 files changed, 368 insertions(+), 443 deletions(-) delete mode 100644 lib/src/utils/chessground_compat.dart diff --git a/lib/src/constants.dart b/lib/src/constants.dart index c363b4ab46..b24838d7f5 100644 --- a/lib/src/constants.dart +++ b/lib/src/constants.dart @@ -1,3 +1,5 @@ +import 'package:chessground/chessground.dart'; +import 'package:dartchess/dartchess.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -53,6 +55,12 @@ const kTabletBoardTableSidePadding = 16.0; const kBottomBarHeight = 56.0; const kMaterialPopupMenuMaxWidth = 500.0; +const ChessboardState kEmptyBoardState = ChessboardState( + fen: kEmptyFen, + interactableSide: InteractableSide.none, + orientation: Side.white, +); + /// The threshold to detect screens with a small remaining height left board. const kSmallRemainingHeightLeftBoardThreshold = 160; diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index 66957d6bd3..9a1498454a 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -538,7 +538,7 @@ class AnalysisController extends _$AnalysisController { } else { final uci = n2child['uci'] as String; final san = n2child['san'] as String; - final move = Move.fromUci(uci)!; + final move = Move.parse(uci)!; n1.addChild( Branch( position: n1.position.playUnchecked(move), @@ -656,8 +656,8 @@ class AnalysisState with _$AnalysisState { IList? pgnRootComments, }) = _AnalysisState; - IMap> get validMoves => - algebraicLegalMoves(currentNode.position); + IMap> get validMoves => + makeLegalMoves(currentNode.position); /// Whether the user can request server analysis. /// diff --git a/lib/src/model/common/chess.dart b/lib/src/model/common/chess.dart index a327310f1c..51e838c7b4 100644 --- a/lib/src/model/common/chess.dart +++ b/lib/src/model/common/chess.dart @@ -32,7 +32,7 @@ class MoveConverter implements JsonConverter { // assume we are serializing only valid uci strings @override - Move fromJson(String json) => Move.fromUci(json)!; + Move fromJson(String json) => Move.parse(json)!; @override String toJson(Move object) => object.uci; @@ -208,7 +208,7 @@ extension ChessExtension on Pick { return value; } if (value is String) { - final move = Move.fromUci(value); + final move = Move.parse(value); if (move != null) { return move; } else { diff --git a/lib/src/model/common/eval.dart b/lib/src/model/common/eval.dart index 429e24cd99..3835f67a60 100644 --- a/lib/src/model/common/eval.dart +++ b/lib/src/model/common/eval.dart @@ -79,7 +79,7 @@ class ClientEval with _$ClientEval implements Eval { Move? get bestMove { final uci = pvs.firstOrNull?.moves.firstOrNull; if (uci == null) return null; - return Move.fromUci(uci); + return Move.parse(uci); } IList get bestMoves { @@ -132,7 +132,7 @@ class PvData with _$PvData { final List res = []; for (final uciMove in moves.sublist(0, math.min(12, moves.length))) { // assume uciMove string is valid as it comes from stockfish - final move = Move.fromUci(uciMove)!; + final move = Move.parse(uciMove)!; if (pos.isLegal(move)) { final (newPos, san) = pos.makeSanUnchecked(move); res.add(san); @@ -145,7 +145,7 @@ class PvData with _$PvData { } MoveWithWinningChances? _firstMoveWithWinningChances(Side sideToMove) { - final uciMove = (moves.isNotEmpty) ? Move.fromUci(moves.first) : null; + final uciMove = (moves.isNotEmpty) ? Move.parse(moves.first) : null; return (uciMove != null) ? ( move: uciMove, diff --git a/lib/src/model/common/node.dart b/lib/src/model/common/node.dart index 8916702bac..78bdbb96d4 100644 --- a/lib/src/model/common/node.dart +++ b/lib/src/model/common/node.dart @@ -208,7 +208,7 @@ abstract class Node { /// castling move and converts it to the corresponding standard castling move if so. Move? convertAltCastlingMove(Move move) { return altCastles.containsValue(move.uci) - ? Move.fromUci( + ? Move.parse( altCastles.entries.firstWhere((e) => e.value == move.uci).key, ) : move; diff --git a/lib/src/model/common/uci.dart b/lib/src/model/common/uci.dart index 8dadf811e7..e747bf960b 100644 --- a/lib/src/model/common/uci.dart +++ b/lib/src/model/common/uci.dart @@ -24,7 +24,7 @@ class UciCharPair with _$UciCharPair { /// /// Throws an [ArgumentError] if the move is invalid. factory UciCharPair.fromUci(String uci) { - final move = Move.fromUci(uci); + final move = Move.parse(uci); if (move == null) { throw ArgumentError('Invalid uci $uci'); } @@ -37,7 +37,7 @@ class UciCharPair with _$UciCharPair { String.fromCharCode(35 + f), String.fromCharCode( p != null - ? 35 + 64 + 8 * _promotionRoles.indexOf(p) + squareFile(t) + ? 35 + 64 + 8 * _promotionRoles.indexOf(p) + t.file.value : 35 + t, ), ), diff --git a/lib/src/model/game/game.dart b/lib/src/model/game/game.dart index 3f0d78458a..82f6331006 100644 --- a/lib/src/model/game/game.dart +++ b/lib/src/model/game/game.dart @@ -384,7 +384,7 @@ IList stepsFromJson(String json) { if (uci == null || san == null) { break; } - final move = Move.fromUci(uci)!; + final move = Move.parse(uci)!; position = position.playUnchecked(move); steps.add( GameStep( diff --git a/lib/src/model/game/game_controller.dart b/lib/src/model/game/game_controller.dart index bb1f543e5b..a53577fa3a 100644 --- a/lib/src/model/game/game_controller.dart +++ b/lib/src/model/game/game_controller.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:async/async.dart'; -import 'package:chessground/chessground.dart' as cg; import 'package:collection/collection.dart'; import 'package:dartchess/dartchess.dart'; import 'package:deep_pick/deep_pick.dart'; @@ -196,7 +195,7 @@ class GameController extends _$GameController { } /// Set or unset a premove. - void setPremove(cg.Move? move) { + void setPremove(Move? move) { final curState = state.requireValue; state = AsyncValue.data( curState.copyWith( @@ -530,7 +529,7 @@ class GameController extends _$GameController { // add opponent move if (data.ply == curState.game.lastPly + 1) { final lastPos = curState.game.lastPosition; - final move = Move.fromUci(data.uci)!; + final move = Move.parse(data.uci)!; final sanMove = SanMove(data.san, move); final newPos = lastPos.playUnchecked(move); final newStep = GameStep( @@ -924,7 +923,7 @@ class GameState with _$GameState { int? lastDrawOfferAtPly, Duration? opponentLeftCountdown, required bool stopClockWaitingForServerAck, - cg.Move? premove, + Move? premove, /// Game only setting to override the account preference bool? moveConfirmSettingOverride, diff --git a/lib/src/model/puzzle/puzzle.dart b/lib/src/model/puzzle/puzzle.dart index 4ca49d5e51..4fa9aafaf3 100644 --- a/lib/src/model/puzzle/puzzle.dart +++ b/lib/src/model/puzzle/puzzle.dart @@ -155,7 +155,7 @@ class LitePuzzle with _$LitePuzzle { (Side, String, Move) get preview { final pos1 = Chess.fromSetup(Setup.parseFen(fen)); - final move = Move.fromUci(solution.first); + final move = Move.parse(solution.first); final pos = pos1.play(move!); return (pos.turn, pos.fen, move); } diff --git a/lib/src/model/puzzle/puzzle_controller.dart b/lib/src/model/puzzle/puzzle_controller.dart index bc98fa010b..fd0c00700e 100644 --- a/lib/src/model/puzzle/puzzle_controller.dart +++ b/lib/src/model/puzzle/puzzle_controller.dart @@ -138,7 +138,7 @@ class PuzzleController extends _$PuzzleController { // another puzzle move: let's continue else if (nextUci != null) { await Future.delayed(const Duration(milliseconds: 500)); - _addMove(Move.fromUci(nextUci)!); + _addMove(Move.parse(nextUci)!); } // no more puzzle move: it's a win else { @@ -205,7 +205,7 @@ class PuzzleController extends _$PuzzleController { state = state.copyWith.streak!(hasSkipped: true); final moveIndex = state.currentPath.size - state.initialPath.size; final solution = state.puzzle.puzzle.solution[moveIndex]; - onUserMove(Move.fromUci(solution)!); + onUserMove(Move.parse(solution)!); } } @@ -475,7 +475,7 @@ class PuzzleController extends _$PuzzleController { var currentPosition = initPosition; final pgnMoves = state.puzzle.puzzle.solution.fold>([], (List acc, move) { - final moveObj = Move.fromUci(move); + final moveObj = Move.parse(move); if (moveObj != null) { final String san; (currentPosition, san) = currentPosition.makeSan(moveObj); @@ -524,7 +524,7 @@ class PuzzleController extends _$PuzzleController { final (_, newNodes) = state.puzzle.puzzle.solution.foldIndexed( (initialNode.position, IList(const [])), (index, previous, uci) { - final move = Move.fromUci(uci); + final move = Move.parse(uci); final (pos, nodes) = previous; final (newPos, newSan) = pos.makeSan(move!); return ( @@ -592,5 +592,5 @@ class PuzzleState with _$PuzzleState { bool get canGoBack => mode == PuzzleMode.view && currentPath.size > initialPath.size; - IMap> get validMoves => algebraicLegalMoves(position); + IMap> get validMoves => makeLegalMoves(position); } diff --git a/lib/src/model/puzzle/storm_controller.dart b/lib/src/model/puzzle/storm_controller.dart index 634e203ea8..ed9cd2eb58 100644 --- a/lib/src/model/puzzle/storm_controller.dart +++ b/lib/src/model/puzzle/storm_controller.dart @@ -3,7 +3,6 @@ import 'dart:core'; import 'dart:math' as math; import 'package:async/async.dart'; -import 'package:chessground/chessground.dart' as cg; import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/services.dart'; @@ -119,7 +118,7 @@ class StormController extends _$StormController { } } - void setPremove(cg.Move? move) { + void setPremove(Move? move) { state = state.copyWith( premove: move, ); @@ -335,17 +334,17 @@ class StormState with _$StormState { required bool firstMovePlayed, /// premove to be played - cg.Move? premove, + Move? premove, }) = _StormState; - Move? get expectedMove => Move.fromUci(puzzle.solution[moveIndex + 1]); + Move? get expectedMove => Move.parse(puzzle.solution[moveIndex + 1]); Move? get lastMove => - moveIndex == -1 ? null : Move.fromUci(puzzle.solution[moveIndex]); + moveIndex == -1 ? null : Move.parse(puzzle.solution[moveIndex]); bool get isOver => moveIndex >= puzzle.solution.length - 1; - IMap> get validMoves => algebraicLegalMoves(position); + IMap> get validMoves => makeLegalMoves(position); } enum StormMode { initial, running, ended } diff --git a/lib/src/model/settings/board_preferences.dart b/lib/src/model/settings/board_preferences.dart index 6063b40030..c1bb3bf4d3 100644 --- a/lib/src/model/settings/board_preferences.dart +++ b/lib/src/model/settings/board_preferences.dart @@ -135,15 +135,15 @@ class BoardPrefs with _$BoardPrefs { magnifyDraggedPiece: true, ); - BoardSettings toBoardSettings() { - return BoardSettings( + ChessboardSettings toBoardSettings() { + return ChessboardSettings( pieceAssets: pieceSet.assets, colorScheme: boardTheme.colors, showValidMoves: showLegalMoves, showLastMove: boardHighlights, enableCoordinates: coordinates, animationDuration: pieceAnimationDuration, - dragFeedbackSize: magnifyDraggedPiece ? 2.0 : 1.0, + dragFeedbackScale: magnifyDraggedPiece ? 2.0 : 1.0, dragFeedbackOffset: Offset(0.0, magnifyDraggedPiece ? -1.0 : 0.0), pieceShiftMethod: pieceShiftMethod, drawShape: DrawShapeOptions( @@ -195,56 +195,56 @@ enum BoardTheme { const BoardTheme(this.label); - BoardColorScheme get colors { + ChessboardColorScheme get colors { switch (this) { case BoardTheme.system: - return getBoardColorScheme() ?? BoardColorScheme.brown; + return getBoardColorScheme() ?? ChessboardColorScheme.brown; case BoardTheme.blue: - return BoardColorScheme.blue; + return ChessboardColorScheme.blue; case BoardTheme.blue2: - return BoardColorScheme.blue2; + return ChessboardColorScheme.blue2; case BoardTheme.blue3: - return BoardColorScheme.blue3; + return ChessboardColorScheme.blue3; case BoardTheme.blueMarble: - return BoardColorScheme.blueMarble; + return ChessboardColorScheme.blueMarble; case BoardTheme.canvas: - return BoardColorScheme.canvas; + return ChessboardColorScheme.canvas; case BoardTheme.wood: - return BoardColorScheme.wood; + return ChessboardColorScheme.wood; case BoardTheme.wood2: - return BoardColorScheme.wood2; + return ChessboardColorScheme.wood2; case BoardTheme.wood3: - return BoardColorScheme.wood3; + return ChessboardColorScheme.wood3; case BoardTheme.wood4: - return BoardColorScheme.wood4; + return ChessboardColorScheme.wood4; case BoardTheme.maple: - return BoardColorScheme.maple; + return ChessboardColorScheme.maple; case BoardTheme.maple2: - return BoardColorScheme.maple2; + return ChessboardColorScheme.maple2; case BoardTheme.brown: - return BoardColorScheme.brown; + return ChessboardColorScheme.brown; case BoardTheme.leather: - return BoardColorScheme.leather; + return ChessboardColorScheme.leather; case BoardTheme.green: - return BoardColorScheme.green; + return ChessboardColorScheme.green; case BoardTheme.marble: - return BoardColorScheme.marble; + return ChessboardColorScheme.marble; case BoardTheme.greenPlastic: - return BoardColorScheme.greenPlastic; + return ChessboardColorScheme.greenPlastic; case BoardTheme.grey: - return BoardColorScheme.grey; + return ChessboardColorScheme.grey; case BoardTheme.metal: - return BoardColorScheme.metal; + return ChessboardColorScheme.metal; case BoardTheme.olive: - return BoardColorScheme.olive; + return ChessboardColorScheme.olive; case BoardTheme.newspaper: - return BoardColorScheme.newspaper; + return ChessboardColorScheme.newspaper; case BoardTheme.purpleDiag: - return BoardColorScheme.purpleDiag; + return ChessboardColorScheme.purpleDiag; case BoardTheme.pinkPyramid: - return BoardColorScheme.pinkPyramid; + return ChessboardColorScheme.pinkPyramid; case BoardTheme.horsey: - return BoardColorScheme.horsey; + return ChessboardColorScheme.horsey; } } diff --git a/lib/src/model/tv/tv_controller.dart b/lib/src/model/tv/tv_controller.dart index 3fda475b7c..c6d97744c6 100644 --- a/lib/src/model/tv/tv_controller.dart +++ b/lib/src/model/tv/tv_controller.dart @@ -192,7 +192,7 @@ class TvController extends _$TvController { final curState = state.requireValue; final data = MoveEvent.fromJson(event.data as Map); final lastPos = curState.game.lastPosition; - final move = Move.fromUci(data.uci)!; + final move = Move.parse(data.uci)!; final sanMove = SanMove(data.san, move); final newPos = lastPos.playUnchecked(move); final newStep = GameStep( diff --git a/lib/src/styles/lichess_colors.dart b/lib/src/styles/lichess_colors.dart index dd9cbabc68..d082947f4c 100644 --- a/lib/src/styles/lichess_colors.dart +++ b/lib/src/styles/lichess_colors.dart @@ -6,7 +6,7 @@ class LichessColors { LichessColors._(); // material colors palette generated with: - // http://mcg.mbitson.com + // http://mmbitson.com // primary: blue static const MaterialColor primary = diff --git a/lib/src/utils/chessground_compat.dart b/lib/src/utils/chessground_compat.dart deleted file mode 100644 index a00873c05e..0000000000 --- a/lib/src/utils/chessground_compat.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:chessground/chessground.dart' as chessground; -import 'package:dartchess/dartchess.dart'; - -extension ChessgroundSideCompat on Side { - chessground.Side get cg => - this == Side.white ? chessground.Side.white : chessground.Side.black; -} - -extension ChessgroundMoveCompat on Move { - chessground.Move get cg { - // !! Chessground doesn't support drop moves yet - if (this is DropMove) { - return chessground.Move(from: toAlgebraic(to), to: toAlgebraic(to)); - } - - return chessground.Move( - from: toAlgebraic((this as NormalMove).from), - to: toAlgebraic(to), - promotion: (this as NormalMove).promotion?.cg, - ); - } -} - -extension ChessgroundRoleCompat on Role { - chessground.Role get cg { - switch (this) { - case Role.pawn: - return chessground.Role.pawn; - case Role.knight: - return chessground.Role.knight; - case Role.bishop: - return chessground.Role.bishop; - case Role.rook: - return chessground.Role.rook; - case Role.king: - return chessground.Role.king; - case Role.queen: - return chessground.Role.queen; - } - } -} diff --git a/lib/src/utils/color_palette.dart b/lib/src/utils/color_palette.dart index 51b272b952..9d0e53e7ab 100644 --- a/lib/src/utils/color_palette.dart +++ b/lib/src/utils/color_palette.dart @@ -1,11 +1,12 @@ import 'dart:ui'; import 'package:chessground/chessground.dart'; +import 'package:dartchess/dartchess.dart'; import 'package:material_color_utilities/material_color_utilities.dart'; CorePalette? _corePalette; -BoardColorScheme? _boardColorScheme; +ChessboardColorScheme? _boardColorScheme; /// Set the system core palette if available (android 12+ only). /// @@ -17,19 +18,19 @@ void setCorePalette(CorePalette? palette) { final darkSquare = Color(palette.secondary.get(60)); final lightSquare = Color(palette.primary.get(95)); - _boardColorScheme = BoardColorScheme( + _boardColorScheme = ChessboardColorScheme( darkSquare: darkSquare, lightSquare: lightSquare, - background: SolidColorBackground( + background: SolidColorChessboardBackground( lightSquare: lightSquare, darkSquare: darkSquare, ), - whiteCoordBackground: SolidColorBackground( + whiteCoordBackground: SolidColorChessboardBackground( lightSquare: lightSquare, darkSquare: darkSquare, coordinates: true, ), - blackCoordBackground: SolidColorBackground( + blackCoordBackground: SolidColorChessboardBackground( lightSquare: lightSquare, darkSquare: darkSquare, coordinates: true, @@ -53,6 +54,6 @@ CorePalette? getCorePalette() { } /// Get the board colors based on the core palette, if available (android 12+). -BoardColorScheme? getBoardColorScheme() { +ChessboardColorScheme? getBoardColorScheme() { return _boardColorScheme; } diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index 1ded41d6ad..e9a3713689 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -1,7 +1,7 @@ import 'dart:math' as math; import 'dart:ui'; -import 'package:chessground/chessground.dart' as cg; +import 'package:chessground/chessground.dart'; import 'package:collection/collection.dart'; import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; @@ -27,7 +27,6 @@ import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/model/settings/brightness.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/chessground_compat.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; @@ -377,9 +376,9 @@ class _Board extends ConsumerStatefulWidget { } class _BoardState extends ConsumerState<_Board> { - ISet userShapes = ISet(); + ISet userShapes = ISet(); - ISet _computeBestMoveShapes(IList moves) { + ISet _computeBestMoveShapes(IList moves) { // Scale down all moves with index > 0 based on how much worse their winning chances are compared to the best move // (assume moves are ordered by their winning chances, so index==0 is the best move) double scaleArrowAgainstBestMove(int index) { @@ -414,25 +413,25 @@ class _BoardState extends ConsumerState<_Board> { switch (move) { case NormalMove(from: _, to: _, promotion: final promRole): return [ - cg.Arrow( + Arrow( color: color, - orig: move.cg.from, - dest: move.cg.to, + orig: move.from, + dest: move.to, scale: scaleArrowAgainstBestMove(i), ), if (promRole != null) - cg.PieceShape( + PieceShape( color: color, - orig: move.cg.to, - role: promRole.cg, + orig: move.to, + role: promRole, ), ]; case DropMove(role: final role, to: _): return [ - cg.PieceShape( + PieceShape( color: color, - orig: move.cg.to, - role: role.cg, + orig: move.to, + role: role, ), ]; } @@ -466,45 +465,45 @@ class _BoardState extends ConsumerState<_Board> { final sanMove = currentNode.sanMove; - final ISet bestMoveShapes = showBestMoveArrow && + final ISet bestMoveShapes = showBestMoveArrow && analysisState.isEngineAvailable && bestMoves != null ? _computeBestMoveShapes(bestMoves) : ISet(); - return cg.Board( + return Chessboard( size: widget.boardSize, onMove: (move, {isDrop, isPremove}) => - ref.read(ctrlProvider.notifier).onUserMove(Move.fromUci(move.uci)!), - data: cg.BoardData( - orientation: analysisState.pov.cg, + ref.read(ctrlProvider.notifier).onUserMove(Move.parse(move.uci)!), + state: ChessboardState( + orientation: analysisState.pov, interactableSide: analysisState.position.isGameOver - ? cg.InteractableSide.none + ? InteractableSide.none : analysisState.position.turn == Side.white - ? cg.InteractableSide.white - : cg.InteractableSide.black, + ? InteractableSide.white + : InteractableSide.black, fen: analysisState.position.fen, isCheck: boardPrefs.boardHighlights && analysisState.position.isCheck, - lastMove: analysisState.lastMove?.cg, - sideToMove: analysisState.position.turn.cg, + lastMove: analysisState.lastMove as NormalMove?, + sideToMove: analysisState.position.turn, validMoves: analysisState.validMoves, shapes: userShapes.union(bestMoveShapes), - annotations: - showAnnotationsOnBoard && sanMove != null && annotation != null - ? altCastles.containsKey(sanMove.move.uci) - ? IMap({ - Move.fromUci(altCastles[sanMove.move.uci]!)!.cg.to: - annotation, - }) - : IMap({sanMove.move.cg.to: annotation}) - : null, + annotations: showAnnotationsOnBoard && + sanMove != null && + annotation != null + ? altCastles.containsKey(sanMove.move.uci) + ? IMap({ + Move.parse(altCastles[sanMove.move.uci]!)!.to: annotation, + }) + : IMap({sanMove.move.to: annotation}) + : null, ), settings: boardPrefs.toBoardSettings().copyWith( borderRadius: widget.isTablet ? const BorderRadius.all(Radius.circular(4.0)) : BorderRadius.zero, boxShadow: widget.isTablet ? boardShadows : const [], - drawShape: cg.DrawShapeOptions( + drawShape: DrawShapeOptions( enable: true, onCompleteShape: _onCompleteShape, onClearShapes: _onClearShapes, @@ -513,7 +512,7 @@ class _BoardState extends ConsumerState<_Board> { ); } - void _onCompleteShape(cg.Shape shape) { + void _onCompleteShape(Shape shape) { if (userShapes.any((element) => element == shape)) { setState(() { userShapes = userShapes.remove(shape); @@ -687,7 +686,7 @@ class _Engineline extends ConsumerWidget { return AdaptiveInkWell( onTap: () => ref .read(ctrlProvider.notifier) - .onUserMove(Move.fromUci(pvData.moves[0])!), + .onUserMove(Move.parse(pvData.moves[0])!), child: SizedBox( height: kEvalGaugeSize, child: Padding( diff --git a/lib/src/view/broadcast/broadcast_round_screen.dart b/lib/src/view/broadcast/broadcast_round_screen.dart index 8d1839a8d8..43e9357e2e 100644 --- a/lib/src/view/broadcast/broadcast_round_screen.dart +++ b/lib/src/view/broadcast/broadcast_round_screen.dart @@ -1,7 +1,6 @@ import 'dart:async'; -import 'package:chessground/chessground.dart'; -import 'package:dartchess/dartchess.dart' as dartchess; +import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -12,7 +11,6 @@ import 'package:lichess_mobile/src/model/broadcast/broadcast_round_controller.da import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/chessground_compat.dart'; import 'package:lichess_mobile/src/utils/duration.dart'; import 'package:lichess_mobile/src/utils/lichess_assets.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; @@ -136,16 +134,16 @@ class BroadcastPreview extends StatelessWidget { } final game = games![index]; - final playingSide = dartchess.Setup.parseFen(game.fen).turn.cg; + final playingSide = Setup.parseFen(game.fen).turn; return BoardThumbnail( orientation: Side.white, fen: game.fen, - lastMove: game.lastMove?.cg, + lastMove: game.lastMove, size: boardWidth, header: _PlayerWidget( width: boardWidth, - player: game.players[dartchess.Side.black]!, + player: game.players[Side.black]!, gameStatus: game.status, thinkTime: game.thinkTime, side: Side.black, @@ -153,7 +151,7 @@ class BroadcastPreview extends StatelessWidget { ), footer: _PlayerWidget( width: boardWidth, - player: game.players[dartchess.Side.white]!, + player: game.players[Side.white]!, gameStatus: game.status, thinkTime: game.thinkTime, side: Side.white, diff --git a/lib/src/view/correspondence/offline_correspondence_game_screen.dart b/lib/src/view/correspondence/offline_correspondence_game_screen.dart index 85da1930ff..426ff94aac 100644 --- a/lib/src/view/correspondence/offline_correspondence_game_screen.dart +++ b/lib/src/view/correspondence/offline_correspondence_game_screen.dart @@ -1,4 +1,4 @@ -import 'package:chessground/chessground.dart' as cg; +import 'package:chessground/chessground.dart'; import 'package:collection/collection.dart'; import 'package:dartchess/dartchess.dart'; import 'package:flutter/cupertino.dart'; @@ -15,7 +15,6 @@ import 'package:lichess_mobile/src/model/game/game.dart'; import 'package:lichess_mobile/src/model/game/game_status.dart'; import 'package:lichess_mobile/src/model/game/material_diff.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; -import 'package:lichess_mobile/src/utils/chessground_compat.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; @@ -230,20 +229,20 @@ class _BodyState extends ConsumerState<_Body> { bottom: false, child: BoardTable( onMove: (move, {isDrop, isPremove}) { - onUserMove(Move.fromUci(move.uci)!); + onUserMove(Move.parse(move.uci)!); }, - boardData: cg.BoardData( + boardState: ChessboardState( interactableSide: game.playable && !isReplaying ? youAre == Side.white - ? cg.InteractableSide.white - : cg.InteractableSide.black - : cg.InteractableSide.none, - orientation: isBoardTurned ? youAre.opposite.cg : youAre.cg, + ? InteractableSide.white + : InteractableSide.black + : InteractableSide.none, + orientation: isBoardTurned ? youAre.opposite : youAre, fen: position.fen, - lastMove: game.moveAt(stepCursor)?.cg, + lastMove: game.moveAt(stepCursor) as NormalMove?, isCheck: position.isCheck, - sideToMove: sideToMove.cg, - validMoves: algebraicLegalMoves(position), + sideToMove: sideToMove, + validMoves: makeLegalMoves(position), ), topTable: topPlayer, bottomTable: bottomPlayer, diff --git a/lib/src/view/game/archived_game_screen.dart b/lib/src/view/game/archived_game_screen.dart index 5b12811fa7..a6493d7d5c 100644 --- a/lib/src/view/game/archived_game_screen.dart +++ b/lib/src/view/game/archived_game_screen.dart @@ -1,4 +1,4 @@ -import 'package:chessground/chessground.dart' as cg; +import 'package:chessground/chessground.dart'; import 'package:dartchess/dartchess.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -7,7 +7,6 @@ import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/game/archived_game.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/chessground_compat.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; @@ -153,9 +152,9 @@ class _BoardBody extends ConsumerWidget { final topPlayer = orientation == Side.white ? black : white; final bottomPlayer = orientation == Side.white ? white : black; final loadingBoard = BoardTable( - boardData: cg.BoardData( - interactableSide: cg.InteractableSide.none, - orientation: (isBoardTurned ? orientation.opposite : orientation).cg, + boardState: ChessboardState( + interactableSide: InteractableSide.none, + orientation: (isBoardTurned ? orientation.opposite : orientation), fen: gameData.lastFen ?? kInitialBoardFEN, ), topTable: topPlayer, @@ -196,13 +195,12 @@ class _BoardBody extends ConsumerWidget { final position = game.positionAt(cursor); return BoardTable( - boardData: cg.BoardData( - interactableSide: cg.InteractableSide.none, - orientation: - (isBoardTurned ? orientation.opposite : orientation).cg, + boardState: ChessboardState( + interactableSide: InteractableSide.none, + orientation: (isBoardTurned ? orientation.opposite : orientation), fen: position.fen, - lastMove: game.moveAt(cursor)?.cg, - sideToMove: position.turn.cg, + lastMove: game.moveAt(cursor) as NormalMove?, + sideToMove: position.turn, isCheck: position.isCheck, ), topTable: topPlayer, diff --git a/lib/src/view/game/game_body.dart b/lib/src/view/game/game_body.dart index 5bc42a50d8..322c7e0528 100644 --- a/lib/src/view/game/game_body.dart +++ b/lib/src/view/game/game_body.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'dart:math' as math; -import 'package:chessground/chessground.dart' as cg; +import 'package:chessground/chessground.dart'; import 'package:collection/collection.dart'; import 'package:dartchess/dartchess.dart'; import 'package:flutter/cupertino.dart'; @@ -17,7 +17,6 @@ import 'package:lichess_mobile/src/model/game/game_controller.dart'; import 'package:lichess_mobile/src/model/game/game_preferences.dart'; import 'package:lichess_mobile/src/model/game/playable_game.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; -import 'package:lichess_mobile/src/utils/chessground_compat.dart'; import 'package:lichess_mobile/src/utils/gestures_exclusion.dart'; import 'package:lichess_mobile/src/utils/immersive_mode.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; @@ -242,7 +241,7 @@ class GameBody extends ConsumerWidget { ), onMove: (move, {isDrop, isPremove}) { ref.read(ctrlProvider.notifier).onUserMove( - Move.fromUci(move.uci)!, + Move.parse(move.uci)!, isPremove: isPremove, isDrop: isDrop, ); @@ -252,23 +251,22 @@ class GameBody extends ConsumerWidget { ref.read(ctrlProvider.notifier).setPremove(move); } : null, - boardData: cg.BoardData( + boardState: ChessboardState( interactableSide: gameState.game.playable && !gameState.isReplaying ? youAre == Side.white - ? cg.InteractableSide.white - : cg.InteractableSide.black - : cg.InteractableSide.none, - orientation: - isBoardTurned ? youAre.opposite.cg : youAre.cg, + ? InteractableSide.white + : InteractableSide.black + : InteractableSide.none, + orientation: isBoardTurned ? youAre.opposite : youAre, fen: position.fen, - lastMove: - gameState.game.moveAt(gameState.stepCursor)?.cg, + lastMove: gameState.game.moveAt(gameState.stepCursor) + as NormalMove?, isCheck: boardPreferences.boardHighlights && position.isCheck, - sideToMove: position.turn.cg, - validMoves: algebraicLegalMoves(position), - premove: gameState.premove, + sideToMove: position.turn, + validMoves: makeLegalMoves(position), + premove: gameState.premove as NormalMove?, ), topTable: topPlayer, bottomTable: gameState.canShowClaimWinCountdown && diff --git a/lib/src/view/game/game_list_tile.dart b/lib/src/view/game/game_list_tile.dart index 1cedf12865..5aaabe2997 100644 --- a/lib/src/view/game/game_list_tile.dart +++ b/lib/src/view/game/game_list_tile.dart @@ -11,7 +11,6 @@ import 'package:lichess_mobile/src/model/game/game_share_service.dart'; import 'package:lichess_mobile/src/model/game/game_status.dart'; import 'package:lichess_mobile/src/styles/lichess_colors.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/chessground_compat.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/share.dart'; @@ -152,8 +151,8 @@ class _ContextMenu extends ConsumerWidget { size: constraints.maxWidth - (constraints.maxWidth / 1.618), fen: game.lastFen!, - orientation: mySide.cg, - lastMove: game.lastMove?.cg, + orientation: mySide, + lastMove: game.lastMove, ), Expanded( child: Padding( diff --git a/lib/src/view/game/game_loading_board.dart b/lib/src/view/game/game_loading_board.dart index 52145a83d4..dabe062ec5 100644 --- a/lib/src/view/game/game_loading_board.dart +++ b/lib/src/view/game/game_loading_board.dart @@ -1,4 +1,4 @@ -import 'package:chessground/chessground.dart' as cg; +import 'package:chessground/chessground.dart'; import 'package:dartchess/dartchess.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -8,7 +8,6 @@ import 'package:lichess_mobile/src/model/challenge/challenge.dart'; import 'package:lichess_mobile/src/model/lobby/game_seek.dart'; import 'package:lichess_mobile/src/model/lobby/lobby_numbers.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; -import 'package:lichess_mobile/src/utils/chessground_compat.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/widgets/board_table.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; @@ -29,11 +28,7 @@ class LobbyScreenLoadingContent extends StatelessWidget { child: SafeArea( bottom: false, child: BoardTable( - boardData: const cg.BoardData( - interactableSide: cg.InteractableSide.none, - orientation: cg.Side.white, - fen: kEmptyFen, - ), + boardState: kEmptyBoardState, topTable: const SizedBox.shrink(), bottomTable: const SizedBox.shrink(), showMoveListPlaceholder: true, @@ -112,11 +107,7 @@ class ChallengeLoadingContent extends StatelessWidget { child: SafeArea( bottom: false, child: BoardTable( - boardData: const cg.BoardData( - interactableSide: cg.InteractableSide.none, - orientation: cg.Side.white, - fen: kEmptyFen, - ), + boardState: kEmptyBoardState, topTable: const SizedBox.shrink(), bottomTable: const SizedBox.shrink(), showMoveListPlaceholder: true, @@ -192,11 +183,11 @@ class StandaloneGameLoadingBoard extends StatelessWidget { @override Widget build(BuildContext context) { return BoardTable( - boardData: cg.BoardData( - interactableSide: cg.InteractableSide.none, - orientation: orientation?.cg ?? cg.Side.white, + boardState: ChessboardState( + interactableSide: InteractableSide.none, + orientation: orientation ?? Side.white, fen: fen ?? kEmptyFen, - lastMove: lastMove?.cg, + lastMove: lastMove as NormalMove?, ), topTable: const SizedBox.shrink(), bottomTable: const SizedBox.shrink(), @@ -218,9 +209,9 @@ class LoadGameError extends StatelessWidget { child: SafeArea( bottom: false, child: BoardTable( - boardData: const cg.BoardData( - interactableSide: cg.InteractableSide.none, - orientation: cg.Side.white, + boardState: const ChessboardState( + interactableSide: InteractableSide.none, + orientation: Side.white, fen: kEmptyFen, ), topTable: const SizedBox.shrink(), @@ -263,9 +254,9 @@ class ChallengeDeclinedBoard extends StatelessWidget { child: SafeArea( bottom: false, child: BoardTable( - boardData: const cg.BoardData( - interactableSide: cg.InteractableSide.none, - orientation: cg.Side.white, + boardState: const ChessboardState( + interactableSide: InteractableSide.none, + orientation: Side.white, fen: kEmptyFen, ), topTable: const SizedBox.shrink(), diff --git a/lib/src/view/game/offline_correspondence_games_screen.dart b/lib/src/view/game/offline_correspondence_games_screen.dart index 5e6cc6198f..f8fae92671 100644 --- a/lib/src/view/game/offline_correspondence_games_screen.dart +++ b/lib/src/view/game/offline_correspondence_games_screen.dart @@ -4,7 +4,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/correspondence/correspondence_game_storage.dart'; import 'package:lichess_mobile/src/model/correspondence/offline_correspondence_game.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/chessground_compat.dart' as cg; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/correspondence/offline_correspondence_game_screen.dart'; @@ -77,8 +76,8 @@ class OfflineCorrespondenceGamePreview extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { return SmallBoardPreview( - orientation: game.orientation.cg, - lastMove: game.lastMove?.cg, + orientation: game.orientation, + lastMove: game.lastMove, fen: game.lastPosition.fen, description: Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/src/view/home/home_tab_screen.dart b/lib/src/view/home/home_tab_screen.dart index 7a04182745..019fe2bf7d 100644 --- a/lib/src/view/home/home_tab_screen.dart +++ b/lib/src/view/home/home_tab_screen.dart @@ -11,7 +11,6 @@ import 'package:lichess_mobile/src/model/game/game_history.dart'; import 'package:lichess_mobile/src/model/settings/home_preferences.dart'; import 'package:lichess_mobile/src/navigation.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/chessground_compat.dart'; import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:lichess_mobile/src/utils/l10n.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; @@ -744,8 +743,8 @@ class _GamePreviewCarouselItem extends StatelessWidget { Widget build(BuildContext context) { return BoardCarouselItem( fen: game.fen, - orientation: game.orientation.cg, - lastMove: game.lastMove?.cg, + orientation: game.orientation, + lastMove: game.lastMove, description: Align( alignment: Alignment.centerLeft, child: Padding( diff --git a/lib/src/view/play/challenge_screen.dart b/lib/src/view/play/challenge_screen.dart index 536750fc07..7ed5978826 100644 --- a/lib/src/view/play/challenge_screen.dart +++ b/lib/src/view/play/challenge_screen.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:chessground/chessground.dart' as cg; import 'package:dartchess/dartchess.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -323,8 +322,8 @@ class _ChallengeBodyState extends ConsumerState<_ChallengeBody> { expand: preferences.variant == Variant.fromPosition, child: SmallBoardPreview( orientation: preferences.sideChoice == SideChoice.black - ? cg.Side.black - : cg.Side.white, + ? Side.black + : Side.white, fen: fromPositionFenInput ?? kEmptyFen, description: AdaptiveTextField( maxLines: 5, diff --git a/lib/src/view/play/ongoing_games_screen.dart b/lib/src/view/play/ongoing_games_screen.dart index 2ebda0b091..0318403c1b 100644 --- a/lib/src/view/play/ongoing_games_screen.dart +++ b/lib/src/view/play/ongoing_games_screen.dart @@ -4,7 +4,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/account/ongoing_game.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/chessground_compat.dart' as cg; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/game/game_screen.dart'; @@ -69,8 +68,8 @@ class OngoingGamePreview extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { return SmallBoardPreview( - orientation: game.orientation.cg, - lastMove: game.lastMove?.cg, + orientation: game.orientation, + lastMove: game.lastMove, fen: game.fen, description: Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/src/view/puzzle/puzzle_feedback_widget.dart b/lib/src/view/puzzle/puzzle_feedback_widget.dart index 87576cb802..88b2b20d6d 100644 --- a/lib/src/view/puzzle/puzzle_feedback_widget.dart +++ b/lib/src/view/puzzle/puzzle_feedback_widget.dart @@ -1,4 +1,3 @@ -import 'package:chessground/chessground.dart' as cg; import 'package:dartchess/dartchess.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -30,9 +29,7 @@ class PuzzleFeedbackWidget extends ConsumerWidget { ref.watch(boardPreferencesProvider.select((state) => state.boardTheme)); final brightness = ref.watch(currentBrightnessProvider); - final piece = state.pov == Side.white - ? cg.PieceKind.whiteKing - : cg.PieceKind.blackKing; + final piece = state.pov == Side.white ? kWhiteKingKind : kBlackKingKind; final asset = pieceSet.assets[piece]!; switch (state.mode) { diff --git a/lib/src/view/puzzle/puzzle_history_screen.dart b/lib/src/view/puzzle/puzzle_history_screen.dart index 23ba4d699c..be900aaea4 100644 --- a/lib/src/view/puzzle/puzzle_history_screen.dart +++ b/lib/src/view/puzzle/puzzle_history_screen.dart @@ -10,8 +10,6 @@ import 'package:lichess_mobile/src/model/puzzle/puzzle_activity.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_angle.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_theme.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/chessground_compat.dart' as cg; -import 'package:lichess_mobile/src/utils/chessground_compat.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; @@ -75,9 +73,9 @@ class PuzzleHistoryPreview extends ConsumerWidget { ), ); }, - orientation: side.cg, + orientation: side, fen: fen, - lastMove: lastMove.cg, + lastMove: lastMove, footer: Padding( padding: const EdgeInsets.only(top: 2.0), child: Row( @@ -241,9 +239,9 @@ class _HistoryBoard extends ConsumerWidget { ), ); }, - orientation: turn.cg, + orientation: turn, fen: fen, - lastMove: lastMove.cg, + lastMove: lastMove, footer: Padding( padding: const EdgeInsets.only(top: 2), child: _PuzzleResult(puzzle), diff --git a/lib/src/view/puzzle/puzzle_screen.dart b/lib/src/view/puzzle/puzzle_screen.dart index d845ad745f..c92902c0d5 100644 --- a/lib/src/view/puzzle/puzzle_screen.dart +++ b/lib/src/view/puzzle/puzzle_screen.dart @@ -1,4 +1,4 @@ -import 'package:chessground/chessground.dart' as cg; +import 'package:chessground/chessground.dart'; import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/cupertino.dart'; @@ -23,7 +23,6 @@ import 'package:lichess_mobile/src/model/puzzle/puzzle_theme.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/navigation.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/chessground_compat.dart'; import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:lichess_mobile/src/utils/immersive_mode.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; @@ -175,11 +174,7 @@ class _LoadNextPuzzle extends ConsumerWidget { child: BoardTable( topTable: kEmptyWidget, bottomTable: kEmptyWidget, - boardData: cg.BoardData( - fen: kEmptyFen, - interactableSide: cg.InteractableSide.none, - orientation: cg.Side.white, - ), + boardState: kEmptyBoardState, errorMessage: 'No more puzzles. Go online to get more.', ), ); @@ -198,11 +193,7 @@ class _LoadNextPuzzle extends ConsumerWidget { child: BoardTable( topTable: kEmptyWidget, bottomTable: kEmptyWidget, - boardData: const cg.BoardData( - fen: kEmptyFen, - interactableSide: cg.InteractableSide.none, - orientation: cg.Side.white, - ), + boardState: kEmptyBoardState, errorMessage: e.toString(), ), ); @@ -238,10 +229,10 @@ class _LoadPuzzleFromId extends ConsumerWidget { child: SafeArea( bottom: false, child: BoardTable( - boardData: cg.BoardData( + boardState: ChessboardState( fen: kEmptyFen, - interactableSide: cg.InteractableSide.none, - orientation: cg.Side.white, + interactableSide: InteractableSide.none, + orientation: Side.white, ), topTable: kEmptyWidget, bottomTable: kEmptyWidget, @@ -261,11 +252,7 @@ class _LoadPuzzleFromId extends ConsumerWidget { child: SafeArea( bottom: false, child: BoardTable( - boardData: const cg.BoardData( - fen: kEmptyFen, - interactableSide: cg.InteractableSide.none, - orientation: cg.Side.white, - ), + boardState: kEmptyBoardState, topTable: kEmptyWidget, bottomTable: kEmptyWidget, errorMessage: e.toString(), @@ -298,7 +285,7 @@ class _Body extends ConsumerWidget { engineEvaluationProvider.select((s) => s.eval?.bestMove), ); final evalBestMove = - (currentEvalBest ?? puzzleState.node.eval?.bestMove)?.cg; + (currentEvalBest ?? puzzleState.node.eval?.bestMove) as NormalMove?; return Column( children: [ @@ -309,27 +296,27 @@ class _Body extends ConsumerWidget { onMove: (move, {isDrop, isPremove}) { ref .read(ctrlProvider.notifier) - .onUserMove(Move.fromUci(move.uci)!); + .onUserMove(Move.parse(move.uci)!); }, - boardData: cg.BoardData( - orientation: puzzleState.pov.cg, + boardState: ChessboardState( + orientation: puzzleState.pov, interactableSide: puzzleState.mode == PuzzleMode.load || puzzleState.position.isGameOver - ? cg.InteractableSide.none + ? InteractableSide.none : puzzleState.mode == PuzzleMode.view - ? cg.InteractableSide.both + ? InteractableSide.both : puzzleState.pov == Side.white - ? cg.InteractableSide.white - : cg.InteractableSide.black, + ? InteractableSide.white + : InteractableSide.black, fen: puzzleState.fen, isCheck: boardPreferences.boardHighlights && puzzleState.position.isCheck, - lastMove: puzzleState.lastMove?.cg, - sideToMove: puzzleState.position.turn.cg, + lastMove: puzzleState.lastMove as NormalMove?, + sideToMove: puzzleState.position.turn, validMoves: puzzleState.validMoves, shapes: puzzleState.isEngineEnabled && evalBestMove != null ? ISet([ - cg.Arrow( + Arrow( color: const Color(0x40003088), orig: evalBestMove.from, dest: evalBestMove.to, diff --git a/lib/src/view/puzzle/puzzle_tab_screen.dart b/lib/src/view/puzzle/puzzle_tab_screen.dart index 7482c3d211..96cbfc9615 100644 --- a/lib/src/view/puzzle/puzzle_tab_screen.dart +++ b/lib/src/view/puzzle/puzzle_tab_screen.dart @@ -13,7 +13,6 @@ import 'package:lichess_mobile/src/navigation.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/puzzle_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/chessground_compat.dart'; import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; @@ -432,9 +431,9 @@ class _DailyPuzzle extends ConsumerWidget { data: (data) { final preview = PuzzlePreview.fromPuzzle(data); return SmallBoardPreview( - orientation: preview.orientation.cg, + orientation: preview.orientation, fen: preview.initialFen, - lastMove: preview.initialMove.cg, + lastMove: preview.initialMove, description: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -480,7 +479,7 @@ class _DailyPuzzle extends ConsumerWidget { ); }, loading: () => SmallBoardPreview( - orientation: Side.white.cg, + orientation: Side.white, fen: kEmptyFen, description: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -514,9 +513,9 @@ class _OfflinePuzzlePreview extends ConsumerWidget { final preview = data != null ? PuzzlePreview.fromPuzzle(data.puzzle) : null; return SmallBoardPreview( - orientation: preview?.orientation.cg ?? Side.white.cg, + orientation: preview?.orientation ?? Side.white, fen: preview?.initialFen ?? kEmptyFen, - lastMove: preview?.initialMove.cg, + lastMove: preview?.initialMove, description: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.max, diff --git a/lib/src/view/puzzle/storm_screen.dart b/lib/src/view/puzzle/storm_screen.dart index a0395b05f9..a482f2e6c3 100644 --- a/lib/src/view/puzzle/storm_screen.dart +++ b/lib/src/view/puzzle/storm_screen.dart @@ -1,4 +1,4 @@ -import 'package:chessground/chessground.dart' as cg; +import 'package:chessground/chessground.dart'; import 'package:collection/collection.dart'; import 'package:dartchess/dartchess.dart'; import 'package:flutter/cupertino.dart'; @@ -15,7 +15,6 @@ import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/model/settings/brightness.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/chessground_compat.dart'; import 'package:lichess_mobile/src/utils/gestures_exclusion.dart'; import 'package:lichess_mobile/src/utils/immersive_mode.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; @@ -101,11 +100,7 @@ class _Load extends ConsumerWidget { child: BoardTable( topTable: kEmptyWidget, bottomTable: kEmptyWidget, - boardData: const cg.BoardData( - fen: kEmptyFen, - interactableSide: cg.InteractableSide.none, - orientation: cg.Side.white, - ), + boardState: kEmptyBoardState, errorMessage: e.toString(), ), ); @@ -173,25 +168,25 @@ class _Body extends ConsumerWidget { boardKey: boardKey, onMove: (move, {isDrop, isPremove}) => ref .read(ctrlProvider.notifier) - .onUserMove(Move.fromUci(move.uci)!), + .onUserMove(Move.parse(move.uci)!), onPremove: (move) => ref.read(ctrlProvider.notifier).setPremove(move), - boardData: cg.BoardData( - orientation: stormState.pov.cg, + boardState: ChessboardState( + orientation: stormState.pov, interactableSide: !stormState.firstMovePlayed || stormState.mode == StormMode.ended || stormState.position.isGameOver - ? cg.InteractableSide.none + ? InteractableSide.none : stormState.pov == Side.white - ? cg.InteractableSide.white - : cg.InteractableSide.black, + ? InteractableSide.white + : InteractableSide.black, fen: stormState.position.fen, isCheck: boardPreferences.boardHighlights && stormState.position.isCheck, - lastMove: stormState.lastMove?.cg, - sideToMove: stormState.position.turn.cg, + lastMove: stormState.lastMove as NormalMove?, + sideToMove: stormState.position.turn, validMoves: stormState.validMoves, - premove: stormState.premove, + premove: stormState.premove as NormalMove?, ), topTable: _TopTable(data), bottomTable: _Combo(stormState.combo), diff --git a/lib/src/view/puzzle/streak_screen.dart b/lib/src/view/puzzle/streak_screen.dart index 0851d0f223..cbf343815c 100644 --- a/lib/src/view/puzzle/streak_screen.dart +++ b/lib/src/view/puzzle/streak_screen.dart @@ -1,4 +1,4 @@ -import 'package:chessground/chessground.dart' as cg; +import 'package:chessground/chessground.dart'; import 'package:dartchess/dartchess.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -16,7 +16,6 @@ import 'package:lichess_mobile/src/model/puzzle/puzzle_streak.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_theme.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/chessground_compat.dart'; import 'package:lichess_mobile/src/utils/immersive_mode.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; @@ -102,11 +101,7 @@ class _Load extends ConsumerWidget { child: BoardTable( topTable: kEmptyWidget, bottomTable: kEmptyWidget, - boardData: const cg.BoardData( - fen: kEmptyFen, - interactableSide: cg.InteractableSide.none, - orientation: cg.Side.white, - ), + boardState: kEmptyBoardState, errorMessage: e.toString(), ), ); @@ -152,22 +147,22 @@ class _Body extends ConsumerWidget { onMove: (move, {isDrop, isPremove}) { ref .read(ctrlProvider.notifier) - .onUserMove(Move.fromUci(move.uci)!); + .onUserMove(Move.parse(move.uci)!); }, - boardData: cg.BoardData( - orientation: puzzleState.pov.cg, + boardState: ChessboardState( + orientation: puzzleState.pov, interactableSide: puzzleState.mode == PuzzleMode.load || puzzleState.position.isGameOver - ? cg.InteractableSide.none + ? InteractableSide.none : puzzleState.mode == PuzzleMode.view - ? cg.InteractableSide.both + ? InteractableSide.both : puzzleState.pov == Side.white - ? cg.InteractableSide.white - : cg.InteractableSide.black, + ? InteractableSide.white + : InteractableSide.black, fen: puzzleState.fen, isCheck: puzzleState.position.isCheck, - lastMove: puzzleState.lastMove?.cg, - sideToMove: puzzleState.position.turn.cg, + lastMove: puzzleState.lastMove as NormalMove?, + sideToMove: puzzleState.position.turn, validMoves: puzzleState.validMoves, ), topTable: Center( diff --git a/lib/src/view/settings/piece_set_screen.dart b/lib/src/view/settings/piece_set_screen.dart index efb1cf2234..b4251ad127 100644 --- a/lib/src/view/settings/piece_set_screen.dart +++ b/lib/src/view/settings/piece_set_screen.dart @@ -1,4 +1,5 @@ import 'package:chessground/chessground.dart'; +import 'package:dartchess/dartchess.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -40,12 +41,12 @@ class _Body extends ConsumerWidget { List getPieceImages(PieceSet set) { return [ - set.assets[PieceKind.whiteKing]!, - set.assets[PieceKind.blackQueen]!, - set.assets[PieceKind.whiteRook]!, - set.assets[PieceKind.blackBishop]!, - set.assets[PieceKind.whiteKnight]!, - set.assets[PieceKind.blackPawn]!, + set.assets[kWhiteKingKind]!, + set.assets[kBlackQueenKind]!, + set.assets[kWhiteRookKind]!, + set.assets[kBlackBishopKind]!, + set.assets[kWhiteKnightKind]!, + set.assets[kBlackPawnKind]!, ]; } diff --git a/lib/src/view/settings/theme_screen.dart b/lib/src/view/settings/theme_screen.dart index 332d0098b8..8bd675effa 100644 --- a/lib/src/view/settings/theme_screen.dart +++ b/lib/src/view/settings/theme_screen.dart @@ -1,6 +1,7 @@ import 'dart:math' as math; import 'package:chessground/chessground.dart'; import 'package:dartchess/dartchess.dart' as dartchess; +import 'package:dartchess/dartchess.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -63,14 +64,14 @@ class _Body extends ConsumerWidget { vertical: 16, ), child: Center( - child: Board( + child: Chessboard( size: boardSize, - data: const BoardData( + state: const ChessboardState( interactableSide: InteractableSide.none, orientation: Side.white, fen: dartchess.kInitialFEN, ), - settings: BoardSettings( + settings: ChessboardSettings( enableCoordinates: false, borderRadius: const BorderRadius.all(Radius.circular(4.0)), diff --git a/lib/src/view/watch/live_tv_channels_screen.dart b/lib/src/view/watch/live_tv_channels_screen.dart index 3e2a8c52e8..7640f7f456 100644 --- a/lib/src/view/watch/live_tv_channels_screen.dart +++ b/lib/src/view/watch/live_tv_channels_screen.dart @@ -5,7 +5,6 @@ import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/tv/live_tv_channels.dart'; import 'package:lichess_mobile/src/model/tv/tv_channel.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/chessground_compat.dart'; import 'package:lichess_mobile/src/utils/focus_detector.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/watch/tv_screen.dart'; @@ -84,9 +83,9 @@ class _Body extends ConsumerWidget { ), ); }, - orientation: game.orientation.cg, + orientation: game.orientation, fen: game.fen ?? kEmptyFen, - lastMove: game.lastMove?.cg, + lastMove: game.lastMove, description: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.max, diff --git a/lib/src/view/watch/tv_screen.dart b/lib/src/view/watch/tv_screen.dart index c63f218cb3..2a475768af 100644 --- a/lib/src/view/watch/tv_screen.dart +++ b/lib/src/view/watch/tv_screen.dart @@ -1,4 +1,4 @@ -import 'package:chessground/chessground.dart' as cg; +import 'package:chessground/chessground.dart'; import 'package:dartchess/dartchess.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -9,7 +9,6 @@ import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/model/tv/tv_channel.dart'; import 'package:lichess_mobile/src/model/tv/tv_controller.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/chessground_compat.dart'; import 'package:lichess_mobile/src/utils/focus_detector.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/view/game/game_player.dart'; @@ -122,12 +121,12 @@ class _Body extends ConsumerWidget { gameState.game.positionAt(gameState.stepCursor); final sideToMove = position.turn; - final boardData = cg.BoardData( - interactableSide: cg.InteractableSide.none, - orientation: gameState.orientation.cg, + final boardData = ChessboardState( + interactableSide: InteractableSide.none, + orientation: gameState.orientation, fen: position.fen, - sideToMove: sideToMove.cg, - lastMove: game.moveAt(gameState.stepCursor)?.cg, + sideToMove: sideToMove, + lastMove: game.moveAt(gameState.stepCursor) as NormalMove?, isCheck: boardPreferences.boardHighlights && position.isCheck, ); final blackPlayerWidget = GamePlayer( @@ -153,7 +152,7 @@ class _Body extends ConsumerWidget { materialDiff: game.lastMaterialDiffAt(Side.white), ); return BoardTable( - boardData: boardData, + boardState: boardData, boardSettingsOverrides: const BoardSettingsOverrides( animationDuration: Duration.zero, ), @@ -173,11 +172,7 @@ class _Body extends ConsumerWidget { loading: () => const BoardTable( topTable: kEmptyWidget, bottomTable: kEmptyWidget, - boardData: cg.BoardData( - interactableSide: cg.InteractableSide.none, - orientation: cg.Side.white, - fen: kEmptyFen, - ), + boardState: kEmptyBoardState, showMoveListPlaceholder: true, ), error: (err, stackTrace) { @@ -187,11 +182,7 @@ class _Body extends ConsumerWidget { return const BoardTable( topTable: kEmptyWidget, bottomTable: kEmptyWidget, - boardData: cg.BoardData( - fen: kEmptyFen, - interactableSide: cg.InteractableSide.none, - orientation: cg.Side.white, - ), + boardState: kEmptyBoardState, errorMessage: 'Could not load TV stream.', showMoveListPlaceholder: true, ); diff --git a/lib/src/widgets/board_carousel_item.dart b/lib/src/widgets/board_carousel_item.dart index ddb83cd537..608e3089a9 100644 --- a/lib/src/widgets/board_carousel_item.dart +++ b/lib/src/widgets/board_carousel_item.dart @@ -1,4 +1,5 @@ import 'package:chessground/chessground.dart'; +import 'package:dartchess/dartchess.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; @@ -79,15 +80,15 @@ class BoardCarouselItem extends ConsumerWidget { }, child: SizedBox( height: boardSize, - child: Board( + child: Chessboard( size: boardSize, - data: BoardData( + state: ChessboardState( interactableSide: InteractableSide.none, fen: fen, orientation: orientation, - lastMove: lastMove, + lastMove: lastMove as NormalMove?, ), - settings: BoardSettings( + settings: ChessboardSettings( enableCoordinates: false, borderRadius: const BorderRadius.only( topLeft: Radius.circular(10.0), diff --git a/lib/src/widgets/board_preview.dart b/lib/src/widgets/board_preview.dart index c004cfb4a3..1a5e17f755 100644 --- a/lib/src/widgets/board_preview.dart +++ b/lib/src/widgets/board_preview.dart @@ -1,4 +1,5 @@ import 'package:chessground/chessground.dart'; +import 'package:dartchess/dartchess.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -64,15 +65,15 @@ class _SmallBoardPreviewState extends ConsumerState { height: boardSize, child: Row( children: [ - Board( + Chessboard( size: boardSize, - data: BoardData( + state: ChessboardState( interactableSide: InteractableSide.none, fen: widget.fen, orientation: widget.orientation, - lastMove: widget.lastMove, + lastMove: widget.lastMove as NormalMove?, ), - settings: BoardSettings( + settings: ChessboardSettings( enableCoordinates: false, borderRadius: const BorderRadius.all(Radius.circular(4.0)), diff --git a/lib/src/widgets/board_table.dart b/lib/src/widgets/board_table.dart index 7d364113a0..82bfa9c205 100644 --- a/lib/src/widgets/board_table.dart +++ b/lib/src/widgets/board_table.dart @@ -1,5 +1,6 @@ import 'package:chessground/chessground.dart'; import 'package:collection/collection.dart'; +import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -31,7 +32,7 @@ class BoardTable extends ConsumerStatefulWidget { const BoardTable({ this.onMove, this.onPremove, - required this.boardData, + required this.boardState, this.boardSettingsOverrides, required this.topTable, required this.bottomTable, @@ -54,7 +55,7 @@ class BoardTable extends ConsumerStatefulWidget { final void Function(Move, {bool? isDrop, bool? isPremove})? onMove; final void Function(Move?)? onPremove; - final BoardData boardData; + final ChessboardState boardState; final BoardSettingsOverrides? boardSettingsOverrides; @@ -162,11 +163,11 @@ class _BoardTableState extends ConsumerState { ? widget.boardSettingsOverrides!.merge(defaultSettings) : defaultSettings; - final board = Board( + final board = Chessboard( key: widget.boardKey, size: boardSize, - data: widget.boardData.copyWith( - shapes: userShapes.union(widget.boardData.shapes ?? ISet()), + state: widget.boardState.copyWith( + shapes: userShapes.union(widget.boardState.shapes ?? ISet()), ), settings: settings, onMove: widget.onMove, @@ -357,7 +358,7 @@ class BoardSettingsOverrides { final bool? autoQueenPromotionOnPremove; final bool? blindfoldMode; - BoardSettings merge(BoardSettings settings) { + ChessboardSettings merge(ChessboardSettings settings) { return settings.copyWith( animationDuration: animationDuration, autoQueenPromotion: autoQueenPromotion, diff --git a/lib/src/widgets/board_thumbnail.dart b/lib/src/widgets/board_thumbnail.dart index 2b26132d88..6bf77234bd 100644 --- a/lib/src/widgets/board_thumbnail.dart +++ b/lib/src/widgets/board_thumbnail.dart @@ -1,5 +1,5 @@ import 'package:chessground/chessground.dart'; -import 'package:dartchess/dartchess.dart' as dartchess; +import 'package:dartchess/dartchess.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/constants.dart'; @@ -22,7 +22,7 @@ class BoardThumbnail extends ConsumerStatefulWidget { this.header, this.footer, }) : orientation = Side.white, - fen = dartchess.kInitialFEN, + fen = kInitialFEN, lastMove = null, onTap = null; @@ -67,15 +67,15 @@ class _BoardThumbnailState extends ConsumerState { Widget build(BuildContext context) { final boardPrefs = ref.watch(boardPreferencesProvider); - final board = Board( + final board = Chessboard( size: widget.size, - data: BoardData( + state: ChessboardState( interactableSide: InteractableSide.none, fen: widget.fen, orientation: widget.orientation, - lastMove: widget.lastMove, + lastMove: widget.lastMove as NormalMove?, ), - settings: BoardSettings( + settings: ChessboardSettings( enableCoordinates: false, borderRadius: const BorderRadius.all(Radius.circular(4.0)), boxShadow: boardShadows, diff --git a/pubspec.lock b/pubspec.lock index 91ce44d61b..85e31d8108 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -194,10 +194,10 @@ packages: dependency: "direct main" description: name: chessground - sha256: "44b2f20c8df56d7f42c5d10c68dc8b79f766db65f9f1b4cca45c4d30579d4e57" + sha256: ff6c770780c7b6d5232d743d44010077fa26b2301dbe71feaaf943679aef3241 url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "4.0.0" ci: dependency: transitive description: @@ -346,10 +346,10 @@ packages: dependency: "direct main" description: name: dartchess - sha256: "047ee9973f2546744f3d24eeee8c01d563714d3f10a73b0b0799218dc00c65c1" + sha256: f07896a6c29f169ce21b44429c0ff8057d58cd4f9b17b35c5ffc8a1b2cfc4f23 url: "https://pub.dev" source: hosted - version: "0.7.1" + version: "0.8.0" dbus: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 7ccdce88b8..f9837dd935 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,14 +11,14 @@ dependencies: app_settings: ^5.1.1 async: ^2.10.0 cached_network_image: ^3.2.2 - chessground: ^3.2.0 + chessground: ^4.0.0 collection: ^1.17.0 connectivity_plus: ^6.0.2 cronet_http: ^1.3.1 crypto: ^3.0.3 cupertino_http: ^1.1.0 cupertino_icons: ^1.0.2 - dartchess: ^0.7.0 + dartchess: ^0.8.0 deep_pick: ^1.0.0 device_info_plus: ^10.1.0 dynamic_color: ^1.6.9 diff --git a/test/model/common/node_test.dart b/test/model/common/node_test.dart index f0ee966732..5594321bcd 100644 --- a/test/model/common/node_test.dart +++ b/test/model/common/node_test.dart @@ -13,15 +13,15 @@ void main() { expect(root.position, equals(Chess.initial)); expect(root.children.length, equals(1)); final child = root.children.first; - expect(child.id, equals(UciCharPair.fromMove(Move.fromUci('e2e4')!))); - expect(child.sanMove, equals(SanMove('e4', Move.fromUci('e2e4')!))); + expect(child.id, equals(UciCharPair.fromMove(Move.parse('e2e4')!))); + expect(child.sanMove, equals(SanMove('e4', Move.parse('e2e4')!))); expect( child.position.fen, equals('rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1'), ); expect( child.position, - equals(Chess.initial.playUnchecked(Move.fromUci('e2e4')!)), + equals(Chess.initial.playUnchecked(Move.parse('e2e4')!)), ); }); @@ -71,7 +71,7 @@ void main() { test('branchesOn, with variation', () { final root = Root.fromPgnMoves('e4 e5 Nf3'); - final move = Move.fromUci('b1c3')!; + final move = Move.parse('b1c3')!; final (newPath, _) = root.addMoveAt( UciPath.fromIds( [UciCharPair.fromUci('e2e4'), UciCharPair.fromUci('e7e5')].lock, @@ -98,8 +98,8 @@ void main() { expect(mainline.length, equals(2)); final list = mainline.toList(); - expect(list[0].sanMove, equals(SanMove('e4', Move.fromUci('e2e4')!))); - expect(list[1].sanMove, equals(SanMove('e5', Move.fromUci('e7e5')!))); + expect(list[0].sanMove, equals(SanMove('e4', Move.parse('e2e4')!))); + expect(list[1].sanMove, equals(SanMove('e5', Move.parse('e7e5')!))); }); test('isOnMainline', () { @@ -107,7 +107,7 @@ void main() { final path = UciPath.fromId(UciCharPair.fromUci('e2e4')); expect(root.isOnMainline(path), isTrue); - final move = Move.fromUci('b1c3')!; + final move = Move.parse('b1c3')!; final (newPath, _) = root.addMoveAt( UciPath.fromIds( [UciCharPair.fromUci('e2e4'), UciCharPair.fromUci('e7e5')].lock, @@ -123,7 +123,7 @@ void main() { position: Chess.initial, ); final child = Branch( - sanMove: SanMove('e4', Move.fromUci('e2e4')!), + sanMove: SanMove('e4', Move.parse('e2e4')!), position: Chess.initial, ); root.addChild(child); @@ -134,7 +134,7 @@ void main() { test('prepend child', () { final root = Root.fromPgnMoves('e4 e5'); final child = Branch( - sanMove: SanMove('d4', Move.fromUci('d2d4')!), + sanMove: SanMove('d4', Move.parse('d2d4')!), position: Chess.initial, ); root.prependChild(child); @@ -177,7 +177,7 @@ void main() { test('updateAt', () { final root = Root.fromPgnMoves('e4 e5'); final branch = Branch( - sanMove: SanMove('Nc6', Move.fromUci('b8c6')!), + sanMove: SanMove('Nc6', Move.parse('b8c6')!), position: Chess.initial, ); @@ -250,7 +250,7 @@ void main() { test('addNodeAt', () { final root = Root.fromPgnMoves('e4 e5'); final branch = Branch( - sanMove: SanMove('Nc6', Move.fromUci('b8c6')!), + sanMove: SanMove('Nc6', Move.parse('b8c6')!), position: Chess.initial, ); final (newPath, isNewNode) = @@ -275,7 +275,7 @@ void main() { test('addNodeAt, prepend', () { final root = Root.fromPgnMoves('e4 e5'); final branch = Branch( - sanMove: SanMove('Nc6', Move.fromUci('b8c6')!), + sanMove: SanMove('Nc6', Move.parse('b8c6')!), position: Chess.initial, ); root.addNodeAt( @@ -292,7 +292,7 @@ void main() { test('addNodeAt, with an existing node at path', () { final root = Root.fromPgnMoves('e4 e5'); final branch = Branch( - sanMove: SanMove('e5', Move.fromUci('e7e5')!), + sanMove: SanMove('e5', Move.parse('e7e5')!), position: Chess.initial, ); final (newPath, isNewNode) = @@ -319,11 +319,11 @@ void main() { test('addNodesAt', () { final root = Root.fromPgnMoves('e4 e5'); final branch = Branch( - sanMove: SanMove('Nc6', Move.fromUci('b8c6')!), + sanMove: SanMove('Nc6', Move.parse('b8c6')!), position: Chess.initial, ); final branch2 = Branch( - sanMove: SanMove('Na6', Move.fromUci('b8a6')!), + sanMove: SanMove('Na6', Move.parse('b8a6')!), position: Chess.initial, ); root.addNodesAt( @@ -339,7 +339,7 @@ void main() { test('addMoveAt', () { final root = Root.fromPgnMoves('e4 e5'); - final move = Move.fromUci('b1c3')!; + final move = Move.parse('b1c3')!; final path = UciPath.fromIds( [UciCharPair.fromUci('e2e4'), UciCharPair.fromUci('e7e5')].lock, ); @@ -459,12 +459,12 @@ void main() { final initialPath = root.mainlinePath; final initialPng = root.makePgn(); - final move = Move.fromUci(alt1); + final move = Move.parse(alt1); expect(move, isNotNull); final newMove = root.convertAltCastlingMove(move!); expect(newMove, isNotNull); - expect(newMove, Move.fromUci(alt2)); + expect(newMove, Move.parse(alt2)); expect(root.mainline.last.sanMove.move, newMove); final previousUciPath = root.mainlinePath.penultimate; @@ -512,7 +512,7 @@ void main() { final root = Root.fromPgnGame(PgnGame.parsePgn(pgn)); final initialPng = root.makePgn(); final previousUciPath = root.mainlinePath.penultimate; - final move = Move.fromUci('e1g1'); + final move = Move.parse('e1g1'); root.addMoveAt(previousUciPath, move!); expect(root.makePgn(), isNot(initialPng)); }); diff --git a/test/model/common/uci_test.dart b/test/model/common/uci_test.dart index 1fb157bbd4..3ee7d7bd8e 100644 --- a/test/model/common/uci_test.dart +++ b/test/model/common/uci_test.dart @@ -5,18 +5,18 @@ import 'package:lichess_mobile/src/model/common/uci.dart'; void main() { test('UciCharPair', () { // regular moves - expect(UciCharPair.fromMove(Move.fromUci('a1b1')!).toString(), '#\$'); - expect(UciCharPair.fromMove(Move.fromUci('a1a2')!).toString(), '#+'); - expect(UciCharPair.fromMove(Move.fromUci('h7h8')!).toString(), 'Zb'); + expect(UciCharPair.fromMove(Move.parse('a1b1')!).toString(), '#\$'); + expect(UciCharPair.fromMove(Move.parse('a1a2')!).toString(), '#+'); + expect(UciCharPair.fromMove(Move.parse('h7h8')!).toString(), 'Zb'); // promotions - expect(UciCharPair.fromMove(Move.fromUci('b7b8q')!).toString(), 'Td'); - expect(UciCharPair.fromMove(Move.fromUci('b7c8q')!).toString(), 'Te'); - expect(UciCharPair.fromMove(Move.fromUci('b7c8n')!).toString(), 'T}'); + expect(UciCharPair.fromMove(Move.parse('b7b8q')!).toString(), 'Td'); + expect(UciCharPair.fromMove(Move.parse('b7c8q')!).toString(), 'Te'); + expect(UciCharPair.fromMove(Move.parse('b7c8n')!).toString(), 'T}'); // drops - expect(UciCharPair.fromMove(Move.fromUci('P@a1')!).toString(), '#\x8f'); - expect(UciCharPair.fromMove(Move.fromUci('Q@h8')!).toString(), 'b\x8b'); + expect(UciCharPair.fromMove(Move.parse('P@a1')!).toString(), '#\x8f'); + expect(UciCharPair.fromMove(Move.parse('Q@h8')!).toString(), 'b\x8b'); }); group('UciPath', () { diff --git a/test/test_utils.dart b/test/test_utils.dart index e03dac1d02..525dafbb1b 100644 --- a/test/test_utils.dart +++ b/test/test_utils.dart @@ -1,6 +1,6 @@ import 'dart:convert'; -import 'package:chessground/chessground.dart' as cg; +import 'package:dartchess/dartchess.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -65,15 +65,22 @@ Future meetsTapTargetGuideline(WidgetTester tester) async { } Offset squareOffset( - cg.SquareId id, + Square square, Rect boardRect, { - cg.Side orientation = cg.Side.white, + Side orientation = Side.white, }) { final squareSize = boardRect.width / 8; - final o = cg.Coord.fromSquareId(id).offset(orientation, squareSize); + + final dx = + (orientation == Side.white ? square.file.value : 7 - square.file.value) * + squareSize; + final dy = + (orientation == Side.white ? 7 - square.rank.value : square.rank.value) * + squareSize; + return Offset( - o.dx + boardRect.left + squareSize / 2, - o.dy + boardRect.top + squareSize / 2, + dx + boardRect.left + squareSize / 2, + dy + boardRect.top + squareSize / 2, ); } @@ -82,11 +89,15 @@ Future playMove( Rect boardRect, String from, String to, { - cg.Side orientation = cg.Side.white, + Side orientation = Side.white, }) async { - await tester.tapAt(squareOffset(from, boardRect, orientation: orientation)); + await tester.tapAt( + squareOffset(Square.fromName(from), boardRect, orientation: orientation), + ); await tester.pump(); - await tester.tapAt(squareOffset(to, boardRect, orientation: orientation)); + await tester.tapAt( + squareOffset(Square.fromName(to), boardRect, orientation: orientation), + ); await tester.pump(); } diff --git a/test/view/analysis/analysis_screen_test.dart b/test/view/analysis/analysis_screen_test.dart index 81a7d1494d..45cc239ca9 100644 --- a/test/view/analysis/analysis_screen_test.dart +++ b/test/view/analysis/analysis_screen_test.dart @@ -1,6 +1,6 @@ import 'dart:convert'; -import 'package:chessground/chessground.dart' as cg; +import 'package:chessground/chessground.dart'; import 'package:dartchess/dartchess.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -45,8 +45,8 @@ void main() { await tester.pumpWidget(app); - expect(find.byType(cg.Board), findsOneWidget); - expect(find.byType(cg.PieceWidget), findsNWidgets(25)); + expect(find.byType(Chessboard), findsOneWidget); + expect(find.byType(PieceWidget), findsNWidgets(25)); final currentMove = find.widgetWithText(InlineMove, 'Qe1#'); expect(currentMove, findsOneWidget); expect( diff --git a/test/view/game/archived_game_screen_test.dart b/test/view/game/archived_game_screen_test.dart index 4c699279b3..a3ae065c4d 100644 --- a/test/view/game/archived_game_screen_test.dart +++ b/test/view/game/archived_game_screen_test.dart @@ -1,4 +1,4 @@ -import 'package:chessground/chessground.dart' as cg; +import 'package:chessground/chessground.dart'; import 'package:dartchess/dartchess.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -47,8 +47,8 @@ void main() { await tester.pumpWidget(app); // data shown immediately - expect(find.byType(cg.Board), findsOneWidget); - expect(find.byType(cg.PieceWidget), findsNWidgets(25)); + expect(find.byType(Chessboard), findsOneWidget); + expect(find.byType(PieceWidget), findsNWidgets(25)); expect(find.widgetWithText(GamePlayer, 'veloce'), findsOneWidget); expect( find.widgetWithText(GamePlayer, 'Stockfish level 1'), @@ -57,8 +57,11 @@ void main() { // cannot interact with board expect( - tester.widget(find.byType(cg.Board)).data.interactableSide, - cg.InteractableSide.none, + tester + .widget(find.byType(Chessboard)) + .state + .interactableSide, + InteractableSide.none, ); // moves are not loaded @@ -78,8 +81,8 @@ void main() { await tester.pumpAndSettle(); // same info still displayed - expect(find.byType(cg.Board), findsOneWidget); - expect(find.byType(cg.PieceWidget), findsNWidgets(25)); + expect(find.byType(Chessboard), findsOneWidget); + expect(find.byType(PieceWidget), findsNWidgets(25)); expect(find.widgetWithText(GamePlayer, 'veloce'), findsOneWidget); expect( find.widgetWithText(GamePlayer, 'Stockfish level 1'), diff --git a/test/view/puzzle/puzzle_screen_test.dart b/test/view/puzzle/puzzle_screen_test.dart index f7e47eb86f..0f95563c8b 100644 --- a/test/view/puzzle/puzzle_screen_test.dart +++ b/test/view/puzzle/puzzle_screen_test.dart @@ -1,4 +1,4 @@ -import 'package:chessground/chessground.dart' as cg; +import 'package:chessground/chessground.dart'; import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/cupertino.dart'; @@ -98,7 +98,7 @@ void main() { // wait for the puzzle to load await tester.pump(const Duration(milliseconds: 200)); - expect(find.byType(cg.Board), findsOneWidget); + expect(find.byType(Chessboard), findsOneWidget); expect(find.text('Your turn'), findsOneWidget); }, ); @@ -127,13 +127,13 @@ void main() { await tester.pumpWidget(app); - expect(find.byType(cg.Board), findsNothing); + expect(find.byType(Chessboard), findsNothing); expect(find.text('Your turn'), findsNothing); // wait for the puzzle to load await tester.pump(const Duration(milliseconds: 200)); - expect(find.byType(cg.Board), findsOneWidget); + expect(find.byType(Chessboard), findsOneWidget); expect(find.text('Your turn'), findsOneWidget); }); @@ -189,16 +189,16 @@ void main() { // wait for the puzzle to load await tester.pump(const Duration(milliseconds: 200)); - expect(find.byType(cg.Board), findsOneWidget); + expect(find.byType(Chessboard), findsOneWidget); expect(find.text('Your turn'), findsOneWidget); // before the first move is played, puzzle is not interactable - expect(find.byKey(const Key('g4-blackRook')), findsOneWidget); - await tester.tap(find.byKey(const Key('g4-blackRook'))); + expect(find.byKey(const Key('g4-blackrook')), findsOneWidget); + await tester.tap(find.byKey(const Key('g4-blackrook'))); await tester.pump(); expect(find.byKey(const Key('g4-selected')), findsNothing); - const orientation = cg.Side.black; + const orientation = Side.black; // await for first move to be played await tester.pump(const Duration(milliseconds: 1500)); @@ -208,25 +208,25 @@ void main() { // in play mode we see the solution button expect(find.byIcon(Icons.help), findsOneWidget); - expect(find.byKey(const Key('g4-blackRook')), findsOneWidget); - expect(find.byKey(const Key('h8-whiteQueen')), findsOneWidget); + expect(find.byKey(const Key('g4-blackrook')), findsOneWidget); + expect(find.byKey(const Key('h8-whitequeen')), findsOneWidget); - final boardRect = tester.getRect(find.byType(cg.Board)); + final boardRect = tester.getRect(find.byType(Chessboard)); await playMove(tester, boardRect, 'g4', 'h4', orientation: orientation); - expect(find.byKey(const Key('h4-blackRook')), findsOneWidget); + expect(find.byKey(const Key('h4-blackrook')), findsOneWidget); expect(find.text('Best move!'), findsOneWidget); // wait for line reply and move animation await tester.pump(const Duration(milliseconds: 500)); await tester.pumpAndSettle(); - expect(find.byKey(const Key('h4-whiteQueen')), findsOneWidget); + expect(find.byKey(const Key('h4-whitequeen')), findsOneWidget); await playMove(tester, boardRect, 'b4', 'h4', orientation: orientation); - expect(find.byKey(const Key('h4-blackRook')), findsOneWidget); + expect(find.byKey(const Key('h4-blackrook')), findsOneWidget); expect(find.text('Success!'), findsOneWidget); // wait for move animation @@ -305,17 +305,17 @@ void main() { // wait for the puzzle to load await tester.pump(const Duration(milliseconds: 200)); - expect(find.byType(cg.Board), findsOneWidget); + expect(find.byType(Chessboard), findsOneWidget); expect(find.text('Your turn'), findsOneWidget); - const orientation = cg.Side.black; + const orientation = Side.black; // await for first move to be played await tester.pump(const Duration(milliseconds: 1500)); - expect(find.byKey(const Key('g4-blackRook')), findsOneWidget); + expect(find.byKey(const Key('g4-blackrook')), findsOneWidget); - final boardRect = tester.getRect(find.byType(cg.Board)); + final boardRect = tester.getRect(find.byType(Chessboard)); await playMove(tester, boardRect, 'g4', 'f4', orientation: orientation); @@ -329,11 +329,11 @@ void main() { await tester.pumpAndSettle(); // can still play the puzzle - expect(find.byKey(const Key('g4-blackRook')), findsOneWidget); + expect(find.byKey(const Key('g4-blackrook')), findsOneWidget); await playMove(tester, boardRect, 'g4', 'h4', orientation: orientation); - expect(find.byKey(const Key('h4-blackRook')), findsOneWidget); + expect(find.byKey(const Key('h4-blackrook')), findsOneWidget); expect(find.text('Best move!'), findsOneWidget); // wait for line reply and move animation @@ -342,7 +342,7 @@ void main() { await playMove(tester, boardRect, 'b4', 'h4', orientation: orientation); - expect(find.byKey(const Key('h4-blackRook')), findsOneWidget); + expect(find.byKey(const Key('h4-blackrook')), findsOneWidget); expect( find.text('Puzzle complete!'), findsOneWidget, @@ -418,13 +418,13 @@ void main() { // wait for the puzzle to load await tester.pump(const Duration(milliseconds: 200)); - expect(find.byType(cg.Board), findsOneWidget); + expect(find.byType(Chessboard), findsOneWidget); expect(find.text('Your turn'), findsOneWidget); // await for first move to be played and view solution button to appear await tester.pump(const Duration(seconds: 5)); - expect(find.byKey(const Key('g4-blackRook')), findsOneWidget); + expect(find.byKey(const Key('g4-blackrook')), findsOneWidget); expect(find.byIcon(Icons.help), findsOneWidget); await tester.tap(find.byIcon(Icons.help)); @@ -433,8 +433,8 @@ void main() { await tester.pump(const Duration(seconds: 1)); await tester.pumpAndSettle(); - expect(find.byKey(const Key('h4-blackRook')), findsOneWidget); - expect(find.byKey(const Key('h8-whiteQueen')), findsOneWidget); + expect(find.byKey(const Key('h4-blackrook')), findsOneWidget); + expect(find.byKey(const Key('h8-whitequeen')), findsOneWidget); expect( find.text('Puzzle complete!'), findsOneWidget, diff --git a/test/view/puzzle/storm_screen_test.dart b/test/view/puzzle/storm_screen_test.dart index 7e38602448..4bd3b50cd6 100644 --- a/test/view/puzzle/storm_screen_test.dart +++ b/test/view/puzzle/storm_screen_test.dart @@ -1,4 +1,5 @@ -import 'package:chessground/chessground.dart' as cg; +import 'package:chessground/chessground.dart'; +import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -60,7 +61,7 @@ void main() { await tester.pumpWidget(app); - expect(find.byType(cg.Board), findsOneWidget); + expect(find.byType(Chessboard), findsOneWidget); expect( find.text('You play the white pieces in all puzzles'), findsWidgets, @@ -85,43 +86,43 @@ void main() { await tester.pumpWidget(app); // before the first move is played, puzzle is not interactable - expect(find.byKey(const Key('h5-whiteRook')), findsOneWidget); - await tester.tap(find.byKey(const Key('h5-whiteRook'))); + expect(find.byKey(const Key('h5-whiterook')), findsOneWidget); + await tester.tap(find.byKey(const Key('h5-whiterook'))); await tester.pump(); expect(find.byKey(const Key('h5-selected')), findsNothing); // wait for first move to be played await tester.pump(const Duration(seconds: 1)); - expect(find.byKey(const Key('g8-blackKing')), findsOneWidget); + expect(find.byKey(const Key('g8-blackking')), findsOneWidget); - final boardRect = tester.getRect(find.byType(cg.Board)); + final boardRect = tester.getRect(find.byType(Chessboard)); await playMove( tester, boardRect, 'h5', 'h7', - orientation: cg.Side.white, + orientation: Side.white, ); await tester.pump(const Duration(milliseconds: 500)); await tester.pumpAndSettle(); - expect(find.byKey(const Key('h7-whiteRook')), findsOneWidget); - expect(find.byKey(const Key('d1-blackQueen')), findsOneWidget); + expect(find.byKey(const Key('h7-whiterook')), findsOneWidget); + expect(find.byKey(const Key('d1-blackqueen')), findsOneWidget); await playMove( tester, boardRect, 'e3', 'g1', - orientation: cg.Side.white, + orientation: Side.white, ); await tester.pump(const Duration(milliseconds: 500)); // should have loaded next puzzle - expect(find.byKey(const Key('h6-blackKing')), findsOneWidget); + expect(find.byKey(const Key('h6-blackking')), findsOneWidget); }, variant: kPlatformVariant, ); @@ -142,14 +143,14 @@ void main() { // wait for first move to be played await tester.pump(const Duration(seconds: 1)); - final boardRect = tester.getRect(find.byType(cg.Board)); + final boardRect = tester.getRect(find.byType(Chessboard)); await playMove( tester, boardRect, 'h5', 'h7', - orientation: cg.Side.white, + orientation: Side.white, ); await tester.pump(const Duration(milliseconds: 500)); @@ -158,12 +159,12 @@ void main() { boardRect, 'e3', 'g1', - orientation: cg.Side.white, + orientation: Side.white, ); await tester.pump(const Duration(milliseconds: 500)); // should have loaded next puzzle - expect(find.byKey(const Key('h6-blackKing')), findsOneWidget); + expect(find.byKey(const Key('h6-blackking')), findsOneWidget); await tester.tap(find.text('End run')); await tester.pumpAndSettle(); @@ -185,12 +186,12 @@ void main() { await tester.pumpWidget(app); await tester.pump(const Duration(seconds: 1)); - final boardRect = tester.getRect(find.byType(cg.Board)); + final boardRect = tester.getRect(find.byType(Chessboard)); await playMove(tester, boardRect, 'h5', 'h6'); await tester.pump(const Duration(milliseconds: 500)); - expect(find.byKey(const Key('h6-blackKing')), findsOneWidget); + expect(find.byKey(const Key('h6-blackking')), findsOneWidget); }); }); } From d300dee48389145d283d28a39358491f4ee25667 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Sun, 28 Jul 2024 23:49:56 +0200 Subject: [PATCH 104/979] feat: add board editor --- .../board_editor/board_editor_controller.dart | 149 +++++++ lib/src/model/engine/evaluation_service.dart | 1 + .../view/board_editor/board_editor_menu.dart | 65 +++ .../board_editor/board_editor_screen.dart | 378 ++++++++++++++++++ lib/src/view/tools/tools_tab_screen.dart | 27 ++ test/view/board_editor/board_editor_test.dart | 278 +++++++++++++ 6 files changed, 898 insertions(+) create mode 100644 lib/src/model/board_editor/board_editor_controller.dart create mode 100644 lib/src/view/board_editor/board_editor_menu.dart create mode 100644 lib/src/view/board_editor/board_editor_screen.dart create mode 100644 test/view/board_editor/board_editor_test.dart diff --git a/lib/src/model/board_editor/board_editor_controller.dart b/lib/src/model/board_editor/board_editor_controller.dart new file mode 100644 index 0000000000..b8d08c6aa7 --- /dev/null +++ b/lib/src/model/board_editor/board_editor_controller.dart @@ -0,0 +1,149 @@ +import 'package:chessground/chessground.dart'; +import 'package:dartchess/dartchess.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'board_editor_controller.freezed.dart'; +part 'board_editor_controller.g.dart'; + +@riverpod +class BoardEditorController extends _$BoardEditorController { + @override + BoardEditorState build() { + return BoardEditorState( + orientation: Side.white, + sideToPlay: Side.white, + pieces: readFen(kInitialFEN).lock, + unmovedRooks: SquareSet.corners, + editorPointerMode: EditorPointerMode.drag, + pieceToAddOnEdit: null, + ); + } + + void updateMode(EditorPointerMode mode, [Piece? pieceToAddOnEdit]) { + state = state.copyWith( + editorPointerMode: mode, + pieceToAddOnEdit: pieceToAddOnEdit, + ); + } + + void discardPiece(Square square) { + _updatePosition(state.pieces.remove(square)); + } + + void movePiece(Square? origin, Square destination, Piece piece) { + if (origin != destination) { + _updatePosition( + state.pieces.remove(origin ?? destination).add(destination, piece), + ); + } + } + + void editSquare(Square square) { + final piece = state.pieceToAddOnEdit; + if (piece != null) { + _updatePosition(state.pieces.add(square, piece)); + } else { + discardPiece(square); + } + } + + void flipBoard() { + state = state.copyWith( + orientation: state.orientation.opposite, + ); + } + + void setSideToPlay(Side side) { + state = state.copyWith( + sideToPlay: side, + ); + } + + void loadFen(String fen) { + _updatePosition(readFen(fen).lock); + } + + void _updatePosition(IMap pieces) { + state = state.copyWith(pieces: pieces); + } + + void setWhiteKingsideCastlingAllowed(bool allowed) { + _setRookUnmoved(Square.h1, allowed); + } + + void setWhiteQueensideCastlingAllowed(bool allowed) { + _setRookUnmoved(Square.a1, allowed); + } + + void setBlackKingsideCastlingAllowed(bool allowed) { + _setRookUnmoved(Square.h8, allowed); + } + + void setBlackQueensideCastlingAllowed(bool allowed) { + _setRookUnmoved(Square.a8, allowed); + } + + void _setRookUnmoved(Square square, bool unmoved) { + state = state.copyWith( + unmovedRooks: unmoved + ? state.unmovedRooks.withSquare(square) + : state.unmovedRooks.withoutSquare(square), + ); + } +} + +@freezed +class BoardEditorState with _$BoardEditorState { + const BoardEditorState._(); + + const factory BoardEditorState({ + required Side orientation, + required Side sideToPlay, + required IMap pieces, + required SquareSet unmovedRooks, + required EditorPointerMode editorPointerMode, + + /// When null, clears squares when in edit mode. Has no effect in drag mode. + required Piece? pieceToAddOnEdit, + }) = _BoardEditorState; + + bool get canWhiteCastleKingside => unmovedRooks.has(Square.h1); + bool get canWhiteCastleQueenside => unmovedRooks.has(Square.a1); + bool get canBlackCastleKingside => unmovedRooks.has(Square.h8); + bool get canBlackCastleQueenside => unmovedRooks.has(Square.a8); + + Setup get _setup { + final boardFen = writeFen(pieces.unlock); + final board = Board.parseFen(boardFen); + return Setup( + board: board, + unmovedRooks: unmovedRooks, + turn: sideToPlay == Side.white ? Side.white : Side.black, + halfmoves: 0, + fullmoves: 1, + ); + } + + Piece? get activePieceOnEdit => + editorPointerMode == EditorPointerMode.edit ? pieceToAddOnEdit : null; + + bool get deletePiecesActive => + editorPointerMode == EditorPointerMode.edit && pieceToAddOnEdit == null; + + String get fen => _setup.fen; + + String? get pgn { + try { + final position = Chess.fromSetup(_setup); + return PgnGame( + headers: {'FEN': position.fen}, + moves: PgnNode(), + comments: [], + ).makePgn(); + } catch (_) { + return null; + } + } +} diff --git a/lib/src/model/engine/evaluation_service.dart b/lib/src/model/engine/evaluation_service.dart index 1721654562..8ac29f59fb 100644 --- a/lib/src/model/engine/evaluation_service.dart +++ b/lib/src/model/engine/evaluation_service.dart @@ -27,6 +27,7 @@ final defaultEngineCores = const engineSupportedVariants = { Variant.standard, Variant.chess960, + Variant.fromPosition, }; class EvaluationService { diff --git a/lib/src/view/board_editor/board_editor_menu.dart b/lib/src/view/board_editor/board_editor_menu.dart new file mode 100644 index 0000000000..470f0eaa56 --- /dev/null +++ b/lib/src/view/board_editor/board_editor_menu.dart @@ -0,0 +1,65 @@ +import 'package:dartchess/dartchess.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/board_editor/board_editor_controller.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/widgets/list.dart'; +import 'package:lichess_mobile/src/widgets/settings.dart'; + +class BoardEditorMenu extends ConsumerWidget { + const BoardEditorMenu(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final boardEditorController = ref.watch(boardEditorControllerProvider); + final boardEditorNotifier = + ref.read(boardEditorControllerProvider.notifier); + + return Container( + color: Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoTheme.of(context).barBackgroundColor + : Theme.of(context).bottomAppBarTheme.color, + child: SafeArea( + child: Padding( + padding: Styles.bodyPadding, + child: ListView( + shrinkWrap: true, + children: [ + SwitchSettingTile( + title: const Text('White to play'), + value: boardEditorController.sideToPlay == Side.white, + onChanged: (white) => boardEditorNotifier.setSideToPlay( + white ? Side.white : Side.black, + ), + ), + PlatformListTile( + title: Text('Castling Rights', style: Styles.sectionTitle), + ), + SwitchSettingTile( + title: const Text('White O-O'), + value: boardEditorController.canWhiteCastleKingside, + onChanged: boardEditorNotifier.setWhiteKingsideCastlingAllowed, + ), + SwitchSettingTile( + title: const Text('White O-O-O'), + value: boardEditorController.canWhiteCastleQueenside, + onChanged: boardEditorNotifier.setWhiteQueensideCastlingAllowed, + ), + SwitchSettingTile( + title: const Text('Black O-O'), + value: boardEditorController.canBlackCastleKingside, + onChanged: boardEditorNotifier.setBlackKingsideCastlingAllowed, + ), + SwitchSettingTile( + title: const Text('Black O-O-O'), + value: boardEditorController.canBlackCastleQueenside, + onChanged: boardEditorNotifier.setBlackQueensideCastlingAllowed, + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/src/view/board_editor/board_editor_screen.dart b/lib/src/view/board_editor/board_editor_screen.dart new file mode 100644 index 0000000000..baeb52251d --- /dev/null +++ b/lib/src/view/board_editor/board_editor_screen.dart @@ -0,0 +1,378 @@ +import 'package:chessground/chessground.dart'; +import 'package:dartchess/dartchess.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/constants.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; +import 'package:lichess_mobile/src/model/board_editor/board_editor_controller.dart'; +import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; +import 'package:lichess_mobile/src/styles/lichess_icons.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/utils/screen.dart'; +import 'package:lichess_mobile/src/utils/share.dart'; +import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; +import 'package:lichess_mobile/src/view/board_editor/board_editor_menu.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; +import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; +import 'package:lichess_mobile/src/widgets/platform.dart'; + +class BoardEditorScreen extends StatelessWidget { + const BoardEditorScreen({super.key}); + + @override + Widget build(BuildContext context) { + return PlatformWidget( + androidBuilder: _androidBuilder, + iosBuilder: _iosBuilder, + ); + } + + Widget _androidBuilder(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(context.l10n.boardEditor), + ), + body: const _Body(), + ); + } + + Widget _iosBuilder(BuildContext context) { + return CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + backgroundColor: Styles.cupertinoScaffoldColor.resolveFrom(context), + border: null, + middle: Text(context.l10n.boardEditor), + ), + child: const _Body(), + ); + } +} + +class _Body extends ConsumerWidget { + const _Body(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + ref.read(boardEditorControllerProvider.notifier); + + final boardEditorState = ref.watch(boardEditorControllerProvider); + + return Column( + children: [ + Expanded( + child: SafeArea( + bottom: false, + child: LayoutBuilder( + builder: (context, constraints) { + final aspectRatio = constraints.biggest.aspectRatio; + + final defaultBoardSize = constraints.biggest.shortestSide; + final isTablet = isTabletOrLarger(context); + final remainingHeight = + constraints.maxHeight - defaultBoardSize; + final isSmallScreen = + remainingHeight < kSmallRemainingHeightLeftBoardThreshold; + final boardSize = isTablet || isSmallScreen + ? defaultBoardSize - kTabletBoardTableSidePadding * 2 + : defaultBoardSize; + + final direction = + aspectRatio > 1 ? Axis.horizontal : Axis.vertical; + + return Flex( + direction: direction, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + _PieceMenu( + boardSize, + direction: flipAxis(direction), + side: boardEditorState.orientation.opposite, + isTablet: isTablet, + ), + _BoardEditor( + boardSize, + orientation: boardEditorState.orientation, + isTablet: isTablet, + pieces: boardEditorState.pieces.unlock, + ), + _PieceMenu( + boardSize, + direction: flipAxis(direction), + side: boardEditorState.orientation, + isTablet: isTablet, + ), + ], + ); + }, + ), + ), + ), + const _BottomBar(), + ], + ); + } +} + +class _BoardEditor extends ConsumerWidget { + const _BoardEditor( + this.boardSize, { + required this.isTablet, + required this.orientation, + required this.pieces, + }); + + final double boardSize; + final bool isTablet; + final Side orientation; + final Pieces pieces; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final boardPrefs = ref.watch(boardPreferencesProvider); + + return ChessboardEditor( + size: boardSize, + pieces: pieces, + orientation: orientation, + settings: ChessboardEditorSettings( + pieceAssets: boardPrefs.pieceSet.assets, + colorScheme: boardPrefs.boardTheme.colors, + enableCoordinates: boardPrefs.coordinates, + borderRadius: isTablet + ? const BorderRadius.all(Radius.circular(4.0)) + : BorderRadius.zero, + boxShadow: isTablet ? boardShadows : const [], + ), + pointerMode: ref.watch(boardEditorControllerProvider).editorPointerMode, + onDiscardedPiece: + ref.read(boardEditorControllerProvider.notifier).discardPiece, + onDroppedPiece: + ref.read(boardEditorControllerProvider.notifier).movePiece, + onEditedSquare: + ref.read(boardEditorControllerProvider.notifier).editSquare, + ); + } +} + +class _PieceMenu extends ConsumerStatefulWidget { + const _PieceMenu( + this.boardSize, { + required this.direction, + required this.side, + required this.isTablet, + }); + + final double boardSize; + + final Axis direction; + + final Side side; + + final bool isTablet; + + @override + ConsumerState<_PieceMenu> createState() => _PieceMenuState(); +} + +class _PieceMenuState extends ConsumerState<_PieceMenu> { + @override + Widget build(BuildContext context) { + final boardPrefs = ref.watch(boardPreferencesProvider); + + final squareSize = widget.boardSize / 8; + + return Container( + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + borderRadius: widget.isTablet + ? const BorderRadius.all(Radius.circular(4.0)) + : BorderRadius.zero, + boxShadow: widget.isTablet ? boardShadows : const [], + ), + child: ColoredBox( + color: Theme.of(context).disabledColor, + child: Flex( + direction: widget.direction, + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: squareSize, + height: squareSize, + child: ColoredBox( + key: Key('drag-button-${widget.side.name}'), + color: ref + .watch(boardEditorControllerProvider) + .editorPointerMode == + EditorPointerMode.drag + ? Theme.of(context).colorScheme.tertiary + : Colors.transparent, + child: GestureDetector( + onTap: () => ref + .read(boardEditorControllerProvider.notifier) + .updateMode(EditorPointerMode.drag), + child: Icon( + CupertinoIcons.hand_draw, + size: 0.9 * squareSize, + ), + ), + ), + ), + ...Role.values.map( + (role) { + final piece = Piece(role: role, color: widget.side); + final pieceWidget = PieceWidget( + piece: piece, + size: squareSize, + pieceAssets: boardPrefs.pieceSet.assets, + ); + + return ColoredBox( + key: Key( + 'piece-button-${piece.color.name}-${piece.role.name}', + ), + color: ref + .read(boardEditorControllerProvider) + .activePieceOnEdit == + piece + ? Theme.of(context).colorScheme.primary + : Colors.transparent, + child: GestureDetector( + child: Draggable( + data: Piece(role: role, color: widget.side), + feedback: PieceDragFeedback( + piece: piece, + squareSize: squareSize, + pieceAssets: boardPrefs.pieceSet.assets, + ), + child: pieceWidget, + onDragEnd: (_) => ref + .read(boardEditorControllerProvider.notifier) + .updateMode(EditorPointerMode.drag), + ), + onTap: () => ref + .read(boardEditorControllerProvider.notifier) + .updateMode(EditorPointerMode.edit, piece), + ), + ); + }, + ), + SizedBox( + key: Key('delete-button-${widget.side.name}'), + width: squareSize, + height: squareSize, + child: ColoredBox( + color: + ref.read(boardEditorControllerProvider).deletePiecesActive + ? Theme.of(context).colorScheme.error + : Colors.transparent, + child: GestureDetector( + onTap: () => { + ref + .read(boardEditorControllerProvider.notifier) + .updateMode(EditorPointerMode.edit, null), + }, + child: Icon( + CupertinoIcons.delete, + size: 0.8 * squareSize, + ), + ), + ), + ), + ], + ), + ), + ); + } +} + +class _BottomBar extends ConsumerWidget { + const _BottomBar(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final pgn = ref.watch(boardEditorControllerProvider).pgn; + final orientation = ref.read(boardEditorControllerProvider).orientation; + + return Container( + color: Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoTheme.of(context).barBackgroundColor + : Theme.of(context).bottomAppBarTheme.color, + child: SafeArea( + top: false, + child: SizedBox( + height: kBottomBarHeight, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Expanded( + child: BottomBarButton( + label: context.l10n.menu, + onTap: () => showAdaptiveBottomSheet( + context: context, + isScrollControlled: true, + showDragHandle: true, + builder: (BuildContext context) => const BoardEditorMenu(), + ), + icon: Icons.menu, + ), + ), + Expanded( + child: BottomBarButton( + key: const Key('flip-button'), + label: context.l10n.flipBoard, + onTap: ref + .read(boardEditorControllerProvider.notifier) + .flipBoard, + icon: Icons.flip, + ), + ), + Expanded( + child: BottomBarButton( + label: context.l10n.analysis, + key: const Key('analysis-board-button'), + onTap: pgn != null + ? () { + pushPlatformRoute( + context, + rootNavigator: true, + builder: (context) => AnalysisScreen( + pgnOrId: pgn, + options: AnalysisOptions( + isLocalEvaluationAllowed: true, + variant: Variant.fromPosition, + orientation: orientation, + id: standaloneAnalysisId, + ), + ), + ); + } + : null, + icon: LichessIcons.microscope, + ), + ), + Expanded( + child: BottomBarButton( + label: context.l10n.mobileSharePositionAsFEN, + onTap: () => launchShareDialog( + context, + text: ref.read(boardEditorControllerProvider).fen, + ), + icon: Icons.share, + ), + ), + const SizedBox(height: 26.0), + const SizedBox(height: 20.0), + ], + ), + ), + ), + ); + } +} diff --git a/lib/src/view/tools/tools_tab_screen.dart b/lib/src/view/tools/tools_tab_screen.dart index 689f3bd60c..8af42df2ed 100644 --- a/lib/src/view/tools/tools_tab_screen.dart +++ b/lib/src/view/tools/tools_tab_screen.dart @@ -10,6 +10,7 @@ import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_position_choice_screen.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; +import 'package:lichess_mobile/src/view/board_editor/board_editor_screen.dart'; import 'package:lichess_mobile/src/view/clock/clock_screen.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; @@ -107,6 +108,32 @@ class _Body extends StatelessWidget { ), ), ), + Padding( + padding: Theme.of(context).platform == TargetPlatform.android + ? const EdgeInsets.only(bottom: 16.0) + : EdgeInsets.zero, + child: PlatformListTile( + leading: Icon( + Icons.edit, + size: Styles.mainListTileIconSize, + color: Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoTheme.of(context).primaryColor + : Theme.of(context).colorScheme.primary, + ), + title: Padding( + padding: tilePadding, + child: Text(context.l10n.boardEditor, style: Styles.callout), + ), + trailing: Theme.of(context).platform == TargetPlatform.iOS + ? const CupertinoListTileChevron() + : null, + onTap: () => pushPlatformRoute( + context, + builder: (context) => const BoardEditorScreen(), + rootNavigator: true, + ), + ), + ), Padding( padding: Theme.of(context).platform == TargetPlatform.android ? const EdgeInsets.only(bottom: 16.0) diff --git a/test/view/board_editor/board_editor_test.dart b/test/view/board_editor/board_editor_test.dart new file mode 100644 index 0000000000..656512ade8 --- /dev/null +++ b/test/view/board_editor/board_editor_test.dart @@ -0,0 +1,278 @@ +import 'package:chessground/chessground.dart'; +import 'package:dartchess/dartchess.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:lichess_mobile/src/model/board_editor/board_editor_controller.dart'; +import 'package:lichess_mobile/src/view/board_editor/board_editor_screen.dart'; +import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; + +import '../../test_app.dart'; + +void main() { + group('Board Editor', () { + testWidgets('Displays initial FEN on start', (tester) async { + await setupBoardEditor(tester); + + final editor = tester.widget( + find.byType(ChessboardEditor), + ); + expect(editor.pieces, readFen(kInitialFEN)); + expect(editor.orientation, Side.white); + expect(editor.pointerMode, EditorPointerMode.drag); + + // Legal position, so allowed top open analysis board + expect( + tester + .widget( + find.byKey(const Key('analysis-board-button')), + ) + .onTap, + isNotNull, + ); + }); + + testWidgets('Flip board', (tester) async { + await setupBoardEditor(tester); + + await tester.tap(find.byKey(const Key('flip-button'))); + await tester.pump(); + + expect( + tester + .widget( + find.byType(ChessboardEditor), + ) + .orientation, + Side.black, + ); + }); + + testWidgets('Side to play and castling rights', (tester) async { + final boardEditorController = await setupBoardEditor(tester); + + await tester.tap(find.byKey(const Key('flip-button'))); + await tester.pump(); + + boardEditorController.setSideToPlay(Side.black); + expect( + boardEditorController.state.fen, + 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR b KQkq - 0 1', + ); + + boardEditorController.setWhiteKingsideCastlingAllowed(false); + expect( + boardEditorController.state.fen, + 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR b Qkq - 0 1', + ); + + boardEditorController.setWhiteQueensideCastlingAllowed(false); + expect( + boardEditorController.state.fen, + 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR b kq - 0 1', + ); + + boardEditorController.setBlackKingsideCastlingAllowed(false); + expect( + boardEditorController.state.fen, + 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR b q - 0 1', + ); + + boardEditorController.setBlackQueensideCastlingAllowed(false); + expect( + boardEditorController.state.fen, + 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR b - - 0 1', + ); + + boardEditorController.setWhiteKingsideCastlingAllowed(true); + expect( + boardEditorController.state.fen, + 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR b K - 0 1', + ); + }); + + testWidgets('Castling rights ignored when rook is missing', (tester) async { + final boardEditorController = await setupBoardEditor(tester); + + // Starting position, but with all rooks removed + boardEditorController + .loadFen('1nbqkbn1/pppppppp/8/8/8/8/PPPPPPPP/1NBQKBN1'); + + // By default, all castling rights are true, but since there are no rooks, the final FEN should have no castling rights + expect( + boardEditorController.state.fen, + '1nbqkbn1/pppppppp/8/8/8/8/PPPPPPPP/1NBQKBN1 w - - 0 1', + ); + }); + + testWidgets('Can drag pieces to new squares', (tester) async { + final boardEditorController = await setupBoardEditor(tester); + + // Two legal moves by white + await dragFromTo(tester, 'e2', 'e4'); + await dragFromTo(tester, 'd2', 'd4'); + + // Illegal move by black + await dragFromTo(tester, 'a8', 'a6'); + + // White queen captures white bishop + await dragFromTo(tester, 'd1', 'c1'); + + expect( + boardEditorController.state.fen, + // Obtained by playing the moves above on lichess.org/editor + '1nbqkbnr/pppppppp/r7/8/3PP3/8/PPP2PPP/RNQ1KBNR w KQk - 0 1', + ); + }); + + testWidgets('illegal position cannot be analyzed', (tester) async { + await setupBoardEditor(tester); + + // White queen "captures" white king => illegal position + await dragFromTo(tester, 'd1', 'e1'); + + expect( + tester + .widget( + find.byKey(const Key('analysis-board-button')), + ) + .onTap, + isNull, + ); + }); + + testWidgets('Delete pieces via bin button', (tester) async { + final boardEditorController = await setupBoardEditor(tester); + + await tester.tap(find.byKey(const Key('delete-button-white'))); + await tester.pump(); + + await tapSquare(tester, 'e2'); + expect( + boardEditorController.state.fen, + 'rnbqkbnr/pppppppp/8/8/8/8/PPPP1PPP/RNBQKBNR w KQkq - 0 1', + ); + + // Change back to drag mode -> tapping has no effect anymore + await tester.tap(find.byKey(const Key('drag-button-white'))); + await tester.pump(); + await tapSquare(tester, 'e3'); + expect( + boardEditorController.state.fen, + 'rnbqkbnr/pppppppp/8/8/8/8/PPPP1PPP/RNBQKBNR w KQkq - 0 1', + ); + + // Now remove all of black's pawns + await tester.tap(find.byKey(const Key('delete-button-black'))); + await tester.pump(); + await panFromTo(tester, 'a7', 'h7'); + + expect( + boardEditorController.state.fen, + 'rnbqkbnr/8/8/8/8/8/PPPP1PPP/RNBQKBNR w KQkq - 0 1', + ); + }); + + testWidgets('Add pieces via tap and pan', (tester) async { + final boardEditorController = await setupBoardEditor(tester); + + await tester.tap(find.byKey(const Key('piece-button-white-queen'))); + await panFromTo(tester, 'a1', 'a8'); + await tester.tap(find.byKey(const Key('piece-button-black-rook'))); + await tapSquare(tester, 'h1'); + await tapSquare(tester, 'h3'); + + expect( + boardEditorController.state.fen, + 'Qnbqkbnr/Qppppppp/Q7/Q7/Q7/Q6r/QPPPPPPP/QNBQKBNr w k - 0 1', + ); + }); + + testWidgets('Drag pieces onto the board', (tester) async { + final boardEditorController = await setupBoardEditor(tester); + + // Start by pressing bin button, dragging a piece should override this + await tester.tap(find.byKey(const Key('delete-button-black'))); + await tester.pump(); + + final pieceButtonOffset = + tester.getCenter(find.byKey(const Key('piece-button-white-pawn'))); + await tester.dragFrom( + pieceButtonOffset, + tester.getCenter(find.byKey(const Key('d3-empty'))) - pieceButtonOffset, + ); + await tester.dragFrom( + pieceButtonOffset, + tester.getCenter(find.byKey(const Key('d1-whitequeen'))) - + pieceButtonOffset, + ); + + expect( + boardEditorController.state.editorPointerMode, + EditorPointerMode.drag, + ); + + expect( + boardEditorController.state.fen, + 'rnbqkbnr/pppppppp/8/8/8/3P4/PPPPPPPP/RNBPKBNR w KQkq - 0 1', + ); + }); + }); +} + +Future setupBoardEditor(WidgetTester tester) async { + final boardEditorController = BoardEditorController(); + final app = await buildTestApp( + tester, + home: const BoardEditorScreen(), + overrides: [ + boardEditorControllerProvider.overrideWith(() => boardEditorController), + ], + ); + await tester.pumpWidget(app); + return boardEditorController; +} + +Future dragFromTo( + WidgetTester tester, + String from, + String to, +) async { + final fromOffset = squareOffset(tester, Square.fromName(from)); + + await tester.dragFrom( + fromOffset, + squareOffset(tester, Square.fromName(to)) - fromOffset, + ); + await tester.pumpAndSettle(); +} + +Future panFromTo( + WidgetTester tester, + String from, + String to, +) async { + final fromOffset = squareOffset(tester, Square.fromName(from)); + + await tester.timedDragFrom( + fromOffset, + squareOffset(tester, Square.fromName(to)) - fromOffset, + const Duration(seconds: 1), + ); + await tester.pumpAndSettle(); +} + +Future tapSquare(WidgetTester tester, String square) async { + await tester.tapAt(squareOffset(tester, Square.fromName(square))); + await tester.pumpAndSettle(); +} + +Offset squareOffset(WidgetTester tester, Square square) { + final editor = find.byType(ChessboardEditor); + final squareSize = tester.getSize(editor).width / 8; + + return tester.getTopLeft(editor) + + Offset( + square.file.value * squareSize + squareSize / 2, + (7 - square.rank.value) * squareSize + squareSize / 2, + ); +} From 32bac2c72c44fc4eaca672e3c206a985276dec2f Mon Sep 17 00:00:00 2001 From: Mauritz Date: Sat, 3 Aug 2024 17:20:22 +0200 Subject: [PATCH 105/979] refactor: minor changes --- .../opening_explorer/opening_explorer.dart | 10 ++++++---- .../opening_explorer_repository.dart | 20 +++++++++---------- lib/src/view/user/player_screen.dart | 6 +++--- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/lib/src/model/opening_explorer/opening_explorer.dart b/lib/src/model/opening_explorer/opening_explorer.dart index db680eb325..8a5df93336 100644 --- a/lib/src/model/opening_explorer/opening_explorer.dart +++ b/lib/src/model/opening_explorer/opening_explorer.dart @@ -12,13 +12,14 @@ class OpeningExplorer with _$OpeningExplorer { const OpeningExplorer._(); const factory OpeningExplorer({ - LightOpening? opening, required int white, required int draws, required int black, required IList moves, IList? topGames, IList? recentGames, + LightOpening? opening, + int? queuePosition, }) = _OpeningExplorer; factory OpeningExplorer.fromJson(Map json) => @@ -32,12 +33,13 @@ class OpeningMove with _$OpeningMove { const factory OpeningMove({ required String uci, required String san, - int? averageRating, - int? averageOpponentRating, - int? performance, required int white, required int draws, required int black, + int? averageRating, + int? averageOpponentRating, + int? performance, + Game? game, }) = _OpeningMove; factory OpeningMove.fromJson(Map json) => diff --git a/lib/src/model/opening_explorer/opening_explorer_repository.dart b/lib/src/model/opening_explorer/opening_explorer_repository.dart index 4d277887d1..0d1641cfb5 100644 --- a/lib/src/model/opening_explorer/opening_explorer_repository.dart +++ b/lib/src/model/opening_explorer/opening_explorer_repository.dart @@ -28,16 +28,16 @@ Future openingExplorer( ratings: prefs.lichessDb.ratings, since: prefs.lichessDb.since, ), - OpeningDatabase.player => - OpeningExplorerRepository(client).getPlayerDatabase( - fen, - // null check handled by widget - usernameOrId: prefs.playerDb.usernameOrId!, - color: prefs.playerDb.side, - speeds: prefs.playerDb.speeds, - modes: prefs.playerDb.modes, - since: prefs.playerDb.since, - ), + OpeningDatabase.player => OpeningExplorerRepository(client) + .getPlayerDatabase( + fen, + // null check handled by widget + usernameOrId: prefs.playerDb.usernameOrId!, + color: prefs.playerDb.side, + speeds: prefs.playerDb.speeds, + modes: prefs.playerDb.modes, + since: prefs.playerDb.since, + ) }, ); } diff --git a/lib/src/view/user/player_screen.dart b/lib/src/view/user/player_screen.dart index 06e69f63c8..4719e6926e 100644 --- a/lib/src/view/user/player_screen.dart +++ b/lib/src/view/user/player_screen.dart @@ -88,7 +88,7 @@ class _SearchButton extends StatelessWidget { @override Widget build(BuildContext context) { - void onTapUser(LightUser user) => pushPlatformRoute( + void onUserTap(LightUser user) => pushPlatformRoute( context, builder: (ctx) => UserScreen(user: user), ); @@ -101,7 +101,7 @@ class _SearchButton extends StatelessWidget { onTap: () => pushPlatformRoute( context, fullscreenDialog: true, - builder: (_) => SearchScreen(onUserTap: onTapUser), + builder: (_) => SearchScreen(onUserTap: onUserTap), ), ), iosBuilder: (context) => CupertinoSearchTextField( @@ -110,7 +110,7 @@ class _SearchButton extends StatelessWidget { onTap: () => pushPlatformRoute( context, fullscreenDialog: true, - builder: (_) => SearchScreen(onUserTap: onTapUser), + builder: (_) => SearchScreen(onUserTap: onUserTap), ), ), ); From 66cc6f935f2c3b5752b6c09afd79245bdbf3a68b Mon Sep 17 00:00:00 2001 From: Mauritz Date: Sat, 3 Aug 2024 17:48:13 +0200 Subject: [PATCH 106/979] fix: format + remove unused imports + pass parameter correctly --- .../opening_explorer_repository.dart | 20 +++++++++---------- .../opening_explorer_screen.dart | 3 ++- .../opening_explorer_settings.dart | 3 --- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/lib/src/model/opening_explorer/opening_explorer_repository.dart b/lib/src/model/opening_explorer/opening_explorer_repository.dart index 0d1641cfb5..b0ff7ce795 100644 --- a/lib/src/model/opening_explorer/opening_explorer_repository.dart +++ b/lib/src/model/opening_explorer/opening_explorer_repository.dart @@ -28,16 +28,16 @@ Future openingExplorer( ratings: prefs.lichessDb.ratings, since: prefs.lichessDb.since, ), - OpeningDatabase.player => OpeningExplorerRepository(client) - .getPlayerDatabase( - fen, - // null check handled by widget - usernameOrId: prefs.playerDb.usernameOrId!, - color: prefs.playerDb.side, - speeds: prefs.playerDb.speeds, - modes: prefs.playerDb.modes, - since: prefs.playerDb.since, - ) + OpeningDatabase.player => + OpeningExplorerRepository(client).getPlayerDatabase( + fen, + // null check handled by widget + usernameOrId: prefs.playerDb.usernameOrId!, + color: prefs.playerDb.side, + speeds: prefs.playerDb.speeds, + modes: prefs.playerDb.modes, + since: prefs.playerDb.since, + ) }, ); } diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index d5bb84bc0b..ad789f9717 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -518,8 +518,9 @@ class _GameTile extends ConsumerWidget { return; case OpeningDatabase.lichess: case OpeningDatabase.player: + final gameId = GameId(game.id); final archivedGame = await ref.read( - archivedGameProvider(id: GameId(game.id)).future, + archivedGameProvider(id: gameId).future, ); if (context.mounted) { pushPlatformRoute( diff --git a/lib/src/view/opening_explorer/opening_explorer_settings.dart b/lib/src/view/opening_explorer/opening_explorer_settings.dart index 6a43935922..d444f82773 100644 --- a/lib/src/view/opening_explorer/opening_explorer_settings.dart +++ b/lib/src/view/opening_explorer/opening_explorer_settings.dart @@ -1,5 +1,4 @@ import 'package:dartchess/dartchess.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -9,10 +8,8 @@ import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_prefe import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; -import 'package:lichess_mobile/src/view/user/player_screen.dart'; import 'package:lichess_mobile/src/view/user/search_screen.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; class OpeningExplorerSettings extends ConsumerWidget { const OpeningExplorerSettings(this.pgn, this.options); From 14c1f57205c99af0f37fd9fc9b93225531babf67 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Mon, 5 Aug 2024 09:24:55 +0200 Subject: [PATCH 107/979] refactor: address a few pr comments * use ref.watch instead of ref.read inside build method * define count getter directly in class * use padding instead of container --- lib/src/model/game/game_filter.dart | 16 +++++++++------- lib/src/view/user/game_history_screen.dart | 10 ++++++---- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/lib/src/model/game/game_filter.dart b/lib/src/model/game/game_filter.dart index e88bef9641..7c544122d7 100644 --- a/lib/src/model/game/game_filter.dart +++ b/lib/src/model/game/game_filter.dart @@ -18,19 +18,21 @@ class GameFilter extends _$GameFilter { perfs: filter.perfs, side: filter.side, ); - - int countFiltersInUse() { - final fields = [state.perfs, state.side]; - return fields - .where((field) => field is Iterable ? field.isNotEmpty : field != null) - .length; - } } @freezed class GameFilterState with _$GameFilterState { + const GameFilterState._(); + const factory GameFilterState({ @Default(ISet.empty()) ISet perfs, Side? side, }) = _GameFilterState; + + int get count { + final fields = [perfs, side]; + return fields + .where((field) => field is Iterable ? field.isNotEmpty : field != null) + .length; + } } diff --git a/lib/src/view/user/game_history_screen.dart b/lib/src/view/user/game_history_screen.dart index 8906d2e46f..c2ad384889 100644 --- a/lib/src/view/user/game_history_screen.dart +++ b/lib/src/view/user/game_history_screen.dart @@ -29,14 +29,16 @@ class GameHistoryScreen extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final session = ref.read(authSessionProvider); final username = user?.name ?? session?.user.name; + final filtersInUse = ref.watch( + gameFilterProvider(filter: gameFilter).select( + (state) => state.count, + ), + ); final title = Text( username != null ? '$username ${context.l10n.games.toLowerCase()}' : context.l10n.games, ); - final filtersInUse = ref - .read(gameFilterProvider(filter: gameFilter).notifier) - .countFiltersInUse(); final filterBtn = Stack( alignment: Alignment.center, children: [ @@ -335,7 +337,7 @@ class _FilterGamesState extends ConsumerState<_FilterGames> { ), ); - return Container( + return Padding( padding: const EdgeInsets.all(16), child: DraggableScrollableSheet( initialChildSize: .7, From d5e858120913a309ac5752c8cd3f32de430d027d Mon Sep 17 00:00:00 2001 From: Mauritz Date: Mon, 5 Aug 2024 09:38:41 +0200 Subject: [PATCH 108/979] feat: if no filter is used, set title to number of games --- lib/src/view/user/game_history_screen.dart | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/lib/src/view/user/game_history_screen.dart b/lib/src/view/user/game_history_screen.dart index c2ad384889..9e2f0d1c77 100644 --- a/lib/src/view/user/game_history_screen.dart +++ b/lib/src/view/user/game_history_screen.dart @@ -34,11 +34,20 @@ class GameHistoryScreen extends ConsumerWidget { (state) => state.count, ), ); - final title = Text( - username != null - ? '$username ${context.l10n.games.toLowerCase()}' - : context.l10n.games, + final nbGamesAsync = ref.watch( + userNumberOfGamesProvider(user, isOnline: isOnline), ); + final title = filtersInUse == 0 + ? nbGamesAsync.when( + data: (nbGames) => Text(context.l10n.nbGames(nbGames)), + loading: () => const ButtonLoadingIndicator(), + error: (e, s) => Text(context.l10n.mobileAllGames), + ) + : Text( + username != null + ? '$username ${context.l10n.games.toLowerCase()}' + : context.l10n.games, + ); final filterBtn = Stack( alignment: Alignment.center, children: [ From d418bd804c92590495a69ac9d1940de7cf3b96c3 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Mon, 5 Aug 2024 10:35:34 +0200 Subject: [PATCH 109/979] refactor: make UserGameHistory not depend on recent games providers --- lib/src/model/game/game_history.dart | 39 +++++++++++++++----------- lib/src/view/home/home_tab_screen.dart | 4 +-- lib/src/view/user/recent_games.dart | 2 +- 3 files changed, 25 insertions(+), 20 deletions(-) diff --git a/lib/src/model/game/game_history.dart b/lib/src/model/game/game_history.dart index c4e0fed031..12895158a6 100644 --- a/lib/src/model/game/game_history.dart +++ b/lib/src/model/game/game_history.dart @@ -33,19 +33,15 @@ const _nbPerPage = 20; /// stored locally are fetched instead. @riverpod Future> myRecentGames( - MyRecentGamesRef ref, { - GameFilterState filter = const GameFilterState(), -}) async { + MyRecentGamesRef ref, +) async { final online = await ref .watch(connectivityChangesProvider.selectAsync((c) => c.isOnline)); final session = ref.watch(authSessionProvider); if (session != null && online) { return ref.withClientCacheFor( - (client) => GameRepository(client).getUserGames( - session.user.id, - max: kNumberOfRecentGames, - filter: filter, - ), + (client) => GameRepository(client) + .getUserGames(session.user.id, max: kNumberOfRecentGames), const Duration(hours: 1), ); } else { @@ -68,10 +64,9 @@ Future> myRecentGames( Future> userRecentGames( UserRecentGamesRef ref, { required UserId userId, - GameFilterState filter = const GameFilterState(), }) { return ref.withClientCacheFor( - (client) => GameRepository(client).getUserGames(userId, filter: filter), + (client) => GameRepository(client).getUserGames(userId), // cache is important because the associated widget is in a [ListView] and // the provider may be instanciated multiple times in a short period of time // (e.g. when scrolling) @@ -128,15 +123,25 @@ class UserGameHistory extends _$UserGameHistory { }); final session = ref.watch(authSessionProvider); + final online = await ref + .watch(connectivityChangesProvider.selectAsync((c) => c.isOnline)); + final storage = ref.watch(gameStorageProvider); - final recentGames = userId != null - ? ref.read( - userRecentGamesProvider( - userId: userId, - filter: filter, - ).future, + final id = userId ?? session?.user.id; + final recentGames = id != null && online + ? ref.withClient( + (client) => GameRepository(client).getUserGames(id, filter: filter), ) - : ref.read(myRecentGamesProvider(filter: filter).future); + : storage.page(userId: id, max: kNumberOfRecentGames).then( + (value) => value + // we can assume that `youAre` is not null either for logged + // in users or for anonymous users + .map( + (e) => + (game: e.game.data, pov: e.game.youAre ?? Side.white), + ) + .toIList(), + ); _list.addAll(await recentGames); diff --git a/lib/src/view/home/home_tab_screen.dart b/lib/src/view/home/home_tab_screen.dart index 2b27803a77..6298778c27 100644 --- a/lib/src/view/home/home_tab_screen.dart +++ b/lib/src/view/home/home_tab_screen.dart @@ -179,7 +179,7 @@ class _HomeScreenState extends ConsumerState with RouteAware { Future _refreshData() { return Future.wait([ ref.refresh(accountProvider.future), - ref.refresh(myRecentGamesProvider().future), + ref.refresh(myRecentGamesProvider.future), ref.refresh(ongoingGamesProvider.future), ]); } @@ -214,7 +214,7 @@ class _HomeBody extends ConsumerWidget { data: (status) { final session = ref.watch(authSessionProvider); final isTablet = isTabletOrLarger(context); - final emptyRecent = ref.watch(myRecentGamesProvider()).maybeWhen( + final emptyRecent = ref.watch(myRecentGamesProvider).maybeWhen( data: (data) => data.isEmpty, orElse: () => false, ); diff --git a/lib/src/view/user/recent_games.dart b/lib/src/view/user/recent_games.dart index 53618dc26e..599e23746a 100644 --- a/lib/src/view/user/recent_games.dart +++ b/lib/src/view/user/recent_games.dart @@ -30,7 +30,7 @@ class RecentGamesWidget extends ConsumerWidget { final recentGames = user != null ? ref.watch(userRecentGamesProvider(userId: user!.id)) - : ref.watch(myRecentGamesProvider()); + : ref.watch(myRecentGamesProvider); final nbOfGames = ref .watch( From 0b565b1298ab9e8d589aa088e9437ac198e5d695 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 5 Aug 2024 10:47:52 +0200 Subject: [PATCH 110/979] Apply riverpod best practices --- .../board_editor/board_editor_screen.dart | 37 +++++++++---------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/lib/src/view/board_editor/board_editor_screen.dart b/lib/src/view/board_editor/board_editor_screen.dart index baeb52251d..9bcf729b34 100644 --- a/lib/src/view/board_editor/board_editor_screen.dart +++ b/lib/src/view/board_editor/board_editor_screen.dart @@ -57,8 +57,6 @@ class _Body extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - ref.read(boardEditorControllerProvider.notifier); - final boardEditorState = ref.watch(boardEditorControllerProvider); return Column( @@ -134,6 +132,7 @@ class _BoardEditor extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final editorState = ref.watch(boardEditorControllerProvider); final boardPrefs = ref.watch(boardPreferencesProvider); return ChessboardEditor( @@ -149,13 +148,14 @@ class _BoardEditor extends ConsumerWidget { : BorderRadius.zero, boxShadow: isTablet ? boardShadows : const [], ), - pointerMode: ref.watch(boardEditorControllerProvider).editorPointerMode, - onDiscardedPiece: - ref.read(boardEditorControllerProvider.notifier).discardPiece, - onDroppedPiece: - ref.read(boardEditorControllerProvider.notifier).movePiece, - onEditedSquare: - ref.read(boardEditorControllerProvider.notifier).editSquare, + pointerMode: editorState.editorPointerMode, + onDiscardedPiece: (Square square) => + ref.read(boardEditorControllerProvider.notifier).discardPiece(square), + onDroppedPiece: (Square? origin, Square dest, Piece piece) => ref + .read(boardEditorControllerProvider.notifier) + .movePiece(origin, dest, piece), + onEditedSquare: (Square square) => + ref.read(boardEditorControllerProvider.notifier).editSquare(square), ); } } @@ -184,6 +184,7 @@ class _PieceMenuState extends ConsumerState<_PieceMenu> { @override Widget build(BuildContext context) { final boardPrefs = ref.watch(boardPreferencesProvider); + final editorState = ref.watch(boardEditorControllerProvider); final squareSize = widget.boardSize / 8; @@ -268,10 +269,9 @@ class _PieceMenuState extends ConsumerState<_PieceMenu> { width: squareSize, height: squareSize, child: ColoredBox( - color: - ref.read(boardEditorControllerProvider).deletePiecesActive - ? Theme.of(context).colorScheme.error - : Colors.transparent, + color: editorState.deletePiecesActive + ? Theme.of(context).colorScheme.error + : Colors.transparent, child: GestureDetector( onTap: () => { ref @@ -297,8 +297,7 @@ class _BottomBar extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final pgn = ref.watch(boardEditorControllerProvider).pgn; - final orientation = ref.read(boardEditorControllerProvider).orientation; + final editorState = ref.watch(boardEditorControllerProvider); return Container( color: Theme.of(context).platform == TargetPlatform.iOS @@ -337,17 +336,17 @@ class _BottomBar extends ConsumerWidget { child: BottomBarButton( label: context.l10n.analysis, key: const Key('analysis-board-button'), - onTap: pgn != null + onTap: editorState.pgn != null ? () { pushPlatformRoute( context, rootNavigator: true, builder: (context) => AnalysisScreen( - pgnOrId: pgn, + pgnOrId: editorState.pgn!, options: AnalysisOptions( isLocalEvaluationAllowed: true, variant: Variant.fromPosition, - orientation: orientation, + orientation: editorState.orientation, id: standaloneAnalysisId, ), ), @@ -362,7 +361,7 @@ class _BottomBar extends ConsumerWidget { label: context.l10n.mobileSharePositionAsFEN, onTap: () => launchShareDialog( context, - text: ref.read(boardEditorControllerProvider).fen, + text: editorState.fen, ), icon: Icons.share, ), From 09e1251edeb43f5d249a90b5d891de4e5c18fa9a Mon Sep 17 00:00:00 2001 From: Mauritz Date: Mon, 5 Aug 2024 11:02:15 +0200 Subject: [PATCH 111/979] feat: support filtering games offline --- lib/src/model/game/game_filter.dart | 2 +- lib/src/model/game/game_history.dart | 2 +- lib/src/model/game/game_storage.dart | 37 ++++++++++++++-------- lib/src/view/user/game_history_screen.dart | 2 +- test/model/game/mock_game_storage.dart | 2 ++ 5 files changed, 28 insertions(+), 17 deletions(-) diff --git a/lib/src/model/game/game_filter.dart b/lib/src/model/game/game_filter.dart index 7c544122d7..d0ec1abcd6 100644 --- a/lib/src/model/game/game_filter.dart +++ b/lib/src/model/game/game_filter.dart @@ -1,4 +1,4 @@ -import 'package:chessground/chessground.dart'; +import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; diff --git a/lib/src/model/game/game_history.dart b/lib/src/model/game/game_history.dart index 12895158a6..0ba4a51b5f 100644 --- a/lib/src/model/game/game_history.dart +++ b/lib/src/model/game/game_history.dart @@ -132,7 +132,7 @@ class UserGameHistory extends _$UserGameHistory { ? ref.withClient( (client) => GameRepository(client).getUserGames(id, filter: filter), ) - : storage.page(userId: id, max: kNumberOfRecentGames).then( + : storage.page(userId: id, filter: filter).then( (value) => value // we can assume that `youAre` is not null either for logged // in users or for anonymous users diff --git a/lib/src/model/game/game_storage.dart b/lib/src/model/game/game_storage.dart index 7d957682d0..b28ff6728b 100644 --- a/lib/src/model/game/game_storage.dart +++ b/lib/src/model/game/game_storage.dart @@ -4,6 +4,7 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:lichess_mobile/src/db/database.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/game/archived_game.dart'; +import 'package:lichess_mobile/src/model/game/game_filter.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:sqflite/sqflite.dart'; @@ -44,6 +45,7 @@ class GameStorage { UserId? userId, DateTime? until, int max = 20, + GameFilterState filter = const GameFilterState(), }) async { final list = await _db.query( kGameStorageTable, @@ -59,20 +61,27 @@ class GameStorage { limit: max, ); - return list.map((e) { - final raw = e['data']! as String; - final json = jsonDecode(raw); - if (json is! Map) { - throw const FormatException( - '[GameStorage] cannot fetch game: expected an object', - ); - } - return ( - userId: UserId(e['userId']! as String), - lastModified: DateTime.parse(e['lastModified']! as String), - game: ArchivedGame.fromJson(json), - ); - }).toIList(); + return list + .map((e) { + final raw = e['data']! as String; + final json = jsonDecode(raw); + if (json is! Map) { + throw const FormatException( + '[GameStorage] cannot fetch game: expected an object', + ); + } + return ( + userId: UserId(e['userId']! as String), + lastModified: DateTime.parse(e['lastModified']! as String), + game: ArchivedGame.fromJson(json), + ); + }) + .where( + (e) => + filter.perfs.isEmpty || filter.perfs.contains(e.game.meta.perf), + ) + .where((e) => filter.side == null || filter.side == e.game.youAre) + .toIList(); } Future fetch({ diff --git a/lib/src/view/user/game_history_screen.dart b/lib/src/view/user/game_history_screen.dart index 9e2f0d1c77..2bda25bdc5 100644 --- a/lib/src/view/user/game_history_screen.dart +++ b/lib/src/view/user/game_history_screen.dart @@ -1,4 +1,4 @@ -import 'package:chessground/chessground.dart'; +import 'package:dartchess/dartchess.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; diff --git a/test/model/game/mock_game_storage.dart b/test/model/game/mock_game_storage.dart index 0ed719063f..c2fd45f075 100644 --- a/test/model/game/mock_game_storage.dart +++ b/test/model/game/mock_game_storage.dart @@ -1,6 +1,7 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/game/archived_game.dart'; +import 'package:lichess_mobile/src/model/game/game_filter.dart'; import 'package:lichess_mobile/src/model/game/game_storage.dart'; class MockGameStorage implements GameStorage { @@ -19,6 +20,7 @@ class MockGameStorage implements GameStorage { UserId? userId, DateTime? until, int max = 10, + GameFilterState filter = const GameFilterState(), }) { return Future.value(IList()); } From 770e6ffb3b8b198deb9140d2ecc4ad4f48cf4697 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 5 Aug 2024 12:13:43 +0200 Subject: [PATCH 112/979] Simplify board editor menu, fix style on iOS --- .../board_editor/board_editor_controller.dart | 47 ++++---- lib/src/model/common/uci.dart | 2 +- .../view/board_editor/board_editor_menu.dart | 101 +++++++++++------- .../board_editor/board_editor_screen.dart | 22 ++-- ...est.dart => board_editor_screen_test.dart} | 10 +- 5 files changed, 108 insertions(+), 74 deletions(-) rename test/view/board_editor/{board_editor_test.dart => board_editor_screen_test.dart} (95%) diff --git a/lib/src/model/board_editor/board_editor_controller.dart b/lib/src/model/board_editor/board_editor_controller.dart index b8d08c6aa7..21d72fe7f2 100644 --- a/lib/src/model/board_editor/board_editor_controller.dart +++ b/lib/src/model/board_editor/board_editor_controller.dart @@ -69,20 +69,21 @@ class BoardEditorController extends _$BoardEditorController { state = state.copyWith(pieces: pieces); } - void setWhiteKingsideCastlingAllowed(bool allowed) { - _setRookUnmoved(Square.h1, allowed); - } - - void setWhiteQueensideCastlingAllowed(bool allowed) { - _setRookUnmoved(Square.a1, allowed); - } - - void setBlackKingsideCastlingAllowed(bool allowed) { - _setRookUnmoved(Square.h8, allowed); - } - - void setBlackQueensideCastlingAllowed(bool allowed) { - _setRookUnmoved(Square.a8, allowed); + void setCastling(Side side, CastlingSide castlingSide, bool allowed) { + switch (side) { + case Side.white: + if (castlingSide == CastlingSide.king) { + _setRookUnmoved(Square.h1, allowed); + } else { + _setRookUnmoved(Square.a1, allowed); + } + case Side.black: + if (castlingSide == CastlingSide.king) { + _setRookUnmoved(Square.h8, allowed); + } else { + _setRookUnmoved(Square.a8, allowed); + } + } } void _setRookUnmoved(Square square, bool unmoved) { @@ -109,10 +110,17 @@ class BoardEditorState with _$BoardEditorState { required Piece? pieceToAddOnEdit, }) = _BoardEditorState; - bool get canWhiteCastleKingside => unmovedRooks.has(Square.h1); - bool get canWhiteCastleQueenside => unmovedRooks.has(Square.a1); - bool get canBlackCastleKingside => unmovedRooks.has(Square.h8); - bool get canBlackCastleQueenside => unmovedRooks.has(Square.a8); + bool isCastlingAllowed(Side side, CastlingSide castlingSide) => + switch (side) { + Side.white => switch (castlingSide) { + CastlingSide.king => unmovedRooks.has(Square.h1), + CastlingSide.queen => unmovedRooks.has(Square.a1), + }, + Side.black => switch (castlingSide) { + CastlingSide.king => unmovedRooks.has(Square.h8), + CastlingSide.queen => unmovedRooks.has(Square.a8), + }, + }; Setup get _setup { final boardFen = writeFen(pieces.unlock); @@ -134,6 +142,9 @@ class BoardEditorState with _$BoardEditorState { String get fen => _setup.fen; + /// Returns the PGN representation of the current position if it is valid. + /// + /// Returns `null` if the position is invalid. String? get pgn { try { final position = Chess.fromSetup(_setup); diff --git a/lib/src/model/common/uci.dart b/lib/src/model/common/uci.dart index e747bf960b..4ff3c5a1e7 100644 --- a/lib/src/model/common/uci.dart +++ b/lib/src/model/common/uci.dart @@ -37,7 +37,7 @@ class UciCharPair with _$UciCharPair { String.fromCharCode(35 + f), String.fromCharCode( p != null - ? 35 + 64 + 8 * _promotionRoles.indexOf(p) + t.file.value + ? 35 + 64 + 8 * _promotionRoles.indexOf(p) + t.file : 35 + t, ), ), diff --git a/lib/src/view/board_editor/board_editor_menu.dart b/lib/src/view/board_editor/board_editor_menu.dart index 470f0eaa56..879a9841a9 100644 --- a/lib/src/view/board_editor/board_editor_menu.dart +++ b/lib/src/view/board_editor/board_editor_menu.dart @@ -1,61 +1,82 @@ import 'package:dartchess/dartchess.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/board_editor/board_editor_controller.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/widgets/list.dart'; -import 'package:lichess_mobile/src/widgets/settings.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; class BoardEditorMenu extends ConsumerWidget { const BoardEditorMenu(); @override Widget build(BuildContext context, WidgetRef ref) { - final boardEditorController = ref.watch(boardEditorControllerProvider); + final editorState = ref.watch(boardEditorControllerProvider); final boardEditorNotifier = ref.read(boardEditorControllerProvider.notifier); - return Container( - color: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoTheme.of(context).barBackgroundColor - : Theme.of(context).bottomAppBarTheme.color, - child: SafeArea( - child: Padding( - padding: Styles.bodyPadding, - child: ListView( - shrinkWrap: true, + return SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 8.0), + child: SizedBox( + width: double.infinity, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - SwitchSettingTile( - title: const Text('White to play'), - value: boardEditorController.sideToPlay == Side.white, - onChanged: (white) => boardEditorNotifier.setSideToPlay( - white ? Side.white : Side.black, + Padding( + padding: Styles.horizontalBodyPadding, + child: Wrap( + spacing: 8.0, + children: Side.values.map((side) { + return ChoiceChip( + label: Text( + side == Side.white + ? context.l10n.whitePlays + : context.l10n.blackPlays, + ), + selected: editorState.sideToPlay == side, + onSelected: (selected) { + if (selected) { + boardEditorNotifier.setSideToPlay(side); + } + }, + ); + }).toList(), ), ), - PlatformListTile( - title: Text('Castling Rights', style: Styles.sectionTitle), - ), - SwitchSettingTile( - title: const Text('White O-O'), - value: boardEditorController.canWhiteCastleKingside, - onChanged: boardEditorNotifier.setWhiteKingsideCastlingAllowed, - ), - SwitchSettingTile( - title: const Text('White O-O-O'), - value: boardEditorController.canWhiteCastleQueenside, - onChanged: boardEditorNotifier.setWhiteQueensideCastlingAllowed, - ), - SwitchSettingTile( - title: const Text('Black O-O'), - value: boardEditorController.canBlackCastleKingside, - onChanged: boardEditorNotifier.setBlackKingsideCastlingAllowed, - ), - SwitchSettingTile( - title: const Text('Black O-O-O'), - value: boardEditorController.canBlackCastleQueenside, - onChanged: boardEditorNotifier.setBlackQueensideCastlingAllowed, + Padding( + padding: Styles.bodySectionPadding, + child: Text(context.l10n.castling, style: Styles.subtitle), ), + ...Side.values.map((side) { + return Padding( + padding: Styles.horizontalBodyPadding, + child: Wrap( + spacing: 8.0, + children: [CastlingSide.king, CastlingSide.queen] + .map((castlingSide) { + return ChoiceChip( + label: Text( + castlingSide == CastlingSide.king + ? side == Side.white + ? context.l10n.whiteCastlingKingside + : context.l10n.blackCastlingKingside + : 'O-O-O', + ), + selected: + editorState.isCastlingAllowed(side, castlingSide), + onSelected: (selected) { + boardEditorNotifier.setCastling( + side, + castlingSide, + selected, + ); + }, + ); + }).toList(), + ), + ); + }), ], ), ), diff --git a/lib/src/view/board_editor/board_editor_screen.dart b/lib/src/view/board_editor/board_editor_screen.dart index 9bcf729b34..c8cbf36af1 100644 --- a/lib/src/view/board_editor/board_editor_screen.dart +++ b/lib/src/view/board_editor/board_editor_screen.dart @@ -8,7 +8,6 @@ import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/board_editor/board_editor_controller.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; -import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; @@ -212,7 +211,7 @@ class _PieceMenuState extends ConsumerState<_PieceMenu> { .watch(boardEditorControllerProvider) .editorPointerMode == EditorPointerMode.drag - ? Theme.of(context).colorScheme.tertiary + ? context.lichessColors.good : Colors.transparent, child: GestureDetector( onTap: () => ref @@ -270,7 +269,7 @@ class _PieceMenuState extends ConsumerState<_PieceMenu> { height: squareSize, child: ColoredBox( color: editorState.deletePiecesActive - ? Theme.of(context).colorScheme.error + ? context.lichessColors.error : Colors.transparent, child: GestureDetector( onTap: () => { @@ -301,7 +300,10 @@ class _BottomBar extends ConsumerWidget { return Container( color: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoTheme.of(context).barBackgroundColor + ? CupertinoDynamicColor.resolve( + CupertinoColors.tertiarySystemGroupedBackground, + context, + ) : Theme.of(context).bottomAppBarTheme.color, child: SafeArea( top: false, @@ -315,11 +317,9 @@ class _BottomBar extends ConsumerWidget { label: context.l10n.menu, onTap: () => showAdaptiveBottomSheet( context: context, - isScrollControlled: true, - showDragHandle: true, builder: (BuildContext context) => const BoardEditorMenu(), ), - icon: Icons.menu, + icon: Icons.tune, ), ), Expanded( @@ -329,7 +329,7 @@ class _BottomBar extends ConsumerWidget { onTap: ref .read(boardEditorControllerProvider.notifier) .flipBoard, - icon: Icons.flip, + icon: CupertinoIcons.arrow_2_squarepath, ), ), Expanded( @@ -353,7 +353,7 @@ class _BottomBar extends ConsumerWidget { ); } : null, - icon: LichessIcons.microscope, + icon: Icons.biotech, ), ), Expanded( @@ -363,7 +363,9 @@ class _BottomBar extends ConsumerWidget { context, text: editorState.fen, ), - icon: Icons.share, + icon: Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoIcons.share + : Icons.share, ), ), const SizedBox(height: 26.0), diff --git a/test/view/board_editor/board_editor_test.dart b/test/view/board_editor/board_editor_screen_test.dart similarity index 95% rename from test/view/board_editor/board_editor_test.dart rename to test/view/board_editor/board_editor_screen_test.dart index 656512ade8..d80147bdfc 100644 --- a/test/view/board_editor/board_editor_test.dart +++ b/test/view/board_editor/board_editor_screen_test.dart @@ -59,31 +59,31 @@ void main() { 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR b KQkq - 0 1', ); - boardEditorController.setWhiteKingsideCastlingAllowed(false); + boardEditorController.setCastling(Side.white, CastlingSide.king, false); expect( boardEditorController.state.fen, 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR b Qkq - 0 1', ); - boardEditorController.setWhiteQueensideCastlingAllowed(false); + boardEditorController.setCastling(Side.white, CastlingSide.queen, false); expect( boardEditorController.state.fen, 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR b kq - 0 1', ); - boardEditorController.setBlackKingsideCastlingAllowed(false); + boardEditorController.setCastling(Side.black, CastlingSide.king, false); expect( boardEditorController.state.fen, 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR b q - 0 1', ); - boardEditorController.setBlackQueensideCastlingAllowed(false); + boardEditorController.setCastling(Side.black, CastlingSide.queen, false); expect( boardEditorController.state.fen, 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR b - - 0 1', ); - boardEditorController.setWhiteKingsideCastlingAllowed(true); + boardEditorController.setCastling(Side.white, CastlingSide.king, true); expect( boardEditorController.state.fen, 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR b K - 0 1', From fd6ad5237b69a268b25153d703ac1015e3665544 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 5 Aug 2024 12:23:00 +0200 Subject: [PATCH 113/979] Tweak board editor menu alignment --- lib/src/view/board_editor/board_editor_menu.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/view/board_editor/board_editor_menu.dart b/lib/src/view/board_editor/board_editor_menu.dart index 879a9841a9..919ba274cf 100644 --- a/lib/src/view/board_editor/board_editor_menu.dart +++ b/lib/src/view/board_editor/board_editor_menu.dart @@ -21,7 +21,7 @@ class BoardEditorMenu extends ConsumerWidget { width: double.infinity, child: Column( mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, children: [ Padding( padding: Styles.horizontalBodyPadding, From 9be06fdd882b78e1ee5ac56fa5806fe1e17faed2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Nowak?= Date: Mon, 5 Aug 2024 12:23:31 +0200 Subject: [PATCH 114/979] feat: remove deleting database --- .tool-versions | 1 + lib/src/db/database.dart | 12 ------------ lib/src/view/settings/settings_screen.dart | 14 +------------- 3 files changed, 2 insertions(+), 25 deletions(-) create mode 100644 .tool-versions diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000000..4b4932a526 --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +flutter 3.23.0-0.1.pre \ No newline at end of file diff --git a/lib/src/db/database.dart b/lib/src/db/database.dart index 90e42d0781..27b84c0940 100644 --- a/lib/src/db/database.dart +++ b/lib/src/db/database.dart @@ -51,18 +51,6 @@ Future getDbSizeInBytes(GetDbSizeInBytesRef ref) async { return dbFile.length(); } -/// Clears all database rows regardless of TTL. -Future clearDatabase(Database db) async { - await Future.wait([ - _deleteEntry(db, 'puzzle_batchs'), - _deleteEntry(db, 'puzzle'), - _deleteEntry(db, 'correspondence_game'), - _deleteEntry(db, 'game'), - _deleteEntry(db, 'chat_read_messages'), - ]); - await db.execute('VACUUM'); -} - Future openDb(DatabaseFactory dbFactory, String path) async { return dbFactory.openDatabase( path, diff --git a/lib/src/view/settings/settings_screen.dart b/lib/src/view/settings/settings_screen.dart index de8ea9054a..2b2891b1ac 100644 --- a/lib/src/view/settings/settings_screen.dart +++ b/lib/src/view/settings/settings_screen.dart @@ -396,18 +396,12 @@ class _Body extends ConsumerWidget { ), PlatformListTile( leading: const Icon(Icons.storage), - title: const Text('Delete local database'), + title: const Text('Local database size'), additionalInfo: dbSize.hasValue ? Text(_getSizeString(dbSize.value)) : null, trailing: Theme.of(context).platform == TargetPlatform.iOS ? const CupertinoListTileChevron() : Text(_getSizeString(dbSize.value)), - onTap: () => showConfirmDialog( - context, - title: const Text('Delete local database'), - onConfirm: (_) => _deleteDatabase(ref), - isDestructiveAction: true, - ), ), ], ), @@ -484,12 +478,6 @@ class _Body extends ConsumerWidget { double _bytesToMB(int bytes) => bytes * 0.000001; - Future _deleteDatabase(WidgetRef ref) async { - final db = ref.read(databaseProvider); - await clearDatabase(db); - ref.invalidate(getDbSizeInBytesProvider); - } - void _refreshData(WidgetRef ref) { ref.invalidate(getDbSizeInBytesProvider); } From 499e710c466422bbe976852cefb0d904b30c3962 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Nowak?= Date: Mon, 5 Aug 2024 12:40:02 +0200 Subject: [PATCH 115/979] fix: remove unused method --- lib/src/db/database.dart | 8 -------- pubspec.lock | 26 +++++++++++++------------- 2 files changed, 13 insertions(+), 21 deletions(-) diff --git a/lib/src/db/database.dart b/lib/src/db/database.dart index 27b84c0940..fd3e8ca06a 100644 --- a/lib/src/db/database.dart +++ b/lib/src/db/database.dart @@ -165,14 +165,6 @@ Future _deleteOldEntries(Database db, String table, Duration ttl) async { ); } -Future _deleteEntry(Database db, String table) async { - if (!await _doesTableExist(db, table)) { - return; - } - - await db.delete(table); -} - Future _doesTableExist(Database db, String table) async { final tableExists = await db.rawQuery( "SELECT name FROM sqlite_master WHERE type='table' AND name='$table'", diff --git a/pubspec.lock b/pubspec.lock index 872bc8b96b..78f8d1886b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "5aaf60d96c4cd00fe7f21594b5ad6a1b699c80a27420f8a837f4d68473ef09e3" + sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834 url: "https://pub.dev" source: hosted - version: "68.0.0" + version: "72.0.0" _flutterfire_internals: dependency: transitive description: @@ -21,15 +21,15 @@ packages: dependency: transitive description: dart source: sdk - version: "0.1.5" + version: "0.3.2" analyzer: dependency: transitive description: name: analyzer - sha256: "21f1d3720fd1c70316399d5e2bccaebb415c434592d778cce8acb967b8578808" + sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139 url: "https://pub.dev" source: hosted - version: "6.5.0" + version: "6.7.0" analyzer_plugin: dependency: transitive description: @@ -862,10 +862,10 @@ packages: dependency: transitive description: name: macros - sha256: a8403c89b36483b4cbf9f1fcd24562f483cb34a5c9bf101cf2b0d8a083cf1239 + sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" url: "https://pub.dev" source: hosted - version: "0.1.0-main.5" + version: "0.1.2-main.4" matcher: dependency: transitive description: @@ -886,10 +886,10 @@ packages: dependency: "direct main" description: name: meta - sha256: "25dfcaf170a0190f47ca6355bdd4552cb8924b430512ff0cafb8db9bd41fe33b" + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.14.0" + version: "1.15.0" mime: dependency: transitive description: @@ -1388,10 +1388,10 @@ packages: dependency: transitive description: name: test_api - sha256: "2419f20b0c8677b2d67c8ac4d1ac7372d862dc6c460cdbb052b40155408cd794" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.7.1" + version: "0.7.2" timeago: dependency: "direct main" description: @@ -1540,10 +1540,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "7475cb4dd713d57b6f7464c0e13f06da0d535d8b2067e188962a59bac2cf280b" + sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc url: "https://pub.dev" source: hosted - version: "14.2.2" + version: "14.2.4" wakelock_plus: dependency: "direct main" description: From a67d6358922e48db4c90351a8c900f2e7965e569 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 5 Aug 2024 12:59:49 +0200 Subject: [PATCH 116/979] Rework load a position screen to allow loading a position into editor --- .../board_editor/board_editor_controller.dart | 4 +- .../view/board_editor/board_editor_menu.dart | 23 +++--- .../board_editor/board_editor_screen.dart | 73 +++++++++++------- .../load_position_screen.dart} | 77 +++++++++++-------- lib/src/view/tools/tools_tab_screen.dart | 38 ++++----- 5 files changed, 128 insertions(+), 87 deletions(-) rename lib/src/view/{analysis/analysis_position_choice_screen.dart => tools/load_position_screen.dart} (67%) diff --git a/lib/src/model/board_editor/board_editor_controller.dart b/lib/src/model/board_editor/board_editor_controller.dart index 21d72fe7f2..98218a21d6 100644 --- a/lib/src/model/board_editor/board_editor_controller.dart +++ b/lib/src/model/board_editor/board_editor_controller.dart @@ -10,11 +10,11 @@ part 'board_editor_controller.g.dart'; @riverpod class BoardEditorController extends _$BoardEditorController { @override - BoardEditorState build() { + BoardEditorState build(String? initialFen) { return BoardEditorState( orientation: Side.white, sideToPlay: Side.white, - pieces: readFen(kInitialFEN).lock, + pieces: readFen(initialFen ?? kInitialFEN).lock, unmovedRooks: SquareSet.corners, editorPointerMode: EditorPointerMode.drag, pieceToAddOnEdit: null, diff --git a/lib/src/view/board_editor/board_editor_menu.dart b/lib/src/view/board_editor/board_editor_menu.dart index 919ba274cf..c523dabb55 100644 --- a/lib/src/view/board_editor/board_editor_menu.dart +++ b/lib/src/view/board_editor/board_editor_menu.dart @@ -6,13 +6,14 @@ import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; class BoardEditorMenu extends ConsumerWidget { - const BoardEditorMenu(); + const BoardEditorMenu({required this.initialFen, super.key}); + + final String? initialFen; @override Widget build(BuildContext context, WidgetRef ref) { - final editorState = ref.watch(boardEditorControllerProvider); - final boardEditorNotifier = - ref.read(boardEditorControllerProvider.notifier); + final editorController = boardEditorControllerProvider(initialFen); + final editorState = ref.watch(editorController); return SafeArea( child: Padding( @@ -37,7 +38,9 @@ class BoardEditorMenu extends ConsumerWidget { selected: editorState.sideToPlay == side, onSelected: (selected) { if (selected) { - boardEditorNotifier.setSideToPlay(side); + ref + .read(editorController.notifier) + .setSideToPlay(side); } }, ); @@ -66,11 +69,11 @@ class BoardEditorMenu extends ConsumerWidget { selected: editorState.isCastlingAllowed(side, castlingSide), onSelected: (selected) { - boardEditorNotifier.setCastling( - side, - castlingSide, - selected, - ); + ref.read(editorController.notifier).setCastling( + side, + castlingSide, + selected, + ); }, ); }).toList(), diff --git a/lib/src/view/board_editor/board_editor_screen.dart b/lib/src/view/board_editor/board_editor_screen.dart index c8cbf36af1..cded0f7f0b 100644 --- a/lib/src/view/board_editor/board_editor_screen.dart +++ b/lib/src/view/board_editor/board_editor_screen.dart @@ -20,7 +20,9 @@ import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; class BoardEditorScreen extends StatelessWidget { - const BoardEditorScreen({super.key}); + const BoardEditorScreen({super.key, this.initialFen}); + + final String? initialFen; @override Widget build(BuildContext context) { @@ -35,7 +37,7 @@ class BoardEditorScreen extends StatelessWidget { appBar: AppBar( title: Text(context.l10n.boardEditor), ), - body: const _Body(), + body: _Body(initialFen), ); } @@ -46,17 +48,20 @@ class BoardEditorScreen extends StatelessWidget { border: null, middle: Text(context.l10n.boardEditor), ), - child: const _Body(), + child: _Body(initialFen), ); } } class _Body extends ConsumerWidget { - const _Body(); + const _Body(this.initialFen); + + final String? initialFen; @override Widget build(BuildContext context, WidgetRef ref) { - final boardEditorState = ref.watch(boardEditorControllerProvider); + final boardEditorState = + ref.watch(boardEditorControllerProvider(initialFen)); return Column( children: [ @@ -88,18 +93,21 @@ class _Body extends ConsumerWidget { children: [ _PieceMenu( boardSize, + initialFen: initialFen, direction: flipAxis(direction), side: boardEditorState.orientation.opposite, isTablet: isTablet, ), _BoardEditor( boardSize, + initialFen: initialFen, orientation: boardEditorState.orientation, isTablet: isTablet, pieces: boardEditorState.pieces.unlock, ), _PieceMenu( boardSize, + initialFen: initialFen, direction: flipAxis(direction), side: boardEditorState.orientation, isTablet: isTablet, @@ -110,7 +118,7 @@ class _Body extends ConsumerWidget { ), ), ), - const _BottomBar(), + _BottomBar(initialFen), ], ); } @@ -119,11 +127,13 @@ class _Body extends ConsumerWidget { class _BoardEditor extends ConsumerWidget { const _BoardEditor( this.boardSize, { + required this.initialFen, required this.isTablet, required this.orientation, required this.pieces, }); + final String? initialFen; final double boardSize; final bool isTablet; final Side orientation; @@ -131,7 +141,7 @@ class _BoardEditor extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final editorState = ref.watch(boardEditorControllerProvider); + final editorState = ref.watch(boardEditorControllerProvider(initialFen)); final boardPrefs = ref.watch(boardPreferencesProvider); return ChessboardEditor( @@ -148,13 +158,15 @@ class _BoardEditor extends ConsumerWidget { boxShadow: isTablet ? boardShadows : const [], ), pointerMode: editorState.editorPointerMode, - onDiscardedPiece: (Square square) => - ref.read(boardEditorControllerProvider.notifier).discardPiece(square), + onDiscardedPiece: (Square square) => ref + .read(boardEditorControllerProvider(initialFen).notifier) + .discardPiece(square), onDroppedPiece: (Square? origin, Square dest, Piece piece) => ref - .read(boardEditorControllerProvider.notifier) + .read(boardEditorControllerProvider(initialFen).notifier) .movePiece(origin, dest, piece), - onEditedSquare: (Square square) => - ref.read(boardEditorControllerProvider.notifier).editSquare(square), + onEditedSquare: (Square square) => ref + .read(boardEditorControllerProvider(initialFen).notifier) + .editSquare(square), ); } } @@ -162,11 +174,14 @@ class _BoardEditor extends ConsumerWidget { class _PieceMenu extends ConsumerStatefulWidget { const _PieceMenu( this.boardSize, { + required this.initialFen, required this.direction, required this.side, required this.isTablet, }); + final String? initialFen; + final double boardSize; final Axis direction; @@ -183,7 +198,8 @@ class _PieceMenuState extends ConsumerState<_PieceMenu> { @override Widget build(BuildContext context) { final boardPrefs = ref.watch(boardPreferencesProvider); - final editorState = ref.watch(boardEditorControllerProvider); + final editorController = boardEditorControllerProvider(widget.initialFen); + final editorState = ref.watch(editorController); final squareSize = widget.boardSize / 8; @@ -207,15 +223,12 @@ class _PieceMenuState extends ConsumerState<_PieceMenu> { height: squareSize, child: ColoredBox( key: Key('drag-button-${widget.side.name}'), - color: ref - .watch(boardEditorControllerProvider) - .editorPointerMode == - EditorPointerMode.drag + color: editorState.editorPointerMode == EditorPointerMode.drag ? context.lichessColors.good : Colors.transparent, child: GestureDetector( onTap: () => ref - .read(boardEditorControllerProvider.notifier) + .read(editorController.notifier) .updateMode(EditorPointerMode.drag), child: Icon( CupertinoIcons.hand_draw, @@ -238,7 +251,11 @@ class _PieceMenuState extends ConsumerState<_PieceMenu> { 'piece-button-${piece.color.name}-${piece.role.name}', ), color: ref - .read(boardEditorControllerProvider) + .read( + boardEditorControllerProvider( + widget.initialFen, + ), + ) .activePieceOnEdit == piece ? Theme.of(context).colorScheme.primary @@ -253,11 +270,11 @@ class _PieceMenuState extends ConsumerState<_PieceMenu> { ), child: pieceWidget, onDragEnd: (_) => ref - .read(boardEditorControllerProvider.notifier) + .read(editorController.notifier) .updateMode(EditorPointerMode.drag), ), onTap: () => ref - .read(boardEditorControllerProvider.notifier) + .read(editorController.notifier) .updateMode(EditorPointerMode.edit, piece), ), ); @@ -274,7 +291,7 @@ class _PieceMenuState extends ConsumerState<_PieceMenu> { child: GestureDetector( onTap: () => { ref - .read(boardEditorControllerProvider.notifier) + .read(editorController.notifier) .updateMode(EditorPointerMode.edit, null), }, child: Icon( @@ -292,11 +309,13 @@ class _PieceMenuState extends ConsumerState<_PieceMenu> { } class _BottomBar extends ConsumerWidget { - const _BottomBar(); + const _BottomBar(this.initialFen); + + final String? initialFen; @override Widget build(BuildContext context, WidgetRef ref) { - final editorState = ref.watch(boardEditorControllerProvider); + final editorState = ref.watch(boardEditorControllerProvider(initialFen)); return Container( color: Theme.of(context).platform == TargetPlatform.iOS @@ -317,7 +336,9 @@ class _BottomBar extends ConsumerWidget { label: context.l10n.menu, onTap: () => showAdaptiveBottomSheet( context: context, - builder: (BuildContext context) => const BoardEditorMenu(), + builder: (BuildContext context) => BoardEditorMenu( + initialFen: initialFen, + ), ), icon: Icons.tune, ), @@ -327,7 +348,7 @@ class _BottomBar extends ConsumerWidget { key: const Key('flip-button'), label: context.l10n.flipBoard, onTap: ref - .read(boardEditorControllerProvider.notifier) + .read(boardEditorControllerProvider(initialFen).notifier) .flipBoard, icon: CupertinoIcons.arrow_2_squarepath, ), diff --git a/lib/src/view/analysis/analysis_position_choice_screen.dart b/lib/src/view/tools/load_position_screen.dart similarity index 67% rename from lib/src/view/analysis/analysis_position_choice_screen.dart rename to lib/src/view/tools/load_position_screen.dart index b84bc3daca..58e8a8e203 100644 --- a/lib/src/view/analysis/analysis_position_choice_screen.dart +++ b/lib/src/view/tools/load_position_screen.dart @@ -8,12 +8,13 @@ import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; +import 'package:lichess_mobile/src/view/board_editor/board_editor_screen.dart'; import 'package:lichess_mobile/src/widgets/adaptive_text_field.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; -class AnalysisPositionChoiceScreen extends StatelessWidget { - const AnalysisPositionChoiceScreen({super.key}); +class LoadPositionScreen extends StatelessWidget { + const LoadPositionScreen({super.key}); @override Widget build(BuildContext context) { @@ -85,7 +86,7 @@ class _BodyState extends State<_Body> { child: AdaptiveTextField( maxLines: 500, placeholder: - '${context.l10n.pasteTheFenStringHere}\n\n${context.l10n.pasteThePgnStringHere}\n\nLeave empty for initial position', + '${context.l10n.pasteTheFenStringHere} / ${context.l10n.pasteThePgnStringHere}', controller: _controller, readOnly: true, onTap: _getClipboardData, @@ -94,19 +95,36 @@ class _BodyState extends State<_Body> { ), Padding( padding: Styles.bodySectionBottomPadding, - child: FatButton( - semanticsLabel: context.l10n.analysis, - onPressed: parsedInput != null - ? () => pushPlatformRoute( - context, - rootNavigator: true, - builder: (context) => AnalysisScreen( - pgnOrId: parsedInput!.$1, - options: parsedInput!.$2, - ), - ) - : null, - child: Text(context.l10n.studyStart), + child: Column( + children: [ + FatButton( + semanticsLabel: context.l10n.analysis, + onPressed: parsedInput != null + ? () => pushPlatformRoute( + context, + rootNavigator: true, + builder: (context) => AnalysisScreen( + pgnOrId: parsedInput!.pgn, + options: parsedInput!.options, + ), + ) + : null, + child: Text(context.l10n.analysis), + ), + const SizedBox(height: 16.0), + FatButton( + semanticsLabel: context.l10n.boardEditor, + onPressed: parsedInput != null + ? () => pushPlatformRoute( + context, + rootNavigator: true, + builder: (context) => + BoardEditorScreen(initialFen: parsedInput!.fen), + ) + : null, + child: Text(context.l10n.boardEditor), + ), + ], ), ), ], @@ -121,25 +139,18 @@ class _BodyState extends State<_Body> { } } - (String, AnalysisOptions)? get parsedInput { + ({String pgn, String fen, AnalysisOptions options})? get parsedInput { if (textInput == null || textInput!.trim().isEmpty) { - return const ( - '', - AnalysisOptions( - isLocalEvaluationAllowed: true, - variant: Variant.standard, - orientation: Side.white, - id: standaloneAnalysisId, - ) - ); + return null; } // try to parse as FEN first try { final pos = Chess.fromSetup(Setup.parseFen(textInput!.trim())); return ( - '[FEN "${pos.fen}"]', - const AnalysisOptions( + pgn: '[FEN "${pos.fen}"]', + fen: pos.fen, + options: const AnalysisOptions( isLocalEvaluationAllowed: true, variant: Variant.standard, orientation: Side.white, @@ -162,9 +173,15 @@ class _BodyState extends State<_Body> { return null; } + final lastPosition = mainlineMoves.fold( + initialPosition, + (pos, move) => pos.play(pos.parseSan(move.san)!), + ); + return ( - textInput!, - AnalysisOptions( + pgn: textInput!, + fen: lastPosition.fen, + options: AnalysisOptions( isLocalEvaluationAllowed: true, variant: rule != null ? Variant.fromRule(rule) : Variant.standard, initialMoveCursor: mainlineMoves.isEmpty ? 0 : 1, diff --git a/lib/src/view/tools/tools_tab_screen.dart b/lib/src/view/tools/tools_tab_screen.dart index 8af42df2ed..49e0dbffdc 100644 --- a/lib/src/view/tools/tools_tab_screen.dart +++ b/lib/src/view/tools/tools_tab_screen.dart @@ -8,10 +8,10 @@ import 'package:lichess_mobile/src/navigation.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; -import 'package:lichess_mobile/src/view/analysis/analysis_position_choice_screen.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; import 'package:lichess_mobile/src/view/board_editor/board_editor_screen.dart'; import 'package:lichess_mobile/src/view/clock/clock_screen.dart'; +import 'package:lichess_mobile/src/view/tools/load_position_screen.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; @@ -80,7 +80,7 @@ class _Body extends StatelessWidget { : EdgeInsets.zero, child: PlatformListTile( leading: Icon( - Icons.biotech, + Icons.upload_file, size: Styles.mainListTileIconSize, color: Theme.of(context).platform == TargetPlatform.iOS ? CupertinoTheme.of(context).primaryColor @@ -88,23 +88,14 @@ class _Body extends StatelessWidget { ), title: Padding( padding: tilePadding, - child: Text(context.l10n.analysis, style: Styles.callout), + child: Text(context.l10n.loadPosition, style: Styles.callout), ), trailing: Theme.of(context).platform == TargetPlatform.iOS ? const CupertinoListTileChevron() : null, onTap: () => pushPlatformRoute( context, - rootNavigator: true, - builder: (context) => const AnalysisScreen( - pgnOrId: '', - options: AnalysisOptions( - isLocalEvaluationAllowed: true, - variant: Variant.standard, - orientation: Side.white, - id: standaloneAnalysisId, - ), - ), + builder: (context) => const LoadPositionScreen(), ), ), ), @@ -114,7 +105,7 @@ class _Body extends StatelessWidget { : EdgeInsets.zero, child: PlatformListTile( leading: Icon( - Icons.edit, + Icons.biotech, size: Styles.mainListTileIconSize, color: Theme.of(context).platform == TargetPlatform.iOS ? CupertinoTheme.of(context).primaryColor @@ -122,15 +113,23 @@ class _Body extends StatelessWidget { ), title: Padding( padding: tilePadding, - child: Text(context.l10n.boardEditor, style: Styles.callout), + child: Text(context.l10n.analysis, style: Styles.callout), ), trailing: Theme.of(context).platform == TargetPlatform.iOS ? const CupertinoListTileChevron() : null, onTap: () => pushPlatformRoute( context, - builder: (context) => const BoardEditorScreen(), rootNavigator: true, + builder: (context) => const AnalysisScreen( + pgnOrId: '', + options: AnalysisOptions( + isLocalEvaluationAllowed: true, + variant: Variant.standard, + orientation: Side.white, + id: standaloneAnalysisId, + ), + ), ), ), ), @@ -140,7 +139,7 @@ class _Body extends StatelessWidget { : EdgeInsets.zero, child: PlatformListTile( leading: Icon( - Icons.upload_file, + Icons.edit, size: Styles.mainListTileIconSize, color: Theme.of(context).platform == TargetPlatform.iOS ? CupertinoTheme.of(context).primaryColor @@ -148,14 +147,15 @@ class _Body extends StatelessWidget { ), title: Padding( padding: tilePadding, - child: Text(context.l10n.loadPosition, style: Styles.callout), + child: Text(context.l10n.boardEditor, style: Styles.callout), ), trailing: Theme.of(context).platform == TargetPlatform.iOS ? const CupertinoListTileChevron() : null, onTap: () => pushPlatformRoute( context, - builder: (context) => const AnalysisPositionChoiceScreen(), + builder: (context) => const BoardEditorScreen(), + rootNavigator: true, ), ), ), From aa1069d44ca8f91a18c01925ae14459b151761db Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 5 Aug 2024 13:12:29 +0200 Subject: [PATCH 117/979] Fix tests --- test/view/board_editor/board_editor_screen_test.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/view/board_editor/board_editor_screen_test.dart b/test/view/board_editor/board_editor_screen_test.dart index d80147bdfc..15ea7f3023 100644 --- a/test/view/board_editor/board_editor_screen_test.dart +++ b/test/view/board_editor/board_editor_screen_test.dart @@ -225,7 +225,8 @@ Future setupBoardEditor(WidgetTester tester) async { tester, home: const BoardEditorScreen(), overrides: [ - boardEditorControllerProvider.overrideWith(() => boardEditorController), + boardEditorControllerProvider(null) + .overrideWith(() => boardEditorController), ], ); await tester.pumpWidget(app); From 86dd3fbbd0a19b4789cb9c31f6db00cba221a9f4 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 5 Aug 2024 14:24:14 +0200 Subject: [PATCH 118/979] Use ProviderScope.containerOf to interact with provider in tests --- .../board_editor_screen_test.dart | 154 +++++++++++++----- 1 file changed, 110 insertions(+), 44 deletions(-) diff --git a/test/view/board_editor/board_editor_screen_test.dart b/test/view/board_editor/board_editor_screen_test.dart index 15ea7f3023..89410b2285 100644 --- a/test/view/board_editor/board_editor_screen_test.dart +++ b/test/view/board_editor/board_editor_screen_test.dart @@ -1,6 +1,7 @@ import 'package:chessground/chessground.dart'; import 'package:dartchess/dartchess.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:lichess_mobile/src/model/board_editor/board_editor_controller.dart'; import 'package:lichess_mobile/src/view/board_editor/board_editor_screen.dart'; @@ -11,7 +12,11 @@ import '../../test_app.dart'; void main() { group('Board Editor', () { testWidgets('Displays initial FEN on start', (tester) async { - await setupBoardEditor(tester); + final app = await buildTestApp( + tester, + home: const BoardEditorScreen(), + ); + await tester.pumpWidget(app); final editor = tester.widget( find.byType(ChessboardEditor), @@ -32,7 +37,11 @@ void main() { }); testWidgets('Flip board', (tester) async { - await setupBoardEditor(tester); + final app = await buildTestApp( + tester, + home: const BoardEditorScreen(), + ); + await tester.pumpWidget(app); await tester.tap(find.byKey(const Key('flip-button'))); await tester.pump(); @@ -48,64 +57,103 @@ void main() { }); testWidgets('Side to play and castling rights', (tester) async { - final boardEditorController = await setupBoardEditor(tester); + final app = await buildTestApp( + tester, + home: const BoardEditorScreen(), + ); + await tester.pumpWidget(app); await tester.tap(find.byKey(const Key('flip-button'))); await tester.pump(); - boardEditorController.setSideToPlay(Side.black); + final container = ProviderScope.containerOf( + tester.element(find.byType(ChessboardEditor)), + ); + + final controllerProvider = boardEditorControllerProvider(null); + + container.read(controllerProvider.notifier).setSideToPlay(Side.black); expect( - boardEditorController.state.fen, + container.read(controllerProvider).fen, 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR b KQkq - 0 1', ); - boardEditorController.setCastling(Side.white, CastlingSide.king, false); + container + .read(controllerProvider.notifier) + .setCastling(Side.white, CastlingSide.king, false); expect( - boardEditorController.state.fen, + container.read(controllerProvider).fen, 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR b Qkq - 0 1', ); - boardEditorController.setCastling(Side.white, CastlingSide.queen, false); + container + .read(controllerProvider.notifier) + .setCastling(Side.white, CastlingSide.queen, false); expect( - boardEditorController.state.fen, + container.read(controllerProvider).fen, 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR b kq - 0 1', ); - boardEditorController.setCastling(Side.black, CastlingSide.king, false); + container + .read(controllerProvider.notifier) + .setCastling(Side.black, CastlingSide.king, false); expect( - boardEditorController.state.fen, + container.read(controllerProvider).fen, 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR b q - 0 1', ); - boardEditorController.setCastling(Side.black, CastlingSide.queen, false); + container + .read(controllerProvider.notifier) + .setCastling(Side.black, CastlingSide.queen, false); expect( - boardEditorController.state.fen, + container.read(controllerProvider).fen, 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR b - - 0 1', ); - boardEditorController.setCastling(Side.white, CastlingSide.king, true); + container + .read(controllerProvider.notifier) + .setCastling(Side.white, CastlingSide.king, true); expect( - boardEditorController.state.fen, + container.read(controllerProvider).fen, 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR b K - 0 1', ); }); testWidgets('Castling rights ignored when rook is missing', (tester) async { - final boardEditorController = await setupBoardEditor(tester); + final app = await buildTestApp( + tester, + home: const BoardEditorScreen(), + ); + await tester.pumpWidget(app); + + final container = ProviderScope.containerOf( + tester.element(find.byType(ChessboardEditor)), + ); + final controllerProvider = boardEditorControllerProvider(null); // Starting position, but with all rooks removed - boardEditorController + container + .read(controllerProvider.notifier) .loadFen('1nbqkbn1/pppppppp/8/8/8/8/PPPPPPPP/1NBQKBN1'); // By default, all castling rights are true, but since there are no rooks, the final FEN should have no castling rights expect( - boardEditorController.state.fen, + container.read(controllerProvider).fen, '1nbqkbn1/pppppppp/8/8/8/8/PPPPPPPP/1NBQKBN1 w - - 0 1', ); }); testWidgets('Can drag pieces to new squares', (tester) async { - final boardEditorController = await setupBoardEditor(tester); + final app = await buildTestApp( + tester, + home: const BoardEditorScreen(), + ); + await tester.pumpWidget(app); + + final container = ProviderScope.containerOf( + tester.element(find.byType(ChessboardEditor)), + ); + final controllerProvider = boardEditorControllerProvider(null); // Two legal moves by white await dragFromTo(tester, 'e2', 'e4'); @@ -118,14 +166,18 @@ void main() { await dragFromTo(tester, 'd1', 'c1'); expect( - boardEditorController.state.fen, + container.read(controllerProvider).fen, // Obtained by playing the moves above on lichess.org/editor '1nbqkbnr/pppppppp/r7/8/3PP3/8/PPP2PPP/RNQ1KBNR w KQk - 0 1', ); }); testWidgets('illegal position cannot be analyzed', (tester) async { - await setupBoardEditor(tester); + final app = await buildTestApp( + tester, + home: const BoardEditorScreen(), + ); + await tester.pumpWidget(app); // White queen "captures" white king => illegal position await dragFromTo(tester, 'd1', 'e1'); @@ -141,14 +193,23 @@ void main() { }); testWidgets('Delete pieces via bin button', (tester) async { - final boardEditorController = await setupBoardEditor(tester); + final app = await buildTestApp( + tester, + home: const BoardEditorScreen(), + ); + await tester.pumpWidget(app); + + final container = ProviderScope.containerOf( + tester.element(find.byType(ChessboardEditor)), + ); + final controllerProvider = boardEditorControllerProvider(null); await tester.tap(find.byKey(const Key('delete-button-white'))); await tester.pump(); await tapSquare(tester, 'e2'); expect( - boardEditorController.state.fen, + container.read(controllerProvider).fen, 'rnbqkbnr/pppppppp/8/8/8/8/PPPP1PPP/RNBQKBNR w KQkq - 0 1', ); @@ -156,8 +217,9 @@ void main() { await tester.tap(find.byKey(const Key('drag-button-white'))); await tester.pump(); await tapSquare(tester, 'e3'); + expect( - boardEditorController.state.fen, + container.read(controllerProvider).fen, 'rnbqkbnr/pppppppp/8/8/8/8/PPPP1PPP/RNBQKBNR w KQkq - 0 1', ); @@ -167,13 +229,17 @@ void main() { await panFromTo(tester, 'a7', 'h7'); expect( - boardEditorController.state.fen, + container.read(controllerProvider).fen, 'rnbqkbnr/8/8/8/8/8/PPPP1PPP/RNBQKBNR w KQkq - 0 1', ); }); testWidgets('Add pieces via tap and pan', (tester) async { - final boardEditorController = await setupBoardEditor(tester); + final app = await buildTestApp( + tester, + home: const BoardEditorScreen(), + ); + await tester.pumpWidget(app); await tester.tap(find.byKey(const Key('piece-button-white-queen'))); await panFromTo(tester, 'a1', 'a8'); @@ -181,14 +247,23 @@ void main() { await tapSquare(tester, 'h1'); await tapSquare(tester, 'h3'); + final container = ProviderScope.containerOf( + tester.element(find.byType(ChessboardEditor)), + ); + final controllerProvider = boardEditorControllerProvider(null); + expect( - boardEditorController.state.fen, + container.read(controllerProvider).fen, 'Qnbqkbnr/Qppppppp/Q7/Q7/Q7/Q6r/QPPPPPPP/QNBQKBNr w k - 0 1', ); }); testWidgets('Drag pieces onto the board', (tester) async { - final boardEditorController = await setupBoardEditor(tester); + final app = await buildTestApp( + tester, + home: const BoardEditorScreen(), + ); + await tester.pumpWidget(app); // Start by pressing bin button, dragging a piece should override this await tester.tap(find.byKey(const Key('delete-button-black'))); @@ -206,33 +281,24 @@ void main() { pieceButtonOffset, ); + final container = ProviderScope.containerOf( + tester.element(find.byType(ChessboardEditor)), + ); + final controllerProvider = boardEditorControllerProvider(null); + expect( - boardEditorController.state.editorPointerMode, + container.read(controllerProvider).editorPointerMode, EditorPointerMode.drag, ); expect( - boardEditorController.state.fen, + container.read(controllerProvider).fen, 'rnbqkbnr/pppppppp/8/8/8/3P4/PPPPPPPP/RNBPKBNR w KQkq - 0 1', ); }); }); } -Future setupBoardEditor(WidgetTester tester) async { - final boardEditorController = BoardEditorController(); - final app = await buildTestApp( - tester, - home: const BoardEditorScreen(), - overrides: [ - boardEditorControllerProvider(null) - .overrideWith(() => boardEditorController), - ], - ); - await tester.pumpWidget(app); - return boardEditorController; -} - Future dragFromTo( WidgetTester tester, String from, From 201e18da3c820f6fa0bf439fd5ab10131b8c33ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Nowak?= Date: Mon, 5 Aug 2024 14:44:38 +0200 Subject: [PATCH 119/979] fix: delete .tool-versions --- .tool-versions | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .tool-versions diff --git a/.tool-versions b/.tool-versions deleted file mode 100644 index 4b4932a526..0000000000 --- a/.tool-versions +++ /dev/null @@ -1 +0,0 @@ -flutter 3.23.0-0.1.pre \ No newline at end of file From e017866b4d0625076cab8540025279dfffa2d33c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Nowak?= Date: Mon, 5 Aug 2024 14:51:51 +0200 Subject: [PATCH 120/979] fix: formatting --- lib/src/view/settings/settings_tab_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/view/settings/settings_tab_screen.dart b/lib/src/view/settings/settings_tab_screen.dart index 049a7f735e..0b2ac820a0 100644 --- a/lib/src/view/settings/settings_tab_screen.dart +++ b/lib/src/view/settings/settings_tab_screen.dart @@ -87,7 +87,7 @@ class _Body extends ConsumerWidget { _refreshData(ref); } }); - + final generalPrefs = ref.watch(generalPreferencesProvider); final boardPrefs = ref.watch(boardPreferencesProvider); final authController = ref.watch(authControllerProvider); From 99c7a1c838b1d23ae024527ecfe07c4dee0e4305 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Nowak?= Date: Mon, 5 Aug 2024 14:53:29 +0200 Subject: [PATCH 121/979] fix: upgrade lock --- pubspec.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 78f8d1886b..33ac8c55d2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -122,18 +122,18 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "644dc98a0f179b872f612d3eb627924b578897c629788e858157fa5e704ca0c7" + sha256: dd09dd4e2b078992f42aac7f1a622f01882a8492fef08486b27ddde929c19f04 url: "https://pub.dev" source: hosted - version: "2.4.11" + version: "2.4.12" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: e3c79f69a64bdfcd8a776a3c28db4eb6e3fb5356d013ae5eb2e52007706d5dbe + sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 url: "https://pub.dev" source: hosted - version: "7.3.1" + version: "7.3.2" built_collection: dependency: transitive description: @@ -1633,5 +1633,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.4.0 <4.0.0" + dart: ">=3.5.0-259.0.dev <4.0.0" flutter: ">=3.22.0" From 6af68a65956dc2437fc7784435a2d4258f6d4a02 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 5 Aug 2024 15:45:58 +0200 Subject: [PATCH 122/979] Game filter improvements --- lib/src/model/game/game_filter.dart | 20 ++++++ lib/src/view/user/game_history_screen.dart | 76 ++++++++-------------- 2 files changed, 47 insertions(+), 49 deletions(-) diff --git a/lib/src/model/game/game_filter.dart b/lib/src/model/game/game_filter.dart index d0ec1abcd6..218f5f6ea1 100644 --- a/lib/src/model/game/game_filter.dart +++ b/lib/src/model/game/game_filter.dart @@ -1,7 +1,9 @@ import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/widgets.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'game_filter.freezed.dart'; @@ -29,6 +31,24 @@ class GameFilterState with _$GameFilterState { Side? side, }) = _GameFilterState; + /// Returns a translated label of the selected filters. + String selectionLabel(BuildContext context) { + final fields = [side, perfs]; + final labels = fields + .map( + (field) => field is ISet + ? field.map((e) => e.shortTitle).join(', ') + : (field as Side?) != null + ? field == Side.white + ? context.l10n.white + : context.l10n.black + : null, + ) + .where((label) => label != null && label.isNotEmpty) + .toList(); + return labels.isEmpty ? 'All' : labels.join(', '); + } + int get count { final fields = [perfs, side]; return fields diff --git a/lib/src/view/user/game_history_screen.dart b/lib/src/view/user/game_history_screen.dart index a345b9e7fd..2c4f9f50a2 100644 --- a/lib/src/view/user/game_history_screen.dart +++ b/lib/src/view/user/game_history_screen.dart @@ -8,10 +8,10 @@ import 'package:lichess_mobile/src/model/game/game_filter.dart'; import 'package:lichess_mobile/src/model/game/game_history.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/model/user/user_repository_providers.dart'; -import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/view/game/game_list_tile.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; @@ -28,37 +28,25 @@ class GameHistoryScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final session = ref.read(authSessionProvider); - final username = user?.name ?? session?.user.name; - final filtersInUse = ref.watch( - gameFilterProvider(filter: gameFilter).select( - (state) => state.count, - ), - ); + final filtersInUse = ref.watch(gameFilterProvider(filter: gameFilter)); final nbGamesAsync = ref.watch( userNumberOfGamesProvider(user, isOnline: isOnline), ); - final title = filtersInUse == 0 + final title = filtersInUse.count == 0 ? nbGamesAsync.when( data: (nbGames) => Text(context.l10n.nbGames(nbGames)), loading: () => const ButtonLoadingIndicator(), error: (e, s) => Text(context.l10n.mobileAllGames), ) - : Text( - username != null - ? '$username ${context.l10n.games.toLowerCase()}' - : context.l10n.games, - ); + : Text(filtersInUse.selectionLabel(context)); final filterBtn = Stack( alignment: Alignment.center, children: [ - IconButton( + AppBarIconButton( icon: const Icon(Icons.tune), - tooltip: context.l10n.filterGames, + semanticsLabel: context.l10n.filterGames, onPressed: () => showAdaptiveBottomSheet( context: context, - isScrollControlled: true, - showDragHandle: true, builder: (_) => _FilterGames( filter: ref.read(gameFilterProvider(filter: gameFilter)), user: user, @@ -71,7 +59,7 @@ class GameHistoryScreen extends ConsumerWidget { } }), ), - if (filtersInUse > 0) + if (filtersInUse.count > 0) Positioned( top: 2.0, right: 2.0, @@ -90,7 +78,7 @@ class GameHistoryScreen extends ConsumerWidget { color: Theme.of(context).colorScheme.onSecondary, fontWeight: FontWeight.bold, ), - child: Text(filtersInUse.toString()), + child: Text(filtersInUse.count.toString()), ), ), ], @@ -118,6 +106,10 @@ class GameHistoryScreen extends ConsumerWidget { }) { return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( + padding: const EdgeInsetsDirectional.only( + start: 16.0, + end: 8.0, + ), middle: title, trailing: filterBtn, ), @@ -348,11 +340,10 @@ class _FilterGamesState extends ConsumerState<_FilterGames> { Widget perfFilter(List choices) => _Filter( filterName: context.l10n.variant, - icon: const Icon(LichessIcons.classical), filterType: FilterType.multipleChoice, choices: choices, choiceSelected: (choice) => filter.perfs.contains(choice), - choiceLabel: (t) => t.title, + choiceLabel: (t) => t.shortTitle, onSelected: (value, selected) => setState( () { filter = filter.copyWith( @@ -364,16 +355,13 @@ class _FilterGamesState extends ConsumerState<_FilterGames> { ), ); - return Padding( - padding: const EdgeInsets.all(16), - child: DraggableScrollableSheet( - initialChildSize: .7, - expand: false, - snap: true, - snapSizes: const [.7], - builder: (context, scrollController) => ListView( - controller: scrollController, + return SafeArea( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, children: [ + const SizedBox(height: 12.0), if (userId != null) ref.watch(userProvider(id: userId)).when( data: (user) => perfFilter(availablePerfs(user)), @@ -384,11 +372,10 @@ class _FilterGamesState extends ConsumerState<_FilterGames> { ) else perfFilter(gamePerfs), - const Divider(), + const PlatformDivider(thickness: 1, indent: 0), filterGroupSpace, _Filter( filterName: context.l10n.side, - icon: const Icon(LichessIcons.chess_pawn), filterType: FilterType.singleChoice, choices: Side.values, choiceSelected: (choice) => filter.side == choice, @@ -406,13 +393,14 @@ class _FilterGamesState extends ConsumerState<_FilterGames> { mainAxisAlignment: MainAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end, children: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('Cancel'), + AdaptiveTextButton( + onPressed: () => + setState(() => filter = const GameFilterState()), + child: Text(context.l10n.reset), ), - TextButton( + AdaptiveTextButton( onPressed: () => Navigator.of(context).pop(filter), - child: const Text('OK'), + child: Text(context.l10n.apply), ), ], ), @@ -431,7 +419,6 @@ enum FilterType { class _Filter extends StatelessWidget { const _Filter({ required this.filterName, - required this.icon, required this.filterType, required this.choices, required this.choiceSelected, @@ -440,7 +427,6 @@ class _Filter extends StatelessWidget { }); final String filterName; - final Icon icon; final FilterType filterType; final Iterable choices; final bool Function(T choice) choiceSelected; @@ -452,15 +438,7 @@ class _Filter extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - children: [ - Container( - margin: const EdgeInsets.only(right: 10), - child: icon, - ), - Text(filterName, style: const TextStyle(fontSize: 18)), - ], - ), + Text(filterName, style: const TextStyle(fontSize: 18)), const SizedBox(height: 10), SizedBox( width: double.infinity, From 31688f9982dbfb54d6ea925405a05bc85eb96c82 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Mon, 5 Aug 2024 16:11:02 +0200 Subject: [PATCH 123/979] feat: show move percentage in table instead of on long press --- .../opening_explorer_screen.dart | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index ad789f9717..0d9be60294 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -349,9 +349,9 @@ class _MoveTable extends ConsumerWidget { return Table( columnWidths: const { - 0: FractionColumnWidth(0.2), - 1: FractionColumnWidth(0.3), - 2: FractionColumnWidth(0.5), + 0: FractionColumnWidth(0.15), + 1: FractionColumnWidth(0.35), + 2: FractionColumnWidth(0.50), }, children: [ TableRow( @@ -400,10 +400,7 @@ class _MoveTable extends ConsumerWidget { .onUserMove(Move.fromUci(move.uci)!), child: Container( padding: rowPadding, - child: Tooltip( - message: '$percentGames%', - child: Text(formatNum(move.games)), - ), + child: Text('${formatNum(move.games)} ($percentGames%)'), ), ), TableRowInkWell( @@ -437,10 +434,7 @@ class _MoveTable extends ConsumerWidget { ), Container( padding: rowPadding, - child: Tooltip( - message: '100%', - child: Text(formatNum(games)), - ), + child: Text('${formatNum(games)} (100%)'), ), Container( padding: rowPadding, From 6a35cc805b9d6798ac16725825ef0ef55dc718eb Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Sun, 4 Aug 2024 09:53:20 +0200 Subject: [PATCH 124/979] feat: add setting to change shape colors --- lib/src/model/settings/board_preferences.dart | 28 ++++++++ lib/src/view/analysis/analysis_screen.dart | 1 + lib/src/view/settings/theme_screen.dart | 70 ++++++++++++++++++- lib/src/widgets/board_table.dart | 1 + 4 files changed, 97 insertions(+), 3 deletions(-) diff --git a/lib/src/model/settings/board_preferences.dart b/lib/src/model/settings/board_preferences.dart index c67dbb996d..8e75b1a78c 100644 --- a/lib/src/model/settings/board_preferences.dart +++ b/lib/src/model/settings/board_preferences.dart @@ -85,6 +85,10 @@ class BoardPreferences extends _$BoardPreferences { ); } + Future setShapeColor(ShapeColor shapeColor) { + return _save(state.copyWith(shapeColor: shapeColor)); + } + Future _save(BoardPrefs newState) async { final prefs = ref.read(sharedPreferencesProvider); await prefs.setString( @@ -118,6 +122,11 @@ class BoardPrefs with _$BoardPrefs { /// Whether to enable shape drawings on the board for games and puzzles. @JsonKey(defaultValue: true) required bool enableShapeDrawings, @JsonKey(defaultValue: true) required bool magnifyDraggedPiece, + @JsonKey( + defaultValue: ShapeColor.green, + unknownEnumValue: ShapeColor.green, + ) + required ShapeColor shapeColor, }) = _BoardPrefs; static const defaults = BoardPrefs( @@ -133,6 +142,7 @@ class BoardPrefs with _$BoardPrefs { pieceShiftMethod: PieceShiftMethod.either, enableShapeDrawings: true, magnifyDraggedPiece: true, + shapeColor: ShapeColor.green, ); ChessboardSettings toBoardSettings() { @@ -148,6 +158,7 @@ class BoardPrefs with _$BoardPrefs { pieceShiftMethod: pieceShiftMethod, drawShape: DrawShapeOptions( enable: enableShapeDrawings, + newShapeColor: shapeColor.color, ), ); } @@ -164,6 +175,23 @@ class BoardPrefs with _$BoardPrefs { pieceAnimation ? const Duration(milliseconds: 150) : Duration.zero; } +/// Colors taken from lila: https://github.com/lichess-org/chessground/blob/54a7e71bf88701c1109d3b9b8106b464012b94cf/src/state.ts#L178 +enum ShapeColor { + green, + red, + blue, + yellow; + + Color get color => Color( + switch (this) { + ShapeColor.green => 0x15781B, + ShapeColor.red => 0x882020, + ShapeColor.blue => 0x003088, + ShapeColor.yellow => 0xe68f00, + }, + ).withAlpha(0xAA); +} + /// The chessboard theme. enum BoardTheme { system('System'), diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index e9a3713689..3feff8ed6b 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -507,6 +507,7 @@ class _BoardState extends ConsumerState<_Board> { enable: true, onCompleteShape: _onCompleteShape, onClearShapes: _onClearShapes, + newShapeColor: boardPrefs.shapeColor.color, ), ), ); diff --git a/lib/src/view/settings/theme_screen.dart b/lib/src/view/settings/theme_screen.dart index b811606d2f..6043963c5e 100644 --- a/lib/src/view/settings/theme_screen.dart +++ b/lib/src/view/settings/theme_screen.dart @@ -1,7 +1,7 @@ import 'dart:math' as math; import 'package:chessground/chessground.dart'; -import 'package:dartchess/dartchess.dart' as dartchess; import 'package:dartchess/dartchess.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -12,6 +12,7 @@ import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/settings/board_theme_screen.dart'; import 'package:lichess_mobile/src/view/settings/piece_set_screen.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_choice_picker.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/settings.dart'; @@ -42,6 +43,18 @@ class ThemeScreen extends StatelessWidget { } } +String shapeColorL10n( + BuildContext context, + ShapeColor shapeColor, +) => + // TODO add l10n + switch (shapeColor) { + ShapeColor.green => 'Green', + ShapeColor.red => 'Red', + ShapeColor.blue => 'Blue', + ShapeColor.yellow => 'Yellow', + }; + class _Body extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { @@ -66,10 +79,26 @@ class _Body extends ConsumerWidget { child: Center( child: Chessboard( size: boardSize, - state: const ChessboardState( + state: ChessboardState( interactableSide: InteractableSide.none, orientation: Side.white, - fen: dartchess.kInitialFEN, + fen: kInitialFEN, + shapes: { + Arrow( + color: boardPrefs.shapeColor.color, + orig: Square.fromName('e2'), + dest: Square.fromName('e4'), + ), + Circle( + color: boardPrefs.shapeColor.color, + orig: Square.fromName('d2'), + ), + Arrow( + color: boardPrefs.shapeColor.color, + orig: Square.fromName('b1'), + dest: Square.fromName('c3'), + ), + }.lock, ), settings: ChessboardSettings( enableCoordinates: false, @@ -111,6 +140,41 @@ class _Body extends ConsumerWidget { ); }, ), + SettingsListTile( + icon: const Icon(LichessIcons.arrow_full_upperright), + settingsLabel: const Text('Shape color'), + settingsValue: shapeColorL10n(context, boardPrefs.shapeColor), + showCupertinoTrailingValue: false, + onTap: () { + showChoicePicker( + context, + choices: ShapeColor.values, + selectedItem: boardPrefs.shapeColor, + labelBuilder: (t) => RichText( + text: TextSpan( + children: [ + TextSpan( + text: shapeColorL10n(context, t), + ), + const TextSpan(text: ' '), + WidgetSpan( + child: Container( + width: 15, + height: 15, + color: t.color, + ), + ), + ], + ), + ), + onSelectedItemChanged: (ShapeColor? value) { + ref + .read(boardPreferencesProvider.notifier) + .setShapeColor(value ?? ShapeColor.green); + }, + ); + }, + ), ], ), ], diff --git a/lib/src/widgets/board_table.dart b/lib/src/widgets/board_table.dart index 82bfa9c205..f27ba53d70 100644 --- a/lib/src/widgets/board_table.dart +++ b/lib/src/widgets/board_table.dart @@ -156,6 +156,7 @@ class _BoardTableState extends ConsumerState { enable: boardPrefs.enableShapeDrawings, onCompleteShape: _onCompleteShape, onClearShapes: _onClearShapes, + newShapeColor: boardPrefs.shapeColor.color, ), ); From e8f5ad7b9ccde674175627e580184f8d4ed16f04 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Tue, 6 Aug 2024 08:26:24 +0200 Subject: [PATCH 125/979] fix: make opening game uci field nullable --- .../opening_explorer/opening_explorer.dart | 49 ++++++++++--------- .../opening_explorer_screen.dart | 4 +- 2 files changed, 27 insertions(+), 26 deletions(-) diff --git a/lib/src/model/opening_explorer/opening_explorer.dart b/lib/src/model/opening_explorer/opening_explorer.dart index 8a5df93336..60e639de20 100644 --- a/lib/src/model/opening_explorer/opening_explorer.dart +++ b/lib/src/model/opening_explorer/opening_explorer.dart @@ -16,8 +16,8 @@ class OpeningExplorer with _$OpeningExplorer { required int draws, required int black, required IList moves, - IList? topGames, - IList? recentGames, + IList? topGames, + IList? recentGames, LightOpening? opening, int? queuePosition, }) = _OpeningExplorer; @@ -39,7 +39,7 @@ class OpeningMove with _$OpeningMove { int? averageRating, int? averageOpponentRating, int? performance, - Game? game, + OpeningGame? game, }) = _OpeningMove; factory OpeningMove.fromJson(Map json) => @@ -51,32 +51,32 @@ class OpeningMove with _$OpeningMove { } @Freezed(fromJson: true) -class Game with _$Game { - factory Game({ - required String uci, +class OpeningGame with _$OpeningGame { + factory OpeningGame({ required String id, + required OpeningPlayer white, + required OpeningPlayer black, + String? uci, String? winner, Perf? speed, Mode? mode, - required Player white, - required Player black, - required int year, + int? year, String? month, - }) = _Game; + }) = _OpeningGame; - factory Game.fromJson(Map json) => - Game.fromPick(pick(json).required()); + factory OpeningGame.fromJson(Map json) => + OpeningGame.fromPick(pick(json).required()); - factory Game.fromPick(RequiredPick pick) { - return Game( - uci: pick('uci').asStringOrThrow(), + factory OpeningGame.fromPick(RequiredPick pick) { + return OpeningGame( id: pick('id').asStringOrThrow(), + white: pick('white').letOrThrow(OpeningPlayer.fromPick), + black: pick('black').letOrThrow(OpeningPlayer.fromPick), + uci: pick('uci').asStringOrNull(), winner: pick('winner').asStringOrNull(), speed: pick('speed').asPerfOrNull(), mode: pick('mode').asModeOrNull(), - white: pick('white').letOrThrow(Player.fromPick), - black: pick('black').letOrThrow(Player.fromPick), - year: pick('year').asIntOrThrow(), + year: pick('year').asIntOrNull(), month: pick('month').asStringOrNull(), ); } @@ -116,16 +116,17 @@ extension ModeExtension on Pick { } @Freezed(fromJson: true) -class Player with _$Player { - const factory Player({ +class OpeningPlayer with _$OpeningPlayer { + const factory OpeningPlayer({ required String name, required int rating, - }) = _Player; + }) = _OpeningPlayer; - factory Player.fromJson(Map json) => _$PlayerFromJson(json); + factory OpeningPlayer.fromJson(Map json) => + _$OpeningPlayerFromJson(json); - factory Player.fromPick(RequiredPick pick) { - return Player( + factory OpeningPlayer.fromPick(RequiredPick pick) { + return OpeningPlayer( name: pick('name').asStringOrThrow(), rating: pick('rating').asIntOrThrow(), ); diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index 0d9be60294..cb1939f227 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -458,7 +458,7 @@ class _GameList extends StatelessWidget { }); final String title; - final IList games; + final IList games; @override Widget build(BuildContext context) { @@ -492,7 +492,7 @@ class _GameTile extends ConsumerWidget { required this.color, }); - final Game game; + final OpeningGame game; final Color color; @override From ad9752c376e9c3d556b10719be4363baaca6f774 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Tue, 6 Aug 2024 09:31:55 +0200 Subject: [PATCH 126/979] feat: view master games --- lib/src/model/game/archived_game.dart | 1 + lib/src/model/game/player.dart | 2 ++ .../opening_explorer_screen.dart | 32 +++++++------------ 3 files changed, 15 insertions(+), 20 deletions(-) diff --git a/lib/src/model/game/archived_game.dart b/lib/src/model/game/archived_game.dart index 99ca23a623..cc9ae33892 100644 --- a/lib/src/model/game/archived_game.dart +++ b/lib/src/model/game/archived_game.dart @@ -261,6 +261,7 @@ ClockData _clockDataFromPick(RequiredPick pick) { Player _playerFromUserGamePick(RequiredPick pick) { return Player( user: pick('user').asLightUserOrNull(), + name: pick('name').asStringOrNull(), rating: pick('rating').asIntOrNull(), ratingDiff: pick('ratingDiff').asIntOrNull(), aiLevel: pick('aiLevel').asIntOrNull(), diff --git a/lib/src/model/game/player.dart b/lib/src/model/game/player.dart index 6eac700062..e3abfebb38 100644 --- a/lib/src/model/game/player.dart +++ b/lib/src/model/game/player.dart @@ -13,6 +13,7 @@ class Player with _$Player { const factory Player({ LightUser? user, + String? name, int? aiLevel, int? rating, int? ratingDiff, @@ -47,6 +48,7 @@ class Player with _$Player { /// Returns the name of the player, without title String displayName(BuildContext context) => user?.name ?? + name ?? (aiLevel != null ? context.l10n.aiNameLevelAiLevel( 'Stockfish', diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index cb1939f227..d98529b0b1 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -500,31 +500,23 @@ class _GameTile extends ConsumerWidget { const widthResultBox = 50.0; const paddingResultBox = EdgeInsets.all(5); - final openingDb = ref.watch(openingExplorerPreferencesProvider).db; - return Container( padding: const EdgeInsets.all(6.0), color: color, child: AdaptiveInkWell( onTap: () async { - switch (openingDb) { - case OpeningDatabase.master: - return; - case OpeningDatabase.lichess: - case OpeningDatabase.player: - final gameId = GameId(game.id); - final archivedGame = await ref.read( - archivedGameProvider(id: gameId).future, - ); - if (context.mounted) { - pushPlatformRoute( - context, - builder: (_) => ArchivedGameScreen( - gameData: archivedGame.data, - orientation: Side.white, - ), - ); - } + final gameId = GameId(game.id); + final archivedGame = await ref.read( + archivedGameProvider(id: gameId).future, + ); + if (context.mounted) { + pushPlatformRoute( + context, + builder: (_) => ArchivedGameScreen( + gameData: archivedGame.data, + orientation: Side.white, + ), + ); } }, child: Row( From 132e90a48ea073664038fe6af3faeca160b5eee5 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Tue, 6 Aug 2024 09:41:14 +0200 Subject: [PATCH 127/979] feat: start rating setting at 400 instead of 0 --- .../model/opening_explorer/opening_explorer_preferences.dart | 2 +- lib/src/view/opening_explorer/opening_explorer_settings.dart | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/model/opening_explorer/opening_explorer_preferences.dart b/lib/src/model/opening_explorer/opening_explorer_preferences.dart index 1e5352cbe1..536a743fc5 100644 --- a/lib/src/model/opening_explorer/opening_explorer_preferences.dart +++ b/lib/src/model/opening_explorer/opening_explorer_preferences.dart @@ -183,7 +183,7 @@ class LichessDbPrefState with _$LichessDbPrefState { Perf.correspondence, }); static const kAvailableRatings = ISetConst({ - 0, + 400, 1000, 1200, 1400, diff --git a/lib/src/view/opening_explorer/opening_explorer_settings.dart b/lib/src/view/opening_explorer/opening_explorer_settings.dart index d444f82773..502a630b4e 100644 --- a/lib/src/view/opening_explorer/opening_explorer_settings.dart +++ b/lib/src/view/opening_explorer/opening_explorer_settings.dart @@ -69,8 +69,8 @@ class OpeningExplorerSettings extends ConsumerWidget { .map( (rating) => FilterChip( label: Text(rating.toString()), - tooltip: rating == 0 - ? '0-1000' + tooltip: rating == 400 + ? '400-1000' : rating == 2500 ? '2500+' : '$rating-${rating + 200}', From 1bccb7af638b0d96e8f4b8bb179cfa20851c67fc Mon Sep 17 00:00:00 2001 From: Mauritz Date: Tue, 6 Aug 2024 11:06:06 +0200 Subject: [PATCH 128/979] fix: flip players on flip board action in archived game screen --- lib/src/view/game/archived_game_screen.dart | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/src/view/game/archived_game_screen.dart b/lib/src/view/game/archived_game_screen.dart index a6493d7d5c..1d35af8f6c 100644 --- a/lib/src/view/game/archived_game_screen.dart +++ b/lib/src/view/game/archived_game_screen.dart @@ -189,8 +189,11 @@ class _BoardBody extends ConsumerWidget { : null, materialDiff: game.materialDiffAt(cursor, Side.white), ); - final topPlayer = orientation == Side.white ? black : white; - final bottomPlayer = orientation == Side.white ? white : black; + + final topPlayerIsBlack = orientation == Side.white && !isBoardTurned || + orientation == Side.black && isBoardTurned; + final topPlayer = topPlayerIsBlack ? black : white; + final bottomPlayer = topPlayerIsBlack ? white : black; final position = game.positionAt(cursor); From a9dff705539e4c6408b7cf44fe4f19bdd743f545 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Wed, 7 Aug 2024 14:05:46 +0200 Subject: [PATCH 129/979] feat: support indexing opening database --- lib/src/model/common/http.dart | 39 ++++++++++ .../opening_explorer_repository.dart | 75 +++++++++++++------ .../opening_explorer_screen.dart | 6 ++ 3 files changed, 97 insertions(+), 23 deletions(-) diff --git a/lib/src/model/common/http.dart b/lib/src/model/common/http.dart index 5e247b7dda..3c58a1be51 100644 --- a/lib/src/model/common/http.dart +++ b/lib/src/model/common/http.dart @@ -439,6 +439,45 @@ extension ClientExtension on Client { } } + /// Sends an HTTP GET request with the given headers to the given URL and + /// returns a Future that completes to the stream of the response as a ND-JSON + /// object mapped to T. + /// + /// The Future will emit a [ClientException] if the response doesn't have a + /// success status code or if an object in the response body can't be read + /// as ND-JSON. + Future> readNdJsonStream( + Uri url, { + Map? headers, + required T Function(Map) mapper, + }) async { + final request = Request( + 'GET', + lichessUri(url.path, url.hasQuery ? url.queryParameters : null), + ); + final response = await send(request); + if (response.statusCode > 400) { + var message = 'Request to $url failed with status ${response.statusCode}'; + if (response.reasonPhrase != null) { + message = '$message: ${response.reasonPhrase}'; + } + throw ServerException(response.statusCode, '$message.', url, null); + } + try { + return response.stream + .map(utf8.decode) + .where((e) => e.isNotEmpty && e != '\n') + .map((e) => jsonDecode(e) as Map) + .map(mapper); + } catch (e) { + _logger.severe('Could not read nd-json object as $T.'); + throw ClientException( + 'Could not read nd-json object as $T: $e', + url, + ); + } + } + /// Sends an HTTP POST request with the given headers and body to the given URL and /// returns a Future that completes to the body of the response as a JSON object /// mapped to [T]. diff --git a/lib/src/model/opening_explorer/opening_explorer_repository.dart b/lib/src/model/opening_explorer/opening_explorer_repository.dart index b0ff7ce795..0700843d5b 100644 --- a/lib/src/model/opening_explorer/opening_explorer_repository.dart +++ b/lib/src/model/opening_explorer/opening_explorer_repository.dart @@ -1,5 +1,6 @@ import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; @@ -7,39 +8,47 @@ import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_prefe import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'opening_explorer_repository.g.dart'; +part 'opening_explorer_repository.freezed.dart'; @riverpod -Future openingExplorer( +Stream openingExplorer( OpeningExplorerRef ref, { required String fen, -}) async { +}) async* { final prefs = ref.watch(openingExplorerPreferencesProvider); - return ref.withClient( - (client) => switch (prefs.db) { - OpeningDatabase.master => - OpeningExplorerRepository(client).getMasterDatabase( + final client = ref.read(lichessClientProvider); + final stream = switch (prefs.db) { + OpeningDatabase.master => OpeningExplorerRepository(client) + .getMasterDatabase( fen, since: prefs.masterDb.sinceYear, - ), - OpeningDatabase.lichess => - OpeningExplorerRepository(client).getLichessDatabase( + ) + .asStream(), + OpeningDatabase.lichess => OpeningExplorerRepository(client) + .getLichessDatabase( fen, speeds: prefs.lichessDb.speeds, ratings: prefs.lichessDb.ratings, since: prefs.lichessDb.since, - ), - OpeningDatabase.player => - OpeningExplorerRepository(client).getPlayerDatabase( - fen, - // null check handled by widget - usernameOrId: prefs.playerDb.usernameOrId!, - color: prefs.playerDb.side, - speeds: prefs.playerDb.speeds, - modes: prefs.playerDb.modes, - since: prefs.playerDb.since, ) - }, - ); + .asStream(), + OpeningDatabase.player => + await OpeningExplorerRepository(client).getPlayerDatabase( + fen, + // null check handled by widget + usernameOrId: prefs.playerDb.usernameOrId!, + color: prefs.playerDb.side, + speeds: prefs.playerDb.speeds, + modes: prefs.playerDb.modes, + since: prefs.playerDb.since, + ), + }; + + ref.read(openingExplorerDataProvider.notifier).setIndexing(true); + await for (final openingExplorer in stream) { + yield openingExplorer; + } + ref.read(openingExplorerDataProvider.notifier).setIndexing(false); } class OpeningExplorerRepository { @@ -84,7 +93,7 @@ class OpeningExplorerRepository { ); } - Future getPlayerDatabase( + Future> getPlayerDatabase( String fen, { required String usernameOrId, required Side color, @@ -92,7 +101,7 @@ class OpeningExplorerRepository { required ISet modes, DateTime? since, }) { - return client.readJson( + return client.readNdJsonStream( Uri( path: '/player', queryParameters: { @@ -110,3 +119,23 @@ class OpeningExplorerRepository { ); } } + +@riverpod +class OpeningExplorerData extends _$OpeningExplorerData { + @override + OpeningExplorerDataState build() { + return const OpeningExplorerDataState( + isIndexing: false, + ); + } + + void setIndexing(bool isIndexing) => + state = state.copyWith(isIndexing: isIndexing); +} + +@freezed +class OpeningExplorerDataState with _$OpeningExplorerDataState { + const factory OpeningExplorerDataState({ + required bool isIndexing, + }) = _OpeningExplorerDataState; +} diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index d98529b0b1..c601cdbfaf 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -243,6 +243,11 @@ class _OpeningExplorer extends ConsumerWidget { ) : nodeOpening ?? branchOpening ?? contextOpening; + final isIndexing = ref.watch( + openingExplorerDataProvider.select( + (state) => state.isIndexing, + ), + ); final openingExplorerAsync = ref.watch( openingExplorerProvider( fen: position.fen, @@ -268,6 +273,7 @@ class _OpeningExplorer extends ConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ if (opening != null) _Opening(opening: opening), + if (isIndexing) const Text('Indexing...'), _MoveTable( moves: openingExplorer.moves, whiteWins: openingExplorer.white, From 320d351da1ce983192b6e9c43667493f2f01f4cb Mon Sep 17 00:00:00 2001 From: Mauritz Date: Wed, 7 Aug 2024 14:49:59 +0200 Subject: [PATCH 130/979] feat: show loading indicator when indexing --- .../opening_explorer_screen.dart | 72 +++++++++++++++---- 1 file changed, 59 insertions(+), 13 deletions(-) diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index c601cdbfaf..b2d004204b 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -272,8 +272,17 @@ class _OpeningExplorer extends ConsumerWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (opening != null) _Opening(opening: opening), - if (isIndexing) const Text('Indexing...'), + Container( + padding: const EdgeInsets.symmetric(horizontal: 6.0), + color: Theme.of(context).colorScheme.primaryContainer, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (opening != null) _Opening(opening: opening), + if (isIndexing) _IndexingIndicator(), + ], + ), + ), _MoveTable( moves: openingExplorer.moves, whiteWins: openingExplorer.white, @@ -316,17 +325,54 @@ class _Opening extends StatelessWidget { final Opening opening; @override Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.only(left: 6.0), - color: Theme.of(context).colorScheme.primaryContainer, - child: Row( - children: [ - if (opening.eco.isEmpty) - Text(opening.name) - else - Text('${opening.eco} ${opening.name}'), - ], - ), + return opening.eco.isEmpty + ? Text(opening.name) + : Text('${opening.eco} ${opening.name}'); + } +} + +class _IndexingIndicator extends StatefulWidget { + + @override + State<_IndexingIndicator> createState() => _IndexingIndicatorState(); +} + +class _IndexingIndicatorState extends State<_IndexingIndicator> with TickerProviderStateMixin { + late AnimationController controller; + + @override + void initState() { + controller = AnimationController( + vsync: this, + duration: const Duration(seconds: 3), + )..addListener(() { + setState(() {}); + }); + controller.repeat(); + super.initState(); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Row( + children: [ + SizedBox( + width: 10, + height: 10, + child: CircularProgressIndicator.adaptive( + value: controller.value, + semanticsLabel: 'Indexing...', + ), + ), + const SizedBox(width: 10), + const Text('Indexing...'), + ], ); } } From 1a15bda0f7f480aebb81ac96aa64a78fb9912e8d Mon Sep 17 00:00:00 2001 From: Mauritz Date: Wed, 7 Aug 2024 15:04:20 +0200 Subject: [PATCH 131/979] feat: handle overflow when long opening name --- .../opening_explorer_screen.dart | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index b2d004204b..1f72cfe2ce 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -278,8 +278,16 @@ class _OpeningExplorer extends ConsumerWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - if (opening != null) _Opening(opening: opening), - if (isIndexing) _IndexingIndicator(), + if (opening != null) + Expanded( + flex: 75, + child: _Opening(opening: opening), + ), + if (isIndexing) + Expanded( + flex: 25, + child: _IndexingIndicator(), + ), ], ), ), @@ -325,19 +333,19 @@ class _Opening extends StatelessWidget { final Opening opening; @override Widget build(BuildContext context) { - return opening.eco.isEmpty - ? Text(opening.name) - : Text('${opening.eco} ${opening.name}'); + return Text( + '${opening.eco.isEmpty ? "" : "${opening.eco} "}${opening.name}', + ); } } class _IndexingIndicator extends StatefulWidget { - @override State<_IndexingIndicator> createState() => _IndexingIndicatorState(); } -class _IndexingIndicatorState extends State<_IndexingIndicator> with TickerProviderStateMixin { +class _IndexingIndicatorState extends State<_IndexingIndicator> + with TickerProviderStateMixin { late AnimationController controller; @override @@ -346,8 +354,8 @@ class _IndexingIndicatorState extends State<_IndexingIndicator> with TickerProvi vsync: this, duration: const Duration(seconds: 3), )..addListener(() { - setState(() {}); - }); + setState(() {}); + }); controller.repeat(); super.initState(); } From 7868d0cf4db4b40fd0d99a3d37742648937de19b Mon Sep 17 00:00:00 2001 From: Mauritz Date: Thu, 8 Aug 2024 09:56:41 +0200 Subject: [PATCH 132/979] feat: cache opening explorer data --- .../opening_explorer_repository.dart | 63 +++++++++++++++---- .../opening_explorer_screen.dart | 19 ++++-- 2 files changed, 65 insertions(+), 17 deletions(-) diff --git a/lib/src/model/opening_explorer/opening_explorer_repository.dart b/lib/src/model/opening_explorer/opening_explorer_repository.dart index 0700843d5b..f90a40a8b5 100644 --- a/lib/src/model/opening_explorer/opening_explorer_repository.dart +++ b/lib/src/model/opening_explorer/opening_explorer_repository.dart @@ -16,6 +16,16 @@ Stream openingExplorer( required String fen, }) async* { final prefs = ref.watch(openingExplorerPreferencesProvider); + final cacheKey = OpeningExplorerCacheKey( + fen: fen, + prefs: prefs, + ); + final cacheEntry = ref.read(openingExplorerCacheProvider).get(cacheKey); + if (cacheEntry != null) { + yield cacheEntry.openingExplorer; + return; + } + final client = ref.read(lichessClientProvider); final stream = switch (prefs.db) { OpeningDatabase.master => OpeningExplorerRepository(client) @@ -44,11 +54,17 @@ Stream openingExplorer( ), }; - ref.read(openingExplorerDataProvider.notifier).setIndexing(true); await for (final openingExplorer in stream) { + ref.read(openingExplorerCacheProvider.notifier).addEntry( + cacheKey, + OpeningExplorerCacheEntry( + openingExplorer: openingExplorer, + isIndexing: true, + ), + ); yield openingExplorer; } - ref.read(openingExplorerDataProvider.notifier).setIndexing(false); + ref.read(openingExplorerCacheProvider.notifier).setIndexing(cacheKey, false); } class OpeningExplorerRepository { @@ -121,21 +137,44 @@ class OpeningExplorerRepository { } @riverpod -class OpeningExplorerData extends _$OpeningExplorerData { +class OpeningExplorerCache extends _$OpeningExplorerCache { @override - OpeningExplorerDataState build() { - return const OpeningExplorerDataState( - isIndexing: false, - ); + IMap build() { + return const IMap.empty(); + } + + void addEntry(OpeningExplorerCacheKey key, OpeningExplorerCacheEntry entry) { + state = state.add(key, entry); } - void setIndexing(bool isIndexing) => - state = state.copyWith(isIndexing: isIndexing); + void setIndexing(OpeningExplorerCacheKey key, bool isIndexing) { + final entry = state.get(key); + if (entry != null) { + state = state.add( + key, + OpeningExplorerCacheEntry( + openingExplorer: entry.openingExplorer, + isIndexing: isIndexing, + ), + ); + } + } +} + +@freezed +class OpeningExplorerCacheKey with _$OpeningExplorerCacheKey { + const OpeningExplorerCacheKey._(); + + const factory OpeningExplorerCacheKey({ + required String fen, + required OpeningExplorerPrefState prefs, + }) = _OpeningExplorerCacheKey; } @freezed -class OpeningExplorerDataState with _$OpeningExplorerDataState { - const factory OpeningExplorerDataState({ +class OpeningExplorerCacheEntry with _$OpeningExplorerCacheEntry { + const factory OpeningExplorerCacheEntry({ + required OpeningExplorer openingExplorer, required bool isIndexing, - }) = _OpeningExplorerDataState; + }) = _OpeningExplorerCacheEntry; } diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index 1f72cfe2ce..fcb84df424 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -243,11 +243,20 @@ class _OpeningExplorer extends ConsumerWidget { ) : nodeOpening ?? branchOpening ?? contextOpening; - final isIndexing = ref.watch( - openingExplorerDataProvider.select( - (state) => state.isIndexing, - ), + final isIndexing = ref.read( + openingExplorerCacheProvider + .select( + (state) => state.get( + OpeningExplorerCacheKey( + fen: position.fen, + prefs: prefs, + ), + ), + ) + .select((state) => state?.isIndexing), ); + + ref.watch(openingExplorerCacheProvider); final openingExplorerAsync = ref.watch( openingExplorerProvider( fen: position.fen, @@ -283,7 +292,7 @@ class _OpeningExplorer extends ConsumerWidget { flex: 75, child: _Opening(opening: opening), ), - if (isIndexing) + if (isIndexing != null && isIndexing) Expanded( flex: 25, child: _IndexingIndicator(), From d9eabd4f95725e12f7da7a1cfc701aa5677134dd Mon Sep 17 00:00:00 2001 From: Mauritz Date: Thu, 8 Aug 2024 10:26:21 +0200 Subject: [PATCH 133/979] fix: do not use cache when still indexing --- .../opening_explorer_repository.dart | 81 ++++++++++--------- .../opening_explorer_screen.dart | 20 ++--- 2 files changed, 47 insertions(+), 54 deletions(-) diff --git a/lib/src/model/opening_explorer/opening_explorer_repository.dart b/lib/src/model/opening_explorer/opening_explorer_repository.dart index f90a40a8b5..c6498be5e9 100644 --- a/lib/src/model/opening_explorer/opening_explorer_repository.dart +++ b/lib/src/model/opening_explorer/opening_explorer_repository.dart @@ -21,50 +21,51 @@ Stream openingExplorer( prefs: prefs, ); final cacheEntry = ref.read(openingExplorerCacheProvider).get(cacheKey); - if (cacheEntry != null) { + if (cacheEntry != null && !cacheEntry.isIndexing) { yield cacheEntry.openingExplorer; - return; - } - - final client = ref.read(lichessClientProvider); - final stream = switch (prefs.db) { - OpeningDatabase.master => OpeningExplorerRepository(client) - .getMasterDatabase( - fen, - since: prefs.masterDb.sinceYear, - ) - .asStream(), - OpeningDatabase.lichess => OpeningExplorerRepository(client) - .getLichessDatabase( + } else { + final client = ref.read(lichessClientProvider); + final stream = switch (prefs.db) { + OpeningDatabase.master => OpeningExplorerRepository(client) + .getMasterDatabase( + fen, + since: prefs.masterDb.sinceYear, + ) + .asStream(), + OpeningDatabase.lichess => OpeningExplorerRepository(client) + .getLichessDatabase( + fen, + speeds: prefs.lichessDb.speeds, + ratings: prefs.lichessDb.ratings, + since: prefs.lichessDb.since, + ) + .asStream(), + OpeningDatabase.player => + await OpeningExplorerRepository(client).getPlayerDatabase( fen, - speeds: prefs.lichessDb.speeds, - ratings: prefs.lichessDb.ratings, - since: prefs.lichessDb.since, - ) - .asStream(), - OpeningDatabase.player => - await OpeningExplorerRepository(client).getPlayerDatabase( - fen, - // null check handled by widget - usernameOrId: prefs.playerDb.usernameOrId!, - color: prefs.playerDb.side, - speeds: prefs.playerDb.speeds, - modes: prefs.playerDb.modes, - since: prefs.playerDb.since, - ), - }; + // null check handled by widget + usernameOrId: prefs.playerDb.usernameOrId!, + color: prefs.playerDb.side, + speeds: prefs.playerDb.speeds, + modes: prefs.playerDb.modes, + since: prefs.playerDb.since, + ), + }; - await for (final openingExplorer in stream) { - ref.read(openingExplorerCacheProvider.notifier).addEntry( - cacheKey, - OpeningExplorerCacheEntry( - openingExplorer: openingExplorer, - isIndexing: true, - ), - ); - yield openingExplorer; + await for (final openingExplorer in stream) { + ref.read(openingExplorerCacheProvider.notifier).addEntry( + cacheKey, + OpeningExplorerCacheEntry( + openingExplorer: openingExplorer, + isIndexing: true, + ), + ); + yield openingExplorer; + } + ref + .read(openingExplorerCacheProvider.notifier) + .setIndexing(cacheKey, false); } - ref.read(openingExplorerCacheProvider.notifier).setIndexing(cacheKey, false); } class OpeningExplorerRepository { diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index fcb84df424..ff1784c718 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -243,20 +243,12 @@ class _OpeningExplorer extends ConsumerWidget { ) : nodeOpening ?? branchOpening ?? contextOpening; - final isIndexing = ref.read( - openingExplorerCacheProvider - .select( - (state) => state.get( - OpeningExplorerCacheKey( - fen: position.fen, - prefs: prefs, - ), - ), - ) - .select((state) => state?.isIndexing), - ); - - ref.watch(openingExplorerCacheProvider); + final cache = ref.watch(openingExplorerCacheProvider); + final isIndexing = cache + .get( + OpeningExplorerCacheKey(fen: position.fen, prefs: prefs), + ) + ?.isIndexing; final openingExplorerAsync = ref.watch( openingExplorerProvider( fen: position.fen, From bfce5bda6b128fe09d31fa1817c20e00dad94164 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 8 Aug 2024 12:08:02 +0200 Subject: [PATCH 134/979] Add try/catch on app init session check Fixes #906 --- lib/src/app_initialization.dart | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/lib/src/app_initialization.dart b/lib/src/app_initialization.dart index 61b600704a..8aff94ad36 100644 --- a/lib/src/app_initialization.dart +++ b/lib/src/app_initialization.dart @@ -102,16 +102,21 @@ Future appInitialization( final storedSession = await sessionStorage.read(); if (storedSession != null) { - final client = ref.read(defaultClientProvider); - final response = await client.get( - lichessUri('/api/account'), - headers: { - 'Authorization': 'Bearer ${signBearerToken(storedSession.token)}', - 'User-Agent': makeUserAgent(pInfo, deviceInfo, sri, storedSession.user), - }, - ).timeout(const Duration(seconds: 3)); - if (response.statusCode == 401) { - await sessionStorage.delete(); + try { + final client = ref.read(defaultClientProvider); + final response = await client.get( + lichessUri('/api/account'), + headers: { + 'Authorization': 'Bearer ${signBearerToken(storedSession.token)}', + 'User-Agent': + makeUserAgent(pInfo, deviceInfo, sri, storedSession.user), + }, + ).timeout(const Duration(seconds: 3)); + if (response.statusCode == 401) { + await sessionStorage.delete(); + } + } catch (e) { + _logger.warning('Could not while checking session: $e'); } } From 6619ffc534de73dbfb23ca75c3912a42af3b05ac Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 8 Aug 2024 12:14:11 +0200 Subject: [PATCH 135/979] Bump version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 8d1440b2f3..eadf519986 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: lichess_mobile description: Lichess mobile app V2 publish_to: "none" -version: 0.9.6+000906 # see README.md for details about versioning +version: 0.9.7+000907 # see README.md for details about versioning environment: sdk: ">=3.3.0 <4.0.0" From db2fe68a985491f9937da2755f5aa8683c961cdf Mon Sep 17 00:00:00 2001 From: Mauritz Date: Thu, 8 Aug 2024 12:40:26 +0200 Subject: [PATCH 136/979] feat: go to wikibooks page on tap opening name --- .../model/analysis/analysis_controller.dart | 18 +++++++++- .../opening_explorer_repository.dart | 10 ++++++ .../opening_explorer_screen.dart | 36 ++++++++++++++++--- 3 files changed, 58 insertions(+), 6 deletions(-) diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index 66957d6bd3..495714d54e 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -145,6 +145,7 @@ class AnalysisController extends _$AnalysisController { lastMove: lastMove, pov: options.orientation, contextOpening: options.opening, + wikiBooksUrl: _wikiBooksUrl(currentPath), isLocalEvaluationAllowed: options.isLocalEvaluationAllowed, isLocalEvaluationEnabled: prefs.enableLocalEvaluation, displayMode: DisplayMode.moves, @@ -413,8 +414,9 @@ class AnalysisController extends _$AnalysisController { currentPath: path, isOnMainline: _root.isOnMainline(path), currentNode: AnalysisCurrentNode.fromNode(currentNode), - lastMove: currentNode.sanMove.move, currentBranchOpening: opening, + wikiBooksUrl: _wikiBooksUrl(path), + lastMove: currentNode.sanMove.move, root: rootView, ); } else { @@ -423,6 +425,7 @@ class AnalysisController extends _$AnalysisController { isOnMainline: _root.isOnMainline(path), currentNode: AnalysisCurrentNode.fromNode(currentNode), currentBranchOpening: opening, + wikiBooksUrl: _wikiBooksUrl(path), lastMove: null, root: rootView, ); @@ -454,6 +457,16 @@ class AnalysisController extends _$AnalysisController { } } + String _wikiBooksUrl(UciPath path) { + final nodes = _root.branchesOn(path); + final moves = nodes.map((node) { + final move = node.view.sanMove.san; + final moveNr = (node.position.ply / 2).ceil(); + return node.position.ply.isOdd ? '$moveNr._$move' : '$moveNr...$move'; + }); + return 'https://en.wikibooks.org/wiki/Chess_Opening_Theory/${moves.join("/")}'; + } + void _startEngineEval() { if (!state.isEngineAvailable) return; ref @@ -641,6 +654,9 @@ class AnalysisState with _$AnalysisState { /// The opening of the current branch. Opening? currentBranchOpening, + /// wikibooks.org opening theory page for the current path + required String wikiBooksUrl, + /// Optional server analysis to display player stats. ({PlayerAnalysis white, PlayerAnalysis black})? playersAnalysis, diff --git a/lib/src/model/opening_explorer/opening_explorer_repository.dart b/lib/src/model/opening_explorer/opening_explorer_repository.dart index c6498be5e9..cfbef08f98 100644 --- a/lib/src/model/opening_explorer/opening_explorer_repository.dart +++ b/lib/src/model/opening_explorer/opening_explorer_repository.dart @@ -179,3 +179,13 @@ class OpeningExplorerCacheEntry with _$OpeningExplorerCacheEntry { required bool isIndexing, }) = _OpeningExplorerCacheEntry; } + +@riverpod +Future wikiBooksPageExists( + WikiBooksPageExistsRef ref, { + required String url, +}) async { + final client = ref.read(defaultClientProvider); + final response = await client.get(Uri.parse(url)); + return response.statusCode == 200; +} diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index ff1784c718..bd9c2e9e56 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -29,6 +29,7 @@ import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:url_launcher/url_launcher.dart'; import 'opening_explorer_settings.dart'; @@ -243,6 +244,9 @@ class _OpeningExplorer extends ConsumerWidget { ) : nodeOpening ?? branchOpening ?? contextOpening; + final wikiBooksUrl = ref.watch(ctrlProvider.select((s) => s.wikiBooksUrl)); + print('wikiBooksUrl: $wikiBooksUrl'); + final cache = ref.watch(openingExplorerCacheProvider); final isIndexing = cache .get( @@ -282,7 +286,10 @@ class _OpeningExplorer extends ConsumerWidget { if (opening != null) Expanded( flex: 75, - child: _Opening(opening: opening), + child: _Opening( + opening: opening, + wikiBooksUrl: wikiBooksUrl, + ), ), if (isIndexing != null && isIndexing) Expanded( @@ -328,15 +335,34 @@ class _OpeningExplorer extends ConsumerWidget { } } -class _Opening extends StatelessWidget { - const _Opening({required this.opening}); +class _Opening extends ConsumerWidget { + const _Opening({ + required this.opening, + required this.wikiBooksUrl, + }); final Opening opening; + final String wikiBooksUrl; @override - Widget build(BuildContext context) { - return Text( + Widget build(BuildContext context, WidgetRef ref) { + final wikiBooksPageExistsAsync = ref.watch( + wikiBooksPageExistsProvider(url: wikiBooksUrl), + ); + + final openingWidget = Text( '${opening.eco.isEmpty ? "" : "${opening.eco} "}${opening.name}', ); + + return wikiBooksPageExistsAsync.when( + data: (wikiBooksPageExists) => wikiBooksPageExists + ? GestureDetector( + onTap: () => launchUrl(Uri.parse(wikiBooksUrl)), + child: openingWidget, + ) + : openingWidget, + loading: () => openingWidget, + error: (e, s) => openingWidget, + ); } } From 162ca252a8f3c8fc70dc8366eaf358bbaea2fc68 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 8 Aug 2024 12:46:35 +0200 Subject: [PATCH 137/979] Fix shape settings on iOS --- lib/src/view/settings/theme_screen.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/src/view/settings/theme_screen.dart b/lib/src/view/settings/theme_screen.dart index 6043963c5e..b20569fbac 100644 --- a/lib/src/view/settings/theme_screen.dart +++ b/lib/src/view/settings/theme_screen.dart @@ -144,14 +144,13 @@ class _Body extends ConsumerWidget { icon: const Icon(LichessIcons.arrow_full_upperright), settingsLabel: const Text('Shape color'), settingsValue: shapeColorL10n(context, boardPrefs.shapeColor), - showCupertinoTrailingValue: false, onTap: () { showChoicePicker( context, choices: ShapeColor.values, selectedItem: boardPrefs.shapeColor, - labelBuilder: (t) => RichText( - text: TextSpan( + labelBuilder: (t) => Text.rich( + TextSpan( children: [ TextSpan( text: shapeColorL10n(context, t), From af96a89efb6cbb1e0ea67b3c20845b262a32bd6f Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 8 Aug 2024 12:52:57 +0200 Subject: [PATCH 138/979] Tweak local db widget appearance --- lib/src/view/settings/settings_tab_screen.dart | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/src/view/settings/settings_tab_screen.dart b/lib/src/view/settings/settings_tab_screen.dart index 0b2ac820a0..93d513b19b 100644 --- a/lib/src/view/settings/settings_tab_screen.dart +++ b/lib/src/view/settings/settings_tab_screen.dart @@ -367,14 +367,20 @@ class _Body extends ConsumerWidget { launchUrl(Uri.parse('https://lichess.org/thanks')); }, ), + ], + ), + ListSection( + hasLeading: true, + showDivider: true, + children: [ PlatformListTile( leading: const Icon(Icons.storage), title: const Text('Local database size'), + subtitle: Theme.of(context).platform == TargetPlatform.iOS + ? null + : Text(_getSizeString(dbSize.value)), additionalInfo: dbSize.hasValue ? Text(_getSizeString(dbSize.value)) : null, - trailing: Theme.of(context).platform == TargetPlatform.iOS - ? const CupertinoListTileChevron() - : Text(_getSizeString(dbSize.value)), ), ], ), From 71f686ba84028020f561b63dc7b91eccae3517ad Mon Sep 17 00:00:00 2001 From: Mauritz Date: Thu, 8 Aug 2024 13:43:31 +0200 Subject: [PATCH 139/979] test: add unit tests opening explorer repository --- .../opening_explorer_repository_test.dart | 236 ++++++++++++++++++ 1 file changed, 236 insertions(+) create mode 100644 test/model/opening_explorer/opening_explorer_repository_test.dart diff --git a/test/model/opening_explorer/opening_explorer_repository_test.dart b/test/model/opening_explorer/opening_explorer_repository_test.dart new file mode 100644 index 0000000000..7773f820f4 --- /dev/null +++ b/test/model/opening_explorer/opening_explorer_repository_test.dart @@ -0,0 +1,236 @@ +import 'package:dartchess/dartchess.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/testing.dart'; +import 'package:lichess_mobile/src/model/common/http.dart'; +import 'package:lichess_mobile/src/model/common/perf.dart'; +import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; +import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_repository.dart'; + +import '../../test_container.dart'; +import '../../test_utils.dart'; + +void main() { + group('OpeningExplorerRepository.getMasterDatabase', () { + test('parse json', () async { + const response = ''' +{ + "white": 834333, + "draws": 1085272, + "black": 600303, + "moves": [ + { + "uci": "e2e4", + "san": "e4", + "averageRating": 2399, + "white": 372266, + "draws": 486092, + "black": 280238, + "game": null + }, + { + "uci": "d2d4", + "san": "d4", + "averageRating": 2414, + "white": 302160, + "draws": 397224, + "black": 209077, + "game": null + } + ], + "topGames": [ + { + "uci": "d2d4", + "id": "QR5UbqUY", + "winner": null, + "black": { + "name": "Caruana, F.", + "rating": 2818 + }, + "white": { + "name": "Carlsen, M.", + "rating": 2882 + }, + "year": 2019, + "month": "2019-08" + }, + { + "uci": "e2e4", + "id": "Sxov6E94", + "winner": "white", + "black": { + "name": "Carlsen, M.", + "rating": 2882 + }, + "white": { + "name": "Caruana, F.", + "rating": 2818 + }, + "year": 2019, + "month": "2019-08" + } + ], + "opening": null +} + '''; + + final mockClient = MockClient((request) { + if (request.url.path == '/masters') { + return mockResponse(response, 200); + } + return mockResponse('', 404); + }); + + final container = await lichessClientContainer(mockClient); + final client = container.read(lichessClientProvider); + + final repo = OpeningExplorerRepository(client); + + final result = await repo.getMasterDatabase('fen'); + expect(result, isA()); + expect(result.moves.length, 2); + expect(result.topGames, isNotNull); + expect(result.topGames!.length, 2); + }); + }); + + group('OpeningExplorerRepository.getLichessDatabase', () { + test('parse json', () async { + const response = ''' +{ + "white": 2848672002, + "draws": 225287646, + "black": 2649860106, + "moves": [ + { + "uci": "e2e4", + "san": "e4", + "averageRating": 1604, + "white": 1661457614, + "draws": 129433754, + "black": 1565161663, + "game": null + } + ], + "recentGames": [ + { + "uci": "e2e4", + "id": "RVb19S9O", + "winner": "white", + "speed": "rapid", + "mode": "rated", + "black": { + "name": "Jcats1", + "rating": 1548 + }, + "white": { + "name": "carlosrivero32", + "rating": 1690 + }, + "year": 2024, + "month": "2024-06" + } + ], + "topGames": [], + "opening": null +} + '''; + + final mockClient = MockClient((request) { + if (request.url.path == '/lichess') { + return mockResponse(response, 200); + } + return mockResponse('', 404); + }); + + final container = await lichessClientContainer(mockClient); + final client = container.read(lichessClientProvider); + + final repo = OpeningExplorerRepository(client); + + final result = await repo.getLichessDatabase( + 'fen', + speeds: const ISetConst({Perf.rapid}), + ratings: const ISetConst({1000, 1200}), + ); + expect(result, isA()); + expect(result.moves.length, 1); + expect(result.recentGames, isNotNull); + expect(result.recentGames!.length, 1); + expect(result.topGames, isNotNull); + expect(result.topGames!.length, 0); + }); + }); + + group('OpeningExplorerRepository.getPlayerDatabase', () { + test('parse json', () async { + const response = ''' +{ + "white": 1713, + "draws": 119, + "black": 1459, + "moves": [ + { + "uci": "e2e4", + "san": "e4", + "averageOpponentRating": 1767, + "performance": 1796, + "white": 1691, + "draws": 116, + "black": 1432, + "game": null + } + ], + "recentGames": [ + { + "uci": "e2e4", + "id": "abc", + "winner": "white", + "speed": "bullet", + "mode": "rated", + "black": { + "name": "foo", + "rating": 1869 + }, + "white": { + "name": "baz", + "rating": 1912 + }, + "year": 2023, + "month": "2023-08" + } + ], + "opening": null, + "queuePosition": 0 +} + '''; + + final mockClient = MockClient((request) { + if (request.url.path == '/player') { + return mockResponse(response, 200); + } + return mockResponse('', 404); + }); + + final container = await lichessClientContainer(mockClient); + final client = container.read(lichessClientProvider); + + final repo = OpeningExplorerRepository(client); + + final results = await repo.getPlayerDatabase( + 'fen', + usernameOrId: 'baz', + color: Side.white, + speeds: const ISetConst({Perf.bullet}), + modes: const ISetConst({Mode.rated}), + ); + expect(results, isA>()); + await for (final result in results) { + expect(result, isA()); + expect(result.moves.length, 1); + expect(result.recentGames, isNotNull); + expect(result.recentGames!.length, 1); + } + }); + }); +} From d0da71e813baeed233cbb052be0a5e62cbf3cb0f Mon Sep 17 00:00:00 2001 From: Mauritz Date: Fri, 9 Aug 2024 09:44:29 +0200 Subject: [PATCH 140/979] refactor: restructure opening explorer class according to pr review --- .../opening_explorer/opening_explorer.dart | 110 +++++++----------- .../opening_explorer_preferences.dart | 12 +- .../opening_explorer_repository.dart | 30 ++--- .../opening_explorer_screen.dart | 4 +- .../opening_explorer_settings.dart | 15 ++- .../opening_explorer_repository_test.dart | 10 +- 6 files changed, 77 insertions(+), 104 deletions(-) diff --git a/lib/src/model/opening_explorer/opening_explorer.dart b/lib/src/model/opening_explorer/opening_explorer.dart index 60e639de20..d77ee38684 100644 --- a/lib/src/model/opening_explorer/opening_explorer.dart +++ b/lib/src/model/opening_explorer/opening_explorer.dart @@ -8,22 +8,22 @@ part 'opening_explorer.freezed.dart'; part 'opening_explorer.g.dart'; @Freezed(fromJson: true) -class OpeningExplorer with _$OpeningExplorer { - const OpeningExplorer._(); +class OpeningExplorerEntry with _$OpeningExplorerEntry { + const OpeningExplorerEntry._(); - const factory OpeningExplorer({ + const factory OpeningExplorerEntry({ required int white, required int draws, required int black, required IList moves, - IList? topGames, - IList? recentGames, + IList? topGames, + IList? recentGames, LightOpening? opening, int? queuePosition, - }) = _OpeningExplorer; + }) = _OpeningExplorerEntry; - factory OpeningExplorer.fromJson(Map json) => - _$OpeningExplorerFromJson(json); + factory OpeningExplorerEntry.fromJson(Map json) => + _$OpeningExplorerEntryFromJson(json); } @Freezed(fromJson: true) @@ -39,7 +39,7 @@ class OpeningMove with _$OpeningMove { int? averageRating, int? averageOpponentRating, int? performance, - OpeningGame? game, + OpeningExplorerGame? game, }) = _OpeningMove; factory OpeningMove.fromJson(Map json) => @@ -51,84 +51,52 @@ class OpeningMove with _$OpeningMove { } @Freezed(fromJson: true) -class OpeningGame with _$OpeningGame { - factory OpeningGame({ +class OpeningExplorerGame with _$OpeningExplorerGame { + factory OpeningExplorerGame({ required String id, - required OpeningPlayer white, - required OpeningPlayer black, + required ({String name, int rating}) white, + required ({String name, int rating}) black, String? uci, String? winner, Perf? speed, - Mode? mode, + GameMode? mode, int? year, String? month, - }) = _OpeningGame; + }) = _OpeningExplorerGame; - factory OpeningGame.fromJson(Map json) => - OpeningGame.fromPick(pick(json).required()); + factory OpeningExplorerGame.fromJson(Map json) => + OpeningExplorerGame.fromPick(pick(json).required()); - factory OpeningGame.fromPick(RequiredPick pick) { - return OpeningGame( + factory OpeningExplorerGame.fromPick(RequiredPick pick) { + return OpeningExplorerGame( id: pick('id').asStringOrThrow(), - white: pick('white').letOrThrow(OpeningPlayer.fromPick), - black: pick('black').letOrThrow(OpeningPlayer.fromPick), + white: pick('white').letOrThrow( + (pick) => ( + name: pick('name').asStringOrThrow(), + rating: pick('rating').asIntOrThrow() + ), + ), + black: pick('black').letOrThrow( + (pick) => ( + name: pick('name').asStringOrThrow(), + rating: pick('rating').asIntOrThrow() + ), + ), uci: pick('uci').asStringOrNull(), winner: pick('winner').asStringOrNull(), speed: pick('speed').asPerfOrNull(), - mode: pick('mode').asModeOrNull(), + mode: switch (pick('mode').value) { + 'casual' => GameMode.casual, + 'rated' => GameMode.rated, + _ => null, + }, year: pick('year').asIntOrNull(), month: pick('month').asStringOrNull(), ); } } -enum Mode { - casual('Casual'), - rated('Rated'); - - const Mode(this.title); - - final String title; -} - -extension ModeExtension on Pick { - Mode asModeOrThrow() { - switch (this.required().value) { - case 'casual': - return Mode.casual; - case 'rated': - return Mode.rated; - default: - throw PickException( - "value $value at $debugParsingExit can't be casted to Mode", - ); - } - } - - Mode? asModeOrNull() { - if (value == null) return null; - try { - return asModeOrThrow(); - } catch (_) { - return null; - } - } -} - -@Freezed(fromJson: true) -class OpeningPlayer with _$OpeningPlayer { - const factory OpeningPlayer({ - required String name, - required int rating, - }) = _OpeningPlayer; - - factory OpeningPlayer.fromJson(Map json) => - _$OpeningPlayerFromJson(json); - - factory OpeningPlayer.fromPick(RequiredPick pick) { - return OpeningPlayer( - name: pick('name').asStringOrThrow(), - rating: pick('rating').asIntOrThrow(), - ); - } +enum GameMode { + casual, + rated, } diff --git a/lib/src/model/opening_explorer/opening_explorer_preferences.dart b/lib/src/model/opening_explorer/opening_explorer_preferences.dart index 536a743fc5..de8b0cd082 100644 --- a/lib/src/model/opening_explorer/opening_explorer_preferences.dart +++ b/lib/src/model/opening_explorer/opening_explorer_preferences.dart @@ -80,12 +80,12 @@ class OpeningExplorerPreferences extends _$OpeningExplorerPreferences { ), ); - Future togglePlayerDbMode(Mode mode) => _save( + Future togglePlayerDbGameMode(GameMode gameMode) => _save( state.copyWith( playerDb: state.playerDb.copyWith( - modes: state.playerDb.modes.contains(mode) - ? state.playerDb.modes.remove(mode) - : state.playerDb.modes.add(mode), + gameModes: state.playerDb.gameModes.contains(gameMode) + ? state.playerDb.gameModes.remove(gameMode) + : state.playerDb.gameModes.add(gameMode), ), ), ); @@ -224,7 +224,7 @@ class PlayerDbPrefState with _$PlayerDbPrefState { String? usernameOrId, required Side side, required ISet speeds, - required ISet modes, + required ISet gameModes, required DateTime since, }) = _PlayerDbPrefState; @@ -248,7 +248,7 @@ class PlayerDbPrefState with _$PlayerDbPrefState { static final defaults = PlayerDbPrefState( side: Side.white, speeds: kAvailableSpeeds, - modes: Mode.values.toISet(), + gameModes: GameMode.values.toISet(), since: earliestDate, ); diff --git a/lib/src/model/opening_explorer/opening_explorer_repository.dart b/lib/src/model/opening_explorer/opening_explorer_repository.dart index cfbef08f98..8e29c31f77 100644 --- a/lib/src/model/opening_explorer/opening_explorer_repository.dart +++ b/lib/src/model/opening_explorer/opening_explorer_repository.dart @@ -11,7 +11,7 @@ part 'opening_explorer_repository.g.dart'; part 'opening_explorer_repository.freezed.dart'; @riverpod -Stream openingExplorer( +Stream openingExplorer( OpeningExplorerRef ref, { required String fen, }) async* { @@ -22,7 +22,7 @@ Stream openingExplorer( ); final cacheEntry = ref.read(openingExplorerCacheProvider).get(cacheKey); if (cacheEntry != null && !cacheEntry.isIndexing) { - yield cacheEntry.openingExplorer; + yield cacheEntry.openingExplorerEntry; } else { final client = ref.read(lichessClientProvider); final stream = switch (prefs.db) { @@ -47,7 +47,7 @@ Stream openingExplorer( usernameOrId: prefs.playerDb.usernameOrId!, color: prefs.playerDb.side, speeds: prefs.playerDb.speeds, - modes: prefs.playerDb.modes, + gameModes: prefs.playerDb.gameModes, since: prefs.playerDb.since, ), }; @@ -56,7 +56,7 @@ Stream openingExplorer( ref.read(openingExplorerCacheProvider.notifier).addEntry( cacheKey, OpeningExplorerCacheEntry( - openingExplorer: openingExplorer, + openingExplorerEntry: openingExplorer, isIndexing: true, ), ); @@ -73,7 +73,7 @@ class OpeningExplorerRepository { final LichessClient client; - Future getMasterDatabase( + Future getMasterDatabase( String fen, { int? since, }) { @@ -85,11 +85,11 @@ class OpeningExplorerRepository { if (since != null) 'since': since.toString(), }, ), - mapper: OpeningExplorer.fromJson, + mapper: OpeningExplorerEntry.fromJson, ); } - Future getLichessDatabase( + Future getLichessDatabase( String fen, { required ISet speeds, required ISet ratings, @@ -106,16 +106,16 @@ class OpeningExplorerRepository { if (since != null) 'since': '${since.year}-${since.month}', }, ), - mapper: OpeningExplorer.fromJson, + mapper: OpeningExplorerEntry.fromJson, ); } - Future> getPlayerDatabase( + Future> getPlayerDatabase( String fen, { required String usernameOrId, required Side color, required ISet speeds, - required ISet modes, + required ISet gameModes, DateTime? since, }) { return client.readNdJsonStream( @@ -127,12 +127,12 @@ class OpeningExplorerRepository { 'color': color.name, if (speeds.isNotEmpty) 'speeds': speeds.map((speed) => speed.name).join(','), - if (modes.isNotEmpty) - 'modes': modes.map((mode) => mode.name).join(','), + if (gameModes.isNotEmpty) + 'modes': gameModes.map((gameMode) => gameMode.name).join(','), if (since != null) 'since': '${since.year}-${since.month}', }, ), - mapper: OpeningExplorer.fromJson, + mapper: OpeningExplorerEntry.fromJson, ); } } @@ -154,7 +154,7 @@ class OpeningExplorerCache extends _$OpeningExplorerCache { state = state.add( key, OpeningExplorerCacheEntry( - openingExplorer: entry.openingExplorer, + openingExplorerEntry: entry.openingExplorerEntry, isIndexing: isIndexing, ), ); @@ -175,7 +175,7 @@ class OpeningExplorerCacheKey with _$OpeningExplorerCacheKey { @freezed class OpeningExplorerCacheEntry with _$OpeningExplorerCacheEntry { const factory OpeningExplorerCacheEntry({ - required OpeningExplorer openingExplorer, + required OpeningExplorerEntry openingExplorerEntry, required bool isIndexing, }) = _OpeningExplorerCacheEntry; } diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index 65349e9ef9..8427936884 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -544,7 +544,7 @@ class _GameList extends StatelessWidget { }); final String title; - final IList games; + final IList games; @override Widget build(BuildContext context) { @@ -578,7 +578,7 @@ class _GameTile extends ConsumerWidget { required this.color, }); - final OpeningGame game; + final OpeningExplorerGame game; final Color color; @override diff --git a/lib/src/view/opening_explorer/opening_explorer_settings.dart b/lib/src/view/opening_explorer/opening_explorer_settings.dart index 502a630b4e..404a21a4cf 100644 --- a/lib/src/view/opening_explorer/opening_explorer_settings.dart +++ b/lib/src/view/opening_explorer/opening_explorer_settings.dart @@ -180,14 +180,19 @@ class OpeningExplorerSettings extends ConsumerWidget { title: Text(context.l10n.mode), subtitle: Wrap( spacing: 5, - children: Mode.values + children: GameMode.values .map( - (mode) => FilterChip( - label: Text(mode.title), - selected: prefs.playerDb.modes.contains(mode), + (gameMode) => FilterChip( + label: Text( + switch (gameMode) { + GameMode.casual => 'Casual', + GameMode.rated => 'Rated', + }, + ), + selected: prefs.playerDb.gameModes.contains(gameMode), onSelected: (_) => ref .read(openingExplorerPreferencesProvider.notifier) - .togglePlayerDbMode(mode), + .togglePlayerDbGameMode(gameMode), ), ) .toList(growable: false), diff --git a/test/model/opening_explorer/opening_explorer_repository_test.dart b/test/model/opening_explorer/opening_explorer_repository_test.dart index 7773f820f4..38ce825d07 100644 --- a/test/model/opening_explorer/opening_explorer_repository_test.dart +++ b/test/model/opening_explorer/opening_explorer_repository_test.dart @@ -87,7 +87,7 @@ void main() { final repo = OpeningExplorerRepository(client); final result = await repo.getMasterDatabase('fen'); - expect(result, isA()); + expect(result, isA()); expect(result.moves.length, 2); expect(result.topGames, isNotNull); expect(result.topGames!.length, 2); @@ -153,7 +153,7 @@ void main() { speeds: const ISetConst({Perf.rapid}), ratings: const ISetConst({1000, 1200}), ); - expect(result, isA()); + expect(result, isA()); expect(result.moves.length, 1); expect(result.recentGames, isNotNull); expect(result.recentGames!.length, 1); @@ -222,11 +222,11 @@ void main() { usernameOrId: 'baz', color: Side.white, speeds: const ISetConst({Perf.bullet}), - modes: const ISetConst({Mode.rated}), + gameModes: const ISetConst({GameMode.rated}), ); - expect(results, isA>()); + expect(results, isA>()); await for (final result in results) { - expect(result, isA()); + expect(result, isA()); expect(result.moves.length, 1); expect(result.recentGames, isNotNull); expect(result.recentGames!.length, 1); From 3ffca40513907530abf24a2c6f3092626e542047 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Fri, 9 Aug 2024 10:17:34 +0200 Subject: [PATCH 141/979] refactor: use defaultClient instead of lichessClient --- lib/src/model/common/http.dart | 11 +++----- .../opening_explorer_repository.dart | 27 +++++++++++-------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/lib/src/model/common/http.dart b/lib/src/model/common/http.dart index 3c58a1be51..6ebb5f3f1a 100644 --- a/lib/src/model/common/http.dart +++ b/lib/src/model/common/http.dart @@ -37,16 +37,11 @@ final _logger = Logger('HttpClient'); const _maxCacheSize = 2 * 1024 * 1024; -const _openingExplorerEndpoints = ['/masters', '/lichess', '/player']; - /// Creates a Uri pointing to lichess server with the given unencoded path and query parameters. Uri lichessUri(String unencodedPath, [Map? queryParameters]) { - final host = _openingExplorerEndpoints.contains(unencodedPath) - ? kLichessOpeningExplorerHost - : kLichessHost; - return host.startsWith('localhost') - ? Uri.http(host, unencodedPath, queryParameters) - : Uri.https(host, unencodedPath, queryParameters); + return kLichessHost.startsWith('localhost') + ? Uri.http(kLichessHost, unencodedPath, queryParameters) + : Uri.https(kLichessHost, unencodedPath, queryParameters); } /// Creates the appropriate http client for the platform. diff --git a/lib/src/model/opening_explorer/opening_explorer_repository.dart b/lib/src/model/opening_explorer/opening_explorer_repository.dart index 8e29c31f77..6472966376 100644 --- a/lib/src/model/opening_explorer/opening_explorer_repository.dart +++ b/lib/src/model/opening_explorer/opening_explorer_repository.dart @@ -1,6 +1,8 @@ import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:http/http.dart'; +import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; @@ -24,7 +26,7 @@ Stream openingExplorer( if (cacheEntry != null && !cacheEntry.isIndexing) { yield cacheEntry.openingExplorerEntry; } else { - final client = ref.read(lichessClientProvider); + final client = ref.read(defaultClientProvider); final stream = switch (prefs.db) { OpeningDatabase.master => OpeningExplorerRepository(client) .getMasterDatabase( @@ -71,16 +73,17 @@ Stream openingExplorer( class OpeningExplorerRepository { const OpeningExplorerRepository(this.client); - final LichessClient client; + final Client client; Future getMasterDatabase( String fen, { int? since, }) { return client.readJson( - Uri( - path: '/masters', - queryParameters: { + Uri.https( + kLichessOpeningExplorerHost, + '/masters', + { 'fen': fen, if (since != null) 'since': since.toString(), }, @@ -96,9 +99,10 @@ class OpeningExplorerRepository { DateTime? since, }) { return client.readJson( - Uri( - path: '/lichess', - queryParameters: { + Uri.https( + kLichessOpeningExplorerHost, + '/lichess', + { 'fen': fen, if (speeds.isNotEmpty) 'speeds': speeds.map((speed) => speed.name).join(','), @@ -119,9 +123,10 @@ class OpeningExplorerRepository { DateTime? since, }) { return client.readNdJsonStream( - Uri( - path: '/player', - queryParameters: { + Uri.https( + kLichessOpeningExplorerHost, + '/player', + { 'fen': fen, 'player': usernameOrId, 'color': color.name, From 78937491d127789da89123d83a177cf4b145e77c Mon Sep 17 00:00:00 2001 From: Mauritz Date: Fri, 9 Aug 2024 10:45:44 +0200 Subject: [PATCH 142/979] refactor: address pr review on opening_explorer_screen --- .../opening_explorer_screen.dart | 66 +++++++++---------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index 8427936884..47eab264bc 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -209,10 +209,9 @@ class _OpeningExplorer extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final ctrlProvider = analysisControllerProvider(pgn, options); - final position = ref.watch(ctrlProvider.select((value) => value.position)); + final ctrlProvider = ref.watch(analysisControllerProvider(pgn, options)); - if (position.fullmoves > 24) { + if (ctrlProvider.position.fullmoves > 24) { return const Align( alignment: Alignment.center, child: Text('Max depth reached'), @@ -228,33 +227,24 @@ class _OpeningExplorer extends ConsumerWidget { ); } - final isRootNode = ref.watch( - ctrlProvider.select((s) => s.currentNode.isRoot), - ); - final nodeOpening = - ref.watch(ctrlProvider.select((s) => s.currentNode.opening)); - final branchOpening = - ref.watch(ctrlProvider.select((s) => s.currentBranchOpening)); - final contextOpening = - ref.watch(ctrlProvider.select((s) => s.contextOpening)); - final opening = isRootNode + final opening = ctrlProvider.currentNode.isRoot ? LightOpening( eco: '', name: context.l10n.startPosition, ) - : nodeOpening ?? branchOpening ?? contextOpening; - - final wikiBooksUrl = ref.watch(ctrlProvider.select((s) => s.wikiBooksUrl)); + : ctrlProvider.currentNode.opening ?? + ctrlProvider.currentBranchOpening ?? + ctrlProvider.contextOpening; final cache = ref.watch(openingExplorerCacheProvider); final isIndexing = cache .get( - OpeningExplorerCacheKey(fen: position.fen, prefs: prefs), + OpeningExplorerCacheKey(fen: ctrlProvider.position.fen, prefs: prefs), ) ?.isIndexing; final openingExplorerAsync = ref.watch( openingExplorerProvider( - fen: position.fen, + fen: ctrlProvider.position.fen, ), ); @@ -287,7 +277,7 @@ class _OpeningExplorer extends ConsumerWidget { flex: 75, child: _Opening( opening: opening, - wikiBooksUrl: wikiBooksUrl, + wikiBooksUrl: ctrlProvider.wikiBooksUrl, ), ), if (isIndexing != null && isIndexing) @@ -303,7 +293,8 @@ class _OpeningExplorer extends ConsumerWidget { whiteWins: openingExplorer.white, draws: openingExplorer.draws, blackWins: openingExplorer.black, - ctrlProvider: ctrlProvider, + pgn: pgn, + options: options, ), if (openingExplorer.topGames != null && openingExplorer.topGames!.isNotEmpty) @@ -327,9 +318,14 @@ class _OpeningExplorer extends ConsumerWidget { loading: () => const Center( child: CircularProgressIndicator(), ), - error: (error, stackTrace) => Center( - child: Text(error.toString()), - ), + error: (e, s) { + debugPrint( + 'SEVERE: [OpeningExplorerScreen] could not load opening explorer data; $e\n$s', + ); + return Center( + child: Text(e.toString()), + ); + }, ); } } @@ -417,14 +413,16 @@ class _MoveTable extends ConsumerWidget { required this.whiteWins, required this.draws, required this.blackWins, - required this.ctrlProvider, + required this.pgn, + required this.options, }); final IList moves; final int whiteWins; final int draws; final int blackWins; - final AnalysisControllerProvider ctrlProvider; + final String pgn; + final AnalysisOptions options; String formatNum(int num) => NumberFormat.decimalPatternDigits().format(num); @@ -433,6 +431,8 @@ class _MoveTable extends ConsumerWidget { const rowPadding = EdgeInsets.all(6.0); final games = whiteWins + draws + blackWins; + final ctrlProvider = analysisControllerProvider(pgn, options); + return Table( columnWidths: const { 0: FractionColumnWidth(0.15), @@ -445,15 +445,15 @@ class _MoveTable extends ConsumerWidget { color: Theme.of(context).colorScheme.primaryContainer, ), children: [ - Container( + Padding( padding: rowPadding, child: Text(context.l10n.move), ), - Container( + Padding( padding: rowPadding, child: Text(context.l10n.games), ), - Container( + Padding( padding: rowPadding, child: Text(context.l10n.whiteDrawBlack), ), @@ -475,7 +475,7 @@ class _MoveTable extends ConsumerWidget { onTap: () => ref .read(ctrlProvider.notifier) .onUserMove(Move.parse(move.uci)!), - child: Container( + child: Padding( padding: rowPadding, child: Text(move.san), ), @@ -484,7 +484,7 @@ class _MoveTable extends ConsumerWidget { onTap: () => ref .read(ctrlProvider.notifier) .onUserMove(Move.parse(move.uci)!), - child: Container( + child: Padding( padding: rowPadding, child: Text('${formatNum(move.games)} ($percentGames%)'), ), @@ -493,7 +493,7 @@ class _MoveTable extends ConsumerWidget { onTap: () => ref .read(ctrlProvider.notifier) .onUserMove(Move.parse(move.uci)!), - child: Container( + child: Padding( padding: rowPadding, child: _WinPercentageChart( whiteWins: move.white, @@ -518,11 +518,11 @@ class _MoveTable extends ConsumerWidget { alignment: Alignment.centerLeft, child: const Icon(Icons.functions), ), - Container( + Padding( padding: rowPadding, child: Text('${formatNum(games)} (100%)'), ), - Container( + Padding( padding: rowPadding, child: _WinPercentageChart( whiteWins: whiteWins, From ae154e2228ba21d8ccce63e49e32b8830f9ba166 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Fri, 9 Aug 2024 12:32:06 +0200 Subject: [PATCH 143/979] feat: update opening explorer screen bottom bar --- lib/src/view/analysis/analysis_screen.dart | 2 +- .../opening_explorer_screen.dart | 100 +----------------- lib/src/view/tools/tools_tab_screen.dart | 2 +- 3 files changed, 5 insertions(+), 99 deletions(-) diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index c165f17c32..e2e51c0e5a 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -627,7 +627,7 @@ class _BottomBar extends ConsumerWidget { Expanded( child: BottomBarButton( label: context.l10n.openingExplorer, - onTap: () => pushReplacementPlatformRoute( + onTap: () => pushPlatformRoute( context, builder: (_) => OpeningExplorerScreen( pgn: pgn, diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index 47eab264bc..83869e3e33 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -7,10 +7,8 @@ import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/game/game_repository_providers.dart'; -import 'package:lichess_mobile/src/model/game/game_share_service.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_preferences.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_repository.dart'; @@ -18,16 +16,12 @@ import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; -import 'package:lichess_mobile/src/utils/share.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_board.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; -import 'package:lichess_mobile/src/view/analysis/analysis_share_screen.dart'; import 'package:lichess_mobile/src/view/game/archived_game_screen.dart'; -import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; -import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -785,24 +779,9 @@ class _BottomBar extends ConsumerWidget { children: [ Expanded( child: BottomBarButton( - label: context.l10n.menu, - onTap: () { - _showAnalysisMenu(context, ref); - }, - icon: Icons.menu, - ), - ), - Expanded( - child: BottomBarButton( - label: context.l10n.analysis, - onTap: () => pushReplacementPlatformRoute( - context, - builder: (_) => AnalysisScreen( - pgnOrId: pgn, - options: options, - ), - ), - icon: Icons.biotech, + label: context.l10n.flipBoard, + onTap: () => ref.read(ctrlProvider.notifier).toggleBoard(), + icon: CupertinoIcons.arrow_2_squarepath, ), ), Expanded( @@ -841,77 +820,4 @@ class _BottomBar extends ConsumerWidget { void _moveBackward(WidgetRef ref) => ref .read(analysisControllerProvider(pgn, options).notifier) .userPrevious(); - - Future _showAnalysisMenu(BuildContext context, WidgetRef ref) { - return showAdaptiveActionSheet( - context: context, - actions: [ - BottomSheetAction( - makeLabel: (context) => Text(context.l10n.flipBoard), - onPressed: (context) { - ref - .read(analysisControllerProvider(pgn, options).notifier) - .toggleBoard(); - }, - ), - BottomSheetAction( - makeLabel: (context) => Text(context.l10n.mobileShareGamePGN), - onPressed: (_) { - pushPlatformRoute( - context, - title: context.l10n.studyShareAndExport, - builder: (_) => AnalysisShareScreen(pgn: pgn, options: options), - ); - }, - ), - BottomSheetAction( - makeLabel: (context) => Text(context.l10n.mobileSharePositionAsFEN), - onPressed: (_) { - launchShareDialog( - context, - text: ref - .read(analysisControllerProvider(pgn, options)) - .position - .fen, - ); - }, - ), - if (options.gameAnyId != null) - BottomSheetAction( - makeLabel: (context) => - Text(context.l10n.screenshotCurrentPosition), - onPressed: (_) async { - final gameId = options.gameAnyId!.gameId; - final state = ref.read(analysisControllerProvider(pgn, options)); - try { - final image = - await ref.read(gameShareServiceProvider).screenshotPosition( - gameId, - options.orientation, - state.position.fen, - state.lastMove, - ); - if (context.mounted) { - launchShareDialog( - context, - files: [image], - subject: context.l10n.puzzleFromGameLink( - lichessUri('/$gameId').toString(), - ), - ); - } - } catch (e) { - if (context.mounted) { - showPlatformSnackbar( - context, - 'Failed to get GIF', - type: SnackBarType.error, - ); - } - } - }, - ), - ], - ); - } } diff --git a/lib/src/view/tools/tools_tab_screen.dart b/lib/src/view/tools/tools_tab_screen.dart index 8a5088d0f2..97a6562c6f 100644 --- a/lib/src/view/tools/tools_tab_screen.dart +++ b/lib/src/view/tools/tools_tab_screen.dart @@ -160,7 +160,7 @@ class _Body extends StatelessWidget { builder: (context) => const OpeningExplorerScreen( pgn: '', options: AnalysisOptions( - isLocalEvaluationAllowed: true, + isLocalEvaluationAllowed: false, variant: Variant.standard, orientation: Side.white, id: standaloneAnalysisId, From 2949c68e0b5a645c0adadbee5ac7714c7aedca43 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Fri, 9 Aug 2024 12:42:07 +0200 Subject: [PATCH 144/979] feat: make opening name more visible --- lib/src/view/opening_explorer/opening_explorer_screen.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index 83869e3e33..b3c6fa40ae 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -17,7 +17,6 @@ import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_board.dart'; -import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; import 'package:lichess_mobile/src/view/game/archived_game_screen.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; @@ -340,6 +339,10 @@ class _Opening extends ConsumerWidget { final openingWidget = Text( '${opening.eco.isEmpty ? "" : "${opening.eco} "}${opening.name}', + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.bold, + ), ); return wikiBooksPageExistsAsync.when( From f9db8b422d8aace815c0962e3145684b0c8380fa Mon Sep 17 00:00:00 2001 From: Mauritz Date: Fri, 9 Aug 2024 13:33:00 +0200 Subject: [PATCH 145/979] fix: do not use lichessUri function anymore --- lib/src/model/common/http.dart | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/lib/src/model/common/http.dart b/lib/src/model/common/http.dart index 6ebb5f3f1a..c141274538 100644 --- a/lib/src/model/common/http.dart +++ b/lib/src/model/common/http.dart @@ -38,11 +38,10 @@ final _logger = Logger('HttpClient'); const _maxCacheSize = 2 * 1024 * 1024; /// Creates a Uri pointing to lichess server with the given unencoded path and query parameters. -Uri lichessUri(String unencodedPath, [Map? queryParameters]) { - return kLichessHost.startsWith('localhost') - ? Uri.http(kLichessHost, unencodedPath, queryParameters) - : Uri.https(kLichessHost, unencodedPath, queryParameters); -} +Uri lichessUri(String unencodedPath, [Map? queryParameters]) => + kLichessHost.startsWith('localhost') + ? Uri.http(kLichessHost, unencodedPath, queryParameters) + : Uri.https(kLichessHost, unencodedPath, queryParameters); /// Creates the appropriate http client for the platform. /// @@ -446,10 +445,8 @@ extension ClientExtension on Client { Map? headers, required T Function(Map) mapper, }) async { - final request = Request( - 'GET', - lichessUri(url.path, url.hasQuery ? url.queryParameters : null), - ); + final request = Request('GET', url); + if (headers != null) request.headers.addAll(headers); final response = await send(request); if (response.statusCode > 400) { var message = 'Request to $url failed with status ${response.statusCode}'; From 8f2f963a99518c7475cd2d50d5b5e8c4ada97ccd Mon Sep 17 00:00:00 2001 From: Felix Burk Date: Sun, 11 Aug 2024 08:10:40 +0200 Subject: [PATCH 146/979] Remove SafeArea to make clock more immersive --- lib/src/view/clock/clock_screen.dart | 32 +++++++++++++--------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/lib/src/view/clock/clock_screen.dart b/lib/src/view/clock/clock_screen.dart index 2254c53c8a..14e86dc312 100644 --- a/lib/src/view/clock/clock_screen.dart +++ b/lib/src/view/clock/clock_screen.dart @@ -28,25 +28,23 @@ class _Body extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final state = ref.watch(clockControllerProvider); - return SafeArea( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Expanded( - child: ClockTile( - playerType: ClockPlayerType.top, - clockState: state, - ), + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: ClockTile( + playerType: ClockPlayerType.top, + clockState: state, ), - const ClockSettings(), - Expanded( - child: ClockTile( - playerType: ClockPlayerType.bottom, - clockState: state, - ), + ), + const ClockSettings(), + Expanded( + child: ClockTile( + playerType: ClockPlayerType.bottom, + clockState: state, ), - ], - ), + ), + ], ); } } From 899df34a0ef2ad587ba84461bb7dd1374fa2c7ff Mon Sep 17 00:00:00 2001 From: Mauritz Date: Sun, 11 Aug 2024 10:54:54 +0200 Subject: [PATCH 147/979] refactor: improve cache implementation --- .../opening_explorer/opening_explorer.dart | 7 + .../opening_explorer_repository.dart | 130 +++++------------- .../opening_explorer_screen.dart | 74 ++++++---- 3 files changed, 89 insertions(+), 122 deletions(-) diff --git a/lib/src/model/opening_explorer/opening_explorer.dart b/lib/src/model/opening_explorer/opening_explorer.dart index d77ee38684..0ae434982e 100644 --- a/lib/src/model/opening_explorer/opening_explorer.dart +++ b/lib/src/model/opening_explorer/opening_explorer.dart @@ -22,6 +22,13 @@ class OpeningExplorerEntry with _$OpeningExplorerEntry { int? queuePosition, }) = _OpeningExplorerEntry; + factory OpeningExplorerEntry.empty() => const OpeningExplorerEntry( + white: 0, + draws: 0, + black: 0, + moves: IList.empty(), + ); + factory OpeningExplorerEntry.fromJson(Map json) => _$OpeningExplorerEntryFromJson(json); } diff --git a/lib/src/model/opening_explorer/opening_explorer_repository.dart b/lib/src/model/opening_explorer/opening_explorer_repository.dart index 6472966376..77599cb224 100644 --- a/lib/src/model/opening_explorer/opening_explorer_repository.dart +++ b/lib/src/model/opening_explorer/opening_explorer_repository.dart @@ -1,6 +1,5 @@ import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:http/http.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; @@ -10,63 +9,49 @@ import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_prefe import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'opening_explorer_repository.g.dart'; -part 'opening_explorer_repository.freezed.dart'; @riverpod -Stream openingExplorer( +Stream<({OpeningExplorerEntry entry, bool isIndexing})> openingExplorer( OpeningExplorerRef ref, { required String fen, }) async* { final prefs = ref.watch(openingExplorerPreferencesProvider); - final cacheKey = OpeningExplorerCacheKey( - fen: fen, - prefs: prefs, - ); - final cacheEntry = ref.read(openingExplorerCacheProvider).get(cacheKey); - if (cacheEntry != null && !cacheEntry.isIndexing) { - yield cacheEntry.openingExplorerEntry; - } else { - final client = ref.read(defaultClientProvider); - final stream = switch (prefs.db) { - OpeningDatabase.master => OpeningExplorerRepository(client) - .getMasterDatabase( - fen, - since: prefs.masterDb.sinceYear, - ) - .asStream(), - OpeningDatabase.lichess => OpeningExplorerRepository(client) - .getLichessDatabase( - fen, - speeds: prefs.lichessDb.speeds, - ratings: prefs.lichessDb.ratings, - since: prefs.lichessDb.since, - ) - .asStream(), - OpeningDatabase.player => - await OpeningExplorerRepository(client).getPlayerDatabase( - fen, - // null check handled by widget - usernameOrId: prefs.playerDb.usernameOrId!, - color: prefs.playerDb.side, - speeds: prefs.playerDb.speeds, - gameModes: prefs.playerDb.gameModes, - since: prefs.playerDb.since, - ), - }; + final client = ref.read(defaultClientProvider); + switch (prefs.db) { + case OpeningDatabase.master: + final openingExplorer = + await OpeningExplorerRepository(client).getMasterDatabase( + fen, + since: prefs.masterDb.sinceYear, + ); + yield (entry: openingExplorer, isIndexing: false); + case OpeningDatabase.lichess: + final openingExplorer = + await OpeningExplorerRepository(client).getLichessDatabase( + fen, + speeds: prefs.lichessDb.speeds, + ratings: prefs.lichessDb.ratings, + since: prefs.lichessDb.since, + ); + yield (entry: openingExplorer, isIndexing: false); + case OpeningDatabase.player: + final openingExplorerStream = + await OpeningExplorerRepository(client).getPlayerDatabase( + fen, + // null check handled by widget + usernameOrId: prefs.playerDb.usernameOrId!, + color: prefs.playerDb.side, + speeds: prefs.playerDb.speeds, + gameModes: prefs.playerDb.gameModes, + since: prefs.playerDb.since, + ); - await for (final openingExplorer in stream) { - ref.read(openingExplorerCacheProvider.notifier).addEntry( - cacheKey, - OpeningExplorerCacheEntry( - openingExplorerEntry: openingExplorer, - isIndexing: true, - ), - ); - yield openingExplorer; - } - ref - .read(openingExplorerCacheProvider.notifier) - .setIndexing(cacheKey, false); + OpeningExplorerEntry openingExplorer = OpeningExplorerEntry.empty(); + await for (final value in openingExplorerStream) { + openingExplorer = value; + yield (entry: openingExplorer, isIndexing: true); + } + yield (entry: openingExplorer, isIndexing: false); } } @@ -142,49 +127,6 @@ class OpeningExplorerRepository { } } -@riverpod -class OpeningExplorerCache extends _$OpeningExplorerCache { - @override - IMap build() { - return const IMap.empty(); - } - - void addEntry(OpeningExplorerCacheKey key, OpeningExplorerCacheEntry entry) { - state = state.add(key, entry); - } - - void setIndexing(OpeningExplorerCacheKey key, bool isIndexing) { - final entry = state.get(key); - if (entry != null) { - state = state.add( - key, - OpeningExplorerCacheEntry( - openingExplorerEntry: entry.openingExplorerEntry, - isIndexing: isIndexing, - ), - ); - } - } -} - -@freezed -class OpeningExplorerCacheKey with _$OpeningExplorerCacheKey { - const OpeningExplorerCacheKey._(); - - const factory OpeningExplorerCacheKey({ - required String fen, - required OpeningExplorerPrefState prefs, - }) = _OpeningExplorerCacheKey; -} - -@freezed -class OpeningExplorerCacheEntry with _$OpeningExplorerCacheEntry { - const factory OpeningExplorerCacheEntry({ - required OpeningExplorerEntry openingExplorerEntry, - required bool isIndexing, - }) = _OpeningExplorerCacheEntry; -} - @riverpod Future wikiBooksPageExists( WikiBooksPageExistsRef ref, { diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index b3c6fa40ae..49b324e23b 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -191,7 +191,7 @@ class _Body extends ConsumerWidget { } } -class _OpeningExplorer extends ConsumerWidget { +class _OpeningExplorer extends ConsumerStatefulWidget { const _OpeningExplorer({ required this.pgn, required this.options, @@ -201,8 +201,16 @@ class _OpeningExplorer extends ConsumerWidget { final AnalysisOptions options; @override - Widget build(BuildContext context, WidgetRef ref) { - final ctrlProvider = ref.watch(analysisControllerProvider(pgn, options)); + ConsumerState<_OpeningExplorer> createState() => _OpeningExplorerState(); +} + +class _OpeningExplorerState extends ConsumerState<_OpeningExplorer> { + final Map cache = {}; + + @override + Widget build(BuildContext context) { + final ctrlProvider = + ref.watch(analysisControllerProvider(widget.pgn, widget.options)); if (ctrlProvider.position.fullmoves > 24) { return const Align( @@ -212,6 +220,10 @@ class _OpeningExplorer extends ConsumerWidget { } final prefs = ref.watch(openingExplorerPreferencesProvider); + ref.listen( + openingExplorerPreferencesProvider, + (prev, cur) => cache.clear(), + ); if (prefs.db == OpeningDatabase.player && prefs.playerDb.usernameOrId == null) { return const Align( @@ -229,23 +241,29 @@ class _OpeningExplorer extends ConsumerWidget { ctrlProvider.currentBranchOpening ?? ctrlProvider.contextOpening; - final cache = ref.watch(openingExplorerCacheProvider); - final isIndexing = cache - .get( - OpeningExplorerCacheKey(fen: ctrlProvider.position.fen, prefs: prefs), - ) - ?.isIndexing; - final openingExplorerAsync = ref.watch( - openingExplorerProvider( - fen: ctrlProvider.position.fen, - ), - ); + final openingExplorerAsync = cache[ctrlProvider.position.fen] != null + ? AsyncValue.data( + (entry: cache[ctrlProvider.position.fen]!, isIndexing: false), + ) + : ref.watch(openingExplorerProvider(fen: ctrlProvider.position.fen)); + + if (cache[ctrlProvider.position.fen] == null) { + ref.listen(openingExplorerProvider(fen: ctrlProvider.position.fen), + (_, curAsync) { + curAsync.whenData((cur) { + if (!cur.isIndexing && + !cache.containsKey(ctrlProvider.position.fen)) { + cache[ctrlProvider.position.fen] = cur.entry; + } + }); + }); + } return openingExplorerAsync.when( data: (openingExplorer) { return Column( children: [ - if (openingExplorer.moves.isEmpty) + if (openingExplorer.entry.moves.isEmpty) const Expanded( child: Align( alignment: Alignment.center, @@ -273,7 +291,7 @@ class _OpeningExplorer extends ConsumerWidget { wikiBooksUrl: ctrlProvider.wikiBooksUrl, ), ), - if (isIndexing != null && isIndexing) + if (openingExplorer.isIndexing) Expanded( flex: 25, child: _IndexingIndicator(), @@ -282,24 +300,24 @@ class _OpeningExplorer extends ConsumerWidget { ), ), _MoveTable( - moves: openingExplorer.moves, - whiteWins: openingExplorer.white, - draws: openingExplorer.draws, - blackWins: openingExplorer.black, - pgn: pgn, - options: options, + moves: openingExplorer.entry.moves, + whiteWins: openingExplorer.entry.white, + draws: openingExplorer.entry.draws, + blackWins: openingExplorer.entry.black, + pgn: widget.pgn, + options: widget.options, ), - if (openingExplorer.topGames != null && - openingExplorer.topGames!.isNotEmpty) + if (openingExplorer.entry.topGames != null && + openingExplorer.entry.topGames!.isNotEmpty) _GameList( title: context.l10n.topGames, - games: openingExplorer.topGames!, + games: openingExplorer.entry.topGames!, ), - if (openingExplorer.recentGames != null && - openingExplorer.recentGames!.isNotEmpty) + if (openingExplorer.entry.recentGames != null && + openingExplorer.entry.recentGames!.isNotEmpty) _GameList( title: context.l10n.recentGames, - games: openingExplorer.recentGames!, + games: openingExplorer.entry.recentGames!, ), ], ), From baca45f9dd1caeb8efcdaa42b9a530341c84757d Mon Sep 17 00:00:00 2001 From: Mauritz Date: Sun, 11 Aug 2024 11:19:11 +0200 Subject: [PATCH 148/979] feat: include preferences in cache key again --- .../opening_explorer/opening_explorer.dart | 9 ++++++++ .../opening_explorer_screen.dart | 22 +++++++++---------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/lib/src/model/opening_explorer/opening_explorer.dart b/lib/src/model/opening_explorer/opening_explorer.dart index 0ae434982e..1bdeaba0a9 100644 --- a/lib/src/model/opening_explorer/opening_explorer.dart +++ b/lib/src/model/opening_explorer/opening_explorer.dart @@ -3,6 +3,7 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; +import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_preferences.dart'; part 'opening_explorer.freezed.dart'; part 'opening_explorer.g.dart'; @@ -107,3 +108,11 @@ enum GameMode { casual, rated, } + +@freezed +class OpeningExplorerCacheKey with _$OpeningExplorerCacheKey { + const factory OpeningExplorerCacheKey({ + required String fen, + required OpeningExplorerPrefState prefs, + }) = _OpeningExplorerCacheKey; +} diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index 49b324e23b..10e00777b5 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -205,7 +205,7 @@ class _OpeningExplorer extends ConsumerStatefulWidget { } class _OpeningExplorerState extends ConsumerState<_OpeningExplorer> { - final Map cache = {}; + final Map cache = {}; @override Widget build(BuildContext context) { @@ -220,10 +220,6 @@ class _OpeningExplorerState extends ConsumerState<_OpeningExplorer> { } final prefs = ref.watch(openingExplorerPreferencesProvider); - ref.listen( - openingExplorerPreferencesProvider, - (prev, cur) => cache.clear(), - ); if (prefs.db == OpeningDatabase.player && prefs.playerDb.usernameOrId == null) { return const Align( @@ -241,19 +237,23 @@ class _OpeningExplorerState extends ConsumerState<_OpeningExplorer> { ctrlProvider.currentBranchOpening ?? ctrlProvider.contextOpening; - final openingExplorerAsync = cache[ctrlProvider.position.fen] != null + final cacheKey = OpeningExplorerCacheKey( + fen: ctrlProvider.position.fen, + prefs: prefs, + ); + final cacheOpeningExplorer = cache[cacheKey]; + final openingExplorerAsync = cacheOpeningExplorer != null ? AsyncValue.data( - (entry: cache[ctrlProvider.position.fen]!, isIndexing: false), + (entry: cacheOpeningExplorer, isIndexing: false), ) : ref.watch(openingExplorerProvider(fen: ctrlProvider.position.fen)); - if (cache[ctrlProvider.position.fen] == null) { + if (cacheOpeningExplorer == null) { ref.listen(openingExplorerProvider(fen: ctrlProvider.position.fen), (_, curAsync) { curAsync.whenData((cur) { - if (!cur.isIndexing && - !cache.containsKey(ctrlProvider.position.fen)) { - cache[ctrlProvider.position.fen] = cur.entry; + if (!cur.isIndexing) { + cache[cacheKey] = cur.entry; } }); }); From e50019e7e0e3f89118a6b13085761af7ed8d356f Mon Sep 17 00:00:00 2001 From: Mauritz Date: Sun, 11 Aug 2024 12:02:24 +0200 Subject: [PATCH 149/979] feat: add move number cap for trying to fetch wikibooks url --- .../model/analysis/analysis_controller.dart | 7 ++-- .../opening_explorer_screen.dart | 34 +++++++++---------- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index 9e00a99b0a..2df9698a2b 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -457,7 +457,10 @@ class AnalysisController extends _$AnalysisController { } } - String _wikiBooksUrl(UciPath path) { + String? _wikiBooksUrl(UciPath path) { + if (_root.position.ply > 30) { + return null; + } final nodes = _root.branchesOn(path); final moves = nodes.map((node) { final move = node.view.sanMove.san; @@ -655,7 +658,7 @@ class AnalysisState with _$AnalysisState { Opening? currentBranchOpening, /// wikibooks.org opening theory page for the current path - required String wikiBooksUrl, + String? wikiBooksUrl, /// Optional server analysis to display player stats. ({PlayerAnalysis white, PlayerAnalysis black})? playersAnalysis, diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index 10e00777b5..e2f5dcb360 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -212,10 +212,10 @@ class _OpeningExplorerState extends ConsumerState<_OpeningExplorer> { final ctrlProvider = ref.watch(analysisControllerProvider(widget.pgn, widget.options)); - if (ctrlProvider.position.fullmoves > 24) { - return const Align( + if (ctrlProvider.position.ply >= 50) { + return Align( alignment: Alignment.center, - child: Text('Max depth reached'), + child: Text(context.l10n.maxDepthReached), ); } @@ -348,13 +348,9 @@ class _Opening extends ConsumerWidget { }); final Opening opening; - final String wikiBooksUrl; + final String? wikiBooksUrl; @override Widget build(BuildContext context, WidgetRef ref) { - final wikiBooksPageExistsAsync = ref.watch( - wikiBooksPageExistsProvider(url: wikiBooksUrl), - ); - final openingWidget = Text( '${opening.eco.isEmpty ? "" : "${opening.eco} "}${opening.name}', style: TextStyle( @@ -363,16 +359,18 @@ class _Opening extends ConsumerWidget { ), ); - return wikiBooksPageExistsAsync.when( - data: (wikiBooksPageExists) => wikiBooksPageExists - ? GestureDetector( - onTap: () => launchUrl(Uri.parse(wikiBooksUrl)), - child: openingWidget, - ) - : openingWidget, - loading: () => openingWidget, - error: (e, s) => openingWidget, - ); + return wikiBooksUrl == null + ? openingWidget + : ref.watch(wikiBooksPageExistsProvider(url: wikiBooksUrl!)).when( + data: (wikiBooksPageExists) => wikiBooksPageExists + ? GestureDetector( + onTap: () => launchUrl(Uri.parse(wikiBooksUrl!)), + child: openingWidget, + ) + : openingWidget, + loading: () => openingWidget, + error: (e, s) => openingWidget, + ); } } From 820c8c2e4536f456ceb9a7298e4e1b4e8ed3cf09 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Sun, 11 Aug 2024 12:37:11 +0200 Subject: [PATCH 150/979] feat: select current user as default for player db --- .../opening_explorer_preferences.dart | 52 ++++++++++++------- .../opening_explorer_repository.dart | 2 +- .../opening_explorer_screen.dart | 3 +- .../opening_explorer_settings.dart | 2 +- 4 files changed, 35 insertions(+), 24 deletions(-) diff --git a/lib/src/model/opening_explorer/opening_explorer_preferences.dart b/lib/src/model/opening_explorer/opening_explorer_preferences.dart index de8b0cd082..a335ab0d90 100644 --- a/lib/src/model/opening_explorer/opening_explorer_preferences.dart +++ b/lib/src/model/opening_explorer/opening_explorer_preferences.dart @@ -4,8 +4,10 @@ import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/db/shared_preferences.dart'; +import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; +import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'opening_explorer_preferences.freezed.dart'; @@ -18,13 +20,15 @@ class OpeningExplorerPreferences extends _$OpeningExplorerPreferences { @override OpeningExplorerPrefState build() { final prefs = ref.watch(sharedPreferencesProvider); + final session = ref.watch(authSessionProvider); final stored = prefs.getString(prefKey); return stored != null ? OpeningExplorerPrefState.fromJson( jsonDecode(stored) as Map, + user: session?.user, ) - : OpeningExplorerPrefState.defaults; + : OpeningExplorerPrefState.defaults(user: session?.user); } Future setDatabase(OpeningDatabase db) => _save( @@ -58,10 +62,10 @@ class OpeningExplorerPreferences extends _$OpeningExplorerPreferences { state.copyWith(lichessDb: state.lichessDb.copyWith(since: since)), ); - Future setPlayerDbUsernameOrId(String usernameOrId) => _save( + Future setPlayerDbUsernameOrId(String username) => _save( state.copyWith( playerDb: state.playerDb.copyWith( - usernameOrId: usernameOrId, + username: username, ), ), ); @@ -121,18 +125,22 @@ class OpeningExplorerPrefState with _$OpeningExplorerPrefState { required PlayerDbPrefState playerDb, }) = _OpeningExplorerPrefState; - static final defaults = OpeningExplorerPrefState( - db: OpeningDatabase.master, - masterDb: MasterDbPrefState.defaults, - lichessDb: LichessDbPrefState.defaults, - playerDb: PlayerDbPrefState.defaults, - ); + factory OpeningExplorerPrefState.defaults({LightUser? user}) => + OpeningExplorerPrefState( + db: OpeningDatabase.master, + masterDb: MasterDbPrefState.defaults, + lichessDb: LichessDbPrefState.defaults, + playerDb: PlayerDbPrefState.defaults(user: user), + ); - factory OpeningExplorerPrefState.fromJson(Map json) { + factory OpeningExplorerPrefState.fromJson( + Map json, { + LightUser? user, + }) { try { return _$OpeningExplorerPrefStateFromJson(json); } catch (_) { - return defaults; + return OpeningExplorerPrefState.defaults(user: user); } } } @@ -221,7 +229,7 @@ class PlayerDbPrefState with _$PlayerDbPrefState { const PlayerDbPrefState._(); const factory PlayerDbPrefState({ - String? usernameOrId, + String? username, required Side side, required ISet speeds, required ISet gameModes, @@ -245,18 +253,22 @@ class PlayerDbPrefState with _$PlayerDbPrefState { 'Last year': now.subtract(const Duration(days: 365)), 'All time': earliestDate, }; - static final defaults = PlayerDbPrefState( - side: Side.white, - speeds: kAvailableSpeeds, - gameModes: GameMode.values.toISet(), - since: earliestDate, - ); + factory PlayerDbPrefState.defaults({LightUser? user}) => PlayerDbPrefState( + username: user?.name, + side: Side.white, + speeds: kAvailableSpeeds, + gameModes: GameMode.values.toISet(), + since: earliestDate, + ); - factory PlayerDbPrefState.fromJson(Map json) { + factory PlayerDbPrefState.fromJson( + Map json, { + LightUser? user, + }) { try { return _$PlayerDbPrefStateFromJson(json); } catch (_) { - return defaults; + return PlayerDbPrefState.defaults(user: user); } } } diff --git a/lib/src/model/opening_explorer/opening_explorer_repository.dart b/lib/src/model/opening_explorer/opening_explorer_repository.dart index 77599cb224..e5375ca207 100644 --- a/lib/src/model/opening_explorer/opening_explorer_repository.dart +++ b/lib/src/model/opening_explorer/opening_explorer_repository.dart @@ -39,7 +39,7 @@ Stream<({OpeningExplorerEntry entry, bool isIndexing})> openingExplorer( await OpeningExplorerRepository(client).getPlayerDatabase( fen, // null check handled by widget - usernameOrId: prefs.playerDb.usernameOrId!, + usernameOrId: prefs.playerDb.username!, color: prefs.playerDb.side, speeds: prefs.playerDb.speeds, gameModes: prefs.playerDb.gameModes, diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index e2f5dcb360..b84ee15b1c 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -220,8 +220,7 @@ class _OpeningExplorerState extends ConsumerState<_OpeningExplorer> { } final prefs = ref.watch(openingExplorerPreferencesProvider); - if (prefs.db == OpeningDatabase.player && - prefs.playerDb.usernameOrId == null) { + if (prefs.db == OpeningDatabase.player && prefs.playerDb.username == null) { return const Align( alignment: Alignment.center, child: Text('Select a Lichess player in the settings'), diff --git a/lib/src/view/opening_explorer/opening_explorer_settings.dart b/lib/src/view/opening_explorer/opening_explorer_settings.dart index 404a21a4cf..3eea38f603 100644 --- a/lib/src/view/opening_explorer/opening_explorer_settings.dart +++ b/lib/src/view/opening_explorer/opening_explorer_settings.dart @@ -132,7 +132,7 @@ class OpeningExplorerSettings extends ConsumerWidget { fontSize: 18, decoration: TextDecoration.underline, ), - text: prefs.playerDb.usernameOrId ?? 'Select a Lichess player', + text: prefs.playerDb.username ?? 'Select a Lichess player', ), ], ), From 86c294db68a51b5afb7ed2b7b88750a668f0cce2 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Mon, 12 Aug 2024 08:41:42 +0200 Subject: [PATCH 151/979] test: add opening explorer screen widget tests checks that the screen loads with data, for all database types --- .../opening_explorer_screen.dart | 4 + .../opening_explorer_screen_test.dart | 341 ++++++++++++++++++ 2 files changed, 345 insertions(+) create mode 100644 test/view/opening_explorer/opening_explorer_screen_test.dart diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index b84ee15b1c..fcfb3bb0cd 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -446,6 +446,7 @@ class _MoveTable extends ConsumerWidget { final ctrlProvider = analysisControllerProvider(pgn, options); return Table( + key: const Key('moves-table'), columnWidths: const { 0: FractionColumnWidth(0.15), 1: FractionColumnWidth(0.35), @@ -561,8 +562,10 @@ class _GameList extends StatelessWidget { @override Widget build(BuildContext context) { return Column( + key: const Key('game-list'), children: [ Container( + key: const Key('game-list-title'), padding: const EdgeInsets.all(6.0), color: Theme.of(context).colorScheme.primaryContainer, child: Row( @@ -599,6 +602,7 @@ class _GameTile extends ConsumerWidget { const paddingResultBox = EdgeInsets.all(5); return Container( + key: const Key('game-tile'), padding: const EdgeInsets.all(6.0), color: color, child: AdaptiveInkWell( diff --git a/test/view/opening_explorer/opening_explorer_screen_test.dart b/test/view/opening_explorer/opening_explorer_screen_test.dart new file mode 100644 index 0000000000..8f5ce99cd0 --- /dev/null +++ b/test/view/opening_explorer/opening_explorer_screen_test.dart @@ -0,0 +1,341 @@ +import 'package:dartchess/dartchess.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/testing.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; +import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/common/http.dart'; +import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_preferences.dart'; +import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_screen.dart'; + +import '../../test_app.dart'; +import '../../test_utils.dart'; + +MockClient client(OpeningDatabase db) => MockClient((request) { + return request.url.host == 'explorer.lichess.ovh' + ? switch (db) { + OpeningDatabase.master => + mockResponse(mastersOpeningExplorerResponse, 200), + OpeningDatabase.lichess => + mockResponse(lichessOpeningExplorerResponse, 200), + OpeningDatabase.player => + mockResponse(playerOpeningExplorerResponse, 200), + } + : request.url.host == 'en.wikibooks.org' + ? mockResponse('', 200) + : mockResponse('', 404); + }); + +void main() { + const options = AnalysisOptions( + id: standaloneAnalysisId, + isLocalEvaluationAllowed: false, + orientation: Side.white, + variant: Variant.standard, + ); + + group('OpeningExplorerScreen', () { + testWidgets( + 'meets accessibility guidelines', + (WidgetTester tester) async { + final SemanticsHandle handle = tester.ensureSemantics(); + + final app = await buildTestApp( + tester, + home: const OpeningExplorerScreen( + pgn: '', + options: options, + ), + overrides: [ + defaultClientProvider + .overrideWithValue(client(OpeningDatabase.master)), + ], + ); + + await tester.pumpWidget(app); + + // wait for opening explorer data to load + await tester.pump(const Duration(milliseconds: 50)); + + await meetsTapTargetGuideline(tester); + + await tester.pump(const Duration(milliseconds: 50)); + + handle.dispose(); + }, + variant: kPlatformVariant, + ); + + testWidgets( + 'master opening explorer loads', + (WidgetTester tester) async { + final app = await buildTestApp( + tester, + home: const OpeningExplorerScreen( + pgn: '', + options: options, + ), + overrides: [ + defaultClientProvider + .overrideWithValue(client(OpeningDatabase.master)), + ], + ); + await tester.pumpWidget(app); + + // wait for opening explorer data to load + await tester.pump(const Duration(milliseconds: 50)); + + final moves = [ + 'e4', + 'd4', + ]; + expect(find.byKey(const Key('moves-table')), findsOneWidget); + for (final move in moves) { + expect(find.widgetWithText(TableRowInkWell, move), findsOneWidget); + } + + expect(find.widgetWithText(Container, 'Top games'), findsOneWidget); + expect(find.widgetWithText(Container, 'Recent games'), findsNothing); + expect( + find.byKey(const Key('game-list')), + findsOneWidget, + ); + expect( + find.byKey(const Key('game-tile')), + findsNWidgets(2), + ); + + await tester.pump(const Duration(milliseconds: 50)); + }, + variant: kPlatformVariant, + ); + + testWidgets( + 'lichess opening explorer loads', + (WidgetTester tester) async { + final app = await buildTestApp( + tester, + home: const OpeningExplorerScreen( + pgn: '', + options: options, + ), + overrides: [ + defaultClientProvider + .overrideWithValue(client(OpeningDatabase.lichess)), + ], + ); + await tester.pumpWidget(app); + + // wait for opening explorer data to load + await tester.pump(const Duration(milliseconds: 50)); + + final moves = [ + 'd4', + ]; + expect(find.byKey(const Key('moves-table')), findsOneWidget); + for (final move in moves) { + expect(find.widgetWithText(TableRowInkWell, move), findsOneWidget); + } + + expect(find.widgetWithText(Container, 'Top games'), findsNothing); + expect(find.widgetWithText(Container, 'Recent games'), findsOneWidget); + expect( + find.byKey(const Key('game-list')), + findsOneWidget, + ); + expect( + find.byKey(const Key('game-tile')), + findsOneWidget, + ); + + await tester.pump(const Duration(milliseconds: 50)); + }, + variant: kPlatformVariant, + ); + + testWidgets( + 'player opening explorer loads', + (WidgetTester tester) async { + final app = await buildTestApp( + tester, + home: const OpeningExplorerScreen( + pgn: '', + options: options, + ), + overrides: [ + defaultClientProvider + .overrideWithValue(client(OpeningDatabase.player)), + ], + ); + await tester.pumpWidget(app); + + // wait for opening explorer data to load + await tester.pump(const Duration(milliseconds: 50)); + + final moves = [ + 'c4', + ]; + expect(find.byKey(const Key('moves-table')), findsOneWidget); + for (final move in moves) { + expect(find.widgetWithText(TableRowInkWell, move), findsOneWidget); + } + + expect(find.widgetWithText(Container, 'Top games'), findsNothing); + expect(find.widgetWithText(Container, 'Recent games'), findsOneWidget); + expect( + find.byKey(const Key('game-list')), + findsOneWidget, + ); + expect( + find.byKey(const Key('game-tile')), + findsOneWidget, + ); + + await tester.pump(const Duration(milliseconds: 50)); + }, + variant: kPlatformVariant, + ); + }); +} + +const mastersOpeningExplorerResponse = ''' +{ + "white": 834333, + "draws": 1085272, + "black": 600303, + "moves": [ + { + "uci": "e2e4", + "san": "e4", + "averageRating": 2399, + "white": 372266, + "draws": 486092, + "black": 280238, + "game": null + }, + { + "uci": "d2d4", + "san": "d4", + "averageRating": 2414, + "white": 302160, + "draws": 397224, + "black": 209077, + "game": null + } + ], + "topGames": [ + { + "uci": "d2d4", + "id": "QR5UbqUY", + "winner": null, + "black": { + "name": "Caruana, F.", + "rating": 2818 + }, + "white": { + "name": "Carlsen, M.", + "rating": 2882 + }, + "year": 2019, + "month": "2019-08" + }, + { + "uci": "e2e4", + "id": "Sxov6E94", + "winner": "white", + "black": { + "name": "Carlsen, M.", + "rating": 2882 + }, + "white": { + "name": "Caruana, F.", + "rating": 2818 + }, + "year": 2019, + "month": "2019-08" + } + ], + "opening": null +} +'''; + +const lichessOpeningExplorerResponse = ''' +{ + "white": 2848672002, + "draws": 225287646, + "black": 2649860106, + "moves": [ + { + "uci": "d2d4", + "san": "d4", + "averageRating": 1604, + "white": 1661457614, + "draws": 129433754, + "black": 1565161663, + "game": null + } + ], + "recentGames": [ + { + "uci": "e2e4", + "id": "RVb19S9O", + "winner": "white", + "speed": "rapid", + "mode": "rated", + "black": { + "name": "Jcats1", + "rating": 1548 + }, + "white": { + "name": "carlosrivero32", + "rating": 1690 + }, + "year": 2024, + "month": "2024-06" + } + ], + "topGames": [], + "opening": null +} +'''; + +const playerOpeningExplorerResponse = ''' +{ + "white": 1713, + "draws": 119, + "black": 1459, + "moves": [ + { + "uci": "c2c4", + "san": "c4", + "averageOpponentRating": 1767, + "performance": 1796, + "white": 1691, + "draws": 116, + "black": 1432, + "game": null + } + ], + "recentGames": [ + { + "uci": "e2e4", + "id": "abc", + "winner": "white", + "speed": "bullet", + "mode": "rated", + "black": { + "name": "foo", + "rating": 1869 + }, + "white": { + "name": "baz", + "rating": 1912 + }, + "year": 2023, + "month": "2023-08" + } + ], + "opening": null, + "queuePosition": 0 +} +'''; From ca42a48b8ed7c9c56c8595fc9f2048f2e968c106 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Mon, 12 Aug 2024 13:15:12 +0200 Subject: [PATCH 152/979] fix: ignore pointer events when editing widgets Closes #915 --- lib/src/view/home/home_tab_screen.dart | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/src/view/home/home_tab_screen.dart b/lib/src/view/home/home_tab_screen.dart index 019fe2bf7d..e9f98b2356 100644 --- a/lib/src/view/home/home_tab_screen.dart +++ b/lib/src/view/home/home_tab_screen.dart @@ -372,7 +372,12 @@ class _EditableWidget extends ConsumerWidget { }, ), ), - Expanded(child: child), + Expanded( + child: IgnorePointer( + ignoring: isEditing, + child: child, + ), + ), ], ) : isEnabled From 2c91056f37bd911e0e86ab6d754a04f2866dfe21 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 13 Aug 2024 11:57:52 +0200 Subject: [PATCH 153/979] Upgrade dependencies --- pubspec.lock | 72 ++++++++++++++++++++++++++-------------------------- pubspec.yaml | 2 +- 2 files changed, 37 insertions(+), 37 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index e454a545cd..d79b3ba8e5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -42,10 +42,10 @@ packages: dependency: transitive description: name: ansicolor - sha256: "8bf17a8ff6ea17499e40a2d2542c2f481cd7615760c6d34065cb22bfd22e6880" + sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.0.3" app_settings: dependency: "direct main" description: @@ -242,10 +242,10 @@ packages: dependency: "direct main" description: name: connectivity_plus - sha256: "3e7d1d9dbae40ae82cbe6c23c518f0c4ffe32764ee9749b9a99d32cbac8734f6" + sha256: "2056db5241f96cdc0126bd94459fc4cdc13876753768fc7a31c425e50a7177d0" url: "https://pub.dev" source: hosted - version: "6.0.4" + version: "6.0.5" connectivity_plus_platform_interface: dependency: transitive description: @@ -370,10 +370,10 @@ packages: dependency: "direct main" description: name: device_info_plus - sha256: "93429694c9253d2871b3af80cf11b3cbb5c65660d402ed7bf69854ce4a089f82" + sha256: a7fd703482b391a87d60b6061d04dfdeab07826b96f9abd8f5ed98068acc0074 url: "https://pub.dev" source: hosted - version: "10.1.1" + version: "10.1.2" device_info_plus_platform_interface: dependency: transitive description: @@ -418,10 +418,10 @@ packages: dependency: transitive description: name: ffi - sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" file: dependency: transitive description: @@ -519,10 +519,10 @@ packages: dependency: "direct main" description: name: flutter_appauth - sha256: f2696d4cf437f627fa09bc4864afdd8c80273f2e293fde544b18202a627754b1 + sha256: "8492fb10afa2368d47a1c2784accafc64fa898ff9f36c47113799a142ca00043" url: "https://pub.dev" source: hosted - version: "6.0.6" + version: "6.0.7" flutter_appauth_platform_interface: dependency: transitive description: @@ -950,10 +950,10 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: "4de6c36df77ffbcef0a5aefe04669d33f2d18397fea228277b852a2d4e58e860" + sha256: a75164ade98cb7d24cfd0a13c6408927c6b217fa60dee5a7ff5c116a58f28918 url: "https://pub.dev" source: hosted - version: "8.0.1" + version: "8.0.2" package_info_plus_platform_interface: dependency: transitive description: @@ -990,10 +990,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "490539678396d4c3c0b06efdaab75ae60675c3e0c66f72bc04c2e2c1e0e2abeb" + sha256: "6f01f8e37ec30b07bc424b4deabac37cacb1bc7e2e515ad74486039918a37eb7" url: "https://pub.dev" source: hosted - version: "2.2.9" + version: "2.2.10" path_provider_foundation: dependency: transitive description: @@ -1150,18 +1150,18 @@ packages: dependency: "direct main" description: name: share_plus - sha256: ef3489a969683c4f3d0239010cc8b7a2a46543a8d139e111c06c558875083544 + sha256: "59dfd53f497340a0c3a81909b220cfdb9b8973a91055c4e5ab9b9b9ad7c513c0" url: "https://pub.dev" source: hosted - version: "9.0.0" + version: "10.0.0" share_plus_platform_interface: dependency: transitive description: name: share_plus_platform_interface - sha256: "0f9e4418835d1b2c3ae78fdb918251959106cefdbc4dd43526e182f80e82f6d4" + sha256: "6ababf341050edff57da8b6990f11f4e99eaba837865e2e6defe16d039619db5" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "5.0.0" shared_preferences: dependency: "direct main" description: @@ -1174,26 +1174,26 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: "041be4d9d2dc6079cf342bc8b761b03787e3b71192d658220a56cac9c04a0294" + sha256: a7e8467e9181cef109f601e3f65765685786c1a738a83d7fbbde377589c0d974 url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.1" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "671e7a931f55a08aa45be2a13fe7247f2a41237897df434b30d2012388191833" + sha256: c4b35f6cb8f63c147312c054ce7c2254c8066745125264f0c88739c417fc9d9f url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.5.2" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: "2ba0510d3017f91655b7543e9ee46d48619de2a2af38e5c790423f7007c7ccc1" + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" shared_preferences_platform_interface: dependency: transitive description: @@ -1206,18 +1206,18 @@ packages: dependency: transitive description: name: shared_preferences_web - sha256: "59dc807b94d29d52ddbb1b3c0d3b9d0a67fc535a64e62a5542c8db0513fcb6c2" + sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - sha256: "398084b47b7f92110683cac45c6dc4aae853db47e470e5ddcd52cab7f7196ab2" + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" shelf: dependency: transitive description: @@ -1452,10 +1452,10 @@ packages: dependency: transitive description: name: url_launcher_linux - sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 + sha256: e2b9622b4007f97f504cd64c0128309dfb978ae66adbe944125ed9e1750f06af url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.2.0" url_launcher_macos: dependency: transitive description: @@ -1476,10 +1476,10 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: a36e2d7981122fa185006b216eb6b5b97ede3f9a54b7a511bc966971ab98d049 + sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.3" url_launcher_windows: dependency: transitive description: @@ -1548,10 +1548,10 @@ packages: dependency: "direct main" description: name: wakelock_plus - sha256: "4fa83a128b4127619e385f686b4f080a5d2de46cff8e8c94eccac5fcf76550e5" + sha256: bf4ee6f17a2fa373ed3753ad0e602b7603f8c75af006d5b9bdade263928c0484 url: "https://pub.dev" source: hosted - version: "1.2.7" + version: "1.2.8" wakelock_plus_platform_interface: dependency: transitive description: @@ -1596,10 +1596,10 @@ packages: dependency: transitive description: name: win32 - sha256: "015002c060f1ae9f41a818f2d5640389cc05283e368be19dc8d77cecb43c40c9" + sha256: "68d1e89a91ed61ad9c370f9f8b6effed9ae5e0ede22a270bdfa6daf79fc2290a" url: "https://pub.dev" source: hosted - version: "5.5.3" + version: "5.5.4" win32_registry: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index e23e8a951f..f46fcae649 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -56,7 +56,7 @@ dependencies: pub_semver: ^2.1.4 result_extensions: ^0.1.0 riverpod_annotation: ^2.3.0 - share_plus: ^9.0.0 + share_plus: ^10.0.0 shared_preferences: ^2.1.0 signal_strength_indicator: ^0.4.1 sound_effect: ^0.0.2 From 84a40216a255de128f1176daddd2697bc19135a0 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Wed, 14 Aug 2024 11:59:17 +0200 Subject: [PATCH 154/979] refactor: use async notifier for opening explorer provider --- .../opening_explorer_repository.dart | 102 +++++++++++------- 1 file changed, 62 insertions(+), 40 deletions(-) diff --git a/lib/src/model/opening_explorer/opening_explorer_repository.dart b/lib/src/model/opening_explorer/opening_explorer_repository.dart index e5375ca207..6c2c2cf994 100644 --- a/lib/src/model/opening_explorer/opening_explorer_repository.dart +++ b/lib/src/model/opening_explorer/opening_explorer_repository.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:http/http.dart'; @@ -11,47 +13,67 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'opening_explorer_repository.g.dart'; @riverpod -Stream<({OpeningExplorerEntry entry, bool isIndexing})> openingExplorer( - OpeningExplorerRef ref, { - required String fen, -}) async* { - final prefs = ref.watch(openingExplorerPreferencesProvider); - final client = ref.read(defaultClientProvider); - switch (prefs.db) { - case OpeningDatabase.master: - final openingExplorer = - await OpeningExplorerRepository(client).getMasterDatabase( - fen, - since: prefs.masterDb.sinceYear, - ); - yield (entry: openingExplorer, isIndexing: false); - case OpeningDatabase.lichess: - final openingExplorer = - await OpeningExplorerRepository(client).getLichessDatabase( - fen, - speeds: prefs.lichessDb.speeds, - ratings: prefs.lichessDb.ratings, - since: prefs.lichessDb.since, - ); - yield (entry: openingExplorer, isIndexing: false); - case OpeningDatabase.player: - final openingExplorerStream = - await OpeningExplorerRepository(client).getPlayerDatabase( - fen, - // null check handled by widget - usernameOrId: prefs.playerDb.username!, - color: prefs.playerDb.side, - speeds: prefs.playerDb.speeds, - gameModes: prefs.playerDb.gameModes, - since: prefs.playerDb.since, - ); +class OpeningExplorer extends _$OpeningExplorer { + @override + Future<({OpeningExplorerEntry entry, bool isIndexing})> build({ + required String fen, + }) { + final prefs = ref.watch(openingExplorerPreferencesProvider); + final client = ref.read(defaultClientProvider); + switch (prefs.db) { + case OpeningDatabase.master: + final openingExplorerFuture = + OpeningExplorerRepository(client).getMasterDatabase( + fen, + since: prefs.masterDb.sinceYear, + ); + return openingExplorerFuture.then( + (openingExplorer) => (entry: openingExplorer, isIndexing: false), + ); + case OpeningDatabase.lichess: + final openingExplorerFuture = + OpeningExplorerRepository(client).getLichessDatabase( + fen, + speeds: prefs.lichessDb.speeds, + ratings: prefs.lichessDb.ratings, + since: prefs.lichessDb.since, + ); + return openingExplorerFuture.then( + (openingExplorer) => (entry: openingExplorer, isIndexing: false), + ); + case OpeningDatabase.player: + final openingExplorerStreamFuture = + OpeningExplorerRepository(client).getPlayerDatabase( + fen, + // null check handled by widget + usernameOrId: prefs.playerDb.username!, + color: prefs.playerDb.side, + speeds: prefs.playerDb.speeds, + gameModes: prefs.playerDb.gameModes, + since: prefs.playerDb.since, + ); - OpeningExplorerEntry openingExplorer = OpeningExplorerEntry.empty(); - await for (final value in openingExplorerStream) { - openingExplorer = value; - yield (entry: openingExplorer, isIndexing: true); - } - yield (entry: openingExplorer, isIndexing: false); + openingExplorerStreamFuture.then( + (openingExplorerStream) => openingExplorerStream.listen( + (openingExplorer) => state = + AsyncValue.data((entry: openingExplorer, isIndexing: true)), + onDone: () => state.value != null + ? state = AsyncValue.data( + (entry: state.value!.entry, isIndexing: false), + ) + : state = AsyncValue.error( + 'No opening explorer data returned for player ${prefs.playerDb.username}', + StackTrace.current, + ), + ), + ); + return Future.value( + ( + entry: OpeningExplorerEntry.empty(), + isIndexing: true, + ), + ); + } } } From ba871fe83aeffd16da6878f0fc994420f41c84f9 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 14 Aug 2024 15:47:05 +0200 Subject: [PATCH 155/979] Tweak nd-json list reader --- lib/src/model/common/http.dart | 36 +++++++++++++++++----------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/lib/src/model/common/http.dart b/lib/src/model/common/http.dart index 0c308bf32f..924d869f6b 100644 --- a/lib/src/model/common/http.dart +++ b/lib/src/model/common/http.dart @@ -419,18 +419,7 @@ extension ClientExtension on Client { }) async { final response = await get(url, headers: headers); _checkResponseSuccess(url, response); - try { - final json = LineSplitter.split(utf8.decode(response.bodyBytes)) - .where((e) => e.isNotEmpty && e != '\n') - .map((e) => jsonDecode(e) as Map); - return IList(json.map(mapper)); - } catch (e) { - _logger.severe('Could not read nd-json objects as List<$T>.'); - throw ClientException( - 'Could not read nd-json objects as List<$T>: $e', - url, - ); - } + return _readNdJsonList(response, mapper); } /// Sends an HTTP POST request with the given headers and body to the given URL and @@ -484,16 +473,27 @@ extension ClientExtension on Client { final response = await post(url, headers: headers, body: body, encoding: encoding); _checkResponseSuccess(url, response); + return _readNdJsonList(response, mapper); + } + + IList _readNdJsonList( + Response response, + T Function(Map) mapper, + ) { try { - final json = LineSplitter.split(utf8.decode(response.bodyBytes)) - .where((e) => e.isNotEmpty && e != '\n') - .map((e) => jsonDecode(e) as Map); - return IList(json.map(mapper)); + return IList( + LineSplitter.split(utf8.decode(response.bodyBytes)) + .where((e) => e.isNotEmpty && e != '\n') + .map((e) { + final json = jsonDecode(e) as Map; + return mapper(json); + }), + ); } catch (e) { _logger.severe('Could not read nd-json objects as List<$T>.'); throw ClientException( - 'Could not read nd-json objects as List<$T>.', - url, + 'Could not read nd-json objects as List<$T>: $e', + response.request?.url, ); } } From 92e1f5d2ccce3ec3a7d73cbc3a54e98460078553 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Thu, 15 Aug 2024 09:25:14 +0200 Subject: [PATCH 156/979] fix: update after pr review --- lib/src/model/common/http.dart | 6 +- .../opening_explorer_repository.dart | 61 ++++---- .../opening_explorer_screen.dart | 135 ++++++++++-------- test/test_app.dart | 3 +- .../opening_explorer_screen_test.dart | 76 +++++++--- 5 files changed, 159 insertions(+), 122 deletions(-) diff --git a/lib/src/model/common/http.dart b/lib/src/model/common/http.dart index c141274538..d33a182f86 100644 --- a/lib/src/model/common/http.dart +++ b/lib/src/model/common/http.dart @@ -459,8 +459,10 @@ extension ClientExtension on Client { return response.stream .map(utf8.decode) .where((e) => e.isNotEmpty && e != '\n') - .map((e) => jsonDecode(e) as Map) - .map(mapper); + .map((e) { + final json = jsonDecode(e) as Map; + return mapper(json); + }); } catch (e) { _logger.severe('Could not read nd-json object as $T.'); throw ClientException( diff --git a/lib/src/model/opening_explorer/opening_explorer_repository.dart b/lib/src/model/opening_explorer/opening_explorer_repository.dart index 6c2c2cf994..8a3d29651d 100644 --- a/lib/src/model/opening_explorer/opening_explorer_repository.dart +++ b/lib/src/model/opening_explorer/opening_explorer_repository.dart @@ -14,36 +14,38 @@ part 'opening_explorer_repository.g.dart'; @riverpod class OpeningExplorer extends _$OpeningExplorer { + StreamSubscription? _openingExplorerSubscription; + @override - Future<({OpeningExplorerEntry entry, bool isIndexing})> build({ + Future<({OpeningExplorerEntry entry, bool isIndexing})?> build({ required String fen, - }) { + }) async { + ref.onDispose(() { + _openingExplorerSubscription?.cancel(); + }); + final prefs = ref.watch(openingExplorerPreferencesProvider); final client = ref.read(defaultClientProvider); switch (prefs.db) { case OpeningDatabase.master: - final openingExplorerFuture = - OpeningExplorerRepository(client).getMasterDatabase( + final openingExplorer = + await OpeningExplorerRepository(client).getMasterDatabase( fen, since: prefs.masterDb.sinceYear, ); - return openingExplorerFuture.then( - (openingExplorer) => (entry: openingExplorer, isIndexing: false), - ); + return (entry: openingExplorer, isIndexing: false); case OpeningDatabase.lichess: - final openingExplorerFuture = - OpeningExplorerRepository(client).getLichessDatabase( + final openingExplorer = + await OpeningExplorerRepository(client).getLichessDatabase( fen, speeds: prefs.lichessDb.speeds, ratings: prefs.lichessDb.ratings, since: prefs.lichessDb.since, ); - return openingExplorerFuture.then( - (openingExplorer) => (entry: openingExplorer, isIndexing: false), - ); + return (entry: openingExplorer, isIndexing: false); case OpeningDatabase.player: - final openingExplorerStreamFuture = - OpeningExplorerRepository(client).getPlayerDatabase( + final openingExplorerStream = + await OpeningExplorerRepository(client).getPlayerDatabase( fen, // null check handled by widget usernameOrId: prefs.playerDb.username!, @@ -53,26 +55,19 @@ class OpeningExplorer extends _$OpeningExplorer { since: prefs.playerDb.since, ); - openingExplorerStreamFuture.then( - (openingExplorerStream) => openingExplorerStream.listen( - (openingExplorer) => state = - AsyncValue.data((entry: openingExplorer, isIndexing: true)), - onDone: () => state.value != null - ? state = AsyncValue.data( - (entry: state.value!.entry, isIndexing: false), - ) - : state = AsyncValue.error( - 'No opening explorer data returned for player ${prefs.playerDb.username}', - StackTrace.current, - ), - ), - ); - return Future.value( - ( - entry: OpeningExplorerEntry.empty(), - isIndexing: true, - ), + _openingExplorerSubscription = openingExplorerStream.listen( + (openingExplorer) => state = + AsyncValue.data((entry: openingExplorer, isIndexing: true)), + onDone: () => state.value != null + ? state = AsyncValue.data( + (entry: state.value!.entry, isIndexing: false), + ) + : state = AsyncValue.error( + 'No opening explorer data returned for player ${prefs.playerDb.username}', + StackTrace.current, + ), ); + return null; } } } diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index fcfb3bb0cd..3f7d7e46ee 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -209,10 +209,10 @@ class _OpeningExplorerState extends ConsumerState<_OpeningExplorer> { @override Widget build(BuildContext context) { - final ctrlProvider = + final analysisState = ref.watch(analysisControllerProvider(widget.pgn, widget.options)); - if (ctrlProvider.position.ply >= 50) { + if (analysisState.position.ply >= 50) { return Align( alignment: Alignment.center, child: Text(context.l10n.maxDepthReached), @@ -227,17 +227,17 @@ class _OpeningExplorerState extends ConsumerState<_OpeningExplorer> { ); } - final opening = ctrlProvider.currentNode.isRoot + final opening = analysisState.currentNode.isRoot ? LightOpening( eco: '', name: context.l10n.startPosition, ) - : ctrlProvider.currentNode.opening ?? - ctrlProvider.currentBranchOpening ?? - ctrlProvider.contextOpening; + : analysisState.currentNode.opening ?? + analysisState.currentBranchOpening ?? + analysisState.contextOpening; final cacheKey = OpeningExplorerCacheKey( - fen: ctrlProvider.position.fen, + fen: analysisState.position.fen, prefs: prefs, ); final cacheOpeningExplorer = cache[cacheKey]; @@ -245,13 +245,13 @@ class _OpeningExplorerState extends ConsumerState<_OpeningExplorer> { ? AsyncValue.data( (entry: cacheOpeningExplorer, isIndexing: false), ) - : ref.watch(openingExplorerProvider(fen: ctrlProvider.position.fen)); + : ref.watch(openingExplorerProvider(fen: analysisState.position.fen)); if (cacheOpeningExplorer == null) { - ref.listen(openingExplorerProvider(fen: ctrlProvider.position.fen), + ref.listen(openingExplorerProvider(fen: analysisState.position.fen), (_, curAsync) { curAsync.whenData((cur) { - if (!cur.isIndexing) { + if (cur != null && !cur.isIndexing) { cache[cacheKey] = cur.entry; } }); @@ -260,68 +260,77 @@ class _OpeningExplorerState extends ConsumerState<_OpeningExplorer> { return openingExplorerAsync.when( data: (openingExplorer) { - return Column( - children: [ - if (openingExplorer.entry.moves.isEmpty) - const Expanded( + if (openingExplorer == null) { + return const Center( + child: CircularProgressIndicator(), + ); + } + if (openingExplorer.entry.moves.isEmpty) { + return const Column( + children: [ + Expanded( child: Align( alignment: Alignment.center, child: Text('No game found'), ), - ) - else - Expanded( - child: SingleChildScrollView( - scrollDirection: Axis.vertical, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - padding: const EdgeInsets.symmetric(horizontal: 6.0), - color: Theme.of(context).colorScheme.primaryContainer, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - if (opening != null) - Expanded( - flex: 75, - child: _Opening( - opening: opening, - wikiBooksUrl: ctrlProvider.wikiBooksUrl, - ), - ), - if (openingExplorer.isIndexing) - Expanded( - flex: 25, - child: _IndexingIndicator(), + ), + ], + ); + } + return Column( + children: [ + Expanded( + child: SingleChildScrollView( + scrollDirection: Axis.vertical, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 6.0), + color: Theme.of(context).colorScheme.primaryContainer, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (opening != null) + Expanded( + flex: 75, + child: _Opening( + opening: opening, + wikiBooksUrl: analysisState.wikiBooksUrl, ), - ], - ), + ), + if (openingExplorer.isIndexing) + Expanded( + flex: 25, + child: _IndexingIndicator(), + ), + ], ), - _MoveTable( - moves: openingExplorer.entry.moves, - whiteWins: openingExplorer.entry.white, - draws: openingExplorer.entry.draws, - blackWins: openingExplorer.entry.black, - pgn: widget.pgn, - options: widget.options, + ), + _MoveTable( + moves: openingExplorer.entry.moves, + whiteWins: openingExplorer.entry.white, + draws: openingExplorer.entry.draws, + blackWins: openingExplorer.entry.black, + pgn: widget.pgn, + options: widget.options, + ), + if (openingExplorer.entry.topGames != null && + openingExplorer.entry.topGames!.isNotEmpty) + _GameList( + title: context.l10n.topGames, + games: openingExplorer.entry.topGames!, ), - if (openingExplorer.entry.topGames != null && - openingExplorer.entry.topGames!.isNotEmpty) - _GameList( - title: context.l10n.topGames, - games: openingExplorer.entry.topGames!, - ), - if (openingExplorer.entry.recentGames != null && - openingExplorer.entry.recentGames!.isNotEmpty) - _GameList( - title: context.l10n.recentGames, - games: openingExplorer.entry.recentGames!, - ), - ], - ), + if (openingExplorer.entry.recentGames != null && + openingExplorer.entry.recentGames!.isNotEmpty) + _GameList( + title: context.l10n.recentGames, + games: openingExplorer.entry.recentGames!, + ), + ], ), ), + ), ], ); }, diff --git a/test/test_app.dart b/test/test_app.dart index d8a39d3a02..4e897ca89c 100644 --- a/test/test_app.dart +++ b/test/test_app.dart @@ -51,12 +51,13 @@ Future buildTestApp( required Widget home, List? overrides, AuthSessionState? userSession, + Map? defaultPreferences, }) async { await tester.binding.setSurfaceSize(kTestSurfaceSize); VisibilityDetectorController.instance.updateInterval = Duration.zero; - SharedPreferences.setMockInitialValues({}); + SharedPreferences.setMockInitialValues(defaultPreferences ?? {}); final sharedPreferences = await SharedPreferences.getInstance(); diff --git a/test/view/opening_explorer/opening_explorer_screen_test.dart b/test/view/opening_explorer/opening_explorer_screen_test.dart index 8f5ce99cd0..bcd6a0d8a5 100644 --- a/test/view/opening_explorer/opening_explorer_screen_test.dart +++ b/test/view/opening_explorer/opening_explorer_screen_test.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:dartchess/dartchess.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -5,28 +7,33 @@ import 'package:http/testing.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_preferences.dart'; +import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_screen.dart'; import '../../test_app.dart'; import '../../test_utils.dart'; -MockClient client(OpeningDatabase db) => MockClient((request) { - return request.url.host == 'explorer.lichess.ovh' - ? switch (db) { - OpeningDatabase.master => - mockResponse(mastersOpeningExplorerResponse, 200), - OpeningDatabase.lichess => - mockResponse(lichessOpeningExplorerResponse, 200), - OpeningDatabase.player => - mockResponse(playerOpeningExplorerResponse, 200), - } - : request.url.host == 'en.wikibooks.org' - ? mockResponse('', 200) - : mockResponse('', 404); - }); - void main() { + final mockClient = MockClient((request) { + if (request.url.host == 'explorer.lichess.ovh') { + if (request.url.path == '/masters') { + return mockResponse(mastersOpeningExplorerResponse, 200); + } + if (request.url.path == '/lichess') { + return mockResponse(lichessOpeningExplorerResponse, 200); + } + if (request.url.path == '/player') { + return mockResponse(playerOpeningExplorerResponse, 200); + } + } + if (request.url.host == 'en.wikibooks.org') { + return mockResponse('', 200); + } + return mockResponse('', 404); + }); + const options = AnalysisOptions( id: standaloneAnalysisId, isLocalEvaluationAllowed: false, @@ -34,6 +41,12 @@ void main() { variant: Variant.standard, ); + const name = 'John'; + final user = LightUser( + id: UserId.fromUserName(name), + name: name, + ); + group('OpeningExplorerScreen', () { testWidgets( 'meets accessibility guidelines', @@ -47,8 +60,7 @@ void main() { options: options, ), overrides: [ - defaultClientProvider - .overrideWithValue(client(OpeningDatabase.master)), + defaultClientProvider.overrideWithValue(mockClient), ], ); @@ -76,9 +88,15 @@ void main() { options: options, ), overrides: [ - defaultClientProvider - .overrideWithValue(client(OpeningDatabase.master)), + defaultClientProvider.overrideWithValue(mockClient), ], + defaultPreferences: { + OpeningExplorerPreferences.prefKey: jsonEncode( + OpeningExplorerPrefState.defaults() + .copyWith(db: OpeningDatabase.master) + .toJson(), + ), + }, ); await tester.pumpWidget(app); @@ -120,9 +138,15 @@ void main() { options: options, ), overrides: [ - defaultClientProvider - .overrideWithValue(client(OpeningDatabase.lichess)), + defaultClientProvider.overrideWithValue(mockClient), ], + defaultPreferences: { + OpeningExplorerPreferences.prefKey: jsonEncode( + OpeningExplorerPrefState.defaults() + .copyWith(db: OpeningDatabase.lichess) + .toJson(), + ), + }, ); await tester.pumpWidget(app); @@ -163,9 +187,15 @@ void main() { options: options, ), overrides: [ - defaultClientProvider - .overrideWithValue(client(OpeningDatabase.player)), + defaultClientProvider.overrideWithValue(mockClient), ], + defaultPreferences: { + OpeningExplorerPreferences.prefKey: jsonEncode( + OpeningExplorerPrefState.defaults(user: user) + .copyWith(db: OpeningDatabase.player) + .toJson(), + ), + }, ); await tester.pumpWidget(app); From 81bbcdcf8ae6af8981fbd4a7b6bc56defa2ee109 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 27 Aug 2024 12:33:57 +0200 Subject: [PATCH 157/979] Upgrade dependencies, fix lint errors. Closes #939 --- lib/src/model/auth/auth_repository.dart | 6 -- lib/src/model/common/eval.dart | 2 +- lib/src/model/puzzle/puzzle_session.dart | 2 +- lib/src/model/user/profile.dart | 2 +- pubspec.lock | 121 ++++++++++++----------- pubspec.yaml | 9 +- 6 files changed, 70 insertions(+), 72 deletions(-) diff --git a/lib/src/model/auth/auth_repository.dart b/lib/src/model/auth/auth_repository.dart index 3311f11a40..bc7a302845 100644 --- a/lib/src/model/auth/auth_repository.dart +++ b/lib/src/model/auth/auth_repository.dart @@ -50,12 +50,6 @@ class AuthRepository { ), ); - if (authResp == null) { - throw Exception( - 'FlutterAppAuth.authorizeAndExchangeCode failed to get token', - ); - } - _log.fine('Got oAuth response $authResp'); final token = authResp.accessToken; diff --git a/lib/src/model/common/eval.dart b/lib/src/model/common/eval.dart index 3835f67a60..0691c850e7 100644 --- a/lib/src/model/common/eval.dart +++ b/lib/src/model/common/eval.dart @@ -86,7 +86,7 @@ class ClientEval with _$ClientEval implements Eval { return pvs .where((e) => e.moves.isNotEmpty) .map((e) => e._firstMoveWithWinningChances(position.turn)) - .whereNotNull() + .nonNulls .sorted((a, b) => b.winningChances.compareTo(a.winningChances)) .toIList(); } diff --git a/lib/src/model/puzzle/puzzle_session.dart b/lib/src/model/puzzle/puzzle_session.dart index 609112286a..031331dbda 100644 --- a/lib/src/model/puzzle/puzzle_session.dart +++ b/lib/src/model/puzzle/puzzle_session.dart @@ -63,7 +63,7 @@ class PuzzleSession extends _$PuzzleSession { Future _update( PuzzleSessionData Function(PuzzleSessionData d) update, ) async { - await _store.setString(_storageKey, jsonEncode((update(state)).toJson())); + await _store.setString(_storageKey, jsonEncode(update(state).toJson())); } PuzzleSessionData? get _stored { diff --git a/lib/src/model/user/profile.dart b/lib/src/model/user/profile.dart index 932325ea91..a6d708a938 100644 --- a/lib/src/model/user/profile.dart +++ b/lib/src/model/user/profile.dart @@ -53,7 +53,7 @@ class Profile with _$Profile { } return link; }) - .whereNotNull() + .nonNulls .toIList(), ); } diff --git a/pubspec.lock b/pubspec.lock index d79b3ba8e5..0084f24bee 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834 + sha256: "45cfa8471b89fb6643fe9bf51bd7931a76b8f5ec2d65de4fb176dba8d4f22c77" url: "https://pub.dev" source: hosted - version: "72.0.0" + version: "73.0.0" _flutterfire_internals: dependency: transitive description: @@ -26,10 +26,10 @@ packages: dependency: transitive description: name: analyzer - sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139 + sha256: "4959fec185fe70cce007c57e9ab6983101dbe593d2bf8bbfb4453aaec0cf470a" url: "https://pub.dev" source: hosted - version: "6.7.0" + version: "6.8.0" analyzer_plugin: dependency: transitive description: @@ -162,10 +162,10 @@ packages: dependency: transitive description: name: cached_network_image_platform_interface - sha256: ff0c949e323d2a1b52be73acce5b4a7b04063e61414c8ca542dbba47281630a7 + sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" url: "https://pub.dev" source: hosted - version: "4.1.0" + version: "4.1.1" cached_network_image_web: dependency: transitive description: @@ -234,10 +234,10 @@ packages: dependency: "direct main" description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.0" connectivity_plus: dependency: "direct main" description: @@ -282,10 +282,10 @@ packages: dependency: "direct main" description: name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27 url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.5" csslib: dependency: transitive description: @@ -314,26 +314,26 @@ packages: dependency: "direct dev" description: name: custom_lint - sha256: "7c0aec12df22f9082146c354692056677f1e70bc43471644d1fdb36c6fdda799" + sha256: "4939d89e580c36215e48a7de8fd92f22c79dcc3eb11fda84f3402b3b45aec663" url: "https://pub.dev" source: hosted - version: "0.6.4" + version: "0.6.5" custom_lint_builder: dependency: transitive description: name: custom_lint_builder - sha256: d7dc41e709dde223806660268678be7993559e523eb3164e2a1425fd6f7615a9 + sha256: d9e5bb63ed52c1d006f5a1828992ba6de124c27a531e8fba0a31afffa81621b3 url: "https://pub.dev" source: hosted - version: "0.6.4" + version: "0.6.5" custom_lint_core: dependency: transitive description: name: custom_lint_core - sha256: a85e8f78f4c52f6c63cdaf8c872eb573db0231dcdf3c3a5906d493c1f8bc20e6 + sha256: "4ddbbdaa774265de44c97054dcec058a83d9081d071785ece601e348c18c267d" url: "https://pub.dev" source: hosted - version: "0.6.3" + version: "0.6.5" dart_style: dependency: transitive description: @@ -506,10 +506,10 @@ packages: dependency: "direct main" description: name: fl_chart - sha256: d0f0d49112f2f4b192481c16d05b6418bd7820e021e265a3c22db98acf7ed7fb + sha256: "94307bef3a324a0d329d3ab77b2f0c6e5ed739185ffc029ed28c0f9b019ea7ef" url: "https://pub.dev" source: hosted - version: "0.68.0" + version: "0.69.0" flutter: dependency: "direct main" description: flutter @@ -519,26 +519,26 @@ packages: dependency: "direct main" description: name: flutter_appauth - sha256: "8492fb10afa2368d47a1c2784accafc64fa898ff9f36c47113799a142ca00043" + sha256: e0c861713626a24ab31d3f03c7d04048e58cb30b01be9b4b5a05eee510f2d561 url: "https://pub.dev" source: hosted - version: "6.0.7" + version: "7.0.0" flutter_appauth_platform_interface: dependency: transitive description: name: flutter_appauth_platform_interface - sha256: "44feaa7058191b5d3cd7c9ff195262725773643121bcada172d49c2ddcff71cb" + sha256: fede468075c149200ba792520cf16fee5ed7750fc446a0839c42fcc9364e31b8 url: "https://pub.dev" source: hosted - version: "6.0.0" + version: "7.0.0" flutter_cache_manager: dependency: transitive description: name: flutter_cache_manager - sha256: a77f77806a790eb9ba0118a5a3a936e81c4fea2b61533033b2b0c3d50bbde5ea + sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" url: "https://pub.dev" source: hosted - version: "3.4.0" + version: "3.4.1" flutter_displaymode: dependency: "direct main" description: @@ -635,10 +635,11 @@ packages: flutter_slidable: dependency: "direct main" description: - name: flutter_slidable - sha256: "2c5611c0b44e20d180e4342318e1bbc28b0a44ad2c442f5df16962606fd3e8e3" - url: "https://pub.dev" - source: hosted + path: "." + ref: master + resolved-ref: "3280106581fc8d54eae45f4a446f92cae36d7837" + url: "https://github.com/letsar/flutter_slidable.git" + source: git version: "3.1.1" flutter_spinkit: dependency: "direct main" @@ -742,10 +743,10 @@ packages: dependency: transitive description: name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + sha256: "40f592dd352890c3b60fec1b68e786cefb9603e05ff303dbc4dda49b304ecdf4" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.1.0" http_profile: dependency: transitive description: @@ -894,10 +895,10 @@ packages: dependency: transitive description: name: mime - sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" + sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "1.0.6" mockito: dependency: transitive description: @@ -1062,10 +1063,10 @@ packages: dependency: "direct main" description: name: popover - sha256: "5cba40e04115cbbf15c35e00767b91e8bf3f769763a34beb2f8a1b9e8b5fc876" + sha256: "0606f3e10f92fc0459f5c52fd917738c29e7552323b28694d50c2d3312d0e1a2" url: "https://pub.dev" source: hosted - version: "0.3.0+1" + version: "0.3.1" pub_semver: dependency: "direct main" description: @@ -1110,10 +1111,10 @@ packages: dependency: transitive description: name: riverpod_analyzer_utils - sha256: ee72770090078e6841d51355292335f1bc254907c6694283389dcb8156d99a4d + sha256: ac28d7bc678471ec986b42d88e5a0893513382ff7542c7ac9634463b044ac72c url: "https://pub.dev" source: hosted - version: "0.5.3" + version: "0.5.4" riverpod_annotation: dependency: "direct main" description: @@ -1126,26 +1127,26 @@ packages: dependency: "direct dev" description: name: riverpod_generator - sha256: "1ad626afbd8b01d168870b13c0b036f8a5bdb57c14cd426dc5b4595466bd6e2f" + sha256: "63311e361ffc578d655dfc31b48dfa4ed3bc76fd06f9be845e9bf97c5c11a429" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.3" riverpod_lint: dependency: "direct dev" description: name: riverpod_lint - sha256: b95a8cdc6102397f7d51037131c25ce7e51be900be021af4bf0c2d6f1b8f7aa7 + sha256: a35a92f2c2a4b7a5d95671c96c5432b42c20f26bb3e985e83d0b186471b61a85 url: "https://pub.dev" source: hosted - version: "2.3.12" + version: "2.3.13" rxdart: dependency: transitive description: name: rxdart - sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" url: "https://pub.dev" source: hosted - version: "0.27.7" + version: "0.28.0" share_plus: dependency: "direct main" description: @@ -1166,18 +1167,18 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: c272f9cabca5a81adc9b0894381e9c1def363e980f960fa903c604c471b22f68 + sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: a7e8467e9181cef109f601e3f65765685786c1a738a83d7fbbde377589c0d974 + sha256: "480ba4345773f56acda9abf5f50bd966f581dac5d514e5fc4a18c62976bbba7e" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" shared_preferences_foundation: dependency: transitive description: @@ -1222,10 +1223,10 @@ packages: dependency: transitive description: name: shelf - sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.2" shelf_web_socket: dependency: transitive description: @@ -1299,10 +1300,10 @@ packages: dependency: transitive description: name: sqflite_common - sha256: "3da423ce7baf868be70e2c0976c28a1bb2f73644268b7ffa7d2e08eab71f16a4" + sha256: "7b41b6c3507854a159e24ae90a8e3e9cc01eb26a477c118d6dca065b5f55453e" url: "https://pub.dev" source: hosted - version: "2.5.4" + version: "2.5.4+2" sqflite_common_ffi: dependency: "direct dev" description: @@ -1364,18 +1365,18 @@ packages: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" synchronized: dependency: transitive description: name: synchronized - sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" + sha256: a824e842b8a054f91a728b783c177c1e4731f6b124f9192468457a8913371255 url: "https://pub.dev" source: hosted - version: "3.1.0+1" + version: "3.2.0" term_glyph: dependency: transitive description: @@ -1388,10 +1389,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.7.3" timeago: dependency: "direct main" description: @@ -1436,10 +1437,10 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "94d8ad05f44c6d4e2ffe5567ab4d741b82d62e3c8e288cc1fcea45965edf47c9" + sha256: e35a698ac302dd68e41f73250bd9517fe3ab5fa4f18fe4647a0872db61bacbab url: "https://pub.dev" source: hosted - version: "6.3.8" + version: "6.3.10" url_launcher_ios: dependency: transitive description: @@ -1633,5 +1634,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.5.0-259.0.dev <4.0.0" - flutter: ">=3.22.0" + dart: ">=3.5.0 <4.0.0" + flutter: ">=3.24.0" diff --git a/pubspec.yaml b/pubspec.yaml index f46fcae649..c93316d418 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -26,10 +26,10 @@ dependencies: firebase_core: ^3.0.0 firebase_crashlytics: ^4.0.0 firebase_messaging: ^15.0.0 - fl_chart: ^0.68.0 + fl_chart: ^0.69.0 flutter: sdk: flutter - flutter_appauth: ^6.0.0 + flutter_appauth: ^7.0.0 flutter_displaymode: ^0.6.0 flutter_layout_grid: ^2.0.1 flutter_linkify: ^6.0.0 @@ -38,7 +38,10 @@ dependencies: flutter_native_splash: ^2.3.5 flutter_riverpod: ^2.3.4 flutter_secure_storage: ^9.2.0 - flutter_slidable: ^3.0.0 + flutter_slidable: + git: + url: https://github.com/letsar/flutter_slidable.git + ref: master flutter_spinkit: ^5.2.0 flutter_svg: ^2.0.10+1 freezed_annotation: ^2.2.0 From b0b981ab7550087443a169cbde3ef9f5682e42b4 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 28 Aug 2024 08:38:52 +0200 Subject: [PATCH 158/979] Improve opening explorer loading and style --- .../opening_explorer_preferences.dart | 4 +- .../opening_explorer_screen.dart | 309 +++++++++++------- .../opening_explorer_settings.dart | 6 - lib/src/widgets/buttons.dart | 13 +- 4 files changed, 189 insertions(+), 143 deletions(-) diff --git a/lib/src/model/opening_explorer/opening_explorer_preferences.dart b/lib/src/model/opening_explorer/opening_explorer_preferences.dart index a335ab0d90..c5c1ae7e6b 100644 --- a/lib/src/model/opening_explorer/opening_explorer_preferences.dart +++ b/lib/src/model/opening_explorer/opening_explorer_preferences.dart @@ -210,8 +210,8 @@ class LichessDbPrefState with _$LichessDbPrefState { 'All time': earliestDate, }; static final defaults = LichessDbPrefState( - speeds: kAvailableSpeeds, - ratings: kAvailableRatings, + speeds: kAvailableSpeeds.remove(Perf.ultraBullet), + ratings: kAvailableRatings.remove(400), since: earliestDate, ); diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index 3f7d7e46ee..79cc61e3af 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -66,7 +66,7 @@ class OpeningExplorerScreen extends StatelessWidget { return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( backgroundColor: Styles.cupertinoScaffoldColor.resolveFrom(context), - border: null, + padding: Styles.cupertinoAppBarTrailingWidgetPadding, middle: Text(context.l10n.openingExplorer), trailing: AppBarIconButton( onPressed: () => showAdaptiveBottomSheet( @@ -207,6 +207,8 @@ class _OpeningExplorer extends ConsumerStatefulWidget { class _OpeningExplorerState extends ConsumerState<_OpeningExplorer> { final Map cache = {}; + List? _cachedExplorerContent; + @override Widget build(BuildContext context) { final analysisState = @@ -266,6 +268,7 @@ class _OpeningExplorerState extends ConsumerState<_OpeningExplorer> { ); } if (openingExplorer.entry.moves.isEmpty) { + _cachedExplorerContent = null; return const Column( children: [ Expanded( @@ -277,65 +280,52 @@ class _OpeningExplorerState extends ConsumerState<_OpeningExplorer> { ], ); } - return Column( - children: [ - Expanded( - child: SingleChildScrollView( - scrollDirection: Axis.vertical, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - padding: const EdgeInsets.symmetric(horizontal: 6.0), - color: Theme.of(context).colorScheme.primaryContainer, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - if (opening != null) - Expanded( - flex: 75, - child: _Opening( - opening: opening, - wikiBooksUrl: analysisState.wikiBooksUrl, - ), - ), - if (openingExplorer.isIndexing) - Expanded( - flex: 25, - child: _IndexingIndicator(), - ), - ], - ), - ), - _MoveTable( - moves: openingExplorer.entry.moves, - whiteWins: openingExplorer.entry.white, - draws: openingExplorer.entry.draws, - blackWins: openingExplorer.entry.black, - pgn: widget.pgn, - options: widget.options, - ), - if (openingExplorer.entry.topGames != null && - openingExplorer.entry.topGames!.isNotEmpty) - _GameList( - title: context.l10n.topGames, - games: openingExplorer.entry.topGames!, - ), - if (openingExplorer.entry.recentGames != null && - openingExplorer.entry.recentGames!.isNotEmpty) - _GameList( - title: context.l10n.recentGames, - games: openingExplorer.entry.recentGames!, - ), - ], - ), - ), + + final explorerContent = [ + _MoveTable( + moves: openingExplorer.entry.moves, + whiteWins: openingExplorer.entry.white, + draws: openingExplorer.entry.draws, + blackWins: openingExplorer.entry.black, + pgn: widget.pgn, + options: widget.options, + ), + if (openingExplorer.entry.topGames != null && + openingExplorer.entry.topGames!.isNotEmpty) + _GameList( + title: context.l10n.topGames, + games: openingExplorer.entry.topGames!, ), - ], + if (openingExplorer.entry.recentGames != null && + openingExplorer.entry.recentGames!.isNotEmpty) + _GameList( + title: context.l10n.recentGames, + games: openingExplorer.entry.recentGames!, + ), + ]; + + _cachedExplorerContent = explorerContent; + + return _OpeningExplorerContent( + pgn: widget.pgn, + options: widget.options, + opening: opening, + openingExplorer: openingExplorer, + wikiBooksUrl: analysisState.wikiBooksUrl, + explorerContent: explorerContent, ); }, - loading: () => const Center( - child: CircularProgressIndicator(), + loading: () => _OpeningExplorerContent.loading( + pgn: widget.pgn, + options: widget.options, + opening: opening, + wikiBooksUrl: analysisState.wikiBooksUrl, + explorerContent: _cachedExplorerContent ?? + const [ + Center( + child: CircularProgressIndicator.adaptive(), + ), + ], ), error: (e, s) { debugPrint( @@ -349,6 +339,76 @@ class _OpeningExplorerState extends ConsumerState<_OpeningExplorer> { } } +class _OpeningExplorerContent extends StatelessWidget { + const _OpeningExplorerContent({ + required this.pgn, + required this.options, + required this.opening, + required this.openingExplorer, + required this.wikiBooksUrl, + required this.explorerContent, + }) : loading = false; + + const _OpeningExplorerContent.loading({ + required this.pgn, + required this.options, + required this.opening, + required this.wikiBooksUrl, + required this.explorerContent, + }) : loading = true, + openingExplorer = null; + + final String pgn; + final AnalysisOptions options; + final Opening? opening; + final ({OpeningExplorerEntry entry, bool isIndexing})? openingExplorer; + final String? wikiBooksUrl; + final List explorerContent; + final bool loading; + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + scrollDirection: Axis.vertical, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 6.0).add( + const EdgeInsets.only(top: 6.0), + ), + color: Theme.of(context).colorScheme.primaryContainer, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (opening != null) + Expanded( + flex: 75, + child: _Opening( + opening: opening!, + wikiBooksUrl: wikiBooksUrl, + ), + ), + if (openingExplorer?.isIndexing == true) + Expanded( + flex: 25, + child: _IndexingIndicator(), + ), + ], + ), + ), + Opacity( + opacity: loading ? 0.5 : 1.0, + child: Column( + children: explorerContent, + ), + ), + ], + ), + ); + } +} + class _Opening extends ConsumerWidget { const _Opening({ required this.opening, @@ -455,7 +515,6 @@ class _MoveTable extends ConsumerWidget { final ctrlProvider = analysisControllerProvider(pgn, options); return Table( - key: const Key('moves-table'), columnWidths: const { 0: FractionColumnWidth(0.15), 1: FractionColumnWidth(0.35), @@ -571,16 +630,12 @@ class _GameList extends StatelessWidget { @override Widget build(BuildContext context) { return Column( - key: const Key('game-list'), children: [ Container( - key: const Key('game-list-title'), padding: const EdgeInsets.all(6.0), color: Theme.of(context).colorScheme.primaryContainer, child: Row( - children: [ - Text(title), - ], + children: [Text(title)], ), ), ...List.generate(games.length, (int index) { @@ -611,7 +666,6 @@ class _GameTile extends ConsumerWidget { const paddingResultBox = EdgeInsets.all(5); return Container( - key: const Key('game-tile'), padding: const EdgeInsets.all(6.0), color: color, child: AdaptiveInkWell( @@ -631,83 +685,84 @@ class _GameTile extends ConsumerWidget { } }, child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: MainAxisAlignment.start, children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(game.white.rating.toString()), + Text(game.black.rating.toString()), + ], + ), + const SizedBox(width: 10), Expanded( - flex: 6, - child: Row( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(game.white.rating.toString()), - Text(game.black.rating.toString()), - ], - ), - const SizedBox(width: 10), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(game.white.name), - Text(game.black.name), - ], - ), + Text(game.white.name, overflow: TextOverflow.ellipsis), + Text(game.black.name, overflow: TextOverflow.ellipsis), ], ), ), - Expanded( - flex: 4, - child: Row( - children: [ - if (game.winner == 'white') - Container( - width: widthResultBox, - padding: paddingResultBox, + Row( + children: [ + if (game.winner == 'white') + Container( + width: widthResultBox, + padding: paddingResultBox, + decoration: BoxDecoration( color: Colors.white, - child: const Text( - '1-0', - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.black, - ), + borderRadius: BorderRadius.circular(5), + ), + child: const Text( + '1-0', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.black, ), - ) - else if (game.winner == 'black') - Container( - width: widthResultBox, - padding: paddingResultBox, + ), + ) + else if (game.winner == 'black') + Container( + width: widthResultBox, + padding: paddingResultBox, + decoration: BoxDecoration( color: Colors.black, - child: const Text( - '0-1', - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white, - ), + borderRadius: BorderRadius.circular(5), + ), + child: const Text( + '0-1', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, ), - ) - else - Container( - width: widthResultBox, - padding: paddingResultBox, + ), + ) + else + Container( + width: widthResultBox, + padding: paddingResultBox, + decoration: BoxDecoration( color: Colors.grey, - child: const Text( - '½-½', - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white, - ), + borderRadius: BorderRadius.circular(5), + ), + child: const Text( + '½-½', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, ), ), - if (game.month != null) ...[ - const SizedBox(width: 10.0), - Text(game.month!), - ], - if (game.speed != null) ...[ - const SizedBox(width: 10.0), - Icon(game.speed!.icon), - ], + ), + if (game.month != null) ...[ + const SizedBox(width: 10.0), + Text(game.month!), ], - ), + if (game.speed != null) ...[ + const SizedBox(width: 10.0), + Icon(game.speed!.icon, size: 20), + ], + ], ), ], ), diff --git a/lib/src/view/opening_explorer/opening_explorer_settings.dart b/lib/src/view/opening_explorer/opening_explorer_settings.dart index 3eea38f603..5138645986 100644 --- a/lib/src/view/opening_explorer/opening_explorer_settings.dart +++ b/lib/src/view/opening_explorer/opening_explorer_settings.dart @@ -226,12 +226,6 @@ class OpeningExplorerSettings extends ConsumerWidget { builder: (context, scrollController) => ListView( controller: scrollController, children: [ - PlatformListTile( - title: - Text(context.l10n.settingsSettings, style: Styles.sectionTitle), - subtitle: const SizedBox.shrink(), - ), - const SizedBox(height: 8.0), PlatformListTile( title: Text(context.l10n.database), subtitle: Wrap( diff --git a/lib/src/widgets/buttons.dart b/lib/src/widgets/buttons.dart index e52f71eee5..58c2fa1a40 100644 --- a/lib/src/widgets/buttons.dart +++ b/lib/src/widgets/buttons.dart @@ -179,14 +179,11 @@ class AppBarIconButton extends StatelessWidget { @override Widget build(BuildContext context) { return Theme.of(context).platform == TargetPlatform.iOS - ? IconTheme( - data: const IconThemeData(size: 26.0), - child: CupertinoIconButton( - padding: EdgeInsets.zero, - semanticsLabel: semanticsLabel, - onPressed: onPressed, - icon: icon, - ), + ? CupertinoIconButton( + padding: EdgeInsets.zero, + semanticsLabel: semanticsLabel, + onPressed: onPressed, + icon: icon, ) : IconButton( tooltip: semanticsLabel, From 3fba398ca1bf6f4926ae984dc72469828262408f Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 28 Aug 2024 08:39:20 +0200 Subject: [PATCH 159/979] Remove cupertino fidelity color scheme variant --- lib/src/app.dart | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/lib/src/app.dart b/lib/src/app.dart index 1b640f5065..c5be0bbcde 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -171,18 +171,12 @@ class _AppState extends ConsumerState { brightness: brightness, ); - final cupertinoColorScheme = ColorScheme.fromSeed( - seedColor: boardTheme.colors.darkSquare, - brightness: brightness, - dynamicSchemeVariant: DynamicSchemeVariant.fidelity, - ); - final cupertinoThemeData = CupertinoThemeData( - primaryColor: cupertinoColorScheme.primary, - primaryContrastingColor: cupertinoColorScheme.onPrimary, + primaryColor: colorScheme.primary, + primaryContrastingColor: colorScheme.onPrimary, brightness: brightness, textTheme: CupertinoTheme.of(context).textTheme.copyWith( - primaryColor: cupertinoColorScheme.primary, + primaryColor: colorScheme.primary, textStyle: CupertinoTheme.of(context) .textTheme .textStyle @@ -220,11 +214,7 @@ class _AppState extends ConsumerState { : null, ), extensions: [ - lichessCustomColors.harmonized( - Theme.of(context).platform == TargetPlatform.iOS - ? cupertinoColorScheme - : colorScheme, - ), + lichessCustomColors.harmonized(colorScheme), ], ), themeMode: generalPrefs.themeMode, From 267ee841d67501059c37d943d22c826350378be0 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 28 Aug 2024 09:30:52 +0200 Subject: [PATCH 160/979] Use a shimmer for first loading of the explorer --- .../opening_explorer_screen.dart | 135 ++++++++++++++---- 1 file changed, 108 insertions(+), 27 deletions(-) diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index 79cc61e3af..e604565cf1 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -22,6 +22,7 @@ import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/shimmer.dart'; import 'package:url_launcher/url_launcher.dart'; import 'opening_explorer_settings.dart'; @@ -207,7 +208,9 @@ class _OpeningExplorer extends ConsumerStatefulWidget { class _OpeningExplorerState extends ConsumerState<_OpeningExplorer> { final Map cache = {}; - List? _cachedExplorerContent; + /// Last explorer content that was successfully loaded. This is used to + /// display a loading indicator while the new content is being fetched. + List? lastExplorerWidgets; @override Widget build(BuildContext context) { @@ -263,12 +266,27 @@ class _OpeningExplorerState extends ConsumerState<_OpeningExplorer> { return openingExplorerAsync.when( data: (openingExplorer) { if (openingExplorer == null) { - return const Center( - child: CircularProgressIndicator(), + return _OpeningExplorerView.loading( + pgn: widget.pgn, + options: widget.options, + opening: opening, + wikiBooksUrl: analysisState.wikiBooksUrl, + explorerContent: lastExplorerWidgets ?? + [ + Shimmer( + child: ShimmerLoading( + isLoading: true, + child: _MoveTable.loading( + pgn: widget.pgn, + options: widget.options, + ), + ), + ), + ], ); } if (openingExplorer.entry.moves.isEmpty) { - _cachedExplorerContent = null; + lastExplorerWidgets = null; return const Column( children: [ Expanded( @@ -304,9 +322,9 @@ class _OpeningExplorerState extends ConsumerState<_OpeningExplorer> { ), ]; - _cachedExplorerContent = explorerContent; + lastExplorerWidgets = explorerContent; - return _OpeningExplorerContent( + return _OpeningExplorerView( pgn: widget.pgn, options: widget.options, opening: opening, @@ -315,15 +333,21 @@ class _OpeningExplorerState extends ConsumerState<_OpeningExplorer> { explorerContent: explorerContent, ); }, - loading: () => _OpeningExplorerContent.loading( + loading: () => _OpeningExplorerView.loading( pgn: widget.pgn, options: widget.options, opening: opening, wikiBooksUrl: analysisState.wikiBooksUrl, - explorerContent: _cachedExplorerContent ?? - const [ - Center( - child: CircularProgressIndicator.adaptive(), + explorerContent: lastExplorerWidgets ?? + [ + Shimmer( + child: ShimmerLoading( + isLoading: true, + child: _MoveTable.loading( + pgn: widget.pgn, + options: widget.options, + ), + ), ), ], ), @@ -339,8 +363,9 @@ class _OpeningExplorerState extends ConsumerState<_OpeningExplorer> { } } -class _OpeningExplorerContent extends StatelessWidget { - const _OpeningExplorerContent({ +/// The opening header and the opening explorer move table. +class _OpeningExplorerView extends StatelessWidget { + const _OpeningExplorerView({ required this.pgn, required this.options, required this.opening, @@ -349,7 +374,7 @@ class _OpeningExplorerContent extends StatelessWidget { required this.explorerContent, }) : loading = false; - const _OpeningExplorerContent.loading({ + const _OpeningExplorerView.loading({ required this.pgn, required this.options, required this.opening, @@ -374,9 +399,7 @@ class _OpeningExplorerContent extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( - padding: const EdgeInsets.symmetric(horizontal: 6.0).add( - const EdgeInsets.only(top: 6.0), - ), + padding: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 6.0), color: Theme.of(context).colorScheme.primaryContainer, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -496,7 +519,16 @@ class _MoveTable extends ConsumerWidget { required this.blackWins, required this.pgn, required this.options, - }); + }) : _isLoading = false; + + const _MoveTable.loading({ + required this.pgn, + required this.options, + }) : _isLoading = true, + moves = const IListConst([]), + whiteWins = 0, + draws = 0, + blackWins = 0; final IList moves; final int whiteWins; @@ -505,21 +537,70 @@ class _MoveTable extends ConsumerWidget { final String pgn; final AnalysisOptions options; + final bool _isLoading; + String formatNum(int num) => NumberFormat.decimalPatternDigits().format(num); @override Widget build(BuildContext context, WidgetRef ref) { const rowPadding = EdgeInsets.all(6.0); - final games = whiteWins + draws + blackWins; + const columnWidths = { + 0: FractionColumnWidth(0.15), + 1: FractionColumnWidth(0.35), + 2: FractionColumnWidth(0.50), + }; + + if (_isLoading) { + return Table( + columnWidths: columnWidths, + children: List.generate( + 10, + (int index) => TableRow( + children: [ + Padding( + padding: rowPadding, + child: Container( + height: 20, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(5), + ), + ), + ), + Padding( + padding: rowPadding, + child: Container( + height: 20, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(5), + ), + ), + ), + Padding( + padding: rowPadding, + child: Container( + height: 20, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(5), + ), + ), + ), + ], + ), + ), + ); + } + final games = whiteWins + draws + blackWins; final ctrlProvider = analysisControllerProvider(pgn, options); return Table( - columnWidths: const { - 0: FractionColumnWidth(0.15), - 1: FractionColumnWidth(0.35), - 2: FractionColumnWidth(0.50), - }, + columnWidths: columnWidths, children: [ TableRow( decoration: BoxDecoration( @@ -527,15 +608,15 @@ class _MoveTable extends ConsumerWidget { ), children: [ Padding( - padding: rowPadding, + padding: rowPadding.subtract(const EdgeInsets.only(top: 6.0)), child: Text(context.l10n.move), ), Padding( - padding: rowPadding, + padding: rowPadding.subtract(const EdgeInsets.only(top: 6.0)), child: Text(context.l10n.games), ), Padding( - padding: rowPadding, + padding: rowPadding.subtract(const EdgeInsets.only(top: 6.0)), child: Text(context.l10n.whiteDrawBlack), ), ], From 09237cdf9ad01c9cb89d3f4168df7acdeaaf19d3 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 28 Aug 2024 09:38:17 +0200 Subject: [PATCH 161/979] Animate opacity transition in explorer --- lib/src/view/opening_explorer/opening_explorer_screen.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index e604565cf1..39d7120bf2 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -420,8 +420,9 @@ class _OpeningExplorerView extends StatelessWidget { ], ), ), - Opacity( - opacity: loading ? 0.5 : 1.0, + AnimatedOpacity( + duration: const Duration(milliseconds: 500), + opacity: loading ? 0.4 : 1.0, child: Column( children: explorerContent, ), From b60d468146af0b338045a52df56460026d8dbaf8 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 28 Aug 2024 09:50:50 +0200 Subject: [PATCH 162/979] Use types instead of keys to test opening explorer --- .../opening_explorer_screen.dart | 29 ++++++++++--------- .../opening_explorer_screen_test.dart | 18 ++++++------ 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index 39d7120bf2..26f3cfdf5f 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -276,7 +276,7 @@ class _OpeningExplorerState extends ConsumerState<_OpeningExplorer> { Shimmer( child: ShimmerLoading( isLoading: true, - child: _MoveTable.loading( + child: OpeningExplorerMoveTable.loading( pgn: widget.pgn, options: widget.options, ), @@ -300,7 +300,7 @@ class _OpeningExplorerState extends ConsumerState<_OpeningExplorer> { } final explorerContent = [ - _MoveTable( + OpeningExplorerMoveTable( moves: openingExplorer.entry.moves, whiteWins: openingExplorer.entry.white, draws: openingExplorer.entry.draws, @@ -310,13 +310,13 @@ class _OpeningExplorerState extends ConsumerState<_OpeningExplorer> { ), if (openingExplorer.entry.topGames != null && openingExplorer.entry.topGames!.isNotEmpty) - _GameList( + OpeningExplorerGameList( title: context.l10n.topGames, games: openingExplorer.entry.topGames!, ), if (openingExplorer.entry.recentGames != null && openingExplorer.entry.recentGames!.isNotEmpty) - _GameList( + OpeningExplorerGameList( title: context.l10n.recentGames, games: openingExplorer.entry.recentGames!, ), @@ -343,7 +343,7 @@ class _OpeningExplorerState extends ConsumerState<_OpeningExplorer> { Shimmer( child: ShimmerLoading( isLoading: true, - child: _MoveTable.loading( + child: OpeningExplorerMoveTable.loading( pgn: widget.pgn, options: widget.options, ), @@ -512,8 +512,9 @@ class _IndexingIndicatorState extends State<_IndexingIndicator> } } -class _MoveTable extends ConsumerWidget { - const _MoveTable({ +/// Table of moves for the opening explorer. +class OpeningExplorerMoveTable extends ConsumerWidget { + const OpeningExplorerMoveTable({ required this.moves, required this.whiteWins, required this.draws, @@ -522,7 +523,7 @@ class _MoveTable extends ConsumerWidget { required this.options, }) : _isLoading = false; - const _MoveTable.loading({ + const OpeningExplorerMoveTable.loading({ required this.pgn, required this.options, }) : _isLoading = true, @@ -700,8 +701,9 @@ class _MoveTable extends ConsumerWidget { } } -class _GameList extends StatelessWidget { - const _GameList({ +/// List of games for the opening explorer. +class OpeningExplorerGameList extends StatelessWidget { + const OpeningExplorerGameList({ required this.title, required this.games, }); @@ -721,7 +723,7 @@ class _GameList extends StatelessWidget { ), ), ...List.generate(games.length, (int index) { - return _GameTile( + return OpeningExplorerGameTile( game: games.get(index), color: index.isEven ? Theme.of(context).colorScheme.surfaceContainerLow @@ -733,8 +735,9 @@ class _GameList extends StatelessWidget { } } -class _GameTile extends ConsumerWidget { - const _GameTile({ +/// A game tile for the opening explorer. +class OpeningExplorerGameTile extends ConsumerWidget { + const OpeningExplorerGameTile({ required this.game, required this.color, }); diff --git a/test/view/opening_explorer/opening_explorer_screen_test.dart b/test/view/opening_explorer/opening_explorer_screen_test.dart index bcd6a0d8a5..c8010157ba 100644 --- a/test/view/opening_explorer/opening_explorer_screen_test.dart +++ b/test/view/opening_explorer/opening_explorer_screen_test.dart @@ -107,7 +107,7 @@ void main() { 'e4', 'd4', ]; - expect(find.byKey(const Key('moves-table')), findsOneWidget); + expect(find.byType(OpeningExplorerMoveTable), findsOneWidget); for (final move in moves) { expect(find.widgetWithText(TableRowInkWell, move), findsOneWidget); } @@ -115,11 +115,11 @@ void main() { expect(find.widgetWithText(Container, 'Top games'), findsOneWidget); expect(find.widgetWithText(Container, 'Recent games'), findsNothing); expect( - find.byKey(const Key('game-list')), + find.byType(OpeningExplorerGameList), findsOneWidget, ); expect( - find.byKey(const Key('game-tile')), + find.byType(OpeningExplorerGameTile), findsNWidgets(2), ); @@ -156,7 +156,7 @@ void main() { final moves = [ 'd4', ]; - expect(find.byKey(const Key('moves-table')), findsOneWidget); + expect(find.byType(OpeningExplorerMoveTable), findsOneWidget); for (final move in moves) { expect(find.widgetWithText(TableRowInkWell, move), findsOneWidget); } @@ -164,11 +164,11 @@ void main() { expect(find.widgetWithText(Container, 'Top games'), findsNothing); expect(find.widgetWithText(Container, 'Recent games'), findsOneWidget); expect( - find.byKey(const Key('game-list')), + find.byType(OpeningExplorerGameList), findsOneWidget, ); expect( - find.byKey(const Key('game-tile')), + find.byType(OpeningExplorerGameTile), findsOneWidget, ); @@ -205,7 +205,7 @@ void main() { final moves = [ 'c4', ]; - expect(find.byKey(const Key('moves-table')), findsOneWidget); + expect(find.byType(OpeningExplorerMoveTable), findsOneWidget); for (final move in moves) { expect(find.widgetWithText(TableRowInkWell, move), findsOneWidget); } @@ -213,11 +213,11 @@ void main() { expect(find.widgetWithText(Container, 'Top games'), findsNothing); expect(find.widgetWithText(Container, 'Recent games'), findsOneWidget); expect( - find.byKey(const Key('game-list')), + find.byType(OpeningExplorerGameList), findsOneWidget, ); expect( - find.byKey(const Key('game-tile')), + find.byType(OpeningExplorerGameTile), findsOneWidget, ); From c9fa344da19257ee92d4784901aa1d081dc1d26b Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 28 Aug 2024 10:26:49 +0200 Subject: [PATCH 163/979] Improve explorer layout and loading animations --- .../opening_explorer_screen.dart | 87 +++++++++++-------- 1 file changed, 51 insertions(+), 36 deletions(-) diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index 26f3cfdf5f..64c9cc0f46 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -287,12 +287,17 @@ class _OpeningExplorerState extends ConsumerState<_OpeningExplorer> { } if (openingExplorer.entry.moves.isEmpty) { lastExplorerWidgets = null; - return const Column( - children: [ - Expanded( - child: Align( - alignment: Alignment.center, - child: Text('No game found'), + return _OpeningExplorerView( + pgn: widget.pgn, + options: widget.options, + opening: opening, + openingExplorer: openingExplorer, + wikiBooksUrl: analysisState.wikiBooksUrl, + explorerContent: [ + Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Text(context.l10n.noGameFound), ), ), ], @@ -393,42 +398,52 @@ class _OpeningExplorerView extends StatelessWidget { @override Widget build(BuildContext context) { - return SingleChildScrollView( - scrollDirection: Axis.vertical, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - padding: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 6.0), - color: Theme.of(context).colorScheme.primaryContainer, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + return Column( + mainAxisSize: MainAxisSize.max, + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 6.0), + color: Theme.of(context).colorScheme.primaryContainer, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (opening != null) + Expanded( + flex: 75, + child: _Opening( + opening: opening!, + wikiBooksUrl: wikiBooksUrl, + ), + ), + if (openingExplorer?.isIndexing == true) + Expanded( + flex: 25, + child: _IndexingIndicator(), + ), + ], + ), + ), + Expanded( + child: Center( + child: Stack( children: [ - if (opening != null) - Expanded( - flex: 75, - child: _Opening( - opening: opening!, - wikiBooksUrl: wikiBooksUrl, + ListView(children: explorerContent), + Positioned.fill( + child: IgnorePointer( + ignoring: !loading, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 300), + curve: Curves.fastOutSlowIn, + opacity: loading ? 0.5 : 0.0, + child: const ColoredBox(color: Colors.white), ), ), - if (openingExplorer?.isIndexing == true) - Expanded( - flex: 25, - child: _IndexingIndicator(), - ), + ), ], ), ), - AnimatedOpacity( - duration: const Duration(milliseconds: 500), - opacity: loading ? 0.4 : 1.0, - child: Column( - children: explorerContent, - ), - ), - ], - ), + ), + ], ); } } From 80b9842a6507cdbe75413e8b582fa1e26cf54574 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 28 Aug 2024 11:18:52 +0200 Subject: [PATCH 164/979] Put tuning button in bottom bar and show selected db --- .../opening_explorer_screen.dart | 57 +++++++++---------- .../opening_explorer_settings.dart | 5 +- 2 files changed, 30 insertions(+), 32 deletions(-) diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index 64c9cc0f46..c6837cfb4d 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -45,19 +45,6 @@ class OpeningExplorerScreen extends StatelessWidget { return Scaffold( appBar: AppBar( title: Text(context.l10n.openingExplorer), - actions: [ - AppBarIconButton( - onPressed: () => showAdaptiveBottomSheet( - context: context, - isScrollControlled: true, - showDragHandle: true, - isDismissible: true, - builder: (_) => OpeningExplorerSettings(pgn, options), - ), - semanticsLabel: context.l10n.settingsSettings, - icon: const Icon(Icons.settings), - ), - ], ), body: _Body(pgn: pgn, options: options), ); @@ -67,19 +54,7 @@ class OpeningExplorerScreen extends StatelessWidget { return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( backgroundColor: Styles.cupertinoScaffoldColor.resolveFrom(context), - padding: Styles.cupertinoAppBarTrailingWidgetPadding, middle: Text(context.l10n.openingExplorer), - trailing: AppBarIconButton( - onPressed: () => showAdaptiveBottomSheet( - context: context, - isScrollControlled: true, - showDragHandle: true, - isDismissible: true, - builder: (_) => OpeningExplorerSettings(pgn, options), - ), - semanticsLabel: context.l10n.settingsSettings, - icon: const Icon(Icons.settings), - ), ), child: _Body(pgn: pgn, options: options), ); @@ -946,12 +921,20 @@ class _BottomBar extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final db = ref + .watch(openingExplorerPreferencesProvider.select((value) => value.db)); final ctrlProvider = analysisControllerProvider(pgn, options); final canGoBack = ref.watch(ctrlProvider.select((value) => value.canGoBack)); final canGoNext = ref.watch(ctrlProvider.select((value) => value.canGoNext)); + final dbLabel = switch (db) { + OpeningDatabase.master => 'Masters', + OpeningDatabase.lichess => 'Lichess', + OpeningDatabase.player => context.l10n.player, + }; + return Container( color: Theme.of(context).platform == TargetPlatform.iOS ? CupertinoTheme.of(context).barBackgroundColor @@ -965,7 +948,23 @@ class _BottomBar extends ConsumerWidget { children: [ Expanded( child: BottomBarButton( - label: context.l10n.flipBoard, + label: dbLabel, + showLabel: true, + onTap: () => showAdaptiveBottomSheet( + context: context, + isScrollControlled: true, + showDragHandle: true, + isDismissible: true, + builder: (_) => OpeningExplorerSettings(pgn, options), + ), + icon: Icons.tune, + ), + ), + Expanded( + child: BottomBarButton( + label: 'Flip', + tooltip: context.l10n.flipBoard, + showLabel: true, onTap: () => ref.read(ctrlProvider.notifier).toggleBoard(), icon: CupertinoIcons.arrow_2_squarepath, ), @@ -974,9 +973,9 @@ class _BottomBar extends ConsumerWidget { child: RepeatButton( onLongPress: canGoBack ? () => _moveBackward(ref) : null, child: BottomBarButton( - key: const ValueKey('goto-previous'), onTap: canGoBack ? () => _moveBackward(ref) : null, label: 'Previous', + showLabel: true, icon: CupertinoIcons.chevron_back, showTooltip: false, ), @@ -986,9 +985,9 @@ class _BottomBar extends ConsumerWidget { child: RepeatButton( onLongPress: canGoNext ? () => _moveForward(ref) : null, child: BottomBarButton( - key: const ValueKey('goto-next'), icon: CupertinoIcons.chevron_forward, - label: context.l10n.next, + label: 'Next', + showLabel: true, onTap: canGoNext ? () => _moveForward(ref) : null, showTooltip: false, ), diff --git a/lib/src/view/opening_explorer/opening_explorer_settings.dart b/lib/src/view/opening_explorer/opening_explorer_settings.dart index 5138645986..6b01a12de9 100644 --- a/lib/src/view/opening_explorer/opening_explorer_settings.dart +++ b/lib/src/view/opening_explorer/opening_explorer_settings.dart @@ -5,7 +5,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_preferences.dart'; -import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/user/search_screen.dart'; @@ -219,10 +218,10 @@ class OpeningExplorerSettings extends ConsumerWidget { ]; return DraggableScrollableSheet( - initialChildSize: .85, + initialChildSize: .5, expand: false, snap: true, - snapSizes: const [.85], + snapSizes: const [.5, .75], builder: (context, scrollController) => ListView( controller: scrollController, children: [ From 7d7150de1752ea4e4b1c495fb7132483b8dab7a5 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 28 Aug 2024 11:42:02 +0200 Subject: [PATCH 165/979] Increase opening explorer padding --- .../opening_explorer_screen.dart | 40 +++++++++++-------- .../opening_explorer_screen_test.dart | 30 -------------- 2 files changed, 24 insertions(+), 46 deletions(-) diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index c6837cfb4d..6de6adffd6 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -27,6 +27,13 @@ import 'package:url_launcher/url_launcher.dart'; import 'opening_explorer_settings.dart'; +const _kTableRowVerticalPadding = 10.0; +const _kTableRowHorizontalPadding = 8.0; +const _kTableRowPadding = EdgeInsets.symmetric( + horizontal: _kTableRowHorizontalPadding, + vertical: _kTableRowVerticalPadding, +); + class OpeningExplorerScreen extends StatelessWidget { const OpeningExplorerScreen({required this.pgn, required this.options}); @@ -377,7 +384,7 @@ class _OpeningExplorerView extends StatelessWidget { mainAxisSize: MainAxisSize.max, children: [ Container( - padding: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 6.0), + padding: _kTableRowPadding, color: Theme.of(context).colorScheme.primaryContainer, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -535,7 +542,6 @@ class OpeningExplorerMoveTable extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - const rowPadding = EdgeInsets.all(6.0); const columnWidths = { 0: FractionColumnWidth(0.15), 1: FractionColumnWidth(0.35), @@ -550,7 +556,7 @@ class OpeningExplorerMoveTable extends ConsumerWidget { (int index) => TableRow( children: [ Padding( - padding: rowPadding, + padding: _kTableRowPadding, child: Container( height: 20, width: double.infinity, @@ -561,7 +567,7 @@ class OpeningExplorerMoveTable extends ConsumerWidget { ), ), Padding( - padding: rowPadding, + padding: _kTableRowPadding, child: Container( height: 20, width: double.infinity, @@ -572,7 +578,7 @@ class OpeningExplorerMoveTable extends ConsumerWidget { ), ), Padding( - padding: rowPadding, + padding: _kTableRowPadding, child: Container( height: 20, width: double.infinity, @@ -591,6 +597,8 @@ class OpeningExplorerMoveTable extends ConsumerWidget { final games = whiteWins + draws + blackWins; final ctrlProvider = analysisControllerProvider(pgn, options); + const topPadding = EdgeInsets.only(top: _kTableRowVerticalPadding); + return Table( columnWidths: columnWidths, children: [ @@ -600,15 +608,15 @@ class OpeningExplorerMoveTable extends ConsumerWidget { ), children: [ Padding( - padding: rowPadding.subtract(const EdgeInsets.only(top: 6.0)), + padding: _kTableRowPadding.subtract(topPadding), child: Text(context.l10n.move), ), Padding( - padding: rowPadding.subtract(const EdgeInsets.only(top: 6.0)), + padding: _kTableRowPadding.subtract(topPadding), child: Text(context.l10n.games), ), Padding( - padding: rowPadding.subtract(const EdgeInsets.only(top: 6.0)), + padding: _kTableRowPadding.subtract(topPadding), child: Text(context.l10n.whiteDrawBlack), ), ], @@ -630,7 +638,7 @@ class OpeningExplorerMoveTable extends ConsumerWidget { .read(ctrlProvider.notifier) .onUserMove(Move.parse(move.uci)!), child: Padding( - padding: rowPadding, + padding: _kTableRowPadding, child: Text(move.san), ), ), @@ -639,7 +647,7 @@ class OpeningExplorerMoveTable extends ConsumerWidget { .read(ctrlProvider.notifier) .onUserMove(Move.parse(move.uci)!), child: Padding( - padding: rowPadding, + padding: _kTableRowPadding, child: Text('${formatNum(move.games)} ($percentGames%)'), ), ), @@ -648,7 +656,7 @@ class OpeningExplorerMoveTable extends ConsumerWidget { .read(ctrlProvider.notifier) .onUserMove(Move.parse(move.uci)!), child: Padding( - padding: rowPadding, + padding: _kTableRowPadding, child: _WinPercentageChart( whiteWins: move.white, draws: move.draws, @@ -668,16 +676,16 @@ class OpeningExplorerMoveTable extends ConsumerWidget { ), children: [ Container( - padding: rowPadding, + padding: _kTableRowPadding, alignment: Alignment.centerLeft, child: const Icon(Icons.functions), ), Padding( - padding: rowPadding, + padding: _kTableRowPadding, child: Text('${formatNum(games)} (100%)'), ), Padding( - padding: rowPadding, + padding: _kTableRowPadding, child: _WinPercentageChart( whiteWins: whiteWins, draws: draws, @@ -706,7 +714,7 @@ class OpeningExplorerGameList extends StatelessWidget { return Column( children: [ Container( - padding: const EdgeInsets.all(6.0), + padding: _kTableRowPadding, color: Theme.of(context).colorScheme.primaryContainer, child: Row( children: [Text(title)], @@ -741,7 +749,7 @@ class OpeningExplorerGameTile extends ConsumerWidget { const paddingResultBox = EdgeInsets.all(5); return Container( - padding: const EdgeInsets.all(6.0), + padding: _kTableRowPadding, color: color, child: AdaptiveInkWell( onTap: () async { diff --git a/test/view/opening_explorer/opening_explorer_screen_test.dart b/test/view/opening_explorer/opening_explorer_screen_test.dart index c8010157ba..562a85e3e0 100644 --- a/test/view/opening_explorer/opening_explorer_screen_test.dart +++ b/test/view/opening_explorer/opening_explorer_screen_test.dart @@ -48,36 +48,6 @@ void main() { ); group('OpeningExplorerScreen', () { - testWidgets( - 'meets accessibility guidelines', - (WidgetTester tester) async { - final SemanticsHandle handle = tester.ensureSemantics(); - - final app = await buildTestApp( - tester, - home: const OpeningExplorerScreen( - pgn: '', - options: options, - ), - overrides: [ - defaultClientProvider.overrideWithValue(mockClient), - ], - ); - - await tester.pumpWidget(app); - - // wait for opening explorer data to load - await tester.pump(const Duration(milliseconds: 50)); - - await meetsTapTargetGuideline(tester); - - await tester.pump(const Duration(milliseconds: 50)); - - handle.dispose(); - }, - variant: kPlatformVariant, - ); - testWidgets( 'master opening explorer loads', (WidgetTester tester) async { From 25a03794378be285c9c1986451cf226c20c9187a Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 28 Aug 2024 12:13:54 +0200 Subject: [PATCH 166/979] Tweak explorer border radius on tablets --- .../opening_explorer/opening_explorer_screen.dart | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index 6de6adffd6..814e160fca 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -122,6 +122,9 @@ class _Body extends ConsumerWidget { children: [ Expanded( child: PlatformCard( + borderRadius: const BorderRadius.all( + Radius.circular(4.0), + ), margin: const EdgeInsets.all( kTabletBoardTableSidePadding, ), @@ -380,12 +383,21 @@ class _OpeningExplorerView extends StatelessWidget { @override Widget build(BuildContext context) { + final isLandscape = + MediaQuery.orientationOf(context) == Orientation.landscape; + return Column( mainAxisSize: MainAxisSize.max, children: [ Container( padding: _kTableRowPadding, - color: Theme.of(context).colorScheme.primaryContainer, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primaryContainer, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(isLandscape ? 4.0 : 0), + topRight: Radius.circular(isLandscape ? 4.0 : 0), + ), + ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ From f055097cc7ce79f1e36875ba920efeb0e76ea644 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Thu, 22 Aug 2024 20:08:37 +0200 Subject: [PATCH 167/979] fix: copy paste error in title of PieceNotation settings screen --- lib/src/view/settings/account_preferences_screen.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/src/view/settings/account_preferences_screen.dart b/lib/src/view/settings/account_preferences_screen.dart index 154b118107..54d4af5d15 100644 --- a/lib/src/view/settings/account_preferences_screen.dart +++ b/lib/src/view/settings/account_preferences_screen.dart @@ -119,8 +119,7 @@ class _AccountPreferencesScreenState } else { pushPlatformRoute( context, - title: context - .l10n.preferencesPromoteToQueenAutomatically, + title: context.l10n.preferencesPgnPieceNotation, builder: (context) => const PieceNotationSettingsScreen(), ); From 7dcd9d74aa43ed2b1262665c8ea3d06b120ffbac Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 29 Aug 2024 14:26:28 +0200 Subject: [PATCH 168/979] Tweak editor screen --- lib/src/view/board_editor/board_editor_screen.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/view/board_editor/board_editor_screen.dart b/lib/src/view/board_editor/board_editor_screen.dart index cded0f7f0b..7d62fada35 100644 --- a/lib/src/view/board_editor/board_editor_screen.dart +++ b/lib/src/view/board_editor/board_editor_screen.dart @@ -103,7 +103,8 @@ class _Body extends ConsumerWidget { initialFen: initialFen, orientation: boardEditorState.orientation, isTablet: isTablet, - pieces: boardEditorState.pieces.unlock, + // unlockView is safe because chessground will never modify the pieces + pieces: boardEditorState.pieces.unlockView, ), _PieceMenu( boardSize, From 29eca306f16d2b3c0e325ef29fd3ec96f1d47467 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Fri, 16 Aug 2024 23:53:59 +0200 Subject: [PATCH 169/979] add a new screen with tabs for broadcast screen, move the broadcast round screen to a tab and create overview tab --- lib/src/model/broadcast/broadcast.dart | 14 ++ .../model/broadcast/broadcast_repository.dart | 12 ++ .../broadcast/broadcast_round_controller.dart | 7 +- ..._screen.dart => broadcast_boards_tab.dart} | 138 ++++++----------- .../broadcast/broadcast_overview_tab.dart | 85 +++++++++++ lib/src/view/broadcast/broadcast_screen.dart | 143 ++++++++++++++++++ lib/src/view/broadcast/broadcast_tile.dart | 10 +- .../broadcast/broadcasts_list_screen.dart | 24 ++- lib/src/view/watch/watch_tab_screen.dart | 4 +- pubspec.lock | 16 ++ pubspec.yaml | 1 + 11 files changed, 345 insertions(+), 109 deletions(-) rename lib/src/view/broadcast/{broadcast_round_screen.dart => broadcast_boards_tab.dart} (73%) create mode 100644 lib/src/view/broadcast/broadcast_overview_tab.dart create mode 100644 lib/src/view/broadcast/broadcast_screen.dart diff --git a/lib/src/model/broadcast/broadcast.dart b/lib/src/model/broadcast/broadcast.dart index ac8ccdb1e8..9ab1bbaefb 100644 --- a/lib/src/model/broadcast/broadcast.dart +++ b/lib/src/model/broadcast/broadcast.dart @@ -35,6 +35,20 @@ class Broadcast with _$Broadcast { typedef BroadcastTournament = ({ String name, String? imageUrl, + String description, + BroadcastTournamentInformation information, +}); + +typedef BroadcastTournamentInformation = ({ + String? format, + String? timeControl, + String? players, + BroadcastTournamentDates? dates, +}); + +typedef BroadcastTournamentDates = ({ + DateTime startsAt, + DateTime? endsAt, }); @freezed diff --git a/lib/src/model/broadcast/broadcast_repository.dart b/lib/src/model/broadcast/broadcast_repository.dart index 05c6b6e7fb..55e02f3d24 100644 --- a/lib/src/model/broadcast/broadcast_repository.dart +++ b/lib/src/model/broadcast/broadcast_repository.dart @@ -64,6 +64,18 @@ Broadcast _broadcastFromPick(RequiredPick pick) { tour: ( name: pick('tour', 'name').asStringOrThrow(), imageUrl: pick('tour', 'image').asStringOrNull(), + description: pick('tour', 'description').asStringOrThrow(), + information: ( + format: pick('tour', 'info', 'format').asStringOrNull(), + timeControl: pick('tour', 'info', 'tc').asStringOrNull(), + players: pick('tour', 'info', 'players').asStringOrNull(), + dates: pick('tour', 'dates').letOrNull( + (pick) => ( + startsAt: pick(0).asDateTimeFromMillisecondsOrThrow().toLocal(), + endsAt: pick(1).asDateTimeFromMillisecondsOrNull()?.toLocal(), + ), + ), + ), ), round: BroadcastRound( id: roundId, diff --git a/lib/src/model/broadcast/broadcast_round_controller.dart b/lib/src/model/broadcast/broadcast_round_controller.dart index d14118d8e2..4244255415 100644 --- a/lib/src/model/broadcast/broadcast_round_controller.dart +++ b/lib/src/model/broadcast/broadcast_round_controller.dart @@ -58,13 +58,14 @@ class BroadcastRoundController extends _$BroadcastRoundController { void _handleAddNodeEvent(SocketEvent event) { // The path of the last and current move of the broadcasted game + // Its value is "!" if the path is identical to one of the node that was received final currentPath = pick(event.data, 'relayPath').asUciPathOrThrow(); // The path for the node that was received - final path = pick(event.data, 'p', 'path').asUciPathOrThrow(); - final nodeId = pick(event.data, 'n', 'id').asUciCharPairOrThrow(); + // final path = pick(event.data, 'p', 'path').asUciPathOrThrow(); + // final nodeId = pick(event.data, 'n', 'id').asUciCharPairOrThrow(); // We check that the event we received is for the last move of the game - if (currentPath != path + nodeId) return; + if (currentPath.value != '!') return; final broadcastGameId = pick(event.data, 'p', 'chapterId').asBroadcastGameIdOrThrow(); diff --git a/lib/src/view/broadcast/broadcast_round_screen.dart b/lib/src/view/broadcast/broadcast_boards_tab.dart similarity index 73% rename from lib/src/view/broadcast/broadcast_round_screen.dart rename to lib/src/view/broadcast/broadcast_boards_tab.dart index 43e9357e2e..30dcc0bbe3 100644 --- a/lib/src/view/broadcast/broadcast_round_screen.dart +++ b/lib/src/view/broadcast/broadcast_boards_tab.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -15,7 +14,6 @@ import 'package:lichess_mobile/src/utils/duration.dart'; import 'package:lichess_mobile/src/utils/lichess_assets.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/widgets/board_thumbnail.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; // height of 1.0 is important because we need to determine the height of the text @@ -24,52 +22,11 @@ const _kPlayerWidgetTextStyle = TextStyle(fontSize: 13, height: 1.0); const _kPlayerWidgetPadding = EdgeInsets.symmetric(vertical: 5.0); -/// A screen that displays the live games of a broadcast round. -class BroadcastRoundScreen extends StatelessWidget { - final String broadCastTitle; +/// A tab that displays the live games of a broadcast round. +class BroadcastBoardsTab extends ConsumerWidget { final BroadcastRoundId roundId; - const BroadcastRoundScreen({ - super.key, - required this.broadCastTitle, - required this.roundId, - }); - - @override - Widget build(BuildContext context) { - return PlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, - ); - } - - Widget _androidBuilder( - BuildContext context, - ) { - return Scaffold( - appBar: AppBar( - title: Text(broadCastTitle), - ), - body: _Body(roundId), - ); - } - - Widget _iosBuilder( - BuildContext context, - ) { - return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - middle: Text(broadCastTitle), - ), - child: _Body(roundId), - ); - } -} - -class _Body extends ConsumerWidget { - final BroadcastRoundId roundId; - - const _Body(this.roundId); + const BroadcastBoardsTab(this.roundId); @override Widget build(BuildContext context, WidgetRef ref) { @@ -77,7 +34,7 @@ class _Body extends ConsumerWidget { return games.when( data: (games) => (games.isEmpty) - ? const Text('No games to show for now') + ? const Text('No boards to show for now') : BroadcastPreview(games: games.values.toIList()), loading: () => const Shimmer( child: ShimmerLoading( @@ -113,54 +70,49 @@ class BroadcastPreview extends StatelessWidget { (numberOfBoardsByRow - 1) * boardSpacing) / numberOfBoardsByRow; - return SafeArea( - child: Padding( - padding: Styles.horizontalBodyPadding, - child: GridView.builder( - itemCount: games == null ? numberLoadingBoards : games!.length, - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: numberOfBoardsByRow, - crossAxisSpacing: boardSpacing, - mainAxisSpacing: boardSpacing, - mainAxisExtent: boardWidth + 2 * headerAndFooterHeight, - ), - itemBuilder: (context, index) { - if (games == null) { - return BoardThumbnail.loading( - size: boardWidth, - header: _PlayerWidget.loading(width: boardWidth), - footer: _PlayerWidget.loading(width: boardWidth), - ); - } + return GridView.builder( + itemCount: games == null ? numberLoadingBoards : games!.length, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: numberOfBoardsByRow, + crossAxisSpacing: boardSpacing, + mainAxisSpacing: boardSpacing, + mainAxisExtent: boardWidth + 2 * headerAndFooterHeight, + ), + itemBuilder: (context, index) { + if (games == null) { + return BoardThumbnail.loading( + size: boardWidth, + header: _PlayerWidget.loading(width: boardWidth), + footer: _PlayerWidget.loading(width: boardWidth), + ); + } - final game = games![index]; - final playingSide = Setup.parseFen(game.fen).turn; + final game = games![index]; + final playingSide = Setup.parseFen(game.fen).turn; - return BoardThumbnail( - orientation: Side.white, - fen: game.fen, - lastMove: game.lastMove, - size: boardWidth, - header: _PlayerWidget( - width: boardWidth, - player: game.players[Side.black]!, - gameStatus: game.status, - thinkTime: game.thinkTime, - side: Side.black, - playingSide: playingSide, - ), - footer: _PlayerWidget( - width: boardWidth, - player: game.players[Side.white]!, - gameStatus: game.status, - thinkTime: game.thinkTime, - side: Side.white, - playingSide: playingSide, - ), - ); - }, - ), - ), + return BoardThumbnail( + orientation: Side.white, + fen: game.fen, + lastMove: game.lastMove, + size: boardWidth, + header: _PlayerWidget( + width: boardWidth, + player: game.players[Side.black]!, + gameStatus: game.status, + thinkTime: game.thinkTime, + side: Side.black, + playingSide: playingSide, + ), + footer: _PlayerWidget( + width: boardWidth, + player: game.players[Side.white]!, + gameStatus: game.status, + thinkTime: game.thinkTime, + side: Side.white, + playingSide: playingSide, + ), + ); + }, ); } } diff --git a/lib/src/view/broadcast/broadcast_overview_tab.dart b/lib/src/view/broadcast/broadcast_overview_tab.dart new file mode 100644 index 0000000000..d4c2dafaa0 --- /dev/null +++ b/lib/src/view/broadcast/broadcast_overview_tab.dart @@ -0,0 +1,85 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:intl/intl.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; +import 'package:url_launcher/url_launcher.dart'; + +final _dateFormatter = DateFormat.MMMd(Intl.getCurrentLocale()); + +/// A tab that displays the overview of a broadcast. +class BroadcastOverviewTab extends StatelessWidget { + final Broadcast broadcast; + + const BroadcastOverviewTab(this.broadcast); + + @override + Widget build(BuildContext context) { + final tourInformation = broadcast.tour.information; + + return Column( + children: [ + Wrap( + alignment: WrapAlignment.center, + children: [ + if (tourInformation.dates != null) + BroadcastOverviewCard( + CupertinoIcons.calendar, + tourInformation.dates!.endsAt == null + ? _dateFormatter.format(tourInformation.dates!.startsAt) + : '${_dateFormatter.format(tourInformation.dates!.startsAt)} - ${_dateFormatter.format(tourInformation.dates!.endsAt!)}', + ), + if (tourInformation.format != null) + BroadcastOverviewCard( + Icons.emoji_events, + '${tourInformation.format}', + ), + if (tourInformation.timeControl != null) + BroadcastOverviewCard( + CupertinoIcons.stopwatch_fill, + '${tourInformation.timeControl}', + ), + if (tourInformation.players != null) + BroadcastOverviewCard( + Icons.person, + '${tourInformation.players}', + ), + ], + ), + Expanded( + child: Markdown( + data: broadcast.tour.description, + onTapLink: (text, url, title) { + if (url == null) return; + launchUrl(Uri.parse(url)); + }, + ), + ), + ], + ); + } +} + +class BroadcastOverviewCard extends StatelessWidget { + final IconData iconData; + final String text; + + const BroadcastOverviewCard(this.iconData, this.text); + + @override + Widget build(BuildContext context) { + return Card( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(iconData), + const SizedBox(width: 10), + Flexible(child: Text(text)), + ], + ), + ), + ); + } +} diff --git a/lib/src/view/broadcast/broadcast_screen.dart b/lib/src/view/broadcast/broadcast_screen.dart new file mode 100644 index 0000000000..1ac9f3bd42 --- /dev/null +++ b/lib/src/view/broadcast/broadcast_screen.dart @@ -0,0 +1,143 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/view/broadcast/broadcast_boards_tab.dart'; +import 'package:lichess_mobile/src/view/broadcast/broadcast_overview_tab.dart'; +import 'package:lichess_mobile/src/widgets/platform.dart'; + +class BroadcastScreen extends StatelessWidget { + final Broadcast broadcast; + + const BroadcastScreen({required this.broadcast}); + + @override + Widget build(BuildContext context) { + return PlatformWidget(androidBuilder: _buildAndroid, iosBuilder: _buildIos); + } + + Widget _buildIos(BuildContext context) { + return CupertinoPageScaffold( + navigationBar: const CupertinoNavigationBar(), + child: _CupertinoBody(broadcast: broadcast), + ); + } + + Widget _buildAndroid(BuildContext context) { + return _AndroidBody(broadcast: broadcast); + } +} + +class _AndroidBody extends StatefulWidget { + final Broadcast broadcast; + + const _AndroidBody({required this.broadcast}); + + @override + State<_AndroidBody> createState() => _AndroidBodyState(); +} + +class _AndroidBodyState extends State<_AndroidBody> + with SingleTickerProviderStateMixin { + late final TabController _tabController; + + @override + void initState() { + super.initState(); + _tabController = TabController(initialIndex: 1, length: 2, vsync: this); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.broadcast.title), + bottom: TabBar( + controller: _tabController, + tabs: const [ + Tab(text: 'Overview'), + Tab(text: 'Boards'), + ], + ), + ), + body: TabBarView( + controller: _tabController, + children: [ + SafeArea( + child: Padding( + padding: Styles.bodyPadding, + child: BroadcastOverviewTab(widget.broadcast), + ), + ), + SafeArea( + child: Padding( + padding: Styles.bodyPadding, + child: BroadcastBoardsTab(widget.broadcast.roundToLinkId), + ), + ), + ], + ), + ); + } +} + +class _CupertinoBody extends StatefulWidget { + final Broadcast broadcast; + + const _CupertinoBody({required this.broadcast}); + + @override + _CupertinoBodyState createState() => _CupertinoBodyState(); +} + +enum _ViewMode { overview, boards } + +class _CupertinoBodyState extends State<_CupertinoBody> { + _ViewMode _selectedSegment = _ViewMode.boards; + + void setViewMode(_ViewMode mode) { + setState(() { + _selectedSegment = mode; + }); + } + + @override + Widget build(BuildContext context) { + return SafeArea( + child: Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: Styles.bodyPadding, + child: CupertinoSlidingSegmentedControl<_ViewMode>( + groupValue: _selectedSegment, + children: const { + _ViewMode.overview: Text('Overview'), + _ViewMode.boards: Text('Boards'), + }, + onValueChanged: (_ViewMode? view) { + if (view != null) { + setState(() { + _selectedSegment = view; + }); + } + }, + ), + ), + Expanded( + child: _selectedSegment == _ViewMode.overview + ? BroadcastOverviewTab(widget.broadcast) + : BroadcastBoardsTab(widget.broadcast.roundToLinkId), + ), + ], + ), + ); + } +} diff --git a/lib/src/view/broadcast/broadcast_tile.dart b/lib/src/view/broadcast/broadcast_tile.dart index 2097846ca9..7a9d7054c3 100644 --- a/lib/src/view/broadcast/broadcast_tile.dart +++ b/lib/src/view/broadcast/broadcast_tile.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; import 'package:lichess_mobile/src/styles/transparent_image.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; -import 'package:lichess_mobile/src/view/broadcast/broadcast_round_screen.dart'; +import 'package:lichess_mobile/src/view/broadcast/broadcast_screen.dart'; import 'package:lichess_mobile/src/view/broadcast/default_broadcast_image.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; @@ -27,10 +28,9 @@ class BroadcastTile extends StatelessWidget { onTap: () { pushPlatformRoute( context, - builder: (context) => BroadcastRoundScreen( - broadCastTitle: broadcast.tour.name, - roundId: broadcast.roundToLinkId, - ), + title: context.l10n.broadcastBroadcasts, + rootNavigator: true, + builder: (context) => BroadcastScreen(broadcast: broadcast), ); }, title: Padding( diff --git a/lib/src/view/broadcast/broadcasts_list_screen.dart b/lib/src/view/broadcast/broadcasts_list_screen.dart index ccdf262370..ab77a4e148 100644 --- a/lib/src/view/broadcast/broadcasts_list_screen.dart +++ b/lib/src/view/broadcast/broadcasts_list_screen.dart @@ -10,7 +10,7 @@ import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/styles/transparent_image.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; -import 'package:lichess_mobile/src/view/broadcast/broadcast_round_screen.dart'; +import 'package:lichess_mobile/src/view/broadcast/broadcast_screen.dart'; import 'package:lichess_mobile/src/view/broadcast/default_broadcast_image.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; @@ -194,7 +194,20 @@ class BroadcastGridItem extends StatelessWidget { BroadcastGridItem.loading() : broadcast = Broadcast( - tour: const (name: '', imageUrl: null), + tour: ( + name: '', + imageUrl: null, + description: '', + information: ( + format: '', + timeControl: '', + players: '', + dates: ( + startsAt: DateTime.now(), + endsAt: DateTime.now(), + ), + ), + ), round: BroadcastRound( id: const BroadcastRoundId(''), name: '', @@ -212,10 +225,9 @@ class BroadcastGridItem extends StatelessWidget { onTap: () { pushPlatformRoute( context, - builder: (context) => BroadcastRoundScreen( - broadCastTitle: broadcast.tour.name, - roundId: broadcast.roundToLinkId, - ), + title: context.l10n.broadcastBroadcasts, + rootNavigator: true, + builder: (context) => BroadcastScreen(broadcast: broadcast), ); }, child: Container( diff --git a/lib/src/view/watch/watch_tab_screen.dart b/lib/src/view/watch/watch_tab_screen.dart index 92c839b361..fbdeca25d3 100644 --- a/lib/src/view/watch/watch_tab_screen.dart +++ b/lib/src/view/watch/watch_tab_screen.dart @@ -89,7 +89,7 @@ class _WatchScreenState extends ConsumerState { List get watchTabWidgets => const [ // TODO: show widget when broadcasts feature is ready - //_BroadcastWidget(), + _BroadcastWidget(), _WatchTvWidget(), _StreamerWidget(), ]; @@ -180,7 +180,7 @@ class _WatchScreenState extends ConsumerState { Future _refreshData(WidgetRef ref) { return Future.wait([ // TODO uncomment when broadcasts feature is ready - // ref.refresh(broadcastsPaginatorProvider.future), + ref.refresh(broadcastsPaginatorProvider.future), ref.refresh(featuredChannelsProvider.future), ref.refresh(liveStreamersProvider.future), ]); diff --git a/pubspec.lock b/pubspec.lock index 0084f24bee..0313956935 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -568,6 +568,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_markdown: + dependency: "direct main" + description: + name: flutter_markdown + sha256: a23c41ee57573e62fc2190a1f36a0480c4d90bde3a8a8d7126e5d5992fb53fb7 + url: "https://pub.dev" + source: hosted + version: "0.7.3+1" flutter_native_splash: dependency: "direct main" description: @@ -867,6 +875,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.2-main.4" + markdown: + dependency: transitive + description: + name: markdown + sha256: ef2a1298144e3f985cc736b22e0ccdaf188b5b3970648f2d9dc13efd1d9df051 + url: "https://pub.dev" + source: hosted + version: "7.2.2" matcher: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c93316d418..69187d2323 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -35,6 +35,7 @@ dependencies: flutter_linkify: ^6.0.0 flutter_localizations: sdk: flutter + flutter_markdown: ^0.7.3+1 flutter_native_splash: ^2.3.5 flutter_riverpod: ^2.3.4 flutter_secure_storage: ^9.2.0 From c1ddc119c22c03e3e016cb972cdaf11ad08c8b28 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Thu, 22 Aug 2024 02:23:22 +0200 Subject: [PATCH 170/979] add a dropdown menu to choose tournament in a group and round --- lib/src/model/broadcast/broadcast.dart | 35 +++- .../model/broadcast/broadcast_providers.dart | 22 +++ .../model/broadcast/broadcast_repository.dart | 93 +++++++--- .../broadcast/broadcast_round_controller.dart | 11 +- lib/src/model/common/id.dart | 21 +++ .../view/broadcast/broadcast_boards_tab.dart | 30 ++-- .../broadcast/broadcast_overview_tab.dart | 110 +++++++----- lib/src/view/broadcast/broadcast_screen.dart | 166 +++++++++++++++--- lib/src/view/broadcast/broadcast_tile.dart | 5 +- .../broadcast/broadcasts_list_screen.dart | 11 +- 10 files changed, 375 insertions(+), 129 deletions(-) diff --git a/lib/src/model/broadcast/broadcast.dart b/lib/src/model/broadcast/broadcast.dart index 9ab1bbaefb..c152cfad34 100644 --- a/lib/src/model/broadcast/broadcast.dart +++ b/lib/src/model/broadcast/broadcast.dart @@ -17,7 +17,7 @@ class Broadcast with _$Broadcast { const Broadcast._(); const factory Broadcast({ - required BroadcastTournament tour, + required BroadcastTournamentData tour, required BroadcastRound round, required String? group, @@ -32,12 +32,26 @@ class Broadcast with _$Broadcast { String get title => group ?? tour.name; } -typedef BroadcastTournament = ({ - String name, - String? imageUrl, - String description, - BroadcastTournamentInformation information, -}); +@freezed +class BroadcastTournament with _$BroadcastTournament { + const factory BroadcastTournament({ + required BroadcastTournamentData data, + required IList rounds, + required BroadcastRoundId defaultRoundId, + required IList? group, + }) = _BroadcastTournament; +} + +@freezed +class BroadcastTournamentData with _$BroadcastTournamentData { + const factory BroadcastTournamentData({ + required BroadcastTournamentId id, + required String name, + required String? imageUrl, + required String description, + required BroadcastTournamentInformation information, + }) = _BroadcastTournamentData; +} typedef BroadcastTournamentInformation = ({ String? format, @@ -51,6 +65,11 @@ typedef BroadcastTournamentDates = ({ DateTime? endsAt, }); +typedef BroadcastTournamentGroup = ({ + BroadcastTournamentId id, + String name, +}); + @freezed class BroadcastRound with _$BroadcastRound { const BroadcastRound._(); @@ -59,7 +78,7 @@ class BroadcastRound with _$BroadcastRound { required BroadcastRoundId id, required String name, required RoundStatus status, - required DateTime startsAt, + required DateTime? startsAt, }) = _BroadcastRound; } diff --git a/lib/src/model/broadcast/broadcast_providers.dart b/lib/src/model/broadcast/broadcast_providers.dart index 0642c913e6..7527351bac 100644 --- a/lib/src/model/broadcast/broadcast_providers.dart +++ b/lib/src/model/broadcast/broadcast_providers.dart @@ -1,6 +1,7 @@ import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_repository.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'broadcast_providers.g.dart'; @@ -40,3 +41,24 @@ class BroadcastsPaginator extends _$BroadcastsPaginator { ); } } + +@riverpod +Future broadcastTournament( + BroadcastTournamentRef ref, + BroadcastTournamentId broadcastTournamentId, +) { + return ref.withClient( + (client) => + BroadcastRepository(client).getTournament(broadcastTournamentId), + ); +} + +@riverpod +Future broadcastRound( + BroadcastRoundRef ref, + BroadcastRoundId broadcastRoundId, +) { + return ref.withClient( + (client) => BroadcastRepository(client).getRound(broadcastRoundId), + ); +} diff --git a/lib/src/model/broadcast/broadcast_repository.dart b/lib/src/model/broadcast/broadcast_repository.dart index 55e02f3d24..bae404e85d 100644 --- a/lib/src/model/broadcast/broadcast_repository.dart +++ b/lib/src/model/broadcast/broadcast_repository.dart @@ -23,6 +23,16 @@ class BroadcastRepository { ); } + Future getTournament( + BroadcastTournamentId broadcastTournamentId, + ) { + return client.readJson( + Uri(path: 'api/broadcast/$broadcastTournamentId'), + headers: {'Accept': 'application/json'}, + mapper: _makeTournamentFromJson, + ); + } + Future getRound( BroadcastRoundId broadcastRoundId, ) { @@ -51,43 +61,72 @@ BroadcastsList _makeBroadcastResponseFromJson( } Broadcast _broadcastFromPick(RequiredPick pick) { - final live = pick('round', 'ongoing').asBoolOrFalse(); - final finished = pick('round', 'finished').asBoolOrFalse(); - final status = live - ? RoundStatus.live - : finished - ? RoundStatus.finished - : RoundStatus.upcoming; final roundId = pick('round', 'id').asBroadcastRoundIdOrThrow(); return Broadcast( - tour: ( - name: pick('tour', 'name').asStringOrThrow(), - imageUrl: pick('tour', 'image').asStringOrNull(), - description: pick('tour', 'description').asStringOrThrow(), + tour: _tournamentDataFromPick(pick('tour').required()), + round: _roundFromPick(pick('round').required()), + group: pick('group').asStringOrNull(), + roundToLinkId: + pick('roundToLink', 'id').asBroadcastRoundIddOrNull() ?? roundId, + ); +} + +BroadcastTournamentData _tournamentDataFromPick( + RequiredPick pick, +) => + BroadcastTournamentData( + id: pick('id').asBroadcastTournamentIdOrThrow(), + name: pick('name').asStringOrThrow(), + imageUrl: pick('image').asStringOrNull(), + description: pick('description').asStringOrThrow(), information: ( - format: pick('tour', 'info', 'format').asStringOrNull(), - timeControl: pick('tour', 'info', 'tc').asStringOrNull(), - players: pick('tour', 'info', 'players').asStringOrNull(), - dates: pick('tour', 'dates').letOrNull( + format: pick('info', 'format').asStringOrNull(), + timeControl: pick('info', 'tc').asStringOrNull(), + players: pick('info', 'players').asStringOrNull(), + dates: pick('dates').letOrNull( (pick) => ( startsAt: pick(0).asDateTimeFromMillisecondsOrThrow().toLocal(), endsAt: pick(1).asDateTimeFromMillisecondsOrNull()?.toLocal(), ), ), ), - ), - round: BroadcastRound( - id: roundId, - name: pick('round', 'name').asStringOrThrow(), - status: status, - startsAt: pick('round', 'startsAt') - .asDateTimeFromMillisecondsOrThrow() - .toLocal(), - ), - group: pick('group').asStringOrNull(), - roundToLinkId: - pick('roundToLink', 'id').asBroadcastRoundIddOrNull() ?? roundId, + ); + +BroadcastTournament _makeTournamentFromJson( + Map json, +) { + return BroadcastTournament( + data: _tournamentDataFromPick(pick(json, 'tour').required()), + rounds: pick(json, 'rounds').asListOrThrow(_roundFromPick).toIList(), + defaultRoundId: pick(json, 'defaultRoundId').asBroadcastRoundIdOrThrow(), + group: pick(json, 'group', 'tours') + .asListOrNull(_tournamentGroupFromPick) + ?.toIList(), + ); +} + +BroadcastTournamentGroup _tournamentGroupFromPick(RequiredPick pick) { + final id = pick('id').asBroadcastTournamentIdOrThrow(); + final name = pick('name').asStringOrThrow(); + + return (id: id, name: name); +} + +BroadcastRound _roundFromPick(RequiredPick pick) { + final live = pick('ongoing').asBoolOrFalse(); + final finished = pick('finished').asBoolOrFalse(); + final status = live + ? RoundStatus.live + : finished + ? RoundStatus.finished + : RoundStatus.upcoming; + + return BroadcastRound( + id: pick('id').asBroadcastRoundIdOrThrow(), + name: pick('name').asStringOrThrow(), + status: status, + startsAt: pick('startsAt').asDateTimeFromMillisecondsOrNull()?.toLocal(), ); } diff --git a/lib/src/model/broadcast/broadcast_round_controller.dart b/lib/src/model/broadcast/broadcast_round_controller.dart index 4244255415..439f3b0b62 100644 --- a/lib/src/model/broadcast/broadcast_round_controller.dart +++ b/lib/src/model/broadcast/broadcast_round_controller.dart @@ -4,9 +4,9 @@ import 'package:dartchess/dartchess.dart'; import 'package:deep_pick/deep_pick.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast_providers.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_repository.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/utils/json.dart'; @@ -26,7 +26,7 @@ class BroadcastRoundController extends _$BroadcastRoundController { @override Future build(BroadcastRoundId broadcastRoundId) async { _socketClient = ref - .read(socketPoolProvider) + .watch(socketPoolProvider) .open(BroadcastRoundController.broadcastSocketUri(broadcastRoundId)); _subscription = _socketClient.stream.listen(_handleSocketEvent); @@ -35,9 +35,10 @@ class BroadcastRoundController extends _$BroadcastRoundController { _subscription?.cancel(); }); - return await ref.withClient( - (client) => BroadcastRepository(client).getRound(broadcastRoundId), - ); + final games = + await ref.watch(broadcastRoundProvider(broadcastRoundId).future); + + return games; } void _handleSocketEvent(SocketEvent event) { diff --git a/lib/src/model/common/id.dart b/lib/src/model/common/id.dart index 4ab5fb6edb..6aadf924f2 100644 --- a/lib/src/model/common/id.dart +++ b/lib/src/model/common/id.dart @@ -49,6 +49,8 @@ extension type const UserId(String value) implements StringId { extension type const ChallengeId(String value) implements StringId {} +extension type const BroadcastTournamentId(String value) implements StringId {} + extension type const BroadcastRoundId(String value) implements StringId {} extension type const BroadcastGameId(String value) implements StringId {} @@ -149,6 +151,25 @@ extension IDPick on Pick { } } + BroadcastTournamentId asBroadcastTournamentIdOrThrow() { + final value = required().value; + if (value is String) { + return BroadcastTournamentId(value); + } + throw PickException( + "value $value at $debugParsingExit can't be casted to BroadcastRoundId", + ); + } + + BroadcastTournamentId? asBroadcastTournamentIdOrNull() { + if (value == null) return null; + try { + return asBroadcastTournamentIdOrThrow(); + } catch (_) { + return null; + } + } + BroadcastRoundId asBroadcastRoundIdOrThrow() { final value = required().value; if (value is String) { diff --git a/lib/src/view/broadcast/broadcast_boards_tab.dart b/lib/src/view/broadcast/broadcast_boards_tab.dart index 30dcc0bbe3..92214845ab 100644 --- a/lib/src/view/broadcast/broadcast_boards_tab.dart +++ b/lib/src/view/broadcast/broadcast_boards_tab.dart @@ -26,24 +26,29 @@ const _kPlayerWidgetPadding = EdgeInsets.symmetric(vertical: 5.0); class BroadcastBoardsTab extends ConsumerWidget { final BroadcastRoundId roundId; - const BroadcastBoardsTab(this.roundId); + const BroadcastBoardsTab({super.key, required this.roundId}); @override Widget build(BuildContext context, WidgetRef ref) { final games = ref.watch(broadcastRoundControllerProvider(roundId)); - return games.when( - data: (games) => (games.isEmpty) - ? const Text('No boards to show for now') - : BroadcastPreview(games: games.values.toIList()), - loading: () => const Shimmer( - child: ShimmerLoading( - isLoading: true, - child: BroadcastPreview(), + return SafeArea( + child: games.when( + data: (games) => (games.isEmpty) + ? const Padding( + padding: Styles.bodyPadding, + child: Text('No boards to show for now'), + ) + : BroadcastPreview(games: games.values.toIList()), + loading: () => const Shimmer( + child: ShimmerLoading( + isLoading: true, + child: BroadcastPreview(), + ), + ), + error: (error, stackTrace) => Center( + child: Text(error.toString()), ), - ), - error: (error, stackTrace) => Center( - child: Text(error.toString()), ), ); } @@ -71,6 +76,7 @@ class BroadcastPreview extends StatelessWidget { numberOfBoardsByRow; return GridView.builder( + padding: Styles.bodyPadding, itemCount: games == null ? numberLoadingBoards : games!.length, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: numberOfBoardsByRow, diff --git a/lib/src/view/broadcast/broadcast_overview_tab.dart b/lib/src/view/broadcast/broadcast_overview_tab.dart index d4c2dafaa0..094eb0fa73 100644 --- a/lib/src/view/broadcast/broadcast_overview_tab.dart +++ b/lib/src/view/broadcast/broadcast_overview_tab.dart @@ -1,61 +1,83 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; -import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast_providers.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:url_launcher/url_launcher.dart'; final _dateFormatter = DateFormat.MMMd(Intl.getCurrentLocale()); /// A tab that displays the overview of a broadcast. -class BroadcastOverviewTab extends StatelessWidget { - final Broadcast broadcast; +class BroadcastOverviewTab extends ConsumerWidget { + final BroadcastTournamentId tournamentId; - const BroadcastOverviewTab(this.broadcast); + const BroadcastOverviewTab({super.key, required this.tournamentId}); @override - Widget build(BuildContext context) { - final tourInformation = broadcast.tour.information; + Widget build(BuildContext context, WidgetRef ref) { + final tournament = ref.watch(broadcastTournamentProvider(tournamentId)); - return Column( - children: [ - Wrap( - alignment: WrapAlignment.center, - children: [ - if (tourInformation.dates != null) - BroadcastOverviewCard( - CupertinoIcons.calendar, - tourInformation.dates!.endsAt == null - ? _dateFormatter.format(tourInformation.dates!.startsAt) - : '${_dateFormatter.format(tourInformation.dates!.startsAt)} - ${_dateFormatter.format(tourInformation.dates!.endsAt!)}', - ), - if (tourInformation.format != null) - BroadcastOverviewCard( - Icons.emoji_events, - '${tourInformation.format}', - ), - if (tourInformation.timeControl != null) - BroadcastOverviewCard( - CupertinoIcons.stopwatch_fill, - '${tourInformation.timeControl}', - ), - if (tourInformation.players != null) - BroadcastOverviewCard( - Icons.person, - '${tourInformation.players}', - ), - ], - ), - Expanded( - child: Markdown( - data: broadcast.tour.description, - onTapLink: (text, url, title) { - if (url == null) return; - launchUrl(Uri.parse(url)); - }, - ), + return SafeArea( + child: Padding( + padding: Styles.bodyPadding, + child: tournament.when( + data: (tournament) { + final information = tournament.data.information; + final description = tournament.data.description; + + return Column( + children: [ + Wrap( + alignment: WrapAlignment.center, + children: [ + if (information.dates != null) + BroadcastOverviewCard( + CupertinoIcons.calendar, + information.dates!.endsAt == null + ? _dateFormatter.format(information.dates!.startsAt) + : '${_dateFormatter.format(information.dates!.startsAt)} - ${_dateFormatter.format(information.dates!.endsAt!)}', + ), + if (information.format != null) + BroadcastOverviewCard( + Icons.emoji_events, + '${information.format}', + ), + if (information.timeControl != null) + BroadcastOverviewCard( + CupertinoIcons.stopwatch_fill, + '${information.timeControl}', + ), + if (information.players != null) + BroadcastOverviewCard( + Icons.person, + '${information.players}', + ), + ], + ), + Expanded( + child: Markdown( + data: description, + onTapLink: (text, url, title) { + if (url == null) return; + launchUrl(Uri.parse(url)); + }, + ), + ), + ], + ); + }, + loading: () => + const Center(child: CircularProgressIndicator.adaptive()), + error: (error, _) { + return Center( + child: Text('Cannot load game analysis: $error'), + ); + }, ), - ], + ), ); } } diff --git a/lib/src/view/broadcast/broadcast_screen.dart b/lib/src/view/broadcast/broadcast_screen.dart index 1ac9f3bd42..ed7f42e0fa 100644 --- a/lib/src/view/broadcast/broadcast_screen.dart +++ b/lib/src/view/broadcast/broadcast_screen.dart @@ -1,6 +1,9 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast_providers.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_boards_tab.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_overview_tab.dart'; @@ -40,11 +43,27 @@ class _AndroidBody extends StatefulWidget { class _AndroidBodyState extends State<_AndroidBody> with SingleTickerProviderStateMixin { late final TabController _tabController; + late BroadcastTournamentId _selectedTournamentId; + late BroadcastRoundId _selectedRoundId; @override void initState() { super.initState(); _tabController = TabController(initialIndex: 1, length: 2, vsync: this); + _selectedTournamentId = widget.broadcast.tour.id; + _selectedRoundId = widget.broadcast.roundToLinkId; + } + + void setTournamentId(BroadcastTournamentId tournamentId) { + setState(() { + _selectedTournamentId = tournamentId; + }); + } + + void setRoundId(BroadcastRoundId roundId) { + setState(() { + _selectedRoundId = roundId; + }); } @override @@ -69,20 +88,17 @@ class _AndroidBodyState extends State<_AndroidBody> body: TabBarView( controller: _tabController, children: [ - SafeArea( - child: Padding( - padding: Styles.bodyPadding, - child: BroadcastOverviewTab(widget.broadcast), - ), - ), - SafeArea( - child: Padding( - padding: Styles.bodyPadding, - child: BroadcastBoardsTab(widget.broadcast.roundToLinkId), - ), - ), + BroadcastOverviewTab(tournamentId: _selectedTournamentId), + BroadcastBoardsTab(roundId: _selectedRoundId), ], ), + bottomNavigationBar: BottomAppBar( + child: TournamentAndRoundDropdowns( + tournamentId: _selectedTournamentId, + setTournamentId: setTournamentId, + setRoundId: setRoundId, + ), + ), ); } } @@ -100,6 +116,15 @@ enum _ViewMode { overview, boards } class _CupertinoBodyState extends State<_CupertinoBody> { _ViewMode _selectedSegment = _ViewMode.boards; + late BroadcastTournamentId _selectedTournamentId; + late BroadcastRoundId _selectedRoundId; + + @override + void initState() { + super.initState(); + _selectedTournamentId = widget.broadcast.tour.id; + _selectedRoundId = widget.broadcast.roundToLinkId; + } void setViewMode(_ViewMode mode) { setState(() { @@ -107,16 +132,27 @@ class _CupertinoBodyState extends State<_CupertinoBody> { }); } + void setTournamentId(BroadcastTournamentId tournamentId) { + setState(() { + _selectedTournamentId = tournamentId; + }); + } + + void setRoundId(BroadcastRoundId roundId) { + setState(() { + _selectedRoundId = roundId; + }); + } + @override Widget build(BuildContext context) { return SafeArea( - child: Column( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: Styles.bodyPadding, - child: CupertinoSlidingSegmentedControl<_ViewMode>( + child: Padding( + padding: Styles.bodyPadding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + CupertinoSlidingSegmentedControl<_ViewMode>( groupValue: _selectedSegment, children: const { _ViewMode.overview: Text('Overview'), @@ -130,14 +166,92 @@ class _CupertinoBodyState extends State<_CupertinoBody> { } }, ), - ), - Expanded( - child: _selectedSegment == _ViewMode.overview - ? BroadcastOverviewTab(widget.broadcast) - : BroadcastBoardsTab(widget.broadcast.roundToLinkId), - ), - ], + const SizedBox(height: 16), + TournamentAndRoundDropdowns( + tournamentId: _selectedTournamentId, + setTournamentId: setTournamentId, + setRoundId: setRoundId, + ), + const SizedBox(height: 16), + Expanded( + child: _selectedSegment == _ViewMode.overview + ? BroadcastOverviewTab(tournamentId: _selectedTournamentId) + : BroadcastBoardsTab(roundId: _selectedRoundId), + ), + ], + ), ), ); } } + +class TournamentAndRoundDropdowns extends ConsumerWidget { + final BroadcastTournamentId tournamentId; + final void Function(BroadcastTournamentId) setTournamentId; + final void Function(BroadcastRoundId) setRoundId; + + const TournamentAndRoundDropdowns({ + super.key, + required this.tournamentId, + required this.setTournamentId, + required this.setRoundId, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final tournament = ref.watch(broadcastTournamentProvider(tournamentId)); + + return tournament.when( + data: (tournament) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + if (tournament.group != null) + Flexible( + child: DropdownMenu( + label: const Text('Tournament'), + initialSelection: tournament.data.id, + dropdownMenuEntries: tournament.group! + .map( + (tournament) => + DropdownMenuEntry( + value: tournament.id, + label: tournament.name, + ), + ) + .toList(), + onSelected: (BroadcastTournamentId? value) async { + setTournamentId(value!); + final newTournament = await ref.read( + broadcastTournamentProvider(value).future, + ); + setRoundId(newTournament.defaultRoundId); + }, + ), + ), + Flexible( + child: DropdownMenu( + label: const Text('Round'), + initialSelection: tournament.defaultRoundId, + dropdownMenuEntries: tournament.rounds + .map( + (BroadcastRound round) => + DropdownMenuEntry( + value: round.id, + label: round.name, + ), + ) + .toList(), + onSelected: (BroadcastRoundId? value) { + setRoundId(value!); + }, + ), + ), + ], + ); + }, + loading: () => const SizedBox.shrink(), + error: (error, stackTrace) => Center(child: Text(error.toString())), + ); + } +} diff --git a/lib/src/view/broadcast/broadcast_tile.dart b/lib/src/view/broadcast/broadcast_tile.dart index 7a9d7054c3..818d07beed 100644 --- a/lib/src/view/broadcast/broadcast_tile.dart +++ b/lib/src/view/broadcast/broadcast_tile.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; import 'package:lichess_mobile/src/styles/transparent_image.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; @@ -7,7 +8,7 @@ import 'package:lichess_mobile/src/view/broadcast/broadcast_screen.dart'; import 'package:lichess_mobile/src/view/broadcast/default_broadcast_image.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; -class BroadcastTile extends StatelessWidget { +class BroadcastTile extends ConsumerWidget { const BroadcastTile({ required this.broadcast, }); @@ -15,7 +16,7 @@ class BroadcastTile extends StatelessWidget { final Broadcast broadcast; @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { return PlatformListTile( leading: (broadcast.tour.imageUrl != null) ? FadeInImage.memoryNetwork( diff --git a/lib/src/view/broadcast/broadcasts_list_screen.dart b/lib/src/view/broadcast/broadcasts_list_screen.dart index ab77a4e148..6ac0813f9e 100644 --- a/lib/src/view/broadcast/broadcasts_list_screen.dart +++ b/lib/src/view/broadcast/broadcasts_list_screen.dart @@ -187,14 +187,15 @@ class _BodyState extends ConsumerState<_Body> { } } -class BroadcastGridItem extends StatelessWidget { +class BroadcastGridItem extends ConsumerWidget { final Broadcast broadcast; const BroadcastGridItem({required this.broadcast}); BroadcastGridItem.loading() : broadcast = Broadcast( - tour: ( + tour: BroadcastTournamentData( + id: const BroadcastTournamentId(''), name: '', imageUrl: null, description: '', @@ -219,7 +220,7 @@ class BroadcastGridItem extends StatelessWidget { ); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { return AdaptiveInkWell( borderRadius: BorderRadius.circular(20), onTap: () { @@ -290,9 +291,9 @@ class BroadcastGridItem extends StatelessWidget { color: Colors.red, ), ) - else + else if (broadcast.round.startsAt != null) Text( - _dateFormatter.format(broadcast.round.startsAt), + _dateFormatter.format(broadcast.round.startsAt!), style: Theme.of(context) .textTheme .labelSmall From 82063f662f89566728c2f110656d8439293ec7ce Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Fri, 23 Aug 2024 17:58:25 +0200 Subject: [PATCH 171/979] use user defined locale instead of default locale --- lib/src/utils/current_locale.dart | 20 ++++++++++ .../broadcast/broadcast_overview_tab.dart | 8 ++-- .../broadcast/broadcasts_list_screen.dart | 8 ++-- lib/src/view/game/game_list_tile.dart | 7 ++-- .../view/puzzle/puzzle_history_screen.dart | 6 +-- lib/src/view/user/perf_stats_screen.dart | 40 ++++++++++--------- lib/src/view/user/user_activity.dart | 7 ++-- 7 files changed, 61 insertions(+), 35 deletions(-) create mode 100644 lib/src/utils/current_locale.dart diff --git a/lib/src/utils/current_locale.dart b/lib/src/utils/current_locale.dart new file mode 100644 index 0000000000..e2205a29e0 --- /dev/null +++ b/lib/src/utils/current_locale.dart @@ -0,0 +1,20 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:intl/intl.dart'; +import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'current_locale.g.dart'; + +@riverpod +String currentLocale(CurrentLocaleRef ref) { + return ref.watch(generalPreferencesProvider).locale?.languageCode ?? + Intl.getCurrentLocale(); +} + +extension LocaleWidgetRefExtension on WidgetRef { + /// Runs [fn] with the current locale. + T withLocale(T Function(String) fn) { + final currentLocale = watch(currentLocaleProvider); + return fn(currentLocale); + } +} diff --git a/lib/src/view/broadcast/broadcast_overview_tab.dart b/lib/src/view/broadcast/broadcast_overview_tab.dart index 094eb0fa73..458be81ef8 100644 --- a/lib/src/view/broadcast/broadcast_overview_tab.dart +++ b/lib/src/view/broadcast/broadcast_overview_tab.dart @@ -6,10 +6,9 @@ import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_providers.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/utils/current_locale.dart'; import 'package:url_launcher/url_launcher.dart'; -final _dateFormatter = DateFormat.MMMd(Intl.getCurrentLocale()); - /// A tab that displays the overview of a broadcast. class BroadcastOverviewTab extends ConsumerWidget { final BroadcastTournamentId tournamentId; @@ -19,6 +18,7 @@ class BroadcastOverviewTab extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final tournament = ref.watch(broadcastTournamentProvider(tournamentId)); + final dateFormatter = ref.withLocale((locale) => DateFormat.MMMd(locale)); return SafeArea( child: Padding( @@ -37,8 +37,8 @@ class BroadcastOverviewTab extends ConsumerWidget { BroadcastOverviewCard( CupertinoIcons.calendar, information.dates!.endsAt == null - ? _dateFormatter.format(information.dates!.startsAt) - : '${_dateFormatter.format(information.dates!.startsAt)} - ${_dateFormatter.format(information.dates!.endsAt!)}', + ? dateFormatter.format(information.dates!.startsAt) + : '${dateFormatter.format(information.dates!.startsAt)} - ${dateFormatter.format(information.dates!.endsAt!)}', ), if (information.format != null) BroadcastOverviewCard( diff --git a/lib/src/view/broadcast/broadcasts_list_screen.dart b/lib/src/view/broadcast/broadcasts_list_screen.dart index 6ac0813f9e..ec217d1f60 100644 --- a/lib/src/view/broadcast/broadcasts_list_screen.dart +++ b/lib/src/view/broadcast/broadcasts_list_screen.dart @@ -8,6 +8,7 @@ import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/styles/lichess_colors.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/styles/transparent_image.dart'; +import 'package:lichess_mobile/src/utils/current_locale.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_screen.dart'; @@ -16,8 +17,6 @@ import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; -final _dateFormatter = DateFormat.MMMd(Intl.getCurrentLocale()).add_Hm(); - /// A screen that displays a paginated list of broadcasts. class BroadcastsListScreen extends StatelessWidget { const BroadcastsListScreen({super.key}); @@ -221,6 +220,9 @@ class BroadcastGridItem extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final dateFormatter = + ref.withLocale((locale) => DateFormat.yMMMd(locale).add_Hm()); + return AdaptiveInkWell( borderRadius: BorderRadius.circular(20), onTap: () { @@ -293,7 +295,7 @@ class BroadcastGridItem extends ConsumerWidget { ) else if (broadcast.round.startsAt != null) Text( - _dateFormatter.format(broadcast.round.startsAt!), + dateFormatter.format(broadcast.round.startsAt!), style: Theme.of(context) .textTheme .labelSmall diff --git a/lib/src/view/game/game_list_tile.dart b/lib/src/view/game/game_list_tile.dart index 89dffb101b..f3817c4f75 100644 --- a/lib/src/view/game/game_list_tile.dart +++ b/lib/src/view/game/game_list_tile.dart @@ -11,6 +11,7 @@ import 'package:lichess_mobile/src/model/game/game_share_service.dart'; import 'package:lichess_mobile/src/model/game/game_status.dart'; import 'package:lichess_mobile/src/styles/lichess_colors.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/utils/current_locale.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/share.dart'; @@ -25,8 +26,6 @@ import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/user_full_name.dart'; import 'package:timeago/timeago.dart' as timeago; -final _dateFormatter = DateFormat.yMMMd(Intl.getCurrentLocale()).add_Hm(); - /// A list tile that shows game info. class GameListTile extends StatelessWidget { const GameListTile({ @@ -110,6 +109,8 @@ class _ContextMenu extends ConsumerWidget { final orientation = mySide; final customColors = Theme.of(context).extension(); + final dateFormatter = + ref.withLocale((locale) => DateFormat.yMMMd(locale).add_Hm()); return DraggableScrollableSheet( initialChildSize: .7, @@ -178,7 +179,7 @@ class _ContextMenu extends ConsumerWidget { ), ), Text( - _dateFormatter.format(game.lastMoveAt), + dateFormatter.format(game.lastMoveAt), style: TextStyle( color: textShade( context, diff --git a/lib/src/view/puzzle/puzzle_history_screen.dart b/lib/src/view/puzzle/puzzle_history_screen.dart index be900aaea4..2f6af5037f 100644 --- a/lib/src/view/puzzle/puzzle_history_screen.dart +++ b/lib/src/view/puzzle/puzzle_history_screen.dart @@ -10,6 +10,7 @@ import 'package:lichess_mobile/src/model/puzzle/puzzle_activity.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_angle.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_theme.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/utils/current_locale.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; @@ -19,8 +20,6 @@ import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:timeago/timeago.dart' as timeago; -final _dateFormatter = DateFormat.yMMMd(Intl.getCurrentLocale()); - class PuzzleHistoryScreen extends StatelessWidget { @override Widget build(BuildContext context) { @@ -128,6 +127,7 @@ class _BodyState extends ConsumerState<_Body> { @override Widget build(BuildContext context) { final historyState = ref.watch(puzzleActivityProvider); + final dateFormatter = ref.withLocale((locale) => DateFormat.yMMMd(locale)); return historyState.when( data: (state) { @@ -182,7 +182,7 @@ class _BodyState extends ConsumerState<_Body> { ); } else if (element is DateTime) { final title = DateTime.now().difference(element).inDays >= 15 - ? _dateFormatter.format(element) + ? dateFormatter.format(element) : timeago.format(element); return Padding( padding: const EdgeInsets.only(left: _kPuzzlePadding) diff --git a/lib/src/view/user/perf_stats_screen.dart b/lib/src/view/user/perf_stats_screen.dart index 8b447d669c..46a346d929 100644 --- a/lib/src/view/user/perf_stats_screen.dart +++ b/lib/src/view/user/perf_stats_screen.dart @@ -19,6 +19,7 @@ import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/model/user/user_repository_providers.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/utils/current_locale.dart'; import 'package:lichess_mobile/src/utils/duration.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; @@ -34,9 +35,6 @@ import 'package:lichess_mobile/src/widgets/rating.dart'; import 'package:lichess_mobile/src/widgets/stat_card.dart'; import 'package:lichess_mobile/src/widgets/user_full_name.dart'; -final _currentLocale = Intl.getCurrentLocale(); -final _dateFormatter = DateFormat.yMMMd(_currentLocale); - const _customOpacity = 0.6; const _defaultStatFontSize = 12.0; const _defaultValueFontSize = 18.0; @@ -164,6 +162,7 @@ class _Body extends ConsumerWidget { final loggedInUser = ref.watch(authSessionProvider); const statGroupSpace = SizedBox(height: 15.0); const subStatSpace = SizedBox(height: 10); + final currentLocale = ref.watch(currentLocaleProvider); return perfStats.when( data: (data) { @@ -238,9 +237,8 @@ class _Body extends ConsumerWidget { context.l10n.rank, value: data.rank == null ? '?' - : NumberFormat.decimalPattern( - Intl.getCurrentLocale(), - ).format(data.rank), + : NumberFormat.decimalPattern(currentLocale) + .format(data.rank), ), StatCard( context.l10n @@ -485,13 +483,13 @@ class _ProgressionWidget extends StatelessWidget { } } -class _UserGameWidget extends StatelessWidget { +class _UserGameWidget extends ConsumerWidget { final UserPerfGame? game; const _UserGameWidget(this.game); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { // TODO: Implement functionality to view game on tap. // (Return a button? Wrap with InkWell?) const defaultDateFontSize = 16.0; @@ -499,11 +497,12 @@ class _UserGameWidget extends StatelessWidget { color: Theme.of(context).colorScheme.tertiary, fontSize: defaultDateFontSize, ); + final dateFormatter = ref.withLocale((locale) => DateFormat.yMMMd(locale)); return game == null ? Text('?', style: defaultDateStyle) : Text( - _dateFormatter.format(game!.finishedAt), + dateFormatter.format(game!.finishedAt), style: defaultDateStyle, ); } @@ -686,6 +685,8 @@ class _GameListWidget extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final dateFormatter = ref.withLocale((locale) => DateFormat.yMMMd(locale)); + return ListSection( header: header, margin: const EdgeInsets.only(top: 10.0), @@ -725,7 +726,7 @@ class _GameListWidget extends ConsumerWidget { rating: game.opponentRating, ), subtitle: Text( - _dateFormatter.format(game.finishedAt), + dateFormatter.format(game.finishedAt), ), ), ], @@ -761,16 +762,16 @@ class _GameListTile extends StatelessWidget { } } -class _EloChart extends StatefulWidget { +class _EloChart extends ConsumerStatefulWidget { final UserRatingHistoryPerf value; const _EloChart(this.value); @override - State<_EloChart> createState() => _EloChartState(); + ConsumerState<_EloChart> createState() => _EloChartState(); } -class _EloChartState extends State<_EloChart> { +class _EloChartState extends ConsumerState<_EloChart> { late DateRange _selectedRange; late List _allFlSpot; @@ -860,12 +861,13 @@ class _EloChartState extends State<_EloChart> { final borderColor = Theme.of(context).colorScheme.onSurface.withOpacity(0.5); final chartColor = Theme.of(context).colorScheme.tertiary; + final currentLocale = ref.watch(currentLocaleProvider); final chartDateFormatter = switch (_selectedRange) { - DateRange.oneWeek => DateFormat.MMMd(_currentLocale), - DateRange.oneMonth => DateFormat.MMMd(_currentLocale), - DateRange.threeMonths => DateFormat.yMMM(_currentLocale), - DateRange.oneYear => DateFormat.yMMM(_currentLocale), - DateRange.allTime => DateFormat.yMMM(_currentLocale), + DateRange.oneWeek => DateFormat.MMMd(currentLocale), + DateRange.oneMonth => DateFormat.MMMd(currentLocale), + DateRange.threeMonths => DateFormat.yMMM(currentLocale), + DateRange.oneYear => DateFormat.yMMM(currentLocale), + DateRange.allTime => DateFormat.yMMM(currentLocale), }; String formatDateFromTimestamp(double nbDays) => chartDateFormatter.format( @@ -873,7 +875,7 @@ class _EloChartState extends State<_EloChart> { ); String formatDateFromTimestampForTooltip(double nbDays) => - DateFormat.yMMMd(_currentLocale).format( + DateFormat.yMMMd(currentLocale).format( _firstDate.add(Duration(days: nbDays.toInt())), ); diff --git a/lib/src/view/user/user_activity.dart b/lib/src/view/user/user_activity.dart index 1e87d8cbe9..9500f7b542 100644 --- a/lib/src/view/user/user_activity.dart +++ b/lib/src/view/user/user_activity.dart @@ -10,6 +10,7 @@ import 'package:lichess_mobile/src/model/user/user_repository.dart'; import 'package:lichess_mobile/src/styles/lichess_colors.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/utils/current_locale.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/view/account/rating_pref_aware.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; @@ -19,8 +20,6 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'user_activity.g.dart'; -final _dateFormatter = DateFormat.yMMMd(Intl.getCurrentLocale()); - @riverpod Future> _userActivity( _UserActivityRef ref, { @@ -98,6 +97,8 @@ class UserActivityEntry extends ConsumerWidget { final redColor = theme.extension()?.error; final greenColor = theme.extension()?.good; + final dateFormatter = ref.withLocale((locale) => DateFormat.yMMMd(locale)); + return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ @@ -109,7 +110,7 @@ class UserActivityEntry extends ConsumerWidget { bottom: 4.0, ), child: Text( - _dateFormatter.format(entry.startTime), + dateFormatter.format(entry.startTime), style: TextStyle( color: context.lichessColors.brag, fontWeight: FontWeight.bold, From 1eaae203383c727613c05990b29f86fb32b1da2d Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Mon, 19 Aug 2024 23:49:09 +0200 Subject: [PATCH 172/979] add broadcast board screen --- lib/src/model/broadcast/broadcast.dart | 1 + .../broadcast/broadcast_game_controller.dart | 111 ++++++++++++++++++ .../model/broadcast/broadcast_providers.dart | 11 ++ .../model/broadcast/broadcast_repository.dart | 11 ++ .../broadcast/broadcast_round_controller.dart | 5 +- lib/src/view/analysis/analysis_screen.dart | 106 ++++++++--------- .../board_editor/board_editor_screen.dart | 2 +- .../view/broadcast/broadcast_boards_tab.dart | 28 ++++- .../broadcast_boards_tab_provider.dart | 33 ++++++ .../offline_correspondence_game_screen.dart | 2 +- lib/src/view/game/archived_game_screen.dart | 2 +- lib/src/view/game/game_body.dart | 4 +- lib/src/view/game/game_list_tile.dart | 13 +- .../view/game/game_list_tile_providers.dart | 34 ++++++ lib/src/view/game/game_result_dialog.dart | 2 +- lib/src/view/puzzle/puzzle_screen.dart | 2 +- lib/src/view/puzzle/streak_screen.dart | 2 +- lib/src/view/tools/load_position_screen.dart | 2 +- lib/src/view/tools/tools_tab_screen.dart | 2 +- test/view/analysis/analysis_screen_test.dart | 4 +- 20 files changed, 292 insertions(+), 85 deletions(-) create mode 100644 lib/src/model/broadcast/broadcast_game_controller.dart create mode 100644 lib/src/view/broadcast/broadcast_boards_tab_provider.dart create mode 100644 lib/src/view/game/game_list_tile_providers.dart diff --git a/lib/src/model/broadcast/broadcast.dart b/lib/src/model/broadcast/broadcast.dart index c152cfad34..d265619007 100644 --- a/lib/src/model/broadcast/broadcast.dart +++ b/lib/src/model/broadcast/broadcast.dart @@ -89,6 +89,7 @@ class BroadcastGameSnapshot with _$BroadcastGameSnapshot { const BroadcastGameSnapshot._(); const factory BroadcastGameSnapshot({ + required BroadcastGameId id, required IMap players, required String fen, required Move? lastMove, diff --git a/lib/src/model/broadcast/broadcast_game_controller.dart b/lib/src/model/broadcast/broadcast_game_controller.dart new file mode 100644 index 0000000000..031f56337e --- /dev/null +++ b/lib/src/model/broadcast/broadcast_game_controller.dart @@ -0,0 +1,111 @@ +import 'dart:async'; + +import 'package:dartchess/dartchess.dart'; +import 'package:deep_pick/deep_pick.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast_providers.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast_round_controller.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/common/node.dart'; +import 'package:lichess_mobile/src/model/common/socket.dart'; +import 'package:lichess_mobile/src/utils/json.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'broadcast_game_controller.g.dart'; + +@riverpod +class BroadcastGameController extends _$BroadcastGameController { + static Uri broadcastSocketUri(BroadcastRoundId broadcastRoundId) => + Uri(path: 'study/$broadcastRoundId/socket/v6'); + + StreamSubscription? _subscription; + + late SocketClient _socketClient; + + @override + Future build({ + required BroadcastRoundId broadcastRoundId, + required BroadcastGameId broadcastGameId, + }) async { + _socketClient = ref + .watch(socketPoolProvider) + .open(BroadcastRoundController.broadcastSocketUri(broadcastRoundId)); + + _subscription = _socketClient.stream.listen(_handleSocketEvent); + + ref.onDispose(() { + _subscription?.cancel(); + }); + + final pgn = await ref.watch( + broadcastGameProvider( + roundId: broadcastRoundId, + gameId: broadcastGameId, + ).future, + ); + + return pgn; + } + + void _handleSocketEvent(SocketEvent event) { + if (!state.hasValue) return; + + switch (event.topic) { + // Sent when a node is recevied from the broadcast + case 'addNode': + _handleAddNodeEvent(event); + // Sent when a pgn tag changes + case 'setTags': + _handleSetTagsEvent(event); + } + } + + void _handleAddNodeEvent(SocketEvent event) { + final gameId = + pick(event.data, 'p', 'chapterId').asBroadcastGameIdOrThrow(); + + // We check that the event we received is for the game we are currently watching + if (gameId != broadcastGameId) return; + + // The path of the last and current move of the broadcasted game + // Its value is "!" if the path is identical to one of the node that was received + final currentPath = pick(event.data, 'relayPath').asUciPathOrThrow(); + + // We check that the event we received is for the last move of the game + if (currentPath.value != '!') return; + + // The path for the node that was received + final path = pick(event.data, 'p', 'path').asUciPathOrThrow(); + final nodeId = pick(event.data, 'n', 'id').asUciCharPairOrThrow(); + + print(state.requireValue); + + print(path + nodeId); + + final newPgn = (Root.fromPgnGame(PgnGame.parsePgn(state.requireValue)) + ..promoteAt(path + nodeId, toMainline: true)) + .makePgn(); + + print(newPgn); + + state = AsyncData(newPgn); + } + + void _handleSetTagsEvent(SocketEvent event) { + final gameId = pick(event.data, 'chapterId').asBroadcastGameIdOrThrow(); + + // We check that the event we received is for the game we are currently watching + if (gameId != broadcastGameId) return; + + final headers = pick(event.data, 'tags').asMapOrThrow(); + + final pgnGame = PgnGame.parsePgn(state.requireValue); + + final newPgnGame = PgnGame( + headers: headers, + moves: pgnGame.moves, + comments: pgnGame.comments, + ); + + state = AsyncData(newPgnGame.makePgn()); + } +} diff --git a/lib/src/model/broadcast/broadcast_providers.dart b/lib/src/model/broadcast/broadcast_providers.dart index 7527351bac..6d101c54b7 100644 --- a/lib/src/model/broadcast/broadcast_providers.dart +++ b/lib/src/model/broadcast/broadcast_providers.dart @@ -62,3 +62,14 @@ Future broadcastRound( (client) => BroadcastRepository(client).getRound(broadcastRoundId), ); } + +@riverpod +Future broadcastGame( + BroadcastGameRef ref, { + required BroadcastRoundId roundId, + required BroadcastGameId gameId, +}) { + return ref.withClient( + (client) => BroadcastRepository(client).getGame(roundId, gameId), + ); +} diff --git a/lib/src/model/broadcast/broadcast_repository.dart b/lib/src/model/broadcast/broadcast_repository.dart index bae404e85d..410e03914c 100644 --- a/lib/src/model/broadcast/broadcast_repository.dart +++ b/lib/src/model/broadcast/broadcast_repository.dart @@ -44,6 +44,16 @@ class BroadcastRepository { mapper: _makeGamesFromJson, ); } + + Future getGame( + BroadcastRoundId roundId, + BroadcastGameId gameId, + ) { + return client.read( + Uri(path: 'api/study/$roundId/$gameId.pgn'), + headers: {'Accept': 'application/json'}, + ); + } } BroadcastsList _makeBroadcastResponseFromJson( @@ -144,6 +154,7 @@ MapEntry gameFromPick( MapEntry( pick('id').asBroadcastGameIdOrThrow(), BroadcastGameSnapshot( + id: pick('id').asBroadcastGameIdOrThrow(), players: IMap({ Side.white: _playerFromPick(pick('players', 0).required()), Side.black: _playerFromPick(pick('players', 1).required()), diff --git a/lib/src/model/broadcast/broadcast_round_controller.dart b/lib/src/model/broadcast/broadcast_round_controller.dart index 439f3b0b62..6d112b4818 100644 --- a/lib/src/model/broadcast/broadcast_round_controller.dart +++ b/lib/src/model/broadcast/broadcast_round_controller.dart @@ -48,7 +48,7 @@ class BroadcastRoundController extends _$BroadcastRoundController { // Sent when a node is recevied from the broadcast case 'addNode': _handleAddNodeEvent(event); - // Sent when a game ends + // Sent when the state of games changes case 'chapters': _handleChaptersEvent(event); // Sent when clocks are updated from the broadcast @@ -61,9 +61,6 @@ class BroadcastRoundController extends _$BroadcastRoundController { // The path of the last and current move of the broadcasted game // Its value is "!" if the path is identical to one of the node that was received final currentPath = pick(event.data, 'relayPath').asUciPathOrThrow(); - // The path for the node that was received - // final path = pick(event.data, 'p', 'path').asUciPathOrThrow(); - // final nodeId = pick(event.data, 'n', 'id').asUciCharPairOrThrow(); // We check that the event we received is for the last move of the game if (currentPath.value != '!') return; diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index e2e51c0e5a..6f7dc14779 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -16,10 +16,8 @@ import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/eval.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; -import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/engine/engine.dart'; import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; -import 'package:lichess_mobile/src/model/game/game_repository_providers.dart'; import 'package:lichess_mobile/src/model/game/game_share_service.dart'; import 'package:lichess_mobile/src/model/settings/brightness.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; @@ -45,79 +43,75 @@ import 'analysis_board.dart'; import 'analysis_settings.dart'; import 'tree_view.dart'; -class AnalysisScreen extends StatelessWidget { - const AnalysisScreen({ - required this.options, - required this.pgnOrId, +typedef OptionsAndPgn = ({AnalysisOptions options, String pgn}); + +class AnalysisLoadingScreen extends ConsumerWidget { + const AnalysisLoadingScreen({ + required this.pgnAndOptionsProvider, this.title, }); - /// The analysis options. - final AnalysisOptions options; - - /// The PGN or game ID to load. - final String pgnOrId; - - final String? title; + /// The async provider that returns analysis options and pgn. + final ProviderListenable> pgnAndOptionsProvider; - @override - Widget build(BuildContext context) { - return pgnOrId.length == 8 && GameId(pgnOrId).isValid - ? _LoadGame(GameId(pgnOrId), options, title) - : _LoadedAnalysisScreen( - options: options, - pgn: pgnOrId, - title: title, - ); - } -} - -class _LoadGame extends ConsumerWidget { - const _LoadGame(this.gameId, this.options, this.title); - - final AnalysisOptions options; - final GameId gameId; final String? title; @override Widget build(BuildContext context, WidgetRef ref) { - final gameAsync = ref.watch(archivedGameProvider(id: gameId)); - - return gameAsync.when( - data: (game) { - final serverAnalysis = - game.white.analysis != null && game.black.analysis != null - ? (white: game.white.analysis!, black: game.black.analysis!) - : null; - return _LoadedAnalysisScreen( - options: options.copyWith( - id: game.id, - opening: game.meta.opening, - division: game.meta.division, - serverAnalysis: serverAnalysis, - ), - pgn: game.makePgn(), - title: title, - ); - }, - loading: () => const Center(child: CircularProgressIndicator.adaptive()), - error: (error, _) { - return Center( + final pgnAndOptions = ref.watch(pgnAndOptionsProvider); + + Widget buildPlatformScreen({required Widget child}) => + switch (Theme.of(context).platform) { + TargetPlatform.android => _androidBuilder(context, child), + TargetPlatform.iOS => _iosBuilder(context, child), + _ => throw UnimplementedError( + 'Unexpected platform ${Theme.of(context).platform}', + ) + }; + + return pgnAndOptions.when( + data: (pgnAndOptions) => AnalysisScreen( + pgn: pgnAndOptions.pgn, + options: pgnAndOptions.options, + title: title, + ), + loading: () => buildPlatformScreen( + child: const Center(child: CircularProgressIndicator.adaptive())), + error: (error, _) => buildPlatformScreen( + child: Center( child: Text('Cannot load game analysis: $error'), - ); - }, + ), + ), ); } + + Widget _androidBuilder(BuildContext context, Widget child) => Scaffold( + appBar: AppBar( + title: Text(title ?? context.l10n.analysis), + ), + body: child, + ); + + Widget _iosBuilder(BuildContext context, Widget child) => + CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + middle: Text(title ?? context.l10n.analysis), + ), + child: child, + ); } -class _LoadedAnalysisScreen extends ConsumerWidget { - const _LoadedAnalysisScreen({ +class AnalysisScreen extends ConsumerWidget { + const AnalysisScreen({ required this.options, required this.pgn, this.title, }); + /// The analysis options. final AnalysisOptions options; + + /// The PGN or game ID to load. final String pgn; final String? title; diff --git a/lib/src/view/board_editor/board_editor_screen.dart b/lib/src/view/board_editor/board_editor_screen.dart index 7d62fada35..577e3fcaaa 100644 --- a/lib/src/view/board_editor/board_editor_screen.dart +++ b/lib/src/view/board_editor/board_editor_screen.dart @@ -364,7 +364,7 @@ class _BottomBar extends ConsumerWidget { context, rootNavigator: true, builder: (context) => AnalysisScreen( - pgnOrId: editorState.pgn!, + pgn: editorState.pgn!, options: AnalysisOptions( isLocalEvaluationAllowed: true, variant: Variant.fromPosition, diff --git a/lib/src/view/broadcast/broadcast_boards_tab.dart b/lib/src/view/broadcast/broadcast_boards_tab.dart index 92214845ab..fc71a97ea9 100644 --- a/lib/src/view/broadcast/broadcast_boards_tab.dart +++ b/lib/src/view/broadcast/broadcast_boards_tab.dart @@ -12,7 +12,10 @@ import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/duration.dart'; import 'package:lichess_mobile/src/utils/lichess_assets.dart'; +import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; +import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; +import 'package:lichess_mobile/src/view/broadcast/broadcast_boards_tab_provider.dart'; import 'package:lichess_mobile/src/widgets/board_thumbnail.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; @@ -39,11 +42,14 @@ class BroadcastBoardsTab extends ConsumerWidget { padding: Styles.bodyPadding, child: Text('No boards to show for now'), ) - : BroadcastPreview(games: games.values.toIList()), + : BroadcastPreview( + games: games.values.toIList(), + roundId: roundId, + ), loading: () => const Shimmer( child: ShimmerLoading( isLoading: true, - child: BroadcastPreview(), + child: BroadcastPreview(roundId: BroadcastRoundId('')), ), ), error: (error, stackTrace) => Center( @@ -54,13 +60,14 @@ class BroadcastBoardsTab extends ConsumerWidget { } } -class BroadcastPreview extends StatelessWidget { +class BroadcastPreview extends ConsumerWidget { + final BroadcastRoundId roundId; final IList? games; - const BroadcastPreview({super.key, this.games}); + const BroadcastPreview({super.key, required this.roundId, this.games}); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { const numberLoadingBoards = 12; const boardSpacing = 10.0; // height of the text based on the font size @@ -97,6 +104,17 @@ class BroadcastPreview extends StatelessWidget { final playingSide = Setup.parseFen(game.fen).turn; return BoardThumbnail( + onTap: () { + pushPlatformRoute( + context, + builder: (context) => AnalysisLoadingScreen( + pgnAndOptionsProvider: broadcastGameAnalysisProvider( + roundId: roundId, + gameId: game.id, + ), + ), + ); + }, orientation: Side.white, fen: game.fen, lastMove: game.lastMove, diff --git a/lib/src/view/broadcast/broadcast_boards_tab_provider.dart b/lib/src/view/broadcast/broadcast_boards_tab_provider.dart new file mode 100644 index 0000000000..9522226197 --- /dev/null +++ b/lib/src/view/broadcast/broadcast_boards_tab_provider.dart @@ -0,0 +1,33 @@ +import 'package:dartchess/dartchess.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast_game_controller.dart'; +import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'broadcast_boards_tab_provider.g.dart'; + +@riverpod +Future broadcastGameAnalysis( + BroadcastGameAnalysisRef ref, { + required BroadcastRoundId roundId, + required BroadcastGameId gameId, +}) async { + final pgn = await ref.watch( + broadcastGameControllerProvider( + broadcastRoundId: roundId, + broadcastGameId: gameId, + ).future, + ); + + return ( + options: const AnalysisOptions( + id: StringId('standalone_analysis'), + isLocalEvaluationAllowed: true, + orientation: Side.white, + variant: Variant.standard, + ), + pgn: pgn, + ); +} diff --git a/lib/src/view/correspondence/offline_correspondence_game_screen.dart b/lib/src/view/correspondence/offline_correspondence_game_screen.dart index 426ff94aac..71da4ce456 100644 --- a/lib/src/view/correspondence/offline_correspondence_game_screen.dart +++ b/lib/src/view/correspondence/offline_correspondence_game_screen.dart @@ -286,7 +286,7 @@ class _BodyState extends ConsumerState<_Body> { pushPlatformRoute( context, builder: (_) => AnalysisScreen( - pgnOrId: game.makePgn(), + pgn: game.makePgn(), options: AnalysisOptions( isLocalEvaluationAllowed: false, variant: game.variant, diff --git a/lib/src/view/game/archived_game_screen.dart b/lib/src/view/game/archived_game_screen.dart index a6493d7d5c..c8bdfc8699 100644 --- a/lib/src/view/game/archived_game_screen.dart +++ b/lib/src/view/game/archived_game_screen.dart @@ -294,7 +294,7 @@ class _BottomBar extends ConsumerWidget { context, builder: (context) => AnalysisScreen( title: context.l10n.gameAnalysis, - pgnOrId: game.makePgn(), + pgn: game.makePgn(), options: AnalysisOptions( isLocalEvaluationAllowed: true, variant: gameData.variant, diff --git a/lib/src/view/game/game_body.dart b/lib/src/view/game/game_body.dart index 322c7e0528..97e8af92ad 100644 --- a/lib/src/view/game/game_body.dart +++ b/lib/src/view/game/game_body.dart @@ -550,7 +550,7 @@ class _GameBottomBar extends ConsumerWidget { pushPlatformRoute( context, builder: (_) => AnalysisScreen( - pgnOrId: gameState.analysisPgn, + pgn: gameState.analysisPgn, options: gameState.analysisOptions, title: context.l10n.gameAnalysis, ), @@ -700,7 +700,7 @@ class _GameBottomBar extends ConsumerWidget { pushPlatformRoute( context, builder: (_) => AnalysisScreen( - pgnOrId: gameState.analysisPgn, + pgn: gameState.analysisPgn, options: gameState.analysisOptions.copyWith( isLocalEvaluationAllowed: false, ), diff --git a/lib/src/view/game/game_list_tile.dart b/lib/src/view/game/game_list_tile.dart index f3817c4f75..3a4e9b9799 100644 --- a/lib/src/view/game/game_list_tile.dart +++ b/lib/src/view/game/game_list_tile.dart @@ -3,7 +3,6 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; -import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/game/archived_game.dart'; @@ -17,6 +16,7 @@ import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/share.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; import 'package:lichess_mobile/src/view/game/archived_game_screen.dart'; +import 'package:lichess_mobile/src/view/game/game_list_tile_providers.dart'; import 'package:lichess_mobile/src/view/game/game_screen.dart'; import 'package:lichess_mobile/src/view/game/status_l10n.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; @@ -239,15 +239,12 @@ class _ContextMenu extends ConsumerWidget { ? () { pushPlatformRoute( context, - builder: (context) => AnalysisScreen( - title: context.l10n.gameAnalysis, - pgnOrId: game.id.value, - options: AnalysisOptions( - isLocalEvaluationAllowed: true, - variant: game.variant, - orientation: orientation, + builder: (context) => AnalysisLoadingScreen( + pgnAndOptionsProvider: archivedGameAnalysisProvider( id: game.id, + orientation: orientation, ), + title: context.l10n.gameAnalysis, ), ); } diff --git a/lib/src/view/game/game_list_tile_providers.dart b/lib/src/view/game/game_list_tile_providers.dart new file mode 100644 index 0000000000..517931c002 --- /dev/null +++ b/lib/src/view/game/game_list_tile_providers.dart @@ -0,0 +1,34 @@ +import 'package:dartchess/dartchess.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/game/game_repository_providers.dart'; +import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'game_list_tile_providers.g.dart'; + +@riverpod +Future archivedGameAnalysis( + ArchivedGameAnalysisRef ref, { + required GameId id, + required Side orientation, +}) async { + final game = await ref.watch(archivedGameProvider(id: id).future); + final serverAnalysis = + game.white.analysis != null && game.black.analysis != null + ? (white: game.white.analysis!, black: game.black.analysis!) + : null; + + return ( + options: AnalysisOptions( + id: game.id, + isLocalEvaluationAllowed: true, + orientation: orientation, + variant: game.meta.variant, + opening: game.meta.opening, + division: game.meta.division, + serverAnalysis: serverAnalysis, + ), + pgn: game.makePgn(), + ); +} diff --git a/lib/src/view/game/game_result_dialog.dart b/lib/src/view/game/game_result_dialog.dart index 4c3dd5da43..d219a18dc2 100644 --- a/lib/src/view/game/game_result_dialog.dart +++ b/lib/src/view/game/game_result_dialog.dart @@ -211,7 +211,7 @@ class _GameEndDialogState extends ConsumerState { pushPlatformRoute( context, builder: (_) => AnalysisScreen( - pgnOrId: gameState.analysisPgn, + pgn: gameState.analysisPgn, options: gameState.analysisOptions, title: context.l10n.gameAnalysis, ), diff --git a/lib/src/view/puzzle/puzzle_screen.dart b/lib/src/view/puzzle/puzzle_screen.dart index c92902c0d5..ce3a5188e6 100644 --- a/lib/src/view/puzzle/puzzle_screen.dart +++ b/lib/src/view/puzzle/puzzle_screen.dart @@ -536,7 +536,7 @@ class _BottomBar extends ConsumerWidget { context, builder: (context) => AnalysisScreen( title: context.l10n.analysis, - pgnOrId: ref.read(ctrlProvider.notifier).makePgn(), + pgn: ref.read(ctrlProvider.notifier).makePgn(), options: AnalysisOptions( isLocalEvaluationAllowed: true, variant: Variant.standard, diff --git a/lib/src/view/puzzle/streak_screen.dart b/lib/src/view/puzzle/streak_screen.dart index cbf343815c..e053991e29 100644 --- a/lib/src/view/puzzle/streak_screen.dart +++ b/lib/src/view/puzzle/streak_screen.dart @@ -319,7 +319,7 @@ class _BottomBar extends ConsumerWidget { context, builder: (context) => AnalysisScreen( title: context.l10n.analysis, - pgnOrId: ref.read(ctrlProvider.notifier).makePgn(), + pgn: ref.read(ctrlProvider.notifier).makePgn(), options: AnalysisOptions( isLocalEvaluationAllowed: true, variant: Variant.standard, diff --git a/lib/src/view/tools/load_position_screen.dart b/lib/src/view/tools/load_position_screen.dart index 58e8a8e203..75375b90b5 100644 --- a/lib/src/view/tools/load_position_screen.dart +++ b/lib/src/view/tools/load_position_screen.dart @@ -104,7 +104,7 @@ class _BodyState extends State<_Body> { context, rootNavigator: true, builder: (context) => AnalysisScreen( - pgnOrId: parsedInput!.pgn, + pgn: parsedInput!.pgn, options: parsedInput!.options, ), ) diff --git a/lib/src/view/tools/tools_tab_screen.dart b/lib/src/view/tools/tools_tab_screen.dart index 97a6562c6f..3ce37fc2ea 100644 --- a/lib/src/view/tools/tools_tab_screen.dart +++ b/lib/src/view/tools/tools_tab_screen.dart @@ -123,7 +123,7 @@ class _Body extends StatelessWidget { context, rootNavigator: true, builder: (context) => const AnalysisScreen( - pgnOrId: '', + pgn: '', options: AnalysisOptions( isLocalEvaluationAllowed: true, variant: Variant.standard, diff --git a/test/view/analysis/analysis_screen_test.dart b/test/view/analysis/analysis_screen_test.dart index 45cc239ca9..c59047353a 100644 --- a/test/view/analysis/analysis_screen_test.dart +++ b/test/view/analysis/analysis_screen_test.dart @@ -32,7 +32,7 @@ void main() { final app = await buildTestApp( tester, home: AnalysisScreen( - pgnOrId: sanMoves, + pgn: sanMoves, options: AnalysisOptions( isLocalEvaluationAllowed: false, variant: Variant.standard, @@ -59,7 +59,7 @@ void main() { final app = await buildTestApp( tester, home: AnalysisScreen( - pgnOrId: sanMoves, + pgn: sanMoves, options: AnalysisOptions( isLocalEvaluationAllowed: false, variant: Variant.standard, From b292db35ef6224db5381adb5aa128b2f5480759a Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Sun, 25 Aug 2024 17:54:03 +0200 Subject: [PATCH 173/979] use adaptive circular progress indicator for broadcast list screen --- lib/src/view/broadcast/broadcasts_list_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/view/broadcast/broadcasts_list_screen.dart b/lib/src/view/broadcast/broadcasts_list_screen.dart index ec217d1f60..50406653ae 100644 --- a/lib/src/view/broadcast/broadcasts_list_screen.dart +++ b/lib/src/view/broadcast/broadcasts_list_screen.dart @@ -92,7 +92,7 @@ class _BodyState extends ConsumerState<_Body> { if (!broadcasts.hasValue && broadcasts.isLoading) { return const Center( - child: CircularProgressIndicator(), + child: CircularProgressIndicator.adaptive(), ); } From acf391f6224729fecff2686349fec4f61d350554 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Tue, 27 Aug 2024 00:15:13 +0200 Subject: [PATCH 174/979] tweak start round date on broadcast cards --- .../broadcast/broadcasts_list_screen.dart | 214 ++++++++++-------- 1 file changed, 115 insertions(+), 99 deletions(-) diff --git a/lib/src/view/broadcast/broadcasts_list_screen.dart b/lib/src/view/broadcast/broadcasts_list_screen.dart index 50406653ae..ce5c6a0df8 100644 --- a/lib/src/view/broadcast/broadcasts_list_screen.dart +++ b/lib/src/view/broadcast/broadcasts_list_screen.dart @@ -186,7 +186,7 @@ class _BodyState extends ConsumerState<_Body> { } } -class BroadcastGridItem extends ConsumerWidget { +class BroadcastGridItem extends StatelessWidget { final Broadcast broadcast; const BroadcastGridItem({required this.broadcast}); @@ -219,110 +219,126 @@ class BroadcastGridItem extends ConsumerWidget { ); @override - Widget build(BuildContext context, WidgetRef ref) { - final dateFormatter = - ref.withLocale((locale) => DateFormat.yMMMd(locale).add_Hm()); - - return AdaptiveInkWell( - borderRadius: BorderRadius.circular(20), - onTap: () { - pushPlatformRoute( - context, - title: context.l10n.broadcastBroadcasts, - rootNavigator: true, - builder: (context) => BroadcastScreen(broadcast: broadcast), - ); - }, - child: Container( - clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - boxShadow: [ - BoxShadow( - color: LichessColors.grey.withOpacity(0.5), - blurRadius: 5, - spreadRadius: 1, - ), - ], - ), - foregroundDecoration: BoxDecoration( - border: (broadcast.isLive) - ? Border.all(color: LichessColors.red, width: 2) - : Border.all(color: LichessColors.grey), - borderRadius: BorderRadius.circular(20), - ), - child: Column( - children: [ - if (broadcast.tour.imageUrl != null) - AspectRatio( - aspectRatio: 2.0, - child: FadeInImage.memoryNetwork( - placeholder: transparentImage, - image: broadcast.tour.imageUrl!, - ), - ) - else - const DefaultBroadcastImage(aspectRatio: 2.0), - Expanded( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - if (!broadcast.isFinished) ...[ - Text( - broadcast.round.name, - style: Theme.of(context) - .textTheme - .labelMedium - ?.copyWith( - color: textShade(context, 0.5), - ), - ), - const SizedBox(width: 4.0), - ], - if (broadcast.isLive) - const Text( - 'LIVE', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: Colors.red, + Widget build(BuildContext context) => AdaptiveInkWell( + borderRadius: BorderRadius.circular(20), + onTap: () { + pushPlatformRoute( + context, + title: context.l10n.broadcastBroadcasts, + rootNavigator: true, + builder: (context) => BroadcastScreen(broadcast: broadcast), + ); + }, + child: Container( + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: LichessColors.grey.withOpacity(0.5), + blurRadius: 5, + spreadRadius: 1, + ), + ], + ), + foregroundDecoration: BoxDecoration( + border: (broadcast.isLive) + ? Border.all(color: LichessColors.red, width: 2) + : Border.all(color: LichessColors.grey), + borderRadius: BorderRadius.circular(20), + ), + child: Column( + children: [ + if (broadcast.tour.imageUrl != null) + AspectRatio( + aspectRatio: 2.0, + child: FadeInImage.memoryNetwork( + placeholder: transparentImage, + image: broadcast.tour.imageUrl!, + ), + ) + else + const DefaultBroadcastImage(aspectRatio: 2.0), + Expanded( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + if (!broadcast.isFinished) ...[ + Text( + broadcast.round.name, + style: Theme.of(context) + .textTheme + .labelMedium + ?.copyWith( + color: textShade(context, 0.5), + ), ), - ) - else if (broadcast.round.startsAt != null) - Text( - dateFormatter.format(broadcast.round.startsAt!), - style: Theme.of(context) - .textTheme - .labelSmall - ?.copyWith( - color: textShade(context, 0.5), - ), - ), - ], - ), - const SizedBox(height: 4.0), - Flexible( - child: Text( - broadcast.title, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.labelLarge?.copyWith( - fontWeight: FontWeight.bold, + const SizedBox(width: 4.0), + ], + if (broadcast.isLive) + const Text( + 'LIVE', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: Colors.red, + ), + ) + else if (broadcast.round.startsAt != null) + StartsRoundDate( + startsAt: broadcast.round.startsAt!, ), + ], + ), + const SizedBox(height: 4.0), + Flexible( + child: Text( + broadcast.title, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: + Theme.of(context).textTheme.labelLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + ), ), - ), - ], + ], + ), ), ), - ), - ], + ], + ), ), - ), + ); +} + +class StartsRoundDate extends ConsumerWidget { + final DateTime startsAt; + + const StartsRoundDate({required this.startsAt}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final dateFormatter = + ref.withLocale((locale) => DateFormat.MMMd(locale).add_Hm()); + final dateFormatterWithYear = + ref.withLocale((locale) => DateFormat.yMMMd(locale).add_Hm()); + final timeBeforeRound = startsAt.difference(DateTime.now()); + + return Text( + timeBeforeRound.inDays == 0 + ? 'In ${timeBeforeRound.inHours} hours' + : timeBeforeRound.inDays < 365 + ? dateFormatter.format(startsAt) + : dateFormatterWithYear.format(startsAt), + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: textShade(context, 0.5), + ), + overflow: TextOverflow.ellipsis, ); } } From 4c69c2cab09b620802a7299e94dece56ac74822b Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Tue, 27 Aug 2024 13:06:43 +0200 Subject: [PATCH 175/979] make status of game nullable --- lib/src/model/broadcast/broadcast.dart | 2 +- lib/src/model/broadcast/broadcast_repository.dart | 2 +- lib/src/view/broadcast/broadcast_boards_tab.dart | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/src/model/broadcast/broadcast.dart b/lib/src/model/broadcast/broadcast.dart index d265619007..72a3702762 100644 --- a/lib/src/model/broadcast/broadcast.dart +++ b/lib/src/model/broadcast/broadcast.dart @@ -93,7 +93,7 @@ class BroadcastGameSnapshot with _$BroadcastGameSnapshot { required IMap players, required String fen, required Move? lastMove, - required String status, + required String? status, /// The amount of time that the player whose turn it is has been thinking since his last move required Duration? thinkTime, diff --git a/lib/src/model/broadcast/broadcast_repository.dart b/lib/src/model/broadcast/broadcast_repository.dart index 410e03914c..9acc777974 100644 --- a/lib/src/model/broadcast/broadcast_repository.dart +++ b/lib/src/model/broadcast/broadcast_repository.dart @@ -162,7 +162,7 @@ MapEntry gameFromPick( fen: pick('fen').asStringOrNull() ?? Variant.standard.initialPosition.fen, lastMove: pick('lastMove').asUciMoveOrNull(), - status: pick('status').asStringOrThrow(), + status: pick('status').asStringOrNull(), thinkTime: pick('thinkTime').asDurationFromSecondsOrNull(), ), ); diff --git a/lib/src/view/broadcast/broadcast_boards_tab.dart b/lib/src/view/broadcast/broadcast_boards_tab.dart index fc71a97ea9..a0f8f24994 100644 --- a/lib/src/view/broadcast/broadcast_boards_tab.dart +++ b/lib/src/view/broadcast/broadcast_boards_tab.dart @@ -160,14 +160,14 @@ class _PlayerWidget extends StatelessWidget { clock: null, federation: null, ), - gameStatus = '*', + gameStatus = null, thinkTime = null, side = Side.white, playingSide = Side.white, _displayShimmerPlaceholder = true; final BroadcastPlayer player; - final String gameStatus; + final String? gameStatus; final Duration? thinkTime; final Side side; final Side playingSide; @@ -240,7 +240,7 @@ class _PlayerWidget extends StatelessWidget { ), ), const SizedBox(width: 5), - if (gameStatus != '*') + if (gameStatus != null && gameStatus != '*') Text( (gameStatus == '½-½') ? '½' From 8ba6d4d6d29fa48e17dd80af69c316799ee9bf23 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Tue, 27 Aug 2024 13:12:19 +0200 Subject: [PATCH 176/979] fix round date starts show on broadcast grid card --- lib/src/view/broadcast/broadcasts_list_screen.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/src/view/broadcast/broadcasts_list_screen.dart b/lib/src/view/broadcast/broadcasts_list_screen.dart index ce5c6a0df8..e4bd272aaf 100644 --- a/lib/src/view/broadcast/broadcasts_list_screen.dart +++ b/lib/src/view/broadcast/broadcasts_list_screen.dart @@ -331,7 +331,9 @@ class StartsRoundDate extends ConsumerWidget { return Text( timeBeforeRound.inDays == 0 - ? 'In ${timeBeforeRound.inHours} hours' + ? timeBeforeRound.inHours == 0 + ? 'In ${timeBeforeRound.inMinutes} minutes' // TODO: translate + : 'In ${timeBeforeRound.inHours} hours' // TODO: translate : timeBeforeRound.inDays < 365 ? dateFormatter.format(startsAt) : dateFormatterWithYear.format(startsAt), From 8474de8b9de56678db93a56b5d8fad7022ec4f99 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Wed, 28 Aug 2024 22:25:08 +0200 Subject: [PATCH 177/979] add a link for broadcast in nb of hours and minutes translations --- lib/src/view/broadcast/broadcasts_list_screen.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/view/broadcast/broadcasts_list_screen.dart b/lib/src/view/broadcast/broadcasts_list_screen.dart index e4bd272aaf..cc93cabebd 100644 --- a/lib/src/view/broadcast/broadcasts_list_screen.dart +++ b/lib/src/view/broadcast/broadcasts_list_screen.dart @@ -332,8 +332,8 @@ class StartsRoundDate extends ConsumerWidget { return Text( timeBeforeRound.inDays == 0 ? timeBeforeRound.inHours == 0 - ? 'In ${timeBeforeRound.inMinutes} minutes' // TODO: translate - : 'In ${timeBeforeRound.inHours} hours' // TODO: translate + ? 'In ${timeBeforeRound.inMinutes} minutes' // TODO: translate with https://github.com/lichess-org/lila/blob/65b28ea8e43e0133df6c7ed40e03c2954f247d1e/translation/source/timeago.xml#L8 + : 'In ${timeBeforeRound.inHours} hours' // TODO: translate with https://github.com/lichess-org/lila/blob/65b28ea8e43e0133df6c7ed40e03c2954f247d1e/translation/source/timeago.xml#L12 : timeBeforeRound.inDays < 365 ? dateFormatter.format(startsAt) : dateFormatterWithYear.format(startsAt), From 1913546472b76ac81838e90fb938c9ca426200e8 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Thu, 29 Aug 2024 23:22:33 +0200 Subject: [PATCH 178/979] handle case where tournament description is null --- lib/src/model/broadcast/broadcast.dart | 2 +- .../model/broadcast/broadcast_repository.dart | 2 +- .../view/broadcast/broadcast_overview_tab.dart | 17 +++++++++-------- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/lib/src/model/broadcast/broadcast.dart b/lib/src/model/broadcast/broadcast.dart index 72a3702762..c24629bcd6 100644 --- a/lib/src/model/broadcast/broadcast.dart +++ b/lib/src/model/broadcast/broadcast.dart @@ -48,7 +48,7 @@ class BroadcastTournamentData with _$BroadcastTournamentData { required BroadcastTournamentId id, required String name, required String? imageUrl, - required String description, + required String? description, required BroadcastTournamentInformation information, }) = _BroadcastTournamentData; } diff --git a/lib/src/model/broadcast/broadcast_repository.dart b/lib/src/model/broadcast/broadcast_repository.dart index 9acc777974..8ab68b7a1d 100644 --- a/lib/src/model/broadcast/broadcast_repository.dart +++ b/lib/src/model/broadcast/broadcast_repository.dart @@ -89,7 +89,7 @@ BroadcastTournamentData _tournamentDataFromPick( id: pick('id').asBroadcastTournamentIdOrThrow(), name: pick('name').asStringOrThrow(), imageUrl: pick('image').asStringOrNull(), - description: pick('description').asStringOrThrow(), + description: pick('description').asStringOrNull(), information: ( format: pick('info', 'format').asStringOrNull(), timeControl: pick('info', 'tc').asStringOrNull(), diff --git a/lib/src/view/broadcast/broadcast_overview_tab.dart b/lib/src/view/broadcast/broadcast_overview_tab.dart index 458be81ef8..e81aa1d0c6 100644 --- a/lib/src/view/broadcast/broadcast_overview_tab.dart +++ b/lib/src/view/broadcast/broadcast_overview_tab.dart @@ -57,15 +57,16 @@ class BroadcastOverviewTab extends ConsumerWidget { ), ], ), - Expanded( - child: Markdown( - data: description, - onTapLink: (text, url, title) { - if (url == null) return; - launchUrl(Uri.parse(url)); - }, + if (description != null) + Expanded( + child: Markdown( + data: description, + onTapLink: (text, url, title) { + if (url == null) return; + launchUrl(Uri.parse(url)); + }, + ), ), - ), ], ); }, From 92e4da1bd3ac54e8683d32ece85c3b2570be0156 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Thu, 29 Aug 2024 23:44:10 +0200 Subject: [PATCH 179/979] use Cupertino style for the broadcast screen --- .../view/broadcast/broadcast_boards_tab.dart | 1 + .../broadcast/broadcast_overview_tab.dart | 1 + lib/src/view/broadcast/broadcast_screen.dart | 276 ++++++++++++++---- 3 files changed, 223 insertions(+), 55 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_boards_tab.dart b/lib/src/view/broadcast/broadcast_boards_tab.dart index a0f8f24994..861cab8325 100644 --- a/lib/src/view/broadcast/broadcast_boards_tab.dart +++ b/lib/src/view/broadcast/broadcast_boards_tab.dart @@ -36,6 +36,7 @@ class BroadcastBoardsTab extends ConsumerWidget { final games = ref.watch(broadcastRoundControllerProvider(roundId)); return SafeArea( + bottom: false, child: games.when( data: (games) => (games.isEmpty) ? const Padding( diff --git a/lib/src/view/broadcast/broadcast_overview_tab.dart b/lib/src/view/broadcast/broadcast_overview_tab.dart index e81aa1d0c6..86cda01986 100644 --- a/lib/src/view/broadcast/broadcast_overview_tab.dart +++ b/lib/src/view/broadcast/broadcast_overview_tab.dart @@ -21,6 +21,7 @@ class BroadcastOverviewTab extends ConsumerWidget { final dateFormatter = ref.withLocale((locale) => DateFormat.MMMd(locale)); return SafeArea( + bottom: false, child: Padding( padding: Styles.bodyPadding, child: tournament.when( diff --git a/lib/src/view/broadcast/broadcast_screen.dart b/lib/src/view/broadcast/broadcast_screen.dart index ed7f42e0fa..54bb200c3f 100644 --- a/lib/src/view/broadcast/broadcast_screen.dart +++ b/lib/src/view/broadcast/broadcast_screen.dart @@ -1,12 +1,15 @@ +import 'dart:ui'; + import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_providers.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_boards_tab.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_overview_tab.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_choice_picker.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; class BroadcastScreen extends StatelessWidget { @@ -19,28 +22,23 @@ class BroadcastScreen extends StatelessWidget { return PlatformWidget(androidBuilder: _buildAndroid, iosBuilder: _buildIos); } - Widget _buildIos(BuildContext context) { - return CupertinoPageScaffold( - navigationBar: const CupertinoNavigationBar(), - child: _CupertinoBody(broadcast: broadcast), - ); - } + Widget _buildAndroid(BuildContext context) => + _AndroidScreen(broadcast: broadcast); - Widget _buildAndroid(BuildContext context) { - return _AndroidBody(broadcast: broadcast); - } + Widget _buildIos(BuildContext context) => + _CupertinoScreen(broadcast: broadcast); } -class _AndroidBody extends StatefulWidget { +class _AndroidScreen extends StatefulWidget { final Broadcast broadcast; - const _AndroidBody({required this.broadcast}); + const _AndroidScreen({required this.broadcast}); @override - State<_AndroidBody> createState() => _AndroidBodyState(); + State<_AndroidScreen> createState() => _AndroidScreenState(); } -class _AndroidBodyState extends State<_AndroidBody> +class _AndroidScreenState extends State<_AndroidScreen> with SingleTickerProviderStateMixin { late final TabController _tabController; late BroadcastTournamentId _selectedTournamentId; @@ -93,7 +91,7 @@ class _AndroidBodyState extends State<_AndroidBody> ], ), bottomNavigationBar: BottomAppBar( - child: TournamentAndRoundDropdowns( + child: _AndroidTournamentAndRoundSelector( tournamentId: _selectedTournamentId, setTournamentId: setTournamentId, setRoundId: setRoundId, @@ -103,18 +101,18 @@ class _AndroidBodyState extends State<_AndroidBody> } } -class _CupertinoBody extends StatefulWidget { +class _CupertinoScreen extends StatefulWidget { final Broadcast broadcast; - const _CupertinoBody({required this.broadcast}); + const _CupertinoScreen({required this.broadcast}); @override - _CupertinoBodyState createState() => _CupertinoBodyState(); + _CupertinoScreenState createState() => _CupertinoScreenState(); } enum _ViewMode { overview, boards } -class _CupertinoBodyState extends State<_CupertinoBody> { +class _CupertinoScreenState extends State<_CupertinoScreen> { _ViewMode _selectedSegment = _ViewMode.boards; late BroadcastTournamentId _selectedTournamentId; late BroadcastRoundId _selectedRoundId; @@ -146,52 +144,48 @@ class _CupertinoBodyState extends State<_CupertinoBody> { @override Widget build(BuildContext context) { - return SafeArea( - child: Padding( - padding: Styles.bodyPadding, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - CupertinoSlidingSegmentedControl<_ViewMode>( - groupValue: _selectedSegment, - children: const { - _ViewMode.overview: Text('Overview'), - _ViewMode.boards: Text('Boards'), - }, - onValueChanged: (_ViewMode? view) { - if (view != null) { - setState(() { - _selectedSegment = view; - }); - } - }, - ), - const SizedBox(height: 16), - TournamentAndRoundDropdowns( - tournamentId: _selectedTournamentId, - setTournamentId: setTournamentId, - setRoundId: setRoundId, - ), - const SizedBox(height: 16), - Expanded( - child: _selectedSegment == _ViewMode.overview - ? BroadcastOverviewTab(tournamentId: _selectedTournamentId) - : BroadcastBoardsTab(roundId: _selectedRoundId), - ), - ], + return CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + middle: CupertinoSlidingSegmentedControl<_ViewMode>( + groupValue: _selectedSegment, + children: const { + _ViewMode.overview: Text('Overview'), + _ViewMode.boards: Text('Boards'), + }, + onValueChanged: (_ViewMode? view) { + if (view != null) { + setState(() { + _selectedSegment = view; + }); + } + }, ), ), + child: Column( + children: [ + Expanded( + child: _selectedSegment == _ViewMode.overview + ? BroadcastOverviewTab(tournamentId: _selectedTournamentId) + : BroadcastBoardsTab(roundId: _selectedRoundId), + ), + _IOSTournamentAndRoundSelector( + tournamentId: _selectedTournamentId, + roundId: _selectedRoundId, + setTournamentId: setTournamentId, + setRoundId: setRoundId, + ), + ], + ), ); } } -class TournamentAndRoundDropdowns extends ConsumerWidget { +class _AndroidTournamentAndRoundSelector extends ConsumerWidget { final BroadcastTournamentId tournamentId; final void Function(BroadcastTournamentId) setTournamentId; final void Function(BroadcastRoundId) setRoundId; - const TournamentAndRoundDropdowns({ - super.key, + const _AndroidTournamentAndRoundSelector({ required this.tournamentId, required this.setTournamentId, required this.setRoundId, @@ -255,3 +249,175 @@ class TournamentAndRoundDropdowns extends ConsumerWidget { ); } } + +const Color _kDefaultToolBarBorderColor = Color(0x4D000000); + +const Border _kDefaultToolBarBorder = Border( + top: BorderSide( + color: _kDefaultToolBarBorderColor, + width: 0.0, // 0.0 means one physical pixel + ), +); + +// Code taken from the Cupertino navigation bar widget +Widget _wrapWithBackground({ + Border? border, + required Color backgroundColor, + Brightness? brightness, + required Widget child, + bool updateSystemUiOverlay = true, +}) { + Widget result = child; + if (updateSystemUiOverlay) { + final bool isDark = backgroundColor.computeLuminance() < 0.179; + final Brightness newBrightness = + brightness ?? (isDark ? Brightness.dark : Brightness.light); + final SystemUiOverlayStyle overlayStyle = switch (newBrightness) { + Brightness.dark => SystemUiOverlayStyle.light, + Brightness.light => SystemUiOverlayStyle.dark, + }; + result = AnnotatedRegion( + value: SystemUiOverlayStyle( + statusBarColor: overlayStyle.statusBarColor, + statusBarBrightness: overlayStyle.statusBarBrightness, + statusBarIconBrightness: overlayStyle.statusBarIconBrightness, + systemStatusBarContrastEnforced: + overlayStyle.systemStatusBarContrastEnforced, + ), + child: result, + ); + } + final DecoratedBox childWithBackground = DecoratedBox( + decoration: BoxDecoration( + border: border, + color: backgroundColor, + ), + child: result, + ); + + if (backgroundColor.alpha == 0xFF) { + return childWithBackground; + } + + return ClipRect( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0), + child: childWithBackground, + ), + ); +} + +class _IOSTournamentAndRoundSelector extends ConsumerWidget { + final BroadcastTournamentId tournamentId; + final BroadcastRoundId roundId; + final void Function(BroadcastTournamentId) setTournamentId; + final void Function(BroadcastRoundId) setRoundId; + + const _IOSTournamentAndRoundSelector({ + required this.tournamentId, + required this.roundId, + required this.setTournamentId, + required this.setRoundId, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final backgroundColor = CupertinoTheme.of(context).barBackgroundColor; + final tournament = ref.watch(broadcastTournamentProvider(tournamentId)); + + return tournament.when( + data: (tournament) { + /// It should be replaced with a Flutter toolbar widget once it is implemented. + /// See https://github.com/flutter/flutter/issues/134454 + + return _wrapWithBackground( + backgroundColor: backgroundColor, + border: _kDefaultToolBarBorder, + child: SafeArea( + top: false, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Row( + spacing: 16.0, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + if (tournament.group != null) + Flexible( + child: CupertinoButton.tinted( + child: Text( + tournament.group! + .firstWhere( + (tournament) => tournament.id == tournamentId, + ) + .name, + overflow: TextOverflow.ellipsis, + ), + onPressed: () { + showChoicePicker( + context, + choices: tournament.group! + .map((tournament) => tournament.id) + .toList(), + labelBuilder: (tournamentId) => Text( + tournament.group! + .firstWhere( + (tournament) => + tournament.id == tournamentId, + ) + .name, + ), + selectedItem: tournamentId, + onSelectedItemChanged: (tournamentId) async { + setTournamentId(tournamentId); + final newTournament = await ref.read( + broadcastTournamentProvider(tournamentId) + .future, + ); + setRoundId(newTournament.defaultRoundId); + }, + ); + }, + ), + ), + Flexible( + child: CupertinoButton.tinted( + child: Text( + tournament.rounds + .firstWhere( + (round) => round.id == roundId, + ) + .name, + overflow: TextOverflow.ellipsis, + ), + onPressed: () { + showChoicePicker( + context, + choices: tournament.rounds + .map( + (round) => round.id, + ) + .toList(), + labelBuilder: (roundId) => Text( + tournament.rounds + .firstWhere((round) => round.id == roundId) + .name, + ), + selectedItem: roundId, + onSelectedItemChanged: (roundId) { + setRoundId(roundId); + }, + ); + }, + ), + ), + ], + ), + ), + ), + ); + }, + loading: () => const SizedBox.shrink(), + error: (error, stackTrace) => Center(child: Text(error.toString())), + ); + } +} From 92adc76cc032ea6b30a2663c152ad9bd12869be7 Mon Sep 17 00:00:00 2001 From: Noah Date: Fri, 30 Aug 2024 22:32:37 +0200 Subject: [PATCH 180/979] enpassant in the editor --- .../board_editor/board_editor_controller.dart | 53 +++++++++++++++++++ .../view/board_editor/board_editor_menu.dart | 23 ++++++++ .../board_editor_screen_test.dart | 38 +++++++++++++ 3 files changed, 114 insertions(+) diff --git a/lib/src/model/board_editor/board_editor_controller.dart b/lib/src/model/board_editor/board_editor_controller.dart index 98218a21d6..5281de23c3 100644 --- a/lib/src/model/board_editor/board_editor_controller.dart +++ b/lib/src/model/board_editor/board_editor_controller.dart @@ -17,6 +17,7 @@ class BoardEditorController extends _$BoardEditorController { pieces: readFen(initialFen ?? kInitialFEN).lock, unmovedRooks: SquareSet.corners, editorPointerMode: EditorPointerMode.drag, + enPassantSquare: null, pieceToAddOnEdit: null, ); } @@ -65,6 +66,56 @@ class BoardEditorController extends _$BoardEditorController { _updatePosition(readFen(fen).lock); } + /// Calculates the squares where an en passant capture could be possible. + SquareSet calculateEnPassantOptions() { + final side = state.sideToPlay; + final pieces = state.pieces; + SquareSet enPassantSquares = SquareSet.empty; + final boardFen = writeFen(pieces.unlock); + final board = Board.parseFen(boardFen); + + /// For en passant to be possible, there needs to be an adjacent pawn which has moved two squares forward. + /// So the two squares behind must be empty + void checkEnPassant(Square square, int fileOffset) { + final adjacentSquare = + Square.fromCoords(square.file.offset(fileOffset)!, square.rank); + final targetSquare = Square.fromCoords( + square.file.offset(fileOffset)!, + square.rank.offset(side == Side.white ? 1 : -1)!, + ); + final originSquare = Square.fromCoords( + square.file.offset(fileOffset)!, + square.rank.offset(side == Side.white ? 2 : -2)!, + ); + + if (board.sideAt(adjacentSquare) == side.opposite && + board.roleAt(adjacentSquare) == Role.pawn && + board.sideAt(targetSquare) == null && + board.sideAt(originSquare) == null) { + enPassantSquares = + enPassantSquares.union(SquareSet.fromSquare(targetSquare)); + } + } + + pieces.forEach((square, piece) { + if (piece.color == side && piece.role == Role.pawn) { + if ((side == Side.white && square.rank == Rank.fifth) || + (side == Side.black && square.rank == Rank.fourth)) { + if (square.file != File.a) checkEnPassant(square, -1); + if (square.file != File.h) checkEnPassant(square, 1); + } + } + }); + + return enPassantSquares; + } + + void toggleEnPassantSquare(Square square) { + state = state.copyWith( + enPassantSquare: state.enPassantSquare == square ? null : square, + ); + } + void _updatePosition(IMap pieces) { state = state.copyWith(pieces: pieces); } @@ -105,6 +156,7 @@ class BoardEditorState with _$BoardEditorState { required IMap pieces, required SquareSet unmovedRooks, required EditorPointerMode editorPointerMode, + required Square? enPassantSquare, /// When null, clears squares when in edit mode. Has no effect in drag mode. required Piece? pieceToAddOnEdit, @@ -129,6 +181,7 @@ class BoardEditorState with _$BoardEditorState { board: board, unmovedRooks: unmovedRooks, turn: sideToPlay == Side.white ? Side.white : Side.black, + epSquare: enPassantSquare, halfmoves: 0, fullmoves: 1, ); diff --git a/lib/src/view/board_editor/board_editor_menu.dart b/lib/src/view/board_editor/board_editor_menu.dart index c523dabb55..72649585d2 100644 --- a/lib/src/view/board_editor/board_editor_menu.dart +++ b/lib/src/view/board_editor/board_editor_menu.dart @@ -15,6 +15,9 @@ class BoardEditorMenu extends ConsumerWidget { final editorController = boardEditorControllerProvider(initialFen); final editorState = ref.watch(editorController); + final enPassantSquares = + ref.read(editorController.notifier).calculateEnPassantOptions(); + return SafeArea( child: Padding( padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 8.0), @@ -80,6 +83,26 @@ class BoardEditorMenu extends ConsumerWidget { ), ); }), + if (enPassantSquares.isNotEmpty) ...[ + Padding( + padding: Styles.bodySectionPadding, + child: const Text('En passant', style: Styles.subtitle), + ), + Wrap( + spacing: 8.0, + children: enPassantSquares.squares.map((square) { + return ChoiceChip( + label: Text(square.name), + selected: editorState.enPassantSquare == square, + onSelected: (selected) { + ref + .read(editorController.notifier) + .toggleEnPassantSquare(square); + }, + ); + }).toList(), + ), + ], ], ), ), diff --git a/test/view/board_editor/board_editor_screen_test.dart b/test/view/board_editor/board_editor_screen_test.dart index 89410b2285..e8e8966510 100644 --- a/test/view/board_editor/board_editor_screen_test.dart +++ b/test/view/board_editor/board_editor_screen_test.dart @@ -143,6 +143,44 @@ void main() { ); }); + testWidgets('Possible en passant squares are calculated correctly', + (tester) async { + final app = await buildTestApp( + tester, + home: const BoardEditorScreen(), + ); + await tester.pumpWidget(app); + + final container = ProviderScope.containerOf( + tester.element(find.byType(ChessboardEditor)), + ); + final controllerProvider = boardEditorControllerProvider(null); + container + .read(controllerProvider.notifier) + .loadFen('1nbqkbn1/pppppppp/8/8/8/8/PPPPPPPP/1NBQKBN1'); + + expect( + container.read(controllerProvider.notifier).calculateEnPassantOptions(), + SquareSet.empty, + ); + + container.read(controllerProvider.notifier).loadFen( + 'r1bqkbnr/4p1p1/3n4/pPppPppP/8/8/P1PP1P2/RNBQKBNR w KQkq - 0 1', + ); + expect( + container.read(controllerProvider.notifier).calculateEnPassantOptions(), + SquareSet.fromSquares([Square.a6, Square.c6, Square.f6]), + ); + container.read(controllerProvider.notifier).loadFen( + 'rnbqkbnr/pp1p1p1p/8/8/PpPpPQpP/8/NPRP1PP1/2B1KBNR b Kkq - 0 1', + ); + container.read(controllerProvider.notifier).setSideToPlay(Side.black); + expect( + container.read(controllerProvider.notifier).calculateEnPassantOptions(), + SquareSet.fromSquares([Square.e3, Square.h3]), + ); + }); + testWidgets('Can drag pieces to new squares', (tester) async { final app = await buildTestApp( tester, From af054821ff004ad7378336a99cc9e67def738f0b Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Sat, 31 Aug 2024 12:27:41 +0200 Subject: [PATCH 181/979] add eval bar to board --- .../broadcast/broadcast_preferences.dart | 50 ++++++++++++++++ .../view/broadcast/broadcast_boards_tab.dart | 29 +++++++--- lib/src/view/broadcast/broadcast_screen.dart | 58 +++++++++++++++++++ lib/src/widgets/board_thumbnail.dart | 55 ++++++++++++++++-- lib/src/widgets/evaluation_bar.dart | 49 ++++++++++++++++ 5 files changed, 228 insertions(+), 13 deletions(-) create mode 100644 lib/src/model/broadcast/broadcast_preferences.dart create mode 100644 lib/src/widgets/evaluation_bar.dart diff --git a/lib/src/model/broadcast/broadcast_preferences.dart b/lib/src/model/broadcast/broadcast_preferences.dart new file mode 100644 index 0000000000..624e26261e --- /dev/null +++ b/lib/src/model/broadcast/broadcast_preferences.dart @@ -0,0 +1,50 @@ +import 'dart:convert'; + +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:lichess_mobile/src/db/shared_preferences.dart'; +import 'package:lichess_mobile/src/model/auth/auth_session.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'broadcast_preferences.freezed.dart'; +part 'broadcast_preferences.g.dart'; + +const _prefKey = 'broadcast.preferences'; + +@riverpod +class BroadcastPreferences extends _$BroadcastPreferences { + @override + BroadcastPrefState build() { + final id = ref.watch(authSessionProvider)?.user.id; + final prefs = ref.watch(sharedPreferencesProvider); + final stored = prefs.getString(_makeKey(id)); + return stored != null + ? BroadcastPrefState.fromJson( + jsonDecode(stored) as Map, + ) + : BroadcastPrefState.defaults(); + } + + Future toggleEvaluationBar() async { + final id = ref.read(authSessionProvider)?.user.id; + final prefs = ref.read(sharedPreferencesProvider); + state = state.copyWith(showEvaluationBar: !state.showEvaluationBar); + await prefs.setString(_makeKey(id), jsonEncode(state.toJson())); + } + + String _makeKey(UserId? id) => '$_prefKey.${id ?? ''}'; +} + +@Freezed(fromJson: true, toJson: true) +class BroadcastPrefState with _$BroadcastPrefState { + const factory BroadcastPrefState({ + required bool showEvaluationBar, + }) = _BroadcastPrefState; + + factory BroadcastPrefState.defaults() => const BroadcastPrefState( + showEvaluationBar: true, + ); + + factory BroadcastPrefState.fromJson(Map json) => + _$BroadcastPrefStateFromJson(json); +} diff --git a/lib/src/view/broadcast/broadcast_boards_tab.dart b/lib/src/view/broadcast/broadcast_boards_tab.dart index 861cab8325..8bf9ca78bd 100644 --- a/lib/src/view/broadcast/broadcast_boards_tab.dart +++ b/lib/src/view/broadcast/broadcast_boards_tab.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast_preferences.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_round_controller.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; @@ -17,6 +18,7 @@ import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_boards_tab_provider.dart'; import 'package:lichess_mobile/src/widgets/board_thumbnail.dart'; +import 'package:lichess_mobile/src/widgets/evaluation_bar.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; // height of 1.0 is important because we need to determine the height of the text @@ -69,6 +71,9 @@ class BroadcastPreview extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final showEvaluationBar = ref.watch( + broadcastPreferencesProvider.select((value) => value.showEvaluationBar), + ); const numberLoadingBoards = 12; const boardSpacing = 10.0; // height of the text based on the font size @@ -78,7 +83,7 @@ class BroadcastPreview extends ConsumerWidget { final headerAndFooterHeight = textHeight + _kPlayerWidgetPadding.vertical; final numberOfBoardsByRow = isTabletOrLarger(context) ? 4 : 2; final screenWidth = MediaQuery.sizeOf(context).width; - final boardWidth = (screenWidth - + final boardWithMaybeEvalBarWidth = (screenWidth - Styles.horizontalBodyPadding.horizontal - (numberOfBoardsByRow - 1) * boardSpacing) / numberOfBoardsByRow; @@ -90,14 +95,21 @@ class BroadcastPreview extends ConsumerWidget { crossAxisCount: numberOfBoardsByRow, crossAxisSpacing: boardSpacing, mainAxisSpacing: boardSpacing, - mainAxisExtent: boardWidth + 2 * headerAndFooterHeight, + mainAxisExtent: boardWithMaybeEvalBarWidth + 2 * headerAndFooterHeight, + childAspectRatio: 1 + evaluationBarAspectRatio, ), itemBuilder: (context, index) { + final boardSize = boardWithMaybeEvalBarWidth - + (showEvaluationBar + ? evaluationBarAspectRatio * boardWithMaybeEvalBarWidth + : 0); + if (games == null) { return BoardThumbnail.loading( - size: boardWidth, - header: _PlayerWidget.loading(width: boardWidth), - footer: _PlayerWidget.loading(width: boardWidth), + size: boardSize, + header: _PlayerWidget.loading(width: boardWithMaybeEvalBarWidth), + footer: _PlayerWidget.loading(width: boardWithMaybeEvalBarWidth), + showEvaluationBar: showEvaluationBar, ); } @@ -118,10 +130,11 @@ class BroadcastPreview extends ConsumerWidget { }, orientation: Side.white, fen: game.fen, + showEvaluationBar: showEvaluationBar, lastMove: game.lastMove, - size: boardWidth, + size: boardSize, header: _PlayerWidget( - width: boardWidth, + width: boardWithMaybeEvalBarWidth, player: game.players[Side.black]!, gameStatus: game.status, thinkTime: game.thinkTime, @@ -129,7 +142,7 @@ class BroadcastPreview extends ConsumerWidget { playingSide: playingSide, ), footer: _PlayerWidget( - width: boardWidth, + width: boardWithMaybeEvalBarWidth, player: game.players[Side.white]!, gameStatus: game.status, thinkTime: game.thinkTime, diff --git a/lib/src/view/broadcast/broadcast_screen.dart b/lib/src/view/broadcast/broadcast_screen.dart index 54bb200c3f..a3fcb102e4 100644 --- a/lib/src/view/broadcast/broadcast_screen.dart +++ b/lib/src/view/broadcast/broadcast_screen.dart @@ -5,12 +5,19 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast_preferences.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_providers.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_boards_tab.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_overview_tab.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/adaptive_choice_picker.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/settings.dart'; class BroadcastScreen extends StatelessWidget { final Broadcast broadcast; @@ -82,6 +89,7 @@ class _AndroidScreenState extends State<_AndroidScreen> Tab(text: 'Boards'), ], ), + actions: [_BroadcastSettingsButton()], ), body: TabBarView( controller: _tabController, @@ -160,6 +168,7 @@ class _CupertinoScreenState extends State<_CupertinoScreen> { } }, ), + trailing: _BroadcastSettingsButton(), ), child: Column( children: [ @@ -421,3 +430,52 @@ class _IOSTournamentAndRoundSelector extends ConsumerWidget { ); } } + +class _BroadcastSettingsButton extends StatelessWidget { + @override + Widget build(BuildContext context) => AppBarIconButton( + icon: const Icon(Icons.settings), + onPressed: () => showAdaptiveBottomSheet( + context: context, + isDismissible: true, + isScrollControlled: true, + showDragHandle: true, + builder: (_) => const _BroadcastSettingsBottomSheet(), + ), + semanticsLabel: context.l10n.settingsSettings, + ); +} + +class _BroadcastSettingsBottomSheet extends ConsumerWidget { + const _BroadcastSettingsBottomSheet(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final broadcastPreferences = ref.watch(broadcastPreferencesProvider); + + return DraggableScrollableSheet( + initialChildSize: .6, + expand: false, + builder: (context, scrollController) => ListView( + controller: scrollController, + children: [ + PlatformListTile( + title: + Text(context.l10n.settingsSettings, style: Styles.sectionTitle), + subtitle: const SizedBox.shrink(), + ), + const SizedBox(height: 8.0), + SwitchSettingTile( + title: const Text('Evaluation bar'), + value: broadcastPreferences.showEvaluationBar, + onChanged: (value) { + ref + .read(broadcastPreferencesProvider.notifier) + .toggleEvaluationBar(); + }, + ), + ], + ), + ); + } +} diff --git a/lib/src/widgets/board_thumbnail.dart b/lib/src/widgets/board_thumbnail.dart index 6bf77234bd..6717fc5988 100644 --- a/lib/src/widgets/board_thumbnail.dart +++ b/lib/src/widgets/board_thumbnail.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; +import 'package:lichess_mobile/src/widgets/evaluation_bar.dart'; /// A board thumbnail widget class BoardThumbnail extends ConsumerStatefulWidget { @@ -11,6 +12,8 @@ class BoardThumbnail extends ConsumerStatefulWidget { required this.size, required this.orientation, required this.fen, + this.showEvaluationBar = false, + this.whiteWinningChances, this.header, this.footer, this.lastMove, @@ -21,7 +24,9 @@ class BoardThumbnail extends ConsumerStatefulWidget { required this.size, this.header, this.footer, - }) : orientation = Side.white, + this.showEvaluationBar = false, + }) : whiteWinningChances = null, + orientation = Side.white, fen = kInitialFEN, lastMove = null, onTap = null; @@ -35,6 +40,12 @@ class BoardThumbnail extends ConsumerStatefulWidget { /// FEN string describing the position of the board. final String fen; + /// Whether the evaluation bar should be shown. + final bool showEvaluationBar; + + /// Winning chances from the white pov for the given fen. + final double? whiteWinningChances; + /// Last move played, used to highlight corresponding squares. final Move? lastMove; @@ -77,8 +88,13 @@ class _BoardThumbnailState extends ConsumerState { ), settings: ChessboardSettings( enableCoordinates: false, - borderRadius: const BorderRadius.all(Radius.circular(4.0)), - boxShadow: boardShadows, + borderRadius: (widget.showEvaluationBar) + ? const BorderRadius.only( + topLeft: Radius.circular(4.0), + bottomLeft: Radius.circular(4.0), + ) + : const BorderRadius.all(Radius.circular(4.0)), + boxShadow: (widget.showEvaluationBar) ? [] : boardShadows, animationDuration: const Duration(milliseconds: 150), pieceAssets: boardPrefs.pieceSet.assets, colorScheme: boardPrefs.boardTheme.colors, @@ -99,15 +115,44 @@ class _BoardThumbnailState extends ConsumerState { ) : board; + final boardWithMaybeEvalBar = widget.showEvaluationBar + ? DecoratedBox( + decoration: BoxDecoration(boxShadow: boardShadows), + child: Row( + children: [ + Expanded(child: maybeTappableBoard), + ClipRRect( + borderRadius: const BorderRadius.only( + topRight: Radius.circular(4.0), + bottomRight: Radius.circular(4.0), + ), + clipBehavior: Clip.hardEdge, + child: (widget.whiteWinningChances != null) + ? EvaluationBar( + height: widget.size, + whiteWinnigChances: widget.whiteWinningChances!, + ) + : SizedBox( + height: widget.size, + width: widget.size * evaluationBarAspectRatio, + child: + ColoredBox(color: Colors.grey.withOpacity(0.6)), + ), + ), + ], + ), + ) + : maybeTappableBoard; + return widget.header != null || widget.footer != null ? Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (widget.header != null) widget.header!, - maybeTappableBoard, + boardWithMaybeEvalBar, if (widget.footer != null) widget.footer!, ], ) - : maybeTappableBoard; + : boardWithMaybeEvalBar; } } diff --git a/lib/src/widgets/evaluation_bar.dart b/lib/src/widgets/evaluation_bar.dart new file mode 100644 index 0000000000..35b27d3d5d --- /dev/null +++ b/lib/src/widgets/evaluation_bar.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; + +const evaluationBarAspectRatio = 1 / 20; + +class EvaluationBar extends StatelessWidget { + final double height; + final double whiteWinnigChances; + + const EvaluationBar({ + super.key, + required this.height, + required this.whiteWinnigChances, + }); + + const EvaluationBar.loading({ + super.key, + required this.height, + }) : whiteWinnigChances = 0; + + @override + Widget build(BuildContext context) { + final whiteBarHeight = height * (whiteWinnigChances + 1) / 2; + + return Stack( + alignment: Alignment.center, + children: [ + Column( + children: [ + SizedBox( + height: height - whiteBarHeight, + width: height * evaluationBarAspectRatio, + child: ColoredBox(color: Colors.black.withOpacity(0.6)), + ), + SizedBox( + height: whiteBarHeight, + width: height * evaluationBarAspectRatio, + child: ColoredBox(color: Colors.white.withOpacity(0.6)), + ), + ], + ), + SizedBox( + height: height / 100, + width: height * evaluationBarAspectRatio, + child: const ColoredBox(color: Colors.red), + ), + ], + ); + } +} From 03c5f21a024673e8139d119debf3b67dcf7b5772 Mon Sep 17 00:00:00 2001 From: Noah Date: Sat, 31 Aug 2024 15:40:34 +0200 Subject: [PATCH 182/979] improve share pgn --- .../view/analysis/analysis_share_screen.dart | 94 ++++++++++++++++--- 1 file changed, 80 insertions(+), 14 deletions(-) diff --git a/lib/src/view/analysis/analysis_share_screen.dart b/lib/src/view/analysis/analysis_share_screen.dart index caf1b2ede8..d5fa171bab 100644 --- a/lib/src/view/analysis/analysis_share_screen.dart +++ b/lib/src/view/analysis/analysis_share_screen.dart @@ -1,4 +1,5 @@ import 'package:collection/collection.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -50,18 +51,65 @@ const Set _ratingHeaders = { 'BlackRatingDiff', }; -class _EditPgnTagsForm extends ConsumerWidget { +class _EditPgnTagsForm extends ConsumerStatefulWidget { const _EditPgnTagsForm(this.pgn, this.options); final String pgn; final AnalysisOptions options; @override - Widget build(BuildContext context, WidgetRef ref) { - final ctrlProvider = analysisControllerProvider(pgn, options); + _EditPgnTagsFormState createState() => _EditPgnTagsFormState(); +} + +class _EditPgnTagsFormState extends ConsumerState<_EditPgnTagsForm> { + final Map _controllers = {}; + final Map _focusNodes = {}; + + @override + void dispose() { + for (final controller in _controllers.values) { + controller.dispose(); + } + for (final focusNode in _focusNodes.values) { + focusNode.dispose(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final ctrlProvider = analysisControllerProvider(widget.pgn, widget.options); final pgnHeaders = ref.watch(ctrlProvider.select((c) => c.pgnHeaders)); final showRatingAsync = ref.watch(showRatingsPrefProvider); + /// Moves focus to the next field or shows a picker for 'Result'. + void focusAndSelectNextField(int index, IMap pgnHeaders) { + if (index + 1 < pgnHeaders.entries.length) { + final nextEntry = pgnHeaders.entries.elementAt(index + 1); + if (nextEntry.key == 'Result') { + showChoicePicker( + context, + choices: ['1-0', '0-1', '1/2-1/2', '*'], + selectedItem: nextEntry.value, + labelBuilder: (choice) => Text(choice), + onSelectedItemChanged: (choice) { + ref + .read(ctrlProvider.notifier) + .updatePgnHeader(nextEntry.key, choice); + _controllers[nextEntry.key]!.text = choice; + focusAndSelectNextField(index + 1, pgnHeaders); + }, + ); + } else { + _focusNodes[nextEntry.key]!.requestFocus(); + _controllers[nextEntry.key]!.selection = TextSelection( + baseOffset: 0, + extentOffset: _controllers[nextEntry.key]!.text.length, + ); + } + } + } + return showRatingAsync.maybeWhen( data: (showRatings) { return SafeArea( @@ -75,6 +123,20 @@ class _EditPgnTagsForm extends ConsumerWidget { (e) => showRatings || !_ratingHeaders.contains(e.key), ) .mapIndexed((index, e) { + if (!_controllers.containsKey(e.key)) { + _controllers[e.key] = + TextEditingController(text: e.value); + _focusNodes[e.key] = FocusNode(); + _focusNodes[e.key]!.addListener(() { + if (!_focusNodes[e.key]!.hasFocus) { + ref.read(ctrlProvider.notifier).updatePgnHeader( + e.key, + _controllers[e.key]!.text, + ); + } + }); + } + return Padding( padding: Styles.horizontalBodyPadding .add(const EdgeInsets.only(bottom: 8.0)), @@ -93,6 +155,7 @@ class _EditPgnTagsForm extends ConsumerWidget { const SizedBox(width: 8), Expanded( child: AdaptiveTextField( + focusNode: _focusNodes[e.key], cupertinoDecoration: BoxDecoration( color: CupertinoColors.tertiarySystemBackground, border: Border.all( @@ -101,21 +164,18 @@ class _EditPgnTagsForm extends ConsumerWidget { ), borderRadius: BorderRadius.circular(8), ), - controller: TextEditingController.fromValue( - TextEditingValue( - text: e.value, - selection: TextSelection( - baseOffset: 0, - extentOffset: e.value.length, - ), - ), - ), + controller: _controllers[e.key], textInputAction: TextInputAction.next, keyboardType: e.key == 'WhiteElo' || e.key == 'BlackElo' ? TextInputType.number : TextInputType.text, onTap: () { + _controllers[e.key]!.selection = TextSelection( + baseOffset: 0, + extentOffset: + _controllers[e.key]!.text.length, + ); if (e.key == 'Result') { showChoicePicker( context, @@ -126,6 +186,11 @@ class _EditPgnTagsForm extends ConsumerWidget { ref .read(ctrlProvider.notifier) .updatePgnHeader(e.key, choice); + _controllers[e.key]!.text = choice; + focusAndSelectNextField( + index, + pgnHeaders, + ); }, ); } @@ -134,6 +199,7 @@ class _EditPgnTagsForm extends ConsumerWidget { ref .read(ctrlProvider.notifier) .updatePgnHeader(e.key, value); + focusAndSelectNextField(index, pgnHeaders); }, ), ), @@ -154,8 +220,8 @@ class _EditPgnTagsForm extends ConsumerWidget { text: ref .read( analysisControllerProvider( - pgn, - options, + widget.pgn, + widget.options, ).notifier, ) .makeGamePgn(), From 66820f0a444a2673b9f00982d59dd6f51855e7e6 Mon Sep 17 00:00:00 2001 From: Noah Date: Sun, 1 Sep 2024 14:33:24 +0200 Subject: [PATCH 183/979] improve display and sharing of master games --- lib/src/model/game/archived_game.dart | 22 ++++++++++++++++++++-- lib/src/model/game/game.dart | 2 ++ lib/src/view/game/game_player.dart | 12 +++++++----- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/lib/src/model/game/archived_game.dart b/lib/src/model/game/archived_game.dart index cc9ae33892..112da35e8c 100644 --- a/lib/src/model/game/archived_game.dart +++ b/lib/src/model/game/archived_game.dart @@ -259,16 +259,34 @@ ClockData _clockDataFromPick(RequiredPick pick) { } Player _playerFromUserGamePick(RequiredPick pick) { + final originalName = pick('name').asStringOrNull(); return Player( user: pick('user').asLightUserOrNull(), - name: pick('name').asStringOrNull(), - rating: pick('rating').asIntOrNull(), + name: _removeRatingFromName(originalName), + rating: + pick('rating').asIntOrNull() ?? _extractRatingFromName(originalName), ratingDiff: pick('ratingDiff').asIntOrNull(), aiLevel: pick('aiLevel').asIntOrNull(), analysis: pick('analysis').letOrNull(_playerAnalysisFromPick), ); } +int? _extractRatingFromName(String? name) { + if (name == null) return null; + final regex = RegExp(r'\((\d+)\)'); + final match = regex.firstMatch(name); + if (match != null) { + return int.tryParse(match.group(1)!); + } + return null; +} + +String? _removeRatingFromName(String? name) { + if (name == null) return null; + final regex = RegExp(r'\s*\(\d+\)\s*'); + return name.replaceAll(regex, ''); +} + PlayerAnalysis _playerAnalysisFromPick(RequiredPick pick) { return PlayerAnalysis( inaccuracies: pick('inaccuracy').asIntOrThrow(), diff --git a/lib/src/model/game/game.dart b/lib/src/model/game/game.dart index 82f6331006..3221ac9f9c 100644 --- a/lib/src/model/game/game.dart +++ b/lib/src/model/game/game.dart @@ -163,10 +163,12 @@ abstract mixin class BaseGame { 'Site': lichessUri('/$id').toString(), 'Date': _dateFormat.format(meta.createdAt), 'White': white.user?.name ?? + white.name ?? (white.aiLevel != null ? 'Stockfish level ${white.aiLevel}' : 'Anonymous'), 'Black': black.user?.name ?? + black.name ?? (black.aiLevel != null ? 'Stockfish level ${black.aiLevel}' : 'Anonymous'), diff --git a/lib/src/view/game/game_player.dart b/lib/src/view/game/game_player.dart index 9dee3c6ad5..619f583af3 100644 --- a/lib/src/view/game/game_player.dart +++ b/lib/src/view/game/game_player.dart @@ -61,11 +61,13 @@ class GamePlayer extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.start, children: [ - Icon( - player.onGame == true ? Icons.cloud : Icons.cloud_off, - color: player.onGame == true ? LichessColors.green : null, - size: 14, - ), + if (player.user != null) ...[ + Icon( + player.onGame == true ? Icons.cloud : Icons.cloud_off, + color: player.onGame == true ? LichessColors.green : null, + size: 14, + ), + ], const SizedBox(width: 5), if (player.user?.isPatron == true) ...[ Icon( From 0b05ebe998d94572b740beca586573b471b64a2f Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Sat, 31 Aug 2024 18:41:15 +0200 Subject: [PATCH 184/979] add broadcast own game analysis screen with player and clock widget --- lib/src/model/broadcast/broadcast.dart | 11 +- .../broadcast/broadcast_game_controller.dart | 111 -- .../model/broadcast/broadcast_repository.dart | 5 +- .../broadcast/broadcast_round_controller.dart | 83 +- lib/src/view/analysis/analysis_screen.dart | 106 +- .../board_editor/board_editor_screen.dart | 2 +- .../view/broadcast/broadcast_boards_tab.dart | 38 +- .../broadcast_boards_tab_provider.dart | 33 - .../view/broadcast/broadcast_game_screen.dart | 1509 +++++++++++++++++ lib/src/view/broadcast/broadcast_screen.dart | 10 +- .../offline_correspondence_game_screen.dart | 2 +- lib/src/view/game/archived_game_screen.dart | 2 +- lib/src/view/game/game_body.dart | 4 +- lib/src/view/game/game_list_tile.dart | 20 +- .../view/game/game_list_tile_providers.dart | 34 - lib/src/view/game/game_result_dialog.dart | 2 +- .../view/puzzle/puzzle_history_screen.dart | 6 +- lib/src/view/puzzle/puzzle_screen.dart | 2 +- lib/src/view/puzzle/streak_screen.dart | 2 +- lib/src/view/tools/load_position_screen.dart | 2 +- lib/src/view/tools/tools_tab_screen.dart | 2 +- test/view/analysis/analysis_screen_test.dart | 4 +- 22 files changed, 1712 insertions(+), 278 deletions(-) delete mode 100644 lib/src/model/broadcast/broadcast_game_controller.dart delete mode 100644 lib/src/view/broadcast/broadcast_boards_tab_provider.dart create mode 100644 lib/src/view/broadcast/broadcast_game_screen.dart delete mode 100644 lib/src/view/game/game_list_tile_providers.dart diff --git a/lib/src/model/broadcast/broadcast.dart b/lib/src/model/broadcast/broadcast.dart index c24629bcd6..5935f3ae44 100644 --- a/lib/src/model/broadcast/broadcast.dart +++ b/lib/src/model/broadcast/broadcast.dart @@ -82,22 +82,23 @@ class BroadcastRound with _$BroadcastRound { }) = _BroadcastRound; } -typedef BroadcastRoundGames = IMap; +typedef BroadcastRoundGames = IMap; @freezed -class BroadcastGameSnapshot with _$BroadcastGameSnapshot { - const BroadcastGameSnapshot._(); +class BroadcastGame with _$BroadcastGame { + const BroadcastGame._(); - const factory BroadcastGameSnapshot({ + const factory BroadcastGame({ required BroadcastGameId id, required IMap players, required String fen, required Move? lastMove, required String? status, + required String? pgn, /// The amount of time that the player whose turn it is has been thinking since his last move required Duration? thinkTime, - }) = _BroadcastGameSnapshot; + }) = _BroadcastGame; } @freezed diff --git a/lib/src/model/broadcast/broadcast_game_controller.dart b/lib/src/model/broadcast/broadcast_game_controller.dart deleted file mode 100644 index 031f56337e..0000000000 --- a/lib/src/model/broadcast/broadcast_game_controller.dart +++ /dev/null @@ -1,111 +0,0 @@ -import 'dart:async'; - -import 'package:dartchess/dartchess.dart'; -import 'package:deep_pick/deep_pick.dart'; -import 'package:lichess_mobile/src/model/broadcast/broadcast_providers.dart'; -import 'package:lichess_mobile/src/model/broadcast/broadcast_round_controller.dart'; -import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/model/common/node.dart'; -import 'package:lichess_mobile/src/model/common/socket.dart'; -import 'package:lichess_mobile/src/utils/json.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'broadcast_game_controller.g.dart'; - -@riverpod -class BroadcastGameController extends _$BroadcastGameController { - static Uri broadcastSocketUri(BroadcastRoundId broadcastRoundId) => - Uri(path: 'study/$broadcastRoundId/socket/v6'); - - StreamSubscription? _subscription; - - late SocketClient _socketClient; - - @override - Future build({ - required BroadcastRoundId broadcastRoundId, - required BroadcastGameId broadcastGameId, - }) async { - _socketClient = ref - .watch(socketPoolProvider) - .open(BroadcastRoundController.broadcastSocketUri(broadcastRoundId)); - - _subscription = _socketClient.stream.listen(_handleSocketEvent); - - ref.onDispose(() { - _subscription?.cancel(); - }); - - final pgn = await ref.watch( - broadcastGameProvider( - roundId: broadcastRoundId, - gameId: broadcastGameId, - ).future, - ); - - return pgn; - } - - void _handleSocketEvent(SocketEvent event) { - if (!state.hasValue) return; - - switch (event.topic) { - // Sent when a node is recevied from the broadcast - case 'addNode': - _handleAddNodeEvent(event); - // Sent when a pgn tag changes - case 'setTags': - _handleSetTagsEvent(event); - } - } - - void _handleAddNodeEvent(SocketEvent event) { - final gameId = - pick(event.data, 'p', 'chapterId').asBroadcastGameIdOrThrow(); - - // We check that the event we received is for the game we are currently watching - if (gameId != broadcastGameId) return; - - // The path of the last and current move of the broadcasted game - // Its value is "!" if the path is identical to one of the node that was received - final currentPath = pick(event.data, 'relayPath').asUciPathOrThrow(); - - // We check that the event we received is for the last move of the game - if (currentPath.value != '!') return; - - // The path for the node that was received - final path = pick(event.data, 'p', 'path').asUciPathOrThrow(); - final nodeId = pick(event.data, 'n', 'id').asUciCharPairOrThrow(); - - print(state.requireValue); - - print(path + nodeId); - - final newPgn = (Root.fromPgnGame(PgnGame.parsePgn(state.requireValue)) - ..promoteAt(path + nodeId, toMainline: true)) - .makePgn(); - - print(newPgn); - - state = AsyncData(newPgn); - } - - void _handleSetTagsEvent(SocketEvent event) { - final gameId = pick(event.data, 'chapterId').asBroadcastGameIdOrThrow(); - - // We check that the event we received is for the game we are currently watching - if (gameId != broadcastGameId) return; - - final headers = pick(event.data, 'tags').asMapOrThrow(); - - final pgnGame = PgnGame.parsePgn(state.requireValue); - - final newPgnGame = PgnGame( - headers: headers, - moves: pgnGame.moves, - comments: pgnGame.comments, - ); - - state = AsyncData(newPgnGame.makePgn()); - } -} diff --git a/lib/src/model/broadcast/broadcast_repository.dart b/lib/src/model/broadcast/broadcast_repository.dart index 8ab68b7a1d..c870ed1071 100644 --- a/lib/src/model/broadcast/broadcast_repository.dart +++ b/lib/src/model/broadcast/broadcast_repository.dart @@ -148,12 +148,12 @@ BroadcastRoundGames _gamesFromPick( ) => IMap.fromEntries(pick('games').asListOrThrow(gameFromPick)); -MapEntry gameFromPick( +MapEntry gameFromPick( RequiredPick pick, ) => MapEntry( pick('id').asBroadcastGameIdOrThrow(), - BroadcastGameSnapshot( + BroadcastGame( id: pick('id').asBroadcastGameIdOrThrow(), players: IMap({ Side.white: _playerFromPick(pick('players', 0).required()), @@ -164,6 +164,7 @@ MapEntry gameFromPick( lastMove: pick('lastMove').asUciMoveOrNull(), status: pick('status').asStringOrNull(), thinkTime: pick('thinkTime').asDurationFromSecondsOrNull(), + pgn: null, ), ); diff --git a/lib/src/model/broadcast/broadcast_round_controller.dart b/lib/src/model/broadcast/broadcast_round_controller.dart index 6d112b4818..2c458b69c0 100644 --- a/lib/src/model/broadcast/broadcast_round_controller.dart +++ b/lib/src/model/broadcast/broadcast_round_controller.dart @@ -41,6 +41,23 @@ class BroadcastRoundController extends _$BroadcastRoundController { return games; } + Future setPgn(BroadcastGameId gameId) async { + final pgn = await ref.watch( + broadcastGameProvider( + roundId: broadcastRoundId, + gameId: gameId, + ).future, + ); + state = AsyncData( + state.requireValue.update( + gameId, + (broadcastGame) => broadcastGame.copyWith( + pgn: pgn, + ), + ), + ); + } + void _handleSocketEvent(SocketEvent event) { if (!state.hasValue) return; @@ -54,6 +71,9 @@ class BroadcastRoundController extends _$BroadcastRoundController { // Sent when clocks are updated from the broadcast case 'clock': _handleClockEvent(event); + // Sent when a pgn tag changes + case 'setTags': + _handleSetTagsEvent(event); } } @@ -75,15 +95,15 @@ class BroadcastRoundController extends _$BroadcastRoundController { state = AsyncData( state.requireValue.update( broadcastGameId, - (broadcastGameSnapshot) => broadcastGameSnapshot.copyWith( + (broadcastGame) => broadcastGame.copyWith( players: IMap( { - playingSide: broadcastGameSnapshot.players[playingSide]!.copyWith( + playingSide: broadcastGame.players[playingSide]!.copyWith( clock: pick(event.data, 'n', 'clock') .asDurationFromCentiSecondsOrNull(), ), playingSide.opposite: - broadcastGameSnapshot.players[playingSide.opposite]!, + broadcastGame.players[playingSide.opposite]!, }, ), fen: fen, @@ -109,13 +129,13 @@ class BroadcastRoundController extends _$BroadcastRoundController { state = AsyncData( state.requireValue.update( broadcastGameId, - (broadcastGameSnapshot) => broadcastGameSnapshot.copyWith( + (broadcastsGame) => broadcastsGame.copyWith( players: IMap( { - Side.white: broadcastGameSnapshot.players[Side.white]!.copyWith( + Side.white: broadcastsGame.players[Side.white]!.copyWith( clock: whiteClock, ), - Side.black: broadcastGameSnapshot.players[Side.black]!.copyWith( + Side.black: broadcastsGame.players[Side.black]!.copyWith( clock: blackClock, ), }, @@ -124,4 +144,55 @@ class BroadcastRoundController extends _$BroadcastRoundController { ), ); } + + void _handleSetTagsEvent(SocketEvent event) { + final gameId = pick(event.data, 'chapterId').asBroadcastGameIdOrThrow(); + + if (state.requireValue[gameId]?.pgn == null) return; + + final headers = pick(event.data, 'tags').asMapOrThrow(); + + final pgnGame = PgnGame.parsePgn(state.requireValue[gameId]!.pgn!); + + final newPgnGame = PgnGame( + headers: headers, + moves: pgnGame.moves, + comments: pgnGame.comments, + ); + + state = AsyncData( + state.requireValue.update( + gameId, + (broadcastsGame) => broadcastsGame.copyWith( + pgn: newPgnGame.makePgn(), + ), + ), + ); + } } + + +// void _handleAddNodeEvent(SocketEvent event) { +// final gameId = +// pick(event.data, 'p', 'chapterId').asBroadcastGameIdOrThrow(); + +// // We check that the event we received is for the game we are currently watching +// if (gameId != broadcastGameId) return; + +// // The path of the last and current move of the broadcasted game +// // Its value is "!" if the path is identical to one of the node that was received +// final currentPath = pick(event.data, 'relayPath').asUciPathOrThrow(); + +// // We check that the event we received is for the last move of the game +// if (currentPath.value != '!') return; + +// // The path for the node that was received +// final path = pick(event.data, 'p', 'path').asUciPathOrThrow(); +// final nodeId = pick(event.data, 'n', 'id').asUciCharPairOrThrow(); + +// final newPgn = (Root.fromPgnGame(PgnGame.parsePgn(state.requireValue)) +// ..promoteAt(path + nodeId, toMainline: true)) +// .makePgn(); + +// state = AsyncData(newPgn); +// } diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index 6f7dc14779..e2e51c0e5a 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -16,8 +16,10 @@ import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/eval.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/engine/engine.dart'; import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; +import 'package:lichess_mobile/src/model/game/game_repository_providers.dart'; import 'package:lichess_mobile/src/model/game/game_share_service.dart'; import 'package:lichess_mobile/src/model/settings/brightness.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; @@ -43,75 +45,79 @@ import 'analysis_board.dart'; import 'analysis_settings.dart'; import 'tree_view.dart'; -typedef OptionsAndPgn = ({AnalysisOptions options, String pgn}); - -class AnalysisLoadingScreen extends ConsumerWidget { - const AnalysisLoadingScreen({ - required this.pgnAndOptionsProvider, +class AnalysisScreen extends StatelessWidget { + const AnalysisScreen({ + required this.options, + required this.pgnOrId, this.title, }); - /// The async provider that returns analysis options and pgn. - final ProviderListenable> pgnAndOptionsProvider; + /// The analysis options. + final AnalysisOptions options; + + /// The PGN or game ID to load. + final String pgnOrId; + + final String? title; + @override + Widget build(BuildContext context) { + return pgnOrId.length == 8 && GameId(pgnOrId).isValid + ? _LoadGame(GameId(pgnOrId), options, title) + : _LoadedAnalysisScreen( + options: options, + pgn: pgnOrId, + title: title, + ); + } +} + +class _LoadGame extends ConsumerWidget { + const _LoadGame(this.gameId, this.options, this.title); + + final AnalysisOptions options; + final GameId gameId; final String? title; @override Widget build(BuildContext context, WidgetRef ref) { - final pgnAndOptions = ref.watch(pgnAndOptionsProvider); - - Widget buildPlatformScreen({required Widget child}) => - switch (Theme.of(context).platform) { - TargetPlatform.android => _androidBuilder(context, child), - TargetPlatform.iOS => _iosBuilder(context, child), - _ => throw UnimplementedError( - 'Unexpected platform ${Theme.of(context).platform}', - ) - }; - - return pgnAndOptions.when( - data: (pgnAndOptions) => AnalysisScreen( - pgn: pgnAndOptions.pgn, - options: pgnAndOptions.options, - title: title, - ), - loading: () => buildPlatformScreen( - child: const Center(child: CircularProgressIndicator.adaptive())), - error: (error, _) => buildPlatformScreen( - child: Center( + final gameAsync = ref.watch(archivedGameProvider(id: gameId)); + + return gameAsync.when( + data: (game) { + final serverAnalysis = + game.white.analysis != null && game.black.analysis != null + ? (white: game.white.analysis!, black: game.black.analysis!) + : null; + return _LoadedAnalysisScreen( + options: options.copyWith( + id: game.id, + opening: game.meta.opening, + division: game.meta.division, + serverAnalysis: serverAnalysis, + ), + pgn: game.makePgn(), + title: title, + ); + }, + loading: () => const Center(child: CircularProgressIndicator.adaptive()), + error: (error, _) { + return Center( child: Text('Cannot load game analysis: $error'), - ), - ), + ); + }, ); } - - Widget _androidBuilder(BuildContext context, Widget child) => Scaffold( - appBar: AppBar( - title: Text(title ?? context.l10n.analysis), - ), - body: child, - ); - - Widget _iosBuilder(BuildContext context, Widget child) => - CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - middle: Text(title ?? context.l10n.analysis), - ), - child: child, - ); } -class AnalysisScreen extends ConsumerWidget { - const AnalysisScreen({ +class _LoadedAnalysisScreen extends ConsumerWidget { + const _LoadedAnalysisScreen({ required this.options, required this.pgn, this.title, }); - /// The analysis options. final AnalysisOptions options; - - /// The PGN or game ID to load. final String pgn; final String? title; diff --git a/lib/src/view/board_editor/board_editor_screen.dart b/lib/src/view/board_editor/board_editor_screen.dart index 577e3fcaaa..7d62fada35 100644 --- a/lib/src/view/board_editor/board_editor_screen.dart +++ b/lib/src/view/board_editor/board_editor_screen.dart @@ -364,7 +364,7 @@ class _BottomBar extends ConsumerWidget { context, rootNavigator: true, builder: (context) => AnalysisScreen( - pgn: editorState.pgn!, + pgnOrId: editorState.pgn!, options: AnalysisOptions( isLocalEvaluationAllowed: true, variant: Variant.fromPosition, diff --git a/lib/src/view/broadcast/broadcast_boards_tab.dart b/lib/src/view/broadcast/broadcast_boards_tab.dart index 8bf9ca78bd..7fe3036305 100644 --- a/lib/src/view/broadcast/broadcast_boards_tab.dart +++ b/lib/src/view/broadcast/broadcast_boards_tab.dart @@ -15,8 +15,7 @@ import 'package:lichess_mobile/src/utils/duration.dart'; import 'package:lichess_mobile/src/utils/lichess_assets.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; -import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; -import 'package:lichess_mobile/src/view/broadcast/broadcast_boards_tab_provider.dart'; +import 'package:lichess_mobile/src/view/broadcast/broadcast_game_screen.dart'; import 'package:lichess_mobile/src/widgets/board_thumbnail.dart'; import 'package:lichess_mobile/src/widgets/evaluation_bar.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; @@ -30,8 +29,13 @@ const _kPlayerWidgetPadding = EdgeInsets.symmetric(vertical: 5.0); /// A tab that displays the live games of a broadcast round. class BroadcastBoardsTab extends ConsumerWidget { final BroadcastRoundId roundId; + final String title; - const BroadcastBoardsTab({super.key, required this.roundId}); + const BroadcastBoardsTab({ + super.key, + required this.roundId, + required this.title, + }); @override Widget build(BuildContext context, WidgetRef ref) { @@ -48,11 +52,15 @@ class BroadcastBoardsTab extends ConsumerWidget { : BroadcastPreview( games: games.values.toIList(), roundId: roundId, + title: title, ), loading: () => const Shimmer( child: ShimmerLoading( isLoading: true, - child: BroadcastPreview(roundId: BroadcastRoundId('')), + child: BroadcastPreview( + roundId: BroadcastRoundId(''), + title: '', + ), ), ), error: (error, stackTrace) => Center( @@ -65,9 +73,15 @@ class BroadcastBoardsTab extends ConsumerWidget { class BroadcastPreview extends ConsumerWidget { final BroadcastRoundId roundId; - final IList? games; + final IList? games; + final String title; - const BroadcastPreview({super.key, required this.roundId, this.games}); + const BroadcastPreview({ + super.key, + required this.roundId, + this.games, + required this.title, + }); @override Widget build(BuildContext context, WidgetRef ref) { @@ -118,13 +132,15 @@ class BroadcastPreview extends ConsumerWidget { return BoardThumbnail( onTap: () { + ref + .read(BroadcastRoundControllerProvider(roundId).notifier) + .setPgn(game.id); pushPlatformRoute( context, - builder: (context) => AnalysisLoadingScreen( - pgnAndOptionsProvider: broadcastGameAnalysisProvider( - roundId: roundId, - gameId: game.id, - ), + builder: (context) => BroadcastGameScreen( + roundId: roundId, + gameId: game.id, + title: title, ), ); }, diff --git a/lib/src/view/broadcast/broadcast_boards_tab_provider.dart b/lib/src/view/broadcast/broadcast_boards_tab_provider.dart deleted file mode 100644 index 9522226197..0000000000 --- a/lib/src/view/broadcast/broadcast_boards_tab_provider.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'package:dartchess/dartchess.dart'; -import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; -import 'package:lichess_mobile/src/model/broadcast/broadcast_game_controller.dart'; -import 'package:lichess_mobile/src/model/common/chess.dart'; -import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'broadcast_boards_tab_provider.g.dart'; - -@riverpod -Future broadcastGameAnalysis( - BroadcastGameAnalysisRef ref, { - required BroadcastRoundId roundId, - required BroadcastGameId gameId, -}) async { - final pgn = await ref.watch( - broadcastGameControllerProvider( - broadcastRoundId: roundId, - broadcastGameId: gameId, - ).future, - ); - - return ( - options: const AnalysisOptions( - id: StringId('standalone_analysis'), - isLocalEvaluationAllowed: true, - orientation: Side.white, - variant: Variant.standard, - ), - pgn: pgn, - ); -} diff --git a/lib/src/view/broadcast/broadcast_game_screen.dart b/lib/src/view/broadcast/broadcast_game_screen.dart new file mode 100644 index 0000000000..f5e3ee5355 --- /dev/null +++ b/lib/src/view/broadcast/broadcast_game_screen.dart @@ -0,0 +1,1509 @@ +import 'dart:async'; +import 'dart:math' as math; + +import 'package:collection/collection.dart'; +import 'package:dartchess/dartchess.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:lichess_mobile/src/constants.dart'; +import 'package:lichess_mobile/src/model/account/account_preferences.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; +import 'package:lichess_mobile/src/model/analysis/server_analysis_service.dart'; +import 'package:lichess_mobile/src/model/auth/auth_session.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast_round_controller.dart'; +import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/common/eval.dart'; +import 'package:lichess_mobile/src/model/common/http.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/engine/engine.dart'; +import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; +import 'package:lichess_mobile/src/model/game/game_share_service.dart'; +import 'package:lichess_mobile/src/model/settings/brightness.dart'; +import 'package:lichess_mobile/src/styles/lichess_icons.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/utils/duration.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/lichess_assets.dart'; +import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/utils/screen.dart'; +import 'package:lichess_mobile/src/utils/share.dart'; +import 'package:lichess_mobile/src/utils/string.dart'; +import 'package:lichess_mobile/src/view/analysis/analysis_board.dart'; +import 'package:lichess_mobile/src/view/analysis/analysis_settings.dart'; +import 'package:lichess_mobile/src/view/analysis/analysis_share_screen.dart'; +import 'package:lichess_mobile/src/view/analysis/tree_view.dart'; +import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; +import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_screen.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; +import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/feedback.dart'; +import 'package:lichess_mobile/src/widgets/list.dart'; +import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:popover/popover.dart'; + +const _options = AnalysisOptions( + id: StringId('standalone_analysis'), + isLocalEvaluationAllowed: true, + orientation: Side.white, + variant: Variant.standard, +); + +class BroadcastGameScreen extends ConsumerWidget { + final BroadcastRoundId roundId; + final BroadcastGameId gameId; + final String title; + + const BroadcastGameScreen({ + required this.roundId, + required this.gameId, + required this.title, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return ConsumerPlatformWidget( + androidBuilder: _androidBuilder, + iosBuilder: _iosBuilder, + ref: ref, + ); + } + + Widget _androidBuilder(BuildContext context, WidgetRef ref) { + final game = ref.watch( + broadcastRoundControllerProvider( + roundId, + ).select((games) => games.value?[gameId]), + ); + final pgn = game?.pgn; + + return Scaffold( + resizeToAvoidBottomInset: false, + appBar: AppBar( + title: Text(title), + actions: [ + if (pgn != null) + _EngineDepth( + analysisControllerProvider(pgn, _options), + ), + AppBarIconButton( + onPressed: () => (pgn != null) + ? showAdaptiveBottomSheet( + context: context, + isScrollControlled: true, + showDragHandle: true, + isDismissible: true, + builder: (_) => AnalysisSettings(pgn, _options), + ) + : null, + semanticsLabel: context.l10n.settingsSettings, + icon: const Icon(Icons.settings), + ), + ], + ), + body: (pgn != null) + ? _Body(game: game!, options: _options) + : const Center(child: CircularProgressIndicator.adaptive()), + ); + } + + Widget _iosBuilder(BuildContext context, WidgetRef ref) { + final game = ref.watch( + broadcastRoundControllerProvider( + roundId, + ).select((games) => games.value?[gameId]), + ); + final pgn = game?.pgn; + + return CupertinoPageScaffold( + resizeToAvoidBottomInset: false, + navigationBar: CupertinoNavigationBar( + backgroundColor: Styles.cupertinoScaffoldColor.resolveFrom(context), + border: null, + padding: Styles.cupertinoAppBarTrailingWidgetPadding, + middle: Text(title), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (pgn != null) + _EngineDepth( + analysisControllerProvider(pgn, _options), + ), + AppBarIconButton( + onPressed: () => (pgn != null) + ? showAdaptiveBottomSheet( + context: context, + isScrollControlled: true, + showDragHandle: true, + isDismissible: true, + builder: (_) => AnalysisSettings(pgn, _options), + ) + : null, + semanticsLabel: context.l10n.settingsSettings, + icon: const Icon(Icons.settings), + ), + ], + ), + ), + child: (pgn != null) + ? _Body(game: game!, options: _options) + : const Center(child: CircularProgressIndicator.adaptive()), + ); + } +} + +class _Body extends ConsumerWidget { + const _Body({required this.game, required this.options}); + + final BroadcastGame game; + final AnalysisOptions options; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final ctrlProvider = analysisControllerProvider(game.pgn!, options); + final showEvaluationGauge = ref.watch( + analysisPreferencesProvider.select((value) => value.showEvaluationGauge), + ); + + final isEngineAvailable = ref.watch( + ctrlProvider.select( + (value) => value.isEngineAvailable, + ), + ); + + final hasEval = + ref.watch(ctrlProvider.select((value) => value.hasAvailableEval)); + + final showAnalysisSummary = ref.watch( + ctrlProvider.select((value) => value.displayMode == DisplayMode.summary), + ); + + final pov = ref.watch(ctrlProvider.select((value) => value.pov)); + + return Column( + children: [ + Expanded( + child: SafeArea( + bottom: false, + child: LayoutBuilder( + builder: (context, constraints) { + final aspectRatio = constraints.biggest.aspectRatio; + final defaultBoardSize = constraints.biggest.shortestSide; + final isTablet = isTabletOrLarger(context); + final remainingHeight = + constraints.maxHeight - defaultBoardSize; + final isSmallScreen = + remainingHeight < kSmallRemainingHeightLeftBoardThreshold; + final boardSize = isTablet || isSmallScreen + ? defaultBoardSize - kTabletBoardTableSidePadding * 2 + : defaultBoardSize; + + return aspectRatio > 1 + ? Row( + mainAxisSize: MainAxisSize.max, + children: [ + Padding( + padding: const EdgeInsets.only( + left: kTabletBoardTableSidePadding, + top: kTabletBoardTableSidePadding, + bottom: kTabletBoardTableSidePadding, + ), + child: Row( + children: [ + _AnalysisBoardPlayersAndClocks( + game, + pov, + boardSize, + isTablet: isTablet, + ), + if (hasEval && showEvaluationGauge) ...[ + const SizedBox(width: 4.0), + _EngineGaugeVertical(ctrlProvider), + ], + ], + ), + ), + Flexible( + fit: FlexFit.loose, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + if (isEngineAvailable) + _EngineLines( + ctrlProvider, + isLandscape: true, + ), + Expanded( + child: PlatformCard( + margin: const EdgeInsets.all( + kTabletBoardTableSidePadding, + ), + semanticContainer: false, + child: showAnalysisSummary + ? ServerAnalysisSummary( + game.pgn!, + options, + ) + : AnalysisTreeView( + game.pgn!, + options, + Orientation.landscape, + ), + ), + ), + ], + ), + ), + ], + ) + : Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + _ColumnTopTable(ctrlProvider), + if (isTablet) + Padding( + padding: const EdgeInsets.all( + kTabletBoardTableSidePadding, + ), + child: _AnalysisBoardPlayersAndClocks( + game, + pov, + boardSize, + isTablet: isTablet, + ), + ) + else + _AnalysisBoardPlayersAndClocks( + game, + pov, + boardSize, + isTablet: isTablet, + ), + if (showAnalysisSummary) + Expanded( + child: ServerAnalysisSummary(game.pgn!, options), + ) + else + Expanded( + child: AnalysisTreeView( + game.pgn!, + options, + Orientation.portrait, + ), + ), + ], + ); + }, + ), + ), + ), + _BottomBar(pgn: game.pgn!, options: options), + ], + ); + } +} + +enum _PlayerWidgetSide { bottom, top } + +class _AnalysisBoardPlayersAndClocks extends StatelessWidget { + final BroadcastGame game; + final Side pov; + final double boardSize; + final bool isTablet; + + const _AnalysisBoardPlayersAndClocks( + this.game, + this.pov, + this.boardSize, { + required this.isTablet, + }); + + @override + Widget build(BuildContext context) { + final playingSide = Setup.parseFen(game.fen).turn; + + return Column( + children: [ + _PlayerWidget( + width: boardSize, + player: game.players[pov.opposite]!, + gameStatus: game.status, + thinkTime: game.thinkTime, + pov: pov.opposite, + playingSide: playingSide, + side: _PlayerWidgetSide.top, + ), + AnalysisBoard( + game.pgn!, + _options, + boardSize, + isTablet: isTablet, + ), + _PlayerWidget( + width: boardSize, + player: game.players[pov]!, + gameStatus: game.status, + thinkTime: game.thinkTime, + pov: pov, + playingSide: playingSide, + side: _PlayerWidgetSide.bottom, + ), + ], + ); + } +} + +class _PlayerWidget extends StatelessWidget { + const _PlayerWidget({ + required this.width, + required this.player, + required this.gameStatus, + required this.thinkTime, + required this.pov, + required this.playingSide, + required this.side, + }); + + final BroadcastPlayer player; + final String? gameStatus; + final Duration? thinkTime; + final Side pov; + final Side playingSide; + final double width; + final _PlayerWidgetSide side; + + @override + Widget build(BuildContext context) { + return SizedBox( + width: width, + child: Card( + margin: EdgeInsets.zero, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(side == _PlayerWidgetSide.top ? 8 : 0), + topRight: Radius.circular(side == _PlayerWidgetSide.top ? 8 : 0), + bottomLeft: + Radius.circular(side == _PlayerWidgetSide.bottom ? 8 : 0), + bottomRight: + Radius.circular(side == _PlayerWidgetSide.bottom ? 8 : 0), + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Row( + children: [ + if (gameStatus != null && gameStatus != '*') ...[ + Text( + (gameStatus == '½-½') + ? '½' + : (gameStatus == '1-0') + ? pov == Side.white + ? '1' + : '0' + : pov == Side.black + ? '1' + : '0', + style: const TextStyle() + .copyWith(fontWeight: FontWeight.bold), + ), + const SizedBox(width: 15), + ], + if (player.federation != null) ...[ + Consumer( + builder: (context, widgetRef, _) { + return SvgPicture.network( + lichessFideFedSrc(player.federation!), + height: 12, + httpClient: widgetRef.read(defaultClientProvider), + ); + }, + ), + ], + const SizedBox(width: 5), + if (player.title != null) ...[ + Text( + player.title!, + style: const TextStyle().copyWith( + color: context.lichessColors.brag, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(width: 5), + ], + Text( + player.name, + style: const TextStyle().copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(width: 5), + Text(player.rating.toString(), style: const TextStyle()), + ], + ), + ), + if (player.clock != null) + (pov == playingSide) + ? _Clock( + clock: player.clock! - (thinkTime ?? Duration.zero), + ) + : Text( + player.clock!.toHoursMinutesSeconds(), + style: const TextStyle( + fontFeatures: [FontFeature.tabularFigures()], + ), + ), + ], + ), + ), + ), + ); + } +} + +class _Clock extends StatefulWidget { + const _Clock({required this.clock}); + + final Duration clock; + + @override + _ClockState createState() => _ClockState(); +} + +class _ClockState extends State<_Clock> { + Timer? _timer; + late Duration _clock; + + @override + void initState() { + super.initState(); + _clock = widget.clock; + if (_clock.inSeconds <= 0) { + _clock = Duration.zero; + return; + } + _timer = Timer.periodic(const Duration(seconds: 1), (timer) { + setState(() { + _clock = _clock - const Duration(seconds: 1); + }); + if (_clock.inSeconds == 0) { + timer.cancel(); + return; + } + }); + } + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Text( + _clock.toHoursMinutesSeconds(), + style: TextStyle( + color: Colors.orange[900], + fontFeatures: const [FontFeature.tabularFigures()], + ), + ); + } +} + +class _EngineGaugeVertical extends ConsumerWidget { + const _EngineGaugeVertical(this.ctrlProvider); + + final AnalysisControllerProvider ctrlProvider; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final analysisState = ref.watch(ctrlProvider); + + return Container( + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4.0), + ), + child: EngineGauge( + displayMode: EngineGaugeDisplayMode.vertical, + params: analysisState.engineGaugeParams, + ), + ); + } +} + +class _ColumnTopTable extends ConsumerWidget { + const _ColumnTopTable(this.ctrlProvider); + + final AnalysisControllerProvider ctrlProvider; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final analysisState = ref.watch(ctrlProvider); + final showEvaluationGauge = ref.watch( + analysisPreferencesProvider.select((p) => p.showEvaluationGauge), + ); + + return analysisState.hasAvailableEval + ? Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (showEvaluationGauge) + EngineGauge( + displayMode: EngineGaugeDisplayMode.horizontal, + params: analysisState.engineGaugeParams, + ), + if (analysisState.isEngineAvailable) + _EngineLines(ctrlProvider, isLandscape: false), + ], + ) + : kEmptyWidget; + } +} + +class _EngineLines extends ConsumerWidget { + const _EngineLines(this.ctrlProvider, {required this.isLandscape}); + final AnalysisControllerProvider ctrlProvider; + final bool isLandscape; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final analysisState = ref.watch(ctrlProvider); + final numEvalLines = ref.watch( + analysisPreferencesProvider.select( + (p) => p.numEvalLines, + ), + ); + final engineEval = ref.watch(engineEvaluationProvider).eval; + final eval = engineEval ?? analysisState.currentNode.eval; + + final emptyLines = List.filled( + numEvalLines, + _Engineline.empty(ctrlProvider), + ); + + final content = !analysisState.position.isGameOver + ? (eval != null + ? eval.pvs + .take(numEvalLines) + .map( + (pv) => _Engineline(ctrlProvider, eval.position, pv), + ) + .toList() + : emptyLines) + : emptyLines; + + if (content.length < numEvalLines) { + final padding = List.filled( + numEvalLines - content.length, + _Engineline.empty(ctrlProvider), + ); + content.addAll(padding); + } + + return Padding( + padding: EdgeInsets.symmetric( + vertical: isLandscape ? kTabletBoardTableSidePadding : 0.0, + horizontal: isLandscape ? kTabletBoardTableSidePadding : 0.0, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: content, + ), + ); + } +} + +class _Engineline extends ConsumerWidget { + const _Engineline( + this.ctrlProvider, + this.fromPosition, + this.pvData, + ); + + const _Engineline.empty(this.ctrlProvider) + : pvData = const PvData(moves: IListConst([])), + fromPosition = Chess.initial; + + final AnalysisControllerProvider ctrlProvider; + final Position fromPosition; + final PvData pvData; + + @override + Widget build(BuildContext context, WidgetRef ref) { + if (pvData.moves.isEmpty) { + return const SizedBox( + height: kEvalGaugeSize, + child: SizedBox.shrink(), + ); + } + + final pieceNotation = ref.watch(pieceNotationProvider).maybeWhen( + data: (value) => value, + orElse: () => defaultAccountPreferences.pieceNotation, + ); + + final lineBuffer = StringBuffer(); + int ply = fromPosition.ply + 1; + pvData.sanMoves(fromPosition).forEachIndexed((i, s) { + lineBuffer.write( + ply.isOdd + ? '${(ply / 2).ceil()}. $s ' + : i == 0 + ? '${(ply / 2).ceil()}... $s ' + : '$s ', + ); + ply += 1; + }); + + final brightness = ref.watch(currentBrightnessProvider); + + final evalString = pvData.evalString; + return AdaptiveInkWell( + onTap: () => ref + .read(ctrlProvider.notifier) + .onUserMove(Move.parse(pvData.moves[0])!), + child: SizedBox( + height: kEvalGaugeSize, + child: Padding( + padding: const EdgeInsets.all(2.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + decoration: BoxDecoration( + color: pvData.winningSide == Side.black + ? EngineGauge.backgroundColor(context, brightness) + : EngineGauge.valueColor(context, brightness), + borderRadius: BorderRadius.circular(4.0), + ), + padding: const EdgeInsets.symmetric( + horizontal: 4.0, + vertical: 2.0, + ), + child: Text( + evalString, + style: TextStyle( + color: pvData.winningSide == Side.black + ? Colors.white + : Colors.black, + fontSize: kEvalGaugeFontSize, + fontWeight: FontWeight.w600, + ), + ), + ), + const SizedBox(width: 8.0), + Expanded( + child: Text( + lineBuffer.toString(), + maxLines: 1, + softWrap: false, + style: TextStyle( + fontFamily: pieceNotation == PieceNotation.symbol + ? 'ChessFont' + : null, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ), + ); + } +} + +class _BottomBar extends ConsumerWidget { + const _BottomBar({ + required this.pgn, + required this.options, + }); + + final String pgn; + final AnalysisOptions options; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final ctrlProvider = analysisControllerProvider(pgn, options); + final canGoBack = + ref.watch(ctrlProvider.select((value) => value.canGoBack)); + final canGoNext = + ref.watch(ctrlProvider.select((value) => value.canGoNext)); + final displayMode = + ref.watch(ctrlProvider.select((value) => value.displayMode)); + final canShowGameSummary = + ref.watch(ctrlProvider.select((value) => value.canShowGameSummary)); + + return Container( + color: Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoTheme.of(context).barBackgroundColor + : Theme.of(context).bottomAppBarTheme.color, + child: SafeArea( + top: false, + child: SizedBox( + height: kBottomBarHeight, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Expanded( + child: BottomBarButton( + label: context.l10n.menu, + onTap: () { + _showAnalysisMenu(context, ref); + }, + icon: Icons.menu, + ), + ), + if (canShowGameSummary) + Expanded( + child: BottomBarButton( + label: displayMode == DisplayMode.summary + ? 'Moves' + : 'Summary', + onTap: () { + ref.read(ctrlProvider.notifier).toggleDisplayMode(); + }, + icon: displayMode == DisplayMode.summary + ? LichessIcons.flow_cascade + : Icons.area_chart, + ), + ), + Expanded( + child: BottomBarButton( + label: context.l10n.openingExplorer, + onTap: () => pushPlatformRoute( + context, + builder: (_) => OpeningExplorerScreen( + pgn: pgn, + options: options, + ), + ), + icon: Icons.explore, + ), + ), + Expanded( + child: RepeatButton( + onLongPress: canGoBack ? () => _moveBackward(ref) : null, + child: BottomBarButton( + key: const ValueKey('goto-previous'), + onTap: canGoBack ? () => _moveBackward(ref) : null, + label: 'Previous', + icon: CupertinoIcons.chevron_back, + showTooltip: false, + ), + ), + ), + Expanded( + child: RepeatButton( + onLongPress: canGoNext ? () => _moveForward(ref) : null, + child: BottomBarButton( + key: const ValueKey('goto-next'), + icon: CupertinoIcons.chevron_forward, + label: context.l10n.next, + onTap: canGoNext ? () => _moveForward(ref) : null, + showTooltip: false, + ), + ), + ), + ], + ), + ), + ), + ); + } + + void _moveForward(WidgetRef ref) => + ref.read(analysisControllerProvider(pgn, options).notifier).userNext(); + void _moveBackward(WidgetRef ref) => ref + .read(analysisControllerProvider(pgn, options).notifier) + .userPrevious(); + + Future _showAnalysisMenu(BuildContext context, WidgetRef ref) { + return showAdaptiveActionSheet( + context: context, + actions: [ + BottomSheetAction( + makeLabel: (context) => Text(context.l10n.flipBoard), + onPressed: (context) { + ref + .read(analysisControllerProvider(pgn, options).notifier) + .toggleBoard(); + }, + ), + BottomSheetAction( + makeLabel: (context) => Text(context.l10n.mobileShareGamePGN), + onPressed: (_) { + pushPlatformRoute( + context, + title: context.l10n.studyShareAndExport, + builder: (_) => AnalysisShareScreen(pgn: pgn, options: options), + ); + }, + ), + BottomSheetAction( + makeLabel: (context) => Text(context.l10n.mobileSharePositionAsFEN), + onPressed: (_) { + launchShareDialog( + context, + text: ref + .read(analysisControllerProvider(pgn, options)) + .position + .fen, + ); + }, + ), + if (options.gameAnyId != null) + BottomSheetAction( + makeLabel: (context) => + Text(context.l10n.screenshotCurrentPosition), + onPressed: (_) async { + final gameId = options.gameAnyId!.gameId; + final state = ref.read(analysisControllerProvider(pgn, options)); + try { + final image = + await ref.read(gameShareServiceProvider).screenshotPosition( + gameId, + options.orientation, + state.position.fen, + state.lastMove, + ); + if (context.mounted) { + launchShareDialog( + context, + files: [image], + subject: context.l10n.puzzleFromGameLink( + lichessUri('/$gameId').toString(), + ), + ); + } + } catch (e) { + if (context.mounted) { + showPlatformSnackbar( + context, + 'Failed to get GIF', + type: SnackBarType.error, + ); + } + } + }, + ), + ], + ); + } +} + +class _EngineDepth extends ConsumerWidget { + const _EngineDepth(this.ctrlProvider); + + final AnalysisControllerProvider ctrlProvider; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isEngineAvailable = ref.watch( + ctrlProvider.select( + (value) => value.isEngineAvailable, + ), + ); + final currentNode = ref.watch( + ctrlProvider.select((value) => value.currentNode), + ); + final depth = ref.watch( + engineEvaluationProvider.select((value) => value.eval?.depth), + ) ?? + currentNode.eval?.depth; + + return isEngineAvailable && depth != null + ? AppBarTextButton( + onPressed: () { + showPopover( + context: context, + bodyBuilder: (context) { + return _StockfishInfo(currentNode); + }, + direction: PopoverDirection.top, + width: 240, + backgroundColor: + Theme.of(context).platform == TargetPlatform.android + ? Theme.of(context).dialogBackgroundColor + : CupertinoDynamicColor.resolve( + CupertinoColors.tertiarySystemBackground, + context, + ), + transitionDuration: Duration.zero, + popoverTransitionBuilder: (_, child) => child, + ); + }, + child: RepaintBoundary( + child: Container( + width: 20.0, + height: 20.0, + padding: const EdgeInsets.all(2.0), + decoration: BoxDecoration( + color: Theme.of(context).platform == TargetPlatform.android + ? Theme.of(context).colorScheme.secondary + : CupertinoTheme.of(context).primaryColor, + borderRadius: BorderRadius.circular(4.0), + ), + child: FittedBox( + fit: BoxFit.contain, + child: Text( + '${math.min(99, depth)}', + style: TextStyle( + color: Theme.of(context).platform == + TargetPlatform.android + ? Theme.of(context).colorScheme.onSecondary + : CupertinoTheme.of(context).primaryContrastingColor, + fontFeatures: const [ + FontFeature.tabularFigures(), + ], + ), + ), + ), + ), + ), + ) + : const SizedBox.shrink(); + } +} + +class _StockfishInfo extends ConsumerWidget { + const _StockfishInfo(this.currentNode); + + final AnalysisCurrentNode currentNode; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final (engineName: engineName, eval: eval, state: engineState) = + ref.watch(engineEvaluationProvider); + + final currentEval = eval ?? currentNode.eval; + + final knps = engineState == EngineState.computing + ? ', ${eval?.knps.round()}kn/s' + : ''; + final depth = currentEval?.depth ?? 0; + final maxDepth = math.max(depth, kMaxEngineDepth); + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + PlatformListTile( + leading: Image.asset( + 'assets/images/stockfish/icon.png', + width: 44, + height: 44, + ), + title: Text(engineName), + subtitle: Text( + context.l10n.depthX( + '$depth/$maxDepth$knps', + ), + ), + ), + ], + ); + } +} + +class ServerAnalysisSummary extends ConsumerWidget { + const ServerAnalysisSummary(this.pgn, this.options); + + final String pgn; + final AnalysisOptions options; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final ctrlProvider = analysisControllerProvider(pgn, options); + final playersAnalysis = + ref.watch(ctrlProvider.select((value) => value.playersAnalysis)); + final pgnHeaders = + ref.watch(ctrlProvider.select((value) => value.pgnHeaders)); + final currentGameAnalysis = ref.watch(currentAnalysisProvider); + + return playersAnalysis != null + ? ListView( + children: [ + if (currentGameAnalysis == options.gameAnyId?.gameId) + const Padding( + padding: EdgeInsets.only(top: 16.0), + child: WaitingForServerAnalysis(), + ), + AcplChart(pgn, options), + Center( + child: SizedBox( + width: math.min(MediaQuery.sizeOf(context).width, 500), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Table( + defaultVerticalAlignment: + TableCellVerticalAlignment.middle, + columnWidths: const { + 0: FlexColumnWidth(1), + 1: FlexColumnWidth(1), + 2: FlexColumnWidth(1), + }, + children: [ + TableRow( + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide(color: Colors.grey), + ), + ), + children: [ + _SummaryPlayerName(Side.white, pgnHeaders), + Center( + child: Text( + pgnHeaders.get('Result') ?? '', + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + ), + _SummaryPlayerName(Side.black, pgnHeaders), + ], + ), + if (playersAnalysis.white.accuracy != null && + playersAnalysis.black.accuracy != null) + TableRow( + children: [ + _SummaryNumber( + '${playersAnalysis.white.accuracy}%', + ), + Center( + heightFactor: 1.8, + child: Text( + context.l10n.accuracy, + softWrap: true, + ), + ), + _SummaryNumber( + '${playersAnalysis.black.accuracy}%', + ), + ], + ), + for (final item in [ + ( + playersAnalysis.white.inaccuracies.toString(), + context.l10n + .nbInaccuracies(2) + .replaceAll('2', '') + .trim() + .capitalize(), + playersAnalysis.black.inaccuracies.toString() + ), + ( + playersAnalysis.white.mistakes.toString(), + context.l10n + .nbMistakes(2) + .replaceAll('2', '') + .trim() + .capitalize(), + playersAnalysis.black.mistakes.toString() + ), + ( + playersAnalysis.white.blunders.toString(), + context.l10n + .nbBlunders(2) + .replaceAll('2', '') + .trim() + .capitalize(), + playersAnalysis.black.blunders.toString() + ), + ]) + TableRow( + children: [ + _SummaryNumber(item.$1), + Center( + heightFactor: 1.2, + child: Text( + item.$2, + softWrap: true, + ), + ), + _SummaryNumber(item.$3), + ], + ), + if (playersAnalysis.white.acpl != null && + playersAnalysis.black.acpl != null) + TableRow( + children: [ + _SummaryNumber( + playersAnalysis.white.acpl.toString(), + ), + Center( + heightFactor: 1.5, + child: Text( + context.l10n.averageCentipawnLoss, + softWrap: true, + textAlign: TextAlign.center, + ), + ), + _SummaryNumber( + playersAnalysis.black.acpl.toString(), + ), + ], + ), + ], + ), + ), + ), + ), + ], + ) + : Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Spacer(), + if (currentGameAnalysis == options.gameAnyId?.gameId) + const Center( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 16.0), + child: WaitingForServerAnalysis(), + ), + ) + else + Center( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Builder( + builder: (context) { + Future? pendingRequest; + return StatefulBuilder( + builder: (context, setState) { + return FutureBuilder( + future: pendingRequest, + builder: (context, snapshot) { + return SecondaryButton( + semanticsLabel: + context.l10n.requestAComputerAnalysis, + onPressed: ref.watch(authSessionProvider) == + null + ? () { + showPlatformSnackbar( + context, + context + .l10n.youNeedAnAccountToDoThat, + ); + } + : snapshot.connectionState == + ConnectionState.waiting + ? null + : () { + setState(() { + pendingRequest = ref + .read(ctrlProvider.notifier) + .requestServerAnalysis() + .catchError((Object e) { + if (context.mounted) { + showPlatformSnackbar( + context, + e.toString(), + type: SnackBarType.error, + ); + } + }); + }); + }, + child: Text( + context.l10n.requestAComputerAnalysis, + ), + ); + }, + ); + }, + ); + }, + ), + ), + ), + const Spacer(), + ], + ); + } +} + +class WaitingForServerAnalysis extends StatelessWidget { + const WaitingForServerAnalysis({super.key}); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ + Image.asset( + 'assets/images/stockfish/icon.png', + width: 30, + height: 30, + ), + const SizedBox(width: 8.0), + Text(context.l10n.waitingForAnalysis), + const SizedBox(width: 8.0), + const CircularProgressIndicator.adaptive(), + ], + ); + } +} + +class _SummaryNumber extends StatelessWidget { + const _SummaryNumber(this.data); + final String data; + + @override + Widget build(BuildContext context) { + return Center( + child: Text( + data, + softWrap: true, + ), + ); + } +} + +class _SummaryPlayerName extends StatelessWidget { + const _SummaryPlayerName(this.side, this.pgnHeaders); + final Side side; + final IMap pgnHeaders; + + @override + Widget build(BuildContext context) { + final playerTitle = side == Side.white + ? pgnHeaders.get('WhiteTitle') + : pgnHeaders.get('BlackTitle'); + final playerName = side == Side.white + ? pgnHeaders.get('White') ?? context.l10n.white + : pgnHeaders.get('Black') ?? context.l10n.black; + + final brightness = Theme.of(context).brightness; + + return TableCell( + verticalAlignment: TableCellVerticalAlignment.top, + child: Center( + child: Padding( + padding: const EdgeInsets.only(bottom: 5), + child: Column( + children: [ + Icon( + side == Side.white + ? brightness == Brightness.light + ? CupertinoIcons.circle + : CupertinoIcons.circle_filled + : brightness == Brightness.light + ? CupertinoIcons.circle_filled + : CupertinoIcons.circle, + size: 14, + ), + Text( + '${playerTitle != null ? '$playerTitle ' : ''}$playerName', + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + softWrap: true, + ), + ], + ), + ), + ), + ); + } +} + +class AcplChart extends ConsumerWidget { + const AcplChart(this.pgn, this.options); + + final String pgn; + final AnalysisOptions options; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final mainLineColor = Theme.of(context).colorScheme.secondary; + // yes it looks like below/above are inverted in fl_chart + final brightness = Theme.of(context).brightness; + final white = Theme.of(context).colorScheme.surfaceContainerHighest; + final black = Theme.of(context).colorScheme.outline; + // yes it looks like below/above are inverted in fl_chart + final belowLineColor = brightness == Brightness.light ? white : black; + final aboveLineColor = brightness == Brightness.light ? black : white; + + VerticalLine phaseVerticalBar(double x, String label) => VerticalLine( + x: x, + color: const Color(0xFF707070), + strokeWidth: 0.5, + label: VerticalLineLabel( + style: TextStyle( + fontSize: 10, + color: Theme.of(context) + .textTheme + .labelMedium + ?.color + ?.withOpacity(0.3), + ), + labelResolver: (line) => label, + padding: const EdgeInsets.only(right: 1), + alignment: Alignment.topRight, + direction: LabelDirection.vertical, + show: true, + ), + ); + + final data = ref.watch( + analysisControllerProvider(pgn, options) + .select((value) => value.acplChartData), + ); + + final rootPly = ref.watch( + analysisControllerProvider(pgn, options) + .select((value) => value.root.position.ply), + ); + + final currentNode = ref.watch( + analysisControllerProvider(pgn, options) + .select((value) => value.currentNode), + ); + + final isOnMainline = ref.watch( + analysisControllerProvider(pgn, options) + .select((value) => value.isOnMainline), + ); + + if (data == null) { + return const SizedBox.shrink(); + } + + final spots = data + .mapIndexed( + (i, e) => FlSpot(i.toDouble(), e.winningChances(Side.white)), + ) + .toList(growable: false); + + final divisionLines = []; + + if (options.division?.middlegame != null) { + if (options.division!.middlegame! > 0) { + divisionLines.add(phaseVerticalBar(0.0, context.l10n.opening)); + divisionLines.add( + phaseVerticalBar( + options.division!.middlegame! - 1, + context.l10n.middlegame, + ), + ); + } else { + divisionLines.add(phaseVerticalBar(0.0, context.l10n.middlegame)); + } + } + + if (options.division?.endgame != null) { + if (options.division!.endgame! > 0) { + divisionLines.add( + phaseVerticalBar( + options.division!.endgame! - 1, + context.l10n.endgame, + ), + ); + } else { + divisionLines.add( + phaseVerticalBar( + 0.0, + context.l10n.endgame, + ), + ); + } + } + return Center( + child: AspectRatio( + aspectRatio: 2.5, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: LineChart( + LineChartData( + lineTouchData: LineTouchData( + enabled: false, + touchCallback: + (FlTouchEvent event, LineTouchResponse? touchResponse) { + if (event is FlTapDownEvent || + event is FlPanUpdateEvent || + event is FlLongPressMoveUpdate) { + final touchX = event.localPosition!.dx; + final chartWidth = context.size!.width - + 32; // Insets on both sides of the chart of 16 + final minX = spots.first.x; + final maxX = spots.last.x; + final touchXDataValue = + minX + (touchX / chartWidth) * (maxX - minX); + final closestSpot = spots.reduce( + (a, b) => (a.x - touchXDataValue).abs() < + (b.x - touchXDataValue).abs() + ? a + : b, + ); + final closestNodeIndex = closestSpot.x.round(); + ref + .read(analysisControllerProvider(pgn, options).notifier) + .jumpToNthNodeOnMainline(closestNodeIndex); + } + }, + ), + minY: -1.0, + maxY: 1.0, + lineBarsData: [ + LineChartBarData( + spots: spots, + isCurved: false, + barWidth: 1, + color: mainLineColor.withOpacity(0.7), + aboveBarData: BarAreaData( + show: true, + color: aboveLineColor, + applyCutOffY: true, + ), + belowBarData: BarAreaData( + show: true, + color: belowLineColor, + applyCutOffY: true, + ), + dotData: const FlDotData( + show: false, + ), + ), + ], + extraLinesData: ExtraLinesData( + verticalLines: [ + if (isOnMainline) + VerticalLine( + x: (currentNode.position.ply - 1 - rootPly).toDouble(), + color: mainLineColor, + strokeWidth: 1.0, + ), + ...divisionLines, + ], + ), + gridData: const FlGridData(show: false), + borderData: FlBorderData(show: false), + titlesData: const FlTitlesData(show: false), + ), + ), + ), + ), + ); + } +} diff --git a/lib/src/view/broadcast/broadcast_screen.dart b/lib/src/view/broadcast/broadcast_screen.dart index a3fcb102e4..979cdc737d 100644 --- a/lib/src/view/broadcast/broadcast_screen.dart +++ b/lib/src/view/broadcast/broadcast_screen.dart @@ -95,7 +95,10 @@ class _AndroidScreenState extends State<_AndroidScreen> controller: _tabController, children: [ BroadcastOverviewTab(tournamentId: _selectedTournamentId), - BroadcastBoardsTab(roundId: _selectedRoundId), + BroadcastBoardsTab( + roundId: _selectedRoundId, + title: widget.broadcast.title, + ), ], ), bottomNavigationBar: BottomAppBar( @@ -175,7 +178,10 @@ class _CupertinoScreenState extends State<_CupertinoScreen> { Expanded( child: _selectedSegment == _ViewMode.overview ? BroadcastOverviewTab(tournamentId: _selectedTournamentId) - : BroadcastBoardsTab(roundId: _selectedRoundId), + : BroadcastBoardsTab( + roundId: _selectedRoundId, + title: widget.broadcast.title, + ), ), _IOSTournamentAndRoundSelector( tournamentId: _selectedTournamentId, diff --git a/lib/src/view/correspondence/offline_correspondence_game_screen.dart b/lib/src/view/correspondence/offline_correspondence_game_screen.dart index 71da4ce456..426ff94aac 100644 --- a/lib/src/view/correspondence/offline_correspondence_game_screen.dart +++ b/lib/src/view/correspondence/offline_correspondence_game_screen.dart @@ -286,7 +286,7 @@ class _BodyState extends ConsumerState<_Body> { pushPlatformRoute( context, builder: (_) => AnalysisScreen( - pgn: game.makePgn(), + pgnOrId: game.makePgn(), options: AnalysisOptions( isLocalEvaluationAllowed: false, variant: game.variant, diff --git a/lib/src/view/game/archived_game_screen.dart b/lib/src/view/game/archived_game_screen.dart index c8bdfc8699..a6493d7d5c 100644 --- a/lib/src/view/game/archived_game_screen.dart +++ b/lib/src/view/game/archived_game_screen.dart @@ -294,7 +294,7 @@ class _BottomBar extends ConsumerWidget { context, builder: (context) => AnalysisScreen( title: context.l10n.gameAnalysis, - pgn: game.makePgn(), + pgnOrId: game.makePgn(), options: AnalysisOptions( isLocalEvaluationAllowed: true, variant: gameData.variant, diff --git a/lib/src/view/game/game_body.dart b/lib/src/view/game/game_body.dart index 97e8af92ad..322c7e0528 100644 --- a/lib/src/view/game/game_body.dart +++ b/lib/src/view/game/game_body.dart @@ -550,7 +550,7 @@ class _GameBottomBar extends ConsumerWidget { pushPlatformRoute( context, builder: (_) => AnalysisScreen( - pgn: gameState.analysisPgn, + pgnOrId: gameState.analysisPgn, options: gameState.analysisOptions, title: context.l10n.gameAnalysis, ), @@ -700,7 +700,7 @@ class _GameBottomBar extends ConsumerWidget { pushPlatformRoute( context, builder: (_) => AnalysisScreen( - pgn: gameState.analysisPgn, + pgnOrId: gameState.analysisPgn, options: gameState.analysisOptions.copyWith( isLocalEvaluationAllowed: false, ), diff --git a/lib/src/view/game/game_list_tile.dart b/lib/src/view/game/game_list_tile.dart index 3a4e9b9799..89dffb101b 100644 --- a/lib/src/view/game/game_list_tile.dart +++ b/lib/src/view/game/game_list_tile.dart @@ -3,6 +3,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/game/archived_game.dart'; @@ -10,13 +11,11 @@ import 'package:lichess_mobile/src/model/game/game_share_service.dart'; import 'package:lichess_mobile/src/model/game/game_status.dart'; import 'package:lichess_mobile/src/styles/lichess_colors.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/current_locale.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/share.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; import 'package:lichess_mobile/src/view/game/archived_game_screen.dart'; -import 'package:lichess_mobile/src/view/game/game_list_tile_providers.dart'; import 'package:lichess_mobile/src/view/game/game_screen.dart'; import 'package:lichess_mobile/src/view/game/status_l10n.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; @@ -26,6 +25,8 @@ import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/user_full_name.dart'; import 'package:timeago/timeago.dart' as timeago; +final _dateFormatter = DateFormat.yMMMd(Intl.getCurrentLocale()).add_Hm(); + /// A list tile that shows game info. class GameListTile extends StatelessWidget { const GameListTile({ @@ -109,8 +110,6 @@ class _ContextMenu extends ConsumerWidget { final orientation = mySide; final customColors = Theme.of(context).extension(); - final dateFormatter = - ref.withLocale((locale) => DateFormat.yMMMd(locale).add_Hm()); return DraggableScrollableSheet( initialChildSize: .7, @@ -179,7 +178,7 @@ class _ContextMenu extends ConsumerWidget { ), ), Text( - dateFormatter.format(game.lastMoveAt), + _dateFormatter.format(game.lastMoveAt), style: TextStyle( color: textShade( context, @@ -239,12 +238,15 @@ class _ContextMenu extends ConsumerWidget { ? () { pushPlatformRoute( context, - builder: (context) => AnalysisLoadingScreen( - pgnAndOptionsProvider: archivedGameAnalysisProvider( - id: game.id, + builder: (context) => AnalysisScreen( + title: context.l10n.gameAnalysis, + pgnOrId: game.id.value, + options: AnalysisOptions( + isLocalEvaluationAllowed: true, + variant: game.variant, orientation: orientation, + id: game.id, ), - title: context.l10n.gameAnalysis, ), ); } diff --git a/lib/src/view/game/game_list_tile_providers.dart b/lib/src/view/game/game_list_tile_providers.dart deleted file mode 100644 index 517931c002..0000000000 --- a/lib/src/view/game/game_list_tile_providers.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:dartchess/dartchess.dart'; -import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; -import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/model/game/game_repository_providers.dart'; -import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'game_list_tile_providers.g.dart'; - -@riverpod -Future archivedGameAnalysis( - ArchivedGameAnalysisRef ref, { - required GameId id, - required Side orientation, -}) async { - final game = await ref.watch(archivedGameProvider(id: id).future); - final serverAnalysis = - game.white.analysis != null && game.black.analysis != null - ? (white: game.white.analysis!, black: game.black.analysis!) - : null; - - return ( - options: AnalysisOptions( - id: game.id, - isLocalEvaluationAllowed: true, - orientation: orientation, - variant: game.meta.variant, - opening: game.meta.opening, - division: game.meta.division, - serverAnalysis: serverAnalysis, - ), - pgn: game.makePgn(), - ); -} diff --git a/lib/src/view/game/game_result_dialog.dart b/lib/src/view/game/game_result_dialog.dart index d219a18dc2..4c3dd5da43 100644 --- a/lib/src/view/game/game_result_dialog.dart +++ b/lib/src/view/game/game_result_dialog.dart @@ -211,7 +211,7 @@ class _GameEndDialogState extends ConsumerState { pushPlatformRoute( context, builder: (_) => AnalysisScreen( - pgn: gameState.analysisPgn, + pgnOrId: gameState.analysisPgn, options: gameState.analysisOptions, title: context.l10n.gameAnalysis, ), diff --git a/lib/src/view/puzzle/puzzle_history_screen.dart b/lib/src/view/puzzle/puzzle_history_screen.dart index 2f6af5037f..be900aaea4 100644 --- a/lib/src/view/puzzle/puzzle_history_screen.dart +++ b/lib/src/view/puzzle/puzzle_history_screen.dart @@ -10,7 +10,6 @@ import 'package:lichess_mobile/src/model/puzzle/puzzle_activity.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_angle.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_theme.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/current_locale.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; @@ -20,6 +19,8 @@ import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:timeago/timeago.dart' as timeago; +final _dateFormatter = DateFormat.yMMMd(Intl.getCurrentLocale()); + class PuzzleHistoryScreen extends StatelessWidget { @override Widget build(BuildContext context) { @@ -127,7 +128,6 @@ class _BodyState extends ConsumerState<_Body> { @override Widget build(BuildContext context) { final historyState = ref.watch(puzzleActivityProvider); - final dateFormatter = ref.withLocale((locale) => DateFormat.yMMMd(locale)); return historyState.when( data: (state) { @@ -182,7 +182,7 @@ class _BodyState extends ConsumerState<_Body> { ); } else if (element is DateTime) { final title = DateTime.now().difference(element).inDays >= 15 - ? dateFormatter.format(element) + ? _dateFormatter.format(element) : timeago.format(element); return Padding( padding: const EdgeInsets.only(left: _kPuzzlePadding) diff --git a/lib/src/view/puzzle/puzzle_screen.dart b/lib/src/view/puzzle/puzzle_screen.dart index ce3a5188e6..c92902c0d5 100644 --- a/lib/src/view/puzzle/puzzle_screen.dart +++ b/lib/src/view/puzzle/puzzle_screen.dart @@ -536,7 +536,7 @@ class _BottomBar extends ConsumerWidget { context, builder: (context) => AnalysisScreen( title: context.l10n.analysis, - pgn: ref.read(ctrlProvider.notifier).makePgn(), + pgnOrId: ref.read(ctrlProvider.notifier).makePgn(), options: AnalysisOptions( isLocalEvaluationAllowed: true, variant: Variant.standard, diff --git a/lib/src/view/puzzle/streak_screen.dart b/lib/src/view/puzzle/streak_screen.dart index e053991e29..cbf343815c 100644 --- a/lib/src/view/puzzle/streak_screen.dart +++ b/lib/src/view/puzzle/streak_screen.dart @@ -319,7 +319,7 @@ class _BottomBar extends ConsumerWidget { context, builder: (context) => AnalysisScreen( title: context.l10n.analysis, - pgn: ref.read(ctrlProvider.notifier).makePgn(), + pgnOrId: ref.read(ctrlProvider.notifier).makePgn(), options: AnalysisOptions( isLocalEvaluationAllowed: true, variant: Variant.standard, diff --git a/lib/src/view/tools/load_position_screen.dart b/lib/src/view/tools/load_position_screen.dart index 75375b90b5..58e8a8e203 100644 --- a/lib/src/view/tools/load_position_screen.dart +++ b/lib/src/view/tools/load_position_screen.dart @@ -104,7 +104,7 @@ class _BodyState extends State<_Body> { context, rootNavigator: true, builder: (context) => AnalysisScreen( - pgn: parsedInput!.pgn, + pgnOrId: parsedInput!.pgn, options: parsedInput!.options, ), ) diff --git a/lib/src/view/tools/tools_tab_screen.dart b/lib/src/view/tools/tools_tab_screen.dart index 3ce37fc2ea..97a6562c6f 100644 --- a/lib/src/view/tools/tools_tab_screen.dart +++ b/lib/src/view/tools/tools_tab_screen.dart @@ -123,7 +123,7 @@ class _Body extends StatelessWidget { context, rootNavigator: true, builder: (context) => const AnalysisScreen( - pgn: '', + pgnOrId: '', options: AnalysisOptions( isLocalEvaluationAllowed: true, variant: Variant.standard, diff --git a/test/view/analysis/analysis_screen_test.dart b/test/view/analysis/analysis_screen_test.dart index c59047353a..45cc239ca9 100644 --- a/test/view/analysis/analysis_screen_test.dart +++ b/test/view/analysis/analysis_screen_test.dart @@ -32,7 +32,7 @@ void main() { final app = await buildTestApp( tester, home: AnalysisScreen( - pgn: sanMoves, + pgnOrId: sanMoves, options: AnalysisOptions( isLocalEvaluationAllowed: false, variant: Variant.standard, @@ -59,7 +59,7 @@ void main() { final app = await buildTestApp( tester, home: AnalysisScreen( - pgn: sanMoves, + pgnOrId: sanMoves, options: AnalysisOptions( isLocalEvaluationAllowed: false, variant: Variant.standard, From 996b7e76396cba5445fb886cc618b6b6ea763499 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Sun, 1 Sep 2024 15:01:49 +0200 Subject: [PATCH 185/979] update comments and hide eval bar until it is ready --- lib/src/view/broadcast/broadcast_boards_tab.dart | 13 ++++++++++--- lib/src/view/broadcast/broadcast_screen.dart | 3 ++- lib/src/view/broadcast/broadcasts_list_screen.dart | 4 ++-- lib/src/view/watch/watch_tab_screen.dart | 4 ---- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_boards_tab.dart b/lib/src/view/broadcast/broadcast_boards_tab.dart index 7fe3036305..b164790a10 100644 --- a/lib/src/view/broadcast/broadcast_boards_tab.dart +++ b/lib/src/view/broadcast/broadcast_boards_tab.dart @@ -6,6 +6,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; +// TODO remove when eval bar is ready +// ignore: unused_import import 'package:lichess_mobile/src/model/broadcast/broadcast_preferences.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_round_controller.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; @@ -85,9 +87,12 @@ class BroadcastPreview extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final showEvaluationBar = ref.watch( - broadcastPreferencesProvider.select((value) => value.showEvaluationBar), - ); + // TODO uncomment when eval bar is ready + // final showEvaluationBar = ref.watch( + // broadcastPreferencesProvider.select((value) => value.showEvaluationBar), + // ); + // TODO remove when eval bar is ready + const showEvaluationBar = false; const numberLoadingBoards = 12; const boardSpacing = 10.0; // height of the text based on the font size @@ -115,6 +120,8 @@ class BroadcastPreview extends ConsumerWidget { itemBuilder: (context, index) { final boardSize = boardWithMaybeEvalBarWidth - (showEvaluationBar + // TODO remove when eval bar is ready + // ignore: dead_code ? evaluationBarAspectRatio * boardWithMaybeEvalBarWidth : 0); diff --git a/lib/src/view/broadcast/broadcast_screen.dart b/lib/src/view/broadcast/broadcast_screen.dart index 979cdc737d..151fcbff88 100644 --- a/lib/src/view/broadcast/broadcast_screen.dart +++ b/lib/src/view/broadcast/broadcast_screen.dart @@ -89,7 +89,8 @@ class _AndroidScreenState extends State<_AndroidScreen> Tab(text: 'Boards'), ], ), - actions: [_BroadcastSettingsButton()], + // TODO uncomment when eval bar is ready + // actions: [_BroadcastSettingsButton()], ), body: TabBarView( controller: _tabController, diff --git a/lib/src/view/broadcast/broadcasts_list_screen.dart b/lib/src/view/broadcast/broadcasts_list_screen.dart index cc93cabebd..786d0eca07 100644 --- a/lib/src/view/broadcast/broadcasts_list_screen.dart +++ b/lib/src/view/broadcast/broadcasts_list_screen.dart @@ -332,8 +332,8 @@ class StartsRoundDate extends ConsumerWidget { return Text( timeBeforeRound.inDays == 0 ? timeBeforeRound.inHours == 0 - ? 'In ${timeBeforeRound.inMinutes} minutes' // TODO: translate with https://github.com/lichess-org/lila/blob/65b28ea8e43e0133df6c7ed40e03c2954f247d1e/translation/source/timeago.xml#L8 - : 'In ${timeBeforeRound.inHours} hours' // TODO: translate with https://github.com/lichess-org/lila/blob/65b28ea8e43e0133df6c7ed40e03c2954f247d1e/translation/source/timeago.xml#L12 + ? 'In ${timeBeforeRound.inMinutes} minutes' // TODO translate with https://github.com/lichess-org/lila/blob/65b28ea8e43e0133df6c7ed40e03c2954f247d1e/translation/source/timeago.xml#L8 + : 'In ${timeBeforeRound.inHours} hours' // TODO translate with https://github.com/lichess-org/lila/blob/65b28ea8e43e0133df6c7ed40e03c2954f247d1e/translation/source/timeago.xml#L12 : timeBeforeRound.inDays < 365 ? dateFormatter.format(startsAt) : dateFormatterWithYear.format(startsAt), diff --git a/lib/src/view/watch/watch_tab_screen.dart b/lib/src/view/watch/watch_tab_screen.dart index fbdeca25d3..71de58ac64 100644 --- a/lib/src/view/watch/watch_tab_screen.dart +++ b/lib/src/view/watch/watch_tab_screen.dart @@ -88,7 +88,6 @@ class _WatchScreenState extends ConsumerState { } List get watchTabWidgets => const [ - // TODO: show widget when broadcasts feature is ready _BroadcastWidget(), _WatchTvWidget(), _StreamerWidget(), @@ -179,15 +178,12 @@ class _WatchScreenState extends ConsumerState { Future _refreshData(WidgetRef ref) { return Future.wait([ - // TODO uncomment when broadcasts feature is ready ref.refresh(broadcastsPaginatorProvider.future), ref.refresh(featuredChannelsProvider.future), ref.refresh(liveStreamersProvider.future), ]); } -// TODO remove this ignore comment when broadcasts feature is ready -// ignore: unused_element class _BroadcastWidget extends ConsumerWidget { const _BroadcastWidget(); From 98786a87a8ed76e4f5c2742c9e5a6ab8c040900e Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Thu, 22 Aug 2024 20:55:15 +0200 Subject: [PATCH 186/979] refactor: add common widget for BottomBar This gets rid of a lot code duplication and makes it more straight-forward to build new screens for new features. Note that the Row in BottomBar now has MainAxisAlignment.spaceAround by default, so it's not necessary anymore to wrap buttons in `Expanded`. --- lib/src/view/analysis/analysis_screen.dart | 123 +++--- .../board_editor/board_editor_screen.dart | 126 +++--- .../offline_correspondence_game_screen.dart | 189 ++++----- lib/src/view/game/archived_game_screen.dart | 176 ++++----- lib/src/view/game/game_body.dart | 360 ++++++++---------- lib/src/view/game/game_loading_board.dart | 36 +- .../opening_explorer_screen.dart | 101 ++--- lib/src/view/puzzle/puzzle_screen.dart | 171 ++++----- lib/src/view/puzzle/storm_screen.dart | 88 ++--- lib/src/view/puzzle/streak_screen.dart | 179 ++++----- lib/src/view/watch/tv_screen.dart | 111 +++--- lib/src/widgets/bottom_bar.dart | 38 ++ 12 files changed, 746 insertions(+), 952 deletions(-) create mode 100644 lib/src/widgets/bottom_bar.dart diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index e2e51c0e5a..ba1753442e 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -33,6 +33,7 @@ import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_screen.dart'; import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; +import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; @@ -590,81 +591,57 @@ class _BottomBar extends ConsumerWidget { final canShowGameSummary = ref.watch(ctrlProvider.select((value) => value.canShowGameSummary)); - return Container( - color: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoTheme.of(context).barBackgroundColor - : Theme.of(context).bottomAppBarTheme.color, - child: SafeArea( - top: false, - child: SizedBox( - height: kBottomBarHeight, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - Expanded( - child: BottomBarButton( - label: context.l10n.menu, - onTap: () { - _showAnalysisMenu(context, ref); - }, - icon: Icons.menu, - ), - ), - if (canShowGameSummary) - Expanded( - child: BottomBarButton( - label: displayMode == DisplayMode.summary - ? 'Moves' - : 'Summary', - onTap: () { - ref.read(ctrlProvider.notifier).toggleDisplayMode(); - }, - icon: displayMode == DisplayMode.summary - ? LichessIcons.flow_cascade - : Icons.area_chart, - ), - ), - Expanded( - child: BottomBarButton( - label: context.l10n.openingExplorer, - onTap: () => pushPlatformRoute( - context, - builder: (_) => OpeningExplorerScreen( - pgn: pgn, - options: options, - ), - ), - icon: Icons.explore, - ), - ), - Expanded( - child: RepeatButton( - onLongPress: canGoBack ? () => _moveBackward(ref) : null, - child: BottomBarButton( - key: const ValueKey('goto-previous'), - onTap: canGoBack ? () => _moveBackward(ref) : null, - label: 'Previous', - icon: CupertinoIcons.chevron_back, - showTooltip: false, - ), - ), - ), - Expanded( - child: RepeatButton( - onLongPress: canGoNext ? () => _moveForward(ref) : null, - child: BottomBarButton( - key: const ValueKey('goto-next'), - icon: CupertinoIcons.chevron_forward, - label: context.l10n.next, - onTap: canGoNext ? () => _moveForward(ref) : null, - showTooltip: false, - ), - ), - ), - ], + return BottomBar( + children: [ + BottomBarButton( + label: context.l10n.menu, + onTap: () { + _showAnalysisMenu(context, ref); + }, + icon: Icons.menu, + ), + if (canShowGameSummary) + BottomBarButton( + label: displayMode == DisplayMode.summary ? 'Moves' : 'Summary', + onTap: () { + ref.read(ctrlProvider.notifier).toggleDisplayMode(); + }, + icon: displayMode == DisplayMode.summary + ? LichessIcons.flow_cascade + : Icons.area_chart, ), + BottomBarButton( + label: context.l10n.openingExplorer, + onTap: () => pushPlatformRoute( + context, + builder: (_) => OpeningExplorerScreen( + pgn: pgn, + options: options, + ), + ), + icon: Icons.explore, ), - ), + RepeatButton( + onLongPress: canGoBack ? () => _moveBackward(ref) : null, + child: BottomBarButton( + key: const ValueKey('goto-previous'), + onTap: canGoBack ? () => _moveBackward(ref) : null, + label: 'Previous', + icon: CupertinoIcons.chevron_back, + showTooltip: false, + ), + ), + RepeatButton( + onLongPress: canGoNext ? () => _moveForward(ref) : null, + child: BottomBarButton( + key: const ValueKey('goto-next'), + icon: CupertinoIcons.chevron_forward, + label: context.l10n.next, + onTap: canGoNext ? () => _moveForward(ref) : null, + showTooltip: false, + ), + ), + ], ); } diff --git a/lib/src/view/board_editor/board_editor_screen.dart b/lib/src/view/board_editor/board_editor_screen.dart index 7d62fada35..b08aff7767 100644 --- a/lib/src/view/board_editor/board_editor_screen.dart +++ b/lib/src/view/board_editor/board_editor_screen.dart @@ -16,6 +16,7 @@ import 'package:lichess_mobile/src/utils/share.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; import 'package:lichess_mobile/src/view/board_editor/board_editor_menu.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; +import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; @@ -318,84 +319,59 @@ class _BottomBar extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final editorState = ref.watch(boardEditorControllerProvider(initialFen)); - return Container( - color: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoDynamicColor.resolve( - CupertinoColors.tertiarySystemGroupedBackground, - context, - ) - : Theme.of(context).bottomAppBarTheme.color, - child: SafeArea( - top: false, - child: SizedBox( - height: kBottomBarHeight, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - Expanded( - child: BottomBarButton( - label: context.l10n.menu, - onTap: () => showAdaptiveBottomSheet( - context: context, - builder: (BuildContext context) => BoardEditorMenu( - initialFen: initialFen, - ), - ), - icon: Icons.tune, - ), - ), - Expanded( - child: BottomBarButton( - key: const Key('flip-button'), - label: context.l10n.flipBoard, - onTap: ref - .read(boardEditorControllerProvider(initialFen).notifier) - .flipBoard, - icon: CupertinoIcons.arrow_2_squarepath, - ), - ), - Expanded( - child: BottomBarButton( - label: context.l10n.analysis, - key: const Key('analysis-board-button'), - onTap: editorState.pgn != null - ? () { - pushPlatformRoute( - context, - rootNavigator: true, - builder: (context) => AnalysisScreen( - pgnOrId: editorState.pgn!, - options: AnalysisOptions( - isLocalEvaluationAllowed: true, - variant: Variant.fromPosition, - orientation: editorState.orientation, - id: standaloneAnalysisId, - ), - ), - ); - } - : null, - icon: Icons.biotech, - ), - ), - Expanded( - child: BottomBarButton( - label: context.l10n.mobileSharePositionAsFEN, - onTap: () => launchShareDialog( + return BottomBar( + children: [ + BottomBarButton( + label: context.l10n.menu, + onTap: () => showAdaptiveBottomSheet( + context: context, + builder: (BuildContext context) => BoardEditorMenu( + initialFen: initialFen, + ), + ), + icon: Icons.tune, + ), + BottomBarButton( + key: const Key('flip-button'), + label: context.l10n.flipBoard, + onTap: ref + .read(boardEditorControllerProvider(initialFen).notifier) + .flipBoard, + icon: CupertinoIcons.arrow_2_squarepath, + ), + BottomBarButton( + label: context.l10n.analysis, + key: const Key('analysis-board-button'), + onTap: editorState.pgn != null + ? () { + pushPlatformRoute( context, - text: editorState.fen, - ), - icon: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoIcons.share - : Icons.share, - ), - ), - const SizedBox(height: 26.0), - const SizedBox(height: 20.0), - ], + rootNavigator: true, + builder: (context) => AnalysisScreen( + pgnOrId: editorState.pgn!, + options: AnalysisOptions( + isLocalEvaluationAllowed: true, + variant: Variant.fromPosition, + orientation: editorState.orientation, + id: standaloneAnalysisId, + ), + ), + ); + } + : null, + icon: Icons.biotech, + ), + BottomBarButton( + label: context.l10n.mobileSharePositionAsFEN, + onTap: () => launchShareDialog( + context, + text: editorState.fen, ), + icon: Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoIcons.share + : Icons.share, ), - ), + ], ); } } diff --git a/lib/src/view/correspondence/offline_correspondence_game_screen.dart b/lib/src/view/correspondence/offline_correspondence_game_screen.dart index 426ff94aac..f1a2c0801e 100644 --- a/lib/src/view/correspondence/offline_correspondence_game_screen.dart +++ b/lib/src/view/correspondence/offline_correspondence_game_screen.dart @@ -4,7 +4,6 @@ import 'package:dartchess/dartchess.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/service/move_feedback.dart'; @@ -22,6 +21,7 @@ import 'package:lichess_mobile/src/view/game/correspondence_clock_widget.dart'; import 'package:lichess_mobile/src/view/game/game_player.dart'; import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; import 'package:lichess_mobile/src/widgets/board_table.dart'; +import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; @@ -257,115 +257,92 @@ class _BodyState extends ConsumerState<_Body> { ), ), ), - Container( - color: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoTheme.of(context).barBackgroundColor - : Theme.of(context).bottomAppBarTheme.color, - child: SafeArea( - top: false, - child: SizedBox( - height: kBottomBarHeight, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - Expanded( - child: BottomBarButton( - label: context.l10n.flipBoard, - onTap: () { - setState(() { - isBoardTurned = !isBoardTurned; - }); - }, - icon: CupertinoIcons.arrow_2_squarepath, - ), - ), - Expanded( - child: BottomBarButton( - label: context.l10n.analysis, - onTap: () { - pushPlatformRoute( - context, - builder: (_) => AnalysisScreen( - pgnOrId: game.makePgn(), - options: AnalysisOptions( - isLocalEvaluationAllowed: false, - variant: game.variant, - initialMoveCursor: stepCursor, - orientation: game.youAre, - id: game.id, - division: game.meta.division, - ), - title: context.l10n.analysis, - ), - ); - }, - icon: Icons.biotech, - ), - ), - Expanded( - child: BottomBarButton( - label: 'Go to the next game', - icon: Icons.skip_next, - onTap: offlineOngoingGames.maybeWhen( - data: (games) { - final nextTurn = games - .whereNot((g) => g.$2.id == game.id) - .firstWhereOrNull((g) => g.$2.isPlayerTurn); - return nextTurn != null - ? () { - widget.onGameChanged(nextTurn); - } - : null; - }, - orElse: () => null, - ), - ), - ), - Expanded( - child: BottomBarButton( - label: context.l10n.mobileCorrespondenceClearSavedMove, - onTap: game.registeredMoveAtPgn != null - ? () { - showConfirmDialog( - context, - title: Text( - context - .l10n.mobileCorrespondenceClearSavedMove, - ), - isDestructiveAction: true, - onConfirm: (_) => deleteRegisteredMove(), - ); - } - : null, - icon: Icons.save, - ), - ), - Expanded( - child: RepeatButton( - onLongPress: canGoBackward ? () => moveBackward() : null, - child: BottomBarButton( - onTap: canGoBackward ? () => moveBackward() : null, - label: 'Previous', - icon: CupertinoIcons.chevron_back, - showTooltip: false, - ), - ), - ), - Expanded( - child: RepeatButton( - onLongPress: canGoForward ? () => moveForward() : null, - child: BottomBarButton( - onTap: canGoForward ? () => moveForward() : null, - label: context.l10n.next, - icon: CupertinoIcons.chevron_forward, - showTooltip: false, - ), + BottomBar( + children: [ + BottomBarButton( + label: context.l10n.flipBoard, + onTap: () { + setState(() { + isBoardTurned = !isBoardTurned; + }); + }, + icon: CupertinoIcons.arrow_2_squarepath, + ), + BottomBarButton( + label: context.l10n.analysis, + onTap: () { + pushPlatformRoute( + context, + builder: (_) => AnalysisScreen( + pgnOrId: game.makePgn(), + options: AnalysisOptions( + isLocalEvaluationAllowed: false, + variant: game.variant, + initialMoveCursor: stepCursor, + orientation: game.youAre, + id: game.id, + division: game.meta.division, ), + title: context.l10n.analysis, ), - ], + ); + }, + icon: Icons.biotech, + ), + BottomBarButton( + label: 'Go to the next game', + icon: Icons.skip_next, + onTap: offlineOngoingGames.maybeWhen( + data: (games) { + final nextTurn = games + .whereNot((g) => g.$2.id == game.id) + .firstWhereOrNull((g) => g.$2.isPlayerTurn); + return nextTurn != null + ? () { + widget.onGameChanged(nextTurn); + } + : null; + }, + orElse: () => null, ), ), - ), + BottomBarButton( + label: context.l10n.mobileCorrespondenceClearSavedMove, + onTap: game.registeredMoveAtPgn != null + ? () { + showConfirmDialog( + context, + title: Text( + context.l10n.mobileCorrespondenceClearSavedMove, + ), + isDestructiveAction: true, + onConfirm: (_) => deleteRegisteredMove(), + ); + } + : null, + icon: Icons.save, + ), + RepeatButton( + onLongPress: canGoBackward ? () => moveBackward() : null, + child: BottomBarButton( + onTap: canGoBackward ? () => moveBackward() : null, + label: 'Previous', + icon: CupertinoIcons.chevron_back, + showTooltip: false, + ), + ), + Expanded( + child: RepeatButton( + onLongPress: canGoForward ? () => moveForward() : null, + child: BottomBarButton( + onTap: canGoForward ? () => moveForward() : null, + label: context.l10n.next, + icon: CupertinoIcons.chevron_forward, + showTooltip: false, + ), + ), + ), + ], ), ], ); diff --git a/lib/src/view/game/archived_game_screen.dart b/lib/src/view/game/archived_game_screen.dart index a6493d7d5c..ee170b88b0 100644 --- a/lib/src/view/game/archived_game_screen.dart +++ b/lib/src/view/game/archived_game_screen.dart @@ -3,7 +3,6 @@ import 'package:dartchess/dartchess.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/game/archived_game.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; @@ -16,6 +15,7 @@ import 'package:lichess_mobile/src/view/game/game_result_dialog.dart'; import 'package:lichess_mobile/src/view/settings/toggle_sound_button.dart'; import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; import 'package:lichess_mobile/src/widgets/board_table.dart'; +import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/countdown_clock.dart'; @@ -240,109 +240,89 @@ class _BottomBar extends ConsumerWidget { final canGoBackward = ref.watch(canGoBackwardProvider(gameData.id)); final gameCursor = ref.watch(gameCursorProvider(gameData.id)); - return Container( - color: Theme.of(context).platform == TargetPlatform.iOS - ? null - : Theme.of(context).bottomAppBarTheme.color, - child: SafeArea( - top: false, - child: SizedBox( - height: kBottomBarHeight, - child: Row( - children: [ - Expanded( - child: BottomBarButton( - label: context.l10n.menu, - onTap: () { - _showGameMenu(context, ref); - }, - icon: Icons.menu, - ), - ), - gameCursor.when( - data: (data) { - return Expanded( - child: BottomBarButton( - label: context.l10n.mobileShowResult, - icon: Icons.info_outline, - onTap: () { - showAdaptiveDialog( - context: context, - builder: (context) => - ArchivedGameResultDialog(game: data.$1), - barrierDismissible: true, - ); - }, - ), + return BottomBar( + children: [ + BottomBarButton( + label: context.l10n.menu, + onTap: () { + _showGameMenu(context, ref); + }, + icon: Icons.menu, + ), + gameCursor.when( + data: (data) { + return Expanded( + child: BottomBarButton( + label: context.l10n.mobileShowResult, + icon: Icons.info_outline, + onTap: () { + showAdaptiveDialog( + context: context, + builder: (context) => + ArchivedGameResultDialog(game: data.$1), + barrierDismissible: true, ); }, - loading: () => const SizedBox.shrink(), - error: (_, __) => const SizedBox.shrink(), ), - Expanded( - child: BottomBarButton( - label: context.l10n.gameAnalysis, - onTap: ref.read(gameCursorProvider(gameData.id)).hasValue - ? () { - final (game, cursor) = ref - .read( - gameCursorProvider(gameData.id), - ) - .requireValue; + ); + }, + loading: () => const SizedBox.shrink(), + error: (_, __) => const SizedBox.shrink(), + ), + BottomBarButton( + label: context.l10n.gameAnalysis, + onTap: ref.read(gameCursorProvider(gameData.id)).hasValue + ? () { + final (game, cursor) = ref + .read( + gameCursorProvider(gameData.id), + ) + .requireValue; - pushPlatformRoute( - context, - builder: (context) => AnalysisScreen( - title: context.l10n.gameAnalysis, - pgnOrId: game.makePgn(), - options: AnalysisOptions( - isLocalEvaluationAllowed: true, - variant: gameData.variant, - initialMoveCursor: cursor, - orientation: orientation, - id: gameData.id, - opening: gameData.opening, - serverAnalysis: game.serverAnalysis, - division: game.meta.division, - ), - ), - ); - } - : null, - icon: Icons.biotech, - ), - ), - Expanded( - child: RepeatButton( - onLongPress: - canGoBackward ? () => _cursorBackward(ref) : null, - child: BottomBarButton( - key: const ValueKey('cursor-back'), - // TODO add translation - label: 'Backward', - showTooltip: false, - onTap: canGoBackward ? () => _cursorBackward(ref) : null, - icon: CupertinoIcons.chevron_back, - ), - ), - ), - Expanded( - child: RepeatButton( - onLongPress: canGoForward ? () => _cursorForward(ref) : null, - child: BottomBarButton( - key: const ValueKey('cursor-forward'), - // TODO add translation - label: 'Forward', - showTooltip: false, - onTap: canGoForward ? () => _cursorForward(ref) : null, - icon: CupertinoIcons.chevron_forward, - ), - ), - ), - ], + pushPlatformRoute( + context, + builder: (context) => AnalysisScreen( + title: context.l10n.gameAnalysis, + pgnOrId: game.makePgn(), + options: AnalysisOptions( + isLocalEvaluationAllowed: true, + variant: gameData.variant, + initialMoveCursor: cursor, + orientation: orientation, + id: gameData.id, + opening: gameData.opening, + serverAnalysis: game.serverAnalysis, + division: game.meta.division, + ), + ), + ); + } + : null, + icon: Icons.biotech, + ), + RepeatButton( + onLongPress: canGoBackward ? () => _cursorBackward(ref) : null, + child: BottomBarButton( + key: const ValueKey('cursor-back'), + // TODO add translation + label: 'Backward', + showTooltip: false, + onTap: canGoBackward ? () => _cursorBackward(ref) : null, + icon: CupertinoIcons.chevron_back, ), ), - ), + RepeatButton( + onLongPress: canGoForward ? () => _cursorForward(ref) : null, + child: BottomBarButton( + key: const ValueKey('cursor-forward'), + // TODO add translation + label: 'Forward', + showTooltip: false, + onTap: canGoForward ? () => _cursorForward(ref) : null, + icon: CupertinoIcons.chevron_forward, + ), + ), + ], ); } diff --git a/lib/src/view/game/game_body.dart b/lib/src/view/game/game_body.dart index 322c7e0528..cf3cb8a743 100644 --- a/lib/src/view/game/game_body.dart +++ b/lib/src/view/game/game_body.dart @@ -7,7 +7,6 @@ import 'package:dartchess/dartchess.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/account/account_preferences.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; @@ -26,6 +25,7 @@ import 'package:lichess_mobile/src/view/game/correspondence_clock_widget.dart'; import 'package:lichess_mobile/src/view/game/message_screen.dart'; import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; import 'package:lichess_mobile/src/widgets/board_table.dart'; +import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/countdown_clock.dart'; @@ -443,212 +443,189 @@ class _GameBottomBar extends ConsumerWidget { : null; return [ - Expanded( - child: BottomBarButton( - label: context.l10n.menu, - onTap: () { - _showGameMenu(context, ref); - }, - icon: Icons.menu, - ), + BottomBarButton( + label: context.l10n.menu, + onTap: () { + _showGameMenu(context, ref); + }, + icon: Icons.menu, ), if (!gameState.game.playable) - Expanded( - child: BottomBarButton( - label: context.l10n.mobileShowResult, - onTap: () { - showAdaptiveDialog( - context: context, - builder: (context) => GameResultDialog( - id: id, - onNewOpponentCallback: onNewOpponentCallback, - ), - barrierDismissible: true, - ); - }, - icon: Icons.info_outline, - ), + BottomBarButton( + label: context.l10n.mobileShowResult, + onTap: () { + showAdaptiveDialog( + context: context, + builder: (context) => GameResultDialog( + id: id, + onNewOpponentCallback: onNewOpponentCallback, + ), + barrierDismissible: true, + ); + }, + icon: Icons.info_outline, ), if (gameState.game.playable && gameState.game.opponent?.offeringDraw == true) - Expanded( - child: BottomBarButton( - label: context.l10n.yourOpponentOffersADraw, - highlighted: true, - onTap: () { - showAdaptiveDialog( - context: context, - builder: (context) => _GameNegotiationDialog( - title: Text(context.l10n.yourOpponentOffersADraw), - onAccept: () { - ref - .read(gameControllerProvider(id).notifier) - .offerOrAcceptDraw(); - }, - onDecline: () { - ref - .read(gameControllerProvider(id).notifier) - .cancelOrDeclineDraw(); - }, - ), - barrierDismissible: true, - ); - }, - icon: Icons.handshake_outlined, - ), + BottomBarButton( + label: context.l10n.yourOpponentOffersADraw, + highlighted: true, + onTap: () { + showAdaptiveDialog( + context: context, + builder: (context) => _GameNegotiationDialog( + title: Text(context.l10n.yourOpponentOffersADraw), + onAccept: () { + ref + .read(gameControllerProvider(id).notifier) + .offerOrAcceptDraw(); + }, + onDecline: () { + ref + .read(gameControllerProvider(id).notifier) + .cancelOrDeclineDraw(); + }, + ), + barrierDismissible: true, + ); + }, + icon: Icons.handshake_outlined, ) else if (gameState.game.playable && gameState.game.isThreefoldRepetition == true) - Expanded( - child: BottomBarButton( - label: context.l10n.threefoldRepetition, - highlighted: true, - onTap: () { - showAdaptiveDialog( - context: context, - builder: (context) => _ThreefoldDialog(id: id), - barrierDismissible: true, - ); - }, - icon: Icons.handshake_outlined, - ), + BottomBarButton( + label: context.l10n.threefoldRepetition, + highlighted: true, + onTap: () { + showAdaptiveDialog( + context: context, + builder: (context) => _ThreefoldDialog(id: id), + barrierDismissible: true, + ); + }, + icon: Icons.handshake_outlined, ) else if (gameState.game.playable && gameState.game.opponent?.proposingTakeback == true) - Expanded( - child: BottomBarButton( - label: context.l10n.yourOpponentProposesATakeback, - highlighted: true, - onTap: () { - showAdaptiveDialog( - context: context, - builder: (context) => _GameNegotiationDialog( - title: Text(context.l10n.yourOpponentProposesATakeback), - onAccept: () { - ref - .read(gameControllerProvider(id).notifier) - .acceptTakeback(); - }, - onDecline: () { - ref - .read(gameControllerProvider(id).notifier) - .cancelOrDeclineTakeback(); - }, - ), - barrierDismissible: true, - ); - }, - icon: CupertinoIcons.arrowshape_turn_up_left, - ), + BottomBarButton( + label: context.l10n.yourOpponentProposesATakeback, + highlighted: true, + onTap: () { + showAdaptiveDialog( + context: context, + builder: (context) => _GameNegotiationDialog( + title: Text(context.l10n.yourOpponentProposesATakeback), + onAccept: () { + ref + .read(gameControllerProvider(id).notifier) + .acceptTakeback(); + }, + onDecline: () { + ref + .read(gameControllerProvider(id).notifier) + .cancelOrDeclineTakeback(); + }, + ), + barrierDismissible: true, + ); + }, + icon: CupertinoIcons.arrowshape_turn_up_left, ) else if (gameState.game.finished) - Expanded( - child: BottomBarButton( - label: context.l10n.gameAnalysis, - icon: Icons.biotech, - onTap: () { - pushPlatformRoute( - context, - builder: (_) => AnalysisScreen( - pgnOrId: gameState.analysisPgn, - options: gameState.analysisOptions, - title: context.l10n.gameAnalysis, - ), - ); - }, - ), + BottomBarButton( + label: context.l10n.gameAnalysis, + icon: Icons.biotech, + onTap: () { + pushPlatformRoute( + context, + builder: (_) => AnalysisScreen( + pgnOrId: gameState.analysisPgn, + options: gameState.analysisOptions, + title: context.l10n.gameAnalysis, + ), + ); + }, ) else - Expanded( - child: BottomBarButton( - label: context.l10n.resign, - onTap: gameState.game.resignable - ? gameState.shouldConfirmResignAndDrawOffer - ? () => _showConfirmDialog( - context, - description: Text(context.l10n.resignTheGame), - onConfirm: () { - ref - .read(gameControllerProvider(id).notifier) - .resignGame(); - }, - ) - : () { - ref - .read(gameControllerProvider(id).notifier) - .resignGame(); - } - : null, - icon: Icons.flag, - ), + BottomBarButton( + label: context.l10n.resign, + onTap: gameState.game.resignable + ? gameState.shouldConfirmResignAndDrawOffer + ? () => _showConfirmDialog( + context, + description: Text(context.l10n.resignTheGame), + onConfirm: () { + ref + .read(gameControllerProvider(id).notifier) + .resignGame(); + }, + ) + : () { + ref + .read(gameControllerProvider(id).notifier) + .resignGame(); + } + : null, + icon: Icons.flag, ), if (gameState.game.meta.speed == Speed.correspondence && !gameState.game.finished) - Expanded( - child: BottomBarButton( - label: 'Go to the next game', - icon: Icons.skip_next, - onTap: ongoingGames.maybeWhen( - data: (games) { - final nextTurn = games - .whereNot((g) => g.fullId == id) - .firstWhereOrNull((g) => g.isMyTurn); - return nextTurn != null - ? () => onLoadGameCallback(nextTurn.fullId) - : null; - }, - orElse: () => null, - ), + BottomBarButton( + label: 'Go to the next game', + icon: Icons.skip_next, + onTap: ongoingGames.maybeWhen( + data: (games) { + final nextTurn = games + .whereNot((g) => g.fullId == id) + .firstWhereOrNull((g) => g.isMyTurn); + return nextTurn != null + ? () => onLoadGameCallback(nextTurn.fullId) + : null; + }, + orElse: () => null, ), ), - Expanded( - child: BottomBarButton( - label: context.l10n.chat, - onTap: isChatEnabled - ? () { - pushPlatformRoute( - context, - builder: (BuildContext context) { - return MessageScreen( - title: UserFullNameWidget( - user: gameState.game.opponent?.user, - ), - me: gameState.game.me?.user, - id: id, - ); - }, - ); - } - : null, - icon: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoIcons.chat_bubble - : Icons.chat_bubble_outline, - chip: chatUnreadChip, - ), + BottomBarButton( + label: context.l10n.chat, + onTap: isChatEnabled + ? () { + pushPlatformRoute( + context, + builder: (BuildContext context) { + return MessageScreen( + title: UserFullNameWidget( + user: gameState.game.opponent?.user, + ), + me: gameState.game.me?.user, + id: id, + ); + }, + ); + } + : null, + icon: Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoIcons.chat_bubble + : Icons.chat_bubble_outline, + chip: chatUnreadChip, ), - Expanded( - child: RepeatButton( - onLongPress: - gameState.canGoBackward ? () => _moveBackward(ref) : null, - child: BottomBarButton( - onTap: - gameState.canGoBackward ? () => _moveBackward(ref) : null, - label: 'Previous', - icon: CupertinoIcons.chevron_back, - showTooltip: false, - ), + RepeatButton( + onLongPress: + gameState.canGoBackward ? () => _moveBackward(ref) : null, + child: BottomBarButton( + onTap: gameState.canGoBackward ? () => _moveBackward(ref) : null, + label: 'Previous', + icon: CupertinoIcons.chevron_back, + showTooltip: false, ), ), - Expanded( - child: RepeatButton( - onLongPress: - gameState.canGoForward ? () => _moveForward(ref) : null, - child: BottomBarButton( - onTap: gameState.canGoForward ? () => _moveForward(ref) : null, - label: context.l10n.next, - icon: CupertinoIcons.chevron_forward, - showTooltip: false, - ), + RepeatButton( + onLongPress: + gameState.canGoForward ? () => _moveForward(ref) : null, + child: BottomBarButton( + onTap: gameState.canGoForward ? () => _moveForward(ref) : null, + label: context.l10n.next, + icon: CupertinoIcons.chevron_forward, + showTooltip: false, ), ), ]; @@ -657,19 +634,8 @@ class _GameBottomBar extends ConsumerWidget { error: (e, s) => [], ); - return Container( - color: Theme.of(context).platform == TargetPlatform.iOS - ? null - : Theme.of(context).bottomAppBarTheme.color, - child: SafeArea( - top: false, - child: SizedBox( - height: kBottomBarHeight, - child: Row( - children: children, - ), - ), - ), + return BottomBar( + children: children, ); } diff --git a/lib/src/view/game/game_loading_board.dart b/lib/src/view/game/game_loading_board.dart index dabe062ec5..e9b1565c8c 100644 --- a/lib/src/view/game/game_loading_board.dart +++ b/lib/src/view/game/game_loading_board.dart @@ -10,6 +10,7 @@ import 'package:lichess_mobile/src/model/lobby/lobby_numbers.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/widgets/board_table.dart'; +import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/user_full_name.dart'; @@ -73,7 +74,7 @@ class LobbyScreenLoadingContent extends StatelessWidget { ), ), ), - _BottomBar( + BottomBar( children: [ BottomBarButton( onTap: () async { @@ -148,7 +149,7 @@ class ChallengeLoadingContent extends StatelessWidget { ), ), ), - _BottomBar( + BottomBar( children: [ BottomBarButton( onTap: () async { @@ -221,7 +222,7 @@ class LoadGameError extends StatelessWidget { ), ), ), - _BottomBar( + BottomBar( children: [ BottomBarButton( onTap: () => Navigator.of(context).pop(), @@ -295,7 +296,7 @@ class ChallengeDeclinedBoard extends StatelessWidget { ), ), ), - _BottomBar( + BottomBar( children: [ BottomBarButton( onTap: () => Navigator.of(context).pop(), @@ -310,33 +311,6 @@ class ChallengeDeclinedBoard extends StatelessWidget { } } -class _BottomBar extends StatelessWidget { - const _BottomBar({ - required this.children, - }); - - final List children; - - @override - Widget build(BuildContext context) { - return Container( - color: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoTheme.of(context).barBackgroundColor - : Theme.of(context).bottomAppBarTheme.color, - child: SafeArea( - top: false, - child: SizedBox( - height: kBottomBarHeight, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: children, - ), - ), - ), - ); - } -} - class _LobbyNumbers extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index 814e160fca..d96c469d57 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -19,6 +19,7 @@ import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_board.dart'; import 'package:lichess_mobile/src/view/game/archived_game_screen.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; +import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; @@ -955,68 +956,48 @@ class _BottomBar extends ConsumerWidget { OpeningDatabase.player => context.l10n.player, }; - return Container( - color: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoTheme.of(context).barBackgroundColor - : Theme.of(context).bottomAppBarTheme.color, - child: SafeArea( - top: false, - child: SizedBox( - height: kBottomBarHeight, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - Expanded( - child: BottomBarButton( - label: dbLabel, - showLabel: true, - onTap: () => showAdaptiveBottomSheet( - context: context, - isScrollControlled: true, - showDragHandle: true, - isDismissible: true, - builder: (_) => OpeningExplorerSettings(pgn, options), - ), - icon: Icons.tune, - ), - ), - Expanded( - child: BottomBarButton( - label: 'Flip', - tooltip: context.l10n.flipBoard, - showLabel: true, - onTap: () => ref.read(ctrlProvider.notifier).toggleBoard(), - icon: CupertinoIcons.arrow_2_squarepath, - ), - ), - Expanded( - child: RepeatButton( - onLongPress: canGoBack ? () => _moveBackward(ref) : null, - child: BottomBarButton( - onTap: canGoBack ? () => _moveBackward(ref) : null, - label: 'Previous', - showLabel: true, - icon: CupertinoIcons.chevron_back, - showTooltip: false, - ), - ), - ), - Expanded( - child: RepeatButton( - onLongPress: canGoNext ? () => _moveForward(ref) : null, - child: BottomBarButton( - icon: CupertinoIcons.chevron_forward, - label: 'Next', - showLabel: true, - onTap: canGoNext ? () => _moveForward(ref) : null, - showTooltip: false, - ), - ), - ), - ], + return BottomBar( + children: [ + BottomBarButton( + label: dbLabel, + showLabel: true, + onTap: () => showAdaptiveBottomSheet( + context: context, + isScrollControlled: true, + showDragHandle: true, + isDismissible: true, + builder: (_) => OpeningExplorerSettings(pgn, options), ), + icon: Icons.tune, ), - ), + BottomBarButton( + label: 'Flip', + tooltip: context.l10n.flipBoard, + showLabel: true, + onTap: () => ref.read(ctrlProvider.notifier).toggleBoard(), + icon: CupertinoIcons.arrow_2_squarepath, + ), + RepeatButton( + onLongPress: canGoBack ? () => _moveBackward(ref) : null, + child: BottomBarButton( + onTap: canGoBack ? () => _moveBackward(ref) : null, + label: 'Previous', + showLabel: true, + icon: CupertinoIcons.chevron_back, + showTooltip: false, + ), + ), + RepeatButton( + onLongPress: canGoNext ? () => _moveForward(ref) : null, + child: BottomBarButton( + icon: CupertinoIcons.chevron_forward, + label: 'Next', + showLabel: true, + onTap: canGoNext ? () => _moveForward(ref) : null, + showTooltip: false, + ), + ), + ], ); } diff --git a/lib/src/view/puzzle/puzzle_screen.dart b/lib/src/view/puzzle/puzzle_screen.dart index c92902c0d5..c35fa4dbdf 100644 --- a/lib/src/view/puzzle/puzzle_screen.dart +++ b/lib/src/view/puzzle/puzzle_screen.dart @@ -37,6 +37,7 @@ import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/adaptive_choice_picker.dart'; import 'package:lichess_mobile/src/widgets/board_table.dart'; +import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; @@ -412,105 +413,79 @@ class _BottomBar extends ConsumerWidget { final puzzleState = ref.watch(ctrlProvider); final isDailyPuzzle = puzzleState.puzzle.isDailyPuzzle == true; - return Container( - color: Theme.of(context).platform == TargetPlatform.iOS - ? null - : Theme.of(context).bottomAppBarTheme.color, - child: SafeArea( - top: false, - child: SizedBox( - height: kBottomBarHeight, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - if (initialPuzzleContext.userId != null && - !isDailyPuzzle && - puzzleState.mode != PuzzleMode.view) - _DifficultySelector( - initialPuzzleContext: initialPuzzleContext, - ctrlProvider: ctrlProvider, - ), - if (puzzleState.mode != PuzzleMode.view) - BottomBarButton( - icon: Icons.help, - label: context.l10n.viewTheSolution, - showLabel: true, - onTap: puzzleState.canViewSolution - ? () => ref.read(ctrlProvider.notifier).viewSolution() - : null, - ), - if (puzzleState.mode == PuzzleMode.view) - Expanded( - child: BottomBarButton( - label: context.l10n.menu, - onTap: () { - _showPuzzleMenu(context, ref); - }, - icon: Icons.menu, - ), - ), - if (puzzleState.mode == PuzzleMode.view) - Expanded( - child: BottomBarButton( - onTap: () { - ref.read(ctrlProvider.notifier).toggleLocalEvaluation(); - }, - label: context.l10n.toggleLocalEvaluation, - icon: CupertinoIcons.gauge, - highlighted: puzzleState.isLocalEvalEnabled, - ), - ), - if (puzzleState.mode == PuzzleMode.view) - Expanded( - child: RepeatButton( - triggerDelays: _repeatTriggerDelays, - onLongPress: - puzzleState.canGoBack ? () => _moveBackward(ref) : null, - child: BottomBarButton( - onTap: puzzleState.canGoBack - ? () => _moveBackward(ref) - : null, - label: 'Previous', - icon: CupertinoIcons.chevron_back, - showTooltip: false, - ), - ), - ), - if (puzzleState.mode == PuzzleMode.view) - Expanded( - child: RepeatButton( - triggerDelays: _repeatTriggerDelays, - onLongPress: - puzzleState.canGoNext ? () => _moveForward(ref) : null, - child: BottomBarButton( - onTap: puzzleState.canGoNext - ? () => _moveForward(ref) - : null, - label: context.l10n.next, - icon: CupertinoIcons.chevron_forward, - showTooltip: false, - blink: puzzleState.viewedSolutionRecently, - ), - ), - ), - if (puzzleState.mode == PuzzleMode.view) - Expanded( - child: BottomBarButton( - onTap: puzzleState.mode == PuzzleMode.view && - puzzleState.nextContext != null - ? () => ref - .read(ctrlProvider.notifier) - .loadPuzzle(puzzleState.nextContext!) - : null, - highlighted: true, - label: context.l10n.puzzleContinueTraining, - icon: CupertinoIcons.play_arrow_solid, - ), - ), - ], + return BottomBar( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + if (initialPuzzleContext.userId != null && + !isDailyPuzzle && + puzzleState.mode != PuzzleMode.view) + _DifficultySelector( + initialPuzzleContext: initialPuzzleContext, + ctrlProvider: ctrlProvider, ), - ), - ), + if (puzzleState.mode != PuzzleMode.view) + BottomBarButton( + icon: Icons.help, + label: context.l10n.viewTheSolution, + showLabel: true, + onTap: puzzleState.canViewSolution + ? () => ref.read(ctrlProvider.notifier).viewSolution() + : null, + ), + if (puzzleState.mode == PuzzleMode.view) + BottomBarButton( + label: context.l10n.menu, + onTap: () { + _showPuzzleMenu(context, ref); + }, + icon: Icons.menu, + ), + if (puzzleState.mode == PuzzleMode.view) + BottomBarButton( + onTap: () { + ref.read(ctrlProvider.notifier).toggleLocalEvaluation(); + }, + label: context.l10n.toggleLocalEvaluation, + icon: CupertinoIcons.gauge, + highlighted: puzzleState.isLocalEvalEnabled, + ), + if (puzzleState.mode == PuzzleMode.view) + RepeatButton( + triggerDelays: _repeatTriggerDelays, + onLongPress: + puzzleState.canGoBack ? () => _moveBackward(ref) : null, + child: BottomBarButton( + onTap: puzzleState.canGoBack ? () => _moveBackward(ref) : null, + label: 'Previous', + icon: CupertinoIcons.chevron_back, + showTooltip: false, + ), + ), + if (puzzleState.mode == PuzzleMode.view) + RepeatButton( + triggerDelays: _repeatTriggerDelays, + onLongPress: puzzleState.canGoNext ? () => _moveForward(ref) : null, + child: BottomBarButton( + onTap: puzzleState.canGoNext ? () => _moveForward(ref) : null, + label: context.l10n.next, + icon: CupertinoIcons.chevron_forward, + showTooltip: false, + blink: puzzleState.viewedSolutionRecently, + ), + ), + if (puzzleState.mode == PuzzleMode.view) + BottomBarButton( + onTap: puzzleState.mode == PuzzleMode.view && + puzzleState.nextContext != null + ? () => ref + .read(ctrlProvider.notifier) + .loadPuzzle(puzzleState.nextContext!) + : null, + highlighted: true, + label: context.l10n.puzzleContinueTraining, + icon: CupertinoIcons.play_arrow_solid, + ), + ], ); } diff --git a/lib/src/view/puzzle/storm_screen.dart b/lib/src/view/puzzle/storm_screen.dart index a482f2e6c3..53f4458767 100644 --- a/lib/src/view/puzzle/storm_screen.dart +++ b/lib/src/view/puzzle/storm_screen.dart @@ -24,6 +24,7 @@ import 'package:lichess_mobile/src/view/puzzle/storm_clock.dart'; import 'package:lichess_mobile/src/view/puzzle/storm_dashboard.dart'; import 'package:lichess_mobile/src/view/settings/toggle_sound_button.dart'; import 'package:lichess_mobile/src/widgets/board_table.dart'; +import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; @@ -607,58 +608,45 @@ class _BottomBar extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final stormState = ref.watch(ctrl); - return Container( - color: Theme.of(context).platform == TargetPlatform.iOS - ? null - : Theme.of(context).bottomAppBarTheme.color, - child: SafeArea( - top: false, - child: SizedBox( - height: kBottomBarHeight, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - if (stormState.mode == StormMode.initial) - BottomBarButton( - icon: Icons.info_outline, - label: context.l10n.aboutX('Storm'), - showLabel: true, - onTap: () => _stormInfoDialogBuilder(context), - ), - BottomBarButton( - icon: Icons.delete, - label: context.l10n.stormNewRun.split('(').first.trimRight(), - showLabel: true, - onTap: () { - stormState.clock.reset(); - ref.invalidate(stormProvider); - }, - ), - if (stormState.mode == StormMode.running) - BottomBarButton( - icon: LichessIcons.flag, - label: context.l10n.stormEndRun.split('(').first.trimRight(), - showLabel: true, - onTap: stormState.puzzleIndex >= 1 - ? () { - if (stormState.clock.startAt != null) { - stormState.clock.sendEnd(); - } - } - : null, - ), - if (stormState.mode == StormMode.ended && - stormState.stats != null) - BottomBarButton( - icon: Icons.open_in_new, - label: 'Result', - showLabel: true, - onTap: () => _showStats(context, stormState.stats!), - ), - ], + return BottomBar( + children: [ + if (stormState.mode == StormMode.initial) + BottomBarButton( + icon: Icons.info_outline, + label: context.l10n.aboutX('Storm'), + showLabel: true, + onTap: () => _stormInfoDialogBuilder(context), ), + BottomBarButton( + icon: Icons.delete, + label: context.l10n.stormNewRun.split('(').first.trimRight(), + showLabel: true, + onTap: () { + stormState.clock.reset(); + ref.invalidate(stormProvider); + }, ), - ), + if (stormState.mode == StormMode.running) + BottomBarButton( + icon: LichessIcons.flag, + label: context.l10n.stormEndRun.split('(').first.trimRight(), + showLabel: true, + onTap: stormState.puzzleIndex >= 1 + ? () { + if (stormState.clock.startAt != null) { + stormState.clock.sendEnd(); + } + } + : null, + ), + if (stormState.mode == StormMode.ended && stormState.stats != null) + BottomBarButton( + icon: Icons.open_in_new, + label: 'Result', + showLabel: true, + onTap: () => _showStats(context, stormState.stats!), + ), + ], ); } } diff --git a/lib/src/view/puzzle/streak_screen.dart b/lib/src/view/puzzle/streak_screen.dart index cbf343815c..c6651bcad6 100644 --- a/lib/src/view/puzzle/streak_screen.dart +++ b/lib/src/view/puzzle/streak_screen.dart @@ -23,6 +23,7 @@ import 'package:lichess_mobile/src/utils/share.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; import 'package:lichess_mobile/src/view/settings/toggle_sound_button.dart'; import 'package:lichess_mobile/src/widgets/board_table.dart'; +import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/yes_no_dialog.dart'; @@ -266,109 +267,87 @@ class _BottomBar extends ConsumerWidget { puzzleControllerProvider(initialPuzzleContext, initialStreak: streak); final puzzleState = ref.watch(ctrlProvider); - return Container( - color: Theme.of(context).platform == TargetPlatform.iOS - ? null - : Theme.of(context).bottomAppBarTheme.color, - child: SafeArea( - top: false, - child: SizedBox( - height: kBottomBarHeight, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - if (!puzzleState.streak!.finished) - BottomBarButton( - icon: Icons.info_outline, - label: context.l10n.aboutX('Streak'), - showLabel: true, - onTap: () => _streakInfoDialogBuilder(context), - ), - if (!puzzleState.streak!.finished) - BottomBarButton( - icon: Icons.skip_next, - label: context.l10n.skipThisMove, - showLabel: true, - onTap: puzzleState.streak!.hasSkipped || - puzzleState.mode == PuzzleMode.view - ? null - : () => ref.read(ctrlProvider.notifier).skipMove(), - ), - if (puzzleState.streak!.finished) - Expanded( - child: BottomBarButton( - onTap: () { - launchShareDialog( - context, - text: lichessUri( - '/training/${puzzleState.puzzle.puzzle.id}', - ).toString(), - ); - }, - label: 'Share this puzzle', - icon: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoIcons.share - : Icons.share, - ), - ), - if (puzzleState.streak!.finished) - Expanded( - child: BottomBarButton( - onTap: () { - pushPlatformRoute( - context, - builder: (context) => AnalysisScreen( - title: context.l10n.analysis, - pgnOrId: ref.read(ctrlProvider.notifier).makePgn(), - options: AnalysisOptions( - isLocalEvaluationAllowed: true, - variant: Variant.standard, - orientation: puzzleState.pov, - id: standaloneAnalysisId, - initialMoveCursor: 0, - ), - ), - ); - }, - label: context.l10n.analysis, - icon: Icons.biotech, - ), - ), - if (puzzleState.streak!.finished) - Expanded( - child: BottomBarButton( - onTap: puzzleState.canGoBack - ? () => ref.read(ctrlProvider.notifier).userPrevious() - : null, - label: 'Previous', - icon: CupertinoIcons.chevron_back, - ), - ), - if (puzzleState.streak!.finished) - Expanded( - child: BottomBarButton( - onTap: puzzleState.canGoNext - ? () => ref.read(ctrlProvider.notifier).userNext() - : null, - label: context.l10n.next, - icon: CupertinoIcons.chevron_forward, - ), - ), - if (puzzleState.streak!.finished) - Expanded( - child: BottomBarButton( - onTap: ref.read(streakProvider).isLoading == false - ? () => ref.invalidate(streakProvider) - : null, - highlighted: true, - label: context.l10n.puzzleNewStreak, - icon: CupertinoIcons.play_arrow_solid, + return BottomBar( + children: [ + if (!puzzleState.streak!.finished) + BottomBarButton( + icon: Icons.info_outline, + label: context.l10n.aboutX('Streak'), + showLabel: true, + onTap: () => _streakInfoDialogBuilder(context), + ), + if (!puzzleState.streak!.finished) + BottomBarButton( + icon: Icons.skip_next, + label: context.l10n.skipThisMove, + showLabel: true, + onTap: puzzleState.streak!.hasSkipped || + puzzleState.mode == PuzzleMode.view + ? null + : () => ref.read(ctrlProvider.notifier).skipMove(), + ), + if (puzzleState.streak!.finished) + BottomBarButton( + onTap: () { + launchShareDialog( + context, + text: lichessUri( + '/training/${puzzleState.puzzle.puzzle.id}', + ).toString(), + ); + }, + label: 'Share this puzzle', + icon: Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoIcons.share + : Icons.share, + ), + if (puzzleState.streak!.finished) + BottomBarButton( + onTap: () { + pushPlatformRoute( + context, + builder: (context) => AnalysisScreen( + title: context.l10n.analysis, + pgnOrId: ref.read(ctrlProvider.notifier).makePgn(), + options: AnalysisOptions( + isLocalEvaluationAllowed: true, + variant: Variant.standard, + orientation: puzzleState.pov, + id: standaloneAnalysisId, + initialMoveCursor: 0, ), ), - ], + ); + }, + label: context.l10n.analysis, + icon: Icons.biotech, ), - ), - ), + if (puzzleState.streak!.finished) + BottomBarButton( + onTap: puzzleState.canGoBack + ? () => ref.read(ctrlProvider.notifier).userPrevious() + : null, + label: 'Previous', + icon: CupertinoIcons.chevron_back, + ), + if (puzzleState.streak!.finished) + BottomBarButton( + onTap: puzzleState.canGoNext + ? () => ref.read(ctrlProvider.notifier).userNext() + : null, + label: context.l10n.next, + icon: CupertinoIcons.chevron_forward, + ), + if (puzzleState.streak!.finished) + BottomBarButton( + onTap: ref.read(streakProvider).isLoading == false + ? () => ref.invalidate(streakProvider) + : null, + highlighted: true, + label: context.l10n.puzzleNewStreak, + icon: CupertinoIcons.play_arrow_solid, + ), + ], ); } diff --git a/lib/src/view/watch/tv_screen.dart b/lib/src/view/watch/tv_screen.dart index 2a475768af..46316b7ae0 100644 --- a/lib/src/view/watch/tv_screen.dart +++ b/lib/src/view/watch/tv_screen.dart @@ -14,6 +14,7 @@ import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/view/game/game_player.dart'; import 'package:lichess_mobile/src/view/settings/toggle_sound_button.dart'; import 'package:lichess_mobile/src/widgets/board_table.dart'; +import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/countdown_clock.dart'; @@ -209,72 +210,54 @@ class _BottomBar extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return Container( - color: Theme.of(context).platform == TargetPlatform.iOS - ? null - : Theme.of(context).bottomAppBarTheme.color, - child: SafeArea( - top: false, - child: SizedBox( - height: kBottomBarHeight, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - Expanded( - child: BottomBarButton( - label: context.l10n.flipBoard, - onTap: () => _flipBoard(ref), - icon: CupertinoIcons.arrow_2_squarepath, - ), - ), - Expanded( - child: RepeatButton( - onLongPress: ref - .read(tvControllerProvider(tvChannel, game).notifier) - .canGoBack() - ? () => _moveBackward(ref) - : null, - child: BottomBarButton( - key: const ValueKey('goto-previous'), - onTap: ref - .read( - tvControllerProvider(tvChannel, game).notifier, - ) - .canGoBack() - ? () => _moveBackward(ref) - : null, - label: 'Previous', - icon: CupertinoIcons.chevron_back, - showTooltip: false, - ), - ), - ), - Expanded( - child: RepeatButton( - onLongPress: ref - .read(tvControllerProvider(tvChannel, game).notifier) - .canGoForward() - ? () => _moveForward(ref) - : null, - child: BottomBarButton( - key: const ValueKey('goto-next'), - icon: CupertinoIcons.chevron_forward, - label: context.l10n.next, - onTap: ref - .read( - tvControllerProvider(tvChannel, game).notifier, - ) - .canGoForward() - ? () => _moveForward(ref) - : null, - showTooltip: false, - ), - ), - ), - ], + return BottomBar( + children: [ + BottomBarButton( + label: context.l10n.flipBoard, + onTap: () => _flipBoard(ref), + icon: CupertinoIcons.arrow_2_squarepath, + ), + RepeatButton( + onLongPress: ref + .read(tvControllerProvider(tvChannel, game).notifier) + .canGoBack() + ? () => _moveBackward(ref) + : null, + child: BottomBarButton( + key: const ValueKey('goto-previous'), + onTap: ref + .read( + tvControllerProvider(tvChannel, game).notifier, + ) + .canGoBack() + ? () => _moveBackward(ref) + : null, + label: 'Previous', + icon: CupertinoIcons.chevron_back, + showTooltip: false, ), ), - ), + RepeatButton( + onLongPress: ref + .read(tvControllerProvider(tvChannel, game).notifier) + .canGoForward() + ? () => _moveForward(ref) + : null, + child: BottomBarButton( + key: const ValueKey('goto-next'), + icon: CupertinoIcons.chevron_forward, + label: context.l10n.next, + onTap: ref + .read( + tvControllerProvider(tvChannel, game).notifier, + ) + .canGoForward() + ? () => _moveForward(ref) + : null, + showTooltip: false, + ), + ), + ], ); } diff --git a/lib/src/widgets/bottom_bar.dart b/lib/src/widgets/bottom_bar.dart new file mode 100644 index 0000000000..c1f9f84953 --- /dev/null +++ b/lib/src/widgets/bottom_bar.dart @@ -0,0 +1,38 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:lichess_mobile/src/constants.dart'; + +/// A container in the style of a bottom app bar, containg a [Row] of children widgets. +/// +/// The height of the bar is always [kBottomBarHeight]. +class BottomBar extends StatelessWidget { + const BottomBar({ + required this.children, + this.mainAxisAlignment = MainAxisAlignment.spaceAround, + }); + + /// Children to display in the bottom bar's [Row]. Typically instances of [BottomBarButton]. + final List children; + + /// Alignment of the bottom bar's internal row. Defaults to [MainAxisAlignment.spaceAround]. + final MainAxisAlignment mainAxisAlignment; + + @override + Widget build(BuildContext context) { + return Container( + color: Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoTheme.of(context).barBackgroundColor + : Theme.of(context).bottomAppBarTheme.color, + child: SafeArea( + top: false, + child: SizedBox( + height: kBottomBarHeight, + child: Row( + mainAxisAlignment: mainAxisAlignment, + children: children, + ), + ), + ), + ); + } +} From 8fbe89d77c5b26d95fed0f310d259c49a9712d7f Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Sun, 1 Sep 2024 16:48:36 +0200 Subject: [PATCH 187/979] refactor: add PlatformScaffold and PlatformAppBar widgets We had many screens where there was a lot of code duplication between androidBuilder and iosBuilder when using PlatformWidget. With these two new classes we clean a lot of this up. Custom screens that need to do something different for Android vs iOS still use PlatformWidget. --- lib/src/view/account/edit_profile_screen.dart | 20 +- lib/src/view/account/profile_screen.dart | 65 +------ lib/src/view/analysis/analysis_screen.dart | 5 +- .../view/analysis/analysis_share_screen.dart | 21 +-- .../board_editor/board_editor_screen.dart | 24 +-- .../broadcast/broadcast_round_screen.dart | 27 +-- .../broadcast/broadcasts_list_screen.dart | 27 +-- .../offline_correspondence_game_screen.dart | 27 +-- lib/src/view/game/archived_game_screen.dart | 57 ++---- lib/src/view/game/game_common_widgets.dart | 72 +------ lib/src/view/game/game_screen.dart | 53 ++---- lib/src/view/game/message_screen.dart | 33 +--- .../offline_correspondence_games_screen.dart | 28 +-- .../opening_explorer_screen.dart | 23 +-- lib/src/view/play/challenge_screen.dart | 20 +- lib/src/view/play/online_bots_screen.dart | 20 +- lib/src/view/play/play_screen.dart | 48 ++--- lib/src/view/puzzle/dashboard_screen.dart | 24 +-- lib/src/view/puzzle/opening_screen.dart | 22 +-- .../view/puzzle/puzzle_history_screen.dart | 20 +- lib/src/view/puzzle/puzzle_screen.dart | 44 +---- lib/src/view/puzzle/puzzle_themes_screen.dart | 22 +-- lib/src/view/puzzle/storm_dashboard.dart | 41 ++-- lib/src/view/puzzle/storm_screen.dart | 68 ++----- lib/src/view/puzzle/streak_screen.dart | 34 +--- lib/src/view/relation/following_screen.dart | 17 +- .../settings/account_preferences_screen.dart | 37 +--- lib/src/view/tools/load_position_screen.dart | 24 +-- lib/src/view/user/game_history_screen.dart | 40 +--- lib/src/view/user/leaderboard_screen.dart | 18 +- lib/src/view/user/perf_stats_screen.dart | 23 +-- lib/src/view/user/player_screen.dart | 25 +-- lib/src/view/user/user_screen.dart | 69 +------ .../view/watch/live_tv_channels_screen.dart | 33 +--- lib/src/view/watch/tv_screen.dart | 56 ++---- lib/src/widgets/platform_scaffold.dart | 177 ++++++++++++++++++ 36 files changed, 388 insertions(+), 976 deletions(-) create mode 100644 lib/src/widgets/platform_scaffold.dart diff --git a/lib/src/view/account/edit_profile_screen.dart b/lib/src/view/account/edit_profile_screen.dart index 0c19c32b40..2a6529abaa 100644 --- a/lib/src/view/account/edit_profile_screen.dart +++ b/lib/src/view/account/edit_profile_screen.dart @@ -12,7 +12,7 @@ import 'package:lichess_mobile/src/widgets/adaptive_autocomplete.dart'; import 'package:lichess_mobile/src/widgets/adaptive_text_field.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:result_extensions/result_extensions.dart'; final _countries = countries.values.toList(); @@ -22,27 +22,13 @@ class EditProfileScreen extends StatelessWidget { @override Widget build(BuildContext context) { - return PlatformWidget( - androidBuilder: _buildAndroid, - iosBuilder: _buildIos, - ); - } - - Widget _buildAndroid(BuildContext context) { - return Scaffold( - appBar: AppBar( + return PlatformScaffold( + appBar: PlatformAppBar( title: Text(context.l10n.editProfile), ), body: _Body(), ); } - - Widget _buildIos(BuildContext context) { - return CupertinoPageScaffold( - navigationBar: const CupertinoNavigationBar(), - child: _Body(), - ); - } } class _Body extends ConsumerWidget { diff --git a/lib/src/view/account/profile_screen.dart b/lib/src/view/account/profile_screen.dart index b43c3a222f..e94baae744 100644 --- a/lib/src/view/account/profile_screen.dart +++ b/lib/src/view/account/profile_screen.dart @@ -1,4 +1,3 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; @@ -12,7 +11,7 @@ import 'package:lichess_mobile/src/view/user/user_activity.dart'; import 'package:lichess_mobile/src/view/user/user_profile.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; import 'package:lichess_mobile/src/widgets/user_full_name.dart'; @@ -21,17 +20,9 @@ class ProfileScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return ConsumerPlatformWidget( - ref: ref, - androidBuilder: _buildAndroid, - iosBuilder: _buildIos, - ); - } - - Widget _buildAndroid(BuildContext context, WidgetRef ref) { final account = ref.watch(accountProvider); - return Scaffold( - appBar: AppBar( + return PlatformScaffold( + appBar: PlatformAppBar( title: account.when( data: (user) => user == null ? const SizedBox.shrink() @@ -75,56 +66,6 @@ class ProfileScreen extends ConsumerWidget { ), ); } - - Widget _buildIos(BuildContext context, WidgetRef ref) { - final account = ref.watch(accountProvider); - return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - middle: account.when( - data: (user) => user == null - ? const SizedBox.shrink() - : UserFullNameWidget(user: user.lightUser), - loading: () => const SizedBox.shrink(), - error: (error, _) => const SizedBox.shrink(), - ), - trailing: AppBarIconButton( - icon: const Icon(CupertinoIcons.square_pencil), - semanticsLabel: context.l10n.editProfile, - onPressed: () => pushPlatformRoute( - title: context.l10n.editProfile, - context, - builder: (_) => const EditProfileScreen(), - ), - ), - ), - child: account.when( - data: (user) { - if (user == null) { - return Center( - child: Text(context.l10n.mobileMustBeLoggedIn), - ); - } - return SafeArea( - child: ListView( - children: [ - UserProfileWidget(user: user), - const AccountPerfCards(), - const UserActivityWidget(), - const RecentGamesWidget(), - ], - ), - ); - }, - loading: () => - const Center(child: CircularProgressIndicator.adaptive()), - error: (error, _) { - return FullScreenRetryRequest( - onRetry: () => ref.invalidate(accountProvider), - ); - }, - ), - ); - } } class AccountPerfCards extends ConsumerWidget { diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index e2e51c0e5a..51fc94a710 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -38,6 +38,7 @@ import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:popover/popover.dart'; import '../../utils/share.dart'; @@ -134,9 +135,9 @@ class _LoadedAnalysisScreen extends ConsumerWidget { Widget _androidBuilder(BuildContext context, WidgetRef ref) { final ctrlProvider = analysisControllerProvider(pgn, options); - return Scaffold( + return PlatformScaffold( resizeToAvoidBottomInset: false, - appBar: AppBar( + appBar: PlatformAppBar( title: _Title(options: options, title: title), actions: [ _EngineDepth(ctrlProvider), diff --git a/lib/src/view/analysis/analysis_share_screen.dart b/lib/src/view/analysis/analysis_share_screen.dart index caf1b2ede8..0cc88f0b8e 100644 --- a/lib/src/view/analysis/analysis_share_screen.dart +++ b/lib/src/view/analysis/analysis_share_screen.dart @@ -1,6 +1,5 @@ import 'package:collection/collection.dart'; import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/account/account_preferences.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; @@ -10,7 +9,7 @@ import 'package:lichess_mobile/src/utils/share.dart'; import 'package:lichess_mobile/src/widgets/adaptive_choice_picker.dart'; import 'package:lichess_mobile/src/widgets/adaptive_text_field.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; class AnalysisShareScreen extends StatelessWidget { const AnalysisShareScreen({required this.pgn, required this.options}); @@ -20,27 +19,13 @@ class AnalysisShareScreen extends StatelessWidget { @override Widget build(BuildContext context) { - return PlatformWidget( - androidBuilder: _buildAndroid, - iosBuilder: _buildIos, - ); - } - - Widget _buildAndroid(BuildContext context) { - return Scaffold( - appBar: AppBar( + return PlatformScaffold( + appBar: PlatformAppBar( title: Text(context.l10n.studyShareAndExport), ), body: _EditPgnTagsForm(pgn, options), ); } - - Widget _buildIos(BuildContext context) { - return CupertinoPageScaffold( - navigationBar: const CupertinoNavigationBar(), - child: _EditPgnTagsForm(pgn, options), - ); - } } const Set _ratingHeaders = { diff --git a/lib/src/view/board_editor/board_editor_screen.dart b/lib/src/view/board_editor/board_editor_screen.dart index 7d62fada35..4b788a4d24 100644 --- a/lib/src/view/board_editor/board_editor_screen.dart +++ b/lib/src/view/board_editor/board_editor_screen.dart @@ -17,7 +17,7 @@ import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; import 'package:lichess_mobile/src/view/board_editor/board_editor_menu.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; class BoardEditorScreen extends StatelessWidget { const BoardEditorScreen({super.key, this.initialFen}); @@ -26,31 +26,13 @@ class BoardEditorScreen extends StatelessWidget { @override Widget build(BuildContext context) { - return PlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, - ); - } - - Widget _androidBuilder(BuildContext context) { - return Scaffold( - appBar: AppBar( + return PlatformScaffold( + appBar: PlatformAppBar( title: Text(context.l10n.boardEditor), ), body: _Body(initialFen), ); } - - Widget _iosBuilder(BuildContext context) { - return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - backgroundColor: Styles.cupertinoScaffoldColor.resolveFrom(context), - border: null, - middle: Text(context.l10n.boardEditor), - ), - child: _Body(initialFen), - ); - } } class _Body extends ConsumerWidget { diff --git a/lib/src/view/broadcast/broadcast_round_screen.dart b/lib/src/view/broadcast/broadcast_round_screen.dart index 43e9357e2e..f3a2a7b4e9 100644 --- a/lib/src/view/broadcast/broadcast_round_screen.dart +++ b/lib/src/view/broadcast/broadcast_round_screen.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -15,7 +14,7 @@ import 'package:lichess_mobile/src/utils/duration.dart'; import 'package:lichess_mobile/src/utils/lichess_assets.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/widgets/board_thumbnail.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; // height of 1.0 is important because we need to determine the height of the text @@ -37,33 +36,13 @@ class BroadcastRoundScreen extends StatelessWidget { @override Widget build(BuildContext context) { - return PlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, - ); - } - - Widget _androidBuilder( - BuildContext context, - ) { - return Scaffold( - appBar: AppBar( + return PlatformScaffold( + appBar: PlatformAppBar( title: Text(broadCastTitle), ), body: _Body(roundId), ); } - - Widget _iosBuilder( - BuildContext context, - ) { - return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - middle: Text(broadCastTitle), - ), - child: _Body(roundId), - ); - } } class _Body extends ConsumerWidget { diff --git a/lib/src/view/broadcast/broadcasts_list_screen.dart b/lib/src/view/broadcast/broadcasts_list_screen.dart index ccdf262370..f1dbe8dea2 100644 --- a/lib/src/view/broadcast/broadcasts_list_screen.dart +++ b/lib/src/view/broadcast/broadcasts_list_screen.dart @@ -1,4 +1,3 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; @@ -13,7 +12,7 @@ import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_round_screen.dart'; import 'package:lichess_mobile/src/view/broadcast/default_broadcast_image.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; final _dateFormatter = DateFormat.MMMd(Intl.getCurrentLocale()).add_Hm(); @@ -24,33 +23,13 @@ class BroadcastsListScreen extends StatelessWidget { @override Widget build(BuildContext context) { - return PlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, - ); - } - - Widget _androidBuilder( - BuildContext context, - ) { - return Scaffold( - appBar: AppBar( + return PlatformScaffold( + appBar: PlatformAppBar( title: Text(context.l10n.broadcastLiveBroadcasts), ), body: const _Body(), ); } - - Widget _iosBuilder( - BuildContext context, - ) { - return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - middle: Text(context.l10n.broadcastLiveBroadcasts), - ), - child: const _Body(), - ); - } } class _Body extends ConsumerStatefulWidget { diff --git a/lib/src/view/correspondence/offline_correspondence_game_screen.dart b/lib/src/view/correspondence/offline_correspondence_game_screen.dart index 426ff94aac..662f30ee17 100644 --- a/lib/src/view/correspondence/offline_correspondence_game_screen.dart +++ b/lib/src/view/correspondence/offline_correspondence_game_screen.dart @@ -24,7 +24,7 @@ import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; import 'package:lichess_mobile/src/widgets/board_table.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; class OfflineCorrespondenceGameScreen extends StatefulWidget { const OfflineCorrespondenceGameScreen({ @@ -57,16 +57,9 @@ class _OfflineCorrespondenceGameScreenState @override Widget build(BuildContext context) { - return PlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, - ); - } - - Widget _androidBuilder(BuildContext context) { final (lastModified, game) = currentGame; - return Scaffold( - appBar: AppBar(title: _Title(game)), + return PlatformScaffold( + appBar: PlatformAppBar(title: _Title(game)), body: _Body( game: game, lastModified: lastModified, @@ -74,20 +67,6 @@ class _OfflineCorrespondenceGameScreenState ), ); } - - Widget _iosBuilder(BuildContext context) { - final (lastModified, game) = currentGame; - return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - middle: _Title(game), - ), - child: _Body( - game: game, - lastModified: lastModified, - onGameChanged: goToNextGame, - ), - ); - } } class _Title extends StatelessWidget { diff --git a/lib/src/view/game/archived_game_screen.dart b/lib/src/view/game/archived_game_screen.dart index a6493d7d5c..7b690f6055 100644 --- a/lib/src/view/game/archived_game_screen.dart +++ b/lib/src/view/game/archived_game_screen.dart @@ -6,7 +6,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/game/archived_game.dart'; -import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; @@ -19,7 +18,7 @@ import 'package:lichess_mobile/src/widgets/board_table.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/countdown_clock.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'archived_game_screen_providers.dart'; @@ -38,54 +37,24 @@ class ArchivedGameScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return ConsumerPlatformWidget( - ref: ref, - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, - ); - } - - Widget _androidBuilder(BuildContext context, WidgetRef ref) { - return Scaffold( - appBar: AppBar( + return PlatformScaffold( + appBar: PlatformAppBar( title: _GameTitle(gameData: gameData), actions: [ ToggleSoundButton(), ], ), - body: _BoardBody( - gameData: gameData, - orientation: orientation, - initialCursor: initialCursor, - ), - bottomNavigationBar: - _BottomBar(gameData: gameData, orientation: orientation), - ); - } - - Widget _iosBuilder(BuildContext context, WidgetRef ref) { - return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - backgroundColor: Styles.cupertinoScaffoldColor.resolveFrom(context), - border: null, - middle: _GameTitle(gameData: gameData), - padding: const EdgeInsetsDirectional.only(end: 16.0), - trailing: ToggleSoundButton(), - ), - child: SafeArea( - bottom: false, - child: Column( - children: [ - Expanded( - child: _BoardBody( - gameData: gameData, - orientation: orientation, - initialCursor: initialCursor, - ), + body: Column( + children: [ + Expanded( + child: _BoardBody( + gameData: gameData, + orientation: orientation, + initialCursor: initialCursor, ), - _BottomBar(gameData: gameData, orientation: orientation), - ], - ), + ), + _BottomBar(gameData: gameData, orientation: orientation), + ], ), ); } diff --git a/lib/src/view/game/game_common_widgets.dart b/lib/src/view/game/game_common_widgets.dart index 29fe7dbb9e..d05a0a5261 100644 --- a/lib/src/view/game/game_common_widgets.dart +++ b/lib/src/view/game/game_common_widgets.dart @@ -1,5 +1,4 @@ import 'package:dartchess/dartchess.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/challenge/challenge.dart'; @@ -9,19 +8,19 @@ import 'package:lichess_mobile/src/model/common/time_increment.dart'; import 'package:lichess_mobile/src/model/game/game.dart'; import 'package:lichess_mobile/src/model/game/game_share_service.dart'; import 'package:lichess_mobile/src/model/lobby/game_seek.dart'; -import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/share.dart'; import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'game_screen_providers.dart'; import 'game_settings.dart'; import 'ping_rating.dart'; -class GameAppBar extends ConsumerWidget implements PreferredSizeWidget { +class GameAppBar extends ConsumerWidget { const GameAppBar({this.id, this.seek, this.challenge, super.key}); final GameSeek? seek; @@ -39,7 +38,7 @@ class GameAppBar extends ConsumerWidget implements PreferredSizeWidget { ? ref.watch(shouldPreventGoingBackProvider(id!)) : const AsyncValue.data(true); - return AppBar( + return PlatformAppBar( leading: shouldPreventGoingBackAsync.maybeWhen( data: (prevent) => prevent ? pingRating : null, orElse: () => pingRating, @@ -67,71 +66,6 @@ class GameAppBar extends ConsumerWidget implements PreferredSizeWidget { ], ); } - - @override - Size get preferredSize => const Size.fromHeight(kToolbarHeight); -} - -class GameCupertinoNavBar extends ConsumerWidget - implements ObstructingPreferredSizeWidget { - const GameCupertinoNavBar({this.id, this.seek, this.challenge, super.key}); - - final GameSeek? seek; - final ChallengeRequest? challenge; - final GameFullId? id; - - static const pingRating = Padding( - padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), - child: PingRating(size: 24.0), - ); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final shouldPreventGoingBackAsync = id != null - ? ref.watch(shouldPreventGoingBackProvider(id!)) - : const AsyncValue.data(true); - - return CupertinoNavigationBar( - backgroundColor: Styles.cupertinoScaffoldColor.resolveFrom(context), - border: null, - padding: Styles.cupertinoAppBarTrailingWidgetPadding, - leading: shouldPreventGoingBackAsync.maybeWhen( - data: (prevent) => prevent ? pingRating : null, - orElse: () => pingRating, - ), - middle: id != null - ? StandaloneGameTitle(id: id!) - : seek != null - ? _LobbyGameTitle(seek: seek!) - : challenge != null - ? _ChallengeGameTitle(challenge: challenge!) - : const SizedBox.shrink(), - trailing: id != null - ? AppBarIconButton( - onPressed: () => showAdaptiveBottomSheet( - context: context, - isDismissible: true, - isScrollControlled: true, - showDragHandle: true, - builder: (_) => GameSettings(id: id!), - ), - semanticsLabel: context.l10n.settingsSettings, - icon: const Icon(Icons.settings), - ) - : null, - ); - } - - @override - Size get preferredSize => - const Size.fromHeight(kMinInteractiveDimensionCupertino); - - /// True if the navigation bar's background color has no transparency. - @override - bool shouldFullyObstruct(BuildContext context) { - final Color backgroundColor = CupertinoTheme.of(context).barBackgroundColor; - return backgroundColor.alpha == 0xFF; - } } List makeFinishedGameShareActions( diff --git a/lib/src/view/game/game_screen.dart b/lib/src/view/game/game_screen.dart index 94528e2550..fbbc91ff4d 100644 --- a/lib/src/view/game/game_screen.dart +++ b/lib/src/view/game/game_screen.dart @@ -13,7 +13,7 @@ import 'package:lichess_mobile/src/model/lobby/game_setup.dart'; import 'package:lichess_mobile/src/navigation.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/game/game_loading_board.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'game_body.dart'; @@ -174,17 +174,10 @@ class _GameScreenState extends ConsumerState with RouteAware { destUser: widget.challenge?.destUser, ) : const LoadGameError('Could not create the game.'); - return PlatformWidget( - androidBuilder: (context) => Scaffold( - resizeToAvoidBottomInset: false, - appBar: GameAppBar(id: gameId), - body: body, - ), - iosBuilder: (context) => CupertinoPageScaffold( - resizeToAvoidBottomInset: false, - navigationBar: GameCupertinoNavBar(id: gameId), - child: body, - ), + return PlatformScaffold( + resizeToAvoidBottomInset: false, + appBar: GameAppBar(id: gameId), + body: body, ); }, loading: () { @@ -200,22 +193,12 @@ class _GameScreenState extends ConsumerState with RouteAware { ) : const StandaloneGameLoadingBoard(); - return PlatformWidget( - androidBuilder: (context) => Scaffold( - resizeToAvoidBottomInset: false, - appBar: GameAppBar(seek: widget.seek), - body: PopScope( - canPop: false, - child: loadingBoard, - ), - ), - iosBuilder: (context) => CupertinoPageScaffold( - resizeToAvoidBottomInset: false, - navigationBar: GameCupertinoNavBar(seek: widget.seek), - child: PopScope( - canPop: false, - child: loadingBoard, - ), + return PlatformScaffold( + resizeToAvoidBottomInset: false, + appBar: GameAppBar(seek: widget.seek), + body: PopScope( + canPop: false, + child: loadingBoard, ), ); }, @@ -235,17 +218,9 @@ class _GameScreenState extends ConsumerState with RouteAware { final body = PopScope(child: message); - return PlatformWidget( - androidBuilder: (context) => Scaffold( - resizeToAvoidBottomInset: false, - appBar: GameAppBar(seek: widget.seek), - body: body, - ), - iosBuilder: (context) => CupertinoPageScaffold( - resizeToAvoidBottomInset: false, - navigationBar: GameCupertinoNavBar(seek: widget.seek), - child: body, - ), + return PlatformScaffold( + appBar: GameAppBar(seek: widget.seek), + body: body, ); }, ); diff --git a/lib/src/view/game/message_screen.dart b/lib/src/view/game/message_screen.dart index 3b91dbb864..d684f855c0 100644 --- a/lib/src/view/game/message_screen.dart +++ b/lib/src/view/game/message_screen.dart @@ -12,7 +12,7 @@ import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/widgets/adaptive_text_field.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; class MessageScreen extends ConsumerStatefulWidget { final GameFullId id; @@ -53,37 +53,12 @@ class _MessageScreenState extends ConsumerState with RouteAware { @override Widget build(BuildContext context) { - final body = _Body(me: widget.me, id: widget.id); - - return PlatformWidget( - androidBuilder: (context) => - _androidBuilder(context: context, body: body), - iosBuilder: (context) => _iosBuilder(context: context, body: body), - ); - } - - Widget _androidBuilder({ - required BuildContext context, - required Widget body, - }) { - return Scaffold( - appBar: AppBar( + return PlatformScaffold( + appBar: PlatformAppBar( title: widget.title, centerTitle: true, ), - body: body, - ); - } - - Widget _iosBuilder({ - required BuildContext context, - required Widget body, - }) { - return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - middle: widget.title, - ), - child: body, + body: _Body(me: widget.me, id: widget.id), ); } } diff --git a/lib/src/view/game/offline_correspondence_games_screen.dart b/lib/src/view/game/offline_correspondence_games_screen.dart index f8fae92671..e4f8dcc356 100644 --- a/lib/src/view/game/offline_correspondence_games_screen.dart +++ b/lib/src/view/game/offline_correspondence_games_screen.dart @@ -8,7 +8,7 @@ import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/correspondence/offline_correspondence_game_screen.dart'; import 'package:lichess_mobile/src/widgets/board_preview.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:lichess_mobile/src/widgets/user_full_name.dart'; import 'package:timeago/timeago.dart' as timeago; @@ -17,27 +17,13 @@ class OfflineCorrespondenceGamesScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return ConsumerPlatformWidget( - ref: ref, - androidBuilder: _buildAndroid, - iosBuilder: _buildIos, - ); - } - - Widget _buildIos(BuildContext context, WidgetRef ref) { - return CupertinoPageScaffold( - navigationBar: const CupertinoNavigationBar(), - child: _Body(), - ); - } - - Widget _buildAndroid(BuildContext context, WidgetRef ref) { final offlineGames = ref.watch(offlineOngoingCorrespondenceGamesProvider); - return Scaffold( - appBar: offlineGames.maybeWhen( - data: (data) => - AppBar(title: Text(context.l10n.nbGamesInPlay(data.length))), - orElse: () => AppBar(title: const SizedBox.shrink()), + return PlatformScaffold( + appBar: PlatformAppBar( + title: offlineGames.maybeWhen( + data: (data) => Text(context.l10n.nbGamesInPlay(data.length)), + orElse: () => const SizedBox.shrink(), + ), ), body: _Body(), ); diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index 814e160fca..02a33070b7 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -12,7 +12,6 @@ import 'package:lichess_mobile/src/model/game/game_repository_providers.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_preferences.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_repository.dart'; -import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; @@ -22,6 +21,7 @@ import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -42,30 +42,13 @@ class OpeningExplorerScreen extends StatelessWidget { @override Widget build(BuildContext context) { - return PlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, - ); - } - - Widget _androidBuilder(BuildContext context) { - return Scaffold( - appBar: AppBar( + return PlatformScaffold( + appBar: PlatformAppBar( title: Text(context.l10n.openingExplorer), ), body: _Body(pgn: pgn, options: options), ); } - - Widget _iosBuilder(BuildContext context) { - return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - backgroundColor: Styles.cupertinoScaffoldColor.resolveFrom(context), - middle: Text(context.l10n.openingExplorer), - ), - child: _Body(pgn: pgn, options: options), - ); - } } class _Body extends ConsumerWidget { diff --git a/lib/src/view/play/challenge_screen.dart b/lib/src/view/play/challenge_screen.dart index 7ed5978826..dbf1a32249 100644 --- a/lib/src/view/play/challenge_screen.dart +++ b/lib/src/view/play/challenge_screen.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:dartchess/dartchess.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -25,7 +24,7 @@ import 'package:lichess_mobile/src/widgets/expanded_section.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/non_linear_slider.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; class ChallengeScreen extends StatelessWidget { const ChallengeScreen(this.user); @@ -34,19 +33,10 @@ class ChallengeScreen extends StatelessWidget { @override Widget build(BuildContext context) { - return PlatformWidget(androidBuilder: _buildAndroid, iosBuilder: _buildIos); - } - - Widget _buildIos(BuildContext context) { - return CupertinoPageScaffold( - navigationBar: const CupertinoNavigationBar(), - child: _ChallengeBody(user), - ); - } - - Widget _buildAndroid(BuildContext context) { - return Scaffold( - appBar: AppBar(title: Text(context.l10n.challengeChallengesX(user.name))), + return PlatformScaffold( + appBar: PlatformAppBar( + title: Text(context.l10n.challengeChallengesX(user.name)), + ), body: _ChallengeBody(user), ); } diff --git a/lib/src/view/play/online_bots_screen.dart b/lib/src/view/play/online_bots_screen.dart index 0dfd06a20c..fa18dcff40 100644 --- a/lib/src/view/play/online_bots_screen.dart +++ b/lib/src/view/play/online_bots_screen.dart @@ -17,7 +17,7 @@ import 'package:lichess_mobile/src/view/user/user_screen.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:lichess_mobile/src/widgets/user_full_name.dart'; import 'package:linkify/linkify.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -53,22 +53,8 @@ class OnlineBotsScreen extends StatelessWidget { @override Widget build(BuildContext context) { - return PlatformWidget( - androidBuilder: _buildAndroid, - iosBuilder: _buildIos, - ); - } - - Widget _buildIos(BuildContext context) { - return CupertinoPageScaffold( - navigationBar: const CupertinoNavigationBar(), - child: _Body(), - ); - } - - Widget _buildAndroid(BuildContext context) { - return Scaffold( - appBar: AppBar( + return PlatformScaffold( + appBar: PlatformAppBar( title: Text(context.l10n.onlineBots), ), body: _Body(), diff --git a/lib/src/view/play/play_screen.dart b/lib/src/view/play/play_screen.dart index 8523537850..0c82615ed9 100644 --- a/lib/src/view/play/play_screen.dart +++ b/lib/src/view/play/play_screen.dart @@ -4,48 +4,28 @@ import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/view/home/create_game_options.dart'; import 'package:lichess_mobile/src/view/play/quick_game_button.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; class PlayScreen extends StatelessWidget { const PlayScreen(); @override Widget build(BuildContext context) { - return PlatformWidget(androidBuilder: _buildAndroid, iosBuilder: _buildIos); - } - - Widget _buildIos(BuildContext context) { - return const CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar(), - child: _Body(), - ); - } - - Widget _buildAndroid(BuildContext context) { - return Scaffold( - appBar: AppBar( + return PlatformScaffold( + appBar: PlatformAppBar( title: Text(context.l10n.play), ), - body: const _Body(), - ); - } -} - -class _Body extends StatelessWidget { - const _Body(); - - @override - Widget build(BuildContext context) { - return SafeArea( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: Styles.bodySectionPadding, - child: const QuickGameButton(), - ), - const CreateGameOptions(), - ], + body: SafeArea( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: Styles.bodySectionPadding, + child: const QuickGameButton(), + ), + const CreateGameOptions(), + ], + ), ), ); } diff --git a/lib/src/view/puzzle/dashboard_screen.dart b/lib/src/view/puzzle/dashboard_screen.dart index 147ee226a0..2ad8ff23e8 100644 --- a/lib/src/view/puzzle/dashboard_screen.dart +++ b/lib/src/view/puzzle/dashboard_screen.dart @@ -1,6 +1,5 @@ import 'package:collection/collection.dart'; import 'package:fl_chart/fl_chart.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:http/http.dart' show ClientException; @@ -15,6 +14,7 @@ import 'package:lichess_mobile/src/utils/string.dart'; import 'package:lichess_mobile/src/widgets/adaptive_choice_picker.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; import 'package:lichess_mobile/src/widgets/stat_card.dart'; @@ -25,21 +25,13 @@ class PuzzleDashboardScreen extends StatelessWidget { @override Widget build(BuildContext context) { - return Theme.of(context).platform == TargetPlatform.iOS - ? const CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - middle: SizedBox.shrink(), - trailing: DaysSelector(), - ), - child: _Body(), - ) - : Scaffold( - body: const _Body(), - appBar: AppBar( - title: const SizedBox.shrink(), - actions: const [DaysSelector()], - ), - ); + return const PlatformScaffold( + body: _Body(), + appBar: PlatformAppBar( + title: SizedBox.shrink(), + actions: [DaysSelector()], + ), + ); } } diff --git a/lib/src/view/puzzle/opening_screen.dart b/lib/src/view/puzzle/opening_screen.dart index 81d9a3ae28..6089ce5dd9 100644 --- a/lib/src/view/puzzle/opening_screen.dart +++ b/lib/src/view/puzzle/opening_screen.dart @@ -10,7 +10,7 @@ import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'puzzle_screen.dart'; @@ -37,29 +37,13 @@ class OpeningThemeScreen extends StatelessWidget { @override Widget build(BuildContext context) { - return PlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, - ); - } - - Widget _androidBuilder(BuildContext context) { - return Scaffold( - appBar: AppBar( + return PlatformScaffold( + appBar: PlatformAppBar( title: Text(context.l10n.puzzlePuzzlesByOpenings), ), body: const _Body(), ); } - - Widget _iosBuilder(BuildContext context) { - return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - middle: Text(context.l10n.puzzlePuzzlesByOpenings), - ), - child: const _Body(), - ); - } } class _Body extends ConsumerWidget { diff --git a/lib/src/view/puzzle/puzzle_history_screen.dart b/lib/src/view/puzzle/puzzle_history_screen.dart index be900aaea4..3c8d611cd7 100644 --- a/lib/src/view/puzzle/puzzle_history_screen.dart +++ b/lib/src/view/puzzle/puzzle_history_screen.dart @@ -1,6 +1,5 @@ import 'package:collection/collection.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_layout_grid/flutter_layout_grid.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -16,7 +15,7 @@ import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/view/puzzle/puzzle_screen.dart'; import 'package:lichess_mobile/src/widgets/board_thumbnail.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:timeago/timeago.dart' as timeago; final _dateFormatter = DateFormat.yMMMd(Intl.getCurrentLocale()); @@ -24,21 +23,8 @@ final _dateFormatter = DateFormat.yMMMd(Intl.getCurrentLocale()); class PuzzleHistoryScreen extends StatelessWidget { @override Widget build(BuildContext context) { - return PlatformWidget(androidBuilder: _buildAndroid, iosBuilder: _buildIos); - } - - Widget _buildIos(BuildContext context) { - return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - middle: Text(context.l10n.puzzleHistory), - ), - child: _Body(), - ); - } - - Widget _buildAndroid(BuildContext context) { - return Scaffold( - appBar: AppBar(title: Text(context.l10n.puzzleHistory)), + return PlatformScaffold( + appBar: PlatformAppBar(title: Text(context.l10n.puzzleHistory)), body: _Body(), ); } diff --git a/lib/src/view/puzzle/puzzle_screen.dart b/lib/src/view/puzzle/puzzle_screen.dart index c92902c0d5..ee7d0af539 100644 --- a/lib/src/view/puzzle/puzzle_screen.dart +++ b/lib/src/view/puzzle/puzzle_screen.dart @@ -22,7 +22,6 @@ import 'package:lichess_mobile/src/model/puzzle/puzzle_service.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_theme.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/navigation.dart'; -import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:lichess_mobile/src/utils/immersive_mode.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; @@ -40,7 +39,7 @@ import 'package:lichess_mobile/src/widgets/board_table.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'puzzle_feedback_widget.dart'; import 'puzzle_session_widget.dart'; @@ -90,44 +89,17 @@ class _PuzzleScreenState extends ConsumerState with RouteAware { @override Widget build(BuildContext context) { return WakelockWidget( - child: PlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, - ), - ); - } - - Widget _androidBuilder(BuildContext context) { - return Scaffold( - appBar: AppBar( - actions: const [ - _PuzzleSettingsButton(), - ], - title: _Title(angle: widget.angle), - ), - body: widget.puzzleId != null - ? _LoadPuzzleFromId(angle: widget.angle, id: widget.puzzleId!) - : _LoadNextPuzzle(angle: widget.angle), - ); - } - - Widget _iosBuilder(BuildContext context) { - return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - backgroundColor: Styles.cupertinoScaffoldColor.resolveFrom(context), - border: null, - padding: Styles.cupertinoAppBarTrailingWidgetPadding, - middle: _Title(angle: widget.angle), - trailing: const Row( - mainAxisSize: MainAxisSize.min, - children: [ + child: PlatformScaffold( + appBar: PlatformAppBar( + actions: const [ _PuzzleSettingsButton(), ], + title: _Title(angle: widget.angle), ), + body: widget.puzzleId != null + ? _LoadPuzzleFromId(angle: widget.angle, id: widget.puzzleId!) + : _LoadNextPuzzle(angle: widget.angle), ), - child: widget.puzzleId != null - ? _LoadPuzzleFromId(angle: widget.angle, id: widget.puzzleId!) - : _LoadNextPuzzle(angle: widget.angle), ); } } diff --git a/lib/src/view/puzzle/puzzle_themes_screen.dart b/lib/src/view/puzzle/puzzle_themes_screen.dart index 3887cc76e8..969f27eff5 100644 --- a/lib/src/view/puzzle/puzzle_themes_screen.dart +++ b/lib/src/view/puzzle/puzzle_themes_screen.dart @@ -11,7 +11,7 @@ import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/puzzle/opening_screen.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'puzzle_screen.dart'; @@ -50,29 +50,13 @@ class PuzzleThemesScreen extends StatelessWidget { @override Widget build(BuildContext context) { - return PlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, - ); - } - - Widget _androidBuilder(BuildContext context) { - return Scaffold( - appBar: AppBar( + return PlatformScaffold( + appBar: PlatformAppBar( title: Text(context.l10n.puzzlePuzzleThemes), ), body: const _Body(), ); } - - Widget _iosBuilder(BuildContext context) { - return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - middle: Text(context.l10n.puzzlePuzzleThemes), - ), - child: const _Body(), - ); - } } class _Body extends ConsumerWidget { diff --git a/lib/src/view/puzzle/storm_dashboard.dart b/lib/src/view/puzzle/storm_dashboard.dart index 5ae03b1a81..720c786a2b 100644 --- a/lib/src/view/puzzle/storm_dashboard.dart +++ b/lib/src/view/puzzle/storm_dashboard.dart @@ -1,4 +1,3 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; @@ -9,6 +8,7 @@ import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; import 'package:lichess_mobile/src/widgets/stat_card.dart'; @@ -19,32 +19,19 @@ class StormDashboardModal extends StatelessWidget { @override Widget build(BuildContext context) { - return Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - middle: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(LichessIcons.storm, size: 20), - const SizedBox(width: 8.0), - Text(context.l10n.stormHighscores), - ], - ), - ), - child: _Body(user: user), - ) - : Scaffold( - body: _Body(user: user), - appBar: AppBar( - title: Row( - children: [ - const Icon(LichessIcons.storm, size: 20), - const SizedBox(width: 8.0), - Text(context.l10n.stormHighscores), - ], - ), - ), - ); + return PlatformScaffold( + body: _Body(user: user), + appBar: PlatformAppBar( + title: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(LichessIcons.storm, size: 20), + const SizedBox(width: 8.0), + Text(context.l10n.stormHighscores), + ], + ), + ), + ); } } diff --git a/lib/src/view/puzzle/storm_screen.dart b/lib/src/view/puzzle/storm_screen.dart index a482f2e6c3..92e5fdd171 100644 --- a/lib/src/view/puzzle/storm_screen.dart +++ b/lib/src/view/puzzle/storm_screen.dart @@ -28,7 +28,7 @@ import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:lichess_mobile/src/widgets/yes_no_dialog.dart'; class StormScreen extends ConsumerStatefulWidget { @@ -44,37 +44,13 @@ class _StormScreenState extends ConsumerState { @override Widget build(BuildContext context) { return WakelockWidget( - child: PlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, - ), - ); - } - - Widget _androidBuilder(BuildContext context) { - return Scaffold( - appBar: AppBar( - actions: [_StormDashboardButton(), ToggleSoundButton()], - title: const Text('Puzzle Storm'), - ), - body: _Load(_boardKey), - ); - } - - Widget _iosBuilder(BuildContext context) { - return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - backgroundColor: Styles.cupertinoScaffoldColor.resolveFrom(context), - border: null, - padding: Styles.cupertinoAppBarTrailingWidgetPadding, - middle: const Text('Puzzle Storm'), - trailing: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.end, - children: [_StormDashboardButton(), ToggleSoundButton()], + child: PlatformScaffold( + appBar: PlatformAppBar( + actions: [_StormDashboardButton(), ToggleSoundButton()], + title: const Text('Puzzle Storm'), ), + body: _Load(_boardKey), ), - child: _Load(_boardKey), ); } } @@ -669,28 +645,16 @@ class _RunStats extends StatelessWidget { @override Widget build(BuildContext context) { - return Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - leading: CupertinoButton( - padding: EdgeInsets.zero, - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text(context.l10n.close), - ), - ), - child: _RunStatsPopup(stats), - ) - : Scaffold( - body: _RunStatsPopup(stats), - appBar: AppBar( - leading: IconButton( - icon: const Icon(Icons.close), - onPressed: () => Navigator.of(context).pop(), - ), - ), - ); + return PlatformScaffold( + body: _RunStatsPopup(stats), + appBar: PlatformAppBar( + leading: IconButton( + icon: const Icon(Icons.close), + onPressed: () => Navigator.of(context).pop(), + ), + title: const SizedBox.shrink(), + ), + ); } } diff --git a/lib/src/view/puzzle/streak_screen.dart b/lib/src/view/puzzle/streak_screen.dart index cbf343815c..9e9a7b9178 100644 --- a/lib/src/view/puzzle/streak_screen.dart +++ b/lib/src/view/puzzle/streak_screen.dart @@ -24,7 +24,7 @@ import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; import 'package:lichess_mobile/src/view/settings/toggle_sound_button.dart'; import 'package:lichess_mobile/src/widgets/board_table.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:lichess_mobile/src/widgets/yes_no_dialog.dart'; import 'package:result_extensions/result_extensions.dart'; @@ -36,33 +36,13 @@ class StreakScreen extends StatelessWidget { @override Widget build(BuildContext context) { return WakelockWidget( - child: PlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, - ), - ); - } - - Widget _androidBuilder(BuildContext context) { - return Scaffold( - appBar: AppBar( - actions: [ToggleSoundButton()], - title: const Text('Puzzle Streak'), - ), - body: const _Load(), - ); - } - - Widget _iosBuilder(BuildContext context) { - return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - backgroundColor: Styles.cupertinoScaffoldColor.resolveFrom(context), - border: null, - padding: Styles.cupertinoAppBarTrailingWidgetPadding, - middle: const Text('Puzzle Streak'), - trailing: ToggleSoundButton(), + child: PlatformScaffold( + appBar: PlatformAppBar( + actions: [ToggleSoundButton()], + title: const Text('Puzzle Streak'), + ), + body: const _Load(), ), - child: const _Load(), ); } } diff --git a/lib/src/view/relation/following_screen.dart b/lib/src/view/relation/following_screen.dart index 746e66c181..b3ce4c3c0f 100644 --- a/lib/src/view/relation/following_screen.dart +++ b/lib/src/view/relation/following_screen.dart @@ -14,7 +14,7 @@ import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/user/user_screen.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:lichess_mobile/src/widgets/user_list_tile.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -33,19 +33,8 @@ class FollowingScreen extends StatelessWidget { const FollowingScreen({super.key}); @override Widget build(BuildContext context) { - return PlatformWidget(androidBuilder: _buildAndroid, iosBuilder: _buildIos); - } - - Widget _buildIos(BuildContext context) { - return const CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar(), - child: _Body(), - ); - } - - Widget _buildAndroid(BuildContext context) { - return Scaffold( - appBar: AppBar( + return PlatformScaffold( + appBar: PlatformAppBar( title: Text(context.l10n.friends), ), body: const _Body(), diff --git a/lib/src/view/settings/account_preferences_screen.dart b/lib/src/view/settings/account_preferences_screen.dart index 54d4af5d15..13c79d3925 100644 --- a/lib/src/view/settings/account_preferences_screen.dart +++ b/lib/src/view/settings/account_preferences_screen.dart @@ -7,6 +7,7 @@ import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/widgets/adaptive_choice_picker.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:lichess_mobile/src/widgets/settings.dart'; class AccountPreferencesScreen extends ConsumerStatefulWidget { @@ -409,33 +410,15 @@ class _AccountPreferencesScreenState }, ); - return Theme.of(context).platform == TargetPlatform.android - ? Scaffold( - appBar: AppBar( - title: Text(context.l10n.preferencesPreferences), - actions: [ - if (isLoading) - const Padding( - padding: EdgeInsets.only(right: 16), - child: SizedBox( - height: 24, - width: 24, - child: Center( - child: CircularProgressIndicator.adaptive(), - ), - ), - ), - ], - ), - body: content, - ) - : CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - trailing: - isLoading ? const CircularProgressIndicator.adaptive() : null, - ), - child: content, - ); + return PlatformScaffold( + appBar: PlatformAppBar( + title: Text(context.l10n.preferencesPreferences), + actions: [ + if (isLoading) const PlatformAppBarLoadingIndicator(), + ], + ), + body: content, + ); } } diff --git a/lib/src/view/tools/load_position_screen.dart b/lib/src/view/tools/load_position_screen.dart index 58e8a8e203..ac3b4d97b7 100644 --- a/lib/src/view/tools/load_position_screen.dart +++ b/lib/src/view/tools/load_position_screen.dart @@ -11,38 +11,20 @@ import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; import 'package:lichess_mobile/src/view/board_editor/board_editor_screen.dart'; import 'package:lichess_mobile/src/widgets/adaptive_text_field.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; class LoadPositionScreen extends StatelessWidget { const LoadPositionScreen({super.key}); @override Widget build(BuildContext context) { - return PlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, - ); - } - - Widget _androidBuilder(BuildContext context) { - return Scaffold( - appBar: AppBar( + return PlatformScaffold( + appBar: PlatformAppBar( title: Text(context.l10n.loadPosition), ), body: const _Body(), ); } - - Widget _iosBuilder(BuildContext context) { - return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - backgroundColor: Styles.cupertinoScaffoldColor.resolveFrom(context), - border: null, - middle: Text(context.l10n.loadPosition), - ), - child: const _Body(), - ); - } } class _Body extends StatefulWidget { diff --git a/lib/src/view/user/game_history_screen.dart b/lib/src/view/user/game_history_screen.dart index 2c4f9f50a2..9be6f6b5cb 100644 --- a/lib/src/view/user/game_history_screen.dart +++ b/lib/src/view/user/game_history_screen.dart @@ -1,5 +1,4 @@ import 'package:dartchess/dartchess.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; @@ -14,6 +13,7 @@ import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; class GameHistoryScreen extends ConsumerWidget { const GameHistoryScreen({ @@ -87,44 +87,12 @@ class GameHistoryScreen extends ConsumerWidget { ], ); - switch (Theme.of(context).platform) { - case TargetPlatform.android: - return _buildAndroid(context, ref, title: title, filterBtn: filterBtn); - case TargetPlatform.iOS: - return _buildIos(context, ref, title: title, filterBtn: filterBtn); - default: - assert(false, 'Unexpected platform ${Theme.of(context).platform}'); - return const SizedBox.shrink(); - } - } - - Widget _buildIos( - BuildContext context, - WidgetRef ref, { - required Widget title, - required Widget filterBtn, - }) { - return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - padding: const EdgeInsetsDirectional.only( + return PlatformScaffold( + appBar: PlatformAppBar( + cupertinoPadding: const EdgeInsetsDirectional.only( start: 16.0, end: 8.0, ), - middle: title, - trailing: filterBtn, - ), - child: _Body(user: user, isOnline: isOnline, gameFilter: gameFilter), - ); - } - - Widget _buildAndroid( - BuildContext context, - WidgetRef ref, { - required Widget title, - required Widget filterBtn, - }) { - return Scaffold( - appBar: AppBar( title: title, actions: [filterBtn], ), diff --git a/lib/src/view/user/leaderboard_screen.dart b/lib/src/view/user/leaderboard_screen.dart index 52d408725a..c544eecc24 100644 --- a/lib/src/view/user/leaderboard_screen.dart +++ b/lib/src/view/user/leaderboard_screen.dart @@ -1,6 +1,5 @@ import 'dart:math' as math; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_layout_grid/flutter_layout_grid.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -12,7 +11,7 @@ import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/user/user_screen.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:lichess_mobile/src/widgets/user_full_name.dart'; /// Create a Screen with Top 10 players for each Lichess Variant @@ -21,19 +20,8 @@ class LeaderboardScreen extends StatelessWidget { @override Widget build(BuildContext context) { - return PlatformWidget(androidBuilder: _buildAndroid, iosBuilder: _buildIos); - } - - Widget _buildIos(BuildContext context) { - return const CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar(), - child: _Body(), - ); - } - - Widget _buildAndroid(BuildContext context) { - return Scaffold( - appBar: AppBar( + return PlatformScaffold( + appBar: PlatformAppBar( title: Text(context.l10n.leaderboard), ), body: const _Body(), diff --git a/lib/src/view/user/perf_stats_screen.dart b/lib/src/view/user/perf_stats_screen.dart index 8b447d669c..d2b0562f9b 100644 --- a/lib/src/view/user/perf_stats_screen.dart +++ b/lib/src/view/user/perf_stats_screen.dart @@ -30,6 +30,7 @@ import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:lichess_mobile/src/widgets/rating.dart'; import 'package:lichess_mobile/src/widgets/stat_card.dart'; import 'package:lichess_mobile/src/widgets/user_full_name.dart'; @@ -54,30 +55,14 @@ class PerfStatsScreen extends StatelessWidget { @override Widget build(BuildContext context) { - return PlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, - ); - } - - Widget _androidBuilder(BuildContext context) { - return Scaffold( - appBar: AppBar( - titleSpacing: 0, + return PlatformScaffold( + appBar: PlatformAppBar( + androidTitleSpacing: 0, title: _Title(user: user, perf: perf), ), body: _Body(user: user, perf: perf), ); } - - Widget _iosBuilder(BuildContext context) { - return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - middle: _Title(user: user, perf: perf), - ), - child: _Body(user: user, perf: perf), - ); - } } class _Title extends StatelessWidget { diff --git a/lib/src/view/user/player_screen.dart b/lib/src/view/user/player_screen.dart index 4719e6926e..0a38424f62 100644 --- a/lib/src/view/user/player_screen.dart +++ b/lib/src/view/user/player_screen.dart @@ -17,6 +17,7 @@ import 'package:lichess_mobile/src/view/user/user_screen.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; import 'package:lichess_mobile/src/widgets/user_full_name.dart'; @@ -34,26 +35,12 @@ class PlayerScreen extends ConsumerWidget { ref.read(onlineFriendsProvider.notifier).stopWatchingFriends(); } }, - child: PlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, - ), - ); - } - - Widget _androidBuilder(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(context.l10n.players), + child: PlatformScaffold( + appBar: PlatformAppBar( + title: Text(context.l10n.players), + ), + body: _Body(), ), - body: _Body(), - ); - } - - Widget _iosBuilder(BuildContext context) { - return CupertinoPageScaffold( - navigationBar: const CupertinoNavigationBar(), - child: _Body(), ); } } diff --git a/lib/src/view/user/user_screen.dart b/lib/src/view/user/user_screen.dart index 14250e25e5..1607ba874a 100644 --- a/lib/src/view/user/user_screen.dart +++ b/lib/src/view/user/user_screen.dart @@ -1,4 +1,3 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:http/http.dart' show ClientException; @@ -12,7 +11,7 @@ import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/view/user/recent_games.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:lichess_mobile/src/widgets/user_full_name.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -32,45 +31,27 @@ class UserScreen extends ConsumerStatefulWidget { class _UserScreenState extends ConsumerState { bool isLoading = false; - @override - Widget build(BuildContext context) { - return ConsumerPlatformWidget( - ref: ref, - androidBuilder: _buildAndroid, - iosBuilder: _buildIos, - ); - } - void setIsLoading(bool value) { setState(() { isLoading = value; }); } - Widget _buildAndroid(BuildContext context, WidgetRef ref) { + @override + Widget build(BuildContext context) { final asyncUser = ref.watch(userAndStatusProvider(id: widget.user.id)); final updatedLightUser = asyncUser.maybeWhen( data: (data) => data.$1.lightUser.copyWith(isOnline: data.$2.online), orElse: () => null, ); - return Scaffold( - appBar: AppBar( + return PlatformScaffold( + appBar: PlatformAppBar( title: UserFullNameWidget( user: updatedLightUser ?? widget.user, shouldShowOnline: updatedLightUser != null, ), actions: [ - if (isLoading) - const Padding( - padding: EdgeInsets.only(right: 16), - child: SizedBox( - height: 24, - width: 24, - child: Center( - child: CircularProgressIndicator(), - ), - ), - ), + if (isLoading) const PlatformAppBarLoadingIndicator(), ], ), body: asyncUser.when( @@ -93,44 +74,6 @@ class _UserScreenState extends ConsumerState { ), ); } - - Widget _buildIos(BuildContext context, WidgetRef ref) { - final asyncUser = ref.watch(userAndStatusProvider(id: widget.user.id)); - final updatedLightUser = asyncUser.maybeWhen( - data: (data) => data.$1.lightUser.copyWith(isOnline: data.$2.online), - orElse: () => null, - ); - return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - middle: UserFullNameWidget( - user: updatedLightUser ?? widget.user, - shouldShowOnline: updatedLightUser != null, - ), - trailing: isLoading ? const CircularProgressIndicator.adaptive() : null, - ), - child: asyncUser.when( - data: (data) => SafeArea( - child: _UserProfileListView(data.$1, isLoading, setIsLoading), - ), - loading: () => - const Center(child: CircularProgressIndicator.adaptive()), - error: (error, _) { - if (error is ClientException && error.message.contains('404')) { - return Center( - child: Text( - textAlign: TextAlign.center, - context.l10n.usernameNotFound(widget.user.name), - style: Styles.bold, - ), - ); - } - return FullScreenRetryRequest( - onRetry: () => ref.invalidate(userProvider(id: widget.user.id)), - ); - }, - ), - ); - } } class _UserProfileListView extends ConsumerWidget { diff --git a/lib/src/view/watch/live_tv_channels_screen.dart b/lib/src/view/watch/live_tv_channels_screen.dart index 7640f7f456..2b3eec375a 100644 --- a/lib/src/view/watch/live_tv_channels_screen.dart +++ b/lib/src/view/watch/live_tv_channels_screen.dart @@ -1,4 +1,3 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/constants.dart'; @@ -9,7 +8,7 @@ import 'package:lichess_mobile/src/utils/focus_detector.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/watch/tv_screen.dart'; import 'package:lichess_mobile/src/widgets/board_preview.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:lichess_mobile/src/widgets/user_full_name.dart'; class LiveTvChannelsScreen extends ConsumerWidget { @@ -26,34 +25,14 @@ class LiveTvChannelsScreen extends ConsumerWidget { ref.read(liveTvChannelsProvider.notifier).stopWatching(); } }, - child: PlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, + child: const PlatformScaffold( + appBar: PlatformAppBar( + title: Text('Lichess TV'), + ), + body: _Body(), ), ); } - - Widget _androidBuilder( - BuildContext context, - ) { - return Scaffold( - appBar: AppBar( - title: const Text('Lichess TV'), - ), - body: const _Body(), - ); - } - - Widget _iosBuilder( - BuildContext context, - ) { - return const CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - middle: Text('Lichess TV'), - ), - child: _Body(), - ); - } } class _Body extends ConsumerWidget { diff --git a/lib/src/view/watch/tv_screen.dart b/lib/src/view/watch/tv_screen.dart index 2a475768af..c73a58b177 100644 --- a/lib/src/view/watch/tv_screen.dart +++ b/lib/src/view/watch/tv_screen.dart @@ -8,7 +8,6 @@ import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/model/tv/tv_channel.dart'; import 'package:lichess_mobile/src/model/tv/tv_controller.dart'; -import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/focus_detector.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/view/game/game_player.dart'; @@ -17,7 +16,7 @@ import 'package:lichess_mobile/src/widgets/board_table.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/countdown_clock.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; class TvScreen extends ConsumerStatefulWidget { const TvScreen({required this.channel, this.initialGame, super.key}); @@ -47,46 +46,19 @@ class _TvScreenState extends ConsumerState { ref.read(_tvGameCtrl.notifier).stopWatching(); } }, - child: PlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, - ), - ); - } - - Widget _androidBuilder( - BuildContext context, - ) { - return Scaffold( - appBar: AppBar( - title: Text('${widget.channel.label} TV'), - actions: [ - ToggleSoundButton(), - ], - ), - body: _Body( - widget.channel, - widget.initialGame, - whiteClockKey: _whiteClockKey, - blackClockKey: _blackClockKey, - ), - ); - } - - Widget _iosBuilder( - BuildContext context, - ) { - return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - padding: Styles.cupertinoAppBarTrailingWidgetPadding, - middle: Text('${widget.channel.label} TV'), - trailing: ToggleSoundButton(), - ), - child: _Body( - widget.channel, - widget.initialGame, - whiteClockKey: _whiteClockKey, - blackClockKey: _blackClockKey, + child: PlatformScaffold( + appBar: PlatformAppBar( + title: Text('${widget.channel.label} TV'), + actions: [ + ToggleSoundButton(), + ], + ), + body: _Body( + widget.channel, + widget.initialGame, + whiteClockKey: _whiteClockKey, + blackClockKey: _blackClockKey, + ), ), ); } diff --git a/lib/src/widgets/platform_scaffold.dart b/lib/src/widgets/platform_scaffold.dart new file mode 100644 index 0000000000..8a9776f017 --- /dev/null +++ b/lib/src/widgets/platform_scaffold.dart @@ -0,0 +1,177 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/widgets/platform.dart'; + +/// Displays an [AppBar] for Android and a [CupertinoNavigationBar] for iOS. +/// +/// Intended to be passed to [PlatformScaffold]. +class PlatformAppBar extends StatelessWidget { + const PlatformAppBar({ + super.key, + required this.title, + this.centerTitle = false, + this.leading, + this.actions = const [], + this.cupertinoPadding, + this.androidTitleSpacing, + }); + + /// Widget to place at the start of the navigation bar + /// + /// See [AppBar.leading] and [CupertinoNavigationBar.leading] for details + final Widget? leading; + + /// The title displayed in the middle of the bar. + /// + /// On Android, this is [AppBar.title], on iOS [CupertinoNavigationBar.middle] + final Widget title; + + /// On Android, this is passed directly to [AppBar.centerTitle]. Has no effect on iOS. + final bool centerTitle; + + /// Action widgets to place at the end of the navigation bar. + /// + /// On Android, this is passed directlty to [AppBar.actions]. + /// On iOS, this is wrapped in a [Row] and passed to [CupertinoNavigationBar.trailing] + final List actions; + + /// Will be passed to [CupertinoNavigationBar.padding] on iOS. Has no effect on Android. + /// + /// If null, will use [Styles.cupertinoAppBarTrailingWidgetPadding]. + final EdgeInsetsDirectional? cupertinoPadding; + + /// Will be passed to [AppBar.titleSpacing] on Android. Has no effect on iOS. + final double? androidTitleSpacing; + + AppBar _androidBuilder(BuildContext context) { + return AppBar( + key: key, + titleSpacing: androidTitleSpacing, + leading: leading, + title: title, + centerTitle: centerTitle, + actions: actions, + ); + } + + CupertinoNavigationBar _iosBuilder(BuildContext context) { + return CupertinoNavigationBar( + key: key, + backgroundColor: Styles.cupertinoScaffoldColor.resolveFrom(context), + border: null, + padding: cupertinoPadding ?? Styles.cupertinoAppBarTrailingWidgetPadding, + middle: title, + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: actions, + ), + ); + } + + @override + Widget build(BuildContext context) { + return PlatformWidget( + androidBuilder: _androidBuilder, + iosBuilder: _iosBuilder, + ); + } +} + +/// A platform-aware circular loading indicator to be used in [PlatformAppBar.actions]. +class PlatformAppBarLoadingIndicator extends StatelessWidget { + const PlatformAppBarLoadingIndicator({super.key}); + + @override + Widget build(BuildContext context) { + return PlatformWidget( + iosBuilder: (_) => const CircularProgressIndicator.adaptive(), + androidBuilder: (_) => const Padding( + padding: EdgeInsets.only(right: 16), + child: SizedBox( + height: 24, + width: 24, + child: Center( + child: CircularProgressIndicator.adaptive(), + ), + ), + ), + ); + } +} + +class _CupertinoNavBarWrapper extends StatelessWidget + implements ObstructingPreferredSizeWidget { + const _CupertinoNavBarWrapper({ + required this.child, + }); + + final Widget child; + + @override + Widget build(BuildContext context) => child; + + @override + Size get preferredSize => + const Size.fromHeight(kMinInteractiveDimensionCupertino); + + /// True if the navigation bar's background color has no transparency. + @override + bool shouldFullyObstruct(BuildContext context) { + final Color backgroundColor = CupertinoTheme.of(context).barBackgroundColor; + return backgroundColor.alpha == 0xFF; + } +} + +/// A screen with a navigation bar and a body that adapts to the platform. +/// +/// On Android, this is a [Scaffold] with an [AppBar], +/// on iOS a [CupertinoPageScaffold] with a [CupertinoNavigationBar]. +/// +/// See [PlatformAppBar] for an app bar that adapts to the platform and needs to be passed to this widget. +class PlatformScaffold extends StatelessWidget { + const PlatformScaffold({ + super.key, + required this.appBar, + required this.body, + this.resizeToAvoidBottomInset = true, + }); + + /// Acts as the [AppBar] for Android and as the [CupertinoNavigationBar] for iOS. + /// + /// Usually an instance of [PlatformAppBar]. + final Widget appBar; + + /// The main content of the screen, displayed below the navigation bar. + final Widget body; + + /// See [Scaffold.resizeToAvoidBottomInset] and [CupertinoPageScaffold.resizeToAvoidBottomInset] + final bool resizeToAvoidBottomInset; + + Widget _androidBuilder(BuildContext context) { + return Scaffold( + resizeToAvoidBottomInset: resizeToAvoidBottomInset, + appBar: PreferredSize( + preferredSize: const Size.fromHeight(kToolbarHeight), + child: appBar, + ), + body: body, + ); + } + + Widget _iosBuilder(BuildContext context) { + return CupertinoPageScaffold( + resizeToAvoidBottomInset: resizeToAvoidBottomInset, + navigationBar: _CupertinoNavBarWrapper(child: appBar), + child: body, + ); + } + + @override + Widget build(BuildContext context) { + return PlatformWidget( + androidBuilder: _androidBuilder, + iosBuilder: _iosBuilder, + ); + } +} From 1186528ae01117a5e58e0e147686a2d116dda99c Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 2 Sep 2024 20:23:59 +0200 Subject: [PATCH 188/979] Upgrade dependencies --- pubspec.lock | 52 ++++++++++++++++++++++++++-------------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 0084f24bee..358b3002ff 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -13,10 +13,10 @@ packages: dependency: transitive description: name: _flutterfire_internals - sha256: b1595874fbc8f7a50da90f5d8f327bb0bfd6a95dc906c390efe991540c3b54aa + sha256: "9371d13b8ee442e3bfc08a24e3a1b3742c839abbfaf5eef11b79c4b862c89bf7" url: "https://pub.dev" source: hosted - version: "1.3.40" + version: "1.3.41" _macros: dependency: transitive description: dart @@ -362,10 +362,10 @@ packages: dependency: "direct main" description: name: deep_pick - sha256: ba19f93cae54e5d601c0d2c11f873bf0038fc6207cc7ab23115f4a5873bb917f + sha256: "79d96b94a9c9ca2e823f05f72ec1a4efcc5ad5fd3875f46d1a8fe61ebbd9f359" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.0" device_info_plus: dependency: "direct main" description: @@ -434,66 +434,66 @@ packages: dependency: "direct main" description: name: firebase_core - sha256: "3187f4f8e49968573fd7403011dca67ba95aae419bc0d8131500fae160d94f92" + sha256: "06537da27db981947fa535bb91ca120b4e9cb59cb87278dbdde718558cafc9ff" url: "https://pub.dev" source: hosted - version: "3.3.0" + version: "3.4.0" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface - sha256: "3c3a1e92d6f4916c32deea79c4a7587aa0e9dbbe5889c7a16afcf005a485ee02" + sha256: f7d7180c7f99babd4b4c517754d41a09a4943a0f7a69b65c894ca5c68ba66315 url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "5.2.1" firebase_core_web: dependency: transitive description: name: firebase_core_web - sha256: e8d1e22de72cb21cdcfc5eed7acddab3e99cd83f3b317f54f7a96c32f25fd11e + sha256: "362e52457ed2b7b180964769c1e04d1e0ea0259fdf7025fdfedd019d4ae2bd88" url: "https://pub.dev" source: hosted - version: "2.17.4" + version: "2.17.5" firebase_crashlytics: dependency: "direct main" description: name: firebase_crashlytics - sha256: "30260e1b8ad1464b41ca4531b44ce63d752daaf2f12c92ca6cdcd82b270abecc" + sha256: "4c9872020c0d97a161362ee6af7000cfdb8666234ddc290a15252ad379bb235a" url: "https://pub.dev" source: hosted - version: "4.0.4" + version: "4.1.0" firebase_crashlytics_platform_interface: dependency: transitive description: name: firebase_crashlytics_platform_interface - sha256: a75e1826d92ea4e86e4a753c7b5d64b844a362676fa653185f1581c859186d18 + sha256: ede8a199ff03378857d3c8cbb7fa58d37c27bb5a6b75faf8415ff6925dcaae2a url: "https://pub.dev" source: hosted - version: "3.6.40" + version: "3.6.41" firebase_messaging: dependency: "direct main" description: name: firebase_messaging - sha256: "1b0a4f9ecbaf9007771bac152afad738ddfacc4b8431a7591c00829480d99553" + sha256: "29941ba5a3204d80656c0e52103369aa9a53edfd9ceae05a2bb3376f24fda453" url: "https://pub.dev" source: hosted - version: "15.0.4" + version: "15.1.0" firebase_messaging_platform_interface: dependency: transitive description: name: firebase_messaging_platform_interface - sha256: c5a6443e66ae064fe186901d740ee7ce648ca2a6fd0484b8c5e963849ac0fc28 + sha256: "26c5370d3a79b15c8032724a68a4741e28f63e1f1a45699c4f0a8ae740aadd72" url: "https://pub.dev" source: hosted - version: "4.5.42" + version: "4.5.43" firebase_messaging_web: dependency: transitive description: name: firebase_messaging_web - sha256: "232ef63b986467ae5b5577a09c2502b26e2e2aebab5b85e6c966a5ca9b038b89" + sha256: "58276cd5d9e22a9320ef9e5bc358628920f770f93c91221f8b638e8346ed5df4" url: "https://pub.dev" source: hosted - version: "3.8.12" + version: "3.8.13" fixnum: dependency: transitive description: @@ -527,10 +527,10 @@ packages: dependency: transitive description: name: flutter_appauth_platform_interface - sha256: fede468075c149200ba792520cf16fee5ed7750fc446a0839c42fcc9364e31b8 + sha256: "0959824b401f3ee209c869734252bd5d4d4aab804b019c03815c56e3b9a4bc34" url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.1" flutter_cache_manager: dependency: transitive description: @@ -1087,10 +1087,10 @@ packages: dependency: transitive description: name: quiver - sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47 + sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2 url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.2" result_extensions: dependency: "direct main" description: @@ -1493,10 +1493,10 @@ packages: dependency: transitive description: name: uuid - sha256: "83d37c7ad7aaf9aa8e275490669535c8080377cfa7a7004c24dfac53afffaa90" + sha256: f33d6bb662f0e4f79dcd7ada2e6170f3b3a2530c28fc41f49a411ddedd576a77 url: "https://pub.dev" source: hosted - version: "4.4.2" + version: "4.5.0" vector_graphics: dependency: transitive description: From e385531119733b565fc70d0cd7507a12486f4093 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 3 Sep 2024 09:03:19 +0200 Subject: [PATCH 189/979] Update podfile --- ios/Podfile.lock | 168 +++++++++++++++++++++++------------------------ 1 file changed, 83 insertions(+), 85 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 74671449b0..7150ebad0c 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -14,65 +14,65 @@ PODS: - Flutter - device_info_plus (0.0.1): - Flutter - - Firebase/CoreOnly (10.29.0): - - FirebaseCore (= 10.29.0) - - Firebase/Crashlytics (10.29.0): + - Firebase/CoreOnly (11.0.0): + - FirebaseCore (= 11.0.0) + - Firebase/Crashlytics (11.0.0): - Firebase/CoreOnly - - FirebaseCrashlytics (~> 10.29.0) - - Firebase/Messaging (10.29.0): + - FirebaseCrashlytics (~> 11.0.0) + - Firebase/Messaging (11.0.0): - Firebase/CoreOnly - - FirebaseMessaging (~> 10.29.0) - - firebase_core (3.3.0): - - Firebase/CoreOnly (= 10.29.0) + - FirebaseMessaging (~> 11.0.0) + - firebase_core (3.4.0): + - Firebase/CoreOnly (= 11.0.0) - Flutter - - firebase_crashlytics (4.0.4): - - Firebase/Crashlytics (= 10.29.0) + - firebase_crashlytics (4.1.0): + - Firebase/Crashlytics (= 11.0.0) - firebase_core - Flutter - - firebase_messaging (15.0.4): - - Firebase/Messaging (= 10.29.0) + - firebase_messaging (15.1.0): + - Firebase/Messaging (= 11.0.0) - firebase_core - Flutter - - FirebaseCore (10.29.0): - - FirebaseCoreInternal (~> 10.0) - - GoogleUtilities/Environment (~> 7.12) - - GoogleUtilities/Logger (~> 7.12) - - FirebaseCoreExtension (10.29.0): - - FirebaseCore (~> 10.0) - - FirebaseCoreInternal (10.29.0): - - "GoogleUtilities/NSData+zlib (~> 7.8)" - - FirebaseCrashlytics (10.29.0): - - FirebaseCore (~> 10.5) - - FirebaseInstallations (~> 10.0) - - FirebaseRemoteConfigInterop (~> 10.23) - - FirebaseSessions (~> 10.5) - - GoogleDataTransport (~> 9.2) - - GoogleUtilities/Environment (~> 7.8) - - nanopb (< 2.30911.0, >= 2.30908.0) - - PromisesObjC (~> 2.1) - - FirebaseInstallations (10.29.0): - - FirebaseCore (~> 10.0) - - GoogleUtilities/Environment (~> 7.8) - - GoogleUtilities/UserDefaults (~> 7.8) - - PromisesObjC (~> 2.1) - - FirebaseMessaging (10.29.0): - - FirebaseCore (~> 10.0) - - FirebaseInstallations (~> 10.0) - - GoogleDataTransport (~> 9.3) - - GoogleUtilities/AppDelegateSwizzler (~> 7.8) - - GoogleUtilities/Environment (~> 7.8) - - GoogleUtilities/Reachability (~> 7.8) - - GoogleUtilities/UserDefaults (~> 7.8) - - nanopb (< 2.30911.0, >= 2.30908.0) - - FirebaseRemoteConfigInterop (10.29.0) - - FirebaseSessions (10.29.0): - - FirebaseCore (~> 10.5) - - FirebaseCoreExtension (~> 10.0) - - FirebaseInstallations (~> 10.0) - - GoogleDataTransport (~> 9.2) - - GoogleUtilities/Environment (~> 7.13) - - GoogleUtilities/UserDefaults (~> 7.13) - - nanopb (< 2.30911.0, >= 2.30908.0) + - FirebaseCore (11.0.0): + - FirebaseCoreInternal (~> 11.0) + - GoogleUtilities/Environment (~> 8.0) + - GoogleUtilities/Logger (~> 8.0) + - FirebaseCoreExtension (11.1.0): + - FirebaseCore (~> 11.0) + - FirebaseCoreInternal (11.1.0): + - "GoogleUtilities/NSData+zlib (~> 8.0)" + - FirebaseCrashlytics (11.0.0): + - FirebaseCore (~> 11.0) + - FirebaseInstallations (~> 11.0) + - FirebaseRemoteConfigInterop (~> 11.0) + - FirebaseSessions (~> 11.0) + - GoogleDataTransport (~> 10.0) + - GoogleUtilities/Environment (~> 8.0) + - nanopb (~> 3.30910.0) + - PromisesObjC (~> 2.4) + - FirebaseInstallations (11.1.0): + - FirebaseCore (~> 11.0) + - GoogleUtilities/Environment (~> 8.0) + - GoogleUtilities/UserDefaults (~> 8.0) + - PromisesObjC (~> 2.4) + - FirebaseMessaging (11.0.0): + - FirebaseCore (~> 11.0) + - FirebaseInstallations (~> 11.0) + - GoogleDataTransport (~> 10.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.0) + - GoogleUtilities/Environment (~> 8.0) + - GoogleUtilities/Reachability (~> 8.0) + - GoogleUtilities/UserDefaults (~> 8.0) + - nanopb (~> 3.30910.0) + - FirebaseRemoteConfigInterop (11.1.0) + - FirebaseSessions (11.1.0): + - FirebaseCore (~> 11.0) + - FirebaseCoreExtension (~> 11.0) + - FirebaseInstallations (~> 11.0) + - GoogleDataTransport (~> 10.0) + - GoogleUtilities/Environment (~> 8.0) + - GoogleUtilities/UserDefaults (~> 8.0) + - nanopb (~> 3.30910.0) - PromisesSwift (~> 2.1) - Flutter (1.0.0) - flutter_appauth (0.0.1): @@ -82,40 +82,38 @@ PODS: - Flutter - flutter_secure_storage (6.0.0): - Flutter - - GoogleDataTransport (9.4.1): - - GoogleUtilities/Environment (~> 7.7) - - nanopb (< 2.30911.0, >= 2.30908.0) - - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/AppDelegateSwizzler (7.13.3): + - GoogleDataTransport (10.1.0): + - nanopb (~> 3.30910.0) + - PromisesObjC (~> 2.4) + - GoogleUtilities/AppDelegateSwizzler (8.0.2): - GoogleUtilities/Environment - GoogleUtilities/Logger - GoogleUtilities/Network - GoogleUtilities/Privacy - - GoogleUtilities/Environment (7.13.3): + - GoogleUtilities/Environment (8.0.2): - GoogleUtilities/Privacy - - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/Logger (7.13.3): + - GoogleUtilities/Logger (8.0.2): - GoogleUtilities/Environment - GoogleUtilities/Privacy - - GoogleUtilities/Network (7.13.3): + - GoogleUtilities/Network (8.0.2): - GoogleUtilities/Logger - "GoogleUtilities/NSData+zlib" - GoogleUtilities/Privacy - GoogleUtilities/Reachability - - "GoogleUtilities/NSData+zlib (7.13.3)": + - "GoogleUtilities/NSData+zlib (8.0.2)": - GoogleUtilities/Privacy - - GoogleUtilities/Privacy (7.13.3) - - GoogleUtilities/Reachability (7.13.3): + - GoogleUtilities/Privacy (8.0.2) + - GoogleUtilities/Reachability (8.0.2): - GoogleUtilities/Logger - GoogleUtilities/Privacy - - GoogleUtilities/UserDefaults (7.13.3): + - GoogleUtilities/UserDefaults (8.0.2): - GoogleUtilities/Logger - GoogleUtilities/Privacy - - nanopb (2.30910.0): - - nanopb/decode (= 2.30910.0) - - nanopb/encode (= 2.30910.0) - - nanopb/decode (2.30910.0) - - nanopb/encode (2.30910.0) + - nanopb (3.30910.0): + - nanopb/decode (= 3.30910.0) + - nanopb/encode (= 3.30910.0) + - nanopb/decode (3.30910.0) + - nanopb/encode (3.30910.0) - package_info_plus (0.4.5): - Flutter - path_provider_foundation (0.0.1): @@ -229,25 +227,25 @@ SPEC CHECKSUMS: connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db cupertino_http: 1a3a0f163c1b26e7f1a293b33d476e0fde7a64ec device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d - Firebase: cec914dab6fd7b1bd8ab56ea07ce4e03dd251c2d - firebase_core: 57aeb91680e5d5e6df6b888064be7c785f146efb - firebase_crashlytics: e3d3e0c99bad5aaab5908385133dea8ec344693f - firebase_messaging: c862b3d2b973ecc769194dc8de09bd22c77ae757 - FirebaseCore: 30e9c1cbe3d38f5f5e75f48bfcea87d7c358ec16 - FirebaseCoreExtension: 705ca5b14bf71d2564a0ddc677df1fc86ffa600f - FirebaseCoreInternal: df84dd300b561c27d5571684f389bf60b0a5c934 - FirebaseCrashlytics: 34647b41e18de773717fdd348a22206f2f9bc774 - FirebaseInstallations: 913cf60d0400ebd5d6b63a28b290372ab44590dd - FirebaseMessaging: 7b5d8033e183ab59eb5b852a53201559e976d366 - FirebaseRemoteConfigInterop: 6efda51fb5e2f15b16585197e26eaa09574e8a4d - FirebaseSessions: dbd14adac65ce996228652c1fc3a3f576bdf3ecc + Firebase: 9f574c08c2396885b5e7e100ed4293d956218af9 + firebase_core: ceec591a66629daaee82d3321551692c4a871493 + firebase_crashlytics: e4f04180f443d5a8b56fbc0685bdbd7d90dd26f0 + firebase_messaging: 15d8b557010f3bb7b98d0302e1c7c8fbcd244425 + FirebaseCore: 3cf438f431f18c12cdf2aaf64434648b63f7e383 + FirebaseCoreExtension: aa5c9779c2d0d39d83f1ceb3fdbafe80c4feecfa + FirebaseCoreInternal: adefedc9a88dbe393c4884640a73ec9e8e790f8c + FirebaseCrashlytics: 745d8f0221fe49c62865391d1bf56f5a12eeec0b + FirebaseInstallations: d0a8fea5a6fa91abc661591cf57c0f0d70863e57 + FirebaseMessaging: d2d1d9c62c46dd2db49a952f7deb5b16ad2c9742 + FirebaseRemoteConfigInterop: abf8b1bbc0bf1b84abd22b66746926410bf91a87 + FirebaseSessions: 78f137e68dc01ca71606169ba4ac73b98c13752a Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_appauth: 1ce438877bc111c5d8f42da47729909290624886 flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778 flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12 - GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a - GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15 - nanopb: 438bc412db1928dac798aa6fd75726007be04262 + GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 + GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d + nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 From 1d916811b34fe0d01ba9dfd132de354a342d9d4c Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 2 Sep 2024 13:32:45 +0200 Subject: [PATCH 190/979] WIP on instant premoves --- lib/src/constants.dart | 8 - .../model/analysis/analysis_controller.dart | 25 ++- lib/src/model/common/chess.dart | 8 + lib/src/model/game/game_controller.dart | 51 +++++- lib/src/model/puzzle/puzzle_controller.dart | 25 ++- lib/src/model/puzzle/storm_controller.dart | 28 ++- lib/src/view/analysis/analysis_board.dart | 167 ++++++++++-------- lib/src/view/analysis/analysis_screen.dart | 2 +- .../offline_correspondence_game_screen.dart | 49 +++-- lib/src/view/game/archived_game_screen.dart | 19 +- lib/src/view/game/game_body.dart | 56 +++--- lib/src/view/game/game_loading_board.dart | 30 ++-- .../opening_explorer_screen.dart | 6 +- lib/src/view/puzzle/puzzle_screen.dart | 67 +++---- lib/src/view/puzzle/storm_screen.dart | 32 ++-- lib/src/view/puzzle/streak_screen.dart | 36 ++-- lib/src/view/settings/theme_screen.dart | 41 ++--- lib/src/view/watch/tv_screen.dart | 21 +-- lib/src/widgets/board_carousel_item.dart | 11 +- lib/src/widgets/board_preview.dart | 11 +- lib/src/widgets/board_table.dart | 29 +-- lib/src/widgets/board_thumbnail.dart | 11 +- pubspec.lock | 9 +- pubspec.yaml | 3 +- test/view/game/archived_game_screen_test.dart | 7 +- 25 files changed, 443 insertions(+), 309 deletions(-) diff --git a/lib/src/constants.dart b/lib/src/constants.dart index d9e52edc2d..da60c02e8f 100644 --- a/lib/src/constants.dart +++ b/lib/src/constants.dart @@ -1,5 +1,3 @@ -import 'package:chessground/chessground.dart'; -import 'package:dartchess/dartchess.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -60,12 +58,6 @@ const kTabletBoardTableSidePadding = 16.0; const kBottomBarHeight = 56.0; const kMaterialPopupMenuMaxWidth = 500.0; -const ChessboardState kEmptyBoardState = ChessboardState( - fen: kEmptyFen, - interactableSide: InteractableSide.none, - orientation: Side.white, -); - /// The threshold to detect screens with a small remaining height left board. const kSmallRemainingHeightLeftBoardThreshold = 160; diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index 2df9698a2b..f6b2ae0a58 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -177,8 +177,14 @@ class AnalysisController extends _$AnalysisController { initialPosition: _root.position, ); - void onUserMove(Move move) { + void onUserMove(NormalMove move) { if (!state.position.isLegal(move)) return; + + if (isPromotionPawnMove(state.position, move)) { + state = state.copyWith(promotionMove: move); + return; + } + final (newPath, isNewNode) = _root.addMoveAt(state.currentPath, move); if (newPath != null) { _setPath( @@ -189,6 +195,18 @@ class AnalysisController extends _$AnalysisController { } } + void onPromotionSelect(Role role) { + final promotionMove = state.promotionMove; + if (promotionMove != null) { + final promotion = promotionMove.withPromotion(role); + onUserMove(promotion); + } + } + + void onPromotionCancel() { + state = state.copyWith(promotionMove: null); + } + void userNext() { if (!state.currentNode.hasChild) return; _setPath( @@ -417,6 +435,7 @@ class AnalysisController extends _$AnalysisController { currentBranchOpening: opening, wikiBooksUrl: _wikiBooksUrl(path), lastMove: currentNode.sanMove.move, + promotionMove: null, root: rootView, ); } else { @@ -427,6 +446,7 @@ class AnalysisController extends _$AnalysisController { currentBranchOpening: opening, wikiBooksUrl: _wikiBooksUrl(path), lastMove: null, + promotionMove: null, root: rootView, ); } @@ -651,6 +671,9 @@ class AnalysisState with _$AnalysisState { /// The last move played. Move? lastMove, + /// Possible promotion move to be played. + NormalMove? promotionMove, + /// Opening of the analysis context (from lichess archived games). Opening? contextOpening, diff --git a/lib/src/model/common/chess.dart b/lib/src/model/common/chess.dart index 51e838c7b4..896b02d7c4 100644 --- a/lib/src/model/common/chess.dart +++ b/lib/src/model/common/chess.dart @@ -46,6 +46,14 @@ const altCastles = { 'e8h8': 'e8g8', }; +/// Returns `true` if the move is a pawn promotion move that is not yet promoted. +bool isPromotionPawnMove(Position position, NormalMove move) { + return move.promotion == null && + position.board.roleAt(move.from) == Role.pawn && + ((move.to.rank == Rank.first && position.turn == Side.black) || + (move.to.rank == Rank.eighth && position.turn == Side.white)); +} + /// Set of supported variants for reading a game (not playing). const ISet readSupportedVariants = ISetConst({ Variant.standard, diff --git a/lib/src/model/game/game_controller.dart b/lib/src/model/game/game_controller.dart index a53577fa3a..90b512b5c1 100644 --- a/lib/src/model/game/game_controller.dart +++ b/lib/src/model/game/game_controller.dart @@ -112,9 +112,16 @@ class GameController extends _$GameController { ); } - void onUserMove(Move move, {bool? isDrop, bool? isPremove}) { + void userMove(NormalMove move, {bool? isDrop, bool? isPremove}) { final curState = state.requireValue; + if (isPromotionPawnMove(curState.game.lastPosition, move)) { + state = AsyncValue.data( + curState.copyWith(promotionMove: move), + ); + return; + } + final (newPos, newSan) = curState.game.lastPosition.makeSan(move); final sanMove = SanMove(newSan, move); final newStep = GameStep( @@ -133,6 +140,8 @@ class GameController extends _$GameController { stepCursor: curState.stepCursor + 1, stopClockWaitingForServerAck: !shouldConfirmMove, moveToConfirm: shouldConfirmMove ? move : null, + promotionMove: null, + premove: null, ), ); @@ -151,6 +160,24 @@ class GameController extends _$GameController { } } + void onPromotionSelect(Role role) { + final curState = state.requireValue; + if (curState.promotionMove == null) { + assert(false, 'promotionMove must not be null on promotion select'); + return; + } + + final move = curState.promotionMove!.withPromotion(role); + userMove(move, isDrop: true); + } + + void onPromotionCancel() { + final curState = state.requireValue; + state = AsyncValue.data( + curState.copyWith(promotionMove: null), + ); + } + /// Called if the player cancels the move when confirm move preference is enabled void cancelMove() { final curState = state.requireValue; @@ -195,7 +222,7 @@ class GameController extends _$GameController { } /// Set or unset a premove. - void setPremove(Move? move) { + void setPremove(NormalMove? move) { final curState = state.requireValue; state = AsyncValue.data( curState.copyWith( @@ -597,6 +624,19 @@ class GameController extends _$GameController { .updateGame(gameFullId, newState.game); } + if (!curState.isReplaying && + playedSide == curState.game.youAre?.opposite && + curState.premove != null) { + Timer.run(() { + final postMovePremove = state.valueOrNull?.premove; + final postMovePosition = state.valueOrNull?.game.lastPosition; + if (postMovePremove != null && + postMovePosition?.isLegal(postMovePremove) == true) { + userMove(postMovePremove, isPremove: true); + } + }); + } + state = AsyncValue.data(newState); // End game event @@ -923,7 +963,12 @@ class GameState with _$GameState { int? lastDrawOfferAtPly, Duration? opponentLeftCountdown, required bool stopClockWaitingForServerAck, - Move? premove, + + /// Promotion waiting to be selected (only if auto queen is disabled) + NormalMove? promotionMove, + + /// Premove waiting to be played + NormalMove? premove, /// Game only setting to override the account preference bool? moveConfirmSettingOverride, diff --git a/lib/src/model/puzzle/puzzle_controller.dart b/lib/src/model/puzzle/puzzle_controller.dart index fd0c00700e..2ac34bf542 100644 --- a/lib/src/model/puzzle/puzzle_controller.dart +++ b/lib/src/model/puzzle/puzzle_controller.dart @@ -113,7 +113,12 @@ class PuzzleController extends _$PuzzleController { ); } - Future onUserMove(Move move) async { + Future onUserMove(NormalMove move) async { + if (isPromotionPawnMove(state.position, move)) { + state = state.copyWith(promotionMove: move); + return; + } + _addMove(move); if (state.mode == PuzzleMode.play) { @@ -157,6 +162,20 @@ class PuzzleController extends _$PuzzleController { } } + void onPromotionSelect(Role role) { + final promotionMove = state.promotionMove; + if (promotionMove != null) { + final move = promotionMove.withPromotion(role); + onUserMove(move); + } + } + + void onPromotionCancel() { + state = state.copyWith( + promotionMove: null, + ); + } + void userNext() { _viewSolutionTimer?.cancel(); _goToNextNode(replaying: true); @@ -205,7 +224,7 @@ class PuzzleController extends _$PuzzleController { state = state.copyWith.streak!(hasSkipped: true); final moveIndex = state.currentPath.size - state.initialPath.size; final solution = state.puzzle.puzzle.solution[moveIndex]; - onUserMove(Move.parse(solution)!); + onUserMove(NormalMove.fromUci(solution)); } } @@ -451,6 +470,7 @@ class PuzzleController extends _$PuzzleController { currentPath: path, node: newNode, lastMove: sanMove.move, + promotionMove: null, ); if (pathChange) { @@ -562,6 +582,7 @@ class PuzzleState with _$PuzzleState { required Side pov, required ViewBranch node, Move? lastMove, + NormalMove? promotionMove, PuzzleResult? result, PuzzleFeedback? feedback, required bool canViewSolution, diff --git a/lib/src/model/puzzle/storm_controller.dart b/lib/src/model/puzzle/storm_controller.dart index ed9cd2eb58..24e21aecf7 100644 --- a/lib/src/model/puzzle/storm_controller.dart +++ b/lib/src/model/puzzle/storm_controller.dart @@ -8,6 +8,7 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/services.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; +import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/service/move_feedback.dart'; import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; @@ -75,9 +76,15 @@ class StormController extends _$StormController { return newState; } - Future onUserMove(Move move) async { + Future onUserMove(NormalMove move) async { if (state.clock.endAt != null) return; state.clock.start(); + + if (isPromotionPawnMove(state.position, move)) { + state = state.copyWith(promotionMove: move); + return; + } + final expected = state.expectedMove; _addMove(move, ComboState.noChange, runStarted: true, userMove: true); state = state.copyWith(moves: state.moves + 1); @@ -118,10 +125,16 @@ class StormController extends _$StormController { } } - void setPremove(Move? move) { - state = state.copyWith( - premove: move, - ); + void onPromotionSelect(Role role) { + final promotionMove = state.promotionMove; + if (promotionMove != null) { + final move = promotionMove.withPromotion(role); + onUserMove(move); + } + } + + void onPromotionCancel() { + state = state.copyWith(promotionMove: null); } Future end() async { @@ -227,6 +240,7 @@ class StormController extends _$StormController { current: newComboCurrent, best: math.max(state.combo.best, state.combo.current + 1), ), + promotionMove: null, ); Future.delayed( userMove ? Duration.zero : const Duration(milliseconds: 250), () { @@ -333,8 +347,8 @@ class StormState with _$StormState { /// bool to indicate that the first move has been played required bool firstMovePlayed, - /// premove to be played - Move? premove, + /// Promotion move to be selected + NormalMove? promotionMove, }) = _StormState; Move? get expectedMove => Move.parse(puzzle.solution[moveIndex + 1]); diff --git a/lib/src/view/analysis/analysis_board.dart b/lib/src/view/analysis/analysis_board.dart index b7d5578c25..d0ab270d37 100644 --- a/lib/src/view/analysis/analysis_board.dart +++ b/lib/src/view/analysis/analysis_board.dart @@ -37,7 +37,92 @@ class AnalysisBoard extends ConsumerStatefulWidget { class AnalysisBoardState extends ConsumerState { ISet userShapes = ISet(); - ISet _computeBestMoveShapes(IList moves) { + @override + Widget build(BuildContext context) { + final ctrlProvider = analysisControllerProvider(widget.pgn, widget.options); + final analysisState = ref.watch(ctrlProvider); + final boardPrefs = ref.watch(boardPreferencesProvider); + final showBestMoveArrow = ref.watch( + analysisPreferencesProvider.select( + (value) => value.showBestMoveArrow, + ), + ); + final showAnnotationsOnBoard = ref.watch( + analysisPreferencesProvider.select((value) => value.showAnnotations), + ); + + final evalBestMoves = ref.watch( + engineEvaluationProvider.select((s) => s.eval?.bestMoves), + ); + + final currentNode = analysisState.currentNode; + final annotation = makeAnnotation(currentNode.nags); + + final bestMoves = evalBestMoves ?? currentNode.eval?.bestMoves; + + final sanMove = currentNode.sanMove; + + final ISet bestMoveShapes = showBestMoveArrow && + analysisState.isEngineAvailable && + bestMoves != null + ? _computeBestMoveShapes( + bestMoves, + currentNode.position.turn, + boardPrefs.pieceSet.assets, + ) + : ISet(); + + return Chessboard( + size: widget.boardSize, + fen: analysisState.position.fen, + lastMove: analysisState.lastMove as NormalMove?, + orientation: analysisState.pov, + game: GameData( + playerSide: analysisState.position.isGameOver + ? PlayerSide.none + : analysisState.position.turn == Side.white + ? PlayerSide.white + : PlayerSide.black, + isCheck: boardPrefs.boardHighlights && analysisState.position.isCheck, + sideToMove: analysisState.position.turn, + validMoves: analysisState.validMoves, + promotionMove: analysisState.promotionMove, + onMove: (move, {isDrop, captured}) => + ref.read(ctrlProvider.notifier).onUserMove(move), + onPromotionSelect: (role) => + ref.read(ctrlProvider.notifier).onPromotionSelect(role), + onPromotionCancel: () => + ref.read(ctrlProvider.notifier).onPromotionCancel(), + ), + shapes: userShapes.union(bestMoveShapes), + annotations: + showAnnotationsOnBoard && sanMove != null && annotation != null + ? altCastles.containsKey(sanMove.move.uci) + ? IMap({ + Move.parse(altCastles[sanMove.move.uci]!)!.to: annotation, + }) + : IMap({sanMove.move.to: annotation}) + : null, + settings: boardPrefs.toBoardSettings().copyWith( + borderRadius: widget.isTablet + ? const BorderRadius.all(Radius.circular(4.0)) + : BorderRadius.zero, + boxShadow: widget.isTablet ? boardShadows : const [], + drawShape: DrawShapeOptions( + enable: true, + onCompleteShape: _onCompleteShape, + onClearShapes: _onClearShapes, + newShapeColor: boardPrefs.shapeColor.color, + ), + ), + ); + } + + ISet _computeBestMoveShapes( + IList moves, + Side sideToMove, + PieceAssets pieceAssets, + ) { // Scale down all moves with index > 0 based on how much worse their winning chances are compared to the best move // (assume moves are ordered by their winning chances, so index==0 is the best move) double scaleArrowAgainstBestMove(int index) { @@ -82,7 +167,8 @@ class AnalysisBoardState extends ConsumerState { PieceShape( color: color, orig: move.to, - role: promRole, + pieceAssets: pieceAssets, + piece: Piece(color: sideToMove, role: promRole), ), ]; case DropMove(role: final role, to: _): @@ -90,7 +176,9 @@ class AnalysisBoardState extends ConsumerState { PieceShape( color: color, orig: move.to, - role: role, + pieceAssets: pieceAssets, + opacity: 0.5, + piece: Piece(color: sideToMove, role: role), ), ]; } @@ -99,79 +187,6 @@ class AnalysisBoardState extends ConsumerState { ); } - @override - Widget build(BuildContext context) { - final ctrlProvider = analysisControllerProvider(widget.pgn, widget.options); - final analysisState = ref.watch(ctrlProvider); - final boardPrefs = ref.watch(boardPreferencesProvider); - final showBestMoveArrow = ref.watch( - analysisPreferencesProvider.select( - (value) => value.showBestMoveArrow, - ), - ); - final showAnnotationsOnBoard = ref.watch( - analysisPreferencesProvider.select((value) => value.showAnnotations), - ); - - final evalBestMoves = ref.watch( - engineEvaluationProvider.select((s) => s.eval?.bestMoves), - ); - - final currentNode = analysisState.currentNode; - final annotation = makeAnnotation(currentNode.nags); - - final bestMoves = evalBestMoves ?? currentNode.eval?.bestMoves; - - final sanMove = currentNode.sanMove; - - final ISet bestMoveShapes = showBestMoveArrow && - analysisState.isEngineAvailable && - bestMoves != null - ? _computeBestMoveShapes(bestMoves) - : ISet(); - - return Chessboard( - size: widget.boardSize, - onMove: (move, {isDrop, isPremove}) => - ref.read(ctrlProvider.notifier).onUserMove(Move.parse(move.uci)!), - state: ChessboardState( - orientation: analysisState.pov, - interactableSide: analysisState.position.isGameOver - ? InteractableSide.none - : analysisState.position.turn == Side.white - ? InteractableSide.white - : InteractableSide.black, - fen: analysisState.position.fen, - isCheck: boardPrefs.boardHighlights && analysisState.position.isCheck, - lastMove: analysisState.lastMove as NormalMove?, - sideToMove: analysisState.position.turn, - validMoves: analysisState.validMoves, - shapes: userShapes.union(bestMoveShapes), - annotations: showAnnotationsOnBoard && - sanMove != null && - annotation != null - ? altCastles.containsKey(sanMove.move.uci) - ? IMap({ - Move.parse(altCastles[sanMove.move.uci]!)!.to: annotation, - }) - : IMap({sanMove.move.to: annotation}) - : null, - ), - settings: boardPrefs.toBoardSettings().copyWith( - borderRadius: widget.isTablet - ? const BorderRadius.all(Radius.circular(4.0)) - : BorderRadius.zero, - boxShadow: widget.isTablet ? boardShadows : const [], - drawShape: DrawShapeOptions( - enable: true, - onCompleteShape: _onCompleteShape, - onClearShapes: _onClearShapes, - newShapeColor: boardPrefs.shapeColor.color, - ), - ), - ); - } - void _onCompleteShape(Shape shape) { if (userShapes.any((element) => element == shape)) { setState(() { diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index 4529318417..60f09e3b01 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -517,7 +517,7 @@ class _Engineline extends ConsumerWidget { return AdaptiveInkWell( onTap: () => ref .read(ctrlProvider.notifier) - .onUserMove(Move.parse(pvData.moves[0])!), + .onUserMove(NormalMove.fromUci(pvData.moves[0])), child: SizedBox( height: kEvalGaugeSize, child: Padding( diff --git a/lib/src/view/correspondence/offline_correspondence_game_screen.dart b/lib/src/view/correspondence/offline_correspondence_game_screen.dart index be2be63702..48462988bb 100644 --- a/lib/src/view/correspondence/offline_correspondence_game_screen.dart +++ b/lib/src/view/correspondence/offline_correspondence_game_screen.dart @@ -116,6 +116,7 @@ class _BodyState extends ConsumerState<_Body> { int stepCursor = 0; (String, Move)? moveToConfirm; bool isBoardTurned = false; + NormalMove? promotionMove; bool get isReplaying => stepCursor < game.steps.length - 1; bool get canGoForward => stepCursor < game.steps.length - 1; @@ -207,21 +208,24 @@ class _BodyState extends ConsumerState<_Body> { child: SafeArea( bottom: false, child: BoardTable( - onMove: (move, {isDrop, isPremove}) { - onUserMove(Move.parse(move.uci)!); - }, - boardState: ChessboardState( - interactableSide: game.playable && !isReplaying + orientation: isBoardTurned ? youAre.opposite : youAre, + fen: position.fen, + lastMove: game.moveAt(stepCursor) as NormalMove?, + gameData: GameData( + playerSide: game.playable && !isReplaying ? youAre == Side.white - ? InteractableSide.white - : InteractableSide.black - : InteractableSide.none, - orientation: isBoardTurned ? youAre.opposite : youAre, - fen: position.fen, - lastMove: game.moveAt(stepCursor) as NormalMove?, + ? PlayerSide.white + : PlayerSide.black + : PlayerSide.none, isCheck: position.isCheck, sideToMove: sideToMove, validMoves: makeLegalMoves(position), + promotionMove: promotionMove, + onMove: (move, {isDrop, captured}) { + onUserMove(move); + }, + onPromotionSelect: onPromotionSelected, + onPromotionCancel: onPromotionCanceled, ), topTable: topPlayer, bottomTable: bottomPlayer, @@ -345,7 +349,14 @@ class _BodyState extends ConsumerState<_Body> { } } - void onUserMove(Move move) { + void onUserMove(NormalMove move) { + if (isPromotionPawnMove(game.lastPosition, move)) { + setState(() { + promotionMove = move; + }); + return; + } + final (newPos, newSan) = game.lastPosition.makeSan(move); final sanMove = SanMove(newSan, move); final newStep = GameStep( @@ -359,12 +370,26 @@ class _BodyState extends ConsumerState<_Body> { game = game.copyWith( steps: game.steps.add(newStep), ); + promotionMove = null; stepCursor = stepCursor + 1; }); _moveFeedback(sanMove); } + void onPromotionSelected(Role role) { + if (promotionMove != null) { + final move = promotionMove!.withPromotion(role); + onUserMove(move); + } + } + + void onPromotionCanceled() { + setState(() { + promotionMove = null; + }); + } + void confirmMove() { setState(() { game = game.copyWith( diff --git a/lib/src/view/game/archived_game_screen.dart b/lib/src/view/game/archived_game_screen.dart index 5e69714734..73469c6e44 100644 --- a/lib/src/view/game/archived_game_screen.dart +++ b/lib/src/view/game/archived_game_screen.dart @@ -1,4 +1,3 @@ -import 'package:chessground/chessground.dart'; import 'package:dartchess/dartchess.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -121,11 +120,8 @@ class _BoardBody extends ConsumerWidget { final topPlayer = orientation == Side.white ? black : white; final bottomPlayer = orientation == Side.white ? white : black; final loadingBoard = BoardTable( - boardState: ChessboardState( - interactableSide: InteractableSide.none, - orientation: (isBoardTurned ? orientation.opposite : orientation), - fen: gameData.lastFen ?? kInitialBoardFEN, - ), + orientation: (isBoardTurned ? orientation.opposite : orientation), + fen: gameData.lastFen ?? kInitialBoardFEN, topTable: topPlayer, bottomTable: bottomPlayer, showMoveListPlaceholder: true, @@ -167,14 +163,9 @@ class _BoardBody extends ConsumerWidget { final position = game.positionAt(cursor); return BoardTable( - boardState: ChessboardState( - interactableSide: InteractableSide.none, - orientation: (isBoardTurned ? orientation.opposite : orientation), - fen: position.fen, - lastMove: game.moveAt(cursor) as NormalMove?, - sideToMove: position.turn, - isCheck: position.isCheck, - ), + orientation: (isBoardTurned ? orientation.opposite : orientation), + fen: position.fen, + lastMove: game.moveAt(cursor) as NormalMove?, topTable: topPlayer, bottomTable: bottomPlayer, moves: game.steps diff --git a/lib/src/view/game/game_body.dart b/lib/src/view/game/game_body.dart index cf3cb8a743..ea3b5b918b 100644 --- a/lib/src/view/game/game_body.dart +++ b/lib/src/view/game/game_body.dart @@ -239,34 +239,46 @@ class GameBody extends ConsumerWidget { gameState.canAutoQueenOnPremove, blindfoldMode: blindfoldMode, ), - onMove: (move, {isDrop, isPremove}) { - ref.read(ctrlProvider.notifier).onUserMove( - Move.parse(move.uci)!, - isPremove: isPremove, - isDrop: isDrop, - ); - }, - onPremove: gameState.canPremove - ? (move) { - ref.read(ctrlProvider.notifier).setPremove(move); - } - : null, - boardState: ChessboardState( - interactableSide: + orientation: isBoardTurned ? youAre.opposite : youAre, + fen: position.fen, + lastMove: gameState.game.moveAt(gameState.stepCursor) + as NormalMove?, + gameData: GameData( + playerSide: gameState.game.playable && !gameState.isReplaying ? youAre == Side.white - ? InteractableSide.white - : InteractableSide.black - : InteractableSide.none, - orientation: isBoardTurned ? youAre.opposite : youAre, - fen: position.fen, - lastMove: gameState.game.moveAt(gameState.stepCursor) - as NormalMove?, + ? PlayerSide.white + : PlayerSide.black + : PlayerSide.none, isCheck: boardPreferences.boardHighlights && position.isCheck, sideToMove: position.turn, validMoves: makeLegalMoves(position), - premove: gameState.premove as NormalMove?, + promotionMove: gameState.promotionMove, + onMove: (move, {isDrop}) { + ref.read(ctrlProvider.notifier).userMove( + move, + isDrop: isDrop, + ); + }, + onPromotionSelect: (role) { + ref + .read(ctrlProvider.notifier) + .onPromotionSelect(role); + }, + onPromotionCancel: () { + ref.read(ctrlProvider.notifier).onPromotionCancel(); + }, + premovable: gameState.canPremove + ? ( + onSetPremove: (move) { + ref + .read(ctrlProvider.notifier) + .setPremove(move); + }, + premove: gameState.premove, + ) + : null, ), topTable: topPlayer, bottomTable: gameState.canShowClaimWinCountdown && diff --git a/lib/src/view/game/game_loading_board.dart b/lib/src/view/game/game_loading_board.dart index e9b1565c8c..66daae645d 100644 --- a/lib/src/view/game/game_loading_board.dart +++ b/lib/src/view/game/game_loading_board.dart @@ -1,4 +1,3 @@ -import 'package:chessground/chessground.dart'; import 'package:dartchess/dartchess.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -29,7 +28,8 @@ class LobbyScreenLoadingContent extends StatelessWidget { child: SafeArea( bottom: false, child: BoardTable( - boardState: kEmptyBoardState, + orientation: Side.white, + fen: kEmptyFen, topTable: const SizedBox.shrink(), bottomTable: const SizedBox.shrink(), showMoveListPlaceholder: true, @@ -108,7 +108,8 @@ class ChallengeLoadingContent extends StatelessWidget { child: SafeArea( bottom: false, child: BoardTable( - boardState: kEmptyBoardState, + orientation: Side.white, + fen: kEmptyFen, topTable: const SizedBox.shrink(), bottomTable: const SizedBox.shrink(), showMoveListPlaceholder: true, @@ -184,12 +185,9 @@ class StandaloneGameLoadingBoard extends StatelessWidget { @override Widget build(BuildContext context) { return BoardTable( - boardState: ChessboardState( - interactableSide: InteractableSide.none, - orientation: orientation ?? Side.white, - fen: fen ?? kEmptyFen, - lastMove: lastMove as NormalMove?, - ), + orientation: orientation ?? Side.white, + fen: fen ?? kEmptyFen, + lastMove: lastMove as NormalMove?, topTable: const SizedBox.shrink(), bottomTable: const SizedBox.shrink(), showMoveListPlaceholder: true, @@ -210,11 +208,8 @@ class LoadGameError extends StatelessWidget { child: SafeArea( bottom: false, child: BoardTable( - boardState: const ChessboardState( - interactableSide: InteractableSide.none, - orientation: Side.white, - fen: kEmptyFen, - ), + orientation: Side.white, + fen: kEmptyFen, topTable: const SizedBox.shrink(), bottomTable: const SizedBox.shrink(), showMoveListPlaceholder: true, @@ -255,11 +250,8 @@ class ChallengeDeclinedBoard extends StatelessWidget { child: SafeArea( bottom: false, child: BoardTable( - boardState: const ChessboardState( - interactableSide: InteractableSide.none, - orientation: Side.white, - fen: kEmptyFen, - ), + orientation: Side.white, + fen: kEmptyFen, topTable: const SizedBox.shrink(), bottomTable: const SizedBox.shrink(), showMoveListPlaceholder: true, diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index 0194c375ca..a5dc2489ee 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -632,7 +632,7 @@ class OpeningExplorerMoveTable extends ConsumerWidget { TableRowInkWell( onTap: () => ref .read(ctrlProvider.notifier) - .onUserMove(Move.parse(move.uci)!), + .onUserMove(NormalMove.fromUci(move.uci)), child: Padding( padding: _kTableRowPadding, child: Text(move.san), @@ -641,7 +641,7 @@ class OpeningExplorerMoveTable extends ConsumerWidget { TableRowInkWell( onTap: () => ref .read(ctrlProvider.notifier) - .onUserMove(Move.parse(move.uci)!), + .onUserMove(NormalMove.fromUci(move.uci)), child: Padding( padding: _kTableRowPadding, child: Text('${formatNum(move.games)} ($percentGames%)'), @@ -650,7 +650,7 @@ class OpeningExplorerMoveTable extends ConsumerWidget { TableRowInkWell( onTap: () => ref .read(ctrlProvider.notifier) - .onUserMove(Move.parse(move.uci)!), + .onUserMove(NormalMove.fromUci(move.uci)), child: Padding( padding: _kTableRowPadding, child: _WinPercentageChart( diff --git a/lib/src/view/puzzle/puzzle_screen.dart b/lib/src/view/puzzle/puzzle_screen.dart index 7a86d625b8..86c054b06d 100644 --- a/lib/src/view/puzzle/puzzle_screen.dart +++ b/lib/src/view/puzzle/puzzle_screen.dart @@ -147,7 +147,8 @@ class _LoadNextPuzzle extends ConsumerWidget { child: BoardTable( topTable: kEmptyWidget, bottomTable: kEmptyWidget, - boardState: kEmptyBoardState, + fen: kEmptyFen, + orientation: Side.white, errorMessage: 'No more puzzles. Go online to get more.', ), ); @@ -166,7 +167,8 @@ class _LoadNextPuzzle extends ConsumerWidget { child: BoardTable( topTable: kEmptyWidget, bottomTable: kEmptyWidget, - boardState: kEmptyBoardState, + fen: kEmptyFen, + orientation: Side.white, errorMessage: e.toString(), ), ); @@ -202,11 +204,8 @@ class _LoadPuzzleFromId extends ConsumerWidget { child: SafeArea( bottom: false, child: BoardTable( - boardState: ChessboardState( - fen: kEmptyFen, - interactableSide: InteractableSide.none, - orientation: Side.white, - ), + fen: kEmptyFen, + orientation: Side.white, topTable: kEmptyWidget, bottomTable: kEmptyWidget, ), @@ -225,7 +224,8 @@ class _LoadPuzzleFromId extends ConsumerWidget { child: SafeArea( bottom: false, child: BoardTable( - boardState: kEmptyBoardState, + fen: kEmptyFen, + orientation: Side.white, topTable: kEmptyWidget, bottomTable: kEmptyWidget, errorMessage: e.toString(), @@ -266,37 +266,42 @@ class _Body extends ConsumerWidget { child: SafeArea( bottom: false, child: BoardTable( - onMove: (move, {isDrop, isPremove}) { - ref - .read(ctrlProvider.notifier) - .onUserMove(Move.parse(move.uci)!); - }, - boardState: ChessboardState( - orientation: puzzleState.pov, - interactableSide: puzzleState.mode == PuzzleMode.load || + orientation: puzzleState.pov, + fen: puzzleState.fen, + lastMove: puzzleState.lastMove as NormalMove?, + gameData: GameData( + playerSide: puzzleState.mode == PuzzleMode.load || puzzleState.position.isGameOver - ? InteractableSide.none + ? PlayerSide.none : puzzleState.mode == PuzzleMode.view - ? InteractableSide.both + ? PlayerSide.both : puzzleState.pov == Side.white - ? InteractableSide.white - : InteractableSide.black, - fen: puzzleState.fen, + ? PlayerSide.white + : PlayerSide.black, isCheck: boardPreferences.boardHighlights && puzzleState.position.isCheck, - lastMove: puzzleState.lastMove as NormalMove?, sideToMove: puzzleState.position.turn, validMoves: puzzleState.validMoves, - shapes: puzzleState.isEngineEnabled && evalBestMove != null - ? ISet([ - Arrow( - color: const Color(0x40003088), - orig: evalBestMove.from, - dest: evalBestMove.to, - ), - ]) - : null, + promotionMove: puzzleState.promotionMove, + onMove: (move, {isDrop, captured}) { + ref.read(ctrlProvider.notifier).onUserMove(move); + }, + onPromotionSelect: (role) { + ref.read(ctrlProvider.notifier).onPromotionSelect(role); + }, + onPromotionCancel: () { + ref.read(ctrlProvider.notifier).onPromotionCancel(); + }, ), + shapes: puzzleState.isEngineEnabled && evalBestMove != null + ? ISet([ + Arrow( + color: const Color(0x40003088), + orig: evalBestMove.from, + dest: evalBestMove.to, + ), + ]) + : null, engineGauge: puzzleState.isEngineEnabled ? EngineGaugeParams( orientation: puzzleState.pov, diff --git a/lib/src/view/puzzle/storm_screen.dart b/lib/src/view/puzzle/storm_screen.dart index 8dac35b9e2..88a3d5e2e5 100644 --- a/lib/src/view/puzzle/storm_screen.dart +++ b/lib/src/view/puzzle/storm_screen.dart @@ -77,7 +77,8 @@ class _Load extends ConsumerWidget { child: BoardTable( topTable: kEmptyWidget, bottomTable: kEmptyWidget, - boardState: kEmptyBoardState, + fen: kEmptyFen, + orientation: Side.white, errorMessage: e.toString(), ), ); @@ -143,27 +144,28 @@ class _Body extends ConsumerWidget { bottom: false, child: BoardTable( boardKey: boardKey, - onMove: (move, {isDrop, isPremove}) => ref - .read(ctrlProvider.notifier) - .onUserMove(Move.parse(move.uci)!), - onPremove: (move) => - ref.read(ctrlProvider.notifier).setPremove(move), - boardState: ChessboardState( - orientation: stormState.pov, - interactableSide: !stormState.firstMovePlayed || + orientation: stormState.pov, + lastMove: stormState.lastMove as NormalMove?, + fen: stormState.position.fen, + gameData: GameData( + playerSide: !stormState.firstMovePlayed || stormState.mode == StormMode.ended || stormState.position.isGameOver - ? InteractableSide.none + ? PlayerSide.none : stormState.pov == Side.white - ? InteractableSide.white - : InteractableSide.black, - fen: stormState.position.fen, + ? PlayerSide.white + : PlayerSide.black, isCheck: boardPreferences.boardHighlights && stormState.position.isCheck, - lastMove: stormState.lastMove as NormalMove?, sideToMove: stormState.position.turn, validMoves: stormState.validMoves, - premove: stormState.premove as NormalMove?, + promotionMove: stormState.promotionMove, + onMove: (move, {isDrop, captured}) => + ref.read(ctrlProvider.notifier).onUserMove(move), + onPromotionSelect: (role) => + ref.read(ctrlProvider.notifier).onPromotionSelect(role), + onPromotionCancel: () => + ref.read(ctrlProvider.notifier).onPromotionCancel(), ), topTable: _TopTable(data), bottomTable: _Combo(stormState.combo), diff --git a/lib/src/view/puzzle/streak_screen.dart b/lib/src/view/puzzle/streak_screen.dart index 2ea5d6ecd2..15f4bb6549 100644 --- a/lib/src/view/puzzle/streak_screen.dart +++ b/lib/src/view/puzzle/streak_screen.dart @@ -82,7 +82,8 @@ class _Load extends ConsumerWidget { child: BoardTable( topTable: kEmptyWidget, bottomTable: kEmptyWidget, - boardState: kEmptyBoardState, + fen: kEmptyFen, + orientation: Side.white, errorMessage: e.toString(), ), ); @@ -125,26 +126,31 @@ class _Body extends ConsumerWidget { child: Center( child: SafeArea( child: BoardTable( - onMove: (move, {isDrop, isPremove}) { - ref - .read(ctrlProvider.notifier) - .onUserMove(Move.parse(move.uci)!); - }, - boardState: ChessboardState( - orientation: puzzleState.pov, - interactableSide: puzzleState.mode == PuzzleMode.load || + orientation: puzzleState.pov, + fen: puzzleState.fen, + lastMove: puzzleState.lastMove as NormalMove?, + gameData: GameData( + playerSide: puzzleState.mode == PuzzleMode.load || puzzleState.position.isGameOver - ? InteractableSide.none + ? PlayerSide.none : puzzleState.mode == PuzzleMode.view - ? InteractableSide.both + ? PlayerSide.both : puzzleState.pov == Side.white - ? InteractableSide.white - : InteractableSide.black, - fen: puzzleState.fen, + ? PlayerSide.white + : PlayerSide.black, isCheck: puzzleState.position.isCheck, - lastMove: puzzleState.lastMove as NormalMove?, sideToMove: puzzleState.position.turn, validMoves: puzzleState.validMoves, + promotionMove: puzzleState.promotionMove, + onMove: (move, {isDrop, captured}) { + ref.read(ctrlProvider.notifier).onUserMove(move); + }, + onPromotionSelect: (role) { + ref.read(ctrlProvider.notifier).onPromotionSelect(role); + }, + onPromotionCancel: () { + ref.read(ctrlProvider.notifier).onPromotionCancel(); + }, ), topTable: Center( child: Padding( diff --git a/lib/src/view/settings/theme_screen.dart b/lib/src/view/settings/theme_screen.dart index b20569fbac..dea7be647f 100644 --- a/lib/src/view/settings/theme_screen.dart +++ b/lib/src/view/settings/theme_screen.dart @@ -77,29 +77,26 @@ class _Body extends ConsumerWidget { vertical: 16, ), child: Center( - child: Chessboard( + child: Chessboard.fixed( size: boardSize, - state: ChessboardState( - interactableSide: InteractableSide.none, - orientation: Side.white, - fen: kInitialFEN, - shapes: { - Arrow( - color: boardPrefs.shapeColor.color, - orig: Square.fromName('e2'), - dest: Square.fromName('e4'), - ), - Circle( - color: boardPrefs.shapeColor.color, - orig: Square.fromName('d2'), - ), - Arrow( - color: boardPrefs.shapeColor.color, - orig: Square.fromName('b1'), - dest: Square.fromName('c3'), - ), - }.lock, - ), + orientation: Side.white, + fen: kInitialFEN, + shapes: { + Arrow( + color: boardPrefs.shapeColor.color, + orig: Square.fromName('e2'), + dest: Square.fromName('e4'), + ), + Circle( + color: boardPrefs.shapeColor.color, + orig: Square.fromName('d2'), + ), + Arrow( + color: boardPrefs.shapeColor.color, + orig: Square.fromName('b1'), + dest: Square.fromName('c3'), + ), + }.lock, settings: ChessboardSettings( enableCoordinates: false, borderRadius: diff --git a/lib/src/view/watch/tv_screen.dart b/lib/src/view/watch/tv_screen.dart index 64ad47bd45..3e681c8398 100644 --- a/lib/src/view/watch/tv_screen.dart +++ b/lib/src/view/watch/tv_screen.dart @@ -1,11 +1,9 @@ -import 'package:chessground/chessground.dart'; import 'package:dartchess/dartchess.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/model/tv/tv_channel.dart'; import 'package:lichess_mobile/src/model/tv/tv_controller.dart'; import 'package:lichess_mobile/src/utils/focus_detector.dart'; @@ -80,7 +78,6 @@ class _Body extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final boardPreferences = ref.watch(boardPreferencesProvider); final asyncGame = ref.watch(tvControllerProvider(channel, initialGame)); return Column( @@ -92,16 +89,7 @@ class _Body extends ConsumerWidget { final game = gameState.game; final position = gameState.game.positionAt(gameState.stepCursor); - final sideToMove = position.turn; - final boardData = ChessboardState( - interactableSide: InteractableSide.none, - orientation: gameState.orientation, - fen: position.fen, - sideToMove: sideToMove, - lastMove: game.moveAt(gameState.stepCursor) as NormalMove?, - isCheck: boardPreferences.boardHighlights && position.isCheck, - ); final blackPlayerWidget = GamePlayer( player: game.black.setOnGame(true), clock: gameState.game.clock != null @@ -125,7 +113,8 @@ class _Body extends ConsumerWidget { materialDiff: game.lastMaterialDiffAt(Side.white), ); return BoardTable( - boardState: boardData, + orientation: gameState.orientation, + fen: position.fen, boardSettingsOverrides: const BoardSettingsOverrides( animationDuration: Duration.zero, ), @@ -145,7 +134,8 @@ class _Body extends ConsumerWidget { loading: () => const BoardTable( topTable: kEmptyWidget, bottomTable: kEmptyWidget, - boardState: kEmptyBoardState, + orientation: Side.white, + fen: kEmptyFEN, showMoveListPlaceholder: true, ), error: (err, stackTrace) { @@ -155,7 +145,8 @@ class _Body extends ConsumerWidget { return const BoardTable( topTable: kEmptyWidget, bottomTable: kEmptyWidget, - boardState: kEmptyBoardState, + orientation: Side.white, + fen: kEmptyFEN, errorMessage: 'Could not load TV stream.', showMoveListPlaceholder: true, ); diff --git a/lib/src/widgets/board_carousel_item.dart b/lib/src/widgets/board_carousel_item.dart index 608e3089a9..ed6d1efbd4 100644 --- a/lib/src/widgets/board_carousel_item.dart +++ b/lib/src/widgets/board_carousel_item.dart @@ -80,14 +80,11 @@ class BoardCarouselItem extends ConsumerWidget { }, child: SizedBox( height: boardSize, - child: Chessboard( + child: Chessboard.fixed( size: boardSize, - state: ChessboardState( - interactableSide: InteractableSide.none, - fen: fen, - orientation: orientation, - lastMove: lastMove as NormalMove?, - ), + fen: fen, + orientation: orientation, + lastMove: lastMove, settings: ChessboardSettings( enableCoordinates: false, borderRadius: const BorderRadius.only( diff --git a/lib/src/widgets/board_preview.dart b/lib/src/widgets/board_preview.dart index 1a5e17f755..ea72369549 100644 --- a/lib/src/widgets/board_preview.dart +++ b/lib/src/widgets/board_preview.dart @@ -65,14 +65,11 @@ class _SmallBoardPreviewState extends ConsumerState { height: boardSize, child: Row( children: [ - Chessboard( + Chessboard.fixed( size: boardSize, - state: ChessboardState( - interactableSide: InteractableSide.none, - fen: widget.fen, - orientation: widget.orientation, - lastMove: widget.lastMove as NormalMove?, - ), + fen: widget.fen, + orientation: widget.orientation, + lastMove: widget.lastMove as NormalMove?, settings: ChessboardSettings( enableCoordinates: false, borderRadius: diff --git a/lib/src/widgets/board_table.dart b/lib/src/widgets/board_table.dart index f27ba53d70..6145df6a57 100644 --- a/lib/src/widgets/board_table.dart +++ b/lib/src/widgets/board_table.dart @@ -30,12 +30,14 @@ const _moveListOpacity = 0.6; /// An optional overlay or error message can be displayed on top of the board. class BoardTable extends ConsumerStatefulWidget { const BoardTable({ - this.onMove, - this.onPremove, - required this.boardState, + required this.fen, + required this.orientation, + this.gameData, + this.lastMove, this.boardSettingsOverrides, required this.topTable, required this.bottomTable, + this.shapes, this.engineGauge, this.moves, this.currentMoveIndex, @@ -52,13 +54,18 @@ class BoardTable extends ConsumerStatefulWidget { 'You must provide `currentMoveIndex` along with `moves`', ); - final void Function(Move, {bool? isDrop, bool? isPremove})? onMove; - final void Function(Move?)? onPremove; + final String fen; - final ChessboardState boardState; + final Side orientation; + + final GameData? gameData; + + final Move? lastMove; final BoardSettingsOverrides? boardSettingsOverrides; + final ISet? shapes; + /// [GlobalKey] for the board. /// /// Used to set gestures exclusion on android. @@ -167,12 +174,12 @@ class _BoardTableState extends ConsumerState { final board = Chessboard( key: widget.boardKey, size: boardSize, - state: widget.boardState.copyWith( - shapes: userShapes.union(widget.boardState.shapes ?? ISet()), - ), + fen: widget.fen, + orientation: widget.orientation, + game: widget.gameData, + lastMove: widget.lastMove, + shapes: userShapes.union(widget.shapes ?? ISet()), settings: settings, - onMove: widget.onMove, - onPremove: widget.onPremove, ); Widget boardWidget = board; diff --git a/lib/src/widgets/board_thumbnail.dart b/lib/src/widgets/board_thumbnail.dart index 6bf77234bd..91f1629810 100644 --- a/lib/src/widgets/board_thumbnail.dart +++ b/lib/src/widgets/board_thumbnail.dart @@ -67,14 +67,11 @@ class _BoardThumbnailState extends ConsumerState { Widget build(BuildContext context) { final boardPrefs = ref.watch(boardPreferencesProvider); - final board = Chessboard( + final board = Chessboard.fixed( size: widget.size, - state: ChessboardState( - interactableSide: InteractableSide.none, - fen: widget.fen, - orientation: widget.orientation, - lastMove: widget.lastMove as NormalMove?, - ), + fen: widget.fen, + orientation: widget.orientation, + lastMove: widget.lastMove as NormalMove?, settings: ChessboardSettings( enableCoordinates: false, borderRadius: const BorderRadius.all(Radius.circular(4.0)), diff --git a/pubspec.lock b/pubspec.lock index 358b3002ff..32d007bf85 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -193,11 +193,10 @@ packages: chessground: dependency: "direct main" description: - name: chessground - sha256: ff6c770780c7b6d5232d743d44010077fa26b2301dbe71feaaf943679aef3241 - url: "https://pub.dev" - source: hosted - version: "4.0.0" + path: "../flutter-chessground" + relative: true + source: path + version: "5.0.0" ci: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c93316d418..0b6b9ea9de 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,8 @@ dependencies: app_settings: ^5.1.1 async: ^2.10.0 cached_network_image: ^3.2.2 - chessground: ^4.0.0 + chessground: + path: ../flutter-chessground collection: ^1.17.0 connectivity_plus: ^6.0.2 cronet_http: ^1.3.1 diff --git a/test/view/game/archived_game_screen_test.dart b/test/view/game/archived_game_screen_test.dart index a3ae065c4d..cd18a3975b 100644 --- a/test/view/game/archived_game_screen_test.dart +++ b/test/view/game/archived_game_screen_test.dart @@ -57,11 +57,8 @@ void main() { // cannot interact with board expect( - tester - .widget(find.byType(Chessboard)) - .state - .interactableSide, - InteractableSide.none, + tester.widget(find.byType(Chessboard)).game, + null, ); // moves are not loaded From 7d5bcd141ecdc8252f553d1fc086836df3df8b2d Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 2 Sep 2024 14:05:30 +0200 Subject: [PATCH 191/979] More wip --- lib/src/model/analysis/analysis_controller.dart | 10 +++++----- lib/src/model/game/game_controller.dart | 15 +++++++-------- lib/src/model/puzzle/puzzle_controller.dart | 12 +++++------- lib/src/model/puzzle/storm_controller.dart | 10 +++++----- lib/src/view/analysis/analysis_board.dart | 6 ++---- .../offline_correspondence_game_screen.dart | 17 ++++++++--------- lib/src/view/game/game_body.dart | 7 ++----- lib/src/view/puzzle/puzzle_screen.dart | 7 ++----- lib/src/view/puzzle/storm_screen.dart | 7 +++---- lib/src/view/puzzle/streak_screen.dart | 7 ++----- 10 files changed, 41 insertions(+), 57 deletions(-) diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index f6b2ae0a58..5ac4a7da1f 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -195,7 +195,11 @@ class AnalysisController extends _$AnalysisController { } } - void onPromotionSelect(Role role) { + void onPromotionSelection(Role? role) { + if (role == null) { + state = state.copyWith(promotionMove: null); + return; + } final promotionMove = state.promotionMove; if (promotionMove != null) { final promotion = promotionMove.withPromotion(role); @@ -203,10 +207,6 @@ class AnalysisController extends _$AnalysisController { } } - void onPromotionCancel() { - state = state.copyWith(promotionMove: null); - } - void userNext() { if (!state.currentNode.hasChild) return; _setPath( diff --git a/lib/src/model/game/game_controller.dart b/lib/src/model/game/game_controller.dart index 90b512b5c1..4f0145c54a 100644 --- a/lib/src/model/game/game_controller.dart +++ b/lib/src/model/game/game_controller.dart @@ -160,8 +160,14 @@ class GameController extends _$GameController { } } - void onPromotionSelect(Role role) { + void onPromotionSelection(Role? role) { final curState = state.requireValue; + if (role == null) { + state = AsyncValue.data( + curState.copyWith(promotionMove: null), + ); + return; + } if (curState.promotionMove == null) { assert(false, 'promotionMove must not be null on promotion select'); return; @@ -171,13 +177,6 @@ class GameController extends _$GameController { userMove(move, isDrop: true); } - void onPromotionCancel() { - final curState = state.requireValue; - state = AsyncValue.data( - curState.copyWith(promotionMove: null), - ); - } - /// Called if the player cancels the move when confirm move preference is enabled void cancelMove() { final curState = state.requireValue; diff --git a/lib/src/model/puzzle/puzzle_controller.dart b/lib/src/model/puzzle/puzzle_controller.dart index 2ac34bf542..bfe0e64972 100644 --- a/lib/src/model/puzzle/puzzle_controller.dart +++ b/lib/src/model/puzzle/puzzle_controller.dart @@ -162,7 +162,11 @@ class PuzzleController extends _$PuzzleController { } } - void onPromotionSelect(Role role) { + void onPromotionSelection(Role? role) { + if (role == null) { + state = state.copyWith(promotionMove: null); + return; + } final promotionMove = state.promotionMove; if (promotionMove != null) { final move = promotionMove.withPromotion(role); @@ -170,12 +174,6 @@ class PuzzleController extends _$PuzzleController { } } - void onPromotionCancel() { - state = state.copyWith( - promotionMove: null, - ); - } - void userNext() { _viewSolutionTimer?.cancel(); _goToNextNode(replaying: true); diff --git a/lib/src/model/puzzle/storm_controller.dart b/lib/src/model/puzzle/storm_controller.dart index 24e21aecf7..60300aaf43 100644 --- a/lib/src/model/puzzle/storm_controller.dart +++ b/lib/src/model/puzzle/storm_controller.dart @@ -125,7 +125,11 @@ class StormController extends _$StormController { } } - void onPromotionSelect(Role role) { + void onPromotionSelection(Role? role) { + if (role == null) { + state = state.copyWith(promotionMove: null); + return; + } final promotionMove = state.promotionMove; if (promotionMove != null) { final move = promotionMove.withPromotion(role); @@ -133,10 +137,6 @@ class StormController extends _$StormController { } } - void onPromotionCancel() { - state = state.copyWith(promotionMove: null); - } - Future end() async { ref.read(soundServiceProvider).play(Sound.puzzleStormEnd); diff --git a/lib/src/view/analysis/analysis_board.dart b/lib/src/view/analysis/analysis_board.dart index d0ab270d37..5607d34b96 100644 --- a/lib/src/view/analysis/analysis_board.dart +++ b/lib/src/view/analysis/analysis_board.dart @@ -89,10 +89,8 @@ class AnalysisBoardState extends ConsumerState { promotionMove: analysisState.promotionMove, onMove: (move, {isDrop, captured}) => ref.read(ctrlProvider.notifier).onUserMove(move), - onPromotionSelect: (role) => - ref.read(ctrlProvider.notifier).onPromotionSelect(role), - onPromotionCancel: () => - ref.read(ctrlProvider.notifier).onPromotionCancel(), + onPromotionSelection: (role) => + ref.read(ctrlProvider.notifier).onPromotionSelection(role), ), shapes: userShapes.union(bestMoveShapes), annotations: diff --git a/lib/src/view/correspondence/offline_correspondence_game_screen.dart b/lib/src/view/correspondence/offline_correspondence_game_screen.dart index 48462988bb..c00130793d 100644 --- a/lib/src/view/correspondence/offline_correspondence_game_screen.dart +++ b/lib/src/view/correspondence/offline_correspondence_game_screen.dart @@ -224,8 +224,7 @@ class _BodyState extends ConsumerState<_Body> { onMove: (move, {isDrop, captured}) { onUserMove(move); }, - onPromotionSelect: onPromotionSelected, - onPromotionCancel: onPromotionCanceled, + onPromotionSelection: onPromotionSelection, ), topTable: topPlayer, bottomTable: bottomPlayer, @@ -377,19 +376,19 @@ class _BodyState extends ConsumerState<_Body> { _moveFeedback(sanMove); } - void onPromotionSelected(Role role) { + void onPromotionSelection(Role? role) { + if (role == null) { + setState(() { + promotionMove = null; + }); + return; + } if (promotionMove != null) { final move = promotionMove!.withPromotion(role); onUserMove(move); } } - void onPromotionCanceled() { - setState(() { - promotionMove = null; - }); - } - void confirmMove() { setState(() { game = game.copyWith( diff --git a/lib/src/view/game/game_body.dart b/lib/src/view/game/game_body.dart index ea3b5b918b..c411f1a5e2 100644 --- a/lib/src/view/game/game_body.dart +++ b/lib/src/view/game/game_body.dart @@ -261,13 +261,10 @@ class GameBody extends ConsumerWidget { isDrop: isDrop, ); }, - onPromotionSelect: (role) { + onPromotionSelection: (role) { ref .read(ctrlProvider.notifier) - .onPromotionSelect(role); - }, - onPromotionCancel: () { - ref.read(ctrlProvider.notifier).onPromotionCancel(); + .onPromotionSelection(role); }, premovable: gameState.canPremove ? ( diff --git a/lib/src/view/puzzle/puzzle_screen.dart b/lib/src/view/puzzle/puzzle_screen.dart index 86c054b06d..17785e2313 100644 --- a/lib/src/view/puzzle/puzzle_screen.dart +++ b/lib/src/view/puzzle/puzzle_screen.dart @@ -286,11 +286,8 @@ class _Body extends ConsumerWidget { onMove: (move, {isDrop, captured}) { ref.read(ctrlProvider.notifier).onUserMove(move); }, - onPromotionSelect: (role) { - ref.read(ctrlProvider.notifier).onPromotionSelect(role); - }, - onPromotionCancel: () { - ref.read(ctrlProvider.notifier).onPromotionCancel(); + onPromotionSelection: (role) { + ref.read(ctrlProvider.notifier).onPromotionSelection(role); }, ), shapes: puzzleState.isEngineEnabled && evalBestMove != null diff --git a/lib/src/view/puzzle/storm_screen.dart b/lib/src/view/puzzle/storm_screen.dart index 88a3d5e2e5..8540df9efd 100644 --- a/lib/src/view/puzzle/storm_screen.dart +++ b/lib/src/view/puzzle/storm_screen.dart @@ -162,10 +162,9 @@ class _Body extends ConsumerWidget { promotionMove: stormState.promotionMove, onMove: (move, {isDrop, captured}) => ref.read(ctrlProvider.notifier).onUserMove(move), - onPromotionSelect: (role) => - ref.read(ctrlProvider.notifier).onPromotionSelect(role), - onPromotionCancel: () => - ref.read(ctrlProvider.notifier).onPromotionCancel(), + onPromotionSelection: (role) => ref + .read(ctrlProvider.notifier) + .onPromotionSelection(role), ), topTable: _TopTable(data), bottomTable: _Combo(stormState.combo), diff --git a/lib/src/view/puzzle/streak_screen.dart b/lib/src/view/puzzle/streak_screen.dart index 15f4bb6549..2ef6652d1e 100644 --- a/lib/src/view/puzzle/streak_screen.dart +++ b/lib/src/view/puzzle/streak_screen.dart @@ -145,11 +145,8 @@ class _Body extends ConsumerWidget { onMove: (move, {isDrop, captured}) { ref.read(ctrlProvider.notifier).onUserMove(move); }, - onPromotionSelect: (role) { - ref.read(ctrlProvider.notifier).onPromotionSelect(role); - }, - onPromotionCancel: () { - ref.read(ctrlProvider.notifier).onPromotionCancel(); + onPromotionSelection: (role) { + ref.read(ctrlProvider.notifier).onPromotionSelection(role); }, ), topTable: Center( From f1b2f33cbcd2777d73f9fc8fd4b6300ecd67ded8 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 2 Sep 2024 14:28:31 +0200 Subject: [PATCH 192/979] Tweak --- lib/src/view/puzzle/puzzle_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/view/puzzle/puzzle_screen.dart b/lib/src/view/puzzle/puzzle_screen.dart index 17785e2313..78a4a44070 100644 --- a/lib/src/view/puzzle/puzzle_screen.dart +++ b/lib/src/view/puzzle/puzzle_screen.dart @@ -283,7 +283,7 @@ class _Body extends ConsumerWidget { sideToMove: puzzleState.position.turn, validMoves: puzzleState.validMoves, promotionMove: puzzleState.promotionMove, - onMove: (move, {isDrop, captured}) { + onMove: (move, {isDrop}) { ref.read(ctrlProvider.notifier).onUserMove(move); }, onPromotionSelection: (role) { From 28577548bad7be4b827be95eac9a275e603c26d8 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 2 Sep 2024 14:38:16 +0200 Subject: [PATCH 193/979] Disable piece animation in all tests --- test/test_app.dart | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/test/test_app.dart b/test/test_app.dart index 4e897ca89c..43d87bb102 100644 --- a/test/test_app.dart +++ b/test/test_app.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -18,6 +20,7 @@ import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/game/game_storage.dart'; +import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/notification_service.dart'; import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:logging/logging.dart'; @@ -57,7 +60,19 @@ Future buildTestApp( VisibilityDetectorController.instance.updateInterval = Duration.zero; - SharedPreferences.setMockInitialValues(defaultPreferences ?? {}); + SharedPreferences.setMockInitialValues( + defaultPreferences ?? + { + // disable piece animation to simplify tests + 'preferences.board': jsonEncode( + BoardPrefs.defaults + .copyWith( + pieceAnimation: false, + ) + .toJson(), + ), + }, + ); final sharedPreferences = await SharedPreferences.getInstance(); From ed63b5457717bc72dbf639f7090a651aed73aa33 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 2 Sep 2024 15:16:59 +0200 Subject: [PATCH 194/979] Replace local chessground with git ref --- pubspec.lock | 8 +++++--- pubspec.yaml | 4 +++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 32d007bf85..892df99e62 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -193,9 +193,11 @@ packages: chessground: dependency: "direct main" description: - path: "../flutter-chessground" - relative: true - source: path + path: "." + ref: "032aea7ea11966f48b007bd2ba1da0818bcb77c1" + resolved-ref: "032aea7ea11966f48b007bd2ba1da0818bcb77c1" + url: "https://github.com/lichess-org/flutter-chessground.git" + source: git version: "5.0.0" ci: dependency: transitive diff --git a/pubspec.yaml b/pubspec.yaml index 0b6b9ea9de..50ea8e06d5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,7 +12,9 @@ dependencies: async: ^2.10.0 cached_network_image: ^3.2.2 chessground: - path: ../flutter-chessground + git: + url: https://github.com/lichess-org/flutter-chessground.git + ref: 032aea7ea11966f48b007bd2ba1da0818bcb77c1 collection: ^1.17.0 connectivity_plus: ^6.0.2 cronet_http: ^1.3.1 From 3438ecbd4e4a1399aa8b4aee1968f33d7e9f68b5 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 3 Sep 2024 09:09:29 +0200 Subject: [PATCH 195/979] Fix lint --- lib/src/view/watch/tv_screen.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/src/view/watch/tv_screen.dart b/lib/src/view/watch/tv_screen.dart index 3e681c8398..7de0f2ffa7 100644 --- a/lib/src/view/watch/tv_screen.dart +++ b/lib/src/view/watch/tv_screen.dart @@ -1,6 +1,5 @@ import 'package:dartchess/dartchess.dart'; import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; From 70861c1fa85d3b731f7eabef459a518789ff9964 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 3 Sep 2024 09:14:50 +0200 Subject: [PATCH 196/979] Update chessground --- pubspec.lock | 9 ++++----- pubspec.yaml | 5 +---- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 892df99e62..78cccb65d3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -193,11 +193,10 @@ packages: chessground: dependency: "direct main" description: - path: "." - ref: "032aea7ea11966f48b007bd2ba1da0818bcb77c1" - resolved-ref: "032aea7ea11966f48b007bd2ba1da0818bcb77c1" - url: "https://github.com/lichess-org/flutter-chessground.git" - source: git + name: chessground + sha256: "5471f307a2f50801f2f7fcf196b241a87a8c075b755db75ff005407a0f1299cc" + url: "https://pub.dev" + source: hosted version: "5.0.0" ci: dependency: transitive diff --git a/pubspec.yaml b/pubspec.yaml index 50ea8e06d5..099472c61c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,10 +11,7 @@ dependencies: app_settings: ^5.1.1 async: ^2.10.0 cached_network_image: ^3.2.2 - chessground: - git: - url: https://github.com/lichess-org/flutter-chessground.git - ref: 032aea7ea11966f48b007bd2ba1da0818bcb77c1 + chessground: ^5.0.0 collection: ^1.17.0 connectivity_plus: ^6.0.2 cronet_http: ^1.3.1 From 60c8d2adc6a7d4203ab940e53e9929e1b7a2fd19 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 3 Sep 2024 10:02:54 +0200 Subject: [PATCH 197/979] Expand bottom bar children --- lib/src/widgets/bottom_bar.dart | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/src/widgets/bottom_bar.dart b/lib/src/widgets/bottom_bar.dart index c1f9f84953..3bf0a104d4 100644 --- a/lib/src/widgets/bottom_bar.dart +++ b/lib/src/widgets/bottom_bar.dart @@ -9,6 +9,7 @@ class BottomBar extends StatelessWidget { const BottomBar({ required this.children, this.mainAxisAlignment = MainAxisAlignment.spaceAround, + this.expandChildren = true, }); /// Children to display in the bottom bar's [Row]. Typically instances of [BottomBarButton]. @@ -17,6 +18,9 @@ class BottomBar extends StatelessWidget { /// Alignment of the bottom bar's internal row. Defaults to [MainAxisAlignment.spaceAround]. final MainAxisAlignment mainAxisAlignment; + /// Whether to expand the children to fill the available space. Defaults to true. + final bool expandChildren; + @override Widget build(BuildContext context) { return Container( @@ -29,7 +33,9 @@ class BottomBar extends StatelessWidget { height: kBottomBarHeight, child: Row( mainAxisAlignment: mainAxisAlignment, - children: children, + children: expandChildren + ? children.map((child) => Expanded(child: child)).toList() + : children, ), ), ), From 7dfa91e9b32e709a5c6eaa9acbb05e8cf3abab18 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 3 Sep 2024 10:13:48 +0200 Subject: [PATCH 198/979] Tweak platform scaffold --- lib/src/view/analysis/analysis_screen.dart | 2 -- lib/src/widgets/platform_scaffold.dart | 9 +-------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index 60f09e3b01..280728ba17 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -165,8 +165,6 @@ class _LoadedAnalysisScreen extends ConsumerWidget { return CupertinoPageScaffold( resizeToAvoidBottomInset: false, navigationBar: CupertinoNavigationBar( - backgroundColor: Styles.cupertinoScaffoldColor.resolveFrom(context), - border: null, padding: Styles.cupertinoAppBarTrailingWidgetPadding, middle: _Title(options: options, title: title), trailing: Row( diff --git a/lib/src/widgets/platform_scaffold.dart b/lib/src/widgets/platform_scaffold.dart index 8a9776f017..015ed2f6dc 100644 --- a/lib/src/widgets/platform_scaffold.dart +++ b/lib/src/widgets/platform_scaffold.dart @@ -1,6 +1,5 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; /// Displays an [AppBar] for Android and a [CupertinoNavigationBar] for iOS. @@ -37,8 +36,6 @@ class PlatformAppBar extends StatelessWidget { final List actions; /// Will be passed to [CupertinoNavigationBar.padding] on iOS. Has no effect on Android. - /// - /// If null, will use [Styles.cupertinoAppBarTrailingWidgetPadding]. final EdgeInsetsDirectional? cupertinoPadding; /// Will be passed to [AppBar.titleSpacing] on Android. Has no effect on iOS. @@ -46,7 +43,6 @@ class PlatformAppBar extends StatelessWidget { AppBar _androidBuilder(BuildContext context) { return AppBar( - key: key, titleSpacing: androidTitleSpacing, leading: leading, title: title, @@ -57,10 +53,7 @@ class PlatformAppBar extends StatelessWidget { CupertinoNavigationBar _iosBuilder(BuildContext context) { return CupertinoNavigationBar( - key: key, - backgroundColor: Styles.cupertinoScaffoldColor.resolveFrom(context), - border: null, - padding: cupertinoPadding ?? Styles.cupertinoAppBarTrailingWidgetPadding, + padding: cupertinoPadding, middle: title, trailing: Row( mainAxisSize: MainAxisSize.min, From 4e1d3e1254633cc286eced879f7a0d8d2cca262a Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 3 Sep 2024 10:29:24 +0200 Subject: [PATCH 199/979] Only show resign button in bullet games --- lib/src/view/game/game_body.dart | 400 ++++++++++++++++--------------- 1 file changed, 204 insertions(+), 196 deletions(-) diff --git a/lib/src/view/game/game_body.dart b/lib/src/view/game/game_body.dart index c411f1a5e2..0b4715b9c4 100644 --- a/lib/src/view/game/game_body.dart +++ b/lib/src/view/game/game_body.dart @@ -437,214 +437,222 @@ class _GameBottomBar extends ConsumerWidget { ? ref.watch(chatControllerProvider(id)) : null; - final List children = gameStateAsync.when( - data: (gameState) { - final isChatEnabled = - chatStateAsync != null && !gameState.isZenModeActive; + return BottomBar( + children: gameStateAsync.when( + data: (gameState) { + final isChatEnabled = + chatStateAsync != null && !gameState.isZenModeActive; - final chatUnreadChip = isChatEnabled - ? chatStateAsync.maybeWhen( - data: (s) => s.unreadMessages > 0 - ? Text(math.min(9, s.unreadMessages).toString()) - : null, - orElse: () => null, - ) - : null; + final chatUnreadChip = isChatEnabled + ? chatStateAsync.maybeWhen( + data: (s) => s.unreadMessages > 0 + ? Text(math.min(9, s.unreadMessages).toString()) + : null, + orElse: () => null, + ) + : null; - return [ - BottomBarButton( - label: context.l10n.menu, - onTap: () { - _showGameMenu(context, ref); - }, - icon: Icons.menu, - ), - if (!gameState.game.playable) + return [ BottomBarButton( - label: context.l10n.mobileShowResult, + label: context.l10n.menu, onTap: () { - showAdaptiveDialog( - context: context, - builder: (context) => GameResultDialog( - id: id, - onNewOpponentCallback: onNewOpponentCallback, - ), - barrierDismissible: true, - ); + _showGameMenu(context, ref); }, - icon: Icons.info_outline, + icon: Icons.menu, ), - if (gameState.game.playable && - gameState.game.opponent?.offeringDraw == true) - BottomBarButton( - label: context.l10n.yourOpponentOffersADraw, - highlighted: true, - onTap: () { - showAdaptiveDialog( - context: context, - builder: (context) => _GameNegotiationDialog( - title: Text(context.l10n.yourOpponentOffersADraw), - onAccept: () { - ref - .read(gameControllerProvider(id).notifier) - .offerOrAcceptDraw(); - }, - onDecline: () { - ref - .read(gameControllerProvider(id).notifier) - .cancelOrDeclineDraw(); - }, - ), - barrierDismissible: true, - ); - }, - icon: Icons.handshake_outlined, - ) - else if (gameState.game.playable && - gameState.game.isThreefoldRepetition == true) - BottomBarButton( - label: context.l10n.threefoldRepetition, - highlighted: true, - onTap: () { - showAdaptiveDialog( - context: context, - builder: (context) => _ThreefoldDialog(id: id), - barrierDismissible: true, - ); - }, - icon: Icons.handshake_outlined, - ) - else if (gameState.game.playable && - gameState.game.opponent?.proposingTakeback == true) - BottomBarButton( - label: context.l10n.yourOpponentProposesATakeback, - highlighted: true, - onTap: () { - showAdaptiveDialog( - context: context, - builder: (context) => _GameNegotiationDialog( - title: Text(context.l10n.yourOpponentProposesATakeback), - onAccept: () { - ref - .read(gameControllerProvider(id).notifier) - .acceptTakeback(); - }, - onDecline: () { - ref - .read(gameControllerProvider(id).notifier) - .cancelOrDeclineTakeback(); - }, - ), - barrierDismissible: true, - ); - }, - icon: CupertinoIcons.arrowshape_turn_up_left, - ) - else if (gameState.game.finished) - BottomBarButton( - label: context.l10n.gameAnalysis, - icon: Icons.biotech, - onTap: () { - pushPlatformRoute( - context, - builder: (_) => AnalysisScreen( - pgnOrId: gameState.analysisPgn, - options: gameState.analysisOptions, - title: context.l10n.gameAnalysis, - ), - ); - }, - ) - else + if (!gameState.game.playable) + BottomBarButton( + label: context.l10n.mobileShowResult, + onTap: () { + showAdaptiveDialog( + context: context, + builder: (context) => GameResultDialog( + id: id, + onNewOpponentCallback: onNewOpponentCallback, + ), + barrierDismissible: true, + ); + }, + icon: Icons.info_outline, + ), + if (gameState.game.playable && + gameState.game.opponent?.offeringDraw == true) + BottomBarButton( + label: context.l10n.yourOpponentOffersADraw, + highlighted: true, + onTap: () { + showAdaptiveDialog( + context: context, + builder: (context) => _GameNegotiationDialog( + title: Text(context.l10n.yourOpponentOffersADraw), + onAccept: () { + ref + .read(gameControllerProvider(id).notifier) + .offerOrAcceptDraw(); + }, + onDecline: () { + ref + .read(gameControllerProvider(id).notifier) + .cancelOrDeclineDraw(); + }, + ), + barrierDismissible: true, + ); + }, + icon: Icons.handshake_outlined, + ) + else if (gameState.game.playable && + gameState.game.isThreefoldRepetition == true) + BottomBarButton( + label: context.l10n.threefoldRepetition, + highlighted: true, + onTap: () { + showAdaptiveDialog( + context: context, + builder: (context) => _ThreefoldDialog(id: id), + barrierDismissible: true, + ); + }, + icon: Icons.handshake_outlined, + ) + else if (gameState.game.playable && + gameState.game.opponent?.proposingTakeback == true) + BottomBarButton( + label: context.l10n.yourOpponentProposesATakeback, + highlighted: true, + onTap: () { + showAdaptiveDialog( + context: context, + builder: (context) => _GameNegotiationDialog( + title: Text(context.l10n.yourOpponentProposesATakeback), + onAccept: () { + ref + .read(gameControllerProvider(id).notifier) + .acceptTakeback(); + }, + onDecline: () { + ref + .read(gameControllerProvider(id).notifier) + .cancelOrDeclineTakeback(); + }, + ), + barrierDismissible: true, + ); + }, + icon: CupertinoIcons.arrowshape_turn_up_left, + ) + else if (gameState.game.finished) + BottomBarButton( + label: context.l10n.gameAnalysis, + icon: Icons.biotech, + onTap: () { + pushPlatformRoute( + context, + builder: (_) => AnalysisScreen( + pgnOrId: gameState.analysisPgn, + options: gameState.analysisOptions, + title: context.l10n.gameAnalysis, + ), + ); + }, + ) + else if (gameState.game.meta.speed == Speed.bullet || + gameState.game.meta.speed == Speed.ultraBullet) + BottomBarButton( + label: context.l10n.resign, + onTap: gameState.game.resignable + ? gameState.shouldConfirmResignAndDrawOffer + ? () => _showConfirmDialog( + context, + description: Text(context.l10n.resignTheGame), + onConfirm: () { + ref + .read(gameControllerProvider(id).notifier) + .resignGame(); + }, + ) + : () { + ref + .read(gameControllerProvider(id).notifier) + .resignGame(); + } + : null, + icon: Icons.flag, + ) + else + BottomBarButton( + label: context.l10n.flipBoard, + onTap: () { + ref.read(isBoardTurnedProvider.notifier).toggle(); + }, + icon: CupertinoIcons.arrow_2_squarepath, + ), + if (gameState.game.meta.speed == Speed.correspondence && + !gameState.game.finished) + BottomBarButton( + label: 'Go to the next game', + icon: Icons.skip_next, + onTap: ongoingGames.maybeWhen( + data: (games) { + final nextTurn = games + .whereNot((g) => g.fullId == id) + .firstWhereOrNull((g) => g.isMyTurn); + return nextTurn != null + ? () => onLoadGameCallback(nextTurn.fullId) + : null; + }, + orElse: () => null, + ), + ), BottomBarButton( - label: context.l10n.resign, - onTap: gameState.game.resignable - ? gameState.shouldConfirmResignAndDrawOffer - ? () => _showConfirmDialog( - context, - description: Text(context.l10n.resignTheGame), - onConfirm: () { - ref - .read(gameControllerProvider(id).notifier) - .resignGame(); - }, - ) - : () { - ref - .read(gameControllerProvider(id).notifier) - .resignGame(); - } + label: context.l10n.chat, + onTap: isChatEnabled + ? () { + pushPlatformRoute( + context, + builder: (BuildContext context) { + return MessageScreen( + title: UserFullNameWidget( + user: gameState.game.opponent?.user, + ), + me: gameState.game.me?.user, + id: id, + ); + }, + ); + } : null, - icon: Icons.flag, + icon: Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoIcons.chat_bubble + : Icons.chat_bubble_outline, + chip: chatUnreadChip, ), - if (gameState.game.meta.speed == Speed.correspondence && - !gameState.game.finished) - BottomBarButton( - label: 'Go to the next game', - icon: Icons.skip_next, - onTap: ongoingGames.maybeWhen( - data: (games) { - final nextTurn = games - .whereNot((g) => g.fullId == id) - .firstWhereOrNull((g) => g.isMyTurn); - return nextTurn != null - ? () => onLoadGameCallback(nextTurn.fullId) - : null; - }, - orElse: () => null, + RepeatButton( + onLongPress: + gameState.canGoBackward ? () => _moveBackward(ref) : null, + child: BottomBarButton( + onTap: + gameState.canGoBackward ? () => _moveBackward(ref) : null, + label: 'Previous', + icon: CupertinoIcons.chevron_back, + showTooltip: false, ), ), - BottomBarButton( - label: context.l10n.chat, - onTap: isChatEnabled - ? () { - pushPlatformRoute( - context, - builder: (BuildContext context) { - return MessageScreen( - title: UserFullNameWidget( - user: gameState.game.opponent?.user, - ), - me: gameState.game.me?.user, - id: id, - ); - }, - ); - } - : null, - icon: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoIcons.chat_bubble - : Icons.chat_bubble_outline, - chip: chatUnreadChip, - ), - RepeatButton( - onLongPress: - gameState.canGoBackward ? () => _moveBackward(ref) : null, - child: BottomBarButton( - onTap: gameState.canGoBackward ? () => _moveBackward(ref) : null, - label: 'Previous', - icon: CupertinoIcons.chevron_back, - showTooltip: false, - ), - ), - RepeatButton( - onLongPress: - gameState.canGoForward ? () => _moveForward(ref) : null, - child: BottomBarButton( - onTap: gameState.canGoForward ? () => _moveForward(ref) : null, - label: context.l10n.next, - icon: CupertinoIcons.chevron_forward, - showTooltip: false, + RepeatButton( + onLongPress: + gameState.canGoForward ? () => _moveForward(ref) : null, + child: BottomBarButton( + onTap: gameState.canGoForward ? () => _moveForward(ref) : null, + label: context.l10n.next, + icon: CupertinoIcons.chevron_forward, + showTooltip: false, + ), ), - ), - ]; - }, - loading: () => [], - error: (e, s) => [], - ); - - return BottomBar( - children: children, + ]; + }, + loading: () => [], + error: (e, s) => [], + ), ); } From 26510d3df01570ba724feb7e1899c2eaa777067a Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 3 Sep 2024 11:08:55 +0200 Subject: [PATCH 200/979] Improve share menu --- lib/src/view/game/game_common_widgets.dart | 324 +++++++++++++-------- lib/src/view/game/game_list_tile.dart | 4 +- 2 files changed, 212 insertions(+), 116 deletions(-) diff --git a/lib/src/view/game/game_common_widgets.dart b/lib/src/view/game/game_common_widgets.dart index d05a0a5261..f502c768eb 100644 --- a/lib/src/view/game/game_common_widgets.dart +++ b/lib/src/view/game/game_common_widgets.dart @@ -1,4 +1,5 @@ import 'package:dartchess/dartchess.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/challenge/challenge.dart'; @@ -78,126 +79,221 @@ List makeFinishedGameShareActions( }) { return [ BottomSheetAction( - makeLabel: (_) => Text(context.l10n.mobileShareGameURL), - dismissOnPress: false, + makeLabel: (_) => Text(context.l10n.studyShareAndExport), + dismissOnPress: true, onPressed: (context) { - launchShareDialog( - context, - uri: lichessUri('/${game.id}'), + showAdaptiveBottomSheet( + context: context, + useRootNavigator: true, + isDismissible: true, + isScrollControlled: true, + showDragHandle: true, + builder: (context) => GameShareBottomSheet( + game: game, + currentGamePosition: currentGamePosition, + orientation: orientation, + lastMove: lastMove, + ), ); }, ), - BottomSheetAction( - makeLabel: (context) => Text(context.l10n.gameAsGIF), - dismissOnPress: false, - onPressed: (context) async { - try { - final gif = await ref - .read(gameShareServiceProvider) - .gameGif(game.id, orientation); - if (context.mounted) { - launchShareDialog( - context, - files: [gif], - subject: context.l10n.resVsX( - game.white.fullName(context), - game.black.fullName(context), + ]; +} + +class GameShareBottomSheet extends ConsumerWidget { + const GameShareBottomSheet({ + required this.game, + required this.currentGamePosition, + required this.orientation, + this.lastMove, + super.key, + }); + + final BaseGame game; + final Position currentGamePosition; + final Side orientation; + final Move? lastMove; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return DraggableScrollableSheet( + initialChildSize: .5, + expand: false, + snap: true, + snapSizes: const [.5], + builder: (context, scrollController) => SingleChildScrollView( + controller: scrollController, + child: Column( + children: [ + BottomSheetContextMenuAction( + icon: CupertinoIcons.link, + closeOnPressed: false, + onPressed: () { + launchShareDialog( + context, + uri: lichessUri('/${game.id}'), + ); + }, + child: Text(context.l10n.mobileShareGameURL), + ), + // Builder is used to retrieve the context immediately surrounding the + // BottomSheetContextMenuAction + // This is necessary to get the correct context for the iPad share dialog + // which needs the position of the action to display the share dialog + Builder( + builder: (context) { + return BottomSheetContextMenuAction( + icon: Icons.gif, + closeOnPressed: false, // needed for the share dialog on iPad + child: Text(context.l10n.gameAsGIF), + onPressed: () async { + try { + final gif = await ref + .read(gameShareServiceProvider) + .gameGif(game.id, orientation); + if (context.mounted) { + launchShareDialog( + context, + files: [gif], + subject: + '${game.meta.perf.title} • ${context.l10n.resVsX( + game.white.fullName(context), + game.black.fullName(context), + )}', + ); + } + } catch (e) { + debugPrint(e.toString()); + if (context.mounted) { + showPlatformSnackbar( + context, + 'Failed to get GIF', + type: SnackBarType.error, + ); + } + } + }, + ); + }, + ), + if (lastMove != null) + // Builder is used to retrieve the context immediately surrounding the + // BottomSheetContextMenuAction + // This is necessary to get the correct context for the iPad share dialog + // which needs the position of the action to display the share dialog + Builder( + builder: (context) { + return BottomSheetContextMenuAction( + icon: Icons.image, + closeOnPressed: + false, // needed for the share dialog on iPad + child: Text(context.l10n.screenshotCurrentPosition), + onPressed: () async { + try { + final image = await ref + .read(gameShareServiceProvider) + .screenshotPosition( + game.id, + orientation, + currentGamePosition.fen, + lastMove, + ); + if (context.mounted) { + launchShareDialog( + context, + files: [image], + subject: context.l10n.puzzleFromGameLink( + lichessUri('/${game.id}').toString(), + ), + ); + } + } catch (e) { + if (context.mounted) { + showPlatformSnackbar( + context, + 'Failed to get GIF', + type: SnackBarType.error, + ); + } + } + }, + ); + }, ), - ); - } - } catch (e) { - debugPrint(e.toString()); - if (context.mounted) { - showPlatformSnackbar( - context, - 'Failed to get GIF', - type: SnackBarType.error, - ); - } - } - }, - ), - if (lastMove != null) - BottomSheetAction( - makeLabel: (context) => Text(context.l10n.screenshotCurrentPosition), - dismissOnPress: false, - onPressed: (context) async { - try { - final image = - await ref.read(gameShareServiceProvider).screenshotPosition( - game.id, - orientation, - currentGamePosition.fen, - lastMove, - ); - if (context.mounted) { - launchShareDialog( - context, - files: [image], - subject: context.l10n.puzzleFromGameLink( - lichessUri('/${game.id}').toString(), - ), - ); - } - } catch (e) { - if (context.mounted) { - showPlatformSnackbar( - context, - 'Failed to get GIF', - type: SnackBarType.error, - ); - } - } - }, + // Builder is used to retrieve the context immediately surrounding the + // BottomSheetContextMenuAction + // This is necessary to get the correct context for the iPad share dialog + // which needs the position of the action to display the share dialog + Builder( + builder: (context) { + return BottomSheetContextMenuAction( + icon: Icons.text_snippet, + closeOnPressed: false, // needed for the share dialog on iPad + child: Text('PGN: ${context.l10n.downloadAnnotated}'), + onPressed: () async { + try { + final pgn = await ref + .read(gameShareServiceProvider) + .annotatedPgn(game.id); + if (context.mounted) { + launchShareDialog( + context, + text: pgn, + ); + } + } catch (e) { + if (context.mounted) { + showPlatformSnackbar( + context, + 'Failed to get PGN', + type: SnackBarType.error, + ); + } + } + }, + ); + }, + ), + // Builder is used to retrieve the context immediately surrounding the + // BottomSheetContextMenuAction + // This is necessary to get the correct context for the iPad share dialog + // which needs the position of the action to display the share dialog + Builder( + builder: (context) { + return BottomSheetContextMenuAction( + icon: Icons.text_snippet, + closeOnPressed: false, // needed for the share dialog on iPad + // TODO improve translation + child: Text('PGN: ${context.l10n.downloadRaw}'), + onPressed: () async { + try { + final pgn = await ref + .read(gameShareServiceProvider) + .rawPgn(game.id); + if (context.mounted) { + launchShareDialog( + context, + text: pgn, + ); + } + } catch (e) { + if (context.mounted) { + showPlatformSnackbar( + context, + 'Failed to get PGN', + type: SnackBarType.error, + ); + } + } + }, + ); + }, + ), + ], + ), ), - BottomSheetAction( - makeLabel: (context) => Text('PGN: ${context.l10n.downloadAnnotated}'), - dismissOnPress: false, - onPressed: (context) async { - try { - final pgn = - await ref.read(gameShareServiceProvider).annotatedPgn(game.id); - if (context.mounted) { - launchShareDialog( - context, - text: pgn, - ); - } - } catch (e) { - if (context.mounted) { - showPlatformSnackbar( - context, - 'Failed to get PGN', - type: SnackBarType.error, - ); - } - } - }, - ), - BottomSheetAction( - makeLabel: (context) => Text('PGN: ${context.l10n.downloadRaw}'), - dismissOnPress: false, - onPressed: (context) async { - try { - final pgn = await ref.read(gameShareServiceProvider).rawPgn(game.id); - if (context.mounted) { - launchShareDialog( - context, - text: pgn, - ); - } - } catch (e) { - if (context.mounted) { - showPlatformSnackbar( - context, - 'Failed to get PGN', - type: SnackBarType.error, - ); - } - } - }, - ), - ]; + ); + } } class _LobbyGameTitle extends ConsumerWidget { diff --git a/lib/src/view/game/game_list_tile.dart b/lib/src/view/game/game_list_tile.dart index 89dffb101b..78f6b42514 100644 --- a/lib/src/view/game/game_list_tile.dart +++ b/lib/src/view/game/game_list_tile.dart @@ -362,7 +362,7 @@ class _ContextMenu extends ConsumerWidget { Builder( builder: (context) { return BottomSheetContextMenuAction( - icon: CupertinoIcons.share, + icon: Icons.text_snippet, closeOnPressed: false, // needed for the share dialog on iPad child: Text('PGN: ${context.l10n.downloadAnnotated}'), @@ -397,7 +397,7 @@ class _ContextMenu extends ConsumerWidget { Builder( builder: (context) { return BottomSheetContextMenuAction( - icon: CupertinoIcons.share, + icon: Icons.text_snippet, closeOnPressed: false, // needed for the share dialog on iPad // TODO improve translation From 753b74a85cae1642178fea1d76019fa6a2b56f7a Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 3 Sep 2024 11:14:01 +0200 Subject: [PATCH 201/979] Fix archived game screen --- lib/src/view/game/archived_game_screen.dart | 23 +++++++++------------ 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/lib/src/view/game/archived_game_screen.dart b/lib/src/view/game/archived_game_screen.dart index 73469c6e44..f52fe09671 100644 --- a/lib/src/view/game/archived_game_screen.dart +++ b/lib/src/view/game/archived_game_screen.dart @@ -214,19 +214,16 @@ class _BottomBar extends ConsumerWidget { ), gameCursor.when( data: (data) { - return Expanded( - child: BottomBarButton( - label: context.l10n.mobileShowResult, - icon: Icons.info_outline, - onTap: () { - showAdaptiveDialog( - context: context, - builder: (context) => - ArchivedGameResultDialog(game: data.$1), - barrierDismissible: true, - ); - }, - ), + return BottomBarButton( + label: context.l10n.mobileShowResult, + icon: Icons.info_outline, + onTap: () { + showAdaptiveDialog( + context: context, + builder: (context) => ArchivedGameResultDialog(game: data.$1), + barrierDismissible: true, + ); + }, ); }, loading: () => const SizedBox.shrink(), From 9a8c57937e60dacd1980726a41ddfeafb0814025 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 3 Sep 2024 11:23:40 +0200 Subject: [PATCH 202/979] Only show explorer if online --- lib/src/view/tools/tools_tab_screen.dart | 71 +++++++++++++----------- 1 file changed, 38 insertions(+), 33 deletions(-) diff --git a/lib/src/view/tools/tools_tab_screen.dart b/lib/src/view/tools/tools_tab_screen.dart index 97a6562c6f..2e7b30aa0f 100644 --- a/lib/src/view/tools/tools_tab_screen.dart +++ b/lib/src/view/tools/tools_tab_screen.dart @@ -6,6 +6,7 @@ import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/navigation.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; @@ -61,15 +62,18 @@ class ToolsTabScreen extends ConsumerWidget { } } -class _Body extends StatelessWidget { +class _Body extends ConsumerWidget { const _Body(); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { final tilePadding = Theme.of(context).platform == TargetPlatform.iOS ? const EdgeInsets.symmetric(vertical: 8.0) : EdgeInsets.zero; + final isOnline = + ref.watch(connectivityChangesProvider).valueOrNull?.isOnline ?? false; + final content = [ const SizedBox(height: 16.0), ListSection( @@ -134,41 +138,42 @@ class _Body extends StatelessWidget { ), ), ), - Padding( - padding: Theme.of(context).platform == TargetPlatform.android - ? const EdgeInsets.only(bottom: 16.0) - : EdgeInsets.zero, - child: PlatformListTile( - leading: Icon( - Icons.explore, - size: Styles.mainListTileIconSize, - color: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoTheme.of(context).primaryColor - : Theme.of(context).colorScheme.primary, - ), - title: Padding( - padding: tilePadding, - child: - Text(context.l10n.openingExplorer, style: Styles.callout), - ), - trailing: Theme.of(context).platform == TargetPlatform.iOS - ? const CupertinoListTileChevron() - : null, - onTap: () => pushPlatformRoute( - context, - rootNavigator: true, - builder: (context) => const OpeningExplorerScreen( - pgn: '', - options: AnalysisOptions( - isLocalEvaluationAllowed: false, - variant: Variant.standard, - orientation: Side.white, - id: standaloneAnalysisId, + if (isOnline) + Padding( + padding: Theme.of(context).platform == TargetPlatform.android + ? const EdgeInsets.only(bottom: 16.0) + : EdgeInsets.zero, + child: PlatformListTile( + leading: Icon( + Icons.explore, + size: Styles.mainListTileIconSize, + color: Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoTheme.of(context).primaryColor + : Theme.of(context).colorScheme.primary, + ), + title: Padding( + padding: tilePadding, + child: + Text(context.l10n.openingExplorer, style: Styles.callout), + ), + trailing: Theme.of(context).platform == TargetPlatform.iOS + ? const CupertinoListTileChevron() + : null, + onTap: () => pushPlatformRoute( + context, + rootNavigator: true, + builder: (context) => const OpeningExplorerScreen( + pgn: '', + options: AnalysisOptions( + isLocalEvaluationAllowed: false, + variant: Variant.standard, + orientation: Side.white, + id: standaloneAnalysisId, + ), ), ), ), ), - ), Padding( padding: Theme.of(context).platform == TargetPlatform.android ? const EdgeInsets.only(bottom: 16.0) From 7d3aa3e37ae0d54de8a61f894f84789b6e34ee15 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 3 Sep 2024 11:41:01 +0200 Subject: [PATCH 203/979] Disable explorer in analysis if offline --- lib/src/view/analysis/analysis_screen.dart | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index 280728ba17..44f4611679 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -24,6 +24,7 @@ import 'package:lichess_mobile/src/model/game/game_share_service.dart'; import 'package:lichess_mobile/src/model/settings/brightness.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; @@ -589,6 +590,8 @@ class _BottomBar extends ConsumerWidget { ref.watch(ctrlProvider.select((value) => value.displayMode)); final canShowGameSummary = ref.watch(ctrlProvider.select((value) => value.canShowGameSummary)); + final isOnline = + ref.watch(connectivityChangesProvider).valueOrNull?.isOnline ?? false; return BottomBar( children: [ @@ -611,13 +614,17 @@ class _BottomBar extends ConsumerWidget { ), BottomBarButton( label: context.l10n.openingExplorer, - onTap: () => pushPlatformRoute( - context, - builder: (_) => OpeningExplorerScreen( - pgn: pgn, - options: options, - ), - ), + onTap: isOnline + ? () { + pushPlatformRoute( + context, + builder: (_) => OpeningExplorerScreen( + pgn: pgn, + options: options, + ), + ); + } + : null, icon: Icons.explore, ), RepeatButton( From 26f129ed1c217affb218cdc54202146cbcf02842 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 3 Sep 2024 11:41:32 +0200 Subject: [PATCH 204/979] Bump version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 099472c61c..57bc99e76a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: lichess_mobile description: Lichess mobile app V2 publish_to: "none" -version: 0.9.7+000907 # see README.md for details about versioning +version: 0.10.0+001000 # see README.md for details about versioning environment: sdk: ">=3.3.0 <4.0.0" From 25423d6c0f6d99ffe5061a6ef869d5161121de4a Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 3 Sep 2024 12:03:51 +0200 Subject: [PATCH 205/979] Improve explorer white and black colors --- .../opening_explorer_screen.dart | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index a5dc2489ee..956ce05401 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -35,6 +35,16 @@ const _kTableRowPadding = EdgeInsets.symmetric( vertical: _kTableRowVerticalPadding, ); +Color _whiteBoxColor(BuildContext context) => + Theme.of(context).brightness == Brightness.dark + ? Colors.white.withOpacity(0.8) + : Colors.white; + +Color _blackBoxColor(BuildContext context) => + Theme.of(context).brightness == Brightness.light + ? Colors.black.withOpacity(0.7) + : Colors.black; + class OpeningExplorerScreen extends StatelessWidget { const OpeningExplorerScreen({required this.pgn, required this.options}); @@ -370,6 +380,10 @@ class _OpeningExplorerView extends StatelessWidget { final isLandscape = MediaQuery.orientationOf(context) == Orientation.landscape; + final loadingOverlayColor = Theme.of(context).brightness == Brightness.dark + ? Colors.black + : Colors.white; + return Column( mainAxisSize: MainAxisSize.max, children: [ @@ -413,7 +427,7 @@ class _OpeningExplorerView extends StatelessWidget { duration: const Duration(milliseconds: 300), curve: Curves.fastOutSlowIn, opacity: loading ? 0.5 : 0.0, - child: const ColoredBox(color: Colors.white), + child: ColoredBox(color: loadingOverlayColor), ), ), ), @@ -790,7 +804,7 @@ class OpeningExplorerGameTile extends ConsumerWidget { width: widthResultBox, padding: paddingResultBox, decoration: BoxDecoration( - color: Colors.white, + color: _whiteBoxColor(context), borderRadius: BorderRadius.circular(5), ), child: const Text( @@ -806,7 +820,7 @@ class OpeningExplorerGameTile extends ConsumerWidget { width: widthResultBox, padding: paddingResultBox, decoration: BoxDecoration( - color: Colors.black, + color: _blackBoxColor(context), borderRadius: BorderRadius.circular(5), ), child: const Text( @@ -878,7 +892,7 @@ class _WinPercentageChart extends StatelessWidget { Expanded( flex: percentWhite, child: ColoredBox( - color: Colors.white, + color: _whiteBoxColor(context), child: Text( label(percentWhite), textAlign: TextAlign.center, @@ -900,7 +914,7 @@ class _WinPercentageChart extends StatelessWidget { Expanded( flex: percentBlack, child: ColoredBox( - color: Colors.black, + color: _blackBoxColor(context), child: Text( label(percentBlack), textAlign: TextAlign.center, From 51352393dfbadc8e581c0756892a807359e7ce28 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 3 Sep 2024 12:09:29 +0200 Subject: [PATCH 206/979] Tweak explorer colors --- .../view/opening_explorer/opening_explorer_screen.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index 956ce05401..b886bbdc09 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -390,7 +390,7 @@ class _OpeningExplorerView extends StatelessWidget { Container( padding: _kTableRowPadding, decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primaryContainer, + color: Theme.of(context).colorScheme.secondaryContainer, borderRadius: BorderRadius.only( topLeft: Radius.circular(isLandscape ? 4.0 : 0), topRight: Radius.circular(isLandscape ? 4.0 : 0), @@ -453,7 +453,7 @@ class _Opening extends ConsumerWidget { final openingWidget = Text( '${opening.eco.isEmpty ? "" : "${opening.eco} "}${opening.name}', style: TextStyle( - color: Theme.of(context).colorScheme.primary, + color: Theme.of(context).colorScheme.onSecondaryContainer, fontWeight: FontWeight.bold, ), ); @@ -614,7 +614,7 @@ class OpeningExplorerMoveTable extends ConsumerWidget { children: [ TableRow( decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primaryContainer, + color: Theme.of(context).colorScheme.secondaryContainer, ), children: [ Padding( @@ -725,7 +725,7 @@ class OpeningExplorerGameList extends StatelessWidget { children: [ Container( padding: _kTableRowPadding, - color: Theme.of(context).colorScheme.primaryContainer, + color: Theme.of(context).colorScheme.secondaryContainer, child: Row( children: [Text(title)], ), From 04ad6b7607d94719660d42b46507ff764a19241d Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Tue, 3 Sep 2024 22:41:15 +0200 Subject: [PATCH 207/979] refactor: add PlatformAlertDialog --- lib/src/view/game/game_body.dart | 129 +++++++-------------- lib/src/view/puzzle/storm_screen.dart | 33 ++---- lib/src/view/puzzle/streak_screen.dart | 83 +++++-------- lib/src/widgets/platform_alert_dialog.dart | 77 ++++++++++++ lib/src/widgets/yes_no_dialog.dart | 47 +++----- 5 files changed, 172 insertions(+), 197 deletions(-) create mode 100644 lib/src/widgets/platform_alert_dialog.dart diff --git a/lib/src/view/game/game_body.dart b/lib/src/view/game/game_body.dart index 0b4715b9c4..7499082212 100644 --- a/lib/src/view/game/game_body.dart +++ b/lib/src/view/game/game_body.dart @@ -29,6 +29,7 @@ import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/countdown_clock.dart'; +import 'package:lichess_mobile/src/widgets/platform_alert_dialog.dart'; import 'package:lichess_mobile/src/widgets/user_full_name.dart'; import 'package:lichess_mobile/src/widgets/yes_no_dialog.dart'; @@ -874,35 +875,19 @@ class _GameNegotiationDialog extends StatelessWidget { onAccept(); } - if (Theme.of(context).platform == TargetPlatform.iOS) { - return CupertinoAlertDialog( - content: title, - actions: [ - CupertinoDialogAction( - onPressed: accept, - child: Text(context.l10n.accept), - ), - CupertinoDialogAction( - onPressed: decline, - child: Text(context.l10n.decline), - ), - ], - ); - } else { - return AlertDialog( - content: title, - actions: [ - TextButton( - onPressed: accept, - child: Text(context.l10n.accept), - ), - TextButton( - onPressed: decline, - child: Text(context.l10n.decline), - ), - ], - ); - } + return PlatformAlertDialog( + content: title, + actions: [ + PlatformDialogAction( + onPressed: accept, + child: Text(context.l10n.accept), + ), + PlatformDialogAction( + onPressed: decline, + child: Text(context.l10n.decline), + ), + ], + ); } } @@ -926,35 +911,19 @@ class _ThreefoldDialog extends ConsumerWidget { ref.read(gameControllerProvider(id).notifier).claimDraw(); } - if (Theme.of(context).platform == TargetPlatform.iOS) { - return CupertinoAlertDialog( - content: content, - actions: [ - CupertinoDialogAction( - onPressed: accept, - child: Text(context.l10n.claimADraw), - ), - CupertinoDialogAction( - onPressed: decline, - child: Text(context.l10n.cancel), - ), - ], - ); - } else { - return AlertDialog( - content: content, - actions: [ - TextButton( - onPressed: accept, - child: Text(context.l10n.claimADraw), - ), - TextButton( - onPressed: decline, - child: Text(context.l10n.cancel), - ), - ], - ); - } + return PlatformAlertDialog( + content: content, + actions: [ + PlatformDialogAction( + onPressed: accept, + child: Text(context.l10n.claimADraw), + ), + PlatformDialogAction( + onPressed: decline, + child: Text(context.l10n.cancel), + ), + ], + ); } } @@ -982,36 +951,20 @@ class _ClaimWinDialog extends ConsumerWidget { ref.read(ctrlProvider.notifier).forceDraw(); } - if (Theme.of(context).platform == TargetPlatform.iOS) { - return CupertinoAlertDialog( - content: content, - actions: [ - CupertinoDialogAction( - onPressed: gameState.game.canClaimWin ? onClaimWin : null, - isDefaultAction: true, - child: Text(context.l10n.forceResignation), - ), - CupertinoDialogAction( - onPressed: gameState.game.canClaimWin ? onClaimDraw : null, - child: Text(context.l10n.forceDraw), - ), - ], - ); - } else { - return AlertDialog( - content: content, - actions: [ - TextButton( - onPressed: gameState.game.canClaimWin ? onClaimWin : null, - child: Text(context.l10n.forceResignation), - ), - TextButton( - onPressed: gameState.game.canClaimWin ? onClaimDraw : null, - child: Text(context.l10n.forceDraw), - ), - ], - ); - } + return PlatformAlertDialog( + content: content, + actions: [ + PlatformDialogAction( + onPressed: gameState.game.canClaimWin ? onClaimWin : null, + cupertinoIsDefaultAction: true, + child: Text(context.l10n.forceResignation), + ), + PlatformDialogAction( + onPressed: gameState.game.canClaimWin ? onClaimDraw : null, + child: Text(context.l10n.forceDraw), + ), + ], + ); } } diff --git a/lib/src/view/puzzle/storm_screen.dart b/lib/src/view/puzzle/storm_screen.dart index 8540df9efd..c7b3a8cfd4 100644 --- a/lib/src/view/puzzle/storm_screen.dart +++ b/lib/src/view/puzzle/storm_screen.dart @@ -29,6 +29,7 @@ import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; +import 'package:lichess_mobile/src/widgets/platform_alert_dialog.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:lichess_mobile/src/widgets/yes_no_dialog.dart'; @@ -259,27 +260,17 @@ Future _stormInfoDialogBuilder(BuildContext context) { ), ), ); - return Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoAlertDialog( - title: Text(context.l10n.aboutX('Puzzle Storm')), - content: content, - actions: [ - CupertinoDialogAction( - onPressed: () => Navigator.of(context).pop(), - child: Text(context.l10n.mobileOkButton), - ), - ], - ) - : AlertDialog( - title: Text(context.l10n.aboutX('Puzzle Storm')), - content: content, - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text(context.l10n.mobileOkButton), - ), - ], - ); + + return PlatformAlertDialog( + title: Text(context.l10n.aboutX('Puzzle Storm')), + content: content, + actions: [ + PlatformDialogAction( + onPressed: () => Navigator.of(context).pop(), + child: Text(context.l10n.mobileOkButton), + ), + ], + ); }, ); } diff --git a/lib/src/view/puzzle/streak_screen.dart b/lib/src/view/puzzle/streak_screen.dart index 2ef6652d1e..77331d132d 100644 --- a/lib/src/view/puzzle/streak_screen.dart +++ b/lib/src/view/puzzle/streak_screen.dart @@ -25,6 +25,7 @@ import 'package:lichess_mobile/src/view/settings/toggle_sound_button.dart'; import 'package:lichess_mobile/src/widgets/board_table.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; +import 'package:lichess_mobile/src/widgets/platform_alert_dialog.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:lichess_mobile/src/widgets/yes_no_dialog.dart'; import 'package:result_extensions/result_extensions.dart'; @@ -337,29 +338,16 @@ class _BottomBar extends ConsumerWidget { Future _streakInfoDialogBuilder(BuildContext context) { return showAdaptiveDialog( context: context, - builder: (context) { - return Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoAlertDialog( - title: Text(context.l10n.aboutX('Puzzle Streak')), - content: Text(context.l10n.puzzleStreakDescription), - actions: [ - CupertinoDialogAction( - onPressed: () => Navigator.of(context).pop(), - child: Text(context.l10n.mobileOkButton), - ), - ], - ) - : AlertDialog( - title: Text(context.l10n.aboutX('Puzzle Streak')), - content: Text(context.l10n.puzzleStreakDescription), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text(context.l10n.mobileOkButton), - ), - ], - ); - }, + builder: (context) => PlatformAlertDialog( + title: Text(context.l10n.aboutX('Puzzle Streak')), + content: Text(context.l10n.puzzleStreakDescription), + actions: [ + PlatformDialogAction( + onPressed: () => Navigator.of(context).pop(), + child: Text(context.l10n.mobileOkButton), + ), + ], + ), ); } } @@ -405,38 +393,21 @@ class _RetryFetchPuzzleDialog extends ConsumerWidget { final canRetry = state.nextPuzzleStreakFetchError && !state.nextPuzzleStreakFetchIsRetrying; - if (Theme.of(context).platform == TargetPlatform.iOS) { - return CupertinoAlertDialog( - title: const Text(title), - content: const Text(content), - actions: [ - CupertinoDialogAction( - onPressed: () => Navigator.of(context).pop(), - isDestructiveAction: true, - child: Text(context.l10n.cancel), - ), - CupertinoDialogAction( - onPressed: canRetry ? retryStreakNext : null, - isDefaultAction: true, - child: Text(context.l10n.retry), - ), - ], - ); - } else { - return AlertDialog( - title: const Text(title), - content: const Text(content), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text(context.l10n.cancel), - ), - TextButton( - onPressed: canRetry ? retryStreakNext : null, - child: Text(context.l10n.retry), - ), - ], - ); - } + return PlatformAlertDialog( + title: const Text(title), + content: const Text(content), + actions: [ + PlatformDialogAction( + onPressed: () => Navigator.of(context).pop(), + cupertinoIsDestructiveAction: true, + child: Text(context.l10n.cancel), + ), + PlatformDialogAction( + onPressed: canRetry ? retryStreakNext : null, + cupertinoIsDefaultAction: true, + child: Text(context.l10n.retry), + ), + ], + ); } } diff --git a/lib/src/widgets/platform_alert_dialog.dart b/lib/src/widgets/platform_alert_dialog.dart new file mode 100644 index 0000000000..7b135aacc3 --- /dev/null +++ b/lib/src/widgets/platform_alert_dialog.dart @@ -0,0 +1,77 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:lichess_mobile/src/widgets/platform.dart'; + +/// Displays an [AlertDialog] for Android and a [CupertinoAlertDialog] for iOS. +class PlatformAlertDialog extends StatelessWidget { + const PlatformAlertDialog({ + super.key, + this.title, + this.content, + this.actions = const [], + }); + + /// See [AlertDialog.title] for Android and [CupertinoAlertDialog.title] for iOS. + final Widget? title; + + /// See [AlertDialog.content] for Android and [CupertinoAlertDialog.content] for iOS. + final Widget? content; + + /// See [AlertDialog.actions] for Android and [CupertinoAlertDialog.actions] for iOS. + final List actions; + + @override + Widget build(BuildContext context) { + return PlatformWidget( + androidBuilder: (context) => AlertDialog( + title: title, + content: content, + actions: actions, + ), + iosBuilder: (context) => CupertinoAlertDialog( + title: title, + content: content, + actions: actions, + ), + ); + } +} + +/// To be used with [PlatformAlertDialog.actions]. Displays a [TextButton] for Android and a [CupertinoDialogAction] for iOS. +class PlatformDialogAction extends StatelessWidget { + const PlatformDialogAction({ + super.key, + required this.onPressed, + required this.child, + this.cupertinoIsDefaultAction = false, + this.cupertinoIsDestructiveAction = false, + }); + + /// Callback invoked when the action is pressed. + final VoidCallback? onPressed; + + /// See [TextButton.child] for Android and [CupertinoDialogAction.child] for iOS. + final Widget child; + + /// Passed to [CupertinoDialogAction.isDefaultAction] on iOS, no effect on Android. + final bool cupertinoIsDefaultAction; + + /// Passed to [CupertinoDialogAction.isDestructiveAction] on iOS, no effect on Android. + final bool cupertinoIsDestructiveAction; + + @override + Widget build(BuildContext context) { + return PlatformWidget( + androidBuilder: (context) => TextButton( + onPressed: onPressed, + child: child, + ), + iosBuilder: (context) => CupertinoDialogAction( + onPressed: onPressed, + isDefaultAction: cupertinoIsDefaultAction, + isDestructiveAction: cupertinoIsDestructiveAction, + child: child, + ), + ); + } +} diff --git a/lib/src/widgets/yes_no_dialog.dart b/lib/src/widgets/yes_no_dialog.dart index 292815c050..fb1d0ff1b5 100644 --- a/lib/src/widgets/yes_no_dialog.dart +++ b/lib/src/widgets/yes_no_dialog.dart @@ -1,6 +1,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/widgets/platform_alert_dialog.dart'; class YesNoDialog extends StatelessWidget { const YesNoDialog({ @@ -20,37 +21,19 @@ class YesNoDialog extends StatelessWidget { @override Widget build(BuildContext context) { - if (Theme.of(context).platform == TargetPlatform.iOS) { - return CupertinoAlertDialog( - title: title, - content: content, - actions: [ - CupertinoDialogAction( - onPressed: onNo, - child: Text(context.l10n.no), - ), - CupertinoDialogAction( - onPressed: onYes, - child: Text(context.l10n.yes), - ), - ], - ); - } else { - return AlertDialog( - title: title, - content: content, - alignment: alignment, - actions: [ - TextButton( - onPressed: onNo, - child: Text(context.l10n.no), - ), - TextButton( - onPressed: onYes, - child: Text(context.l10n.yes), - ), - ], - ); - } + return PlatformAlertDialog( + title: title, + content: content, + actions: [ + PlatformDialogAction( + onPressed: onNo, + child: Text(context.l10n.no), + ), + PlatformDialogAction( + onPressed: onYes, + child: Text(context.l10n.yes), + ), + ], + ); } } From 5a89e91c98ebca5a8fa66e46335860bf6fdcae0c Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Mon, 19 Aug 2024 18:14:18 +0200 Subject: [PATCH 208/979] refactor: factor out CountdownClock code into stateless Clock widget We'll reuse this for implementing the Over-the-Board clock --- lib/src/widgets/countdown_clock.dart | 49 ++++++++++++++++++++++++---- 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/lib/src/widgets/countdown_clock.dart b/lib/src/widgets/countdown_clock.dart index acb2a81948..a9e61df89a 100644 --- a/lib/src/widgets/countdown_clock.dart +++ b/lib/src/widgets/countdown_clock.dart @@ -142,23 +142,58 @@ class _CountdownClockState extends ConsumerState { _timer?.cancel(); } + @override + Widget build(BuildContext context) { + final brightness = ref.watch(currentBrightnessProvider); + return Clock( + timeLeft: timeLeft, + active: widget.active, + emergencyThreshold: widget.emergencyThreshold, + clockStyle: getStyle(brightness), + ); + } +} + +/// A stateless widget that displays the time left on the clock. +/// +/// For a clock widget that automatically counts down, see [CountdownClock]. +class Clock extends StatelessWidget { + /// The time left to be displayed on the clock. + final Duration timeLeft; + + /// If `true`, [ClockStyle.activeBackgroundColor] will be used, otherwise [ClockStyle.backgroundColor]. + final bool active; + + /// If [timeLeft] is less than [emergencyThreshold], the clock will set + /// its background color to [ClockStyle.emergencyBackgroundColor]. + final Duration? emergencyThreshold; + + /// Clock style to use. + final ClockStyle clockStyle; + + const Clock({ + required this.timeLeft, + required this.active, + required this.clockStyle, + this.emergencyThreshold, + super.key, + }); + @override Widget build(BuildContext context) { final hours = timeLeft.inHours; final mins = timeLeft.inMinutes.remainder(60); final secs = timeLeft.inSeconds.remainder(60).toString().padLeft(2, '0'); final showTenths = timeLeft < const Duration(seconds: 10); - final isEmergency = widget.emergencyThreshold != null && - timeLeft <= widget.emergencyThreshold!; - final brightness = ref.watch(currentBrightnessProvider); - final clockStyle = getStyle(brightness); + final isEmergency = + emergencyThreshold != null && timeLeft <= emergencyThreshold!; final remainingHeight = estimateRemainingHeightLeftBoard(context); return RepaintBoundary( child: Container( decoration: BoxDecoration( borderRadius: const BorderRadius.all(Radius.circular(5.0)), - color: widget.active + color: active ? isEmergency ? clockStyle.emergencyBackgroundColor : clockStyle.activeBackgroundColor @@ -174,7 +209,7 @@ class _CountdownClockState extends ConsumerState { ? '$hours:${mins.toString().padLeft(2, '0')}:$secs' : '$mins:$secs', style: TextStyle( - color: widget.active + color: active ? isEmergency ? clockStyle.emergencyTextColor : clockStyle.activeTextColor @@ -195,7 +230,7 @@ class _CountdownClockState extends ConsumerState { '.${timeLeft.inMilliseconds.remainder(1000) ~/ 100}', style: const TextStyle(fontSize: 20), ), - if (!widget.active && timeLeft < const Duration(seconds: 1)) + if (!active && timeLeft < const Duration(seconds: 1)) TextSpan( text: '${timeLeft.inMilliseconds.remainder(1000) ~/ 10 % 10}', From c9f9a5dc5c549a6e7ee5165d61a71abd836a8b9a Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Mon, 19 Aug 2024 20:33:50 +0200 Subject: [PATCH 209/979] feat: add randomChess960Position() --- lib/src/model/common/chess.dart | 3 + lib/src/model/common/chess960.dart | 983 +++++++++++++++++++++++++++++ 2 files changed, 986 insertions(+) create mode 100644 lib/src/model/common/chess960.dart diff --git a/lib/src/model/common/chess.dart b/lib/src/model/common/chess.dart index 896b02d7c4..3bc5732bae 100644 --- a/lib/src/model/common/chess.dart +++ b/lib/src/model/common/chess.dart @@ -125,6 +125,9 @@ enum Variant { case Variant.standard: return Chess.initial; case Variant.chess960: + throw ArgumentError( + 'Chess960 has not single initial position, use randomChess960Position() instead.', + ); case Variant.fromPosition: throw ArgumentError('This variant has no defined initial position!'); case Variant.antichess: diff --git a/lib/src/model/common/chess960.dart b/lib/src/model/common/chess960.dart new file mode 100644 index 0000000000..d16fb640fd --- /dev/null +++ b/lib/src/model/common/chess960.dart @@ -0,0 +1,983 @@ +import 'dart:math'; + +import 'package:dartchess/dartchess.dart'; + +final _random = Random.secure(); + +Position randomChess960Position() { + final rank8 = _positions[_random.nextInt(_positions.length)]; + + return Chess( + board: Board.parseFen( + '$rank8/pppppppp/8/8/8/8/PPPPPPPP/${rank8.toUpperCase()}', + ), + turn: Side.white, + castles: Castles.standard, + halfmoves: 0, + fullmoves: 1, + ); +} + +// https://github.com/lichess-org/scalachess/blob/bd139c6dc1acdc8fff08c46e412f784d49a16578/core/src/main/scala/variant/Chess960.scala#L49 +final _positions = [ + 'bbqnnrkr', + 'bqnbnrkr', + 'bqnnrbkr', + 'bqnnrkrb', + 'qbbnnrkr', + 'qnbbnrkr', + 'qnbnrbkr', + 'qnbnrkrb', + 'qbnnbrkr', + 'qnnbbrkr', + 'qnnrbbkr', + 'qnnrbkrb', + 'qbnnrkbr', + 'qnnbrkbr', + 'qnnrkbbr', + 'qnnrkrbb', + 'bbnqnrkr', + 'bnqbnrkr', + 'bnqnrbkr', + 'bnqnrkrb', + 'nbbqnrkr', + 'nqbbnrkr', + 'nqbnrbkr', + 'nqbnrkrb', + 'nbqnbrkr', + 'nqnbbrkr', + 'nqnrbbkr', + 'nqnrbkrb', + 'nbqnrkbr', + 'nqnbrkbr', + 'nqnrkbbr', + 'nqnrkrbb', + 'bbnnqrkr', + 'bnnbqrkr', + 'bnnqrbkr', + 'bnnqrkrb', + 'nbbnqrkr', + 'nnbbqrkr', + 'nnbqrbkr', + 'nnbqrkrb', + 'nbnqbrkr', + 'nnqbbrkr', + 'nnqrbbkr', + 'nnqrbkrb', + 'nbnqrkbr', + 'nnqbrkbr', + 'nnqrkbbr', + 'nnqrkrbb', + 'bbnnrqkr', + 'bnnbrqkr', + 'bnnrqbkr', + 'bnnrqkrb', + 'nbbnrqkr', + 'nnbbrqkr', + 'nnbrqbkr', + 'nnbrqkrb', + 'nbnrbqkr', + 'nnrbbqkr', + 'nnrqbbkr', + 'nnrqbkrb', + 'nbnrqkbr', + 'nnrbqkbr', + 'nnrqkbbr', + 'nnrqkrbb', + 'bbnnrkqr', + 'bnnbrkqr', + 'bnnrkbqr', + 'bnnrkqrb', + 'nbbnrkqr', + 'nnbbrkqr', + 'nnbrkbqr', + 'nnbrkqrb', + 'nbnrbkqr', + 'nnrbbkqr', + 'nnrkbbqr', + 'nnrkbqrb', + 'nbnrkqbr', + 'nnrbkqbr', + 'nnrkqbbr', + 'nnrkqrbb', + 'bbnnrkrq', + 'bnnbrkrq', + 'bnnrkbrq', + 'bnnrkrqb', + 'nbbnrkrq', + 'nnbbrkrq', + 'nnbrkbrq', + 'nnbrkrqb', + 'nbnrbkrq', + 'nnrbbkrq', + 'nnrkbbrq', + 'nnrkbrqb', + 'nbnrkrbq', + 'nnrbkrbq', + 'nnrkrbbq', + 'nnrkrqbb', + 'bbqnrnkr', + 'bqnbrnkr', + 'bqnrnbkr', + 'bqnrnkrb', + 'qbbnrnkr', + 'qnbbrnkr', + 'qnbrnbkr', + 'qnbrnkrb', + 'qbnrbnkr', + 'qnrbbnkr', + 'qnrnbbkr', + 'qnrnbkrb', + 'qbnrnkbr', + 'qnrbnkbr', + 'qnrnkbbr', + 'qnrnkrbb', + 'bbnqrnkr', + 'bnqbrnkr', + 'bnqrnbkr', + 'bnqrnkrb', + 'nbbqrnkr', + 'nqbbrnkr', + 'nqbrnbkr', + 'nqbrnkrb', + 'nbqrbnkr', + 'nqrbbnkr', + 'nqrnbbkr', + 'nqrnbkrb', + 'nbqrnkbr', + 'nqrbnkbr', + 'nqrnkbbr', + 'nqrnkrbb', + 'bbnrqnkr', + 'bnrbqnkr', + 'bnrqnbkr', + 'bnrqnkrb', + 'nbbrqnkr', + 'nrbbqnkr', + 'nrbqnbkr', + 'nrbqnkrb', + 'nbrqbnkr', + 'nrqbbnkr', + 'nrqnbbkr', + 'nrqnbkrb', + 'nbrqnkbr', + 'nrqbnkbr', + 'nrqnkbbr', + 'nrqnkrbb', + 'bbnrnqkr', + 'bnrbnqkr', + 'bnrnqbkr', + 'bnrnqkrb', + 'nbbrnqkr', + 'nrbbnqkr', + 'nrbnqbkr', + 'nrbnqkrb', + 'nbrnbqkr', + 'nrnbbqkr', + 'nrnqbbkr', + 'nrnqbkrb', + 'nbrnqkbr', + 'nrnbqkbr', + 'nrnqkbbr', + 'nrnqkrbb', + 'bbnrnkqr', + 'bnrbnkqr', + 'bnrnkbqr', + 'bnrnkqrb', + 'nbbrnkqr', + 'nrbbnkqr', + 'nrbnkbqr', + 'nrbnkqrb', + 'nbrnbkqr', + 'nrnbbkqr', + 'nrnkbbqr', + 'nrnkbqrb', + 'nbrnkqbr', + 'nrnbkqbr', + 'nrnkqbbr', + 'nrnkqrbb', + 'bbnrnkrq', + 'bnrbnkrq', + 'bnrnkbrq', + 'bnrnkrqb', + 'nbbrnkrq', + 'nrbbnkrq', + 'nrbnkbrq', + 'nrbnkrqb', + 'nbrnbkrq', + 'nrnbbkrq', + 'nrnkbbrq', + 'nrnkbrqb', + 'nbrnkrbq', + 'nrnbkrbq', + 'nrnkrbbq', + 'nrnkrqbb', + 'bbqnrknr', + 'bqnbrknr', + 'bqnrkbnr', + 'bqnrknrb', + 'qbbnrknr', + 'qnbbrknr', + 'qnbrkbnr', + 'qnbrknrb', + 'qbnrbknr', + 'qnrbbknr', + 'qnrkbbnr', + 'qnrkbnrb', + 'qbnrknbr', + 'qnrbknbr', + 'qnrknbbr', + 'qnrknrbb', + 'bbnqrknr', + 'bnqbrknr', + 'bnqrkbnr', + 'bnqrknrb', + 'nbbqrknr', + 'nqbbrknr', + 'nqbrkbnr', + 'nqbrknrb', + 'nbqrbknr', + 'nqrbbknr', + 'nqrkbbnr', + 'nqrkbnrb', + 'nbqrknbr', + 'nqrbknbr', + 'nqrknbbr', + 'nqrknrbb', + 'bbnrqknr', + 'bnrbqknr', + 'bnrqkbnr', + 'bnrqknrb', + 'nbbrqknr', + 'nrbbqknr', + 'nrbqkbnr', + 'nrbqknrb', + 'nbrqbknr', + 'nrqbbknr', + 'nrqkbbnr', + 'nrqkbnrb', + 'nbrqknbr', + 'nrqbknbr', + 'nrqknbbr', + 'nrqknrbb', + 'bbnrkqnr', + 'bnrbkqnr', + 'bnrkqbnr', + 'bnrkqnrb', + 'nbbrkqnr', + 'nrbbkqnr', + 'nrbkqbnr', + 'nrbkqnrb', + 'nbrkbqnr', + 'nrkbbqnr', + 'nrkqbbnr', + 'nrkqbnrb', + 'nbrkqnbr', + 'nrkbqnbr', + 'nrkqnbbr', + 'nrkqnrbb', + 'bbnrknqr', + 'bnrbknqr', + 'bnrknbqr', + 'bnrknqrb', + 'nbbrknqr', + 'nrbbknqr', + 'nrbknbqr', + 'nrbknqrb', + 'nbrkbnqr', + 'nrkbbnqr', + 'nrknbbqr', + 'nrknbqrb', + 'nbrknqbr', + 'nrkbnqbr', + 'nrknqbbr', + 'nrknqrbb', + 'bbnrknrq', + 'bnrbknrq', + 'bnrknbrq', + 'bnrknrqb', + 'nbbrknrq', + 'nrbbknrq', + 'nrbknbrq', + 'nrbknrqb', + 'nbrkbnrq', + 'nrkbbnrq', + 'nrknbbrq', + 'nrknbrqb', + 'nbrknrbq', + 'nrkbnrbq', + 'nrknrbbq', + 'nrknrqbb', + 'bbqnrkrn', + 'bqnbrkrn', + 'bqnrkbrn', + 'bqnrkrnb', + 'qbbnrkrn', + 'qnbbrkrn', + 'qnbrkbrn', + 'qnbrkrnb', + 'qbnrbkrn', + 'qnrbbkrn', + 'qnrkbbrn', + 'qnrkbrnb', + 'qbnrkrbn', + 'qnrbkrbn', + 'qnrkrbbn', + 'qnrkrnbb', + 'bbnqrkrn', + 'bnqbrkrn', + 'bnqrkbrn', + 'bnqrkrnb', + 'nbbqrkrn', + 'nqbbrkrn', + 'nqbrkbrn', + 'nqbrkrnb', + 'nbqrbkrn', + 'nqrbbkrn', + 'nqrkbbrn', + 'nqrkbrnb', + 'nbqrkrbn', + 'nqrbkrbn', + 'nqrkrbbn', + 'nqrkrnbb', + 'bbnrqkrn', + 'bnrbqkrn', + 'bnrqkbrn', + 'bnrqkrnb', + 'nbbrqkrn', + 'nrbbqkrn', + 'nrbqkbrn', + 'nrbqkrnb', + 'nbrqbkrn', + 'nrqbbkrn', + 'nrqkbbrn', + 'nrqkbrnb', + 'nbrqkrbn', + 'nrqbkrbn', + 'nrqkrbbn', + 'nrqkrnbb', + 'bbnrkqrn', + 'bnrbkqrn', + 'bnrkqbrn', + 'bnrkqrnb', + 'nbbrkqrn', + 'nrbbkqrn', + 'nrbkqbrn', + 'nrbkqrnb', + 'nbrkbqrn', + 'nrkbbqrn', + 'nrkqbbrn', + 'nrkqbrnb', + 'nbrkqrbn', + 'nrkbqrbn', + 'nrkqrbbn', + 'nrkqrnbb', + 'bbnrkrqn', + 'bnrbkrqn', + 'bnrkrbqn', + 'bnrkrqnb', + 'nbbrkrqn', + 'nrbbkrqn', + 'nrbkrbqn', + 'nrbkrqnb', + 'nbrkbrqn', + 'nrkbbrqn', + 'nrkrbbqn', + 'nrkrbqnb', + 'nbrkrqbn', + 'nrkbrqbn', + 'nrkrqbbn', + 'nrkrqnbb', + 'bbnrkrnq', + 'bnrbkrnq', + 'bnrkrbnq', + 'bnrkrnqb', + 'nbbrkrnq', + 'nrbbkrnq', + 'nrbkrbnq', + 'nrbkrnqb', + 'nbrkbrnq', + 'nrkbbrnq', + 'nrkrbbnq', + 'nrkrbnqb', + 'nbrkrnbq', + 'nrkbrnbq', + 'nrkrnbbq', + 'nrkrnqbb', + 'bbqrnnkr', + 'bqrbnnkr', + 'bqrnnbkr', + 'bqrnnkrb', + 'qbbrnnkr', + 'qrbbnnkr', + 'qrbnnbkr', + 'qrbnnkrb', + 'qbrnbnkr', + 'qrnbbnkr', + 'qrnnbbkr', + 'qrnnbkrb', + 'qbrnnkbr', + 'qrnbnkbr', + 'qrnnkbbr', + 'qrnnkrbb', + 'bbrqnnkr', + 'brqbnnkr', + 'brqnnbkr', + 'brqnnkrb', + 'rbbqnnkr', + 'rqbbnnkr', + 'rqbnnbkr', + 'rqbnnkrb', + 'rbqnbnkr', + 'rqnbbnkr', + 'rqnnbbkr', + 'rqnnbkrb', + 'rbqnnkbr', + 'rqnbnkbr', + 'rqnnkbbr', + 'rqnnkrbb', + 'bbrnqnkr', + 'brnbqnkr', + 'brnqnbkr', + 'brnqnkrb', + 'rbbnqnkr', + 'rnbbqnkr', + 'rnbqnbkr', + 'rnbqnkrb', + 'rbnqbnkr', + 'rnqbbnkr', + 'rnqnbbkr', + 'rnqnbkrb', + 'rbnqnkbr', + 'rnqbnkbr', + 'rnqnkbbr', + 'rnqnkrbb', + 'bbrnnqkr', + 'brnbnqkr', + 'brnnqbkr', + 'brnnqkrb', + 'rbbnnqkr', + 'rnbbnqkr', + 'rnbnqbkr', + 'rnbnqkrb', + 'rbnnbqkr', + 'rnnbbqkr', + 'rnnqbbkr', + 'rnnqbkrb', + 'rbnnqkbr', + 'rnnbqkbr', + 'rnnqkbbr', + 'rnnqkrbb', + 'bbrnnkqr', + 'brnbnkqr', + 'brnnkbqr', + 'brnnkqrb', + 'rbbnnkqr', + 'rnbbnkqr', + 'rnbnkbqr', + 'rnbnkqrb', + 'rbnnbkqr', + 'rnnbbkqr', + 'rnnkbbqr', + 'rnnkbqrb', + 'rbnnkqbr', + 'rnnbkqbr', + 'rnnkqbbr', + 'rnnkqrbb', + 'bbrnnkrq', + 'brnbnkrq', + 'brnnkbrq', + 'brnnkrqb', + 'rbbnnkrq', + 'rnbbnkrq', + 'rnbnkbrq', + 'rnbnkrqb', + 'rbnnbkrq', + 'rnnbbkrq', + 'rnnkbbrq', + 'rnnkbrqb', + 'rbnnkrbq', + 'rnnbkrbq', + 'rnnkrbbq', + 'rnnkrqbb', + 'bbqrnknr', + 'bqrbnknr', + 'bqrnkbnr', + 'bqrnknrb', + 'qbbrnknr', + 'qrbbnknr', + 'qrbnkbnr', + 'qrbnknrb', + 'qbrnbknr', + 'qrnbbknr', + 'qrnkbbnr', + 'qrnkbnrb', + 'qbrnknbr', + 'qrnbknbr', + 'qrnknbbr', + 'qrnknrbb', + 'bbrqnknr', + 'brqbnknr', + 'brqnkbnr', + 'brqnknrb', + 'rbbqnknr', + 'rqbbnknr', + 'rqbnkbnr', + 'rqbnknrb', + 'rbqnbknr', + 'rqnbbknr', + 'rqnkbbnr', + 'rqnkbnrb', + 'rbqnknbr', + 'rqnbknbr', + 'rqnknbbr', + 'rqnknrbb', + 'bbrnqknr', + 'brnbqknr', + 'brnqkbnr', + 'brnqknrb', + 'rbbnqknr', + 'rnbbqknr', + 'rnbqkbnr', + 'rnbqknrb', + 'rbnqbknr', + 'rnqbbknr', + 'rnqkbbnr', + 'rnqkbnrb', + 'rbnqknbr', + 'rnqbknbr', + 'rnqknbbr', + 'rnqknrbb', + 'bbrnkqnr', + 'brnbkqnr', + 'brnkqbnr', + 'brnkqnrb', + 'rbbnkqnr', + 'rnbbkqnr', + 'rnbkqbnr', + 'rnbkqnrb', + 'rbnkbqnr', + 'rnkbbqnr', + 'rnkqbbnr', + 'rnkqbnrb', + 'rbnkqnbr', + 'rnkbqnbr', + 'rnkqnbbr', + 'rnkqnrbb', + 'bbrnknqr', + 'brnbknqr', + 'brnknbqr', + 'brnknqrb', + 'rbbnknqr', + 'rnbbknqr', + 'rnbknbqr', + 'rnbknqrb', + 'rbnkbnqr', + 'rnkbbnqr', + 'rnknbbqr', + 'rnknbqrb', + 'rbnknqbr', + 'rnkbnqbr', + 'rnknqbbr', + 'rnknqrbb', + 'bbrnknrq', + 'brnbknrq', + 'brnknbrq', + 'brnknrqb', + 'rbbnknrq', + 'rnbbknrq', + 'rnbknbrq', + 'rnbknrqb', + 'rbnkbnrq', + 'rnkbbnrq', + 'rnknbbrq', + 'rnknbrqb', + 'rbnknrbq', + 'rnkbnrbq', + 'rnknrbbq', + 'rnknrqbb', + 'bbqrnkrn', + 'bqrbnkrn', + 'bqrnkbrn', + 'bqrnkrnb', + 'qbbrnkrn', + 'qrbbnkrn', + 'qrbnkbrn', + 'qrbnkrnb', + 'qbrnbkrn', + 'qrnbbkrn', + 'qrnkbbrn', + 'qrnkbrnb', + 'qbrnkrbn', + 'qrnbkrbn', + 'qrnkrbbn', + 'qrnkrnbb', + 'bbrqnkrn', + 'brqbnkrn', + 'brqnkbrn', + 'brqnkrnb', + 'rbbqnkrn', + 'rqbbnkrn', + 'rqbnkbrn', + 'rqbnkrnb', + 'rbqnbkrn', + 'rqnbbkrn', + 'rqnkbbrn', + 'rqnkbrnb', + 'rbqnkrbn', + 'rqnbkrbn', + 'rqnkrbbn', + 'rqnkrnbb', + 'bbrnqkrn', + 'brnbqkrn', + 'brnqkbrn', + 'brnqkrnb', + 'rbbnqkrn', + 'rnbbqkrn', + 'rnbqkbrn', + 'rnbqkrnb', + 'rbnqbkrn', + 'rnqbbkrn', + 'rnqkbbrn', + 'rnqkbrnb', + 'rbnqkrbn', + 'rnqbkrbn', + 'rnqkrbbn', + 'rnqkrnbb', + 'bbrnkqrn', + 'brnbkqrn', + 'brnkqbrn', + 'brnkqrnb', + 'rbbnkqrn', + 'rnbbkqrn', + 'rnbkqbrn', + 'rnbkqrnb', + 'rbnkbqrn', + 'rnkbbqrn', + 'rnkqbbrn', + 'rnkqbrnb', + 'rbnkqrbn', + 'rnkbqrbn', + 'rnkqrbbn', + 'rnkqrnbb', + 'bbrnkrqn', + 'brnbkrqn', + 'brnkrbqn', + 'brnkrqnb', + 'rbbnkrqn', + 'rnbbkrqn', + 'rnbkrbqn', + 'rnbkrqnb', + 'rbnkbrqn', + 'rnkbbrqn', + 'rnkrbbqn', + 'rnkrbqnb', + 'rbnkrqbn', + 'rnkbrqbn', + 'rnkrqbbn', + 'rnkrqnbb', + 'bbrnkrnq', + 'brnbkrnq', + 'brnkrbnq', + 'brnkrnqb', + 'rbbnkrnq', + 'rnbbkrnq', + 'rnbkrbnq', + 'rnbkrnqb', + 'rbnkbrnq', + 'rnkbbrnq', + 'rnkrbbnq', + 'rnkrbnqb', + 'rbnkrnbq', + 'rnkbrnbq', + 'rnkrnbbq', + 'rnkrnqbb', + 'bbqrknnr', + 'bqrbknnr', + 'bqrknbnr', + 'bqrknnrb', + 'qbbrknnr', + 'qrbbknnr', + 'qrbknbnr', + 'qrbknnrb', + 'qbrkbnnr', + 'qrkbbnnr', + 'qrknbbnr', + 'qrknbnrb', + 'qbrknnbr', + 'qrkbnnbr', + 'qrknnbbr', + 'qrknnrbb', + 'bbrqknnr', + 'brqbknnr', + 'brqknbnr', + 'brqknnrb', + 'rbbqknnr', + 'rqbbknnr', + 'rqbknbnr', + 'rqbknnrb', + 'rbqkbnnr', + 'rqkbbnnr', + 'rqknbbnr', + 'rqknbnrb', + 'rbqknnbr', + 'rqkbnnbr', + 'rqknnbbr', + 'rqknnrbb', + 'bbrkqnnr', + 'brkbqnnr', + 'brkqnbnr', + 'brkqnnrb', + 'rbbkqnnr', + 'rkbbqnnr', + 'rkbqnbnr', + 'rkbqnnrb', + 'rbkqbnnr', + 'rkqbbnnr', + 'rkqnbbnr', + 'rkqnbnrb', + 'rbkqnnbr', + 'rkqbnnbr', + 'rkqnnbbr', + 'rkqnnrbb', + 'bbrknqnr', + 'brkbnqnr', + 'brknqbnr', + 'brknqnrb', + 'rbbknqnr', + 'rkbbnqnr', + 'rkbnqbnr', + 'rkbnqnrb', + 'rbknbqnr', + 'rknbbqnr', + 'rknqbbnr', + 'rknqbnrb', + 'rbknqnbr', + 'rknbqnbr', + 'rknqnbbr', + 'rknqnrbb', + 'bbrknnqr', + 'brkbnnqr', + 'brknnbqr', + 'brknnqrb', + 'rbbknnqr', + 'rkbbnnqr', + 'rkbnnbqr', + 'rkbnnqrb', + 'rbknbnqr', + 'rknbbnqr', + 'rknnbbqr', + 'rknnbqrb', + 'rbknnqbr', + 'rknbnqbr', + 'rknnqbbr', + 'rknnqrbb', + 'bbrknnrq', + 'brkbnnrq', + 'brknnbrq', + 'brknnrqb', + 'rbbknnrq', + 'rkbbnnrq', + 'rkbnnbrq', + 'rkbnnrqb', + 'rbknbnrq', + 'rknbbnrq', + 'rknnbbrq', + 'rknnbrqb', + 'rbknnrbq', + 'rknbnrbq', + 'rknnrbbq', + 'rknnrqbb', + 'bbqrknrn', + 'bqrbknrn', + 'bqrknbrn', + 'bqrknrnb', + 'qbbrknrn', + 'qrbbknrn', + 'qrbknbrn', + 'qrbknrnb', + 'qbrkbnrn', + 'qrkbbnrn', + 'qrknbbrn', + 'qrknbrnb', + 'qbrknrbn', + 'qrkbnrbn', + 'qrknrbbn', + 'qrknrnbb', + 'bbrqknrn', + 'brqbknrn', + 'brqknbrn', + 'brqknrnb', + 'rbbqknrn', + 'rqbbknrn', + 'rqbknbrn', + 'rqbknrnb', + 'rbqkbnrn', + 'rqkbbnrn', + 'rqknbbrn', + 'rqknbrnb', + 'rbqknrbn', + 'rqkbnrbn', + 'rqknrbbn', + 'rqknrnbb', + 'bbrkqnrn', + 'brkbqnrn', + 'brkqnbrn', + 'brkqnrnb', + 'rbbkqnrn', + 'rkbbqnrn', + 'rkbqnbrn', + 'rkbqnrnb', + 'rbkqbnrn', + 'rkqbbnrn', + 'rkqnbbrn', + 'rkqnbrnb', + 'rbkqnrbn', + 'rkqbnrbn', + 'rkqnrbbn', + 'rkqnrnbb', + 'bbrknqrn', + 'brkbnqrn', + 'brknqbrn', + 'brknqrnb', + 'rbbknqrn', + 'rkbbnqrn', + 'rkbnqbrn', + 'rkbnqrnb', + 'rbknbqrn', + 'rknbbqrn', + 'rknqbbrn', + 'rknqbrnb', + 'rbknqrbn', + 'rknbqrbn', + 'rknqrbbn', + 'rknqrnbb', + 'bbrknrqn', + 'brkbnrqn', + 'brknrbqn', + 'brknrqnb', + 'rbbknrqn', + 'rkbbnrqn', + 'rkbnrbqn', + 'rkbnrqnb', + 'rbknbrqn', + 'rknbbrqn', + 'rknrbbqn', + 'rknrbqnb', + 'rbknrqbn', + 'rknbrqbn', + 'rknrqbbn', + 'rknrqnbb', + 'bbrknrnq', + 'brkbnrnq', + 'brknrbnq', + 'brknrnqb', + 'rbbknrnq', + 'rkbbnrnq', + 'rkbnrbnq', + 'rkbnrnqb', + 'rbknbrnq', + 'rknbbrnq', + 'rknrbbnq', + 'rknrbnqb', + 'rbknrnbq', + 'rknbrnbq', + 'rknrnbbq', + 'rknrnqbb', + 'bbqrkrnn', + 'bqrbkrnn', + 'bqrkrbnn', + 'bqrkrnnb', + 'qbbrkrnn', + 'qrbbkrnn', + 'qrbkrbnn', + 'qrbkrnnb', + 'qbrkbrnn', + 'qrkbbrnn', + 'qrkrbbnn', + 'qrkrbnnb', + 'qbrkrnbn', + 'qrkbrnbn', + 'qrkrnbbn', + 'qrkrnnbb', + 'bbrqkrnn', + 'brqbkrnn', + 'brqkrbnn', + 'brqkrnnb', + 'rbbqkrnn', + 'rqbbkrnn', + 'rqbkrbnn', + 'rqbkrnnb', + 'rbqkbrnn', + 'rqkbbrnn', + 'rqkrbbnn', + 'rqkrbnnb', + 'rbqkrnbn', + 'rqkbrnbn', + 'rqkrnbbn', + 'rqkrnnbb', + 'bbrkqrnn', + 'brkbqrnn', + 'brkqrbnn', + 'brkqrnnb', + 'rbbkqrnn', + 'rkbbqrnn', + 'rkbqrbnn', + 'rkbqrnnb', + 'rbkqbrnn', + 'rkqbbrnn', + 'rkqrbbnn', + 'rkqrbnnb', + 'rbkqrnbn', + 'rkqbrnbn', + 'rkqrnbbn', + 'rkqrnnbb', + 'bbrkrqnn', + 'brkbrqnn', + 'brkrqbnn', + 'brkrqnnb', + 'rbbkrqnn', + 'rkbbrqnn', + 'rkbrqbnn', + 'rkbrqnnb', + 'rbkrbqnn', + 'rkrbbqnn', + 'rkrqbbnn', + 'rkrqbnnb', + 'rbkrqnbn', + 'rkrbqnbn', + 'rkrqnbbn', + 'rkrqnnbb', + 'bbrkrnqn', + 'brkbrnqn', + 'brkrnbqn', + 'brkrnqnb', + 'rbbkrnqn', + 'rkbbrnqn', + 'rkbrnbqn', + 'rkbrnqnb', + 'rbkrbnqn', + 'rkrbbnqn', + 'rkrnbbqn', + 'rkrnbqnb', + 'rbkrnqbn', + 'rkrbnqbn', + 'rkrnqbbn', + 'rkrnqnbb', + 'bbrkrnnq', + 'brkbrnnq', + 'brkrnbnq', + 'brkrnnqb', + 'rbbkrnnq', + 'rkbbrnnq', + 'rkbrnbnq', + 'rkbrnnqb', + 'rbkrbnnq', + 'rkrbbnnq', + 'rkrnbbnq', + 'rkrnbnqb', + 'rbkrnnbq', + 'rkrbnnbq', + 'rkrnnbbq', + 'rkrnnqbb', +]; From 4021fff0837c73074ab7c2bf15e8a6c3a44123ba Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Tue, 3 Sep 2024 23:35:41 +0200 Subject: [PATCH 210/979] fix: keep zen mode after game finishes Closes #964 --- lib/src/model/game/game_controller.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/model/game/game_controller.dart b/lib/src/model/game/game_controller.dart index 4f0145c54a..0c193c361e 100644 --- a/lib/src/model/game/game_controller.dart +++ b/lib/src/model/game/game_controller.dart @@ -986,7 +986,8 @@ class GameState with _$GameState { }) = _GameState; /// Whether the zen mode is active - bool get isZenModeActive => game.playable && isZenModeEnabled; + bool get isZenModeActive => + game.playable ? isZenModeEnabled : game.prefs?.zenMode == Zen.yes; /// Whether zen mode is enabled by account preference or local game setting bool get isZenModeEnabled => From 3a6a4ceef093bf34e692dffd38b82f144c83c4ab Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 29 Aug 2024 14:26:28 +0200 Subject: [PATCH 211/979] Tweak editor screen --- lib/src/view/board_editor/board_editor_screen.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/view/board_editor/board_editor_screen.dart b/lib/src/view/board_editor/board_editor_screen.dart index cded0f7f0b..7d62fada35 100644 --- a/lib/src/view/board_editor/board_editor_screen.dart +++ b/lib/src/view/board_editor/board_editor_screen.dart @@ -103,7 +103,8 @@ class _Body extends ConsumerWidget { initialFen: initialFen, orientation: boardEditorState.orientation, isTablet: isTablet, - pieces: boardEditorState.pieces.unlock, + // unlockView is safe because chessground will never modify the pieces + pieces: boardEditorState.pieces.unlockView, ), _PieceMenu( boardSize, From 44103a7ddd930583ef4d7d83d62ece077d510a53 Mon Sep 17 00:00:00 2001 From: inc0g_ Date: Mon, 5 Aug 2024 14:21:16 +1000 Subject: [PATCH 212/979] Local Notification Service: Initial implimentation --- android/app/build.gradle | 3 + android/app/src/main/AndroidManifest.xml | 24 +++-- android/app/src/main/res/drawable/cross.png | Bin 0 -> 422 bytes .../app/src/main/res/drawable/logo_black.png | Bin 0 -> 34782 bytes android/app/src/main/res/drawable/tick.png | Bin 0 -> 412 bytes ios/Runner/AppDelegate.swift | 10 ++ lib/src/app.dart | 3 + .../local_notification_service.dart | 95 ++++++++++++++++++ pubspec.lock | 54 ++++++++-- pubspec.yaml | 1 + 10 files changed, 168 insertions(+), 22 deletions(-) create mode 100644 android/app/src/main/res/drawable/cross.png create mode 100644 android/app/src/main/res/drawable/logo_black.png create mode 100644 android/app/src/main/res/drawable/tick.png create mode 100644 lib/src/model/notifications/local_notification_service.dart diff --git a/android/app/build.gradle b/android/app/build.gradle index 860c8c35d0..ff93ebd5f9 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -36,6 +36,7 @@ android { ndkVersion flutter.ndkVersion compileOptions { + coreLibraryDesugaringEnabled true sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } @@ -49,6 +50,7 @@ android { } defaultConfig { + multiDexEnabled true applicationId "org.lichess.mobileV2" minSdkVersion 26 targetSdkVersion flutter.targetSdkVersion @@ -96,4 +98,5 @@ flutter { } dependencies { + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.2.2' } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index f21c4f27c7..01d13313d0 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,13 +1,13 @@ + android:name="android.permission.INTERNET" /> + android:name="android.intent.action.VIEW" /> + android:scheme="https" /> - + + android:resource="@style/NormalTheme" /> - - + + + android:value="2" /> + - + \ No newline at end of file diff --git a/android/app/src/main/res/drawable/cross.png b/android/app/src/main/res/drawable/cross.png new file mode 100644 index 0000000000000000000000000000000000000000..a0ae4179baf383b374dc4827639f6feeaccdcac7 GIT binary patch literal 422 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I3?%1nZ+ru!0t0+PT!Hj?1A~)hW@n9zAdEA{ z#-~k8fFw`^NFp;JTs-9e|Nl!i_T>T%l`IMJ3uaJIR8dtkGBdYWvTXUvRjXgTeD&t- zyZ0YHu5~}i2UNtEEaj?aro^tN3kXY9+$wtH5x$zr|g##@)UoZ)-*(++cCZNA_==A~*586r(@AvR z##yW-9)fXoUvw6@H+}e)oWLl4q2*-1HusIY9*54@zxkPL;nQ+aP`)`@o25zNlQ7G| z|IG^D^f|cdx4cwj@Rz+>TX U>vDP&H_)dHp00i_>zopr0C?!F*#H0l literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable/logo_black.png b/android/app/src/main/res/drawable/logo_black.png new file mode 100644 index 0000000000000000000000000000000000000000..b844aa25469200a90327948b98fbbc32cc57ffa8 GIT binary patch literal 34782 zcmXs!2{@G9_wQJeG*m+NuZ=7rG4`|~M79~bLH2dh*q5ok^x414R<_DMgBfHU%Sb3$ zipIW#kg*PpWtf@&>-#zks{9Z&-$otxSg0yLs5`uJ;>xPJ8V=_PZ{0)zEtn-Zs!h5ww6 zIcM!PaN6vIS^Qt;{xPWh>(bv-cf#>!4P=*Ace*qm-&LMZZSEh9!0)$IENWgoYwWJO zY!fi<)4W~;5*~6-p~RB~(q}g_c4^M!QSva34<1l4W#;e~nh*`U3jqK^A~#b&;iR3( zCZAnL-rh6HFgCH)Kun;5^{1i>EN0;4o5I!NQ?I(_N-E@&RiD z$|UMkho>8H2!d0Xd5zgquMRG|t_w3GoUA7+>4=OI@`t#d>fmrzi1K6yI?XCwYA>RX zx=7y~Sk&mhc!UR>R+4%yfA1f>twWSd#n^^F^L_H%x?S^^!skmPHlPNOw^p4Th>b%7 z{XQy@xR`H8gHXCWfRaw`Ax&DPQM=mi@R-ZTm~zUc(!IEKOMOH3!yRD4DxJ`b!g|lD z7@9YtbhLs;RM4<5<5;FP9-JkOtvEQ;HMR+&<|9XIMY2Q$@Ajo0BY-o2s8rUgxW6N~Rq z`afNQ143h1735~@*SlYCZg2p`ugPWru+a97oRcBNsKal+eEIC5dHb@ckO8N0iZr+; z(+YuKYf)KhMYSt1<%{?-51t(JV5x6my-%L#{BtN#p*X1W7LS z(KG_(9T_svcCpFy7>NzUyye59w2{wamkqeasSp_q&6y7zm0F^c^V6kR5qG(?0-e#; zv|FW^rLDs&=z|VyKhI1lsZbCvBMIGOwjCm=USGsIm9LqtN{N?!+%E(UWv7${Lxfc1 z!Y6NQf;%{rkhT22YSTEYo?@_sya{g1S{DZ40FV(C);i%b+Znu4zq!b61(2ctL^JXx z%hI%L&1Ut2xbfpI)_-I~9|WQCl)L1l!lyD@ySj(!N3u&XLn@;2jmeJ)CZHaddnCZo zx!m39;&44QbO|Y*<0yVVM^1nceW+!nYgt&LWk0q;PTUv6_xgY0F4CpOxTAHJknCZs z|J070O#x+Wn*%OLSeAfh?K>FBW}aL`aKVcWIm;AHK??FkqE`Lo{xSkRnf;ju?VTg? zpyGJuH_13rn9bEa4?ztkAMBq)9SgR%k=NlV6~Qlf-AA|z09dRJIJz+2N5;c^H$pIv zl(>@1TFyfWSHwZvwzaf~6LNd1yU?X(%yga`VF$O!6dT>hWfAW&UtgVKJ%|3{2JhBh zr4Q}z*e*s$2JgZ*l=*-19cghYVU}s?$%67Oi@gUx0Qd13auD{pA!pFqFUngATPY^# zP`#MZ4;D00GN?V96Ou}M&y(f_4hMshD0r->5hu^5BN#&4f8x>#F3mNMn)oHj1CRn4s18MK^&mC&eOAR}M!}{`VPv=rT*f zPcatPRT4kA^W}gHiVd0T9+o)8MsTTU z4}}|`J<+cVog}ONEZ@qAjv5)bkh>?TCjQ|xXyXs>dyxMQrG)a(2xWlYg-LA_4TVU& z%}&RVun~?Em@A`UUdk~)1YaGT$9)aBGNWtx29R3dg7OrT5|954AkD<=9h|++#eBKT zG&6d|$|>MRPKF*Fo@^3U94>C*5Vo{<2UXbQ!3Ww(kQ&Ke_RLcJb(WuPF&X$g`k zy8enSQ+yj#cm9f2P4jkXuFo zBTt}!^ARo-KxmGh&7{!DwAx*-x+tJ_@c=xtr$I_HSt3Af_KH)O+$#p7;0Q*5F8QBD zBl(UrC4Ff*zYSz^tp6uo*ypq4d)O>1T|0;22a*^WI_5NKrIHIExzw&A%KdF8BfVy0 zRJ|ZbadE3CI}v1UC>wacRvjnb&Wtm%F5lbbOBV!SpVj`SG9IW$tQ~+*?`0SzveCTw_+R&e45_h2dWo)DDC7J#SVS!U^*4N<>-+oi=Es6 zs2YslNOfa}AU|^%CN{`LT2XJMd(!aaRz(Iq+v-4r&w4{UXJVs~{SeeUBQ}3m;myW$ z{g~Auqbb;!iYXh0K6*fe)ijBVxE$Jd&D!DZBhcoPto^Xj?p{?fS+N28JL!U$*MD*- zdDuEj7I~wWRWMOG7SOh}$T)PmN}3UBzMGGVDShM_b2g>wmU@+$7^C)lhLFip>lW$c zuYrA*Qac#QkUoi3tBke=(SkJI@FCNq-i15R7AQf>8`I;(^cyu@Y>R)gy;%xTn-Hb* zpt&G~6IHbPV!ju!uprt&6YLadCfa$K%S#eG*Y@!AzrP$Bck2sWh`Nfk#?$*o+$3Y- zTY5i5m7ImW3j4iIeS7j3J*pcDp6MRqGI-9`?HzRW|3G@4_QPWaH!@jN(Ss0;dT0KT zJ3A0JF3|fd^cGR{$Whn`1S0hmrXdhc+aVi?!nU#mcDzFUd|xyLPP@17ux@I?tZ%hPFMVf_Z@Ma8ZIVf`4R_85hMSLr|koyo@ zn?HF~q-Ckq4*eDc%fZ7HJx4~?6E)}&y2uN|U7(P;Kp_*}!Vq;O5{)X<5PL~dB;A@e zLFpy$1#O44deferw9n@k$t@w?Z}{j7dTN4aa=C=PPJyPOCa^H@j7-EXTCKmH>!*@l z*yHDn4~-ZpT!Y}e$b7DvoRkg-k{igns*!W7O3qzvR~ul1d8mR+td3~#T1+5%Kem?v zWSSNuVr-gA=-W>si$wz(|4?}_Pw;^y0v z4t8L0st?LbiGABYw2AeC3@v0wI%nJKw9cSd7W$j7LrPl^;@VI8!*8^^Fk}qH7oJK3 zn50Kf3-7i<8Gzu|C1?s=7mtdZYb_j4da&t4)0JW!YBOTHT;je?B z$zuZ$G54FRW^!T_)*O?3sN?_XP47Wo?A(6teTRFRfZ)(wFjf#kc--xR=>$H=+fmOD zTSy6@VA3>;!tH5alYH{eV=6U>^lC~9mcP)I$ct;STUB}4A7O|_q8}UaZFTU20rO-* zxHmc@wu=d|HWsqP2aJrevIZ*UcSOJcYK@&^{de#P7=I_7^2RhvK)z|m%Czj7lz`kO z=0(&LU;Qso&emdCqOhyswqQsGScf{{a?P9X%EDKUK~29T#r-EB0uWp-QKfRUDgF_N z!a`(<8iQ)rVwFv>iww8eE?P>WMnN$uwQgf2Y;;N+vk#%Ra0ts*yPUtoeUxh_S-Lhk zGgY8N27@bWhAnNGd}9Ejbc{7b__=RWYF+JPIV>^ivHf3+xzKYGnx(DNIA*5_=*=V; z`LVPLTwEm7KlVpvf92isd3BmpVqfI!+NzHr-sK`)%C==V(q2;Rmok5=)4!6_^!~h7 zv==c7_2XDbl?ISvy#C+Qa7K1??-!XNqv3Rri^OptAnX?(72bI}aSugJBS!aRR4 zF~vGOBgaRp!+mLX&=dx{60ZkB5z8cK-qS2vODE5OFktp}2s2U+a$cmq*#xsfflY|V3Jz`Dh;?E1S6x<3aF#mty236DnelmchccyPmT)4s}R(WM*b z86d0AISF@X?66X^KBX{q@IsOSNs z%#swVSvX@ioG|-?^6PSR41J(AXlh`+zXA+|Ot;GlS&a8xZl+_i{ms9pTMMewnXTg# z+|KCi4$&&YpSs^Hl6-Oe$42tn9bGlVz8^KH*n7IrX}$T5SYX`r`r>9lCLV5D#w_7;H3=qzI@(uq9ZkDAYA9|=G)v^5WZu1odal$!i z%jxnKi>QdD;b}YiqOSFu6_%1HqWlLZl;^Cj?VX6=K=jDK6H*)54foY|zIc1BnArlV zB*3V{SnGY`yn!z9zl2N)65=!R^GgIX&}&iQ-ajoZcOrdcT8O7xWO6QhC^%Hl8$aM1 zbGh;JA+#&ou|@NtOtMJZxi$}xWZJ%B;Z>ov=}^=DD)1D3Lb(u_6_(BQjn$S)4|V<3 zr#6j@bX#jxzO(e&>*$Mjzn6&Dc03_{H}YG&VuZ46&popByMQ80b}FGZY`&nxo@cCN z1XxFMg8o~MpilcjsoUI@n?o@NJ~4TW_!)|RSC&{6e+Y&|IHLa3d(s`$GxwnUS{`gG zHk#-xd2FHUiqY!OnJA)j8`S!QBLqW34TEWg&RYMu{b*V%d9ikPFD`bH>in{AshG#m zF6cTsi82yf%%u>ItG$vX5M2h^$Fmv!3Ob&j@)%JHht@ZJJ+m+l485OlKJ=z7Q7nw8 zOO!1sde6VFusez!=`dCKPmV!ekAJ?;Lj-7=LL*yezTk&d?4*qd$~%%&j}S0iKZ6HI zk5+7S%IUq@YyFY?(d~7+Lie_HM>h;wzZ_3^uo%lZ?sNI_QG3^=VT3o@DLs3fu)Dtx z<#F%9=h=AgcTzN=2~CG*Np#JgVbdN_qGW1PdToj@O4i4wz2y~J5j2#gv7xu2^<&TU;UMe=432Ob~gN|&Nb zdWGiE*-9Uak41Pn|1>1GF#g<5Fm?A}-4T_JX?7MmxDt*)>$?F79TI?&Fkh*owYa=M zPsyMN>@O{UnQI?ZyddzkE+~(OWLo^Q;K7roOgoC)p;Z!8m<~+e5-)fny!DjQN?+3R z%=_Y+4ai;DmZr-CB(}|CKv~}crIIXJb4A~Hj$wu7FGaHF*fSOf9)R_t)|@?v)Ew-5?t#;fu2Ape7_;#juw*ba zaNFn~T4Io>MAyM&Y)5g<#7n?pOO|0`YI%eZ3L2KLL30!VsQ&e~@V#(afDv^q_WWxg zvtklAhhymFKIg}|Vqc|l0Irq#?Q6`%`7EIF+Vo~?UHHTHldeEnYs6AY^r?OXFOb*) z#r7{BbfnI)u)}b^65haZh;Mz&H#NqEK5WI?3xM?lEw!fQnB-(9W0@9P9eiWh)I3+_ z#eLXHBpy%qI>@O4Q$2@5`oDdS#%Hft=-}wWb;6{c@JuGf9CZVIs}MpuH)_f7MmHuq z8V8d8yra~Kz|7c?+sziN8geLp;4o7(Yc^U`kUA|{9$l^3UCyb5;Po0wv~sGY)#FY< zkW-Vz&ipWOqP5XM0Ieq$VE~4JtytZ0|HyaFc^808EiOy^;Ohj{PZ?mjNq13(#`NHrvE54qMH+A6S7BP*YvA=vU{7h49&U;A&gvmH*FVuQ18s{24vn1 z!ABRg3x55_0^3^Vzg7!5lZ0Dh!Np<qLt#8^DLJxDwC3d)tQ6=`CIagpYd}| zlsAT47#ox^4JH0z#N>bgx+QcS52+-|EUz9L`g{O_6x4n@W9FYzJz^VKPw(&!sXIpc z?O@khUsPXyiVe(&9hq0X`<`6)QfIW)odSIOztUd&BnMbUT)LRacciPQ z+5wkgh3{Tl{h>j|;g+rHDyGH)**E~-uchV$cYS#4StwvUc#@2=K?|WDwomb81la+E zn|#A5CKpV~c`Zx;n>i1b#uOGuHYcPLdWraHjw$jsl0FKOS0xQ z+et0F5=2RuRUVLIkZs{$qXALkCW0pp|5QBSAObB~#|H2Ri$<+{n!VS}jj3t0r#a_P zF3lZK{K)-{P$2Vt%)p|FynjVDU|nv9kp69~FSH0-`5j|b2NrpL#*>BV({Z0rWT}gs zBtspP*SV6b;=`Xqg#sf zrMORVhW zhXYJ6^pz>#vw@PaQo2RX+{Z31(c~Zwu!8O3Xq0W)WUB!o1kiYMW^Z<7U?Udgk-);B z4sD))d}pz-n;Y4r;1s3zyq-X92SW}8Lvf78jsk;zdsa8~6iJ=oON$bp`6#ipGwU7W z#!GslUZiOxsr#b-)kC&38T)XL-+<8O_Z9>~Uj_vTev@2YmOjNAB(V5_^BeVraqoEp zD`(G>@1?2%JUy|PR|)!;q%j?}GIEOvXQoRfvY|}i%=3N9zVxZ-@@;byAi=AcGOtN| z7TF&oIS(a$9yt-HOg&!6K?n`TO%=SdD+#qje;7D|EU0kIan&X)sF!QnYe3|a*>O9| zKLw>)?oXQ7uwU=Ls*q`L@XVae`uKB1-azH$+u3G1)iYka05X(wO{?>$Px2Pl2jpZg zm`2Icta3DSSs$B1`~SVOrDmN z?oeDWI@?Z1O@9l_8SH~f*HvPz?IKn5kFocpak=SFgr%fd4V!fXgma22v6%+i2H?8q zTx!0v@3-Ep-(v2w}7xd#kZ-u!6%1qcaFCgV?L5rSCQ0@Bfs35ns9i&z3sOglzi^lgy%xj2Gu|ili0)!bycl)Y^ z-J3NDAd@Zk$WD~d1&Tw15P~S{0AzA=!F5&iY=REJ+Daz#7pqV+yCE(BUT0YgC0N>qM2#~3hh2s;&muWDsgEL*#Xyu@j#c)2e3P5SgKNSj* zu56>afuek%W5IP*i28*h+bsYr#LlG-OT?5X0#b|In&}PyAP^C<2q06A%ZlY8R$Zjt z+E@V|A)?c8X-{Of`J_`R8)+iEjN0>NZnFC*(l5Gj=BP$a_e!TeHa|@k=&9*7ifXLv z9h=<-WrdE`$QtN*Xn$V^KXwOPc(dNCCDg_z(%b6_Cq|$?1f6U0ph$ABHA9&b^K474 zQ^P0O$y^Wk8iJ!3=XCXSF5+C%WB~bsTew$=Aw#F9zZ88YNNL?|^R&m&hrI(zGw|B{&IYnY>LnoYqX~4qN|e3)+M$g# z1nferX#zPNJ=Z#e`7a&=@;8GNoj zn;Mnbc1tI*AXM9dL?g^vh*( zafz5abrG~^D6{8N1{Qb82tW$>2guhHsaH3GI0eZ?6jhP^*KuN6adpUq1-{48{pN709T90K2nPO+ zIfbaaIDrLeIeh^6F}S?Nv#h45J^>`sc7@ERO1WD3rKdUVJfL(jM0v{A;vk@=TA6KO zl&K-%4o}`AK6}Uk6s_bX5{FKwnBOEdjoC92Vk6tbiNwlnc8E-X;)0EC)V+&fiHcE}jpKB-IFgyuZdqh|| zsS4Vm59&4Tg&|wB-cVet$VO7aj^)1W9RT@sqBqp{9`)fxj-IFNSVf-?YAVczF$Wno zK(xp4J}w2cq_Ss`>8qB_7EhbsQL!s8AV5#7E4DhZA1=;%|2M{FyGw)uP15yK4+HfE zKX`m)?9FVg;nB=HFvj@EuEWKnK+jtpT)-Jzx#qQ8#ZZiJ5Y3lf_kyZrcml|D-cfe6 zv(UcH4XOTE6eB7mWSN3^0+7T)pI44!=fJ@U9}x9F)wM2ZMmtfsow0-2Gx1Q+)}bX_`&)bYbkVIfohb}=PO4!~r0L&243 z)unq$5~&lm9^)#-;)90+pr5Rr`Jy-1MDFMXnUL@=tDO4xOKY_g1aoIAa<0h=bkVup zoSARfaRHuz!;F&F=Q28A)qK?=sw*^rj8eSFVVtnT9M)ARktqBFQrSkjwpeu`a1LoF zl%P}lPOQeAYEc0AUoSuaQ?gf*G@w-Mqk=I3Ho3 z3Qd!eZHt?UZ6@Xfg>4+Oe1;J-)HR3|GGqsoPDxvGsrtftyH{#HoY?R$1*B%ytEw7v zsrrWC@^=jEToNZjz{64>RCc*QB8FfJ3u)5Q)E^*jgKIbd|=nQJDfdz3?LNhUe&Wl zPu$o8al>fDO9v1N6CL-O&2K9k4>GE3-}6 zwZA4Pd|N7Fgb{zjlVCG@?P&cyVEmy$yKAAy*QL(a(3DL!U$0?G4$c=bm1=Pt)3Gho z`7bWsLe!B9s83b%zECW&XtiVM4aU{pNvmHI3Qt%g5B-gN12**aCPZQBnn)KquIrL2 zQf>|QczgZ{WyqXco}e#kR3Y0}UQrZ@Nf67{XAPgvE&l;-+mg}$_tlp&wU!E6tC|6Y_dvF}S2Wi)$nYl~{|E9V?=oDpHi` z`O&YBzu>fWAYP@#-YW8flR;aF`28qTS9S8POM8(jFuvKwU8fauxsdckt3dO+dXN-b(xKo>D)|s4adK!6;NQ?tMnSRV}JUaIJguF>mj(7pLCO z1IloEr$bp!0D{YqJ+Vu!YEz?kSN!4;`N6KDZ$~EeZ6BZ96Lsyk>d3n!e&+eOm5}G3 zB0+rr8kV#vc0Wh$S@7?`Qck^J1j+-~ZvtI5WPvn3`C1)O81vijd+VNQI;p}0BLORwQc5L;#BDglMo8CVwFG6GPJ)xX&MK?DJ3DQ zDrCz^|1|fFJhDQm6|yJHBUF4X5Bd z;C=LySU*|n(8>)+Szo{DyL*&+U3k&bMoCKv!xR_B5jbB~&qd$DYnj5YfE~?u%#O*tZ7qT!C{3FD}|h z1R1^hhTQMvM-Snf=PdhHcKoBJ5v^j(gooh_txNixX;A(q#J!}nZzwr7!2UZDWx zry}jhr4UA1n|+K96ow59vU8qcaB{k8V094c?x6W_T>~Js#{`z$9(L zT_**)@?=Fb#dpY;?x|e+7WjsZ^dVdOS?%(Hw{$7z$h&+|7>CN5L-b*)yF}_7&!;!) zdAXp>(VT9a#^3v(c;S2bP%VRSNc#Ni&Stsf7)uy|8m;fD@_l1USIO_WNhNe^^*CvB z<@+PYF55r#uLm9{1&zjrTvOG16_+6+gA^Cj=?GrY8{i@MhNv+e$hfMkR9@digy-K< zf_Yzri%InQ?$hH{S_X=w_@+tq&<>Q78~gIQH@QLNZQQ5VU?_5$V3M5U60v=BiPQRp z9B8SN;xZ(i%7ZO5eG7WeAN2HljTpq&MA)~3eH6>iZ~S)B3WSh%T5K6{DrKL)lU>2y znf~7DP4&{yrw`eNM$yG1MU1C^*vdw7$!bXnh|{`d+XzkH)Jh%5hFC`AxfVwqTM-Nz z)+-Gpzi`9|Xyu2$2NAWA2MZuY>y9?4?AzH+lja%_(Vs;YzT+mZ95GfA&#!F8?|g4; zs(QRM+CXpWbU#OZ-py@I;>7Ua&KI6RT2Y}4^09g!anGM^HgudJcsYUUTYAr zmB>`)h;_~489DqVg^~)qFLHCcq^11D>(BXy!odI9G>VlZPs4a3P8xpi$-2K5sC! zhK(>S{FhCB3_;39iAgz#(|P5=<}fAU{=pLCUu6UBX{VIfgB_m3)@28d6QsJ}i)U`( zFG-a8B3F5RLhUW{t7_}(%-P&Of8a`MmF)n0wj*7C3v}#Yn^U1>y?LI-Lt=OfJyN0u zs(W?HuHX!^ciZ934-r#E-Dt!piQ*tN%`|nnxOu&hqbF0*xM5YRX6()z$k58!&Oy2O z!OgMD%Sba*NT_$a*@dzxSJ4(P_RNc2rhEO<#f>ji*vm$ju{(zCRjUp<79z;`WKZ?j zPlmIUxq!5jO8Jhhyna8|?_MR{wkK*1kA#n+{M)7Cl~faUIY<^b9s4n>eMj&v0q#Gj z;QM<$QH9<3lLuBeBi3!aAv{@ywCPZ*c@_8ECr(S|JTjIs1ZMAuG;d7Y2;P}Gq#m({ z@^NebAV0Y={B7QhTyfF{>}CKm>Wt#4Ge>#(cz$1R_GzxaS4g#^#E> zUnI`?nIJNK(m!YHlj&Vm(kEYR^3^FvGi~cp(sr})t;(YMtW#_Yq_p8mho(1=*o}M8 zxUIy74Dso{7I(rueYl6uCAfG@@!uGY$}P>>omRO?fS+bQwD($%MWP<7mTrvyC zYju+(wimg)A0IGC_%ToGGjG~qame(8>h~h%%4qY$Pl)!>mwuaEY=hrVCVhNmE z7wF(gJ8nO*?sWz(zV1Jg%8AU?4iF#z5|iqWi5s&A)9}bqR4_WR>T#PRG=<U1O|E0qJrRt?pAPgvz^Xmt zeJZ>0k1iYn)ZS!WW2K5xFwttdH~y#h?5)v}W0_7|ySyY^F$ zyY3i3+px3Q6x(jD%pfPb#oDd2=DbR%lq)bNvk8kYN^mVJr-7qovbn{PV7y#}_Se!q zI}h;JHPADudsTA@Vds)wH0B*aT2XsrllUPElY_9g(B`BM?8XyGxUHnY4Dl2z<-`|h zAITk)X}pBL{97S@zqJa)k+RG&^{7$Q&6>QI&c ztiOfhPa4h;k12U@=2ZxB2_dFik!-F>da_49@}W2&Js4BEtLnP#)+J1y2lo=_r*?py z{bI;veLcX_HJ7bE_Xo&^30XR6JVP9BuAKOI=P6tw?~DIClFL}hT>g|44X{8}$3BC* z?uvKqg@JZ2#1mbV71c3qIcW?sL-7_JX_+#hLyBetl4df*Sy+4P2mOJs@ajzXD8S=u zL1RF@ou*|NAez^d2Xnh_U5b~41;1B_s(;QJRYY12sie`&vj~qW`<);oe`iUfWKPd6ns$o(AlhM z6x#egG~p#gZ^2D;VvX5yIIXX;;^UUs+Qew(wNmaO%E1>K-erh;$j=`~8m~3A`K>RJ z%@o*4mIp;wm{oIy-Os{y*wi-TXKf6RoO!2{E;W{E9H0OqNu1JTU)^wU0LxSLNtG>}UV_u@lO(x#E*W9uCyDRA@}I_@2_rS9Jeyiq zcEjdBYSaJ83K^xo^L_>Lxw7FtS6`wVECUqHex`JFG8T@=7cDoSGQ@=})vT0(sQS@R z)~1L8C&^NqJQ6dCiUARXHDm>kQg^yJ-(sbE42TCz@9M7*B@_|vq{4K9N1|n%l(x3N zgynrrs>aF52xGiMp-6aga#RxtB~W*j%Q{&5MuQTc#IifBLWU&U?4dWdIEa%YiUAf> z_brTD$6>whnaz%u+H%z_7rCXI^QVi1>aEp3`N(FAD8r@FB%vzjSA&26MROrNTusV72N5+{k8G%|WRq@AB#!A8JLZFD1B|=Y$cP@E zbOF4DLeKlB*sGIozs3_~gfM>L-x6y^CCh??Z2+mYz6H*&*I)-nKvuSHt1VVd%Wxvc zw>?L8J=4e5rh{xik>&OBr3_hCw`_ESa#uMMe;zJuy zi209Y_3uO<+E4`x*$DE(xoxlmQ8H5jO8TVpRomHy+R*f}GcmMO^x<=qBf88SVqMGE zkF;a*De#a!t(rykMz-B`>3Kehm240WIfK713&?GL7WV6xTo?dpO!f+;3ryp;;R?P5 z`wb^U&K%~R0vLf&q9*#t_#a8ca9|j?oz(^C*LV*Zfr*l8@t87W06sr|b*zFFP?PbM zUC9hl(We|@J#_QW$?j)KfuZP%tUw|)-&cf_U_0!q&I%PA-x;pg2KaZLFYq}kx(Dlw z_SUtBHY!7`U&Zvh8-wp)9<+Nw|G4=nMcE@LU8m zMF~O(PRXIdSPoe`*O|kY?tTMxY%b8GqICc{>i}cq%IkLOdMFb@#-9BEH)a0 z4nZs^-mT+S+Off4KDDvnIXk0-3qYdBU!gZ>*Fi!q+Z8RY{*~>!?lsRJI=^8~CDl%b zrk;Xi*0*D?x;HU&s~aC1aVlNmQ>wFuYu_nVx*TDp8~n=eY71D`GX}M9nw1jZ!ixeU z+=a=7Gg^rjxIajU|4^d2@(yPhO_aic()ivYD*$07N6%HG;}Y1+h4kYKEB5pA|RS1N@by zG>5Gu@y@K&W3T=kRU^aZBFJlo97u;Ie-nD_7H+>D1fre@N_NXJ2(l>rSTKTd9j9h> zvm+<%^ECrZ=+6gnqKyv#pW0G-z*e$&C(5YdiK<3q@)5gbab53Fd2S4Ci=si1ZkZ-L zJ`4B+^)4)`5|R=l;JVU=~TEf|Wl#DTY_B)Hry*UuN8l_ ziuTiDfS&BI$?AnvO1Od~3F`}%QIf0L>+{W>^(B7kje2Vv zdjUNKd4AC;O#HcK|>Xg$wNdqoik@E`+V=sG}L)m zj(PJnp)jjKMnQm8E)Oj9WeF9qs4Y^Y?$9@3`zU`j+U?^V=C^yO()w1Al$JhhW!mK= zfVAf=H&K2*>VHtJJaaDZ(Jgz5ue&}A(o0vQ%s9nB4{`o?Wqv@_9@zPO;)@CCgle2?>X%UayWgGUr;0$3p<6

E=K8mdFO-F! z*U9eYed5D9q;imMg2k(7$UY$ercLG|4e5+VdJw6PegAIR2=4oS#;$VXJP5#748GH8PHUp&R`OV-WY_f=!&owBiKm6BpkrC9O;`9aSGcRWk3 z;v(ul0U2QNz;!EcbuFPbMfvq*t$RQFNAl^_u~9(hS{Wrnq>sdb)NU~shffAVbiofV zqHyc*yho*ALFWA+;D27gt_IOQyBlNJ;nLs$uTzMRxepSkiJ-maT$@uU$z4)rx9_Qd zt`oC({B*#sFY_myo4(H&uj-2*0%vM`)U=gbKSxdS1U2!K($|m@A0~HwE>>AzD=Hxp zX3l(?Ok*eg)`{kbn)0b8UL~bZxm^3utFBwzEuYB1ZJkC7!SNtDl}IgS?a4n8tlZul zK;fY!jAO#F$tX@fHbQP8?^AgiJ7C0H+M@fUWxbu;rl5A+s$WTZ%zwf$uBSCa_LqY1 zh;Uf-LjcK>5UKJrojOXX0%3@1*~eU?KhSnlH3kp%6K}44q?o3%0d~}h7k{2kmsC(5 zvOf|E%3tkNlzp*>Mz+WD9&4z;ivc6?Ik=sa0!8h2jg>~cO7T%hxaf4n_Wg5POBKiJ zc|`jd92ma_ds_R8KXD$GJE$*@Ul7d`VlJ%M&Mp}~m@AtXPqZ^|vI=;~2GoL2EbSuO z?ADJWECb4Nm#Wy$Z<8ru1B82~<-D(A_qHCFIXYN7n92Zz<1s2#OsX3ce;r8tE%X?|_; z%0dNQRF-5qRBq>ZzcL3Ov2v9@MsIzH-KvCiy8A|u$}U{;^T9xA7i6V0LYr{l55unuKnpKU%ljl2@7)CDqi(3 zYsotG+93r|kaK^v;51;JFJ~p4{6R6wwT5^K(EH@`V?4P_E@hN)?&TtNkda)C%Lln_Ncgy-3nH-~=(nQ`TvweG zOLB91A&)zDt_NZLI(^c*e8bY;uf%l&7#UF6j2AoA0iNp0JjfT#T}|FNRJ0>cmdv_& zs)LO%pmayAv`-|=Y5xml(-2~f?1DAf`VPb_h2l3ep;;oq9%4aPqVEcWFKyA^0m3>$ z4Gw;M7V)??ItwKR?#2yX^^KRg|ew!*U-lu)urKi zuxK2$Io6eb^e;)VFbFf@6!_7Y*bf3%QrpjepW|e>k!|+Xnh4QWa%j=!$Qh!gM{?6{ z?%U(Q$X|#rdQCzdDcEsY5UA<)ehYaQg|6SUj?vBEZ7fm-@l0wbP=z|e$QB|JIh@uU z`!Mazilo*@P4;1Bt(B7ckD6{A0j~aRA@3b|svD*O(vs-l;i{Xz zscHyz^HMFJwk32d;&5_2NQiHDCw|*C>*TsbSR3O#$ZqVxv$mfdvjr4|ZZ>OR*k`xX zZajJk%-JfVJ6ovV=2HRS>Vrj!_>reV30t`7a+n++vX9XKi^5S+eg|}Q?_bZhH7vVHqglWx`WbzlV=e)~O@{7bZ(m}Wz&DpCy^(&%P0Xj%a zNeX$k^n`LBq?$C$(D0%|G_de#5*GwL zUMIq`a?pzdf_@8(ZIpK=q;@`98Q-|PYt6Gf56Venlkj3^d^};@QVo8Nl&wLazk?l^ zqfF3l^H;*^&V)JZSaJhO9?^rOz$_G;cIF~m&udmOW6Hb9%*%KxU*FLZai0z7d8Z3g zn(%L`9kY841i2Jb=El~_*M=9qRB%U7|KXvGE9e6Bjb}-Umc!;!;9KZjN9veuBw?&T zTWWDy-`*G>vAOUn^W5hTav{`nsQjHl=FRG zyKq}+Tma8(D#e71D0Crimor=Fue42$^%QOGFIfNwKibF^1Qv6u#K{?ODLtLafPSr8;3k_55W)ijno|Cq7@y}t-f+mLw`waiPUsu1 zzkPraJ`87qK_1i$U1=?_*^e3nAkbrnI{J&pvsJTCD5ImIM>BOyf9N^M_TejvRp9>p zg};(2JzEwx*_GJTy6g1i;#egL`Mwz8DkGZB>Q>?GbZTd}L&zu2EqI0`=0*M8_AhEj z62B(Q;l|}ek+BoD<4xoZ`Vh?5ho2;uqAm>sIFRz!>QxLh@5!dm6TlbEgdjseNpxy3 zFq~UxXQHXG=A3Cj6YfqOdJl5ub=Kv}Y`a6Ad(;;c&qDSd%vTk$9jsqV=t2B|bqgZ7 z{yy&`5bT{DqF7X^;*v?(+Pg;>;SIa7Uq&4vBHOjD!c$rU=TIgg?7nXuaQ!~76a`p$1J=3gi65>{Er4IjJ*>m^sd(C>BHyFd-*Nahje9023KY+C zWQdRDJF7+{a&B@d=(2PpcEtUctOS|+S1+GU`uFB3TTkw0{(`61Ph9w0aKq?T-Z{OK z9G0gAPToF!`}7Sn$O{*U%)pJfzy4~LdVJvz=EPqYPM90?_pGPvD7k08)ClSu^;l<2 z?Vxosj^4^-lzB8ql-CRs1%b(~b0mch4RSvCY8*fH+os9tpuB3NGApTGJ=$c8{koMt zLP%JB&AjQ{dJY?I?i)qBf9CS+ONySM>%N4_>}>tx1ujhQ8p6SIquvN*h&H;&QTF7@ z^zYi{(1faymT3_$v+NAwIml0G@xhJdILNPAuv5?)e1soaBWGwotpn>QNq+m|F9bhz zsptT{gYDP^s6>dy)cw`sL<~3Grhlq@c8)AkSXr}^_~9?6;~+aJSUl}1BqN->F$tEv zZv;?RCS@fmHk?`$i@0D-sHNua zM)39W0f^uJ(BVm80|h@((6?}BVm!hZ`~_M|<(d6tefy(`!2@8&lPB^2djSX*V7v6U zzaHL21=BnWIm(o{*!;zHOL*^#JRMK@a5TDP34%29uT%O*In&;@sc}qS10wg^vwy67 zmb2SsgYZHS1GW_)ss83skqGR5%A)>9WU*R!((V=-Z_r|xt}uRX9I*$f3l<r^?4UKZtD z)~skOdIWr3!HsS+Ur9JlqE_yPy3S3oZT>~$1B@Q#bVj6YJnVYiDMdOqQtezh6tvBn zWwXeXN)(U*vzMyoK7M`d_{SA#q)wETEJ?tG| zR&gZtt6;HZofVv<9G>B^EJ%G;*~b-vhb0r*T7L@zz*h>1G6*7~yLPP4W3D5^Cf?4R zj_e<1B{+$@o)v=)bVk>CfZu}>n?-p)TqW4I?0TMWu03WQ+)p!}JZFNh(2k95J-7_G z>Y#>7I{?B#A1=9KN>6A_}y-3A3_&)7NLW7ySB`kf>0WTSAWGiRWQwE)5*8M{_F??PC|mC zQ!_#YHl828ka=yxdYZi<$(9Qf7iv4F9TwmlmnxZDZlN(#^A1^aB5D^-|s z`eUCF-`62X7iKHv)YBfflzywa`;H`q-Jt$&D2Y?Lo%?_F4@^Ulvl8iYCGc~7_+0C9 zGZ)gi6^on&;l?{+8nLmGb5YHCLm-UR1o&?AS5QZEv*ztMNW#X^{=Unz)qY`)Q}!Zr z*nUwO8n9Na!}qCtvwxwUKlAoFCa1C}ywzh-keV6hzfqI46{RhGik(DhMA2H0ypaQ6 z>(P%%eGOi`98HmtxUnD!X{Hqyd|BW6SMky!K){-mf{kuwvhYvs*jL#60=9ZKX)DDS zvAq>$I$n;t6TKpf*uL{*RlxY!Q>^K{P;pw5!bV5$4Bm!RHC9JY_E=chbw-oJynjlr z1NCDDJO_4kbr5*nDg-|zBOvE;4{XmK=A63oe=S{kAe7(Ne};sFB1_ha2w6&Y(k97P zm>K&FLUz*Fm(fD@vP9OhHZvHq?-Sxzk-{LmY#DnR`|{rD`~Ep|?{m(%=icX@bI(2J z^QdB%l z&K1APmg{M+U}b{}yjQnfpMK+tH-szQcPA;1e5mX5eiY&nzuS6J@z*BR1xj^p9#r;_ zNPA%Uy>!&K_z`_b3o7uo`$=!=P9G|+6sB2dNKfihs{=M@E~;Y}?;@o%@ud;V`s=fOYWs=4<-r#zs7)MM!Cs+^p#Isjp=B@{zZ8V=&L~gX54q_??M2y!$CC z@Lnq+fj#LMZ=*gL+*{`5@VUR*g|k5A^LkVkNPZ@e)q*^JmP=~+Ek&BVK-kZtzrEB; z*1Xf|(47{sz3$65z7dNusfXke`{AJ}#qSfNh_V+OpsC8xRyODEwzFAE3#QpaN*yi^m1l{UdXCnIfj-`o)?_nUpJ;-k$wIPF%r`>?0jA$(%F{MtJhwb9 z_Z;a{KwyWPv^#Gw^qztNyD+F?_}l=-LHXUuh#3e#DJc) z+;FLu{tKEQEgzw|x;HGanO=FcVUCG^zJ5m<>dYS{gkCz6=_i1+(%t^}a4&6cY*xAU zbl`c)?~{r>5}?kbv$&EKdF6Dff#Ns3Q0^Z3R6jg>ZDd#=K0UuZ@WE7&BS=E`aAz{j z^mD{uW*EI!3~HmOf7CiR-#ouspVyl4+Wx&>M#*^=pe;Qr7zezB7FC@k#H*nRc z(VP#jTC)>tw*B?s_LRjt|3p3p?2j5JjC064^H{%N%v+9UwU~P)Hv3)~7FKRU{H|`5 zn@lWJfnY8Wz{m@?h`%bz#kpBZStmcuqnqv7#5?NTlk+&Z^(^= z0i926f|iZ7HcBK=tmi%F!Dqc|qkMd|ptlKYP!Tq!1#wO4Udb zU1EvY4`@}~Dzg~yWQk?KO4u<1g1zk&Up&gW<}CKJYDIt{wdYmbbsOt_Ln^W79(kV- zsJ~8!MFM(d+a2MkobIHkNDE^@3qjPVmMYqPugbsQq1(c*Uw?*-Y$hP%$pTY&%(-uc~WVMo_L?ra0>*J&aId ztl`CXXe;a-M7U&-%>Y%+r0|7YSY04fKCO|Zlg@Qk4EVpU#O++ikgh0nn)*VP?+xjo z;ngTg7I`zzmOS!d$E4LA%i05zpU1J*6WZOIDf7J>DXQ<*irf4;z>)3M0zA8$h$ILK z6W#L!Y{WFW%Xw27lfH&qkTq(%L)4A_HY-+!WJd%L1=)?0}>JaEHP!* z7xADLn22ym677sp3h;5WZp%0}E@<C7pNaNGzOirmfwIbEczdp#O7i^pWjbGpCNf)OGD#^ zd{tAk7L(z|orF1fIv!=Q)UNFNOlRCs_z3hGt>60jZ_4RT1FgNc0ZAX8;Wp&6;BNRP zq}5M4(t|0<9bph=390gGGEgaA+gbek%xMGBjRUFdWi+?}C{s8r7Y=Z}vOq>ef zy&YZ@O!*!P1YCa4c~i+1pFWYx`Pe*;AUcFtBQSMMk84h&&$ZXk zPw?&VL2@>}?+xU?XVz`qYehUjW7ONcLF#O;c=D=noLud|k8TJH_asw-kJcIU%pnb3 zT2?0M(i^{ezxU?<7H8%)&TsRGeV_w$`p^`R^>Nvx-HTkAbU|;#hWtG1dgB9*&q7m( zigbz{M3M}A{EZ>CUAbNr;os6`#w+M9S^N2}ctA{q%!v6R9aKPC;%5uJyq4AEr;91& zcn&VOb_yH~J*+>A65W(x7+0U(W)QhqU8B4RfBHLBhP~)2Qc#3T?xZFjWT*w)!we+v z32y~WS-@+f>j1eI-mn5SYt&Mqp3S*~R{6qsqzgjwWA|EN$LyG#ne+oajAEGfi(!ZE{Qe+a2SnskEi|p zY`@n0ucm6U=~I~(fZ~zUZK^0`T9pJ{k?zW*HGOhe5A5r#H(B>~)>A2X=!XmzXf9SR z(g$p#<+D%1UFbsdy@Y2VAS%dXOVgGgV zFd+pIc$e|wPZjC)z*YH&`qU>g=1?gOW2Y+UeVu(P?6KY)dVi3@$+C3hVwVe^Jx(1s z-pd{^^nj%n*jgM$PJ>7`kM5_wIw$In@cM>C;h!!N= zwYaz!RM0MxxdjZQ|P;V_t%Di(}%c! z&D-1It=aljg-SBGEb?lcyyvHoPu{sAzURGo;^gi(zioyf5%1yfg_od@+ClDQo(H<4 zACWDvz5c6L+V;n4N9bzNN$%*I5Db{v$C$R3BZ>1MpV6BfoKNTT(604*8ozeN(_xc% z^F=xm`|Z^CH~;cL=x0qRLAXZYW zoVP9nW?977q?P@8)u^HGo?+|9QBsu^xxA=wNiU~;?-*?zkp>&fCoQqvcRrca>jFTr z&4gov1@d^u7LFQGY(IqllQ5+#I z`PlELF)0e?^PsgB zQhI&t_M#Sf?)KjHN<$+9_EHwTXRnQ54TE}L*-H8A7wM?FEeTk zgn+Yo(U{q2)lQ6wUU7Eun1$dOu9*LQVr5K;>vF56dMpnsuW0S%(E6PNCI2~I;~zDa zyzZ8?I|Pkg`4AG)vcciC!6Y2O17Dy5SR=0WujJ=q!^ZsMApc$O^eY#nCf|MG=G6}G zu}aNXFD^A5Ewx|+<*}wZvc%;cKa@(Q&p^>Z;f@3jE!uQ{fBwbQm1VNcusOuLa2*>l z4>ngRiZScTJ0Wo{W2fflRkZ+dfJ63u)8(A*z+N|LNSWyba3~%*v`%`5CJ^Wfe#gMefi5}i(JNYcgf92aA=$z z!I9UsS5>P0SNALV=E&kH@IYKJa2VFx!hIiEeO0pZ@}l0G`J4grmxnSoXJT1d0xe8? zYnE>|1PQ!bhCv8$3iWvN*wD(-NW;kg@|=*|lcu1j*+C3wV=DQT;+8&H9f0TT+-Ze} zt)xWZfFGhyWik3F5NJVtqjDG%Q8L=zylqcg97PGegg~W{A4_=Bd|63=7JzY+>*xp7 z#-NS#z{-5zhw4qP?YX4Tj|d(2w5IROC3J1iskPAo$N>oJL~W-O;*Ilcor`eRmiwHa1J z8P7F@55Pd3;H`jy)@*p_8P?87m-q?!Sk00U zBws3hMBIeDP$}r7Pe$Gz`dy8n5nG~XMKXlwNfxvN_aWt|!w5!u^>lSD{o8O~kbwX! zer%J~{$nb;(Ith0ynwnZa;@#Fs#{IqwNrM=iw%hmU(g3$vO5pF*%BmTn5q`Ecs~+@ z$`{&2wqp3JHigw|CgIpi?Xi7VA(CFLH#PHQJgQLo;GWK9JCK2aemu52`(}8za(DWq zQqut7=kPVcTZVLHI~UxDl8)EfRE8YM_TJy%hMI;-i@yMG1uCEDn4@d31ip6fMLM~g z?T-Za!k0i0%slurtyz9LhcZY(K9V6-T%_B3b48BcT|J?@Hbk3oES7ra8f_NKmw?AX zSh@QNzne*k&xf%<#x|pI+)#S}8wo~!w*%%@Ad%Ws8&Gw_@RuMKZTxh2KHsTtu& zI&|8Iv&~8LUls^u=7)~^u|%bZu(f5l0C(yHz{N3zha&dFugGaFU&-mNYh}Rd61qWP z)!oAFI}eB+d>Q6Gx?2bY-if9RKOE2uw5~O?>#SIzodYx4U$7S@@Ofx~V{YsM}$~lrQ9+i9y@#7s?q2upj zIX!we18fFz&ERBeqTw}(Di4T;n$K_*C8)(Mu3Jyiks@O^D@ zBQ^4klUEACwqI6H0lLY36hi$@VE}Og9}iRP+D!qer4UdTfEs_J`S?zyQ=XSsWg)O^ za(A06~G;6KoSDWG#kzpGK7QB5yli!+Uzsf{!J6uY>yO|`lT zm{lx(9RO^NAiE!o)=cVQ33nKDS6{peS=N$<`fAwe8d5^&KolaHpbeLO9Tiy*r&c+$ zr)MxF1*8R@4q11RX9}ay|NTH`YxF!DZ6BtMw@td$$nE5Ec5Xw#Q;FmWDh2TrOvnM@*h}d77`h6$ty3fVpx)OmUZ!& zw!)a8i0}~k@K?5C!_<}bnj+PE&0wK3E)SHt_#~!2RM(pKVlX)EG`{t1Cb87#mCo+! z3l;~r)a04A^+ZWn+UzaH1h`Z4<)@?cac2 z%!XI_+Jp0+xcT+G(A-T3ZPUc9xl8Zho9H2ufohJb{P4Q?={a~cGQjei0zO)MMBF`i z`$5s@^CNNF3fKjB#KKMBK0Gs27%@9^!=|mCg4kL8MPW< z0R~l@Wp#jdWwlAnP%hyqaFfOx1rA>-uv##;@34J(E+h)LKXJJ-M))5>Y7AbC49Lao zy>L1FMUm8e^U~@ktP~e;_#*n@<_K-rzi2y{>UBx6Sw}*O;@*qTH3DX%kJ_>ibII%} zcCfdUW^HRdYud>R8AJqY$`G(uui6oD;>|3p-cC+}edNSq9A(#$RRAUoxT2fuKk=6YEYzJ=V58lPQj1VM+$<=_~ z(Lk^Ww~|9HuN5QEfme)e^r>%N4Q%F%HH#)F^(WZJHUozg>#aeFv95pT&`0i2DzlEB}0#kLhh43`7{$^#N0 z^CmQ{P4A-h+v(Z%72KQw(Z}MmsHVd-b5qTbA&8QoPg~&vsy}zO{W3kTv|jSV^Zo3g z%l5WZ=I@*MSP{cNxl|=U23%cP=sy(m(U@pV9?=UdR&_VVOzDX3K6@p;t-k~A$E47k zlqE3*&d72x1%KP0qtcppJ@8{nxMzpk|Kkj{Zml;5i+W`7NUCL!3UTvW8iu^)T&++3 zk$88f`;HHL-zFH&S$=$|(rnfp0c0`qk^Gb|*)<(6tN4*$dgJ{!VgD9X?g~JfY>9?E zF8_FQXRr2AI~oO#TmD!y^o8A9#gak9B~*Q=e!1sj;E1_=;Y!=6iP$Rye?|jN6N>^G8@)6E=W% zfR(amTo(NmN_d(y+^3Rbb4f3jP3^}fD)zRiaj?tuXHBA(Ya^KZ^MkpQL$Zl)lVjNx zU6mXICHsJ60r5FAKJxpQcHN|*KELDu%YD>g=;FiJ@BCC+ z^TTY4Nc$ukgFu)adGhbx4ytuyvkb-PchGW^7c|?C10dd8QU`S_`bk50m2=1gtE=Cf zc|!M5^)a!)kc^ASc53kX+yE~~UWV1a9#sNodMOa@;a;;J{duT4Ah@#L#C4;P%w8}l z`)8}Dy}?QY92AeSy*H2(LjBm{u~)qBLhvQjOaEG6RSMI4?gHo69y`M`R|j5sLSDi{3dO-8$`#f=Z>klL<0mVAg)g0Gs*Z@1CbSY(aD?JtyR378dc`RZ z^P1qry%7N9&Rz+XdM)*ibZ7>0^j@TN^i6^>xleIw(8;4gM8PN>A2<)*9=$L%lR3U@ z;+>!mLvPz&PYnvq(nn_U6t?QZ0fCSE%T! zd0+Rl=*l$Vis>98aG@8_ip(WMgQ^@28N%|r2JR)k+G1PnHOBJ3RQ5i!8(ydoyl8S- zLe^H!8BL38UCyC1mAxev!8)U^N>{Clv-6LRimaU8-^v7=NUB)#h$pcovj0!JrbKb( zqiD)7#XiE~(Bo1x>+aFmfr9WvQ1^gN+bi(eiE8_C*Ck>Zk(qBW>$q?@qj6&jMBAnI z1!$qWi*{7|cn{>RM6p|dCtFTK&ek>gpv=9iKAM zky25@S4N`bddJ?M9qudT2WGOMD~-a{zVKB@TcQ~6gG8wbc68o+s%dhw3Ku|ZVuwgc z=BUO`sljGb7G(HsYb26zHHX_r(pphAwn-Zkcht=e6Fe>r6tq^ePv5@um5<2lr7`Mm zbMlrTNTl$2ci0sh+W+1SpEVZ<&ZH7m9+(EN-46#$egm(_-_s}q1eVG}gHL8%lkA_g zT?Q55<6mvCfLb*Hj#BVJRc%>T+K5&HD}-=sde4x<*lBmx^`SB|N@Tz;s1Ue=GbO0t zj=!$0;GOsn7ho{>=P#Vd`#1H-@xK1(D9y~dCtHlHBLx(@CPScmSar!a#SqkMx#2Ob(CSgdVR9yjtrQl+v=jt%Q8p-1#yM4z}5|_ZpD1gR41jhOkLgj?Z;u0%yG|a zz@l|NLAfiJcAADbt<5-BpC&%9hl@LXwTg-+1jfvN^wN#Jihp_y$R}*56pniB_a&z4 zkj!7=TYaKpVXfxURb_+kt8UIU=Jv%aC&4M?e<=PAS(PT#lIBgfrgxJaU%e|v6Mn_F z?|k2qj%`+1UQs&&Qq(?_OvAm+&j6VE0T-JzSV<@Ref)-ygsKZq5C9sfzo|KWg4+s+QgM`A_KurB)vB z#+$# zY}lW3zDoy0dt6UZx7)Fbn?ls?FIzWit6dBXDpMVtveA`A@}^J4gX~$5*Y=*mLF-hn zU(={~W$}Lpc_ZLm&Q5O3uf|#0E^m4a z!uLLA%xZpC?wStuvMzjBRU;01Is8lpHF!(nk3>iqZn1q2qRi{o$c_QM1D=&T&&%+_>ML5*}9!y#a5;&_ix>7&;^D=j&a%43LVt^ zek;XodhIrucyZ5h{B)yMqo1nF{92m^aSGRm1ua3 z2eMjNHzK`%iPgiMjos4pNh7`x0Ehr=l}-wbTx;}Ea_d^_wzqIf6nV3+==T1!43;1X zj-s>Q=?PzBwm&`l^&xz7e;~d%w>bS*^YCYoWiYDFzVOIj*?^`Fc~p^1@$z}DqEb-h zi;#6u-inv_Ghmik7nFgrvI5COauNL|M1@%%DPCcG367voY%S;dEe?u{LEOgMIi6zN zzGwReWWwy-u9*qtZiIhyKgoj}Fs6+G=Yn+P-Hf)6T0v?rAj+qNqI?Kj;`X$;sz3C`dQHU7RqcQ9V8LmMs+FF|K8}tVM-p8jVKg%!{%&k?lFt*AK z$ONyQTp<9YtEt18lrm)#mXZ6rEw+&S!(eJ<(S!Zz>)h2yKES=Kkw@%t`=PC)kB(;Z zkX7{?cEfvp762lMCiwCy<)U3^*;8=Um|k)jwNcbmSW_LfdjZ(U*^zy3jhaE9Jv)rO z)q}ato_9urLR4rsp^F~Spe+}z=Z~Oijf5u$z9&uq^e&Z zGIE#!bajza4cAvobil6$ia#sSiS)t0@mpor*7ugDz;kS$cuP^6g&BL1#>bHn7jzIU zu>@UAg!_+Z-=vAEnCf~uctds9fV|Fr6YOLSc-o8+il)+C@Q## z`LhJD$SUswdv|hMA5@&i8=7bmDpI*ai;dltgbM#XQ^{)4*TwK!NVsU3ke8~+2%r1? zb~r|!K%dM+6R?ZII7?SAaCm{Z7B1Ia>EJp@xs$sbugGrt7ZU>TzVFV|3ub=E5T(t(>fMFLJcQ@^`9gvd6UVe>tV$Jdrp>*4Ic5P z;@wWpw}w%_Iu776JP@)GD%A=q-&t}-<$qN7dvDM<3QBk0{c9V0AoJak|C?5+RBG*q zf}K>hrdEP_{{>ElV6_RKJ{7?|Ek`cdd_&%g|$kX3;}wtmqa{=$lr;`kbp}=zXdO zYyDfBK5KMNbE`5)=mx`Y9^cEDJp}lhX{U}NaZQdn&7)gBmCcvI7;Y(=h@XR!ijr=?Y;#1Bxc$~Jf!9pctjtn;UnPnF6Ra@D_4Gu+cY5lQS5$`P_`9G_CQ2D zcG>H2Opjk4;T}p{=J{>(-Bh zUlp>h5Ixu$ zxS=r-nmRf^+$4Cf>mJ70+O={44v<`L$S`e4>15bl+6n*`!X2lP2U1|;aF@p#t}E}$wV+id|G6gZ$h%sjQ2;7#ZyFzS5bs=JN}q1xcU|7b*`MDyy3o~5}U&Jz4ig-4h; z()b1yPLgR4rs}X?t{tBTMV%1|G(ixJ{nH_ZQ~$3T%r#8f=Y4|?&ZOg@Z#iX*e48d$ zG@)MNH2A}Tj7l3>d7NQwrTZEDl#BKu#wpRL=~Whfpi$!dzHq@L#7)%wIvNS6ZF)sRCME54k@p%4*q z;lDpFv98pl0}es>A&Ys?Mb5<Ru`cY-za=C9w{igTY(2u1{`ZIa z=(CU%kUf-~42%j?4w7Cy#QxTw5-!<5fa4<68P!PWe)E0B2;?*BFar{DQs4s#9ygkG z*Iqk62 zY7#4})Jwos$k6yU1etV~D9Sv#=zpL35JwN_MK2MMz*S6OjUazA+>#JdHRcw40#J>)sW9;&{Q!B*Zw8^R* zNc+}<%AZ`v8A}QDw~KSTdUmnZh;9_bmLP|X(DE|~a!KSN){(rZ?sd@~+eF34? z4<6FlX|sR}Q!`X!t7yn)0DO9l)=-|M`Oa%0!^lck1>iO&DUhSZ zl^udo!IuPN-KKkC-Qi__`nr<8msPW~5 zNG3lUCne`};#q~IsHBQIe`;{z*7**5a^k-L(Iy+I`pCVWDF(9n zdGM(zXSVlOn~qJ9R^W`fnsO}5CYdgD%@@}SoY_IEoEMT{V3~hu;%2^Z>Um$>H~BEs z>#aMOx1g{yHFW`wGRrR;C*%o(-Ne17x=%D@-s=&@jYjsDyG3TxUAra^_^uNj8Ue80 z{&ytA?AI{4IO!BbirStJCu|}X&ioDpt^FE#b#1iP49W%Cvx`D~lx6FEd42FLo(N=0 z8c9|cly_+9fY0-!qbz31DB^_CEA(=PLa#HSucu{+iWj6HtW{M(K)+=C{5OL8dkL)n z-|rOL#7+C?DNq~=TD69YcjhE`yESebL+gvY(a={u)EE3E@KvZqQ4#Y=HTKXq&H-@MzTCvli_7UW%+UpLf5-n2YVlDcjJLq z4+xM31u}swg0e;ZQqmAsdZch?GAU*tt4IY~R_B3firoFs9tvl?0NU_bgMUYEQ;&AR zVn(gNA|FXnIH;~G?xBNjyIN6mB>1v}RaW-tO&;p+H1qP&cHLX zaO)N#ABO#jAy5{KRln`9cF}{&D}uF4OVc?o2Mmh#VCvz(s`J#9IO2iXI&`sXh~l9) zh!m9}WF-^mVA%QUl7NF#@dSY##?qK9`<5o#H5B>m-Q!TI(XP@cF+3}4KnnJHbc&#> z51s-yrZF7!P(kNOGR{~%yzXsXt^=LvX?E9u@!hltBwg09XFeN5at;i_OrDC*R7Ax0 z=|NHpajct!vrg|8yG}*Kn$m+76fD13*cMI56{jH zE9+^7yIkQRO$%5C_dE#Js;&gzt4J^cX`>T|@VQPB@BEa?nlKu^Xw~=?G*7irX793s zJBoUxz29;yQ#l?c$leDK!7-R>#7X7SoZOK3NjVB#B(@R^yZ`6+yNFVdb{ws$UOOoo z$hnameTXl4*9Wq0R!g7jP%p-W$>5jsKSKpypW@f``jA(?6$#~lW~&NcD&AoMKONzs zh@F2tOc@rVNC^>5x9Aw5L9=RnlboGhR-rR!^C_nICI01#mqq#-xr4=RYP=tSnT(0W zSQpJh4y{+oYWh7$b#NP=y#OY`t=^g1e_CLw0cTFwh8#NTD6)Dz;vC}eYU4-XDpUX- z?0d9BVWlgcACg}2|5<=7D&L8sL+6{EnXL>yhD^c_6^0rjRc6y_{Fv zq#E%EZ1kG&;If~iKm6W`9Z6*$z+bg{a`jbBO@Kq?^`9`(g(B3}a`S`R9Wp3l3GOtE zR{p)9{~g?Exz(hdgi=uvNa6I!wJ*LrSLq>dkTe-Xxy<=DGLv<6jwM`fOlkVbJL=Znp80t*r$sR^o5 zTnOSyyUjZuco}de)PcRpm3? z+PVB6U`isl@gtvir=2r7jsR$z>VW&0pBefG!g)@xK{F+sfuaN|dCL^T4}gKCmQyJe zD*W1(FX<1VL54u&q4SL$DYjAg*%m~#W-%om zhoL|F5JIXa6|?;LE7mAD;!0f8`bZrY9MV5LENG_dia2OXJ1PfW_#X zxCW061?8TnVCX`t#(*V(cvb_Ct#?NuM4R0sU6Ql#@_NQ|hSLywxF6YUmMPz1_r$|Q z>3bhkRKbtvNRJ^p2h|5ymarbdW&>j%>3x-L>jp+zpOutni3`xF{P7?NKIL7z%m_LA z=`Y8MY%L$^VP4K2MJotc1$W;HB2Q@&EG}q+k6&d|YRS(x-tZN{D8{Pgtr05^tSBPggjf_O@d- zg&uMkxX#-A1f`y{vJ#EY9{3BqL=gfp9c!qcVFbz~>%eXZf`5)Cb1{o*yBUcGUM&N;JuQO33`%%YMqz+eEJ4}CNKYU1kELO zup+$eySh%OybHtnSB6q$#YT-!ui%#RZoy1#stIEne^y4K2^|1>q5aoc@;t5e@Qola z#LekQk8iM#PLzue7eAe+$p)&8fJ`x27wtjBW0VHaDml zS|p;D>o+sTH8y=kah|bsS({BY+6^owegSt9Oq4ocy#m*!gIz@$rsmIRCmhp0xvGN3 zZ+>iSPrRmP$f=V+DdgQ!EdBiP*10jSpkuuAPEtBJGT+wG((k)t)^?1mfqo?|yDK{e4&6yeQae8=Du*i*s9{LayK7{;0NWEV zood8=yL9^ZEKWqnsP{Ls`YqF_Ig+T^8nc{`a$;$5lBk5zZ94Xjh0@L3I^XT)pS#JT zI@>n9s|EB)ZSA`x{cAji_Bzg|+`<;Z{Kvfc`Ud-D4iiLY-kfVui;)s65>>3CkEhG^ zl(pCS5`LZ3IM~(vZtckTkG*#i^7hEwHSz{xf(@cd+$RtWk2jNEg9s*rB}|WM-7_(G z#Bv5Fs$=x%FHFe$MB>&UgF2%nTg67j>%3q-XoM~3cjMGsTXTV%fYK2@)=tc|#3)nt zk(Zz0@+kNaX_Pq6OIX9eSHKr-5bGQ0B zNs2AVh$lU)88$b`VW2bronebxZqy~H-|nnuT4AcF&hx1o=%%M{P1^Y;+|d`@B`!#q z-DM`EiE$uQ{PnvJj1TMAaLuAR3^?8`$7uC(|qpjLZNfT%KsO}38k=3rU13e`z?42?1ee%n2;w`t>;@FiVvJ##^#J`Ltb z7YZQ)8@tS0>D|vaSfABNFcA+9TYFY;o$dWGk$z4e&aHFzmJP2N(O1A^;sz3^)<|z5 zBDAl;dS1uqCM!%#?Y(5pAe%b9CF9>!$1_fvbr6!ClgTiK3ZVdq0@RTNEm)MA?}&j+fBOYhO8NT zb+*5MOu!1)w_@Ve_2XECy8aiF{LFm<35(b^e)Z?B)WpRJwj0^(<1^hWpt4TG{=C8l z);fU_II0|9dboO`_g$kOyC1WkN;`+~xf#7^KQ|%f4A^l7^R(9uRcu?AR<+TE^DiI3 zt{F9{igVK$C~^(Oe~GarNs-hFZhcAXCrOZAD9>wtW)8Jo6m5v|k}BOMjHUu-(Nqw8JBEL&lRuZp`E#2kCJ7afWZ=uHqNS z1CP*rHqS}N(PE_^D=y5`!NgBwt!Cfwt3x@M8%@&R2-^z@>P96nKspj+_L`Ghmz%)n z1j_kM?{pmtd$ZP?gH2HXDQqcKm;So%=kXEdJux9&T4&q z<6c9wVrhHd`&X3=aYv7~>4Y$Ns&iju`RqR7HQ|5``={T88+L?freM0$*^waMo>5Kb z)^!tuD}rBMoqu-Ja;5G4Rcjk(6GJPeuB{yoYYatLd|yLf|9bif@q==`=b|i7LWQr$ zdnexC3Of3ex#rKqHp8@@4X`X;x>sXPug#J5do%X_NzLImLiT>lTPJPAId#A!>0rKR zr-g?}>qY!&t21RXHCgb!WHUozGfsMFh0XnSt9ipwsW+EC)nKq^#mxBZXF9B?f*x{0 zb;6l(;){1ZQ&;0_=qon=nthD@CD#M;<1b;>B%8Hz@$YIO8!_YcYLR@le5>;492{){ zC49&|{mzgyz)WPmqf-C{@L z=EDjJ+Up`Q85PzW=7R0!tl~}*Nct5s#nKZ+8zk>NxGO5LHp?-# zawzrgbG9grWbj?XmMAPCELSf=^!#1wgs5g!P0l$-wyBzfq){)6dbT(9J>8`jMhD zeH<-`2fJZ2Kx`n8kxUXN;)vz3n4@WLIDo|-#)^k&(kC(*9-gN?i_>`wHJyltiFk^` zfT^DM>N%7v9eaF-_Tqj~8V!rKyEGw6EK4wvB`{fRYCxUxg8y`ykfS8WU4tzpT#J-= zm8anvGVWD1iyDB z-I0XgBp1Mxx3oX$A5%$DEz(QV?bCPA`JxEDgox z@zrDJN#4)w_lYOdc64ZhCvcP~$T2A^a38(hb}lThrR;*i2EnfJKNaQubulpIl6z8m z7oExglfb;Dmu*iyei00twxIKD-wK0``r*Wtw~0=#IPAGrFUv9baiuaZDoQ8N)>0n* zsQ3JR}Sm%@oGf^lhwVzp;yQ9v$VN2KB#exhMG?y*U`kQxq!LR$arav+YK7~BK@!*-}Tz=lZdvS+u zJ$Tc(MRdJ&_P-v}y}y68+ZsHN=l=Un { setOptimalDisplayMode(); } + ref.read(localNotificationServiceProvider).init(); + ref.listenManual(connectivityChangesProvider, (prev, current) async { // Play registered moves whenever the app comes back online. if (prev?.hasValue == true && diff --git a/lib/src/model/notifications/local_notification_service.dart b/lib/src/model/notifications/local_notification_service.dart new file mode 100644 index 0000000000..8255bd823f --- /dev/null +++ b/lib/src/model/notifications/local_notification_service.dart @@ -0,0 +1,95 @@ +import 'dart:async'; + +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'local_notification_service.g.dart'; + +@riverpod +LocalNotificationService localNotificationService( + LocalNotificationServiceRef ref, +) { + return LocalNotificationService.instance; +} + +class LocalNotificationService { + static final instance = LocalNotificationService(); + + final _notificationPlugin = FlutterLocalNotificationsPlugin(); + int currentId = 0; + + Future init() async { + await _notificationPlugin.initialize( + const InitializationSettings( + android: AndroidInitializationSettings('logo_black'), + iOS: DarwinInitializationSettings( + requestAlertPermission: false, + requestBadgePermission: false, + requestSoundPermission: false, + notificationCategories: [], + ), + ), + onDidReceiveNotificationResponse: _notificationRespsonse, + onDidReceiveBackgroundNotificationResponse: _notificationRespsonse, + ); + } + + Future showActionNotification( + String title, + ActionNotification notification, { + String? body, + }) async { + final id = currentId++; + await _notificationPlugin.show( + id, + title, + body, + notification.notificationDetails, + payload: notification.payload, + ); + return id; + } + + Future show( + String title, { + String? body, + }) async { + const notificationDetails = NotificationDetails( + android: AndroidNotificationDetails( + 'info', + 'Information', + importance: Importance.high, + priority: Priority.high, + ), + ); + final id = currentId++; + await _notificationPlugin.show( + id, + title, + body, + notificationDetails, + ); + return id; + } + + Future cancel(int id) async { + return _notificationPlugin.cancel(id); + } + + @pragma('vm:entry-point') + static void _notificationRespsonse(NotificationResponse response) { + final splits = response.payload?.split(':'); + if (splits == null) return; + + final id = splits[0]; + final payload = splits[1]; + switch (id) {} + } +} + +abstract class ActionNotification { + void callback(String? actionId); + String? get payload; + NotificationDetails get notificationDetails; + DarwinNotificationCategory get darwinNotificationCategory; +} diff --git a/pubspec.lock b/pubspec.lock index 0084f24bee..1501f16687 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -21,7 +21,7 @@ packages: dependency: transitive description: dart source: sdk - version: "0.3.2" + version: "0.1.5" analyzer: dependency: transitive description: @@ -122,18 +122,18 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: dd09dd4e2b078992f42aac7f1a622f01882a8492fef08486b27ddde929c19f04 + sha256: "644dc98a0f179b872f612d3eb627924b578897c629788e858157fa5e704ca0c7" url: "https://pub.dev" source: hosted - version: "2.4.12" + version: "2.4.11" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 + sha256: e3c79f69a64bdfcd8a776a3c28db4eb6e3fb5356d013ae5eb2e52007706d5dbe url: "https://pub.dev" source: hosted - version: "7.3.2" + version: "7.3.1" built_collection: dependency: transitive description: @@ -563,6 +563,30 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.0" + flutter_local_notifications: + dependency: "direct main" + description: + name: flutter_local_notifications + sha256: dd6676d8c2926537eccdf9f72128bbb2a9d0814689527b17f92c248ff192eaf3 + url: "https://pub.dev" + source: hosted + version: "17.2.1+2" + flutter_local_notifications_linux: + dependency: transitive + description: + name: flutter_local_notifications_linux + sha256: c49bd06165cad9beeb79090b18cd1eb0296f4bf4b23b84426e37dd7c027fc3af + url: "https://pub.dev" + source: hosted + version: "4.0.1" + flutter_local_notifications_platform_interface: + dependency: transitive + description: + name: flutter_local_notifications_platform_interface + sha256: "85f8d07fe708c1bdcf45037f2c0109753b26ae077e9d9e899d55971711a4ea66" + url: "https://pub.dev" + source: hosted + version: "7.2.0" flutter_localizations: dependency: "direct main" description: flutter @@ -863,10 +887,10 @@ packages: dependency: transitive description: name: macros - sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" + sha256: a8403c89b36483b4cbf9f1fcd24562f483cb34a5c9bf101cf2b0d8a083cf1239 url: "https://pub.dev" source: hosted - version: "0.1.2-main.4" + version: "0.1.0-main.5" matcher: dependency: transitive description: @@ -887,10 +911,10 @@ packages: dependency: "direct main" description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: "25dfcaf170a0190f47ca6355bdd4552cb8924b430512ff0cafb8db9bd41fe33b" url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.14.0" mime: dependency: transitive description: @@ -1401,6 +1425,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.7.0" + timezone: + dependency: transitive + description: + name: timezone + sha256: "2236ec079a174ce07434e89fcd3fcda430025eb7692244139a9cf54fdcf1fc7d" + url: "https://pub.dev" + source: hosted + version: "0.9.4" timing: dependency: transitive description: @@ -1541,10 +1573,10 @@ packages: dependency: transitive description: name: vm_service - sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc + sha256: "7475cb4dd713d57b6f7464c0e13f06da0d535d8b2067e188962a59bac2cf280b" url: "https://pub.dev" source: hosted - version: "14.2.4" + version: "14.2.2" wakelock_plus: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index c93316d418..4cd85abba5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -33,6 +33,7 @@ dependencies: flutter_displaymode: ^0.6.0 flutter_layout_grid: ^2.0.1 flutter_linkify: ^6.0.0 + flutter_local_notifications: ^17.2.1+2 flutter_localizations: sdk: flutter flutter_native_splash: ^2.3.5 From 6552de8acd33134b8bbb5c7ee9815dc15c6d79c5 Mon Sep 17 00:00:00 2001 From: incog Date: Sun, 21 Jul 2024 08:33:04 +1000 Subject: [PATCH 213/979] Challenge Repo: watch socket for new challenge requests --- lib/src/model/challenge/challenge.dart | 4 +-- .../model/challenge/challenge_repository.dart | 28 +++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/lib/src/model/challenge/challenge.dart b/lib/src/model/challenge/challenge.dart index d56420b316..3abb37c5de 100644 --- a/lib/src/model/challenge/challenge.dart +++ b/lib/src/model/challenge/challenge.dart @@ -45,7 +45,7 @@ class Challenge with _$Challenge, BaseChallenge implements BaseChallenge { 'Either clock or days must be set but not both.', ) const factory Challenge({ - required int socketVersion, + int? socketVersion, required ChallengeId id, GameFullId? gameFullId, required ChallengeStatus status, @@ -382,7 +382,7 @@ extension ChallengeExtension on Pick { Challenge _challengeFromPick(RequiredPick pick) { return Challenge( - socketVersion: pick('socketVersion').asIntOrThrow(), + socketVersion: pick('socketVersion').asIntOrNull(), id: pick('id').asChallengeIdOrThrow(), gameFullId: pick('fullId').asGameFullIdOrNull(), status: pick('status').asChallengeStatusOrThrow(), diff --git a/lib/src/model/challenge/challenge_repository.dart b/lib/src/model/challenge/challenge_repository.dart index d0cf8e653c..b0c58629c5 100644 --- a/lib/src/model/challenge/challenge_repository.dart +++ b/lib/src/model/challenge/challenge_repository.dart @@ -1,9 +1,37 @@ +import 'dart:async'; + import 'package:deep_pick/deep_pick.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:http/http.dart' as http; import 'package:lichess_mobile/src/model/challenge/challenge.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/common/socket.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'challenge_repository.g.dart'; + +@Riverpod(keepAlive: true) +ChallengeRepository challengeRepository(ChallengeRepositoryRef ref) { + final repo = ChallengeRepository(ref.read(lichessClientProvider)); + final socketClient = + ref.read(socketPoolProvider).open(Uri(path: '/lobby/socket/v5')); + socketClient.stream.listen( + (event) { + if (event.topic != 'challenges') return; + ref.invalidate(challengesListProvider); + }, + ); + return repo; +} + +@riverpod +Future challengesList( + ChallengesListRef ref, +) { + final repo = ref.read(challengeRepositoryProvider); + return repo.list(); +} typedef ChallengesList = ({ IList inward, From 5a83478dec59eaf91f26fc6e660dad5da5a6992c Mon Sep 17 00:00:00 2001 From: incog Date: Sun, 21 Jul 2024 08:33:43 +1000 Subject: [PATCH 214/979] Home Screen: add notification bubble to community icon --- lib/src/view/home/home_tab_screen.dart | 6 ++- lib/src/view/user/player_screen.dart | 70 ++++++++++++++++++++++---- lib/src/widgets/buttons.dart | 52 +++++++++++++++++++ 3 files changed, 118 insertions(+), 10 deletions(-) diff --git a/lib/src/view/home/home_tab_screen.dart b/lib/src/view/home/home_tab_screen.dart index e9f98b2356..b2726ecc7f 100644 --- a/lib/src/view/home/home_tab_screen.dart +++ b/lib/src/view/home/home_tab_screen.dart @@ -6,6 +6,7 @@ import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/account/ongoing_game.dart'; import 'package:lichess_mobile/src/model/auth/auth_controller.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; +import 'package:lichess_mobile/src/model/challenge/challenge_repository.dart'; import 'package:lichess_mobile/src/model/correspondence/correspondence_game_storage.dart'; import 'package:lichess_mobile/src/model/game/game_history.dart'; import 'package:lichess_mobile/src/model/settings/home_preferences.dart'; @@ -923,9 +924,11 @@ class _PlayerScreenButton extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final connectivity = ref.watch(connectivityChangesProvider); + final challenges = ref.watch(challengesListProvider); + final count = challenges.valueOrNull?.inward.length; return connectivity.maybeWhen( - data: (connectivity) => AppBarIconButton( + data: (connectivity) => AppBarNotificationIconButton( icon: const Icon(Icons.group), semanticsLabel: context.l10n.players, onPressed: !connectivity.isOnline @@ -937,6 +940,7 @@ class _PlayerScreenButton extends ConsumerWidget { builder: (_) => const PlayerScreen(), ); }, + count: count ?? 0, ), orElse: () => AppBarIconButton( icon: const Icon(Icons.group), diff --git a/lib/src/view/user/player_screen.dart b/lib/src/view/user/player_screen.dart index 4719e6926e..750348101f 100644 --- a/lib/src/view/user/player_screen.dart +++ b/lib/src/view/user/player_screen.dart @@ -42,23 +42,68 @@ class PlayerScreen extends ConsumerWidget { } Widget _androidBuilder(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(context.l10n.players), - ), - body: _Body(), + return const Scaffold( + body: _AndroidBody(), ); } Widget _iosBuilder(BuildContext context) { - return CupertinoPageScaffold( - navigationBar: const CupertinoNavigationBar(), - child: _Body(), + // return CupertinoPageScaffold( + // navigationBar: const CupertinoNavigationBar(), + // child: _Body(), + // ); + return const Placeholder(); + } +} + +class _AndroidBody extends StatefulWidget { + const _AndroidBody(); + + @override + State<_AndroidBody> createState() => _AndriodBodyState(); +} + +class _AndriodBodyState extends State<_AndroidBody> + with TickerProviderStateMixin { + late final TabController _tabController; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 2, vsync: this); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(context.l10n.players), + bottom: TabBar( + controller: _tabController, + tabs: [ + Tab(text: context.l10n.friends), + Tab(text: context.l10n.preferencesNotifyChallenge), + ], + ), + ), + body: TabBarView( + controller: _tabController, + children: [ + _PlayersBody(), + _ChallengesBody(), + ], + ), ); } } -class _Body extends ConsumerWidget { +class _PlayersBody extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final session = ref.watch(authSessionProvider); @@ -78,6 +123,13 @@ class _Body extends ConsumerWidget { } } +class _ChallengesBody extends ConsumerWidget { + @override + Widget build(BuildContext context, WidgetRef ref) { + return const Placeholder(); + } +} + class AlwaysDisabledFocusNode extends FocusNode { @override bool get hasFocus => false; diff --git a/lib/src/widgets/buttons.dart b/lib/src/widgets/buttons.dart index 58c2fa1a40..401ca33972 100644 --- a/lib/src/widgets/buttons.dart +++ b/lib/src/widgets/buttons.dart @@ -193,6 +193,58 @@ class AppBarIconButton extends StatelessWidget { } } +/// Platform agnostic icon button to appear in the app bar, that has a notification bubble. +class AppBarNotificationIconButton extends StatelessWidget { + const AppBarNotificationIconButton({ + required this.icon, + required this.onPressed, + required this.semanticsLabel, + required this.count, + this.color, + super.key, + }); + + final Widget icon; + final VoidCallback? onPressed; + final String semanticsLabel; + final Color? color; + final int count; + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + AppBarIconButton( + icon: icon, + onPressed: onPressed, + semanticsLabel: semanticsLabel, + ), + if (count > 0) + Positioned( + top: 0, + right: 5, + child: Container( + padding: const EdgeInsets.fromLTRB(4, 1, 4, 1), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: color ?? Colors.red, + ), + child: Text( + count.toString(), + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ), + ) + else + Container(), + ], + ); + } +} + class AdaptiveTextButton extends StatelessWidget { const AdaptiveTextButton({ required this.child, From b3f35da47da3331356dc518ae926b9e14ec27817 Mon Sep 17 00:00:00 2001 From: incog Date: Mon, 22 Jul 2024 02:46:02 +1000 Subject: [PATCH 215/979] Create Custom Game Screen: extract challenge display to widget --- .../view/play/create_custom_game_screen.dart | 101 ++++++---------- lib/src/widgets/challenge_display.dart | 111 ++++++++++++++++++ 2 files changed, 146 insertions(+), 66 deletions(-) create mode 100644 lib/src/widgets/challenge_display.dart diff --git a/lib/src/view/play/create_custom_game_screen.dart b/lib/src/view/play/create_custom_game_screen.dart index fe7c97f903..3e2b1f6f91 100644 --- a/lib/src/view/play/create_custom_game_screen.dart +++ b/lib/src/view/play/create_custom_game_screen.dart @@ -1,11 +1,9 @@ import 'dart:async'; -import 'package:dartchess/dartchess.dart'; import 'package:deep_pick/deep_pick.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; @@ -19,7 +17,6 @@ import 'package:lichess_mobile/src/model/lobby/game_setup.dart'; import 'package:lichess_mobile/src/model/lobby/lobby_repository.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/styles/lichess_colors.dart'; -import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; @@ -27,12 +24,12 @@ import 'package:lichess_mobile/src/view/game/game_screen.dart'; import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; import 'package:lichess_mobile/src/widgets/adaptive_choice_picker.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/challenge_display.dart'; import 'package:lichess_mobile/src/widgets/expanded_section.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/non_linear_slider.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; -import 'package:lichess_mobile/src/widgets/user_full_name.dart'; import 'common_play_widgets.dart'; @@ -242,73 +239,45 @@ class _ChallengesBodyState extends ConsumerState<_ChallengesBody> { final isMySeek = UserId.fromUserName(challenge.username) == session?.user.id; - return Container( + return CorrespondenceChallengeDisplay( + challenge: challenge, + user: LightUser( + id: UserId.fromUserName(challenge.username), + name: challenge.username, + title: challenge.title, + ), + subtitle: subtitle, color: isMySeek ? LichessColors.green.withOpacity(0.2) : null, - child: Slidable( - endActionPane: isMySeek - ? ActionPane( - motion: const ScrollMotion(), - extentRatio: 0.3, - children: [ - SlidableAction( - onPressed: (BuildContext context) { + onPressed: isMySeek + ? null + : session == null + ? () { + showPlatformSnackbar( + context, + context.l10n.youNeedAnAccountToDoThat, + ); + } + : () { + showConfirmDialog( + context, + title: Text(context.l10n.accept), + isDestructiveAction: true, + onConfirm: (_) { socketClient.send( - 'cancelSeek', + 'joinSeek', challenge.id.toString(), ); }, - backgroundColor: context.lichessColors.error, - foregroundColor: Colors.white, - icon: Icons.cancel, - label: context.l10n.cancel, - ), - ], - ) - : null, - child: PlatformListTile( - padding: Styles.bodyPadding, - leading: Icon(challenge.perf.icon), - trailing: Icon( - challenge.side == null - ? LichessIcons.adjust - : challenge.side == Side.white - ? LichessIcons.circle - : LichessIcons.circle_empty, - ), - title: UserFullNameWidget( - user: LightUser( - id: UserId.fromUserName(challenge.username), - name: challenge.username, - title: challenge.title, - ), - rating: challenge.rating, - provisional: challenge.provisional, - ), - subtitle: Text(subtitle), - onTap: isMySeek - ? null - : session == null - ? () { - showPlatformSnackbar( - context, - context.l10n.youNeedAnAccountToDoThat, - ); - } - : () { - showConfirmDialog( - context, - title: Text(context.l10n.accept), - isDestructiveAction: true, - onConfirm: (_) { - socketClient.send( - 'joinSeek', - challenge.id.toString(), - ); - }, - ); - }, - ), - ), + ); + }, + onCancel: isMySeek + ? () { + socketClient.send( + 'cancelSeek', + challenge.id.toString(), + ); + } + : null, ); }, ); diff --git a/lib/src/widgets/challenge_display.dart b/lib/src/widgets/challenge_display.dart new file mode 100644 index 0000000000..f6fa3e0e9f --- /dev/null +++ b/lib/src/widgets/challenge_display.dart @@ -0,0 +1,111 @@ +import 'package:dartchess/dartchess.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_slidable/flutter_slidable.dart'; +import 'package:lichess_mobile/src/model/challenge/challenge.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/common/speed.dart'; +import 'package:lichess_mobile/src/model/lobby/correspondence_challenge.dart'; +import 'package:lichess_mobile/src/model/user/user.dart'; +import 'package:lichess_mobile/src/styles/lichess_icons.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/widgets/list.dart'; +import 'package:lichess_mobile/src/widgets/user_full_name.dart'; + +class ChallengeDisplay extends StatelessWidget { + const ChallengeDisplay({ + super.key, + required this.challenge, + required this.user, + required this.subtitle, + this.color, + this.onPressed, + this.onCancel, + }); + + final Challenge challenge; + final LightUser user; + final String subtitle; + final Color? color; + final VoidCallback? onPressed; + final VoidCallback? onCancel; + + @override + Widget build(BuildContext context) { + return Container( + color: color, + child: Slidable( + endActionPane: onCancel != null + ? ActionPane( + motion: const ScrollMotion(), + extentRatio: 0.3, + children: [ + SlidableAction( + onPressed: (BuildContext context) => onCancel!(), + backgroundColor: context.lichessColors.error, + foregroundColor: Colors.white, + icon: Icons.cancel, + label: context.l10n.cancel, + ), + ], + ) + : null, + child: PlatformListTile( + padding: Styles.bodyPadding, + leading: Icon(challenge.perf.icon), + trailing: Icon( + challenge.sideChoice == SideChoice.random + ? LichessIcons.adjust + : challenge.sideChoice == SideChoice.white + ? LichessIcons.circle + : LichessIcons.circle_empty, + ), + title: UserFullNameWidget(user: user), + subtitle: Text(subtitle), + onTap: onPressed, + ), + ), + ); + } +} + +class CorrespondenceChallengeDisplay extends StatelessWidget { + const CorrespondenceChallengeDisplay({ + super.key, + required this.challenge, + required this.user, + required this.subtitle, + this.color, + this.onPressed, + this.onCancel, + }); + + final CorrespondenceChallenge challenge; + final LightUser user; + final String subtitle; + final Color? color; + final VoidCallback? onPressed; + final VoidCallback? onCancel; + + @override + Widget build(BuildContext context) { + return ChallengeDisplay( + challenge: Challenge( + id: ChallengeId(challenge.id.value), + status: ChallengeStatus.created, + variant: challenge.variant, + speed: Speed.correspondence, + timeControl: ChallengeTimeControlType.correspondence, + rated: challenge.rated, + sideChoice: challenge.side == null + ? SideChoice.random + : challenge.side == Side.white + ? SideChoice.white + : SideChoice.black, + days: challenge.days, + ), + user: user, + subtitle: subtitle, + ); + } +} From b7f9a4ece4add0725ab64e13a7e7095e04f4db70 Mon Sep 17 00:00:00 2001 From: incog Date: Mon, 22 Jul 2024 05:01:35 +1000 Subject: [PATCH 216/979] Player Screen: finish converting to stateful widget --- lib/src/view/user/player_screen.dart | 57 +++++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 5 deletions(-) diff --git a/lib/src/view/user/player_screen.dart b/lib/src/view/user/player_screen.dart index 750348101f..1ec825410c 100644 --- a/lib/src/view/user/player_screen.dart +++ b/lib/src/view/user/player_screen.dart @@ -20,6 +20,8 @@ import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; import 'package:lichess_mobile/src/widgets/user_full_name.dart'; +enum _ViewMode { players, challenges } + class PlayerScreen extends ConsumerWidget { const PlayerScreen({super.key}); @@ -48,11 +50,10 @@ class PlayerScreen extends ConsumerWidget { } Widget _iosBuilder(BuildContext context) { - // return CupertinoPageScaffold( - // navigationBar: const CupertinoNavigationBar(), - // child: _Body(), - // ); - return const Placeholder(); + return const CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar(), + child: _CupertinoBody(), + ); } } @@ -103,6 +104,52 @@ class _AndriodBodyState extends State<_AndroidBody> } } +class _CupertinoBody extends StatefulWidget { + const _CupertinoBody(); + + @override + _CupertinoBodyState createState() => _CupertinoBodyState(); +} + +class _CupertinoBodyState extends State<_CupertinoBody> { + _ViewMode _selectedSegment = _ViewMode.players; + + @override + Widget build(BuildContext context) { + return SafeArea( + child: Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: Styles.bodyPadding, + child: CupertinoSlidingSegmentedControl<_ViewMode>( + groupValue: _selectedSegment, + children: { + _ViewMode.players: Text(context.l10n.players), + _ViewMode.challenges: + Text(context.l10n.preferencesNotifyChallenge), + }, + onValueChanged: (_ViewMode? view) { + if (view != null) { + setState(() { + _selectedSegment = view; + }); + } + }, + ), + ), + Expanded( + child: _selectedSegment == _ViewMode.players + ? _PlayersBody() + : _ChallengesBody(), + ), + ], + ), + ); + } +} + class _PlayersBody extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { From b304c9062bd93e8a0dd41bce15d4fc2e6996a39e Mon Sep 17 00:00:00 2001 From: inc0g_ Date: Thu, 1 Aug 2024 11:09:08 +1000 Subject: [PATCH 217/979] Challenge Repository: Add ChallengesProvider --- .../model/challenge/challenge_repository.dart | 75 ++++++++++++---- lib/src/view/home/home_tab_screen.dart | 2 +- lib/src/view/play/challenge_screen.dart | 45 ++++++++-- lib/src/view/user/player_screen.dart | 87 ++++++++++++++++++- 4 files changed, 182 insertions(+), 27 deletions(-) diff --git a/lib/src/model/challenge/challenge_repository.dart b/lib/src/model/challenge/challenge_repository.dart index b0c58629c5..bb52c9b694 100644 --- a/lib/src/model/challenge/challenge_repository.dart +++ b/lib/src/model/challenge/challenge_repository.dart @@ -11,26 +11,9 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'challenge_repository.g.dart'; -@Riverpod(keepAlive: true) -ChallengeRepository challengeRepository(ChallengeRepositoryRef ref) { - final repo = ChallengeRepository(ref.read(lichessClientProvider)); - final socketClient = - ref.read(socketPoolProvider).open(Uri(path: '/lobby/socket/v5')); - socketClient.stream.listen( - (event) { - if (event.topic != 'challenges') return; - ref.invalidate(challengesListProvider); - }, - ); - return repo; -} - @riverpod -Future challengesList( - ChallengesListRef ref, -) { - final repo = ref.read(challengeRepositoryProvider); - return repo.list(); +ChallengeRepository challengeRepository(ChallengeRepositoryRef ref) { + return ChallengeRepository(ref.read(lichessClientProvider)); } typedef ChallengesList = ({ @@ -110,3 +93,57 @@ class ChallengeRepository { } } } + +@Riverpod(keepAlive: true) +class Challenges extends _$Challenges { + StreamSubscription? _subscription; + + late SocketClient _socketClient; + + @override + Future build() async { + _socketClient = ref.watch(socketPoolProvider).open(Uri(path: '/socket/v5')); + + _subscription?.cancel(); + _subscription = _socketClient.stream.listen(_handleSocketEvent); + + ref.onDispose(() { + _subscription?.cancel(); + }); + + return ref.read(challengeRepositoryProvider).list(); + } + + Future accept(ChallengeId id) async { + final repo = ref.read(challengeRepositoryProvider); + return repo + .accept(id) + .then((_) => repo.show(id).then((challenge) => challenge.gameFullId)); + } + + void cancel(ChallengeId id) { + ref.read(challengeRepositoryProvider).cancel(id); + } + + void _handleSocketEvent(SocketEvent event) { + if (event.topic != 'challenges') return; + + final listPick = pick(event.data).required(); + final inward = listPick('in').asListOrEmpty(Challenge.fromPick); + final outward = listPick('out').asListOrEmpty(Challenge.fromPick); + + final prevIds = state.value?.inward.map((element) => element.id) ?? []; + // find any challenges that weren't in the inward list before + inward + .map((element) => element.id) + .where((id) => !prevIds.contains(id)) + .map((id) => inward.firstWhere((element) => element.id == id)) + .forEach(_notifyUser); + + state = AsyncValue.data((inward: inward.lock, outward: outward.lock)); + } + + void _notifyUser(Challenge challenge) { + // send a notification to the user + } +} diff --git a/lib/src/view/home/home_tab_screen.dart b/lib/src/view/home/home_tab_screen.dart index b2726ecc7f..46138e9363 100644 --- a/lib/src/view/home/home_tab_screen.dart +++ b/lib/src/view/home/home_tab_screen.dart @@ -924,7 +924,7 @@ class _PlayerScreenButton extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final connectivity = ref.watch(connectivityChangesProvider); - final challenges = ref.watch(challengesListProvider); + final challenges = ref.watch(challengesProvider); final count = challenges.valueOrNull?.inward.length; return connectivity.maybeWhen( diff --git a/lib/src/view/play/challenge_screen.dart b/lib/src/view/play/challenge_screen.dart index 7ed5978826..65c62b3a57 100644 --- a/lib/src/view/play/challenge_screen.dart +++ b/lib/src/view/play/challenge_screen.dart @@ -11,6 +11,7 @@ import 'package:lichess_mobile/src/model/challenge/challenge.dart'; import 'package:lichess_mobile/src/model/challenge/challenge_preferences.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/time_increment.dart'; +import 'package:lichess_mobile/src/model/lobby/create_game_service.dart'; import 'package:lichess_mobile/src/model/lobby/game_setup.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; @@ -406,15 +407,47 @@ class _ChallengeBodyState extends ConsumerState<_ChallengeBody> { ); } : null - : snapshot.connectionState == ConnectionState.waiting - ? null - // TODO handle correspondence time control - : () async { + : timeControl == + ChallengeTimeControlType.correspondence + ? () async { + final createGameService = + ref.read(createGameServiceProvider); showPlatformSnackbar( context, - 'Correspondence time control is not supported yet', + 'Sent challenge to ${widget.user.name}', ); - }, + final response = + createGameService.newChallenge( + preferences.makeRequest( + widget.user, + preferences.variant != + Variant.fromPosition + ? null + : fromPositionFenInput, + ), + ); + response.then((value) { + if (!context.mounted) return; + + if (value.$1 != null) { + pushPlatformRoute( + context, + rootNavigator: true, + builder: (BuildContext context) { + return GameScreen( + initialGameId: value.$1, + ); + }, + ); + } else { + showPlatformSnackbar( + context, + '${widget.user.name}: ${declineReasonMessage(context, value.$2!)}', + ); + } + }); + } + : null, child: Text( context.l10n.challengeChallengeToPlay, style: Styles.bold, diff --git a/lib/src/view/user/player_screen.dart b/lib/src/view/user/player_screen.dart index 1ec825410c..c904219ba2 100644 --- a/lib/src/view/user/player_screen.dart +++ b/lib/src/view/user/player_screen.dart @@ -3,18 +3,25 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; +import 'package:lichess_mobile/src/model/challenge/challenge.dart'; +import 'package:lichess_mobile/src/model/challenge/challenge_repository.dart'; import 'package:lichess_mobile/src/model/relation/online_friends.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; +import 'package:lichess_mobile/src/styles/lichess_colors.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/focus_detector.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/account/rating_pref_aware.dart'; +import 'package:lichess_mobile/src/view/game/game_screen.dart'; import 'package:lichess_mobile/src/view/relation/following_screen.dart'; import 'package:lichess_mobile/src/view/user/leaderboard_widget.dart'; import 'package:lichess_mobile/src/view/user/search_screen.dart'; import 'package:lichess_mobile/src/view/user/user_screen.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/challenge_display.dart'; +import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; @@ -173,7 +180,85 @@ class _PlayersBody extends ConsumerWidget { class _ChallengesBody extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return const Placeholder(); + final challengeNotifier = ref.read(challengesProvider.notifier); + final challenges = ref.watch(challengesProvider); + final session = ref.watch(authSessionProvider); + + return challenges.when( + data: (challenges) { + final list = challenges.inward.addAll(challenges.outward); + + return ListView.separated( + itemCount: list.length, + separatorBuilder: (context, index) => + const PlatformDivider(height: 1, cupertinoHasLeading: true), + itemBuilder: (context, index) { + final challenge = list[index]; + final user = challenge.challenger?.user; + if (user == null) { + return null; // not sure why there wouldn't be a challenger + } + + final time = challenge.days == null + ? '∞' + : '${context.l10n.daysPerTurn}: ${challenge.days}'; + final subtitle = challenge.rated + ? '${context.l10n.rated} • $time' + : '${context.l10n.casual} • $time'; + final isMyChallenge = + challenge.direction == ChallengeDirection.outward; + + return ChallengeDisplay( + challenge: challenge, + user: user, + subtitle: subtitle, + color: + isMyChallenge ? LichessColors.green.withOpacity(0.2) : null, + onPressed: isMyChallenge + ? null + : session == null + ? () { + showPlatformSnackbar( + context, + context.l10n.youNeedAnAccountToDoThat, + ); + } + : () { + showConfirmDialog( + context, + title: Text(context.l10n.accept), + isDestructiveAction: true, + onConfirm: (_) { + challengeNotifier.accept(challenge.id).then((id) { + if (!context.mounted) return; + pushPlatformRoute( + context, + rootNavigator: true, + builder: (BuildContext context) { + return GameScreen( + initialGameId: id, + ); + }, + ); + }); + }, + ); + }, + onCancel: isMyChallenge + ? () { + challengeNotifier.cancel(challenge.id); + } + : null, + ); + }, + ); + }, + loading: () { + return const Center(child: CircularProgressIndicator.adaptive()); + }, + error: (error, stack) => + const Center(child: Text('Error loading challenges')), + ); } } From a0512aa6e09349541032eb987bc21500daa2ff32 Mon Sep 17 00:00:00 2001 From: inc0g_ Date: Mon, 5 Aug 2024 20:07:57 +1000 Subject: [PATCH 218/979] Local Notifications: simplify api --- .../notifications/info_notification.dart | 37 +++++++++ .../local_notification_service.dart | 78 +++++++++---------- 2 files changed, 74 insertions(+), 41 deletions(-) create mode 100644 lib/src/model/notifications/info_notification.dart diff --git a/lib/src/model/notifications/info_notification.dart b/lib/src/model/notifications/info_notification.dart new file mode 100644 index 0000000000..2d3d59e8e8 --- /dev/null +++ b/lib/src/model/notifications/info_notification.dart @@ -0,0 +1,37 @@ +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:lichess_mobile/l10n/l10n.dart'; +import 'package:lichess_mobile/l10n/l10n_en.dart'; +import 'package:lichess_mobile/src/model/notifications/local_notification_service.dart'; + +class InfoNotificationDetails { + InfoNotificationDetails(this._locale) { + InfoNotificationDetails.instance = this; + } + + // the default instance is set to english but this is overridden in LocalNotificationService.init() + static InfoNotificationDetails instance = + InfoNotificationDetails(AppLocalizationsEn()); + + final AppLocalizations _locale; + + NotificationDetails get notificationDetails => NotificationDetails( + android: AndroidNotificationDetails( + 'general', + _locale.mobileGeneralNotificationCategory, + importance: Importance.high, + priority: Priority.high, + ), + ); +} + +class InfoNotification { + InfoNotification(); + + LocalNotification build(String title, {String? body}) { + return LocalNotification( + title: title, + body: body, + notificationDetails: InfoNotificationDetails.instance.notificationDetails, + ); + } +} diff --git a/lib/src/model/notifications/local_notification_service.dart b/lib/src/model/notifications/local_notification_service.dart index 8255bd823f..f7616b76bc 100644 --- a/lib/src/model/notifications/local_notification_service.dart +++ b/lib/src/model/notifications/local_notification_service.dart @@ -1,11 +1,16 @@ import 'dart:async'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/notifications/challenge_notification.dart'; +import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'local_notification_service.g.dart'; +part 'local_notification_service.freezed.dart'; -@riverpod +@Riverpod(keepAlive: true) LocalNotificationService localNotificationService( LocalNotificationServiceRef ref, ) { @@ -13,9 +18,12 @@ LocalNotificationService localNotificationService( } class LocalNotificationService { - static final instance = LocalNotificationService(); + LocalNotificationService(this._log); + static final instance = + LocalNotificationService(Logger('LocalNotificationService')); final _notificationPlugin = FlutterLocalNotificationsPlugin(); + final Logger _log; int currentId = 0; Future init() async { @@ -34,62 +42,50 @@ class LocalNotificationService { ); } - Future showActionNotification( - String title, - ActionNotification notification, { - String? body, - }) async { + Future show(LocalNotification notification) async { final id = currentId++; await _notificationPlugin.show( id, - title, - body, + notification.title, + notification.body, notification.notificationDetails, payload: notification.payload, ); return id; } - Future show( - String title, { - String? body, - }) async { - const notificationDetails = NotificationDetails( - android: AndroidNotificationDetails( - 'info', - 'Information', - importance: Importance.high, - priority: Priority.high, - ), - ); - final id = currentId++; - await _notificationPlugin.show( - id, - title, - body, - notificationDetails, - ); - return id; - } - Future cancel(int id) async { return _notificationPlugin.cancel(id); } @pragma('vm:entry-point') static void _notificationRespsonse(NotificationResponse response) { - final splits = response.payload?.split(':'); - if (splits == null) return; + if (response.payload == null) return; + + try { + final splits = response.payload!.split(':'); - final id = splits[0]; - final payload = splits[1]; - switch (id) {} + final id = splits[0]; + final payload = splits[1]; + switch (id) { + case 'challenge-notification': + final notification = ChallengeNotification(ChallengeId(payload)); + notification.callback(response.actionId); + } + } catch (error) { + LocalNotificationService.instance._log.warning( + 'Malformed notification payload: [ID: ${response.id}] ${response.payload}', + ); + } } } -abstract class ActionNotification { - void callback(String? actionId); - String? get payload; - NotificationDetails get notificationDetails; - DarwinNotificationCategory get darwinNotificationCategory; +@freezed +class LocalNotification with _$LocalNotification { + factory LocalNotification({ + required String title, + String? body, + String? payload, + required NotificationDetails notificationDetails, + }) = _LocalNotification; } From 5994e3f92d807516635fe35a0fabf760bbfdee42 Mon Sep 17 00:00:00 2001 From: inc0g_ Date: Mon, 5 Aug 2024 21:20:30 +1000 Subject: [PATCH 219/979] Challenge Repository: add notifications for new challenges --- .../model/challenge/challenge_repository.dart | 15 ++- .../notifications/challenge_notification.dart | 107 ++++++++++++++++++ .../local_notification_service.dart | 49 ++++++-- 3 files changed, 161 insertions(+), 10 deletions(-) create mode 100644 lib/src/model/notifications/challenge_notification.dart diff --git a/lib/src/model/challenge/challenge_repository.dart b/lib/src/model/challenge/challenge_repository.dart index bb52c9b694..c3cedda566 100644 --- a/lib/src/model/challenge/challenge_repository.dart +++ b/lib/src/model/challenge/challenge_repository.dart @@ -7,6 +7,8 @@ import 'package:lichess_mobile/src/model/challenge/challenge.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; +import 'package:lichess_mobile/src/model/notifications/challenge_notification.dart'; +import 'package:lichess_mobile/src/model/notifications/local_notification_service.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'challenge_repository.g.dart'; @@ -116,6 +118,13 @@ class Challenges extends _$Challenges { Future accept(ChallengeId id) async { final repo = ref.read(challengeRepositoryProvider); + + final inward = state.value!.inward; + final outward = state.value!.outward; + final newInward = + inward.remove(inward.firstWhere((challenge) => challenge.id == id)); + state = AsyncValue.data((inward: newInward, outward: outward)); + return repo .accept(id) .then((_) => repo.show(id).then((challenge) => challenge.gameFullId)); @@ -144,6 +153,10 @@ class Challenges extends _$Challenges { } void _notifyUser(Challenge challenge) { - // send a notification to the user + ref.read(localNotificationServiceProvider).show( + ChallengeNotification(challenge.id).build( + 'Challenge request from ${challenge.challenger!.user.name}', + ), + ); } } diff --git a/lib/src/model/notifications/challenge_notification.dart b/lib/src/model/notifications/challenge_notification.dart new file mode 100644 index 0000000000..b95026549c --- /dev/null +++ b/lib/src/model/notifications/challenge_notification.dart @@ -0,0 +1,107 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:lichess_mobile/l10n/l10n.dart'; +import 'package:lichess_mobile/l10n/l10n_en.dart'; +import 'package:lichess_mobile/src/model/challenge/challenge_repository.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/notifications/local_notification_service.dart'; +import 'package:lichess_mobile/src/navigation.dart'; +import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/view/game/game_screen.dart'; + +class ChallengeNotificationDetails { + ChallengeNotificationDetails(this._locale) { + ChallengeNotificationDetails.instance = this; + } + + // the default instance is set to english but this is overridden in LocalNotificationService.init() + static ChallengeNotificationDetails instance = + ChallengeNotificationDetails(AppLocalizationsEn()); + + final AppLocalizations _locale; + + NotificationDetails get notificationDetails => NotificationDetails( + android: AndroidNotificationDetails( + 'challenges', + _locale.preferencesNotifyChallenge, + importance: Importance.max, + priority: Priority.high, + actions: [ + AndroidNotificationAction( + 'accept', + _locale.accept, + icon: const DrawableResourceAndroidBitmap('tick'), + showsUserInterface: true, + contextual: true, + ), + AndroidNotificationAction( + 'decline', + _locale.decline, + icon: const DrawableResourceAndroidBitmap('cross'), + contextual: true, + ), + ], + ), + iOS: const DarwinNotificationDetails( + categoryIdentifier: 'challenge-notification', + ), + ); + + DarwinNotificationCategory get darwinNotificationCategory => + DarwinNotificationCategory( + 'challenge-notification', + actions: [ + DarwinNotificationAction.plain( + 'accept', + _locale.accept, + options: { + DarwinNotificationActionOption.foreground, + }, + ), + DarwinNotificationAction.plain( + 'decline', + _locale.decline, + options: { + DarwinNotificationActionOption.destructive, + }, + ), + ], + options: { + DarwinNotificationCategoryOption.hiddenPreviewShowTitle, + }, + ); +} + +class ChallengeNotification { + ChallengeNotification(this.id); + + final ChallengeId id; + + void _callback(String? actionId, LocalNotificationServiceRef ref) { + switch (actionId) { + case 'accept': + final repo = ref.read(challengesProvider.notifier); + repo.accept(id).then((fullId) { + pushPlatformRoute( + ref.read(currentNavigatorKeyProvider).currentContext!, + builder: (BuildContext context) => + GameScreen(initialGameId: fullId), + ); + }); + case 'decline': + ref.read(challengeRepositoryProvider).decline(id); + } + } + + NotificationCallback get callback => _callback; + + LocalNotification build(String title, {String? body}) { + return LocalNotification( + title: title, + body: body, + payload: 'challenge-notification:${id.value}', + notificationDetails: + ChallengeNotificationDetails.instance.notificationDetails, + ); + } +} diff --git a/lib/src/model/notifications/local_notification_service.dart b/lib/src/model/notifications/local_notification_service.dart index f7616b76bc..44001d1fad 100644 --- a/lib/src/model/notifications/local_notification_service.dart +++ b/lib/src/model/notifications/local_notification_service.dart @@ -4,6 +4,8 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/notifications/challenge_notification.dart'; +import 'package:lichess_mobile/src/model/notifications/info_notification.dart'; +import 'package:lichess_mobile/src/utils/l10n.dart'; import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -14,32 +16,49 @@ part 'local_notification_service.freezed.dart'; LocalNotificationService localNotificationService( LocalNotificationServiceRef ref, ) { - return LocalNotificationService.instance; + return LocalNotificationService(ref, Logger('LocalNotificationService')); } +typedef NotificationCallback = void Function( + String?, + LocalNotificationServiceRef, +); + class LocalNotificationService { - LocalNotificationService(this._log); - static final instance = - LocalNotificationService(Logger('LocalNotificationService')); + LocalNotificationService(this._ref, this._log); + static LocalNotificationService? instance; final _notificationPlugin = FlutterLocalNotificationsPlugin(); + final LocalNotificationServiceRef _ref; final Logger _log; int currentId = 0; Future init() async { + instance = this; + final l10n = _ref.watch(l10nProvider); + + // update localizations + InfoNotificationDetails(l10n.strings); + ChallengeNotificationDetails(l10n.strings); + await _notificationPlugin.initialize( - const InitializationSettings( - android: AndroidInitializationSettings('logo_black'), + InitializationSettings( + android: const AndroidInitializationSettings('logo_black'), iOS: DarwinInitializationSettings( requestAlertPermission: false, requestBadgePermission: false, requestSoundPermission: false, - notificationCategories: [], + notificationCategories: [ + ChallengeNotificationDetails.instance.darwinNotificationCategory, + ], ), ), onDidReceiveNotificationResponse: _notificationRespsonse, onDidReceiveBackgroundNotificationResponse: _notificationRespsonse, ); + _log.info( + '[Local Notifications] initialized', + ); } Future show(LocalNotification notification) async { @@ -51,17 +70,29 @@ class LocalNotificationService { notification.notificationDetails, payload: notification.payload, ); + _log.info( + '[Local Notifications] Sent notification: ($id | ${notification.title}) ${notification.body} (Payload: ${notification.payload})', + ); return id; } Future cancel(int id) async { + _log.info( + '[Local Notifications] canceled notification (id: $id)', + ); return _notificationPlugin.cancel(id); } + void call(NotificationCallback callback, String? actionId) => + callback(actionId, _ref); + @pragma('vm:entry-point') static void _notificationRespsonse(NotificationResponse response) { if (response.payload == null) return; + final service = LocalNotificationService.instance; + if (service == null) return; + try { final splits = response.payload!.split(':'); @@ -70,10 +101,10 @@ class LocalNotificationService { switch (id) { case 'challenge-notification': final notification = ChallengeNotification(ChallengeId(payload)); - notification.callback(response.actionId); + service.call(notification.callback, response.actionId); } } catch (error) { - LocalNotificationService.instance._log.warning( + Logger('LocalNotificationService').warning( 'Malformed notification payload: [ID: ${response.id}] ${response.payload}', ); } From b5c646de82d8a5d58497939077cdf45581da06da Mon Sep 17 00:00:00 2001 From: inc0g_ Date: Mon, 5 Aug 2024 23:50:14 +1000 Subject: [PATCH 220/979] Challenge Requests Screen: relocate and bugfix --- lib/src/model/challenge/challenge.dart | 4 +- .../model/challenge/challenge_repository.dart | 12 ++ .../notifications/challenge_notification.dart | 8 +- lib/src/view/home/home_tab_screen.dart | 41 +++- .../view/user/challenge_requests_screen.dart | 138 ++++++++++++ lib/src/view/user/player_screen.dart | 196 +----------------- lib/src/widgets/buttons.dart | 1 + lib/src/widgets/challenge_display.dart | 4 +- 8 files changed, 206 insertions(+), 198 deletions(-) create mode 100644 lib/src/view/user/challenge_requests_screen.dart diff --git a/lib/src/model/challenge/challenge.dart b/lib/src/model/challenge/challenge.dart index 3abb37c5de..0e7cc6babc 100644 --- a/lib/src/model/challenge/challenge.dart +++ b/lib/src/model/challenge/challenge.dart @@ -203,9 +203,9 @@ extension ChallengeExtension on Pick { } if (value is String) { switch (value) { - case 'outward': + case 'outward' || 'out': return ChallengeDirection.outward; - case 'inward': + case 'inward' || 'in': return ChallengeDirection.inward; default: throw PickException( diff --git a/lib/src/model/challenge/challenge_repository.dart b/lib/src/model/challenge/challenge_repository.dart index c3cedda566..32a103a083 100644 --- a/lib/src/model/challenge/challenge_repository.dart +++ b/lib/src/model/challenge/challenge_repository.dart @@ -9,6 +9,8 @@ import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/notifications/challenge_notification.dart'; import 'package:lichess_mobile/src/model/notifications/local_notification_service.dart'; +import 'package:lichess_mobile/src/navigation.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'challenge_repository.g.dart'; @@ -153,9 +155,19 @@ class Challenges extends _$Challenges { } void _notifyUser(Challenge challenge) { + final context = ref.read(currentNavigatorKeyProvider).currentContext; + + final time = challenge.days == null + ? '∞' + : '${context?.l10n.daysPerTurn}: ${challenge.days}'; + final body = challenge.rated + ? '${context?.l10n.rated} • $time' + : '${context?.l10n.casual} • $time'; + ref.read(localNotificationServiceProvider).show( ChallengeNotification(challenge.id).build( 'Challenge request from ${challenge.challenger!.user.name}', + body: context != null ? body : null, ), ); } diff --git a/lib/src/model/notifications/challenge_notification.dart b/lib/src/model/notifications/challenge_notification.dart index b95026549c..13b7e8a243 100644 --- a/lib/src/model/notifications/challenge_notification.dart +++ b/lib/src/model/notifications/challenge_notification.dart @@ -8,6 +8,7 @@ import 'package:lichess_mobile/src/model/notifications/local_notification_servic import 'package:lichess_mobile/src/navigation.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/game/game_screen.dart'; +import 'package:lichess_mobile/src/view/user/challenge_requests_screen.dart'; class ChallengeNotificationDetails { ChallengeNotificationDetails(this._locale) { @@ -79,7 +80,7 @@ class ChallengeNotification { void _callback(String? actionId, LocalNotificationServiceRef ref) { switch (actionId) { - case 'accept': + case 'accept': // accept the game and open board final repo = ref.read(challengesProvider.notifier); repo.accept(id).then((fullId) { pushPlatformRoute( @@ -88,6 +89,11 @@ class ChallengeNotification { GameScreen(initialGameId: fullId), ); }); + case null: // open the challenge screen + pushPlatformRoute( + ref.read(currentNavigatorKeyProvider).currentContext!, + builder: (BuildContext context) => const ChallengeRequestsScreen(), + ); case 'decline': ref.read(challengeRepositoryProvider).decline(id); } diff --git a/lib/src/view/home/home_tab_screen.dart b/lib/src/view/home/home_tab_screen.dart index 46138e9363..f02c4903b3 100644 --- a/lib/src/view/home/home_tab_screen.dart +++ b/lib/src/view/home/home_tab_screen.dart @@ -11,6 +11,7 @@ import 'package:lichess_mobile/src/model/correspondence/correspondence_game_stor import 'package:lichess_mobile/src/model/game/game_history.dart'; import 'package:lichess_mobile/src/model/settings/home_preferences.dart'; import 'package:lichess_mobile/src/navigation.dart'; +import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:lichess_mobile/src/utils/l10n.dart'; @@ -26,6 +27,7 @@ import 'package:lichess_mobile/src/view/play/ongoing_games_screen.dart'; import 'package:lichess_mobile/src/view/play/play_screen.dart'; import 'package:lichess_mobile/src/view/play/quick_game_button.dart'; import 'package:lichess_mobile/src/view/play/quick_game_matrix.dart'; +import 'package:lichess_mobile/src/view/user/challenge_requests_screen.dart'; import 'package:lichess_mobile/src/view/user/player_screen.dart'; import 'package:lichess_mobile/src/view/user/recent_games.dart'; import 'package:lichess_mobile/src/widgets/board_carousel_item.dart'; @@ -88,6 +90,7 @@ class _HomeScreenState extends ConsumerState with RouteAware { icon: Icon(isEditing ? Icons.save : Icons.app_registration), tooltip: isEditing ? 'Save' : 'Edit', ), + const _ChallengeScreenButton(), const _PlayerScreenButton(), ], ), @@ -924,11 +927,9 @@ class _PlayerScreenButton extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final connectivity = ref.watch(connectivityChangesProvider); - final challenges = ref.watch(challengesProvider); - final count = challenges.valueOrNull?.inward.length; return connectivity.maybeWhen( - data: (connectivity) => AppBarNotificationIconButton( + data: (connectivity) => AppBarIconButton( icon: const Icon(Icons.group), semanticsLabel: context.l10n.players, onPressed: !connectivity.isOnline @@ -940,7 +941,6 @@ class _PlayerScreenButton extends ConsumerWidget { builder: (_) => const PlayerScreen(), ); }, - count: count ?? 0, ), orElse: () => AppBarIconButton( icon: const Icon(Icons.group), @@ -950,3 +950,36 @@ class _PlayerScreenButton extends ConsumerWidget { ); } } + +class _ChallengeScreenButton extends ConsumerWidget { + const _ChallengeScreenButton(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final connectivity = ref.watch(connectivityChangesProvider); + final challenges = ref.watch(challengesProvider); + final count = challenges.valueOrNull?.inward.length; + + return connectivity.maybeWhen( + data: (connectivity) => AppBarNotificationIconButton( + icon: const Icon(LichessIcons.crossed_swords), + semanticsLabel: context.l10n.preferencesNotifyChallenge, + onPressed: !connectivity.isOnline + ? null + : () { + pushPlatformRoute( + context, + title: context.l10n.preferencesNotifyChallenge, + builder: (_) => const ChallengeRequestsScreen(), + ); + }, + count: count ?? 0, + ), + orElse: () => AppBarIconButton( + icon: const Icon(LichessIcons.crossed_swords), + semanticsLabel: context.l10n.preferencesNotifyChallenge, + onPressed: null, + ), + ); + } +} diff --git a/lib/src/view/user/challenge_requests_screen.dart b/lib/src/view/user/challenge_requests_screen.dart new file mode 100644 index 0000000000..8e47725ae5 --- /dev/null +++ b/lib/src/view/user/challenge_requests_screen.dart @@ -0,0 +1,138 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/auth/auth_session.dart'; +import 'package:lichess_mobile/src/model/challenge/challenge.dart'; +import 'package:lichess_mobile/src/model/challenge/challenge_repository.dart'; +import 'package:lichess_mobile/src/styles/lichess_colors.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/view/game/game_screen.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; +import 'package:lichess_mobile/src/widgets/challenge_display.dart'; +import 'package:lichess_mobile/src/widgets/feedback.dart'; +import 'package:lichess_mobile/src/widgets/list.dart'; +import 'package:lichess_mobile/src/widgets/platform.dart'; + +class ChallengeRequestsScreen extends ConsumerWidget { + const ChallengeRequestsScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return PlatformWidget( + androidBuilder: _androidBuilder, + iosBuilder: _iosBuilder, + ); + } + + Widget _androidBuilder(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(context.l10n.preferencesNotifyChallenge), + ), + body: _Body(), + ); + } + + Widget _iosBuilder(BuildContext context) { + return CupertinoPageScaffold( + navigationBar: const CupertinoNavigationBar(), + child: _Body(), + ); + } +} + +class _Body extends ConsumerWidget { + @override + Widget build(BuildContext context, WidgetRef ref) { + final challengeNotifier = ref.read(challengesProvider.notifier); + final challenges = ref.watch(challengesProvider); + final session = ref.watch(authSessionProvider); + + return challenges.when( + data: (challenges) { + final list = challenges.inward.addAll(challenges.outward); + + return list.isEmpty + ? Center(child: Text(context.l10n.nothingToSeeHere)) + : ListView.separated( + itemCount: list.length, + separatorBuilder: (context, index) => + const PlatformDivider(height: 1, cupertinoHasLeading: true), + itemBuilder: (context, index) { + final challenge = list[index]; + + // not sure why there wouldn't be a challenger + final user = challenge.challenger?.user; + if (user == null) return null; + + final time = challenge.days == null + ? '∞' + : '${context.l10n.daysPerTurn}: ${challenge.days}'; + final subtitle = challenge.rated + ? '${context.l10n.rated} • $time' + : '${context.l10n.casual} • $time'; + final isMyChallenge = + challenge.direction == ChallengeDirection.outward; + + return ChallengeDisplay( + challenge: challenge, + user: user, + subtitle: subtitle, + color: isMyChallenge + ? LichessColors.green.withOpacity(0.2) + : null, + onPressed: isMyChallenge + ? null + : session == null + ? () { + showPlatformSnackbar( + context, + context.l10n.youNeedAnAccountToDoThat, + ); + } + : () { + showConfirmDialog( + context, + title: Text(context.l10n.accept), + isDestructiveAction: true, + onConfirm: (_) { + challengeNotifier + .accept(challenge.id) + .then((id) { + if (!context.mounted) return; + pushPlatformRoute( + context, + rootNavigator: true, + builder: (BuildContext context) { + return GameScreen( + initialGameId: id, + ); + }, + ); + }); + }, + ); + }, + cancelText: !isMyChallenge ? context.l10n.decline : null, + onCancel: isMyChallenge + ? () { + challengeNotifier.cancel(challenge.id); + } + : () { + ref + .read(challengeRepositoryProvider) + .decline(challenge.id); + }, + ); + }, + ); + }, + loading: () { + return const Center(child: CircularProgressIndicator.adaptive()); + }, + error: (error, stack) => + const Center(child: Text('Error loading challenges')), + ); + } +} diff --git a/lib/src/view/user/player_screen.dart b/lib/src/view/user/player_screen.dart index c904219ba2..4719e6926e 100644 --- a/lib/src/view/user/player_screen.dart +++ b/lib/src/view/user/player_screen.dart @@ -3,32 +3,23 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; -import 'package:lichess_mobile/src/model/challenge/challenge.dart'; -import 'package:lichess_mobile/src/model/challenge/challenge_repository.dart'; import 'package:lichess_mobile/src/model/relation/online_friends.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; -import 'package:lichess_mobile/src/styles/lichess_colors.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/focus_detector.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/account/rating_pref_aware.dart'; -import 'package:lichess_mobile/src/view/game/game_screen.dart'; import 'package:lichess_mobile/src/view/relation/following_screen.dart'; import 'package:lichess_mobile/src/view/user/leaderboard_widget.dart'; import 'package:lichess_mobile/src/view/user/search_screen.dart'; import 'package:lichess_mobile/src/view/user/user_screen.dart'; -import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; -import 'package:lichess_mobile/src/widgets/challenge_display.dart'; -import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; import 'package:lichess_mobile/src/widgets/user_full_name.dart'; -enum _ViewMode { players, challenges } - class PlayerScreen extends ConsumerWidget { const PlayerScreen({super.key}); @@ -51,113 +42,23 @@ class PlayerScreen extends ConsumerWidget { } Widget _androidBuilder(BuildContext context) { - return const Scaffold( - body: _AndroidBody(), - ); - } - - Widget _iosBuilder(BuildContext context) { - return const CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar(), - child: _CupertinoBody(), - ); - } -} - -class _AndroidBody extends StatefulWidget { - const _AndroidBody(); - - @override - State<_AndroidBody> createState() => _AndriodBodyState(); -} - -class _AndriodBodyState extends State<_AndroidBody> - with TickerProviderStateMixin { - late final TabController _tabController; - - @override - void initState() { - super.initState(); - _tabController = TabController(length: 2, vsync: this); - } - - @override - void dispose() { - _tabController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(context.l10n.players), - bottom: TabBar( - controller: _tabController, - tabs: [ - Tab(text: context.l10n.friends), - Tab(text: context.l10n.preferencesNotifyChallenge), - ], - ), - ), - body: TabBarView( - controller: _tabController, - children: [ - _PlayersBody(), - _ChallengesBody(), - ], ), + body: _Body(), ); } -} - -class _CupertinoBody extends StatefulWidget { - const _CupertinoBody(); - @override - _CupertinoBodyState createState() => _CupertinoBodyState(); -} - -class _CupertinoBodyState extends State<_CupertinoBody> { - _ViewMode _selectedSegment = _ViewMode.players; - - @override - Widget build(BuildContext context) { - return SafeArea( - child: Column( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: Styles.bodyPadding, - child: CupertinoSlidingSegmentedControl<_ViewMode>( - groupValue: _selectedSegment, - children: { - _ViewMode.players: Text(context.l10n.players), - _ViewMode.challenges: - Text(context.l10n.preferencesNotifyChallenge), - }, - onValueChanged: (_ViewMode? view) { - if (view != null) { - setState(() { - _selectedSegment = view; - }); - } - }, - ), - ), - Expanded( - child: _selectedSegment == _ViewMode.players - ? _PlayersBody() - : _ChallengesBody(), - ), - ], - ), + Widget _iosBuilder(BuildContext context) { + return CupertinoPageScaffold( + navigationBar: const CupertinoNavigationBar(), + child: _Body(), ); } } -class _PlayersBody extends ConsumerWidget { +class _Body extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final session = ref.watch(authSessionProvider); @@ -177,91 +78,6 @@ class _PlayersBody extends ConsumerWidget { } } -class _ChallengesBody extends ConsumerWidget { - @override - Widget build(BuildContext context, WidgetRef ref) { - final challengeNotifier = ref.read(challengesProvider.notifier); - final challenges = ref.watch(challengesProvider); - final session = ref.watch(authSessionProvider); - - return challenges.when( - data: (challenges) { - final list = challenges.inward.addAll(challenges.outward); - - return ListView.separated( - itemCount: list.length, - separatorBuilder: (context, index) => - const PlatformDivider(height: 1, cupertinoHasLeading: true), - itemBuilder: (context, index) { - final challenge = list[index]; - final user = challenge.challenger?.user; - if (user == null) { - return null; // not sure why there wouldn't be a challenger - } - - final time = challenge.days == null - ? '∞' - : '${context.l10n.daysPerTurn}: ${challenge.days}'; - final subtitle = challenge.rated - ? '${context.l10n.rated} • $time' - : '${context.l10n.casual} • $time'; - final isMyChallenge = - challenge.direction == ChallengeDirection.outward; - - return ChallengeDisplay( - challenge: challenge, - user: user, - subtitle: subtitle, - color: - isMyChallenge ? LichessColors.green.withOpacity(0.2) : null, - onPressed: isMyChallenge - ? null - : session == null - ? () { - showPlatformSnackbar( - context, - context.l10n.youNeedAnAccountToDoThat, - ); - } - : () { - showConfirmDialog( - context, - title: Text(context.l10n.accept), - isDestructiveAction: true, - onConfirm: (_) { - challengeNotifier.accept(challenge.id).then((id) { - if (!context.mounted) return; - pushPlatformRoute( - context, - rootNavigator: true, - builder: (BuildContext context) { - return GameScreen( - initialGameId: id, - ); - }, - ); - }); - }, - ); - }, - onCancel: isMyChallenge - ? () { - challengeNotifier.cancel(challenge.id); - } - : null, - ); - }, - ); - }, - loading: () { - return const Center(child: CircularProgressIndicator.adaptive()); - }, - error: (error, stack) => - const Center(child: Text('Error loading challenges')), - ); - } -} - class AlwaysDisabledFocusNode extends FocusNode { @override bool get hasFocus => false; diff --git a/lib/src/widgets/buttons.dart b/lib/src/widgets/buttons.dart index 401ca33972..39de7d0a7f 100644 --- a/lib/src/widgets/buttons.dart +++ b/lib/src/widgets/buttons.dart @@ -213,6 +213,7 @@ class AppBarNotificationIconButton extends StatelessWidget { @override Widget build(BuildContext context) { return Stack( + alignment: AlignmentDirectional.center, children: [ AppBarIconButton( icon: icon, diff --git a/lib/src/widgets/challenge_display.dart b/lib/src/widgets/challenge_display.dart index f6fa3e0e9f..674c463b89 100644 --- a/lib/src/widgets/challenge_display.dart +++ b/lib/src/widgets/challenge_display.dart @@ -18,6 +18,7 @@ class ChallengeDisplay extends StatelessWidget { required this.challenge, required this.user, required this.subtitle, + this.cancelText, this.color, this.onPressed, this.onCancel, @@ -26,6 +27,7 @@ class ChallengeDisplay extends StatelessWidget { final Challenge challenge; final LightUser user; final String subtitle; + final String? cancelText; final Color? color; final VoidCallback? onPressed; final VoidCallback? onCancel; @@ -45,7 +47,7 @@ class ChallengeDisplay extends StatelessWidget { backgroundColor: context.lichessColors.error, foregroundColor: Colors.white, icon: Icons.cancel, - label: context.l10n.cancel, + label: cancelText ?? context.l10n.cancel, ), ], ) From 3787da43403054450522c963d125d779cad7edb5 Mon Sep 17 00:00:00 2001 From: inc0g_ Date: Thu, 8 Aug 2024 04:10:31 +1000 Subject: [PATCH 221/979] Info Notification: remove localisation string --- lib/src/model/notifications/info_notification.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/src/model/notifications/info_notification.dart b/lib/src/model/notifications/info_notification.dart index 2d3d59e8e8..f7f26f0267 100644 --- a/lib/src/model/notifications/info_notification.dart +++ b/lib/src/model/notifications/info_notification.dart @@ -12,12 +12,13 @@ class InfoNotificationDetails { static InfoNotificationDetails instance = InfoNotificationDetails(AppLocalizationsEn()); + // ignore: unused_field final AppLocalizations _locale; - NotificationDetails get notificationDetails => NotificationDetails( + NotificationDetails get notificationDetails => const NotificationDetails( android: AndroidNotificationDetails( 'general', - _locale.mobileGeneralNotificationCategory, + 'General', importance: Importance.high, priority: Priority.high, ), From 4ca5fb49052dfd267f9f3106e2cb15cb58081129 Mon Sep 17 00:00:00 2001 From: incog Date: Sun, 21 Jul 2024 08:33:43 +1000 Subject: [PATCH 222/979] Home Screen: add notification bubble to community icon --- lib/src/view/home/home_tab_screen.dart | 5 +- lib/src/view/user/player_screen.dart | 70 ++++++++++++++++++++++---- 2 files changed, 65 insertions(+), 10 deletions(-) diff --git a/lib/src/view/home/home_tab_screen.dart b/lib/src/view/home/home_tab_screen.dart index f02c4903b3..7f783b1f22 100644 --- a/lib/src/view/home/home_tab_screen.dart +++ b/lib/src/view/home/home_tab_screen.dart @@ -927,9 +927,11 @@ class _PlayerScreenButton extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final connectivity = ref.watch(connectivityChangesProvider); + final challenges = ref.watch(challengesListProvider); + final count = challenges.valueOrNull?.inward.length; return connectivity.maybeWhen( - data: (connectivity) => AppBarIconButton( + data: (connectivity) => AppBarNotificationIconButton( icon: const Icon(Icons.group), semanticsLabel: context.l10n.players, onPressed: !connectivity.isOnline @@ -941,6 +943,7 @@ class _PlayerScreenButton extends ConsumerWidget { builder: (_) => const PlayerScreen(), ); }, + count: count ?? 0, ), orElse: () => AppBarIconButton( icon: const Icon(Icons.group), diff --git a/lib/src/view/user/player_screen.dart b/lib/src/view/user/player_screen.dart index 4719e6926e..750348101f 100644 --- a/lib/src/view/user/player_screen.dart +++ b/lib/src/view/user/player_screen.dart @@ -42,23 +42,68 @@ class PlayerScreen extends ConsumerWidget { } Widget _androidBuilder(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(context.l10n.players), - ), - body: _Body(), + return const Scaffold( + body: _AndroidBody(), ); } Widget _iosBuilder(BuildContext context) { - return CupertinoPageScaffold( - navigationBar: const CupertinoNavigationBar(), - child: _Body(), + // return CupertinoPageScaffold( + // navigationBar: const CupertinoNavigationBar(), + // child: _Body(), + // ); + return const Placeholder(); + } +} + +class _AndroidBody extends StatefulWidget { + const _AndroidBody(); + + @override + State<_AndroidBody> createState() => _AndriodBodyState(); +} + +class _AndriodBodyState extends State<_AndroidBody> + with TickerProviderStateMixin { + late final TabController _tabController; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 2, vsync: this); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(context.l10n.players), + bottom: TabBar( + controller: _tabController, + tabs: [ + Tab(text: context.l10n.friends), + Tab(text: context.l10n.preferencesNotifyChallenge), + ], + ), + ), + body: TabBarView( + controller: _tabController, + children: [ + _PlayersBody(), + _ChallengesBody(), + ], + ), ); } } -class _Body extends ConsumerWidget { +class _PlayersBody extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final session = ref.watch(authSessionProvider); @@ -78,6 +123,13 @@ class _Body extends ConsumerWidget { } } +class _ChallengesBody extends ConsumerWidget { + @override + Widget build(BuildContext context, WidgetRef ref) { + return const Placeholder(); + } +} + class AlwaysDisabledFocusNode extends FocusNode { @override bool get hasFocus => false; From b12ecf71d6b1223ec65ac78798ae972328ca8b92 Mon Sep 17 00:00:00 2001 From: incog Date: Mon, 22 Jul 2024 05:01:35 +1000 Subject: [PATCH 223/979] Player Screen: finish converting to stateful widget --- lib/src/view/user/player_screen.dart | 57 +++++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 5 deletions(-) diff --git a/lib/src/view/user/player_screen.dart b/lib/src/view/user/player_screen.dart index 750348101f..1ec825410c 100644 --- a/lib/src/view/user/player_screen.dart +++ b/lib/src/view/user/player_screen.dart @@ -20,6 +20,8 @@ import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; import 'package:lichess_mobile/src/widgets/user_full_name.dart'; +enum _ViewMode { players, challenges } + class PlayerScreen extends ConsumerWidget { const PlayerScreen({super.key}); @@ -48,11 +50,10 @@ class PlayerScreen extends ConsumerWidget { } Widget _iosBuilder(BuildContext context) { - // return CupertinoPageScaffold( - // navigationBar: const CupertinoNavigationBar(), - // child: _Body(), - // ); - return const Placeholder(); + return const CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar(), + child: _CupertinoBody(), + ); } } @@ -103,6 +104,52 @@ class _AndriodBodyState extends State<_AndroidBody> } } +class _CupertinoBody extends StatefulWidget { + const _CupertinoBody(); + + @override + _CupertinoBodyState createState() => _CupertinoBodyState(); +} + +class _CupertinoBodyState extends State<_CupertinoBody> { + _ViewMode _selectedSegment = _ViewMode.players; + + @override + Widget build(BuildContext context) { + return SafeArea( + child: Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: Styles.bodyPadding, + child: CupertinoSlidingSegmentedControl<_ViewMode>( + groupValue: _selectedSegment, + children: { + _ViewMode.players: Text(context.l10n.players), + _ViewMode.challenges: + Text(context.l10n.preferencesNotifyChallenge), + }, + onValueChanged: (_ViewMode? view) { + if (view != null) { + setState(() { + _selectedSegment = view; + }); + } + }, + ), + ), + Expanded( + child: _selectedSegment == _ViewMode.players + ? _PlayersBody() + : _ChallengesBody(), + ), + ], + ), + ); + } +} + class _PlayersBody extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { From a95c03ea0a6f13e147f1452a60584c0cca04cfbb Mon Sep 17 00:00:00 2001 From: inc0g_ Date: Thu, 1 Aug 2024 11:09:08 +1000 Subject: [PATCH 224/979] Challenge Repository: Add ChallengesProvider --- lib/src/view/home/home_tab_screen.dart | 2 +- lib/src/view/user/player_screen.dart | 87 +++++++++++++++++++++++++- 2 files changed, 87 insertions(+), 2 deletions(-) diff --git a/lib/src/view/home/home_tab_screen.dart b/lib/src/view/home/home_tab_screen.dart index 7f783b1f22..3936eecdb7 100644 --- a/lib/src/view/home/home_tab_screen.dart +++ b/lib/src/view/home/home_tab_screen.dart @@ -927,7 +927,7 @@ class _PlayerScreenButton extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final connectivity = ref.watch(connectivityChangesProvider); - final challenges = ref.watch(challengesListProvider); + final challenges = ref.watch(challengesProvider); final count = challenges.valueOrNull?.inward.length; return connectivity.maybeWhen( diff --git a/lib/src/view/user/player_screen.dart b/lib/src/view/user/player_screen.dart index 1ec825410c..c904219ba2 100644 --- a/lib/src/view/user/player_screen.dart +++ b/lib/src/view/user/player_screen.dart @@ -3,18 +3,25 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; +import 'package:lichess_mobile/src/model/challenge/challenge.dart'; +import 'package:lichess_mobile/src/model/challenge/challenge_repository.dart'; import 'package:lichess_mobile/src/model/relation/online_friends.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; +import 'package:lichess_mobile/src/styles/lichess_colors.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/focus_detector.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/account/rating_pref_aware.dart'; +import 'package:lichess_mobile/src/view/game/game_screen.dart'; import 'package:lichess_mobile/src/view/relation/following_screen.dart'; import 'package:lichess_mobile/src/view/user/leaderboard_widget.dart'; import 'package:lichess_mobile/src/view/user/search_screen.dart'; import 'package:lichess_mobile/src/view/user/user_screen.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/challenge_display.dart'; +import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; @@ -173,7 +180,85 @@ class _PlayersBody extends ConsumerWidget { class _ChallengesBody extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return const Placeholder(); + final challengeNotifier = ref.read(challengesProvider.notifier); + final challenges = ref.watch(challengesProvider); + final session = ref.watch(authSessionProvider); + + return challenges.when( + data: (challenges) { + final list = challenges.inward.addAll(challenges.outward); + + return ListView.separated( + itemCount: list.length, + separatorBuilder: (context, index) => + const PlatformDivider(height: 1, cupertinoHasLeading: true), + itemBuilder: (context, index) { + final challenge = list[index]; + final user = challenge.challenger?.user; + if (user == null) { + return null; // not sure why there wouldn't be a challenger + } + + final time = challenge.days == null + ? '∞' + : '${context.l10n.daysPerTurn}: ${challenge.days}'; + final subtitle = challenge.rated + ? '${context.l10n.rated} • $time' + : '${context.l10n.casual} • $time'; + final isMyChallenge = + challenge.direction == ChallengeDirection.outward; + + return ChallengeDisplay( + challenge: challenge, + user: user, + subtitle: subtitle, + color: + isMyChallenge ? LichessColors.green.withOpacity(0.2) : null, + onPressed: isMyChallenge + ? null + : session == null + ? () { + showPlatformSnackbar( + context, + context.l10n.youNeedAnAccountToDoThat, + ); + } + : () { + showConfirmDialog( + context, + title: Text(context.l10n.accept), + isDestructiveAction: true, + onConfirm: (_) { + challengeNotifier.accept(challenge.id).then((id) { + if (!context.mounted) return; + pushPlatformRoute( + context, + rootNavigator: true, + builder: (BuildContext context) { + return GameScreen( + initialGameId: id, + ); + }, + ); + }); + }, + ); + }, + onCancel: isMyChallenge + ? () { + challengeNotifier.cancel(challenge.id); + } + : null, + ); + }, + ); + }, + loading: () { + return const Center(child: CircularProgressIndicator.adaptive()); + }, + error: (error, stack) => + const Center(child: Text('Error loading challenges')), + ); } } From f3dc8982511fbddac3ffb985b011cab6cfe20320 Mon Sep 17 00:00:00 2001 From: inc0g_ Date: Mon, 5 Aug 2024 23:50:14 +1000 Subject: [PATCH 225/979] Challenge Requests Screen: relocate and bugfix --- lib/src/view/home/home_tab_screen.dart | 5 +- lib/src/view/user/player_screen.dart | 196 +------------------------ 2 files changed, 7 insertions(+), 194 deletions(-) diff --git a/lib/src/view/home/home_tab_screen.dart b/lib/src/view/home/home_tab_screen.dart index 3936eecdb7..f02c4903b3 100644 --- a/lib/src/view/home/home_tab_screen.dart +++ b/lib/src/view/home/home_tab_screen.dart @@ -927,11 +927,9 @@ class _PlayerScreenButton extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final connectivity = ref.watch(connectivityChangesProvider); - final challenges = ref.watch(challengesProvider); - final count = challenges.valueOrNull?.inward.length; return connectivity.maybeWhen( - data: (connectivity) => AppBarNotificationIconButton( + data: (connectivity) => AppBarIconButton( icon: const Icon(Icons.group), semanticsLabel: context.l10n.players, onPressed: !connectivity.isOnline @@ -943,7 +941,6 @@ class _PlayerScreenButton extends ConsumerWidget { builder: (_) => const PlayerScreen(), ); }, - count: count ?? 0, ), orElse: () => AppBarIconButton( icon: const Icon(Icons.group), diff --git a/lib/src/view/user/player_screen.dart b/lib/src/view/user/player_screen.dart index c904219ba2..4719e6926e 100644 --- a/lib/src/view/user/player_screen.dart +++ b/lib/src/view/user/player_screen.dart @@ -3,32 +3,23 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; -import 'package:lichess_mobile/src/model/challenge/challenge.dart'; -import 'package:lichess_mobile/src/model/challenge/challenge_repository.dart'; import 'package:lichess_mobile/src/model/relation/online_friends.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; -import 'package:lichess_mobile/src/styles/lichess_colors.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/focus_detector.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/account/rating_pref_aware.dart'; -import 'package:lichess_mobile/src/view/game/game_screen.dart'; import 'package:lichess_mobile/src/view/relation/following_screen.dart'; import 'package:lichess_mobile/src/view/user/leaderboard_widget.dart'; import 'package:lichess_mobile/src/view/user/search_screen.dart'; import 'package:lichess_mobile/src/view/user/user_screen.dart'; -import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; -import 'package:lichess_mobile/src/widgets/challenge_display.dart'; -import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; import 'package:lichess_mobile/src/widgets/user_full_name.dart'; -enum _ViewMode { players, challenges } - class PlayerScreen extends ConsumerWidget { const PlayerScreen({super.key}); @@ -51,113 +42,23 @@ class PlayerScreen extends ConsumerWidget { } Widget _androidBuilder(BuildContext context) { - return const Scaffold( - body: _AndroidBody(), - ); - } - - Widget _iosBuilder(BuildContext context) { - return const CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar(), - child: _CupertinoBody(), - ); - } -} - -class _AndroidBody extends StatefulWidget { - const _AndroidBody(); - - @override - State<_AndroidBody> createState() => _AndriodBodyState(); -} - -class _AndriodBodyState extends State<_AndroidBody> - with TickerProviderStateMixin { - late final TabController _tabController; - - @override - void initState() { - super.initState(); - _tabController = TabController(length: 2, vsync: this); - } - - @override - void dispose() { - _tabController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(context.l10n.players), - bottom: TabBar( - controller: _tabController, - tabs: [ - Tab(text: context.l10n.friends), - Tab(text: context.l10n.preferencesNotifyChallenge), - ], - ), - ), - body: TabBarView( - controller: _tabController, - children: [ - _PlayersBody(), - _ChallengesBody(), - ], ), + body: _Body(), ); } -} - -class _CupertinoBody extends StatefulWidget { - const _CupertinoBody(); - @override - _CupertinoBodyState createState() => _CupertinoBodyState(); -} - -class _CupertinoBodyState extends State<_CupertinoBody> { - _ViewMode _selectedSegment = _ViewMode.players; - - @override - Widget build(BuildContext context) { - return SafeArea( - child: Column( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: Styles.bodyPadding, - child: CupertinoSlidingSegmentedControl<_ViewMode>( - groupValue: _selectedSegment, - children: { - _ViewMode.players: Text(context.l10n.players), - _ViewMode.challenges: - Text(context.l10n.preferencesNotifyChallenge), - }, - onValueChanged: (_ViewMode? view) { - if (view != null) { - setState(() { - _selectedSegment = view; - }); - } - }, - ), - ), - Expanded( - child: _selectedSegment == _ViewMode.players - ? _PlayersBody() - : _ChallengesBody(), - ), - ], - ), + Widget _iosBuilder(BuildContext context) { + return CupertinoPageScaffold( + navigationBar: const CupertinoNavigationBar(), + child: _Body(), ); } } -class _PlayersBody extends ConsumerWidget { +class _Body extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final session = ref.watch(authSessionProvider); @@ -177,91 +78,6 @@ class _PlayersBody extends ConsumerWidget { } } -class _ChallengesBody extends ConsumerWidget { - @override - Widget build(BuildContext context, WidgetRef ref) { - final challengeNotifier = ref.read(challengesProvider.notifier); - final challenges = ref.watch(challengesProvider); - final session = ref.watch(authSessionProvider); - - return challenges.when( - data: (challenges) { - final list = challenges.inward.addAll(challenges.outward); - - return ListView.separated( - itemCount: list.length, - separatorBuilder: (context, index) => - const PlatformDivider(height: 1, cupertinoHasLeading: true), - itemBuilder: (context, index) { - final challenge = list[index]; - final user = challenge.challenger?.user; - if (user == null) { - return null; // not sure why there wouldn't be a challenger - } - - final time = challenge.days == null - ? '∞' - : '${context.l10n.daysPerTurn}: ${challenge.days}'; - final subtitle = challenge.rated - ? '${context.l10n.rated} • $time' - : '${context.l10n.casual} • $time'; - final isMyChallenge = - challenge.direction == ChallengeDirection.outward; - - return ChallengeDisplay( - challenge: challenge, - user: user, - subtitle: subtitle, - color: - isMyChallenge ? LichessColors.green.withOpacity(0.2) : null, - onPressed: isMyChallenge - ? null - : session == null - ? () { - showPlatformSnackbar( - context, - context.l10n.youNeedAnAccountToDoThat, - ); - } - : () { - showConfirmDialog( - context, - title: Text(context.l10n.accept), - isDestructiveAction: true, - onConfirm: (_) { - challengeNotifier.accept(challenge.id).then((id) { - if (!context.mounted) return; - pushPlatformRoute( - context, - rootNavigator: true, - builder: (BuildContext context) { - return GameScreen( - initialGameId: id, - ); - }, - ); - }); - }, - ); - }, - onCancel: isMyChallenge - ? () { - challengeNotifier.cancel(challenge.id); - } - : null, - ); - }, - ); - }, - loading: () { - return const Center(child: CircularProgressIndicator.adaptive()); - }, - error: (error, stack) => - const Center(child: Text('Error loading challenges')), - ); - } -} - class AlwaysDisabledFocusNode extends FocusNode { @override bool get hasFocus => false; From f16951f665b9a2898cdc3c2891bca6f75366e144 Mon Sep 17 00:00:00 2001 From: inc0g_ Date: Wed, 7 Aug 2024 14:44:25 +1000 Subject: [PATCH 226/979] Challenge Repository: fix provider dependency --- .../model/challenge/challenge_repository.dart | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/lib/src/model/challenge/challenge_repository.dart b/lib/src/model/challenge/challenge_repository.dart index 32a103a083..7a2eef6803 100644 --- a/lib/src/model/challenge/challenge_repository.dart +++ b/lib/src/model/challenge/challenge_repository.dart @@ -9,8 +9,7 @@ import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/notifications/challenge_notification.dart'; import 'package:lichess_mobile/src/model/notifications/local_notification_service.dart'; -import 'package:lichess_mobile/src/navigation.dart'; -import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/l10n.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'challenge_repository.g.dart'; @@ -155,19 +154,17 @@ class Challenges extends _$Challenges { } void _notifyUser(Challenge challenge) { - final context = ref.read(currentNavigatorKeyProvider).currentContext; + final l10n = ref.read(l10nProvider).strings; - final time = challenge.days == null - ? '∞' - : '${context?.l10n.daysPerTurn}: ${challenge.days}'; - final body = challenge.rated - ? '${context?.l10n.rated} • $time' - : '${context?.l10n.casual} • $time'; + final time = + challenge.days == null ? '∞' : '${l10n.daysPerTurn}: ${challenge.days}'; + final body = + challenge.rated ? '${l10n.rated} • $time' : '${l10n.casual} • $time'; ref.read(localNotificationServiceProvider).show( ChallengeNotification(challenge.id).build( 'Challenge request from ${challenge.challenger!.user.name}', - body: context != null ? body : null, + body: body, ), ); } From 3d68d3b64316f7de8840dbf12a0b146f85755c1e Mon Sep 17 00:00:00 2001 From: inc0g_ Date: Wed, 7 Aug 2024 16:07:00 +1000 Subject: [PATCH 227/979] Local Notifications: send messages between isolates --- .../local_notification_service.dart | 82 ++++++++++++------- 1 file changed, 53 insertions(+), 29 deletions(-) diff --git a/lib/src/model/notifications/local_notification_service.dart b/lib/src/model/notifications/local_notification_service.dart index 44001d1fad..57bc39797c 100644 --- a/lib/src/model/notifications/local_notification_service.dart +++ b/lib/src/model/notifications/local_notification_service.dart @@ -1,8 +1,10 @@ import 'dart:async'; +import 'dart:isolate'; +import 'dart:ui'; +import 'package:flutter/foundation.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/notifications/challenge_notification.dart'; import 'package:lichess_mobile/src/model/notifications/info_notification.dart'; import 'package:lichess_mobile/src/utils/l10n.dart'; @@ -28,9 +30,11 @@ class LocalNotificationService { LocalNotificationService(this._ref, this._log); static LocalNotificationService? instance; - final _notificationPlugin = FlutterLocalNotificationsPlugin(); final LocalNotificationServiceRef _ref; final Logger _log; + final _notificationPlugin = FlutterLocalNotificationsPlugin(); + final _receivePort = ReceivePort(); + int currentId = 0; Future init() async { @@ -41,6 +45,12 @@ class LocalNotificationService { InfoNotificationDetails(l10n.strings); ChallengeNotificationDetails(l10n.strings); + IsolateNameServer.registerPortWithName( + _receivePort.sendPort, + 'localNotificationServicePort', + ); + _receivePort.listen(_onReceivePortMessage); + await _notificationPlugin.initialize( InitializationSettings( android: const AndroidInitializationSettings('logo_black'), @@ -53,12 +63,11 @@ class LocalNotificationService { ], ), ), - onDidReceiveNotificationResponse: _notificationRespsonse, - onDidReceiveBackgroundNotificationResponse: _notificationRespsonse, - ); - _log.info( - '[Local Notifications] initialized', + onDidReceiveNotificationResponse: _notificationResponse, + onDidReceiveBackgroundNotificationResponse: + _notificationBackgroundResponse, ); + _log.info('[Local Notifications] initialized'); } Future show(LocalNotification notification) async { @@ -77,38 +86,53 @@ class LocalNotificationService { } Future cancel(int id) async { - _log.info( - '[Local Notifications] canceled notification (id: $id)', - ); + _log.info('[Local Notifications] canceled notification (id: $id)'); return _notificationPlugin.cancel(id); } void call(NotificationCallback callback, String? actionId) => callback(actionId, _ref); - @pragma('vm:entry-point') - static void _notificationRespsonse(NotificationResponse response) { - if (response.payload == null) return; - - final service = LocalNotificationService.instance; - if (service == null) return; - + void _onReceivePortMessage(dynamic message) { try { - final splits = response.payload!.split(':'); - - final id = splits[0]; - final payload = splits[1]; - switch (id) { - case 'challenge-notification': - final notification = ChallengeNotification(ChallengeId(payload)); - service.call(notification.callback, response.actionId); - } - } catch (error) { - Logger('LocalNotificationService').warning( - 'Malformed notification payload: [ID: ${response.id}] ${response.payload}', + final data = message as Map; + final response = NotificationResponse( + notificationResponseType: + NotificationResponseType.values[data['index']! as int], + id: data['id'] as int?, + actionId: data['actionId'] as String?, + input: data['input'] as String?, + payload: data['payload'] as String?, + ); + _notificationResponse(response); + } catch (e) { + _log.severe( + '[Local Notifications] failed to parse message from background isoalte', ); } } + + void _notificationResponse(NotificationResponse response) { + _log.info('[Local Notifcations] recieved notification resopnse'); + debugPrint('hello!'); + + if (response.payload == null) return; + } + + @pragma('vm:entry-point') + static void _notificationBackgroundResponse(NotificationResponse response) { + final sendPort = + IsolateNameServer.lookupPortByName('localNotificationServicePort'); + sendPort!.send( + { + 'index': response.notificationResponseType.index, + 'id': response.id, + 'actionId': response.actionId, + 'input': response.input, + 'payload': response.payload, + }, + ); + } } @freezed From 5fa843c60446b2a074d7948c9285d44b4cc59784 Mon Sep 17 00:00:00 2001 From: inc0g_ Date: Wed, 7 Aug 2024 17:18:10 +1000 Subject: [PATCH 228/979] Local Notifiations: change api --- .../model/challenge/challenge_repository.dart | 71 +++++++++++++----- .../notifications/challenge_notification.dart | 74 +++++++++---------- .../notifications/info_notification.dart | 13 +--- .../local_notification_service.dart | 51 +++++++------ 4 files changed, 120 insertions(+), 89 deletions(-) diff --git a/lib/src/model/challenge/challenge_repository.dart b/lib/src/model/challenge/challenge_repository.dart index 7a2eef6803..904162a4c1 100644 --- a/lib/src/model/challenge/challenge_repository.dart +++ b/lib/src/model/challenge/challenge_repository.dart @@ -1,7 +1,9 @@ import 'dart:async'; +import 'package:collection/collection.dart'; import 'package:deep_pick/deep_pick.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/widgets.dart'; import 'package:http/http.dart' as http; import 'package:lichess_mobile/src/model/challenge/challenge.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; @@ -9,7 +11,11 @@ import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/notifications/challenge_notification.dart'; import 'package:lichess_mobile/src/model/notifications/local_notification_service.dart'; +import 'package:lichess_mobile/src/navigation.dart'; import 'package:lichess_mobile/src/utils/l10n.dart'; +import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/view/game/game_screen.dart'; +import 'package:lichess_mobile/src/view/user/challenge_requests_screen.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'challenge_repository.g.dart'; @@ -119,18 +125,19 @@ class Challenges extends _$Challenges { Future accept(ChallengeId id) async { final repo = ref.read(challengeRepositoryProvider); - - final inward = state.value!.inward; - final outward = state.value!.outward; - final newInward = - inward.remove(inward.firstWhere((challenge) => challenge.id == id)); - state = AsyncValue.data((inward: newInward, outward: outward)); + _stateRemove(id); return repo .accept(id) .then((_) => repo.show(id).then((challenge) => challenge.gameFullId)); } + Future decline(ChallengeId id) async { + final repo = ref.read(challengeRepositoryProvider); + _stateRemove(id); + return repo.decline(id); + } + void cancel(ChallengeId id) { ref.read(challengeRepositoryProvider).cancel(id); } @@ -153,19 +160,49 @@ class Challenges extends _$Challenges { state = AsyncValue.data((inward: inward.lock, outward: outward.lock)); } + void _stateRemove(ChallengeId id) { + final inward = state.value!.inward; + final outward = state.value!.outward; + final challengeIn = + inward.firstWhereOrNull((challenge) => challenge.id == id); + final challengeOut = + outward.firstWhereOrNull((challenge) => challenge.id == id); + + final newInward = challengeIn == null ? inward : inward.remove(challengeIn); + final newOutward = + challengeOut == null ? outward : outward.remove(challengeOut); + state = AsyncValue.data((inward: newInward, outward: newOutward)); + } + void _notifyUser(Challenge challenge) { final l10n = ref.read(l10nProvider).strings; - final time = - challenge.days == null ? '∞' : '${l10n.daysPerTurn}: ${challenge.days}'; - final body = - challenge.rated ? '${l10n.rated} • $time' : '${l10n.casual} • $time'; - - ref.read(localNotificationServiceProvider).show( - ChallengeNotification(challenge.id).build( - 'Challenge request from ${challenge.challenger!.user.name}', - body: body, - ), - ); + ref.read(localNotificationServiceProvider).show(ChallengeNotification( + challenge, + l10n, + onPressed: (action, id) { + switch (action) { + case ChallengeNotificationAction + .accept: // accept the game and open board + final repo = ref.read(challengesProvider.notifier); + repo.accept(id).then((fullId) { + pushPlatformRoute( + ref.read(currentNavigatorKeyProvider).currentContext!, + builder: (BuildContext context) => + GameScreen(initialGameId: fullId), + ); + }); + case ChallengeNotificationAction + .pressed: // open the challenge screen + pushPlatformRoute( + ref.read(currentNavigatorKeyProvider).currentContext!, + builder: (BuildContext context) => + const ChallengeRequestsScreen(), + ); + case ChallengeNotificationAction.decline: + decline(id); + } + }, + )); } } diff --git a/lib/src/model/notifications/challenge_notification.dart b/lib/src/model/notifications/challenge_notification.dart index 13b7e8a243..8aea417626 100644 --- a/lib/src/model/notifications/challenge_notification.dart +++ b/lib/src/model/notifications/challenge_notification.dart @@ -1,14 +1,9 @@ -import 'package:flutter/material.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:lichess_mobile/l10n/l10n.dart'; import 'package:lichess_mobile/l10n/l10n_en.dart'; -import 'package:lichess_mobile/src/model/challenge/challenge_repository.dart'; +import 'package:lichess_mobile/src/model/challenge/challenge.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/notifications/local_notification_service.dart'; -import 'package:lichess_mobile/src/navigation.dart'; -import 'package:lichess_mobile/src/utils/navigation.dart'; -import 'package:lichess_mobile/src/view/game/game_screen.dart'; -import 'package:lichess_mobile/src/view/user/challenge_requests_screen.dart'; class ChallengeNotificationDetails { ChallengeNotificationDetails(this._locale) { @@ -73,41 +68,44 @@ class ChallengeNotificationDetails { ); } -class ChallengeNotification { - ChallengeNotification(this.id); +enum ChallengeNotificationAction { accept, decline, pressed } - final ChallengeId id; - - void _callback(String? actionId, LocalNotificationServiceRef ref) { - switch (actionId) { - case 'accept': // accept the game and open board - final repo = ref.read(challengesProvider.notifier); - repo.accept(id).then((fullId) { - pushPlatformRoute( - ref.read(currentNavigatorKeyProvider).currentContext!, - builder: (BuildContext context) => - GameScreen(initialGameId: fullId), - ); - }); - case null: // open the challenge screen - pushPlatformRoute( - ref.read(currentNavigatorKeyProvider).currentContext!, - builder: (BuildContext context) => const ChallengeRequestsScreen(), +class ChallengeNotification extends LocalNotification + implements LocalNotificationCallback { + ChallengeNotification(this._challenge, this._locale, {this.onPressed}) + : super( + '${_challenge.challenger!.user.name} challenges you!', + ChallengeNotificationDetails.instance.notificationDetails, ); - case 'decline': - ref.read(challengeRepositoryProvider).decline(id); - } - } - NotificationCallback get callback => _callback; + final Challenge _challenge; + final AppLocalizations _locale; + final void Function(ChallengeNotificationAction action, ChallengeId id)? + onPressed; - LocalNotification build(String title, {String? body}) { - return LocalNotification( - title: title, - body: body, - payload: 'challenge-notification:${id.value}', - notificationDetails: - ChallengeNotificationDetails.instance.notificationDetails, - ); + @override + String get payload => _challenge.id.value; + + @override + String get body => _body(); + + String _body() { + final time = _challenge.days == null + ? '∞' + : '${_locale.daysPerTurn}: ${_challenge.days}'; + return _challenge.rated + ? '${_locale.rated} • $time' + : '${_locale.casual} • $time'; + } + + @override + void callback(String? actionId, String? payload) { + final action = switch (actionId) { + 'accept' => ChallengeNotificationAction.accept, + 'decline' => ChallengeNotificationAction.decline, + null || String() => ChallengeNotificationAction.pressed, + }; + final id = ChallengeId(payload!); + onPressed!(action, id); } } diff --git a/lib/src/model/notifications/info_notification.dart b/lib/src/model/notifications/info_notification.dart index f7f26f0267..d334e1add4 100644 --- a/lib/src/model/notifications/info_notification.dart +++ b/lib/src/model/notifications/info_notification.dart @@ -25,14 +25,7 @@ class InfoNotificationDetails { ); } -class InfoNotification { - InfoNotification(); - - LocalNotification build(String title, {String? body}) { - return LocalNotification( - title: title, - body: body, - notificationDetails: InfoNotificationDetails.instance.notificationDetails, - ); - } +class InfoNotification extends LocalNotification { + InfoNotification(String title, {super.body}) + : super(title, InfoNotificationDetails.instance.notificationDetails); } diff --git a/lib/src/model/notifications/local_notification_service.dart b/lib/src/model/notifications/local_notification_service.dart index 57bc39797c..f24e07bd31 100644 --- a/lib/src/model/notifications/local_notification_service.dart +++ b/lib/src/model/notifications/local_notification_service.dart @@ -2,9 +2,7 @@ import 'dart:async'; import 'dart:isolate'; import 'dart:ui'; -import 'package:flutter/foundation.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/notifications/challenge_notification.dart'; import 'package:lichess_mobile/src/model/notifications/info_notification.dart'; import 'package:lichess_mobile/src/utils/l10n.dart'; @@ -12,7 +10,6 @@ import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'local_notification_service.g.dart'; -part 'local_notification_service.freezed.dart'; @Riverpod(keepAlive: true) LocalNotificationService localNotificationService( @@ -21,17 +18,13 @@ LocalNotificationService localNotificationService( return LocalNotificationService(ref, Logger('LocalNotificationService')); } -typedef NotificationCallback = void Function( - String?, - LocalNotificationServiceRef, -); - class LocalNotificationService { LocalNotificationService(this._ref, this._log); static LocalNotificationService? instance; final LocalNotificationServiceRef _ref; final Logger _log; + final Map _callbacks = {}; final _notificationPlugin = FlutterLocalNotificationsPlugin(); final _receivePort = ReceivePort(); @@ -82,17 +75,21 @@ class LocalNotificationService { _log.info( '[Local Notifications] Sent notification: ($id | ${notification.title}) ${notification.body} (Payload: ${notification.payload})', ); + + // if the notification implements LocalNotificaitonCallback + if (notification is LocalNotificationCallback) { + _callbacks[id] = (notification as LocalNotificationCallback).callback; + _log.info('[Local Notifications] registered callback for id [$id]'); + } + return id; } Future cancel(int id) async { - _log.info('[Local Notifications] canceled notification (id: $id)'); + _log.info('[Local Notifications] canceled notification id: [$id]'); return _notificationPlugin.cancel(id); } - void call(NotificationCallback callback, String? actionId) => - callback(actionId, _ref); - void _onReceivePortMessage(dynamic message) { try { final data = message as Map; @@ -113,10 +110,8 @@ class LocalNotificationService { } void _notificationResponse(NotificationResponse response) { - _log.info('[Local Notifcations] recieved notification resopnse'); - debugPrint('hello!'); - - if (response.payload == null) return; + final callback = _callbacks[response.id]; + if (callback != null) callback(response.actionId, response.payload); } @pragma('vm:entry-point') @@ -135,12 +130,20 @@ class LocalNotificationService { } } -@freezed -class LocalNotification with _$LocalNotification { - factory LocalNotification({ - required String title, - String? body, - String? payload, - required NotificationDetails notificationDetails, - }) = _LocalNotification; +abstract class LocalNotification { + LocalNotification( + this.title, + this.notificationDetails, { + this.body, + this.payload, + }); + + String title; + String? body; + String? payload; + NotificationDetails notificationDetails; +} + +abstract class LocalNotificationCallback { + void callback(String? actionId, String? payload); } From e90d59298d4c8021d150de6830ca7b11c4dabdd7 Mon Sep 17 00:00:00 2001 From: inc0g_ Date: Thu, 8 Aug 2024 04:47:53 +1000 Subject: [PATCH 229/979] minor bugfixes --- .../model/challenge/challenge_repository.dart | 48 ++++++++++--------- .../notifications/challenge_notification.dart | 1 + .../local_notification_service.dart | 2 + .../view/user/challenge_requests_screen.dart | 6 ++- 4 files changed, 32 insertions(+), 25 deletions(-) diff --git a/lib/src/model/challenge/challenge_repository.dart b/lib/src/model/challenge/challenge_repository.dart index 904162a4c1..092da7143a 100644 --- a/lib/src/model/challenge/challenge_repository.dart +++ b/lib/src/model/challenge/challenge_repository.dart @@ -177,32 +177,34 @@ class Challenges extends _$Challenges { void _notifyUser(Challenge challenge) { final l10n = ref.read(l10nProvider).strings; - ref.read(localNotificationServiceProvider).show(ChallengeNotification( - challenge, - l10n, - onPressed: (action, id) { - switch (action) { - case ChallengeNotificationAction - .accept: // accept the game and open board - final repo = ref.read(challengesProvider.notifier); - repo.accept(id).then((fullId) { + ref.read(localNotificationServiceProvider).show( + ChallengeNotification( + challenge, + l10n, + onPressed: (action, id) { + switch (action) { + case ChallengeNotificationAction + .accept: // accept the game and open board + final repo = ref.read(challengesProvider.notifier); + repo.accept(id).then((fullId) { + pushPlatformRoute( + ref.read(currentNavigatorKeyProvider).currentContext!, + builder: (BuildContext context) => + GameScreen(initialGameId: fullId), + ); + }); + case ChallengeNotificationAction + .pressed: // open the challenge screen pushPlatformRoute( ref.read(currentNavigatorKeyProvider).currentContext!, builder: (BuildContext context) => - GameScreen(initialGameId: fullId), + const ChallengeRequestsScreen(), ); - }); - case ChallengeNotificationAction - .pressed: // open the challenge screen - pushPlatformRoute( - ref.read(currentNavigatorKeyProvider).currentContext!, - builder: (BuildContext context) => - const ChallengeRequestsScreen(), - ); - case ChallengeNotificationAction.decline: - decline(id); - } - }, - )); + case ChallengeNotificationAction.decline: + decline(id); + } + }, + ), + ); } } diff --git a/lib/src/model/notifications/challenge_notification.dart b/lib/src/model/notifications/challenge_notification.dart index 8aea417626..ca6fe7fa88 100644 --- a/lib/src/model/notifications/challenge_notification.dart +++ b/lib/src/model/notifications/challenge_notification.dart @@ -22,6 +22,7 @@ class ChallengeNotificationDetails { _locale.preferencesNotifyChallenge, importance: Importance.max, priority: Priority.high, + autoCancel: false, actions: [ AndroidNotificationAction( 'accept', diff --git a/lib/src/model/notifications/local_notification_service.dart b/lib/src/model/notifications/local_notification_service.dart index f24e07bd31..c6c6b6eb66 100644 --- a/lib/src/model/notifications/local_notification_service.dart +++ b/lib/src/model/notifications/local_notification_service.dart @@ -38,6 +38,8 @@ class LocalNotificationService { InfoNotificationDetails(l10n.strings); ChallengeNotificationDetails(l10n.strings); + // hot reloading doesnt remove the port so we just make sure it doesnt exist before registering it + IsolateNameServer.removePortNameMapping('localNotificationServicePort'); IsolateNameServer.registerPortWithName( _receivePort.sendPort, 'localNotificationServicePort', diff --git a/lib/src/view/user/challenge_requests_screen.dart b/lib/src/view/user/challenge_requests_screen.dart index 8e47725ae5..ead2a7f4a3 100644 --- a/lib/src/view/user/challenge_requests_screen.dart +++ b/lib/src/view/user/challenge_requests_screen.dart @@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/challenge/challenge.dart'; import 'package:lichess_mobile/src/model/challenge/challenge_repository.dart'; +import 'package:lichess_mobile/src/navigation.dart'; import 'package:lichess_mobile/src/styles/lichess_colors.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; @@ -100,9 +101,10 @@ class _Body extends ConsumerWidget { challengeNotifier .accept(challenge.id) .then((id) { - if (!context.mounted) return; pushPlatformRoute( - context, + ref + .read(currentNavigatorKeyProvider) + .currentContext!, rootNavigator: true, builder: (BuildContext context) { return GameScreen( From 80cb129a647267444122fd803db0c9693259e4e2 Mon Sep 17 00:00:00 2001 From: inc0g_ Date: Fri, 9 Aug 2024 14:52:26 +1000 Subject: [PATCH 230/979] Andriod Manifest: fix formatting changes --- android/app/src/main/AndroidManifest.xml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 01d13313d0..10ef091294 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,13 +1,13 @@ + android:name="android.permission.INTERNET"/> + android:name="android.intent.action.VIEW"/> + android:scheme="https"/> - + + android:resource="@style/NormalTheme"/> - - + + + android:value="2"/> + android:name="com.dexterous.flutterlocalnotifications.ActionBroadcastReceiver"/> - \ No newline at end of file + From 77f9c8331adde0f898fe68cf9b0de61a7a6bd1cf Mon Sep 17 00:00:00 2001 From: inc0g_ Date: Fri, 9 Aug 2024 15:57:10 +1000 Subject: [PATCH 231/979] User Screen: re-enable challenges --- lib/src/view/user/user_screen.dart | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/lib/src/view/user/user_screen.dart b/lib/src/view/user/user_screen.dart index 14250e25e5..a7d7ba12f6 100644 --- a/lib/src/view/user/user_screen.dart +++ b/lib/src/view/user/user_screen.dart @@ -7,8 +7,11 @@ import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/relation/relation_repository.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/model/user/user_repository_providers.dart'; +import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/view/play/challenge_screen.dart'; import 'package:lichess_mobile/src/view/user/recent_games.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; @@ -174,18 +177,17 @@ class _UserProfileListView extends ConsumerWidget { ListSection( hasLeading: true, children: [ - // TODO: re-enable when challenges are fully supported - // if (user.canChallenge == true) - // PlatformListTile( - // title: Text(context.l10n.challengeChallengeToPlay), - // leading: const Icon(LichessIcons.crossed_swords), - // onTap: () { - // pushPlatformRoute( - // context, - // builder: (context) => ChallengeScreen(user.lightUser), - // ); - // }, - // ), + if (user.canChallenge == true) + PlatformListTile( + title: Text(context.l10n.challengeChallengeToPlay), + leading: const Icon(LichessIcons.crossed_swords), + onTap: () { + pushPlatformRoute( + context, + builder: (context) => ChallengeScreen(user.lightUser), + ); + }, + ), if (user.followable == true && user.following != true) PlatformListTile( leading: const Icon(Icons.person_add), From 6c68c09d4b48b1f02246f58f05d207053198b5bd Mon Sep 17 00:00:00 2001 From: inc0g_ Date: Fri, 9 Aug 2024 15:58:26 +1000 Subject: [PATCH 232/979] Notfications: rename to _l10n --- .../notifications/challenge_notification.dart | 24 +++++++++---------- .../notifications/info_notification.dart | 4 ++-- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/lib/src/model/notifications/challenge_notification.dart b/lib/src/model/notifications/challenge_notification.dart index ca6fe7fa88..2b27fef9db 100644 --- a/lib/src/model/notifications/challenge_notification.dart +++ b/lib/src/model/notifications/challenge_notification.dart @@ -6,7 +6,7 @@ import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/notifications/local_notification_service.dart'; class ChallengeNotificationDetails { - ChallengeNotificationDetails(this._locale) { + ChallengeNotificationDetails(this._l10n) { ChallengeNotificationDetails.instance = this; } @@ -14,26 +14,26 @@ class ChallengeNotificationDetails { static ChallengeNotificationDetails instance = ChallengeNotificationDetails(AppLocalizationsEn()); - final AppLocalizations _locale; + final AppLocalizations _l10n; NotificationDetails get notificationDetails => NotificationDetails( android: AndroidNotificationDetails( 'challenges', - _locale.preferencesNotifyChallenge, + _l10n.preferencesNotifyChallenge, importance: Importance.max, priority: Priority.high, autoCancel: false, actions: [ AndroidNotificationAction( 'accept', - _locale.accept, + _l10n.accept, icon: const DrawableResourceAndroidBitmap('tick'), showsUserInterface: true, contextual: true, ), AndroidNotificationAction( 'decline', - _locale.decline, + _l10n.decline, icon: const DrawableResourceAndroidBitmap('cross'), contextual: true, ), @@ -50,14 +50,14 @@ class ChallengeNotificationDetails { actions: [ DarwinNotificationAction.plain( 'accept', - _locale.accept, + _l10n.accept, options: { DarwinNotificationActionOption.foreground, }, ), DarwinNotificationAction.plain( 'decline', - _locale.decline, + _l10n.decline, options: { DarwinNotificationActionOption.destructive, }, @@ -73,14 +73,14 @@ enum ChallengeNotificationAction { accept, decline, pressed } class ChallengeNotification extends LocalNotification implements LocalNotificationCallback { - ChallengeNotification(this._challenge, this._locale, {this.onPressed}) + ChallengeNotification(this._challenge, this._l10n, {this.onPressed}) : super( '${_challenge.challenger!.user.name} challenges you!', ChallengeNotificationDetails.instance.notificationDetails, ); final Challenge _challenge; - final AppLocalizations _locale; + final AppLocalizations _l10n; final void Function(ChallengeNotificationAction action, ChallengeId id)? onPressed; @@ -93,10 +93,10 @@ class ChallengeNotification extends LocalNotification String _body() { final time = _challenge.days == null ? '∞' - : '${_locale.daysPerTurn}: ${_challenge.days}'; + : '${_l10n.daysPerTurn}: ${_challenge.days}'; return _challenge.rated - ? '${_locale.rated} • $time' - : '${_locale.casual} • $time'; + ? '${_l10n.rated} • $time' + : '${_l10n.casual} • $time'; } @override diff --git a/lib/src/model/notifications/info_notification.dart b/lib/src/model/notifications/info_notification.dart index d334e1add4..78c8132e55 100644 --- a/lib/src/model/notifications/info_notification.dart +++ b/lib/src/model/notifications/info_notification.dart @@ -4,7 +4,7 @@ import 'package:lichess_mobile/l10n/l10n_en.dart'; import 'package:lichess_mobile/src/model/notifications/local_notification_service.dart'; class InfoNotificationDetails { - InfoNotificationDetails(this._locale) { + InfoNotificationDetails(this._l10n) { InfoNotificationDetails.instance = this; } @@ -13,7 +13,7 @@ class InfoNotificationDetails { InfoNotificationDetails(AppLocalizationsEn()); // ignore: unused_field - final AppLocalizations _locale; + final AppLocalizations _l10n; NotificationDetails get notificationDetails => const NotificationDetails( android: AndroidNotificationDetails( From a4fda9271d157c4e09b5be93ae89ccd856b71118 Mon Sep 17 00:00:00 2001 From: inc0g_ Date: Fri, 9 Aug 2024 15:58:53 +1000 Subject: [PATCH 233/979] Local Notification: mark fields as final --- .../model/notifications/local_notification_service.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/src/model/notifications/local_notification_service.dart b/lib/src/model/notifications/local_notification_service.dart index c6c6b6eb66..1c54250e3a 100644 --- a/lib/src/model/notifications/local_notification_service.dart +++ b/lib/src/model/notifications/local_notification_service.dart @@ -140,10 +140,10 @@ abstract class LocalNotification { this.payload, }); - String title; - String? body; - String? payload; - NotificationDetails notificationDetails; + final String title; + final String? body; + final String? payload; + final NotificationDetails notificationDetails; } abstract class LocalNotificationCallback { From ac1cb543655c84e61c792913d80e09b484ea74a8 Mon Sep 17 00:00:00 2001 From: inc0g_ Date: Fri, 9 Aug 2024 16:00:37 +1000 Subject: [PATCH 234/979] Challenge List Item: rename --- lib/src/view/play/create_custom_game_screen.dart | 2 +- lib/src/view/user/challenge_requests_screen.dart | 2 +- lib/src/widgets/challenge_display.dart | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/src/view/play/create_custom_game_screen.dart b/lib/src/view/play/create_custom_game_screen.dart index 3e2b1f6f91..5c1303c8d6 100644 --- a/lib/src/view/play/create_custom_game_screen.dart +++ b/lib/src/view/play/create_custom_game_screen.dart @@ -239,7 +239,7 @@ class _ChallengesBodyState extends ConsumerState<_ChallengesBody> { final isMySeek = UserId.fromUserName(challenge.username) == session?.user.id; - return CorrespondenceChallengeDisplay( + return CorrespondenceChallengeListItem( challenge: challenge, user: LightUser( id: UserId.fromUserName(challenge.username), diff --git a/lib/src/view/user/challenge_requests_screen.dart b/lib/src/view/user/challenge_requests_screen.dart index ead2a7f4a3..a7128017b1 100644 --- a/lib/src/view/user/challenge_requests_screen.dart +++ b/lib/src/view/user/challenge_requests_screen.dart @@ -76,7 +76,7 @@ class _Body extends ConsumerWidget { final isMyChallenge = challenge.direction == ChallengeDirection.outward; - return ChallengeDisplay( + return ChallengeListItem( challenge: challenge, user: user, subtitle: subtitle, diff --git a/lib/src/widgets/challenge_display.dart b/lib/src/widgets/challenge_display.dart index 674c463b89..82135fe04b 100644 --- a/lib/src/widgets/challenge_display.dart +++ b/lib/src/widgets/challenge_display.dart @@ -12,8 +12,8 @@ import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/user_full_name.dart'; -class ChallengeDisplay extends StatelessWidget { - const ChallengeDisplay({ +class ChallengeListItem extends StatelessWidget { + const ChallengeListItem({ super.key, required this.challenge, required this.user, @@ -71,8 +71,8 @@ class ChallengeDisplay extends StatelessWidget { } } -class CorrespondenceChallengeDisplay extends StatelessWidget { - const CorrespondenceChallengeDisplay({ +class CorrespondenceChallengeListItem extends StatelessWidget { + const CorrespondenceChallengeListItem({ super.key, required this.challenge, required this.user, @@ -91,7 +91,7 @@ class CorrespondenceChallengeDisplay extends StatelessWidget { @override Widget build(BuildContext context) { - return ChallengeDisplay( + return ChallengeListItem( challenge: Challenge( id: ChallengeId(challenge.id.value), status: ChallengeStatus.created, From f37f3f025609658018a63fe1736a85d7ed03d57e Mon Sep 17 00:00:00 2001 From: inc0g_ Date: Fri, 9 Aug 2024 16:02:45 +1000 Subject: [PATCH 235/979] Challenges Provider: refactor and move to new file --- .../model/challenge/challenge_repository.dart | 116 ------------------ lib/src/model/challenge/challenges.dart | 51 ++++++++ lib/src/view/home/home_tab_screen.dart | 2 +- .../view/user/challenge_requests_screen.dart | 47 ++++--- 4 files changed, 74 insertions(+), 142 deletions(-) create mode 100644 lib/src/model/challenge/challenges.dart diff --git a/lib/src/model/challenge/challenge_repository.dart b/lib/src/model/challenge/challenge_repository.dart index 092da7143a..4d0075c38f 100644 --- a/lib/src/model/challenge/challenge_repository.dart +++ b/lib/src/model/challenge/challenge_repository.dart @@ -1,21 +1,11 @@ import 'dart:async'; -import 'package:collection/collection.dart'; import 'package:deep_pick/deep_pick.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:flutter/widgets.dart'; import 'package:http/http.dart' as http; import 'package:lichess_mobile/src/model/challenge/challenge.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/model/common/socket.dart'; -import 'package:lichess_mobile/src/model/notifications/challenge_notification.dart'; -import 'package:lichess_mobile/src/model/notifications/local_notification_service.dart'; -import 'package:lichess_mobile/src/navigation.dart'; -import 'package:lichess_mobile/src/utils/l10n.dart'; -import 'package:lichess_mobile/src/utils/navigation.dart'; -import 'package:lichess_mobile/src/view/game/game_screen.dart'; -import 'package:lichess_mobile/src/view/user/challenge_requests_screen.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'challenge_repository.g.dart'; @@ -102,109 +92,3 @@ class ChallengeRepository { } } } - -@Riverpod(keepAlive: true) -class Challenges extends _$Challenges { - StreamSubscription? _subscription; - - late SocketClient _socketClient; - - @override - Future build() async { - _socketClient = ref.watch(socketPoolProvider).open(Uri(path: '/socket/v5')); - - _subscription?.cancel(); - _subscription = _socketClient.stream.listen(_handleSocketEvent); - - ref.onDispose(() { - _subscription?.cancel(); - }); - - return ref.read(challengeRepositoryProvider).list(); - } - - Future accept(ChallengeId id) async { - final repo = ref.read(challengeRepositoryProvider); - _stateRemove(id); - - return repo - .accept(id) - .then((_) => repo.show(id).then((challenge) => challenge.gameFullId)); - } - - Future decline(ChallengeId id) async { - final repo = ref.read(challengeRepositoryProvider); - _stateRemove(id); - return repo.decline(id); - } - - void cancel(ChallengeId id) { - ref.read(challengeRepositoryProvider).cancel(id); - } - - void _handleSocketEvent(SocketEvent event) { - if (event.topic != 'challenges') return; - - final listPick = pick(event.data).required(); - final inward = listPick('in').asListOrEmpty(Challenge.fromPick); - final outward = listPick('out').asListOrEmpty(Challenge.fromPick); - - final prevIds = state.value?.inward.map((element) => element.id) ?? []; - // find any challenges that weren't in the inward list before - inward - .map((element) => element.id) - .where((id) => !prevIds.contains(id)) - .map((id) => inward.firstWhere((element) => element.id == id)) - .forEach(_notifyUser); - - state = AsyncValue.data((inward: inward.lock, outward: outward.lock)); - } - - void _stateRemove(ChallengeId id) { - final inward = state.value!.inward; - final outward = state.value!.outward; - final challengeIn = - inward.firstWhereOrNull((challenge) => challenge.id == id); - final challengeOut = - outward.firstWhereOrNull((challenge) => challenge.id == id); - - final newInward = challengeIn == null ? inward : inward.remove(challengeIn); - final newOutward = - challengeOut == null ? outward : outward.remove(challengeOut); - state = AsyncValue.data((inward: newInward, outward: newOutward)); - } - - void _notifyUser(Challenge challenge) { - final l10n = ref.read(l10nProvider).strings; - - ref.read(localNotificationServiceProvider).show( - ChallengeNotification( - challenge, - l10n, - onPressed: (action, id) { - switch (action) { - case ChallengeNotificationAction - .accept: // accept the game and open board - final repo = ref.read(challengesProvider.notifier); - repo.accept(id).then((fullId) { - pushPlatformRoute( - ref.read(currentNavigatorKeyProvider).currentContext!, - builder: (BuildContext context) => - GameScreen(initialGameId: fullId), - ); - }); - case ChallengeNotificationAction - .pressed: // open the challenge screen - pushPlatformRoute( - ref.read(currentNavigatorKeyProvider).currentContext!, - builder: (BuildContext context) => - const ChallengeRequestsScreen(), - ); - case ChallengeNotificationAction.decline: - decline(id); - } - }, - ), - ); - } -} diff --git a/lib/src/model/challenge/challenges.dart b/lib/src/model/challenge/challenges.dart new file mode 100644 index 0000000000..f79859f7e8 --- /dev/null +++ b/lib/src/model/challenge/challenges.dart @@ -0,0 +1,51 @@ +import 'dart:async'; + +import 'package:deep_pick/deep_pick.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:lichess_mobile/src/model/auth/auth_session.dart'; +import 'package:lichess_mobile/src/model/challenge/challenge.dart'; +import 'package:lichess_mobile/src/model/challenge/challenge_repository.dart'; +import 'package:lichess_mobile/src/model/common/socket.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'challenges.g.dart'; + +@Riverpod(keepAlive: true) +class Challenges extends _$Challenges { + StreamSubscription? _subscription; + + late SocketClient _socketClient; + + @override + Future build() async { + _socketClient = ref.watch(socketPoolProvider).open(Uri(path: '/socket/v5')); + + _subscription?.cancel(); + _subscription = _socketClient.stream.listen(_handleSocketEvent); + + // invalidate the challenges list if the user signs out + ref.listen( + authSessionProvider, + (prev, now) { + if (now == null) return; + ref.invalidateSelf(); + }, + ); + + ref.onDispose(() { + _subscription?.cancel(); + }); + + return ref.read(challengeRepositoryProvider).list(); + } + + void _handleSocketEvent(SocketEvent event) { + if (event.topic != 'challenges') return; + + final listPick = pick(event.data).required(); + final inward = listPick('in').asListOrEmpty(Challenge.fromPick); + final outward = listPick('out').asListOrEmpty(Challenge.fromPick); + + state = AsyncValue.data((inward: inward.lock, outward: outward.lock)); + } +} diff --git a/lib/src/view/home/home_tab_screen.dart b/lib/src/view/home/home_tab_screen.dart index f02c4903b3..5501e49000 100644 --- a/lib/src/view/home/home_tab_screen.dart +++ b/lib/src/view/home/home_tab_screen.dart @@ -6,7 +6,7 @@ import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/account/ongoing_game.dart'; import 'package:lichess_mobile/src/model/auth/auth_controller.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; -import 'package:lichess_mobile/src/model/challenge/challenge_repository.dart'; +import 'package:lichess_mobile/src/model/challenge/challenges.dart'; import 'package:lichess_mobile/src/model/correspondence/correspondence_game_storage.dart'; import 'package:lichess_mobile/src/model/game/game_history.dart'; import 'package:lichess_mobile/src/model/settings/home_preferences.dart'; diff --git a/lib/src/view/user/challenge_requests_screen.dart b/lib/src/view/user/challenge_requests_screen.dart index a7128017b1..7e0b6d440e 100644 --- a/lib/src/view/user/challenge_requests_screen.dart +++ b/lib/src/view/user/challenge_requests_screen.dart @@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/challenge/challenge.dart'; import 'package:lichess_mobile/src/model/challenge/challenge_repository.dart'; +import 'package:lichess_mobile/src/model/challenge/challenges.dart'; import 'package:lichess_mobile/src/navigation.dart'; import 'package:lichess_mobile/src/styles/lichess_colors.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; @@ -46,7 +47,7 @@ class ChallengeRequestsScreen extends ConsumerWidget { class _Body extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final challengeNotifier = ref.read(challengesProvider.notifier); + final challengeRepo = ref.read(challengeRepositoryProvider); final challenges = ref.watch(challengesProvider); final session = ref.watch(authSessionProvider); @@ -97,35 +98,31 @@ class _Body extends ConsumerWidget { context, title: Text(context.l10n.accept), isDestructiveAction: true, - onConfirm: (_) { - challengeNotifier - .accept(challenge.id) - .then((id) { - pushPlatformRoute( - ref - .read(currentNavigatorKeyProvider) - .currentContext!, - rootNavigator: true, - builder: (BuildContext context) { - return GameScreen( - initialGameId: id, - ); - }, - ); - }); + onConfirm: (_) async { + await challengeRepo.accept(challenge.id); + final fullId = await challengeRepo + .show(challenge.id) + .then( + (challenge) => challenge.gameFullId, + ); + pushPlatformRoute( + ref + .read(currentNavigatorKeyProvider) + .currentContext!, + rootNavigator: true, + builder: (BuildContext context) { + return GameScreen( + initialGameId: fullId, + ); + }, + ); }, ); }, cancelText: !isMyChallenge ? context.l10n.decline : null, onCancel: isMyChallenge - ? () { - challengeNotifier.cancel(challenge.id); - } - : () { - ref - .read(challengeRepositoryProvider) - .decline(challenge.id); - }, + ? () => challengeRepo.cancel(challenge.id) + : () => challengeRepo.decline(challenge.id), ); }, ); From 75f91eb1b5d5a4e7cf408c4e4fb7a969e3c269b7 Mon Sep 17 00:00:00 2001 From: inc0g_ Date: Fri, 9 Aug 2024 16:21:48 +1000 Subject: [PATCH 236/979] Local Notification Service: listen for app localisation changes --- .../local_notification_service.dart | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/lib/src/model/notifications/local_notification_service.dart b/lib/src/model/notifications/local_notification_service.dart index 1c54250e3a..ef98b182b9 100644 --- a/lib/src/model/notifications/local_notification_service.dart +++ b/lib/src/model/notifications/local_notification_service.dart @@ -5,6 +5,7 @@ import 'dart:ui'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:lichess_mobile/src/model/notifications/challenge_notification.dart'; import 'package:lichess_mobile/src/model/notifications/info_notification.dart'; +import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; import 'package:lichess_mobile/src/utils/l10n.dart'; import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -21,7 +22,6 @@ LocalNotificationService localNotificationService( class LocalNotificationService { LocalNotificationService(this._ref, this._log); - static LocalNotificationService? instance; final LocalNotificationServiceRef _ref; final Logger _log; final Map _callbacks = {}; @@ -31,12 +31,11 @@ class LocalNotificationService { int currentId = 0; Future init() async { - instance = this; - final l10n = _ref.watch(l10nProvider); - - // update localizations - InfoNotificationDetails(l10n.strings); - ChallengeNotificationDetails(l10n.strings); + _updateLocalisations(); + _ref.listen(generalPreferencesProvider, (prev, now) { + if (prev!.locale == now.locale) return; + _updateLocalisations(); + }); // hot reloading doesnt remove the port so we just make sure it doesnt exist before registering it IsolateNameServer.removePortNameMapping('localNotificationServicePort'); @@ -65,6 +64,12 @@ class LocalNotificationService { _log.info('[Local Notifications] initialized'); } + void _updateLocalisations() { + final l10n = _ref.read(l10nProvider); + InfoNotificationDetails(l10n.strings); + ChallengeNotificationDetails(l10n.strings); + } + Future show(LocalNotification notification) async { final id = currentId++; await _notificationPlugin.show( From 13f11f8c10b9b5684106cabceb4e057730a709b8 Mon Sep 17 00:00:00 2001 From: inc0g_ Date: Fri, 9 Aug 2024 16:45:22 +1000 Subject: [PATCH 237/979] App State: listen for challenges and send notifications --- lib/src/app.dart | 53 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/lib/src/app.dart b/lib/src/app.dart index 9a8a1ad8f9..6a974bd4fd 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -12,9 +12,12 @@ import 'package:lichess_mobile/main.dart'; import 'package:lichess_mobile/src/app_initialization.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; +import 'package:lichess_mobile/src/model/challenge/challenge_repository.dart'; +import 'package:lichess_mobile/src/model/challenge/challenges.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/correspondence/correspondence_service.dart'; +import 'package:lichess_mobile/src/model/notifications/challenge_notification.dart'; import 'package:lichess_mobile/src/model/notifications/local_notification_service.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/model/settings/brightness.dart'; @@ -23,10 +26,12 @@ import 'package:lichess_mobile/src/navigation.dart'; import 'package:lichess_mobile/src/notification_service.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/connectivity.dart'; +import 'package:lichess_mobile/src/utils/l10n.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/utils/system.dart'; import 'package:lichess_mobile/src/view/game/game_screen.dart'; +import 'package:lichess_mobile/src/view/user/challenge_requests_screen.dart'; /// Application initialization and main entry point. class AppInitializationScreen extends ConsumerWidget { @@ -109,6 +114,54 @@ class _AppState extends ConsumerState { ref.read(localNotificationServiceProvider).init(); + ref.listenManual(challengesProvider, (prev, current) { + final prevIds = prev!.value!.inward.map((challenge) => challenge.id); + final inward = current.value!.inward; + final repo = ref.read(challengeRepositoryProvider); + final l10n = ref.read(l10nProvider).strings; + + inward + .map((element) => element.id) + .where((id) => !prevIds.contains(id)) + .map((id) => inward.firstWhere((element) => element.id == id)) + .forEach((challenge) { + ref.read(localNotificationServiceProvider).show( + ChallengeNotification( + challenge, + l10n, + onPressed: (action, id) async { + switch (action) { + case ChallengeNotificationAction + .accept: // accept the game and open board + await repo.accept(challenge.id); + final fullId = await repo.show(challenge.id).then( + (challenge) => challenge.gameFullId, + ); + pushPlatformRoute( + ref.read(currentNavigatorKeyProvider).currentContext!, + rootNavigator: true, + builder: (BuildContext context) { + return GameScreen( + initialGameId: fullId, + ); + }, + ); + case ChallengeNotificationAction + .pressed: // open the challenge screen + pushPlatformRoute( + ref.read(currentNavigatorKeyProvider).currentContext!, + builder: (BuildContext context) => + const ChallengeRequestsScreen(), + ); + case ChallengeNotificationAction.decline: + repo.decline(id); + } + }, + ), + ); + }); + }); + ref.listenManual(connectivityChangesProvider, (prev, current) async { // Play registered moves whenever the app comes back online. if (prev?.hasValue == true && From 2f2ce924b3c509819c40f6755ae92e42f712d436 Mon Sep 17 00:00:00 2001 From: inc0g_ Date: Sat, 10 Aug 2024 04:31:45 +1000 Subject: [PATCH 238/979] App State: add null checks and remove redundant code --- lib/src/app.dart | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/src/app.dart b/lib/src/app.dart index 6a974bd4fd..424cfd3e0c 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -115,15 +115,14 @@ class _AppState extends ConsumerState { ref.read(localNotificationServiceProvider).init(); ref.listenManual(challengesProvider, (prev, current) { - final prevIds = prev!.value!.inward.map((challenge) => challenge.id); + if (prev == null || !prev.hasValue || !current.hasValue) return; + final prevIds = prev.value!.inward.map((challenge) => challenge.id); final inward = current.value!.inward; final repo = ref.read(challengeRepositoryProvider); final l10n = ref.read(l10nProvider).strings; inward - .map((element) => element.id) - .where((id) => !prevIds.contains(id)) - .map((id) => inward.firstWhere((element) => element.id == id)) + .where((challenge) => !prevIds.contains(challenge.id)) .forEach((challenge) { ref.read(localNotificationServiceProvider).show( ChallengeNotification( From d7a38894378e8203e82f39bebfeb6457d3dc04ad Mon Sep 17 00:00:00 2001 From: inc0g_ Date: Sat, 10 Aug 2024 04:33:23 +1000 Subject: [PATCH 239/979] Local Notifications: change id and callback declaration --- .../notifications/challenge_notification.dart | 14 ++++++---- .../notifications/info_notification.dart | 3 +++ .../local_notification_service.dart | 26 ++++++++----------- 3 files changed, 23 insertions(+), 20 deletions(-) diff --git a/lib/src/model/notifications/challenge_notification.dart b/lib/src/model/notifications/challenge_notification.dart index 2b27fef9db..ecd37b9872 100644 --- a/lib/src/model/notifications/challenge_notification.dart +++ b/lib/src/model/notifications/challenge_notification.dart @@ -71,8 +71,7 @@ class ChallengeNotificationDetails { enum ChallengeNotificationAction { accept, decline, pressed } -class ChallengeNotification extends LocalNotification - implements LocalNotificationCallback { +class ChallengeNotification extends LocalNotification { ChallengeNotification(this._challenge, this._l10n, {this.onPressed}) : super( '${_challenge.challenger!.user.name} challenges you!', @@ -84,12 +83,18 @@ class ChallengeNotification extends LocalNotification final void Function(ChallengeNotificationAction action, ChallengeId id)? onPressed; + @override + int get id => _challenge.id.value.hashCode; + @override String get payload => _challenge.id.value; @override String get body => _body(); + @override + void Function(String? actionId, String? payload) get callback => _callback; + String _body() { final time = _challenge.days == null ? '∞' @@ -99,14 +104,13 @@ class ChallengeNotification extends LocalNotification : '${_l10n.casual} • $time'; } - @override - void callback(String? actionId, String? payload) { + void _callback(String? actionId, String? payload) { final action = switch (actionId) { 'accept' => ChallengeNotificationAction.accept, 'decline' => ChallengeNotificationAction.decline, null || String() => ChallengeNotificationAction.pressed, }; final id = ChallengeId(payload!); - onPressed!(action, id); + onPressed?.call(action, id); } } diff --git a/lib/src/model/notifications/info_notification.dart b/lib/src/model/notifications/info_notification.dart index 78c8132e55..6e58e247ad 100644 --- a/lib/src/model/notifications/info_notification.dart +++ b/lib/src/model/notifications/info_notification.dart @@ -28,4 +28,7 @@ class InfoNotificationDetails { class InfoNotification extends LocalNotification { InfoNotification(String title, {super.body}) : super(title, InfoNotificationDetails.instance.notificationDetails); + + @override + int get id => hashCode; } diff --git a/lib/src/model/notifications/local_notification_service.dart b/lib/src/model/notifications/local_notification_service.dart index ef98b182b9..0253333abc 100644 --- a/lib/src/model/notifications/local_notification_service.dart +++ b/lib/src/model/notifications/local_notification_service.dart @@ -28,8 +28,6 @@ class LocalNotificationService { final _notificationPlugin = FlutterLocalNotificationsPlugin(); final _receivePort = ReceivePort(); - int currentId = 0; - Future init() async { _updateLocalisations(); _ref.listen(generalPreferencesProvider, (prev, now) { @@ -61,7 +59,7 @@ class LocalNotificationService { onDidReceiveBackgroundNotificationResponse: _notificationBackgroundResponse, ); - _log.info('[Local Notifications] initialized'); + _log.info('initialized'); } void _updateLocalisations() { @@ -71,7 +69,7 @@ class LocalNotificationService { } Future show(LocalNotification notification) async { - final id = currentId++; + final id = notification.id; await _notificationPlugin.show( id, notification.title, @@ -80,20 +78,19 @@ class LocalNotificationService { payload: notification.payload, ); _log.info( - '[Local Notifications] Sent notification: ($id | ${notification.title}) ${notification.body} (Payload: ${notification.payload})', + 'Sent notification: ($id | ${notification.title}) ${notification.body} (Payload: ${notification.payload})', ); - // if the notification implements LocalNotificaitonCallback - if (notification is LocalNotificationCallback) { - _callbacks[id] = (notification as LocalNotificationCallback).callback; - _log.info('[Local Notifications] registered callback for id [$id]'); + if (notification.callback != null) { + _callbacks[id] = notification.callback!; + _log.info('registered callback for id [$id]'); } return id; } Future cancel(int id) async { - _log.info('[Local Notifications] canceled notification id: [$id]'); + _log.info('canceled notification id: [$id]'); return _notificationPlugin.cancel(id); } @@ -111,7 +108,7 @@ class LocalNotificationService { _notificationResponse(response); } catch (e) { _log.severe( - '[Local Notifications] failed to parse message from background isoalte', + 'failed to parse message from background isoalte', ); } } @@ -145,12 +142,11 @@ abstract class LocalNotification { this.payload, }); + int get id; + final String title; final String? body; final String? payload; final NotificationDetails notificationDetails; -} - -abstract class LocalNotificationCallback { - void callback(String? actionId, String? payload); + final void Function(String? actionId, String? payload)? callback = null; } From 4e32eceb557507ab821c62a9ff923bcb8745d056 Mon Sep 17 00:00:00 2001 From: inc0g_ Date: Sat, 10 Aug 2024 04:35:42 +1000 Subject: [PATCH 240/979] Challenge Screen: use _pendingCreateChallenge --- lib/src/view/play/challenge_screen.dart | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/lib/src/view/play/challenge_screen.dart b/lib/src/view/play/challenge_screen.dart index 65c62b3a57..6ce3ec1a65 100644 --- a/lib/src/view/play/challenge_screen.dart +++ b/lib/src/view/play/challenge_screen.dart @@ -10,6 +10,7 @@ import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/challenge/challenge.dart'; import 'package:lichess_mobile/src/model/challenge/challenge_preferences.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/time_increment.dart'; import 'package:lichess_mobile/src/model/lobby/create_game_service.dart'; import 'package:lichess_mobile/src/model/lobby/game_setup.dart'; @@ -63,7 +64,7 @@ class _ChallengeBody extends ConsumerStatefulWidget { } class _ChallengeBodyState extends ConsumerState<_ChallengeBody> { - Future? _pendingCreateGame; + Future<(GameFullId?, DeclineReason?)>? _pendingCreateChallenge; final _controller = TextEditingController(); String? fromPositionFenInput; @@ -381,7 +382,7 @@ class _ChallengeBodyState extends ConsumerState<_ChallengeBody> { ), const SizedBox(height: 20), FutureBuilder( - future: _pendingCreateGame, + future: _pendingCreateChallenge, builder: (context, snapshot) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 20.0), @@ -416,7 +417,7 @@ class _ChallengeBodyState extends ConsumerState<_ChallengeBody> { context, 'Sent challenge to ${widget.user.name}', ); - final response = + _pendingCreateChallenge = createGameService.newChallenge( preferences.makeRequest( widget.user, @@ -426,23 +427,26 @@ class _ChallengeBodyState extends ConsumerState<_ChallengeBody> { : fromPositionFenInput, ), ); - response.then((value) { + + _pendingCreateChallenge!.then((value) { if (!context.mounted) return; - if (value.$1 != null) { + final (fullId, declineReason) = value; + + if (fullId != null) { pushPlatformRoute( context, rootNavigator: true, builder: (BuildContext context) { return GameScreen( - initialGameId: value.$1, + initialGameId: fullId, ); }, ); } else { showPlatformSnackbar( context, - '${widget.user.name}: ${declineReasonMessage(context, value.$2!)}', + '${widget.user.name}: ${declineReasonMessage(context, declineReason!)}', ); } }); From a50d9f2c4cc413f139c6b4c34f292cef994dae45 Mon Sep 17 00:00:00 2001 From: inc0g_ Date: Sat, 10 Aug 2024 04:38:03 +1000 Subject: [PATCH 241/979] Challenge List Item: rename file --- lib/src/view/play/create_custom_game_screen.dart | 2 +- lib/src/view/user/challenge_requests_screen.dart | 2 +- .../{challenge_display.dart => challenge_list_item.dart} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename lib/src/widgets/{challenge_display.dart => challenge_list_item.dart} (100%) diff --git a/lib/src/view/play/create_custom_game_screen.dart b/lib/src/view/play/create_custom_game_screen.dart index 5c1303c8d6..00f5ebf2ae 100644 --- a/lib/src/view/play/create_custom_game_screen.dart +++ b/lib/src/view/play/create_custom_game_screen.dart @@ -24,7 +24,7 @@ import 'package:lichess_mobile/src/view/game/game_screen.dart'; import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; import 'package:lichess_mobile/src/widgets/adaptive_choice_picker.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; -import 'package:lichess_mobile/src/widgets/challenge_display.dart'; +import 'package:lichess_mobile/src/widgets/challenge_list_item.dart'; import 'package:lichess_mobile/src/widgets/expanded_section.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; diff --git a/lib/src/view/user/challenge_requests_screen.dart b/lib/src/view/user/challenge_requests_screen.dart index 7e0b6d440e..2dcc058fa9 100644 --- a/lib/src/view/user/challenge_requests_screen.dart +++ b/lib/src/view/user/challenge_requests_screen.dart @@ -11,7 +11,7 @@ import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/game/game_screen.dart'; import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; -import 'package:lichess_mobile/src/widgets/challenge_display.dart'; +import 'package:lichess_mobile/src/widgets/challenge_list_item.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; diff --git a/lib/src/widgets/challenge_display.dart b/lib/src/widgets/challenge_list_item.dart similarity index 100% rename from lib/src/widgets/challenge_display.dart rename to lib/src/widgets/challenge_list_item.dart From aa915ce064ddef11838ff56299906aa902d2b5a2 Mon Sep 17 00:00:00 2001 From: inc0g_ Date: Sat, 10 Aug 2024 04:38:20 +1000 Subject: [PATCH 242/979] Challenge Requests Screen: cancel notification on decline --- lib/src/view/user/challenge_requests_screen.dart | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/src/view/user/challenge_requests_screen.dart b/lib/src/view/user/challenge_requests_screen.dart index 2dcc058fa9..4d60e71443 100644 --- a/lib/src/view/user/challenge_requests_screen.dart +++ b/lib/src/view/user/challenge_requests_screen.dart @@ -5,6 +5,7 @@ import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/challenge/challenge.dart'; import 'package:lichess_mobile/src/model/challenge/challenge_repository.dart'; import 'package:lichess_mobile/src/model/challenge/challenges.dart'; +import 'package:lichess_mobile/src/model/notifications/local_notification_service.dart'; import 'package:lichess_mobile/src/navigation.dart'; import 'package:lichess_mobile/src/styles/lichess_colors.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; @@ -122,7 +123,12 @@ class _Body extends ConsumerWidget { cancelText: !isMyChallenge ? context.l10n.decline : null, onCancel: isMyChallenge ? () => challengeRepo.cancel(challenge.id) - : () => challengeRepo.decline(challenge.id), + : () { + challengeRepo.decline(challenge.id); + ref + .read(localNotificationServiceProvider) + .cancel(challenge.id.value.hashCode); + }, ); }, ); From 7c4d98f0a61ea78e18ceb33f520f2dee9ffa47c3 Mon Sep 17 00:00:00 2001 From: inc0g_ Date: Mon, 19 Aug 2024 05:31:24 +1000 Subject: [PATCH 243/979] Challenge Notification: move class --- .../notifications/challenge_notification.dart | 98 ++++++++++--------- 1 file changed, 52 insertions(+), 46 deletions(-) diff --git a/lib/src/model/notifications/challenge_notification.dart b/lib/src/model/notifications/challenge_notification.dart index ecd37b9872..311b9fb039 100644 --- a/lib/src/model/notifications/challenge_notification.dart +++ b/lib/src/model/notifications/challenge_notification.dart @@ -4,6 +4,58 @@ import 'package:lichess_mobile/l10n/l10n_en.dart'; import 'package:lichess_mobile/src/model/challenge/challenge.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/notifications/local_notification_service.dart'; +class ChallengeNotification extends LocalNotification { + ChallengeNotification(this._challenge, this._l10n) + : super( + '${_challenge.challenger!.user.name} challenges you!', + ChallengeNotificationDetails.instance.notificationDetails, + ); + + final Challenge _challenge; + final AppLocalizations _l10n; + + @override + int get id => _challenge.id.value.hashCode; + + @override + NotificationPayload get payload => + ChallengePayload(_challenge.id).toNotifiationPayload(); + + @override + String get body => _body(); + + String _body() { + final time = _challenge.days == null + ? '∞' + : '${_l10n.daysPerTurn}: ${_challenge.days}'; + return _challenge.rated + ? '${_l10n.rated} • $time' + : '${_l10n.casual} • $time'; + } +} + +class ChallengePayload { + const ChallengePayload(this.id); + + final ChallengeId id; + + NotificationPayload toNotifiationPayload() { + return NotificationPayload( + type: PayloadType.challenge, + data: { + 'id': id.value, + }, + ); + } + + factory ChallengePayload.fromNotificationPayload( + NotificationPayload payload, + ) { + assert(payload.type == PayloadType.challenge); + final id = payload.data['id'] as String; + return ChallengePayload(ChallengeId(id)); + } +} class ChallengeNotificationDetails { ChallengeNotificationDetails(this._l10n) { @@ -68,49 +120,3 @@ class ChallengeNotificationDetails { }, ); } - -enum ChallengeNotificationAction { accept, decline, pressed } - -class ChallengeNotification extends LocalNotification { - ChallengeNotification(this._challenge, this._l10n, {this.onPressed}) - : super( - '${_challenge.challenger!.user.name} challenges you!', - ChallengeNotificationDetails.instance.notificationDetails, - ); - - final Challenge _challenge; - final AppLocalizations _l10n; - final void Function(ChallengeNotificationAction action, ChallengeId id)? - onPressed; - - @override - int get id => _challenge.id.value.hashCode; - - @override - String get payload => _challenge.id.value; - - @override - String get body => _body(); - - @override - void Function(String? actionId, String? payload) get callback => _callback; - - String _body() { - final time = _challenge.days == null - ? '∞' - : '${_l10n.daysPerTurn}: ${_challenge.days}'; - return _challenge.rated - ? '${_l10n.rated} • $time' - : '${_l10n.casual} • $time'; - } - - void _callback(String? actionId, String? payload) { - final action = switch (actionId) { - 'accept' => ChallengeNotificationAction.accept, - 'decline' => ChallengeNotificationAction.decline, - null || String() => ChallengeNotificationAction.pressed, - }; - final id = ChallengeId(payload!); - onPressed?.call(action, id); - } -} From 687f9622398270b2864ead54955172f5d6161626 Mon Sep 17 00:00:00 2001 From: inc0g_ Date: Mon, 19 Aug 2024 05:33:29 +1000 Subject: [PATCH 244/979] System: change getTotalRam to return 256 on failed call --- lib/src/app_initialization.dart | 2 +- lib/src/utils/system.dart | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/src/app_initialization.dart b/lib/src/app_initialization.dart index 8aff94ad36..35e9db1ed2 100644 --- a/lib/src/app_initialization.dart +++ b/lib/src/app_initialization.dart @@ -120,7 +120,7 @@ Future appInitialization( } } - final physicalMemory = await System.instance.getTotalRam() ?? 256.0; + final physicalMemory = await System.instance.getTotalRam(); final engineMaxMemory = (physicalMemory / 10).ceil(); return AppInitializationData( diff --git a/lib/src/utils/system.dart b/lib/src/utils/system.dart index 70937039de..228ad7e491 100644 --- a/lib/src/utils/system.dart +++ b/lib/src/utils/system.dart @@ -14,12 +14,11 @@ class System { static const instance = System._(); /// Returns the system total RAM in megabytes. - Future getTotalRam() async { + Future getTotalRam() async { try { - return await _channel.invokeMethod('getTotalRam'); - } on PlatformException catch (e) { - debugPrint('Failed to get total RAM: ${e.message}'); - return null; + return (await _channel.invokeMethod('getTotalRam'))!; + } catch (_) { + return 256; } } From 6f333455cc354b49b5489fd56b194ea24e84e33c Mon Sep 17 00:00:00 2001 From: inc0g_ Date: Mon, 19 Aug 2024 05:34:44 +1000 Subject: [PATCH 245/979] App: move local notifications init --- lib/src/app.dart | 66 +++++++++++------------------------------------- 1 file changed, 15 insertions(+), 51 deletions(-) diff --git a/lib/src/app.dart b/lib/src/app.dart index 424cfd3e0c..192e0b8830 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -12,7 +12,6 @@ import 'package:lichess_mobile/main.dart'; import 'package:lichess_mobile/src/app_initialization.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; -import 'package:lichess_mobile/src/model/challenge/challenge_repository.dart'; import 'package:lichess_mobile/src/model/challenge/challenges.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; @@ -31,7 +30,6 @@ import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/utils/system.dart'; import 'package:lichess_mobile/src/view/game/game_screen.dart'; -import 'package:lichess_mobile/src/view/user/challenge_requests_screen.dart'; /// Application initialization and main entry point. class AppInitializationScreen extends ConsumerWidget { @@ -112,55 +110,6 @@ class _AppState extends ConsumerState { setOptimalDisplayMode(); } - ref.read(localNotificationServiceProvider).init(); - - ref.listenManual(challengesProvider, (prev, current) { - if (prev == null || !prev.hasValue || !current.hasValue) return; - final prevIds = prev.value!.inward.map((challenge) => challenge.id); - final inward = current.value!.inward; - final repo = ref.read(challengeRepositoryProvider); - final l10n = ref.read(l10nProvider).strings; - - inward - .where((challenge) => !prevIds.contains(challenge.id)) - .forEach((challenge) { - ref.read(localNotificationServiceProvider).show( - ChallengeNotification( - challenge, - l10n, - onPressed: (action, id) async { - switch (action) { - case ChallengeNotificationAction - .accept: // accept the game and open board - await repo.accept(challenge.id); - final fullId = await repo.show(challenge.id).then( - (challenge) => challenge.gameFullId, - ); - pushPlatformRoute( - ref.read(currentNavigatorKeyProvider).currentContext!, - rootNavigator: true, - builder: (BuildContext context) { - return GameScreen( - initialGameId: fullId, - ); - }, - ); - case ChallengeNotificationAction - .pressed: // open the challenge screen - pushPlatformRoute( - ref.read(currentNavigatorKeyProvider).currentContext!, - builder: (BuildContext context) => - const ChallengeRequestsScreen(), - ); - case ChallengeNotificationAction.decline: - repo.decline(id); - } - }, - ), - ); - }); - }); - ref.listenManual(connectivityChangesProvider, (prev, current) async { // Play registered moves whenever the app comes back online. if (prev?.hasValue == true && @@ -359,6 +308,21 @@ class _EntryPointState extends ConsumerState<_EntryPointWidget> { } } }); + + ref.read(localNotificationServiceProvider).init(); + + ref.listenManual(challengesProvider, (prev, current) { + if (prev == null || !prev.hasValue || !current.hasValue) return; + final prevIds = prev.value!.inward.map((challenge) => challenge.id); + final inward = current.value!.inward; + final l10n = ref.read(l10nProvider).strings; + + inward.where((challenge) => !prevIds.contains(challenge.id)).forEach( + (challenge) => ref + .read(localNotificationServiceProvider) + .show(ChallengeNotification(challenge, l10n)), + ); + }); } @override From e5192163fe0331bf3c11d437da5e85bae7bd8237 Mon Sep 17 00:00:00 2001 From: inc0g_ Date: Mon, 19 Aug 2024 05:35:10 +1000 Subject: [PATCH 246/979] Local notifications: change implementation --- .../notifications/challenge_notification.dart | 69 +++++++++ .../local_notification_service.dart | 132 +++++++++++------- 2 files changed, 153 insertions(+), 48 deletions(-) diff --git a/lib/src/model/notifications/challenge_notification.dart b/lib/src/model/notifications/challenge_notification.dart index 311b9fb039..a6d8efeeed 100644 --- a/lib/src/model/notifications/challenge_notification.dart +++ b/lib/src/model/notifications/challenge_notification.dart @@ -1,9 +1,78 @@ +import 'package:flutter/widgets.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:lichess_mobile/l10n/l10n.dart'; import 'package:lichess_mobile/l10n/l10n_en.dart'; import 'package:lichess_mobile/src/model/challenge/challenge.dart'; +import 'package:lichess_mobile/src/model/challenge/challenge_repository.dart'; +import 'package:lichess_mobile/src/model/challenge/challenges.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/notifications/local_notification_service.dart'; +import 'package:lichess_mobile/src/navigation.dart'; +import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/view/game/game_screen.dart'; +import 'package:lichess_mobile/src/view/user/challenge_requests_screen.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'challenge_notification.g.dart'; + +@riverpod +ChallengeService challengeService(ChallengeServiceRef ref) { + return ChallengeService(ref); +} + +class ChallengeService { + ChallengeService(this.ref); + + final ChallengeServiceRef ref; + + Future onNotificationResponse( + int id, + String? actionid, + ChallengePayload payload, + ) async { + switch (actionid) { + case 'accept': + final repo = ref.read(challengeRepositoryProvider); + await repo.accept(payload.id); + + final fullId = await repo + .show(payload.id) + .then((challenge) => challenge.gameFullId); + + final context = ref.read(currentNavigatorKeyProvider).currentContext!; + if (!context.mounted) break; + + final navState = Navigator.of(context); + if (navState.canPop()) { + navState.popUntil((route) => route.isFirst); + } + + // sometimes becuase GameScreen connects to a socket we dont recieve a new event on the event listener in time + // so we just make sure to refresh this value when we go back to the home tab + ref.invalidate(challengesProvider); + pushPlatformRoute( + context, + rootNavigator: true, + builder: (BuildContext context) => GameScreen(initialGameId: fullId), + ); + + case null: + final context = ref.read(currentNavigatorKeyProvider).currentContext!; + final navState = Navigator.of(context); + if (navState.canPop()) { + navState.popUntil((route) => route.isFirst); + } + pushPlatformRoute( + context, + builder: (BuildContext context) => const ChallengeRequestsScreen(), + ); + case 'decline': + final repo = ref.read(challengeRepositoryProvider); + repo.decline(payload.id); + } + } +} + class ChallengeNotification extends LocalNotification { ChallengeNotification(this._challenge, this._l10n) : super( diff --git a/lib/src/model/notifications/local_notification_service.dart b/lib/src/model/notifications/local_notification_service.dart index 0253333abc..461143339b 100644 --- a/lib/src/model/notifications/local_notification_service.dart +++ b/lib/src/model/notifications/local_notification_service.dart @@ -1,8 +1,11 @@ import 'dart:async'; -import 'dart:isolate'; -import 'dart:ui'; +import 'dart:convert'; +import 'package:flutter/foundation.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:lichess_mobile/src/app_initialization.dart'; import 'package:lichess_mobile/src/model/notifications/challenge_notification.dart'; import 'package:lichess_mobile/src/model/notifications/info_notification.dart'; import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; @@ -11,6 +14,7 @@ import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'local_notification_service.g.dart'; +part 'local_notification_service.freezed.dart'; @Riverpod(keepAlive: true) LocalNotificationService localNotificationService( @@ -24,9 +28,7 @@ class LocalNotificationService { final LocalNotificationServiceRef _ref; final Logger _log; - final Map _callbacks = {}; final _notificationPlugin = FlutterLocalNotificationsPlugin(); - final _receivePort = ReceivePort(); Future init() async { _updateLocalisations(); @@ -35,14 +37,6 @@ class LocalNotificationService { _updateLocalisations(); }); - // hot reloading doesnt remove the port so we just make sure it doesnt exist before registering it - IsolateNameServer.removePortNameMapping('localNotificationServicePort'); - IsolateNameServer.registerPortWithName( - _receivePort.sendPort, - 'localNotificationServicePort', - ); - _receivePort.listen(_onReceivePortMessage); - await _notificationPlugin.initialize( InitializationSettings( android: const AndroidInitializationSettings('logo_black'), @@ -70,22 +64,21 @@ class LocalNotificationService { Future show(LocalNotification notification) async { final id = notification.id; + final payload = notification.payload != null + ? jsonEncode(notification.payload!.toJson()) + : ''; + await _notificationPlugin.show( id, notification.title, notification.body, notification.notificationDetails, - payload: notification.payload, + payload: payload, ); _log.info( 'Sent notification: ($id | ${notification.title}) ${notification.body} (Payload: ${notification.payload})', ); - if (notification.callback != null) { - _callbacks[id] = notification.callback!; - _log.info('registered callback for id [$id]'); - } - return id; } @@ -94,46 +87,90 @@ class LocalNotificationService { return _notificationPlugin.cancel(id); } - void _onReceivePortMessage(dynamic message) { - try { - final data = message as Map; - final response = NotificationResponse( - notificationResponseType: - NotificationResponseType.values[data['index']! as int], - id: data['id'] as int?, - actionId: data['actionId'] as String?, - input: data['input'] as String?, - payload: data['payload'] as String?, - ); - _notificationResponse(response); - } catch (e) { - _log.severe( - 'failed to parse message from background isoalte', - ); + void _handleResponse(int id, String? actionId, NotificationPayload payload) { + switch (payload.type) { + case PayloadType.info: + case PayloadType.challenge: + _ref.read(challengeServiceProvider).onNotificationResponse( + id, + actionId, + ChallengePayload.fromNotificationPayload(payload), + ); } } void _notificationResponse(NotificationResponse response) { - final callback = _callbacks[response.id]; - if (callback != null) callback(response.actionId, response.payload); + _log.info('processing response in foreground. id [${response.id}]'); + if (response.id == null || response.payload == null) return; + + try { + final payload = NotificationPayload.fromJson( + jsonDecode(response.payload!) as Map, + ); + _handleResponse(response.id!, response.actionId, payload); + } catch (e) { + _log.warning('Failed to parse notification payload: $e'); + } } @pragma('vm:entry-point') - static void _notificationBackgroundResponse(NotificationResponse response) { - final sendPort = - IsolateNameServer.lookupPortByName('localNotificationServicePort'); - sendPort!.send( - { - 'index': response.notificationResponseType.index, - 'id': response.id, - 'actionId': response.actionId, - 'input': response.input, - 'payload': response.payload, + static void _notificationBackgroundResponse( + NotificationResponse response, + ) { + final logger = Logger('LocalNotificationService'); + logger.info('processing response in background. id [${response.id}]'); + + // create a new provider scope for the background isolate + final ref = ProviderContainer(); + + ref.listen( + appInitializationProvider, + (prev, now) { + if (!now.hasValue) return; + + try { + final payload = NotificationPayload.fromJson( + jsonDecode(response.payload!) as Map, + ); + ref + .read(localNotificationServiceProvider) + ._handleResponse(response.id!, response.actionId, payload); + } catch (e) { + logger.warning('failed to parse notification payload: $e'); + } }, ); + + ref.read(appInitializationProvider); } } +enum PayloadType { + info, + challenge, +} + +@freezed +class LocalnotificationResponse with _$LocalnotificationResponse { + factory LocalnotificationResponse({ + required int id, + required String? actionId, + required NotificationPayload payload, + String? input, + }) = _LocalnotificationResponse; +} + +@Freezed(fromJson: true, toJson: true) +class NotificationPayload with _$NotificationPayload { + factory NotificationPayload({ + required PayloadType type, + required Map data, + }) = _NotificationPayload; + + factory NotificationPayload.fromJson(Map json) => + _$NotificationPayloadFromJson(json); +} + abstract class LocalNotification { LocalNotification( this.title, @@ -146,7 +183,6 @@ abstract class LocalNotification { final String title; final String? body; - final String? payload; + final NotificationPayload? payload; final NotificationDetails notificationDetails; - final void Function(String? actionId, String? payload)? callback = null; } From 5373d1250c420e13d758f6a7fd55f0ce68c9f2b7 Mon Sep 17 00:00:00 2001 From: inc0g_ Date: Mon, 19 Aug 2024 17:24:26 +1000 Subject: [PATCH 247/979] Local notifications: remove redundant code --- .../notifications/local_notification_service.dart | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/lib/src/model/notifications/local_notification_service.dart b/lib/src/model/notifications/local_notification_service.dart index 461143339b..ad14ecab04 100644 --- a/lib/src/model/notifications/local_notification_service.dart +++ b/lib/src/model/notifications/local_notification_service.dart @@ -150,16 +150,6 @@ enum PayloadType { challenge, } -@freezed -class LocalnotificationResponse with _$LocalnotificationResponse { - factory LocalnotificationResponse({ - required int id, - required String? actionId, - required NotificationPayload payload, - String? input, - }) = _LocalnotificationResponse; -} - @Freezed(fromJson: true, toJson: true) class NotificationPayload with _$NotificationPayload { factory NotificationPayload({ From b5ff72d7364a6c298f613a806d210060a68c1740 Mon Sep 17 00:00:00 2001 From: inc0g_ Date: Mon, 19 Aug 2024 19:50:34 +1000 Subject: [PATCH 248/979] Challenge Notification: fix body string --- .../notifications/challenge_notification.dart | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/lib/src/model/notifications/challenge_notification.dart b/lib/src/model/notifications/challenge_notification.dart index a6d8efeeed..5f6d0e8808 100644 --- a/lib/src/model/notifications/challenge_notification.dart +++ b/lib/src/model/notifications/challenge_notification.dart @@ -94,9 +94,23 @@ class ChallengeNotification extends LocalNotification { String get body => _body(); String _body() { - final time = _challenge.days == null - ? '∞' - : '${_l10n.daysPerTurn}: ${_challenge.days}'; + final time = switch (_challenge.timeControl) { + ChallengeTimeControlType.clock => () { + final clock = _challenge.clock!; + final minutes = switch (clock.time.inSeconds) { + 15 => '¼', + 30 => '½', + 45 => '¾', + 90 => '1.5', + _ => clock.time.inMinutes, + }; + return '$minutes+${clock.increment.inSeconds}'; + }(), + ChallengeTimeControlType.correspondence => + '${_l10n.daysPerTurn}: ${_challenge.days}', + ChallengeTimeControlType.unlimited => '∞', + }; + return _challenge.rated ? '${_l10n.rated} • $time' : '${_l10n.casual} • $time'; From 19ce69015b3fa03e3b0b083a4f06412c83d3aaba Mon Sep 17 00:00:00 2001 From: inc0g_ Date: Mon, 19 Aug 2024 19:59:57 +1000 Subject: [PATCH 249/979] App: dismiss notification for cancelled challenge --- lib/src/app.dart | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/src/app.dart b/lib/src/app.dart index 192e0b8830..b9b3f1f8d0 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -317,6 +317,16 @@ class _EntryPointState extends ConsumerState<_EntryPointWidget> { final inward = current.value!.inward; final l10n = ref.read(l10nProvider).strings; + // if a challenge was cancelled by the challenger + prevIds + .where((id) => !inward.map((challenge) => challenge.id).contains(id)) + .forEach( + (id) => ref + .read(localNotificationServiceProvider) + .cancel(id.value.hashCode), + ); + + // if there is a new challenge inward.where((challenge) => !prevIds.contains(challenge.id)).forEach( (challenge) => ref .read(localNotificationServiceProvider) From 796c2ad6ec920c2de2243edea5e36d85514e60c4 Mon Sep 17 00:00:00 2001 From: inc0g_ Date: Mon, 19 Aug 2024 22:32:24 +1000 Subject: [PATCH 250/979] Challenge list item: simplify usage --- lib/src/model/lobby/create_game_service.dart | 2 + .../view/play/create_custom_game_screen.dart | 9 --- .../view/user/challenge_requests_screen.dart | 19 +----- lib/src/widgets/challenge_list_item.dart | 58 ++++++++++++++----- 4 files changed, 47 insertions(+), 41 deletions(-) diff --git a/lib/src/model/lobby/create_game_service.dart b/lib/src/model/lobby/create_game_service.dart index 97a36fd8d2..61bb6a83ed 100644 --- a/lib/src/model/lobby/create_game_service.dart +++ b/lib/src/model/lobby/create_game_service.dart @@ -4,6 +4,7 @@ import 'package:deep_pick/deep_pick.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/challenge/challenge.dart'; import 'package:lichess_mobile/src/model/challenge/challenge_repository.dart'; +import 'package:lichess_mobile/src/model/challenge/challenges.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; @@ -143,6 +144,7 @@ class CreateGameService { ); }), socketClient.stream.listen((event) async { + if (event.topic == 'challenges') ref.invalidate(challengesProvider); if (event.topic == 'reload') { try { final updatedChallenge = await repo.show(challenge.id); diff --git a/lib/src/view/play/create_custom_game_screen.dart b/lib/src/view/play/create_custom_game_screen.dart index 00f5ebf2ae..28ec9e86ae 100644 --- a/lib/src/view/play/create_custom_game_screen.dart +++ b/lib/src/view/play/create_custom_game_screen.dart @@ -16,7 +16,6 @@ import 'package:lichess_mobile/src/model/lobby/game_seek.dart'; import 'package:lichess_mobile/src/model/lobby/game_setup.dart'; import 'package:lichess_mobile/src/model/lobby/lobby_repository.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; -import 'package:lichess_mobile/src/styles/lichess_colors.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; @@ -230,12 +229,6 @@ class _ChallengesBodyState extends ConsumerState<_ChallengesBody> { const PlatformDivider(height: 1, cupertinoHasLeading: true), itemBuilder: (context, index) { final challenge = supportedChallenges[index]; - final time = challenge.days == null - ? '∞' - : '${context.l10n.daysPerTurn}: ${challenge.days}'; - final subtitle = challenge.rated - ? '${context.l10n.rated} • $time' - : '${context.l10n.casual} • $time'; final isMySeek = UserId.fromUserName(challenge.username) == session?.user.id; @@ -246,8 +239,6 @@ class _ChallengesBodyState extends ConsumerState<_ChallengesBody> { name: challenge.username, title: challenge.title, ), - subtitle: subtitle, - color: isMySeek ? LichessColors.green.withOpacity(0.2) : null, onPressed: isMySeek ? null : session == null diff --git a/lib/src/view/user/challenge_requests_screen.dart b/lib/src/view/user/challenge_requests_screen.dart index 4d60e71443..5f4d6b4abc 100644 --- a/lib/src/view/user/challenge_requests_screen.dart +++ b/lib/src/view/user/challenge_requests_screen.dart @@ -7,7 +7,6 @@ import 'package:lichess_mobile/src/model/challenge/challenge_repository.dart'; import 'package:lichess_mobile/src/model/challenge/challenges.dart'; import 'package:lichess_mobile/src/model/notifications/local_notification_service.dart'; import 'package:lichess_mobile/src/navigation.dart'; -import 'package:lichess_mobile/src/styles/lichess_colors.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/game/game_screen.dart'; @@ -69,23 +68,10 @@ class _Body extends ConsumerWidget { final user = challenge.challenger?.user; if (user == null) return null; - final time = challenge.days == null - ? '∞' - : '${context.l10n.daysPerTurn}: ${challenge.days}'; - final subtitle = challenge.rated - ? '${context.l10n.rated} • $time' - : '${context.l10n.casual} • $time'; - final isMyChallenge = - challenge.direction == ChallengeDirection.outward; - return ChallengeListItem( challenge: challenge, user: user, - subtitle: subtitle, - color: isMyChallenge - ? LichessColors.green.withOpacity(0.2) - : null, - onPressed: isMyChallenge + onPressed: challenge.direction == ChallengeDirection.outward ? null : session == null ? () { @@ -120,8 +106,7 @@ class _Body extends ConsumerWidget { }, ); }, - cancelText: !isMyChallenge ? context.l10n.decline : null, - onCancel: isMyChallenge + onCancel: challenge.direction == ChallengeDirection.outward ? () => challengeRepo.cancel(challenge.id) : () { challengeRepo.decline(challenge.id); diff --git a/lib/src/widgets/challenge_list_item.dart b/lib/src/widgets/challenge_list_item.dart index 82135fe04b..ba952eed9a 100644 --- a/lib/src/widgets/challenge_list_item.dart +++ b/lib/src/widgets/challenge_list_item.dart @@ -1,39 +1,62 @@ import 'package:dartchess/dartchess.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; +import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/challenge/challenge.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/speed.dart'; import 'package:lichess_mobile/src/model/lobby/correspondence_challenge.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; +import 'package:lichess_mobile/src/styles/lichess_colors.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/user_full_name.dart'; -class ChallengeListItem extends StatelessWidget { +class ChallengeListItem extends ConsumerWidget { const ChallengeListItem({ super.key, required this.challenge, required this.user, - required this.subtitle, - this.cancelText, - this.color, this.onPressed, this.onCancel, }); final Challenge challenge; final LightUser user; - final String subtitle; - final String? cancelText; - final Color? color; final VoidCallback? onPressed; final VoidCallback? onCancel; @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final me = ref.read(authSessionProvider)?.user; + final isMyChallenge = me != null && me.id == user.id; + + final time = switch (challenge.timeControl) { + ChallengeTimeControlType.clock => () { + final clock = challenge.clock!; + final minutes = switch (clock.time.inSeconds) { + 15 => '¼', + 30 => '½', + 45 => '¾', + 90 => '1.5', + _ => clock.time.inMinutes, + }; + return '$minutes+${clock.increment.inSeconds}'; + }(), + ChallengeTimeControlType.correspondence => + '${context.l10n.daysPerTurn}: ${challenge.days}', + ChallengeTimeControlType.unlimited => '∞', + }; + + final subtitle = challenge.rated + ? '${context.l10n.rated} • $time' + : '${context.l10n.casual} • $time'; + + final color = isMyChallenge ? null : LichessColors.green.withOpacity(0.2); + return Container( color: color, child: Slidable( @@ -47,7 +70,9 @@ class ChallengeListItem extends StatelessWidget { backgroundColor: context.lichessColors.error, foregroundColor: Colors.white, icon: Icons.cancel, - label: cancelText ?? context.l10n.cancel, + label: isMyChallenge + ? context.l10n.cancel + : context.l10n.decline, ), ], ) @@ -62,7 +87,13 @@ class ChallengeListItem extends StatelessWidget { ? LichessIcons.circle : LichessIcons.circle_empty, ), - title: UserFullNameWidget(user: user), + title: isMyChallenge + ? UserFullNameWidget( + user: challenge.destUser != null + ? challenge.destUser!.user + : user, + ) + : UserFullNameWidget(user: user), subtitle: Text(subtitle), onTap: onPressed, ), @@ -76,16 +107,12 @@ class CorrespondenceChallengeListItem extends StatelessWidget { super.key, required this.challenge, required this.user, - required this.subtitle, - this.color, this.onPressed, this.onCancel, }); final CorrespondenceChallenge challenge; final LightUser user; - final String subtitle; - final Color? color; final VoidCallback? onPressed; final VoidCallback? onCancel; @@ -107,7 +134,8 @@ class CorrespondenceChallengeListItem extends StatelessWidget { days: challenge.days, ), user: user, - subtitle: subtitle, + onPressed: onPressed, + onCancel: onCancel, ); } } From 851fa39555856db41d3f0397dba6000810c60a6c Mon Sep 17 00:00:00 2001 From: inc0g_ Date: Thu, 29 Aug 2024 21:37:51 +1000 Subject: [PATCH 251/979] System: revert and add new error check --- lib/src/app_initialization.dart | 2 +- lib/src/utils/system.dart | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/src/app_initialization.dart b/lib/src/app_initialization.dart index 35e9db1ed2..8aff94ad36 100644 --- a/lib/src/app_initialization.dart +++ b/lib/src/app_initialization.dart @@ -120,7 +120,7 @@ Future appInitialization( } } - final physicalMemory = await System.instance.getTotalRam(); + final physicalMemory = await System.instance.getTotalRam() ?? 256.0; final engineMaxMemory = (physicalMemory / 10).ceil(); return AppInitializationData( diff --git a/lib/src/utils/system.dart b/lib/src/utils/system.dart index 228ad7e491..f63a2e5869 100644 --- a/lib/src/utils/system.dart +++ b/lib/src/utils/system.dart @@ -14,11 +14,14 @@ class System { static const instance = System._(); /// Returns the system total RAM in megabytes. - Future getTotalRam() async { + Future getTotalRam() async { try { - return (await _channel.invokeMethod('getTotalRam'))!; - } catch (_) { - return 256; + return await _channel.invokeMethod('getTotalRam'); + } on PlatformException catch (e) { + debugPrint('Failed to get total RAM: ${e.message}'); + return null; + } on MissingPluginException catch (_) { + return null; } } From a0c12815bee3e38f6615b76ba211f1a5e7a01c9f Mon Sep 17 00:00:00 2001 From: inc0g_ Date: Thu, 29 Aug 2024 21:38:21 +1000 Subject: [PATCH 252/979] Challenge List Item: relocate --- lib/src/{widgets => view/play}/challenge_list_item.dart | 2 +- lib/src/view/play/create_custom_game_screen.dart | 2 +- lib/src/view/user/challenge_requests_screen.dart | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename lib/src/{widgets => view/play}/challenge_list_item.dart (98%) diff --git a/lib/src/widgets/challenge_list_item.dart b/lib/src/view/play/challenge_list_item.dart similarity index 98% rename from lib/src/widgets/challenge_list_item.dart rename to lib/src/view/play/challenge_list_item.dart index ba952eed9a..6184a9f0b5 100644 --- a/lib/src/widgets/challenge_list_item.dart +++ b/lib/src/view/play/challenge_list_item.dart @@ -31,7 +31,7 @@ class ChallengeListItem extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final me = ref.read(authSessionProvider)?.user; + final me = ref.watch(authSessionProvider)?.user; final isMyChallenge = me != null && me.id == user.id; final time = switch (challenge.timeControl) { diff --git a/lib/src/view/play/create_custom_game_screen.dart b/lib/src/view/play/create_custom_game_screen.dart index 28ec9e86ae..6b3810eabb 100644 --- a/lib/src/view/play/create_custom_game_screen.dart +++ b/lib/src/view/play/create_custom_game_screen.dart @@ -20,10 +20,10 @@ import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/game/game_screen.dart'; +import 'package:lichess_mobile/src/view/play/challenge_list_item.dart'; import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; import 'package:lichess_mobile/src/widgets/adaptive_choice_picker.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; -import 'package:lichess_mobile/src/widgets/challenge_list_item.dart'; import 'package:lichess_mobile/src/widgets/expanded_section.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; diff --git a/lib/src/view/user/challenge_requests_screen.dart b/lib/src/view/user/challenge_requests_screen.dart index 5f4d6b4abc..504be82bca 100644 --- a/lib/src/view/user/challenge_requests_screen.dart +++ b/lib/src/view/user/challenge_requests_screen.dart @@ -10,8 +10,8 @@ import 'package:lichess_mobile/src/navigation.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/game/game_screen.dart'; +import 'package:lichess_mobile/src/view/play/challenge_list_item.dart'; import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; -import 'package:lichess_mobile/src/widgets/challenge_list_item.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; From 13c9dcecc5785bf35992e5cab9efb4d5cc37d0f5 Mon Sep 17 00:00:00 2001 From: inc0g_ Date: Thu, 29 Aug 2024 21:39:33 +1000 Subject: [PATCH 253/979] Challenges: listen to global socket stream --- lib/src/model/challenge/challenges.dart | 32 +++++++------------- lib/src/model/common/socket.dart | 2 ++ lib/src/model/lobby/create_game_service.dart | 2 -- 3 files changed, 13 insertions(+), 23 deletions(-) diff --git a/lib/src/model/challenge/challenges.dart b/lib/src/model/challenge/challenges.dart index f79859f7e8..10496b0d01 100644 --- a/lib/src/model/challenge/challenges.dart +++ b/lib/src/model/challenge/challenges.dart @@ -12,29 +12,19 @@ part 'challenges.g.dart'; @Riverpod(keepAlive: true) class Challenges extends _$Challenges { - StreamSubscription? _subscription; - - late SocketClient _socketClient; - @override Future build() async { - _socketClient = ref.watch(socketPoolProvider).open(Uri(path: '/socket/v5')); - - _subscription?.cancel(); - _subscription = _socketClient.stream.listen(_handleSocketEvent); - - // invalidate the challenges list if the user signs out - ref.listen( - authSessionProvider, - (prev, now) { - if (now == null) return; - ref.invalidateSelf(); - }, - ); - - ref.onDispose(() { - _subscription?.cancel(); - }); + socketGlobalStream.listen(_handleSocketEvent); + + final session = ref.watch(authSessionProvider); + if (session == null) { + return Future.value( + ( + inward: const IList.empty(), + outward: const IList.empty(), + ), + ); + } return ref.read(challengeRepositoryProvider).list(); } diff --git a/lib/src/model/common/socket.dart b/lib/src/model/common/socket.dart index 58a503ae86..762e2b3367 100644 --- a/lib/src/model/common/socket.dart +++ b/lib/src/model/common/socket.dart @@ -40,6 +40,7 @@ final _logger = Logger('Socket'); const _globalSocketStreamAllowedTopics = { 'n', 'message', + 'challenges', }; final _globalStreamController = StreamController.broadcast(); @@ -49,6 +50,7 @@ final _globalStreamController = StreamController.broadcast(); /// Only a subset of topics are allowed to be broadcasted to the global stream: /// - 'n' (number of players and games currently on the server) /// - 'message' +/// - 'challenges' final socketGlobalStream = _globalStreamController.stream; /// Creates a WebSocket URI for the lichess server. diff --git a/lib/src/model/lobby/create_game_service.dart b/lib/src/model/lobby/create_game_service.dart index 61bb6a83ed..97a36fd8d2 100644 --- a/lib/src/model/lobby/create_game_service.dart +++ b/lib/src/model/lobby/create_game_service.dart @@ -4,7 +4,6 @@ import 'package:deep_pick/deep_pick.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/challenge/challenge.dart'; import 'package:lichess_mobile/src/model/challenge/challenge_repository.dart'; -import 'package:lichess_mobile/src/model/challenge/challenges.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; @@ -144,7 +143,6 @@ class CreateGameService { ); }), socketClient.stream.listen((event) async { - if (event.topic == 'challenges') ref.invalidate(challengesProvider); if (event.topic == 'reload') { try { final updatedChallenge = await repo.show(challenge.id); From 222d04805aa02989658004132fac48023db0f58c Mon Sep 17 00:00:00 2001 From: inc0g_ Date: Thu, 29 Aug 2024 22:00:01 +1000 Subject: [PATCH 254/979] Challenge Service: move listening to the challenge service --- lib/src/app.dart | 26 ---------- .../model/challenge/challenge_repository.dart | 2 +- .../notifications/challenge_notification.dart | 47 ++++++++++++++----- .../local_notification_service.dart | 14 ++++-- 4 files changed, 45 insertions(+), 44 deletions(-) diff --git a/lib/src/app.dart b/lib/src/app.dart index b9b3f1f8d0..bec5ecf01a 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -12,11 +12,9 @@ import 'package:lichess_mobile/main.dart'; import 'package:lichess_mobile/src/app_initialization.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; -import 'package:lichess_mobile/src/model/challenge/challenges.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/correspondence/correspondence_service.dart'; -import 'package:lichess_mobile/src/model/notifications/challenge_notification.dart'; import 'package:lichess_mobile/src/model/notifications/local_notification_service.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/model/settings/brightness.dart'; @@ -25,7 +23,6 @@ import 'package:lichess_mobile/src/navigation.dart'; import 'package:lichess_mobile/src/notification_service.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/connectivity.dart'; -import 'package:lichess_mobile/src/utils/l10n.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/utils/system.dart'; @@ -310,29 +307,6 @@ class _EntryPointState extends ConsumerState<_EntryPointWidget> { }); ref.read(localNotificationServiceProvider).init(); - - ref.listenManual(challengesProvider, (prev, current) { - if (prev == null || !prev.hasValue || !current.hasValue) return; - final prevIds = prev.value!.inward.map((challenge) => challenge.id); - final inward = current.value!.inward; - final l10n = ref.read(l10nProvider).strings; - - // if a challenge was cancelled by the challenger - prevIds - .where((id) => !inward.map((challenge) => challenge.id).contains(id)) - .forEach( - (id) => ref - .read(localNotificationServiceProvider) - .cancel(id.value.hashCode), - ); - - // if there is a new challenge - inward.where((challenge) => !prevIds.contains(challenge.id)).forEach( - (challenge) => ref - .read(localNotificationServiceProvider) - .show(ChallengeNotification(challenge, l10n)), - ); - }); } @override diff --git a/lib/src/model/challenge/challenge_repository.dart b/lib/src/model/challenge/challenge_repository.dart index 4d0075c38f..3eea52e106 100644 --- a/lib/src/model/challenge/challenge_repository.dart +++ b/lib/src/model/challenge/challenge_repository.dart @@ -10,7 +10,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'challenge_repository.g.dart'; -@riverpod +@Riverpod(keepAlive: true) ChallengeRepository challengeRepository(ChallengeRepositoryRef ref) { return ChallengeRepository(ref.read(lichessClientProvider)); } diff --git a/lib/src/model/notifications/challenge_notification.dart b/lib/src/model/notifications/challenge_notification.dart index 5f6d0e8808..87e079d01e 100644 --- a/lib/src/model/notifications/challenge_notification.dart +++ b/lib/src/model/notifications/challenge_notification.dart @@ -8,6 +8,7 @@ import 'package:lichess_mobile/src/model/challenge/challenges.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/notifications/local_notification_service.dart'; import 'package:lichess_mobile/src/navigation.dart'; +import 'package:lichess_mobile/src/utils/l10n.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/game/game_screen.dart'; import 'package:lichess_mobile/src/view/user/challenge_requests_screen.dart'; @@ -15,7 +16,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'challenge_notification.g.dart'; -@riverpod +@Riverpod(keepAlive: true) ChallengeService challengeService(ChallengeServiceRef ref) { return ChallengeService(ref); } @@ -25,6 +26,31 @@ class ChallengeService { final ChallengeServiceRef ref; + void init() { + ref.listen(challengesProvider, (prev, current) { + if (prev == null || !prev.hasValue || !current.hasValue) return; + final prevIds = prev.value!.inward.map((challenge) => challenge.id); + final inward = current.value!.inward; + final l10n = ref.read(l10nProvider).strings; + + // if a challenge was cancelled by the challenger + prevIds + .where((id) => !inward.map((challenge) => challenge.id).contains(id)) + .forEach( + (id) => ref + .read(localNotificationServiceProvider) + .cancel(id.value.hashCode), + ); + + // if there is a new challenge + inward.where((challenge) => !prevIds.contains(challenge.id)).forEach( + (challenge) => ref + .read(localNotificationServiceProvider) + .show(ChallengeNotification(challenge, l10n)), + ); + }); + } + Future onNotificationResponse( int id, String? actionid, @@ -47,9 +73,6 @@ class ChallengeService { navState.popUntil((route) => route.isFirst); } - // sometimes becuase GameScreen connects to a socket we dont recieve a new event on the event listener in time - // so we just make sure to refresh this value when we go back to the home tab - ref.invalidate(challengesProvider); pushPlatformRoute( context, rootNavigator: true, @@ -88,7 +111,7 @@ class ChallengeNotification extends LocalNotification { @override NotificationPayload get payload => - ChallengePayload(_challenge.id).toNotifiationPayload(); + ChallengePayload(_challenge.id).notificationPayload; @override String get body => _body(); @@ -122,14 +145,12 @@ class ChallengePayload { final ChallengeId id; - NotificationPayload toNotifiationPayload() { - return NotificationPayload( - type: PayloadType.challenge, - data: { - 'id': id.value, - }, - ); - } + NotificationPayload get notificationPayload => NotificationPayload( + type: PayloadType.challenge, + data: { + 'id': id.value, + }, + ); factory ChallengePayload.fromNotificationPayload( NotificationPayload payload, diff --git a/lib/src/model/notifications/local_notification_service.dart b/lib/src/model/notifications/local_notification_service.dart index ad14ecab04..083848af28 100644 --- a/lib/src/model/notifications/local_notification_service.dart +++ b/lib/src/model/notifications/local_notification_service.dart @@ -37,6 +37,8 @@ class LocalNotificationService { _updateLocalisations(); }); + _initServices(); + await _notificationPlugin.initialize( InitializationSettings( android: const AndroidInitializationSettings('logo_black'), @@ -56,6 +58,10 @@ class LocalNotificationService { _log.info('initialized'); } + void _initServices() { + _ref.read(challengeServiceProvider).init(); + } + void _updateLocalisations() { final l10n = _ref.read(l10nProvider); InfoNotificationDetails(l10n.strings); @@ -90,6 +96,7 @@ class LocalNotificationService { void _handleResponse(int id, String? actionId, NotificationPayload payload) { switch (payload.type) { case PayloadType.info: + break; case PayloadType.challenge: _ref.read(challengeServiceProvider).onNotificationResponse( id, @@ -117,9 +124,6 @@ class LocalNotificationService { static void _notificationBackgroundResponse( NotificationResponse response, ) { - final logger = Logger('LocalNotificationService'); - logger.info('processing response in background. id [${response.id}]'); - // create a new provider scope for the background isolate final ref = ProviderContainer(); @@ -136,7 +140,9 @@ class LocalNotificationService { .read(localNotificationServiceProvider) ._handleResponse(response.id!, response.actionId, payload); } catch (e) { - logger.warning('failed to parse notification payload: $e'); + debugPrint( + 'failed to parse notification payload: $e', + ); // loggers dont work from the background isolate } }, ); From c405b9ddbd29f59db653a1b13018c52f6364ab8d Mon Sep 17 00:00:00 2001 From: inc0g_ Date: Wed, 4 Sep 2024 15:04:13 +1000 Subject: [PATCH 255/979] Challenge Service: move to new file and rework --- .../model/challenge/challenge_service.dart | 113 ++++++++++++++++++ lib/src/model/challenge/challenges.dart | 24 ++-- .../notifications/challenge_notification.dart | 91 -------------- .../local_notification_service.dart | 1 + 4 files changed, 124 insertions(+), 105 deletions(-) create mode 100644 lib/src/model/challenge/challenge_service.dart diff --git a/lib/src/model/challenge/challenge_service.dart b/lib/src/model/challenge/challenge_service.dart new file mode 100644 index 0000000000..4f94b31848 --- /dev/null +++ b/lib/src/model/challenge/challenge_service.dart @@ -0,0 +1,113 @@ +import 'dart:async'; + +import 'package:deep_pick/deep_pick.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/widgets.dart'; +import 'package:lichess_mobile/src/model/challenge/challenge.dart'; +import 'package:lichess_mobile/src/model/challenge/challenge_repository.dart'; +import 'package:lichess_mobile/src/model/common/socket.dart'; +import 'package:lichess_mobile/src/model/notifications/challenge_notification.dart'; +import 'package:lichess_mobile/src/model/notifications/local_notification_service.dart'; +import 'package:lichess_mobile/src/navigation.dart'; +import 'package:lichess_mobile/src/utils/l10n.dart'; +import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/view/game/game_screen.dart'; +import 'package:lichess_mobile/src/view/user/challenge_requests_screen.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'challenge_service.g.dart'; + +@Riverpod(keepAlive: true) +ChallengeService challengeService(ChallengeServiceRef ref) { + return ChallengeService(ref); +} + +final challengeStreamController = StreamController.broadcast(); +final challengeStream = challengeStreamController.stream; + +class ChallengeService { + ChallengeService(this.ref); + + final ChallengeServiceRef ref; + + ChallengesList? _current; + ChallengesList? _previous; + + void init() { + socketGlobalStream.listen((event) { + if (event.topic != 'challenges') return; + + final listPick = pick(event.data).required(); + final inward = listPick('in').asListOrEmpty(Challenge.fromPick); + final outward = listPick('out').asListOrEmpty(Challenge.fromPick); + + _previous = _current; + _current = (inward: inward.lock, outward: outward.lock); + challengeStreamController.add(_current!); + + if (_previous == null) return; + final prevIds = _previous!.inward.map((challenge) => challenge.id); + final l10n = ref.read(l10nProvider).strings; + + // if a challenge was cancelled by the challenger + prevIds + .where((id) => !inward.map((challenge) => challenge.id).contains(id)) + .forEach( + (id) => ref + .read(localNotificationServiceProvider) + .cancel(id.value.hashCode), + ); + + // if there is a new challenge + inward.where((challenge) => !prevIds.contains(challenge.id)).forEach( + (challenge) => ref + .read(localNotificationServiceProvider) + .show(ChallengeNotification(challenge, l10n)), + ); + }); + } + + Future onNotificationResponse( + int id, + String? actionid, + ChallengePayload payload, + ) async { + switch (actionid) { + case 'accept': + final repo = ref.read(challengeRepositoryProvider); + await repo.accept(payload.id); + + final fullId = await repo + .show(payload.id) + .then((challenge) => challenge.gameFullId); + + final context = ref.read(currentNavigatorKeyProvider).currentContext!; + if (!context.mounted) break; + + final navState = Navigator.of(context); + if (navState.canPop()) { + navState.popUntil((route) => route.isFirst); + } + + pushPlatformRoute( + context, + rootNavigator: true, + builder: (BuildContext context) => GameScreen(initialGameId: fullId), + ); + + case null: + final context = ref.read(currentNavigatorKeyProvider).currentContext!; + final navState = Navigator.of(context); + if (navState.canPop()) { + navState.popUntil((route) => route.isFirst); + } + pushPlatformRoute( + context, + builder: (BuildContext context) => const ChallengeRequestsScreen(), + ); + case 'decline': + final repo = ref.read(challengeRepositoryProvider); + repo.decline(payload.id); + } + } +} diff --git a/lib/src/model/challenge/challenges.dart b/lib/src/model/challenge/challenges.dart index 10496b0d01..5a7dc7cfdf 100644 --- a/lib/src/model/challenge/challenges.dart +++ b/lib/src/model/challenge/challenges.dart @@ -1,20 +1,26 @@ import 'dart:async'; -import 'package:deep_pick/deep_pick.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/challenge/challenge.dart'; import 'package:lichess_mobile/src/model/challenge/challenge_repository.dart'; -import 'package:lichess_mobile/src/model/common/socket.dart'; +import 'package:lichess_mobile/src/model/challenge/challenge_service.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'challenges.g.dart'; -@Riverpod(keepAlive: true) +@riverpod class Challenges extends _$Challenges { + StreamSubscription? _subscription; + @override Future build() async { - socketGlobalStream.listen(_handleSocketEvent); + _subscription = + challengeStream.listen((list) => state = AsyncValue.data(list)); + + ref.onDispose(() { + _subscription?.cancel(); + }); final session = ref.watch(authSessionProvider); if (session == null) { @@ -28,14 +34,4 @@ class Challenges extends _$Challenges { return ref.read(challengeRepositoryProvider).list(); } - - void _handleSocketEvent(SocketEvent event) { - if (event.topic != 'challenges') return; - - final listPick = pick(event.data).required(); - final inward = listPick('in').asListOrEmpty(Challenge.fromPick); - final outward = listPick('out').asListOrEmpty(Challenge.fromPick); - - state = AsyncValue.data((inward: inward.lock, outward: outward.lock)); - } } diff --git a/lib/src/model/notifications/challenge_notification.dart b/lib/src/model/notifications/challenge_notification.dart index 87e079d01e..9ac58664c3 100644 --- a/lib/src/model/notifications/challenge_notification.dart +++ b/lib/src/model/notifications/challenge_notification.dart @@ -1,100 +1,9 @@ -import 'package:flutter/widgets.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:lichess_mobile/l10n/l10n.dart'; import 'package:lichess_mobile/l10n/l10n_en.dart'; import 'package:lichess_mobile/src/model/challenge/challenge.dart'; -import 'package:lichess_mobile/src/model/challenge/challenge_repository.dart'; -import 'package:lichess_mobile/src/model/challenge/challenges.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/notifications/local_notification_service.dart'; -import 'package:lichess_mobile/src/navigation.dart'; -import 'package:lichess_mobile/src/utils/l10n.dart'; -import 'package:lichess_mobile/src/utils/navigation.dart'; -import 'package:lichess_mobile/src/view/game/game_screen.dart'; -import 'package:lichess_mobile/src/view/user/challenge_requests_screen.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'challenge_notification.g.dart'; - -@Riverpod(keepAlive: true) -ChallengeService challengeService(ChallengeServiceRef ref) { - return ChallengeService(ref); -} - -class ChallengeService { - ChallengeService(this.ref); - - final ChallengeServiceRef ref; - - void init() { - ref.listen(challengesProvider, (prev, current) { - if (prev == null || !prev.hasValue || !current.hasValue) return; - final prevIds = prev.value!.inward.map((challenge) => challenge.id); - final inward = current.value!.inward; - final l10n = ref.read(l10nProvider).strings; - - // if a challenge was cancelled by the challenger - prevIds - .where((id) => !inward.map((challenge) => challenge.id).contains(id)) - .forEach( - (id) => ref - .read(localNotificationServiceProvider) - .cancel(id.value.hashCode), - ); - - // if there is a new challenge - inward.where((challenge) => !prevIds.contains(challenge.id)).forEach( - (challenge) => ref - .read(localNotificationServiceProvider) - .show(ChallengeNotification(challenge, l10n)), - ); - }); - } - - Future onNotificationResponse( - int id, - String? actionid, - ChallengePayload payload, - ) async { - switch (actionid) { - case 'accept': - final repo = ref.read(challengeRepositoryProvider); - await repo.accept(payload.id); - - final fullId = await repo - .show(payload.id) - .then((challenge) => challenge.gameFullId); - - final context = ref.read(currentNavigatorKeyProvider).currentContext!; - if (!context.mounted) break; - - final navState = Navigator.of(context); - if (navState.canPop()) { - navState.popUntil((route) => route.isFirst); - } - - pushPlatformRoute( - context, - rootNavigator: true, - builder: (BuildContext context) => GameScreen(initialGameId: fullId), - ); - - case null: - final context = ref.read(currentNavigatorKeyProvider).currentContext!; - final navState = Navigator.of(context); - if (navState.canPop()) { - navState.popUntil((route) => route.isFirst); - } - pushPlatformRoute( - context, - builder: (BuildContext context) => const ChallengeRequestsScreen(), - ); - case 'decline': - final repo = ref.read(challengeRepositoryProvider); - repo.decline(payload.id); - } - } -} class ChallengeNotification extends LocalNotification { ChallengeNotification(this._challenge, this._l10n) diff --git a/lib/src/model/notifications/local_notification_service.dart b/lib/src/model/notifications/local_notification_service.dart index 083848af28..37fb8c6d70 100644 --- a/lib/src/model/notifications/local_notification_service.dart +++ b/lib/src/model/notifications/local_notification_service.dart @@ -6,6 +6,7 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/app_initialization.dart'; +import 'package:lichess_mobile/src/model/challenge/challenge_service.dart'; import 'package:lichess_mobile/src/model/notifications/challenge_notification.dart'; import 'package:lichess_mobile/src/model/notifications/info_notification.dart'; import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; From e7cfc2f2136943598357f8e1c31a746b7b81b0cf Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 4 Sep 2024 11:57:39 +0200 Subject: [PATCH 256/979] Explorer layout and alignment fixes --- .../opening_explorer_screen.dart | 242 +++++++++--------- .../opening_explorer_screen_test.dart | 26 +- 2 files changed, 140 insertions(+), 128 deletions(-) diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index b886bbdc09..223b9ff3fd 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -70,11 +70,11 @@ class _Body extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return Column( - children: [ - Expanded( - child: SafeArea( - bottom: false, + return SafeArea( + bottom: false, + child: Column( + children: [ + Expanded( child: LayoutBuilder( builder: (context, constraints) { final aspectRatio = constraints.biggest.aspectRatio; @@ -135,7 +135,6 @@ class _Body extends ConsumerWidget { ], ) : Column( - mainAxisAlignment: MainAxisAlignment.center, children: [ if (isTablet) Padding( @@ -164,9 +163,9 @@ class _Body extends ConsumerWidget { }, ), ), - ), - _BottomBar(pgn: pgn, options: options), - ], + _BottomBar(pgn: pgn, options: options), + ], + ), ); } } @@ -283,6 +282,9 @@ class _OpeningExplorerState extends ConsumerState<_OpeningExplorer> { ); } + final topGames = openingExplorer.entry.topGames; + final recentGames = openingExplorer.entry.recentGames; + final explorerContent = [ OpeningExplorerMoveTable( moves: openingExplorer.entry.moves, @@ -292,18 +294,36 @@ class _OpeningExplorerState extends ConsumerState<_OpeningExplorer> { pgn: widget.pgn, options: widget.options, ), - if (openingExplorer.entry.topGames != null && - openingExplorer.entry.topGames!.isNotEmpty) - OpeningExplorerGameList( - title: context.l10n.topGames, - games: openingExplorer.entry.topGames!, + if (topGames != null && topGames.isNotEmpty) ...[ + _OpeningExplorerHeader(child: Text(context.l10n.topGames)), + ...List.generate( + topGames.length, + (int index) { + return OpeningExplorerGameTile( + game: topGames.get(index), + color: index.isEven + ? Theme.of(context).colorScheme.surfaceContainerLow + : Theme.of(context).colorScheme.surfaceContainerHigh, + ); + }, + growable: false, ), - if (openingExplorer.entry.recentGames != null && - openingExplorer.entry.recentGames!.isNotEmpty) - OpeningExplorerGameList( - title: context.l10n.recentGames, - games: openingExplorer.entry.recentGames!, + ], + if (recentGames != null && recentGames.isNotEmpty) ...[ + _OpeningExplorerHeader(child: Text(context.l10n.recentGames)), + ...List.generate( + recentGames.length, + (int index) { + return OpeningExplorerGameTile( + game: recentGames.get(index), + color: index.isEven + ? Theme.of(context).colorScheme.surfaceContainerLow + : Theme.of(context).colorScheme.surfaceContainerHigh, + ); + }, + growable: false, ), + ], ]; lastExplorerWidgets = explorerContent; @@ -385,7 +405,6 @@ class _OpeningExplorerView extends StatelessWidget { : Colors.white; return Column( - mainAxisSize: MainAxisSize.max, children: [ Container( padding: _kTableRowPadding, @@ -416,23 +435,21 @@ class _OpeningExplorerView extends StatelessWidget { ), ), Expanded( - child: Center( - child: Stack( - children: [ - ListView(children: explorerContent), - Positioned.fill( - child: IgnorePointer( - ignoring: !loading, - child: AnimatedOpacity( - duration: const Duration(milliseconds: 300), - curve: Curves.fastOutSlowIn, - opacity: loading ? 0.5 : 0.0, - child: ColoredBox(color: loadingOverlayColor), - ), + child: Stack( + children: [ + ListView(padding: EdgeInsets.zero, children: explorerContent), + Positioned.fill( + child: IgnorePointer( + ignoring: !loading, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 300), + curve: Curves.fastOutSlowIn, + opacity: loading ? 0.5 : 0.0, + child: ColoredBox(color: loadingOverlayColor), ), ), - ], - ), + ), + ], ), ), ], @@ -550,58 +567,16 @@ class OpeningExplorerMoveTable extends ConsumerWidget { String formatNum(int num) => NumberFormat.decimalPatternDigits().format(num); + static const columnWidths = { + 0: FractionColumnWidth(0.15), + 1: FractionColumnWidth(0.35), + 2: FractionColumnWidth(0.50), + }; + @override Widget build(BuildContext context, WidgetRef ref) { - const columnWidths = { - 0: FractionColumnWidth(0.15), - 1: FractionColumnWidth(0.35), - 2: FractionColumnWidth(0.50), - }; - if (_isLoading) { - return Table( - columnWidths: columnWidths, - children: List.generate( - 10, - (int index) => TableRow( - children: [ - Padding( - padding: _kTableRowPadding, - child: Container( - height: 20, - width: double.infinity, - decoration: BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(5), - ), - ), - ), - Padding( - padding: _kTableRowPadding, - child: Container( - height: 20, - width: double.infinity, - decoration: BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(5), - ), - ), - ), - Padding( - padding: _kTableRowPadding, - child: Container( - height: 20, - width: double.infinity, - decoration: BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(5), - ), - ), - ), - ], - ), - ), - ); + return loadingTable; } final games = whiteWins + draws + blackWins; @@ -707,40 +682,50 @@ class OpeningExplorerMoveTable extends ConsumerWidget { ], ); } -} - -/// List of games for the opening explorer. -class OpeningExplorerGameList extends StatelessWidget { - const OpeningExplorerGameList({ - required this.title, - required this.games, - }); - - final String title; - final IList games; - @override - Widget build(BuildContext context) { - return Column( - children: [ - Container( - padding: _kTableRowPadding, - color: Theme.of(context).colorScheme.secondaryContainer, - child: Row( - children: [Text(title)], + static final loadingTable = Table( + columnWidths: columnWidths, + children: List.generate( + 10, + (int index) => TableRow( + children: [ + Padding( + padding: _kTableRowPadding, + child: Container( + height: 20, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(5), + ), + ), ), - ), - ...List.generate(games.length, (int index) { - return OpeningExplorerGameTile( - game: games.get(index), - color: index.isEven - ? Theme.of(context).colorScheme.surfaceContainerLow - : Theme.of(context).colorScheme.surfaceContainerHigh, - ); - }), - ], - ); - } + Padding( + padding: _kTableRowPadding, + child: Container( + height: 20, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(5), + ), + ), + ), + Padding( + padding: _kTableRowPadding, + child: Container( + height: 20, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(5), + ), + ), + ), + ], + ), + ), + ); } /// A game tile for the opening explorer. @@ -849,7 +834,12 @@ class OpeningExplorerGameTile extends ConsumerWidget { ), if (game.month != null) ...[ const SizedBox(width: 10.0), - Text(game.month!), + Text( + game.month!, + style: const TextStyle( + fontFeatures: [FontFeature.tabularFigures()], + ), + ), ], if (game.speed != null) ...[ const SizedBox(width: 10.0), @@ -864,6 +854,24 @@ class OpeningExplorerGameTile extends ConsumerWidget { } } +class _OpeningExplorerHeader extends StatelessWidget { + const _OpeningExplorerHeader({required this.child}); + + final Widget child; + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + padding: _kTableRowPadding, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.secondaryContainer, + ), + child: child, + ); + } +} + class _WinPercentageChart extends StatelessWidget { const _WinPercentageChart({ required this.whiteWins, diff --git a/test/view/opening_explorer/opening_explorer_screen_test.dart b/test/view/opening_explorer/opening_explorer_screen_test.dart index 562a85e3e0..8a50458f49 100644 --- a/test/view/opening_explorer/opening_explorer_screen_test.dart +++ b/test/view/opening_explorer/opening_explorer_screen_test.dart @@ -84,10 +84,12 @@ void main() { expect(find.widgetWithText(Container, 'Top games'), findsOneWidget); expect(find.widgetWithText(Container, 'Recent games'), findsNothing); - expect( - find.byType(OpeningExplorerGameList), - findsOneWidget, + + await tester.scrollUntilVisible( + find.text('Firouzja, A.'), + 200, ); + expect( find.byType(OpeningExplorerGameTile), findsNWidgets(2), @@ -133,10 +135,11 @@ void main() { expect(find.widgetWithText(Container, 'Top games'), findsNothing); expect(find.widgetWithText(Container, 'Recent games'), findsOneWidget); - expect( - find.byType(OpeningExplorerGameList), - findsOneWidget, + await tester.scrollUntilVisible( + find.byType(OpeningExplorerGameTile), + 200, ); + expect( find.byType(OpeningExplorerGameTile), findsOneWidget, @@ -182,10 +185,11 @@ void main() { expect(find.widgetWithText(Container, 'Top games'), findsNothing); expect(find.widgetWithText(Container, 'Recent games'), findsOneWidget); - expect( - find.byType(OpeningExplorerGameList), - findsOneWidget, + await tester.scrollUntilVisible( + find.byType(OpeningExplorerGameTile), + 200, ); + expect( find.byType(OpeningExplorerGameTile), findsOneWidget, @@ -248,8 +252,8 @@ const mastersOpeningExplorerResponse = ''' "rating": 2882 }, "white": { - "name": "Caruana, F.", - "rating": 2818 + "name": "Firouzja, A.", + "rating": 2808 }, "year": 2019, "month": "2019-08" From 8aba342b3a9075c5b857e69b81aa1635dccb05c2 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 4 Sep 2024 13:15:32 +0200 Subject: [PATCH 257/979] Fix archived game screen layout --- lib/src/view/game/archived_game_screen.dart | 23 ++++++++++++--------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/lib/src/view/game/archived_game_screen.dart b/lib/src/view/game/archived_game_screen.dart index f52fe09671..7a5a8fd986 100644 --- a/lib/src/view/game/archived_game_screen.dart +++ b/lib/src/view/game/archived_game_screen.dart @@ -43,17 +43,20 @@ class ArchivedGameScreen extends ConsumerWidget { ToggleSoundButton(), ], ), - body: Column( - children: [ - Expanded( - child: _BoardBody( - gameData: gameData, - orientation: orientation, - initialCursor: initialCursor, + body: SafeArea( + bottom: false, + child: Column( + children: [ + Expanded( + child: _BoardBody( + gameData: gameData, + orientation: orientation, + initialCursor: initialCursor, + ), ), - ), - _BottomBar(gameData: gameData, orientation: orientation), - ], + _BottomBar(gameData: gameData, orientation: orientation), + ], + ), ), ); } From 7da14bfe1876296ef678a4f34a57769872d51c75 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 4 Sep 2024 14:03:44 +0200 Subject: [PATCH 258/979] More explorer improvements - requests to fetch games are now cached and debounced - going to a game will jump now to the current position instead of the last one --- .../opening_explorer/opening_explorer.dart | 5 +- .../opening_explorer_screen.dart | 251 ++++++++++-------- .../opening_explorer_repository_test.dart | 2 +- .../opening_explorer_screen_test.dart | 2 +- 4 files changed, 152 insertions(+), 108 deletions(-) diff --git a/lib/src/model/opening_explorer/opening_explorer.dart b/lib/src/model/opening_explorer/opening_explorer.dart index 1bdeaba0a9..cfc34cede3 100644 --- a/lib/src/model/opening_explorer/opening_explorer.dart +++ b/lib/src/model/opening_explorer/opening_explorer.dart @@ -2,6 +2,7 @@ import 'package:deep_pick/deep_pick.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_preferences.dart'; @@ -61,7 +62,7 @@ class OpeningMove with _$OpeningMove { @Freezed(fromJson: true) class OpeningExplorerGame with _$OpeningExplorerGame { factory OpeningExplorerGame({ - required String id, + required GameId id, required ({String name, int rating}) white, required ({String name, int rating}) black, String? uci, @@ -77,7 +78,7 @@ class OpeningExplorerGame with _$OpeningExplorerGame { factory OpeningExplorerGame.fromPick(RequiredPick pick) { return OpeningExplorerGame( - id: pick('id').asStringOrThrow(), + id: pick('id').asGameIdOrThrow(), white: pick('white').letOrThrow( (pick) => ( name: pick('name').asStringOrThrow(), diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index 223b9ff3fd..7d720ac90a 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -7,7 +7,7 @@ import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; -import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/game/archived_game.dart'; import 'package:lichess_mobile/src/model/game/game_repository_providers.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_preferences.dart'; @@ -285,6 +285,8 @@ class _OpeningExplorerState extends ConsumerState<_OpeningExplorer> { final topGames = openingExplorer.entry.topGames; final recentGames = openingExplorer.entry.recentGames; + final ply = analysisState.position.ply; + final explorerContent = [ OpeningExplorerMoveTable( moves: openingExplorer.entry.moves, @@ -295,30 +297,40 @@ class _OpeningExplorerState extends ConsumerState<_OpeningExplorer> { options: widget.options, ), if (topGames != null && topGames.isNotEmpty) ...[ - _OpeningExplorerHeader(child: Text(context.l10n.topGames)), + _OpeningExplorerHeader( + key: const Key('topGamesHeader'), + child: Text(context.l10n.topGames), + ), ...List.generate( topGames.length, (int index) { return OpeningExplorerGameTile( + key: Key('top-game-${topGames.get(index).id}'), game: topGames.get(index), color: index.isEven ? Theme.of(context).colorScheme.surfaceContainerLow : Theme.of(context).colorScheme.surfaceContainerHigh, + ply: ply, ); }, growable: false, ), ], if (recentGames != null && recentGames.isNotEmpty) ...[ - _OpeningExplorerHeader(child: Text(context.l10n.recentGames)), + _OpeningExplorerHeader( + key: const Key('recentGamesHeader'), + child: Text(context.l10n.recentGames), + ), ...List.generate( recentGames.length, (int index) { return OpeningExplorerGameTile( + key: Key('recent-game-${recentGames.get(index).id}'), game: recentGames.get(index), color: index.isEven ? Theme.of(context).colorScheme.surfaceContainerLow : Theme.of(context).colorScheme.surfaceContainerHigh, + ply: ply, ); }, growable: false, @@ -729,133 +741,164 @@ class OpeningExplorerMoveTable extends ConsumerWidget { } /// A game tile for the opening explorer. -class OpeningExplorerGameTile extends ConsumerWidget { +class OpeningExplorerGameTile extends ConsumerStatefulWidget { const OpeningExplorerGameTile({ required this.game, required this.color, + required this.ply, + super.key, }); final OpeningExplorerGame game; final Color color; + final int ply; @override - Widget build(BuildContext context, WidgetRef ref) { + ConsumerState createState() => + _OpeningExplorerGameTileState(); +} + +class _OpeningExplorerGameTileState + extends ConsumerState { + Future? gameRequest; + + @override + Widget build(BuildContext context) { const widthResultBox = 50.0; const paddingResultBox = EdgeInsets.all(5); return Container( padding: _kTableRowPadding, - color: color, - child: AdaptiveInkWell( - onTap: () async { - final gameId = GameId(game.id); - final archivedGame = await ref.read( - archivedGameProvider(id: gameId).future, - ); - if (context.mounted) { - pushPlatformRoute( - context, - builder: (_) => ArchivedGameScreen( - gameData: archivedGame.data, - orientation: Side.white, - ), - ); - } - }, - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(game.white.rating.toString()), - Text(game.black.rating.toString()), - ], - ), - const SizedBox(width: 10), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(game.white.name, overflow: TextOverflow.ellipsis), - Text(game.black.name, overflow: TextOverflow.ellipsis), - ], - ), - ), - Row( + color: widget.color, + child: FutureBuilder( + future: gameRequest, + builder: (context, snapshot) { + return AdaptiveInkWell( + onTap: snapshot.connectionState == ConnectionState.waiting + ? null + : () async { + if (gameRequest == null) { + setState(() { + gameRequest = ref.read( + archivedGameProvider(id: widget.game.id).future, + ); + }); + } + + final archivedGame = await gameRequest!; + if (context.mounted) { + pushPlatformRoute( + context, + builder: (_) => ArchivedGameScreen( + gameData: archivedGame.data, + orientation: Side.white, + initialCursor: widget.ply, + ), + ); + } + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, children: [ - if (game.winner == 'white') - Container( - width: widthResultBox, - padding: paddingResultBox, - decoration: BoxDecoration( - color: _whiteBoxColor(context), - borderRadius: BorderRadius.circular(5), - ), - child: const Text( - '1-0', - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.black, - ), - ), - ) - else if (game.winner == 'black') - Container( - width: widthResultBox, - padding: paddingResultBox, - decoration: BoxDecoration( - color: _blackBoxColor(context), - borderRadius: BorderRadius.circular(5), - ), - child: const Text( - '0-1', - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white, + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(widget.game.white.rating.toString()), + Text(widget.game.black.rating.toString()), + ], + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.game.white.name, + overflow: TextOverflow.ellipsis, ), - ), - ) - else - Container( - width: widthResultBox, - padding: paddingResultBox, - decoration: BoxDecoration( - color: Colors.grey, - borderRadius: BorderRadius.circular(5), - ), - child: const Text( - '½-½', - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white, + Text( + widget.game.black.name, + overflow: TextOverflow.ellipsis, ), - ), - ), - if (game.month != null) ...[ - const SizedBox(width: 10.0), - Text( - game.month!, - style: const TextStyle( - fontFeatures: [FontFeature.tabularFigures()], - ), + ], ), - ], - if (game.speed != null) ...[ - const SizedBox(width: 10.0), - Icon(game.speed!.icon, size: 20), - ], + ), + Row( + children: [ + if (widget.game.winner == 'white') + Container( + width: widthResultBox, + padding: paddingResultBox, + decoration: BoxDecoration( + color: _whiteBoxColor(context), + borderRadius: BorderRadius.circular(5), + ), + child: const Text( + '1-0', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.black, + ), + ), + ) + else if (widget.game.winner == 'black') + Container( + width: widthResultBox, + padding: paddingResultBox, + decoration: BoxDecoration( + color: _blackBoxColor(context), + borderRadius: BorderRadius.circular(5), + ), + child: const Text( + '0-1', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + ), + ), + ) + else + Container( + width: widthResultBox, + padding: paddingResultBox, + decoration: BoxDecoration( + color: Colors.grey, + borderRadius: BorderRadius.circular(5), + ), + child: const Text( + '½-½', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + ), + ), + ), + if (widget.game.month != null) ...[ + const SizedBox(width: 10.0), + Text( + widget.game.month!, + style: const TextStyle( + fontFeatures: [FontFeature.tabularFigures()], + ), + ), + ], + if (widget.game.speed != null) ...[ + const SizedBox(width: 10.0), + Icon(widget.game.speed!.icon, size: 20), + ], + ], + ), ], ), - ], - ), + ); + }, ), ); } } class _OpeningExplorerHeader extends StatelessWidget { - const _OpeningExplorerHeader({required this.child}); + const _OpeningExplorerHeader({required this.child, super.key}); final Widget child; diff --git a/test/model/opening_explorer/opening_explorer_repository_test.dart b/test/model/opening_explorer/opening_explorer_repository_test.dart index 38ce825d07..80ebca81b3 100644 --- a/test/model/opening_explorer/opening_explorer_repository_test.dart +++ b/test/model/opening_explorer/opening_explorer_repository_test.dart @@ -184,7 +184,7 @@ void main() { "recentGames": [ { "uci": "e2e4", - "id": "abc", + "id": "RVb19S9O", "winner": "white", "speed": "bullet", "mode": "rated", diff --git a/test/view/opening_explorer/opening_explorer_screen_test.dart b/test/view/opening_explorer/opening_explorer_screen_test.dart index 8a50458f49..3098eddcd3 100644 --- a/test/view/opening_explorer/opening_explorer_screen_test.dart +++ b/test/view/opening_explorer/opening_explorer_screen_test.dart @@ -323,7 +323,7 @@ const playerOpeningExplorerResponse = ''' "recentGames": [ { "uci": "e2e4", - "id": "abc", + "id": "RVb19S9O", "winner": "white", "speed": "bullet", "mode": "rated", From b85c2bc239b93c6df6a951b7299e1d8520635166 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Tue, 3 Sep 2024 21:30:06 +0200 Subject: [PATCH 259/979] refactor: add _ToolsButton widget --- lib/src/view/tools/tools_tab_screen.dart | 214 ++++++++++------------- 1 file changed, 92 insertions(+), 122 deletions(-) diff --git a/lib/src/view/tools/tools_tab_screen.dart b/lib/src/view/tools/tools_tab_screen.dart index 2e7b30aa0f..cbfa82383e 100644 --- a/lib/src/view/tools/tools_tab_screen.dart +++ b/lib/src/view/tools/tools_tab_screen.dart @@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/navigation.dart'; +import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; @@ -12,6 +13,7 @@ import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; import 'package:lichess_mobile/src/view/board_editor/board_editor_screen.dart'; import 'package:lichess_mobile/src/view/clock/clock_screen.dart'; +import 'package:lichess_mobile/src/view/coordinate_training/coordinate_training_screen.dart'; import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_screen.dart'; import 'package:lichess_mobile/src/view/tools/load_position_screen.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; @@ -62,15 +64,55 @@ class ToolsTabScreen extends ConsumerWidget { } } -class _Body extends ConsumerWidget { - const _Body(); +class _ToolsButton extends StatelessWidget { + const _ToolsButton({ + required this.icon, + required this.title, + required this.onTap, + }); + + final IconData icon; + + final String title; + + final VoidCallback onTap; @override - Widget build(BuildContext context, WidgetRef ref) { + Widget build(BuildContext context) { final tilePadding = Theme.of(context).platform == TargetPlatform.iOS ? const EdgeInsets.symmetric(vertical: 8.0) : EdgeInsets.zero; + return Padding( + padding: Theme.of(context).platform == TargetPlatform.android + ? const EdgeInsets.only(bottom: 16.0) + : EdgeInsets.zero, + child: PlatformListTile( + leading: Icon( + icon, + size: Styles.mainListTileIconSize, + color: Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoTheme.of(context).primaryColor + : Theme.of(context).colorScheme.primary, + ), + title: Padding( + padding: tilePadding, + child: Text(title, style: Styles.callout), + ), + trailing: Theme.of(context).platform == TargetPlatform.iOS + ? const CupertinoListTileChevron() + : null, + onTap: onTap, + ), + ); + } +} + +class _Body extends ConsumerWidget { + const _Body(); + + @override + Widget build(BuildContext context, WidgetRef ref) { final isOnline = ref.watch(connectivityChangesProvider).valueOrNull?.isOnline ?? false; @@ -79,57 +121,42 @@ class _Body extends ConsumerWidget { ListSection( hasLeading: true, children: [ - Padding( - padding: Theme.of(context).platform == TargetPlatform.android - ? const EdgeInsets.only(bottom: 16.0) - : EdgeInsets.zero, - child: PlatformListTile( - leading: Icon( - Icons.upload_file, - size: Styles.mainListTileIconSize, - color: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoTheme.of(context).primaryColor - : Theme.of(context).colorScheme.primary, - ), - title: Padding( - padding: tilePadding, - child: Text(context.l10n.loadPosition, style: Styles.callout), - ), - trailing: Theme.of(context).platform == TargetPlatform.iOS - ? const CupertinoListTileChevron() - : null, - onTap: () => pushPlatformRoute( - context, - builder: (context) => const LoadPositionScreen(), - ), + _ToolsButton( + icon: Icons.upload_file, + title: context.l10n.loadPosition, + onTap: () => pushPlatformRoute( + context, + builder: (context) => const LoadPositionScreen(), ), ), - Padding( - padding: Theme.of(context).platform == TargetPlatform.android - ? const EdgeInsets.only(bottom: 16.0) - : EdgeInsets.zero, - child: PlatformListTile( - leading: Icon( - Icons.biotech, - size: Styles.mainListTileIconSize, - color: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoTheme.of(context).primaryColor - : Theme.of(context).colorScheme.primary, - ), - title: Padding( - padding: tilePadding, - child: Text(context.l10n.analysis, style: Styles.callout), + _ToolsButton( + icon: Icons.biotech, + title: context.l10n.analysis, + onTap: () => pushPlatformRoute( + context, + rootNavigator: true, + builder: (context) => const AnalysisScreen( + pgnOrId: '', + options: AnalysisOptions( + isLocalEvaluationAllowed: true, + variant: Variant.standard, + orientation: Side.white, + id: standaloneAnalysisId, + ), ), - trailing: Theme.of(context).platform == TargetPlatform.iOS - ? const CupertinoListTileChevron() - : null, + ), + ), + if (isOnline) + _ToolsButton( + icon: Icons.explore, + title: context.l10n.openingExplorer, onTap: () => pushPlatformRoute( context, rootNavigator: true, - builder: (context) => const AnalysisScreen( - pgnOrId: '', + builder: (context) => const OpeningExplorerScreen( + pgn: '', options: AnalysisOptions( - isLocalEvaluationAllowed: true, + isLocalEvaluationAllowed: false, variant: Variant.standard, orientation: Side.white, id: standaloneAnalysisId, @@ -137,84 +164,27 @@ class _Body extends ConsumerWidget { ), ), ), - ), - if (isOnline) - Padding( - padding: Theme.of(context).platform == TargetPlatform.android - ? const EdgeInsets.only(bottom: 16.0) - : EdgeInsets.zero, - child: PlatformListTile( - leading: Icon( - Icons.explore, - size: Styles.mainListTileIconSize, - color: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoTheme.of(context).primaryColor - : Theme.of(context).colorScheme.primary, - ), - title: Padding( - padding: tilePadding, - child: - Text(context.l10n.openingExplorer, style: Styles.callout), - ), - trailing: Theme.of(context).platform == TargetPlatform.iOS - ? const CupertinoListTileChevron() - : null, - onTap: () => pushPlatformRoute( - context, - rootNavigator: true, - builder: (context) => const OpeningExplorerScreen( - pgn: '', - options: AnalysisOptions( - isLocalEvaluationAllowed: false, - variant: Variant.standard, - orientation: Side.white, - id: standaloneAnalysisId, - ), - ), - ), - ), - ), - Padding( - padding: Theme.of(context).platform == TargetPlatform.android - ? const EdgeInsets.only(bottom: 16.0) - : EdgeInsets.zero, - child: PlatformListTile( - leading: Icon( - Icons.edit, - size: Styles.mainListTileIconSize, - color: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoTheme.of(context).primaryColor - : Theme.of(context).colorScheme.primary, - ), - title: Padding( - padding: tilePadding, - child: Text(context.l10n.boardEditor, style: Styles.callout), - ), - trailing: Theme.of(context).platform == TargetPlatform.iOS - ? const CupertinoListTileChevron() - : null, - onTap: () => pushPlatformRoute( - context, - builder: (context) => const BoardEditorScreen(), - rootNavigator: true, - ), + _ToolsButton( + icon: Icons.edit, + title: context.l10n.boardEditor, + onTap: () => pushPlatformRoute( + context, + builder: (context) => const BoardEditorScreen(), + rootNavigator: true, ), ), - PlatformListTile( - leading: Icon( - Icons.alarm, - size: Styles.mainListTileIconSize, - color: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoTheme.of(context).primaryColor - : Theme.of(context).colorScheme.primary, - ), - title: Padding( - padding: tilePadding, - child: Text(context.l10n.clock, style: Styles.callout), + _ToolsButton( + icon: LichessIcons.chess_board, + title: 'Coordinate Training', // TODO l10n + onTap: () => pushPlatformRoute( + context, + rootNavigator: true, + builder: (context) => const CoordinateTrainingScreen(), ), - trailing: Theme.of(context).platform == TargetPlatform.iOS - ? const CupertinoListTileChevron() - : null, + ), + _ToolsButton( + icon: Icons.alarm, + title: context.l10n.clock, onTap: () => pushPlatformRoute( context, builder: (context) => const ClockScreen(), From 211113de07522d69f9082b4be06bfd798322a580 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Wed, 4 Sep 2024 22:48:35 +0200 Subject: [PATCH 260/979] docs: various clean ups and improvements - Move dev-setup related docs from README.md to setting_dev_env.md, some of this was duplicated between the two. - Add "Before submitting a Pull Request" to CONTRIBUTING.md - Add section about `PlatformWidget` to coding_style.md --- CONTRIBUTING.md | 21 ++++++++-- README.md | 85 +---------------------------------------- docs/coding_style.md | 19 +++++++++ docs/setting_dev_env.md | 85 ++++++++++++++++++++++++++++++++++------- 4 files changed, 110 insertions(+), 100 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 56b8e1eec5..fad7f4fb8d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,11 +2,8 @@ ## I want to contribute code to Lichess Mobile -- [Set up your development environment](https://docs.flutter.dev/get-started/install); +- [Set up your development environment](https://github.com/lichess-org/mobile/blob/main/docs/setting_dev_env.md); - Communicate with other devs on [Discord](https://discord.gg/lichess). -- You may want to install a local instance of lila, [see lila wiki for that](https://github.com/lichess-org/lila/wiki/Lichess-Development-Onboarding); -- read the [coding style guide](https://github.com/lichess-org/mobile/blob/main/docs/coding_style.md) -- don't edit manually the `app_en.arb` file! See [about internationalization](https://github.com/lichess-org/mobile/blob/main/docs/internationalisation.md) in the docs. - check the [docs](https://github.com/lichess-org/mobile/tree/main/docs) for more documentation ### What to work on @@ -33,6 +30,22 @@ like, but check the existing pull requests to avoid duplicated work. Once you start working on an issue, submit a pull request as soon as possible (in draft mode if it's not ready yet) to let others know that you're working on it. +## Before submitting a Pull Request + +- Make sure your code follows the [coding style guide](https://github.com/lichess-org/mobile/blob/main/docs/coding_style.md) +- Don't manually edit the `app_en.arb` file! See the [internalizations docs](https://github.com/lichess-org/mobile/blob/main/docs/internationalisation.md) for instructions on how to add new translations. +- If possible, write a new widget test for your bugfix or new feature. +- Consider adding a screenshot and/or screen recording to the PR description. +- Run the linter and tests: +```sh +flutter analyze +flutter test +``` +- Ensure your code is formatted correctly (or use your editor's "format on save" feature): +```sh +dart format --output=none --set-exit-if-changed $(find lib/src -name "*.dart" -not \( -name "*.*freezed.dart" -o -name "*.*g.dart" -o -name "*lichess_icons.dart" \) ) +dart format --output=none --set-exit-if-changed $(find test -name "*.dart" -not \( -name "*.*freezed.dart" -o -name "*.*g.dart" \) ) +``` ## I want to report a bug or a problem about Lichess Mobile diff --git a/README.md b/README.md index 7999423c0d..d8822da6fb 100644 --- a/README.md +++ b/README.md @@ -8,90 +8,9 @@ Contributions to this project are welcome! If you want to contribute, please read the [contributing guide](./CONTRIBUTING.md). -## Setup +## Setup and Run -1. Follow the [Flutter guide](https://docs.flutter.dev/get-started/install) to - install Flutter and the platform of you choice (iOS and/or Android). **Note, if you're on Linux**, you should install flutter manually because there is an [issue](https://github.com/lichess-org/mobile/issues/123) with snapd when building Stockfish. Note that this project is not meant to be run on web platform. -2. Switch to the beta channel by running `flutter channel beta` and `flutter - upgrade` -3. Ensure Flutter is correctly configured by running `flutter doctor` - -### Flutter version - -While the app is in beta we'll use the `beta` channel of Flutter. - -#### Flutter Version Management - -If you want to use FVM to manage your Flutter versions effectively, please consult the [FVM (Flutter Version Management) guide](https://fvm.app/documentation/getting-started/installation) for comprehensive instructions on installing Flutter on your specific operating system. - -**Pro Tip:** Remember to prepend the 'flutter' prefix when using FVM commands, like this: `fvm flutter [command]`. - -### Local lila - -You will need a local [lila](https://github.com/lichess-org/lila) (lichess server scala app) instance to work on this -project. You will also need to setup [lila-ws](https://github.com/lichess-org/lila-ws) (websocket server). - -Instructions to install both are found in [the lila wiki](https://github.com/lichess-org/lila/wiki). - -The mobile application is configured by default to target `http://127.0.0.1:9663` and `ws://127.0.0.1:9664`, so keep these when installing lila. - -### Using Lichess dev server - -To use the [Lichess dev](https://lichess.dev/) to run this app, run the following command to set up the Lichess host URLs in the app. - -``` -flutter run --dart-define=LICHESS_HOST=lichess.dev --dart-define=LICHESS_WS_HOST=socket.lichess.dev -``` - -**Note : Do not use any scheme (https:// or ws://) in url in host, since it's already handled by URI helper methods** - - -## Run - -We don't commit generated code to the repository. So you need to run the code -generator first: - -```sh -flutter pub get -dart run build_runner watch -``` - -Check you have some connected device with: `flutter devices`. -If you target an android emulator you need to run these commands so the device can reach the local lila instance. - -**Note: Only run the command if you are using a local Lila server; otherwise, there's no need to set up port forwarding.** - -```sh -adb reverse tcp:9663 tcp:9663 -adb reverse tcp:9664 tcp:9664 -``` - -Then run on your device: - -```sh -flutter run -d -``` - -You can find more information about emulators [in the documentation](https://github.com/lichess-org/mobile/blob/main/docs/setting_dev_env.md). - -You can find more information about the `flutter run` command by running `flutter run --help`. - -### Test - -Before submitting a pull request, please run the tests: - -```sh -flutter analyze -flutter test -``` - -### Logging - -```sh -dart devtools -``` - -Then run the app with the `flutter run` command above, and a link to the logging page will be printed in the console. +See [the dev environment docs](./docs/setting_dev_env.md) for detailed instructions. ## Internationalisation diff --git a/docs/coding_style.md b/docs/coding_style.md index 87787a3817..24d0804fa3 100644 --- a/docs/coding_style.md +++ b/docs/coding_style.md @@ -44,6 +44,25 @@ return [ ]; ``` +## Platform-dependent code + +If one of your widget needs to display different things on iOS and Android, you can use the `PlatformWidget` helper: + +```dart +PlatformWidget( + androidBuilder: (context) => { + // return widget to display for Android + }, + iosBuilder: (context) => { + // return widget to display for iOS + }, +) +``` + +> [!TIP] +> The codebase already has some common platform-aware widgets that you can use, for example `PlatformScaffold`, +> `PlatformAppBar`, `PlatformListTile`, ... + ## Writing UI code Here is a list of practical tips when writing widgets. These are generally coding best practices in flutter, and serve to keep the project consistent. diff --git a/docs/setting_dev_env.md b/docs/setting_dev_env.md index 6f64881e95..70a5bcdf81 100644 --- a/docs/setting_dev_env.md +++ b/docs/setting_dev_env.md @@ -4,13 +4,56 @@ The following instructions outline how to set up your development environment fo If you get stuck during the installation process the most suitable place to seek help is the `#lichess-dev-onboarding` channel on Discord (https://discord.gg/lichess). -This project uses Flutter. The best way to get started is to follow [the flutter install guide](https://docs.flutter.dev/get-started/install). -Installing on Linux using `snapd` might cause some [problems](../../issues/123) building stockfish. Installing flutter manually is a known workaround. +## Installing Flutter -This project is meant to run on iOS and Android, so you need to follow the "Platform setup" section of that guide to install the iOS and/or Android platform. +This project uses Flutter. + +1. Follow [the flutter install guide](https://docs.flutter.dev/get-started/install). + This project is meant to run on iOS and Android, so you need to follow the "Platform setup" section of that guide to + install the iOS and/or Android platform. +> [!WARNING] +> Installing on Linux using `snapd` might cause some [problems](../../issues/123) building stockfish. +> Installing flutter manually is a known workaround. +2. Switch to the beta channel by running `flutter channel beta` and `flutter upgrade` +> [!NOTE] +> We'll use Flutter's `beta` channel while the app itself is in Beta. +3. Ensure Flutter is correctly configured by running `flutter doctor` Note that this application is not meant to be run on web platform. +### Flutter Version Management + +If you want to use FVM to manage your Flutter versions effectively, please consult the [FVM (Flutter Version Management) guide](https://fvm.app/documentation/getting-started/installation) for comprehensive instructions on installing Flutter on your specific operating system. + +**Pro Tip:** Remember to prepend the 'flutter' prefix when using FVM commands, like this: `fvm flutter [command]`. + +## Lila Server + +During development, you will need a local [lila](https://github.com/lichess-org/lila) (lichess server scala app) +instance to work on this project. You will also need to setup [lila-ws](https://github.com/lichess-org/lila-ws) +(websocket server). + +### lila-docker + +The fastest and most straight-forward way to get started is using [lila-docker](https://github.com/lichess-org/lila-docker). + +### Local lila server (manual installation) + +Instructions to install both `lila` and `lila-ws` locally can be found in [the lila wiki](https://github.com/lichess-org/lila/wiki/Lichess-Development-Onboarding). + +The mobile application is configured by default to target `http://127.0.0.1:9663` and `ws://127.0.0.1:9664`, so keep these when installing lila. + +### Using Lichess dev server + +To use the [Lichess dev](https://lichess.dev/) to run this app, run the following command to set up the Lichess host +URLs in the app. + +``` +flutter run --dart-define=LICHESS_HOST=lichess.dev --dart-define=LICHESS_WS_HOST=socket.lichess.dev +``` + +**Note : Do not use any scheme (https:// or ws://) in url in host, since it's already handled by URI helper methods** + ## Setting up the emulators ### iOS @@ -21,7 +64,10 @@ simulator. ### Android -If you are working with a local `lila` server, you need to expose some ports from the emulator. You can do this by running the following command (once the emulator is running): +#### When using a manually installed lila server + +If you are working with a local `lila` server, you need to expose some ports from the emulator. You can do this by +running the following command (once the emulator is running): ```bash adb reverse tcp:9663 tcp:9663 @@ -32,8 +78,10 @@ This will allow the app to communicate with the local server. 9663 is for `http` and 9664 is for `websocket`. It assumes that the server is running on the default ports. -When using [lila-docker](https://github.com/lichess-org/lila-docker), first run `./lila-docker hostname` and select your computer's IP address. -Then, instead of the commands above, use this: +#### When using lila-docker + +When using [lila-docker](https://github.com/lichess-org/lila-docker), first run `./lila-docker hostname` and select your +computer's IP address. Then, instead of the commands above, use this: ```bash adb reverse tcp:9663 tcp:8080 @@ -46,13 +94,14 @@ If Chrome instacrashes, it is likely you need to disable vulkan in emulator sett If you cannot access internet you can try launching the emulator with a DNS address: -``` +```bash $ emulator -avd -dns-server 1.1.1.1 ``` -If you experience high lags or freezes, check the memory settings and be sure to enable hardware acceleration. Also disabling the snapshot storage may help: +If you experience high lags or freezes, check the memory settings and be sure to enable hardware acceleration (`-gpu host`) +Also disabling the snapshot storage may help: -``` +```bash $ emulator -avd -no-snapshot-load -no-snapstorage -no-snapshot -no-snapshot-save' ``` @@ -60,15 +109,17 @@ $ emulator -avd -no-snapshot-load -no-snapstorage -no-snapshot -no-sna This project uses code generation to generate data classes with [freezed](https://pub.dev/packages/freezed) among other things. -You need to run it before anything else otherwise the project won't compile: +We don't commit generated code to the repository, so you need to run it before anything else otherwise the project won't +compile: -``` +```bash +flutter pub get dart run build_runner build ``` While developing you can use the watch command: -``` +```bash dart run build_runner watch ``` @@ -82,7 +133,6 @@ flutter analyze --watch It will run analysis continuously, watching the filesystem for changes. It is important to always check for analysis errors. - ## Run Use the `flutter run` command to run on an emulator or device. If you need to change the lichess host you can do it like so: @@ -91,3 +141,12 @@ Use the `flutter run` command to run on an emulator or device. If you need to ch flutter run --dart-define=LICHESS_HOST=lichess.dev --dart-define=LICHESS_WS_HOST=socket.lichess.dev ``` + +### Logging + +```sh +dart devtools +``` + +Then run the app with the `flutter run` command above, and a link to the logging page will be printed in the console. + From 60ecf7079724b4ac2dce46eb32169b85eb153893 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Mon, 19 Aug 2024 20:34:13 +0200 Subject: [PATCH 261/979] feat: offline games ("over the board") --- lib/src/model/game/over_the_board_game.dart | 47 +++ .../over_the_board/over_the_board_clock.dart | 141 ++++++++ .../over_the_board_game_controller.dart | 185 ++++++++++ .../settings/over_the_board_preferences.dart | 68 ++++ lib/src/view/game/game_result_dialog.dart | 57 +++ lib/src/view/home/create_game_options.dart | 137 ++++---- .../configure_over_the_board_game.dart | 234 +++++++++++++ .../over_the_board/over_the_board_screen.dart | 326 ++++++++++++++++++ lib/src/widgets/board_table.dart | 9 + .../over_the_board_screen_test.dart | 256 ++++++++++++++ 10 files changed, 1398 insertions(+), 62 deletions(-) create mode 100644 lib/src/model/game/over_the_board_game.dart create mode 100644 lib/src/model/over_the_board/over_the_board_clock.dart create mode 100644 lib/src/model/over_the_board/over_the_board_game_controller.dart create mode 100644 lib/src/model/settings/over_the_board_preferences.dart create mode 100644 lib/src/view/over_the_board/configure_over_the_board_game.dart create mode 100644 lib/src/view/over_the_board/over_the_board_screen.dart create mode 100644 test/view/over_the_board/over_the_board_screen_test.dart diff --git a/lib/src/model/game/over_the_board_game.dart b/lib/src/model/game/over_the_board_game.dart new file mode 100644 index 0000000000..daa88b95b0 --- /dev/null +++ b/lib/src/model/game/over_the_board_game.dart @@ -0,0 +1,47 @@ +import 'package:dartchess/dartchess.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:lichess_mobile/src/model/common/eval.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; + +import 'game.dart'; +import 'game_status.dart'; +import 'player.dart'; + +part 'over_the_board_game.freezed.dart'; +part 'over_the_board_game.g.dart'; + +/// An offline game played in real life by two human players on the same device. +/// +/// See [PlayableGame] for a game that is played online. +@Freezed(fromJson: true, toJson: true) +abstract class OverTheBoardGame + with _$OverTheBoardGame, BaseGame, IndexableSteps { + const OverTheBoardGame._(); + + @override + Player get white => const Player(); + @override + Player get black => const Player(); + + @override + IList? get evals => null; + @override + IList? get clocks => null; + + @override + GameId get id => const GameId('--------'); + + @Assert('steps.isNotEmpty') + factory OverTheBoardGame({ + @JsonKey(fromJson: stepsFromJson, toJson: stepsToJson) + required IList steps, + required GameMeta meta, + required String? initialFen, + required GameStatus status, + Side? winner, + bool? isThreefoldRepetition, + }) = _OverTheBoardGame; + + bool get finished => status.value >= GameStatus.mate.value; +} diff --git a/lib/src/model/over_the_board/over_the_board_clock.dart b/lib/src/model/over_the_board/over_the_board_clock.dart new file mode 100644 index 0000000000..2821527d75 --- /dev/null +++ b/lib/src/model/over_the_board/over_the_board_clock.dart @@ -0,0 +1,141 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:dartchess/dartchess.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:lichess_mobile/src/model/common/time_increment.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'over_the_board_clock.freezed.dart'; +part 'over_the_board_clock.g.dart'; + +@riverpod +class OverTheBoardClock extends _$OverTheBoardClock { + final Stopwatch _stopwatch = Stopwatch(); + + late Timer _updateTimer; + + @override + OverTheBoardClockState build() { + _updateTimer = Timer.periodic(const Duration(milliseconds: 100), (_) { + if (_stopwatch.isRunning) { + final newTime = + state.timeLeft(state.activeClock!)! - _stopwatch.elapsed; + + if (state.activeClock == Side.white) { + state = state.copyWith(whiteTimeLeft: newTime); + } else { + state = state.copyWith(blackTimeLeft: newTime); + } + + if (newTime <= Duration.zero) { + state = state.copyWith( + flagSide: state.activeClock, + ); + } + + _stopwatch.reset(); + } + }); + + ref.onDispose(() { + _updateTimer.cancel(); + }); + + return OverTheBoardClockState.fromTimeIncrement( + TimeIncrement( + const Duration(minutes: 5).inSeconds, + const Duration(seconds: 3).inSeconds, + ), + ); + } + + void setupClock(TimeIncrement timeIncrement) { + _stopwatch.stop(); + _stopwatch.reset(); + + state = OverTheBoardClockState.fromTimeIncrement(timeIncrement); + } + + void restart() { + setupClock(state.timeIncrement); + } + + void switchSide({required Side newSideToMove, required bool addIncrement}) { + if (state.timeIncrement.isInfinite || state.flagSide != null) return; + + final increment = + Duration(seconds: addIncrement ? state.timeIncrement.increment : 0); + if (newSideToMove == Side.black) { + state = state.copyWith( + whiteTimeLeft: state.whiteTimeLeft! + increment, + activeClock: Side.black, + ); + } else { + state = state.copyWith( + blackTimeLeft: state.blackTimeLeft! + increment, + activeClock: Side.white, + ); + } + + _stopwatch.reset(); + _stopwatch.start(); + } + + void onMove({required Side newSideToMove}) { + switchSide(newSideToMove: newSideToMove, addIncrement: state.active); + } + + void pause() { + if (_stopwatch.isRunning) { + state = state.copyWith( + activeClock: null, + ); + _stopwatch.reset(); + _stopwatch.stop(); + } + } + + void resume(Side newSideToMove) { + _stopwatch.reset(); + _stopwatch.start(); + + state = state.copyWith( + activeClock: newSideToMove, + ); + } +} + +@freezed +class OverTheBoardClockState with _$OverTheBoardClockState { + const OverTheBoardClockState._(); + + const factory OverTheBoardClockState({ + required TimeIncrement timeIncrement, + required Duration? whiteTimeLeft, + required Duration? blackTimeLeft, + required Side? activeClock, + required Side? flagSide, + }) = _OverTheBoardClockState; + + factory OverTheBoardClockState.fromTimeIncrement( + TimeIncrement timeIncrement, + ) { + final initialTime = timeIncrement.isInfinite + ? null + : Duration(seconds: max(timeIncrement.time, timeIncrement.increment)); + + return OverTheBoardClockState( + timeIncrement: timeIncrement, + whiteTimeLeft: initialTime, + blackTimeLeft: initialTime, + activeClock: null, + flagSide: null, + ); + } + + bool get active => activeClock != null || flagSide != null; + + Duration? timeLeft(Side side) => + side == Side.white ? whiteTimeLeft : blackTimeLeft; +} diff --git a/lib/src/model/over_the_board/over_the_board_game_controller.dart b/lib/src/model/over_the_board/over_the_board_game_controller.dart new file mode 100644 index 0000000000..5f333caa8f --- /dev/null +++ b/lib/src/model/over_the_board/over_the_board_game_controller.dart @@ -0,0 +1,185 @@ +import 'package:dartchess/dartchess.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/common/chess960.dart'; +import 'package:lichess_mobile/src/model/common/perf.dart'; +import 'package:lichess_mobile/src/model/common/service/move_feedback.dart'; +import 'package:lichess_mobile/src/model/common/speed.dart'; +import 'package:lichess_mobile/src/model/common/time_increment.dart'; +import 'package:lichess_mobile/src/model/game/game.dart'; +import 'package:lichess_mobile/src/model/game/game_status.dart'; +import 'package:lichess_mobile/src/model/game/material_diff.dart'; +import 'package:lichess_mobile/src/model/game/over_the_board_game.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'over_the_board_game_controller.freezed.dart'; +part 'over_the_board_game_controller.g.dart'; + +@riverpod +class OverTheBoardGameController extends _$OverTheBoardGameController { + @override + OverTheBoardGameState build() => OverTheBoardGameState.fromVariant( + Variant.standard, + Speed.fromTimeIncrement(const TimeIncrement(0, 0)), + ); + + void startNewGame(Variant variant, TimeIncrement timeIncrement) { + state = OverTheBoardGameState.fromVariant( + variant, + Speed.fromTimeIncrement(timeIncrement), + ); + } + + void rematch() { + state = OverTheBoardGameState.fromVariant( + state.game.meta.variant, + state.game.meta.speed, + ); + } + + void makeMove(NormalMove move) { + if (isPromotionPawnMove(state.currentPosition, move)) { + state = state.copyWith(promotionMove: move); + return; + } + + final (newPos, newSan) = + state.currentPosition.makeSan(Move.parse(move.uci)!); + final sanMove = SanMove(newSan, move); + final newStep = GameStep( + position: newPos, + sanMove: sanMove, + diff: MaterialDiff.fromBoard(newPos.board), + ); + + // In an over-the-board game, we support "implicit takebacks": + // When going back one or more steps (i.e. stepCursor < game.steps.length - 1), + // a new move can be made, removing all steps after the current stepCursor. + state = state.copyWith( + game: state.game.copyWith( + steps: state.game.steps + .removeRange(state.stepCursor + 1, state.game.steps.length) + .add(newStep), + ), + stepCursor: state.stepCursor + 1, + ); + + if (state.currentPosition.isCheckmate) { + state = state.copyWith( + game: state.game.copyWith( + status: GameStatus.mate, + winner: state.turn.opposite, + ), + ); + } else if (state.currentPosition.isStalemate) { + state = state.copyWith( + game: state.game.copyWith( + status: GameStatus.stalemate, + ), + ); + } + + _moveFeedback(sanMove); + } + + void onPromotionSelection(Role? role) { + if (role == null) { + state = state.copyWith(promotionMove: null); + return; + } + final promotionMove = state.promotionMove; + if (promotionMove != null) { + final move = promotionMove.withPromotion(role); + makeMove(move); + state = state.copyWith(promotionMove: null); + } + } + + void onFlag(Side side) { + state = state.copyWith( + game: state.game.copyWith( + status: GameStatus.outoftime, + winner: side.opposite, + ), + ); + } + + void goForward() { + if (state.canGoForward) { + state = state.copyWith(stepCursor: state.stepCursor + 1); + } + } + + void goBack() { + if (state.canGoBack) { + state = state.copyWith( + stepCursor: state.stepCursor - 1, + ); + } + } + + void _moveFeedback(SanMove sanMove) { + final isCheck = sanMove.san.contains('+'); + if (sanMove.san.contains('x')) { + ref.read(moveFeedbackServiceProvider).captureFeedback(check: isCheck); + } else { + ref.read(moveFeedbackServiceProvider).moveFeedback(check: isCheck); + } + } +} + +@freezed +class OverTheBoardGameState with _$OverTheBoardGameState { + const OverTheBoardGameState._(); + + const factory OverTheBoardGameState({ + required OverTheBoardGame game, + @Default(0) int stepCursor, + @Default(null) NormalMove? promotionMove, + }) = _OverTheBoardGameState; + + factory OverTheBoardGameState.fromVariant( + Variant variant, + Speed speed, + ) { + final position = variant == Variant.chess960 + ? randomChess960Position() + : variant.initialPosition; + return OverTheBoardGameState( + game: OverTheBoardGame( + steps: [ + GameStep( + position: position, + ), + ].lock, + status: GameStatus.started, + initialFen: position.fen, + meta: GameMeta( + createdAt: DateTime.now(), + rated: false, + variant: variant, + speed: speed, + perf: Perf.fromVariantAndSpeed(variant, speed), + ), + ), + ); + } + + Position get currentPosition => game.stepAt(stepCursor).position; + Side get turn => currentPosition.turn; + bool get finished => game.finished; + NormalMove? get lastMove => stepCursor > 0 + ? NormalMove.fromUci(game.steps[stepCursor].sanMove!.move.uci) + : null; + + MaterialDiffSide? currentMaterialDiff(Side side) { + return game.steps[stepCursor].diff?.bySide(side); + } + + List get moves => + game.steps.skip(1).map((e) => e.sanMove!.san).toList(growable: false); + + bool get canGoForward => stepCursor < game.steps.length - 1; + bool get canGoBack => stepCursor > 0; +} diff --git a/lib/src/model/settings/over_the_board_preferences.dart b/lib/src/model/settings/over_the_board_preferences.dart new file mode 100644 index 0000000000..9eb24c0ed7 --- /dev/null +++ b/lib/src/model/settings/over_the_board_preferences.dart @@ -0,0 +1,68 @@ +import 'dart:convert'; + +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:lichess_mobile/src/db/shared_preferences.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'over_the_board_preferences.freezed.dart'; +part 'over_the_board_preferences.g.dart'; + +@Riverpod(keepAlive: true) +class OverTheBoardPreferences extends _$OverTheBoardPreferences { + static const String prefKey = 'preferences.board'; + + @override + OverTheBoardPrefs build() { + final prefs = ref.watch(sharedPreferencesProvider); + final stored = prefs.getString(prefKey); + return stored != null + ? OverTheBoardPrefs.fromJson( + jsonDecode(stored) as Map, + ) + : OverTheBoardPrefs.defaults; + } + + Future toggleFlipPiecesAfterMove() { + return _save( + state.copyWith(flipPiecesAfterMove: !state.flipPiecesAfterMove), + ); + } + + Future toggleSymmetricPieces() { + return _save( + state.copyWith(symmetricPieces: !state.symmetricPieces), + ); + } + + Future _save(OverTheBoardPrefs newState) async { + final prefs = ref.read(sharedPreferencesProvider); + await prefs.setString( + prefKey, + jsonEncode(newState.toJson()), + ); + state = newState; + } +} + +@Freezed(fromJson: true, toJson: true) +class OverTheBoardPrefs with _$OverTheBoardPrefs { + const OverTheBoardPrefs._(); + + const factory OverTheBoardPrefs({ + required bool flipPiecesAfterMove, + required bool symmetricPieces, + }) = _OverTheBoardPrefs; + + static const defaults = OverTheBoardPrefs( + flipPiecesAfterMove: false, + symmetricPieces: false, + ); + + factory OverTheBoardPrefs.fromJson(Map json) { + try { + return _$OverTheBoardPrefsFromJson(json); + } catch (_) { + return defaults; + } + } +} diff --git a/lib/src/view/game/game_result_dialog.dart b/lib/src/view/game/game_result_dialog.dart index 4c3dd5da43..9b1f6b462f 100644 --- a/lib/src/view/game/game_result_dialog.dart +++ b/lib/src/view/game/game_result_dialog.dart @@ -9,6 +9,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/constants.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/analysis/server_analysis_service.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/common/eval.dart'; @@ -16,6 +17,7 @@ import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/game/game.dart'; import 'package:lichess_mobile/src/model/game/game_controller.dart'; import 'package:lichess_mobile/src/model/game/game_status.dart'; +import 'package:lichess_mobile/src/model/game/over_the_board_game.dart'; import 'package:lichess_mobile/src/model/game/playable_game.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; @@ -309,6 +311,61 @@ class ArchivedGameResultDialog extends StatelessWidget { } } +class OverTheBoardGameResultDialog extends StatelessWidget { + const OverTheBoardGameResultDialog({ + super.key, + required this.game, + required this.onRematch, + }); + + final OverTheBoardGame game; + + final void Function() onRematch; + + @override + Widget build(BuildContext context) { + final content = Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + GameResult(game: game), + SecondaryButton( + semanticsLabel: context.l10n.rematch, + onPressed: onRematch, + child: Text( + context.l10n.rematch, + textAlign: TextAlign.center, + ), + ), + SecondaryButton( + semanticsLabel: context.l10n.analysis, + onPressed: () { + pushPlatformRoute( + context, + builder: (_) => AnalysisScreen( + pgnOrId: game.makePgn(), + options: AnalysisOptions( + isLocalEvaluationAllowed: true, + variant: game.meta.variant, + orientation: Side.white, + id: standaloneAnalysisId, + ), + title: context.l10n.gameAnalysis, + ), + ); + }, + child: Text( + context.l10n.analysis, + textAlign: TextAlign.center, + ), + ), + ], + ); + + return _adaptiveDialog(context, content); + } +} + class PlayerSummary extends ConsumerWidget { const PlayerSummary({required this.game, super.key}); diff --git a/lib/src/view/home/create_game_options.dart b/lib/src/view/home/create_game_options.dart index 653da12cd5..d77cb785a3 100644 --- a/lib/src/view/home/create_game_options.dart +++ b/lib/src/view/home/create_game_options.dart @@ -2,9 +2,11 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; +import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/view/over_the_board/over_the_board_screen.dart'; import 'package:lichess_mobile/src/view/play/create_custom_game_screen.dart'; import 'package:lichess_mobile/src/view/play/online_bots_screen.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; @@ -14,77 +16,88 @@ class CreateGameOptions extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final buttons = [ + _CreateGamePlatformButton( + onTap: () { + ref.invalidate(accountProvider); + pushPlatformRoute( + context, + title: context.l10n.custom, + builder: (_) => const CreateCustomGameScreen(), + ); + }, + icon: Icons.tune, + label: context.l10n.custom, + ), + _CreateGamePlatformButton( + onTap: () { + pushPlatformRoute( + context, + title: context.l10n.onlineBots, + builder: (_) => const OnlineBotsScreen(), + ); + }, + icon: Icons.computer, + label: context.l10n.onlineBots, + ), + _CreateGamePlatformButton( + onTap: () { + pushPlatformRoute( + context, + title: 'Over the Board', + rootNavigator: true, + builder: (_) => const OverTheBoardScreen(), + ); + }, + icon: LichessIcons.chess_board, + label: 'Over the board', + ), + ]; + return Theme.of(context).platform == TargetPlatform.iOS ? ListSection( hasLeading: true, - children: [ - PlatformListTile( - leading: const Icon( - Icons.tune, - size: 28, - ), - trailing: const CupertinoListTileChevron(), - title: - Text(context.l10n.custom, style: Styles.mainListTileTitle), - onTap: () { - ref.invalidate(accountProvider); - pushPlatformRoute( - context, - title: context.l10n.custom, - builder: (_) => const CreateCustomGameScreen(), - ); - }, - ), - PlatformListTile( - title: Text( - context.l10n.onlineBots, - style: Styles.mainListTileTitle, - ), - leading: const Icon( - Icons.computer, - size: 28, - ), - trailing: const CupertinoListTileChevron(), - onTap: () { - pushPlatformRoute( - context, - title: context.l10n.onlineBots, - builder: (_) => const OnlineBotsScreen(), - ); - }, - ), - ], + children: buttons, ) : Padding( padding: Styles.bodySectionPadding, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - ElevatedButton.icon( - onPressed: () { - ref.invalidate(accountProvider); - pushPlatformRoute( - context, - title: context.l10n.custom, - builder: (_) => const CreateCustomGameScreen(), - ); - }, - icon: const Icon(Icons.tune), - label: Text(context.l10n.custom), - ), - ElevatedButton.icon( - onPressed: () { - pushPlatformRoute( - context, - title: context.l10n.onlineBots, - builder: (_) => const OnlineBotsScreen(), - ); - }, - icon: const Icon(Icons.computer), - label: Text(context.l10n.onlineBots), - ), - ], + children: buttons, + ), + ); + } +} + +class _CreateGamePlatformButton extends StatelessWidget { + const _CreateGamePlatformButton({ + required this.icon, + required this.label, + required this.onTap, + }); + + final IconData icon; + + final String label; + + final void Function() onTap; + + @override + Widget build(BuildContext context) { + return Theme.of(context).platform == TargetPlatform.iOS + ? PlatformListTile( + leading: Icon( + icon, + size: 28, ), + trailing: const CupertinoListTileChevron(), + title: Text(label, style: Styles.mainListTileTitle), + onTap: onTap, + ) + : ElevatedButton.icon( + onPressed: onTap, + icon: Icon(icon), + label: Text(label), ); } } diff --git a/lib/src/view/over_the_board/configure_over_the_board_game.dart b/lib/src/view/over_the_board/configure_over_the_board_game.dart new file mode 100644 index 0000000000..2ec3d0bc3b --- /dev/null +++ b/lib/src/view/over_the_board/configure_over_the_board_game.dart @@ -0,0 +1,234 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/common/time_increment.dart'; +import 'package:lichess_mobile/src/model/lobby/game_setup.dart'; +import 'package:lichess_mobile/src/model/over_the_board/over_the_board_clock.dart'; +import 'package:lichess_mobile/src/model/over_the_board/over_the_board_game_controller.dart'; +import 'package:lichess_mobile/src/model/settings/over_the_board_preferences.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_choice_picker.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/list.dart'; +import 'package:lichess_mobile/src/widgets/non_linear_slider.dart'; +import 'package:lichess_mobile/src/widgets/settings.dart'; + +void showConfigureGameSheet( + BuildContext context, { + required bool isDismissible, +}) { + final double screenHeight = MediaQuery.sizeOf(context).height; + showAdaptiveBottomSheet( + context: context, + isScrollControlled: true, + showDragHandle: true, + isDismissible: isDismissible, + constraints: BoxConstraints( + maxHeight: screenHeight - (screenHeight / 10), + ), + builder: (BuildContext context) { + return const _ConfigureOverTheBoardGameSheet(); + }, + ); +} + +class _ConfigureOverTheBoardGameSheet extends ConsumerStatefulWidget { + const _ConfigureOverTheBoardGameSheet(); + + @override + ConsumerState<_ConfigureOverTheBoardGameSheet> createState() => + _ConfigureOverTheBoardGameSheetState(); +} + +class _ConfigureOverTheBoardGameSheetState + extends ConsumerState<_ConfigureOverTheBoardGameSheet> { + late Variant chosenVariant; + + late TimeIncrement timeIncrement; + + @override + void initState() { + final gameState = ref.read(overTheBoardGameControllerProvider); + chosenVariant = gameState.game.meta.variant; + final clockProvider = ref.read(overTheBoardClockProvider); + timeIncrement = clockProvider.timeIncrement; + super.initState(); + } + + void _setTotalTime(num seconds) { + setState(() { + timeIncrement = TimeIncrement( + seconds.toInt(), + timeIncrement.increment, + ); + }); + } + + void _setIncrement(num seconds) { + setState(() { + timeIncrement = TimeIncrement( + timeIncrement.time, + seconds.toInt(), + ); + }); + } + + @override + Widget build(BuildContext context) { + return SafeArea( + child: Padding( + padding: Styles.bodyPadding, + child: ListView( + shrinkWrap: true, + children: [ + ListSection( + header: const SettingsSectionTitle('Start new game'), + hasLeading: false, + children: [ + SettingsListTile( + settingsLabel: Text(context.l10n.variant), + settingsValue: chosenVariant.label, + onTap: () { + showChoicePicker( + context, + choices: Variant.values + .where( + (variant) => variant != Variant.fromPosition, + ) + .toList(), + selectedItem: chosenVariant, + labelBuilder: (Variant variant) => Text(variant.label), + onSelectedItemChanged: (Variant variant) => setState(() { + chosenVariant = variant; + }), + ); + }, + ), + PlatformListTile( + title: Text.rich( + TextSpan( + text: '${context.l10n.minutesPerSide}: ', + children: [ + TextSpan( + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + text: clockLabelInMinutes(timeIncrement.time), + ), + ], + ), + ), + subtitle: NonLinearSlider( + value: timeIncrement.time, + values: kAvailableTimesInSeconds, + labelBuilder: clockLabelInMinutes, + onChange: Theme.of(context).platform == TargetPlatform.iOS + ? _setTotalTime + : null, + onChangeEnd: _setTotalTime, + ), + ), + PlatformListTile( + title: Text.rich( + TextSpan( + text: '${context.l10n.incrementInSeconds}: ', + children: [ + TextSpan( + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + text: timeIncrement.increment.toString(), + ), + ], + ), + ), + subtitle: NonLinearSlider( + value: timeIncrement.increment, + values: kAvailableIncrementsInSeconds, + onChange: Theme.of(context).platform == TargetPlatform.iOS + ? _setIncrement + : null, + onChangeEnd: _setIncrement, + ), + ), + ], + ), + SecondaryButton( + onPressed: () { + ref + .read(overTheBoardClockProvider.notifier) + .setupClock(timeIncrement); + ref + .read(overTheBoardGameControllerProvider.notifier) + .startNewGame( + chosenVariant, + timeIncrement, + ); + Navigator.pop(context); + }, + semanticsLabel: context.l10n.play, + child: Text( + context.l10n.play, + style: Styles.bold, + ), + ), + ], + ), + ), + ); + } +} + +void showConfigureDisplaySettings(BuildContext context) { + showAdaptiveBottomSheet( + context: context, + isDismissible: true, + showDragHandle: true, + builder: (BuildContext context) { + return const OverTheBoardDisplaySettings(); + }, + ); +} + +class OverTheBoardDisplaySettings extends ConsumerWidget { + const OverTheBoardDisplaySettings(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final prefs = ref.watch(overTheBoardPreferencesProvider); + + return DraggableScrollableSheet( + initialChildSize: 1.0, + expand: false, + builder: (context, scrollController) => ListView( + controller: scrollController, + children: [ + ListSection( + header: SettingsSectionTitle(context.l10n.settingsSettings), + hasLeading: false, + children: [ + SwitchSettingTile( + title: const Text('Use symmetric pieces'), + value: prefs.symmetricPieces, + onChanged: (_) => ref + .read(overTheBoardPreferencesProvider.notifier) + .toggleSymmetricPieces(), + ), + SwitchSettingTile( + title: const Text('Flip pieces and oponent info after move'), + value: prefs.flipPiecesAfterMove, + onChanged: (_) => ref + .read(overTheBoardPreferencesProvider.notifier) + .toggleFlipPiecesAfterMove(), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/src/view/over_the_board/over_the_board_screen.dart b/lib/src/view/over_the_board/over_the_board_screen.dart new file mode 100644 index 0000000000..2a87bb9cf6 --- /dev/null +++ b/lib/src/view/over_the_board/over_the_board_screen.dart @@ -0,0 +1,326 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:chessground/chessground.dart'; +import 'package:dartchess/dartchess.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/game/player.dart'; +import 'package:lichess_mobile/src/model/over_the_board/over_the_board_clock.dart'; +import 'package:lichess_mobile/src/model/over_the_board/over_the_board_game_controller.dart'; +import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; +import 'package:lichess_mobile/src/model/settings/brightness.dart'; +import 'package:lichess_mobile/src/model/settings/over_the_board_preferences.dart'; +import 'package:lichess_mobile/src/model/user/user.dart'; +import 'package:lichess_mobile/src/utils/immersive_mode.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/string.dart'; +import 'package:lichess_mobile/src/view/game/game_player.dart'; +import 'package:lichess_mobile/src/view/game/game_result_dialog.dart'; +import 'package:lichess_mobile/src/view/over_the_board/configure_over_the_board_game.dart'; +import 'package:lichess_mobile/src/widgets/board_table.dart'; +import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; +import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/countdown_clock.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; + +class OverTheBoardScreen extends StatelessWidget { + const OverTheBoardScreen({super.key}); + + @override + Widget build(BuildContext context) { + return PlatformScaffold( + appBar: PlatformAppBar( + title: const Text('Over the board'), // TODO: l10n + actions: [ + AppBarIconButton( + onPressed: () => showConfigureDisplaySettings(context), + semanticsLabel: context.l10n.settingsSettings, + icon: const Icon(Icons.settings), + ), + ], + ), + body: const _Body(), + ); + } +} + +class _Body extends ConsumerStatefulWidget { + const _Body(); + + @override + ConsumerState<_Body> createState() => _BodyState(); +} + +class _BodyState extends ConsumerState<_Body> { + final _boardKey = GlobalKey(debugLabel: 'boardOnOverTheBoardScreen'); + + Side orientation = Side.white; + + @override + void initState() { + SchedulerBinding.instance.addPostFrameCallback((_) { + showConfigureGameSheet(context, isDismissible: false); + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + final gameState = ref.watch(overTheBoardGameControllerProvider); + final boardPreferences = ref.watch(boardPreferencesProvider); + + final overTheBoardPrefs = ref.watch(overTheBoardPreferencesProvider); + + ref.listen(overTheBoardClockProvider.select((value) => value.flagSide), + (previous, flagSide) { + if (previous == null && flagSide != null) { + ref.read(overTheBoardGameControllerProvider.notifier).onFlag(flagSide); + } + }); + + ref.listen(overTheBoardGameControllerProvider, (previous, newGameState) { + if (previous?.finished == false && newGameState.finished) { + ref.read(overTheBoardClockProvider.notifier).pause(); + Timer(const Duration(milliseconds: 500), () { + if (context.mounted) { + showAdaptiveDialog( + context: context, + builder: (context) => OverTheBoardGameResultDialog( + game: newGameState.game, + onRematch: () { + setState(() { + orientation = orientation.opposite; + ref + .read(overTheBoardGameControllerProvider.notifier) + .rematch(); + ref.read(overTheBoardClockProvider.notifier).restart(); + Navigator.pop(context); + }); + }, + ), + barrierDismissible: true, + ); + } + }); + } + }); + + return WakelockWidget( + child: PopScope( + child: Column( + children: [ + Expanded( + child: SafeArea( + bottom: false, + child: BoardTable( + key: _boardKey, + topTable: _Player( + side: orientation.opposite, + upsideDown: !overTheBoardPrefs.flipPiecesAfterMove || + orientation != gameState.turn, + clockKey: const ValueKey('topClock'), + ), + bottomTable: _Player( + side: orientation, + upsideDown: overTheBoardPrefs.flipPiecesAfterMove && + orientation != gameState.turn, + clockKey: const ValueKey('bottomClock'), + ), + orientation: orientation, + fen: gameState.currentPosition.fen, + lastMove: gameState.lastMove, + gameData: GameData( + isCheck: boardPreferences.boardHighlights && + gameState.currentPosition.isCheck, + playerSide: gameState.game.finished + ? PlayerSide.none + : gameState.turn == Side.white + ? PlayerSide.white + : PlayerSide.black, + sideToMove: gameState.turn, + validMoves: makeLegalMoves(gameState.currentPosition), + onPromotionSelection: ref + .read(overTheBoardGameControllerProvider.notifier) + .onPromotionSelection, + promotionMove: gameState.promotionMove, + onMove: (move, {isDrop}) { + ref.read(overTheBoardClockProvider.notifier).onMove( + newSideToMove: gameState.turn.opposite, + ); + ref + .read(overTheBoardGameControllerProvider.notifier) + .makeMove(move); + }, + ), + moves: gameState.moves, + currentMoveIndex: gameState.stepCursor, + boardSettingsOverrides: BoardSettingsOverrides( + drawShape: const DrawShapeOptions(enable: false), + pieceOrientationBehavior: + overTheBoardPrefs.flipPiecesAfterMove + ? PieceOrientationBehavior.sideToPlay + : PieceOrientationBehavior.opponentUpsideDown, + pieceAssets: overTheBoardPrefs.symmetricPieces + ? PieceSet.symmetric.assets + : null, + ), + ), + ), + ), + _BottomBar( + onFlipBoard: () { + setState(() { + orientation = orientation.opposite; + }); + }, + ), + ], + ), + ), + ); + } +} + +class _BottomBar extends ConsumerWidget { + const _BottomBar({ + required this.onFlipBoard, + }); + + final VoidCallback onFlipBoard; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final gameState = ref.watch(overTheBoardGameControllerProvider); + + final clock = ref.watch(overTheBoardClockProvider); + + return BottomBar( + children: [ + BottomBarButton( + label: 'Configure game', + onTap: () => showConfigureGameSheet(context, isDismissible: true), + icon: Icons.add_circle, + ), + BottomBarButton( + key: const Key('flip-button'), + label: context.l10n.flipBoard, + onTap: onFlipBoard, + icon: CupertinoIcons.arrow_2_squarepath, + ), + if (!clock.timeIncrement.isInfinite) + BottomBarButton( + label: clock.active ? 'Pause' : 'Resume', + onTap: gameState.finished + ? null + : () { + if (clock.active) { + ref.read(overTheBoardClockProvider.notifier).pause(); + } else { + ref + .read(overTheBoardClockProvider.notifier) + .resume(gameState.turn); + } + }, + icon: clock.active ? CupertinoIcons.pause : CupertinoIcons.play, + ), + BottomBarButton( + label: 'Previous', + onTap: gameState.canGoBack + ? () { + ref + .read(overTheBoardGameControllerProvider.notifier) + .goBack(); + if (clock.active) { + ref.read(overTheBoardClockProvider.notifier).switchSide( + newSideToMove: gameState.turn.opposite, + addIncrement: false, + ); + } + } + : null, + icon: CupertinoIcons.chevron_back, + ), + BottomBarButton( + label: 'Next', + onTap: gameState.canGoForward + ? () { + ref + .read(overTheBoardGameControllerProvider.notifier) + .goForward(); + if (clock.active) { + ref.read(overTheBoardClockProvider.notifier).switchSide( + newSideToMove: gameState.turn.opposite, + addIncrement: false, + ); + } + } + : null, + icon: CupertinoIcons.chevron_forward, + ), + ], + ); + } +} + +class _Player extends ConsumerWidget { + const _Player({ + required this.clockKey, + required this.side, + required this.upsideDown, + }); + + final Side side; + + final Key clockKey; + + final bool upsideDown; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final gameState = ref.watch(overTheBoardGameControllerProvider); + final boardPreferences = ref.watch(boardPreferencesProvider); + final clock = ref.watch(overTheBoardClockProvider); + + final brightness = ref.watch(currentBrightnessProvider); + final clockStyle = brightness == Brightness.dark + ? ClockStyle.darkThemeStyle + : ClockStyle.lightThemeStyle; + + return RotatedBox( + quarterTurns: upsideDown ? 2 : 0, + child: GamePlayer( + player: Player( + onGame: true, + user: LightUser( + id: UserId(side.name), + name: side.name.capitalize(), + ), + ), + materialDiff: boardPreferences.showMaterialDifference + ? gameState.currentMaterialDiff(side) + : null, + shouldLinkToUserProfile: false, + clock: clock.timeIncrement.isInfinite + ? null + : Clock( + timeLeft: Duration( + milliseconds: max(0, clock.timeLeft(side)!.inMilliseconds), + ), + key: clockKey, + active: clock.activeClock == side, + clockStyle: clockStyle, + // https://github.com/lichess-org/mobile/issues/785#issuecomment-2183903498 + emergencyThreshold: Duration( + seconds: + (clock.timeIncrement.time * 0.125).clamp(10, 60).toInt(), + ), + ), + ), + ); + } +} diff --git a/lib/src/widgets/board_table.dart b/lib/src/widgets/board_table.dart index 6145df6a57..691afba4bd 100644 --- a/lib/src/widgets/board_table.dart +++ b/lib/src/widgets/board_table.dart @@ -359,12 +359,18 @@ class BoardSettingsOverrides { this.autoQueenPromotion, this.autoQueenPromotionOnPremove, this.blindfoldMode, + this.drawShape, + this.pieceOrientationBehavior, + this.pieceAssets, }); final Duration? animationDuration; final bool? autoQueenPromotion; final bool? autoQueenPromotionOnPremove; final bool? blindfoldMode; + final DrawShapeOptions? drawShape; + final PieceOrientationBehavior? pieceOrientationBehavior; + final PieceAssets? pieceAssets; ChessboardSettings merge(ChessboardSettings settings) { return settings.copyWith( @@ -372,6 +378,9 @@ class BoardSettingsOverrides { autoQueenPromotion: autoQueenPromotion, autoQueenPromotionOnPremove: autoQueenPromotionOnPremove, blindfoldMode: blindfoldMode, + drawShape: drawShape, + pieceOrientationBehavior: pieceOrientationBehavior, + pieceAssets: pieceAssets, ); } } diff --git a/test/view/over_the_board/over_the_board_screen_test.dart b/test/view/over_the_board/over_the_board_screen_test.dart new file mode 100644 index 0000000000..02552d2743 --- /dev/null +++ b/test/view/over_the_board/over_the_board_screen_test.dart @@ -0,0 +1,256 @@ +import 'dart:io'; + +import 'package:chessground/chessground.dart'; +import 'package:dartchess/dartchess.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:lichess_mobile/src/model/common/time_increment.dart'; +import 'package:lichess_mobile/src/model/over_the_board/over_the_board_clock.dart'; +import 'package:lichess_mobile/src/model/over_the_board/over_the_board_game_controller.dart'; +import 'package:lichess_mobile/src/view/over_the_board/over_the_board_screen.dart'; +import 'package:lichess_mobile/src/widgets/countdown_clock.dart'; + +import '../../test_app.dart'; +import '../../test_utils.dart'; + +void main() { + group('Playing over the board (offline)', () { + testWidgets('Checkmate and Rematch', (tester) async { + final boardRect = await initOverTheBoardGame( + tester, + const TimeIncrement(60, 5), + ); + + // Default orientation is white at the bottom + expect( + tester.getBottomLeft(find.byKey(const ValueKey('a1-whiterook'))), + boardRect.bottomLeft, + ); + + await playMove(tester, boardRect, 'e2', 'e4'); + await playMove(tester, boardRect, 'f7', 'f6'); + await playMove(tester, boardRect, 'd2', 'd4'); + await playMove(tester, boardRect, 'g7', 'g5'); + await playMove(tester, boardRect, 'd1', 'h5'); + + await tester.pumpAndSettle(const Duration(milliseconds: 600)); + expect(find.text('Checkmate • White is victorious'), findsOneWidget); + + await tester.tap(find.text('Rematch')); + await tester.pumpAndSettle(); + + final container = ProviderScope.containerOf( + tester.element(find.byType(Chessboard)), + ); + final gameState = container.read(overTheBoardGameControllerProvider); + expect(gameState.game.steps.length, 1); + expect(gameState.game.steps.first.position, Chess.initial); + + // Rematch should flip orientation + expect( + tester.getTopRight(find.byKey(const ValueKey('a1-whiterook'))), + boardRect.topRight, + ); + + expect(activeClock(tester), null); + }); + + testWidgets('Game ends when out of time', (tester) async { + const time = Duration(seconds: 1); + final boardRect = await initOverTheBoardGame( + tester, + TimeIncrement(time.inSeconds, 0), + ); + + await playMove(tester, boardRect, 'e2', 'e4'); + await playMove(tester, boardRect, 'e7', 'e5'); + + // The clock measures system time internally, so we need to actually sleep in order + // for the clock to reach 0, instead of using tester.pump() + sleep(time + const Duration(milliseconds: 100)); + + // Now for game result dialog to show up + await tester.pumpAndSettle(const Duration(milliseconds: 600)); + expect(find.text('White time out • Black is victorious'), findsOneWidget); + + await tester.tap(find.text('Rematch')); + expect(activeClock(tester), null); + }); + + testWidgets('Pausing the clock', (tester) async { + const time = Duration(seconds: 10); + + final boardRect = await initOverTheBoardGame( + tester, + TimeIncrement(time.inSeconds, 0), + ); + + await playMove(tester, boardRect, 'e2', 'e4'); + await playMove(tester, boardRect, 'e7', 'e5'); + + await tester.tap(find.byTooltip('Pause')); + await tester.pump(); + + expect(activeClock(tester), null); + + await tester.tap(find.byTooltip('Resume')); + await tester.pump(); + + expect(activeClock(tester), Side.white); + + // Going back a move should not unpause... + await tester.tap(find.byTooltip('Pause')); + await tester.pump(); + await tester.tap(find.byTooltip('Previous')); + await tester.pump(); + + expect(activeClock(tester), null); + + // ... but playing a move resumes the clock + await playMove(tester, boardRect, 'd7', 'd5'); + + expect(activeClock(tester), Side.white); + }); + + testWidgets('Go back and Forward', (tester) async { + const time = Duration(seconds: 10); + + final boardRect = await initOverTheBoardGame( + tester, + TimeIncrement(time.inSeconds, 0), + ); + + await playMove(tester, boardRect, 'e2', 'e4'); + await playMove(tester, boardRect, 'e7', 'e5'); + + await tester.tap(find.byTooltip('Previous')); + await tester.pumpAndSettle(); + expect(find.byKey(const ValueKey('e7-blackpawn')), findsOneWidget); + + expect(activeClock(tester), Side.black); + + await tester.tap(find.byTooltip('Next')); + await tester.pumpAndSettle(); + expect(find.byKey(const ValueKey('e5-blackpawn')), findsOneWidget); + + expect(activeClock(tester), Side.white); + + // Go back all the way to the initial position + await tester.tap(find.byTooltip('Previous')); + await tester.pumpAndSettle(); + await tester.tap(find.byTooltip('Previous')); + await tester.pumpAndSettle(); + await tester.tap(find.byTooltip('Previous')); + await tester.pumpAndSettle(); + expect(find.byKey(const ValueKey('e2-whitepawn')), findsOneWidget); + expect(find.byKey(const ValueKey('e7-blackpawn')), findsOneWidget); + + expect(activeClock(tester), Side.white); + + await playMove(tester, boardRect, 'e2', 'e4'); + expect(find.byKey(const ValueKey('e4-whitepawn')), findsOneWidget); + + expect(activeClock(tester), Side.black); + }); + + testWidgets('No clock if time is infinite', (tester) async { + await initOverTheBoardGame( + tester, + const TimeIncrement(0, 0), + ); + + expect(find.byType(Clock), findsNothing); + }); + + testWidgets('Clock logic', (tester) async { + const time = Duration(minutes: 5); + + final boardRect = await initOverTheBoardGame( + tester, + TimeIncrement(time.inSeconds, 3), + ); + + expect(activeClock(tester), null); + + expect(findWhiteClock(tester).timeLeft, time); + expect(findBlackClock(tester).timeLeft, time); + + await playMove(tester, boardRect, 'e2', 'e4'); + + const moveTime = Duration(milliseconds: 500); + await tester.pumpAndSettle(moveTime); + + expect(activeClock(tester), Side.black); + + expect(findWhiteClock(tester).timeLeft, time); + expect(findBlackClock(tester).timeLeft, lessThan(time)); + + await playMove(tester, boardRect, 'e7', 'e5'); + await tester.pumpAndSettle(); + + expect(activeClock(tester), Side.white); + + // Expect increment to be added + expect(findBlackClock(tester).timeLeft, greaterThan(time)); + + expect(findWhiteClock(tester).timeLeft, lessThan(time)); + }); + }); +} + +Future initOverTheBoardGame( + WidgetTester tester, + TimeIncrement timeIncrement, +) async { + final app = await buildTestApp( + tester, + home: const OverTheBoardScreen(), + ); + await tester.pumpWidget(app); + + await tester.pumpAndSettle(); + await tester.tap(find.text('Play')); + await tester.pumpAndSettle(); + + final container = ProviderScope.containerOf( + tester.element(find.byType(Chessboard)), + ); + container.read(overTheBoardClockProvider.notifier).setupClock(timeIncrement); + await tester.pumpAndSettle(); + + return tester.getRect(find.byType(Chessboard)); +} + +Side? activeClock(WidgetTester tester, {Side orientation = Side.white}) { + final whiteClock = findWhiteClock(tester, orientation: orientation); + final blackClock = findBlackClock(tester, orientation: orientation); + + if (whiteClock.active) { + expect(blackClock.active, false); + return Side.white; + } + + if (blackClock.active) { + expect(whiteClock.active, false); + return Side.black; + } + + return null; +} + +Clock findWhiteClock(WidgetTester tester, {Side orientation = Side.white}) { + return tester.widget( + find.byKey( + ValueKey(orientation == Side.white ? 'bottomClock' : 'topClock'), + ), + ); +} + +Clock findBlackClock(WidgetTester tester, {Side orientation = Side.white}) { + return tester.widget( + find.byKey( + ValueKey(orientation == Side.white ? 'topClock' : 'bottomClock'), + ), + ); +} From a8ac4d1d148d4ebbdf959ac0b395f9b17f044841 Mon Sep 17 00:00:00 2001 From: inc0g_ Date: Thu, 5 Sep 2024 09:08:15 +1000 Subject: [PATCH 262/979] Challenge: remove assert to allow for unlimited challenges --- lib/src/model/challenge/challenge.dart | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/src/model/challenge/challenge.dart b/lib/src/model/challenge/challenge.dart index 0e7cc6babc..94c97543d2 100644 --- a/lib/src/model/challenge/challenge.dart +++ b/lib/src/model/challenge/challenge.dart @@ -40,10 +40,6 @@ abstract mixin class BaseChallenge { class Challenge with _$Challenge, BaseChallenge implements BaseChallenge { const Challenge._(); - @Assert( - 'clock != null || days != null', - 'Either clock or days must be set but not both.', - ) const factory Challenge({ int? socketVersion, required ChallengeId id, From c832cd3b82bde2cd395ef866bee8554ab697057e Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Fri, 30 Aug 2024 23:24:35 +0200 Subject: [PATCH 263/979] use badge widget for icons --- lib/src/view/game/game_body.dart | 9 ++- lib/src/view/user/game_history_screen.dart | 64 +++++---------- lib/src/widgets/bottom_bar_button.dart | 93 +++++++++------------- 3 files changed, 60 insertions(+), 106 deletions(-) diff --git a/lib/src/view/game/game_body.dart b/lib/src/view/game/game_body.dart index 7499082212..01097316e4 100644 --- a/lib/src/view/game/game_body.dart +++ b/lib/src/view/game/game_body.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:math' as math; import 'package:chessground/chessground.dart'; import 'package:collection/collection.dart'; @@ -444,10 +443,12 @@ class _GameBottomBar extends ConsumerWidget { final isChatEnabled = chatStateAsync != null && !gameState.isZenModeActive; - final chatUnreadChip = isChatEnabled + final chatUnreadLabel = isChatEnabled ? chatStateAsync.maybeWhen( data: (s) => s.unreadMessages > 0 - ? Text(math.min(9, s.unreadMessages).toString()) + ? (s.unreadMessages < 10) + ? s.unreadMessages.toString() + : '9+' : null, orElse: () => null, ) @@ -626,7 +627,7 @@ class _GameBottomBar extends ConsumerWidget { icon: Theme.of(context).platform == TargetPlatform.iOS ? CupertinoIcons.chat_bubble : Icons.chat_bubble_outline, - chip: chatUnreadChip, + badgeLabel: chatUnreadLabel, ), RepeatButton( onLongPress: diff --git a/lib/src/view/user/game_history_screen.dart b/lib/src/view/user/game_history_screen.dart index 9be6f6b5cb..62e48761f5 100644 --- a/lib/src/view/user/game_history_screen.dart +++ b/lib/src/view/user/game_history_screen.dart @@ -39,52 +39,26 @@ class GameHistoryScreen extends ConsumerWidget { error: (e, s) => Text(context.l10n.mobileAllGames), ) : Text(filtersInUse.selectionLabel(context)); - final filterBtn = Stack( - alignment: Alignment.center, - children: [ - AppBarIconButton( - icon: const Icon(Icons.tune), - semanticsLabel: context.l10n.filterGames, - onPressed: () => showAdaptiveBottomSheet( - context: context, - builder: (_) => _FilterGames( - filter: ref.read(gameFilterProvider(filter: gameFilter)), - user: user, - ), - ).then((value) { - if (value != null) { - ref - .read(gameFilterProvider(filter: gameFilter).notifier) - .setFilter(value); - } - }), + final filterBtn = AppBarIconButton( + icon: Badge.count( + count: filtersInUse.count, + isLabelVisible: filtersInUse.count > 0, + child: const Icon(Icons.tune), + ), + semanticsLabel: context.l10n.filterGames, + onPressed: () => showAdaptiveBottomSheet( + context: context, + builder: (_) => _FilterGames( + filter: ref.read(gameFilterProvider(filter: gameFilter)), + user: user, ), - if (filtersInUse.count > 0) - Positioned( - top: 2.0, - right: 2.0, - child: Stack( - alignment: Alignment.center, - children: [ - Icon( - Icons.brightness_1, - size: 20.0, - color: Theme.of(context).colorScheme.secondary, - ), - FittedBox( - fit: BoxFit.contain, - child: DefaultTextStyle.merge( - style: TextStyle( - color: Theme.of(context).colorScheme.onSecondary, - fontWeight: FontWeight.bold, - ), - child: Text(filtersInUse.count.toString()), - ), - ), - ], - ), - ), - ], + ).then((value) { + if (value != null) { + ref + .read(gameFilterProvider(filter: gameFilter).notifier) + .setFilter(value); + } + }), ); return PlatformScaffold( diff --git a/lib/src/widgets/bottom_bar_button.dart b/lib/src/widgets/bottom_bar_button.dart index 523730aefd..77d63546fd 100644 --- a/lib/src/widgets/bottom_bar_button.dart +++ b/lib/src/widgets/bottom_bar_button.dart @@ -7,7 +7,7 @@ class BottomBarButton extends StatelessWidget { required this.icon, required this.label, required this.onTap, - this.chip, + this.badgeLabel, this.highlighted = false, this.showLabel = false, this.showTooltip = true, @@ -18,7 +18,7 @@ class BottomBarButton extends StatelessWidget { final IconData icon; final String label; - final Widget? chip; + final String? badgeLabel; final VoidCallback? onTap; final bool highlighted; @@ -36,8 +36,6 @@ class BottomBarButton extends StatelessWidget { Widget build(BuildContext context) { final primary = Theme.of(context).colorScheme.primary; - final chipColor = Theme.of(context).colorScheme.secondary; - final labelFontSize = Theme.of(context).platform == TargetPlatform.iOS ? 11.0 : Theme.of(context).textTheme.bodySmall?.fontSize; @@ -59,58 +57,33 @@ class BottomBarButton extends StatelessWidget { onTap: onTap, child: Opacity( opacity: enabled ? 1.0 : 0.4, - child: Stack( - alignment: Alignment.center, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - if (blink) - _BlinkIcon( - icon: icon, - color: highlighted - ? primary - : Theme.of(context).iconTheme.color ?? Colors.black, - ) - else - Icon(icon, color: highlighted ? primary : null), - if (showLabel) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Text( - label, - style: TextStyle( - fontSize: labelFontSize, - color: highlighted ? primary : null, - ), - ), + if (blink) + _BlinkIcon( + badgeLabel: badgeLabel, + icon: icon, + color: highlighted + ? primary + : Theme.of(context).iconTheme.color ?? Colors.black, + ) + else + Badge( + isLabelVisible: badgeLabel != null, + label: (badgeLabel != null) ? Text(badgeLabel!) : null, + child: Icon(icon, color: highlighted ? primary : null), + ), + if (showLabel) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Text( + label, + style: TextStyle( + fontSize: labelFontSize, + color: highlighted ? primary : null, ), - ], - ), - if (chip != null) - Positioned( - top: 2.0, - right: 2.0, - child: Stack( - alignment: Alignment.center, - children: [ - Icon( - Icons.brightness_1, - size: 20.0, - color: chipColor, - ), - FittedBox( - fit: BoxFit.contain, - child: DefaultTextStyle.merge( - style: TextStyle( - color: Theme.of(context).colorScheme.onSecondary, - fontWeight: FontWeight.bold, - ), - child: chip!, - ), - ), - ], ), ), ], @@ -124,10 +97,12 @@ class BottomBarButton extends StatelessWidget { class _BlinkIcon extends StatefulWidget { const _BlinkIcon({ + this.badgeLabel, required this.icon, required this.color, }); + final String? badgeLabel; final IconData icon; final Color color; @@ -173,9 +148,13 @@ class _BlinkIconState extends State<_BlinkIcon> return AnimatedBuilder( animation: _colorAnimation, builder: (context, child) { - return Icon( - widget.icon, - color: _colorAnimation.value ?? Colors.transparent, + return Badge( + isLabelVisible: widget.badgeLabel != null, + label: widget.badgeLabel != null ? Text(widget.badgeLabel!) : null, + child: Icon( + widget.icon, + color: _colorAnimation.value ?? Colors.transparent, + ), ); }, ); From 3c34db00dcef7a677b2bb26da908ef932767a3c7 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 5 Sep 2024 11:14:50 +0200 Subject: [PATCH 264/979] Update chessground --- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 78cccb65d3..19ba4d30ed 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -194,10 +194,10 @@ packages: dependency: "direct main" description: name: chessground - sha256: "5471f307a2f50801f2f7fcf196b241a87a8c075b755db75ff005407a0f1299cc" + sha256: ac2bfe069b6ad96d7c1299d6901a4c6aeb15223fdcc3dd5c4e8073d2a9baa5e9 url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "5.1.0" ci: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 57bc99e76a..3890a5a21b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ dependencies: app_settings: ^5.1.1 async: ^2.10.0 cached_network_image: ^3.2.2 - chessground: ^5.0.0 + chessground: ^5.1.0 collection: ^1.17.0 connectivity_plus: ^6.0.2 cronet_http: ^1.3.1 From f4b985558d65405622097f3f670fa516e8c85071 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 5 Sep 2024 12:08:08 +0200 Subject: [PATCH 265/979] Fix a crash on iOS when putting app in background This is a workaround fix. Crash is investigated in #973. Closes #970. --- lib/src/view/analysis/analysis_screen.dart | 45 +++++++------------ .../offline_correspondence_game_screen.dart | 1 - lib/src/view/game/archived_game_screen.dart | 1 - lib/src/view/game/game_body.dart | 2 - lib/src/view/game/game_list_tile.dart | 1 - lib/src/view/game/game_result_dialog.dart | 1 - lib/src/view/puzzle/puzzle_screen.dart | 1 - lib/src/view/puzzle/streak_screen.dart | 1 - 8 files changed, 15 insertions(+), 38 deletions(-) diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index 44f4611679..27d2a1142c 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -52,7 +52,6 @@ class AnalysisScreen extends StatelessWidget { const AnalysisScreen({ required this.options, required this.pgnOrId, - this.title, }); /// The analysis options. @@ -61,26 +60,22 @@ class AnalysisScreen extends StatelessWidget { /// The PGN or game ID to load. final String pgnOrId; - final String? title; - @override Widget build(BuildContext context) { return pgnOrId.length == 8 && GameId(pgnOrId).isValid - ? _LoadGame(GameId(pgnOrId), options, title) + ? _LoadGame(GameId(pgnOrId), options) : _LoadedAnalysisScreen( options: options, pgn: pgnOrId, - title: title, ); } } class _LoadGame extends ConsumerWidget { - const _LoadGame(this.gameId, this.options, this.title); + const _LoadGame(this.gameId, this.options); final AnalysisOptions options; final GameId gameId; - final String? title; @override Widget build(BuildContext context, WidgetRef ref) { @@ -100,7 +95,6 @@ class _LoadGame extends ConsumerWidget { serverAnalysis: serverAnalysis, ), pgn: game.makePgn(), - title: title, ); }, loading: () => const Center(child: CircularProgressIndicator.adaptive()), @@ -117,14 +111,11 @@ class _LoadedAnalysisScreen extends ConsumerWidget { const _LoadedAnalysisScreen({ required this.options, required this.pgn, - this.title, }); final AnalysisOptions options; final String pgn; - final String? title; - @override Widget build(BuildContext context, WidgetRef ref) { return ConsumerPlatformWidget( @@ -140,7 +131,7 @@ class _LoadedAnalysisScreen extends ConsumerWidget { return PlatformScaffold( resizeToAvoidBottomInset: false, appBar: PlatformAppBar( - title: _Title(options: options, title: title), + title: _Title(options: options), actions: [ _EngineDepth(ctrlProvider), AppBarIconButton( @@ -167,7 +158,7 @@ class _LoadedAnalysisScreen extends ConsumerWidget { resizeToAvoidBottomInset: false, navigationBar: CupertinoNavigationBar( padding: Styles.cupertinoAppBarTrailingWidgetPadding, - middle: _Title(options: options, title: title), + middle: _Title(options: options), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ @@ -192,27 +183,21 @@ class _LoadedAnalysisScreen extends ConsumerWidget { } class _Title extends StatelessWidget { - const _Title({ - required this.options, - this.title, - }); + const _Title({required this.options}); final AnalysisOptions options; - final String? title; @override Widget build(BuildContext context) { - return title != null - ? Text(title!) - : Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (options.variant != Variant.standard) ...[ - Icon(options.variant.icon), - const SizedBox(width: 5.0), - ], - Text(context.l10n.analysis), - ], - ); + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (options.variant != Variant.standard) ...[ + Icon(options.variant.icon), + const SizedBox(width: 5.0), + ], + Text(context.l10n.analysis), + ], + ); } } diff --git a/lib/src/view/correspondence/offline_correspondence_game_screen.dart b/lib/src/view/correspondence/offline_correspondence_game_screen.dart index c00130793d..a142b27025 100644 --- a/lib/src/view/correspondence/offline_correspondence_game_screen.dart +++ b/lib/src/view/correspondence/offline_correspondence_game_screen.dart @@ -265,7 +265,6 @@ class _BodyState extends ConsumerState<_Body> { id: game.id, division: game.meta.division, ), - title: context.l10n.analysis, ), ); }, diff --git a/lib/src/view/game/archived_game_screen.dart b/lib/src/view/game/archived_game_screen.dart index 7a5a8fd986..abb311d733 100644 --- a/lib/src/view/game/archived_game_screen.dart +++ b/lib/src/view/game/archived_game_screen.dart @@ -245,7 +245,6 @@ class _BottomBar extends ConsumerWidget { pushPlatformRoute( context, builder: (context) => AnalysisScreen( - title: context.l10n.gameAnalysis, pgnOrId: game.makePgn(), options: AnalysisOptions( isLocalEvaluationAllowed: true, diff --git a/lib/src/view/game/game_body.dart b/lib/src/view/game/game_body.dart index 7499082212..6d492f200c 100644 --- a/lib/src/view/game/game_body.dart +++ b/lib/src/view/game/game_body.dart @@ -552,7 +552,6 @@ class _GameBottomBar extends ConsumerWidget { builder: (_) => AnalysisScreen( pgnOrId: gameState.analysisPgn, options: gameState.analysisOptions, - title: context.l10n.gameAnalysis, ), ); }, @@ -688,7 +687,6 @@ class _GameBottomBar extends ConsumerWidget { options: gameState.analysisOptions.copyWith( isLocalEvaluationAllowed: false, ), - title: context.l10n.analysis, ), ); }, diff --git a/lib/src/view/game/game_list_tile.dart b/lib/src/view/game/game_list_tile.dart index 78f6b42514..199b555812 100644 --- a/lib/src/view/game/game_list_tile.dart +++ b/lib/src/view/game/game_list_tile.dart @@ -239,7 +239,6 @@ class _ContextMenu extends ConsumerWidget { pushPlatformRoute( context, builder: (context) => AnalysisScreen( - title: context.l10n.gameAnalysis, pgnOrId: game.id.value, options: AnalysisOptions( isLocalEvaluationAllowed: true, diff --git a/lib/src/view/game/game_result_dialog.dart b/lib/src/view/game/game_result_dialog.dart index 4c3dd5da43..59e968a960 100644 --- a/lib/src/view/game/game_result_dialog.dart +++ b/lib/src/view/game/game_result_dialog.dart @@ -213,7 +213,6 @@ class _GameEndDialogState extends ConsumerState { builder: (_) => AnalysisScreen( pgnOrId: gameState.analysisPgn, options: gameState.analysisOptions, - title: context.l10n.gameAnalysis, ), ); }, diff --git a/lib/src/view/puzzle/puzzle_screen.dart b/lib/src/view/puzzle/puzzle_screen.dart index 78a4a44070..5173bb08d5 100644 --- a/lib/src/view/puzzle/puzzle_screen.dart +++ b/lib/src/view/puzzle/puzzle_screen.dart @@ -484,7 +484,6 @@ class _BottomBar extends ConsumerWidget { pushPlatformRoute( context, builder: (context) => AnalysisScreen( - title: context.l10n.analysis, pgnOrId: ref.read(ctrlProvider.notifier).makePgn(), options: AnalysisOptions( isLocalEvaluationAllowed: true, diff --git a/lib/src/view/puzzle/streak_screen.dart b/lib/src/view/puzzle/streak_screen.dart index 77331d132d..3ca9365466 100644 --- a/lib/src/view/puzzle/streak_screen.dart +++ b/lib/src/view/puzzle/streak_screen.dart @@ -291,7 +291,6 @@ class _BottomBar extends ConsumerWidget { pushPlatformRoute( context, builder: (context) => AnalysisScreen( - title: context.l10n.analysis, pgnOrId: ref.read(ctrlProvider.notifier).makePgn(), options: AnalysisOptions( isLocalEvaluationAllowed: true, From fc962970f14deeca66f3a4d8118cba02871481fb Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 5 Sep 2024 12:28:34 +0200 Subject: [PATCH 266/979] Disable impeller for now on android Fixes #972 --- android/app/src/main/AndroidManifest.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index f21c4f27c7..0371f10eff 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -17,6 +17,10 @@ android:roundIcon="@mipmap/ic_launcher_round" android:fullBackupContent="@xml/backup_rules"> + + From 96c3c1cd1531b23df0020faf76c29c3b842fd57d Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 5 Sep 2024 12:58:45 +0200 Subject: [PATCH 267/979] Bump version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 3890a5a21b..7ddb885312 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: lichess_mobile description: Lichess mobile app V2 publish_to: "none" -version: 0.10.0+001000 # see README.md for details about versioning +version: 0.10.1+001001 # see README.md for details about versioning environment: sdk: ">=3.3.0 <4.0.0" From a51e1e60a5fb73b2eeae600ffb01de15e545036e Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Tue, 3 Sep 2024 22:19:30 +0200 Subject: [PATCH 268/979] feat: coordinate trainer --- .../coordinate_training_controller.dart | 110 ++++ .../coordinate_training_preferences.dart | 136 +++++ .../coordinate_display.dart | 135 +++++ .../coordinate_training_screen.dart | 535 ++++++++++++++++++ .../coordinate_training_screen_test.dart | 157 +++++ 5 files changed, 1073 insertions(+) create mode 100644 lib/src/model/coordinate_training/coordinate_training_controller.dart create mode 100644 lib/src/model/coordinate_training/coordinate_training_preferences.dart create mode 100644 lib/src/view/coordinate_training/coordinate_display.dart create mode 100644 lib/src/view/coordinate_training/coordinate_training_screen.dart create mode 100644 test/view/coordinate_training/coordinate_training_screen_test.dart diff --git a/lib/src/model/coordinate_training/coordinate_training_controller.dart b/lib/src/model/coordinate_training/coordinate_training_controller.dart new file mode 100644 index 0000000000..5c33e7d829 --- /dev/null +++ b/lib/src/model/coordinate_training/coordinate_training_controller.dart @@ -0,0 +1,110 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:dartchess/dartchess.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'coordinate_training_controller.freezed.dart'; +part 'coordinate_training_controller.g.dart'; + +enum Guess { correct, incorrect } + +@riverpod +class CoordinateTrainingController extends _$CoordinateTrainingController { + final _random = Random(DateTime.now().millisecondsSinceEpoch); + + final _stopwatch = Stopwatch(); + + Timer? _updateTimer; + + @override + CoordinateTrainingState build() { + ref.onDispose(() { + _updateTimer?.cancel(); + }); + return const CoordinateTrainingState(); + } + + void startTraining(Duration? timeLimit) { + final currentCoord = _randomCoord(); + state = state.copyWith( + currentCoord: currentCoord, + nextCoord: _randomCoord(previous: currentCoord), + score: 0, + timeLimit: timeLimit, + elapsed: Duration.zero, + lastGuess: null, + ); + + _updateTimer?.cancel(); + _stopwatch.reset(); + _stopwatch.start(); + _updateTimer = Timer.periodic(const Duration(milliseconds: 100), (_) { + if (state.timeLimit != null && _stopwatch.elapsed > state.timeLimit!) { + _finishTraining(); + } else { + state = state.copyWith( + elapsed: _stopwatch.elapsed, + ); + } + }); + } + + void _finishTraining() { + // TODO save score in local storage here (and display high score and/or average score in UI) + + stopTraining(); + } + + void stopTraining() { + _updateTimer?.cancel(); + state = const CoordinateTrainingState(); + } + + /// Generate a random square that is not the same as the [previous] square + Square _randomCoord({Square? previous}) { + while (true) { + final square = Square.values[_random.nextInt(Square.values.length)]; + if (square != previous) { + return square; + } + } + } + + void guessCoordinate(Square coord) { + final correctGuess = coord == state.currentCoord; + + if (correctGuess) { + state = state.copyWith( + currentCoord: state.nextCoord, + nextCoord: _randomCoord(previous: state.nextCoord), + score: state.score + 1, + ); + } + + state = state.copyWith( + lastGuess: correctGuess ? Guess.correct : Guess.incorrect, + ); + } +} + +@freezed +class CoordinateTrainingState with _$CoordinateTrainingState { + const CoordinateTrainingState._(); + + const factory CoordinateTrainingState({ + @Default(null) Square? currentCoord, + @Default(null) Square? nextCoord, + @Default(0) int score, + @Default(null) Duration? timeLimit, + @Default(null) Duration? elapsed, + @Default(null) Guess? lastGuess, + }) = _CoordinateTrainingState; + + bool get trainingActive => elapsed != null; + + double? get timeFractionElapsed => (elapsed != null && timeLimit != null) + ? elapsed!.inMilliseconds / timeLimit!.inMilliseconds + : null; +} diff --git a/lib/src/model/coordinate_training/coordinate_training_preferences.dart b/lib/src/model/coordinate_training/coordinate_training_preferences.dart new file mode 100644 index 0000000000..fbc5b11725 --- /dev/null +++ b/lib/src/model/coordinate_training/coordinate_training_preferences.dart @@ -0,0 +1,136 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:lichess_mobile/src/db/shared_preferences.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'coordinate_training_preferences.freezed.dart'; +part 'coordinate_training_preferences.g.dart'; + +enum SideChoice { + white, + random, + black, +} + +String sideChoiceL10n(BuildContext context, SideChoice side) { + switch (side) { + case SideChoice.white: + return context.l10n.white; + case SideChoice.black: + return context.l10n.black; + case SideChoice.random: + // TODO l10n + return 'Random'; + } +} + +enum TimeChoice { + thirtySeconds(Duration(seconds: 30)), + unlimited(null); + + const TimeChoice(this.duration); + + final Duration? duration; +} + +// TODO l10n +Widget timeChoiceL10n(BuildContext context, TimeChoice time) { + switch (time) { + case TimeChoice.thirtySeconds: + return const Text('30s'); + case TimeChoice.unlimited: + return const Icon(Icons.all_inclusive); + } +} + +enum TrainingMode { + findSquare, + nameSquare, +} + +// TODO l10n +String trainingModeL10n(BuildContext context, TrainingMode mode) { + switch (mode) { + case TrainingMode.findSquare: + return 'Find Square'; + case TrainingMode.nameSquare: + return 'Name Square'; + } +} + +@Riverpod(keepAlive: true) +class CoordinateTrainingPreferences extends _$CoordinateTrainingPreferences { + static const String prefKey = 'preferences.coordinate_training'; + + @override + CoordinateTrainingPrefs build() { + final prefs = ref.watch(sharedPreferencesProvider); + final stored = prefs.getString(prefKey); + return stored != null + ? CoordinateTrainingPrefs.fromJson( + jsonDecode(stored) as Map, + ) + : CoordinateTrainingPrefs.defaults; + } + + Future setShowCoordinates(bool showCoordinates) { + return _save(state.copyWith(showCoordinates: showCoordinates)); + } + + Future setShowPieces(bool showPieces) { + return _save(state.copyWith(showPieces: showPieces)); + } + + Future setMode(TrainingMode mode) { + return _save(state.copyWith(mode: mode)); + } + + Future setTimeChoice(TimeChoice timeChoice) { + return _save(state.copyWith(timeChoice: timeChoice)); + } + + Future setSideChoice(SideChoice sideChoice) { + return _save(state.copyWith(sideChoice: sideChoice)); + } + + Future _save(CoordinateTrainingPrefs newState) async { + final prefs = ref.read(sharedPreferencesProvider); + await prefs.setString( + prefKey, + jsonEncode(newState.toJson()), + ); + state = newState; + } +} + +@Freezed(fromJson: true, toJson: true) +class CoordinateTrainingPrefs with _$CoordinateTrainingPrefs { + const CoordinateTrainingPrefs._(); + + const factory CoordinateTrainingPrefs({ + required bool showCoordinates, + required bool showPieces, + required TrainingMode mode, + required TimeChoice timeChoice, + required SideChoice sideChoice, + }) = _CoordinateTrainingPrefs; + + static const defaults = CoordinateTrainingPrefs( + showCoordinates: false, + showPieces: true, + mode: TrainingMode.findSquare, + timeChoice: TimeChoice.thirtySeconds, + sideChoice: SideChoice.random, + ); + + factory CoordinateTrainingPrefs.fromJson(Map json) { + try { + return _$CoordinateTrainingPrefsFromJson(json); + } catch (_) { + return defaults; + } + } +} diff --git a/lib/src/view/coordinate_training/coordinate_display.dart b/lib/src/view/coordinate_training/coordinate_display.dart new file mode 100644 index 0000000000..82ceb038e0 --- /dev/null +++ b/lib/src/view/coordinate_training/coordinate_display.dart @@ -0,0 +1,135 @@ +import 'package:dartchess/dartchess.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/coordinate_training/coordinate_training_controller.dart'; + +const Offset _kNextCoordFractionalTranslation = Offset(0.8, 0.3); +const double _kNextCoordScale = 0.4; + +const double _kCurrCoordOpacity = 0.9; +const double _kNextCoordOpacity = 0.7; + +class CoordinateDisplay extends ConsumerStatefulWidget { + const CoordinateDisplay({ + required this.currentCoord, + required this.nextCoord, + }); + + final Square currentCoord; + + final Square nextCoord; + + @override + ConsumerState createState() => + CoordinateDisplayState(); +} + +class CoordinateDisplayState extends ConsumerState + with SingleTickerProviderStateMixin { + late final AnimationController _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 150), + )..value = 1.0; + + late final Animation _scaleAnimation = Tween( + begin: _kNextCoordScale, + end: 1.0, + ).animate( + CurvedAnimation(parent: _controller, curve: Curves.linear), + ); + + late final Animation _currCoordSlideInAnimation = Tween( + begin: _kNextCoordFractionalTranslation, + end: Offset.zero, + ).animate( + CurvedAnimation(parent: _controller, curve: Curves.linear), + ); + + late final Animation _nextCoordSlideInAnimation = Tween( + begin: const Offset(0.5, 0), + end: Offset.zero, + ).animate( + CurvedAnimation(parent: _controller, curve: Curves.linear), + ); + + late final Animation _currCoordOpacityAnimation = Tween( + begin: _kNextCoordOpacity, + end: _kCurrCoordOpacity, + ).animate( + CurvedAnimation(parent: _controller, curve: Curves.linear), + ); + + late final Animation _nextCoordFadeInAnimation = + Tween(begin: 0.0, end: _kNextCoordOpacity) + .animate(CurvedAnimation(parent: _controller, curve: Curves.easeIn)); + + @override + Widget build(BuildContext context) { + final trainingState = ref.watch(coordinateTrainingControllerProvider); + + final textStyle = DefaultTextStyle.of(context).style.copyWith( + fontSize: 110.0, + fontFamily: 'monospace', + fontWeight: FontWeight.bold, + fontFeatures: [const FontFeature.tabularFigures()], + shadows: const [ + Shadow( + color: Colors.black, + offset: Offset(0, 5), + blurRadius: 40.0, + ), + ], + ); + + return IgnorePointer( + child: Stack( + children: [ + FadeTransition( + opacity: _currCoordOpacityAnimation, + child: SlideTransition( + position: _currCoordSlideInAnimation, + child: ScaleTransition( + scale: _scaleAnimation, + child: Text( + trainingState.currentCoord?.name ?? '', + style: textStyle, + ), + ), + ), + ), + FadeTransition( + opacity: _nextCoordFadeInAnimation, + child: SlideTransition( + position: _nextCoordSlideInAnimation, + child: FractionalTranslation( + translation: _kNextCoordFractionalTranslation, + child: Transform.scale( + scale: _kNextCoordScale, + child: Text( + trainingState.nextCoord?.name ?? '', + style: textStyle, + ), + ), + ), + ), + ), + ], + ), + ); + } + + @override + void didUpdateWidget(covariant CoordinateDisplay oldWidget) { + super.didUpdateWidget(oldWidget); + + if (oldWidget.nextCoord != widget.nextCoord) { + _controller.forward(from: 0.0); + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } +} diff --git a/lib/src/view/coordinate_training/coordinate_training_screen.dart b/lib/src/view/coordinate_training/coordinate_training_screen.dart new file mode 100644 index 0000000000..4240270da2 --- /dev/null +++ b/lib/src/view/coordinate_training/coordinate_training_screen.dart @@ -0,0 +1,535 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:chessground/chessground.dart'; +import 'package:dartchess/dartchess.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/constants.dart'; +import 'package:lichess_mobile/src/model/coordinate_training/coordinate_training_controller.dart'; +import 'package:lichess_mobile/src/model/coordinate_training/coordinate_training_preferences.dart'; +import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; +import 'package:lichess_mobile/src/styles/lichess_colors.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/screen.dart'; +import 'package:lichess_mobile/src/view/coordinate_training/coordinate_display.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; +import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; +import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/list.dart'; +import 'package:lichess_mobile/src/widgets/platform_alert_dialog.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; +import 'package:lichess_mobile/src/widgets/settings.dart'; + +Future _coordinateTrainingInfoDialogBuilder(BuildContext context) { + return showAdaptiveDialog( + context: context, + builder: (context) { + final content = SingleChildScrollView( + child: RichText( + text: TextSpan( + style: DefaultTextStyle.of(context).style, + children: const [ + TextSpan( + text: '\n', + ), + // TODO add explanation here once l10n download from crowdin works again + ], + ), + ), + ); + + return PlatformAlertDialog( + title: Text(context.l10n.aboutX('Coordinate Training')), + content: content, + actions: [ + PlatformDialogAction( + onPressed: () => Navigator.of(context).pop(), + child: Text(context.l10n.mobileOkButton), + ), + ], + ); + }, + ); +} + +class CoordinateTrainingScreen extends StatelessWidget { + const CoordinateTrainingScreen({super.key}); + + @override + Widget build(BuildContext context) { + return const PlatformScaffold( + appBar: PlatformAppBar( + title: Text('Coordinate Training'), // TODO l10n once script works + ), + body: _Body(), + ); + } +} + +class _Body extends ConsumerStatefulWidget { + const _Body(); + + @override + ConsumerState<_Body> createState() => _BodyState(); +} + +class _BodyState extends ConsumerState<_Body> { + late Side orientation; + + Square? highlightLastGuess; + + Timer? highlightTimer; + + void _setOrientation(SideChoice choice) { + setState(() { + orientation = switch (choice) { + SideChoice.white => Side.white, + SideChoice.black => Side.black, + SideChoice.random => Side.values[Random().nextInt(Side.values.length)], + }; + }); + } + + @override + void initState() { + super.initState(); + _setOrientation(ref.read(coordinateTrainingPreferencesProvider).sideChoice); + } + + @override + void dispose() { + super.dispose(); + highlightTimer?.cancel(); + } + + @override + Widget build(BuildContext context) { + final trainingState = ref.watch(coordinateTrainingControllerProvider); + final trainingPrefs = ref.watch(coordinateTrainingPreferencesProvider); + + final IMap squareHighlights = + { + if (trainingState.trainingActive) + if (trainingPrefs.mode == TrainingMode.findSquare) ...{ + if (highlightLastGuess != null) ...{ + highlightLastGuess!: SquareHighlight( + details: HighlightDetails( + solidColor: (trainingState.lastGuess == Guess.correct + ? LichessColors.good + : LichessColors.error) + .withOpacity( + 0.5, + ), + ), + ), + }, + } else ...{ + trainingState.currentCoord!: SquareHighlight( + details: HighlightDetails( + solidColor: LichessColors.good.withOpacity( + 0.5, + ), + ), + ), + }, + }.lock; + + return SafeArea( + child: Column( + children: [ + Expanded( + child: LayoutBuilder( + builder: (context, constraints) { + final aspectRatio = constraints.biggest.aspectRatio; + + final defaultBoardSize = constraints.biggest.shortestSide; + final isTablet = isTabletOrLarger(context); + final remainingHeight = + constraints.maxHeight - defaultBoardSize; + final isSmallScreen = + remainingHeight < kSmallRemainingHeightLeftBoardThreshold; + final boardSize = isTablet || isSmallScreen + ? defaultBoardSize - kTabletBoardTableSidePadding * 2 + : defaultBoardSize; + + final direction = + aspectRatio > 1 ? Axis.horizontal : Axis.vertical; + + return Flex( + direction: direction, + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Column( + children: [ + _TimeBar( + maxWidth: boardSize, + timeFractionElapsed: + trainingState.timeFractionElapsed, + color: trainingState.lastGuess == Guess.incorrect + ? LichessColors.error + : LichessColors.good, + ), + _TrainingBoard( + boardSize: boardSize, + isTablet: isTablet, + orientation: orientation, + squareHighlights: squareHighlights, + onGuess: _onGuess, + ), + ], + ), + if (trainingState.trainingActive) + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _Score( + score: trainingState.score, + size: boardSize / 8, + color: trainingState.lastGuess == Guess.incorrect + ? LichessColors.error + : LichessColors.good, + ), + FatButton( + semanticsLabel: 'Abort Training', + onPressed: ref + .read( + coordinateTrainingControllerProvider + .notifier, + ) + .stopTraining, + child: const Text( + 'Abort Training', + style: Styles.bold, + ), + ), + ], + ), + ) + else + _Settings( + onSideChoiceSelected: _setOrientation, + ), + ], + ); + }, + ), + ), + BottomBar( + children: [ + BottomBarButton( + label: context.l10n.menu, + onTap: () => showAdaptiveBottomSheet( + context: context, + builder: (BuildContext context) => + const _CoordinateTrainingMenu(), + ), + icon: Icons.tune, + ), + BottomBarButton( + icon: Icons.info_outline, + label: context.l10n.aboutX('Coordinate Training'), + onTap: () => _coordinateTrainingInfoDialogBuilder(context), + ), + ], + ), + ], + ), + ); + } + + void _onGuess(Square square) { + ref + .read(coordinateTrainingControllerProvider.notifier) + .guessCoordinate(square); + + setState(() { + highlightLastGuess = square; + + highlightTimer?.cancel(); + highlightTimer = Timer(const Duration(milliseconds: 200), () { + setState(() { + highlightLastGuess = null; + }); + }); + }); + } +} + +class _TimeBar extends StatelessWidget { + const _TimeBar({ + required this.maxWidth, + required this.timeFractionElapsed, + required this.color, + }); + + final double maxWidth; + final double? timeFractionElapsed; + final Color color; + + @override + Widget build(BuildContext context) { + return Align( + alignment: Alignment.center, + child: SizedBox( + width: maxWidth * (timeFractionElapsed ?? 0.0), + height: 15.0, + child: ColoredBox( + color: color, + ), + ), + ); + } +} + +class _CoordinateTrainingMenu extends ConsumerWidget { + const _CoordinateTrainingMenu(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final trainingPrefs = ref.watch(coordinateTrainingPreferencesProvider); + + return SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 8.0), + child: SizedBox( + width: double.infinity, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + ListSection( + header: Text( + context.l10n.preferencesDisplay, + style: Styles.sectionTitle, + ), + children: [ + SwitchSettingTile( + title: const Text('Show Coordinates'), + value: trainingPrefs.showCoordinates, + onChanged: ref + .read(coordinateTrainingPreferencesProvider.notifier) + .setShowCoordinates, + ), + SwitchSettingTile( + title: const Text('Show Pieces'), + value: trainingPrefs.showPieces, + onChanged: ref + .read(coordinateTrainingPreferencesProvider.notifier) + .setShowPieces, + ), + ], + ), + ], + ), + ), + ), + ); + } +} + +class _Score extends StatelessWidget { + const _Score({ + required this.size, + required this.color, + required this.score, + }); + + final int score; + + final double size; + + final Color color; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only( + top: 10.0, + left: 10.0, + right: 10.0, + ), + child: Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all( + Radius.circular(4.0), + ), + color: color, + ), + width: size, + height: size, + child: Center( + child: Text( + score.toString(), + style: Styles.bold.copyWith( + color: Colors.white, + fontSize: 24.0, + ), + ), + ), + ), + ); + } +} + +class _Settings extends ConsumerStatefulWidget { + const _Settings({ + required this.onSideChoiceSelected, + }); + + final void Function(SideChoice) onSideChoiceSelected; + + @override + ConsumerState<_Settings> createState() => _SettingsState(); +} + +class _SettingsState extends ConsumerState<_Settings> { + @override + Widget build(BuildContext context) { + final trainingPrefs = ref.watch(coordinateTrainingPreferencesProvider); + + return Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + PlatformListTile( + title: Text(context.l10n.side), + trailing: Padding( + padding: Styles.horizontalBodyPadding, + child: Wrap( + spacing: 8.0, + children: SideChoice.values.map((choice) { + return ChoiceChip( + label: Text(sideChoiceL10n(context, choice)), + selected: trainingPrefs.sideChoice == choice, + showCheckmark: false, + onSelected: (selected) { + widget.onSideChoiceSelected(choice); + ref + .read(coordinateTrainingPreferencesProvider.notifier) + .setSideChoice(choice); + }, + ); + }).toList(), + ), + ), + ), + PlatformListTile( + title: Text(context.l10n.time), + trailing: Padding( + padding: Styles.horizontalBodyPadding, + child: Wrap( + spacing: 8.0, + children: TimeChoice.values.map((choice) { + return ChoiceChip( + label: timeChoiceL10n(context, choice), + selected: trainingPrefs.timeChoice == choice, + showCheckmark: false, + onSelected: (selected) { + if (selected) { + ref + .read( + coordinateTrainingPreferencesProvider.notifier, + ) + .setTimeChoice(choice); + } + }, + ); + }).toList(), + ), + ), + ), + FatButton( + semanticsLabel: 'Start Training', + onPressed: () => ref + .read(coordinateTrainingControllerProvider.notifier) + .startTraining(trainingPrefs.timeChoice.duration), + child: const Text( + // TODO l10n once script works + 'Start Training', + style: Styles.bold, + ), + ), + ], + ), + ); + } +} + +class _TrainingBoard extends ConsumerStatefulWidget { + const _TrainingBoard({ + required this.boardSize, + required this.isTablet, + required this.orientation, + required this.onGuess, + required this.squareHighlights, + }); + + final double boardSize; + + final bool isTablet; + + final Side orientation; + + final void Function(Square) onGuess; + + final IMap squareHighlights; + + @override + ConsumerState<_TrainingBoard> createState() => _TrainingBoardState(); +} + +class _TrainingBoardState extends ConsumerState<_TrainingBoard> { + @override + Widget build(BuildContext context) { + final boardPrefs = ref.watch(boardPreferencesProvider); + final trainingPrefs = ref.watch(coordinateTrainingPreferencesProvider); + final trainingState = ref.watch(coordinateTrainingControllerProvider); + + return Column( + children: [ + Stack( + alignment: Alignment.center, + children: [ + ChessboardEditor( + size: widget.boardSize, + pieces: readFen( + trainingPrefs.showPieces ? kInitialFEN : kEmptyFEN, + ), + squareHighlights: widget.squareHighlights, + orientation: widget.orientation, + settings: ChessboardEditorSettings( + pieceAssets: boardPrefs.pieceSet.assets, + colorScheme: boardPrefs.boardTheme.colors, + enableCoordinates: trainingPrefs.showCoordinates, + borderRadius: widget.isTablet + ? const BorderRadius.all(Radius.circular(4.0)) + : BorderRadius.zero, + boxShadow: widget.isTablet ? boardShadows : const [], + ), + pointerMode: EditorPointerMode.edit, + onEditedSquare: (square) { + if (trainingState.trainingActive && + trainingPrefs.mode == TrainingMode.findSquare) { + widget.onGuess(square); + } + }, + ), + if (trainingState.trainingActive && + trainingPrefs.mode == TrainingMode.findSquare) + CoordinateDisplay( + currentCoord: trainingState.currentCoord!, + nextCoord: trainingState.nextCoord!, + ), + ], + ), + ], + ); + } +} diff --git a/test/view/coordinate_training/coordinate_training_screen_test.dart b/test/view/coordinate_training/coordinate_training_screen_test.dart new file mode 100644 index 0000000000..5c2f253e85 --- /dev/null +++ b/test/view/coordinate_training/coordinate_training_screen_test.dart @@ -0,0 +1,157 @@ +import 'package:chessground/chessground.dart'; +import 'package:dartchess/dartchess.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:lichess_mobile/src/model/coordinate_training/coordinate_training_controller.dart'; +import 'package:lichess_mobile/src/model/coordinate_training/coordinate_training_preferences.dart'; +import 'package:lichess_mobile/src/view/coordinate_training/coordinate_training_screen.dart'; + +import '../../test_app.dart'; + +void main() { + group('Coordinate Training', () { + testWidgets('Initial state when started in FindSquare mode', + (tester) async { + final app = await buildTestApp( + tester, + home: const CoordinateTrainingScreen(), + ); + await tester.pumpWidget(app); + + await tester.tap(find.text('Start Training')); + await tester.pumpAndSettle(); + + final container = ProviderScope.containerOf( + tester.element(find.byType(ChessboardEditor)), + ); + final controllerProvider = coordinateTrainingControllerProvider; + + final trainingPrefsNotifier = container.read( + coordinateTrainingPreferencesProvider.notifier, + ); + trainingPrefsNotifier.setMode(TrainingMode.findSquare); + // This way all squares can be found via find.byKey(ValueKey('${square.name}-empty')) + trainingPrefsNotifier.setShowPieces(false); + await tester.pumpAndSettle(); + + expect(container.read(controllerProvider).score, 0); + expect(container.read(controllerProvider).currentCoord, isNotNull); + expect(container.read(controllerProvider).nextCoord, isNotNull); + expect(container.read(controllerProvider).trainingActive, true); + + // Current and next coordinate prompt should be displayed + expect( + find.text(container.read(controllerProvider).currentCoord!.name), + findsOneWidget, + ); + expect( + find.text(container.read(controllerProvider).nextCoord!.name), + findsOneWidget, + ); + }); + + testWidgets('Tap wrong square', (tester) async { + final app = await buildTestApp( + tester, + home: const CoordinateTrainingScreen(), + ); + await tester.pumpWidget(app); + + await tester.tap(find.text('Start Training')); + await tester.pumpAndSettle(); + + final container = ProviderScope.containerOf( + tester.element(find.byType(ChessboardEditor)), + ); + final controllerProvider = coordinateTrainingControllerProvider; + + final trainingPrefsNotifier = container.read( + coordinateTrainingPreferencesProvider.notifier, + ); + trainingPrefsNotifier.setMode(TrainingMode.findSquare); + // This way all squares can be found via find.byKey(ValueKey('${square.name}-empty')) + trainingPrefsNotifier.setShowPieces(false); + await tester.pumpAndSettle(); + + final currentCoord = container.read(controllerProvider).currentCoord; + final nextCoord = container.read(controllerProvider).nextCoord; + + final wrongCoord = + Square.values[(currentCoord! + 1) % Square.values.length]; + + await tester.tap(find.byKey(ValueKey('${wrongCoord.name}-empty'))); + await tester.pump(); + + expect(container.read(controllerProvider).score, 0); + expect(container.read(controllerProvider).currentCoord, currentCoord); + expect(container.read(controllerProvider).nextCoord, nextCoord); + expect(container.read(controllerProvider).trainingActive, true); + + expect( + find.byKey(ValueKey('${wrongCoord.name}-highlight')), + findsOneWidget, + ); + + await tester.pump(const Duration(milliseconds: 300)); + expect( + find.byKey(ValueKey('${wrongCoord.name}-highlight')), + findsNothing, + ); + }); + + testWidgets('Tap correct square', (tester) async { + final app = await buildTestApp( + tester, + home: const CoordinateTrainingScreen(), + ); + await tester.pumpWidget(app); + + await tester.tap(find.text('Start Training')); + await tester.pumpAndSettle(); + + final container = ProviderScope.containerOf( + tester.element(find.byType(ChessboardEditor)), + ); + final controllerProvider = coordinateTrainingControllerProvider; + + final trainingPrefsNotifier = container.read( + coordinateTrainingPreferencesProvider.notifier, + ); + trainingPrefsNotifier.setMode(TrainingMode.findSquare); + // This way all squares can be found via find.byKey(ValueKey('${square.name}-empty')) + trainingPrefsNotifier.setShowPieces(false); + await tester.pumpAndSettle(); + + final currentCoord = container.read(controllerProvider).currentCoord; + final nextCoord = container.read(controllerProvider).nextCoord; + + await tester.tap(find.byKey(ValueKey('${currentCoord!.name}-empty'))); + await tester.pump(); + + expect( + find.byKey(ValueKey('${currentCoord.name}-highlight')), + findsOneWidget, + ); + + expect(container.read(controllerProvider).score, 1); + expect(container.read(controllerProvider).currentCoord, nextCoord); + expect(container.read(controllerProvider).trainingActive, true); + + await tester.pumpAndSettle(const Duration(milliseconds: 300)); + expect( + find.byKey(ValueKey('${currentCoord.name}-highlight')), + findsNothing, + ); + + expect( + find.text(container.read(controllerProvider).currentCoord!.name), + findsOneWidget, + ); + expect( + find.text(container.read(controllerProvider).nextCoord!.name), + findsOneWidget, + ); + }); + }); +} From 7489d98fd02b25eed61967f7ca23df2e370efbe2 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Thu, 5 Sep 2024 19:55:18 +0200 Subject: [PATCH 269/979] change the badge background color for bottom bar button --- lib/src/widgets/bottom_bar_button.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/src/widgets/bottom_bar_button.dart b/lib/src/widgets/bottom_bar_button.dart index 77d63546fd..cb5e403dca 100644 --- a/lib/src/widgets/bottom_bar_button.dart +++ b/lib/src/widgets/bottom_bar_button.dart @@ -71,6 +71,7 @@ class BottomBarButton extends StatelessWidget { ) else Badge( + backgroundColor: Theme.of(context).colorScheme.onSecondary, isLabelVisible: badgeLabel != null, label: (badgeLabel != null) ? Text(badgeLabel!) : null, child: Icon(icon, color: highlighted ? primary : null), @@ -149,6 +150,7 @@ class _BlinkIconState extends State<_BlinkIcon> animation: _colorAnimation, builder: (context, child) { return Badge( + backgroundColor: Theme.of(context).colorScheme.onSecondary, isLabelVisible: widget.badgeLabel != null, label: widget.badgeLabel != null ? Text(widget.badgeLabel!) : null, child: Icon( From 5ce3d5992e00d4e80ad2b01729e35fb5c489e053 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Fri, 6 Sep 2024 10:30:53 +0200 Subject: [PATCH 270/979] use the same style as before --- lib/src/view/user/game_history_screen.dart | 5 +++++ lib/src/widgets/bottom_bar_button.dart | 12 ++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/lib/src/view/user/game_history_screen.dart b/lib/src/view/user/game_history_screen.dart index 62e48761f5..a84fc56bd0 100644 --- a/lib/src/view/user/game_history_screen.dart +++ b/lib/src/view/user/game_history_screen.dart @@ -41,6 +41,11 @@ class GameHistoryScreen extends ConsumerWidget { : Text(filtersInUse.selectionLabel(context)); final filterBtn = AppBarIconButton( icon: Badge.count( + backgroundColor: Theme.of(context).colorScheme.secondary, + textStyle: TextStyle( + color: Theme.of(context).colorScheme.onSecondary, + fontWeight: FontWeight.bold, + ), count: filtersInUse.count, isLabelVisible: filtersInUse.count > 0, child: const Icon(Icons.tune), diff --git a/lib/src/widgets/bottom_bar_button.dart b/lib/src/widgets/bottom_bar_button.dart index cb5e403dca..42d340dc3b 100644 --- a/lib/src/widgets/bottom_bar_button.dart +++ b/lib/src/widgets/bottom_bar_button.dart @@ -71,7 +71,11 @@ class BottomBarButton extends StatelessWidget { ) else Badge( - backgroundColor: Theme.of(context).colorScheme.onSecondary, + backgroundColor: Theme.of(context).colorScheme.secondary, + textStyle: TextStyle( + color: Theme.of(context).colorScheme.onSecondary, + fontWeight: FontWeight.bold, + ), isLabelVisible: badgeLabel != null, label: (badgeLabel != null) ? Text(badgeLabel!) : null, child: Icon(icon, color: highlighted ? primary : null), @@ -150,7 +154,11 @@ class _BlinkIconState extends State<_BlinkIcon> animation: _colorAnimation, builder: (context, child) { return Badge( - backgroundColor: Theme.of(context).colorScheme.onSecondary, + backgroundColor: Theme.of(context).colorScheme.secondary, + textStyle: TextStyle( + color: Theme.of(context).colorScheme.onSecondary, + fontWeight: FontWeight.bold, + ), isLabelVisible: widget.badgeLabel != null, label: widget.badgeLabel != null ? Text(widget.badgeLabel!) : null, child: Icon( From 6fae32ff9e1e4b7fce77f864620b122e818ad0cd Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 6 Sep 2024 12:39:42 +0200 Subject: [PATCH 271/979] Fix CountdownClock: update time left on stop Closes #932 --- lib/src/widgets/countdown_clock.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/src/widgets/countdown_clock.dart b/lib/src/widgets/countdown_clock.dart index 467464715a..0fb7216e20 100644 --- a/lib/src/widgets/countdown_clock.dart +++ b/lib/src/widgets/countdown_clock.dart @@ -88,6 +88,12 @@ class _CountdownClockState extends ConsumerState { } void stopClock() { + setState(() { + timeLeft = timeLeft - _stopwatch.elapsed; + if (timeLeft < Duration.zero) { + timeLeft = Duration.zero; + } + }); _timer?.cancel(); _stopwatch.stop(); scheduleMicrotask(() { From 8768258f9768c0c33b65f137c0e752dda3cd7499 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 6 Sep 2024 13:18:43 +0200 Subject: [PATCH 272/979] Improve standalone clock --- lib/src/model/clock/clock_controller.dart | 22 +++++++++++++++------- lib/src/view/clock/clock_settings.dart | 18 ++++++++++++++---- lib/src/view/clock/clock_tile.dart | 17 +++++++++++++---- 3 files changed, 42 insertions(+), 15 deletions(-) diff --git a/lib/src/model/clock/clock_controller.dart b/lib/src/model/clock/clock_controller.dart index c54eea1de0..1a70a0ff92 100644 --- a/lib/src/model/clock/clock_controller.dart +++ b/lib/src/model/clock/clock_controller.dart @@ -27,13 +27,13 @@ class ClockController extends _$ClockController { if (playerType == ClockPlayerType.top) { state = state.copyWith( started: true, - currentPlayer: ClockPlayerType.bottom, + activeSide: ClockPlayerType.bottom, playerTopMoves: started ? state.playerTopMoves + 1 : 0, ); } else { state = state.copyWith( started: true, - currentPlayer: ClockPlayerType.top, + activeSide: ClockPlayerType.top, playerBottomMoves: started ? state.playerBottomMoves + 1 : 0, ); } @@ -41,7 +41,7 @@ class ClockController extends _$ClockController { } void updateDuration(ClockPlayerType playerType, Duration duration) { - if (state.loser != null || state.currentPlayer == null || state.paused) { + if (state.loser != null || state.paused) { return; } @@ -80,11 +80,16 @@ class ClockController extends _$ClockController { ), ); + void setActiveSide(ClockPlayerType playerType) => + state = state.copyWith(activeSide: playerType); + void setLoser(ClockPlayerType playerType) => - state = state.copyWith(currentPlayer: null, loser: playerType); + state = state.copyWith(loser: playerType); void reset() => state = ClockState.fromOptions(state.options); + void start() => state = state.copyWith(started: true); + void pause() => state = state.copyWith(paused: true); void resume() => state = state.copyWith(paused: false); @@ -113,7 +118,7 @@ class ClockState with _$ClockState { required ClockOptions options, required Duration playerTopTime, required Duration playerBottomTime, - ClockPlayerType? currentPlayer, + required ClockPlayerType activeSide, ClockPlayerType? loser, @Default(false) bool started, @Default(false) bool paused, @@ -132,6 +137,7 @@ class ClockState with _$ClockState { return ClockState( id: DateTime.now().millisecondsSinceEpoch, options: options, + activeSide: ClockPlayerType.top, playerTopTime: options.timePlayerTop, playerBottomTime: options.timePlayerBottom, ); @@ -149,6 +155,7 @@ class ClockState with _$ClockState { ); return ClockState( id: DateTime.now().millisecondsSinceEpoch, + activeSide: ClockPlayerType.top, options: options, playerTopTime: options.timePlayerTop, playerBottomTime: options.timePlayerBottom, @@ -158,6 +165,7 @@ class ClockState with _$ClockState { factory ClockState.fromOptions(ClockOptions options) { return ClockState( id: DateTime.now().millisecondsSinceEpoch, + activeSide: ClockPlayerType.top, options: options, playerTopTime: options.timePlayerTop, playerBottomTime: options.timePlayerBottom, @@ -171,13 +179,13 @@ class ClockState with _$ClockState { playerType == ClockPlayerType.top ? playerTopMoves : playerBottomMoves; bool isPlayersTurn(ClockPlayerType playerType) => - currentPlayer == playerType || (currentPlayer == null && loser == null); + started && activeSide == playerType && loser == null; bool isPlayersMoveAllowed(ClockPlayerType playerType) => isPlayersTurn(playerType) && !paused; bool isActivePlayer(ClockPlayerType playerType) => - currentPlayer == playerType && !paused; + isPlayersTurn(playerType) && !paused; bool isLoser(ClockPlayerType playerType) => loser == playerType; } diff --git a/lib/src/view/clock/clock_settings.dart b/lib/src/view/clock/clock_settings.dart index 6101474b2e..8ea4868a6e 100644 --- a/lib/src/view/clock/clock_settings.dart +++ b/lib/src/view/clock/clock_settings.dart @@ -16,8 +16,7 @@ class ClockSettings extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final controller = ref.read(clockControllerProvider.notifier); final buttonsEnabled = ref.watch( - clockControllerProvider - .select((value) => value.paused || value.currentPlayer == null), + clockControllerProvider.select((value) => value.paused), ); return Padding( @@ -30,7 +29,7 @@ class ClockSettings extends ConsumerWidget { semanticsLabel: context.l10n.reset, iconSize: _iconSize, onTap: buttonsEnabled ? () => controller.reset() : null, - icon: Icons.cached, + icon: Icons.refresh, ), PlatformIconButton( semanticsLabel: context.l10n.settingsSettings, @@ -86,6 +85,16 @@ class _PlayResumeButton extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final controller = ref.read(clockControllerProvider.notifier); final state = ref.watch(clockControllerProvider); + + if (!state.started) { + return PlatformIconButton( + semanticsLabel: context.l10n.play, + iconSize: 35, + onTap: () => controller.start(), + icon: Icons.play_arrow, + ); + } + if (state.paused) { return PlatformIconButton( semanticsLabel: context.l10n.resume, @@ -94,10 +103,11 @@ class _PlayResumeButton extends ConsumerWidget { icon: Icons.play_arrow, ); } + return PlatformIconButton( semanticsLabel: context.l10n.pause, iconSize: 35, - onTap: state.currentPlayer != null ? () => controller.pause() : null, + onTap: () => controller.pause(), icon: Icons.pause, ); } diff --git a/lib/src/view/clock/clock_tile.dart b/lib/src/view/clock/clock_tile.dart index f1a3c41180..253bcd2377 100644 --- a/lib/src/view/clock/clock_tile.dart +++ b/lib/src/view/clock/clock_tile.dart @@ -42,10 +42,11 @@ class ClockTile extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final backgroundColor = clockState.isLoser(playerType) ? LichessColors.red - : clockState.isPlayersTurn(playerType) && - clockState.currentPlayer != null + : clockState.isPlayersTurn(playerType) ? LichessColors.brag - : Colors.grey; + : clockState.activeSide == playerType + ? Colors.grey + : Colors.grey[300]; return RotatedBox( quarterTurns: playerType == ClockPlayerType.top ? 2 : 0, @@ -58,8 +59,16 @@ class ClockTile extends ConsumerWidget { color: backgroundColor, child: InkWell( splashFactory: NoSplash.splashFactory, - onTap: clockState.isPlayersMoveAllowed(playerType) + onTap: !clockState.started ? () { + ref + .read(clockControllerProvider.notifier) + .setActiveSide(playerType); + } + : null, + onTapDown: clockState.started && + clockState.isPlayersMoveAllowed(playerType) + ? (_) { ref .read(clockControllerProvider.notifier) .onTap(playerType); From e187b9396f94c444aaef40f7849c831e9e17a576 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 6 Sep 2024 13:25:13 +0200 Subject: [PATCH 273/979] Fix clock buttons --- lib/src/view/clock/clock_settings.dart | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/src/view/clock/clock_settings.dart b/lib/src/view/clock/clock_settings.dart index 8ea4868a6e..244002c087 100644 --- a/lib/src/view/clock/clock_settings.dart +++ b/lib/src/view/clock/clock_settings.dart @@ -14,10 +14,8 @@ class ClockSettings extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final controller = ref.read(clockControllerProvider.notifier); - final buttonsEnabled = ref.watch( - clockControllerProvider.select((value) => value.paused), - ); + final state = ref.watch(clockControllerProvider); + final buttonsEnabled = !state.started || state.paused; return Padding( padding: const EdgeInsets.all(8.0), @@ -28,7 +26,11 @@ class ClockSettings extends ConsumerWidget { PlatformIconButton( semanticsLabel: context.l10n.reset, iconSize: _iconSize, - onTap: buttonsEnabled ? () => controller.reset() : null, + onTap: buttonsEnabled + ? () { + ref.read(clockControllerProvider.notifier).reset(); + } + : null, icon: Icons.refresh, ), PlatformIconButton( @@ -57,7 +59,9 @@ class ClockSettings extends ConsumerWidget { options.incrementPlayerTop.inSeconds, ), onSelected: (choice) { - controller.updateOptions(choice); + ref + .read(clockControllerProvider.notifier) + .updateOptions(choice); }, ); }, From da083153deeb1da9504d0628f8ae8ba504bd53b6 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Fri, 6 Sep 2024 12:52:21 +0200 Subject: [PATCH 274/979] fix timer by moving it in the controller --- lib/src/model/broadcast/broadcast.dart | 2 + .../broadcast/broadcast_round_controller.dart | 44 ++++++++++-- .../view/broadcast/broadcast_boards_tab.dart | 68 ++++--------------- .../view/broadcast/broadcast_game_screen.dart | 59 ++-------------- 4 files changed, 61 insertions(+), 112 deletions(-) diff --git a/lib/src/model/broadcast/broadcast.dart b/lib/src/model/broadcast/broadcast.dart index 5935f3ae44..89ae3a82cb 100644 --- a/lib/src/model/broadcast/broadcast.dart +++ b/lib/src/model/broadcast/broadcast.dart @@ -99,6 +99,8 @@ class BroadcastGame with _$BroadcastGame { /// The amount of time that the player whose turn it is has been thinking since his last move required Duration? thinkTime, }) = _BroadcastGame; + + bool get isPlaying => status == '*'; } @freezed diff --git a/lib/src/model/broadcast/broadcast_round_controller.dart b/lib/src/model/broadcast/broadcast_round_controller.dart index 2c458b69c0..f422ad9683 100644 --- a/lib/src/model/broadcast/broadcast_round_controller.dart +++ b/lib/src/model/broadcast/broadcast_round_controller.dart @@ -21,6 +21,8 @@ class BroadcastRoundController extends _$BroadcastRoundController { StreamSubscription? _subscription; + Timer? _timer; + late SocketClient _socketClient; @override @@ -33,14 +35,47 @@ class BroadcastRoundController extends _$BroadcastRoundController { ref.onDispose(() { _subscription?.cancel(); + _timer?.cancel(); }); final games = await ref.watch(broadcastRoundProvider(broadcastRoundId).future); + _timer = Timer.periodic( + const Duration(seconds: 1), + (_) => _updateClocks(), + ); + return games; } + void _updateClocks() { + state = AsyncData( + state.requireValue.map((gameId, game) { + if (!game.isPlaying) return MapEntry(gameId, game); + final playingSide = Setup.parseFen(game.fen).turn; + final clock = game.players[playingSide]!.clock; + final newClock = + (clock != null) ? clock - const Duration(seconds: 1) : null; + return MapEntry( + gameId, + game.copyWith( + players: IMap( + { + playingSide: game.players[playingSide]!.copyWith( + clock: (newClock?.isNegative ?? false) + ? Duration.zero + : newClock, + ), + playingSide.opposite: game.players[playingSide.opposite]!, + }, + ), + ), + ); + }), + ); + } + Future setPgn(BroadcastGameId gameId) async { final pgn = await ref.watch( broadcastGameProvider( @@ -90,7 +125,7 @@ class BroadcastRoundController extends _$BroadcastRoundController { final fen = pick(event.data, 'n', 'fen').asStringOrThrow(); - final playingSide = Setup.parseFen(fen).turn.opposite; + final playingSide = Setup.parseFen(fen).turn; state = AsyncData( state.requireValue.update( @@ -98,12 +133,12 @@ class BroadcastRoundController extends _$BroadcastRoundController { (broadcastGame) => broadcastGame.copyWith( players: IMap( { - playingSide: broadcastGame.players[playingSide]!.copyWith( + playingSide: broadcastGame.players[playingSide]!, + playingSide.opposite: + broadcastGame.players[playingSide.opposite]!.copyWith( clock: pick(event.data, 'n', 'clock') .asDurationFromCentiSecondsOrNull(), ), - playingSide.opposite: - broadcastGame.players[playingSide.opposite]!, }, ), fen: fen, @@ -171,7 +206,6 @@ class BroadcastRoundController extends _$BroadcastRoundController { } } - // void _handleAddNodeEvent(SocketEvent event) { // final gameId = // pick(event.data, 'p', 'chapterId').asBroadcastGameIdOrThrow(); diff --git a/lib/src/view/broadcast/broadcast_boards_tab.dart b/lib/src/view/broadcast/broadcast_boards_tab.dart index b164790a10..ec8e1332e0 100644 --- a/lib/src/view/broadcast/broadcast_boards_tab.dart +++ b/lib/src/view/broadcast/broadcast_boards_tab.dart @@ -1,5 +1,3 @@ -import 'dart:async'; - import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/material.dart'; @@ -293,11 +291,21 @@ class _PlayerWidget extends StatelessWidget { ) else if (player.clock != null) if (side == playingSide) - _Clock( - clock: player.clock! - (thinkTime ?? Duration.zero), + Text( + (player.clock! - (thinkTime ?? Duration.zero)) + .toHoursMinutesSeconds(), + style: TextStyle( + color: Colors.orange[900], + fontFeatures: const [FontFeature.tabularFigures()], + ), ) else - Text(player.clock!.toHoursMinutesSeconds()), + Text( + player.clock!.toHoursMinutesSeconds(), + style: const TextStyle( + fontFeatures: [FontFeature.tabularFigures()], + ), + ), ], ), ), @@ -305,53 +313,3 @@ class _PlayerWidget extends StatelessWidget { ); } } - -class _Clock extends StatefulWidget { - const _Clock({required this.clock}); - - final Duration clock; - - @override - _ClockState createState() => _ClockState(); -} - -class _ClockState extends State<_Clock> { - Timer? _timer; - late Duration _clock; - - @override - void initState() { - super.initState(); - _clock = widget.clock; - if (_clock.inSeconds <= 0) { - _clock = Duration.zero; - return; - } - _timer = Timer.periodic(const Duration(seconds: 1), (timer) { - setState(() { - _clock = _clock - const Duration(seconds: 1); - }); - if (_clock.inSeconds == 0) { - timer.cancel(); - return; - } - }); - } - - @override - void dispose() { - _timer?.cancel(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Text( - _clock.toHoursMinutesSeconds(), - style: TextStyle( - color: Colors.orange[900], - fontFeatures: const [FontFeature.tabularFigures()], - ), - ); - } -} diff --git a/lib/src/view/broadcast/broadcast_game_screen.dart b/lib/src/view/broadcast/broadcast_game_screen.dart index f5e3ee5355..52bff39c9f 100644 --- a/lib/src/view/broadcast/broadcast_game_screen.dart +++ b/lib/src/view/broadcast/broadcast_game_screen.dart @@ -456,8 +456,13 @@ class _PlayerWidget extends StatelessWidget { ), if (player.clock != null) (pov == playingSide) - ? _Clock( - clock: player.clock! - (thinkTime ?? Duration.zero), + ? Text( + (player.clock! - (thinkTime ?? Duration.zero)) + .toHoursMinutesSeconds(), + style: TextStyle( + color: Colors.orange[900], + fontFeatures: const [FontFeature.tabularFigures()], + ), ) : Text( player.clock!.toHoursMinutesSeconds(), @@ -473,56 +478,6 @@ class _PlayerWidget extends StatelessWidget { } } -class _Clock extends StatefulWidget { - const _Clock({required this.clock}); - - final Duration clock; - - @override - _ClockState createState() => _ClockState(); -} - -class _ClockState extends State<_Clock> { - Timer? _timer; - late Duration _clock; - - @override - void initState() { - super.initState(); - _clock = widget.clock; - if (_clock.inSeconds <= 0) { - _clock = Duration.zero; - return; - } - _timer = Timer.periodic(const Duration(seconds: 1), (timer) { - setState(() { - _clock = _clock - const Duration(seconds: 1); - }); - if (_clock.inSeconds == 0) { - timer.cancel(); - return; - } - }); - } - - @override - void dispose() { - _timer?.cancel(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Text( - _clock.toHoursMinutesSeconds(), - style: TextStyle( - color: Colors.orange[900], - fontFeatures: const [FontFeature.tabularFigures()], - ), - ); - } -} - class _EngineGaugeVertical extends ConsumerWidget { const _EngineGaugeVertical(this.ctrlProvider); From 81c8984fc83e6e48a269310d72ce231c1c59c036 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 6 Sep 2024 15:29:22 +0200 Subject: [PATCH 275/979] Use scheduleMicrotask to execute premoves --- lib/src/model/game/game_controller.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/model/game/game_controller.dart b/lib/src/model/game/game_controller.dart index 0c193c361e..7248b0462b 100644 --- a/lib/src/model/game/game_controller.dart +++ b/lib/src/model/game/game_controller.dart @@ -626,7 +626,7 @@ class GameController extends _$GameController { if (!curState.isReplaying && playedSide == curState.game.youAre?.opposite && curState.premove != null) { - Timer.run(() { + scheduleMicrotask(() { final postMovePremove = state.valueOrNull?.premove; final postMovePosition = state.valueOrNull?.game.lastPosition; if (postMovePremove != null && From 70d4adb4221305a32241abb6be3e3501bf1a23d4 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 6 Sep 2024 15:36:19 +0200 Subject: [PATCH 276/979] Update chessground Closes #982 --- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 19ba4d30ed..8242329ca1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -194,10 +194,10 @@ packages: dependency: "direct main" description: name: chessground - sha256: ac2bfe069b6ad96d7c1299d6901a4c6aeb15223fdcc3dd5c4e8073d2a9baa5e9 + sha256: "3267db8b04e0857761c40eab5895c556a885263388386f0f26b85d61401a1e05" url: "https://pub.dev" source: hosted - version: "5.1.0" + version: "5.1.1" ci: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 7ddb885312..d437ac658e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ dependencies: app_settings: ^5.1.1 async: ^2.10.0 cached_network_image: ^3.2.2 - chessground: ^5.1.0 + chessground: ^5.1.1 collection: ^1.17.0 connectivity_plus: ^6.0.2 cronet_http: ^1.3.1 From 609cc71371f507112314efa70c33f4aa69914ab4 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 6 Sep 2024 16:21:50 +0200 Subject: [PATCH 277/979] Fix game history screen overflow --- lib/src/view/user/game_history_screen.dart | 115 +++++++++++---------- 1 file changed, 59 insertions(+), 56 deletions(-) diff --git a/lib/src/view/user/game_history_screen.dart b/lib/src/view/user/game_history_screen.dart index a84fc56bd0..3b3c79f269 100644 --- a/lib/src/view/user/game_history_screen.dart +++ b/lib/src/view/user/game_history_screen.dart @@ -53,6 +53,7 @@ class GameHistoryScreen extends ConsumerWidget { semanticsLabel: context.l10n.filterGames, onPressed: () => showAdaptiveBottomSheet( context: context, + isScrollControlled: true, builder: (_) => _FilterGames( filter: ref.read(gameFilterProvider(filter: gameFilter)), user: user, @@ -250,75 +251,48 @@ class _FilterGamesState extends ConsumerState<_FilterGames> { filter = widget.filter; } + static const gamePerfs = [ + Perf.ultraBullet, + Perf.bullet, + Perf.blitz, + Perf.rapid, + Perf.classical, + Perf.correspondence, + Perf.chess960, + Perf.antichess, + Perf.kingOfTheHill, + Perf.threeCheck, + Perf.atomic, + Perf.horde, + Perf.racingKings, + Perf.crazyhouse, + ]; + static const filterGroupSpace = SizedBox(height: 10.0); + @override Widget build(BuildContext context) { - const gamePerfs = [ - Perf.ultraBullet, - Perf.bullet, - Perf.blitz, - Perf.rapid, - Perf.classical, - Perf.correspondence, - Perf.chess960, - Perf.antichess, - Perf.kingOfTheHill, - Perf.threeCheck, - Perf.atomic, - Perf.horde, - Perf.racingKings, - Perf.crazyhouse, - ]; - const filterGroupSpace = SizedBox(height: 10.0); - final session = ref.read(authSessionProvider); final userId = widget.user?.id ?? session?.user.id; - List availablePerfs(User user) { - final perfs = gamePerfs.where((perf) { - final p = user.perfs[perf]; - return p != null && p.numberOfGamesOrRuns > 0; - }).toList(growable: false); - perfs.sort( - (p1, p2) => user.perfs[p2]!.numberOfGamesOrRuns - .compareTo(user.perfs[p1]!.numberOfGamesOrRuns), - ); - return perfs; - } - - Widget perfFilter(List choices) => _Filter( - filterName: context.l10n.variant, - filterType: FilterType.multipleChoice, - choices: choices, - choiceSelected: (choice) => filter.perfs.contains(choice), - choiceLabel: (t) => t.shortTitle, - onSelected: (value, selected) => setState( - () { - filter = filter.copyWith( - perfs: selected - ? filter.perfs.add(value) - : filter.perfs.remove(value), - ); - }, - ), - ); + final Widget filters = userId != null + ? ref.watch(userProvider(id: userId)).when( + data: (user) => perfFilter(availablePerfs(user)), + loading: () => const Center( + child: CircularProgressIndicator.adaptive(), + ), + error: (_, __) => perfFilter(gamePerfs), + ) + : perfFilter(gamePerfs); return SafeArea( child: Padding( padding: const EdgeInsets.all(16.0), child: Column( mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, children: [ + filters, const SizedBox(height: 12.0), - if (userId != null) - ref.watch(userProvider(id: userId)).when( - data: (user) => perfFilter(availablePerfs(user)), - loading: () => const Center( - child: CircularProgressIndicator.adaptive(), - ), - error: (_, __) => perfFilter(gamePerfs), - ) - else - perfFilter(gamePerfs), const PlatformDivider(thickness: 1, indent: 0), filterGroupSpace, _Filter( @@ -356,6 +330,35 @@ class _FilterGamesState extends ConsumerState<_FilterGames> { ), ); } + + List availablePerfs(User user) { + final perfs = gamePerfs.where((perf) { + final p = user.perfs[perf]; + return p != null && p.numberOfGamesOrRuns > 0; + }).toList(growable: false); + perfs.sort( + (p1, p2) => user.perfs[p2]!.numberOfGamesOrRuns + .compareTo(user.perfs[p1]!.numberOfGamesOrRuns), + ); + return perfs; + } + + Widget perfFilter(List choices) => _Filter( + filterName: context.l10n.variant, + filterType: FilterType.multipleChoice, + choices: choices, + choiceSelected: (choice) => filter.perfs.contains(choice), + choiceLabel: (t) => t.shortTitle, + onSelected: (value, selected) => setState( + () { + filter = filter.copyWith( + perfs: selected + ? filter.perfs.add(value) + : filter.perfs.remove(value), + ); + }, + ), + ); } enum FilterType { From f400ec5d376aaaaf029e40ed65b8aec06671be75 Mon Sep 17 00:00:00 2001 From: Mauritz Date: Fri, 6 Sep 2024 17:38:18 +0200 Subject: [PATCH 278/979] feat: link opening name to lichess page instead of wikibooks in explorer --- .../model/analysis/analysis_controller.dart | 19 --------- .../opening_explorer_repository.dart | 10 ----- .../opening_explorer_screen.dart | 40 ++++++------------- .../opening_explorer_screen_test.dart | 3 -- 4 files changed, 12 insertions(+), 60 deletions(-) diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index 5ac4a7da1f..93ddc71ec7 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -145,7 +145,6 @@ class AnalysisController extends _$AnalysisController { lastMove: lastMove, pov: options.orientation, contextOpening: options.opening, - wikiBooksUrl: _wikiBooksUrl(currentPath), isLocalEvaluationAllowed: options.isLocalEvaluationAllowed, isLocalEvaluationEnabled: prefs.enableLocalEvaluation, displayMode: DisplayMode.moves, @@ -433,7 +432,6 @@ class AnalysisController extends _$AnalysisController { isOnMainline: _root.isOnMainline(path), currentNode: AnalysisCurrentNode.fromNode(currentNode), currentBranchOpening: opening, - wikiBooksUrl: _wikiBooksUrl(path), lastMove: currentNode.sanMove.move, promotionMove: null, root: rootView, @@ -444,7 +442,6 @@ class AnalysisController extends _$AnalysisController { isOnMainline: _root.isOnMainline(path), currentNode: AnalysisCurrentNode.fromNode(currentNode), currentBranchOpening: opening, - wikiBooksUrl: _wikiBooksUrl(path), lastMove: null, promotionMove: null, root: rootView, @@ -477,19 +474,6 @@ class AnalysisController extends _$AnalysisController { } } - String? _wikiBooksUrl(UciPath path) { - if (_root.position.ply > 30) { - return null; - } - final nodes = _root.branchesOn(path); - final moves = nodes.map((node) { - final move = node.view.sanMove.san; - final moveNr = (node.position.ply / 2).ceil(); - return node.position.ply.isOdd ? '$moveNr._$move' : '$moveNr...$move'; - }); - return 'https://en.wikibooks.org/wiki/Chess_Opening_Theory/${moves.join("/")}'; - } - void _startEngineEval() { if (!state.isEngineAvailable) return; ref @@ -680,9 +664,6 @@ class AnalysisState with _$AnalysisState { /// The opening of the current branch. Opening? currentBranchOpening, - /// wikibooks.org opening theory page for the current path - String? wikiBooksUrl, - /// Optional server analysis to display player stats. ({PlayerAnalysis white, PlayerAnalysis black})? playersAnalysis, diff --git a/lib/src/model/opening_explorer/opening_explorer_repository.dart b/lib/src/model/opening_explorer/opening_explorer_repository.dart index 8a3d29651d..2a3cee4ab9 100644 --- a/lib/src/model/opening_explorer/opening_explorer_repository.dart +++ b/lib/src/model/opening_explorer/opening_explorer_repository.dart @@ -143,13 +143,3 @@ class OpeningExplorerRepository { ); } } - -@riverpod -Future wikiBooksPageExists( - WikiBooksPageExistsRef ref, { - required String url, -}) async { - final client = ref.read(defaultClientProvider); - final response = await client.get(Uri.parse(url)); - return response.statusCode == 200; -} diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index 7d720ac90a..4a07fe7adf 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -248,7 +248,6 @@ class _OpeningExplorerState extends ConsumerState<_OpeningExplorer> { pgn: widget.pgn, options: widget.options, opening: opening, - wikiBooksUrl: analysisState.wikiBooksUrl, explorerContent: lastExplorerWidgets ?? [ Shimmer( @@ -270,7 +269,6 @@ class _OpeningExplorerState extends ConsumerState<_OpeningExplorer> { options: widget.options, opening: opening, openingExplorer: openingExplorer, - wikiBooksUrl: analysisState.wikiBooksUrl, explorerContent: [ Center( child: Padding( @@ -345,7 +343,6 @@ class _OpeningExplorerState extends ConsumerState<_OpeningExplorer> { options: widget.options, opening: opening, openingExplorer: openingExplorer, - wikiBooksUrl: analysisState.wikiBooksUrl, explorerContent: explorerContent, ); }, @@ -353,7 +350,6 @@ class _OpeningExplorerState extends ConsumerState<_OpeningExplorer> { pgn: widget.pgn, options: widget.options, opening: opening, - wikiBooksUrl: analysisState.wikiBooksUrl, explorerContent: lastExplorerWidgets ?? [ Shimmer( @@ -386,7 +382,6 @@ class _OpeningExplorerView extends StatelessWidget { required this.options, required this.opening, required this.openingExplorer, - required this.wikiBooksUrl, required this.explorerContent, }) : loading = false; @@ -394,7 +389,6 @@ class _OpeningExplorerView extends StatelessWidget { required this.pgn, required this.options, required this.opening, - required this.wikiBooksUrl, required this.explorerContent, }) : loading = true, openingExplorer = null; @@ -403,7 +397,6 @@ class _OpeningExplorerView extends StatelessWidget { final AnalysisOptions options; final Opening? opening; final ({OpeningExplorerEntry entry, bool isIndexing})? openingExplorer; - final String? wikiBooksUrl; final List explorerContent; final bool loading; @@ -435,7 +428,6 @@ class _OpeningExplorerView extends StatelessWidget { flex: 75, child: _Opening( opening: opening!, - wikiBooksUrl: wikiBooksUrl, ), ), if (openingExplorer?.isIndexing == true) @@ -472,33 +464,25 @@ class _OpeningExplorerView extends StatelessWidget { class _Opening extends ConsumerWidget { const _Opening({ required this.opening, - required this.wikiBooksUrl, }); final Opening opening; - final String? wikiBooksUrl; @override Widget build(BuildContext context, WidgetRef ref) { - final openingWidget = Text( - '${opening.eco.isEmpty ? "" : "${opening.eco} "}${opening.name}', - style: TextStyle( - color: Theme.of(context).colorScheme.onSecondaryContainer, - fontWeight: FontWeight.bold, + return GestureDetector( + onTap: opening.name == context.l10n.startPosition + ? null + : () => launchUrl( + Uri.parse('https://lichess.org/opening/${opening.name}'), + ), + child: Text( + '${opening.eco.isEmpty ? "" : "${opening.eco} "}${opening.name}', + style: TextStyle( + color: Theme.of(context).colorScheme.onSecondaryContainer, + fontWeight: FontWeight.bold, + ), ), ); - - return wikiBooksUrl == null - ? openingWidget - : ref.watch(wikiBooksPageExistsProvider(url: wikiBooksUrl!)).when( - data: (wikiBooksPageExists) => wikiBooksPageExists - ? GestureDetector( - onTap: () => launchUrl(Uri.parse(wikiBooksUrl!)), - child: openingWidget, - ) - : openingWidget, - loading: () => openingWidget, - error: (e, s) => openingWidget, - ); } } diff --git a/test/view/opening_explorer/opening_explorer_screen_test.dart b/test/view/opening_explorer/opening_explorer_screen_test.dart index 3098eddcd3..73641be9e7 100644 --- a/test/view/opening_explorer/opening_explorer_screen_test.dart +++ b/test/view/opening_explorer/opening_explorer_screen_test.dart @@ -28,9 +28,6 @@ void main() { return mockResponse(playerOpeningExplorerResponse, 200); } } - if (request.url.host == 'en.wikibooks.org') { - return mockResponse('', 200); - } return mockResponse('', 404); }); From 5adb49a5508219ec1337b17aed878b54022d4f0c Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Fri, 6 Sep 2024 18:04:51 +0200 Subject: [PATCH 279/979] rename broadcast game screeen to broadcast analysis screen --- ...adcast_game_screen.dart => broadcast_analysis_screen.dart} | 4 ++-- lib/src/view/broadcast/broadcast_boards_tab.dart | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) rename lib/src/view/broadcast/{broadcast_game_screen.dart => broadcast_analysis_screen.dart} (99%) diff --git a/lib/src/view/broadcast/broadcast_game_screen.dart b/lib/src/view/broadcast/broadcast_analysis_screen.dart similarity index 99% rename from lib/src/view/broadcast/broadcast_game_screen.dart rename to lib/src/view/broadcast/broadcast_analysis_screen.dart index 9235e7673f..1433bee92a 100644 --- a/lib/src/view/broadcast/broadcast_game_screen.dart +++ b/lib/src/view/broadcast/broadcast_analysis_screen.dart @@ -56,12 +56,12 @@ const _options = AnalysisOptions( variant: Variant.standard, ); -class BroadcastGameScreen extends ConsumerWidget { +class BroadcastAnalysisScreen extends ConsumerWidget { final BroadcastRoundId roundId; final BroadcastGameId gameId; final String title; - const BroadcastGameScreen({ + const BroadcastAnalysisScreen({ required this.roundId, required this.gameId, required this.title, diff --git a/lib/src/view/broadcast/broadcast_boards_tab.dart b/lib/src/view/broadcast/broadcast_boards_tab.dart index ec8e1332e0..7bfeedb70f 100644 --- a/lib/src/view/broadcast/broadcast_boards_tab.dart +++ b/lib/src/view/broadcast/broadcast_boards_tab.dart @@ -15,7 +15,7 @@ import 'package:lichess_mobile/src/utils/duration.dart'; import 'package:lichess_mobile/src/utils/lichess_assets.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; -import 'package:lichess_mobile/src/view/broadcast/broadcast_game_screen.dart'; +import 'package:lichess_mobile/src/view/broadcast/broadcast_analysis_screen.dart'; import 'package:lichess_mobile/src/widgets/board_thumbnail.dart'; import 'package:lichess_mobile/src/widgets/evaluation_bar.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; @@ -142,7 +142,7 @@ class BroadcastPreview extends ConsumerWidget { .setPgn(game.id); pushPlatformRoute( context, - builder: (context) => BroadcastGameScreen( + builder: (context) => BroadcastAnalysisScreen( roundId: roundId, gameId: game.id, title: title, From f9ff0c463724622d23c510bb174a859022c2e593 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Fri, 6 Sep 2024 22:27:07 +0200 Subject: [PATCH 280/979] refactor player widget and fix negative clocks --- lib/src/model/broadcast/broadcast.dart | 11 ++- .../model/broadcast/broadcast_repository.dart | 3 +- .../broadcast/broadcast_round_controller.dart | 24 +----- .../broadcast/broadcast_analysis_screen.dart | 56 ++++++------- .../view/broadcast/broadcast_boards_tab.dart | 84 ++++++++----------- 5 files changed, 77 insertions(+), 101 deletions(-) diff --git a/lib/src/model/broadcast/broadcast.dart b/lib/src/model/broadcast/broadcast.dart index 89ae3a82cb..c6f27e22bd 100644 --- a/lib/src/model/broadcast/broadcast.dart +++ b/lib/src/model/broadcast/broadcast.dart @@ -97,10 +97,19 @@ class BroadcastGame with _$BroadcastGame { required String? pgn, /// The amount of time that the player whose turn it is has been thinking since his last move - required Duration? thinkTime, + required Duration thinkTime, }) = _BroadcastGame; bool get isPlaying => status == '*'; + Side get playingSide => Setup.parseFen(fen).turn; + Duration? get timeLeft { + final clock = players[playingSide]!.clock; + if (clock == null) return null; + final timeLeftMaybeNegative = clock - thinkTime; + return timeLeftMaybeNegative.isNegative + ? Duration.zero + : timeLeftMaybeNegative; + } } @freezed diff --git a/lib/src/model/broadcast/broadcast_repository.dart b/lib/src/model/broadcast/broadcast_repository.dart index c870ed1071..fe2b13d345 100644 --- a/lib/src/model/broadcast/broadcast_repository.dart +++ b/lib/src/model/broadcast/broadcast_repository.dart @@ -163,7 +163,8 @@ MapEntry gameFromPick( Variant.standard.initialPosition.fen, lastMove: pick('lastMove').asUciMoveOrNull(), status: pick('status').asStringOrNull(), - thinkTime: pick('thinkTime').asDurationFromSecondsOrNull(), + thinkTime: + pick('thinkTime').asDurationFromSecondsOrNull() ?? Duration.zero, pgn: null, ), ); diff --git a/lib/src/model/broadcast/broadcast_round_controller.dart b/lib/src/model/broadcast/broadcast_round_controller.dart index f422ad9683..eadf69c63e 100644 --- a/lib/src/model/broadcast/broadcast_round_controller.dart +++ b/lib/src/model/broadcast/broadcast_round_controller.dart @@ -53,25 +53,9 @@ class BroadcastRoundController extends _$BroadcastRoundController { state = AsyncData( state.requireValue.map((gameId, game) { if (!game.isPlaying) return MapEntry(gameId, game); - final playingSide = Setup.parseFen(game.fen).turn; - final clock = game.players[playingSide]!.clock; - final newClock = - (clock != null) ? clock - const Duration(seconds: 1) : null; - return MapEntry( - gameId, - game.copyWith( - players: IMap( - { - playingSide: game.players[playingSide]!.copyWith( - clock: (newClock?.isNegative ?? false) - ? Duration.zero - : newClock, - ), - playingSide.opposite: game.players[playingSide.opposite]!, - }, - ), - ), - ); + final thinkTime = game.thinkTime; + final newThinkTime = thinkTime + const Duration(seconds: 1); + return MapEntry(gameId, game.copyWith(thinkTime: newThinkTime)); }), ); } @@ -143,7 +127,7 @@ class BroadcastRoundController extends _$BroadcastRoundController { ), fen: fen, lastMove: pick(event.data, 'n', 'uci').asUciMoveOrThrow(), - thinkTime: null, + thinkTime: Duration.zero, ), ), ); diff --git a/lib/src/view/broadcast/broadcast_analysis_screen.dart b/lib/src/view/broadcast/broadcast_analysis_screen.dart index 1433bee92a..e87b3cac44 100644 --- a/lib/src/view/broadcast/broadcast_analysis_screen.dart +++ b/lib/src/view/broadcast/broadcast_analysis_screen.dart @@ -335,27 +335,23 @@ class _AnalysisBoardPlayersAndClocks extends StatelessWidget { children: [ _PlayerWidget( width: boardSize, - player: game.players[pov.opposite]!, - gameStatus: game.status, - thinkTime: game.thinkTime, - pov: pov.opposite, + game: game, + side: pov.opposite, playingSide: playingSide, - side: _PlayerWidgetSide.top, + boardSide: _PlayerWidgetSide.top, ), AnalysisBoard( game.pgn!, - _options, + broadcastAnalysisOptions, boardSize, isTablet: isTablet, ), _PlayerWidget( width: boardSize, - player: game.players[pov]!, - gameStatus: game.status, - thinkTime: game.thinkTime, - pov: pov, + game: game, + side: pov, playingSide: playingSide, - side: _PlayerWidgetSide.bottom, + boardSide: _PlayerWidgetSide.bottom, ), ], ); @@ -365,36 +361,37 @@ class _AnalysisBoardPlayersAndClocks extends StatelessWidget { class _PlayerWidget extends StatelessWidget { const _PlayerWidget({ required this.width, - required this.player, - required this.gameStatus, - required this.thinkTime, - required this.pov, - required this.playingSide, + required this.game, required this.side, + required this.playingSide, + required this.boardSide, }); - final BroadcastPlayer player; - final String? gameStatus; - final Duration? thinkTime; - final Side pov; + final BroadcastGame game; + final Side side; final Side playingSide; final double width; - final _PlayerWidgetSide side; + final _PlayerWidgetSide boardSide; @override Widget build(BuildContext context) { + final player = game.players[side]!; + final gameStatus = game.status; + return SizedBox( width: width, child: Card( margin: EdgeInsets.zero, shape: RoundedRectangleBorder( borderRadius: BorderRadius.only( - topLeft: Radius.circular(side == _PlayerWidgetSide.top ? 8 : 0), - topRight: Radius.circular(side == _PlayerWidgetSide.top ? 8 : 0), + topLeft: + Radius.circular(boardSide == _PlayerWidgetSide.top ? 8 : 0), + topRight: + Radius.circular(boardSide == _PlayerWidgetSide.top ? 8 : 0), bottomLeft: - Radius.circular(side == _PlayerWidgetSide.bottom ? 8 : 0), + Radius.circular(boardSide == _PlayerWidgetSide.bottom ? 8 : 0), bottomRight: - Radius.circular(side == _PlayerWidgetSide.bottom ? 8 : 0), + Radius.circular(boardSide == _PlayerWidgetSide.bottom ? 8 : 0), ), ), child: Padding( @@ -410,10 +407,10 @@ class _PlayerWidget extends StatelessWidget { (gameStatus == '½-½') ? '½' : (gameStatus == '1-0') - ? pov == Side.white + ? side == Side.white ? '1' : '0' - : pov == Side.black + : side == Side.black ? '1' : '0', style: const TextStyle() @@ -455,10 +452,9 @@ class _PlayerWidget extends StatelessWidget { ), ), if (player.clock != null) - (pov == playingSide) + (side == playingSide) ? Text( - (player.clock! - (thinkTime ?? Duration.zero)) - .toHoursMinutesSeconds(), + game.timeLeft!.toHoursMinutesSeconds(), style: TextStyle( color: Colors.orange[900], fontFeatures: const [FontFeature.tabularFigures()], diff --git a/lib/src/view/broadcast/broadcast_boards_tab.dart b/lib/src/view/broadcast/broadcast_boards_tab.dart index 7bfeedb70f..e354e6bde8 100644 --- a/lib/src/view/broadcast/broadcast_boards_tab.dart +++ b/lib/src/view/broadcast/broadcast_boards_tab.dart @@ -126,8 +126,8 @@ class BroadcastPreview extends ConsumerWidget { if (games == null) { return BoardThumbnail.loading( size: boardSize, - header: _PlayerWidget.loading(width: boardWithMaybeEvalBarWidth), - footer: _PlayerWidget.loading(width: boardWithMaybeEvalBarWidth), + header: _PlayerWidgetLoading(width: boardWithMaybeEvalBarWidth), + footer: _PlayerWidgetLoading(width: boardWithMaybeEvalBarWidth), showEvaluationBar: showEvaluationBar, ); } @@ -156,17 +156,13 @@ class BroadcastPreview extends ConsumerWidget { size: boardSize, header: _PlayerWidget( width: boardWithMaybeEvalBarWidth, - player: game.players[Side.black]!, - gameStatus: game.status, - thinkTime: game.thinkTime, + game: game, side: Side.black, playingSide: playingSide, ), footer: _PlayerWidget( width: boardWithMaybeEvalBarWidth, - player: game.players[Side.white]!, - gameStatus: game.status, - thinkTime: game.thinkTime, + game: game, side: Side.white, playingSide: playingSide, ), @@ -176,57 +172,48 @@ class BroadcastPreview extends ConsumerWidget { } } +class _PlayerWidgetLoading extends StatelessWidget { + const _PlayerWidgetLoading({ + required this.width, + }); + + final double width; + + @override + Widget build(BuildContext context) { + return SizedBox( + width: width, + child: Padding( + padding: _kPlayerWidgetPadding, + child: Container( + height: _kPlayerWidgetTextStyle.fontSize, + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(5), + ), + ), + ), + ); + } +} + class _PlayerWidget extends StatelessWidget { const _PlayerWidget({ required this.width, - required this.player, - required this.gameStatus, - required this.thinkTime, + required this.game, required this.side, required this.playingSide, - }) : _displayShimmerPlaceholder = false; - - const _PlayerWidget.loading({ - required this.width, - }) : player = const BroadcastPlayer( - name: '', - title: null, - rating: null, - clock: null, - federation: null, - ), - gameStatus = null, - thinkTime = null, - side = Side.white, - playingSide = Side.white, - _displayShimmerPlaceholder = true; + }); - final BroadcastPlayer player; - final String? gameStatus; - final Duration? thinkTime; + final BroadcastGame game; final Side side; final Side playingSide; final double width; - final bool _displayShimmerPlaceholder; - @override Widget build(BuildContext context) { - if (_displayShimmerPlaceholder) { - return SizedBox( - width: width, - child: Padding( - padding: _kPlayerWidgetPadding, - child: Container( - height: _kPlayerWidgetTextStyle.fontSize, - decoration: BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(5), - ), - ), - ), - ); - } + final player = game.players[side]!; + final gameStatus = game.status; return SizedBox( width: width, @@ -292,8 +279,7 @@ class _PlayerWidget extends StatelessWidget { else if (player.clock != null) if (side == playingSide) Text( - (player.clock! - (thinkTime ?? Duration.zero)) - .toHoursMinutesSeconds(), + (game.timeLeft!).toHoursMinutesSeconds(), style: TextStyle( color: Colors.orange[900], fontFeatures: const [FontFeature.tabularFigures()], From 488d520a34f1692d33ee0d05ce402aa19cecd2f2 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Fri, 6 Sep 2024 22:41:38 +0200 Subject: [PATCH 281/979] fix text overflow and rating null case --- lib/src/view/broadcast/broadcast_analysis_screen.dart | 11 +++++++++-- lib/src/view/broadcast/broadcasts_list_screen.dart | 2 ++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_analysis_screen.dart b/lib/src/view/broadcast/broadcast_analysis_screen.dart index e87b3cac44..7faca8a5cb 100644 --- a/lib/src/view/broadcast/broadcast_analysis_screen.dart +++ b/lib/src/view/broadcast/broadcast_analysis_screen.dart @@ -445,9 +445,16 @@ class _PlayerWidget extends StatelessWidget { style: const TextStyle().copyWith( fontWeight: FontWeight.bold, ), + overflow: TextOverflow.ellipsis, ), - const SizedBox(width: 5), - Text(player.rating.toString(), style: const TextStyle()), + if (player.rating != null) ...[ + const SizedBox(width: 5), + Text( + player.rating.toString(), + style: const TextStyle(), + overflow: TextOverflow.ellipsis, + ), + ], ], ), ), diff --git a/lib/src/view/broadcast/broadcasts_list_screen.dart b/lib/src/view/broadcast/broadcasts_list_screen.dart index 99437b29c6..d3ee9bd530 100644 --- a/lib/src/view/broadcast/broadcasts_list_screen.dart +++ b/lib/src/view/broadcast/broadcasts_list_screen.dart @@ -255,6 +255,7 @@ class BroadcastGridItem extends StatelessWidget { ?.copyWith( color: textShade(context, 0.5), ), + overflow: TextOverflow.ellipsis, ), const SizedBox(width: 4.0), ], @@ -266,6 +267,7 @@ class BroadcastGridItem extends StatelessWidget { fontWeight: FontWeight.bold, color: Colors.red, ), + overflow: TextOverflow.ellipsis, ) else if (broadcast.round.startsAt != null) StartsRoundDate( From d5192d17cde03cecaa7803cc701788cc02e23ecb Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Sat, 7 Sep 2024 11:23:35 +0200 Subject: [PATCH 282/979] Fix loading games from explorer Fixes #984 --- lib/src/view/game/archived_game_screen.dart | 195 ++++++++++++---- .../opening_explorer_screen.dart | 220 ++++++++---------- lib/src/widgets/board_table.dart | 22 ++ lib/src/widgets/platform_scaffold.dart | 2 +- test/view/game/archived_game_screen_test.dart | 35 ++- 5 files changed, 312 insertions(+), 162 deletions(-) diff --git a/lib/src/view/game/archived_game_screen.dart b/lib/src/view/game/archived_game_screen.dart index abb311d733..40f7317516 100644 --- a/lib/src/view/game/archived_game_screen.dart +++ b/lib/src/view/game/archived_game_screen.dart @@ -3,7 +3,10 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; +import 'package:lichess_mobile/src/model/common/http.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/game/archived_game.dart'; +import 'package:lichess_mobile/src/model/game/game_repository_providers.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; @@ -24,22 +27,112 @@ import 'archived_game_screen_providers.dart'; /// Screen for viewing an archived game. class ArchivedGameScreen extends ConsumerWidget { const ArchivedGameScreen({ - required this.gameData, + this.gameId, + this.gameData, required this.orientation, this.initialCursor, super.key, + }) : assert(gameId != null || gameData != null); + + final LightArchivedGame? gameData; + final GameId? gameId; + + final Side orientation; + final int? initialCursor; + + @override + Widget build(BuildContext context, WidgetRef ref) { + if (gameData != null) { + return _Body( + gameData: gameData, + orientation: orientation, + initialCursor: initialCursor, + ); + } else { + return _LoadGame( + gameId: gameId!, + orientation: orientation, + initialCursor: initialCursor, + ); + } + } +} + +class _LoadGame extends ConsumerWidget { + const _LoadGame({ + required this.gameId, + required this.orientation, + required this.initialCursor, }); - final LightArchivedGame gameData; + final GameId gameId; final Side orientation; final int? initialCursor; @override Widget build(BuildContext context, WidgetRef ref) { + final game = ref.watch(archivedGameProvider(id: gameId)); + return game.when( + data: (game) { + return _Body( + gameData: game.data, + orientation: orientation, + initialCursor: initialCursor, + ); + }, + loading: () => _Body( + gameData: null, + orientation: orientation, + initialCursor: initialCursor, + ), + error: (error, stackTrace) { + debugPrint( + 'SEVERE: [ArchivedGameScreen] could not load game; $error\n$stackTrace', + ); + switch (error) { + case ServerException _ when error.statusCode == 404: + return _Body( + gameData: null, + orientation: orientation, + initialCursor: initialCursor, + error: 'Game not found.', + ); + default: + return _Body( + gameData: null, + orientation: orientation, + initialCursor: initialCursor, + error: error, + ); + } + }, + ); + } +} + +class _Body extends StatelessWidget { + const _Body({ + required this.gameData, + required this.orientation, + this.initialCursor, + this.error, + }); + + final LightArchivedGame? gameData; + final Object? error; + final Side orientation; + final int? initialCursor; + + @override + Widget build(BuildContext context) { return PlatformScaffold( appBar: PlatformAppBar( - title: _GameTitle(gameData: gameData), + title: gameData != null + ? _GameTitle(gameData: gameData!) + : const SizedBox.shrink(), actions: [ + if (gameData == null && error == null) + const PlatformAppBarLoadingIndicator(), ToggleSoundButton(), ], ), @@ -49,12 +142,13 @@ class ArchivedGameScreen extends ConsumerWidget { children: [ Expanded( child: _BoardBody( - gameData: gameData, + archivedGameData: gameData, orientation: orientation, initialCursor: initialCursor, + error: error, ), ), - _BottomBar(gameData: gameData, orientation: orientation), + _BottomBar(archivedGameData: gameData, orientation: orientation), ], ), ), @@ -89,17 +183,28 @@ class _GameTitle extends StatelessWidget { class _BoardBody extends ConsumerWidget { const _BoardBody({ - required this.gameData, + required this.archivedGameData, required this.orientation, + this.error, this.initialCursor, }); - final LightArchivedGame gameData; + final LightArchivedGame? archivedGameData; final Side orientation; final int? initialCursor; + final Object? error; @override Widget build(BuildContext context, WidgetRef ref) { + final gameData = archivedGameData; + + if (gameData == null) { + return BoardTable.empty( + showMoveListPlaceholder: true, + errorMessage: error?.toString(), + ); + } + if (initialCursor != null) { ref.listen(gameCursorProvider(gameData.id), (prev, cursor) { if (prev?.isLoading == true && cursor.hasValue) { @@ -124,7 +229,9 @@ class _BoardBody extends ConsumerWidget { final bottomPlayer = orientation == Side.white ? white : black; final loadingBoard = BoardTable( orientation: (isBoardTurned ? orientation.opposite : orientation), - fen: gameData.lastFen ?? kInitialBoardFEN, + fen: initialCursor == null + ? gameData.lastFen ?? kEmptyBoardFEN + : kEmptyBoardFEN, topTable: topPlayer, bottomTable: bottomPlayer, showMoveListPlaceholder: true, @@ -195,24 +302,53 @@ class _BoardBody extends ConsumerWidget { } class _BottomBar extends ConsumerWidget { - const _BottomBar({required this.gameData, required this.orientation}); + const _BottomBar({required this.archivedGameData, required this.orientation}); final Side orientation; - final LightArchivedGame gameData; + final LightArchivedGame? archivedGameData; @override Widget build(BuildContext context, WidgetRef ref) { + final gameData = archivedGameData; + + if (gameData == null) { + return const BottomBar(children: []); + } + final canGoForward = ref.watch(canGoForwardProvider(gameData.id)); final canGoBackward = ref.watch(canGoBackwardProvider(gameData.id)); final gameCursor = ref.watch(gameCursorProvider(gameData.id)); + Future showGameMenu() { + final game = gameCursor.valueOrNull?.$1; + final cursor = gameCursor.valueOrNull?.$2; + return showAdaptiveActionSheet( + context: context, + actions: [ + BottomSheetAction( + makeLabel: (context) => Text(context.l10n.flipBoard), + onPressed: (context) { + ref.read(isBoardTurnedProvider.notifier).toggle(); + }, + ), + if (game != null && cursor != null) + ...makeFinishedGameShareActions( + game, + context: context, + ref: ref, + currentGamePosition: game.positionAt(cursor), + orientation: orientation, + lastMove: game.moveAt(cursor), + ), + ], + ); + } + return BottomBar( children: [ BottomBarButton( label: context.l10n.menu, - onTap: () { - _showGameMenu(context, ref); - }, + onTap: showGameMenu, icon: Icons.menu, ), gameCursor.when( @@ -289,35 +425,14 @@ class _BottomBar extends ConsumerWidget { } void _cursorForward(WidgetRef ref) { - ref.read(gameCursorProvider(gameData.id).notifier).cursorForward(); + if (archivedGameData == null) return; + ref.read(gameCursorProvider(archivedGameData!.id).notifier).cursorForward(); } void _cursorBackward(WidgetRef ref) { - ref.read(gameCursorProvider(gameData.id).notifier).cursorBackward(); - } - - Future _showGameMenu(BuildContext context, WidgetRef ref) { - final game = ref.read(gameCursorProvider(gameData.id)).valueOrNull?.$1; - final cursor = ref.read(gameCursorProvider(gameData.id)).valueOrNull?.$2; - return showAdaptiveActionSheet( - context: context, - actions: [ - BottomSheetAction( - makeLabel: (context) => Text(context.l10n.flipBoard), - onPressed: (context) { - ref.read(isBoardTurnedProvider.notifier).toggle(); - }, - ), - if (game != null && cursor != null) - ...makeFinishedGameShareActions( - game, - context: context, - ref: ref, - currentGamePosition: game.positionAt(cursor), - orientation: orientation, - lastMove: game.moveAt(cursor), - ), - ], - ); + if (archivedGameData == null) return; + ref + .read(gameCursorProvider(archivedGameData!.id).notifier) + .cursorBackward(); } } diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index 7d720ac90a..b9b2ae524b 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -7,8 +7,6 @@ import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; -import 'package:lichess_mobile/src/model/game/archived_game.dart'; -import 'package:lichess_mobile/src/model/game/game_repository_providers.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_preferences.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_repository.dart'; @@ -557,11 +555,13 @@ class OpeningExplorerMoveTable extends ConsumerWidget { required this.blackWins, required this.pgn, required this.options, + super.key, }) : _isLoading = false; const OpeningExplorerMoveTable.loading({ required this.pgn, required this.options, + super.key, }) : _isLoading = true, moves = const IListConst([]), whiteWins = 0, @@ -760,8 +760,6 @@ class OpeningExplorerGameTile extends ConsumerStatefulWidget { class _OpeningExplorerGameTileState extends ConsumerState { - Future? gameRequest; - @override Widget build(BuildContext context) { const widthResultBox = 50.0; @@ -770,128 +768,110 @@ class _OpeningExplorerGameTileState return Container( padding: _kTableRowPadding, color: widget.color, - child: FutureBuilder( - future: gameRequest, - builder: (context, snapshot) { - return AdaptiveInkWell( - onTap: snapshot.connectionState == ConnectionState.waiting - ? null - : () async { - if (gameRequest == null) { - setState(() { - gameRequest = ref.read( - archivedGameProvider(id: widget.game.id).future, - ); - }); - } - - final archivedGame = await gameRequest!; - if (context.mounted) { - pushPlatformRoute( - context, - builder: (_) => ArchivedGameScreen( - gameData: archivedGame.data, - orientation: Side.white, - initialCursor: widget.ply, - ), - ); - } - }, - child: Row( - mainAxisAlignment: MainAxisAlignment.start, + child: AdaptiveInkWell( + onTap: () { + pushPlatformRoute( + context, + builder: (_) => ArchivedGameScreen( + gameId: widget.game.id, + orientation: Side.white, + initialCursor: widget.ply, + ), + ); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(widget.game.white.rating.toString()), - Text(widget.game.black.rating.toString()), - ], - ), - const SizedBox(width: 10), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - widget.game.white.name, - overflow: TextOverflow.ellipsis, - ), - Text( - widget.game.black.name, - overflow: TextOverflow.ellipsis, - ), - ], + Text(widget.game.white.rating.toString()), + Text(widget.game.black.rating.toString()), + ], + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.game.white.name, + overflow: TextOverflow.ellipsis, ), - ), - Row( - children: [ - if (widget.game.winner == 'white') - Container( - width: widthResultBox, - padding: paddingResultBox, - decoration: BoxDecoration( - color: _whiteBoxColor(context), - borderRadius: BorderRadius.circular(5), - ), - child: const Text( - '1-0', - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.black, - ), - ), - ) - else if (widget.game.winner == 'black') - Container( - width: widthResultBox, - padding: paddingResultBox, - decoration: BoxDecoration( - color: _blackBoxColor(context), - borderRadius: BorderRadius.circular(5), - ), - child: const Text( - '0-1', - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white, - ), - ), - ) - else - Container( - width: widthResultBox, - padding: paddingResultBox, - decoration: BoxDecoration( - color: Colors.grey, - borderRadius: BorderRadius.circular(5), - ), - child: const Text( - '½-½', - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white, - ), - ), + Text( + widget.game.black.name, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + Row( + children: [ + if (widget.game.winner == 'white') + Container( + width: widthResultBox, + padding: paddingResultBox, + decoration: BoxDecoration( + color: _whiteBoxColor(context), + borderRadius: BorderRadius.circular(5), + ), + child: const Text( + '1-0', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.black, ), - if (widget.game.month != null) ...[ - const SizedBox(width: 10.0), - Text( - widget.game.month!, - style: const TextStyle( - fontFeatures: [FontFeature.tabularFigures()], - ), + ), + ) + else if (widget.game.winner == 'black') + Container( + width: widthResultBox, + padding: paddingResultBox, + decoration: BoxDecoration( + color: _blackBoxColor(context), + borderRadius: BorderRadius.circular(5), + ), + child: const Text( + '0-1', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, ), - ], - if (widget.game.speed != null) ...[ - const SizedBox(width: 10.0), - Icon(widget.game.speed!.icon, size: 20), - ], - ], - ), + ), + ) + else + Container( + width: widthResultBox, + padding: paddingResultBox, + decoration: BoxDecoration( + color: Colors.grey, + borderRadius: BorderRadius.circular(5), + ), + child: const Text( + '½-½', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + ), + ), + ), + if (widget.game.month != null) ...[ + const SizedBox(width: 10.0), + Text( + widget.game.month!, + style: const TextStyle( + fontFeatures: [FontFeature.tabularFigures()], + ), + ), + ], + if (widget.game.speed != null) ...[ + const SizedBox(width: 10.0), + Icon(widget.game.speed!.icon, size: 20), + ], ], ), - ); - }, + ], + ), ), ); } diff --git a/lib/src/widgets/board_table.dart b/lib/src/widgets/board_table.dart index 6145df6a57..3f575eacf4 100644 --- a/lib/src/widgets/board_table.dart +++ b/lib/src/widgets/board_table.dart @@ -29,6 +29,7 @@ const _moveListOpacity = 0.6; /// /// An optional overlay or error message can be displayed on top of the board. class BoardTable extends ConsumerStatefulWidget { + /// Creates a board table with the given values. const BoardTable({ required this.fen, required this.orientation, @@ -54,6 +55,27 @@ class BoardTable extends ConsumerStatefulWidget { 'You must provide `currentMoveIndex` along with `moves`', ); + /// Creates an empty board table (useful for loading). + const BoardTable.empty({ + this.showMoveListPlaceholder = false, + this.showEngineGaugePlaceholder = false, + this.errorMessage, + }) : fen = kEmptyBoardFEN, + orientation = Side.white, + gameData = null, + lastMove = null, + boardSettingsOverrides = null, + topTable = const SizedBox.shrink(), + bottomTable = const SizedBox.shrink(), + shapes = null, + engineGauge = null, + moves = null, + currentMoveIndex = null, + onSelectMove = null, + boardOverlay = null, + boardKey = null, + zenMode = false; + final String fen; final Side orientation; diff --git a/lib/src/widgets/platform_scaffold.dart b/lib/src/widgets/platform_scaffold.dart index 015ed2f6dc..147ee72d2b 100644 --- a/lib/src/widgets/platform_scaffold.dart +++ b/lib/src/widgets/platform_scaffold.dart @@ -85,7 +85,7 @@ class PlatformAppBarLoadingIndicator extends StatelessWidget { height: 24, width: 24, child: Center( - child: CircularProgressIndicator.adaptive(), + child: CircularProgressIndicator(), ), ), ), diff --git a/test/view/game/archived_game_screen_test.dart b/test/view/game/archived_game_screen_test.dart index cd18a3975b..288db21c50 100644 --- a/test/view/game/archived_game_screen_test.dart +++ b/test/view/game/archived_game_screen_test.dart @@ -29,6 +29,39 @@ final client = MockClient((request) { void main() { group('ArchivedGameScreen', () { + testWidgets( + 'loads game data if only game id is provided', + (tester) async { + final app = await buildTestApp( + tester, + home: const ArchivedGameScreen( + gameId: GameId('qVChCOTc'), + orientation: Side.white, + ), + overrides: [ + lichessClientProvider + .overrideWith((ref) => LichessClient(client, ref)), + ], + ); + + await tester.pumpWidget(app); + + expect(find.byType(PieceWidget), findsNothing); + expect(find.byType(CircularProgressIndicator), findsOneWidget); + + // wait for game data loading + await tester.pump(const Duration(milliseconds: 100)); + + expect(find.byType(PieceWidget), findsNWidgets(25)); + expect(find.widgetWithText(GamePlayer, 'veloce'), findsOneWidget); + expect( + find.widgetWithText(GamePlayer, 'Stockfish level 1'), + findsOneWidget, + ); + }, + variant: kPlatformVariant, + ); + testWidgets( 'displays game data and last fen immediately, then moves', (tester) async { @@ -188,7 +221,7 @@ void main() { // -- const gameResponse = ''' -{"id":"qVChCOTc","rated":false,"variant":"standard","speed":"blitz","perf":"blitz","createdAt":1673443822389,"lastMoveAt":1673444036416,"status":"mate","players":{"white":{"aiLevel":1},"black":{"user":{"name":"veloce","patron":true,"id":"veloce"},"rating":1435,"provisional":true}},"winner":"black","opening":{"eco":"C20","name":"King's Pawn Game: Wayward Queen Attack, Kiddie Countergambit","ply":4},"moves":"e4 e5 Qh5 Nf6 Qxe5+ Be7 b3 d6 Qb5+ Bd7 Qxb7 Nc6 Ba3 Rb8 Qa6 Nxe4 Bb2 O-O Nc3 Nb4 Nf3 Nxa6 Nd5 Nb4 Nxe7+ Qxe7 Nd4 Qf6 f4 Qe7 Ke2 Ng3+ Kd1 Nxh1 Bc4 Nf2+ Kc1 Qe1#","clocks":[18003,18003,17915,17627,17771,16691,17667,16243,17475,15459,17355,14779,17155,13795,16915,13267,14771,11955,14451,10995,14339,10203,13899,9099,12427,8379,12003,7547,11787,6691,11355,6091,11147,5763,10851,5099,10635,4657],"clock":{"initial":180,"increment":0,"totalTime":180}} +{"id":"qVChCOTc","rated":false,"variant":"standard","speed":"blitz","perf":"blitz","createdAt":1673443822389,"lastMoveAt":1673444036416,"status":"mate","players":{"white":{"aiLevel":1},"black":{"user":{"name":"veloce","patron":true,"id":"veloce"},"rating":1435,"provisional":true}},"winner":"black","opening":{"eco":"C20","name":"King's Pawn Game: Wayward Queen Attack, Kiddie Countergambit","ply":4},"moves":"e4 e5 Qh5 Nf6 Qxe5+ Be7 b3 d6 Qb5+ Bd7 Qxb7 Nc6 Ba3 Rb8 Qa6 Nxe4 Bb2 O-O Nc3 Nb4 Nf3 Nxa6 Nd5 Nb4 Nxe7+ Qxe7 Nd4 Qf6 f4 Qe7 Ke2 Ng3+ Kd1 Nxh1 Bc4 Nf2+ Kc1 Qe1#","clocks":[18003,18003,17915,17627,17771,16691,17667,16243,17475,15459,17355,14779,17155,13795,16915,13267,14771,11955,14451,10995,14339,10203,13899,9099,12427,8379,12003,7547,11787,6691,11355,6091,11147,5763,10851,5099,10635,4657],"clock":{"initial":180,"increment":0,"totalTime":180},"lastFen":"1r3rk1/p1pb1ppp/3p4/8/1nBN1P2/1P6/PBPP1nPP/R1K1q3 w - - 4 1"} '''; final gameData = LightArchivedGame( From e8e576a7ef1a327aaf6915c9abeefe9d1a9abc35 Mon Sep 17 00:00:00 2001 From: Noah Date: Sat, 7 Sep 2024 12:04:47 +0200 Subject: [PATCH 283/979] convert calulateEnPassantOptions to private method --- .../board_editor/board_editor_controller.dart | 14 +++++++++----- lib/src/view/board_editor/board_editor_menu.dart | 7 ++----- .../board_editor/board_editor_screen_test.dart | 6 +++--- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/lib/src/model/board_editor/board_editor_controller.dart b/lib/src/model/board_editor/board_editor_controller.dart index 5281de23c3..fcb8259051 100644 --- a/lib/src/model/board_editor/board_editor_controller.dart +++ b/lib/src/model/board_editor/board_editor_controller.dart @@ -17,6 +17,7 @@ class BoardEditorController extends _$BoardEditorController { pieces: readFen(initialFen ?? kInitialFEN).lock, unmovedRooks: SquareSet.corners, editorPointerMode: EditorPointerMode.drag, + enPassantOptions: SquareSet.empty, enPassantSquare: null, pieceToAddOnEdit: null, ); @@ -60,6 +61,7 @@ class BoardEditorController extends _$BoardEditorController { state = state.copyWith( sideToPlay: side, ); + _calculateEnPassantOptions(); } void loadFen(String fen) { @@ -67,10 +69,10 @@ class BoardEditorController extends _$BoardEditorController { } /// Calculates the squares where an en passant capture could be possible. - SquareSet calculateEnPassantOptions() { + void _calculateEnPassantOptions() { final side = state.sideToPlay; final pieces = state.pieces; - SquareSet enPassantSquares = SquareSet.empty; + SquareSet enPassantOptions = SquareSet.empty; final boardFen = writeFen(pieces.unlock); final board = Board.parseFen(boardFen); @@ -92,8 +94,8 @@ class BoardEditorController extends _$BoardEditorController { board.roleAt(adjacentSquare) == Role.pawn && board.sideAt(targetSquare) == null && board.sideAt(originSquare) == null) { - enPassantSquares = - enPassantSquares.union(SquareSet.fromSquare(targetSquare)); + enPassantOptions = + enPassantOptions.union(SquareSet.fromSquare(targetSquare)); } } @@ -107,7 +109,7 @@ class BoardEditorController extends _$BoardEditorController { } }); - return enPassantSquares; + state = state.copyWith(enPassantOptions: enPassantOptions); } void toggleEnPassantSquare(Square square) { @@ -118,6 +120,7 @@ class BoardEditorController extends _$BoardEditorController { void _updatePosition(IMap pieces) { state = state.copyWith(pieces: pieces); + _calculateEnPassantOptions(); } void setCastling(Side side, CastlingSide castlingSide, bool allowed) { @@ -156,6 +159,7 @@ class BoardEditorState with _$BoardEditorState { required IMap pieces, required SquareSet unmovedRooks, required EditorPointerMode editorPointerMode, + required SquareSet enPassantOptions, required Square? enPassantSquare, /// When null, clears squares when in edit mode. Has no effect in drag mode. diff --git a/lib/src/view/board_editor/board_editor_menu.dart b/lib/src/view/board_editor/board_editor_menu.dart index 72649585d2..65e4e045b8 100644 --- a/lib/src/view/board_editor/board_editor_menu.dart +++ b/lib/src/view/board_editor/board_editor_menu.dart @@ -15,9 +15,6 @@ class BoardEditorMenu extends ConsumerWidget { final editorController = boardEditorControllerProvider(initialFen); final editorState = ref.watch(editorController); - final enPassantSquares = - ref.read(editorController.notifier).calculateEnPassantOptions(); - return SafeArea( child: Padding( padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 8.0), @@ -83,14 +80,14 @@ class BoardEditorMenu extends ConsumerWidget { ), ); }), - if (enPassantSquares.isNotEmpty) ...[ + if (editorState.enPassantOptions.isNotEmpty) ...[ Padding( padding: Styles.bodySectionPadding, child: const Text('En passant', style: Styles.subtitle), ), Wrap( spacing: 8.0, - children: enPassantSquares.squares.map((square) { + children: editorState.enPassantOptions.squares.map((square) { return ChoiceChip( label: Text(square.name), selected: editorState.enPassantSquare == square, diff --git a/test/view/board_editor/board_editor_screen_test.dart b/test/view/board_editor/board_editor_screen_test.dart index e8e8966510..c5a5ded5f3 100644 --- a/test/view/board_editor/board_editor_screen_test.dart +++ b/test/view/board_editor/board_editor_screen_test.dart @@ -160,7 +160,7 @@ void main() { .loadFen('1nbqkbn1/pppppppp/8/8/8/8/PPPPPPPP/1NBQKBN1'); expect( - container.read(controllerProvider.notifier).calculateEnPassantOptions(), + container.read(controllerProvider).enPassantOptions, SquareSet.empty, ); @@ -168,7 +168,7 @@ void main() { 'r1bqkbnr/4p1p1/3n4/pPppPppP/8/8/P1PP1P2/RNBQKBNR w KQkq - 0 1', ); expect( - container.read(controllerProvider.notifier).calculateEnPassantOptions(), + container.read(controllerProvider).enPassantOptions, SquareSet.fromSquares([Square.a6, Square.c6, Square.f6]), ); container.read(controllerProvider.notifier).loadFen( @@ -176,7 +176,7 @@ void main() { ); container.read(controllerProvider.notifier).setSideToPlay(Side.black); expect( - container.read(controllerProvider.notifier).calculateEnPassantOptions(), + container.read(controllerProvider).enPassantOptions, SquareSet.fromSquares([Square.e3, Square.h3]), ); }); From 9ff8f794e52d3c022b7a9f69c63cb9d406d5eb4c Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Sat, 7 Sep 2024 12:17:58 +0200 Subject: [PATCH 284/979] fix broadcast analysis screen, live move are now played on the analysis screen --- .../model/analysis/analysis_controller.dart | 12 + lib/src/model/broadcast/broadcast.dart | 1 - .../broadcast/broadcast_game_controller.dart | 113 ++++++++ .../model/broadcast/broadcast_repository.dart | 1 - .../broadcast/broadcast_round_controller.dart | 70 ----- lib/src/view/analysis/tree_view.dart | 67 ++--- .../broadcast/broadcast_analysis_screen.dart | 267 +++++++++++------- .../view/broadcast/broadcast_boards_tab.dart | 3 - 8 files changed, 315 insertions(+), 219 deletions(-) create mode 100644 lib/src/model/broadcast/broadcast_game_controller.dart diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index 5ac4a7da1f..0a017c9dee 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -195,6 +195,18 @@ class AnalysisController extends _$AnalysisController { } } + void onBroadcastMove(UciPath path, Move move) { + final (newPath, isNewNode) = _root.addMoveAt(path, move); + + if (newPath != null) { + _setPath( + newPath, + shouldRecomputeRootView: isNewNode, + shouldForceShowVariation: true, + ); + } + } + void onPromotionSelection(Role? role) { if (role == null) { state = state.copyWith(promotionMove: null); diff --git a/lib/src/model/broadcast/broadcast.dart b/lib/src/model/broadcast/broadcast.dart index c6f27e22bd..d906906181 100644 --- a/lib/src/model/broadcast/broadcast.dart +++ b/lib/src/model/broadcast/broadcast.dart @@ -94,7 +94,6 @@ class BroadcastGame with _$BroadcastGame { required String fen, required Move? lastMove, required String? status, - required String? pgn, /// The amount of time that the player whose turn it is has been thinking since his last move required Duration thinkTime, diff --git a/lib/src/model/broadcast/broadcast_game_controller.dart b/lib/src/model/broadcast/broadcast_game_controller.dart new file mode 100644 index 0000000000..14472562ad --- /dev/null +++ b/lib/src/model/broadcast/broadcast_game_controller.dart @@ -0,0 +1,113 @@ +import 'dart:async'; + +import 'package:dartchess/dartchess.dart'; +import 'package:deep_pick/deep_pick.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast_providers.dart'; +import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/common/socket.dart'; +import 'package:lichess_mobile/src/utils/json.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'broadcast_game_controller.g.dart'; + +const broadcastAnalysisOptions = AnalysisOptions( + id: StringId('standalone_analysis'), + isLocalEvaluationAllowed: true, + orientation: Side.white, + variant: Variant.standard, +); + +@riverpod +class BroadcastGameController extends _$BroadcastGameController { + static Uri broadcastSocketUri(BroadcastRoundId broadcastRoundId) => + Uri(path: 'study/$broadcastRoundId/socket/v6'); + + StreamSubscription? _subscription; + + late SocketClient _socketClient; + + @override + Future build(BroadcastRoundId roundId, BroadcastGameId gameId) async { + _socketClient = ref + .watch(socketPoolProvider) + .open(BroadcastGameController.broadcastSocketUri(roundId)); + + _subscription = _socketClient.stream.listen(_handleSocketEvent); + + ref.onDispose(() { + _subscription?.cancel(); + }); + + final pgn = await ref.watch( + broadcastGameProvider(roundId: roundId, gameId: gameId).future, + ); + return pgn; + } + + void _handleSocketEvent(SocketEvent event) { + if (!state.hasValue) return; + + switch (event.topic) { + // Sent when a node is recevied from the broadcast + case 'addNode': + _handleAddNodeEvent(event); + // Sent when a pgn tag changes + case 'setTags': + _handleSetTagsEvent(event); + } + } + + void _handleAddNodeEvent(SocketEvent event) { + final broadcastGameId = + pick(event.data, 'p', 'chapterId').asBroadcastGameIdOrThrow(); + + // We check if the event is for this game + if (broadcastGameId != gameId) return; + + // The path of the last and current move of the broadcasted game + // Its value is "!" if the path is identical to one of the node that was received + final currentPath = pick(event.data, 'relayPath').asUciPathOrThrow(); + + // We check that the event we received is for the last move of the game + if (currentPath.value != '!') return; + + // The path for the node that was received + final path = pick(event.data, 'p', 'path').asUciPathOrThrow(); + final uciMove = pick(event.data, 'n', 'uci').asUciMoveOrThrow(); + + final ctrlProviderNotifier = ref.read( + analysisControllerProvider(state.requireValue, broadcastAnalysisOptions) + .notifier, + ); + + ctrlProviderNotifier.onBroadcastMove(path, uciMove); + } + + void _handleSetTagsEvent(SocketEvent event) { + final broadcastGameId = + pick(event.data, 'chapterId').asBroadcastGameIdOrThrow(); + + // We check if the event is for this game + if (broadcastGameId != gameId) return; + + final ctrlProviderNotifier = ref.read( + analysisControllerProvider(state.requireValue, broadcastAnalysisOptions) + .notifier, + ); + + final headers = Map.fromEntries( + pick(event.data, 'tags').asListOrThrow( + (header) => MapEntry( + header(0).asStringOrThrow(), + header(1).asStringOrThrow(), + ), + ), + ); + + for (final entry in headers.entries) { + ctrlProviderNotifier.updatePgnHeader(entry.key, entry.value); + } + } +} diff --git a/lib/src/model/broadcast/broadcast_repository.dart b/lib/src/model/broadcast/broadcast_repository.dart index fe2b13d345..72fdd1504c 100644 --- a/lib/src/model/broadcast/broadcast_repository.dart +++ b/lib/src/model/broadcast/broadcast_repository.dart @@ -165,7 +165,6 @@ MapEntry gameFromPick( status: pick('status').asStringOrNull(), thinkTime: pick('thinkTime').asDurationFromSecondsOrNull() ?? Duration.zero, - pgn: null, ), ); diff --git a/lib/src/model/broadcast/broadcast_round_controller.dart b/lib/src/model/broadcast/broadcast_round_controller.dart index eadf69c63e..dc7331e856 100644 --- a/lib/src/model/broadcast/broadcast_round_controller.dart +++ b/lib/src/model/broadcast/broadcast_round_controller.dart @@ -60,23 +60,6 @@ class BroadcastRoundController extends _$BroadcastRoundController { ); } - Future setPgn(BroadcastGameId gameId) async { - final pgn = await ref.watch( - broadcastGameProvider( - roundId: broadcastRoundId, - gameId: gameId, - ).future, - ); - state = AsyncData( - state.requireValue.update( - gameId, - (broadcastGame) => broadcastGame.copyWith( - pgn: pgn, - ), - ), - ); - } - void _handleSocketEvent(SocketEvent event) { if (!state.hasValue) return; @@ -90,9 +73,6 @@ class BroadcastRoundController extends _$BroadcastRoundController { // Sent when clocks are updated from the broadcast case 'clock': _handleClockEvent(event); - // Sent when a pgn tag changes - case 'setTags': - _handleSetTagsEvent(event); } } @@ -163,54 +143,4 @@ class BroadcastRoundController extends _$BroadcastRoundController { ), ); } - - void _handleSetTagsEvent(SocketEvent event) { - final gameId = pick(event.data, 'chapterId').asBroadcastGameIdOrThrow(); - - if (state.requireValue[gameId]?.pgn == null) return; - - final headers = pick(event.data, 'tags').asMapOrThrow(); - - final pgnGame = PgnGame.parsePgn(state.requireValue[gameId]!.pgn!); - - final newPgnGame = PgnGame( - headers: headers, - moves: pgnGame.moves, - comments: pgnGame.comments, - ); - - state = AsyncData( - state.requireValue.update( - gameId, - (broadcastsGame) => broadcastsGame.copyWith( - pgn: newPgnGame.makePgn(), - ), - ), - ); - } } - -// void _handleAddNodeEvent(SocketEvent event) { -// final gameId = -// pick(event.data, 'p', 'chapterId').asBroadcastGameIdOrThrow(); - -// // We check that the event we received is for the game we are currently watching -// if (gameId != broadcastGameId) return; - -// // The path of the last and current move of the broadcasted game -// // Its value is "!" if the path is identical to one of the node that was received -// final currentPath = pick(event.data, 'relayPath').asUciPathOrThrow(); - -// // We check that the event we received is for the last move of the game -// if (currentPath.value != '!') return; - -// // The path for the node that was received -// final path = pick(event.data, 'p', 'path').asUciPathOrThrow(); -// final nodeId = pick(event.data, 'n', 'id').asUciCharPairOrThrow(); - -// final newPgn = (Root.fromPgnGame(PgnGame.parsePgn(state.requireValue)) -// ..promoteAt(path + nodeId, toMainline: true)) -// .makePgn(); - -// state = AsyncData(newPgn); -// } diff --git a/lib/src/view/analysis/tree_view.dart b/lib/src/view/analysis/tree_view.dart index 8ee1655c7b..38e3a873f7 100644 --- a/lib/src/view/analysis/tree_view.dart +++ b/lib/src/view/analysis/tree_view.dart @@ -151,23 +151,19 @@ class _InlineTreeViewState extends ConsumerState { ); } - return CustomScrollView( - slivers: [ + return ListView( + padding: EdgeInsets.zero, + children: [ if (kOpeningAllowedVariants.contains(widget.options.variant)) - SliverPersistentHeader( - delegate: _OpeningHeaderDelegate( - ctrlProvider, - displayMode: widget.displayMode, - ), + _OpeningHeader( + ctrlProvider, + displayMode: widget.displayMode, ), - SliverFillRemaining( - hasScrollBody: false, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 10), - child: Wrap( - spacing: kInlineMoveSpacing, - children: moveWidgets, - ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 10), + child: Wrap( + spacing: kInlineMoveSpacing, + children: moveWidgets, ), ), ], @@ -308,6 +304,7 @@ class InlineMove extends ConsumerWidget { this.startMainline = false, this.startSideline = false, this.endSideline = false, + this.isLiveMove = false, }); final String pgn; @@ -322,6 +319,7 @@ class InlineMove extends ConsumerWidget { final bool startMainline; final bool startSideline; final bool endSideline; + final bool isLiveMove; static const borderRadius = BorderRadius.all(Radius.circular(4.0)); static const baseTextStyle = TextStyle( @@ -423,8 +421,15 @@ class InlineMove extends ConsumerWidget { : Theme.of(context).focusColor, shape: BoxShape.rectangle, borderRadius: borderRadius, + border: + isLiveMove ? Border.all(color: Colors.orange) : null, ) - : null, + : BoxDecoration( + shape: BoxShape.rectangle, + borderRadius: borderRadius, + border: + isLiveMove ? Border.all(color: Colors.orange) : null, + ), child: Text( moveWithNag, style: isCurrentMove @@ -682,8 +687,8 @@ class _Comments extends StatelessWidget { } } -class _OpeningHeaderDelegate extends SliverPersistentHeaderDelegate { - const _OpeningHeaderDelegate( +class _OpeningHeader extends ConsumerWidget { + const _OpeningHeader( this.ctrlProvider, { required this.displayMode, }); @@ -691,32 +696,6 @@ class _OpeningHeaderDelegate extends SliverPersistentHeaderDelegate { final AnalysisControllerProvider ctrlProvider; final Orientation displayMode; - @override - Widget build( - BuildContext context, - double shrinkOffset, - bool overlapsContent, - ) { - return _Opening(ctrlProvider, displayMode); - } - - @override - double get minExtent => kOpeningHeaderHeight; - - @override - double get maxExtent => kOpeningHeaderHeight; - - @override - bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) => - true; -} - -class _Opening extends ConsumerWidget { - const _Opening(this.ctrlProvider, this.displayMode); - - final AnalysisControllerProvider ctrlProvider; - final Orientation displayMode; - @override Widget build(BuildContext context, WidgetRef ref) { final isRootNode = ref.watch( diff --git a/lib/src/view/broadcast/broadcast_analysis_screen.dart b/lib/src/view/broadcast/broadcast_analysis_screen.dart index 7faca8a5cb..5425e6d762 100644 --- a/lib/src/view/broadcast/broadcast_analysis_screen.dart +++ b/lib/src/view/broadcast/broadcast_analysis_screen.dart @@ -16,8 +16,8 @@ import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; import 'package:lichess_mobile/src/model/analysis/server_analysis_service.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast_game_controller.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_round_controller.dart'; -import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/eval.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; @@ -49,13 +49,6 @@ import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:popover/popover.dart'; -const _options = AnalysisOptions( - id: StringId('standalone_analysis'), - isLocalEvaluationAllowed: true, - orientation: Side.white, - variant: Variant.standard, -); - class BroadcastAnalysisScreen extends ConsumerWidget { final BroadcastRoundId roundId; final BroadcastGameId gameId; @@ -77,30 +70,32 @@ class BroadcastAnalysisScreen extends ConsumerWidget { } Widget _androidBuilder(BuildContext context, WidgetRef ref) { - final game = ref.watch( - broadcastRoundControllerProvider( + final pgn = ref.watch( + broadcastGameControllerProvider( roundId, - ).select((games) => games.value?[gameId]), + gameId, + ), ); - final pgn = game?.pgn; return Scaffold( resizeToAvoidBottomInset: false, appBar: AppBar( title: Text(title), actions: [ - if (pgn != null) + if (pgn.hasValue) _EngineDepth( - analysisControllerProvider(pgn, _options), + analysisControllerProvider( + pgn.requireValue, broadcastAnalysisOptions), ), AppBarIconButton( - onPressed: () => (pgn != null) + onPressed: () => (pgn.hasValue) ? showAdaptiveBottomSheet( context: context, isScrollControlled: true, showDragHandle: true, isDismissible: true, - builder: (_) => AnalysisSettings(pgn, _options), + builder: (_) => AnalysisSettings( + pgn.requireValue, broadcastAnalysisOptions), ) : null, semanticsLabel: context.l10n.settingsSettings, @@ -108,19 +103,31 @@ class BroadcastAnalysisScreen extends ConsumerWidget { ), ], ), - body: (pgn != null) - ? _Body(game: game!, options: _options) - : const Center(child: CircularProgressIndicator.adaptive()), + body: pgn.when( + data: (pgn) => _Body( + roundId: roundId, + gameId: gameId, + pgn: pgn, + broadcastAnalysisOptions: broadcastAnalysisOptions, + ), + loading: () => + const Center(child: CircularProgressIndicator.adaptive()), + error: (error, _) { + return Center( + child: Text('Cannot load game analysis: $error'), + ); + }, + ), ); } Widget _iosBuilder(BuildContext context, WidgetRef ref) { - final game = ref.watch( - broadcastRoundControllerProvider( + final pgn = ref.watch( + broadcastGameControllerProvider( roundId, - ).select((games) => games.value?[gameId]), + gameId, + ), ); - final pgn = game?.pgn; return CupertinoPageScaffold( resizeToAvoidBottomInset: false, @@ -132,18 +139,20 @@ class BroadcastAnalysisScreen extends ConsumerWidget { trailing: Row( mainAxisSize: MainAxisSize.min, children: [ - if (pgn != null) + if (pgn.hasValue) _EngineDepth( - analysisControllerProvider(pgn, _options), + analysisControllerProvider( + pgn.requireValue, broadcastAnalysisOptions), ), AppBarIconButton( - onPressed: () => (pgn != null) + onPressed: () => (pgn.hasValue) ? showAdaptiveBottomSheet( context: context, isScrollControlled: true, showDragHandle: true, isDismissible: true, - builder: (_) => AnalysisSettings(pgn, _options), + builder: (_) => AnalysisSettings( + pgn.requireValue, broadcastAnalysisOptions), ) : null, semanticsLabel: context.l10n.settingsSettings, @@ -152,22 +161,42 @@ class BroadcastAnalysisScreen extends ConsumerWidget { ], ), ), - child: (pgn != null) - ? _Body(game: game!, options: _options) - : const Center(child: CircularProgressIndicator.adaptive()), + child: pgn.when( + data: (pgn) => _Body( + roundId: roundId, + gameId: gameId, + pgn: pgn, + broadcastAnalysisOptions: broadcastAnalysisOptions, + ), + loading: () => + const Center(child: CircularProgressIndicator.adaptive()), + error: (error, _) { + return Center( + child: Text('Cannot load game analysis: $error'), + ); + }, + ), ); } } class _Body extends ConsumerWidget { - const _Body({required this.game, required this.options}); + const _Body({ + required this.roundId, + required this.gameId, + required this.pgn, + required this.broadcastAnalysisOptions, + }); - final BroadcastGame game; - final AnalysisOptions options; + final BroadcastRoundId roundId; + final BroadcastGameId gameId; + final String pgn; + final AnalysisOptions broadcastAnalysisOptions; @override Widget build(BuildContext context, WidgetRef ref) { - final ctrlProvider = analysisControllerProvider(game.pgn!, options); + final ctrlProvider = + analysisControllerProvider(pgn, broadcastAnalysisOptions); final showEvaluationGauge = ref.watch( analysisPreferencesProvider.select((value) => value.showEvaluationGauge), ); @@ -218,7 +247,9 @@ class _Body extends ConsumerWidget { child: Row( children: [ _AnalysisBoardPlayersAndClocks( - game, + roundId, + gameId, + pgn, pov, boardSize, isTablet: isTablet, @@ -248,12 +279,12 @@ class _Body extends ConsumerWidget { semanticContainer: false, child: showAnalysisSummary ? ServerAnalysisSummary( - game.pgn!, - options, + pgn, + broadcastAnalysisOptions, ) : AnalysisTreeView( - game.pgn!, - options, + pgn, + broadcastAnalysisOptions, Orientation.landscape, ), ), @@ -275,7 +306,9 @@ class _Body extends ConsumerWidget { kTabletBoardTableSidePadding, ), child: _AnalysisBoardPlayersAndClocks( - game, + roundId, + gameId, + pgn, pov, boardSize, isTablet: isTablet, @@ -283,20 +316,25 @@ class _Body extends ConsumerWidget { ) else _AnalysisBoardPlayersAndClocks( - game, + roundId, + gameId, + pgn, pov, boardSize, isTablet: isTablet, ), if (showAnalysisSummary) Expanded( - child: ServerAnalysisSummary(game.pgn!, options), + child: ServerAnalysisSummary( + pgn, + broadcastAnalysisOptions, + ), ) else Expanded( child: AnalysisTreeView( - game.pgn!, - options, + pgn, + broadcastAnalysisOptions, Orientation.portrait, ), ), @@ -306,7 +344,10 @@ class _Body extends ConsumerWidget { ), ), ), - _BottomBar(pgn: game.pgn!, options: options), + _BottomBar( + pgn: pgn, + broadcastAnalysisOptions: broadcastAnalysisOptions, + ), ], ); } @@ -314,45 +355,52 @@ class _Body extends ConsumerWidget { enum _PlayerWidgetSide { bottom, top } -class _AnalysisBoardPlayersAndClocks extends StatelessWidget { - final BroadcastGame game; +class _AnalysisBoardPlayersAndClocks extends ConsumerWidget { + final BroadcastRoundId roundId; + final BroadcastGameId gameId; + final String pgn; final Side pov; final double boardSize; final bool isTablet; const _AnalysisBoardPlayersAndClocks( - this.game, + this.roundId, + this.gameId, + this.pgn, this.pov, this.boardSize, { required this.isTablet, }); @override - Widget build(BuildContext context) { - final playingSide = Setup.parseFen(game.fen).turn; + Widget build(BuildContext context, WidgetRef ref) { + final game = ref.watch( + broadcastRoundControllerProvider(roundId) + .select((game) => game.value?[gameId]), + ); return Column( children: [ - _PlayerWidget( - width: boardSize, - game: game, - side: pov.opposite, - playingSide: playingSide, - boardSide: _PlayerWidgetSide.top, - ), + if (game != null) + _PlayerWidget( + width: boardSize, + game: game, + side: pov.opposite, + boardSide: _PlayerWidgetSide.top, + ), AnalysisBoard( - game.pgn!, + pgn, broadcastAnalysisOptions, boardSize, isTablet: isTablet, ), - _PlayerWidget( - width: boardSize, - game: game, - side: pov, - playingSide: playingSide, - boardSide: _PlayerWidgetSide.bottom, - ), + if (game != null) + _PlayerWidget( + width: boardSize, + game: game, + side: pov, + boardSide: _PlayerWidgetSide.bottom, + ), ], ); } @@ -363,13 +411,11 @@ class _PlayerWidget extends StatelessWidget { required this.width, required this.game, required this.side, - required this.playingSide, required this.boardSide, }); final BroadcastGame game; final Side side; - final Side playingSide; final double width; final _PlayerWidgetSide boardSide; @@ -377,6 +423,7 @@ class _PlayerWidget extends StatelessWidget { Widget build(BuildContext context) { final player = game.players[side]!; final gameStatus = game.status; + final playingSide = game.playingSide; return SizedBox( width: width, @@ -692,15 +739,16 @@ class _Engineline extends ConsumerWidget { class _BottomBar extends ConsumerWidget { const _BottomBar({ required this.pgn, - required this.options, + required this.broadcastAnalysisOptions, }); final String pgn; - final AnalysisOptions options; + final AnalysisOptions broadcastAnalysisOptions; @override Widget build(BuildContext context, WidgetRef ref) { - final ctrlProvider = analysisControllerProvider(pgn, options); + final ctrlProvider = + analysisControllerProvider(pgn, broadcastAnalysisOptions); final canGoBack = ref.watch(ctrlProvider.select((value) => value.canGoBack)); final canGoNext = @@ -751,7 +799,7 @@ class _BottomBar extends ConsumerWidget { context, builder: (_) => OpeningExplorerScreen( pgn: pgn, - options: options, + options: broadcastAnalysisOptions, ), ), icon: Icons.explore, @@ -788,10 +836,11 @@ class _BottomBar extends ConsumerWidget { ); } - void _moveForward(WidgetRef ref) => - ref.read(analysisControllerProvider(pgn, options).notifier).userNext(); + void _moveForward(WidgetRef ref) => ref + .read(analysisControllerProvider(pgn, broadcastAnalysisOptions).notifier) + .userNext(); void _moveBackward(WidgetRef ref) => ref - .read(analysisControllerProvider(pgn, options).notifier) + .read(analysisControllerProvider(pgn, broadcastAnalysisOptions).notifier) .userPrevious(); Future _showAnalysisMenu(BuildContext context, WidgetRef ref) { @@ -802,7 +851,10 @@ class _BottomBar extends ConsumerWidget { makeLabel: (context) => Text(context.l10n.flipBoard), onPressed: (context) { ref - .read(analysisControllerProvider(pgn, options).notifier) + .read( + analysisControllerProvider(pgn, broadcastAnalysisOptions) + .notifier, + ) .toggleBoard(); }, ), @@ -812,7 +864,10 @@ class _BottomBar extends ConsumerWidget { pushPlatformRoute( context, title: context.l10n.studyShareAndExport, - builder: (_) => AnalysisShareScreen(pgn: pgn, options: options), + builder: (_) => AnalysisShareScreen( + pgn: pgn, + options: broadcastAnalysisOptions, + ), ); }, ), @@ -822,24 +877,28 @@ class _BottomBar extends ConsumerWidget { launchShareDialog( context, text: ref - .read(analysisControllerProvider(pgn, options)) + .read( + analysisControllerProvider(pgn, broadcastAnalysisOptions), + ) .position .fen, ); }, ), - if (options.gameAnyId != null) + if (broadcastAnalysisOptions.gameAnyId != null) BottomSheetAction( makeLabel: (context) => Text(context.l10n.screenshotCurrentPosition), onPressed: (_) async { - final gameId = options.gameAnyId!.gameId; - final state = ref.read(analysisControllerProvider(pgn, options)); + final gameId = broadcastAnalysisOptions.gameAnyId!.gameId; + final state = ref.read( + analysisControllerProvider(pgn, broadcastAnalysisOptions), + ); try { final image = await ref.read(gameShareServiceProvider).screenshotPosition( gameId, - options.orientation, + broadcastAnalysisOptions.orientation, state.position.fen, state.lastMove, ); @@ -982,14 +1041,15 @@ class _StockfishInfo extends ConsumerWidget { } class ServerAnalysisSummary extends ConsumerWidget { - const ServerAnalysisSummary(this.pgn, this.options); + const ServerAnalysisSummary(this.pgn, this.broadcastAnalysisOptions); final String pgn; - final AnalysisOptions options; + final AnalysisOptions broadcastAnalysisOptions; @override Widget build(BuildContext context, WidgetRef ref) { - final ctrlProvider = analysisControllerProvider(pgn, options); + final ctrlProvider = + analysisControllerProvider(pgn, broadcastAnalysisOptions); final playersAnalysis = ref.watch(ctrlProvider.select((value) => value.playersAnalysis)); final pgnHeaders = @@ -999,12 +1059,13 @@ class ServerAnalysisSummary extends ConsumerWidget { return playersAnalysis != null ? ListView( children: [ - if (currentGameAnalysis == options.gameAnyId?.gameId) + if (currentGameAnalysis == + broadcastAnalysisOptions.gameAnyId?.gameId) const Padding( padding: EdgeInsets.only(top: 16.0), child: WaitingForServerAnalysis(), ), - AcplChart(pgn, options), + AcplChart(pgn, broadcastAnalysisOptions), Center( child: SizedBox( width: math.min(MediaQuery.sizeOf(context).width, 500), @@ -1131,7 +1192,8 @@ class ServerAnalysisSummary extends ConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ const Spacer(), - if (currentGameAnalysis == options.gameAnyId?.gameId) + if (currentGameAnalysis == + broadcastAnalysisOptions.gameAnyId?.gameId) const Center( child: Padding( padding: EdgeInsets.symmetric(vertical: 16.0), @@ -1287,10 +1349,10 @@ class _SummaryPlayerName extends StatelessWidget { } class AcplChart extends ConsumerWidget { - const AcplChart(this.pgn, this.options); + const AcplChart(this.pgn, this.broadcastAnalysisOptions); final String pgn; - final AnalysisOptions options; + final AnalysisOptions broadcastAnalysisOptions; @override Widget build(BuildContext context, WidgetRef ref) { @@ -1325,22 +1387,22 @@ class AcplChart extends ConsumerWidget { ); final data = ref.watch( - analysisControllerProvider(pgn, options) + analysisControllerProvider(pgn, broadcastAnalysisOptions) .select((value) => value.acplChartData), ); final rootPly = ref.watch( - analysisControllerProvider(pgn, options) + analysisControllerProvider(pgn, broadcastAnalysisOptions) .select((value) => value.root.position.ply), ); final currentNode = ref.watch( - analysisControllerProvider(pgn, options) + analysisControllerProvider(pgn, broadcastAnalysisOptions) .select((value) => value.currentNode), ); final isOnMainline = ref.watch( - analysisControllerProvider(pgn, options) + analysisControllerProvider(pgn, broadcastAnalysisOptions) .select((value) => value.isOnMainline), ); @@ -1356,12 +1418,12 @@ class AcplChart extends ConsumerWidget { final divisionLines = []; - if (options.division?.middlegame != null) { - if (options.division!.middlegame! > 0) { + if (broadcastAnalysisOptions.division?.middlegame != null) { + if (broadcastAnalysisOptions.division!.middlegame! > 0) { divisionLines.add(phaseVerticalBar(0.0, context.l10n.opening)); divisionLines.add( phaseVerticalBar( - options.division!.middlegame! - 1, + broadcastAnalysisOptions.division!.middlegame! - 1, context.l10n.middlegame, ), ); @@ -1370,11 +1432,11 @@ class AcplChart extends ConsumerWidget { } } - if (options.division?.endgame != null) { - if (options.division!.endgame! > 0) { + if (broadcastAnalysisOptions.division?.endgame != null) { + if (broadcastAnalysisOptions.division!.endgame! > 0) { divisionLines.add( phaseVerticalBar( - options.division!.endgame! - 1, + broadcastAnalysisOptions.division!.endgame! - 1, context.l10n.endgame, ), ); @@ -1416,7 +1478,12 @@ class AcplChart extends ConsumerWidget { ); final closestNodeIndex = closestSpot.x.round(); ref - .read(analysisControllerProvider(pgn, options).notifier) + .read( + analysisControllerProvider( + pgn, + broadcastAnalysisOptions, + ).notifier, + ) .jumpToNthNodeOnMainline(closestNodeIndex); } }, diff --git a/lib/src/view/broadcast/broadcast_boards_tab.dart b/lib/src/view/broadcast/broadcast_boards_tab.dart index e354e6bde8..5016a8a97e 100644 --- a/lib/src/view/broadcast/broadcast_boards_tab.dart +++ b/lib/src/view/broadcast/broadcast_boards_tab.dart @@ -137,9 +137,6 @@ class BroadcastPreview extends ConsumerWidget { return BoardThumbnail( onTap: () { - ref - .read(BroadcastRoundControllerProvider(roundId).notifier) - .setPgn(game.id); pushPlatformRoute( context, builder: (context) => BroadcastAnalysisScreen( From 9e59e489ee1797869a4d38af9c587364e0c79aff Mon Sep 17 00:00:00 2001 From: Noah Date: Sat, 7 Sep 2024 12:23:24 +0200 Subject: [PATCH 285/979] move initialization to initState() --- .../view/analysis/analysis_share_screen.dart | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/lib/src/view/analysis/analysis_share_screen.dart b/lib/src/view/analysis/analysis_share_screen.dart index d5fa171bab..779d265b98 100644 --- a/lib/src/view/analysis/analysis_share_screen.dart +++ b/lib/src/view/analysis/analysis_share_screen.dart @@ -65,6 +65,26 @@ class _EditPgnTagsFormState extends ConsumerState<_EditPgnTagsForm> { final Map _controllers = {}; final Map _focusNodes = {}; + @override + void initState() { + super.initState(); + final ctrlProvider = analysisControllerProvider(widget.pgn, widget.options); + final pgnHeaders = ref.read(ctrlProvider).pgnHeaders; + + for (final entry in pgnHeaders.entries) { + _controllers[entry.key] = TextEditingController(text: entry.value); + _focusNodes[entry.key] = FocusNode(); + _focusNodes[entry.key]!.addListener(() { + if (!_focusNodes[entry.key]!.hasFocus) { + ref.read(ctrlProvider.notifier).updatePgnHeader( + entry.key, + _controllers[entry.key]!.text, + ); + } + }); + } + } + @override void dispose() { for (final controller in _controllers.values) { @@ -123,20 +143,6 @@ class _EditPgnTagsFormState extends ConsumerState<_EditPgnTagsForm> { (e) => showRatings || !_ratingHeaders.contains(e.key), ) .mapIndexed((index, e) { - if (!_controllers.containsKey(e.key)) { - _controllers[e.key] = - TextEditingController(text: e.value); - _focusNodes[e.key] = FocusNode(); - _focusNodes[e.key]!.addListener(() { - if (!_focusNodes[e.key]!.hasFocus) { - ref.read(ctrlProvider.notifier).updatePgnHeader( - e.key, - _controllers[e.key]!.text, - ); - } - }); - } - return Padding( padding: Styles.horizontalBodyPadding .add(const EdgeInsets.only(bottom: 8.0)), From 34428a11e49da64a94c8687a416cf744809f1c6e Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Sat, 7 Sep 2024 12:30:06 +0200 Subject: [PATCH 286/979] fix linting --- lib/src/view/broadcast/broadcast_analysis_screen.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_analysis_screen.dart b/lib/src/view/broadcast/broadcast_analysis_screen.dart index 5425e6d762..905a268d43 100644 --- a/lib/src/view/broadcast/broadcast_analysis_screen.dart +++ b/lib/src/view/broadcast/broadcast_analysis_screen.dart @@ -85,7 +85,7 @@ class BroadcastAnalysisScreen extends ConsumerWidget { if (pgn.hasValue) _EngineDepth( analysisControllerProvider( - pgn.requireValue, broadcastAnalysisOptions), + pgn.requireValue, broadcastAnalysisOptions,), ), AppBarIconButton( onPressed: () => (pgn.hasValue) @@ -95,7 +95,7 @@ class BroadcastAnalysisScreen extends ConsumerWidget { showDragHandle: true, isDismissible: true, builder: (_) => AnalysisSettings( - pgn.requireValue, broadcastAnalysisOptions), + pgn.requireValue, broadcastAnalysisOptions,), ) : null, semanticsLabel: context.l10n.settingsSettings, @@ -142,7 +142,7 @@ class BroadcastAnalysisScreen extends ConsumerWidget { if (pgn.hasValue) _EngineDepth( analysisControllerProvider( - pgn.requireValue, broadcastAnalysisOptions), + pgn.requireValue, broadcastAnalysisOptions,), ), AppBarIconButton( onPressed: () => (pgn.hasValue) @@ -152,7 +152,7 @@ class BroadcastAnalysisScreen extends ConsumerWidget { showDragHandle: true, isDismissible: true, builder: (_) => AnalysisSettings( - pgn.requireValue, broadcastAnalysisOptions), + pgn.requireValue, broadcastAnalysisOptions,), ) : null, semanticsLabel: context.l10n.settingsSettings, From b0d1c32b66214494c7e1d74921cfe8e60a1e8f13 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Sat, 7 Sep 2024 12:34:51 +0200 Subject: [PATCH 287/979] fix formatting --- .../broadcast/broadcast_analysis_screen.dart | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_analysis_screen.dart b/lib/src/view/broadcast/broadcast_analysis_screen.dart index 905a268d43..f141b773ee 100644 --- a/lib/src/view/broadcast/broadcast_analysis_screen.dart +++ b/lib/src/view/broadcast/broadcast_analysis_screen.dart @@ -85,7 +85,9 @@ class BroadcastAnalysisScreen extends ConsumerWidget { if (pgn.hasValue) _EngineDepth( analysisControllerProvider( - pgn.requireValue, broadcastAnalysisOptions,), + pgn.requireValue, + broadcastAnalysisOptions, + ), ), AppBarIconButton( onPressed: () => (pgn.hasValue) @@ -95,7 +97,9 @@ class BroadcastAnalysisScreen extends ConsumerWidget { showDragHandle: true, isDismissible: true, builder: (_) => AnalysisSettings( - pgn.requireValue, broadcastAnalysisOptions,), + pgn.requireValue, + broadcastAnalysisOptions, + ), ) : null, semanticsLabel: context.l10n.settingsSettings, @@ -142,7 +146,9 @@ class BroadcastAnalysisScreen extends ConsumerWidget { if (pgn.hasValue) _EngineDepth( analysisControllerProvider( - pgn.requireValue, broadcastAnalysisOptions,), + pgn.requireValue, + broadcastAnalysisOptions, + ), ), AppBarIconButton( onPressed: () => (pgn.hasValue) @@ -152,7 +158,9 @@ class BroadcastAnalysisScreen extends ConsumerWidget { showDragHandle: true, isDismissible: true, builder: (_) => AnalysisSettings( - pgn.requireValue, broadcastAnalysisOptions,), + pgn.requireValue, + broadcastAnalysisOptions, + ), ) : null, semanticsLabel: context.l10n.settingsSettings, From f6f844f2c83d994ded87977ceaf5c53ca1a34bc3 Mon Sep 17 00:00:00 2001 From: Noah Date: Sat, 7 Sep 2024 19:18:17 +0200 Subject: [PATCH 288/979] fix-castling-960 --- lib/src/model/common/node.dart | 10 +++++++--- test/model/common/node_test.dart | 13 +++++++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/lib/src/model/common/node.dart b/lib/src/model/common/node.dart index 78bdbb96d4..e654fc7d0f 100644 --- a/lib/src/model/common/node.dart +++ b/lib/src/model/common/node.dart @@ -192,10 +192,14 @@ abstract class Node { bool prepend = false, }) { final pos = nodeAt(path).position; - final isKingMove = - move is NormalMove && pos.board.roleAt(move.from) == Role.king; + + final potentialAltCastlingMove = move is NormalMove && + pos.board.roleAt(move.from) == Role.king && + pos.board.roleAt(move.to) != Role.rook; + final convertedMove = - isKingMove ? convertAltCastlingMove(move) ?? move : move; + potentialAltCastlingMove ? convertAltCastlingMove(move) ?? move : move; + final (newPos, newSan) = pos.makeSan(convertedMove); final newNode = Branch( sanMove: SanMove(newSan, convertedMove), diff --git a/test/model/common/node_test.dart b/test/model/common/node_test.dart index 5594321bcd..40a9964d31 100644 --- a/test/model/common/node_test.dart +++ b/test/model/common/node_test.dart @@ -516,6 +516,19 @@ void main() { root.addMoveAt(previousUciPath, move!); expect(root.makePgn(), isNot(initialPng)); }); + test( + 'do not convert castling move if rook is on the alternative castling square', + () { + const pgn = + '[FEN "rnbqkbnr/pppppppp/8/8/8/2NBQ3/PPPPPPPP/2R1KBNR w KQkq - 0 1"]'; + final root = Root.fromPgnGame(PgnGame.parsePgn(pgn)); + final initialPng = root.makePgn(); + final previousUciPath = root.mainlinePath.penultimate; + final move = Move.parse('e1c1'); + root.addMoveAt(previousUciPath, move!); + expect(root.makePgn(), isNot(initialPng)); + expect(root.mainline.last.sanMove.move, move); + }); }); } From 10fca53e84b28af408b224052e75aa7ea0a99d6e Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Sun, 8 Sep 2024 00:01:49 +0200 Subject: [PATCH 289/979] add an orange border around the latest move of a broadcast game --- .../model/analysis/analysis_controller.dart | 24 +++++++++++++++---- .../broadcast/broadcast_game_controller.dart | 1 + lib/src/view/analysis/tree_view.dart | 23 ++++++++++++++---- 3 files changed, 39 insertions(+), 9 deletions(-) diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index 0a017c9dee..d0a2ab95e6 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -40,6 +40,7 @@ class AnalysisOptions with _$AnalysisOptions { int? initialMoveCursor, LightOpening? opening, Division? division, + @Default(false) bool isBroadcast, /// Optional server analysis to display player stats. ({PlayerAnalysis white, PlayerAnalysis black})? serverAnalysis, @@ -137,6 +138,7 @@ class AnalysisController extends _$AnalysisController { variant: options.variant, id: options.id, currentPath: currentPath, + livePath: options.isBroadcast ? currentPath : null, isOnMainline: _root.isOnMainline(currentPath), root: _root.view, currentNode: AnalysisCurrentNode.fromNode(currentNode), @@ -199,11 +201,17 @@ class AnalysisController extends _$AnalysisController { final (newPath, isNewNode) = _root.addMoveAt(path, move); if (newPath != null) { - _setPath( - newPath, - shouldRecomputeRootView: isNewNode, - shouldForceShowVariation: true, - ); + if (state.livePath == state.currentPath) { + _setPath( + newPath, + shouldRecomputeRootView: isNewNode, + shouldForceShowVariation: true, + isBroadcastMove: true, + ); + } else { + _root.promoteAt(newPath, toMainline: true); + state = state.copyWith(livePath: newPath, root: _root.view); + } } } @@ -394,6 +402,7 @@ class AnalysisController extends _$AnalysisController { bool shouldForceShowVariation = false, bool shouldRecomputeRootView = false, bool replaying = false, + bool isBroadcastMove = false, }) { final pathChange = state.currentPath != path; final (currentNode, opening) = _nodeOpeningAt(_root, path); @@ -442,6 +451,7 @@ class AnalysisController extends _$AnalysisController { state = state.copyWith( currentPath: path, + livePath: isBroadcastMove ? path : state.livePath, isOnMainline: _root.isOnMainline(path), currentNode: AnalysisCurrentNode.fromNode(currentNode), currentBranchOpening: opening, @@ -453,6 +463,7 @@ class AnalysisController extends _$AnalysisController { } else { state = state.copyWith( currentPath: path, + livePath: isBroadcastMove ? path : state.livePath, isOnMainline: _root.isOnMainline(path), currentNode: AnalysisCurrentNode.fromNode(currentNode), currentBranchOpening: opening, @@ -665,6 +676,9 @@ class AnalysisState with _$AnalysisState { /// The path to the current node in the analysis view. required UciPath currentPath, + // The path to the current broadcast live move. + required UciPath? livePath, + /// Whether the current path is on the mainline. required bool isOnMainline, diff --git a/lib/src/model/broadcast/broadcast_game_controller.dart b/lib/src/model/broadcast/broadcast_game_controller.dart index 14472562ad..07a80f965b 100644 --- a/lib/src/model/broadcast/broadcast_game_controller.dart +++ b/lib/src/model/broadcast/broadcast_game_controller.dart @@ -17,6 +17,7 @@ const broadcastAnalysisOptions = AnalysisOptions( isLocalEvaluationAllowed: true, orientation: Side.white, variant: Variant.standard, + isBroadcast: true, ); @riverpod diff --git a/lib/src/view/analysis/tree_view.dart b/lib/src/view/analysis/tree_view.dart index 38e3a873f7..846d3ffbd2 100644 --- a/lib/src/view/analysis/tree_view.dart +++ b/lib/src/view/analysis/tree_view.dart @@ -119,6 +119,12 @@ class _InlineTreeViewState extends ConsumerState { analysisPreferencesProvider.select((value) => value.showAnnotations), ); + final broadcastPath = ref.watch( + analysisControllerProvider(widget.pgn, widget.options).select( + (value) => value.livePath, + ), + ); + final List moveWidgets = _buildTreeWidget( widget.pgn, widget.options, @@ -130,6 +136,7 @@ class _InlineTreeViewState extends ConsumerState { startMainline: true, startSideline: false, initialPath: UciPath.empty, + broadcastPath: broadcastPath, ); // trick to make auto-scroll work when returning to the root position @@ -181,6 +188,7 @@ class _InlineTreeViewState extends ConsumerState { required bool shouldShowAnnotations, required bool shouldShowComments, required UciPath initialPath, + required UciPath? broadcastPath, }) { if (nodes.isEmpty) return []; final List widgets = []; @@ -188,6 +196,7 @@ class _InlineTreeViewState extends ConsumerState { final firstChild = nodes.first; final newPath = initialPath + firstChild.id; final currentMove = newPath == currentPath; + final broadcastMove = newPath == broadcastPath; // add the first child widgets.add( @@ -205,6 +214,7 @@ class _InlineTreeViewState extends ConsumerState { startMainline: startMainline, startSideline: startSideline, endSideline: !inMainline && firstChild.children.isEmpty, + isLiveMove: broadcastMove, ), ); @@ -230,6 +240,7 @@ class _InlineTreeViewState extends ConsumerState { startMainline: false, startSideline: true, initialPath: initialPath, + broadcastPath: broadcastPath, ), ), ), @@ -247,6 +258,7 @@ class _InlineTreeViewState extends ConsumerState { startMainline: false, startSideline: true, initialPath: initialPath, + broadcastPath: broadcastPath, ), ); } @@ -265,6 +277,7 @@ class _InlineTreeViewState extends ConsumerState { startMainline: false, startSideline: false, initialPath: newPath, + broadcastPath: broadcastPath, ), ); @@ -421,14 +434,16 @@ class InlineMove extends ConsumerWidget { : Theme.of(context).focusColor, shape: BoxShape.rectangle, borderRadius: borderRadius, - border: - isLiveMove ? Border.all(color: Colors.orange) : null, + border: isLiveMove + ? Border.all(width: 2, color: Colors.orange) + : null, ) : BoxDecoration( shape: BoxShape.rectangle, borderRadius: borderRadius, - border: - isLiveMove ? Border.all(color: Colors.orange) : null, + border: isLiveMove + ? Border.all(width: 2, color: Colors.orange) + : null, ), child: Text( moveWithNag, From 040eca05cdc2b80786ea66bec1567e77e1d072bf Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Sun, 8 Sep 2024 22:48:44 +0200 Subject: [PATCH 290/979] show correct clocks on analysis screen board --- .../model/analysis/analysis_controller.dart | 20 +++++++++++++-- .../broadcast/broadcast_game_controller.dart | 4 ++- lib/src/model/common/node.dart | 2 ++ .../broadcast/broadcast_analysis_screen.dart | 25 ++++++++++++++++--- 4 files changed, 44 insertions(+), 7 deletions(-) diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index d0a2ab95e6..55c321daf3 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -153,6 +153,7 @@ class AnalysisController extends _$AnalysisController { displayMode: DisplayMode.moves, playersAnalysis: options.serverAnalysis, acplChartData: _makeAcplChartData(), + clocks: options.isBroadcast ? _makeClocks(currentPath) : null, ); if (analysisState.isEngineAvailable) { @@ -197,8 +198,8 @@ class AnalysisController extends _$AnalysisController { } } - void onBroadcastMove(UciPath path, Move move) { - final (newPath, isNewNode) = _root.addMoveAt(path, move); + void onBroadcastMove(UciPath path, Move move, Duration? clock) { + final (newPath, isNewNode) = _root.addMoveAt(path, move, clock: clock); if (newPath != null) { if (state.livePath == state.currentPath) { @@ -459,6 +460,7 @@ class AnalysisController extends _$AnalysisController { lastMove: currentNode.sanMove.move, promotionMove: null, root: rootView, + clocks: options.isBroadcast ? _makeClocks(path) : null, ); } else { state = state.copyWith( @@ -471,6 +473,7 @@ class AnalysisController extends _$AnalysisController { lastMove: null, promotionMove: null, root: rootView, + clocks: options.isBroadcast ? _makeClocks(path) : null, ); } @@ -645,6 +648,16 @@ class AnalysisController extends _$AnalysisController { ).toList(growable: false); return list.isEmpty ? null : IList(list); } + + ({Duration? parentClock, Duration? clock}) _makeClocks(UciPath path) { + final nodeView = _root.nodeAt(path).view; + final parentView = _root.parentAt(path).view; + + return ( + parentClock: (parentView is ViewBranch) ? parentView.clock : null, + clock: (nodeView is ViewBranch) ? nodeView.clock : null, + ); + } } enum DisplayMode { @@ -694,6 +707,9 @@ class AnalysisState with _$AnalysisState { /// Whether to show the ACPL chart instead of tree view. required DisplayMode displayMode, + /// Clocks if avaible. Only used by the broadcast analysis screen. + ({Duration? parentClock, Duration? clock})? clocks, + /// The last move played. Move? lastMove, diff --git a/lib/src/model/broadcast/broadcast_game_controller.dart b/lib/src/model/broadcast/broadcast_game_controller.dart index 07a80f965b..12d22b2101 100644 --- a/lib/src/model/broadcast/broadcast_game_controller.dart +++ b/lib/src/model/broadcast/broadcast_game_controller.dart @@ -77,13 +77,15 @@ class BroadcastGameController extends _$BroadcastGameController { // The path for the node that was received final path = pick(event.data, 'p', 'path').asUciPathOrThrow(); final uciMove = pick(event.data, 'n', 'uci').asUciMoveOrThrow(); + final clock = + pick(event.data, 'n', 'clock').asDurationFromCentiSecondsOrNull(); final ctrlProviderNotifier = ref.read( analysisControllerProvider(state.requireValue, broadcastAnalysisOptions) .notifier, ); - ctrlProviderNotifier.onBroadcastMove(path, uciMove); + ctrlProviderNotifier.onBroadcastMove(path, uciMove, clock); } void _handleSetTagsEvent(SocketEvent event) { diff --git a/lib/src/model/common/node.dart b/lib/src/model/common/node.dart index 78bdbb96d4..068df987cb 100644 --- a/lib/src/model/common/node.dart +++ b/lib/src/model/common/node.dart @@ -190,6 +190,7 @@ abstract class Node { UciPath path, Move move, { bool prepend = false, + Duration? clock, }) { final pos = nodeAt(path).position; final isKingMove = @@ -200,6 +201,7 @@ abstract class Node { final newNode = Branch( sanMove: SanMove(newSan, convertedMove), position: newPos, + comments: (clock != null) ? [PgnComment(clock: clock)] : null, ); return addNodeAt(path, newNode, prepend: prepend); } diff --git a/lib/src/view/broadcast/broadcast_analysis_screen.dart b/lib/src/view/broadcast/broadcast_analysis_screen.dart index f141b773ee..703c8af42f 100644 --- a/lib/src/view/broadcast/broadcast_analysis_screen.dart +++ b/lib/src/view/broadcast/broadcast_analysis_screen.dart @@ -255,6 +255,7 @@ class _Body extends ConsumerWidget { child: Row( children: [ _AnalysisBoardPlayersAndClocks( + ctrlProvider, roundId, gameId, pgn, @@ -314,6 +315,7 @@ class _Body extends ConsumerWidget { kTabletBoardTableSidePadding, ), child: _AnalysisBoardPlayersAndClocks( + ctrlProvider, roundId, gameId, pgn, @@ -324,6 +326,7 @@ class _Body extends ConsumerWidget { ) else _AnalysisBoardPlayersAndClocks( + ctrlProvider, roundId, gameId, pgn, @@ -364,6 +367,7 @@ class _Body extends ConsumerWidget { enum _PlayerWidgetSide { bottom, top } class _AnalysisBoardPlayersAndClocks extends ConsumerWidget { + final AnalysisControllerProvider ctrlProvider; final BroadcastRoundId roundId; final BroadcastGameId gameId; final String pgn; @@ -372,6 +376,7 @@ class _AnalysisBoardPlayersAndClocks extends ConsumerWidget { final bool isTablet; const _AnalysisBoardPlayersAndClocks( + this.ctrlProvider, this.roundId, this.gameId, this.pgn, @@ -382,6 +387,9 @@ class _AnalysisBoardPlayersAndClocks extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final clocks = ref.watch(ctrlProvider.select((value) => value.clocks)); + final playingSide = + ref.watch(ctrlProvider.select((value) => value.position.turn)); final game = ref.watch( broadcastRoundControllerProvider(roundId) .select((game) => game.value?[gameId]), @@ -391,10 +399,14 @@ class _AnalysisBoardPlayersAndClocks extends ConsumerWidget { children: [ if (game != null) _PlayerWidget( + clock: (playingSide == pov.opposite) + ? clocks?.parentClock + : clocks?.clock, width: boardSize, game: game, side: pov.opposite, boardSide: _PlayerWidgetSide.top, + playingSide: playingSide, ), AnalysisBoard( pgn, @@ -404,10 +416,12 @@ class _AnalysisBoardPlayersAndClocks extends ConsumerWidget { ), if (game != null) _PlayerWidget( + clock: (playingSide == pov) ? clocks?.parentClock : clocks?.clock, width: boardSize, game: game, side: pov, boardSide: _PlayerWidgetSide.bottom, + playingSide: playingSide, ), ], ); @@ -417,21 +431,24 @@ class _AnalysisBoardPlayersAndClocks extends ConsumerWidget { class _PlayerWidget extends StatelessWidget { const _PlayerWidget({ required this.width, + required this.clock, required this.game, required this.side, required this.boardSide, + required this.playingSide, }); final BroadcastGame game; + final Duration? clock; final Side side; final double width; final _PlayerWidgetSide boardSide; + final Side playingSide; @override Widget build(BuildContext context) { final player = game.players[side]!; final gameStatus = game.status; - final playingSide = game.playingSide; return SizedBox( width: width, @@ -513,8 +530,8 @@ class _PlayerWidget extends StatelessWidget { ], ), ), - if (player.clock != null) - (side == playingSide) + if (clock != null) + (side == playingSide && game.isPlaying) ? Text( game.timeLeft!.toHoursMinutesSeconds(), style: TextStyle( @@ -523,7 +540,7 @@ class _PlayerWidget extends StatelessWidget { ), ) : Text( - player.clock!.toHoursMinutesSeconds(), + clock!.toHoursMinutesSeconds(), style: const TextStyle( fontFeatures: [FontFeature.tabularFigures()], ), From b012e4c650dac5a18ceb79f5a2e6a25cb0b57818 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Mon, 9 Sep 2024 01:27:09 +0200 Subject: [PATCH 291/979] improve player widget design on broadcast analysis screen and show only the orange box around last move when the game is still ongoing --- .../model/analysis/analysis_controller.dart | 4 +- .../broadcast/broadcast_analysis_screen.dart | 236 ++++++++++++------ 2 files changed, 159 insertions(+), 81 deletions(-) diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index 55c321daf3..8271de4e1f 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -138,7 +138,9 @@ class AnalysisController extends _$AnalysisController { variant: options.variant, id: options.id, currentPath: currentPath, - livePath: options.isBroadcast ? currentPath : null, + livePath: options.isBroadcast && pgnHeaders['Result'] == '*' + ? currentPath + : null, isOnMainline: _root.isOnMainline(currentPath), root: _root.view, currentNode: AnalysisCurrentNode.fromNode(currentNode), diff --git a/lib/src/view/broadcast/broadcast_analysis_screen.dart b/lib/src/view/broadcast/broadcast_analysis_screen.dart index 703c8af42f..f8cc1ad5e2 100644 --- a/lib/src/view/broadcast/broadcast_analysis_screen.dart +++ b/lib/src/view/broadcast/broadcast_analysis_screen.dart @@ -388,6 +388,12 @@ class _AnalysisBoardPlayersAndClocks extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final clocks = ref.watch(ctrlProvider.select((value) => value.clocks)); + final currentPath = ref.watch( + ctrlProvider.select((value) => value.currentPath), + ); + final livePath = ref.watch( + ctrlProvider.select((value) => value.livePath), + ); final playingSide = ref.watch(ctrlProvider.select((value) => value.position.turn)); final game = ref.watch( @@ -407,6 +413,7 @@ class _AnalysisBoardPlayersAndClocks extends ConsumerWidget { side: pov.opposite, boardSide: _PlayerWidgetSide.top, playingSide: playingSide, + playClock: currentPath == livePath, ), AnalysisBoard( pgn, @@ -422,6 +429,7 @@ class _AnalysisBoardPlayersAndClocks extends ConsumerWidget { side: pov, boardSide: _PlayerWidgetSide.bottom, playingSide: playingSide, + playClock: currentPath == livePath, ), ], ); @@ -436,6 +444,7 @@ class _PlayerWidget extends StatelessWidget { required this.side, required this.boardSide, required this.playingSide, + required this.playClock, }); final BroadcastGame game; @@ -444,6 +453,7 @@ class _PlayerWidget extends StatelessWidget { final double width; final _PlayerWidgetSide boardSide; final Side playingSide; + final bool playClock; @override Widget build(BuildContext context) { @@ -452,102 +462,168 @@ class _PlayerWidget extends StatelessWidget { return SizedBox( width: width, - child: Card( - margin: EdgeInsets.zero, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: - Radius.circular(boardSide == _PlayerWidgetSide.top ? 8 : 0), - topRight: - Radius.circular(boardSide == _PlayerWidgetSide.top ? 8 : 0), - bottomLeft: - Radius.circular(boardSide == _PlayerWidgetSide.bottom ? 8 : 0), - bottomRight: - Radius.circular(boardSide == _PlayerWidgetSide.bottom ? 8 : 0), - ), - ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( + child: Row( + children: [ + if (gameStatus != null && gameStatus != '*') + Card( + margin: EdgeInsets.zero, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular( + boardSide == _PlayerWidgetSide.top ? 8 : 0, + ), + bottomLeft: Radius.circular( + boardSide == _PlayerWidgetSide.bottom ? 8 : 0, + ), + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8.0, + vertical: 4.0, + ), + child: Text( + (gameStatus == '½-½') + ? '½' + : (gameStatus == '1-0') + ? side == Side.white + ? '1' + : '0' + : side == Side.black + ? '1' + : '0', + style: + const TextStyle().copyWith(fontWeight: FontWeight.bold), + )), + ), + Expanded( + child: Card( + margin: EdgeInsets.zero, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular( + boardSide == _PlayerWidgetSide.top && + (gameStatus == null || gameStatus == '*') + ? 8 + : 0, + ), + topRight: Radius.circular( + boardSide == _PlayerWidgetSide.top && clock == null ? 8 : 0, + ), + bottomLeft: Radius.circular( + boardSide == _PlayerWidgetSide.bottom && + (gameStatus == null || gameStatus == '*') + ? 8 + : 0, + ), + bottomRight: Radius.circular( + boardSide == _PlayerWidgetSide.bottom && clock == null + ? 8 + : 0, + ), + ), + ), + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - if (gameStatus != null && gameStatus != '*') ...[ - Text( - (gameStatus == '½-½') - ? '½' - : (gameStatus == '1-0') - ? side == Side.white - ? '1' - : '0' - : side == Side.black - ? '1' - : '0', - style: const TextStyle() - .copyWith(fontWeight: FontWeight.bold), - ), - const SizedBox(width: 15), - ], - if (player.federation != null) ...[ - Consumer( - builder: (context, widgetRef, _) { - return SvgPicture.network( - lichessFideFedSrc(player.federation!), - height: 12, - httpClient: widgetRef.read(defaultClientProvider), - ); - }, - ), - ], - const SizedBox(width: 5), - if (player.title != null) ...[ - Text( - player.title!, - style: const TextStyle().copyWith( - color: context.lichessColors.brag, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(width: 5), - ], - Text( - player.name, - style: const TextStyle().copyWith( - fontWeight: FontWeight.bold, + Expanded( + child: Row( + children: [ + if (player.federation != null) ...[ + Consumer( + builder: (context, widgetRef, _) { + return SvgPicture.network( + lichessFideFedSrc(player.federation!), + height: 12, + httpClient: + widgetRef.read(defaultClientProvider), + ); + }, + ), + const SizedBox(width: 5), + ], + if (player.title != null) ...[ + Text( + player.title!, + style: const TextStyle().copyWith( + color: context.lichessColors.brag, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(width: 5), + ], + Text( + player.name, + style: const TextStyle().copyWith( + fontWeight: FontWeight.bold, + ), + overflow: TextOverflow.ellipsis, + ), + if (player.rating != null) ...[ + const SizedBox(width: 5), + Text( + player.rating.toString(), + style: const TextStyle(), + overflow: TextOverflow.ellipsis, + ), + ], + ], ), - overflow: TextOverflow.ellipsis, ), - if (player.rating != null) ...[ - const SizedBox(width: 5), - Text( - player.rating.toString(), - style: const TextStyle(), - overflow: TextOverflow.ellipsis, - ), - ], ], ), ), - if (clock != null) - (side == playingSide && game.isPlaying) + ), + ), + if (clock != null) + Card( + color: (side == playingSide) + ? playClock + ? Theme.of(context).colorScheme.tertiaryContainer + : Theme.of(context).colorScheme.secondaryContainer + : null, + margin: EdgeInsets.zero, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topRight: Radius.circular( + boardSide == _PlayerWidgetSide.top ? 8 : 0, + ), + bottomRight: Radius.circular( + boardSide == _PlayerWidgetSide.bottom ? 8 : 0, + ), + ), + ), + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), + child: (side == playingSide && playClock) ? Text( game.timeLeft!.toHoursMinutesSeconds(), style: TextStyle( - color: Colors.orange[900], - fontFeatures: const [FontFeature.tabularFigures()], + color: + Theme.of(context).colorScheme.onTertiaryContainer, + fontFeatures: const [ + FontFeature.tabularFigures(), + ], ), ) : Text( clock!.toHoursMinutesSeconds(), - style: const TextStyle( - fontFeatures: [FontFeature.tabularFigures()], + style: TextStyle( + color: (side == playingSide) + ? Theme.of(context) + .colorScheme + .onSecondaryContainer + : null, + fontFeatures: const [FontFeature.tabularFigures()], ), ), - ], - ), - ), + ), + ), + ], ), ); } From 83ca78eeb378da159eb459878383fb541730c0eb Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Mon, 9 Sep 2024 08:13:18 +0200 Subject: [PATCH 292/979] fix negative duration case --- lib/src/view/broadcast/broadcasts_list_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/view/broadcast/broadcasts_list_screen.dart b/lib/src/view/broadcast/broadcasts_list_screen.dart index d3ee9bd530..c055d5f517 100644 --- a/lib/src/view/broadcast/broadcasts_list_screen.dart +++ b/lib/src/view/broadcast/broadcasts_list_screen.dart @@ -311,7 +311,7 @@ class StartsRoundDate extends ConsumerWidget { final timeBeforeRound = startsAt.difference(DateTime.now()); return Text( - timeBeforeRound.inDays == 0 + (!timeBeforeRound.isNegative && timeBeforeRound.inDays == 0) ? timeBeforeRound.inHours == 0 ? 'In ${timeBeforeRound.inMinutes} minutes' // TODO translate with https://github.com/lichess-org/lila/blob/65b28ea8e43e0133df6c7ed40e03c2954f247d1e/translation/source/timeago.xml#L8 : 'In ${timeBeforeRound.inHours} hours' // TODO translate with https://github.com/lichess-org/lila/blob/65b28ea8e43e0133df6c7ed40e03c2954f247d1e/translation/source/timeago.xml#L12 From 6c07742593c124e86dcb9609c8c83c18c353994a Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 9 Sep 2024 09:50:21 +0200 Subject: [PATCH 293/979] Refactor and add date picker to pgn share screen --- .../view/analysis/analysis_share_screen.dart | 272 ++++++++++++------ 1 file changed, 192 insertions(+), 80 deletions(-) diff --git a/lib/src/view/analysis/analysis_share_screen.dart b/lib/src/view/analysis/analysis_share_screen.dart index 1889d01cdc..397ca5b043 100644 --- a/lib/src/view/analysis/analysis_share_screen.dart +++ b/lib/src/view/analysis/analysis_share_screen.dart @@ -1,7 +1,9 @@ import 'package:collection/collection.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/model/account/account_preferences.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; @@ -12,6 +14,8 @@ import 'package:lichess_mobile/src/widgets/adaptive_text_field.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; +final _dateFormatter = DateFormat('yyyy.MM.dd'); + class AnalysisShareScreen extends StatelessWidget { const AnalysisShareScreen({required this.pgn, required this.options}); @@ -87,21 +91,22 @@ class _EditPgnTagsFormState extends ConsumerState<_EditPgnTagsForm> { final pgnHeaders = ref.watch(ctrlProvider.select((c) => c.pgnHeaders)); final showRatingAsync = ref.watch(showRatingsPrefProvider); - /// Moves focus to the next field or shows a picker for 'Result'. void focusAndSelectNextField(int index, IMap pgnHeaders) { if (index + 1 < pgnHeaders.entries.length) { final nextEntry = pgnHeaders.entries.elementAt(index + 1); - if (nextEntry.key == 'Result') { - showChoicePicker( - context, - choices: ['1-0', '0-1', '1/2-1/2', '*'], - selectedItem: nextEntry.value, - labelBuilder: (choice) => Text(choice), - onSelectedItemChanged: (choice) { - ref - .read(ctrlProvider.notifier) - .updatePgnHeader(nextEntry.key, choice); - _controllers[nextEntry.key]!.text = choice; + if (nextEntry.key == 'Date') { + _showDatePicker( + nextEntry, + context: context, + onEntryChanged: () { + focusAndSelectNextField(index + 1, pgnHeaders); + }, + ); + } else if (nextEntry.key == 'Result') { + _showResultChoicePicker( + nextEntry, + context: context, + onEntryChanged: () { focusAndSelectNextField(index + 1, pgnHeaders); }, ); @@ -128,74 +133,39 @@ class _EditPgnTagsFormState extends ConsumerState<_EditPgnTagsForm> { (e) => showRatings || !_ratingHeaders.contains(e.key), ) .mapIndexed((index, e) { - return Padding( - padding: Styles.horizontalBodyPadding - .add(const EdgeInsets.only(bottom: 8.0)), - child: Row( - children: [ - SizedBox( - width: 110, - child: Text( - e.key, - style: const TextStyle( - fontWeight: FontWeight.bold, - overflow: TextOverflow.ellipsis, - ), - ), - ), - const SizedBox(width: 8), - Expanded( - child: AdaptiveTextField( - focusNode: _focusNodes[e.key], - cupertinoDecoration: BoxDecoration( - color: CupertinoColors.tertiarySystemBackground, - border: Border.all( - color: CupertinoColors.systemGrey4, - width: 1, - ), - borderRadius: BorderRadius.circular(8), - ), - controller: _controllers[e.key], - textInputAction: TextInputAction.next, - keyboardType: - e.key == 'WhiteElo' || e.key == 'BlackElo' - ? TextInputType.number - : TextInputType.text, - onTap: () { - _controllers[e.key]!.selection = TextSelection( - baseOffset: 0, - extentOffset: - _controllers[e.key]!.text.length, - ); - if (e.key == 'Result') { - showChoicePicker( - context, - choices: ['1-0', '0-1', '1/2-1/2', '*'], - selectedItem: e.value, - labelBuilder: (choice) => Text(choice), - onSelectedItemChanged: (choice) { - ref - .read(ctrlProvider.notifier) - .updatePgnHeader(e.key, choice); - _controllers[e.key]!.text = choice; - focusAndSelectNextField( - index, - pgnHeaders, - ); - }, - ); - } - }, - onSubmitted: (value) { - ref - .read(ctrlProvider.notifier) - .updatePgnHeader(e.key, value); - focusAndSelectNextField(index, pgnHeaders); - }, - ), - ), - ], - ), + return _EditablePgnField( + entry: e, + controller: _controllers[e.key]!, + focusNode: _focusNodes[e.key]!, + onTap: () { + _controllers[e.key]!.selection = TextSelection( + baseOffset: 0, + extentOffset: _controllers[e.key]!.text.length, + ); + if (e.key == 'Result') { + _showResultChoicePicker( + e, + context: context, + onEntryChanged: () { + focusAndSelectNextField(index, pgnHeaders); + }, + ); + } else if (e.key == 'Date') { + _showDatePicker( + e, + context: context, + onEntryChanged: () { + focusAndSelectNextField(index, pgnHeaders); + }, + ); + } + }, + onSubmitted: (value) { + ref + .read(ctrlProvider.notifier) + .updatePgnHeader(e.key, value); + focusAndSelectNextField(index, pgnHeaders); + }, ); }).toList(), ), @@ -231,4 +201,146 @@ class _EditPgnTagsFormState extends ConsumerState<_EditPgnTagsForm> { orElse: () => const SizedBox.shrink(), ); } + + Future _showDatePicker( + MapEntry entry, { + required BuildContext context, + required void Function() onEntryChanged, + }) { + final ctrlProvider = analysisControllerProvider(widget.pgn, widget.options); + if (Theme.of(context).platform == TargetPlatform.iOS) { + return showCupertinoModalPopup( + context: context, + builder: (BuildContext context) => Container( + height: 216, + padding: const EdgeInsets.only(top: 6.0), + margin: EdgeInsets.only( + bottom: MediaQuery.viewInsetsOf(context).bottom, + ), + color: CupertinoColors.systemBackground.resolveFrom(context), + child: SafeArea( + top: false, + child: CupertinoDatePicker( + mode: CupertinoDatePickerMode.date, + initialDateTime: entry.value.isNotEmpty + ? _dateFormatter.parse(entry.value) + : DateTime.now(), + onDateTimeChanged: (DateTime newDateTime) { + final newDate = _dateFormatter.format(newDateTime); + ref.read(ctrlProvider.notifier).updatePgnHeader( + entry.key, + newDate, + ); + _controllers[entry.key]!.text = newDate; + }, + ), + ), + ), + ).then((_) { + onEntryChanged(); + }); + } else { + return showDatePicker( + context: context, + initialDate: entry.value.isNotEmpty + ? _dateFormatter.parse(entry.value) + : DateTime.now(), + firstDate: DateTime(1900), + lastDate: DateTime(2100), + ).then((date) { + if (date != null) { + final formatted = _dateFormatter.format(date); + ref.read(ctrlProvider.notifier).updatePgnHeader(entry.key, formatted); + _controllers[entry.key]!.text = formatted; + } + }).then((_) { + onEntryChanged(); + }); + } + } + + Future _showResultChoicePicker( + MapEntry entry, { + required BuildContext context, + required void Function() onEntryChanged, + }) { + return showChoicePicker( + context, + choices: ['1-0', '0-1', '1/2-1/2', '*'], + selectedItem: entry.value, + labelBuilder: (choice) => Text(choice), + onSelectedItemChanged: (choice) { + ref + .read( + analysisControllerProvider(widget.pgn, widget.options).notifier, + ) + .updatePgnHeader( + entry.key, + choice, + ); + _controllers[entry.key]!.text = choice; + }, + ).then((_) { + onEntryChanged(); + }); + } +} + +class _EditablePgnField extends StatelessWidget { + const _EditablePgnField({ + required this.entry, + required this.controller, + required this.focusNode, + required this.onTap, + required this.onSubmitted, + }); + + final MapEntry entry; + final TextEditingController controller; + final FocusNode focusNode; + final GestureTapCallback onTap; + final ValueChanged onSubmitted; + + @override + Widget build(BuildContext context) { + return Padding( + padding: + Styles.horizontalBodyPadding.add(const EdgeInsets.only(bottom: 8.0)), + child: Row( + children: [ + SizedBox( + width: 110, + child: Text( + entry.key, + style: const TextStyle( + fontWeight: FontWeight.bold, + overflow: TextOverflow.ellipsis, + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: AdaptiveTextField( + focusNode: focusNode, + cupertinoDecoration: BoxDecoration( + color: CupertinoColors.tertiarySystemBackground, + border: Border.all( + color: CupertinoColors.systemGrey4, + width: 1, + ), + borderRadius: BorderRadius.circular(8), + ), + controller: controller, + textInputAction: TextInputAction.next, + keyboardType: entry.key == 'WhiteElo' || entry.key == 'BlackElo' + ? TextInputType.number + : TextInputType.text, + onTap: onTap, + onSubmitted: onSubmitted, + ), + ), + ], + ), + ); + } } From 9dc7df076e8e1eb9a5b1e03204e5dfcd8b88de74 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 9 Sep 2024 10:02:58 +0200 Subject: [PATCH 294/979] Ensure atomic state updates --- .../board_editor/board_editor_controller.dart | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/src/model/board_editor/board_editor_controller.dart b/lib/src/model/board_editor/board_editor_controller.dart index fcb8259051..f0459c4c2d 100644 --- a/lib/src/model/board_editor/board_editor_controller.dart +++ b/lib/src/model/board_editor/board_editor_controller.dart @@ -60,8 +60,8 @@ class BoardEditorController extends _$BoardEditorController { void setSideToPlay(Side side) { state = state.copyWith( sideToPlay: side, + enPassantOptions: _calculateEnPassantOptions(state.pieces, side), ); - _calculateEnPassantOptions(); } void loadFen(String fen) { @@ -69,9 +69,7 @@ class BoardEditorController extends _$BoardEditorController { } /// Calculates the squares where an en passant capture could be possible. - void _calculateEnPassantOptions() { - final side = state.sideToPlay; - final pieces = state.pieces; + SquareSet _calculateEnPassantOptions(IMap pieces, Side side) { SquareSet enPassantOptions = SquareSet.empty; final boardFen = writeFen(pieces.unlock); final board = Board.parseFen(boardFen); @@ -109,7 +107,7 @@ class BoardEditorController extends _$BoardEditorController { } }); - state = state.copyWith(enPassantOptions: enPassantOptions); + return enPassantOptions; } void toggleEnPassantSquare(Square square) { @@ -119,8 +117,10 @@ class BoardEditorController extends _$BoardEditorController { } void _updatePosition(IMap pieces) { - state = state.copyWith(pieces: pieces); - _calculateEnPassantOptions(); + state = state.copyWith( + pieces: pieces, + enPassantOptions: _calculateEnPassantOptions(pieces, state.sideToPlay), + ); } void setCastling(Side side, CastlingSide castlingSide, bool allowed) { From 0755e79c5a4ccd8fc71963de378b6944b2a0c2d9 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 9 Sep 2024 11:12:18 +0200 Subject: [PATCH 295/979] Fix explorer prev and next button unexpected behaviour Fixes #994 --- .../model/analysis/analysis_controller.dart | 49 +++++++++++++++---- lib/src/model/engine/evaluation_service.dart | 15 +++--- lib/src/view/analysis/analysis_screen.dart | 7 ++- .../view/analysis/analysis_share_screen.dart | 2 +- lib/src/view/tools/tools_tab_screen.dart | 2 +- .../opening_explorer_screen_test.dart | 2 +- 6 files changed, 56 insertions(+), 21 deletions(-) diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index 93ddc71ec7..c42e9d6009 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -26,13 +26,19 @@ part 'analysis_controller.freezed.dart'; part 'analysis_controller.g.dart'; const standaloneAnalysisId = StringId('standalone_analysis'); +const standaloneOpeningExplorerId = StringId('standalone_opening_explorer'); + final _dateFormat = DateFormat('yyyy.MM.dd'); +/// Whether the analysis is a standalone analysis (not a lichess game analysis). +bool _isStandaloneAnalysis(StringId id) => + id == standaloneAnalysisId || id == standaloneOpeningExplorerId; + @freezed class AnalysisOptions with _$AnalysisOptions { const AnalysisOptions._(); const factory AnalysisOptions({ - /// The ID of the analysis. Can be a game ID or a standalone analysis ID. + /// The ID of the analysis. Can be a game ID or a standalone ID. required StringId id, required bool isLocalEvaluationAllowed, required Side orientation, @@ -50,7 +56,7 @@ class AnalysisOptions with _$AnalysisOptions { /// The game ID of the analysis, if it's a lichess game. GameAnyId? get gameAnyId => - id != standaloneAnalysisId ? GameAnyId(id.value) : null; + _isStandaloneAnalysis(id) ? null : GameAnyId(id.value); } @riverpod @@ -66,10 +72,15 @@ class AnalysisController extends _$AnalysisController { final evaluationService = ref.watch(evaluationServiceProvider); final serverAnalysisService = ref.watch(serverAnalysisServiceProvider); + final isEngineAllowed = options.isLocalEvaluationAllowed && + engineSupportedVariants.contains(options.variant); + ref.onDispose(() { _startEngineEvalTimer?.cancel(); _engineEvalDebounce.dispose(); - evaluationService.disposeEngine(); + if (isEngineAllowed) { + evaluationService.disposeEngine(); + } serverAnalysisService.lastAnalysisEvent .removeListener(_listenToServerAnalysisEvents); }); @@ -184,7 +195,11 @@ class AnalysisController extends _$AnalysisController { return; } - final (newPath, isNewNode) = _root.addMoveAt(state.currentPath, move); + // For the opening explorer, last played move should always be the mainline + final shouldPrepend = options.id == standaloneOpeningExplorerId; + + final (newPath, isNewNode) = + _root.addMoveAt(state.currentPath, move, prepend: shouldPrepend); if (newPath != null) { _setPath( newPath, @@ -372,10 +387,16 @@ class AnalysisController extends _$AnalysisController { } } - String makeGamePgn() { + /// Makes a full PGN string (including headers and comments) of the current game state. + String makeExportPgn() { return _root.makePgn(state.pgnHeaders, state.pgnRootComments); } + /// Makes an internal PGN string (without headers and comments) of the current game state. + String makeInternalPgn() { + return _root.makePgn(); + } + void _setPath( UciPath path, { bool shouldForceShowVariation = false, @@ -679,6 +700,13 @@ class AnalysisState with _$AnalysisState { IList? pgnRootComments, }) = _AnalysisState; + /// The game ID of the analysis, if it's a lichess game. + GameAnyId? get gameAnyId => + _isStandaloneAnalysis(id) ? null : GameAnyId(id.value); + + /// Whether the analysis is for a lichess game. + bool get isLichessGameAnalysis => gameAnyId != null; + IMap> get validMoves => makeLegalMoves(currentNode.position); @@ -686,7 +714,7 @@ class AnalysisState with _$AnalysisState { /// /// It must be a lichess game, which is finished and not already analyzed. bool get canRequestServerAnalysis => - id != standaloneAnalysisId && + gameAnyId != null && (id.length == 8 || id.length == 12) && !hasServerAnalysis && pgnHeaders['Result'] != '*'; @@ -702,11 +730,12 @@ class AnalysisState with _$AnalysisState { acplChartData != null && acplChartData!.isNotEmpty); + /// Whether the engine is allowed for this analysis and variant. + bool get isEngineAllowed => + isLocalEvaluationAllowed && engineSupportedVariants.contains(variant); + /// Whether the engine is available for evaluation - bool get isEngineAvailable => - isLocalEvaluationAllowed && - engineSupportedVariants.contains(variant) && - isLocalEvaluationEnabled; + bool get isEngineAvailable => isEngineAllowed && isLocalEvaluationEnabled; Position get position => currentNode.position; bool get canGoNext => currentNode.hasChild; diff --git a/lib/src/model/engine/evaluation_service.dart b/lib/src/model/engine/evaluation_service.dart index 8ac29f59fb..346a8fa642 100644 --- a/lib/src/model/engine/evaluation_service.dart +++ b/lib/src/model/engine/evaluation_service.dart @@ -64,9 +64,11 @@ class EvaluationService { Engine Function() engineFactory = StockfishEngine.new, EvaluationOptions? options, }) async { + if (context != _context) { + await disposeEngine(); + } _context = context; if (options != null) _options = options; - await (_engine?.dispose() ?? Future.value()); _engine = engineFactory.call(); _engine!.state.addListener(() { debugPrint('Engine state: ${_engine?.state.value}'); @@ -89,11 +91,12 @@ class EvaluationService { _options = options; } - void disposeEngine() { - _engine?.dispose().then((_) { - _engine = null; - }); - _context = null; + Future disposeEngine() { + return _engine?.dispose().then((_) { + _engine = null; + _context = null; + }) ?? + Future.value(); } /// Start the engine evaluation with the given [path] and [steps]. diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index 27d2a1142c..55f513bcc0 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -604,8 +604,11 @@ class _BottomBar extends ConsumerWidget { pushPlatformRoute( context, builder: (_) => OpeningExplorerScreen( - pgn: pgn, - options: options, + pgn: ref.read(ctrlProvider.notifier).makeInternalPgn(), + options: options.copyWith( + isLocalEvaluationAllowed: false, + id: standaloneOpeningExplorerId, + ), ), ); } diff --git a/lib/src/view/analysis/analysis_share_screen.dart b/lib/src/view/analysis/analysis_share_screen.dart index 397ca5b043..97e626ad3f 100644 --- a/lib/src/view/analysis/analysis_share_screen.dart +++ b/lib/src/view/analysis/analysis_share_screen.dart @@ -185,7 +185,7 @@ class _EditPgnTagsFormState extends ConsumerState<_EditPgnTagsForm> { widget.options, ).notifier, ) - .makeGamePgn(), + .makeExportPgn(), ); }, child: Text(context.l10n.mobileShareGamePGN), diff --git a/lib/src/view/tools/tools_tab_screen.dart b/lib/src/view/tools/tools_tab_screen.dart index 2e7b30aa0f..a7073299b0 100644 --- a/lib/src/view/tools/tools_tab_screen.dart +++ b/lib/src/view/tools/tools_tab_screen.dart @@ -168,7 +168,7 @@ class _Body extends ConsumerWidget { isLocalEvaluationAllowed: false, variant: Variant.standard, orientation: Side.white, - id: standaloneAnalysisId, + id: standaloneOpeningExplorerId, ), ), ), diff --git a/test/view/opening_explorer/opening_explorer_screen_test.dart b/test/view/opening_explorer/opening_explorer_screen_test.dart index 73641be9e7..a18e72aef7 100644 --- a/test/view/opening_explorer/opening_explorer_screen_test.dart +++ b/test/view/opening_explorer/opening_explorer_screen_test.dart @@ -32,7 +32,7 @@ void main() { }); const options = AnalysisOptions( - id: standaloneAnalysisId, + id: standaloneOpeningExplorerId, isLocalEvaluationAllowed: false, orientation: Side.white, variant: Variant.standard, From c3d215a564fbe2a5a439a2140dec22781d0ac29b Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 9 Sep 2024 12:43:40 +0200 Subject: [PATCH 296/979] Fix game title when it comes from import --- lib/src/model/game/archived_game.dart | 14 +++++++ lib/src/model/game/playable_game.dart | 1 + .../broadcast/broadcasts_list_screen.dart | 2 +- lib/src/view/game/archived_game_screen.dart | 27 +++++++++---- lib/src/view/game/game_common_widgets.dart | 38 ++++++++++++++----- lib/src/view/game/game_list_tile.dart | 10 ++++- lib/src/view/game/game_screen.dart | 16 ++++++-- .../view/puzzle/puzzle_history_screen.dart | 2 +- lib/src/view/user/perf_stats_screen.dart | 15 ++++---- lib/src/view/user/user_activity.dart | 2 +- test/model/game/game_repository_test.dart | 4 +- test/model/game/game_storage_test.dart | 2 + test/model/game/game_test.dart | 4 +- test/view/game/archived_game_screen_test.dart | 2 +- 14 files changed, 100 insertions(+), 39 deletions(-) diff --git a/lib/src/model/game/archived_game.dart b/lib/src/model/game/archived_game.dart index 112da35e8c..2be53c5065 100644 --- a/lib/src/model/game/archived_game.dart +++ b/lib/src/model/game/archived_game.dart @@ -47,6 +47,11 @@ class ArchivedGame required IList steps, String? initialFen, required GameStatus status, + @JsonKey( + defaultValue: GameSource.unknown, + unknownEnumValue: GameSource.unknown, + ) + required GameSource source, Side? winner, /// The point of view of the current player or null if spectating. @@ -113,6 +118,7 @@ class LightArchivedGame with _$LightArchivedGame { required Player white, required Player black, required Variant variant, + GameSource? source, LightOpening? opening, String? lastFen, @MoveConverter() Move? lastMove, @@ -182,6 +188,10 @@ ArchivedGame _archivedGameFromPick(RequiredPick pick) { opening: data.opening, division: division, ), + source: pick('source').letOrThrow( + (pick) => + GameSource.nameMap[pick.asStringOrThrow()] ?? GameSource.unknown, + ), data: data, status: data.status, winner: data.winner, @@ -227,6 +237,10 @@ LightArchivedGame _lightArchivedGameFromPick(RequiredPick pick) { return LightArchivedGame( id: pick('id').asGameIdOrThrow(), fullId: pick('fullId').asGameFullIdOrNull(), + source: pick('source').letOrNull( + (pick) => + GameSource.nameMap[pick.asStringOrThrow()] ?? GameSource.unknown, + ), rated: pick('rated').asBoolOrThrow(), speed: pick('speed').asSpeedOrThrow(), perf: pick('perf').asPerfOrThrow(), diff --git a/lib/src/model/game/playable_game.dart b/lib/src/model/game/playable_game.dart index a9cc421a8e..7ac6cb17b3 100644 --- a/lib/src/model/game/playable_game.dart +++ b/lib/src/model/game/playable_game.dart @@ -137,6 +137,7 @@ class PlayableGame return ArchivedGame( id: id, meta: meta, + source: source, data: LightArchivedGame( id: id, variant: meta.variant, diff --git a/lib/src/view/broadcast/broadcasts_list_screen.dart b/lib/src/view/broadcast/broadcasts_list_screen.dart index f1dbe8dea2..6f011dbf05 100644 --- a/lib/src/view/broadcast/broadcasts_list_screen.dart +++ b/lib/src/view/broadcast/broadcasts_list_screen.dart @@ -15,7 +15,7 @@ import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; -final _dateFormatter = DateFormat.MMMd(Intl.getCurrentLocale()).add_Hm(); +final _dateFormatter = DateFormat.MMMd().add_Hm(); /// A screen that displays a paginated list of broadcasts. class BroadcastsListScreen extends StatelessWidget { diff --git a/lib/src/view/game/archived_game_screen.dart b/lib/src/view/game/archived_game_screen.dart index 40f7317516..c85e19dea8 100644 --- a/lib/src/view/game/archived_game_screen.dart +++ b/lib/src/view/game/archived_game_screen.dart @@ -2,10 +2,12 @@ import 'package:dartchess/dartchess.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/game/archived_game.dart'; +import 'package:lichess_mobile/src/model/game/game.dart'; import 'package:lichess_mobile/src/model/game/game_repository_providers.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; @@ -163,19 +165,30 @@ class _GameTitle extends StatelessWidget { final LightArchivedGame gameData; + static final _dateFormat = DateFormat.yMMMd(); + @override Widget build(BuildContext context) { return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - gameData.perf.icon, - color: DefaultTextStyle.of(context).style.color, - ), + if (gameData.source == GameSource.import) + Icon( + Icons.cloud_upload, + color: DefaultTextStyle.of(context).style.color, + ) + else + Icon( + gameData.perf.icon, + color: DefaultTextStyle.of(context).style.color, + ), const SizedBox(width: 4.0), - Text( - '${gameData.clockDisplay} • ${gameData.rated ? context.l10n.rated : context.l10n.casual}', - ), + if (gameData.source == GameSource.import) + Text('Import • ${_dateFormat.format(gameData.createdAt)}') + else + Text( + '${gameData.clockDisplay} • ${_dateFormat.format(gameData.lastMoveAt)}', + ), ], ); } diff --git a/lib/src/view/game/game_common_widgets.dart b/lib/src/view/game/game_common_widgets.dart index f502c768eb..ad4e671a84 100644 --- a/lib/src/view/game/game_common_widgets.dart +++ b/lib/src/view/game/game_common_widgets.dart @@ -2,6 +2,7 @@ import 'package:dartchess/dartchess.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/model/challenge/challenge.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; @@ -21,13 +22,24 @@ import 'game_screen_providers.dart'; import 'game_settings.dart'; import 'ping_rating.dart'; +final _gameTitledateFormat = DateFormat.yMMMd(); + class GameAppBar extends ConsumerWidget { - const GameAppBar({this.id, this.seek, this.challenge, super.key}); + const GameAppBar({ + this.id, + this.seek, + this.challenge, + this.lastMoveAt, + super.key, + }); final GameSeek? seek; final ChallengeRequest? challenge; final GameFullId? id; + /// The date of the last move played in the game. If null, the game is in progress. + final DateTime? lastMoveAt; + static const pingRating = Padding( padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 18.0), child: PingRating(size: 24.0), @@ -45,7 +57,7 @@ class GameAppBar extends ConsumerWidget { orElse: () => pingRating, ), title: id != null - ? StandaloneGameTitle(id: id!) + ? _StandaloneGameTitle(id: id!, lastMoveAt: lastMoveAt) : seek != null ? _LobbyGameTitle(seek: seek!) : challenge != null @@ -297,9 +309,7 @@ class GameShareBottomSheet extends ConsumerWidget { } class _LobbyGameTitle extends ConsumerWidget { - const _LobbyGameTitle({ - required this.seek, - }); + const _LobbyGameTitle({required this.seek}); final GameSeek seek; @@ -352,13 +362,16 @@ class _ChallengeGameTitle extends ConsumerWidget { } } -class StandaloneGameTitle extends ConsumerWidget { - const StandaloneGameTitle({ +class _StandaloneGameTitle extends ConsumerWidget { + const _StandaloneGameTitle({ required this.id, + this.lastMoveAt, }); final GameFullId id; + final DateTime? lastMoveAt; + @override Widget build(BuildContext context, WidgetRef ref) { final metaAsync = ref.watch(gameMetaProvider(id)); @@ -367,6 +380,11 @@ class StandaloneGameTitle extends ConsumerWidget { final mode = meta.rated ? ' • ${context.l10n.rated}' : ' • ${context.l10n.casual}'; + + final info = lastMoveAt != null + ? ' • ${_gameTitledateFormat.format(lastMoveAt!)}' + : mode; + return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -377,14 +395,14 @@ class StandaloneGameTitle extends ConsumerWidget { const SizedBox(width: 4.0), if (meta.clock != null) Text( - '${TimeIncrement(meta.clock!.initial.inSeconds, meta.clock!.increment.inSeconds).display}$mode', + '${TimeIncrement(meta.clock!.initial.inSeconds, meta.clock!.increment.inSeconds).display}$info', ) else if (meta.daysPerTurn != null) Text( - '${context.l10n.nbDays(meta.daysPerTurn!)}$mode', + '${context.l10n.nbDays(meta.daysPerTurn!)}$info', ) else - Text('${meta.perf.title}$mode'), + Text('${meta.perf.title}$info'), ], ); }, diff --git a/lib/src/view/game/game_list_tile.dart b/lib/src/view/game/game_list_tile.dart index 199b555812..1882b85aa6 100644 --- a/lib/src/view/game/game_list_tile.dart +++ b/lib/src/view/game/game_list_tile.dart @@ -25,7 +25,7 @@ import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/user_full_name.dart'; import 'package:timeago/timeago.dart' as timeago; -final _dateFormatter = DateFormat.yMMMd(Intl.getCurrentLocale()).add_Hm(); +final _dateFormatter = DateFormat.yMMMd().add_Hm(); /// A list tile that shows game info. class GameListTile extends StatelessWidget { @@ -487,7 +487,13 @@ class ExtendedGameListTile extends StatelessWidget { context, rootNavigator: true, builder: (context) => game.fullId != null - ? GameScreen(initialGameId: game.fullId) + ? GameScreen( + initialGameId: game.fullId, + loadingFen: game.lastFen, + loadingLastMove: game.lastMove, + loadingOrientation: youAre, + lastMoveAt: game.lastMoveAt, + ) : ArchivedGameScreen( gameData: game, orientation: youAre, diff --git a/lib/src/view/game/game_screen.dart b/lib/src/view/game/game_screen.dart index fbbc91ff4d..2f0069d3af 100644 --- a/lib/src/view/game/game_screen.dart +++ b/lib/src/view/game/game_screen.dart @@ -70,6 +70,7 @@ class GameScreen extends ConsumerStatefulWidget { this.loadingFen, this.loadingLastMove, this.loadingOrientation, + this.lastMoveAt, super.key, }) : assert( initialGameId != null || seek != null || challenge != null, @@ -86,6 +87,9 @@ class GameScreen extends ConsumerStatefulWidget { final Move? loadingLastMove; final Side? loadingOrientation; + /// The date of the last move played in the game. If null, the game is in progress. + final DateTime? lastMoveAt; + _GameSource get source { if (initialGameId != null) { return _GameSource.game; @@ -144,7 +148,11 @@ class _GameScreenState extends ConsumerState with RouteAware { final body = gameId != null ? GameBody( id: gameId, - loadingBoardWidget: const StandaloneGameLoadingBoard(), + loadingBoardWidget: StandaloneGameLoadingBoard( + fen: widget.loadingFen, + lastMove: widget.loadingLastMove, + orientation: widget.loadingOrientation, + ), whiteClockKey: _whiteClockKey, blackClockKey: _blackClockKey, boardKey: _boardKey, @@ -176,7 +184,7 @@ class _GameScreenState extends ConsumerState with RouteAware { : const LoadGameError('Could not create the game.'); return PlatformScaffold( resizeToAvoidBottomInset: false, - appBar: GameAppBar(id: gameId), + appBar: GameAppBar(id: gameId, lastMoveAt: widget.lastMoveAt), body: body, ); }, @@ -195,7 +203,7 @@ class _GameScreenState extends ConsumerState with RouteAware { return PlatformScaffold( resizeToAvoidBottomInset: false, - appBar: GameAppBar(seek: widget.seek), + appBar: GameAppBar(seek: widget.seek, lastMoveAt: widget.lastMoveAt), body: PopScope( canPop: false, child: loadingBoard, @@ -219,7 +227,7 @@ class _GameScreenState extends ConsumerState with RouteAware { final body = PopScope(child: message); return PlatformScaffold( - appBar: GameAppBar(seek: widget.seek), + appBar: GameAppBar(seek: widget.seek, lastMoveAt: widget.lastMoveAt), body: body, ); }, diff --git a/lib/src/view/puzzle/puzzle_history_screen.dart b/lib/src/view/puzzle/puzzle_history_screen.dart index 3c8d611cd7..bf3282aec2 100644 --- a/lib/src/view/puzzle/puzzle_history_screen.dart +++ b/lib/src/view/puzzle/puzzle_history_screen.dart @@ -18,7 +18,7 @@ import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:timeago/timeago.dart' as timeago; -final _dateFormatter = DateFormat.yMMMd(Intl.getCurrentLocale()); +final _dateFormatter = DateFormat.yMMMd(); class PuzzleHistoryScreen extends StatelessWidget { @override diff --git a/lib/src/view/user/perf_stats_screen.dart b/lib/src/view/user/perf_stats_screen.dart index d2b0562f9b..2113325751 100644 --- a/lib/src/view/user/perf_stats_screen.dart +++ b/lib/src/view/user/perf_stats_screen.dart @@ -35,8 +35,7 @@ import 'package:lichess_mobile/src/widgets/rating.dart'; import 'package:lichess_mobile/src/widgets/stat_card.dart'; import 'package:lichess_mobile/src/widgets/user_full_name.dart'; -final _currentLocale = Intl.getCurrentLocale(); -final _dateFormatter = DateFormat.yMMMd(_currentLocale); +final _dateFormatter = DateFormat.yMMMd(); const _customOpacity = 0.6; const _defaultStatFontSize = 12.0; @@ -846,11 +845,11 @@ class _EloChartState extends State<_EloChart> { Theme.of(context).colorScheme.onSurface.withOpacity(0.5); final chartColor = Theme.of(context).colorScheme.tertiary; final chartDateFormatter = switch (_selectedRange) { - DateRange.oneWeek => DateFormat.MMMd(_currentLocale), - DateRange.oneMonth => DateFormat.MMMd(_currentLocale), - DateRange.threeMonths => DateFormat.yMMM(_currentLocale), - DateRange.oneYear => DateFormat.yMMM(_currentLocale), - DateRange.allTime => DateFormat.yMMM(_currentLocale), + DateRange.oneWeek => DateFormat.MMMd(), + DateRange.oneMonth => DateFormat.MMMd(), + DateRange.threeMonths => DateFormat.yMMM(), + DateRange.oneYear => DateFormat.yMMM(), + DateRange.allTime => DateFormat.yMMM(), }; String formatDateFromTimestamp(double nbDays) => chartDateFormatter.format( @@ -858,7 +857,7 @@ class _EloChartState extends State<_EloChart> { ); String formatDateFromTimestampForTooltip(double nbDays) => - DateFormat.yMMMd(_currentLocale).format( + DateFormat.yMMMd().format( _firstDate.add(Duration(days: nbDays.toInt())), ); diff --git a/lib/src/view/user/user_activity.dart b/lib/src/view/user/user_activity.dart index 1e87d8cbe9..faf9157550 100644 --- a/lib/src/view/user/user_activity.dart +++ b/lib/src/view/user/user_activity.dart @@ -19,7 +19,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'user_activity.g.dart'; -final _dateFormatter = DateFormat.yMMMd(Intl.getCurrentLocale()); +final _dateFormatter = DateFormat.yMMMd(); @riverpod Future> _userActivity( diff --git a/test/model/game/game_repository_test.dart b/test/model/game/game_repository_test.dart index 1d5311e424..d3b86c83b6 100644 --- a/test/model/game/game_repository_test.dart +++ b/test/model/game/game_repository_test.dart @@ -73,7 +73,7 @@ void main() { group('GameRepository.getGame', () { test('game with clocks', () async { const testResponse = ''' -{"id":"qVChCOTc","rated":false,"variant":"standard","speed":"blitz","perf":"blitz","createdAt":1673443822389,"lastMoveAt":1673444036416,"status":"mate","players":{"white":{"aiLevel":1},"black":{"user":{"name":"veloce","patron":true,"id":"veloce"},"rating":1435,"provisional":true}},"winner":"black","opening":{"eco":"C20","name":"King's Pawn Game: Wayward Queen Attack, Kiddie Countergambit","ply":4},"moves":"e4 e5 Qh5 Nf6 Qxe5+ Be7 b3 d6 Qb5+ Bd7 Qxb7 Nc6 Ba3 Rb8 Qa6 Nxe4 Bb2 O-O Nc3 Nb4 Nf3 Nxa6 Nd5 Nb4 Nxe7+ Qxe7 Nd4 Qf6 f4 Qe7 Ke2 Ng3+ Kd1 Nxh1 Bc4 Nf2+ Kc1 Qe1#","clocks":[18003,18003,17915,17627,17771,16691,17667,16243,17475,15459,17355,14779,17155,13795,16915,13267,14771,11955,14451,10995,14339,10203,13899,9099,12427,8379,12003,7547,11787,6691,11355,6091,11147,5763,10851,5099,10635,4657],"clock":{"initial":180,"increment":0,"totalTime":180},"division":{"middle":18,"end":42}} +{"id":"qVChCOTc","rated":false,"source":"lobby","variant":"standard","speed":"blitz","perf":"blitz","createdAt":1673443822389,"lastMoveAt":1673444036416,"status":"mate","players":{"white":{"aiLevel":1},"black":{"user":{"name":"veloce","patron":true,"id":"veloce"},"rating":1435,"provisional":true}},"winner":"black","opening":{"eco":"C20","name":"King's Pawn Game: Wayward Queen Attack, Kiddie Countergambit","ply":4},"moves":"e4 e5 Qh5 Nf6 Qxe5+ Be7 b3 d6 Qb5+ Bd7 Qxb7 Nc6 Ba3 Rb8 Qa6 Nxe4 Bb2 O-O Nc3 Nb4 Nf3 Nxa6 Nd5 Nb4 Nxe7+ Qxe7 Nd4 Qf6 f4 Qe7 Ke2 Ng3+ Kd1 Nxh1 Bc4 Nf2+ Kc1 Qe1#","clocks":[18003,18003,17915,17627,17771,16691,17667,16243,17475,15459,17355,14779,17155,13795,16915,13267,14771,11955,14451,10995,14339,10203,13899,9099,12427,8379,12003,7547,11787,6691,11355,6091,11147,5763,10851,5099,10635,4657],"clock":{"initial":180,"increment":0,"totalTime":180},"division":{"middle":18,"end":42}} '''; final mockClient = MockClient((request) { @@ -96,7 +96,7 @@ void main() { test('threeCheck game', () async { const testResponse = ''' -{"id":"1vdsvmxp","rated":true,"variant":"threeCheck","speed":"bullet","perf":"threeCheck","createdAt":1604194310939,"lastMoveAt":1604194361831,"status":"variantEnd","players":{"white":{"user":{"name":"Zhigalko_Sergei","title":"GM","patron":true,"id":"zhigalko_sergei"},"rating":2448,"ratingDiff":6,"analysis":{"inaccuracy":1,"mistake":1,"blunder":1,"acpl":75}},"black":{"user":{"name":"catask","id":"catask"},"rating":2485,"ratingDiff":-6,"analysis":{"inaccuracy":1,"mistake":0,"blunder":2,"acpl":115}}},"winner":"white","opening":{"eco":"B02","name":"Alekhine Defense: Scandinavian Variation, Geschev Gambit","ply":6},"moves":"e4 c6 Nc3 d5 exd5 Nf6 Nf3 e5 Bc4 Bd6 O-O O-O h3 e4 Kh1 exf3 Qxf3 cxd5 Nxd5 Nxd5 Bxd5 Nc6 Re1 Be6 Rxe6 fxe6 Bxe6+ Kh8 Qh5 h6 Qg6 Qf6 Qh7+ Kxh7 Bf5+","analysis":[{"eval":340},{"eval":359},{"eval":231},{"eval":300,"best":"g8f6","variation":"Nf6 e5 d5 d4 Ne4 Bd3 Bf5 Nf3 e6 O-O","judgment":{"name":"Inaccuracy","comment":"Inaccuracy. Nf6 was best."}},{"eval":262},{"eval":286},{"eval":184,"best":"f1c4","variation":"Bc4 e6 dxe6 Bxe6 Bxe6 fxe6 Qe2 Qd7 Nf3 Bd6","judgment":{"name":"Inaccuracy","comment":"Inaccuracy. Bc4 was best."}},{"eval":235},{"eval":193},{"eval":243},{"eval":269},{"eval":219},{"eval":-358,"best":"d2d3","variation":"d3 Bg4 h3 e4 Nxe4 Bh2+ Kh1 Nxe4 dxe4 Qf6","judgment":{"name":"Blunder","comment":"Blunder. d3 was best."}},{"eval":-376},{"eval":-386},{"eval":-383},{"eval":-405},{"eval":-363},{"eval":-372},{"eval":-369},{"eval":-345},{"eval":-276},{"eval":-507,"best":"b2b3","variation":"b3 Be6","judgment":{"name":"Mistake","comment":"Mistake. b3 was best."}},{"eval":-49,"best":"c6e5","variation":"Ne5 Qh5","judgment":{"name":"Blunder","comment":"Blunder. Ne5 was best."}},{"eval":-170},{"mate":7,"best":"g8h8","variation":"Kh8 Rh6","judgment":{"name":"Blunder","comment":"Checkmate is now unavoidable. Kh8 was best."}},{"mate":6},{"mate":6},{"mate":5},{"mate":3},{"mate":2},{"mate":2},{"mate":1},{"mate":1}],"clock":{"initial":60,"increment":0,"totalTime":60},"division":{"middle":18,"end":42}} +{"id":"1vdsvmxp","rated":true,"source":"lobby","variant":"threeCheck","speed":"bullet","perf":"threeCheck","createdAt":1604194310939,"lastMoveAt":1604194361831,"status":"variantEnd","players":{"white":{"user":{"name":"Zhigalko_Sergei","title":"GM","patron":true,"id":"zhigalko_sergei"},"rating":2448,"ratingDiff":6,"analysis":{"inaccuracy":1,"mistake":1,"blunder":1,"acpl":75}},"black":{"user":{"name":"catask","id":"catask"},"rating":2485,"ratingDiff":-6,"analysis":{"inaccuracy":1,"mistake":0,"blunder":2,"acpl":115}}},"winner":"white","opening":{"eco":"B02","name":"Alekhine Defense: Scandinavian Variation, Geschev Gambit","ply":6},"moves":"e4 c6 Nc3 d5 exd5 Nf6 Nf3 e5 Bc4 Bd6 O-O O-O h3 e4 Kh1 exf3 Qxf3 cxd5 Nxd5 Nxd5 Bxd5 Nc6 Re1 Be6 Rxe6 fxe6 Bxe6+ Kh8 Qh5 h6 Qg6 Qf6 Qh7+ Kxh7 Bf5+","analysis":[{"eval":340},{"eval":359},{"eval":231},{"eval":300,"best":"g8f6","variation":"Nf6 e5 d5 d4 Ne4 Bd3 Bf5 Nf3 e6 O-O","judgment":{"name":"Inaccuracy","comment":"Inaccuracy. Nf6 was best."}},{"eval":262},{"eval":286},{"eval":184,"best":"f1c4","variation":"Bc4 e6 dxe6 Bxe6 Bxe6 fxe6 Qe2 Qd7 Nf3 Bd6","judgment":{"name":"Inaccuracy","comment":"Inaccuracy. Bc4 was best."}},{"eval":235},{"eval":193},{"eval":243},{"eval":269},{"eval":219},{"eval":-358,"best":"d2d3","variation":"d3 Bg4 h3 e4 Nxe4 Bh2+ Kh1 Nxe4 dxe4 Qf6","judgment":{"name":"Blunder","comment":"Blunder. d3 was best."}},{"eval":-376},{"eval":-386},{"eval":-383},{"eval":-405},{"eval":-363},{"eval":-372},{"eval":-369},{"eval":-345},{"eval":-276},{"eval":-507,"best":"b2b3","variation":"b3 Be6","judgment":{"name":"Mistake","comment":"Mistake. b3 was best."}},{"eval":-49,"best":"c6e5","variation":"Ne5 Qh5","judgment":{"name":"Blunder","comment":"Blunder. Ne5 was best."}},{"eval":-170},{"mate":7,"best":"g8h8","variation":"Kh8 Rh6","judgment":{"name":"Blunder","comment":"Checkmate is now unavoidable. Kh8 was best."}},{"mate":6},{"mate":6},{"mate":5},{"mate":3},{"mate":2},{"mate":2},{"mate":1},{"mate":1}],"clock":{"initial":60,"increment":0,"totalTime":60},"division":{"middle":18,"end":42}} '''; final mockClient = MockClient((request) { diff --git a/test/model/game/game_storage_test.dart b/test/model/game/game_storage_test.dart index ac49917849..3cee848c15 100644 --- a/test/model/game/game_storage_test.dart +++ b/test/model/game/game_storage_test.dart @@ -107,6 +107,7 @@ final game = ArchivedGame( speed: Speed.correspondence, variant: Variant.standard, ), + source: GameSource.lobby, data: LightArchivedGame( id: gameId, variant: Variant.standard, @@ -167,6 +168,7 @@ final games = List.generate(100, (index) { speed: Speed.correspondence, variant: Variant.standard, ), + source: GameSource.lobby, data: LightArchivedGame( id: id, variant: Variant.standard, diff --git a/test/model/game/game_test.dart b/test/model/game/game_test.dart index a00b06145d..20bea8e841 100644 --- a/test/model/game/game_test.dart +++ b/test/model/game/game_test.dart @@ -122,9 +122,9 @@ const _playableGameJson = ''' '''; const _archivedGameJson = ''' -{"id":"CCW6EEru","rated":true,"variant":"standard","speed":"bullet","perf":"bullet","createdAt":1706185945680,"lastMoveAt":1706186170504,"status":"resign","players":{"white":{"user":{"name":"veloce","id":"veloce"},"rating":1789,"ratingDiff":9,"analysis":{"inaccuracy":2,"mistake":1,"blunder":3,"acpl":90}},"black":{"user":{"name":"chabrot","id":"chabrot"},"rating":1810,"ratingDiff":-9,"analysis":{"inaccuracy":3,"mistake":0,"blunder":5,"acpl":135}}},"winner":"white","opening":{"eco":"C52","name":"Italian Game: Evans Gambit, Main Line","ply":10},"moves":"e4 e5 Nf3 Nc6 Bc4 Bc5 b4 Bxb4 c3 Ba5 d4 Bb6 Ba3 Nf6 Qb3 d6 Bxf7+ Kf8 O-O Qe7 Nxe5 Nxe5 dxe5 Be6 Bxe6 Nxe4 Re1 Nc5 Bxc5 Bxc5 Qxb7 Re8 Bh3 dxe5 Qf3+ Kg8 Nd2 Rf8 Qd5+ Rf7 Be6 Qxe6 Qxe6","clocks":[12003,12003,11883,11811,11683,11379,11307,11163,11043,11043,10899,10707,10155,10483,10019,9995,9635,9923,8963,8603,7915,8283,7763,7459,7379,6083,6587,5819,6363,5651,6075,5507,5675,4803,5059,4515,4547,3555,3971,3411,3235,3123,3120,2742],"analysis":[{"eval":32},{"eval":41},{"eval":39},{"eval":20},{"eval":17},{"eval":21},{"eval":-21},{"eval":-14},{"eval":-23},{"eval":-24},{"eval":-24},{"eval":52,"best":"d7d6","variation":"d6","judgment":{"name":"Inaccuracy","comment":"Inaccuracy. d6 was best."}},{"eval":-56,"best":"f3e5","variation":"Nxe5","judgment":{"name":"Inaccuracy","comment":"Inaccuracy. Nxe5 was best."}},{"eval":177,"best":"d7d6","variation":"d6","judgment":{"name":"Blunder","comment":"Blunder. d6 was best."}},{"eval":-19,"best":"d4e5","variation":"dxe5 Ng4 Qd5 Nh6 Nbd2 Ne7 Qd3 O-O h3 d6 g4 Kh8 exd6 cxd6","judgment":{"name":"Blunder","comment":"Blunder. dxe5 was best."}},{"eval":-16},{"eval":-20},{"eval":-12},{"eval":-145,"best":"f7d5","variation":"Bd5 Nxd5","judgment":{"name":"Mistake","comment":"Mistake. Bd5 was best."}},{"eval":72,"best":"c6a5","variation":"Na5 Qd1 Kxf7 dxe5 dxe5 Nxe5+ Ke8 Nd2 Be6 Qa4+ Bd7 Qd1 Nc6 Ndc4","judgment":{"name":"Blunder","comment":"Blunder. Na5 was best."}},{"eval":-36,"best":"f7d5","variation":"Bd5 Nxd5 exd5 Na5 Qb4 exd4 cxd4 Kg8 Re1 Qf7 Ng5 Qg6 Nc3 h6","judgment":{"name":"Inaccuracy","comment":"Inaccuracy. Bd5 was best."}},{"eval":-41},{"eval":-42},{"eval":593,"best":"e7f7","variation":"Qxf7 exf6 gxf6 c4 Rg8 Nd2 Qh5 c5 Bh3 g3 Bxc5 Bxc5 dxc5 Rfe1","judgment":{"name":"Blunder","comment":"Blunder. Qxf7 was best."}},{"eval":589},{"eval":630},{"eval":-32,"best":"e5d6","variation":"exd6 cxd6 Bd5 Nxf2 Nd2 g5 Nc4 Kg7 Nxb6 Qe3 Rxf2 Rhf8 Bf3 axb6","judgment":{"name":"Blunder","comment":"Blunder. exd6 was best."}},{"eval":602,"best":"b6f2","variation":"Bxf2+","judgment":{"name":"Blunder","comment":"Blunder. Bxf2+ was best."}},{"eval":581},{"eval":656},{"eval":662},{"mate":15,"best":"g7g6","variation":"g6 Qxa8+ Kg7 Qd5 c6 Qb3 Rf8 Re2 Qh4 Qb7+ Kh6 Qb2 dxe5 Nd2","judgment":{"name":"Blunder","comment":"Checkmate is now unavoidable. g6 was best."}},{"eval":566,"best":"b7f3","variation":"Qf3+ Qf6 exf6 g6 f7 Kg7 fxe8=Q Rxe8 Qf7+ Kh6 Qxe8 d5 g4 c6","judgment":{"name":"Blunder","comment":"Lost forced checkmate sequence. Qf3+ was best."}},{"eval":574},{"eval":566},{"eval":580},{"eval":569},{"eval":774,"best":"g7g6","variation":"g6 Ne4 Kg7 Qe2 Rd8 a4 h5 Rad1 Rxd1 Rxd1 Bb6 Rd7 Qxd7 Bxd7","judgment":{"name":"Inaccuracy","comment":"Inaccuracy. g6 was best."}},{"eval":739},{"eval":743},{"eval":615},{"eval":934,"best":"c5f2","variation":"Bxf2+ Kh1 Bxe1 Rxe1 g6 Rf1 Kg7 Rxf7+ Qxf7 Bxf7 Rf8 Be6 e4 Qxe4","judgment":{"name":"Inaccuracy","comment":"Inaccuracy. Bxf2+ was best."}},{"eval":861}],"clock":{"initial":120,"increment":1,"totalTime":160}} +{"id":"CCW6EEru","rated":true,"source":"lobby","variant":"standard","speed":"bullet","perf":"bullet","createdAt":1706185945680,"lastMoveAt":1706186170504,"status":"resign","players":{"white":{"user":{"name":"veloce","id":"veloce"},"rating":1789,"ratingDiff":9,"analysis":{"inaccuracy":2,"mistake":1,"blunder":3,"acpl":90}},"black":{"user":{"name":"chabrot","id":"chabrot"},"rating":1810,"ratingDiff":-9,"analysis":{"inaccuracy":3,"mistake":0,"blunder":5,"acpl":135}}},"winner":"white","opening":{"eco":"C52","name":"Italian Game: Evans Gambit, Main Line","ply":10},"moves":"e4 e5 Nf3 Nc6 Bc4 Bc5 b4 Bxb4 c3 Ba5 d4 Bb6 Ba3 Nf6 Qb3 d6 Bxf7+ Kf8 O-O Qe7 Nxe5 Nxe5 dxe5 Be6 Bxe6 Nxe4 Re1 Nc5 Bxc5 Bxc5 Qxb7 Re8 Bh3 dxe5 Qf3+ Kg8 Nd2 Rf8 Qd5+ Rf7 Be6 Qxe6 Qxe6","clocks":[12003,12003,11883,11811,11683,11379,11307,11163,11043,11043,10899,10707,10155,10483,10019,9995,9635,9923,8963,8603,7915,8283,7763,7459,7379,6083,6587,5819,6363,5651,6075,5507,5675,4803,5059,4515,4547,3555,3971,3411,3235,3123,3120,2742],"analysis":[{"eval":32},{"eval":41},{"eval":39},{"eval":20},{"eval":17},{"eval":21},{"eval":-21},{"eval":-14},{"eval":-23},{"eval":-24},{"eval":-24},{"eval":52,"best":"d7d6","variation":"d6","judgment":{"name":"Inaccuracy","comment":"Inaccuracy. d6 was best."}},{"eval":-56,"best":"f3e5","variation":"Nxe5","judgment":{"name":"Inaccuracy","comment":"Inaccuracy. Nxe5 was best."}},{"eval":177,"best":"d7d6","variation":"d6","judgment":{"name":"Blunder","comment":"Blunder. d6 was best."}},{"eval":-19,"best":"d4e5","variation":"dxe5 Ng4 Qd5 Nh6 Nbd2 Ne7 Qd3 O-O h3 d6 g4 Kh8 exd6 cxd6","judgment":{"name":"Blunder","comment":"Blunder. dxe5 was best."}},{"eval":-16},{"eval":-20},{"eval":-12},{"eval":-145,"best":"f7d5","variation":"Bd5 Nxd5","judgment":{"name":"Mistake","comment":"Mistake. Bd5 was best."}},{"eval":72,"best":"c6a5","variation":"Na5 Qd1 Kxf7 dxe5 dxe5 Nxe5+ Ke8 Nd2 Be6 Qa4+ Bd7 Qd1 Nc6 Ndc4","judgment":{"name":"Blunder","comment":"Blunder. Na5 was best."}},{"eval":-36,"best":"f7d5","variation":"Bd5 Nxd5 exd5 Na5 Qb4 exd4 cxd4 Kg8 Re1 Qf7 Ng5 Qg6 Nc3 h6","judgment":{"name":"Inaccuracy","comment":"Inaccuracy. Bd5 was best."}},{"eval":-41},{"eval":-42},{"eval":593,"best":"e7f7","variation":"Qxf7 exf6 gxf6 c4 Rg8 Nd2 Qh5 c5 Bh3 g3 Bxc5 Bxc5 dxc5 Rfe1","judgment":{"name":"Blunder","comment":"Blunder. Qxf7 was best."}},{"eval":589},{"eval":630},{"eval":-32,"best":"e5d6","variation":"exd6 cxd6 Bd5 Nxf2 Nd2 g5 Nc4 Kg7 Nxb6 Qe3 Rxf2 Rhf8 Bf3 axb6","judgment":{"name":"Blunder","comment":"Blunder. exd6 was best."}},{"eval":602,"best":"b6f2","variation":"Bxf2+","judgment":{"name":"Blunder","comment":"Blunder. Bxf2+ was best."}},{"eval":581},{"eval":656},{"eval":662},{"mate":15,"best":"g7g6","variation":"g6 Qxa8+ Kg7 Qd5 c6 Qb3 Rf8 Re2 Qh4 Qb7+ Kh6 Qb2 dxe5 Nd2","judgment":{"name":"Blunder","comment":"Checkmate is now unavoidable. g6 was best."}},{"eval":566,"best":"b7f3","variation":"Qf3+ Qf6 exf6 g6 f7 Kg7 fxe8=Q Rxe8 Qf7+ Kh6 Qxe8 d5 g4 c6","judgment":{"name":"Blunder","comment":"Lost forced checkmate sequence. Qf3+ was best."}},{"eval":574},{"eval":566},{"eval":580},{"eval":569},{"eval":774,"best":"g7g6","variation":"g6 Ne4 Kg7 Qe2 Rd8 a4 h5 Rad1 Rxd1 Rxd1 Bb6 Rd7 Qxd7 Bxd7","judgment":{"name":"Inaccuracy","comment":"Inaccuracy. g6 was best."}},{"eval":739},{"eval":743},{"eval":615},{"eval":934,"best":"c5f2","variation":"Bxf2+ Kh1 Bxe1 Rxe1 g6 Rf1 Kg7 Rxf7+ Qxf7 Bxf7 Rf8 Be6 e4 Qxe4","judgment":{"name":"Inaccuracy","comment":"Inaccuracy. Bxf2+ was best."}},{"eval":861}],"clock":{"initial":120,"increment":1,"totalTime":160}} '''; const _archivedGameJsonNoEvals = ''' -{"id":"CCW6EEru","rated":true,"variant":"standard","speed":"bullet","perf":"bullet","createdAt":1706185945680,"lastMoveAt":1706186170504,"status":"resign","players":{"white":{"user":{"name":"veloce","id":"veloce"},"rating":1789,"ratingDiff":9},"black":{"user":{"name":"chabrot","id":"chabrot"},"rating":1810,"ratingDiff":-9}},"winner":"white","opening":{"eco":"C52","name":"Italian Game: Evans Gambit, Main Line","ply":10},"moves":"e4 e5 Nf3 Nc6 Bc4 Bc5 b4 Bxb4 c3 Ba5 d4 Bb6 Ba3 Nf6 Qb3 d6 Bxf7+ Kf8 O-O Qe7 Nxe5 Nxe5 dxe5 Be6 Bxe6 Nxe4 Re1 Nc5 Bxc5 Bxc5 Qxb7 Re8 Bh3 dxe5 Qf3+ Kg8 Nd2 Rf8 Qd5+ Rf7 Be6 Qxe6 Qxe6","clocks":[12003,12003,11883,11811,11683,11379,11307,11163,11043,11043,10899,10707,10155,10483,10019,9995,9635,9923,8963,8603,7915,8283,7763,7459,7379,6083,6587,5819,6363,5651,6075,5507,5675,4803,5059,4515,4547,3555,3971,3411,3235,3123,3120,2742],"clock":{"initial":120,"increment":1,"totalTime":160}} +{"id":"CCW6EEru","rated":true,"source":"lobby","variant":"standard","speed":"bullet","perf":"bullet","createdAt":1706185945680,"lastMoveAt":1706186170504,"status":"resign","players":{"white":{"user":{"name":"veloce","id":"veloce"},"rating":1789,"ratingDiff":9},"black":{"user":{"name":"chabrot","id":"chabrot"},"rating":1810,"ratingDiff":-9}},"winner":"white","opening":{"eco":"C52","name":"Italian Game: Evans Gambit, Main Line","ply":10},"moves":"e4 e5 Nf3 Nc6 Bc4 Bc5 b4 Bxb4 c3 Ba5 d4 Bb6 Ba3 Nf6 Qb3 d6 Bxf7+ Kf8 O-O Qe7 Nxe5 Nxe5 dxe5 Be6 Bxe6 Nxe4 Re1 Nc5 Bxc5 Bxc5 Qxb7 Re8 Bh3 dxe5 Qf3+ Kg8 Nd2 Rf8 Qd5+ Rf7 Be6 Qxe6 Qxe6","clocks":[12003,12003,11883,11811,11683,11379,11307,11163,11043,11043,10899,10707,10155,10483,10019,9995,9635,9923,8963,8603,7915,8283,7763,7459,7379,6083,6587,5819,6363,5651,6075,5507,5675,4803,5059,4515,4547,3555,3971,3411,3235,3123,3120,2742],"clock":{"initial":120,"increment":1,"totalTime":160}} '''; diff --git a/test/view/game/archived_game_screen_test.dart b/test/view/game/archived_game_screen_test.dart index 288db21c50..acd5f5b5ab 100644 --- a/test/view/game/archived_game_screen_test.dart +++ b/test/view/game/archived_game_screen_test.dart @@ -221,7 +221,7 @@ void main() { // -- const gameResponse = ''' -{"id":"qVChCOTc","rated":false,"variant":"standard","speed":"blitz","perf":"blitz","createdAt":1673443822389,"lastMoveAt":1673444036416,"status":"mate","players":{"white":{"aiLevel":1},"black":{"user":{"name":"veloce","patron":true,"id":"veloce"},"rating":1435,"provisional":true}},"winner":"black","opening":{"eco":"C20","name":"King's Pawn Game: Wayward Queen Attack, Kiddie Countergambit","ply":4},"moves":"e4 e5 Qh5 Nf6 Qxe5+ Be7 b3 d6 Qb5+ Bd7 Qxb7 Nc6 Ba3 Rb8 Qa6 Nxe4 Bb2 O-O Nc3 Nb4 Nf3 Nxa6 Nd5 Nb4 Nxe7+ Qxe7 Nd4 Qf6 f4 Qe7 Ke2 Ng3+ Kd1 Nxh1 Bc4 Nf2+ Kc1 Qe1#","clocks":[18003,18003,17915,17627,17771,16691,17667,16243,17475,15459,17355,14779,17155,13795,16915,13267,14771,11955,14451,10995,14339,10203,13899,9099,12427,8379,12003,7547,11787,6691,11355,6091,11147,5763,10851,5099,10635,4657],"clock":{"initial":180,"increment":0,"totalTime":180},"lastFen":"1r3rk1/p1pb1ppp/3p4/8/1nBN1P2/1P6/PBPP1nPP/R1K1q3 w - - 4 1"} +{"id":"qVChCOTc","rated":false,"source":"lobby","variant":"standard","speed":"blitz","perf":"blitz","createdAt":1673443822389,"lastMoveAt":1673444036416,"status":"mate","players":{"white":{"aiLevel":1},"black":{"user":{"name":"veloce","patron":true,"id":"veloce"},"rating":1435,"provisional":true}},"winner":"black","opening":{"eco":"C20","name":"King's Pawn Game: Wayward Queen Attack, Kiddie Countergambit","ply":4},"moves":"e4 e5 Qh5 Nf6 Qxe5+ Be7 b3 d6 Qb5+ Bd7 Qxb7 Nc6 Ba3 Rb8 Qa6 Nxe4 Bb2 O-O Nc3 Nb4 Nf3 Nxa6 Nd5 Nb4 Nxe7+ Qxe7 Nd4 Qf6 f4 Qe7 Ke2 Ng3+ Kd1 Nxh1 Bc4 Nf2+ Kc1 Qe1#","clocks":[18003,18003,17915,17627,17771,16691,17667,16243,17475,15459,17355,14779,17155,13795,16915,13267,14771,11955,14451,10995,14339,10203,13899,9099,12427,8379,12003,7547,11787,6691,11355,6091,11147,5763,10851,5099,10635,4657],"clock":{"initial":180,"increment":0,"totalTime":180},"lastFen":"1r3rk1/p1pb1ppp/3p4/8/1nBN1P2/1P6/PBPP1nPP/R1K1q3 w - - 4 1"} '''; final gameData = LightArchivedGame( From b6ee9376dd06fa58b9b90b462d611683757cf629 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 9 Sep 2024 15:18:25 +0200 Subject: [PATCH 297/979] Fix correspondence game not updated after resuming app Fixes #981 --- lib/src/model/game/game_controller.dart | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lib/src/model/game/game_controller.dart b/lib/src/model/game/game_controller.dart index 7248b0462b..c4cd6830c3 100644 --- a/lib/src/model/game/game_controller.dart +++ b/lib/src/model/game/game_controller.dart @@ -6,6 +6,7 @@ import 'package:dartchess/dartchess.dart'; import 'package:deep_pick/deep_pick.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/account/account_preferences.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; @@ -40,6 +41,7 @@ part 'game_controller.g.dart'; class GameController extends _$GameController { final _logger = Logger('GameController'); + AppLifecycleListener? _appLifecycleListener; StreamSubscription? _socketSubscription; /// Periodic timer when the opponent has left the game, to display the countdown @@ -81,6 +83,7 @@ class GameController extends _$GameController { _socketSubscription?.cancel(); _opponentLeftCountdownTimer?.cancel(); _transientMoveTimer?.cancel(); + _appLifecycleListener?.dispose(); }); return _socketClient.stream.firstWhere((e) => e.topic == 'full').then( @@ -102,6 +105,16 @@ class GameController extends _$GameController { _socketEventVersion = fullEvent.socketEventVersion; + if (game.playable) { + _appLifecycleListener = AppLifecycleListener( + onResume: () { + if (_socketClient.isConnected) { + _resyncGameData(); + } + }, + ); + } + return GameState( gameFullId: gameFullId, game: game, From 3e0f8cb461fa06bd51cbfa73b5f02a9e13d37124 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 9 Sep 2024 15:21:22 +0200 Subject: [PATCH 298/979] Tweak ongoing games carousel --- lib/src/view/home/home_tab_screen.dart | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/src/view/home/home_tab_screen.dart b/lib/src/view/home/home_tab_screen.dart index e9f98b2356..d4694ee964 100644 --- a/lib/src/view/home/home_tab_screen.dart +++ b/lib/src/view/home/home_tab_screen.dart @@ -762,12 +762,14 @@ class _GamePreviewCarouselItem extends StatelessWidget { opacity: game.isMyTurn ? 1.0 : 0.6, child: Row( children: [ - Icon( - game.isMyTurn ? Icons.timer : Icons.timer_off, - size: 16.0, - color: Colors.white, - ), - const SizedBox(width: 4.0), + if (game.isMyTurn) ...const [ + Icon( + Icons.timer, + size: 16.0, + color: Colors.white, + ), + SizedBox(width: 4.0), + ], Text( game.secondsLeft != null && game.isMyTurn ? timeago.format( From 99147e438278cb0cf557182184b1633d1db392d0 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 9 Sep 2024 15:41:53 +0200 Subject: [PATCH 299/979] Cupertino style fixes --- lib/src/view/user/game_history_screen.dart | 4 ---- lib/src/widgets/buttons.dart | 5 ++++- lib/src/widgets/platform_scaffold.dart | 11 ++++++----- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/src/view/user/game_history_screen.dart b/lib/src/view/user/game_history_screen.dart index 3b3c79f269..b67232b106 100644 --- a/lib/src/view/user/game_history_screen.dart +++ b/lib/src/view/user/game_history_screen.dart @@ -69,10 +69,6 @@ class GameHistoryScreen extends ConsumerWidget { return PlatformScaffold( appBar: PlatformAppBar( - cupertinoPadding: const EdgeInsetsDirectional.only( - start: 16.0, - end: 8.0, - ), title: title, actions: [filterBtn], ), diff --git a/lib/src/widgets/buttons.dart b/lib/src/widgets/buttons.dart index 58c2fa1a40..a1405dcea9 100644 --- a/lib/src/widgets/buttons.dart +++ b/lib/src/widgets/buttons.dart @@ -279,7 +279,10 @@ class CupertinoIconButton extends StatelessWidget { child: CupertinoButton( padding: padding, onPressed: onPressed, - child: icon, + child: IconTheme.merge( + data: const IconThemeData(size: 24.0), + child: icon, + ), ), ); } diff --git a/lib/src/widgets/platform_scaffold.dart b/lib/src/widgets/platform_scaffold.dart index 147ee72d2b..f9da3ff32d 100644 --- a/lib/src/widgets/platform_scaffold.dart +++ b/lib/src/widgets/platform_scaffold.dart @@ -2,6 +2,11 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; +const kCupertinoAppBarWithActionPadding = EdgeInsetsDirectional.only( + start: 16.0, + end: 8.0, +); + /// Displays an [AppBar] for Android and a [CupertinoNavigationBar] for iOS. /// /// Intended to be passed to [PlatformScaffold]. @@ -12,7 +17,6 @@ class PlatformAppBar extends StatelessWidget { this.centerTitle = false, this.leading, this.actions = const [], - this.cupertinoPadding, this.androidTitleSpacing, }); @@ -35,9 +39,6 @@ class PlatformAppBar extends StatelessWidget { /// On iOS, this is wrapped in a [Row] and passed to [CupertinoNavigationBar.trailing] final List actions; - /// Will be passed to [CupertinoNavigationBar.padding] on iOS. Has no effect on Android. - final EdgeInsetsDirectional? cupertinoPadding; - /// Will be passed to [AppBar.titleSpacing] on Android. Has no effect on iOS. final double? androidTitleSpacing; @@ -53,7 +54,7 @@ class PlatformAppBar extends StatelessWidget { CupertinoNavigationBar _iosBuilder(BuildContext context) { return CupertinoNavigationBar( - padding: cupertinoPadding, + padding: actions.isNotEmpty ? kCupertinoAppBarWithActionPadding : null, middle: title, trailing: Row( mainAxisSize: MainAxisSize.min, From 0739d97ecdddb7b9d9b14f79d11b441bb67788e4 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 9 Sep 2024 16:12:25 +0200 Subject: [PATCH 300/979] Improve bottom sheets --- lib/src/view/analysis/analysis_settings.dart | 175 +++--- lib/src/view/analysis/tree_view.dart | 268 ++++---- lib/src/view/game/game_common_widgets.dart | 327 +++++----- lib/src/view/game/game_list_tile.dart | 589 +++++++++--------- lib/src/view/game/game_settings.dart | 213 +++---- .../opening_explorer_settings.dart | 80 ++- lib/src/view/play/online_bots_screen.dart | 122 ++-- .../view/puzzle/puzzle_settings_screen.dart | 100 ++- 8 files changed, 885 insertions(+), 989 deletions(-) diff --git a/lib/src/view/analysis/analysis_settings.dart b/lib/src/view/analysis/analysis_settings.dart index 725619a68a..d2efbd7dd5 100644 --- a/lib/src/view/analysis/analysis_settings.dart +++ b/lib/src/view/analysis/analysis_settings.dart @@ -5,7 +5,6 @@ import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; import 'package:lichess_mobile/src/model/engine/evaluation_service.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/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/non_linear_slider.dart'; @@ -30,31 +29,51 @@ class AnalysisSettings extends ConsumerWidget { generalPreferencesProvider.select((pref) => pref.isSoundEnabled), ); - return DraggableScrollableSheet( - initialChildSize: 1.0, - expand: false, - builder: (context, scrollController) => ListView( - controller: scrollController, - children: [ - PlatformListTile( - title: - Text(context.l10n.settingsSettings, style: Styles.sectionTitle), - subtitle: const SizedBox.shrink(), + return ListView( + shrinkWrap: true, + children: [ + SwitchSettingTile( + title: Text(context.l10n.toggleLocalEvaluation), + value: prefs.enableLocalEvaluation, + onChanged: isLocalEvaluationAllowed + ? (_) { + ref.read(ctrlProvider.notifier).toggleLocalEvaluation(); + } + : null, + ), + PlatformListTile( + title: Text.rich( + TextSpan( + text: '${context.l10n.multipleLines}: ', + style: const TextStyle( + fontWeight: FontWeight.normal, + ), + children: [ + TextSpan( + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + text: prefs.numEvalLines.toString(), + ), + ], + ), ), - const SizedBox(height: 8.0), - SwitchSettingTile( - title: Text(context.l10n.toggleLocalEvaluation), - value: prefs.enableLocalEvaluation, - onChanged: isLocalEvaluationAllowed - ? (_) { - ref.read(ctrlProvider.notifier).toggleLocalEvaluation(); - } + subtitle: NonLinearSlider( + value: prefs.numEvalLines, + values: const [1, 2, 3], + onChangeEnd: isEngineAvailable + ? (value) => ref + .read(ctrlProvider.notifier) + .setNumEvalLines(value.toInt()) : null, ), + ), + if (maxEngineCores > 1) PlatformListTile( title: Text.rich( TextSpan( - text: '${context.l10n.multipleLines}: ', + text: '${context.l10n.cpus}: ', style: const TextStyle( fontWeight: FontWeight.normal, ), @@ -64,91 +83,59 @@ class AnalysisSettings extends ConsumerWidget { fontWeight: FontWeight.bold, fontSize: 18, ), - text: prefs.numEvalLines.toString(), + text: prefs.numEngineCores.toString(), ), ], ), ), subtitle: NonLinearSlider( - value: prefs.numEvalLines, - values: const [1, 2, 3], + value: prefs.numEngineCores, + values: List.generate(maxEngineCores, (index) => index + 1), onChangeEnd: isEngineAvailable ? (value) => ref .read(ctrlProvider.notifier) - .setNumEvalLines(value.toInt()) + .setEngineCores(value.toInt()) : null, ), ), - if (maxEngineCores > 1) - PlatformListTile( - title: Text.rich( - TextSpan( - text: '${context.l10n.cpus}: ', - style: const TextStyle( - fontWeight: FontWeight.normal, - ), - children: [ - TextSpan( - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - ), - text: prefs.numEngineCores.toString(), - ), - ], - ), - ), - subtitle: NonLinearSlider( - value: prefs.numEngineCores, - values: List.generate(maxEngineCores, (index) => index + 1), - onChangeEnd: isEngineAvailable - ? (value) => ref - .read(ctrlProvider.notifier) - .setEngineCores(value.toInt()) - : null, - ), - ), - SwitchSettingTile( - title: Text(context.l10n.bestMoveArrow), - value: prefs.showBestMoveArrow, - onChanged: isEngineAvailable - ? (value) => ref - .read(analysisPreferencesProvider.notifier) - .toggleShowBestMoveArrow() - : null, - ), - SwitchSettingTile( - title: Text(context.l10n.evaluationGauge), - value: prefs.showEvaluationGauge, - onChanged: (value) => ref - .read(analysisPreferencesProvider.notifier) - .toggleShowEvaluationGauge(), - ), - SwitchSettingTile( - title: Text(context.l10n.toggleGlyphAnnotations), - value: prefs.showAnnotations, - onChanged: (_) => ref - .read(analysisPreferencesProvider.notifier) - .toggleAnnotations(), - ), - SwitchSettingTile( - title: Text(context.l10n.mobileShowComments), - value: prefs.showPgnComments, - onChanged: (_) => ref - .read(analysisPreferencesProvider.notifier) - .togglePgnComments(), - ), - SwitchSettingTile( - title: Text(context.l10n.sound), - value: isSoundEnabled, - onChanged: (value) { - ref - .read(generalPreferencesProvider.notifier) - .toggleSoundEnabled(); - }, - ), - ], - ), + SwitchSettingTile( + title: Text(context.l10n.bestMoveArrow), + value: prefs.showBestMoveArrow, + onChanged: isEngineAvailable + ? (value) => ref + .read(analysisPreferencesProvider.notifier) + .toggleShowBestMoveArrow() + : null, + ), + SwitchSettingTile( + title: Text(context.l10n.evaluationGauge), + value: prefs.showEvaluationGauge, + onChanged: (value) => ref + .read(analysisPreferencesProvider.notifier) + .toggleShowEvaluationGauge(), + ), + SwitchSettingTile( + title: Text(context.l10n.toggleGlyphAnnotations), + value: prefs.showAnnotations, + onChanged: (_) => ref + .read(analysisPreferencesProvider.notifier) + .toggleAnnotations(), + ), + SwitchSettingTile( + title: Text(context.l10n.mobileShowComments), + value: prefs.showPgnComments, + onChanged: (_) => ref + .read(analysisPreferencesProvider.notifier) + .togglePgnComments(), + ), + SwitchSettingTile( + title: Text(context.l10n.sound), + value: isSoundEnabled, + onChanged: (value) { + ref.read(generalPreferencesProvider.notifier).toggleSoundEnabled(); + }, + ), + ], ); } } diff --git a/lib/src/view/analysis/tree_view.dart b/lib/src/view/analysis/tree_view.dart index 8ee1655c7b..8cdb736479 100644 --- a/lib/src/view/analysis/tree_view.dart +++ b/lib/src/view/analysis/tree_view.dart @@ -488,139 +488,122 @@ class _MoveContextMenu extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final ctrlProvider = analysisControllerProvider(pgn, options); - return DraggableScrollableSheet( - initialChildSize: .3, - expand: false, - snap: true, - snapSizes: const [.3, .7], - builder: (context, scrollController) => SingleChildScrollView( - controller: scrollController, - child: Padding( - padding: const EdgeInsets.only(bottom: 16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, + return ListView( + shrinkWrap: true, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Padding( - padding: - const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, + Text( + title, + style: Theme.of(context).textTheme.titleLarge, + ), + if (branch.clock != null) + Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - title, - style: Theme.of(context).textTheme.titleLarge, + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.punch_clock, + ), + const SizedBox(width: 4.0), + Text( + branch.clock!.toHoursMinutesSeconds( + showTenths: + branch.clock! < const Duration(minutes: 1), + ), + ), + ], ), - if (branch.clock != null) - Column( - crossAxisAlignment: CrossAxisAlignment.start, + if (branch.elapsedMoveTime != null) ...[ + const SizedBox(height: 4.0), + Row( + mainAxisSize: MainAxisSize.min, children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon( - Icons.punch_clock, - ), - const SizedBox(width: 4.0), - Text( - branch.clock!.toHoursMinutesSeconds( - showTenths: branch.clock! < - const Duration(minutes: 1), - ), - ), - ], + const Icon( + Icons.hourglass_bottom, + ), + const SizedBox(width: 4.0), + Text( + branch.elapsedMoveTime! + .toHoursMinutesSeconds(showTenths: true), ), - if (branch.elapsedMoveTime != null) ...[ - const SizedBox(height: 4.0), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon( - Icons.hourglass_bottom, - ), - const SizedBox(width: 4.0), - Text( - branch.elapsedMoveTime! - .toHoursMinutesSeconds(showTenths: true), - ), - ], - ), - ], ], ), + ], ], ), - ), - if (branch.hasLichessAnalysisTextComment) - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 8.0, - ), - child: Text( - branch.lichessAnalysisComments! - .map((c) => c.text ?? '') - .join(' '), - ), - ), - if (branch.hasTextComment) - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 8.0, - ), - child: Text( - branch.comments!.map((c) => c.text ?? '').join(' '), - ), - ), - const PlatformDivider(indent: 0), - if (parent.children.any((c) => c.isHidden)) - BottomSheetContextMenuAction( - icon: Icons.subtitles, - child: Text(context.l10n.mobileShowVariations), - onPressed: () { - ref.read(ctrlProvider.notifier).showAllVariations(path); - }, - ), - if (isSideline) - BottomSheetContextMenuAction( - icon: Icons.subtitles_off, - child: Text(context.l10n.mobileHideVariation), - onPressed: () { - ref.read(ctrlProvider.notifier).hideVariation(path); - }, - ), - if (isSideline) - BottomSheetContextMenuAction( - icon: Icons.expand_less, - child: Text(context.l10n.promoteVariation), - onPressed: () { - ref - .read(ctrlProvider.notifier) - .promoteVariation(path, false); - }, - ), - if (isSideline) - BottomSheetContextMenuAction( - icon: Icons.check, - child: Text(context.l10n.makeMainLine), - onPressed: () { - ref - .read(ctrlProvider.notifier) - .promoteVariation(path, true); - }, - ), - BottomSheetContextMenuAction( - icon: Icons.delete, - child: Text(context.l10n.deleteFromHere), - onPressed: () { - ref.read(ctrlProvider.notifier).deleteFromHere(path); - }, - ), ], ), ), - ), + if (branch.hasLichessAnalysisTextComment) + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 8.0, + ), + child: Text( + branch.lichessAnalysisComments! + .map((c) => c.text ?? '') + .join(' '), + ), + ), + if (branch.hasTextComment) + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 8.0, + ), + child: Text( + branch.comments!.map((c) => c.text ?? '').join(' '), + ), + ), + const PlatformDivider(indent: 0), + if (parent.children.any((c) => c.isHidden)) + BottomSheetContextMenuAction( + icon: Icons.subtitles, + child: Text(context.l10n.mobileShowVariations), + onPressed: () { + ref.read(ctrlProvider.notifier).showAllVariations(path); + }, + ), + if (isSideline) + BottomSheetContextMenuAction( + icon: Icons.subtitles_off, + child: Text(context.l10n.mobileHideVariation), + onPressed: () { + ref.read(ctrlProvider.notifier).hideVariation(path); + }, + ), + if (isSideline) + BottomSheetContextMenuAction( + icon: Icons.expand_less, + child: Text(context.l10n.promoteVariation), + onPressed: () { + ref.read(ctrlProvider.notifier).promoteVariation(path, false); + }, + ), + if (isSideline) + BottomSheetContextMenuAction( + icon: Icons.check, + child: Text(context.l10n.makeMainLine), + onPressed: () { + ref.read(ctrlProvider.notifier).promoteVariation(path, true); + }, + ), + BottomSheetContextMenuAction( + icon: Icons.delete, + child: Text(context.l10n.deleteFromHere), + onPressed: () { + ref.read(ctrlProvider.notifier).deleteFromHere(path); + }, + ), + ], ); } } @@ -639,31 +622,26 @@ class _Comments extends StatelessWidget { showAdaptiveBottomSheet( context: context, isDismissible: true, + showDragHandle: true, isScrollControlled: true, - builder: (context) => DraggableScrollableSheet( - initialChildSize: 0.3, - expand: false, - snap: true, - snapSizes: const [.3, .7], - builder: (context, scrollController) => SingleChildScrollView( - controller: scrollController, - child: Padding( - padding: const EdgeInsets.all(24.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: comments.map( - (comment) { - if (comment.text == null || comment.text!.isEmpty) { - return const SizedBox.shrink(); - } - return Padding( - padding: const EdgeInsets.only(bottom: 16.0), - child: Text(comment.text!.replaceAll('\n', ' ')), - ); - }, - ).toList(), - ), - ), + builder: (context) => Padding( + padding: const EdgeInsets.symmetric( + vertical: 8.0, + horizontal: 16.0, + ), + child: ListView( + shrinkWrap: true, + children: comments.map( + (comment) { + if (comment.text == null || comment.text!.isEmpty) { + return const SizedBox.shrink(); + } + return Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: Text(comment.text!.replaceAll('\n', ' ')), + ); + }, + ).toList(), ), ), ); diff --git a/lib/src/view/game/game_common_widgets.dart b/lib/src/view/game/game_common_widgets.dart index ad4e671a84..db69af84c4 100644 --- a/lib/src/view/game/game_common_widgets.dart +++ b/lib/src/view/game/game_common_widgets.dart @@ -128,182 +128,171 @@ class GameShareBottomSheet extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return DraggableScrollableSheet( - initialChildSize: .5, - expand: false, - snap: true, - snapSizes: const [.5], - builder: (context, scrollController) => SingleChildScrollView( - controller: scrollController, - child: Column( - children: [ - BottomSheetContextMenuAction( - icon: CupertinoIcons.link, - closeOnPressed: false, - onPressed: () { - launchShareDialog( - context, - uri: lichessUri('/${game.id}'), - ); + return ListView( + shrinkWrap: true, + children: [ + BottomSheetContextMenuAction( + icon: CupertinoIcons.link, + closeOnPressed: false, + onPressed: () { + launchShareDialog( + context, + uri: lichessUri('/${game.id}'), + ); + }, + child: Text(context.l10n.mobileShareGameURL), + ), + // Builder is used to retrieve the context immediately surrounding the + // BottomSheetContextMenuAction + // This is necessary to get the correct context for the iPad share dialog + // which needs the position of the action to display the share dialog + Builder( + builder: (context) { + return BottomSheetContextMenuAction( + icon: Icons.gif, + closeOnPressed: false, // needed for the share dialog on iPad + child: Text(context.l10n.gameAsGIF), + onPressed: () async { + try { + final gif = await ref + .read(gameShareServiceProvider) + .gameGif(game.id, orientation); + if (context.mounted) { + launchShareDialog( + context, + files: [gif], + subject: '${game.meta.perf.title} • ${context.l10n.resVsX( + game.white.fullName(context), + game.black.fullName(context), + )}', + ); + } + } catch (e) { + debugPrint(e.toString()); + if (context.mounted) { + showPlatformSnackbar( + context, + 'Failed to get GIF', + type: SnackBarType.error, + ); + } + } }, - child: Text(context.l10n.mobileShareGameURL), - ), - // Builder is used to retrieve the context immediately surrounding the - // BottomSheetContextMenuAction - // This is necessary to get the correct context for the iPad share dialog - // which needs the position of the action to display the share dialog - Builder( - builder: (context) { - return BottomSheetContextMenuAction( - icon: Icons.gif, - closeOnPressed: false, // needed for the share dialog on iPad - child: Text(context.l10n.gameAsGIF), - onPressed: () async { - try { - final gif = await ref - .read(gameShareServiceProvider) - .gameGif(game.id, orientation); - if (context.mounted) { - launchShareDialog( - context, - files: [gif], - subject: - '${game.meta.perf.title} • ${context.l10n.resVsX( - game.white.fullName(context), - game.black.fullName(context), - )}', - ); - } - } catch (e) { - debugPrint(e.toString()); - if (context.mounted) { - showPlatformSnackbar( - context, - 'Failed to get GIF', - type: SnackBarType.error, + ); + }, + ), + if (lastMove != null) + // Builder is used to retrieve the context immediately surrounding the + // BottomSheetContextMenuAction + // This is necessary to get the correct context for the iPad share dialog + // which needs the position of the action to display the share dialog + Builder( + builder: (context) { + return BottomSheetContextMenuAction( + icon: Icons.image, + closeOnPressed: false, // needed for the share dialog on iPad + child: Text(context.l10n.screenshotCurrentPosition), + onPressed: () async { + try { + final image = await ref + .read(gameShareServiceProvider) + .screenshotPosition( + game.id, + orientation, + currentGamePosition.fen, + lastMove, ); - } + if (context.mounted) { + launchShareDialog( + context, + files: [image], + subject: context.l10n.puzzleFromGameLink( + lichessUri('/${game.id}').toString(), + ), + ); } - }, - ); - }, - ), - if (lastMove != null) - // Builder is used to retrieve the context immediately surrounding the - // BottomSheetContextMenuAction - // This is necessary to get the correct context for the iPad share dialog - // which needs the position of the action to display the share dialog - Builder( - builder: (context) { - return BottomSheetContextMenuAction( - icon: Icons.image, - closeOnPressed: - false, // needed for the share dialog on iPad - child: Text(context.l10n.screenshotCurrentPosition), - onPressed: () async { - try { - final image = await ref - .read(gameShareServiceProvider) - .screenshotPosition( - game.id, - orientation, - currentGamePosition.fen, - lastMove, - ); - if (context.mounted) { - launchShareDialog( - context, - files: [image], - subject: context.l10n.puzzleFromGameLink( - lichessUri('/${game.id}').toString(), - ), - ); - } - } catch (e) { - if (context.mounted) { - showPlatformSnackbar( - context, - 'Failed to get GIF', - type: SnackBarType.error, - ); - } - } - }, - ); - }, - ), - // Builder is used to retrieve the context immediately surrounding the - // BottomSheetContextMenuAction - // This is necessary to get the correct context for the iPad share dialog - // which needs the position of the action to display the share dialog - Builder( - builder: (context) { - return BottomSheetContextMenuAction( - icon: Icons.text_snippet, - closeOnPressed: false, // needed for the share dialog on iPad - child: Text('PGN: ${context.l10n.downloadAnnotated}'), - onPressed: () async { - try { - final pgn = await ref - .read(gameShareServiceProvider) - .annotatedPgn(game.id); - if (context.mounted) { - launchShareDialog( - context, - text: pgn, - ); - } - } catch (e) { - if (context.mounted) { - showPlatformSnackbar( - context, - 'Failed to get PGN', - type: SnackBarType.error, - ); - } + } catch (e) { + if (context.mounted) { + showPlatformSnackbar( + context, + 'Failed to get GIF', + type: SnackBarType.error, + ); } - }, - ); + } + }, + ); + }, + ), + // Builder is used to retrieve the context immediately surrounding the + // BottomSheetContextMenuAction + // This is necessary to get the correct context for the iPad share dialog + // which needs the position of the action to display the share dialog + Builder( + builder: (context) { + return BottomSheetContextMenuAction( + icon: Icons.text_snippet, + closeOnPressed: false, // needed for the share dialog on iPad + child: Text('PGN: ${context.l10n.downloadAnnotated}'), + onPressed: () async { + try { + final pgn = await ref + .read(gameShareServiceProvider) + .annotatedPgn(game.id); + if (context.mounted) { + launchShareDialog( + context, + text: pgn, + ); + } + } catch (e) { + if (context.mounted) { + showPlatformSnackbar( + context, + 'Failed to get PGN', + type: SnackBarType.error, + ); + } + } }, - ), - // Builder is used to retrieve the context immediately surrounding the - // BottomSheetContextMenuAction - // This is necessary to get the correct context for the iPad share dialog - // which needs the position of the action to display the share dialog - Builder( - builder: (context) { - return BottomSheetContextMenuAction( - icon: Icons.text_snippet, - closeOnPressed: false, // needed for the share dialog on iPad - // TODO improve translation - child: Text('PGN: ${context.l10n.downloadRaw}'), - onPressed: () async { - try { - final pgn = await ref - .read(gameShareServiceProvider) - .rawPgn(game.id); - if (context.mounted) { - launchShareDialog( - context, - text: pgn, - ); - } - } catch (e) { - if (context.mounted) { - showPlatformSnackbar( - context, - 'Failed to get PGN', - type: SnackBarType.error, - ); - } - } - }, - ); + ); + }, + ), + // Builder is used to retrieve the context immediately surrounding the + // BottomSheetContextMenuAction + // This is necessary to get the correct context for the iPad share dialog + // which needs the position of the action to display the share dialog + Builder( + builder: (context) { + return BottomSheetContextMenuAction( + icon: Icons.text_snippet, + closeOnPressed: false, // needed for the share dialog on iPad + // TODO improve translation + child: Text('PGN: ${context.l10n.downloadRaw}'), + onPressed: () async { + try { + final pgn = + await ref.read(gameShareServiceProvider).rawPgn(game.id); + if (context.mounted) { + launchShareDialog( + context, + text: pgn, + ); + } + } catch (e) { + if (context.mounted) { + showPlatformSnackbar( + context, + 'Failed to get PGN', + type: SnackBarType.error, + ); + } + } }, - ), - ], + ); + }, ), - ), + ], ); } } diff --git a/lib/src/view/game/game_list_tile.dart b/lib/src/view/game/game_list_tile.dart index 1882b85aa6..5b5631dc59 100644 --- a/lib/src/view/game/game_list_tile.dart +++ b/lib/src/view/game/game_list_tile.dart @@ -111,324 +111,303 @@ class _ContextMenu extends ConsumerWidget { final customColors = Theme.of(context).extension(); - return DraggableScrollableSheet( - initialChildSize: .7, - expand: false, - snap: true, - snapSizes: const [.7], - builder: (context, scrollController) => SingleChildScrollView( - controller: scrollController, - child: Padding( - padding: const EdgeInsets.only(bottom: 16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0).add( - const EdgeInsets.only(bottom: 8.0), - ), - child: Text( - context.l10n.resVsX( - game.white.fullName(context), - game.black.fullName(context), - ), - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - letterSpacing: -0.5, - ), - ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0).add( - const EdgeInsets.only(bottom: 8.0), - ), - child: LayoutBuilder( - builder: (context, constraints) { - return IntrinsicHeight( - child: Row( - mainAxisSize: MainAxisSize.max, - children: [ - if (game.lastFen != null) - BoardThumbnail( - size: constraints.maxWidth - - (constraints.maxWidth / 1.618), - fen: game.lastFen!, - orientation: mySide, - lastMove: game.lastMove, - ), - Expanded( - child: Padding( - padding: - const EdgeInsets.symmetric(horizontal: 8.0), - child: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - '${game.clockDisplay} • ${game.rated ? context.l10n.rated : context.l10n.casual}', - style: const TextStyle( - fontWeight: FontWeight.w500, - ), - ), - Text( - _dateFormatter.format(game.lastMoveAt), - style: TextStyle( - color: textShade( - context, - Styles.subtitleOpacity, - ), - fontSize: 12, - ), - ), - ], + return ListView( + shrinkWrap: true, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0).add( + const EdgeInsets.only(bottom: 8.0), + ), + child: Text( + context.l10n.resVsX( + game.white.fullName(context), + game.black.fullName(context), + ), + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + letterSpacing: -0.5, + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0).add( + const EdgeInsets.only(bottom: 8.0), + ), + child: LayoutBuilder( + builder: (context, constraints) { + return IntrinsicHeight( + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + if (game.lastFen != null) + BoardThumbnail( + size: constraints.maxWidth - + (constraints.maxWidth / 1.618), + fen: game.lastFen!, + orientation: mySide, + lastMove: game.lastMove, + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${game.clockDisplay} • ${game.rated ? context.l10n.rated : context.l10n.casual}', + style: const TextStyle( + fontWeight: FontWeight.w500, ), - if (game.lastFen != null) - Text( - gameStatusL10n( - context, - variant: game.variant, - status: game.status, - lastPosition: Position.setupPosition( - game.variant.rule, - Setup.parseFen(game.lastFen!), - ), - winner: game.winner, - ), - style: TextStyle( - color: game.winner == null - ? customColors?.brag - : game.winner == mySide - ? customColors?.good - : customColors?.error, - ), - ), - if (game.opening != null) - Text( - game.opening!.name, - maxLines: 2, - style: TextStyle( - color: textShade( - context, - Styles.subtitleOpacity, - ), - fontSize: 12, - ), - overflow: TextOverflow.ellipsis, + ), + Text( + _dateFormatter.format(game.lastMoveAt), + style: TextStyle( + color: textShade( + context, + Styles.subtitleOpacity, ), - ], - ), + fontSize: 12, + ), + ), + ], ), - ), - ], + if (game.lastFen != null) + Text( + gameStatusL10n( + context, + variant: game.variant, + status: game.status, + lastPosition: Position.setupPosition( + game.variant.rule, + Setup.parseFen(game.lastFen!), + ), + winner: game.winner, + ), + style: TextStyle( + color: game.winner == null + ? customColors?.brag + : game.winner == mySide + ? customColors?.good + : customColors?.error, + ), + ), + if (game.opening != null) + Text( + game.opening!.name, + maxLines: 2, + style: TextStyle( + color: textShade( + context, + Styles.subtitleOpacity, + ), + fontSize: 12, + ), + overflow: TextOverflow.ellipsis, + ), + ], + ), ), - ); - }, + ), + ], ), - ), - BottomSheetContextMenuAction( - icon: Icons.biotech, - onPressed: game.variant.isReadSupported - ? () { - pushPlatformRoute( - context, - builder: (context) => AnalysisScreen( - pgnOrId: game.id.value, - options: AnalysisOptions( - isLocalEvaluationAllowed: true, - variant: game.variant, - orientation: orientation, - id: game.id, - ), - ), - ); - } - : () { - showPlatformSnackbar( - context, - 'This variant is not supported yet.', - type: SnackBarType.info, - ); - }, - child: Text(context.l10n.gameAnalysis), - ), - BottomSheetContextMenuAction( - onPressed: () { - launchShareDialog( + ); + }, + ), + ), + BottomSheetContextMenuAction( + icon: Icons.biotech, + onPressed: game.variant.isReadSupported + ? () { + pushPlatformRoute( context, - uri: lichessUri('/${game.id}'), + builder: (context) => AnalysisScreen( + pgnOrId: game.id.value, + options: AnalysisOptions( + isLocalEvaluationAllowed: true, + variant: game.variant, + orientation: orientation, + id: game.id, + ), + ), ); - }, - icon: CupertinoIcons.link, - closeOnPressed: false, - child: Text(context.l10n.mobileShareGameURL), - ), - // Builder is used to retrieve the context immediately surrounding the - // BottomSheetContextMenuAction - // This is necessary to get the correct context for the iPad share dialog - // which needs the position of the action to display the share dialog - Builder( - builder: (context) { - return BottomSheetContextMenuAction( - icon: Icons.gif, - closeOnPressed: - false, // needed for the share dialog on iPad - child: Text(context.l10n.gameAsGIF), - onPressed: () async { - try { - final gif = await ref - .read(gameShareServiceProvider) - .gameGif(game.id, orientation); - if (context.mounted) { - launchShareDialog( - context, - files: [gif], - subject: - '${game.perf.title} • ${context.l10n.resVsX( - game.white.fullName(context), - game.black.fullName(context), - )}', - ); - } - } catch (e) { - debugPrint(e.toString()); - if (context.mounted) { - showPlatformSnackbar( - context, - 'Failed to get GIF', - type: SnackBarType.error, - ); - } - } - }, + } + : () { + showPlatformSnackbar( + context, + 'This variant is not supported yet.', + type: SnackBarType.info, ); }, - ), - if (game.lastFen != null) - // Builder is used to retrieve the context immediately surrounding the - // BottomSheetContextMenuAction - // This is necessary to get the correct context for the iPad share dialog - // which needs the position of the action to display the share dialog - Builder( - builder: (context) { - return BottomSheetContextMenuAction( - icon: Icons.image, - closeOnPressed: - false, // needed for the share dialog on iPad - child: Text(context.l10n.screenshotCurrentPosition), - onPressed: () async { - try { - final image = await ref - .read(gameShareServiceProvider) - .screenshotPosition( - game.id, - orientation, - game.lastFen!, - game.lastMove, - ); - if (context.mounted) { - launchShareDialog( - context, - files: [image], - subject: context.l10n.puzzleFromGameLink( - lichessUri('/${game.id}').toString(), - ), - ); - } - } catch (e) { - if (context.mounted) { - showPlatformSnackbar( - context, - 'Failed to get GIF', - type: SnackBarType.error, - ); - } - } - }, + child: Text(context.l10n.gameAnalysis), + ), + BottomSheetContextMenuAction( + onPressed: () { + launchShareDialog( + context, + uri: lichessUri('/${game.id}'), + ); + }, + icon: CupertinoIcons.link, + closeOnPressed: false, + child: Text(context.l10n.mobileShareGameURL), + ), + // Builder is used to retrieve the context immediately surrounding the + // BottomSheetContextMenuAction + // This is necessary to get the correct context for the iPad share dialog + // which needs the position of the action to display the share dialog + Builder( + builder: (context) { + return BottomSheetContextMenuAction( + icon: Icons.gif, + closeOnPressed: false, // needed for the share dialog on iPad + child: Text(context.l10n.gameAsGIF), + onPressed: () async { + try { + final gif = await ref + .read(gameShareServiceProvider) + .gameGif(game.id, orientation); + if (context.mounted) { + launchShareDialog( + context, + files: [gif], + subject: '${game.perf.title} • ${context.l10n.resVsX( + game.white.fullName(context), + game.black.fullName(context), + )}', ); - }, - ), - // Builder is used to retrieve the context immediately surrounding the - // BottomSheetContextMenuAction - // This is necessary to get the correct context for the iPad share dialog - // which needs the position of the action to display the share dialog - Builder( - builder: (context) { - return BottomSheetContextMenuAction( - icon: Icons.text_snippet, - closeOnPressed: - false, // needed for the share dialog on iPad - child: Text('PGN: ${context.l10n.downloadAnnotated}'), - onPressed: () async { - try { - final pgn = await ref - .read(gameShareServiceProvider) - .annotatedPgn(game.id); - if (context.mounted) { - launchShareDialog( - context, - text: pgn, - ); - } - } catch (e) { - if (context.mounted) { - showPlatformSnackbar( - context, - 'Failed to get PGN', - type: SnackBarType.error, - ); - } - } - }, - ); - }, - ), - // Builder is used to retrieve the context immediately surrounding the - // BottomSheetContextMenuAction - // This is necessary to get the correct context for the iPad share dialog - // which needs the position of the action to display the share dialog - Builder( - builder: (context) { - return BottomSheetContextMenuAction( - icon: Icons.text_snippet, - closeOnPressed: - false, // needed for the share dialog on iPad - // TODO improve translation - child: Text('PGN: ${context.l10n.downloadRaw}'), - onPressed: () async { - try { - final pgn = await ref - .read(gameShareServiceProvider) - .rawPgn(game.id); - if (context.mounted) { - launchShareDialog( - context, - text: pgn, - ); - } - } catch (e) { - if (context.mounted) { - showPlatformSnackbar( - context, - 'Failed to get PGN', - type: SnackBarType.error, - ); - } - } - }, - ); + } + } catch (e) { + debugPrint(e.toString()); + if (context.mounted) { + showPlatformSnackbar( + context, + 'Failed to get GIF', + type: SnackBarType.error, + ); + } + } + }, + ); + }, + ), + if (game.lastFen != null) + // Builder is used to retrieve the context immediately surrounding the + // BottomSheetContextMenuAction + // This is necessary to get the correct context for the iPad share dialog + // which needs the position of the action to display the share dialog + Builder( + builder: (context) { + return BottomSheetContextMenuAction( + icon: Icons.image, + closeOnPressed: false, // needed for the share dialog on iPad + child: Text(context.l10n.screenshotCurrentPosition), + onPressed: () async { + try { + final image = await ref + .read(gameShareServiceProvider) + .screenshotPosition( + game.id, + orientation, + game.lastFen!, + game.lastMove, + ); + if (context.mounted) { + launchShareDialog( + context, + files: [image], + subject: context.l10n.puzzleFromGameLink( + lichessUri('/${game.id}').toString(), + ), + ); + } + } catch (e) { + if (context.mounted) { + showPlatformSnackbar( + context, + 'Failed to get GIF', + type: SnackBarType.error, + ); + } + } }, - ), - ], + ); + }, ), + // Builder is used to retrieve the context immediately surrounding the + // BottomSheetContextMenuAction + // This is necessary to get the correct context for the iPad share dialog + // which needs the position of the action to display the share dialog + Builder( + builder: (context) { + return BottomSheetContextMenuAction( + icon: Icons.text_snippet, + closeOnPressed: false, // needed for the share dialog on iPad + child: Text('PGN: ${context.l10n.downloadAnnotated}'), + onPressed: () async { + try { + final pgn = await ref + .read(gameShareServiceProvider) + .annotatedPgn(game.id); + if (context.mounted) { + launchShareDialog( + context, + text: pgn, + ); + } + } catch (e) { + if (context.mounted) { + showPlatformSnackbar( + context, + 'Failed to get PGN', + type: SnackBarType.error, + ); + } + } + }, + ); + }, ), - ), + // Builder is used to retrieve the context immediately surrounding the + // BottomSheetContextMenuAction + // This is necessary to get the correct context for the iPad share dialog + // which needs the position of the action to display the share dialog + Builder( + builder: (context) { + return BottomSheetContextMenuAction( + icon: Icons.text_snippet, + closeOnPressed: false, // needed for the share dialog on iPad + // TODO improve translation + child: Text('PGN: ${context.l10n.downloadRaw}'), + onPressed: () async { + try { + final pgn = + await ref.read(gameShareServiceProvider).rawPgn(game.id); + if (context.mounted) { + launchShareDialog( + context, + text: pgn, + ); + } + } catch (e) { + if (context.mounted) { + showPlatformSnackbar( + context, + 'Failed to get PGN', + type: SnackBarType.error, + ); + } + } + }, + ); + }, + ), + ], ); } } diff --git a/lib/src/view/game/game_settings.dart b/lib/src/view/game/game_settings.dart index 9818b140ae..9a78f9ee23 100644 --- a/lib/src/view/game/game_settings.dart +++ b/lib/src/view/game/game_settings.dart @@ -7,9 +7,7 @@ import 'package:lichess_mobile/src/model/game/game_controller.dart'; import 'package:lichess_mobile/src/model/game/game_preferences.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.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/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/settings.dart'; import 'game_screen_providers.dart'; @@ -30,128 +28,117 @@ class GameSettings extends ConsumerWidget { final gamePrefs = ref.watch(gamePreferencesProvider); final userPrefsAsync = ref.watch(userGamePrefsProvider(id)); - final List content = [ - PlatformListTile( - title: Text(context.l10n.settingsSettings, style: Styles.sectionTitle), - subtitle: const SizedBox.shrink(), - ), - const SizedBox(height: 8.0), - SwitchSettingTile( - title: Text(context.l10n.sound), - value: isSoundEnabled, - onChanged: (value) { - ref.read(generalPreferencesProvider.notifier).toggleSoundEnabled(); - }, - ), - SwitchSettingTile( - title: Text(context.l10n.mobileSettingsHapticFeedback), - value: boardPrefs.hapticFeedback, - onChanged: (value) { - ref.read(boardPreferencesProvider.notifier).toggleHapticFeedback(); - }, - ), - ...userPrefsAsync.maybeWhen( - data: (data) { - return [ - if (data.prefs?.submitMove == true) - SwitchSettingTile( - title: Text( - context.l10n.preferencesMoveConfirmation, + return ListView( + shrinkWrap: true, + children: [ + SwitchSettingTile( + title: Text(context.l10n.sound), + value: isSoundEnabled, + onChanged: (value) { + ref.read(generalPreferencesProvider.notifier).toggleSoundEnabled(); + }, + ), + SwitchSettingTile( + title: Text(context.l10n.mobileSettingsHapticFeedback), + value: boardPrefs.hapticFeedback, + onChanged: (value) { + ref.read(boardPreferencesProvider.notifier).toggleHapticFeedback(); + }, + ), + ...userPrefsAsync.maybeWhen( + data: (data) { + return [ + if (data.prefs?.submitMove == true) + SwitchSettingTile( + title: Text( + context.l10n.preferencesMoveConfirmation, + ), + value: data.shouldConfirmMove, + onChanged: (value) { + ref + .read(gameControllerProvider(id).notifier) + .toggleMoveConfirmation(); + }, + ), + if (data.prefs?.autoQueen == AutoQueen.always) + SwitchSettingTile( + title: Text( + context.l10n.preferencesPromoteToQueenAutomatically, + ), + value: data.canAutoQueen, + onChanged: (value) { + ref + .read(gameControllerProvider(id).notifier) + .toggleAutoQueen(); + }, ), - value: data.shouldConfirmMove, - onChanged: (value) { - ref - .read(gameControllerProvider(id).notifier) - .toggleMoveConfirmation(); - }, - ), - if (data.prefs?.autoQueen == AutoQueen.always) SwitchSettingTile( title: Text( - context.l10n.preferencesPromoteToQueenAutomatically, + context.l10n.preferencesZenMode, ), - value: data.canAutoQueen, + value: data.isZenModeEnabled, onChanged: (value) { - ref - .read(gameControllerProvider(id).notifier) - .toggleAutoQueen(); + ref.read(gameControllerProvider(id).notifier).toggleZenMode(); }, ), - SwitchSettingTile( - title: Text( - context.l10n.preferencesZenMode, - ), - value: data.isZenModeEnabled, - onChanged: (value) { - ref.read(gameControllerProvider(id).notifier).toggleZenMode(); - }, - ), - ]; - }, - orElse: () => [], - ), - SwitchSettingTile( - // TODO: Add l10n - title: const Text('Shape drawing'), - subtitle: const Text( - 'Draw shapes using two fingers.', - maxLines: 5, - textAlign: TextAlign.justify, + ]; + }, + orElse: () => [], ), - value: boardPrefs.enableShapeDrawings, - onChanged: (value) { - ref - .read(boardPreferencesProvider.notifier) - .toggleEnableShapeDrawings(); - }, - ), - SwitchSettingTile( - title: Text( - context.l10n.preferencesPieceAnimation, + SwitchSettingTile( + // TODO: Add l10n + title: const Text('Shape drawing'), + subtitle: const Text( + 'Draw shapes using two fingers.', + maxLines: 5, + textAlign: TextAlign.justify, + ), + value: boardPrefs.enableShapeDrawings, + onChanged: (value) { + ref + .read(boardPreferencesProvider.notifier) + .toggleEnableShapeDrawings(); + }, ), - value: boardPrefs.pieceAnimation, - onChanged: (value) { - ref.read(boardPreferencesProvider.notifier).togglePieceAnimation(); - }, - ), - SwitchSettingTile( - title: Text( - context.l10n.preferencesMaterialDifference, + SwitchSettingTile( + title: Text( + context.l10n.preferencesPieceAnimation, + ), + value: boardPrefs.pieceAnimation, + onChanged: (value) { + ref.read(boardPreferencesProvider.notifier).togglePieceAnimation(); + }, ), - value: boardPrefs.showMaterialDifference, - onChanged: (value) { - ref - .read(boardPreferencesProvider.notifier) - .toggleShowMaterialDifference(); - }, - ), - SwitchSettingTile( - title: Text( - context.l10n.toggleTheChat, + SwitchSettingTile( + title: Text( + context.l10n.preferencesMaterialDifference, + ), + value: boardPrefs.showMaterialDifference, + onChanged: (value) { + ref + .read(boardPreferencesProvider.notifier) + .toggleShowMaterialDifference(); + }, ), - value: gamePrefs.enableChat ?? false, - onChanged: (value) { - ref.read(gamePreferencesProvider.notifier).toggleChat(); - ref.read(gameControllerProvider(id).notifier).onToggleChat(value); - }, - ), - SwitchSettingTile( - title: Text(context.l10n.mobileBlindfoldMode), - value: gamePrefs.blindfoldMode ?? false, - onChanged: (value) { - ref.read(gamePreferencesProvider.notifier).toggleBlindfoldMode(); - }, - ), - const SizedBox(height: 16.0), - ]; - - return DraggableScrollableSheet( - initialChildSize: 1.0, - expand: false, - builder: (context, scrollController) => ListView( - controller: scrollController, - children: content, - ), + SwitchSettingTile( + title: Text( + context.l10n.toggleTheChat, + ), + value: gamePrefs.enableChat ?? false, + onChanged: (value) { + ref.read(gamePreferencesProvider.notifier).toggleChat(); + ref.read(gameControllerProvider(id).notifier).onToggleChat(value); + }, + ), + SwitchSettingTile( + title: Text(context.l10n.mobileBlindfoldMode), + value: gamePrefs.blindfoldMode ?? false, + onChanged: (value) { + ref.read(gamePreferencesProvider.notifier).toggleBlindfoldMode(); + }, + ), + const SizedBox(height: 16.0), + ], ); } } diff --git a/lib/src/view/opening_explorer/opening_explorer_settings.dart b/lib/src/view/opening_explorer/opening_explorer_settings.dart index 6b01a12de9..1ace2c878f 100644 --- a/lib/src/view/opening_explorer/opening_explorer_settings.dart +++ b/lib/src/view/opening_explorer/opening_explorer_settings.dart @@ -217,50 +217,44 @@ class OpeningExplorerSettings extends ConsumerWidget { ), ]; - return DraggableScrollableSheet( - initialChildSize: .5, - expand: false, - snap: true, - snapSizes: const [.5, .75], - builder: (context, scrollController) => ListView( - controller: scrollController, - children: [ - PlatformListTile( - title: Text(context.l10n.database), - subtitle: Wrap( - spacing: 5, - children: [ - ChoiceChip( - label: const Text('Masters'), - selected: prefs.db == OpeningDatabase.master, - onSelected: (_) => ref - .read(openingExplorerPreferencesProvider.notifier) - .setDatabase(OpeningDatabase.master), - ), - ChoiceChip( - label: const Text('Lichess'), - selected: prefs.db == OpeningDatabase.lichess, - onSelected: (_) => ref - .read(openingExplorerPreferencesProvider.notifier) - .setDatabase(OpeningDatabase.lichess), - ), - ChoiceChip( - label: Text(context.l10n.player), - selected: prefs.db == OpeningDatabase.player, - onSelected: (_) => ref - .read(openingExplorerPreferencesProvider.notifier) - .setDatabase(OpeningDatabase.player), - ), - ], - ), + return ListView( + shrinkWrap: true, + children: [ + PlatformListTile( + title: Text(context.l10n.database), + subtitle: Wrap( + spacing: 5, + children: [ + ChoiceChip( + label: const Text('Masters'), + selected: prefs.db == OpeningDatabase.master, + onSelected: (_) => ref + .read(openingExplorerPreferencesProvider.notifier) + .setDatabase(OpeningDatabase.master), + ), + ChoiceChip( + label: const Text('Lichess'), + selected: prefs.db == OpeningDatabase.lichess, + onSelected: (_) => ref + .read(openingExplorerPreferencesProvider.notifier) + .setDatabase(OpeningDatabase.lichess), + ), + ChoiceChip( + label: Text(context.l10n.player), + selected: prefs.db == OpeningDatabase.player, + onSelected: (_) => ref + .read(openingExplorerPreferencesProvider.notifier) + .setDatabase(OpeningDatabase.player), + ), + ], ), - ...switch (prefs.db) { - OpeningDatabase.master => masterDbSettings, - OpeningDatabase.lichess => lichessDbSettings, - OpeningDatabase.player => playerDbSettings, - }, - ], - ), + ), + ...switch (prefs.db) { + OpeningDatabase.master => masterDbSettings, + OpeningDatabase.lichess => lichessDbSettings, + OpeningDatabase.player => playerDbSettings, + }, + ], ); } } diff --git a/lib/src/view/play/online_bots_screen.dart b/lib/src/view/play/online_bots_screen.dart index fa18dcff40..190b13839d 100644 --- a/lib/src/view/play/online_bots_screen.dart +++ b/lib/src/view/play/online_bots_screen.dart @@ -168,6 +168,7 @@ class _Body extends ConsumerWidget { useRootNavigator: true, isDismissible: true, isScrollControlled: true, + showDragHandle: true, builder: (context) => _ContextMenu(bot: bot), ); }, @@ -194,72 +195,69 @@ class _ContextMenu extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return DraggableScrollableSheet( - initialChildSize: .6, - expand: false, - snap: true, - snapSizes: const [.6, .8], - builder: (context, scrollController) => ListView( - controller: scrollController, - children: [ - Padding( - padding: Styles.bodyPadding.add(const EdgeInsets.only(top: 8.0)), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - UserFullNameWidget( - user: bot.lightUser, - style: Styles.title, - ), - const SizedBox(height: 8.0), - if (bot.profile?.bio != null) - Linkify( - onOpen: (link) async { - if (link.originText.startsWith('@')) { - final username = link.originText.substring(1); - pushPlatformRoute( - context, - builder: (ctx) => UserScreen( - user: LightUser( - id: UserId.fromUserName(username), - name: username, - ), + return ListView( + shrinkWrap: true, + children: [ + Padding( + padding: Styles.horizontalBodyPadding.add( + const EdgeInsets.only(bottom: 16.0), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + UserFullNameWidget( + user: bot.lightUser, + style: Styles.title, + ), + const SizedBox(height: 8.0), + if (bot.profile?.bio != null) + Linkify( + onOpen: (link) async { + if (link.originText.startsWith('@')) { + final username = link.originText.substring(1); + pushPlatformRoute( + context, + builder: (ctx) => UserScreen( + user: LightUser( + id: UserId.fromUserName(username), + name: username, ), - ); - } else { - launchUrl(Uri.parse(link.url)); - } - }, - linkifiers: const [ - UrlLinkifier(), - EmailLinkifier(), - UserTagLinkifier(), - ], - text: bot.profile!.bio!, - maxLines: 20, - overflow: TextOverflow.ellipsis, - linkStyle: const TextStyle( - color: Colors.blueAccent, - decoration: TextDecoration.none, - ), + ), + ); + } else { + launchUrl(Uri.parse(link.url)); + } + }, + linkifiers: const [ + UrlLinkifier(), + EmailLinkifier(), + UserTagLinkifier(), + ], + text: bot.profile!.bio!, + maxLines: 20, + overflow: TextOverflow.ellipsis, + linkStyle: const TextStyle( + color: Colors.blueAccent, + decoration: TextDecoration.none, ), - ], - ), - ), - BottomSheetContextMenuAction( - onPressed: () { - pushPlatformRoute( - context, - builder: (context) => UserScreen( - user: bot.lightUser, ), - ); - }, - icon: Icons.person, - child: Text(context.l10n.profile), + ], ), - ], - ), + ), + const PlatformDivider(), + BottomSheetContextMenuAction( + onPressed: () { + pushPlatformRoute( + context, + builder: (context) => UserScreen( + user: bot.lightUser, + ), + ); + }, + icon: Icons.person, + child: Text(context.l10n.profile), + ), + ], ); } } diff --git a/lib/src/view/puzzle/puzzle_settings_screen.dart b/lib/src/view/puzzle/puzzle_settings_screen.dart index 24660c765c..81ff3b922f 100644 --- a/lib/src/view/puzzle/puzzle_settings_screen.dart +++ b/lib/src/view/puzzle/puzzle_settings_screen.dart @@ -5,9 +5,7 @@ import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_preferences.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.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/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/settings.dart'; class PuzzleSettingsScreen extends ConsumerWidget { @@ -25,64 +23,50 @@ class PuzzleSettingsScreen extends ConsumerWidget { ); final boardPrefs = ref.watch(boardPreferencesProvider); - return DraggableScrollableSheet( - initialChildSize: .6, - expand: false, - builder: (context, scrollController) => ListView( - controller: scrollController, - children: [ - PlatformListTile( - title: - Text(context.l10n.settingsSettings, style: Styles.sectionTitle), - subtitle: const SizedBox.shrink(), + return ListView( + shrinkWrap: true, + children: [ + SwitchSettingTile( + title: Text(context.l10n.sound), + value: isSoundEnabled, + onChanged: (value) { + ref.read(generalPreferencesProvider.notifier).toggleSoundEnabled(); + }, + ), + SwitchSettingTile( + title: Text(context.l10n.puzzleJumpToNextPuzzleImmediately), + value: autoNext, + onChanged: (value) { + ref + .read(puzzlePreferencesProvider(userId).notifier) + .setAutoNext(value); + }, + ), + SwitchSettingTile( + // TODO: Add l10n + title: const Text('Shape drawing'), + subtitle: const Text( + 'Draw shapes using two fingers.', + maxLines: 5, + textAlign: TextAlign.justify, ), - const SizedBox(height: 8.0), - SwitchSettingTile( - title: Text(context.l10n.sound), - value: isSoundEnabled, - onChanged: (value) { - ref - .read(generalPreferencesProvider.notifier) - .toggleSoundEnabled(); - }, + value: boardPrefs.enableShapeDrawings, + onChanged: (value) { + ref + .read(boardPreferencesProvider.notifier) + .toggleEnableShapeDrawings(); + }, + ), + SwitchSettingTile( + title: Text( + context.l10n.preferencesPieceAnimation, ), - SwitchSettingTile( - title: Text(context.l10n.puzzleJumpToNextPuzzleImmediately), - value: autoNext, - onChanged: (value) { - ref - .read(puzzlePreferencesProvider(userId).notifier) - .setAutoNext(value); - }, - ), - SwitchSettingTile( - // TODO: Add l10n - title: const Text('Shape drawing'), - subtitle: const Text( - 'Draw shapes using two fingers.', - maxLines: 5, - textAlign: TextAlign.justify, - ), - value: boardPrefs.enableShapeDrawings, - onChanged: (value) { - ref - .read(boardPreferencesProvider.notifier) - .toggleEnableShapeDrawings(); - }, - ), - SwitchSettingTile( - title: Text( - context.l10n.preferencesPieceAnimation, - ), - value: boardPrefs.pieceAnimation, - onChanged: (value) { - ref - .read(boardPreferencesProvider.notifier) - .togglePieceAnimation(); - }, - ), - ], - ), + value: boardPrefs.pieceAnimation, + onChanged: (value) { + ref.read(boardPreferencesProvider.notifier).togglePieceAnimation(); + }, + ), + ], ); } } From 387040df0892746288e1cf226bcfd52c8d72ad84 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 9 Sep 2024 16:13:00 +0200 Subject: [PATCH 301/979] Bump version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index d437ac658e..7805d9aedc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: lichess_mobile description: Lichess mobile app V2 publish_to: "none" -version: 0.10.1+001001 # see README.md for details about versioning +version: 0.10.2+001002 # see README.md for details about versioning environment: sdk: ">=3.3.0 <4.0.0" From f52d264c38bee2fac9525b69ef987050162a4fb7 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 9 Sep 2024 16:29:52 +0200 Subject: [PATCH 302/979] Improve cupertino fat button Closes #986 --- lib/src/view/play/quick_game_button.dart | 2 +- lib/src/widgets/buttons.dart | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/view/play/quick_game_button.dart b/lib/src/view/play/quick_game_button.dart index db28d5905d..aaeb306360 100644 --- a/lib/src/view/play/quick_game_button.dart +++ b/lib/src/view/play/quick_game_button.dart @@ -70,7 +70,7 @@ class QuickGameButton extends ConsumerWidget { Expanded( flex: kFlexGoldenRatio, child: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoButton.filled( + ? CupertinoButton.tinted( padding: const EdgeInsets.symmetric( horizontal: 8.0, vertical: 16.0, diff --git a/lib/src/widgets/buttons.dart b/lib/src/widgets/buttons.dart index a1405dcea9..9733efe29a 100644 --- a/lib/src/widgets/buttons.dart +++ b/lib/src/widgets/buttons.dart @@ -7,7 +7,7 @@ import 'package:flutter/services.dart'; /// Platform agnostic button which is used for important actions. /// -/// Will use an [FilledButton] on Android and a [CupertinoButton.filled] on iOS. +/// Will use an [FilledButton] on Android and a [CupertinoButton.tinted] on iOS. class FatButton extends StatelessWidget { const FatButton({ required this.semanticsLabel, @@ -29,7 +29,7 @@ class FatButton extends StatelessWidget { label: semanticsLabel, excludeSemantics: true, child: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoButton.filled(onPressed: onPressed, child: child) + ? CupertinoButton.tinted(onPressed: onPressed, child: child) : FilledButton( onPressed: onPressed, child: child, From 62224d514fd1267ae97db7ee7a77dc249400d78e Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 9 Sep 2024 16:36:52 +0200 Subject: [PATCH 303/979] Upgrade dependencies --- pubspec.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 8242329ca1..cc28534ccc 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -314,18 +314,18 @@ packages: dependency: "direct dev" description: name: custom_lint - sha256: "4939d89e580c36215e48a7de8fd92f22c79dcc3eb11fda84f3402b3b45aec663" + sha256: "6e1ec47427ca968f22bce734d00028ae7084361999b41673291138945c5baca0" url: "https://pub.dev" source: hosted - version: "0.6.5" + version: "0.6.7" custom_lint_builder: dependency: transitive description: name: custom_lint_builder - sha256: d9e5bb63ed52c1d006f5a1828992ba6de124c27a531e8fba0a31afffa81621b3 + sha256: ba2f90fff4eff71d202d097eb14b14f87087eaaef742e956208c0eb9d3a40a21 url: "https://pub.dev" source: hosted - version: "0.6.5" + version: "0.6.7" custom_lint_core: dependency: transitive description: From 22ea82c331b94a78a8dce0d2a925af907c3165c4 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Mon, 9 Sep 2024 00:08:39 +0200 Subject: [PATCH 304/979] feat: allow hiding flair in UserFullNameWidget --- lib/src/widgets/user_full_name.dart | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/src/widgets/user_full_name.dart b/lib/src/widgets/user_full_name.dart index d5e494d7fe..87fc25dc85 100644 --- a/lib/src/widgets/user_full_name.dart +++ b/lib/src/widgets/user_full_name.dart @@ -9,7 +9,7 @@ import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/lichess_assets.dart'; -/// Displays a user name, title, flair with an optional rating. +/// Displays a user name, title, flair (optional) with an optional rating. class UserFullNameWidget extends ConsumerWidget { const UserFullNameWidget({ required this.user, @@ -17,6 +17,7 @@ class UserFullNameWidget extends ConsumerWidget { this.rating, this.provisional, this.shouldShowOnline = false, + this.showFlair = true, this.style, super.key, }); @@ -27,6 +28,7 @@ class UserFullNameWidget extends ConsumerWidget { this.rating, this.provisional, this.shouldShowOnline = false, + this.showFlair = true, this.style, super.key, }); @@ -44,6 +46,9 @@ class UserFullNameWidget extends ConsumerWidget { /// Whether to show the online status. final bool? shouldShowOnline; + /// Whether to show the user's flair. Defaults to `true`. + final bool showFlair; + @override Widget build(BuildContext context, WidgetRef ref) { final provisionalStr = provisional == true ? '?' : ''; @@ -105,7 +110,7 @@ class UserFullNameWidget extends ConsumerWidget { style: style, ), ), - if (user?.flair != null) ...[ + if (showFlair && user?.flair != null) ...[ const SizedBox(width: 5), CachedNetworkImage( imageUrl: lichessFlairSrc(user!.flair!), From a9e872b1ea2476b8fc7709e437247bb47f755f06 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 10 Sep 2024 08:52:29 +0200 Subject: [PATCH 305/979] Refactor opening explorer --- .../opening_explorer_screen.dart | 810 +----------------- .../opening_explorer_widget.dart | 807 +++++++++++++++++ .../opening_explorer_screen_test.dart | 7 +- 3 files changed, 817 insertions(+), 807 deletions(-) create mode 100644 lib/src/view/opening_explorer/opening_explorer_widget.dart diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index 854fd7025f..b99b79dc04 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -1,47 +1,21 @@ -import 'package:dartchess/dartchess.dart'; -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; -import 'package:lichess_mobile/src/model/common/chess.dart'; -import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_preferences.dart'; -import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_repository.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; -import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_board.dart'; -import 'package:lichess_mobile/src/view/game/archived_game_screen.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; -import 'package:lichess_mobile/src/widgets/shimmer.dart'; -import 'package:url_launcher/url_launcher.dart'; import 'opening_explorer_settings.dart'; - -const _kTableRowVerticalPadding = 10.0; -const _kTableRowHorizontalPadding = 8.0; -const _kTableRowPadding = EdgeInsets.symmetric( - horizontal: _kTableRowHorizontalPadding, - vertical: _kTableRowVerticalPadding, -); - -Color _whiteBoxColor(BuildContext context) => - Theme.of(context).brightness == Brightness.dark - ? Colors.white.withOpacity(0.8) - : Colors.white; - -Color _blackBoxColor(BuildContext context) => - Theme.of(context).brightness == Brightness.light - ? Colors.black.withOpacity(0.7) - : Colors.black; +import 'opening_explorer_widget.dart'; class OpeningExplorerScreen extends StatelessWidget { const OpeningExplorerScreen({required this.pgn, required this.options}); @@ -121,7 +95,7 @@ class _Body extends ConsumerWidget { kTabletBoardTableSidePadding, ), semanticContainer: false, - child: _OpeningExplorer( + child: OpeningExplorerWidget( pgn: pgn, options: options, ), @@ -154,7 +128,10 @@ class _Body extends ConsumerWidget { isTablet: isTablet, ), Expanded( - child: _OpeningExplorer(pgn: pgn, options: options), + child: OpeningExplorerWidget( + pgn: pgn, + options: options, + ), ), ], ); @@ -168,781 +145,6 @@ class _Body extends ConsumerWidget { } } -class _OpeningExplorer extends ConsumerStatefulWidget { - const _OpeningExplorer({ - required this.pgn, - required this.options, - }); - - final String pgn; - final AnalysisOptions options; - - @override - ConsumerState<_OpeningExplorer> createState() => _OpeningExplorerState(); -} - -class _OpeningExplorerState extends ConsumerState<_OpeningExplorer> { - final Map cache = {}; - - /// Last explorer content that was successfully loaded. This is used to - /// display a loading indicator while the new content is being fetched. - List? lastExplorerWidgets; - - @override - Widget build(BuildContext context) { - final analysisState = - ref.watch(analysisControllerProvider(widget.pgn, widget.options)); - - if (analysisState.position.ply >= 50) { - return Align( - alignment: Alignment.center, - child: Text(context.l10n.maxDepthReached), - ); - } - - final prefs = ref.watch(openingExplorerPreferencesProvider); - if (prefs.db == OpeningDatabase.player && prefs.playerDb.username == null) { - return const Align( - alignment: Alignment.center, - child: Text('Select a Lichess player in the settings'), - ); - } - - final opening = analysisState.currentNode.isRoot - ? LightOpening( - eco: '', - name: context.l10n.startPosition, - ) - : analysisState.currentNode.opening ?? - analysisState.currentBranchOpening ?? - analysisState.contextOpening; - - final cacheKey = OpeningExplorerCacheKey( - fen: analysisState.position.fen, - prefs: prefs, - ); - final cacheOpeningExplorer = cache[cacheKey]; - final openingExplorerAsync = cacheOpeningExplorer != null - ? AsyncValue.data( - (entry: cacheOpeningExplorer, isIndexing: false), - ) - : ref.watch(openingExplorerProvider(fen: analysisState.position.fen)); - - if (cacheOpeningExplorer == null) { - ref.listen(openingExplorerProvider(fen: analysisState.position.fen), - (_, curAsync) { - curAsync.whenData((cur) { - if (cur != null && !cur.isIndexing) { - cache[cacheKey] = cur.entry; - } - }); - }); - } - - return openingExplorerAsync.when( - data: (openingExplorer) { - if (openingExplorer == null) { - return _OpeningExplorerView.loading( - pgn: widget.pgn, - options: widget.options, - opening: opening, - explorerContent: lastExplorerWidgets ?? - [ - Shimmer( - child: ShimmerLoading( - isLoading: true, - child: OpeningExplorerMoveTable.loading( - pgn: widget.pgn, - options: widget.options, - ), - ), - ), - ], - ); - } - if (openingExplorer.entry.moves.isEmpty) { - lastExplorerWidgets = null; - return _OpeningExplorerView( - pgn: widget.pgn, - options: widget.options, - opening: opening, - openingExplorer: openingExplorer, - explorerContent: [ - Center( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Text(context.l10n.noGameFound), - ), - ), - ], - ); - } - - final topGames = openingExplorer.entry.topGames; - final recentGames = openingExplorer.entry.recentGames; - - final ply = analysisState.position.ply; - - final explorerContent = [ - OpeningExplorerMoveTable( - moves: openingExplorer.entry.moves, - whiteWins: openingExplorer.entry.white, - draws: openingExplorer.entry.draws, - blackWins: openingExplorer.entry.black, - pgn: widget.pgn, - options: widget.options, - ), - if (topGames != null && topGames.isNotEmpty) ...[ - _OpeningExplorerHeader( - key: const Key('topGamesHeader'), - child: Text(context.l10n.topGames), - ), - ...List.generate( - topGames.length, - (int index) { - return OpeningExplorerGameTile( - key: Key('top-game-${topGames.get(index).id}'), - game: topGames.get(index), - color: index.isEven - ? Theme.of(context).colorScheme.surfaceContainerLow - : Theme.of(context).colorScheme.surfaceContainerHigh, - ply: ply, - ); - }, - growable: false, - ), - ], - if (recentGames != null && recentGames.isNotEmpty) ...[ - _OpeningExplorerHeader( - key: const Key('recentGamesHeader'), - child: Text(context.l10n.recentGames), - ), - ...List.generate( - recentGames.length, - (int index) { - return OpeningExplorerGameTile( - key: Key('recent-game-${recentGames.get(index).id}'), - game: recentGames.get(index), - color: index.isEven - ? Theme.of(context).colorScheme.surfaceContainerLow - : Theme.of(context).colorScheme.surfaceContainerHigh, - ply: ply, - ); - }, - growable: false, - ), - ], - ]; - - lastExplorerWidgets = explorerContent; - - return _OpeningExplorerView( - pgn: widget.pgn, - options: widget.options, - opening: opening, - openingExplorer: openingExplorer, - explorerContent: explorerContent, - ); - }, - loading: () => _OpeningExplorerView.loading( - pgn: widget.pgn, - options: widget.options, - opening: opening, - explorerContent: lastExplorerWidgets ?? - [ - Shimmer( - child: ShimmerLoading( - isLoading: true, - child: OpeningExplorerMoveTable.loading( - pgn: widget.pgn, - options: widget.options, - ), - ), - ), - ], - ), - error: (e, s) { - debugPrint( - 'SEVERE: [OpeningExplorerScreen] could not load opening explorer data; $e\n$s', - ); - return Center( - child: Text(e.toString()), - ); - }, - ); - } -} - -/// The opening header and the opening explorer move table. -class _OpeningExplorerView extends StatelessWidget { - const _OpeningExplorerView({ - required this.pgn, - required this.options, - required this.opening, - required this.openingExplorer, - required this.explorerContent, - }) : loading = false; - - const _OpeningExplorerView.loading({ - required this.pgn, - required this.options, - required this.opening, - required this.explorerContent, - }) : loading = true, - openingExplorer = null; - - final String pgn; - final AnalysisOptions options; - final Opening? opening; - final ({OpeningExplorerEntry entry, bool isIndexing})? openingExplorer; - final List explorerContent; - final bool loading; - - @override - Widget build(BuildContext context) { - final isLandscape = - MediaQuery.orientationOf(context) == Orientation.landscape; - - final loadingOverlayColor = Theme.of(context).brightness == Brightness.dark - ? Colors.black - : Colors.white; - - return Column( - children: [ - Container( - padding: _kTableRowPadding, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.secondaryContainer, - borderRadius: BorderRadius.only( - topLeft: Radius.circular(isLandscape ? 4.0 : 0), - topRight: Radius.circular(isLandscape ? 4.0 : 0), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - if (opening != null) - Expanded( - flex: 75, - child: _Opening( - opening: opening!, - ), - ), - if (openingExplorer?.isIndexing == true) - Expanded( - flex: 25, - child: _IndexingIndicator(), - ), - ], - ), - ), - Expanded( - child: Stack( - children: [ - ListView(padding: EdgeInsets.zero, children: explorerContent), - Positioned.fill( - child: IgnorePointer( - ignoring: !loading, - child: AnimatedOpacity( - duration: const Duration(milliseconds: 300), - curve: Curves.fastOutSlowIn, - opacity: loading ? 0.5 : 0.0, - child: ColoredBox(color: loadingOverlayColor), - ), - ), - ), - ], - ), - ), - ], - ); - } -} - -class _Opening extends ConsumerWidget { - const _Opening({ - required this.opening, - }); - - final Opening opening; - @override - Widget build(BuildContext context, WidgetRef ref) { - return GestureDetector( - onTap: opening.name == context.l10n.startPosition - ? null - : () => launchUrl( - Uri.parse('https://lichess.org/opening/${opening.name}'), - ), - child: Text( - '${opening.eco.isEmpty ? "" : "${opening.eco} "}${opening.name}', - style: TextStyle( - color: Theme.of(context).colorScheme.onSecondaryContainer, - fontWeight: FontWeight.bold, - ), - ), - ); - } -} - -class _IndexingIndicator extends StatefulWidget { - @override - State<_IndexingIndicator> createState() => _IndexingIndicatorState(); -} - -class _IndexingIndicatorState extends State<_IndexingIndicator> - with TickerProviderStateMixin { - late AnimationController controller; - - @override - void initState() { - controller = AnimationController( - vsync: this, - duration: const Duration(seconds: 3), - )..addListener(() { - setState(() {}); - }); - controller.repeat(); - super.initState(); - } - - @override - void dispose() { - controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Row( - children: [ - SizedBox( - width: 10, - height: 10, - child: CircularProgressIndicator.adaptive( - value: controller.value, - semanticsLabel: 'Indexing...', - ), - ), - const SizedBox(width: 10), - const Text('Indexing...'), - ], - ); - } -} - -/// Table of moves for the opening explorer. -class OpeningExplorerMoveTable extends ConsumerWidget { - const OpeningExplorerMoveTable({ - required this.moves, - required this.whiteWins, - required this.draws, - required this.blackWins, - required this.pgn, - required this.options, - super.key, - }) : _isLoading = false; - - const OpeningExplorerMoveTable.loading({ - required this.pgn, - required this.options, - super.key, - }) : _isLoading = true, - moves = const IListConst([]), - whiteWins = 0, - draws = 0, - blackWins = 0; - - final IList moves; - final int whiteWins; - final int draws; - final int blackWins; - final String pgn; - final AnalysisOptions options; - - final bool _isLoading; - - String formatNum(int num) => NumberFormat.decimalPatternDigits().format(num); - - static const columnWidths = { - 0: FractionColumnWidth(0.15), - 1: FractionColumnWidth(0.35), - 2: FractionColumnWidth(0.50), - }; - - @override - Widget build(BuildContext context, WidgetRef ref) { - if (_isLoading) { - return loadingTable; - } - - final games = whiteWins + draws + blackWins; - final ctrlProvider = analysisControllerProvider(pgn, options); - - const topPadding = EdgeInsets.only(top: _kTableRowVerticalPadding); - - return Table( - columnWidths: columnWidths, - children: [ - TableRow( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.secondaryContainer, - ), - children: [ - Padding( - padding: _kTableRowPadding.subtract(topPadding), - child: Text(context.l10n.move), - ), - Padding( - padding: _kTableRowPadding.subtract(topPadding), - child: Text(context.l10n.games), - ), - Padding( - padding: _kTableRowPadding.subtract(topPadding), - child: Text(context.l10n.whiteDrawBlack), - ), - ], - ), - ...List.generate( - moves.length, - (int index) { - final move = moves.get(index); - final percentGames = ((move.games / games) * 100).round(); - return TableRow( - decoration: BoxDecoration( - color: index.isEven - ? Theme.of(context).colorScheme.surfaceContainerLow - : Theme.of(context).colorScheme.surfaceContainerHigh, - ), - children: [ - TableRowInkWell( - onTap: () => ref - .read(ctrlProvider.notifier) - .onUserMove(NormalMove.fromUci(move.uci)), - child: Padding( - padding: _kTableRowPadding, - child: Text(move.san), - ), - ), - TableRowInkWell( - onTap: () => ref - .read(ctrlProvider.notifier) - .onUserMove(NormalMove.fromUci(move.uci)), - child: Padding( - padding: _kTableRowPadding, - child: Text('${formatNum(move.games)} ($percentGames%)'), - ), - ), - TableRowInkWell( - onTap: () => ref - .read(ctrlProvider.notifier) - .onUserMove(NormalMove.fromUci(move.uci)), - child: Padding( - padding: _kTableRowPadding, - child: _WinPercentageChart( - whiteWins: move.white, - draws: move.draws, - blackWins: move.black, - ), - ), - ), - ], - ); - }, - ), - TableRow( - decoration: BoxDecoration( - color: moves.length.isEven - ? Theme.of(context).colorScheme.surfaceContainerLow - : Theme.of(context).colorScheme.surfaceContainerHigh, - ), - children: [ - Container( - padding: _kTableRowPadding, - alignment: Alignment.centerLeft, - child: const Icon(Icons.functions), - ), - Padding( - padding: _kTableRowPadding, - child: Text('${formatNum(games)} (100%)'), - ), - Padding( - padding: _kTableRowPadding, - child: _WinPercentageChart( - whiteWins: whiteWins, - draws: draws, - blackWins: blackWins, - ), - ), - ], - ), - ], - ); - } - - static final loadingTable = Table( - columnWidths: columnWidths, - children: List.generate( - 10, - (int index) => TableRow( - children: [ - Padding( - padding: _kTableRowPadding, - child: Container( - height: 20, - width: double.infinity, - decoration: BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(5), - ), - ), - ), - Padding( - padding: _kTableRowPadding, - child: Container( - height: 20, - width: double.infinity, - decoration: BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(5), - ), - ), - ), - Padding( - padding: _kTableRowPadding, - child: Container( - height: 20, - width: double.infinity, - decoration: BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(5), - ), - ), - ), - ], - ), - ), - ); -} - -/// A game tile for the opening explorer. -class OpeningExplorerGameTile extends ConsumerStatefulWidget { - const OpeningExplorerGameTile({ - required this.game, - required this.color, - required this.ply, - super.key, - }); - - final OpeningExplorerGame game; - final Color color; - final int ply; - - @override - ConsumerState createState() => - _OpeningExplorerGameTileState(); -} - -class _OpeningExplorerGameTileState - extends ConsumerState { - @override - Widget build(BuildContext context) { - const widthResultBox = 50.0; - const paddingResultBox = EdgeInsets.all(5); - - return Container( - padding: _kTableRowPadding, - color: widget.color, - child: AdaptiveInkWell( - onTap: () { - pushPlatformRoute( - context, - builder: (_) => ArchivedGameScreen( - gameId: widget.game.id, - orientation: Side.white, - initialCursor: widget.ply, - ), - ); - }, - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(widget.game.white.rating.toString()), - Text(widget.game.black.rating.toString()), - ], - ), - const SizedBox(width: 10), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - widget.game.white.name, - overflow: TextOverflow.ellipsis, - ), - Text( - widget.game.black.name, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - Row( - children: [ - if (widget.game.winner == 'white') - Container( - width: widthResultBox, - padding: paddingResultBox, - decoration: BoxDecoration( - color: _whiteBoxColor(context), - borderRadius: BorderRadius.circular(5), - ), - child: const Text( - '1-0', - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.black, - ), - ), - ) - else if (widget.game.winner == 'black') - Container( - width: widthResultBox, - padding: paddingResultBox, - decoration: BoxDecoration( - color: _blackBoxColor(context), - borderRadius: BorderRadius.circular(5), - ), - child: const Text( - '0-1', - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white, - ), - ), - ) - else - Container( - width: widthResultBox, - padding: paddingResultBox, - decoration: BoxDecoration( - color: Colors.grey, - borderRadius: BorderRadius.circular(5), - ), - child: const Text( - '½-½', - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white, - ), - ), - ), - if (widget.game.month != null) ...[ - const SizedBox(width: 10.0), - Text( - widget.game.month!, - style: const TextStyle( - fontFeatures: [FontFeature.tabularFigures()], - ), - ), - ], - if (widget.game.speed != null) ...[ - const SizedBox(width: 10.0), - Icon(widget.game.speed!.icon, size: 20), - ], - ], - ), - ], - ), - ), - ); - } -} - -class _OpeningExplorerHeader extends StatelessWidget { - const _OpeningExplorerHeader({required this.child, super.key}); - - final Widget child; - - @override - Widget build(BuildContext context) { - return Container( - width: double.infinity, - padding: _kTableRowPadding, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.secondaryContainer, - ), - child: child, - ); - } -} - -class _WinPercentageChart extends StatelessWidget { - const _WinPercentageChart({ - required this.whiteWins, - required this.draws, - required this.blackWins, - }); - - final int whiteWins; - final int draws; - final int blackWins; - - int percentGames(int games) => - ((games / (whiteWins + draws + blackWins)) * 100).round(); - String label(int percent) => percent < 20 ? '' : '$percent%'; - - @override - Widget build(BuildContext context) { - final percentWhite = percentGames(whiteWins); - final percentDraws = percentGames(draws); - final percentBlack = percentGames(blackWins); - - return ClipRRect( - borderRadius: BorderRadius.circular(5), - child: Row( - children: [ - Expanded( - flex: percentWhite, - child: ColoredBox( - color: _whiteBoxColor(context), - child: Text( - label(percentWhite), - textAlign: TextAlign.center, - style: const TextStyle(color: Colors.black), - ), - ), - ), - Expanded( - flex: percentDraws, - child: ColoredBox( - color: Colors.grey, - child: Text( - label(percentDraws), - textAlign: TextAlign.center, - style: const TextStyle(color: Colors.white), - ), - ), - ), - Expanded( - flex: percentBlack, - child: ColoredBox( - color: _blackBoxColor(context), - child: Text( - label(percentBlack), - textAlign: TextAlign.center, - style: const TextStyle(color: Colors.white), - ), - ), - ), - ], - ), - ); - } -} - class _BottomBar extends ConsumerWidget { const _BottomBar({ required this.pgn, diff --git a/lib/src/view/opening_explorer/opening_explorer_widget.dart b/lib/src/view/opening_explorer/opening_explorer_widget.dart new file mode 100644 index 0000000000..0b310cd5f6 --- /dev/null +++ b/lib/src/view/opening_explorer/opening_explorer_widget.dart @@ -0,0 +1,807 @@ +import 'package:dartchess/dartchess.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:intl/intl.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; +import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; +import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_preferences.dart'; +import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_repository.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/view/game/archived_game_screen.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/shimmer.dart'; +import 'package:url_launcher/url_launcher.dart'; + +const _kTableRowVerticalPadding = 10.0; +const _kTableRowHorizontalPadding = 8.0; +const _kTableRowPadding = EdgeInsets.symmetric( + horizontal: _kTableRowHorizontalPadding, + vertical: _kTableRowVerticalPadding, +); + +Color _whiteBoxColor(BuildContext context) => + Theme.of(context).brightness == Brightness.dark + ? Colors.white.withOpacity(0.8) + : Colors.white; + +Color _blackBoxColor(BuildContext context) => + Theme.of(context).brightness == Brightness.light + ? Colors.black.withOpacity(0.7) + : Colors.black; + +class OpeningExplorerWidget extends ConsumerStatefulWidget { + const OpeningExplorerWidget({ + required this.pgn, + required this.options, + super.key, + }); + + final String pgn; + final AnalysisOptions options; + + @override + ConsumerState createState() => _OpeningExplorerState(); +} + +class _OpeningExplorerState extends ConsumerState { + final Map cache = {}; + + /// Last explorer content that was successfully loaded. This is used to + /// display a loading indicator while the new content is being fetched. + List? lastExplorerWidgets; + + @override + Widget build(BuildContext context) { + final analysisState = + ref.watch(analysisControllerProvider(widget.pgn, widget.options)); + + if (analysisState.position.ply >= 50) { + return Align( + alignment: Alignment.center, + child: Text(context.l10n.maxDepthReached), + ); + } + + final prefs = ref.watch(openingExplorerPreferencesProvider); + if (prefs.db == OpeningDatabase.player && prefs.playerDb.username == null) { + return const Align( + alignment: Alignment.center, + child: Text('Select a Lichess player in the settings'), + ); + } + + final opening = analysisState.currentNode.isRoot + ? LightOpening( + eco: '', + name: context.l10n.startPosition, + ) + : analysisState.currentNode.opening ?? + analysisState.currentBranchOpening ?? + analysisState.contextOpening; + + final cacheKey = OpeningExplorerCacheKey( + fen: analysisState.position.fen, + prefs: prefs, + ); + final cacheOpeningExplorer = cache[cacheKey]; + final openingExplorerAsync = cacheOpeningExplorer != null + ? AsyncValue.data( + (entry: cacheOpeningExplorer, isIndexing: false), + ) + : ref.watch(openingExplorerProvider(fen: analysisState.position.fen)); + + if (cacheOpeningExplorer == null) { + ref.listen(openingExplorerProvider(fen: analysisState.position.fen), + (_, curAsync) { + curAsync.whenData((cur) { + if (cur != null && !cur.isIndexing) { + cache[cacheKey] = cur.entry; + } + }); + }); + } + + return openingExplorerAsync.when( + data: (openingExplorer) { + if (openingExplorer == null) { + return _OpeningExplorerView.loading( + pgn: widget.pgn, + options: widget.options, + opening: opening, + explorerContent: lastExplorerWidgets ?? + [ + Shimmer( + child: ShimmerLoading( + isLoading: true, + child: _OpeningExplorerMoveTable.loading( + pgn: widget.pgn, + options: widget.options, + ), + ), + ), + ], + ); + } + if (openingExplorer.entry.moves.isEmpty) { + lastExplorerWidgets = null; + return _OpeningExplorerView( + pgn: widget.pgn, + options: widget.options, + opening: opening, + openingExplorer: openingExplorer, + explorerContent: [ + Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Text(context.l10n.noGameFound), + ), + ), + ], + ); + } + + final topGames = openingExplorer.entry.topGames; + final recentGames = openingExplorer.entry.recentGames; + + final ply = analysisState.position.ply; + + final explorerContent = [ + _OpeningExplorerMoveTable( + moves: openingExplorer.entry.moves, + whiteWins: openingExplorer.entry.white, + draws: openingExplorer.entry.draws, + blackWins: openingExplorer.entry.black, + pgn: widget.pgn, + options: widget.options, + ), + if (topGames != null && topGames.isNotEmpty) ...[ + _OpeningExplorerHeader( + key: const Key('topGamesHeader'), + child: Text(context.l10n.topGames), + ), + ...List.generate( + topGames.length, + (int index) { + return OpeningExplorerGameTile( + key: Key('top-game-${topGames.get(index).id}'), + game: topGames.get(index), + color: index.isEven + ? Theme.of(context).colorScheme.surfaceContainerLow + : Theme.of(context).colorScheme.surfaceContainerHigh, + ply: ply, + ); + }, + growable: false, + ), + ], + if (recentGames != null && recentGames.isNotEmpty) ...[ + _OpeningExplorerHeader( + key: const Key('recentGamesHeader'), + child: Text(context.l10n.recentGames), + ), + ...List.generate( + recentGames.length, + (int index) { + return OpeningExplorerGameTile( + key: Key('recent-game-${recentGames.get(index).id}'), + game: recentGames.get(index), + color: index.isEven + ? Theme.of(context).colorScheme.surfaceContainerLow + : Theme.of(context).colorScheme.surfaceContainerHigh, + ply: ply, + ); + }, + growable: false, + ), + ], + ]; + + lastExplorerWidgets = explorerContent; + + return _OpeningExplorerView( + pgn: widget.pgn, + options: widget.options, + opening: opening, + openingExplorer: openingExplorer, + explorerContent: explorerContent, + ); + }, + loading: () => _OpeningExplorerView.loading( + pgn: widget.pgn, + options: widget.options, + opening: opening, + explorerContent: lastExplorerWidgets ?? + [ + Shimmer( + child: ShimmerLoading( + isLoading: true, + child: _OpeningExplorerMoveTable.loading( + pgn: widget.pgn, + options: widget.options, + ), + ), + ), + ], + ), + error: (e, s) { + debugPrint( + 'SEVERE: [OpeningExplorerScreen] could not load opening explorer data; $e\n$s', + ); + return Center( + child: Text(e.toString()), + ); + }, + ); + } +} + +/// The opening header and the opening explorer move table. +class _OpeningExplorerView extends StatelessWidget { + const _OpeningExplorerView({ + required this.pgn, + required this.options, + required this.opening, + required this.openingExplorer, + required this.explorerContent, + }) : loading = false; + + const _OpeningExplorerView.loading({ + required this.pgn, + required this.options, + required this.opening, + required this.explorerContent, + }) : loading = true, + openingExplorer = null; + + final String pgn; + final AnalysisOptions options; + final Opening? opening; + final ({OpeningExplorerEntry entry, bool isIndexing})? openingExplorer; + final List explorerContent; + final bool loading; + + @override + Widget build(BuildContext context) { + final isLandscape = + MediaQuery.orientationOf(context) == Orientation.landscape; + + final loadingOverlayColor = Theme.of(context).brightness == Brightness.dark + ? Colors.black + : Colors.white; + + return Column( + children: [ + Container( + padding: _kTableRowPadding, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.secondaryContainer, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(isLandscape ? 4.0 : 0), + topRight: Radius.circular(isLandscape ? 4.0 : 0), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (opening != null) + Expanded( + flex: 75, + child: _Opening( + opening: opening!, + ), + ), + if (openingExplorer?.isIndexing == true) + Expanded( + flex: 25, + child: _IndexingIndicator(), + ), + ], + ), + ), + Expanded( + child: Stack( + children: [ + ListView(padding: EdgeInsets.zero, children: explorerContent), + Positioned.fill( + child: IgnorePointer( + ignoring: !loading, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 300), + curve: Curves.fastOutSlowIn, + opacity: loading ? 0.5 : 0.0, + child: ColoredBox(color: loadingOverlayColor), + ), + ), + ), + ], + ), + ), + ], + ); + } +} + +class _Opening extends ConsumerWidget { + const _Opening({ + required this.opening, + }); + + final Opening opening; + @override + Widget build(BuildContext context, WidgetRef ref) { + return GestureDetector( + onTap: opening.name == context.l10n.startPosition + ? null + : () => launchUrl( + Uri.parse('https://lichess.org/opening/${opening.name}'), + ), + child: Text( + '${opening.eco.isEmpty ? "" : "${opening.eco} "}${opening.name}', + style: TextStyle( + color: Theme.of(context).colorScheme.onSecondaryContainer, + fontWeight: FontWeight.bold, + ), + ), + ); + } +} + +class _IndexingIndicator extends StatefulWidget { + @override + State<_IndexingIndicator> createState() => _IndexingIndicatorState(); +} + +class _IndexingIndicatorState extends State<_IndexingIndicator> + with TickerProviderStateMixin { + late AnimationController controller; + + @override + void initState() { + controller = AnimationController( + vsync: this, + duration: const Duration(seconds: 3), + )..addListener(() { + setState(() {}); + }); + controller.repeat(); + super.initState(); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Row( + children: [ + SizedBox( + width: 10, + height: 10, + child: CircularProgressIndicator.adaptive( + value: controller.value, + semanticsLabel: 'Indexing...', + ), + ), + const SizedBox(width: 10), + const Text('Indexing...'), + ], + ); + } +} + +/// Table of moves for the opening explorer. +class _OpeningExplorerMoveTable extends ConsumerWidget { + const _OpeningExplorerMoveTable({ + required this.moves, + required this.whiteWins, + required this.draws, + required this.blackWins, + required this.pgn, + required this.options, + }) : _isLoading = false; + + const _OpeningExplorerMoveTable.loading({ + required this.pgn, + required this.options, + }) : _isLoading = true, + moves = const IListConst([]), + whiteWins = 0, + draws = 0, + blackWins = 0; + + final IList moves; + final int whiteWins; + final int draws; + final int blackWins; + final String pgn; + final AnalysisOptions options; + + final bool _isLoading; + + String formatNum(int num) => NumberFormat.decimalPatternDigits().format(num); + + static const columnWidths = { + 0: FractionColumnWidth(0.15), + 1: FractionColumnWidth(0.35), + 2: FractionColumnWidth(0.50), + }; + + @override + Widget build(BuildContext context, WidgetRef ref) { + if (_isLoading) { + return loadingTable; + } + + final games = whiteWins + draws + blackWins; + final ctrlProvider = analysisControllerProvider(pgn, options); + + const topPadding = EdgeInsets.only(top: _kTableRowVerticalPadding); + + return Table( + columnWidths: columnWidths, + children: [ + TableRow( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.secondaryContainer, + ), + children: [ + Padding( + padding: _kTableRowPadding.subtract(topPadding), + child: Text(context.l10n.move), + ), + Padding( + padding: _kTableRowPadding.subtract(topPadding), + child: Text(context.l10n.games), + ), + Padding( + padding: _kTableRowPadding.subtract(topPadding), + child: Text(context.l10n.whiteDrawBlack), + ), + ], + ), + ...List.generate( + moves.length, + (int index) { + final move = moves.get(index); + final percentGames = ((move.games / games) * 100).round(); + return TableRow( + decoration: BoxDecoration( + color: index.isEven + ? Theme.of(context).colorScheme.surfaceContainerLow + : Theme.of(context).colorScheme.surfaceContainerHigh, + ), + children: [ + TableRowInkWell( + onTap: () => ref + .read(ctrlProvider.notifier) + .onUserMove(NormalMove.fromUci(move.uci)), + child: Padding( + padding: _kTableRowPadding, + child: Text(move.san), + ), + ), + TableRowInkWell( + onTap: () => ref + .read(ctrlProvider.notifier) + .onUserMove(NormalMove.fromUci(move.uci)), + child: Padding( + padding: _kTableRowPadding, + child: Text('${formatNum(move.games)} ($percentGames%)'), + ), + ), + TableRowInkWell( + onTap: () => ref + .read(ctrlProvider.notifier) + .onUserMove(NormalMove.fromUci(move.uci)), + child: Padding( + padding: _kTableRowPadding, + child: _WinPercentageChart( + whiteWins: move.white, + draws: move.draws, + blackWins: move.black, + ), + ), + ), + ], + ); + }, + ), + TableRow( + decoration: BoxDecoration( + color: moves.length.isEven + ? Theme.of(context).colorScheme.surfaceContainerLow + : Theme.of(context).colorScheme.surfaceContainerHigh, + ), + children: [ + Container( + padding: _kTableRowPadding, + alignment: Alignment.centerLeft, + child: const Icon(Icons.functions), + ), + Padding( + padding: _kTableRowPadding, + child: Text('${formatNum(games)} (100%)'), + ), + Padding( + padding: _kTableRowPadding, + child: _WinPercentageChart( + whiteWins: whiteWins, + draws: draws, + blackWins: blackWins, + ), + ), + ], + ), + ], + ); + } + + static final loadingTable = Table( + columnWidths: columnWidths, + children: List.generate( + 10, + (int index) => TableRow( + children: [ + Padding( + padding: _kTableRowPadding, + child: Container( + height: 20, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(5), + ), + ), + ), + Padding( + padding: _kTableRowPadding, + child: Container( + height: 20, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(5), + ), + ), + ), + Padding( + padding: _kTableRowPadding, + child: Container( + height: 20, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(5), + ), + ), + ), + ], + ), + ), + ); +} + +/// A game tile for the opening explorer. +class OpeningExplorerGameTile extends ConsumerStatefulWidget { + const OpeningExplorerGameTile({ + required this.game, + required this.color, + required this.ply, + super.key, + }); + + final OpeningExplorerGame game; + final Color color; + final int ply; + + @override + ConsumerState createState() => + _OpeningExplorerGameTileState(); +} + +class _OpeningExplorerGameTileState + extends ConsumerState { + @override + Widget build(BuildContext context) { + const widthResultBox = 50.0; + const paddingResultBox = EdgeInsets.all(5); + + return Container( + padding: _kTableRowPadding, + color: widget.color, + child: AdaptiveInkWell( + onTap: () { + pushPlatformRoute( + context, + builder: (_) => ArchivedGameScreen( + gameId: widget.game.id, + orientation: Side.white, + initialCursor: widget.ply, + ), + ); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(widget.game.white.rating.toString()), + Text(widget.game.black.rating.toString()), + ], + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.game.white.name, + overflow: TextOverflow.ellipsis, + ), + Text( + widget.game.black.name, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + Row( + children: [ + if (widget.game.winner == 'white') + Container( + width: widthResultBox, + padding: paddingResultBox, + decoration: BoxDecoration( + color: _whiteBoxColor(context), + borderRadius: BorderRadius.circular(5), + ), + child: const Text( + '1-0', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.black, + ), + ), + ) + else if (widget.game.winner == 'black') + Container( + width: widthResultBox, + padding: paddingResultBox, + decoration: BoxDecoration( + color: _blackBoxColor(context), + borderRadius: BorderRadius.circular(5), + ), + child: const Text( + '0-1', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + ), + ), + ) + else + Container( + width: widthResultBox, + padding: paddingResultBox, + decoration: BoxDecoration( + color: Colors.grey, + borderRadius: BorderRadius.circular(5), + ), + child: const Text( + '½-½', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + ), + ), + ), + if (widget.game.month != null) ...[ + const SizedBox(width: 10.0), + Text( + widget.game.month!, + style: const TextStyle( + fontFeatures: [FontFeature.tabularFigures()], + ), + ), + ], + if (widget.game.speed != null) ...[ + const SizedBox(width: 10.0), + Icon(widget.game.speed!.icon, size: 20), + ], + ], + ), + ], + ), + ), + ); + } +} + +class _OpeningExplorerHeader extends StatelessWidget { + const _OpeningExplorerHeader({required this.child, super.key}); + + final Widget child; + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + padding: _kTableRowPadding, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.secondaryContainer, + ), + child: child, + ); + } +} + +class _WinPercentageChart extends StatelessWidget { + const _WinPercentageChart({ + required this.whiteWins, + required this.draws, + required this.blackWins, + }); + + final int whiteWins; + final int draws; + final int blackWins; + + int percentGames(int games) => + ((games / (whiteWins + draws + blackWins)) * 100).round(); + String label(int percent) => percent < 20 ? '' : '$percent%'; + + @override + Widget build(BuildContext context) { + final percentWhite = percentGames(whiteWins); + final percentDraws = percentGames(draws); + final percentBlack = percentGames(blackWins); + + return ClipRRect( + borderRadius: BorderRadius.circular(5), + child: Row( + children: [ + Expanded( + flex: percentWhite, + child: ColoredBox( + color: _whiteBoxColor(context), + child: Text( + label(percentWhite), + textAlign: TextAlign.center, + style: const TextStyle(color: Colors.black), + ), + ), + ), + Expanded( + flex: percentDraws, + child: ColoredBox( + color: Colors.grey, + child: Text( + label(percentDraws), + textAlign: TextAlign.center, + style: const TextStyle(color: Colors.white), + ), + ), + ), + Expanded( + flex: percentBlack, + child: ColoredBox( + color: _blackBoxColor(context), + child: Text( + label(percentBlack), + textAlign: TextAlign.center, + style: const TextStyle(color: Colors.white), + ), + ), + ), + ], + ), + ); + } +} diff --git a/test/view/opening_explorer/opening_explorer_screen_test.dart b/test/view/opening_explorer/opening_explorer_screen_test.dart index a18e72aef7..1d34e378e6 100644 --- a/test/view/opening_explorer/opening_explorer_screen_test.dart +++ b/test/view/opening_explorer/opening_explorer_screen_test.dart @@ -11,6 +11,7 @@ import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_preferences.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_screen.dart'; +import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_widget.dart'; import '../../test_app.dart'; import '../../test_utils.dart'; @@ -74,7 +75,7 @@ void main() { 'e4', 'd4', ]; - expect(find.byType(OpeningExplorerMoveTable), findsOneWidget); + expect(find.byType(Table), findsOneWidget); for (final move in moves) { expect(find.widgetWithText(TableRowInkWell, move), findsOneWidget); } @@ -125,7 +126,7 @@ void main() { final moves = [ 'd4', ]; - expect(find.byType(OpeningExplorerMoveTable), findsOneWidget); + expect(find.byType(Table), findsOneWidget); for (final move in moves) { expect(find.widgetWithText(TableRowInkWell, move), findsOneWidget); } @@ -175,7 +176,7 @@ void main() { final moves = [ 'c4', ]; - expect(find.byType(OpeningExplorerMoveTable), findsOneWidget); + expect(find.byType(Table), findsOneWidget); for (final move in moves) { expect(find.widgetWithText(TableRowInkWell, move), findsOneWidget); } From cb17e93430c9c44e48e8dcf800f8527c141a8a4e Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 10 Sep 2024 11:12:16 +0200 Subject: [PATCH 306/979] Integrate explorer in analysis screen --- lib/src/app.dart | 10 +- .../model/analysis/analysis_controller.dart | 16 +- .../opening_explorer_preferences.dart | 36 +-- .../opening_explorer_repository.dart | 6 +- lib/src/view/analysis/analysis_screen.dart | 228 ++++++++++-------- .../opening_explorer_settings.dart | 28 ++- .../opening_explorer_widget.dart | 8 +- 7 files changed, 195 insertions(+), 137 deletions(-) diff --git a/lib/src/app.dart b/lib/src/app.dart index c5be0bbcde..ea965d9f6e 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -222,7 +222,15 @@ class _AppState extends ConsumerState { ? (context, child) { return CupertinoTheme( data: cupertinoThemeData, - child: Material(child: child), + child: IconTheme.merge( + data: IconThemeData( + color: CupertinoTheme.of(context) + .textTheme + .textStyle + .color, + ), + child: Material(child: child), + ), ); } : null, diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index c42e9d6009..c22d2ad0d6 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -160,7 +160,8 @@ class AnalysisController extends _$AnalysisController { isLocalEvaluationEnabled: prefs.enableLocalEvaluation, displayMode: DisplayMode.moves, playersAnalysis: options.serverAnalysis, - acplChartData: _makeAcplChartData(), + acplChartData: + options.serverAnalysis != null ? _makeAcplChartData() : null, ); if (analysisState.isEngineAvailable) { @@ -357,12 +358,8 @@ class AnalysisController extends _$AnalysisController { state = state.copyWith(pgnHeaders: headers); } - void toggleDisplayMode() { - state = state.copyWith( - displayMode: state.displayMode == DisplayMode.moves - ? DisplayMode.summary - : DisplayMode.moves, - ); + void setDisplayMode(DisplayMode mode) { + state = state.copyWith(displayMode: mode); } Future requestServerAnalysis() { @@ -632,6 +629,7 @@ class AnalysisController extends _$AnalysisController { enum DisplayMode { moves, summary, + openingExplorer, } @freezed @@ -670,7 +668,9 @@ class AnalysisState with _$AnalysisState { /// Whether the user has enabled local evaluation. required bool isLocalEvaluationEnabled, - /// Whether to show the ACPL chart instead of tree view. + /// The display mode of the analysis. + /// + /// It can be either moves, summary or opening explorer. required DisplayMode displayMode, /// The last move played. diff --git a/lib/src/model/opening_explorer/opening_explorer_preferences.dart b/lib/src/model/opening_explorer/opening_explorer_preferences.dart index c5c1ae7e6b..7a1e221301 100644 --- a/lib/src/model/opening_explorer/opening_explorer_preferences.dart +++ b/lib/src/model/opening_explorer/opening_explorer_preferences.dart @@ -5,7 +5,7 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/db/shared_preferences.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; -import 'package:lichess_mobile/src/model/common/perf.dart'; +import 'package:lichess_mobile/src/model/common/speed.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -38,7 +38,7 @@ class OpeningExplorerPreferences extends _$OpeningExplorerPreferences { Future setMasterDbSince(int year) => _save(state.copyWith(masterDb: state.masterDb.copyWith(sinceYear: year))); - Future toggleLichessDbSpeed(Perf speed) => _save( + Future toggleLichessDbSpeed(Speed speed) => _save( state.copyWith( lichessDb: state.lichessDb.copyWith( speeds: state.lichessDb.speeds.contains(speed) @@ -74,7 +74,7 @@ class OpeningExplorerPreferences extends _$OpeningExplorerPreferences { state.copyWith(playerDb: state.playerDb.copyWith(side: side)), ); - Future togglePlayerDbSpeed(Perf speed) => _save( + Future togglePlayerDbSpeed(Speed speed) => _save( state.copyWith( playerDb: state.playerDb.copyWith( speeds: state.playerDb.speeds.contains(speed) @@ -177,18 +177,18 @@ class LichessDbPrefState with _$LichessDbPrefState { const LichessDbPrefState._(); const factory LichessDbPrefState({ - required ISet speeds, + required ISet speeds, required ISet ratings, required DateTime since, }) = _LichessDbPrefState; static const kAvailableSpeeds = ISetConst({ - Perf.ultraBullet, - Perf.bullet, - Perf.blitz, - Perf.rapid, - Perf.classical, - Perf.correspondence, + Speed.ultraBullet, + Speed.bullet, + Speed.blitz, + Speed.rapid, + Speed.classical, + Speed.correspondence, }); static const kAvailableRatings = ISetConst({ 400, @@ -210,7 +210,7 @@ class LichessDbPrefState with _$LichessDbPrefState { 'All time': earliestDate, }; static final defaults = LichessDbPrefState( - speeds: kAvailableSpeeds.remove(Perf.ultraBullet), + speeds: kAvailableSpeeds.remove(Speed.ultraBullet), ratings: kAvailableRatings.remove(400), since: earliestDate, ); @@ -231,18 +231,18 @@ class PlayerDbPrefState with _$PlayerDbPrefState { const factory PlayerDbPrefState({ String? username, required Side side, - required ISet speeds, + required ISet speeds, required ISet gameModes, required DateTime since, }) = _PlayerDbPrefState; static const kAvailableSpeeds = ISetConst({ - Perf.ultraBullet, - Perf.bullet, - Perf.blitz, - Perf.rapid, - Perf.classical, - Perf.correspondence, + Speed.ultraBullet, + Speed.bullet, + Speed.blitz, + Speed.rapid, + Speed.classical, + Speed.correspondence, }); static final earliestDate = DateTime.utc(2012, 12); static final now = DateTime.now(); diff --git a/lib/src/model/opening_explorer/opening_explorer_repository.dart b/lib/src/model/opening_explorer/opening_explorer_repository.dart index 2a3cee4ab9..c4b7938c33 100644 --- a/lib/src/model/opening_explorer/opening_explorer_repository.dart +++ b/lib/src/model/opening_explorer/opening_explorer_repository.dart @@ -5,7 +5,7 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:http/http.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; -import 'package:lichess_mobile/src/model/common/perf.dart'; +import 'package:lichess_mobile/src/model/common/speed.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_preferences.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -96,7 +96,7 @@ class OpeningExplorerRepository { Future getLichessDatabase( String fen, { - required ISet speeds, + required ISet speeds, required ISet ratings, DateTime? since, }) { @@ -120,7 +120,7 @@ class OpeningExplorerRepository { String fen, { required String usernameOrId, required Side color, - required ISet speeds, + required ISet speeds, required ISet gameModes, DateTime? since, }) { diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index 55f513bcc0..9d52ad8671 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -21,6 +21,7 @@ import 'package:lichess_mobile/src/model/engine/engine.dart'; import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; import 'package:lichess_mobile/src/model/game/game_repository_providers.dart'; import 'package:lichess_mobile/src/model/game/game_share_service.dart'; +import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_preferences.dart'; import 'package:lichess_mobile/src/model/settings/brightness.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; @@ -31,7 +32,8 @@ import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/utils/string.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_share_screen.dart'; import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; -import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_screen.dart'; +import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_settings.dart'; +import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_widget.dart'; import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; @@ -223,9 +225,8 @@ class _Body extends ConsumerWidget { final hasEval = ref.watch(ctrlProvider.select((value) => value.hasAvailableEval)); - final showAnalysisSummary = ref.watch( - ctrlProvider.select((value) => value.displayMode == DisplayMode.summary), - ); + final displayMode = + ref.watch(ctrlProvider.select((value) => value.displayMode)); return Column( children: [ @@ -245,98 +246,103 @@ class _Body extends ConsumerWidget { ? defaultBoardSize - kTabletBoardTableSidePadding * 2 : defaultBoardSize; - return aspectRatio > 1 - ? Row( - mainAxisSize: MainAxisSize.max, - children: [ - Padding( - padding: const EdgeInsets.only( - left: kTabletBoardTableSidePadding, - top: kTabletBoardTableSidePadding, - bottom: kTabletBoardTableSidePadding, - ), - child: Row( - children: [ - AnalysisBoard( - pgn, - options, - boardSize, - isTablet: isTablet, - ), - if (hasEval && showEvaluationGauge) ...[ - const SizedBox(width: 4.0), - _EngineGaugeVertical(ctrlProvider), - ], - ], - ), - ), - Flexible( - fit: FlexFit.loose, - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - if (isEngineAvailable) - _EngineLines( - ctrlProvider, - isLandscape: true, - ), - Expanded( - child: PlatformCard( - margin: const EdgeInsets.all( - kTabletBoardTableSidePadding, - ), - semanticContainer: false, - child: showAnalysisSummary - ? ServerAnalysisSummary(pgn, options) - : AnalysisTreeView( - pgn, - options, - Orientation.landscape, - ), - ), - ), - ], - ), - ), - ], - ) - : Column( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - _ColumnTopTable(ctrlProvider), - if (isTablet) - Padding( - padding: const EdgeInsets.all( - kTabletBoardTableSidePadding, - ), - child: AnalysisBoard( - pgn, - options, - boardSize, - isTablet: isTablet, - ), - ) - else + final display = switch (displayMode) { + DisplayMode.openingExplorer => OpeningExplorerWidget( + pgn: pgn, + options: options, + ), + DisplayMode.summary => ServerAnalysisSummary(pgn, options), + DisplayMode.moves => AnalysisTreeView( + pgn, + options, + aspectRatio > 1 + ? Orientation.landscape + : Orientation.portrait, + ), + }; + + // If the aspect ratio is greater than 1, we are in landscape mode. + if (aspectRatio > 1) { + return Row( + mainAxisSize: MainAxisSize.max, + children: [ + Padding( + padding: const EdgeInsets.only( + left: kTabletBoardTableSidePadding, + top: kTabletBoardTableSidePadding, + bottom: kTabletBoardTableSidePadding, + ), + child: Row( + children: [ AnalysisBoard( pgn, options, boardSize, isTablet: isTablet, ), - if (showAnalysisSummary) - Expanded(child: ServerAnalysisSummary(pgn, options)) - else + if (hasEval && showEvaluationGauge) ...[ + const SizedBox(width: 4.0), + _EngineGaugeVertical(ctrlProvider), + ], + ], + ), + ), + Flexible( + fit: FlexFit.loose, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + if (isEngineAvailable) + _EngineLines( + ctrlProvider, + isLandscape: true, + ), Expanded( - child: AnalysisTreeView( - pgn, - options, - Orientation.portrait, + child: PlatformCard( + margin: const EdgeInsets.all( + kTabletBoardTableSidePadding, + ), + semanticContainer: false, + child: display, ), ), - ], - ); + ], + ), + ), + ], + ); + } + // If the aspect ratio is less than 1, we are in portrait mode. + else { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + _ColumnTopTable(ctrlProvider), + if (isTablet) + Padding( + padding: const EdgeInsets.all( + kTabletBoardTableSidePadding, + ), + child: AnalysisBoard( + pgn, + options, + boardSize, + isTablet: isTablet, + ), + ) + else + AnalysisBoard( + pgn, + options, + boardSize, + isTablet: isTablet, + ), + Expanded(child: display), + ], + ); + } }, ), ), @@ -381,6 +387,9 @@ class _ColumnTopTable extends ConsumerWidget { analysisPreferencesProvider.select((p) => p.showEvaluationGauge), ); + final displayMode = + ref.watch(ctrlProvider.select((value) => value.displayMode)); + return analysisState.hasAvailableEval ? Column( mainAxisSize: MainAxisSize.min, @@ -391,7 +400,8 @@ class _ColumnTopTable extends ConsumerWidget { displayMode: EngineGaugeDisplayMode.horizontal, params: analysisState.engineGaugeParams, ), - if (analysisState.isEngineAvailable) + if (displayMode != DisplayMode.openingExplorer && + analysisState.isEngineAvailable) _EngineLines(ctrlProvider, isLandscape: false), ], ) @@ -587,30 +597,44 @@ class _BottomBar extends ConsumerWidget { }, icon: Icons.menu, ), - if (canShowGameSummary) + if (displayMode != DisplayMode.openingExplorer && canShowGameSummary) BottomBarButton( + // TODO: l10n label: displayMode == DisplayMode.summary ? 'Moves' : 'Summary', - onTap: () { - ref.read(ctrlProvider.notifier).toggleDisplayMode(); - }, + onTap: displayMode != DisplayMode.openingExplorer + ? () { + final newMode = displayMode == DisplayMode.summary + ? DisplayMode.moves + : DisplayMode.summary; + ref.read(ctrlProvider.notifier).setDisplayMode(newMode); + } + : null, icon: displayMode == DisplayMode.summary ? LichessIcons.flow_cascade : Icons.area_chart, ), + if (displayMode == DisplayMode.openingExplorer) + BottomBarButton( + label: 'Opening Explorer Settings', + onTap: () => showAdaptiveBottomSheet( + context: context, + isScrollControlled: true, + showDragHandle: true, + isDismissible: true, + builder: (_) => OpeningExplorerSettings(pgn, options), + ), + icon: Icons.tune, + ), BottomBarButton( label: context.l10n.openingExplorer, + highlighted: displayMode == DisplayMode.openingExplorer, onTap: isOnline ? () { - pushPlatformRoute( - context, - builder: (_) => OpeningExplorerScreen( - pgn: ref.read(ctrlProvider.notifier).makeInternalPgn(), - options: options.copyWith( - isLocalEvaluationAllowed: false, - id: standaloneOpeningExplorerId, - ), - ), - ); + ref.read(ctrlProvider.notifier).setDisplayMode( + displayMode == DisplayMode.openingExplorer + ? DisplayMode.moves + : DisplayMode.openingExplorer, + ); } : null, icon: Icons.explore, diff --git a/lib/src/view/opening_explorer/opening_explorer_settings.dart b/lib/src/view/opening_explorer/opening_explorer_settings.dart index 1ace2c878f..d18d0c2bb7 100644 --- a/lib/src/view/opening_explorer/opening_explorer_settings.dart +++ b/lib/src/view/opening_explorer/opening_explorer_settings.dart @@ -3,6 +3,8 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; +import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/common/perf.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_preferences.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; @@ -49,8 +51,17 @@ class OpeningExplorerSettings extends ConsumerWidget { children: LichessDbPrefState.kAvailableSpeeds .map( (speed) => FilterChip( - label: Icon(speed.icon), - tooltip: speed.title, + label: Text( + String.fromCharCode(speed.icon.codePoint), + style: TextStyle( + fontFamily: speed.icon.fontFamily, + fontSize: 18.0, + ), + ), + tooltip: Perf.fromVariantAndSpeed( + Variant.standard, + speed, + ).title, selected: prefs.lichessDb.speeds.contains(speed), onSelected: (_) => ref .read(openingExplorerPreferencesProvider.notifier) @@ -164,8 +175,17 @@ class OpeningExplorerSettings extends ConsumerWidget { children: PlayerDbPrefState.kAvailableSpeeds .map( (speed) => FilterChip( - label: Icon(speed.icon), - tooltip: speed.title, + label: Text( + String.fromCharCode(speed.icon.codePoint), + style: TextStyle( + fontFamily: speed.icon.fontFamily, + fontSize: 18.0, + ), + ), + tooltip: Perf.fromVariantAndSpeed( + Variant.standard, + speed, + ).title, selected: prefs.playerDb.speeds.contains(speed), onSelected: (_) => ref .read(openingExplorerPreferencesProvider.notifier) diff --git a/lib/src/view/opening_explorer/opening_explorer_widget.dart b/lib/src/view/opening_explorer/opening_explorer_widget.dart index 0b310cd5f6..50a0fa73f0 100644 --- a/lib/src/view/opening_explorer/opening_explorer_widget.dart +++ b/lib/src/view/opening_explorer/opening_explorer_widget.dart @@ -304,7 +304,13 @@ class _OpeningExplorerView extends StatelessWidget { Expanded( child: Stack( children: [ - ListView(padding: EdgeInsets.zero, children: explorerContent), + Center( + child: ListView( + shrinkWrap: true, + padding: EdgeInsets.zero, + children: explorerContent, + ), + ), Positioned.fill( child: IgnorePointer( ignoring: !loading, From dd892ca686aa4087c34cab52f225dda855876987 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 10 Sep 2024 12:31:51 +0200 Subject: [PATCH 307/979] Add move list to opening explorer --- .../model/analysis/analysis_controller.dart | 4 +- lib/src/model/common/node.dart | 30 +- lib/src/view/analysis/analysis_screen.dart | 1 - .../opening_explorer_screen.dart | 174 ++++++----- lib/src/widgets/board_table.dart | 278 +---------------- lib/src/widgets/move_list.dart | 281 ++++++++++++++++++ test/model/common/node_test.dart | 13 + .../opening_explorer_repository_test.dart | 6 +- test/view/game/archived_game_screen_test.dart | 2 +- .../opening_explorer_screen_test.dart | 8 + 10 files changed, 444 insertions(+), 353 deletions(-) create mode 100644 lib/src/widgets/move_list.dart diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index c22d2ad0d6..db3f231616 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -197,10 +197,10 @@ class AnalysisController extends _$AnalysisController { } // For the opening explorer, last played move should always be the mainline - final shouldPrepend = options.id == standaloneOpeningExplorerId; + final shouldReplace = options.id == standaloneOpeningExplorerId; final (newPath, isNewNode) = - _root.addMoveAt(state.currentPath, move, prepend: shouldPrepend); + _root.addMoveAt(state.currentPath, move, replace: shouldReplace); if (newPath != null) { _setPath( newPath, diff --git a/lib/src/model/common/node.dart b/lib/src/model/common/node.dart index e654fc7d0f..a8b47c5dae 100644 --- a/lib/src/model/common/node.dart +++ b/lib/src/model/common/node.dart @@ -144,16 +144,23 @@ abstract class Node { /// Returns null if the node at path does not exist. /// /// If the node already exists, it is not added again. + /// + /// If [prepend] is true, the new node is added at the beginning of the children. + /// If [replace] is true, the children of the existing node are replaced. (UciPath?, bool) addNodeAt( UciPath path, Branch newNode, { bool prepend = false, + bool replace = false, }) { final newPath = path + newNode.id; final node = nodeAtOrNull(path); if (node != null) { final existing = nodeAtOrNull(newPath) != null; if (!existing) { + if (replace) { + node.children.clear(); + } if (prepend) { node.prependChild(newNode); } else { @@ -186,10 +193,14 @@ abstract class Node { /// Returns null if the node at path does not exist. /// /// If the node already exists, it is not added again. + /// + /// If [prepend] is true, the new node is added at the beginning of the children. + /// If [replace] is true, the children of the existing node are replaced. (UciPath?, bool) addMoveAt( UciPath path, Move move, { bool prepend = false, + bool replace = false, }) { final pos = nodeAt(path).position; @@ -205,7 +216,7 @@ abstract class Node { sanMove: SanMove(newSan, convertedMove), position: newPos, ); - return addNodeAt(path, newNode, prepend: prepend); + return addNodeAt(path, newNode, prepend: prepend, replace: replace); } /// The function `convertAltCastlingMove` checks if a move is an alternative @@ -527,6 +538,7 @@ abstract class ViewNode { IList? get comments; IList? get lichessAnalysisComments; IList? get nags; + Iterable get mainline; } /// An immutable view of a [Root] node. @@ -559,6 +571,14 @@ class ViewRoot with _$ViewRoot implements ViewNode { @override IList? get nags => null; + + @override + Iterable get mainline sync* { + for (final child in children) { + yield child; + yield* child.mainline; + } + } } /// An immutable view of a [Branch] node. @@ -605,4 +625,12 @@ class ViewBranch with _$ViewBranch implements ViewNode { @override UciCharPair get id => UciCharPair.fromMove(sanMove.move); + + @override + Iterable get mainline sync* { + for (final child in children) { + yield child; + yield* child.mainline; + } + } } diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index 9d52ad8671..c522f99b46 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -21,7 +21,6 @@ import 'package:lichess_mobile/src/model/engine/engine.dart'; import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; import 'package:lichess_mobile/src/model/game/game_repository_providers.dart'; import 'package:lichess_mobile/src/model/game/game_share_service.dart'; -import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_preferences.dart'; import 'package:lichess_mobile/src/model/settings/brightness.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index b99b79dc04..a6c37bb777 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -1,3 +1,4 @@ +import 'package:collection/collection.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -11,6 +12,7 @@ import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/move_list.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; @@ -60,81 +62,88 @@ class _Body extends ConsumerWidget { ? defaultBoardSize - kTabletBoardTableSidePadding * 2 : defaultBoardSize; - return aspectRatio > 1 - ? Row( - mainAxisSize: MainAxisSize.max, - children: [ - Padding( - padding: const EdgeInsets.only( - left: kTabletBoardTableSidePadding, - top: kTabletBoardTableSidePadding, - bottom: kTabletBoardTableSidePadding, - ), - child: Row( - children: [ - AnalysisBoard( - pgn, - options, - boardSize, - isTablet: isTablet, - ), - ], - ), - ), - Flexible( - fit: FlexFit.loose, - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Expanded( - child: PlatformCard( - borderRadius: const BorderRadius.all( - Radius.circular(4.0), - ), - margin: const EdgeInsets.all( - kTabletBoardTableSidePadding, - ), - semanticContainer: false, - child: OpeningExplorerWidget( - pgn: pgn, - options: options, - ), - ), - ), - ], - ), - ), - ], - ) - : Column( - children: [ - if (isTablet) - Padding( - padding: const EdgeInsets.all( - kTabletBoardTableSidePadding, - ), - child: AnalysisBoard( - pgn, - options, - boardSize, - isTablet: isTablet, - ), - ) - else + // If the aspect ratio is greater than 1, we are in landscape mode + if (aspectRatio > 1) { + return Row( + mainAxisSize: MainAxisSize.max, + children: [ + Padding( + padding: const EdgeInsets.only( + left: kTabletBoardTableSidePadding, + top: kTabletBoardTableSidePadding, + bottom: kTabletBoardTableSidePadding, + ), + child: Row( + children: [ AnalysisBoard( pgn, options, boardSize, isTablet: isTablet, ), - Expanded( - child: OpeningExplorerWidget( - pgn: pgn, - options: options, + ], + ), + ), + Flexible( + fit: FlexFit.loose, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Expanded( + child: PlatformCard( + borderRadius: const BorderRadius.all( + Radius.circular(4.0), + ), + margin: const EdgeInsets.all( + kTabletBoardTableSidePadding, + ), + semanticContainer: false, + child: OpeningExplorerWidget( + pgn: pgn, + options: options, + ), + ), ), - ), - ], - ); + ], + ), + ), + ], + ); + } + // If the aspect ratio is less than 1, we are in portrait mode + else { + return Column( + children: [ + Padding( + padding: isTablet + ? const EdgeInsets.symmetric( + horizontal: kTabletBoardTableSidePadding, + ) + : EdgeInsets.zero, + child: _MoveList(pgn: pgn, options: options), + ), + Padding( + padding: isTablet + ? const EdgeInsets.all( + kTabletBoardTableSidePadding, + ) + : EdgeInsets.zero, + child: AnalysisBoard( + pgn, + options, + boardSize, + isTablet: isTablet, + ), + ), + Expanded( + child: OpeningExplorerWidget( + pgn: pgn, + options: options, + ), + ), + ], + ); + } }, ), ), @@ -145,6 +154,35 @@ class _Body extends ConsumerWidget { } } +class _MoveList extends ConsumerWidget { + const _MoveList({ + required this.pgn, + required this.options, + }); + + final String pgn; + final AnalysisOptions options; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final ctrlProvider = analysisControllerProvider(pgn, options); + final state = ref.watch(ctrlProvider); + final slicedMoves = state.root.mainline + .map((e) => e.sanMove.san) + .toList() + .asMap() + .entries + .slices(2); + final currentMoveIndex = state.currentNode.position.ply; + + return MoveList( + type: MoveListType.inline, + slicedMoves: slicedMoves, + currentMoveIndex: currentMoveIndex, + ); + } +} + class _BottomBar extends ConsumerWidget { const _BottomBar({ required this.pgn, diff --git a/lib/src/widgets/board_table.dart b/lib/src/widgets/board_table.dart index 3f575eacf4..512732f8d9 100644 --- a/lib/src/widgets/board_table.dart +++ b/lib/src/widgets/board_table.dart @@ -6,17 +6,10 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/constants.dart'; -import 'package:lichess_mobile/src/model/account/account_preferences.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; -import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/rate_limit.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; - -import 'platform.dart'; - -const _scrollAnimationDuration = Duration(milliseconds: 200); -const _moveListOpacity = 0.6; +import 'package:lichess_mobile/src/widgets/move_list.dart'; /// Board layout that adapts to screen size and aspect ratio. /// @@ -397,272 +390,3 @@ class BoardSettingsOverrides { ); } } - -enum MoveListType { inline, stacked } - -class MoveList extends ConsumerStatefulWidget { - const MoveList({ - required this.type, - required this.slicedMoves, - required this.currentMoveIndex, - this.onSelectMove, - }); - - final MoveListType type; - - final Iterable>> slicedMoves; - - final int currentMoveIndex; - final void Function(int moveIndex)? onSelectMove; - - @override - ConsumerState createState() => _MoveListState(); -} - -class _MoveListState extends ConsumerState { - final currentMoveKey = GlobalKey(); - final _debounce = Debouncer(const Duration(milliseconds: 100)); - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) { - if (currentMoveKey.currentContext != null) { - Scrollable.ensureVisible( - currentMoveKey.currentContext!, - alignment: 0.5, - ); - } - }); - } - - @override - void dispose() { - _debounce.dispose(); - super.dispose(); - } - - @override - void didUpdateWidget(covariant MoveList oldWidget) { - super.didUpdateWidget(oldWidget); - _debounce(() { - if (currentMoveKey.currentContext != null) { - Scrollable.ensureVisible( - currentMoveKey.currentContext!, - alignment: 0.5, - duration: _scrollAnimationDuration, - curve: Curves.easeIn, - ); - } - }); - } - - @override - Widget build(BuildContext context) { - final pieceNotation = ref.watch(pieceNotationProvider).maybeWhen( - data: (value) => value, - orElse: () => defaultAccountPreferences.pieceNotation, - ); - - return widget.type == MoveListType.inline - ? Container( - padding: const EdgeInsets.only(left: 5), - height: 40, - width: double.infinity, - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: widget.slicedMoves - .mapIndexed( - (index, moves) => Container( - margin: const EdgeInsets.only(right: 10), - child: Row( - children: [ - InlineMoveCount(count: index + 1), - ...moves.map( - (move) { - // cursor index starts at 0, move index starts at 1 - final isCurrentMove = - widget.currentMoveIndex == move.key + 1; - return InlineMoveItem( - key: isCurrentMove ? currentMoveKey : null, - move: move, - pieceNotation: pieceNotation, - current: isCurrentMove, - onSelectMove: widget.onSelectMove, - ); - }, - ), - ], - ), - ), - ) - .toList(growable: false), - ), - ), - ) - : PlatformCard( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: SingleChildScrollView( - child: Column( - children: widget.slicedMoves - .mapIndexed( - (index, moves) => Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - StackedMoveCount(count: index + 1), - Expanded( - child: Row( - children: [ - ...moves.map( - (move) { - // cursor index starts at 0, move index starts at 1 - final isCurrentMove = - widget.currentMoveIndex == - move.key + 1; - return Expanded( - child: StackedMoveItem( - key: isCurrentMove - ? currentMoveKey - : null, - move: move, - current: isCurrentMove, - onSelectMove: widget.onSelectMove, - ), - ); - }, - ), - ], - ), - ), - ], - ), - ) - .toList(growable: false), - ), - ), - ), - ); - } -} - -class InlineMoveCount extends StatelessWidget { - const InlineMoveCount({required this.count}); - - final int count; - - @override - Widget build(BuildContext context) { - return Container( - margin: const EdgeInsets.only(right: 3), - child: Text( - '$count.', - style: TextStyle( - fontWeight: FontWeight.w600, - color: textShade(context, _moveListOpacity), - ), - ), - ); - } -} - -class InlineMoveItem extends StatelessWidget { - const InlineMoveItem({ - required this.move, - required this.pieceNotation, - this.current, - this.onSelectMove, - super.key, - }); - - final MapEntry move; - final PieceNotation pieceNotation; - final bool? current; - final void Function(int moveIndex)? onSelectMove; - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: onSelectMove != null ? () => onSelectMove!(move.key + 1) : null, - child: Container( - padding: const EdgeInsets.symmetric(vertical: 3, horizontal: 4), - decoration: ShapeDecoration( - color: current == true - ? Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoDynamicColor.resolve( - CupertinoColors.secondarySystemBackground, - context, - ) - : null - // TODO add bg color on android - : null, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(4.0)), - ), - ), - child: Text( - move.value, - style: TextStyle( - fontFamily: - pieceNotation == PieceNotation.symbol ? 'ChessFont' : null, - fontWeight: FontWeight.w600, - color: - current != true ? textShade(context, _moveListOpacity) : null, - ), - ), - ), - ); - } -} - -class StackedMoveCount extends StatelessWidget { - const StackedMoveCount({required this.count}); - - final int count; - - @override - Widget build(BuildContext context) { - return SizedBox( - width: 40, - child: Text( - '$count.', - style: TextStyle( - fontWeight: FontWeight.w600, - color: textShade(context, _moveListOpacity), - ), - ), - ); - } -} - -class StackedMoveItem extends StatelessWidget { - const StackedMoveItem({ - required this.move, - this.current, - this.onSelectMove, - super.key, - }); - - final MapEntry move; - final bool? current; - final void Function(int moveIndex)? onSelectMove; - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: onSelectMove != null ? () => onSelectMove!(move.key + 1) : null, - child: Container( - padding: const EdgeInsets.all(8), - child: Text( - move.value, - style: TextStyle( - fontWeight: current == true ? FontWeight.bold : null, - color: current != true ? textShade(context, 0.8) : null, - ), - ), - ), - ); - } -} diff --git a/lib/src/widgets/move_list.dart b/lib/src/widgets/move_list.dart new file mode 100644 index 0000000000..7b66064341 --- /dev/null +++ b/lib/src/widgets/move_list.dart @@ -0,0 +1,281 @@ +import 'package:collection/collection.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/account/account_preferences.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/utils/rate_limit.dart'; + +import 'platform.dart'; + +const _scrollAnimationDuration = Duration(milliseconds: 200); +const _moveListOpacity = 0.6; + +enum MoveListType { inline, stacked } + +class MoveList extends ConsumerStatefulWidget { + const MoveList({ + required this.type, + required this.slicedMoves, + required this.currentMoveIndex, + this.onSelectMove, + }); + + final MoveListType type; + + final Iterable>> slicedMoves; + + final int currentMoveIndex; + final void Function(int moveIndex)? onSelectMove; + + @override + ConsumerState createState() => _MoveListState(); +} + +class _MoveListState extends ConsumerState { + final currentMoveKey = GlobalKey(); + final _debounce = Debouncer(const Duration(milliseconds: 100)); + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (currentMoveKey.currentContext != null) { + Scrollable.ensureVisible( + currentMoveKey.currentContext!, + alignment: 0.5, + ); + } + }); + } + + @override + void dispose() { + _debounce.dispose(); + super.dispose(); + } + + @override + void didUpdateWidget(covariant MoveList oldWidget) { + super.didUpdateWidget(oldWidget); + _debounce(() { + if (currentMoveKey.currentContext != null) { + Scrollable.ensureVisible( + currentMoveKey.currentContext!, + alignment: 0.5, + duration: _scrollAnimationDuration, + curve: Curves.easeIn, + ); + } + }); + } + + @override + Widget build(BuildContext context) { + final pieceNotation = ref.watch(pieceNotationProvider).maybeWhen( + data: (value) => value, + orElse: () => defaultAccountPreferences.pieceNotation, + ); + + return widget.type == MoveListType.inline + ? Container( + padding: const EdgeInsets.only(left: 5), + height: 40, + width: double.infinity, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: widget.slicedMoves + .mapIndexed( + (index, moves) => Container( + margin: const EdgeInsets.only(right: 10), + child: Row( + children: [ + InlineMoveCount(count: index + 1), + ...moves.map( + (move) { + // cursor index starts at 0, move index starts at 1 + final isCurrentMove = + widget.currentMoveIndex == move.key + 1; + return InlineMoveItem( + key: isCurrentMove ? currentMoveKey : null, + move: move, + pieceNotation: pieceNotation, + current: isCurrentMove, + onSelectMove: widget.onSelectMove, + ); + }, + ), + ], + ), + ), + ) + .toList(growable: false), + ), + ), + ) + : PlatformCard( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: SingleChildScrollView( + child: Column( + children: widget.slicedMoves + .mapIndexed( + (index, moves) => Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + StackedMoveCount(count: index + 1), + Expanded( + child: Row( + children: [ + ...moves.map( + (move) { + // cursor index starts at 0, move index starts at 1 + final isCurrentMove = + widget.currentMoveIndex == + move.key + 1; + return Expanded( + child: StackedMoveItem( + key: isCurrentMove + ? currentMoveKey + : null, + move: move, + current: isCurrentMove, + onSelectMove: widget.onSelectMove, + ), + ); + }, + ), + ], + ), + ), + ], + ), + ) + .toList(growable: false), + ), + ), + ), + ); + } +} + +class InlineMoveCount extends StatelessWidget { + const InlineMoveCount({required this.count}); + + final int count; + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.only(right: 3), + child: Text( + '$count.', + style: TextStyle( + fontWeight: FontWeight.w600, + color: textShade(context, _moveListOpacity), + ), + ), + ); + } +} + +class InlineMoveItem extends StatelessWidget { + const InlineMoveItem({ + required this.move, + required this.pieceNotation, + this.current, + this.onSelectMove, + super.key, + }); + + final MapEntry move; + final PieceNotation pieceNotation; + final bool? current; + final void Function(int moveIndex)? onSelectMove; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onSelectMove != null ? () => onSelectMove!(move.key + 1) : null, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 3, horizontal: 4), + decoration: ShapeDecoration( + color: current == true + ? Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoDynamicColor.resolve( + CupertinoColors.secondarySystemBackground, + context, + ) + : null + // TODO add bg color on android + : null, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(4.0)), + ), + ), + child: Text( + move.value, + style: TextStyle( + fontFamily: + pieceNotation == PieceNotation.symbol ? 'ChessFont' : null, + fontWeight: FontWeight.w600, + color: + current != true ? textShade(context, _moveListOpacity) : null, + ), + ), + ), + ); + } +} + +class StackedMoveCount extends StatelessWidget { + const StackedMoveCount({required this.count}); + + final int count; + + @override + Widget build(BuildContext context) { + return SizedBox( + width: 40, + child: Text( + '$count.', + style: TextStyle( + fontWeight: FontWeight.w600, + color: textShade(context, _moveListOpacity), + ), + ), + ); + } +} + +class StackedMoveItem extends StatelessWidget { + const StackedMoveItem({ + required this.move, + this.current, + this.onSelectMove, + super.key, + }); + + final MapEntry move; + final bool? current; + final void Function(int moveIndex)? onSelectMove; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onSelectMove != null ? () => onSelectMove!(move.key + 1) : null, + child: Container( + padding: const EdgeInsets.all(8), + child: Text( + move.value, + style: TextStyle( + fontWeight: current == true ? FontWeight.bold : null, + color: current != true ? textShade(context, 0.8) : null, + ), + ), + ), + ); + } +} diff --git a/test/model/common/node_test.dart b/test/model/common/node_test.dart index 40a9964d31..36ed27c8a2 100644 --- a/test/model/common/node_test.dart +++ b/test/model/common/node_test.dart @@ -530,6 +530,19 @@ void main() { expect(root.mainline.last.sanMove.move, move); }); }); + + group('ViewNode', () { + test('mainline', () { + final root = Root.fromPgnMoves('e4 e5'); + final viewRoot = root.view; + final mainline = viewRoot.mainline; + + expect(mainline.length, equals(2)); + final list = mainline.toList(); + expect(list[0].sanMove, equals(SanMove('e4', Move.parse('e2e4')!))); + expect(list[1].sanMove, equals(SanMove('e5', Move.parse('e7e5')!))); + }); + }); } const fisherSpasskyPgn = ''' diff --git a/test/model/opening_explorer/opening_explorer_repository_test.dart b/test/model/opening_explorer/opening_explorer_repository_test.dart index 80ebca81b3..7a4c7225d5 100644 --- a/test/model/opening_explorer/opening_explorer_repository_test.dart +++ b/test/model/opening_explorer/opening_explorer_repository_test.dart @@ -3,7 +3,7 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; -import 'package:lichess_mobile/src/model/common/perf.dart'; +import 'package:lichess_mobile/src/model/common/speed.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_repository.dart'; @@ -150,7 +150,7 @@ void main() { final result = await repo.getLichessDatabase( 'fen', - speeds: const ISetConst({Perf.rapid}), + speeds: const ISetConst({Speed.rapid}), ratings: const ISetConst({1000, 1200}), ); expect(result, isA()); @@ -221,7 +221,7 @@ void main() { 'fen', usernameOrId: 'baz', color: Side.white, - speeds: const ISetConst({Perf.bullet}), + speeds: const ISetConst({Speed.bullet}), gameModes: const ISetConst({GameMode.rated}), ); expect(results, isA>()); diff --git a/test/view/game/archived_game_screen_test.dart b/test/view/game/archived_game_screen_test.dart index acd5f5b5ab..bd85e02275 100644 --- a/test/view/game/archived_game_screen_test.dart +++ b/test/view/game/archived_game_screen_test.dart @@ -14,8 +14,8 @@ import 'package:lichess_mobile/src/model/game/player.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/view/game/archived_game_screen.dart'; import 'package:lichess_mobile/src/view/game/game_player.dart'; -import 'package:lichess_mobile/src/widgets/board_table.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; +import 'package:lichess_mobile/src/widgets/move_list.dart'; import '../../test_app.dart'; import '../../test_utils.dart'; diff --git a/test/view/opening_explorer/opening_explorer_screen_test.dart b/test/view/opening_explorer/opening_explorer_screen_test.dart index 1d34e378e6..490e7b141f 100644 --- a/test/view/opening_explorer/opening_explorer_screen_test.dart +++ b/test/view/opening_explorer/opening_explorer_screen_test.dart @@ -17,6 +17,11 @@ import '../../test_app.dart'; import '../../test_utils.dart'; void main() { + final explorerViewFinder = find.descendant( + of: find.byType(OpeningExplorerWidget), + matching: find.byType(Scrollable), + ); + final mockClient = MockClient((request) { if (request.url.host == 'explorer.lichess.ovh') { if (request.url.path == '/masters') { @@ -86,6 +91,7 @@ void main() { await tester.scrollUntilVisible( find.text('Firouzja, A.'), 200, + scrollable: explorerViewFinder, ); expect( @@ -136,6 +142,7 @@ void main() { await tester.scrollUntilVisible( find.byType(OpeningExplorerGameTile), 200, + scrollable: explorerViewFinder, ); expect( @@ -186,6 +193,7 @@ void main() { await tester.scrollUntilVisible( find.byType(OpeningExplorerGameTile), 200, + scrollable: explorerViewFinder, ); expect( From 6059350cee5b10a91db69f3e5b68d208f7c1f356 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 10 Sep 2024 13:09:05 +0200 Subject: [PATCH 308/979] Open opening explorer from analysis on the current ply --- .../model/analysis/analysis_controller.dart | 9 ++- lib/src/view/analysis/analysis_screen.dart | 79 +++++++------------ .../opening_explorer_screen.dart | 3 + 3 files changed, 38 insertions(+), 53 deletions(-) diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index db3f231616..c98e2d658b 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -629,7 +629,6 @@ class AnalysisController extends _$AnalysisController { enum DisplayMode { moves, summary, - openingExplorer, } @freezed @@ -747,6 +746,14 @@ class AnalysisState with _$AnalysisState { position: position, savedEval: currentNode.eval ?? currentNode.serverEval, ); + + AnalysisOptions get openingExplorerOptions => AnalysisOptions( + id: standaloneOpeningExplorerId, + isLocalEvaluationAllowed: false, + orientation: pov, + variant: variant, + initialMoveCursor: currentPath.size, + ); } @freezed diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index c522f99b46..ee10ca2ac9 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -31,8 +31,7 @@ import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/utils/string.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_share_screen.dart'; import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; -import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_settings.dart'; -import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_widget.dart'; +import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_screen.dart'; import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; @@ -246,10 +245,6 @@ class _Body extends ConsumerWidget { : defaultBoardSize; final display = switch (displayMode) { - DisplayMode.openingExplorer => OpeningExplorerWidget( - pgn: pgn, - options: options, - ), DisplayMode.summary => ServerAnalysisSummary(pgn, options), DisplayMode.moves => AnalysisTreeView( pgn, @@ -386,9 +381,6 @@ class _ColumnTopTable extends ConsumerWidget { analysisPreferencesProvider.select((p) => p.showEvaluationGauge), ); - final displayMode = - ref.watch(ctrlProvider.select((value) => value.displayMode)); - return analysisState.hasAvailableEval ? Column( mainAxisSize: MainAxisSize.min, @@ -399,8 +391,7 @@ class _ColumnTopTable extends ConsumerWidget { displayMode: EngineGaugeDisplayMode.horizontal, params: analysisState.engineGaugeParams, ), - if (displayMode != DisplayMode.openingExplorer && - analysisState.isEngineAvailable) + if (analysisState.isEngineAvailable) _EngineLines(ctrlProvider, isLandscape: false), ], ) @@ -576,14 +567,7 @@ class _BottomBar extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final ctrlProvider = analysisControllerProvider(pgn, options); - final canGoBack = - ref.watch(ctrlProvider.select((value) => value.canGoBack)); - final canGoNext = - ref.watch(ctrlProvider.select((value) => value.canGoNext)); - final displayMode = - ref.watch(ctrlProvider.select((value) => value.displayMode)); - final canShowGameSummary = - ref.watch(ctrlProvider.select((value) => value.canShowGameSummary)); + final analysisState = ref.watch(ctrlProvider); final isOnline = ref.watch(connectivityChangesProvider).valueOrNull?.isOnline ?? false; @@ -596,65 +580,56 @@ class _BottomBar extends ConsumerWidget { }, icon: Icons.menu, ), - if (displayMode != DisplayMode.openingExplorer && canShowGameSummary) + if (analysisState.canShowGameSummary) BottomBarButton( // TODO: l10n - label: displayMode == DisplayMode.summary ? 'Moves' : 'Summary', - onTap: displayMode != DisplayMode.openingExplorer - ? () { - final newMode = displayMode == DisplayMode.summary - ? DisplayMode.moves - : DisplayMode.summary; - ref.read(ctrlProvider.notifier).setDisplayMode(newMode); - } - : null, - icon: displayMode == DisplayMode.summary + label: analysisState.displayMode == DisplayMode.summary + ? 'Moves' + : 'Summary', + onTap: () { + final newMode = analysisState.displayMode == DisplayMode.summary + ? DisplayMode.moves + : DisplayMode.summary; + ref.read(ctrlProvider.notifier).setDisplayMode(newMode); + }, + icon: analysisState.displayMode == DisplayMode.summary ? LichessIcons.flow_cascade : Icons.area_chart, ), - if (displayMode == DisplayMode.openingExplorer) - BottomBarButton( - label: 'Opening Explorer Settings', - onTap: () => showAdaptiveBottomSheet( - context: context, - isScrollControlled: true, - showDragHandle: true, - isDismissible: true, - builder: (_) => OpeningExplorerSettings(pgn, options), - ), - icon: Icons.tune, - ), BottomBarButton( label: context.l10n.openingExplorer, - highlighted: displayMode == DisplayMode.openingExplorer, onTap: isOnline ? () { - ref.read(ctrlProvider.notifier).setDisplayMode( - displayMode == DisplayMode.openingExplorer - ? DisplayMode.moves - : DisplayMode.openingExplorer, - ); + pushPlatformRoute( + context, + title: context.l10n.openingExplorer, + builder: (_) => OpeningExplorerScreen( + pgn: pgn, + options: analysisState.openingExplorerOptions, + ), + ); } : null, icon: Icons.explore, ), RepeatButton( - onLongPress: canGoBack ? () => _moveBackward(ref) : null, + onLongPress: + analysisState.canGoBack ? () => _moveBackward(ref) : null, child: BottomBarButton( key: const ValueKey('goto-previous'), - onTap: canGoBack ? () => _moveBackward(ref) : null, + onTap: analysisState.canGoBack ? () => _moveBackward(ref) : null, label: 'Previous', icon: CupertinoIcons.chevron_back, showTooltip: false, ), ), RepeatButton( - onLongPress: canGoNext ? () => _moveForward(ref) : null, + onLongPress: analysisState.canGoNext ? () => _moveForward(ref) : null, child: BottomBarButton( key: const ValueKey('goto-next'), icon: CupertinoIcons.chevron_forward, label: context.l10n.next, - onTap: canGoNext ? () => _moveForward(ref) : null, + onTap: analysisState.canGoNext ? () => _moveForward(ref) : null, showTooltip: false, ), ), diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index a6c37bb777..9cf4bd63f4 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -179,6 +179,9 @@ class _MoveList extends ConsumerWidget { type: MoveListType.inline, slicedMoves: slicedMoves, currentMoveIndex: currentMoveIndex, + onSelectMove: (index) { + ref.read(ctrlProvider.notifier).jumpToNthNodeOnMainline(index - 1); + }, ); } } From 0552c4cd513e09686c3d4a6b0794b3b94c707cc0 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 10 Sep 2024 15:13:01 +0200 Subject: [PATCH 309/979] Analysis and explorer fixes --- .../model/analysis/analysis_controller.dart | 7 +- lib/src/view/analysis/analysis_board.dart | 12 +- lib/src/view/analysis/analysis_screen.dart | 26 ++- lib/src/view/analysis/tree_view.dart | 13 +- .../opening_explorer_screen.dart | 57 ++++--- .../opening_explorer_widget.dart | 149 +++++++----------- lib/src/widgets/move_list.dart | 5 +- lib/src/widgets/platform.dart | 4 + 8 files changed, 132 insertions(+), 141 deletions(-) diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index c98e2d658b..0341665c95 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -389,9 +389,10 @@ class AnalysisController extends _$AnalysisController { return _root.makePgn(state.pgnHeaders, state.pgnRootComments); } - /// Makes an internal PGN string (without headers and comments) of the current game state. - String makeInternalPgn() { - return _root.makePgn(); + /// Makes a PGN string up to the current node only. + String makeCurrentNodePgn() { + final nodes = _root.branchesOn(state.currentPath); + return nodes.map((node) => node.sanMove.san).join(' '); } void _setPath( diff --git a/lib/src/view/analysis/analysis_board.dart b/lib/src/view/analysis/analysis_board.dart index 5607d34b96..7ecca1eac5 100644 --- a/lib/src/view/analysis/analysis_board.dart +++ b/lib/src/view/analysis/analysis_board.dart @@ -22,13 +22,13 @@ class AnalysisBoard extends ConsumerStatefulWidget { this.pgn, this.options, this.boardSize, { - required this.isTablet, + this.borderRadius, }); final String pgn; final AnalysisOptions options; final double boardSize; - final bool isTablet; + final BorderRadiusGeometry? borderRadius; @override ConsumerState createState() => AnalysisBoardState(); @@ -102,10 +102,10 @@ class AnalysisBoardState extends ConsumerState { : IMap({sanMove.move.to: annotation}) : null, settings: boardPrefs.toBoardSettings().copyWith( - borderRadius: widget.isTablet - ? const BorderRadius.all(Radius.circular(4.0)) - : BorderRadius.zero, - boxShadow: widget.isTablet ? boardShadows : const [], + borderRadius: widget.borderRadius, + boxShadow: widget.borderRadius != null + ? boardShadows + : const [], drawShape: DrawShapeOptions( enable: true, onCompleteShape: _onCompleteShape, diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index ee10ca2ac9..90edb335f3 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -244,6 +244,9 @@ class _Body extends ConsumerWidget { ? defaultBoardSize - kTabletBoardTableSidePadding * 2 : defaultBoardSize; + const tabletBoardRadius = + BorderRadius.all(Radius.circular(4.0)); + final display = switch (displayMode) { DisplayMode.summary => ServerAnalysisSummary(pgn, options), DisplayMode.moves => AnalysisTreeView( @@ -272,7 +275,7 @@ class _Body extends ConsumerWidget { pgn, options, boardSize, - isTablet: isTablet, + borderRadius: isTablet ? tabletBoardRadius : null, ), if (hasEval && showEvaluationGauge) ...[ const SizedBox(width: 4.0), @@ -293,6 +296,10 @@ class _Body extends ConsumerWidget { ), Expanded( child: PlatformCard( + clipBehavior: Clip.hardEdge, + borderRadius: const BorderRadius.all( + Radius.circular(4.0), + ), margin: const EdgeInsets.all( kTabletBoardTableSidePadding, ), @@ -323,7 +330,7 @@ class _Body extends ConsumerWidget { pgn, options, boardSize, - isTablet: isTablet, + borderRadius: isTablet ? tabletBoardRadius : null, ), ) else @@ -331,9 +338,18 @@ class _Body extends ConsumerWidget { pgn, options, boardSize, - isTablet: isTablet, + borderRadius: isTablet ? tabletBoardRadius : null, ), - Expanded(child: display), + Expanded( + child: Padding( + padding: isTablet + ? const EdgeInsets.symmetric( + horizontal: kTabletBoardTableSidePadding, + ) + : EdgeInsets.zero, + child: display, + ), + ), ], ); } @@ -604,7 +620,7 @@ class _BottomBar extends ConsumerWidget { context, title: context.l10n.openingExplorer, builder: (_) => OpeningExplorerScreen( - pgn: pgn, + pgn: ref.read(ctrlProvider.notifier).makeCurrentNodePgn(), options: analysisState.openingExplorerOptions, ), ); diff --git a/lib/src/view/analysis/tree_view.dart b/lib/src/view/analysis/tree_view.dart index 8cdb736479..de57954f63 100644 --- a/lib/src/view/analysis/tree_view.dart +++ b/lib/src/view/analysis/tree_view.dart @@ -718,18 +718,7 @@ class _Opening extends ConsumerWidget { height: kOpeningHeaderHeight, width: double.infinity, decoration: BoxDecoration( - color: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoDynamicColor.resolve( - CupertinoColors.systemGrey5, - context, - ) - : Theme.of(context).colorScheme.secondaryContainer, - borderRadius: displayMode == Orientation.landscape - ? const BorderRadius.only( - topLeft: Radius.circular(10.0), - topRight: Radius.circular(10.0), - ) - : null, + color: Theme.of(context).colorScheme.secondaryContainer, ), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index 9cf4bd63f4..bbd41281c8 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -36,6 +36,8 @@ class OpeningExplorerScreen extends StatelessWidget { } } +const _kTabletBoardRadius = BorderRadius.all(Radius.circular(4.0)); + class _Body extends ConsumerWidget { final String pgn; @@ -44,16 +46,25 @@ class _Body extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final isTablet = isTabletOrLarger(context); + return SafeArea( bottom: false, child: Column( children: [ + Padding( + padding: isTablet + ? const EdgeInsets.symmetric( + horizontal: kTabletBoardTableSidePadding, + ) + : EdgeInsets.zero, + child: _MoveList(pgn: pgn, options: options), + ), Expanded( child: LayoutBuilder( builder: (context, constraints) { final aspectRatio = constraints.biggest.aspectRatio; final defaultBoardSize = constraints.biggest.shortestSide; - final isTablet = isTabletOrLarger(context); final remainingHeight = constraints.maxHeight - defaultBoardSize; final isSmallScreen = @@ -73,15 +84,11 @@ class _Body extends ConsumerWidget { top: kTabletBoardTableSidePadding, bottom: kTabletBoardTableSidePadding, ), - child: Row( - children: [ - AnalysisBoard( - pgn, - options, - boardSize, - isTablet: isTablet, - ), - ], + child: AnalysisBoard( + pgn, + options, + boardSize, + borderRadius: isTablet ? _kTabletBoardRadius : null, ), ), Flexible( @@ -91,6 +98,7 @@ class _Body extends ConsumerWidget { children: [ Expanded( child: PlatformCard( + clipBehavior: Clip.hardEdge, borderRadius: const BorderRadius.all( Radius.circular(4.0), ), @@ -116,29 +124,30 @@ class _Body extends ConsumerWidget { children: [ Padding( padding: isTablet - ? const EdgeInsets.symmetric( - horizontal: kTabletBoardTableSidePadding, - ) - : EdgeInsets.zero, - child: _MoveList(pgn: pgn, options: options), - ), - Padding( - padding: isTablet - ? const EdgeInsets.all( - kTabletBoardTableSidePadding, + ? const EdgeInsets.only( + left: kTabletBoardTableSidePadding, + right: kTabletBoardTableSidePadding, + bottom: kTabletBoardTableSidePadding, ) : EdgeInsets.zero, child: AnalysisBoard( pgn, options, boardSize, - isTablet: isTablet, + borderRadius: isTablet ? _kTabletBoardRadius : null, ), ), Expanded( - child: OpeningExplorerWidget( - pgn: pgn, - options: options, + child: Padding( + padding: isTablet + ? const EdgeInsets.symmetric( + horizontal: kTabletBoardTableSidePadding, + ) + : EdgeInsets.zero, + child: OpeningExplorerWidget( + pgn: pgn, + options: options, + ), ), ), ], diff --git a/lib/src/view/opening_explorer/opening_explorer_widget.dart b/lib/src/view/opening_explorer/opening_explorer_widget.dart index 50a0fa73f0..af2bc8711a 100644 --- a/lib/src/view/opening_explorer/opening_explorer_widget.dart +++ b/lib/src/view/opening_explorer/opening_explorer_widget.dart @@ -108,10 +108,7 @@ class _OpeningExplorerState extends ConsumerState { data: (openingExplorer) { if (openingExplorer == null) { return _OpeningExplorerView.loading( - pgn: widget.pgn, - options: widget.options, - opening: opening, - explorerContent: lastExplorerWidgets ?? + children: lastExplorerWidgets ?? [ Shimmer( child: ShimmerLoading( @@ -127,12 +124,8 @@ class _OpeningExplorerState extends ConsumerState { } if (openingExplorer.entry.moves.isEmpty) { lastExplorerWidgets = null; - return _OpeningExplorerView( - pgn: widget.pgn, - options: widget.options, - opening: opening, - openingExplorer: openingExplorer, - explorerContent: [ + return _OpeningExplorerView.empty( + children: [ Center( child: Padding( padding: const EdgeInsets.all(16.0), @@ -148,7 +141,30 @@ class _OpeningExplorerState extends ConsumerState { final ply = analysisState.position.ply; - final explorerContent = [ + final children = [ + Container( + padding: _kTableRowPadding, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.secondaryContainer, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (opening != null) + Expanded( + flex: 75, + child: _Opening( + opening: opening, + ), + ), + if (openingExplorer.isIndexing == true) + Expanded( + flex: 25, + child: _IndexingIndicator(), + ), + ], + ), + ), _OpeningExplorerMoveTable( moves: openingExplorer.entry.moves, whiteWins: openingExplorer.entry.white, @@ -199,21 +215,14 @@ class _OpeningExplorerState extends ConsumerState { ], ]; - lastExplorerWidgets = explorerContent; + lastExplorerWidgets = children; return _OpeningExplorerView( - pgn: widget.pgn, - options: widget.options, - opening: opening, - openingExplorer: openingExplorer, - explorerContent: explorerContent, + children: children, ); }, loading: () => _OpeningExplorerView.loading( - pgn: widget.pgn, - options: widget.options, - opening: opening, - explorerContent: lastExplorerWidgets ?? + children: lastExplorerWidgets ?? [ Shimmer( child: ShimmerLoading( @@ -241,88 +250,50 @@ class _OpeningExplorerState extends ConsumerState { /// The opening header and the opening explorer move table. class _OpeningExplorerView extends StatelessWidget { const _OpeningExplorerView({ - required this.pgn, - required this.options, - required this.opening, - required this.openingExplorer, - required this.explorerContent, - }) : loading = false; + required this.children, + }) : loading = false, + empty = false; const _OpeningExplorerView.loading({ - required this.pgn, - required this.options, - required this.opening, - required this.explorerContent, + required this.children, }) : loading = true, - openingExplorer = null; + empty = false; - final String pgn; - final AnalysisOptions options; - final Opening? opening; - final ({OpeningExplorerEntry entry, bool isIndexing})? openingExplorer; - final List explorerContent; + const _OpeningExplorerView.empty({ + required this.children, + }) : loading = false, + empty = true; + + final List children; final bool loading; + final bool empty; @override Widget build(BuildContext context) { - final isLandscape = - MediaQuery.orientationOf(context) == Orientation.landscape; - final loadingOverlayColor = Theme.of(context).brightness == Brightness.dark ? Colors.black : Colors.white; - return Column( + return Stack( children: [ - Container( - padding: _kTableRowPadding, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.secondaryContainer, - borderRadius: BorderRadius.only( - topLeft: Radius.circular(isLandscape ? 4.0 : 0), - topRight: Radius.circular(isLandscape ? 4.0 : 0), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - if (opening != null) - Expanded( - flex: 75, - child: _Opening( - opening: opening!, - ), - ), - if (openingExplorer?.isIndexing == true) - Expanded( - flex: 25, - child: _IndexingIndicator(), - ), - ], + if (empty) + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: children, + ) + else + Center( + child: ListView(padding: EdgeInsets.zero, children: children), ), - ), - Expanded( - child: Stack( - children: [ - Center( - child: ListView( - shrinkWrap: true, - padding: EdgeInsets.zero, - children: explorerContent, - ), - ), - Positioned.fill( - child: IgnorePointer( - ignoring: !loading, - child: AnimatedOpacity( - duration: const Duration(milliseconds: 300), - curve: Curves.fastOutSlowIn, - opacity: loading ? 0.5 : 0.0, - child: ColoredBox(color: loadingOverlayColor), - ), - ), - ), - ], + Positioned.fill( + child: IgnorePointer( + ignoring: !loading, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 300), + curve: Curves.fastOutSlowIn, + opacity: loading ? 0.5 : 0.0, + child: ColoredBox(color: loadingOverlayColor), + ), ), ), ], diff --git a/lib/src/widgets/move_list.dart b/lib/src/widgets/move_list.dart index 7b66064341..997d594cc0 100644 --- a/lib/src/widgets/move_list.dart +++ b/lib/src/widgets/move_list.dart @@ -221,8 +221,9 @@ class InlineMoveItem extends StatelessWidget { fontFamily: pieceNotation == PieceNotation.symbol ? 'ChessFont' : null, fontWeight: FontWeight.w600, - color: - current != true ? textShade(context, _moveListOpacity) : null, + color: current != true + ? textShade(context, _moveListOpacity) + : Theme.of(context).colorScheme.primary, ), ), ), diff --git a/lib/src/widgets/platform.dart b/lib/src/widgets/platform.dart index 483e1c6ff7..35e6783100 100644 --- a/lib/src/widgets/platform.dart +++ b/lib/src/widgets/platform.dart @@ -71,6 +71,7 @@ class PlatformCard extends StatelessWidget { this.elevation, this.color, this.shadowColor, + this.clipBehavior, }); final Widget child; @@ -79,6 +80,7 @@ class PlatformCard extends StatelessWidget { final double? elevation; final Color? color; final Color? shadowColor; + final Clip? clipBehavior; /// The empty space that surrounds the card. /// @@ -106,6 +108,7 @@ class PlatformCard extends StatelessWidget { borderRadius: BorderRadius.all(Radius.circular(10.0)), ), semanticContainer: semanticContainer, + clipBehavior: clipBehavior, child: child, ) : Card( @@ -121,6 +124,7 @@ class PlatformCard extends StatelessWidget { semanticContainer: semanticContainer, elevation: elevation, margin: margin, + clipBehavior: clipBehavior, child: child, ), ); From 9303854e84933b09093205574a038df82edcfd22 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 10 Sep 2024 15:22:04 +0200 Subject: [PATCH 310/979] Tweak theme screen --- lib/src/view/settings/theme_screen.dart | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/lib/src/view/settings/theme_screen.dart b/lib/src/view/settings/theme_screen.dart index dea7be647f..91c08deca8 100644 --- a/lib/src/view/settings/theme_screen.dart +++ b/lib/src/view/settings/theme_screen.dart @@ -80,21 +80,18 @@ class _Body extends ConsumerWidget { child: Chessboard.fixed( size: boardSize, orientation: Side.white, - fen: kInitialFEN, + lastMove: const NormalMove(from: Square.e2, to: Square.e4), + fen: + 'rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1', shapes: { - Arrow( - color: boardPrefs.shapeColor.color, - orig: Square.fromName('e2'), - dest: Square.fromName('e4'), - ), Circle( color: boardPrefs.shapeColor.color, - orig: Square.fromName('d2'), + orig: Square.fromName('b8'), ), Arrow( color: boardPrefs.shapeColor.color, - orig: Square.fromName('b1'), - dest: Square.fromName('c3'), + orig: Square.fromName('b8'), + dest: Square.fromName('c6'), ), }.lock, settings: ChessboardSettings( From 995d3fe9c7bd2ea4eae6abedd129ed946c596e34 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 10 Sep 2024 15:26:42 +0200 Subject: [PATCH 311/979] Bump version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 7805d9aedc..ab8d2497a5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: lichess_mobile description: Lichess mobile app V2 publish_to: "none" -version: 0.10.2+001002 # see README.md for details about versioning +version: 0.10.3+001003 # see README.md for details about versioning environment: sdk: ">=3.3.0 <4.0.0" From e6fd5798a1ad9cf0295d908c865fc04e5c64e0c4 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 10 Sep 2024 15:36:51 +0200 Subject: [PATCH 312/979] Tweak explorer style --- .../opening_explorer_widget.dart | 51 ++++++++----------- 1 file changed, 21 insertions(+), 30 deletions(-) diff --git a/lib/src/view/opening_explorer/opening_explorer_widget.dart b/lib/src/view/opening_explorer/opening_explorer_widget.dart index af2bc8711a..8c59cd5ace 100644 --- a/lib/src/view/opening_explorer/opening_explorer_widget.dart +++ b/lib/src/view/opening_explorer/opening_explorer_widget.dart @@ -153,8 +153,23 @@ class _OpeningExplorerState extends ConsumerState { if (opening != null) Expanded( flex: 75, - child: _Opening( - opening: opening, + child: GestureDetector( + onTap: opening.name == context.l10n.startPosition + ? null + : () => launchUrl( + Uri.parse( + 'https://lichess.org/opening/${opening.name}', + ), + ), + child: Text( + '${opening.eco.isEmpty ? "" : "${opening.eco} "}${opening.name}', + style: TextStyle( + color: Theme.of(context) + .colorScheme + .onSecondaryContainer, + fontWeight: FontWeight.bold, + ), + ), ), ), if (openingExplorer.isIndexing == true) @@ -301,31 +316,6 @@ class _OpeningExplorerView extends StatelessWidget { } } -class _Opening extends ConsumerWidget { - const _Opening({ - required this.opening, - }); - - final Opening opening; - @override - Widget build(BuildContext context, WidgetRef ref) { - return GestureDetector( - onTap: opening.name == context.l10n.startPosition - ? null - : () => launchUrl( - Uri.parse('https://lichess.org/opening/${opening.name}'), - ), - child: Text( - '${opening.eco.isEmpty ? "" : "${opening.eco} "}${opening.name}', - style: TextStyle( - color: Theme.of(context).colorScheme.onSecondaryContainer, - fontWeight: FontWeight.bold, - ), - ), - ); - } -} - class _IndexingIndicator extends StatefulWidget { @override State<_IndexingIndicator> createState() => _IndexingIndicatorState(); @@ -419,6 +409,7 @@ class _OpeningExplorerMoveTable extends ConsumerWidget { final ctrlProvider = analysisControllerProvider(pgn, options); const topPadding = EdgeInsets.only(top: _kTableRowVerticalPadding); + const headerTextStyle = TextStyle(fontSize: 12); return Table( columnWidths: columnWidths, @@ -430,15 +421,15 @@ class _OpeningExplorerMoveTable extends ConsumerWidget { children: [ Padding( padding: _kTableRowPadding.subtract(topPadding), - child: Text(context.l10n.move), + child: Text(context.l10n.move, style: headerTextStyle), ), Padding( padding: _kTableRowPadding.subtract(topPadding), - child: Text(context.l10n.games), + child: Text(context.l10n.games, style: headerTextStyle), ), Padding( padding: _kTableRowPadding.subtract(topPadding), - child: Text(context.l10n.whiteDrawBlack), + child: Text(context.l10n.whiteDrawBlack, style: headerTextStyle), ), ], ), From 3eac7253c03b105bbfdd65c23b4c98cc83d57829 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 10 Sep 2024 15:53:50 +0200 Subject: [PATCH 313/979] Another explorer style tweak --- lib/src/view/opening_explorer/opening_explorer_widget.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/src/view/opening_explorer/opening_explorer_widget.dart b/lib/src/view/opening_explorer/opening_explorer_widget.dart index 8c59cd5ace..a2f9e6ae92 100644 --- a/lib/src/view/opening_explorer/opening_explorer_widget.dart +++ b/lib/src/view/opening_explorer/opening_explorer_widget.dart @@ -143,7 +143,9 @@ class _OpeningExplorerState extends ConsumerState { final children = [ Container( - padding: _kTableRowPadding, + padding: _kTableRowPadding.subtract( + const EdgeInsets.only(bottom: _kTableRowVerticalPadding / 2), + ), decoration: BoxDecoration( color: Theme.of(context).colorScheme.secondaryContainer, ), @@ -408,7 +410,7 @@ class _OpeningExplorerMoveTable extends ConsumerWidget { final games = whiteWins + draws + blackWins; final ctrlProvider = analysisControllerProvider(pgn, options); - const topPadding = EdgeInsets.only(top: _kTableRowVerticalPadding); + const topPadding = EdgeInsets.only(top: _kTableRowVerticalPadding / 2); const headerTextStyle = TextStyle(fontSize: 12); return Table( From ae3cbbaea0c7674839ff89d0847c38a424cf1511 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 10 Sep 2024 16:03:49 +0200 Subject: [PATCH 314/979] Tweak analysis move context menu --- lib/src/view/analysis/tree_view.dart | 196 ++++++++++++++------------- 1 file changed, 100 insertions(+), 96 deletions(-) diff --git a/lib/src/view/analysis/tree_view.dart b/lib/src/view/analysis/tree_view.dart index de57954f63..d2b502d365 100644 --- a/lib/src/view/analysis/tree_view.dart +++ b/lib/src/view/analysis/tree_view.dart @@ -488,122 +488,126 @@ class _MoveContextMenu extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final ctrlProvider = analysisControllerProvider(pgn, options); - return ListView( - shrinkWrap: true, - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - title, - style: Theme.of(context).textTheme.titleLarge, - ), - if (branch.clock != null) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon( - Icons.punch_clock, - ), - const SizedBox(width: 4.0), - Text( - branch.clock!.toHoursMinutesSeconds( - showTenths: - branch.clock! < const Duration(minutes: 1), - ), - ), - ], - ), - if (branch.elapsedMoveTime != null) ...[ - const SizedBox(height: 4.0), + return SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: + const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + title, + style: Theme.of(context).textTheme.titleLarge, + ), + if (branch.clock != null) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ Row( mainAxisSize: MainAxisSize.min, children: [ const Icon( - Icons.hourglass_bottom, + Icons.punch_clock, ), const SizedBox(width: 4.0), Text( - branch.elapsedMoveTime! - .toHoursMinutesSeconds(showTenths: true), + branch.clock!.toHoursMinutesSeconds( + showTenths: + branch.clock! < const Duration(minutes: 1), + ), ), ], ), + if (branch.elapsedMoveTime != null) ...[ + const SizedBox(height: 4.0), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.hourglass_bottom, + ), + const SizedBox(width: 4.0), + Text( + branch.elapsedMoveTime! + .toHoursMinutesSeconds(showTenths: true), + ), + ], + ), + ], ], - ], - ), - ], + ), + ], + ), ), - ), - if (branch.hasLichessAnalysisTextComment) - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 8.0, + if (branch.hasLichessAnalysisTextComment) + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 8.0, + ), + child: Text( + branch.lichessAnalysisComments! + .map((c) => c.text ?? '') + .join(' '), + ), ), - child: Text( - branch.lichessAnalysisComments! - .map((c) => c.text ?? '') - .join(' '), + if (branch.hasTextComment) + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 8.0, + ), + child: Text( + branch.comments!.map((c) => c.text ?? '').join(' '), + ), ), - ), - if (branch.hasTextComment) - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 8.0, + const PlatformDivider(indent: 0), + if (parent.children.any((c) => c.isHidden)) + BottomSheetContextMenuAction( + icon: Icons.subtitles, + child: Text(context.l10n.mobileShowVariations), + onPressed: () { + ref.read(ctrlProvider.notifier).showAllVariations(path); + }, ), - child: Text( - branch.comments!.map((c) => c.text ?? '').join(' '), + if (isSideline) + BottomSheetContextMenuAction( + icon: Icons.subtitles_off, + child: Text(context.l10n.mobileHideVariation), + onPressed: () { + ref.read(ctrlProvider.notifier).hideVariation(path); + }, + ), + if (isSideline) + BottomSheetContextMenuAction( + icon: Icons.expand_less, + child: Text(context.l10n.promoteVariation), + onPressed: () { + ref.read(ctrlProvider.notifier).promoteVariation(path, false); + }, + ), + if (isSideline) + BottomSheetContextMenuAction( + icon: Icons.check, + child: Text(context.l10n.makeMainLine), + onPressed: () { + ref.read(ctrlProvider.notifier).promoteVariation(path, true); + }, ), - ), - const PlatformDivider(indent: 0), - if (parent.children.any((c) => c.isHidden)) - BottomSheetContextMenuAction( - icon: Icons.subtitles, - child: Text(context.l10n.mobileShowVariations), - onPressed: () { - ref.read(ctrlProvider.notifier).showAllVariations(path); - }, - ), - if (isSideline) - BottomSheetContextMenuAction( - icon: Icons.subtitles_off, - child: Text(context.l10n.mobileHideVariation), - onPressed: () { - ref.read(ctrlProvider.notifier).hideVariation(path); - }, - ), - if (isSideline) - BottomSheetContextMenuAction( - icon: Icons.expand_less, - child: Text(context.l10n.promoteVariation), - onPressed: () { - ref.read(ctrlProvider.notifier).promoteVariation(path, false); - }, - ), - if (isSideline) BottomSheetContextMenuAction( - icon: Icons.check, - child: Text(context.l10n.makeMainLine), + icon: Icons.delete, + child: Text(context.l10n.deleteFromHere), onPressed: () { - ref.read(ctrlProvider.notifier).promoteVariation(path, true); + ref.read(ctrlProvider.notifier).deleteFromHere(path); }, ), - BottomSheetContextMenuAction( - icon: Icons.delete, - child: Text(context.l10n.deleteFromHere), - onPressed: () { - ref.read(ctrlProvider.notifier).deleteFromHere(path); - }, - ), - ], + const SizedBox(height: 8.0), + ], + ), ); } } From 8f27204a6d04c6a4b81735104bb5acaccb3861cf Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 11 Sep 2024 08:03:24 +0200 Subject: [PATCH 315/979] Add connectivity banner to tools tab --- lib/src/view/tools/tools_tab_screen.dart | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/src/view/tools/tools_tab_screen.dart b/lib/src/view/tools/tools_tab_screen.dart index a7073299b0..83d48eb25a 100644 --- a/lib/src/view/tools/tools_tab_screen.dart +++ b/lib/src/view/tools/tools_tab_screen.dart @@ -14,6 +14,7 @@ import 'package:lichess_mobile/src/view/board_editor/board_editor_screen.dart'; import 'package:lichess_mobile/src/view/clock/clock_screen.dart'; import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_screen.dart'; import 'package:lichess_mobile/src/view/tools/load_position_screen.dart'; +import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; @@ -41,7 +42,12 @@ class ToolsTabScreen extends ConsumerWidget { appBar: AppBar( title: Text(context.l10n.tools), ), - body: const Center(child: _Body()), + body: const Column( + children: [ + ConnectivityBanner(), + Expanded(child: _Body()), + ], + ), ), ); } @@ -52,6 +58,7 @@ class ToolsTabScreen extends ConsumerWidget { controller: puzzlesScrollController, slivers: [ CupertinoSliverNavigationBar(largeTitle: Text(context.l10n.tools)), + const SliverToBoxAdapter(child: ConnectivityBanner()), const SliverSafeArea( top: false, sliver: _Body(), From 0796fec1f8c8c5bb56c58e99364b03e548056547 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 11 Sep 2024 08:21:09 +0200 Subject: [PATCH 316/979] Fill coordinates about popup --- .../coordinate_training_screen.dart | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/lib/src/view/coordinate_training/coordinate_training_screen.dart b/lib/src/view/coordinate_training/coordinate_training_screen.dart index 4240270da2..11b077ffe4 100644 --- a/lib/src/view/coordinate_training/coordinate_training_screen.dart +++ b/lib/src/view/coordinate_training/coordinate_training_screen.dart @@ -32,11 +32,39 @@ Future _coordinateTrainingInfoDialogBuilder(BuildContext context) { child: RichText( text: TextSpan( style: DefaultTextStyle.of(context).style, + // TODO translate children: const [ + TextSpan( + text: + 'Knowing the chessboard coordinates is a very important skill for several reasons:\n', + ), + TextSpan( + text: + ' • Most chess courses and exercises use the algebraic notation extensively.\n', + ), + TextSpan( + text: + " • It makes it easier to talk to your chess friends, since you both understand the 'language of chess'.\n", + ), + TextSpan( + text: + ' • You can analyse a game more effectively if you can quickly recognise coordinates.\n', + ), TextSpan( text: '\n', ), - // TODO add explanation here once l10n download from crowdin works again + TextSpan( + text: 'Find Square\n', + style: TextStyle(fontWeight: FontWeight.bold), + ), + TextSpan( + text: + 'A coordinate appears on the board and you must click on the corresponding square.\n', + ), + TextSpan( + text: + 'You have 30 seconds to correctly map as many squares as possible!\n', + ), ], ), ), From c1dc527b66a272b063340ea563c3785197b2661c Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 11 Sep 2024 08:26:03 +0200 Subject: [PATCH 317/979] Center settings in coord training screen --- .../coordinate_training_screen.dart | 243 +++++++++--------- 1 file changed, 122 insertions(+), 121 deletions(-) diff --git a/lib/src/view/coordinate_training/coordinate_training_screen.dart b/lib/src/view/coordinate_training/coordinate_training_screen.dart index 11b077ffe4..20fdad1689 100644 --- a/lib/src/view/coordinate_training/coordinate_training_screen.dart +++ b/lib/src/view/coordinate_training/coordinate_training_screen.dart @@ -24,66 +24,6 @@ import 'package:lichess_mobile/src/widgets/platform_alert_dialog.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:lichess_mobile/src/widgets/settings.dart'; -Future _coordinateTrainingInfoDialogBuilder(BuildContext context) { - return showAdaptiveDialog( - context: context, - builder: (context) { - final content = SingleChildScrollView( - child: RichText( - text: TextSpan( - style: DefaultTextStyle.of(context).style, - // TODO translate - children: const [ - TextSpan( - text: - 'Knowing the chessboard coordinates is a very important skill for several reasons:\n', - ), - TextSpan( - text: - ' • Most chess courses and exercises use the algebraic notation extensively.\n', - ), - TextSpan( - text: - " • It makes it easier to talk to your chess friends, since you both understand the 'language of chess'.\n", - ), - TextSpan( - text: - ' • You can analyse a game more effectively if you can quickly recognise coordinates.\n', - ), - TextSpan( - text: '\n', - ), - TextSpan( - text: 'Find Square\n', - style: TextStyle(fontWeight: FontWeight.bold), - ), - TextSpan( - text: - 'A coordinate appears on the board and you must click on the corresponding square.\n', - ), - TextSpan( - text: - 'You have 30 seconds to correctly map as many squares as possible!\n', - ), - ], - ), - ), - ); - - return PlatformAlertDialog( - title: Text(context.l10n.aboutX('Coordinate Training')), - content: content, - actions: [ - PlatformDialogAction( - onPressed: () => Navigator.of(context).pop(), - child: Text(context.l10n.mobileOkButton), - ), - ], - ); - }, - ); -} - class CoordinateTrainingScreen extends StatelessWidget { const CoordinateTrainingScreen({super.key}); @@ -241,8 +181,10 @@ class _BodyState extends ConsumerState<_Body> { ), ) else - _Settings( - onSideChoiceSelected: _setOrientation, + Expanded( + child: _Settings( + onSideChoiceSelected: _setOrientation, + ), ), ], ); @@ -422,70 +364,69 @@ class _SettingsState extends ConsumerState<_Settings> { Widget build(BuildContext context) { final trainingPrefs = ref.watch(coordinateTrainingPreferencesProvider); - return Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - PlatformListTile( - title: Text(context.l10n.side), - trailing: Padding( - padding: Styles.horizontalBodyPadding, - child: Wrap( - spacing: 8.0, - children: SideChoice.values.map((choice) { - return ChoiceChip( - label: Text(sideChoiceL10n(context, choice)), - selected: trainingPrefs.sideChoice == choice, - showCheckmark: false, - onSelected: (selected) { - widget.onSideChoiceSelected(choice); - ref - .read(coordinateTrainingPreferencesProvider.notifier) - .setSideChoice(choice); - }, - ); - }).toList(), - ), + return Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + PlatformListTile( + title: Text(context.l10n.side), + trailing: Padding( + padding: Styles.horizontalBodyPadding, + child: Wrap( + spacing: 8.0, + children: SideChoice.values.map((choice) { + return ChoiceChip( + label: Text(sideChoiceL10n(context, choice)), + selected: trainingPrefs.sideChoice == choice, + showCheckmark: false, + onSelected: (selected) { + widget.onSideChoiceSelected(choice); + ref + .read(coordinateTrainingPreferencesProvider.notifier) + .setSideChoice(choice); + }, + ); + }).toList(), ), ), - PlatformListTile( - title: Text(context.l10n.time), - trailing: Padding( - padding: Styles.horizontalBodyPadding, - child: Wrap( - spacing: 8.0, - children: TimeChoice.values.map((choice) { - return ChoiceChip( - label: timeChoiceL10n(context, choice), - selected: trainingPrefs.timeChoice == choice, - showCheckmark: false, - onSelected: (selected) { - if (selected) { - ref - .read( - coordinateTrainingPreferencesProvider.notifier, - ) - .setTimeChoice(choice); - } - }, - ); - }).toList(), - ), + ), + PlatformListTile( + title: Text(context.l10n.time), + trailing: Padding( + padding: Styles.horizontalBodyPadding, + child: Wrap( + spacing: 8.0, + children: TimeChoice.values.map((choice) { + return ChoiceChip( + label: timeChoiceL10n(context, choice), + selected: trainingPrefs.timeChoice == choice, + showCheckmark: false, + onSelected: (selected) { + if (selected) { + ref + .read( + coordinateTrainingPreferencesProvider.notifier, + ) + .setTimeChoice(choice); + } + }, + ); + }).toList(), ), ), - FatButton( - semanticsLabel: 'Start Training', - onPressed: () => ref - .read(coordinateTrainingControllerProvider.notifier) - .startTraining(trainingPrefs.timeChoice.duration), - child: const Text( - // TODO l10n once script works - 'Start Training', - style: Styles.bold, - ), + ), + FatButton( + semanticsLabel: 'Start Training', + onPressed: () => ref + .read(coordinateTrainingControllerProvider.notifier) + .startTraining(trainingPrefs.timeChoice.duration), + child: const Text( + // TODO l10n once script works + 'Start Training', + style: Styles.bold, ), - ], - ), + ), + ], ); } } @@ -561,3 +502,63 @@ class _TrainingBoardState extends ConsumerState<_TrainingBoard> { ); } } + +Future _coordinateTrainingInfoDialogBuilder(BuildContext context) { + return showAdaptiveDialog( + context: context, + builder: (context) { + final content = SingleChildScrollView( + child: RichText( + text: TextSpan( + style: DefaultTextStyle.of(context).style, + // TODO translate + children: const [ + TextSpan( + text: + 'Knowing the chessboard coordinates is a very important skill for several reasons:\n', + ), + TextSpan( + text: + ' • Most chess courses and exercises use the algebraic notation extensively.\n', + ), + TextSpan( + text: + " • It makes it easier to talk to your chess friends, since you both understand the 'language of chess'.\n", + ), + TextSpan( + text: + ' • You can analyse a game more effectively if you can quickly recognise coordinates.\n', + ), + TextSpan( + text: '\n', + ), + TextSpan( + text: 'Find Square\n', + style: TextStyle(fontWeight: FontWeight.bold), + ), + TextSpan( + text: + 'A coordinate appears on the board and you must click on the corresponding square.\n', + ), + TextSpan( + text: + 'You have 30 seconds to correctly map as many squares as possible!\n', + ), + ], + ), + ), + ); + + return PlatformAlertDialog( + title: Text(context.l10n.aboutX('Coordinate Training')), + content: content, + actions: [ + PlatformDialogAction( + onPressed: () => Navigator.of(context).pop(), + child: Text(context.l10n.mobileOkButton), + ), + ], + ); + }, + ); +} From 78fba9d1dd2dc191add7099465b2cae2efe082c3 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 11 Sep 2024 08:30:30 +0200 Subject: [PATCH 318/979] Show disabled offline buttons in tools tab --- lib/src/view/tools/tools_tab_screen.dart | 72 +++++++++++++----------- 1 file changed, 38 insertions(+), 34 deletions(-) diff --git a/lib/src/view/tools/tools_tab_screen.dart b/lib/src/view/tools/tools_tab_screen.dart index 82ce96f08c..00d3de7b39 100644 --- a/lib/src/view/tools/tools_tab_screen.dart +++ b/lib/src/view/tools/tools_tab_screen.dart @@ -82,7 +82,7 @@ class _ToolsButton extends StatelessWidget { final String title; - final VoidCallback onTap; + final VoidCallback? onTap; @override Widget build(BuildContext context) { @@ -94,22 +94,25 @@ class _ToolsButton extends StatelessWidget { padding: Theme.of(context).platform == TargetPlatform.android ? const EdgeInsets.only(bottom: 16.0) : EdgeInsets.zero, - child: PlatformListTile( - leading: Icon( - icon, - size: Styles.mainListTileIconSize, - color: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoTheme.of(context).primaryColor - : Theme.of(context).colorScheme.primary, - ), - title: Padding( - padding: tilePadding, - child: Text(title, style: Styles.callout), + child: Opacity( + opacity: onTap == null ? 0.5 : 1.0, + child: PlatformListTile( + leading: Icon( + icon, + size: Styles.mainListTileIconSize, + color: Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoTheme.of(context).primaryColor + : Theme.of(context).colorScheme.primary, + ), + title: Padding( + padding: tilePadding, + child: Text(title, style: Styles.callout), + ), + trailing: Theme.of(context).platform == TargetPlatform.iOS + ? const CupertinoListTileChevron() + : null, + onTap: onTap, ), - trailing: Theme.of(context).platform == TargetPlatform.iOS - ? const CupertinoListTileChevron() - : null, - onTap: onTap, ), ); } @@ -153,24 +156,25 @@ class _Body extends ConsumerWidget { ), ), ), - if (isOnline) - _ToolsButton( - icon: Icons.explore, - title: context.l10n.openingExplorer, - onTap: () => pushPlatformRoute( - context, - rootNavigator: true, - builder: (context) => const OpeningExplorerScreen( - pgn: '', - options: AnalysisOptions( - isLocalEvaluationAllowed: false, - variant: Variant.standard, - orientation: Side.white, - id: standaloneOpeningExplorerId, - ), - ), - ), - ), + _ToolsButton( + icon: Icons.explore, + title: context.l10n.openingExplorer, + onTap: isOnline + ? () => pushPlatformRoute( + context, + rootNavigator: true, + builder: (context) => const OpeningExplorerScreen( + pgn: '', + options: AnalysisOptions( + isLocalEvaluationAllowed: false, + variant: Variant.standard, + orientation: Side.white, + id: standaloneOpeningExplorerId, + ), + ), + ) + : null, + ), _ToolsButton( icon: Icons.edit, title: context.l10n.boardEditor, From 6ebd3177258f9e5e7a1428aeb90d08399a5a2536 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 11 Sep 2024 08:39:46 +0200 Subject: [PATCH 319/979] Change coordinate icon --- lib/src/view/tools/tools_tab_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/view/tools/tools_tab_screen.dart b/lib/src/view/tools/tools_tab_screen.dart index 00d3de7b39..6b0cffa4d7 100644 --- a/lib/src/view/tools/tools_tab_screen.dart +++ b/lib/src/view/tools/tools_tab_screen.dart @@ -185,7 +185,7 @@ class _Body extends ConsumerWidget { ), ), _ToolsButton( - icon: LichessIcons.chess_board, + icon: Icons.where_to_vote_outlined, title: 'Coordinate Training', // TODO l10n onTap: () => pushPlatformRoute( context, From b00667f799ad3f8a84c9a75e0a0030cb013f41b3 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 11 Sep 2024 09:30:43 +0200 Subject: [PATCH 320/979] Refactor modal bottom sheet container --- lib/src/view/analysis/analysis_settings.dart | 4 +- lib/src/view/analysis/tree_view.dart | 222 ++++++------ .../view/board_editor/board_editor_menu.dart | 159 ++++----- lib/src/view/clock/custom_clock_settings.dart | 46 +-- .../coordinate_training_screen.dart | 57 ++- lib/src/view/game/game_common_widgets.dart | 3 +- lib/src/view/game/game_list_tile.dart | 3 +- lib/src/view/game/game_settings.dart | 5 +- .../opening_explorer_settings.dart | 4 +- lib/src/view/play/online_bots_screen.dart | 3 +- lib/src/view/play/time_control_modal.dart | 328 +++++++++--------- .../view/puzzle/puzzle_settings_screen.dart | 4 +- lib/src/view/user/game_history_screen.dart | 71 ++-- lib/src/widgets/adaptive_bottom_sheet.dart | 28 ++ 14 files changed, 456 insertions(+), 481 deletions(-) diff --git a/lib/src/view/analysis/analysis_settings.dart b/lib/src/view/analysis/analysis_settings.dart index d2efbd7dd5..dac59b62f7 100644 --- a/lib/src/view/analysis/analysis_settings.dart +++ b/lib/src/view/analysis/analysis_settings.dart @@ -6,6 +6,7 @@ import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/non_linear_slider.dart'; import 'package:lichess_mobile/src/widgets/settings.dart'; @@ -29,8 +30,7 @@ class AnalysisSettings extends ConsumerWidget { generalPreferencesProvider.select((pref) => pref.isSoundEnabled), ); - return ListView( - shrinkWrap: true, + return BottomSheetScrollableContainer( children: [ SwitchSettingTile( title: Text(context.l10n.toggleLocalEvaluation), diff --git a/lib/src/view/analysis/tree_view.dart b/lib/src/view/analysis/tree_view.dart index d2b502d365..53d2f27995 100644 --- a/lib/src/view/analysis/tree_view.dart +++ b/lib/src/view/analysis/tree_view.dart @@ -488,126 +488,121 @@ class _MoveContextMenu extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final ctrlProvider = analysisControllerProvider(pgn, options); - return SafeArea( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: - const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - title, - style: Theme.of(context).textTheme.titleLarge, - ), - if (branch.clock != null) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ + return BottomSheetScrollableContainer( + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + title, + style: Theme.of(context).textTheme.titleLarge, + ), + if (branch.clock != null) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.punch_clock, + ), + const SizedBox(width: 4.0), + Text( + branch.clock!.toHoursMinutesSeconds( + showTenths: + branch.clock! < const Duration(minutes: 1), + ), + ), + ], + ), + if (branch.elapsedMoveTime != null) ...[ + const SizedBox(height: 4.0), Row( mainAxisSize: MainAxisSize.min, children: [ const Icon( - Icons.punch_clock, + Icons.hourglass_bottom, ), const SizedBox(width: 4.0), Text( - branch.clock!.toHoursMinutesSeconds( - showTenths: - branch.clock! < const Duration(minutes: 1), - ), + branch.elapsedMoveTime! + .toHoursMinutesSeconds(showTenths: true), ), ], ), - if (branch.elapsedMoveTime != null) ...[ - const SizedBox(height: 4.0), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon( - Icons.hourglass_bottom, - ), - const SizedBox(width: 4.0), - Text( - branch.elapsedMoveTime! - .toHoursMinutesSeconds(showTenths: true), - ), - ], - ), - ], ], - ), - ], - ), + ], + ), + ], ), - if (branch.hasLichessAnalysisTextComment) - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 8.0, - ), - child: Text( - branch.lichessAnalysisComments! - .map((c) => c.text ?? '') - .join(' '), - ), - ), - if (branch.hasTextComment) - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 8.0, - ), - child: Text( - branch.comments!.map((c) => c.text ?? '').join(' '), - ), - ), - const PlatformDivider(indent: 0), - if (parent.children.any((c) => c.isHidden)) - BottomSheetContextMenuAction( - icon: Icons.subtitles, - child: Text(context.l10n.mobileShowVariations), - onPressed: () { - ref.read(ctrlProvider.notifier).showAllVariations(path); - }, + ), + if (branch.hasLichessAnalysisTextComment) + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 8.0, ), - if (isSideline) - BottomSheetContextMenuAction( - icon: Icons.subtitles_off, - child: Text(context.l10n.mobileHideVariation), - onPressed: () { - ref.read(ctrlProvider.notifier).hideVariation(path); - }, + child: Text( + branch.lichessAnalysisComments! + .map((c) => c.text ?? '') + .join(' '), ), - if (isSideline) - BottomSheetContextMenuAction( - icon: Icons.expand_less, - child: Text(context.l10n.promoteVariation), - onPressed: () { - ref.read(ctrlProvider.notifier).promoteVariation(path, false); - }, + ), + if (branch.hasTextComment) + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 8.0, ), - if (isSideline) - BottomSheetContextMenuAction( - icon: Icons.check, - child: Text(context.l10n.makeMainLine), - onPressed: () { - ref.read(ctrlProvider.notifier).promoteVariation(path, true); - }, + child: Text( + branch.comments!.map((c) => c.text ?? '').join(' '), ), + ), + const PlatformDivider(indent: 0), + if (parent.children.any((c) => c.isHidden)) BottomSheetContextMenuAction( - icon: Icons.delete, - child: Text(context.l10n.deleteFromHere), + icon: Icons.subtitles, + child: Text(context.l10n.mobileShowVariations), onPressed: () { - ref.read(ctrlProvider.notifier).deleteFromHere(path); + ref.read(ctrlProvider.notifier).showAllVariations(path); }, ), - const SizedBox(height: 8.0), - ], - ), + if (isSideline) + BottomSheetContextMenuAction( + icon: Icons.subtitles_off, + child: Text(context.l10n.mobileHideVariation), + onPressed: () { + ref.read(ctrlProvider.notifier).hideVariation(path); + }, + ), + if (isSideline) + BottomSheetContextMenuAction( + icon: Icons.expand_less, + child: Text(context.l10n.promoteVariation), + onPressed: () { + ref.read(ctrlProvider.notifier).promoteVariation(path, false); + }, + ), + if (isSideline) + BottomSheetContextMenuAction( + icon: Icons.check, + child: Text(context.l10n.makeMainLine), + onPressed: () { + ref.read(ctrlProvider.notifier).promoteVariation(path, true); + }, + ), + BottomSheetContextMenuAction( + icon: Icons.delete, + child: Text(context.l10n.deleteFromHere), + onPressed: () { + ref.read(ctrlProvider.notifier).deleteFromHere(path); + }, + ), + ], ); } } @@ -628,25 +623,22 @@ class _Comments extends StatelessWidget { isDismissible: true, showDragHandle: true, isScrollControlled: true, - builder: (context) => Padding( + builder: (context) => BottomSheetScrollableContainer( padding: const EdgeInsets.symmetric( vertical: 8.0, horizontal: 16.0, ), - child: ListView( - shrinkWrap: true, - children: comments.map( - (comment) { - if (comment.text == null || comment.text!.isEmpty) { - return const SizedBox.shrink(); - } - return Padding( - padding: const EdgeInsets.only(bottom: 16.0), - child: Text(comment.text!.replaceAll('\n', ' ')), - ); - }, - ).toList(), - ), + children: comments.map( + (comment) { + if (comment.text == null || comment.text!.isEmpty) { + return const SizedBox.shrink(); + } + return Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: Text(comment.text!.replaceAll('\n', ' ')), + ); + }, + ).toList(), ), ); }, diff --git a/lib/src/view/board_editor/board_editor_menu.dart b/lib/src/view/board_editor/board_editor_menu.dart index 65e4e045b8..e0f0f870df 100644 --- a/lib/src/view/board_editor/board_editor_menu.dart +++ b/lib/src/view/board_editor/board_editor_menu.dart @@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/board_editor/board_editor_controller.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; class BoardEditorMenu extends ConsumerWidget { const BoardEditorMenu({required this.initialFen, super.key}); @@ -15,95 +16,83 @@ class BoardEditorMenu extends ConsumerWidget { final editorController = boardEditorControllerProvider(initialFen); final editorState = ref.watch(editorController); - return SafeArea( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 8.0), - child: SizedBox( - width: double.infinity, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Padding( - padding: Styles.horizontalBodyPadding, - child: Wrap( - spacing: 8.0, - children: Side.values.map((side) { - return ChoiceChip( - label: Text( - side == Side.white - ? context.l10n.whitePlays - : context.l10n.blackPlays, - ), - selected: editorState.sideToPlay == side, - onSelected: (selected) { - if (selected) { - ref - .read(editorController.notifier) - .setSideToPlay(side); - } - }, - ); - }).toList(), + return BottomSheetScrollableContainer( + padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 8.0), + children: [ + Padding( + padding: Styles.horizontalBodyPadding, + child: Wrap( + spacing: 8.0, + children: Side.values.map((side) { + return ChoiceChip( + label: Text( + side == Side.white + ? context.l10n.whitePlays + : context.l10n.blackPlays, ), - ), - Padding( - padding: Styles.bodySectionPadding, - child: Text(context.l10n.castling, style: Styles.subtitle), - ), - ...Side.values.map((side) { - return Padding( - padding: Styles.horizontalBodyPadding, - child: Wrap( - spacing: 8.0, - children: [CastlingSide.king, CastlingSide.queen] - .map((castlingSide) { - return ChoiceChip( - label: Text( - castlingSide == CastlingSide.king - ? side == Side.white - ? context.l10n.whiteCastlingKingside - : context.l10n.blackCastlingKingside - : 'O-O-O', - ), - selected: - editorState.isCastlingAllowed(side, castlingSide), - onSelected: (selected) { - ref.read(editorController.notifier).setCastling( - side, - castlingSide, - selected, - ); - }, - ); - }).toList(), + selected: editorState.sideToPlay == side, + onSelected: (selected) { + if (selected) { + ref.read(editorController.notifier).setSideToPlay(side); + } + }, + ); + }).toList(), + ), + ), + Padding( + padding: Styles.bodySectionPadding, + child: Text(context.l10n.castling, style: Styles.subtitle), + ), + ...Side.values.map((side) { + return Padding( + padding: Styles.horizontalBodyPadding, + child: Wrap( + spacing: 8.0, + children: + [CastlingSide.king, CastlingSide.queen].map((castlingSide) { + return ChoiceChip( + label: Text( + castlingSide == CastlingSide.king + ? side == Side.white + ? context.l10n.whiteCastlingKingside + : context.l10n.blackCastlingKingside + : 'O-O-O', ), + selected: editorState.isCastlingAllowed(side, castlingSide), + onSelected: (selected) { + ref.read(editorController.notifier).setCastling( + side, + castlingSide, + selected, + ); + }, ); - }), - if (editorState.enPassantOptions.isNotEmpty) ...[ - Padding( - padding: Styles.bodySectionPadding, - child: const Text('En passant', style: Styles.subtitle), - ), - Wrap( - spacing: 8.0, - children: editorState.enPassantOptions.squares.map((square) { - return ChoiceChip( - label: Text(square.name), - selected: editorState.enPassantSquare == square, - onSelected: (selected) { - ref - .read(editorController.notifier) - .toggleEnPassantSquare(square); - }, - ); - }).toList(), - ), - ], - ], + }).toList(), + ), + ); + }), + if (editorState.enPassantOptions.isNotEmpty) ...[ + Padding( + padding: Styles.bodySectionPadding, + child: const Text('En passant', style: Styles.subtitle), ), - ), - ), + Wrap( + spacing: 8.0, + children: editorState.enPassantOptions.squares.map((square) { + return ChoiceChip( + label: Text(square.name), + selected: editorState.enPassantSquare == square, + onSelected: (selected) { + ref + .read(editorController.notifier) + .toggleEnPassantSquare(square); + }, + ); + }).toList(), + ), + ], + ], ); } } diff --git a/lib/src/view/clock/custom_clock_settings.dart b/lib/src/view/clock/custom_clock_settings.dart index d15cb8a6fb..58563a18d8 100644 --- a/lib/src/view/clock/custom_clock_settings.dart +++ b/lib/src/view/clock/custom_clock_settings.dart @@ -4,6 +4,7 @@ import 'package:lichess_mobile/src/model/common/time_increment.dart'; import 'package:lichess_mobile/src/model/lobby/game_setup.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/non_linear_slider.dart'; @@ -36,35 +37,26 @@ class _CustomClockSettingsState extends State { @override Widget build(BuildContext context) { - return SafeArea( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 8.0), - child: SizedBox( - width: double.infinity, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - _PlayerTimeSlider( - clock: TimeIncrement(time, increment), - updateTime: (int t) => setState(() => time = t), - updateIncrement: (int inc) => setState(() => increment = inc), - ), - Padding( - padding: Styles.horizontalBodyPadding, - child: FatButton( - semanticsLabel: context.l10n.apply, - child: Text(context.l10n.apply), - onPressed: () => widget.onSubmit( - widget.player, - TimeIncrement(time, increment), - ), - ), - ), - ], + return BottomSheetScrollableContainer( + padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 8.0), + children: [ + _PlayerTimeSlider( + clock: TimeIncrement(time, increment), + updateTime: (int t) => setState(() => time = t), + updateIncrement: (int inc) => setState(() => increment = inc), + ), + Padding( + padding: Styles.horizontalBodyPadding, + child: FatButton( + semanticsLabel: context.l10n.apply, + child: Text(context.l10n.apply), + onPressed: () => widget.onSubmit( + widget.player, + TimeIncrement(time, increment), + ), ), ), - ), + ], ); } } diff --git a/lib/src/view/coordinate_training/coordinate_training_screen.dart b/lib/src/view/coordinate_training/coordinate_training_screen.dart index 20fdad1689..47663917a7 100644 --- a/lib/src/view/coordinate_training/coordinate_training_screen.dart +++ b/lib/src/view/coordinate_training/coordinate_training_screen.dart @@ -265,41 +265,32 @@ class _CoordinateTrainingMenu extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final trainingPrefs = ref.watch(coordinateTrainingPreferencesProvider); - return SafeArea( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 8.0), - child: SizedBox( - width: double.infinity, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - ListSection( - header: Text( - context.l10n.preferencesDisplay, - style: Styles.sectionTitle, - ), - children: [ - SwitchSettingTile( - title: const Text('Show Coordinates'), - value: trainingPrefs.showCoordinates, - onChanged: ref - .read(coordinateTrainingPreferencesProvider.notifier) - .setShowCoordinates, - ), - SwitchSettingTile( - title: const Text('Show Pieces'), - value: trainingPrefs.showPieces, - onChanged: ref - .read(coordinateTrainingPreferencesProvider.notifier) - .setShowPieces, - ), - ], - ), - ], + return BottomSheetScrollableContainer( + padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 8.0), + children: [ + ListSection( + header: Text( + context.l10n.preferencesDisplay, + style: Styles.sectionTitle, ), + children: [ + SwitchSettingTile( + title: const Text('Show Coordinates'), + value: trainingPrefs.showCoordinates, + onChanged: ref + .read(coordinateTrainingPreferencesProvider.notifier) + .setShowCoordinates, + ), + SwitchSettingTile( + title: const Text('Show Pieces'), + value: trainingPrefs.showPieces, + onChanged: ref + .read(coordinateTrainingPreferencesProvider.notifier) + .setShowPieces, + ), + ], ), - ), + ], ); } } diff --git a/lib/src/view/game/game_common_widgets.dart b/lib/src/view/game/game_common_widgets.dart index db69af84c4..d68a17fb7f 100644 --- a/lib/src/view/game/game_common_widgets.dart +++ b/lib/src/view/game/game_common_widgets.dart @@ -128,8 +128,7 @@ class GameShareBottomSheet extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return ListView( - shrinkWrap: true, + return BottomSheetScrollableContainer( children: [ BottomSheetContextMenuAction( icon: CupertinoIcons.link, diff --git a/lib/src/view/game/game_list_tile.dart b/lib/src/view/game/game_list_tile.dart index 5b5631dc59..1c1213c1b0 100644 --- a/lib/src/view/game/game_list_tile.dart +++ b/lib/src/view/game/game_list_tile.dart @@ -111,8 +111,7 @@ class _ContextMenu extends ConsumerWidget { final customColors = Theme.of(context).extension(); - return ListView( - shrinkWrap: true, + return BottomSheetScrollableContainer( children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0).add( diff --git a/lib/src/view/game/game_settings.dart b/lib/src/view/game/game_settings.dart index 9a78f9ee23..3785e3333a 100644 --- a/lib/src/view/game/game_settings.dart +++ b/lib/src/view/game/game_settings.dart @@ -8,6 +8,7 @@ import 'package:lichess_mobile/src/model/game/game_preferences.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/settings.dart'; import 'game_screen_providers.dart'; @@ -28,8 +29,7 @@ class GameSettings extends ConsumerWidget { final gamePrefs = ref.watch(gamePreferencesProvider); final userPrefsAsync = ref.watch(userGamePrefsProvider(id)); - return ListView( - shrinkWrap: true, + return BottomSheetScrollableContainer( children: [ SwitchSettingTile( title: Text(context.l10n.sound), @@ -137,7 +137,6 @@ class GameSettings extends ConsumerWidget { ref.read(gamePreferencesProvider.notifier).toggleBlindfoldMode(); }, ), - const SizedBox(height: 16.0), ], ); } diff --git a/lib/src/view/opening_explorer/opening_explorer_settings.dart b/lib/src/view/opening_explorer/opening_explorer_settings.dart index d18d0c2bb7..bd2d5b30f2 100644 --- a/lib/src/view/opening_explorer/opening_explorer_settings.dart +++ b/lib/src/view/opening_explorer/opening_explorer_settings.dart @@ -10,6 +10,7 @@ import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_prefe import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/user/search_screen.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; class OpeningExplorerSettings extends ConsumerWidget { @@ -237,8 +238,7 @@ class OpeningExplorerSettings extends ConsumerWidget { ), ]; - return ListView( - shrinkWrap: true, + return BottomSheetScrollableContainer( children: [ PlatformListTile( title: Text(context.l10n.database), diff --git a/lib/src/view/play/online_bots_screen.dart b/lib/src/view/play/online_bots_screen.dart index 190b13839d..bd5e26e427 100644 --- a/lib/src/view/play/online_bots_screen.dart +++ b/lib/src/view/play/online_bots_screen.dart @@ -195,8 +195,7 @@ class _ContextMenu extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return ListView( - shrinkWrap: true, + return BottomSheetScrollableContainer( children: [ Padding( padding: Styles.horizontalBodyPadding.add( diff --git a/lib/src/view/play/time_control_modal.dart b/lib/src/view/play/time_control_modal.dart index 6384a70b78..db5c677ea1 100644 --- a/lib/src/view/play/time_control_modal.dart +++ b/lib/src/view/play/time_control_modal.dart @@ -7,6 +7,7 @@ import 'package:lichess_mobile/src/model/lobby/game_setup.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/non_linear_slider.dart'; @@ -30,184 +31,177 @@ class TimeControlModal extends ConsumerWidget { this.onSelected(choice); } - return SafeArea( - child: Padding( - padding: Styles.bodyPadding, - child: ListView( - shrinkWrap: true, - children: [ - Text( - context.l10n.timeControl, - style: Styles.title, - ), - const SizedBox(height: 26.0), - _SectionChoices( - value, - choices: [ - if (!excludeUltraBullet) const TimeIncrement(0, 1), - const TimeIncrement(60, 0), - const TimeIncrement(60, 1), - const TimeIncrement(120, 1), - ], - title: const _SectionTitle( - title: 'Bullet', - icon: LichessIcons.bullet, - ), - onSelected: onSelected, - ), - const SizedBox(height: 20.0), - _SectionChoices( - value, - choices: const [ - TimeIncrement(180, 0), - TimeIncrement(180, 2), - TimeIncrement(300, 0), - TimeIncrement(300, 3), - ], - title: const _SectionTitle( - title: 'Blitz', - icon: LichessIcons.blitz, - ), - onSelected: onSelected, - ), - const SizedBox(height: 20.0), - _SectionChoices( - value, - choices: const [ - TimeIncrement(600, 0), - TimeIncrement(600, 5), - TimeIncrement(900, 0), - TimeIncrement(900, 10), - ], - title: const _SectionTitle( - title: 'Rapid', - icon: LichessIcons.rapid, - ), - onSelected: onSelected, - ), - const SizedBox(height: 20.0), - _SectionChoices( - value, - choices: const [ - TimeIncrement(1500, 0), - TimeIncrement(1800, 0), - TimeIncrement(1800, 20), - TimeIncrement(3600, 0), - ], - title: const _SectionTitle( - title: 'Classical', - icon: LichessIcons.classical, - ), - onSelected: onSelected, + return BottomSheetScrollableContainer( + padding: Styles.bodyPadding, + children: [ + Text( + context.l10n.timeControl, + style: Styles.title, + ), + const SizedBox(height: 26.0), + _SectionChoices( + value, + choices: [ + if (!excludeUltraBullet) const TimeIncrement(0, 1), + const TimeIncrement(60, 0), + const TimeIncrement(60, 1), + const TimeIncrement(120, 1), + ], + title: const _SectionTitle( + title: 'Bullet', + icon: LichessIcons.bullet, + ), + onSelected: onSelected, + ), + const SizedBox(height: 20.0), + _SectionChoices( + value, + choices: const [ + TimeIncrement(180, 0), + TimeIncrement(180, 2), + TimeIncrement(300, 0), + TimeIncrement(300, 3), + ], + title: const _SectionTitle( + title: 'Blitz', + icon: LichessIcons.blitz, + ), + onSelected: onSelected, + ), + const SizedBox(height: 20.0), + _SectionChoices( + value, + choices: const [ + TimeIncrement(600, 0), + TimeIncrement(600, 5), + TimeIncrement(900, 0), + TimeIncrement(900, 10), + ], + title: const _SectionTitle( + title: 'Rapid', + icon: LichessIcons.rapid, + ), + onSelected: onSelected, + ), + const SizedBox(height: 20.0), + _SectionChoices( + value, + choices: const [ + TimeIncrement(1500, 0), + TimeIncrement(1800, 0), + TimeIncrement(1800, 20), + TimeIncrement(3600, 0), + ], + title: const _SectionTitle( + title: 'Classical', + icon: LichessIcons.classical, + ), + onSelected: onSelected, + ), + const SizedBox(height: 20.0), + Theme( + data: Theme.of(context).copyWith(dividerColor: Colors.transparent), + child: ExpansionTile( + title: _SectionTitle( + title: context.l10n.custom, + icon: Icons.tune, ), - const SizedBox(height: 20.0), - Theme( - data: - Theme.of(context).copyWith(dividerColor: Colors.transparent), - child: ExpansionTile( - title: _SectionTitle( - title: context.l10n.custom, - icon: Icons.tune, - ), - tilePadding: EdgeInsets.zero, - minTileHeight: 0, - children: [ - Builder( - builder: (context) { - TimeIncrement custom = value; - return StatefulBuilder( - builder: (context, setState) { - return Column( + tilePadding: EdgeInsets.zero, + minTileHeight: 0, + children: [ + Builder( + builder: (context) { + TimeIncrement custom = value; + return StatefulBuilder( + builder: (context, setState) { + return Column( + children: [ + Row( + mainAxisSize: MainAxisSize.max, children: [ - Row( - mainAxisSize: MainAxisSize.max, - children: [ - Expanded( - child: NonLinearSlider( - value: custom.time, - values: kAvailableTimesInSeconds, - labelBuilder: clockLabelInMinutes, - onChange: Theme.of(context).platform == - TargetPlatform.iOS - ? (num value) { - setState(() { - custom = TimeIncrement( - value.toInt(), - custom.increment, - ); - }); - } - : null, - onChangeEnd: (num value) { - setState(() { - custom = TimeIncrement( - value.toInt(), - custom.increment, - ); - }); - }, - ), - ), - SizedBox( - width: 80, - child: Center( - child: Text( - custom.display, - style: Styles.timeControl - .merge(Styles.bold), - ), - ), - ), - Expanded( - child: NonLinearSlider( - value: custom.increment, - values: kAvailableIncrementsInSeconds, - onChange: Theme.of(context).platform == - TargetPlatform.iOS - ? (num value) { - setState(() { - custom = TimeIncrement( - custom.time, - value.toInt(), - ); - }); - } - : null, - onChangeEnd: (num value) { - setState(() { - custom = TimeIncrement( - custom.time, - value.toInt(), - ); - }); - }, - ), + Expanded( + child: NonLinearSlider( + value: custom.time, + values: kAvailableTimesInSeconds, + labelBuilder: clockLabelInMinutes, + onChange: Theme.of(context).platform == + TargetPlatform.iOS + ? (num value) { + setState(() { + custom = TimeIncrement( + value.toInt(), + custom.increment, + ); + }); + } + : null, + onChangeEnd: (num value) { + setState(() { + custom = TimeIncrement( + value.toInt(), + custom.increment, + ); + }); + }, + ), + ), + SizedBox( + width: 80, + child: Center( + child: Text( + custom.display, + style: + Styles.timeControl.merge(Styles.bold), ), - ], + ), ), - SecondaryButton( - onPressed: custom.isInfinite - ? null - : () => onSelected(custom), - semanticsLabel: 'OK', - child: Text( - context.l10n.mobileOkButton, - style: Styles.bold, + Expanded( + child: NonLinearSlider( + value: custom.increment, + values: kAvailableIncrementsInSeconds, + onChange: Theme.of(context).platform == + TargetPlatform.iOS + ? (num value) { + setState(() { + custom = TimeIncrement( + custom.time, + value.toInt(), + ); + }); + } + : null, + onChangeEnd: (num value) { + setState(() { + custom = TimeIncrement( + custom.time, + value.toInt(), + ); + }); + }, ), ), ], - ); - }, + ), + SecondaryButton( + onPressed: custom.isInfinite + ? null + : () => onSelected(custom), + semanticsLabel: 'OK', + child: Text( + context.l10n.mobileOkButton, + style: Styles.bold, + ), + ), + ], ); }, - ), - ], + ); + }, ), - ), - const SizedBox(height: 40.0), - ], + ], + ), ), - ), + ], ); } } diff --git a/lib/src/view/puzzle/puzzle_settings_screen.dart b/lib/src/view/puzzle/puzzle_settings_screen.dart index 81ff3b922f..bcb2e0f28b 100644 --- a/lib/src/view/puzzle/puzzle_settings_screen.dart +++ b/lib/src/view/puzzle/puzzle_settings_screen.dart @@ -6,6 +6,7 @@ import 'package:lichess_mobile/src/model/puzzle/puzzle_preferences.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/settings.dart'; class PuzzleSettingsScreen extends ConsumerWidget { @@ -23,8 +24,7 @@ class PuzzleSettingsScreen extends ConsumerWidget { ); final boardPrefs = ref.watch(boardPreferencesProvider); - return ListView( - shrinkWrap: true, + return BottomSheetScrollableContainer( children: [ SwitchSettingTile( title: Text(context.l10n.sound), diff --git a/lib/src/view/user/game_history_screen.dart b/lib/src/view/user/game_history_screen.dart index b67232b106..0233b8fe7c 100644 --- a/lib/src/view/user/game_history_screen.dart +++ b/lib/src/view/user/game_history_screen.dart @@ -280,50 +280,43 @@ class _FilterGamesState extends ConsumerState<_FilterGames> { ) : perfFilter(gamePerfs); - return SafeArea( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, + return BottomSheetScrollableContainer( + padding: const EdgeInsets.all(16.0), + children: [ + filters, + const SizedBox(height: 12.0), + const PlatformDivider(thickness: 1, indent: 0), + filterGroupSpace, + _Filter( + filterName: context.l10n.side, + filterType: FilterType.singleChoice, + choices: Side.values, + choiceSelected: (choice) => filter.side == choice, + choiceLabel: (t) => switch (t) { + Side.white => context.l10n.white, + Side.black => context.l10n.black, + }, + onSelected: (value, selected) => setState( + () { + filter = filter.copyWith(side: selected ? value : null); + }, + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.end, children: [ - filters, - const SizedBox(height: 12.0), - const PlatformDivider(thickness: 1, indent: 0), - filterGroupSpace, - _Filter( - filterName: context.l10n.side, - filterType: FilterType.singleChoice, - choices: Side.values, - choiceSelected: (choice) => filter.side == choice, - choiceLabel: (t) => switch (t) { - Side.white => context.l10n.white, - Side.black => context.l10n.black, - }, - onSelected: (value, selected) => setState( - () { - filter = filter.copyWith(side: selected ? value : null); - }, - ), + AdaptiveTextButton( + onPressed: () => setState(() => filter = const GameFilterState()), + child: Text(context.l10n.reset), ), - Row( - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - AdaptiveTextButton( - onPressed: () => - setState(() => filter = const GameFilterState()), - child: Text(context.l10n.reset), - ), - AdaptiveTextButton( - onPressed: () => Navigator.of(context).pop(filter), - child: Text(context.l10n.apply), - ), - ], + AdaptiveTextButton( + onPressed: () => Navigator.of(context).pop(filter), + child: Text(context.l10n.apply), ), ], ), - ), + ], ); } diff --git a/lib/src/widgets/adaptive_bottom_sheet.dart b/lib/src/widgets/adaptive_bottom_sheet.dart index 072eba48d7..b4ec9d7686 100644 --- a/lib/src/widgets/adaptive_bottom_sheet.dart +++ b/lib/src/widgets/adaptive_bottom_sheet.dart @@ -40,6 +40,34 @@ Future showAdaptiveBottomSheet({ ); } +/// A modal bottom sheet container that adapts to the content size. +/// +/// This is typically used with [showAdaptiveBottomSheet] to display a +/// context menu. +/// +/// This is meant for content that mostly fits on the screen, not for long lists. +class BottomSheetScrollableContainer extends StatelessWidget { + const BottomSheetScrollableContainer({ + required this.children, + this.padding = const EdgeInsets.only(bottom: 8.0), + }); + + final List children; + final EdgeInsetsGeometry? padding; + + @override + Widget build(BuildContext context) { + return SafeArea( + child: SingleChildScrollView( + padding: padding, + child: ListBody( + children: children, + ), + ), + ); + } +} + class BottomSheetContextMenuAction extends StatelessWidget { const BottomSheetContextMenuAction({ required this.child, From 375a8bfee661a8b6b7c141f68f4b10bda4b56c3f Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 11 Sep 2024 10:14:20 +0200 Subject: [PATCH 321/979] Fix clock dart theme style --- lib/src/view/clock/clock_tile.dart | 74 ++++++++++++++---------- lib/src/view/tools/tools_tab_screen.dart | 1 - lib/src/widgets/countdown_clock.dart | 30 +++++----- 3 files changed, 56 insertions(+), 49 deletions(-) diff --git a/lib/src/view/clock/clock_tile.dart b/lib/src/view/clock/clock_tile.dart index 253bcd2377..7d4c087dc1 100644 --- a/lib/src/view/clock/clock_tile.dart +++ b/lib/src/view/clock/clock_tile.dart @@ -51,6 +51,7 @@ class ClockTile extends ConsumerWidget { return RotatedBox( quarterTurns: playerType == ClockPlayerType.top ? 2 : 0, child: Stack( + alignment: Alignment.center, fit: StackFit.expand, children: [ Opacity( @@ -108,37 +109,6 @@ class ClockTile extends ConsumerWidget { : CrossFadeState.showFirst, ), ), - if (!clockState.started) - PlatformIconButton( - semanticsLabel: context.l10n.settingsSettings, - iconSize: 32, - icon: Icons.tune, - onTap: () => showAdaptiveBottomSheet( - context: context, - builder: (BuildContext context) => - CustomClockSettings( - player: playerType, - clock: playerType == ClockPlayerType.top - ? TimeIncrement.fromDurations( - clockState.options.timePlayerTop, - clockState.options.incrementPlayerTop, - ) - : TimeIncrement.fromDurations( - clockState.options.timePlayerBottom, - clockState.options.incrementPlayerBottom, - ), - onSubmit: ( - ClockPlayerType player, - TimeIncrement clock, - ) { - Navigator.of(context).pop(); - ref - .read(clockControllerProvider.notifier) - .updateOptionsCustom(clock, player); - }, - ), - ), - ), ], ), ), @@ -153,6 +123,48 @@ class ClockTile extends ConsumerWidget { style: const TextStyle(fontSize: 13, color: Colors.black), ), ), + Positioned( + bottom: MediaQuery.paddingOf(context).bottom + 24.0, + child: AnimatedOpacity( + opacity: clockState.started ? 0 : 1.0, + duration: const Duration(milliseconds: 300), + child: PlatformIconButton( + semanticsLabel: context.l10n.settingsSettings, + iconSize: 32, + icon: Icons.tune, + color: Theme.of(context).brightness == Brightness.dark + ? _darkClockStyle.textColor + : _lightClockStyle.textColor, + onTap: clockState.started + ? null + : () => showAdaptiveBottomSheet( + context: context, + builder: (BuildContext context) => + CustomClockSettings( + player: playerType, + clock: playerType == ClockPlayerType.top + ? TimeIncrement.fromDurations( + clockState.options.timePlayerTop, + clockState.options.incrementPlayerTop, + ) + : TimeIncrement.fromDurations( + clockState.options.timePlayerBottom, + clockState.options.incrementPlayerBottom, + ), + onSubmit: ( + ClockPlayerType player, + TimeIncrement clock, + ) { + Navigator.of(context).pop(); + ref + .read(clockControllerProvider.notifier) + .updateOptionsCustom(clock, player); + }, + ), + ), + ), + ), + ), ], ), ); diff --git a/lib/src/view/tools/tools_tab_screen.dart b/lib/src/view/tools/tools_tab_screen.dart index 6b0cffa4d7..374ec18b02 100644 --- a/lib/src/view/tools/tools_tab_screen.dart +++ b/lib/src/view/tools/tools_tab_screen.dart @@ -5,7 +5,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/navigation.dart'; -import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; diff --git a/lib/src/widgets/countdown_clock.dart b/lib/src/widgets/countdown_clock.dart index 0fb7216e20..84a913ecf5 100644 --- a/lib/src/widgets/countdown_clock.dart +++ b/lib/src/widgets/countdown_clock.dart @@ -3,11 +3,14 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; import 'package:lichess_mobile/src/model/settings/brightness.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; +part 'countdown_clock.freezed.dart'; + /// A simple countdown clock. /// /// The clock starts only when [active] is `true`. @@ -227,16 +230,16 @@ class _CountdownClockState extends ConsumerState { } } -@immutable -class ClockStyle { - const ClockStyle({ - required this.textColor, - required this.activeTextColor, - required this.emergencyTextColor, - required this.backgroundColor, - required this.activeBackgroundColor, - required this.emergencyBackgroundColor, - }); +@freezed +class ClockStyle with _$ClockStyle { + const factory ClockStyle({ + required Color textColor, + required Color activeTextColor, + required Color emergencyTextColor, + required Color backgroundColor, + required Color activeBackgroundColor, + required Color emergencyBackgroundColor, + }) = _ClockStyle; static const darkThemeStyle = ClockStyle( textColor: Colors.grey, @@ -255,11 +258,4 @@ class ClockStyle { activeBackgroundColor: Color(0xFFD0E0BD), emergencyBackgroundColor: Color(0xFFF2CCCC), ); - - final Color textColor; - final Color activeTextColor; - final Color emergencyTextColor; - final Color backgroundColor; - final Color activeBackgroundColor; - final Color emergencyBackgroundColor; } From dad8ff389bb4803805153d3df602c0872498f13f Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 11 Sep 2024 15:06:49 +0200 Subject: [PATCH 322/979] Scroll the whole view in explorer portrait mode --- .../opening_explorer_preferences.dart | 14 +- lib/src/view/analysis/analysis_board.dart | 33 +- .../opening_explorer_screen.dart | 892 +++++++++++++++++- .../opening_explorer_widget.dart | 777 --------------- lib/src/widgets/move_list.dart | 62 +- 5 files changed, 924 insertions(+), 854 deletions(-) delete mode 100644 lib/src/view/opening_explorer/opening_explorer_widget.dart diff --git a/lib/src/model/opening_explorer/opening_explorer_preferences.dart b/lib/src/model/opening_explorer/opening_explorer_preferences.dart index 7a1e221301..4ef3ab1f78 100644 --- a/lib/src/model/opening_explorer/opening_explorer_preferences.dart +++ b/lib/src/model/opening_explorer/opening_explorer_preferences.dart @@ -138,7 +138,12 @@ class OpeningExplorerPrefState with _$OpeningExplorerPrefState { LightUser? user, }) { try { - return _$OpeningExplorerPrefStateFromJson(json); + final prefs = _$OpeningExplorerPrefStateFromJson(json); + return prefs.copyWith( + playerDb: user != null + ? prefs.playerDb.copyWith(username: user.name) + : prefs.playerDb, + ); } catch (_) { return OpeningExplorerPrefState.defaults(user: user); } @@ -261,14 +266,11 @@ class PlayerDbPrefState with _$PlayerDbPrefState { since: earliestDate, ); - factory PlayerDbPrefState.fromJson( - Map json, { - LightUser? user, - }) { + factory PlayerDbPrefState.fromJson(Map json) { try { return _$PlayerDbPrefStateFromJson(json); } catch (_) { - return PlayerDbPrefState.defaults(user: user); + return PlayerDbPrefState.defaults(); } } } diff --git a/lib/src/view/analysis/analysis_board.dart b/lib/src/view/analysis/analysis_board.dart index 7ecca1eac5..6a0eac77bb 100644 --- a/lib/src/view/analysis/analysis_board.dart +++ b/lib/src/view/analysis/analysis_board.dart @@ -23,6 +23,7 @@ class AnalysisBoard extends ConsumerStatefulWidget { this.options, this.boardSize, { this.borderRadius, + this.disableDraggingPieces, }); final String pgn; @@ -30,6 +31,10 @@ class AnalysisBoard extends ConsumerStatefulWidget { final double boardSize; final BorderRadiusGeometry? borderRadius; + /// If true, the user won't be able to drag pieces. This settings is meant for + /// the opening explorer screen, where the user can drag the board. + final bool? disableDraggingPieces; + @override ConsumerState createState() => AnalysisBoardState(); } @@ -72,6 +77,8 @@ class AnalysisBoardState extends ConsumerState { ) : ISet(); + final boardPrefSettings = boardPrefs.toBoardSettings(); + return Chessboard( size: widget.boardSize, fen: analysisState.position.fen, @@ -101,18 +108,20 @@ class AnalysisBoardState extends ConsumerState { }) : IMap({sanMove.move.to: annotation}) : null, - settings: boardPrefs.toBoardSettings().copyWith( - borderRadius: widget.borderRadius, - boxShadow: widget.borderRadius != null - ? boardShadows - : const [], - drawShape: DrawShapeOptions( - enable: true, - onCompleteShape: _onCompleteShape, - onClearShapes: _onClearShapes, - newShapeColor: boardPrefs.shapeColor.color, - ), - ), + settings: boardPrefSettings.copyWith( + borderRadius: widget.borderRadius, + boxShadow: + widget.borderRadius != null ? boardShadows : const [], + drawShape: DrawShapeOptions( + enable: true, + onCompleteShape: _onCompleteShape, + onClearShapes: _onClearShapes, + newShapeColor: boardPrefs.shapeColor.color, + ), + pieceShiftMethod: widget.disableDraggingPieces == true + ? PieceShiftMethod.tapTwoSquares + : boardPrefSettings.pieceShiftMethod, + ), ); } diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index bbd41281c8..e43c3e9f67 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -1,13 +1,21 @@ import 'package:collection/collection.dart'; +import 'package:dartchess/dartchess.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; +import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_preferences.dart'; +import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_repository.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_board.dart'; +import 'package:lichess_mobile/src/view/game/archived_game_screen.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; @@ -15,21 +23,50 @@ import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/move_list.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; +import 'package:lichess_mobile/src/widgets/shimmer.dart'; +import 'package:url_launcher/url_launcher.dart'; import 'opening_explorer_settings.dart'; -import 'opening_explorer_widget.dart'; -class OpeningExplorerScreen extends StatelessWidget { +const _kTableRowVerticalPadding = 12.0; +const _kTableRowHorizontalPadding = 8.0; +const _kTableRowPadding = EdgeInsets.symmetric( + horizontal: _kTableRowHorizontalPadding, + vertical: _kTableRowVerticalPadding, +); + +Color _whiteBoxColor(BuildContext context) => + Theme.of(context).brightness == Brightness.dark + ? Colors.white.withOpacity(0.8) + : Colors.white; + +Color _blackBoxColor(BuildContext context) => + Theme.of(context).brightness == Brightness.light + ? Colors.black.withOpacity(0.7) + : Colors.black; + +class OpeningExplorerScreen extends ConsumerWidget { const OpeningExplorerScreen({required this.pgn, required this.options}); final String pgn; final AnalysisOptions options; @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final fen = ref.watch( + analysisControllerProvider(pgn, options) + .select((value) => value.currentNode.position.fen), + ); + final isIndexing = + ref.watch(openingExplorerProvider(fen: fen)).valueOrNull?.isIndexing ?? + false; + return PlatformScaffold( appBar: PlatformAppBar( title: Text(context.l10n.openingExplorer), + actions: [ + if (isIndexing) const _IndexingIndicator(), + ], ), body: _Body(pgn: pgn, options: options), ); @@ -38,15 +75,269 @@ class OpeningExplorerScreen extends StatelessWidget { const _kTabletBoardRadius = BorderRadius.all(Radius.circular(4.0)); -class _Body extends ConsumerWidget { +class _Body extends ConsumerStatefulWidget { + const _Body({ + required this.pgn, + required this.options, + }); + final String pgn; + final AnalysisOptions options; + + @override + ConsumerState<_Body> createState() => _OpeningExplorerState(); +} + +class _OpeningExplorerState extends ConsumerState<_Body> { + final Map cache = {}; + + /// Last explorer content that was successfully loaded. This is used to + /// display a loading indicator while the new content is being fetched. + List? lastExplorerWidgets; + + @override + Widget build(BuildContext context) { + final analysisState = + ref.watch(analysisControllerProvider(widget.pgn, widget.options)); + + final opening = analysisState.currentNode.isRoot + ? LightOpening( + eco: '', + name: context.l10n.startPosition, + ) + : analysisState.currentNode.opening ?? + analysisState.currentBranchOpening ?? + analysisState.contextOpening; + + final Widget openingHeader = Container( + padding: _kTableRowPadding, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.secondaryContainer, + ), + child: opening != null + ? GestureDetector( + onTap: opening.name == context.l10n.startPosition + ? null + : () => launchUrl( + Uri.parse( + 'https://lichess.org/opening/${opening.name}', + ), + ), + child: Row( + children: [ + Icon( + Icons.open_in_browser_outlined, + color: Theme.of(context).colorScheme.onSecondaryContainer, + ), + const SizedBox(width: 6.0), + Expanded( + child: Text( + '${opening.eco.isEmpty ? "" : "${opening.eco} "}${opening.name}', + style: TextStyle( + color: + Theme.of(context).colorScheme.onSecondaryContainer, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ) + : const SizedBox.shrink(), + ); + + if (analysisState.position.ply >= 50) { + return _OpeningExplorerView( + pgn: widget.pgn, + options: widget.options, + isLoading: false, + children: [ + openingHeader, + _OpeningExplorerMoveTable.maxDepth( + pgn: widget.pgn, + options: widget.options, + ), + ], + ); + } + + final prefs = ref.watch(openingExplorerPreferencesProvider); + + if (prefs.db == OpeningDatabase.player && prefs.playerDb.username == null) { + return _OpeningExplorerView( + pgn: widget.pgn, + options: widget.options, + isLoading: false, + children: [ + openingHeader, + const Padding( + padding: _kTableRowPadding, + child: Center( + // TODO: l10n + child: Text('Select a Lichess player in the settings.'), + ), + ), + ], + ); + } + + final cacheKey = OpeningExplorerCacheKey( + fen: analysisState.position.fen, + prefs: prefs, + ); + final cacheOpeningExplorer = cache[cacheKey]; + final openingExplorerAsync = cacheOpeningExplorer != null + ? AsyncValue.data( + (entry: cacheOpeningExplorer, isIndexing: false), + ) + : ref.watch(openingExplorerProvider(fen: analysisState.position.fen)); + + if (cacheOpeningExplorer == null) { + ref.listen(openingExplorerProvider(fen: analysisState.position.fen), + (_, curAsync) { + curAsync.whenData((cur) { + if (cur != null && !cur.isIndexing) { + cache[cacheKey] = cur.entry; + } + }); + }); + } + + final isLoading = + openingExplorerAsync.isLoading || openingExplorerAsync.value == null; + + return _OpeningExplorerView( + pgn: widget.pgn, + options: widget.options, + isLoading: isLoading, + children: openingExplorerAsync.when( + data: (openingExplorer) { + if (openingExplorer == null) { + return lastExplorerWidgets ?? + [ + Shimmer( + child: ShimmerLoading( + isLoading: true, + child: _OpeningExplorerMoveTable.loading( + pgn: widget.pgn, + options: widget.options, + ), + ), + ), + ]; + } + + final topGames = openingExplorer.entry.topGames; + final recentGames = openingExplorer.entry.recentGames; + + final ply = analysisState.position.ply; + + final children = [ + openingHeader, + _OpeningExplorerMoveTable( + moves: openingExplorer.entry.moves, + whiteWins: openingExplorer.entry.white, + draws: openingExplorer.entry.draws, + blackWins: openingExplorer.entry.black, + pgn: widget.pgn, + options: widget.options, + ), + if (topGames != null && topGames.isNotEmpty) ...[ + _OpeningExplorerHeader( + key: const Key('topGamesHeader'), + child: Text(context.l10n.topGames), + ), + ...List.generate( + topGames.length, + (int index) { + return OpeningExplorerGameTile( + key: Key('top-game-${topGames.get(index).id}'), + game: topGames.get(index), + color: index.isEven + ? Theme.of(context).colorScheme.surfaceContainerLow + : Theme.of(context).colorScheme.surfaceContainerHigh, + ply: ply, + ); + }, + growable: false, + ), + ], + if (recentGames != null && recentGames.isNotEmpty) ...[ + _OpeningExplorerHeader( + key: const Key('recentGamesHeader'), + child: Text(context.l10n.recentGames), + ), + ...List.generate( + recentGames.length, + (int index) { + return OpeningExplorerGameTile( + key: Key('recent-game-${recentGames.get(index).id}'), + game: recentGames.get(index), + color: index.isEven + ? Theme.of(context).colorScheme.surfaceContainerLow + : Theme.of(context).colorScheme.surfaceContainerHigh, + ply: ply, + ); + }, + growable: false, + ), + ], + ]; + + lastExplorerWidgets = children; + return children; + }, + loading: () => + lastExplorerWidgets ?? + [ + Shimmer( + child: ShimmerLoading( + isLoading: true, + child: _OpeningExplorerMoveTable.loading( + pgn: widget.pgn, + options: widget.options, + ), + ), + ), + ], + error: (e, s) { + debugPrint( + 'SEVERE: [OpeningExplorerScreen] could not load opening explorer data; $e\n$s', + ); + return [ + Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Text(e.toString()), + ), + ), + ]; + }, + ), + ); + } +} + +class _OpeningExplorerView extends StatelessWidget { + const _OpeningExplorerView({ + required this.pgn, + required this.options, + required this.children, + required this.isLoading, + }); + + final String pgn; final AnalysisOptions options; - const _Body({required this.pgn, required this.options}); + final List children; + final bool isLoading; @override - Widget build(BuildContext context, WidgetRef ref) { + Widget build(BuildContext context) { final isTablet = isTabletOrLarger(context); + final loadingOverlayColor = Theme.of(context).brightness == Brightness.dark + ? Colors.black + : Colors.white; return SafeArea( bottom: false, @@ -73,8 +364,21 @@ class _Body extends ConsumerWidget { ? defaultBoardSize - kTabletBoardTableSidePadding * 2 : defaultBoardSize; - // If the aspect ratio is greater than 1, we are in landscape mode - if (aspectRatio > 1) { + final isLandscape = aspectRatio > 1; + + final loadingOverlay = Positioned.fill( + child: IgnorePointer( + ignoring: !isLoading, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 300), + curve: Curves.fastOutSlowIn, + opacity: isLoading ? 0.3 : 0.0, + child: ColoredBox(color: loadingOverlayColor), + ), + ), + ); + + if (isLandscape) { return Row( mainAxisSize: MainAxisSize.max, children: [ @@ -106,9 +410,14 @@ class _Body extends ConsumerWidget { kTabletBoardTableSidePadding, ), semanticContainer: false, - child: OpeningExplorerWidget( - pgn: pgn, - options: options, + child: Stack( + children: [ + ListView( + padding: EdgeInsets.zero, + children: children, + ), + loadingOverlay, + ], ), ), ), @@ -117,39 +426,28 @@ class _Body extends ConsumerWidget { ), ], ); - } - // If the aspect ratio is less than 1, we are in portrait mode - else { - return Column( + } else { + return Stack( children: [ - Padding( + ListView( padding: isTablet ? const EdgeInsets.only( left: kTabletBoardTableSidePadding, right: kTabletBoardTableSidePadding, - bottom: kTabletBoardTableSidePadding, ) : EdgeInsets.zero, - child: AnalysisBoard( - pgn, - options, - boardSize, - borderRadius: isTablet ? _kTabletBoardRadius : null, - ), - ), - Expanded( - child: Padding( - padding: isTablet - ? const EdgeInsets.symmetric( - horizontal: kTabletBoardTableSidePadding, - ) - : EdgeInsets.zero, - child: OpeningExplorerWidget( - pgn: pgn, - options: options, + children: [ + AnalysisBoard( + pgn, + options, + boardSize, + borderRadius: isTablet ? _kTabletBoardRadius : null, + disableDraggingPieces: true, ), - ), + ...children, + ], ), + loadingOverlay, ], ); } @@ -163,6 +461,528 @@ class _Body extends ConsumerWidget { } } +class _IndexingIndicator extends StatefulWidget { + const _IndexingIndicator(); + + @override + State<_IndexingIndicator> createState() => _IndexingIndicatorState(); +} + +class _IndexingIndicatorState extends State<_IndexingIndicator> + with TickerProviderStateMixin { + late AnimationController controller; + + @override + void initState() { + controller = AnimationController( + vsync: this, + duration: const Duration(seconds: 3), + )..addListener(() { + setState(() {}); + }); + controller.repeat(); + super.initState(); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(6.0), + child: SizedBox( + width: 10, + height: 10, + child: CircularProgressIndicator.adaptive( + value: controller.value, + // TODO: l10n + semanticsLabel: 'Indexing', + ), + ), + ); + } +} + +/// Table of moves for the opening explorer. +class _OpeningExplorerMoveTable extends ConsumerWidget { + const _OpeningExplorerMoveTable({ + required this.moves, + required this.whiteWins, + required this.draws, + required this.blackWins, + required this.pgn, + required this.options, + }) : _isLoading = false, + _maxDepthReached = false; + + const _OpeningExplorerMoveTable.loading({ + required this.pgn, + required this.options, + }) : _isLoading = true, + moves = const IListConst([]), + whiteWins = 0, + draws = 0, + blackWins = 0, + _maxDepthReached = false; + + const _OpeningExplorerMoveTable.maxDepth({ + required this.pgn, + required this.options, + }) : _isLoading = false, + moves = const IListConst([]), + whiteWins = 0, + draws = 0, + blackWins = 0, + _maxDepthReached = true; + + final IList moves; + final int whiteWins; + final int draws; + final int blackWins; + final String pgn; + final AnalysisOptions options; + + final bool _isLoading; + final bool _maxDepthReached; + + String formatNum(int num) => NumberFormat.decimalPatternDigits().format(num); + + static const columnWidths = { + 0: FractionColumnWidth(0.15), + 1: FractionColumnWidth(0.35), + 2: FractionColumnWidth(0.50), + }; + + @override + Widget build(BuildContext context, WidgetRef ref) { + if (_isLoading) { + return loadingTable; + } + + final games = whiteWins + draws + blackWins; + final ctrlProvider = analysisControllerProvider(pgn, options); + + const topPadding = EdgeInsets.only(top: _kTableRowVerticalPadding / 2); + const headerTextStyle = TextStyle(fontSize: 12); + + return Table( + columnWidths: columnWidths, + children: [ + TableRow( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.secondaryContainer, + ), + children: [ + Padding( + padding: _kTableRowPadding.subtract(topPadding), + child: Text(context.l10n.move, style: headerTextStyle), + ), + Padding( + padding: _kTableRowPadding.subtract(topPadding), + child: Text(context.l10n.games, style: headerTextStyle), + ), + Padding( + padding: _kTableRowPadding.subtract(topPadding), + child: Text(context.l10n.whiteDrawBlack, style: headerTextStyle), + ), + ], + ), + ...List.generate( + moves.length, + (int index) { + final move = moves.get(index); + final percentGames = ((move.games / games) * 100).round(); + return TableRow( + decoration: BoxDecoration( + color: index.isEven + ? Theme.of(context).colorScheme.surfaceContainerLow + : Theme.of(context).colorScheme.surfaceContainerHigh, + ), + children: [ + TableRowInkWell( + onTap: () => ref + .read(ctrlProvider.notifier) + .onUserMove(NormalMove.fromUci(move.uci)), + child: Padding( + padding: _kTableRowPadding, + child: Text(move.san), + ), + ), + TableRowInkWell( + onTap: () => ref + .read(ctrlProvider.notifier) + .onUserMove(NormalMove.fromUci(move.uci)), + child: Padding( + padding: _kTableRowPadding, + child: Text('${formatNum(move.games)} ($percentGames%)'), + ), + ), + TableRowInkWell( + onTap: () => ref + .read(ctrlProvider.notifier) + .onUserMove(NormalMove.fromUci(move.uci)), + child: Padding( + padding: _kTableRowPadding, + child: _WinPercentageChart( + whiteWins: move.white, + draws: move.draws, + blackWins: move.black, + ), + ), + ), + ], + ); + }, + ), + if (_maxDepthReached) + TableRow( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerLow, + ), + children: [ + Padding( + padding: _kTableRowPadding, + child: Text( + String.fromCharCode(Icons.not_interested_outlined.codePoint), + style: TextStyle( + fontFamily: Icons.not_interested_outlined.fontFamily, + ), + ), + ), + Padding( + padding: _kTableRowPadding, + child: Text(context.l10n.maxDepthReached), + ), + const Padding( + padding: _kTableRowPadding, + child: SizedBox.shrink(), + ), + ], + ) + else if (moves.isNotEmpty) + TableRow( + decoration: BoxDecoration( + color: moves.length.isEven + ? Theme.of(context).colorScheme.surfaceContainerLow + : Theme.of(context).colorScheme.surfaceContainerHigh, + ), + children: [ + Container( + padding: _kTableRowPadding, + alignment: Alignment.centerLeft, + child: const Icon(Icons.functions), + ), + Padding( + padding: _kTableRowPadding, + child: Text('${formatNum(games)} (100%)'), + ), + Padding( + padding: _kTableRowPadding, + child: _WinPercentageChart( + whiteWins: whiteWins, + draws: draws, + blackWins: blackWins, + ), + ), + ], + ) + else + TableRow( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerLow, + ), + children: [ + Padding( + padding: _kTableRowPadding, + child: Text( + String.fromCharCode(Icons.not_interested_outlined.codePoint), + style: TextStyle( + fontFamily: Icons.not_interested_outlined.fontFamily, + ), + ), + ), + Padding( + padding: _kTableRowPadding, + child: Text(context.l10n.noGameFound), + ), + const Padding( + padding: _kTableRowPadding, + child: SizedBox.shrink(), + ), + ], + ), + ], + ); + } + + static final loadingTable = Table( + columnWidths: columnWidths, + children: List.generate( + 10, + (int index) => TableRow( + children: [ + Padding( + padding: _kTableRowPadding, + child: Container( + height: 20, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(5), + ), + ), + ), + Padding( + padding: _kTableRowPadding, + child: Container( + height: 20, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(5), + ), + ), + ), + Padding( + padding: _kTableRowPadding, + child: Container( + height: 20, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(5), + ), + ), + ), + ], + ), + ), + ); +} + +/// A game tile for the opening explorer. +class OpeningExplorerGameTile extends ConsumerStatefulWidget { + const OpeningExplorerGameTile({ + required this.game, + required this.color, + required this.ply, + super.key, + }); + + final OpeningExplorerGame game; + final Color color; + final int ply; + + @override + ConsumerState createState() => + _OpeningExplorerGameTileState(); +} + +class _OpeningExplorerGameTileState + extends ConsumerState { + @override + Widget build(BuildContext context) { + const widthResultBox = 50.0; + const paddingResultBox = EdgeInsets.all(5); + + return Container( + padding: _kTableRowPadding, + color: widget.color, + child: AdaptiveInkWell( + onTap: () { + pushPlatformRoute( + context, + builder: (_) => ArchivedGameScreen( + gameId: widget.game.id, + orientation: Side.white, + initialCursor: widget.ply, + ), + ); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(widget.game.white.rating.toString()), + Text(widget.game.black.rating.toString()), + ], + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.game.white.name, + overflow: TextOverflow.ellipsis, + ), + Text( + widget.game.black.name, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + Row( + children: [ + if (widget.game.winner == 'white') + Container( + width: widthResultBox, + padding: paddingResultBox, + decoration: BoxDecoration( + color: _whiteBoxColor(context), + borderRadius: BorderRadius.circular(5), + ), + child: const Text( + '1-0', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.black, + ), + ), + ) + else if (widget.game.winner == 'black') + Container( + width: widthResultBox, + padding: paddingResultBox, + decoration: BoxDecoration( + color: _blackBoxColor(context), + borderRadius: BorderRadius.circular(5), + ), + child: const Text( + '0-1', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + ), + ), + ) + else + Container( + width: widthResultBox, + padding: paddingResultBox, + decoration: BoxDecoration( + color: Colors.grey, + borderRadius: BorderRadius.circular(5), + ), + child: const Text( + '½-½', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + ), + ), + ), + if (widget.game.month != null) ...[ + const SizedBox(width: 10.0), + Text( + widget.game.month!, + style: const TextStyle( + fontFeatures: [FontFeature.tabularFigures()], + ), + ), + ], + if (widget.game.speed != null) ...[ + const SizedBox(width: 10.0), + Icon(widget.game.speed!.icon, size: 20), + ], + ], + ), + ], + ), + ), + ); + } +} + +class _OpeningExplorerHeader extends StatelessWidget { + const _OpeningExplorerHeader({required this.child, super.key}); + + final Widget child; + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + padding: _kTableRowPadding, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.secondaryContainer, + ), + child: child, + ); + } +} + +class _WinPercentageChart extends StatelessWidget { + const _WinPercentageChart({ + required this.whiteWins, + required this.draws, + required this.blackWins, + }); + + final int whiteWins; + final int draws; + final int blackWins; + + int percentGames(int games) => + ((games / (whiteWins + draws + blackWins)) * 100).round(); + String label(int percent) => percent < 20 ? '' : '$percent%'; + + @override + Widget build(BuildContext context) { + final percentWhite = percentGames(whiteWins); + final percentDraws = percentGames(draws); + final percentBlack = percentGames(blackWins); + + return ClipRRect( + borderRadius: BorderRadius.circular(5), + child: Row( + children: [ + Expanded( + flex: percentWhite, + child: ColoredBox( + color: _whiteBoxColor(context), + child: Text( + label(percentWhite), + textAlign: TextAlign.center, + style: const TextStyle(color: Colors.black), + ), + ), + ), + Expanded( + flex: percentDraws, + child: ColoredBox( + color: Colors.grey, + child: Text( + label(percentDraws), + textAlign: TextAlign.center, + style: const TextStyle(color: Colors.white), + ), + ), + ), + Expanded( + flex: percentBlack, + child: ColoredBox( + color: _blackBoxColor(context), + child: Text( + label(percentBlack), + textAlign: TextAlign.center, + style: const TextStyle(color: Colors.white), + ), + ), + ), + ], + ), + ); + } +} + class _MoveList extends ConsumerWidget { const _MoveList({ required this.pgn, @@ -185,6 +1005,8 @@ class _MoveList extends ConsumerWidget { final currentMoveIndex = state.currentNode.position.ply; return MoveList( + inlineBackgroundColor: Theme.of(context).colorScheme.surfaceContainer, + inlineColor: Theme.of(context).colorScheme.onSurface, type: MoveListType.inline, slicedMoves: slicedMoves, currentMoveIndex: currentMoveIndex, diff --git a/lib/src/view/opening_explorer/opening_explorer_widget.dart b/lib/src/view/opening_explorer/opening_explorer_widget.dart deleted file mode 100644 index a2f9e6ae92..0000000000 --- a/lib/src/view/opening_explorer/opening_explorer_widget.dart +++ /dev/null @@ -1,777 +0,0 @@ -import 'package:dartchess/dartchess.dart'; -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:intl/intl.dart'; -import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; -import 'package:lichess_mobile/src/model/common/chess.dart'; -import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; -import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_preferences.dart'; -import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_repository.dart'; -import 'package:lichess_mobile/src/utils/l10n_context.dart'; -import 'package:lichess_mobile/src/utils/navigation.dart'; -import 'package:lichess_mobile/src/view/game/archived_game_screen.dart'; -import 'package:lichess_mobile/src/widgets/buttons.dart'; -import 'package:lichess_mobile/src/widgets/shimmer.dart'; -import 'package:url_launcher/url_launcher.dart'; - -const _kTableRowVerticalPadding = 10.0; -const _kTableRowHorizontalPadding = 8.0; -const _kTableRowPadding = EdgeInsets.symmetric( - horizontal: _kTableRowHorizontalPadding, - vertical: _kTableRowVerticalPadding, -); - -Color _whiteBoxColor(BuildContext context) => - Theme.of(context).brightness == Brightness.dark - ? Colors.white.withOpacity(0.8) - : Colors.white; - -Color _blackBoxColor(BuildContext context) => - Theme.of(context).brightness == Brightness.light - ? Colors.black.withOpacity(0.7) - : Colors.black; - -class OpeningExplorerWidget extends ConsumerStatefulWidget { - const OpeningExplorerWidget({ - required this.pgn, - required this.options, - super.key, - }); - - final String pgn; - final AnalysisOptions options; - - @override - ConsumerState createState() => _OpeningExplorerState(); -} - -class _OpeningExplorerState extends ConsumerState { - final Map cache = {}; - - /// Last explorer content that was successfully loaded. This is used to - /// display a loading indicator while the new content is being fetched. - List? lastExplorerWidgets; - - @override - Widget build(BuildContext context) { - final analysisState = - ref.watch(analysisControllerProvider(widget.pgn, widget.options)); - - if (analysisState.position.ply >= 50) { - return Align( - alignment: Alignment.center, - child: Text(context.l10n.maxDepthReached), - ); - } - - final prefs = ref.watch(openingExplorerPreferencesProvider); - if (prefs.db == OpeningDatabase.player && prefs.playerDb.username == null) { - return const Align( - alignment: Alignment.center, - child: Text('Select a Lichess player in the settings'), - ); - } - - final opening = analysisState.currentNode.isRoot - ? LightOpening( - eco: '', - name: context.l10n.startPosition, - ) - : analysisState.currentNode.opening ?? - analysisState.currentBranchOpening ?? - analysisState.contextOpening; - - final cacheKey = OpeningExplorerCacheKey( - fen: analysisState.position.fen, - prefs: prefs, - ); - final cacheOpeningExplorer = cache[cacheKey]; - final openingExplorerAsync = cacheOpeningExplorer != null - ? AsyncValue.data( - (entry: cacheOpeningExplorer, isIndexing: false), - ) - : ref.watch(openingExplorerProvider(fen: analysisState.position.fen)); - - if (cacheOpeningExplorer == null) { - ref.listen(openingExplorerProvider(fen: analysisState.position.fen), - (_, curAsync) { - curAsync.whenData((cur) { - if (cur != null && !cur.isIndexing) { - cache[cacheKey] = cur.entry; - } - }); - }); - } - - return openingExplorerAsync.when( - data: (openingExplorer) { - if (openingExplorer == null) { - return _OpeningExplorerView.loading( - children: lastExplorerWidgets ?? - [ - Shimmer( - child: ShimmerLoading( - isLoading: true, - child: _OpeningExplorerMoveTable.loading( - pgn: widget.pgn, - options: widget.options, - ), - ), - ), - ], - ); - } - if (openingExplorer.entry.moves.isEmpty) { - lastExplorerWidgets = null; - return _OpeningExplorerView.empty( - children: [ - Center( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Text(context.l10n.noGameFound), - ), - ), - ], - ); - } - - final topGames = openingExplorer.entry.topGames; - final recentGames = openingExplorer.entry.recentGames; - - final ply = analysisState.position.ply; - - final children = [ - Container( - padding: _kTableRowPadding.subtract( - const EdgeInsets.only(bottom: _kTableRowVerticalPadding / 2), - ), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.secondaryContainer, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - if (opening != null) - Expanded( - flex: 75, - child: GestureDetector( - onTap: opening.name == context.l10n.startPosition - ? null - : () => launchUrl( - Uri.parse( - 'https://lichess.org/opening/${opening.name}', - ), - ), - child: Text( - '${opening.eco.isEmpty ? "" : "${opening.eco} "}${opening.name}', - style: TextStyle( - color: Theme.of(context) - .colorScheme - .onSecondaryContainer, - fontWeight: FontWeight.bold, - ), - ), - ), - ), - if (openingExplorer.isIndexing == true) - Expanded( - flex: 25, - child: _IndexingIndicator(), - ), - ], - ), - ), - _OpeningExplorerMoveTable( - moves: openingExplorer.entry.moves, - whiteWins: openingExplorer.entry.white, - draws: openingExplorer.entry.draws, - blackWins: openingExplorer.entry.black, - pgn: widget.pgn, - options: widget.options, - ), - if (topGames != null && topGames.isNotEmpty) ...[ - _OpeningExplorerHeader( - key: const Key('topGamesHeader'), - child: Text(context.l10n.topGames), - ), - ...List.generate( - topGames.length, - (int index) { - return OpeningExplorerGameTile( - key: Key('top-game-${topGames.get(index).id}'), - game: topGames.get(index), - color: index.isEven - ? Theme.of(context).colorScheme.surfaceContainerLow - : Theme.of(context).colorScheme.surfaceContainerHigh, - ply: ply, - ); - }, - growable: false, - ), - ], - if (recentGames != null && recentGames.isNotEmpty) ...[ - _OpeningExplorerHeader( - key: const Key('recentGamesHeader'), - child: Text(context.l10n.recentGames), - ), - ...List.generate( - recentGames.length, - (int index) { - return OpeningExplorerGameTile( - key: Key('recent-game-${recentGames.get(index).id}'), - game: recentGames.get(index), - color: index.isEven - ? Theme.of(context).colorScheme.surfaceContainerLow - : Theme.of(context).colorScheme.surfaceContainerHigh, - ply: ply, - ); - }, - growable: false, - ), - ], - ]; - - lastExplorerWidgets = children; - - return _OpeningExplorerView( - children: children, - ); - }, - loading: () => _OpeningExplorerView.loading( - children: lastExplorerWidgets ?? - [ - Shimmer( - child: ShimmerLoading( - isLoading: true, - child: _OpeningExplorerMoveTable.loading( - pgn: widget.pgn, - options: widget.options, - ), - ), - ), - ], - ), - error: (e, s) { - debugPrint( - 'SEVERE: [OpeningExplorerScreen] could not load opening explorer data; $e\n$s', - ); - return Center( - child: Text(e.toString()), - ); - }, - ); - } -} - -/// The opening header and the opening explorer move table. -class _OpeningExplorerView extends StatelessWidget { - const _OpeningExplorerView({ - required this.children, - }) : loading = false, - empty = false; - - const _OpeningExplorerView.loading({ - required this.children, - }) : loading = true, - empty = false; - - const _OpeningExplorerView.empty({ - required this.children, - }) : loading = false, - empty = true; - - final List children; - final bool loading; - final bool empty; - - @override - Widget build(BuildContext context) { - final loadingOverlayColor = Theme.of(context).brightness == Brightness.dark - ? Colors.black - : Colors.white; - - return Stack( - children: [ - if (empty) - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: children, - ) - else - Center( - child: ListView(padding: EdgeInsets.zero, children: children), - ), - Positioned.fill( - child: IgnorePointer( - ignoring: !loading, - child: AnimatedOpacity( - duration: const Duration(milliseconds: 300), - curve: Curves.fastOutSlowIn, - opacity: loading ? 0.5 : 0.0, - child: ColoredBox(color: loadingOverlayColor), - ), - ), - ), - ], - ); - } -} - -class _IndexingIndicator extends StatefulWidget { - @override - State<_IndexingIndicator> createState() => _IndexingIndicatorState(); -} - -class _IndexingIndicatorState extends State<_IndexingIndicator> - with TickerProviderStateMixin { - late AnimationController controller; - - @override - void initState() { - controller = AnimationController( - vsync: this, - duration: const Duration(seconds: 3), - )..addListener(() { - setState(() {}); - }); - controller.repeat(); - super.initState(); - } - - @override - void dispose() { - controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Row( - children: [ - SizedBox( - width: 10, - height: 10, - child: CircularProgressIndicator.adaptive( - value: controller.value, - semanticsLabel: 'Indexing...', - ), - ), - const SizedBox(width: 10), - const Text('Indexing...'), - ], - ); - } -} - -/// Table of moves for the opening explorer. -class _OpeningExplorerMoveTable extends ConsumerWidget { - const _OpeningExplorerMoveTable({ - required this.moves, - required this.whiteWins, - required this.draws, - required this.blackWins, - required this.pgn, - required this.options, - }) : _isLoading = false; - - const _OpeningExplorerMoveTable.loading({ - required this.pgn, - required this.options, - }) : _isLoading = true, - moves = const IListConst([]), - whiteWins = 0, - draws = 0, - blackWins = 0; - - final IList moves; - final int whiteWins; - final int draws; - final int blackWins; - final String pgn; - final AnalysisOptions options; - - final bool _isLoading; - - String formatNum(int num) => NumberFormat.decimalPatternDigits().format(num); - - static const columnWidths = { - 0: FractionColumnWidth(0.15), - 1: FractionColumnWidth(0.35), - 2: FractionColumnWidth(0.50), - }; - - @override - Widget build(BuildContext context, WidgetRef ref) { - if (_isLoading) { - return loadingTable; - } - - final games = whiteWins + draws + blackWins; - final ctrlProvider = analysisControllerProvider(pgn, options); - - const topPadding = EdgeInsets.only(top: _kTableRowVerticalPadding / 2); - const headerTextStyle = TextStyle(fontSize: 12); - - return Table( - columnWidths: columnWidths, - children: [ - TableRow( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.secondaryContainer, - ), - children: [ - Padding( - padding: _kTableRowPadding.subtract(topPadding), - child: Text(context.l10n.move, style: headerTextStyle), - ), - Padding( - padding: _kTableRowPadding.subtract(topPadding), - child: Text(context.l10n.games, style: headerTextStyle), - ), - Padding( - padding: _kTableRowPadding.subtract(topPadding), - child: Text(context.l10n.whiteDrawBlack, style: headerTextStyle), - ), - ], - ), - ...List.generate( - moves.length, - (int index) { - final move = moves.get(index); - final percentGames = ((move.games / games) * 100).round(); - return TableRow( - decoration: BoxDecoration( - color: index.isEven - ? Theme.of(context).colorScheme.surfaceContainerLow - : Theme.of(context).colorScheme.surfaceContainerHigh, - ), - children: [ - TableRowInkWell( - onTap: () => ref - .read(ctrlProvider.notifier) - .onUserMove(NormalMove.fromUci(move.uci)), - child: Padding( - padding: _kTableRowPadding, - child: Text(move.san), - ), - ), - TableRowInkWell( - onTap: () => ref - .read(ctrlProvider.notifier) - .onUserMove(NormalMove.fromUci(move.uci)), - child: Padding( - padding: _kTableRowPadding, - child: Text('${formatNum(move.games)} ($percentGames%)'), - ), - ), - TableRowInkWell( - onTap: () => ref - .read(ctrlProvider.notifier) - .onUserMove(NormalMove.fromUci(move.uci)), - child: Padding( - padding: _kTableRowPadding, - child: _WinPercentageChart( - whiteWins: move.white, - draws: move.draws, - blackWins: move.black, - ), - ), - ), - ], - ); - }, - ), - TableRow( - decoration: BoxDecoration( - color: moves.length.isEven - ? Theme.of(context).colorScheme.surfaceContainerLow - : Theme.of(context).colorScheme.surfaceContainerHigh, - ), - children: [ - Container( - padding: _kTableRowPadding, - alignment: Alignment.centerLeft, - child: const Icon(Icons.functions), - ), - Padding( - padding: _kTableRowPadding, - child: Text('${formatNum(games)} (100%)'), - ), - Padding( - padding: _kTableRowPadding, - child: _WinPercentageChart( - whiteWins: whiteWins, - draws: draws, - blackWins: blackWins, - ), - ), - ], - ), - ], - ); - } - - static final loadingTable = Table( - columnWidths: columnWidths, - children: List.generate( - 10, - (int index) => TableRow( - children: [ - Padding( - padding: _kTableRowPadding, - child: Container( - height: 20, - width: double.infinity, - decoration: BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(5), - ), - ), - ), - Padding( - padding: _kTableRowPadding, - child: Container( - height: 20, - width: double.infinity, - decoration: BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(5), - ), - ), - ), - Padding( - padding: _kTableRowPadding, - child: Container( - height: 20, - width: double.infinity, - decoration: BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(5), - ), - ), - ), - ], - ), - ), - ); -} - -/// A game tile for the opening explorer. -class OpeningExplorerGameTile extends ConsumerStatefulWidget { - const OpeningExplorerGameTile({ - required this.game, - required this.color, - required this.ply, - super.key, - }); - - final OpeningExplorerGame game; - final Color color; - final int ply; - - @override - ConsumerState createState() => - _OpeningExplorerGameTileState(); -} - -class _OpeningExplorerGameTileState - extends ConsumerState { - @override - Widget build(BuildContext context) { - const widthResultBox = 50.0; - const paddingResultBox = EdgeInsets.all(5); - - return Container( - padding: _kTableRowPadding, - color: widget.color, - child: AdaptiveInkWell( - onTap: () { - pushPlatformRoute( - context, - builder: (_) => ArchivedGameScreen( - gameId: widget.game.id, - orientation: Side.white, - initialCursor: widget.ply, - ), - ); - }, - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(widget.game.white.rating.toString()), - Text(widget.game.black.rating.toString()), - ], - ), - const SizedBox(width: 10), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - widget.game.white.name, - overflow: TextOverflow.ellipsis, - ), - Text( - widget.game.black.name, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - Row( - children: [ - if (widget.game.winner == 'white') - Container( - width: widthResultBox, - padding: paddingResultBox, - decoration: BoxDecoration( - color: _whiteBoxColor(context), - borderRadius: BorderRadius.circular(5), - ), - child: const Text( - '1-0', - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.black, - ), - ), - ) - else if (widget.game.winner == 'black') - Container( - width: widthResultBox, - padding: paddingResultBox, - decoration: BoxDecoration( - color: _blackBoxColor(context), - borderRadius: BorderRadius.circular(5), - ), - child: const Text( - '0-1', - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white, - ), - ), - ) - else - Container( - width: widthResultBox, - padding: paddingResultBox, - decoration: BoxDecoration( - color: Colors.grey, - borderRadius: BorderRadius.circular(5), - ), - child: const Text( - '½-½', - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white, - ), - ), - ), - if (widget.game.month != null) ...[ - const SizedBox(width: 10.0), - Text( - widget.game.month!, - style: const TextStyle( - fontFeatures: [FontFeature.tabularFigures()], - ), - ), - ], - if (widget.game.speed != null) ...[ - const SizedBox(width: 10.0), - Icon(widget.game.speed!.icon, size: 20), - ], - ], - ), - ], - ), - ), - ); - } -} - -class _OpeningExplorerHeader extends StatelessWidget { - const _OpeningExplorerHeader({required this.child, super.key}); - - final Widget child; - - @override - Widget build(BuildContext context) { - return Container( - width: double.infinity, - padding: _kTableRowPadding, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.secondaryContainer, - ), - child: child, - ); - } -} - -class _WinPercentageChart extends StatelessWidget { - const _WinPercentageChart({ - required this.whiteWins, - required this.draws, - required this.blackWins, - }); - - final int whiteWins; - final int draws; - final int blackWins; - - int percentGames(int games) => - ((games / (whiteWins + draws + blackWins)) * 100).round(); - String label(int percent) => percent < 20 ? '' : '$percent%'; - - @override - Widget build(BuildContext context) { - final percentWhite = percentGames(whiteWins); - final percentDraws = percentGames(draws); - final percentBlack = percentGames(blackWins); - - return ClipRRect( - borderRadius: BorderRadius.circular(5), - child: Row( - children: [ - Expanded( - flex: percentWhite, - child: ColoredBox( - color: _whiteBoxColor(context), - child: Text( - label(percentWhite), - textAlign: TextAlign.center, - style: const TextStyle(color: Colors.black), - ), - ), - ), - Expanded( - flex: percentDraws, - child: ColoredBox( - color: Colors.grey, - child: Text( - label(percentDraws), - textAlign: TextAlign.center, - style: const TextStyle(color: Colors.white), - ), - ), - ), - Expanded( - flex: percentBlack, - child: ColoredBox( - color: _blackBoxColor(context), - child: Text( - label(percentBlack), - textAlign: TextAlign.center, - style: const TextStyle(color: Colors.white), - ), - ), - ), - ], - ), - ); - } -} diff --git a/lib/src/widgets/move_list.dart b/lib/src/widgets/move_list.dart index 997d594cc0..a516ee4ab5 100644 --- a/lib/src/widgets/move_list.dart +++ b/lib/src/widgets/move_list.dart @@ -1,5 +1,4 @@ import 'package:collection/collection.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/account/account_preferences.dart'; @@ -9,7 +8,9 @@ import 'package:lichess_mobile/src/utils/rate_limit.dart'; import 'platform.dart'; const _scrollAnimationDuration = Duration(milliseconds: 200); -const _moveListOpacity = 0.6; +const _moveListOpacity = 0.8; + +const _kMoveListHeight = 40.0; enum MoveListType { inline, stacked } @@ -18,11 +19,17 @@ class MoveList extends ConsumerStatefulWidget { required this.type, required this.slicedMoves, required this.currentMoveIndex, + this.inlineColor, + this.inlineBackgroundColor, this.onSelectMove, }); final MoveListType type; + final Color? inlineColor; + + final Color? inlineBackgroundColor; + final Iterable>> slicedMoves; final int currentMoveIndex; @@ -79,8 +86,9 @@ class _MoveListState extends ConsumerState { return widget.type == MoveListType.inline ? Container( + color: widget.inlineBackgroundColor, padding: const EdgeInsets.only(left: 5), - height: 40, + height: _kMoveListHeight, width: double.infinity, child: SingleChildScrollView( scrollDirection: Axis.horizontal, @@ -91,7 +99,11 @@ class _MoveListState extends ConsumerState { margin: const EdgeInsets.only(right: 10), child: Row( children: [ - InlineMoveCount(count: index + 1), + InlineMoveCount( + pieceNotation: pieceNotation, + count: index + 1, + color: widget.inlineColor, + ), ...moves.map( (move) { // cursor index starts at 0, move index starts at 1 @@ -100,6 +112,7 @@ class _MoveListState extends ConsumerState { return InlineMoveItem( key: isCurrentMove ? currentMoveKey : null, move: move, + color: widget.inlineColor, pieceNotation: pieceNotation, current: isCurrentMove, onSelectMove: widget.onSelectMove, @@ -162,10 +175,17 @@ class _MoveListState extends ConsumerState { } class InlineMoveCount extends StatelessWidget { - const InlineMoveCount({required this.count}); + const InlineMoveCount({ + required this.count, + required this.pieceNotation, + this.color, + }); + final PieceNotation pieceNotation; final int count; + final Color? color; + @override Widget build(BuildContext context) { return Container( @@ -173,8 +193,11 @@ class InlineMoveCount extends StatelessWidget { child: Text( '$count.', style: TextStyle( - fontWeight: FontWeight.w600, - color: textShade(context, _moveListOpacity), + fontWeight: FontWeight.w500, + color: color?.withOpacity(_moveListOpacity) ?? + textShade(context, _moveListOpacity), + fontFamily: + pieceNotation == PieceNotation.symbol ? 'ChessFont' : null, ), ), ); @@ -185,11 +208,14 @@ class InlineMoveItem extends StatelessWidget { const InlineMoveItem({ required this.move, required this.pieceNotation, + this.color, this.current, this.onSelectMove, super.key, }); + final Color? color; + final MapEntry move; final PieceNotation pieceNotation; final bool? current; @@ -201,28 +227,16 @@ class InlineMoveItem extends StatelessWidget { onTap: onSelectMove != null ? () => onSelectMove!(move.key + 1) : null, child: Container( padding: const EdgeInsets.symmetric(vertical: 3, horizontal: 4), - decoration: ShapeDecoration( - color: current == true - ? Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoDynamicColor.resolve( - CupertinoColors.secondarySystemBackground, - context, - ) - : null - // TODO add bg color on android - : null, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(4.0)), - ), - ), child: Text( move.value, style: TextStyle( fontFamily: pieceNotation == PieceNotation.symbol ? 'ChessFont' : null, - fontWeight: FontWeight.w600, + fontWeight: current == true ? FontWeight.bold : FontWeight.w500, color: current != true - ? textShade(context, _moveListOpacity) + ? color != null + ? color!.withOpacity(_moveListOpacity) + : textShade(context, _moveListOpacity) : Theme.of(context).colorScheme.primary, ), ), @@ -239,7 +253,7 @@ class StackedMoveCount extends StatelessWidget { @override Widget build(BuildContext context) { return SizedBox( - width: 40, + width: _kMoveListHeight, child: Text( '$count.', style: TextStyle( From a01d3cba8e0705eba9364fa55abbd223f65e392c Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 11 Sep 2024 15:10:32 +0200 Subject: [PATCH 323/979] Fix potential bad state in search screen --- lib/src/view/user/search_screen.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/src/view/user/search_screen.dart b/lib/src/view/user/search_screen.dart index efd0f2d828..30f52660db 100644 --- a/lib/src/view/user/search_screen.dart +++ b/lib/src/view/user/search_screen.dart @@ -44,6 +44,9 @@ class _SearchScreenState extends ConsumerState { } void _onSearchChanged() { + if (!context.mounted) { + return; + } final term = _searchController.text; if (term.length >= 3) { ref.read(autoCompleteUserProvider(term)); From adb044138a6a077b7c91b5aca4b349b9eb380b50 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 11 Sep 2024 15:15:13 +0200 Subject: [PATCH 324/979] Fix explorer test --- test/view/opening_explorer/opening_explorer_screen_test.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/view/opening_explorer/opening_explorer_screen_test.dart b/test/view/opening_explorer/opening_explorer_screen_test.dart index 490e7b141f..df7d23218a 100644 --- a/test/view/opening_explorer/opening_explorer_screen_test.dart +++ b/test/view/opening_explorer/opening_explorer_screen_test.dart @@ -11,14 +11,13 @@ import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_preferences.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_screen.dart'; -import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_widget.dart'; import '../../test_app.dart'; import '../../test_utils.dart'; void main() { final explorerViewFinder = find.descendant( - of: find.byType(OpeningExplorerWidget), + of: find.byType(LayoutBuilder), matching: find.byType(Scrollable), ); From 9aa05e3127e03c6406d45e2dfa3919b15066b476 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 11 Sep 2024 16:07:28 +0200 Subject: [PATCH 325/979] Log requests, ensure they are not sent twice --- lib/src/model/common/http.dart | 16 +- .../opening_explorer_screen.dart | 272 ++++++++---------- 2 files changed, 142 insertions(+), 146 deletions(-) diff --git a/lib/src/model/common/http.dart b/lib/src/model/common/http.dart index ecf3f5e0cc..2a05a871a8 100644 --- a/lib/src/model/common/http.dart +++ b/lib/src/model/common/http.dart @@ -11,6 +11,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:http/http.dart' show + BaseClient, BaseRequest, BaseResponse, Client, @@ -74,7 +75,7 @@ Client httpClientFactory() { /// Only one instance of this client is created and kept alive for the whole app. @Riverpod(keepAlive: true) Client defaultClient(DefaultClientRef ref) { - final client = httpClientFactory(); + final client = LoggingClient(httpClientFactory()); ref.onDispose(() => client.close()); return client; } @@ -132,6 +133,19 @@ String makeUserAgent( return base; } +/// A [Client] that logs all requests. +class LoggingClient extends BaseClient { + LoggingClient(this._inner); + + final Client _inner; + + @override + Future send(BaseRequest request) { + _logger.info('${request.method} ${request.url}'); + return _inner.send(request); + } +} + /// Lichess HTTP client. /// /// - All requests go to the lichess server, defined in [kLichessHost]. diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index e43c3e9f67..6597718109 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -34,6 +34,7 @@ const _kTableRowPadding = EdgeInsets.symmetric( horizontal: _kTableRowHorizontalPadding, vertical: _kTableRowVerticalPadding, ); +const _kTabletBoardRadius = BorderRadius.all(Radius.circular(4.0)); Color _whiteBoxColor(BuildContext context) => Theme.of(context).brightness == Brightness.dark @@ -45,50 +46,17 @@ Color _blackBoxColor(BuildContext context) => ? Colors.black.withOpacity(0.7) : Colors.black; -class OpeningExplorerScreen extends ConsumerWidget { +class OpeningExplorerScreen extends ConsumerStatefulWidget { const OpeningExplorerScreen({required this.pgn, required this.options}); final String pgn; final AnalysisOptions options; @override - Widget build(BuildContext context, WidgetRef ref) { - final fen = ref.watch( - analysisControllerProvider(pgn, options) - .select((value) => value.currentNode.position.fen), - ); - final isIndexing = - ref.watch(openingExplorerProvider(fen: fen)).valueOrNull?.isIndexing ?? - false; - - return PlatformScaffold( - appBar: PlatformAppBar( - title: Text(context.l10n.openingExplorer), - actions: [ - if (isIndexing) const _IndexingIndicator(), - ], - ), - body: _Body(pgn: pgn, options: options), - ); - } + ConsumerState createState() => _OpeningExplorerState(); } -const _kTabletBoardRadius = BorderRadius.all(Radius.circular(4.0)); - -class _Body extends ConsumerStatefulWidget { - const _Body({ - required this.pgn, - required this.options, - }); - - final String pgn; - final AnalysisOptions options; - - @override - ConsumerState<_Body> createState() => _OpeningExplorerState(); -} - -class _OpeningExplorerState extends ConsumerState<_Body> { +class _OpeningExplorerState extends ConsumerState { final Map cache = {}; /// Last explorer content that was successfully loaded. This is used to @@ -151,6 +119,7 @@ class _OpeningExplorerState extends ConsumerState<_Body> { pgn: widget.pgn, options: widget.options, isLoading: false, + isIndexing: false, children: [ openingHeader, _OpeningExplorerMoveTable.maxDepth( @@ -168,6 +137,7 @@ class _OpeningExplorerState extends ConsumerState<_Body> { pgn: widget.pgn, options: widget.options, isLoading: false, + isIndexing: false, children: [ openingHeader, const Padding( @@ -210,6 +180,7 @@ class _OpeningExplorerState extends ConsumerState<_Body> { pgn: widget.pgn, options: widget.options, isLoading: isLoading, + isIndexing: openingExplorerAsync.value?.isIndexing ?? false, children: openingExplorerAsync.when( data: (openingExplorer) { if (openingExplorer == null) { @@ -325,12 +296,14 @@ class _OpeningExplorerView extends StatelessWidget { required this.options, required this.children, required this.isLoading, + required this.isIndexing, }); final String pgn; final AnalysisOptions options; final List children; final bool isLoading; + final bool isIndexing; @override Widget build(BuildContext context) { @@ -339,123 +312,132 @@ class _OpeningExplorerView extends StatelessWidget { ? Colors.black : Colors.white; - return SafeArea( - bottom: false, - child: Column( - children: [ - Padding( - padding: isTablet - ? const EdgeInsets.symmetric( - horizontal: kTabletBoardTableSidePadding, - ) - : EdgeInsets.zero, - child: _MoveList(pgn: pgn, options: options), - ), - Expanded( - child: LayoutBuilder( - builder: (context, constraints) { - final aspectRatio = constraints.biggest.aspectRatio; - final defaultBoardSize = constraints.biggest.shortestSide; - final remainingHeight = - constraints.maxHeight - defaultBoardSize; - final isSmallScreen = - remainingHeight < kSmallRemainingHeightLeftBoardThreshold; - final boardSize = isTablet || isSmallScreen - ? defaultBoardSize - kTabletBoardTableSidePadding * 2 - : defaultBoardSize; - - final isLandscape = aspectRatio > 1; - - final loadingOverlay = Positioned.fill( - child: IgnorePointer( - ignoring: !isLoading, - child: AnimatedOpacity( - duration: const Duration(milliseconds: 300), - curve: Curves.fastOutSlowIn, - opacity: isLoading ? 0.3 : 0.0, - child: ColoredBox(color: loadingOverlayColor), - ), - ), - ); - - if (isLandscape) { - return Row( - mainAxisSize: MainAxisSize.max, - children: [ - Padding( - padding: const EdgeInsets.only( - left: kTabletBoardTableSidePadding, - top: kTabletBoardTableSidePadding, - bottom: kTabletBoardTableSidePadding, - ), - child: AnalysisBoard( - pgn, - options, - boardSize, - borderRadius: isTablet ? _kTabletBoardRadius : null, - ), - ), - Flexible( - fit: FlexFit.loose, - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Expanded( - child: PlatformCard( - clipBehavior: Clip.hardEdge, - borderRadius: const BorderRadius.all( - Radius.circular(4.0), - ), - margin: const EdgeInsets.all( - kTabletBoardTableSidePadding, - ), - semanticContainer: false, - child: Stack( - children: [ - ListView( - padding: EdgeInsets.zero, - children: children, - ), - loadingOverlay, - ], - ), - ), - ), - ], - ), + return PlatformScaffold( + appBar: PlatformAppBar( + title: Text(context.l10n.openingExplorer), + actions: [ + if (isIndexing) const _IndexingIndicator(), + ], + ), + body: SafeArea( + bottom: false, + child: Column( + children: [ + Padding( + padding: isTablet + ? const EdgeInsets.symmetric( + horizontal: kTabletBoardTableSidePadding, + ) + : EdgeInsets.zero, + child: _MoveList(pgn: pgn, options: options), + ), + Expanded( + child: LayoutBuilder( + builder: (context, constraints) { + final aspectRatio = constraints.biggest.aspectRatio; + final defaultBoardSize = constraints.biggest.shortestSide; + final remainingHeight = + constraints.maxHeight - defaultBoardSize; + final isSmallScreen = + remainingHeight < kSmallRemainingHeightLeftBoardThreshold; + final boardSize = isTablet || isSmallScreen + ? defaultBoardSize - kTabletBoardTableSidePadding * 2 + : defaultBoardSize; + + final isLandscape = aspectRatio > 1; + + final loadingOverlay = Positioned.fill( + child: IgnorePointer( + ignoring: !isLoading, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 300), + curve: Curves.fastOutSlowIn, + opacity: isLoading ? 0.3 : 0.0, + child: ColoredBox(color: loadingOverlayColor), ), - ], + ), ); - } else { - return Stack( - children: [ - ListView( - padding: isTablet - ? const EdgeInsets.only( - left: kTabletBoardTableSidePadding, - right: kTabletBoardTableSidePadding, - ) - : EdgeInsets.zero, - children: [ - AnalysisBoard( + + if (isLandscape) { + return Row( + mainAxisSize: MainAxisSize.max, + children: [ + Padding( + padding: const EdgeInsets.only( + left: kTabletBoardTableSidePadding, + top: kTabletBoardTableSidePadding, + bottom: kTabletBoardTableSidePadding, + ), + child: AnalysisBoard( pgn, options, boardSize, borderRadius: isTablet ? _kTabletBoardRadius : null, - disableDraggingPieces: true, ), - ...children, - ], - ), - loadingOverlay, - ], - ); - } - }, + ), + Flexible( + fit: FlexFit.loose, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Expanded( + child: PlatformCard( + clipBehavior: Clip.hardEdge, + borderRadius: const BorderRadius.all( + Radius.circular(4.0), + ), + margin: const EdgeInsets.all( + kTabletBoardTableSidePadding, + ), + semanticContainer: false, + child: Stack( + children: [ + ListView( + padding: EdgeInsets.zero, + children: children, + ), + loadingOverlay, + ], + ), + ), + ), + ], + ), + ), + ], + ); + } else { + return Stack( + children: [ + ListView( + padding: isTablet + ? const EdgeInsets.only( + left: kTabletBoardTableSidePadding, + right: kTabletBoardTableSidePadding, + ) + : EdgeInsets.zero, + children: [ + AnalysisBoard( + pgn, + options, + boardSize, + borderRadius: + isTablet ? _kTabletBoardRadius : null, + disableDraggingPieces: true, + ), + ...children, + ], + ), + loadingOverlay, + ], + ); + } + }, + ), ), - ), - _BottomBar(pgn: pgn, options: options), - ], + _BottomBar(pgn: pgn, options: options), + ], + ), ), ); } From 6398fcf595ee1571e3a42d2c2358ec21a7c92cd8 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 11 Sep 2024 16:13:31 +0200 Subject: [PATCH 326/979] Don't show from position icon in analysis title --- lib/src/view/analysis/analysis_screen.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index 90edb335f3..88c11421c4 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -186,12 +186,14 @@ class _Title extends StatelessWidget { const _Title({required this.options}); final AnalysisOptions options; + static const excludedIcons = [Variant.standard, Variant.fromPosition]; + @override Widget build(BuildContext context) { return Row( mainAxisSize: MainAxisSize.min, children: [ - if (options.variant != Variant.standard) ...[ + if (!excludedIcons.contains(options.variant)) ...[ Icon(options.variant.icon), const SizedBox(width: 5.0), ], From 342980d74452deb39935551687ce42e53a256687 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 11 Sep 2024 16:16:56 +0200 Subject: [PATCH 327/979] Tweak explorer tablet style --- lib/src/view/opening_explorer/opening_explorer_screen.dart | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index 6597718109..fd7b3b9003 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -411,9 +411,8 @@ class _OpeningExplorerView extends StatelessWidget { children: [ ListView( padding: isTablet - ? const EdgeInsets.only( - left: kTabletBoardTableSidePadding, - right: kTabletBoardTableSidePadding, + ? const EdgeInsets.symmetric( + horizontal: kTabletBoardTableSidePadding, ) : EdgeInsets.zero, children: [ @@ -421,8 +420,6 @@ class _OpeningExplorerView extends StatelessWidget { pgn, options, boardSize, - borderRadius: - isTablet ? _kTabletBoardRadius : null, disableDraggingPieces: true, ), ...children, From fc3e691f78cb8d0e488c2f0ad7baeccd9ff0ba8e Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 11 Sep 2024 16:35:26 +0200 Subject: [PATCH 328/979] Tweak explorer indexing indicator --- lib/src/view/opening_explorer/opening_explorer_screen.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index fd7b3b9003..6379ceb8de 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -472,10 +472,10 @@ class _IndexingIndicatorState extends State<_IndexingIndicator> @override Widget build(BuildContext context) { return Padding( - padding: const EdgeInsets.all(6.0), + padding: const EdgeInsets.all(10.0), child: SizedBox( - width: 10, - height: 10, + width: 16, + height: 16, child: CircularProgressIndicator.adaptive( value: controller.value, // TODO: l10n From b8d97c1d24c959fbf0c70a5ba42d7c3544a4799c Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 11 Sep 2024 16:35:47 +0200 Subject: [PATCH 329/979] Bump version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index ab8d2497a5..da0ea032b9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: lichess_mobile description: Lichess mobile app V2 publish_to: "none" -version: 0.10.3+001003 # see README.md for details about versioning +version: 0.10.4+001004 # see README.md for details about versioning environment: sdk: ">=3.3.0 <4.0.0" From e37acbf183803b8abdb0215ca1780461b9e43c45 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 11 Sep 2024 17:07:45 +0200 Subject: [PATCH 330/979] Fix conflicts after merging --- lib/src/view/game/game_result_dialog.dart | 1 - lib/src/widgets/countdown_clock.dart | 130 +++++++++++----------- 2 files changed, 66 insertions(+), 65 deletions(-) diff --git a/lib/src/view/game/game_result_dialog.dart b/lib/src/view/game/game_result_dialog.dart index 0846619c79..4f4886159e 100644 --- a/lib/src/view/game/game_result_dialog.dart +++ b/lib/src/view/game/game_result_dialog.dart @@ -349,7 +349,6 @@ class OverTheBoardGameResultDialog extends StatelessWidget { orientation: Side.white, id: standaloneAnalysisId, ), - title: context.l10n.gameAnalysis, ), ); }, diff --git a/lib/src/widgets/countdown_clock.dart b/lib/src/widgets/countdown_clock.dart index 84acf10055..ac6985d8c1 100644 --- a/lib/src/widgets/countdown_clock.dart +++ b/lib/src/widgets/countdown_clock.dart @@ -6,7 +6,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; -import 'package:lichess_mobile/src/model/settings/brightness.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; part 'countdown_clock.freezed.dart'; @@ -158,12 +157,14 @@ class _CountdownClockState extends ConsumerState { @override Widget build(BuildContext context) { - final brightness = ref.watch(currentBrightnessProvider); - return Clock( - timeLeft: timeLeft, - active: widget.active, - emergencyThreshold: widget.emergencyThreshold, - clockStyle: getStyle(brightness), + final brightness = Theme.of(context).brightness; + return RepaintBoundary( + child: Clock( + timeLeft: timeLeft, + active: widget.active, + emergencyThreshold: widget.emergencyThreshold, + clockStyle: getStyle(brightness), + ), ); } } @@ -172,6 +173,15 @@ class _CountdownClockState extends ConsumerState { /// /// For a clock widget that automatically counts down, see [CountdownClock]. class Clock extends StatelessWidget { + const Clock({ + required this.timeLeft, + required this.active, + required this.clockStyle, + this.emergencyThreshold, + this.padLeft = false, + super.key, + }); + /// The time left to be displayed on the clock. final Duration timeLeft; @@ -185,13 +195,8 @@ class Clock extends StatelessWidget { /// Clock style to use. final ClockStyle clockStyle; - const Clock({ - required this.timeLeft, - required this.active, - required this.clockStyle, - this.emergencyThreshold, - super.key, - }); + /// Whether to pad with a leading zero (default is `false`). + final bool padLeft; @override Widget build(BuildContext context) { @@ -204,59 +209,56 @@ class Clock extends StatelessWidget { final remainingHeight = estimateRemainingHeightLeftBoard(context); final hoursDisplay = - widget.padLeft ? hours.toString().padLeft(2, '0') : hours.toString(); + padLeft ? hours.toString().padLeft(2, '0') : hours.toString(); final minsDisplay = - widget.padLeft ? mins.toString().padLeft(2, '0') : mins.toString(); - - return RepaintBoundary( - child: Container( - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(5.0)), - color: active - ? isEmergency - ? clockStyle.emergencyBackgroundColor - : clockStyle.activeBackgroundColor - : clockStyle.backgroundColor, - ), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 3.0, horizontal: 5.0), - child: MediaQuery.withClampedTextScaling( - maxScaleFactor: kMaxClockTextScaleFactor, - child: RichText( - text: TextSpan( - text: hours > 0 - ? '$hoursDisplay:${mins.toString().padLeft(2, '0')}:$secs' - : '$minsDisplay:$secs', - style: TextStyle( - color: active - ? isEmergency - ? clockStyle.emergencyTextColor - : clockStyle.activeTextColor - : clockStyle.textColor, - fontSize: 26, - height: - remainingHeight < kSmallRemainingHeightLeftBoardThreshold - ? 1.0 - : null, - fontFeatures: const [ - FontFeature.tabularFigures(), - ], - ), - children: [ - if (showTenths) - TextSpan( - text: - '.${timeLeft.inMilliseconds.remainder(1000) ~/ 100}', - style: const TextStyle(fontSize: 20), - ), - if (!active && timeLeft < const Duration(seconds: 1)) - TextSpan( - text: - '${timeLeft.inMilliseconds.remainder(1000) ~/ 10 % 10}', - style: const TextStyle(fontSize: 18), - ), + padLeft ? mins.toString().padLeft(2, '0') : mins.toString(); + + return Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(5.0)), + color: active + ? isEmergency + ? clockStyle.emergencyBackgroundColor + : clockStyle.activeBackgroundColor + : clockStyle.backgroundColor, + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 3.0, horizontal: 5.0), + child: MediaQuery.withClampedTextScaling( + maxScaleFactor: kMaxClockTextScaleFactor, + child: RichText( + text: TextSpan( + text: hours > 0 + ? '$hoursDisplay:${mins.toString().padLeft(2, '0')}:$secs' + : '$minsDisplay:$secs', + style: TextStyle( + color: active + ? isEmergency + ? clockStyle.emergencyTextColor + : clockStyle.activeTextColor + : clockStyle.textColor, + fontSize: 26, + height: + remainingHeight < kSmallRemainingHeightLeftBoardThreshold + ? 1.0 + : null, + fontFeatures: const [ + FontFeature.tabularFigures(), ], ), + children: [ + if (showTenths) + TextSpan( + text: '.${timeLeft.inMilliseconds.remainder(1000) ~/ 100}', + style: const TextStyle(fontSize: 20), + ), + if (!active && timeLeft < const Duration(seconds: 1)) + TextSpan( + text: + '${timeLeft.inMilliseconds.remainder(1000) ~/ 10 % 10}', + style: const TextStyle(fontSize: 18), + ), + ], ), ), ), From c2ca8ca6b1226d55a6794d489ca80e99fb71b57c Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 11 Sep 2024 17:13:30 +0200 Subject: [PATCH 331/979] Restrict variants and style fixes --- .../configure_over_the_board_game.dart | 220 ++++++++---------- .../over_the_board/over_the_board_screen.dart | 2 +- 2 files changed, 99 insertions(+), 123 deletions(-) diff --git a/lib/src/view/over_the_board/configure_over_the_board_game.dart b/lib/src/view/over_the_board/configure_over_the_board_game.dart index 2ec3d0bc3b..02e95c4588 100644 --- a/lib/src/view/over_the_board/configure_over_the_board_game.dart +++ b/lib/src/view/over_the_board/configure_over_the_board_game.dart @@ -77,108 +77,95 @@ class _ConfigureOverTheBoardGameSheetState @override Widget build(BuildContext context) { - return SafeArea( - child: Padding( - padding: Styles.bodyPadding, - child: ListView( - shrinkWrap: true, - children: [ - ListSection( - header: const SettingsSectionTitle('Start new game'), - hasLeading: false, + return BottomSheetScrollableContainer( + padding: Styles.bodyPadding, + children: [ + SettingsListTile( + settingsLabel: Text(context.l10n.variant), + settingsValue: chosenVariant.label, + onTap: () { + showChoicePicker( + context, + choices: playSupportedVariants + .where( + (variant) => variant != Variant.fromPosition, + ) + .toList(), + selectedItem: chosenVariant, + labelBuilder: (Variant variant) => Text(variant.label), + onSelectedItemChanged: (Variant variant) => setState(() { + chosenVariant = variant; + }), + ); + }, + ), + PlatformListTile( + title: Text.rich( + TextSpan( + text: '${context.l10n.minutesPerSide}: ', children: [ - SettingsListTile( - settingsLabel: Text(context.l10n.variant), - settingsValue: chosenVariant.label, - onTap: () { - showChoicePicker( - context, - choices: Variant.values - .where( - (variant) => variant != Variant.fromPosition, - ) - .toList(), - selectedItem: chosenVariant, - labelBuilder: (Variant variant) => Text(variant.label), - onSelectedItemChanged: (Variant variant) => setState(() { - chosenVariant = variant; - }), - ); - }, - ), - PlatformListTile( - title: Text.rich( - TextSpan( - text: '${context.l10n.minutesPerSide}: ', - children: [ - TextSpan( - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - ), - text: clockLabelInMinutes(timeIncrement.time), - ), - ], - ), - ), - subtitle: NonLinearSlider( - value: timeIncrement.time, - values: kAvailableTimesInSeconds, - labelBuilder: clockLabelInMinutes, - onChange: Theme.of(context).platform == TargetPlatform.iOS - ? _setTotalTime - : null, - onChangeEnd: _setTotalTime, + TextSpan( + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, ), + text: clockLabelInMinutes(timeIncrement.time), ), - PlatformListTile( - title: Text.rich( - TextSpan( - text: '${context.l10n.incrementInSeconds}: ', - children: [ - TextSpan( - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - ), - text: timeIncrement.increment.toString(), - ), - ], - ), - ), - subtitle: NonLinearSlider( - value: timeIncrement.increment, - values: kAvailableIncrementsInSeconds, - onChange: Theme.of(context).platform == TargetPlatform.iOS - ? _setIncrement - : null, - onChangeEnd: _setIncrement, + ], + ), + ), + subtitle: NonLinearSlider( + value: timeIncrement.time, + values: kAvailableTimesInSeconds, + labelBuilder: clockLabelInMinutes, + onChange: Theme.of(context).platform == TargetPlatform.iOS + ? _setTotalTime + : null, + onChangeEnd: _setTotalTime, + ), + ), + PlatformListTile( + title: Text.rich( + TextSpan( + text: '${context.l10n.incrementInSeconds}: ', + children: [ + TextSpan( + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, ), + text: timeIncrement.increment.toString(), ), ], ), - SecondaryButton( - onPressed: () { - ref - .read(overTheBoardClockProvider.notifier) - .setupClock(timeIncrement); - ref - .read(overTheBoardGameControllerProvider.notifier) - .startNewGame( - chosenVariant, - timeIncrement, - ); - Navigator.pop(context); - }, - semanticsLabel: context.l10n.play, - child: Text( - context.l10n.play, - style: Styles.bold, - ), - ), - ], + ), + subtitle: NonLinearSlider( + value: timeIncrement.increment, + values: kAvailableIncrementsInSeconds, + onChange: Theme.of(context).platform == TargetPlatform.iOS + ? _setIncrement + : null, + onChangeEnd: _setIncrement, + ), ), - ), + SecondaryButton( + onPressed: () { + ref + .read(overTheBoardClockProvider.notifier) + .setupClock(timeIncrement); + ref.read(overTheBoardGameControllerProvider.notifier).startNewGame( + chosenVariant, + timeIncrement, + ); + Navigator.pop(context); + }, + semanticsLabel: context.l10n.play, + child: Text( + context.l10n.play, + style: Styles.bold, + ), + ), + ], ); } } @@ -201,34 +188,23 @@ class OverTheBoardDisplaySettings extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final prefs = ref.watch(overTheBoardPreferencesProvider); - return DraggableScrollableSheet( - initialChildSize: 1.0, - expand: false, - builder: (context, scrollController) => ListView( - controller: scrollController, - children: [ - ListSection( - header: SettingsSectionTitle(context.l10n.settingsSettings), - hasLeading: false, - children: [ - SwitchSettingTile( - title: const Text('Use symmetric pieces'), - value: prefs.symmetricPieces, - onChanged: (_) => ref - .read(overTheBoardPreferencesProvider.notifier) - .toggleSymmetricPieces(), - ), - SwitchSettingTile( - title: const Text('Flip pieces and oponent info after move'), - value: prefs.flipPiecesAfterMove, - onChanged: (_) => ref - .read(overTheBoardPreferencesProvider.notifier) - .toggleFlipPiecesAfterMove(), - ), - ], - ), - ], - ), + return BottomSheetScrollableContainer( + children: [ + SwitchSettingTile( + title: const Text('Use symmetric pieces'), + value: prefs.symmetricPieces, + onChanged: (_) => ref + .read(overTheBoardPreferencesProvider.notifier) + .toggleSymmetricPieces(), + ), + SwitchSettingTile( + title: const Text('Flip pieces and oponent info after move'), + value: prefs.flipPiecesAfterMove, + onChanged: (_) => ref + .read(overTheBoardPreferencesProvider.notifier) + .toggleFlipPiecesAfterMove(), + ), + ], ); } } diff --git a/lib/src/view/over_the_board/over_the_board_screen.dart b/lib/src/view/over_the_board/over_the_board_screen.dart index 2a87bb9cf6..384091c939 100644 --- a/lib/src/view/over_the_board/over_the_board_screen.dart +++ b/lib/src/view/over_the_board/over_the_board_screen.dart @@ -204,7 +204,7 @@ class _BottomBar extends ConsumerWidget { BottomBarButton( label: 'Configure game', onTap: () => showConfigureGameSheet(context, isDismissible: true), - icon: Icons.add_circle, + icon: Icons.add, ), BottomBarButton( key: const Key('flip-button'), From 40f2d8c85c051189b62e8a8900b43fac0d4271e0 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 11 Sep 2024 17:17:22 +0200 Subject: [PATCH 332/979] Bump version 0.11.0 --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index da0ea032b9..487df6e6db 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: lichess_mobile description: Lichess mobile app V2 publish_to: "none" -version: 0.10.4+001004 # see README.md for details about versioning +version: 0.11.0+001100 # see README.md for details about versioning environment: sdk: ">=3.3.0 <4.0.0" From 5f4246277f9f4d3e5a4fa6c60e1e4f61bc58126a Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 12 Sep 2024 08:54:21 +0200 Subject: [PATCH 333/979] Improve explorer app bars --- .../opening_explorer_screen.dart | 243 ++++++++++-------- lib/src/widgets/move_list.dart | 8 +- 2 files changed, 137 insertions(+), 114 deletions(-) diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index 6379ceb8de..eb93249a33 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -11,6 +11,7 @@ import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_preferences.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_repository.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; @@ -22,7 +23,6 @@ import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/move_list.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; -import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -312,17 +312,11 @@ class _OpeningExplorerView extends StatelessWidget { ? Colors.black : Colors.white; - return PlatformScaffold( - appBar: PlatformAppBar( - title: Text(context.l10n.openingExplorer), - actions: [ - if (isIndexing) const _IndexingIndicator(), - ], - ), - body: SafeArea( - bottom: false, - child: Column( - children: [ + final body = SafeArea( + bottom: false, + child: Column( + children: [ + if (Theme.of(context).platform == TargetPlatform.iOS) Padding( padding: isTablet ? const EdgeInsets.symmetric( @@ -331,110 +325,127 @@ class _OpeningExplorerView extends StatelessWidget { : EdgeInsets.zero, child: _MoveList(pgn: pgn, options: options), ), - Expanded( - child: LayoutBuilder( - builder: (context, constraints) { - final aspectRatio = constraints.biggest.aspectRatio; - final defaultBoardSize = constraints.biggest.shortestSide; - final remainingHeight = - constraints.maxHeight - defaultBoardSize; - final isSmallScreen = - remainingHeight < kSmallRemainingHeightLeftBoardThreshold; - final boardSize = isTablet || isSmallScreen - ? defaultBoardSize - kTabletBoardTableSidePadding * 2 - : defaultBoardSize; - - final isLandscape = aspectRatio > 1; - - final loadingOverlay = Positioned.fill( - child: IgnorePointer( - ignoring: !isLoading, - child: AnimatedOpacity( - duration: const Duration(milliseconds: 300), - curve: Curves.fastOutSlowIn, - opacity: isLoading ? 0.3 : 0.0, - child: ColoredBox(color: loadingOverlayColor), - ), + Expanded( + child: LayoutBuilder( + builder: (context, constraints) { + final aspectRatio = constraints.biggest.aspectRatio; + final defaultBoardSize = constraints.biggest.shortestSide; + final remainingHeight = + constraints.maxHeight - defaultBoardSize; + final isSmallScreen = + remainingHeight < kSmallRemainingHeightLeftBoardThreshold; + final boardSize = isTablet || isSmallScreen + ? defaultBoardSize - kTabletBoardTableSidePadding * 2 + : defaultBoardSize; + + final isLandscape = aspectRatio > 1; + + final loadingOverlay = Positioned.fill( + child: IgnorePointer( + ignoring: !isLoading, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 300), + curve: Curves.fastOutSlowIn, + opacity: isLoading ? 0.3 : 0.0, + child: ColoredBox(color: loadingOverlayColor), ), - ); - - if (isLandscape) { - return Row( - mainAxisSize: MainAxisSize.max, - children: [ - Padding( - padding: const EdgeInsets.only( - left: kTabletBoardTableSidePadding, - top: kTabletBoardTableSidePadding, - bottom: kTabletBoardTableSidePadding, - ), - child: AnalysisBoard( - pgn, - options, - boardSize, - borderRadius: isTablet ? _kTabletBoardRadius : null, - ), + ), + ); + + if (isLandscape) { + return Row( + mainAxisSize: MainAxisSize.max, + children: [ + Padding( + padding: const EdgeInsets.only( + left: kTabletBoardTableSidePadding, + top: kTabletBoardTableSidePadding, + bottom: kTabletBoardTableSidePadding, ), - Flexible( - fit: FlexFit.loose, - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Expanded( - child: PlatformCard( - clipBehavior: Clip.hardEdge, - borderRadius: const BorderRadius.all( - Radius.circular(4.0), - ), - margin: const EdgeInsets.all( - kTabletBoardTableSidePadding, - ), - semanticContainer: false, - child: Stack( - children: [ - ListView( - padding: EdgeInsets.zero, - children: children, - ), - loadingOverlay, - ], - ), - ), - ), - ], - ), + child: AnalysisBoard( + pgn, + options, + boardSize, + borderRadius: isTablet ? _kTabletBoardRadius : null, ), - ], - ); - } else { - return Stack( - children: [ - ListView( - padding: isTablet - ? const EdgeInsets.symmetric( - horizontal: kTabletBoardTableSidePadding, - ) - : EdgeInsets.zero, + ), + Flexible( + fit: FlexFit.loose, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, children: [ - AnalysisBoard( - pgn, - options, - boardSize, - disableDraggingPieces: true, + Expanded( + child: PlatformCard( + clipBehavior: Clip.hardEdge, + borderRadius: const BorderRadius.all( + Radius.circular(4.0), + ), + margin: const EdgeInsets.all( + kTabletBoardTableSidePadding, + ), + semanticContainer: false, + child: Stack( + children: [ + ListView( + padding: EdgeInsets.zero, + children: children, + ), + loadingOverlay, + ], + ), + ), ), - ...children, ], ), - loadingOverlay, - ], - ); - } - }, - ), + ), + ], + ); + } else { + return Stack( + children: [ + ListView( + padding: isTablet + ? const EdgeInsets.symmetric( + horizontal: kTabletBoardTableSidePadding, + ) + : EdgeInsets.zero, + children: [ + AnalysisBoard( + pgn, + options, + boardSize, + disableDraggingPieces: true, + ), + ...children, + ], + ), + loadingOverlay, + ], + ); + } + }, ), - _BottomBar(pgn: pgn, options: options), - ], + ), + _BottomBar(pgn: pgn, options: options), + ], + ), + ); + + return PlatformWidget( + androidBuilder: (_) => Scaffold( + body: body, + appBar: AppBar( + title: Text(context.l10n.openingExplorer), + bottom: _MoveList(pgn: pgn, options: options), + ), + ), + iosBuilder: (_) => CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + middle: Text(context.l10n.openingExplorer), + automaticBackgroundVisibility: false, + border: null, ), + child: body, ), ); } @@ -962,7 +973,7 @@ class _WinPercentageChart extends StatelessWidget { } } -class _MoveList extends ConsumerWidget { +class _MoveList extends ConsumerWidget implements PreferredSizeWidget { const _MoveList({ required this.pgn, required this.options, @@ -971,6 +982,9 @@ class _MoveList extends ConsumerWidget { final String pgn; final AnalysisOptions options; + @override + Size get preferredSize => const Size.fromHeight(40.0); + @override Widget build(BuildContext context, WidgetRef ref) { final ctrlProvider = analysisControllerProvider(pgn, options); @@ -984,8 +998,17 @@ class _MoveList extends ConsumerWidget { final currentMoveIndex = state.currentNode.position.ply; return MoveList( - inlineBackgroundColor: Theme.of(context).colorScheme.surfaceContainer, - inlineColor: Theme.of(context).colorScheme.onSurface, + inlineDecoration: Theme.of(context).platform == TargetPlatform.iOS + ? BoxDecoration( + color: Styles.cupertinoAppBarColor.resolveFrom(context), + border: const Border( + bottom: BorderSide( + color: Color(0x4D000000), + width: 0.0, + ), + ), + ) + : null, type: MoveListType.inline, slicedMoves: slicedMoves, currentMoveIndex: currentMoveIndex, diff --git a/lib/src/widgets/move_list.dart b/lib/src/widgets/move_list.dart index a516ee4ab5..91417c7b24 100644 --- a/lib/src/widgets/move_list.dart +++ b/lib/src/widgets/move_list.dart @@ -20,7 +20,7 @@ class MoveList extends ConsumerStatefulWidget { required this.slicedMoves, required this.currentMoveIndex, this.inlineColor, - this.inlineBackgroundColor, + this.inlineDecoration, this.onSelectMove, }); @@ -28,7 +28,7 @@ class MoveList extends ConsumerStatefulWidget { final Color? inlineColor; - final Color? inlineBackgroundColor; + final BoxDecoration? inlineDecoration; final Iterable>> slicedMoves; @@ -86,7 +86,7 @@ class _MoveListState extends ConsumerState { return widget.type == MoveListType.inline ? Container( - color: widget.inlineBackgroundColor, + decoration: widget.inlineDecoration, padding: const EdgeInsets.only(left: 5), height: _kMoveListHeight, width: double.infinity, @@ -253,7 +253,7 @@ class StackedMoveCount extends StatelessWidget { @override Widget build(BuildContext context) { return SizedBox( - width: _kMoveListHeight, + width: 40.0, child: Text( '$count.', style: TextStyle( From 6d837cffcb49c8b296bb9f41724a09cb464848c6 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 12 Sep 2024 09:25:21 +0200 Subject: [PATCH 334/979] Fix deprecation warnings --- lib/src/styles/styles.dart | 19 +++++------- lib/src/utils/async_value.dart | 1 - lib/src/utils/color_palette.dart | 8 ++--- lib/src/view/analysis/analysis_screen.dart | 4 +-- lib/src/view/analysis/tree_view.dart | 4 +-- .../broadcast/broadcasts_list_screen.dart | 2 +- .../broadcast/default_broadcast_image.dart | 4 +-- .../coordinate_training_screen.dart | 8 ++--- lib/src/view/game/archived_game_screen.dart | 2 +- .../game/correspondence_clock_widget.dart | 2 +- lib/src/view/game/game_result_dialog.dart | 2 +- lib/src/view/game/ping_rating.dart | 4 +-- .../opening_explorer_screen.dart | 4 +-- .../view/play/create_custom_game_screen.dart | 3 +- lib/src/view/play/ongoing_games_screen.dart | 5 ++- lib/src/view/play/quick_game_matrix.dart | 6 ++-- lib/src/view/puzzle/dashboard_screen.dart | 5 +-- .../view/puzzle/puzzle_history_screen.dart | 4 +-- .../view/puzzle/puzzle_session_widget.dart | 2 +- lib/src/view/puzzle/puzzle_tab_screen.dart | 6 ++-- lib/src/view/puzzle/storm_dashboard.dart | 2 +- lib/src/view/puzzle/storm_screen.dart | 31 +++++++++++-------- lib/src/view/puzzle/streak_screen.dart | 6 ++-- lib/src/view/user/perf_stats_screen.dart | 8 ++--- lib/src/view/watch/tv_screen.dart | 2 +- lib/src/widgets/board_carousel_item.dart | 6 ++-- lib/src/widgets/buttons.dart | 4 +-- lib/src/widgets/feedback.dart | 2 +- lib/src/widgets/move_list.dart | 4 +-- lib/src/widgets/platform_scaffold.dart | 2 +- pubspec.lock | 14 ++++----- 31 files changed, 89 insertions(+), 87 deletions(-) diff --git a/lib/src/styles/styles.dart b/lib/src/styles/styles.dart index bbcc12ca30..9216fe6627 100644 --- a/lib/src/styles/styles.dart +++ b/lib/src/styles/styles.dart @@ -211,7 +211,7 @@ abstract class Styles { /// Retrieve the default text color and apply an opacity to it. Color? textShade(BuildContext context, double opacity) => - DefaultTextStyle.of(context).style.color?.withOpacity(opacity); + DefaultTextStyle.of(context).style.color?.withValues(alpha: opacity); Color? dividerColor(BuildContext context) => defaultTargetPlatform == TargetPlatform.iOS @@ -221,21 +221,16 @@ Color? dividerColor(BuildContext context) => Color darken(Color c, [double amount = .1]) { assert(amount >= 0 && amount <= 1); final f = 1 - amount; - return Color.fromARGB( - c.alpha, - (c.red * f).round(), - (c.green * f).round(), - (c.blue * f).round(), - ); + return Color.from(alpha: c.a, red: c.r * f, green: c.g * f, blue: c.b * f); } Color lighten(Color c, [double amount = .1]) { assert(amount >= 0 && amount <= 1); - return Color.fromARGB( - c.alpha, - c.red + ((255 - c.red) * amount).round(), - c.green + ((255 - c.green) * amount).round(), - c.blue + ((255 - c.blue) * amount).round(), + return Color.from( + alpha: c.a, + red: c.r + ((255 - c.r) * amount), + green: c.g + ((255 - c.g) * amount), + blue: c.b + ((255 - c.b) * amount), ); } diff --git a/lib/src/utils/async_value.dart b/lib/src/utils/async_value.dart index 76d71b967b..3dd5db17ca 100644 --- a/lib/src/utils/async_value.dart +++ b/lib/src/utils/async_value.dart @@ -18,7 +18,6 @@ extension AsyncValueUI on AsyncValue { ); default: assert(false, 'Unexpected platform ${Theme.of(context).platform}'); - break; } } } diff --git a/lib/src/utils/color_palette.dart b/lib/src/utils/color_palette.dart index 9d0e53e7ab..4ecbb07e12 100644 --- a/lib/src/utils/color_palette.dart +++ b/lib/src/utils/color_palette.dart @@ -37,13 +37,13 @@ void setCorePalette(CorePalette? palette) { orientation: Side.black, ), lastMove: HighlightDetails( - solidColor: Color(palette.tertiary.get(80)).withOpacity(0.6), + solidColor: Color(palette.tertiary.get(80)).withValues(alpha: 0.6), ), selected: HighlightDetails( - solidColor: Color(palette.neutral.get(40)).withOpacity(0.80), + solidColor: Color(palette.neutral.get(40)).withValues(alpha: 0.80), ), - validMoves: Color(palette.neutral.get(40)).withOpacity(0.40), - validPremoves: Color(palette.error.get(40)).withOpacity(0.30), + validMoves: Color(palette.neutral.get(40)).withValues(alpha: 0.40), + validPremoves: Color(palette.error.get(40)).withValues(alpha: 0.30), ); } } diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index 88c11421c4..dd70a52018 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -1181,7 +1181,7 @@ class AcplChart extends ConsumerWidget { .textTheme .labelMedium ?.color - ?.withOpacity(0.3), + ?.withValues(alpha: 0.3), ), labelResolver: (line) => label, padding: const EdgeInsets.only(right: 1), @@ -1295,7 +1295,7 @@ class AcplChart extends ConsumerWidget { spots: spots, isCurved: false, barWidth: 1, - color: mainLineColor.withOpacity(0.7), + color: mainLineColor.withValues(alpha: 0.7), aboveBarData: BarAreaData( show: true, color: aboveLineColor, diff --git a/lib/src/view/analysis/tree_view.dart b/lib/src/view/analysis/tree_view.dart index 53d2f27995..9662c5410c 100644 --- a/lib/src/view/analysis/tree_view.dart +++ b/lib/src/view/analysis/tree_view.dart @@ -283,12 +283,12 @@ Color? _textColor( int? nag, }) { final defaultColor = Theme.of(context).platform == TargetPlatform.android - ? Theme.of(context).textTheme.bodyLarge?.color?.withOpacity(opacity) + ? Theme.of(context).textTheme.bodyLarge?.color?.withValues(alpha: opacity) : CupertinoTheme.of(context) .textTheme .textStyle .color - ?.withOpacity(opacity); + ?.withValues(alpha: opacity); return nag != null && nag > 0 ? nagColor(nag) : defaultColor; } diff --git a/lib/src/view/broadcast/broadcasts_list_screen.dart b/lib/src/view/broadcast/broadcasts_list_screen.dart index 6f011dbf05..5fa052feb6 100644 --- a/lib/src/view/broadcast/broadcasts_list_screen.dart +++ b/lib/src/view/broadcast/broadcasts_list_screen.dart @@ -203,7 +203,7 @@ class BroadcastGridItem extends StatelessWidget { borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( - color: LichessColors.grey.withOpacity(0.5), + color: LichessColors.grey.withValues(alpha: 0.5), blurRadius: 5, spreadRadius: 1, ), diff --git a/lib/src/view/broadcast/default_broadcast_image.dart b/lib/src/view/broadcast/default_broadcast_image.dart index 223de8bcf3..e4c944db0b 100644 --- a/lib/src/view/broadcast/default_broadcast_image.dart +++ b/lib/src/view/broadcast/default_broadcast_image.dart @@ -24,8 +24,8 @@ class DefaultBroadcastImage extends StatelessWidget { begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ - LichessColors.primary.withOpacity(0.7), - LichessColors.brag.withOpacity(0.7), + LichessColors.primary.withValues(alpha: 0.7), + LichessColors.brag.withValues(alpha: 0.7), ], ), ), diff --git a/lib/src/view/coordinate_training/coordinate_training_screen.dart b/lib/src/view/coordinate_training/coordinate_training_screen.dart index 47663917a7..1d505f409b 100644 --- a/lib/src/view/coordinate_training/coordinate_training_screen.dart +++ b/lib/src/view/coordinate_training/coordinate_training_screen.dart @@ -89,18 +89,14 @@ class _BodyState extends ConsumerState<_Body> { solidColor: (trainingState.lastGuess == Guess.correct ? LichessColors.good : LichessColors.error) - .withOpacity( - 0.5, - ), + .withValues(alpha: 0.5), ), ), }, } else ...{ trainingState.currentCoord!: SquareHighlight( details: HighlightDetails( - solidColor: LichessColors.good.withOpacity( - 0.5, - ), + solidColor: LichessColors.good.withValues(alpha: 0.5), ), ), }, diff --git a/lib/src/view/game/archived_game_screen.dart b/lib/src/view/game/archived_game_screen.dart index c85e19dea8..d9418fec6a 100644 --- a/lib/src/view/game/archived_game_screen.dart +++ b/lib/src/view/game/archived_game_screen.dart @@ -135,7 +135,7 @@ class _Body extends StatelessWidget { actions: [ if (gameData == null && error == null) const PlatformAppBarLoadingIndicator(), - ToggleSoundButton(), + const ToggleSoundButton(), ], ), body: SafeArea( diff --git a/lib/src/view/game/correspondence_clock_widget.dart b/lib/src/view/game/correspondence_clock_widget.dart index eb7c07ae11..418162624f 100644 --- a/lib/src/view/game/correspondence_clock_widget.dart +++ b/lib/src/view/game/correspondence_clock_widget.dart @@ -149,7 +149,7 @@ class _CorrespondenceClockState extends ConsumerState { style: TextStyle( color: widget.active && timeLeft.inSeconds.remainder(2) == 0 - ? clockStyle.activeTextColor.withOpacity(0.5) + ? clockStyle.activeTextColor.withValues(alpha: 0.5) : null, ), ), diff --git a/lib/src/view/game/game_result_dialog.dart b/lib/src/view/game/game_result_dialog.dart index 4f4886159e..029ff5af77 100644 --- a/lib/src/view/game/game_result_dialog.dart +++ b/lib/src/view/game/game_result_dialog.dart @@ -262,7 +262,7 @@ class _AcplChart extends StatelessWidget { LineChartBarData( spots: spots, isCurved: true, - color: mainLineColor.withOpacity(0.3), + color: mainLineColor.withValues(alpha: 0.3), barWidth: 1, aboveBarData: BarAreaData( show: true, diff --git a/lib/src/view/game/ping_rating.dart b/lib/src/view/game/ping_rating.dart index e5297b7ce5..65125d8d82 100644 --- a/lib/src/view/game/ping_rating.dart +++ b/lib/src/view/game/ping_rating.dart @@ -68,8 +68,8 @@ class PingRating extends ConsumerWidget { ? CupertinoDynamicColor.resolve( CupertinoColors.systemGrey, context, - ).withOpacity(0.2) - : Colors.grey.withOpacity(0.2), + ).withValues(alpha: 0.2) + : Colors.grey.withValues(alpha: 0.2), levels: Theme.of(context).platform == TargetPlatform.iOS ? cupertinoLevels : materialLevels, diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index eb93249a33..47ef21e9d3 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -38,12 +38,12 @@ const _kTabletBoardRadius = BorderRadius.all(Radius.circular(4.0)); Color _whiteBoxColor(BuildContext context) => Theme.of(context).brightness == Brightness.dark - ? Colors.white.withOpacity(0.8) + ? Colors.white.withValues(alpha: 0.8) : Colors.white; Color _blackBoxColor(BuildContext context) => Theme.of(context).brightness == Brightness.light - ? Colors.black.withOpacity(0.7) + ? Colors.black.withValues(alpha: 0.7) : Colors.black; class OpeningExplorerScreen extends ConsumerStatefulWidget { diff --git a/lib/src/view/play/create_custom_game_screen.dart b/lib/src/view/play/create_custom_game_screen.dart index fe7c97f903..bdde81cc24 100644 --- a/lib/src/view/play/create_custom_game_screen.dart +++ b/lib/src/view/play/create_custom_game_screen.dart @@ -243,7 +243,8 @@ class _ChallengesBodyState extends ConsumerState<_ChallengesBody> { UserId.fromUserName(challenge.username) == session?.user.id; return Container( - color: isMySeek ? LichessColors.green.withOpacity(0.2) : null, + color: + isMySeek ? LichessColors.green.withValues(alpha: 0.2) : null, child: Slidable( endActionPane: isMySeek ? ActionPane( diff --git a/lib/src/view/play/ongoing_games_screen.dart b/lib/src/view/play/ongoing_games_screen.dart index 0318403c1b..7856985951 100644 --- a/lib/src/view/play/ongoing_games_screen.dart +++ b/lib/src/view/play/ongoing_games_screen.dart @@ -84,7 +84,10 @@ class OngoingGamePreview extends ConsumerWidget { Icon( game.perf.icon, size: 34, - color: DefaultTextStyle.of(context).style.color?.withOpacity(0.6), + color: DefaultTextStyle.of(context) + .style + .color + ?.withValues(alpha: 0.6), ), if (game.secondsLeft != null && game.secondsLeft! > 0) Text( diff --git a/lib/src/view/play/quick_game_matrix.dart b/lib/src/view/play/quick_game_matrix.dart index 970485e734..0a49662d6d 100644 --- a/lib/src/view/play/quick_game_matrix.dart +++ b/lib/src/view/play/quick_game_matrix.dart @@ -162,8 +162,8 @@ class _ChoiceChipState extends State<_ChoiceChip> { @override Widget build(BuildContext context) { final cardColor = Theme.of(context).platform == TargetPlatform.iOS - ? Styles.cupertinoCardColor.resolveFrom(context).withOpacity(0.7) - : Theme.of(context).colorScheme.surfaceContainer.withOpacity(0.7); + ? Styles.cupertinoCardColor.resolveFrom(context).withValues(alpha: 0.7) + : Theme.of(context).colorScheme.surfaceContainer.withValues(alpha: 0.7); return Container( decoration: BoxDecoration( @@ -173,7 +173,7 @@ class _ChoiceChipState extends State<_ChoiceChip> { child: AdaptiveInkWell( borderRadius: const BorderRadius.all(Radius.circular(5.0)), onTap: () => widget.onSelected(true), - splashColor: Theme.of(context).primaryColor.withOpacity(0.2), + splashColor: Theme.of(context).primaryColor.withValues(alpha: 0.2), child: Padding( padding: const EdgeInsets.symmetric(vertical: 16.0), child: Center(child: widget.label), diff --git a/lib/src/view/puzzle/dashboard_screen.dart b/lib/src/view/puzzle/dashboard_screen.dart index 2ad8ff23e8..c890efebf5 100644 --- a/lib/src/view/puzzle/dashboard_screen.dart +++ b/lib/src/view/puzzle/dashboard_screen.dart @@ -189,7 +189,8 @@ class PuzzleChart extends StatelessWidget { @override Widget build(BuildContext context) { - final radarColor = Theme.of(context).colorScheme.onSurface.withOpacity(0.5); + final radarColor = + Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5); final chartColor = Theme.of(context).colorScheme.tertiary; return RadarChart( RadarChartData( @@ -199,7 +200,7 @@ class PuzzleChart extends StatelessWidget { radarShape: RadarShape.polygon, dataSets: [ RadarDataSet( - fillColor: chartColor.withOpacity(0.2), + fillColor: chartColor.withValues(alpha: 0.2), borderColor: chartColor, dataEntries: puzzleData .map((theme) => RadarEntry(value: theme.performance.toDouble())) diff --git a/lib/src/view/puzzle/puzzle_history_screen.dart b/lib/src/view/puzzle/puzzle_history_screen.dart index bf3282aec2..9fbbab7774 100644 --- a/lib/src/view/puzzle/puzzle_history_screen.dart +++ b/lib/src/view/puzzle/puzzle_history_screen.dart @@ -246,8 +246,8 @@ class _PuzzleResult extends StatelessWidget { Widget build(BuildContext context) { return ColoredBox( color: entry.win - ? context.lichessColors.good.withOpacity(0.7) - : context.lichessColors.error.withOpacity(0.7), + ? context.lichessColors.good.withValues(alpha: 0.7) + : context.lichessColors.error.withValues(alpha: 0.7), child: Padding( padding: const EdgeInsets.symmetric( vertical: 1, diff --git a/lib/src/view/puzzle/puzzle_session_widget.dart b/lib/src/view/puzzle/puzzle_session_widget.dart index a6f01e57b2..763ba870ce 100644 --- a/lib/src/view/puzzle/puzzle_session_widget.dart +++ b/lib/src/view/puzzle/puzzle_session_widget.dart @@ -175,7 +175,7 @@ class _SessionItem extends StatelessWidget { ? LichessColors.error.shade600 : LichessColors.error.shade400; - Color get next => Colors.grey.withOpacity(0.5); + Color get next => Colors.grey.withValues(alpha: 0.5); @override Widget build(BuildContext context) { diff --git a/lib/src/view/puzzle/puzzle_tab_screen.dart b/lib/src/view/puzzle/puzzle_tab_screen.dart index 96cbfc9615..4415219e4c 100644 --- a/lib/src/view/puzzle/puzzle_tab_screen.dart +++ b/lib/src/view/puzzle/puzzle_tab_screen.dart @@ -455,8 +455,10 @@ class _DailyPuzzle extends ConsumerWidget { Icon( Icons.today, size: 34, - color: - DefaultTextStyle.of(context).style.color?.withOpacity(0.6), + color: DefaultTextStyle.of(context) + .style + .color + ?.withValues(alpha: 0.6), ), Text( data.puzzle.initialPly.isOdd diff --git a/lib/src/view/puzzle/storm_dashboard.dart b/lib/src/view/puzzle/storm_dashboard.dart index 720c786a2b..1d7e7ee5d0 100644 --- a/lib/src/view/puzzle/storm_dashboard.dart +++ b/lib/src/view/puzzle/storm_dashboard.dart @@ -129,7 +129,7 @@ class _Body extends ConsumerWidget { // Date row final entryIndex = index ~/ 2; return ColoredBox( - color: LichessColors.grey.withOpacity(0.23), + color: LichessColors.grey.withValues(alpha: 0.23), child: Padding( padding: Styles.horizontalBodyPadding, child: Text( diff --git a/lib/src/view/puzzle/storm_screen.dart b/lib/src/view/puzzle/storm_screen.dart index c7b3a8cfd4..63e263fc89 100644 --- a/lib/src/view/puzzle/storm_screen.dart +++ b/lib/src/view/puzzle/storm_screen.dart @@ -48,7 +48,7 @@ class _StormScreenState extends ConsumerState { return WakelockWidget( child: PlatformScaffold( appBar: PlatformAppBar( - actions: [_StormDashboardButton(), ToggleSoundButton()], + actions: [_StormDashboardButton(), const ToggleSoundButton()], title: const Text('Puzzle Storm'), ), body: _Load(_boardKey), @@ -470,7 +470,8 @@ class _ComboState extends ConsumerState<_Combo> boxShadow: _controller.value == 1.0 ? [ BoxShadow( - color: indicatorColor.withOpacity(0.3), + color: + indicatorColor.withValues(alpha: 0.3), blurRadius: 10.0, spreadRadius: 2.0, ), @@ -538,26 +539,30 @@ class _ComboState extends ConsumerState<_Combo> List generateShades(Color baseColor, bool light) { final shades = []; - final int r = baseColor.red; - final int g = baseColor.green; - final int b = baseColor.blue; + final double r = baseColor.r; + final double g = baseColor.g; + final double b = baseColor.b; const int step = 20; // Generate darker shades for (int i = 4; i >= 2; i = i - 2) { - final int newR = (r - i * step).clamp(0, 255); - final int newG = (g - i * step).clamp(0, 255); - final int newB = (b - i * step).clamp(0, 255); - shades.add(Color.fromARGB(baseColor.alpha, newR, newG, newB)); + final double newR = (r - i * step).clamp(0, 255); + final double newG = (g - i * step).clamp(0, 255); + final double newB = (b - i * step).clamp(0, 255); + shades.add( + Color.from(alpha: baseColor.a, red: newR, green: newG, blue: newB), + ); } // Generate lighter shades for (int i = 2; i <= 3; i++) { - final int newR = (r + i * step).clamp(0, 255); - final int newG = (g + i * step).clamp(0, 255); - final int newB = (b + i * step).clamp(0, 255); - shades.add(Color.fromARGB(baseColor.alpha, newR, newG, newB)); + final double newR = (r + i * step).clamp(0, 255); + final double newG = (g + i * step).clamp(0, 255); + final double newB = (b + i * step).clamp(0, 255); + shades.add( + Color.from(alpha: baseColor.a, red: newR, green: newG, blue: newB), + ); } if (light) { diff --git a/lib/src/view/puzzle/streak_screen.dart b/lib/src/view/puzzle/streak_screen.dart index 3ca9365466..1507428ed6 100644 --- a/lib/src/view/puzzle/streak_screen.dart +++ b/lib/src/view/puzzle/streak_screen.dart @@ -37,13 +37,13 @@ class StreakScreen extends StatelessWidget { @override Widget build(BuildContext context) { - return WakelockWidget( + return const WakelockWidget( child: PlatformScaffold( appBar: PlatformAppBar( actions: [ToggleSoundButton()], - title: const Text('Puzzle Streak'), + title: Text('Puzzle Streak'), ), - body: const _Load(), + body: _Load(), ), ); } diff --git a/lib/src/view/user/perf_stats_screen.dart b/lib/src/view/user/perf_stats_screen.dart index 2113325751..2ecda93d41 100644 --- a/lib/src/view/user/perf_stats_screen.dart +++ b/lib/src/view/user/perf_stats_screen.dart @@ -842,7 +842,7 @@ class _EloChartState extends State<_EloChart> { @override Widget build(BuildContext context) { final borderColor = - Theme.of(context).colorScheme.onSurface.withOpacity(0.5); + Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5); final chartColor = Theme.of(context).colorScheme.tertiary; final chartDateFormatter = switch (_selectedRange) { DateRange.oneWeek => DateFormat.MMMd(), @@ -926,7 +926,7 @@ class _EloChartState extends State<_EloChart> { dotData: const FlDotData(show: false), color: chartColor, belowBarData: BarAreaData( - color: chartColor.withOpacity(0.2), + color: chartColor.withValues(alpha: 0.2), show: true, ), barWidth: 1.5, @@ -950,7 +950,7 @@ class _EloChartState extends State<_EloChart> { lineTouchData: LineTouchData( touchSpotThreshold: double.infinity, touchTooltipData: LineTouchTooltipData( - getTooltipColor: (_) => chartColor.withOpacity(0.5), + getTooltipColor: (_) => chartColor.withValues(alpha: 0.5), fitInsideHorizontally: true, fitInsideVertically: true, getTooltipItems: (touchedSpots) { @@ -1043,7 +1043,7 @@ class _RangeButton extends StatelessWidget { final chartColor = Theme.of(context).colorScheme.tertiary; return PlatformCard( - color: selected ? chartColor.withOpacity(0.2) : null, + color: selected ? chartColor.withValues(alpha: 0.2) : null, shadowColor: selected ? Colors.transparent : null, child: AdaptiveInkWell( borderRadius: const BorderRadius.all(Radius.circular(8.0)), diff --git a/lib/src/view/watch/tv_screen.dart b/lib/src/view/watch/tv_screen.dart index 7de0f2ffa7..8eab4f0c4f 100644 --- a/lib/src/view/watch/tv_screen.dart +++ b/lib/src/view/watch/tv_screen.dart @@ -47,7 +47,7 @@ class _TvScreenState extends ConsumerState { child: PlatformScaffold( appBar: PlatformAppBar( title: Text('${widget.channel.label} TV'), - actions: [ + actions: const [ ToggleSoundButton(), ], ), diff --git a/lib/src/widgets/board_carousel_item.dart b/lib/src/widgets/board_carousel_item.dart index ed6d1efbd4..16d2896753 100644 --- a/lib/src/widgets/board_carousel_item.dart +++ b/lib/src/widgets/board_carousel_item.dart @@ -71,8 +71,8 @@ class BoardCarouselItem extends ConsumerWidget { begin: Alignment.center, end: Alignment.bottomCenter, colors: [ - backgroundColor.withOpacity(0.25), - backgroundColor.withOpacity(1.0), + backgroundColor.withValues(alpha: 0.25), + backgroundColor.withValues(alpha: 1.0), ], stops: const [0.3, 1.00], tileMode: TileMode.clamp, @@ -120,7 +120,7 @@ class BoardCarouselItem extends ConsumerWidget { borderRadius: BorderRadius.circular(8.0), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withValues(alpha: 0.05), blurRadius: 6.0, ), ], diff --git a/lib/src/widgets/buttons.dart b/lib/src/widgets/buttons.dart index 9733efe29a..7130e96521 100644 --- a/lib/src/widgets/buttons.dart +++ b/lib/src/widgets/buttons.dart @@ -115,7 +115,7 @@ class _SecondaryButtonState extends State color: widget.glowing ? CupertinoTheme.of(context) .primaryColor - .withOpacity(_animation.value) + .withValues(alpha: _animation.value) : null, onPressed: widget.onPressed, child: widget.child, @@ -128,7 +128,7 @@ class _SecondaryButtonState extends State ? Theme.of(context) .colorScheme .primary - .withOpacity(_animation.value) + .withValues(alpha: _animation.value) : null, ), child: widget.child, diff --git a/lib/src/widgets/feedback.dart b/lib/src/widgets/feedback.dart index 493d4c3c90..087039534c 100644 --- a/lib/src/widgets/feedback.dart +++ b/lib/src/widgets/feedback.dart @@ -175,7 +175,7 @@ void showCupertinoSnackBar({ : type == SnackBarType.success ? context.lichessColors.good : CupertinoColors.systemGrey.resolveFrom(context)) - .withOpacity(0.6), + .withValues(alpha: 0.6), textStyle: const TextStyle(color: Colors.white), ), duration: duration, diff --git a/lib/src/widgets/move_list.dart b/lib/src/widgets/move_list.dart index 91417c7b24..631f2127e3 100644 --- a/lib/src/widgets/move_list.dart +++ b/lib/src/widgets/move_list.dart @@ -194,7 +194,7 @@ class InlineMoveCount extends StatelessWidget { '$count.', style: TextStyle( fontWeight: FontWeight.w500, - color: color?.withOpacity(_moveListOpacity) ?? + color: color?.withValues(alpha: _moveListOpacity) ?? textShade(context, _moveListOpacity), fontFamily: pieceNotation == PieceNotation.symbol ? 'ChessFont' : null, @@ -235,7 +235,7 @@ class InlineMoveItem extends StatelessWidget { fontWeight: current == true ? FontWeight.bold : FontWeight.w500, color: current != true ? color != null - ? color!.withOpacity(_moveListOpacity) + ? color!.withValues(alpha: _moveListOpacity) : textShade(context, _moveListOpacity) : Theme.of(context).colorScheme.primary, ), diff --git a/lib/src/widgets/platform_scaffold.dart b/lib/src/widgets/platform_scaffold.dart index f9da3ff32d..d8c3020249 100644 --- a/lib/src/widgets/platform_scaffold.dart +++ b/lib/src/widgets/platform_scaffold.dart @@ -113,7 +113,7 @@ class _CupertinoNavBarWrapper extends StatelessWidget @override bool shouldFullyObstruct(BuildContext context) { final Color backgroundColor = CupertinoTheme.of(context).barBackgroundColor; - return backgroundColor.alpha == 0xFF; + return backgroundColor.a == 0xFF; } } diff --git a/pubspec.lock b/pubspec.lock index cc28534ccc..80e69f5329 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -815,18 +815,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" url: "https://pub.dev" source: hosted - version: "10.0.5" + version: "10.0.7" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.8" leak_tracker_testing: dependency: transitive description: @@ -1247,7 +1247,7 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" sound_effect: dependency: "direct main" description: @@ -1541,10 +1541,10 @@ packages: dependency: transitive description: name: vm_service - sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "14.2.4" + version: "14.2.5" wakelock_plus: dependency: "direct main" description: From 816aa384b21aff8f1f7d7ee09e42fe14f8ff7f9b Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 12 Sep 2024 09:26:08 +0200 Subject: [PATCH 335/979] Upgrade dependencies --- ios/Podfile.lock | 16 ++++++------ pubspec.lock | 64 ++++++++++++++++++++++++------------------------ pubspec.yaml | 4 +-- 3 files changed, 42 insertions(+), 42 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 7150ebad0c..59a7a706a6 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -22,14 +22,14 @@ PODS: - Firebase/Messaging (11.0.0): - Firebase/CoreOnly - FirebaseMessaging (~> 11.0.0) - - firebase_core (3.4.0): + - firebase_core (3.4.1): - Firebase/CoreOnly (= 11.0.0) - Flutter - - firebase_crashlytics (4.1.0): + - firebase_crashlytics (4.1.1): - Firebase/Crashlytics (= 11.0.0) - firebase_core - Flutter - - firebase_messaging (15.1.0): + - firebase_messaging (15.1.1): - Firebase/Messaging (= 11.0.0) - firebase_core - Flutter @@ -132,7 +132,7 @@ PODS: - sqflite (0.0.3): - Flutter - FlutterMacOS - - stockfish (1.6.1): + - stockfish (1.6.2): - Flutter - url_launcher_ios (0.0.1): - Flutter @@ -228,9 +228,9 @@ SPEC CHECKSUMS: cupertino_http: 1a3a0f163c1b26e7f1a293b33d476e0fde7a64ec device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d Firebase: 9f574c08c2396885b5e7e100ed4293d956218af9 - firebase_core: ceec591a66629daaee82d3321551692c4a871493 - firebase_crashlytics: e4f04180f443d5a8b56fbc0685bdbd7d90dd26f0 - firebase_messaging: 15d8b557010f3bb7b98d0302e1c7c8fbcd244425 + firebase_core: ba84e940cf5cbbc601095f86556560937419195c + firebase_crashlytics: 4111f8198b78c99471c955af488cecd8224967e6 + firebase_messaging: c40f84e7a98da956d5262fada373b5c458edcf13 FirebaseCore: 3cf438f431f18c12cdf2aaf64434648b63f7e383 FirebaseCoreExtension: aa5c9779c2d0d39d83f1ceb3fdbafe80c4feecfa FirebaseCoreInternal: adefedc9a88dbe393c4884640a73ec9e8e790f8c @@ -254,7 +254,7 @@ SPEC CHECKSUMS: shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 sound_effect: 5280cfa89d4a576032186f15600dc948ca6d39ce sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec - stockfish: 9e398e73bfb36580f16b79e8b9b45568b9e1dcd9 + stockfish: d00cf6b95579f1d7032cbfd8e4fe874972fe2ff9 url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1 diff --git a/pubspec.lock b/pubspec.lock index 80e69f5329..e18bed9a76 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -13,10 +13,10 @@ packages: dependency: transitive description: name: _flutterfire_internals - sha256: "9371d13b8ee442e3bfc08a24e3a1b3742c839abbfaf5eef11b79c4b862c89bf7" + sha256: ddc6f775260b89176d329dee26f88b9469ef46aa3228ff6a0b91caf2b2989692 url: "https://pub.dev" source: hosted - version: "1.3.41" + version: "1.3.42" _macros: dependency: transitive description: dart @@ -154,10 +154,10 @@ packages: dependency: "direct main" description: name: cached_network_image - sha256: "4a5d8d2c728b0f3d0245f69f921d7be90cae4c2fd5288f773088672c0893f819" + sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" url: "https://pub.dev" source: hosted - version: "3.4.0" + version: "3.4.1" cached_network_image_platform_interface: dependency: transitive description: @@ -170,10 +170,10 @@ packages: dependency: transitive description: name: cached_network_image_web - sha256: "6322dde7a5ad92202e64df659241104a43db20ed594c41ca18de1014598d7996" + sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.3.1" characters: dependency: transitive description: @@ -338,10 +338,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" + sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab" url: "https://pub.dev" source: hosted - version: "2.3.6" + version: "2.3.7" dartchess: dependency: "direct main" description: @@ -434,10 +434,10 @@ packages: dependency: "direct main" description: name: firebase_core - sha256: "06537da27db981947fa535bb91ca120b4e9cb59cb87278dbdde718558cafc9ff" + sha256: "40921de9795fbf5887ed5c0adfdf4972d5a8d7ae7e1b2bb98dea39bc02626a88" url: "https://pub.dev" source: hosted - version: "3.4.0" + version: "3.4.1" firebase_core_platform_interface: dependency: transitive description: @@ -450,50 +450,50 @@ packages: dependency: transitive description: name: firebase_core_web - sha256: "362e52457ed2b7b180964769c1e04d1e0ea0259fdf7025fdfedd019d4ae2bd88" + sha256: f4ee170441ca141c5f9ee5ad8737daba3ee9c8e7efb6902aee90b4fbd178ce25 url: "https://pub.dev" source: hosted - version: "2.17.5" + version: "2.18.0" firebase_crashlytics: dependency: "direct main" description: name: firebase_crashlytics - sha256: "4c9872020c0d97a161362ee6af7000cfdb8666234ddc290a15252ad379bb235a" + sha256: c4fdbb14ba6f36794f89dc27fb5c759c9cc67ecbaeb079edc4dba515bbf9f555 url: "https://pub.dev" source: hosted - version: "4.1.0" + version: "4.1.1" firebase_crashlytics_platform_interface: dependency: transitive description: name: firebase_crashlytics_platform_interface - sha256: ede8a199ff03378857d3c8cbb7fa58d37c27bb5a6b75faf8415ff6925dcaae2a + sha256: "891d6f7ba4b93672d0e1265f27b6a9dccd56ba2cc30ce6496586b32d1d8770ac" url: "https://pub.dev" source: hosted - version: "3.6.41" + version: "3.6.42" firebase_messaging: dependency: "direct main" description: name: firebase_messaging - sha256: "29941ba5a3204d80656c0e52103369aa9a53edfd9ceae05a2bb3376f24fda453" + sha256: cc02c4afd6510cd84586020670140c4a23fbe52af16cd260ccf8ede101bb8d1b url: "https://pub.dev" source: hosted - version: "15.1.0" + version: "15.1.1" firebase_messaging_platform_interface: dependency: transitive description: name: firebase_messaging_platform_interface - sha256: "26c5370d3a79b15c8032724a68a4741e28f63e1f1a45699c4f0a8ae740aadd72" + sha256: d8a4984635f09213302243ea670fe5c42f3261d7d8c7c0a5f7dcd5d6c84be459 url: "https://pub.dev" source: hosted - version: "4.5.43" + version: "4.5.44" firebase_messaging_web: dependency: transitive description: name: firebase_messaging_web - sha256: "58276cd5d9e22a9320ef9e5bc358628920f770f93c91221f8b638e8346ed5df4" + sha256: "258b9d637965db7855299b123533609ed95e52350746a723dfd1d8d6f3fac678" url: "https://pub.dev" source: hosted - version: "3.8.13" + version: "3.9.0" fixnum: dependency: transitive description: @@ -636,7 +636,7 @@ packages: dependency: "direct main" description: path: "." - ref: master + ref: "3280106581fc8d54eae45f4a446f92cae36d7837" resolved-ref: "3280106581fc8d54eae45f4a446f92cae36d7837" url: "https://github.com/letsar/flutter_slidable.git" source: git @@ -1151,10 +1151,10 @@ packages: dependency: "direct main" description: name: share_plus - sha256: "59dfd53f497340a0c3a81909b220cfdb9b8973a91055c4e5ab9b9b9ad7c513c0" + sha256: "468c43f285207c84bcabf5737f33b914ceb8eb38398b91e5e3ad1698d1b72a52" url: "https://pub.dev" source: hosted - version: "10.0.0" + version: "10.0.2" share_plus_platform_interface: dependency: transitive description: @@ -1316,10 +1316,10 @@ packages: dependency: transitive description: name: sqlite3 - sha256: fde692580bee3379374af1f624eb3e113ab2865ecb161dbe2d8ac2de9735dbdb + sha256: "45f168ae2213201b54e09429ed0c593dc2c88c924a1488d6f9c523a255d567cb" url: "https://pub.dev" source: hosted - version: "2.4.5" + version: "2.4.6" stack_trace: dependency: transitive description: @@ -1340,11 +1340,11 @@ packages: dependency: "direct main" description: path: "." - ref: "0965a99ea143db00ec495eecbd54dfe10acf70ea" - resolved-ref: "0965a99ea143db00ec495eecbd54dfe10acf70ea" + ref: "0b4d8f20f72beb43c853230dd18063bee242487d" + resolved-ref: "0b4d8f20f72beb43c853230dd18063bee242487d" url: "https://github.com/lichess-org/dart-stockfish.git" source: git - version: "1.6.1" + version: "1.6.2" stream_channel: dependency: "direct dev" description: @@ -1573,10 +1573,10 @@ packages: dependency: transitive description: name: web - sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + sha256: d43c1d6b787bf0afad444700ae7f4db8827f701bc61c255ac8d328c6f4d52062 url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "1.0.0" web_socket: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 487df6e6db..fc8db055cb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -41,7 +41,7 @@ dependencies: flutter_slidable: git: url: https://github.com/letsar/flutter_slidable.git - ref: master + ref: 3280106581fc8d54eae45f4a446f92cae36d7837 flutter_spinkit: ^5.2.0 flutter_svg: ^2.0.10+1 freezed_annotation: ^2.2.0 @@ -67,7 +67,7 @@ dependencies: stockfish: git: url: https://github.com/lichess-org/dart-stockfish.git - ref: 0965a99ea143db00ec495eecbd54dfe10acf70ea + ref: 0b4d8f20f72beb43c853230dd18063bee242487d stream_transform: ^2.1.0 timeago: ^3.6.0 url_launcher: ^6.1.9 From f89761b1cd816abeb5454019c75a1fc110674803 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 12 Sep 2024 10:41:02 +0200 Subject: [PATCH 336/979] Update gradle plugin --- android/gradle/wrapper/gradle-wrapper.properties | 2 +- android/settings.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 3c472b99c6..8bc9958ab0 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-all.zip diff --git a/android/settings.gradle b/android/settings.gradle index 662e8f0f62..5a558f4d5b 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -19,7 +19,7 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "7.4.2" apply false + id "com.android.application" version '8.1.0' apply false id "org.jetbrains.kotlin.android" version "1.8.21" apply false id "com.google.gms.google-services" version "4.4.0" apply false id "com.google.firebase.crashlytics" version "2.9.9" apply false From 882adb4fef3323354b8946c492635a9c50511894 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 12 Sep 2024 16:20:36 +0200 Subject: [PATCH 337/979] Use the app colorscheme for clock tool --- lib/src/view/clock/clock_settings.dart | 80 +++++---- lib/src/view/clock/clock_tile.dart | 160 +++++++++--------- .../view/settings/toggle_sound_button.dart | 40 ++--- lib/src/widgets/countdown_clock.dart | 66 ++++---- 4 files changed, 167 insertions(+), 179 deletions(-) diff --git a/lib/src/view/clock/clock_settings.dart b/lib/src/view/clock/clock_settings.dart index 244002c087..0d8b6b4cab 100644 --- a/lib/src/view/clock/clock_settings.dart +++ b/lib/src/view/clock/clock_settings.dart @@ -2,12 +2,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/clock/clock_controller.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/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/buttons.dart'; -const _iconSize = 45.0; +const _iconSize = 38.0; class ClockSettings extends ConsumerWidget { const ClockSettings({super.key}); @@ -17,26 +17,30 @@ class ClockSettings extends ConsumerWidget { final state = ref.watch(clockControllerProvider); final buttonsEnabled = !state.started || state.paused; + final isSoundEnabled = ref.watch( + generalPreferencesProvider.select((prefs) => prefs.isSoundEnabled), + ); + return Padding( - padding: const EdgeInsets.all(8.0), + padding: const EdgeInsets.symmetric(vertical: 8.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - const _PlayResumeButton(), - PlatformIconButton( - semanticsLabel: context.l10n.reset, + const _PlayResumeButton(_iconSize), + IconButton( + tooltip: context.l10n.reset, iconSize: _iconSize, - onTap: buttonsEnabled + onPressed: buttonsEnabled ? () { ref.read(clockControllerProvider.notifier).reset(); } : null, - icon: Icons.refresh, + icon: const Icon(Icons.refresh), ), - PlatformIconButton( - semanticsLabel: context.l10n.settingsSettings, + IconButton( + tooltip: context.l10n.settingsSettings, iconSize: _iconSize, - onTap: buttonsEnabled + onPressed: buttonsEnabled ? () { final double screenHeight = MediaQuery.sizeOf(context).height; @@ -68,13 +72,23 @@ class ClockSettings extends ConsumerWidget { ); } : null, - icon: Icons.settings, + icon: const Icon(Icons.settings), ), - PlatformIconButton( - semanticsLabel: context.l10n.close, + IconButton( iconSize: _iconSize, - onTap: buttonsEnabled ? () => Navigator.of(context).pop() : null, - icon: Icons.home, + // TODO: translate + tooltip: 'Toggle sound', + onPressed: () => ref + .read(generalPreferencesProvider.notifier) + .toggleSoundEnabled(), + icon: Icon(isSoundEnabled ? Icons.volume_up : Icons.volume_off), + ), + IconButton( + tooltip: context.l10n.close, + iconSize: _iconSize, + onPressed: + buttonsEnabled ? () => Navigator.of(context).pop() : null, + icon: const Icon(Icons.home), ), ], ), @@ -83,7 +97,9 @@ class ClockSettings extends ConsumerWidget { } class _PlayResumeButton extends ConsumerWidget { - const _PlayResumeButton(); + const _PlayResumeButton(this.iconSize); + + final double iconSize; @override Widget build(BuildContext context, WidgetRef ref) { @@ -91,28 +107,28 @@ class _PlayResumeButton extends ConsumerWidget { final state = ref.watch(clockControllerProvider); if (!state.started) { - return PlatformIconButton( - semanticsLabel: context.l10n.play, - iconSize: 35, - onTap: () => controller.start(), - icon: Icons.play_arrow, + return IconButton( + tooltip: context.l10n.play, + iconSize: iconSize, + onPressed: () => controller.start(), + icon: const Icon(Icons.play_arrow), ); } if (state.paused) { - return PlatformIconButton( - semanticsLabel: context.l10n.resume, - iconSize: 35, - onTap: () => controller.resume(), - icon: Icons.play_arrow, + return IconButton( + tooltip: context.l10n.resume, + iconSize: iconSize, + onPressed: () => controller.resume(), + icon: const Icon(Icons.play_arrow), ); } - return PlatformIconButton( - semanticsLabel: context.l10n.pause, - iconSize: 35, - onTap: () => controller.pause(), - icon: Icons.pause, + return IconButton( + tooltip: context.l10n.pause, + iconSize: iconSize, + onPressed: () => controller.pause(), + icon: const Icon(Icons.pause), ); } } diff --git a/lib/src/view/clock/clock_tile.dart b/lib/src/view/clock/clock_tile.dart index 7d4c087dc1..1e7bf68201 100644 --- a/lib/src/view/clock/clock_tile.dart +++ b/lib/src/view/clock/clock_tile.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/clock/clock_controller.dart'; import 'package:lichess_mobile/src/model/common/time_increment.dart'; -import 'package:lichess_mobile/src/styles/lichess_colors.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; @@ -10,24 +10,6 @@ import 'package:lichess_mobile/src/widgets/countdown_clock.dart'; import 'custom_clock_settings.dart'; -const _darkClockStyle = ClockStyle( - textColor: Colors.black87, - activeTextColor: Colors.white, - emergencyTextColor: Colors.white, - backgroundColor: Colors.transparent, - activeBackgroundColor: Colors.transparent, - emergencyBackgroundColor: Color(0xFF673431), -); - -const _lightClockStyle = ClockStyle( - textColor: Colors.black87, - activeTextColor: Colors.white, - emergencyTextColor: Colors.black, - backgroundColor: Colors.transparent, - activeBackgroundColor: Colors.transparent, - emergencyBackgroundColor: Color(0xFFF2CCCC), -); - class ClockTile extends ConsumerWidget { const ClockTile({ required this.playerType, @@ -40,13 +22,25 @@ class ClockTile extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final colorScheme = Theme.of(context).colorScheme; final backgroundColor = clockState.isLoser(playerType) - ? LichessColors.red - : clockState.isPlayersTurn(playerType) - ? LichessColors.brag + ? context.lichessColors.error + : !clockState.paused && clockState.isPlayersTurn(playerType) + ? colorScheme.primary : clockState.activeSide == playerType - ? Colors.grey - : Colors.grey[300]; + ? colorScheme.secondaryContainer + : colorScheme.surfaceContainer; + + final clockStyle = ClockStyle( + textColor: clockState.activeSide == playerType + ? colorScheme.onSecondaryContainer + : colorScheme.onSurface, + activeTextColor: colorScheme.onPrimary, + emergencyTextColor: Colors.white, + backgroundColor: Colors.transparent, + activeBackgroundColor: Colors.transparent, + emergencyBackgroundColor: const Color(0xFF673431), + ); return RotatedBox( quarterTurns: playerType == ClockPlayerType.top ? 2 : 0, @@ -54,63 +48,59 @@ class ClockTile extends ConsumerWidget { alignment: Alignment.center, fit: StackFit.expand, children: [ - Opacity( - opacity: clockState.paused ? 0.7 : 1, - child: Material( - color: backgroundColor, - child: InkWell( - splashFactory: NoSplash.splashFactory, - onTap: !clockState.started - ? () { - ref - .read(clockControllerProvider.notifier) - .setActiveSide(playerType); - } - : null, - onTapDown: clockState.started && - clockState.isPlayersMoveAllowed(playerType) - ? (_) { - ref - .read(clockControllerProvider.notifier) - .onTap(playerType); - } - : null, - child: Padding( - padding: const EdgeInsets.all(40), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - FittedBox( - child: AnimatedCrossFade( - duration: const Duration(milliseconds: 300), - firstChild: CountdownClock( - key: Key('${clockState.id}-$playerType'), - padLeft: true, - lightColorStyle: _lightClockStyle, - darkColorStyle: _darkClockStyle, - duration: clockState.getDuration(playerType), - active: clockState.isActivePlayer(playerType), - onFlag: () { - ref - .read(clockControllerProvider.notifier) - .setLoser(playerType); - }, - onStop: (remaining) { - ref - .read(clockControllerProvider.notifier) - .updateDuration(playerType, remaining); - }, - ), - secondChild: const Icon(Icons.flag), - crossFadeState: clockState.isLoser(playerType) - ? CrossFadeState.showSecond - : CrossFadeState.showFirst, + Material( + color: backgroundColor, + child: InkWell( + splashFactory: NoSplash.splashFactory, + onTap: !clockState.started + ? () { + ref + .read(clockControllerProvider.notifier) + .setActiveSide(playerType); + } + : null, + onTapDown: clockState.started && + clockState.isPlayersMoveAllowed(playerType) + ? (_) { + ref + .read(clockControllerProvider.notifier) + .onTap(playerType); + } + : null, + child: Padding( + padding: const EdgeInsets.all(40), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FittedBox( + child: AnimatedCrossFade( + duration: const Duration(milliseconds: 300), + firstChild: CountdownClock( + key: Key('${clockState.id}-$playerType'), + padLeft: true, + clockStyle: clockStyle, + duration: clockState.getDuration(playerType), + active: clockState.isActivePlayer(playerType), + onFlag: () { + ref + .read(clockControllerProvider.notifier) + .setLoser(playerType); + }, + onStop: (remaining) { + ref + .read(clockControllerProvider.notifier) + .updateDuration(playerType, remaining); + }, ), + secondChild: const Icon(Icons.flag), + crossFadeState: clockState.isLoser(playerType) + ? CrossFadeState.showSecond + : CrossFadeState.showFirst, ), - ], - ), + ), + ], ), ), ), @@ -120,7 +110,13 @@ class ClockTile extends ConsumerWidget { right: 24, child: Text( '${context.l10n.stormMoves}: ${clockState.getMovesCount(playerType)}', - style: const TextStyle(fontSize: 13, color: Colors.black), + style: TextStyle( + fontSize: 13, + color: + !clockState.paused && clockState.isPlayersTurn(playerType) + ? clockStyle.activeTextColor + : clockStyle.textColor, + ), ), ), Positioned( @@ -132,9 +128,7 @@ class ClockTile extends ConsumerWidget { semanticsLabel: context.l10n.settingsSettings, iconSize: 32, icon: Icons.tune, - color: Theme.of(context).brightness == Brightness.dark - ? _darkClockStyle.textColor - : _lightClockStyle.textColor, + color: clockStyle.textColor, onTap: clockState.started ? null : () => showAdaptiveBottomSheet( diff --git a/lib/src/view/settings/toggle_sound_button.dart b/lib/src/view/settings/toggle_sound_button.dart index 06f6bc4927..3bbf111eb6 100644 --- a/lib/src/view/settings/toggle_sound_button.dart +++ b/lib/src/view/settings/toggle_sound_button.dart @@ -1,10 +1,13 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; class ToggleSoundButton extends ConsumerWidget { + const ToggleSoundButton({this.iconSize, super.key}); + + final double? iconSize; + @override Widget build(BuildContext context, WidgetRef ref) { final isSoundEnabled = ref.watch( @@ -13,32 +16,13 @@ class ToggleSoundButton extends ConsumerWidget { ), ); - switch (Theme.of(context).platform) { - case TargetPlatform.android: - return IconButton( - // TODO translate - tooltip: 'Toggle sound', - icon: isSoundEnabled - ? const Icon(Icons.volume_up) - : const Icon(Icons.volume_off), - onPressed: () => ref - .read(generalPreferencesProvider.notifier) - .toggleSoundEnabled(), - ); - case TargetPlatform.iOS: - return CupertinoIconButton( - padding: EdgeInsets.zero, - semanticsLabel: 'Toggle sound', - icon: isSoundEnabled - ? const Icon(CupertinoIcons.volume_up) - : const Icon(CupertinoIcons.volume_off), - onPressed: () => ref - .read(generalPreferencesProvider.notifier) - .toggleSoundEnabled(), - ); - default: - assert(false, 'Unexpected platform $Theme.of(context).platform'); - return const SizedBox.shrink(); - } + return PlatformIconButton( + iconSize: iconSize, + // TODO: translate + semanticsLabel: 'Toggle sound', + onTap: () => + ref.read(generalPreferencesProvider.notifier).toggleSoundEnabled(), + icon: isSoundEnabled ? Icons.volume_up : Icons.volume_off, + ); } } diff --git a/lib/src/widgets/countdown_clock.dart b/lib/src/widgets/countdown_clock.dart index ac6985d8c1..09fa81dc84 100644 --- a/lib/src/widgets/countdown_clock.dart +++ b/lib/src/widgets/countdown_clock.dart @@ -14,6 +14,18 @@ part 'countdown_clock.freezed.dart'; /// /// The clock starts only when [active] is `true`. class CountdownClock extends ConsumerStatefulWidget { + const CountdownClock({ + required this.duration, + required this.active, + this.emergencyThreshold, + this.emergencySoundEnabled = true, + this.onFlag, + this.onStop, + this.clockStyle, + this.padLeft = false, + super.key, + }); + /// The duration left on the clock. final Duration duration; @@ -34,28 +46,12 @@ class CountdownClock extends ConsumerStatefulWidget { /// Callback with the remaining duration when the clock stops final ValueSetter? onStop; - /// Custom light color style - final ClockStyle? lightColorStyle; - - /// Custom dark color style - final ClockStyle? darkColorStyle; + /// Custom color style + final ClockStyle? clockStyle; /// Whether to pad with a leading zero (default is `false`). final bool padLeft; - const CountdownClock({ - required this.duration, - required this.active, - this.emergencyThreshold, - this.emergencySoundEnabled = true, - this.onFlag, - this.onStop, - this.lightColorStyle, - this.darkColorStyle, - this.padLeft = false, - super.key, - }); - @override ConsumerState createState() => _CountdownClockState(); } @@ -120,14 +116,6 @@ class _CountdownClockState extends ConsumerState { } } - ClockStyle getStyle(Brightness brightness) { - if (brightness == Brightness.dark) { - return widget.darkColorStyle ?? ClockStyle.darkThemeStyle; - } - - return widget.lightColorStyle ?? ClockStyle.lightThemeStyle; - } - @override void initState() { super.initState(); @@ -157,13 +145,13 @@ class _CountdownClockState extends ConsumerState { @override Widget build(BuildContext context) { - final brightness = Theme.of(context).brightness; return RepaintBoundary( child: Clock( + padLeft: widget.padLeft, timeLeft: timeLeft, active: widget.active, emergencyThreshold: widget.emergencyThreshold, - clockStyle: getStyle(brightness), + clockStyle: widget.clockStyle, ), ); } @@ -176,7 +164,7 @@ class Clock extends StatelessWidget { const Clock({ required this.timeLeft, required this.active, - required this.clockStyle, + this.clockStyle, this.emergencyThreshold, this.padLeft = false, super.key, @@ -193,7 +181,7 @@ class Clock extends StatelessWidget { final Duration? emergencyThreshold; /// Clock style to use. - final ClockStyle clockStyle; + final ClockStyle? clockStyle; /// Whether to pad with a leading zero (default is `false`). final bool padLeft; @@ -213,14 +201,20 @@ class Clock extends StatelessWidget { final minsDisplay = padLeft ? mins.toString().padLeft(2, '0') : mins.toString(); + final brightness = Theme.of(context).brightness; + final activeClockStyle = clockStyle ?? + (brightness == Brightness.dark + ? ClockStyle.darkThemeStyle + : ClockStyle.lightThemeStyle); + return Container( decoration: BoxDecoration( borderRadius: const BorderRadius.all(Radius.circular(5.0)), color: active ? isEmergency - ? clockStyle.emergencyBackgroundColor - : clockStyle.activeBackgroundColor - : clockStyle.backgroundColor, + ? activeClockStyle.emergencyBackgroundColor + : activeClockStyle.activeBackgroundColor + : activeClockStyle.backgroundColor, ), child: Padding( padding: const EdgeInsets.symmetric(vertical: 3.0, horizontal: 5.0), @@ -234,9 +228,9 @@ class Clock extends StatelessWidget { style: TextStyle( color: active ? isEmergency - ? clockStyle.emergencyTextColor - : clockStyle.activeTextColor - : clockStyle.textColor, + ? activeClockStyle.emergencyTextColor + : activeClockStyle.activeTextColor + : activeClockStyle.textColor, fontSize: 26, height: remainingHeight < kSmallRemainingHeightLeftBoardThreshold From fd41a9d517f9d47852b47c1f6ce543e1c4176276 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 12 Sep 2024 18:45:29 +0200 Subject: [PATCH 338/979] Restore explorer indexing indicator --- lib/src/view/opening_explorer/opening_explorer_screen.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index 47ef21e9d3..79cea2fadb 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -346,7 +346,7 @@ class _OpeningExplorerView extends StatelessWidget { child: AnimatedOpacity( duration: const Duration(milliseconds: 300), curve: Curves.fastOutSlowIn, - opacity: isLoading ? 0.3 : 0.0, + opacity: isLoading ? 0.15 : 0.0, child: ColoredBox(color: loadingOverlayColor), ), ), @@ -437,6 +437,9 @@ class _OpeningExplorerView extends StatelessWidget { appBar: AppBar( title: Text(context.l10n.openingExplorer), bottom: _MoveList(pgn: pgn, options: options), + actions: [ + if (isIndexing) const _IndexingIndicator(), + ], ), ), iosBuilder: (_) => CupertinoPageScaffold( @@ -444,6 +447,7 @@ class _OpeningExplorerView extends StatelessWidget { middle: Text(context.l10n.openingExplorer), automaticBackgroundVisibility: false, border: null, + trailing: isIndexing ? const _IndexingIndicator() : null, ), child: body, ), From 2f208cf9313608596eb4a33354e30caa841bf347 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 12 Sep 2024 19:11:54 +0200 Subject: [PATCH 339/979] Fix lighten and darken --- lib/src/styles/styles.dart | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/lib/src/styles/styles.dart b/lib/src/styles/styles.dart index 9216fe6627..31807a3f65 100644 --- a/lib/src/styles/styles.dart +++ b/lib/src/styles/styles.dart @@ -220,18 +220,12 @@ Color? dividerColor(BuildContext context) => Color darken(Color c, [double amount = .1]) { assert(amount >= 0 && amount <= 1); - final f = 1 - amount; - return Color.from(alpha: c.a, red: c.r * f, green: c.g * f, blue: c.b * f); + return Color.lerp(c, Colors.black, amount) ?? c; } Color lighten(Color c, [double amount = .1]) { assert(amount >= 0 && amount <= 1); - return Color.from( - alpha: c.a, - red: c.r + ((255 - c.r) * amount), - green: c.g + ((255 - c.g) * amount), - blue: c.b + ((255 - c.b) * amount), - ); + return Color.lerp(c, Colors.white, amount) ?? c; } @immutable From 7e48b158896fc6e1dd7b9cfa566199f022106b48 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 12 Sep 2024 19:13:58 +0200 Subject: [PATCH 340/979] Tweak explorer overlay --- lib/src/view/opening_explorer/opening_explorer_screen.dart | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index 79cea2fadb..bcf741c33b 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -308,9 +308,6 @@ class _OpeningExplorerView extends StatelessWidget { @override Widget build(BuildContext context) { final isTablet = isTabletOrLarger(context); - final loadingOverlayColor = Theme.of(context).brightness == Brightness.dark - ? Colors.black - : Colors.white; final body = SafeArea( bottom: false, @@ -346,8 +343,8 @@ class _OpeningExplorerView extends StatelessWidget { child: AnimatedOpacity( duration: const Duration(milliseconds: 300), curve: Curves.fastOutSlowIn, - opacity: isLoading ? 0.15 : 0.0, - child: ColoredBox(color: loadingOverlayColor), + opacity: isLoading ? 0.10 : 0.0, + child: const ColoredBox(color: Colors.black), ), ), ); From 82c62229f9c00897dc2af95dd26b8f9c310f5fc2 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Wed, 11 Sep 2024 22:54:57 +0200 Subject: [PATCH 341/979] feat: add asSquareOrThrow() and asSquareOrNull() extensions --- lib/src/model/common/chess.dart | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lib/src/model/common/chess.dart b/lib/src/model/common/chess.dart index 3bc5732bae..74e521ef97 100644 --- a/lib/src/model/common/chess.dart +++ b/lib/src/model/common/chess.dart @@ -270,6 +270,19 @@ extension ChessExtension on Pick { } } + Square asSquareOrThrow() { + return Square.parse(this.required().value as String)!; + } + + Square? asSquareOrNull() { + if (value == null) return null; + try { + return asSquareOrThrow(); + } catch (_) { + return null; + } + } + Variant asVariantOrThrow() { final value = this.required().value; if (value is Variant) { From f5eaed7cabd1291e158d63f95b2817b6f33fda1f Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 13 Sep 2024 16:48:12 +0200 Subject: [PATCH 342/979] Style fixes --- lib/src/styles/styles.dart | 23 +- .../view/board_editor/board_editor_menu.dart | 4 +- lib/src/view/clock/clock_screen.dart | 212 ++++++++++++++++-- lib/src/view/clock/clock_settings.dart | 10 +- lib/src/view/clock/clock_tile.dart | 166 -------------- .../coordinate_display.dart | 1 + .../coordinate_training_screen.dart | 15 +- lib/src/view/home/create_game_options.dart | 111 +++++---- lib/src/view/home/home_tab_screen.dart | 18 +- lib/src/view/play/play_screen.dart | 8 +- lib/src/view/play/quick_game_button.dart | 55 +++-- lib/src/view/puzzle/puzzle_tab_screen.dart | 8 +- lib/src/view/user/leaderboard_widget.dart | 4 +- lib/src/view/user/player_screen.dart | 4 +- lib/src/view/user/recent_games.dart | 4 +- lib/src/view/watch/watch_tab_screen.dart | 12 +- lib/src/widgets/adaptive_bottom_sheet.dart | 2 +- 17 files changed, 349 insertions(+), 308 deletions(-) delete mode 100644 lib/src/view/clock/clock_tile.dart diff --git a/lib/src/styles/styles.dart b/lib/src/styles/styles.dart index 31807a3f65..a9c45f1bc8 100644 --- a/lib/src/styles/styles.dart +++ b/lib/src/styles/styles.dart @@ -56,6 +56,17 @@ abstract class Styles { static const bodyPadding = EdgeInsets.symmetric(vertical: 16.0, horizontal: 16.0); static const verticalBodyPadding = EdgeInsets.symmetric(vertical: 16.0); + static const horizontalBodyPadding = EdgeInsets.symmetric(horizontal: 16.0); + static const sectionBottomPadding = EdgeInsets.only(bottom: 16.0); + static const sectionTopPadding = EdgeInsets.only(top: 16.0); + static const bodySectionPadding = EdgeInsets.all(16.0); + + /// Horizontal and bottom padding for the body section. + static const bodySectionBottomPadding = EdgeInsets.only( + bottom: 16.0, + left: 16.0, + right: 16.0, + ); // colors static Color? expansionTileColor(BuildContext context) => @@ -188,18 +199,6 @@ abstract class Styles { ), ); - /// Gets horizontal padding according to platform. - static const horizontalBodyPadding = EdgeInsets.symmetric(horizontal: 16.0); - static const sectionBottomPadding = EdgeInsets.only(bottom: 16); - static const sectionTopPadding = EdgeInsets.only(top: 16); - - /// Horizontal and bottom padding for a body section - static EdgeInsetsGeometry get bodySectionPadding => - horizontalBodyPadding.add(sectionBottomPadding).add(sectionTopPadding); - - static EdgeInsetsGeometry get bodySectionBottomPadding => - horizontalBodyPadding.add(sectionBottomPadding); - // from: // https://github.com/flutter/flutter/blob/796c8ef79279f9c774545b3771238c3098dbefab/packages/flutter/lib/src/cupertino/bottom_tab_bar.dart#L17 static const CupertinoDynamicColor cupertinoDefaultTabBarBorderColor = diff --git a/lib/src/view/board_editor/board_editor_menu.dart b/lib/src/view/board_editor/board_editor_menu.dart index e0f0f870df..f3b641bac1 100644 --- a/lib/src/view/board_editor/board_editor_menu.dart +++ b/lib/src/view/board_editor/board_editor_menu.dart @@ -73,9 +73,9 @@ class BoardEditorMenu extends ConsumerWidget { ); }), if (editorState.enPassantOptions.isNotEmpty) ...[ - Padding( + const Padding( padding: Styles.bodySectionPadding, - child: const Text('En passant', style: Styles.subtitle), + child: Text('En passant', style: Styles.subtitle), ), Wrap( spacing: 8.0, diff --git a/lib/src/view/clock/clock_screen.dart b/lib/src/view/clock/clock_screen.dart index 8d447df06a..f01b4d4880 100644 --- a/lib/src/view/clock/clock_screen.dart +++ b/lib/src/view/clock/clock_screen.dart @@ -1,18 +1,20 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/clock/clock_controller.dart'; +import 'package:lichess_mobile/src/model/common/time_increment.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/immersive_mode.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/view/clock/clock_settings.dart'; -import 'package:lichess_mobile/src/view/clock/clock_tile.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/countdown_clock.dart'; -class ClockScreen extends StatefulWidget { - const ClockScreen({super.key}); +import 'custom_clock_settings.dart'; - @override - State createState() => _ClockScreenState(); -} +class ClockScreen extends StatelessWidget { + const ClockScreen({super.key}); -class _ClockScreenState extends State { @override Widget build(BuildContext context) { return const ImmersiveModeWidget( @@ -28,23 +30,189 @@ class _Body extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final state = ref.watch(clockControllerProvider); - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Expanded( - child: ClockTile( - playerType: ClockPlayerType.top, - clockState: state, + return OrientationBuilder( + builder: (context, orientation) { + return (orientation == Orientation.portrait ? Column.new : Row.new)( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: ClockTile( + orientation: orientation, + playerType: ClockPlayerType.top, + clockState: state, + ), + ), + ClockSettings(orientation: orientation), + Expanded( + child: ClockTile( + orientation: orientation, + playerType: ClockPlayerType.bottom, + clockState: state, + ), + ), + ], + ); + }, + ); + } +} + +class ClockTile extends ConsumerWidget { + const ClockTile({ + required this.playerType, + required this.clockState, + required this.orientation, + super.key, + }); + + final ClockPlayerType playerType; + final ClockState clockState; + final Orientation orientation; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final colorScheme = Theme.of(context).colorScheme; + final backgroundColor = clockState.isLoser(playerType) + ? context.lichessColors.error + : !clockState.paused && clockState.isPlayersTurn(playerType) + ? colorScheme.primary + : clockState.activeSide == playerType + ? colorScheme.secondaryContainer + : colorScheme.surfaceContainer; + + final clockStyle = ClockStyle( + textColor: clockState.activeSide == playerType + ? colorScheme.onSecondaryContainer + : colorScheme.onSurface, + activeTextColor: colorScheme.onPrimary, + emergencyTextColor: Colors.white, + backgroundColor: Colors.transparent, + activeBackgroundColor: Colors.transparent, + emergencyBackgroundColor: const Color(0xFF673431), + ); + + return RotatedBox( + quarterTurns: orientation == Orientation.portrait && + playerType == ClockPlayerType.top + ? 2 + : 0, + child: Stack( + alignment: Alignment.center, + fit: StackFit.expand, + children: [ + Material( + color: backgroundColor, + child: InkWell( + splashFactory: NoSplash.splashFactory, + onTap: !clockState.started + ? () { + ref + .read(clockControllerProvider.notifier) + .setActiveSide(playerType); + } + : null, + onTapDown: clockState.started && + clockState.isPlayersMoveAllowed(playerType) + ? (_) { + ref + .read(clockControllerProvider.notifier) + .onTap(playerType); + } + : null, + child: Padding( + padding: const EdgeInsets.all(40), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FittedBox( + child: AnimatedCrossFade( + duration: const Duration(milliseconds: 300), + firstChild: CountdownClock( + key: Key('${clockState.id}-$playerType'), + padLeft: true, + clockStyle: clockStyle, + duration: clockState.getDuration(playerType), + active: clockState.isActivePlayer(playerType), + onFlag: () { + ref + .read(clockControllerProvider.notifier) + .setLoser(playerType); + }, + onStop: (remaining) { + ref + .read(clockControllerProvider.notifier) + .updateDuration(playerType, remaining); + }, + ), + secondChild: const Icon(Icons.flag), + crossFadeState: clockState.isLoser(playerType) + ? CrossFadeState.showSecond + : CrossFadeState.showFirst, + ), + ), + ], + ), + ), + ), + ), + Positioned( + top: 24, + right: 24, + child: Text( + '${context.l10n.stormMoves}: ${clockState.getMovesCount(playerType)}', + style: TextStyle( + fontSize: 13, + color: + !clockState.paused && clockState.isPlayersTurn(playerType) + ? clockStyle.activeTextColor + : clockStyle.textColor, + ), + ), ), - ), - const ClockSettings(), - Expanded( - child: ClockTile( - playerType: ClockPlayerType.bottom, - clockState: state, + Positioned( + bottom: MediaQuery.paddingOf(context).bottom + 48.0, + child: AnimatedOpacity( + opacity: clockState.started ? 0 : 1.0, + duration: const Duration(milliseconds: 300), + child: PlatformIconButton( + semanticsLabel: context.l10n.settingsSettings, + iconSize: 32, + icon: Icons.tune, + color: clockStyle.textColor, + onTap: clockState.started + ? null + : () => showAdaptiveBottomSheet( + context: context, + builder: (BuildContext context) => + CustomClockSettings( + player: playerType, + clock: playerType == ClockPlayerType.top + ? TimeIncrement.fromDurations( + clockState.options.timePlayerTop, + clockState.options.incrementPlayerTop, + ) + : TimeIncrement.fromDurations( + clockState.options.timePlayerBottom, + clockState.options.incrementPlayerBottom, + ), + onSubmit: ( + ClockPlayerType player, + TimeIncrement clock, + ) { + Navigator.of(context).pop(); + ref + .read(clockControllerProvider.notifier) + .updateOptionsCustom(clock, player); + }, + ), + ), + ), + ), ), - ), - ], + ], + ), ); } } diff --git a/lib/src/view/clock/clock_settings.dart b/lib/src/view/clock/clock_settings.dart index 0d8b6b4cab..a5371b4f91 100644 --- a/lib/src/view/clock/clock_settings.dart +++ b/lib/src/view/clock/clock_settings.dart @@ -10,7 +10,9 @@ import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; const _iconSize = 38.0; class ClockSettings extends ConsumerWidget { - const ClockSettings({super.key}); + const ClockSettings({required this.orientation, super.key}); + + final Orientation orientation; @override Widget build(BuildContext context, WidgetRef ref) { @@ -22,8 +24,10 @@ class ClockSettings extends ConsumerWidget { ); return Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Row( + padding: orientation == Orientation.portrait + ? const EdgeInsets.symmetric(vertical: 10.0) + : const EdgeInsets.symmetric(horizontal: 10.0), + child: (orientation == Orientation.portrait ? Row.new : Column.new)( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ const _PlayResumeButton(_iconSize), diff --git a/lib/src/view/clock/clock_tile.dart b/lib/src/view/clock/clock_tile.dart deleted file mode 100644 index 1e7bf68201..0000000000 --- a/lib/src/view/clock/clock_tile.dart +++ /dev/null @@ -1,166 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lichess_mobile/src/model/clock/clock_controller.dart'; -import 'package:lichess_mobile/src/model/common/time_increment.dart'; -import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/l10n_context.dart'; -import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; -import 'package:lichess_mobile/src/widgets/buttons.dart'; -import 'package:lichess_mobile/src/widgets/countdown_clock.dart'; - -import 'custom_clock_settings.dart'; - -class ClockTile extends ConsumerWidget { - const ClockTile({ - required this.playerType, - required this.clockState, - super.key, - }); - - final ClockPlayerType playerType; - final ClockState clockState; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final colorScheme = Theme.of(context).colorScheme; - final backgroundColor = clockState.isLoser(playerType) - ? context.lichessColors.error - : !clockState.paused && clockState.isPlayersTurn(playerType) - ? colorScheme.primary - : clockState.activeSide == playerType - ? colorScheme.secondaryContainer - : colorScheme.surfaceContainer; - - final clockStyle = ClockStyle( - textColor: clockState.activeSide == playerType - ? colorScheme.onSecondaryContainer - : colorScheme.onSurface, - activeTextColor: colorScheme.onPrimary, - emergencyTextColor: Colors.white, - backgroundColor: Colors.transparent, - activeBackgroundColor: Colors.transparent, - emergencyBackgroundColor: const Color(0xFF673431), - ); - - return RotatedBox( - quarterTurns: playerType == ClockPlayerType.top ? 2 : 0, - child: Stack( - alignment: Alignment.center, - fit: StackFit.expand, - children: [ - Material( - color: backgroundColor, - child: InkWell( - splashFactory: NoSplash.splashFactory, - onTap: !clockState.started - ? () { - ref - .read(clockControllerProvider.notifier) - .setActiveSide(playerType); - } - : null, - onTapDown: clockState.started && - clockState.isPlayersMoveAllowed(playerType) - ? (_) { - ref - .read(clockControllerProvider.notifier) - .onTap(playerType); - } - : null, - child: Padding( - padding: const EdgeInsets.all(40), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - FittedBox( - child: AnimatedCrossFade( - duration: const Duration(milliseconds: 300), - firstChild: CountdownClock( - key: Key('${clockState.id}-$playerType'), - padLeft: true, - clockStyle: clockStyle, - duration: clockState.getDuration(playerType), - active: clockState.isActivePlayer(playerType), - onFlag: () { - ref - .read(clockControllerProvider.notifier) - .setLoser(playerType); - }, - onStop: (remaining) { - ref - .read(clockControllerProvider.notifier) - .updateDuration(playerType, remaining); - }, - ), - secondChild: const Icon(Icons.flag), - crossFadeState: clockState.isLoser(playerType) - ? CrossFadeState.showSecond - : CrossFadeState.showFirst, - ), - ), - ], - ), - ), - ), - ), - Positioned( - top: 24, - right: 24, - child: Text( - '${context.l10n.stormMoves}: ${clockState.getMovesCount(playerType)}', - style: TextStyle( - fontSize: 13, - color: - !clockState.paused && clockState.isPlayersTurn(playerType) - ? clockStyle.activeTextColor - : clockStyle.textColor, - ), - ), - ), - Positioned( - bottom: MediaQuery.paddingOf(context).bottom + 24.0, - child: AnimatedOpacity( - opacity: clockState.started ? 0 : 1.0, - duration: const Duration(milliseconds: 300), - child: PlatformIconButton( - semanticsLabel: context.l10n.settingsSettings, - iconSize: 32, - icon: Icons.tune, - color: clockStyle.textColor, - onTap: clockState.started - ? null - : () => showAdaptiveBottomSheet( - context: context, - builder: (BuildContext context) => - CustomClockSettings( - player: playerType, - clock: playerType == ClockPlayerType.top - ? TimeIncrement.fromDurations( - clockState.options.timePlayerTop, - clockState.options.incrementPlayerTop, - ) - : TimeIncrement.fromDurations( - clockState.options.timePlayerBottom, - clockState.options.incrementPlayerBottom, - ), - onSubmit: ( - ClockPlayerType player, - TimeIncrement clock, - ) { - Navigator.of(context).pop(); - ref - .read(clockControllerProvider.notifier) - .updateOptionsCustom(clock, player); - }, - ), - ), - ), - ), - ), - ], - ), - ); - } -} diff --git a/lib/src/view/coordinate_training/coordinate_display.dart b/lib/src/view/coordinate_training/coordinate_display.dart index 82ceb038e0..9e4275b0ec 100644 --- a/lib/src/view/coordinate_training/coordinate_display.dart +++ b/lib/src/view/coordinate_training/coordinate_display.dart @@ -70,6 +70,7 @@ class CoordinateDisplayState extends ConsumerState final textStyle = DefaultTextStyle.of(context).style.copyWith( fontSize: 110.0, fontFamily: 'monospace', + color: Colors.white.withValues(alpha: 0.9), fontWeight: FontWeight.bold, fontFeatures: [const FontFeature.tabularFigures()], shadows: const [ diff --git a/lib/src/view/coordinate_training/coordinate_training_screen.dart b/lib/src/view/coordinate_training/coordinate_training_screen.dart index 1d505f409b..125f7a0919 100644 --- a/lib/src/view/coordinate_training/coordinate_training_screen.dart +++ b/lib/src/view/coordinate_training/coordinate_training_screen.dart @@ -10,7 +10,6 @@ import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/coordinate_training/coordinate_training_controller.dart'; import 'package:lichess_mobile/src/model/coordinate_training/coordinate_training_preferences.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; -import 'package:lichess_mobile/src/styles/lichess_colors.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; @@ -87,8 +86,8 @@ class _BodyState extends ConsumerState<_Body> { highlightLastGuess!: SquareHighlight( details: HighlightDetails( solidColor: (trainingState.lastGuess == Guess.correct - ? LichessColors.good - : LichessColors.error) + ? context.lichessColors.good + : context.lichessColors.error) .withValues(alpha: 0.5), ), ), @@ -96,7 +95,7 @@ class _BodyState extends ConsumerState<_Body> { } else ...{ trainingState.currentCoord!: SquareHighlight( details: HighlightDetails( - solidColor: LichessColors.good.withValues(alpha: 0.5), + solidColor: context.lichessColors.good.withValues(alpha: 0.5), ), ), }, @@ -136,8 +135,8 @@ class _BodyState extends ConsumerState<_Body> { timeFractionElapsed: trainingState.timeFractionElapsed, color: trainingState.lastGuess == Guess.incorrect - ? LichessColors.error - : LichessColors.good, + ? context.lichessColors.error + : context.lichessColors.good, ), _TrainingBoard( boardSize: boardSize, @@ -157,8 +156,8 @@ class _BodyState extends ConsumerState<_Body> { score: trainingState.score, size: boardSize / 8, color: trainingState.lastGuess == Guess.incorrect - ? LichessColors.error - : LichessColors.good, + ? context.lichessColors.error + : context.lichessColors.good, ), FatButton( semanticsLabel: 'Abort Training', diff --git a/lib/src/view/home/create_game_options.dart b/lib/src/view/home/create_game_options.dart index d77cb785a3..c8918e4393 100644 --- a/lib/src/view/home/create_game_options.dart +++ b/lib/src/view/home/create_game_options.dart @@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/over_the_board/over_the_board_screen.dart'; @@ -16,54 +17,82 @@ class CreateGameOptions extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final buttons = [ - _CreateGamePlatformButton( - onTap: () { - ref.invalidate(accountProvider); - pushPlatformRoute( - context, - title: context.l10n.custom, - builder: (_) => const CreateCustomGameScreen(), - ); - }, - icon: Icons.tune, - label: context.l10n.custom, - ), - _CreateGamePlatformButton( - onTap: () { - pushPlatformRoute( - context, - title: context.l10n.onlineBots, - builder: (_) => const OnlineBotsScreen(), - ); - }, - icon: Icons.computer, - label: context.l10n.onlineBots, - ), - _CreateGamePlatformButton( - onTap: () { - pushPlatformRoute( - context, - title: 'Over the Board', - rootNavigator: true, - builder: (_) => const OverTheBoardScreen(), - ); - }, - icon: LichessIcons.chess_board, - label: 'Over the board', - ), - ]; + final isOnline = + ref.watch(connectivityChangesProvider).valueOrNull?.isOnline ?? false; + + return Column( + children: [ + _Section( + children: [ + _CreateGamePlatformButton( + onTap: isOnline + ? () { + ref.invalidate(accountProvider); + pushPlatformRoute( + context, + title: context.l10n.custom, + builder: (_) => const CreateCustomGameScreen(), + ); + } + : null, + icon: Icons.tune, + label: context.l10n.custom, + ), + _CreateGamePlatformButton( + onTap: isOnline + ? () { + pushPlatformRoute( + context, + title: context.l10n.onlineBots, + builder: (_) => const OnlineBotsScreen(), + ); + } + : null, + icon: Icons.computer, + label: context.l10n.onlineBots, + ), + ], + ), + _Section( + children: [ + _CreateGamePlatformButton( + onTap: () { + pushPlatformRoute( + context, + title: 'Over the Board', + rootNavigator: true, + builder: (_) => const OverTheBoardScreen(), + ); + }, + icon: LichessIcons.chess_board, + label: 'Over the board', + ), + ], + ), + ], + ); + } +} + +class _Section extends StatelessWidget { + const _Section({ + required this.children, + }); + final List children; + + @override + Widget build(BuildContext context) { return Theme.of(context).platform == TargetPlatform.iOS ? ListSection( hasLeading: true, - children: buttons, + children: children, ) : Padding( - padding: Styles.bodySectionPadding, + padding: Styles.horizontalBodyPadding.add(Styles.sectionTopPadding), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, - children: buttons, + children: children, ), ); } @@ -80,7 +109,7 @@ class _CreateGamePlatformButton extends StatelessWidget { final String label; - final void Function() onTap; + final void Function()? onTap; @override Widget build(BuildContext context) { diff --git a/lib/src/view/home/home_tab_screen.dart b/lib/src/view/home/home_tab_screen.dart index d4694ee964..10c04af055 100644 --- a/lib/src/view/home/home_tab_screen.dart +++ b/lib/src/view/home/home_tab_screen.dart @@ -242,7 +242,7 @@ class _HomeBody extends ConsumerWidget { widget: EnabledWidget.perfCards, isEditing: isEditing, shouldShow: session != null, - child: AccountPerfCards(padding: Styles.bodySectionPadding), + child: const AccountPerfCards(padding: Styles.bodySectionPadding), ), Row( crossAxisAlignment: CrossAxisAlignment.start, @@ -294,9 +294,9 @@ class _HomeBody extends ConsumerWidget { widget: EnabledWidget.quickPairing, isEditing: isEditing, shouldShow: status.isOnline, - child: Padding( + child: const Padding( padding: Styles.bodySectionPadding, - child: const QuickGameMatrix(), + child: QuickGameMatrix(), ), ), if (status.isOnline) @@ -461,9 +461,9 @@ class _WelcomeScreen extends StatelessWidget { widget: EnabledWidget.quickPairing, isEditing: isEditing, shouldShow: true, - child: Padding( + child: const Padding( padding: Styles.bodySectionPadding, - child: const QuickGameMatrix(), + child: QuickGameMatrix(), ), ), ...welcomeWidgets, @@ -562,14 +562,14 @@ class _TabletCreateAGameSection extends StatelessWidget { widget: EnabledWidget.quickPairing, isEditing: isEditing, shouldShow: true, - child: Padding( + child: const Padding( padding: Styles.bodySectionPadding, - child: const QuickGameMatrix(), + child: QuickGameMatrix(), ), ), - Padding( + const Padding( padding: Styles.bodySectionPadding, - child: const QuickGameButton(), + child: QuickGameButton(), ), const CreateGameOptions(), ], diff --git a/lib/src/view/play/play_screen.dart b/lib/src/view/play/play_screen.dart index 0c82615ed9..41c50050d0 100644 --- a/lib/src/view/play/play_screen.dart +++ b/lib/src/view/play/play_screen.dart @@ -15,15 +15,15 @@ class PlayScreen extends StatelessWidget { appBar: PlatformAppBar( title: Text(context.l10n.play), ), - body: SafeArea( + body: const SafeArea( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Padding( - padding: Styles.bodySectionPadding, - child: const QuickGameButton(), + padding: Styles.horizontalBodyPadding, + child: QuickGameButton(), ), - const CreateGameOptions(), + CreateGameOptions(), ], ), ), diff --git a/lib/src/view/play/quick_game_button.dart b/lib/src/view/play/quick_game_button.dart index aaeb306360..6d134c4018 100644 --- a/lib/src/view/play/quick_game_button.dart +++ b/lib/src/view/play/quick_game_button.dart @@ -6,6 +6,7 @@ import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/lobby/game_seek.dart'; import 'package:lichess_mobile/src/model/lobby/game_setup.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/game/game_screen.dart'; @@ -20,6 +21,8 @@ class QuickGameButton extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final playPrefs = ref.watch(gameSetupPreferencesProvider); final session = ref.watch(authSessionProvider); + final isOnline = + ref.watch(connectivityChangesProvider).valueOrNull?.isOnline ?? false; return Row( children: [ @@ -75,33 +78,37 @@ class QuickGameButton extends ConsumerWidget { horizontal: 8.0, vertical: 16.0, ), - onPressed: () { - pushPlatformRoute( - context, - rootNavigator: true, - builder: (_) => GameScreen( - seek: GameSeek.fastPairing( - playPrefs.quickPairingTimeIncrement, - session, - ), - ), - ); - }, + onPressed: isOnline + ? () { + pushPlatformRoute( + context, + rootNavigator: true, + builder: (_) => GameScreen( + seek: GameSeek.fastPairing( + playPrefs.quickPairingTimeIncrement, + session, + ), + ), + ); + } + : null, child: Text(context.l10n.play, style: Styles.bold), ) : FilledButton( - onPressed: () { - pushPlatformRoute( - context, - rootNavigator: true, - builder: (_) => GameScreen( - seek: GameSeek.fastPairing( - playPrefs.quickPairingTimeIncrement, - session, - ), - ), - ); - }, + onPressed: isOnline + ? () { + pushPlatformRoute( + context, + rootNavigator: true, + builder: (_) => GameScreen( + seek: GameSeek.fastPairing( + playPrefs.quickPairingTimeIncrement, + session, + ), + ), + ); + } + : null, child: Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), child: Text(context.l10n.play, style: Styles.bold), diff --git a/lib/src/view/puzzle/puzzle_tab_screen.dart b/lib/src/view/puzzle/puzzle_tab_screen.dart index 4415219e4c..7ebf8dbdfe 100644 --- a/lib/src/view/puzzle/puzzle_tab_screen.dart +++ b/lib/src/view/puzzle/puzzle_tab_screen.dart @@ -376,9 +376,9 @@ class PuzzleHistoryWidget extends ConsumerWidget { debugPrint( 'SEVERE: [PuzzleHistoryWidget] could not load puzzle history', ); - return Padding( + return const Padding( padding: Styles.bodySectionPadding, - child: const Text('Could not load Puzzle history.'), + child: Text('Could not load Puzzle history.'), ); }, loading: () => Shimmer( @@ -495,9 +495,9 @@ class _DailyPuzzle extends ConsumerWidget { ], ), ), - error: (error, stack) => Padding( + error: (error, stack) => const Padding( padding: Styles.bodySectionPadding, - child: const Text('Could not load the daily puzzle.'), + child: Text('Could not load the daily puzzle.'), ), ); } diff --git a/lib/src/view/user/leaderboard_widget.dart b/lib/src/view/user/leaderboard_widget.dart index 954f866aab..2f35923970 100644 --- a/lib/src/view/user/leaderboard_widget.dart +++ b/lib/src/view/user/leaderboard_widget.dart @@ -50,9 +50,9 @@ class LeaderboardWidget extends ConsumerWidget { debugPrint( 'SEVERE: [LeaderboardWidget] could not lead leaderboard data; $error\n $stackTrace', ); - return Padding( + return const Padding( padding: Styles.bodySectionPadding, - child: const Text('Could not load leaderboard.'), + child: Text('Could not load leaderboard.'), ); }, loading: () => Shimmer( diff --git a/lib/src/view/user/player_screen.dart b/lib/src/view/user/player_screen.dart index 0a38424f62..7cdc48637b 100644 --- a/lib/src/view/user/player_screen.dart +++ b/lib/src/view/user/player_screen.dart @@ -53,9 +53,9 @@ class _Body extends ConsumerWidget { return SafeArea( child: ListView( children: [ - Padding( + const Padding( padding: Styles.bodySectionPadding, - child: const _SearchButton(), + child: _SearchButton(), ), if (session != null) _OnlineFriendsWidget(), RatingPrefAware(child: LeaderboardWidget()), diff --git a/lib/src/view/user/recent_games.dart b/lib/src/view/user/recent_games.dart index 599e23746a..1e8d8c2c0f 100644 --- a/lib/src/view/user/recent_games.dart +++ b/lib/src/view/user/recent_games.dart @@ -75,9 +75,9 @@ class RecentGamesWidget extends ConsumerWidget { debugPrint( 'SEVERE: [RecentGames] could not recent games; $error\n$stackTrace', ); - return Padding( + return const Padding( padding: Styles.bodySectionPadding, - child: const Text('Could not load recent games.'), + child: Text('Could not load recent games.'), ); }, loading: () => Shimmer( diff --git a/lib/src/view/watch/watch_tab_screen.dart b/lib/src/view/watch/watch_tab_screen.dart index 92c839b361..68c49af20c 100644 --- a/lib/src/view/watch/watch_tab_screen.dart +++ b/lib/src/view/watch/watch_tab_screen.dart @@ -224,9 +224,9 @@ class _BroadcastWidget extends ConsumerWidget { debugPrint( 'SEVERE: [BroadcastWidget] could not load broadcast data; $error\n $stackTrace', ); - return Padding( + return const Padding( padding: Styles.bodySectionPadding, - child: const Text('Could not load broadcasts'), + child: Text('Could not load broadcasts'), ); }, loading: () => Shimmer( @@ -288,9 +288,9 @@ class _WatchTvWidget extends ConsumerWidget { debugPrint( 'SEVERE: [StreamerWidget] could not load channels data; $error\n $stackTrace', ); - return Padding( + return const Padding( padding: Styles.bodySectionPadding, - child: const Text('Could not load TV channels'), + child: Text('Could not load TV channels'), ); }, loading: () => Shimmer( @@ -343,9 +343,9 @@ class _StreamerWidget extends ConsumerWidget { debugPrint( 'SEVERE: [StreamerWidget] could not load streamer data; $error\n $stackTrace', ); - return Padding( + return const Padding( padding: Styles.bodySectionPadding, - child: const Text('Could not load live streamers'), + child: Text('Could not load live streamers'), ); }, loading: () => Shimmer( diff --git a/lib/src/widgets/adaptive_bottom_sheet.dart b/lib/src/widgets/adaptive_bottom_sheet.dart index b4ec9d7686..996b301ec1 100644 --- a/lib/src/widgets/adaptive_bottom_sheet.dart +++ b/lib/src/widgets/adaptive_bottom_sheet.dart @@ -49,7 +49,7 @@ Future showAdaptiveBottomSheet({ class BottomSheetScrollableContainer extends StatelessWidget { const BottomSheetScrollableContainer({ required this.children, - this.padding = const EdgeInsets.only(bottom: 8.0), + this.padding = const EdgeInsets.only(bottom: 16.0), }); final List children; From 3326e08f364a0060ee8190ab050fd6dcb0721af2 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 13 Sep 2024 17:00:06 +0200 Subject: [PATCH 343/979] Fix formatting --- lib/src/view/home/home_tab_screen.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/src/view/home/home_tab_screen.dart b/lib/src/view/home/home_tab_screen.dart index 10c04af055..6e0d10233b 100644 --- a/lib/src/view/home/home_tab_screen.dart +++ b/lib/src/view/home/home_tab_screen.dart @@ -242,7 +242,9 @@ class _HomeBody extends ConsumerWidget { widget: EnabledWidget.perfCards, isEditing: isEditing, shouldShow: session != null, - child: const AccountPerfCards(padding: Styles.bodySectionPadding), + child: const AccountPerfCards( + padding: Styles.bodySectionPadding, + ), ), Row( crossAxisAlignment: CrossAxisAlignment.start, From 02b2b0278e72ffb333a26089304363846bf8dd89 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 13 Sep 2024 17:22:25 +0200 Subject: [PATCH 344/979] Fix PR comments --- lib/src/model/challenge/challenge_service.dart | 10 +++++++--- lib/src/view/user/challenge_requests_screen.dart | 11 ++++++++--- pubspec.lock | 10 +++++----- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/lib/src/model/challenge/challenge_service.dart b/lib/src/model/challenge/challenge_service.dart index 4f94b31848..93fc6ff131 100644 --- a/lib/src/model/challenge/challenge_service.dart +++ b/lib/src/model/challenge/challenge_service.dart @@ -22,9 +22,13 @@ ChallengeService challengeService(ChallengeServiceRef ref) { return ChallengeService(ref); } -final challengeStreamController = StreamController.broadcast(); -final challengeStream = challengeStreamController.stream; +final _challengeStreamController = StreamController.broadcast(); +/// The stream of challenge events that are received from the server. +final Stream challengeStream = + _challengeStreamController.stream; + +/// A service that listens to challenge events and shows notifications. class ChallengeService { ChallengeService(this.ref); @@ -43,7 +47,7 @@ class ChallengeService { _previous = _current; _current = (inward: inward.lock, outward: outward.lock); - challengeStreamController.add(_current!); + _challengeStreamController.add(_current!); if (_previous == null) return; final prevIds = _previous!.inward.map((challenge) => challenge.id); diff --git a/lib/src/view/user/challenge_requests_screen.dart b/lib/src/view/user/challenge_requests_screen.dart index 504be82bca..86c8080c07 100644 --- a/lib/src/view/user/challenge_requests_screen.dart +++ b/lib/src/view/user/challenge_requests_screen.dart @@ -47,7 +47,6 @@ class ChallengeRequestsScreen extends ConsumerWidget { class _Body extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final challengeRepo = ref.read(challengeRepositoryProvider); final challenges = ref.watch(challengesProvider); final session = ref.watch(authSessionProvider); @@ -86,6 +85,8 @@ class _Body extends ConsumerWidget { title: Text(context.l10n.accept), isDestructiveAction: true, onConfirm: (_) async { + final challengeRepo = + ref.read(challengeRepositoryProvider); await challengeRepo.accept(challenge.id); final fullId = await challengeRepo .show(challenge.id) @@ -107,9 +108,13 @@ class _Body extends ConsumerWidget { ); }, onCancel: challenge.direction == ChallengeDirection.outward - ? () => challengeRepo.cancel(challenge.id) + ? () => ref + .read(challengeRepositoryProvider) + .cancel(challenge.id) : () { - challengeRepo.decline(challenge.id); + ref + .read(challengeRepositoryProvider) + .decline(challenge.id); ref .read(localNotificationServiceProvider) .cancel(challenge.id.value.hashCode); diff --git a/pubspec.lock b/pubspec.lock index 351275fcf9..b018bbb65b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -21,7 +21,7 @@ packages: dependency: transitive description: dart source: sdk - version: "0.1.5" + version: "0.3.2" analyzer: dependency: transitive description: @@ -887,10 +887,10 @@ packages: dependency: transitive description: name: macros - sha256: a8403c89b36483b4cbf9f1fcd24562f483cb34a5c9bf101cf2b0d8a083cf1239 + sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" url: "https://pub.dev" source: hosted - version: "0.1.0-main.5" + version: "0.1.2-main.4" matcher: dependency: transitive description: @@ -911,10 +911,10 @@ packages: dependency: "direct main" description: name: meta - sha256: "25dfcaf170a0190f47ca6355bdd4552cb8924b430512ff0cafb8db9bd41fe33b" + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.14.0" + version: "1.15.0" mime: dependency: transitive description: From bc8e43c75e3910fdd5d8be3df9bb8b7f2a09dbef Mon Sep 17 00:00:00 2001 From: Joe Stein <569991+jas14@users.noreply.github.com> Date: Sat, 14 Sep 2024 14:49:05 -0700 Subject: [PATCH 345/979] Update FVM config files --- .fvm/fvm_config.json | 4 ---- .fvmrc | 4 ++++ .gitignore | 4 +++- docs/setting_dev_env.md | 2 ++ 4 files changed, 9 insertions(+), 5 deletions(-) delete mode 100644 .fvm/fvm_config.json create mode 100644 .fvmrc diff --git a/.fvm/fvm_config.json b/.fvm/fvm_config.json deleted file mode 100644 index 2038915c29..0000000000 --- a/.fvm/fvm_config.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "flutterSdkVersion": "beta", - "flavors": {} -} \ No newline at end of file diff --git a/.fvmrc b/.fvmrc new file mode 100644 index 0000000000..7ac2fba5fe --- /dev/null +++ b/.fvmrc @@ -0,0 +1,4 @@ +{ + "flutter": "beta", + "flavors": {} +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 4376ea4237..ca77e71239 100644 --- a/.gitignore +++ b/.gitignore @@ -35,4 +35,6 @@ doc/api/ *.ipr *.iws .idea/ -.fvm/flutter_sdk + +# FVM Version Cache +.fvm/ diff --git a/docs/setting_dev_env.md b/docs/setting_dev_env.md index 70a5bcdf81..3da965b679 100644 --- a/docs/setting_dev_env.md +++ b/docs/setting_dev_env.md @@ -25,6 +25,8 @@ Note that this application is not meant to be run on web platform. If you want to use FVM to manage your Flutter versions effectively, please consult the [FVM (Flutter Version Management) guide](https://fvm.app/documentation/getting-started/installation) for comprehensive instructions on installing Flutter on your specific operating system. +This project is currently using FVM 3.x. + **Pro Tip:** Remember to prepend the 'flutter' prefix when using FVM commands, like this: `fvm flutter [command]`. ## Lila Server From 7bc1795ead12bc0c1eeec65fa7e764294f3d20b3 Mon Sep 17 00:00:00 2001 From: Joe Stein <569991+jas14@users.noreply.github.com> Date: Sat, 14 Sep 2024 15:03:52 -0700 Subject: [PATCH 346/979] Add a tiny Markdown fix --- docs/internationalisation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/internationalisation.md b/docs/internationalisation.md index 8e40517433..9475c0ba03 100644 --- a/docs/internationalisation.md +++ b/docs/internationalisation.md @@ -1,7 +1,7 @@ # Internationalisation We're using the standard way of internationalising our app, as desbribed in the -[official documentation)(https://docs.flutter.dev/ui/accessibility-and-internationalization/internationalization#setting-up). +[official documentation](https://docs.flutter.dev/ui/accessibility-and-internationalization/internationalization#setting-up). What is specific to this project is the way we produce the template `lib/l10n/app_en.arb` file. From cdbd7f6c6307137a5b4572e9b7291ff90eadc17e Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Mon, 9 Sep 2024 23:04:15 +0200 Subject: [PATCH 347/979] feat: add models for listing available studies --- lib/src/model/common/id.dart | 8 ++++ lib/src/model/study/study.dart | 42 +++++++++++++++++ lib/src/model/study/study_filter.dart | 43 +++++++++++++++++ lib/src/model/study/study_repository.dart | 56 +++++++++++++++++++++++ 4 files changed, 149 insertions(+) create mode 100644 lib/src/model/study/study.dart create mode 100644 lib/src/model/study/study_filter.dart create mode 100644 lib/src/model/study/study_repository.dart diff --git a/lib/src/model/common/id.dart b/lib/src/model/common/id.dart index 4ab5fb6edb..71b991c9b7 100644 --- a/lib/src/model/common/id.dart +++ b/lib/src/model/common/id.dart @@ -53,6 +53,14 @@ extension type const BroadcastRoundId(String value) implements StringId {} extension type const BroadcastGameId(String value) implements StringId {} +extension type const StudyId(String value) implements StringId { + StudyId.fromJson(dynamic json) : this(json as String); +} + +extension type const StudyChapterId(String value) implements StringId { + StudyChapterId.fromJson(dynamic json) : this(json as String); +} + extension IDPick on Pick { UserId asUserIdOrThrow() { final value = required().value; diff --git a/lib/src/model/study/study.dart b/lib/src/model/study/study.dart new file mode 100644 index 0000000000..8ff3fa9c1a --- /dev/null +++ b/lib/src/model/study/study.dart @@ -0,0 +1,42 @@ +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/user/user.dart'; + +part 'study.freezed.dart'; +part 'study.g.dart'; + +@Freezed(fromJson: true) +class StudyPageData with _$StudyPageData { + const StudyPageData._(); + + const factory StudyPageData({ + required StudyId id, + required String name, + required bool liked, + required int likes, + @JsonKey(fromJson: DateTime.fromMillisecondsSinceEpoch) + required DateTime updatedAt, + required LightUser? owner, + required IList topics, + required IList members, + required IList chapters, + required String? flair, + }) = _StudyPageData; + + factory StudyPageData.fromJson(Map json) => + _$StudyPageDataFromJson(json); +} + +@Freezed(fromJson: true) +class StudyMember with _$StudyMember { + const StudyMember._(); + + const factory StudyMember({ + required LightUser user, + required String role, + }) = _StudyMember; + + factory StudyMember.fromJson(Map json) => + _$StudyMemberFromJson(json); +} diff --git a/lib/src/model/study/study_filter.dart b/lib/src/model/study/study_filter.dart new file mode 100644 index 0000000000..74edc31f94 --- /dev/null +++ b/lib/src/model/study/study_filter.dart @@ -0,0 +1,43 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'study_filter.freezed.dart'; +part 'study_filter.g.dart'; + +enum StudyCategory { + all, + mine, + member, + public, + private, + likes, +} + +enum StudyListOrder { + hot, + popular, + newest, + oldest, + updated, +} + +@riverpod +class StudyFilter extends _$StudyFilter { + @override + StudyFilterState build() => const StudyFilterState(); + + void setCategory(StudyCategory category) => + state = state.copyWith(category: category); + + void setOrder(StudyListOrder order) => state = state.copyWith(order: order); +} + +@freezed +class StudyFilterState with _$StudyFilterState { + const StudyFilterState._(); + + const factory StudyFilterState({ + @Default(StudyCategory.all) StudyCategory category, + @Default(StudyListOrder.hot) StudyListOrder order, + }) = _StudyFilterState; +} diff --git a/lib/src/model/study/study_repository.dart b/lib/src/model/study/study_repository.dart new file mode 100644 index 0000000000..bedc2ff78a --- /dev/null +++ b/lib/src/model/study/study_repository.dart @@ -0,0 +1,56 @@ +import 'package:deep_pick/deep_pick.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:http/http.dart'; +import 'package:lichess_mobile/src/model/common/http.dart'; +import 'package:lichess_mobile/src/model/study/study.dart'; +import 'package:lichess_mobile/src/model/study/study_filter.dart'; + +class StudyRepository { + StudyRepository(this.client); + + final Client client; + + Future> getStudies({ + required StudyCategory category, + required StudyListOrder order, + int page = 1, + }) { + return _requestStudies( + path: '${category.name}/${order.name}', + queryParameters: {'page': page.toString()}, + ); + } + + Future> searchStudies({ + required String query, + int page = 1, + }) { + return _requestStudies( + path: 'search', + queryParameters: {'page': page.toString(), 'q': query}, + ); + } + + Future> _requestStudies({ + required String path, + required Map queryParameters, + }) { + return client.readJson( + Uri( + path: '/study/$path', + queryParameters: queryParameters, + ), + headers: {'Accept': 'application/json'}, + mapper: (Map json) { + final paginator = + pick(json, 'paginator').asMapOrThrow(); + + return pick(paginator, 'currentPageResults') + .asListOrThrow( + (pick) => StudyPageData.fromJson(pick.asMapOrThrow()), + ) + .toIList(); + }, + ); + } +} From 960e03d72b2cd8e78749165af01b8ee83ced9947 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Sun, 8 Sep 2024 10:51:38 +0200 Subject: [PATCH 348/979] refactor: make _Filter from game filter public We can (and will) resuse it in other screens. --- lib/src/view/user/game_history_screen.dart | 61 +----------------- lib/src/widgets/filter.dart | 72 ++++++++++++++++++++++ 2 files changed, 75 insertions(+), 58 deletions(-) create mode 100644 lib/src/widgets/filter.dart diff --git a/lib/src/view/user/game_history_screen.dart b/lib/src/view/user/game_history_screen.dart index 0233b8fe7c..973d16a5f4 100644 --- a/lib/src/view/user/game_history_screen.dart +++ b/lib/src/view/user/game_history_screen.dart @@ -12,6 +12,7 @@ import 'package:lichess_mobile/src/view/game/game_list_tile.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; +import 'package:lichess_mobile/src/widgets/filter.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; @@ -287,7 +288,7 @@ class _FilterGamesState extends ConsumerState<_FilterGames> { const SizedBox(height: 12.0), const PlatformDivider(thickness: 1, indent: 0), filterGroupSpace, - _Filter( + Filter( filterName: context.l10n.side, filterType: FilterType.singleChoice, choices: Side.values, @@ -332,7 +333,7 @@ class _FilterGamesState extends ConsumerState<_FilterGames> { return perfs; } - Widget perfFilter(List choices) => _Filter( + Widget perfFilter(List choices) => Filter( filterName: context.l10n.variant, filterType: FilterType.multipleChoice, choices: choices, @@ -349,59 +350,3 @@ class _FilterGamesState extends ConsumerState<_FilterGames> { ), ); } - -enum FilterType { - singleChoice, - multipleChoice, -} - -class _Filter extends StatelessWidget { - const _Filter({ - required this.filterName, - required this.filterType, - required this.choices, - required this.choiceSelected, - required this.choiceLabel, - required this.onSelected, - }); - - final String filterName; - final FilterType filterType; - final Iterable choices; - final bool Function(T choice) choiceSelected; - final String Function(T choice) choiceLabel; - final void Function(T value, bool selected) onSelected; - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(filterName, style: const TextStyle(fontSize: 18)), - const SizedBox(height: 10), - SizedBox( - width: double.infinity, - child: Wrap( - spacing: 8.0, - children: choices - .map( - (choice) => switch (filterType) { - FilterType.singleChoice => ChoiceChip( - label: Text(choiceLabel(choice)), - selected: choiceSelected(choice), - onSelected: (value) => onSelected(choice, value), - ), - FilterType.multipleChoice => FilterChip( - label: Text(choiceLabel(choice)), - selected: choiceSelected(choice), - onSelected: (value) => onSelected(choice, value), - ), - }, - ) - .toList(growable: false), - ), - ), - ], - ); - } -} diff --git a/lib/src/widgets/filter.dart b/lib/src/widgets/filter.dart new file mode 100644 index 0000000000..af26cfdc6f --- /dev/null +++ b/lib/src/widgets/filter.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; + +enum FilterType { + /// Only one choice is intended to be selected at a time. Uses [ChoiceChip] to display choices. + singleChoice, + + /// Multiple choices can be selected at the same time. Uses [FilterChip] to display choices. + multipleChoice, +} + +/// Displays a row of choices that can be selected or deselected. +class Filter extends StatelessWidget { + const Filter({ + required this.filterName, + required this.filterType, + required this.choices, + required this.choiceSelected, + required this.choiceLabel, + required this.onSelected, + }); + + /// Will be displayed above the choices as a title. + final String filterName; + + /// Controls how choices in a [Filter] are displayed. + final FilterType filterType; + + /// The choices that will be displayed. + final Iterable choices; + + /// Called to determine whether a choice is currently selected. + final bool Function(T choice) choiceSelected; + + /// Determines label to display for the given choice. + final String Function(T choice) choiceLabel; + + /// Called when a choice is selected or deselected. + final void Function(T value, bool selected) onSelected; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(filterName, style: const TextStyle(fontSize: 18)), + const SizedBox(height: 10), + SizedBox( + width: double.infinity, + child: Wrap( + spacing: 8.0, + children: choices + .map( + (choice) => switch (filterType) { + FilterType.singleChoice => ChoiceChip( + label: Text(choiceLabel(choice)), + selected: choiceSelected(choice), + onSelected: (value) => onSelected(choice, value), + ), + FilterType.multipleChoice => FilterChip( + label: Text(choiceLabel(choice)), + selected: choiceSelected(choice), + onSelected: (value) => onSelected(choice, value), + ), + }, + ) + .toList(growable: false), + ), + ), + ], + ); + } +} From 17a45653b5dc937a10160ec7289ba69214279bbd Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Sun, 15 Sep 2024 10:01:46 +0200 Subject: [PATCH 349/979] Fix iOS local notification setup --- ios/Podfile.lock | 6 ++++++ ios/Runner/AppDelegate.swift | 1 + 2 files changed, 7 insertions(+) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 59a7a706a6..69dbbc8133 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -78,6 +78,8 @@ PODS: - flutter_appauth (0.0.1): - AppAuth (= 1.7.4) - Flutter + - flutter_local_notifications (0.0.1): + - Flutter - flutter_native_splash (0.0.1): - Flutter - flutter_secure_storage (6.0.0): @@ -149,6 +151,7 @@ DEPENDENCIES: - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) - Flutter (from `Flutter`) - flutter_appauth (from `.symlinks/plugins/flutter_appauth/ios`) + - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) @@ -198,6 +201,8 @@ EXTERNAL SOURCES: :path: Flutter flutter_appauth: :path: ".symlinks/plugins/flutter_appauth/ios" + flutter_local_notifications: + :path: ".symlinks/plugins/flutter_local_notifications/ios" flutter_native_splash: :path: ".symlinks/plugins/flutter_native_splash/ios" flutter_secure_storage: @@ -241,6 +246,7 @@ SPEC CHECKSUMS: FirebaseSessions: 78f137e68dc01ca71606169ba4ac73b98c13752a Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_appauth: 1ce438877bc111c5d8f42da47729909290624886 + flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086 flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778 flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12 GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index d612839cac..a2bbd6786d 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -1,5 +1,6 @@ import UIKit import Flutter +import flutter_local_notifications @main @objc class AppDelegate: FlutterAppDelegate { From cec7adca3c31e05e353bf5a4ff0d76fdd348763f Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Sun, 15 Sep 2024 10:02:19 +0200 Subject: [PATCH 350/979] Tweak challenges button style --- lib/src/view/home/home_tab_screen.dart | 17 +++++++---- lib/src/widgets/buttons.dart | 41 ++++++++------------------ 2 files changed, 24 insertions(+), 34 deletions(-) diff --git a/lib/src/view/home/home_tab_screen.dart b/lib/src/view/home/home_tab_screen.dart index 5cda782b43..bff4db350e 100644 --- a/lib/src/view/home/home_tab_screen.dart +++ b/lib/src/view/home/home_tab_screen.dart @@ -87,7 +87,8 @@ class _HomeScreenState extends ConsumerState with RouteAware { onPressed: () { ref.read(editModeProvider.notifier).state = !isEditing; }, - icon: Icon(isEditing ? Icons.save : Icons.app_registration), + icon: + Icon(isEditing ? Icons.save_outlined : Icons.app_registration), tooltip: isEditing ? 'Save' : 'Edit', ), const _ChallengeScreenButton(), @@ -145,6 +146,7 @@ class _HomeScreenState extends ConsumerState with RouteAware { trailing: const Row( mainAxisSize: MainAxisSize.min, children: [ + _ChallengeScreenButton(), _PlayerScreenButton(), ], ), @@ -934,7 +936,7 @@ class _PlayerScreenButton extends ConsumerWidget { return connectivity.maybeWhen( data: (connectivity) => AppBarIconButton( - icon: const Icon(Icons.group), + icon: const Icon(Icons.group_outlined), semanticsLabel: context.l10n.players, onPressed: !connectivity.isOnline ? null @@ -947,7 +949,7 @@ class _PlayerScreenButton extends ConsumerWidget { }, ), orElse: () => AppBarIconButton( - icon: const Icon(Icons.group), + icon: const Icon(Icons.group_outlined), semanticsLabel: context.l10n.players, onPressed: null, ), @@ -960,13 +962,18 @@ class _ChallengeScreenButton extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final session = ref.watch(authSessionProvider); final connectivity = ref.watch(connectivityChangesProvider); final challenges = ref.watch(challengesProvider); final count = challenges.valueOrNull?.inward.length; + if (session == null) { + return const SizedBox.shrink(); + } + return connectivity.maybeWhen( data: (connectivity) => AppBarNotificationIconButton( - icon: const Icon(LichessIcons.crossed_swords), + icon: const Icon(LichessIcons.crossed_swords, size: 18.0), semanticsLabel: context.l10n.preferencesNotifyChallenge, onPressed: !connectivity.isOnline ? null @@ -980,7 +987,7 @@ class _ChallengeScreenButton extends ConsumerWidget { count: count ?? 0, ), orElse: () => AppBarIconButton( - icon: const Icon(LichessIcons.crossed_swords), + icon: const Icon(LichessIcons.crossed_swords, size: 18.0), semanticsLabel: context.l10n.preferencesNotifyChallenge, onPressed: null, ), diff --git a/lib/src/widgets/buttons.dart b/lib/src/widgets/buttons.dart index 58685ee183..414110e398 100644 --- a/lib/src/widgets/buttons.dart +++ b/lib/src/widgets/buttons.dart @@ -212,36 +212,19 @@ class AppBarNotificationIconButton extends StatelessWidget { @override Widget build(BuildContext context) { - return Stack( - alignment: AlignmentDirectional.center, - children: [ - AppBarIconButton( - icon: icon, - onPressed: onPressed, - semanticsLabel: semanticsLabel, + return AppBarIconButton( + icon: Badge.count( + backgroundColor: Theme.of(context).colorScheme.primary, + textStyle: TextStyle( + color: Theme.of(context).colorScheme.onPrimary, + fontWeight: FontWeight.bold, ), - if (count > 0) - Positioned( - top: 0, - right: 5, - child: Container( - padding: const EdgeInsets.fromLTRB(4, 1, 4, 1), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - color: color ?? Colors.red, - ), - child: Text( - count.toString(), - style: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - ), - ), - ), - ) - else - Container(), - ], + count: count, + isLabelVisible: count > 0, + child: icon, + ), + onPressed: onPressed, + semanticsLabel: semanticsLabel, ); } } From 3096db571778ebbdef0d9b15377c323c16e0e9e1 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Sun, 15 Sep 2024 10:02:40 +0200 Subject: [PATCH 351/979] Correctly initialize and dispose services --- lib/src/app.dart | 22 +++-- .../model/challenge/challenge_service.dart | 93 ++++++++++++------- .../local_notification_service.dart | 10 +- 3 files changed, 74 insertions(+), 51 deletions(-) diff --git a/lib/src/app.dart b/lib/src/app.dart index 684a07efe8..b1e268f3f2 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -12,6 +12,7 @@ import 'package:lichess_mobile/main.dart'; import 'package:lichess_mobile/src/app_initialization.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; +import 'package:lichess_mobile/src/model/challenge/challenge_service.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/correspondence/correspondence_service.dart'; @@ -99,7 +100,8 @@ class Application extends ConsumerStatefulWidget { } class _AppState extends ConsumerState { - bool _correspondenceSynced = false; + /// Whether the app has checked for online status for the first time. + bool _firstTimeOnlineCheck = false; @override void initState() { @@ -107,13 +109,16 @@ class _AppState extends ConsumerState { setOptimalDisplayMode(); } + // Initialize services + ref.read(challengeServiceProvider).initialize(); + + // Listen for connectivity changes and perform actions accordingly. ref.listenManual(connectivityChangesProvider, (prev, current) async { + final prevWasOffline = prev?.value?.isOnline == false; + final currentIsOnline = current.value?.isOnline == true; + // Play registered moves whenever the app comes back online. - if (prev?.hasValue == true && - !prev!.value!.isOnline && - !current.isRefreshing && - current.hasValue && - current.value!.isOnline) { + if (prevWasOffline && currentIsOnline) { final nbMovesPlayed = await ref.read(correspondenceServiceProvider).playRegisteredMoves(); if (nbMovesPlayed > 0) { @@ -121,8 +126,9 @@ class _AppState extends ConsumerState { } } - if (current.value?.isOnline == true && !_correspondenceSynced) { - _correspondenceSynced = true; + // Perform actions once when the app comes online. + if (current.value?.isOnline == true && !_firstTimeOnlineCheck) { + _firstTimeOnlineCheck = true; ref.read(correspondenceServiceProvider).syncGames(); } diff --git a/lib/src/model/challenge/challenge_service.dart b/lib/src/model/challenge/challenge_service.dart index 93fc6ff131..8c173f1e4d 100644 --- a/lib/src/model/challenge/challenge_service.dart +++ b/lib/src/model/challenge/challenge_service.dart @@ -1,10 +1,12 @@ import 'dart:async'; +import 'package:collection/collection.dart'; import 'package:deep_pick/deep_pick.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/widgets.dart'; import 'package:lichess_mobile/src/model/challenge/challenge.dart'; import 'package:lichess_mobile/src/model/challenge/challenge_repository.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/notifications/challenge_notification.dart'; import 'package:lichess_mobile/src/model/notifications/local_notification_service.dart'; @@ -19,7 +21,9 @@ part 'challenge_service.g.dart'; @Riverpod(keepAlive: true) ChallengeService challengeService(ChallengeServiceRef ref) { - return ChallengeService(ref); + final service = ChallengeService(ref); + ref.onDispose(() => service.dispose()); + return service; } final _challengeStreamController = StreamController.broadcast(); @@ -37,40 +41,56 @@ class ChallengeService { ChallengesList? _current; ChallengesList? _previous; - void init() { - socketGlobalStream.listen((event) { - if (event.topic != 'challenges') return; - - final listPick = pick(event.data).required(); - final inward = listPick('in').asListOrEmpty(Challenge.fromPick); - final outward = listPick('out').asListOrEmpty(Challenge.fromPick); - - _previous = _current; - _current = (inward: inward.lock, outward: outward.lock); - _challengeStreamController.add(_current!); - - if (_previous == null) return; - final prevIds = _previous!.inward.map((challenge) => challenge.id); - final l10n = ref.read(l10nProvider).strings; - - // if a challenge was cancelled by the challenger - prevIds - .where((id) => !inward.map((challenge) => challenge.id).contains(id)) - .forEach( - (id) => ref - .read(localNotificationServiceProvider) - .cancel(id.value.hashCode), - ); - - // if there is a new challenge - inward.where((challenge) => !prevIds.contains(challenge.id)).forEach( - (challenge) => ref - .read(localNotificationServiceProvider) - .show(ChallengeNotification(challenge, l10n)), - ); - }); + StreamSubscription? _socketSubscription; + + /// Start listening to challenge events from the server. + void initialize() { + _socketSubscription = socketGlobalStream.listen(_onSocketEvent); + } + + void _onSocketEvent(SocketEvent event) { + if (event.topic != 'challenges') return; + + final listPick = pick(event.data).required(); + final inward = listPick('in').asListOrEmpty(Challenge.fromPick); + final outward = listPick('out').asListOrEmpty(Challenge.fromPick); + + _previous = _current; + _current = (inward: inward.lock, outward: outward.lock); + _challengeStreamController.add(_current!); + + final Iterable prevInwardIds = + _previous?.inward.map((challenge) => challenge.id) ?? []; + final Iterable currentInwardIds = + inward.map((challenge) => challenge.id); + + final l10n = ref.read(l10nProvider).strings; + + // if a challenge was cancelled by the challenger + prevInwardIds + .whereNot((challengeId) => currentInwardIds.contains(challengeId)) + .forEach( + (id) => ref + .read(localNotificationServiceProvider) + .cancel(id.value.hashCode), + ); + + // if there is a new challenge + inward + .whereNot((challenge) => prevInwardIds.contains(challenge.id)) + .forEach( + (challenge) => ref + .read(localNotificationServiceProvider) + .show(ChallengeNotification(challenge, l10n)), + ); + } + + /// Stop listening to challenge events from the server. + void dispose() { + _socketSubscription?.cancel(); } + /// Handle a local notification response. Future onNotificationResponse( int id, String? actionid, @@ -99,6 +119,10 @@ class ChallengeService { builder: (BuildContext context) => GameScreen(initialGameId: fullId), ); + case 'decline': + final repo = ref.read(challengeRepositoryProvider); + repo.decline(payload.id); + case null: final context = ref.read(currentNavigatorKeyProvider).currentContext!; final navState = Navigator.of(context); @@ -109,9 +133,6 @@ class ChallengeService { context, builder: (BuildContext context) => const ChallengeRequestsScreen(), ); - case 'decline': - final repo = ref.read(challengeRepositoryProvider); - repo.decline(payload.id); } } } diff --git a/lib/src/model/notifications/local_notification_service.dart b/lib/src/model/notifications/local_notification_service.dart index 37fb8c6d70..29b0370a8b 100644 --- a/lib/src/model/notifications/local_notification_service.dart +++ b/lib/src/model/notifications/local_notification_service.dart @@ -38,8 +38,6 @@ class LocalNotificationService { _updateLocalisations(); }); - _initServices(); - await _notificationPlugin.initialize( InitializationSettings( android: const AndroidInitializationSettings('logo_black'), @@ -59,10 +57,6 @@ class LocalNotificationService { _log.info('initialized'); } - void _initServices() { - _ref.read(challengeServiceProvider).init(); - } - void _updateLocalisations() { final l10n = _ref.read(l10nProvider); InfoNotificationDetails(l10n.strings); @@ -73,7 +67,7 @@ class LocalNotificationService { final id = notification.id; final payload = notification.payload != null ? jsonEncode(notification.payload!.toJson()) - : ''; + : null; await _notificationPlugin.show( id, @@ -140,10 +134,12 @@ class LocalNotificationService { ref .read(localNotificationServiceProvider) ._handleResponse(response.id!, response.actionId, payload); + ref.dispose(); } catch (e) { debugPrint( 'failed to parse notification payload: $e', ); // loggers dont work from the background isolate + ref.dispose(); } }, ); From 207e48fe0fad7cfa72d179a7920132bb9563e12e Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 16 Sep 2024 11:26:27 +0200 Subject: [PATCH 352/979] Setup Intl with saved locale on startup --- lib/main.dart | 19 +++++++++++++------ lib/src/intl.dart | 15 +++++++++++---- .../local_notification_service.dart | 3 +-- .../model/settings/general_preferences.dart | 10 ++++------ 4 files changed, 29 insertions(+), 18 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 982f02c275..83e1bdd8cf 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -16,6 +16,7 @@ import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/correspondence/correspondence_game_storage.dart'; import 'package:lichess_mobile/src/model/correspondence/offline_correspondence_game.dart'; import 'package:lichess_mobile/src/model/game/playable_game.dart'; +import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; import 'package:lichess_mobile/src/utils/badge_service.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:path/path.dart' as path; @@ -32,15 +33,21 @@ Future main() async { // logging setup setupLogger(); - // Intl and timeago setup - await setupIntl(); - SharedPreferences.setPrefix('lichess.'); + // Get locale from shared preferences, if any + final prefs = await SharedPreferences.getInstance(); + final json = prefs.getString(kGeneralPreferencesKey); + final generalPref = json != null + ? GeneralPrefsState.fromJson(jsonDecode(json) as Map) + : GeneralPrefsState.defaults; + final locale = generalPref.locale; + + // Intl and timeago setup + await setupIntl(locale); + // Firebase setup - await Firebase.initializeApp( - options: DefaultFirebaseOptions.currentPlatform, - ); + await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); // Crashlytics if (kReleaseMode) { diff --git a/lib/src/intl.dart b/lib/src/intl.dart index 47dad98260..507af5a7ab 100644 --- a/lib/src/intl.dart +++ b/lib/src/intl.dart @@ -1,11 +1,18 @@ +import 'dart:ui'; + import 'package:intl/intl.dart'; import 'package:intl/intl_standalone.dart'; import 'package:timeago/timeago.dart' as timeago; -Future setupIntl() async { - // we need call this because it is not setup automatically - final systemLocale = await findSystemLocale(); - Intl.defaultLocale = systemLocale; +/// Setup [Intl.defaultLocale] and [timeago.setLocaleMessages]. +Future setupIntl(Locale? locale) async { + if (locale != null) { + Intl.defaultLocale = locale.toLanguageTag(); + } else { + // we need call this because it is not setup automatically + final systemLocale = await findSystemLocale(); + Intl.defaultLocale = systemLocale; + } // we need to setup timeago locale manually final currentLocale = Intl.getCurrentLocale(); diff --git a/lib/src/model/notifications/local_notification_service.dart b/lib/src/model/notifications/local_notification_service.dart index 29b0370a8b..d68a0b5a01 100644 --- a/lib/src/model/notifications/local_notification_service.dart +++ b/lib/src/model/notifications/local_notification_service.dart @@ -42,9 +42,7 @@ class LocalNotificationService { InitializationSettings( android: const AndroidInitializationSettings('logo_black'), iOS: DarwinInitializationSettings( - requestAlertPermission: false, requestBadgePermission: false, - requestSoundPermission: false, notificationCategories: [ ChallengeNotificationDetails.instance.darwinNotificationCategory, ], @@ -164,6 +162,7 @@ class NotificationPayload with _$NotificationPayload { _$NotificationPayloadFromJson(json); } +/// A local notification that can be shown to the user. abstract class LocalNotification { LocalNotification( this.title, diff --git a/lib/src/model/settings/general_preferences.dart b/lib/src/model/settings/general_preferences.dart index 44cd15bb94..b12d1c50d2 100644 --- a/lib/src/model/settings/general_preferences.dart +++ b/lib/src/model/settings/general_preferences.dart @@ -12,16 +12,14 @@ import 'package:shared_preferences/shared_preferences.dart'; part 'general_preferences.freezed.dart'; part 'general_preferences.g.dart'; -const _prefKey = 'preferences.general'; +const kGeneralPreferencesKey = 'preferences.general'; @Riverpod(keepAlive: true) class GeneralPreferences extends _$GeneralPreferences { static GeneralPrefsState fetchFromStorage(SharedPreferences prefs) { - final stored = prefs.getString(_prefKey); + final stored = prefs.getString(kGeneralPreferencesKey); return stored != null - ? GeneralPrefsState.fromJson( - jsonDecode(stored) as Map, - ) + ? GeneralPrefsState.fromJson(jsonDecode(stored) as Map) : GeneralPrefsState.defaults; } @@ -73,7 +71,7 @@ class GeneralPreferences extends _$GeneralPreferences { Future _save(GeneralPrefsState newState) async { final prefs = ref.read(sharedPreferencesProvider); await prefs.setString( - _prefKey, + kGeneralPreferencesKey, jsonEncode(newState.toJson()), ); state = newState; From 7fbf6bdf6ad9be4f636a849c19862d725615d23c Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 16 Sep 2024 11:32:09 +0200 Subject: [PATCH 353/979] Rename and move pushNotificationService --- lib/src/app.dart | 8 ++++---- lib/src/intl.dart | 2 +- lib/src/model/auth/auth_controller.dart | 6 +++--- .../push_notification_service.dart} | 16 ++++++++-------- test/fake_notification_service.dart | 6 +++--- test/test_app.dart | 5 +++-- test/test_container.dart | 5 +++-- 7 files changed, 25 insertions(+), 23 deletions(-) rename lib/src/{notification_service.dart => model/notifications/push_notification_service.dart} (89%) diff --git a/lib/src/app.dart b/lib/src/app.dart index b1e268f3f2..0e01ff881e 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -17,11 +17,11 @@ import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/correspondence/correspondence_service.dart'; import 'package:lichess_mobile/src/model/notifications/local_notification_service.dart'; +import 'package:lichess_mobile/src/model/notifications/push_notification_service.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/model/settings/brightness.dart'; import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; import 'package:lichess_mobile/src/navigation.dart'; -import 'package:lichess_mobile/src/notification_service.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; @@ -333,7 +333,7 @@ class _EntryPointState extends ConsumerState<_EntryPointWidget> { Future _setupPushNotifications() async { // Listen for incoming messages while the app is in the foreground. FirebaseMessaging.onMessage.listen((RemoteMessage message) { - ref.read(notificationServiceProvider).processDataMessage(message); + ref.read(pushNotificationServiceProvider).processDataMessage(message); }); // Listen for incoming messages while the app is in the background. @@ -354,11 +354,11 @@ class _EntryPointState extends ConsumerState<_EntryPointWidget> { // Listen for token refresh and update the token on the server accordingly. _fcmTokenRefreshSubscription = FirebaseMessaging.instance.onTokenRefresh.listen((String token) { - ref.read(notificationServiceProvider).registerToken(token); + ref.read(pushNotificationServiceProvider).registerToken(token); }); // Register the device with the server. - await ref.read(notificationServiceProvider).registerDevice(); + await ref.read(pushNotificationServiceProvider).registerDevice(); // Get any messages which caused the application to open from // a terminated state. diff --git a/lib/src/intl.dart b/lib/src/intl.dart index 507af5a7ab..0fff2fa349 100644 --- a/lib/src/intl.dart +++ b/lib/src/intl.dart @@ -4,7 +4,7 @@ import 'package:intl/intl.dart'; import 'package:intl/intl_standalone.dart'; import 'package:timeago/timeago.dart' as timeago; -/// Setup [Intl.defaultLocale] and [timeago.setLocaleMessages]. +/// Setup [Intl.defaultLocale] and timeago locale and messages. Future setupIntl(Locale? locale) async { if (locale != null) { Intl.defaultLocale = locale.toLanguageTag(); diff --git a/lib/src/model/auth/auth_controller.dart b/lib/src/model/auth/auth_controller.dart index c87e9c1719..c5e68ea43d 100644 --- a/lib/src/model/auth/auth_controller.dart +++ b/lib/src/model/auth/auth_controller.dart @@ -1,7 +1,7 @@ import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; -import 'package:lichess_mobile/src/notification_service.dart'; +import 'package:lichess_mobile/src/model/notifications/push_notification_service.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'auth_repository.dart'; @@ -28,7 +28,7 @@ class AuthController extends _$AuthController { // register device and reconnect to the current socket once the session token is updated await Future.wait([ - ref.read(notificationServiceProvider).registerDevice(), + ref.read(pushNotificationServiceProvider).registerDevice(), // force reconnect to the current socket with the new token ref.read(socketPoolProvider).currentClient.connect(), ]); @@ -49,7 +49,7 @@ class AuthController extends _$AuthController { await ref.withClient( (client) => AuthRepository(client, appAuth).signOut(), ); - ref.read(notificationServiceProvider).unregister(); + ref.read(pushNotificationServiceProvider).unregister(); // force reconnect to the current socket ref.read(socketPoolProvider).currentClient.connect(); await ref.read(authSessionProvider.notifier).delete(); diff --git a/lib/src/notification_service.dart b/lib/src/model/notifications/push_notification_service.dart similarity index 89% rename from lib/src/notification_service.dart rename to lib/src/model/notifications/push_notification_service.dart index c3381d1c13..6a0087f116 100644 --- a/lib/src/notification_service.dart +++ b/lib/src/model/notifications/push_notification_service.dart @@ -11,22 +11,22 @@ import 'package:lichess_mobile/src/utils/badge_service.dart'; import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -part 'notification_service.g.dart'; +part 'push_notification_service.g.dart'; @Riverpod(keepAlive: true) -NotificationService notificationService( - NotificationServiceRef ref, +PushNotificationService pushNotificationService( + PushNotificationServiceRef ref, ) { - return NotificationService( - Logger('NotificationServiceService'), + return PushNotificationService( + Logger('PushNotificationService'), ref: ref, ); } -class NotificationService { - NotificationService(this._log, {required this.ref}); +class PushNotificationService { + PushNotificationService(this._log, {required this.ref}); - final NotificationServiceRef ref; + final PushNotificationServiceRef ref; final Logger _log; Future registerDevice() async { diff --git a/test/fake_notification_service.dart b/test/fake_notification_service.dart index 0c4387a2d3..f6b8badaa7 100644 --- a/test/fake_notification_service.dart +++ b/test/fake_notification_service.dart @@ -1,12 +1,12 @@ import 'package:firebase_messaging/firebase_messaging.dart'; -import 'package:lichess_mobile/src/notification_service.dart'; +import 'package:lichess_mobile/src/model/notifications/push_notification_service.dart'; -class FakeNotificationService implements NotificationService { +class FakeNotificationService implements PushNotificationService { @override Future processDataMessage(RemoteMessage message) async {} @override - NotificationServiceRef get ref => throw UnimplementedError(); + PushNotificationServiceRef get ref => throw UnimplementedError(); @override Future registerToken(String token) async {} diff --git a/test/test_app.dart b/test/test_app.dart index 43d87bb102..d465d6ae34 100644 --- a/test/test_app.dart +++ b/test/test_app.dart @@ -20,8 +20,8 @@ import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/game/game_storage.dart'; +import 'package:lichess_mobile/src/model/notifications/push_notification_service.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; -import 'package:lichess_mobile/src/notification_service.dart'; import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:logging/logging.dart'; import 'package:mocktail/mocktail.dart'; @@ -121,7 +121,8 @@ Future buildTestApp( return true; }), // ignore: scoped_providers_should_specify_dependencies - notificationServiceProvider.overrideWithValue(FakeNotificationService()), + pushNotificationServiceProvider + .overrideWithValue(FakeNotificationService()), // ignore: scoped_providers_should_specify_dependencies crashlyticsProvider.overrideWithValue(FakeCrashlytics()), // ignore: scoped_providers_should_specify_dependencies diff --git a/test/test_container.dart b/test/test_container.dart index adb11bddb6..782b0fd55a 100644 --- a/test/test_container.dart +++ b/test/test_container.dart @@ -14,7 +14,7 @@ import 'package:lichess_mobile/src/model/auth/session_storage.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; -import 'package:lichess_mobile/src/notification_service.dart'; +import 'package:lichess_mobile/src/model/notifications/push_notification_service.dart'; import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:logging/logging.dart'; import 'package:mocktail/mocktail.dart'; @@ -86,7 +86,8 @@ Future makeContainer({ }), defaultClientProvider.overrideWithValue(MockHttpClient()), crashlyticsProvider.overrideWithValue(FakeCrashlytics()), - notificationServiceProvider.overrideWithValue(FakeNotificationService()), + pushNotificationServiceProvider + .overrideWithValue(FakeNotificationService()), soundServiceProvider.overrideWithValue(FakeSoundService()), sharedPreferencesProvider.overrideWithValue(sharedPreferences), sessionStorageProvider.overrideWithValue(FakeSessionStorage()), From 91aab4d485eb25f0d601d550dc5e102ea67a96b6 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 16 Sep 2024 15:04:56 +0200 Subject: [PATCH 354/979] WIP on local notifications --- lib/main.dart | 28 +++- lib/src/intl.dart | 11 +- .../model/challenge/challenge_service.dart | 7 +- .../notifications/challenge_notification.dart | 81 +++++----- .../notifications/info_notification.dart | 34 ---- .../notifications/local_notification.dart | 59 +++++++ .../local_notification_service.dart | 150 +----------------- 7 files changed, 132 insertions(+), 238 deletions(-) delete mode 100644 lib/src/model/notifications/info_notification.dart create mode 100644 lib/src/model/notifications/local_notification.dart diff --git a/lib/main.dart b/lib/main.dart index 83e1bdd8cf..9721a9b147 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,8 +7,10 @@ import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/l10n/l10n.dart'; import 'package:lichess_mobile/src/db/database.dart'; import 'package:lichess_mobile/src/intl.dart'; import 'package:lichess_mobile/src/log.dart'; @@ -16,6 +18,8 @@ import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/correspondence/correspondence_game_storage.dart'; import 'package:lichess_mobile/src/model/correspondence/offline_correspondence_game.dart'; import 'package:lichess_mobile/src/model/game/playable_game.dart'; +import 'package:lichess_mobile/src/model/notifications/challenge_notification.dart'; +import 'package:lichess_mobile/src/model/notifications/local_notification.dart'; import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; import 'package:lichess_mobile/src/utils/badge_service.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; @@ -27,9 +31,13 @@ import 'firebase_options.dart'; import 'src/app.dart'; import 'src/utils/color_palette.dart'; +final _notificationPlugin = FlutterLocalNotificationsPlugin(); + Future main() async { final widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); + final systemLocale = widgetsBinding.platformDispatcher.locale; + // logging setup setupLogger(); @@ -41,11 +49,29 @@ Future main() async { final generalPref = json != null ? GeneralPrefsState.fromJson(jsonDecode(json) as Map) : GeneralPrefsState.defaults; - final locale = generalPref.locale; + final prefsLocale = generalPref.locale; + + final locale = prefsLocale ?? systemLocale; // Intl and timeago setup await setupIntl(locale); + // Local notifications setup + final l10n = await AppLocalizations.delegate.load(locale); + + await _notificationPlugin.initialize( + InitializationSettings( + android: const AndroidInitializationSettings('logo_black'), + iOS: DarwinInitializationSettings( + requestBadgePermission: false, + notificationCategories: [ + ChallengeNotification.darwinNotificationCategory(l10n), + ], + ), + ), + onDidReceiveNotificationResponse: onDidReceiveNotificationResponse, + ); + // Firebase setup await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); diff --git a/lib/src/intl.dart b/lib/src/intl.dart index 0fff2fa349..625a4c1eb7 100644 --- a/lib/src/intl.dart +++ b/lib/src/intl.dart @@ -1,18 +1,11 @@ import 'dart:ui'; import 'package:intl/intl.dart'; -import 'package:intl/intl_standalone.dart'; import 'package:timeago/timeago.dart' as timeago; /// Setup [Intl.defaultLocale] and timeago locale and messages. -Future setupIntl(Locale? locale) async { - if (locale != null) { - Intl.defaultLocale = locale.toLanguageTag(); - } else { - // we need call this because it is not setup automatically - final systemLocale = await findSystemLocale(); - Intl.defaultLocale = systemLocale; - } +Future setupIntl(Locale locale) async { + Intl.defaultLocale = locale.toLanguageTag(); // we need to setup timeago locale manually final currentLocale = Intl.getCurrentLocale(); diff --git a/lib/src/model/challenge/challenge_service.dart b/lib/src/model/challenge/challenge_service.dart index 8c173f1e4d..c2ce2b0ba0 100644 --- a/lib/src/model/challenge/challenge_service.dart +++ b/lib/src/model/challenge/challenge_service.dart @@ -70,17 +70,14 @@ class ChallengeService { prevInwardIds .whereNot((challengeId) => currentInwardIds.contains(challengeId)) .forEach( - (id) => ref - .read(localNotificationServiceProvider) - .cancel(id.value.hashCode), + (id) => LocalNotificationService.instance.cancel(id.value.hashCode), ); // if there is a new challenge inward .whereNot((challenge) => prevInwardIds.contains(challenge.id)) .forEach( - (challenge) => ref - .read(localNotificationServiceProvider) + (challenge) => LocalNotificationService.instance .show(ChallengeNotification(challenge, l10n)), ); } diff --git a/lib/src/model/notifications/challenge_notification.dart b/lib/src/model/notifications/challenge_notification.dart index 9ac58664c3..059e30dd76 100644 --- a/lib/src/model/notifications/challenge_notification.dart +++ b/lib/src/model/notifications/challenge_notification.dart @@ -1,16 +1,11 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:lichess_mobile/l10n/l10n.dart'; -import 'package:lichess_mobile/l10n/l10n_en.dart'; import 'package:lichess_mobile/src/model/challenge/challenge.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/model/notifications/local_notification_service.dart'; +import 'package:lichess_mobile/src/model/notifications/local_notification.dart'; -class ChallengeNotification extends LocalNotification { - ChallengeNotification(this._challenge, this._l10n) - : super( - '${_challenge.challenger!.user.name} challenges you!', - ChallengeNotificationDetails.instance.notificationDetails, - ); +class ChallengeNotification implements LocalNotification { + ChallengeNotification(this._challenge, this._l10n); final Challenge _challenge; final AppLocalizations _l10n; @@ -47,40 +42,8 @@ class ChallengeNotification extends LocalNotification { ? '${_l10n.rated} • $time' : '${_l10n.casual} • $time'; } -} - -class ChallengePayload { - const ChallengePayload(this.id); - - final ChallengeId id; - - NotificationPayload get notificationPayload => NotificationPayload( - type: PayloadType.challenge, - data: { - 'id': id.value, - }, - ); - - factory ChallengePayload.fromNotificationPayload( - NotificationPayload payload, - ) { - assert(payload.type == PayloadType.challenge); - final id = payload.data['id'] as String; - return ChallengePayload(ChallengeId(id)); - } -} - -class ChallengeNotificationDetails { - ChallengeNotificationDetails(this._l10n) { - ChallengeNotificationDetails.instance = this; - } - - // the default instance is set to english but this is overridden in LocalNotificationService.init() - static ChallengeNotificationDetails instance = - ChallengeNotificationDetails(AppLocalizationsEn()); - - final AppLocalizations _l10n; + @override NotificationDetails get notificationDetails => NotificationDetails( android: AndroidNotificationDetails( 'challenges', @@ -109,20 +72,27 @@ class ChallengeNotificationDetails { ), ); - DarwinNotificationCategory get darwinNotificationCategory => + @override + String get title => '${_challenge.challenger!.user.name} challenges you!'; + + static const darwinCategoryId = 'challenge-notification-category'; + + static DarwinNotificationCategory darwinNotificationCategory( + AppLocalizations l10n, + ) => DarwinNotificationCategory( - 'challenge-notification', + darwinCategoryId, actions: [ DarwinNotificationAction.plain( 'accept', - _l10n.accept, + l10n.accept, options: { DarwinNotificationActionOption.foreground, }, ), DarwinNotificationAction.plain( 'decline', - _l10n.decline, + l10n.decline, options: { DarwinNotificationActionOption.destructive, }, @@ -133,3 +103,24 @@ class ChallengeNotificationDetails { }, ); } + +class ChallengePayload { + const ChallengePayload(this.id); + + final ChallengeId id; + + NotificationPayload get notificationPayload => NotificationPayload( + type: PayloadType.challenge, + data: { + 'id': id.value, + }, + ); + + factory ChallengePayload.fromNotificationPayload( + NotificationPayload payload, + ) { + assert(payload.type == PayloadType.challenge); + final id = payload.data['id'] as String; + return ChallengePayload(ChallengeId(id)); + } +} diff --git a/lib/src/model/notifications/info_notification.dart b/lib/src/model/notifications/info_notification.dart deleted file mode 100644 index 6e58e247ad..0000000000 --- a/lib/src/model/notifications/info_notification.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:flutter_local_notifications/flutter_local_notifications.dart'; -import 'package:lichess_mobile/l10n/l10n.dart'; -import 'package:lichess_mobile/l10n/l10n_en.dart'; -import 'package:lichess_mobile/src/model/notifications/local_notification_service.dart'; - -class InfoNotificationDetails { - InfoNotificationDetails(this._l10n) { - InfoNotificationDetails.instance = this; - } - - // the default instance is set to english but this is overridden in LocalNotificationService.init() - static InfoNotificationDetails instance = - InfoNotificationDetails(AppLocalizationsEn()); - - // ignore: unused_field - final AppLocalizations _l10n; - - NotificationDetails get notificationDetails => const NotificationDetails( - android: AndroidNotificationDetails( - 'general', - 'General', - importance: Importance.high, - priority: Priority.high, - ), - ); -} - -class InfoNotification extends LocalNotification { - InfoNotification(String title, {super.body}) - : super(title, InfoNotificationDetails.instance.notificationDetails); - - @override - int get id => hashCode; -} diff --git a/lib/src/model/notifications/local_notification.dart b/lib/src/model/notifications/local_notification.dart new file mode 100644 index 0000000000..b98f3c874f --- /dev/null +++ b/lib/src/model/notifications/local_notification.dart @@ -0,0 +1,59 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:logging/logging.dart'; + +part 'local_notification.g.dart'; +part 'local_notification.freezed.dart'; + +final _logger = Logger('LocalNotification'); + +final StreamController<(NotificationResponse, NotificationPayload)> + _responseStreamController = StreamController.broadcast(); + +/// Stream of locale notification responses (when the user interacts with a notification). +final Stream<(NotificationResponse, NotificationPayload)> + localNotificationResponseStream = _responseStreamController.stream; + +/// Function called by the flutter_local_notifications plugin when a notification is received in the foreground. +void onDidReceiveNotificationResponse(NotificationResponse response) { + _logger.info('processing response in foreground. id [${response.id}]'); + + if (response.id == null || response.payload == null) return; + + try { + final payload = NotificationPayload.fromJson( + jsonDecode(response.payload!) as Map, + ); + _responseStreamController.add((response, payload)); + } catch (e) { + _logger.warning('Failed to parse notification payload: $e'); + } +} + +enum PayloadType { + info, + challenge, +} + +@Freezed(fromJson: true, toJson: true) +class NotificationPayload with _$NotificationPayload { + factory NotificationPayload({ + required PayloadType type, + required Map data, + }) = _NotificationPayload; + + factory NotificationPayload.fromJson(Map json) => + _$NotificationPayloadFromJson(json); +} + +/// A local notification that can be shown to the user. +abstract class LocalNotification { + int get id; + String get title; + String? get body; + NotificationPayload? get payload; + NotificationDetails get notificationDetails; +} diff --git a/lib/src/model/notifications/local_notification_service.dart b/lib/src/model/notifications/local_notification_service.dart index d68a0b5a01..853aa64e3a 100644 --- a/lib/src/model/notifications/local_notification_service.dart +++ b/lib/src/model/notifications/local_notification_service.dart @@ -1,65 +1,19 @@ import 'dart:async'; import 'dart:convert'; -import 'package:flutter/foundation.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:lichess_mobile/src/app_initialization.dart'; -import 'package:lichess_mobile/src/model/challenge/challenge_service.dart'; -import 'package:lichess_mobile/src/model/notifications/challenge_notification.dart'; -import 'package:lichess_mobile/src/model/notifications/info_notification.dart'; -import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; -import 'package:lichess_mobile/src/utils/l10n.dart'; +import 'package:lichess_mobile/src/model/notifications/local_notification.dart'; import 'package:logging/logging.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; -part 'local_notification_service.g.dart'; -part 'local_notification_service.freezed.dart'; - -@Riverpod(keepAlive: true) -LocalNotificationService localNotificationService( - LocalNotificationServiceRef ref, -) { - return LocalNotificationService(ref, Logger('LocalNotificationService')); -} +final _notificationPlugin = FlutterLocalNotificationsPlugin(); class LocalNotificationService { - LocalNotificationService(this._ref, this._log); - - final LocalNotificationServiceRef _ref; - final Logger _log; - final _notificationPlugin = FlutterLocalNotificationsPlugin(); + const LocalNotificationService._(this._log); - Future init() async { - _updateLocalisations(); - _ref.listen(generalPreferencesProvider, (prev, now) { - if (prev!.locale == now.locale) return; - _updateLocalisations(); - }); + static final instance = + LocalNotificationService._(Logger('LocalNotificationService')); - await _notificationPlugin.initialize( - InitializationSettings( - android: const AndroidInitializationSettings('logo_black'), - iOS: DarwinInitializationSettings( - requestBadgePermission: false, - notificationCategories: [ - ChallengeNotificationDetails.instance.darwinNotificationCategory, - ], - ), - ), - onDidReceiveNotificationResponse: _notificationResponse, - onDidReceiveBackgroundNotificationResponse: - _notificationBackgroundResponse, - ); - _log.info('initialized'); - } - - void _updateLocalisations() { - final l10n = _ref.read(l10nProvider); - InfoNotificationDetails(l10n.strings); - ChallengeNotificationDetails(l10n.strings); - } + final Logger _log; Future show(LocalNotification notification) async { final id = notification.id; @@ -85,96 +39,4 @@ class LocalNotificationService { _log.info('canceled notification id: [$id]'); return _notificationPlugin.cancel(id); } - - void _handleResponse(int id, String? actionId, NotificationPayload payload) { - switch (payload.type) { - case PayloadType.info: - break; - case PayloadType.challenge: - _ref.read(challengeServiceProvider).onNotificationResponse( - id, - actionId, - ChallengePayload.fromNotificationPayload(payload), - ); - } - } - - void _notificationResponse(NotificationResponse response) { - _log.info('processing response in foreground. id [${response.id}]'); - if (response.id == null || response.payload == null) return; - - try { - final payload = NotificationPayload.fromJson( - jsonDecode(response.payload!) as Map, - ); - _handleResponse(response.id!, response.actionId, payload); - } catch (e) { - _log.warning('Failed to parse notification payload: $e'); - } - } - - @pragma('vm:entry-point') - static void _notificationBackgroundResponse( - NotificationResponse response, - ) { - // create a new provider scope for the background isolate - final ref = ProviderContainer(); - - ref.listen( - appInitializationProvider, - (prev, now) { - if (!now.hasValue) return; - - try { - final payload = NotificationPayload.fromJson( - jsonDecode(response.payload!) as Map, - ); - ref - .read(localNotificationServiceProvider) - ._handleResponse(response.id!, response.actionId, payload); - ref.dispose(); - } catch (e) { - debugPrint( - 'failed to parse notification payload: $e', - ); // loggers dont work from the background isolate - ref.dispose(); - } - }, - ); - - ref.read(appInitializationProvider); - } -} - -enum PayloadType { - info, - challenge, -} - -@Freezed(fromJson: true, toJson: true) -class NotificationPayload with _$NotificationPayload { - factory NotificationPayload({ - required PayloadType type, - required Map data, - }) = _NotificationPayload; - - factory NotificationPayload.fromJson(Map json) => - _$NotificationPayloadFromJson(json); -} - -/// A local notification that can be shown to the user. -abstract class LocalNotification { - LocalNotification( - this.title, - this.notificationDetails, { - this.body, - this.payload, - }); - - int get id; - - final String title; - final String? body; - final NotificationPayload? payload; - final NotificationDetails notificationDetails; } From e93f9a6d41e520c8cd72bfcb853f554a959fcaec Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 17 Sep 2024 09:30:50 +0200 Subject: [PATCH 355/979] Prevent scrolling the board in opening explorer screen Close #1030 --- lib/src/view/analysis/analysis_board.dart | 33 +++++++------------ .../opening_explorer_screen.dart | 13 +++++--- 2 files changed, 20 insertions(+), 26 deletions(-) diff --git a/lib/src/view/analysis/analysis_board.dart b/lib/src/view/analysis/analysis_board.dart index 6a0eac77bb..7ecca1eac5 100644 --- a/lib/src/view/analysis/analysis_board.dart +++ b/lib/src/view/analysis/analysis_board.dart @@ -23,7 +23,6 @@ class AnalysisBoard extends ConsumerStatefulWidget { this.options, this.boardSize, { this.borderRadius, - this.disableDraggingPieces, }); final String pgn; @@ -31,10 +30,6 @@ class AnalysisBoard extends ConsumerStatefulWidget { final double boardSize; final BorderRadiusGeometry? borderRadius; - /// If true, the user won't be able to drag pieces. This settings is meant for - /// the opening explorer screen, where the user can drag the board. - final bool? disableDraggingPieces; - @override ConsumerState createState() => AnalysisBoardState(); } @@ -77,8 +72,6 @@ class AnalysisBoardState extends ConsumerState { ) : ISet(); - final boardPrefSettings = boardPrefs.toBoardSettings(); - return Chessboard( size: widget.boardSize, fen: analysisState.position.fen, @@ -108,20 +101,18 @@ class AnalysisBoardState extends ConsumerState { }) : IMap({sanMove.move.to: annotation}) : null, - settings: boardPrefSettings.copyWith( - borderRadius: widget.borderRadius, - boxShadow: - widget.borderRadius != null ? boardShadows : const [], - drawShape: DrawShapeOptions( - enable: true, - onCompleteShape: _onCompleteShape, - onClearShapes: _onClearShapes, - newShapeColor: boardPrefs.shapeColor.color, - ), - pieceShiftMethod: widget.disableDraggingPieces == true - ? PieceShiftMethod.tapTwoSquares - : boardPrefSettings.pieceShiftMethod, - ), + settings: boardPrefs.toBoardSettings().copyWith( + borderRadius: widget.borderRadius, + boxShadow: widget.borderRadius != null + ? boardShadows + : const [], + drawShape: DrawShapeOptions( + enable: true, + onCompleteShape: _onCompleteShape, + onClearShapes: _onClearShapes, + newShapeColor: boardPrefs.shapeColor.color, + ), + ), ); } diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index bcf741c33b..0e2d1dbc29 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -407,11 +407,14 @@ class _OpeningExplorerView extends StatelessWidget { ) : EdgeInsets.zero, children: [ - AnalysisBoard( - pgn, - options, - boardSize, - disableDraggingPieces: true, + GestureDetector( + // disable scrolling when dragging the board + onVerticalDragStart: (_) {}, + child: AnalysisBoard( + pgn, + options, + boardSize, + ), ), ...children, ], From 380be36486ba50142995dde7b8cdf38f293c3578 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 17 Sep 2024 11:31:45 +0200 Subject: [PATCH 356/979] Support background notification responses --- ios/Runner/AppDelegate.swift | 2 +- lib/main.dart | 42 +++++++++++++++- lib/src/app.dart | 23 +++++++-- lib/src/app_initialization.dart | 11 ---- .../model/challenge/challenge_service.dart | 13 +++-- .../notifications/challenge_notification.dart | 18 +++---- .../notifications/local_notification.dart | 23 ++++++--- .../local_notification_service.dart | 50 ++++++++++++++++++- .../view/user/challenge_requests_screen.dart | 3 +- 9 files changed, 141 insertions(+), 44 deletions(-) diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index a2bbd6786d..329a693791 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -42,7 +42,7 @@ import flutter_local_notifications result(self.getPhysicalMemory()) }) - // Taken from: https://github.com/MaikuB/flutter_local_notifications/blob/master/flutter_local_notifications/example/ios/Runner/AppDelegate.swift + // Cf: https://github.com/MaikuB/flutter_local_notifications/tree/master/flutter_local_notifications#notification-actions // This is required to make any communication available in the action isolate. FlutterLocalNotificationsPlugin.setPluginRegistrantCallback { (registry) in GeneratedPluginRegistrant.register(with: registry) diff --git a/lib/main.dart b/lib/main.dart index 9721a9b147..890de6f50c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -11,9 +11,11 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/l10n/l10n.dart'; +import 'package:lichess_mobile/src/app_initialization.dart'; import 'package:lichess_mobile/src/db/database.dart'; import 'package:lichess_mobile/src/intl.dart'; import 'package:lichess_mobile/src/log.dart'; +import 'package:lichess_mobile/src/model/challenge/challenge_repository.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/correspondence/correspondence_game_storage.dart'; import 'package:lichess_mobile/src/model/correspondence/offline_correspondence_game.dart'; @@ -58,7 +60,6 @@ Future main() async { // Local notifications setup final l10n = await AppLocalizations.delegate.load(locale); - await _notificationPlugin.initialize( InitializationSettings( android: const AndroidInitializationSettings('logo_black'), @@ -70,6 +71,7 @@ Future main() async { ), ), onDidReceiveNotificationResponse: onDidReceiveNotificationResponse, + onDidReceiveBackgroundNotificationResponse: notificationTapBackground, ); // Firebase setup @@ -129,6 +131,44 @@ Future main() async { ); } +@pragma('vm:entry-point') +void notificationTapBackground(NotificationResponse response) { + // create a new provider container for the background isolate + final ref = ProviderContainer(); + + ref.listen( + appInitializationProvider, + (prev, now) { + if (!now.hasValue) return; + + if (response.id == null || response.payload == null) return; + + try { + final payload = NotificationPayload.fromJson( + jsonDecode(response.payload!) as Map, + ); + switch (payload.type) { + case NotificationType.challenge: + // only decline action is supported in the background + if (response.actionId == 'decline') { + final challengeId = ChallengePayload.fromNotification(payload).id; + ref.read(challengeRepositoryProvider).decline(challengeId); + } + default: + debugPrint('Unknown notification type: $payload'); + } + + ref.dispose(); + } catch (e) { + debugPrint('Failed to handle notification background response: $e'); + ref.dispose(); + } + }, + ); + + ref.read(appInitializationProvider); +} + @pragma('vm:entry-point') Future firebaseMessagingBackgroundHandler(RemoteMessage message) async { debugPrint('Handling a background message: ${message.data}'); diff --git a/lib/src/app.dart b/lib/src/app.dart index 0e01ff881e..3bc3ba8822 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -14,6 +14,7 @@ import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/challenge/challenge_service.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/correspondence/correspondence_service.dart'; import 'package:lichess_mobile/src/model/notifications/local_notification_service.dart'; @@ -109,8 +110,7 @@ class _AppState extends ConsumerState { setOptimalDisplayMode(); } - // Initialize services - ref.read(challengeServiceProvider).initialize(); + preloadSoundAssets(); // Listen for connectivity changes and perform actions accordingly. ref.listenManual(connectivityChangesProvider, (prev, current) async { @@ -250,6 +250,17 @@ class _AppState extends ConsumerState { ); } + /// Preload sounds to avoid delays when playing them. + Future preloadSoundAssets() async { + final soundTheme = ref.read(generalPreferencesProvider).soundTheme; + final soundService = ref.read(soundServiceProvider); + try { + await soundService.initialize(soundTheme); + } catch (e) { + debugPrint('Cannot initialize SoundService: $e'); + } + } + // Code taken from https://stackoverflow.com/questions/63631522/flutter-120fps-issue /// Enables high refresh rate for devices where it was previously disabled Future setOptimalDisplayMode() async { @@ -307,6 +318,10 @@ class _EntryPointState extends ConsumerState<_EntryPointWidget> { void initState() { super.initState(); + // Initialize services + ref.read(localNotificationDispatcherProvider).initialize(); + ref.read(challengeServiceProvider).initialize(); + _connectivitySubscription = ref.listenManual(connectivityChangesProvider, (prev, current) async { // setup push notifications once when the app comes online @@ -315,12 +330,10 @@ class _EntryPointState extends ConsumerState<_EntryPointWidget> { await _setupPushNotifications(); _pushNotificationsSetup = true; } catch (e, st) { - debugPrint('Could not sync correspondence games; $e\n$st'); + debugPrint('Could not setup push notifications; $e\n$st'); } } }); - - ref.read(localNotificationServiceProvider).init(); } @override diff --git a/lib/src/app_initialization.dart b/lib/src/app_initialization.dart index 8aff94ad36..f341ea849f 100644 --- a/lib/src/app_initialization.dart +++ b/lib/src/app_initialization.dart @@ -10,10 +10,8 @@ import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/auth/bearer.dart'; import 'package:lichess_mobile/src/model/auth/session_storage.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; -import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; -import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; import 'package:lichess_mobile/src/utils/color_palette.dart'; import 'package:lichess_mobile/src/utils/string.dart'; import 'package:lichess_mobile/src/utils/system.dart'; @@ -50,15 +48,6 @@ Future appInitialization( prefs.setString('installed_version', appVersion.canonicalizedVersion); } - // preload sounds - final soundTheme = GeneralPreferences.fetchFromStorage(prefs).soundTheme; - final soundService = ref.read(soundServiceProvider); - try { - await soundService.initialize(soundTheme); - } catch (e) { - _logger.warning('Cannot initialize SoundService: $e'); - } - final db = await openDb(databaseFactory, dbPath); if (prefs.getBool('first_run') ?? true) { diff --git a/lib/src/model/challenge/challenge_service.dart b/lib/src/model/challenge/challenge_service.dart index c2ce2b0ba0..a1b9cf4fe6 100644 --- a/lib/src/model/challenge/challenge_service.dart +++ b/lib/src/model/challenge/challenge_service.dart @@ -9,6 +9,7 @@ import 'package:lichess_mobile/src/model/challenge/challenge_repository.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/notifications/challenge_notification.dart'; +import 'package:lichess_mobile/src/model/notifications/local_notification.dart'; import 'package:lichess_mobile/src/model/notifications/local_notification_service.dart'; import 'package:lichess_mobile/src/navigation.dart'; import 'package:lichess_mobile/src/utils/l10n.dart'; @@ -87,19 +88,21 @@ class ChallengeService { _socketSubscription?.cancel(); } - /// Handle a local notification response. + /// Handle a local notification response when the app is in the foreground. Future onNotificationResponse( int id, String? actionid, - ChallengePayload payload, + NotificationPayload payload, ) async { + final challengeId = ChallengePayload.fromNotification(payload).id; + switch (actionid) { case 'accept': final repo = ref.read(challengeRepositoryProvider); - await repo.accept(payload.id); + await repo.accept(challengeId); final fullId = await repo - .show(payload.id) + .show(challengeId) .then((challenge) => challenge.gameFullId); final context = ref.read(currentNavigatorKeyProvider).currentContext!; @@ -118,7 +121,7 @@ class ChallengeService { case 'decline': final repo = ref.read(challengeRepositoryProvider); - repo.decline(payload.id); + repo.decline(challengeId); case null: final context = ref.read(currentNavigatorKeyProvider).currentContext!; diff --git a/lib/src/model/notifications/challenge_notification.dart b/lib/src/model/notifications/challenge_notification.dart index 059e30dd76..e37774fed5 100644 --- a/lib/src/model/notifications/challenge_notification.dart +++ b/lib/src/model/notifications/challenge_notification.dart @@ -18,9 +18,10 @@ class ChallengeNotification implements LocalNotification { ChallengePayload(_challenge.id).notificationPayload; @override - String get body => _body(); + String get title => '${_challenge.challenger!.user.name} challenges you!'; - String _body() { + @override + String get body { final time = switch (_challenge.timeControl) { ChallengeTimeControlType.clock => () { final clock = _challenge.clock!; @@ -44,7 +45,7 @@ class ChallengeNotification implements LocalNotification { } @override - NotificationDetails get notificationDetails => NotificationDetails( + NotificationDetails get details => NotificationDetails( android: AndroidNotificationDetails( 'challenges', _l10n.preferencesNotifyChallenge, @@ -68,13 +69,10 @@ class ChallengeNotification implements LocalNotification { ], ), iOS: const DarwinNotificationDetails( - categoryIdentifier: 'challenge-notification', + categoryIdentifier: darwinCategoryId, ), ); - @override - String get title => '${_challenge.challenger!.user.name} challenges you!'; - static const darwinCategoryId = 'challenge-notification-category'; static DarwinNotificationCategory darwinNotificationCategory( @@ -110,16 +108,16 @@ class ChallengePayload { final ChallengeId id; NotificationPayload get notificationPayload => NotificationPayload( - type: PayloadType.challenge, + type: NotificationType.challenge, data: { 'id': id.value, }, ); - factory ChallengePayload.fromNotificationPayload( + factory ChallengePayload.fromNotification( NotificationPayload payload, ) { - assert(payload.type == PayloadType.challenge); + assert(payload.type == NotificationType.challenge); final id = payload.data['id'] as String; return ChallengePayload(ChallengeId(id)); } diff --git a/lib/src/model/notifications/local_notification.dart b/lib/src/model/notifications/local_notification.dart index b98f3c874f..9701710260 100644 --- a/lib/src/model/notifications/local_notification.dart +++ b/lib/src/model/notifications/local_notification.dart @@ -10,12 +10,19 @@ part 'local_notification.freezed.dart'; final _logger = Logger('LocalNotification'); -final StreamController<(NotificationResponse, NotificationPayload)> - _responseStreamController = StreamController.broadcast(); +/// A notification response and its id and payload. +typedef ParsedNotificationResponse = ( + int, + NotificationResponse, + NotificationPayload +); + +final StreamController _responseStreamController = + StreamController.broadcast(); /// Stream of locale notification responses (when the user interacts with a notification). -final Stream<(NotificationResponse, NotificationPayload)> - localNotificationResponseStream = _responseStreamController.stream; +final Stream localNotificationResponseStream = + _responseStreamController.stream; /// Function called by the flutter_local_notifications plugin when a notification is received in the foreground. void onDidReceiveNotificationResponse(NotificationResponse response) { @@ -27,13 +34,13 @@ void onDidReceiveNotificationResponse(NotificationResponse response) { final payload = NotificationPayload.fromJson( jsonDecode(response.payload!) as Map, ); - _responseStreamController.add((response, payload)); + _responseStreamController.add((response.id!, response, payload)); } catch (e) { _logger.warning('Failed to parse notification payload: $e'); } } -enum PayloadType { +enum NotificationType { info, challenge, } @@ -41,7 +48,7 @@ enum PayloadType { @Freezed(fromJson: true, toJson: true) class NotificationPayload with _$NotificationPayload { factory NotificationPayload({ - required PayloadType type, + required NotificationType type, required Map data, }) = _NotificationPayload; @@ -55,5 +62,5 @@ abstract class LocalNotification { String get title; String? get body; NotificationPayload? get payload; - NotificationDetails get notificationDetails; + NotificationDetails get details; } diff --git a/lib/src/model/notifications/local_notification_service.dart b/lib/src/model/notifications/local_notification_service.dart index 853aa64e3a..6b6376201b 100644 --- a/lib/src/model/notifications/local_notification_service.dart +++ b/lib/src/model/notifications/local_notification_service.dart @@ -2,11 +2,16 @@ import 'dart:async'; import 'dart:convert'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:lichess_mobile/src/model/challenge/challenge_service.dart'; import 'package:lichess_mobile/src/model/notifications/local_notification.dart'; import 'package:logging/logging.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'local_notification_service.g.dart'; final _notificationPlugin = FlutterLocalNotificationsPlugin(); +/// A service that manages local notifications. class LocalNotificationService { const LocalNotificationService._(this._log); @@ -15,6 +20,7 @@ class LocalNotificationService { final Logger _log; + /// Show a local notification. Future show(LocalNotification notification) async { final id = notification.id; final payload = notification.payload != null @@ -25,7 +31,7 @@ class LocalNotificationService { id, notification.title, notification.body, - notification.notificationDetails, + notification.details, payload: payload, ); _log.info( @@ -35,8 +41,50 @@ class LocalNotificationService { return id; } + /// Cancel a local notification. Future cancel(int id) async { _log.info('canceled notification id: [$id]'); return _notificationPlugin.cancel(id); } } + +/// A service that dispatches user interaction responses from local notifications to the appropriate handlers. +class LocalNotificationDispatcher { + LocalNotificationDispatcher(this.ref); + + final LocalNotificationDispatcherRef ref; + + StreamSubscription? _responseSubscription; + + /// Start listening for notification responses. + void initialize() { + _responseSubscription = localNotificationResponseStream.listen( + (data) { + final (notifId, response, payload) = data; + switch (payload.type) { + case NotificationType.challenge: + ref.read(challengeServiceProvider).onNotificationResponse( + notifId, + response.actionId, + payload, + ); + case NotificationType.info: + break; + } + }, + ); + } + + void onDispose() { + _responseSubscription?.cancel(); + } +} + +@Riverpod(keepAlive: true) +LocalNotificationDispatcher localNotificationDispatcher( + LocalNotificationDispatcherRef ref, +) { + final service = LocalNotificationDispatcher(ref); + ref.onDispose(service.onDispose); + return service; +} diff --git a/lib/src/view/user/challenge_requests_screen.dart b/lib/src/view/user/challenge_requests_screen.dart index 86c8080c07..631adf378d 100644 --- a/lib/src/view/user/challenge_requests_screen.dart +++ b/lib/src/view/user/challenge_requests_screen.dart @@ -115,8 +115,7 @@ class _Body extends ConsumerWidget { ref .read(challengeRepositoryProvider) .decline(challenge.id); - ref - .read(localNotificationServiceProvider) + LocalNotificationService.instance .cancel(challenge.id.value.hashCode); }, ); From 2ddc49569c3e1c210ac0cdc01e74696117699739 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 17 Sep 2024 12:09:59 +0200 Subject: [PATCH 357/979] Do not initialize the database early --- lib/src/app_initialization.dart | 9 --- lib/src/db/database.dart | 10 ++- .../correspondence_game_storage.dart | 8 +-- .../correspondence_service.dart | 69 ++++++++++--------- lib/src/model/game/chat_controller.dart | 4 +- lib/src/model/game/game_controller.dart | 12 ++-- lib/src/model/game/game_history.dart | 12 ++-- .../model/game/game_repository_providers.dart | 2 +- lib/src/model/game/game_storage.dart | 6 +- .../model/puzzle/puzzle_batch_storage.dart | 4 +- lib/src/model/puzzle/puzzle_controller.dart | 10 +-- lib/src/model/puzzle/puzzle_providers.dart | 14 ++-- lib/src/model/puzzle/puzzle_service.dart | 6 +- lib/src/model/puzzle/puzzle_storage.dart | 4 +- .../offline_correspondence_game_screen.dart | 10 +-- .../correspondence_game_storage_test.dart | 3 +- test/model/game/game_storage_test.dart | 4 +- .../puzzle/puzzle_batch_storage_test.dart | 4 +- test/model/puzzle/puzzle_service_test.dart | 42 +++++------ test/model/puzzle/puzzle_storage_test.dart | 2 +- test/test_app.dart | 9 +-- test/test_container.dart | 4 -- 22 files changed, 120 insertions(+), 128 deletions(-) diff --git a/lib/src/app_initialization.dart b/lib/src/app_initialization.dart index f341ea849f..78cd02388b 100644 --- a/lib/src/app_initialization.dart +++ b/lib/src/app_initialization.dart @@ -4,7 +4,6 @@ import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:lichess_mobile/src/db/database.dart'; import 'package:lichess_mobile/src/db/secure_storage.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/auth/bearer.dart'; @@ -17,11 +16,9 @@ import 'package:lichess_mobile/src/utils/string.dart'; import 'package:lichess_mobile/src/utils/system.dart'; import 'package:logging/logging.dart'; import 'package:package_info_plus/package_info_plus.dart'; -import 'package:path/path.dart' as p; import 'package:pub_semver/pub_semver.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:sqflite/sqflite.dart'; part 'app_initialization.freezed.dart'; part 'app_initialization.g.dart'; @@ -38,8 +35,6 @@ Future appInitialization( final deviceInfo = await DeviceInfoPlugin().deviceInfo; final prefs = await SharedPreferences.getInstance(); - final dbPath = p.join(await getDatabasesPath(), kLichessDatabaseName); - final appVersion = Version.parse(pInfo.version); final installedVersion = prefs.getString('installed_version'); @@ -48,8 +43,6 @@ Future appInitialization( prefs.setString('installed_version', appVersion.canonicalizedVersion); } - final db = await openDb(databaseFactory, dbPath); - if (prefs.getBool('first_run') ?? true) { // Clear secure storage on first run because it is not deleted on app uninstall await secureStorage.deleteAll(); @@ -117,7 +110,6 @@ Future appInitialization( deviceInfo: deviceInfo, sharedPreferences: prefs, userSession: await sessionStorage.read(), - database: db, sri: sri, engineMaxMemoryInMb: engineMaxMemory, ); @@ -130,7 +122,6 @@ class AppInitializationData with _$AppInitializationData { required BaseDeviceInfo deviceInfo, required SharedPreferences sharedPreferences, required AuthSessionState? userSession, - required Database database, required String sri, required int engineMaxMemoryInMb, }) = _AppInitializationData; diff --git a/lib/src/db/database.dart b/lib/src/db/database.dart index fd3e8ca06a..ba4279d75f 100644 --- a/lib/src/db/database.dart +++ b/lib/src/db/database.dart @@ -1,6 +1,5 @@ import 'dart:io'; -import 'package:lichess_mobile/src/app_initialization.dart'; import 'package:path/path.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:sqflite/sqflite.dart'; @@ -17,10 +16,9 @@ const chatReadMessagesTTL = Duration(days: 60); const kStorageAnonId = '**anonymous**'; @Riverpod(keepAlive: true) -Database database(DatabaseRef ref) { - // requireValue is possible because appInitializationProvider is loaded before - // anything. See: lib/src/app.dart - final db = ref.read(appInitializationProvider).requireValue.database; +Future database(DatabaseRef ref) async { + final dbPath = join(await getDatabasesPath(), kLichessDatabaseName); + final db = await openDb(databaseFactory, dbPath); ref.onDispose(db.close); return db; } @@ -28,7 +26,7 @@ Database database(DatabaseRef ref) { /// Returns the sqlite version as an integer. @Riverpod(keepAlive: true) Future sqliteVersion(SqliteVersionRef ref) async { - final db = ref.read(databaseProvider); + final db = await ref.read(databaseProvider.future); try { final versionStr = (await db.rawQuery('SELECT sqlite_version()')) .first diff --git a/lib/src/model/correspondence/correspondence_game_storage.dart b/lib/src/model/correspondence/correspondence_game_storage.dart index 36fc0d1e56..7a56f11ca6 100644 --- a/lib/src/model/correspondence/correspondence_game_storage.dart +++ b/lib/src/model/correspondence/correspondence_game_storage.dart @@ -12,10 +12,10 @@ import 'offline_correspondence_game.dart'; part 'correspondence_game_storage.g.dart'; @Riverpod(keepAlive: true) -CorrespondenceGameStorage correspondenceGameStorage( +Future correspondenceGameStorage( CorrespondenceGameStorageRef ref, -) { - final db = ref.watch(databaseProvider); +) async { + final db = await ref.watch(databaseProvider.future); return CorrespondenceGameStorage(db, ref); } @@ -27,7 +27,7 @@ Future> final session = ref.watch(authSessionProvider); // cannot use ref.watch because it would create a circular dependency // as we invalidate this provider in the storage save and delete methods - final storage = ref.read(correspondenceGameStorageProvider); + final storage = await ref.read(correspondenceGameStorageProvider.future); final data = await storage.fetchOngoingGames(session?.user.id); return data.sort( (a, b) { diff --git a/lib/src/model/correspondence/correspondence_service.dart b/lib/src/model/correspondence/correspondence_service.dart index 21d1d6d25f..8a06913f8d 100644 --- a/lib/src/model/correspondence/correspondence_service.dart +++ b/lib/src/model/correspondence/correspondence_service.dart @@ -45,7 +45,7 @@ class CorrespondenceService { await playRegisteredMoves(); final storedOngoingGames = - await _storage.fetchOngoingGames(_session?.user.id); + await (await _storage).fetchOngoingGames(_session?.user.id); ref.withClient((client) async { try { @@ -60,7 +60,7 @@ class CorrespondenceService { _log.info( 'Deleting correspondence game ${sg.$2.id} because it is not present on the server anymore', ); - _storage.delete(sg.$2.id); + (await _storage).delete(sg.$2.id); } } @@ -87,10 +87,11 @@ class CorrespondenceService { Future playRegisteredMoves() async { _log.info('Playing registered correspondence moves...'); - final games = - await _storage.fetchGamesWithRegisteredMove(_session?.user.id).then( - (games) => games.map((e) => e.$2).toList(), - ); + final games = await (await _storage) + .fetchGamesWithRegisteredMove(_session?.user.id) + .then( + (games) => games.map((e) => e.$2).toList(), + ); WebSocket.userAgent = ref.read(userAgentProvider); final Map wsHeaders = _session != null @@ -157,11 +158,11 @@ class CorrespondenceService { await movePlayedCompleter.future.timeout(const Duration(seconds: 3)); - ref.read(correspondenceGameStorageProvider).save( - gameToSync.copyWith( - registeredMoveAtPgn: null, - ), - ); + (await ref.read(correspondenceGameStorageProvider.future)).save( + gameToSync.copyWith( + registeredMoveAtPgn: null, + ), + ); } else { _log.info( 'Cannot play game ${gameToSync.id} move because its state has changed', @@ -184,31 +185,31 @@ class CorrespondenceService { } Future updateGame(GameFullId fullId, PlayableGame game) async { - return ref.read(correspondenceGameStorageProvider).save( - OfflineCorrespondenceGame( - id: game.id, - fullId: fullId, - meta: game.meta, - rated: game.meta.rated, - steps: game.steps, - initialFen: game.initialFen, - status: game.status, - variant: game.meta.variant, - speed: game.meta.speed, - perf: game.meta.perf, - white: game.white, - black: game.black, - youAre: game.youAre!, - daysPerTurn: game.meta.daysPerTurn, - clock: game.correspondenceClock, - winner: game.winner, - isThreefoldRepetition: game.isThreefoldRepetition, - ), - ); + return (await ref.read(correspondenceGameStorageProvider.future)).save( + OfflineCorrespondenceGame( + id: game.id, + fullId: fullId, + meta: game.meta, + rated: game.meta.rated, + steps: game.steps, + initialFen: game.initialFen, + status: game.status, + variant: game.meta.variant, + speed: game.meta.speed, + perf: game.meta.perf, + white: game.white, + black: game.black, + youAre: game.youAre!, + daysPerTurn: game.meta.daysPerTurn, + clock: game.correspondenceClock, + winner: game.winner, + isThreefoldRepetition: game.isThreefoldRepetition, + ), + ); } AuthSessionState? get _session => ref.read(authSessionProvider); - CorrespondenceGameStorage get _storage => - ref.read(correspondenceGameStorageProvider); + Future get _storage => + ref.read(correspondenceGameStorageProvider.future); } diff --git a/lib/src/model/game/chat_controller.dart b/lib/src/model/game/chat_controller.dart index d9e711e7d5..1d4f8c9cc2 100644 --- a/lib/src/model/game/chat_controller.dart +++ b/lib/src/model/game/chat_controller.dart @@ -69,7 +69,7 @@ class ChatController extends _$ChatController { } Future _getReadMessagesCount() async { - final db = ref.read(databaseProvider); + final db = await ref.read(databaseProvider.future); final result = await db.query( _tableName, columns: ['nbRead'], @@ -80,7 +80,7 @@ class ChatController extends _$ChatController { } Future _setReadMessagesCount(int count) async { - final db = ref.read(databaseProvider); + final db = await ref.read(databaseProvider.future); await db.insert( _tableName, { diff --git a/lib/src/model/game/game_controller.dart b/lib/src/model/game/game_controller.dart index c4cd6830c3..79c42d253a 100644 --- a/lib/src/model/game/game_controller.dart +++ b/lib/src/model/game/game_controller.dart @@ -700,10 +700,7 @@ class GameController extends _$GameController { state = AsyncValue.data( state.requireValue.copyWith(game: game), ); - - ref - .read(gameStorageProvider) - .save(game.toArchivedGame(finishedAt: DateTime.now())); + _storeGame(game); }, (e, s) { _logger.warning('Could not get post game data', e, s); }); @@ -910,6 +907,13 @@ class GameController extends _$GameController { } } + Future _storeGame(PlayableGame game) async { + if (game.finished) { + (await ref.read(gameStorageProvider.future)) + .save(game.toArchivedGame(finishedAt: DateTime.now())); + } + } + FutureResult _getPostGameData() { return Result.capture( ref.withClient( diff --git a/lib/src/model/game/game_history.dart b/lib/src/model/game/game_history.dart index 0ba4a51b5f..fa71d886b5 100644 --- a/lib/src/model/game/game_history.dart +++ b/lib/src/model/game/game_history.dart @@ -45,7 +45,7 @@ Future> myRecentGames( const Duration(hours: 1), ); } else { - final storage = ref.watch(gameStorageProvider); + final storage = await ref.watch(gameStorageProvider.future); ref.cacheFor(const Duration(hours: 1)); return storage .page(userId: session?.user.id, max: kNumberOfRecentGames) @@ -94,7 +94,8 @@ Future userNumberOfGames( ) : session != null && isOnline ? ref.watch(accountProvider.selectAsync((u) => u?.count?.all ?? 0)) - : ref.watch(gameStorageProvider).count(userId: user?.id); + : (await ref.watch(gameStorageProvider.future)) + .count(userId: user?.id); } /// A provider that paginates the game history for a given user, or the current app user if no user is provided. @@ -125,7 +126,7 @@ class UserGameHistory extends _$UserGameHistory { final session = ref.watch(authSessionProvider); final online = await ref .watch(connectivityChangesProvider.selectAsync((c) => c.isOnline)); - final storage = ref.watch(gameStorageProvider); + final storage = await ref.watch(gameStorageProvider.future); final id = userId ?? session?.user.id; final recentGames = id != null && online @@ -157,7 +158,7 @@ class UserGameHistory extends _$UserGameHistory { } /// Fetches the next page of games. - void getNext() { + Future getNext() async { if (!state.hasValue) return; final currentVal = state.requireValue; @@ -181,8 +182,7 @@ class UserGameHistory extends _$UserGameHistory { filter: currentVal.filter, ), ) - : ref - .watch(gameStorageProvider) + : (await ref.watch(gameStorageProvider.future)) .page(max: _nbPerPage, until: _list.last.game.createdAt) .then( (value) => value diff --git a/lib/src/model/game/game_repository_providers.dart b/lib/src/model/game/game_repository_providers.dart index dded6e0df1..5006f2c6c3 100644 --- a/lib/src/model/game/game_repository_providers.dart +++ b/lib/src/model/game/game_repository_providers.dart @@ -15,7 +15,7 @@ Future archivedGame( ArchivedGameRef ref, { required GameId id, }) async { - final gameStorage = ref.watch(gameStorageProvider); + final gameStorage = await ref.watch(gameStorageProvider.future); final game = await gameStorage.fetch(gameId: id); if (game != null) return game; return ref.withClientCacheFor( diff --git a/lib/src/model/game/game_storage.dart b/lib/src/model/game/game_storage.dart index b28ff6728b..dc89b9b04b 100644 --- a/lib/src/model/game/game_storage.dart +++ b/lib/src/model/game/game_storage.dart @@ -11,10 +11,10 @@ import 'package:sqflite/sqflite.dart'; part 'game_storage.g.dart'; @Riverpod(keepAlive: true) -GameStorage gameStorage( +Future gameStorage( GameStorageRef ref, -) { - final db = ref.watch(databaseProvider); +) async { + final db = await ref.watch(databaseProvider.future); return GameStorage(db); } diff --git a/lib/src/model/puzzle/puzzle_batch_storage.dart b/lib/src/model/puzzle/puzzle_batch_storage.dart index f110930790..da828b5dba 100644 --- a/lib/src/model/puzzle/puzzle_batch_storage.dart +++ b/lib/src/model/puzzle/puzzle_batch_storage.dart @@ -16,8 +16,8 @@ part 'puzzle_batch_storage.freezed.dart'; part 'puzzle_batch_storage.g.dart'; @Riverpod(keepAlive: true) -PuzzleBatchStorage puzzleBatchStorage(PuzzleBatchStorageRef ref) { - final database = ref.watch(databaseProvider); +Future puzzleBatchStorage(PuzzleBatchStorageRef ref) async { + final database = await ref.watch(databaseProvider.future); return PuzzleBatchStorage(database); } diff --git a/lib/src/model/puzzle/puzzle_controller.dart b/lib/src/model/puzzle/puzzle_controller.dart index bfe0e64972..349a0a55a7 100644 --- a/lib/src/model/puzzle/puzzle_controller.dart +++ b/lib/src/model/puzzle/puzzle_controller.dart @@ -41,9 +41,9 @@ class PuzzleController extends _$PuzzleController { final _engineEvalDebounce = Debouncer(const Duration(milliseconds: 100)); - late final _service = ref.read(puzzleServiceFactoryProvider)( - queueLength: kPuzzleLocalQueueLength, - ); + Future get _service => ref.read(puzzleServiceFactoryProvider)( + queueLength: kPuzzleLocalQueueLength, + ); @override PuzzleState build( PuzzleContext initialContext, { @@ -237,7 +237,7 @@ class PuzzleController extends _$PuzzleController { ) .setDifficulty(difficulty); - final nextPuzzle = _service.resetBatch( + final nextPuzzle = (await _service).resetBatch( userId: initialContext.userId, angle: initialContext.angle, ); @@ -348,7 +348,7 @@ class PuzzleController extends _$PuzzleController { final soundService = ref.read(soundServiceProvider); if (state.streak == null) { - final next = await _service.solve( + final next = await (await _service).solve( userId: initialContext.userId, angle: initialContext.angle, puzzle: state.puzzle, diff --git a/lib/src/model/puzzle/puzzle_providers.dart b/lib/src/model/puzzle/puzzle_providers.dart index 13d71c7662..de22f470ff 100644 --- a/lib/src/model/puzzle/puzzle_providers.dart +++ b/lib/src/model/puzzle/puzzle_providers.dart @@ -21,9 +21,9 @@ part 'puzzle_providers.g.dart'; Future nextPuzzle( NextPuzzleRef ref, PuzzleAngle angle, -) { +) async { final session = ref.watch(authSessionProvider); - final puzzleService = ref.read(puzzleServiceFactoryProvider)( + final puzzleService = await ref.read(puzzleServiceFactoryProvider)( queueLength: kPuzzleLocalQueueLength, ); @@ -46,7 +46,7 @@ Future storm(StormRef ref) { /// Fetches a puzzle from the local storage if available, otherwise fetches it from the server. @riverpod Future puzzle(PuzzleRef ref, PuzzleId id) async { - final puzzleStorage = ref.watch(puzzleStorageProvider); + final puzzleStorage = await ref.watch(puzzleStorageProvider.future); final puzzle = await puzzleStorage.fetch(puzzleId: id); if (puzzle != null) return puzzle; return ref.withClient((client) => PuzzleRepository(client).fetch(id)); @@ -61,9 +61,11 @@ Future dailyPuzzle(DailyPuzzleRef ref) { } @riverpod -Future> savedThemeBatches(SavedThemeBatchesRef ref) { +Future> savedThemeBatches( + SavedThemeBatchesRef ref, +) async { final session = ref.watch(authSessionProvider); - final storage = ref.watch(puzzleBatchStorageProvider); + final storage = await ref.watch(puzzleBatchStorageProvider.future); return storage.fetchSavedThemes(userId: session?.user.id); } @@ -72,7 +74,7 @@ Future> savedOpeningBatches( SavedOpeningBatchesRef ref, ) async { final session = ref.watch(authSessionProvider); - final storage = ref.watch(puzzleBatchStorageProvider); + final storage = await ref.watch(puzzleBatchStorageProvider.future); return storage.fetchSavedOpenings(userId: session?.user.id); } diff --git a/lib/src/model/puzzle/puzzle_service.dart b/lib/src/model/puzzle/puzzle_service.dart index 1e0a01dc56..5ca5cf52e3 100644 --- a/lib/src/model/puzzle/puzzle_service.dart +++ b/lib/src/model/puzzle/puzzle_service.dart @@ -33,11 +33,11 @@ class PuzzleServiceFactory { final PuzzleServiceFactoryRef _ref; - PuzzleService call({required int queueLength}) { + Future call({required int queueLength}) async { return PuzzleService( _ref, - batchStorage: _ref.read(puzzleBatchStorageProvider), - puzzleStorage: _ref.read(puzzleStorageProvider), + batchStorage: await _ref.read(puzzleBatchStorageProvider.future), + puzzleStorage: await _ref.read(puzzleStorageProvider.future), queueLength: queueLength, ); } diff --git a/lib/src/model/puzzle/puzzle_storage.dart b/lib/src/model/puzzle/puzzle_storage.dart index 39fa23a440..ef146a1d6f 100644 --- a/lib/src/model/puzzle/puzzle_storage.dart +++ b/lib/src/model/puzzle/puzzle_storage.dart @@ -9,8 +9,8 @@ import 'package:sqflite/sqflite.dart'; part 'puzzle_storage.g.dart'; @Riverpod(keepAlive: true) -PuzzleStorage puzzleStorage(PuzzleStorageRef ref) { - final db = ref.watch(databaseProvider); +Future puzzleStorage(PuzzleStorageRef ref) async { + final db = await ref.watch(databaseProvider.future); return PuzzleStorage(db); } diff --git a/lib/src/view/correspondence/offline_correspondence_game_screen.dart b/lib/src/view/correspondence/offline_correspondence_game_screen.dart index a142b27025..bc0477fd10 100644 --- a/lib/src/view/correspondence/offline_correspondence_game_screen.dart +++ b/lib/src/view/correspondence/offline_correspondence_game_screen.dart @@ -388,7 +388,7 @@ class _BodyState extends ConsumerState<_Body> { } } - void confirmMove() { + Future confirmMove() async { setState(() { game = game.copyWith( registeredMoveAtPgn: (moveToConfirm!.$1, moveToConfirm!.$2), @@ -396,7 +396,8 @@ class _BodyState extends ConsumerState<_Body> { moveToConfirm = null; }); - ref.read(correspondenceGameStorageProvider).save(game); + final storage = await ref.read(correspondenceGameStorageProvider.future); + storage.save(game); } void cancelMove() { @@ -409,7 +410,7 @@ class _BodyState extends ConsumerState<_Body> { }); } - void deleteRegisteredMove() { + Future deleteRegisteredMove() async { setState(() { stepCursor = stepCursor - 1; game = game.copyWith( @@ -418,7 +419,8 @@ class _BodyState extends ConsumerState<_Body> { ); }); - ref.read(correspondenceGameStorageProvider).save(game); + final storage = await ref.read(correspondenceGameStorageProvider.future); + storage.save(game); } Side? get activeClockSide { diff --git a/test/model/correspondence/correspondence_game_storage_test.dart b/test/model/correspondence/correspondence_game_storage_test.dart index 400e843d62..b62700c9be 100644 --- a/test/model/correspondence/correspondence_game_storage_test.dart +++ b/test/model/correspondence/correspondence_game_storage_test.dart @@ -34,7 +34,8 @@ void main() { ], ); - final storage = container.read(correspondenceGameStorageProvider); + final storage = + await container.read(correspondenceGameStorageProvider.future); await storage.save(corresGame); expect( diff --git a/test/model/game/game_storage_test.dart b/test/model/game/game_storage_test.dart index 3cee848c15..aaf562eb26 100644 --- a/test/model/game/game_storage_test.dart +++ b/test/model/game/game_storage_test.dart @@ -34,7 +34,7 @@ void main() { ], ); - final storage = container.read(gameStorageProvider); + final storage = await container.read(gameStorageProvider.future); await storage.save(game); expect( @@ -57,7 +57,7 @@ void main() { ], ); - final storage = container.read(gameStorageProvider); + final storage = await container.read(gameStorageProvider.future); for (final game in games) { await storage.save(game); diff --git a/test/model/puzzle/puzzle_batch_storage_test.dart b/test/model/puzzle/puzzle_batch_storage_test.dart index cb1508e685..bd8bd173c4 100644 --- a/test/model/puzzle/puzzle_batch_storage_test.dart +++ b/test/model/puzzle/puzzle_batch_storage_test.dart @@ -29,7 +29,7 @@ void main() { ], ); - final storage = container.read(puzzleBatchStorageProvider); + final storage = await container.read(puzzleBatchStorageProvider.future); await storage.save( userId: null, @@ -58,7 +58,7 @@ void main() { ], ); - final storage = container.read(puzzleBatchStorageProvider); + final storage = await container.read(puzzleBatchStorageProvider.future); await storage.save( userId: null, diff --git a/test/model/puzzle/puzzle_service_test.dart b/test/model/puzzle/puzzle_service_test.dart index 734b3b15b2..c5ef75b830 100644 --- a/test/model/puzzle/puzzle_service_test.dart +++ b/test/model/puzzle/puzzle_service_test.dart @@ -51,8 +51,8 @@ void main() { }); final container = await makeTestContainer(mockClient); - final storage = container.read(puzzleBatchStorageProvider); - final service = container.read(puzzleServiceFactoryProvider)( + final storage = await container.read(puzzleBatchStorageProvider.future); + final service = await container.read(puzzleServiceFactoryProvider)( queueLength: 3, ); @@ -79,8 +79,8 @@ void main() { }); final container = await makeTestContainer(mockClient); - final storage = container.read(puzzleBatchStorageProvider); - final service = container.read(puzzleServiceFactoryProvider)( + final storage = await container.read(puzzleBatchStorageProvider.future); + final service = await container.read(puzzleServiceFactoryProvider)( queueLength: 1, ); @@ -110,8 +110,8 @@ void main() { }); final container = await makeTestContainer(mockClient); - final storage = container.read(puzzleBatchStorageProvider); - final service = container.read(puzzleServiceFactoryProvider)( + final storage = await container.read(puzzleBatchStorageProvider.future); + final service = await container.read(puzzleServiceFactoryProvider)( queueLength: 2, ); await storage.save( @@ -137,8 +137,8 @@ void main() { }); final container = await makeTestContainer(mockClient); - final storage = container.read(puzzleBatchStorageProvider); - final service = container.read(puzzleServiceFactoryProvider)( + final storage = await container.read(puzzleBatchStorageProvider.future); + final service = await container.read(puzzleServiceFactoryProvider)( queueLength: 1, ); await storage.save( @@ -169,7 +169,7 @@ void main() { }); final container = await makeTestContainer(mockClient); - final service = container.read(puzzleServiceFactoryProvider)( + final service = await container.read(puzzleServiceFactoryProvider)( queueLength: 1, ); @@ -191,8 +191,8 @@ void main() { }); final container = await makeTestContainer(mockClient); - final storage = container.read(puzzleBatchStorageProvider); - final service = container.read(puzzleServiceFactoryProvider)( + final storage = await container.read(puzzleBatchStorageProvider.future); + final service = await container.read(puzzleServiceFactoryProvider)( queueLength: 1, ); await storage.save( @@ -224,8 +224,8 @@ void main() { }); final container = await makeTestContainer(mockClient); - final storage = container.read(puzzleBatchStorageProvider); - final service = container.read(puzzleServiceFactoryProvider)( + final storage = await container.read(puzzleBatchStorageProvider.future); + final service = await container.read(puzzleServiceFactoryProvider)( queueLength: 1, ); await storage.save( @@ -261,8 +261,8 @@ void main() { }); final container = await makeTestContainer(mockClient); - final storage = container.read(puzzleBatchStorageProvider); - final service = container.read(puzzleServiceFactoryProvider)( + final storage = await container.read(puzzleBatchStorageProvider.future); + final service = await container.read(puzzleServiceFactoryProvider)( queueLength: 1, ); await storage.save( @@ -307,8 +307,8 @@ void main() { }); final container = await makeTestContainer(mockClient); - final storage = container.read(puzzleBatchStorageProvider); - final service = container.read(puzzleServiceFactoryProvider)( + final storage = await container.read(puzzleBatchStorageProvider.future); + final service = await container.read(puzzleServiceFactoryProvider)( queueLength: 1, ); await storage.save( @@ -356,8 +356,8 @@ void main() { }); final container = await makeTestContainer(mockClient); - final storage = container.read(puzzleBatchStorageProvider); - final service = container.read(puzzleServiceFactoryProvider)( + final storage = await container.read(puzzleBatchStorageProvider.future); + final service = await container.read(puzzleServiceFactoryProvider)( queueLength: 2, ); await storage.save( @@ -396,8 +396,8 @@ void main() { }); final container = await makeTestContainer(mockClient); - final storage = container.read(puzzleBatchStorageProvider); - final service = container.read(puzzleServiceFactoryProvider)( + final storage = await container.read(puzzleBatchStorageProvider.future); + final service = await container.read(puzzleServiceFactoryProvider)( queueLength: 2, ); diff --git a/test/model/puzzle/puzzle_storage_test.dart b/test/model/puzzle/puzzle_storage_test.dart index 5f66c4c43d..878859ff40 100644 --- a/test/model/puzzle/puzzle_storage_test.dart +++ b/test/model/puzzle/puzzle_storage_test.dart @@ -27,7 +27,7 @@ void main() { ], ); - final storage = container.read(puzzleStorageProvider); + final storage = await container.read(puzzleStorageProvider.future); await storage.save( puzzle: puzzle, diff --git a/test/test_app.dart b/test/test_app.dart index d465d6ae34..3abbd8e43d 100644 --- a/test/test_app.dart +++ b/test/test_app.dart @@ -24,10 +24,8 @@ import 'package:lichess_mobile/src/model/notifications/push_notification_service import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:logging/logging.dart'; -import 'package:mocktail/mocktail.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:sqflite/sqflite.dart'; import 'package:visibility_detector/visibility_detector.dart'; import './fake_crashlytics.dart'; @@ -38,8 +36,6 @@ import 'model/common/fake_websocket_channel.dart'; import 'model/game/mock_game_storage.dart'; import 'utils/fake_connectivity_changes.dart'; -class MockDatabase extends Mock implements Database {} - final mockClient = MockClient((request) async { return http.Response('', 200); }); @@ -132,7 +128,9 @@ Future buildTestApp( // ignore: scoped_providers_should_specify_dependencies sessionStorageProvider.overrideWithValue(FakeSessionStorage(userSession)), // ignore: scoped_providers_should_specify_dependencies - gameStorageProvider.overrideWithValue(MockGameStorage()), + gameStorageProvider.overrideWith((_) async { + return MockGameStorage(); + }), // ignore: scoped_providers_should_specify_dependencies appInitializationProvider.overrideWith((ref) { return AppInitializationData( @@ -153,7 +151,6 @@ Future buildTestApp( }), sharedPreferences: sharedPreferences, userSession: userSession, - database: MockDatabase(), sri: 'test', engineMaxMemoryInMb: 16, ); diff --git a/test/test_container.dart b/test/test_container.dart index 782b0fd55a..e589871570 100644 --- a/test/test_container.dart +++ b/test/test_container.dart @@ -20,7 +20,6 @@ import 'package:logging/logging.dart'; import 'package:mocktail/mocktail.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:sqflite/sqflite.dart'; import './fake_crashlytics.dart'; import './model/auth/fake_session_storage.dart'; @@ -29,8 +28,6 @@ import 'fake_notification_service.dart'; import 'model/common/fake_websocket_channel.dart'; import 'utils/fake_connectivity_changes.dart'; -class MockDatabase extends Mock implements Database {} - class MockHttpClient extends Mock implements http.Client {} const shouldLog = false; @@ -110,7 +107,6 @@ Future makeContainer({ }), sharedPreferences: sharedPreferences, userSession: userSession, - database: MockDatabase(), sri: 'test', engineMaxMemoryInMb: 16, ); From 4357ea87e08ca3ccb152a83fcc1a8f82ef37df4e Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 17 Sep 2024 12:37:55 +0200 Subject: [PATCH 358/979] Check session after app is loaded --- lib/main.dart | 5 ++++ lib/src/app.dart | 30 ++++++++++++------- lib/src/app_initialization.dart | 22 -------------- .../model/common/service/sound_service.dart | 16 +++++----- 4 files changed, 33 insertions(+), 40 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 890de6f50c..57feb4f568 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -17,6 +17,7 @@ import 'package:lichess_mobile/src/intl.dart'; import 'package:lichess_mobile/src/log.dart'; import 'package:lichess_mobile/src/model/challenge/challenge_repository.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; import 'package:lichess_mobile/src/model/correspondence/correspondence_game_storage.dart'; import 'package:lichess_mobile/src/model/correspondence/offline_correspondence_game.dart'; import 'package:lichess_mobile/src/model/game/playable_game.dart'; @@ -87,6 +88,10 @@ Future main() async { }; } + // preload sounds + final soundTheme = GeneralPreferences.fetchFromStorage(prefs).soundTheme; + await preloadSounds(soundTheme); + // Get android 12+ core palette try { await DynamicColorPlugin.getCorePalette().then((value) { diff --git a/lib/src/app.dart b/lib/src/app.dart index 3bc3ba8822..3f3c395a55 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -12,9 +12,10 @@ import 'package:lichess_mobile/main.dart'; import 'package:lichess_mobile/src/app_initialization.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; +import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/challenge/challenge_service.dart'; +import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/correspondence/correspondence_service.dart'; import 'package:lichess_mobile/src/model/notifications/local_notification_service.dart'; @@ -110,7 +111,8 @@ class _AppState extends ConsumerState { setOptimalDisplayMode(); } - preloadSoundAssets(); + // check if session is still active + checkSession(); // Listen for connectivity changes and perform actions accordingly. ref.listenManual(connectivityChangesProvider, (prev, current) async { @@ -250,14 +252,22 @@ class _AppState extends ConsumerState { ); } - /// Preload sounds to avoid delays when playing them. - Future preloadSoundAssets() async { - final soundTheme = ref.read(generalPreferencesProvider).soundTheme; - final soundService = ref.read(soundServiceProvider); - try { - await soundService.initialize(soundTheme); - } catch (e) { - debugPrint('Cannot initialize SoundService: $e'); + /// Check if the session is still active and delete it if it is not. + Future checkSession() async { + // check if session is still active + final session = ref.read(authSessionProvider); + if (session != null) { + try { + final client = ref.read(lichessClientProvider); + final response = await client + .get(Uri(path: '/api/account')) + .timeout(const Duration(seconds: 3)); + if (response.statusCode == 401) { + await ref.read(authSessionProvider.notifier).delete(); + } + } catch (e) { + debugPrint('Could not check session: $e'); + } } } diff --git a/lib/src/app_initialization.dart b/lib/src/app_initialization.dart index 78cd02388b..46e56d12cb 100644 --- a/lib/src/app_initialization.dart +++ b/lib/src/app_initialization.dart @@ -6,9 +6,7 @@ import 'package:flutter/services.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/db/secure_storage.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; -import 'package:lichess_mobile/src/model/auth/bearer.dart'; import 'package:lichess_mobile/src/model/auth/session_storage.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/utils/color_palette.dart'; @@ -82,26 +80,6 @@ Future appInitialization( await secureStorage.read(key: kSRIStorageKey) ?? genRandomString(12); - final storedSession = await sessionStorage.read(); - if (storedSession != null) { - try { - final client = ref.read(defaultClientProvider); - final response = await client.get( - lichessUri('/api/account'), - headers: { - 'Authorization': 'Bearer ${signBearerToken(storedSession.token)}', - 'User-Agent': - makeUserAgent(pInfo, deviceInfo, sri, storedSession.user), - }, - ).timeout(const Duration(seconds: 3)); - if (response.statusCode == 401) { - await sessionStorage.delete(); - } - } catch (e) { - _logger.warning('Could not while checking session: $e'); - } - } - final physicalMemory = await System.instance.getTotalRam() ?? 256.0; final engineMaxMemory = (physicalMemory / 10).ceil(); diff --git a/lib/src/model/common/service/sound_service.dart b/lib/src/model/common/service/sound_service.dart index 875de4a4ea..fe74f2b722 100644 --- a/lib/src/model/common/service/sound_service.dart +++ b/lib/src/model/common/service/sound_service.dart @@ -32,6 +32,14 @@ final _extension = defaultTargetPlatform == TargetPlatform.iOS ? 'aifc' : 'mp3'; const Set _emtpySet = {}; +/// Initialize the sound service with the given sound theme. +/// +/// This will load the sounds from assets and make them ready to be played. +Future preloadSounds(SoundTheme theme) async { + await _soundEffectPlugin.initialize(); + await _loadAllSounds(theme); +} + /// Loads all sounds of the given [SoundTheme]. Future _loadAllSounds( SoundTheme soundTheme, { @@ -66,14 +74,6 @@ class SoundService { final SoundServiceRef _ref; - /// Initialize the sound service with the given sound theme. - /// - /// This will load the sounds from assets and make them ready to be played. - Future initialize(SoundTheme theme) async { - await _soundEffectPlugin.initialize(); - await _loadAllSounds(theme); - } - /// Play the given sound if sound is enabled. Future play(Sound sound) async { final isEnabled = _ref.read(generalPreferencesProvider).isSoundEnabled; From 9df2515e9e482eedb11728eec217c3d166b76303 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 17 Sep 2024 14:26:42 +0200 Subject: [PATCH 359/979] Decline not supported variants --- .../model/challenge/challenge_repository.dart | 7 +++++-- .../model/challenge/challenge_service.dart | 19 ++++++++++++++----- .../common/service/fake_sound_service.dart | 3 --- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/lib/src/model/challenge/challenge_repository.dart b/lib/src/model/challenge/challenge_repository.dart index 3eea52e106..8b95a32b01 100644 --- a/lib/src/model/challenge/challenge_repository.dart +++ b/lib/src/model/challenge/challenge_repository.dart @@ -68,9 +68,12 @@ class ChallengeRepository { } } - Future decline(ChallengeId id) async { + Future decline(ChallengeId id, {DeclineReason? reason}) async { final uri = Uri(path: '/api/challenge/$id/decline'); - final response = await client.post(uri); + final response = await client.post( + uri, + body: reason != null ? {'reason': reason.name} : null, + ); if (response.statusCode >= 400) { throw http.ClientException( diff --git a/lib/src/model/challenge/challenge_service.dart b/lib/src/model/challenge/challenge_service.dart index a1b9cf4fe6..28ef4f8072 100644 --- a/lib/src/model/challenge/challenge_service.dart +++ b/lib/src/model/challenge/challenge_service.dart @@ -6,6 +6,7 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/widgets.dart'; import 'package:lichess_mobile/src/model/challenge/challenge.dart'; import 'package:lichess_mobile/src/model/challenge/challenge_repository.dart'; +import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/notifications/challenge_notification.dart'; @@ -67,20 +68,28 @@ class ChallengeService { final l10n = ref.read(l10nProvider).strings; - // if a challenge was cancelled by the challenger + // challenges that were canceled by challenger or expired prevInwardIds .whereNot((challengeId) => currentInwardIds.contains(challengeId)) .forEach( (id) => LocalNotificationService.instance.cancel(id.value.hashCode), ); - // if there is a new challenge + // new incoming challenges inward .whereNot((challenge) => prevInwardIds.contains(challenge.id)) .forEach( - (challenge) => LocalNotificationService.instance - .show(ChallengeNotification(challenge, l10n)), - ); + (challenge) { + if (playSupportedVariants.contains(challenge.variant)) { + LocalNotificationService.instance + .show(ChallengeNotification(challenge, l10n)); + } else { + ref + .read(challengeRepositoryProvider) + .decline(challenge.id, reason: DeclineReason.variant); + } + }, + ); } /// Stop listening to challenge events from the server. diff --git a/test/model/common/service/fake_sound_service.dart b/test/model/common/service/fake_sound_service.dart index 3e765d133d..9dd58149c8 100644 --- a/test/model/common/service/fake_sound_service.dart +++ b/test/model/common/service/fake_sound_service.dart @@ -11,9 +11,6 @@ class FakeSoundService implements SoundService { bool playSound = false, }) async {} - @override - Future initialize(SoundTheme theme) async {} - @override Future release() async {} } From 4268866b1e79e03b97d1e7ae165394874979c4a7 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 17 Sep 2024 14:58:57 +0200 Subject: [PATCH 360/979] Improve challenge description --- lib/src/model/challenge/challenge.dart | 36 +++++++++++++++++++ .../notifications/challenge_notification.dart | 23 +----------- lib/src/view/play/challenge_list_item.dart | 36 ++----------------- lib/src/widgets/buttons.dart | 4 +-- 4 files changed, 42 insertions(+), 57 deletions(-) diff --git a/lib/src/model/challenge/challenge.dart b/lib/src/model/challenge/challenge.dart index 94c97543d2..3bae87725c 100644 --- a/lib/src/model/challenge/challenge.dart +++ b/lib/src/model/challenge/challenge.dart @@ -64,6 +64,42 @@ class Challenge with _$Challenge, BaseChallenge implements BaseChallenge { } factory Challenge.fromPick(RequiredPick pick) => _challengeFromPick(pick); + + /// The description of the challenge. + String description(AppLocalizations l10n) { + final time = switch (timeControl) { + ChallengeTimeControlType.clock => () { + final minutes = switch (clock!.time.inSeconds) { + 15 => '¼', + 30 => '½', + 45 => '¾', + 90 => '1.5', + _ => clock!.time.inMinutes, + }; + return '$minutes+${clock!.increment.inSeconds}'; + }(), + ChallengeTimeControlType.correspondence => '${l10n.daysPerTurn}: $days', + ChallengeTimeControlType.unlimited => '∞', + }; + + final variantStr = variant == Variant.standard ? '' : ' • ${variant.label}'; + + final sidePiece = sideChoice == SideChoice.black + ? '♔ ' + : sideChoice == SideChoice.white + ? '♚ ' + : ''; + + final side = sideChoice == SideChoice.black + ? l10n.white + : sideChoice == SideChoice.white + ? l10n.black + : l10n.randomColor; + + final mode = rated ? l10n.rated : l10n.casual; + + return '$sidePiece$side • $mode • $time$variantStr'; + } } /// A challenge request to play a game with another user. diff --git a/lib/src/model/notifications/challenge_notification.dart b/lib/src/model/notifications/challenge_notification.dart index e37774fed5..9f666539d3 100644 --- a/lib/src/model/notifications/challenge_notification.dart +++ b/lib/src/model/notifications/challenge_notification.dart @@ -21,28 +21,7 @@ class ChallengeNotification implements LocalNotification { String get title => '${_challenge.challenger!.user.name} challenges you!'; @override - String get body { - final time = switch (_challenge.timeControl) { - ChallengeTimeControlType.clock => () { - final clock = _challenge.clock!; - final minutes = switch (clock.time.inSeconds) { - 15 => '¼', - 30 => '½', - 45 => '¾', - 90 => '1.5', - _ => clock.time.inMinutes, - }; - return '$minutes+${clock.increment.inSeconds}'; - }(), - ChallengeTimeControlType.correspondence => - '${_l10n.daysPerTurn}: ${_challenge.days}', - ChallengeTimeControlType.unlimited => '∞', - }; - - return _challenge.rated - ? '${_l10n.rated} • $time' - : '${_l10n.casual} • $time'; - } + String get body => _challenge.description(_l10n); @override NotificationDetails get details => NotificationDetails( diff --git a/lib/src/view/play/challenge_list_item.dart b/lib/src/view/play/challenge_list_item.dart index fdfe4ca5d3..01c96753b1 100644 --- a/lib/src/view/play/challenge_list_item.dart +++ b/lib/src/view/play/challenge_list_item.dart @@ -9,7 +9,6 @@ import 'package:lichess_mobile/src/model/common/speed.dart'; import 'package:lichess_mobile/src/model/lobby/correspondence_challenge.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/styles/lichess_colors.dart'; -import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; @@ -34,29 +33,8 @@ class ChallengeListItem extends ConsumerWidget { final me = ref.watch(authSessionProvider)?.user; final isMyChallenge = me != null && me.id == user.id; - final time = switch (challenge.timeControl) { - ChallengeTimeControlType.clock => () { - final clock = challenge.clock!; - final minutes = switch (clock.time.inSeconds) { - 15 => '¼', - 30 => '½', - 45 => '¾', - 90 => '1.5', - _ => clock.time.inMinutes, - }; - return '$minutes+${clock.increment.inSeconds}'; - }(), - ChallengeTimeControlType.correspondence => - '${context.l10n.daysPerTurn}: ${challenge.days}', - ChallengeTimeControlType.unlimited => '∞', - }; - - final subtitle = challenge.rated - ? '${context.l10n.rated} • $time' - : '${context.l10n.casual} • $time'; - final color = - isMyChallenge ? null : LichessColors.green.withValues(alpha: 0.2); + isMyChallenge ? LichessColors.green.withValues(alpha: 0.2) : null; return Container( color: color, @@ -70,7 +48,6 @@ class ChallengeListItem extends ConsumerWidget { onPressed: (BuildContext context) => onCancel!(), backgroundColor: context.lichessColors.error, foregroundColor: Colors.white, - icon: Icons.cancel, label: isMyChallenge ? context.l10n.cancel : context.l10n.decline, @@ -80,14 +57,7 @@ class ChallengeListItem extends ConsumerWidget { : null, child: PlatformListTile( padding: Styles.bodyPadding, - leading: Icon(challenge.perf.icon), - trailing: Icon( - challenge.sideChoice == SideChoice.random - ? LichessIcons.adjust - : challenge.sideChoice == SideChoice.white - ? LichessIcons.circle - : LichessIcons.circle_empty, - ), + trailing: Icon(challenge.perf.icon, size: 36), title: isMyChallenge ? UserFullNameWidget( user: challenge.destUser != null @@ -95,7 +65,7 @@ class ChallengeListItem extends ConsumerWidget { : user, ) : UserFullNameWidget(user: user), - subtitle: Text(subtitle), + subtitle: Text(challenge.description(context.l10n)), onTap: onPressed, ), ), diff --git a/lib/src/widgets/buttons.dart b/lib/src/widgets/buttons.dart index 414110e398..912db892f4 100644 --- a/lib/src/widgets/buttons.dart +++ b/lib/src/widgets/buttons.dart @@ -214,9 +214,9 @@ class AppBarNotificationIconButton extends StatelessWidget { Widget build(BuildContext context) { return AppBarIconButton( icon: Badge.count( - backgroundColor: Theme.of(context).colorScheme.primary, + backgroundColor: Theme.of(context).colorScheme.tertiary, textStyle: TextStyle( - color: Theme.of(context).colorScheme.onPrimary, + color: Theme.of(context).colorScheme.onTertiary, fontWeight: FontWeight.bold, ), count: count, From c855129ca5577de1c44ab6577d36e3cda7187cb8 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 17 Sep 2024 15:17:09 +0200 Subject: [PATCH 361/979] More work on challenge presentation --- lib/src/model/challenge/challenge.dart | 3 + lib/src/view/game/game_common_widgets.dart | 2 +- lib/src/view/game/ping_rating.dart | 53 +++-------------- lib/src/view/play/challenge_list_item.dart | 17 ++++-- lib/src/widgets/feedback.dart | 67 ++++++++++++++++++++++ 5 files changed, 90 insertions(+), 52 deletions(-) diff --git a/lib/src/model/challenge/challenge.dart b/lib/src/model/challenge/challenge.dart index 3bae87725c..aca960c359 100644 --- a/lib/src/model/challenge/challenge.dart +++ b/lib/src/model/challenge/challenge.dart @@ -206,6 +206,7 @@ String declineReasonMessage(BuildContext context, DeclineReason key) { typedef ChallengeUser = ({ LightUser user, + int? rating, bool? provisionalRating, int? lagRating, }); @@ -439,6 +440,7 @@ Challenge _challengeFromPick(RequiredPick pick) { final challengerUser = pick('challenger').asLightUserOrThrow(); return ( user: challengerUser, + rating: challengerPick('rating').asIntOrNull(), provisionalRating: challengerPick('provisional').asBoolOrNull(), lagRating: challengerPick('lag').asIntOrNull(), ); @@ -449,6 +451,7 @@ Challenge _challengeFromPick(RequiredPick pick) { final destUser = pick('destUser').asLightUserOrThrow(); return ( user: destUser, + rating: destPick('rating').asIntOrNull(), provisionalRating: destPick('provisional').asBoolOrNull(), lagRating: destPick('lag').asIntOrNull(), ); diff --git a/lib/src/view/game/game_common_widgets.dart b/lib/src/view/game/game_common_widgets.dart index d68a17fb7f..a173509637 100644 --- a/lib/src/view/game/game_common_widgets.dart +++ b/lib/src/view/game/game_common_widgets.dart @@ -42,7 +42,7 @@ class GameAppBar extends ConsumerWidget { static const pingRating = Padding( padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 18.0), - child: PingRating(size: 24.0), + child: SocketPingRating(size: 24.0), ); @override diff --git a/lib/src/view/game/ping_rating.dart b/lib/src/view/game/ping_rating.dart index 65125d8d82..6248f3e43a 100644 --- a/lib/src/view/game/ping_rating.dart +++ b/lib/src/view/game/ping_rating.dart @@ -1,18 +1,12 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; +import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:signal_strength_indicator/signal_strength_indicator.dart'; part 'ping_rating.g.dart'; -const spinKit = SpinKitThreeBounce( - color: Colors.grey, - size: 15, -); - @riverpod int pingRating(PingRatingRef ref) { final ping = ref.watch(averageLagProvider).inMicroseconds / 1000; @@ -28,55 +22,22 @@ int pingRating(PingRatingRef ref) { : 1; } -class PingRating extends ConsumerWidget { - const PingRating({ +class SocketPingRating extends ConsumerWidget { + const SocketPingRating({ required this.size, super.key, }); final double size; - static const cupertinoLevels = { - 0: CupertinoColors.systemRed, - 1: CupertinoColors.systemYellow, - 2: CupertinoColors.systemGreen, - 3: CupertinoColors.systemGreen, - }; - - static const materialLevels = { - 0: Colors.red, - 1: Colors.yellow, - 2: Colors.green, - 3: Colors.green, - }; - @override Widget build(BuildContext context, WidgetRef ref) { final pingRating = ref.watch(pingRatingProvider); - return SizedBox.square( - dimension: size, - child: Stack( - children: [ - SignalStrengthIndicator.bars( - barCount: 4, - minValue: 1, - maxValue: 4, - value: pingRating, - size: size, - inactiveColor: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoDynamicColor.resolve( - CupertinoColors.systemGrey, - context, - ).withValues(alpha: 0.2) - : Colors.grey.withValues(alpha: 0.2), - levels: Theme.of(context).platform == TargetPlatform.iOS - ? cupertinoLevels - : materialLevels, - ), - if (pingRating == 0) spinKit, - ], - ), + return LagIndicator( + lagRating: pingRating, + size: size, + showLoadingIndicator: true, ); } } diff --git a/lib/src/view/play/challenge_list_item.dart b/lib/src/view/play/challenge_list_item.dart index 01c96753b1..7be1d71780 100644 --- a/lib/src/view/play/challenge_list_item.dart +++ b/lib/src/view/play/challenge_list_item.dart @@ -8,9 +8,9 @@ import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/speed.dart'; import 'package:lichess_mobile/src/model/lobby/correspondence_challenge.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; -import 'package:lichess_mobile/src/styles/lichess_colors.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/user_full_name.dart'; @@ -33,8 +33,9 @@ class ChallengeListItem extends ConsumerWidget { final me = ref.watch(authSessionProvider)?.user; final isMyChallenge = me != null && me.id == user.id; - final color = - isMyChallenge ? LichessColors.green.withValues(alpha: 0.2) : null; + final color = isMyChallenge + ? context.lichessColors.good.withValues(alpha: 0.2) + : null; return Container( color: color, @@ -57,14 +58,20 @@ class ChallengeListItem extends ConsumerWidget { : null, child: PlatformListTile( padding: Styles.bodyPadding, - trailing: Icon(challenge.perf.icon, size: 36), + leading: Icon(challenge.perf.icon, size: 36), + trailing: challenge.challenger?.lagRating != null + ? LagIndicator(lagRating: challenge.challenger!.lagRating!) + : null, title: isMyChallenge ? UserFullNameWidget( user: challenge.destUser != null ? challenge.destUser!.user : user, ) - : UserFullNameWidget(user: user), + : UserFullNameWidget( + user: user, + rating: challenge.challenger?.rating, + ), subtitle: Text(challenge.description(context.l10n)), onTap: onPressed, ), diff --git a/lib/src/widgets/feedback.dart b/lib/src/widgets/feedback.dart index 087039534c..2bc129e248 100644 --- a/lib/src/widgets/feedback.dart +++ b/lib/src/widgets/feedback.dart @@ -1,10 +1,77 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:signal_strength_indicator/signal_strength_indicator.dart'; + +class LagIndicator extends StatelessWidget { + const LagIndicator({ + required this.lagRating, + this.size = 20.0, + this.showLoadingIndicator = false, + super.key, + }) : assert(lagRating >= 0 && lagRating <= 4); + + /// The lag rating from 0 to 4. + final int lagRating; + + /// Visual size of the indicator. + final double size; + + /// Whether to show a loading indicator when the lag rating is 0. + final bool showLoadingIndicator; + + static const spinKit = SpinKitThreeBounce( + color: Colors.grey, + size: 15, + ); + + static const cupertinoLevels = { + 0: CupertinoColors.systemRed, + 1: CupertinoColors.systemYellow, + 2: CupertinoColors.systemGreen, + 3: CupertinoColors.systemGreen, + }; + + static const materialLevels = { + 0: Colors.red, + 1: Colors.yellow, + 2: Colors.green, + 3: Colors.green, + }; + + @override + Widget build(BuildContext context) { + return SizedBox.square( + dimension: size, + child: Stack( + children: [ + SignalStrengthIndicator.bars( + barCount: 4, + minValue: 1, + maxValue: 4, + value: lagRating, + size: size, + inactiveColor: Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoDynamicColor.resolve( + CupertinoColors.systemGrey, + context, + ).withValues(alpha: 0.2) + : Colors.grey.withValues(alpha: 0.2), + levels: Theme.of(context).platform == TargetPlatform.iOS + ? cupertinoLevels + : materialLevels, + ), + if (showLoadingIndicator && lagRating == 0) spinKit, + ], + ), + ); + } +} class ConnectivityBanner extends ConsumerWidget { const ConnectivityBanner(); From ae78048f6c83d253f0971346f9a5776ff30495f7 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 17 Sep 2024 15:45:20 +0200 Subject: [PATCH 362/979] Temporarily disable failing explorer tests --- .../opening_explorer_screen_test.dart | 67 ++++++++++--------- 1 file changed, 36 insertions(+), 31 deletions(-) diff --git a/test/view/opening_explorer/opening_explorer_screen_test.dart b/test/view/opening_explorer/opening_explorer_screen_test.dart index df7d23218a..e8b03b6925 100644 --- a/test/view/opening_explorer/opening_explorer_screen_test.dart +++ b/test/view/opening_explorer/opening_explorer_screen_test.dart @@ -16,10 +16,10 @@ import '../../test_app.dart'; import '../../test_utils.dart'; void main() { - final explorerViewFinder = find.descendant( - of: find.byType(LayoutBuilder), - matching: find.byType(Scrollable), - ); + // final explorerViewFinder = find.descendant( + // of: find.byType(LayoutBuilder), + // matching: find.byType(Scrollable), + // ); final mockClient = MockClient((request) { if (request.url.host == 'explorer.lichess.ovh') { @@ -87,16 +87,19 @@ void main() { expect(find.widgetWithText(Container, 'Top games'), findsOneWidget); expect(find.widgetWithText(Container, 'Recent games'), findsNothing); - await tester.scrollUntilVisible( - find.text('Firouzja, A.'), - 200, - scrollable: explorerViewFinder, - ); + // TODO: make a custom scrollUntilVisible that works with the non-scrollable + // board widget - expect( - find.byType(OpeningExplorerGameTile), - findsNWidgets(2), - ); + // await tester.scrollUntilVisible( + // find.text('Firouzja, A.'), + // 200, + // scrollable: explorerViewFinder, + // ); + + // expect( + // find.byType(OpeningExplorerGameTile), + // findsNWidgets(2), + // ); await tester.pump(const Duration(milliseconds: 50)); }, @@ -138,16 +141,17 @@ void main() { expect(find.widgetWithText(Container, 'Top games'), findsNothing); expect(find.widgetWithText(Container, 'Recent games'), findsOneWidget); - await tester.scrollUntilVisible( - find.byType(OpeningExplorerGameTile), - 200, - scrollable: explorerViewFinder, - ); - expect( - find.byType(OpeningExplorerGameTile), - findsOneWidget, - ); + // await tester.scrollUntilVisible( + // find.byType(OpeningExplorerGameTile), + // 200, + // scrollable: explorerViewFinder, + // ); + + // expect( + // find.byType(OpeningExplorerGameTile), + // findsOneWidget, + // ); await tester.pump(const Duration(milliseconds: 50)); }, @@ -189,16 +193,17 @@ void main() { expect(find.widgetWithText(Container, 'Top games'), findsNothing); expect(find.widgetWithText(Container, 'Recent games'), findsOneWidget); - await tester.scrollUntilVisible( - find.byType(OpeningExplorerGameTile), - 200, - scrollable: explorerViewFinder, - ); - expect( - find.byType(OpeningExplorerGameTile), - findsOneWidget, - ); + // await tester.scrollUntilVisible( + // find.byType(OpeningExplorerGameTile), + // 200, + // scrollable: explorerViewFinder, + // ); + + // expect( + // find.byType(OpeningExplorerGameTile), + // findsOneWidget, + // ); await tester.pump(const Duration(milliseconds: 50)); }, From d30d5a80e1842c56f3418b9dcddc1784de2f6455 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Tue, 17 Sep 2024 20:07:08 +0200 Subject: [PATCH 363/979] revert format change to avoid conflict --- .../broadcast/broadcasts_list_screen.dart | 183 +++++++++--------- 1 file changed, 92 insertions(+), 91 deletions(-) diff --git a/lib/src/view/broadcast/broadcasts_list_screen.dart b/lib/src/view/broadcast/broadcasts_list_screen.dart index c055d5f517..59dde260e4 100644 --- a/lib/src/view/broadcast/broadcasts_list_screen.dart +++ b/lib/src/view/broadcast/broadcasts_list_screen.dart @@ -198,103 +198,104 @@ class BroadcastGridItem extends StatelessWidget { ); @override - Widget build(BuildContext context) => AdaptiveInkWell( - borderRadius: BorderRadius.circular(20), - onTap: () { - pushPlatformRoute( - context, - title: context.l10n.broadcastBroadcasts, - rootNavigator: true, - builder: (context) => BroadcastScreen(broadcast: broadcast), - ); - }, - child: Container( - clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - boxShadow: [ - BoxShadow( - color: LichessColors.grey.withOpacity(0.5), - blurRadius: 5, - spreadRadius: 1, - ), - ], - ), - foregroundDecoration: BoxDecoration( - border: (broadcast.isLive) - ? Border.all(color: LichessColors.red, width: 2) - : Border.all(color: LichessColors.grey), - borderRadius: BorderRadius.circular(20), - ), - child: Column( - children: [ - if (broadcast.tour.imageUrl != null) - AspectRatio( - aspectRatio: 2.0, - child: FadeInImage.memoryNetwork( - placeholder: transparentImage, - image: broadcast.tour.imageUrl!, - ), - ) - else - const DefaultBroadcastImage(aspectRatio: 2.0), - Expanded( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - if (!broadcast.isFinished) ...[ - Text( - broadcast.round.name, - style: Theme.of(context) - .textTheme - .labelMedium - ?.copyWith( - color: textShade(context, 0.5), - ), - overflow: TextOverflow.ellipsis, + Widget build(BuildContext context) { + return AdaptiveInkWell( + borderRadius: BorderRadius.circular(20), + onTap: () { + pushPlatformRoute( + context, + title: context.l10n.broadcastBroadcasts, + rootNavigator: true, + builder: (context) => BroadcastScreen(broadcast: broadcast), + ); + }, + child: Container( + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: LichessColors.grey.withOpacity(0.5), + blurRadius: 5, + spreadRadius: 1, + ), + ], + ), + foregroundDecoration: BoxDecoration( + border: (broadcast.isLive) + ? Border.all(color: LichessColors.red, width: 2) + : Border.all(color: LichessColors.grey), + borderRadius: BorderRadius.circular(20), + ), + child: Column( + children: [ + if (broadcast.tour.imageUrl != null) + AspectRatio( + aspectRatio: 2.0, + child: FadeInImage.memoryNetwork( + placeholder: transparentImage, + image: broadcast.tour.imageUrl!, + ), + ) + else + const DefaultBroadcastImage(aspectRatio: 2.0), + Expanded( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + if (!broadcast.isFinished) ...[ + Text( + broadcast.round.name, + style: Theme.of(context) + .textTheme + .labelMedium + ?.copyWith( + color: textShade(context, 0.5), + ), + overflow: TextOverflow.ellipsis, + ), + const SizedBox(width: 4.0), + ], + if (broadcast.isLive) + const Text( + 'LIVE', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: Colors.red, ), - const SizedBox(width: 4.0), - ], - if (broadcast.isLive) - const Text( - 'LIVE', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: Colors.red, - ), - overflow: TextOverflow.ellipsis, - ) - else if (broadcast.round.startsAt != null) - StartsRoundDate( - startsAt: broadcast.round.startsAt!, + overflow: TextOverflow.ellipsis, + ) + else if (broadcast.round.startsAt != null) + StartsRoundDate( + startsAt: broadcast.round.startsAt!, + ), + ], + ), + const SizedBox(height: 4.0), + Flexible( + child: Text( + broadcast.title, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.labelLarge?.copyWith( + fontWeight: FontWeight.bold, ), - ], - ), - const SizedBox(height: 4.0), - Flexible( - child: Text( - broadcast.title, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: - Theme.of(context).textTheme.labelLarge?.copyWith( - fontWeight: FontWeight.bold, - ), - ), ), - ], - ), + ), + ], ), ), - ], - ), + ), + ], ), - ); + ), + ); + } } class StartsRoundDate extends ConsumerWidget { From 954e2c9041e68e761c2592e8588ea28ff6d87776 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Tue, 17 Sep 2024 20:47:35 +0200 Subject: [PATCH 364/979] revert the sync of date formatting with selected user locale This reverts commit 82063f662f89566728c2f110656d8439293ec7ce. --- lib/src/utils/current_locale.dart | 20 ---------- .../broadcast/broadcast_overview_tab.dart | 8 ++-- .../broadcast/broadcasts_list_screen.dart | 13 +++--- lib/src/view/game/game_list_tile.dart | 7 ++-- .../view/puzzle/puzzle_history_screen.dart | 6 +-- lib/src/view/user/perf_stats_screen.dart | 40 +++++++++---------- lib/src/view/user/user_activity.dart | 7 ++-- 7 files changed, 38 insertions(+), 63 deletions(-) delete mode 100644 lib/src/utils/current_locale.dart diff --git a/lib/src/utils/current_locale.dart b/lib/src/utils/current_locale.dart deleted file mode 100644 index e2205a29e0..0000000000 --- a/lib/src/utils/current_locale.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:intl/intl.dart'; -import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'current_locale.g.dart'; - -@riverpod -String currentLocale(CurrentLocaleRef ref) { - return ref.watch(generalPreferencesProvider).locale?.languageCode ?? - Intl.getCurrentLocale(); -} - -extension LocaleWidgetRefExtension on WidgetRef { - /// Runs [fn] with the current locale. - T withLocale(T Function(String) fn) { - final currentLocale = watch(currentLocaleProvider); - return fn(currentLocale); - } -} diff --git a/lib/src/view/broadcast/broadcast_overview_tab.dart b/lib/src/view/broadcast/broadcast_overview_tab.dart index 86cda01986..d912f4b0c8 100644 --- a/lib/src/view/broadcast/broadcast_overview_tab.dart +++ b/lib/src/view/broadcast/broadcast_overview_tab.dart @@ -6,9 +6,10 @@ import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_providers.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/current_locale.dart'; import 'package:url_launcher/url_launcher.dart'; +final _dateFormatter = DateFormat.MMMd(Intl.getCurrentLocale()); + /// A tab that displays the overview of a broadcast. class BroadcastOverviewTab extends ConsumerWidget { final BroadcastTournamentId tournamentId; @@ -18,7 +19,6 @@ class BroadcastOverviewTab extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final tournament = ref.watch(broadcastTournamentProvider(tournamentId)); - final dateFormatter = ref.withLocale((locale) => DateFormat.MMMd(locale)); return SafeArea( bottom: false, @@ -38,8 +38,8 @@ class BroadcastOverviewTab extends ConsumerWidget { BroadcastOverviewCard( CupertinoIcons.calendar, information.dates!.endsAt == null - ? dateFormatter.format(information.dates!.startsAt) - : '${dateFormatter.format(information.dates!.startsAt)} - ${dateFormatter.format(information.dates!.endsAt!)}', + ? _dateFormatter.format(information.dates!.startsAt) + : '${_dateFormatter.format(information.dates!.startsAt)} - ${_dateFormatter.format(information.dates!.endsAt!)}', ), if (information.format != null) BroadcastOverviewCard( diff --git a/lib/src/view/broadcast/broadcasts_list_screen.dart b/lib/src/view/broadcast/broadcasts_list_screen.dart index 67a54b2ec1..895c30ef1f 100644 --- a/lib/src/view/broadcast/broadcasts_list_screen.dart +++ b/lib/src/view/broadcast/broadcasts_list_screen.dart @@ -7,7 +7,6 @@ import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/styles/lichess_colors.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/styles/transparent_image.dart'; -import 'package:lichess_mobile/src/utils/current_locale.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_screen.dart'; @@ -16,6 +15,10 @@ import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; +final _dateFormatter = DateFormat.MMMd(Intl.getCurrentLocale()).add_Hm(); +final _dateFormatterWithYear = + DateFormat.yMMMd(Intl.getCurrentLocale()).add_Hm(); + /// A screen that displays a paginated list of broadcasts. class BroadcastsListScreen extends StatelessWidget { const BroadcastsListScreen({super.key}); @@ -305,10 +308,6 @@ class StartsRoundDate extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final dateFormatter = - ref.withLocale((locale) => DateFormat.MMMd(locale).add_Hm()); - final dateFormatterWithYear = - ref.withLocale((locale) => DateFormat.yMMMd(locale).add_Hm()); final timeBeforeRound = startsAt.difference(DateTime.now()); return Text( @@ -317,8 +316,8 @@ class StartsRoundDate extends ConsumerWidget { ? 'In ${timeBeforeRound.inMinutes} minutes' // TODO translate with https://github.com/lichess-org/lila/blob/65b28ea8e43e0133df6c7ed40e03c2954f247d1e/translation/source/timeago.xml#L8 : 'In ${timeBeforeRound.inHours} hours' // TODO translate with https://github.com/lichess-org/lila/blob/65b28ea8e43e0133df6c7ed40e03c2954f247d1e/translation/source/timeago.xml#L12 : timeBeforeRound.inDays < 365 - ? dateFormatter.format(startsAt) - : dateFormatterWithYear.format(startsAt), + ? _dateFormatter.format(startsAt) + : _dateFormatterWithYear.format(startsAt), style: Theme.of(context).textTheme.labelSmall?.copyWith( color: textShade(context, 0.5), ), diff --git a/lib/src/view/game/game_list_tile.dart b/lib/src/view/game/game_list_tile.dart index fb8838d898..1c1213c1b0 100644 --- a/lib/src/view/game/game_list_tile.dart +++ b/lib/src/view/game/game_list_tile.dart @@ -11,7 +11,6 @@ import 'package:lichess_mobile/src/model/game/game_share_service.dart'; import 'package:lichess_mobile/src/model/game/game_status.dart'; import 'package:lichess_mobile/src/styles/lichess_colors.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/current_locale.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/share.dart'; @@ -26,6 +25,8 @@ import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/user_full_name.dart'; import 'package:timeago/timeago.dart' as timeago; +final _dateFormatter = DateFormat.yMMMd().add_Hm(); + /// A list tile that shows game info. class GameListTile extends StatelessWidget { const GameListTile({ @@ -109,8 +110,6 @@ class _ContextMenu extends ConsumerWidget { final orientation = mySide; final customColors = Theme.of(context).extension(); - final dateFormatter = - ref.withLocale((locale) => DateFormat.yMMMd(locale).add_Hm()); return BottomSheetScrollableContainer( children: [ @@ -166,7 +165,7 @@ class _ContextMenu extends ConsumerWidget { ), ), Text( - dateFormatter.format(game.lastMoveAt), + _dateFormatter.format(game.lastMoveAt), style: TextStyle( color: textShade( context, diff --git a/lib/src/view/puzzle/puzzle_history_screen.dart b/lib/src/view/puzzle/puzzle_history_screen.dart index 50c3926649..9a4d7504ae 100644 --- a/lib/src/view/puzzle/puzzle_history_screen.dart +++ b/lib/src/view/puzzle/puzzle_history_screen.dart @@ -9,7 +9,6 @@ import 'package:lichess_mobile/src/model/puzzle/puzzle_activity.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_angle.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_theme.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/current_locale.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; @@ -19,6 +18,8 @@ import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:timeago/timeago.dart' as timeago; +final _dateFormatter = DateFormat.yMMMd(Intl.getCurrentLocale()); + class PuzzleHistoryScreen extends StatelessWidget { @override Widget build(BuildContext context) { @@ -113,7 +114,6 @@ class _BodyState extends ConsumerState<_Body> { @override Widget build(BuildContext context) { final historyState = ref.watch(puzzleActivityProvider); - final dateFormatter = ref.withLocale((locale) => DateFormat.yMMMd(locale)); return historyState.when( data: (state) { @@ -168,7 +168,7 @@ class _BodyState extends ConsumerState<_Body> { ); } else if (element is DateTime) { final title = DateTime.now().difference(element).inDays >= 15 - ? dateFormatter.format(element) + ? _dateFormatter.format(element) : timeago.format(element); return Padding( padding: const EdgeInsets.only(left: _kPuzzlePadding) diff --git a/lib/src/view/user/perf_stats_screen.dart b/lib/src/view/user/perf_stats_screen.dart index 7007ed5dca..3b8cb7301c 100644 --- a/lib/src/view/user/perf_stats_screen.dart +++ b/lib/src/view/user/perf_stats_screen.dart @@ -19,7 +19,6 @@ import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/model/user/user_repository_providers.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/current_locale.dart'; import 'package:lichess_mobile/src/utils/duration.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; @@ -36,6 +35,9 @@ import 'package:lichess_mobile/src/widgets/rating.dart'; import 'package:lichess_mobile/src/widgets/stat_card.dart'; import 'package:lichess_mobile/src/widgets/user_full_name.dart'; +final _currentLocale = Intl.getCurrentLocale(); +final _dateFormatter = DateFormat.yMMMd(_currentLocale); + const _customOpacity = 0.6; const _defaultStatFontSize = 12.0; const _defaultValueFontSize = 18.0; @@ -147,7 +149,6 @@ class _Body extends ConsumerWidget { final loggedInUser = ref.watch(authSessionProvider); const statGroupSpace = SizedBox(height: 15.0); const subStatSpace = SizedBox(height: 10); - final currentLocale = ref.watch(currentLocaleProvider); return perfStats.when( data: (data) { @@ -222,8 +223,9 @@ class _Body extends ConsumerWidget { context.l10n.rank, value: data.rank == null ? '?' - : NumberFormat.decimalPattern(currentLocale) - .format(data.rank), + : NumberFormat.decimalPattern( + Intl.getCurrentLocale(), + ).format(data.rank), ), StatCard( context.l10n @@ -468,13 +470,13 @@ class _ProgressionWidget extends StatelessWidget { } } -class _UserGameWidget extends ConsumerWidget { +class _UserGameWidget extends StatelessWidget { final UserPerfGame? game; const _UserGameWidget(this.game); @override - Widget build(BuildContext context, WidgetRef ref) { + Widget build(BuildContext context) { // TODO: Implement functionality to view game on tap. // (Return a button? Wrap with InkWell?) const defaultDateFontSize = 16.0; @@ -482,12 +484,11 @@ class _UserGameWidget extends ConsumerWidget { color: Theme.of(context).colorScheme.tertiary, fontSize: defaultDateFontSize, ); - final dateFormatter = ref.withLocale((locale) => DateFormat.yMMMd(locale)); return game == null ? Text('?', style: defaultDateStyle) : Text( - dateFormatter.format(game!.finishedAt), + _dateFormatter.format(game!.finishedAt), style: defaultDateStyle, ); } @@ -670,8 +671,6 @@ class _GameListWidget extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final dateFormatter = ref.withLocale((locale) => DateFormat.yMMMd(locale)); - return ListSection( header: header, margin: const EdgeInsets.only(top: 10.0), @@ -711,7 +710,7 @@ class _GameListWidget extends ConsumerWidget { rating: game.opponentRating, ), subtitle: Text( - dateFormatter.format(game.finishedAt), + _dateFormatter.format(game.finishedAt), ), ), ], @@ -747,16 +746,16 @@ class _GameListTile extends StatelessWidget { } } -class _EloChart extends ConsumerStatefulWidget { +class _EloChart extends StatefulWidget { final UserRatingHistoryPerf value; const _EloChart(this.value); @override - ConsumerState<_EloChart> createState() => _EloChartState(); + State<_EloChart> createState() => _EloChartState(); } -class _EloChartState extends ConsumerState<_EloChart> { +class _EloChartState extends State<_EloChart> { late DateRange _selectedRange; late List _allFlSpot; @@ -846,13 +845,12 @@ class _EloChartState extends ConsumerState<_EloChart> { final borderColor = Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5); final chartColor = Theme.of(context).colorScheme.tertiary; - final currentLocale = ref.watch(currentLocaleProvider); final chartDateFormatter = switch (_selectedRange) { - DateRange.oneWeek => DateFormat.MMMd(currentLocale), - DateRange.oneMonth => DateFormat.MMMd(currentLocale), - DateRange.threeMonths => DateFormat.yMMM(currentLocale), - DateRange.oneYear => DateFormat.yMMM(currentLocale), - DateRange.allTime => DateFormat.yMMM(currentLocale), + DateRange.oneWeek => DateFormat.MMMd(_currentLocale), + DateRange.oneMonth => DateFormat.MMMd(_currentLocale), + DateRange.threeMonths => DateFormat.yMMM(_currentLocale), + DateRange.oneYear => DateFormat.yMMM(_currentLocale), + DateRange.allTime => DateFormat.yMMM(_currentLocale), }; String formatDateFromTimestamp(double nbDays) => chartDateFormatter.format( @@ -860,7 +858,7 @@ class _EloChartState extends ConsumerState<_EloChart> { ); String formatDateFromTimestampForTooltip(double nbDays) => - DateFormat.yMMMd(currentLocale).format( + DateFormat.yMMMd(_currentLocale).format( _firstDate.add(Duration(days: nbDays.toInt())), ); diff --git a/lib/src/view/user/user_activity.dart b/lib/src/view/user/user_activity.dart index 9500f7b542..1e87d8cbe9 100644 --- a/lib/src/view/user/user_activity.dart +++ b/lib/src/view/user/user_activity.dart @@ -10,7 +10,6 @@ import 'package:lichess_mobile/src/model/user/user_repository.dart'; import 'package:lichess_mobile/src/styles/lichess_colors.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/current_locale.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/view/account/rating_pref_aware.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; @@ -20,6 +19,8 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'user_activity.g.dart'; +final _dateFormatter = DateFormat.yMMMd(Intl.getCurrentLocale()); + @riverpod Future> _userActivity( _UserActivityRef ref, { @@ -97,8 +98,6 @@ class UserActivityEntry extends ConsumerWidget { final redColor = theme.extension()?.error; final greenColor = theme.extension()?.good; - final dateFormatter = ref.withLocale((locale) => DateFormat.yMMMd(locale)); - return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ @@ -110,7 +109,7 @@ class UserActivityEntry extends ConsumerWidget { bottom: 4.0, ), child: Text( - dateFormatter.format(entry.startTime), + _dateFormatter.format(entry.startTime), style: TextStyle( color: context.lichessColors.brag, fontWeight: FontWeight.bold, From 5fde6b402103d31353e924e9ec95a901b47e1152 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Tue, 17 Sep 2024 20:58:39 +0200 Subject: [PATCH 365/979] remove Intl.getCurentLocale() to complete the revert --- .../view/broadcast/broadcast_overview_tab.dart | 2 +- .../view/broadcast/broadcasts_list_screen.dart | 5 ++--- lib/src/view/puzzle/puzzle_history_screen.dart | 2 +- lib/src/view/user/perf_stats_screen.dart | 15 +++++++-------- lib/src/view/user/user_activity.dart | 2 +- 5 files changed, 12 insertions(+), 14 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_overview_tab.dart b/lib/src/view/broadcast/broadcast_overview_tab.dart index d912f4b0c8..f4f395c9e2 100644 --- a/lib/src/view/broadcast/broadcast_overview_tab.dart +++ b/lib/src/view/broadcast/broadcast_overview_tab.dart @@ -8,7 +8,7 @@ import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:url_launcher/url_launcher.dart'; -final _dateFormatter = DateFormat.MMMd(Intl.getCurrentLocale()); +final _dateFormatter = DateFormat.MMMd(); /// A tab that displays the overview of a broadcast. class BroadcastOverviewTab extends ConsumerWidget { diff --git a/lib/src/view/broadcast/broadcasts_list_screen.dart b/lib/src/view/broadcast/broadcasts_list_screen.dart index 895c30ef1f..5eb6ea697c 100644 --- a/lib/src/view/broadcast/broadcasts_list_screen.dart +++ b/lib/src/view/broadcast/broadcasts_list_screen.dart @@ -15,9 +15,8 @@ import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; -final _dateFormatter = DateFormat.MMMd(Intl.getCurrentLocale()).add_Hm(); -final _dateFormatterWithYear = - DateFormat.yMMMd(Intl.getCurrentLocale()).add_Hm(); +final _dateFormatter = DateFormat.MMMd().add_Hm(); +final _dateFormatterWithYear = DateFormat.yMMMd().add_Hm(); /// A screen that displays a paginated list of broadcasts. class BroadcastsListScreen extends StatelessWidget { diff --git a/lib/src/view/puzzle/puzzle_history_screen.dart b/lib/src/view/puzzle/puzzle_history_screen.dart index 9a4d7504ae..9fbbab7774 100644 --- a/lib/src/view/puzzle/puzzle_history_screen.dart +++ b/lib/src/view/puzzle/puzzle_history_screen.dart @@ -18,7 +18,7 @@ import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:timeago/timeago.dart' as timeago; -final _dateFormatter = DateFormat.yMMMd(Intl.getCurrentLocale()); +final _dateFormatter = DateFormat.yMMMd(); class PuzzleHistoryScreen extends StatelessWidget { @override diff --git a/lib/src/view/user/perf_stats_screen.dart b/lib/src/view/user/perf_stats_screen.dart index 3b8cb7301c..2ecda93d41 100644 --- a/lib/src/view/user/perf_stats_screen.dart +++ b/lib/src/view/user/perf_stats_screen.dart @@ -35,8 +35,7 @@ import 'package:lichess_mobile/src/widgets/rating.dart'; import 'package:lichess_mobile/src/widgets/stat_card.dart'; import 'package:lichess_mobile/src/widgets/user_full_name.dart'; -final _currentLocale = Intl.getCurrentLocale(); -final _dateFormatter = DateFormat.yMMMd(_currentLocale); +final _dateFormatter = DateFormat.yMMMd(); const _customOpacity = 0.6; const _defaultStatFontSize = 12.0; @@ -846,11 +845,11 @@ class _EloChartState extends State<_EloChart> { Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5); final chartColor = Theme.of(context).colorScheme.tertiary; final chartDateFormatter = switch (_selectedRange) { - DateRange.oneWeek => DateFormat.MMMd(_currentLocale), - DateRange.oneMonth => DateFormat.MMMd(_currentLocale), - DateRange.threeMonths => DateFormat.yMMM(_currentLocale), - DateRange.oneYear => DateFormat.yMMM(_currentLocale), - DateRange.allTime => DateFormat.yMMM(_currentLocale), + DateRange.oneWeek => DateFormat.MMMd(), + DateRange.oneMonth => DateFormat.MMMd(), + DateRange.threeMonths => DateFormat.yMMM(), + DateRange.oneYear => DateFormat.yMMM(), + DateRange.allTime => DateFormat.yMMM(), }; String formatDateFromTimestamp(double nbDays) => chartDateFormatter.format( @@ -858,7 +857,7 @@ class _EloChartState extends State<_EloChart> { ); String formatDateFromTimestampForTooltip(double nbDays) => - DateFormat.yMMMd(_currentLocale).format( + DateFormat.yMMMd().format( _firstDate.add(Duration(days: nbDays.toInt())), ); diff --git a/lib/src/view/user/user_activity.dart b/lib/src/view/user/user_activity.dart index 1e87d8cbe9..faf9157550 100644 --- a/lib/src/view/user/user_activity.dart +++ b/lib/src/view/user/user_activity.dart @@ -19,7 +19,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'user_activity.g.dart'; -final _dateFormatter = DateFormat.yMMMd(Intl.getCurrentLocale()); +final _dateFormatter = DateFormat.yMMMd(); @riverpod Future> _userActivity( From f1c1e95e3b47a7ba8cfec0bf1a30f7a5ebfdb280 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Tue, 17 Sep 2024 21:05:40 +0200 Subject: [PATCH 366/979] fix deprecation warnings --- lib/src/view/broadcast/broadcast_analysis_screen.dart | 4 ++-- lib/src/view/broadcast/broadcast_screen.dart | 2 +- lib/src/widgets/board_thumbnail.dart | 5 +++-- lib/src/widgets/evaluation_bar.dart | 4 ++-- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_analysis_screen.dart b/lib/src/view/broadcast/broadcast_analysis_screen.dart index a1fd80494f..2f2f8703d7 100644 --- a/lib/src/view/broadcast/broadcast_analysis_screen.dart +++ b/lib/src/view/broadcast/broadcast_analysis_screen.dart @@ -1449,7 +1449,7 @@ class AcplChart extends ConsumerWidget { .textTheme .labelMedium ?.color - ?.withOpacity(0.3), + ?.withValues(alpha: 0.3), ), labelResolver: (line) => label, padding: const EdgeInsets.only(right: 1), @@ -1563,7 +1563,7 @@ class AcplChart extends ConsumerWidget { spots: spots, isCurved: false, barWidth: 1, - color: mainLineColor.withOpacity(0.7), + color: mainLineColor.withValues(alpha: 0.7), aboveBarData: BarAreaData( show: true, color: aboveLineColor, diff --git a/lib/src/view/broadcast/broadcast_screen.dart b/lib/src/view/broadcast/broadcast_screen.dart index 151fcbff88..c89ba4ac82 100644 --- a/lib/src/view/broadcast/broadcast_screen.dart +++ b/lib/src/view/broadcast/broadcast_screen.dart @@ -311,7 +311,7 @@ Widget _wrapWithBackground({ child: result, ); - if (backgroundColor.alpha == 0xFF) { + if (backgroundColor.a == 0xFF) { return childWithBackground; } diff --git a/lib/src/widgets/board_thumbnail.dart b/lib/src/widgets/board_thumbnail.dart index 9f872d0c76..a2b0390dca 100644 --- a/lib/src/widgets/board_thumbnail.dart +++ b/lib/src/widgets/board_thumbnail.dart @@ -132,8 +132,9 @@ class _BoardThumbnailState extends ConsumerState { : SizedBox( height: widget.size, width: widget.size * evaluationBarAspectRatio, - child: - ColoredBox(color: Colors.grey.withOpacity(0.6)), + child: ColoredBox( + color: Colors.grey.withValues(alpha: 0.6), + ), ), ), ], diff --git a/lib/src/widgets/evaluation_bar.dart b/lib/src/widgets/evaluation_bar.dart index 35b27d3d5d..00374e6c9b 100644 --- a/lib/src/widgets/evaluation_bar.dart +++ b/lib/src/widgets/evaluation_bar.dart @@ -29,12 +29,12 @@ class EvaluationBar extends StatelessWidget { SizedBox( height: height - whiteBarHeight, width: height * evaluationBarAspectRatio, - child: ColoredBox(color: Colors.black.withOpacity(0.6)), + child: ColoredBox(color: Colors.black.withValues(alpha: 0.6)), ), SizedBox( height: whiteBarHeight, width: height * evaluationBarAspectRatio, - child: ColoredBox(color: Colors.white.withOpacity(0.6)), + child: ColoredBox(color: Colors.white.withValues(alpha: 0.6)), ), ], ), From f6666b9d194090173c99dd256910b9db851a4dec Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Tue, 17 Sep 2024 23:25:52 +0200 Subject: [PATCH 367/979] add a try/catch when getting broadcast preferences --- lib/src/model/broadcast/broadcast_preferences.dart | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/src/model/broadcast/broadcast_preferences.dart b/lib/src/model/broadcast/broadcast_preferences.dart index 624e26261e..2d81f05e88 100644 --- a/lib/src/model/broadcast/broadcast_preferences.dart +++ b/lib/src/model/broadcast/broadcast_preferences.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:flutter/foundation.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/db/shared_preferences.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; @@ -45,6 +46,14 @@ class BroadcastPrefState with _$BroadcastPrefState { showEvaluationBar: true, ); - factory BroadcastPrefState.fromJson(Map json) => - _$BroadcastPrefStateFromJson(json); + factory BroadcastPrefState.fromJson(Map json) { + try { + return _$BroadcastPrefStateFromJson(json); + } catch (e) { + debugPrint( + '[BroadcastPreferences] Error getting broadcast preferences: $e', + ); + return BroadcastPrefState.defaults(); + } + } } From 13787a9494aa6f80f531a3ec92ba6004e9cae926 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 18 Sep 2024 16:33:35 +0200 Subject: [PATCH 368/979] More work on challenges: added decline reasons --- lib/main.dart | 5 -- lib/src/app.dart | 5 ++ lib/src/model/challenge/challenge.dart | 86 ++++++++----------- .../model/challenge/challenge_repository.dart | 2 +- .../model/challenge/challenge_service.dart | 11 +-- lib/src/model/lobby/create_game_service.dart | 4 +- .../notifications/challenge_notification.dart | 15 ++-- lib/src/view/game/game_screen.dart | 7 +- lib/src/view/play/challenge_list_item.dart | 63 ++++++++++---- ...reen.dart => create_challenge_screen.dart} | 8 +- lib/src/view/play/online_bots_screen.dart | 4 +- .../view/user/challenge_requests_screen.dart | 38 +++----- lib/src/view/user/user_screen.dart | 5 +- pubspec.lock | 6 +- pubspec.yaml | 4 +- 15 files changed, 135 insertions(+), 128 deletions(-) rename lib/src/view/play/{challenge_screen.dart => create_challenge_screen.dart} (98%) diff --git a/lib/main.dart b/lib/main.dart index 57feb4f568..890de6f50c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -17,7 +17,6 @@ import 'package:lichess_mobile/src/intl.dart'; import 'package:lichess_mobile/src/log.dart'; import 'package:lichess_mobile/src/model/challenge/challenge_repository.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; import 'package:lichess_mobile/src/model/correspondence/correspondence_game_storage.dart'; import 'package:lichess_mobile/src/model/correspondence/offline_correspondence_game.dart'; import 'package:lichess_mobile/src/model/game/playable_game.dart'; @@ -88,10 +87,6 @@ Future main() async { }; } - // preload sounds - final soundTheme = GeneralPreferences.fetchFromStorage(prefs).soundTheme; - await preloadSounds(soundTheme); - // Get android 12+ core palette try { await DynamicColorPlugin.getCorePalette().then((value) { diff --git a/lib/src/app.dart b/lib/src/app.dart index 3f3c395a55..ad15979d88 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -16,6 +16,7 @@ import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/challenge/challenge_service.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/correspondence/correspondence_service.dart'; import 'package:lichess_mobile/src/model/notifications/local_notification_service.dart'; @@ -111,6 +112,10 @@ class _AppState extends ConsumerState { setOptimalDisplayMode(); } + // preload sounds + final soundTheme = ref.read(generalPreferencesProvider).soundTheme; + preloadSounds(soundTheme); + // check if session is still active checkSession(); diff --git a/lib/src/model/challenge/challenge.dart b/lib/src/model/challenge/challenge.dart index aca960c359..c7f1c0612c 100644 --- a/lib/src/model/challenge/challenge.dart +++ b/lib/src/model/challenge/challenge.dart @@ -1,5 +1,4 @@ import 'package:deep_pick/deep_pick.dart'; -import 'package:flutter/widgets.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/l10n/l10n.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; @@ -9,7 +8,6 @@ import 'package:lichess_mobile/src/model/common/speed.dart'; import 'package:lichess_mobile/src/model/common/time_increment.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/utils/json.dart'; -import 'package:lichess_mobile/src/utils/l10n_context.dart'; part 'challenge.freezed.dart'; @@ -54,7 +52,7 @@ class Challenge with _$Challenge, BaseChallenge implements BaseChallenge { required SideChoice sideChoice, ChallengeUser? challenger, ChallengeUser? destUser, - DeclineReason? declineReason, + ChallengeDeclineReason? declineReason, String? initialFen, ChallengeDirection? direction, }) = _Challenge; @@ -67,6 +65,11 @@ class Challenge with _$Challenge, BaseChallenge implements BaseChallenge { /// The description of the challenge. String description(AppLocalizations l10n) { + if (!variant.isPlaySupported) { + // TODO: l10n + return 'This variant is not yet supported on the app.'; + } + final time = switch (timeControl) { ChallengeTimeControlType.clock => () { final minutes = switch (clock!.time.inSeconds) { @@ -163,7 +166,7 @@ enum ChallengeTimeControlType { correspondence, } -enum DeclineReason { +enum ChallengeDeclineReason { generic, later, tooFast, @@ -174,34 +177,21 @@ enum DeclineReason { standard, variant, noBot, - onlyBot, -} - -String declineReasonMessage(BuildContext context, DeclineReason key) { - switch (key) { - case DeclineReason.generic: - return context.l10n.challengeDeclineGeneric; - case DeclineReason.later: - return context.l10n.challengeDeclineLater; - case DeclineReason.tooFast: - return context.l10n.challengeDeclineTooFast; - case DeclineReason.tooSlow: - return context.l10n.challengeDeclineTooSlow; - case DeclineReason.timeControl: - return context.l10n.challengeDeclineTimeControl; - case DeclineReason.rated: - return context.l10n.challengeDeclineRated; - case DeclineReason.casual: - return context.l10n.challengeDeclineCasual; - case DeclineReason.standard: - return context.l10n.challengeDeclineStandard; - case DeclineReason.variant: - return context.l10n.challengeDeclineVariant; - case DeclineReason.noBot: - return context.l10n.challengeDeclineNoBot; - case DeclineReason.onlyBot: - return context.l10n.challengeDeclineOnlyBot; - } + onlyBot; + + String label(AppLocalizations l10n) => switch (this) { + ChallengeDeclineReason.generic => l10n.challengeDeclineGeneric, + ChallengeDeclineReason.later => l10n.challengeDeclineLater, + ChallengeDeclineReason.tooFast => l10n.challengeDeclineTooFast, + ChallengeDeclineReason.tooSlow => l10n.challengeDeclineTooSlow, + ChallengeDeclineReason.timeControl => l10n.challengeDeclineTimeControl, + ChallengeDeclineReason.rated => l10n.challengeDeclineRated, + ChallengeDeclineReason.casual => l10n.challengeDeclineCasual, + ChallengeDeclineReason.standard => l10n.challengeDeclineStandard, + ChallengeDeclineReason.variant => l10n.challengeDeclineVariant, + ChallengeDeclineReason.noBot => l10n.challengeDeclineNoBot, + ChallengeDeclineReason.onlyBot => l10n.challengeDeclineOnlyBot, + }; } typedef ChallengeUser = ({ @@ -363,47 +353,47 @@ extension ChallengeExtension on Pick { } } - DeclineReason asDeclineReasonOrThrow() { + ChallengeDeclineReason asDeclineReasonOrThrow() { final value = this.required().value; - if (value is DeclineReason) { + if (value is ChallengeDeclineReason) { return value; } if (value is String) { switch (value) { case 'generic': - return DeclineReason.generic; + return ChallengeDeclineReason.generic; case 'later': - return DeclineReason.later; + return ChallengeDeclineReason.later; case 'tooFast': - return DeclineReason.tooFast; + return ChallengeDeclineReason.tooFast; case 'tooSlow': - return DeclineReason.tooSlow; + return ChallengeDeclineReason.tooSlow; case 'timeControl': - return DeclineReason.timeControl; + return ChallengeDeclineReason.timeControl; case 'rated': - return DeclineReason.rated; + return ChallengeDeclineReason.rated; case 'casual': - return DeclineReason.casual; + return ChallengeDeclineReason.casual; case 'standard': - return DeclineReason.standard; + return ChallengeDeclineReason.standard; case 'variant': - return DeclineReason.variant; + return ChallengeDeclineReason.variant; case 'noBot': - return DeclineReason.noBot; + return ChallengeDeclineReason.noBot; case 'onlyBot': - return DeclineReason.onlyBot; + return ChallengeDeclineReason.onlyBot; default: throw PickException( - "value $value at $debugParsingExit can't be casted to DeclineReason: invalid string.", + "value $value at $debugParsingExit can't be casted to ChallengeDeclineReason: invalid string.", ); } } throw PickException( - "value $value at $debugParsingExit can't be casted to DeclineReason", + "value $value at $debugParsingExit can't be casted to ChallengeDeclineReason", ); } - DeclineReason? asDeclineReasonOrNull() { + ChallengeDeclineReason? asDeclineReasonOrNull() { if (value == null) return null; try { return asDeclineReasonOrThrow(); diff --git a/lib/src/model/challenge/challenge_repository.dart b/lib/src/model/challenge/challenge_repository.dart index 8b95a32b01..e44d5a7079 100644 --- a/lib/src/model/challenge/challenge_repository.dart +++ b/lib/src/model/challenge/challenge_repository.dart @@ -68,7 +68,7 @@ class ChallengeRepository { } } - Future decline(ChallengeId id, {DeclineReason? reason}) async { + Future decline(ChallengeId id, {ChallengeDeclineReason? reason}) async { final uri = Uri(path: '/api/challenge/$id/decline'); final response = await client.post( uri, diff --git a/lib/src/model/challenge/challenge_service.dart b/lib/src/model/challenge/challenge_service.dart index 28ef4f8072..d68ae64171 100644 --- a/lib/src/model/challenge/challenge_service.dart +++ b/lib/src/model/challenge/challenge_service.dart @@ -6,7 +6,6 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/widgets.dart'; import 'package:lichess_mobile/src/model/challenge/challenge.dart'; import 'package:lichess_mobile/src/model/challenge/challenge_repository.dart'; -import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/notifications/challenge_notification.dart'; @@ -80,14 +79,8 @@ class ChallengeService { .whereNot((challenge) => prevInwardIds.contains(challenge.id)) .forEach( (challenge) { - if (playSupportedVariants.contains(challenge.variant)) { - LocalNotificationService.instance - .show(ChallengeNotification(challenge, l10n)); - } else { - ref - .read(challengeRepositoryProvider) - .decline(challenge.id, reason: DeclineReason.variant); - } + LocalNotificationService.instance + .show(ChallengeNotification(challenge, l10n)); }, ); } diff --git a/lib/src/model/lobby/create_game_service.dart b/lib/src/model/lobby/create_game_service.dart index 97a36fd8d2..dc950388a6 100644 --- a/lib/src/model/lobby/create_game_service.dart +++ b/lib/src/model/lobby/create_game_service.dart @@ -107,7 +107,7 @@ class CreateGameService { /// Create a new challenge game. /// /// Returns the game id or the decline reason if the challenge was declined. - Future<(GameFullId?, DeclineReason?)> newChallenge( + Future<(GameFullId?, ChallengeDeclineReason?)> newChallenge( ChallengeRequest challengeReq, ) async { if (_challengeConnection != null) { @@ -115,7 +115,7 @@ class CreateGameService { } // ensure the pending connection is closed in any case - final completer = Completer<(GameFullId?, DeclineReason?)>() + final completer = Completer<(GameFullId?, ChallengeDeclineReason?)>() ..future.whenComplete(dispose); try { diff --git a/lib/src/model/notifications/challenge_notification.dart b/lib/src/model/notifications/challenge_notification.dart index 9f666539d3..0b130e9c11 100644 --- a/lib/src/model/notifications/challenge_notification.dart +++ b/lib/src/model/notifications/challenge_notification.dart @@ -32,13 +32,14 @@ class ChallengeNotification implements LocalNotification { priority: Priority.high, autoCancel: false, actions: [ - AndroidNotificationAction( - 'accept', - _l10n.accept, - icon: const DrawableResourceAndroidBitmap('tick'), - showsUserInterface: true, - contextual: true, - ), + if (_challenge.variant.isPlaySupported) + AndroidNotificationAction( + 'accept', + _l10n.accept, + icon: const DrawableResourceAndroidBitmap('tick'), + showsUserInterface: true, + contextual: true, + ), AndroidNotificationAction( 'decline', _l10n.decline, diff --git a/lib/src/view/game/game_screen.dart b/lib/src/view/game/game_screen.dart index 2f0069d3af..508b3a8094 100644 --- a/lib/src/view/game/game_screen.dart +++ b/lib/src/view/game/game_screen.dart @@ -11,6 +11,7 @@ import 'package:lichess_mobile/src/model/lobby/create_game_service.dart'; import 'package:lichess_mobile/src/model/lobby/game_seek.dart'; import 'package:lichess_mobile/src/model/lobby/game_setup.dart'; import 'package:lichess_mobile/src/navigation.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/game/game_loading_board.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; @@ -24,7 +25,7 @@ part 'game_screen.g.dart'; @riverpod class _LoadGame extends _$LoadGame { @override - Future<(GameFullId?, DeclineReason?)> build( + Future<(GameFullId?, ChallengeDeclineReason?)> build( GameSeek? seek, ChallengeRequest? challenge, GameFullId? gameId, @@ -177,8 +178,8 @@ class _GameScreenState extends ConsumerState with RouteAware { : widget.challenge != null ? ChallengeDeclinedBoard( declineReason: declineReason != null - ? declineReasonMessage(context, declineReason) - : declineReasonMessage(context, DeclineReason.generic), + ? declineReason.label(context.l10n) + : ChallengeDeclineReason.generic.label(context.l10n), destUser: widget.challenge?.destUser, ) : const LoadGameError('Could not create the game.'); diff --git a/lib/src/view/play/challenge_list_item.dart b/lib/src/view/play/challenge_list_item.dart index 7be1d71780..2d1f369144 100644 --- a/lib/src/view/play/challenge_list_item.dart +++ b/lib/src/view/play/challenge_list_item.dart @@ -10,6 +10,7 @@ import 'package:lichess_mobile/src/model/lobby/correspondence_challenge.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/user_full_name.dart'; @@ -20,13 +21,17 @@ class ChallengeListItem extends ConsumerWidget { required this.challenge, required this.user, this.onPressed, + this.onAccept, + this.onDecline, this.onCancel, }); final Challenge challenge; final LightUser user; final VoidCallback? onPressed; + final VoidCallback? onAccept; final VoidCallback? onCancel; + final void Function(ChallengeDeclineReason reason)? onDecline; @override Widget build(BuildContext context, WidgetRef ref) { @@ -40,22 +45,32 @@ class ChallengeListItem extends ConsumerWidget { return Container( color: color, child: Slidable( - endActionPane: onCancel != null - ? ActionPane( - motion: const ScrollMotion(), - extentRatio: 0.3, - children: [ - SlidableAction( - onPressed: (BuildContext context) => onCancel!(), - backgroundColor: context.lichessColors.error, - foregroundColor: Colors.white, - label: isMyChallenge - ? context.l10n.cancel - : context.l10n.decline, - ), - ], - ) - : null, + endActionPane: ActionPane( + motion: const ScrollMotion(), + extentRatio: 0.6, + children: [ + if (onAccept != null) + SlidableAction( + icon: Icons.check, + onPressed: (_) => onAccept!(), + spacing: 8.0, + backgroundColor: context.lichessColors.good, + foregroundColor: Colors.white, + label: context.l10n.accept, + ), + if (onDecline != null || (isMyChallenge && onCancel != null)) + SlidableAction( + icon: Icons.close, + onPressed: + isMyChallenge ? (_) => onCancel!() : _showDeclineReasons, + spacing: 8.0, + backgroundColor: context.lichessColors.error, + foregroundColor: Colors.white, + label: + isMyChallenge ? context.l10n.cancel : context.l10n.decline, + ), + ], + ), child: PlatformListTile( padding: Styles.bodyPadding, leading: Icon(challenge.perf.icon, size: 36), @@ -78,6 +93,22 @@ class ChallengeListItem extends ConsumerWidget { ), ); } + + void _showDeclineReasons(BuildContext context) { + showAdaptiveActionSheet( + context: context, + actions: ChallengeDeclineReason.values + .map( + (reason) => BottomSheetAction( + makeLabel: (context) => Text(reason.label(context.l10n)), + onPressed: (_) { + onDecline?.call(reason); + }, + ), + ) + .toList(), + ); + } } class CorrespondenceChallengeListItem extends StatelessWidget { diff --git a/lib/src/view/play/challenge_screen.dart b/lib/src/view/play/create_challenge_screen.dart similarity index 98% rename from lib/src/view/play/challenge_screen.dart rename to lib/src/view/play/create_challenge_screen.dart index 3ebe8bc94b..73e05c5f51 100644 --- a/lib/src/view/play/challenge_screen.dart +++ b/lib/src/view/play/create_challenge_screen.dart @@ -28,8 +28,8 @@ import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/non_linear_slider.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; -class ChallengeScreen extends StatelessWidget { - const ChallengeScreen(this.user); +class CreateChallengeScreen extends StatelessWidget { + const CreateChallengeScreen(this.user); final LightUser user; @@ -54,7 +54,7 @@ class _ChallengeBody extends ConsumerStatefulWidget { } class _ChallengeBodyState extends ConsumerState<_ChallengeBody> { - Future<(GameFullId?, DeclineReason?)>? _pendingCreateChallenge; + Future<(GameFullId?, ChallengeDeclineReason?)>? _pendingCreateChallenge; final _controller = TextEditingController(); String? fromPositionFenInput; @@ -436,7 +436,7 @@ class _ChallengeBodyState extends ConsumerState<_ChallengeBody> { } else { showPlatformSnackbar( context, - '${widget.user.name}: ${declineReasonMessage(context, declineReason!)}', + '${widget.user.name}: ${declineReason!.label(context.l10n)}', ); } }); diff --git a/lib/src/view/play/online_bots_screen.dart b/lib/src/view/play/online_bots_screen.dart index bd5e26e427..91139e47a0 100644 --- a/lib/src/view/play/online_bots_screen.dart +++ b/lib/src/view/play/online_bots_screen.dart @@ -12,7 +12,7 @@ import 'package:lichess_mobile/src/model/user/user_repository.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; -import 'package:lichess_mobile/src/view/play/challenge_screen.dart'; +import 'package:lichess_mobile/src/view/play/create_challenge_screen.dart'; import 'package:lichess_mobile/src/view/user/user_screen.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; @@ -159,7 +159,7 @@ class _Body extends ConsumerWidget { pushPlatformRoute( context, title: context.l10n.challengeChallengesX(bot.lightUser.name), - builder: (context) => ChallengeScreen(bot.lightUser), + builder: (context) => CreateChallengeScreen(bot.lightUser), ); }, onLongPress: () { diff --git a/lib/src/view/user/challenge_requests_screen.dart b/lib/src/view/user/challenge_requests_screen.dart index 631adf378d..80afbae3e0 100644 --- a/lib/src/view/user/challenge_requests_screen.dart +++ b/lib/src/view/user/challenge_requests_screen.dart @@ -1,4 +1,3 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; @@ -14,34 +13,20 @@ import 'package:lichess_mobile/src/view/play/challenge_list_item.dart'; import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; -class ChallengeRequestsScreen extends ConsumerWidget { +class ChallengeRequestsScreen extends StatelessWidget { const ChallengeRequestsScreen({super.key}); @override - Widget build(BuildContext context, WidgetRef ref) { - return PlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, - ); - } - - Widget _androidBuilder(BuildContext context) { - return Scaffold( - appBar: AppBar( + Widget build(BuildContext context) { + return PlatformScaffold( + appBar: PlatformAppBar( title: Text(context.l10n.preferencesNotifyChallenge), ), body: _Body(), ); } - - Widget _iosBuilder(BuildContext context) { - return CupertinoPageScaffold( - navigationBar: const CupertinoNavigationBar(), - child: _Body(), - ); - } } class _Body extends ConsumerWidget { @@ -70,7 +55,9 @@ class _Body extends ConsumerWidget { return ChallengeListItem( challenge: challenge, user: user, - onPressed: challenge.direction == ChallengeDirection.outward + onAccept: challenge.direction == + ChallengeDirection.outward || + !challenge.variant.isPlaySupported ? null : session == null ? () { @@ -111,13 +98,16 @@ class _Body extends ConsumerWidget { ? () => ref .read(challengeRepositoryProvider) .cancel(challenge.id) - : () { + : null, + onDecline: challenge.direction == ChallengeDirection.inward + ? (ChallengeDeclineReason reason) { ref .read(challengeRepositoryProvider) - .decline(challenge.id); + .decline(challenge.id, reason: reason); LocalNotificationService.instance .cancel(challenge.id.value.hashCode); - }, + } + : null, ); }, ); diff --git a/lib/src/view/user/user_screen.dart b/lib/src/view/user/user_screen.dart index 41241faafc..747e2210ae 100644 --- a/lib/src/view/user/user_screen.dart +++ b/lib/src/view/user/user_screen.dart @@ -10,7 +10,7 @@ import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; -import 'package:lichess_mobile/src/view/play/challenge_screen.dart'; +import 'package:lichess_mobile/src/view/play/create_challenge_screen.dart'; import 'package:lichess_mobile/src/view/user/recent_games.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; @@ -127,7 +127,8 @@ class _UserProfileListView extends ConsumerWidget { onTap: () { pushPlatformRoute( context, - builder: (context) => ChallengeScreen(user.lightUser), + builder: (context) => + CreateChallengeScreen(user.lightUser), ); }, ), diff --git a/pubspec.lock b/pubspec.lock index b018bbb65b..0df15aa7f2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -660,9 +660,9 @@ packages: dependency: "direct main" description: path: "." - ref: "3280106581fc8d54eae45f4a446f92cae36d7837" - resolved-ref: "3280106581fc8d54eae45f4a446f92cae36d7837" - url: "https://github.com/letsar/flutter_slidable.git" + ref: "89b8384667d3b6c1c2967a8ff10846bcf0a170c7" + resolved-ref: "89b8384667d3b6c1c2967a8ff10846bcf0a170c7" + url: "https://github.com/veloce/flutter_slidable.git" source: git version: "3.1.1" flutter_spinkit: diff --git a/pubspec.yaml b/pubspec.yaml index eebe28fa50..4a16ffdeed 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -41,8 +41,8 @@ dependencies: flutter_secure_storage: ^9.2.0 flutter_slidable: git: - url: https://github.com/letsar/flutter_slidable.git - ref: 3280106581fc8d54eae45f4a446f92cae36d7837 + url: https://github.com/veloce/flutter_slidable.git + ref: 89b8384667d3b6c1c2967a8ff10846bcf0a170c7 flutter_spinkit: ^5.2.0 flutter_svg: ^2.0.10+1 freezed_annotation: ^2.2.0 From b2112daf4be788d3a58966f50f2ef81ce51e2889 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 18 Sep 2024 22:54:51 +0200 Subject: [PATCH 369/979] More work on challenges --- lib/src/model/challenge/challenge.dart | 34 +----- lib/src/model/lobby/create_game_service.dart | 28 ++++- lib/src/view/game/game_loading_board.dart | 38 +++++-- lib/src/view/game/game_screen.dart | 107 +++++++++++------- .../view/play/create_challenge_screen.dart | 9 +- 5 files changed, 125 insertions(+), 91 deletions(-) diff --git a/lib/src/model/challenge/challenge.dart b/lib/src/model/challenge/challenge.dart index c7f1c0612c..fbaef3394f 100644 --- a/lib/src/model/challenge/challenge.dart +++ b/lib/src/model/challenge/challenge.dart @@ -359,34 +359,12 @@ extension ChallengeExtension on Pick { return value; } if (value is String) { - switch (value) { - case 'generic': - return ChallengeDeclineReason.generic; - case 'later': - return ChallengeDeclineReason.later; - case 'tooFast': - return ChallengeDeclineReason.tooFast; - case 'tooSlow': - return ChallengeDeclineReason.tooSlow; - case 'timeControl': - return ChallengeDeclineReason.timeControl; - case 'rated': - return ChallengeDeclineReason.rated; - case 'casual': - return ChallengeDeclineReason.casual; - case 'standard': - return ChallengeDeclineReason.standard; - case 'variant': - return ChallengeDeclineReason.variant; - case 'noBot': - return ChallengeDeclineReason.noBot; - case 'onlyBot': - return ChallengeDeclineReason.onlyBot; - default: - throw PickException( - "value $value at $debugParsingExit can't be casted to ChallengeDeclineReason: invalid string.", - ); - } + return ChallengeDeclineReason.values.firstWhere( + (element) => element.name.toLowerCase() == value, + orElse: () => throw PickException( + "value $value at $debugParsingExit can't be casted to ChallengeDeclineReason: invalid string.", + ), + ); } throw PickException( "value $value at $debugParsingExit can't be casted to ChallengeDeclineReason", diff --git a/lib/src/model/lobby/create_game_service.dart b/lib/src/model/lobby/create_game_service.dart index dc950388a6..177bbe26b4 100644 --- a/lib/src/model/lobby/create_game_service.dart +++ b/lib/src/model/lobby/create_game_service.dart @@ -14,6 +14,12 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'create_game_service.g.dart'; +typedef ChallengeResponse = ({ + GameFullId? gameFullId, + Challenge? challenge, + ChallengeDeclineReason? declineReason, +}); + @riverpod CreateGameService createGameService(CreateGameServiceRef ref) { final service = CreateGameService(Logger('CreateGameService'), ref: ref); @@ -107,15 +113,13 @@ class CreateGameService { /// Create a new challenge game. /// /// Returns the game id or the decline reason if the challenge was declined. - Future<(GameFullId?, ChallengeDeclineReason?)> newChallenge( - ChallengeRequest challengeReq, - ) async { + Future newChallenge(ChallengeRequest challengeReq) async { if (_challengeConnection != null) { throw StateError('Already creating a game.'); } // ensure the pending connection is closed in any case - final completer = Completer<(GameFullId?, ChallengeDeclineReason?)>() + final completer = Completer() ..future.whenComplete(dispose); try { @@ -147,9 +151,21 @@ class CreateGameService { try { final updatedChallenge = await repo.show(challenge.id); if (updatedChallenge.gameFullId != null) { - completer.complete((updatedChallenge.gameFullId, null)); + completer.complete( + ( + gameFullId: updatedChallenge.gameFullId, + challenge: null, + declineReason: null, + ), + ); } else if (updatedChallenge.status == ChallengeStatus.declined) { - completer.complete((null, updatedChallenge.declineReason)); + completer.complete( + ( + gameFullId: null, + challenge: challenge, + declineReason: updatedChallenge.declineReason, + ), + ); } } catch (e) { _log.warning('Failed to reload challenge', e); diff --git a/lib/src/view/game/game_loading_board.dart b/lib/src/view/game/game_loading_board.dart index 66daae645d..bf0a216b2b 100644 --- a/lib/src/view/game/game_loading_board.dart +++ b/lib/src/view/game/game_loading_board.dart @@ -6,11 +6,11 @@ import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/challenge/challenge.dart'; import 'package:lichess_mobile/src/model/lobby/game_seek.dart'; import 'package:lichess_mobile/src/model/lobby/lobby_numbers.dart'; -import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/widgets/board_table.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; +import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/user_full_name.dart'; @@ -232,18 +232,20 @@ class LoadGameError extends StatelessWidget { } } +/// A board that shows a message that a challenge has been declined. class ChallengeDeclinedBoard extends StatelessWidget { const ChallengeDeclinedBoard({ required this.declineReason, - this.destUser, + required this.challenge, }); final String declineReason; - - final LightUser? destUser; + final Challenge challenge; @override Widget build(BuildContext context) { + final textColor = DefaultTextStyle.of(context).style.color; + return Column( children: [ Expanded( @@ -258,7 +260,7 @@ class ChallengeDeclinedBoard extends StatelessWidget { boardOverlay: PlatformCard( elevation: 2.0, child: Padding( - padding: const EdgeInsets.all(12.0), + padding: const EdgeInsets.all(16.0), child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -268,18 +270,34 @@ class ChallengeDeclinedBoard extends StatelessWidget { context.l10n.challengeChallengeDeclined, style: Theme.of(context).textTheme.titleMedium, ), - const SizedBox(height: 16.0), + const SizedBox(height: 8.0), + Divider(height: 26.0, thickness: 0.0, color: textColor), Text( declineReason, style: const TextStyle(fontStyle: FontStyle.italic), ), - if (destUser != null) ...[ - const SizedBox(height: 8.0), + Divider(height: 26.0, thickness: 0.0, color: textColor), + if (challenge.destUser != null) Align( alignment: Alignment.centerRight, - child: UserFullNameWidget(user: destUser), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Text(' — '), + UserFullNameWidget( + user: challenge.destUser?.user, + ), + if (challenge.destUser?.lagRating != null) ...[ + const SizedBox(width: 6.0), + LagIndicator( + lagRating: challenge.destUser!.lagRating!, + size: 13.0, + showLoadingIndicator: false, + ), + ], + ], + ), ), - ], ], ), ), diff --git a/lib/src/view/game/game_screen.dart b/lib/src/view/game/game_screen.dart index 508b3a8094..8dd52d7634 100644 --- a/lib/src/view/game/game_screen.dart +++ b/lib/src/view/game/game_screen.dart @@ -22,47 +22,14 @@ import 'game_common_widgets.dart'; part 'game_screen.g.dart'; -@riverpod -class _LoadGame extends _$LoadGame { - @override - Future<(GameFullId?, ChallengeDeclineReason?)> build( - GameSeek? seek, - ChallengeRequest? challenge, - GameFullId? gameId, - ) { - assert( - gameId != null || seek != null || challenge != null, - 'Either a seek, challenge or a game id must be provided.', - ); - - final service = ref.watch(createGameServiceProvider); - - if (seek != null) { - return service.newLobbyGame(seek).then((id) => (id, null)); - } else if (challenge != null) { - return service.newChallenge(challenge).then((c) => (c.$1, c.$2)); - } - - return Future.value((gameId!, null)); - } - - /// Search for a new opponent (lobby only). - Future newOpponent() async { - if (seek != null) { - final service = ref.read(createGameServiceProvider); - state = const AsyncValue.loading(); - state = AsyncValue.data( - await service.newLobbyGame(seek!).then((id) => (id, null)), - ); - } - } - - /// Load a game from its id. - void loadGame(GameFullId id) { - state = AsyncValue.data((id, null)); - } -} - +/// Screen to play a game, or to show a challenge or to show current user's past games. +/// +/// The screen can be created in three ways: +/// - From the lobby, to play a game with a random opponent: using a [GameSeek] as [seek]. +/// - From a challenge, to accept or decline a challenge: using a [ChallengeRequest] as [challenge]. +/// - From a game id, to show a game that is already in progress: using a [GameFullId] as [initialGameId]. +/// +/// The screen will show a loading board while the game is being created. class GameScreen extends ConsumerStatefulWidget { const GameScreen({ this.seek, @@ -145,7 +112,11 @@ class _GameScreenState extends ConsumerState with RouteAware { return ref.watch(gameProvider).when( data: (data) { - final (gameId, declineReason) = data; + final ( + gameFullId: gameId, + challenge: challenge, + declineReason: declineReason + ) = data; final body = gameId != null ? GameBody( id: gameId, @@ -175,12 +146,12 @@ class _GameScreenState extends ConsumerState with RouteAware { } }, ) - : widget.challenge != null + : widget.challenge != null && challenge != null ? ChallengeDeclinedBoard( + challenge: challenge, declineReason: declineReason != null ? declineReason.label(context.l10n) : ChallengeDeclineReason.generic.label(context.l10n), - destUser: widget.challenge?.destUser, ) : const LoadGameError('Could not create the game.'); return PlatformScaffold( @@ -235,3 +206,51 @@ class _GameScreenState extends ConsumerState with RouteAware { ); } } + +@riverpod +class _LoadGame extends _$LoadGame { + @override + Future build( + GameSeek? seek, + ChallengeRequest? challenge, + GameFullId? gameId, + ) { + assert( + gameId != null || seek != null || challenge != null, + 'Either a seek, challenge or a game id must be provided.', + ); + + final service = ref.watch(createGameServiceProvider); + + if (seek != null) { + return service + .newLobbyGame(seek) + .then((id) => (gameFullId: id, challenge: null, declineReason: null)); + } else if (challenge != null) { + return service.newChallenge(challenge); + } + + return Future.value( + (gameFullId: gameId!, challenge: null, declineReason: null), + ); + } + + /// Search for a new opponent (lobby only). + Future newOpponent() async { + if (seek != null) { + final service = ref.read(createGameServiceProvider); + state = const AsyncValue.loading(); + state = AsyncValue.data( + await service.newLobbyGame(seek!).then( + (id) => (gameFullId: id, challenge: null, declineReason: null), + ), + ); + } + } + + /// Load a game from its id. + void loadGame(GameFullId id) { + state = + AsyncValue.data((gameFullId: id, challenge: null, declineReason: null)); + } +} diff --git a/lib/src/view/play/create_challenge_screen.dart b/lib/src/view/play/create_challenge_screen.dart index 73e05c5f51..ea7da5072a 100644 --- a/lib/src/view/play/create_challenge_screen.dart +++ b/lib/src/view/play/create_challenge_screen.dart @@ -9,7 +9,6 @@ import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/challenge/challenge.dart'; import 'package:lichess_mobile/src/model/challenge/challenge_preferences.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; -import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/time_increment.dart'; import 'package:lichess_mobile/src/model/lobby/create_game_service.dart'; import 'package:lichess_mobile/src/model/lobby/game_setup.dart'; @@ -54,7 +53,7 @@ class _ChallengeBody extends ConsumerStatefulWidget { } class _ChallengeBodyState extends ConsumerState<_ChallengeBody> { - Future<(GameFullId?, ChallengeDeclineReason?)>? _pendingCreateChallenge; + Future? _pendingCreateChallenge; final _controller = TextEditingController(); String? fromPositionFenInput; @@ -421,7 +420,11 @@ class _ChallengeBodyState extends ConsumerState<_ChallengeBody> { _pendingCreateChallenge!.then((value) { if (!context.mounted) return; - final (fullId, declineReason) = value; + final ( + gameFullId: fullId, + challenge: _, + :declineReason + ) = value; if (fullId != null) { pushPlatformRoute( From a85c85558bb82cb9d4f94e2781115ffb529166bf Mon Sep 17 00:00:00 2001 From: anon Date: Tue, 17 Sep 2024 17:00:38 +0530 Subject: [PATCH 370/979] [BoardAnalysis] disable board analysis if more than 32 pieces on board --- lib/src/view/board_editor/board_editor_screen.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/src/view/board_editor/board_editor_screen.dart b/lib/src/view/board_editor/board_editor_screen.dart index 3339e30935..2012ab8195 100644 --- a/lib/src/view/board_editor/board_editor_screen.dart +++ b/lib/src/view/board_editor/board_editor_screen.dart @@ -300,6 +300,7 @@ class _BottomBar extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final editorState = ref.watch(boardEditorControllerProvider(initialFen)); + final pieceCount = editorState.pieces.length; return BottomBar( children: [ @@ -324,7 +325,10 @@ class _BottomBar extends ConsumerWidget { BottomBarButton( label: context.l10n.analysis, key: const Key('analysis-board-button'), - onTap: editorState.pgn != null + onTap: editorState.pgn != null && + // 1 condition (of many) where stockfish segfaults + pieceCount > 0 && + pieceCount <= 32 ? () { pushPlatformRoute( context, From ea014810ad99cdcbd2b00903d527f875ce1a6deb Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 19 Sep 2024 15:16:40 +0200 Subject: [PATCH 371/979] More improvements to challenge list screen --- lib/main.dart | 40 +---- .../model/challenge/challenge_service.dart | 27 ++- .../notifications/challenge_notification.dart | 38 +++- lib/src/view/play/challenge_list_item.dart | 28 +-- .../view/user/challenge_requests_screen.dart | 167 +++++++++++------- lib/src/widgets/adaptive_action_sheet.dart | 16 +- 6 files changed, 174 insertions(+), 142 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 890de6f50c..738df6d9f0 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -11,11 +11,9 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/l10n/l10n.dart'; -import 'package:lichess_mobile/src/app_initialization.dart'; import 'package:lichess_mobile/src/db/database.dart'; import 'package:lichess_mobile/src/intl.dart'; import 'package:lichess_mobile/src/log.dart'; -import 'package:lichess_mobile/src/model/challenge/challenge_repository.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/correspondence/correspondence_game_storage.dart'; import 'package:lichess_mobile/src/model/correspondence/offline_correspondence_game.dart'; @@ -66,7 +64,8 @@ Future main() async { iOS: DarwinInitializationSettings( requestBadgePermission: false, notificationCategories: [ - ChallengeNotification.darwinNotificationCategory(l10n), + ChallengeNotification.darwinPlayableVariantCategory(l10n), + ChallengeNotification.darwinUnplayableVariantCategory(l10n), ], ), ), @@ -133,40 +132,7 @@ Future main() async { @pragma('vm:entry-point') void notificationTapBackground(NotificationResponse response) { - // create a new provider container for the background isolate - final ref = ProviderContainer(); - - ref.listen( - appInitializationProvider, - (prev, now) { - if (!now.hasValue) return; - - if (response.id == null || response.payload == null) return; - - try { - final payload = NotificationPayload.fromJson( - jsonDecode(response.payload!) as Map, - ); - switch (payload.type) { - case NotificationType.challenge: - // only decline action is supported in the background - if (response.actionId == 'decline') { - final challengeId = ChallengePayload.fromNotification(payload).id; - ref.read(challengeRepositoryProvider).decline(challengeId); - } - default: - debugPrint('Unknown notification type: $payload'); - } - - ref.dispose(); - } catch (e) { - debugPrint('Failed to handle notification background response: $e'); - ref.dispose(); - } - }, - ); - - ref.read(appInitializationProvider); + debugPrint('Background local notification response: $response'); } @pragma('vm:entry-point') diff --git a/lib/src/model/challenge/challenge_service.dart b/lib/src/model/challenge/challenge_service.dart index d68ae64171..7a73ad148c 100644 --- a/lib/src/model/challenge/challenge_service.dart +++ b/lib/src/model/challenge/challenge_service.dart @@ -13,9 +13,11 @@ import 'package:lichess_mobile/src/model/notifications/local_notification.dart'; import 'package:lichess_mobile/src/model/notifications/local_notification_service.dart'; import 'package:lichess_mobile/src/navigation.dart'; import 'package:lichess_mobile/src/utils/l10n.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/game/game_screen.dart'; import 'package:lichess_mobile/src/view/user/challenge_requests_screen.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'challenge_service.g.dart'; @@ -107,8 +109,8 @@ class ChallengeService { .show(challengeId) .then((challenge) => challenge.gameFullId); - final context = ref.read(currentNavigatorKeyProvider).currentContext!; - if (!context.mounted) break; + final context = ref.read(currentNavigatorKeyProvider).currentContext; + if (context == null || !context.mounted) break; final navState = Navigator.of(context); if (navState.canPop()) { @@ -122,11 +124,26 @@ class ChallengeService { ); case 'decline': - final repo = ref.read(challengeRepositoryProvider); - repo.decline(challengeId); + final context = ref.read(currentNavigatorKeyProvider).currentContext; + if (context == null || !context.mounted) break; + showAdaptiveActionSheet( + context: context, + actions: ChallengeDeclineReason.values + .map( + (reason) => BottomSheetAction( + makeLabel: (context) => Text(reason.label(context.l10n)), + onPressed: (_) { + final repo = ref.read(challengeRepositoryProvider); + repo.decline(challengeId, reason: reason); + }, + ), + ) + .toList(), + ); case null: - final context = ref.read(currentNavigatorKeyProvider).currentContext!; + final context = ref.read(currentNavigatorKeyProvider).currentContext; + if (context == null || !context.mounted) break; final navState = Navigator.of(context); if (navState.canPop()) { navState.popUntil((route) => route.isFirst); diff --git a/lib/src/model/notifications/challenge_notification.dart b/lib/src/model/notifications/challenge_notification.dart index 0b130e9c11..87edb3f191 100644 --- a/lib/src/model/notifications/challenge_notification.dart +++ b/lib/src/model/notifications/challenge_notification.dart @@ -44,22 +44,29 @@ class ChallengeNotification implements LocalNotification { 'decline', _l10n.decline, icon: const DrawableResourceAndroidBitmap('cross'), + showsUserInterface: true, contextual: true, ), ], ), - iOS: const DarwinNotificationDetails( - categoryIdentifier: darwinCategoryId, + iOS: DarwinNotificationDetails( + categoryIdentifier: _challenge.variant.isPlaySupported + ? darwinPlayableVariantCategoryId + : darwinUnplayableVariantCategoryId, ), ); - static const darwinCategoryId = 'challenge-notification-category'; + static const darwinPlayableVariantCategoryId = + 'challenge-notification-playable-variant'; - static DarwinNotificationCategory darwinNotificationCategory( + static const darwinUnplayableVariantCategoryId = + 'challenge-notification-unplayable-variant'; + + static DarwinNotificationCategory darwinPlayableVariantCategory( AppLocalizations l10n, ) => DarwinNotificationCategory( - darwinCategoryId, + darwinPlayableVariantCategoryId, actions: [ DarwinNotificationAction.plain( 'accept', @@ -72,7 +79,26 @@ class ChallengeNotification implements LocalNotification { 'decline', l10n.decline, options: { - DarwinNotificationActionOption.destructive, + DarwinNotificationActionOption.foreground, + }, + ), + ], + options: { + DarwinNotificationCategoryOption.hiddenPreviewShowTitle, + }, + ); + + static DarwinNotificationCategory darwinUnplayableVariantCategory( + AppLocalizations l10n, + ) => + DarwinNotificationCategory( + darwinUnplayableVariantCategoryId, + actions: [ + DarwinNotificationAction.plain( + 'decline', + l10n.decline, + options: { + DarwinNotificationActionOption.foreground, }, ), ], diff --git a/lib/src/view/play/challenge_list_item.dart b/lib/src/view/play/challenge_list_item.dart index 2d1f369144..7318c1b018 100644 --- a/lib/src/view/play/challenge_list_item.dart +++ b/lib/src/view/play/challenge_list_item.dart @@ -10,7 +10,6 @@ import 'package:lichess_mobile/src/model/lobby/correspondence_challenge.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; -import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/user_full_name.dart'; @@ -31,7 +30,7 @@ class ChallengeListItem extends ConsumerWidget { final VoidCallback? onPressed; final VoidCallback? onAccept; final VoidCallback? onCancel; - final void Function(ChallengeDeclineReason reason)? onDecline; + final void Function(ChallengeDeclineReason? reason)? onDecline; @override Widget build(BuildContext context, WidgetRef ref) { @@ -46,7 +45,7 @@ class ChallengeListItem extends ConsumerWidget { color: color, child: Slidable( endActionPane: ActionPane( - motion: const ScrollMotion(), + motion: const StretchMotion(), extentRatio: 0.6, children: [ if (onAccept != null) @@ -61,8 +60,11 @@ class ChallengeListItem extends ConsumerWidget { if (onDecline != null || (isMyChallenge && onCancel != null)) SlidableAction( icon: Icons.close, - onPressed: - isMyChallenge ? (_) => onCancel!() : _showDeclineReasons, + onPressed: isMyChallenge + ? (_) => onCancel!() + : onDecline != null + ? (_) => onDecline!(null) + : null, spacing: 8.0, backgroundColor: context.lichessColors.error, foregroundColor: Colors.white, @@ -93,22 +95,6 @@ class ChallengeListItem extends ConsumerWidget { ), ); } - - void _showDeclineReasons(BuildContext context) { - showAdaptiveActionSheet( - context: context, - actions: ChallengeDeclineReason.values - .map( - (reason) => BottomSheetAction( - makeLabel: (context) => Text(reason.label(context.l10n)), - onPressed: (_) { - onDecline?.call(reason); - }, - ), - ) - .toList(), - ); - } } class CorrespondenceChallengeListItem extends StatelessWidget { diff --git a/lib/src/view/user/challenge_requests_screen.dart b/lib/src/view/user/challenge_requests_screen.dart index 80afbae3e0..e740ef1cc6 100644 --- a/lib/src/view/user/challenge_requests_screen.dart +++ b/lib/src/view/user/challenge_requests_screen.dart @@ -5,7 +5,7 @@ import 'package:lichess_mobile/src/model/challenge/challenge.dart'; import 'package:lichess_mobile/src/model/challenge/challenge_repository.dart'; import 'package:lichess_mobile/src/model/challenge/challenges.dart'; import 'package:lichess_mobile/src/model/notifications/local_notification_service.dart'; -import 'package:lichess_mobile/src/navigation.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/game/game_screen.dart'; @@ -39,78 +39,109 @@ class _Body extends ConsumerWidget { data: (challenges) { final list = challenges.inward.addAll(challenges.outward); - return list.isEmpty - ? Center(child: Text(context.l10n.nothingToSeeHere)) - : ListView.separated( - itemCount: list.length, - separatorBuilder: (context, index) => - const PlatformDivider(height: 1, cupertinoHasLeading: true), - itemBuilder: (context, index) { - final challenge = list[index]; + if (list.isEmpty) { + return Center(child: Text(context.l10n.nothingToSeeHere)); + } - // not sure why there wouldn't be a challenger - final user = challenge.challenger?.user; - if (user == null) return null; + return ListView.separated( + itemCount: list.length, + separatorBuilder: (context, index) => + const PlatformDivider(height: 1, cupertinoHasLeading: true), + itemBuilder: (context, index) { + final challenge = list[index]; + final user = challenge.challenger?.user; - return ChallengeListItem( - challenge: challenge, - user: user, - onAccept: challenge.direction == - ChallengeDirection.outward || - !challenge.variant.isPlaySupported - ? null - : session == null - ? () { - showPlatformSnackbar( - context, - context.l10n.youNeedAnAccountToDoThat, - ); - } - : () { - showConfirmDialog( - context, - title: Text(context.l10n.accept), - isDestructiveAction: true, - onConfirm: (_) async { - final challengeRepo = - ref.read(challengeRepositoryProvider); - await challengeRepo.accept(challenge.id); - final fullId = await challengeRepo - .show(challenge.id) - .then( - (challenge) => challenge.gameFullId, - ); - pushPlatformRoute( - ref - .read(currentNavigatorKeyProvider) - .currentContext!, - rootNavigator: true, - builder: (BuildContext context) { - return GameScreen( - initialGameId: fullId, - ); - }, - ); - }, - ); - }, - onCancel: challenge.direction == ChallengeDirection.outward - ? () => ref - .read(challengeRepositoryProvider) - .cancel(challenge.id) - : null, - onDecline: challenge.direction == ChallengeDirection.inward - ? (ChallengeDeclineReason reason) { - ref - .read(challengeRepositoryProvider) - .decline(challenge.id, reason: reason); - LocalNotificationService.instance - .cancel(challenge.id.value.hashCode); - } - : null, + if (user == null) return null; + + Future acceptChallenge(BuildContext context) async { + final challengeRepo = ref.read(challengeRepositoryProvider); + await challengeRepo.accept(challenge.id); + final fullId = await challengeRepo.show(challenge.id).then( + (challenge) => challenge.gameFullId, + ); + if (!context.mounted) return; + pushPlatformRoute( + context, + rootNavigator: true, + builder: (BuildContext context) { + return GameScreen( + initialGameId: fullId, ); }, ); + } + + Future declineChallenge( + ChallengeDeclineReason? reason, + ) async { + ref + .read(challengeRepositoryProvider) + .decline(challenge.id, reason: reason); + LocalNotificationService.instance + .cancel(challenge.id.value.hashCode); + } + + void confirmDialog() { + showAdaptiveActionSheet( + context: context, + title: challenge.variant.isPlaySupported + ? const Text('Do you accept the challenge?') + : null, + actions: [ + if (challenge.variant.isPlaySupported) + BottomSheetAction( + makeLabel: (context) => Text(context.l10n.accept), + leading: + Icon(Icons.check, color: context.lichessColors.good), + isDefaultAction: true, + onPressed: (context) => acceptChallenge(context), + ), + ...ChallengeDeclineReason.values.map( + (reason) => BottomSheetAction( + makeLabel: (context) => Text(reason.label(context.l10n)), + leading: + Icon(Icons.close, color: context.lichessColors.error), + isDestructiveAction: true, + onPressed: (_) { + declineChallenge(reason); + }, + ), + ), + ], + ); + } + + void showMissingAccountMessage() { + showPlatformSnackbar( + context, + context.l10n.youNeedAnAccountToDoThat, + ); + } + + return ChallengeListItem( + challenge: challenge, + user: user, + onPressed: challenge.direction == ChallengeDirection.inward + ? session == null + ? showMissingAccountMessage + : confirmDialog + : null, + onAccept: challenge.direction == ChallengeDirection.outward || + !challenge.variant.isPlaySupported + ? null + : session == null + ? showMissingAccountMessage + : () => acceptChallenge(context), + onCancel: challenge.direction == ChallengeDirection.outward + ? () => + ref.read(challengeRepositoryProvider).cancel(challenge.id) + : null, + onDecline: challenge.direction == ChallengeDirection.inward + ? declineChallenge + : null, + ); + }, + ); }, loading: () { return const Center(child: CircularProgressIndicator.adaptive()); diff --git a/lib/src/widgets/adaptive_action_sheet.dart b/lib/src/widgets/adaptive_action_sheet.dart index 922dc41542..50ad66219b 100644 --- a/lib/src/widgets/adaptive_action_sheet.dart +++ b/lib/src/widgets/adaptive_action_sheet.dart @@ -116,6 +116,7 @@ Future showCupertinoActionSheet({ action.onPressed(context); }, isDestructiveAction: action.isDestructiveAction, + isDefaultAction: action.isDefaultAction, child: action.makeLabel(context), ); }, @@ -140,8 +141,8 @@ Future showMaterialActionSheet({ required List actions, bool isDismissible = true, }) { - final defaultTextStyle = - Theme.of(context).textTheme.titleMedium ?? const TextStyle(fontSize: 20); + final actionTextStyle = + Theme.of(context).textTheme.titleMedium ?? const TextStyle(fontSize: 18); final screenWidth = MediaQuery.of(context).size.width; return showDialog( @@ -188,7 +189,7 @@ Future showMaterialActionSheet({ ], Expanded( child: DefaultTextStyle( - style: defaultTextStyle, + style: actionTextStyle, textAlign: action.leading != null ? TextAlign.start : TextAlign.center, @@ -233,16 +234,20 @@ class BottomSheetAction { /// A widget to display after the label. /// - /// Typically an [Icon] widget. Only for android. + /// Typically an [Icon] widget. (Android only). final Widget? trailing; /// A widget to display before the label. /// - /// Typically an [Icon] or a [CircleAvatar] widget. Only for android. + /// Typically an [Icon] or a [CircleAvatar] widget. (Android only). final Widget? leading; + /// Whether the action is destructive. (iOS only). final bool isDestructiveAction; + /// Whether the action is the default action. (iOS only). + final bool isDefaultAction; + BottomSheetAction({ required this.makeLabel, required this.onPressed, @@ -250,5 +255,6 @@ class BottomSheetAction { this.trailing, this.leading, this.isDestructiveAction = false, + this.isDefaultAction = false, }); } From fd1a4f23d43692c10c0e19716bce812fd8a569ec Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 19 Sep 2024 15:34:09 +0200 Subject: [PATCH 372/979] Refactoring --- lib/src/model/challenge/challenge.dart | 17 +- lib/src/model/puzzle/puzzle_theme.dart | 626 +++++++++--------- .../view/play/create_challenge_screen.dart | 4 +- lib/src/view/puzzle/dashboard_screen.dart | 3 +- lib/src/view/puzzle/puzzle_screen.dart | 2 +- lib/src/view/puzzle/puzzle_themes_screen.dart | 4 +- 6 files changed, 325 insertions(+), 331 deletions(-) diff --git a/lib/src/model/challenge/challenge.dart b/lib/src/model/challenge/challenge.dart index fbaef3394f..06b10fe7e8 100644 --- a/lib/src/model/challenge/challenge.dart +++ b/lib/src/model/challenge/challenge.dart @@ -204,18 +204,13 @@ typedef ChallengeUser = ({ enum SideChoice { random, white, - black, -} + black; -String sideChoiceL10n(AppLocalizations l10n, SideChoice side) { - switch (side) { - case SideChoice.white: - return l10n.white; - case SideChoice.black: - return l10n.black; - case SideChoice.random: - return l10n.randomColor; - } + String label(AppLocalizations l10n) => switch (this) { + SideChoice.random => l10n.randomColor, + SideChoice.white => l10n.white, + SideChoice.black => l10n.black, + }; } extension ChallengeExtension on Pick { diff --git a/lib/src/model/puzzle/puzzle_theme.dart b/lib/src/model/puzzle/puzzle_theme.dart index fbba9af65f..fcad5c973a 100644 --- a/lib/src/model/puzzle/puzzle_theme.dart +++ b/lib/src/model/puzzle/puzzle_theme.dart @@ -1,9 +1,9 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/widgets.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:lichess_mobile/l10n/l10n.dart'; import 'package:lichess_mobile/src/styles/puzzle_icons.dart'; import 'package:lichess_mobile/src/utils/l10n.dart'; -import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'puzzle_theme.freezed.dart'; @@ -84,7 +84,318 @@ enum PuzzleThemeKey { // checkFirst, // used internally to filter out unsupported keys - unsupported, + unsupported; + + PuzzleThemeL10n l10n(AppLocalizations l10n) { + switch (this) { + case PuzzleThemeKey.mix: + case PuzzleThemeKey.unsupported: + return PuzzleThemeL10n( + name: l10n.puzzleThemeHealthyMix, + description: l10n.puzzleThemeHealthyMixDescription, + ); + case PuzzleThemeKey.advancedPawn: + return PuzzleThemeL10n( + name: l10n.puzzleThemeAdvancedPawn, + description: l10n.puzzleThemeAdvancedPawnDescription, + ); + case PuzzleThemeKey.advantage: + return PuzzleThemeL10n( + name: l10n.puzzleThemeAdvantage, + description: l10n.puzzleThemeAdvantageDescription, + ); + case PuzzleThemeKey.anastasiaMate: + return PuzzleThemeL10n( + name: l10n.puzzleThemeAnastasiaMate, + description: l10n.puzzleThemeAnastasiaMateDescription, + ); + case PuzzleThemeKey.arabianMate: + return PuzzleThemeL10n( + name: l10n.puzzleThemeArabianMate, + description: l10n.puzzleThemeArabianMateDescription, + ); + case PuzzleThemeKey.attackingF2F7: + return PuzzleThemeL10n( + name: l10n.puzzleThemeAttackingF2F7, + description: l10n.puzzleThemeAttackingF2F7Description, + ); + case PuzzleThemeKey.attraction: + return PuzzleThemeL10n( + name: l10n.puzzleThemeAttraction, + description: l10n.puzzleThemeAttractionDescription, + ); + case PuzzleThemeKey.backRankMate: + return PuzzleThemeL10n( + name: l10n.puzzleThemeBackRankMate, + description: l10n.puzzleThemeBackRankMateDescription, + ); + case PuzzleThemeKey.bishopEndgame: + return PuzzleThemeL10n( + name: l10n.puzzleThemeBishopEndgame, + description: l10n.puzzleThemeBishopEndgameDescription, + ); + case PuzzleThemeKey.bodenMate: + return PuzzleThemeL10n( + name: l10n.puzzleThemeBodenMate, + description: l10n.puzzleThemeBodenMateDescription, + ); + case PuzzleThemeKey.capturingDefender: + return PuzzleThemeL10n( + name: l10n.puzzleThemeCapturingDefender, + description: l10n.puzzleThemeCapturingDefenderDescription, + ); + case PuzzleThemeKey.castling: + return PuzzleThemeL10n( + name: l10n.puzzleThemeCastling, + description: l10n.puzzleThemeCastlingDescription, + ); + case PuzzleThemeKey.clearance: + return PuzzleThemeL10n( + name: l10n.puzzleThemeClearance, + description: l10n.puzzleThemeClearanceDescription, + ); + case PuzzleThemeKey.crushing: + return PuzzleThemeL10n( + name: l10n.puzzleThemeCrushing, + description: l10n.puzzleThemeCrushingDescription, + ); + case PuzzleThemeKey.defensiveMove: + return PuzzleThemeL10n( + name: l10n.puzzleThemeDefensiveMove, + description: l10n.puzzleThemeDefensiveMoveDescription, + ); + case PuzzleThemeKey.deflection: + return PuzzleThemeL10n( + name: l10n.puzzleThemeDeflection, + description: l10n.puzzleThemeDeflectionDescription, + ); + case PuzzleThemeKey.discoveredAttack: + return PuzzleThemeL10n( + name: l10n.puzzleThemeDiscoveredAttack, + description: l10n.puzzleThemeDiscoveredAttackDescription, + ); + case PuzzleThemeKey.doubleBishopMate: + return PuzzleThemeL10n( + name: l10n.puzzleThemeDoubleBishopMate, + description: l10n.puzzleThemeDoubleBishopMateDescription, + ); + case PuzzleThemeKey.doubleCheck: + return PuzzleThemeL10n( + name: l10n.puzzleThemeDoubleCheck, + description: l10n.puzzleThemeDoubleCheckDescription, + ); + case PuzzleThemeKey.dovetailMate: + return PuzzleThemeL10n( + name: l10n.puzzleThemeDovetailMate, + description: l10n.puzzleThemeDovetailMateDescription, + ); + case PuzzleThemeKey.equality: + return PuzzleThemeL10n( + name: l10n.puzzleThemeEquality, + description: l10n.puzzleThemeEqualityDescription, + ); + case PuzzleThemeKey.endgame: + return PuzzleThemeL10n( + name: l10n.puzzleThemeEndgame, + description: l10n.puzzleThemeEndgameDescription, + ); + case PuzzleThemeKey.enPassant: + return PuzzleThemeL10n( + name: 'En passant', + description: l10n.puzzleThemeEnPassantDescription, + ); + case PuzzleThemeKey.exposedKing: + return PuzzleThemeL10n( + name: l10n.puzzleThemeExposedKing, + description: l10n.puzzleThemeExposedKingDescription, + ); + case PuzzleThemeKey.fork: + return PuzzleThemeL10n( + name: l10n.puzzleThemeFork, + description: l10n.puzzleThemeForkDescription, + ); + case PuzzleThemeKey.hangingPiece: + return PuzzleThemeL10n( + name: l10n.puzzleThemeHangingPiece, + description: l10n.puzzleThemeHangingPieceDescription, + ); + case PuzzleThemeKey.hookMate: + return PuzzleThemeL10n( + name: l10n.puzzleThemeHookMate, + description: l10n.puzzleThemeHookMateDescription, + ); + case PuzzleThemeKey.interference: + return PuzzleThemeL10n( + name: l10n.puzzleThemeInterference, + description: l10n.puzzleThemeInterferenceDescription, + ); + case PuzzleThemeKey.intermezzo: + return PuzzleThemeL10n( + name: l10n.puzzleThemeIntermezzo, + description: l10n.puzzleThemeIntermezzoDescription, + ); + case PuzzleThemeKey.kingsideAttack: + return PuzzleThemeL10n( + name: l10n.puzzleThemeKingsideAttack, + description: l10n.puzzleThemeKingsideAttackDescription, + ); + case PuzzleThemeKey.knightEndgame: + return PuzzleThemeL10n( + name: l10n.puzzleThemeKnightEndgame, + description: l10n.puzzleThemeKnightEndgameDescription, + ); + case PuzzleThemeKey.long: + return PuzzleThemeL10n( + name: l10n.puzzleThemeLong, + description: l10n.puzzleThemeLongDescription, + ); + case PuzzleThemeKey.master: + return PuzzleThemeL10n( + name: l10n.puzzleThemeMaster, + description: l10n.puzzleThemeMasterDescription, + ); + case PuzzleThemeKey.masterVsMaster: + return PuzzleThemeL10n( + name: l10n.puzzleThemeMasterVsMaster, + description: l10n.puzzleThemeMasterVsMasterDescription, + ); + case PuzzleThemeKey.mate: + return PuzzleThemeL10n( + name: l10n.puzzleThemeMate, + description: l10n.puzzleThemeMateDescription, + ); + case PuzzleThemeKey.mateIn1: + return PuzzleThemeL10n( + name: l10n.puzzleThemeMateIn1, + description: l10n.puzzleThemeMateIn1Description, + ); + case PuzzleThemeKey.mateIn2: + return PuzzleThemeL10n( + name: l10n.puzzleThemeMateIn2, + description: l10n.puzzleThemeMateIn2Description, + ); + case PuzzleThemeKey.mateIn3: + return PuzzleThemeL10n( + name: l10n.puzzleThemeMateIn3, + description: l10n.puzzleThemeMateIn3Description, + ); + case PuzzleThemeKey.mateIn4: + return PuzzleThemeL10n( + name: l10n.puzzleThemeMateIn4, + description: l10n.puzzleThemeMateIn4Description, + ); + case PuzzleThemeKey.mateIn5: + return PuzzleThemeL10n( + name: l10n.puzzleThemeMateIn5, + description: l10n.puzzleThemeMateIn5Description, + ); + case PuzzleThemeKey.smotheredMate: + return PuzzleThemeL10n( + name: l10n.puzzleThemeSmotheredMate, + description: l10n.puzzleThemeSmotheredMateDescription, + ); + case PuzzleThemeKey.middlegame: + return PuzzleThemeL10n( + name: l10n.puzzleThemeMiddlegame, + description: l10n.puzzleThemeMiddlegameDescription, + ); + case PuzzleThemeKey.oneMove: + return PuzzleThemeL10n( + name: l10n.puzzleThemeOneMove, + description: l10n.puzzleThemeOneMoveDescription, + ); + case PuzzleThemeKey.opening: + return PuzzleThemeL10n( + name: l10n.puzzleThemeOpening, + description: l10n.puzzleThemeOpeningDescription, + ); + case PuzzleThemeKey.pawnEndgame: + return PuzzleThemeL10n( + name: l10n.puzzleThemePawnEndgame, + description: l10n.puzzleThemePawnEndgameDescription, + ); + case PuzzleThemeKey.pin: + return PuzzleThemeL10n( + name: l10n.puzzleThemePin, + description: l10n.puzzleThemePinDescription, + ); + case PuzzleThemeKey.promotion: + return PuzzleThemeL10n( + name: l10n.puzzleThemePromotion, + description: l10n.puzzleThemePromotionDescription, + ); + case PuzzleThemeKey.queenEndgame: + return PuzzleThemeL10n( + name: l10n.puzzleThemeQueenEndgame, + description: l10n.puzzleThemeQueenEndgameDescription, + ); + case PuzzleThemeKey.queenRookEndgame: + return PuzzleThemeL10n( + name: l10n.puzzleThemeQueenRookEndgame, + description: l10n.puzzleThemeQueenRookEndgameDescription, + ); + case PuzzleThemeKey.queensideAttack: + return PuzzleThemeL10n( + name: l10n.puzzleThemeQueensideAttack, + description: l10n.puzzleThemeQueensideAttackDescription, + ); + case PuzzleThemeKey.quietMove: + return PuzzleThemeL10n( + name: l10n.puzzleThemeQuietMove, + description: l10n.puzzleThemeQuietMoveDescription, + ); + case PuzzleThemeKey.rookEndgame: + return PuzzleThemeL10n( + name: l10n.puzzleThemeRookEndgame, + description: l10n.puzzleThemeRookEndgameDescription, + ); + case PuzzleThemeKey.sacrifice: + return PuzzleThemeL10n( + name: l10n.puzzleThemeSacrifice, + description: l10n.puzzleThemeSacrificeDescription, + ); + case PuzzleThemeKey.short: + return PuzzleThemeL10n( + name: l10n.puzzleThemeShort, + description: l10n.puzzleThemeShortDescription, + ); + case PuzzleThemeKey.skewer: + return PuzzleThemeL10n( + name: l10n.puzzleThemeSkewer, + description: l10n.puzzleThemeSkewerDescription, + ); + case PuzzleThemeKey.superGM: + return PuzzleThemeL10n( + name: l10n.puzzleThemeSuperGM, + description: l10n.puzzleThemeSuperGMDescription, + ); + case PuzzleThemeKey.trappedPiece: + return PuzzleThemeL10n( + name: l10n.puzzleThemeTrappedPiece, + description: l10n.puzzleThemeTrappedPieceDescription, + ); + case PuzzleThemeKey.underPromotion: + return PuzzleThemeL10n( + name: l10n.puzzleThemeUnderPromotion, + description: l10n.puzzleThemeUnderPromotionDescription, + ); + case PuzzleThemeKey.veryLong: + return PuzzleThemeL10n( + name: l10n.puzzleThemeVeryLong, + description: l10n.puzzleThemeVeryLongDescription, + ); + case PuzzleThemeKey.xRayAttack: + return PuzzleThemeL10n( + name: l10n.puzzleThemeXRayAttack, + description: l10n.puzzleThemeXRayAttackDescription, + ); + case PuzzleThemeKey.zugzwang: + return PuzzleThemeL10n( + name: l10n.puzzleThemeZugzwang, + description: l10n.puzzleThemeZugzwangDescription, + ); + } + } } final IMap puzzleThemeNameMap = @@ -215,317 +526,6 @@ class PuzzleThemeL10n { final String description; } -PuzzleThemeL10n puzzleThemeL10n(BuildContext context, PuzzleThemeKey theme) { - switch (theme) { - case PuzzleThemeKey.mix: - case PuzzleThemeKey.unsupported: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeHealthyMix, - description: context.l10n.puzzleThemeHealthyMixDescription, - ); - case PuzzleThemeKey.advancedPawn: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeAdvancedPawn, - description: context.l10n.puzzleThemeAdvancedPawnDescription, - ); - case PuzzleThemeKey.advantage: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeAdvantage, - description: context.l10n.puzzleThemeAdvantageDescription, - ); - case PuzzleThemeKey.anastasiaMate: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeAnastasiaMate, - description: context.l10n.puzzleThemeAnastasiaMateDescription, - ); - case PuzzleThemeKey.arabianMate: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeArabianMate, - description: context.l10n.puzzleThemeArabianMateDescription, - ); - case PuzzleThemeKey.attackingF2F7: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeAttackingF2F7, - description: context.l10n.puzzleThemeAttackingF2F7Description, - ); - case PuzzleThemeKey.attraction: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeAttraction, - description: context.l10n.puzzleThemeAttractionDescription, - ); - case PuzzleThemeKey.backRankMate: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeBackRankMate, - description: context.l10n.puzzleThemeBackRankMateDescription, - ); - case PuzzleThemeKey.bishopEndgame: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeBishopEndgame, - description: context.l10n.puzzleThemeBishopEndgameDescription, - ); - case PuzzleThemeKey.bodenMate: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeBodenMate, - description: context.l10n.puzzleThemeBodenMateDescription, - ); - case PuzzleThemeKey.capturingDefender: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeCapturingDefender, - description: context.l10n.puzzleThemeCapturingDefenderDescription, - ); - case PuzzleThemeKey.castling: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeCastling, - description: context.l10n.puzzleThemeCastlingDescription, - ); - case PuzzleThemeKey.clearance: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeClearance, - description: context.l10n.puzzleThemeClearanceDescription, - ); - case PuzzleThemeKey.crushing: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeCrushing, - description: context.l10n.puzzleThemeCrushingDescription, - ); - case PuzzleThemeKey.defensiveMove: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeDefensiveMove, - description: context.l10n.puzzleThemeDefensiveMoveDescription, - ); - case PuzzleThemeKey.deflection: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeDeflection, - description: context.l10n.puzzleThemeDeflectionDescription, - ); - case PuzzleThemeKey.discoveredAttack: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeDiscoveredAttack, - description: context.l10n.puzzleThemeDiscoveredAttackDescription, - ); - case PuzzleThemeKey.doubleBishopMate: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeDoubleBishopMate, - description: context.l10n.puzzleThemeDoubleBishopMateDescription, - ); - case PuzzleThemeKey.doubleCheck: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeDoubleCheck, - description: context.l10n.puzzleThemeDoubleCheckDescription, - ); - case PuzzleThemeKey.dovetailMate: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeDovetailMate, - description: context.l10n.puzzleThemeDovetailMateDescription, - ); - case PuzzleThemeKey.equality: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeEquality, - description: context.l10n.puzzleThemeEqualityDescription, - ); - case PuzzleThemeKey.endgame: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeEndgame, - description: context.l10n.puzzleThemeEndgameDescription, - ); - case PuzzleThemeKey.enPassant: - return PuzzleThemeL10n( - name: 'En passant', - description: context.l10n.puzzleThemeEnPassantDescription, - ); - case PuzzleThemeKey.exposedKing: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeExposedKing, - description: context.l10n.puzzleThemeExposedKingDescription, - ); - case PuzzleThemeKey.fork: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeFork, - description: context.l10n.puzzleThemeForkDescription, - ); - case PuzzleThemeKey.hangingPiece: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeHangingPiece, - description: context.l10n.puzzleThemeHangingPieceDescription, - ); - case PuzzleThemeKey.hookMate: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeHookMate, - description: context.l10n.puzzleThemeHookMateDescription, - ); - case PuzzleThemeKey.interference: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeInterference, - description: context.l10n.puzzleThemeInterferenceDescription, - ); - case PuzzleThemeKey.intermezzo: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeIntermezzo, - description: context.l10n.puzzleThemeIntermezzoDescription, - ); - case PuzzleThemeKey.kingsideAttack: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeKingsideAttack, - description: context.l10n.puzzleThemeKingsideAttackDescription, - ); - case PuzzleThemeKey.knightEndgame: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeKnightEndgame, - description: context.l10n.puzzleThemeKnightEndgameDescription, - ); - case PuzzleThemeKey.long: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeLong, - description: context.l10n.puzzleThemeLongDescription, - ); - case PuzzleThemeKey.master: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeMaster, - description: context.l10n.puzzleThemeMasterDescription, - ); - case PuzzleThemeKey.masterVsMaster: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeMasterVsMaster, - description: context.l10n.puzzleThemeMasterVsMasterDescription, - ); - case PuzzleThemeKey.mate: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeMate, - description: context.l10n.puzzleThemeMateDescription, - ); - case PuzzleThemeKey.mateIn1: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeMateIn1, - description: context.l10n.puzzleThemeMateIn1Description, - ); - case PuzzleThemeKey.mateIn2: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeMateIn2, - description: context.l10n.puzzleThemeMateIn2Description, - ); - case PuzzleThemeKey.mateIn3: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeMateIn3, - description: context.l10n.puzzleThemeMateIn3Description, - ); - case PuzzleThemeKey.mateIn4: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeMateIn4, - description: context.l10n.puzzleThemeMateIn4Description, - ); - case PuzzleThemeKey.mateIn5: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeMateIn5, - description: context.l10n.puzzleThemeMateIn5Description, - ); - case PuzzleThemeKey.smotheredMate: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeSmotheredMate, - description: context.l10n.puzzleThemeSmotheredMateDescription, - ); - case PuzzleThemeKey.middlegame: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeMiddlegame, - description: context.l10n.puzzleThemeMiddlegameDescription, - ); - case PuzzleThemeKey.oneMove: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeOneMove, - description: context.l10n.puzzleThemeOneMoveDescription, - ); - case PuzzleThemeKey.opening: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeOpening, - description: context.l10n.puzzleThemeOpeningDescription, - ); - case PuzzleThemeKey.pawnEndgame: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemePawnEndgame, - description: context.l10n.puzzleThemePawnEndgameDescription, - ); - case PuzzleThemeKey.pin: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemePin, - description: context.l10n.puzzleThemePinDescription, - ); - case PuzzleThemeKey.promotion: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemePromotion, - description: context.l10n.puzzleThemePromotionDescription, - ); - case PuzzleThemeKey.queenEndgame: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeQueenEndgame, - description: context.l10n.puzzleThemeQueenEndgameDescription, - ); - case PuzzleThemeKey.queenRookEndgame: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeQueenRookEndgame, - description: context.l10n.puzzleThemeQueenRookEndgameDescription, - ); - case PuzzleThemeKey.queensideAttack: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeQueensideAttack, - description: context.l10n.puzzleThemeQueensideAttackDescription, - ); - case PuzzleThemeKey.quietMove: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeQuietMove, - description: context.l10n.puzzleThemeQuietMoveDescription, - ); - case PuzzleThemeKey.rookEndgame: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeRookEndgame, - description: context.l10n.puzzleThemeRookEndgameDescription, - ); - case PuzzleThemeKey.sacrifice: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeSacrifice, - description: context.l10n.puzzleThemeSacrificeDescription, - ); - case PuzzleThemeKey.short: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeShort, - description: context.l10n.puzzleThemeShortDescription, - ); - case PuzzleThemeKey.skewer: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeSkewer, - description: context.l10n.puzzleThemeSkewerDescription, - ); - case PuzzleThemeKey.superGM: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeSuperGM, - description: context.l10n.puzzleThemeSuperGMDescription, - ); - case PuzzleThemeKey.trappedPiece: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeTrappedPiece, - description: context.l10n.puzzleThemeTrappedPieceDescription, - ); - case PuzzleThemeKey.underPromotion: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeUnderPromotion, - description: context.l10n.puzzleThemeUnderPromotionDescription, - ); - case PuzzleThemeKey.veryLong: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeVeryLong, - description: context.l10n.puzzleThemeVeryLongDescription, - ); - case PuzzleThemeKey.xRayAttack: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeXRayAttack, - description: context.l10n.puzzleThemeXRayAttackDescription, - ); - case PuzzleThemeKey.zugzwang: - return PuzzleThemeL10n( - name: context.l10n.puzzleThemeZugzwang, - description: context.l10n.puzzleThemeZugzwangDescription, - ); - } -} - IconData puzzleThemeIcon(PuzzleThemeKey theme) { switch (theme) { case PuzzleThemeKey.mix: diff --git a/lib/src/view/play/create_challenge_screen.dart b/lib/src/view/play/create_challenge_screen.dart index ea7da5072a..d21caaf4f6 100644 --- a/lib/src/view/play/create_challenge_screen.dart +++ b/lib/src/view/play/create_challenge_screen.dart @@ -338,7 +338,7 @@ class _ChallengeBodyState extends ConsumerState<_ChallengeBody> { choices: SideChoice.values, selectedItem: preferences.sideChoice, labelBuilder: (SideChoice side) => - Text(sideChoiceL10n(context.l10n, side)), + Text(side.label(context.l10n)), onSelectedItemChanged: (SideChoice side) { ref .read(challengePreferencesProvider.notifier) @@ -347,7 +347,7 @@ class _ChallengeBodyState extends ConsumerState<_ChallengeBody> { ); }, child: Text( - sideChoiceL10n(context.l10n, preferences.sideChoice), + preferences.sideChoice.label(context.l10n), ), ), ), diff --git a/lib/src/view/puzzle/dashboard_screen.dart b/lib/src/view/puzzle/dashboard_screen.dart index c890efebf5..705c13a762 100644 --- a/lib/src/view/puzzle/dashboard_screen.dart +++ b/lib/src/view/puzzle/dashboard_screen.dart @@ -6,7 +6,6 @@ import 'package:http/http.dart' show ClientException; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_providers.dart'; -import 'package:lichess_mobile/src/model/puzzle/puzzle_theme.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; @@ -208,7 +207,7 @@ class PuzzleChart extends StatelessWidget { ), ], getTitle: (index, angle) => RadarChartTitle( - text: puzzleThemeL10n(context, puzzleData[index].theme).name, + text: puzzleData[index].theme.l10n(context.l10n).name, ), titleTextStyle: const TextStyle(fontSize: 10), titlePositionPercentageOffset: 0.09, diff --git a/lib/src/view/puzzle/puzzle_screen.dart b/lib/src/view/puzzle/puzzle_screen.dart index 5173bb08d5..0c8eb4eb58 100644 --- a/lib/src/view/puzzle/puzzle_screen.dart +++ b/lib/src/view/puzzle/puzzle_screen.dart @@ -117,7 +117,7 @@ class _Title extends ConsumerWidget { return switch (angle) { PuzzleTheme(themeKey: final key) => key == PuzzleThemeKey.mix ? Text(context.l10n.puzzleDesc) - : Text(puzzleThemeL10n(context, key).name), + : Text(key.l10n(context.l10n).name), PuzzleOpening(key: final key) => ref .watch( puzzleOpeningNameProvider(key), diff --git a/lib/src/view/puzzle/puzzle_themes_screen.dart b/lib/src/view/puzzle/puzzle_themes_screen.dart index 969f27eff5..74436183de 100644 --- a/lib/src/view/puzzle/puzzle_themes_screen.dart +++ b/lib/src/view/puzzle/puzzle_themes_screen.dart @@ -184,7 +184,7 @@ class _Category extends ConsumerWidget { ? const EdgeInsets.only(top: 6.0) : EdgeInsets.zero, child: Text( - puzzleThemeL10n(context, theme).name, + theme.l10n(context.l10n).name, style: Theme.of(context).platform == TargetPlatform.iOS ? TextStyle( color: CupertinoTheme.of(context) @@ -198,7 +198,7 @@ class _Category extends ConsumerWidget { subtitle: Padding( padding: const EdgeInsets.only(bottom: 6.0), child: Text( - puzzleThemeL10n(context, theme).description, + theme.l10n(context.l10n).description, maxLines: 10, overflow: TextOverflow.ellipsis, style: TextStyle( From 75b38384176620b44190b9bcda31dd375d351447 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 19 Sep 2024 15:46:44 +0200 Subject: [PATCH 373/979] Add proguard rules for Gson --- android/app/proguard-rules.pro | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro index 00a7e574f6..8cfc52e999 100644 --- a/android/app/proguard-rules.pro +++ b/android/app/proguard-rules.pro @@ -2,3 +2,36 @@ -keep class java.net.URL { *; } -keep class java.util.concurrent.Executors { *; } -keep class org.chromium.net.** { *; } + +##---------------Begin: proguard configuration for Gson ---------- +# Gson uses generic type information stored in a class file when working with fields. Proguard +# removes such information by default, so configure it to keep all of it. +-keepattributes Signature + +# For using GSON @Expose annotation +-keepattributes *Annotation* + +# Gson specific classes +-dontwarn sun.misc.** +#-keep class com.google.gson.stream.** { *; } + +# Application classes that will be serialized/deserialized over Gson +-keep class com.google.gson.examples.android.model.** { ; } + +# Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory, +# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter) +-keep class * extends com.google.gson.TypeAdapter +-keep class * implements com.google.gson.TypeAdapterFactory +-keep class * implements com.google.gson.JsonSerializer +-keep class * implements com.google.gson.JsonDeserializer + +# Prevent R8 from leaving Data object members always null +-keepclassmembers,allowobfuscation class * { + @com.google.gson.annotations.SerializedName ; +} + +# Retain generic signatures of TypeToken and its subclasses with R8 version 3.0 and higher. +-keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken +-keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken + +##---------------End: proguard configuration for Gson ---------- From 461e119f869b1ec222035bd82ddf741dcfdd57e2 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Thu, 19 Sep 2024 23:35:49 +0200 Subject: [PATCH 374/979] some styles changes, show score at the end of game and more consistency with the website --- .../coordinate_training_controller.dart | 11 +- .../coordinate_training_screen.dart | 202 +++++++++++------- 2 files changed, 134 insertions(+), 79 deletions(-) diff --git a/lib/src/model/coordinate_training/coordinate_training_controller.dart b/lib/src/model/coordinate_training/coordinate_training_controller.dart index 5c33e7d829..9441f428d5 100644 --- a/lib/src/model/coordinate_training/coordinate_training_controller.dart +++ b/lib/src/model/coordinate_training/coordinate_training_controller.dart @@ -54,7 +54,11 @@ class CoordinateTrainingController extends _$CoordinateTrainingController { void _finishTraining() { // TODO save score in local storage here (and display high score and/or average score in UI) - stopTraining(); + _updateTimer?.cancel(); + state = CoordinateTrainingState( + lastGuess: state.lastGuess, + lastScore: state.score, + ); } void stopTraining() { @@ -62,6 +66,10 @@ class CoordinateTrainingController extends _$CoordinateTrainingController { state = const CoordinateTrainingState(); } + void newTraining() { + state = const CoordinateTrainingState(); + } + /// Generate a random square that is not the same as the [previous] square Square _randomCoord({Square? previous}) { while (true) { @@ -100,6 +108,7 @@ class CoordinateTrainingState with _$CoordinateTrainingState { @Default(null) Duration? timeLimit, @Default(null) Duration? elapsed, @Default(null) Guess? lastGuess, + @Default(null) int? lastScore, }) = _CoordinateTrainingState; bool get trainingActive => elapsed != null; diff --git a/lib/src/view/coordinate_training/coordinate_training_screen.dart b/lib/src/view/coordinate_training/coordinate_training_screen.dart index 125f7a0919..845590f4b2 100644 --- a/lib/src/view/coordinate_training/coordinate_training_screen.dart +++ b/lib/src/view/coordinate_training/coordinate_training_screen.dart @@ -47,17 +47,31 @@ class _Body extends ConsumerStatefulWidget { class _BodyState extends ConsumerState<_Body> { late Side orientation; + late bool computeRandomOrientation; + Square? highlightLastGuess; Timer? highlightTimer; + Side _randomSide() => Side.values[Random().nextInt(Side.values.length)]; + void _setOrientation(SideChoice choice) { setState(() { orientation = switch (choice) { SideChoice.white => Side.white, SideChoice.black => Side.black, - SideChoice.random => Side.values[Random().nextInt(Side.values.length)], + SideChoice.random => _randomSide(), }; + computeRandomOrientation = false; + }); + } + + void _maybeSetOrientation() { + setState(() { + if (computeRandomOrientation) { + orientation = _randomSide(); + } + computeRandomOrientation = true; }); } @@ -148,37 +162,32 @@ class _BodyState extends ConsumerState<_Body> { ], ), if (trainingState.trainingActive) - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - _Score( - score: trainingState.score, - size: boardSize / 8, - color: trainingState.lastGuess == Guess.incorrect - ? context.lichessColors.error - : context.lichessColors.good, - ), - FatButton( - semanticsLabel: 'Abort Training', - onPressed: ref - .read( - coordinateTrainingControllerProvider - .notifier, - ) - .stopTraining, - child: const Text( - 'Abort Training', - style: Styles.bold, - ), - ), - ], - ), + _ScoreAndTrainingButton( + scoreSize: boardSize / 8, + score: trainingState.score, + onPressed: ref + .read( + coordinateTrainingControllerProvider.notifier, + ) + .stopTraining, + label: 'Abort Training', + ) + else if (trainingState.lastScore != null) + _ScoreAndTrainingButton( + scoreSize: boardSize / 8, + score: trainingState.lastScore!, + onPressed: ref + .read( + coordinateTrainingControllerProvider.notifier, + ) + .newTraining, + label: 'New Training', ) else Expanded( child: _Settings( onSideChoiceSelected: _setOrientation, + maybeSetOrientation: _maybeSetOrientation, ), ), ], @@ -241,7 +250,7 @@ class _TimeBar extends StatelessWidget { @override Widget build(BuildContext context) { return Align( - alignment: Alignment.center, + alignment: Alignment.centerLeft, child: SizedBox( width: maxWidth * (timeFractionElapsed ?? 0.0), height: 15.0, @@ -290,6 +299,48 @@ class _CoordinateTrainingMenu extends ConsumerWidget { } } +class _ScoreAndTrainingButton extends ConsumerWidget { + const _ScoreAndTrainingButton({ + required this.scoreSize, + required this.score, + required this.onPressed, + required this.label, + }); + + final double scoreSize; + final int score; + final VoidCallback onPressed; + final String label; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final trainingState = ref.watch(coordinateTrainingControllerProvider); + + return Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _Score( + score: score, + size: scoreSize, + color: trainingState.lastGuess == Guess.incorrect + ? context.lichessColors.error + : context.lichessColors.good, + ), + FatButton( + semanticsLabel: label, + onPressed: onPressed, + child: Text( + label, + style: Styles.bold, + ), + ), + ], + ), + ); + } +} + class _Score extends StatelessWidget { const _Score({ required this.size, @@ -337,9 +388,11 @@ class _Score extends StatelessWidget { class _Settings extends ConsumerStatefulWidget { const _Settings({ required this.onSideChoiceSelected, + required this.maybeSetOrientation, }); final void Function(SideChoice) onSideChoiceSelected; + final VoidCallback maybeSetOrientation; @override ConsumerState<_Settings> createState() => _SettingsState(); @@ -351,61 +404,54 @@ class _SettingsState extends ConsumerState<_Settings> { final trainingPrefs = ref.watch(coordinateTrainingPreferencesProvider); return Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - PlatformListTile( - title: Text(context.l10n.side), - trailing: Padding( - padding: Styles.horizontalBodyPadding, - child: Wrap( - spacing: 8.0, - children: SideChoice.values.map((choice) { - return ChoiceChip( - label: Text(sideChoiceL10n(context, choice)), - selected: trainingPrefs.sideChoice == choice, - showCheckmark: false, - onSelected: (selected) { - widget.onSideChoiceSelected(choice); - ref - .read(coordinateTrainingPreferencesProvider.notifier) - .setSideChoice(choice); - }, - ); - }).toList(), - ), - ), + Wrap( + spacing: 8.0, + children: SideChoice.values.map((choice) { + return ChoiceChip( + label: Text(sideChoiceL10n(context, choice)), + selected: trainingPrefs.sideChoice == choice, + showCheckmark: false, + onSelected: (selected) { + widget.onSideChoiceSelected(choice); + ref + .read(coordinateTrainingPreferencesProvider.notifier) + .setSideChoice(choice); + }, + ); + }).toList(), ), - PlatformListTile( - title: Text(context.l10n.time), - trailing: Padding( - padding: Styles.horizontalBodyPadding, - child: Wrap( - spacing: 8.0, - children: TimeChoice.values.map((choice) { - return ChoiceChip( - label: timeChoiceL10n(context, choice), - selected: trainingPrefs.timeChoice == choice, - showCheckmark: false, - onSelected: (selected) { - if (selected) { - ref - .read( - coordinateTrainingPreferencesProvider.notifier, - ) - .setTimeChoice(choice); - } - }, - ); - }).toList(), - ), - ), + Wrap( + spacing: 8.0, + children: TimeChoice.values.map((choice) { + return ChoiceChip( + label: timeChoiceL10n(context, choice), + selected: trainingPrefs.timeChoice == choice, + showCheckmark: false, + onSelected: (selected) { + if (selected) { + ref + .read( + coordinateTrainingPreferencesProvider.notifier, + ) + .setTimeChoice(choice); + } + }, + ); + }).toList(), ), FatButton( semanticsLabel: 'Start Training', - onPressed: () => ref - .read(coordinateTrainingControllerProvider.notifier) - .startTraining(trainingPrefs.timeChoice.duration), + onPressed: () { + if (trainingPrefs.sideChoice == SideChoice.random) { + widget.maybeSetOrientation(); + } + ref + .read(coordinateTrainingControllerProvider.notifier) + .startTraining(trainingPrefs.timeChoice.duration); + }, child: const Text( // TODO l10n once script works 'Start Training', From 958c7d4913b55dc4996ca7177089487f56278a97 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Sat, 21 Sep 2024 19:56:22 +0200 Subject: [PATCH 375/979] fix null clock on broadcast analysis screen --- lib/src/view/broadcast/broadcast_analysis_screen.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/view/broadcast/broadcast_analysis_screen.dart b/lib/src/view/broadcast/broadcast_analysis_screen.dart index 2f2f8703d7..8beee8393d 100644 --- a/lib/src/view/broadcast/broadcast_analysis_screen.dart +++ b/lib/src/view/broadcast/broadcast_analysis_screen.dart @@ -582,7 +582,8 @@ class _PlayerWidget extends StatelessWidget { ), ), ), - if (clock != null) + if (((side == playingSide && playClock) && game.timeLeft != null) || + (!(side == playingSide && playClock) && clock != null)) Card( color: (side == playingSide) ? playClock From 7815bdbb710f067d314499eb06a77d921d9ac08b Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Sun, 22 Sep 2024 23:35:06 +0200 Subject: [PATCH 376/979] remove isUtc since local time is used --- lib/src/model/broadcast/broadcast_repository.dart | 6 +++--- lib/src/utils/json.dart | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/src/model/broadcast/broadcast_repository.dart b/lib/src/model/broadcast/broadcast_repository.dart index 72fdd1504c..84363dd769 100644 --- a/lib/src/model/broadcast/broadcast_repository.dart +++ b/lib/src/model/broadcast/broadcast_repository.dart @@ -96,8 +96,8 @@ BroadcastTournamentData _tournamentDataFromPick( players: pick('info', 'players').asStringOrNull(), dates: pick('dates').letOrNull( (pick) => ( - startsAt: pick(0).asDateTimeFromMillisecondsOrThrow().toLocal(), - endsAt: pick(1).asDateTimeFromMillisecondsOrNull()?.toLocal(), + startsAt: pick(0).asDateTimeFromMillisecondsOrThrow(), + endsAt: pick(1).asDateTimeFromMillisecondsOrNull(), ), ), ), @@ -136,7 +136,7 @@ BroadcastRound _roundFromPick(RequiredPick pick) { id: pick('id').asBroadcastRoundIdOrThrow(), name: pick('name').asStringOrThrow(), status: status, - startsAt: pick('startsAt').asDateTimeFromMillisecondsOrNull()?.toLocal(), + startsAt: pick('startsAt').asDateTimeFromMillisecondsOrNull(), ); } diff --git a/lib/src/utils/json.dart b/lib/src/utils/json.dart index 6066eaa7b0..b8c013b980 100644 --- a/lib/src/utils/json.dart +++ b/lib/src/utils/json.dart @@ -23,7 +23,7 @@ extension TimeExtension on Pick { return value; } if (value is int) { - return DateTime.fromMillisecondsSinceEpoch(value, isUtc: true); + return DateTime.fromMillisecondsSinceEpoch(value); } throw PickException( "value $value at $debugParsingExit can't be casted to DateTime", From c3e36c062071ff6ca2a2be638a89e05d9d4d38bd Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Mon, 23 Sep 2024 00:04:58 +0200 Subject: [PATCH 377/979] fix test --- test/model/game/game_socket_events_test.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/model/game/game_socket_events_test.dart b/test/model/game/game_socket_events_test.dart index 12d4ee32a4..07d3073ae9 100644 --- a/test/model/game/game_socket_events_test.dart +++ b/test/model/game/game_socket_events_test.dart @@ -25,8 +25,7 @@ void main() { expect( game.meta, GameMeta( - createdAt: - DateTime.fromMillisecondsSinceEpoch(1685698678928, isUtc: true), + createdAt: DateTime.fromMillisecondsSinceEpoch(1685698678928), rated: false, variant: Variant.standard, speed: Speed.classical, From 039dba94b40768c879e830d77cc746272bbdc2ea Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 23 Sep 2024 12:02:38 +0200 Subject: [PATCH 378/979] Tools tab style tweaks --- lib/src/view/tools/tools_tab_screen.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/src/view/tools/tools_tab_screen.dart b/lib/src/view/tools/tools_tab_screen.dart index 374ec18b02..f469a5ed50 100644 --- a/lib/src/view/tools/tools_tab_screen.dart +++ b/lib/src/view/tools/tools_tab_screen.dart @@ -126,7 +126,8 @@ class _Body extends ConsumerWidget { ref.watch(connectivityChangesProvider).valueOrNull?.isOnline ?? false; final content = [ - const SizedBox(height: 16.0), + if (Theme.of(context).platform == TargetPlatform.android) + const SizedBox(height: 16.0), ListSection( hasLeading: true, children: [ @@ -156,7 +157,7 @@ class _Body extends ConsumerWidget { ), ), _ToolsButton( - icon: Icons.explore, + icon: Icons.explore_outlined, title: context.l10n.openingExplorer, onTap: isOnline ? () => pushPlatformRoute( @@ -175,7 +176,7 @@ class _Body extends ConsumerWidget { : null, ), _ToolsButton( - icon: Icons.edit, + icon: Icons.edit_outlined, title: context.l10n.boardEditor, onTap: () => pushPlatformRoute( context, From 707131378a106cb9cd33292bb4c90b5f4c01cf1e Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 23 Sep 2024 15:23:45 +0200 Subject: [PATCH 379/979] Use outlined icons in settings screen --- lib/src/utils/navigation.dart | 5 +-- .../view/settings/settings_tab_screen.dart | 39 ++++++++++--------- 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/lib/src/utils/navigation.dart b/lib/src/utils/navigation.dart index 46beeaa5d8..1edc004627 100644 --- a/lib/src/utils/navigation.dart +++ b/lib/src/utils/navigation.dart @@ -11,10 +11,7 @@ Future pushPlatformRoute( bool fullscreenDialog = false, String? title, }) { - return Navigator.of( - context, - rootNavigator: rootNavigator, - ).push( + return Navigator.of(context, rootNavigator: rootNavigator).push( Theme.of(context).platform == TargetPlatform.iOS ? CupertinoPageRoute( builder: builder, diff --git a/lib/src/view/settings/settings_tab_screen.dart b/lib/src/view/settings/settings_tab_screen.dart index 93d513b19b..dc12828b6d 100644 --- a/lib/src/view/settings/settings_tab_screen.dart +++ b/lib/src/view/settings/settings_tab_screen.dart @@ -128,7 +128,7 @@ class _Body extends ConsumerWidget { children: [ if (userSession != null) ...[ PlatformListTile( - leading: const Icon(Icons.person), + leading: const Icon(Icons.person_outline), title: Text(context.l10n.profile), trailing: Theme.of(context).platform == TargetPlatform.iOS ? const CupertinoListTileChevron() @@ -142,7 +142,7 @@ class _Body extends ConsumerWidget { }, ), PlatformListTile( - leading: const Icon(Icons.manage_accounts), + leading: const Icon(Icons.manage_accounts_outlined), title: Text(context.l10n.preferencesPreferences), trailing: Theme.of(context).platform == TargetPlatform.iOS ? const CupertinoListTileChevron() @@ -157,12 +157,12 @@ class _Body extends ConsumerWidget { ), if (authController.isLoading) const PlatformListTile( - leading: Icon(Icons.logout), + leading: Icon(Icons.logout_outlined), title: Center(child: ButtonLoadingIndicator()), ) else PlatformListTile( - leading: const Icon(Icons.logout), + leading: const Icon(Icons.logout_outlined), title: Text(context.l10n.logOut), onTap: () { _showSignOutConfirmDialog(context, ref); @@ -171,12 +171,12 @@ class _Body extends ConsumerWidget { ] else ...[ if (authController.isLoading) const PlatformListTile( - leading: Icon(Icons.login), + leading: Icon(Icons.login_outlined), title: Center(child: ButtonLoadingIndicator()), ) else PlatformListTile( - leading: const Icon(Icons.login), + leading: const Icon(Icons.login_outlined), title: Text(context.l10n.signIn), onTap: () { ref.read(authControllerProvider.notifier).signIn(); @@ -193,7 +193,7 @@ class _Body extends ConsumerWidget { showDivider: true, children: [ SettingsListTile( - icon: const Icon(Icons.music_note), + icon: const Icon(Icons.music_note_outlined), settingsLabel: Text(context.l10n.sound), settingsValue: '${soundThemeL10n(context, generalPrefs.soundTheme)} (${volumeLabel(generalPrefs.masterVolume)})', @@ -209,7 +209,7 @@ class _Body extends ConsumerWidget { androidVersionAsync.maybeWhen( data: (version) => version != null && version.sdkInt >= 31 ? SwitchSettingTile( - leading: const Icon(Icons.colorize), + leading: const Icon(Icons.colorize_outlined), title: Text(context.l10n.mobileSystemColors), value: generalPrefs.systemColors, onChanged: (value) { @@ -222,7 +222,7 @@ class _Body extends ConsumerWidget { orElse: () => const SizedBox.shrink(), ), SettingsListTile( - icon: const Icon(Icons.brightness_medium), + icon: const Icon(Icons.brightness_medium_outlined), settingsLabel: Text(context.l10n.background), settingsValue: AppBackgroundModeScreen.themeTitle( context, @@ -250,7 +250,7 @@ class _Body extends ConsumerWidget { }, ), SettingsListTile( - icon: const Icon(Icons.palette), + icon: const Icon(Icons.palette_outlined), settingsLabel: const Text('Theme'), settingsValue: '${boardPrefs.boardTheme.label} / ${boardPrefs.pieceSet.label}', @@ -277,7 +277,7 @@ class _Body extends ConsumerWidget { }, ), SettingsListTile( - icon: const Icon(Icons.language), + icon: const Icon(Icons.language_outlined), settingsLabel: Text(context.l10n.language), settingsValue: localeToLocalizedName( generalPrefs.locale ?? Localizations.localeOf(context), @@ -306,7 +306,7 @@ class _Body extends ConsumerWidget { showDivider: true, children: [ PlatformListTile( - leading: const Icon(Icons.info), + leading: const Icon(Icons.info_outlined), title: Text(context.l10n.aboutX('Lichess')), trailing: const _OpenInNewIcon(), onTap: () { @@ -314,7 +314,7 @@ class _Body extends ConsumerWidget { }, ), PlatformListTile( - leading: const Icon(Icons.feedback), + leading: const Icon(Icons.feedback_outlined), title: Text(context.l10n.mobileFeedbackButton), trailing: const _OpenInNewIcon(), onTap: () { @@ -322,7 +322,7 @@ class _Body extends ConsumerWidget { }, ), PlatformListTile( - leading: const Icon(Icons.article), + leading: const Icon(Icons.article_outlined), title: Text(context.l10n.termsOfService), trailing: const _OpenInNewIcon(), onTap: () { @@ -330,7 +330,7 @@ class _Body extends ConsumerWidget { }, ), PlatformListTile( - leading: const Icon(Icons.privacy_tip), + leading: const Icon(Icons.privacy_tip_outlined), title: Text(context.l10n.privacyPolicy), trailing: const _OpenInNewIcon(), onTap: () { @@ -344,7 +344,7 @@ class _Body extends ConsumerWidget { showDivider: true, children: [ PlatformListTile( - leading: const Icon(Icons.code), + leading: const Icon(Icons.code_outlined), title: Text(context.l10n.sourceCode), trailing: const _OpenInNewIcon(), onTap: () { @@ -352,7 +352,7 @@ class _Body extends ConsumerWidget { }, ), PlatformListTile( - leading: const Icon(Icons.bug_report), + leading: const Icon(Icons.bug_report_outlined), title: Text(context.l10n.contribute), trailing: const _OpenInNewIcon(), onTap: () { @@ -360,7 +360,7 @@ class _Body extends ConsumerWidget { }, ), PlatformListTile( - leading: const Icon(Icons.star), + leading: const Icon(Icons.star_border_outlined), title: Text(context.l10n.thankYou), trailing: const _OpenInNewIcon(), onTap: () { @@ -374,7 +374,7 @@ class _Body extends ConsumerWidget { showDivider: true, children: [ PlatformListTile( - leading: const Icon(Icons.storage), + leading: const Icon(Icons.storage_outlined), title: const Text('Local database size'), subtitle: Theme.of(context).platform == TargetPlatform.iOS ? null @@ -469,6 +469,7 @@ class _OpenInNewIcon extends StatelessWidget { Widget build(BuildContext context) { return Icon( Icons.open_in_new, + size: 18, color: Theme.of(context).platform == TargetPlatform.iOS ? CupertinoColors.systemGrey2.resolveFrom(context) : null, From c115b18aa0f2921351ce8786c7b904a14aa349ea Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 23 Sep 2024 16:05:28 +0200 Subject: [PATCH 380/979] Display the board when challenge is created from position --- lib/src/view/play/challenge_list_item.dart | 72 ++++++++++++++++------ lib/src/widgets/list.dart | 45 ++++++++++++++ 2 files changed, 98 insertions(+), 19 deletions(-) diff --git a/lib/src/view/play/challenge_list_item.dart b/lib/src/view/play/challenge_list_item.dart index 7318c1b018..959fb5d42d 100644 --- a/lib/src/view/play/challenge_list_item.dart +++ b/lib/src/view/play/challenge_list_item.dart @@ -1,15 +1,19 @@ +import 'dart:math'; + import 'package:dartchess/dartchess.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/challenge/challenge.dart'; +import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/speed.dart'; import 'package:lichess_mobile/src/model/lobby/correspondence_challenge.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/widgets/board_thumbnail.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/user_full_name.dart'; @@ -41,6 +45,24 @@ class ChallengeListItem extends ConsumerWidget { ? context.lichessColors.good.withValues(alpha: 0.2) : null; + final isFromPosition = challenge.variant == Variant.fromPosition; + + final leading = Icon(challenge.perf.icon, size: 36); + final trailing = challenge.challenger?.lagRating != null + ? LagIndicator(lagRating: challenge.challenger!.lagRating!) + : null; + final title = isMyChallenge + ? UserFullNameWidget( + user: challenge.destUser != null ? challenge.destUser!.user : user, + ) + : UserFullNameWidget( + user: user, + rating: challenge.challenger?.rating, + ); + final subtitle = Text(challenge.description(context.l10n)); + + final screenWidth = MediaQuery.sizeOf(context).width; + return Container( color: color, child: Slidable( @@ -73,25 +95,37 @@ class ChallengeListItem extends ConsumerWidget { ), ], ), - child: PlatformListTile( - padding: Styles.bodyPadding, - leading: Icon(challenge.perf.icon, size: 36), - trailing: challenge.challenger?.lagRating != null - ? LagIndicator(lagRating: challenge.challenger!.lagRating!) - : null, - title: isMyChallenge - ? UserFullNameWidget( - user: challenge.destUser != null - ? challenge.destUser!.user - : user, - ) - : UserFullNameWidget( - user: user, - rating: challenge.challenger?.rating, - ), - subtitle: Text(challenge.description(context.l10n)), - onTap: onPressed, - ), + child: isFromPosition + ? ExpansionTile( + childrenPadding: Styles.bodyPadding + .subtract(const EdgeInsets.only(top: 8.0)), + leading: leading, + title: title, + subtitle: subtitle, + children: [ + if (challenge.variant == Variant.fromPosition && + challenge.initialFen != null) + BoardThumbnail( + size: min( + 400, + screenWidth - 2 * Styles.bodyPadding.horizontal, + ), + orientation: challenge.sideChoice == SideChoice.white + ? Side.white + : Side.black, + fen: challenge.initialFen!, + onTap: onPressed, + ), + ], + // onTap: onPressed, + ) + : AdaptiveListTile( + leading: leading, + title: title, + subtitle: subtitle, + trailing: trailing, + onTap: onPressed, + ), ), ); } diff --git a/lib/src/widgets/list.dart b/lib/src/widgets/list.dart index 6ca7f46559..d60937bf4d 100644 --- a/lib/src/widgets/list.dart +++ b/lib/src/widgets/list.dart @@ -372,3 +372,48 @@ class PlatformListTile extends StatelessWidget { } } } + +/// A [ListTile] that adapts to the platform. +/// +/// Contrary to [PlatformListTile], this widget uses a [ListTile] on both iOS and +/// Android. +/// On Android the list tile will be displayed without modifications. +/// On iOS the list tile will have a custom splash factory to remove the splash +/// effect. +class AdaptiveListTile extends StatelessWidget { + const AdaptiveListTile({ + this.leading, + required this.title, + this.subtitle, + this.trailing, + this.onTap, + super.key, + }); + + final Widget? leading; + final Widget title; + final Widget? subtitle; + final Widget? trailing; + final GestureTapCallback? onTap; + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + child: Theme( + data: ThemeData( + splashFactory: Theme.of(context).platform == TargetPlatform.iOS + ? NoSplash.splashFactory + : null, + ), + child: ListTile( + leading: leading, + title: title, + subtitle: subtitle, + trailing: trailing, + onTap: onTap, + ), + ), + ); + } +} From 05d64669ac780a328078f5dda3feba151233c5c8 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 23 Sep 2024 18:02:33 +0200 Subject: [PATCH 381/979] Refactor notifications setup --- lib/main.dart | 168 ++---------------- lib/src/app.dart | 141 +-------------- lib/src/app_initialization.dart | 57 ++++++ lib/src/intl.dart | 20 ++- lib/src/log.dart | 13 +- .../model/challenge/challenge_service.dart | 1 - .../correspondence_service.dart | 22 +++ .../notifications/local_notification.dart | 130 ++++++++++++-- .../local_notification_service.dart | 90 ---------- .../push_notification_service.dart | 155 ++++++++++++++-- lib/src/utils/connectivity.dart | 2 +- .../view/user/challenge_requests_screen.dart | 2 +- test/fake_notification_service.dart | 6 +- 13 files changed, 389 insertions(+), 418 deletions(-) delete mode 100644 lib/src/model/notifications/local_notification_service.dart diff --git a/lib/main.dart b/lib/main.dart index 738df6d9f0..27d6c34455 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,123 +1,39 @@ -import 'dart:convert'; - -import 'package:dynamic_color/dynamic_color.dart'; import 'package:firebase_core/firebase_core.dart'; -import 'package:firebase_crashlytics/firebase_crashlytics.dart'; -import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lichess_mobile/l10n/l10n.dart'; -import 'package:lichess_mobile/src/db/database.dart'; +import 'package:lichess_mobile/src/app_initialization.dart'; import 'package:lichess_mobile/src/intl.dart'; import 'package:lichess_mobile/src/log.dart'; -import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/model/correspondence/correspondence_game_storage.dart'; -import 'package:lichess_mobile/src/model/correspondence/offline_correspondence_game.dart'; -import 'package:lichess_mobile/src/model/game/playable_game.dart'; -import 'package:lichess_mobile/src/model/notifications/challenge_notification.dart'; import 'package:lichess_mobile/src/model/notifications/local_notification.dart'; -import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; -import 'package:lichess_mobile/src/utils/badge_service.dart'; -import 'package:lichess_mobile/src/utils/screen.dart'; -import 'package:path/path.dart' as path; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:sqflite/sqflite.dart'; import 'firebase_options.dart'; import 'src/app.dart'; -import 'src/utils/color_palette.dart'; - -final _notificationPlugin = FlutterLocalNotificationsPlugin(); Future main() async { final widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); - final systemLocale = widgetsBinding.platformDispatcher.locale; + // Show splash screen until app is ready + // See src/app.dart for splash screen removal + FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); - // logging setup - setupLogger(); + setupLogging(); SharedPreferences.setPrefix('lichess.'); - // Get locale from shared preferences, if any - final prefs = await SharedPreferences.getInstance(); - final json = prefs.getString(kGeneralPreferencesKey); - final generalPref = json != null - ? GeneralPrefsState.fromJson(jsonDecode(json) as Map) - : GeneralPrefsState.defaults; - final prefsLocale = generalPref.locale; - - final locale = prefsLocale ?? systemLocale; - - // Intl and timeago setup - await setupIntl(locale); + // Locale, Intl and timeago setup + final locale = await setupIntl(widgetsBinding); - // Local notifications setup - final l10n = await AppLocalizations.delegate.load(locale); - await _notificationPlugin.initialize( - InitializationSettings( - android: const AndroidInitializationSettings('logo_black'), - iOS: DarwinInitializationSettings( - requestBadgePermission: false, - notificationCategories: [ - ChallengeNotification.darwinPlayableVariantCategory(l10n), - ChallengeNotification.darwinUnplayableVariantCategory(l10n), - ], - ), - ), - onDidReceiveNotificationResponse: onDidReceiveNotificationResponse, - onDidReceiveBackgroundNotificationResponse: notificationTapBackground, - ); + // local notifications service setup + await LocalNotificationService.initialize(locale); // Firebase setup await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); - // Crashlytics - if (kReleaseMode) { - FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError; - // Pass all uncaught asynchronous errors that aren't handled by the Flutter framework to Crashlytics - PlatformDispatcher.instance.onError = (error, stack) { - FirebaseCrashlytics.instance.recordError(error, stack, fatal: true); - return true; - }; - } - - // Get android 12+ core palette - try { - await DynamicColorPlugin.getCorePalette().then((value) { - setCorePalette(value); - }); - } catch (e) { - debugPrint('Could not get core palette: $e'); - } - - // Show splash screen until app is ready - // See src/app.dart for splash screen removal - FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); - if (defaultTargetPlatform == TargetPlatform.android) { - // lock orientation to portrait on android phones - final view = widgetsBinding.platformDispatcher.views.first; - final data = MediaQueryData.fromView(view); - if (data.size.shortestSide < FormFactor.tablet) { - await SystemChrome.setPreferredOrientations( - [DeviceOrientation.portraitUp], - ); - } - - // Sets edge-to-edge system UI mode on Android 12+ - SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); - SystemChrome.setSystemUIOverlayStyle( - const SystemUiOverlayStyle( - systemNavigationBarColor: Colors.transparent, - systemNavigationBarDividerColor: Colors.transparent, - systemNavigationBarContrastEnforced: true, - ), - ); + androidDisplayInitialization(widgetsBinding); } runApp( @@ -129,67 +45,3 @@ Future main() async { ), ); } - -@pragma('vm:entry-point') -void notificationTapBackground(NotificationResponse response) { - debugPrint('Background local notification response: $response'); -} - -@pragma('vm:entry-point') -Future firebaseMessagingBackgroundHandler(RemoteMessage message) async { - debugPrint('Handling a background message: ${message.data}'); - - final gameFullId = message.data['lichess.fullId'] as String?; - final round = message.data['lichess.round'] as String?; - - // update correspondence game - if (gameFullId != null && round != null) { - final dbPath = path.join(await getDatabasesPath(), kLichessDatabaseName); - final db = await openDb(databaseFactory, dbPath); - final fullId = GameFullId(gameFullId); - final game = PlayableGame.fromServerJson( - jsonDecode(round) as Map, - ); - final corresGame = OfflineCorrespondenceGame( - id: game.id, - fullId: fullId, - meta: game.meta, - rated: game.meta.rated, - steps: game.steps, - initialFen: game.initialFen, - status: game.status, - variant: game.meta.variant, - speed: game.meta.speed, - perf: game.meta.perf, - white: game.white, - black: game.black, - youAre: game.youAre!, - daysPerTurn: game.meta.daysPerTurn, - clock: game.correspondenceClock, - winner: game.winner, - isThreefoldRepetition: game.isThreefoldRepetition, - ); - - await db.insert( - kCorrespondenceStorageTable, - { - 'userId': - corresGame.me.user?.id.toString() ?? kCorrespondenceStorageAnonId, - 'gameId': corresGame.id.toString(), - 'lastModified': DateTime.now().toIso8601String(), - 'data': jsonEncode(corresGame.toJson()), - }, - conflictAlgorithm: ConflictAlgorithm.replace, - ); - } - - // update badge - final badge = message.data['lichess.iosBadge'] as String?; - if (badge != null) { - try { - BadgeService.instance.setBadge(int.parse(badge)); - } catch (e) { - debugPrint('Could not parse badge: $badge'); - } - } -} diff --git a/lib/src/app.dart b/lib/src/app.dart index ad15979d88..405dad505c 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -1,36 +1,29 @@ import 'dart:async'; import 'package:dynamic_color/dynamic_color.dart'; -import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_displaymode/flutter_displaymode.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/l10n/l10n.dart'; -import 'package:lichess_mobile/main.dart'; import 'package:lichess_mobile/src/app_initialization.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/challenge/challenge_service.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; -import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/correspondence/correspondence_service.dart'; -import 'package:lichess_mobile/src/model/notifications/local_notification_service.dart'; -import 'package:lichess_mobile/src/model/notifications/push_notification_service.dart'; +import 'package:lichess_mobile/src/model/notifications/local_notification.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/model/settings/brightness.dart'; import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; import 'package:lichess_mobile/src/navigation.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/connectivity.dart'; -import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/utils/system.dart'; -import 'package:lichess_mobile/src/view/game/game_screen.dart'; /// Application initialization and main entry point. class AppInitializationScreen extends ConsumerWidget { @@ -108,10 +101,6 @@ class _AppState extends ConsumerState { @override void initState() { - if (Theme.of(context).platform == TargetPlatform.android) { - setOptimalDisplayMode(); - } - // preload sounds final soundTheme = ref.read(generalPreferencesProvider).soundTheme; preloadSounds(soundTheme); @@ -119,6 +108,10 @@ class _AppState extends ConsumerState { // check if session is still active checkSession(); + // Initialize services + ref.read(localNotificationDispatcherProvider).initialize(); + ref.read(challengeServiceProvider).initialize(); + // Listen for connectivity changes and perform actions accordingly. ref.listenManual(connectivityChangesProvider, (prev, current) async { final prevWasOffline = prev?.value?.isOnline == false; @@ -275,30 +268,6 @@ class _AppState extends ConsumerState { } } } - - // Code taken from https://stackoverflow.com/questions/63631522/flutter-120fps-issue - /// Enables high refresh rate for devices where it was previously disabled - Future setOptimalDisplayMode() async { - final List supported = await FlutterDisplayMode.supported; - final DisplayMode active = await FlutterDisplayMode.active; - - final List sameResolution = supported - .where( - (DisplayMode m) => - m.width == active.width && m.height == active.height, - ) - .toList() - ..sort( - (DisplayMode a, DisplayMode b) => - b.refreshRate.compareTo(a.refreshRate), - ); - - final DisplayMode mostOptimalMode = - sameResolution.isNotEmpty ? sameResolution.first : active; - - // This setting is per session. - await FlutterDisplayMode.setPreferredMode(mostOptimalMode); - } } /// The entry point widget for the application. @@ -318,12 +287,6 @@ class _EntryPointWidget extends ConsumerStatefulWidget { } class _EntryPointState extends ConsumerState<_EntryPointWidget> { - StreamSubscription? _fcmTokenRefreshSubscription; - ProviderSubscription>? - _connectivitySubscription; - - bool _pushNotificationsSetup = false; - @override Widget build(BuildContext context) { return const BottomNavScaffold(); @@ -332,106 +295,12 @@ class _EntryPointState extends ConsumerState<_EntryPointWidget> { @override void initState() { super.initState(); - - // Initialize services - ref.read(localNotificationDispatcherProvider).initialize(); - ref.read(challengeServiceProvider).initialize(); - - _connectivitySubscription = - ref.listenManual(connectivityChangesProvider, (prev, current) async { - // setup push notifications once when the app comes online - if (current.value?.isOnline == true && !_pushNotificationsSetup) { - try { - await _setupPushNotifications(); - _pushNotificationsSetup = true; - } catch (e, st) { - debugPrint('Could not setup push notifications; $e\n$st'); - } - } - }); } @override void dispose() { - _fcmTokenRefreshSubscription?.cancel(); - _connectivitySubscription?.close(); super.dispose(); } - - Future _setupPushNotifications() async { - // Listen for incoming messages while the app is in the foreground. - FirebaseMessaging.onMessage.listen((RemoteMessage message) { - ref.read(pushNotificationServiceProvider).processDataMessage(message); - }); - - // Listen for incoming messages while the app is in the background. - FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler); - - // Request permission to receive notifications. Pop-up will appear only - // once. - await FirebaseMessaging.instance.requestPermission( - alert: true, - badge: true, - sound: true, - announcement: false, - carPlay: false, - criticalAlert: false, - provisional: false, - ); - - // Listen for token refresh and update the token on the server accordingly. - _fcmTokenRefreshSubscription = - FirebaseMessaging.instance.onTokenRefresh.listen((String token) { - ref.read(pushNotificationServiceProvider).registerToken(token); - }); - - // Register the device with the server. - await ref.read(pushNotificationServiceProvider).registerDevice(); - - // Get any messages which caused the application to open from - // a terminated state. - final RemoteMessage? initialMessage = - await FirebaseMessaging.instance.getInitialMessage(); - - if (initialMessage != null) { - _handleMessage(initialMessage); - } - - // Also handle any interaction when the app is in the background via a - // Stream listener - FirebaseMessaging.onMessageOpenedApp.listen(_handleMessage); - } - - /// Handle a message that caused the application to open - /// - /// This method must be part of a State object which is a child of [MaterialApp] - /// otherwise the [Navigator] will not be accessible. - void _handleMessage(RemoteMessage message) { - switch (message.data['lichess.type']) { - // correspondence game message types - case 'corresAlarm': - case 'gameTakebackOffer': - case 'gameDrawOffer': - case 'gameMove': - case 'gameFinish': - final gameFullId = message.data['lichess.fullId'] as String?; - if (gameFullId != null) { - // remove any existing routes before navigating to the game - // screen to avoid stacking multiple game screens - final navState = Navigator.of(context); - if (navState.canPop()) { - navState.popUntil((route) => route.isFirst); - } - pushPlatformRoute( - context, - rootNavigator: true, - builder: (_) => GameScreen( - initialGameId: GameFullId(gameFullId), - ), - ); - } - } - } } // -- diff --git a/lib/src/app_initialization.dart b/lib/src/app_initialization.dart index 46e56d12cb..b471624f23 100644 --- a/lib/src/app_initialization.dart +++ b/lib/src/app_initialization.dart @@ -1,8 +1,11 @@ import 'dart:convert'; import 'package:device_info_plus/device_info_plus.dart'; +import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_displaymode/flutter_displaymode.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/db/secure_storage.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; @@ -10,6 +13,7 @@ import 'package:lichess_mobile/src/model/auth/session_storage.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/utils/color_palette.dart'; +import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/utils/string.dart'; import 'package:lichess_mobile/src/utils/system.dart'; import 'package:logging/logging.dart'; @@ -104,3 +108,56 @@ class AppInitializationData with _$AppInitializationData { required int engineMaxMemoryInMb, }) = _AppInitializationData; } + +/// Display setup on Android. +/// +/// This is meant to be called once during app initialization. +Future androidDisplayInitialization(WidgetsBinding widgetsBinding) async { + // Get android 12+ core palette + try { + await DynamicColorPlugin.getCorePalette().then((value) { + setCorePalette(value); + }); + } catch (e) { + debugPrint('Could not get core palette: $e'); + } + + // lock orientation to portrait on android phones + final view = widgetsBinding.platformDispatcher.views.first; + final data = MediaQueryData.fromView(view); + if (data.size.shortestSide < FormFactor.tablet) { + await SystemChrome.setPreferredOrientations( + [DeviceOrientation.portraitUp], + ); + } + + // Sets edge-to-edge system UI mode on Android 12+ + SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + SystemChrome.setSystemUIOverlayStyle( + const SystemUiOverlayStyle( + systemNavigationBarColor: Colors.transparent, + systemNavigationBarDividerColor: Colors.transparent, + systemNavigationBarContrastEnforced: true, + ), + ); + + /// Enables high refresh rate for devices where it was previously disabled + + final List supported = await FlutterDisplayMode.supported; + final DisplayMode active = await FlutterDisplayMode.active; + + final List sameResolution = supported + .where( + (DisplayMode m) => m.width == active.width && m.height == active.height, + ) + .toList() + ..sort( + (DisplayMode a, DisplayMode b) => b.refreshRate.compareTo(a.refreshRate), + ); + + final DisplayMode mostOptimalMode = + sameResolution.isNotEmpty ? sameResolution.first : active; + + // This setting is per session. + await FlutterDisplayMode.setPreferredMode(mostOptimalMode); +} diff --git a/lib/src/intl.dart b/lib/src/intl.dart index 625a4c1eb7..ded7c47127 100644 --- a/lib/src/intl.dart +++ b/lib/src/intl.dart @@ -1,10 +1,24 @@ -import 'dart:ui'; +import 'dart:convert'; +import 'package:flutter/widgets.dart'; import 'package:intl/intl.dart'; +import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:timeago/timeago.dart' as timeago; /// Setup [Intl.defaultLocale] and timeago locale and messages. -Future setupIntl(Locale locale) async { +Future setupIntl(WidgetsBinding widgetsBinding) async { + final systemLocale = widgetsBinding.platformDispatcher.locale; + + // Get locale from shared preferences, if any + final prefs = await SharedPreferences.getInstance(); + final json = prefs.getString(kGeneralPreferencesKey); + final generalPref = json != null + ? GeneralPrefsState.fromJson(jsonDecode(json) as Map) + : GeneralPrefsState.defaults; + final prefsLocale = generalPref.locale; + final locale = prefsLocale ?? systemLocale; + Intl.defaultLocale = locale.toLanguageTag(); // we need to setup timeago locale manually @@ -22,6 +36,8 @@ Future setupIntl(Locale locale) async { timeago.setDefaultLocale(shortLocale); } } + + return locale; } final Map _timeagoLocales = { diff --git a/lib/src/log.dart b/lib/src/log.dart index 4c1ab06426..1a663a1c82 100644 --- a/lib/src/log.dart +++ b/lib/src/log.dart @@ -1,5 +1,6 @@ import 'dart:developer' as developer; +import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:logging/logging.dart'; @@ -10,7 +11,7 @@ const _loggersToShowInTerminal = { 'Socket', }; -void setupLogger() { +void setupLogging() { if (kDebugMode) { Logger.root.level = Level.FINE; Logger.root.onRecord.listen((record) { @@ -34,6 +35,16 @@ void setupLogger() { } }); } + + // Crashlytics + if (kReleaseMode) { + FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError; + // Pass all uncaught asynchronous errors that aren't handled by the Flutter framework to Crashlytics + PlatformDispatcher.instance.onError = (error, stack) { + FirebaseCrashlytics.instance.recordError(error, stack, fatal: true); + return true; + }; + } } class ProviderLogger extends ProviderObserver { diff --git a/lib/src/model/challenge/challenge_service.dart b/lib/src/model/challenge/challenge_service.dart index 7a73ad148c..7aac6b70ae 100644 --- a/lib/src/model/challenge/challenge_service.dart +++ b/lib/src/model/challenge/challenge_service.dart @@ -10,7 +10,6 @@ import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/notifications/challenge_notification.dart'; import 'package:lichess_mobile/src/model/notifications/local_notification.dart'; -import 'package:lichess_mobile/src/model/notifications/local_notification_service.dart'; import 'package:lichess_mobile/src/navigation.dart'; import 'package:lichess_mobile/src/utils/l10n.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; diff --git a/lib/src/model/correspondence/correspondence_service.dart b/lib/src/model/correspondence/correspondence_service.dart index 8a06913f8d..c1352ea64d 100644 --- a/lib/src/model/correspondence/correspondence_service.dart +++ b/lib/src/model/correspondence/correspondence_service.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:collection/collection.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/widgets.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/auth/bearer.dart'; @@ -15,6 +16,9 @@ import 'package:lichess_mobile/src/model/correspondence/offline_correspondence_g import 'package:lichess_mobile/src/model/game/game_repository.dart'; import 'package:lichess_mobile/src/model/game/game_socket_events.dart'; import 'package:lichess_mobile/src/model/game/playable_game.dart'; +import 'package:lichess_mobile/src/navigation.dart'; +import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/view/game/game_screen.dart'; import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -28,12 +32,29 @@ CorrespondenceService correspondenceService(CorrespondenceServiceRef ref) { ); } +/// Services that manages correspondence games. class CorrespondenceService { CorrespondenceService(this._log, {required this.ref}); final CorrespondenceServiceRef ref; final Logger _log; + /// Handles a fcm notification response that caused the app to open. + Future onNotificationResponse(GameFullId fullId) async { + final context = ref.read(currentNavigatorKeyProvider).currentContext; + if (context == null || !context.mounted) return; + + final navState = Navigator.of(context); + if (navState.canPop()) { + navState.popUntil((route) => route.isFirst); + } + pushPlatformRoute( + context, + rootNavigator: true, + builder: (_) => GameScreen(initialGameId: fullId), + ); + } + /// Syncs offline correspondence games with the server. Future syncGames() async { if (_session == null) { @@ -184,6 +205,7 @@ class CorrespondenceService { return movesPlayed; } + /// Updates a stored correspondence game. Future updateGame(GameFullId fullId, PlayableGame game) async { return (await ref.read(correspondenceGameStorageProvider.future)).save( OfflineCorrespondenceGame( diff --git a/lib/src/model/notifications/local_notification.dart b/lib/src/model/notifications/local_notification.dart index 9701710260..bccd1db883 100644 --- a/lib/src/model/notifications/local_notification.dart +++ b/lib/src/model/notifications/local_notification.dart @@ -1,15 +1,23 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:ui'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:lichess_mobile/l10n/l10n.dart'; +import 'package:lichess_mobile/src/model/challenge/challenge_service.dart'; import 'package:logging/logging.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +import 'challenge_notification.dart'; part 'local_notification.g.dart'; part 'local_notification.freezed.dart'; final _logger = Logger('LocalNotification'); +final _notificationPlugin = FlutterLocalNotificationsPlugin(); + /// A notification response and its id and payload. typedef ParsedNotificationResponse = ( int, @@ -24,25 +32,121 @@ final StreamController _responseStreamController = final Stream localNotificationResponseStream = _responseStreamController.stream; -/// Function called by the flutter_local_notifications plugin when a notification is received in the foreground. -void onDidReceiveNotificationResponse(NotificationResponse response) { - _logger.info('processing response in foreground. id [${response.id}]'); +enum NotificationType { + info, + challenge, +} + +/// A service that manages local notifications. +class LocalNotificationService { + const LocalNotificationService._(this._log); + + static final instance = + LocalNotificationService._(Logger('LocalNotificationService')); + + final Logger _log; + + static Future initialize(Locale locale) async { + final l10n = await AppLocalizations.delegate.load(locale); + await _notificationPlugin.initialize( + InitializationSettings( + android: const AndroidInitializationSettings('logo_black'), + iOS: DarwinInitializationSettings( + requestBadgePermission: false, + notificationCategories: [ + ChallengeNotification.darwinPlayableVariantCategory(l10n), + ChallengeNotification.darwinUnplayableVariantCategory(l10n), + ], + ), + ), + onDidReceiveNotificationResponse: _onDidReceiveNotificationResponse, + // onDidReceiveBackgroundNotificationResponse: notificationTapBackground, + ); + } + + /// Function called by the flutter_local_notifications plugin when a notification is received in the foreground. + static void _onDidReceiveNotificationResponse(NotificationResponse response) { + _logger.info('processing response in foreground. id [${response.id}]'); + + if (response.id == null || response.payload == null) return; + + try { + final payload = NotificationPayload.fromJson( + jsonDecode(response.payload!) as Map, + ); + _responseStreamController.add((response.id!, response, payload)); + } catch (e) { + _logger.warning('Failed to parse notification payload: $e'); + } + } + + /// Show a local notification. + Future show(LocalNotification notification) async { + final id = notification.id; + final payload = notification.payload != null + ? jsonEncode(notification.payload!.toJson()) + : null; + + await _notificationPlugin.show( + id, + notification.title, + notification.body, + notification.details, + payload: payload, + ); + _log.info( + 'Sent notification: ($id | ${notification.title}) ${notification.body} (Payload: ${notification.payload})', + ); + + return id; + } + + /// Cancel a local notification. + Future cancel(int id) async { + _log.info('canceled notification id: [$id]'); + return _notificationPlugin.cancel(id); + } +} + +/// A service that dispatches user interaction responses from local notifications to the appropriate handlers. +class LocalNotificationDispatcher { + LocalNotificationDispatcher(this.ref); - if (response.id == null || response.payload == null) return; + final LocalNotificationDispatcherRef ref; - try { - final payload = NotificationPayload.fromJson( - jsonDecode(response.payload!) as Map, + StreamSubscription? _responseSubscription; + + /// Start listening for notification responses. + void initialize() { + _responseSubscription = localNotificationResponseStream.listen( + (data) { + final (notifId, response, payload) = data; + switch (payload.type) { + case NotificationType.challenge: + ref.read(challengeServiceProvider).onNotificationResponse( + notifId, + response.actionId, + payload, + ); + case NotificationType.info: + break; + } + }, ); - _responseStreamController.add((response.id!, response, payload)); - } catch (e) { - _logger.warning('Failed to parse notification payload: $e'); + } + + void onDispose() { + _responseSubscription?.cancel(); } } -enum NotificationType { - info, - challenge, +@Riverpod(keepAlive: true) +LocalNotificationDispatcher localNotificationDispatcher( + LocalNotificationDispatcherRef ref, +) { + final service = LocalNotificationDispatcher(ref); + ref.onDispose(service.onDispose); + return service; } @Freezed(fromJson: true, toJson: true) diff --git a/lib/src/model/notifications/local_notification_service.dart b/lib/src/model/notifications/local_notification_service.dart deleted file mode 100644 index 6b6376201b..0000000000 --- a/lib/src/model/notifications/local_notification_service.dart +++ /dev/null @@ -1,90 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; - -import 'package:flutter_local_notifications/flutter_local_notifications.dart'; -import 'package:lichess_mobile/src/model/challenge/challenge_service.dart'; -import 'package:lichess_mobile/src/model/notifications/local_notification.dart'; -import 'package:logging/logging.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'local_notification_service.g.dart'; - -final _notificationPlugin = FlutterLocalNotificationsPlugin(); - -/// A service that manages local notifications. -class LocalNotificationService { - const LocalNotificationService._(this._log); - - static final instance = - LocalNotificationService._(Logger('LocalNotificationService')); - - final Logger _log; - - /// Show a local notification. - Future show(LocalNotification notification) async { - final id = notification.id; - final payload = notification.payload != null - ? jsonEncode(notification.payload!.toJson()) - : null; - - await _notificationPlugin.show( - id, - notification.title, - notification.body, - notification.details, - payload: payload, - ); - _log.info( - 'Sent notification: ($id | ${notification.title}) ${notification.body} (Payload: ${notification.payload})', - ); - - return id; - } - - /// Cancel a local notification. - Future cancel(int id) async { - _log.info('canceled notification id: [$id]'); - return _notificationPlugin.cancel(id); - } -} - -/// A service that dispatches user interaction responses from local notifications to the appropriate handlers. -class LocalNotificationDispatcher { - LocalNotificationDispatcher(this.ref); - - final LocalNotificationDispatcherRef ref; - - StreamSubscription? _responseSubscription; - - /// Start listening for notification responses. - void initialize() { - _responseSubscription = localNotificationResponseStream.listen( - (data) { - final (notifId, response, payload) = data; - switch (payload.type) { - case NotificationType.challenge: - ref.read(challengeServiceProvider).onNotificationResponse( - notifId, - response.actionId, - payload, - ); - case NotificationType.info: - break; - } - }, - ); - } - - void onDispose() { - _responseSubscription?.cancel(); - } -} - -@Riverpod(keepAlive: true) -LocalNotificationDispatcher localNotificationDispatcher( - LocalNotificationDispatcherRef ref, -) { - final service = LocalNotificationDispatcher(ref); - ref.onDispose(service.onDispose); - return service; -} diff --git a/lib/src/model/notifications/push_notification_service.dart b/lib/src/model/notifications/push_notification_service.dart index 6a0087f116..a451cc2564 100644 --- a/lib/src/model/notifications/push_notification_service.dart +++ b/lib/src/model/notifications/push_notification_service.dart @@ -1,6 +1,10 @@ +import 'dart:async'; import 'dart:convert'; import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/app_initialization.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; @@ -8,6 +12,7 @@ import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/correspondence/correspondence_service.dart'; import 'package:lichess_mobile/src/model/game/playable_game.dart'; import 'package:lichess_mobile/src/utils/badge_service.dart'; +import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -17,10 +22,14 @@ part 'push_notification_service.g.dart'; PushNotificationService pushNotificationService( PushNotificationServiceRef ref, ) { - return PushNotificationService( + final service = PushNotificationService( Logger('PushNotificationService'), ref: ref, ); + + ref.onDispose(() => service._dispose()); + + return service; } class PushNotificationService { @@ -29,14 +38,103 @@ class PushNotificationService { final PushNotificationServiceRef ref; final Logger _log; + StreamSubscription? _fcmTokenRefreshSubscription; + ProviderSubscription>? + _connectivitySubscription; + + bool _registeredDevice = false; + + /// Starts the push notification service. + Future start() async { + _connectivitySubscription = + ref.listen(connectivityChangesProvider, (prev, current) async { + // register device once the app is online + if (current.value?.isOnline == true && !_registeredDevice) { + try { + await registerDevice(); + _registeredDevice = true; + } catch (e, st) { + debugPrint('Could not setup push notifications; $e\n$st'); + } + } + }); + + // Listen for incoming messages while the app is in the foreground. + FirebaseMessaging.onMessage.listen((RemoteMessage message) { + _log.fine('processing message received in foreground: $message'); + _processDataMessage(message, fromBackground: false); + }); + + // Listen for incoming messages while the app is in the background. + FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler); + + // Request permission to receive notifications. Pop-up will appear only + // once. + await FirebaseMessaging.instance.requestPermission( + alert: true, + badge: true, + sound: true, + announcement: false, + carPlay: false, + criticalAlert: false, + provisional: false, + ); + + // Listen for token refresh and update the token on the server accordingly. + _fcmTokenRefreshSubscription = + FirebaseMessaging.instance.onTokenRefresh.listen((String token) { + _registerToken(token); + }); + + // Get any messages which caused the application to open from + // a terminated state. + final RemoteMessage? initialMessage = + await FirebaseMessaging.instance.getInitialMessage(); + + if (initialMessage != null) { + _handleMessage(initialMessage); + } + + // Also handle any interaction when the app is in the background via a + // Stream listener + FirebaseMessaging.onMessageOpenedApp.listen(_handleMessage); + } + + void _dispose() { + _fcmTokenRefreshSubscription?.cancel(); + _connectivitySubscription?.close(); + } + + /// Handle a message that caused the application to open + /// + /// This method must be part of a State object which is a child of [MaterialApp] + /// otherwise the [Navigator] will not be accessible. + void _handleMessage(RemoteMessage message) { + switch (message.data['lichess.type']) { + // correspondence game message types + case 'corresAlarm': + case 'gameTakebackOffer': + case 'gameDrawOffer': + case 'gameMove': + case 'gameFinish': + final gameFullId = message.data['lichess.fullId'] as String?; + if (gameFullId != null) { + ref.read(correspondenceServiceProvider).onNotificationResponse( + GameFullId(gameFullId), + ); + } + } + } + + /// Register the device for push notifications. Future registerDevice() async { final token = await FirebaseMessaging.instance.getToken(); if (token != null) { - await registerToken(token); + await _registerToken(token); } } - Future registerToken(String token) async { + Future _registerToken(String token) async { final settings = await FirebaseMessaging.instance.getNotificationSettings(); if (settings.authorizationStatus == AuthorizationStatus.denied) { return; @@ -55,6 +153,7 @@ class PushNotificationService { } } + /// Unregister the device from push notifications. Future unregister() async { _log.info('will unregister'); final session = ref.read(authSessionProvider); @@ -70,22 +169,28 @@ class PushNotificationService { } } - /// Process a message received while the app is in the foreground. - Future processDataMessage(RemoteMessage message) async { - _log.fine('processing message received in foreground: $message'); + /// Process a message received + Future _processDataMessage( + RemoteMessage message, { + required bool fromBackground, + }) async { final gameFullId = message.data['lichess.fullId'] as String?; final round = message.data['lichess.round'] as String?; + // update correspondence game if (gameFullId != null && round != null) { final fullId = GameFullId(gameFullId); final game = PlayableGame.fromServerJson( jsonDecode(round) as Map, ); - // opponent just played, invalidate ongoing games - if (game.sideToMove == game.youAre) { - ref.invalidate(ongoingGamesProvider); - } ref.read(correspondenceServiceProvider).updateGame(fullId, game); + + if (!fromBackground) { + // opponent just played, invalidate ongoing games + if (game.sideToMove == game.youAre) { + ref.invalidate(ongoingGamesProvider); + } + } } // update badge @@ -98,4 +203,34 @@ class PushNotificationService { } } } + + @pragma('vm:entry-point') + static Future _firebaseMessagingBackgroundHandler( + RemoteMessage message, + ) async { + debugPrint('Handling a fcm background message: ${message.data}'); + + // create a new provider scope for the background isolate + final ref = ProviderContainer(); + + ref.listen( + appInitializationProvider, + (prev, now) { + if (!now.hasValue) return; + try { + ref.read(pushNotificationServiceProvider)._processDataMessage( + message, + fromBackground: true, + ); + + ref.dispose(); + } catch (e) { + debugPrint('Error processing background message: $e'); + ref.dispose(); + } + }, + ); + + ref.read(appInitializationProvider); + } } diff --git a/lib/src/utils/connectivity.dart b/lib/src/utils/connectivity.dart index c6d37e3483..214baad4e8 100644 --- a/lib/src/utils/connectivity.dart +++ b/lib/src/utils/connectivity.dart @@ -20,7 +20,7 @@ final _logger = Logger('Connectivity'); /// /// - Uses the [Connectivity] plugin to listen to connectivity changes /// - Uses [AppLifecycleListener] to check connectivity on app resume -@riverpod +@Riverpod(keepAlive: true) class ConnectivityChanges extends _$ConnectivityChanges { StreamSubscription>? _socketSubscription; AppLifecycleListener? _appLifecycleListener; diff --git a/lib/src/view/user/challenge_requests_screen.dart b/lib/src/view/user/challenge_requests_screen.dart index e740ef1cc6..4ddd0750e2 100644 --- a/lib/src/view/user/challenge_requests_screen.dart +++ b/lib/src/view/user/challenge_requests_screen.dart @@ -4,7 +4,7 @@ import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/challenge/challenge.dart'; import 'package:lichess_mobile/src/model/challenge/challenge_repository.dart'; import 'package:lichess_mobile/src/model/challenge/challenges.dart'; -import 'package:lichess_mobile/src/model/notifications/local_notification_service.dart'; +import 'package:lichess_mobile/src/model/notifications/local_notification.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; diff --git a/test/fake_notification_service.dart b/test/fake_notification_service.dart index f6b8badaa7..6ed06f8d79 100644 --- a/test/fake_notification_service.dart +++ b/test/fake_notification_service.dart @@ -1,16 +1,12 @@ -import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:lichess_mobile/src/model/notifications/push_notification_service.dart'; class FakeNotificationService implements PushNotificationService { @override - Future processDataMessage(RemoteMessage message) async {} + Future start() async {} @override PushNotificationServiceRef get ref => throw UnimplementedError(); - @override - Future registerToken(String token) async {} - @override Future registerDevice() async {} From 70302020b7d26d5374d2d9766b3c00f6e80559c6 Mon Sep 17 00:00:00 2001 From: anon Date: Thu, 19 Sep 2024 23:28:00 +0530 Subject: [PATCH 382/979] Added tv last move highlights and check highlights --- lib/src/view/watch/tv_screen.dart | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/lib/src/view/watch/tv_screen.dart b/lib/src/view/watch/tv_screen.dart index 8eab4f0c4f..2b41c0392e 100644 --- a/lib/src/view/watch/tv_screen.dart +++ b/lib/src/view/watch/tv_screen.dart @@ -1,4 +1,6 @@ +import 'package:chessground/chessground.dart'; import 'package:dartchess/dartchess.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/constants.dart'; @@ -111,6 +113,19 @@ class _Body extends ConsumerWidget { : null, materialDiff: game.lastMaterialDiffAt(Side.white), ); + + final gameData = GameData( + playerSide: gameState.orientation == Side.white + ? PlayerSide.white + : PlayerSide.black, + isCheck: game.positionAt(gameState.stepCursor).isCheck, + sideToMove: game.sideToMove, + validMoves: IMap>(), + promotionMove: null, + onMove: (NormalMove n, {bool? isDrop}) => {}, + onPromotionSelection: (role) => {}, + ); + return BoardTable( orientation: gameState.orientation, fen: position.fen, @@ -128,6 +143,8 @@ class _Body extends ConsumerWidget { .map((e) => e.sanMove!.san) .toList(growable: false), currentMoveIndex: gameState.stepCursor, + lastMove: game.moveAt(gameState.stepCursor), + gameData: gameData, ); }, loading: () => const BoardTable( From 1c4385780378bf88211903f4451453137c86309a Mon Sep 17 00:00:00 2001 From: anon Date: Thu, 19 Sep 2024 23:53:34 +0530 Subject: [PATCH 383/979] gameData fix --- lib/src/view/watch/tv_screen.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/src/view/watch/tv_screen.dart b/lib/src/view/watch/tv_screen.dart index 2b41c0392e..9214fb6438 100644 --- a/lib/src/view/watch/tv_screen.dart +++ b/lib/src/view/watch/tv_screen.dart @@ -114,12 +114,16 @@ class _Body extends ConsumerWidget { materialDiff: game.lastMaterialDiffAt(Side.white), ); + final gameAtCursor = game.positionAt(gameState.stepCursor); + + print('MOVE NO: ${gameState.stepCursor}'); + final gameData = GameData( playerSide: gameState.orientation == Side.white ? PlayerSide.white : PlayerSide.black, - isCheck: game.positionAt(gameState.stepCursor).isCheck, - sideToMove: game.sideToMove, + isCheck: gameAtCursor.isCheck, + sideToMove: gameAtCursor.turn, validMoves: IMap>(), promotionMove: null, onMove: (NormalMove n, {bool? isDrop}) => {}, From 226c3f3c3d6a66bf1cb32727e81ad7d9f99ed21f Mon Sep 17 00:00:00 2001 From: anon Date: Thu, 19 Sep 2024 23:57:16 +0530 Subject: [PATCH 384/979] variable name fix --- lib/src/view/watch/tv_screen.dart | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/src/view/watch/tv_screen.dart b/lib/src/view/watch/tv_screen.dart index 9214fb6438..673c896f09 100644 --- a/lib/src/view/watch/tv_screen.dart +++ b/lib/src/view/watch/tv_screen.dart @@ -116,9 +116,7 @@ class _Body extends ConsumerWidget { final gameAtCursor = game.positionAt(gameState.stepCursor); - print('MOVE NO: ${gameState.stepCursor}'); - - final gameData = GameData( + final gameDataAtCursor = GameData( playerSide: gameState.orientation == Side.white ? PlayerSide.white : PlayerSide.black, @@ -148,7 +146,7 @@ class _Body extends ConsumerWidget { .toList(growable: false), currentMoveIndex: gameState.stepCursor, lastMove: game.moveAt(gameState.stepCursor), - gameData: gameData, + gameData: gameDataAtCursor, ); }, loading: () => const BoardTable( From 3b12b637d8592690b1db936b8df2ed564ed240ed Mon Sep 17 00:00:00 2001 From: anon Date: Tue, 24 Sep 2024 03:22:05 +0530 Subject: [PATCH 385/979] removing check highlight --- lib/src/view/watch/tv_screen.dart | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/lib/src/view/watch/tv_screen.dart b/lib/src/view/watch/tv_screen.dart index 673c896f09..5d55705d7e 100644 --- a/lib/src/view/watch/tv_screen.dart +++ b/lib/src/view/watch/tv_screen.dart @@ -1,6 +1,4 @@ -import 'package:chessground/chessground.dart'; import 'package:dartchess/dartchess.dart'; -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/constants.dart'; @@ -114,20 +112,6 @@ class _Body extends ConsumerWidget { materialDiff: game.lastMaterialDiffAt(Side.white), ); - final gameAtCursor = game.positionAt(gameState.stepCursor); - - final gameDataAtCursor = GameData( - playerSide: gameState.orientation == Side.white - ? PlayerSide.white - : PlayerSide.black, - isCheck: gameAtCursor.isCheck, - sideToMove: gameAtCursor.turn, - validMoves: IMap>(), - promotionMove: null, - onMove: (NormalMove n, {bool? isDrop}) => {}, - onPromotionSelection: (role) => {}, - ); - return BoardTable( orientation: gameState.orientation, fen: position.fen, @@ -146,7 +130,6 @@ class _Body extends ConsumerWidget { .toList(growable: false), currentMoveIndex: gameState.stepCursor, lastMove: game.moveAt(gameState.stepCursor), - gameData: gameDataAtCursor, ); }, loading: () => const BoardTable( From 0903fd44be9f22f8a5e575f0817f6bff0e4fda08 Mon Sep 17 00:00:00 2001 From: anon Date: Tue, 17 Sep 2024 01:36:31 +0530 Subject: [PATCH 386/979] add option to open board editor from analysis --- lib/src/view/analysis/analysis_screen.dart | 25 ++++++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index dd70a52018..83fd75d7bc 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -30,6 +30,7 @@ import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/utils/string.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_share_screen.dart'; +import 'package:lichess_mobile/src/view/board_editor/board_editor_screen.dart'; import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_screen.dart'; import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; @@ -662,6 +663,7 @@ class _BottomBar extends ConsumerWidget { .userPrevious(); Future _showAnalysisMenu(BuildContext context, WidgetRef ref) { + final analysisState = ref.read(analysisControllerProvider(pgn, options)); return showAdaptiveActionSheet( context: context, actions: [ @@ -673,6 +675,19 @@ class _BottomBar extends ConsumerWidget { .toggleBoard(); }, ), + BottomSheetAction( + makeLabel: (context) => Text(context.l10n.boardEditor), + onPressed: (context) { + final boardFen = analysisState.position.fen; + pushPlatformRoute( + context, + title: context.l10n.boardEditor, + builder: (_) => BoardEditorScreen( + initialFen: boardFen, + ), + ); + }, + ), BottomSheetAction( makeLabel: (context) => Text(context.l10n.mobileShareGamePGN), onPressed: (_) { @@ -688,10 +703,7 @@ class _BottomBar extends ConsumerWidget { onPressed: (_) { launchShareDialog( context, - text: ref - .read(analysisControllerProvider(pgn, options)) - .position - .fen, + text: analysisState.position.fen, ); }, ), @@ -701,14 +713,13 @@ class _BottomBar extends ConsumerWidget { Text(context.l10n.screenshotCurrentPosition), onPressed: (_) async { final gameId = options.gameAnyId!.gameId; - final state = ref.read(analysisControllerProvider(pgn, options)); try { final image = await ref.read(gameShareServiceProvider).screenshotPosition( gameId, options.orientation, - state.position.fen, - state.lastMove, + analysisState.position.fen, + analysisState.lastMove, ); if (context.mounted) { launchShareDialog( From e9dbda970b3db7e87ea31c2846160e51930e2cad Mon Sep 17 00:00:00 2001 From: anon Date: Tue, 24 Sep 2024 03:29:28 +0530 Subject: [PATCH 387/979] reading state data onclick --- lib/src/view/analysis/analysis_screen.dart | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index 83fd75d7bc..d49ba403b4 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -663,7 +663,6 @@ class _BottomBar extends ConsumerWidget { .userPrevious(); Future _showAnalysisMenu(BuildContext context, WidgetRef ref) { - final analysisState = ref.read(analysisControllerProvider(pgn, options)); return showAdaptiveActionSheet( context: context, actions: [ @@ -678,6 +677,8 @@ class _BottomBar extends ConsumerWidget { BottomSheetAction( makeLabel: (context) => Text(context.l10n.boardEditor), onPressed: (context) { + final analysisState = + ref.read(analysisControllerProvider(pgn, options)); final boardFen = analysisState.position.fen; pushPlatformRoute( context, @@ -701,6 +702,8 @@ class _BottomBar extends ConsumerWidget { BottomSheetAction( makeLabel: (context) => Text(context.l10n.mobileSharePositionAsFEN), onPressed: (_) { + final analysisState = + ref.read(analysisControllerProvider(pgn, options)); launchShareDialog( context, text: analysisState.position.fen, @@ -713,6 +716,8 @@ class _BottomBar extends ConsumerWidget { Text(context.l10n.screenshotCurrentPosition), onPressed: (_) async { final gameId = options.gameAnyId!.gameId; + final analysisState = + ref.read(analysisControllerProvider(pgn, options)); try { final image = await ref.read(gameShareServiceProvider).screenshotPosition( From b42163003d48e1f064040bb7153e80339d9943a3 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 24 Sep 2024 12:01:19 +0200 Subject: [PATCH 388/979] More app init refactoring --- lib/main.dart | 13 +- lib/src/app.dart | 127 +++++++----------- lib/src/db/secure_storage.dart | 10 +- lib/src/db/shared_preferences.dart | 6 +- .../{app_initialization.dart => init.dart} | 127 ++++++++++-------- lib/src/log.dart | 3 +- lib/src/model/auth/auth_session.dart | 6 +- lib/src/model/auth/session_storage.dart | 19 +-- .../model/challenge/challenge_service.dart | 2 +- lib/src/model/common/socket.dart | 6 +- lib/src/model/engine/evaluation_service.dart | 6 +- .../notifications/local_notification.dart | 2 +- .../push_notification_service.dart | 6 +- lib/src/utils/device_info.dart | 6 +- lib/src/utils/package_info.dart | 6 +- test/test_app.dart | 8 +- test/test_container.dart | 11 +- ...est.dart => settings_tab_screen_test.dart} | 0 18 files changed, 164 insertions(+), 200 deletions(-) rename lib/src/{app_initialization.dart => init.dart} (70%) rename test/view/settings/{settigs_tab_screen_test.dart => settings_tab_screen_test.dart} (100%) diff --git a/lib/main.dart b/lib/main.dart index 27d6c34455..8b0415a635 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,7 +3,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lichess_mobile/src/app_initialization.dart'; +import 'package:lichess_mobile/src/init.dart'; import 'package:lichess_mobile/src/intl.dart'; import 'package:lichess_mobile/src/log.dart'; import 'package:lichess_mobile/src/model/notifications/local_notification.dart'; @@ -15,25 +15,24 @@ import 'src/app.dart'; Future main() async { final widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); + SharedPreferences.setPrefix('lichess.'); + // Show splash screen until app is ready // See src/app.dart for splash screen removal FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); - setupLogging(); + setupLoggingAndCrashReporting(); - SharedPreferences.setPrefix('lichess.'); + await setupFirstLaunch(); - // Locale, Intl and timeago setup final locale = await setupIntl(widgetsBinding); - // local notifications service setup await LocalNotificationService.initialize(locale); - // Firebase setup await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); if (defaultTargetPlatform == TargetPlatform.android) { - androidDisplayInitialization(widgetsBinding); + await androidDisplayInitialization(widgetsBinding); } runApp( diff --git a/lib/src/app.dart b/lib/src/app.dart index 405dad505c..df76123a3b 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -6,8 +6,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/l10n/l10n.dart'; -import 'package:lichess_mobile/src/app_initialization.dart'; import 'package:lichess_mobile/src/constants.dart'; +import 'package:lichess_mobile/src/init.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/challenge/challenge_service.dart'; @@ -31,8 +31,8 @@ class AppInitializationScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - ref.listen>( - appInitializationProvider, + ref.listen>( + cachedDataProvider, (_, state) { if (state.hasValue || state.hasError) { FlutterNativeSplash.remove(); @@ -40,47 +40,44 @@ class AppInitializationScreen extends ConsumerWidget { }, ); - return ref.watch(appInitializationProvider).when( - data: (_) => const Application(), - // loading screen is handled by the native splash screen - loading: () => const SizedBox.shrink(), - error: (err, st) { - debugPrint( - 'SEVERE: [App] could not initialize app; $err\n$st', - ); - // We should really do everything we can to avoid this screen - // but in last resort, let's show an error message and invite the - // user to clear app data. - // TODO implement it on iOS - return Theme.of(context).platform == TargetPlatform.android - ? MaterialApp( - home: Scaffold( - body: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Padding( - padding: EdgeInsets.all(16.0), - child: Text( - "Something went wrong :'(\n\nIf the problem persists, you can try to clear the storage and restart the application.\n\nSorry for the inconvenience.", - textAlign: TextAlign.center, - style: TextStyle(fontSize: 18.0), - ), - ), - const SizedBox(height: 16.0), - ElevatedButton( - onPressed: () { - System.instance.clearUserData(); - }, - child: const Text('Clear storage'), - ), - ], - ), - ), - ) - : const SizedBox.shrink(); - }, - ); + final result = ref.watch(cachedDataProvider); + + if (result.isLoading) { + // loading screen is handled by the native splash screen + return const SizedBox.shrink(); + } else if (result.hasError) { + // We should really do everything we can to avoid this screen + // but in last resort, let's show an error message and invite the + // user to clear app data. + return MaterialApp( + home: Scaffold( + body: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Padding( + padding: EdgeInsets.all(16.0), + child: Text( + "Something went wrong :'(\n\nIf the problem persists, you can try to clear the storage and restart the application.\n\nSorry for the inconvenience.", + textAlign: TextAlign.center, + style: TextStyle(fontSize: 18.0), + ), + ), + const SizedBox(height: 16.0), + if (Theme.of(context).platform == TargetPlatform.android) + ElevatedButton( + onPressed: () { + System.instance.clearUserData(); + }, + child: const Text('Clear storage'), + ), + ], + ), + ), + ); + } else { + return const Application(); + } } } @@ -101,16 +98,15 @@ class _AppState extends ConsumerState { @override void initState() { - // preload sounds final soundTheme = ref.read(generalPreferencesProvider).soundTheme; preloadSounds(soundTheme); // check if session is still active checkSession(); - // Initialize services - ref.read(localNotificationDispatcherProvider).initialize(); - ref.read(challengeServiceProvider).initialize(); + // Start services + ref.read(localNotificationDispatcherProvider).start(); + ref.read(challengeServiceProvider).start(); // Listen for connectivity changes and perform actions accordingly. ref.listenManual(connectivityChangesProvider, (prev, current) async { @@ -241,7 +237,7 @@ class _AppState extends ConsumerState { ); } : null, - home: const _EntryPointWidget(), + home: const BottomNavScaffold(), navigatorObservers: [ rootNavPageRouteObserver, ], @@ -270,39 +266,6 @@ class _AppState extends ConsumerState { } } -/// The entry point widget for the application. -/// -/// This widget needs to be a desendant of [MaterialApp] to be able to handle -/// the [Navigator] properly. -/// -/// This widget is responsible for setting up the bottom navigation scaffold and -/// the main navigation routes. -/// -/// It also sets up the push notifications and handles incoming messages. -class _EntryPointWidget extends ConsumerStatefulWidget { - const _EntryPointWidget(); - - @override - ConsumerState<_EntryPointWidget> createState() => _EntryPointState(); -} - -class _EntryPointState extends ConsumerState<_EntryPointWidget> { - @override - Widget build(BuildContext context) { - return const BottomNavScaffold(); - } - - @override - void initState() { - super.initState(); - } - - @override - void dispose() { - super.dispose(); - } -} - // -- (ColorScheme light, ColorScheme dark) _generateDynamicColourSchemes( diff --git a/lib/src/db/secure_storage.dart b/lib/src/db/secure_storage.dart index 73e8e8760a..67bd33b4d4 100644 --- a/lib/src/db/secure_storage.dart +++ b/lib/src/db/secure_storage.dart @@ -1,14 +1,12 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'secure_storage.g.dart'; AndroidOptions _getAndroidOptions() => const AndroidOptions( encryptedSharedPreferences: true, sharedPreferencesName: 'org.lichess.mobile.secure', ); -@Riverpod(keepAlive: true) -FlutterSecureStorage secureStorage(SecureStorageRef ref) { - return FlutterSecureStorage(aOptions: _getAndroidOptions()); +class SecureStorage extends FlutterSecureStorage { + const SecureStorage._({super.aOptions}); + + static final instance = SecureStorage._(aOptions: _getAndroidOptions()); } diff --git a/lib/src/db/shared_preferences.dart b/lib/src/db/shared_preferences.dart index f5e00fd9d4..4848c53a3d 100644 --- a/lib/src/db/shared_preferences.dart +++ b/lib/src/db/shared_preferences.dart @@ -1,4 +1,4 @@ -import 'package:lichess_mobile/src/app_initialization.dart'; +import 'package:lichess_mobile/src/init.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -6,7 +6,7 @@ part 'shared_preferences.g.dart'; @Riverpod(keepAlive: true) SharedPreferences sharedPreferences(SharedPreferencesRef ref) { - // requireValue is possible because appInitializationProvider is loaded before + // requireValue is possible because cachedDataProvider is loaded before // anything. See: lib/src/app.dart - return ref.read(appInitializationProvider).requireValue.sharedPreferences; + return ref.read(cachedDataProvider).requireValue.sharedPreferences; } diff --git a/lib/src/app_initialization.dart b/lib/src/init.dart similarity index 70% rename from lib/src/app_initialization.dart rename to lib/src/init.dart index b471624f23..df7d5b0471 100644 --- a/lib/src/app_initialization.dart +++ b/lib/src/init.dart @@ -22,21 +22,63 @@ import 'package:pub_semver/pub_semver.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:shared_preferences/shared_preferences.dart'; -part 'app_initialization.freezed.dart'; -part 'app_initialization.g.dart'; +part 'init.freezed.dart'; +part 'init.g.dart'; -final _logger = Logger('AppInitialization'); +final _logger = Logger('Init'); +/// A provider that caches useful data. +/// +/// This provider is meant to be called once during app initialization, after +/// the provider scope has been created. @Riverpod(keepAlive: true) -Future appInitialization( - AppInitializationRef ref, -) async { - final secureStorage = ref.watch(secureStorageProvider); +Future cachedData(CachedDataRef ref) async { final sessionStorage = ref.watch(sessionStorageProvider); final pInfo = await PackageInfo.fromPlatform(); final deviceInfo = await DeviceInfoPlugin().deviceInfo; final prefs = await SharedPreferences.getInstance(); + final sri = await SecureStorage.instance.read(key: kSRIStorageKey) ?? + genRandomString(12); + + final physicalMemory = await System.instance.getTotalRam() ?? 256.0; + final engineMaxMemory = (physicalMemory / 10).ceil(); + + return CachedData( + packageInfo: pInfo, + deviceInfo: deviceInfo, + sharedPreferences: prefs, + initialUserSession: await sessionStorage.read(), + sri: sri, + engineMaxMemoryInMb: engineMaxMemory, + ); +} + +@freezed +class CachedData with _$CachedData { + const factory CachedData({ + required PackageInfo packageInfo, + required BaseDeviceInfo deviceInfo, + required SharedPreferences sharedPreferences, + + /// The user session read during app initialization. + required AuthSessionState? initialUserSession, + + /// Socket Random Identifier. + required String sri, + + /// Maximum memory in MB that the engine can use. + /// + /// This is 10% of the total physical memory. + required int engineMaxMemoryInMb, + }) = _CachedData; +} + +/// Run initialization tasks only once on first app launch or after an update. +Future setupFirstLaunch() async { + final prefs = await SharedPreferences.getInstance(); + final pInfo = await PackageInfo.fromPlatform(); + final appVersion = Version.parse(pInfo.version); final installedVersion = prefs.getString('installed_version'); @@ -47,19 +89,7 @@ Future appInitialization( if (prefs.getBool('first_run') ?? true) { // Clear secure storage on first run because it is not deleted on app uninstall - await secureStorage.deleteAll(); - - // on android 12+ set the default board theme as system - if (getCorePalette() != null) { - prefs.setString( - BoardPreferences.prefKey, - jsonEncode( - BoardPrefs.defaults.copyWith( - boardTheme: BoardTheme.system, - ), - ), - ); - } + await SecureStorage.instance.deleteAll(); await prefs.setBool('first_run', false); } @@ -67,68 +97,50 @@ Future appInitialization( // Generate a socket random identifier and store it for the app lifetime String? storedSri; try { - storedSri = await secureStorage.read(key: kSRIStorageKey); + storedSri = await SecureStorage.instance.read(key: kSRIStorageKey); if (storedSri == null) { final sri = genRandomString(12); _logger.info('Generated new SRI: $sri'); - await secureStorage.write(key: kSRIStorageKey, value: sri); + await SecureStorage.instance.write(key: kSRIStorageKey, value: sri); } } on PlatformException catch (e) { - _logger.warning('[AppInitialization] Error while reading SRI: $e'); + _logger.severe('Could not get SRI from storage: $e'); // Clear all secure storage if an error occurs because it probably means the key has // been lost - await secureStorage.deleteAll(); + await SecureStorage.instance.deleteAll(); } - - final sri = storedSri ?? - await secureStorage.read(key: kSRIStorageKey) ?? - genRandomString(12); - - final physicalMemory = await System.instance.getTotalRam() ?? 256.0; - final engineMaxMemory = (physicalMemory / 10).ceil(); - - return AppInitializationData( - packageInfo: pInfo, - deviceInfo: deviceInfo, - sharedPreferences: prefs, - userSession: await sessionStorage.read(), - sri: sri, - engineMaxMemoryInMb: engineMaxMemory, - ); -} - -@freezed -class AppInitializationData with _$AppInitializationData { - const factory AppInitializationData({ - required PackageInfo packageInfo, - required BaseDeviceInfo deviceInfo, - required SharedPreferences sharedPreferences, - required AuthSessionState? userSession, - required String sri, - required int engineMaxMemoryInMb, - }) = _AppInitializationData; } /// Display setup on Android. /// /// This is meant to be called once during app initialization. Future androidDisplayInitialization(WidgetsBinding widgetsBinding) async { - // Get android 12+ core palette + final prefs = await SharedPreferences.getInstance(); + + // On android 12+ get core palette and set the board theme to system if it is not set try { await DynamicColorPlugin.getCorePalette().then((value) { setCorePalette(value); + + if (getCorePalette() != null && + prefs.getString(BoardPreferences.prefKey) == null) { + prefs.setString( + BoardPreferences.prefKey, + jsonEncode( + BoardPrefs.defaults.copyWith(boardTheme: BoardTheme.system), + ), + ); + } }); } catch (e) { - debugPrint('Could not get core palette: $e'); + _logger.fine('Device does not support core palette: $e'); } // lock orientation to portrait on android phones final view = widgetsBinding.platformDispatcher.views.first; final data = MediaQueryData.fromView(view); if (data.size.shortestSide < FormFactor.tablet) { - await SystemChrome.setPreferredOrientations( - [DeviceOrientation.portraitUp], - ); + await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); } // Sets edge-to-edge system UI mode on Android 12+ @@ -142,7 +154,6 @@ Future androidDisplayInitialization(WidgetsBinding widgetsBinding) async { ); /// Enables high refresh rate for devices where it was previously disabled - final List supported = await FlutterDisplayMode.supported; final DisplayMode active = await FlutterDisplayMode.active; diff --git a/lib/src/log.dart b/lib/src/log.dart index 1a663a1c82..f328a773ec 100644 --- a/lib/src/log.dart +++ b/lib/src/log.dart @@ -11,7 +11,8 @@ const _loggersToShowInTerminal = { 'Socket', }; -void setupLogging() { +/// Setup logging and crash reporting. +void setupLoggingAndCrashReporting() { if (kDebugMode) { Logger.root.level = Level.FINE; Logger.root.onRecord.listen((record) { diff --git a/lib/src/model/auth/auth_session.dart b/lib/src/model/auth/auth_session.dart index 3bf6b3508f..352345d459 100644 --- a/lib/src/model/auth/auth_session.dart +++ b/lib/src/model/auth/auth_session.dart @@ -1,5 +1,5 @@ import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:lichess_mobile/src/app_initialization.dart'; +import 'package:lichess_mobile/src/init.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -12,10 +12,10 @@ part 'auth_session.g.dart'; class AuthSession extends _$AuthSession { @override AuthSessionState? build() { - // requireValue is possible because appInitializationProvider is loaded before + // requireValue is possible because cachedDataProvider is loaded before // anything. See: lib/src/app.dart return ref.watch( - appInitializationProvider.select((data) => data.requireValue.userSession), + cachedDataProvider.select((data) => data.requireValue.initialUserSession), ); } diff --git a/lib/src/model/auth/session_storage.dart b/lib/src/model/auth/session_storage.dart index a749963fc9..549594a0cd 100644 --- a/lib/src/model/auth/session_storage.dart +++ b/lib/src/model/auth/session_storage.dart @@ -1,6 +1,5 @@ import 'dart:convert'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/db/secure_storage.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; @@ -8,20 +7,18 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'session_storage.g.dart'; +const _kSessionStorageKey = '$kLichessHost.userSession'; + @Riverpod(keepAlive: true) SessionStorage sessionStorage(SessionStorageRef ref) { - return SessionStorage(ref); + return const SessionStorage(); } class SessionStorage { - const SessionStorage(SessionStorageRef ref) : _ref = ref; - - final SessionStorageRef _ref; - - FlutterSecureStorage get _storage => _ref.read(secureStorageProvider); + const SessionStorage(); Future read() async { - final string = await _storage.read(key: _kSessionStorageKey); + final string = await SecureStorage.instance.read(key: _kSessionStorageKey); if (string != null) { return AuthSessionState.fromJson( jsonDecode(string) as Map, @@ -31,15 +28,13 @@ class SessionStorage { } Future write(AuthSessionState session) async { - await _storage.write( + await SecureStorage.instance.write( key: _kSessionStorageKey, value: jsonEncode(session.toJson()), ); } Future delete() async { - await _storage.delete(key: _kSessionStorageKey); + await SecureStorage.instance.delete(key: _kSessionStorageKey); } } - -const _kSessionStorageKey = '$kLichessHost.userSession'; diff --git a/lib/src/model/challenge/challenge_service.dart b/lib/src/model/challenge/challenge_service.dart index 7aac6b70ae..93d47c54ea 100644 --- a/lib/src/model/challenge/challenge_service.dart +++ b/lib/src/model/challenge/challenge_service.dart @@ -46,7 +46,7 @@ class ChallengeService { StreamSubscription? _socketSubscription; /// Start listening to challenge events from the server. - void initialize() { + void start() { _socketSubscription = socketGlobalStream.listen(_onSocketEvent); } diff --git a/lib/src/model/common/socket.dart b/lib/src/model/common/socket.dart index 762e2b3367..c7aeb7f74f 100644 --- a/lib/src/model/common/socket.dart +++ b/lib/src/model/common/socket.dart @@ -7,8 +7,8 @@ import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:lichess_mobile/src/app_initialization.dart'; import 'package:lichess_mobile/src/constants.dart'; +import 'package:lichess_mobile/src/init.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/auth/bearer.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; @@ -605,9 +605,9 @@ SocketPool socketPool(SocketPoolRef ref) { /// Socket Random Identifier. @Riverpod(keepAlive: true) String sri(SriRef ref) { - // requireValue is possible because appInitializationProvider is loaded before + // requireValue is possible because cachedDataProvider is loaded before // anything. See: lib/src/app.dart - return ref.read(appInitializationProvider).requireValue.sri; + return ref.read(cachedDataProvider).requireValue.sri; } /// Average lag computed from WebSocket ping/pong protocol. diff --git a/lib/src/model/engine/evaluation_service.dart b/lib/src/model/engine/evaluation_service.dart index 346a8fa642..b7e529fa7e 100644 --- a/lib/src/model/engine/evaluation_service.dart +++ b/lib/src/model/engine/evaluation_service.dart @@ -6,7 +6,7 @@ import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/foundation.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:lichess_mobile/src/app_initialization.dart'; +import 'package:lichess_mobile/src/init.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/eval.dart'; import 'package:lichess_mobile/src/model/common/uci.dart'; @@ -176,10 +176,10 @@ class EvaluationService { @Riverpod(keepAlive: true) EvaluationService evaluationService(EvaluationServiceRef ref) { - // requireValue is possible because appInitializationProvider is loaded before + // requireValue is possible because cachedDataProvider is loaded before // anything. See: lib/src/app.dart final maxMemory = - ref.read(appInitializationProvider).requireValue.engineMaxMemoryInMb; + ref.read(cachedDataProvider).requireValue.engineMaxMemoryInMb; final service = EvaluationService(ref, maxMemory: maxMemory); ref.onDispose(() { diff --git a/lib/src/model/notifications/local_notification.dart b/lib/src/model/notifications/local_notification.dart index bccd1db883..f614d79024 100644 --- a/lib/src/model/notifications/local_notification.dart +++ b/lib/src/model/notifications/local_notification.dart @@ -117,7 +117,7 @@ class LocalNotificationDispatcher { StreamSubscription? _responseSubscription; /// Start listening for notification responses. - void initialize() { + void start() { _responseSubscription = localNotificationResponseStream.listen( (data) { final (notifId, response, payload) = data; diff --git a/lib/src/model/notifications/push_notification_service.dart b/lib/src/model/notifications/push_notification_service.dart index a451cc2564..21c77e98da 100644 --- a/lib/src/model/notifications/push_notification_service.dart +++ b/lib/src/model/notifications/push_notification_service.dart @@ -4,7 +4,7 @@ import 'dart:convert'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lichess_mobile/src/app_initialization.dart'; +import 'package:lichess_mobile/src/init.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; @@ -214,7 +214,7 @@ class PushNotificationService { final ref = ProviderContainer(); ref.listen( - appInitializationProvider, + cachedDataProvider, (prev, now) { if (!now.hasValue) return; try { @@ -231,6 +231,6 @@ class PushNotificationService { }, ); - ref.read(appInitializationProvider); + ref.read(cachedDataProvider); } } diff --git a/lib/src/utils/device_info.dart b/lib/src/utils/device_info.dart index 9ffb9fd5ed..0c5823dba7 100644 --- a/lib/src/utils/device_info.dart +++ b/lib/src/utils/device_info.dart @@ -1,12 +1,12 @@ import 'package:device_info_plus/device_info_plus.dart'; -import 'package:lichess_mobile/src/app_initialization.dart'; +import 'package:lichess_mobile/src/init.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'device_info.g.dart'; @Riverpod(keepAlive: true) BaseDeviceInfo deviceInfo(DeviceInfoRef ref) { - // requireValue is possible because appInitializationProvider is loaded before + // requireValue is possible because cachedDataProvider is loaded before // anything. See: lib/src/app.dart - return ref.read(appInitializationProvider).requireValue.deviceInfo; + return ref.read(cachedDataProvider).requireValue.deviceInfo; } diff --git a/lib/src/utils/package_info.dart b/lib/src/utils/package_info.dart index e9f5a8cf79..9180cd81d2 100644 --- a/lib/src/utils/package_info.dart +++ b/lib/src/utils/package_info.dart @@ -1,4 +1,4 @@ -import 'package:lichess_mobile/src/app_initialization.dart'; +import 'package:lichess_mobile/src/init.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -6,7 +6,7 @@ part 'package_info.g.dart'; @Riverpod(keepAlive: true) PackageInfo packageInfo(PackageInfoRef ref) { - // requireValue is possible because appInitializationProvider is loaded before + // requireValue is possible because cachedDataProvider is loaded before // anything. See: lib/src/app.dart - return ref.read(appInitializationProvider).requireValue.packageInfo; + return ref.read(cachedDataProvider).requireValue.packageInfo; } diff --git a/test/test_app.dart b/test/test_app.dart index 3abbd8e43d..6d39da67eb 100644 --- a/test/test_app.dart +++ b/test/test_app.dart @@ -10,9 +10,9 @@ import 'package:http/http.dart' as http; import 'package:http/testing.dart'; import 'package:intl/intl.dart'; import 'package:lichess_mobile/l10n/l10n.dart'; -import 'package:lichess_mobile/src/app_initialization.dart'; import 'package:lichess_mobile/src/crashlytics.dart'; import 'package:lichess_mobile/src/db/shared_preferences.dart'; +import 'package:lichess_mobile/src/init.dart'; import 'package:lichess_mobile/src/model/account/account_preferences.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/auth/session_storage.dart'; @@ -132,8 +132,8 @@ Future buildTestApp( return MockGameStorage(); }), // ignore: scoped_providers_should_specify_dependencies - appInitializationProvider.overrideWith((ref) { - return AppInitializationData( + cachedDataProvider.overrideWith((ref) { + return CachedData( packageInfo: PackageInfo( appName: 'lichess_mobile_test', version: 'test', @@ -150,7 +150,7 @@ Future buildTestApp( 'isPhysicalDevice': true, }), sharedPreferences: sharedPreferences, - userSession: userSession, + initialUserSession: userSession, sri: 'test', engineMaxMemoryInMb: 16, ); diff --git a/test/test_container.dart b/test/test_container.dart index e589871570..619940cbc9 100644 --- a/test/test_container.dart +++ b/test/test_container.dart @@ -6,11 +6,10 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; import 'package:intl/intl.dart'; -import 'package:lichess_mobile/src/app_initialization.dart'; import 'package:lichess_mobile/src/crashlytics.dart'; import 'package:lichess_mobile/src/db/shared_preferences.dart'; +import 'package:lichess_mobile/src/init.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; -import 'package:lichess_mobile/src/model/auth/session_storage.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; @@ -22,7 +21,6 @@ import 'package:package_info_plus/package_info_plus.dart'; import 'package:shared_preferences/shared_preferences.dart'; import './fake_crashlytics.dart'; -import './model/auth/fake_session_storage.dart'; import './model/common/service/fake_sound_service.dart'; import 'fake_notification_service.dart'; import 'model/common/fake_websocket_channel.dart'; @@ -87,9 +85,8 @@ Future makeContainer({ .overrideWithValue(FakeNotificationService()), soundServiceProvider.overrideWithValue(FakeSoundService()), sharedPreferencesProvider.overrideWithValue(sharedPreferences), - sessionStorageProvider.overrideWithValue(FakeSessionStorage()), - appInitializationProvider.overrideWith((ref) { - return AppInitializationData( + cachedDataProvider.overrideWith((ref) { + return CachedData( packageInfo: PackageInfo( appName: 'lichess_mobile_test', version: 'test', @@ -106,7 +103,7 @@ Future makeContainer({ 'isPhysicalDevice': true, }), sharedPreferences: sharedPreferences, - userSession: userSession, + initialUserSession: userSession, sri: 'test', engineMaxMemoryInMb: 16, ); diff --git a/test/view/settings/settigs_tab_screen_test.dart b/test/view/settings/settings_tab_screen_test.dart similarity index 100% rename from test/view/settings/settigs_tab_screen_test.dart rename to test/view/settings/settings_tab_screen_test.dart From 643fa8b09c7c26171c99f3bc323d1c5552dc79e0 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 24 Sep 2024 16:26:03 +0200 Subject: [PATCH 389/979] Unify local and remote notification services --- lib/main.dart | 11 +- lib/src/app.dart | 10 +- lib/src/model/auth/auth_controller.dart | 6 +- .../model/challenge/challenge_service.dart | 8 +- .../model/common/service/sound_service.dart | 29 +- .../notifications/challenge_notification.dart | 2 +- .../notifications/local_notification.dart | 170 -------- .../notifications/notification_service.dart | 402 ++++++++++++++++++ .../push_notification_service.dart | 236 ---------- .../view/user/challenge_requests_screen.dart | 5 +- test/fake_notification_service.dart | 15 - .../fake_notification_service.dart | 25 ++ test/test_app.dart | 7 +- test/test_container.dart | 7 +- 14 files changed, 474 insertions(+), 459 deletions(-) delete mode 100644 lib/src/model/notifications/local_notification.dart create mode 100644 lib/src/model/notifications/notification_service.dart delete mode 100644 lib/src/model/notifications/push_notification_service.dart delete mode 100644 test/fake_notification_service.dart create mode 100644 test/model/notifications/fake_notification_service.dart diff --git a/lib/main.dart b/lib/main.dart index 8b0415a635..d4d02def92 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,3 @@ -import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; @@ -6,10 +5,10 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/init.dart'; import 'package:lichess_mobile/src/intl.dart'; import 'package:lichess_mobile/src/log.dart'; -import 'package:lichess_mobile/src/model/notifications/local_notification.dart'; +import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; +import 'package:lichess_mobile/src/model/notifications/notification_service.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'firebase_options.dart'; import 'src/app.dart'; Future main() async { @@ -25,11 +24,11 @@ Future main() async { await setupFirstLaunch(); - final locale = await setupIntl(widgetsBinding); + await SoundService.initialize(); - await LocalNotificationService.initialize(locale); + final locale = await setupIntl(widgetsBinding); - await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); + await NotificationService.initialize(locale); if (defaultTargetPlatform == TargetPlatform.android) { await androidDisplayInitialization(widgetsBinding); diff --git a/lib/src/app.dart b/lib/src/app.dart index df76123a3b..7d68dbebde 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -12,10 +12,9 @@ import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/challenge/challenge_service.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; -import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/correspondence/correspondence_service.dart'; -import 'package:lichess_mobile/src/model/notifications/local_notification.dart'; +import 'package:lichess_mobile/src/model/notifications/notification_service.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/model/settings/brightness.dart'; import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; @@ -98,14 +97,11 @@ class _AppState extends ConsumerState { @override void initState() { - final soundTheme = ref.read(generalPreferencesProvider).soundTheme; - preloadSounds(soundTheme); - - // check if session is still active + // check if session is still active and delete it if it is not checkSession(); // Start services - ref.read(localNotificationDispatcherProvider).start(); + ref.read(notificationServiceProvider).start(); ref.read(challengeServiceProvider).start(); // Listen for connectivity changes and perform actions accordingly. diff --git a/lib/src/model/auth/auth_controller.dart b/lib/src/model/auth/auth_controller.dart index c5e68ea43d..afbdccf1bd 100644 --- a/lib/src/model/auth/auth_controller.dart +++ b/lib/src/model/auth/auth_controller.dart @@ -1,7 +1,7 @@ import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; -import 'package:lichess_mobile/src/model/notifications/push_notification_service.dart'; +import 'package:lichess_mobile/src/model/notifications/notification_service.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'auth_repository.dart'; @@ -28,7 +28,7 @@ class AuthController extends _$AuthController { // register device and reconnect to the current socket once the session token is updated await Future.wait([ - ref.read(pushNotificationServiceProvider).registerDevice(), + ref.read(notificationServiceProvider).registerDevice(), // force reconnect to the current socket with the new token ref.read(socketPoolProvider).currentClient.connect(), ]); @@ -49,7 +49,7 @@ class AuthController extends _$AuthController { await ref.withClient( (client) => AuthRepository(client, appAuth).signOut(), ); - ref.read(pushNotificationServiceProvider).unregister(); + ref.read(notificationServiceProvider).unregister(); // force reconnect to the current socket ref.read(socketPoolProvider).currentClient.connect(); await ref.read(authSessionProvider.notifier).delete(); diff --git a/lib/src/model/challenge/challenge_service.dart b/lib/src/model/challenge/challenge_service.dart index 93d47c54ea..196a228b4d 100644 --- a/lib/src/model/challenge/challenge_service.dart +++ b/lib/src/model/challenge/challenge_service.dart @@ -9,7 +9,7 @@ import 'package:lichess_mobile/src/model/challenge/challenge_repository.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/notifications/challenge_notification.dart'; -import 'package:lichess_mobile/src/model/notifications/local_notification.dart'; +import 'package:lichess_mobile/src/model/notifications/notification_service.dart'; import 'package:lichess_mobile/src/navigation.dart'; import 'package:lichess_mobile/src/utils/l10n.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; @@ -72,7 +72,8 @@ class ChallengeService { prevInwardIds .whereNot((challengeId) => currentInwardIds.contains(challengeId)) .forEach( - (id) => LocalNotificationService.instance.cancel(id.value.hashCode), + (id) => + ref.read(notificationServiceProvider).cancel(id.value.hashCode), ); // new incoming challenges @@ -80,7 +81,8 @@ class ChallengeService { .whereNot((challenge) => prevInwardIds.contains(challenge.id)) .forEach( (challenge) { - LocalNotificationService.instance + ref + .read(notificationServiceProvider) .show(ChallengeNotification(challenge, l10n)); }, ); diff --git a/lib/src/model/common/service/sound_service.dart b/lib/src/model/common/service/sound_service.dart index fe74f2b722..98db8ad3cc 100644 --- a/lib/src/model/common/service/sound_service.dart +++ b/lib/src/model/common/service/sound_service.dart @@ -1,8 +1,11 @@ +import 'dart:convert'; + import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart' show rootBundle; import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; import 'package:lichess_mobile/src/model/settings/sound_theme.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:sound_effect/sound_effect.dart'; part 'sound_service.g.dart'; @@ -32,14 +35,6 @@ final _extension = defaultTargetPlatform == TargetPlatform.iOS ? 'aifc' : 'mp3'; const Set _emtpySet = {}; -/// Initialize the sound service with the given sound theme. -/// -/// This will load the sounds from assets and make them ready to be played. -Future preloadSounds(SoundTheme theme) async { - await _soundEffectPlugin.initialize(); - await _loadAllSounds(theme); -} - /// Loads all sounds of the given [SoundTheme]. Future _loadAllSounds( SoundTheme soundTheme, { @@ -74,6 +69,24 @@ class SoundService { final SoundServiceRef _ref; + /// Initialize the sound service. + /// + /// This will load the sounds from assets and make them ready to be played. + /// This should be called once when the app starts. + static Future initialize() async { + final prefs = await SharedPreferences.getInstance(); + + final stored = prefs.getString(kGeneralPreferencesKey); + final theme = (stored != null + ? GeneralPrefsState.fromJson( + jsonDecode(stored) as Map, + ) + : GeneralPrefsState.defaults) + .soundTheme; + await _soundEffectPlugin.initialize(); + await _loadAllSounds(theme); + } + /// Play the given sound if sound is enabled. Future play(Sound sound) async { final isEnabled = _ref.read(generalPreferencesProvider).isSoundEnabled; diff --git a/lib/src/model/notifications/challenge_notification.dart b/lib/src/model/notifications/challenge_notification.dart index 87edb3f191..f827f4b73f 100644 --- a/lib/src/model/notifications/challenge_notification.dart +++ b/lib/src/model/notifications/challenge_notification.dart @@ -2,7 +2,7 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:lichess_mobile/l10n/l10n.dart'; import 'package:lichess_mobile/src/model/challenge/challenge.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/model/notifications/local_notification.dart'; +import 'package:lichess_mobile/src/model/notifications/notification_service.dart'; class ChallengeNotification implements LocalNotification { ChallengeNotification(this._challenge, this._l10n); diff --git a/lib/src/model/notifications/local_notification.dart b/lib/src/model/notifications/local_notification.dart deleted file mode 100644 index f614d79024..0000000000 --- a/lib/src/model/notifications/local_notification.dart +++ /dev/null @@ -1,170 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:ui'; - -import 'package:flutter_local_notifications/flutter_local_notifications.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:lichess_mobile/l10n/l10n.dart'; -import 'package:lichess_mobile/src/model/challenge/challenge_service.dart'; -import 'package:logging/logging.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -import 'challenge_notification.dart'; - -part 'local_notification.g.dart'; -part 'local_notification.freezed.dart'; - -final _logger = Logger('LocalNotification'); - -final _notificationPlugin = FlutterLocalNotificationsPlugin(); - -/// A notification response and its id and payload. -typedef ParsedNotificationResponse = ( - int, - NotificationResponse, - NotificationPayload -); - -final StreamController _responseStreamController = - StreamController.broadcast(); - -/// Stream of locale notification responses (when the user interacts with a notification). -final Stream localNotificationResponseStream = - _responseStreamController.stream; - -enum NotificationType { - info, - challenge, -} - -/// A service that manages local notifications. -class LocalNotificationService { - const LocalNotificationService._(this._log); - - static final instance = - LocalNotificationService._(Logger('LocalNotificationService')); - - final Logger _log; - - static Future initialize(Locale locale) async { - final l10n = await AppLocalizations.delegate.load(locale); - await _notificationPlugin.initialize( - InitializationSettings( - android: const AndroidInitializationSettings('logo_black'), - iOS: DarwinInitializationSettings( - requestBadgePermission: false, - notificationCategories: [ - ChallengeNotification.darwinPlayableVariantCategory(l10n), - ChallengeNotification.darwinUnplayableVariantCategory(l10n), - ], - ), - ), - onDidReceiveNotificationResponse: _onDidReceiveNotificationResponse, - // onDidReceiveBackgroundNotificationResponse: notificationTapBackground, - ); - } - - /// Function called by the flutter_local_notifications plugin when a notification is received in the foreground. - static void _onDidReceiveNotificationResponse(NotificationResponse response) { - _logger.info('processing response in foreground. id [${response.id}]'); - - if (response.id == null || response.payload == null) return; - - try { - final payload = NotificationPayload.fromJson( - jsonDecode(response.payload!) as Map, - ); - _responseStreamController.add((response.id!, response, payload)); - } catch (e) { - _logger.warning('Failed to parse notification payload: $e'); - } - } - - /// Show a local notification. - Future show(LocalNotification notification) async { - final id = notification.id; - final payload = notification.payload != null - ? jsonEncode(notification.payload!.toJson()) - : null; - - await _notificationPlugin.show( - id, - notification.title, - notification.body, - notification.details, - payload: payload, - ); - _log.info( - 'Sent notification: ($id | ${notification.title}) ${notification.body} (Payload: ${notification.payload})', - ); - - return id; - } - - /// Cancel a local notification. - Future cancel(int id) async { - _log.info('canceled notification id: [$id]'); - return _notificationPlugin.cancel(id); - } -} - -/// A service that dispatches user interaction responses from local notifications to the appropriate handlers. -class LocalNotificationDispatcher { - LocalNotificationDispatcher(this.ref); - - final LocalNotificationDispatcherRef ref; - - StreamSubscription? _responseSubscription; - - /// Start listening for notification responses. - void start() { - _responseSubscription = localNotificationResponseStream.listen( - (data) { - final (notifId, response, payload) = data; - switch (payload.type) { - case NotificationType.challenge: - ref.read(challengeServiceProvider).onNotificationResponse( - notifId, - response.actionId, - payload, - ); - case NotificationType.info: - break; - } - }, - ); - } - - void onDispose() { - _responseSubscription?.cancel(); - } -} - -@Riverpod(keepAlive: true) -LocalNotificationDispatcher localNotificationDispatcher( - LocalNotificationDispatcherRef ref, -) { - final service = LocalNotificationDispatcher(ref); - ref.onDispose(service.onDispose); - return service; -} - -@Freezed(fromJson: true, toJson: true) -class NotificationPayload with _$NotificationPayload { - factory NotificationPayload({ - required NotificationType type, - required Map data, - }) = _NotificationPayload; - - factory NotificationPayload.fromJson(Map json) => - _$NotificationPayloadFromJson(json); -} - -/// A local notification that can be shown to the user. -abstract class LocalNotification { - int get id; - String get title; - String? get body; - NotificationPayload? get payload; - NotificationDetails get details; -} diff --git a/lib/src/model/notifications/notification_service.dart b/lib/src/model/notifications/notification_service.dart new file mode 100644 index 0000000000..57a2a9a9d7 --- /dev/null +++ b/lib/src/model/notifications/notification_service.dart @@ -0,0 +1,402 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:lichess_mobile/firebase_options.dart'; +import 'package:lichess_mobile/l10n/l10n.dart'; +import 'package:lichess_mobile/src/init.dart'; +import 'package:lichess_mobile/src/model/account/account_repository.dart'; +import 'package:lichess_mobile/src/model/auth/auth_session.dart'; +import 'package:lichess_mobile/src/model/challenge/challenge_service.dart'; +import 'package:lichess_mobile/src/model/common/http.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/correspondence/correspondence_service.dart'; +import 'package:lichess_mobile/src/model/game/playable_game.dart'; +import 'package:lichess_mobile/src/model/notifications/challenge_notification.dart'; +import 'package:lichess_mobile/src/utils/badge_service.dart'; +import 'package:lichess_mobile/src/utils/connectivity.dart'; +import 'package:logging/logging.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'notification_service.g.dart'; +part 'notification_service.freezed.dart'; + +final _localNotificationPlugin = FlutterLocalNotificationsPlugin(); +final _logger = Logger('NotificationService'); + +/// A notification response with its ID and payload. +typedef NotificationResponseData = ({ + /// The notification id. + int id, + + /// The id of the action that was triggered. + String? actionId, + + /// The value of the input field if the notification action had an input + /// field. + String? input, + + /// The parsed notification payload. + NotificationPayload? payload, +}); + +enum NotificationType { + corresGameUpdate, + challenge, +} + +@Freezed(fromJson: true, toJson: true) +class NotificationPayload with _$NotificationPayload { + factory NotificationPayload({ + required NotificationType type, + required Map data, + }) = _NotificationPayload; + + factory NotificationPayload.fromJson(Map json) => + _$NotificationPayloadFromJson(json); +} + +/// A local notification that can be shown to the user. +abstract class LocalNotification { + int get id; + String get title; + String? get body; + NotificationPayload? get payload; + NotificationDetails get details; +} + +@Riverpod(keepAlive: true) +NotificationService notificationService(NotificationServiceRef ref) { + final service = NotificationService(ref); + + ref.onDispose(() => service._dispose()); + + return service; +} + +/// A service that manages notifications. +/// +/// This service is responsible for handling incoming messages from the Firebase +/// Cloud Messaging service and updating the application state accordingly. +class NotificationService { + NotificationService(this._ref); + + final NotificationServiceRef _ref; + + StreamSubscription? _fcmTokenRefreshSubscription; + ProviderSubscription>? + _connectivitySubscription; + + static final StreamController + _responseStreamController = StreamController.broadcast(); + + /// Stream of locale notification responses (when the user interacts with a notification). + static final Stream responseStream = + _responseStreamController.stream; + + StreamSubscription? _responseStreamSubscription; + + bool _registeredDevice = false; + + /// Initialize the notification service. + /// + /// It will initialize the Firebase and the local notification plugins. + /// + /// This should be called once when the app starts. + static Future initialize(Locale locale) async { + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); + + final l10n = await AppLocalizations.delegate.load(locale); + await _localNotificationPlugin.initialize( + InitializationSettings( + android: const AndroidInitializationSettings('logo_black'), + iOS: DarwinInitializationSettings( + requestBadgePermission: false, + notificationCategories: [ + ChallengeNotification.darwinPlayableVariantCategory(l10n), + ChallengeNotification.darwinUnplayableVariantCategory(l10n), + ], + ), + ), + onDidReceiveNotificationResponse: _onDidReceiveNotificationResponse, + // onDidReceiveBackgroundNotificationResponse: notificationTapBackground, + ); + } + + /// Starts the remote notification service. + /// + /// This method listens for incoming messages and updates the application state + /// accordingly. + /// It also registers the device for push notifications once the app is online. + /// + /// This method should be called once the app is ready to receive notifications and after [initialize]. + Future start() async { + _connectivitySubscription = + _ref.listen(connectivityChangesProvider, (prev, current) async { + // register device once the app is online + if (current.value?.isOnline == true && !_registeredDevice) { + try { + await registerDevice(); + _registeredDevice = true; + } catch (e, st) { + debugPrint('Could not setup push notifications; $e\n$st'); + } + } + }); + + // Listen for incoming messages while the app is in the foreground. + FirebaseMessaging.onMessage.listen((RemoteMessage message) { + _logger.fine('processing FCM message received in foreground: $message'); + _processFcmMessage(message, fromBackground: false); + }); + + // Listen for incoming messages while the app is in the background. + FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler); + + // Request permission to receive notifications. Pop-up will appear only + // once. + await FirebaseMessaging.instance.requestPermission( + alert: true, + badge: true, + sound: true, + announcement: false, + carPlay: false, + criticalAlert: false, + provisional: false, + ); + + // Listen for token refresh and update the token on the server accordingly. + _fcmTokenRefreshSubscription = + FirebaseMessaging.instance.onTokenRefresh.listen((String token) { + _registerToken(token); + }); + + // Get any messages which caused the application to open from + // a terminated state. + final RemoteMessage? initialMessage = + await FirebaseMessaging.instance.getInitialMessage(); + + if (initialMessage != null) { + _handleFcmMessageOpenedApp(initialMessage); + } + + // Handle any other interaction that caused the app to open when in background. + FirebaseMessaging.onMessageOpenedApp.listen(_handleFcmMessageOpenedApp); + + // start listening for notification responses + _responseStreamSubscription = responseStream.listen( + (data) => _dispatchNotificationResponse(data), + ); + } + + /// Shows a notification. + Future show(LocalNotification notification) async { + final id = notification.id; + final payload = notification.payload != null + ? jsonEncode(notification.payload!.toJson()) + : null; + + await _localNotificationPlugin.show( + id, + notification.title, + notification.body, + notification.details, + payload: payload, + ); + _logger.info( + 'Show local notification: ($id | ${notification.title}) ${notification.body} (Payload: ${notification.payload})', + ); + + return id; + } + + /// Cancels/removes a notification. + Future cancel(int id) async { + _logger.info('canceled notification id: [$id]'); + return _localNotificationPlugin.cancel(id); + } + + void _dispose() { + _fcmTokenRefreshSubscription?.cancel(); + _connectivitySubscription?.close(); + _responseStreamSubscription?.cancel(); + } + + void _dispatchNotificationResponse(NotificationResponseData data) { + final (id: id, payload: payload, actionId: actionId, input: _) = data; + + if (payload == null) return; + + switch (payload.type) { + case NotificationType.challenge: + _ref.read(challengeServiceProvider).onNotificationResponse( + id, + actionId, + payload, + ); + case NotificationType.corresGameUpdate: + // TODO handle corres game update notifs + break; + } + } + + /// Function called by the flutter_local_notifications plugin when the user interacts with a notification that causes the app to open. + static void _onDidReceiveNotificationResponse(NotificationResponse response) { + _logger.info('processing response in foreground. id [${response.id}]'); + + if (response.id == null) return; + + try { + final payload = response.payload != null + ? NotificationPayload.fromJson( + jsonDecode(response.payload!) as Map, + ) + : null; + _responseStreamController.add( + ( + id: response.id!, + actionId: response.actionId, + input: response.input, + payload: payload, + ), + ); + } catch (e) { + _logger.warning('Failed to parse notification payload: $e'); + } + } + + /// Handle an FCM message that caused the application to open + void _handleFcmMessageOpenedApp(RemoteMessage message) { + switch (message.data['lichess.type']) { + // correspondence game message types + // TODO: handle other message types + case 'corresAlarm': + case 'gameTakebackOffer': + case 'gameDrawOffer': + case 'gameMove': + case 'gameFinish': + final gameFullId = message.data['lichess.fullId'] as String?; + if (gameFullId != null) { + _ref.read(correspondenceServiceProvider).onNotificationResponse( + GameFullId(gameFullId), + ); + } + } + } + + /// Register the device for push notifications. + Future registerDevice() async { + final token = await FirebaseMessaging.instance.getToken(); + if (token != null) { + await _registerToken(token); + } + } + + Future _registerToken(String token) async { + final settings = await FirebaseMessaging.instance.getNotificationSettings(); + if (settings.authorizationStatus == AuthorizationStatus.denied) { + return; + } + _logger.info('will register fcmToken: $token'); + final session = _ref.read(authSessionProvider); + if (session == null) { + return; + } + try { + await _ref.withClient( + (client) => client.post(Uri(path: '/mobile/register/firebase/$token')), + ); + } catch (e, st) { + _logger.severe('could not register device; $e', e, st); + } + } + + /// Unregister the device from push notifications. + Future unregister() async { + _logger.info('will unregister'); + final session = _ref.read(authSessionProvider); + if (session == null) { + return; + } + try { + await _ref.withClient( + (client) => client.post(Uri(path: '/mobile/unregister')), + ); + } catch (e, st) { + _logger.severe('could not unregister device; $e', e, st); + } + } + + /// Process a message received from the Firebase Cloud Messaging service. + Future _processFcmMessage( + RemoteMessage message, { + required bool fromBackground, + }) async { + final gameFullId = message.data['lichess.fullId'] as String?; + final round = message.data['lichess.round'] as String?; + + // update correspondence game + if (gameFullId != null && round != null) { + final fullId = GameFullId(gameFullId); + final game = PlayableGame.fromServerJson( + jsonDecode(round) as Map, + ); + _ref.read(correspondenceServiceProvider).updateGame(fullId, game); + + if (!fromBackground) { + // opponent just played, invalidate ongoing games + if (game.sideToMove == game.youAre) { + _ref.invalidate(ongoingGamesProvider); + } + } + } + + // update badge + final badge = message.data['lichess.iosBadge'] as String?; + if (badge != null) { + try { + BadgeService.instance.setBadge(int.parse(badge)); + } catch (e) { + _logger.severe('Could not parse badge: $badge'); + } + } + } + + @pragma('vm:entry-point') + static Future _firebaseMessagingBackgroundHandler( + RemoteMessage message, + ) async { + debugPrint('Handling a FCM background message: ${message.data}'); + + // create a new provider scope for the background isolate + final ref = ProviderContainer(); + + ref.listen( + cachedDataProvider, + (prev, now) { + if (!now.hasValue) return; + + try { + ref.read(notificationServiceProvider)._processFcmMessage( + message, + fromBackground: true, + ); + + ref.dispose(); + } catch (e) { + debugPrint('Error when processing an FCM background message: $e'); + ref.dispose(); + } + }, + ); + + ref.read(cachedDataProvider); + } +} diff --git a/lib/src/model/notifications/push_notification_service.dart b/lib/src/model/notifications/push_notification_service.dart deleted file mode 100644 index 21c77e98da..0000000000 --- a/lib/src/model/notifications/push_notification_service.dart +++ /dev/null @@ -1,236 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; - -import 'package:firebase_messaging/firebase_messaging.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lichess_mobile/src/init.dart'; -import 'package:lichess_mobile/src/model/account/account_repository.dart'; -import 'package:lichess_mobile/src/model/auth/auth_session.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; -import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/model/correspondence/correspondence_service.dart'; -import 'package:lichess_mobile/src/model/game/playable_game.dart'; -import 'package:lichess_mobile/src/utils/badge_service.dart'; -import 'package:lichess_mobile/src/utils/connectivity.dart'; -import 'package:logging/logging.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'push_notification_service.g.dart'; - -@Riverpod(keepAlive: true) -PushNotificationService pushNotificationService( - PushNotificationServiceRef ref, -) { - final service = PushNotificationService( - Logger('PushNotificationService'), - ref: ref, - ); - - ref.onDispose(() => service._dispose()); - - return service; -} - -class PushNotificationService { - PushNotificationService(this._log, {required this.ref}); - - final PushNotificationServiceRef ref; - final Logger _log; - - StreamSubscription? _fcmTokenRefreshSubscription; - ProviderSubscription>? - _connectivitySubscription; - - bool _registeredDevice = false; - - /// Starts the push notification service. - Future start() async { - _connectivitySubscription = - ref.listen(connectivityChangesProvider, (prev, current) async { - // register device once the app is online - if (current.value?.isOnline == true && !_registeredDevice) { - try { - await registerDevice(); - _registeredDevice = true; - } catch (e, st) { - debugPrint('Could not setup push notifications; $e\n$st'); - } - } - }); - - // Listen for incoming messages while the app is in the foreground. - FirebaseMessaging.onMessage.listen((RemoteMessage message) { - _log.fine('processing message received in foreground: $message'); - _processDataMessage(message, fromBackground: false); - }); - - // Listen for incoming messages while the app is in the background. - FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler); - - // Request permission to receive notifications. Pop-up will appear only - // once. - await FirebaseMessaging.instance.requestPermission( - alert: true, - badge: true, - sound: true, - announcement: false, - carPlay: false, - criticalAlert: false, - provisional: false, - ); - - // Listen for token refresh and update the token on the server accordingly. - _fcmTokenRefreshSubscription = - FirebaseMessaging.instance.onTokenRefresh.listen((String token) { - _registerToken(token); - }); - - // Get any messages which caused the application to open from - // a terminated state. - final RemoteMessage? initialMessage = - await FirebaseMessaging.instance.getInitialMessage(); - - if (initialMessage != null) { - _handleMessage(initialMessage); - } - - // Also handle any interaction when the app is in the background via a - // Stream listener - FirebaseMessaging.onMessageOpenedApp.listen(_handleMessage); - } - - void _dispose() { - _fcmTokenRefreshSubscription?.cancel(); - _connectivitySubscription?.close(); - } - - /// Handle a message that caused the application to open - /// - /// This method must be part of a State object which is a child of [MaterialApp] - /// otherwise the [Navigator] will not be accessible. - void _handleMessage(RemoteMessage message) { - switch (message.data['lichess.type']) { - // correspondence game message types - case 'corresAlarm': - case 'gameTakebackOffer': - case 'gameDrawOffer': - case 'gameMove': - case 'gameFinish': - final gameFullId = message.data['lichess.fullId'] as String?; - if (gameFullId != null) { - ref.read(correspondenceServiceProvider).onNotificationResponse( - GameFullId(gameFullId), - ); - } - } - } - - /// Register the device for push notifications. - Future registerDevice() async { - final token = await FirebaseMessaging.instance.getToken(); - if (token != null) { - await _registerToken(token); - } - } - - Future _registerToken(String token) async { - final settings = await FirebaseMessaging.instance.getNotificationSettings(); - if (settings.authorizationStatus == AuthorizationStatus.denied) { - return; - } - _log.info('will register fcmToken: $token'); - final session = ref.read(authSessionProvider); - if (session == null) { - return; - } - try { - await ref.withClient( - (client) => client.post(Uri(path: '/mobile/register/firebase/$token')), - ); - } catch (e, st) { - _log.severe('could not register device; $e', e, st); - } - } - - /// Unregister the device from push notifications. - Future unregister() async { - _log.info('will unregister'); - final session = ref.read(authSessionProvider); - if (session == null) { - return; - } - try { - await ref.withClient( - (client) => client.post(Uri(path: '/mobile/unregister')), - ); - } catch (e, st) { - _log.severe('could not unregister device; $e', e, st); - } - } - - /// Process a message received - Future _processDataMessage( - RemoteMessage message, { - required bool fromBackground, - }) async { - final gameFullId = message.data['lichess.fullId'] as String?; - final round = message.data['lichess.round'] as String?; - - // update correspondence game - if (gameFullId != null && round != null) { - final fullId = GameFullId(gameFullId); - final game = PlayableGame.fromServerJson( - jsonDecode(round) as Map, - ); - ref.read(correspondenceServiceProvider).updateGame(fullId, game); - - if (!fromBackground) { - // opponent just played, invalidate ongoing games - if (game.sideToMove == game.youAre) { - ref.invalidate(ongoingGamesProvider); - } - } - } - - // update badge - final badge = message.data['lichess.iosBadge'] as String?; - if (badge != null) { - try { - BadgeService.instance.setBadge(int.parse(badge)); - } catch (e) { - _log.severe('Could not parse badge: $badge'); - } - } - } - - @pragma('vm:entry-point') - static Future _firebaseMessagingBackgroundHandler( - RemoteMessage message, - ) async { - debugPrint('Handling a fcm background message: ${message.data}'); - - // create a new provider scope for the background isolate - final ref = ProviderContainer(); - - ref.listen( - cachedDataProvider, - (prev, now) { - if (!now.hasValue) return; - try { - ref.read(pushNotificationServiceProvider)._processDataMessage( - message, - fromBackground: true, - ); - - ref.dispose(); - } catch (e) { - debugPrint('Error processing background message: $e'); - ref.dispose(); - } - }, - ); - - ref.read(cachedDataProvider); - } -} diff --git a/lib/src/view/user/challenge_requests_screen.dart b/lib/src/view/user/challenge_requests_screen.dart index 4ddd0750e2..98ecaea174 100644 --- a/lib/src/view/user/challenge_requests_screen.dart +++ b/lib/src/view/user/challenge_requests_screen.dart @@ -4,7 +4,7 @@ import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/challenge/challenge.dart'; import 'package:lichess_mobile/src/model/challenge/challenge_repository.dart'; import 'package:lichess_mobile/src/model/challenge/challenges.dart'; -import 'package:lichess_mobile/src/model/notifications/local_notification.dart'; +import 'package:lichess_mobile/src/model/notifications/notification_service.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; @@ -77,7 +77,8 @@ class _Body extends ConsumerWidget { ref .read(challengeRepositoryProvider) .decline(challenge.id, reason: reason); - LocalNotificationService.instance + ref + .read(notificationServiceProvider) .cancel(challenge.id.value.hashCode); } diff --git a/test/fake_notification_service.dart b/test/fake_notification_service.dart deleted file mode 100644 index 6ed06f8d79..0000000000 --- a/test/fake_notification_service.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:lichess_mobile/src/model/notifications/push_notification_service.dart'; - -class FakeNotificationService implements PushNotificationService { - @override - Future start() async {} - - @override - PushNotificationServiceRef get ref => throw UnimplementedError(); - - @override - Future registerDevice() async {} - - @override - Future unregister() async {} -} diff --git a/test/model/notifications/fake_notification_service.dart b/test/model/notifications/fake_notification_service.dart new file mode 100644 index 0000000000..559ed589be --- /dev/null +++ b/test/model/notifications/fake_notification_service.dart @@ -0,0 +1,25 @@ +import 'package:lichess_mobile/src/model/notifications/notification_service.dart'; + +class FakeNotificationService implements NotificationService { + Map notifications = {}; + + @override + Future start() async {} + + @override + Future registerDevice() async {} + + @override + Future unregister() async {} + + @override + Future cancel(int id) async { + notifications.remove(id); + } + + @override + Future show(LocalNotification notification) async { + notifications[notification.id] = notification; + return notification.id; + } +} diff --git a/test/test_app.dart b/test/test_app.dart index 6d39da67eb..1324673d92 100644 --- a/test/test_app.dart +++ b/test/test_app.dart @@ -20,7 +20,7 @@ import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/game/game_storage.dart'; -import 'package:lichess_mobile/src/model/notifications/push_notification_service.dart'; +import 'package:lichess_mobile/src/model/notifications/notification_service.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:logging/logging.dart'; @@ -31,9 +31,9 @@ import 'package:visibility_detector/visibility_detector.dart'; import './fake_crashlytics.dart'; import './model/auth/fake_session_storage.dart'; import './model/common/service/fake_sound_service.dart'; -import 'fake_notification_service.dart'; import 'model/common/fake_websocket_channel.dart'; import 'model/game/mock_game_storage.dart'; +import 'model/notifications/fake_notification_service.dart'; import 'utils/fake_connectivity_changes.dart'; final mockClient = MockClient((request) async { @@ -117,8 +117,7 @@ Future buildTestApp( return true; }), // ignore: scoped_providers_should_specify_dependencies - pushNotificationServiceProvider - .overrideWithValue(FakeNotificationService()), + notificationServiceProvider.overrideWithValue(FakeNotificationService()), // ignore: scoped_providers_should_specify_dependencies crashlyticsProvider.overrideWithValue(FakeCrashlytics()), // ignore: scoped_providers_should_specify_dependencies diff --git a/test/test_container.dart b/test/test_container.dart index 619940cbc9..8d88da7b36 100644 --- a/test/test_container.dart +++ b/test/test_container.dart @@ -13,7 +13,7 @@ import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; -import 'package:lichess_mobile/src/model/notifications/push_notification_service.dart'; +import 'package:lichess_mobile/src/model/notifications/notification_service.dart'; import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:logging/logging.dart'; import 'package:mocktail/mocktail.dart'; @@ -22,8 +22,8 @@ import 'package:shared_preferences/shared_preferences.dart'; import './fake_crashlytics.dart'; import './model/common/service/fake_sound_service.dart'; -import 'fake_notification_service.dart'; import 'model/common/fake_websocket_channel.dart'; +import 'model/notifications/fake_notification_service.dart'; import 'utils/fake_connectivity_changes.dart'; class MockHttpClient extends Mock implements http.Client {} @@ -81,8 +81,7 @@ Future makeContainer({ }), defaultClientProvider.overrideWithValue(MockHttpClient()), crashlyticsProvider.overrideWithValue(FakeCrashlytics()), - pushNotificationServiceProvider - .overrideWithValue(FakeNotificationService()), + notificationServiceProvider.overrideWithValue(FakeNotificationService()), soundServiceProvider.overrideWithValue(FakeSoundService()), sharedPreferencesProvider.overrideWithValue(sharedPreferences), cachedDataProvider.overrideWith((ref) { From 5d17743a8c3921927b5a6eb236bf5f2b5862f806 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 24 Sep 2024 19:38:31 +0200 Subject: [PATCH 390/979] More wip on notifications --- lib/src/app.dart | 19 ++ lib/src/db/database.dart | 8 +- .../model/common/service/sound_service.dart | 12 +- .../correspondence_game_storage.dart | 27 ++- .../correspondence_service.dart | 25 +- lib/src/model/game/game_controller.dart | 7 + .../notifications/challenge_notification.dart | 4 +- .../notifications/notification_service.dart | 219 ++++++++++++------ .../correspondence_game_storage_test.dart | 16 +- test/model/game/game_storage_test.dart | 27 +-- .../puzzle/puzzle_batch_storage_test.dart | 27 +-- test/model/puzzle/puzzle_service_test.dart | 11 - test/model/puzzle/puzzle_storage_test.dart | 2 +- test/test_app.dart | 9 + test/test_container.dart | 8 + 15 files changed, 246 insertions(+), 175 deletions(-) diff --git a/lib/src/app.dart b/lib/src/app.dart index 7d68dbebde..5bdfe576f4 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -95,8 +95,21 @@ class _AppState extends ConsumerState { /// Whether the app has checked for online status for the first time. bool _firstTimeOnlineCheck = false; + AppLifecycleListener? _appLifecycleListener; + @override void initState() { + debugPrint('AppState init'); + + _appLifecycleListener = AppLifecycleListener( + onResume: () async { + final online = await isOnline(ref.read(defaultClientProvider)); + if (online) { + ref.invalidate(ongoingGamesProvider); + } + }, + ); + // check if session is still active and delete it if it is not checkSession(); @@ -137,6 +150,12 @@ class _AppState extends ConsumerState { super.initState(); } + @override + void dispose() { + _appLifecycleListener?.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { final generalPrefs = ref.watch(generalPreferencesProvider); diff --git a/lib/src/db/database.dart b/lib/src/db/database.dart index ba4279d75f..410bd3247a 100644 --- a/lib/src/db/database.dart +++ b/lib/src/db/database.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:path/path.dart'; @@ -18,9 +19,7 @@ const kStorageAnonId = '**anonymous**'; @Riverpod(keepAlive: true) Future database(DatabaseRef ref) async { final dbPath = join(await getDatabasesPath(), kLichessDatabaseName); - final db = await openDb(databaseFactory, dbPath); - ref.onDispose(db.close); - return db; + return openAppDatabase(databaseFactory, dbPath); } /// Returns the sqlite version as an integer. @@ -49,7 +48,8 @@ Future getDbSizeInBytes(GetDbSizeInBytesRef ref) async { return dbFile.length(); } -Future openDb(DatabaseFactory dbFactory, String path) async { +/// Opens the app database. +Future openAppDatabase(DatabaseFactory dbFactory, String path) async { return dbFactory.openDatabase( path, options: OpenDatabaseOptions( diff --git a/lib/src/model/common/service/sound_service.dart b/lib/src/model/common/service/sound_service.dart index 98db8ad3cc..c6108473fc 100644 --- a/lib/src/model/common/service/sound_service.dart +++ b/lib/src/model/common/service/sound_service.dart @@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart' show rootBundle; import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; import 'package:lichess_mobile/src/model/settings/sound_theme.dart'; +import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:sound_effect/sound_effect.dart'; @@ -12,6 +13,8 @@ part 'sound_service.g.dart'; final _soundEffectPlugin = SoundEffect(); +final _logger = Logger('SoundService'); + // Must match name of files in assets/sounds/standard enum Sound { move, @@ -83,8 +86,13 @@ class SoundService { ) : GeneralPrefsState.defaults) .soundTheme; - await _soundEffectPlugin.initialize(); - await _loadAllSounds(theme); + + try { + await _soundEffectPlugin.initialize(); + await _loadAllSounds(theme); + } catch (e) { + _logger.warning('Failed to initialize sound service: $e'); + } } /// Play the given sound if sound is enabled. diff --git a/lib/src/model/correspondence/correspondence_game_storage.dart b/lib/src/model/correspondence/correspondence_game_storage.dart index 7a56f11ca6..d28d4bb6d0 100644 --- a/lib/src/model/correspondence/correspondence_game_storage.dart +++ b/lib/src/model/correspondence/correspondence_game_storage.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/foundation.dart'; import 'package:lichess_mobile/src/db/database.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; @@ -127,17 +128,21 @@ class CorrespondenceGameStorage { } Future save(OfflineCorrespondenceGame game) async { - await _db.insert( - kCorrespondenceStorageTable, - { - 'userId': game.me.user?.id.toString() ?? kCorrespondenceStorageAnonId, - 'gameId': game.id.toString(), - 'lastModified': DateTime.now().toIso8601String(), - 'data': jsonEncode(game.toJson()), - }, - conflictAlgorithm: ConflictAlgorithm.replace, - ); - ref.invalidate(offlineOngoingCorrespondenceGamesProvider); + try { + await _db.insert( + kCorrespondenceStorageTable, + { + 'userId': game.me.user?.id.toString() ?? kCorrespondenceStorageAnonId, + 'gameId': game.id.toString(), + 'lastModified': DateTime.now().toIso8601String(), + 'data': jsonEncode(game.toJson()), + }, + conflictAlgorithm: ConflictAlgorithm.replace, + ); + ref.invalidate(offlineOngoingCorrespondenceGamesProvider); + } catch (e) { + debugPrint('[CorrespondenceGameStorage] failed to save game: $e'); + } } Future delete(GameId gameId) async { diff --git a/lib/src/model/correspondence/correspondence_service.dart b/lib/src/model/correspondence/correspondence_service.dart index c1352ea64d..f1e89d5f2b 100644 --- a/lib/src/model/correspondence/correspondence_service.dart +++ b/lib/src/model/correspondence/correspondence_service.dart @@ -39,15 +39,16 @@ class CorrespondenceService { final CorrespondenceServiceRef ref; final Logger _log; - /// Handles a fcm notification response that caused the app to open. + /// Handles a notification response that caused the app to open. Future onNotificationResponse(GameFullId fullId) async { final context = ref.read(currentNavigatorKeyProvider).currentContext; if (context == null || !context.mounted) return; - final navState = Navigator.of(context); - if (navState.canPop()) { - navState.popUntil((route) => route.isFirst); + final rootNavState = Navigator.of(context, rootNavigator: true); + if (rootNavState.canPop()) { + rootNavState.popUntil((route) => route.isFirst); } + pushPlatformRoute( context, rootNavigator: true, @@ -205,6 +206,22 @@ class CorrespondenceService { return movesPlayed; } + /// Handles a game update event from the server. + Future onServerUpdateEvent( + GameFullId fullId, + PlayableGame game, { + required bool fromBackground, + }) async { + if (!fromBackground) { + // opponent just played, invalidate ongoing games + if (game.sideToMove == game.youAre) { + ref.invalidate(ongoingGamesProvider); + } + } + + await updateGame(fullId, game); + } + /// Updates a stored correspondence game. Future updateGame(GameFullId fullId, PlayableGame game) async { return (await ref.read(correspondenceGameStorageProvider.future)).save( diff --git a/lib/src/model/game/game_controller.dart b/lib/src/model/game/game_controller.dart index 79c42d253a..89b866cdc6 100644 --- a/lib/src/model/game/game_controller.dart +++ b/lib/src/model/game/game_controller.dart @@ -94,6 +94,13 @@ class GameController extends _$GameController { PlayableGame game = fullEvent.game; if (fullEvent.game.finished) { + if (fullEvent.game.meta.speed == Speed.correspondence) { + ref.invalidate(ongoingGamesProvider); + ref + .read(correspondenceServiceProvider) + .updateGame(gameFullId, fullEvent.game); + } + final result = await _getPostGameData(); game = result.fold( (data) => _mergePostGameData(game, data, rewriteSteps: true), diff --git a/lib/src/model/notifications/challenge_notification.dart b/lib/src/model/notifications/challenge_notification.dart index f827f4b73f..8b4658cf0c 100644 --- a/lib/src/model/notifications/challenge_notification.dart +++ b/lib/src/model/notifications/challenge_notification.dart @@ -114,7 +114,7 @@ class ChallengePayload { final ChallengeId id; NotificationPayload get notificationPayload => NotificationPayload( - type: NotificationType.challenge, + type: AppNotificationType.challenge, data: { 'id': id.value, }, @@ -123,7 +123,7 @@ class ChallengePayload { factory ChallengePayload.fromNotification( NotificationPayload payload, ) { - assert(payload.type == NotificationType.challenge); + assert(payload.type == AppNotificationType.challenge); final id = payload.data['id'] as String; return ChallengePayload(ChallengeId(id)); } diff --git a/lib/src/model/notifications/notification_service.dart b/lib/src/model/notifications/notification_service.dart index 57a2a9a9d7..329627b5e0 100644 --- a/lib/src/model/notifications/notification_service.dart +++ b/lib/src/model/notifications/notification_service.dart @@ -10,7 +10,6 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/firebase_options.dart'; import 'package:lichess_mobile/l10n/l10n.dart'; import 'package:lichess_mobile/src/init.dart'; -import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/challenge/challenge_service.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; @@ -30,6 +29,9 @@ final _localNotificationPlugin = FlutterLocalNotificationsPlugin(); final _logger = Logger('NotificationService'); /// A notification response with its ID and payload. +/// +/// A notification response is the user's interaction with a notification shown +/// by the app. typedef NotificationResponseData = ({ /// The notification id. int id, @@ -45,15 +47,56 @@ typedef NotificationResponseData = ({ NotificationPayload? payload, }); -enum NotificationType { +/// The type of notification handled by the app. +enum AppNotificationType { corresGameUpdate, challenge, } +/// Notification types defined by the server. +/// +/// This corresponds to the 'lichess.type' field in the FCM message's data. +enum ServerNotificationType { + /// There is not much time left to make a move in a correspondence game. + corresAlarm, + + /// A takeback offer has been made in a correspondence game. + gameTakebackOffer, + + /// A draw offer has been made in a correspondence game. + gameDrawOffer, + + /// A move has been made in a correspondence game. + gameMove, + + /// A correspondence game just finished. + gameFinish, + + /// Server notification type not handled by the app. + unhandled; + + static ServerNotificationType fromString(String type) { + switch (type) { + case 'corresAlarm': + return corresAlarm; + case 'gameTakebackOffer': + return gameTakebackOffer; + case 'gameDrawOffer': + return gameDrawOffer; + case 'gameMove': + return gameMove; + case 'gameFinish': + return gameFinish; + default: + return unhandled; + } + } +} + @Freezed(fromJson: true, toJson: true) class NotificationPayload with _$NotificationPayload { factory NotificationPayload({ - required NotificationType type, + required AppNotificationType type, required Map data, }) = _NotificationPayload; @@ -82,25 +125,30 @@ NotificationService notificationService(NotificationServiceRef ref) { /// A service that manages notifications. /// /// This service is responsible for handling incoming messages from the Firebase -/// Cloud Messaging service and updating the application state accordingly. +/// Cloud Messaging service and showing notifications. +/// +/// It also listens for notification interaction responses and dispatches them to the +/// appropriate services. class NotificationService { NotificationService(this._ref); final NotificationServiceRef _ref; + /// The Firebase Cloud Messaging token refresh subscription. StreamSubscription? _fcmTokenRefreshSubscription; + + /// The connectivity changes stream subscription. ProviderSubscription>? _connectivitySubscription; + /// The stream controller for notification responses. static final StreamController _responseStreamController = StreamController.broadcast(); - /// Stream of locale notification responses (when the user interacts with a notification). - static final Stream responseStream = - _responseStreamController.stream; - + /// The stream subscription for notification responses. StreamSubscription? _responseStreamSubscription; + /// Whether the device has been registered for push notifications. bool _registeredDevice = false; /// Initialize the notification service. @@ -130,7 +178,7 @@ class NotificationService { ); } - /// Starts the remote notification service. + /// Starts the notification service. /// /// This method listens for incoming messages and updates the application state /// accordingly. @@ -146,14 +194,13 @@ class NotificationService { await registerDevice(); _registeredDevice = true; } catch (e, st) { - debugPrint('Could not setup push notifications; $e\n$st'); + _logger.severe('Could not setup push notifications; $e\n$st'); } } }); // Listen for incoming messages while the app is in the foreground. FirebaseMessaging.onMessage.listen((RemoteMessage message) { - _logger.fine('processing FCM message received in foreground: $message'); _processFcmMessage(message, fromBackground: false); }); @@ -191,7 +238,7 @@ class NotificationService { FirebaseMessaging.onMessageOpenedApp.listen(_handleFcmMessageOpenedApp); // start listening for notification responses - _responseStreamSubscription = responseStream.listen( + _responseStreamSubscription = _responseStreamController.stream.listen( (data) => _dispatchNotificationResponse(data), ); } @@ -235,13 +282,13 @@ class NotificationService { if (payload == null) return; switch (payload.type) { - case NotificationType.challenge: + case AppNotificationType.challenge: _ref.read(challengeServiceProvider).onNotificationResponse( id, actionId, payload, ); - case NotificationType.corresGameUpdate: + case AppNotificationType.corresGameUpdate: // TODO handle corres game update notifs break; } @@ -274,20 +321,79 @@ class NotificationService { /// Handle an FCM message that caused the application to open void _handleFcmMessageOpenedApp(RemoteMessage message) { - switch (message.data['lichess.type']) { - // correspondence game message types - // TODO: handle other message types - case 'corresAlarm': - case 'gameTakebackOffer': - case 'gameDrawOffer': - case 'gameMove': - case 'gameFinish': + final messageType = message.data['lichess.type'] as String?; + if (messageType == null) return; + + switch (ServerNotificationType.fromString(messageType)) { + case ServerNotificationType.corresAlarm: + case ServerNotificationType.gameTakebackOffer: + case ServerNotificationType.gameDrawOffer: + case ServerNotificationType.gameMove: + case ServerNotificationType.gameFinish: final gameFullId = message.data['lichess.fullId'] as String?; if (gameFullId != null) { _ref.read(correspondenceServiceProvider).onNotificationResponse( GameFullId(gameFullId), ); } + case ServerNotificationType.unhandled: + _logger + .warning('Received unhandled FCM notification type: $messageType'); + } + } + + /// Process a message received from the Firebase Cloud Messaging service. + /// + /// If the message contains a [RemoteMessage.notification] field, it will show + /// a local notification to the user. + /// + /// Some messages (whether or not they have an associated notification), have + /// a [RemoteMessage.data] field used to update the application state. + Future _processFcmMessage( + RemoteMessage message, { + required bool fromBackground, + }) async { + _logger.fine( + 'Processing a FCM message from ${fromBackground ? 'background' : 'foreground'}: ${message.data}', + ); + + final messageType = message.data['lichess.type'] as String?; + if (messageType != null) { + switch (ServerNotificationType.fromString(messageType)) { + case ServerNotificationType.corresAlarm: + case ServerNotificationType.gameTakebackOffer: + case ServerNotificationType.gameDrawOffer: + case ServerNotificationType.gameMove: + case ServerNotificationType.gameFinish: + final gameFullId = message.data['lichess.fullId'] as String?; + final round = message.data['lichess.round'] as String?; + if (gameFullId != null && round != null) { + final fullId = GameFullId(gameFullId); + final game = PlayableGame.fromServerJson( + jsonDecode(round) as Map, + ); + await _ref.read(correspondenceServiceProvider).onServerUpdateEvent( + fullId, + game, + fromBackground: fromBackground, + ); + } + + case ServerNotificationType.unhandled: + _logger.warning( + 'Received unhandled FCM notification type: $messageType', + ); + } + } + + // update badge + final badge = message.data['lichess.iosBadge'] as String?; + if (badge != null) { + try { + await BadgeService.instance.setBadge(int.parse(badge)); + } catch (e) { + _logger.severe('Could not parse badge: $badge'); + } } } @@ -299,73 +405,38 @@ class NotificationService { } } - Future _registerToken(String token) async { - final settings = await FirebaseMessaging.instance.getNotificationSettings(); - if (settings.authorizationStatus == AuthorizationStatus.denied) { - return; - } - _logger.info('will register fcmToken: $token'); + /// Unregister the device from push notifications. + Future unregister() async { + _logger.info('will unregister'); final session = _ref.read(authSessionProvider); if (session == null) { return; } try { await _ref.withClient( - (client) => client.post(Uri(path: '/mobile/register/firebase/$token')), + (client) => client.post(Uri(path: '/mobile/unregister')), ); } catch (e, st) { - _logger.severe('could not register device; $e', e, st); + _logger.severe('could not unregister device; $e', e, st); } } - /// Unregister the device from push notifications. - Future unregister() async { - _logger.info('will unregister'); + Future _registerToken(String token) async { + final settings = await FirebaseMessaging.instance.getNotificationSettings(); + if (settings.authorizationStatus == AuthorizationStatus.denied) { + return; + } + _logger.info('will register fcmToken: $token'); final session = _ref.read(authSessionProvider); if (session == null) { return; } try { await _ref.withClient( - (client) => client.post(Uri(path: '/mobile/unregister')), + (client) => client.post(Uri(path: '/mobile/register/firebase/$token')), ); } catch (e, st) { - _logger.severe('could not unregister device; $e', e, st); - } - } - - /// Process a message received from the Firebase Cloud Messaging service. - Future _processFcmMessage( - RemoteMessage message, { - required bool fromBackground, - }) async { - final gameFullId = message.data['lichess.fullId'] as String?; - final round = message.data['lichess.round'] as String?; - - // update correspondence game - if (gameFullId != null && round != null) { - final fullId = GameFullId(gameFullId); - final game = PlayableGame.fromServerJson( - jsonDecode(round) as Map, - ); - _ref.read(correspondenceServiceProvider).updateGame(fullId, game); - - if (!fromBackground) { - // opponent just played, invalidate ongoing games - if (game.sideToMove == game.youAre) { - _ref.invalidate(ongoingGamesProvider); - } - } - } - - // update badge - final badge = message.data['lichess.iosBadge'] as String?; - if (badge != null) { - try { - BadgeService.instance.setBadge(int.parse(badge)); - } catch (e) { - _logger.severe('Could not parse badge: $badge'); - } + _logger.severe('could not register device; $e', e, st); } } @@ -373,25 +444,23 @@ class NotificationService { static Future _firebaseMessagingBackgroundHandler( RemoteMessage message, ) async { - debugPrint('Handling a FCM background message: ${message.data}'); - // create a new provider scope for the background isolate final ref = ProviderContainer(); ref.listen( cachedDataProvider, - (prev, now) { + (prev, now) async { if (!now.hasValue) return; try { - ref.read(notificationServiceProvider)._processFcmMessage( + await ref.read(notificationServiceProvider)._processFcmMessage( message, fromBackground: true, ); ref.dispose(); } catch (e) { - debugPrint('Error when processing an FCM background message: $e'); + _logger.severe('Error when processing an FCM background message: $e'); ref.dispose(); } }, diff --git a/test/model/correspondence/correspondence_game_storage_test.dart b/test/model/correspondence/correspondence_game_storage_test.dart index b62700c9be..58e901f34c 100644 --- a/test/model/correspondence/correspondence_game_storage_test.dart +++ b/test/model/correspondence/correspondence_game_storage_test.dart @@ -1,7 +1,6 @@ import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:lichess_mobile/src/db/database.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; @@ -13,26 +12,13 @@ import 'package:lichess_mobile/src/model/game/game_status.dart'; import 'package:lichess_mobile/src/model/game/material_diff.dart'; import 'package:lichess_mobile/src/model/game/player.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; -import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import '../../test_container.dart'; void main() { - final dbFactory = databaseFactoryFfi; - sqfliteFfiInit(); - group('CorrespondenceGameStorage', () { test('save and fetch data', () async { - final db = await openDb(dbFactory, inMemoryDatabasePath); - - final container = await makeContainer( - overrides: [ - databaseProvider.overrideWith((ref) { - ref.onDispose(db.close); - return db; - }), - ], - ); + final container = await makeContainer(); final storage = await container.read(correspondenceGameStorageProvider.future); diff --git a/test/model/game/game_storage_test.dart b/test/model/game/game_storage_test.dart index aaf562eb26..acf60a92a2 100644 --- a/test/model/game/game_storage_test.dart +++ b/test/model/game/game_storage_test.dart @@ -1,7 +1,6 @@ import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:lichess_mobile/src/db/database.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; @@ -13,26 +12,13 @@ import 'package:lichess_mobile/src/model/game/game_storage.dart'; import 'package:lichess_mobile/src/model/game/material_diff.dart'; import 'package:lichess_mobile/src/model/game/player.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; -import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import '../../test_container.dart'; void main() { - final dbFactory = databaseFactoryFfi; - sqfliteFfiInit(); - group('GameStorage', () { test('save and fetch data', () async { - final db = await openDb(dbFactory, inMemoryDatabasePath); - - final container = await makeContainer( - overrides: [ - databaseProvider.overrideWith((ref) { - ref.onDispose(db.close); - return db; - }), - ], - ); + final container = await makeContainer(); final storage = await container.read(gameStorageProvider.future); @@ -46,16 +32,7 @@ void main() { }); test('paginate games', () async { - final db = await openDb(dbFactory, inMemoryDatabasePath); - - final container = await makeContainer( - overrides: [ - databaseProvider.overrideWith((ref) { - ref.onDispose(db.close); - return db; - }), - ], - ); + final container = await makeContainer(); final storage = await container.read(gameStorageProvider.future); diff --git a/test/model/puzzle/puzzle_batch_storage_test.dart b/test/model/puzzle/puzzle_batch_storage_test.dart index bd8bd173c4..623304a96b 100644 --- a/test/model/puzzle/puzzle_batch_storage_test.dart +++ b/test/model/puzzle/puzzle_batch_storage_test.dart @@ -1,33 +1,19 @@ import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:lichess_mobile/src/db/database.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_angle.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_batch_storage.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_theme.dart'; -import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import '../../test_container.dart'; void main() { - final dbFactory = databaseFactoryFfi; - sqfliteFfiInit(); - group('PuzzleBatchStorage', () { test('save and fetch data', () async { - final db = await openDb(dbFactory, inMemoryDatabasePath); - - final container = await makeContainer( - overrides: [ - databaseProvider.overrideWith((ref) { - ref.onDispose(db.close); - return db; - }), - ], - ); + final container = await makeContainer(); final storage = await container.read(puzzleBatchStorageProvider.future); @@ -47,16 +33,7 @@ void main() { }); test('fetchSavedThemes', () async { - final db = await openDb(dbFactory, inMemoryDatabasePath); - - final container = await makeContainer( - overrides: [ - databaseProvider.overrideWith((ref) { - ref.onDispose(db.close); - return db; - }), - ], - ); + final container = await makeContainer(); final storage = await container.read(puzzleBatchStorageProvider.future); diff --git a/test/model/puzzle/puzzle_service_test.dart b/test/model/puzzle/puzzle_service_test.dart index c5ef75b830..6a23e38b7c 100644 --- a/test/model/puzzle/puzzle_service_test.dart +++ b/test/model/puzzle/puzzle_service_test.dart @@ -5,7 +5,6 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; -import 'package:lichess_mobile/src/db/database.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; @@ -14,24 +13,14 @@ import 'package:lichess_mobile/src/model/puzzle/puzzle_angle.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_batch_storage.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_service.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_theme.dart'; -import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import '../../test_container.dart'; import '../../test_utils.dart'; void main() { - final dbFactory = databaseFactoryFfi; - sqfliteFfiInit(); - Future makeTestContainer(MockClient mockClient) async { - final db = await openDb(dbFactory, inMemoryDatabasePath); - return makeContainer( overrides: [ - databaseProvider.overrideWith((ref) { - ref.onDispose(db.close); - return db; - }), lichessClientProvider.overrideWith((ref) { return LichessClient(mockClient, ref); }), diff --git a/test/model/puzzle/puzzle_storage_test.dart b/test/model/puzzle/puzzle_storage_test.dart index 878859ff40..88f6e9a9f4 100644 --- a/test/model/puzzle/puzzle_storage_test.dart +++ b/test/model/puzzle/puzzle_storage_test.dart @@ -16,7 +16,7 @@ void main() { group('PuzzleHistoryStorage', () { test('save and fetch data', () async { - final db = await openDb(dbFactory, inMemoryDatabasePath); + final db = await openAppDatabase(dbFactory, inMemoryDatabasePath); final container = await makeContainer( overrides: [ diff --git a/test/test_app.dart b/test/test_app.dart index 1324673d92..dab572bdc6 100644 --- a/test/test_app.dart +++ b/test/test_app.dart @@ -11,6 +11,7 @@ import 'package:http/testing.dart'; import 'package:intl/intl.dart'; import 'package:lichess_mobile/l10n/l10n.dart'; import 'package:lichess_mobile/src/crashlytics.dart'; +import 'package:lichess_mobile/src/db/database.dart'; import 'package:lichess_mobile/src/db/shared_preferences.dart'; import 'package:lichess_mobile/src/init.dart'; import 'package:lichess_mobile/src/model/account/account_preferences.dart'; @@ -26,6 +27,7 @@ import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:logging/logging.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:visibility_detector/visibility_detector.dart'; import './fake_crashlytics.dart'; @@ -90,6 +92,13 @@ Future buildTestApp( return ProviderScope( overrides: [ + // ignore: scoped_providers_should_specify_dependencies + databaseProvider.overrideWith((ref) async { + final db = + await openAppDatabase(databaseFactoryFfi, inMemoryDatabasePath); + ref.onDispose(db.close); + return db; + }), // ignore: scoped_providers_should_specify_dependencies lichessClientProvider.overrideWith((ref) { return LichessClient(mockClient, ref); diff --git a/test/test_container.dart b/test/test_container.dart index 8d88da7b36..564ae17565 100644 --- a/test/test_container.dart +++ b/test/test_container.dart @@ -7,6 +7,7 @@ import 'package:http/http.dart' as http; import 'package:http/testing.dart'; import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/crashlytics.dart'; +import 'package:lichess_mobile/src/db/database.dart'; import 'package:lichess_mobile/src/db/shared_preferences.dart'; import 'package:lichess_mobile/src/init.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; @@ -19,6 +20,7 @@ import 'package:logging/logging.dart'; import 'package:mocktail/mocktail.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import './fake_crashlytics.dart'; import './model/common/service/fake_sound_service.dart'; @@ -65,6 +67,12 @@ Future makeContainer({ final container = ProviderContainer( overrides: [ + databaseProvider.overrideWith((ref) async { + final db = + await openAppDatabase(databaseFactoryFfi, inMemoryDatabasePath); + ref.onDispose(db.close); + return db; + }), webSocketChannelFactoryProvider.overrideWith((ref) { return FakeWebSocketChannelFactory(() => FakeWebSocketChannel()); }), From 5800b7497d78d35c312d9ad34e0eca6cf035d441 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Tue, 24 Sep 2024 19:47:32 +0200 Subject: [PATCH 391/979] move orientation to coordinate training controller state and randomize orientation when abort or new training button is pressed instead of start training --- .../coordinate_training_controller.dart | 29 ++++++++-- .../coordinate_training_screen.dart | 54 ++----------------- 2 files changed, 28 insertions(+), 55 deletions(-) diff --git a/lib/src/model/coordinate_training/coordinate_training_controller.dart b/lib/src/model/coordinate_training/coordinate_training_controller.dart index 9441f428d5..23480028a7 100644 --- a/lib/src/model/coordinate_training/coordinate_training_controller.dart +++ b/lib/src/model/coordinate_training/coordinate_training_controller.dart @@ -3,6 +3,7 @@ import 'dart:math'; import 'package:dartchess/dartchess.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:lichess_mobile/src/model/coordinate_training/coordinate_training_preferences.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'coordinate_training_controller.freezed.dart'; @@ -20,10 +21,13 @@ class CoordinateTrainingController extends _$CoordinateTrainingController { @override CoordinateTrainingState build() { + final trainingPrefs = ref.watch(coordinateTrainingPreferencesProvider); ref.onDispose(() { _updateTimer?.cancel(); }); - return const CoordinateTrainingState(); + return CoordinateTrainingState( + orientation: _getOrientation(trainingPrefs.sideChoice), + ); } void startTraining(Duration? timeLimit) { @@ -58,18 +62,34 @@ class CoordinateTrainingController extends _$CoordinateTrainingController { state = CoordinateTrainingState( lastGuess: state.lastGuess, lastScore: state.score, + orientation: state.orientation, ); } - void stopTraining() { + void abortTraining() { + final orientation = _getOrientation( + ref.read(coordinateTrainingPreferencesProvider).sideChoice, + ); _updateTimer?.cancel(); - state = const CoordinateTrainingState(); + state = CoordinateTrainingState(orientation: orientation); } void newTraining() { - state = const CoordinateTrainingState(); + final orientation = _getOrientation( + ref.read(coordinateTrainingPreferencesProvider).sideChoice, + ); + state = CoordinateTrainingState(orientation: orientation); } + Side _getOrientation(SideChoice choice) => switch (choice) { + SideChoice.white => Side.white, + SideChoice.black => Side.black, + SideChoice.random => _randomSide(), + }; + + /// Generate a random side + Side _randomSide() => Side.values[Random().nextInt(Side.values.length)]; + /// Generate a random square that is not the same as the [previous] square Square _randomCoord({Square? previous}) { while (true) { @@ -108,6 +128,7 @@ class CoordinateTrainingState with _$CoordinateTrainingState { @Default(null) Duration? timeLimit, @Default(null) Duration? elapsed, @Default(null) Guess? lastGuess, + required Side orientation, @Default(null) int? lastScore, }) = _CoordinateTrainingState; diff --git a/lib/src/view/coordinate_training/coordinate_training_screen.dart b/lib/src/view/coordinate_training/coordinate_training_screen.dart index 845590f4b2..f1e30cf57a 100644 --- a/lib/src/view/coordinate_training/coordinate_training_screen.dart +++ b/lib/src/view/coordinate_training/coordinate_training_screen.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:math'; import 'package:chessground/chessground.dart'; import 'package:dartchess/dartchess.dart'; @@ -45,42 +44,10 @@ class _Body extends ConsumerStatefulWidget { } class _BodyState extends ConsumerState<_Body> { - late Side orientation; - - late bool computeRandomOrientation; - Square? highlightLastGuess; Timer? highlightTimer; - Side _randomSide() => Side.values[Random().nextInt(Side.values.length)]; - - void _setOrientation(SideChoice choice) { - setState(() { - orientation = switch (choice) { - SideChoice.white => Side.white, - SideChoice.black => Side.black, - SideChoice.random => _randomSide(), - }; - computeRandomOrientation = false; - }); - } - - void _maybeSetOrientation() { - setState(() { - if (computeRandomOrientation) { - orientation = _randomSide(); - } - computeRandomOrientation = true; - }); - } - - @override - void initState() { - super.initState(); - _setOrientation(ref.read(coordinateTrainingPreferencesProvider).sideChoice); - } - @override void dispose() { super.dispose(); @@ -155,7 +122,7 @@ class _BodyState extends ConsumerState<_Body> { _TrainingBoard( boardSize: boardSize, isTablet: isTablet, - orientation: orientation, + orientation: trainingState.orientation, squareHighlights: squareHighlights, onGuess: _onGuess, ), @@ -169,7 +136,7 @@ class _BodyState extends ConsumerState<_Body> { .read( coordinateTrainingControllerProvider.notifier, ) - .stopTraining, + .abortTraining, label: 'Abort Training', ) else if (trainingState.lastScore != null) @@ -185,10 +152,7 @@ class _BodyState extends ConsumerState<_Body> { ) else Expanded( - child: _Settings( - onSideChoiceSelected: _setOrientation, - maybeSetOrientation: _maybeSetOrientation, - ), + child: _Settings(), ), ], ); @@ -386,14 +350,6 @@ class _Score extends StatelessWidget { } class _Settings extends ConsumerStatefulWidget { - const _Settings({ - required this.onSideChoiceSelected, - required this.maybeSetOrientation, - }); - - final void Function(SideChoice) onSideChoiceSelected; - final VoidCallback maybeSetOrientation; - @override ConsumerState<_Settings> createState() => _SettingsState(); } @@ -415,7 +371,6 @@ class _SettingsState extends ConsumerState<_Settings> { selected: trainingPrefs.sideChoice == choice, showCheckmark: false, onSelected: (selected) { - widget.onSideChoiceSelected(choice); ref .read(coordinateTrainingPreferencesProvider.notifier) .setSideChoice(choice); @@ -445,9 +400,6 @@ class _SettingsState extends ConsumerState<_Settings> { FatButton( semanticsLabel: 'Start Training', onPressed: () { - if (trainingPrefs.sideChoice == SideChoice.random) { - widget.maybeSetOrientation(); - } ref .read(coordinateTrainingControllerProvider.notifier) .startTraining(trainingPrefs.timeChoice.duration); From fb9be8fc1d22deae363e2e37b65e38b739d36c41 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Tue, 24 Sep 2024 23:48:29 +0200 Subject: [PATCH 392/979] fix state update --- .../coordinate_training/coordinate_training_controller.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/src/model/coordinate_training/coordinate_training_controller.dart b/lib/src/model/coordinate_training/coordinate_training_controller.dart index 23480028a7..cb15fbf7b6 100644 --- a/lib/src/model/coordinate_training/coordinate_training_controller.dart +++ b/lib/src/model/coordinate_training/coordinate_training_controller.dart @@ -21,12 +21,14 @@ class CoordinateTrainingController extends _$CoordinateTrainingController { @override CoordinateTrainingState build() { - final trainingPrefs = ref.watch(coordinateTrainingPreferencesProvider); ref.onDispose(() { _updateTimer?.cancel(); }); + final sideChoice = ref.watch( + coordinateTrainingPreferencesProvider.select((value) => value.sideChoice), + ); return CoordinateTrainingState( - orientation: _getOrientation(trainingPrefs.sideChoice), + orientation: _getOrientation(sideChoice), ); } From 0a836809e0b9fa624cc9d521b0c23d3176166b4d Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 25 Sep 2024 12:47:45 +0200 Subject: [PATCH 393/979] Show a corres game update notification while the app is in foreground --- lib/src/model/challenge/challenge.dart | 6 +- .../model/challenge/challenge_service.dart | 18 +- lib/src/model/common/id.dart | 4 +- .../notifications/challenge_notification.dart | 130 ----------- .../notifications/notification_service.dart | 148 +++++-------- .../model/notifications/notifications.dart | 208 ++++++++++++++++++ .../fake_notification_service.dart | 1 + 7 files changed, 279 insertions(+), 236 deletions(-) delete mode 100644 lib/src/model/notifications/challenge_notification.dart create mode 100644 lib/src/model/notifications/notifications.dart diff --git a/lib/src/model/challenge/challenge.dart b/lib/src/model/challenge/challenge.dart index 06b10fe7e8..4ea9003933 100644 --- a/lib/src/model/challenge/challenge.dart +++ b/lib/src/model/challenge/challenge.dart @@ -10,6 +10,7 @@ import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/utils/json.dart'; part 'challenge.freezed.dart'; +part 'challenge.g.dart'; abstract mixin class BaseChallenge { Variant get variant; @@ -34,7 +35,7 @@ abstract mixin class BaseChallenge { } /// A challenge already created server-side. -@freezed +@Freezed(fromJson: true, toJson: true) class Challenge with _$Challenge, BaseChallenge implements BaseChallenge { const Challenge._(); @@ -57,6 +58,9 @@ class Challenge with _$Challenge, BaseChallenge implements BaseChallenge { ChallengeDirection? direction, }) = _Challenge; + factory Challenge.fromJson(Map json) => + _$ChallengeFromJson(json); + factory Challenge.fromServerJson(Map json) { return _challengeFromPick(pick(json).required()); } diff --git a/lib/src/model/challenge/challenge_service.dart b/lib/src/model/challenge/challenge_service.dart index 196a228b4d..2e716385f1 100644 --- a/lib/src/model/challenge/challenge_service.dart +++ b/lib/src/model/challenge/challenge_service.dart @@ -8,10 +8,9 @@ import 'package:lichess_mobile/src/model/challenge/challenge.dart'; import 'package:lichess_mobile/src/model/challenge/challenge_repository.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; -import 'package:lichess_mobile/src/model/notifications/challenge_notification.dart'; import 'package:lichess_mobile/src/model/notifications/notification_service.dart'; +import 'package:lichess_mobile/src/model/notifications/notifications.dart'; import 'package:lichess_mobile/src/navigation.dart'; -import 'package:lichess_mobile/src/utils/l10n.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/game/game_screen.dart'; @@ -66,8 +65,6 @@ class ChallengeService { final Iterable currentInwardIds = inward.map((challenge) => challenge.id); - final l10n = ref.read(l10nProvider).strings; - // challenges that were canceled by challenger or expired prevInwardIds .whereNot((challengeId) => currentInwardIds.contains(challengeId)) @@ -83,7 +80,7 @@ class ChallengeService { (challenge) { ref .read(notificationServiceProvider) - .show(ChallengeNotification(challenge, l10n)); + .show(ChallengeNotification(challenge)); }, ); } @@ -95,11 +92,10 @@ class ChallengeService { /// Handle a local notification response when the app is in the foreground. Future onNotificationResponse( - int id, String? actionid, - NotificationPayload payload, + Challenge challenge, ) async { - final challengeId = ChallengePayload.fromNotification(payload).id; + final challengeId = challenge.id; switch (actionid) { case 'accept': @@ -113,9 +109,9 @@ class ChallengeService { final context = ref.read(currentNavigatorKeyProvider).currentContext; if (context == null || !context.mounted) break; - final navState = Navigator.of(context); - if (navState.canPop()) { - navState.popUntil((route) => route.isFirst); + final rootNavState = Navigator.of(context, rootNavigator: true); + if (rootNavState.canPop()) { + rootNavState.popUntil((route) => route.isFirst); } pushPlatformRoute( diff --git a/lib/src/model/common/id.dart b/lib/src/model/common/id.dart index 4ab5fb6edb..2fed0b7e00 100644 --- a/lib/src/model/common/id.dart +++ b/lib/src/model/common/id.dart @@ -47,7 +47,9 @@ extension type const UserId(String value) implements StringId { UserId.fromJson(dynamic json) : this(json as String); } -extension type const ChallengeId(String value) implements StringId {} +extension type const ChallengeId(String value) implements StringId { + ChallengeId.fromJson(dynamic json) : this(json as String); +} extension type const BroadcastRoundId(String value) implements StringId {} diff --git a/lib/src/model/notifications/challenge_notification.dart b/lib/src/model/notifications/challenge_notification.dart deleted file mode 100644 index 8b4658cf0c..0000000000 --- a/lib/src/model/notifications/challenge_notification.dart +++ /dev/null @@ -1,130 +0,0 @@ -import 'package:flutter_local_notifications/flutter_local_notifications.dart'; -import 'package:lichess_mobile/l10n/l10n.dart'; -import 'package:lichess_mobile/src/model/challenge/challenge.dart'; -import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/model/notifications/notification_service.dart'; - -class ChallengeNotification implements LocalNotification { - ChallengeNotification(this._challenge, this._l10n); - - final Challenge _challenge; - final AppLocalizations _l10n; - - @override - int get id => _challenge.id.value.hashCode; - - @override - NotificationPayload get payload => - ChallengePayload(_challenge.id).notificationPayload; - - @override - String get title => '${_challenge.challenger!.user.name} challenges you!'; - - @override - String get body => _challenge.description(_l10n); - - @override - NotificationDetails get details => NotificationDetails( - android: AndroidNotificationDetails( - 'challenges', - _l10n.preferencesNotifyChallenge, - importance: Importance.max, - priority: Priority.high, - autoCancel: false, - actions: [ - if (_challenge.variant.isPlaySupported) - AndroidNotificationAction( - 'accept', - _l10n.accept, - icon: const DrawableResourceAndroidBitmap('tick'), - showsUserInterface: true, - contextual: true, - ), - AndroidNotificationAction( - 'decline', - _l10n.decline, - icon: const DrawableResourceAndroidBitmap('cross'), - showsUserInterface: true, - contextual: true, - ), - ], - ), - iOS: DarwinNotificationDetails( - categoryIdentifier: _challenge.variant.isPlaySupported - ? darwinPlayableVariantCategoryId - : darwinUnplayableVariantCategoryId, - ), - ); - - static const darwinPlayableVariantCategoryId = - 'challenge-notification-playable-variant'; - - static const darwinUnplayableVariantCategoryId = - 'challenge-notification-unplayable-variant'; - - static DarwinNotificationCategory darwinPlayableVariantCategory( - AppLocalizations l10n, - ) => - DarwinNotificationCategory( - darwinPlayableVariantCategoryId, - actions: [ - DarwinNotificationAction.plain( - 'accept', - l10n.accept, - options: { - DarwinNotificationActionOption.foreground, - }, - ), - DarwinNotificationAction.plain( - 'decline', - l10n.decline, - options: { - DarwinNotificationActionOption.foreground, - }, - ), - ], - options: { - DarwinNotificationCategoryOption.hiddenPreviewShowTitle, - }, - ); - - static DarwinNotificationCategory darwinUnplayableVariantCategory( - AppLocalizations l10n, - ) => - DarwinNotificationCategory( - darwinUnplayableVariantCategoryId, - actions: [ - DarwinNotificationAction.plain( - 'decline', - l10n.decline, - options: { - DarwinNotificationActionOption.foreground, - }, - ), - ], - options: { - DarwinNotificationCategoryOption.hiddenPreviewShowTitle, - }, - ); -} - -class ChallengePayload { - const ChallengePayload(this.id); - - final ChallengeId id; - - NotificationPayload get notificationPayload => NotificationPayload( - type: AppNotificationType.challenge, - data: { - 'id': id.value, - }, - ); - - factory ChallengePayload.fromNotification( - NotificationPayload payload, - ) { - assert(payload.type == AppNotificationType.challenge); - final id = payload.data['id'] as String; - return ChallengePayload(ChallengeId(id)); - } -} diff --git a/lib/src/model/notifications/notification_service.dart b/lib/src/model/notifications/notification_service.dart index 329627b5e0..f5d7c030bc 100644 --- a/lib/src/model/notifications/notification_service.dart +++ b/lib/src/model/notifications/notification_service.dart @@ -6,7 +6,6 @@ import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/firebase_options.dart'; import 'package:lichess_mobile/l10n/l10n.dart'; import 'package:lichess_mobile/src/init.dart'; @@ -16,43 +15,18 @@ import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/correspondence/correspondence_service.dart'; import 'package:lichess_mobile/src/model/game/playable_game.dart'; -import 'package:lichess_mobile/src/model/notifications/challenge_notification.dart'; +import 'package:lichess_mobile/src/model/notifications/notifications.dart'; import 'package:lichess_mobile/src/utils/badge_service.dart'; import 'package:lichess_mobile/src/utils/connectivity.dart'; +import 'package:lichess_mobile/src/utils/l10n.dart'; import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'notification_service.g.dart'; -part 'notification_service.freezed.dart'; final _localNotificationPlugin = FlutterLocalNotificationsPlugin(); final _logger = Logger('NotificationService'); -/// A notification response with its ID and payload. -/// -/// A notification response is the user's interaction with a notification shown -/// by the app. -typedef NotificationResponseData = ({ - /// The notification id. - int id, - - /// The id of the action that was triggered. - String? actionId, - - /// The value of the input field if the notification action had an input - /// field. - String? input, - - /// The parsed notification payload. - NotificationPayload? payload, -}); - -/// The type of notification handled by the app. -enum AppNotificationType { - corresGameUpdate, - challenge, -} - /// Notification types defined by the server. /// /// This corresponds to the 'lichess.type' field in the FCM message's data. @@ -93,26 +67,7 @@ enum ServerNotificationType { } } -@Freezed(fromJson: true, toJson: true) -class NotificationPayload with _$NotificationPayload { - factory NotificationPayload({ - required AppNotificationType type, - required Map data, - }) = _NotificationPayload; - - factory NotificationPayload.fromJson(Map json) => - _$NotificationPayloadFromJson(json); -} - -/// A local notification that can be shown to the user. -abstract class LocalNotification { - int get id; - String get title; - String? get body; - NotificationPayload? get payload; - NotificationDetails get details; -} - +/// A provider instance of the [NotificationService]. @Riverpod(keepAlive: true) NotificationService notificationService(NotificationServiceRef ref) { final service = NotificationService(ref); @@ -142,15 +97,17 @@ class NotificationService { _connectivitySubscription; /// The stream controller for notification responses. - static final StreamController + static final StreamController _responseStreamController = StreamController.broadcast(); /// The stream subscription for notification responses. - StreamSubscription? _responseStreamSubscription; + StreamSubscription? _responseStreamSubscription; /// Whether the device has been registered for push notifications. bool _registeredDevice = false; + AppLocalizations get _l10n => _ref.read(l10nProvider).strings; + /// Initialize the notification service. /// /// It will initialize the Firebase and the local notification plugins. @@ -238,23 +195,21 @@ class NotificationService { FirebaseMessaging.onMessageOpenedApp.listen(_handleFcmMessageOpenedApp); // start listening for notification responses - _responseStreamSubscription = _responseStreamController.stream.listen( - (data) => _dispatchNotificationResponse(data), - ); + _responseStreamSubscription = + _responseStreamController.stream.listen(_dispatchNotificationResponse); } /// Shows a notification. Future show(LocalNotification notification) async { final id = notification.id; - final payload = notification.payload != null - ? jsonEncode(notification.payload!.toJson()) - : null; + final payload = + notification.payload != null ? jsonEncode(notification.payload) : null; await _localNotificationPlugin.show( id, - notification.title, - notification.body, - notification.details, + notification.title(_l10n), + notification.body(_l10n), + notification.details(_l10n), payload: payload, ); _logger.info( @@ -276,47 +231,34 @@ class NotificationService { _responseStreamSubscription?.cancel(); } - void _dispatchNotificationResponse(NotificationResponseData data) { - final (id: id, payload: payload, actionId: actionId, input: _) = data; + /// Dispatch a notification response to the appropriate service according to the notification type. + void _dispatchNotificationResponse(NotificationResponse response) { + final rawPayload = response.payload; - if (payload == null) return; + if (rawPayload == null) return; - switch (payload.type) { - case AppNotificationType.challenge: + final json = jsonDecode(rawPayload) as Map; + + final notification = LocalNotification.fromJson(json); + + switch (notification) { + case CorresGameUpdateNotification(fullId: final gameFullId): + _ref + .read(correspondenceServiceProvider) + .onNotificationResponse(gameFullId); + case ChallengeNotification(challenge: final challenge): _ref.read(challengeServiceProvider).onNotificationResponse( - id, - actionId, - payload, + response.actionId, + challenge, ); - case AppNotificationType.corresGameUpdate: - // TODO handle corres game update notifs - break; } } /// Function called by the flutter_local_notifications plugin when the user interacts with a notification that causes the app to open. static void _onDidReceiveNotificationResponse(NotificationResponse response) { - _logger.info('processing response in foreground. id [${response.id}]'); - - if (response.id == null) return; + _logger.fine('received response in foreground. id [${response.id}]'); - try { - final payload = response.payload != null - ? NotificationPayload.fromJson( - jsonDecode(response.payload!) as Map, - ) - : null; - _responseStreamController.add( - ( - id: response.id!, - actionId: response.actionId, - input: response.input, - payload: payload, - ), - ); - } catch (e) { - _logger.warning('Failed to parse notification payload: $e'); - } + _responseStreamController.add(response); } /// Handle an FCM message that caused the application to open @@ -336,6 +278,7 @@ class NotificationService { GameFullId(gameFullId), ); } + case ServerNotificationType.unhandled: _logger .warning('Received unhandled FCM notification type: $messageType'); @@ -344,11 +287,15 @@ class NotificationService { /// Process a message received from the Firebase Cloud Messaging service. /// - /// If the message contains a [RemoteMessage.notification] field, it will show - /// a local notification to the user. + /// If the message contains a [RemoteMessage.notification] field, it may show + /// a local notification to the user, depending on the message type. /// /// Some messages (whether or not they have an associated notification), have - /// a [RemoteMessage.data] field used to update the application state. + /// a [RemoteMessage.data] field used to update the application state according + /// to the message type. + /// + /// A special data field, 'lichess.iosBadge', is used to update the iOS app's + /// badge count according to the value held by the server. Future _processFcmMessage( RemoteMessage message, { required bool fromBackground, @@ -357,7 +304,9 @@ class NotificationService { 'Processing a FCM message from ${fromBackground ? 'background' : 'foreground'}: ${message.data}', ); + final RemoteNotification? notification = message.notification; final messageType = message.data['lichess.type'] as String?; + if (messageType != null) { switch (ServerNotificationType.fromString(messageType)) { case ServerNotificationType.corresAlarm: @@ -367,6 +316,7 @@ class NotificationService { case ServerNotificationType.gameFinish: final gameFullId = message.data['lichess.fullId'] as String?; final round = message.data['lichess.round'] as String?; + if (gameFullId != null && round != null) { final fullId = GameFullId(gameFullId); final game = PlayableGame.fromServerJson( @@ -379,6 +329,18 @@ class NotificationService { ); } + if (gameFullId != null && notification != null) { + await show( + CorresGameUpdateNotification( + GameFullId(gameFullId), + notification.title!, + notification.body!, + ), + ); + } + + // TODO: handle other notification types + case ServerNotificationType.unhandled: _logger.warning( 'Received unhandled FCM notification type: $messageType', diff --git a/lib/src/model/notifications/notifications.dart b/lib/src/model/notifications/notifications.dart new file mode 100644 index 0000000000..dc5f700f58 --- /dev/null +++ b/lib/src/model/notifications/notifications.dart @@ -0,0 +1,208 @@ +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:lichess_mobile/l10n/l10n.dart'; +import 'package:lichess_mobile/src/model/challenge/challenge.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; + +/// A notification shown to the user from the platform's notification system. +sealed class LocalNotification { + /// The unique identifier of the notification. + int get id; + + /// The localized title of the notification. + String title(AppLocalizations l10n); + + /// The localized body of the notification. + String? body(AppLocalizations l10n); + + /// The payload of the notification. + /// + /// It must contain a field named 'type' (of type [String]) to identify the + /// notification. + Map? get payload; + + /// The localized details of the notification for each platform. + NotificationDetails details(AppLocalizations l10n); + + /// Retrives a local notification from a JSON payload. + factory LocalNotification.fromJson(Map json) { + final type = json['type'] as String; + switch (type) { + case 'corresGameUpdate': + return CorresGameUpdateNotification.fromJson(json); + case 'challenge': + return ChallengeNotification.fromJson(json); + default: + throw ArgumentError('Unknown notification type: $type'); + } + } +} + +/// A local notification for a correspondence game update. +/// +/// This notification is shown when a correspondence game is updated on the server +/// and a FCM message is received and contains itself a notification. +/// +/// Fields [title] and [body] are dynamic and part of the payload because they +/// are generated server side and are included in the FCM message's [RemoteMessage.notification] field. +class CorresGameUpdateNotification implements LocalNotification { + const CorresGameUpdateNotification(this.fullId, String title, String body) + : _title = title, + _body = body; + + final GameFullId fullId; + + final String _title; + final String _body; + + factory CorresGameUpdateNotification.fromJson(Map json) { + final gameId = GameFullId.fromJson(json['fullId'] as String); + final title = json['title'] as String; + final body = json['body'] as String; + return CorresGameUpdateNotification(gameId, title, body); + } + + @override + int get id => fullId.hashCode; + + @override + Map get payload => { + 'type': 'corresGameUpdate', + 'fullId': fullId.toJson(), + 'title': _title, + 'body': _body, + }; + + @override + String title(_) => _title; + + @override + String? body(_) => _body; + + @override + NotificationDetails details(AppLocalizations l10n) => NotificationDetails( + android: AndroidNotificationDetails( + 'corresGameUpdate', + l10n.preferencesNotifyGameEvent, + importance: Importance.defaultImportance, + priority: Priority.defaultPriority, + autoCancel: true, + ), + ); +} + +/// A local notification for a challenge. +/// +/// This notification is shown when a challenge is received from the server through +/// the web socket. +class ChallengeNotification implements LocalNotification { + const ChallengeNotification(this.challenge); + + final Challenge challenge; + + factory ChallengeNotification.fromJson(Map json) { + final challenge = + Challenge.fromJson(json['challenge'] as Map); + return ChallengeNotification(challenge); + } + + @override + int get id => challenge.id.value.hashCode; + + @override + Map get payload => { + 'type': 'challenge', + 'challenge': challenge.toJson(), + }; + + @override + String title(AppLocalizations _) => + '${challenge.challenger!.user.name} challenges you!'; + + @override + String body(AppLocalizations l10n) => challenge.description(l10n); + + @override + NotificationDetails details(AppLocalizations l10n) => NotificationDetails( + android: AndroidNotificationDetails( + 'challenges', + l10n.preferencesNotifyChallenge, + importance: Importance.max, + priority: Priority.high, + autoCancel: false, + actions: [ + if (challenge.variant.isPlaySupported) + AndroidNotificationAction( + 'accept', + l10n.accept, + icon: const DrawableResourceAndroidBitmap('tick'), + showsUserInterface: true, + contextual: true, + ), + AndroidNotificationAction( + 'decline', + l10n.decline, + icon: const DrawableResourceAndroidBitmap('cross'), + showsUserInterface: true, + contextual: true, + ), + ], + ), + iOS: DarwinNotificationDetails( + categoryIdentifier: challenge.variant.isPlaySupported + ? darwinPlayableVariantCategoryId + : darwinUnplayableVariantCategoryId, + ), + ); + + static const darwinPlayableVariantCategoryId = + 'challenge-notification-playable-variant'; + + static const darwinUnplayableVariantCategoryId = + 'challenge-notification-unplayable-variant'; + + static DarwinNotificationCategory darwinPlayableVariantCategory( + AppLocalizations l10n, + ) => + DarwinNotificationCategory( + darwinPlayableVariantCategoryId, + actions: [ + DarwinNotificationAction.plain( + 'accept', + l10n.accept, + options: { + DarwinNotificationActionOption.foreground, + }, + ), + DarwinNotificationAction.plain( + 'decline', + l10n.decline, + options: { + DarwinNotificationActionOption.foreground, + }, + ), + ], + options: { + DarwinNotificationCategoryOption.hiddenPreviewShowTitle, + }, + ); + + static DarwinNotificationCategory darwinUnplayableVariantCategory( + AppLocalizations l10n, + ) => + DarwinNotificationCategory( + darwinUnplayableVariantCategoryId, + actions: [ + DarwinNotificationAction.plain( + 'decline', + l10n.decline, + options: { + DarwinNotificationActionOption.foreground, + }, + ), + ], + options: { + DarwinNotificationCategoryOption.hiddenPreviewShowTitle, + }, + ); +} diff --git a/test/model/notifications/fake_notification_service.dart b/test/model/notifications/fake_notification_service.dart index 559ed589be..7337f1b49e 100644 --- a/test/model/notifications/fake_notification_service.dart +++ b/test/model/notifications/fake_notification_service.dart @@ -1,4 +1,5 @@ import 'package:lichess_mobile/src/model/notifications/notification_service.dart'; +import 'package:lichess_mobile/src/model/notifications/notifications.dart'; class FakeNotificationService implements NotificationService { Map notifications = {}; From 9b2e416354aed3e7024d53e545181b680915ec2c Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 25 Sep 2024 15:42:53 +0200 Subject: [PATCH 394/979] Specify the thread identifier on iOS --- .../notifications/notification_service.dart | 7 ++-- .../model/notifications/notifications.dart | 34 ++++++++++++++----- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/lib/src/model/notifications/notification_service.dart b/lib/src/model/notifications/notification_service.dart index f5d7c030bc..955a2991c7 100644 --- a/lib/src/model/notifications/notification_service.dart +++ b/lib/src/model/notifications/notification_service.dart @@ -238,7 +238,6 @@ class NotificationService { if (rawPayload == null) return; final json = jsonDecode(rawPayload) as Map; - final notification = LocalNotification.fromJson(json); switch (notification) { @@ -254,9 +253,11 @@ class NotificationService { } } - /// Function called by the flutter_local_notifications plugin when the user interacts with a notification that causes the app to open. + /// Function called when a notification has been interacted with and the app is in the foreground. static void _onDidReceiveNotificationResponse(NotificationResponse response) { - _logger.fine('received response in foreground. id [${response.id}]'); + _logger.fine( + 'received local notification ${response.id} response in foreground.', + ); _responseStreamController.add(response); } diff --git a/lib/src/model/notifications/notifications.dart b/lib/src/model/notifications/notifications.dart index dc5f700f58..5754c6fd01 100644 --- a/lib/src/model/notifications/notifications.dart +++ b/lib/src/model/notifications/notifications.dart @@ -9,6 +9,14 @@ sealed class LocalNotification { /// The unique identifier of the notification. int get id; + /// The channel identifier of the notification. + /// + /// Corresponds to [AndroidNotificationDetails.channelId] for android and + /// [DarwinNotificationDetails.threadIdentifier] for iOS. + /// + /// It must match the channel identifier of the notification details. + String get channelId; + /// The localized title of the notification. String title(AppLocalizations l10n); @@ -17,7 +25,7 @@ sealed class LocalNotification { /// The payload of the notification. /// - /// It must contain a field named 'type' (of type [String]) to identify the + /// It must contain a field named 'channel' (of type [String]) to identify the /// notification. Map? get payload; @@ -26,14 +34,14 @@ sealed class LocalNotification { /// Retrives a local notification from a JSON payload. factory LocalNotification.fromJson(Map json) { - final type = json['type'] as String; - switch (type) { + final channel = json['channel'] as String; + switch (channel) { case 'corresGameUpdate': return CorresGameUpdateNotification.fromJson(json); case 'challenge': return ChallengeNotification.fromJson(json); default: - throw ArgumentError('Unknown notification type: $type'); + throw ArgumentError('Unknown notification channel: $channel'); } } } @@ -62,12 +70,15 @@ class CorresGameUpdateNotification implements LocalNotification { return CorresGameUpdateNotification(gameId, title, body); } + @override + String get channelId => 'corresGameUpdate'; + @override int get id => fullId.hashCode; @override Map get payload => { - 'type': 'corresGameUpdate', + 'channel': channelId, 'fullId': fullId.toJson(), 'title': _title, 'body': _body, @@ -82,12 +93,13 @@ class CorresGameUpdateNotification implements LocalNotification { @override NotificationDetails details(AppLocalizations l10n) => NotificationDetails( android: AndroidNotificationDetails( - 'corresGameUpdate', + channelId, l10n.preferencesNotifyGameEvent, - importance: Importance.defaultImportance, + importance: Importance.high, priority: Priority.defaultPriority, autoCancel: true, ), + iOS: DarwinNotificationDetails(threadIdentifier: channelId), ); } @@ -106,12 +118,15 @@ class ChallengeNotification implements LocalNotification { return ChallengeNotification(challenge); } + @override + String get channelId => 'challenge'; + @override int get id => challenge.id.value.hashCode; @override Map get payload => { - 'type': 'challenge', + 'channel': channelId, 'challenge': challenge.toJson(), }; @@ -125,7 +140,7 @@ class ChallengeNotification implements LocalNotification { @override NotificationDetails details(AppLocalizations l10n) => NotificationDetails( android: AndroidNotificationDetails( - 'challenges', + channelId, l10n.preferencesNotifyChallenge, importance: Importance.max, priority: Priority.high, @@ -149,6 +164,7 @@ class ChallengeNotification implements LocalNotification { ], ), iOS: DarwinNotificationDetails( + threadIdentifier: channelId, categoryIdentifier: challenge.variant.isPlaySupported ? darwinPlayableVariantCategoryId : darwinUnplayableVariantCategoryId, From bb385fb89086354d51a51b1e5c3e1ee8624d3240 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 25 Sep 2024 16:05:00 +0200 Subject: [PATCH 395/979] Do not show twice the notification if on background --- lib/src/model/notifications/notification_service.dart | 11 ++++++++--- lib/src/model/notifications/notifications.dart | 4 ++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/lib/src/model/notifications/notification_service.dart b/lib/src/model/notifications/notification_service.dart index 955a2991c7..9610021fef 100644 --- a/lib/src/model/notifications/notification_service.dart +++ b/lib/src/model/notifications/notification_service.dart @@ -288,8 +288,10 @@ class NotificationService { /// Process a message received from the Firebase Cloud Messaging service. /// - /// If the message contains a [RemoteMessage.notification] field, it may show - /// a local notification to the user, depending on the message type. + /// If the message contains a [RemoteMessage.notification] field and if it is + /// received while the app was in foreground, the notification is by default not + /// shown to the user. + /// Depending on the message type, we may as well show a local notification. /// /// Some messages (whether or not they have an associated notification), have /// a [RemoteMessage.data] field used to update the application state according @@ -299,6 +301,7 @@ class NotificationService { /// badge count according to the value held by the server. Future _processFcmMessage( RemoteMessage message, { + /// Whether the message was received while the app was in the background. required bool fromBackground, }) async { _logger.fine( @@ -330,7 +333,9 @@ class NotificationService { ); } - if (gameFullId != null && notification != null) { + if (fromBackground == false && + gameFullId != null && + notification != null) { await show( CorresGameUpdateNotification( GameFullId(gameFullId), diff --git a/lib/src/model/notifications/notifications.dart b/lib/src/model/notifications/notifications.dart index 5754c6fd01..da2d74a817 100644 --- a/lib/src/model/notifications/notifications.dart +++ b/lib/src/model/notifications/notifications.dart @@ -46,7 +46,7 @@ sealed class LocalNotification { } } -/// A local notification for a correspondence game update. +/// A notification for a correspondence game update. /// /// This notification is shown when a correspondence game is updated on the server /// and a FCM message is received and contains itself a notification. @@ -103,7 +103,7 @@ class CorresGameUpdateNotification implements LocalNotification { ); } -/// A local notification for a challenge. +/// A notification for a received challenge. /// /// This notification is shown when a challenge is received from the server through /// the web socket. From b74a40933f4dffc515001d873279464ddc154dfc Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 26 Sep 2024 10:10:18 +0200 Subject: [PATCH 396/979] Tweak notifications --- .../notifications/notification_service.dart | 3 +- .../model/notifications/notifications.dart | 30 +++++++++++++------ 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/lib/src/model/notifications/notification_service.dart b/lib/src/model/notifications/notification_service.dart index 9610021fef..4e916078e5 100644 --- a/lib/src/model/notifications/notification_service.dart +++ b/lib/src/model/notifications/notification_service.dart @@ -202,8 +202,7 @@ class NotificationService { /// Shows a notification. Future show(LocalNotification notification) async { final id = notification.id; - final payload = - notification.payload != null ? jsonEncode(notification.payload) : null; + final payload = jsonEncode(notification.payload); await _localNotificationPlugin.show( id, diff --git a/lib/src/model/notifications/notifications.dart b/lib/src/model/notifications/notifications.dart index da2d74a817..dc3c08a0fa 100644 --- a/lib/src/model/notifications/notifications.dart +++ b/lib/src/model/notifications/notifications.dart @@ -3,9 +3,13 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:lichess_mobile/l10n/l10n.dart'; import 'package:lichess_mobile/src/model/challenge/challenge.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:meta/meta.dart'; /// A notification shown to the user from the platform's notification system. +@immutable sealed class LocalNotification { + const LocalNotification(); + /// The unique identifier of the notification. int get id; @@ -25,9 +29,19 @@ sealed class LocalNotification { /// The payload of the notification. /// - /// It must contain a field named 'channel' (of type [String]) to identify the - /// notification. - Map? get payload; + /// Implementations must not override this getter, but [_concretePayload] instead. + /// + /// See [LocalNotification.fromJson] where the [channelId] is used to determine the + /// concrete type of the notification, to be able to deserialize it. + Map get payload => { + 'channel': channelId, + ..._concretePayload, + }; + + /// The actual payload of the notification. + /// + /// Will be merged with the channel:[channelId] entry to form the final payload. + Map get _concretePayload; /// The localized details of the notification for each platform. NotificationDetails details(AppLocalizations l10n); @@ -53,7 +67,7 @@ sealed class LocalNotification { /// /// Fields [title] and [body] are dynamic and part of the payload because they /// are generated server side and are included in the FCM message's [RemoteMessage.notification] field. -class CorresGameUpdateNotification implements LocalNotification { +class CorresGameUpdateNotification extends LocalNotification { const CorresGameUpdateNotification(this.fullId, String title, String body) : _title = title, _body = body; @@ -77,8 +91,7 @@ class CorresGameUpdateNotification implements LocalNotification { int get id => fullId.hashCode; @override - Map get payload => { - 'channel': channelId, + Map get _concretePayload => { 'fullId': fullId.toJson(), 'title': _title, 'body': _body, @@ -107,7 +120,7 @@ class CorresGameUpdateNotification implements LocalNotification { /// /// This notification is shown when a challenge is received from the server through /// the web socket. -class ChallengeNotification implements LocalNotification { +class ChallengeNotification extends LocalNotification { const ChallengeNotification(this.challenge); final Challenge challenge; @@ -125,8 +138,7 @@ class ChallengeNotification implements LocalNotification { int get id => challenge.id.value.hashCode; @override - Map get payload => { - 'channel': channelId, + Map get _concretePayload => { 'challenge': challenge.toJson(), }; From 02e957a2ccddb307b5ca2133ab78a393b5c3c8ea Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Thu, 26 Sep 2024 12:30:11 +0200 Subject: [PATCH 397/979] fix: ViewRoot.mainline & ViewBranch.mainline align implementation with Node.mainline --- lib/src/model/common/node.dart | 32 +++++++++++++------------------- test/model/common/node_test.dart | 26 ++++++++++++++++++++------ 2 files changed, 33 insertions(+), 25 deletions(-) diff --git a/lib/src/model/common/node.dart b/lib/src/model/common/node.dart index a8b47c5dae..dab74db865 100644 --- a/lib/src/model/common/node.dart +++ b/lib/src/model/common/node.dart @@ -528,6 +528,8 @@ class Root extends Node { /// An immutable view of a [Node]. abstract class ViewNode { + const ViewNode(); + UciCharPair? get id; SanMove? get sanMove; Position get position; @@ -538,12 +540,20 @@ abstract class ViewNode { IList? get comments; IList? get lichessAnalysisComments; IList? get nags; - Iterable get mainline; + + Iterable get mainline sync* { + ViewNode current = this; + while (current.children.isNotEmpty) { + final child = current.children.first; + yield child; + current = child; + } + } } /// An immutable view of a [Root] node. @freezed -class ViewRoot with _$ViewRoot implements ViewNode { +class ViewRoot extends ViewNode with _$ViewRoot { const ViewRoot._(); const factory ViewRoot({ required Position position, @@ -571,19 +581,11 @@ class ViewRoot with _$ViewRoot implements ViewNode { @override IList? get nags => null; - - @override - Iterable get mainline sync* { - for (final child in children) { - yield child; - yield* child.mainline; - } - } } /// An immutable view of a [Branch] node. @freezed -class ViewBranch with _$ViewBranch implements ViewNode { +class ViewBranch extends ViewNode with _$ViewBranch { const ViewBranch._(); const factory ViewBranch({ @@ -625,12 +627,4 @@ class ViewBranch with _$ViewBranch implements ViewNode { @override UciCharPair get id => UciCharPair.fromMove(sanMove.move); - - @override - Iterable get mainline sync* { - for (final child in children) { - yield child; - yield* child.mainline; - } - } } diff --git a/test/model/common/node_test.dart b/test/model/common/node_test.dart index 36ed27c8a2..a182fd2df7 100644 --- a/test/model/common/node_test.dart +++ b/test/model/common/node_test.dart @@ -533,14 +533,28 @@ void main() { group('ViewNode', () { test('mainline', () { - final root = Root.fromPgnMoves('e4 e5'); + const pgn = '1. e4 e5 (1... d5 2. a4) 2. a4'; + final root = Root.fromPgnGame(PgnGame.parsePgn(pgn)); final viewRoot = root.view; - final mainline = viewRoot.mainline; - expect(mainline.length, equals(2)); - final list = mainline.toList(); - expect(list[0].sanMove, equals(SanMove('e4', Move.parse('e2e4')!))); - expect(list[1].sanMove, equals(SanMove('e5', Move.parse('e7e5')!))); + { + final mainline = viewRoot.mainline; + + expect(mainline.length, equals(3)); + final list = mainline.toList(); + expect(list[0].sanMove, equals(SanMove('e4', Move.parse('e2e4')!))); + expect(list[1].sanMove, equals(SanMove('e5', Move.parse('e7e5')!))); + expect(list[2].sanMove, equals(SanMove('a4', Move.parse('a2a4')!))); + } + + { + final childMainline = viewRoot.children.first.mainline; + + expect(childMainline.length, equals(2)); + final list = childMainline.toList(); + expect(list[0].sanMove, equals(SanMove('e5', Move.parse('e7e5')!))); + expect(list[1].sanMove, equals(SanMove('a4', Move.parse('a2a4')!))); + } }); }); } From 23bffa2e7b958aac825aafb5461768636a26d300 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 26 Sep 2024 16:16:22 +0200 Subject: [PATCH 398/979] WIP on testing notifications --- lib/main.dart | 6 +- lib/src/binding.dart | 132 ++++++++++++++++++ .../notifications/notification_service.dart | 105 +++----------- .../model/notifications/notifications.dart | 40 ++++++ test/binding.dart | 86 ++++++++++++ ...me_storage.dart => fake_game_storage.dart} | 2 +- test/test_app.dart | 4 +- 7 files changed, 286 insertions(+), 89 deletions(-) create mode 100644 lib/src/binding.dart create mode 100644 test/binding.dart rename test/model/game/{mock_game_storage.dart => fake_game_storage.dart} (95%) diff --git a/lib/main.dart b/lib/main.dart index d4d02def92..25f1f3e091 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,11 +2,11 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/binding.dart'; import 'package:lichess_mobile/src/init.dart'; import 'package:lichess_mobile/src/intl.dart'; import 'package:lichess_mobile/src/log.dart'; import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; -import 'package:lichess_mobile/src/model/notifications/notification_service.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'src/app.dart'; @@ -14,6 +14,8 @@ import 'src/app.dart'; Future main() async { final widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); + final lichessBinding = AppLichessBinding.ensureInitialized(); + SharedPreferences.setPrefix('lichess.'); // Show splash screen until app is ready @@ -28,7 +30,7 @@ Future main() async { final locale = await setupIntl(widgetsBinding); - await NotificationService.initialize(locale); + await lichessBinding.initializeNotifications(locale); if (defaultTargetPlatform == TargetPlatform.android) { await androidDisplayInitialization(widgetsBinding); diff --git a/lib/src/binding.dart b/lib/src/binding.dart new file mode 100644 index 0000000000..8e3fb6752a --- /dev/null +++ b/lib/src/binding.dart @@ -0,0 +1,132 @@ +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:lichess_mobile/firebase_options.dart'; +import 'package:lichess_mobile/l10n/l10n.dart'; +import 'package:lichess_mobile/src/model/notifications/notification_service.dart'; +import 'package:lichess_mobile/src/model/notifications/notifications.dart'; + +/// The glue between the platform-specific plugins and the app. +/// +/// Only one instance of this class will be created during the app's lifetime. +/// See [AppLichessBinding] for the concrete implementation. +/// +/// Modeled after the Flutter framework's [WidgetsBinding] class. +abstract class LichessBinding { + LichessBinding() : assert(_instance == null) { + initInstance(); + } + + /// The single instance of [LichessBinding]. + static LichessBinding get instance => checkInstance(_instance); + static LichessBinding? _instance; + + @protected + @mustCallSuper + void initInstance() { + _instance = this; + } + + static T checkInstance(T? instance) { + assert(() { + if (instance == null) { + throw FlutterError.fromParts([ + ErrorSummary('Lichess binding has not yet been initialized.'), + ErrorHint( + 'In the app, this is done by the `AppLichessBinding.ensureInitialized()` call ' + 'in the `void main()` method.', + ), + ErrorHint( + 'In a test, one can call `TestLichessBinding.ensureInitialized()` as the ' + "first line in the test's `main()` method to initialize the binding.", + ), + ]); + } + return true; + }()); + return instance!; + } + + /// Initialize notifications. + /// + /// This wraps [Firebase.initializeApp] and [FlutterLocalNotificationsPlugin.initialize]. + /// + /// This should be called only once before the app starts. + Future initializeNotifications(Locale locale); + + /// Wraps [FirebaseMessaging.instance]. + FirebaseMessaging get firebaseMessaging; + + /// Wraps [FirebaseMessaging.onMessage]. + Stream get firebaseMessagingOnMessage; + + /// Wraps [FirebaseMessaging.onMessageOpenedApp]. + Stream get firebaseMessagingOnMessageOpenedApp; + + /// Wraps [FirebaseMessaging.onBackgroundMessage]. + void firebaseMessagingOnBackgroundMessage(BackgroundMessageHandler handler); + + /// Wraps the [FlutterLocalNotificationsPlugin] singleton constructor. + FlutterLocalNotificationsPlugin get notifications; +} + +/// A concrete implementation of [LichessBinding] for the app. +class AppLichessBinding extends LichessBinding { + AppLichessBinding(); + + /// Returns an instance of the binding that implements [LichessBinding]. + /// + /// If no binding has yet been initialized, the [AppLichessBinding] class is + /// used to create and initialize one. + factory AppLichessBinding.ensureInitialized() { + if (LichessBinding._instance == null) { + AppLichessBinding(); + } + return LichessBinding.instance as AppLichessBinding; + } + + @override + Future initializeNotifications(Locale locale) async { + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); + + final l10n = await AppLocalizations.delegate.load(locale); + await FlutterLocalNotificationsPlugin().initialize( + InitializationSettings( + android: const AndroidInitializationSettings('logo_black'), + iOS: DarwinInitializationSettings( + requestBadgePermission: false, + notificationCategories: [ + ChallengeNotification.darwinPlayableVariantCategory(l10n), + ChallengeNotification.darwinUnplayableVariantCategory(l10n), + ], + ), + ), + onDidReceiveNotificationResponse: + NotificationService.onDidReceiveNotificationResponse, + // onDidReceiveBackgroundNotificationResponse: notificationTapBackground, + ); + } + + @override + FirebaseMessaging get firebaseMessaging => FirebaseMessaging.instance; + + @override + void firebaseMessagingOnBackgroundMessage(BackgroundMessageHandler handler) { + FirebaseMessaging.onBackgroundMessage(handler); + } + + @override + Stream get firebaseMessagingOnMessage => + FirebaseMessaging.onMessage; + + @override + Stream get firebaseMessagingOnMessageOpenedApp => + FirebaseMessaging.onMessageOpenedApp; + + @override + FlutterLocalNotificationsPlugin get notifications => + FlutterLocalNotificationsPlugin(); +} diff --git a/lib/src/model/notifications/notification_service.dart b/lib/src/model/notifications/notification_service.dart index 4e916078e5..f96838f529 100644 --- a/lib/src/model/notifications/notification_service.dart +++ b/lib/src/model/notifications/notification_service.dart @@ -1,13 +1,11 @@ import 'dart:async'; import 'dart:convert'; -import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lichess_mobile/firebase_options.dart'; import 'package:lichess_mobile/l10n/l10n.dart'; +import 'package:lichess_mobile/src/binding.dart'; import 'package:lichess_mobile/src/init.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/challenge/challenge_service.dart'; @@ -24,49 +22,8 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'notification_service.g.dart'; -final _localNotificationPlugin = FlutterLocalNotificationsPlugin(); final _logger = Logger('NotificationService'); -/// Notification types defined by the server. -/// -/// This corresponds to the 'lichess.type' field in the FCM message's data. -enum ServerNotificationType { - /// There is not much time left to make a move in a correspondence game. - corresAlarm, - - /// A takeback offer has been made in a correspondence game. - gameTakebackOffer, - - /// A draw offer has been made in a correspondence game. - gameDrawOffer, - - /// A move has been made in a correspondence game. - gameMove, - - /// A correspondence game just finished. - gameFinish, - - /// Server notification type not handled by the app. - unhandled; - - static ServerNotificationType fromString(String type) { - switch (type) { - case 'corresAlarm': - return corresAlarm; - case 'gameTakebackOffer': - return gameTakebackOffer; - case 'gameDrawOffer': - return gameDrawOffer; - case 'gameMove': - return gameMove; - case 'gameFinish': - return gameFinish; - default: - return unhandled; - } - } -} - /// A provider instance of the [NotificationService]. @Riverpod(keepAlive: true) NotificationService notificationService(NotificationServiceRef ref) { @@ -108,40 +65,14 @@ class NotificationService { AppLocalizations get _l10n => _ref.read(l10nProvider).strings; - /// Initialize the notification service. - /// - /// It will initialize the Firebase and the local notification plugins. - /// - /// This should be called once when the app starts. - static Future initialize(Locale locale) async { - await Firebase.initializeApp( - options: DefaultFirebaseOptions.currentPlatform, - ); - - final l10n = await AppLocalizations.delegate.load(locale); - await _localNotificationPlugin.initialize( - InitializationSettings( - android: const AndroidInitializationSettings('logo_black'), - iOS: DarwinInitializationSettings( - requestBadgePermission: false, - notificationCategories: [ - ChallengeNotification.darwinPlayableVariantCategory(l10n), - ChallengeNotification.darwinUnplayableVariantCategory(l10n), - ], - ), - ), - onDidReceiveNotificationResponse: _onDidReceiveNotificationResponse, - // onDidReceiveBackgroundNotificationResponse: notificationTapBackground, - ); - } - /// Starts the notification service. /// /// This method listens for incoming messages and updates the application state /// accordingly. /// It also registers the device for push notifications once the app is online. /// - /// This method should be called once the app is ready to receive notifications and after [initialize]. + /// This method should be called once the app is ready to receive notifications, + /// and after [LichessBinding.initializeNotifications] has been called. Future start() async { _connectivitySubscription = _ref.listen(connectivityChangesProvider, (prev, current) async { @@ -157,16 +88,19 @@ class NotificationService { }); // Listen for incoming messages while the app is in the foreground. - FirebaseMessaging.onMessage.listen((RemoteMessage message) { + LichessBinding.instance.firebaseMessagingOnMessage + .listen((RemoteMessage message) { _processFcmMessage(message, fromBackground: false); }); // Listen for incoming messages while the app is in the background. - FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler); + LichessBinding.instance.firebaseMessagingOnBackgroundMessage( + _firebaseMessagingBackgroundHandler, + ); // Request permission to receive notifications. Pop-up will appear only // once. - await FirebaseMessaging.instance.requestPermission( + await LichessBinding.instance.firebaseMessaging.requestPermission( alert: true, badge: true, sound: true, @@ -177,22 +111,24 @@ class NotificationService { ); // Listen for token refresh and update the token on the server accordingly. - _fcmTokenRefreshSubscription = - FirebaseMessaging.instance.onTokenRefresh.listen((String token) { + _fcmTokenRefreshSubscription = LichessBinding + .instance.firebaseMessaging.onTokenRefresh + .listen((String token) { _registerToken(token); }); // Get any messages which caused the application to open from // a terminated state. final RemoteMessage? initialMessage = - await FirebaseMessaging.instance.getInitialMessage(); + await LichessBinding.instance.firebaseMessaging.getInitialMessage(); if (initialMessage != null) { _handleFcmMessageOpenedApp(initialMessage); } // Handle any other interaction that caused the app to open when in background. - FirebaseMessaging.onMessageOpenedApp.listen(_handleFcmMessageOpenedApp); + LichessBinding.instance.firebaseMessagingOnMessageOpenedApp + .listen(_handleFcmMessageOpenedApp); // start listening for notification responses _responseStreamSubscription = @@ -204,7 +140,7 @@ class NotificationService { final id = notification.id; final payload = jsonEncode(notification.payload); - await _localNotificationPlugin.show( + await LichessBinding.instance.notifications.show( id, notification.title(_l10n), notification.body(_l10n), @@ -221,7 +157,7 @@ class NotificationService { /// Cancels/removes a notification. Future cancel(int id) async { _logger.info('canceled notification id: [$id]'); - return _localNotificationPlugin.cancel(id); + return LichessBinding.instance.notifications.cancel(id); } void _dispose() { @@ -253,7 +189,7 @@ class NotificationService { } /// Function called when a notification has been interacted with and the app is in the foreground. - static void _onDidReceiveNotificationResponse(NotificationResponse response) { + static void onDidReceiveNotificationResponse(NotificationResponse response) { _logger.fine( 'received local notification ${response.id} response in foreground.', ); @@ -366,7 +302,7 @@ class NotificationService { /// Register the device for push notifications. Future registerDevice() async { - final token = await FirebaseMessaging.instance.getToken(); + final token = await LichessBinding.instance.firebaseMessaging.getToken(); if (token != null) { await _registerToken(token); } @@ -389,7 +325,8 @@ class NotificationService { } Future _registerToken(String token) async { - final settings = await FirebaseMessaging.instance.getNotificationSettings(); + final settings = await LichessBinding.instance.firebaseMessaging + .getNotificationSettings(); if (settings.authorizationStatus == AuthorizationStatus.denied) { return; } diff --git a/lib/src/model/notifications/notifications.dart b/lib/src/model/notifications/notifications.dart index dc3c08a0fa..fa8d319243 100644 --- a/lib/src/model/notifications/notifications.dart +++ b/lib/src/model/notifications/notifications.dart @@ -5,6 +5,46 @@ import 'package:lichess_mobile/src/model/challenge/challenge.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:meta/meta.dart'; +/// Notification types defined by the server. +/// +/// This corresponds to the 'lichess.type' field in the FCM message's data. +enum ServerNotificationType { + /// There is not much time left to make a move in a correspondence game. + corresAlarm, + + /// A takeback offer has been made in a correspondence game. + gameTakebackOffer, + + /// A draw offer has been made in a correspondence game. + gameDrawOffer, + + /// A move has been made in a correspondence game. + gameMove, + + /// A correspondence game just finished. + gameFinish, + + /// Server notification type not handled by the app. + unhandled; + + static ServerNotificationType fromString(String type) { + switch (type) { + case 'corresAlarm': + return corresAlarm; + case 'gameTakebackOffer': + return gameTakebackOffer; + case 'gameDrawOffer': + return gameDrawOffer; + case 'gameMove': + return gameMove; + case 'gameFinish': + return gameFinish; + default: + return unhandled; + } + } +} + /// A notification shown to the user from the platform's notification system. @immutable sealed class LocalNotification { diff --git a/test/binding.dart b/test/binding.dart new file mode 100644 index 0000000000..579bf76327 --- /dev/null +++ b/test/binding.dart @@ -0,0 +1,86 @@ +import 'dart:async'; +import 'dart:ui'; + +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter_local_notifications/src/flutter_local_notifications_plugin.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:lichess_mobile/src/binding.dart'; + +class TestLichessBinding extends LichessBinding { + TestLichessBinding(); + + /// Initialize the binding if necessary, and ensure it is a [TestLichessBinding]. + /// + /// If there is an existing binding but it is not a [TestLichessBinding], + /// this method throws an error. + factory TestLichessBinding.ensureInitialized() { + if (_instance == null) { + TestLichessBinding(); + } + return instance; + } + + /// The single instance of the binding. + static TestLichessBinding get instance => + LichessBinding.checkInstance(_instance); + static TestLichessBinding? _instance; + + @override + void initInstance() { + super.initInstance(); + _instance = this; + } + + FakeFirebaseMessaging? _firebaseMessaging; + FakeFlutterLocalNotificationsPlugin? _notificationsPlugin; + + @override + Future initializeNotifications(Locale locale) async {} + + @override + FakeFirebaseMessaging get firebaseMessaging { + return _firebaseMessaging ??= FakeFirebaseMessaging(); + } + + @override + void firebaseMessagingOnBackgroundMessage(BackgroundMessageHandler handler) { + firebaseMessaging.onBackgroundMessage.stream.listen(handler); + } + + @override + Stream get firebaseMessagingOnMessage => + firebaseMessaging.onMessage.stream; + + @override + Stream get firebaseMessagingOnMessageOpenedApp => + firebaseMessaging.onMessageOpenedApp.stream; + + @override + FakeFlutterLocalNotificationsPlugin get notifications => + _notificationsPlugin ??= FakeFlutterLocalNotificationsPlugin(); +} + +class FakeFirebaseMessaging extends Fake implements FirebaseMessaging { + /// Controller for [onMessage]. + /// + /// Call [StreamController.add] to simulate a message received from FCM while + /// the application is in foreground. + StreamController onMessage = StreamController.broadcast(); + + /// Controller for [onMessageOpenedApp]. + /// + /// Call [StreamController.add] to simulate a user press on a notification message + /// sent by FCM. + StreamController onMessageOpenedApp = + StreamController.broadcast(); + + /// Controller for [onBackgroundMessage]. + /// + /// Call [StreamController.add] to simulate a message received from FCM while + /// the application is in background. + StreamController onBackgroundMessage = + StreamController.broadcast(); +} + +class FakeFlutterLocalNotificationsPlugin extends Fake + implements FlutterLocalNotificationsPlugin {} diff --git a/test/model/game/mock_game_storage.dart b/test/model/game/fake_game_storage.dart similarity index 95% rename from test/model/game/mock_game_storage.dart rename to test/model/game/fake_game_storage.dart index c2fd45f075..2d3dedc38d 100644 --- a/test/model/game/mock_game_storage.dart +++ b/test/model/game/fake_game_storage.dart @@ -4,7 +4,7 @@ import 'package:lichess_mobile/src/model/game/archived_game.dart'; import 'package:lichess_mobile/src/model/game/game_filter.dart'; import 'package:lichess_mobile/src/model/game/game_storage.dart'; -class MockGameStorage implements GameStorage { +class FakeGameStorage implements GameStorage { @override Future delete(GameId gameId) { return Future.value(); diff --git a/test/test_app.dart b/test/test_app.dart index dab572bdc6..a6bf8fef78 100644 --- a/test/test_app.dart +++ b/test/test_app.dart @@ -34,7 +34,7 @@ import './fake_crashlytics.dart'; import './model/auth/fake_session_storage.dart'; import './model/common/service/fake_sound_service.dart'; import 'model/common/fake_websocket_channel.dart'; -import 'model/game/mock_game_storage.dart'; +import 'model/game/fake_game_storage.dart'; import 'model/notifications/fake_notification_service.dart'; import 'utils/fake_connectivity_changes.dart'; @@ -137,7 +137,7 @@ Future buildTestApp( sessionStorageProvider.overrideWithValue(FakeSessionStorage(userSession)), // ignore: scoped_providers_should_specify_dependencies gameStorageProvider.overrideWith((_) async { - return MockGameStorage(); + return FakeGameStorage(); }), // ignore: scoped_providers_should_specify_dependencies cachedDataProvider.overrideWith((ref) { From 5440275ece780d9ec296f74640ae9da9bf139caf Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 27 Sep 2024 17:11:18 +0200 Subject: [PATCH 399/979] Add notification service tests --- lib/src/binding.dart | 15 +- .../notifications/notification_service.dart | 124 ++++---- .../model/notifications/notifications.dart | 134 +++++--- lib/src/utils/connectivity.dart | 18 +- pubspec.lock | 2 +- pubspec.yaml | 1 + test/binding.dart | 141 ++++++++- test/model/game/game_test.dart | 4 +- .../fake_notification_display.dart | 35 +++ .../fake_notification_service.dart | 26 -- .../notification_service_test.dart | 285 ++++++++++++++++++ test/test_app.dart | 19 +- test/test_container.dart | 72 +++-- test/utils/fake_connectivity.dart | 21 ++ test/utils/fake_connectivity_changes.dart | 15 - 15 files changed, 709 insertions(+), 203 deletions(-) create mode 100644 test/model/notifications/fake_notification_display.dart delete mode 100644 test/model/notifications/fake_notification_service.dart create mode 100644 test/model/notifications/notification_service_test.dart create mode 100644 test/utils/fake_connectivity.dart delete mode 100644 test/utils/fake_connectivity_changes.dart diff --git a/lib/src/binding.dart b/lib/src/binding.dart index 8e3fb6752a..b1679880a7 100644 --- a/lib/src/binding.dart +++ b/lib/src/binding.dart @@ -7,12 +7,18 @@ import 'package:lichess_mobile/l10n/l10n.dart'; import 'package:lichess_mobile/src/model/notifications/notification_service.dart'; import 'package:lichess_mobile/src/model/notifications/notifications.dart'; -/// The glue between the platform-specific plugins and the app. +/// The glue between some platform-specific plugins and the app. /// /// Only one instance of this class will be created during the app's lifetime. /// See [AppLichessBinding] for the concrete implementation. /// /// Modeled after the Flutter framework's [WidgetsBinding] class. +/// +/// The preferred way to mock or fake a plugin or external API is to create a +/// provider with riverpod because it gives more flexibility and control over +/// the behavior of the fake. +/// However, if the plugin is used in a way that doesn't allow for easy mocking +///with riverpod, a binding can be used to provide a fake implementation. abstract class LichessBinding { LichessBinding() : assert(_instance == null) { initInstance(); @@ -66,9 +72,6 @@ abstract class LichessBinding { /// Wraps [FirebaseMessaging.onBackgroundMessage]. void firebaseMessagingOnBackgroundMessage(BackgroundMessageHandler handler); - - /// Wraps the [FlutterLocalNotificationsPlugin] singleton constructor. - FlutterLocalNotificationsPlugin get notifications; } /// A concrete implementation of [LichessBinding] for the app. @@ -125,8 +128,4 @@ class AppLichessBinding extends LichessBinding { @override Stream get firebaseMessagingOnMessageOpenedApp => FirebaseMessaging.onMessageOpenedApp; - - @override - FlutterLocalNotificationsPlugin get notifications => - FlutterLocalNotificationsPlugin(); } diff --git a/lib/src/model/notifications/notification_service.dart b/lib/src/model/notifications/notification_service.dart index f96838f529..f73ff7be2d 100644 --- a/lib/src/model/notifications/notification_service.dart +++ b/lib/src/model/notifications/notification_service.dart @@ -10,9 +10,7 @@ import 'package:lichess_mobile/src/init.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/challenge/challenge_service.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; -import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/correspondence/correspondence_service.dart'; -import 'package:lichess_mobile/src/model/game/playable_game.dart'; import 'package:lichess_mobile/src/model/notifications/notifications.dart'; import 'package:lichess_mobile/src/utils/badge_service.dart'; import 'package:lichess_mobile/src/utils/connectivity.dart'; @@ -24,6 +22,11 @@ part 'notification_service.g.dart'; final _logger = Logger('NotificationService'); +/// A provider instance of the [FlutterLocalNotificationsPlugin]. +@Riverpod(keepAlive: true) +FlutterLocalNotificationsPlugin notificationDisplay(NotificationDisplayRef _) => + FlutterLocalNotificationsPlugin(); + /// A provider instance of the [NotificationService]. @Riverpod(keepAlive: true) NotificationService notificationService(NotificationServiceRef ref) { @@ -65,6 +68,9 @@ class NotificationService { AppLocalizations get _l10n => _ref.read(l10nProvider).strings; + FlutterLocalNotificationsPlugin get _notificationDisplay => + _ref.read(notificationDisplayProvider); + /// Starts the notification service. /// /// This method listens for incoming messages and updates the application state @@ -74,9 +80,9 @@ class NotificationService { /// This method should be called once the app is ready to receive notifications, /// and after [LichessBinding.initializeNotifications] has been called. Future start() async { + // listen for connectivity changes to register device once the app is online _connectivitySubscription = _ref.listen(connectivityChangesProvider, (prev, current) async { - // register device once the app is online if (current.value?.isOnline == true && !_registeredDevice) { try { await registerDevice(); @@ -140,7 +146,7 @@ class NotificationService { final id = notification.id; final payload = jsonEncode(notification.payload); - await LichessBinding.instance.notifications.show( + await _notificationDisplay.show( id, notification.title(_l10n), notification.body(_l10n), @@ -157,7 +163,7 @@ class NotificationService { /// Cancels/removes a notification. Future cancel(int id) async { _logger.info('canceled notification id: [$id]'); - return LichessBinding.instance.notifications.cancel(id); + return _notificationDisplay.cancel(id); } void _dispose() { @@ -188,7 +194,7 @@ class NotificationService { } } - /// Function called when a notification has been interacted with and the app is in the foreground. + /// Function called by the notification plugin when a notification has been tapped on. static void onDidReceiveNotificationResponse(NotificationResponse response) { _logger.fine( 'received local notification ${response.id} response in foreground.', @@ -199,25 +205,20 @@ class NotificationService { /// Handle an FCM message that caused the application to open void _handleFcmMessageOpenedApp(RemoteMessage message) { - final messageType = message.data['lichess.type'] as String?; - if (messageType == null) return; - - switch (ServerNotificationType.fromString(messageType)) { - case ServerNotificationType.corresAlarm: - case ServerNotificationType.gameTakebackOffer: - case ServerNotificationType.gameDrawOffer: - case ServerNotificationType.gameMove: - case ServerNotificationType.gameFinish: - final gameFullId = message.data['lichess.fullId'] as String?; - if (gameFullId != null) { - _ref.read(correspondenceServiceProvider).onNotificationResponse( - GameFullId(gameFullId), - ); - } + final parsedMessage = FcmMessage.fromRemoteMessage(message); - case ServerNotificationType.unhandled: - _logger - .warning('Received unhandled FCM notification type: $messageType'); + switch (parsedMessage) { + case CorresGameUpdateFcmMessage(fullId: final fullId): + _ref.read(correspondenceServiceProvider).onNotificationResponse(fullId); + + // TODO: handle other notification types + case UnhandledFcmMessage(data: final data): + _logger.warning( + 'Received unhandled FCM notification type: ${data['lichess.type']}', + ); + + case MalformedFcmMessage(data: final data): + _logger.severe('Received malformed FCM message: $data'); } } @@ -243,50 +244,41 @@ class NotificationService { 'Processing a FCM message from ${fromBackground ? 'background' : 'foreground'}: ${message.data}', ); - final RemoteNotification? notification = message.notification; - final messageType = message.data['lichess.type'] as String?; - - if (messageType != null) { - switch (ServerNotificationType.fromString(messageType)) { - case ServerNotificationType.corresAlarm: - case ServerNotificationType.gameTakebackOffer: - case ServerNotificationType.gameDrawOffer: - case ServerNotificationType.gameMove: - case ServerNotificationType.gameFinish: - final gameFullId = message.data['lichess.fullId'] as String?; - final round = message.data['lichess.round'] as String?; - - if (gameFullId != null && round != null) { - final fullId = GameFullId(gameFullId); - final game = PlayableGame.fromServerJson( - jsonDecode(round) as Map, - ); - await _ref.read(correspondenceServiceProvider).onServerUpdateEvent( - fullId, - game, - fromBackground: fromBackground, - ); - } - - if (fromBackground == false && - gameFullId != null && - notification != null) { - await show( - CorresGameUpdateNotification( - GameFullId(gameFullId), - notification.title!, - notification.body!, - ), - ); - } - - // TODO: handle other notification types + final parsedMessage = FcmMessage.fromRemoteMessage(message); + + switch (parsedMessage) { + case CorresGameUpdateFcmMessage( + fullId: final fullId, + game: final game, + notification: final notification + ): + if (game != null) { + await _ref.read(correspondenceServiceProvider).onServerUpdateEvent( + fullId, + game, + fromBackground: fromBackground, + ); + } - case ServerNotificationType.unhandled: - _logger.warning( - 'Received unhandled FCM notification type: $messageType', + if (fromBackground == false && notification != null) { + await show( + CorresGameUpdateNotification( + fullId, + notification.title!, + notification.body!, + ), ); - } + } + + // TODO: handle other notification types + + case UnhandledFcmMessage(data: final data): + _logger.warning( + 'Received unhandled FCM notification type: ${data['lichess.type']}', + ); + + case MalformedFcmMessage(data: final data): + _logger.severe('Received malformed FCM message: $data'); } // update badge diff --git a/lib/src/model/notifications/notifications.dart b/lib/src/model/notifications/notifications.dart index fa8d319243..1b3fdd6dfd 100644 --- a/lib/src/model/notifications/notifications.dart +++ b/lib/src/model/notifications/notifications.dart @@ -1,50 +1,114 @@ +import 'dart:convert'; + import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:lichess_mobile/l10n/l10n.dart'; import 'package:lichess_mobile/src/model/challenge/challenge.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/game/playable_game.dart'; import 'package:meta/meta.dart'; -/// Notification types defined by the server. +/// FCM Messages +/////////////// + +/// Parsed data from an FCM message. /// -/// This corresponds to the 'lichess.type' field in the FCM message's data. -enum ServerNotificationType { - /// There is not much time left to make a move in a correspondence game. - corresAlarm, - - /// A takeback offer has been made in a correspondence game. - gameTakebackOffer, - - /// A draw offer has been made in a correspondence game. - gameDrawOffer, - - /// A move has been made in a correspondence game. - gameMove, - - /// A correspondence game just finished. - gameFinish, - - /// Server notification type not handled by the app. - unhandled; - - static ServerNotificationType fromString(String type) { - switch (type) { - case 'corresAlarm': - return corresAlarm; - case 'gameTakebackOffer': - return gameTakebackOffer; - case 'gameDrawOffer': - return gameDrawOffer; - case 'gameMove': - return gameMove; - case 'gameFinish': - return gameFinish; - default: - return unhandled; +/// Messages received from Firebase Cloud Messaging (FCM) service support different +/// kind of use cases, depending on the message content: +/// +/// * when no [RemoteMessage.notification] is present, the message is a data message, +/// and the application can handle it in the foreground or background; It typically +/// serves to update the application state silently. +/// +/// * when a [RemoteMessage.notification] is present, the message is a notification message. +/// If the application is in the background, the system displays the notification to the user. +/// If the application is in the foreground, the system does not display the notification +/// automatically, but we might want to display it ourselves. +/// A notification message can contain additional data in the [RemoteMessage.data] field, +/// which can also be used to update the application state. +@immutable +sealed class FcmMessage { + const FcmMessage(); + + RemoteNotification? get notification; + + factory FcmMessage.fromRemoteMessage(RemoteMessage message) { + final messageType = message.data['lichess.type'] as String?; + + if (messageType == null) { + return UnhandledFcmMessage(message.data); + } else { + switch (messageType) { + case 'corresAlarm': + case 'gameTakebackOffer': + case 'gameDrawOffer': + case 'gameMove': + case 'gameFinish': + final gameFullId = message.data['lichess.fullId'] as String?; + final round = message.data['lichess.round'] as String?; + if (gameFullId != null) { + final fullId = GameFullId(gameFullId); + final game = round != null + ? PlayableGame.fromServerJson( + jsonDecode(round) as Map, + ) + : null; + return CorresGameUpdateFcmMessage( + fullId, + game: game, + notification: message.notification, + ); + } else { + return MalformedFcmMessage(message.data); + } + default: + return UnhandledFcmMessage(message.data); + } } } } +/// An [FcmMessage] that represents a correspondence game update. +@immutable +class CorresGameUpdateFcmMessage extends FcmMessage { + const CorresGameUpdateFcmMessage( + this.fullId, { + required this.game, + required this.notification, + }); + + final GameFullId fullId; + final PlayableGame? game; + + @override + final RemoteNotification? notification; +} + +/// An [FcmMessage] that could not be parsed. +@immutable +class MalformedFcmMessage extends FcmMessage { + const MalformedFcmMessage(this.data); + + final Map data; + + @override + RemoteNotification? get notification => null; +} + +/// An [FcmMessage] that is not handled by the application. +@immutable +class UnhandledFcmMessage extends FcmMessage { + const UnhandledFcmMessage(this.data); + + final Map data; + + @override + RemoteNotification? get notification => null; +} + +/// Local Notifications +/////////////////////// + /// A notification shown to the user from the platform's notification system. @immutable sealed class LocalNotification { diff --git a/lib/src/utils/connectivity.dart b/lib/src/utils/connectivity.dart index 214baad4e8..df9309a6da 100644 --- a/lib/src/utils/connectivity.dart +++ b/lib/src/utils/connectivity.dart @@ -15,6 +15,10 @@ part 'connectivity.g.dart'; final _logger = Logger('Connectivity'); +/// A provider that exposes a [Connectivity] instance. +@Riverpod(keepAlive: true) +Connectivity connectivityPlugin(ConnectivityPluginRef _) => Connectivity(); + /// This provider is used to check the device's connectivity status, reacting to /// changes in connectivity and app lifecycle events. /// @@ -22,23 +26,25 @@ final _logger = Logger('Connectivity'); /// - Uses [AppLifecycleListener] to check connectivity on app resume @Riverpod(keepAlive: true) class ConnectivityChanges extends _$ConnectivityChanges { - StreamSubscription>? _socketSubscription; + StreamSubscription>? _connectivitySubscription; AppLifecycleListener? _appLifecycleListener; final _connectivityChangesDebouncer = Debouncer(const Duration(seconds: 5)); Client get _defaultClient => ref.read(defaultClientProvider); + Connectivity get _connectivity => ref.read(connectivityPluginProvider); @override Future build() { ref.onDispose(() { - _socketSubscription?.cancel(); + _connectivitySubscription?.cancel(); _appLifecycleListener?.dispose(); _connectivityChangesDebouncer.dispose(); }); - _socketSubscription?.cancel(); - _socketSubscription = Connectivity().onConnectivityChanged.listen((result) { + _connectivitySubscription?.cancel(); + _connectivitySubscription = + _connectivity.onConnectivityChanged.listen((result) { _connectivityChangesDebouncer(() => _onConnectivityChange(result)); }); @@ -48,7 +54,7 @@ class ConnectivityChanges extends _$ConnectivityChanges { onStateChange: _onAppLifecycleChange, ); - return Connectivity() + return _connectivity .checkConnectivity() .then((r) => _getConnectivityStatus(r, appState)); } @@ -59,7 +65,7 @@ class ConnectivityChanges extends _$ConnectivityChanges { } if (appState == AppLifecycleState.resumed) { - final newConn = await Connectivity() + final newConn = await _connectivity .checkConnectivity() .then((r) => _getConnectivityStatus(r, appState)); diff --git a/pubspec.lock b/pubspec.lock index 0df15aa7f2..cb21061a9c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -399,7 +399,7 @@ packages: source: hosted version: "2.0.5" fake_async: - dependency: transitive + dependency: "direct dev" description: name: fake_async sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" diff --git a/pubspec.yaml b/pubspec.yaml index 4a16ffdeed..5acf821671 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -79,6 +79,7 @@ dependencies: dev_dependencies: build_runner: ^2.3.2 custom_lint: ^0.6.0 + fake_async: ^1.3.1 flutter_test: sdk: flutter freezed: ^2.3.4 diff --git a/test/binding.dart b/test/binding.dart index 579bf76327..e9d0893006 100644 --- a/test/binding.dart +++ b/test/binding.dart @@ -2,10 +2,13 @@ import 'dart:async'; import 'dart:ui'; import 'package:firebase_messaging/firebase_messaging.dart'; -import 'package:flutter_local_notifications/src/flutter_local_notifications_plugin.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:lichess_mobile/src/binding.dart'; +/// The binding instance used in tests. +TestLichessBinding get testBinding => TestLichessBinding.instance; + +/// Binding instance for testing. class TestLichessBinding extends LichessBinding { TestLichessBinding(); @@ -31,8 +34,14 @@ class TestLichessBinding extends LichessBinding { _instance = this; } + /// Reset the binding instance. + /// + /// Should be called using [addTearDown] in tests. + void reset() { + _firebaseMessaging = null; + } + FakeFirebaseMessaging? _firebaseMessaging; - FakeFlutterLocalNotificationsPlugin? _notificationsPlugin; @override Future initializeNotifications(Locale locale) async {} @@ -54,13 +63,130 @@ class TestLichessBinding extends LichessBinding { @override Stream get firebaseMessagingOnMessageOpenedApp => firebaseMessaging.onMessageOpenedApp.stream; - - @override - FakeFlutterLocalNotificationsPlugin get notifications => - _notificationsPlugin ??= FakeFlutterLocalNotificationsPlugin(); } +typedef FirebaseMessagingRequestPermissionCall = ({ + bool alert, + bool announcement, + bool badge, + bool carPlay, + bool criticalAlert, + bool provisional, + bool sound, +}); + class FakeFirebaseMessaging extends Fake implements FirebaseMessaging { + /// Whether [requestPermission] will grant permission. + bool _willGrantPermission = true; + + /// Set whether [requestPermission] will grant permission. + // ignore: avoid_setters_without_getters + set willGrantPermission(bool value) { + _willGrantPermission = value; + } + + List verifyRequestPermissionCalls() { + final result = _requestPermissionCalls; + _requestPermissionCalls = []; + return result; + } + + List _requestPermissionCalls = []; + + NotificationSettings _notificationSettings = const NotificationSettings( + alert: AppleNotificationSetting.disabled, + announcement: AppleNotificationSetting.disabled, + authorizationStatus: AuthorizationStatus.notDetermined, + badge: AppleNotificationSetting.disabled, + carPlay: AppleNotificationSetting.disabled, + lockScreen: AppleNotificationSetting.disabled, + notificationCenter: AppleNotificationSetting.disabled, + showPreviews: AppleShowPreviewSetting.always, + timeSensitive: AppleNotificationSetting.disabled, + criticalAlert: AppleNotificationSetting.disabled, + sound: AppleNotificationSetting.disabled, + ); + + @override + Future requestPermission({ + bool alert = true, + bool announcement = false, + bool badge = true, + bool carPlay = false, + bool criticalAlert = false, + bool provisional = false, + bool sound = true, + }) async { + _requestPermissionCalls.add( + ( + alert: alert, + announcement: announcement, + badge: badge, + carPlay: carPlay, + criticalAlert: criticalAlert, + provisional: provisional, + sound: sound, + ), + ); + return _notificationSettings = NotificationSettings( + alert: alert + ? AppleNotificationSetting.enabled + : AppleNotificationSetting.disabled, + announcement: announcement + ? AppleNotificationSetting.enabled + : AppleNotificationSetting.disabled, + authorizationStatus: _willGrantPermission + ? AuthorizationStatus.authorized + : AuthorizationStatus.denied, + badge: badge + ? AppleNotificationSetting.enabled + : AppleNotificationSetting.disabled, + carPlay: carPlay + ? AppleNotificationSetting.enabled + : AppleNotificationSetting.disabled, + lockScreen: AppleNotificationSetting.enabled, + notificationCenter: AppleNotificationSetting.enabled, + showPreviews: AppleShowPreviewSetting.whenAuthenticated, + timeSensitive: AppleNotificationSetting.disabled, + criticalAlert: criticalAlert + ? AppleNotificationSetting.enabled + : AppleNotificationSetting.disabled, + sound: sound + ? AppleNotificationSetting.enabled + : AppleNotificationSetting.disabled, + ); + } + + @override + Future getNotificationSettings() { + return Future.value(_notificationSettings); + } + + @override + Future getInitialMessage() async { + return null; + } + + // assume the token is initially available + String? _token = 'test-token'; + + void setToken(String token) { + _token = token; + _tokenController.add(token); + } + + final StreamController _tokenController = + StreamController.broadcast(); + + @override + Future getToken({String? vapidKey}) async { + assert(vapidKey == null); + return _token; + } + + @override + Stream get onTokenRefresh => _tokenController.stream; + /// Controller for [onMessage]. /// /// Call [StreamController.add] to simulate a message received from FCM while @@ -81,6 +207,3 @@ class FakeFirebaseMessaging extends Fake implements FirebaseMessaging { StreamController onBackgroundMessage = StreamController.broadcast(); } - -class FakeFlutterLocalNotificationsPlugin extends Fake - implements FlutterLocalNotificationsPlugin {} diff --git a/test/model/game/game_test.dart b/test/model/game/game_test.dart index 20bea8e841..93dd1132c4 100644 --- a/test/model/game/game_test.dart +++ b/test/model/game/game_test.dart @@ -8,7 +8,7 @@ void main() { group('PlayableGame', () { test('makePgn, unfinished game', () { final game = PlayableGame.fromServerJson( - jsonDecode(_unfishinedPlayableGameJson) as Map, + jsonDecode(_unfinishedGameJson) as Map, ); expect( @@ -113,7 +113,7 @@ void main() { }); } -const _unfishinedPlayableGameJson = ''' +const _unfinishedGameJson = ''' {"game":{"id":"Fn9UvVKF","variant":{"key":"standard","name":"Standard","short":"Std"},"speed":"bullet","perf":"bullet","rated":true,"fen":"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1","turns":0,"source":"lobby","status":{"id":20,"name":"started"},"createdAt":1706204482969,"pgn":""},"white":{"user":{"name":"chabrot","id":"chabrot"},"rating":1801},"black":{"user":{"name":"veloce","id":"veloce"},"rating":1798},"socket":0,"expiration":{"idleMillis":67,"millisToMove":20000},"clock":{"running":false,"initial":120,"increment":1,"white":120,"black":120,"emerg":15,"moretime":15},"takebackable":true,"youAre":"black","prefs":{"autoQueen":2,"zen":2,"confirmResign":true,"enablePremove":true},"chat":{"lines":[]}} '''; diff --git a/test/model/notifications/fake_notification_display.dart b/test/model/notifications/fake_notification_display.dart new file mode 100644 index 0000000000..47484acfa6 --- /dev/null +++ b/test/model/notifications/fake_notification_display.dart @@ -0,0 +1,35 @@ +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:flutter_test/flutter_test.dart'; + +class FakeNotificationDisplay extends Fake + implements FlutterLocalNotificationsPlugin { + final Map _activeNotifications = {}; + + @override + Future show( + int id, + String? title, + String? body, + NotificationDetails? notificationDetails, { + String? payload, + }) { + _activeNotifications[id] = ActiveNotification( + id: id, + title: title, + body: body, + payload: payload, + ); + return Future.value(); + } + + @override + Future cancel(int id, {String? tag}) { + _activeNotifications.remove(id); + return Future.value(); + } + + @override + Future> getActiveNotifications() { + return Future.value(_activeNotifications.values.toList()); + } +} diff --git a/test/model/notifications/fake_notification_service.dart b/test/model/notifications/fake_notification_service.dart deleted file mode 100644 index 7337f1b49e..0000000000 --- a/test/model/notifications/fake_notification_service.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:lichess_mobile/src/model/notifications/notification_service.dart'; -import 'package:lichess_mobile/src/model/notifications/notifications.dart'; - -class FakeNotificationService implements NotificationService { - Map notifications = {}; - - @override - Future start() async {} - - @override - Future registerDevice() async {} - - @override - Future unregister() async {} - - @override - Future cancel(int id) async { - notifications.remove(id); - } - - @override - Future show(LocalNotification notification) async { - notifications[notification.id] = notification; - return notification.id; - } -} diff --git a/test/model/notifications/notification_service_test.dart b/test/model/notifications/notification_service_test.dart new file mode 100644 index 0000000000..ba8d361fd7 --- /dev/null +++ b/test/model/notifications/notification_service_test.dart @@ -0,0 +1,285 @@ +import 'dart:convert'; + +import 'package:fake_async/fake_async.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/testing.dart'; +import 'package:lichess_mobile/src/model/common/http.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/correspondence/correspondence_service.dart'; +import 'package:lichess_mobile/src/model/game/playable_game.dart'; +import 'package:lichess_mobile/src/model/notifications/notification_service.dart'; +import 'package:lichess_mobile/src/model/notifications/notifications.dart'; +import 'package:mocktail/mocktail.dart'; +import '../../binding.dart'; +import '../../test_container.dart'; +import '../../test_utils.dart'; +import '../auth/fake_session_storage.dart'; + +class NotificationDisplayMock extends Mock + implements FlutterLocalNotificationsPlugin {} + +class CorrespondenceServiceMock extends Mock implements CorrespondenceService {} + +class FakePlayableGame extends Fake implements PlayableGame {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + final notificationDisplayMock = NotificationDisplayMock(); + final correspondenceServiceMock = CorrespondenceServiceMock(); + + int registerDeviceCalls = 0; + + setUpAll(() { + registerFallbackValue(FakePlayableGame()); + }); + + tearDown(() { + registerDeviceCalls = 0; + reset(notificationDisplayMock); + }); + + final registerMockClient = MockClient((request) { + if (request.url.path == '/mobile/register/firebase/test-token') { + registerDeviceCalls++; + return mockResponse('{"ok": true}', 200); + } + return mockResponse('', 404); + }); + + group('start service', () { + test('request permissions', () async { + final container = await makeContainer(); + + final notificationService = container.read(notificationServiceProvider); + + await notificationService.start(); + + final calls = + testBinding.firebaseMessaging.verifyRequestPermissionCalls(); + expect(calls, hasLength(1)); + expect( + calls.first, + equals( + ( + alert: true, + badge: true, + sound: true, + announcement: false, + carPlay: false, + criticalAlert: false, + provisional: false, + ), + ), + ); + }); + + test( + 'register device when online, token exists and permissions are granted and a session exists', + () async { + final container = await makeContainer( + userSession: fakeSession, + overrides: [ + lichessClientProvider.overrideWith( + (ref) => LichessClient(registerMockClient, ref), + ), + ], + ); + + final notificationService = container.read(notificationServiceProvider); + + FakeAsync().run((async) { + notificationService.start(); + + async.flushMicrotasks(); + + expect(registerDeviceCalls, 1); + }); + }); + + test("don't try to register device when permissions are not granted", + () async { + final container = await makeContainer( + userSession: fakeSession, + overrides: [ + lichessClientProvider.overrideWith( + (ref) => LichessClient(registerMockClient, ref), + ), + ], + ); + + final notificationService = container.read(notificationServiceProvider); + + FakeAsync().run((async) { + testBinding.firebaseMessaging.willGrantPermission = false; + + notificationService.start(); + + async.flushMicrotasks(); + + expect(registerDeviceCalls, 0); + }); + }); + + test("don't try to register device when user is not logged in", () async { + final container = await makeContainer( + overrides: [ + lichessClientProvider.overrideWith( + (ref) => LichessClient(registerMockClient, ref), + ), + ], + ); + + final notificationService = container.read(notificationServiceProvider); + + FakeAsync().run((async) { + notificationService.start(); + + async.flushMicrotasks(); + + expect(registerDeviceCalls, 0); + }); + }); + }); + + group('receive and show notification', () { + test('correspondence game update shows a notification in foreground', + () async { + final container = await makeContainer( + userSession: fakeSession, + overrides: [ + lichessClientProvider.overrideWith( + (ref) => LichessClient(registerMockClient, ref), + ), + notificationDisplayProvider + .overrideWith((_) => notificationDisplayMock), + ], + ); + + final notificationService = container.read(notificationServiceProvider); + + const fullId = GameFullId('9wlmxmibr9gh'); + + when( + () => notificationDisplayMock.show( + any(), + any(), + any(), + any(), + payload: any(named: 'payload'), + ), + ).thenAnswer((_) => Future.value()); + + FakeAsync().run((async) { + notificationService.start(); + + async.flushMicrotasks(); + + testBinding.firebaseMessaging.onMessage.add( + const RemoteMessage( + data: { + 'lichess.type': 'gameMove', + 'lichess.fullId': '9wlmxmibr9gh', + }, + notification: RemoteNotification( + title: 'It is your turn!', + body: 'Dr-Alaakour played a move', + ), + ), + ); + + async.flushMicrotasks(); + + const expectedNotif = CorresGameUpdateNotification( + fullId, + 'It is your turn!', + 'Dr-Alaakour played a move', + ); + + final result = verify( + () => notificationDisplayMock.show( + fullId.hashCode, + 'It is your turn!', + 'Dr-Alaakour played a move', + captureAny(), + payload: jsonEncode(expectedNotif.payload), + ), + ); + + result.called(1); + expect( + result.captured[0], + isA() + .having( + (d) => d.android?.importance, + 'importance', + Importance.high, + ) + .having( + (d) => d.android?.priority, + 'priority', + Priority.defaultPriority, + ), + ); + }); + }); + + test('correspondence game update data message updates the game', () async { + final container = await makeContainer( + userSession: fakeSession, + overrides: [ + lichessClientProvider.overrideWith( + (ref) => LichessClient(registerMockClient, ref), + ), + correspondenceServiceProvider + .overrideWith((_) => correspondenceServiceMock), + ], + ); + + final notificationService = container.read(notificationServiceProvider); + + const fullId = GameFullId('Fn9UvVKFsopx'); + + when( + () => correspondenceServiceMock.onServerUpdateEvent( + fullId, + any(that: isA()), + fromBackground: false, + ), + ).thenAnswer((_) => Future.value()); + + FakeAsync().run((async) { + notificationService.start(); + + async.flushMicrotasks(); + + testBinding.firebaseMessaging.onMessage.add( + const RemoteMessage( + data: { + 'lichess.type': 'gameMove', + 'lichess.fullId': 'Fn9UvVKFsopx', + 'lichess.round': + '{"game":{"id":"Fn9UvVKF","variant":{"key":"standard","name":"Standard","short":"Std"},"speed":"bullet","perf":"bullet","rated":true,"fen":"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1","turns":0,"source":"lobby","status":{"id":20,"name":"started"},"createdAt":1706204482969,"pgn":""},"white":{"user":{"name":"chabrot","id":"chabrot"},"rating":1801},"black":{"user":{"name":"veloce","id":"veloce"},"rating":1798},"socket":0,"expiration":{"idleMillis":67,"millisToMove":20000},"clock":{"running":false,"initial":120,"increment":1,"white":120,"black":120,"emerg":15,"moretime":15},"takebackable":true,"youAre":"black","prefs":{"autoQueen":2,"zen":2,"confirmResign":true,"enablePremove":true},"chat":{"lines":[]}}', + }, + notification: RemoteNotification( + title: 'It is your turn!', + body: 'Dr-Alaakour played a move', + ), + ), + ); + + async.flushMicrotasks(); + + verify( + () => correspondenceServiceMock.onServerUpdateEvent( + fullId, + any(that: isA()), + fromBackground: false, + ), + ).called(1); + }); + }); + }); +} diff --git a/test/test_app.dart b/test/test_app.dart index a6bf8fef78..fb90dfbd6c 100644 --- a/test/test_app.dart +++ b/test/test_app.dart @@ -33,10 +33,11 @@ import 'package:visibility_detector/visibility_detector.dart'; import './fake_crashlytics.dart'; import './model/auth/fake_session_storage.dart'; import './model/common/service/fake_sound_service.dart'; +import 'binding.dart'; import 'model/common/fake_websocket_channel.dart'; import 'model/game/fake_game_storage.dart'; -import 'model/notifications/fake_notification_service.dart'; -import 'utils/fake_connectivity_changes.dart'; +import 'model/notifications/fake_notification_display.dart'; +import 'utils/fake_connectivity.dart'; final mockClient = MockClient((request) async { return http.Response('', 200); @@ -54,6 +55,10 @@ Future buildTestApp( AuthSessionState? userSession, Map? defaultPreferences, }) async { + final binding = TestLichessBinding.ensureInitialized(); + + addTearDown(binding.reset); + await tester.binding.setSurfaceSize(kTestSurfaceSize); VisibilityDetectorController.instance.updateInterval = Duration.zero; @@ -92,6 +97,10 @@ Future buildTestApp( return ProviderScope( overrides: [ + // ignore: scoped_providers_should_specify_dependencies + notificationDisplayProvider.overrideWith((ref) { + return FakeNotificationDisplay(); + }), // ignore: scoped_providers_should_specify_dependencies databaseProvider.overrideWith((ref) async { final db = @@ -118,16 +127,14 @@ Future buildTestApp( return pool; }), // ignore: scoped_providers_should_specify_dependencies - connectivityChangesProvider.overrideWith(() { - return FakeConnectivityChanges(); + connectivityPluginProvider.overrideWith((_) { + return FakeConnectivity(); }), // ignore: scoped_providers_should_specify_dependencies showRatingsPrefProvider.overrideWith((ref) { return true; }), // ignore: scoped_providers_should_specify_dependencies - notificationServiceProvider.overrideWithValue(FakeNotificationService()), - // ignore: scoped_providers_should_specify_dependencies crashlyticsProvider.overrideWithValue(FakeCrashlytics()), // ignore: scoped_providers_should_specify_dependencies soundServiceProvider.overrideWithValue(FakeSoundService()), diff --git a/test/test_container.dart b/test/test_container.dart index 564ae17565..d58573d350 100644 --- a/test/test_container.dart +++ b/test/test_container.dart @@ -17,18 +17,21 @@ import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/notifications/notification_service.dart'; import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:logging/logging.dart'; -import 'package:mocktail/mocktail.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import './fake_crashlytics.dart'; import './model/common/service/fake_sound_service.dart'; +import 'binding.dart'; import 'model/common/fake_websocket_channel.dart'; -import 'model/notifications/fake_notification_service.dart'; -import 'utils/fake_connectivity_changes.dart'; +import 'model/notifications/fake_notification_display.dart'; +import 'utils/fake_connectivity.dart'; -class MockHttpClient extends Mock implements http.Client {} +/// A mock client that always returns a 200 empty response. +final testContainerMockClient = MockClient((request) async { + return http.Response('', 200); +}); const shouldLog = false; @@ -49,6 +52,8 @@ Future makeContainer({ List? overrides, AuthSessionState? userSession, }) async { + final binding = TestLichessBinding.ensureInitialized(); + SharedPreferences.setMockInitialValues({}); final sharedPreferences = await SharedPreferences.getInstance(); @@ -67,6 +72,12 @@ Future makeContainer({ final container = ProviderContainer( overrides: [ + connectivityPluginProvider.overrideWith((_) { + return FakeConnectivity(); + }), + notificationDisplayProvider.overrideWith((ref) { + return FakeNotificationDisplay(); + }), databaseProvider.overrideWith((ref) async { final db = await openAppDatabase(databaseFactoryFfi, inMemoryDatabasePath); @@ -82,44 +93,47 @@ Future makeContainer({ return pool; }), lichessClientProvider.overrideWith((ref) { - return LichessClient(MockHttpClient(), ref); + return LichessClient(testContainerMockClient, ref); }), - connectivityChangesProvider.overrideWith(() { - return FakeConnectivityChanges(); - }), - defaultClientProvider.overrideWithValue(MockHttpClient()), + defaultClientProvider.overrideWithValue(testContainerMockClient), crashlyticsProvider.overrideWithValue(FakeCrashlytics()), - notificationServiceProvider.overrideWithValue(FakeNotificationService()), soundServiceProvider.overrideWithValue(FakeSoundService()), sharedPreferencesProvider.overrideWithValue(sharedPreferences), cachedDataProvider.overrideWith((ref) { - return CachedData( - packageInfo: PackageInfo( - appName: 'lichess_mobile_test', - version: 'test', - buildNumber: '0.0.0', - packageName: 'lichess_mobile_test', + return Future.value( + CachedData( + packageInfo: PackageInfo( + appName: 'lichess_mobile_test', + version: 'test', + buildNumber: '0.0.0', + packageName: 'lichess_mobile_test', + ), + deviceInfo: BaseDeviceInfo({ + 'name': 'test', + 'model': 'test', + 'manufacturer': 'test', + 'systemName': 'test', + 'systemVersion': 'test', + 'identifierForVendor': 'test', + 'isPhysicalDevice': true, + }), + sharedPreferences: sharedPreferences, + initialUserSession: userSession, + sri: 'test', + engineMaxMemoryInMb: 16, ), - deviceInfo: BaseDeviceInfo({ - 'name': 'test', - 'model': 'test', - 'manufacturer': 'test', - 'systemName': 'test', - 'systemVersion': 'test', - 'identifierForVendor': 'test', - 'isPhysicalDevice': true, - }), - sharedPreferences: sharedPreferences, - initialUserSession: userSession, - sri: 'test', - engineMaxMemoryInMb: 16, ); }), ...overrides ?? [], ], ); + addTearDown(binding.reset); addTearDown(container.dispose); + addTearDown(sharedPreferences.clear); + + // initialize the cached data provider + await container.read(cachedDataProvider.future); return container; } diff --git a/test/utils/fake_connectivity.dart b/test/utils/fake_connectivity.dart new file mode 100644 index 0000000000..3a920a4bdf --- /dev/null +++ b/test/utils/fake_connectivity.dart @@ -0,0 +1,21 @@ +import 'dart:async'; + +import 'package:connectivity_plus/connectivity_plus.dart'; + +/// A fake implementation of [Connectivity] that always returns [ConnectivityResult.wifi]. +class FakeConnectivity implements Connectivity { + @override + Future> checkConnectivity() { + return Future.value([ConnectivityResult.wifi]); + } + + /// A broadcast stream controller of connectivity changes. + /// + /// This is used to simulate connectivity changes in tests. + static StreamController> controller = + StreamController.broadcast(); + + @override + Stream> get onConnectivityChanged => + controller.stream; +} diff --git a/test/utils/fake_connectivity_changes.dart b/test/utils/fake_connectivity_changes.dart deleted file mode 100644 index 9f19a707c9..0000000000 --- a/test/utils/fake_connectivity_changes.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:lichess_mobile/src/utils/connectivity.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'fake_connectivity_changes.g.dart'; - -@riverpod -class FakeConnectivityChanges extends _$FakeConnectivityChanges - implements ConnectivityChanges { - @override - Future build() async { - return const ConnectivityStatus( - isOnline: true, - ); - } -} From 85275c6113bfa867c51a56599910e82f9611870d Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 27 Sep 2024 17:38:50 +0200 Subject: [PATCH 400/979] Tweaks --- lib/src/binding.dart | 2 +- test/binding.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/binding.dart b/lib/src/binding.dart index b1679880a7..a2c5e71322 100644 --- a/lib/src/binding.dart +++ b/lib/src/binding.dart @@ -18,7 +18,7 @@ import 'package:lichess_mobile/src/model/notifications/notifications.dart'; /// provider with riverpod because it gives more flexibility and control over /// the behavior of the fake. /// However, if the plugin is used in a way that doesn't allow for easy mocking -///with riverpod, a binding can be used to provide a fake implementation. +/// with riverpod, a test binding can be used to provide a fake implementation. abstract class LichessBinding { LichessBinding() : assert(_instance == null) { initInstance(); diff --git a/test/binding.dart b/test/binding.dart index e9d0893006..ab9205c01c 100644 --- a/test/binding.dart +++ b/test/binding.dart @@ -8,7 +8,7 @@ import 'package:lichess_mobile/src/binding.dart'; /// The binding instance used in tests. TestLichessBinding get testBinding => TestLichessBinding.instance; -/// Binding instance for testing. +/// Lichess binding for testing. class TestLichessBinding extends LichessBinding { TestLichessBinding(); From 13c79895a8e74a40ef39eaed6e5e4763690e49e1 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 27 Sep 2024 18:52:40 +0200 Subject: [PATCH 401/979] Tests cleanup --- lib/src/model/auth/session_storage.dart | 8 +++--- test/model/game/fake_game_storage.dart | 37 ------------------------- test/test_app.dart | 36 ++++++++---------------- 3 files changed, 15 insertions(+), 66 deletions(-) delete mode 100644 test/model/game/fake_game_storage.dart diff --git a/lib/src/model/auth/session_storage.dart b/lib/src/model/auth/session_storage.dart index 549594a0cd..874ba169ac 100644 --- a/lib/src/model/auth/session_storage.dart +++ b/lib/src/model/auth/session_storage.dart @@ -7,7 +7,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'session_storage.g.dart'; -const _kSessionStorageKey = '$kLichessHost.userSession'; +const kSessionStorageKey = '$kLichessHost.userSession'; @Riverpod(keepAlive: true) SessionStorage sessionStorage(SessionStorageRef ref) { @@ -18,7 +18,7 @@ class SessionStorage { const SessionStorage(); Future read() async { - final string = await SecureStorage.instance.read(key: _kSessionStorageKey); + final string = await SecureStorage.instance.read(key: kSessionStorageKey); if (string != null) { return AuthSessionState.fromJson( jsonDecode(string) as Map, @@ -29,12 +29,12 @@ class SessionStorage { Future write(AuthSessionState session) async { await SecureStorage.instance.write( - key: _kSessionStorageKey, + key: kSessionStorageKey, value: jsonEncode(session.toJson()), ); } Future delete() async { - await SecureStorage.instance.delete(key: _kSessionStorageKey); + await SecureStorage.instance.delete(key: kSessionStorageKey); } } diff --git a/test/model/game/fake_game_storage.dart b/test/model/game/fake_game_storage.dart deleted file mode 100644 index 2d3dedc38d..0000000000 --- a/test/model/game/fake_game_storage.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/model/game/archived_game.dart'; -import 'package:lichess_mobile/src/model/game/game_filter.dart'; -import 'package:lichess_mobile/src/model/game/game_storage.dart'; - -class FakeGameStorage implements GameStorage { - @override - Future delete(GameId gameId) { - return Future.value(); - } - - @override - Future fetch({required GameId gameId}) { - return Future.value(null); - } - - @override - Future> page({ - UserId? userId, - DateTime? until, - int max = 10, - GameFilterState filter = const GameFilterState(), - }) { - return Future.value(IList()); - } - - @override - Future save(ArchivedGame game) { - return Future.value(); - } - - @override - Future count({UserId? userId}) { - return Future.value(0); - } -} diff --git a/test/test_app.dart b/test/test_app.dart index fb90dfbd6c..67dd329d38 100644 --- a/test/test_app.dart +++ b/test/test_app.dart @@ -12,7 +12,6 @@ import 'package:intl/intl.dart'; import 'package:lichess_mobile/l10n/l10n.dart'; import 'package:lichess_mobile/src/crashlytics.dart'; import 'package:lichess_mobile/src/db/database.dart'; -import 'package:lichess_mobile/src/db/shared_preferences.dart'; import 'package:lichess_mobile/src/init.dart'; import 'package:lichess_mobile/src/model/account/account_preferences.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; @@ -20,7 +19,6 @@ import 'package:lichess_mobile/src/model/auth/session_storage.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; -import 'package:lichess_mobile/src/model/game/game_storage.dart'; import 'package:lichess_mobile/src/model/notifications/notification_service.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/utils/connectivity.dart'; @@ -31,11 +29,9 @@ import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:visibility_detector/visibility_detector.dart'; import './fake_crashlytics.dart'; -import './model/auth/fake_session_storage.dart'; import './model/common/service/fake_sound_service.dart'; import 'binding.dart'; import 'model/common/fake_websocket_channel.dart'; -import 'model/game/fake_game_storage.dart'; import 'model/notifications/fake_notification_display.dart'; import 'utils/fake_connectivity.dart'; @@ -81,6 +77,8 @@ Future buildTestApp( FlutterSecureStorage.setMockInitialValues({ kSRIStorageKey: 'test', + if (userSession != null) + kSessionStorageKey: jsonEncode(userSession.toJson()), }); // TODO consider loading true fonts as well @@ -103,19 +101,19 @@ Future buildTestApp( }), // ignore: scoped_providers_should_specify_dependencies databaseProvider.overrideWith((ref) async { - final db = - await openAppDatabase(databaseFactoryFfi, inMemoryDatabasePath); - ref.onDispose(db.close); - return db; + final testDb = await openAppDatabase( + databaseFactoryFfiNoIsolate, + inMemoryDatabasePath, + ); + ref.onDispose(testDb.close); + return testDb; }), // ignore: scoped_providers_should_specify_dependencies lichessClientProvider.overrideWith((ref) { return LichessClient(mockClient, ref); }), // ignore: scoped_providers_should_specify_dependencies - defaultClientProvider.overrideWith((_) { - return mockClient; - }), + defaultClientProvider.overrideWith((_) => mockClient), // ignore: scoped_providers_should_specify_dependencies webSocketChannelFactoryProvider.overrideWith((ref) { return FakeWebSocketChannelFactory(() => FakeWebSocketChannel()); @@ -127,26 +125,14 @@ Future buildTestApp( return pool; }), // ignore: scoped_providers_should_specify_dependencies - connectivityPluginProvider.overrideWith((_) { - return FakeConnectivity(); - }), + connectivityPluginProvider.overrideWith((_) => FakeConnectivity()), // ignore: scoped_providers_should_specify_dependencies - showRatingsPrefProvider.overrideWith((ref) { - return true; - }), + showRatingsPrefProvider.overrideWith((ref) => true), // ignore: scoped_providers_should_specify_dependencies crashlyticsProvider.overrideWithValue(FakeCrashlytics()), // ignore: scoped_providers_should_specify_dependencies soundServiceProvider.overrideWithValue(FakeSoundService()), // ignore: scoped_providers_should_specify_dependencies - sharedPreferencesProvider.overrideWithValue(sharedPreferences), - // ignore: scoped_providers_should_specify_dependencies - sessionStorageProvider.overrideWithValue(FakeSessionStorage(userSession)), - // ignore: scoped_providers_should_specify_dependencies - gameStorageProvider.overrideWith((_) async { - return FakeGameStorage(); - }), - // ignore: scoped_providers_should_specify_dependencies cachedDataProvider.overrideWith((ref) { return CachedData( packageInfo: PackageInfo( From 989eaeeb21d5230b8c01b7cd247a26166eb522c1 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 27 Sep 2024 22:39:17 +0200 Subject: [PATCH 402/979] Add another notification service test --- .../notification_service_test.dart | 94 ++++++++++++++++++- 1 file changed, 89 insertions(+), 5 deletions(-) diff --git a/test/model/notifications/notification_service_test.dart b/test/model/notifications/notification_service_test.dart index ba8d361fd7..32cced836a 100644 --- a/test/model/notifications/notification_service_test.dart +++ b/test/model/notifications/notification_service_test.dart @@ -39,6 +39,7 @@ void main() { tearDown(() { registerDeviceCalls = 0; reset(notificationDisplayMock); + reset(correspondenceServiceMock); }); final registerMockClient = MockClient((request) { @@ -49,7 +50,7 @@ void main() { return mockResponse('', 404); }); - group('start service', () { + group('Start service:', () { test('request permissions', () async { final container = await makeContainer(); @@ -144,9 +145,8 @@ void main() { }); }); - group('receive and show notification', () { - test('correspondence game update shows a notification in foreground', - () async { + group('Receive and show notifications:', () { + test('FCM message will show a notification in foreground', () async { final container = await makeContainer( userSession: fakeSession, overrides: [ @@ -226,13 +226,15 @@ void main() { }); }); - test('correspondence game update data message updates the game', () async { + test('FCM game data message will update the game', () async { final container = await makeContainer( userSession: fakeSession, overrides: [ lichessClientProvider.overrideWith( (ref) => LichessClient(registerMockClient, ref), ), + notificationDisplayProvider + .overrideWith((_) => notificationDisplayMock), correspondenceServiceProvider .overrideWith((_) => correspondenceServiceMock), ], @@ -250,6 +252,16 @@ void main() { ), ).thenAnswer((_) => Future.value()); + when( + () => notificationDisplayMock.show( + any(), + any(), + any(), + any(), + payload: any(named: 'payload'), + ), + ).thenAnswer((_) => Future.value()); + FakeAsync().run((async) { notificationService.start(); @@ -279,6 +291,78 @@ void main() { fromBackground: false, ), ).called(1); + + verify( + () => notificationDisplayMock.show( + any(), + any(), + any(), + any(), + payload: any(named: 'payload'), + ), + ).called(1); + }); + }); + + test('FCM game data message without notification', () async { + final container = await makeContainer( + userSession: fakeSession, + overrides: [ + lichessClientProvider.overrideWith( + (ref) => LichessClient(registerMockClient, ref), + ), + notificationDisplayProvider + .overrideWith((_) => notificationDisplayMock), + correspondenceServiceProvider + .overrideWith((_) => correspondenceServiceMock), + ], + ); + + final notificationService = container.read(notificationServiceProvider); + + when( + () => correspondenceServiceMock.onServerUpdateEvent( + any(that: isA()), + any(that: isA()), + fromBackground: false, + ), + ).thenAnswer((_) => Future.value()); + + FakeAsync().run((async) { + notificationService.start(); + + async.flushMicrotasks(); + + testBinding.firebaseMessaging.onMessage.add( + const RemoteMessage( + data: { + 'lichess.type': 'gameMove', + 'lichess.fullId': 'Fn9UvVKFsopx', + 'lichess.round': + '{"game":{"id":"Fn9UvVKF","variant":{"key":"standard","name":"Standard","short":"Std"},"speed":"bullet","perf":"bullet","rated":true,"fen":"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1","turns":0,"source":"lobby","status":{"id":20,"name":"started"},"createdAt":1706204482969,"pgn":""},"white":{"user":{"name":"chabrot","id":"chabrot"},"rating":1801},"black":{"user":{"name":"veloce","id":"veloce"},"rating":1798},"socket":0,"expiration":{"idleMillis":67,"millisToMove":20000},"clock":{"running":false,"initial":120,"increment":1,"white":120,"black":120,"emerg":15,"moretime":15},"takebackable":true,"youAre":"black","prefs":{"autoQueen":2,"zen":2,"confirmResign":true,"enablePremove":true},"chat":{"lines":[]}}', + }, + ), + ); + + async.flushMicrotasks(); + + verify( + () => correspondenceServiceMock.onServerUpdateEvent( + any(that: isA()), + any(that: isA()), + fromBackground: false, + ), + ).called(1); + + verifyNever( + () => notificationDisplayMock.show( + any(), + any(), + any(), + any(), + payload: any(named: 'payload'), + ), + ); }); }); }); From 3b84ab93dc8c9eaffcb8c454d492e6db46084f9b Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 30 Sep 2024 11:48:44 +0200 Subject: [PATCH 403/979] Refactor navigation utils to allow testing routes --- lib/src/navigation.dart | 1 - lib/src/utils/navigation.dart | 130 ++++++++++++++++++++---- lib/src/view/settings/theme_screen.dart | 20 +--- 3 files changed, 111 insertions(+), 40 deletions(-) diff --git a/lib/src/navigation.dart b/lib/src/navigation.dart index ccfb8e605d..3967d8e8ed 100644 --- a/lib/src/navigation.dart +++ b/lib/src/navigation.dart @@ -18,7 +18,6 @@ enum BottomTab { watch, settings; - // TODO use translations when short strings are available String label(AppLocalizations strings) { switch (this) { case BottomTab.home: diff --git a/lib/src/utils/navigation.dart b/lib/src/utils/navigation.dart index 1edc004627..80e054b7e8 100644 --- a/lib/src/utils/navigation.dart +++ b/lib/src/utils/navigation.dart @@ -1,33 +1,108 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -/// Push a new route using Navigator +/// A page route that always builds the same screen widget. /// -/// Will use [MaterialPageRoute] on Android and [CupertinoPageRoute] on iOS. +/// This is useful to inspect new screens being pushed to the Navigator in tests. +abstract class ScreenRoute extends PageRoute { + /// The widget that this page route always builds. + Widget get screen; +} + +/// A [MaterialPageRoute] that always builds the same screen widget. +/// +/// This is useful to test new screens being pushed to the Navigator. +class MaterialScreenRoute extends MaterialPageRoute + implements ScreenRoute { + MaterialScreenRoute({ + required this.screen, + super.settings, + super.maintainState, + super.fullscreenDialog, + super.allowSnapshotting, + }) : super(builder: (_) => screen); + + @override + final Widget screen; +} + +/// A [CupertinoPageRoute] that always builds the same screen widget. +/// +/// This is useful to test new screens being pushed to the Navigator. +class CupertinoScreenRoute extends CupertinoPageRoute + implements ScreenRoute { + CupertinoScreenRoute({ + required this.screen, + super.settings, + super.maintainState, + super.fullscreenDialog, + super.title, + }) : super(builder: (_) => screen); + + @override + final Widget screen; +} + +/// Push a new route using Navigator. +/// +/// Either [builder] or [screen] must be provided. +/// +/// If [builder] if provided, it will return a [MaterialPageRoute] on Android and +/// a [CupertinoPageRoute] on iOS. +/// +/// If [screen] is provided, it will return a [MaterialScreenRoute] on Android and +/// a [CupertinoScreenRoute] on iOS. Future pushPlatformRoute( BuildContext context, { - required WidgetBuilder builder, + Widget? screen, + WidgetBuilder? builder, bool rootNavigator = false, bool fullscreenDialog = false, String? title, }) { + assert( + screen != null || builder != null, + 'Either screen or builder must be provided.', + ); + return Navigator.of(context, rootNavigator: rootNavigator).push( Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoPageRoute( - builder: builder, - title: title, - fullscreenDialog: fullscreenDialog, - ) - : MaterialPageRoute( - builder: builder, - fullscreenDialog: fullscreenDialog, - ), + ? builder != null + ? CupertinoPageRoute( + builder: builder, + title: title, + fullscreenDialog: fullscreenDialog, + ) + : CupertinoScreenRoute( + screen: screen!, + title: title, + fullscreenDialog: fullscreenDialog, + ) + : builder != null + ? MaterialPageRoute( + builder: builder, + fullscreenDialog: fullscreenDialog, + ) + : MaterialScreenRoute( + screen: screen!, + fullscreenDialog: fullscreenDialog, + ), ); } +/// Push a new route using Navigator and replace the current route. +/// +/// Either [builder] or [screen] must be provided. +/// +/// If [builder] if provided, it will return a [MaterialPageRoute] on Android and +/// a [CupertinoPageRoute] on iOS. +/// +/// If [screen] is provided, it will return a [MaterialScreenRoute] on Android and +/// a [CupertinoScreenRoute] on iOS. Future pushReplacementPlatformRoute( BuildContext context, { - required WidgetBuilder builder, + WidgetBuilder? builder, + Widget? screen, bool rootNavigator = false, bool fullscreenDialog = false, String? title, @@ -37,14 +112,25 @@ Future pushReplacementPlatformRoute( rootNavigator: rootNavigator, ).pushReplacement( Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoPageRoute( - builder: builder, - title: title, - fullscreenDialog: fullscreenDialog, - ) - : MaterialPageRoute( - builder: builder, - fullscreenDialog: fullscreenDialog, - ), + ? builder != null + ? CupertinoPageRoute( + builder: builder, + title: title, + fullscreenDialog: fullscreenDialog, + ) + : CupertinoScreenRoute( + screen: screen!, + title: title, + fullscreenDialog: fullscreenDialog, + ) + : builder != null + ? MaterialPageRoute( + builder: builder, + fullscreenDialog: fullscreenDialog, + ) + : MaterialScreenRoute( + screen: screen!, + fullscreenDialog: fullscreenDialog, + ), ); } diff --git a/lib/src/view/settings/theme_screen.dart b/lib/src/view/settings/theme_screen.dart index 91c08deca8..f3b4b79ea3 100644 --- a/lib/src/view/settings/theme_screen.dart +++ b/lib/src/view/settings/theme_screen.dart @@ -14,7 +14,7 @@ import 'package:lichess_mobile/src/view/settings/board_theme_screen.dart'; import 'package:lichess_mobile/src/view/settings/piece_set_screen.dart'; import 'package:lichess_mobile/src/widgets/adaptive_choice_picker.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:lichess_mobile/src/widgets/settings.dart'; class ThemeScreen extends StatelessWidget { @@ -22,25 +22,11 @@ class ThemeScreen extends StatelessWidget { @override Widget build(BuildContext context) { - return PlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, - ); - } - - Widget _androidBuilder(BuildContext context) { - return Scaffold( - appBar: AppBar(title: const Text('Theme')), + return PlatformScaffold( + appBar: const PlatformAppBar(title: Text('Theme')), body: _Body(), ); } - - Widget _iosBuilder(BuildContext context) { - return CupertinoPageScaffold( - navigationBar: const CupertinoNavigationBar(), - child: _Body(), - ); - } } String shapeColorL10n( From e5ec73c0071ae25f0d5ffc68d368249a78fa45c9 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 30 Sep 2024 15:58:06 +0200 Subject: [PATCH 404/979] Add tests for challenge service --- .../model/challenge/challenge_service.dart | 70 +++-- lib/src/model/challenge/challenges.dart | 2 +- .../challenge/challenge_service_test.dart | 269 ++++++++++++++++++ test/model/common/fake_websocket_channel.dart | 9 +- test/model/common/socket_test.dart | 16 +- .../notification_service_test.dart | 5 +- 6 files changed, 326 insertions(+), 45 deletions(-) create mode 100644 test/model/challenge/challenge_service_test.dart diff --git a/lib/src/model/challenge/challenge_service.dart b/lib/src/model/challenge/challenge_service.dart index 2e716385f1..37a076f04c 100644 --- a/lib/src/model/challenge/challenge_service.dart +++ b/lib/src/model/challenge/challenge_service.dart @@ -17,6 +17,7 @@ import 'package:lichess_mobile/src/view/game/game_screen.dart'; import 'package:lichess_mobile/src/view/user/challenge_requests_screen.dart'; import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:stream_transform/stream_transform.dart'; part 'challenge_service.g.dart'; @@ -27,12 +28,6 @@ ChallengeService challengeService(ChallengeServiceRef ref) { return service; } -final _challengeStreamController = StreamController.broadcast(); - -/// The stream of challenge events that are received from the server. -final Stream challengeStream = - _challengeStreamController.stream; - /// A service that listens to challenge events and shows notifications. class ChallengeService { ChallengeService(this.ref); @@ -42,46 +37,59 @@ class ChallengeService { ChallengesList? _current; ChallengesList? _previous; - StreamSubscription? _socketSubscription; + StreamSubscription? _socketSubscription; + + /// The stream of challenge events that are received from the server. + static Stream get stream => socketGlobalStream.map( + (event) { + if (event.topic != 'challenges') return null; + final listPick = pick(event.data).required(); + final inward = listPick('in').asListOrEmpty(Challenge.fromPick); + final outward = listPick('out').asListOrEmpty(Challenge.fromPick); + return (inward: inward.lock, outward: outward.lock); + }, + ).whereNotNull(); /// Start listening to challenge events from the server. void start() { - _socketSubscription = socketGlobalStream.listen(_onSocketEvent); + _socketSubscription = stream.listen(_onSocketEvent); } - void _onSocketEvent(SocketEvent event) { - if (event.topic != 'challenges') return; + void _onSocketEvent(ChallengesList current) { + _previous = _current; + _current = current; - final listPick = pick(event.data).required(); - final inward = listPick('in').asListOrEmpty(Challenge.fromPick); - final outward = listPick('out').asListOrEmpty(Challenge.fromPick); + _sendNotifications(); + } - _previous = _current; - _current = (inward: inward.lock, outward: outward.lock); - _challengeStreamController.add(_current!); + Future _sendNotifications() async { + final notificationService = ref.read(notificationServiceProvider); final Iterable prevInwardIds = _previous?.inward.map((challenge) => challenge.id) ?? []; final Iterable currentInwardIds = - inward.map((challenge) => challenge.id); + _current?.inward.map((challenge) => challenge.id) ?? []; // challenges that were canceled by challenger or expired - prevInwardIds - .whereNot((challengeId) => currentInwardIds.contains(challengeId)) - .forEach( - (id) => - ref.read(notificationServiceProvider).cancel(id.value.hashCode), - ); + await Future.wait( + prevInwardIds + .whereNot((challengeId) => currentInwardIds.contains(challengeId)) + .map( + (id) async => await notificationService.cancel(id.value.hashCode), + ), + ); // new incoming challenges - inward - .whereNot((challenge) => prevInwardIds.contains(challenge.id)) - .forEach( - (challenge) { - ref - .read(notificationServiceProvider) - .show(ChallengeNotification(challenge)); - }, + await Future.wait( + _current?.inward + .whereNot((challenge) => prevInwardIds.contains(challenge.id)) + .map( + (challenge) async { + return await notificationService + .show(ChallengeNotification(challenge)); + }, + ) ?? + >[], ); } diff --git a/lib/src/model/challenge/challenges.dart b/lib/src/model/challenge/challenges.dart index 5a7dc7cfdf..ff788bffc8 100644 --- a/lib/src/model/challenge/challenges.dart +++ b/lib/src/model/challenge/challenges.dart @@ -16,7 +16,7 @@ class Challenges extends _$Challenges { @override Future build() async { _subscription = - challengeStream.listen((list) => state = AsyncValue.data(list)); + ChallengeService.stream.listen((list) => state = AsyncValue.data(list)); ref.onDispose(() { _subscription?.cancel(); diff --git a/test/model/challenge/challenge_service_test.dart b/test/model/challenge/challenge_service_test.dart new file mode 100644 index 0000000000..99ce46e19d --- /dev/null +++ b/test/model/challenge/challenge_service_test.dart @@ -0,0 +1,269 @@ +import 'package:fake_async/fake_async.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:lichess_mobile/src/model/challenge/challenge.dart'; +import 'package:lichess_mobile/src/model/challenge/challenge_service.dart'; +import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/common/speed.dart'; +import 'package:lichess_mobile/src/model/notifications/notification_service.dart'; +import 'package:lichess_mobile/src/model/user/user.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../../test_container.dart'; +import '../auth/fake_session_storage.dart'; +import '../common/fake_websocket_channel.dart'; +import '../common/socket_test.dart'; + +class NotificationDisplayMock extends Mock + implements FlutterLocalNotificationsPlugin {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + final notificationDisplayMock = NotificationDisplayMock(); + + tearDown(() { + reset(notificationDisplayMock); + }); + + test('exposes a challenges stream', () async { + final fakeChannel = FakeWebSocketChannel(); + final socketClient = + makeTestSocketClient(FakeWebSocketChannelFactory(() => fakeChannel)); + await socketClient.connect(); + await socketClient.firstConnection; + + fakeChannel.addIncomingMessages([ + ''' +{"t": "challenges", "d": {"in": [ { "socketVersion": 0, "id": "H9fIRZUk", "url": "https://lichess.org/H9fIRZUk", "status": "created", "challenger": { "id": "bot1", "name": "Bot1", "rating": 1500, "title": "BOT", "provisional": true, "online": true, "lag": 4 }, "destUser": { "id": "bobby", "name": "Bobby", "rating": 1635, "title": "GM", "provisional": true, "online": true, "lag": 4 }, "variant": { "key": "standard", "name": "Standard", "short": "Std" }, "rated": true, "speed": "rapid", "timeControl": { "type": "clock", "limit": 600, "increment": 0, "show": "10+0" }, "color": "random", "finalColor": "black", "perf": { "icon": "", "name": "Rapid" }, "direction": "in" } ] }, "v": 0 } +''' + ]); + + await expectLater( + ChallengeService.stream, + emitsInOrder([ + const ( + inward: IListConst([ + Challenge( + socketVersion: 0, + id: ChallengeId('H9fIRZUk'), + status: ChallengeStatus.created, + challenger: ( + user: LightUser( + id: UserId('bot1'), + name: 'Bot1', + title: 'BOT', + isOnline: true, + ), + rating: 1500, + provisionalRating: true, + lagRating: 4, + ), + destUser: ( + user: LightUser( + id: UserId('bobby'), + name: 'Bobby', + title: 'GM', + isOnline: true, + ), + rating: 1635, + provisionalRating: true, + lagRating: 4, + ), + variant: Variant.standard, + rated: true, + speed: Speed.rapid, + timeControl: ChallengeTimeControlType.clock, + clock: ( + time: Duration(seconds: 600), + increment: Duration.zero, + ), + sideChoice: SideChoice.random, + direction: ChallengeDirection.inward, + ), + ]), + outward: IListConst([]), + ), + ]), + ); + + socketClient.close(); + }); + + test('Listen to socket and show a notification for any new challenge', + () async { + when( + () => notificationDisplayMock.show( + any(), + any(), + any(), + any(), + payload: any(named: 'payload'), + ), + ).thenAnswer((_) => Future.value()); + + final container = await makeContainer( + userSession: fakeSession, + overrides: [ + notificationDisplayProvider.overrideWithValue(notificationDisplayMock), + ], + ); + + final notificationService = container.read(notificationServiceProvider); + final challengeService = container.read(challengeServiceProvider); + + fakeAsync((async) { + final fakeChannel = FakeWebSocketChannel(); + final socketClient = + makeTestSocketClient(FakeWebSocketChannelFactory(() => fakeChannel)); + socketClient.connect(); + notificationService.start(); + challengeService.start(); + + // wait for the socket to connect + async.elapse(const Duration(milliseconds: 100)); + async.flushMicrotasks(); + + fakeChannel.addIncomingMessages([ + ''' +{"t": "challenges", "d": {"in": [ { "socketVersion": 0, "id": "H9fIRZUk", "url": "https://lichess.org/H9fIRZUk", "status": "created", "challenger": { "id": "bot1", "name": "Bot1", "rating": 1500, "title": "BOT", "provisional": true, "online": true, "lag": 4 }, "destUser": { "id": "bobby", "name": "Bobby", "rating": 1635, "title": "GM", "provisional": true, "online": true, "lag": 4 }, "variant": { "key": "standard", "name": "Standard", "short": "Std" }, "rated": true, "speed": "rapid", "timeControl": { "type": "clock", "limit": 600, "increment": 0, "show": "10+0" }, "color": "random", "finalColor": "black", "perf": { "icon": "", "name": "Rapid" }, "direction": "in" } ] }, "v": 0 } +''' + ]); + + async.flushMicrotasks(); + + final result = verify( + () => notificationDisplayMock.show( + const ChallengeId('H9fIRZUk').hashCode, + 'Bot1 challenges you!', + 'Random side • Rated • 10+0', + captureAny(), + payload: any(named: 'payload'), + ), + ); + + expectLater(result.callCount, 1); + expectLater( + result.captured[0], + isA() + .having( + (details) => details.android?.channelId, + 'channelId', + 'challenge', + ) + .having( + (d) => d.android?.importance, + 'importance', + Importance.max, + ) + .having( + (d) => d.android?.priority, + 'priority', + Priority.high, + ), + ); + + fakeChannel.addIncomingMessages([ + ''' +{"t": "challenges", "d": {"in": [ { "socketVersion": 0, "id": "H9fIRZUk", "url": "https://lichess.org/H9fIRZUk", "status": "created", "challenger": { "id": "bot1", "name": "Bot1", "rating": 1500, "title": "BOT", "provisional": true, "online": true, "lag": 4 }, "destUser": { "id": "bobby", "name": "Bobby", "rating": 1635, "title": "GM", "provisional": true, "online": true, "lag": 4 }, "variant": { "key": "standard", "name": "Standard", "short": "Std" }, "rated": true, "speed": "rapid", "timeControl": { "type": "clock", "limit": 600, "increment": 0, "show": "10+0" }, "color": "random", "finalColor": "black", "perf": { "icon": "", "name": "Rapid" }, "direction": "in" } ] }, "v": 0 } +''' + ]); + + async.flushMicrotasks(); + + // same notification should not be shown again + verifyNever( + () => notificationDisplayMock.show( + any(), + any(), + any(), + any(), + payload: any(named: 'payload'), + ), + ); + + // closing the socket client to be able to flush the timers + socketClient.close(); + async.flushTimers(); + }); + }); + + test('Cancels the notification for any missing challenge', () async { + when( + () => notificationDisplayMock.show( + any(), + any(), + any(), + any(), + payload: any(named: 'payload'), + ), + ).thenAnswer((_) => Future.value()); + + when( + () => notificationDisplayMock.cancel( + any(), + ), + ).thenAnswer((_) => Future.value()); + + final container = await makeContainer( + userSession: fakeSession, + overrides: [ + notificationDisplayProvider.overrideWithValue(notificationDisplayMock), + ], + ); + + final notificationService = container.read(notificationServiceProvider); + final challengeService = container.read(challengeServiceProvider); + + fakeAsync((async) { + final fakeChannel = FakeWebSocketChannel(); + final socketClient = + makeTestSocketClient(FakeWebSocketChannelFactory(() => fakeChannel)); + socketClient.connect(); + notificationService.start(); + challengeService.start(); + + // wait for the socket to connect + async.elapse(const Duration(milliseconds: 100)); + async.flushMicrotasks(); + + fakeChannel.addIncomingMessages([ + ''' +{"t": "challenges", "d": {"in": [ { "socketVersion": 0, "id": "H9fIRZUk", "url": "https://lichess.org/H9fIRZUk", "status": "created", "challenger": { "id": "bot1", "name": "Bot1", "rating": 1500, "title": "BOT", "provisional": true, "online": true, "lag": 4 }, "destUser": { "id": "bobby", "name": "Bobby", "rating": 1635, "title": "GM", "provisional": true, "online": true, "lag": 4 }, "variant": { "key": "standard", "name": "Standard", "short": "Std" }, "rated": true, "speed": "rapid", "timeControl": { "type": "clock", "limit": 600, "increment": 0, "show": "10+0" }, "color": "random", "finalColor": "black", "perf": { "icon": "", "name": "Rapid" }, "direction": "in" } ] }, "v": 0 } +''' + ]); + + async.flushMicrotasks(); + + verify( + () => notificationDisplayMock.show( + any(), + any(), + any(), + captureAny(), + payload: any(named: 'payload'), + ), + ); + + fakeChannel.addIncomingMessages([ + ''' +{"t": "challenges", "d": {"in": [] }, "v": 0 } +''' + ]); + + async.flushMicrotasks(); + + verify( + () => notificationDisplayMock.cancel( + const ChallengeId('H9fIRZUk').hashCode, + ), + ).called(1); + + // closing the socket client to be able to flush the timers + socketClient.close(); + async.flushTimers(); + }); + }); +} diff --git a/test/model/common/fake_websocket_channel.dart b/test/model/common/fake_websocket_channel.dart index 8010b1203f..fce314cdc9 100644 --- a/test/model/common/fake_websocket_channel.dart +++ b/test/model/common/fake_websocket_channel.dart @@ -74,10 +74,11 @@ class FakeWebSocketChannel implements WebSocketChannel { _outcomingController.stream.where((message) => !isPing(message)); /// Simulates incoming messages from the server. - Future addIncomingMessages(Iterable messages) async { - await Future.delayed(const Duration(milliseconds: 5)); - return _incomingController - .addStream(Stream.fromIterable(messages)); + void addIncomingMessages(Iterable messages) { + for (final message in messages) { + _incomingController.add(message); + } + // await _incomingController.addStream(Stream.fromIterable(messages)); } @override diff --git a/test/model/common/socket_test.dart b/test/model/common/socket_test.dart index 04fdef8e4c..3a5eb66e04 100644 --- a/test/model/common/socket_test.dart +++ b/test/model/common/socket_test.dart @@ -7,7 +7,9 @@ import 'package:package_info_plus/package_info_plus.dart'; import 'fake_websocket_channel.dart'; -SocketClient _makeSocketClient(FakeWebSocketChannelFactory fakeChannelFactory) { +SocketClient makeTestSocketClient( + FakeWebSocketChannelFactory fakeChannelFactory, +) { final client = SocketClient( Uri(path: kDefaultSocketRoute), channelFactory: fakeChannelFactory, @@ -43,7 +45,7 @@ void main() { final fakeChannel = FakeWebSocketChannel(); final socketClient = - _makeSocketClient(FakeWebSocketChannelFactory(() => fakeChannel)); + makeTestSocketClient(FakeWebSocketChannelFactory(() => fakeChannel)); socketClient.connect(); int sentPingCount = 0; @@ -75,7 +77,7 @@ void main() { return FakeWebSocketChannel(); }); - final socketClient = _makeSocketClient(fakeChannelFactory); + final socketClient = makeTestSocketClient(fakeChannelFactory); socketClient.connect(); // The first connection attempt will fail, but the second one will succeed @@ -111,7 +113,7 @@ void main() { return channel; }); - final socketClient = _makeSocketClient(fakeChannelFactory); + final socketClient = makeTestSocketClient(fakeChannelFactory); socketClient.connect(); await socketClient.firstConnection; @@ -134,7 +136,7 @@ void main() { final fakeChannel = FakeWebSocketChannel(); final socketClient = - _makeSocketClient(FakeWebSocketChannelFactory(() => fakeChannel)); + makeTestSocketClient(FakeWebSocketChannelFactory(() => fakeChannel)); socketClient.connect(); // before the connection is ready the average lag is zero @@ -186,7 +188,7 @@ void main() { final fakeChannel = FakeWebSocketChannel(); final socketClient = - _makeSocketClient(FakeWebSocketChannelFactory(() => fakeChannel)); + makeTestSocketClient(FakeWebSocketChannelFactory(() => fakeChannel)); socketClient.connect(); await socketClient.firstConnection; @@ -205,7 +207,7 @@ void main() { ); // server acks the message - await fakeChannel.addIncomingMessages(['{"t":"ack","d":1}']); + fakeChannel.addIncomingMessages(['{"t":"ack","d":1}']); // no more messages are expected await expectLater( diff --git a/test/model/notifications/notification_service_test.dart b/test/model/notifications/notification_service_test.dart index 32cced836a..93c18d62ad 100644 --- a/test/model/notifications/notification_service_test.dart +++ b/test/model/notifications/notification_service_test.dart @@ -145,8 +145,9 @@ void main() { }); }); - group('Receive and show notifications:', () { - test('FCM message will show a notification in foreground', () async { + group('Correspondence game update notifications', () { + test('FCM message with associated notification will show it in foreground', + () async { final container = await makeContainer( userSession: fakeSession, overrides: [ From 7bcfc4a8a8c71fcf15d63a94fa3e70165bbfbcb2 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 1 Oct 2024 10:16:58 +0200 Subject: [PATCH 405/979] Rename test helpers --- test/app_test.dart | 1 + .../account/account_repository_test.dart | 2 +- test/model/auth/auth_controller_test.dart | 2 +- .../broadcast/broadcast_repository_test.dart | 2 +- .../challenge/challenge_repository_test.dart | 2 +- test/model/game/game_repository_test.dart | 2 +- test/model/lobby/lobby_repository_test.dart | 2 +- .../notification_service_test.dart | 2 +- .../opening_explorer_repository_test.dart | 2 +- test/model/puzzle/puzzle_repository_test.dart | 2 +- test/model/puzzle/puzzle_service_test.dart | 2 +- .../relation/relation_repository_test.dart | 2 +- test/model/tv/tv_repository_test.dart | 2 +- test/model/user/user_repository_test.dart | 2 +- test/{test_utils.dart => test_helpers.dart} | 9 ++++++ ...test_app.dart => test_provider_scope.dart} | 21 ++++++++------ test/view/analysis/analysis_screen_test.dart | 6 ++-- .../board_editor_screen_test.dart | 22 +++++++------- .../broadcasts_list_screen_test.dart | 8 ++--- .../coordinate_training_screen_test.dart | 8 ++--- test/view/game/archived_game_screen_test.dart | 10 +++---- .../opening_explorer_screen_test.dart | 10 +++---- .../over_the_board_screen_test.dart | 6 ++-- test/view/puzzle/puzzle_screen_test.dart | 16 +++++----- test/view/puzzle/storm_screen_test.dart | 14 ++++----- .../settings/settings_tab_screen_test.dart | 10 +++---- test/view/user/leaderboard_screen_test.dart | 6 ++-- test/view/user/leaderboard_widget_test.dart | 6 ++-- test/view/user/perf_stats_screen_test.dart | 8 ++--- test/view/user/search_screen_test.dart | 8 ++--- test/view/user/user_screen_test.dart | 6 ++-- test/widgets/adaptive_choice_picker_test.dart | 2 +- test_navigator.dart | 29 +++++++++++++++++++ 33 files changed, 137 insertions(+), 95 deletions(-) create mode 100644 test/app_test.dart rename test/{test_utils.dart => test_helpers.dart} (92%) rename test/{test_app.dart => test_provider_scope.dart} (91%) create mode 100644 test_navigator.dart diff --git a/test/app_test.dart b/test/app_test.dart new file mode 100644 index 0000000000..ab73b3a234 --- /dev/null +++ b/test/app_test.dart @@ -0,0 +1 @@ +void main() {} diff --git a/test/model/account/account_repository_test.dart b/test/model/account/account_repository_test.dart index d2bfcc9c55..27aeb64c15 100644 --- a/test/model/account/account_repository_test.dart +++ b/test/model/account/account_repository_test.dart @@ -5,7 +5,7 @@ import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; import '../../test_container.dart'; -import '../../test_utils.dart'; +import '../../test_helpers.dart'; void main() { group('AccountRepository', () { diff --git a/test/model/auth/auth_controller_test.dart b/test/model/auth/auth_controller_test.dart index a7603671c0..cbeeeff794 100644 --- a/test/model/auth/auth_controller_test.dart +++ b/test/model/auth/auth_controller_test.dart @@ -12,7 +12,7 @@ import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:mocktail/mocktail.dart'; import '../../test_container.dart'; -import '../../test_utils.dart'; +import '../../test_helpers.dart'; class MockFlutterAppAuth extends Mock implements FlutterAppAuth {} diff --git a/test/model/broadcast/broadcast_repository_test.dart b/test/model/broadcast/broadcast_repository_test.dart index 2cfd055e69..fb3f95f26a 100644 --- a/test/model/broadcast/broadcast_repository_test.dart +++ b/test/model/broadcast/broadcast_repository_test.dart @@ -6,7 +6,7 @@ import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import '../../test_container.dart'; -import '../../test_utils.dart'; +import '../../test_helpers.dart'; void main() { group('BroadcastRepository', () { diff --git a/test/model/challenge/challenge_repository_test.dart b/test/model/challenge/challenge_repository_test.dart index c0f2fa7ed4..412d9c2bb8 100644 --- a/test/model/challenge/challenge_repository_test.dart +++ b/test/model/challenge/challenge_repository_test.dart @@ -7,7 +7,7 @@ import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import '../../test_container.dart'; -import '../../test_utils.dart'; +import '../../test_helpers.dart'; void main() { group('ChallengeRepository', () { diff --git a/test/model/game/game_repository_test.dart b/test/model/game/game_repository_test.dart index d3b86c83b6..658eb532f2 100644 --- a/test/model/game/game_repository_test.dart +++ b/test/model/game/game_repository_test.dart @@ -8,7 +8,7 @@ import 'package:lichess_mobile/src/model/game/archived_game.dart'; import 'package:lichess_mobile/src/model/game/game_repository.dart'; import '../../test_container.dart'; -import '../../test_utils.dart'; +import '../../test_helpers.dart'; void main() { group('GameRepository.getRecentGames', () { diff --git a/test/model/lobby/lobby_repository_test.dart b/test/model/lobby/lobby_repository_test.dart index e790bae327..6c92f42ef6 100644 --- a/test/model/lobby/lobby_repository_test.dart +++ b/test/model/lobby/lobby_repository_test.dart @@ -7,7 +7,7 @@ import 'package:lichess_mobile/src/model/lobby/correspondence_challenge.dart'; import 'package:lichess_mobile/src/model/lobby/lobby_repository.dart'; import '../../test_container.dart'; -import '../../test_utils.dart'; +import '../../test_helpers.dart'; void main() { final mockClient = MockClient((request) { diff --git a/test/model/notifications/notification_service_test.dart b/test/model/notifications/notification_service_test.dart index 93c18d62ad..5b6b8bee9a 100644 --- a/test/model/notifications/notification_service_test.dart +++ b/test/model/notifications/notification_service_test.dart @@ -14,7 +14,7 @@ import 'package:lichess_mobile/src/model/notifications/notifications.dart'; import 'package:mocktail/mocktail.dart'; import '../../binding.dart'; import '../../test_container.dart'; -import '../../test_utils.dart'; +import '../../test_helpers.dart'; import '../auth/fake_session_storage.dart'; class NotificationDisplayMock extends Mock diff --git a/test/model/opening_explorer/opening_explorer_repository_test.dart b/test/model/opening_explorer/opening_explorer_repository_test.dart index 7a4c7225d5..013fd5516c 100644 --- a/test/model/opening_explorer/opening_explorer_repository_test.dart +++ b/test/model/opening_explorer/opening_explorer_repository_test.dart @@ -8,7 +8,7 @@ import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart' import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_repository.dart'; import '../../test_container.dart'; -import '../../test_utils.dart'; +import '../../test_helpers.dart'; void main() { group('OpeningExplorerRepository.getMasterDatabase', () { diff --git a/test/model/puzzle/puzzle_repository_test.dart b/test/model/puzzle/puzzle_repository_test.dart index 0f884807d0..5647f327a2 100644 --- a/test/model/puzzle/puzzle_repository_test.dart +++ b/test/model/puzzle/puzzle_repository_test.dart @@ -5,7 +5,7 @@ import 'package:lichess_mobile/src/model/puzzle/puzzle.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_repository.dart'; import '../../test_container.dart'; -import '../../test_utils.dart'; +import '../../test_helpers.dart'; void main() { group('PuzzleRepository', () { diff --git a/test/model/puzzle/puzzle_service_test.dart b/test/model/puzzle/puzzle_service_test.dart index 6a23e38b7c..34aa9a8ad7 100644 --- a/test/model/puzzle/puzzle_service_test.dart +++ b/test/model/puzzle/puzzle_service_test.dart @@ -15,7 +15,7 @@ import 'package:lichess_mobile/src/model/puzzle/puzzle_service.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_theme.dart'; import '../../test_container.dart'; -import '../../test_utils.dart'; +import '../../test_helpers.dart'; void main() { Future makeTestContainer(MockClient mockClient) async { diff --git a/test/model/relation/relation_repository_test.dart b/test/model/relation/relation_repository_test.dart index 5800d76431..d190c9ff5c 100644 --- a/test/model/relation/relation_repository_test.dart +++ b/test/model/relation/relation_repository_test.dart @@ -6,7 +6,7 @@ import 'package:lichess_mobile/src/model/relation/relation_repository.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import '../../test_container.dart'; -import '../../test_utils.dart'; +import '../../test_helpers.dart'; void main() { group('RelationRepository.getFollowing', () { diff --git a/test/model/tv/tv_repository_test.dart b/test/model/tv/tv_repository_test.dart index ff5f577df2..66c7fe2057 100644 --- a/test/model/tv/tv_repository_test.dart +++ b/test/model/tv/tv_repository_test.dart @@ -3,7 +3,7 @@ import 'package:http/testing.dart'; import 'package:lichess_mobile/src/model/tv/tv_channel.dart'; import 'package:lichess_mobile/src/model/tv/tv_repository.dart'; -import '../../test_utils.dart'; +import '../../test_helpers.dart'; void main() { group('TvRepository.channels', () { diff --git a/test/model/user/user_repository_test.dart b/test/model/user/user_repository_test.dart index 3a420e19fd..68bec60d90 100644 --- a/test/model/user/user_repository_test.dart +++ b/test/model/user/user_repository_test.dart @@ -9,7 +9,7 @@ import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/model/user/user_repository.dart'; import '../../test_container.dart'; -import '../../test_utils.dart'; +import '../../test_helpers.dart'; const testUserId = UserId('test'); diff --git a/test/test_utils.dart b/test/test_helpers.dart similarity index 92% rename from test/test_utils.dart rename to test/test_helpers.dart index 525dafbb1b..f1b109c6b0 100644 --- a/test/test_utils.dart +++ b/test/test_helpers.dart @@ -7,6 +7,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; +const double _kTestScreenWidth = 390.0; +const double _kTestScreenHeight = 844.0; + +/// iPhone 14 screen size as default test surface size +const kTestSurfaceSize = Size(_kTestScreenWidth, _kTestScreenHeight); + const kPlatformVariant = TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS}); @@ -16,6 +22,7 @@ Matcher sameHeaders(Map headers) => _SameHeaders(headers); Future delayedAnswer(T value) => Future.delayed(const Duration(milliseconds: 5)).then((_) => value); +/// Mocks an http response with a delay of 20ms. Future mockResponse( String body, int code, { @@ -64,6 +71,7 @@ Future meetsTapTargetGuideline(WidgetTester tester) async { } } +/// Returns the offset of a square on a board defined by [Rect]. Offset squareOffset( Square square, Rect boardRect, { @@ -84,6 +92,7 @@ Offset squareOffset( ); } +/// Plays a move on the board. Future playMove( WidgetTester tester, Rect boardRect, diff --git a/test/test_app.dart b/test/test_provider_scope.dart similarity index 91% rename from test/test_app.dart rename to test/test_provider_scope.dart index 67dd329d38..8bfc5a79d2 100644 --- a/test/test_app.dart +++ b/test/test_provider_scope.dart @@ -33,18 +33,22 @@ import './model/common/service/fake_sound_service.dart'; import 'binding.dart'; import 'model/common/fake_websocket_channel.dart'; import 'model/notifications/fake_notification_display.dart'; +import 'test_helpers.dart'; import 'utils/fake_connectivity.dart'; final mockClient = MockClient((request) async { return http.Response('', 200); }); -// iPhone 14 screen size -const double _kTestScreenWidth = 390.0; -const double _kTestScreenHeight = 844.0; -const kTestSurfaceSize = Size(_kTestScreenWidth, _kTestScreenHeight); - -Future buildTestApp( +/// Returns a [MaterialApp] wrapped with a [ProviderScope] and default mocks, ready for testing. +/// +/// The [home] widget is the widget we want to test. It is wrapped in a [MediaQuery] +/// and [MaterialApp] widgets to simulate a simple app. +/// +/// The [overrides] parameter can be used to override any provider in the app. +/// The [userSession] parameter can be used to set the initial user session state. +/// The [defaultPreferences] parameter can be used to set the initial shared preferences. +Future makeProviderScopeApp( WidgetTester tester, { required Widget home, List? overrides, @@ -158,15 +162,14 @@ Future buildTestApp( }), ...overrides ?? [], ], - // simplified version of class [App] in lib/src/app.dart child: Consumer( builder: (context, ref, child) { return MediaQuery( data: const MediaQueryData(size: kTestSurfaceSize), child: Center( child: SizedBox( - width: _kTestScreenWidth, - height: _kTestScreenHeight, + width: kTestSurfaceSize.width, + height: kTestSurfaceSize.height, child: MaterialApp( localizationsDelegates: AppLocalizations.localizationsDelegates, home: home, diff --git a/test/view/analysis/analysis_screen_test.dart b/test/view/analysis/analysis_screen_test.dart index 45cc239ca9..0c74d12460 100644 --- a/test/view/analysis/analysis_screen_test.dart +++ b/test/view/analysis/analysis_screen_test.dart @@ -17,7 +17,7 @@ import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; import 'package:lichess_mobile/src/view/analysis/tree_view.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; -import '../../test_app.dart'; +import '../../test_provider_scope.dart'; void main() { // ignore: avoid_dynamic_calls @@ -29,7 +29,7 @@ void main() { group('Analysis Screen', () { testWidgets('displays correct move and position', (tester) async { - final app = await buildTestApp( + final app = await makeProviderScopeApp( tester, home: AnalysisScreen( pgnOrId: sanMoves, @@ -56,7 +56,7 @@ void main() { }); testWidgets('move backwards and forward', (tester) async { - final app = await buildTestApp( + final app = await makeProviderScopeApp( tester, home: AnalysisScreen( pgnOrId: sanMoves, diff --git a/test/view/board_editor/board_editor_screen_test.dart b/test/view/board_editor/board_editor_screen_test.dart index c5a5ded5f3..37b7021851 100644 --- a/test/view/board_editor/board_editor_screen_test.dart +++ b/test/view/board_editor/board_editor_screen_test.dart @@ -7,12 +7,12 @@ import 'package:lichess_mobile/src/model/board_editor/board_editor_controller.da import 'package:lichess_mobile/src/view/board_editor/board_editor_screen.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; -import '../../test_app.dart'; +import '../../test_provider_scope.dart'; void main() { group('Board Editor', () { testWidgets('Displays initial FEN on start', (tester) async { - final app = await buildTestApp( + final app = await makeProviderScopeApp( tester, home: const BoardEditorScreen(), ); @@ -37,7 +37,7 @@ void main() { }); testWidgets('Flip board', (tester) async { - final app = await buildTestApp( + final app = await makeProviderScopeApp( tester, home: const BoardEditorScreen(), ); @@ -57,7 +57,7 @@ void main() { }); testWidgets('Side to play and castling rights', (tester) async { - final app = await buildTestApp( + final app = await makeProviderScopeApp( tester, home: const BoardEditorScreen(), ); @@ -120,7 +120,7 @@ void main() { }); testWidgets('Castling rights ignored when rook is missing', (tester) async { - final app = await buildTestApp( + final app = await makeProviderScopeApp( tester, home: const BoardEditorScreen(), ); @@ -145,7 +145,7 @@ void main() { testWidgets('Possible en passant squares are calculated correctly', (tester) async { - final app = await buildTestApp( + final app = await makeProviderScopeApp( tester, home: const BoardEditorScreen(), ); @@ -182,7 +182,7 @@ void main() { }); testWidgets('Can drag pieces to new squares', (tester) async { - final app = await buildTestApp( + final app = await makeProviderScopeApp( tester, home: const BoardEditorScreen(), ); @@ -211,7 +211,7 @@ void main() { }); testWidgets('illegal position cannot be analyzed', (tester) async { - final app = await buildTestApp( + final app = await makeProviderScopeApp( tester, home: const BoardEditorScreen(), ); @@ -231,7 +231,7 @@ void main() { }); testWidgets('Delete pieces via bin button', (tester) async { - final app = await buildTestApp( + final app = await makeProviderScopeApp( tester, home: const BoardEditorScreen(), ); @@ -273,7 +273,7 @@ void main() { }); testWidgets('Add pieces via tap and pan', (tester) async { - final app = await buildTestApp( + final app = await makeProviderScopeApp( tester, home: const BoardEditorScreen(), ); @@ -297,7 +297,7 @@ void main() { }); testWidgets('Drag pieces onto the board', (tester) async { - final app = await buildTestApp( + final app = await makeProviderScopeApp( tester, home: const BoardEditorScreen(), ); diff --git a/test/view/broadcast/broadcasts_list_screen_test.dart b/test/view/broadcast/broadcasts_list_screen_test.dart index 1f501b11f0..e5907ee7ea 100644 --- a/test/view/broadcast/broadcasts_list_screen_test.dart +++ b/test/view/broadcast/broadcasts_list_screen_test.dart @@ -4,8 +4,8 @@ import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcasts_list_screen.dart'; import 'package:network_image_mock/network_image_mock.dart'; -import '../../test_app.dart'; -import '../../test_utils.dart'; +import '../../test_helpers.dart'; +import '../../test_provider_scope.dart'; final client = MockClient((request) { if (request.url.path == '/api/broadcast/top') { @@ -24,7 +24,7 @@ void main() { 'Displays broadcast tournament screen', variant: kPlatformVariant, (tester) async { - final app = await buildTestApp( + final app = await makeProviderScopeApp( tester, home: const BroadcastsListScreen(), overrides: [ @@ -48,7 +48,7 @@ void main() { 'Scroll broadcast tournament screen', variant: kPlatformVariant, (tester) async { - final app = await buildTestApp( + final app = await makeProviderScopeApp( tester, home: const BroadcastsListScreen(), overrides: [ diff --git a/test/view/coordinate_training/coordinate_training_screen_test.dart b/test/view/coordinate_training/coordinate_training_screen_test.dart index 5c2f253e85..87e4f1cc99 100644 --- a/test/view/coordinate_training/coordinate_training_screen_test.dart +++ b/test/view/coordinate_training/coordinate_training_screen_test.dart @@ -7,13 +7,13 @@ import 'package:lichess_mobile/src/model/coordinate_training/coordinate_training import 'package:lichess_mobile/src/model/coordinate_training/coordinate_training_preferences.dart'; import 'package:lichess_mobile/src/view/coordinate_training/coordinate_training_screen.dart'; -import '../../test_app.dart'; +import '../../test_provider_scope.dart'; void main() { group('Coordinate Training', () { testWidgets('Initial state when started in FindSquare mode', (tester) async { - final app = await buildTestApp( + final app = await makeProviderScopeApp( tester, home: const CoordinateTrainingScreen(), ); @@ -52,7 +52,7 @@ void main() { }); testWidgets('Tap wrong square', (tester) async { - final app = await buildTestApp( + final app = await makeProviderScopeApp( tester, home: const CoordinateTrainingScreen(), ); @@ -101,7 +101,7 @@ void main() { }); testWidgets('Tap correct square', (tester) async { - final app = await buildTestApp( + final app = await makeProviderScopeApp( tester, home: const CoordinateTrainingScreen(), ); diff --git a/test/view/game/archived_game_screen_test.dart b/test/view/game/archived_game_screen_test.dart index bd85e02275..7ade011d10 100644 --- a/test/view/game/archived_game_screen_test.dart +++ b/test/view/game/archived_game_screen_test.dart @@ -17,8 +17,8 @@ import 'package:lichess_mobile/src/view/game/game_player.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/move_list.dart'; -import '../../test_app.dart'; -import '../../test_utils.dart'; +import '../../test_helpers.dart'; +import '../../test_provider_scope.dart'; final client = MockClient((request) { if (request.url.path == '/game/export/qVChCOTc') { @@ -32,7 +32,7 @@ void main() { testWidgets( 'loads game data if only game id is provided', (tester) async { - final app = await buildTestApp( + final app = await makeProviderScopeApp( tester, home: const ArchivedGameScreen( gameId: GameId('qVChCOTc'), @@ -65,7 +65,7 @@ void main() { testWidgets( 'displays game data and last fen immediately, then moves', (tester) async { - final app = await buildTestApp( + final app = await makeProviderScopeApp( tester, home: ArchivedGameScreen( gameData: gameData, @@ -138,7 +138,7 @@ void main() { ); testWidgets('navigate game positions', (tester) async { - final app = await buildTestApp( + final app = await makeProviderScopeApp( tester, home: ArchivedGameScreen( gameData: gameData, diff --git a/test/view/opening_explorer/opening_explorer_screen_test.dart b/test/view/opening_explorer/opening_explorer_screen_test.dart index df7d23218a..af20b1a79f 100644 --- a/test/view/opening_explorer/opening_explorer_screen_test.dart +++ b/test/view/opening_explorer/opening_explorer_screen_test.dart @@ -12,8 +12,8 @@ import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_prefe import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_screen.dart'; -import '../../test_app.dart'; -import '../../test_utils.dart'; +import '../../test_helpers.dart'; +import '../../test_provider_scope.dart'; void main() { final explorerViewFinder = find.descendant( @@ -53,7 +53,7 @@ void main() { testWidgets( 'master opening explorer loads', (WidgetTester tester) async { - final app = await buildTestApp( + final app = await makeProviderScopeApp( tester, home: const OpeningExplorerScreen( pgn: '', @@ -106,7 +106,7 @@ void main() { testWidgets( 'lichess opening explorer loads', (WidgetTester tester) async { - final app = await buildTestApp( + final app = await makeProviderScopeApp( tester, home: const OpeningExplorerScreen( pgn: '', @@ -157,7 +157,7 @@ void main() { testWidgets( 'player opening explorer loads', (WidgetTester tester) async { - final app = await buildTestApp( + final app = await makeProviderScopeApp( tester, home: const OpeningExplorerScreen( pgn: '', diff --git a/test/view/over_the_board/over_the_board_screen_test.dart b/test/view/over_the_board/over_the_board_screen_test.dart index 02552d2743..defb4f45b9 100644 --- a/test/view/over_the_board/over_the_board_screen_test.dart +++ b/test/view/over_the_board/over_the_board_screen_test.dart @@ -11,8 +11,8 @@ import 'package:lichess_mobile/src/model/over_the_board/over_the_board_game_cont import 'package:lichess_mobile/src/view/over_the_board/over_the_board_screen.dart'; import 'package:lichess_mobile/src/widgets/countdown_clock.dart'; -import '../../test_app.dart'; -import '../../test_utils.dart'; +import '../../test_helpers.dart'; +import '../../test_provider_scope.dart'; void main() { group('Playing over the board (offline)', () { @@ -203,7 +203,7 @@ Future initOverTheBoardGame( WidgetTester tester, TimeIncrement timeIncrement, ) async { - final app = await buildTestApp( + final app = await makeProviderScopeApp( tester, home: const OverTheBoardScreen(), ); diff --git a/test/view/puzzle/puzzle_screen_test.dart b/test/view/puzzle/puzzle_screen_test.dart index 0f95563c8b..f0e169af9f 100644 --- a/test/view/puzzle/puzzle_screen_test.dart +++ b/test/view/puzzle/puzzle_screen_test.dart @@ -19,8 +19,8 @@ import 'package:lichess_mobile/src/view/puzzle/puzzle_screen.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:mocktail/mocktail.dart'; -import '../../test_app.dart'; -import '../../test_utils.dart'; +import '../../test_helpers.dart'; +import '../../test_provider_scope.dart'; class MockPuzzleBatchStorage extends Mock implements PuzzleBatchStorage {} @@ -47,7 +47,7 @@ void main() { (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); - final app = await buildTestApp( + final app = await makeProviderScopeApp( tester, home: PuzzleScreen( angle: const PuzzleTheme(PuzzleThemeKey.mix), @@ -78,7 +78,7 @@ void main() { 'Loads puzzle directly by passing a puzzleId', variant: kPlatformVariant, (tester) async { - final app = await buildTestApp( + final app = await makeProviderScopeApp( tester, home: PuzzleScreen( angle: const PuzzleTheme(PuzzleThemeKey.mix), @@ -104,7 +104,7 @@ void main() { ); testWidgets('Loads next puzzle when no puzzleId is passed', (tester) async { - final app = await buildTestApp( + final app = await makeProviderScopeApp( tester, home: const PuzzleScreen( angle: PuzzleTheme(PuzzleThemeKey.mix), @@ -151,7 +151,7 @@ void main() { when(() => mockHistoryStorage.fetch(puzzleId: puzzle2.puzzle.id)) .thenAnswer((_) async => puzzle2); - final app = await buildTestApp( + final app = await makeProviderScopeApp( tester, home: PuzzleScreen( angle: const PuzzleTheme(PuzzleThemeKey.mix), @@ -264,7 +264,7 @@ void main() { when(() => mockHistoryStorage.fetch(puzzleId: puzzle2.puzzle.id)) .thenAnswer((_) async => puzzle2); - final app = await buildTestApp( + final app = await makeProviderScopeApp( tester, home: PuzzleScreen( angle: const PuzzleTheme(PuzzleThemeKey.mix), @@ -377,7 +377,7 @@ void main() { return mockResponse('', 404); }); - final app = await buildTestApp( + final app = await makeProviderScopeApp( tester, home: PuzzleScreen( angle: const PuzzleTheme(PuzzleThemeKey.mix), diff --git a/test/view/puzzle/storm_screen_test.dart b/test/view/puzzle/storm_screen_test.dart index 4bd3b50cd6..e7e10ba217 100644 --- a/test/view/puzzle/storm_screen_test.dart +++ b/test/view/puzzle/storm_screen_test.dart @@ -11,8 +11,8 @@ import 'package:lichess_mobile/src/model/puzzle/puzzle_providers.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_repository.dart'; import 'package:lichess_mobile/src/view/puzzle/storm_screen.dart'; -import '../../test_app.dart'; -import '../../test_utils.dart'; +import '../../test_helpers.dart'; +import '../../test_provider_scope.dart'; final client = MockClient((request) { if (request.url.path == '/storm') { @@ -28,7 +28,7 @@ void main() { (tester) async { final SemanticsHandle handle = tester.ensureSemantics(); - final app = await buildTestApp( + final app = await makeProviderScopeApp( tester, home: const StormScreen(), overrides: [ @@ -49,7 +49,7 @@ void main() { testWidgets( 'Load puzzle and play white pieces', (tester) async { - final app = await buildTestApp( + final app = await makeProviderScopeApp( tester, home: const StormScreen(), overrides: [ @@ -73,7 +73,7 @@ void main() { testWidgets( 'Play one puzzle', (tester) async { - final app = await buildTestApp( + final app = await makeProviderScopeApp( tester, home: const StormScreen(), overrides: [ @@ -128,7 +128,7 @@ void main() { ); testWidgets('shows end run result', (tester) async { - final app = await buildTestApp( + final app = await makeProviderScopeApp( tester, home: const StormScreen(), overrides: [ @@ -173,7 +173,7 @@ void main() { }); testWidgets('play wrong move', (tester) async { - final app = await buildTestApp( + final app = await makeProviderScopeApp( tester, home: const StormScreen(), overrides: [ diff --git a/test/view/settings/settings_tab_screen_test.dart b/test/view/settings/settings_tab_screen_test.dart index 3275cc89ec..e83b855a62 100644 --- a/test/view/settings/settings_tab_screen_test.dart +++ b/test/view/settings/settings_tab_screen_test.dart @@ -9,8 +9,8 @@ import 'package:lichess_mobile/src/view/settings/settings_tab_screen.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import '../../model/auth/fake_session_storage.dart'; -import '../../test_app.dart'; -import '../../test_utils.dart'; +import '../../test_helpers.dart'; +import '../../test_provider_scope.dart'; final client = MockClient((request) { if (request.method == 'DELETE' && request.url.path == '/api/token') { @@ -26,7 +26,7 @@ void main() { (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); - final app = await buildTestApp( + final app = await makeProviderScopeApp( tester, home: const SettingsTabScreen(), ); @@ -46,7 +46,7 @@ void main() { testWidgets( "don't show signOut if no session", (WidgetTester tester) async { - final app = await buildTestApp( + final app = await makeProviderScopeApp( tester, home: const SettingsTabScreen(), ); @@ -61,7 +61,7 @@ void main() { testWidgets( 'signout', (WidgetTester tester) async { - final app = await buildTestApp( + final app = await makeProviderScopeApp( tester, home: const SettingsTabScreen(), userSession: fakeSession, diff --git a/test/view/user/leaderboard_screen_test.dart b/test/view/user/leaderboard_screen_test.dart index f060f3cf28..1950ae0205 100644 --- a/test/view/user/leaderboard_screen_test.dart +++ b/test/view/user/leaderboard_screen_test.dart @@ -4,8 +4,8 @@ import 'package:http/testing.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/view/user/leaderboard_screen.dart'; -import '../../test_app.dart'; -import '../../test_utils.dart'; +import '../../test_helpers.dart'; +import '../../test_provider_scope.dart'; final client = MockClient((request) { if (request.url.path == '/api/player') { @@ -21,7 +21,7 @@ void main() { (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); - final app = await buildTestApp( + final app = await makeProviderScopeApp( tester, overrides: [ lichessClientProvider diff --git a/test/view/user/leaderboard_widget_test.dart b/test/view/user/leaderboard_widget_test.dart index b51d501dda..fe63f0a9c3 100644 --- a/test/view/user/leaderboard_widget_test.dart +++ b/test/view/user/leaderboard_widget_test.dart @@ -5,8 +5,8 @@ import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/view/user/leaderboard_screen.dart'; import 'package:lichess_mobile/src/view/user/leaderboard_widget.dart'; -import '../../test_app.dart'; -import '../../test_utils.dart'; +import '../../test_helpers.dart'; +import '../../test_provider_scope.dart'; final client = MockClient((request) { if (request.url.path == '/api/player/top/1/standard') { @@ -21,7 +21,7 @@ void main() { 'accessibility and basic info showing test', (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); - final app = await buildTestApp( + final app = await makeProviderScopeApp( tester, home: Column(children: [LeaderboardWidget()]), overrides: [ diff --git a/test/view/user/perf_stats_screen_test.dart b/test/view/user/perf_stats_screen_test.dart index a5791e4ca8..82144e8185 100644 --- a/test/view/user/perf_stats_screen_test.dart +++ b/test/view/user/perf_stats_screen_test.dart @@ -7,8 +7,8 @@ import 'package:lichess_mobile/src/view/user/perf_stats_screen.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; import '../../model/auth/fake_auth_repository.dart'; -import '../../test_app.dart'; -import '../../test_utils.dart'; +import '../../test_helpers.dart'; +import '../../test_provider_scope.dart'; final client = MockClient((request) { if (request.url.path == '/api/user/${fakeUser.id}/perf/${testPerf.name}') { @@ -27,7 +27,7 @@ void main() { (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); - final app = await buildTestApp( + final app = await makeProviderScopeApp( tester, home: PerfStatsScreen( user: fakeUser, @@ -59,7 +59,7 @@ void main() { testWidgets( 'screen loads, required stats are shown', (WidgetTester tester) async { - final app = await buildTestApp( + final app = await makeProviderScopeApp( tester, home: PerfStatsScreen( user: fakeUser, diff --git a/test/view/user/search_screen_test.dart b/test/view/user/search_screen_test.dart index deebdfa861..d1129c9c99 100644 --- a/test/view/user/search_screen_test.dart +++ b/test/view/user/search_screen_test.dart @@ -7,8 +7,8 @@ import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/view/user/search_screen.dart'; import 'package:lichess_mobile/src/widgets/user_list_tile.dart'; -import '../../test_app.dart'; -import '../../test_utils.dart'; +import '../../test_helpers.dart'; +import '../../test_provider_scope.dart'; final client = MockClient((request) { if (request.url.path == '/api/player/autocomplete') { @@ -25,7 +25,7 @@ void main() { testWidgets( 'should see search results', (WidgetTester tester) async { - final app = await buildTestApp( + final app = await makeProviderScopeApp( tester, home: const SearchScreen(), overrides: [ @@ -66,7 +66,7 @@ void main() { testWidgets( 'should see "no result" when search finds nothing', (WidgetTester tester) async { - final app = await buildTestApp( + final app = await makeProviderScopeApp( tester, home: const SearchScreen(), overrides: [ diff --git a/test/view/user/user_screen_test.dart b/test/view/user/user_screen_test.dart index 5f3999cc92..be6e664350 100644 --- a/test/view/user/user_screen_test.dart +++ b/test/view/user/user_screen_test.dart @@ -6,8 +6,8 @@ import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/view/user/user_screen.dart'; import '../../model/user/user_repository_test.dart'; -import '../../test_app.dart'; -import '../../test_utils.dart'; +import '../../test_helpers.dart'; +import '../../test_provider_scope.dart'; final client = MockClient((request) { if (request.url.path == '/api/games/user/$testUserId') { @@ -38,7 +38,7 @@ void main() { testWidgets( 'should see activity and recent games', (WidgetTester tester) async { - final app = await buildTestApp( + final app = await makeProviderScopeApp( tester, home: const UserScreen(user: testUser), overrides: [ diff --git a/test/widgets/adaptive_choice_picker_test.dart b/test/widgets/adaptive_choice_picker_test.dart index cc76f60067..dfba25df4e 100644 --- a/test/widgets/adaptive_choice_picker_test.dart +++ b/test/widgets/adaptive_choice_picker_test.dart @@ -4,7 +4,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:lichess_mobile/l10n/l10n.dart'; import 'package:lichess_mobile/src/widgets/adaptive_choice_picker.dart'; -import '../test_utils.dart'; +import '../test_helpers.dart'; enum TestEnumLarge { one, diff --git a/test_navigator.dart b/test_navigator.dart new file mode 100644 index 0000000000..473ce7176c --- /dev/null +++ b/test_navigator.dart @@ -0,0 +1,29 @@ +import 'package:flutter/widgets.dart'; + +class TestNavigatorObserver extends NavigatorObserver { + void Function(Route route, Route? previousRoute)? onPushed; + void Function(Route route, Route? previousRoute)? onPopped; + void Function(Route route, Route? previousRoute)? onRemoved; + void Function(Route? route, Route? previousRoute)? + onReplaced; + + @override + void didPush(Route route, Route? previousRoute) { + onPushed?.call(route, previousRoute); + } + + @override + void didPop(Route route, Route? previousRoute) { + onPopped?.call(route, previousRoute); + } + + @override + void didRemove(Route route, Route? previousRoute) { + onRemoved?.call(route, previousRoute); + } + + @override + void didReplace({Route? oldRoute, Route? newRoute}) { + onReplaced?.call(newRoute, oldRoute); + } +} From 7aa335227ced4599cfad1b0dce6c5f037769348c Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 1 Oct 2024 17:37:54 +0200 Subject: [PATCH 406/979] WIP on refactoring shared preferences --- lib/main.dart | 30 +++++ lib/src/app.dart | 2 +- lib/src/binding.dart | 41 +++++- lib/src/intl.dart | 12 +- .../model/common/service/sound_service.dart | 12 +- lib/src/model/settings/brightness.dart | 2 +- .../model/settings/general_preferences.dart | 106 +++------------ lib/src/model/settings/home_preferences.dart | 74 ++--------- lib/src/model/settings/preferences.dart | 115 ++++++++++++++++ .../model/settings/preferences_storage.dart | 76 +++++++++++ lib/src/view/home/home_tab_screen.dart | 17 +-- test/app_test.dart | 19 ++- test/binding.dart | 125 ++++++++++++++++++ test/test_provider_scope.dart | 72 ++++++---- 14 files changed, 502 insertions(+), 201 deletions(-) create mode 100644 lib/src/model/settings/preferences.dart create mode 100644 lib/src/model/settings/preferences_storage.dart diff --git a/lib/main.dart b/lib/main.dart index 25f1f3e091..29d97a262b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -16,8 +16,14 @@ Future main() async { final lichessBinding = AppLichessBinding.ensureInitialized(); + // Old API. + // TODO: Remove this once all SharedPreferences usage is migrated to SharedPreferencesAsync. SharedPreferences.setPrefix('lichess.'); + await migrateSharedPreferences(); + + await lichessBinding.preloadSharedPreferences(); + // Show splash screen until app is ready // See src/app.dart for splash screen removal FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); @@ -45,3 +51,27 @@ Future main() async { ), ); } + +Future migrateSharedPreferences() async { + final prefs = await SharedPreferences.getInstance(); + final didMigrate = prefs.getBool('shared_preferences_did_migrate') ?? false; + if (didMigrate) { + return; + } + final newPrefs = SharedPreferencesAsync(); + for (final key in prefs.getKeys()) { + final value = prefs.get(key); + if (value is String) { + await newPrefs.setString(key, value); + } else if (value is int) { + await newPrefs.setInt(key, value); + } else if (value is double) { + await newPrefs.setDouble(key, value); + } else if (value is bool) { + await newPrefs.setBool(key, value); + } else if (value is List) { + await newPrefs.setStringList(key, value); + } + } + await prefs.setBool('shared_preferences_did_migrate', true); +} diff --git a/lib/src/app.dart b/lib/src/app.dart index 5bdfe576f4..8d5771a912 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -43,7 +43,7 @@ class AppInitializationScreen extends ConsumerWidget { if (result.isLoading) { // loading screen is handled by the native splash screen - return const SizedBox.shrink(); + return const SizedBox.shrink(key: Key('app_splash_screen')); } else if (result.hasError) { // We should really do everything we can to avoid this screen // but in last resort, let's show an error message and invite the diff --git a/lib/src/binding.dart b/lib/src/binding.dart index a2c5e71322..ce007cffa9 100644 --- a/lib/src/binding.dart +++ b/lib/src/binding.dart @@ -6,8 +6,9 @@ import 'package:lichess_mobile/firebase_options.dart'; import 'package:lichess_mobile/l10n/l10n.dart'; import 'package:lichess_mobile/src/model/notifications/notification_service.dart'; import 'package:lichess_mobile/src/model/notifications/notifications.dart'; +import 'package:shared_preferences/shared_preferences.dart'; -/// The glue between some platform-specific plugins and the app. +/// A singleton class that provides access to plugins and external APIs. /// /// Only one instance of this class will be created during the app's lifetime. /// See [AppLichessBinding] for the concrete implementation. @@ -54,6 +55,12 @@ abstract class LichessBinding { return instance!; } + /// The shared preferences instance. Must be preloaded before use. + /// + /// This is a synchronous getter that throws an error if shared preferences + /// have not yet been initialized. + SharedPreferencesWithCache get sharedPreferences; + /// Initialize notifications. /// /// This wraps [Firebase.initializeApp] and [FlutterLocalNotificationsPlugin.initialize]. @@ -89,6 +96,38 @@ class AppLichessBinding extends LichessBinding { return LichessBinding.instance as AppLichessBinding; } + late Future _sharedPreferencesWithCache; + SharedPreferencesWithCache? _syncSharedPreferencesWithCache; + + @override + SharedPreferencesWithCache get sharedPreferences { + if (_syncSharedPreferencesWithCache == null) { + throw FlutterError.fromParts([ + ErrorSummary('Shared preferences have not yet been preloaded.'), + ErrorHint( + 'In the app, this is done by the `await AppLichessBinding.preloadSharedPreferences()` call ' + 'in the `Future main()` method.', + ), + ErrorHint( + 'In a test, one can call `TestLichessBinding.setInitialSharedPreferencesValues({})` as the ' + "first line in the test's `main()` method.", + ), + ]); + } + return _syncSharedPreferencesWithCache!; + } + + /// Preload shared preferences. + /// + /// This should be called only once before the app starts. Must be called before + /// [sharedPreferences] is accessed. + Future preloadSharedPreferences() async { + _sharedPreferencesWithCache = SharedPreferencesWithCache.create( + cacheOptions: const SharedPreferencesWithCacheOptions(), + ); + _syncSharedPreferencesWithCache = await _sharedPreferencesWithCache; + } + @override Future initializeNotifications(Locale locale) async { await Firebase.initializeApp( diff --git a/lib/src/intl.dart b/lib/src/intl.dart index ded7c47127..6cd7c4fc02 100644 --- a/lib/src/intl.dart +++ b/lib/src/intl.dart @@ -2,8 +2,8 @@ import 'dart:convert'; import 'package:flutter/widgets.dart'; import 'package:intl/intl.dart'; -import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; -import 'package:shared_preferences/shared_preferences.dart'; +import 'package:lichess_mobile/src/binding.dart'; +import 'package:lichess_mobile/src/model/settings/preferences.dart' as pref; import 'package:timeago/timeago.dart' as timeago; /// Setup [Intl.defaultLocale] and timeago locale and messages. @@ -11,11 +11,11 @@ Future setupIntl(WidgetsBinding widgetsBinding) async { final systemLocale = widgetsBinding.platformDispatcher.locale; // Get locale from shared preferences, if any - final prefs = await SharedPreferences.getInstance(); - final json = prefs.getString(kGeneralPreferencesKey); + final json = LichessBinding.instance.sharedPreferences + .getString(pref.Category.general.storageKey); final generalPref = json != null - ? GeneralPrefsState.fromJson(jsonDecode(json) as Map) - : GeneralPrefsState.defaults; + ? pref.General.fromJson(jsonDecode(json) as Map) + : pref.General.defaults; final prefsLocale = generalPref.locale; final locale = prefsLocale ?? systemLocale; diff --git a/lib/src/model/common/service/sound_service.dart b/lib/src/model/common/service/sound_service.dart index c6108473fc..93765b8b93 100644 --- a/lib/src/model/common/service/sound_service.dart +++ b/lib/src/model/common/service/sound_service.dart @@ -2,11 +2,12 @@ import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart' show rootBundle; +import 'package:lichess_mobile/src/binding.dart'; import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; +import 'package:lichess_mobile/src/model/settings/preferences.dart' as pref; import 'package:lichess_mobile/src/model/settings/sound_theme.dart'; import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:sound_effect/sound_effect.dart'; part 'sound_service.g.dart'; @@ -77,14 +78,13 @@ class SoundService { /// This will load the sounds from assets and make them ready to be played. /// This should be called once when the app starts. static Future initialize() async { - final prefs = await SharedPreferences.getInstance(); - - final stored = prefs.getString(kGeneralPreferencesKey); + final stored = LichessBinding.instance.sharedPreferences + .getString(pref.Category.general.storageKey); final theme = (stored != null - ? GeneralPrefsState.fromJson( + ? pref.General.fromJson( jsonDecode(stored) as Map, ) - : GeneralPrefsState.defaults) + : pref.General.defaults) .soundTheme; try { diff --git a/lib/src/model/settings/brightness.dart b/lib/src/model/settings/brightness.dart index 163de4c988..d3638e1123 100644 --- a/lib/src/model/settings/brightness.dart +++ b/lib/src/model/settings/brightness.dart @@ -4,7 +4,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'brightness.g.dart'; -@Riverpod(keepAlive: true) +@riverpod class CurrentBrightness extends _$CurrentBrightness { @override Brightness build() { diff --git a/lib/src/model/settings/general_preferences.dart b/lib/src/model/settings/general_preferences.dart index b12d1c50d2..a57f638282 100644 --- a/lib/src/model/settings/general_preferences.dart +++ b/lib/src/model/settings/general_preferences.dart @@ -1,59 +1,50 @@ -import 'dart:convert'; - import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:lichess_mobile/src/db/shared_preferences.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; +import 'package:lichess_mobile/src/model/settings/preferences.dart' as pref; +import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; import 'package:lichess_mobile/src/model/settings/sound_theme.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -part 'general_preferences.freezed.dart'; part 'general_preferences.g.dart'; -const kGeneralPreferencesKey = 'preferences.general'; - -@Riverpod(keepAlive: true) -class GeneralPreferences extends _$GeneralPreferences { - static GeneralPrefsState fetchFromStorage(SharedPreferences prefs) { - final stored = prefs.getString(kGeneralPreferencesKey); - return stored != null - ? GeneralPrefsState.fromJson(jsonDecode(stored) as Map) - : GeneralPrefsState.defaults; - } +@riverpod +class GeneralPreferences extends _$GeneralPreferences + with PreferencesStorage { + // ignore: avoid_public_notifier_properties + @override + pref.Category get categoryKey => pref.Category.general; @override - GeneralPrefsState build() { - final prefs = ref.watch(sharedPreferencesProvider); - return fetchFromStorage(prefs); + pref.General build() { + return fetch(); } Future setThemeMode(ThemeMode themeMode) { - return _save(state.copyWith(themeMode: themeMode)); + return save(state.copyWith(themeMode: themeMode)); } Future toggleSoundEnabled() { - return _save(state.copyWith(isSoundEnabled: !state.isSoundEnabled)); + return save(state.copyWith(isSoundEnabled: !state.isSoundEnabled)); } Future setLocale(Locale? locale) { - return _save(state.copyWith(locale: locale)); + return save(state.copyWith(locale: locale)); } Future setSoundTheme(SoundTheme soundTheme) { - return _save(state.copyWith(soundTheme: soundTheme)); + return save(state.copyWith(soundTheme: soundTheme)); } Future setMasterVolume(double volume) { - return _save(state.copyWith(masterVolume: volume)); + return save(state.copyWith(masterVolume: volume)); } Future toggleSystemColors() async { if (defaultTargetPlatform != TargetPlatform.android) { return; } - await _save(state.copyWith(systemColors: !state.systemColors)); + await save(state.copyWith(systemColors: !state.systemColors)); if (state.systemColors == false) { final boardTheme = ref.read(boardPreferencesProvider).boardTheme; if (boardTheme == BoardTheme.system) { @@ -67,69 +58,4 @@ class GeneralPreferences extends _$GeneralPreferences { .setBoardTheme(BoardTheme.system); } } - - Future _save(GeneralPrefsState newState) async { - final prefs = ref.read(sharedPreferencesProvider); - await prefs.setString( - kGeneralPreferencesKey, - jsonEncode(newState.toJson()), - ); - state = newState; - } -} - -@Freezed(fromJson: true, toJson: true) -class GeneralPrefsState with _$GeneralPrefsState { - const factory GeneralPrefsState({ - /// Background theme mode to use in the app - @JsonKey(unknownEnumValue: ThemeMode.system) required ThemeMode themeMode, - required bool isSoundEnabled, - @JsonKey(unknownEnumValue: SoundTheme.standard) - required SoundTheme soundTheme, - @JsonKey(defaultValue: 0.8) required double masterVolume, - - /// Should enable system color palette (android 12+ only) - required bool systemColors, - - /// Locale to use in the app, use system locale if null - @JsonKey(toJson: _localeToJson, fromJson: _localeFromJson) Locale? locale, - }) = _GeneralPrefsState; - - static const defaults = GeneralPrefsState( - themeMode: ThemeMode.system, - isSoundEnabled: true, - soundTheme: SoundTheme.standard, - masterVolume: 0.8, - systemColors: true, - ); - - factory GeneralPrefsState.fromJson(Map json) { - try { - return _$GeneralPrefsStateFromJson(json); - } catch (e) { - debugPrint('Error parsing GeneralPrefsState: $e'); - return defaults; - } - } -} - -Map? _localeToJson(Locale? locale) { - return locale != null - ? { - 'languageCode': locale.languageCode, - 'countryCode': locale.countryCode, - 'scriptCode': locale.scriptCode, - } - : null; -} - -Locale? _localeFromJson(Map? json) { - if (json == null) { - return null; - } - return Locale.fromSubtags( - languageCode: json['languageCode'] as String, - countryCode: json['countryCode'] as String?, - scriptCode: json['scriptCode'] as String?, - ); } diff --git a/lib/src/model/settings/home_preferences.dart b/lib/src/model/settings/home_preferences.dart index 19f5d65b5d..fcbb87cae6 100644 --- a/lib/src/model/settings/home_preferences.dart +++ b/lib/src/model/settings/home_preferences.dart @@ -1,77 +1,27 @@ -import 'dart:convert'; - -import 'package:flutter/foundation.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:lichess_mobile/src/db/shared_preferences.dart'; +import 'package:lichess_mobile/src/model/settings/preferences.dart' as pref; +import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -part 'home_preferences.freezed.dart'; part 'home_preferences.g.dart'; -const _prefKey = 'preferences.home'; - -@Riverpod(keepAlive: true) -class HomePreferences extends _$HomePreferences { - static HomePrefState fetchFromStorage(SharedPreferences prefs) { - final stored = prefs.getString(_prefKey); - return stored != null - ? HomePrefState.fromJson( - jsonDecode(stored) as Map, - ) - : HomePrefState.defaults; - } +@riverpod +class HomePreferences extends _$HomePreferences + with PreferencesStorage { + // ignore: avoid_public_notifier_properties + @override + pref.Category get categoryKey => pref.Category.home; @override - HomePrefState build() { - final prefs = ref.watch(sharedPreferencesProvider); - return fetchFromStorage(prefs); + pref.Home build() { + return fetch(); } - Future toggleWidget(EnabledWidget widget) { + Future toggleWidget(pref.EnabledWidget widget) { final newState = state.copyWith( enabledWidgets: state.enabledWidgets.contains(widget) ? state.enabledWidgets.difference({widget}) : state.enabledWidgets.union({widget}), ); - return _save(newState); - } - - Future _save(HomePrefState newState) async { - final prefs = ref.read(sharedPreferencesProvider); - await prefs.setString( - _prefKey, - jsonEncode(newState.toJson()), - ); - state = newState; - } -} - -enum EnabledWidget { - hello, - perfCards, - quickPairing, -} - -@Freezed(fromJson: true, toJson: true) -class HomePrefState with _$HomePrefState { - const factory HomePrefState({ - required Set enabledWidgets, - }) = _HomePrefState; - - static const defaults = HomePrefState( - enabledWidgets: { - EnabledWidget.hello, - EnabledWidget.perfCards, - EnabledWidget.quickPairing, - }, - ); - - factory HomePrefState.fromJson(Map json) { - try { - return _$HomePrefStateFromJson(json); - } catch (_) { - return defaults; - } + return save(newState); } } diff --git a/lib/src/model/settings/preferences.dart b/lib/src/model/settings/preferences.dart new file mode 100644 index 0000000000..2661fcd6cd --- /dev/null +++ b/lib/src/model/settings/preferences.dart @@ -0,0 +1,115 @@ +import 'package:flutter/material.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:lichess_mobile/src/model/settings/sound_theme.dart'; + +part 'preferences.freezed.dart'; +part 'preferences.g.dart'; + +/// Interface for serializable preferences. +abstract class SerializablePreferences { + Map toJson(); + + static SerializablePreferences fromJson( + Category key, + Map json, + ) { + switch (key) { + case Category.general: + return General.fromJson(json); + case Category.home: + return Home.fromJson(json); + } + } +} + +/// A preference category type with its storage key and default values. +enum Category { + general('preferences.general', General.defaults), + home('preferences.home', Home.defaults); + + const Category(this.storageKey, this.defaults); + + final String storageKey; + final T defaults; +} + +/// General preferences for the app. +@Freezed(fromJson: true, toJson: true) +class General with _$General implements SerializablePreferences { + const factory General({ + @JsonKey(unknownEnumValue: ThemeMode.system, defaultValue: ThemeMode.system) + required ThemeMode themeMode, + required bool isSoundEnabled, + @JsonKey(unknownEnumValue: SoundTheme.standard) + required SoundTheme soundTheme, + @JsonKey(defaultValue: 0.8) required double masterVolume, + + /// Should enable system color palette (android 12+ only) + required bool systemColors, + + /// Locale to use in the app, use system locale if null + @JsonKey(toJson: _localeToJson, fromJson: _localeFromJson) Locale? locale, + }) = _General; + + static const defaults = General( + themeMode: ThemeMode.system, + isSoundEnabled: true, + soundTheme: SoundTheme.standard, + masterVolume: 0.8, + systemColors: true, + ); + + factory General.fromJson(Map json) { + return _$GeneralFromJson(json); + } +} + +Map? _localeToJson(Locale? locale) { + return locale != null + ? { + 'languageCode': locale.languageCode, + 'countryCode': locale.countryCode, + 'scriptCode': locale.scriptCode, + } + : null; +} + +Locale? _localeFromJson(Map? json) { + if (json == null) { + return null; + } + return Locale.fromSubtags( + languageCode: json['languageCode'] as String, + countryCode: json['countryCode'] as String?, + scriptCode: json['scriptCode'] as String?, + ); +} + +enum EnabledWidget { + hello, + perfCards, + quickPairing, +} + +@Freezed(fromJson: true, toJson: true) +class Home with _$Home implements SerializablePreferences { + const factory Home({ + required Set enabledWidgets, + }) = _Home; + + static const defaults = Home( + enabledWidgets: { + EnabledWidget.hello, + EnabledWidget.perfCards, + EnabledWidget.quickPairing, + }, + ); + + factory Home.fromJson(Map json) { + try { + return _$HomeFromJson(json); + } catch (_) { + return defaults; + } + } +} diff --git a/lib/src/model/settings/preferences_storage.dart b/lib/src/model/settings/preferences_storage.dart new file mode 100644 index 0000000000..6dc71fbc79 --- /dev/null +++ b/lib/src/model/settings/preferences_storage.dart @@ -0,0 +1,76 @@ +import 'dart:convert'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/binding.dart'; +import 'package:lichess_mobile/src/model/auth/auth_session.dart'; +import 'package:lichess_mobile/src/model/settings/preferences.dart'; + +/// A mixin to provide a way to store and retrieve preferences. +/// +/// This mixin is intended to be used with a [Notifier] that holds a +/// [SerializablePreferences] object. It provides a way to save and fetch the +/// preferences from the shared preferences. +mixin PreferencesStorage { + AutoDisposeNotifierProviderRef get ref; + abstract T state; + + Category get categoryKey; + + Future save(T value) async { + await LichessBinding.instance.sharedPreferences + .setString(categoryKey.storageKey, jsonEncode(value.toJson())); + + state = value; + } + + T fetch() { + final stored = LichessBinding.instance.sharedPreferences + .getString(categoryKey.storageKey); + if (stored == null) { + return categoryKey.defaults; + } + return SerializablePreferences.fromJson( + categoryKey, + jsonDecode(stored) as Map, + ) as T; + } +} + +/// A mixin to provide a way to store and retrieve preferences per session. +/// +/// This mixin is intended to be used with a [Notifier] that holds a +/// [SerializablePreferences] object. It provides a way to save and fetch the +/// preferences from the shared preferences, using the current session to +/// differentiate between different users. +mixin SessionPreferencesStorage { + AutoDisposeNotifierProviderRef get ref; + abstract T state; + + Category get categoryKey; + + Future save(T value) async { + final session = ref.read(authSessionProvider); + await LichessBinding.instance.sharedPreferences.setString( + _prefKey(categoryKey.storageKey, session), + jsonEncode(value.toJson()), + ); + + state = value; + } + + T fetch() { + final session = ref.watch(authSessionProvider); + final stored = LichessBinding.instance.sharedPreferences + .getString(_prefKey(categoryKey.storageKey, session)); + if (stored == null) { + return categoryKey.defaults; + } + return SerializablePreferences.fromJson( + categoryKey, + jsonDecode(stored) as Map, + ) as T; + } + + String _prefKey(String key, AuthSessionState? session) => + '$key.${session?.user.id ?? '**anon**'}'; +} diff --git a/lib/src/view/home/home_tab_screen.dart b/lib/src/view/home/home_tab_screen.dart index bff4db350e..4a7b78919a 100644 --- a/lib/src/view/home/home_tab_screen.dart +++ b/lib/src/view/home/home_tab_screen.dart @@ -10,6 +10,7 @@ import 'package:lichess_mobile/src/model/challenge/challenges.dart'; import 'package:lichess_mobile/src/model/correspondence/correspondence_game_storage.dart'; import 'package:lichess_mobile/src/model/game/game_history.dart'; import 'package:lichess_mobile/src/model/settings/home_preferences.dart'; +import 'package:lichess_mobile/src/model/settings/preferences.dart' as pref; import 'package:lichess_mobile/src/navigation.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; @@ -239,13 +240,13 @@ class _HomeBody extends ConsumerWidget { final widgets = isTablet ? [ _EditableWidget( - widget: EnabledWidget.hello, + widget: pref.EnabledWidget.hello, isEditing: isEditing, shouldShow: true, child: const _HelloWidget(), ), _EditableWidget( - widget: EnabledWidget.perfCards, + widget: pref.EnabledWidget.perfCards, isEditing: isEditing, shouldShow: session != null, child: const AccountPerfCards( @@ -285,13 +286,13 @@ class _HomeBody extends ConsumerWidget { ] : [ _EditableWidget( - widget: EnabledWidget.hello, + widget: pref.EnabledWidget.hello, isEditing: isEditing, shouldShow: true, child: const _HelloWidget(), ), _EditableWidget( - widget: EnabledWidget.perfCards, + widget: pref.EnabledWidget.perfCards, isEditing: isEditing, shouldShow: session != null, child: const AccountPerfCards( @@ -299,7 +300,7 @@ class _HomeBody extends ConsumerWidget { ), ), _EditableWidget( - widget: EnabledWidget.quickPairing, + widget: pref.EnabledWidget.quickPairing, isEditing: isEditing, shouldShow: status.isOnline, child: const Padding( @@ -352,7 +353,7 @@ class _EditableWidget extends ConsumerWidget { }); final Widget child; - final EnabledWidget widget; + final pref.EnabledWidget widget; final bool isEditing; final bool shouldShow; @@ -466,7 +467,7 @@ class _WelcomeScreen extends StatelessWidget { else ...[ if (status.isOnline) _EditableWidget( - widget: EnabledWidget.quickPairing, + widget: pref.EnabledWidget.quickPairing, isEditing: isEditing, shouldShow: true, child: const Padding( @@ -567,7 +568,7 @@ class _TabletCreateAGameSection extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.center, children: [ _EditableWidget( - widget: EnabledWidget.quickPairing, + widget: pref.EnabledWidget.quickPairing, isEditing: isEditing, shouldShow: true, child: const Padding( diff --git a/test/app_test.dart b/test/app_test.dart index ab73b3a234..5015f30b96 100644 --- a/test/app_test.dart +++ b/test/app_test.dart @@ -1 +1,18 @@ -void main() {} +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:lichess_mobile/src/app.dart'; + +import 'test_provider_scope.dart'; + +void main() { + testWidgets('App initialization screen', (tester) async { + final app = await makeProviderScope( + tester, + child: const AppInitializationScreen(), + ); + + await tester.pumpWidget(app); + + expect(find.byKey(const Key('app_splash_screen')), findsOneWidget); + }); +} diff --git a/test/binding.dart b/test/binding.dart index ab9205c01c..eb9661d27a 100644 --- a/test/binding.dart +++ b/test/binding.dart @@ -4,6 +4,7 @@ import 'dart:ui'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:lichess_mobile/src/binding.dart'; +import 'package:shared_preferences/shared_preferences.dart'; /// The binding instance used in tests. TestLichessBinding get testBinding => TestLichessBinding.instance; @@ -34,11 +35,47 @@ class TestLichessBinding extends LichessBinding { _instance = this; } + /// Set the initial values for shared preferences. + Future setInitialSharedPreferencesValues( + Map values, + ) async { + for (final entry in values.entries) { + if (entry.value is String) { + await sharedPreferences.setString(entry.key, entry.value as String); + } else if (entry.value is bool) { + await sharedPreferences.setBool(entry.key, entry.value as bool); + } else if (entry.value is double) { + await sharedPreferences.setDouble(entry.key, entry.value as double); + } else if (entry.value is int) { + await sharedPreferences.setInt(entry.key, entry.value as int); + } else if (entry.value is List) { + await sharedPreferences.setStringList( + entry.key, + entry.value as List, + ); + } else { + throw ArgumentError.value( + entry.value, + 'values', + 'Unsupported value type: ${entry.value.runtimeType}', + ); + } + } + } + + FakeSharedPreferences? _sharedPreferences; + + @override + FakeSharedPreferences get sharedPreferences { + return _sharedPreferences ??= FakeSharedPreferences(); + } + /// Reset the binding instance. /// /// Should be called using [addTearDown] in tests. void reset() { _firebaseMessaging = null; + _sharedPreferences = null; } FakeFirebaseMessaging? _firebaseMessaging; @@ -65,6 +102,94 @@ class TestLichessBinding extends LichessBinding { firebaseMessaging.onMessageOpenedApp.stream; } +class FakeSharedPreferences implements SharedPreferencesWithCache { + final Map _values = {}; + + @override + Future remove(String key) async { + _values.remove(key); + return true; + } + + @override + Future clear({Set? allowList}) async { + _values.clear(); + } + + @override + bool containsKey(String key) { + return _values.containsKey(key); + } + + @override + String? getString(String key) { + return _values[key] as String?; + } + + @override + bool? getBool(String key) { + return _values[key] as bool?; + } + + @override + double? getDouble(String key) { + return _values[key] as double?; + } + + @override + int? getInt(String key) { + return _values[key] as int?; + } + + @override + List? getStringList(String key) { + return _values[key] as List?; + } + + @override + Future setString(String key, String value) async { + _values[key] = value; + return true; + } + + @override + Future setBool(String key, bool value) { + _values[key] = value; + return Future.value(); + } + + @override + Future setDouble(String key, double value) { + _values[key] = value; + return Future.value(); + } + + @override + Future setInt(String key, int value) { + _values[key] = value; + return Future.value(); + } + + @override + Future setStringList(String key, List value) { + _values[key] = value; + return Future.value(); + } + + @override + Object? get(String key) { + return _values[key]; + } + + @override + Set get keys => _values.keys.toSet(); + + @override + Future reloadCache() { + return Future.value(); + } +} + typedef FirebaseMessagingRequestPermissionCall = ({ bool alert, bool announcement, diff --git a/test/test_provider_scope.dart b/test/test_provider_scope.dart index 8bfc5a79d2..791ba768cc 100644 --- a/test/test_provider_scope.dart +++ b/test/test_provider_scope.dart @@ -40,10 +40,11 @@ final mockClient = MockClient((request) async { return http.Response('', 200); }); -/// Returns a [MaterialApp] wrapped with a [ProviderScope] and default mocks, ready for testing. +/// Returns a [MaterialApp] wrapped in a [ProviderScope] and default mocks, ready for testing. /// -/// The [home] widget is the widget we want to test. It is wrapped in a [MediaQuery] -/// and [MaterialApp] widgets to simulate a simple app. +/// The [home] widget is the widget we want to test. Typically a screen widget, to +/// perform end-to-end tests. +/// It will be wrapped in a [MaterialApp] to simulate a simple app. /// /// The [overrides] parameter can be used to override any provider in the app. /// The [userSession] parameter can be used to set the initial user session state. @@ -54,6 +55,40 @@ Future makeProviderScopeApp( List? overrides, AuthSessionState? userSession, Map? defaultPreferences, +}) async { + return makeProviderScope( + tester, + child: MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + home: home, + builder: (context, child) { + return CupertinoTheme( + data: const CupertinoThemeData(), + child: Material(child: child), + ); + }, + ), + overrides: overrides, + userSession: userSession, + defaultPreferences: defaultPreferences, + ); +} + +/// Returns a [ProviderScope] and default mocks, ready for testing. +/// +/// The [child] widget is the widget we want to test. It will be wrapped in a +/// [MediaQuery.new] widget, to simulate a device with a specific size, controlled +/// by [kTestSurfaceSize]. +/// +/// The [overrides] parameter can be used to override any provider in the app. +/// The [userSession] parameter can be used to set the initial user session state. +/// The [defaultPreferences] parameter can be used to set the initial shared preferences. +Future makeProviderScope( + WidgetTester tester, { + required Widget child, + List? overrides, + AuthSessionState? userSession, + Map? defaultPreferences, }) async { final binding = TestLichessBinding.ensureInitialized(); @@ -162,28 +197,15 @@ Future makeProviderScopeApp( }), ...overrides ?? [], ], - child: Consumer( - builder: (context, ref, child) { - return MediaQuery( - data: const MediaQueryData(size: kTestSurfaceSize), - child: Center( - child: SizedBox( - width: kTestSurfaceSize.width, - height: kTestSurfaceSize.height, - child: MaterialApp( - localizationsDelegates: AppLocalizations.localizationsDelegates, - home: home, - builder: (context, child) { - return CupertinoTheme( - data: const CupertinoThemeData(), - child: Material(child: child), - ); - }, - ), - ), - ), - ); - }, + child: MediaQuery( + data: const MediaQueryData(size: kTestSurfaceSize), + child: Center( + child: SizedBox( + width: kTestSurfaceSize.width, + height: kTestSurfaceSize.height, + child: child, + ), + ), ), ); } From d5b90c3d17a7d55c213d4ac19036de402ae448fc Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 1 Oct 2024 21:42:00 +0200 Subject: [PATCH 407/979] More wip on refactoring preferences --- lib/src/init.dart | 11 +- .../model/analysis/analysis_preferences.dart | 84 +---- .../model/common/service/sound_service.dart | 7 +- lib/src/model/settings/board_preferences.dart | 264 ++------------ .../model/settings/general_preferences.dart | 9 +- .../settings/over_the_board_preferences.dart | 63 +--- lib/src/model/settings/preferences.dart | 326 ++++++++++++++++-- .../model/settings/preferences_storage.dart | 24 +- lib/src/model/settings/sound_theme.dart | 12 - lib/src/view/settings/board_theme_screen.dart | 1 + .../view/settings/sound_settings_screen.dart | 2 +- lib/src/view/settings/theme_screen.dart | 1 + .../common/service/fake_sound_service.dart | 2 +- test/test_provider_scope.dart | 4 +- 14 files changed, 393 insertions(+), 417 deletions(-) delete mode 100644 lib/src/model/settings/sound_theme.dart diff --git a/lib/src/init.dart b/lib/src/init.dart index df7d5b0471..c5f17c5ee1 100644 --- a/lib/src/init.dart +++ b/lib/src/init.dart @@ -7,11 +7,12 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_displaymode/flutter_displaymode.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:lichess_mobile/src/binding.dart'; import 'package:lichess_mobile/src/db/secure_storage.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/auth/session_storage.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; -import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; +import 'package:lichess_mobile/src/model/settings/preferences.dart' as pref; import 'package:lichess_mobile/src/utils/color_palette.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/utils/string.dart'; @@ -115,7 +116,7 @@ Future setupFirstLaunch() async { /// /// This is meant to be called once during app initialization. Future androidDisplayInitialization(WidgetsBinding widgetsBinding) async { - final prefs = await SharedPreferences.getInstance(); + final prefs = LichessBinding.instance.sharedPreferences; // On android 12+ get core palette and set the board theme to system if it is not set try { @@ -123,11 +124,11 @@ Future androidDisplayInitialization(WidgetsBinding widgetsBinding) async { setCorePalette(value); if (getCorePalette() != null && - prefs.getString(BoardPreferences.prefKey) == null) { + prefs.getString(pref.Category.board.storageKey) == null) { prefs.setString( - BoardPreferences.prefKey, + pref.Category.board.storageKey, jsonEncode( - BoardPrefs.defaults.copyWith(boardTheme: BoardTheme.system), + pref.Board.defaults.copyWith(boardTheme: pref.BoardTheme.system), ), ); } diff --git a/lib/src/model/analysis/analysis_preferences.dart b/lib/src/model/analysis/analysis_preferences.dart index 4cfc0e1daa..bd0463ea73 100644 --- a/lib/src/model/analysis/analysis_preferences.dart +++ b/lib/src/model/analysis/analysis_preferences.dart @@ -1,31 +1,24 @@ -import 'dart:convert'; - -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:lichess_mobile/src/db/shared_preferences.dart'; import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; +import 'package:lichess_mobile/src/model/settings/preferences.dart' as pref; +import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -part 'analysis_preferences.freezed.dart'; part 'analysis_preferences.g.dart'; -@Riverpod(keepAlive: true) -class AnalysisPreferences extends _$AnalysisPreferences { - static const prefKey = 'preferences.analysis'; - +@riverpod +class AnalysisPreferences extends _$AnalysisPreferences + with PreferencesStorage { + // ignore: avoid_public_notifier_properties @override - AnalysisPrefState build() { - final prefs = ref.watch(sharedPreferencesProvider); + pref.Category get categoryKey => pref.Category.analysis; - final stored = prefs.getString(prefKey); - return stored != null - ? AnalysisPrefState.fromJson( - jsonDecode(stored) as Map, - ) - : AnalysisPrefState.defaults; + @override + pref.Analysis build() { + return fetch(); } Future toggleEnableLocalEvaluation() { - return _save( + return save( state.copyWith( enableLocalEvaluation: !state.enableLocalEvaluation, ), @@ -33,7 +26,7 @@ class AnalysisPreferences extends _$AnalysisPreferences { } Future toggleShowEvaluationGauge() { - return _save( + return save( state.copyWith( showEvaluationGauge: !state.showEvaluationGauge, ), @@ -41,7 +34,7 @@ class AnalysisPreferences extends _$AnalysisPreferences { } Future toggleAnnotations() { - return _save( + return save( state.copyWith( showAnnotations: !state.showAnnotations, ), @@ -49,7 +42,7 @@ class AnalysisPreferences extends _$AnalysisPreferences { } Future togglePgnComments() { - return _save( + return save( state.copyWith( showPgnComments: !state.showPgnComments, ), @@ -57,7 +50,7 @@ class AnalysisPreferences extends _$AnalysisPreferences { } Future toggleShowBestMoveArrow() { - return _save( + return save( state.copyWith( showBestMoveArrow: !state.showBestMoveArrow, ), @@ -66,7 +59,7 @@ class AnalysisPreferences extends _$AnalysisPreferences { Future setNumEvalLines(int numEvalLines) { assert(numEvalLines >= 1 && numEvalLines <= 3); - return _save( + return save( state.copyWith( numEvalLines: numEvalLines, ), @@ -75,53 +68,10 @@ class AnalysisPreferences extends _$AnalysisPreferences { Future setEngineCores(int numEngineCores) { assert(numEngineCores >= 1 && numEngineCores <= maxEngineCores); - return _save( + return save( state.copyWith( numEngineCores: numEngineCores, ), ); } - - Future _save(AnalysisPrefState newState) async { - final prefs = ref.read(sharedPreferencesProvider); - await prefs.setString( - prefKey, - jsonEncode(newState.toJson()), - ); - state = newState; - } -} - -@Freezed(fromJson: true, toJson: true) -class AnalysisPrefState with _$AnalysisPrefState { - const AnalysisPrefState._(); - - const factory AnalysisPrefState({ - required bool enableLocalEvaluation, - required bool showEvaluationGauge, - required bool showBestMoveArrow, - required bool showAnnotations, - required bool showPgnComments, - @Assert('numEvalLines >= 1 && numEvalLines <= 3') required int numEvalLines, - @Assert('numEngineCores >= 1 && numEngineCores <= maxEngineCores') - required int numEngineCores, - }) = _AnalysisPrefState; - - static final defaults = AnalysisPrefState( - enableLocalEvaluation: true, - showEvaluationGauge: true, - showBestMoveArrow: true, - showAnnotations: true, - showPgnComments: true, - numEvalLines: 2, - numEngineCores: defaultEngineCores, - ); - - factory AnalysisPrefState.fromJson(Map json) { - try { - return _$AnalysisPrefStateFromJson(json); - } catch (_) { - return defaults; - } - } } diff --git a/lib/src/model/common/service/sound_service.dart b/lib/src/model/common/service/sound_service.dart index 93765b8b93..c70ed587ee 100644 --- a/lib/src/model/common/service/sound_service.dart +++ b/lib/src/model/common/service/sound_service.dart @@ -5,7 +5,6 @@ import 'package:flutter/services.dart' show rootBundle; import 'package:lichess_mobile/src/binding.dart'; import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; import 'package:lichess_mobile/src/model/settings/preferences.dart' as pref; -import 'package:lichess_mobile/src/model/settings/sound_theme.dart'; import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:sound_effect/sound_effect.dart'; @@ -41,7 +40,7 @@ const Set _emtpySet = {}; /// Loads all sounds of the given [SoundTheme]. Future _loadAllSounds( - SoundTheme soundTheme, { + pref.SoundTheme soundTheme, { Set excluded = _emtpySet, }) async { await Future.wait( @@ -52,7 +51,7 @@ Future _loadAllSounds( } /// Loads a single sound from the given [SoundTheme]. -Future _loadSound(SoundTheme theme, Sound sound) async { +Future _loadSound(pref.SoundTheme theme, Sound sound) async { final themePath = 'assets/sounds/${theme.name}'; const standardPath = 'assets/sounds/standard'; final soundId = sound.name; @@ -111,7 +110,7 @@ class SoundService { /// /// If [playSound] is true, a move sound will be played. Future changeTheme( - SoundTheme theme, { + pref.SoundTheme theme, { bool playSound = false, }) async { await _soundEffectPlugin.release(); diff --git a/lib/src/model/settings/board_preferences.dart b/lib/src/model/settings/board_preferences.dart index 8e75b1a78c..e22dd3c7a6 100644 --- a/lib/src/model/settings/board_preferences.dart +++ b/lib/src/model/settings/board_preferences.dart @@ -1,48 +1,41 @@ -import 'dart:convert'; - import 'package:chessground/chessground.dart'; -import 'package:flutter/widgets.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:lichess_mobile/src/db/shared_preferences.dart'; -import 'package:lichess_mobile/src/utils/color_palette.dart'; +import 'package:lichess_mobile/src/model/settings/preferences.dart' as pref; +import 'package:lichess_mobile/src/model/settings/preferences.dart'; +import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -part 'board_preferences.freezed.dart'; part 'board_preferences.g.dart'; -@Riverpod(keepAlive: true) -class BoardPreferences extends _$BoardPreferences { - static const String prefKey = 'preferences.board'; +@riverpod +class BoardPreferences extends _$BoardPreferences + with PreferencesStorage { + // ignore: avoid_public_notifier_properties + @override + pref.Category get categoryKey => pref.Category.board; @override - BoardPrefs build() { - final prefs = ref.watch(sharedPreferencesProvider); - final stored = prefs.getString(prefKey); - return stored != null - ? BoardPrefs.fromJson( - jsonDecode(stored) as Map, - ) - : BoardPrefs.defaults; + pref.Board build() { + return fetch(); } Future setPieceSet(PieceSet pieceSet) { - return _save(state.copyWith(pieceSet: pieceSet)); + return save(state.copyWith(pieceSet: pieceSet)); } Future setBoardTheme(BoardTheme boardTheme) async { - await _save(state.copyWith(boardTheme: boardTheme)); + await save(state.copyWith(boardTheme: boardTheme)); } Future setPieceShiftMethod(PieceShiftMethod pieceShiftMethod) async { - await _save(state.copyWith(pieceShiftMethod: pieceShiftMethod)); + await save(state.copyWith(pieceShiftMethod: pieceShiftMethod)); } Future toggleHapticFeedback() { - return _save(state.copyWith(hapticFeedback: !state.hapticFeedback)); + return save(state.copyWith(hapticFeedback: !state.hapticFeedback)); } Future toggleImmersiveModeWhilePlaying() { - return _save( + return save( state.copyWith( immersiveModeWhilePlaying: !(state.immersiveModeWhilePlaying ?? false), ), @@ -50,23 +43,23 @@ class BoardPreferences extends _$BoardPreferences { } Future toggleShowLegalMoves() { - return _save(state.copyWith(showLegalMoves: !state.showLegalMoves)); + return save(state.copyWith(showLegalMoves: !state.showLegalMoves)); } Future toggleBoardHighlights() { - return _save(state.copyWith(boardHighlights: !state.boardHighlights)); + return save(state.copyWith(boardHighlights: !state.boardHighlights)); } Future toggleCoordinates() { - return _save(state.copyWith(coordinates: !state.coordinates)); + return save(state.copyWith(coordinates: !state.coordinates)); } Future togglePieceAnimation() { - return _save(state.copyWith(pieceAnimation: !state.pieceAnimation)); + return save(state.copyWith(pieceAnimation: !state.pieceAnimation)); } Future toggleMagnifyDraggedPiece() { - return _save( + return save( state.copyWith( magnifyDraggedPiece: !state.magnifyDraggedPiece, ), @@ -74,227 +67,18 @@ class BoardPreferences extends _$BoardPreferences { } Future toggleShowMaterialDifference() { - return _save( + return save( state.copyWith(showMaterialDifference: !state.showMaterialDifference), ); } Future toggleEnableShapeDrawings() { - return _save( + return save( state.copyWith(enableShapeDrawings: !state.enableShapeDrawings), ); } Future setShapeColor(ShapeColor shapeColor) { - return _save(state.copyWith(shapeColor: shapeColor)); - } - - Future _save(BoardPrefs newState) async { - final prefs = ref.read(sharedPreferencesProvider); - await prefs.setString( - prefKey, - jsonEncode(newState.toJson()), - ); - state = newState; - } -} - -@Freezed(fromJson: true, toJson: true) -class BoardPrefs with _$BoardPrefs { - const BoardPrefs._(); - - const factory BoardPrefs({ - required PieceSet pieceSet, - required BoardTheme boardTheme, - bool? immersiveModeWhilePlaying, - required bool hapticFeedback, - required bool showLegalMoves, - required bool boardHighlights, - required bool coordinates, - required bool pieceAnimation, - required bool showMaterialDifference, - @JsonKey( - defaultValue: PieceShiftMethod.either, - unknownEnumValue: PieceShiftMethod.either, - ) - required PieceShiftMethod pieceShiftMethod, - - /// Whether to enable shape drawings on the board for games and puzzles. - @JsonKey(defaultValue: true) required bool enableShapeDrawings, - @JsonKey(defaultValue: true) required bool magnifyDraggedPiece, - @JsonKey( - defaultValue: ShapeColor.green, - unknownEnumValue: ShapeColor.green, - ) - required ShapeColor shapeColor, - }) = _BoardPrefs; - - static const defaults = BoardPrefs( - pieceSet: PieceSet.staunty, - boardTheme: BoardTheme.brown, - immersiveModeWhilePlaying: false, - hapticFeedback: true, - showLegalMoves: true, - boardHighlights: true, - coordinates: true, - pieceAnimation: true, - showMaterialDifference: true, - pieceShiftMethod: PieceShiftMethod.either, - enableShapeDrawings: true, - magnifyDraggedPiece: true, - shapeColor: ShapeColor.green, - ); - - ChessboardSettings toBoardSettings() { - return ChessboardSettings( - pieceAssets: pieceSet.assets, - colorScheme: boardTheme.colors, - showValidMoves: showLegalMoves, - showLastMove: boardHighlights, - enableCoordinates: coordinates, - animationDuration: pieceAnimationDuration, - dragFeedbackScale: magnifyDraggedPiece ? 2.0 : 1.0, - dragFeedbackOffset: Offset(0.0, magnifyDraggedPiece ? -1.0 : 0.0), - pieceShiftMethod: pieceShiftMethod, - drawShape: DrawShapeOptions( - enable: enableShapeDrawings, - newShapeColor: shapeColor.color, - ), - ); + return save(state.copyWith(shapeColor: shapeColor)); } - - factory BoardPrefs.fromJson(Map json) { - try { - return _$BoardPrefsFromJson(json); - } catch (_) { - return defaults; - } - } - - Duration get pieceAnimationDuration => - pieceAnimation ? const Duration(milliseconds: 150) : Duration.zero; -} - -/// Colors taken from lila: https://github.com/lichess-org/chessground/blob/54a7e71bf88701c1109d3b9b8106b464012b94cf/src/state.ts#L178 -enum ShapeColor { - green, - red, - blue, - yellow; - - Color get color => Color( - switch (this) { - ShapeColor.green => 0x15781B, - ShapeColor.red => 0x882020, - ShapeColor.blue => 0x003088, - ShapeColor.yellow => 0xe68f00, - }, - ).withAlpha(0xAA); -} - -/// The chessboard theme. -enum BoardTheme { - system('System'), - blue('Blue'), - blue2('Blue2'), - blue3('Blue3'), - blueMarble('Blue Marble'), - canvas('Canvas'), - wood('Wood'), - wood2('Wood2'), - wood3('Wood3'), - wood4('Wood4'), - maple('Maple'), - maple2('Maple 2'), - brown('Brown'), - leather('Leather'), - green('Green'), - marble('Marble'), - greenPlastic('Green Plastic'), - grey('Grey'), - metal('Metal'), - olive('Olive'), - newspaper('Newspaper'), - purpleDiag('Purple-Diag'), - pinkPyramid('Pink'), - horsey('Horsey'); - - final String label; - - const BoardTheme(this.label); - - ChessboardColorScheme get colors { - switch (this) { - case BoardTheme.system: - return getBoardColorScheme() ?? ChessboardColorScheme.brown; - case BoardTheme.blue: - return ChessboardColorScheme.blue; - case BoardTheme.blue2: - return ChessboardColorScheme.blue2; - case BoardTheme.blue3: - return ChessboardColorScheme.blue3; - case BoardTheme.blueMarble: - return ChessboardColorScheme.blueMarble; - case BoardTheme.canvas: - return ChessboardColorScheme.canvas; - case BoardTheme.wood: - return ChessboardColorScheme.wood; - case BoardTheme.wood2: - return ChessboardColorScheme.wood2; - case BoardTheme.wood3: - return ChessboardColorScheme.wood3; - case BoardTheme.wood4: - return ChessboardColorScheme.wood4; - case BoardTheme.maple: - return ChessboardColorScheme.maple; - case BoardTheme.maple2: - return ChessboardColorScheme.maple2; - case BoardTheme.brown: - return ChessboardColorScheme.brown; - case BoardTheme.leather: - return ChessboardColorScheme.leather; - case BoardTheme.green: - return ChessboardColorScheme.green; - case BoardTheme.marble: - return ChessboardColorScheme.marble; - case BoardTheme.greenPlastic: - return ChessboardColorScheme.greenPlastic; - case BoardTheme.grey: - return ChessboardColorScheme.grey; - case BoardTheme.metal: - return ChessboardColorScheme.metal; - case BoardTheme.olive: - return ChessboardColorScheme.olive; - case BoardTheme.newspaper: - return ChessboardColorScheme.newspaper; - case BoardTheme.purpleDiag: - return ChessboardColorScheme.purpleDiag; - case BoardTheme.pinkPyramid: - return ChessboardColorScheme.pinkPyramid; - case BoardTheme.horsey: - return ChessboardColorScheme.horsey; - } - } - - Widget get thumbnail => this == BoardTheme.system - ? SizedBox( - height: 44, - width: 44 * 6, - child: Row( - children: [ - for (final c in const [1, 2, 3, 4, 5, 6]) - Container( - width: 44, - color: c.isEven - ? BoardTheme.system.colors.darkSquare - : BoardTheme.system.colors.lightSquare, - ), - ], - ), - ) - : Image.asset( - 'assets/board-thumbnails/$name.jpg', - height: 44, - errorBuilder: (context, o, st) => const SizedBox.shrink(), - ); } diff --git a/lib/src/model/settings/general_preferences.dart b/lib/src/model/settings/general_preferences.dart index a57f638282..9d9b22ea4e 100644 --- a/lib/src/model/settings/general_preferences.dart +++ b/lib/src/model/settings/general_preferences.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/model/settings/preferences.dart' as pref; import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; -import 'package:lichess_mobile/src/model/settings/sound_theme.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'general_preferences.g.dart'; @@ -32,7 +31,7 @@ class GeneralPreferences extends _$GeneralPreferences return save(state.copyWith(locale: locale)); } - Future setSoundTheme(SoundTheme soundTheme) { + Future setSoundTheme(pref.SoundTheme soundTheme) { return save(state.copyWith(soundTheme: soundTheme)); } @@ -47,15 +46,15 @@ class GeneralPreferences extends _$GeneralPreferences await save(state.copyWith(systemColors: !state.systemColors)); if (state.systemColors == false) { final boardTheme = ref.read(boardPreferencesProvider).boardTheme; - if (boardTheme == BoardTheme.system) { + if (boardTheme == pref.BoardTheme.system) { await ref .read(boardPreferencesProvider.notifier) - .setBoardTheme(BoardTheme.brown); + .setBoardTheme(pref.BoardTheme.brown); } } else { await ref .read(boardPreferencesProvider.notifier) - .setBoardTheme(BoardTheme.system); + .setBoardTheme(pref.BoardTheme.system); } } } diff --git a/lib/src/model/settings/over_the_board_preferences.dart b/lib/src/model/settings/over_the_board_preferences.dart index 9eb24c0ed7..8be6edfffe 100644 --- a/lib/src/model/settings/over_the_board_preferences.dart +++ b/lib/src/model/settings/over_the_board_preferences.dart @@ -1,68 +1,31 @@ -import 'dart:convert'; - -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:lichess_mobile/src/db/shared_preferences.dart'; +import 'package:lichess_mobile/src/model/settings/preferences.dart' as pref; +import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -part 'over_the_board_preferences.freezed.dart'; part 'over_the_board_preferences.g.dart'; -@Riverpod(keepAlive: true) -class OverTheBoardPreferences extends _$OverTheBoardPreferences { - static const String prefKey = 'preferences.board'; +@riverpod +class OverTheBoardPreferences extends _$OverTheBoardPreferences + with PreferencesStorage { + // ignore: avoid_public_notifier_properties + @override + pref.Category get categoryKey => + pref.Category.overTheBoard; @override - OverTheBoardPrefs build() { - final prefs = ref.watch(sharedPreferencesProvider); - final stored = prefs.getString(prefKey); - return stored != null - ? OverTheBoardPrefs.fromJson( - jsonDecode(stored) as Map, - ) - : OverTheBoardPrefs.defaults; + pref.OverTheBoard build() { + return fetch(); } Future toggleFlipPiecesAfterMove() { - return _save( + return save( state.copyWith(flipPiecesAfterMove: !state.flipPiecesAfterMove), ); } Future toggleSymmetricPieces() { - return _save( + return save( state.copyWith(symmetricPieces: !state.symmetricPieces), ); } - - Future _save(OverTheBoardPrefs newState) async { - final prefs = ref.read(sharedPreferencesProvider); - await prefs.setString( - prefKey, - jsonEncode(newState.toJson()), - ); - state = newState; - } -} - -@Freezed(fromJson: true, toJson: true) -class OverTheBoardPrefs with _$OverTheBoardPrefs { - const OverTheBoardPrefs._(); - - const factory OverTheBoardPrefs({ - required bool flipPiecesAfterMove, - required bool symmetricPieces, - }) = _OverTheBoardPrefs; - - static const defaults = OverTheBoardPrefs( - flipPiecesAfterMove: false, - symmetricPieces: false, - ); - - factory OverTheBoardPrefs.fromJson(Map json) { - try { - return _$OverTheBoardPrefsFromJson(json); - } catch (_) { - return defaults; - } - } } diff --git a/lib/src/model/settings/preferences.dart b/lib/src/model/settings/preferences.dart index 2661fcd6cd..3a82d68400 100644 --- a/lib/src/model/settings/preferences.dart +++ b/lib/src/model/settings/preferences.dart @@ -1,6 +1,7 @@ +import 'package:chessground/chessground.dart' as cg; import 'package:flutter/material.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:lichess_mobile/src/model/settings/sound_theme.dart'; +import 'package:lichess_mobile/src/utils/color_palette.dart'; part 'preferences.freezed.dart'; part 'preferences.g.dart'; @@ -18,14 +19,23 @@ abstract class SerializablePreferences { return General.fromJson(json); case Category.home: return Home.fromJson(json); + case Category.board: + return Board.fromJson(json); + case Category.analysis: + return Analysis.fromJson(json); + case Category.overTheBoard: + return OverTheBoard.fromJson(json); } } } -/// A preference category type with its storage key and default values. +/// A preference category with its storage key and default values. enum Category { general('preferences.general', General.defaults), - home('preferences.home', Home.defaults); + home('preferences.home', Home.defaults), + board('preferences.board', Board.defaults), + analysis('preferences.analysis', Analysis.defaults), + overTheBoard('preferences.overTheBoard', OverTheBoard.defaults); const Category(this.storageKey, this.defaults); @@ -33,7 +43,30 @@ enum Category { final T defaults; } -/// General preferences for the app. +Map? _localeToJson(Locale? locale) { + return locale != null + ? { + 'languageCode': locale.languageCode, + 'countryCode': locale.countryCode, + 'scriptCode': locale.scriptCode, + } + : null; +} + +Locale? _localeFromJson(Map? json) { + if (json == null) { + return null; + } + return Locale.fromSubtags( + languageCode: json['languageCode'] as String, + countryCode: json['countryCode'] as String?, + scriptCode: json['scriptCode'] as String?, + ); +} + +/// Preferences models +////////////////////// + @Freezed(fromJson: true, toJson: true) class General with _$General implements SerializablePreferences { const factory General({ @@ -64,25 +97,17 @@ class General with _$General implements SerializablePreferences { } } -Map? _localeToJson(Locale? locale) { - return locale != null - ? { - 'languageCode': locale.languageCode, - 'countryCode': locale.countryCode, - 'scriptCode': locale.scriptCode, - } - : null; -} +enum SoundTheme { + standard('Standard'), + piano('Piano'), + nes('NES'), + sfx('SFX'), + futuristic('Futuristic'), + lisp('Lisp'); -Locale? _localeFromJson(Map? json) { - if (json == null) { - return null; - } - return Locale.fromSubtags( - languageCode: json['languageCode'] as String, - countryCode: json['countryCode'] as String?, - scriptCode: json['scriptCode'] as String?, - ); + final String label; + + const SoundTheme(this.label); } enum EnabledWidget { @@ -113,3 +138,260 @@ class Home with _$Home implements SerializablePreferences { } } } + +@Freezed(fromJson: true, toJson: true) +class Board with _$Board implements SerializablePreferences { + const Board._(); + + const factory Board({ + required cg.PieceSet pieceSet, + required BoardTheme boardTheme, + bool? immersiveModeWhilePlaying, + required bool hapticFeedback, + required bool showLegalMoves, + required bool boardHighlights, + required bool coordinates, + required bool pieceAnimation, + required bool showMaterialDifference, + @JsonKey( + defaultValue: cg.PieceShiftMethod.either, + unknownEnumValue: cg.PieceShiftMethod.either, + ) + required cg.PieceShiftMethod pieceShiftMethod, + + /// Whether to enable shape drawings on the board for games and puzzles. + @JsonKey(defaultValue: true) required bool enableShapeDrawings, + @JsonKey(defaultValue: true) required bool magnifyDraggedPiece, + @JsonKey( + defaultValue: ShapeColor.green, + unknownEnumValue: ShapeColor.green, + ) + required ShapeColor shapeColor, + }) = _Board; + + static const defaults = Board( + pieceSet: cg.PieceSet.staunty, + boardTheme: BoardTheme.brown, + immersiveModeWhilePlaying: false, + hapticFeedback: true, + showLegalMoves: true, + boardHighlights: true, + coordinates: true, + pieceAnimation: true, + showMaterialDifference: true, + pieceShiftMethod: cg.PieceShiftMethod.either, + enableShapeDrawings: true, + magnifyDraggedPiece: true, + shapeColor: ShapeColor.green, + ); + + cg.ChessboardSettings toBoardSettings() { + return cg.ChessboardSettings( + pieceAssets: pieceSet.assets, + colorScheme: boardTheme.colors, + showValidMoves: showLegalMoves, + showLastMove: boardHighlights, + enableCoordinates: coordinates, + animationDuration: pieceAnimationDuration, + dragFeedbackScale: magnifyDraggedPiece ? 2.0 : 1.0, + dragFeedbackOffset: Offset(0.0, magnifyDraggedPiece ? -1.0 : 0.0), + pieceShiftMethod: pieceShiftMethod, + drawShape: cg.DrawShapeOptions( + enable: enableShapeDrawings, + newShapeColor: shapeColor.color, + ), + ); + } + + factory Board.fromJson(Map json) { + try { + return _$BoardFromJson(json); + } catch (_) { + return defaults; + } + } + + Duration get pieceAnimationDuration => + pieceAnimation ? const Duration(milliseconds: 150) : Duration.zero; +} + +/// Colors taken from lila: https://github.com/lichess-org/chessground/blob/54a7e71bf88701c1109d3b9b8106b464012b94cf/src/state.ts#L178 +enum ShapeColor { + green, + red, + blue, + yellow; + + Color get color => Color( + switch (this) { + ShapeColor.green => 0x15781B, + ShapeColor.red => 0x882020, + ShapeColor.blue => 0x003088, + ShapeColor.yellow => 0xe68f00, + }, + ).withAlpha(0xAA); +} + +/// The chessboard theme. +enum BoardTheme { + system('System'), + blue('Blue'), + blue2('Blue2'), + blue3('Blue3'), + blueMarble('Blue Marble'), + canvas('Canvas'), + wood('Wood'), + wood2('Wood2'), + wood3('Wood3'), + wood4('Wood4'), + maple('Maple'), + maple2('Maple 2'), + brown('Brown'), + leather('Leather'), + green('Green'), + marble('Marble'), + greenPlastic('Green Plastic'), + grey('Grey'), + metal('Metal'), + olive('Olive'), + newspaper('Newspaper'), + purpleDiag('Purple-Diag'), + pinkPyramid('Pink'), + horsey('Horsey'); + + final String label; + + const BoardTheme(this.label); + + cg.ChessboardColorScheme get colors { + switch (this) { + case BoardTheme.system: + return getBoardColorScheme() ?? cg.ChessboardColorScheme.brown; + case BoardTheme.blue: + return cg.ChessboardColorScheme.blue; + case BoardTheme.blue2: + return cg.ChessboardColorScheme.blue2; + case BoardTheme.blue3: + return cg.ChessboardColorScheme.blue3; + case BoardTheme.blueMarble: + return cg.ChessboardColorScheme.blueMarble; + case BoardTheme.canvas: + return cg.ChessboardColorScheme.canvas; + case BoardTheme.wood: + return cg.ChessboardColorScheme.wood; + case BoardTheme.wood2: + return cg.ChessboardColorScheme.wood2; + case BoardTheme.wood3: + return cg.ChessboardColorScheme.wood3; + case BoardTheme.wood4: + return cg.ChessboardColorScheme.wood4; + case BoardTheme.maple: + return cg.ChessboardColorScheme.maple; + case BoardTheme.maple2: + return cg.ChessboardColorScheme.maple2; + case BoardTheme.brown: + return cg.ChessboardColorScheme.brown; + case BoardTheme.leather: + return cg.ChessboardColorScheme.leather; + case BoardTheme.green: + return cg.ChessboardColorScheme.green; + case BoardTheme.marble: + return cg.ChessboardColorScheme.marble; + case BoardTheme.greenPlastic: + return cg.ChessboardColorScheme.greenPlastic; + case BoardTheme.grey: + return cg.ChessboardColorScheme.grey; + case BoardTheme.metal: + return cg.ChessboardColorScheme.metal; + case BoardTheme.olive: + return cg.ChessboardColorScheme.olive; + case BoardTheme.newspaper: + return cg.ChessboardColorScheme.newspaper; + case BoardTheme.purpleDiag: + return cg.ChessboardColorScheme.purpleDiag; + case BoardTheme.pinkPyramid: + return cg.ChessboardColorScheme.pinkPyramid; + case BoardTheme.horsey: + return cg.ChessboardColorScheme.horsey; + } + } + + Widget get thumbnail => this == BoardTheme.system + ? SizedBox( + height: 44, + width: 44 * 6, + child: Row( + children: [ + for (final c in const [1, 2, 3, 4, 5, 6]) + Container( + width: 44, + color: c.isEven + ? BoardTheme.system.colors.darkSquare + : BoardTheme.system.colors.lightSquare, + ), + ], + ), + ) + : Image.asset( + 'assets/board-thumbnails/$name.jpg', + height: 44, + errorBuilder: (context, o, st) => const SizedBox.shrink(), + ); +} + +@Freezed(fromJson: true, toJson: true) +class Analysis with _$Analysis implements SerializablePreferences { + const Analysis._(); + + const factory Analysis({ + required bool enableLocalEvaluation, + required bool showEvaluationGauge, + required bool showBestMoveArrow, + required bool showAnnotations, + required bool showPgnComments, + @Assert('numEvalLines >= 1 && numEvalLines <= 3') required int numEvalLines, + @Assert('numEngineCores >= 1 && numEngineCores <= maxEngineCores') + required int numEngineCores, + }) = _Analysis; + + static const defaults = Analysis( + enableLocalEvaluation: true, + showEvaluationGauge: true, + showBestMoveArrow: true, + showAnnotations: true, + showPgnComments: true, + numEvalLines: 2, + numEngineCores: 1, + ); + + factory Analysis.fromJson(Map json) { + try { + return _$AnalysisFromJson(json); + } catch (_) { + return defaults; + } + } +} + +@Freezed(fromJson: true, toJson: true) +class OverTheBoard with _$OverTheBoard implements SerializablePreferences { + const OverTheBoard._(); + + const factory OverTheBoard({ + required bool flipPiecesAfterMove, + required bool symmetricPieces, + }) = _OverTheBoard; + + static const defaults = OverTheBoard( + flipPiecesAfterMove: false, + symmetricPieces: false, + ); + + factory OverTheBoard.fromJson(Map json) { + try { + return _$OverTheBoardFromJson(json); + } catch (_) { + return defaults; + } + } +} diff --git a/lib/src/model/settings/preferences_storage.dart b/lib/src/model/settings/preferences_storage.dart index 6dc71fbc79..dc2668fddd 100644 --- a/lib/src/model/settings/preferences_storage.dart +++ b/lib/src/model/settings/preferences_storage.dart @@ -29,10 +29,14 @@ mixin PreferencesStorage { if (stored == null) { return categoryKey.defaults; } - return SerializablePreferences.fromJson( - categoryKey, - jsonDecode(stored) as Map, - ) as T; + try { + return SerializablePreferences.fromJson( + categoryKey, + jsonDecode(stored) as Map, + ) as T; + } catch (e) { + return categoryKey.defaults; + } } } @@ -65,10 +69,14 @@ mixin SessionPreferencesStorage { if (stored == null) { return categoryKey.defaults; } - return SerializablePreferences.fromJson( - categoryKey, - jsonDecode(stored) as Map, - ) as T; + try { + return SerializablePreferences.fromJson( + categoryKey, + jsonDecode(stored) as Map, + ) as T; + } catch (e) { + return categoryKey.defaults; + } } String _prefKey(String key, AuthSessionState? session) => diff --git a/lib/src/model/settings/sound_theme.dart b/lib/src/model/settings/sound_theme.dart deleted file mode 100644 index 53b5ddd252..0000000000 --- a/lib/src/model/settings/sound_theme.dart +++ /dev/null @@ -1,12 +0,0 @@ -enum SoundTheme { - standard('Standard'), - piano('Piano'), - nes('NES'), - sfx('SFX'), - futuristic('Futuristic'), - lisp('Lisp'); - - final String label; - - const SoundTheme(this.label); -} diff --git a/lib/src/view/settings/board_theme_screen.dart b/lib/src/view/settings/board_theme_screen.dart index c005919905..1b48b80f9f 100644 --- a/lib/src/view/settings/board_theme_screen.dart +++ b/lib/src/view/settings/board_theme_screen.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; +import 'package:lichess_mobile/src/model/settings/preferences.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/system.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; diff --git a/lib/src/view/settings/sound_settings_screen.dart b/lib/src/view/settings/sound_settings_screen.dart index 03737c6d54..cd014e2a7e 100644 --- a/lib/src/view/settings/sound_settings_screen.dart +++ b/lib/src/view/settings/sound_settings_screen.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; -import 'package:lichess_mobile/src/model/settings/sound_theme.dart'; +import 'package:lichess_mobile/src/model/settings/preferences.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; diff --git a/lib/src/view/settings/theme_screen.dart b/lib/src/view/settings/theme_screen.dart index f3b4b79ea3..a85fdad0bf 100644 --- a/lib/src/view/settings/theme_screen.dart +++ b/lib/src/view/settings/theme_screen.dart @@ -7,6 +7,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; +import 'package:lichess_mobile/src/model/settings/preferences.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; diff --git a/test/model/common/service/fake_sound_service.dart b/test/model/common/service/fake_sound_service.dart index 9dd58149c8..ef6c74f130 100644 --- a/test/model/common/service/fake_sound_service.dart +++ b/test/model/common/service/fake_sound_service.dart @@ -1,5 +1,5 @@ import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; -import 'package:lichess_mobile/src/model/settings/sound_theme.dart'; +import 'package:lichess_mobile/src/model/settings/preferences.dart'; class FakeSoundService implements SoundService { @override diff --git a/test/test_provider_scope.dart b/test/test_provider_scope.dart index 791ba768cc..5854f7d834 100644 --- a/test/test_provider_scope.dart +++ b/test/test_provider_scope.dart @@ -20,7 +20,7 @@ import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/notifications/notification_service.dart'; -import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; +import 'package:lichess_mobile/src/model/settings/preferences.dart'; import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:logging/logging.dart'; import 'package:package_info_plus/package_info_plus.dart'; @@ -103,7 +103,7 @@ Future makeProviderScope( { // disable piece animation to simplify tests 'preferences.board': jsonEncode( - BoardPrefs.defaults + Board.defaults .copyWith( pieceAnimation: false, ) From 6c817e83eda8bc2c38622c4c7d3b20e3ac6c1d59 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Tue, 1 Oct 2024 21:30:17 +0200 Subject: [PATCH 408/979] refactor: make EngineLines independent of AnalysisController --- lib/src/view/analysis/analysis_screen.dart | 178 ++------------------- lib/src/view/engine/engine_lines.dart | 174 ++++++++++++++++++++ 2 files changed, 190 insertions(+), 162 deletions(-) create mode 100644 lib/src/view/engine/engine_lines.dart diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index d49ba403b4..771d560f3d 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -8,20 +8,17 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/constants.dart'; -import 'package:lichess_mobile/src/model/account/account_preferences.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; import 'package:lichess_mobile/src/model/analysis/server_analysis_service.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; -import 'package:lichess_mobile/src/model/common/eval.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/engine/engine.dart'; import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; import 'package:lichess_mobile/src/model/game/game_repository_providers.dart'; import 'package:lichess_mobile/src/model/game/game_share_service.dart'; -import 'package:lichess_mobile/src/model/settings/brightness.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/connectivity.dart'; @@ -32,6 +29,7 @@ import 'package:lichess_mobile/src/utils/string.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_share_screen.dart'; import 'package:lichess_mobile/src/view/board_editor/board_editor_screen.dart'; import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; +import 'package:lichess_mobile/src/view/engine/engine_lines.dart'; import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_screen.dart'; import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; @@ -229,6 +227,10 @@ class _Body extends ConsumerWidget { final displayMode = ref.watch(ctrlProvider.select((value) => value.displayMode)); + final currentNode = ref.watch( + ctrlProvider.select((value) => value.currentNode), + ); + return Column( children: [ Expanded( @@ -293,8 +295,11 @@ class _Body extends ConsumerWidget { mainAxisAlignment: MainAxisAlignment.start, children: [ if (isEngineAvailable) - _EngineLines( - ctrlProvider, + EngineLines( + onTapMove: + ref.read(ctrlProvider.notifier).onUserMove, + clientEval: currentNode.eval, + isGameOver: currentNode.position.isGameOver, isLandscape: true, ), Expanded( @@ -411,169 +416,18 @@ class _ColumnTopTable extends ConsumerWidget { params: analysisState.engineGaugeParams, ), if (analysisState.isEngineAvailable) - _EngineLines(ctrlProvider, isLandscape: false), + EngineLines( + clientEval: analysisState.currentNode.eval, + isGameOver: analysisState.currentNode.position.isGameOver, + onTapMove: ref.read(ctrlProvider.notifier).onUserMove, + isLandscape: false, + ), ], ) : kEmptyWidget; } } -class _EngineLines extends ConsumerWidget { - const _EngineLines(this.ctrlProvider, {required this.isLandscape}); - final AnalysisControllerProvider ctrlProvider; - final bool isLandscape; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final analysisState = ref.watch(ctrlProvider); - final numEvalLines = ref.watch( - analysisPreferencesProvider.select( - (p) => p.numEvalLines, - ), - ); - final engineEval = ref.watch(engineEvaluationProvider).eval; - final eval = engineEval ?? analysisState.currentNode.eval; - - final emptyLines = List.filled( - numEvalLines, - _Engineline.empty(ctrlProvider), - ); - - final content = !analysisState.position.isGameOver - ? (eval != null - ? eval.pvs - .take(numEvalLines) - .map( - (pv) => _Engineline(ctrlProvider, eval.position, pv), - ) - .toList() - : emptyLines) - : emptyLines; - - if (content.length < numEvalLines) { - final padding = List.filled( - numEvalLines - content.length, - _Engineline.empty(ctrlProvider), - ); - content.addAll(padding); - } - - return Padding( - padding: EdgeInsets.symmetric( - vertical: isLandscape ? kTabletBoardTableSidePadding : 0.0, - horizontal: isLandscape ? kTabletBoardTableSidePadding : 0.0, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: content, - ), - ); - } -} - -class _Engineline extends ConsumerWidget { - const _Engineline( - this.ctrlProvider, - this.fromPosition, - this.pvData, - ); - - const _Engineline.empty(this.ctrlProvider) - : pvData = const PvData(moves: IListConst([])), - fromPosition = Chess.initial; - - final AnalysisControllerProvider ctrlProvider; - final Position fromPosition; - final PvData pvData; - - @override - Widget build(BuildContext context, WidgetRef ref) { - if (pvData.moves.isEmpty) { - return const SizedBox( - height: kEvalGaugeSize, - child: SizedBox.shrink(), - ); - } - - final pieceNotation = ref.watch(pieceNotationProvider).maybeWhen( - data: (value) => value, - orElse: () => defaultAccountPreferences.pieceNotation, - ); - - final lineBuffer = StringBuffer(); - int ply = fromPosition.ply + 1; - pvData.sanMoves(fromPosition).forEachIndexed((i, s) { - lineBuffer.write( - ply.isOdd - ? '${(ply / 2).ceil()}. $s ' - : i == 0 - ? '${(ply / 2).ceil()}... $s ' - : '$s ', - ); - ply += 1; - }); - - final brightness = ref.watch(currentBrightnessProvider); - - final evalString = pvData.evalString; - return AdaptiveInkWell( - onTap: () => ref - .read(ctrlProvider.notifier) - .onUserMove(NormalMove.fromUci(pvData.moves[0])), - child: SizedBox( - height: kEvalGaugeSize, - child: Padding( - padding: const EdgeInsets.all(2.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Container( - decoration: BoxDecoration( - color: pvData.winningSide == Side.black - ? EngineGauge.backgroundColor(context, brightness) - : EngineGauge.valueColor(context, brightness), - borderRadius: BorderRadius.circular(4.0), - ), - padding: const EdgeInsets.symmetric( - horizontal: 4.0, - vertical: 2.0, - ), - child: Text( - evalString, - style: TextStyle( - color: pvData.winningSide == Side.black - ? Colors.white - : Colors.black, - fontSize: kEvalGaugeFontSize, - fontWeight: FontWeight.w600, - ), - ), - ), - const SizedBox(width: 8.0), - Expanded( - child: Text( - lineBuffer.toString(), - maxLines: 1, - softWrap: false, - style: TextStyle( - fontFamily: pieceNotation == PieceNotation.symbol - ? 'ChessFont' - : null, - ), - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ), - ), - ); - } -} - class _BottomBar extends ConsumerWidget { const _BottomBar({ required this.pgn, diff --git a/lib/src/view/engine/engine_lines.dart b/lib/src/view/engine/engine_lines.dart new file mode 100644 index 0000000000..cb6e23c25f --- /dev/null +++ b/lib/src/view/engine/engine_lines.dart @@ -0,0 +1,174 @@ +import 'package:collection/collection.dart'; +import 'package:dartchess/dartchess.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/constants.dart'; +import 'package:lichess_mobile/src/model/account/account_preferences.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; +import 'package:lichess_mobile/src/model/common/eval.dart'; +import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; +import 'package:lichess_mobile/src/model/settings/brightness.dart'; +import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; + +class EngineLines extends ConsumerWidget { + const EngineLines({ + required this.onTapMove, + required this.clientEval, + required this.isGameOver, + required this.isLandscape, + }); + final void Function(NormalMove move) onTapMove; + final ClientEval? clientEval; + final bool isGameOver; + final bool isLandscape; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final numEvalLines = ref.watch( + analysisPreferencesProvider.select( + (p) => p.numEvalLines, + ), + ); + final engineEval = ref.watch(engineEvaluationProvider).eval; + final eval = engineEval ?? clientEval; + + final emptyLines = List.filled( + numEvalLines, + const Engineline.empty(), + ); + + final content = isGameOver + ? emptyLines + : (eval != null + ? eval.pvs + .take(numEvalLines) + .map( + (pv) => Engineline(onTapMove, eval.position, pv), + ) + .toList() + : emptyLines); + + if (content.length < numEvalLines) { + final padding = List.filled( + numEvalLines - content.length, + const Engineline.empty(), + ); + content.addAll(padding); + } + + return Padding( + padding: EdgeInsets.symmetric( + vertical: isLandscape ? kTabletBoardTableSidePadding : 0.0, + horizontal: isLandscape ? kTabletBoardTableSidePadding : 0.0, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: content, + ), + ); + } +} + +class Engineline extends ConsumerWidget { + const Engineline( + this.onTapMove, + this.fromPosition, + this.pvData, + ); + + const Engineline.empty() + : onTapMove = null, + pvData = const PvData(moves: IListConst([])), + fromPosition = Chess.initial; + + final void Function(NormalMove move)? onTapMove; + final Position fromPosition; + final PvData pvData; + + @override + Widget build(BuildContext context, WidgetRef ref) { + if (pvData.moves.isEmpty) { + return const SizedBox( + height: kEvalGaugeSize, + child: SizedBox.shrink(), + ); + } + + final pieceNotation = ref.watch(pieceNotationProvider).maybeWhen( + data: (value) => value, + orElse: () => defaultAccountPreferences.pieceNotation, + ); + + final lineBuffer = StringBuffer(); + int ply = fromPosition.ply + 1; + pvData.sanMoves(fromPosition).forEachIndexed((i, s) { + lineBuffer.write( + ply.isOdd + ? '${(ply / 2).ceil()}. $s ' + : i == 0 + ? '${(ply / 2).ceil()}... $s ' + : '$s ', + ); + ply += 1; + }); + + final brightness = ref.watch(currentBrightnessProvider); + + final evalString = pvData.evalString; + return AdaptiveInkWell( + onTap: () => onTapMove?.call(NormalMove.fromUci(pvData.moves[0])), + child: SizedBox( + height: kEvalGaugeSize, + child: Padding( + padding: const EdgeInsets.all(2.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + decoration: BoxDecoration( + color: pvData.winningSide == Side.black + ? EngineGauge.backgroundColor(context, brightness) + : EngineGauge.valueColor(context, brightness), + borderRadius: BorderRadius.circular(4.0), + ), + padding: const EdgeInsets.symmetric( + horizontal: 4.0, + vertical: 2.0, + ), + child: Text( + evalString, + style: TextStyle( + color: pvData.winningSide == Side.black + ? Colors.white + : Colors.black, + fontSize: kEvalGaugeFontSize, + fontWeight: FontWeight.w600, + ), + ), + ), + const SizedBox(width: 8.0), + Expanded( + child: Text( + lineBuffer.toString(), + maxLines: 1, + softWrap: false, + style: TextStyle( + fontFamily: pieceNotation == PieceNotation.symbol + ? 'ChessFont' + : null, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ), + ); + } +} From 8755d5781213302f05ee401c23983b96d44c977e Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 2 Oct 2024 10:29:55 +0200 Subject: [PATCH 409/979] More wip on refactoring preferences --- .../model/analysis/analysis_preferences.dart | 2 +- lib/src/model/challenge/challenge.dart | 13 +- .../challenge/challenge_preferences.dart | 108 +----- lib/src/model/common/game.dart | 14 + .../coordinate_training_preferences.dart | 129 +------ lib/src/model/game/game_preferences.dart | 52 +-- lib/src/model/lobby/game_seek.dart | 11 +- lib/src/model/lobby/game_setup.dart | 262 ------------- .../model/lobby/game_setup_preferences.dart | 163 ++++++++ .../opening_explorer/opening_explorer.dart | 8 +- .../opening_explorer_preferences.dart | 160 +++----- lib/src/model/puzzle/puzzle_controller.dart | 6 +- lib/src/model/puzzle/puzzle_preferences.dart | 59 +-- lib/src/model/puzzle/puzzle_service.dart | 2 +- lib/src/model/settings/board_preferences.dart | 2 +- .../model/settings/general_preferences.dart | 2 +- lib/src/model/settings/home_preferences.dart | 2 +- .../settings/over_the_board_preferences.dart | 2 +- lib/src/model/settings/preferences.dart | 361 +++++++++++++++--- .../model/settings/preferences_storage.dart | 36 +- lib/src/view/clock/custom_clock_settings.dart | 2 +- .../coordinate_training_screen.dart | 6 +- lib/src/view/game/game_screen.dart | 2 +- .../opening_explorer_settings.dart | 26 +- .../configure_over_the_board_game.dart | 2 +- lib/src/view/play/challenge_list_item.dart | 1 + lib/src/view/play/common_play_widgets.dart | 2 +- .../view/play/create_challenge_screen.dart | 3 +- .../view/play/create_custom_game_screen.dart | 16 +- lib/src/view/play/quick_game_button.dart | 2 +- lib/src/view/play/time_control_modal.dart | 2 +- lib/src/view/puzzle/puzzle_screen.dart | 3 +- .../view/puzzle/puzzle_settings_screen.dart | 9 +- .../challenge/challenge_service_test.dart | 1 + test/test_provider_scope.dart | 14 + .../coordinate_training_screen_test.dart | 1 + .../opening_explorer_screen_test.dart | 32 +- 37 files changed, 720 insertions(+), 798 deletions(-) create mode 100644 lib/src/model/common/game.dart delete mode 100644 lib/src/model/lobby/game_setup.dart create mode 100644 lib/src/model/lobby/game_setup_preferences.dart diff --git a/lib/src/model/analysis/analysis_preferences.dart b/lib/src/model/analysis/analysis_preferences.dart index bd0463ea73..3075981a74 100644 --- a/lib/src/model/analysis/analysis_preferences.dart +++ b/lib/src/model/analysis/analysis_preferences.dart @@ -10,7 +10,7 @@ class AnalysisPreferences extends _$AnalysisPreferences with PreferencesStorage { // ignore: avoid_public_notifier_properties @override - pref.Category get categoryKey => pref.Category.analysis; + final prefCategory = pref.Category.analysis; @override pref.Analysis build() { diff --git a/lib/src/model/challenge/challenge.dart b/lib/src/model/challenge/challenge.dart index 4ea9003933..2c03c1334e 100644 --- a/lib/src/model/challenge/challenge.dart +++ b/lib/src/model/challenge/challenge.dart @@ -2,6 +2,7 @@ import 'package:deep_pick/deep_pick.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/l10n/l10n.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/common/game.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; import 'package:lichess_mobile/src/model/common/speed.dart'; @@ -205,18 +206,6 @@ typedef ChallengeUser = ({ int? lagRating, }); -enum SideChoice { - random, - white, - black; - - String label(AppLocalizations l10n) => switch (this) { - SideChoice.random => l10n.randomColor, - SideChoice.white => l10n.white, - SideChoice.black => l10n.black, - }; -} - extension ChallengeExtension on Pick { ChallengeDirection asChallengeDirectionOrThrow() { final value = this.required().value; diff --git a/lib/src/model/challenge/challenge_preferences.dart b/lib/src/model/challenge/challenge_preferences.dart index e0abd038e1..9522de377b 100644 --- a/lib/src/model/challenge/challenge_preferences.dart +++ b/lib/src/model/challenge/challenge_preferences.dart @@ -1,119 +1,45 @@ -import 'dart:convert'; - -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:lichess_mobile/src/db/shared_preferences.dart'; -import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/challenge/challenge.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; -import 'package:lichess_mobile/src/model/common/speed.dart'; -import 'package:lichess_mobile/src/model/common/time_increment.dart'; -import 'package:lichess_mobile/src/model/user/user.dart'; +import 'package:lichess_mobile/src/model/common/game.dart'; +import 'package:lichess_mobile/src/model/settings/preferences.dart' as pref; +import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -part 'challenge_preferences.freezed.dart'; part 'challenge_preferences.g.dart'; -@Riverpod(keepAlive: true) -class ChallengePreferences extends _$ChallengePreferences { - static String _prefKey(AuthSessionState? session) => - 'preferences.game_setup.${session?.user.id ?? '**anon**'}'; +@riverpod +class ChallengePreferences extends _$ChallengePreferences + with SessionPreferencesStorage { + // ignore: avoid_public_notifier_properties + @override + pref.Category get prefCategory => pref.Category.challenge; @override - ChallengePreferencesState build() { - final session = ref.watch(authSessionProvider); - final prefs = ref.watch(sharedPreferencesProvider); - final stored = prefs.getString(_prefKey(session)); - return stored != null - ? ChallengePreferencesState.fromJson( - jsonDecode(stored) as Map, - ) - : ChallengePreferencesState.defaults; + pref.Challenge build() { + return fetch(); } Future setVariant(Variant variant) { - return _save(state.copyWith(variant: variant)); + return save(state.copyWith(variant: variant)); } Future setTimeControl(ChallengeTimeControlType timeControl) { - return _save(state.copyWith(timeControl: timeControl)); + return save(state.copyWith(timeControl: timeControl)); } Future setClock(Duration time, Duration increment) { - return _save(state.copyWith(clock: (time: time, increment: increment))); + return save(state.copyWith(clock: (time: time, increment: increment))); } Future setDays(int days) { - return _save(state.copyWith(days: days)); + return save(state.copyWith(days: days)); } Future setSideChoice(SideChoice sideChoice) { - return _save(state.copyWith(sideChoice: sideChoice)); + return save(state.copyWith(sideChoice: sideChoice)); } Future setRated(bool rated) { - return _save(state.copyWith(rated: rated)); - } - - Future _save(ChallengePreferencesState newState) async { - final prefs = ref.read(sharedPreferencesProvider); - final session = ref.read(authSessionProvider); - await prefs.setString( - _prefKey(session), - jsonEncode(newState.toJson()), - ); - state = newState; - } -} - -@Freezed(fromJson: true, toJson: true) -class ChallengePreferencesState with _$ChallengePreferencesState { - const ChallengePreferencesState._(); - - const factory ChallengePreferencesState({ - required Variant variant, - required ChallengeTimeControlType timeControl, - required ({Duration time, Duration increment}) clock, - required int days, - required bool rated, - required SideChoice sideChoice, - }) = _ChallengeSetup; - - static const defaults = ChallengePreferencesState( - variant: Variant.standard, - timeControl: ChallengeTimeControlType.clock, - clock: (time: Duration(minutes: 10), increment: Duration.zero), - days: 3, - rated: false, - sideChoice: SideChoice.random, - ); - - Speed get speed => timeControl == ChallengeTimeControlType.clock - ? Speed.fromTimeIncrement( - TimeIncrement( - clock.time.inSeconds, - clock.increment.inSeconds, - ), - ) - : Speed.correspondence; - - ChallengeRequest makeRequest(LightUser destUser, [String? initialFen]) { - return ChallengeRequest( - destUser: destUser, - variant: variant, - timeControl: timeControl, - clock: clock, - days: days, - rated: rated, - sideChoice: sideChoice, - initialFen: initialFen, - ); - } - - factory ChallengePreferencesState.fromJson(Map json) { - try { - return _$ChallengePreferencesStateFromJson(json); - } catch (_) { - return ChallengePreferencesState.defaults; - } + return save(state.copyWith(rated: rated)); } } diff --git a/lib/src/model/common/game.dart b/lib/src/model/common/game.dart new file mode 100644 index 0000000000..14a4bd9950 --- /dev/null +++ b/lib/src/model/common/game.dart @@ -0,0 +1,14 @@ +import 'package:lichess_mobile/l10n/l10n.dart'; + +/// Represents the choice of a side as a player: white, black or random. +enum SideChoice { + random, + white, + black; + + String label(AppLocalizations l10n) => switch (this) { + SideChoice.random => l10n.randomColor, + SideChoice.white => l10n.white, + SideChoice.black => l10n.black, + }; +} diff --git a/lib/src/model/coordinate_training/coordinate_training_preferences.dart b/lib/src/model/coordinate_training/coordinate_training_preferences.dart index fbc5b11725..fa04ab9387 100644 --- a/lib/src/model/coordinate_training/coordinate_training_preferences.dart +++ b/lib/src/model/coordinate_training/coordinate_training_preferences.dart @@ -1,136 +1,39 @@ -import 'dart:convert'; - -import 'package:flutter/material.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:lichess_mobile/src/db/shared_preferences.dart'; -import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/model/common/game.dart'; +import 'package:lichess_mobile/src/model/settings/preferences.dart'; +import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -part 'coordinate_training_preferences.freezed.dart'; part 'coordinate_training_preferences.g.dart'; -enum SideChoice { - white, - random, - black, -} - -String sideChoiceL10n(BuildContext context, SideChoice side) { - switch (side) { - case SideChoice.white: - return context.l10n.white; - case SideChoice.black: - return context.l10n.black; - case SideChoice.random: - // TODO l10n - return 'Random'; - } -} - -enum TimeChoice { - thirtySeconds(Duration(seconds: 30)), - unlimited(null); - - const TimeChoice(this.duration); - - final Duration? duration; -} - -// TODO l10n -Widget timeChoiceL10n(BuildContext context, TimeChoice time) { - switch (time) { - case TimeChoice.thirtySeconds: - return const Text('30s'); - case TimeChoice.unlimited: - return const Icon(Icons.all_inclusive); - } -} - -enum TrainingMode { - findSquare, - nameSquare, -} - -// TODO l10n -String trainingModeL10n(BuildContext context, TrainingMode mode) { - switch (mode) { - case TrainingMode.findSquare: - return 'Find Square'; - case TrainingMode.nameSquare: - return 'Name Square'; - } -} - -@Riverpod(keepAlive: true) -class CoordinateTrainingPreferences extends _$CoordinateTrainingPreferences { - static const String prefKey = 'preferences.coordinate_training'; +@riverpod +class CoordinateTrainingPreferences extends _$CoordinateTrainingPreferences + with PreferencesStorage { + // ignore: avoid_public_notifier_properties + @override + final Category prefCategory = Category.coordinateTraining; @override - CoordinateTrainingPrefs build() { - final prefs = ref.watch(sharedPreferencesProvider); - final stored = prefs.getString(prefKey); - return stored != null - ? CoordinateTrainingPrefs.fromJson( - jsonDecode(stored) as Map, - ) - : CoordinateTrainingPrefs.defaults; + CoordinateTraining build() { + return fetch(); } Future setShowCoordinates(bool showCoordinates) { - return _save(state.copyWith(showCoordinates: showCoordinates)); + return save(state.copyWith(showCoordinates: showCoordinates)); } Future setShowPieces(bool showPieces) { - return _save(state.copyWith(showPieces: showPieces)); + return save(state.copyWith(showPieces: showPieces)); } Future setMode(TrainingMode mode) { - return _save(state.copyWith(mode: mode)); + return save(state.copyWith(mode: mode)); } Future setTimeChoice(TimeChoice timeChoice) { - return _save(state.copyWith(timeChoice: timeChoice)); + return save(state.copyWith(timeChoice: timeChoice)); } Future setSideChoice(SideChoice sideChoice) { - return _save(state.copyWith(sideChoice: sideChoice)); - } - - Future _save(CoordinateTrainingPrefs newState) async { - final prefs = ref.read(sharedPreferencesProvider); - await prefs.setString( - prefKey, - jsonEncode(newState.toJson()), - ); - state = newState; - } -} - -@Freezed(fromJson: true, toJson: true) -class CoordinateTrainingPrefs with _$CoordinateTrainingPrefs { - const CoordinateTrainingPrefs._(); - - const factory CoordinateTrainingPrefs({ - required bool showCoordinates, - required bool showPieces, - required TrainingMode mode, - required TimeChoice timeChoice, - required SideChoice sideChoice, - }) = _CoordinateTrainingPrefs; - - static const defaults = CoordinateTrainingPrefs( - showCoordinates: false, - showPieces: true, - mode: TrainingMode.findSquare, - timeChoice: TimeChoice.thirtySeconds, - sideChoice: SideChoice.random, - ); - - factory CoordinateTrainingPrefs.fromJson(Map json) { - try { - return _$CoordinateTrainingPrefsFromJson(json); - } catch (_) { - return defaults; - } + return save(state.copyWith(sideChoice: sideChoice)); } } diff --git a/lib/src/model/game/game_preferences.dart b/lib/src/model/game/game_preferences.dart index 1f843172fe..f4fcb335d3 100644 --- a/lib/src/model/game/game_preferences.dart +++ b/lib/src/model/game/game_preferences.dart @@ -1,58 +1,30 @@ -import 'dart:convert'; - -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:lichess_mobile/src/db/shared_preferences.dart'; +import 'package:lichess_mobile/src/model/settings/preferences.dart' as pref; +import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -part 'game_preferences.freezed.dart'; part 'game_preferences.g.dart'; -const _prefKey = 'game.preferences'; - /// Local game preferences, defined client-side only. @riverpod -class GamePreferences extends _$GamePreferences { +class GamePreferences extends _$GamePreferences + with PreferencesStorage { + // ignore: avoid_public_notifier_properties + @override + pref.Category get prefCategory => pref.Category.game; + @override - GamePrefState build() { - final prefs = ref.watch(sharedPreferencesProvider); - final stored = prefs.getString(_prefKey); - return stored != null - ? GamePrefState.fromJson(jsonDecode(stored) as Map) - : GamePrefState.defaults; + pref.Game build() { + return fetch(); } Future toggleChat() { final newState = state.copyWith(enableChat: !(state.enableChat ?? false)); - return _save(newState); + return save(newState); } Future toggleBlindfoldMode() { - return _save( + return save( state.copyWith(blindfoldMode: !(state.blindfoldMode ?? false)), ); } - - Future _save(GamePrefState newState) async { - final prefs = ref.read(sharedPreferencesProvider); - await prefs.setString( - _prefKey, - jsonEncode(newState.toJson()), - ); - state = newState; - } -} - -@Freezed(fromJson: true, toJson: true) -class GamePrefState with _$GamePrefState { - const factory GamePrefState({ - bool? enableChat, - bool? blindfoldMode, - }) = _GamePrefState; - - static const defaults = GamePrefState( - enableChat: true, - ); - - factory GamePrefState.fromJson(Map json) => - _$GamePrefStateFromJson(json); } diff --git a/lib/src/model/lobby/game_seek.dart b/lib/src/model/lobby/game_seek.dart index 15b8eb0f5f..4e481ee10f 100644 --- a/lib/src/model/lobby/game_seek.dart +++ b/lib/src/model/lobby/game_seek.dart @@ -4,12 +4,13 @@ import 'package:dartchess/dartchess.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/common/game.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; import 'package:lichess_mobile/src/model/common/speed.dart'; import 'package:lichess_mobile/src/model/common/time_increment.dart'; import 'package:lichess_mobile/src/model/game/game.dart'; import 'package:lichess_mobile/src/model/game/playable_game.dart'; -import 'package:lichess_mobile/src/model/lobby/game_setup.dart'; +import 'package:lichess_mobile/src/model/settings/preferences.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; part 'game_seek.freezed.dart'; @@ -65,9 +66,9 @@ class GameSeek with _$GameSeek { ), rated: account != null && setup.customRated, variant: setup.customVariant, - side: setup.customRated == true || setup.customSide == PlayableSide.random + side: setup.customRated == true || setup.customSide == SideChoice.random ? null - : setup.customSide == PlayableSide.white + : setup.customSide == SideChoice.white ? Side.white : Side.black, ratingRange: @@ -81,9 +82,9 @@ class GameSeek with _$GameSeek { days: setup.customDaysPerTurn, rated: account != null && setup.customRated, variant: setup.customVariant, - side: setup.customRated == true || setup.customSide == PlayableSide.random + side: setup.customRated == true || setup.customSide == SideChoice.random ? null - : setup.customSide == PlayableSide.white + : setup.customSide == SideChoice.white ? Side.white : Side.black, ratingRange: diff --git a/lib/src/model/lobby/game_setup.dart b/lib/src/model/lobby/game_setup.dart deleted file mode 100644 index 53e64062f0..0000000000 --- a/lib/src/model/lobby/game_setup.dart +++ /dev/null @@ -1,262 +0,0 @@ -import 'dart:convert'; -import 'dart:math' as math; - -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:lichess_mobile/l10n/l10n.dart'; -import 'package:lichess_mobile/src/db/shared_preferences.dart'; -import 'package:lichess_mobile/src/model/auth/auth_session.dart'; -import 'package:lichess_mobile/src/model/common/chess.dart'; -import 'package:lichess_mobile/src/model/common/perf.dart'; -import 'package:lichess_mobile/src/model/common/speed.dart'; -import 'package:lichess_mobile/src/model/common/time_increment.dart'; -import 'package:lichess_mobile/src/model/user/user.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'game_setup.freezed.dart'; -part 'game_setup.g.dart'; - -enum PlayableSide { random, white, black } - -String playableSideL10n(AppLocalizations l10n, PlayableSide side) { - switch (side) { - case PlayableSide.white: - return l10n.white; - case PlayableSide.black: - return l10n.black; - case PlayableSide.random: - return l10n.randomColor; - } -} - -enum TimeControl { realTime, correspondence } - -/// Saved custom game setup preferences. -@Freezed(fromJson: true, toJson: true) -class GameSetup with _$GameSetup { - const GameSetup._(); - - const factory GameSetup({ - required TimeIncrement quickPairingTimeIncrement, - required TimeControl customTimeControl, - required int customTimeSeconds, - required int customIncrementSeconds, - required int customDaysPerTurn, - required Variant customVariant, - required bool customRated, - required PlayableSide customSide, - required (int, int) customRatingDelta, - }) = _GameSetup; - - static const defaults = GameSetup( - quickPairingTimeIncrement: TimeIncrement(600, 0), - customTimeControl: TimeControl.realTime, - customTimeSeconds: 180, - customIncrementSeconds: 0, - customVariant: Variant.standard, - customRated: false, - customSide: PlayableSide.random, - customRatingDelta: (-500, 500), - customDaysPerTurn: 3, - ); - - Speed get speedFromCustom => Speed.fromTimeIncrement( - TimeIncrement( - customTimeSeconds, - customIncrementSeconds, - ), - ); - - Perf get perfFromCustom => Perf.fromVariantAndSpeed( - customVariant, - speedFromCustom, - ); - - /// Returns the rating range for the custom setup, or null if the user - /// doesn't have a rating for the custom setup perf. - (int, int)? ratingRangeFromCustom(User user) { - final perf = user.perfs[perfFromCustom]; - if (perf == null) return null; - if (perf.provisional == true) return null; - final min = math.max(0, perf.rating + customRatingDelta.$1); - final max = perf.rating + customRatingDelta.$2; - return (min, max); - } - - factory GameSetup.fromJson(Map json) { - try { - return _$GameSetupFromJson(json); - } catch (_) { - return defaults; - } - } -} - -@Riverpod(keepAlive: true) -class GameSetupPreferences extends _$GameSetupPreferences { - static String _prefKey(AuthSessionState? session) => - 'preferences.game_setup.${session?.user.id ?? '**anon**'}'; - - @override - GameSetup build() { - final session = ref.watch(authSessionProvider); - final prefs = ref.watch(sharedPreferencesProvider); - final stored = prefs.getString(_prefKey(session)); - return stored != null - ? GameSetup.fromJson( - jsonDecode(stored) as Map, - ) - : GameSetup.defaults; - } - - Future setQuickPairingTimeIncrement(TimeIncrement timeInc) { - return _save(state.copyWith(quickPairingTimeIncrement: timeInc)); - } - - Future setCustomTimeControl(TimeControl control) { - return _save(state.copyWith(customTimeControl: control)); - } - - Future setCustomTimeSeconds(int seconds) { - return _save(state.copyWith(customTimeSeconds: seconds)); - } - - Future setCustomIncrementSeconds(int seconds) { - return _save(state.copyWith(customIncrementSeconds: seconds)); - } - - Future setCustomVariant(Variant variant) { - return _save(state.copyWith(customVariant: variant)); - } - - Future setCustomRated(bool rated) { - return _save(state.copyWith(customRated: rated)); - } - - Future setCustomSide(PlayableSide side) { - return _save(state.copyWith(customSide: side)); - } - - Future setCustomRatingRange(int min, int max) { - return _save(state.copyWith(customRatingDelta: (min, max))); - } - - Future setCustomDaysPerTurn(int days) { - return _save(state.copyWith(customDaysPerTurn: days)); - } - - Future _save(GameSetup newState) async { - final prefs = ref.read(sharedPreferencesProvider); - final session = ref.read(authSessionProvider); - await prefs.setString( - _prefKey(session), - jsonEncode(newState.toJson()), - ); - state = newState; - } -} - -const kSubtractingRatingRange = [ - -500, - -450, - -400, - -350, - -300, - -250, - -200, - -150, - -100, - -50, - 0, -]; - -const kAddingRatingRange = [ - 0, - 50, - 100, - 150, - 200, - 250, - 300, - 350, - 400, - 450, - 500, -]; - -const kAvailableTimesInSeconds = [ - 0, - 15, - 30, - 45, - 60, - 90, - 2 * 60, - 3 * 60, - 4 * 60, - 5 * 60, - 6 * 60, - 7 * 60, - 8 * 60, - 9 * 60, - 10 * 60, - 11 * 60, - 12 * 60, - 13 * 60, - 14 * 60, - 15 * 60, - 16 * 60, - 17 * 60, - 18 * 60, - 19 * 60, - 20 * 60, - 25 * 60, - 30 * 60, - 35 * 60, - 40 * 60, - 45 * 60, - 60 * 60, - 75 * 60, - 90 * 60, - 105 * 60, - 120 * 60, - 135 * 60, - 150 * 60, - 165 * 60, - 180 * 60, -]; - -const kAvailableIncrementsInSeconds = [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15, - 16, - 17, - 18, - 19, - 20, - 25, - 30, - 35, - 40, - 45, - 60, - 90, - 120, - 150, - 180, -]; - -const kAvailableDaysPerTurn = [1, 2, 3, 5, 7, 10, 14]; diff --git a/lib/src/model/lobby/game_setup_preferences.dart b/lib/src/model/lobby/game_setup_preferences.dart new file mode 100644 index 0000000000..7c25f22441 --- /dev/null +++ b/lib/src/model/lobby/game_setup_preferences.dart @@ -0,0 +1,163 @@ +import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/common/game.dart'; +import 'package:lichess_mobile/src/model/common/time_increment.dart'; +import 'package:lichess_mobile/src/model/settings/preferences.dart'; +import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'game_setup_preferences.g.dart'; + +@riverpod +class GameSetupPreferences extends _$GameSetupPreferences + with SessionPreferencesStorage { + // ignore: avoid_public_notifier_properties + @override + Category get prefCategory => Category.gameSetup; + + @override + GameSetup build() { + return fetch(); + } + + Future setQuickPairingTimeIncrement(TimeIncrement timeInc) { + return save(state.copyWith(quickPairingTimeIncrement: timeInc)); + } + + Future setCustomTimeControl(TimeControl control) { + return save(state.copyWith(customTimeControl: control)); + } + + Future setCustomTimeSeconds(int seconds) { + return save(state.copyWith(customTimeSeconds: seconds)); + } + + Future setCustomIncrementSeconds(int seconds) { + return save(state.copyWith(customIncrementSeconds: seconds)); + } + + Future setCustomVariant(Variant variant) { + return save(state.copyWith(customVariant: variant)); + } + + Future setCustomRated(bool rated) { + return save(state.copyWith(customRated: rated)); + } + + Future setCustomSide(SideChoice side) { + return save(state.copyWith(customSide: side)); + } + + Future setCustomRatingRange(int min, int max) { + return save(state.copyWith(customRatingDelta: (min, max))); + } + + Future setCustomDaysPerTurn(int days) { + return save(state.copyWith(customDaysPerTurn: days)); + } +} + +const kSubtractingRatingRange = [ + -500, + -450, + -400, + -350, + -300, + -250, + -200, + -150, + -100, + -50, + 0, +]; + +const kAddingRatingRange = [ + 0, + 50, + 100, + 150, + 200, + 250, + 300, + 350, + 400, + 450, + 500, +]; + +const kAvailableTimesInSeconds = [ + 0, + 15, + 30, + 45, + 60, + 90, + 2 * 60, + 3 * 60, + 4 * 60, + 5 * 60, + 6 * 60, + 7 * 60, + 8 * 60, + 9 * 60, + 10 * 60, + 11 * 60, + 12 * 60, + 13 * 60, + 14 * 60, + 15 * 60, + 16 * 60, + 17 * 60, + 18 * 60, + 19 * 60, + 20 * 60, + 25 * 60, + 30 * 60, + 35 * 60, + 40 * 60, + 45 * 60, + 60 * 60, + 75 * 60, + 90 * 60, + 105 * 60, + 120 * 60, + 135 * 60, + 150 * 60, + 165 * 60, + 180 * 60, +]; + +const kAvailableIncrementsInSeconds = [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 25, + 30, + 35, + 40, + 45, + 60, + 90, + 120, + 150, + 180, +]; + +const kAvailableDaysPerTurn = [1, 2, 3, 5, 7, 10, 14]; diff --git a/lib/src/model/opening_explorer/opening_explorer.dart b/lib/src/model/opening_explorer/opening_explorer.dart index cfc34cede3..6a8b0ca281 100644 --- a/lib/src/model/opening_explorer/opening_explorer.dart +++ b/lib/src/model/opening_explorer/opening_explorer.dart @@ -9,6 +9,12 @@ import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_prefe part 'opening_explorer.freezed.dart'; part 'opening_explorer.g.dart'; +enum OpeningDatabase { + master, + lichess, + player, +} + @Freezed(fromJson: true) class OpeningExplorerEntry with _$OpeningExplorerEntry { const OpeningExplorerEntry._(); @@ -114,6 +120,6 @@ enum GameMode { class OpeningExplorerCacheKey with _$OpeningExplorerCacheKey { const factory OpeningExplorerCacheKey({ required String fen, - required OpeningExplorerPrefState prefs, + required OpeningExplorerPrefs prefs, }) = _OpeningExplorerCacheKey; } diff --git a/lib/src/model/opening_explorer/opening_explorer_preferences.dart b/lib/src/model/opening_explorer/opening_explorer_preferences.dart index 4ef3ab1f78..02c5bf4234 100644 --- a/lib/src/model/opening_explorer/opening_explorer_preferences.dart +++ b/lib/src/model/opening_explorer/opening_explorer_preferences.dart @@ -1,44 +1,37 @@ -import 'dart:convert'; - import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:lichess_mobile/src/db/shared_preferences.dart'; -import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/common/speed.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; +import 'package:lichess_mobile/src/model/settings/preferences.dart' as pref; +import 'package:lichess_mobile/src/model/settings/preferences.dart'; +import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'opening_explorer_preferences.freezed.dart'; part 'opening_explorer_preferences.g.dart'; -@Riverpod(keepAlive: true) -class OpeningExplorerPreferences extends _$OpeningExplorerPreferences { - static const prefKey = 'preferences.opening_explorer'; - +@riverpod +class OpeningExplorerPreferences extends _$OpeningExplorerPreferences + with SessionPreferencesStorage { + // ignore: avoid_public_notifier_properties @override - OpeningExplorerPrefState build() { - final prefs = ref.watch(sharedPreferencesProvider); - final session = ref.watch(authSessionProvider); + final prefCategory = pref.Category.openingExplorer; - final stored = prefs.getString(prefKey); - return stored != null - ? OpeningExplorerPrefState.fromJson( - jsonDecode(stored) as Map, - user: session?.user, - ) - : OpeningExplorerPrefState.defaults(user: session?.user); + @override + OpeningExplorerPrefs build() { + return fetch(); } - Future setDatabase(OpeningDatabase db) => _save( + Future setDatabase(OpeningDatabase db) => save( state.copyWith(db: db), ); Future setMasterDbSince(int year) => - _save(state.copyWith(masterDb: state.masterDb.copyWith(sinceYear: year))); + save(state.copyWith(masterDb: state.masterDb.copyWith(sinceYear: year))); - Future toggleLichessDbSpeed(Speed speed) => _save( + Future toggleLichessDbSpeed(Speed speed) => save( state.copyWith( lichessDb: state.lichessDb.copyWith( speeds: state.lichessDb.speeds.contains(speed) @@ -48,7 +41,7 @@ class OpeningExplorerPreferences extends _$OpeningExplorerPreferences { ), ); - Future toggleLichessDbRating(int rating) => _save( + Future toggleLichessDbRating(int rating) => save( state.copyWith( lichessDb: state.lichessDb.copyWith( ratings: state.lichessDb.ratings.contains(rating) @@ -58,11 +51,11 @@ class OpeningExplorerPreferences extends _$OpeningExplorerPreferences { ), ); - Future setLichessDbSince(DateTime since) => _save( + Future setLichessDbSince(DateTime since) => save( state.copyWith(lichessDb: state.lichessDb.copyWith(since: since)), ); - Future setPlayerDbUsernameOrId(String username) => _save( + Future setPlayerDbUsernameOrId(String username) => save( state.copyWith( playerDb: state.playerDb.copyWith( username: username, @@ -70,11 +63,11 @@ class OpeningExplorerPreferences extends _$OpeningExplorerPreferences { ), ); - Future setPlayerDbSide(Side side) => _save( + Future setPlayerDbSide(Side side) => save( state.copyWith(playerDb: state.playerDb.copyWith(side: side)), ); - Future togglePlayerDbSpeed(Speed speed) => _save( + Future togglePlayerDbSpeed(Speed speed) => save( state.copyWith( playerDb: state.playerDb.copyWith( speeds: state.playerDb.speeds.contains(speed) @@ -84,7 +77,7 @@ class OpeningExplorerPreferences extends _$OpeningExplorerPreferences { ), ); - Future togglePlayerDbGameMode(GameMode gameMode) => _save( + Future togglePlayerDbGameMode(GameMode gameMode) => save( state.copyWith( playerDb: state.playerDb.copyWith( gameModes: state.playerDb.gameModes.contains(gameMode) @@ -94,69 +87,44 @@ class OpeningExplorerPreferences extends _$OpeningExplorerPreferences { ), ); - Future setPlayerDbSince(DateTime since) => _save( + Future setPlayerDbSince(DateTime since) => save( state.copyWith(playerDb: state.playerDb.copyWith(since: since)), ); - - Future _save(OpeningExplorerPrefState newState) async { - final prefs = ref.read(sharedPreferencesProvider); - await prefs.setString( - prefKey, - jsonEncode(newState.toJson()), - ); - state = newState; - } -} - -enum OpeningDatabase { - master, - lichess, - player, } @Freezed(fromJson: true, toJson: true) -class OpeningExplorerPrefState with _$OpeningExplorerPrefState { - const OpeningExplorerPrefState._(); +class OpeningExplorerPrefs + with _$OpeningExplorerPrefs + implements SerializablePreferences { + const OpeningExplorerPrefs._(); - const factory OpeningExplorerPrefState({ + const factory OpeningExplorerPrefs({ required OpeningDatabase db, - required MasterDbPrefState masterDb, - required LichessDbPrefState lichessDb, - required PlayerDbPrefState playerDb, - }) = _OpeningExplorerPrefState; + required MasterDb masterDb, + required LichessDb lichessDb, + required PlayerDb playerDb, + }) = _OpeningExplorerPrefs; - factory OpeningExplorerPrefState.defaults({LightUser? user}) => - OpeningExplorerPrefState( + factory OpeningExplorerPrefs.defaults({LightUser? user}) => + OpeningExplorerPrefs( db: OpeningDatabase.master, - masterDb: MasterDbPrefState.defaults, - lichessDb: LichessDbPrefState.defaults, - playerDb: PlayerDbPrefState.defaults(user: user), + masterDb: MasterDb.defaults, + lichessDb: LichessDb.defaults, + playerDb: PlayerDb.defaults(user: user), ); - factory OpeningExplorerPrefState.fromJson( - Map json, { - LightUser? user, - }) { - try { - final prefs = _$OpeningExplorerPrefStateFromJson(json); - return prefs.copyWith( - playerDb: user != null - ? prefs.playerDb.copyWith(username: user.name) - : prefs.playerDb, - ); - } catch (_) { - return OpeningExplorerPrefState.defaults(user: user); - } + factory OpeningExplorerPrefs.fromJson(Map json) { + return _$OpeningExplorerPrefsFromJson(json); } } @Freezed(fromJson: true, toJson: true) -class MasterDbPrefState with _$MasterDbPrefState { - const MasterDbPrefState._(); +class MasterDb with _$MasterDb { + const MasterDb._(); - const factory MasterDbPrefState({ + const factory MasterDb({ required int sinceYear, - }) = _MasterDbPrefState; + }) = _MasterDb; static const kEarliestYear = 1952; static final now = DateTime.now(); @@ -166,26 +134,22 @@ class MasterDbPrefState with _$MasterDbPrefState { 'Last 20 years': now.year - 20, 'All time': kEarliestYear, }; - static const defaults = MasterDbPrefState(sinceYear: kEarliestYear); + static const defaults = MasterDb(sinceYear: kEarliestYear); - factory MasterDbPrefState.fromJson(Map json) { - try { - return _$MasterDbPrefStateFromJson(json); - } catch (_) { - return defaults; - } + factory MasterDb.fromJson(Map json) { + return _$MasterDbFromJson(json); } } @Freezed(fromJson: true, toJson: true) -class LichessDbPrefState with _$LichessDbPrefState { - const LichessDbPrefState._(); +class LichessDb with _$LichessDb { + const LichessDb._(); - const factory LichessDbPrefState({ + const factory LichessDb({ required ISet speeds, required ISet ratings, required DateTime since, - }) = _LichessDbPrefState; + }) = _LichessDb; static const kAvailableSpeeds = ISetConst({ Speed.ultraBullet, @@ -214,32 +178,28 @@ class LichessDbPrefState with _$LichessDbPrefState { 'Last 5 years': now.subtract(const Duration(days: kDaysInAYear * 5)), 'All time': earliestDate, }; - static final defaults = LichessDbPrefState( + static final defaults = LichessDb( speeds: kAvailableSpeeds.remove(Speed.ultraBullet), ratings: kAvailableRatings.remove(400), since: earliestDate, ); - factory LichessDbPrefState.fromJson(Map json) { - try { - return _$LichessDbPrefStateFromJson(json); - } catch (_) { - return defaults; - } + factory LichessDb.fromJson(Map json) { + return _$LichessDbFromJson(json); } } @Freezed(fromJson: true, toJson: true) -class PlayerDbPrefState with _$PlayerDbPrefState { - const PlayerDbPrefState._(); +class PlayerDb with _$PlayerDb { + const PlayerDb._(); - const factory PlayerDbPrefState({ + const factory PlayerDb({ String? username, required Side side, required ISet speeds, required ISet gameModes, required DateTime since, - }) = _PlayerDbPrefState; + }) = _PlayerDb; static const kAvailableSpeeds = ISetConst({ Speed.ultraBullet, @@ -258,7 +218,7 @@ class PlayerDbPrefState with _$PlayerDbPrefState { 'Last year': now.subtract(const Duration(days: 365)), 'All time': earliestDate, }; - factory PlayerDbPrefState.defaults({LightUser? user}) => PlayerDbPrefState( + factory PlayerDb.defaults({LightUser? user}) => PlayerDb( username: user?.name, side: Side.white, speeds: kAvailableSpeeds, @@ -266,11 +226,7 @@ class PlayerDbPrefState with _$PlayerDbPrefState { since: earliestDate, ); - factory PlayerDbPrefState.fromJson(Map json) { - try { - return _$PlayerDbPrefStateFromJson(json); - } catch (_) { - return PlayerDbPrefState.defaults(); - } + factory PlayerDb.fromJson(Map json) { + return _$PlayerDbFromJson(json); } } diff --git a/lib/src/model/puzzle/puzzle_controller.dart b/lib/src/model/puzzle/puzzle_controller.dart index 349a0a55a7..425ebafcb6 100644 --- a/lib/src/model/puzzle/puzzle_controller.dart +++ b/lib/src/model/puzzle/puzzle_controller.dart @@ -232,9 +232,7 @@ class PuzzleController extends _$PuzzleController { ); await ref - .read( - puzzlePreferencesProvider(initialContext.userId).notifier, - ) + .read(puzzlePreferencesProvider.notifier) .setDifficulty(difficulty); final nextPuzzle = (await _service).resetBatch( @@ -385,7 +383,7 @@ class PuzzleController extends _$PuzzleController { if (next != null && result == PuzzleResult.win && - ref.read(puzzlePreferencesProvider(initialContext.userId)).autoNext) { + ref.read(puzzlePreferencesProvider).autoNext) { loadPuzzle(next); } } else { diff --git a/lib/src/model/puzzle/puzzle_preferences.dart b/lib/src/model/puzzle/puzzle_preferences.dart index 0285ac476f..99d2a01745 100644 --- a/lib/src/model/puzzle/puzzle_preferences.dart +++ b/lib/src/model/puzzle/puzzle_preferences.dart @@ -1,62 +1,27 @@ -import 'dart:convert'; - -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:lichess_mobile/src/db/shared_preferences.dart'; -import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_difficulty.dart'; +import 'package:lichess_mobile/src/model/settings/preferences.dart'; +import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -part 'puzzle_preferences.freezed.dart'; part 'puzzle_preferences.g.dart'; -const _prefKey = 'puzzle.preferences'; - @riverpod -class PuzzlePreferences extends _$PuzzlePreferences { +class PuzzlePreferences extends _$PuzzlePreferences + with SessionPreferencesStorage { + // ignore: avoid_public_notifier_properties @override - PuzzlePrefState build(UserId? id) { - final prefs = ref.watch(sharedPreferencesProvider); - final stored = prefs.getString(_makeKey(id)); - return stored != null - ? PuzzlePrefState.fromJson(jsonDecode(stored) as Map) - : PuzzlePrefState.defaults(id: id); + final prefCategory = Category.puzzle; + + @override + PuzzlePrefs build() { + return fetch(); } Future setDifficulty(PuzzleDifficulty difficulty) async { - final newState = state.copyWith(difficulty: difficulty); - final prefs = ref.read(sharedPreferencesProvider); - await prefs.setString(_makeKey(id), jsonEncode(newState.toJson())); - state = newState; + save(state.copyWith(difficulty: difficulty)); } Future setAutoNext(bool autoNext) async { - final newState = state.copyWith(autoNext: autoNext); - final prefs = ref.read(sharedPreferencesProvider); - await prefs.setString(_makeKey(id), jsonEncode(newState.toJson())); - state = newState; + save(state.copyWith(autoNext: autoNext)); } - - String _makeKey(UserId? id) => '$_prefKey.${id ?? ''}'; -} - -@Freezed(fromJson: true, toJson: true) -class PuzzlePrefState with _$PuzzlePrefState { - const factory PuzzlePrefState({ - required UserId? id, - required PuzzleDifficulty difficulty, - - /// If `true`, will show next puzzle after successful completion. This has - /// no effect on puzzle streaks, which always show next puzzle. Defaults to - /// `false`. - @Default(false) bool autoNext, - }) = _PuzzlePrefState; - - factory PuzzlePrefState.defaults({UserId? id}) => PuzzlePrefState( - id: id, - difficulty: PuzzleDifficulty.normal, - autoNext: false, - ); - - factory PuzzlePrefState.fromJson(Map json) => - _$PuzzlePrefStateFromJson(json); } diff --git a/lib/src/model/puzzle/puzzle_service.dart b/lib/src/model/puzzle/puzzle_service.dart index 5ca5cf52e3..0989724dcf 100644 --- a/lib/src/model/puzzle/puzzle_service.dart +++ b/lib/src/model/puzzle/puzzle_service.dart @@ -158,7 +158,7 @@ class PuzzleService { _log.fine('Have a puzzle deficit of $deficit, will sync with lichess'); - final difficulty = _ref.read(puzzlePreferencesProvider(userId)).difficulty; + final difficulty = _ref.read(puzzlePreferencesProvider).difficulty; // anonymous users can't solve puzzles so we just download the deficit // we send the request even if the deficit is 0 to get the glicko rating diff --git a/lib/src/model/settings/board_preferences.dart b/lib/src/model/settings/board_preferences.dart index e22dd3c7a6..58a7a59485 100644 --- a/lib/src/model/settings/board_preferences.dart +++ b/lib/src/model/settings/board_preferences.dart @@ -11,7 +11,7 @@ class BoardPreferences extends _$BoardPreferences with PreferencesStorage { // ignore: avoid_public_notifier_properties @override - pref.Category get categoryKey => pref.Category.board; + pref.Category get prefCategory => pref.Category.board; @override pref.Board build() { diff --git a/lib/src/model/settings/general_preferences.dart b/lib/src/model/settings/general_preferences.dart index 9d9b22ea4e..9b309a160c 100644 --- a/lib/src/model/settings/general_preferences.dart +++ b/lib/src/model/settings/general_preferences.dart @@ -12,7 +12,7 @@ class GeneralPreferences extends _$GeneralPreferences with PreferencesStorage { // ignore: avoid_public_notifier_properties @override - pref.Category get categoryKey => pref.Category.general; + final prefCategory = pref.Category.general; @override pref.General build() { diff --git a/lib/src/model/settings/home_preferences.dart b/lib/src/model/settings/home_preferences.dart index fcbb87cae6..9f13718b71 100644 --- a/lib/src/model/settings/home_preferences.dart +++ b/lib/src/model/settings/home_preferences.dart @@ -9,7 +9,7 @@ class HomePreferences extends _$HomePreferences with PreferencesStorage { // ignore: avoid_public_notifier_properties @override - pref.Category get categoryKey => pref.Category.home; + pref.Category get prefCategory => pref.Category.home; @override pref.Home build() { diff --git a/lib/src/model/settings/over_the_board_preferences.dart b/lib/src/model/settings/over_the_board_preferences.dart index 8be6edfffe..ab6078ca66 100644 --- a/lib/src/model/settings/over_the_board_preferences.dart +++ b/lib/src/model/settings/over_the_board_preferences.dart @@ -9,7 +9,7 @@ class OverTheBoardPreferences extends _$OverTheBoardPreferences with PreferencesStorage { // ignore: avoid_public_notifier_properties @override - pref.Category get categoryKey => + pref.Category get prefCategory => pref.Category.overTheBoard; @override diff --git a/lib/src/model/settings/preferences.dart b/lib/src/model/settings/preferences.dart index 3a82d68400..2e9ec4561e 100644 --- a/lib/src/model/settings/preferences.dart +++ b/lib/src/model/settings/preferences.dart @@ -1,11 +1,64 @@ -import 'package:chessground/chessground.dart' as cg; +import 'dart:math' as math; +import 'package:chessground/chessground.dart'; import 'package:flutter/material.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:lichess_mobile/l10n/l10n.dart'; +import 'package:lichess_mobile/src/model/challenge/challenge.dart'; +import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/common/game.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/common/perf.dart'; +import 'package:lichess_mobile/src/model/common/speed.dart'; +import 'package:lichess_mobile/src/model/common/time_increment.dart'; +import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_preferences.dart'; +import 'package:lichess_mobile/src/model/puzzle/puzzle_difficulty.dart'; +import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/utils/color_palette.dart'; part 'preferences.freezed.dart'; part 'preferences.g.dart'; +/// A preference category with its storage key and default values. +enum Category { + general('preferences.general', General.defaults), + home('preferences.home', Home.defaults), + board('preferences.board', Board.defaults), + analysis('preferences.analysis', Analysis.defaults), + overTheBoard('preferences.overTheBoard', OverTheBoard.defaults), + challenge('preferences.challenge', Challenge.defaults), + gameSetup('preferences.gameSetup', GameSetup.defaults), + game('preferences.game', Game.defaults), + coordinateTraining( + 'preferences.coordinateTraining', + CoordinateTraining.defaults, + ), + openingExplorer( + 'preferences.opening_explorer', + null as OpeningExplorerPrefs?, + ), + puzzle('preferences.puzzle', null as PuzzlePrefs?); + + const Category(this.storageKey, this._defaults); + + final String storageKey; + final T? _defaults; + + T defaults({LightUser? user}) => switch (this) { + Category.general => _defaults!, + Category.home => _defaults!, + Category.board => _defaults!, + Category.analysis => _defaults!, + Category.overTheBoard => _defaults!, + Category.challenge => _defaults!, + Category.gameSetup => _defaults!, + Category.game => _defaults!, + Category.coordinateTraining => _defaults!, + Category.openingExplorer => + OpeningExplorerPrefs.defaults(user: user) as T, + Category.puzzle => PuzzlePrefs.defaults(id: user?.id) as T, + }; +} + /// Interface for serializable preferences. abstract class SerializablePreferences { Map toJson(); @@ -25,24 +78,22 @@ abstract class SerializablePreferences { return Analysis.fromJson(json); case Category.overTheBoard: return OverTheBoard.fromJson(json); + case Category.challenge: + return Challenge.fromJson(json); + case Category.gameSetup: + return GameSetup.fromJson(json); + case Category.game: + return Game.fromJson(json); + case Category.coordinateTraining: + return CoordinateTraining.fromJson(json); + case Category.openingExplorer: + return OpeningExplorerPrefs.fromJson(json); + case Category.puzzle: + return PuzzlePrefs.fromJson(json); } } } -/// A preference category with its storage key and default values. -enum Category { - general('preferences.general', General.defaults), - home('preferences.home', Home.defaults), - board('preferences.board', Board.defaults), - analysis('preferences.analysis', Analysis.defaults), - overTheBoard('preferences.overTheBoard', OverTheBoard.defaults); - - const Category(this.storageKey, this.defaults); - - final String storageKey; - final T defaults; -} - Map? _localeToJson(Locale? locale) { return locale != null ? { @@ -144,7 +195,7 @@ class Board with _$Board implements SerializablePreferences { const Board._(); const factory Board({ - required cg.PieceSet pieceSet, + required PieceSet pieceSet, required BoardTheme boardTheme, bool? immersiveModeWhilePlaying, required bool hapticFeedback, @@ -154,10 +205,10 @@ class Board with _$Board implements SerializablePreferences { required bool pieceAnimation, required bool showMaterialDifference, @JsonKey( - defaultValue: cg.PieceShiftMethod.either, - unknownEnumValue: cg.PieceShiftMethod.either, + defaultValue: PieceShiftMethod.either, + unknownEnumValue: PieceShiftMethod.either, ) - required cg.PieceShiftMethod pieceShiftMethod, + required PieceShiftMethod pieceShiftMethod, /// Whether to enable shape drawings on the board for games and puzzles. @JsonKey(defaultValue: true) required bool enableShapeDrawings, @@ -170,7 +221,7 @@ class Board with _$Board implements SerializablePreferences { }) = _Board; static const defaults = Board( - pieceSet: cg.PieceSet.staunty, + pieceSet: PieceSet.staunty, boardTheme: BoardTheme.brown, immersiveModeWhilePlaying: false, hapticFeedback: true, @@ -179,14 +230,14 @@ class Board with _$Board implements SerializablePreferences { coordinates: true, pieceAnimation: true, showMaterialDifference: true, - pieceShiftMethod: cg.PieceShiftMethod.either, + pieceShiftMethod: PieceShiftMethod.either, enableShapeDrawings: true, magnifyDraggedPiece: true, shapeColor: ShapeColor.green, ); - cg.ChessboardSettings toBoardSettings() { - return cg.ChessboardSettings( + ChessboardSettings toBoardSettings() { + return ChessboardSettings( pieceAssets: pieceSet.assets, colorScheme: boardTheme.colors, showValidMoves: showLegalMoves, @@ -196,7 +247,7 @@ class Board with _$Board implements SerializablePreferences { dragFeedbackScale: magnifyDraggedPiece ? 2.0 : 1.0, dragFeedbackOffset: Offset(0.0, magnifyDraggedPiece ? -1.0 : 0.0), pieceShiftMethod: pieceShiftMethod, - drawShape: cg.DrawShapeOptions( + drawShape: DrawShapeOptions( enable: enableShapeDrawings, newShapeColor: shapeColor.color, ), @@ -263,56 +314,56 @@ enum BoardTheme { const BoardTheme(this.label); - cg.ChessboardColorScheme get colors { + ChessboardColorScheme get colors { switch (this) { case BoardTheme.system: - return getBoardColorScheme() ?? cg.ChessboardColorScheme.brown; + return getBoardColorScheme() ?? ChessboardColorScheme.brown; case BoardTheme.blue: - return cg.ChessboardColorScheme.blue; + return ChessboardColorScheme.blue; case BoardTheme.blue2: - return cg.ChessboardColorScheme.blue2; + return ChessboardColorScheme.blue2; case BoardTheme.blue3: - return cg.ChessboardColorScheme.blue3; + return ChessboardColorScheme.blue3; case BoardTheme.blueMarble: - return cg.ChessboardColorScheme.blueMarble; + return ChessboardColorScheme.blueMarble; case BoardTheme.canvas: - return cg.ChessboardColorScheme.canvas; + return ChessboardColorScheme.canvas; case BoardTheme.wood: - return cg.ChessboardColorScheme.wood; + return ChessboardColorScheme.wood; case BoardTheme.wood2: - return cg.ChessboardColorScheme.wood2; + return ChessboardColorScheme.wood2; case BoardTheme.wood3: - return cg.ChessboardColorScheme.wood3; + return ChessboardColorScheme.wood3; case BoardTheme.wood4: - return cg.ChessboardColorScheme.wood4; + return ChessboardColorScheme.wood4; case BoardTheme.maple: - return cg.ChessboardColorScheme.maple; + return ChessboardColorScheme.maple; case BoardTheme.maple2: - return cg.ChessboardColorScheme.maple2; + return ChessboardColorScheme.maple2; case BoardTheme.brown: - return cg.ChessboardColorScheme.brown; + return ChessboardColorScheme.brown; case BoardTheme.leather: - return cg.ChessboardColorScheme.leather; + return ChessboardColorScheme.leather; case BoardTheme.green: - return cg.ChessboardColorScheme.green; + return ChessboardColorScheme.green; case BoardTheme.marble: - return cg.ChessboardColorScheme.marble; + return ChessboardColorScheme.marble; case BoardTheme.greenPlastic: - return cg.ChessboardColorScheme.greenPlastic; + return ChessboardColorScheme.greenPlastic; case BoardTheme.grey: - return cg.ChessboardColorScheme.grey; + return ChessboardColorScheme.grey; case BoardTheme.metal: - return cg.ChessboardColorScheme.metal; + return ChessboardColorScheme.metal; case BoardTheme.olive: - return cg.ChessboardColorScheme.olive; + return ChessboardColorScheme.olive; case BoardTheme.newspaper: - return cg.ChessboardColorScheme.newspaper; + return ChessboardColorScheme.newspaper; case BoardTheme.purpleDiag: - return cg.ChessboardColorScheme.purpleDiag; + return ChessboardColorScheme.purpleDiag; case BoardTheme.pinkPyramid: - return cg.ChessboardColorScheme.pinkPyramid; + return ChessboardColorScheme.pinkPyramid; case BoardTheme.horsey: - return cg.ChessboardColorScheme.horsey; + return ChessboardColorScheme.horsey; } } @@ -395,3 +446,215 @@ class OverTheBoard with _$OverTheBoard implements SerializablePreferences { } } } + +@Freezed(fromJson: true, toJson: true) +class Challenge with _$Challenge implements SerializablePreferences { + const Challenge._(); + + const factory Challenge({ + required Variant variant, + required ChallengeTimeControlType timeControl, + required ({Duration time, Duration increment}) clock, + required int days, + required bool rated, + required SideChoice sideChoice, + }) = _Challenge; + + static const defaults = Challenge( + variant: Variant.standard, + timeControl: ChallengeTimeControlType.clock, + clock: (time: Duration(minutes: 10), increment: Duration.zero), + days: 3, + rated: false, + sideChoice: SideChoice.random, + ); + + Speed get speed => timeControl == ChallengeTimeControlType.clock + ? Speed.fromTimeIncrement( + TimeIncrement( + clock.time.inSeconds, + clock.increment.inSeconds, + ), + ) + : Speed.correspondence; + + ChallengeRequest makeRequest(LightUser destUser, [String? initialFen]) { + return ChallengeRequest( + destUser: destUser, + variant: variant, + timeControl: timeControl, + clock: clock, + days: days, + rated: rated, + sideChoice: sideChoice, + initialFen: initialFen, + ); + } + + factory Challenge.fromJson(Map json) { + try { + return _$ChallengeFromJson(json); + } catch (_) { + return Challenge.defaults; + } + } +} + +enum TimeControl { realTime, correspondence } + +@Freezed(fromJson: true, toJson: true) +class GameSetup with _$GameSetup implements SerializablePreferences { + const GameSetup._(); + + const factory GameSetup({ + required TimeIncrement quickPairingTimeIncrement, + required TimeControl customTimeControl, + required int customTimeSeconds, + required int customIncrementSeconds, + required int customDaysPerTurn, + required Variant customVariant, + required bool customRated, + required SideChoice customSide, + required (int, int) customRatingDelta, + }) = _GameSetup; + + static const defaults = GameSetup( + quickPairingTimeIncrement: TimeIncrement(600, 0), + customTimeControl: TimeControl.realTime, + customTimeSeconds: 180, + customIncrementSeconds: 0, + customVariant: Variant.standard, + customRated: false, + customSide: SideChoice.random, + customRatingDelta: (-500, 500), + customDaysPerTurn: 3, + ); + + Speed get speedFromCustom => Speed.fromTimeIncrement( + TimeIncrement( + customTimeSeconds, + customIncrementSeconds, + ), + ); + + Perf get perfFromCustom => Perf.fromVariantAndSpeed( + customVariant, + speedFromCustom, + ); + + /// Returns the rating range for the custom setup, or null if the user + /// doesn't have a rating for the custom setup perf. + (int, int)? ratingRangeFromCustom(User user) { + final perf = user.perfs[perfFromCustom]; + if (perf == null) return null; + if (perf.provisional == true) return null; + final min = math.max(0, perf.rating + customRatingDelta.$1); + final max = perf.rating + customRatingDelta.$2; + return (min, max); + } + + factory GameSetup.fromJson(Map json) { + try { + return _$GameSetupFromJson(json); + } catch (_) { + return defaults; + } + } +} + +@Freezed(fromJson: true, toJson: true) +class Game with _$Game implements SerializablePreferences { + const factory Game({ + bool? enableChat, + bool? blindfoldMode, + }) = _Game; + + static const defaults = Game( + enableChat: true, + ); + + factory Game.fromJson(Map json) => _$GameFromJson(json); +} + +enum TimeChoice { + thirtySeconds(Duration(seconds: 30)), + unlimited(null); + + const TimeChoice(this.duration); + + final Duration? duration; + + // TODO l10n + Widget label(AppLocalizations l10n) { + switch (this) { + case TimeChoice.thirtySeconds: + return const Text('30s'); + case TimeChoice.unlimited: + return const Icon(Icons.all_inclusive); + } + } +} + +enum TrainingMode { + findSquare, + nameSquare; + + // TODO l10n + String label(AppLocalizations l10n) { + switch (this) { + case TrainingMode.findSquare: + return 'Find Square'; + case TrainingMode.nameSquare: + return 'Name Square'; + } + } +} + +@Freezed(fromJson: true, toJson: true) +class CoordinateTraining + with _$CoordinateTraining + implements SerializablePreferences { + const CoordinateTraining._(); + + const factory CoordinateTraining({ + required bool showCoordinates, + required bool showPieces, + required TrainingMode mode, + required TimeChoice timeChoice, + required SideChoice sideChoice, + }) = _CoordinateTraining; + + static const defaults = CoordinateTraining( + showCoordinates: false, + showPieces: true, + mode: TrainingMode.findSquare, + timeChoice: TimeChoice.thirtySeconds, + sideChoice: SideChoice.random, + ); + + factory CoordinateTraining.fromJson(Map json) { + return _$CoordinateTrainingFromJson(json); + } +} + +@Freezed(fromJson: true, toJson: true) +class PuzzlePrefs with _$PuzzlePrefs implements SerializablePreferences { + const factory PuzzlePrefs({ + required UserId? id, + required PuzzleDifficulty difficulty, + + /// If `true`, will show next puzzle after successful completion. This has + /// no effect on puzzle streaks, which always show next puzzle. Defaults to + /// `false`. + @Default(false) bool autoNext, + }) = _PuzzlePrefs; + + factory PuzzlePrefs.defaults({UserId? id}) => PuzzlePrefs( + id: id, + difficulty: PuzzleDifficulty.normal, + autoNext: false, + ); + + factory PuzzlePrefs.fromJson(Map json) => + _$PuzzlePrefsFromJson(json); +} diff --git a/lib/src/model/settings/preferences_storage.dart b/lib/src/model/settings/preferences_storage.dart index dc2668fddd..2bf9326722 100644 --- a/lib/src/model/settings/preferences_storage.dart +++ b/lib/src/model/settings/preferences_storage.dart @@ -4,38 +4,41 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/binding.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/settings/preferences.dart'; +import 'package:logging/logging.dart'; -/// A mixin to provide a way to store and retrieve preferences. +final _logger = Logger('PreferencesStorage'); + +/// A [Notifier] mixin to provide a way to store and retrieve preferences. /// /// This mixin is intended to be used with a [Notifier] that holds a -/// [SerializablePreferences] object. It provides a way to save and fetch the -/// preferences from the shared preferences. +/// [SerializablePreferences] object. mixin PreferencesStorage { AutoDisposeNotifierProviderRef get ref; abstract T state; - Category get categoryKey; + Category get prefCategory; Future save(T value) async { await LichessBinding.instance.sharedPreferences - .setString(categoryKey.storageKey, jsonEncode(value.toJson())); + .setString(prefCategory.storageKey, jsonEncode(value.toJson())); state = value; } T fetch() { final stored = LichessBinding.instance.sharedPreferences - .getString(categoryKey.storageKey); + .getString(prefCategory.storageKey); if (stored == null) { - return categoryKey.defaults; + return prefCategory.defaults(); } try { return SerializablePreferences.fromJson( - categoryKey, + prefCategory, jsonDecode(stored) as Map, ) as T; } catch (e) { - return categoryKey.defaults; + _logger.warning('Failed to decode $prefCategory preferences: $e'); + return prefCategory.defaults(); } } } @@ -50,12 +53,12 @@ mixin SessionPreferencesStorage { AutoDisposeNotifierProviderRef get ref; abstract T state; - Category get categoryKey; + Category get prefCategory; Future save(T value) async { final session = ref.read(authSessionProvider); await LichessBinding.instance.sharedPreferences.setString( - _prefKey(categoryKey.storageKey, session), + key(prefCategory.storageKey, session), jsonEncode(value.toJson()), ); @@ -65,20 +68,21 @@ mixin SessionPreferencesStorage { T fetch() { final session = ref.watch(authSessionProvider); final stored = LichessBinding.instance.sharedPreferences - .getString(_prefKey(categoryKey.storageKey, session)); + .getString(key(prefCategory.storageKey, session)); if (stored == null) { - return categoryKey.defaults; + return prefCategory.defaults(user: session?.user); } try { return SerializablePreferences.fromJson( - categoryKey, + prefCategory, jsonDecode(stored) as Map, ) as T; } catch (e) { - return categoryKey.defaults; + _logger.warning('Failed to decode $prefCategory preferences: $e'); + return prefCategory.defaults(user: session?.user); } } - String _prefKey(String key, AuthSessionState? session) => + static String key(String key, AuthSessionState? session) => '$key.${session?.user.id ?? '**anon**'}'; } diff --git a/lib/src/view/clock/custom_clock_settings.dart b/lib/src/view/clock/custom_clock_settings.dart index 58563a18d8..ee2bdc6d2d 100644 --- a/lib/src/view/clock/custom_clock_settings.dart +++ b/lib/src/view/clock/custom_clock_settings.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:lichess_mobile/src/model/clock/clock_controller.dart'; import 'package:lichess_mobile/src/model/common/time_increment.dart'; -import 'package:lichess_mobile/src/model/lobby/game_setup.dart'; +import 'package:lichess_mobile/src/model/lobby/game_setup_preferences.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; diff --git a/lib/src/view/coordinate_training/coordinate_training_screen.dart b/lib/src/view/coordinate_training/coordinate_training_screen.dart index 125f7a0919..99780471c5 100644 --- a/lib/src/view/coordinate_training/coordinate_training_screen.dart +++ b/lib/src/view/coordinate_training/coordinate_training_screen.dart @@ -7,9 +7,11 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/constants.dart'; +import 'package:lichess_mobile/src/model/common/game.dart'; import 'package:lichess_mobile/src/model/coordinate_training/coordinate_training_controller.dart'; import 'package:lichess_mobile/src/model/coordinate_training/coordinate_training_preferences.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; +import 'package:lichess_mobile/src/model/settings/preferences.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; @@ -362,7 +364,7 @@ class _SettingsState extends ConsumerState<_Settings> { spacing: 8.0, children: SideChoice.values.map((choice) { return ChoiceChip( - label: Text(sideChoiceL10n(context, choice)), + label: Text(choice.label(context.l10n)), selected: trainingPrefs.sideChoice == choice, showCheckmark: false, onSelected: (selected) { @@ -384,7 +386,7 @@ class _SettingsState extends ConsumerState<_Settings> { spacing: 8.0, children: TimeChoice.values.map((choice) { return ChoiceChip( - label: timeChoiceL10n(context, choice), + label: choice.label(context.l10n), selected: trainingPrefs.timeChoice == choice, showCheckmark: false, onSelected: (selected) { diff --git a/lib/src/view/game/game_screen.dart b/lib/src/view/game/game_screen.dart index 8dd52d7634..00e3c9c23e 100644 --- a/lib/src/view/game/game_screen.dart +++ b/lib/src/view/game/game_screen.dart @@ -9,7 +9,7 @@ import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/game/game_history.dart'; import 'package:lichess_mobile/src/model/lobby/create_game_service.dart'; import 'package:lichess_mobile/src/model/lobby/game_seek.dart'; -import 'package:lichess_mobile/src/model/lobby/game_setup.dart'; +import 'package:lichess_mobile/src/model/lobby/game_setup_preferences.dart'; import 'package:lichess_mobile/src/navigation.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; diff --git a/lib/src/view/opening_explorer/opening_explorer_settings.dart b/lib/src/view/opening_explorer/opening_explorer_settings.dart index bd2d5b30f2..59b57c3c62 100644 --- a/lib/src/view/opening_explorer/opening_explorer_settings.dart +++ b/lib/src/view/opening_explorer/opening_explorer_settings.dart @@ -28,15 +28,14 @@ class OpeningExplorerSettings extends ConsumerWidget { title: const Text('Timespan'), subtitle: Wrap( spacing: 5, - children: MasterDbPrefState.datesMap.keys + children: MasterDb.datesMap.keys .map( (key) => ChoiceChip( label: Text(key), - selected: prefs.masterDb.sinceYear == - MasterDbPrefState.datesMap[key], + selected: prefs.masterDb.sinceYear == MasterDb.datesMap[key], onSelected: (_) => ref .read(openingExplorerPreferencesProvider.notifier) - .setMasterDbSince(MasterDbPrefState.datesMap[key]!), + .setMasterDbSince(MasterDb.datesMap[key]!), ), ) .toList(growable: false), @@ -49,7 +48,7 @@ class OpeningExplorerSettings extends ConsumerWidget { title: Text(context.l10n.timeControl), subtitle: Wrap( spacing: 5, - children: LichessDbPrefState.kAvailableSpeeds + children: LichessDb.kAvailableSpeeds .map( (speed) => FilterChip( label: Text( @@ -76,7 +75,7 @@ class OpeningExplorerSettings extends ConsumerWidget { title: Text(context.l10n.rating), subtitle: Wrap( spacing: 5, - children: LichessDbPrefState.kAvailableRatings + children: LichessDb.kAvailableRatings .map( (rating) => FilterChip( label: Text(rating.toString()), @@ -98,15 +97,15 @@ class OpeningExplorerSettings extends ConsumerWidget { title: const Text('Timespan'), subtitle: Wrap( spacing: 5, - children: LichessDbPrefState.datesMap.keys + children: LichessDb.datesMap.keys .map( (key) => ChoiceChip( label: Text(key), selected: - prefs.lichessDb.since == LichessDbPrefState.datesMap[key], + prefs.lichessDb.since == LichessDb.datesMap[key], onSelected: (_) => ref .read(openingExplorerPreferencesProvider.notifier) - .setLichessDbSince(LichessDbPrefState.datesMap[key]!), + .setLichessDbSince(LichessDb.datesMap[key]!), ), ) .toList(growable: false), @@ -173,7 +172,7 @@ class OpeningExplorerSettings extends ConsumerWidget { title: Text(context.l10n.timeControl), subtitle: Wrap( spacing: 5, - children: PlayerDbPrefState.kAvailableSpeeds + children: PlayerDb.kAvailableSpeeds .map( (speed) => FilterChip( label: Text( @@ -222,15 +221,14 @@ class OpeningExplorerSettings extends ConsumerWidget { title: const Text('Timespan'), subtitle: Wrap( spacing: 5, - children: PlayerDbPrefState.datesMap.keys + children: PlayerDb.datesMap.keys .map( (key) => ChoiceChip( label: Text(key), - selected: - prefs.playerDb.since == PlayerDbPrefState.datesMap[key], + selected: prefs.playerDb.since == PlayerDb.datesMap[key], onSelected: (_) => ref .read(openingExplorerPreferencesProvider.notifier) - .setPlayerDbSince(PlayerDbPrefState.datesMap[key]!), + .setPlayerDbSince(PlayerDb.datesMap[key]!), ), ) .toList(growable: false), diff --git a/lib/src/view/over_the_board/configure_over_the_board_game.dart b/lib/src/view/over_the_board/configure_over_the_board_game.dart index 02e95c4588..1cee182f10 100644 --- a/lib/src/view/over_the_board/configure_over_the_board_game.dart +++ b/lib/src/view/over_the_board/configure_over_the_board_game.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/time_increment.dart'; -import 'package:lichess_mobile/src/model/lobby/game_setup.dart'; +import 'package:lichess_mobile/src/model/lobby/game_setup_preferences.dart'; import 'package:lichess_mobile/src/model/over_the_board/over_the_board_clock.dart'; import 'package:lichess_mobile/src/model/over_the_board/over_the_board_game_controller.dart'; import 'package:lichess_mobile/src/model/settings/over_the_board_preferences.dart'; diff --git a/lib/src/view/play/challenge_list_item.dart b/lib/src/view/play/challenge_list_item.dart index 959fb5d42d..3bcbaa5293 100644 --- a/lib/src/view/play/challenge_list_item.dart +++ b/lib/src/view/play/challenge_list_item.dart @@ -7,6 +7,7 @@ import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/challenge/challenge.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/common/game.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/speed.dart'; import 'package:lichess_mobile/src/model/lobby/correspondence_challenge.dart'; diff --git a/lib/src/view/play/common_play_widgets.dart b/lib/src/view/play/common_play_widgets.dart index 450473adf1..846c7da11b 100644 --- a/lib/src/view/play/common_play_widgets.dart +++ b/lib/src/view/play/common_play_widgets.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:lichess_mobile/src/model/lobby/game_setup.dart'; +import 'package:lichess_mobile/src/model/lobby/game_setup_preferences.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; diff --git a/lib/src/view/play/create_challenge_screen.dart b/lib/src/view/play/create_challenge_screen.dart index d21caaf4f6..fd4ff2b159 100644 --- a/lib/src/view/play/create_challenge_screen.dart +++ b/lib/src/view/play/create_challenge_screen.dart @@ -9,9 +9,10 @@ import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/challenge/challenge.dart'; import 'package:lichess_mobile/src/model/challenge/challenge_preferences.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/common/game.dart'; import 'package:lichess_mobile/src/model/common/time_increment.dart'; import 'package:lichess_mobile/src/model/lobby/create_game_service.dart'; -import 'package:lichess_mobile/src/model/lobby/game_setup.dart'; +import 'package:lichess_mobile/src/model/lobby/game_setup_preferences.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; diff --git a/lib/src/view/play/create_custom_game_screen.dart b/lib/src/view/play/create_custom_game_screen.dart index 6b3810eabb..80f3880239 100644 --- a/lib/src/view/play/create_custom_game_screen.dart +++ b/lib/src/view/play/create_custom_game_screen.dart @@ -7,14 +7,16 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/common/game.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/common/time_increment.dart'; import 'package:lichess_mobile/src/model/lobby/create_game_service.dart'; import 'package:lichess_mobile/src/model/lobby/game_seek.dart'; -import 'package:lichess_mobile/src/model/lobby/game_setup.dart'; +import 'package:lichess_mobile/src/model/lobby/game_setup_preferences.dart'; import 'package:lichess_mobile/src/model/lobby/lobby_repository.dart'; +import 'package:lichess_mobile/src/model/settings/preferences.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; @@ -523,13 +525,13 @@ class _CreateGameBodyState extends ConsumerState<_CreateGameBody> { title: Text(context.l10n.side), trailing: AdaptiveTextButton( onPressed: () { - showChoicePicker( + showChoicePicker( context, - choices: PlayableSide.values, + choices: SideChoice.values, selectedItem: preferences.customSide, - labelBuilder: (PlayableSide side) => - Text(playableSideL10n(context.l10n, side)), - onSelectedItemChanged: (PlayableSide side) { + labelBuilder: (SideChoice side) => + Text(side.label(context.l10n)), + onSelectedItemChanged: (SideChoice side) { ref .read(gameSetupPreferencesProvider.notifier) .setCustomSide(side); @@ -537,7 +539,7 @@ class _CreateGameBodyState extends ConsumerState<_CreateGameBody> { ); }, child: Text( - playableSideL10n(context.l10n, preferences.customSide), + preferences.customSide.label(context.l10n), ), ), ), diff --git a/lib/src/view/play/quick_game_button.dart b/lib/src/view/play/quick_game_button.dart index 6d134c4018..a22522b49e 100644 --- a/lib/src/view/play/quick_game_button.dart +++ b/lib/src/view/play/quick_game_button.dart @@ -4,7 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/lobby/game_seek.dart'; -import 'package:lichess_mobile/src/model/lobby/game_setup.dart'; +import 'package:lichess_mobile/src/model/lobby/game_setup_preferences.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; diff --git a/lib/src/view/play/time_control_modal.dart b/lib/src/view/play/time_control_modal.dart index db5c677ea1..4abe8ea2a9 100644 --- a/lib/src/view/play/time_control_modal.dart +++ b/lib/src/view/play/time_control_modal.dart @@ -3,7 +3,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/common/time_increment.dart'; -import 'package:lichess_mobile/src/model/lobby/game_setup.dart'; +import 'package:lichess_mobile/src/model/lobby/game_setup_preferences.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; diff --git a/lib/src/view/puzzle/puzzle_screen.dart b/lib/src/view/puzzle/puzzle_screen.dart index 0c8eb4eb58..04e28521f4 100644 --- a/lib/src/view/puzzle/puzzle_screen.dart +++ b/lib/src/view/puzzle/puzzle_screen.dart @@ -541,8 +541,7 @@ class _DifficultySelector extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final difficulty = ref.watch( - puzzlePreferencesProvider(initialPuzzleContext.userId) - .select((state) => state.difficulty), + puzzlePreferencesProvider.select((state) => state.difficulty), ); final state = ref.watch(ctrlProvider); final connectivity = ref.watch(connectivityChangesProvider); diff --git a/lib/src/view/puzzle/puzzle_settings_screen.dart b/lib/src/view/puzzle/puzzle_settings_screen.dart index bcb2e0f28b..7f59b61d24 100644 --- a/lib/src/view/puzzle/puzzle_settings_screen.dart +++ b/lib/src/view/puzzle/puzzle_settings_screen.dart @@ -1,7 +1,6 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_preferences.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; @@ -14,13 +13,11 @@ class PuzzleSettingsScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final userId = ref.watch(authSessionProvider)?.user.id; - final isSoundEnabled = ref.watch( generalPreferencesProvider.select((pref) => pref.isSoundEnabled), ); final autoNext = ref.watch( - PuzzlePreferencesProvider(userId).select((value) => value.autoNext), + puzzlePreferencesProvider.select((value) => value.autoNext), ); final boardPrefs = ref.watch(boardPreferencesProvider); @@ -37,9 +34,7 @@ class PuzzleSettingsScreen extends ConsumerWidget { title: Text(context.l10n.puzzleJumpToNextPuzzleImmediately), value: autoNext, onChanged: (value) { - ref - .read(puzzlePreferencesProvider(userId).notifier) - .setAutoNext(value); + ref.read(puzzlePreferencesProvider.notifier).setAutoNext(value); }, ), SwitchSettingTile( diff --git a/test/model/challenge/challenge_service_test.dart b/test/model/challenge/challenge_service_test.dart index 99ce46e19d..80e6e5768e 100644 --- a/test/model/challenge/challenge_service_test.dart +++ b/test/model/challenge/challenge_service_test.dart @@ -5,6 +5,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:lichess_mobile/src/model/challenge/challenge.dart'; import 'package:lichess_mobile/src/model/challenge/challenge_service.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/common/game.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/speed.dart'; import 'package:lichess_mobile/src/model/notifications/notification_service.dart'; diff --git a/test/test_provider_scope.dart b/test/test_provider_scope.dart index 5854f7d834..185bc0c98f 100644 --- a/test/test_provider_scope.dart +++ b/test/test_provider_scope.dart @@ -112,6 +112,20 @@ Future makeProviderScope( }, ); + await binding.setInitialSharedPreferencesValues( + defaultPreferences ?? + { + // disable piece animation to simplify tests + 'preferences.board': jsonEncode( + Board.defaults + .copyWith( + pieceAnimation: false, + ) + .toJson(), + ), + }, + ); + final sharedPreferences = await SharedPreferences.getInstance(); FlutterSecureStorage.setMockInitialValues({ diff --git a/test/view/coordinate_training/coordinate_training_screen_test.dart b/test/view/coordinate_training/coordinate_training_screen_test.dart index 87e4f1cc99..d072ba1a59 100644 --- a/test/view/coordinate_training/coordinate_training_screen_test.dart +++ b/test/view/coordinate_training/coordinate_training_screen_test.dart @@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:lichess_mobile/src/model/coordinate_training/coordinate_training_controller.dart'; import 'package:lichess_mobile/src/model/coordinate_training/coordinate_training_preferences.dart'; +import 'package:lichess_mobile/src/model/settings/preferences.dart'; import 'package:lichess_mobile/src/view/coordinate_training/coordinate_training_screen.dart'; import '../../test_provider_scope.dart'; diff --git a/test/view/opening_explorer/opening_explorer_screen_test.dart b/test/view/opening_explorer/opening_explorer_screen_test.dart index af20b1a79f..0a5dd354ff 100644 --- a/test/view/opening_explorer/opening_explorer_screen_test.dart +++ b/test/view/opening_explorer/opening_explorer_screen_test.dart @@ -5,10 +5,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; +import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_preferences.dart'; +import 'package:lichess_mobile/src/model/settings/preferences.dart' as pref; +import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_screen.dart'; @@ -44,11 +48,17 @@ void main() { ); const name = 'John'; + final user = LightUser( id: UserId.fromUserName(name), name: name, ); + final session = AuthSessionState( + user: user, + token: 'test-token', + ); + group('OpeningExplorerScreen', () { testWidgets( 'master opening explorer loads', @@ -62,13 +72,6 @@ void main() { overrides: [ defaultClientProvider.overrideWithValue(mockClient), ], - defaultPreferences: { - OpeningExplorerPreferences.prefKey: jsonEncode( - OpeningExplorerPrefState.defaults() - .copyWith(db: OpeningDatabase.master) - .toJson(), - ), - }, ); await tester.pumpWidget(app); @@ -116,8 +119,11 @@ void main() { defaultClientProvider.overrideWithValue(mockClient), ], defaultPreferences: { - OpeningExplorerPreferences.prefKey: jsonEncode( - OpeningExplorerPrefState.defaults() + SessionPreferencesStorage.key( + pref.Category.openingExplorer.storageKey, + null, + ): jsonEncode( + OpeningExplorerPrefs.defaults() .copyWith(db: OpeningDatabase.lichess) .toJson(), ), @@ -166,9 +172,13 @@ void main() { overrides: [ defaultClientProvider.overrideWithValue(mockClient), ], + userSession: session, defaultPreferences: { - OpeningExplorerPreferences.prefKey: jsonEncode( - OpeningExplorerPrefState.defaults(user: user) + SessionPreferencesStorage.key( + pref.Category.openingExplorer.storageKey, + session, + ): jsonEncode( + OpeningExplorerPrefs.defaults(user: user) .copyWith(db: OpeningDatabase.player) .toJson(), ), From b7acea1ba7528d9980db9f4740f7e470ceb373d0 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 2 Oct 2024 14:30:56 +0200 Subject: [PATCH 410/979] Remove shared preferences provider --- lib/src/db/shared_preferences.dart | 12 ------------ lib/src/init.dart | 6 +----- lib/src/model/puzzle/puzzle_session.dart | 8 +++++--- lib/src/model/user/search_history.dart | 23 +++++++++++++++-------- test/test_container.dart | 8 -------- test/test_provider_scope.dart | 20 +------------------- 6 files changed, 22 insertions(+), 55 deletions(-) delete mode 100644 lib/src/db/shared_preferences.dart diff --git a/lib/src/db/shared_preferences.dart b/lib/src/db/shared_preferences.dart deleted file mode 100644 index 4848c53a3d..0000000000 --- a/lib/src/db/shared_preferences.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:lichess_mobile/src/init.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -part 'shared_preferences.g.dart'; - -@Riverpod(keepAlive: true) -SharedPreferences sharedPreferences(SharedPreferencesRef ref) { - // requireValue is possible because cachedDataProvider is loaded before - // anything. See: lib/src/app.dart - return ref.read(cachedDataProvider).requireValue.sharedPreferences; -} diff --git a/lib/src/init.dart b/lib/src/init.dart index c5f17c5ee1..9d254e7cfa 100644 --- a/lib/src/init.dart +++ b/lib/src/init.dart @@ -21,7 +21,6 @@ import 'package:logging/logging.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:pub_semver/pub_semver.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:shared_preferences/shared_preferences.dart'; part 'init.freezed.dart'; part 'init.g.dart'; @@ -37,7 +36,6 @@ Future cachedData(CachedDataRef ref) async { final sessionStorage = ref.watch(sessionStorageProvider); final pInfo = await PackageInfo.fromPlatform(); final deviceInfo = await DeviceInfoPlugin().deviceInfo; - final prefs = await SharedPreferences.getInstance(); final sri = await SecureStorage.instance.read(key: kSRIStorageKey) ?? genRandomString(12); @@ -48,7 +46,6 @@ Future cachedData(CachedDataRef ref) async { return CachedData( packageInfo: pInfo, deviceInfo: deviceInfo, - sharedPreferences: prefs, initialUserSession: await sessionStorage.read(), sri: sri, engineMaxMemoryInMb: engineMaxMemory, @@ -60,7 +57,6 @@ class CachedData with _$CachedData { const factory CachedData({ required PackageInfo packageInfo, required BaseDeviceInfo deviceInfo, - required SharedPreferences sharedPreferences, /// The user session read during app initialization. required AuthSessionState? initialUserSession, @@ -77,7 +73,7 @@ class CachedData with _$CachedData { /// Run initialization tasks only once on first app launch or after an update. Future setupFirstLaunch() async { - final prefs = await SharedPreferences.getInstance(); + final prefs = LichessBinding.instance.sharedPreferences; final pInfo = await PackageInfo.fromPlatform(); final appVersion = Version.parse(pInfo.version); diff --git a/lib/src/model/puzzle/puzzle_session.dart b/lib/src/model/puzzle/puzzle_session.dart index 031331dbda..5e1aee26ea 100644 --- a/lib/src/model/puzzle/puzzle_session.dart +++ b/lib/src/model/puzzle/puzzle_session.dart @@ -3,7 +3,7 @@ import 'dart:convert'; import 'package:collection/collection.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:lichess_mobile/src/db/shared_preferences.dart'; +import 'package:lichess_mobile/src/binding.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_angle.dart'; @@ -76,8 +76,10 @@ class PuzzleSession extends _$PuzzleSession { ); } - SharedPreferences get _store => ref.read(sharedPreferencesProvider); - String get _storageKey => 'puzzle_session.${userId ?? 'anon'}'; + SharedPreferencesWithCache get _store => + LichessBinding.instance.sharedPreferences; + + String get _storageKey => 'puzzle_session.${userId ?? '**anon**'}'; } @Freezed(fromJson: true, toJson: true) diff --git a/lib/src/model/user/search_history.dart b/lib/src/model/user/search_history.dart index 6ad5d6771e..52f2047cd4 100644 --- a/lib/src/model/user/search_history.dart +++ b/lib/src/model/user/search_history.dart @@ -2,21 +2,28 @@ import 'dart:convert'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:lichess_mobile/src/db/shared_preferences.dart'; +import 'package:lichess_mobile/src/binding.dart'; +import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:shared_preferences/shared_preferences.dart'; part 'search_history.g.dart'; part 'search_history.freezed.dart'; @riverpod class SearchHistory extends _$SearchHistory { - static const prefKey = 'search.history'; static const maxHistory = 10; + String _storageKey(AuthSessionState? session) => + 'search.history.${session?.user.id ?? '**anon**'}'; + + SharedPreferencesWithCache get _prefs => + LichessBinding.instance.sharedPreferences; + @override SearchHistoryState build() { - final prefs = ref.watch(sharedPreferencesProvider); - final stored = prefs.getString(prefKey); + final session = ref.watch(authSessionProvider); + final stored = _prefs.getString(_storageKey(session)); return stored != null ? SearchHistoryState.fromJson( @@ -35,15 +42,15 @@ class SearchHistory extends _$SearchHistory { } currentList.insert(0, term); final newState = SearchHistoryState(history: currentList.toIList()); - final prefs = ref.read(sharedPreferencesProvider); - await prefs.setString(prefKey, jsonEncode(newState.toJson())); + final session = ref.read(authSessionProvider); + await _prefs.setString(_storageKey(session), jsonEncode(newState.toJson())); state = newState; } Future clear() async { final newState = state.copyWith(history: IList()); - final prefs = ref.read(sharedPreferencesProvider); - await prefs.setString(prefKey, jsonEncode(newState.toJson())); + final prefKey = _storageKey(ref.read(authSessionProvider)); + await _prefs.setString(prefKey, jsonEncode(newState.toJson())); state = newState; } } diff --git a/test/test_container.dart b/test/test_container.dart index d58573d350..0c895e7a6d 100644 --- a/test/test_container.dart +++ b/test/test_container.dart @@ -8,7 +8,6 @@ import 'package:http/testing.dart'; import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/crashlytics.dart'; import 'package:lichess_mobile/src/db/database.dart'; -import 'package:lichess_mobile/src/db/shared_preferences.dart'; import 'package:lichess_mobile/src/init.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; @@ -18,7 +17,6 @@ import 'package:lichess_mobile/src/model/notifications/notification_service.dart import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:logging/logging.dart'; import 'package:package_info_plus/package_info_plus.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import './fake_crashlytics.dart'; @@ -54,9 +52,6 @@ Future makeContainer({ }) async { final binding = TestLichessBinding.ensureInitialized(); - SharedPreferences.setMockInitialValues({}); - final sharedPreferences = await SharedPreferences.getInstance(); - FlutterSecureStorage.setMockInitialValues({ kSRIStorageKey: 'test', }); @@ -98,7 +93,6 @@ Future makeContainer({ defaultClientProvider.overrideWithValue(testContainerMockClient), crashlyticsProvider.overrideWithValue(FakeCrashlytics()), soundServiceProvider.overrideWithValue(FakeSoundService()), - sharedPreferencesProvider.overrideWithValue(sharedPreferences), cachedDataProvider.overrideWith((ref) { return Future.value( CachedData( @@ -117,7 +111,6 @@ Future makeContainer({ 'identifierForVendor': 'test', 'isPhysicalDevice': true, }), - sharedPreferences: sharedPreferences, initialUserSession: userSession, sri: 'test', engineMaxMemoryInMb: 16, @@ -130,7 +123,6 @@ Future makeContainer({ addTearDown(binding.reset); addTearDown(container.dispose); - addTearDown(sharedPreferences.clear); // initialize the cached data provider await container.read(cachedDataProvider.future); diff --git a/test/test_provider_scope.dart b/test/test_provider_scope.dart index 185bc0c98f..6f7ff97c14 100644 --- a/test/test_provider_scope.dart +++ b/test/test_provider_scope.dart @@ -24,7 +24,6 @@ import 'package:lichess_mobile/src/model/settings/preferences.dart'; import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:logging/logging.dart'; import 'package:package_info_plus/package_info_plus.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:visibility_detector/visibility_detector.dart'; @@ -98,20 +97,6 @@ Future makeProviderScope( VisibilityDetectorController.instance.updateInterval = Duration.zero; - SharedPreferences.setMockInitialValues( - defaultPreferences ?? - { - // disable piece animation to simplify tests - 'preferences.board': jsonEncode( - Board.defaults - .copyWith( - pieceAnimation: false, - ) - .toJson(), - ), - }, - ); - await binding.setInitialSharedPreferencesValues( defaultPreferences ?? { @@ -126,8 +111,6 @@ Future makeProviderScope( }, ); - final sharedPreferences = await SharedPreferences.getInstance(); - FlutterSecureStorage.setMockInitialValues({ kSRIStorageKey: 'test', if (userSession != null) @@ -186,7 +169,7 @@ Future makeProviderScope( // ignore: scoped_providers_should_specify_dependencies soundServiceProvider.overrideWithValue(FakeSoundService()), // ignore: scoped_providers_should_specify_dependencies - cachedDataProvider.overrideWith((ref) { + cachedDataProvider.overrideWith((ref) async { return CachedData( packageInfo: PackageInfo( appName: 'lichess_mobile_test', @@ -203,7 +186,6 @@ Future makeProviderScope( 'identifierForVendor': 'test', 'isPhysicalDevice': true, }), - sharedPreferences: sharedPreferences, initialUserSession: userSession, sri: 'test', engineMaxMemoryInMb: 16, From ef38388a23446756d057c821a12a8d6a76ec0e21 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 2 Oct 2024 15:44:25 +0200 Subject: [PATCH 411/979] Remove cached data provider --- lib/main.dart | 4 +- lib/src/app.dart | 60 +--------------- lib/src/binding.dart | 70 +++++++++++++++++++ lib/src/init.dart | 54 -------------- lib/src/model/auth/auth_session.dart | 8 +-- lib/src/model/common/http.dart | 16 ++--- lib/src/model/common/socket.dart | 24 ++----- lib/src/model/engine/evaluation_service.dart | 5 +- lib/src/model/lobby/create_game_service.dart | 5 +- .../notifications/notification_service.dart | 32 ++++----- lib/src/utils/device_info.dart | 12 ---- lib/src/utils/package_info.dart | 12 ---- .../view/settings/settings_tab_screen.dart | 4 +- test/app_test.dart | 47 ++++++++++++- test/binding.dart | 41 +++++++++++ test/model/game/game_test.dart | 8 +-- test/test_container.dart | 32 +-------- test/test_provider_scope.dart | 28 +------- 18 files changed, 203 insertions(+), 259 deletions(-) delete mode 100644 lib/src/utils/device_info.dart delete mode 100644 lib/src/utils/package_info.dart diff --git a/lib/main.dart b/lib/main.dart index 29d97a262b..b27fa377a6 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -24,6 +24,8 @@ Future main() async { await lichessBinding.preloadSharedPreferences(); + await lichessBinding.preloadData(); + // Show splash screen until app is ready // See src/app.dart for splash screen removal FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); @@ -47,7 +49,7 @@ Future main() async { observers: [ ProviderLogger(), ], - child: const AppInitializationScreen(), + child: const Application(), ), ); } diff --git a/lib/src/app.dart b/lib/src/app.dart index 8d5771a912..11f86677b5 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -7,7 +7,6 @@ import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/l10n/l10n.dart'; import 'package:lichess_mobile/src/constants.dart'; -import 'package:lichess_mobile/src/init.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/challenge/challenge_service.dart'; @@ -22,63 +21,6 @@ import 'package:lichess_mobile/src/navigation.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; -import 'package:lichess_mobile/src/utils/system.dart'; - -/// Application initialization and main entry point. -class AppInitializationScreen extends ConsumerWidget { - const AppInitializationScreen({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - ref.listen>( - cachedDataProvider, - (_, state) { - if (state.hasValue || state.hasError) { - FlutterNativeSplash.remove(); - } - }, - ); - - final result = ref.watch(cachedDataProvider); - - if (result.isLoading) { - // loading screen is handled by the native splash screen - return const SizedBox.shrink(key: Key('app_splash_screen')); - } else if (result.hasError) { - // We should really do everything we can to avoid this screen - // but in last resort, let's show an error message and invite the - // user to clear app data. - return MaterialApp( - home: Scaffold( - body: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Padding( - padding: EdgeInsets.all(16.0), - child: Text( - "Something went wrong :'(\n\nIf the problem persists, you can try to clear the storage and restart the application.\n\nSorry for the inconvenience.", - textAlign: TextAlign.center, - style: TextStyle(fontSize: 18.0), - ), - ), - const SizedBox(height: 16.0), - if (Theme.of(context).platform == TargetPlatform.android) - ElevatedButton( - onPressed: () { - System.instance.clearUserData(); - }, - child: const Text('Clear storage'), - ), - ], - ), - ), - ); - } else { - return const Application(); - } - } -} /// The main application widget. /// @@ -99,7 +41,7 @@ class _AppState extends ConsumerState { @override void initState() { - debugPrint('AppState init'); + FlutterNativeSplash.remove(); _appLifecycleListener = AppLifecycleListener( onResume: () async { diff --git a/lib/src/binding.dart b/lib/src/binding.dart index ce007cffa9..54a0480447 100644 --- a/lib/src/binding.dart +++ b/lib/src/binding.dart @@ -1,11 +1,21 @@ +import 'dart:convert'; + +import 'package:device_info_plus/device_info_plus.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:lichess_mobile/firebase_options.dart'; import 'package:lichess_mobile/l10n/l10n.dart'; +import 'package:lichess_mobile/src/db/secure_storage.dart'; +import 'package:lichess_mobile/src/model/auth/auth_session.dart'; +import 'package:lichess_mobile/src/model/auth/session_storage.dart'; +import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/notifications/notification_service.dart'; import 'package:lichess_mobile/src/model/notifications/notifications.dart'; +import 'package:lichess_mobile/src/utils/string.dart'; +import 'package:lichess_mobile/src/utils/system.dart'; +import 'package:package_info_plus/package_info_plus.dart'; import 'package:shared_preferences/shared_preferences.dart'; /// A singleton class that provides access to plugins and external APIs. @@ -61,6 +71,23 @@ abstract class LichessBinding { /// have not yet been initialized. SharedPreferencesWithCache get sharedPreferences; + /// Application package information. + PackageInfo get packageInfo; + + /// Device information. + BaseDeviceInfo get deviceInfo; + + /// The user session read during app initialization. + AuthSessionState? get initialUserSession; + + /// Socket Random Identifier. + String get sri; + + /// Maximum memory in MB that the engine can use. + /// + /// This is 10% of the total physical memory. + int get engineMaxMemoryInMb; + /// Initialize notifications. /// /// This wraps [Firebase.initializeApp] and [FlutterLocalNotificationsPlugin.initialize]. @@ -128,6 +155,49 @@ class AppLichessBinding extends LichessBinding { _syncSharedPreferencesWithCache = await _sharedPreferencesWithCache; } + late PackageInfo _syncPackageInfo; + late BaseDeviceInfo _syncDeviceInfo; + AuthSessionState? _syncInitialUserSession; + late String _syncSri; + late int _syncEngineMaxMemoryInMb; + + @override + PackageInfo get packageInfo => _syncPackageInfo; + + @override + BaseDeviceInfo get deviceInfo => _syncDeviceInfo; + + @override + AuthSessionState? get initialUserSession => _syncInitialUserSession; + + @override + String get sri => _syncSri; + + @override + int get engineMaxMemoryInMb => _syncEngineMaxMemoryInMb; + + /// Preload useful data. + /// + /// This must be called only once before the app starts. + Future preloadData() async { + _syncPackageInfo = await PackageInfo.fromPlatform(); + _syncDeviceInfo = await DeviceInfoPlugin().deviceInfo; + + final string = await SecureStorage.instance.read(key: kSessionStorageKey); + if (string != null) { + _syncInitialUserSession = AuthSessionState.fromJson( + jsonDecode(string) as Map, + ); + } + + _syncSri = await SecureStorage.instance.read(key: kSRIStorageKey) ?? + genRandomString(12); + + final physicalMemory = await System.instance.getTotalRam() ?? 256.0; + final engineMaxMemory = (physicalMemory / 10).ceil(); + _syncEngineMaxMemoryInMb = engineMaxMemory; + } + @override Future initializeNotifications(Locale locale) async { await Firebase.initializeApp( diff --git a/lib/src/init.dart b/lib/src/init.dart index 9d254e7cfa..949801f63d 100644 --- a/lib/src/init.dart +++ b/lib/src/init.dart @@ -1,76 +1,22 @@ import 'dart:convert'; -import 'package:device_info_plus/device_info_plus.dart'; import 'package:dynamic_color/dynamic_color.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_displaymode/flutter_displaymode.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/binding.dart'; import 'package:lichess_mobile/src/db/secure_storage.dart'; -import 'package:lichess_mobile/src/model/auth/auth_session.dart'; -import 'package:lichess_mobile/src/model/auth/session_storage.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/settings/preferences.dart' as pref; import 'package:lichess_mobile/src/utils/color_palette.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/utils/string.dart'; -import 'package:lichess_mobile/src/utils/system.dart'; import 'package:logging/logging.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:pub_semver/pub_semver.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'init.freezed.dart'; -part 'init.g.dart'; final _logger = Logger('Init'); -/// A provider that caches useful data. -/// -/// This provider is meant to be called once during app initialization, after -/// the provider scope has been created. -@Riverpod(keepAlive: true) -Future cachedData(CachedDataRef ref) async { - final sessionStorage = ref.watch(sessionStorageProvider); - final pInfo = await PackageInfo.fromPlatform(); - final deviceInfo = await DeviceInfoPlugin().deviceInfo; - - final sri = await SecureStorage.instance.read(key: kSRIStorageKey) ?? - genRandomString(12); - - final physicalMemory = await System.instance.getTotalRam() ?? 256.0; - final engineMaxMemory = (physicalMemory / 10).ceil(); - - return CachedData( - packageInfo: pInfo, - deviceInfo: deviceInfo, - initialUserSession: await sessionStorage.read(), - sri: sri, - engineMaxMemoryInMb: engineMaxMemory, - ); -} - -@freezed -class CachedData with _$CachedData { - const factory CachedData({ - required PackageInfo packageInfo, - required BaseDeviceInfo deviceInfo, - - /// The user session read during app initialization. - required AuthSessionState? initialUserSession, - - /// Socket Random Identifier. - required String sri, - - /// Maximum memory in MB that the engine can use. - /// - /// This is 10% of the total physical memory. - required int engineMaxMemoryInMb, - }) = _CachedData; -} - /// Run initialization tasks only once on first app launch or after an update. Future setupFirstLaunch() async { final prefs = LichessBinding.instance.sharedPreferences; diff --git a/lib/src/model/auth/auth_session.dart b/lib/src/model/auth/auth_session.dart index 352345d459..c53e863607 100644 --- a/lib/src/model/auth/auth_session.dart +++ b/lib/src/model/auth/auth_session.dart @@ -1,5 +1,5 @@ import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:lichess_mobile/src/init.dart'; +import 'package:lichess_mobile/src/binding.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -12,11 +12,7 @@ part 'auth_session.g.dart'; class AuthSession extends _$AuthSession { @override AuthSessionState? build() { - // requireValue is possible because cachedDataProvider is loaded before - // anything. See: lib/src/app.dart - return ref.watch( - cachedDataProvider.select((data) => data.requireValue.initialUserSession), - ); + return LichessBinding.instance.initialUserSession; } Future update(AuthSessionState session) async { diff --git a/lib/src/model/common/http.dart b/lib/src/model/common/http.dart index 2a05a871a8..d3ad3f73aa 100644 --- a/lib/src/model/common/http.dart +++ b/lib/src/model/common/http.dart @@ -21,13 +21,11 @@ import 'package:http/http.dart' StreamedResponse; import 'package:http/io_client.dart'; import 'package:http/retry.dart'; +import 'package:lichess_mobile/src/binding.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/auth/bearer.dart'; -import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; -import 'package:lichess_mobile/src/utils/device_info.dart'; -import 'package:lichess_mobile/src/utils/package_info.dart'; import 'package:logging/logging.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -107,9 +105,9 @@ String userAgent(UserAgentRef ref) { final session = ref.watch(authSessionProvider); return makeUserAgent( - ref.read(packageInfoProvider), - ref.read(deviceInfoProvider), - ref.read(sriProvider), + LichessBinding.instance.packageInfo, + LichessBinding.instance.deviceInfo, + LichessBinding.instance.sri, session?.user, ); } @@ -167,9 +165,9 @@ class LichessClient implements Client { request.headers['Authorization'] = 'Bearer $bearer'; } request.headers['User-Agent'] = makeUserAgent( - _ref.read(packageInfoProvider), - _ref.read(deviceInfoProvider), - _ref.read(sriProvider), + LichessBinding.instance.packageInfo, + LichessBinding.instance.deviceInfo, + LichessBinding.instance.sri, session?.user, ); diff --git a/lib/src/model/common/socket.dart b/lib/src/model/common/socket.dart index c7aeb7f74f..34c28f223b 100644 --- a/lib/src/model/common/socket.dart +++ b/lib/src/model/common/socket.dart @@ -7,13 +7,11 @@ import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:lichess_mobile/src/binding.dart'; import 'package:lichess_mobile/src/constants.dart'; -import 'package:lichess_mobile/src/init.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/auth/bearer.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; -import 'package:lichess_mobile/src/utils/device_info.dart'; -import 'package:lichess_mobile/src/utils/package_info.dart'; import 'package:logging/logging.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -456,11 +454,11 @@ class SocketPool { // Create a default socket client. This one is never disposed. final client = SocketClient( _currentRoute, - sri: _ref.read(sriProvider), + sri: LichessBinding.instance.sri, channelFactory: _ref.read(webSocketChannelFactoryProvider), getSession: () => _ref.read(authSessionProvider), - packageInfo: _ref.read(packageInfoProvider), - deviceInfo: _ref.read(deviceInfoProvider), + packageInfo: LichessBinding.instance.packageInfo, + deviceInfo: LichessBinding.instance.deviceInfo, pingDelay: const Duration(seconds: 25), ); @@ -511,9 +509,9 @@ class SocketPool { route, channelFactory: _ref.read(webSocketChannelFactoryProvider), getSession: () => _ref.read(authSessionProvider), - packageInfo: _ref.read(packageInfoProvider), - deviceInfo: _ref.read(deviceInfoProvider), - sri: _ref.read(sriProvider), + packageInfo: LichessBinding.instance.packageInfo, + deviceInfo: LichessBinding.instance.deviceInfo, + sri: LichessBinding.instance.sri, onStreamListen: () { _disposeTimers[route]?.cancel(); }, @@ -602,14 +600,6 @@ SocketPool socketPool(SocketPoolRef ref) { return pool; } -/// Socket Random Identifier. -@Riverpod(keepAlive: true) -String sri(SriRef ref) { - // requireValue is possible because cachedDataProvider is loaded before - // anything. See: lib/src/app.dart - return ref.read(cachedDataProvider).requireValue.sri; -} - /// Average lag computed from WebSocket ping/pong protocol. @riverpod class AverageLag extends _$AverageLag { diff --git a/lib/src/model/engine/evaluation_service.dart b/lib/src/model/engine/evaluation_service.dart index b7e529fa7e..abeaa041a7 100644 --- a/lib/src/model/engine/evaluation_service.dart +++ b/lib/src/model/engine/evaluation_service.dart @@ -6,7 +6,7 @@ import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/foundation.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:lichess_mobile/src/init.dart'; +import 'package:lichess_mobile/src/binding.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/eval.dart'; import 'package:lichess_mobile/src/model/common/uci.dart'; @@ -178,8 +178,7 @@ class EvaluationService { EvaluationService evaluationService(EvaluationServiceRef ref) { // requireValue is possible because cachedDataProvider is loaded before // anything. See: lib/src/app.dart - final maxMemory = - ref.read(cachedDataProvider).requireValue.engineMaxMemoryInMb; + final maxMemory = LichessBinding.instance.engineMaxMemoryInMb; final service = EvaluationService(ref, maxMemory: maxMemory); ref.onDispose(() { diff --git a/lib/src/model/lobby/create_game_service.dart b/lib/src/model/lobby/create_game_service.dart index 177bbe26b4..cb2fd64c7f 100644 --- a/lib/src/model/lobby/create_game_service.dart +++ b/lib/src/model/lobby/create_game_service.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:deep_pick/deep_pick.dart'; +import 'package:lichess_mobile/src/binding.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/challenge/challenge.dart'; import 'package:lichess_mobile/src/model/challenge/challenge_repository.dart'; @@ -105,7 +106,7 @@ class CreateGameService { await ref.withClient( (client) => LobbyRepository(client).createSeek( seek, - sri: ref.read(sriProvider), + sri: LichessBinding.instance.sri, ), ); } @@ -187,7 +188,7 @@ class CreateGameService { /// Cancel the current game creation. Future cancelSeek() async { _log.info('Cancelling game creation'); - final sri = ref.read(sriProvider); + final sri = LichessBinding.instance.sri; try { await LobbyRepository(lichessClient).cancelSeek(sri: sri); } catch (e) { diff --git a/lib/src/model/notifications/notification_service.dart b/lib/src/model/notifications/notification_service.dart index f73ff7be2d..41be595dd0 100644 --- a/lib/src/model/notifications/notification_service.dart +++ b/lib/src/model/notifications/notification_service.dart @@ -6,7 +6,6 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/l10n/l10n.dart'; import 'package:lichess_mobile/src/binding.dart'; -import 'package:lichess_mobile/src/init.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/challenge/challenge_service.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; @@ -343,25 +342,20 @@ class NotificationService { // create a new provider scope for the background isolate final ref = ProviderContainer(); - ref.listen( - cachedDataProvider, - (prev, now) async { - if (!now.hasValue) return; + final lichessBinding = AppLichessBinding.ensureInitialized(); + await lichessBinding.preloadSharedPreferences(); + await lichessBinding.preloadData(); - try { - await ref.read(notificationServiceProvider)._processFcmMessage( - message, - fromBackground: true, - ); - - ref.dispose(); - } catch (e) { - _logger.severe('Error when processing an FCM background message: $e'); - ref.dispose(); - } - }, - ); + try { + await ref.read(notificationServiceProvider)._processFcmMessage( + message, + fromBackground: true, + ); - ref.read(cachedDataProvider); + ref.dispose(); + } catch (e) { + _logger.severe('Error when processing an FCM background message: $e'); + ref.dispose(); + } } } diff --git a/lib/src/utils/device_info.dart b/lib/src/utils/device_info.dart deleted file mode 100644 index 0c5823dba7..0000000000 --- a/lib/src/utils/device_info.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:device_info_plus/device_info_plus.dart'; -import 'package:lichess_mobile/src/init.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'device_info.g.dart'; - -@Riverpod(keepAlive: true) -BaseDeviceInfo deviceInfo(DeviceInfoRef ref) { - // requireValue is possible because cachedDataProvider is loaded before - // anything. See: lib/src/app.dart - return ref.read(cachedDataProvider).requireValue.deviceInfo; -} diff --git a/lib/src/utils/package_info.dart b/lib/src/utils/package_info.dart deleted file mode 100644 index 9180cd81d2..0000000000 --- a/lib/src/utils/package_info.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:lichess_mobile/src/init.dart'; -import 'package:package_info_plus/package_info_plus.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'package_info.g.dart'; - -@Riverpod(keepAlive: true) -PackageInfo packageInfo(PackageInfoRef ref) { - // requireValue is possible because cachedDataProvider is loaded before - // anything. See: lib/src/app.dart - return ref.read(cachedDataProvider).requireValue.packageInfo; -} diff --git a/lib/src/view/settings/settings_tab_screen.dart b/lib/src/view/settings/settings_tab_screen.dart index dc12828b6d..03b2d566cc 100644 --- a/lib/src/view/settings/settings_tab_screen.dart +++ b/lib/src/view/settings/settings_tab_screen.dart @@ -2,6 +2,7 @@ import 'package:app_settings/app_settings.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/binding.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/db/database.dart'; import 'package:lichess_mobile/src/model/auth/auth_controller.dart'; @@ -14,7 +15,6 @@ import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; -import 'package:lichess_mobile/src/utils/package_info.dart'; import 'package:lichess_mobile/src/utils/system.dart'; import 'package:lichess_mobile/src/view/account/profile_screen.dart'; import 'package:lichess_mobile/src/view/settings/app_background_mode_screen.dart'; @@ -92,7 +92,7 @@ class _Body extends ConsumerWidget { final boardPrefs = ref.watch(boardPreferencesProvider); final authController = ref.watch(authControllerProvider); final userSession = ref.watch(authSessionProvider); - final packageInfo = ref.watch(packageInfoProvider); + final packageInfo = LichessBinding.instance.packageInfo; final dbSize = ref.watch(getDbSizeInBytesProvider); final androidVersionAsync = ref.watch(androidVersionProvider); diff --git a/test/app_test.dart b/test/app_test.dart index 5015f30b96..6f9cfceefa 100644 --- a/test/app_test.dart +++ b/test/app_test.dart @@ -1,18 +1,59 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:lichess_mobile/src/app.dart'; +import 'package:lichess_mobile/src/navigation.dart'; import 'test_provider_scope.dart'; void main() { - testWidgets('App initialization screen', (tester) async { + testWidgets('App loads', (tester) async { final app = await makeProviderScope( tester, - child: const AppInitializationScreen(), + child: const Application(), ); await tester.pumpWidget(app); - expect(find.byKey(const Key('app_splash_screen')), findsOneWidget); + expect(find.byType(MaterialApp), findsOneWidget); + }); + + testWidgets('App loads with system theme, which defaults to light', + (tester) async { + final app = await makeProviderScope( + tester, + child: const Application(), + ); + + await tester.pumpWidget(app); + + expect( + Theme.of(tester.element(find.byType(MaterialApp))).brightness, + Brightness.light, + ); + }); + + testWidgets('Bottom navigation', (tester) async { + final app = await makeProviderScope( + tester, + child: const Application(), + ); + + await tester.pumpWidget(app); + + expect(find.byType(BottomNavScaffold), findsOneWidget); + + if (defaultTargetPlatform == TargetPlatform.iOS) { + expect(find.byType(BottomNavigationBarItem), findsNWidgets(5)); + } else { + expect(find.byType(NavigationDestination), findsNWidgets(5)); + } + + expect(find.text('Home'), findsOneWidget); + expect(find.text('Puzzles'), findsOneWidget); + expect(find.text('Tools'), findsOneWidget); + expect(find.text('Watch'), findsOneWidget); + expect(find.text('Settings'), findsOneWidget); }); } diff --git a/test/binding.dart b/test/binding.dart index eb9661d27a..61509c6a85 100644 --- a/test/binding.dart +++ b/test/binding.dart @@ -1,9 +1,12 @@ import 'dart:async'; import 'dart:ui'; +import 'package:device_info_plus/device_info_plus.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:lichess_mobile/src/binding.dart'; +import 'package:lichess_mobile/src/model/auth/auth_session.dart'; +import 'package:package_info_plus/package_info_plus.dart'; import 'package:shared_preferences/shared_preferences.dart'; /// The binding instance used in tests. @@ -35,6 +38,43 @@ class TestLichessBinding extends LichessBinding { _instance = this; } + /// Preload useful data. + /// + /// This should be called only once before the app starts. + Future preloadData(AuthSessionState? initialUserSession) async { + _initialUserSession = initialUserSession; + } + + AuthSessionState? _initialUserSession; + + @override + BaseDeviceInfo get deviceInfo => BaseDeviceInfo({ + 'name': 'test', + 'model': 'test', + 'manufacturer': 'test', + 'systemName': 'test', + 'systemVersion': 'test', + 'identifierForVendor': 'test', + 'isPhysicalDevice': true, + }); + + @override + int get engineMaxMemoryInMb => 256; + + @override + AuthSessionState? get initialUserSession => _initialUserSession; + + @override + PackageInfo get packageInfo => PackageInfo( + appName: 'lichess_mobile_test', + version: 'test', + buildNumber: '0.0.0', + packageName: 'lichess_mobile_test', + ); + + @override + String get sri => 'test-sri'; + /// Set the initial values for shared preferences. Future setInitialSharedPreferencesValues( Map values, @@ -76,6 +116,7 @@ class TestLichessBinding extends LichessBinding { void reset() { _firebaseMessaging = null; _sharedPreferences = null; + _initialUserSession = null; } FakeFirebaseMessaging? _firebaseMessaging; diff --git a/test/model/game/game_test.dart b/test/model/game/game_test.dart index 93dd1132c4..7a27de8f80 100644 --- a/test/model/game/game_test.dart +++ b/test/model/game/game_test.dart @@ -15,7 +15,7 @@ void main() { game.makePgn(), ''' [Event "Rated Bullet game"] -[Site "http://localhost:9663/Fn9UvVKF"] +[Site "https://lichess.dev/Fn9UvVKF"] [Date "2024.01.25"] [White "chabrot"] [Black "veloce"] @@ -38,7 +38,7 @@ void main() { game.makePgn(), ''' [Event "Rated Bullet game"] -[Site "http://localhost:9663/CCW6EEru"] +[Site "https://lichess.dev/CCW6EEru"] [Date "2024.01.25"] [White "veloce"] [Black "chabrot"] @@ -65,7 +65,7 @@ void main() { game.makePgn(), ''' [Event "Rated Bullet game"] -[Site "http://localhost:9663/CCW6EEru"] +[Site "https://lichess.dev/CCW6EEru"] [Date "2024.01.25"] [White "veloce"] [Black "chabrot"] @@ -92,7 +92,7 @@ void main() { game.makePgn(), ''' [Event "Rated Bullet game"] -[Site "http://localhost:9663/CCW6EEru"] +[Site "https://lichess.dev/CCW6EEru"] [Date "2024.01.25"] [White "veloce"] [Black "chabrot"] diff --git a/test/test_container.dart b/test/test_container.dart index 0c895e7a6d..9e21f5ff16 100644 --- a/test/test_container.dart +++ b/test/test_container.dart @@ -1,4 +1,3 @@ -import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; @@ -8,7 +7,6 @@ import 'package:http/testing.dart'; import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/crashlytics.dart'; import 'package:lichess_mobile/src/db/database.dart'; -import 'package:lichess_mobile/src/init.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; @@ -16,7 +14,6 @@ import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/notifications/notification_service.dart'; import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:logging/logging.dart'; -import 'package:package_info_plus/package_info_plus.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import './fake_crashlytics.dart'; @@ -56,6 +53,8 @@ Future makeContainer({ kSRIStorageKey: 'test', }); + await binding.preloadData(userSession); + Logger.root.onRecord.listen((record) { if (shouldLog && record.level >= Level.FINE) { final time = DateFormat.Hms().format(record.time); @@ -93,30 +92,6 @@ Future makeContainer({ defaultClientProvider.overrideWithValue(testContainerMockClient), crashlyticsProvider.overrideWithValue(FakeCrashlytics()), soundServiceProvider.overrideWithValue(FakeSoundService()), - cachedDataProvider.overrideWith((ref) { - return Future.value( - CachedData( - packageInfo: PackageInfo( - appName: 'lichess_mobile_test', - version: 'test', - buildNumber: '0.0.0', - packageName: 'lichess_mobile_test', - ), - deviceInfo: BaseDeviceInfo({ - 'name': 'test', - 'model': 'test', - 'manufacturer': 'test', - 'systemName': 'test', - 'systemVersion': 'test', - 'identifierForVendor': 'test', - 'isPhysicalDevice': true, - }), - initialUserSession: userSession, - sri: 'test', - engineMaxMemoryInMb: 16, - ), - ); - }), ...overrides ?? [], ], ); @@ -124,8 +99,5 @@ Future makeContainer({ addTearDown(binding.reset); addTearDown(container.dispose); - // initialize the cached data provider - await container.read(cachedDataProvider.future); - return container; } diff --git a/test/test_provider_scope.dart b/test/test_provider_scope.dart index 6f7ff97c14..88f11ac292 100644 --- a/test/test_provider_scope.dart +++ b/test/test_provider_scope.dart @@ -1,6 +1,5 @@ import 'dart:convert'; -import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -12,7 +11,6 @@ import 'package:intl/intl.dart'; import 'package:lichess_mobile/l10n/l10n.dart'; import 'package:lichess_mobile/src/crashlytics.dart'; import 'package:lichess_mobile/src/db/database.dart'; -import 'package:lichess_mobile/src/init.dart'; import 'package:lichess_mobile/src/model/account/account_preferences.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/auth/session_storage.dart'; @@ -23,7 +21,6 @@ import 'package:lichess_mobile/src/model/notifications/notification_service.dart import 'package:lichess_mobile/src/model/settings/preferences.dart'; import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:logging/logging.dart'; -import 'package:package_info_plus/package_info_plus.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:visibility_detector/visibility_detector.dart'; @@ -111,6 +108,8 @@ Future makeProviderScope( }, ); + await binding.preloadData(userSession); + FlutterSecureStorage.setMockInitialValues({ kSRIStorageKey: 'test', if (userSession != null) @@ -168,29 +167,6 @@ Future makeProviderScope( crashlyticsProvider.overrideWithValue(FakeCrashlytics()), // ignore: scoped_providers_should_specify_dependencies soundServiceProvider.overrideWithValue(FakeSoundService()), - // ignore: scoped_providers_should_specify_dependencies - cachedDataProvider.overrideWith((ref) async { - return CachedData( - packageInfo: PackageInfo( - appName: 'lichess_mobile_test', - version: 'test', - buildNumber: '0.0.0', - packageName: 'lichess_mobile_test', - ), - deviceInfo: BaseDeviceInfo({ - 'name': 'test', - 'model': 'test', - 'manufacturer': 'test', - 'systemName': 'test', - 'systemVersion': 'test', - 'identifierForVendor': 'test', - 'isPhysicalDevice': true, - }), - initialUserSession: userSession, - sri: 'test', - engineMaxMemoryInMb: 16, - ); - }), ...overrides ?? [], ], child: MediaQuery( From af1328c64a1680690a8b664abb94321d813b0dce Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 2 Oct 2024 15:44:39 +0200 Subject: [PATCH 412/979] Target lichess.dev server by default now --- docs/setting_dev_env.md | 31 ++++++++++++++++++------------- lib/src/constants.dart | 4 ++-- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/docs/setting_dev_env.md b/docs/setting_dev_env.md index 70a5bcdf81..86a16217e4 100644 --- a/docs/setting_dev_env.md +++ b/docs/setting_dev_env.md @@ -6,16 +6,16 @@ If you get stuck during the installation process the most suitable place to seek ## Installing Flutter -This project uses Flutter. +This project uses Flutter. 1. Follow [the flutter install guide](https://docs.flutter.dev/get-started/install). This project is meant to run on iOS and Android, so you need to follow the "Platform setup" section of that guide to install the iOS and/or Android platform. -> [!WARNING] -> Installing on Linux using `snapd` might cause some [problems](../../issues/123) building stockfish. +> [!WARNING] +> Installing on Linux using `snapd` might cause some [problems](../../issues/123) building stockfish. > Installing flutter manually is a known workaround. 2. Switch to the beta channel by running `flutter channel beta` and `flutter upgrade` -> [!NOTE] +> [!NOTE] > We'll use Flutter's `beta` channel while the app itself is in Beta. 3. Ensure Flutter is correctly configured by running `flutter doctor` @@ -29,9 +29,13 @@ If you want to use FVM to manage your Flutter versions effectively, please consu ## Lila Server -During development, you will need a local [lila](https://github.com/lichess-org/lila) (lichess server scala app) -instance to work on this project. You will also need to setup [lila-ws](https://github.com/lichess-org/lila-ws) -(websocket server). +By default, the app will target the [Lichess dev server](https://lichess.dev/), +so you can start developing without setting up a local server. + +During development, you may need a local [lila](https://github.com/lichess-org/lila) (lichess server scala app) +instance to work on this project. + +If you work with a local lila, you will also need to setup [lila-ws](https://github.com/lichess-org/lila-ws) (websocket server). ### lila-docker @@ -43,16 +47,17 @@ Instructions to install both `lila` and `lila-ws` locally can be found in [the l The mobile application is configured by default to target `http://127.0.0.1:9663` and `ws://127.0.0.1:9664`, so keep these when installing lila. -### Using Lichess dev server +**Do not use any scheme (https:// or ws://) in url in host, since it's already handled by URI helper methods** -To use the [Lichess dev](https://lichess.dev/) to run this app, run the following command to set up the Lichess host -URLs in the app. +To run the application with a local server, you can use the following command: -``` -flutter run --dart-define=LICHESS_HOST=lichess.dev --dart-define=LICHESS_WS_HOST=socket.lichess.dev +```bash +flutter run --dart-define=LICHESS_HOST=localhost.9663 --dart-define=LICHESS_WS_HOST=localhost:9664 ``` -**Note : Do not use any scheme (https:// or ws://) in url in host, since it's already handled by URI helper methods** +> [!NOTE] +> The hosts above are the default ports for lila, if you have changed them, you +will need to adjust the command accordingly. ## Setting up the emulators diff --git a/lib/src/constants.dart b/lib/src/constants.dart index da60c02e8f..638c471c32 100644 --- a/lib/src/constants.dart +++ b/lib/src/constants.dart @@ -3,12 +3,12 @@ import 'package:flutter/material.dart'; const kLichessHost = String.fromEnvironment( 'LICHESS_HOST', - defaultValue: 'localhost:9663', + defaultValue: 'lichess.dev', ); const kLichessWSHost = String.fromEnvironment( 'LICHESS_WS_HOST', - defaultValue: 'localhost:9664', + defaultValue: 'socket.lichess.dev', ); const kLichessWSSecret = String.fromEnvironment( From 86d21e75f00fd189214ff1703ca1e58aa3597308 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 2 Oct 2024 17:40:16 +0200 Subject: [PATCH 413/979] Define preferences in model directories --- lib/src/init.dart | 9 +- lib/src/intl.dart | 9 +- .../model/analysis/analysis_preferences.dart | 40 +- .../challenge/challenge_preferences.dart | 66 +- .../model/common/service/sound_service.dart | 14 +- .../coordinate_training_preferences.dart | 71 +- lib/src/model/game/game.dart | 8 +- lib/src/model/game/game_preferences.dart | 25 +- lib/src/model/game/playable_game.dart | 6 +- lib/src/model/lobby/game_seek.dart | 15 +- .../model/lobby/game_setup_preferences.dart | 74 +- .../opening_explorer_preferences.dart | 3 +- lib/src/model/puzzle/puzzle_preferences.dart | 27 +- lib/src/model/settings/board_preferences.dart | 211 +++++- .../model/settings/general_preferences.dart | 82 ++- lib/src/model/settings/home_preferences.dart | 41 +- .../settings/over_the_board_preferences.dart | 32 +- lib/src/model/settings/preferences.dart | 678 ++---------------- .../model/settings/preferences_storage.dart | 4 +- .../coordinate_training_screen.dart | 1 - lib/src/view/game/game_screen_providers.dart | 2 +- lib/src/view/home/home_tab_screen.dart | 17 +- .../view/play/create_custom_game_screen.dart | 1 - lib/src/view/settings/board_theme_screen.dart | 1 - .../view/settings/sound_settings_screen.dart | 1 - lib/src/view/settings/theme_screen.dart | 1 - test/app_test.dart | 1 - .../common/service/fake_sound_service.dart | 2 +- test/test_provider_scope.dart | 4 +- .../coordinate_training_screen_test.dart | 1 - .../opening_explorer_screen_test.dart | 6 +- 31 files changed, 732 insertions(+), 721 deletions(-) diff --git a/lib/src/init.dart b/lib/src/init.dart index 949801f63d..ea443f4ccc 100644 --- a/lib/src/init.dart +++ b/lib/src/init.dart @@ -7,7 +7,8 @@ import 'package:flutter_displaymode/flutter_displaymode.dart'; import 'package:lichess_mobile/src/binding.dart'; import 'package:lichess_mobile/src/db/secure_storage.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; -import 'package:lichess_mobile/src/model/settings/preferences.dart' as pref; +import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; +import 'package:lichess_mobile/src/model/settings/preferences.dart'; import 'package:lichess_mobile/src/utils/color_palette.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/utils/string.dart'; @@ -66,11 +67,11 @@ Future androidDisplayInitialization(WidgetsBinding widgetsBinding) async { setCorePalette(value); if (getCorePalette() != null && - prefs.getString(pref.Category.board.storageKey) == null) { + prefs.getString(PrefCategory.board.storageKey) == null) { prefs.setString( - pref.Category.board.storageKey, + PrefCategory.board.storageKey, jsonEncode( - pref.Board.defaults.copyWith(boardTheme: pref.BoardTheme.system), + BoardPrefs.defaults.copyWith(boardTheme: BoardTheme.system), ), ); } diff --git a/lib/src/intl.dart b/lib/src/intl.dart index 6cd7c4fc02..ea5d78a04a 100644 --- a/lib/src/intl.dart +++ b/lib/src/intl.dart @@ -3,7 +3,8 @@ import 'dart:convert'; import 'package:flutter/widgets.dart'; import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/binding.dart'; -import 'package:lichess_mobile/src/model/settings/preferences.dart' as pref; +import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; +import 'package:lichess_mobile/src/model/settings/preferences.dart'; import 'package:timeago/timeago.dart' as timeago; /// Setup [Intl.defaultLocale] and timeago locale and messages. @@ -12,10 +13,10 @@ Future setupIntl(WidgetsBinding widgetsBinding) async { // Get locale from shared preferences, if any final json = LichessBinding.instance.sharedPreferences - .getString(pref.Category.general.storageKey); + .getString(PrefCategory.general.storageKey); final generalPref = json != null - ? pref.General.fromJson(jsonDecode(json) as Map) - : pref.General.defaults; + ? GeneralPrefs.fromJson(jsonDecode(json) as Map) + : GeneralPrefs.defaults; final prefsLocale = generalPref.locale; final locale = prefsLocale ?? systemLocale; diff --git a/lib/src/model/analysis/analysis_preferences.dart b/lib/src/model/analysis/analysis_preferences.dart index 3075981a74..c6d5692c1c 100644 --- a/lib/src/model/analysis/analysis_preferences.dart +++ b/lib/src/model/analysis/analysis_preferences.dart @@ -1,19 +1,21 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; -import 'package:lichess_mobile/src/model/settings/preferences.dart' as pref; +import 'package:lichess_mobile/src/model/settings/preferences.dart'; import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; +part 'analysis_preferences.freezed.dart'; part 'analysis_preferences.g.dart'; @riverpod class AnalysisPreferences extends _$AnalysisPreferences - with PreferencesStorage { + with PreferencesStorage { // ignore: avoid_public_notifier_properties @override - final prefCategory = pref.Category.analysis; + final prefCategory = PrefCategory.analysis; @override - pref.Analysis build() { + AnalysisPrefs build() { return fetch(); } @@ -75,3 +77,33 @@ class AnalysisPreferences extends _$AnalysisPreferences ); } } + +@Freezed(fromJson: true, toJson: true) +class AnalysisPrefs with _$AnalysisPrefs implements SerializablePreferences { + const AnalysisPrefs._(); + + const factory AnalysisPrefs({ + required bool enableLocalEvaluation, + required bool showEvaluationGauge, + required bool showBestMoveArrow, + required bool showAnnotations, + required bool showPgnComments, + @Assert('numEvalLines >= 1 && numEvalLines <= 3') required int numEvalLines, + @Assert('numEngineCores >= 1 && numEngineCores <= maxEngineCores') + required int numEngineCores, + }) = _AnalysisPrefs; + + static const defaults = AnalysisPrefs( + enableLocalEvaluation: true, + showEvaluationGauge: true, + showBestMoveArrow: true, + showAnnotations: true, + showPgnComments: true, + numEvalLines: 2, + numEngineCores: 1, + ); + + factory AnalysisPrefs.fromJson(Map json) { + return _$AnalysisPrefsFromJson(json); + } +} diff --git a/lib/src/model/challenge/challenge_preferences.dart b/lib/src/model/challenge/challenge_preferences.dart index 9522de377b..9ba3717cb4 100644 --- a/lib/src/model/challenge/challenge_preferences.dart +++ b/lib/src/model/challenge/challenge_preferences.dart @@ -1,21 +1,26 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/challenge/challenge.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/game.dart'; -import 'package:lichess_mobile/src/model/settings/preferences.dart' as pref; +import 'package:lichess_mobile/src/model/common/speed.dart'; +import 'package:lichess_mobile/src/model/common/time_increment.dart'; +import 'package:lichess_mobile/src/model/settings/preferences.dart'; import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; +import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; +part 'challenge_preferences.freezed.dart'; part 'challenge_preferences.g.dart'; @riverpod class ChallengePreferences extends _$ChallengePreferences - with SessionPreferencesStorage { + with SessionPreferencesStorage { // ignore: avoid_public_notifier_properties @override - pref.Category get prefCategory => pref.Category.challenge; + PrefCategory get prefCategory => PrefCategory.challenge; @override - pref.Challenge build() { + ChallengePrefs build() { return fetch(); } @@ -43,3 +48,56 @@ class ChallengePreferences extends _$ChallengePreferences return save(state.copyWith(rated: rated)); } } + +@Freezed(fromJson: true, toJson: true) +class ChallengePrefs with _$ChallengePrefs implements SerializablePreferences { + const ChallengePrefs._(); + + const factory ChallengePrefs({ + required Variant variant, + required ChallengeTimeControlType timeControl, + required ({Duration time, Duration increment}) clock, + required int days, + required bool rated, + required SideChoice sideChoice, + }) = _ChallengePrefs; + + static const defaults = ChallengePrefs( + variant: Variant.standard, + timeControl: ChallengeTimeControlType.clock, + clock: (time: Duration(minutes: 10), increment: Duration.zero), + days: 3, + rated: false, + sideChoice: SideChoice.random, + ); + + Speed get speed => timeControl == ChallengeTimeControlType.clock + ? Speed.fromTimeIncrement( + TimeIncrement( + clock.time.inSeconds, + clock.increment.inSeconds, + ), + ) + : Speed.correspondence; + + ChallengeRequest makeRequest(LightUser destUser, [String? initialFen]) { + return ChallengeRequest( + destUser: destUser, + variant: variant, + timeControl: timeControl, + clock: clock, + days: days, + rated: rated, + sideChoice: sideChoice, + initialFen: initialFen, + ); + } + + factory ChallengePrefs.fromJson(Map json) { + try { + return _$ChallengePrefsFromJson(json); + } catch (_) { + return ChallengePrefs.defaults; + } + } +} diff --git a/lib/src/model/common/service/sound_service.dart b/lib/src/model/common/service/sound_service.dart index c70ed587ee..ce1b831f0d 100644 --- a/lib/src/model/common/service/sound_service.dart +++ b/lib/src/model/common/service/sound_service.dart @@ -4,7 +4,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart' show rootBundle; import 'package:lichess_mobile/src/binding.dart'; import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; -import 'package:lichess_mobile/src/model/settings/preferences.dart' as pref; +import 'package:lichess_mobile/src/model/settings/preferences.dart'; import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:sound_effect/sound_effect.dart'; @@ -40,7 +40,7 @@ const Set _emtpySet = {}; /// Loads all sounds of the given [SoundTheme]. Future _loadAllSounds( - pref.SoundTheme soundTheme, { + SoundTheme soundTheme, { Set excluded = _emtpySet, }) async { await Future.wait( @@ -51,7 +51,7 @@ Future _loadAllSounds( } /// Loads a single sound from the given [SoundTheme]. -Future _loadSound(pref.SoundTheme theme, Sound sound) async { +Future _loadSound(SoundTheme theme, Sound sound) async { final themePath = 'assets/sounds/${theme.name}'; const standardPath = 'assets/sounds/standard'; final soundId = sound.name; @@ -78,12 +78,12 @@ class SoundService { /// This should be called once when the app starts. static Future initialize() async { final stored = LichessBinding.instance.sharedPreferences - .getString(pref.Category.general.storageKey); + .getString(PrefCategory.general.storageKey); final theme = (stored != null - ? pref.General.fromJson( + ? GeneralPrefs.fromJson( jsonDecode(stored) as Map, ) - : pref.General.defaults) + : GeneralPrefs.defaults) .soundTheme; try { @@ -110,7 +110,7 @@ class SoundService { /// /// If [playSound] is true, a move sound will be played. Future changeTheme( - pref.SoundTheme theme, { + SoundTheme theme, { bool playSound = false, }) async { await _soundEffectPlugin.release(); diff --git a/lib/src/model/coordinate_training/coordinate_training_preferences.dart b/lib/src/model/coordinate_training/coordinate_training_preferences.dart index fa04ab9387..6d81a84d0f 100644 --- a/lib/src/model/coordinate_training/coordinate_training_preferences.dart +++ b/lib/src/model/coordinate_training/coordinate_training_preferences.dart @@ -1,19 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:lichess_mobile/l10n/l10n.dart'; import 'package:lichess_mobile/src/model/common/game.dart'; import 'package:lichess_mobile/src/model/settings/preferences.dart'; import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; +part 'coordinate_training_preferences.freezed.dart'; part 'coordinate_training_preferences.g.dart'; @riverpod class CoordinateTrainingPreferences extends _$CoordinateTrainingPreferences - with PreferencesStorage { + with PreferencesStorage { // ignore: avoid_public_notifier_properties @override - final Category prefCategory = Category.coordinateTraining; + final prefCategory = PrefCategory.coordinateTraining; @override - CoordinateTraining build() { + CoordinateTrainingPrefs build() { return fetch(); } @@ -37,3 +41,64 @@ class CoordinateTrainingPreferences extends _$CoordinateTrainingPreferences return save(state.copyWith(sideChoice: sideChoice)); } } + +enum TimeChoice { + thirtySeconds(Duration(seconds: 30)), + unlimited(null); + + const TimeChoice(this.duration); + + final Duration? duration; + + // TODO l10n + Widget label(AppLocalizations l10n) { + switch (this) { + case TimeChoice.thirtySeconds: + return const Text('30s'); + case TimeChoice.unlimited: + return const Icon(Icons.all_inclusive); + } + } +} + +enum TrainingMode { + findSquare, + nameSquare; + + // TODO l10n + String label(AppLocalizations l10n) { + switch (this) { + case TrainingMode.findSquare: + return 'Find Square'; + case TrainingMode.nameSquare: + return 'Name Square'; + } + } +} + +@Freezed(fromJson: true, toJson: true) +class CoordinateTrainingPrefs + with _$CoordinateTrainingPrefs + implements SerializablePreferences { + const CoordinateTrainingPrefs._(); + + const factory CoordinateTrainingPrefs({ + required bool showCoordinates, + required bool showPieces, + required TrainingMode mode, + required TimeChoice timeChoice, + required SideChoice sideChoice, + }) = _CoordinateTrainingPrefs; + + static const defaults = CoordinateTrainingPrefs( + showCoordinates: false, + showPieces: true, + mode: TrainingMode.findSquare, + timeChoice: TimeChoice.thirtySeconds, + sideChoice: SideChoice.random, + ); + + factory CoordinateTrainingPrefs.fromJson(Map json) { + return _$CoordinateTrainingPrefsFromJson(json); + } +} diff --git a/lib/src/model/game/game.dart b/lib/src/model/game/game.dart index 3221ac9f9c..a3408e0809 100644 --- a/lib/src/model/game/game.dart +++ b/lib/src/model/game/game.dart @@ -270,17 +270,17 @@ enum GameRule { } @freezed -class GamePrefs with _$GamePrefs { - const GamePrefs._(); +class ServerGamePrefs with _$ServerGamePrefs { + const ServerGamePrefs._(); - const factory GamePrefs({ + const factory ServerGamePrefs({ required bool showRatings, required bool enablePremove, required AutoQueen autoQueen, required bool confirmResign, required bool submitMove, required Zen zenMode, - }) = _GamePrefs; + }) = _ServerGamePrefs; } @Freezed(fromJson: true, toJson: true) diff --git a/lib/src/model/game/game_preferences.dart b/lib/src/model/game/game_preferences.dart index f4fcb335d3..d433c51be3 100644 --- a/lib/src/model/game/game_preferences.dart +++ b/lib/src/model/game/game_preferences.dart @@ -1,19 +1,21 @@ -import 'package:lichess_mobile/src/model/settings/preferences.dart' as pref; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:lichess_mobile/src/model/settings/preferences.dart'; import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; +part 'game_preferences.freezed.dart'; part 'game_preferences.g.dart'; /// Local game preferences, defined client-side only. @riverpod class GamePreferences extends _$GamePreferences - with PreferencesStorage { + with PreferencesStorage { // ignore: avoid_public_notifier_properties @override - pref.Category get prefCategory => pref.Category.game; + final prefCategory = PrefCategory.game; @override - pref.Game build() { + GamePrefs build() { return fetch(); } @@ -28,3 +30,18 @@ class GamePreferences extends _$GamePreferences ); } } + +@Freezed(fromJson: true, toJson: true) +class GamePrefs with _$GamePrefs implements SerializablePreferences { + const factory GamePrefs({ + bool? enableChat, + bool? blindfoldMode, + }) = _GamePrefs; + + static const defaults = GamePrefs( + enableChat: true, + ); + + factory GamePrefs.fromJson(Map json) => + _$GamePrefsFromJson(json); +} diff --git a/lib/src/model/game/playable_game.dart b/lib/src/model/game/playable_game.dart index 7ac6cb17b3..1e23968787 100644 --- a/lib/src/model/game/playable_game.dart +++ b/lib/src/model/game/playable_game.dart @@ -54,7 +54,7 @@ class PlayableGame /// The side that the current player is playing as. This is null if viewing /// the game as a spectator. Side? youAre, - GamePrefs? prefs, + ServerGamePrefs? prefs, PlayableClockData? clock, CorrespondenceClockData? correspondenceClock, bool? boosted, @@ -265,8 +265,8 @@ GameMeta _playableGameMetaFromPick(RequiredPick pick) { ); } -GamePrefs _gamePrefsFromPick(RequiredPick pick) { - return GamePrefs( +ServerGamePrefs _gamePrefsFromPick(RequiredPick pick) { + return ServerGamePrefs( showRatings: pick('showRatings').asBoolOrFalse(), enablePremove: pick('enablePremove').asBoolOrFalse(), autoQueen: AutoQueen.fromInt(pick('autoQueen').asIntOrThrow()), diff --git a/lib/src/model/lobby/game_seek.dart b/lib/src/model/lobby/game_seek.dart index 4e481ee10f..6b1901f464 100644 --- a/lib/src/model/lobby/game_seek.dart +++ b/lib/src/model/lobby/game_seek.dart @@ -10,7 +10,7 @@ import 'package:lichess_mobile/src/model/common/speed.dart'; import 'package:lichess_mobile/src/model/common/time_increment.dart'; import 'package:lichess_mobile/src/model/game/game.dart'; import 'package:lichess_mobile/src/model/game/playable_game.dart'; -import 'package:lichess_mobile/src/model/settings/preferences.dart'; +import 'package:lichess_mobile/src/model/lobby/game_setup_preferences.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; part 'game_seek.freezed.dart'; @@ -57,8 +57,8 @@ class GameSeek with _$GameSeek { ); } - /// Construct a game seek from saved [GameSetup], using all the custom params. - factory GameSeek.custom(GameSetup setup, User? account) { + /// Construct a game seek from saved [GameSetupPrefs], using all the custom params. + factory GameSeek.custom(GameSetupPrefs setup, User? account) { return GameSeek( clock: ( Duration(seconds: setup.customTimeSeconds), @@ -76,8 +76,8 @@ class GameSeek with _$GameSeek { ); } - /// Construct a correspondence seek from saved [GameSetup]. - factory GameSeek.correspondence(GameSetup setup, User? account) { + /// Construct a correspondence seek from saved [GameSetupPrefs]. + factory GameSeek.correspondence(GameSetupPrefs setup, User? account) { return GameSeek( days: setup.customDaysPerTurn, rated: account != null && setup.customRated, @@ -94,7 +94,10 @@ class GameSeek with _$GameSeek { /// Construct a game seek from a playable game to find a new opponent, using /// the same time control, variant and rated status. - factory GameSeek.newOpponentFromGame(PlayableGame game, GameSetup setup) { + factory GameSeek.newOpponentFromGame( + PlayableGame game, + GameSetupPrefs setup, + ) { return GameSeek( clock: game.meta.clock != null ? (game.meta.clock!.initial, game.meta.clock!.increment) diff --git a/lib/src/model/lobby/game_setup_preferences.dart b/lib/src/model/lobby/game_setup_preferences.dart index 7c25f22441..eca0c1591a 100644 --- a/lib/src/model/lobby/game_setup_preferences.dart +++ b/lib/src/model/lobby/game_setup_preferences.dart @@ -1,21 +1,27 @@ +import 'dart:math' as math; +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/game.dart'; +import 'package:lichess_mobile/src/model/common/perf.dart'; +import 'package:lichess_mobile/src/model/common/speed.dart'; import 'package:lichess_mobile/src/model/common/time_increment.dart'; import 'package:lichess_mobile/src/model/settings/preferences.dart'; import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; +import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; +part 'game_setup_preferences.freezed.dart'; part 'game_setup_preferences.g.dart'; @riverpod class GameSetupPreferences extends _$GameSetupPreferences - with SessionPreferencesStorage { + with SessionPreferencesStorage { // ignore: avoid_public_notifier_properties @override - Category get prefCategory => Category.gameSetup; + final prefCategory = PrefCategory.gameSetup; @override - GameSetup build() { + GameSetupPrefs build() { return fetch(); } @@ -56,6 +62,68 @@ class GameSetupPreferences extends _$GameSetupPreferences } } +enum TimeControl { realTime, correspondence } + +@Freezed(fromJson: true, toJson: true) +class GameSetupPrefs with _$GameSetupPrefs implements SerializablePreferences { + const GameSetupPrefs._(); + + const factory GameSetupPrefs({ + required TimeIncrement quickPairingTimeIncrement, + required TimeControl customTimeControl, + required int customTimeSeconds, + required int customIncrementSeconds, + required int customDaysPerTurn, + required Variant customVariant, + required bool customRated, + required SideChoice customSide, + required (int, int) customRatingDelta, + }) = _GameSetupPrefs; + + static const defaults = GameSetupPrefs( + quickPairingTimeIncrement: TimeIncrement(600, 0), + customTimeControl: TimeControl.realTime, + customTimeSeconds: 180, + customIncrementSeconds: 0, + customVariant: Variant.standard, + customRated: false, + customSide: SideChoice.random, + customRatingDelta: (-500, 500), + customDaysPerTurn: 3, + ); + + Speed get speedFromCustom => Speed.fromTimeIncrement( + TimeIncrement( + customTimeSeconds, + customIncrementSeconds, + ), + ); + + Perf get perfFromCustom => Perf.fromVariantAndSpeed( + customVariant, + speedFromCustom, + ); + + /// Returns the rating range for the custom setup, or null if the user + /// doesn't have a rating for the custom setup perf. + (int, int)? ratingRangeFromCustom(User user) { + final perf = user.perfs[perfFromCustom]; + if (perf == null) return null; + if (perf.provisional == true) return null; + final min = math.max(0, perf.rating + customRatingDelta.$1); + final max = perf.rating + customRatingDelta.$2; + return (min, max); + } + + factory GameSetupPrefs.fromJson(Map json) { + try { + return _$GameSetupPrefsFromJson(json); + } catch (_) { + return defaults; + } + } +} + const kSubtractingRatingRange = [ -500, -450, diff --git a/lib/src/model/opening_explorer/opening_explorer_preferences.dart b/lib/src/model/opening_explorer/opening_explorer_preferences.dart index 02c5bf4234..79b22b733e 100644 --- a/lib/src/model/opening_explorer/opening_explorer_preferences.dart +++ b/lib/src/model/opening_explorer/opening_explorer_preferences.dart @@ -3,7 +3,6 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/common/speed.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; -import 'package:lichess_mobile/src/model/settings/preferences.dart' as pref; import 'package:lichess_mobile/src/model/settings/preferences.dart'; import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; @@ -17,7 +16,7 @@ class OpeningExplorerPreferences extends _$OpeningExplorerPreferences with SessionPreferencesStorage { // ignore: avoid_public_notifier_properties @override - final prefCategory = pref.Category.openingExplorer; + final prefCategory = PrefCategory.openingExplorer; @override OpeningExplorerPrefs build() { diff --git a/lib/src/model/puzzle/puzzle_preferences.dart b/lib/src/model/puzzle/puzzle_preferences.dart index 99d2a01745..8af4fdbd97 100644 --- a/lib/src/model/puzzle/puzzle_preferences.dart +++ b/lib/src/model/puzzle/puzzle_preferences.dart @@ -1,8 +1,11 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_difficulty.dart'; import 'package:lichess_mobile/src/model/settings/preferences.dart'; import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; +part 'puzzle_preferences.freezed.dart'; part 'puzzle_preferences.g.dart'; @riverpod @@ -10,7 +13,7 @@ class PuzzlePreferences extends _$PuzzlePreferences with SessionPreferencesStorage { // ignore: avoid_public_notifier_properties @override - final prefCategory = Category.puzzle; + final prefCategory = PrefCategory.puzzle; @override PuzzlePrefs build() { @@ -25,3 +28,25 @@ class PuzzlePreferences extends _$PuzzlePreferences save(state.copyWith(autoNext: autoNext)); } } + +@Freezed(fromJson: true, toJson: true) +class PuzzlePrefs with _$PuzzlePrefs implements SerializablePreferences { + const factory PuzzlePrefs({ + required UserId? id, + required PuzzleDifficulty difficulty, + + /// If `true`, will show next puzzle after successful completion. This has + /// no effect on puzzle streaks, which always show next puzzle. Defaults to + /// `false`. + @Default(false) bool autoNext, + }) = _PuzzlePrefs; + + factory PuzzlePrefs.defaults({UserId? id}) => PuzzlePrefs( + id: id, + difficulty: PuzzleDifficulty.normal, + autoNext: false, + ); + + factory PuzzlePrefs.fromJson(Map json) => + _$PuzzlePrefsFromJson(json); +} diff --git a/lib/src/model/settings/board_preferences.dart b/lib/src/model/settings/board_preferences.dart index 58a7a59485..baedf764b8 100644 --- a/lib/src/model/settings/board_preferences.dart +++ b/lib/src/model/settings/board_preferences.dart @@ -1,20 +1,23 @@ import 'package:chessground/chessground.dart'; -import 'package:lichess_mobile/src/model/settings/preferences.dart' as pref; +import 'package:flutter/widgets.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/settings/preferences.dart'; import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; +import 'package:lichess_mobile/src/utils/color_palette.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; +part 'board_preferences.freezed.dart'; part 'board_preferences.g.dart'; @riverpod class BoardPreferences extends _$BoardPreferences - with PreferencesStorage { + with PreferencesStorage { // ignore: avoid_public_notifier_properties @override - pref.Category get prefCategory => pref.Category.board; + PrefCategory get prefCategory => PrefCategory.board; @override - pref.Board build() { + BoardPrefs build() { return fetch(); } @@ -82,3 +85,203 @@ class BoardPreferences extends _$BoardPreferences return save(state.copyWith(shapeColor: shapeColor)); } } + +@Freezed(fromJson: true, toJson: true) +class BoardPrefs with _$BoardPrefs implements SerializablePreferences { + const BoardPrefs._(); + + const factory BoardPrefs({ + required PieceSet pieceSet, + required BoardTheme boardTheme, + bool? immersiveModeWhilePlaying, + required bool hapticFeedback, + required bool showLegalMoves, + required bool boardHighlights, + required bool coordinates, + required bool pieceAnimation, + required bool showMaterialDifference, + @JsonKey( + defaultValue: PieceShiftMethod.either, + unknownEnumValue: PieceShiftMethod.either, + ) + required PieceShiftMethod pieceShiftMethod, + + /// Whether to enable shape drawings on the board for games and puzzles. + @JsonKey(defaultValue: true) required bool enableShapeDrawings, + @JsonKey(defaultValue: true) required bool magnifyDraggedPiece, + @JsonKey( + defaultValue: ShapeColor.green, + unknownEnumValue: ShapeColor.green, + ) + required ShapeColor shapeColor, + }) = _BoardPrefs; + + static const defaults = BoardPrefs( + pieceSet: PieceSet.staunty, + boardTheme: BoardTheme.brown, + immersiveModeWhilePlaying: false, + hapticFeedback: true, + showLegalMoves: true, + boardHighlights: true, + coordinates: true, + pieceAnimation: true, + showMaterialDifference: true, + pieceShiftMethod: PieceShiftMethod.either, + enableShapeDrawings: true, + magnifyDraggedPiece: true, + shapeColor: ShapeColor.green, + ); + + ChessboardSettings toBoardSettings() { + return ChessboardSettings( + pieceAssets: pieceSet.assets, + colorScheme: boardTheme.colors, + showValidMoves: showLegalMoves, + showLastMove: boardHighlights, + enableCoordinates: coordinates, + animationDuration: pieceAnimationDuration, + dragFeedbackScale: magnifyDraggedPiece ? 2.0 : 1.0, + dragFeedbackOffset: Offset(0.0, magnifyDraggedPiece ? -1.0 : 0.0), + pieceShiftMethod: pieceShiftMethod, + drawShape: DrawShapeOptions( + enable: enableShapeDrawings, + newShapeColor: shapeColor.color, + ), + ); + } + + factory BoardPrefs.fromJson(Map json) { + try { + return _$BoardPrefsFromJson(json); + } catch (_) { + return defaults; + } + } + + Duration get pieceAnimationDuration => + pieceAnimation ? const Duration(milliseconds: 150) : Duration.zero; +} + +/// Colors taken from lila: https://github.com/lichess-org/chessground/blob/54a7e71bf88701c1109d3b9b8106b464012b94cf/src/state.ts#L178 +enum ShapeColor { + green, + red, + blue, + yellow; + + Color get color => Color( + switch (this) { + ShapeColor.green => 0x15781B, + ShapeColor.red => 0x882020, + ShapeColor.blue => 0x003088, + ShapeColor.yellow => 0xe68f00, + }, + ).withAlpha(0xAA); +} + +/// The chessboard theme. +enum BoardTheme { + system('System'), + blue('Blue'), + blue2('Blue2'), + blue3('Blue3'), + blueMarble('Blue Marble'), + canvas('Canvas'), + wood('Wood'), + wood2('Wood2'), + wood3('Wood3'), + wood4('Wood4'), + maple('Maple'), + maple2('Maple 2'), + brown('Brown'), + leather('Leather'), + green('Green'), + marble('Marble'), + greenPlastic('Green Plastic'), + grey('Grey'), + metal('Metal'), + olive('Olive'), + newspaper('Newspaper'), + purpleDiag('Purple-Diag'), + pinkPyramid('Pink'), + horsey('Horsey'); + + final String label; + + const BoardTheme(this.label); + + ChessboardColorScheme get colors { + switch (this) { + case BoardTheme.system: + return getBoardColorScheme() ?? ChessboardColorScheme.brown; + case BoardTheme.blue: + return ChessboardColorScheme.blue; + case BoardTheme.blue2: + return ChessboardColorScheme.blue2; + case BoardTheme.blue3: + return ChessboardColorScheme.blue3; + case BoardTheme.blueMarble: + return ChessboardColorScheme.blueMarble; + case BoardTheme.canvas: + return ChessboardColorScheme.canvas; + case BoardTheme.wood: + return ChessboardColorScheme.wood; + case BoardTheme.wood2: + return ChessboardColorScheme.wood2; + case BoardTheme.wood3: + return ChessboardColorScheme.wood3; + case BoardTheme.wood4: + return ChessboardColorScheme.wood4; + case BoardTheme.maple: + return ChessboardColorScheme.maple; + case BoardTheme.maple2: + return ChessboardColorScheme.maple2; + case BoardTheme.brown: + return ChessboardColorScheme.brown; + case BoardTheme.leather: + return ChessboardColorScheme.leather; + case BoardTheme.green: + return ChessboardColorScheme.green; + case BoardTheme.marble: + return ChessboardColorScheme.marble; + case BoardTheme.greenPlastic: + return ChessboardColorScheme.greenPlastic; + case BoardTheme.grey: + return ChessboardColorScheme.grey; + case BoardTheme.metal: + return ChessboardColorScheme.metal; + case BoardTheme.olive: + return ChessboardColorScheme.olive; + case BoardTheme.newspaper: + return ChessboardColorScheme.newspaper; + case BoardTheme.purpleDiag: + return ChessboardColorScheme.purpleDiag; + case BoardTheme.pinkPyramid: + return ChessboardColorScheme.pinkPyramid; + case BoardTheme.horsey: + return ChessboardColorScheme.horsey; + } + } + + Widget get thumbnail => this == BoardTheme.system + ? SizedBox( + height: 44, + width: 44 * 6, + child: Row( + children: [ + for (final c in const [1, 2, 3, 4, 5, 6]) + Container( + width: 44, + color: c.isEven + ? BoardTheme.system.colors.darkSquare + : BoardTheme.system.colors.lightSquare, + ), + ], + ), + ) + : Image.asset( + 'assets/board-thumbnails/$name.jpg', + height: 44, + errorBuilder: (context, o, st) => const SizedBox.shrink(), + ); +} diff --git a/lib/src/model/settings/general_preferences.dart b/lib/src/model/settings/general_preferences.dart index 9b309a160c..e63197513c 100644 --- a/lib/src/model/settings/general_preferences.dart +++ b/lib/src/model/settings/general_preferences.dart @@ -1,21 +1,23 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; -import 'package:lichess_mobile/src/model/settings/preferences.dart' as pref; +import 'package:lichess_mobile/src/model/settings/preferences.dart'; import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; +part 'general_preferences.freezed.dart'; part 'general_preferences.g.dart'; @riverpod class GeneralPreferences extends _$GeneralPreferences - with PreferencesStorage { + with PreferencesStorage { // ignore: avoid_public_notifier_properties @override - final prefCategory = pref.Category.general; + final prefCategory = PrefCategory.general; @override - pref.General build() { + GeneralPrefs build() { return fetch(); } @@ -31,7 +33,7 @@ class GeneralPreferences extends _$GeneralPreferences return save(state.copyWith(locale: locale)); } - Future setSoundTheme(pref.SoundTheme soundTheme) { + Future setSoundTheme(SoundTheme soundTheme) { return save(state.copyWith(soundTheme: soundTheme)); } @@ -46,15 +48,79 @@ class GeneralPreferences extends _$GeneralPreferences await save(state.copyWith(systemColors: !state.systemColors)); if (state.systemColors == false) { final boardTheme = ref.read(boardPreferencesProvider).boardTheme; - if (boardTheme == pref.BoardTheme.system) { + if (boardTheme == BoardTheme.system) { await ref .read(boardPreferencesProvider.notifier) - .setBoardTheme(pref.BoardTheme.brown); + .setBoardTheme(BoardTheme.brown); } } else { await ref .read(boardPreferencesProvider.notifier) - .setBoardTheme(pref.BoardTheme.system); + .setBoardTheme(BoardTheme.system); } } } + +Map? _localeToJson(Locale? locale) { + return locale != null + ? { + 'languageCode': locale.languageCode, + 'countryCode': locale.countryCode, + 'scriptCode': locale.scriptCode, + } + : null; +} + +Locale? _localeFromJson(Map? json) { + if (json == null) { + return null; + } + return Locale.fromSubtags( + languageCode: json['languageCode'] as String, + countryCode: json['countryCode'] as String?, + scriptCode: json['scriptCode'] as String?, + ); +} + +@Freezed(fromJson: true, toJson: true) +class GeneralPrefs with _$GeneralPrefs implements SerializablePreferences { + const factory GeneralPrefs({ + @JsonKey(unknownEnumValue: ThemeMode.system, defaultValue: ThemeMode.system) + required ThemeMode themeMode, + required bool isSoundEnabled, + @JsonKey(unknownEnumValue: SoundTheme.standard) + required SoundTheme soundTheme, + @JsonKey(defaultValue: 0.8) required double masterVolume, + + /// Should enable system color palette (android 12+ only) + required bool systemColors, + + /// Locale to use in the app, use system locale if null + @JsonKey(toJson: _localeToJson, fromJson: _localeFromJson) Locale? locale, + }) = _GeneralPrefs; + + static const defaults = GeneralPrefs( + themeMode: ThemeMode.system, + isSoundEnabled: true, + soundTheme: SoundTheme.standard, + masterVolume: 0.8, + systemColors: true, + ); + + factory GeneralPrefs.fromJson(Map json) { + return _$GeneralPrefsFromJson(json); + } +} + +enum SoundTheme { + standard('Standard'), + piano('Piano'), + nes('NES'), + sfx('SFX'), + futuristic('Futuristic'), + lisp('Lisp'); + + final String label; + + const SoundTheme(this.label); +} diff --git a/lib/src/model/settings/home_preferences.dart b/lib/src/model/settings/home_preferences.dart index 9f13718b71..8431622253 100644 --- a/lib/src/model/settings/home_preferences.dart +++ b/lib/src/model/settings/home_preferences.dart @@ -1,22 +1,24 @@ -import 'package:lichess_mobile/src/model/settings/preferences.dart' as pref; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:lichess_mobile/src/model/settings/preferences.dart'; import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; +part 'home_preferences.freezed.dart'; part 'home_preferences.g.dart'; @riverpod class HomePreferences extends _$HomePreferences - with PreferencesStorage { + with PreferencesStorage { // ignore: avoid_public_notifier_properties @override - pref.Category get prefCategory => pref.Category.home; + PrefCategory get prefCategory => PrefCategory.home; @override - pref.Home build() { + HomePrefs build() { return fetch(); } - Future toggleWidget(pref.EnabledWidget widget) { + Future toggleWidget(EnabledWidget widget) { final newState = state.copyWith( enabledWidgets: state.enabledWidgets.contains(widget) ? state.enabledWidgets.difference({widget}) @@ -25,3 +27,32 @@ class HomePreferences extends _$HomePreferences return save(newState); } } + +enum EnabledWidget { + hello, + perfCards, + quickPairing, +} + +@Freezed(fromJson: true, toJson: true) +class HomePrefs with _$HomePrefs implements SerializablePreferences { + const factory HomePrefs({ + required Set enabledWidgets, + }) = _HomePrefs; + + static const defaults = HomePrefs( + enabledWidgets: { + EnabledWidget.hello, + EnabledWidget.perfCards, + EnabledWidget.quickPairing, + }, + ); + + factory HomePrefs.fromJson(Map json) { + try { + return _$HomePrefsFromJson(json); + } catch (_) { + return defaults; + } + } +} diff --git a/lib/src/model/settings/over_the_board_preferences.dart b/lib/src/model/settings/over_the_board_preferences.dart index ab6078ca66..fe3cd93ff9 100644 --- a/lib/src/model/settings/over_the_board_preferences.dart +++ b/lib/src/model/settings/over_the_board_preferences.dart @@ -1,19 +1,20 @@ -import 'package:lichess_mobile/src/model/settings/preferences.dart' as pref; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:lichess_mobile/src/model/settings/preferences.dart'; import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; +part 'over_the_board_preferences.freezed.dart'; part 'over_the_board_preferences.g.dart'; @riverpod class OverTheBoardPreferences extends _$OverTheBoardPreferences - with PreferencesStorage { + with PreferencesStorage { // ignore: avoid_public_notifier_properties @override - pref.Category get prefCategory => - pref.Category.overTheBoard; + PrefCategory get prefCategory => PrefCategory.overTheBoard; @override - pref.OverTheBoard build() { + OverTheBoardPrefs build() { return fetch(); } @@ -29,3 +30,24 @@ class OverTheBoardPreferences extends _$OverTheBoardPreferences ); } } + +@Freezed(fromJson: true, toJson: true) +class OverTheBoardPrefs + with _$OverTheBoardPrefs + implements SerializablePreferences { + const OverTheBoardPrefs._(); + + const factory OverTheBoardPrefs({ + required bool flipPiecesAfterMove, + required bool symmetricPieces, + }) = _OverTheBoardPrefs; + + static const defaults = OverTheBoardPrefs( + flipPiecesAfterMove: false, + symmetricPieces: false, + ); + + factory OverTheBoardPrefs.fromJson(Map json) { + return _$OverTheBoardPrefsFromJson(json); + } +} diff --git a/lib/src/model/settings/preferences.dart b/lib/src/model/settings/preferences.dart index 2e9ec4561e..519c882c9f 100644 --- a/lib/src/model/settings/preferences.dart +++ b/lib/src/model/settings/preferences.dart @@ -1,36 +1,29 @@ -import 'dart:math' as math; -import 'package:chessground/chessground.dart'; -import 'package:flutter/material.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:lichess_mobile/l10n/l10n.dart'; -import 'package:lichess_mobile/src/model/challenge/challenge.dart'; -import 'package:lichess_mobile/src/model/common/chess.dart'; -import 'package:lichess_mobile/src/model/common/game.dart'; -import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/model/common/perf.dart'; -import 'package:lichess_mobile/src/model/common/speed.dart'; -import 'package:lichess_mobile/src/model/common/time_increment.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; +import 'package:lichess_mobile/src/model/challenge/challenge_preferences.dart'; +import 'package:lichess_mobile/src/model/coordinate_training/coordinate_training_preferences.dart'; +import 'package:lichess_mobile/src/model/game/game_preferences.dart'; +import 'package:lichess_mobile/src/model/lobby/game_setup_preferences.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_preferences.dart'; -import 'package:lichess_mobile/src/model/puzzle/puzzle_difficulty.dart'; +import 'package:lichess_mobile/src/model/puzzle/puzzle_preferences.dart'; +import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; +import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; +import 'package:lichess_mobile/src/model/settings/home_preferences.dart'; +import 'package:lichess_mobile/src/model/settings/over_the_board_preferences.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; -import 'package:lichess_mobile/src/utils/color_palette.dart'; - -part 'preferences.freezed.dart'; -part 'preferences.g.dart'; /// A preference category with its storage key and default values. -enum Category { - general('preferences.general', General.defaults), - home('preferences.home', Home.defaults), - board('preferences.board', Board.defaults), - analysis('preferences.analysis', Analysis.defaults), - overTheBoard('preferences.overTheBoard', OverTheBoard.defaults), - challenge('preferences.challenge', Challenge.defaults), - gameSetup('preferences.gameSetup', GameSetup.defaults), - game('preferences.game', Game.defaults), +enum PrefCategory { + general('preferences.general', GeneralPrefs.defaults), + home('preferences.home', HomePrefs.defaults), + board('preferences.board', BoardPrefs.defaults), + analysis('preferences.analysis', AnalysisPrefs.defaults), + overTheBoard('preferences.overTheBoard', OverTheBoardPrefs.defaults), + challenge('preferences.challenge', ChallengePrefs.defaults), + gameSetup('preferences.gameSetup', GameSetupPrefs.defaults), + game('preferences.game', GamePrefs.defaults), coordinateTraining( 'preferences.coordinateTraining', - CoordinateTraining.defaults, + CoordinateTrainingPrefs.defaults, ), openingExplorer( 'preferences.opening_explorer', @@ -38,24 +31,24 @@ enum Category { ), puzzle('preferences.puzzle', null as PuzzlePrefs?); - const Category(this.storageKey, this._defaults); + const PrefCategory(this.storageKey, this._defaults); final String storageKey; final T? _defaults; T defaults({LightUser? user}) => switch (this) { - Category.general => _defaults!, - Category.home => _defaults!, - Category.board => _defaults!, - Category.analysis => _defaults!, - Category.overTheBoard => _defaults!, - Category.challenge => _defaults!, - Category.gameSetup => _defaults!, - Category.game => _defaults!, - Category.coordinateTraining => _defaults!, - Category.openingExplorer => + PrefCategory.general => _defaults!, + PrefCategory.home => _defaults!, + PrefCategory.board => _defaults!, + PrefCategory.analysis => _defaults!, + PrefCategory.overTheBoard => _defaults!, + PrefCategory.challenge => _defaults!, + PrefCategory.gameSetup => _defaults!, + PrefCategory.game => _defaults!, + PrefCategory.coordinateTraining => _defaults!, + PrefCategory.openingExplorer => OpeningExplorerPrefs.defaults(user: user) as T, - Category.puzzle => PuzzlePrefs.defaults(id: user?.id) as T, + PrefCategory.puzzle => PuzzlePrefs.defaults(id: user?.id) as T, }; } @@ -64,597 +57,32 @@ abstract class SerializablePreferences { Map toJson(); static SerializablePreferences fromJson( - Category key, + PrefCategory key, Map json, ) { switch (key) { - case Category.general: - return General.fromJson(json); - case Category.home: - return Home.fromJson(json); - case Category.board: - return Board.fromJson(json); - case Category.analysis: - return Analysis.fromJson(json); - case Category.overTheBoard: - return OverTheBoard.fromJson(json); - case Category.challenge: - return Challenge.fromJson(json); - case Category.gameSetup: - return GameSetup.fromJson(json); - case Category.game: - return Game.fromJson(json); - case Category.coordinateTraining: - return CoordinateTraining.fromJson(json); - case Category.openingExplorer: + case PrefCategory.general: + return GeneralPrefs.fromJson(json); + case PrefCategory.home: + return HomePrefs.fromJson(json); + case PrefCategory.board: + return BoardPrefs.fromJson(json); + case PrefCategory.analysis: + return AnalysisPrefs.fromJson(json); + case PrefCategory.overTheBoard: + return OverTheBoardPrefs.fromJson(json); + case PrefCategory.challenge: + return ChallengePrefs.fromJson(json); + case PrefCategory.gameSetup: + return GameSetupPrefs.fromJson(json); + case PrefCategory.game: + return GamePrefs.fromJson(json); + case PrefCategory.coordinateTraining: + return CoordinateTrainingPrefs.fromJson(json); + case PrefCategory.openingExplorer: return OpeningExplorerPrefs.fromJson(json); - case Category.puzzle: + case PrefCategory.puzzle: return PuzzlePrefs.fromJson(json); } } } - -Map? _localeToJson(Locale? locale) { - return locale != null - ? { - 'languageCode': locale.languageCode, - 'countryCode': locale.countryCode, - 'scriptCode': locale.scriptCode, - } - : null; -} - -Locale? _localeFromJson(Map? json) { - if (json == null) { - return null; - } - return Locale.fromSubtags( - languageCode: json['languageCode'] as String, - countryCode: json['countryCode'] as String?, - scriptCode: json['scriptCode'] as String?, - ); -} - -/// Preferences models -////////////////////// - -@Freezed(fromJson: true, toJson: true) -class General with _$General implements SerializablePreferences { - const factory General({ - @JsonKey(unknownEnumValue: ThemeMode.system, defaultValue: ThemeMode.system) - required ThemeMode themeMode, - required bool isSoundEnabled, - @JsonKey(unknownEnumValue: SoundTheme.standard) - required SoundTheme soundTheme, - @JsonKey(defaultValue: 0.8) required double masterVolume, - - /// Should enable system color palette (android 12+ only) - required bool systemColors, - - /// Locale to use in the app, use system locale if null - @JsonKey(toJson: _localeToJson, fromJson: _localeFromJson) Locale? locale, - }) = _General; - - static const defaults = General( - themeMode: ThemeMode.system, - isSoundEnabled: true, - soundTheme: SoundTheme.standard, - masterVolume: 0.8, - systemColors: true, - ); - - factory General.fromJson(Map json) { - return _$GeneralFromJson(json); - } -} - -enum SoundTheme { - standard('Standard'), - piano('Piano'), - nes('NES'), - sfx('SFX'), - futuristic('Futuristic'), - lisp('Lisp'); - - final String label; - - const SoundTheme(this.label); -} - -enum EnabledWidget { - hello, - perfCards, - quickPairing, -} - -@Freezed(fromJson: true, toJson: true) -class Home with _$Home implements SerializablePreferences { - const factory Home({ - required Set enabledWidgets, - }) = _Home; - - static const defaults = Home( - enabledWidgets: { - EnabledWidget.hello, - EnabledWidget.perfCards, - EnabledWidget.quickPairing, - }, - ); - - factory Home.fromJson(Map json) { - try { - return _$HomeFromJson(json); - } catch (_) { - return defaults; - } - } -} - -@Freezed(fromJson: true, toJson: true) -class Board with _$Board implements SerializablePreferences { - const Board._(); - - const factory Board({ - required PieceSet pieceSet, - required BoardTheme boardTheme, - bool? immersiveModeWhilePlaying, - required bool hapticFeedback, - required bool showLegalMoves, - required bool boardHighlights, - required bool coordinates, - required bool pieceAnimation, - required bool showMaterialDifference, - @JsonKey( - defaultValue: PieceShiftMethod.either, - unknownEnumValue: PieceShiftMethod.either, - ) - required PieceShiftMethod pieceShiftMethod, - - /// Whether to enable shape drawings on the board for games and puzzles. - @JsonKey(defaultValue: true) required bool enableShapeDrawings, - @JsonKey(defaultValue: true) required bool magnifyDraggedPiece, - @JsonKey( - defaultValue: ShapeColor.green, - unknownEnumValue: ShapeColor.green, - ) - required ShapeColor shapeColor, - }) = _Board; - - static const defaults = Board( - pieceSet: PieceSet.staunty, - boardTheme: BoardTheme.brown, - immersiveModeWhilePlaying: false, - hapticFeedback: true, - showLegalMoves: true, - boardHighlights: true, - coordinates: true, - pieceAnimation: true, - showMaterialDifference: true, - pieceShiftMethod: PieceShiftMethod.either, - enableShapeDrawings: true, - magnifyDraggedPiece: true, - shapeColor: ShapeColor.green, - ); - - ChessboardSettings toBoardSettings() { - return ChessboardSettings( - pieceAssets: pieceSet.assets, - colorScheme: boardTheme.colors, - showValidMoves: showLegalMoves, - showLastMove: boardHighlights, - enableCoordinates: coordinates, - animationDuration: pieceAnimationDuration, - dragFeedbackScale: magnifyDraggedPiece ? 2.0 : 1.0, - dragFeedbackOffset: Offset(0.0, magnifyDraggedPiece ? -1.0 : 0.0), - pieceShiftMethod: pieceShiftMethod, - drawShape: DrawShapeOptions( - enable: enableShapeDrawings, - newShapeColor: shapeColor.color, - ), - ); - } - - factory Board.fromJson(Map json) { - try { - return _$BoardFromJson(json); - } catch (_) { - return defaults; - } - } - - Duration get pieceAnimationDuration => - pieceAnimation ? const Duration(milliseconds: 150) : Duration.zero; -} - -/// Colors taken from lila: https://github.com/lichess-org/chessground/blob/54a7e71bf88701c1109d3b9b8106b464012b94cf/src/state.ts#L178 -enum ShapeColor { - green, - red, - blue, - yellow; - - Color get color => Color( - switch (this) { - ShapeColor.green => 0x15781B, - ShapeColor.red => 0x882020, - ShapeColor.blue => 0x003088, - ShapeColor.yellow => 0xe68f00, - }, - ).withAlpha(0xAA); -} - -/// The chessboard theme. -enum BoardTheme { - system('System'), - blue('Blue'), - blue2('Blue2'), - blue3('Blue3'), - blueMarble('Blue Marble'), - canvas('Canvas'), - wood('Wood'), - wood2('Wood2'), - wood3('Wood3'), - wood4('Wood4'), - maple('Maple'), - maple2('Maple 2'), - brown('Brown'), - leather('Leather'), - green('Green'), - marble('Marble'), - greenPlastic('Green Plastic'), - grey('Grey'), - metal('Metal'), - olive('Olive'), - newspaper('Newspaper'), - purpleDiag('Purple-Diag'), - pinkPyramid('Pink'), - horsey('Horsey'); - - final String label; - - const BoardTheme(this.label); - - ChessboardColorScheme get colors { - switch (this) { - case BoardTheme.system: - return getBoardColorScheme() ?? ChessboardColorScheme.brown; - case BoardTheme.blue: - return ChessboardColorScheme.blue; - case BoardTheme.blue2: - return ChessboardColorScheme.blue2; - case BoardTheme.blue3: - return ChessboardColorScheme.blue3; - case BoardTheme.blueMarble: - return ChessboardColorScheme.blueMarble; - case BoardTheme.canvas: - return ChessboardColorScheme.canvas; - case BoardTheme.wood: - return ChessboardColorScheme.wood; - case BoardTheme.wood2: - return ChessboardColorScheme.wood2; - case BoardTheme.wood3: - return ChessboardColorScheme.wood3; - case BoardTheme.wood4: - return ChessboardColorScheme.wood4; - case BoardTheme.maple: - return ChessboardColorScheme.maple; - case BoardTheme.maple2: - return ChessboardColorScheme.maple2; - case BoardTheme.brown: - return ChessboardColorScheme.brown; - case BoardTheme.leather: - return ChessboardColorScheme.leather; - case BoardTheme.green: - return ChessboardColorScheme.green; - case BoardTheme.marble: - return ChessboardColorScheme.marble; - case BoardTheme.greenPlastic: - return ChessboardColorScheme.greenPlastic; - case BoardTheme.grey: - return ChessboardColorScheme.grey; - case BoardTheme.metal: - return ChessboardColorScheme.metal; - case BoardTheme.olive: - return ChessboardColorScheme.olive; - case BoardTheme.newspaper: - return ChessboardColorScheme.newspaper; - case BoardTheme.purpleDiag: - return ChessboardColorScheme.purpleDiag; - case BoardTheme.pinkPyramid: - return ChessboardColorScheme.pinkPyramid; - case BoardTheme.horsey: - return ChessboardColorScheme.horsey; - } - } - - Widget get thumbnail => this == BoardTheme.system - ? SizedBox( - height: 44, - width: 44 * 6, - child: Row( - children: [ - for (final c in const [1, 2, 3, 4, 5, 6]) - Container( - width: 44, - color: c.isEven - ? BoardTheme.system.colors.darkSquare - : BoardTheme.system.colors.lightSquare, - ), - ], - ), - ) - : Image.asset( - 'assets/board-thumbnails/$name.jpg', - height: 44, - errorBuilder: (context, o, st) => const SizedBox.shrink(), - ); -} - -@Freezed(fromJson: true, toJson: true) -class Analysis with _$Analysis implements SerializablePreferences { - const Analysis._(); - - const factory Analysis({ - required bool enableLocalEvaluation, - required bool showEvaluationGauge, - required bool showBestMoveArrow, - required bool showAnnotations, - required bool showPgnComments, - @Assert('numEvalLines >= 1 && numEvalLines <= 3') required int numEvalLines, - @Assert('numEngineCores >= 1 && numEngineCores <= maxEngineCores') - required int numEngineCores, - }) = _Analysis; - - static const defaults = Analysis( - enableLocalEvaluation: true, - showEvaluationGauge: true, - showBestMoveArrow: true, - showAnnotations: true, - showPgnComments: true, - numEvalLines: 2, - numEngineCores: 1, - ); - - factory Analysis.fromJson(Map json) { - try { - return _$AnalysisFromJson(json); - } catch (_) { - return defaults; - } - } -} - -@Freezed(fromJson: true, toJson: true) -class OverTheBoard with _$OverTheBoard implements SerializablePreferences { - const OverTheBoard._(); - - const factory OverTheBoard({ - required bool flipPiecesAfterMove, - required bool symmetricPieces, - }) = _OverTheBoard; - - static const defaults = OverTheBoard( - flipPiecesAfterMove: false, - symmetricPieces: false, - ); - - factory OverTheBoard.fromJson(Map json) { - try { - return _$OverTheBoardFromJson(json); - } catch (_) { - return defaults; - } - } -} - -@Freezed(fromJson: true, toJson: true) -class Challenge with _$Challenge implements SerializablePreferences { - const Challenge._(); - - const factory Challenge({ - required Variant variant, - required ChallengeTimeControlType timeControl, - required ({Duration time, Duration increment}) clock, - required int days, - required bool rated, - required SideChoice sideChoice, - }) = _Challenge; - - static const defaults = Challenge( - variant: Variant.standard, - timeControl: ChallengeTimeControlType.clock, - clock: (time: Duration(minutes: 10), increment: Duration.zero), - days: 3, - rated: false, - sideChoice: SideChoice.random, - ); - - Speed get speed => timeControl == ChallengeTimeControlType.clock - ? Speed.fromTimeIncrement( - TimeIncrement( - clock.time.inSeconds, - clock.increment.inSeconds, - ), - ) - : Speed.correspondence; - - ChallengeRequest makeRequest(LightUser destUser, [String? initialFen]) { - return ChallengeRequest( - destUser: destUser, - variant: variant, - timeControl: timeControl, - clock: clock, - days: days, - rated: rated, - sideChoice: sideChoice, - initialFen: initialFen, - ); - } - - factory Challenge.fromJson(Map json) { - try { - return _$ChallengeFromJson(json); - } catch (_) { - return Challenge.defaults; - } - } -} - -enum TimeControl { realTime, correspondence } - -@Freezed(fromJson: true, toJson: true) -class GameSetup with _$GameSetup implements SerializablePreferences { - const GameSetup._(); - - const factory GameSetup({ - required TimeIncrement quickPairingTimeIncrement, - required TimeControl customTimeControl, - required int customTimeSeconds, - required int customIncrementSeconds, - required int customDaysPerTurn, - required Variant customVariant, - required bool customRated, - required SideChoice customSide, - required (int, int) customRatingDelta, - }) = _GameSetup; - - static const defaults = GameSetup( - quickPairingTimeIncrement: TimeIncrement(600, 0), - customTimeControl: TimeControl.realTime, - customTimeSeconds: 180, - customIncrementSeconds: 0, - customVariant: Variant.standard, - customRated: false, - customSide: SideChoice.random, - customRatingDelta: (-500, 500), - customDaysPerTurn: 3, - ); - - Speed get speedFromCustom => Speed.fromTimeIncrement( - TimeIncrement( - customTimeSeconds, - customIncrementSeconds, - ), - ); - - Perf get perfFromCustom => Perf.fromVariantAndSpeed( - customVariant, - speedFromCustom, - ); - - /// Returns the rating range for the custom setup, or null if the user - /// doesn't have a rating for the custom setup perf. - (int, int)? ratingRangeFromCustom(User user) { - final perf = user.perfs[perfFromCustom]; - if (perf == null) return null; - if (perf.provisional == true) return null; - final min = math.max(0, perf.rating + customRatingDelta.$1); - final max = perf.rating + customRatingDelta.$2; - return (min, max); - } - - factory GameSetup.fromJson(Map json) { - try { - return _$GameSetupFromJson(json); - } catch (_) { - return defaults; - } - } -} - -@Freezed(fromJson: true, toJson: true) -class Game with _$Game implements SerializablePreferences { - const factory Game({ - bool? enableChat, - bool? blindfoldMode, - }) = _Game; - - static const defaults = Game( - enableChat: true, - ); - - factory Game.fromJson(Map json) => _$GameFromJson(json); -} - -enum TimeChoice { - thirtySeconds(Duration(seconds: 30)), - unlimited(null); - - const TimeChoice(this.duration); - - final Duration? duration; - - // TODO l10n - Widget label(AppLocalizations l10n) { - switch (this) { - case TimeChoice.thirtySeconds: - return const Text('30s'); - case TimeChoice.unlimited: - return const Icon(Icons.all_inclusive); - } - } -} - -enum TrainingMode { - findSquare, - nameSquare; - - // TODO l10n - String label(AppLocalizations l10n) { - switch (this) { - case TrainingMode.findSquare: - return 'Find Square'; - case TrainingMode.nameSquare: - return 'Name Square'; - } - } -} - -@Freezed(fromJson: true, toJson: true) -class CoordinateTraining - with _$CoordinateTraining - implements SerializablePreferences { - const CoordinateTraining._(); - - const factory CoordinateTraining({ - required bool showCoordinates, - required bool showPieces, - required TrainingMode mode, - required TimeChoice timeChoice, - required SideChoice sideChoice, - }) = _CoordinateTraining; - - static const defaults = CoordinateTraining( - showCoordinates: false, - showPieces: true, - mode: TrainingMode.findSquare, - timeChoice: TimeChoice.thirtySeconds, - sideChoice: SideChoice.random, - ); - - factory CoordinateTraining.fromJson(Map json) { - return _$CoordinateTrainingFromJson(json); - } -} - -@Freezed(fromJson: true, toJson: true) -class PuzzlePrefs with _$PuzzlePrefs implements SerializablePreferences { - const factory PuzzlePrefs({ - required UserId? id, - required PuzzleDifficulty difficulty, - - /// If `true`, will show next puzzle after successful completion. This has - /// no effect on puzzle streaks, which always show next puzzle. Defaults to - /// `false`. - @Default(false) bool autoNext, - }) = _PuzzlePrefs; - - factory PuzzlePrefs.defaults({UserId? id}) => PuzzlePrefs( - id: id, - difficulty: PuzzleDifficulty.normal, - autoNext: false, - ); - - factory PuzzlePrefs.fromJson(Map json) => - _$PuzzlePrefsFromJson(json); -} diff --git a/lib/src/model/settings/preferences_storage.dart b/lib/src/model/settings/preferences_storage.dart index 2bf9326722..27e942e590 100644 --- a/lib/src/model/settings/preferences_storage.dart +++ b/lib/src/model/settings/preferences_storage.dart @@ -16,7 +16,7 @@ mixin PreferencesStorage { AutoDisposeNotifierProviderRef get ref; abstract T state; - Category get prefCategory; + PrefCategory get prefCategory; Future save(T value) async { await LichessBinding.instance.sharedPreferences @@ -53,7 +53,7 @@ mixin SessionPreferencesStorage { AutoDisposeNotifierProviderRef get ref; abstract T state; - Category get prefCategory; + PrefCategory get prefCategory; Future save(T value) async { final session = ref.read(authSessionProvider); diff --git a/lib/src/view/coordinate_training/coordinate_training_screen.dart b/lib/src/view/coordinate_training/coordinate_training_screen.dart index 99780471c5..1166ca03bb 100644 --- a/lib/src/view/coordinate_training/coordinate_training_screen.dart +++ b/lib/src/view/coordinate_training/coordinate_training_screen.dart @@ -11,7 +11,6 @@ import 'package:lichess_mobile/src/model/common/game.dart'; import 'package:lichess_mobile/src/model/coordinate_training/coordinate_training_controller.dart'; import 'package:lichess_mobile/src/model/coordinate_training/coordinate_training_preferences.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; -import 'package:lichess_mobile/src/model/settings/preferences.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; diff --git a/lib/src/view/game/game_screen_providers.dart b/lib/src/view/game/game_screen_providers.dart index 73265b8446..8e4fc8f449 100644 --- a/lib/src/view/game/game_screen_providers.dart +++ b/lib/src/view/game/game_screen_providers.dart @@ -35,7 +35,7 @@ Future shouldPreventGoingBack( @riverpod Future< ({ - GamePrefs? prefs, + ServerGamePrefs? prefs, bool shouldConfirmMove, bool isZenModeEnabled, bool canAutoQueen diff --git a/lib/src/view/home/home_tab_screen.dart b/lib/src/view/home/home_tab_screen.dart index 4a7b78919a..bff4db350e 100644 --- a/lib/src/view/home/home_tab_screen.dart +++ b/lib/src/view/home/home_tab_screen.dart @@ -10,7 +10,6 @@ import 'package:lichess_mobile/src/model/challenge/challenges.dart'; import 'package:lichess_mobile/src/model/correspondence/correspondence_game_storage.dart'; import 'package:lichess_mobile/src/model/game/game_history.dart'; import 'package:lichess_mobile/src/model/settings/home_preferences.dart'; -import 'package:lichess_mobile/src/model/settings/preferences.dart' as pref; import 'package:lichess_mobile/src/navigation.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; @@ -240,13 +239,13 @@ class _HomeBody extends ConsumerWidget { final widgets = isTablet ? [ _EditableWidget( - widget: pref.EnabledWidget.hello, + widget: EnabledWidget.hello, isEditing: isEditing, shouldShow: true, child: const _HelloWidget(), ), _EditableWidget( - widget: pref.EnabledWidget.perfCards, + widget: EnabledWidget.perfCards, isEditing: isEditing, shouldShow: session != null, child: const AccountPerfCards( @@ -286,13 +285,13 @@ class _HomeBody extends ConsumerWidget { ] : [ _EditableWidget( - widget: pref.EnabledWidget.hello, + widget: EnabledWidget.hello, isEditing: isEditing, shouldShow: true, child: const _HelloWidget(), ), _EditableWidget( - widget: pref.EnabledWidget.perfCards, + widget: EnabledWidget.perfCards, isEditing: isEditing, shouldShow: session != null, child: const AccountPerfCards( @@ -300,7 +299,7 @@ class _HomeBody extends ConsumerWidget { ), ), _EditableWidget( - widget: pref.EnabledWidget.quickPairing, + widget: EnabledWidget.quickPairing, isEditing: isEditing, shouldShow: status.isOnline, child: const Padding( @@ -353,7 +352,7 @@ class _EditableWidget extends ConsumerWidget { }); final Widget child; - final pref.EnabledWidget widget; + final EnabledWidget widget; final bool isEditing; final bool shouldShow; @@ -467,7 +466,7 @@ class _WelcomeScreen extends StatelessWidget { else ...[ if (status.isOnline) _EditableWidget( - widget: pref.EnabledWidget.quickPairing, + widget: EnabledWidget.quickPairing, isEditing: isEditing, shouldShow: true, child: const Padding( @@ -568,7 +567,7 @@ class _TabletCreateAGameSection extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.center, children: [ _EditableWidget( - widget: pref.EnabledWidget.quickPairing, + widget: EnabledWidget.quickPairing, isEditing: isEditing, shouldShow: true, child: const Padding( diff --git a/lib/src/view/play/create_custom_game_screen.dart b/lib/src/view/play/create_custom_game_screen.dart index 80f3880239..23348be800 100644 --- a/lib/src/view/play/create_custom_game_screen.dart +++ b/lib/src/view/play/create_custom_game_screen.dart @@ -16,7 +16,6 @@ import 'package:lichess_mobile/src/model/lobby/create_game_service.dart'; import 'package:lichess_mobile/src/model/lobby/game_seek.dart'; import 'package:lichess_mobile/src/model/lobby/game_setup_preferences.dart'; import 'package:lichess_mobile/src/model/lobby/lobby_repository.dart'; -import 'package:lichess_mobile/src/model/settings/preferences.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; diff --git a/lib/src/view/settings/board_theme_screen.dart b/lib/src/view/settings/board_theme_screen.dart index 1b48b80f9f..c005919905 100644 --- a/lib/src/view/settings/board_theme_screen.dart +++ b/lib/src/view/settings/board_theme_screen.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; -import 'package:lichess_mobile/src/model/settings/preferences.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/system.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; diff --git a/lib/src/view/settings/sound_settings_screen.dart b/lib/src/view/settings/sound_settings_screen.dart index cd014e2a7e..e4d3fdee62 100644 --- a/lib/src/view/settings/sound_settings_screen.dart +++ b/lib/src/view/settings/sound_settings_screen.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; -import 'package:lichess_mobile/src/model/settings/preferences.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; diff --git a/lib/src/view/settings/theme_screen.dart b/lib/src/view/settings/theme_screen.dart index a85fdad0bf..f3b4b79ea3 100644 --- a/lib/src/view/settings/theme_screen.dart +++ b/lib/src/view/settings/theme_screen.dart @@ -7,7 +7,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; -import 'package:lichess_mobile/src/model/settings/preferences.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; diff --git a/test/app_test.dart b/test/app_test.dart index 6f9cfceefa..0017db01ce 100644 --- a/test/app_test.dart +++ b/test/app_test.dart @@ -1,4 +1,3 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; diff --git a/test/model/common/service/fake_sound_service.dart b/test/model/common/service/fake_sound_service.dart index ef6c74f130..12f09936a0 100644 --- a/test/model/common/service/fake_sound_service.dart +++ b/test/model/common/service/fake_sound_service.dart @@ -1,5 +1,5 @@ import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; -import 'package:lichess_mobile/src/model/settings/preferences.dart'; +import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; class FakeSoundService implements SoundService { @override diff --git a/test/test_provider_scope.dart b/test/test_provider_scope.dart index 88f11ac292..4f2077558a 100644 --- a/test/test_provider_scope.dart +++ b/test/test_provider_scope.dart @@ -18,7 +18,7 @@ import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/notifications/notification_service.dart'; -import 'package:lichess_mobile/src/model/settings/preferences.dart'; +import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:logging/logging.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; @@ -99,7 +99,7 @@ Future makeProviderScope( { // disable piece animation to simplify tests 'preferences.board': jsonEncode( - Board.defaults + BoardPrefs.defaults .copyWith( pieceAnimation: false, ) diff --git a/test/view/coordinate_training/coordinate_training_screen_test.dart b/test/view/coordinate_training/coordinate_training_screen_test.dart index d072ba1a59..87e4f1cc99 100644 --- a/test/view/coordinate_training/coordinate_training_screen_test.dart +++ b/test/view/coordinate_training/coordinate_training_screen_test.dart @@ -5,7 +5,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:lichess_mobile/src/model/coordinate_training/coordinate_training_controller.dart'; import 'package:lichess_mobile/src/model/coordinate_training/coordinate_training_preferences.dart'; -import 'package:lichess_mobile/src/model/settings/preferences.dart'; import 'package:lichess_mobile/src/view/coordinate_training/coordinate_training_screen.dart'; import '../../test_provider_scope.dart'; diff --git a/test/view/opening_explorer/opening_explorer_screen_test.dart b/test/view/opening_explorer/opening_explorer_screen_test.dart index 0a5dd354ff..ea78e5ea0d 100644 --- a/test/view/opening_explorer/opening_explorer_screen_test.dart +++ b/test/view/opening_explorer/opening_explorer_screen_test.dart @@ -11,7 +11,7 @@ import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_preferences.dart'; -import 'package:lichess_mobile/src/model/settings/preferences.dart' as pref; +import 'package:lichess_mobile/src/model/settings/preferences.dart'; import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_screen.dart'; @@ -120,7 +120,7 @@ void main() { ], defaultPreferences: { SessionPreferencesStorage.key( - pref.Category.openingExplorer.storageKey, + PrefCategory.openingExplorer.storageKey, null, ): jsonEncode( OpeningExplorerPrefs.defaults() @@ -175,7 +175,7 @@ void main() { userSession: session, defaultPreferences: { SessionPreferencesStorage.key( - pref.Category.openingExplorer.storageKey, + PrefCategory.openingExplorer.storageKey, session, ): jsonEncode( OpeningExplorerPrefs.defaults(user: user) From 12410ee1232056ec2b6ef5bb8c24f659a63249d7 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 2 Oct 2024 17:53:02 +0200 Subject: [PATCH 414/979] Tweak app initialization --- lib/main.dart | 1 - lib/src/binding.dart | 12 +++++++++-- lib/src/init.dart | 21 +++++-------------- .../model/common/service/sound_service.dart | 17 +++++++-------- 4 files changed, 23 insertions(+), 28 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index b27fa377a6..c31b5b5b04 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -13,7 +13,6 @@ import 'src/app.dart'; Future main() async { final widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); - final lichessBinding = AppLichessBinding.ensureInitialized(); // Old API. diff --git a/lib/src/binding.dart b/lib/src/binding.dart index 54a0480447..3120ab6724 100644 --- a/lib/src/binding.dart +++ b/lib/src/binding.dart @@ -15,9 +15,12 @@ import 'package:lichess_mobile/src/model/notifications/notification_service.dart import 'package:lichess_mobile/src/model/notifications/notifications.dart'; import 'package:lichess_mobile/src/utils/string.dart'; import 'package:lichess_mobile/src/utils/system.dart'; +import 'package:logging/logging.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:shared_preferences/shared_preferences.dart'; +final _logger = Logger('LichessBinding'); + /// A singleton class that provides access to plugins and external APIs. /// /// Only one instance of this class will be created during the app's lifetime. @@ -190,8 +193,13 @@ class AppLichessBinding extends LichessBinding { ); } - _syncSri = await SecureStorage.instance.read(key: kSRIStorageKey) ?? - genRandomString(12); + final storedSri = await SecureStorage.instance.read(key: kSRIStorageKey); + + if (storedSri == null) { + _logger.warning('SRI not found in secure storage'); + } + + _syncSri = storedSri ?? genRandomString(12); final physicalMemory = await System.instance.getTotalRam() ?? 256.0; final engineMaxMemory = (physicalMemory / 10).ceil(); diff --git a/lib/src/init.dart b/lib/src/init.dart index ea443f4ccc..1965c7eed1 100644 --- a/lib/src/init.dart +++ b/lib/src/init.dart @@ -35,23 +35,12 @@ Future setupFirstLaunch() async { // Clear secure storage on first run because it is not deleted on app uninstall await SecureStorage.instance.deleteAll(); - await prefs.setBool('first_run', false); - } + // Generate a socket random identifier and store it for the app lifetime + final sri = genRandomString(12); + _logger.info('Generated new SRI: $sri'); + await SecureStorage.instance.write(key: kSRIStorageKey, value: sri); - // Generate a socket random identifier and store it for the app lifetime - String? storedSri; - try { - storedSri = await SecureStorage.instance.read(key: kSRIStorageKey); - if (storedSri == null) { - final sri = genRandomString(12); - _logger.info('Generated new SRI: $sri'); - await SecureStorage.instance.write(key: kSRIStorageKey, value: sri); - } - } on PlatformException catch (e) { - _logger.severe('Could not get SRI from storage: $e'); - // Clear all secure storage if an error occurs because it probably means the key has - // been lost - await SecureStorage.instance.deleteAll(); + await prefs.setBool('first_run', false); } } diff --git a/lib/src/model/common/service/sound_service.dart b/lib/src/model/common/service/sound_service.dart index ce1b831f0d..c74497b816 100644 --- a/lib/src/model/common/service/sound_service.dart +++ b/lib/src/model/common/service/sound_service.dart @@ -77,16 +77,15 @@ class SoundService { /// This will load the sounds from assets and make them ready to be played. /// This should be called once when the app starts. static Future initialize() async { - final stored = LichessBinding.instance.sharedPreferences - .getString(PrefCategory.general.storageKey); - final theme = (stored != null - ? GeneralPrefs.fromJson( - jsonDecode(stored) as Map, - ) - : GeneralPrefs.defaults) - .soundTheme; - try { + final stored = LichessBinding.instance.sharedPreferences + .getString(PrefCategory.general.storageKey); + final theme = (stored != null + ? GeneralPrefs.fromJson( + jsonDecode(stored) as Map, + ) + : GeneralPrefs.defaults) + .soundTheme; await _soundEffectPlugin.initialize(); await _loadAllSounds(theme); } catch (e) { From 7a94c58b033aade7f0a9a2cba7bee1d25a141246 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 2 Oct 2024 18:03:30 +0200 Subject: [PATCH 415/979] Fix adaptive list tile brightness --- lib/src/widgets/list.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/widgets/list.dart b/lib/src/widgets/list.dart index d60937bf4d..6abeb2b4ad 100644 --- a/lib/src/widgets/list.dart +++ b/lib/src/widgets/list.dart @@ -401,7 +401,7 @@ class AdaptiveListTile extends StatelessWidget { return Material( color: Colors.transparent, child: Theme( - data: ThemeData( + data: Theme.of(context).copyWith( splashFactory: Theme.of(context).platform == TargetPlatform.iOS ? NoSplash.splashFactory : null, From 22eb978227d8017c73db8ee4fa9a73839aa6497a Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 2 Oct 2024 18:15:24 +0200 Subject: [PATCH 416/979] Upgrade dependencies --- pubspec.lock | 84 ++++++++++++++++++++++++++-------------------------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index cb21061a9c..e7f235dc8d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -13,10 +13,10 @@ packages: dependency: transitive description: name: _flutterfire_internals - sha256: ddc6f775260b89176d329dee26f88b9469ef46aa3228ff6a0b91caf2b2989692 + sha256: "5534e701a2c505fed1f0799e652dd6ae23bd4d2c4cf797220e5ced5764a7c1c2" url: "https://pub.dev" source: hosted - version: "1.3.42" + version: "1.3.44" _macros: dependency: transitive description: dart @@ -122,18 +122,18 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "644dc98a0f179b872f612d3eb627924b578897c629788e858157fa5e704ca0c7" + sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" url: "https://pub.dev" source: hosted - version: "2.4.11" + version: "2.4.13" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: e3c79f69a64bdfcd8a776a3c28db4eb6e3fb5356d013ae5eb2e52007706d5dbe + sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 url: "https://pub.dev" source: hosted - version: "7.3.1" + version: "7.3.2" built_collection: dependency: transitive description: @@ -434,66 +434,66 @@ packages: dependency: "direct main" description: name: firebase_core - sha256: "40921de9795fbf5887ed5c0adfdf4972d5a8d7ae7e1b2bb98dea39bc02626a88" + sha256: "51dfe2fbf3a984787a2e7b8592f2f05c986bfedd6fdacea3f9e0a7beb334de96" url: "https://pub.dev" source: hosted - version: "3.4.1" + version: "3.6.0" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface - sha256: f7d7180c7f99babd4b4c517754d41a09a4943a0f7a69b65c894ca5c68ba66315 + sha256: e30da58198a6d4b49d5bce4e852f985c32cb10db329ebef9473db2b9f09ce810 url: "https://pub.dev" source: hosted - version: "5.2.1" + version: "5.3.0" firebase_core_web: dependency: transitive description: name: firebase_core_web - sha256: f4ee170441ca141c5f9ee5ad8737daba3ee9c8e7efb6902aee90b4fbd178ce25 + sha256: f967a7138f5d2ffb1ce15950e2a382924239eaa521150a8f144af34e68b3b3e5 url: "https://pub.dev" source: hosted - version: "2.18.0" + version: "2.18.1" firebase_crashlytics: dependency: "direct main" description: name: firebase_crashlytics - sha256: c4fdbb14ba6f36794f89dc27fb5c759c9cc67ecbaeb079edc4dba515bbf9f555 + sha256: "6899800fff1af819955aef740f18c4c8600f8b952a2a1ea97bc0872ebb257387" url: "https://pub.dev" source: hosted - version: "4.1.1" + version: "4.1.3" firebase_crashlytics_platform_interface: dependency: transitive description: name: firebase_crashlytics_platform_interface - sha256: "891d6f7ba4b93672d0e1265f27b6a9dccd56ba2cc30ce6496586b32d1d8770ac" + sha256: "97c47b0a1779a3d4118416a3f0c6c564cc59ad89095e899893204d4b2ad08f4c" url: "https://pub.dev" source: hosted - version: "3.6.42" + version: "3.6.44" firebase_messaging: dependency: "direct main" description: name: firebase_messaging - sha256: cc02c4afd6510cd84586020670140c4a23fbe52af16cd260ccf8ede101bb8d1b + sha256: eb6e28a3a35deda61fe8634967c84215efc19133ba58d8e0fc6c9a2af2cba05e url: "https://pub.dev" source: hosted - version: "15.1.1" + version: "15.1.3" firebase_messaging_platform_interface: dependency: transitive description: name: firebase_messaging_platform_interface - sha256: d8a4984635f09213302243ea670fe5c42f3261d7d8c7c0a5f7dcd5d6c84be459 + sha256: b316c4ee10d93d32c033644207afc282d9b2b4372f3cf9c6022f3558b3873d2d url: "https://pub.dev" source: hosted - version: "4.5.44" + version: "4.5.46" firebase_messaging_web: dependency: transitive description: name: firebase_messaging_web - sha256: "258b9d637965db7855299b123533609ed95e52350746a723dfd1d8d6f3fac678" + sha256: d7f0147a1a9fe4313168e20154a01fd5cf332898de1527d3930ff77b8c7f5387 url: "https://pub.dev" source: hosted - version: "3.9.0" + version: "3.9.2" fixnum: dependency: transitive description: @@ -567,10 +567,10 @@ packages: dependency: "direct main" description: name: flutter_local_notifications - sha256: dd6676d8c2926537eccdf9f72128bbb2a9d0814689527b17f92c248ff192eaf3 + sha256: "49eeef364fddb71515bc78d5a8c51435a68bccd6e4d68e25a942c5e47761ae71" url: "https://pub.dev" source: hosted - version: "17.2.1+2" + version: "17.2.3" flutter_local_notifications_linux: dependency: transitive description: @@ -1316,26 +1316,26 @@ packages: dependency: "direct main" description: name: sqflite - sha256: a43e5a27235518c03ca238e7b4732cf35eabe863a369ceba6cbefa537a66f16d + sha256: ff5a2436ef8ebdfda748fbfe957f9981524cb5ff11e7bafa8c42771840e8a788 url: "https://pub.dev" source: hosted - version: "2.3.3+1" + version: "2.3.3+2" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: "7b41b6c3507854a159e24ae90a8e3e9cc01eb26a477c118d6dca065b5f55453e" + sha256: "2d8e607db72e9cb7748c9c6e739e2c9618320a5517de693d5a24609c4671b1a4" url: "https://pub.dev" source: hosted - version: "2.5.4+2" + version: "2.5.4+4" sqflite_common_ffi: dependency: "direct dev" description: name: sqflite_common_ffi - sha256: "4d6137c29e930d6e4a8ff373989dd9de7bac12e3bc87bce950f6e844e8ad3bb5" + sha256: a6057d4c87e9260ba1ec436ebac24760a110589b9c0a859e128842eb69a7ef04 url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.3.3+1" sqlite3: dependency: transitive description: @@ -1397,10 +1397,10 @@ packages: dependency: transitive description: name: synchronized - sha256: a824e842b8a054f91a728b783c177c1e4731f6b124f9192468457a8913371255 + sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225" url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "3.3.0+3" term_glyph: dependency: transitive description: @@ -1493,10 +1493,10 @@ packages: dependency: transitive description: name: url_launcher_macos - sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de" + sha256: "769549c999acdb42b8bcfa7c43d72bf79a382ca7441ab18a808e101149daf672" url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "3.2.1" url_launcher_platform_interface: dependency: transitive description: @@ -1525,10 +1525,10 @@ packages: dependency: transitive description: name: uuid - sha256: f33d6bb662f0e4f79dcd7ada2e6170f3b3a2530c28fc41f49a411ddedd576a77 + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff url: "https://pub.dev" source: hosted - version: "4.5.0" + version: "4.5.1" vector_graphics: dependency: transitive description: @@ -1605,10 +1605,10 @@ packages: dependency: transitive description: name: web - sha256: d43c1d6b787bf0afad444700ae7f4db8827f701bc61c255ac8d328c6f4d52062 + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.0" web_socket: dependency: transitive description: @@ -1629,18 +1629,18 @@ packages: dependency: transitive description: name: win32 - sha256: "68d1e89a91ed61ad9c370f9f8b6effed9ae5e0ede22a270bdfa6daf79fc2290a" + sha256: "4d45dc9069dba4619dc0ebd93c7cec5e66d8482cb625a370ac806dcc8165f2ec" url: "https://pub.dev" source: hosted - version: "5.5.4" + version: "5.5.5" win32_registry: dependency: transitive description: name: win32_registry - sha256: "723b7f851e5724c55409bb3d5a32b203b3afe8587eaf5dafb93a5fed8ecda0d6" + sha256: "21ec76dfc731550fd3e2ce7a33a9ea90b828fdf19a5c3bcf556fa992cfa99852" url: "https://pub.dev" source: hosted - version: "1.1.4" + version: "1.1.5" xdg_directories: dependency: transitive description: From ef9fc55fdc08a606f26a7069140e73a1308c3e21 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 2 Oct 2024 18:21:38 +0200 Subject: [PATCH 417/979] Fix formatting --- lib/src/view/opening_explorer/opening_explorer_settings.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/src/view/opening_explorer/opening_explorer_settings.dart b/lib/src/view/opening_explorer/opening_explorer_settings.dart index 59b57c3c62..51c3c38d8f 100644 --- a/lib/src/view/opening_explorer/opening_explorer_settings.dart +++ b/lib/src/view/opening_explorer/opening_explorer_settings.dart @@ -101,8 +101,7 @@ class OpeningExplorerSettings extends ConsumerWidget { .map( (key) => ChoiceChip( label: Text(key), - selected: - prefs.lichessDb.since == LichessDb.datesMap[key], + selected: prefs.lichessDb.since == LichessDb.datesMap[key], onSelected: (_) => ref .read(openingExplorerPreferencesProvider.notifier) .setLichessDbSince(LichessDb.datesMap[key]!), From c073d469f8b12d0b88741c39d74c3711d73ef260 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 3 Oct 2024 09:29:08 +0200 Subject: [PATCH 418/979] Bump version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 5acf821671..eaae5727ac 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: lichess_mobile description: Lichess mobile app V2 publish_to: "none" -version: 0.11.0+001100 # see README.md for details about versioning +version: 0.12.0+001200 # see README.md for details about versioning environment: sdk: ">=3.3.0 <4.0.0" From 1702d9455c2ea3c7d6989c2105a58fe64ee2ff30 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 3 Oct 2024 10:02:04 +0200 Subject: [PATCH 419/979] Fix crashlytics init --- lib/main.dart | 12 ++++-------- lib/src/binding.dart | 16 +++++++++++++++- lib/src/log.dart | 15 ++------------- test/binding.dart | 14 +++++++++++++- 4 files changed, 34 insertions(+), 23 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index c31b5b5b04..1cd7218f2a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -15,22 +15,18 @@ Future main() async { final widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); final lichessBinding = AppLichessBinding.ensureInitialized(); + // Show splash screen until app is ready + // See src/app.dart for splash screen removal + FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); + // Old API. // TODO: Remove this once all SharedPreferences usage is migrated to SharedPreferencesAsync. SharedPreferences.setPrefix('lichess.'); - await migrateSharedPreferences(); await lichessBinding.preloadSharedPreferences(); - await lichessBinding.preloadData(); - // Show splash screen until app is ready - // See src/app.dart for splash screen removal - FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); - - setupLoggingAndCrashReporting(); - await setupFirstLaunch(); await SoundService.initialize(); diff --git a/lib/src/binding.dart b/lib/src/binding.dart index 3120ab6724..1ddfbf9ebe 100644 --- a/lib/src/binding.dart +++ b/lib/src/binding.dart @@ -2,12 +2,15 @@ import 'dart:convert'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:lichess_mobile/firebase_options.dart'; import 'package:lichess_mobile/l10n/l10n.dart'; import 'package:lichess_mobile/src/db/secure_storage.dart'; +import 'package:lichess_mobile/src/log.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/auth/session_storage.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; @@ -113,7 +116,9 @@ abstract class LichessBinding { /// A concrete implementation of [LichessBinding] for the app. class AppLichessBinding extends LichessBinding { - AppLichessBinding(); + AppLichessBinding() { + setupLogging(); + } /// Returns an instance of the binding that implements [LichessBinding]. /// @@ -212,6 +217,15 @@ class AppLichessBinding extends LichessBinding { options: DefaultFirebaseOptions.currentPlatform, ); + if (kReleaseMode) { + FlutterError.onError = + FirebaseCrashlytics.instance.recordFlutterFatalError; + PlatformDispatcher.instance.onError = (error, stack) { + FirebaseCrashlytics.instance.recordError(error, stack, fatal: true); + return true; + }; + } + final l10n = await AppLocalizations.delegate.load(locale); await FlutterLocalNotificationsPlugin().initialize( InitializationSettings( diff --git a/lib/src/log.dart b/lib/src/log.dart index f328a773ec..77eb73912c 100644 --- a/lib/src/log.dart +++ b/lib/src/log.dart @@ -1,6 +1,5 @@ import 'dart:developer' as developer; -import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:logging/logging.dart'; @@ -11,8 +10,8 @@ const _loggersToShowInTerminal = { 'Socket', }; -/// Setup logging and crash reporting. -void setupLoggingAndCrashReporting() { +/// Setup logging +void setupLogging() { if (kDebugMode) { Logger.root.level = Level.FINE; Logger.root.onRecord.listen((record) { @@ -36,16 +35,6 @@ void setupLoggingAndCrashReporting() { } }); } - - // Crashlytics - if (kReleaseMode) { - FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError; - // Pass all uncaught asynchronous errors that aren't handled by the Flutter framework to Crashlytics - PlatformDispatcher.instance.onError = (error, stack) { - FirebaseCrashlytics.instance.recordError(error, stack, fatal: true); - return true; - }; - } } class ProviderLogger extends ProviderObserver { diff --git a/test/binding.dart b/test/binding.dart index 61509c6a85..fb8b67e024 100644 --- a/test/binding.dart +++ b/test/binding.dart @@ -4,8 +4,10 @@ import 'dart:ui'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/binding.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; +import 'package:logging/logging.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -14,7 +16,17 @@ TestLichessBinding get testBinding => TestLichessBinding.instance; /// Lichess binding for testing. class TestLichessBinding extends LichessBinding { - TestLichessBinding(); + TestLichessBinding() { + Logger.root.level = Level.FINE; + Logger.root.onRecord.listen((record) { + if (record.level >= Level.WARNING) { + // ignore: avoid_print + print( + '${DateFormat('H:m:s.S').format(record.time)} [${record.level}] ${record.loggerName}: ${record.message}', + ); + } + }); + } /// Initialize the binding if necessary, and ensure it is a [TestLichessBinding]. /// From 85c8f3d80132c111984cbe1093c0ffabbd7fcb15 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 3 Oct 2024 10:19:14 +0200 Subject: [PATCH 420/979] Fix android notification button icons --- android/app/src/main/res/raw/org_lichess_mobile_keep.xml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 android/app/src/main/res/raw/org_lichess_mobile_keep.xml diff --git a/android/app/src/main/res/raw/org_lichess_mobile_keep.xml b/android/app/src/main/res/raw/org_lichess_mobile_keep.xml new file mode 100644 index 0000000000..7d4fe18883 --- /dev/null +++ b/android/app/src/main/res/raw/org_lichess_mobile_keep.xml @@ -0,0 +1,3 @@ + + From a1e328ec8b5482f62ce104d68d391a25438afbd1 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 3 Oct 2024 10:26:37 +0200 Subject: [PATCH 421/979] Update podfile --- ios/Podfile.lock | 64 ++++++++++++++++++++++++------------------------ 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 69dbbc8133..19ef3e555f 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -14,34 +14,34 @@ PODS: - Flutter - device_info_plus (0.0.1): - Flutter - - Firebase/CoreOnly (11.0.0): - - FirebaseCore (= 11.0.0) - - Firebase/Crashlytics (11.0.0): + - Firebase/CoreOnly (11.2.0): + - FirebaseCore (= 11.2.0) + - Firebase/Crashlytics (11.2.0): - Firebase/CoreOnly - - FirebaseCrashlytics (~> 11.0.0) - - Firebase/Messaging (11.0.0): + - FirebaseCrashlytics (~> 11.2.0) + - Firebase/Messaging (11.2.0): - Firebase/CoreOnly - - FirebaseMessaging (~> 11.0.0) - - firebase_core (3.4.1): - - Firebase/CoreOnly (= 11.0.0) + - FirebaseMessaging (~> 11.2.0) + - firebase_core (3.6.0): + - Firebase/CoreOnly (= 11.2.0) - Flutter - - firebase_crashlytics (4.1.1): - - Firebase/Crashlytics (= 11.0.0) + - firebase_crashlytics (4.1.3): + - Firebase/Crashlytics (= 11.2.0) - firebase_core - Flutter - - firebase_messaging (15.1.1): - - Firebase/Messaging (= 11.0.0) + - firebase_messaging (15.1.3): + - Firebase/Messaging (= 11.2.0) - firebase_core - Flutter - - FirebaseCore (11.0.0): + - FirebaseCore (11.2.0): - FirebaseCoreInternal (~> 11.0) - GoogleUtilities/Environment (~> 8.0) - GoogleUtilities/Logger (~> 8.0) - - FirebaseCoreExtension (11.1.0): + - FirebaseCoreExtension (11.3.0): - FirebaseCore (~> 11.0) - - FirebaseCoreInternal (11.1.0): + - FirebaseCoreInternal (11.3.0): - "GoogleUtilities/NSData+zlib (~> 8.0)" - - FirebaseCrashlytics (11.0.0): + - FirebaseCrashlytics (11.2.0): - FirebaseCore (~> 11.0) - FirebaseInstallations (~> 11.0) - FirebaseRemoteConfigInterop (~> 11.0) @@ -50,12 +50,12 @@ PODS: - GoogleUtilities/Environment (~> 8.0) - nanopb (~> 3.30910.0) - PromisesObjC (~> 2.4) - - FirebaseInstallations (11.1.0): + - FirebaseInstallations (11.3.0): - FirebaseCore (~> 11.0) - GoogleUtilities/Environment (~> 8.0) - GoogleUtilities/UserDefaults (~> 8.0) - PromisesObjC (~> 2.4) - - FirebaseMessaging (11.0.0): + - FirebaseMessaging (11.2.0): - FirebaseCore (~> 11.0) - FirebaseInstallations (~> 11.0) - GoogleDataTransport (~> 10.0) @@ -64,8 +64,8 @@ PODS: - GoogleUtilities/Reachability (~> 8.0) - GoogleUtilities/UserDefaults (~> 8.0) - nanopb (~> 3.30910.0) - - FirebaseRemoteConfigInterop (11.1.0) - - FirebaseSessions (11.1.0): + - FirebaseRemoteConfigInterop (11.3.0) + - FirebaseSessions (11.3.0): - FirebaseCore (~> 11.0) - FirebaseCoreExtension (~> 11.0) - FirebaseInstallations (~> 11.0) @@ -232,18 +232,18 @@ SPEC CHECKSUMS: connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db cupertino_http: 1a3a0f163c1b26e7f1a293b33d476e0fde7a64ec device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d - Firebase: 9f574c08c2396885b5e7e100ed4293d956218af9 - firebase_core: ba84e940cf5cbbc601095f86556560937419195c - firebase_crashlytics: 4111f8198b78c99471c955af488cecd8224967e6 - firebase_messaging: c40f84e7a98da956d5262fada373b5c458edcf13 - FirebaseCore: 3cf438f431f18c12cdf2aaf64434648b63f7e383 - FirebaseCoreExtension: aa5c9779c2d0d39d83f1ceb3fdbafe80c4feecfa - FirebaseCoreInternal: adefedc9a88dbe393c4884640a73ec9e8e790f8c - FirebaseCrashlytics: 745d8f0221fe49c62865391d1bf56f5a12eeec0b - FirebaseInstallations: d0a8fea5a6fa91abc661591cf57c0f0d70863e57 - FirebaseMessaging: d2d1d9c62c46dd2db49a952f7deb5b16ad2c9742 - FirebaseRemoteConfigInterop: abf8b1bbc0bf1b84abd22b66746926410bf91a87 - FirebaseSessions: 78f137e68dc01ca71606169ba4ac73b98c13752a + Firebase: 98e6bf5278170668a7983e12971a66b2cd57fc8c + firebase_core: 2bedc3136ec7c7b8561c6123ed0239387b53f2af + firebase_crashlytics: 37d104d457b51760b48504a93a12b3bf70995d77 + firebase_messaging: 15d114e1a41fc31e4fbabcd48d765a19eec94a38 + FirebaseCore: a282032ae9295c795714ded2ec9c522fc237f8da + FirebaseCoreExtension: 30bb063476ef66cd46925243d64ad8b2c8ac3264 + FirebaseCoreInternal: ac26d09a70c730e497936430af4e60fb0c68ec4e + FirebaseCrashlytics: cfc69af5b53565dc6a5e563788809b5778ac4eac + FirebaseInstallations: 58cf94dabf1e2bb2fa87725a9be5c2249171cda0 + FirebaseMessaging: c9ec7b90c399c7a6100297e9d16f8a27fc7f7152 + FirebaseRemoteConfigInterop: c3a5c31b3c22079f41ba1dc645df889d9ce38cb9 + FirebaseSessions: 655ff17f3cc1a635cbdc2d69b953878001f9e25b Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_appauth: 1ce438877bc111c5d8f42da47729909290624886 flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086 From bf6747153f5a5dec3133389757e9929b7282ebf6 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 3 Oct 2024 18:50:12 +0200 Subject: [PATCH 422/979] Move socket and http to network folder --- lib/src/app.dart | 29 ++++++--- lib/src/binding.dart | 2 +- lib/src/init.dart | 2 +- .../model/account/account_preferences.dart | 2 +- lib/src/model/account/account_repository.dart | 2 +- .../analysis/server_analysis_service.dart | 4 +- lib/src/model/auth/auth_controller.dart | 4 +- lib/src/model/auth/auth_repository.dart | 2 +- .../model/broadcast/broadcast_providers.dart | 2 +- .../model/broadcast/broadcast_repository.dart | 2 +- .../broadcast/broadcast_round_controller.dart | 4 +- .../model/challenge/challenge_repository.dart | 2 +- .../model/challenge/challenge_service.dart | 2 +- .../correspondence_service.dart | 4 +- lib/src/model/game/chat_controller.dart | 2 +- lib/src/model/game/game.dart | 2 +- lib/src/model/game/game_controller.dart | 4 +- lib/src/model/game/game_history.dart | 2 +- lib/src/model/game/game_repository.dart | 2 +- .../model/game/game_repository_providers.dart | 2 +- lib/src/model/game/game_share_service.dart | 2 +- lib/src/model/lobby/create_game_service.dart | 4 +- lib/src/model/lobby/lobby_numbers.dart | 2 +- lib/src/model/lobby/lobby_repository.dart | 2 +- .../notifications/notification_service.dart | 2 +- .../opening_explorer_repository.dart | 2 +- lib/src/model/puzzle/puzzle_activity.dart | 2 +- lib/src/model/puzzle/puzzle_controller.dart | 2 +- lib/src/model/puzzle/puzzle_providers.dart | 2 +- lib/src/model/puzzle/puzzle_repository.dart | 2 +- lib/src/model/puzzle/puzzle_service.dart | 2 +- lib/src/model/puzzle/storm_controller.dart | 2 +- lib/src/model/relation/online_friends.dart | 2 +- .../model/relation/relation_repository.dart | 2 +- .../relation_repository_providers.dart | 2 +- lib/src/model/study/study_repository.dart | 2 +- lib/src/model/tv/live_tv_channels.dart | 4 +- lib/src/model/tv/tv_controller.dart | 4 +- lib/src/model/tv/tv_repository.dart | 2 +- lib/src/model/user/user_repository.dart | 2 +- .../model/user/user_repository_providers.dart | 2 +- lib/src/{model/common => network}/http.dart | 28 ++++++-- lib/src/{model/common => network}/socket.dart | 2 +- lib/src/utils/connectivity.dart | 2 +- lib/src/view/account/edit_profile_screen.dart | 2 +- lib/src/view/analysis/analysis_screen.dart | 2 +- .../broadcast/broadcast_round_screen.dart | 2 +- lib/src/view/game/archived_game_screen.dart | 2 +- lib/src/view/game/game_common_widgets.dart | 2 +- lib/src/view/game/game_list_tile.dart | 2 +- lib/src/view/game/game_screen.dart | 2 +- lib/src/view/game/ping_rating.dart | 2 +- .../view/play/create_custom_game_screen.dart | 2 +- lib/src/view/play/online_bots_screen.dart | 2 +- lib/src/view/puzzle/puzzle_screen.dart | 2 +- lib/src/view/puzzle/streak_screen.dart | 2 +- lib/src/view/relation/following_screen.dart | 2 +- lib/src/view/user/perf_stats_screen.dart | 2 +- lib/src/view/user/user_activity.dart | 2 +- lib/src/view/user/user_screen.dart | 2 +- lib/src/view/watch/watch_tab_screen.dart | 2 +- test/app_test.dart | 59 ++++++++++++++++- test/mock_server_responses.dart | 40 ++++++++++++ .../account/account_repository_test.dart | 2 +- test/model/auth/auth_controller_test.dart | 64 ++++--------------- .../broadcast/broadcast_repository_test.dart | 2 +- .../challenge/challenge_repository_test.dart | 2 +- .../challenge/challenge_service_test.dart | 4 +- test/model/game/game_repository_test.dart | 2 +- test/model/lobby/lobby_repository_test.dart | 2 +- .../notification_service_test.dart | 3 +- .../opening_explorer_repository_test.dart | 2 +- test/model/puzzle/puzzle_repository_test.dart | 2 +- test/model/puzzle/puzzle_service_test.dart | 4 +- .../relation/relation_repository_test.dart | 2 +- test/model/user/user_repository_test.dart | 2 +- .../fake_websocket_channel.dart | 2 +- .../common => network}/socket_test.dart | 2 +- test/test_container.dart | 6 +- test/test_provider_scope.dart | 12 ++-- test/view/analysis/analysis_screen_test.dart | 4 +- .../board_editor_screen_test.dart | 20 +++--- .../broadcasts_list_screen_test.dart | 6 +- .../coordinate_training_screen_test.dart | 6 +- test/view/game/archived_game_screen_test.dart | 8 +-- .../opening_explorer_screen_test.dart | 8 +-- .../over_the_board_screen_test.dart | 2 +- test/view/puzzle/puzzle_screen_test.dart | 14 ++-- test/view/puzzle/storm_screen_test.dart | 12 ++-- .../settings/settings_tab_screen_test.dart | 8 +-- test/view/user/leaderboard_screen_test.dart | 4 +- test/view/user/leaderboard_widget_test.dart | 4 +- test/view/user/perf_stats_screen_test.dart | 6 +- test/view/user/search_screen_test.dart | 6 +- test/view/user/user_screen_test.dart | 4 +- 95 files changed, 303 insertions(+), 214 deletions(-) rename lib/src/{model/common => network}/http.dart (94%) rename lib/src/{model/common => network}/socket.dart (99%) create mode 100644 test/mock_server_responses.dart rename test/{model/common => network}/fake_websocket_channel.dart (98%) rename test/{model/common => network}/socket_test.dart (99%) diff --git a/lib/src/app.dart b/lib/src/app.dart index 11f86677b5..48bbd7e3ab 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -10,17 +10,20 @@ import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/challenge/challenge_service.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; -import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/correspondence/correspondence_service.dart'; import 'package:lichess_mobile/src/model/notifications/notification_service.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/model/settings/brightness.dart'; import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; import 'package:lichess_mobile/src/navigation.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/network/socket.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; +import 'package:logging/logging.dart'; + +final _logger = Logger('Application'); /// The main application widget. /// @@ -52,9 +55,6 @@ class _AppState extends ConsumerState { }, ); - // check if session is still active and delete it if it is not - checkSession(); - // Start services ref.read(notificationServiceProvider).start(); ref.read(challengeServiceProvider).start(); @@ -76,6 +76,9 @@ class _AppState extends ConsumerState { // Perform actions once when the app comes online. if (current.value?.isOnline == true && !_firstTimeOnlineCheck) { _firstTimeOnlineCheck = true; + // check if session is still active and delete it if it is not + checkSession(); + ref.read(correspondenceServiceProvider).syncGames(); } @@ -208,16 +211,24 @@ class _AppState extends ConsumerState { // check if session is still active final session = ref.read(authSessionProvider); if (session != null) { + _logger.fine( + 'Found a stored session: ${session.user.id}. Checking if it is still active.', + ); try { final client = ref.read(lichessClientProvider); - final response = await client - .get(Uri(path: '/api/account')) + final data = await client + .postReadJson( + Uri(path: '/api/token/test'), + body: session.token, + mapper: (json) => json, + ) .timeout(const Duration(seconds: 3)); - if (response.statusCode == 401) { + if (data[session.token] == null) { + _logger.fine('Session is not active. Deleting it.'); await ref.read(authSessionProvider.notifier).delete(); } } catch (e) { - debugPrint('Could not check session: $e'); + _logger.warning('Could not check session: $e'); } } } diff --git a/lib/src/binding.dart b/lib/src/binding.dart index 1ddfbf9ebe..c001f7fa87 100644 --- a/lib/src/binding.dart +++ b/lib/src/binding.dart @@ -13,9 +13,9 @@ import 'package:lichess_mobile/src/db/secure_storage.dart'; import 'package:lichess_mobile/src/log.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/auth/session_storage.dart'; -import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/notifications/notification_service.dart'; import 'package:lichess_mobile/src/model/notifications/notifications.dart'; +import 'package:lichess_mobile/src/network/socket.dart'; import 'package:lichess_mobile/src/utils/string.dart'; import 'package:lichess_mobile/src/utils/system.dart'; import 'package:logging/logging.dart'; diff --git a/lib/src/init.dart b/lib/src/init.dart index 1965c7eed1..837a5a0c60 100644 --- a/lib/src/init.dart +++ b/lib/src/init.dart @@ -6,9 +6,9 @@ import 'package:flutter/services.dart'; import 'package:flutter_displaymode/flutter_displaymode.dart'; import 'package:lichess_mobile/src/binding.dart'; import 'package:lichess_mobile/src/db/secure_storage.dart'; -import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/model/settings/preferences.dart'; +import 'package:lichess_mobile/src/network/socket.dart'; import 'package:lichess_mobile/src/utils/color_palette.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/utils/string.dart'; diff --git a/lib/src/model/account/account_preferences.dart b/lib/src/model/account/account_preferences.dart index 60d89224e3..ee6950f089 100644 --- a/lib/src/model/account/account_preferences.dart +++ b/lib/src/model/account/account_preferences.dart @@ -1,7 +1,7 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/widgets.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; diff --git a/lib/src/model/account/account_repository.dart b/lib/src/model/account/account_repository.dart index b3e3a84203..be4f276dc3 100644 --- a/lib/src/model/account/account_repository.dart +++ b/lib/src/model/account/account_repository.dart @@ -3,12 +3,12 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:http/http.dart' as http; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; import 'package:lichess_mobile/src/model/common/speed.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/model/user/user_repository.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; diff --git a/lib/src/model/analysis/server_analysis_service.dart b/lib/src/model/analysis/server_analysis_service.dart index 6db960d91f..a7e740a60e 100644 --- a/lib/src/model/analysis/server_analysis_service.dart +++ b/lib/src/model/analysis/server_analysis_service.dart @@ -2,11 +2,11 @@ import 'dart:async'; import 'package:dartchess/dartchess.dart'; import 'package:flutter/foundation.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/game/game_repository.dart'; import 'package:lichess_mobile/src/model/game/game_socket_events.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/network/socket.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'server_analysis_service.g.dart'; diff --git a/lib/src/model/auth/auth_controller.dart b/lib/src/model/auth/auth_controller.dart index afbdccf1bd..00c319efe7 100644 --- a/lib/src/model/auth/auth_controller.dart +++ b/lib/src/model/auth/auth_controller.dart @@ -1,7 +1,7 @@ import 'package:lichess_mobile/src/model/auth/auth_session.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; -import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/notifications/notification_service.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/network/socket.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'auth_repository.dart'; diff --git a/lib/src/model/auth/auth_repository.dart b/lib/src/model/auth/auth_repository.dart index bc7a302845..57f2b4a79f 100644 --- a/lib/src/model/auth/auth_repository.dart +++ b/lib/src/model/auth/auth_repository.dart @@ -4,8 +4,8 @@ import 'package:http/http.dart' as http; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/auth/bearer.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; diff --git a/lib/src/model/broadcast/broadcast_providers.dart b/lib/src/model/broadcast/broadcast_providers.dart index 0642c913e6..2e46ce4f87 100644 --- a/lib/src/model/broadcast/broadcast_providers.dart +++ b/lib/src/model/broadcast/broadcast_providers.dart @@ -1,6 +1,6 @@ import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_repository.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'broadcast_providers.g.dart'; diff --git a/lib/src/model/broadcast/broadcast_repository.dart b/lib/src/model/broadcast/broadcast_repository.dart index 05c6b6e7fb..d461efe9c1 100644 --- a/lib/src/model/broadcast/broadcast_repository.dart +++ b/lib/src/model/broadcast/broadcast_repository.dart @@ -3,8 +3,8 @@ import 'package:deep_pick/deep_pick.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/utils/json.dart'; class BroadcastRepository { diff --git a/lib/src/model/broadcast/broadcast_round_controller.dart b/lib/src/model/broadcast/broadcast_round_controller.dart index d14118d8e2..808f517267 100644 --- a/lib/src/model/broadcast/broadcast_round_controller.dart +++ b/lib/src/model/broadcast/broadcast_round_controller.dart @@ -6,9 +6,9 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_repository.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/model/common/socket.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/network/socket.dart'; import 'package:lichess_mobile/src/utils/json.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; diff --git a/lib/src/model/challenge/challenge_repository.dart b/lib/src/model/challenge/challenge_repository.dart index e44d5a7079..67b091bc0e 100644 --- a/lib/src/model/challenge/challenge_repository.dart +++ b/lib/src/model/challenge/challenge_repository.dart @@ -4,8 +4,8 @@ import 'package:deep_pick/deep_pick.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:http/http.dart' as http; import 'package:lichess_mobile/src/model/challenge/challenge.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'challenge_repository.g.dart'; diff --git a/lib/src/model/challenge/challenge_service.dart b/lib/src/model/challenge/challenge_service.dart index 37a076f04c..294aefb154 100644 --- a/lib/src/model/challenge/challenge_service.dart +++ b/lib/src/model/challenge/challenge_service.dart @@ -7,10 +7,10 @@ import 'package:flutter/widgets.dart'; import 'package:lichess_mobile/src/model/challenge/challenge.dart'; import 'package:lichess_mobile/src/model/challenge/challenge_repository.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/notifications/notification_service.dart'; import 'package:lichess_mobile/src/model/notifications/notifications.dart'; import 'package:lichess_mobile/src/navigation.dart'; +import 'package:lichess_mobile/src/network/socket.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/game/game_screen.dart'; diff --git a/lib/src/model/correspondence/correspondence_service.dart b/lib/src/model/correspondence/correspondence_service.dart index f1e89d5f2b..634221fcbc 100644 --- a/lib/src/model/correspondence/correspondence_service.dart +++ b/lib/src/model/correspondence/correspondence_service.dart @@ -8,15 +8,15 @@ import 'package:flutter/widgets.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/auth/bearer.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/correspondence/correspondence_game_storage.dart'; import 'package:lichess_mobile/src/model/correspondence/offline_correspondence_game.dart'; import 'package:lichess_mobile/src/model/game/game_repository.dart'; import 'package:lichess_mobile/src/model/game/game_socket_events.dart'; import 'package:lichess_mobile/src/model/game/playable_game.dart'; import 'package:lichess_mobile/src/navigation.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/network/socket.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/game/game_screen.dart'; import 'package:logging/logging.dart'; diff --git a/lib/src/model/game/chat_controller.dart b/lib/src/model/game/chat_controller.dart index 1d4f8c9cc2..ddddc8f936 100644 --- a/lib/src/model/game/chat_controller.dart +++ b/lib/src/model/game/chat_controller.dart @@ -5,8 +5,8 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/db/database.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/game/game_controller.dart'; +import 'package:lichess_mobile/src/network/socket.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:sqflite/sqflite.dart'; diff --git a/lib/src/model/game/game.dart b/lib/src/model/game/game.dart index a3408e0809..13d11db993 100644 --- a/lib/src/model/game/game.dart +++ b/lib/src/model/game/game.dart @@ -8,11 +8,11 @@ import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/model/account/account_preferences.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/eval.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/node.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; import 'package:lichess_mobile/src/model/common/speed.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'game_status.dart'; import 'material_diff.dart'; diff --git a/lib/src/model/game/game_controller.dart b/lib/src/model/game/game_controller.dart index 89b866cdc6..a75e1b8f3e 100644 --- a/lib/src/model/game/game_controller.dart +++ b/lib/src/model/game/game_controller.dart @@ -13,11 +13,9 @@ import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/analysis/server_analysis_service.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/service/move_feedback.dart'; import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; -import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/common/speed.dart'; import 'package:lichess_mobile/src/model/correspondence/correspondence_service.dart'; import 'package:lichess_mobile/src/model/game/archived_game.dart'; @@ -29,6 +27,8 @@ import 'package:lichess_mobile/src/model/game/game_storage.dart'; import 'package:lichess_mobile/src/model/game/material_diff.dart'; import 'package:lichess_mobile/src/model/game/playable_game.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/network/socket.dart'; import 'package:lichess_mobile/src/utils/rate_limit.dart'; import 'package:logging/logging.dart'; import 'package:result_extensions/result_extensions.dart'; diff --git a/lib/src/model/game/game_history.dart b/lib/src/model/game/game_history.dart index fa71d886b5..dbea52b366 100644 --- a/lib/src/model/game/game_history.dart +++ b/lib/src/model/game/game_history.dart @@ -6,7 +6,6 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/game/archived_game.dart'; import 'package:lichess_mobile/src/model/game/game_filter.dart'; @@ -14,6 +13,7 @@ import 'package:lichess_mobile/src/model/game/game_repository.dart'; import 'package:lichess_mobile/src/model/game/game_storage.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/model/user/user_repository_providers.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:lichess_mobile/src/utils/riverpod.dart'; import 'package:result_extensions/result_extensions.dart'; diff --git a/lib/src/model/game/game_repository.dart b/lib/src/model/game/game_repository.dart index 7d342ef6c6..84f56e4ad2 100644 --- a/lib/src/model/game/game_repository.dart +++ b/lib/src/model/game/game_repository.dart @@ -1,12 +1,12 @@ import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:http/http.dart' as http; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; import 'package:lichess_mobile/src/model/game/archived_game.dart'; import 'package:lichess_mobile/src/model/game/game_filter.dart'; import 'package:lichess_mobile/src/model/game/playable_game.dart'; +import 'package:lichess_mobile/src/network/http.dart'; class GameRepository { const GameRepository(this.client); diff --git a/lib/src/model/game/game_repository_providers.dart b/lib/src/model/game/game_repository_providers.dart index 5006f2c6c3..26fd2d839a 100644 --- a/lib/src/model/game/game_repository_providers.dart +++ b/lib/src/model/game/game_repository_providers.dart @@ -1,8 +1,8 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/game/archived_game.dart'; import 'package:lichess_mobile/src/model/game/game_storage.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'game_repository.dart'; diff --git a/lib/src/model/game/game_share_service.dart b/lib/src/model/game/game_share_service.dart index 837fba6753..8723a221a6 100644 --- a/lib/src/model/game/game_share_service.dart +++ b/lib/src/model/game/game_share_service.dart @@ -2,9 +2,9 @@ import 'dart:convert'; import 'package:dartchess/dartchess.dart'; import 'package:lichess_mobile/src/constants.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:share_plus/share_plus.dart'; diff --git a/lib/src/model/lobby/create_game_service.dart b/lib/src/model/lobby/create_game_service.dart index cb2fd64c7f..30f1f9c45c 100644 --- a/lib/src/model/lobby/create_game_service.dart +++ b/lib/src/model/lobby/create_game_service.dart @@ -5,11 +5,11 @@ import 'package:lichess_mobile/src/binding.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/challenge/challenge.dart'; import 'package:lichess_mobile/src/model/challenge/challenge_repository.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/lobby/game_seek.dart'; import 'package:lichess_mobile/src/model/lobby/lobby_repository.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/network/socket.dart'; import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; diff --git a/lib/src/model/lobby/lobby_numbers.dart b/lib/src/model/lobby/lobby_numbers.dart index db7131ef96..adb4bf22e3 100644 --- a/lib/src/model/lobby/lobby_numbers.dart +++ b/lib/src/model/lobby/lobby_numbers.dart @@ -1,6 +1,6 @@ import 'dart:async'; -import 'package:lichess_mobile/src/model/common/socket.dart'; +import 'package:lichess_mobile/src/network/socket.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'lobby_numbers.g.dart'; diff --git a/lib/src/model/lobby/lobby_repository.dart b/lib/src/model/lobby/lobby_repository.dart index 38cad7d6e2..a6a95f1df3 100644 --- a/lib/src/model/lobby/lobby_repository.dart +++ b/lib/src/model/lobby/lobby_repository.dart @@ -2,9 +2,9 @@ import 'package:deep_pick/deep_pick.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:http/http.dart' as http; import 'package:lichess_mobile/src/model/common/chess.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'correspondence_challenge.dart'; diff --git a/lib/src/model/notifications/notification_service.dart b/lib/src/model/notifications/notification_service.dart index 41be595dd0..c3c378750f 100644 --- a/lib/src/model/notifications/notification_service.dart +++ b/lib/src/model/notifications/notification_service.dart @@ -8,9 +8,9 @@ import 'package:lichess_mobile/l10n/l10n.dart'; import 'package:lichess_mobile/src/binding.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/challenge/challenge_service.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/correspondence/correspondence_service.dart'; import 'package:lichess_mobile/src/model/notifications/notifications.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/utils/badge_service.dart'; import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:lichess_mobile/src/utils/l10n.dart'; diff --git a/lib/src/model/opening_explorer/opening_explorer_repository.dart b/lib/src/model/opening_explorer/opening_explorer_repository.dart index c4b7938c33..f8f6d5210b 100644 --- a/lib/src/model/opening_explorer/opening_explorer_repository.dart +++ b/lib/src/model/opening_explorer/opening_explorer_repository.dart @@ -4,10 +4,10 @@ import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:http/http.dart'; import 'package:lichess_mobile/src/constants.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/speed.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_preferences.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'opening_explorer_repository.g.dart'; diff --git a/lib/src/model/puzzle/puzzle_activity.dart b/lib/src/model/puzzle/puzzle_activity.dart index 55a7a71cbf..9addb76e85 100644 --- a/lib/src/model/puzzle/puzzle_activity.dart +++ b/lib/src/model/puzzle/puzzle_activity.dart @@ -3,10 +3,10 @@ import 'dart:async'; import 'package:async/async.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_providers.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_repository.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/utils/riverpod.dart'; import 'package:result_extensions/result_extensions.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; diff --git a/lib/src/model/puzzle/puzzle_controller.dart b/lib/src/model/puzzle/puzzle_controller.dart index 425ebafcb6..56a2a5e462 100644 --- a/lib/src/model/puzzle/puzzle_controller.dart +++ b/lib/src/model/puzzle/puzzle_controller.dart @@ -6,7 +6,6 @@ import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/node.dart'; import 'package:lichess_mobile/src/model/common/service/move_feedback.dart'; import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; @@ -22,6 +21,7 @@ import 'package:lichess_mobile/src/model/puzzle/puzzle_service.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_session.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_streak.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_theme.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/utils/rate_limit.dart'; import 'package:result_extensions/result_extensions.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; diff --git a/lib/src/model/puzzle/puzzle_providers.dart b/lib/src/model/puzzle/puzzle_providers.dart index de22f470ff..8487c90ce2 100644 --- a/lib/src/model/puzzle/puzzle_providers.dart +++ b/lib/src/model/puzzle/puzzle_providers.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_angle.dart'; @@ -13,6 +12,7 @@ import 'package:lichess_mobile/src/model/puzzle/puzzle_service.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_storage.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_theme.dart'; import 'package:lichess_mobile/src/model/puzzle/storm.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'puzzle_providers.g.dart'; diff --git a/lib/src/model/puzzle/puzzle_repository.dart b/lib/src/model/puzzle/puzzle_repository.dart index de6028f972..cd47368e86 100644 --- a/lib/src/model/puzzle/puzzle_repository.dart +++ b/lib/src/model/puzzle/puzzle_repository.dart @@ -8,9 +8,9 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:http/http.dart' as http; import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/utils/json.dart'; import 'puzzle.dart'; diff --git a/lib/src/model/puzzle/puzzle_service.dart b/lib/src/model/puzzle/puzzle_service.dart index 0989724dcf..f247d7c692 100644 --- a/lib/src/model/puzzle/puzzle_service.dart +++ b/lib/src/model/puzzle/puzzle_service.dart @@ -3,9 +3,9 @@ import 'dart:math' show max; import 'package:async/async.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_storage.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:logging/logging.dart'; import 'package:result_extensions/result_extensions.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; diff --git a/lib/src/model/puzzle/storm_controller.dart b/lib/src/model/puzzle/storm_controller.dart index 60300aaf43..544c890db9 100644 --- a/lib/src/model/puzzle/storm_controller.dart +++ b/lib/src/model/puzzle/storm_controller.dart @@ -9,9 +9,9 @@ import 'package:flutter/services.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/service/move_feedback.dart'; import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:result_extensions/result_extensions.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; diff --git a/lib/src/model/relation/online_friends.dart b/lib/src/model/relation/online_friends.dart index 7997c8b37c..c18466d198 100644 --- a/lib/src/model/relation/online_friends.dart +++ b/lib/src/model/relation/online_friends.dart @@ -2,8 +2,8 @@ import 'dart:async'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; +import 'package:lichess_mobile/src/network/socket.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'online_friends.g.dart'; diff --git a/lib/src/model/relation/relation_repository.dart b/lib/src/model/relation/relation_repository.dart index e2baf82e5d..b82ee33325 100644 --- a/lib/src/model/relation/relation_repository.dart +++ b/lib/src/model/relation/relation_repository.dart @@ -1,8 +1,8 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:http/http.dart' as http; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; +import 'package:lichess_mobile/src/network/http.dart'; class RelationRepository { const RelationRepository(this.client); diff --git a/lib/src/model/relation/relation_repository_providers.dart b/lib/src/model/relation/relation_repository_providers.dart index 9357213429..2c80f6144a 100644 --- a/lib/src/model/relation/relation_repository_providers.dart +++ b/lib/src/model/relation/relation_repository_providers.dart @@ -1,6 +1,6 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'relation_repository.dart'; diff --git a/lib/src/model/study/study_repository.dart b/lib/src/model/study/study_repository.dart index bedc2ff78a..b0e6b84461 100644 --- a/lib/src/model/study/study_repository.dart +++ b/lib/src/model/study/study_repository.dart @@ -1,9 +1,9 @@ import 'package:deep_pick/deep_pick.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:http/http.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/study/study.dart'; import 'package:lichess_mobile/src/model/study/study_filter.dart'; +import 'package:lichess_mobile/src/network/http.dart'; class StudyRepository { StudyRepository(this.client); diff --git a/lib/src/model/tv/live_tv_channels.dart b/lib/src/model/tv/live_tv_channels.dart index 0bd44d76f6..cdc07605b4 100644 --- a/lib/src/model/tv/live_tv_channels.dart +++ b/lib/src/model/tv/live_tv_channels.dart @@ -3,9 +3,9 @@ import 'dart:async'; import 'package:dartchess/dartchess.dart'; import 'package:deep_pick/deep_pick.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; -import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/tv/tv_socket_events.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/network/socket.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'featured_player.dart'; diff --git a/lib/src/model/tv/tv_controller.dart b/lib/src/model/tv/tv_controller.dart index c6d97744c6..68e0dd13b9 100644 --- a/lib/src/model/tv/tv_controller.dart +++ b/lib/src/model/tv/tv_controller.dart @@ -4,10 +4,8 @@ import 'package:dartchess/dartchess.dart'; import 'package:deep_pick/deep_pick.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; -import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/game/game.dart'; import 'package:lichess_mobile/src/model/game/game_socket_events.dart'; import 'package:lichess_mobile/src/model/game/game_status.dart'; @@ -16,6 +14,8 @@ import 'package:lichess_mobile/src/model/game/playable_game.dart'; import 'package:lichess_mobile/src/model/tv/tv_channel.dart'; import 'package:lichess_mobile/src/model/tv/tv_repository.dart'; import 'package:lichess_mobile/src/model/tv/tv_socket_events.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/network/socket.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'tv_controller.freezed.dart'; diff --git a/lib/src/model/tv/tv_repository.dart b/lib/src/model/tv/tv_repository.dart index 804454ce27..aaff5f5914 100644 --- a/lib/src/model/tv/tv_repository.dart +++ b/lib/src/model/tv/tv_repository.dart @@ -2,9 +2,9 @@ import 'package:deep_pick/deep_pick.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:http/http.dart' as http; import 'package:lichess_mobile/src/model/common/chess.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import './tv_channel.dart'; import './tv_game.dart'; diff --git a/lib/src/model/user/user_repository.dart b/lib/src/model/user/user_repository.dart index d77c4a7416..858ff76695 100644 --- a/lib/src/model/user/user_repository.dart +++ b/lib/src/model/user/user_repository.dart @@ -1,10 +1,10 @@ import 'package:collection/collection.dart'; import 'package:deep_pick/deep_pick.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; import 'package:lichess_mobile/src/model/user/leaderboard.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/utils/json.dart'; import 'streamer.dart'; diff --git a/lib/src/model/user/user_repository_providers.dart b/lib/src/model/user/user_repository_providers.dart index 9e45d0917d..cdd6c83240 100644 --- a/lib/src/model/user/user_repository_providers.dart +++ b/lib/src/model/user/user_repository_providers.dart @@ -1,8 +1,8 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; import 'package:lichess_mobile/src/model/user/leaderboard.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'streamer.dart'; diff --git a/lib/src/model/common/http.dart b/lib/src/network/http.dart similarity index 94% rename from lib/src/model/common/http.dart rename to lib/src/network/http.dart index d3ad3f73aa..94d9a34886 100644 --- a/lib/src/model/common/http.dart +++ b/lib/src/network/http.dart @@ -146,10 +146,14 @@ class LoggingClient extends BaseClient { /// Lichess HTTP client. /// -/// - All requests go to the lichess server, defined in [kLichessHost]. -/// - Sets the Authorization header when a token has been stored. -/// - Sets the user-agent header with the app version, build number, and device info. If the user is logged in, it also includes the user's id. -/// - Logs all requests and responses with status code >= 400. +/// * All requests made with [head], [get], [post], [put], [patch], [delete] target +/// the lichess server, defined in [kLichessHost]. It does not apply to the low-level +/// [send] method. +/// * Sets the Authorization header when a token has been stored. +/// * Sets the user-agent header with the app version, build number, and device info. If the user is logged in, it also includes the user's id. +/// * Logs all requests and responses with status code >= 400. +/// * When a response has the 401 status, checks if the session token is still valid, +/// and deletes the session if it's not. class LichessClient implements Client { LichessClient(this._inner, this._ref); @@ -180,6 +184,10 @@ class LichessClient implements Client { _logIfError(response); + if (response.statusCode == 401 && session != null) { + _checkSessionToken(session); + } + return response; } catch (e, st) { _logger.warning('Request to ${request.url} failed: $e', e, st); @@ -187,6 +195,18 @@ class LichessClient implements Client { } } + /// Checks if the session token is still valid, and delete session if it's not. + Future _checkSessionToken(AuthSessionState session) async { + final data = await postReadJson( + Uri(path: '/api/token/test'), + mapper: (json) => json, + ).timeout(const Duration(seconds: 5)); + if (data[session.token] == null) { + _logger.fine('Session is not active. Deleting it.'); + await _ref.read(authSessionProvider.notifier).delete(); + } + } + void _logIfError(BaseResponse response) { if (response.request != null && response.statusCode >= 400) { final request = response.request!; diff --git a/lib/src/model/common/socket.dart b/lib/src/network/socket.dart similarity index 99% rename from lib/src/model/common/socket.dart rename to lib/src/network/socket.dart index 34c28f223b..197c1a58ef 100644 --- a/lib/src/model/common/socket.dart +++ b/lib/src/network/socket.dart @@ -11,7 +11,7 @@ import 'package:lichess_mobile/src/binding.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/auth/bearer.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:logging/logging.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; diff --git a/lib/src/utils/connectivity.dart b/lib/src/utils/connectivity.dart index df9309a6da..6d80a51cee 100644 --- a/lib/src/utils/connectivity.dart +++ b/lib/src/utils/connectivity.dart @@ -5,7 +5,7 @@ import 'package:flutter/widgets.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:http/http.dart'; import 'package:lichess_mobile/src/constants.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/utils/rate_limit.dart'; import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; diff --git a/lib/src/view/account/edit_profile_screen.dart b/lib/src/view/account/edit_profile_screen.dart index 2a6529abaa..c567d5e362 100644 --- a/lib/src/view/account/edit_profile_screen.dart +++ b/lib/src/view/account/edit_profile_screen.dart @@ -3,8 +3,8 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/view/user/countries.dart'; diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index d49ba403b4..19a103a2cd 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -15,13 +15,13 @@ import 'package:lichess_mobile/src/model/analysis/server_analysis_service.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/eval.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/engine/engine.dart'; import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; import 'package:lichess_mobile/src/model/game/game_repository_providers.dart'; import 'package:lichess_mobile/src/model/game/game_share_service.dart'; import 'package:lichess_mobile/src/model/settings/brightness.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/connectivity.dart'; diff --git a/lib/src/view/broadcast/broadcast_round_screen.dart b/lib/src/view/broadcast/broadcast_round_screen.dart index f3a2a7b4e9..4a4d828799 100644 --- a/lib/src/view/broadcast/broadcast_round_screen.dart +++ b/lib/src/view/broadcast/broadcast_round_screen.dart @@ -7,8 +7,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_round_controller.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/duration.dart'; import 'package:lichess_mobile/src/utils/lichess_assets.dart'; diff --git a/lib/src/view/game/archived_game_screen.dart b/lib/src/view/game/archived_game_screen.dart index d9418fec6a..a4666b2863 100644 --- a/lib/src/view/game/archived_game_screen.dart +++ b/lib/src/view/game/archived_game_screen.dart @@ -4,11 +4,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/game/archived_game.dart'; import 'package:lichess_mobile/src/model/game/game.dart'; import 'package:lichess_mobile/src/model/game/game_repository_providers.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; diff --git a/lib/src/view/game/game_common_widgets.dart b/lib/src/view/game/game_common_widgets.dart index a173509637..8d3718cf57 100644 --- a/lib/src/view/game/game_common_widgets.dart +++ b/lib/src/view/game/game_common_widgets.dart @@ -4,12 +4,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/model/challenge/challenge.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/time_increment.dart'; import 'package:lichess_mobile/src/model/game/game.dart'; import 'package:lichess_mobile/src/model/game/game_share_service.dart'; import 'package:lichess_mobile/src/model/lobby/game_seek.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/share.dart'; import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; diff --git a/lib/src/view/game/game_list_tile.dart b/lib/src/view/game/game_list_tile.dart index 1c1213c1b0..5009b84947 100644 --- a/lib/src/view/game/game_list_tile.dart +++ b/lib/src/view/game/game_list_tile.dart @@ -4,11 +4,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/game/archived_game.dart'; import 'package:lichess_mobile/src/model/game/game_share_service.dart'; import 'package:lichess_mobile/src/model/game/game_status.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/styles/lichess_colors.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; diff --git a/lib/src/view/game/game_screen.dart b/lib/src/view/game/game_screen.dart index 00e3c9c23e..9dda6892f9 100644 --- a/lib/src/view/game/game_screen.dart +++ b/lib/src/view/game/game_screen.dart @@ -4,13 +4,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/challenge/challenge.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/game/game_history.dart'; import 'package:lichess_mobile/src/model/lobby/create_game_service.dart'; import 'package:lichess_mobile/src/model/lobby/game_seek.dart'; import 'package:lichess_mobile/src/model/lobby/game_setup_preferences.dart'; import 'package:lichess_mobile/src/navigation.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/game/game_loading_board.dart'; diff --git a/lib/src/view/game/ping_rating.dart b/lib/src/view/game/ping_rating.dart index 6248f3e43a..4b1f416840 100644 --- a/lib/src/view/game/ping_rating.dart +++ b/lib/src/view/game/ping_rating.dart @@ -1,7 +1,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lichess_mobile/src/model/common/socket.dart'; +import 'package:lichess_mobile/src/network/socket.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; diff --git a/lib/src/view/play/create_custom_game_screen.dart b/lib/src/view/play/create_custom_game_screen.dart index 23348be800..a6ff680dfc 100644 --- a/lib/src/view/play/create_custom_game_screen.dart +++ b/lib/src/view/play/create_custom_game_screen.dart @@ -10,13 +10,13 @@ import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/game.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; -import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/common/time_increment.dart'; import 'package:lichess_mobile/src/model/lobby/create_game_service.dart'; import 'package:lichess_mobile/src/model/lobby/game_seek.dart'; import 'package:lichess_mobile/src/model/lobby/game_setup_preferences.dart'; import 'package:lichess_mobile/src/model/lobby/lobby_repository.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; +import 'package:lichess_mobile/src/network/socket.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; diff --git a/lib/src/view/play/online_bots_screen.dart b/lib/src/view/play/online_bots_screen.dart index 91139e47a0..74355f23d7 100644 --- a/lib/src/view/play/online_bots_screen.dart +++ b/lib/src/view/play/online_bots_screen.dart @@ -4,11 +4,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/model/user/user_repository.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; diff --git a/lib/src/view/puzzle/puzzle_screen.dart b/lib/src/view/puzzle/puzzle_screen.dart index 04e28521f4..fa701f8edf 100644 --- a/lib/src/view/puzzle/puzzle_screen.dart +++ b/lib/src/view/puzzle/puzzle_screen.dart @@ -8,7 +8,6 @@ import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; import 'package:lichess_mobile/src/model/game/game_repository_providers.dart'; @@ -22,6 +21,7 @@ import 'package:lichess_mobile/src/model/puzzle/puzzle_service.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_theme.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/navigation.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:lichess_mobile/src/utils/immersive_mode.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; diff --git a/lib/src/view/puzzle/streak_screen.dart b/lib/src/view/puzzle/streak_screen.dart index 1507428ed6..b682b49ebc 100644 --- a/lib/src/view/puzzle/streak_screen.dart +++ b/lib/src/view/puzzle/streak_screen.dart @@ -7,13 +7,13 @@ import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_angle.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_controller.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_providers.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_service.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_streak.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_theme.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/immersive_mode.dart'; diff --git a/lib/src/view/relation/following_screen.dart b/lib/src/view/relation/following_screen.dart index b3ce4c3c0f..18a72bbb7f 100644 --- a/lib/src/view/relation/following_screen.dart +++ b/lib/src/view/relation/following_screen.dart @@ -3,11 +3,11 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/relation/online_friends.dart'; import 'package:lichess_mobile/src/model/relation/relation_repository.dart'; import 'package:lichess_mobile/src/model/relation/relation_repository_providers.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; diff --git a/lib/src/view/user/perf_stats_screen.dart b/lib/src/view/user/perf_stats_screen.dart index 2ecda93d41..bbb6a612d9 100644 --- a/lib/src/view/user/perf_stats_screen.dart +++ b/lib/src/view/user/perf_stats_screen.dart @@ -11,12 +11,12 @@ import 'package:intl/intl.dart'; import 'package:lichess_mobile/l10n/l10n.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; import 'package:lichess_mobile/src/model/game/game_filter.dart'; import 'package:lichess_mobile/src/model/game/game_repository.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/model/user/user_repository_providers.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/duration.dart'; diff --git a/lib/src/view/user/user_activity.dart b/lib/src/view/user/user_activity.dart index faf9157550..d5cd531997 100644 --- a/lib/src/view/user/user_activity.dart +++ b/lib/src/view/user/user_activity.dart @@ -3,10 +3,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/model/user/user_repository.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/styles/lichess_colors.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; diff --git a/lib/src/view/user/user_screen.dart b/lib/src/view/user/user_screen.dart index 747e2210ae..7ae829104d 100644 --- a/lib/src/view/user/user_screen.dart +++ b/lib/src/view/user/user_screen.dart @@ -2,10 +2,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:http/http.dart' show ClientException; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/relation/relation_repository.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/model/user/user_repository_providers.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; diff --git a/lib/src/view/watch/watch_tab_screen.dart b/lib/src/view/watch/watch_tab_screen.dart index 68c49af20c..fd44e08d1b 100644 --- a/lib/src/view/watch/watch_tab_screen.dart +++ b/lib/src/view/watch/watch_tab_screen.dart @@ -5,13 +5,13 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_providers.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/tv/featured_player.dart'; import 'package:lichess_mobile/src/model/tv/tv_channel.dart'; import 'package:lichess_mobile/src/model/tv/tv_game.dart'; import 'package:lichess_mobile/src/model/tv/tv_repository.dart'; import 'package:lichess_mobile/src/model/user/user_repository_providers.dart'; import 'package:lichess_mobile/src/navigation.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; diff --git a/test/app_test.dart b/test/app_test.dart index 0017db01ce..86f6e0d476 100644 --- a/test/app_test.dart +++ b/test/app_test.dart @@ -1,14 +1,19 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:http/testing.dart'; import 'package:lichess_mobile/src/app.dart'; import 'package:lichess_mobile/src/navigation.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'mock_server_responses.dart'; +import 'model/auth/fake_session_storage.dart'; +import 'test_helpers.dart'; import 'test_provider_scope.dart'; void main() { testWidgets('App loads', (tester) async { - final app = await makeProviderScope( + final app = await makeTestProviderScope( tester, child: const Application(), ); @@ -20,7 +25,7 @@ void main() { testWidgets('App loads with system theme, which defaults to light', (tester) async { - final app = await makeProviderScope( + final app = await makeTestProviderScope( tester, child: const Application(), ); @@ -33,8 +38,56 @@ void main() { ); }); + // testWidgets('App checks a stored session', (tester) async { + // int tokenTestRequests = 0; + // final mockClient = MockClient((request) { + // if (request.url.path == '/api/token/test') { + // tokenTestRequests++; + // return mockResponse( + // ''' +// { + // "${fakeSession.token}": { + // "scopes": "web:mobile", + // "userId": "${fakeSession.user.id}" + // } +// } + // ''', + // 200, + // ); + // } else if (request.url.path == '/api/account') { + // return mockResponse( + // mockApiAccountResponse(fakeSession.user.name), + // 200, + // ); + // } + // return mockResponse('', 404); + // }); + + // final app = await makeTestProviderScope( + // tester, + // child: const Application(), + // userSession: fakeSession, + // overrides: [ + // lichessClientProvider + // .overrideWith((ref) => LichessClient(mockClient, ref)), + // ], + // ); + + // await tester.pumpWidget(app); + + // expect(find.byType(MaterialApp), findsOneWidget); + + // // wait for the session check request to complete + // await tester.pump(const Duration(milliseconds: 100)); + + // expect(tokenTestRequests, 1); + + // // session is still active + // expect(find.text('Hello testUser'), findsOneWidget); + // }); + testWidgets('Bottom navigation', (tester) async { - final app = await makeProviderScope( + final app = await makeTestProviderScope( tester, child: const Application(), ); diff --git a/test/mock_server_responses.dart b/test/mock_server_responses.dart new file mode 100644 index 0000000000..5145f36d56 --- /dev/null +++ b/test/mock_server_responses.dart @@ -0,0 +1,40 @@ +/// Mock server response for /api/account endpoint. +String mockApiAccountResponse(String username) => ''' +{ + "id": "${username.toLowerCase()}", + "username": "$username", + "createdAt": 1290415680000, + "seenAt": 1290415680000, + "title": "GM", + "patron": true, + "perfs": { + "blitz": { + "games": 2340, + "rating": 1681, + "rd": 30, + "prog": 10 + }, + "rapid": { + "games": 2340, + "rating": 1677, + "rd": 30, + "prog": 10 + }, + "classical": { + "games": 2340, + "rating": 1618, + "rd": 30, + "prog": 10 + } + }, + "profile": { + "country": "France", + "location": "Lille", + "bio": "test bio", + "firstName": "John", + "lastName": "Doe", + "fideRating": 1800, + "links": "http://test.com" + } +} +'''; diff --git a/test/model/account/account_repository_test.dart b/test/model/account/account_repository_test.dart index 27aeb64c15..efb69915ae 100644 --- a/test/model/account/account_repository_test.dart +++ b/test/model/account/account_repository_test.dart @@ -2,7 +2,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; import 'package:lichess_mobile/src/model/account/account_preferences.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import '../../test_container.dart'; import '../../test_helpers.dart'; diff --git a/test/model/auth/auth_controller_test.dart b/test/model/auth/auth_controller_test.dart index cbeeeff794..e0df8d1895 100644 --- a/test/model/auth/auth_controller_test.dart +++ b/test/model/auth/auth_controller_test.dart @@ -6,11 +6,12 @@ import 'package:lichess_mobile/src/model/auth/auth_controller.dart'; import 'package:lichess_mobile/src/model/auth/auth_repository.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/auth/session_storage.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:mocktail/mocktail.dart'; +import '../../mock_server_responses.dart'; import '../../test_container.dart'; import '../../test_helpers.dart'; @@ -22,15 +23,6 @@ class Listener extends Mock { void call(T? previous, T value); } -final client = MockClient((request) { - if (request.url.path == '/api/account') { - return mockResponse(testAccountResponse, 200); - } else if (request.method == 'DELETE' && request.url.path == '/api/token') { - return mockResponse('ok', 200); - } - return mockResponse('', 404); -}); - void main() { final mockSessionStorage = MockSessionStorage(); final mockFlutterAppAuth = MockFlutterAppAuth(); @@ -47,6 +39,18 @@ void main() { const loading = AsyncLoading(); const nullData = AsyncData(null); + final client = MockClient((request) { + if (request.url.path == '/api/account') { + return mockResponse( + mockApiAccountResponse(testUserSession.user.name), + 200, + ); + } else if (request.method == 'DELETE' && request.url.path == '/api/token') { + return mockResponse('ok', 200); + } + return mockResponse('', 404); + }); + setUpAll(() { registerFallbackValue( AuthorizationTokenRequest( @@ -156,46 +160,6 @@ void main() { }); } -const testAccountResponse = ''' -{ - "id": "test", - "username": "test", - "createdAt": 1290415680000, - "seenAt": 1290415680000, - "title": "GM", - "patron": true, - "perfs": { - "blitz": { - "games": 2340, - "rating": 1681, - "rd": 30, - "prog": 10 - }, - "rapid": { - "games": 2340, - "rating": 1677, - "rd": 30, - "prog": 10 - }, - "classical": { - "games": 2340, - "rating": 1618, - "rd": 30, - "prog": 10 - } - }, - "profile": { - "country": "France", - "location": "Lille", - "bio": "test bio", - "firstName": "John", - "lastName": "Doe", - "fideRating": 1800, - "links": "http://test.com" - } -} -'''; - final signInResponse = AuthorizationTokenResponse( 'testToken', null, diff --git a/test/model/broadcast/broadcast_repository_test.dart b/test/model/broadcast/broadcast_repository_test.dart index fb3f95f26a..7d7b78f0fb 100644 --- a/test/model/broadcast/broadcast_repository_test.dart +++ b/test/model/broadcast/broadcast_repository_test.dart @@ -2,8 +2,8 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_repository.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import '../../test_container.dart'; import '../../test_helpers.dart'; diff --git a/test/model/challenge/challenge_repository_test.dart b/test/model/challenge/challenge_repository_test.dart index 412d9c2bb8..b40f62c9a6 100644 --- a/test/model/challenge/challenge_repository_test.dart +++ b/test/model/challenge/challenge_repository_test.dart @@ -3,8 +3,8 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; import 'package:lichess_mobile/src/model/challenge/challenge.dart'; import 'package:lichess_mobile/src/model/challenge/challenge_repository.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import '../../test_container.dart'; import '../../test_helpers.dart'; diff --git a/test/model/challenge/challenge_service_test.dart b/test/model/challenge/challenge_service_test.dart index 80e6e5768e..10872457a1 100644 --- a/test/model/challenge/challenge_service_test.dart +++ b/test/model/challenge/challenge_service_test.dart @@ -12,10 +12,10 @@ import 'package:lichess_mobile/src/model/notifications/notification_service.dart import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:mocktail/mocktail.dart'; +import '../../network/fake_websocket_channel.dart'; +import '../../network/socket_test.dart'; import '../../test_container.dart'; import '../auth/fake_session_storage.dart'; -import '../common/fake_websocket_channel.dart'; -import '../common/socket_test.dart'; class NotificationDisplayMock extends Mock implements FlutterLocalNotificationsPlugin {} diff --git a/test/model/game/game_repository_test.dart b/test/model/game/game_repository_test.dart index 658eb532f2..176bdb07df 100644 --- a/test/model/game/game_repository_test.dart +++ b/test/model/game/game_repository_test.dart @@ -2,10 +2,10 @@ import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/game/archived_game.dart'; import 'package:lichess_mobile/src/model/game/game_repository.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import '../../test_container.dart'; import '../../test_helpers.dart'; diff --git a/test/model/lobby/lobby_repository_test.dart b/test/model/lobby/lobby_repository_test.dart index 6c92f42ef6..e18f00f7c0 100644 --- a/test/model/lobby/lobby_repository_test.dart +++ b/test/model/lobby/lobby_repository_test.dart @@ -1,10 +1,10 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/lobby/correspondence_challenge.dart'; import 'package:lichess_mobile/src/model/lobby/lobby_repository.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import '../../test_container.dart'; import '../../test_helpers.dart'; diff --git a/test/model/notifications/notification_service_test.dart b/test/model/notifications/notification_service_test.dart index 5b6b8bee9a..472455172a 100644 --- a/test/model/notifications/notification_service_test.dart +++ b/test/model/notifications/notification_service_test.dart @@ -5,13 +5,14 @@ import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/correspondence/correspondence_service.dart'; import 'package:lichess_mobile/src/model/game/playable_game.dart'; import 'package:lichess_mobile/src/model/notifications/notification_service.dart'; import 'package:lichess_mobile/src/model/notifications/notifications.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:mocktail/mocktail.dart'; + import '../../binding.dart'; import '../../test_container.dart'; import '../../test_helpers.dart'; diff --git a/test/model/opening_explorer/opening_explorer_repository_test.dart b/test/model/opening_explorer/opening_explorer_repository_test.dart index 013fd5516c..ffdaa4b93e 100644 --- a/test/model/opening_explorer/opening_explorer_repository_test.dart +++ b/test/model/opening_explorer/opening_explorer_repository_test.dart @@ -2,10 +2,10 @@ import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/speed.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_repository.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import '../../test_container.dart'; import '../../test_helpers.dart'; diff --git a/test/model/puzzle/puzzle_repository_test.dart b/test/model/puzzle/puzzle_repository_test.dart index 5647f327a2..6098c0aa59 100644 --- a/test/model/puzzle/puzzle_repository_test.dart +++ b/test/model/puzzle/puzzle_repository_test.dart @@ -1,8 +1,8 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_repository.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import '../../test_container.dart'; import '../../test_helpers.dart'; diff --git a/test/model/puzzle/puzzle_service_test.dart b/test/model/puzzle/puzzle_service_test.dart index 34aa9a8ad7..df896a6ed2 100644 --- a/test/model/puzzle/puzzle_service_test.dart +++ b/test/model/puzzle/puzzle_service_test.dart @@ -5,7 +5,6 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle.dart'; @@ -13,6 +12,7 @@ import 'package:lichess_mobile/src/model/puzzle/puzzle_angle.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_batch_storage.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_service.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_theme.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import '../../test_container.dart'; import '../../test_helpers.dart'; @@ -318,7 +318,7 @@ void main() { expect(nbReq, equals(1)); final data = await storage.fetch(userId: const UserId('testUserId')); - expect(data?.solved, equals(IList(const []))); + expect(data?.solved, equals(IList(const []))); expect(data?.unsolved[0].puzzle.id, equals(const PuzzleId('20yWT'))); expect(next?.puzzle.puzzle.id, equals(const PuzzleId('20yWT'))); expect(next?.glicko?.rating, equals(1834.54)); diff --git a/test/model/relation/relation_repository_test.dart b/test/model/relation/relation_repository_test.dart index d190c9ff5c..afe1fb9580 100644 --- a/test/model/relation/relation_repository_test.dart +++ b/test/model/relation/relation_repository_test.dart @@ -1,9 +1,9 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/relation/relation_repository.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import '../../test_container.dart'; import '../../test_helpers.dart'; diff --git a/test/model/user/user_repository_test.dart b/test/model/user/user_repository_test.dart index 68bec60d90..87c1a6cf6a 100644 --- a/test/model/user/user_repository_test.dart +++ b/test/model/user/user_repository_test.dart @@ -1,12 +1,12 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; import 'package:lichess_mobile/src/model/user/leaderboard.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/model/user/user_repository.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import '../../test_container.dart'; import '../../test_helpers.dart'; diff --git a/test/model/common/fake_websocket_channel.dart b/test/network/fake_websocket_channel.dart similarity index 98% rename from test/model/common/fake_websocket_channel.dart rename to test/network/fake_websocket_channel.dart index fce314cdc9..659b0cdd73 100644 --- a/test/model/common/fake_websocket_channel.dart +++ b/test/network/fake_websocket_channel.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'package:async/src/stream_sink_transformer.dart'; -import 'package:lichess_mobile/src/model/common/socket.dart'; +import 'package:lichess_mobile/src/network/socket.dart'; import 'package:stream_channel/stream_channel.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; diff --git a/test/model/common/socket_test.dart b/test/network/socket_test.dart similarity index 99% rename from test/model/common/socket_test.dart rename to test/network/socket_test.dart index 3a5eb66e04..86e5d34d80 100644 --- a/test/model/common/socket_test.dart +++ b/test/network/socket_test.dart @@ -2,7 +2,7 @@ import 'dart:io'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:lichess_mobile/src/model/common/socket.dart'; +import 'package:lichess_mobile/src/network/socket.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'fake_websocket_channel.dart'; diff --git a/test/test_container.dart b/test/test_container.dart index 9e21f5ff16..daad1f1690 100644 --- a/test/test_container.dart +++ b/test/test_container.dart @@ -8,10 +8,10 @@ import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/crashlytics.dart'; import 'package:lichess_mobile/src/db/database.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; -import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/notifications/notification_service.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/network/socket.dart'; import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:logging/logging.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; @@ -19,8 +19,8 @@ import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import './fake_crashlytics.dart'; import './model/common/service/fake_sound_service.dart'; import 'binding.dart'; -import 'model/common/fake_websocket_channel.dart'; import 'model/notifications/fake_notification_display.dart'; +import 'network/fake_websocket_channel.dart'; import 'utils/fake_connectivity.dart'; /// A mock client that always returns a 200 empty response. diff --git a/test/test_provider_scope.dart b/test/test_provider_scope.dart index 4f2077558a..18064f509d 100644 --- a/test/test_provider_scope.dart +++ b/test/test_provider_scope.dart @@ -14,11 +14,11 @@ import 'package:lichess_mobile/src/db/database.dart'; import 'package:lichess_mobile/src/model/account/account_preferences.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/auth/session_storage.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; -import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/notifications/notification_service.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/network/socket.dart'; import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:logging/logging.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; @@ -27,8 +27,8 @@ import 'package:visibility_detector/visibility_detector.dart'; import './fake_crashlytics.dart'; import './model/common/service/fake_sound_service.dart'; import 'binding.dart'; -import 'model/common/fake_websocket_channel.dart'; import 'model/notifications/fake_notification_display.dart'; +import 'network/fake_websocket_channel.dart'; import 'test_helpers.dart'; import 'utils/fake_connectivity.dart'; @@ -45,14 +45,14 @@ final mockClient = MockClient((request) async { /// The [overrides] parameter can be used to override any provider in the app. /// The [userSession] parameter can be used to set the initial user session state. /// The [defaultPreferences] parameter can be used to set the initial shared preferences. -Future makeProviderScopeApp( +Future makeTestProviderScopeApp( WidgetTester tester, { required Widget home, List? overrides, AuthSessionState? userSession, Map? defaultPreferences, }) async { - return makeProviderScope( + return makeTestProviderScope( tester, child: MaterialApp( localizationsDelegates: AppLocalizations.localizationsDelegates, @@ -79,7 +79,7 @@ Future makeProviderScopeApp( /// The [overrides] parameter can be used to override any provider in the app. /// The [userSession] parameter can be used to set the initial user session state. /// The [defaultPreferences] parameter can be used to set the initial shared preferences. -Future makeProviderScope( +Future makeTestProviderScope( WidgetTester tester, { required Widget child, List? overrides, diff --git a/test/view/analysis/analysis_screen_test.dart b/test/view/analysis/analysis_screen_test.dart index 0c74d12460..4c68c8476a 100644 --- a/test/view/analysis/analysis_screen_test.dart +++ b/test/view/analysis/analysis_screen_test.dart @@ -29,7 +29,7 @@ void main() { group('Analysis Screen', () { testWidgets('displays correct move and position', (tester) async { - final app = await makeProviderScopeApp( + final app = await makeTestProviderScopeApp( tester, home: AnalysisScreen( pgnOrId: sanMoves, @@ -56,7 +56,7 @@ void main() { }); testWidgets('move backwards and forward', (tester) async { - final app = await makeProviderScopeApp( + final app = await makeTestProviderScopeApp( tester, home: AnalysisScreen( pgnOrId: sanMoves, diff --git a/test/view/board_editor/board_editor_screen_test.dart b/test/view/board_editor/board_editor_screen_test.dart index 37b7021851..1511d59c27 100644 --- a/test/view/board_editor/board_editor_screen_test.dart +++ b/test/view/board_editor/board_editor_screen_test.dart @@ -12,7 +12,7 @@ import '../../test_provider_scope.dart'; void main() { group('Board Editor', () { testWidgets('Displays initial FEN on start', (tester) async { - final app = await makeProviderScopeApp( + final app = await makeTestProviderScopeApp( tester, home: const BoardEditorScreen(), ); @@ -37,7 +37,7 @@ void main() { }); testWidgets('Flip board', (tester) async { - final app = await makeProviderScopeApp( + final app = await makeTestProviderScopeApp( tester, home: const BoardEditorScreen(), ); @@ -57,7 +57,7 @@ void main() { }); testWidgets('Side to play and castling rights', (tester) async { - final app = await makeProviderScopeApp( + final app = await makeTestProviderScopeApp( tester, home: const BoardEditorScreen(), ); @@ -120,7 +120,7 @@ void main() { }); testWidgets('Castling rights ignored when rook is missing', (tester) async { - final app = await makeProviderScopeApp( + final app = await makeTestProviderScopeApp( tester, home: const BoardEditorScreen(), ); @@ -145,7 +145,7 @@ void main() { testWidgets('Possible en passant squares are calculated correctly', (tester) async { - final app = await makeProviderScopeApp( + final app = await makeTestProviderScopeApp( tester, home: const BoardEditorScreen(), ); @@ -182,7 +182,7 @@ void main() { }); testWidgets('Can drag pieces to new squares', (tester) async { - final app = await makeProviderScopeApp( + final app = await makeTestProviderScopeApp( tester, home: const BoardEditorScreen(), ); @@ -211,7 +211,7 @@ void main() { }); testWidgets('illegal position cannot be analyzed', (tester) async { - final app = await makeProviderScopeApp( + final app = await makeTestProviderScopeApp( tester, home: const BoardEditorScreen(), ); @@ -231,7 +231,7 @@ void main() { }); testWidgets('Delete pieces via bin button', (tester) async { - final app = await makeProviderScopeApp( + final app = await makeTestProviderScopeApp( tester, home: const BoardEditorScreen(), ); @@ -273,7 +273,7 @@ void main() { }); testWidgets('Add pieces via tap and pan', (tester) async { - final app = await makeProviderScopeApp( + final app = await makeTestProviderScopeApp( tester, home: const BoardEditorScreen(), ); @@ -297,7 +297,7 @@ void main() { }); testWidgets('Drag pieces onto the board', (tester) async { - final app = await makeProviderScopeApp( + final app = await makeTestProviderScopeApp( tester, home: const BoardEditorScreen(), ); diff --git a/test/view/broadcast/broadcasts_list_screen_test.dart b/test/view/broadcast/broadcasts_list_screen_test.dart index e5907ee7ea..f5da2b5843 100644 --- a/test/view/broadcast/broadcasts_list_screen_test.dart +++ b/test/view/broadcast/broadcasts_list_screen_test.dart @@ -1,6 +1,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcasts_list_screen.dart'; import 'package:network_image_mock/network_image_mock.dart'; @@ -24,7 +24,7 @@ void main() { 'Displays broadcast tournament screen', variant: kPlatformVariant, (tester) async { - final app = await makeProviderScopeApp( + final app = await makeTestProviderScopeApp( tester, home: const BroadcastsListScreen(), overrides: [ @@ -48,7 +48,7 @@ void main() { 'Scroll broadcast tournament screen', variant: kPlatformVariant, (tester) async { - final app = await makeProviderScopeApp( + final app = await makeTestProviderScopeApp( tester, home: const BroadcastsListScreen(), overrides: [ diff --git a/test/view/coordinate_training/coordinate_training_screen_test.dart b/test/view/coordinate_training/coordinate_training_screen_test.dart index 87e4f1cc99..ab5f65e5b9 100644 --- a/test/view/coordinate_training/coordinate_training_screen_test.dart +++ b/test/view/coordinate_training/coordinate_training_screen_test.dart @@ -13,7 +13,7 @@ void main() { group('Coordinate Training', () { testWidgets('Initial state when started in FindSquare mode', (tester) async { - final app = await makeProviderScopeApp( + final app = await makeTestProviderScopeApp( tester, home: const CoordinateTrainingScreen(), ); @@ -52,7 +52,7 @@ void main() { }); testWidgets('Tap wrong square', (tester) async { - final app = await makeProviderScopeApp( + final app = await makeTestProviderScopeApp( tester, home: const CoordinateTrainingScreen(), ); @@ -101,7 +101,7 @@ void main() { }); testWidgets('Tap correct square', (tester) async { - final app = await makeProviderScopeApp( + final app = await makeTestProviderScopeApp( tester, home: const CoordinateTrainingScreen(), ); diff --git a/test/view/game/archived_game_screen_test.dart b/test/view/game/archived_game_screen_test.dart index 7ade011d10..cc09d0f7ed 100644 --- a/test/view/game/archived_game_screen_test.dart +++ b/test/view/game/archived_game_screen_test.dart @@ -4,7 +4,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; import 'package:lichess_mobile/src/model/common/speed.dart'; @@ -12,6 +11,7 @@ import 'package:lichess_mobile/src/model/game/archived_game.dart'; import 'package:lichess_mobile/src/model/game/game_status.dart'; import 'package:lichess_mobile/src/model/game/player.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/view/game/archived_game_screen.dart'; import 'package:lichess_mobile/src/view/game/game_player.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; @@ -32,7 +32,7 @@ void main() { testWidgets( 'loads game data if only game id is provided', (tester) async { - final app = await makeProviderScopeApp( + final app = await makeTestProviderScopeApp( tester, home: const ArchivedGameScreen( gameId: GameId('qVChCOTc'), @@ -65,7 +65,7 @@ void main() { testWidgets( 'displays game data and last fen immediately, then moves', (tester) async { - final app = await makeProviderScopeApp( + final app = await makeTestProviderScopeApp( tester, home: ArchivedGameScreen( gameData: gameData, @@ -138,7 +138,7 @@ void main() { ); testWidgets('navigate game positions', (tester) async { - final app = await makeProviderScopeApp( + final app = await makeTestProviderScopeApp( tester, home: ArchivedGameScreen( gameData: gameData, diff --git a/test/view/opening_explorer/opening_explorer_screen_test.dart b/test/view/opening_explorer/opening_explorer_screen_test.dart index 3e4eea8715..6d0a16971e 100644 --- a/test/view/opening_explorer/opening_explorer_screen_test.dart +++ b/test/view/opening_explorer/opening_explorer_screen_test.dart @@ -7,13 +7,13 @@ import 'package:http/testing.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_preferences.dart'; import 'package:lichess_mobile/src/model/settings/preferences.dart'; import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_screen.dart'; import '../../test_helpers.dart'; @@ -63,7 +63,7 @@ void main() { testWidgets( 'master opening explorer loads', (WidgetTester tester) async { - final app = await makeProviderScopeApp( + final app = await makeTestProviderScopeApp( tester, home: const OpeningExplorerScreen( pgn: '', @@ -112,7 +112,7 @@ void main() { testWidgets( 'lichess opening explorer loads', (WidgetTester tester) async { - final app = await makeProviderScopeApp( + final app = await makeTestProviderScopeApp( tester, home: const OpeningExplorerScreen( pgn: '', @@ -167,7 +167,7 @@ void main() { testWidgets( 'player opening explorer loads', (WidgetTester tester) async { - final app = await makeProviderScopeApp( + final app = await makeTestProviderScopeApp( tester, home: const OpeningExplorerScreen( pgn: '', diff --git a/test/view/over_the_board/over_the_board_screen_test.dart b/test/view/over_the_board/over_the_board_screen_test.dart index defb4f45b9..febe1c1614 100644 --- a/test/view/over_the_board/over_the_board_screen_test.dart +++ b/test/view/over_the_board/over_the_board_screen_test.dart @@ -203,7 +203,7 @@ Future initOverTheBoardGame( WidgetTester tester, TimeIncrement timeIncrement, ) async { - final app = await makeProviderScopeApp( + final app = await makeTestProviderScopeApp( tester, home: const OverTheBoardScreen(), ); diff --git a/test/view/puzzle/puzzle_screen_test.dart b/test/view/puzzle/puzzle_screen_test.dart index f0e169af9f..7cc0dca6a1 100644 --- a/test/view/puzzle/puzzle_screen_test.dart +++ b/test/view/puzzle/puzzle_screen_test.dart @@ -6,7 +6,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; import 'package:lichess_mobile/src/model/account/account_preferences.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle.dart'; @@ -14,6 +13,7 @@ import 'package:lichess_mobile/src/model/puzzle/puzzle_angle.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_batch_storage.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_storage.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_theme.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/utils/string.dart'; import 'package:lichess_mobile/src/view/puzzle/puzzle_screen.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; @@ -47,7 +47,7 @@ void main() { (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); - final app = await makeProviderScopeApp( + final app = await makeTestProviderScopeApp( tester, home: PuzzleScreen( angle: const PuzzleTheme(PuzzleThemeKey.mix), @@ -78,7 +78,7 @@ void main() { 'Loads puzzle directly by passing a puzzleId', variant: kPlatformVariant, (tester) async { - final app = await makeProviderScopeApp( + final app = await makeTestProviderScopeApp( tester, home: PuzzleScreen( angle: const PuzzleTheme(PuzzleThemeKey.mix), @@ -104,7 +104,7 @@ void main() { ); testWidgets('Loads next puzzle when no puzzleId is passed', (tester) async { - final app = await makeProviderScopeApp( + final app = await makeTestProviderScopeApp( tester, home: const PuzzleScreen( angle: PuzzleTheme(PuzzleThemeKey.mix), @@ -151,7 +151,7 @@ void main() { when(() => mockHistoryStorage.fetch(puzzleId: puzzle2.puzzle.id)) .thenAnswer((_) async => puzzle2); - final app = await makeProviderScopeApp( + final app = await makeTestProviderScopeApp( tester, home: PuzzleScreen( angle: const PuzzleTheme(PuzzleThemeKey.mix), @@ -264,7 +264,7 @@ void main() { when(() => mockHistoryStorage.fetch(puzzleId: puzzle2.puzzle.id)) .thenAnswer((_) async => puzzle2); - final app = await makeProviderScopeApp( + final app = await makeTestProviderScopeApp( tester, home: PuzzleScreen( angle: const PuzzleTheme(PuzzleThemeKey.mix), @@ -377,7 +377,7 @@ void main() { return mockResponse('', 404); }); - final app = await makeProviderScopeApp( + final app = await makeTestProviderScopeApp( tester, home: PuzzleScreen( angle: const PuzzleTheme(PuzzleThemeKey.mix), diff --git a/test/view/puzzle/storm_screen_test.dart b/test/view/puzzle/storm_screen_test.dart index e7e10ba217..ac581af0d4 100644 --- a/test/view/puzzle/storm_screen_test.dart +++ b/test/view/puzzle/storm_screen_test.dart @@ -4,11 +4,11 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_providers.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_repository.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/view/puzzle/storm_screen.dart'; import '../../test_helpers.dart'; @@ -28,7 +28,7 @@ void main() { (tester) async { final SemanticsHandle handle = tester.ensureSemantics(); - final app = await makeProviderScopeApp( + final app = await makeTestProviderScopeApp( tester, home: const StormScreen(), overrides: [ @@ -49,7 +49,7 @@ void main() { testWidgets( 'Load puzzle and play white pieces', (tester) async { - final app = await makeProviderScopeApp( + final app = await makeTestProviderScopeApp( tester, home: const StormScreen(), overrides: [ @@ -73,7 +73,7 @@ void main() { testWidgets( 'Play one puzzle', (tester) async { - final app = await makeProviderScopeApp( + final app = await makeTestProviderScopeApp( tester, home: const StormScreen(), overrides: [ @@ -128,7 +128,7 @@ void main() { ); testWidgets('shows end run result', (tester) async { - final app = await makeProviderScopeApp( + final app = await makeTestProviderScopeApp( tester, home: const StormScreen(), overrides: [ @@ -173,7 +173,7 @@ void main() { }); testWidgets('play wrong move', (tester) async { - final app = await makeProviderScopeApp( + final app = await makeTestProviderScopeApp( tester, home: const StormScreen(), overrides: [ diff --git a/test/view/settings/settings_tab_screen_test.dart b/test/view/settings/settings_tab_screen_test.dart index e83b855a62..b907dc94a0 100644 --- a/test/view/settings/settings_tab_screen_test.dart +++ b/test/view/settings/settings_tab_screen_test.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; import 'package:lichess_mobile/src/db/database.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/view/settings/settings_tab_screen.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; @@ -26,7 +26,7 @@ void main() { (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); - final app = await makeProviderScopeApp( + final app = await makeTestProviderScopeApp( tester, home: const SettingsTabScreen(), ); @@ -46,7 +46,7 @@ void main() { testWidgets( "don't show signOut if no session", (WidgetTester tester) async { - final app = await makeProviderScopeApp( + final app = await makeTestProviderScopeApp( tester, home: const SettingsTabScreen(), ); @@ -61,7 +61,7 @@ void main() { testWidgets( 'signout', (WidgetTester tester) async { - final app = await makeProviderScopeApp( + final app = await makeTestProviderScopeApp( tester, home: const SettingsTabScreen(), userSession: fakeSession, diff --git a/test/view/user/leaderboard_screen_test.dart b/test/view/user/leaderboard_screen_test.dart index 1950ae0205..40575d6b6d 100644 --- a/test/view/user/leaderboard_screen_test.dart +++ b/test/view/user/leaderboard_screen_test.dart @@ -1,7 +1,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/view/user/leaderboard_screen.dart'; import '../../test_helpers.dart'; @@ -21,7 +21,7 @@ void main() { (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); - final app = await makeProviderScopeApp( + final app = await makeTestProviderScopeApp( tester, overrides: [ lichessClientProvider diff --git a/test/view/user/leaderboard_widget_test.dart b/test/view/user/leaderboard_widget_test.dart index fe63f0a9c3..a34e0ebd5d 100644 --- a/test/view/user/leaderboard_widget_test.dart +++ b/test/view/user/leaderboard_widget_test.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/view/user/leaderboard_screen.dart'; import 'package:lichess_mobile/src/view/user/leaderboard_widget.dart'; @@ -21,7 +21,7 @@ void main() { 'accessibility and basic info showing test', (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); - final app = await makeProviderScopeApp( + final app = await makeTestProviderScopeApp( tester, home: Column(children: [LeaderboardWidget()]), overrides: [ diff --git a/test/view/user/perf_stats_screen_test.dart b/test/view/user/perf_stats_screen_test.dart index 82144e8185..a2a208f8e4 100644 --- a/test/view/user/perf_stats_screen_test.dart +++ b/test/view/user/perf_stats_screen_test.dart @@ -1,8 +1,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/view/user/perf_stats_screen.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; @@ -27,7 +27,7 @@ void main() { (WidgetTester tester) async { final SemanticsHandle handle = tester.ensureSemantics(); - final app = await makeProviderScopeApp( + final app = await makeTestProviderScopeApp( tester, home: PerfStatsScreen( user: fakeUser, @@ -59,7 +59,7 @@ void main() { testWidgets( 'screen loads, required stats are shown', (WidgetTester tester) async { - final app = await makeProviderScopeApp( + final app = await makeTestProviderScopeApp( tester, home: PerfStatsScreen( user: fakeUser, diff --git a/test/view/user/search_screen_test.dart b/test/view/user/search_screen_test.dart index d1129c9c99..93579a2efa 100644 --- a/test/view/user/search_screen_test.dart +++ b/test/view/user/search_screen_test.dart @@ -3,7 +3,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/view/user/search_screen.dart'; import 'package:lichess_mobile/src/widgets/user_list_tile.dart'; @@ -25,7 +25,7 @@ void main() { testWidgets( 'should see search results', (WidgetTester tester) async { - final app = await makeProviderScopeApp( + final app = await makeTestProviderScopeApp( tester, home: const SearchScreen(), overrides: [ @@ -66,7 +66,7 @@ void main() { testWidgets( 'should see "no result" when search finds nothing', (WidgetTester tester) async { - final app = await makeProviderScopeApp( + final app = await makeTestProviderScopeApp( tester, home: const SearchScreen(), overrides: [ diff --git a/test/view/user/user_screen_test.dart b/test/view/user/user_screen_test.dart index be6e664350..91648e6db8 100644 --- a/test/view/user/user_screen_test.dart +++ b/test/view/user/user_screen_test.dart @@ -1,8 +1,8 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; -import 'package:lichess_mobile/src/model/common/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/view/user/user_screen.dart'; import '../../model/user/user_repository_test.dart'; @@ -38,7 +38,7 @@ void main() { testWidgets( 'should see activity and recent games', (WidgetTester tester) async { - final app = await makeProviderScopeApp( + final app = await makeTestProviderScopeApp( tester, home: const UserScreen(user: testUser), overrides: [ From c03e49ead897fb2798f8dae8bd1d2e44799ea073 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Thu, 3 Oct 2024 23:45:30 +0200 Subject: [PATCH 423/979] use flutter way of getting brightness --- lib/src/view/engine/engine_lines.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/src/view/engine/engine_lines.dart b/lib/src/view/engine/engine_lines.dart index cb6e23c25f..09c493a55a 100644 --- a/lib/src/view/engine/engine_lines.dart +++ b/lib/src/view/engine/engine_lines.dart @@ -8,7 +8,6 @@ import 'package:lichess_mobile/src/model/account/account_preferences.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; import 'package:lichess_mobile/src/model/common/eval.dart'; import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; -import 'package:lichess_mobile/src/model/settings/brightness.dart'; import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; @@ -115,7 +114,7 @@ class Engineline extends ConsumerWidget { ply += 1; }); - final brightness = ref.watch(currentBrightnessProvider); + final brightness = Theme.of(context).brightness; final evalString = pvData.evalString; return AdaptiveInkWell( From e4b007ad03b7be46c2d3ad6491268144b5000646 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Thu, 3 Oct 2024 23:47:52 +0200 Subject: [PATCH 424/979] remove isLandscape and padding --- lib/src/view/analysis/analysis_screen.dart | 18 +++++++++++------- lib/src/view/engine/engine_lines.dart | 17 ++++------------- 2 files changed, 15 insertions(+), 20 deletions(-) diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index 771d560f3d..8a896bd1ef 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -295,12 +295,17 @@ class _Body extends ConsumerWidget { mainAxisAlignment: MainAxisAlignment.start, children: [ if (isEngineAvailable) - EngineLines( - onTapMove: - ref.read(ctrlProvider.notifier).onUserMove, - clientEval: currentNode.eval, - isGameOver: currentNode.position.isGameOver, - isLandscape: true, + Padding( + padding: const EdgeInsets.all( + kTabletBoardTableSidePadding, + ), + child: EngineLines( + onTapMove: ref + .read(ctrlProvider.notifier) + .onUserMove, + clientEval: currentNode.eval, + isGameOver: currentNode.position.isGameOver, + ), ), Expanded( child: PlatformCard( @@ -420,7 +425,6 @@ class _ColumnTopTable extends ConsumerWidget { clientEval: analysisState.currentNode.eval, isGameOver: analysisState.currentNode.position.isGameOver, onTapMove: ref.read(ctrlProvider.notifier).onUserMove, - isLandscape: false, ), ], ) diff --git a/lib/src/view/engine/engine_lines.dart b/lib/src/view/engine/engine_lines.dart index 09c493a55a..f20f21cf6b 100644 --- a/lib/src/view/engine/engine_lines.dart +++ b/lib/src/view/engine/engine_lines.dart @@ -3,7 +3,6 @@ import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/account/account_preferences.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; import 'package:lichess_mobile/src/model/common/eval.dart'; @@ -16,12 +15,10 @@ class EngineLines extends ConsumerWidget { required this.onTapMove, required this.clientEval, required this.isGameOver, - required this.isLandscape, }); final void Function(NormalMove move) onTapMove; final ClientEval? clientEval; final bool isGameOver; - final bool isLandscape; @override Widget build(BuildContext context, WidgetRef ref) { @@ -57,16 +54,10 @@ class EngineLines extends ConsumerWidget { content.addAll(padding); } - return Padding( - padding: EdgeInsets.symmetric( - vertical: isLandscape ? kTabletBoardTableSidePadding : 0.0, - horizontal: isLandscape ? kTabletBoardTableSidePadding : 0.0, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: content, - ), + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: content, ); } } From ec223e77db105d29df3beb324bf795e3fe3c31ab Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 4 Oct 2024 10:53:13 +0200 Subject: [PATCH 425/979] Add back http tests --- lib/src/network/http.dart | 67 ++--- test/binding.dart | 4 +- test/network/fake_http_client_factory.dart | 13 + test/network/http_test.dart | 298 +++++++++++++++++++++ test/test_container.dart | 14 +- test/test_provider_scope.dart | 7 +- 6 files changed, 360 insertions(+), 43 deletions(-) create mode 100644 test/network/fake_http_client_factory.dart create mode 100644 test/network/http_test.dart diff --git a/lib/src/network/http.dart b/lib/src/network/http.dart index 94d9a34886..172b1c0977 100644 --- a/lib/src/network/http.dart +++ b/lib/src/network/http.dart @@ -45,27 +45,34 @@ Uri lichessUri(String unencodedPath, [Map? queryParameters]) => /// Creates the appropriate http client for the platform. /// /// Do not use directly, use [defaultClient] or [lichessClient] instead. -Client httpClientFactory() { - const userAgent = 'Lichess Mobile'; - if (Platform.isAndroid) { - final engine = CronetEngine.build( - cacheMode: CacheMode.memory, - cacheMaxSize: _maxCacheSize, - userAgent: userAgent, - ); - return CronetClient.fromCronetEngine(engine); - } +class HttpClientFactory { + Client call() { + final packageInfo = LichessBinding.instance.packageInfo; + final userAgent = 'Lichess Mobile/${packageInfo.version}'; + if (Platform.isAndroid) { + final engine = CronetEngine.build( + cacheMode: CacheMode.memory, + cacheMaxSize: _maxCacheSize, + userAgent: userAgent, + ); + return CronetClient.fromCronetEngine(engine); + } - if (Platform.isIOS || Platform.isMacOS) { - final config = URLSessionConfiguration.ephemeralSessionConfiguration() - ..cache = URLCache.withCapacity(memoryCapacity: _maxCacheSize) - ..httpAdditionalHeaders = {'User-Agent': userAgent}; - return CupertinoClient.fromSessionConfiguration(config); - } + if (Platform.isIOS || Platform.isMacOS) { + final config = URLSessionConfiguration.ephemeralSessionConfiguration() + ..cache = URLCache.withCapacity(memoryCapacity: _maxCacheSize) + ..httpAdditionalHeaders = {'User-Agent': userAgent}; + return CupertinoClient.fromSessionConfiguration(config); + } - return IOClient(HttpClient()..userAgent = userAgent); + return IOClient(HttpClient()..userAgent = userAgent); + } } +@Riverpod(keepAlive: true) +HttpClientFactory httpClientFactory(HttpClientFactoryRef _) => + HttpClientFactory(); + /// The default http client. /// /// This client is used for all requests that don't go to the lichess server, for @@ -73,7 +80,7 @@ Client httpClientFactory() { /// Only one instance of this client is created and kept alive for the whole app. @Riverpod(keepAlive: true) Client defaultClient(DefaultClientRef ref) { - final client = LoggingClient(httpClientFactory()); + final client = LoggingClient(ref.read(httpClientFactoryProvider)()); ref.onDispose(() => client.close()); return client; } @@ -86,7 +93,7 @@ LichessClient lichessClient(LichessClientRef ref) { final client = LichessClient( // Retry just once, after 500ms, on 429 Too Many Requests. RetryClient( - httpClientFactory(), + ref.read(httpClientFactoryProvider)(), retries: 1, delay: _defaultDelay, when: (response) => response.statusCode == 429, @@ -377,18 +384,18 @@ extension ClientExtension on Client { _checkResponseSuccess(url, response); final json = jsonUtf8Decoder.convert(response.bodyBytes); if (json is! Map) { - _logger.severe('Could not read json object as $T: expected an object.'); + _logger.severe('Could not read JSON object as $T: expected an object.'); throw ClientException( - 'Could not read json object as $T: expected an object.', + 'Could not read JSON object as $T: expected an object.', url, ); } try { return mapper(json); } catch (e, st) { - _logger.severe('Could not read json object as $T: $e', e, st); + _logger.severe('Could not read JSON object as $T: $e', e, st); throw ClientException( - 'Could not read json object as $T: $e\n$st', + 'Could not read JSON object as $T: $e\n$st', url, ); } @@ -409,9 +416,9 @@ extension ClientExtension on Client { _checkResponseSuccess(url, response); final json = jsonUtf8Decoder.convert(response.bodyBytes); if (json is! List) { - _logger.severe('Could not read json object as List: expected a list.'); + _logger.severe('Could not read JSON object as List: expected a list.'); throw ClientException( - 'Could not read json object as List: expected a list.', + 'Could not read JSON object as List: expected a list.', url, ); } @@ -419,9 +426,9 @@ extension ClientExtension on Client { final List list = []; for (final e in json) { if (e is! Map) { - _logger.severe('Could not read json object as $T: expected an object.'); + _logger.severe('Could not read JSON object as $T: expected an object.'); throw ClientException( - 'Could not read json object as $T: expected an object.', + 'Could not read JSON object as $T: expected an object.', url, ); } @@ -431,8 +438,8 @@ extension ClientExtension on Client { list.add(mapped); } } catch (e, st) { - _logger.severe('Could not read json object as $T: $e', e, st); - throw ClientException('Could not read json object as $T: $e', url); + _logger.severe('Could not read JSON object as $T: $e', e, st); + throw ClientException('Could not read JSON object as $T: $e', url); } } return IList(list); @@ -469,7 +476,7 @@ extension ClientExtension on Client { final request = Request('GET', url); if (headers != null) request.headers.addAll(headers); final response = await send(request); - if (response.statusCode > 400) { + if (response.statusCode >= 400) { var message = 'Request to $url failed with status ${response.statusCode}'; if (response.reasonPhrase != null) { message = '$message: ${response.reasonPhrase}'; diff --git a/test/binding.dart b/test/binding.dart index fb8b67e024..606beb5822 100644 --- a/test/binding.dart +++ b/test/binding.dart @@ -79,8 +79,8 @@ class TestLichessBinding extends LichessBinding { @override PackageInfo get packageInfo => PackageInfo( appName: 'lichess_mobile_test', - version: 'test', - buildNumber: '0.0.0', + version: '0.0.0', + buildNumber: '0', packageName: 'lichess_mobile_test', ); diff --git a/test/network/fake_http_client_factory.dart b/test/network/fake_http_client_factory.dart new file mode 100644 index 0000000000..3b07ad48bc --- /dev/null +++ b/test/network/fake_http_client_factory.dart @@ -0,0 +1,13 @@ +import 'package:http/http.dart' as http; +import 'package:lichess_mobile/src/network/http.dart'; + +class FakeHttpClientFactory implements HttpClientFactory { + const FakeHttpClientFactory(this._factory); + + final http.Client Function() _factory; + + @override + http.Client call() { + return _factory(); + } +} diff --git a/test/network/http_test.dart b/test/network/http_test.dart new file mode 100644 index 0000000000..1ab6f1d5e6 --- /dev/null +++ b/test/network/http_test.dart @@ -0,0 +1,298 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/http.dart' as http; +import 'package:lichess_mobile/src/model/auth/auth_session.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/user/user.dart'; +import 'package:lichess_mobile/src/network/http.dart'; + +import '../test_container.dart'; +import 'fake_http_client_factory.dart'; + +void main() { + setUp(() { + FakeClient.reset(); + }); + + group('LichessClient', () { + test('sends requests to lichess host', () async { + final container = await makeContainer( + overrides: [ + httpClientFactoryProvider.overrideWith((ref) { + return FakeHttpClientFactory(() => FakeClient()); + }), + ], + ); + final client = container.read(lichessClientProvider); + final response = await client.get(Uri(path: '/test')); + expect(response.statusCode, 200); + final requests = FakeClient.verifyRequests(); + expect( + requests.first, + isA() + .having((r) => r.url.path, 'path', '/test') + .having((r) => r.url.host, 'host', 'lichess.dev') + .having((r) => r.url.scheme, 'scheme', 'https'), + ); + }); + + test('sets user agent (no session)', () async { + final container = await makeContainer( + overrides: [ + httpClientFactoryProvider.overrideWith((ref) { + return FakeHttpClientFactory(() => FakeClient()); + }), + ], + ); + final client = container.read(lichessClientProvider); + await client.get(Uri(path: '/test')); + final requests = FakeClient.verifyRequests(); + expect( + requests.first, + isA().having( + (r) => r.headers['User-Agent'], + 'User-Agent', + 'Lichess Mobile/0.0.0 as:anon sri:test-sri', + ), + ); + }); + + test('sets user agent (with session)', () async { + final container = await makeContainer( + overrides: [ + httpClientFactoryProvider.overrideWith((ref) { + return FakeHttpClientFactory(() => FakeClient()); + }), + ], + userSession: const AuthSessionState( + token: 'test-token', + user: LightUser(id: UserId('test-user-id'), name: 'test-username'), + ), + ); + final client = container.read(lichessClientProvider); + await client.get(Uri(path: '/test')); + final requests = FakeClient.verifyRequests(); + expect( + requests.first, + isA().having( + (r) => r.headers['User-Agent'], + 'User-Agent', + 'Lichess Mobile/0.0.0 as:test-user-id sri:test-sri', + ), + ); + }); + + test('read methods throw ServerException on status code >= 400', () async { + final container = await makeContainer( + overrides: [ + httpClientFactoryProvider.overrideWith((ref) { + return FakeHttpClientFactory(() => FakeClient()); + }), + ], + ); + final client = container.read(lichessClientProvider); + for (final method in [ + client.read, + client.readBytes, + (Uri url) => client.readJson(url, mapper: (json) => json), + (Uri url) => client.readJsonList(url, mapper: (json) => json), + (Uri url) => client.readNdJsonList(url, mapper: (json) => json), + (Uri url) => client.readNdJsonStream(url, mapper: (json) => json), + ]) { + expect( + () => method(Uri(path: '/will/return/500')), + throwsA( + isA() + .having((e) => e.statusCode, 'statusCode', 500), + ), + ); + expect( + () => method(Uri(path: '/will/return/503')), + throwsA( + isA() + .having((e) => e.statusCode, 'statusCode', 503), + ), + ); + expect( + () => method(Uri(path: '/will/return/400')), + throwsA( + isA() + .having((e) => e.statusCode, 'statusCode', 400), + ), + ); + expect( + () => method(Uri(path: '/will/return/404')), + throwsA( + isA() + .having((e) => e.statusCode, 'statusCode', 404), + ), + ); + expect( + () => method(Uri(path: '/will/return/401')), + throwsA( + isA() + .having((e) => e.statusCode, 'statusCode', 401), + ), + ); + expect( + () => method(Uri(path: '/will/return/403')), + throwsA( + isA() + .having((e) => e.statusCode, 'statusCode', 403), + ), + ); + } + }); + + test('other methods do not throw on status code >= 400', () async { + final container = await makeContainer( + overrides: [ + httpClientFactoryProvider.overrideWith((ref) { + return FakeHttpClientFactory(() => FakeClient()); + }), + ], + ); + final client = container.read(lichessClientProvider); + for (final method in [ + client.get, + client.post, + client.put, + client.patch, + client.delete, + ]) { + expect( + () => method(Uri(path: '/will/return/500')), + returnsNormally, + ); + expect( + () => method(Uri(path: '/will/return/503')), + returnsNormally, + ); + expect( + () => method(Uri(path: '/will/return/400')), + returnsNormally, + ); + expect( + () => method(Uri(path: '/will/return/404')), + returnsNormally, + ); + expect( + () => method(Uri(path: '/will/return/401')), + returnsNormally, + ); + expect( + () => method(Uri(path: '/will/return/403')), + returnsNormally, + ); + } + }); + + test('socket and tls errors do not throw ClientException', () async { + final container = await makeContainer( + overrides: [ + httpClientFactoryProvider.overrideWith((ref) { + return FakeHttpClientFactory(() => FakeClient()); + }), + ], + ); + final client = container.read(lichessClientProvider); + expect( + () => client.get(Uri(path: '/will/throw/socket/exception')), + throwsA( + isA().having( + (e) => e.message, + 'message', + 'no internet', + ), + ), + ); + expect( + () => client.get(Uri(path: '/will/throw/tls/exception')), + throwsA( + isA().having( + (e) => e.message, + 'message', + 'tls error', + ), + ), + ); + }); + + test('failed JSON parsing will throw ClientException', () async { + final container = await makeContainer( + overrides: [ + httpClientFactoryProvider.overrideWith((ref) { + return FakeHttpClientFactory(() => FakeClient()); + }), + ], + ); + final client = container.read(lichessClientProvider); + expect( + () => client.readJson( + Uri(path: '/will/return/204'), + mapper: (json) { + return json; + }, + ), + throwsA( + isA().having( + (e) => e.message, + 'message', + 'Could not read JSON object as Map: expected an object.', + ), + ), + ); + }); + }); +} + +class FakeClient extends http.BaseClient { + static List _requests = []; + + static List verifyRequests() { + final result = _requests; + _requests = []; + return result; + } + + static void reset() { + _requests = []; + } + + @override + Future send(http.BaseRequest request) { + _requests.add(request); + return Future.value(_responseBasedOnPath(request.url.path)); + } + + http.StreamedResponse _responseBasedOnPath(String path) { + switch (path) { + case '/will/throw/socket/exception': + throw const SocketException('no internet'); + case '/will/throw/tls/exception': + throw const TlsException('tls error'); + case '/will/return/500': + return http.StreamedResponse(_streamBody('500'), 500); + case '/will/return/503': + return http.StreamedResponse(_streamBody('503'), 503); + case '/will/return/400': + return http.StreamedResponse(_streamBody('400'), 400); + case '/will/return/404': + return http.StreamedResponse(_streamBody('404'), 404); + case '/will/return/401': + return http.StreamedResponse(_streamBody('401'), 401); + case '/will/return/403': + return http.StreamedResponse(_streamBody('403'), 403); + case '/will/return/204': + return http.StreamedResponse(_streamBody('204'), 204); + case '/will/return/301': + return http.StreamedResponse(_streamBody('301'), 301); + default: + return http.StreamedResponse(_streamBody('200'), 200); + } + } +} + +Stream> _streamBody(String body) => Stream.value(utf8.encode(body)); diff --git a/test/test_container.dart b/test/test_container.dart index daad1f1690..0a1a42cdc1 100644 --- a/test/test_container.dart +++ b/test/test_container.dart @@ -20,6 +20,7 @@ import './fake_crashlytics.dart'; import './model/common/service/fake_sound_service.dart'; import 'binding.dart'; import 'model/notifications/fake_notification_display.dart'; +import 'network/fake_http_client_factory.dart'; import 'network/fake_websocket_channel.dart'; import 'utils/fake_connectivity.dart'; @@ -30,13 +31,13 @@ final testContainerMockClient = MockClient((request) async { const shouldLog = false; -/// Returns a [ProviderContainer] with a mocked [LichessClient] configured with -/// the given [mockClient]. +/// Returns a [ProviderContainer] with the [httpClientFactoryProvider] configured +/// with the given [mockClient]. Future lichessClientContainer(MockClient mockClient) async { return makeContainer( overrides: [ - lichessClientProvider.overrideWith((ref) { - return LichessClient(mockClient, ref); + httpClientFactoryProvider.overrideWith((ref) { + return FakeHttpClientFactory(() => mockClient); }), ], ); @@ -86,10 +87,9 @@ Future makeContainer({ ref.onDispose(pool.dispose); return pool; }), - lichessClientProvider.overrideWith((ref) { - return LichessClient(testContainerMockClient, ref); + httpClientFactoryProvider.overrideWith((ref) { + return FakeHttpClientFactory(() => testContainerMockClient); }), - defaultClientProvider.overrideWithValue(testContainerMockClient), crashlyticsProvider.overrideWithValue(FakeCrashlytics()), soundServiceProvider.overrideWithValue(FakeSoundService()), ...overrides ?? [], diff --git a/test/test_provider_scope.dart b/test/test_provider_scope.dart index 18064f509d..253b503db1 100644 --- a/test/test_provider_scope.dart +++ b/test/test_provider_scope.dart @@ -28,6 +28,7 @@ import './fake_crashlytics.dart'; import './model/common/service/fake_sound_service.dart'; import 'binding.dart'; import 'model/notifications/fake_notification_display.dart'; +import 'network/fake_http_client_factory.dart'; import 'network/fake_websocket_channel.dart'; import 'test_helpers.dart'; import 'utils/fake_connectivity.dart'; @@ -144,12 +145,10 @@ Future makeTestProviderScope( return testDb; }), // ignore: scoped_providers_should_specify_dependencies - lichessClientProvider.overrideWith((ref) { - return LichessClient(mockClient, ref); + httpClientFactoryProvider.overrideWith((ref) { + return FakeHttpClientFactory(() => mockClient); }), // ignore: scoped_providers_should_specify_dependencies - defaultClientProvider.overrideWith((_) => mockClient), - // ignore: scoped_providers_should_specify_dependencies webSocketChannelFactoryProvider.overrideWith((ref) { return FakeWebSocketChannelFactory(() => FakeWebSocketChannel()); }), From 10440b0df4995d3174e3b3eb02dea5a8ba6e31cb Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 4 Oct 2024 11:39:55 +0200 Subject: [PATCH 426/979] Add an http test for the session check --- lib/src/network/http.dart | 11 ++-- test/network/http_test.dart | 104 ++++++++++++++++++++++++++++++++++-- 2 files changed, 108 insertions(+), 7 deletions(-) diff --git a/lib/src/network/http.dart b/lib/src/network/http.dart index 172b1c0977..04f1d3ac09 100644 --- a/lib/src/network/http.dart +++ b/lib/src/network/http.dart @@ -204,10 +204,13 @@ class LichessClient implements Client { /// Checks if the session token is still valid, and delete session if it's not. Future _checkSessionToken(AuthSessionState session) async { - final data = await postReadJson( - Uri(path: '/api/token/test'), - mapper: (json) => json, - ).timeout(const Duration(seconds: 5)); + final defaultClient = _ref.read(defaultClientProvider); + final data = await defaultClient + .postReadJson( + Uri(path: '/api/token/test'), + mapper: (json) => json, + ) + .timeout(const Duration(seconds: 5)); if (data[session.token] == null) { _logger.fine('Session is not active. Deleting it.'); await _ref.read(authSessionProvider.notifier).delete(); diff --git a/test/network/http_test.dart b/test/network/http_test.dart index 1ab6f1d5e6..af181da57f 100644 --- a/test/network/http_test.dart +++ b/test/network/http_test.dart @@ -1,9 +1,11 @@ import 'dart:convert'; import 'dart:io'; +import 'package:fake_async/fake_async.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; +import 'package:lichess_mobile/src/model/auth/bearer.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/network/http.dart'; @@ -245,6 +247,94 @@ void main() { ), ); }); + + test('adds a signed bearer token when a session is available the request', + () async { + final container = await makeContainer( + overrides: [ + httpClientFactoryProvider.overrideWith((ref) { + return FakeHttpClientFactory(() => FakeClient()); + }), + ], + userSession: const AuthSessionState( + token: 'test-token', + user: LightUser(id: UserId('test-user-id'), name: 'test-username'), + ), + ); + + final session = container.read(authSessionProvider); + expect(session, isNotNull); + + final client = container.read(lichessClientProvider); + await client.get(Uri(path: '/test')); + + final requests = FakeClient.verifyRequests(); + expect(requests.length, 1); + expect( + requests.first, + isA().having( + (r) => r.headers['Authorization'], + 'Authorization', + 'Bearer ${signBearerToken('test-token')}', + ), + ); + }); + + test( + 'when receiving a 401, will test session token and delete session if not valid anymore', + () async { + final container = await makeContainer( + overrides: [ + httpClientFactoryProvider.overrideWith((ref) { + return FakeHttpClientFactory(() => FakeClient()); + }), + ], + userSession: const AuthSessionState( + token: 'test-token', + user: LightUser(id: UserId('test-user-id'), name: 'test-username'), + ), + ); + + fakeAsync((async) { + final session = container.read(authSessionProvider); + expect(session, isNotNull); + + final client = container.read(lichessClientProvider); + try { + client.get(Uri(path: '/will/return/401')); + } on ServerException catch (_) {} + + async.flushMicrotasks(); + + final requests = FakeClient.verifyRequests(); + expect(requests.length, 2); + expect( + requests.first, + isA().having( + (r) => r.headers['Authorization'], + 'Authorization', + 'Bearer ${signBearerToken('test-token')}', + ), + ); + + expect( + requests.last, + isA() + .having( + (r) => r.url.path, + 'path', + '/api/token/test', + ) + .having( + (r) => r.headers['Authorization'], + 'Authorization', + null, + ), + ); + + expect(container.read(authSessionProvider), isNull); + }); + }); }); } @@ -264,11 +354,11 @@ class FakeClient extends http.BaseClient { @override Future send(http.BaseRequest request) { _requests.add(request); - return Future.value(_responseBasedOnPath(request.url.path)); + return Future.value(_responseBasedOnPath(request)); } - http.StreamedResponse _responseBasedOnPath(String path) { - switch (path) { + http.StreamedResponse _responseBasedOnPath(http.BaseRequest request) { + switch (request.url.path) { case '/will/throw/socket/exception': throw const SocketException('no internet'); case '/will/throw/tls/exception': @@ -289,6 +379,14 @@ class FakeClient extends http.BaseClient { return http.StreamedResponse(_streamBody('204'), 204); case '/will/return/301': return http.StreamedResponse(_streamBody('301'), 301); + case '/api/token/test': + final token = request.headers['Authorization']; + final response = ''' + { + "$token": null + } +'''; + return http.StreamedResponse(_streamBody(response), 200); default: return http.StreamedResponse(_streamBody('200'), 200); } From 701958245ce628c3fbd440178c4418bb3ce308d6 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 4 Oct 2024 12:34:40 +0200 Subject: [PATCH 427/979] Add app test for the first session check --- lib/src/app.dart | 36 ----------- lib/src/network/http.dart | 2 +- test/app_test.dart | 107 ++++++++++++++++++-------------- test/mock_server_responses.dart | 6 ++ test/network/http_test.dart | 5 ++ 5 files changed, 71 insertions(+), 85 deletions(-) diff --git a/lib/src/app.dart b/lib/src/app.dart index 48bbd7e3ab..24bf9d26f7 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -1,5 +1,3 @@ -import 'dart:async'; - import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -8,7 +6,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/l10n/l10n.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; -import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/challenge/challenge_service.dart'; import 'package:lichess_mobile/src/model/correspondence/correspondence_service.dart'; import 'package:lichess_mobile/src/model/notifications/notification_service.dart'; @@ -21,9 +18,6 @@ import 'package:lichess_mobile/src/network/socket.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; -import 'package:logging/logging.dart'; - -final _logger = Logger('Application'); /// The main application widget. /// @@ -76,9 +70,6 @@ class _AppState extends ConsumerState { // Perform actions once when the app comes online. if (current.value?.isOnline == true && !_firstTimeOnlineCheck) { _firstTimeOnlineCheck = true; - // check if session is still active and delete it if it is not - checkSession(); - ref.read(correspondenceServiceProvider).syncGames(); } @@ -205,33 +196,6 @@ class _AppState extends ConsumerState { }, ); } - - /// Check if the session is still active and delete it if it is not. - Future checkSession() async { - // check if session is still active - final session = ref.read(authSessionProvider); - if (session != null) { - _logger.fine( - 'Found a stored session: ${session.user.id}. Checking if it is still active.', - ); - try { - final client = ref.read(lichessClientProvider); - final data = await client - .postReadJson( - Uri(path: '/api/token/test'), - body: session.token, - mapper: (json) => json, - ) - .timeout(const Duration(seconds: 3)); - if (data[session.token] == null) { - _logger.fine('Session is not active. Deleting it.'); - await ref.read(authSessionProvider.notifier).delete(); - } - } catch (e) { - _logger.warning('Could not check session: $e'); - } - } - } } // -- diff --git a/lib/src/network/http.dart b/lib/src/network/http.dart index 04f1d3ac09..af20b2faa9 100644 --- a/lib/src/network/http.dart +++ b/lib/src/network/http.dart @@ -207,7 +207,7 @@ class LichessClient implements Client { final defaultClient = _ref.read(defaultClientProvider); final data = await defaultClient .postReadJson( - Uri(path: '/api/token/test'), + lichessUri('/api/token/test'), mapper: (json) => json, ) .timeout(const Duration(seconds: 5)); diff --git a/test/app_test.dart b/test/app_test.dart index 86f6e0d476..6ec425e5d2 100644 --- a/test/app_test.dart +++ b/test/app_test.dart @@ -5,9 +5,10 @@ import 'package:http/testing.dart'; import 'package:lichess_mobile/src/app.dart'; import 'package:lichess_mobile/src/navigation.dart'; import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/view/home/home_tab_screen.dart'; -import 'mock_server_responses.dart'; import 'model/auth/fake_session_storage.dart'; +import 'network/fake_http_client_factory.dart'; import 'test_helpers.dart'; import 'test_provider_scope.dart'; @@ -38,53 +39,63 @@ void main() { ); }); - // testWidgets('App checks a stored session', (tester) async { - // int tokenTestRequests = 0; - // final mockClient = MockClient((request) { - // if (request.url.path == '/api/token/test') { - // tokenTestRequests++; - // return mockResponse( - // ''' -// { - // "${fakeSession.token}": { - // "scopes": "web:mobile", - // "userId": "${fakeSession.user.id}" - // } -// } - // ''', - // 200, - // ); - // } else if (request.url.path == '/api/account') { - // return mockResponse( - // mockApiAccountResponse(fakeSession.user.name), - // 200, - // ); - // } - // return mockResponse('', 404); - // }); - - // final app = await makeTestProviderScope( - // tester, - // child: const Application(), - // userSession: fakeSession, - // overrides: [ - // lichessClientProvider - // .overrideWith((ref) => LichessClient(mockClient, ref)), - // ], - // ); - - // await tester.pumpWidget(app); - - // expect(find.byType(MaterialApp), findsOneWidget); - - // // wait for the session check request to complete - // await tester.pump(const Duration(milliseconds: 100)); - - // expect(tokenTestRequests, 1); - - // // session is still active - // expect(find.text('Hello testUser'), findsOneWidget); - // }); + testWidgets( + 'App will delete a stored session on startup if one request return 401', + (tester) async { + int tokenTestRequests = 0; + final mockClient = MockClient((request) async { + if (request.url.path == '/api/token/test') { + tokenTestRequests++; + return mockResponse( + ''' +{ + "${fakeSession.token}": null +} + ''', + 200, + ); + } else if (request.url.path == '/api/account') { + return mockResponse( + '{"error": "Unauthorized"}', + 401, + ); + } + return mockResponse('', 404); + }); + + final app = await makeTestProviderScope( + tester, + child: const Application(), + userSession: fakeSession, + overrides: [ + httpClientFactoryProvider + .overrideWith((ref) => FakeHttpClientFactory(() => mockClient)), + ], + ); + + await tester.pumpWidget(app); + + expect(find.byType(MaterialApp), findsOneWidget); + expect(find.byType(HomeTabScreen), findsOneWidget); + + // wait for the startup requests and animations to complete + await tester.pumpAndSettle(const Duration(milliseconds: 100)); + + // should see welcome message + expect( + find.text( + 'Lichess is a free (really), libre, no-ads, open source chess server.', + findRichText: true, + ), + findsOneWidget, + ); + + // should have made a request to test the token + expect(tokenTestRequests, 1); + + // session is not active anymore + expect(find.text('Sign in'), findsOneWidget); + }); testWidgets('Bottom navigation', (tester) async { final app = await makeTestProviderScope( diff --git a/test/mock_server_responses.dart b/test/mock_server_responses.dart index 5145f36d56..cc58a2b913 100644 --- a/test/mock_server_responses.dart +++ b/test/mock_server_responses.dart @@ -38,3 +38,9 @@ String mockApiAccountResponse(String username) => ''' } } '''; + +String mockUserRecentGameResponse(String username) => ''' +{"id":"Huk88k3D","rated":false,"variant":"fromPosition","speed":"blitz","perf":"blitz","createdAt":1673716450321,"lastMoveAt":1673716450321,"status":"noStart","players":{"white":{"user":{"name":"MightyNanook","id":"mightynanook"},"rating":1116,"provisional":true},"black":{"user":{"name":"$username","patron":true,"id":"${username.toLowerCase()}"},"rating":1772}},"initialFen":"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w - - 0 1","winner":"black","tournament":"ZZQ9tunK","clock":{"initial":300,"increment":0,"totalTime":300},"lastFen":"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w - - 0 1"} +{"id":"g2bzFol8","rated":true,"variant":"standard","speed":"blitz","perf":"blitz","createdAt":1673553626465,"lastMoveAt":1673553936657,"status":"resign","players":{"white":{"user":{"name":"SchallUndRausch","id":"schallundrausch"},"rating":1751,"ratingDiff":-5},"black":{"user":{"name":"$username","patron":true,"id":"${username.toLowerCase()}"},"rating":1767,"ratingDiff":5}},"winner":"black","clock":{"initial":180,"increment":2,"totalTime":260},"lastFen":"r7/pppk4/4p1B1/3pP3/6Pp/q1P1P1nP/P1QK1r2/R5R1 w - - 1 1"} +{"id":"9WLmxmiB","rated":true,"variant":"standard","speed":"blitz","perf":"blitz","createdAt":1673553299064,"lastMoveAt":1673553615438,"status":"resign","players":{"white":{"user":{"name":"Dr-Alaakour","id":"dr-alaakour"},"rating":1806,"ratingDiff":5},"black":{"user":{"name":"$username","patron":true,"id":"${username.toLowerCase()}"},"rating":1772,"ratingDiff":-5}},"winner":"white","clock":{"initial":180,"increment":0,"totalTime":180},"lastFen":"2b1Q1k1/p1r4p/1p2p1p1/3pN3/2qP4/P4R2/1P3PPP/4R1K1 b - - 0 1"} +'''; diff --git a/test/network/http_test.dart b/test/network/http_test.dart index af181da57f..eb48167f16 100644 --- a/test/network/http_test.dart +++ b/test/network/http_test.dart @@ -320,6 +320,11 @@ void main() { expect( requests.last, isA() + .having( + (r) => r.url.host, + 'host', + 'lichess.dev', + ) .having( (r) => r.url.path, 'path', From 297d6b7fdb8273c18cc718f9a2befed9322e1d3d Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Sat, 5 Oct 2024 11:19:02 +0200 Subject: [PATCH 428/979] Revamp puzzle tab --- lib/src/model/puzzle/puzzle.dart | 2 + lib/src/utils/connectivity.dart | 58 +++++ .../view/puzzle/puzzle_history_screen.dart | 2 + lib/src/view/puzzle/puzzle_tab_screen.dart | 224 +++++++++++------- 4 files changed, 198 insertions(+), 88 deletions(-) diff --git a/lib/src/model/puzzle/puzzle.dart b/lib/src/model/puzzle/puzzle.dart index 4fa9aafaf3..d62db26e5b 100644 --- a/lib/src/model/puzzle/puzzle.dart +++ b/lib/src/model/puzzle/puzzle.dart @@ -54,6 +54,8 @@ class PuzzleData with _$PuzzleData { required ISet themes, }) = _PuzzleData; + Side get sideToMove => initialPly.isEven ? Side.black : Side.white; + factory PuzzleData.fromJson(Map json) => _$PuzzleDataFromJson(json); } diff --git a/lib/src/utils/connectivity.dart b/lib/src/utils/connectivity.dart index 6d80a51cee..a7323c8996 100644 --- a/lib/src/utils/connectivity.dart +++ b/lib/src/utils/connectivity.dart @@ -151,3 +151,61 @@ Future isOnline(Client client) { } return completer.future; } + +extension AsyncValueConnectivity on AsyncValue { + /// Switches between two functions based on the device's connectivity status. + /// + /// Using this method assumes the the device is offline when the status is + /// not yet available (i.e. [AsyncValue.isLoading]. + /// If you want to handle the loading state separately, use + /// [whenOnlineLoading] instead. + /// + /// This method is similar to [AsyncValueX.maybeWhen], but it takes two + /// functions, one for when the device is online and another for when it is + /// offline. + /// + /// Example: + /// ```dart + /// final status = ref.watch(connectivityChangesProvider); + /// final result = status.whenOnline( + /// online: () => 'Online', + /// offline: () => 'Offline', + /// ); + /// ``` + R whenOnline({ + required R Function() online, + required R Function() offline, + }) { + return maybeWhen( + data: (status) => status.isOnline ? online() : offline(), + orElse: offline, + ); + } + + /// Switches between three functions based on the device's connectivity status. + /// + /// This method is similar to [AsyncValueX.when], but it takes three + /// functions, one for when the device is online, another for when it is + /// offline, and the last for when the status is still loading. + /// + /// Example: + /// ```dart + /// final status = ref.watch(connectivityChangesProvider); + /// final result = status.whenOnlineLoading( + /// online: () => 'Online', + /// offline: () => 'Offline', + /// loading: () => 'Loading', + /// ); + /// ``` + R whenOnlineLoading({ + required R Function() online, + required R Function() offline, + required R Function() loading, + }) { + return when( + data: (status) => status.isOnline ? online() : offline(), + loading: loading, + error: (error, stack) => offline(), + ); + } +} diff --git a/lib/src/view/puzzle/puzzle_history_screen.dart b/lib/src/view/puzzle/puzzle_history_screen.dart index 9fbbab7774..d2df0b6ca2 100644 --- a/lib/src/view/puzzle/puzzle_history_screen.dart +++ b/lib/src/view/puzzle/puzzle_history_screen.dart @@ -21,6 +21,8 @@ import 'package:timeago/timeago.dart' as timeago; final _dateFormatter = DateFormat.yMMMd(); class PuzzleHistoryScreen extends StatelessWidget { + const PuzzleHistoryScreen(); + @override Widget build(BuildContext context) { return PlatformScaffold( diff --git a/lib/src/view/puzzle/puzzle_tab_screen.dart b/lib/src/view/puzzle/puzzle_tab_screen.dart index 7ebf8dbdfe..ec6dffea5a 100644 --- a/lib/src/view/puzzle/puzzle_tab_screen.dart +++ b/lib/src/view/puzzle/puzzle_tab_screen.dart @@ -64,6 +64,8 @@ class _PuzzleTabScreenState extends ConsumerState { ], ); + final isTablet = isTabletOrLarger(context); + return PopScope( canPop: false, onPopInvokedWithResult: (bool didPop, _) { @@ -74,8 +76,9 @@ class _PuzzleTabScreenState extends ConsumerState { child: Scaffold( appBar: AppBar( title: Text(context.l10n.puzzles), - actions: const [ - _DashboardButton(), + actions: [ + const _DashboardButton(), + if (!isTablet) const _HistoryButton(), ], ), body: userSession != null @@ -90,6 +93,8 @@ class _PuzzleTabScreenState extends ConsumerState { } Widget _iosBuilder(BuildContext context, AuthSessionState? userSession) { + final isTablet = isTabletOrLarger(context); + return CupertinoPageScaffold( child: CustomScrollView( controller: puzzlesScrollController, @@ -100,10 +105,14 @@ class _PuzzleTabScreenState extends ConsumerState { end: 8.0, ), largeTitle: Text(context.l10n.puzzles), - trailing: const Row( + trailing: Row( mainAxisSize: MainAxisSize.min, children: [ - _DashboardButton(), + const _DashboardButton(), + if (!isTablet) ...[ + const SizedBox(width: 6.0), + const _HistoryButton(), + ], ], ), ), @@ -140,17 +149,15 @@ class _Body extends ConsumerWidget { final isTablet = isTabletOrLarger(context); final handsetChildren = [ - connectivity.when( - data: (data) => data.isOnline - ? const _DailyPuzzle() - : const _OfflinePuzzlePreview(), - loading: () => const SizedBox.shrink(), - error: (_, __) => const SizedBox.shrink(), + connectivity.whenOnline( + online: () => const _DailyPuzzle(), + offline: () => const SizedBox.shrink(), ), + const SizedBox(height: 4.0), + const _PuzzlePreview(), if (Theme.of(context).platform == TargetPlatform.android) const SizedBox(height: 8.0), _PuzzleMenu(connectivity: connectivity), - PuzzleHistoryWidget(), ]; final tabletChildren = [ @@ -163,12 +170,9 @@ class _Body extends ConsumerWidget { mainAxisAlignment: MainAxisAlignment.start, children: [ const SizedBox(height: 8.0), - connectivity.when( - data: (data) => data.isOnline - ? const _DailyPuzzle() - : const _OfflinePuzzlePreview(), - loading: () => const SizedBox.shrink(), - error: (_, __) => const SizedBox.shrink(), + connectivity.whenOnline( + online: () => const _DailyPuzzle(), + offline: () => const SizedBox.shrink(), ), _PuzzleMenu(connectivity: connectivity), ], @@ -244,21 +248,6 @@ class _PuzzleMenu extends StatelessWidget { return ListSection( hasLeading: true, children: [ - _PuzzleMenuListTile( - icon: PuzzleIcons.mix, - title: context.l10n.puzzlePuzzles, - subtitle: context.l10n.puzzleDesc, - onTap: () { - pushPlatformRoute( - context, - title: context.l10n.puzzleDesc, - rootNavigator: true, - builder: (context) => const PuzzleScreen( - angle: PuzzleTheme(PuzzleThemeKey.mix), - ), - ); - }, - ), _PuzzleMenuListTile( icon: PuzzleIcons.opening, title: context.l10n.puzzlePuzzleThemes, @@ -353,7 +342,7 @@ class PuzzleHistoryWidget extends ConsumerWidget { headerTrailing: NoPaddingTextButton( onPressed: () => pushPlatformRoute( context, - builder: (context) => PuzzleHistoryScreen(), + builder: (context) => const PuzzleHistoryScreen(), ), child: Text( context.l10n.more, @@ -402,23 +391,50 @@ class _DashboardButton extends ConsumerWidget { final session = ref.watch(authSessionProvider); if (session != null) { return AppBarIconButton( - icon: const Icon(Icons.history), + icon: const Icon(Icons.assessment_outlined), semanticsLabel: context.l10n.puzzlePuzzleDashboard, onPressed: () { ref.invalidate(puzzleDashboardProvider); - _showDashboard(context, session); + pushPlatformRoute( + context, + title: context.l10n.puzzlePuzzleDashboard, + builder: (_) => const PuzzleDashboardScreen(), + ); }, ); } return const SizedBox.shrink(); } +} + +class _HistoryButton extends ConsumerWidget { + const _HistoryButton(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final asyncData = ref.watch(puzzleRecentActivityProvider); + return AppBarIconButton( + icon: const Icon(Icons.history_outlined), + semanticsLabel: context.l10n.puzzleHistory, + onPressed: asyncData.maybeWhen( + data: (_) => () { + pushPlatformRoute( + context, + title: context.l10n.puzzleHistory, + builder: (_) => const PuzzleHistoryScreen(), + ); + }, + orElse: () => null, + ), + ); + } +} - void _showDashboard(BuildContext context, AuthSessionState session) => - pushPlatformRoute( - context, - title: context.l10n.puzzlePuzzleDashboard, - builder: (_) => const PuzzleDashboardScreen(), - ); +TextStyle _puzzlePreviewSubtitleStyle(BuildContext context) { + return TextStyle( + fontSize: 14.0, + color: DefaultTextStyle.of(context).style.color?.withValues(alpha: 0.6), + ); } class _DailyPuzzle extends ConsumerWidget { @@ -449,6 +465,7 @@ class _DailyPuzzle extends ConsumerWidget { context.l10n .puzzlePlayedXTimes(data.puzzle.plays) .localizeNumbers(), + style: _puzzlePreviewSubtitleStyle(context), ), ], ), @@ -461,7 +478,7 @@ class _DailyPuzzle extends ConsumerWidget { ?.withValues(alpha: 0.6), ), Text( - data.puzzle.initialPly.isOdd + data.puzzle.sideToMove == Side.white ? context.l10n.whitePlays : context.l10n.blackPlays, ), @@ -503,59 +520,90 @@ class _DailyPuzzle extends ConsumerWidget { } } -class _OfflinePuzzlePreview extends ConsumerWidget { - const _OfflinePuzzlePreview(); +class _PuzzlePreview extends ConsumerWidget { + const _PuzzlePreview(); @override Widget build(BuildContext context, WidgetRef ref) { final puzzle = ref.watch(nextPuzzleProvider(const PuzzleTheme(PuzzleThemeKey.mix))); - return puzzle.maybeWhen( - data: (data) { - final preview = - data != null ? PuzzlePreview.fromPuzzle(data.puzzle) : null; - return SmallBoardPreview( - orientation: preview?.orientation ?? Side.white, - fen: preview?.initialFen ?? kEmptyFen, - lastMove: preview?.initialMove, - description: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - Text( - context.l10n.puzzleDesc, - style: Styles.boardPreviewTitle, - ), + + Widget buildPuzzlePreview(Puzzle? puzzle, {bool loading = false}) { + final preview = puzzle != null ? PuzzlePreview.fromPuzzle(puzzle) : null; + return SmallBoardPreview( + orientation: preview?.orientation ?? Side.white, + fen: preview?.initialFen ?? kEmptyFen, + lastMove: preview?.initialMove, + description: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + context.l10n.puzzleDesc, + style: Styles.boardPreviewTitle, + ), + Text( + context.l10n.puzzleThemeHealthyMixDescription, + maxLines: 3, + overflow: TextOverflow.ellipsis, + style: TextStyle( + height: 1.2, + fontSize: 12.0, + color: DefaultTextStyle.of(context) + .style + .color + ?.withValues(alpha: 0.6), + ), + ), + ], + ), + Icon( + PuzzleIcons.mix, + size: 34, + color: DefaultTextStyle.of(context) + .style + .color + ?.withValues(alpha: 0.6), + ), + if (puzzle != null) Text( - context.l10n - .puzzlePlayedXTimes(data?.puzzle.puzzle.plays ?? 0) - .localizeNumbers(), + puzzle.puzzle.sideToMove == Side.white + ? context.l10n.whitePlays + : context.l10n.blackPlays, + ) + else if (!loading) + const Text( + 'No puzzles available, please go online to fetch them.', ), - ], - ), - onTap: data != null - ? () { - pushPlatformRoute( - context, - rootNavigator: true, - builder: (context) => const PuzzleScreen( - angle: PuzzleTheme(PuzzleThemeKey.mix), - ), - ).then((_) { - if (context.mounted) { - ref.invalidate( - nextPuzzleProvider( - const PuzzleTheme(PuzzleThemeKey.mix), - ), - ); - } - }); - } - : null, - ); - }, - orElse: () => const SizedBox.shrink(), + ], + ), + onTap: puzzle != null + ? () { + pushPlatformRoute( + context, + rootNavigator: true, + builder: (context) => const PuzzleScreen( + angle: PuzzleTheme(PuzzleThemeKey.mix), + ), + ).then((_) { + if (context.mounted) { + ref.invalidate( + nextPuzzleProvider(const PuzzleTheme(PuzzleThemeKey.mix)), + ); + } + }); + } + : null, + ); + } + + return puzzle.maybeWhen( + data: (data) => buildPuzzlePreview(data?.puzzle), + orElse: () => buildPuzzlePreview(null, loading: true), ); } } From e107aba678a5ba58d874a29786e57904e74e8d78 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Sat, 28 Sep 2024 10:06:01 +0200 Subject: [PATCH 429/979] feat: make analysis tree view more "tree like" --- lib/src/model/common/uci.dart | 2 + lib/src/view/analysis/tree_view.dart | 940 ++++++++++++------- test/view/analysis/analysis_screen_test.dart | 22 +- 3 files changed, 630 insertions(+), 334 deletions(-) diff --git a/lib/src/model/common/uci.dart b/lib/src/model/common/uci.dart index 4ff3c5a1e7..cc8c8b6716 100644 --- a/lib/src/model/common/uci.dart +++ b/lib/src/model/common/uci.dart @@ -90,6 +90,8 @@ class UciPath with _$UciPath { return UciPath(path.toString()); } + factory UciPath.join(UciPath a, UciPath b) => UciPath(a.value + b.value); + /// Creates a UciPath from a list of UCI moves. /// /// Throws an [ArgumentError] if any of the moves is invalid. diff --git a/lib/src/view/analysis/tree_view.dart b/lib/src/view/analysis/tree_view.dart index 9662c5410c..70a7ebefdc 100644 --- a/lib/src/view/analysis/tree_view.dart +++ b/lib/src/view/analysis/tree_view.dart @@ -1,3 +1,4 @@ +import 'package:collection/collection.dart'; import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/cupertino.dart'; @@ -119,38 +120,6 @@ class _InlineTreeViewState extends ConsumerState { analysisPreferencesProvider.select((value) => value.showAnnotations), ); - final List moveWidgets = _buildTreeWidget( - widget.pgn, - widget.options, - parent: root, - nodes: root.children, - shouldShowAnnotations: shouldShowAnnotations, - shouldShowComments: shouldShowComments, - inMainline: true, - startMainline: true, - startSideline: false, - initialPath: UciPath.empty, - ); - - // trick to make auto-scroll work when returning to the root position - moveWidgets.insert( - 0, - currentPath.isEmpty - ? SizedBox.shrink(key: currentMoveKey) - : const SizedBox.shrink(), - ); - - if (shouldShowComments && - rootComments?.any((c) => c.text?.isNotEmpty == true) == true) { - moveWidgets.insert( - 0, - Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: _Comments(rootComments!), - ), - ); - } - return CustomScrollView( slivers: [ if (kOpeningAllowedVariants.contains(widget.options.variant)) @@ -162,124 +131,533 @@ class _InlineTreeViewState extends ConsumerState { ), SliverFillRemaining( hasScrollBody: false, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 10), - child: Wrap( - spacing: kInlineMoveSpacing, - children: moveWidgets, + child: _PgnTreeView( + root: root, + rootComments: rootComments, + params: ( + shouldShowAnnotations: shouldShowAnnotations, + shouldShowComments: shouldShowComments, + currentMoveKey: currentMoveKey, + currentPath: currentPath, + notifier: () => ref.read(ctrlProvider.notifier), ), ), ), ], ); } +} + +// A group of parameters that are passed through various parts of the tree view +// and ultimately evaluated in the InlineMove widget. Grouped in this record to improve readability. +typedef _PgnTreeViewParams = ({ + UciPath currentPath, + bool shouldShowAnnotations, + bool shouldShowComments, + GlobalKey currentMoveKey, + AnalysisController Function() notifier, +}); + +/// True if the side line has no branching and is less than 6 moves deep. +bool _displaySideLineAsInline(ViewBranch node, [int depth = 0]) { + if (depth == 6) return false; + if (node.children.isEmpty) return true; + if (node.children.length > 1) return false; + return _displaySideLineAsInline(node.children.first, depth + 1); +} + +bool _hasNonInlineSideLine(ViewNode node) => + node.children.length > 2 || + (node.children.length == 2 && !_displaySideLineAsInline(node.children[1])); + +Iterable> _mainlineParts(ViewRoot root) => + [root, ...root.mainline].splitAfter(_hasNonInlineSideLine); + +class _PgnTreeView extends StatelessWidget { + const _PgnTreeView({ + required this.root, + required this.rootComments, + required this.params, + }); + + final ViewRoot root; + + final IList? rootComments; + + final _PgnTreeViewParams params; + + @override + Widget build(BuildContext context) { + var path = UciPath.empty; + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // trick to make auto-scroll work when returning to the root position + if (params.currentPath.isEmpty) + SizedBox.shrink(key: params.currentMoveKey), + + if (params.shouldShowComments && + rootComments?.any((c) => c.text?.isNotEmpty == true) == true) + Text.rich( + TextSpan( + children: comments(rootComments!, textStyle: _baseTextStyle), + ), + ), + ..._mainlineParts(root).map( + (nodes) { + final mainlineInitialPath = path; + + final sidelineInitialPath = UciPath.join( + path, + UciPath.fromIds( + nodes.take(nodes.length - 1).map((n) => n.children.first.id), + ), + ); + + path = sidelineInitialPath; + if (nodes.last.children.isNotEmpty) { + path = path + nodes.last.children.first.id; + } + + return [ + _MainLinePart( + params: params, + initialPath: mainlineInitialPath, + nodes: nodes, + ), + if (nodes.last.children.length > 1) + _IndentedSideLines( + nodes.last.children.skip(1), + parent: nodes.last, + params: params, + initialPath: sidelineInitialPath, + ), + ]; + }, + ).flattened, + ], + ), + ); + } +} + +List _buildInlineSideLine({ + required ViewBranch firstNode, + required ViewNode parent, + required UciPath initialPath, + required TextStyle textStyle, + required bool followsComment, + required _PgnTreeViewParams params, +}) { + textStyle = textStyle.copyWith( + fontStyle: FontStyle.italic, + fontSize: textStyle.fontSize != null ? textStyle.fontSize! - 2.0 : null, + ); + + final sidelineNodes = [firstNode, ...firstNode.mainline]; + + var path = initialPath; + return [ + if (followsComment) const WidgetSpan(child: SizedBox(width: 4.0)), + ...sidelineNodes.mapIndexedAndLast( + (i, node, last) { + final pathToNode = path; + path = path + node.id; + + return [ + if (i == 0) ...[ + if (followsComment) const WidgetSpan(child: SizedBox(width: 4.0)), + TextSpan( + text: '(', + style: textStyle, + ), + ], + ...moveWithComment( + node, + parent: i == 0 ? parent : sidelineNodes[i - 1], + lineInfo: ( + type: LineType.inlineSideline, + startLine: i == 0 || sidelineNodes[i - 1].hasTextComment + ), + pathToNode: pathToNode, + textStyle: textStyle, + params: params, + ), + if (last) + TextSpan( + text: ')', + style: textStyle, + ), + ]; + }, + ).flattened, + const WidgetSpan(child: SizedBox(width: 4.0)), + ]; +} + +const _baseTextStyle = TextStyle( + fontSize: 16.0, + height: 1.5, +); - List _buildTreeWidget( - String pgn, - AnalysisOptions options, { - required ViewNode parent, - required IList nodes, - required bool inMainline, - required bool startMainline, - required bool startSideline, - required bool shouldShowAnnotations, - required bool shouldShowComments, - required UciPath initialPath, - }) { - if (nodes.isEmpty) return []; - final List widgets = []; - - final firstChild = nodes.first; - final newPath = initialPath + firstChild.id; - final currentMove = newPath == currentPath; - - // add the first child - widgets.add( - InlineMove( - pgn, - options, - path: newPath, +enum LineType { + mainline, + sideline, + inlineSideline, +} + +typedef LineInfo = ({LineType type, bool startLine}); + +List moveWithComment( + ViewBranch branch, { + required ViewNode parent, + required TextStyle textStyle, + required LineInfo lineInfo, + required UciPath pathToNode, + required _PgnTreeViewParams params, + GlobalKey? moveKey, +}) { + return [ + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: InlineMove( + branch: branch, parent: parent, - branch: firstChild, - isCurrentMove: currentMove, - key: currentMove ? currentMoveKey : null, - shouldShowAnnotations: shouldShowAnnotations, - shouldShowComments: shouldShowComments, - isSideline: !inMainline, - startMainline: startMainline, - startSideline: startSideline, - endSideline: !inMainline && firstChild.children.isEmpty, + lineInfo: lineInfo, + path: pathToNode + branch.id, + key: moveKey, + textStyle: textStyle, + params: params, ), + ), + if (params.shouldShowComments && branch.hasTextComment) + ...comments(branch.comments!, textStyle: textStyle), + ]; +} + +class _SideLinePart extends ConsumerWidget { + _SideLinePart( + this.nodes, { + required this.parent, + required this.initialPath, + required this.firstMoveKey, + required this.params, + }) : assert(nodes.isNotEmpty); + + final List nodes; + + final ViewNode parent; + + final UciPath initialPath; + + final GlobalKey firstMoveKey; + + final _PgnTreeViewParams params; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final textStyle = _baseTextStyle.copyWith( + color: _textColor(context, 0.6), + fontSize: _baseTextStyle.fontSize! - 1.0, ); - // add the sidelines if present - for (var i = 1; i < nodes.length; i++) { - final node = nodes[i]; - if (node.isHidden) continue; - // start new sideline from mainline on a new line - if (inMainline) { - widgets.add( - SizedBox( - width: double.infinity, - child: Wrap( - spacing: kInlineMoveSpacing, - children: _buildTreeWidget( - pgn, - options, - parent: parent, - nodes: [nodes[i]].lockUnsafe, - shouldShowAnnotations: shouldShowAnnotations, - shouldShowComments: shouldShowComments, - inMainline: false, - startMainline: false, - startSideline: true, - initialPath: initialPath, + var path = initialPath + nodes.first.id; + final moves = [ + ...moveWithComment( + nodes.first, + parent: parent, + lineInfo: ( + type: LineType.sideline, + startLine: true, + ), + moveKey: firstMoveKey, + pathToNode: initialPath, + textStyle: textStyle, + params: params, + ), + ...nodes.take(nodes.length - 1).map( + (node) { + final moves = [ + ...moveWithComment( + node.children.first, + parent: node, + lineInfo: ( + type: LineType.sideline, + startLine: node.hasTextComment, ), + pathToNode: path, + textStyle: textStyle, + params: params, ), + if (node.children.length == 2 && + _displaySideLineAsInline(node.children[1])) + ..._buildInlineSideLine( + followsComment: node.children.first.hasTextComment, + firstNode: node.children[1], + parent: node, + initialPath: path, + textStyle: textStyle, + params: params, + ), + ]; + path = path + node.children.first.id; + return moves; + }, + ).flattened, + ]; + + return Text.rich( + TextSpan( + children: moves, + ), + ); + } +} + +class _MainLinePart extends ConsumerWidget { + const _MainLinePart({ + required this.initialPath, + required this.params, + required this.nodes, + }); + + final UciPath initialPath; + + final List nodes; + + final _PgnTreeViewParams params; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final textStyle = _baseTextStyle.copyWith( + color: _textColor(context, 0.9), + ); + + var path = initialPath; + return Text.rich( + TextSpan( + children: nodes + .takeWhile((node) => node.children.isNotEmpty) + .mapIndexed( + (i, node) { + final mainlineNode = node.children.first; + final moves = [ + moveWithComment( + mainlineNode, + parent: node, + lineInfo: ( + type: LineType.mainline, + startLine: i == 0 || (node as ViewBranch).hasTextComment, + ), + pathToNode: path, + textStyle: textStyle, + params: params, + ), + if (node.children.length == 2 && + _displaySideLineAsInline(node.children[1])) ...[ + _buildInlineSideLine( + followsComment: mainlineNode.hasTextComment, + firstNode: node.children[1], + parent: node, + initialPath: path, + textStyle: textStyle, + params: params, + ), + ], + ]; + path = path + mainlineNode.id; + return moves.flattened; + }, + ) + .flattened + .toList(), + ), + ); + } +} + +class _SideLines extends StatelessWidget { + const _SideLines({ + required this.firstNode, + required this.parent, + required this.firstMoveKey, + required this.initialPath, + required this.params, + }); + + final ViewBranch firstNode; + final ViewNode parent; + final GlobalKey firstMoveKey; + final UciPath initialPath; + final _PgnTreeViewParams params; + + @override + Widget build(BuildContext context) { + final sidelineNodes = [ + firstNode, + if (!_hasNonInlineSideLine(firstNode)) + ...firstNode.mainline.takeWhile((node) => !_hasNonInlineSideLine(node)), + ]; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _SideLinePart( + sidelineNodes.toList(), + parent: parent, + firstMoveKey: firstMoveKey, + initialPath: initialPath, + params: params, + ), + if (sidelineNodes.last.children.isNotEmpty) + _IndentedSideLines( + sidelineNodes.last.children, + parent: sidelineNodes.last, + initialPath: UciPath.join( + initialPath, + UciPath.fromIds(sidelineNodes.map((node) => node.id)), + ), + params: params, ), - ); - } else { - widgets.addAll( - _buildTreeWidget( - pgn, - options, - parent: parent, - nodes: [nodes[i]].lockUnsafe, - shouldShowAnnotations: shouldShowAnnotations, - shouldShowComments: shouldShowComments, - inMainline: false, - startMainline: false, - startSideline: true, - initialPath: initialPath, - ), - ); + ], + ); + } +} + +class _IndentPainter extends CustomPainter { + const _IndentPainter({ + required this.sideLineStartPositions, + required this.color, + required this.padding, + }); + + final List sideLineStartPositions; + + final Color color; + + final double padding; + + @override + void paint(Canvas canvas, Size size) { + if (sideLineStartPositions.isNotEmpty) { + final paint = Paint() + ..strokeWidth = 1.5 + ..color = color + ..strokeCap = StrokeCap.round + ..style = PaintingStyle.stroke; + + final origin = Offset(-padding, 0); + + final path = Path()..moveTo(origin.dx, origin.dy); + path.lineTo(origin.dx, sideLineStartPositions.last.dy); + for (final position in sideLineStartPositions) { + path.moveTo(origin.dx, position.dy); + path.lineTo(origin.dx + padding / 2, position.dy); } + canvas.drawPath(path, paint); } + } - // add the children of the first child - widgets.addAll( - _buildTreeWidget( - pgn, - options, - parent: firstChild, - nodes: firstChild.children, - shouldShowAnnotations: shouldShowAnnotations, - shouldShowComments: shouldShowComments, - inMainline: inMainline, - startMainline: false, - startSideline: false, - initialPath: newPath, + @override + bool shouldRepaint(_IndentPainter oldDelegate) { + return oldDelegate.sideLineStartPositions != sideLineStartPositions || + oldDelegate.color != color; + } +} + +class _IndentedSideLines extends StatefulWidget { + const _IndentedSideLines( + this.sideLines, { + required this.parent, + required this.initialPath, + required this.params, + }); + + final Iterable sideLines; + + final ViewNode parent; + + final UciPath initialPath; + + final _PgnTreeViewParams params; + + @override + State<_IndentedSideLines> createState() => _IndentedSideLinesState(); +} + +class _IndentedSideLinesState extends State<_IndentedSideLines> { + late List _keys; + + List _sideLineStartPositions = []; + + final GlobalKey _columnKey = GlobalKey(); + + @override + void initState() { + super.initState(); + _keys = List.generate(widget.sideLines.length, (_) => GlobalKey()); + + WidgetsBinding.instance.addPostFrameCallback((_) { + final RenderBox? columnBox = + _columnKey.currentContext?.findRenderObject() as RenderBox?; + final Offset rowOffset = + columnBox?.localToGlobal(Offset.zero) ?? Offset.zero; + + final positions = _keys.map((key) { + final context = key.currentContext; + final renderBox = context?.findRenderObject() as RenderBox?; + final height = renderBox?.size.height ?? 0; + final offset = renderBox?.localToGlobal(Offset.zero) ?? Offset.zero; + return Offset(offset.dx, offset.dy + height / 2) - rowOffset; + }).toList(); + + setState(() { + _sideLineStartPositions = positions; + }); + }); + } + + @override + Widget build(BuildContext context) { + final sideLineWidgets = widget.sideLines + .mapIndexed( + (i, firstSidelineNode) => _SideLines( + firstNode: firstSidelineNode, + parent: widget.parent, + firstMoveKey: _keys[i], + initialPath: widget.initialPath, + params: widget.params, + ), + ) + .toList(); + + const padding = 12.0; + return RepaintBoundary( + child: Padding( + padding: const EdgeInsets.only(left: padding), + child: CustomPaint( + painter: _IndentPainter( + sideLineStartPositions: _sideLineStartPositions, + color: _textColor(context, 0.6)!, + padding: padding, + ), + child: Column( + key: _columnKey, + crossAxisAlignment: CrossAxisAlignment.start, + children: sideLineWidgets, + ), + ), ), ); - - return widgets; } } Color? _textColor( BuildContext context, double opacity, { - bool isLichessGameAnalysis = true, int? nag, }) { final defaultColor = Theme.of(context).platform == TargetPlatform.android @@ -294,200 +672,149 @@ Color? _textColor( } class InlineMove extends ConsumerWidget { - const InlineMove( - this.pgn, - this.options, { - required this.path, - required this.parent, + const InlineMove({ required this.branch, - required this.shouldShowAnnotations, - required this.shouldShowComments, - required this.isCurrentMove, - required this.isSideline, + required this.parent, + required this.path, + required this.textStyle, + required this.lineInfo, super.key, - this.startMainline = false, - this.startSideline = false, - this.endSideline = false, + required this.params, }); - final String pgn; - final AnalysisOptions options; - final UciPath path; final ViewNode parent; final ViewBranch branch; - final bool shouldShowAnnotations; - final bool shouldShowComments; - final bool isCurrentMove; - final bool isSideline; - final bool startMainline; - final bool startSideline; - final bool endSideline; + final UciPath path; + + final TextStyle textStyle; + + final LineInfo lineInfo; + + final _PgnTreeViewParams params; static const borderRadius = BorderRadius.all(Radius.circular(4.0)); - static const baseTextStyle = TextStyle( - fontSize: 16.0, - height: 1.5, - ); + + bool get isCurrentMove => params.currentPath == path; @override Widget build(BuildContext context, WidgetRef ref) { - final ctrlProvider = analysisControllerProvider(pgn, options); - final move = branch.sanMove; - final ply = branch.position.ply; - final pieceNotation = ref.watch(pieceNotationProvider).maybeWhen( data: (value) => value, orElse: () => defaultAccountPreferences.pieceNotation, ); - final fontFamily = + final moveFontFamily = pieceNotation == PieceNotation.symbol ? 'ChessFont' : null; - final textStyle = isSideline - ? TextStyle( - fontFamily: fontFamily, - color: _textColor(context, 0.6), - ) - : baseTextStyle.copyWith( - fontFamily: fontFamily, - color: _textColor(context, 0.9), - fontWeight: FontWeight.w600, - ); + final moveTextStyle = textStyle.copyWith( + fontFamily: moveFontFamily, + fontWeight: isCurrentMove + ? FontWeight.bold + : lineInfo.type == LineType.inlineSideline + ? FontWeight.normal + : FontWeight.w600, + ); - final indexTextStyle = baseTextStyle.copyWith( + final indexTextStyle = textStyle.copyWith( color: _textColor(context, 0.6), ); - final indexWidget = ply.isOdd - ? Text( - '${(ply / 2).ceil()}.', + final indexText = branch.position.ply.isOdd + ? TextSpan( + text: '${(branch.position.ply / 2).ceil()}.', style: indexTextStyle, ) - : ((startMainline || startSideline) - ? Text( - '${(ply / 2).ceil()}...', + : (lineInfo.startLine + ? TextSpan( + text: '${(branch.position.ply / 2).ceil()}…', style: indexTextStyle, ) : null); - final moveWithNag = move.san + - (branch.nags != null && shouldShowAnnotations + final moveWithNag = branch.sanMove.san + + (branch.nags != null && params.shouldShowAnnotations ? moveAnnotationChar(branch.nags!) : ''); - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (startSideline) - Padding( - padding: const EdgeInsets.only(right: 4.0), - child: Text('(', style: textStyle), - ), - if (shouldShowComments && branch.hasStartingTextComment) - Flexible( - child: Padding( - padding: const EdgeInsets.only(right: 8.0), - child: - _Comments(branch.startingComments!, isSideline: isSideline), - ), + final nag = params.shouldShowAnnotations ? branch.nags?.firstOrNull : null; + + final ply = branch.position.ply; + return AdaptiveInkWell( + key: isCurrentMove ? params.currentMoveKey : null, + borderRadius: borderRadius, + onTap: () => params.notifier().userJump(path), + onLongPress: () { + showAdaptiveBottomSheet( + context: context, + isDismissible: true, + isScrollControlled: true, + showDragHandle: true, + builder: (context) => _MoveContextMenu( + notifier: params.notifier, + title: ply.isOdd + ? '${(ply / 2).ceil()}. $moveWithNag' + : '${(ply / 2).ceil()}... $moveWithNag', + path: path, + parent: parent, + branch: branch, + isSideline: lineInfo.type != LineType.mainline, ), - if (indexWidget != null) indexWidget, - if (indexWidget != null) const SizedBox(width: 1), - AdaptiveInkWell( - borderRadius: borderRadius, - onTap: () => ref.read(ctrlProvider.notifier).userJump(path), - onLongPress: () { - showAdaptiveBottomSheet( - context: context, - isDismissible: true, - isScrollControlled: true, - showDragHandle: true, - builder: (context) => _MoveContextMenu( - pgn, - options, - title: ply.isOdd - ? '${(ply / 2).ceil()}. $moveWithNag' - : '${(ply / 2).ceil()}... $moveWithNag', - path: path, - parent: parent, - branch: branch, - isSideline: isSideline, + ); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 5.0, vertical: 2.0), + decoration: isCurrentMove + ? BoxDecoration( + color: Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoColors.systemGrey3.resolveFrom(context) + : Theme.of(context).focusColor, + shape: BoxShape.rectangle, + borderRadius: borderRadius, + ) + : null, + child: Text.rich( + TextSpan( + children: [ + if (indexText != null) ...[ + indexText, + const WidgetSpan(child: SizedBox(width: 3)), + ], + TextSpan( + text: moveWithNag, + style: moveTextStyle.copyWith( + color: _textColor( + context, + isCurrentMove ? 1 : 0.9, + nag: nag, + ), + ), ), - ); - }, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 5.0, vertical: 2.0), - decoration: isCurrentMove - ? BoxDecoration( - color: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoColors.systemGrey3.resolveFrom(context) - : Theme.of(context).focusColor, - shape: BoxShape.rectangle, - borderRadius: borderRadius, - ) - : null, - child: Text( - moveWithNag, - style: isCurrentMove - ? textStyle.copyWith( - fontWeight: FontWeight.bold, - color: _textColor( - context, - 1, - isLichessGameAnalysis: options.isLichessGameAnalysis, - nag: shouldShowAnnotations - ? branch.nags?.firstOrNull - : null, - ), - ) - : textStyle.copyWith( - color: _textColor( - context, - 0.9, - isLichessGameAnalysis: options.isLichessGameAnalysis, - nag: shouldShowAnnotations - ? branch.nags?.firstOrNull - : null, - ), - ), - ), + ], ), ), - if (shouldShowComments && branch.hasTextComment) - Flexible( - child: Padding( - padding: const EdgeInsets.only(left: 8.0), - child: _Comments(branch.comments!, isSideline: isSideline), - ), - ), - if (endSideline) Text(')', style: textStyle), - ], + ), ); } } class _MoveContextMenu extends ConsumerWidget { - const _MoveContextMenu( - this.pgn, - this.options, { + const _MoveContextMenu({ required this.title, required this.path, required this.parent, required this.branch, required this.isSideline, + required this.notifier, }); final String title; - final String pgn; - final AnalysisOptions options; final UciPath path; final ViewNode parent; final ViewBranch branch; final bool isSideline; + final AnalysisController Function() notifier; @override Widget build(BuildContext context, WidgetRef ref) { - final ctrlProvider = analysisControllerProvider(pgn, options); - return BottomSheetScrollableContainer( children: [ Padding( @@ -567,94 +894,47 @@ class _MoveContextMenu extends ConsumerWidget { BottomSheetContextMenuAction( icon: Icons.subtitles, child: Text(context.l10n.mobileShowVariations), - onPressed: () { - ref.read(ctrlProvider.notifier).showAllVariations(path); - }, + onPressed: () => notifier().showAllVariations(path), ), - if (isSideline) + if (isSideline) ...[ BottomSheetContextMenuAction( icon: Icons.subtitles_off, child: Text(context.l10n.mobileHideVariation), - onPressed: () { - ref.read(ctrlProvider.notifier).hideVariation(path); - }, + onPressed: () => notifier().hideVariation(path), ), - if (isSideline) BottomSheetContextMenuAction( icon: Icons.expand_less, child: Text(context.l10n.promoteVariation), - onPressed: () { - ref.read(ctrlProvider.notifier).promoteVariation(path, false); - }, + onPressed: () => notifier().promoteVariation(path, false), ), - if (isSideline) BottomSheetContextMenuAction( icon: Icons.check, child: Text(context.l10n.makeMainLine), - onPressed: () { - ref.read(ctrlProvider.notifier).promoteVariation(path, true); - }, + onPressed: () => notifier().promoteVariation(path, true), ), + ], BottomSheetContextMenuAction( icon: Icons.delete, child: Text(context.l10n.deleteFromHere), - onPressed: () { - ref.read(ctrlProvider.notifier).deleteFromHere(path); - }, + onPressed: () => notifier().deleteFromHere(path), ), ], ); } } -class _Comments extends StatelessWidget { - _Comments(this.comments, {this.isSideline = false}) - : assert(comments.any((c) => c.text?.isNotEmpty == true)); - - final Iterable comments; - final bool isSideline; - - @override - Widget build(BuildContext context) { - return AdaptiveInkWell( - onTap: () { - showAdaptiveBottomSheet( - context: context, - isDismissible: true, - showDragHandle: true, - isScrollControlled: true, - builder: (context) => BottomSheetScrollableContainer( - padding: const EdgeInsets.symmetric( - vertical: 8.0, - horizontal: 16.0, - ), - children: comments.map( - (comment) { - if (comment.text == null || comment.text!.isEmpty) { - return const SizedBox.shrink(); - } - return Padding( - padding: const EdgeInsets.only(bottom: 16.0), - child: Text(comment.text!.replaceAll('\n', ' ')), - ); - }, - ).toList(), +List comments( + IList comments, { + required TextStyle textStyle, +}) => + comments + .map( + (comment) => TextSpan( + text: comment.text, + style: textStyle.copyWith(fontSize: textStyle.fontSize! - 2.0), ), - ); - }, - child: Text( - comments.map((c) => c.text ?? '').join(' ').replaceAll('\n', ' '), - maxLines: 3, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontStyle: FontStyle.italic, - color: - isSideline ? _textColor(context, 0.6) : _textColor(context, 0.7), - ), - ), - ); - } -} + ) + .toList(); class _OpeningHeaderDelegate extends SliverPersistentHeaderDelegate { const _OpeningHeaderDelegate( diff --git a/test/view/analysis/analysis_screen_test.dart b/test/view/analysis/analysis_screen_test.dart index 4c68c8476a..16c94d7f36 100644 --- a/test/view/analysis/analysis_screen_test.dart +++ b/test/view/analysis/analysis_screen_test.dart @@ -47,10 +47,17 @@ void main() { expect(find.byType(Chessboard), findsOneWidget); expect(find.byType(PieceWidget), findsNWidgets(25)); - final currentMove = find.widgetWithText(InlineMove, 'Qe1#'); + final currentMove = find.textContaining('Qe1#'); expect(currentMove, findsOneWidget); expect( - tester.widgetList(currentMove).any((e) => e.isCurrentMove), + tester + .widget( + find.ancestor( + of: currentMove, + matching: find.byType(InlineMove), + ), + ) + .isCurrentMove, isTrue, ); }); @@ -92,10 +99,17 @@ void main() { await tester.tap(find.byKey(const Key('goto-previous'))); await tester.pumpAndSettle(); - final currentMove = find.widgetWithText(InlineMove, 'Kc1'); + final currentMove = find.textContaining('Kc1'); expect(currentMove, findsOneWidget); expect( - tester.widgetList(currentMove).any((e) => e.isCurrentMove), + tester + .widget( + find.ancestor( + of: currentMove, + matching: find.byType(InlineMove), + ), + ) + .isCurrentMove, isTrue, ); }); From 7b2ea21924eaca8130c347e73c949ae9ffc08628 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Mon, 30 Sep 2024 21:27:06 +0200 Subject: [PATCH 430/979] adapt tree view styling --- lib/src/view/analysis/tree_view.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/src/view/analysis/tree_view.dart b/lib/src/view/analysis/tree_view.dart index 70a7ebefdc..654a875f43 100644 --- a/lib/src/view/analysis/tree_view.dart +++ b/lib/src/view/analysis/tree_view.dart @@ -253,7 +253,6 @@ List _buildInlineSideLine({ required _PgnTreeViewParams params, }) { textStyle = textStyle.copyWith( - fontStyle: FontStyle.italic, fontSize: textStyle.fontSize != null ? textStyle.fontSize! - 2.0 : null, ); @@ -931,7 +930,10 @@ List comments( .map( (comment) => TextSpan( text: comment.text, - style: textStyle.copyWith(fontSize: textStyle.fontSize! - 2.0), + style: textStyle.copyWith( + fontSize: textStyle.fontSize! - 2.0, + fontStyle: FontStyle.italic, + ), ), ) .toList(); From 3c0ed7f193f0b66b6c3ae2aa0659784b37066123 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Mon, 30 Sep 2024 21:36:24 +0200 Subject: [PATCH 431/979] support hide/show variations again --- lib/src/view/analysis/tree_view.dart | 38 +++++++++++++++++++++------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/lib/src/view/analysis/tree_view.dart b/lib/src/view/analysis/tree_view.dart index 654a875f43..5c15ddeeda 100644 --- a/lib/src/view/analysis/tree_view.dart +++ b/lib/src/view/analysis/tree_view.dart @@ -222,15 +222,20 @@ class _PgnTreeView extends StatelessWidget { path = path + nodes.last.children.first.id; } + // Skip the first node which is the continuation of the mainline + final sidelineNodes = nodes.last.children.skip(1).whereNot( + (node) => node.isHidden, + ); + return [ _MainLinePart( params: params, initialPath: mainlineInitialPath, nodes: nodes, ), - if (nodes.last.children.length > 1) + if (sidelineNodes.isNotEmpty) _IndentedSideLines( - nodes.last.children.skip(1), + sidelineNodes, parent: nodes.last, params: params, initialPath: sidelineInitialPath, @@ -501,6 +506,9 @@ class _SideLines extends StatelessWidget { ...firstNode.mainline.takeWhile((node) => !_hasNonInlineSideLine(node)), ]; + final children = + sidelineNodes.last.children.whereNot((node) => node.isHidden); + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -511,9 +519,9 @@ class _SideLines extends StatelessWidget { initialPath: initialPath, params: params, ), - if (sidelineNodes.last.children.isNotEmpty) + if (children.isNotEmpty) _IndentedSideLines( - sidelineNodes.last.children, + children, parent: sidelineNodes.last, initialPath: UciPath.join( initialPath, @@ -594,11 +602,7 @@ class _IndentedSideLinesState extends State<_IndentedSideLines> { final GlobalKey _columnKey = GlobalKey(); - @override - void initState() { - super.initState(); - _keys = List.generate(widget.sideLines.length, (_) => GlobalKey()); - + void _redrawIndents() { WidgetsBinding.instance.addPostFrameCallback((_) { final RenderBox? columnBox = _columnKey.currentContext?.findRenderObject() as RenderBox?; @@ -619,8 +623,24 @@ class _IndentedSideLinesState extends State<_IndentedSideLines> { }); } + @override + void initState() { + super.initState(); + _redrawIndents(); + } + + @override + void didUpdateWidget(covariant _IndentedSideLines oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.sideLines != widget.sideLines) { + _redrawIndents(); + } + } + @override Widget build(BuildContext context) { + _keys = List.generate(widget.sideLines.length, (_) => GlobalKey()); + final sideLineWidgets = widget.sideLines .mapIndexed( (i, firstSidelineNode) => _SideLines( From b129c584764308647453755199f88a1578bde6ff Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Wed, 2 Oct 2024 22:14:56 +0200 Subject: [PATCH 432/979] fix unnecessary sideline nesting --- lib/src/view/analysis/tree_view.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/src/view/analysis/tree_view.dart b/lib/src/view/analysis/tree_view.dart index 5c15ddeeda..ff83677af9 100644 --- a/lib/src/view/analysis/tree_view.dart +++ b/lib/src/view/analysis/tree_view.dart @@ -500,11 +500,11 @@ class _SideLines extends StatelessWidget { @override Widget build(BuildContext context) { - final sidelineNodes = [ - firstNode, - if (!_hasNonInlineSideLine(firstNode)) - ...firstNode.mainline.takeWhile((node) => !_hasNonInlineSideLine(node)), - ]; + final sidelineNodes = [firstNode]; + while (sidelineNodes.last.children.isNotEmpty && + !_hasNonInlineSideLine(sidelineNodes.last)) { + sidelineNodes.add(sidelineNodes.last.children.first); + } final children = sidelineNodes.last.children.whereNot((node) => node.isHidden); From 6409115625fbd61e8e54a7d6d726e4301a14e9df Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Thu, 3 Oct 2024 15:40:15 +0200 Subject: [PATCH 433/979] add indentation limit --- lib/src/view/analysis/tree_view.dart | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/src/view/analysis/tree_view.dart b/lib/src/view/analysis/tree_view.dart index ff83677af9..c6b2a536ee 100644 --- a/lib/src/view/analysis/tree_view.dart +++ b/lib/src/view/analysis/tree_view.dart @@ -239,6 +239,7 @@ class _PgnTreeView extends StatelessWidget { parent: nodes.last, params: params, initialPath: sidelineInitialPath, + nesting: 1, ), ]; }, @@ -490,6 +491,7 @@ class _SideLines extends StatelessWidget { required this.firstMoveKey, required this.initialPath, required this.params, + required this.nesting, }); final ViewBranch firstNode; @@ -497,6 +499,7 @@ class _SideLines extends StatelessWidget { final GlobalKey firstMoveKey; final UciPath initialPath; final _PgnTreeViewParams params; + final int nesting; @override Widget build(BuildContext context) { @@ -528,6 +531,7 @@ class _SideLines extends StatelessWidget { UciPath.fromIds(sidelineNodes.map((node) => node.id)), ), params: params, + nesting: nesting + 1, ), ], ); @@ -581,6 +585,7 @@ class _IndentedSideLines extends StatefulWidget { required this.parent, required this.initialPath, required this.params, + required this.nesting, }); final Iterable sideLines; @@ -591,6 +596,8 @@ class _IndentedSideLines extends StatefulWidget { final _PgnTreeViewParams params; + final int nesting; + @override State<_IndentedSideLines> createState() => _IndentedSideLinesState(); } @@ -649,14 +656,15 @@ class _IndentedSideLinesState extends State<_IndentedSideLines> { firstMoveKey: _keys[i], initialPath: widget.initialPath, params: widget.params, + nesting: widget.nesting, ), ) .toList(); - const padding = 12.0; + final padding = widget.nesting < 6 ? 12.0 : 0.0; return RepaintBoundary( child: Padding( - padding: const EdgeInsets.only(left: padding), + padding: EdgeInsets.only(left: padding), child: CustomPaint( painter: _IndentPainter( sideLineStartPositions: _sideLineStartPositions, From a72f126dbd5ad66f626b33c5aae20546536cc6db Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Thu, 3 Oct 2024 16:03:58 +0200 Subject: [PATCH 434/979] hide sidelines with nesting > 2 when parsing PGN --- lib/src/model/common/node.dart | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/lib/src/model/common/node.dart b/lib/src/model/common/node.dart index dab74db865..334226fead 100644 --- a/lib/src/model/common/node.dart +++ b/lib/src/model/common/node.dart @@ -484,8 +484,8 @@ class Root extends Node { position: PgnGame.startingPosition(game.headers), ); - final List<({PgnNode from, Node to})> stack = [ - (from: game.moves, to: root), + final List<({PgnNode from, Node to, int nesting})> stack = [ + (from: game.moves, to: root, nesting: 1), ]; while (stack.isNotEmpty) { @@ -503,7 +503,7 @@ class Root extends Node { final branch = Branch( sanMove: SanMove(childFrom.data.san, move), position: newPos, - isHidden: hideVariations && childIdx > 0, + isHidden: frame.nesting > 2 || hideVariations && childIdx > 0, lichessAnalysisComments: isLichessAnalysis ? comments?.toList() : null, startingComments: isLichessAnalysis @@ -516,7 +516,17 @@ class Root extends Node { ); frame.to.addChild(branch); - stack.add((from: childFrom, to: branch)); + stack.add( + ( + from: childFrom, + to: branch, + nesting: frame.from.children.length == 1 || + // mainline continuation + (childIdx == 0 && frame.nesting == 1) + ? frame.nesting + : frame.nesting + 1, + ), + ); onVisitNode?.call(root, branch, isMainline); } From 17c5524008300aa3bb7c44a85300f4d931fb765c Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Thu, 3 Oct 2024 23:03:38 +0200 Subject: [PATCH 435/979] add icon to expand variations --- .../model/analysis/analysis_controller.dart | 20 +++++--- lib/src/model/common/node.dart | 16 ------- lib/src/view/analysis/tree_view.dart | 46 ++++++++++++------- 3 files changed, 44 insertions(+), 38 deletions(-) diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index 0341665c95..81a0a94711 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -265,16 +265,24 @@ class AnalysisController extends _$AnalysisController { _setPath(path); } - void showAllVariations(UciPath path) { - final parent = _root.parentAt(path); - for (final node in parent.children) { - node.isHidden = false; + void expandVariations(UciPath path) { + final node = _root.nodeAt(path); + for (final child in node.children) { + child.isHidden = false; + for (final grandChild in child.children) { + grandChild.isHidden = false; + } } state = state.copyWith(root: _root.view); } - void hideVariation(UciPath path) { - _root.hideVariationAt(path); + void collapseVariations(UciPath path) { + final node = _root.parentAt(path); + + for (final child in node.children) { + child.isHidden = true; + } + state = state.copyWith(root: _root.view); } diff --git a/lib/src/model/common/node.dart b/lib/src/model/common/node.dart index 334226fead..f60f812f13 100644 --- a/lib/src/model/common/node.dart +++ b/lib/src/model/common/node.dart @@ -234,22 +234,6 @@ abstract class Node { parentAt(path).children.removeWhere((child) => child.id == path.last); } - /// Hides the variation from the node at the given path. - void hideVariationAt(UciPath path) { - final nodes = nodesOn(path).toList(); - for (int i = nodes.length - 2; i >= 0; i--) { - final node = nodes[i + 1]; - final parent = nodes[i]; - if (node is Branch && parent.children.length > 1) { - for (final child in parent.children) { - if (child.id == node.id) { - child.isHidden = true; - } - } - } - } - } - /// Promotes the node at the given path. void promoteAt(UciPath path, {required bool toMainline}) { final nodes = nodesOn(path).toList(); diff --git a/lib/src/view/analysis/tree_view.dart b/lib/src/view/analysis/tree_view.dart index c6b2a536ee..e86149d591 100644 --- a/lib/src/view/analysis/tree_view.dart +++ b/lib/src/view/analysis/tree_view.dart @@ -223,9 +223,7 @@ class _PgnTreeView extends StatelessWidget { } // Skip the first node which is the continuation of the mainline - final sidelineNodes = nodes.last.children.skip(1).whereNot( - (node) => node.isHidden, - ); + final sidelineNodes = nodes.last.children.skip(1); return [ _MainLinePart( @@ -509,8 +507,7 @@ class _SideLines extends StatelessWidget { sidelineNodes.add(sidelineNodes.last.children.first); } - final children = - sidelineNodes.last.children.whereNot((node) => node.isHidden); + final children = sidelineNodes.last.children; return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -646,9 +643,16 @@ class _IndentedSideLinesState extends State<_IndentedSideLines> { @override Widget build(BuildContext context) { - _keys = List.generate(widget.sideLines.length, (_) => GlobalKey()); + final hasHiddenLines = widget.sideLines.any((node) => node.isHidden); - final sideLineWidgets = widget.sideLines + final visibleSideLines = widget.sideLines.whereNot((node) => node.isHidden); + + _keys = List.generate( + visibleSideLines.length + (hasHiddenLines ? 1 : 0), + (_) => GlobalKey(), + ); + + final sideLineWidgets = visibleSideLines .mapIndexed( (i, firstSidelineNode) => _SideLines( firstNode: firstSidelineNode, @@ -674,7 +678,23 @@ class _IndentedSideLinesState extends State<_IndentedSideLines> { child: Column( key: _columnKey, crossAxisAlignment: CrossAxisAlignment.start, - children: sideLineWidgets, + children: [ + ...sideLineWidgets, + if (hasHiddenLines) + GestureDetector( + child: Icon( + Icons.add_box, + color: _textColor(context, 0.6), + key: _keys.last, + size: _baseTextStyle.fontSize, + ), + onTap: () { + widget.params + .notifier() + .expandVariations(widget.initialPath); + }, + ), + ], ), ), ), @@ -917,17 +937,11 @@ class _MoveContextMenu extends ConsumerWidget { ), ), const PlatformDivider(indent: 0), - if (parent.children.any((c) => c.isHidden)) - BottomSheetContextMenuAction( - icon: Icons.subtitles, - child: Text(context.l10n.mobileShowVariations), - onPressed: () => notifier().showAllVariations(path), - ), if (isSideline) ...[ BottomSheetContextMenuAction( icon: Icons.subtitles_off, - child: Text(context.l10n.mobileHideVariation), - onPressed: () => notifier().hideVariation(path), + child: Text(context.l10n.collapseVariations), + onPressed: () => notifier().collapseVariations(path), ), BottomSheetContextMenuAction( icon: Icons.expand_less, From d3f4ce4dccb8a2dbf8d46f1d5557fa502c75bd18 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Fri, 4 Oct 2024 13:16:41 +0200 Subject: [PATCH 436/979] increase icon size for expanding variations --- lib/src/view/analysis/tree_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/view/analysis/tree_view.dart b/lib/src/view/analysis/tree_view.dart index e86149d591..01fa29685a 100644 --- a/lib/src/view/analysis/tree_view.dart +++ b/lib/src/view/analysis/tree_view.dart @@ -686,7 +686,7 @@ class _IndentedSideLinesState extends State<_IndentedSideLines> { Icons.add_box, color: _textColor(context, 0.6), key: _keys.last, - size: _baseTextStyle.fontSize, + size: _baseTextStyle.fontSize! + 5, ), onTap: () { widget.params From 3e3dbb54ca6d228d01558f02d031557239b846df Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Sat, 5 Oct 2024 09:58:59 +0200 Subject: [PATCH 437/979] fix comment --- lib/src/view/analysis/tree_view.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/view/analysis/tree_view.dart b/lib/src/view/analysis/tree_view.dart index 01fa29685a..808d4238d0 100644 --- a/lib/src/view/analysis/tree_view.dart +++ b/lib/src/view/analysis/tree_view.dart @@ -148,8 +148,8 @@ class _InlineTreeViewState extends ConsumerState { } } -// A group of parameters that are passed through various parts of the tree view -// and ultimately evaluated in the InlineMove widget. Grouped in this record to improve readability. +/// A group of parameters that are passed through various parts of the tree view +/// and ultimately evaluated in the InlineMove widget. Grouped in this record to improve readability. typedef _PgnTreeViewParams = ({ UciPath currentPath, bool shouldShowAnnotations, From 2afbc6b7e0c699ee72b478147a4222a60815d69e Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Sat, 5 Oct 2024 10:05:51 +0200 Subject: [PATCH 438/979] make LineType and LineInfo private and document them --- lib/src/view/analysis/tree_view.dart | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/lib/src/view/analysis/tree_view.dart b/lib/src/view/analysis/tree_view.dart index 808d4238d0..d87ccc16f5 100644 --- a/lib/src/view/analysis/tree_view.dart +++ b/lib/src/view/analysis/tree_view.dart @@ -282,7 +282,7 @@ List _buildInlineSideLine({ node, parent: i == 0 ? parent : sidelineNodes[i - 1], lineInfo: ( - type: LineType.inlineSideline, + type: _LineType.inlineSideline, startLine: i == 0 || sidelineNodes[i - 1].hasTextComment ), pathToNode: pathToNode, @@ -306,19 +306,27 @@ const _baseTextStyle = TextStyle( height: 1.5, ); -enum LineType { +/// The different types of lines (move sequences) that are displayed in the tree view. +enum _LineType { + /// (A part of) the game's main line. mainline, + + /// A sideline branching off the main line or a parent sideline. + /// Each sideline is rendered on a new line and indented. sideline, + + /// A short sideline without any branching, displayed in parantheses inline with it's parent line. inlineSideline, } -typedef LineInfo = ({LineType type, bool startLine}); +/// Metadata about a move's role in the tree view. +typedef _LineInfo = ({_LineType type, bool startLine}); List moveWithComment( ViewBranch branch, { required ViewNode parent, required TextStyle textStyle, - required LineInfo lineInfo, + required _LineInfo lineInfo, required UciPath pathToNode, required _PgnTreeViewParams params, GlobalKey? moveKey, @@ -373,7 +381,7 @@ class _SideLinePart extends ConsumerWidget { nodes.first, parent: parent, lineInfo: ( - type: LineType.sideline, + type: _LineType.sideline, startLine: true, ), moveKey: firstMoveKey, @@ -388,7 +396,7 @@ class _SideLinePart extends ConsumerWidget { node.children.first, parent: node, lineInfo: ( - type: LineType.sideline, + type: _LineType.sideline, startLine: node.hasTextComment, ), pathToNode: path, @@ -452,7 +460,7 @@ class _MainLinePart extends ConsumerWidget { mainlineNode, parent: node, lineInfo: ( - type: LineType.mainline, + type: _LineType.mainline, startLine: i == 0 || (node as ViewBranch).hasTextComment, ), pathToNode: path, @@ -735,7 +743,7 @@ class InlineMove extends ConsumerWidget { final TextStyle textStyle; - final LineInfo lineInfo; + final _LineInfo lineInfo; final _PgnTreeViewParams params; @@ -756,7 +764,7 @@ class InlineMove extends ConsumerWidget { fontFamily: moveFontFamily, fontWeight: isCurrentMove ? FontWeight.bold - : lineInfo.type == LineType.inlineSideline + : lineInfo.type == _LineType.inlineSideline ? FontWeight.normal : FontWeight.w600, ); @@ -803,7 +811,7 @@ class InlineMove extends ConsumerWidget { path: path, parent: parent, branch: branch, - isSideline: lineInfo.type != LineType.mainline, + isSideline: lineInfo.type != _LineType.mainline, ), ); }, From c17d012e8dc8a9d18f25817cee0146e80991c762 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Sat, 5 Oct 2024 10:07:51 +0200 Subject: [PATCH 439/979] make more stuff private --- lib/src/view/analysis/tree_view.dart | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/src/view/analysis/tree_view.dart b/lib/src/view/analysis/tree_view.dart index d87ccc16f5..b85acba92b 100644 --- a/lib/src/view/analysis/tree_view.dart +++ b/lib/src/view/analysis/tree_view.dart @@ -203,7 +203,7 @@ class _PgnTreeView extends StatelessWidget { rootComments?.any((c) => c.text?.isNotEmpty == true) == true) Text.rich( TextSpan( - children: comments(rootComments!, textStyle: _baseTextStyle), + children: _comments(rootComments!, textStyle: _baseTextStyle), ), ), ..._mainlineParts(root).map( @@ -278,7 +278,7 @@ List _buildInlineSideLine({ style: textStyle, ), ], - ...moveWithComment( + ..._moveWithComment( node, parent: i == 0 ? parent : sidelineNodes[i - 1], lineInfo: ( @@ -322,7 +322,7 @@ enum _LineType { /// Metadata about a move's role in the tree view. typedef _LineInfo = ({_LineType type, bool startLine}); -List moveWithComment( +List _moveWithComment( ViewBranch branch, { required ViewNode parent, required TextStyle textStyle, @@ -345,7 +345,7 @@ List moveWithComment( ), ), if (params.shouldShowComments && branch.hasTextComment) - ...comments(branch.comments!, textStyle: textStyle), + ..._comments(branch.comments!, textStyle: textStyle), ]; } @@ -377,7 +377,7 @@ class _SideLinePart extends ConsumerWidget { var path = initialPath + nodes.first.id; final moves = [ - ...moveWithComment( + ..._moveWithComment( nodes.first, parent: parent, lineInfo: ( @@ -392,7 +392,7 @@ class _SideLinePart extends ConsumerWidget { ...nodes.take(nodes.length - 1).map( (node) { final moves = [ - ...moveWithComment( + ..._moveWithComment( node.children.first, parent: node, lineInfo: ( @@ -456,7 +456,7 @@ class _MainLinePart extends ConsumerWidget { (i, node) { final mainlineNode = node.children.first; final moves = [ - moveWithComment( + _moveWithComment( mainlineNode, parent: node, lineInfo: ( @@ -972,7 +972,7 @@ class _MoveContextMenu extends ConsumerWidget { } } -List comments( +List _comments( IList comments, { required TextStyle textStyle, }) => From cca00b99d9c8013b1670315370cce66f6eb3aa60 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Sat, 5 Oct 2024 10:17:14 +0200 Subject: [PATCH 440/979] remove side effects from build() --- lib/src/view/analysis/tree_view.dart | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/src/view/analysis/tree_view.dart b/lib/src/view/analysis/tree_view.dart index b85acba92b..4661fc59d2 100644 --- a/lib/src/view/analysis/tree_view.dart +++ b/lib/src/view/analysis/tree_view.dart @@ -615,6 +615,10 @@ class _IndentedSideLinesState extends State<_IndentedSideLines> { final GlobalKey _columnKey = GlobalKey(); void _redrawIndents() { + _keys = List.generate( + _visibleSideLines().length + (_hasHiddenLines() ? 1 : 0), + (_) => GlobalKey(), + ); WidgetsBinding.instance.addPostFrameCallback((_) { final RenderBox? columnBox = _columnKey.currentContext?.findRenderObject() as RenderBox?; @@ -635,6 +639,11 @@ class _IndentedSideLinesState extends State<_IndentedSideLines> { }); } + bool _hasHiddenLines() => widget.sideLines.any((node) => node.isHidden); + + Iterable _visibleSideLines() => + widget.sideLines.whereNot((node) => node.isHidden); + @override void initState() { super.initState(); @@ -651,16 +660,7 @@ class _IndentedSideLinesState extends State<_IndentedSideLines> { @override Widget build(BuildContext context) { - final hasHiddenLines = widget.sideLines.any((node) => node.isHidden); - - final visibleSideLines = widget.sideLines.whereNot((node) => node.isHidden); - - _keys = List.generate( - visibleSideLines.length + (hasHiddenLines ? 1 : 0), - (_) => GlobalKey(), - ); - - final sideLineWidgets = visibleSideLines + final sideLineWidgets = _visibleSideLines() .mapIndexed( (i, firstSidelineNode) => _SideLines( firstNode: firstSidelineNode, @@ -688,7 +688,7 @@ class _IndentedSideLinesState extends State<_IndentedSideLines> { crossAxisAlignment: CrossAxisAlignment.start, children: [ ...sideLineWidgets, - if (hasHiddenLines) + if (_hasHiddenLines()) GestureDetector( child: Icon( Icons.add_box, From 2e3e49f1157d5d94c9814de8e192bae162b82c08 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Sat, 5 Oct 2024 10:21:12 +0200 Subject: [PATCH 441/979] add comment about RepaintBoundary --- lib/src/view/analysis/tree_view.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/src/view/analysis/tree_view.dart b/lib/src/view/analysis/tree_view.dart index 4661fc59d2..fa7d2437f8 100644 --- a/lib/src/view/analysis/tree_view.dart +++ b/lib/src/view/analysis/tree_view.dart @@ -674,6 +674,8 @@ class _IndentedSideLinesState extends State<_IndentedSideLines> { .toList(); final padding = widget.nesting < 6 ? 12.0 : 0.0; + + // Without the RepaintBoundary, the CustomPaint would continuously repaint when the view is scrolled return RepaintBoundary( child: Padding( padding: EdgeInsets.only(left: padding), From 872b2d56b61249a183ed36ba8b72b3fcca040b92 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Sat, 5 Oct 2024 10:22:25 +0200 Subject: [PATCH 442/979] remove redundant Function indirection --- lib/src/view/analysis/tree_view.dart | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/lib/src/view/analysis/tree_view.dart b/lib/src/view/analysis/tree_view.dart index fa7d2437f8..a4b9edad67 100644 --- a/lib/src/view/analysis/tree_view.dart +++ b/lib/src/view/analysis/tree_view.dart @@ -139,7 +139,7 @@ class _InlineTreeViewState extends ConsumerState { shouldShowComments: shouldShowComments, currentMoveKey: currentMoveKey, currentPath: currentPath, - notifier: () => ref.read(ctrlProvider.notifier), + notifier: ref.read(ctrlProvider.notifier), ), ), ), @@ -155,7 +155,7 @@ typedef _PgnTreeViewParams = ({ bool shouldShowAnnotations, bool shouldShowComments, GlobalKey currentMoveKey, - AnalysisController Function() notifier, + AnalysisController notifier, }); /// True if the side line has no branching and is less than 6 moves deep. @@ -699,9 +699,7 @@ class _IndentedSideLinesState extends State<_IndentedSideLines> { size: _baseTextStyle.fontSize! + 5, ), onTap: () { - widget.params - .notifier() - .expandVariations(widget.initialPath); + widget.params.notifier.expandVariations(widget.initialPath); }, ), ], @@ -798,7 +796,7 @@ class InlineMove extends ConsumerWidget { return AdaptiveInkWell( key: isCurrentMove ? params.currentMoveKey : null, borderRadius: borderRadius, - onTap: () => params.notifier().userJump(path), + onTap: () => params.notifier.userJump(path), onLongPress: () { showAdaptiveBottomSheet( context: context, @@ -868,7 +866,7 @@ class _MoveContextMenu extends ConsumerWidget { final ViewNode parent; final ViewBranch branch; final bool isSideline; - final AnalysisController Function() notifier; + final AnalysisController notifier; @override Widget build(BuildContext context, WidgetRef ref) { @@ -951,23 +949,23 @@ class _MoveContextMenu extends ConsumerWidget { BottomSheetContextMenuAction( icon: Icons.subtitles_off, child: Text(context.l10n.collapseVariations), - onPressed: () => notifier().collapseVariations(path), + onPressed: () => notifier.collapseVariations(path), ), BottomSheetContextMenuAction( icon: Icons.expand_less, child: Text(context.l10n.promoteVariation), - onPressed: () => notifier().promoteVariation(path, false), + onPressed: () => notifier.promoteVariation(path, false), ), BottomSheetContextMenuAction( icon: Icons.check, child: Text(context.l10n.makeMainLine), - onPressed: () => notifier().promoteVariation(path, true), + onPressed: () => notifier.promoteVariation(path, true), ), ], BottomSheetContextMenuAction( icon: Icons.delete, child: Text(context.l10n.deleteFromHere), - onPressed: () => notifier().deleteFromHere(path), + onPressed: () => notifier.deleteFromHere(path), ), ], ); From 6995bdad6c082c4dc47041380090fa05002d2fb1 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Sat, 5 Oct 2024 12:57:49 +0200 Subject: [PATCH 443/979] only rebuild toplevel parts of the tree that actually changed --- lib/src/view/analysis/tree_view.dart | 148 +++++++++++++++++++-------- 1 file changed, 106 insertions(+), 42 deletions(-) diff --git a/lib/src/view/analysis/tree_view.dart b/lib/src/view/analysis/tree_view.dart index a4b9edad67..dbe839801b 100644 --- a/lib/src/view/analysis/tree_view.dart +++ b/lib/src/view/analysis/tree_view.dart @@ -173,7 +173,7 @@ bool _hasNonInlineSideLine(ViewNode node) => Iterable> _mainlineParts(ViewRoot root) => [root, ...root.mainline].splitAfter(_hasNonInlineSideLine); -class _PgnTreeView extends StatelessWidget { +class _PgnTreeView extends StatefulWidget { const _PgnTreeView({ required this.root, required this.rootComments, @@ -187,61 +187,125 @@ class _PgnTreeView extends StatelessWidget { final _PgnTreeViewParams params; @override - Widget build(BuildContext context) { - var path = UciPath.empty; + State<_PgnTreeView> createState() => _PgnTreeViewState(); +} - return Padding( - padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 10), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // trick to make auto-scroll work when returning to the root position - if (params.currentPath.isEmpty) - SizedBox.shrink(key: params.currentMoveKey), +typedef _TreeViewPart = ({ + List lines, + bool containsCurrentMove, +}); - if (params.shouldShowComments && - rootComments?.any((c) => c.text?.isNotEmpty == true) == true) - Text.rich( - TextSpan( - children: _comments(rootComments!, textStyle: _baseTextStyle), - ), - ), - ..._mainlineParts(root).map( - (nodes) { - final mainlineInitialPath = path; - - final sidelineInitialPath = UciPath.join( - path, - UciPath.fromIds( - nodes.take(nodes.length - 1).map((n) => n.children.first.id), - ), - ); +class _PgnTreeViewState extends State<_PgnTreeView> { + List> mainlineParts = []; + List<_TreeViewPart> treeParts = []; - path = sidelineInitialPath; - if (nodes.last.children.isNotEmpty) { - path = path + nodes.last.children.first.id; - } + UciPath _mainlinePartOfCurrentPath() { + var path = UciPath.empty; + for (final node in widget.root.mainline) { + if (!widget.params.currentPath.contains(path + node.id)) { + break; + } + path = path + node.id; + } + return path; + } - // Skip the first node which is the continuation of the mainline - final sidelineNodes = nodes.last.children.skip(1); + void _updateLines({required bool fullRebuild}) { + setState(() { + if (fullRebuild) { + mainlineParts = _mainlineParts(widget.root).toList(); + } + + var path = UciPath.empty; + + treeParts = mainlineParts.mapIndexed( + (i, mainlineNodes) { + final mainlineInitialPath = path; - return [ + final sidelineInitialPath = UciPath.join( + path, + UciPath.fromIds( + mainlineNodes + .take(mainlineNodes.length - 1) + .map((n) => n.children.first.id), + ), + ); + + path = sidelineInitialPath; + if (mainlineNodes.last.children.isNotEmpty) { + path = path + mainlineNodes.last.children.first.id; + } + + final mainlinePartOfCurrentPath = _mainlinePartOfCurrentPath(); + final containsCurrentMove = + mainlinePartOfCurrentPath.size > mainlineInitialPath.size && + mainlinePartOfCurrentPath.size <= path.size; + + if (fullRebuild || + treeParts[i].containsCurrentMove || + containsCurrentMove) { + // Skip the first node which is the continuation of the mainline + final sidelineNodes = mainlineNodes.last.children.skip(1); + return ( + lines: [ _MainLinePart( - params: params, + params: widget.params, initialPath: mainlineInitialPath, - nodes: nodes, + nodes: mainlineNodes, ), if (sidelineNodes.isNotEmpty) _IndentedSideLines( sidelineNodes, - parent: nodes.last, - params: params, + parent: mainlineNodes.last, + params: widget.params, initialPath: sidelineInitialPath, nesting: 1, ), - ]; - }, - ).flattened, + ], + containsCurrentMove: containsCurrentMove, + ); + } else { + // Avoid expensive rebuilds by caching parts of the tree that did not change across a path change + return treeParts[i]; + } + }, + ).toList(); + }); + } + + @override + void initState() { + super.initState(); + _updateLines(fullRebuild: true); + } + + @override + void didUpdateWidget(covariant _PgnTreeView oldWidget) { + super.didUpdateWidget(oldWidget); + _updateLines(fullRebuild: oldWidget.root != widget.root); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // trick to make auto-scroll work when returning to the root position + if (widget.params.currentPath.isEmpty) + SizedBox.shrink(key: widget.params.currentMoveKey), + + if (widget.params.shouldShowComments && + widget.rootComments?.any((c) => c.text?.isNotEmpty == true) == + true) + Text.rich( + TextSpan( + children: + _comments(widget.rootComments!, textStyle: _baseTextStyle), + ), + ), + ...treeParts.map((part) => part.lines).flattened, ], ), ); From 780830df5a6555484b9e419134491ea8f45c45bd Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Sat, 5 Oct 2024 13:42:59 +0200 Subject: [PATCH 444/979] skip last mainline part if it has no children --- lib/src/view/analysis/tree_view.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/src/view/analysis/tree_view.dart b/lib/src/view/analysis/tree_view.dart index dbe839801b..d051d32f40 100644 --- a/lib/src/view/analysis/tree_view.dart +++ b/lib/src/view/analysis/tree_view.dart @@ -171,7 +171,9 @@ bool _hasNonInlineSideLine(ViewNode node) => (node.children.length == 2 && !_displaySideLineAsInline(node.children[1])); Iterable> _mainlineParts(ViewRoot root) => - [root, ...root.mainline].splitAfter(_hasNonInlineSideLine); + [root, ...root.mainline] + .splitAfter(_hasNonInlineSideLine) + .takeWhile((nodes) => nodes.firstOrNull?.children.isNotEmpty == true); class _PgnTreeView extends StatefulWidget { const _PgnTreeView({ From b7aa1c9f41c34305686e278976f021d11ce94498 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Sat, 5 Oct 2024 22:25:32 +0200 Subject: [PATCH 445/979] add more tests for pgn tree view --- lib/src/view/analysis/tree_view.dart | 5 +- test/view/analysis/analysis_screen_test.dart | 142 ++++++++++++++++++- 2 files changed, 143 insertions(+), 4 deletions(-) diff --git a/lib/src/view/analysis/tree_view.dart b/lib/src/view/analysis/tree_view.dart index d051d32f40..ef28e01e43 100644 --- a/lib/src/view/analysis/tree_view.dart +++ b/lib/src/view/analysis/tree_view.dart @@ -841,12 +841,12 @@ class InlineMove extends ConsumerWidget { final indexText = branch.position.ply.isOdd ? TextSpan( - text: '${(branch.position.ply / 2).ceil()}.', + text: '${(branch.position.ply / 2).ceil()}. ', style: indexTextStyle, ) : (lineInfo.startLine ? TextSpan( - text: '${(branch.position.ply / 2).ceil()}…', + text: '${(branch.position.ply / 2).ceil()}… ', style: indexTextStyle, ) : null); @@ -897,7 +897,6 @@ class InlineMove extends ConsumerWidget { children: [ if (indexText != null) ...[ indexText, - const WidgetSpan(child: SizedBox(width: 3)), ], TextSpan( text: moveWithNag, diff --git a/test/view/analysis/analysis_screen_test.dart b/test/view/analysis/analysis_screen_test.dart index 16c94d7f36..9ac5053e70 100644 --- a/test/view/analysis/analysis_screen_test.dart +++ b/test/view/analysis/analysis_screen_test.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import 'package:chessground/chessground.dart'; import 'package:dartchess/dartchess.dart'; -import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; @@ -113,6 +113,146 @@ void main() { isTrue, ); }); + + group('Analysis Tree View', () { + Future buildTree( + WidgetTester tester, + String pgn, + ) async { + final app = await makeTestProviderScopeApp( + tester, + home: AnalysisScreen( + pgnOrId: pgn, + options: const AnalysisOptions( + isLocalEvaluationAllowed: false, + variant: Variant.standard, + orientation: Side.white, + opening: opening, + id: standaloneAnalysisId, + ), + ), + ); + + await tester.pumpWidget(app); + } + + Text parentText(WidgetTester tester, String move) { + return tester.widget( + find.ancestor( + of: find.text(move), + matching: find.byType(Text), + ), + ); + } + + void expectSameLine(WidgetTester tester, Iterable moves) { + final line = parentText(tester, moves.first); + + for (final move in moves.skip(1)) { + final moveText = find.text(move); + expect(moveText, findsOneWidget); + expect( + parentText(tester, move), + line, + ); + } + } + + void expectDifferentLines( + WidgetTester tester, + List moves, + ) { + for (int i = 0; i < moves.length; i++) { + for (int j = i + 1; j < moves.length; j++) { + expect( + parentText(tester, moves[i]), + isNot(parentText(tester, moves[j])), + ); + } + } + } + + testWidgets('displays short sideline as inline', (tester) async { + await buildTree(tester, '1. e4 e5 (1... d5 2. exd5) 2. Nf3 *'); + + final mainline = find.ancestor( + of: find.text('1. e4'), + matching: find.byType(Text), + ); + expect(mainline, findsOneWidget); + + expectSameLine(tester, ['1. e4', 'e5', '1… d5', '2. exd5', '2. Nf3']); + }); + + testWidgets('displays long sideline on its own line', (tester) async { + await buildTree( + tester, + '1. e4 e5 (1... d5 2. exd5 Qxd5 3. Nc3 Qd8 4. d4 Nf6) Nc3 *', + ); + + expectSameLine(tester, ['1. e4', 'e5']); + expectSameLine( + tester, + ['1… d5', '2. exd5', 'Qxd5', '3. Nc3', 'Qd8', '4. d4', 'Nf6'], + ); + expectSameLine(tester, ['2. Nc3']); + + expectDifferentLines(tester, ['1. e4', '1… d5', '2. Nc3']); + }); + + testWidgets('displays sideline with branching on its own line', + (tester) async { + await buildTree(tester, '1. e4 e5 (1... d5 2. exd5 (2. Nc3)) *'); + + expectSameLine(tester, ['1. e4', 'e5']); + + // 2nd branch is rendered inline again + expectSameLine(tester, ['1… d5', '2. exd5', '2. Nc3']); + + expectDifferentLines(tester, ['1. e4', '1… d5']); + }); + + testWidgets('multiple sidelines', (tester) async { + await buildTree( + tester, + '1. e4 e5 (1... d5 2. exd5) (1... Nf6 2. e5) 2. Nf3 Nc6 (2... a5) *', + ); + + expectSameLine(tester, ['1. e4', 'e5']); + expectSameLine(tester, ['1… d5', '2. exd5']); + expectSameLine(tester, ['1… Nf6', '2. e5']); + expectSameLine(tester, ['2. Nf3', 'Nc6', '2… a5']); + + expectDifferentLines(tester, ['1. e4', '1… d5', '1… Nf6', '2. Nf3']); + }); + + testWidgets('collapses lines with nesting > 2', (tester) async { + await buildTree( + tester, + '1. e4 e5 (1... d5 2. Nc3 (2. h4 h5 (2... Nc6 3. d3) (2... Qd7))) *', + ); + + expectSameLine(tester, ['1. e4', 'e5']); + expectSameLine(tester, ['1… d5']); + expectSameLine(tester, ['2. Nc3']); + expectSameLine(tester, ['2. h4']); + + expect(find.text('2… h5'), findsNothing); + expect(find.text('2… Nc6'), findsNothing); + expect(find.text('3. d3'), findsNothing); + expect(find.text('2… Qd7'), findsNothing); + + // sidelines with nesting > 2 are collapsed -> expand them + expect(find.byIcon(Icons.add_box), findsOneWidget); + + await tester.tap(find.byIcon(Icons.add_box)); + await tester.pumpAndSettle(); + + expectSameLine(tester, ['2… h5']); + expectSameLine(tester, ['2… Nc6', '3. d3']); + expectSameLine(tester, ['2… Qd7']); + }); + }); }); } From 9c9d16ff717083217d9201cdfe2edfb4233e1138 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Sun, 6 Oct 2024 15:58:29 +0200 Subject: [PATCH 446/979] remove RepaintBoundary --- lib/src/view/analysis/tree_view.dart | 53 +++++++++++++--------------- 1 file changed, 25 insertions(+), 28 deletions(-) diff --git a/lib/src/view/analysis/tree_view.dart b/lib/src/view/analysis/tree_view.dart index ef28e01e43..20ab7ea37e 100644 --- a/lib/src/view/analysis/tree_view.dart +++ b/lib/src/view/analysis/tree_view.dart @@ -741,35 +741,32 @@ class _IndentedSideLinesState extends State<_IndentedSideLines> { final padding = widget.nesting < 6 ? 12.0 : 0.0; - // Without the RepaintBoundary, the CustomPaint would continuously repaint when the view is scrolled - return RepaintBoundary( - child: Padding( - padding: EdgeInsets.only(left: padding), - child: CustomPaint( - painter: _IndentPainter( - sideLineStartPositions: _sideLineStartPositions, - color: _textColor(context, 0.6)!, - padding: padding, - ), - child: Column( - key: _columnKey, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ...sideLineWidgets, - if (_hasHiddenLines()) - GestureDetector( - child: Icon( - Icons.add_box, - color: _textColor(context, 0.6), - key: _keys.last, - size: _baseTextStyle.fontSize! + 5, - ), - onTap: () { - widget.params.notifier.expandVariations(widget.initialPath); - }, + return Padding( + padding: EdgeInsets.only(left: padding), + child: CustomPaint( + painter: _IndentPainter( + sideLineStartPositions: _sideLineStartPositions, + color: _textColor(context, 0.6)!, + padding: padding, + ), + child: Column( + key: _columnKey, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...sideLineWidgets, + if (_hasHiddenLines()) + GestureDetector( + child: Icon( + Icons.add_box, + color: _textColor(context, 0.6), + key: _keys.last, + size: _baseTextStyle.fontSize! + 5, ), - ], - ), + onTap: () { + widget.params.notifier.expandVariations(widget.initialPath); + }, + ), + ], ), ), ); From 8e5f8ab8571c84f3137e51b1d68eb8aba59ec187 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Sun, 6 Oct 2024 15:31:32 +0200 Subject: [PATCH 447/979] refactor and improve documentation --- lib/src/view/analysis/tree_view.dart | 104 +++++++++++++++++++-------- 1 file changed, 73 insertions(+), 31 deletions(-) diff --git a/lib/src/view/analysis/tree_view.dart b/lib/src/view/analysis/tree_view.dart index 20ab7ea37e..5e70b51fbe 100644 --- a/lib/src/view/analysis/tree_view.dart +++ b/lib/src/view/analysis/tree_view.dart @@ -44,12 +44,12 @@ class AnalysisTreeView extends ConsumerStatefulWidget { class _InlineTreeViewState extends ConsumerState { final currentMoveKey = GlobalKey(); final _debounce = Debouncer(kFastReplayDebounceDelay); - late UciPath currentPath; + late UciPath pathToCurrentMove; @override void initState() { super.initState(); - currentPath = ref.read( + pathToCurrentMove = ref.read( analysisControllerProvider(widget.pgn, widget.options).select( (value) => value.currentPath, ), @@ -88,7 +88,7 @@ class _InlineTreeViewState extends ConsumerState { // the fast replay buttons _debounce(() { setState(() { - currentPath = state.currentPath; + pathToCurrentMove = state.currentPath; }); WidgetsBinding.instance.addPostFrameCallback((_) { if (currentMoveKey.currentContext != null) { @@ -138,7 +138,7 @@ class _InlineTreeViewState extends ConsumerState { shouldShowAnnotations: shouldShowAnnotations, shouldShowComments: shouldShowComments, currentMoveKey: currentMoveKey, - currentPath: currentPath, + pathToCurrentMove: pathToCurrentMove, notifier: ref.read(ctrlProvider.notifier), ), ), @@ -151,14 +151,25 @@ class _InlineTreeViewState extends ConsumerState { /// A group of parameters that are passed through various parts of the tree view /// and ultimately evaluated in the InlineMove widget. Grouped in this record to improve readability. typedef _PgnTreeViewParams = ({ - UciPath currentPath, + /// Path to the currently selected move in the tree. + UciPath pathToCurrentMove, + + /// Whether to show NAG annotations like '!' and '??'. bool shouldShowAnnotations, + + /// Whether to show comments associated with the moves. bool shouldShowComments, + + /// Key that will we assigned to the widget corresponding to [pathToCurrentMove]. + /// Can be used e.g. to ensure that the current move is visible on the screen. GlobalKey currentMoveKey, + + /// Callbacks for when the user interacts with the tree view, e.g. selecting a different move. AnalysisController notifier, }); -/// True if the side line has no branching and is less than 6 moves deep. +/// Sidelines are usually rendered on a new line and indented. +/// However sidelines are rendered inline (in parantheses) if the side line has no branching and is less than 6 moves deep. bool _displaySideLineAsInline(ViewBranch node, [int depth = 0]) { if (depth == 6) return false; if (node.children.isEmpty) return true; @@ -166,15 +177,29 @@ bool _displaySideLineAsInline(ViewBranch node, [int depth = 0]) { return _displaySideLineAsInline(node.children.first, depth + 1); } +/// Returns whether this node has a sideline that should not be displayed inline. bool _hasNonInlineSideLine(ViewNode node) => node.children.length > 2 || (node.children.length == 2 && !_displaySideLineAsInline(node.children[1])); +/// Splits the mainline into parts, where each part is a sequence of moves that are displayed on the same line. +/// A part ends when a mainline node has a sideline that should not be displayed inline. Iterable> _mainlineParts(ViewRoot root) => [root, ...root.mainline] .splitAfter(_hasNonInlineSideLine) .takeWhile((nodes) => nodes.firstOrNull?.children.isNotEmpty == true); +/// Displays a tree-like view of a PGN game's moves. +/// +/// For example, the PGN 1. e4 e5 (1... d5) (1... Nc6) 2. Nf3 Nc6 (2... a5) 3. Bc4 * will be displayed as: +/// 1. e4 e5 // [_MainLinePart] +/// |- 1... d5 // [_SideLinePart] +/// |- 1... Nc6 // [_SideLinePart] +/// 2. Nf3 Nc6 (2... a5) 3. Bc4 // [_MainLinePart], with inline sideline +/// Short sidelines without any branching are displayed inline with their parent line. +/// Longer sidelines are displayed on a new line and indented. +/// The mainline is split into parts whenever a move has a non-inline sideline, this corresponds to the [_MainLinePart] widget. +/// Similarly, a [_SideLinePart] contains the moves sequence of a sideline where each node has only one child. class _PgnTreeView extends StatefulWidget { const _PgnTreeView({ required this.root, @@ -182,8 +207,10 @@ class _PgnTreeView extends StatefulWidget { required this.params, }); + /// Root of the PGN tree final ViewRoot root; + /// Comments associated with the root node final IList? rootComments; final _PgnTreeViewParams params; @@ -192,19 +219,26 @@ class _PgnTreeView extends StatefulWidget { State<_PgnTreeView> createState() => _PgnTreeViewState(); } -typedef _TreeViewPart = ({ - List lines, +typedef _Subtree = ({ + _MainLinePart mainLinePart, + + /// This is nullable since the very last mainline part might not have any sidelines. + _IndentedSideLines? sidelines, bool containsCurrentMove, }); class _PgnTreeViewState extends State<_PgnTreeView> { + /// Caches the result of [_mainlineParts] to avoid recalculating it on every rebuild. List> mainlineParts = []; - List<_TreeViewPart> treeParts = []; + + /// Caches the top-level subtrees, where each subtree is a [_MainLinePart] and its sidelines. + /// Building the whole tree is expensive, so we cache the subtrees that did not change when the current move changes. + List<_Subtree> subtrees = []; UciPath _mainlinePartOfCurrentPath() { var path = UciPath.empty; for (final node in widget.root.mainline) { - if (!widget.params.currentPath.contains(path + node.id)) { + if (!widget.params.pathToCurrentMove.contains(path + node.id)) { break; } path = path + node.id; @@ -220,7 +254,7 @@ class _PgnTreeViewState extends State<_PgnTreeView> { var path = UciPath.empty; - treeParts = mainlineParts.mapIndexed( + subtrees = mainlineParts.mapIndexed( (i, mainlineNodes) { final mainlineInitialPath = path; @@ -244,31 +278,30 @@ class _PgnTreeViewState extends State<_PgnTreeView> { mainlinePartOfCurrentPath.size <= path.size; if (fullRebuild || - treeParts[i].containsCurrentMove || + subtrees[i].containsCurrentMove || containsCurrentMove) { // Skip the first node which is the continuation of the mainline final sidelineNodes = mainlineNodes.last.children.skip(1); return ( - lines: [ - _MainLinePart( - params: widget.params, - initialPath: mainlineInitialPath, - nodes: mainlineNodes, - ), - if (sidelineNodes.isNotEmpty) - _IndentedSideLines( - sidelineNodes, - parent: mainlineNodes.last, - params: widget.params, - initialPath: sidelineInitialPath, - nesting: 1, - ), - ], + mainLinePart: _MainLinePart( + params: widget.params, + initialPath: mainlineInitialPath, + nodes: mainlineNodes, + ), + sidelines: sidelineNodes.isNotEmpty + ? _IndentedSideLines( + sidelineNodes, + parent: mainlineNodes.last, + params: widget.params, + initialPath: sidelineInitialPath, + nesting: 1, + ) + : null, containsCurrentMove: containsCurrentMove, ); } else { // Avoid expensive rebuilds by caching parts of the tree that did not change across a path change - return treeParts[i]; + return subtrees[i]; } }, ).toList(); @@ -295,7 +328,7 @@ class _PgnTreeViewState extends State<_PgnTreeView> { crossAxisAlignment: CrossAxisAlignment.start, children: [ // trick to make auto-scroll work when returning to the root position - if (widget.params.currentPath.isEmpty) + if (widget.params.pathToCurrentMove.isEmpty) SizedBox.shrink(key: widget.params.currentMoveKey), if (widget.params.shouldShowComments && @@ -307,7 +340,14 @@ class _PgnTreeViewState extends State<_PgnTreeView> { _comments(widget.rootComments!, textStyle: _baseTextStyle), ), ), - ...treeParts.map((part) => part.lines).flattened, + ...subtrees + .map( + (part) => [ + part.mainLinePart, + if (part.sidelines != null) part.sidelines!, + ], + ) + .flattened, ], ), ); @@ -415,6 +455,8 @@ List _moveWithComment( ]; } +/// A part of a sideline where each node only has one child +/// (or two children where the second child is rendered as an inline sideline class _SideLinePart extends ConsumerWidget { _SideLinePart( this.nodes, { @@ -812,7 +854,7 @@ class InlineMove extends ConsumerWidget { static const borderRadius = BorderRadius.all(Radius.circular(4.0)); - bool get isCurrentMove => params.currentPath == path; + bool get isCurrentMove => params.pathToCurrentMove == path; @override Widget build(BuildContext context, WidgetRef ref) { From f672de726409e9f06d38e06d2e99e431365ae8a2 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Sun, 6 Oct 2024 15:34:57 +0200 Subject: [PATCH 448/979] remove parent parameter that's not needed anymore --- lib/src/view/analysis/tree_view.dart | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/lib/src/view/analysis/tree_view.dart b/lib/src/view/analysis/tree_view.dart index 5e70b51fbe..ddc79e6085 100644 --- a/lib/src/view/analysis/tree_view.dart +++ b/lib/src/view/analysis/tree_view.dart @@ -386,7 +386,6 @@ List _buildInlineSideLine({ ], ..._moveWithComment( node, - parent: i == 0 ? parent : sidelineNodes[i - 1], lineInfo: ( type: _LineType.inlineSideline, startLine: i == 0 || sidelineNodes[i - 1].hasTextComment @@ -430,7 +429,6 @@ typedef _LineInfo = ({_LineType type, bool startLine}); List _moveWithComment( ViewBranch branch, { - required ViewNode parent, required TextStyle textStyle, required _LineInfo lineInfo, required UciPath pathToNode, @@ -442,7 +440,6 @@ List _moveWithComment( alignment: PlaceholderAlignment.middle, child: InlineMove( branch: branch, - parent: parent, lineInfo: lineInfo, path: pathToNode + branch.id, key: moveKey, @@ -460,7 +457,6 @@ List _moveWithComment( class _SideLinePart extends ConsumerWidget { _SideLinePart( this.nodes, { - required this.parent, required this.initialPath, required this.firstMoveKey, required this.params, @@ -468,8 +464,6 @@ class _SideLinePart extends ConsumerWidget { final List nodes; - final ViewNode parent; - final UciPath initialPath; final GlobalKey firstMoveKey; @@ -487,7 +481,6 @@ class _SideLinePart extends ConsumerWidget { final moves = [ ..._moveWithComment( nodes.first, - parent: parent, lineInfo: ( type: _LineType.sideline, startLine: true, @@ -502,7 +495,6 @@ class _SideLinePart extends ConsumerWidget { final moves = [ ..._moveWithComment( node.children.first, - parent: node, lineInfo: ( type: _LineType.sideline, startLine: node.hasTextComment, @@ -566,7 +558,6 @@ class _MainLinePart extends ConsumerWidget { final moves = [ _moveWithComment( mainlineNode, - parent: node, lineInfo: ( type: _LineType.mainline, startLine: i == 0 || (node as ViewBranch).hasTextComment, @@ -630,7 +621,6 @@ class _SideLines extends StatelessWidget { children: [ _SideLinePart( sidelineNodes.toList(), - parent: parent, firstMoveKey: firstMoveKey, initialPath: initialPath, params: params, @@ -834,7 +824,6 @@ Color? _textColor( class InlineMove extends ConsumerWidget { const InlineMove({ required this.branch, - required this.parent, required this.path, required this.textStyle, required this.lineInfo, @@ -842,7 +831,6 @@ class InlineMove extends ConsumerWidget { required this.params, }); - final ViewNode parent; final ViewBranch branch; final UciPath path; @@ -914,7 +902,6 @@ class InlineMove extends ConsumerWidget { ? '${(ply / 2).ceil()}. $moveWithNag' : '${(ply / 2).ceil()}... $moveWithNag', path: path, - parent: parent, branch: branch, isSideline: lineInfo.type != _LineType.mainline, ), @@ -959,7 +946,6 @@ class _MoveContextMenu extends ConsumerWidget { const _MoveContextMenu({ required this.title, required this.path, - required this.parent, required this.branch, required this.isSideline, required this.notifier, @@ -967,7 +953,6 @@ class _MoveContextMenu extends ConsumerWidget { final String title; final UciPath path; - final ViewNode parent; final ViewBranch branch; final bool isSideline; final AnalysisController notifier; From 0c7d7723967c9abfb1574b76d24486fb5833c73d Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Sun, 6 Oct 2024 15:40:00 +0200 Subject: [PATCH 449/979] add more docs --- lib/src/view/analysis/tree_view.dart | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/lib/src/view/analysis/tree_view.dart b/lib/src/view/analysis/tree_view.dart index ddc79e6085..53e7ec2a9a 100644 --- a/lib/src/view/analysis/tree_view.dart +++ b/lib/src/view/analysis/tree_view.dart @@ -433,6 +433,9 @@ List _moveWithComment( required _LineInfo lineInfo, required UciPath pathToNode, required _PgnTreeViewParams params, + + /// Key that will be assigned to the move text. We use this to track the position of the first move of + /// a sideline, see [_SideLinePart.firstMoveKey] GlobalKey? moveKey, }) { return [ @@ -466,6 +469,8 @@ class _SideLinePart extends ConsumerWidget { final UciPath initialPath; + /// The key that will be assigned to the first move in this sideline. + /// This is needed so that the indent guidelines can be drawn correctly. final GlobalKey firstMoveKey; final _PgnTreeViewParams params; @@ -528,6 +533,7 @@ class _SideLinePart extends ConsumerWidget { } } +/// A part of the mainline that will be rendered on the same line. See [_mainlineParts]. class _MainLinePart extends ConsumerWidget { const _MainLinePart({ required this.initialPath, @@ -589,8 +595,10 @@ class _MainLinePart extends ConsumerWidget { } } -class _SideLines extends StatelessWidget { - const _SideLines({ +/// A sideline where the moves are rendered on the same line (see [_SideLinePart]) until further branching is encountered, +/// at which point the children sidelines are rendered on new lines and indented (see [_IndentedSideLines]). +class _SideLine extends StatelessWidget { + const _SideLine({ required this.firstNode, required this.parent, required this.firstMoveKey, @@ -682,6 +690,8 @@ class _IndentPainter extends CustomPainter { } } +/// Displays one ore more sidelines indented on their own line and adds indent guides. +/// If there are hidden lines, a button is displayed to expand them. class _IndentedSideLines extends StatefulWidget { const _IndentedSideLines( this.sideLines, { @@ -760,7 +770,7 @@ class _IndentedSideLinesState extends State<_IndentedSideLines> { Widget build(BuildContext context) { final sideLineWidgets = _visibleSideLines() .mapIndexed( - (i, firstSidelineNode) => _SideLines( + (i, firstSidelineNode) => _SideLine( firstNode: firstSidelineNode, parent: widget.parent, firstMoveKey: _keys[i], From 0ecc30d7cff65039ce5fe71888bb729a9d4d34ee Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Sun, 6 Oct 2024 15:50:10 +0200 Subject: [PATCH 450/979] factor out rebuilding subtrees into a private function --- lib/src/view/analysis/tree_view.dart | 109 ++++++++++++++------------- 1 file changed, 56 insertions(+), 53 deletions(-) diff --git a/lib/src/view/analysis/tree_view.dart b/lib/src/view/analysis/tree_view.dart index 53e7ec2a9a..caa34276a5 100644 --- a/lib/src/view/analysis/tree_view.dart +++ b/lib/src/view/analysis/tree_view.dart @@ -246,65 +246,68 @@ class _PgnTreeViewState extends State<_PgnTreeView> { return path; } + void _rebuildChangedSubtrees({required bool fullRebuild}) { + var path = UciPath.empty; + subtrees = mainlineParts.mapIndexed( + (i, mainlineNodes) { + final mainlineInitialPath = path; + + final sidelineInitialPath = UciPath.join( + path, + UciPath.fromIds( + mainlineNodes + .take(mainlineNodes.length - 1) + .map((n) => n.children.first.id), + ), + ); + + path = sidelineInitialPath; + if (mainlineNodes.last.children.isNotEmpty) { + path = path + mainlineNodes.last.children.first.id; + } + + final mainlinePartOfCurrentPath = _mainlinePartOfCurrentPath(); + final containsCurrentMove = + mainlinePartOfCurrentPath.size > mainlineInitialPath.size && + mainlinePartOfCurrentPath.size <= path.size; + + if (fullRebuild || + subtrees[i].containsCurrentMove || + containsCurrentMove) { + // Skip the first node which is the continuation of the mainline + final sidelineNodes = mainlineNodes.last.children.skip(1); + return ( + mainLinePart: _MainLinePart( + params: widget.params, + initialPath: mainlineInitialPath, + nodes: mainlineNodes, + ), + sidelines: sidelineNodes.isNotEmpty + ? _IndentedSideLines( + sidelineNodes, + parent: mainlineNodes.last, + params: widget.params, + initialPath: sidelineInitialPath, + nesting: 1, + ) + : null, + containsCurrentMove: containsCurrentMove, + ); + } else { + // Avoid expensive rebuilds by caching parts of the tree that did not change across a path change + return subtrees[i]; + } + }, + ).toList(); + } + void _updateLines({required bool fullRebuild}) { setState(() { if (fullRebuild) { mainlineParts = _mainlineParts(widget.root).toList(); } - var path = UciPath.empty; - - subtrees = mainlineParts.mapIndexed( - (i, mainlineNodes) { - final mainlineInitialPath = path; - - final sidelineInitialPath = UciPath.join( - path, - UciPath.fromIds( - mainlineNodes - .take(mainlineNodes.length - 1) - .map((n) => n.children.first.id), - ), - ); - - path = sidelineInitialPath; - if (mainlineNodes.last.children.isNotEmpty) { - path = path + mainlineNodes.last.children.first.id; - } - - final mainlinePartOfCurrentPath = _mainlinePartOfCurrentPath(); - final containsCurrentMove = - mainlinePartOfCurrentPath.size > mainlineInitialPath.size && - mainlinePartOfCurrentPath.size <= path.size; - - if (fullRebuild || - subtrees[i].containsCurrentMove || - containsCurrentMove) { - // Skip the first node which is the continuation of the mainline - final sidelineNodes = mainlineNodes.last.children.skip(1); - return ( - mainLinePart: _MainLinePart( - params: widget.params, - initialPath: mainlineInitialPath, - nodes: mainlineNodes, - ), - sidelines: sidelineNodes.isNotEmpty - ? _IndentedSideLines( - sidelineNodes, - parent: mainlineNodes.last, - params: widget.params, - initialPath: sidelineInitialPath, - nesting: 1, - ) - : null, - containsCurrentMove: containsCurrentMove, - ); - } else { - // Avoid expensive rebuilds by caching parts of the tree that did not change across a path change - return subtrees[i]; - } - }, - ).toList(); + _rebuildChangedSubtrees(fullRebuild: fullRebuild); }); } From 5acd6068621faa7360c521c18f362d7db673b741 Mon Sep 17 00:00:00 2001 From: Meenbeese Date: Sun, 6 Oct 2024 14:58:16 -0400 Subject: [PATCH 451/979] Disable resizing of app view on Android --- android/app/src/main/AndroidManifest.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 331098d1f5..f68adfbe06 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -15,6 +15,7 @@ android:name="${applicationName}" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" + android:resizeableActivity="false" android:fullBackupContent="@xml/backup_rules"> Date: Mon, 7 Oct 2024 10:23:59 +0200 Subject: [PATCH 452/979] make comment more precise --- lib/src/view/analysis/tree_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/view/analysis/tree_view.dart b/lib/src/view/analysis/tree_view.dart index caa34276a5..c881104524 100644 --- a/lib/src/view/analysis/tree_view.dart +++ b/lib/src/view/analysis/tree_view.dart @@ -294,7 +294,7 @@ class _PgnTreeViewState extends State<_PgnTreeView> { containsCurrentMove: containsCurrentMove, ); } else { - // Avoid expensive rebuilds by caching parts of the tree that did not change across a path change + // Avoid expensive rebuilds ([State.build]) of the entire PGN tree by caching parts of the tree that did not change across a path change return subtrees[i]; } }, From 35203d0d916533360199e6c4f1e072ce6f10c839 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Mon, 7 Oct 2024 10:24:47 +0200 Subject: [PATCH 453/979] add missing `2.` in test --- test/view/analysis/analysis_screen_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/view/analysis/analysis_screen_test.dart b/test/view/analysis/analysis_screen_test.dart index 9ac5053e70..6d4fde1c06 100644 --- a/test/view/analysis/analysis_screen_test.dart +++ b/test/view/analysis/analysis_screen_test.dart @@ -187,7 +187,7 @@ void main() { testWidgets('displays long sideline on its own line', (tester) async { await buildTree( tester, - '1. e4 e5 (1... d5 2. exd5 Qxd5 3. Nc3 Qd8 4. d4 Nf6) Nc3 *', + '1. e4 e5 (1... d5 2. exd5 Qxd5 3. Nc3 Qd8 4. d4 Nf6) 2. Nc3 *', ); expectSameLine(tester, ['1. e4', 'e5']); From 74b56369ab9b12c640d0fdf539d06adde6413d99 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Mon, 7 Oct 2024 10:28:03 +0200 Subject: [PATCH 454/979] make comments about caching more precise --- lib/src/view/analysis/tree_view.dart | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/src/view/analysis/tree_view.dart b/lib/src/view/analysis/tree_view.dart index c881104524..af43edf35a 100644 --- a/lib/src/view/analysis/tree_view.dart +++ b/lib/src/view/analysis/tree_view.dart @@ -228,11 +228,13 @@ typedef _Subtree = ({ }); class _PgnTreeViewState extends State<_PgnTreeView> { - /// Caches the result of [_mainlineParts] to avoid recalculating it on every rebuild. + /// Caches the result of [_mainlineParts], it only needs to be recalculated when the root changes, + /// but not when `params.pathToCurrentMove` changes. List> mainlineParts = []; - /// Caches the top-level subtrees, where each subtree is a [_MainLinePart] and its sidelines. - /// Building the whole tree is expensive, so we cache the subtrees that did not change when the current move changes. + /// Caches the top-level subtrees obtained from the last `build()` method, where each subtree is a [_MainLinePart] and its sidelines. + /// Building the whole tree is expensive, so we cache the subtrees that did not change when the current move changes, + /// the framework will then skip the `build()` of each subtree since the widget reference is the same. List<_Subtree> subtrees = []; UciPath _mainlinePartOfCurrentPath() { From 117760d55b4008ac1f21b87bed90424960c53695 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 7 Oct 2024 11:57:23 +0200 Subject: [PATCH 455/979] Update puzzle tab buttons --- lib/src/utils/connectivity.dart | 14 +- lib/src/view/puzzle/puzzle_tab_screen.dart | 149 +++++++++------------ 2 files changed, 70 insertions(+), 93 deletions(-) diff --git a/lib/src/utils/connectivity.dart b/lib/src/utils/connectivity.dart index a7323c8996..9f2e76829f 100644 --- a/lib/src/utils/connectivity.dart +++ b/lib/src/utils/connectivity.dart @@ -153,12 +153,12 @@ Future isOnline(Client client) { } extension AsyncValueConnectivity on AsyncValue { - /// Switches between two functions based on the device's connectivity status. + /// Switches between device's connectivity status. /// /// Using this method assumes the the device is offline when the status is /// not yet available (i.e. [AsyncValue.isLoading]. /// If you want to handle the loading state separately, use - /// [whenOnlineLoading] instead. + /// [whenIsLoading] instead. /// /// This method is similar to [AsyncValueX.maybeWhen], but it takes two /// functions, one for when the device is online and another for when it is @@ -167,12 +167,12 @@ extension AsyncValueConnectivity on AsyncValue { /// Example: /// ```dart /// final status = ref.watch(connectivityChangesProvider); - /// final result = status.whenOnline( + /// final result = status.whenIs( /// online: () => 'Online', /// offline: () => 'Offline', /// ); /// ``` - R whenOnline({ + R whenIs({ required R Function() online, required R Function() offline, }) { @@ -182,7 +182,7 @@ extension AsyncValueConnectivity on AsyncValue { ); } - /// Switches between three functions based on the device's connectivity status. + /// Switches between device's connectivity status, but handling the loading state. /// /// This method is similar to [AsyncValueX.when], but it takes three /// functions, one for when the device is online, another for when it is @@ -191,13 +191,13 @@ extension AsyncValueConnectivity on AsyncValue { /// Example: /// ```dart /// final status = ref.watch(connectivityChangesProvider); - /// final result = status.whenOnlineLoading( + /// final result = status.whenIsLoading( /// online: () => 'Online', /// offline: () => 'Offline', /// loading: () => 'Loading', /// ); /// ``` - R whenOnlineLoading({ + R whenIsLoading({ required R Function() online, required R Function() offline, required R Function() loading, diff --git a/lib/src/view/puzzle/puzzle_tab_screen.dart b/lib/src/view/puzzle/puzzle_tab_screen.dart index ec6dffea5a..061f63a8bd 100644 --- a/lib/src/view/puzzle/puzzle_tab_screen.dart +++ b/lib/src/view/puzzle/puzzle_tab_screen.dart @@ -35,37 +35,19 @@ import 'streak_screen.dart'; const _kNumberOfHistoryItemsOnHandset = 8; const _kNumberOfHistoryItemsOnTablet = 16; -class PuzzleTabScreen extends ConsumerStatefulWidget { +class PuzzleTabScreen extends ConsumerWidget { const PuzzleTabScreen({super.key}); @override - ConsumerState createState() => _PuzzleTabScreenState(); -} - -class _PuzzleTabScreenState extends ConsumerState { - final _androidRefreshKey = GlobalKey(); - - @override - Widget build(BuildContext context) { - final session = ref.watch(authSessionProvider); - return PlatformWidget( - androidBuilder: (context) => _androidBuilder(context, session), - iosBuilder: (context) => _iosBuilder(context, session), + Widget build(BuildContext context, WidgetRef ref) { + return ConsumerPlatformWidget( + ref: ref, + androidBuilder: _androidBuilder, + iosBuilder: _iosBuilder, ); } - Widget _androidBuilder(BuildContext context, AuthSessionState? userSession) { - final body = Column( - children: [ - const ConnectivityBanner(), - Expanded( - child: _Body(userSession), - ), - ], - ); - - final isTablet = isTabletOrLarger(context); - + Widget _androidBuilder(BuildContext context, WidgetRef ref) { return PopScope( canPop: false, onPopInvokedWithResult: (bool didPop, _) { @@ -76,25 +58,24 @@ class _PuzzleTabScreenState extends ConsumerState { child: Scaffold( appBar: AppBar( title: Text(context.l10n.puzzles), - actions: [ - const _DashboardButton(), - if (!isTablet) const _HistoryButton(), + actions: const [ + _DashboardButton(), + _HistoryButton(), + ], + ), + body: const Column( + children: [ + ConnectivityBanner(), + Expanded( + child: _Body(), + ), ], ), - body: userSession != null - ? RefreshIndicator( - key: _androidRefreshKey, - onRefresh: _refreshData, - child: body, - ) - : body, ), ); } - Widget _iosBuilder(BuildContext context, AuthSessionState? userSession) { - final isTablet = isTabletOrLarger(context); - + Widget _iosBuilder(BuildContext context, WidgetRef ref) { return CupertinoPageScaffold( child: CustomScrollView( controller: puzzlesScrollController, @@ -105,42 +86,28 @@ class _PuzzleTabScreenState extends ConsumerState { end: 8.0, ), largeTitle: Text(context.l10n.puzzles), - trailing: Row( + trailing: const Row( mainAxisSize: MainAxisSize.min, children: [ - const _DashboardButton(), - if (!isTablet) ...[ - const SizedBox(width: 6.0), - const _HistoryButton(), - ], + _DashboardButton(), + SizedBox(width: 6.0), + _HistoryButton(), ], ), ), - if (userSession != null) - CupertinoSliverRefreshControl( - onRefresh: _refreshData, - ), const SliverToBoxAdapter(child: ConnectivityBanner()), - SliverSafeArea( + const SliverSafeArea( top: false, - sliver: _Body(userSession), + sliver: _Body(), ), ], ), ); } - - Future _refreshData() { - return Future.wait([ - ref.refresh(puzzleRecentActivityProvider.future), - ]); - } } class _Body extends ConsumerWidget { - const _Body(this.session); - - final AuthSessionState? session; + const _Body(); @override Widget build(BuildContext context, WidgetRef ref) { @@ -149,7 +116,7 @@ class _Body extends ConsumerWidget { final isTablet = isTabletOrLarger(context); final handsetChildren = [ - connectivity.whenOnline( + connectivity.whenIs( online: () => const _DailyPuzzle(), offline: () => const SizedBox.shrink(), ), @@ -170,7 +137,7 @@ class _Body extends ConsumerWidget { mainAxisAlignment: MainAxisAlignment.start, children: [ const SizedBox(height: 8.0), - connectivity.whenOnline( + connectivity.whenIs( online: () => const _DailyPuzzle(), offline: () => const SizedBox.shrink(), ), @@ -389,21 +356,25 @@ class _DashboardButton extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final session = ref.watch(authSessionProvider); - if (session != null) { - return AppBarIconButton( - icon: const Icon(Icons.assessment_outlined), - semanticsLabel: context.l10n.puzzlePuzzleDashboard, - onPressed: () { - ref.invalidate(puzzleDashboardProvider); - pushPlatformRoute( - context, - title: context.l10n.puzzlePuzzleDashboard, - builder: (_) => const PuzzleDashboardScreen(), - ); - }, - ); + if (session == null) { + return const SizedBox.shrink(); } - return const SizedBox.shrink(); + final onPressed = ref.watch(connectivityChangesProvider).whenIs( + online: () => () { + pushPlatformRoute( + context, + title: context.l10n.puzzlePuzzleDashboard, + builder: (_) => const PuzzleDashboardScreen(), + ); + }, + offline: () => null, + ); + + return AppBarIconButton( + icon: const Icon(Icons.assessment_outlined), + semanticsLabel: context.l10n.puzzlePuzzleDashboard, + onPressed: onPressed, + ); } } @@ -412,20 +383,24 @@ class _HistoryButton extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final asyncData = ref.watch(puzzleRecentActivityProvider); + final session = ref.watch(authSessionProvider); + if (session == null) { + return const SizedBox.shrink(); + } + final onPressed = ref.watch(connectivityChangesProvider).whenIs( + online: () => () { + pushPlatformRoute( + context, + title: context.l10n.puzzleHistory, + builder: (_) => const PuzzleHistoryScreen(), + ); + }, + offline: () => null, + ); return AppBarIconButton( icon: const Icon(Icons.history_outlined), semanticsLabel: context.l10n.puzzleHistory, - onPressed: asyncData.maybeWhen( - data: (_) => () { - pushPlatformRoute( - context, - title: context.l10n.puzzleHistory, - builder: (_) => const PuzzleHistoryScreen(), - ); - }, - orElse: () => null, - ), + onPressed: onPressed, ); } } @@ -547,6 +522,8 @@ class _PuzzlePreview extends ConsumerWidget { style: Styles.boardPreviewTitle, ), Text( + // TODO change this to a better description when + // translation tool is again available (#945) context.l10n.puzzleThemeHealthyMixDescription, maxLines: 3, overflow: TextOverflow.ellipsis, From f22c99889ccf4fa6db01dbd75b445254268879ee Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 7 Oct 2024 12:26:48 +0200 Subject: [PATCH 456/979] Improve puzzle tab loading; fix shimmer style --- lib/src/view/puzzle/puzzle_tab_screen.dart | 156 ++++++++++----------- lib/src/widgets/board_preview.dart | 105 +++++++++++--- lib/src/widgets/shimmer.dart | 63 ++------- 3 files changed, 175 insertions(+), 149 deletions(-) diff --git a/lib/src/view/puzzle/puzzle_tab_screen.dart b/lib/src/view/puzzle/puzzle_tab_screen.dart index 061f63a8bd..79ede93b07 100644 --- a/lib/src/view/puzzle/puzzle_tab_screen.dart +++ b/lib/src/view/puzzle/puzzle_tab_screen.dart @@ -8,6 +8,7 @@ import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_angle.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_providers.dart'; +import 'package:lichess_mobile/src/model/puzzle/puzzle_service.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_theme.dart'; import 'package:lichess_mobile/src/navigation.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; @@ -472,19 +473,10 @@ class _DailyPuzzle extends ConsumerWidget { }, ); }, - loading: () => SmallBoardPreview( - orientation: Side.white, - fen: kEmptyFen, - description: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - Text( - context.l10n.puzzlePuzzleOfTheDay, - style: Styles.boardPreviewTitle, - ), - const Text(''), - ], + loading: () => const Shimmer( + child: ShimmerLoading( + isLoading: true, + child: SmallBoardPreview.loading(), ), ), error: (error, stack) => const Padding( @@ -505,77 +497,85 @@ class _PuzzlePreview extends ConsumerWidget { Widget buildPuzzlePreview(Puzzle? puzzle, {bool loading = false}) { final preview = puzzle != null ? PuzzlePreview.fromPuzzle(puzzle) : null; - return SmallBoardPreview( - orientation: preview?.orientation ?? Side.white, - fen: preview?.initialFen ?? kEmptyFen, - lastMove: preview?.initialMove, - description: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - context.l10n.puzzleDesc, - style: Styles.boardPreviewTitle, - ), - Text( - // TODO change this to a better description when - // translation tool is again available (#945) - context.l10n.puzzleThemeHealthyMixDescription, - maxLines: 3, - overflow: TextOverflow.ellipsis, - style: TextStyle( - height: 1.2, - fontSize: 12.0, + return loading + ? const Shimmer( + child: ShimmerLoading( + isLoading: true, + child: SmallBoardPreview.loading(), + ), + ) + : SmallBoardPreview( + orientation: preview?.orientation ?? Side.white, + fen: preview?.initialFen ?? kEmptyFen, + lastMove: preview?.initialMove, + description: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + context.l10n.puzzleDesc, + style: Styles.boardPreviewTitle, + ), + Text( + // TODO change this to a better description when + // translation tool is again available (#945) + context.l10n.puzzleThemeHealthyMixDescription, + maxLines: 3, + overflow: TextOverflow.ellipsis, + style: TextStyle( + height: 1.2, + fontSize: 12.0, + color: DefaultTextStyle.of(context) + .style + .color + ?.withValues(alpha: 0.6), + ), + ), + ], + ), + Icon( + PuzzleIcons.mix, + size: 34, color: DefaultTextStyle.of(context) .style .color ?.withValues(alpha: 0.6), ), - ), - ], - ), - Icon( - PuzzleIcons.mix, - size: 34, - color: DefaultTextStyle.of(context) - .style - .color - ?.withValues(alpha: 0.6), - ), - if (puzzle != null) - Text( - puzzle.puzzle.sideToMove == Side.white - ? context.l10n.whitePlays - : context.l10n.blackPlays, - ) - else if (!loading) - const Text( - 'No puzzles available, please go online to fetch them.', + if (puzzle != null) + Text( + puzzle.puzzle.sideToMove == Side.white + ? context.l10n.whitePlays + : context.l10n.blackPlays, + ) + else + const Text( + 'No puzzles available, please go online to fetch them.', + ), + ], ), - ], - ), - onTap: puzzle != null - ? () { - pushPlatformRoute( - context, - rootNavigator: true, - builder: (context) => const PuzzleScreen( - angle: PuzzleTheme(PuzzleThemeKey.mix), - ), - ).then((_) { - if (context.mounted) { - ref.invalidate( - nextPuzzleProvider(const PuzzleTheme(PuzzleThemeKey.mix)), - ); - } - }); - } - : null, - ); + onTap: puzzle != null + ? () { + pushPlatformRoute( + context, + rootNavigator: true, + builder: (context) => const PuzzleScreen( + angle: PuzzleTheme(PuzzleThemeKey.mix), + ), + ).then((_) { + if (context.mounted) { + ref.invalidate( + nextPuzzleProvider( + const PuzzleTheme(PuzzleThemeKey.mix)), + ); + } + }); + } + : null, + ); } return puzzle.maybeWhen( diff --git a/lib/src/widgets/board_preview.dart b/lib/src/widgets/board_preview.dart index ea72369549..c52b9e22b6 100644 --- a/lib/src/widgets/board_preview.dart +++ b/lib/src/widgets/board_preview.dart @@ -16,7 +16,16 @@ class SmallBoardPreview extends ConsumerStatefulWidget { this.padding, this.lastMove, this.onTap, - }); + }) : _showLoadingPlaceholder = false; + + const SmallBoardPreview.loading({ + this.padding, + }) : orientation = Side.white, + fen = kEmptyFEN, + lastMove = null, + description = const SizedBox.shrink(), + onTap = null, + _showLoadingPlaceholder = true; /// Side by which the board is oriented. final Side orientation; @@ -33,6 +42,8 @@ class SmallBoardPreview extends ConsumerStatefulWidget { final EdgeInsetsGeometry? padding; + final bool _showLoadingPlaceholder; + @override ConsumerState createState() => _SmallBoardPreviewState(); } @@ -65,23 +76,85 @@ class _SmallBoardPreviewState extends ConsumerState { height: boardSize, child: Row( children: [ - Chessboard.fixed( - size: boardSize, - fen: widget.fen, - orientation: widget.orientation, - lastMove: widget.lastMove as NormalMove?, - settings: ChessboardSettings( - enableCoordinates: false, - borderRadius: - const BorderRadius.all(Radius.circular(4.0)), - boxShadow: boardShadows, - animationDuration: const Duration(milliseconds: 150), - pieceAssets: boardPrefs.pieceSet.assets, - colorScheme: boardPrefs.boardTheme.colors, + if (widget._showLoadingPlaceholder) + Container( + width: boardSize, + height: boardSize, + decoration: const BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.all(Radius.circular(4.0)), + ), + ) + else + Chessboard.fixed( + size: boardSize, + fen: widget.fen, + orientation: widget.orientation, + lastMove: widget.lastMove as NormalMove?, + settings: ChessboardSettings( + enableCoordinates: false, + borderRadius: + const BorderRadius.all(Radius.circular(4.0)), + boxShadow: boardShadows, + animationDuration: const Duration(milliseconds: 150), + pieceAssets: boardPrefs.pieceSet.assets, + colorScheme: boardPrefs.boardTheme.colors, + ), ), - ), const SizedBox(width: 10.0), - Expanded(child: widget.description), + if (widget._showLoadingPlaceholder) + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: 16.0, + width: double.infinity, + decoration: const BoxDecoration( + color: Colors.black, + borderRadius: + BorderRadius.all(Radius.circular(4.0)), + ), + ), + const SizedBox(height: 4.0), + Container( + height: 16.0, + width: MediaQuery.sizeOf(context).width / 3, + decoration: const BoxDecoration( + color: Colors.black, + borderRadius: + BorderRadius.all(Radius.circular(4.0)), + ), + ), + ], + ), + Container( + height: 44.0, + width: 44.0, + decoration: const BoxDecoration( + color: Colors.black, + borderRadius: + BorderRadius.all(Radius.circular(4.0)), + ), + ), + Container( + height: 16.0, + width: double.infinity, + decoration: const BoxDecoration( + color: Colors.black, + borderRadius: + BorderRadius.all(Radius.circular(4.0)), + ), + ), + ], + ), + ) + else + Expanded(child: widget.description), ], ), ), diff --git a/lib/src/widgets/shimmer.dart b/lib/src/widgets/shimmer.dart index 045a9dd546..825c17d88e 100644 --- a/lib/src/widgets/shimmer.dart +++ b/lib/src/widgets/shimmer.dart @@ -1,4 +1,3 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; class Shimmer extends StatefulWidget { @@ -21,26 +20,12 @@ class ShimmerState extends State with SingleTickerProviderStateMixin { late AnimationController _shimmerController; LinearGradient get _defaultGradient { - switch (Theme.of(context).platform) { - case TargetPlatform.android: - final brightness = Theme.of(context).brightness; - switch (brightness) { - case Brightness.light: - return androidLightShimmerGradient; - case Brightness.dark: - return androidDarkShimmerGradient; - } - case TargetPlatform.iOS: - final brightness = - CupertinoTheme.maybeBrightnessOf(context) ?? Brightness.light; - switch (brightness) { - case Brightness.light: - return iOSLightShimmerGradient; - case Brightness.dark: - return iOSDarkShimmerGradient; - } - default: - throw 'Unexpected platform $Theme.of(context).platform'; + final brightness = Theme.of(context).brightness; + switch (brightness) { + case Brightness.light: + return lightShimmerGradient; + case Brightness.dark: + return darkShimmerGradient; } } @@ -167,7 +152,7 @@ class _ShimmerLoadingState extends State { } } -const iOSLightShimmerGradient = LinearGradient( +const lightShimmerGradient = LinearGradient( colors: [ Color(0xFFE3E3E6), Color(0xFFECECEE), @@ -183,39 +168,7 @@ const iOSLightShimmerGradient = LinearGradient( tileMode: TileMode.clamp, ); -const iOSDarkShimmerGradient = LinearGradient( - colors: [ - Color(0xFF111111), - Color(0xFF1a1a1a), - Color(0xFF111111), - ], - stops: [ - 0.1, - 0.3, - 0.4, - ], - begin: Alignment(-1.0, -0.3), - end: Alignment(1.0, 0.3), - tileMode: TileMode.clamp, -); - -const androidLightShimmerGradient = LinearGradient( - colors: [ - Color(0xFFE6E6E6), - Color(0xFFEFEFEF), - Color(0xFFE6E6E6), - ], - stops: [ - 0.1, - 0.3, - 0.4, - ], - begin: Alignment(-1.0, -0.3), - end: Alignment(1.0, 0.3), - tileMode: TileMode.clamp, -); - -const androidDarkShimmerGradient = LinearGradient( +const darkShimmerGradient = LinearGradient( colors: [ Color(0xFF333333), Color(0xFF3c3c3c), From 47a75b80772269a7cfe055940dbb197b192222cb Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 7 Oct 2024 15:35:23 +0200 Subject: [PATCH 457/979] Don't use timers in test http mock responses --- test/model/auth/auth_controller_test.dart | 10 +++++----- test/test_helpers.dart | 23 ++++++----------------- 2 files changed, 11 insertions(+), 22 deletions(-) diff --git a/test/model/auth/auth_controller_test.dart b/test/model/auth/auth_controller_test.dart index e0df8d1895..65e02b35a4 100644 --- a/test/model/auth/auth_controller_test.dart +++ b/test/model/auth/auth_controller_test.dart @@ -72,12 +72,12 @@ void main() { group('AuthController', () { test('sign in', () async { when(() => mockSessionStorage.read()) - .thenAnswer((_) => delayedAnswer(null)); + .thenAnswer((_) => Future.value(null)); when(() => mockFlutterAppAuth.authorizeAndExchangeCode(any())) - .thenAnswer((_) => delayedAnswer(signInResponse)); + .thenAnswer((_) => Future.value(signInResponse)); when( () => mockSessionStorage.write(any()), - ).thenAnswer((_) => delayedAnswer(null)); + ).thenAnswer((_) => Future.value(null)); final container = await makeContainer( overrides: [ @@ -117,10 +117,10 @@ void main() { test('sign out', () async { when(() => mockSessionStorage.read()) - .thenAnswer((_) => delayedAnswer(testUserSession)); + .thenAnswer((_) => Future.value(testUserSession)); when( () => mockSessionStorage.delete(), - ).thenAnswer((_) => delayedAnswer(null)); + ).thenAnswer((_) => Future.value(null)); final container = await makeContainer( overrides: [ diff --git a/test/test_helpers.dart b/test/test_helpers.dart index f1b109c6b0..da5507f50b 100644 --- a/test/test_helpers.dart +++ b/test/test_helpers.dart @@ -19,17 +19,14 @@ const kPlatformVariant = Matcher sameRequest(http.BaseRequest request) => _SameRequest(request); Matcher sameHeaders(Map headers) => _SameHeaders(headers); -Future delayedAnswer(T value) => - Future.delayed(const Duration(milliseconds: 5)).then((_) => value); - /// Mocks an http response with a delay of 20ms. Future mockResponse( String body, int code, { Map headers = const {}, }) => - Future.delayed(const Duration(milliseconds: 20)).then( - (_) => http.Response( + Future.value( + http.Response( body, code, headers: headers, @@ -37,23 +34,21 @@ Future mockResponse( ); Future mockStreamedResponse(String body, int code) => - Future.delayed(const Duration(milliseconds: 20)).then( - (_) => http.StreamedResponse(Stream.value(body).map(utf8.encode), code), + Future.value( + http.StreamedResponse(Stream.value(body).map(utf8.encode), code), ); Future mockHttpStreamFromIterable( Iterable events, ) async { - await Future.delayed(const Duration(milliseconds: 20)); return http.StreamedResponse( - _streamFromFutures(events.map((e) => _withDelay(utf8.encode(e)))), + _streamFromFutures(events.map((e) => Future.value(utf8.encode(e)))), 200, ); } Future mockHttpStream(Stream stream) => - Future.delayed(const Duration(milliseconds: 20)) - .then((_) => http.StreamedResponse(stream.map(utf8.encode), 200)); + Future.value(http.StreamedResponse(stream.map(utf8.encode), 200)); Future tapBackButton(WidgetTester tester) async { if (debugDefaultTargetPlatformOverride == TargetPlatform.iOS) { @@ -148,9 +143,3 @@ Stream _streamFromFutures(Iterable> futures) async* { yield result; } } - -Future _withDelay( - T value, { - Duration delay = const Duration(milliseconds: 10), -}) => - Future.delayed(delay).then((_) => value); From 520ba76ffce06be353e6a4b136d1c1b7afcab6f1 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 7 Oct 2024 15:35:41 +0200 Subject: [PATCH 458/979] Add puzzle tab screen tests --- lib/src/view/puzzle/puzzle_tab_screen.dart | 30 +-- test/model/puzzle/mock_server_responses.dart | 7 + test/model/puzzle/puzzle_repository_test.dart | 8 +- test/test_container.dart | 14 -- test/test_provider_scope.dart | 11 -- test/view/puzzle/example_data.dart | 86 ++++++++ test/view/puzzle/puzzle_screen_test.dart | 84 +------- test/view/puzzle/puzzle_tab_screen_test.dart | 187 ++++++++++++++++++ 8 files changed, 300 insertions(+), 127 deletions(-) create mode 100644 test/model/puzzle/mock_server_responses.dart create mode 100644 test/view/puzzle/example_data.dart create mode 100644 test/view/puzzle/puzzle_tab_screen_test.dart diff --git a/lib/src/view/puzzle/puzzle_tab_screen.dart b/lib/src/view/puzzle/puzzle_tab_screen.dart index 79ede93b07..e34871785a 100644 --- a/lib/src/view/puzzle/puzzle_tab_screen.dart +++ b/lib/src/view/puzzle/puzzle_tab_screen.dart @@ -8,7 +8,6 @@ import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_angle.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_providers.dart'; -import 'package:lichess_mobile/src/model/puzzle/puzzle_service.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_theme.dart'; import 'package:lichess_mobile/src/navigation.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; @@ -118,11 +117,11 @@ class _Body extends ConsumerWidget { final handsetChildren = [ connectivity.whenIs( - online: () => const _DailyPuzzle(), + online: () => const DailyPuzzle(), offline: () => const SizedBox.shrink(), ), const SizedBox(height: 4.0), - const _PuzzlePreview(), + const TacticalTrainingPreview(), if (Theme.of(context).platform == TargetPlatform.android) const SizedBox(height: 8.0), _PuzzleMenu(connectivity: connectivity), @@ -139,7 +138,7 @@ class _Body extends ConsumerWidget { children: [ const SizedBox(height: 8.0), connectivity.whenIs( - online: () => const _DailyPuzzle(), + online: () => const DailyPuzzle(), offline: () => const SizedBox.shrink(), ), _PuzzleMenu(connectivity: connectivity), @@ -413,8 +412,9 @@ TextStyle _puzzlePreviewSubtitleStyle(BuildContext context) { ); } -class _DailyPuzzle extends ConsumerWidget { - const _DailyPuzzle(); +/// A widget that displays the daily puzzle. +class DailyPuzzle extends ConsumerWidget { + const DailyPuzzle(); @override Widget build(BuildContext context, WidgetRef ref) { @@ -479,16 +479,19 @@ class _DailyPuzzle extends ConsumerWidget { child: SmallBoardPreview.loading(), ), ), - error: (error, stack) => const Padding( - padding: Styles.bodySectionPadding, - child: Text('Could not load the daily puzzle.'), - ), + error: (error, _) { + return const Padding( + padding: Styles.bodySectionPadding, + child: Text('Could not load the daily puzzle.'), + ); + }, ); } } -class _PuzzlePreview extends ConsumerWidget { - const _PuzzlePreview(); +/// A widget that displays a preview of the tactical training screen. +class TacticalTrainingPreview extends ConsumerWidget { + const TacticalTrainingPreview(); @override Widget build(BuildContext context, WidgetRef ref) { @@ -569,7 +572,8 @@ class _PuzzlePreview extends ConsumerWidget { if (context.mounted) { ref.invalidate( nextPuzzleProvider( - const PuzzleTheme(PuzzleThemeKey.mix)), + const PuzzleTheme(PuzzleThemeKey.mix), + ), ); } }); diff --git a/test/model/puzzle/mock_server_responses.dart b/test/model/puzzle/mock_server_responses.dart new file mode 100644 index 0000000000..01653958f3 --- /dev/null +++ b/test/model/puzzle/mock_server_responses.dart @@ -0,0 +1,7 @@ +const mockDailyPuzzleResponse = ''' +{"game":{"id":"MNMYnEjm","perf":{"key":"classical","name":"Classical"},"rated":true,"players":[{"name":"Igor76","id":"igor76","color":"white","rating":2211},{"name":"dmitriy_duyun","id":"dmitriy_duyun","color":"black","rating":2180}],"pgn":"e4 c6 d4 d5 Nc3 g6 Nf3 Bg7 h3 dxe4 Nxe4 Nf6 Bd3 Nxe4 Bxe4 Nd7 O-O Nf6 Bd3 O-O Re1 Bf5 Bxf5 gxf5 c3 e6 Bg5 Qb6 Qc2 Rac8 Ne5 Qc7 Rad1 Nd7 Bf4 Nxe5 Bxe5 Bxe5 Rxe5 Rcd8 Qd2 Kh8 Rde1 Rg8 Qf4","clock":"20+15"},"puzzle":{"id":"0XqV2","rating":1929,"plays":93270,"solution":["f7f6","e5f5","c7g7","g2g3","e6f5"],"themes":["clearance","endgame","advantage","intermezzo","long"],"initialPly":44}} +'''; + +const mockMixBatchResponse = ''' +{"puzzles":[{"game":{"id":"PrlkCqOv","perf":{"key":"rapid","name":"Rapid"},"rated":true,"players":[{"userId":"silverjo","name":"silverjo (1777)","color":"white"},{"userId":"robyarchitetto","name":"Robyarchitetto (1742)","color":"black"}],"pgn":"e4 Nc6 Bc4 e6 a3 g6 Nf3 Bg7 c3 Nge7 d3 O-O Be3 Na5 Ba2 b6 Qd2 Bb7 Bh6 d5 e5 d4 Bxg7 Kxg7 Qf4 Bxf3 Qxf3 dxc3 Nxc3 Nac6 Qf6+ Kg8 Rd1 Nd4 O-O c5 Ne4 Nef5 Rd2 Qxf6 Nxf6+ Kg7 Re1 h5 h3 Rad8 b4 Nh4 Re3 Nhf5 Re1 a5 bxc5 bxc5 Bc4 Ra8 Rb1 Nh4 Rdb2 Nc6 Rb7 Nxe5 Bxe6 Kxf6 Bd5 Nf5 R7b6+ Kg7 Bxa8 Rxa8 R6b3 Nd4 Rb7 Nxd3 Rd1 Ne2+ Kh2 Ndf4 Rdd7 Rf8 Ra7 c4 Rxa5 c3 Rc5 Ne6 Rc4 Ra8 a4 Rb8 a5 Rb2 a6 c2","clock":"5+8"},"puzzle":{"id":"20yWT","rating":1859,"plays":551,"initialPly":93,"solution":["a6a7","b2a2","c4c2","a2a7","d7a7"],"themes":["endgame","long","advantage","advancedPawn"]}},{"game":{"id":"0lwkiJbZ","perf":{"key":"classical","name":"Classical"},"rated":true,"players":[{"userId":"nirdosh","name":"nirdosh (2035)","color":"white"},{"userId":"burn_it_down","name":"burn_it_down (2139)","color":"black"}],"pgn":"d4 Nf6 Nf3 c5 e3 g6 Bd3 Bg7 c3 Qc7 O-O O-O Nbd2 d6 Qe2 Nbd7 e4 cxd4 cxd4 e5 dxe5 dxe5 b3 Nc5 Bb2 Nh5 g3 Bh3 Rfc1 Qd6 Bc4 Rac8 Bd5 Qb8 Ng5 Bd7 Ba3 b6 Rc2 h6 Ngf3 Rfe8 Rac1 Ne6 Nc4 Bb5 Qe3 Bxc4 Bxc4 Nd4 Nxd4 exd4 Qd3 Rcd8 f4 Nf6 e5 Ng4 Qxg6 Ne3 Bxf7+ Kh8 Rc7 Qa8 Qxg7+ Kxg7 Bd5+ Kg6 Bxa8 Rxa8 Rd7 Rad8 Rc6+ Kf5 Rcd6 Rxd7 Rxd7 Ke4 Bb2 Nc2 Kf2 d3 Bc1 Nd4 h3","clock":"15+15"},"puzzle":{"id":"7H5EV","rating":1852,"plays":410,"initialPly":84,"solution":["e8c8","d7d4","e4d4"],"themes":["endgame","short","advantage"]}},{"game":{"id":"eWGRX5AI","perf":{"key":"rapid","name":"Rapid"},"rated":true,"players":[{"userId":"sacalot","name":"sacalot (2151)","color":"white"},{"userId":"landitirana","name":"landitirana (1809)","color":"black"}],"pgn":"e4 e5 Nf3 Nc6 d4 exd4 Bc4 Nf6 O-O Nxe4 Re1 d5 Bxd5 Qxd5 Nc3 Qd8 Rxe4+ Be6 Nxd4 Nxd4 Rxd4 Qf6 Ne4 Qe5 f4 Qf5 Ng3 Qa5 Bd2 Qb6 Be3 Bc5 f5 Bd5 Rxd5 Bxe3+ Kh1 O-O Rd3 Rfe8 Qf3 Qxb2 Rf1 Bd4 Nh5 Bf6 Rb3 Qd4 Rxb7 Re3 Nxf6+ gxf6 Qf2 Rae8 Rxc7 Qe5 Rc4 Re1 Rf4 Qa1 h3","clock":"10+0"},"puzzle":{"id":"1qUth","rating":1556,"plays":2661,"initialPly":60,"solution":["e1f1","f2f1","e8e1","f1e1","a1e1"],"themes":["endgame","master","advantage","fork","long","pin"]}}]} +'''; diff --git a/test/model/puzzle/puzzle_repository_test.dart b/test/model/puzzle/puzzle_repository_test.dart index 6098c0aa59..2002320c3a 100644 --- a/test/model/puzzle/puzzle_repository_test.dart +++ b/test/model/puzzle/puzzle_repository_test.dart @@ -6,18 +6,14 @@ import 'package:lichess_mobile/src/network/http.dart'; import '../../test_container.dart'; import '../../test_helpers.dart'; +import 'mock_server_responses.dart'; void main() { group('PuzzleRepository', () { test('selectBatch', () async { final mockClient = MockClient((request) { if (request.url.path == '/api/puzzle/batch/mix') { - return mockResponse( - ''' -{"puzzles":[{"game":{"id":"PrlkCqOv","perf":{"key":"rapid","name":"Rapid"},"rated":true,"players":[{"userId":"silverjo","name":"silverjo (1777)","color":"white"},{"userId":"robyarchitetto","name":"Robyarchitetto (1742)","color":"black"}],"pgn":"e4 Nc6 Bc4 e6 a3 g6 Nf3 Bg7 c3 Nge7 d3 O-O Be3 Na5 Ba2 b6 Qd2 Bb7 Bh6 d5 e5 d4 Bxg7 Kxg7 Qf4 Bxf3 Qxf3 dxc3 Nxc3 Nac6 Qf6+ Kg8 Rd1 Nd4 O-O c5 Ne4 Nef5 Rd2 Qxf6 Nxf6+ Kg7 Re1 h5 h3 Rad8 b4 Nh4 Re3 Nhf5 Re1 a5 bxc5 bxc5 Bc4 Ra8 Rb1 Nh4 Rdb2 Nc6 Rb7 Nxe5 Bxe6 Kxf6 Bd5 Nf5 R7b6+ Kg7 Bxa8 Rxa8 R6b3 Nd4 Rb7 Nxd3 Rd1 Ne2+ Kh2 Ndf4 Rdd7 Rf8 Ra7 c4 Rxa5 c3 Rc5 Ne6 Rc4 Ra8 a4 Rb8 a5 Rb2 a6 c2","clock":"5+8"},"puzzle":{"id":"20yWT","rating":1859,"plays":551,"initialPly":93,"solution":["a6a7","b2a2","c4c2","a2a7","d7a7"],"themes":["endgame","long","advantage","advancedPawn"]}},{"game":{"id":"0lwkiJbZ","perf":{"key":"classical","name":"Classical"},"rated":true,"players":[{"userId":"nirdosh","name":"nirdosh (2035)","color":"white"},{"userId":"burn_it_down","name":"burn_it_down (2139)","color":"black"}],"pgn":"d4 Nf6 Nf3 c5 e3 g6 Bd3 Bg7 c3 Qc7 O-O O-O Nbd2 d6 Qe2 Nbd7 e4 cxd4 cxd4 e5 dxe5 dxe5 b3 Nc5 Bb2 Nh5 g3 Bh3 Rfc1 Qd6 Bc4 Rac8 Bd5 Qb8 Ng5 Bd7 Ba3 b6 Rc2 h6 Ngf3 Rfe8 Rac1 Ne6 Nc4 Bb5 Qe3 Bxc4 Bxc4 Nd4 Nxd4 exd4 Qd3 Rcd8 f4 Nf6 e5 Ng4 Qxg6 Ne3 Bxf7+ Kh8 Rc7 Qa8 Qxg7+ Kxg7 Bd5+ Kg6 Bxa8 Rxa8 Rd7 Rad8 Rc6+ Kf5 Rcd6 Rxd7 Rxd7 Ke4 Bb2 Nc2 Kf2 d3 Bc1 Nd4 h3","clock":"15+15"},"puzzle":{"id":"7H5EV","rating":1852,"plays":410,"initialPly":84,"solution":["e8c8","d7d4","e4d4"],"themes":["endgame","short","advantage"]}},{"game":{"id":"eWGRX5AI","perf":{"key":"rapid","name":"Rapid"},"rated":true,"players":[{"userId":"sacalot","name":"sacalot (2151)","color":"white"},{"userId":"landitirana","name":"landitirana (1809)","color":"black"}],"pgn":"e4 e5 Nf3 Nc6 d4 exd4 Bc4 Nf6 O-O Nxe4 Re1 d5 Bxd5 Qxd5 Nc3 Qd8 Rxe4+ Be6 Nxd4 Nxd4 Rxd4 Qf6 Ne4 Qe5 f4 Qf5 Ng3 Qa5 Bd2 Qb6 Be3 Bc5 f5 Bd5 Rxd5 Bxe3+ Kh1 O-O Rd3 Rfe8 Qf3 Qxb2 Rf1 Bd4 Nh5 Bf6 Rb3 Qd4 Rxb7 Re3 Nxf6+ gxf6 Qf2 Rae8 Rxc7 Qe5 Rc4 Re1 Rf4 Qa1 h3","clock":"10+0"},"puzzle":{"id":"1qUth","rating":1556,"plays":2661,"initialPly":60,"solution":["e1f1","f2f1","e8e1","f1e1","a1e1"],"themes":["endgame","master","advantage","fork","long","pin"]}}]} -''', - 200, - ); + return mockResponse(mockMixBatchResponse, 200); } return mockResponse('', 404); }); diff --git a/test/test_container.dart b/test/test_container.dart index 0a1a42cdc1..bdc0e711fa 100644 --- a/test/test_container.dart +++ b/test/test_container.dart @@ -1,10 +1,8 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; -import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/crashlytics.dart'; import 'package:lichess_mobile/src/db/database.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; @@ -13,7 +11,6 @@ import 'package:lichess_mobile/src/model/notifications/notification_service.dart import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/network/socket.dart'; import 'package:lichess_mobile/src/utils/connectivity.dart'; -import 'package:logging/logging.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import './fake_crashlytics.dart'; @@ -29,8 +26,6 @@ final testContainerMockClient = MockClient((request) async { return http.Response('', 200); }); -const shouldLog = false; - /// Returns a [ProviderContainer] with the [httpClientFactoryProvider] configured /// with the given [mockClient]. Future lichessClientContainer(MockClient mockClient) async { @@ -56,15 +51,6 @@ Future makeContainer({ await binding.preloadData(userSession); - Logger.root.onRecord.listen((record) { - if (shouldLog && record.level >= Level.FINE) { - final time = DateFormat.Hms().format(record.time); - debugPrint( - '${record.level.name} at $time [${record.loggerName}] ${record.message}${record.error != null ? '\n${record.error}' : ''}', - ); - } - }); - final container = ProviderContainer( overrides: [ connectivityPluginProvider.overrideWith((_) { diff --git a/test/test_provider_scope.dart b/test/test_provider_scope.dart index 253b503db1..e081e2df01 100644 --- a/test/test_provider_scope.dart +++ b/test/test_provider_scope.dart @@ -7,7 +7,6 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; -import 'package:intl/intl.dart'; import 'package:lichess_mobile/l10n/l10n.dart'; import 'package:lichess_mobile/src/crashlytics.dart'; import 'package:lichess_mobile/src/db/database.dart'; @@ -20,7 +19,6 @@ import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/network/socket.dart'; import 'package:lichess_mobile/src/utils/connectivity.dart'; -import 'package:logging/logging.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:visibility_detector/visibility_detector.dart'; @@ -120,15 +118,6 @@ Future makeTestProviderScope( // TODO consider loading true fonts as well FlutterError.onError = _ignoreOverflowErrors; - Logger.root.onRecord.listen((record) { - if (record.level > Level.WARNING) { - final time = DateFormat.Hms().format(record.time); - debugPrint( - '${record.level.name} at $time [${record.loggerName}] ${record.message}${record.error != null ? '\n${record.error}' : ''}', - ); - } - }); - return ProviderScope( overrides: [ // ignore: scoped_providers_should_specify_dependencies diff --git a/test/view/puzzle/example_data.dart b/test/view/puzzle/example_data.dart new file mode 100644 index 0000000000..8a7b6a0b8c --- /dev/null +++ b/test/view/puzzle/example_data.dart @@ -0,0 +1,86 @@ +import 'package:dartchess/dartchess.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/common/perf.dart'; +import 'package:lichess_mobile/src/model/puzzle/puzzle.dart'; +import 'package:lichess_mobile/src/model/puzzle/puzzle_batch_storage.dart'; + +final puzzle = Puzzle( + puzzle: PuzzleData( + id: const PuzzleId('6Sz3s'), + initialPly: 40, + plays: 68176, + rating: 1984, + solution: IList(const [ + 'h4h2', + 'h1h2', + 'e5f3', + 'h2h3', + 'b4h4', + ]), + themes: ISet(const [ + 'middlegame', + 'attraction', + 'long', + 'mateIn3', + 'sacrifice', + 'doubleCheck', + ]), + ), + game: const PuzzleGame( + rated: true, + id: GameId('zgBwsXLr'), + perf: Perf.blitz, + pgn: + 'e4 c5 Nf3 e6 c4 Nc6 d4 cxd4 Nxd4 Bc5 Nxc6 bxc6 Be2 Ne7 O-O Ng6 Nc3 Rb8 Kh1 Bb7 f4 d5 f5 Ne5 fxe6 fxe6 cxd5 cxd5 exd5 Bxd5 Qa4+ Bc6 Qf4 Bd6 Ne4 Bxe4 Qxe4 Rb4 Qe3 Qh4 Qxa7', + black: PuzzleGamePlayer( + side: Side.black, + name: 'CAMBIADOR', + ), + white: PuzzleGamePlayer( + side: Side.white, + name: 'arroyoM10', + ), + ), +); + +final batch = PuzzleBatch( + solved: IList(const []), + unsolved: IList([ + puzzle, + ]), +); + +final puzzle2 = Puzzle( + puzzle: PuzzleData( + id: const PuzzleId('2nNdI'), + rating: 1090, + plays: 23890, + initialPly: 88, + solution: IList(const ['g4h4', 'h8h4', 'b4h4']), + themes: ISet(const { + 'endgame', + 'short', + 'crushing', + 'fork', + 'queenRookEndgame', + }), + ), + game: const PuzzleGame( + id: GameId('w32JTzEf'), + perf: Perf.blitz, + rated: true, + white: PuzzleGamePlayer( + side: Side.white, + name: 'Li', + title: null, + ), + black: PuzzleGamePlayer( + side: Side.black, + name: 'Gabriela', + title: null, + ), + pgn: + 'e4 e5 Nf3 Nc6 Bb5 a6 Ba4 b5 Bb3 Nf6 c3 Nxe4 d4 exd4 cxd4 Qe7 O-O Qd8 Bd5 Nf6 Bb3 Bd6 Nc3 O-O Bg5 h6 Bh4 g5 Nxg5 hxg5 Bxg5 Kg7 Ne4 Be7 Bxf6+ Bxf6 Qg4+ Kh8 Qh5+ Kg8 Qg6+ Kh8 Qxf6+ Qxf6 Nxf6 Nxd4 Rfd1 Ne2+ Kh1 d6 Rd5 Kg7 Nh5+ Kh6 Rad1 Be6 R5d2 Bxb3 axb3 Kxh5 Rxe2 Rfe8 Red2 Re5 h3 Rae8 Kh2 Re2 Rd5+ Kg6 f4 Rxb2 R1d3 Ree2 Rg3+ Kf6 h4 Re4 Rg4 Rxb3 h5 Rbb4 h6 Rxf4 h7 Rxg4 h8=Q+ Ke7 Rd3', + ), +); diff --git a/test/view/puzzle/puzzle_screen_test.dart b/test/view/puzzle/puzzle_screen_test.dart index 7cc0dca6a1..41497a07ab 100644 --- a/test/view/puzzle/puzzle_screen_test.dart +++ b/test/view/puzzle/puzzle_screen_test.dart @@ -6,9 +6,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; import 'package:lichess_mobile/src/model/account/account_preferences.dart'; -import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/model/common/perf.dart'; -import 'package:lichess_mobile/src/model/puzzle/puzzle.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_angle.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_batch_storage.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_storage.dart'; @@ -21,6 +18,7 @@ import 'package:mocktail/mocktail.dart'; import '../../test_helpers.dart'; import '../../test_provider_scope.dart'; +import 'example_data.dart'; class MockPuzzleBatchStorage extends Mock implements PuzzleBatchStorage {} @@ -457,86 +455,6 @@ void main() { }); } -final puzzle = Puzzle( - puzzle: PuzzleData( - id: const PuzzleId('6Sz3s'), - initialPly: 40, - plays: 68176, - rating: 1984, - solution: IList(const [ - 'h4h2', - 'h1h2', - 'e5f3', - 'h2h3', - 'b4h4', - ]), - themes: ISet(const [ - 'middlegame', - 'attraction', - 'long', - 'mateIn3', - 'sacrifice', - 'doubleCheck', - ]), - ), - game: const PuzzleGame( - rated: true, - id: GameId('zgBwsXLr'), - perf: Perf.blitz, - pgn: - 'e4 c5 Nf3 e6 c4 Nc6 d4 cxd4 Nxd4 Bc5 Nxc6 bxc6 Be2 Ne7 O-O Ng6 Nc3 Rb8 Kh1 Bb7 f4 d5 f5 Ne5 fxe6 fxe6 cxd5 cxd5 exd5 Bxd5 Qa4+ Bc6 Qf4 Bd6 Ne4 Bxe4 Qxe4 Rb4 Qe3 Qh4 Qxa7', - black: PuzzleGamePlayer( - side: Side.black, - name: 'CAMBIADOR', - ), - white: PuzzleGamePlayer( - side: Side.white, - name: 'arroyoM10', - ), - ), -); - -final batch = PuzzleBatch( - solved: IList(const []), - unsolved: IList([ - puzzle, - ]), -); - -final puzzle2 = Puzzle( - puzzle: PuzzleData( - id: const PuzzleId('2nNdI'), - rating: 1090, - plays: 23890, - initialPly: 88, - solution: IList(const ['g4h4', 'h8h4', 'b4h4']), - themes: ISet(const { - 'endgame', - 'short', - 'crushing', - 'fork', - 'queenRookEndgame', - }), - ), - game: const PuzzleGame( - id: GameId('w32JTzEf'), - perf: Perf.blitz, - rated: true, - white: PuzzleGamePlayer( - side: Side.white, - name: 'Li', - title: null, - ), - black: PuzzleGamePlayer( - side: Side.black, - name: 'Gabriela', - title: null, - ), - pgn: - 'e4 e5 Nf3 Nc6 Bb5 a6 Ba4 b5 Bb3 Nf6 c3 Nxe4 d4 exd4 cxd4 Qe7 O-O Qd8 Bd5 Nf6 Bb3 Bd6 Nc3 O-O Bg5 h6 Bh4 g5 Nxg5 hxg5 Bxg5 Kg7 Ne4 Be7 Bxf6+ Bxf6 Qg4+ Kh8 Qh5+ Kg8 Qg6+ Kh8 Qxf6+ Qxf6 Nxf6 Nxd4 Rfd1 Ne2+ Kh1 d6 Rd5 Kg7 Nh5+ Kh6 Rad1 Be6 R5d2 Bxb3 axb3 Kxh5 Rxe2 Rfe8 Red2 Re5 h3 Rae8 Kh2 Re2 Rd5+ Kg6 f4 Rxb2 R1d3 Ree2 Rg3+ Kf6 h4 Re4 Rg4 Rxb3 h5 Rbb4 h6 Rxf4 h7 Rxg4 h8=Q+ Ke7 Rd3', - ), -); - const batchOf1 = ''' {"puzzles":[{"game":{"id":"PrlkCqOv","perf":{"key":"rapid","name":"Rapid"},"rated":true,"players":[{"name":"silverjo", "rating":1777,"color":"white"},{"name":"Robyarchitetto", "rating":1742,"color":"black"}],"pgn":"e4 Nc6 Bc4 e6 a3 g6 Nf3 Bg7 c3 Nge7 d3 O-O Be3 Na5 Ba2 b6 Qd2 Bb7 Bh6 d5 e5 d4 Bxg7 Kxg7 Qf4 Bxf3 Qxf3 dxc3 Nxc3 Nac6 Qf6+ Kg8 Rd1 Nd4 O-O c5 Ne4 Nef5 Rd2 Qxf6 Nxf6+ Kg7 Re1 h5 h3 Rad8 b4 Nh4 Re3 Nhf5 Re1 a5 bxc5 bxc5 Bc4 Ra8 Rb1 Nh4 Rdb2 Nc6 Rb7 Nxe5 Bxe6 Kxf6 Bd5 Nf5 R7b6+ Kg7 Bxa8 Rxa8 R6b3 Nd4 Rb7 Nxd3 Rd1 Ne2+ Kh2 Ndf4 Rdd7 Rf8 Ra7 c4 Rxa5 c3 Rc5 Ne6 Rc4 Ra8 a4 Rb8 a5 Rb2 a6 c2","clock":"5+8"},"puzzle":{"id":"20yWT","rating":1859,"plays":551,"initialPly":93,"solution":["a6a7","b2a2","c4c2","a2a7","d7a7"],"themes":["endgame","long","advantage","advancedPawn"]}}]} '''; diff --git a/test/view/puzzle/puzzle_tab_screen_test.dart b/test/view/puzzle/puzzle_tab_screen_test.dart new file mode 100644 index 0000000000..32149feea2 --- /dev/null +++ b/test/view/puzzle/puzzle_tab_screen_test.dart @@ -0,0 +1,187 @@ +import 'package:chessground/chessground.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/testing.dart'; +import 'package:lichess_mobile/src/model/puzzle/puzzle_angle.dart'; +import 'package:lichess_mobile/src/model/puzzle/puzzle_batch_storage.dart'; +import 'package:lichess_mobile/src/model/puzzle/puzzle_theme.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/view/puzzle/puzzle_tab_screen.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../../model/puzzle/mock_server_responses.dart'; +import '../../network/fake_http_client_factory.dart'; +import '../../test_helpers.dart'; +import '../../test_provider_scope.dart'; +import 'example_data.dart'; + +final mockClient = MockClient((request) async { + if (request.url.path == '/api/puzzle/daily') { + return mockResponse(mockDailyPuzzleResponse, 200); + } + return mockResponse('', 404); +}); + +class MockPuzzleBatchStorage extends Mock implements PuzzleBatchStorage {} + +void main() { + setUpAll(() { + registerFallbackValue( + PuzzleBatch( + solved: IList(const []), + unsolved: IList([puzzle]), + ), + ); + }); + + final mockBatchStorage = MockPuzzleBatchStorage(); + + testWidgets('meets accessibility guidelines', (WidgetTester tester) async { + final SemanticsHandle handle = tester.ensureSemantics(); + + when( + () => mockBatchStorage.fetch( + userId: null, + angle: const PuzzleTheme(PuzzleThemeKey.mix), + ), + ).thenAnswer((_) async => batch); + + final app = await makeTestProviderScopeApp( + tester, + home: const PuzzleTabScreen(), + overrides: [ + puzzleBatchStorageProvider.overrideWith((ref) => mockBatchStorage), + ], + ); + + await tester.pumpWidget(app); + + // wait for connectivity + await tester.pump(const Duration(milliseconds: 100)); + + // wait for the puzzles to load + await tester.pump(const Duration(milliseconds: 100)); + + await meetsTapTargetGuideline(tester); + + await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); + + await expectLater(tester, meetsGuideline(textContrastGuideline)); + + handle.dispose(); + }); + + testWidgets('shows puzzle menu', (WidgetTester tester) async { + when( + () => mockBatchStorage.fetch( + userId: null, + angle: const PuzzleTheme(PuzzleThemeKey.mix), + ), + ).thenAnswer((_) async => batch); + final app = await makeTestProviderScopeApp( + tester, + home: const PuzzleTabScreen(), + overrides: [ + puzzleBatchStorageProvider.overrideWith((ref) => mockBatchStorage), + httpClientFactoryProvider.overrideWith((ref) { + return FakeHttpClientFactory(() => mockClient); + }), + ], + ); + + await tester.pumpWidget(app); + + // wait for connectivity + await tester.pumpAndSettle(const Duration(milliseconds: 100)); + + expect(find.text('Puzzle Themes'), findsOneWidget); + expect(find.text('Puzzle Streak'), findsOneWidget); + expect(find.text('Puzzle Storm'), findsOneWidget); + }); + + testWidgets('shows daily puzzle', (WidgetTester tester) async { + when( + () => mockBatchStorage.fetch( + userId: null, + angle: const PuzzleTheme(PuzzleThemeKey.mix), + ), + ).thenAnswer((_) async => batch); + + final app = await makeTestProviderScopeApp( + tester, + home: const PuzzleTabScreen(), + overrides: [ + puzzleBatchStorageProvider.overrideWith((ref) => mockBatchStorage), + httpClientFactoryProvider.overrideWith((ref) { + return FakeHttpClientFactory(() => mockClient); + }), + ], + ); + + await tester.pumpWidget(app); + + // wait for connectivity + await tester.pump(const Duration(milliseconds: 100)); + + // wait for the puzzles to load + await tester.pump(const Duration(milliseconds: 100)); + + expect(find.byType(DailyPuzzle), findsOneWidget); + expect( + find.widgetWithText(DailyPuzzle, 'Puzzle of the day'), + findsOneWidget, + ); + expect( + find.widgetWithText(DailyPuzzle, 'Played 93,270 times'), + findsOneWidget, + ); + expect(find.widgetWithText(DailyPuzzle, 'Black to play'), findsOneWidget); + }); + + group('tactical training preview', () { + testWidgets('shows first puzzle from unsolved batch', + (WidgetTester tester) async { + when( + () => mockBatchStorage.fetch( + userId: null, + angle: const PuzzleTheme(PuzzleThemeKey.mix), + ), + ).thenAnswer((_) async => batch); + + final app = await makeTestProviderScopeApp( + tester, + home: const PuzzleTabScreen(), + overrides: [ + puzzleBatchStorageProvider.overrideWith((ref) => mockBatchStorage), + httpClientFactoryProvider.overrideWith((ref) { + return FakeHttpClientFactory(() => mockClient); + }), + ], + ); + + await tester.pumpWidget(app); + + // wait for the puzzle to load + await tester.pump(const Duration(milliseconds: 100)); + + expect(find.byType(TacticalTrainingPreview), findsOneWidget); + expect( + find.widgetWithText(TacticalTrainingPreview, 'Chess tactics trainer'), + findsOneWidget, + ); + final chessboard = find + .descendant( + of: find.byType(TacticalTrainingPreview), + matching: find.byType(Chessboard), + ) + .evaluate() + .first + .widget as Chessboard; + + expect( + chessboard.fen, + equals('4k2r/Q5pp/3bp3/4n3/1r5q/8/PP2B1PP/R1B2R1K b k - 0 21'), + ); + }); + }); +} From 2a48da4510cbbaf55f6d9fb46242db694aaebc79 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 7 Oct 2024 15:56:11 +0200 Subject: [PATCH 459/979] Fix missing import --- lib/src/view/analysis/analysis_screen.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index 80ee90e7b6..89228ebf7c 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -18,6 +18,7 @@ import 'package:lichess_mobile/src/model/engine/engine.dart'; import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; import 'package:lichess_mobile/src/model/game/game_repository_providers.dart'; import 'package:lichess_mobile/src/model/game/game_share_service.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/connectivity.dart'; From bf0b00615c6211a687079c71c5280d303534f7b1 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 7 Oct 2024 16:06:10 +0200 Subject: [PATCH 460/979] Upgrade dependencies --- pubspec.lock | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index e7f235dc8d..c2f46a42b4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -519,10 +519,10 @@ packages: dependency: "direct main" description: name: flutter_appauth - sha256: e0c861713626a24ab31d3f03c7d04048e58cb30b01be9b4b5a05eee510f2d561 + sha256: "84e8753fe20864da241892823ff7dbd252baa34f1649d6feb48118e8ae829ed1" url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.1" flutter_appauth_platform_interface: dependency: transitive description: @@ -1015,10 +1015,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "6f01f8e37ec30b07bc424b4deabac37cacb1bc7e2e515ad74486039918a37eb7" + sha256: f7544c346a0742aee1450f9e5c0f5269d7c602b9c95fdbcd9fb8f5b1df13b1cc url: "https://pub.dev" source: hosted - version: "2.2.10" + version: "2.2.11" path_provider_foundation: dependency: transitive description: @@ -1199,18 +1199,18 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: "480ba4345773f56acda9abf5f50bd966f581dac5d514e5fc4a18c62976bbba7e" + sha256: "3b9febd815c9ca29c9e3520d50ec32f49157711e143b7a4ca039eb87e8ade5ab" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.3" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: c4b35f6cb8f63c147312c054ce7c2254c8066745125264f0c88739c417fc9d9f + sha256: "07e050c7cd39bad516f8d64c455f04508d09df104be326d8c02551590a0d513d" url: "https://pub.dev" source: hosted - version: "2.5.2" + version: "2.5.3" shared_preferences_linux: dependency: transitive description: @@ -1469,10 +1469,10 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: e35a698ac302dd68e41f73250bd9517fe3ab5fa4f18fe4647a0872db61bacbab + sha256: "8fc3bae0b68c02c47c5c86fa8bfa74471d42687b0eded01b78de87872db745e2" url: "https://pub.dev" source: hosted - version: "6.3.10" + version: "6.3.12" url_launcher_ios: dependency: transitive description: @@ -1645,10 +1645,10 @@ packages: dependency: transitive description: name: xdg_directories - sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.1.0" xml: dependency: transitive description: From 9c27a9b3e1f784d9eb89d0b0e0f7b51724116145 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 7 Oct 2024 16:56:54 +0200 Subject: [PATCH 461/979] Tweak tree view comments --- lib/src/view/analysis/tree_view.dart | 64 ++++++++++++++++++++-------- 1 file changed, 47 insertions(+), 17 deletions(-) diff --git a/lib/src/view/analysis/tree_view.dart b/lib/src/view/analysis/tree_view.dart index af43edf35a..8d91393abc 100644 --- a/lib/src/view/analysis/tree_view.dart +++ b/lib/src/view/analysis/tree_view.dart @@ -149,7 +149,9 @@ class _InlineTreeViewState extends ConsumerState { } /// A group of parameters that are passed through various parts of the tree view -/// and ultimately evaluated in the InlineMove widget. Grouped in this record to improve readability. +/// and ultimately evaluated in the [InlineMove] widget. +/// +/// Grouped in this record to improve readability. typedef _PgnTreeViewParams = ({ /// Path to the currently selected move in the tree. UciPath pathToCurrentMove, @@ -168,6 +170,8 @@ typedef _PgnTreeViewParams = ({ AnalysisController notifier, }); +/// Whether to display the sideline inline. +/// /// Sidelines are usually rendered on a new line and indented. /// However sidelines are rendered inline (in parantheses) if the side line has no branching and is less than 6 moves deep. bool _displaySideLineAsInline(ViewBranch node, [int depth = 0]) { @@ -183,6 +187,7 @@ bool _hasNonInlineSideLine(ViewNode node) => (node.children.length == 2 && !_displaySideLineAsInline(node.children[1])); /// Splits the mainline into parts, where each part is a sequence of moves that are displayed on the same line. +/// /// A part ends when a mainline node has a sideline that should not be displayed inline. Iterable> _mainlineParts(ViewRoot root) => [root, ...root.mainline] @@ -219,11 +224,17 @@ class _PgnTreeView extends StatefulWidget { State<_PgnTreeView> createState() => _PgnTreeViewState(); } -typedef _Subtree = ({ +/// A record that holds the rendered parts of a subtree. +typedef _RenderedSubtreeCache = ({ + /// The mainline part of the subtree. _MainLinePart mainLinePart, + /// The sidelines part of the subtree. + /// /// This is nullable since the very last mainline part might not have any sidelines. _IndentedSideLines? sidelines, + + /// Whether the subtree contains the current move. bool containsCurrentMove, }); @@ -232,10 +243,11 @@ class _PgnTreeViewState extends State<_PgnTreeView> { /// but not when `params.pathToCurrentMove` changes. List> mainlineParts = []; - /// Caches the top-level subtrees obtained from the last `build()` method, where each subtree is a [_MainLinePart] and its sidelines. - /// Building the whole tree is expensive, so we cache the subtrees that did not change when the current move changes, - /// the framework will then skip the `build()` of each subtree since the widget reference is the same. - List<_Subtree> subtrees = []; + /// Cache of the top-level subtrees obtained from the last `build()` method. + /// + /// Building the whole tree is expensive, so we cache the subtrees that did not change when the current move changes. + /// The framework will skip the `build()` of each subtree since the widget reference is the same. + List<_RenderedSubtreeCache> subtrees = []; UciPath _mainlinePartOfCurrentPath() { var path = UciPath.empty; @@ -422,6 +434,7 @@ enum _LineType { mainline, /// A sideline branching off the main line or a parent sideline. + /// /// Each sideline is rendered on a new line and indented. sideline, @@ -439,18 +452,20 @@ List _moveWithComment( required UciPath pathToNode, required _PgnTreeViewParams params, - /// Key that will be assigned to the move text. We use this to track the position of the first move of - /// a sideline, see [_SideLinePart.firstMoveKey] - GlobalKey? moveKey, + /// Optional [GlobalKey] that will be assigned to the [InlineMove] widget. + /// + /// It should only be set if it is the first move of a sideline. + /// We use this to track the position of the first move widget. See [_SideLinePart.firstMoveKey]. + GlobalKey? firstMoveKey, }) { return [ WidgetSpan( alignment: PlaceholderAlignment.middle, child: InlineMove( + key: firstMoveKey, branch: branch, lineInfo: lineInfo, path: pathToNode + branch.id, - key: moveKey, textStyle: textStyle, params: params, ), @@ -475,6 +490,7 @@ class _SideLinePart extends ConsumerWidget { final UciPath initialPath; /// The key that will be assigned to the first move in this sideline. + /// /// This is needed so that the indent guidelines can be drawn correctly. final GlobalKey firstMoveKey; @@ -495,7 +511,7 @@ class _SideLinePart extends ConsumerWidget { type: _LineType.sideline, startLine: true, ), - moveKey: firstMoveKey, + firstMoveKey: firstMoveKey, pathToNode: initialPath, textStyle: textStyle, params: params, @@ -538,7 +554,9 @@ class _SideLinePart extends ConsumerWidget { } } -/// A part of the mainline that will be rendered on the same line. See [_mainlineParts]. +/// A widget that renders part of the mainline. +/// +/// A part of the mainline is rendered on a single line. See [_mainlineParts]. class _MainLinePart extends ConsumerWidget { const _MainLinePart({ required this.initialPath, @@ -600,8 +618,11 @@ class _MainLinePart extends ConsumerWidget { } } -/// A sideline where the moves are rendered on the same line (see [_SideLinePart]) until further branching is encountered, -/// at which point the children sidelines are rendered on new lines and indented (see [_IndentedSideLines]). +/// A widget that renders a sideline. +/// +/// The moves are rendered on the same line (see [_SideLinePart]) until further +/// branching is encountered, at which point the children sidelines are rendered +/// on new lines and indented (see [_IndentedSideLines]). class _SideLine extends StatelessWidget { const _SideLine({ required this.firstNode, @@ -695,8 +716,11 @@ class _IndentPainter extends CustomPainter { } } -/// Displays one ore more sidelines indented on their own line and adds indent guides. -/// If there are hidden lines, a button is displayed to expand them. +/// A widget that displays indented sidelines. +/// +/// Will show one ore more sidelines indented on their own line and add indent +/// guides. +/// If there are hidden lines, a "+" button is displayed to expand them. class _IndentedSideLines extends StatefulWidget { const _IndentedSideLines( this.sideLines, { @@ -836,14 +860,20 @@ Color? _textColor( return nag != null && nag > 0 ? nagColor(nag) : defaultColor; } +/// A widget that displays a single move in the tree view. +/// +/// The move can optionnally be preceded by an index, and followed by a nag annotation. +/// The move is displayed as a clickable button that will jump to the move when pressed. +/// The move is highlighted if it is the current move. +/// A long press on the move will display a context menu with options to promote the move to the main line, collapse variations, etc. class InlineMove extends ConsumerWidget { const InlineMove({ required this.branch, required this.path, required this.textStyle, required this.lineInfo, - super.key, required this.params, + super.key, }); final ViewBranch branch; From 937d22dc19acd452bc037bd73f3e69abf6c66514 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 7 Oct 2024 17:21:22 +0200 Subject: [PATCH 462/979] Show lichess analysis comments by default --- lib/src/model/common/node.dart | 15 +++++++++------ lib/src/view/analysis/tree_view.dart | 28 +++++++++------------------- 2 files changed, 18 insertions(+), 25 deletions(-) diff --git a/lib/src/model/common/node.dart b/lib/src/model/common/node.dart index f60f812f13..ea9bd6c638 100644 --- a/lib/src/model/common/node.dart +++ b/lib/src/model/common/node.dart @@ -595,17 +595,20 @@ class ViewBranch extends ViewNode with _$ViewBranch { IList? nags, }) = _ViewBranch; + /// The text comments of this branch. + Iterable get textComments { + return [ + ...lichessAnalysisComments ?? IList(const []), + ...comments ?? IList(const []), + ].where((t) => t.text?.isNotEmpty == true).map((c) => c.text!); + } + /// Has at least one non empty starting comment text. bool get hasStartingTextComment => startingComments?.any((c) => c.text?.isNotEmpty == true) == true; /// Has at least one non empty comment text. - bool get hasTextComment => - comments?.any((c) => c.text?.isNotEmpty == true) == true; - - /// Has at least one non empty lichess analysis comment text. - bool get hasLichessAnalysisTextComment => - lichessAnalysisComments?.any((c) => c.text?.isNotEmpty == true) == true; + bool get hasTextComment => textComments.isNotEmpty; Duration? get clock { final clockComment = (lichessAnalysisComments ?? comments) diff --git a/lib/src/view/analysis/tree_view.dart b/lib/src/view/analysis/tree_view.dart index 8d91393abc..cb3a8b831c 100644 --- a/lib/src/view/analysis/tree_view.dart +++ b/lib/src/view/analysis/tree_view.dart @@ -353,8 +353,10 @@ class _PgnTreeViewState extends State<_PgnTreeView> { true) Text.rich( TextSpan( - children: - _comments(widget.rootComments!, textStyle: _baseTextStyle), + children: _comments( + widget.rootComments!.map((c) => c.text!), + textStyle: _baseTextStyle, + ), ), ), ...subtrees @@ -471,7 +473,7 @@ List _moveWithComment( ), ), if (params.shouldShowComments && branch.hasTextComment) - ..._comments(branch.comments!, textStyle: textStyle), + ..._comments(branch.textComments, textStyle: textStyle), ]; } @@ -1056,18 +1058,6 @@ class _MoveContextMenu extends ConsumerWidget { ], ), ), - if (branch.hasLichessAnalysisTextComment) - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 8.0, - ), - child: Text( - branch.lichessAnalysisComments! - .map((c) => c.text ?? '') - .join(' '), - ), - ), if (branch.hasTextComment) Padding( padding: const EdgeInsets.symmetric( @@ -1075,7 +1065,7 @@ class _MoveContextMenu extends ConsumerWidget { vertical: 8.0, ), child: Text( - branch.comments!.map((c) => c.text ?? '').join(' '), + branch.textComments.join(' '), ), ), const PlatformDivider(indent: 0), @@ -1107,20 +1097,20 @@ class _MoveContextMenu extends ConsumerWidget { } List _comments( - IList comments, { + Iterable comments, { required TextStyle textStyle, }) => comments .map( (comment) => TextSpan( - text: comment.text, + text: comment, style: textStyle.copyWith( fontSize: textStyle.fontSize! - 2.0, fontStyle: FontStyle.italic, ), ), ) - .toList(); + .toList(growable: false); class _OpeningHeaderDelegate extends SliverPersistentHeaderDelegate { const _OpeningHeaderDelegate( From 2eace6486d061310768d84537ebe9281bb2b0694 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Mon, 7 Oct 2024 15:43:45 +0000 Subject: [PATCH 463/979] format code --- lib/src/model/broadcast/broadcast_preferences.dart | 7 ++++--- lib/src/model/settings/preferences.dart | 1 - 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/src/model/broadcast/broadcast_preferences.dart b/lib/src/model/broadcast/broadcast_preferences.dart index 5a514732c1..51a7408ba6 100644 --- a/lib/src/model/broadcast/broadcast_preferences.dart +++ b/lib/src/model/broadcast/broadcast_preferences.dart @@ -8,7 +8,8 @@ part 'broadcast_preferences.freezed.dart'; part 'broadcast_preferences.g.dart'; @riverpod -class BroadcastPreferences extends _$BroadcastPreferences with PreferencesStorage { +class BroadcastPreferences extends _$BroadcastPreferences + with PreferencesStorage { // ignore: avoid_public_notifier_properties @override final prefCategory = PrefCategory.broadcast; @@ -30,8 +31,8 @@ class BroadcastPrefs with _$BroadcastPrefs implements SerializablePreferences { }) = _BroadcastPrefs; static const defaults = BroadcastPrefs( - showEvaluationBar: true, - ); + showEvaluationBar: true, + ); factory BroadcastPrefs.fromJson(Map json) => _$BroadcastPrefsFromJson(json); diff --git a/lib/src/model/settings/preferences.dart b/lib/src/model/settings/preferences.dart index fbb88debfa..d4fa7685ca 100644 --- a/lib/src/model/settings/preferences.dart +++ b/lib/src/model/settings/preferences.dart @@ -33,7 +33,6 @@ enum PrefCategory { puzzle('preferences.puzzle', null as PuzzlePrefs?), broadcast('preferences.broadcast', BroadcastPrefs.defaults); - const PrefCategory(this.storageKey, this._defaults); final String storageKey; From 2c9be6f771010483afe9c3be6bde913a4baac8ad Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Mon, 7 Oct 2024 22:09:48 +0200 Subject: [PATCH 464/979] tweak scroll behavior of overview tab --- .../broadcast/broadcast_overview_tab.dart | 110 +++++++++--------- 1 file changed, 57 insertions(+), 53 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_overview_tab.dart b/lib/src/view/broadcast/broadcast_overview_tab.dart index f4f395c9e2..f2564e612c 100644 --- a/lib/src/view/broadcast/broadcast_overview_tab.dart +++ b/lib/src/view/broadcast/broadcast_overview_tab.dart @@ -22,62 +22,66 @@ class BroadcastOverviewTab extends ConsumerWidget { return SafeArea( bottom: false, - child: Padding( - padding: Styles.bodyPadding, - child: tournament.when( - data: (tournament) { - final information = tournament.data.information; - final description = tournament.data.description; + child: SingleChildScrollView( + child: Padding( + padding: Styles.bodyPadding, + child: tournament.when( + data: (tournament) { + final information = tournament.data.information; + final description = tournament.data.description; - return Column( - children: [ - Wrap( - alignment: WrapAlignment.center, - children: [ - if (information.dates != null) - BroadcastOverviewCard( - CupertinoIcons.calendar, - information.dates!.endsAt == null - ? _dateFormatter.format(information.dates!.startsAt) - : '${_dateFormatter.format(information.dates!.startsAt)} - ${_dateFormatter.format(information.dates!.endsAt!)}', - ), - if (information.format != null) - BroadcastOverviewCard( - Icons.emoji_events, - '${information.format}', - ), - if (information.timeControl != null) - BroadcastOverviewCard( - CupertinoIcons.stopwatch_fill, - '${information.timeControl}', - ), - if (information.players != null) - BroadcastOverviewCard( - Icons.person, - '${information.players}', + return Column( + children: [ + Wrap( + alignment: WrapAlignment.center, + children: [ + if (information.dates != null) + BroadcastOverviewCard( + CupertinoIcons.calendar, + information.dates!.endsAt == null + ? _dateFormatter + .format(information.dates!.startsAt) + : '${_dateFormatter.format(information.dates!.startsAt)} - ${_dateFormatter.format(information.dates!.endsAt!)}', + ), + if (information.format != null) + BroadcastOverviewCard( + Icons.emoji_events, + '${information.format}', + ), + if (information.timeControl != null) + BroadcastOverviewCard( + CupertinoIcons.stopwatch_fill, + '${information.timeControl}', + ), + if (information.players != null) + BroadcastOverviewCard( + Icons.person, + '${information.players}', + ), + ], + ), + if (description != null) + Padding( + padding: const EdgeInsets.all(16), + child: MarkdownBody( + data: description, + onTapLink: (text, url, title) { + if (url == null) return; + launchUrl(Uri.parse(url)); + }, ), - ], - ), - if (description != null) - Expanded( - child: Markdown( - data: description, - onTapLink: (text, url, title) { - if (url == null) return; - launchUrl(Uri.parse(url)); - }, ), - ), - ], - ); - }, - loading: () => - const Center(child: CircularProgressIndicator.adaptive()), - error: (error, _) { - return Center( - child: Text('Cannot load game analysis: $error'), - ); - }, + ], + ); + }, + loading: () => + const Center(child: CircularProgressIndicator.adaptive()), + error: (error, _) { + return Center( + child: Text('Cannot load game analysis: $error'), + ); + }, + ), ), ), ); From 4307f4a86d508aafeae29b21164312671ac1c0ac Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Mon, 7 Oct 2024 23:09:56 +0200 Subject: [PATCH 465/979] swap text --- .../view/coordinate_training/coordinate_training_screen.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/view/coordinate_training/coordinate_training_screen.dart b/lib/src/view/coordinate_training/coordinate_training_screen.dart index 3fe2766195..9cbe16b555 100644 --- a/lib/src/view/coordinate_training/coordinate_training_screen.dart +++ b/lib/src/view/coordinate_training/coordinate_training_screen.dart @@ -389,7 +389,7 @@ class Settings extends ConsumerWidget { child: Column( children: [ Filter( - filterName: context.l10n.time, + filterName: context.l10n.side, filterType: FilterType.singleChoice, choices: SideChoice.values, showCheckmark: false, @@ -409,7 +409,7 @@ class Settings extends ConsumerWidget { const PlatformDivider(thickness: 1, indent: 0), const SizedBox(height: 12.0), Filter( - filterName: context.l10n.side, + filterName: context.l10n.time, filterType: FilterType.singleChoice, choices: TimeChoice.values, showCheckmark: false, From 0cf0a09ded10e7c0e00c0d247324ed7f5a059a0d Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 8 Oct 2024 12:46:03 +0200 Subject: [PATCH 466/979] Add a test to verify the caching mecanism of tree view --- .../model/analysis/analysis_controller.dart | 2 +- lib/src/model/analysis/opening_service.dart | 6 +- lib/src/view/analysis/analysis_board.dart | 5 +- lib/src/view/analysis/analysis_screen.dart | 44 +++++- lib/src/view/analysis/tree_view.dart | 84 +++++++---- test/model/analysis/fake_opening_service.dart | 11 ++ test/test_provider_scope.dart | 33 ++-- test/view/analysis/analysis_screen_test.dart | 142 ++++++++++++++++++ 8 files changed, 275 insertions(+), 52 deletions(-) create mode 100644 test/model/analysis/fake_opening_service.dart diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index 81a0a94711..5921cdaf0a 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -475,7 +475,7 @@ class AnalysisController extends _$AnalysisController { ); } - if (pathChange) { + if (pathChange && state.isEngineAvailable) { _debouncedStartEngineEval(); } } diff --git a/lib/src/model/analysis/opening_service.dart b/lib/src/model/analysis/opening_service.dart index ab850064a9..ea81058abe 100644 --- a/lib/src/model/analysis/opening_service.dart +++ b/lib/src/model/analysis/opening_service.dart @@ -20,11 +20,11 @@ OpeningService openingService(OpeningServiceRef ref) { } class OpeningService { - OpeningService(this.ref); + OpeningService(this._ref); - final OpeningServiceRef ref; + final OpeningServiceRef _ref; - Future get _db => ref.read(openingsDatabaseProvider.future); + Future get _db => _ref.read(openingsDatabaseProvider.future); Future fetchFromMoves(Iterable moves) async { final db = await _db; diff --git a/lib/src/view/analysis/analysis_board.dart b/lib/src/view/analysis/analysis_board.dart index 7ecca1eac5..7e6e1109ae 100644 --- a/lib/src/view/analysis/analysis_board.dart +++ b/lib/src/view/analysis/analysis_board.dart @@ -23,6 +23,7 @@ class AnalysisBoard extends ConsumerStatefulWidget { this.options, this.boardSize, { this.borderRadius, + this.enableDrawingShapes = true, }); final String pgn; @@ -30,6 +31,8 @@ class AnalysisBoard extends ConsumerStatefulWidget { final double boardSize; final BorderRadiusGeometry? borderRadius; + final bool enableDrawingShapes; + @override ConsumerState createState() => AnalysisBoardState(); } @@ -107,7 +110,7 @@ class AnalysisBoardState extends ConsumerState { ? boardShadows : const [], drawShape: DrawShapeOptions( - enable: true, + enable: widget.enableDrawingShapes, onCompleteShape: _onCompleteShape, onClearShapes: _onClearShapes, newShapeColor: boardPrefs.shapeColor.color, diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index 89228ebf7c..9a5c233d08 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -51,6 +51,7 @@ class AnalysisScreen extends StatelessWidget { const AnalysisScreen({ required this.options, required this.pgnOrId, + this.enableDrawingShapes = true, }); /// The analysis options. @@ -59,23 +60,36 @@ class AnalysisScreen extends StatelessWidget { /// The PGN or game ID to load. final String pgnOrId; + final bool enableDrawingShapes; + @override Widget build(BuildContext context) { return pgnOrId.length == 8 && GameId(pgnOrId).isValid - ? _LoadGame(GameId(pgnOrId), options) + ? _LoadGame( + GameId(pgnOrId), + options, + enableDrawingShapes: enableDrawingShapes, + ) : _LoadedAnalysisScreen( options: options, pgn: pgnOrId, + enableDrawingShapes: enableDrawingShapes, ); } } class _LoadGame extends ConsumerWidget { - const _LoadGame(this.gameId, this.options); + const _LoadGame( + this.gameId, + this.options, { + required this.enableDrawingShapes, + }); final AnalysisOptions options; final GameId gameId; + final bool enableDrawingShapes; + @override Widget build(BuildContext context, WidgetRef ref) { final gameAsync = ref.watch(archivedGameProvider(id: gameId)); @@ -94,6 +108,7 @@ class _LoadGame extends ConsumerWidget { serverAnalysis: serverAnalysis, ), pgn: game.makePgn(), + enableDrawingShapes: enableDrawingShapes, ); }, loading: () => const Center(child: CircularProgressIndicator.adaptive()), @@ -110,11 +125,14 @@ class _LoadedAnalysisScreen extends ConsumerWidget { const _LoadedAnalysisScreen({ required this.options, required this.pgn, + required this.enableDrawingShapes, }); final AnalysisOptions options; final String pgn; + final bool enableDrawingShapes; + @override Widget build(BuildContext context, WidgetRef ref) { return ConsumerPlatformWidget( @@ -146,7 +164,11 @@ class _LoadedAnalysisScreen extends ConsumerWidget { ), ], ), - body: _Body(pgn: pgn, options: options), + body: _Body( + pgn: pgn, + options: options, + enableDrawingShapes: enableDrawingShapes, + ), ); } @@ -176,7 +198,11 @@ class _LoadedAnalysisScreen extends ConsumerWidget { ], ), ), - child: _Body(pgn: pgn, options: options), + child: _Body( + pgn: pgn, + options: options, + enableDrawingShapes: enableDrawingShapes, + ), ); } } @@ -203,10 +229,15 @@ class _Title extends StatelessWidget { } class _Body extends ConsumerWidget { - const _Body({required this.pgn, required this.options}); + const _Body({ + required this.pgn, + required this.options, + required this.enableDrawingShapes, + }); final String pgn; final AnalysisOptions options; + final bool enableDrawingShapes; @override Widget build(BuildContext context, WidgetRef ref) { @@ -281,6 +312,7 @@ class _Body extends ConsumerWidget { options, boardSize, borderRadius: isTablet ? tabletBoardRadius : null, + enableDrawingShapes: enableDrawingShapes, ), if (hasEval && showEvaluationGauge) ...[ const SizedBox(width: 4.0), @@ -344,6 +376,7 @@ class _Body extends ConsumerWidget { options, boardSize, borderRadius: isTablet ? tabletBoardRadius : null, + enableDrawingShapes: enableDrawingShapes, ), ) else @@ -352,6 +385,7 @@ class _Body extends ConsumerWidget { options, boardSize, borderRadius: isTablet ? tabletBoardRadius : null, + enableDrawingShapes: enableDrawingShapes, ), Expanded( child: Padding( diff --git a/lib/src/view/analysis/tree_view.dart b/lib/src/view/analysis/tree_view.dart index cb3a8b831c..49896b67a8 100644 --- a/lib/src/view/analysis/tree_view.dart +++ b/lib/src/view/analysis/tree_view.dart @@ -225,7 +225,7 @@ class _PgnTreeView extends StatefulWidget { } /// A record that holds the rendered parts of a subtree. -typedef _RenderedSubtreeCache = ({ +typedef _CachedRenderedSubtree = ({ /// The mainline part of the subtree. _MainLinePart mainLinePart, @@ -247,7 +247,7 @@ class _PgnTreeViewState extends State<_PgnTreeView> { /// /// Building the whole tree is expensive, so we cache the subtrees that did not change when the current move changes. /// The framework will skip the `build()` of each subtree since the widget reference is the same. - List<_RenderedSubtreeCache> subtrees = []; + List<_CachedRenderedSubtree> subtrees = []; UciPath _mainlinePartOfCurrentPath() { var path = UciPath.empty; @@ -260,9 +260,11 @@ class _PgnTreeViewState extends State<_PgnTreeView> { return path; } - void _rebuildChangedSubtrees({required bool fullRebuild}) { + List<_CachedRenderedSubtree> _buildChangedSubtrees({ + required bool fullRebuild, + }) { var path = UciPath.empty; - subtrees = mainlineParts.mapIndexed( + return mainlineParts.mapIndexed( (i, mainlineNodes) { final mainlineInitialPath = path; @@ -312,16 +314,16 @@ class _PgnTreeViewState extends State<_PgnTreeView> { return subtrees[i]; } }, - ).toList(); + ).toList(growable: false); } void _updateLines({required bool fullRebuild}) { setState(() { if (fullRebuild) { - mainlineParts = _mainlineParts(widget.root).toList(); + mainlineParts = _mainlineParts(widget.root).toList(growable: false); } - _rebuildChangedSubtrees(fullRebuild: fullRebuild); + subtrees = _buildChangedSubtrees(fullRebuild: fullRebuild); }); } @@ -477,8 +479,9 @@ List _moveWithComment( ]; } -/// A part of a sideline where each node only has one child -/// (or two children where the second child is rendered as an inline sideline +/// A widget that renders part of a sideline, where each move is displayed on the same line without branching. +/// +/// Each node in the sideline has only one child (or two children where the second child is rendered as an inline sideline). class _SideLinePart extends ConsumerWidget { _SideLinePart( this.nodes, { @@ -614,7 +617,7 @@ class _MainLinePart extends ConsumerWidget { }, ) .flattened - .toList(), + .toList(growable: false), ), ); } @@ -642,32 +645,37 @@ class _SideLine extends StatelessWidget { final _PgnTreeViewParams params; final int nesting; - @override - Widget build(BuildContext context) { + List _getSidelinePartNodes() { final sidelineNodes = [firstNode]; while (sidelineNodes.last.children.isNotEmpty && !_hasNonInlineSideLine(sidelineNodes.last)) { sidelineNodes.add(sidelineNodes.last.children.first); } + return sidelineNodes.toList(growable: false); + } - final children = sidelineNodes.last.children; + @override + Widget build(BuildContext context) { + final sidelinePartNodes = _getSidelinePartNodes(); + + final lastNodeChildren = sidelinePartNodes.last.children; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _SideLinePart( - sidelineNodes.toList(), + sidelinePartNodes, firstMoveKey: firstMoveKey, initialPath: initialPath, params: params, ), - if (children.isNotEmpty) + if (lastNodeChildren.isNotEmpty) _IndentedSideLines( - children, - parent: sidelineNodes.last, + lastNodeChildren, + parent: sidelinePartNodes.last, initialPath: UciPath.join( initialPath, - UciPath.fromIds(sidelineNodes.map((node) => node.id)), + UciPath.fromIds(sidelinePartNodes.map((node) => node.id)), ), params: params, nesting: nesting + 1, @@ -747,15 +755,29 @@ class _IndentedSideLines extends StatefulWidget { } class _IndentedSideLinesState extends State<_IndentedSideLines> { - late List _keys; - + /// Keys for the first move of each sideline. + /// + /// Used to calculate the position of the indent guidelines. The keys are + /// assigned to the first move of each sideline. The position of the keys is + /// used to calculate the position of the indent guidelines. A [GlobalKey] is + /// necessary because the exact position of the first move is not known until the + /// widget is rendered, as the vertical space can vary depending on the length of + /// the line, and if the line is wrapped. + late List _sideLinesStartKeys; + + /// The position of the first move of each sideline computed relative to the column and derived from the [GlobalKey] found in [_sideLinesStartKeys]. List _sideLineStartPositions = []; + /// The [GlobalKey] for the column that contains the side lines. final GlobalKey _columnKey = GlobalKey(); + /// Redraws the indents on demand. + /// + /// Will re-generate the [GlobalKey]s for the first move of each sideline and + /// calculate the position of the indents in a post-frame callback. void _redrawIndents() { - _keys = List.generate( - _visibleSideLines().length + (_hasHiddenLines() ? 1 : 0), + _sideLinesStartKeys = List.generate( + _visibleSideLines.length + (_hasHiddenLines ? 1 : 0), (_) => GlobalKey(), ); WidgetsBinding.instance.addPostFrameCallback((_) { @@ -764,13 +786,13 @@ class _IndentedSideLinesState extends State<_IndentedSideLines> { final Offset rowOffset = columnBox?.localToGlobal(Offset.zero) ?? Offset.zero; - final positions = _keys.map((key) { + final positions = _sideLinesStartKeys.map((key) { final context = key.currentContext; final renderBox = context?.findRenderObject() as RenderBox?; final height = renderBox?.size.height ?? 0; final offset = renderBox?.localToGlobal(Offset.zero) ?? Offset.zero; return Offset(offset.dx, offset.dy + height / 2) - rowOffset; - }).toList(); + }).toList(growable: false); setState(() { _sideLineStartPositions = positions; @@ -778,9 +800,9 @@ class _IndentedSideLinesState extends State<_IndentedSideLines> { }); } - bool _hasHiddenLines() => widget.sideLines.any((node) => node.isHidden); + bool get _hasHiddenLines => widget.sideLines.any((node) => node.isHidden); - Iterable _visibleSideLines() => + Iterable get _visibleSideLines => widget.sideLines.whereNot((node) => node.isHidden); @override @@ -799,18 +821,18 @@ class _IndentedSideLinesState extends State<_IndentedSideLines> { @override Widget build(BuildContext context) { - final sideLineWidgets = _visibleSideLines() + final sideLineWidgets = _visibleSideLines .mapIndexed( (i, firstSidelineNode) => _SideLine( firstNode: firstSidelineNode, parent: widget.parent, - firstMoveKey: _keys[i], + firstMoveKey: _sideLinesStartKeys[i], initialPath: widget.initialPath, params: widget.params, nesting: widget.nesting, ), ) - .toList(); + .toList(growable: false); final padding = widget.nesting < 6 ? 12.0 : 0.0; @@ -827,12 +849,12 @@ class _IndentedSideLinesState extends State<_IndentedSideLines> { crossAxisAlignment: CrossAxisAlignment.start, children: [ ...sideLineWidgets, - if (_hasHiddenLines()) + if (_hasHiddenLines) GestureDetector( child: Icon( Icons.add_box, color: _textColor(context, 0.6), - key: _keys.last, + key: _sideLinesStartKeys.last, size: _baseTextStyle.fontSize! + 5, ), onTap: () { diff --git a/test/model/analysis/fake_opening_service.dart b/test/model/analysis/fake_opening_service.dart new file mode 100644 index 0000000000..ffe1aa6cc1 --- /dev/null +++ b/test/model/analysis/fake_opening_service.dart @@ -0,0 +1,11 @@ +import 'package:dartchess/src/models.dart'; +import 'package:lichess_mobile/src/model/analysis/opening_service.dart'; +import 'package:lichess_mobile/src/model/common/chess.dart'; + +class FakeOpeningService implements OpeningService { + @override + Future fetchFromMoves(Iterable moves) { + // TODO: implement fetchFromMoves when needed + return Future.value(null); + } +} diff --git a/test/test_provider_scope.dart b/test/test_provider_scope.dart index e081e2df01..088256e265 100644 --- a/test/test_provider_scope.dart +++ b/test/test_provider_scope.dart @@ -11,6 +11,7 @@ import 'package:lichess_mobile/l10n/l10n.dart'; import 'package:lichess_mobile/src/crashlytics.dart'; import 'package:lichess_mobile/src/db/database.dart'; import 'package:lichess_mobile/src/model/account/account_preferences.dart'; +import 'package:lichess_mobile/src/model/analysis/opening_service.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/auth/session_storage.dart'; import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; @@ -25,6 +26,7 @@ import 'package:visibility_detector/visibility_detector.dart'; import './fake_crashlytics.dart'; import './model/common/service/fake_sound_service.dart'; import 'binding.dart'; +import 'model/analysis/fake_opening_service.dart'; import 'model/notifications/fake_notification_display.dart'; import 'network/fake_http_client_factory.dart'; import 'network/fake_websocket_channel.dart'; @@ -93,18 +95,25 @@ Future makeTestProviderScope( VisibilityDetectorController.instance.updateInterval = Duration.zero; + // disable piece animation and drawing shapes to simplify tests + final defaultBoardPref = { + 'preferences.board': jsonEncode( + BoardPrefs.defaults + .copyWith( + pieceAnimation: false, + enableShapeDrawings: false, + ) + .toJson(), + ), + }; + await binding.setInitialSharedPreferencesValues( - defaultPreferences ?? - { - // disable piece animation to simplify tests - 'preferences.board': jsonEncode( - BoardPrefs.defaults - .copyWith( - pieceAnimation: false, - ) - .toJson(), - ), - }, + defaultPreferences != null + ? { + ...defaultBoardPref, + ...defaultPreferences, + } + : defaultBoardPref, ); await binding.preloadData(userSession); @@ -155,6 +164,8 @@ Future makeTestProviderScope( crashlyticsProvider.overrideWithValue(FakeCrashlytics()), // ignore: scoped_providers_should_specify_dependencies soundServiceProvider.overrideWithValue(FakeSoundService()), + // ignore: scoped_providers_should_specify_dependencies + openingServiceProvider.overrideWithValue(FakeOpeningService()), ...overrides ?? [], ], child: MediaQuery( diff --git a/test/view/analysis/analysis_screen_test.dart b/test/view/analysis/analysis_screen_test.dart index 6d4fde1c06..b81f4f4892 100644 --- a/test/view/analysis/analysis_screen_test.dart +++ b/test/view/analysis/analysis_screen_test.dart @@ -5,6 +5,7 @@ import 'package:dartchess/dartchess.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; @@ -12,6 +13,7 @@ import 'package:lichess_mobile/src/model/common/speed.dart'; import 'package:lichess_mobile/src/model/game/archived_game.dart'; import 'package:lichess_mobile/src/model/game/game_status.dart'; import 'package:lichess_mobile/src/model/game/player.dart'; +import 'package:lichess_mobile/src/model/settings/preferences.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; import 'package:lichess_mobile/src/view/analysis/tree_view.dart'; @@ -121,6 +123,15 @@ void main() { ) async { final app = await makeTestProviderScopeApp( tester, + defaultPreferences: { + PrefCategory.analysis.storageKey: jsonEncode( + AnalysisPrefs.defaults + .copyWith( + enableLocalEvaluation: false, + ) + .toJson(), + ), + }, home: AnalysisScreen( pgnOrId: pgn, options: const AnalysisOptions( @@ -130,6 +141,7 @@ void main() { opening: opening, id: standaloneAnalysisId, ), + enableDrawingShapes: false, ), ); @@ -252,6 +264,136 @@ void main() { expectSameLine(tester, ['2… Nc6', '3. d3']); expectSameLine(tester, ['2… Qd7']); }); + + testWidgets('subtrees not part of the current mainline part are cached', + (tester) async { + await buildTree( + tester, + '1. e4 e5 (1... d5 2. exd5) (1... Nf6 2. e5) 2. Nf3 Nc6 (2... a5) *', + ); + + // will be rendered as: + // ------------------- + // 1. e4 e5 <-- first mainline part + // |- 1... d5 2. exd5 + // |- 1... Nf6 2. e5 + // 2. Nf3 Nc6 (2... a5) <-- second mainline part + // ^ + // | + // current move + + final firstMainlinePart = parentText(tester, '1. e4'); + final secondMainlinePart = parentText(tester, '2. Nf3'); + + expect( + tester + .widgetList( + find.ancestor( + of: find.textContaining('Nc6'), + matching: find.byType(InlineMove), + ), + ) + .last + .isCurrentMove, + isTrue, + ); + + await tester.tap(find.byKey(const Key('goto-previous'))); + // need to wait for current move change debounce delay + await tester.pump(const Duration(milliseconds: 200)); + + expect( + tester + .widgetList( + find.ancestor( + of: find.textContaining('Nf3'), + matching: find.byType(InlineMove), + ), + ) + .last + .isCurrentMove, + isTrue, + ); + + // first mainline part has not changed since the current move is + // not part of it + expect( + identical(firstMainlinePart, parentText(tester, '1. e4')), + isTrue, + ); + + final secondMainlinePartOnMoveNf3 = parentText(tester, '2. Nf3'); + + // second mainline part has changed since the current move is part of it + expect( + secondMainlinePart, + isNot(secondMainlinePartOnMoveNf3), + ); + + await tester.tap(find.byKey(const Key('goto-previous'))); + // need to wait for current move change debounce delay + await tester.pump(const Duration(milliseconds: 200)); + + expect( + tester + .widgetList( + find.ancestor( + of: find.textContaining('e5'), + matching: find.byType(InlineMove), + ), + ) + .first + .isCurrentMove, + isTrue, + ); + + final firstMainlinePartOnMoveE5 = parentText(tester, '1. e4'); + final secondMainlinePartOnMoveE5 = parentText(tester, '2. Nf3'); + + // first mainline part has changed since the current move is part of it + expect( + firstMainlinePart, + isNot(firstMainlinePartOnMoveE5), + ); + + // second mainline part has changed since the current move is not part of it + // anymore + expect( + secondMainlinePartOnMoveNf3, + isNot(secondMainlinePartOnMoveE5), + ); + + await tester.tap(find.byKey(const Key('goto-previous'))); + // need to wait for current move change debounce delay + await tester.pump(const Duration(milliseconds: 200)); + + expect( + tester + .firstWidget( + find.ancestor( + of: find.textContaining('e4'), + matching: find.byType(InlineMove), + ), + ) + .isCurrentMove, + isTrue, + ); + + final firstMainlinePartOnMoveE4 = parentText(tester, '1. e4'); + final secondMainlinePartOnMoveE4 = parentText(tester, '2. Nf3'); + + // first mainline part has changed since the current move is part of it + expect( + firstMainlinePartOnMoveE4, + isNot(firstMainlinePartOnMoveE5), + ); + + // second mainline part has not changed since the current move is not part of it + expect( + identical(secondMainlinePartOnMoveE5, secondMainlinePartOnMoveE4), + isTrue, + ); + }); }); }); } From d1c50dbe5fbff06e621cd787b55f3de475846c3f Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 8 Oct 2024 12:58:17 +0200 Subject: [PATCH 467/979] Update podfile --- ios/Podfile.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 19ef3e555f..b24aa22cfb 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,11 +1,11 @@ PODS: - app_settings (5.1.1): - Flutter - - AppAuth (1.7.4): - - AppAuth/Core (= 1.7.4) - - AppAuth/ExternalUserAgent (= 1.7.4) - - AppAuth/Core (1.7.4) - - AppAuth/ExternalUserAgent (1.7.4): + - AppAuth (1.7.5): + - AppAuth/Core (= 1.7.5) + - AppAuth/ExternalUserAgent (= 1.7.5) + - AppAuth/Core (1.7.5) + - AppAuth/ExternalUserAgent (1.7.5): - AppAuth/Core - connectivity_plus (0.0.1): - Flutter @@ -76,7 +76,7 @@ PODS: - PromisesSwift (~> 2.1) - Flutter (1.0.0) - flutter_appauth (0.0.1): - - AppAuth (= 1.7.4) + - AppAuth (= 1.7.5) - Flutter - flutter_local_notifications (0.0.1): - Flutter @@ -228,7 +228,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: app_settings: 017320c6a680cdc94c799949d95b84cb69389ebc - AppAuth: 182c5b88630569df5acb672720534756c29b3358 + AppAuth: 501c04eda8a8d11f179dbe8637b7a91bb7e5d2fa connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db cupertino_http: 1a3a0f163c1b26e7f1a293b33d476e0fde7a64ec device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d @@ -245,7 +245,7 @@ SPEC CHECKSUMS: FirebaseRemoteConfigInterop: c3a5c31b3c22079f41ba1dc645df889d9ce38cb9 FirebaseSessions: 655ff17f3cc1a635cbdc2d69b953878001f9e25b Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - flutter_appauth: 1ce438877bc111c5d8f42da47729909290624886 + flutter_appauth: aef998cfbcc307dff7f2fbe1f59a50323748dc25 flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086 flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778 flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12 From 1edbfc5e9cb409ee2b485869e167795115154d7b Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Mon, 7 Oct 2024 10:42:53 +0200 Subject: [PATCH 468/979] refactor: make PgnTreeView independent of the AnalysisController --- .../model/analysis/analysis_controller.dart | 9 +- lib/src/view/analysis/tree_view.dart | 197 +++++++++++------- test/view/analysis/analysis_screen_test.dart | 6 +- 3 files changed, 130 insertions(+), 82 deletions(-) diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index 5921cdaf0a..97b88bdc36 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -19,6 +19,7 @@ import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; import 'package:lichess_mobile/src/model/engine/work.dart'; import 'package:lichess_mobile/src/model/game/player.dart'; import 'package:lichess_mobile/src/utils/rate_limit.dart'; +import 'package:lichess_mobile/src/view/analysis/tree_view.dart'; import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -60,7 +61,8 @@ class AnalysisOptions with _$AnalysisOptions { } @riverpod -class AnalysisController extends _$AnalysisController { +class AnalysisController extends _$AnalysisController + implements PgnTreeNotifier { late Root _root; final _engineEvalDebounce = Debouncer(const Duration(milliseconds: 150)); @@ -261,10 +263,12 @@ class AnalysisController extends _$AnalysisController { _setPath(state.currentPath.penultimate, replaying: true); } + @override void userJump(UciPath path) { _setPath(path); } + @override void expandVariations(UciPath path) { final node = _root.nodeAt(path); for (final child in node.children) { @@ -276,6 +280,7 @@ class AnalysisController extends _$AnalysisController { state = state.copyWith(root: _root.view); } + @override void collapseVariations(UciPath path) { final node = _root.parentAt(path); @@ -286,6 +291,7 @@ class AnalysisController extends _$AnalysisController { state = state.copyWith(root: _root.view); } + @override void promoteVariation(UciPath path, bool toMainline) { _root.promoteAt(path, toMainline: toMainline); state = state.copyWith( @@ -294,6 +300,7 @@ class AnalysisController extends _$AnalysisController { ); } + @override void deleteFromHere(UciPath path) { _root.deleteAt(path); _setPath(path.penultimate, shouldRecomputeRootView: true); diff --git a/lib/src/view/analysis/tree_view.dart b/lib/src/view/analysis/tree_view.dart index 49896b67a8..498970d93c 100644 --- a/lib/src/view/analysis/tree_view.dart +++ b/lib/src/view/analysis/tree_view.dart @@ -26,7 +26,7 @@ const kFastReplayDebounceDelay = Duration(milliseconds: 150); const kOpeningHeaderHeight = 32.0; const kInlineMoveSpacing = 3.0; -class AnalysisTreeView extends ConsumerStatefulWidget { +class AnalysisTreeView extends ConsumerWidget { const AnalysisTreeView( this.pgn, this.options, @@ -38,22 +38,94 @@ class AnalysisTreeView extends ConsumerStatefulWidget { final Orientation displayMode; @override - ConsumerState createState() => _InlineTreeViewState(); + Widget build(BuildContext context, WidgetRef ref) { + final ctrlProvider = analysisControllerProvider(pgn, options); + + final root = ref.watch(ctrlProvider.select((value) => value.root)); + final currentPath = + ref.watch(ctrlProvider.select((value) => value.currentPath)); + final pgnRootComments = + ref.watch(ctrlProvider.select((value) => value.pgnRootComments)); + + return CustomScrollView( + slivers: [ + if (kOpeningAllowedVariants.contains(options.variant)) + SliverPersistentHeader( + delegate: _OpeningHeaderDelegate( + ctrlProvider, + displayMode: displayMode, + ), + ), + SliverFillRemaining( + hasScrollBody: false, + child: DebouncedPgnTreeView( + root: root, + currentPath: currentPath, + pgnRootComments: pgnRootComments, + notifier: ref.read(ctrlProvider.notifier), + ), + ), + ], + ); + } +} + +/// Callbacks for interaction with [DebouncedPgnTreeView] +abstract class PgnTreeNotifier { + void expandVariations(UciPath path); + void collapseVariations(UciPath path); + void promoteVariation(UciPath path, bool toMainLine); + void deleteFromHere(UciPath path); + void userJump(UciPath path); +} + +/// Displays a tree-like view of a PGN game's moves. Path changes are debounced to avoid rebuilding the whole tree on every move. +/// +/// For example, the PGN 1. e4 e5 (1... d5) (1... Nc6) 2. Nf3 Nc6 (2... a5) 3. Bc4 * will be displayed as: +/// 1. e4 e5 // [_MainLinePart] +/// |- 1... d5 // [_SideLinePart] +/// |- 1... Nc6 // [_SideLinePart] +/// 2. Nf3 Nc6 (2... a5) 3. Bc4 // [_MainLinePart], with inline sideline +/// Short sidelines without any branching are displayed inline with their parent line. +/// Longer sidelines are displayed on a new line and indented. +/// The mainline is split into parts whenever a move has a non-inline sideline, this corresponds to the [_MainLinePart] widget. +/// Similarly, a [_SideLinePart] contains the moves sequence of a sideline where each node has only one child. +class DebouncedPgnTreeView extends ConsumerStatefulWidget { + const DebouncedPgnTreeView({ + required this.root, + required this.currentPath, + required this.pgnRootComments, + required this.notifier, + }); + + /// Root of the PGN tree to display + final ViewRoot root; + + /// Path to the currently selected move in the tree + final UciPath currentPath; + + /// Comments associated with the root node + final IList? pgnRootComments; + + /// Callbacks for when the user interacts with the tree view, e.g. selecting a different move or collapsing variations + final PgnTreeNotifier notifier; + + @override + ConsumerState createState() => + _DebouncedPgnTreeViewState(); } -class _InlineTreeViewState extends ConsumerState { +class _DebouncedPgnTreeViewState extends ConsumerState { final currentMoveKey = GlobalKey(); final _debounce = Debouncer(kFastReplayDebounceDelay); + + /// Path to the currently selected move in the tree. When widget.currentPath changes rapidly, we debounce the change to avoid rebuilding the whole tree on every move. late UciPath pathToCurrentMove; @override void initState() { super.initState(); - pathToCurrentMove = ref.read( - analysisControllerProvider(widget.pgn, widget.options).select( - (value) => value.currentPath, - ), - ); + pathToCurrentMove = widget.currentPath; WidgetsBinding.instance.addPostFrameCallback((_) { if (currentMoveKey.currentContext != null) { Scrollable.ensureVisible( @@ -71,7 +143,33 @@ class _InlineTreeViewState extends ConsumerState { super.dispose(); } - // This is the most expensive part of the analysis view because of the tree + @override + void didUpdateWidget(covariant DebouncedPgnTreeView oldWidget) { + super.didUpdateWidget(oldWidget); + + if (oldWidget.currentPath != widget.currentPath) { + // debouncing the current path change to avoid rebuilding when using + // the fast replay buttons + _debounce(() { + setState(() { + pathToCurrentMove = widget.currentPath; + }); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (currentMoveKey.currentContext != null) { + Scrollable.ensureVisible( + currentMoveKey.currentContext!, + duration: const Duration(milliseconds: 200), + curve: Curves.easeIn, + alignment: 0.5, + alignmentPolicy: ScrollPositionAlignmentPolicy.explicit, + ); + } + }); + }); + } + } + + // This is the most expensive part of the pgn tree view because of the tree // that may be very large. // Great care must be taken to avoid unnecessary rebuilds. // This should actually rebuild only when the current path changes or a new node @@ -80,38 +178,6 @@ class _InlineTreeViewState extends ConsumerState { // using the fast replay buttons. @override Widget build(BuildContext context) { - ref.listen( - analysisControllerProvider(widget.pgn, widget.options), - (prev, state) { - if (prev?.currentPath != state.currentPath) { - // debouncing the current path change to avoid rebuilding when using - // the fast replay buttons - _debounce(() { - setState(() { - pathToCurrentMove = state.currentPath; - }); - WidgetsBinding.instance.addPostFrameCallback((_) { - if (currentMoveKey.currentContext != null) { - Scrollable.ensureVisible( - currentMoveKey.currentContext!, - duration: const Duration(milliseconds: 200), - curve: Curves.easeIn, - alignment: 0.5, - alignmentPolicy: ScrollPositionAlignmentPolicy.explicit, - ); - } - }); - }); - } - }, - ); - - final ctrlProvider = analysisControllerProvider(widget.pgn, widget.options); - final root = ref.watch(ctrlProvider.select((value) => value.root)); - final rootComments = ref.watch( - ctrlProvider.select((value) => value.pgnRootComments), - ); - final shouldShowComments = ref.watch( analysisPreferencesProvider.select((value) => value.showPgnComments), ); @@ -120,30 +186,16 @@ class _InlineTreeViewState extends ConsumerState { analysisPreferencesProvider.select((value) => value.showAnnotations), ); - return CustomScrollView( - slivers: [ - if (kOpeningAllowedVariants.contains(widget.options.variant)) - SliverPersistentHeader( - delegate: _OpeningHeaderDelegate( - ctrlProvider, - displayMode: widget.displayMode, - ), - ), - SliverFillRemaining( - hasScrollBody: false, - child: _PgnTreeView( - root: root, - rootComments: rootComments, - params: ( - shouldShowAnnotations: shouldShowAnnotations, - shouldShowComments: shouldShowComments, - currentMoveKey: currentMoveKey, - pathToCurrentMove: pathToCurrentMove, - notifier: ref.read(ctrlProvider.notifier), - ), - ), - ), - ], + return _PgnTreeView( + root: widget.root, + rootComments: widget.pgnRootComments, + params: ( + shouldShowAnnotations: shouldShowAnnotations, + shouldShowComments: shouldShowComments, + currentMoveKey: currentMoveKey, + pathToCurrentMove: pathToCurrentMove, + notifier: widget.notifier, + ), ); } } @@ -167,7 +219,7 @@ typedef _PgnTreeViewParams = ({ GlobalKey currentMoveKey, /// Callbacks for when the user interacts with the tree view, e.g. selecting a different move. - AnalysisController notifier, + PgnTreeNotifier notifier, }); /// Whether to display the sideline inline. @@ -194,17 +246,6 @@ Iterable> _mainlineParts(ViewRoot root) => .splitAfter(_hasNonInlineSideLine) .takeWhile((nodes) => nodes.firstOrNull?.children.isNotEmpty == true); -/// Displays a tree-like view of a PGN game's moves. -/// -/// For example, the PGN 1. e4 e5 (1... d5) (1... Nc6) 2. Nf3 Nc6 (2... a5) 3. Bc4 * will be displayed as: -/// 1. e4 e5 // [_MainLinePart] -/// |- 1... d5 // [_SideLinePart] -/// |- 1... Nc6 // [_SideLinePart] -/// 2. Nf3 Nc6 (2... a5) 3. Bc4 // [_MainLinePart], with inline sideline -/// Short sidelines without any branching are displayed inline with their parent line. -/// Longer sidelines are displayed on a new line and indented. -/// The mainline is split into parts whenever a move has a non-inline sideline, this corresponds to the [_MainLinePart] widget. -/// Similarly, a [_SideLinePart] contains the moves sequence of a sideline where each node has only one child. class _PgnTreeView extends StatefulWidget { const _PgnTreeView({ required this.root, @@ -1024,7 +1065,7 @@ class _MoveContextMenu extends ConsumerWidget { final UciPath path; final ViewBranch branch; final bool isSideline; - final AnalysisController notifier; + final PgnTreeNotifier notifier; @override Widget build(BuildContext context, WidgetRef ref) { diff --git a/test/view/analysis/analysis_screen_test.dart b/test/view/analysis/analysis_screen_test.dart index b81f4f4892..f510ef1516 100644 --- a/test/view/analysis/analysis_screen_test.dart +++ b/test/view/analysis/analysis_screen_test.dart @@ -300,7 +300,7 @@ void main() { await tester.tap(find.byKey(const Key('goto-previous'))); // need to wait for current move change debounce delay - await tester.pump(const Duration(milliseconds: 200)); + await tester.pumpAndSettle(const Duration(milliseconds: 200)); expect( tester @@ -332,7 +332,7 @@ void main() { await tester.tap(find.byKey(const Key('goto-previous'))); // need to wait for current move change debounce delay - await tester.pump(const Duration(milliseconds: 200)); + await tester.pumpAndSettle(const Duration(milliseconds: 200)); expect( tester @@ -365,7 +365,7 @@ void main() { await tester.tap(find.byKey(const Key('goto-previous'))); // need to wait for current move change debounce delay - await tester.pump(const Duration(milliseconds: 200)); + await tester.pumpAndSettle(const Duration(milliseconds: 200)); expect( tester From 3bdd4bfc31e84e28b8db5b931b2963ad92a19fb6 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Tue, 8 Oct 2024 23:43:05 +0200 Subject: [PATCH 469/979] refactor and remove new training button --- .../coordinate_training_controller.dart | 13 ++--- .../coordinate_training_screen.dart | 53 ++++++++++++------- 2 files changed, 39 insertions(+), 27 deletions(-) diff --git a/lib/src/model/coordinate_training/coordinate_training_controller.dart b/lib/src/model/coordinate_training/coordinate_training_controller.dart index 149f913a38..f0e7bf5d93 100644 --- a/lib/src/model/coordinate_training/coordinate_training_controller.dart +++ b/lib/src/model/coordinate_training/coordinate_training_controller.dart @@ -60,12 +60,14 @@ class CoordinateTrainingController extends _$CoordinateTrainingController { void _finishTraining() { // TODO save score in local storage here (and display high score and/or average score in UI) - + final orientation = _getOrientation( + ref.read(coordinateTrainingPreferencesProvider).sideChoice, + ); _updateTimer?.cancel(); state = CoordinateTrainingState( lastGuess: state.lastGuess, lastScore: state.score, - orientation: state.orientation, + orientation: orientation, ); } @@ -77,13 +79,6 @@ class CoordinateTrainingController extends _$CoordinateTrainingController { state = CoordinateTrainingState(orientation: orientation); } - void newTraining() { - final orientation = _getOrientation( - ref.read(coordinateTrainingPreferencesProvider).sideChoice, - ); - state = CoordinateTrainingState(orientation: orientation); - } - Side _getOrientation(SideChoice choice) => switch (choice) { SideChoice.white => Side.white, SideChoice.black => Side.black, diff --git a/lib/src/view/coordinate_training/coordinate_training_screen.dart b/lib/src/view/coordinate_training/coordinate_training_screen.dart index 9cbe16b555..69b6e68730 100644 --- a/lib/src/view/coordinate_training/coordinate_training_screen.dart +++ b/lib/src/view/coordinate_training/coordinate_training_screen.dart @@ -155,18 +155,21 @@ class _BodyState extends ConsumerState<_Body> { _ScoreAndTrainingButton( scoreSize: boardSize / 8, score: trainingState.lastScore!, - onPressed: ref - .read( - coordinateTrainingControllerProvider.notifier, - ) - .newTraining, + onPressed: () { + ref + .read( + coordinateTrainingControllerProvider.notifier, + ) + .startTraining( + trainingPrefs.timeChoice.duration, + ); + }, label: 'New Training', ) else Expanded( child: Center( - child: FatButton( - semanticsLabel: 'Start Training', + child: _Button( onPressed: () { ref .read( @@ -177,11 +180,7 @@ class _BodyState extends ConsumerState<_Body> { trainingPrefs.timeChoice.duration, ); }, - child: const Text( - // TODO l10n once script works - 'Start Training', - style: Styles.bold, - ), + label: 'Start Training', ), ), ), @@ -319,13 +318,9 @@ class _ScoreAndTrainingButton extends ConsumerWidget { ? context.lichessColors.error : context.lichessColors.good, ), - FatButton( - semanticsLabel: label, + _Button( + label: label, onPressed: onPressed, - child: Text( - label, - style: Styles.bold, - ), ), ], ), @@ -377,6 +372,28 @@ class _Score extends StatelessWidget { } } +class _Button extends StatelessWidget { + const _Button({ + required this.onPressed, + required this.label, + }); + + final VoidCallback onPressed; + final String label; + + @override + Widget build(BuildContext context) { + return FatButton( + semanticsLabel: label, + onPressed: onPressed, + child: Text( + label, + style: Styles.bold, + ), + ); + } +} + class Settings extends ConsumerWidget { const Settings(); From 634554b19b3508a9bb58a7715432f11800a5330b Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 11 Oct 2024 11:02:32 +0200 Subject: [PATCH 470/979] Use a global cache to preload chess pieces --- lib/main.dart | 2 + lib/src/init.dart | 17 ++ lib/src/model/settings/board_preferences.dart | 6 +- lib/src/utils/chessboard.dart | 23 +++ .../view/puzzle/puzzle_feedback_widget.dart | 3 +- lib/src/view/settings/piece_set_screen.dart | 160 +++++++++--------- pubspec.lock | 8 +- pubspec.yaml | 4 +- 8 files changed, 132 insertions(+), 91 deletions(-) create mode 100644 lib/src/utils/chessboard.dart diff --git a/lib/main.dart b/lib/main.dart index 1cd7218f2a..ed20196deb 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -27,6 +27,8 @@ Future main() async { await lichessBinding.preloadSharedPreferences(); await lichessBinding.preloadData(); + await preloadPieceImages(); + await setupFirstLaunch(); await SoundService.initialize(); diff --git a/lib/src/init.dart b/lib/src/init.dart index 837a5a0c60..628cc86cfd 100644 --- a/lib/src/init.dart +++ b/lib/src/init.dart @@ -9,6 +9,7 @@ import 'package:lichess_mobile/src/db/secure_storage.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/model/settings/preferences.dart'; import 'package:lichess_mobile/src/network/socket.dart'; +import 'package:lichess_mobile/src/utils/chessboard.dart'; import 'package:lichess_mobile/src/utils/color_palette.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/utils/string.dart'; @@ -44,6 +45,22 @@ Future setupFirstLaunch() async { } } +Future preloadPieceImages() async { + final prefs = LichessBinding.instance.sharedPreferences; + final storedPrefs = prefs.getString(PrefCategory.board.storageKey); + BoardPrefs boardPrefs = BoardPrefs.defaults; + if (storedPrefs != null) { + try { + boardPrefs = + BoardPrefs.fromJson(jsonDecode(storedPrefs) as Map); + } catch (e) { + _logger.warning('Failed to decode board preferences: $e'); + } + } + + await precachePieceImages(boardPrefs.pieceSet); +} + /// Display setup on Android. /// /// This is meant to be called once during app initialization. diff --git a/lib/src/model/settings/board_preferences.dart b/lib/src/model/settings/board_preferences.dart index baedf764b8..b22b995165 100644 --- a/lib/src/model/settings/board_preferences.dart +++ b/lib/src/model/settings/board_preferences.dart @@ -151,11 +151,7 @@ class BoardPrefs with _$BoardPrefs implements SerializablePreferences { } factory BoardPrefs.fromJson(Map json) { - try { - return _$BoardPrefsFromJson(json); - } catch (_) { - return defaults; - } + return _$BoardPrefsFromJson(json); } Duration get pieceAnimationDuration => diff --git a/lib/src/utils/chessboard.dart b/lib/src/utils/chessboard.dart new file mode 100644 index 0000000000..3519e92b40 --- /dev/null +++ b/lib/src/utils/chessboard.dart @@ -0,0 +1,23 @@ +import 'package:chessground/chessground.dart'; +import 'package:flutter/widgets.dart'; + +/// Preload piece images from the specified [PieceSet] into Chessground's image cache. +/// +/// This method clears the cache before loading the images. +Future precachePieceImages(PieceSet pieceSet) async { + try { + final devicePixelRatio = WidgetsBinding + .instance.platformDispatcher.implicitView?.devicePixelRatio ?? + 1.0; + + ChessgroundImages.instance.clear(); + + for (final asset in pieceSet.assets.values) { + await ChessgroundImages.instance + .load(asset, devicePixelRatio: devicePixelRatio); + debugPrint('Preloaded piece image: ${asset.assetName}'); + } + } catch (e) { + debugPrint('Failed to preload piece images: $e'); + } +} diff --git a/lib/src/view/puzzle/puzzle_feedback_widget.dart b/lib/src/view/puzzle/puzzle_feedback_widget.dart index 88b2b20d6d..51d24b4351 100644 --- a/lib/src/view/puzzle/puzzle_feedback_widget.dart +++ b/lib/src/view/puzzle/puzzle_feedback_widget.dart @@ -29,7 +29,8 @@ class PuzzleFeedbackWidget extends ConsumerWidget { ref.watch(boardPreferencesProvider.select((state) => state.boardTheme)); final brightness = ref.watch(currentBrightnessProvider); - final piece = state.pov == Side.white ? kWhiteKingKind : kBlackKingKind; + final piece = + state.pov == Side.white ? PieceKind.whiteKing : PieceKind.blackKing; final asset = pieceSet.assets[piece]!; switch (state.mode) { diff --git a/lib/src/view/settings/piece_set_screen.dart b/lib/src/view/settings/piece_set_screen.dart index b4251ad127..dcaad14fe2 100644 --- a/lib/src/view/settings/piece_set_screen.dart +++ b/lib/src/view/settings/piece_set_screen.dart @@ -4,104 +4,106 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; +import 'package:lichess_mobile/src/utils/chessboard.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; -class PieceSetScreen extends StatelessWidget { +class PieceSetScreen extends ConsumerStatefulWidget { const PieceSetScreen({super.key}); @override - Widget build(BuildContext context) { - return PlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, - ); - } + ConsumerState createState() => _PieceSetScreenState(); +} - Widget _androidBuilder(BuildContext context) { - return Scaffold( - appBar: AppBar(title: Text(context.l10n.pieceSet)), - body: _Body(), - ); +class _PieceSetScreenState extends ConsumerState { + bool isLoading = false; + + Future onChanged(PieceSet? value) async { + if (value != null) { + ref.read(boardPreferencesProvider.notifier).setPieceSet(value); + setState(() { + isLoading = true; + }); + try { + await precachePieceImages(value); + } finally { + setState(() { + isLoading = false; + }); + } + } } - Widget _iosBuilder(BuildContext context) { - return CupertinoPageScaffold( - navigationBar: const CupertinoNavigationBar(), - child: _Body(), - ); + List getPieceImages(PieceSet set) { + return [ + set.assets[PieceKind.whiteKing]!, + set.assets[PieceKind.blackQueen]!, + set.assets[PieceKind.whiteRook]!, + set.assets[PieceKind.blackBishop]!, + set.assets[PieceKind.whiteKnight]!, + set.assets[PieceKind.blackPawn]!, + ]; } -} -class _Body extends ConsumerWidget { @override - Widget build(BuildContext context, WidgetRef ref) { + Widget build(BuildContext context) { final boardPrefs = ref.watch(boardPreferencesProvider); - List getPieceImages(PieceSet set) { - return [ - set.assets[kWhiteKingKind]!, - set.assets[kBlackQueenKind]!, - set.assets[kWhiteRookKind]!, - set.assets[kBlackBishopKind]!, - set.assets[kWhiteKnightKind]!, - set.assets[kBlackPawnKind]!, - ]; - } - - void onChanged(PieceSet? value) => ref - .read(boardPreferencesProvider.notifier) - .setPieceSet(value ?? PieceSet.cburnett); - - return SafeArea( - child: ListView.separated( - itemCount: PieceSet.values.length, - separatorBuilder: (_, __) => PlatformDivider( - height: 1, - // on iOS: 14 (default indent) + 16 (padding) - indent: - Theme.of(context).platform == TargetPlatform.iOS ? 14 + 16 : null, - color: Theme.of(context).platform == TargetPlatform.iOS - ? null - : Colors.transparent, - ), - itemBuilder: (context, index) { - final set = PieceSet.values[index]; - return PlatformListTile( - trailing: boardPrefs.pieceSet == set - ? Theme.of(context).platform == TargetPlatform.android - ? const Icon(Icons.check) - : Icon( - CupertinoIcons.check_mark_circled_solid, - color: CupertinoTheme.of(context).primaryColor, - ) + return PlatformScaffold( + appBar: PlatformAppBar( + title: Text(context.l10n.pieceSet), + actions: [ + if (isLoading) const PlatformAppBarLoadingIndicator(), + ], + ), + body: SafeArea( + child: ListView.separated( + itemCount: PieceSet.values.length, + separatorBuilder: (_, __) => PlatformDivider( + height: 1, + // on iOS: 14 (default indent) + 16 (padding) + indent: Theme.of(context).platform == TargetPlatform.iOS + ? 14 + 16 : null, - title: Text(set.label), - subtitle: ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 264, - ), - child: Stack( - children: [ - boardPrefs.boardTheme.thumbnail, - Row( - children: getPieceImages(set) - .map( - (img) => Image( + color: Theme.of(context).platform == TargetPlatform.iOS + ? null + : Colors.transparent, + ), + itemBuilder: (context, index) { + final pieceSet = PieceSet.values[index]; + return PlatformListTile( + trailing: boardPrefs.pieceSet == pieceSet + ? Theme.of(context).platform == TargetPlatform.android + ? const Icon(Icons.check) + : Icon( + CupertinoIcons.check_mark_circled_solid, + color: CupertinoTheme.of(context).primaryColor, + ) + : null, + title: Text(pieceSet.label), + subtitle: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 264), + child: Stack( + children: [ + boardPrefs.boardTheme.thumbnail, + Row( + children: [ + for (final img in getPieceImages(pieceSet)) + Image( image: img, height: 44, ), - ) - .toList(), - ), - ], + ], + ), + ], + ), ), - ), - onTap: () => onChanged(set), - selected: boardPrefs.pieceSet == set, - ); - }, + onTap: isLoading ? null : () => onChanged(pieceSet), + selected: boardPrefs.pieceSet == pieceSet, + ); + }, + ), ), ); } diff --git a/pubspec.lock b/pubspec.lock index c2f46a42b4..b7f168906c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -194,10 +194,10 @@ packages: dependency: "direct main" description: name: chessground - sha256: "3267db8b04e0857761c40eab5895c556a885263388386f0f26b85d61401a1e05" + sha256: "93d7c7c8e6c049dd9ee99aa158520c6ae6e1d81aedae1dc0dbca93a5113fba9e" url: "https://pub.dev" source: hosted - version: "5.1.1" + version: "5.2.0" ci: dependency: transitive description: @@ -346,10 +346,10 @@ packages: dependency: "direct main" description: name: dartchess - sha256: f07896a6c29f169ce21b44429c0ff8057d58cd4f9b17b35c5ffc8a1b2cfc4f23 + sha256: "9ce59d164b5ddf87fdb3cbe118015674f2f241e33b0891cec1e9f30b68a81b26" url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.9.0" dbus: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index eaae5727ac..34fa7a2071 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,14 +11,14 @@ dependencies: app_settings: ^5.1.1 async: ^2.10.0 cached_network_image: ^3.2.2 - chessground: ^5.1.1 + chessground: ^5.2.0 collection: ^1.17.0 connectivity_plus: ^6.0.2 cronet_http: ^1.3.1 crypto: ^3.0.3 cupertino_http: ^1.1.0 cupertino_icons: ^1.0.2 - dartchess: ^0.8.0 + dartchess: ^0.9.0 deep_pick: ^1.0.0 device_info_plus: ^10.1.0 dynamic_color: ^1.6.9 From 775ad9a1d6a5f9e4ea0f8a98912ac3a6c375b936 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 11 Oct 2024 11:35:53 +0200 Subject: [PATCH 471/979] Fix auth controller test --- lib/src/model/auth/auth_controller.dart | 6 ++--- test/model/auth/auth_controller_test.dart | 31 ++++++++++++++++++++--- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/lib/src/model/auth/auth_controller.dart b/lib/src/model/auth/auth_controller.dart index 00c319efe7..b4ced58fa7 100644 --- a/lib/src/model/auth/auth_controller.dart +++ b/lib/src/model/auth/auth_controller.dart @@ -46,13 +46,13 @@ class AuthController extends _$AuthController { final appAuth = ref.read(appAuthProvider); try { + await ref.read(notificationServiceProvider).unregister(); await ref.withClient( (client) => AuthRepository(client, appAuth).signOut(), ); - ref.read(notificationServiceProvider).unregister(); - // force reconnect to the current socket - ref.read(socketPoolProvider).currentClient.connect(); await ref.read(authSessionProvider.notifier).delete(); + // force reconnect to the current socket + await ref.read(socketPoolProvider).currentClient.connect(); state = const AsyncValue.data(null); } catch (e, st) { state = AsyncValue.error(e, st); diff --git a/test/model/auth/auth_controller_test.dart b/test/model/auth/auth_controller_test.dart index 65e02b35a4..a70502a290 100644 --- a/test/model/auth/auth_controller_test.dart +++ b/test/model/auth/auth_controller_test.dart @@ -12,6 +12,7 @@ import 'package:lichess_mobile/src/network/http.dart'; import 'package:mocktail/mocktail.dart'; import '../../mock_server_responses.dart'; +import '../../network/fake_http_client_factory.dart'; import '../../test_container.dart'; import '../../test_helpers.dart'; @@ -47,6 +48,9 @@ void main() { ); } else if (request.method == 'DELETE' && request.url.path == '/api/token') { return mockResponse('ok', 200); + } else if (request.method == 'POST' && + request.url.path == '/mobile/unregister') { + return mockResponse('ok', 200); } return mockResponse('', 404); }); @@ -83,8 +87,8 @@ void main() { overrides: [ appAuthProvider.overrideWithValue(mockFlutterAppAuth), sessionStorageProvider.overrideWithValue(mockSessionStorage), - lichessClientProvider - .overrideWith((ref) => LichessClient(client, ref)), + httpClientFactoryProvider + .overrideWith((_) => FakeHttpClientFactory(() => client)), ], ); @@ -122,13 +126,29 @@ void main() { () => mockSessionStorage.delete(), ).thenAnswer((_) => Future.value(null)); + int tokenDeleteCount = 0; + int unregisterCount = 0; + + final client = MockClient((request) { + if (request.method == 'DELETE' && request.url.path == '/api/token') { + tokenDeleteCount++; + return mockResponse('ok', 200); + } else if (request.method == 'POST' && + request.url.path == '/mobile/unregister') { + unregisterCount++; + return mockResponse('ok', 200); + } + return mockResponse('', 404); + }); + final container = await makeContainer( overrides: [ appAuthProvider.overrideWithValue(mockFlutterAppAuth), sessionStorageProvider.overrideWithValue(mockSessionStorage), - lichessClientProvider - .overrideWith((ref) => LichessClient(client, ref)), + httpClientFactoryProvider + .overrideWith((_) => FakeHttpClientFactory(() => client)), ], + userSession: testUserSession, ); final listener = Listener>(); @@ -152,6 +172,9 @@ void main() { ]); verifyNoMoreInteractions(listener); + expect(tokenDeleteCount, 1, reason: 'token should be deleted'); + expect(unregisterCount, 1, reason: 'device should be unregistered'); + // session should be deleted verify( () => mockSessionStorage.delete(), From ff2417d0f062250e929d31d7dc8617bc5a2e2486 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 11 Oct 2024 11:57:44 +0200 Subject: [PATCH 472/979] Fix premove autoqueen pref Fixes #1052 --- lib/src/model/game/game_controller.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/src/model/game/game_controller.dart b/lib/src/model/game/game_controller.dart index a75e1b8f3e..a8f99217df 100644 --- a/lib/src/model/game/game_controller.dart +++ b/lib/src/model/game/game_controller.dart @@ -1023,7 +1023,10 @@ class GameState with _$GameState { (game.prefs?.enablePremove ?? true); bool get canAutoQueen => autoQueenSettingOverride ?? (game.prefs?.autoQueen == AutoQueen.always); - bool get canAutoQueenOnPremove => game.prefs?.autoQueen == AutoQueen.premove; + bool get canAutoQueenOnPremove => + autoQueenSettingOverride ?? + (game.prefs?.autoQueen == AutoQueen.always || + game.prefs?.autoQueen == AutoQueen.premove); bool get shouldConfirmResignAndDrawOffer => game.prefs?.confirmResign ?? true; bool get shouldConfirmMove => moveConfirmSettingOverride ?? game.prefs?.submitMove ?? false; From f0669e1a5406bc2b52854010787159fcbcbbf3e7 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 11 Oct 2024 11:59:44 +0200 Subject: [PATCH 473/979] Remove delay in fake socket channel --- test/network/fake_websocket_channel.dart | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/network/fake_websocket_channel.dart b/test/network/fake_websocket_channel.dart index 659b0cdd73..1d639eb2f2 100644 --- a/test/network/fake_websocket_channel.dart +++ b/test/network/fake_websocket_channel.dart @@ -17,9 +17,6 @@ class FakeWebSocketChannelFactory implements WebSocketChannelFactory { Map? headers, Duration timeout = const Duration(seconds: 1), }) async { - // in the real implementation the channel is returned after the [WebSocket] - // is connected, so we need to simulate this delay - await Future.delayed(const Duration(milliseconds: 5)); return createFunction(); } } From 4f23678ea8c7153a1cebf724c2a67d2ecd486af3 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 11 Oct 2024 12:53:50 +0200 Subject: [PATCH 474/979] Fix correspondence challenge creation --- lib/src/model/challenge/challenge.dart | 24 +++--- .../challenge/challenge_preferences.dart | 5 +- lib/src/model/lobby/create_game_service.dart | 28 ++++++- lib/src/view/game/game_screen.dart | 2 +- lib/src/view/home/home_tab_screen.dart | 8 +- .../view/play/create_challenge_screen.dart | 74 +++++++++---------- 6 files changed, 81 insertions(+), 60 deletions(-) diff --git a/lib/src/model/challenge/challenge.dart b/lib/src/model/challenge/challenge.dart index 2c03c1334e..7fc0ac749a 100644 --- a/lib/src/model/challenge/challenge.dart +++ b/lib/src/model/challenge/challenge.dart @@ -130,7 +130,7 @@ class ChallengeRequest required bool rated, required SideChoice sideChoice, String? initialFen, - }) = _ChallengeSetup; + }) = _ChallengeRequest; @override Speed get speed => Speed.fromTimeIncrement( @@ -140,16 +140,18 @@ class ChallengeRequest ), ); - Map get toRequestBody => { - if (clock != null) 'clock.limit': clock!.time.inSeconds.toString(), - if (clock != null) - 'clock.increment': clock!.increment.inSeconds.toString(), - if (days != null) 'days': days.toString(), - 'rated': variant == Variant.fromPosition ? 'false' : rated.toString(), - 'variant': variant.name, - if (variant == Variant.fromPosition) 'fen': initialFen, - if (sideChoice != SideChoice.random) 'color': sideChoice.name, - }; + Map get toRequestBody { + return { + if (clock != null) 'clock.limit': clock!.time.inSeconds.toString(), + if (clock != null) + 'clock.increment': clock!.increment.inSeconds.toString(), + if (days != null) 'days': days.toString(), + 'rated': variant == Variant.fromPosition ? 'false' : rated.toString(), + 'variant': variant.name, + if (variant == Variant.fromPosition) 'fen': initialFen, + if (sideChoice != SideChoice.random) 'color': sideChoice.name, + }; + } } enum ChallengeDirection { diff --git a/lib/src/model/challenge/challenge_preferences.dart b/lib/src/model/challenge/challenge_preferences.dart index 9ba3717cb4..ba1e1d7aef 100644 --- a/lib/src/model/challenge/challenge_preferences.dart +++ b/lib/src/model/challenge/challenge_preferences.dart @@ -85,8 +85,9 @@ class ChallengePrefs with _$ChallengePrefs implements SerializablePreferences { destUser: destUser, variant: variant, timeControl: timeControl, - clock: clock, - days: days, + clock: timeControl == ChallengeTimeControlType.clock ? clock : null, + days: + timeControl == ChallengeTimeControlType.correspondence ? days : null, rated: rated, sideChoice: sideChoice, initialFen: initialFen, diff --git a/lib/src/model/lobby/create_game_service.dart b/lib/src/model/lobby/create_game_service.dart index 30f1f9c45c..46fba2c6ce 100644 --- a/lib/src/model/lobby/create_game_service.dart +++ b/lib/src/model/lobby/create_game_service.dart @@ -111,10 +111,16 @@ class CreateGameService { ); } - /// Create a new challenge game. + /// Create a new real time challenge. /// - /// Returns the game id or the decline reason if the challenge was declined. - Future newChallenge(ChallengeRequest challengeReq) async { + /// Will listen to the challenge socket and await the response from the destinated user. + /// Returns the challenge, along with [GameFullId] if the challenge was accepted, + /// or the [ChallengeDeclineReason] if the challenge was declined. + Future newRealTimeChallenge( + ChallengeRequest challengeReq, + ) async { + assert(challengeReq.timeControl == ChallengeTimeControlType.clock); + if (_challengeConnection != null) { throw StateError('Already creating a game.'); } @@ -185,6 +191,22 @@ class CreateGameService { return completer.future; } + /// Create a new correspondence challenge. + /// + /// Returns the created challenge immediately. If the challenge is accepted, + /// a notification will be sent to the user when the game starts. + Future newCorrespondenceChallenge( + ChallengeRequest challenge, + ) async { + assert(challenge.timeControl == ChallengeTimeControlType.correspondence); + + _log.info('Creating new correspondence challenge'); + + return ref.withClient( + (client) => ChallengeRepository(client).create(challenge), + ); + } + /// Cancel the current game creation. Future cancelSeek() async { _log.info('Cancelling game creation'); diff --git a/lib/src/view/game/game_screen.dart b/lib/src/view/game/game_screen.dart index 9dda6892f9..d61308cfcb 100644 --- a/lib/src/view/game/game_screen.dart +++ b/lib/src/view/game/game_screen.dart @@ -227,7 +227,7 @@ class _LoadGame extends _$LoadGame { .newLobbyGame(seek) .then((id) => (gameFullId: id, challenge: null, declineReason: null)); } else if (challenge != null) { - return service.newChallenge(challenge); + return service.newRealTimeChallenge(challenge); } return Future.value( diff --git a/lib/src/view/home/home_tab_screen.dart b/lib/src/view/home/home_tab_screen.dart index bff4db350e..92be858f6f 100644 --- a/lib/src/view/home/home_tab_screen.dart +++ b/lib/src/view/home/home_tab_screen.dart @@ -963,14 +963,15 @@ class _ChallengeScreenButton extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final session = ref.watch(authSessionProvider); - final connectivity = ref.watch(connectivityChangesProvider); - final challenges = ref.watch(challengesProvider); - final count = challenges.valueOrNull?.inward.length; if (session == null) { return const SizedBox.shrink(); } + final connectivity = ref.watch(connectivityChangesProvider); + final challenges = ref.watch(challengesProvider); + final count = challenges.valueOrNull?.inward.length; + return connectivity.maybeWhen( data: (connectivity) => AppBarNotificationIconButton( icon: const Icon(LichessIcons.crossed_swords, size: 18.0), @@ -978,6 +979,7 @@ class _ChallengeScreenButton extends ConsumerWidget { onPressed: !connectivity.isOnline ? null : () { + ref.invalidate(challengesProvider); pushPlatformRoute( context, title: context.l10n.preferencesNotifyChallenge, diff --git a/lib/src/view/play/create_challenge_screen.dart b/lib/src/view/play/create_challenge_screen.dart index fd4ff2b159..4e1e0ad70f 100644 --- a/lib/src/view/play/create_challenge_screen.dart +++ b/lib/src/view/play/create_challenge_screen.dart @@ -14,10 +14,12 @@ import 'package:lichess_mobile/src/model/common/time_increment.dart'; import 'package:lichess_mobile/src/model/lobby/create_game_service.dart'; import 'package:lichess_mobile/src/model/lobby/game_setup_preferences.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; +import 'package:lichess_mobile/src/navigation.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/game/game_screen.dart'; +import 'package:lichess_mobile/src/view/user/challenge_requests_screen.dart'; import 'package:lichess_mobile/src/widgets/adaptive_choice_picker.dart'; import 'package:lichess_mobile/src/widgets/adaptive_text_field.dart'; import 'package:lichess_mobile/src/widgets/board_preview.dart'; @@ -54,7 +56,7 @@ class _ChallengeBody extends ConsumerStatefulWidget { } class _ChallengeBodyState extends ConsumerState<_ChallengeBody> { - Future? _pendingCreateChallenge; + Future? _pendingCorrespondenceChallenge; final _controller = TextEditingController(); String? fromPositionFenInput; @@ -110,17 +112,18 @@ class _ChallengeBodyState extends ConsumerState<_ChallengeBody> { choices: [ ChallengeTimeControlType.clock, ChallengeTimeControlType.correspondence, - ChallengeTimeControlType.unlimited, ], selectedItem: preferences.timeControl, labelBuilder: (ChallengeTimeControlType timeControl) => Text( - timeControl == ChallengeTimeControlType.clock - ? context.l10n.realTime - : timeControl == - ChallengeTimeControlType.correspondence - ? context.l10n.correspondence - : context.l10n.unlimited, + switch (timeControl) { + ChallengeTimeControlType.clock => + context.l10n.realTime, + ChallengeTimeControlType.correspondence => + context.l10n.correspondence, + ChallengeTimeControlType.unlimited => + context.l10n.unlimited, + }, ), onSelectedItemChanged: (ChallengeTimeControlType value) { ref @@ -372,7 +375,7 @@ class _ChallengeBodyState extends ConsumerState<_ChallengeBody> { ), const SizedBox(height: 20), FutureBuilder( - future: _pendingCreateChallenge, + future: _pendingCorrespondenceChallenge, builder: (context, snapshot) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 20.0), @@ -399,16 +402,15 @@ class _ChallengeBodyState extends ConsumerState<_ChallengeBody> { } : null : timeControl == - ChallengeTimeControlType.correspondence + ChallengeTimeControlType.correspondence && + snapshot.connectionState != + ConnectionState.waiting ? () async { final createGameService = ref.read(createGameServiceProvider); - showPlatformSnackbar( - context, - 'Sent challenge to ${widget.user.name}', - ); - _pendingCreateChallenge = - createGameService.newChallenge( + _pendingCorrespondenceChallenge = + createGameService + .newCorrespondenceChallenge( preferences.makeRequest( widget.user, preferences.variant != @@ -418,32 +420,24 @@ class _ChallengeBodyState extends ConsumerState<_ChallengeBody> { ), ); - _pendingCreateChallenge!.then((value) { - if (!context.mounted) return; + await _pendingCorrespondenceChallenge!; - final ( - gameFullId: fullId, - challenge: _, - :declineReason - ) = value; + if (!context.mounted) return; - if (fullId != null) { - pushPlatformRoute( - context, - rootNavigator: true, - builder: (BuildContext context) { - return GameScreen( - initialGameId: fullId, - ); - }, - ); - } else { - showPlatformSnackbar( - context, - '${widget.user.name}: ${declineReason!.label(context.l10n)}', - ); - } - }); + Navigator.of(context).pop(); + + // Switch to the home tab + ref + .read(currentBottomTabProvider.notifier) + .state = BottomTab.home; + + // Navigate to the challenges screen where + // the new correspondence challenge will be + // displayed + pushPlatformRoute( + context, + screen: const ChallengeRequestsScreen(), + ); } : null, child: Text( From ac8b526336e1b3fcc17943b1e718a1e634b45eb4 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 11 Oct 2024 11:59:44 +0200 Subject: [PATCH 475/979] Remove delay in fake socket channel --- test/network/fake_websocket_channel.dart | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/network/fake_websocket_channel.dart b/test/network/fake_websocket_channel.dart index 659b0cdd73..1d639eb2f2 100644 --- a/test/network/fake_websocket_channel.dart +++ b/test/network/fake_websocket_channel.dart @@ -17,9 +17,6 @@ class FakeWebSocketChannelFactory implements WebSocketChannelFactory { Map? headers, Duration timeout = const Duration(seconds: 1), }) async { - // in the real implementation the channel is returned after the [WebSocket] - // is connected, so we need to simulate this delay - await Future.delayed(const Duration(milliseconds: 5)); return createFunction(); } } From de176dce0ddfca2ebf4b11be26c8ffdde8382855 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 11 Oct 2024 13:09:42 +0200 Subject: [PATCH 476/979] Tweak coordinate training screen style --- .../coordinate_training_screen.dart | 109 +++++++++--------- 1 file changed, 57 insertions(+), 52 deletions(-) diff --git a/lib/src/view/coordinate_training/coordinate_training_screen.dart b/lib/src/view/coordinate_training/coordinate_training_screen.dart index 69b6e68730..0136f7ac00 100644 --- a/lib/src/view/coordinate_training/coordinate_training_screen.dart +++ b/lib/src/view/coordinate_training/coordinate_training_screen.dart @@ -33,8 +33,9 @@ class CoordinateTrainingScreen extends StatelessWidget { appBar: PlatformAppBar( title: const Text('Coordinate Training'), // TODO l10n once script works actions: [ - IconButton( + AppBarIconButton( icon: const Icon(Icons.settings), + semanticsLabel: context.l10n.settingsSettings, onPressed: () => showAdaptiveBottomSheet( context: context, builder: (BuildContext context) => @@ -95,6 +96,7 @@ class _BodyState extends ConsumerState<_Body> { }.lock; return SafeArea( + bottom: false, child: Column( children: [ Expanded( @@ -263,12 +265,17 @@ class _CoordinateTrainingMenu extends ConsumerWidget { return BottomSheetScrollableContainer( padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 8.0), children: [ - ListSection( - header: Text( - context.l10n.preferencesDisplay, - style: Styles.sectionTitle, - ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + context.l10n.preferencesDisplay, + style: Styles.sectionTitle, + ), + ), SwitchSettingTile( title: const Text('Show Coordinates'), value: trainingPrefs.showCoordinates, @@ -394,56 +401,54 @@ class _Button extends StatelessWidget { } } -class Settings extends ConsumerWidget { - const Settings(); +class SettingsBottomSheet extends ConsumerWidget { + const SettingsBottomSheet(); @override Widget build(BuildContext context, WidgetRef ref) { final trainingPrefs = ref.watch(coordinateTrainingPreferencesProvider); - return Padding( - padding: const EdgeInsets.all(20.0), - child: Column( - children: [ - Filter( - filterName: context.l10n.side, - filterType: FilterType.singleChoice, - choices: SideChoice.values, - showCheckmark: false, - choiceSelected: (choice) => trainingPrefs.sideChoice == choice, - choiceLabel: (choice) => Text(choice.label(context.l10n)), - onSelected: (choice, selected) { - if (selected) { - ref - .read( - coordinateTrainingPreferencesProvider.notifier, - ) - .setSideChoice(choice); - } - }, - ), - const SizedBox(height: 12.0), - const PlatformDivider(thickness: 1, indent: 0), - const SizedBox(height: 12.0), - Filter( - filterName: context.l10n.time, - filterType: FilterType.singleChoice, - choices: TimeChoice.values, - showCheckmark: false, - choiceSelected: (choice) => trainingPrefs.timeChoice == choice, - choiceLabel: (choice) => choice.label(context.l10n), - onSelected: (choice, selected) { - if (selected) { - ref - .read( - coordinateTrainingPreferencesProvider.notifier, - ) - .setTimeChoice(choice); - } - }, - ), - ], - ), + return BottomSheetScrollableContainer( + padding: const EdgeInsets.all(16.0), + children: [ + Filter( + filterName: context.l10n.side, + filterType: FilterType.singleChoice, + choices: SideChoice.values, + showCheckmark: false, + choiceSelected: (choice) => trainingPrefs.sideChoice == choice, + choiceLabel: (choice) => Text(choice.label(context.l10n)), + onSelected: (choice, selected) { + if (selected) { + ref + .read( + coordinateTrainingPreferencesProvider.notifier, + ) + .setSideChoice(choice); + } + }, + ), + const SizedBox(height: 12.0), + const PlatformDivider(thickness: 1, indent: 0), + const SizedBox(height: 12.0), + Filter( + filterName: context.l10n.time, + filterType: FilterType.singleChoice, + choices: TimeChoice.values, + showCheckmark: false, + choiceSelected: (choice) => trainingPrefs.timeChoice == choice, + choiceLabel: (choice) => choice.label(context.l10n), + onSelected: (choice, selected) { + if (selected) { + ref + .read( + coordinateTrainingPreferencesProvider.notifier, + ) + .setTimeChoice(choice); + } + }, + ), + ], ); } } @@ -523,7 +528,7 @@ class _TrainingBoardState extends ConsumerState<_TrainingBoard> { Future _coordinateTrainingSettingsBuilder(BuildContext context) { return showAdaptiveBottomSheet( context: context, - builder: (BuildContext context) => const Settings(), + builder: (BuildContext context) => const SettingsBottomSheet(), ); } From 6287450111861b5a7ec3e720af31d111ccea6f23 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 11 Oct 2024 13:56:12 +0200 Subject: [PATCH 477/979] Bump version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 34fa7a2071..7e2f1bd0c5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: lichess_mobile description: Lichess mobile app V2 publish_to: "none" -version: 0.12.0+001200 # see README.md for details about versioning +version: 0.12.1+001201 # see README.md for details about versioning environment: sdk: ">=3.3.0 <4.0.0" From 6995efb877bc69e122bb5d7191631f5e625ef935 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Fri, 11 Oct 2024 14:07:19 +0200 Subject: [PATCH 478/979] fix crash when a root comment has no text --- lib/src/view/analysis/tree_view.dart | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/src/view/analysis/tree_view.dart b/lib/src/view/analysis/tree_view.dart index 498970d93c..af5695e03d 100644 --- a/lib/src/view/analysis/tree_view.dart +++ b/lib/src/view/analysis/tree_view.dart @@ -382,6 +382,7 @@ class _PgnTreeViewState extends State<_PgnTreeView> { @override Widget build(BuildContext context) { + final rootComments = widget.rootComments?.map((c) => c.text).nonNulls ?? []; return Padding( padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 10), child: Column( @@ -391,13 +392,11 @@ class _PgnTreeViewState extends State<_PgnTreeView> { if (widget.params.pathToCurrentMove.isEmpty) SizedBox.shrink(key: widget.params.currentMoveKey), - if (widget.params.shouldShowComments && - widget.rootComments?.any((c) => c.text?.isNotEmpty == true) == - true) + if (widget.params.shouldShowComments && rootComments.isNotEmpty) Text.rich( TextSpan( children: _comments( - widget.rootComments!.map((c) => c.text!), + rootComments, textStyle: _baseTextStyle, ), ), From df9a42c31d0447dcceb953b87b81342c5597aed4 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 11 Oct 2024 15:31:47 +0200 Subject: [PATCH 479/979] More puzzle tab improvements: show saved batches --- lib/src/model/puzzle/puzzle_theme.dart | 257 +++++------------- lib/src/view/puzzle/opening_screen.dart | 6 +- lib/src/view/puzzle/puzzle_tab_screen.dart | 192 +++++++++---- lib/src/view/puzzle/puzzle_themes_screen.dart | 8 +- lib/src/widgets/list.dart | 2 +- test/view/puzzle/puzzle_tab_screen_test.dart | 6 +- 6 files changed, 221 insertions(+), 250 deletions(-) diff --git a/lib/src/model/puzzle/puzzle_theme.dart b/lib/src/model/puzzle/puzzle_theme.dart index fcad5c973a..42cda10e69 100644 --- a/lib/src/model/puzzle/puzzle_theme.dart +++ b/lib/src/model/puzzle/puzzle_theme.dart @@ -20,71 +20,74 @@ class PuzzleThemeData with _$PuzzleThemeData { } enum PuzzleThemeKey { - mix, - advancedPawn, - advantage, - anastasiaMate, - arabianMate, - attackingF2F7, - attraction, - backRankMate, - bishopEndgame, - bodenMate, - capturingDefender, - castling, - clearance, - crushing, - defensiveMove, - deflection, - discoveredAttack, - doubleBishopMate, - doubleCheck, - dovetailMate, - equality, - endgame, - enPassant, - exposedKing, - fork, - hangingPiece, - hookMate, - interference, - intermezzo, - kingsideAttack, - knightEndgame, - long, - master, - masterVsMaster, - mate, - mateIn1, - mateIn2, - mateIn3, - mateIn4, - mateIn5, - smotheredMate, - middlegame, - oneMove, - opening, - pawnEndgame, - pin, - promotion, - queenEndgame, - queenRookEndgame, - queensideAttack, - quietMove, - rookEndgame, - sacrifice, - short, - skewer, - superGM, - trappedPiece, - underPromotion, - veryLong, - xRayAttack, - zugzwang, - // checkFirst, + mix(PuzzleIcons.mix), + advancedPawn(PuzzleIcons.advancedPawn), + advantage(PuzzleIcons.advantage), + anastasiaMate(PuzzleIcons.anastasiaMate), + arabianMate(PuzzleIcons.arabianMate), + attackingF2F7(PuzzleIcons.attackingF2F7), + attraction(PuzzleIcons.attraction), + backRankMate(PuzzleIcons.backRankMate), + bishopEndgame(PuzzleIcons.bishopEndgame), + bodenMate(PuzzleIcons.bodenMate), + capturingDefender(PuzzleIcons.capturingDefender), + castling(PuzzleIcons.castling), + clearance(PuzzleIcons.clearance), + crushing(PuzzleIcons.crushing), + defensiveMove(PuzzleIcons.defensiveMove), + deflection(PuzzleIcons.deflection), + discoveredAttack(PuzzleIcons.discoveredAttack), + doubleBishopMate(PuzzleIcons.doubleBishopMate), + doubleCheck(PuzzleIcons.doubleCheck), + dovetailMate(PuzzleIcons.dovetailMate), + equality(PuzzleIcons.equality), + endgame(PuzzleIcons.endgame), + enPassant(PuzzleIcons.enPassant), + exposedKing(PuzzleIcons.exposedKing), + fork(PuzzleIcons.fork), + hangingPiece(PuzzleIcons.hangingPiece), + hookMate(PuzzleIcons.hookMate), + interference(PuzzleIcons.interference), + intermezzo(PuzzleIcons.intermezzo), + kingsideAttack(PuzzleIcons.kingsideAttack), + knightEndgame(PuzzleIcons.knightEndgame), + long(PuzzleIcons.long), + master(PuzzleIcons.master), + masterVsMaster(PuzzleIcons.masterVsMaster), + mate(PuzzleIcons.mate), + mateIn1(PuzzleIcons.mate), + mateIn2(PuzzleIcons.mate), + mateIn3(PuzzleIcons.mate), + mateIn4(PuzzleIcons.mate), + mateIn5(PuzzleIcons.mate), + smotheredMate(PuzzleIcons.smotheredMate), + middlegame(PuzzleIcons.middlegame), + oneMove(PuzzleIcons.oneMove), + opening(PuzzleIcons.opening), + pawnEndgame(PuzzleIcons.pawnEndgame), + pin(PuzzleIcons.pin), + promotion(PuzzleIcons.promotion), + queenEndgame(PuzzleIcons.queenEndgame), + queenRookEndgame(PuzzleIcons.queenRookEndgame), + queensideAttack(PuzzleIcons.queensideAttack), + quietMove(PuzzleIcons.quietMove), + rookEndgame(PuzzleIcons.rookEndgame), + sacrifice(PuzzleIcons.sacrifice), + short(PuzzleIcons.short), + skewer(PuzzleIcons.skewer), + superGM(PuzzleIcons.superGM), + trappedPiece(PuzzleIcons.trappedPiece), + underPromotion(PuzzleIcons.underPromotion), + veryLong(PuzzleIcons.veryLong), + xRayAttack(PuzzleIcons.xRayAttack), + zugzwang(PuzzleIcons.zugzwang), // used internally to filter out unsupported keys - unsupported; + unsupported(PuzzleIcons.mix); + + const PuzzleThemeKey(this.icon); + + final IconData icon; PuzzleThemeL10n l10n(AppLocalizations l10n) { switch (this) { @@ -525,131 +528,3 @@ class PuzzleThemeL10n { final String name; final String description; } - -IconData puzzleThemeIcon(PuzzleThemeKey theme) { - switch (theme) { - case PuzzleThemeKey.mix: - case PuzzleThemeKey.unsupported: - return PuzzleIcons.mix; - case PuzzleThemeKey.advancedPawn: - return PuzzleIcons.advancedPawn; - case PuzzleThemeKey.advantage: - return PuzzleIcons.advantage; - case PuzzleThemeKey.anastasiaMate: - return PuzzleIcons.anastasiaMate; - case PuzzleThemeKey.arabianMate: - return PuzzleIcons.arabianMate; - case PuzzleThemeKey.attackingF2F7: - return PuzzleIcons.attackingF2F7; - case PuzzleThemeKey.attraction: - return PuzzleIcons.attraction; - case PuzzleThemeKey.backRankMate: - return PuzzleIcons.backRankMate; - case PuzzleThemeKey.bishopEndgame: - return PuzzleIcons.bishopEndgame; - case PuzzleThemeKey.bodenMate: - return PuzzleIcons.bodenMate; - case PuzzleThemeKey.capturingDefender: - return PuzzleIcons.capturingDefender; - case PuzzleThemeKey.castling: - return PuzzleIcons.castling; - case PuzzleThemeKey.clearance: - return PuzzleIcons.clearance; - case PuzzleThemeKey.crushing: - return PuzzleIcons.crushing; - case PuzzleThemeKey.defensiveMove: - return PuzzleIcons.defensiveMove; - case PuzzleThemeKey.deflection: - return PuzzleIcons.deflection; - case PuzzleThemeKey.discoveredAttack: - return PuzzleIcons.discoveredAttack; - case PuzzleThemeKey.doubleBishopMate: - return PuzzleIcons.doubleBishopMate; - case PuzzleThemeKey.doubleCheck: - return PuzzleIcons.doubleCheck; - case PuzzleThemeKey.dovetailMate: - return PuzzleIcons.dovetailMate; - case PuzzleThemeKey.equality: - return PuzzleIcons.equality; - case PuzzleThemeKey.endgame: - return PuzzleIcons.endgame; - case PuzzleThemeKey.enPassant: - return PuzzleIcons.enPassant; - case PuzzleThemeKey.exposedKing: - return PuzzleIcons.exposedKing; - case PuzzleThemeKey.fork: - return PuzzleIcons.fork; - case PuzzleThemeKey.hangingPiece: - return PuzzleIcons.hangingPiece; - case PuzzleThemeKey.hookMate: - return PuzzleIcons.hookMate; - case PuzzleThemeKey.interference: - return PuzzleIcons.interference; - case PuzzleThemeKey.intermezzo: - return PuzzleIcons.intermezzo; - case PuzzleThemeKey.kingsideAttack: - return PuzzleIcons.kingsideAttack; - case PuzzleThemeKey.knightEndgame: - return PuzzleIcons.knightEndgame; - case PuzzleThemeKey.long: - return PuzzleIcons.long; - case PuzzleThemeKey.master: - return PuzzleIcons.master; - case PuzzleThemeKey.masterVsMaster: - return PuzzleIcons.masterVsMaster; - case PuzzleThemeKey.mate: - return PuzzleIcons.mate; - case PuzzleThemeKey.mateIn1: - return PuzzleIcons.mate; - case PuzzleThemeKey.mateIn2: - return PuzzleIcons.mate; - case PuzzleThemeKey.mateIn3: - return PuzzleIcons.mate; - case PuzzleThemeKey.mateIn4: - return PuzzleIcons.mate; - case PuzzleThemeKey.mateIn5: - return PuzzleIcons.mate; - case PuzzleThemeKey.smotheredMate: - return PuzzleIcons.smotheredMate; - case PuzzleThemeKey.middlegame: - return PuzzleIcons.middlegame; - case PuzzleThemeKey.oneMove: - return PuzzleIcons.oneMove; - case PuzzleThemeKey.opening: - return PuzzleIcons.opening; - case PuzzleThemeKey.pawnEndgame: - return PuzzleIcons.pawnEndgame; - case PuzzleThemeKey.pin: - return PuzzleIcons.pin; - case PuzzleThemeKey.promotion: - return PuzzleIcons.promotion; - case PuzzleThemeKey.queenEndgame: - return PuzzleIcons.queenEndgame; - case PuzzleThemeKey.queenRookEndgame: - return PuzzleIcons.queenRookEndgame; - case PuzzleThemeKey.queensideAttack: - return PuzzleIcons.queensideAttack; - case PuzzleThemeKey.quietMove: - return PuzzleIcons.quietMove; - case PuzzleThemeKey.rookEndgame: - return PuzzleIcons.rookEndgame; - case PuzzleThemeKey.sacrifice: - return PuzzleIcons.sacrifice; - case PuzzleThemeKey.short: - return PuzzleIcons.short; - case PuzzleThemeKey.skewer: - return PuzzleIcons.skewer; - case PuzzleThemeKey.superGM: - return PuzzleIcons.superGM; - case PuzzleThemeKey.trappedPiece: - return PuzzleIcons.trappedPiece; - case PuzzleThemeKey.underPromotion: - return PuzzleIcons.underPromotion; - case PuzzleThemeKey.veryLong: - return PuzzleIcons.veryLong; - case PuzzleThemeKey.xRayAttack: - return PuzzleIcons.xRayAttack; - case PuzzleThemeKey.zugzwang: - return PuzzleIcons.zugzwang; - } -} diff --git a/lib/src/view/puzzle/opening_screen.dart b/lib/src/view/puzzle/opening_screen.dart index 6089ce5dd9..6ff61bc10c 100644 --- a/lib/src/view/puzzle/opening_screen.dart +++ b/lib/src/view/puzzle/opening_screen.dart @@ -162,7 +162,11 @@ class _OpeningFamily extends ConsumerWidget { builder: (context) => PuzzleScreen( angle: PuzzleOpening(openingFamily.key), ), - ); + ).then((_) { + if (context.mounted) { + ref.invalidate(savedOpeningBatchesProvider); + } + }); }, ), ); diff --git a/lib/src/view/puzzle/puzzle_tab_screen.dart b/lib/src/view/puzzle/puzzle_tab_screen.dart index e34871785a..f4c2360710 100644 --- a/lib/src/view/puzzle/puzzle_tab_screen.dart +++ b/lib/src/view/puzzle/puzzle_tab_screen.dart @@ -1,3 +1,4 @@ +import 'package:collection/collection.dart'; import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/cupertino.dart'; @@ -112,19 +113,126 @@ class _Body extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final connectivity = ref.watch(connectivityChangesProvider); + final savedThemes = ref.watch(savedThemeBatchesProvider); + final savedOpenings = ref.watch(savedOpeningBatchesProvider); final isTablet = isTabletOrLarger(context); - final handsetChildren = [ - connectivity.whenIs( - online: () => const DailyPuzzle(), - offline: () => const SizedBox.shrink(), + final dailyWidgets = connectivity.whenIs( + online: () => const [ + DailyPuzzle(), + SizedBox(height: 4.0), + ], + offline: () => [], + ); + + final previewDescriptionStyle = TextStyle( + height: 1.2, + fontSize: 12.0, + color: DefaultTextStyle.of(context).style.color?.withValues(alpha: 0.6), + ); + + // we always show the healthy mix theme + final healthyMixPreview = PuzzleAnglePreview( + angle: const PuzzleTheme(PuzzleThemeKey.mix), + icon: PuzzleIcons.mix, + description: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + PuzzleThemeKey.mix.l10n(context.l10n).name, + style: Styles.boardPreviewTitle, + ), + Text( + PuzzleThemeKey.mix.l10n(context.l10n).description, + style: previewDescriptionStyle, + ), + ], + ), + ); + + final savedThemesPreview = savedThemes.maybeWhen( + data: (themes) { + return themes.entries + .whereNot((e) => e.key == PuzzleThemeKey.mix) + .map((entry) { + final theme = entry.key; + return PuzzleAnglePreview( + angle: PuzzleTheme(theme), + icon: theme.icon, + description: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + theme.l10n(context.l10n).name, + style: Styles.boardPreviewTitle, + ), + Text( + theme.l10n(context.l10n).description, + maxLines: 3, + overflow: TextOverflow.ellipsis, + style: TextStyle( + height: 1.2, + fontSize: 12.0, + color: DefaultTextStyle.of(context) + .style + .color + ?.withValues(alpha: 0.6), + ), + ), + ], + ), + ); + }).toList(); + }, + orElse: () => [], + ); + + final savedOpeningsPreview = savedOpenings.maybeWhen( + data: (openings) { + return openings.entries.map((entry) { + final opening = entry.key; + return PuzzleAnglePreview( + angle: PuzzleOpening(opening), + icon: PuzzleIcons.opening, + description: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + opening, + style: Styles.boardPreviewTitle, + ), + ], + ), + ); + }).toList(); + }, + orElse: () => [], + ); + + final tacticalTrainerTitle = Padding( + padding: Styles.horizontalBodyPadding.add( + Theme.of(context).platform == TargetPlatform.iOS + ? Styles.sectionTopPadding + : EdgeInsets.zero, ), - const SizedBox(height: 4.0), - const TacticalTrainingPreview(), - if (Theme.of(context).platform == TargetPlatform.android) - const SizedBox(height: 8.0), + child: Text( + context.l10n.puzzleDesc, + style: Styles.sectionTitle, + ), + ); + + final handsetChildren = [ _PuzzleMenu(connectivity: connectivity), + tacticalTrainerTitle, + ...dailyWidgets, + healthyMixPreview, + ...savedThemesPreview, + ...savedOpeningsPreview, + const SizedBox(height: 8.0), ]; final tabletChildren = [ @@ -136,12 +244,13 @@ class _Body extends ConsumerWidget { mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.start, children: [ - const SizedBox(height: 8.0), - connectivity.whenIs( - online: () => const DailyPuzzle(), - offline: () => const SizedBox.shrink(), - ), _PuzzleMenu(connectivity: connectivity), + tacticalTrainerTitle, + ...dailyWidgets, + healthyMixPreview, + ...savedThemesPreview, + ...savedOpeningsPreview, + const SizedBox(height: 8.0), ], ), ), @@ -489,14 +598,21 @@ class DailyPuzzle extends ConsumerWidget { } } -/// A widget that displays a preview of the tactical training screen. -class TacticalTrainingPreview extends ConsumerWidget { - const TacticalTrainingPreview(); +/// A widget that displays a preview of a puzzle angle. +class PuzzleAnglePreview extends ConsumerWidget { + const PuzzleAnglePreview({ + required this.angle, + required this.icon, + required this.description, + }); + + final PuzzleAngle angle; + final IconData icon; + final Widget description; @override Widget build(BuildContext context, WidgetRef ref) { - final puzzle = - ref.watch(nextPuzzleProvider(const PuzzleTheme(PuzzleThemeKey.mix))); + final puzzle = ref.watch(nextPuzzleProvider(angle)); Widget buildPuzzlePreview(Puzzle? puzzle, {bool loading = false}) { final preview = puzzle != null ? PuzzlePreview.fromPuzzle(puzzle) : null; @@ -515,33 +631,9 @@ class TacticalTrainingPreview extends ConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - context.l10n.puzzleDesc, - style: Styles.boardPreviewTitle, - ), - Text( - // TODO change this to a better description when - // translation tool is again available (#945) - context.l10n.puzzleThemeHealthyMixDescription, - maxLines: 3, - overflow: TextOverflow.ellipsis, - style: TextStyle( - height: 1.2, - fontSize: 12.0, - color: DefaultTextStyle.of(context) - .style - .color - ?.withValues(alpha: 0.6), - ), - ), - ], - ), + description, Icon( - PuzzleIcons.mix, + icon, size: 34, color: DefaultTextStyle.of(context) .style @@ -565,16 +657,12 @@ class TacticalTrainingPreview extends ConsumerWidget { pushPlatformRoute( context, rootNavigator: true, - builder: (context) => const PuzzleScreen( - angle: PuzzleTheme(PuzzleThemeKey.mix), - ), + builder: (context) => PuzzleScreen(angle: angle), ).then((_) { if (context.mounted) { - ref.invalidate( - nextPuzzleProvider( - const PuzzleTheme(PuzzleThemeKey.mix), - ), - ); + ref.invalidate(nextPuzzleProvider(angle)); + ref.invalidate(savedThemeBatchesProvider); + ref.invalidate(savedOpeningBatchesProvider); } }); } diff --git a/lib/src/view/puzzle/puzzle_themes_screen.dart b/lib/src/view/puzzle/puzzle_themes_screen.dart index 74436183de..ca1dcbd40d 100644 --- a/lib/src/view/puzzle/puzzle_themes_screen.dart +++ b/lib/src/view/puzzle/puzzle_themes_screen.dart @@ -160,7 +160,7 @@ class _Category extends ConsumerWidget { return Opacity( opacity: isThemeAvailable ? 1 : 0.5, child: PlatformListTile( - leading: Icon(puzzleThemeIcon(theme)), + leading: Icon(theme.icon), trailing: hasConnectivity && onlineThemes?.containsKey(theme) == true ? Padding( @@ -218,7 +218,11 @@ class _Category extends ConsumerWidget { builder: (context) => PuzzleScreen( angle: PuzzleTheme(theme), ), - ); + ).then((_) { + if (context.mounted) { + ref.invalidate(savedThemeBatchesProvider); + } + }); } : null, ), diff --git a/lib/src/widgets/list.dart b/lib/src/widgets/list.dart index 6abeb2b4ad..a9c62168a7 100644 --- a/lib/src/widgets/list.dart +++ b/lib/src/widgets/list.dart @@ -77,7 +77,7 @@ class ListSection extends StatelessWidget { case TargetPlatform.android: return _isLoading ? Padding( - padding: margin ?? Styles.bodySectionBottomPadding, + padding: margin ?? Styles.sectionBottomPadding, child: Column( children: [ if (header != null) diff --git a/test/view/puzzle/puzzle_tab_screen_test.dart b/test/view/puzzle/puzzle_tab_screen_test.dart index 32149feea2..0c96bd943c 100644 --- a/test/view/puzzle/puzzle_tab_screen_test.dart +++ b/test/view/puzzle/puzzle_tab_screen_test.dart @@ -164,14 +164,14 @@ void main() { // wait for the puzzle to load await tester.pump(const Duration(milliseconds: 100)); - expect(find.byType(TacticalTrainingPreview), findsOneWidget); + expect(find.byType(PuzzleAnglePreview), findsOneWidget); expect( - find.widgetWithText(TacticalTrainingPreview, 'Chess tactics trainer'), + find.widgetWithText(PuzzleAnglePreview, 'Healthy mix'), findsOneWidget, ); final chessboard = find .descendant( - of: find.byType(TacticalTrainingPreview), + of: find.byType(PuzzleAnglePreview), matching: find.byType(Chessboard), ) .evaluate() From 17c7ba8eef4a453d422a5b7e7f6d1bdd8f2be6aa Mon Sep 17 00:00:00 2001 From: Tom Praschan <13141438+tom-anders@users.noreply.github.com> Date: Sun, 13 Oct 2024 21:57:17 +0200 Subject: [PATCH 480/979] docs: fix typo and remove obsolete note --- docs/setting_dev_env.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/setting_dev_env.md b/docs/setting_dev_env.md index 74cdcb65f7..9c5f7a5694 100644 --- a/docs/setting_dev_env.md +++ b/docs/setting_dev_env.md @@ -47,14 +47,12 @@ The fastest and most straight-forward way to get started is using [lila-docker]( Instructions to install both `lila` and `lila-ws` locally can be found in [the lila wiki](https://github.com/lichess-org/lila/wiki/Lichess-Development-Onboarding). -The mobile application is configured by default to target `http://127.0.0.1:9663` and `ws://127.0.0.1:9664`, so keep these when installing lila. - **Do not use any scheme (https:// or ws://) in url in host, since it's already handled by URI helper methods** To run the application with a local server, you can use the following command: ```bash -flutter run --dart-define=LICHESS_HOST=localhost.9663 --dart-define=LICHESS_WS_HOST=localhost:9664 +flutter run --dart-define=LICHESS_HOST=localhost:9663 --dart-define=LICHESS_WS_HOST=localhost:9664 ``` > [!NOTE] From f1d8669c93503abee3ed9340818dbe9efd7b768e Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 14 Oct 2024 16:20:46 +0200 Subject: [PATCH 481/979] WIP on deletable puzzle batches --- .../model/puzzle/puzzle_batch_storage.dart | 7 +- lib/src/model/puzzle/puzzle_controller.dart | 20 + lib/src/model/puzzle/puzzle_opening.dart | 9 +- lib/src/model/puzzle/puzzle_service.dart | 113 +++--- lib/src/view/puzzle/opening_screen.dart | 6 +- lib/src/view/puzzle/puzzle_tab_screen.dart | 363 +++++++++++------- lib/src/view/puzzle/puzzle_themes_screen.dart | 6 +- lib/src/widgets/list.dart | 53 +++ test/model/puzzle/puzzle_service_test.dart | 6 +- 9 files changed, 377 insertions(+), 206 deletions(-) diff --git a/lib/src/model/puzzle/puzzle_batch_storage.dart b/lib/src/model/puzzle/puzzle_batch_storage.dart index da828b5dba..dca6be7e5e 100644 --- a/lib/src/model/puzzle/puzzle_batch_storage.dart +++ b/lib/src/model/puzzle/puzzle_batch_storage.dart @@ -18,7 +18,7 @@ part 'puzzle_batch_storage.g.dart'; @Riverpod(keepAlive: true) Future puzzleBatchStorage(PuzzleBatchStorageRef ref) async { final database = await ref.watch(databaseProvider.future); - return PuzzleBatchStorage(database); + return PuzzleBatchStorage(database, ref); } const _anonUserKey = '**anon**'; @@ -26,9 +26,10 @@ const _tableName = 'puzzle_batchs'; /// Local storage for puzzles. class PuzzleBatchStorage { - const PuzzleBatchStorage(this._db); + const PuzzleBatchStorage(this._db, this._ref); final Database _db; + final PuzzleBatchStorageRef _ref; Future fetch({ required UserId? userId, @@ -74,6 +75,7 @@ class PuzzleBatchStorage { }, conflictAlgorithm: ConflictAlgorithm.replace, ); + _ref.invalidateSelf(); } Future delete({ @@ -91,6 +93,7 @@ class PuzzleBatchStorage { angle.key, ], ); + _ref.invalidateSelf(); } Future> fetchSavedThemes({ diff --git a/lib/src/model/puzzle/puzzle_controller.dart b/lib/src/model/puzzle/puzzle_controller.dart index 56a2a5e462..8d9c1d15e2 100644 --- a/lib/src/model/puzzle/puzzle_controller.dart +++ b/lib/src/model/puzzle/puzzle_controller.dart @@ -59,12 +59,32 @@ class PuzzleController extends _$PuzzleController { evaluationService.disposeEngine(); }); + // we might not have the user rating yet so let's update it now + // then it will be updated on each puzzle completion + if (initialContext.userId != null) { + _updateUserRating(); + } + return _loadNewContext(initialContext, initialStreak); } PuzzleRepository _repository(LichessClient client) => PuzzleRepository(client); + Future _updateUserRating() async { + try { + final data = await ref.withClient( + (client) => _repository(client).selectBatch(nb: 0), + ); + final glicko = data.glicko; + if (glicko != null) { + state = state.copyWith( + glicko: glicko, + ); + } + } catch (_) {} + } + PuzzleState _loadNewContext( PuzzleContext context, PuzzleStreak? streak, diff --git a/lib/src/model/puzzle/puzzle_opening.dart b/lib/src/model/puzzle/puzzle_opening.dart index 8dba915b6f..ed03974aa9 100644 --- a/lib/src/model/puzzle/puzzle_opening.dart +++ b/lib/src/model/puzzle/puzzle_opening.dart @@ -26,9 +26,12 @@ class PuzzleOpeningData with _$PuzzleOpeningData { }) = _PuzzleOpeningData; } +/// Returns a flattened list of openings with their respective counts. +/// +/// The list is cached for 1 day. @riverpod -Future> _flatOpeningsList( - _FlatOpeningsListRef ref, +Future> flatOpeningsList( + FlatOpeningsListRef ref, ) async { ref.cacheFor(const Duration(days: 1)); final families = await ref.watch(puzzleOpeningsProvider.future); @@ -51,6 +54,6 @@ Future> _flatOpeningsList( @riverpod Future puzzleOpeningName(PuzzleOpeningNameRef ref, String key) async { - final openings = await ref.watch(_flatOpeningsListProvider.future); + final openings = await ref.watch(flatOpeningsListProvider.future); return openings.firstWhere((element) => element.key == key).name; } diff --git a/lib/src/model/puzzle/puzzle_service.dart b/lib/src/model/puzzle/puzzle_service.dart index f247d7c692..4bd065e656 100644 --- a/lib/src/model/puzzle/puzzle_service.dart +++ b/lib/src/model/puzzle/puzzle_service.dart @@ -23,6 +23,13 @@ part 'puzzle_service.g.dart'; /// Size of puzzle local cache const kPuzzleLocalQueueLength = 50; +@Riverpod(keepAlive: true) +Future puzzleService(PuzzleServiceRef ref) { + return ref.read(puzzleServiceFactoryProvider)( + queueLength: kPuzzleLocalQueueLength, + ); +} + @Riverpod(keepAlive: true) PuzzleServiceFactory puzzleServiceFactory(PuzzleServiceFactoryRef ref) { return PuzzleServiceFactory(ref); @@ -133,6 +140,14 @@ class PuzzleService { return nextPuzzle(userId: userId, angle: angle); } + /// Deletes the puzzle batch of [angle] from the local storage. + Future deleteBatch({ + required UserId? userId, + required PuzzleAngle angle, + }) async { + await batchStorage.delete(userId: userId, angle: angle); + } + /// Synchronize offline puzzle queue with server and gets latest data. /// /// This task will fetch missing puzzles so the queue length is always equal to @@ -156,56 +171,60 @@ class PuzzleService { final deficit = max(0, queueLength - unsolved.length); - _log.fine('Have a puzzle deficit of $deficit, will sync with lichess'); - - final difficulty = _ref.read(puzzlePreferencesProvider).difficulty; - - // anonymous users can't solve puzzles so we just download the deficit - // we send the request even if the deficit is 0 to get the glicko rating - final batchResponse = _ref.withClient( - (client) => Result.capture( - solved.isNotEmpty && userId != null - ? PuzzleRepository(client).solveBatch( - nb: deficit, - solved: solved, - angle: angle, - difficulty: difficulty, - ) - : PuzzleRepository(client).selectBatch( - nb: deficit, - angle: angle, - difficulty: difficulty, - ), - ), - ); + if (deficit > 0) { + _log.fine('Have a puzzle deficit of $deficit, will sync with lichess'); + + final difficulty = _ref.read(puzzlePreferencesProvider).difficulty; + + // anonymous users can't solve puzzles so we just download the deficit + // we send the request even if the deficit is 0 to get the glicko rating + final batchResponse = _ref.withClient( + (client) => Result.capture( + solved.isNotEmpty && userId != null + ? PuzzleRepository(client).solveBatch( + nb: deficit, + solved: solved, + angle: angle, + difficulty: difficulty, + ) + : PuzzleRepository(client).selectBatch( + nb: deficit, + angle: angle, + difficulty: difficulty, + ), + ), + ); - return batchResponse - .fold( - (value) => Result.value( - ( - PuzzleBatch( - solved: IList(const []), - unsolved: IList([...unsolved, ...value.puzzles]), + return batchResponse + .fold( + (value) => Result.value( + ( + PuzzleBatch( + solved: IList(const []), + unsolved: IList([...unsolved, ...value.puzzles]), + ), + value.glicko, + value.rounds, + true, // should save the batch ), - value.glicko, - value.rounds, - true, // should save the batch ), - ), - // we don't need to save the batch if the request failed - (_, __) => Result.value((data, null, null, false)), - ) - .flatMap((tuple) async { - final (newBatch, glicko, rounds, shouldSave) = tuple; - if (newBatch != null && shouldSave) { - await batchStorage.save( - userId: userId, - angle: angle, - data: newBatch, - ); - } - return Result.value((newBatch, glicko, rounds)); - }); + // we don't need to save the batch if the request failed + (_, __) => Result.value((data, null, null, false)), + ) + .flatMap((tuple) async { + final (newBatch, glicko, rounds, shouldSave) = tuple; + if (newBatch != null && shouldSave) { + await batchStorage.save( + userId: userId, + angle: angle, + data: newBatch, + ); + } + return Result.value((newBatch, glicko, rounds)); + }); + } + + return Result.value((data, null, null)); } } diff --git a/lib/src/view/puzzle/opening_screen.dart b/lib/src/view/puzzle/opening_screen.dart index 6ff61bc10c..6089ce5dd9 100644 --- a/lib/src/view/puzzle/opening_screen.dart +++ b/lib/src/view/puzzle/opening_screen.dart @@ -162,11 +162,7 @@ class _OpeningFamily extends ConsumerWidget { builder: (context) => PuzzleScreen( angle: PuzzleOpening(openingFamily.key), ), - ).then((_) { - if (context.mounted) { - ref.invalidate(savedOpeningBatchesProvider); - } - }); + ); }, ), ); diff --git a/lib/src/view/puzzle/puzzle_tab_screen.dart b/lib/src/view/puzzle/puzzle_tab_screen.dart index f4c2360710..f3d2cc4001 100644 --- a/lib/src/view/puzzle/puzzle_tab_screen.dart +++ b/lib/src/view/puzzle/puzzle_tab_screen.dart @@ -1,14 +1,16 @@ -import 'package:collection/collection.dart'; import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_angle.dart'; +import 'package:lichess_mobile/src/model/puzzle/puzzle_opening.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_providers.dart'; +import 'package:lichess_mobile/src/model/puzzle/puzzle_service.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_theme.dart'; import 'package:lichess_mobile/src/navigation.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; @@ -36,6 +38,21 @@ import 'streak_screen.dart'; const _kNumberOfHistoryItemsOnHandset = 8; const _kNumberOfHistoryItemsOnTablet = 16; +final savedAnglesProvider = + FutureProvider.autoDispose>((ref) async { + final savedThemes = await ref.watch(savedThemeBatchesProvider.future); + final savedOpenings = await ref.watch(savedOpeningBatchesProvider.future); + return IMap.fromEntries([ + ...savedThemes + .remove(PuzzleThemeKey.mix) + .map((themeKey, v) => MapEntry(PuzzleTheme(themeKey), v)) + .entries, + ...savedOpenings + .map((openingKey, v) => MapEntry(PuzzleOpening(openingKey), v)) + .entries, + ]); +}); + class PuzzleTabScreen extends ConsumerWidget { const PuzzleTabScreen({super.key}); @@ -113,105 +130,32 @@ class _Body extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final connectivity = ref.watch(connectivityChangesProvider); - final savedThemes = ref.watch(savedThemeBatchesProvider); - final savedOpenings = ref.watch(savedOpeningBatchesProvider); + final savedAnglesAsync = ref.watch(savedAnglesProvider); final isTablet = isTabletOrLarger(context); - final dailyWidgets = connectivity.whenIs( + final dailyPuzzleWidget = connectivity.whenIs( online: () => const [ DailyPuzzle(), - SizedBox(height: 4.0), ], offline: () => [], ); - final previewDescriptionStyle = TextStyle( - height: 1.2, - fontSize: 12.0, - color: DefaultTextStyle.of(context).style.color?.withValues(alpha: 0.6), - ); - // we always show the healthy mix theme final healthyMixPreview = PuzzleAnglePreview( angle: const PuzzleTheme(PuzzleThemeKey.mix), - icon: PuzzleIcons.mix, - description: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - PuzzleThemeKey.mix.l10n(context.l10n).name, - style: Styles.boardPreviewTitle, + onTap: () { + pushPlatformRoute( + context, + rootNavigator: true, + builder: (context) => const PuzzleScreen( + angle: PuzzleTheme(PuzzleThemeKey.mix), ), - Text( - PuzzleThemeKey.mix.l10n(context.l10n).description, - style: previewDescriptionStyle, - ), - ], - ), - ); - - final savedThemesPreview = savedThemes.maybeWhen( - data: (themes) { - return themes.entries - .whereNot((e) => e.key == PuzzleThemeKey.mix) - .map((entry) { - final theme = entry.key; - return PuzzleAnglePreview( - angle: PuzzleTheme(theme), - icon: theme.icon, - description: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - theme.l10n(context.l10n).name, - style: Styles.boardPreviewTitle, - ), - Text( - theme.l10n(context.l10n).description, - maxLines: 3, - overflow: TextOverflow.ellipsis, - style: TextStyle( - height: 1.2, - fontSize: 12.0, - color: DefaultTextStyle.of(context) - .style - .color - ?.withValues(alpha: 0.6), - ), - ), - ], - ), - ); - }).toList(); + ); }, - orElse: () => [], ); - final savedOpeningsPreview = savedOpenings.maybeWhen( - data: (openings) { - return openings.entries.map((entry) { - final opening = entry.key; - return PuzzleAnglePreview( - angle: PuzzleOpening(opening), - icon: PuzzleIcons.opening, - description: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - opening, - style: Styles.boardPreviewTitle, - ), - ], - ), - ); - }).toList(); - }, - orElse: () => [], - ); + final savedAngles = savedAnglesAsync.valueOrNull; final tacticalTrainerTitle = Padding( padding: Styles.horizontalBodyPadding.add( @@ -228,10 +172,9 @@ class _Body extends ConsumerWidget { final handsetChildren = [ _PuzzleMenu(connectivity: connectivity), tacticalTrainerTitle, - ...dailyWidgets, + ...dailyPuzzleWidget, healthyMixPreview, - ...savedThemesPreview, - ...savedOpeningsPreview, + if (savedAngles != null) _SavedAnglesPreviewList(savedAngles), const SizedBox(height: 8.0), ]; @@ -246,10 +189,9 @@ class _Body extends ConsumerWidget { children: [ _PuzzleMenu(connectivity: connectivity), tacticalTrainerTitle, - ...dailyWidgets, + ...dailyPuzzleWidget, healthyMixPreview, - ...savedThemesPreview, - ...savedOpeningsPreview, + if (savedAngles != null) _SavedAnglesPreviewList(savedAngles), const SizedBox(height: 8.0), ], ), @@ -276,6 +218,99 @@ class _Body extends ConsumerWidget { } } +class _SavedAnglesPreviewList extends StatefulWidget { + const _SavedAnglesPreviewList(this.savedAngles); + + final IMap savedAngles; + + @override + State<_SavedAnglesPreviewList> createState() => + _SavedAnglesPreviewListState(); +} + +class _SavedAnglesPreviewListState extends State<_SavedAnglesPreviewList> { + final GlobalKey _listKey = GlobalKey(); + late final AnimatedListModel _angles; + + @override + void initState() { + super.initState(); + _angles = AnimatedListModel( + listKey: _listKey, + removedItemBuilder: _buildRemovedItem, + initialItems: widget.savedAngles.keys, + ); + } + + @override + void didUpdateWidget(covariant _SavedAnglesPreviewList oldWidget) { + super.didUpdateWidget(oldWidget); + final oldKeys = oldWidget.savedAngles.toKeyISet(); + final newKeys = widget.savedAngles.toKeyISet(); + + if (oldKeys != newKeys) { + final missings = oldKeys.difference(newKeys); + if (missings.isNotEmpty) { + for (final missing in missings) { + final index = _angles.indexOf(missing); + if (index != -1) { + _angles.removeAt(index); + } + } + } + + final additions = newKeys.difference(oldKeys); + if (additions.isNotEmpty) { + for (final addition in additions) { + final index = _angles.length; + _angles.insert(index, addition); + } + } + } + } + + Widget _buildItem( + BuildContext context, + int index, + Animation animation, + ) { + final angle = _angles[index]; + return PuzzleAnglePreview( + angle: angle, + onTap: () { + pushPlatformRoute( + context, + rootNavigator: true, + builder: (context) => PuzzleScreen(angle: angle), + ); + }, + ); + } + + Widget _buildRemovedItem( + PuzzleAngle angle, + BuildContext context, + Animation animation, + ) { + return SizeTransition( + sizeFactor: animation, + child: PuzzleAnglePreview(angle: angle), + ); + } + + @override + Widget build(BuildContext context) { + return AnimatedList( + shrinkWrap: true, + padding: EdgeInsets.zero, + physics: const ClampingScrollPhysics(), + key: _listKey, + initialItemCount: _angles.length, + itemBuilder: _buildItem, + ); + } +} + class _PuzzleMenuListTile extends StatelessWidget { const _PuzzleMenuListTile({ required this.icon, @@ -312,13 +347,13 @@ class _PuzzleMenuListTile extends StatelessWidget { } } -class _PuzzleMenu extends StatelessWidget { +class _PuzzleMenu extends ConsumerWidget { const _PuzzleMenu({required this.connectivity}); final AsyncValue connectivity; @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { final bool isOnline = connectivity.value?.isOnline ?? false; return ListSection( @@ -598,24 +633,21 @@ class DailyPuzzle extends ConsumerWidget { } } -/// A widget that displays a preview of a puzzle angle. +/// A widget that displays a preview of a puzzle angle batch. class PuzzleAnglePreview extends ConsumerWidget { - const PuzzleAnglePreview({ - required this.angle, - required this.icon, - required this.description, - }); + const PuzzleAnglePreview({required this.angle, this.onTap}); final PuzzleAngle angle; - final IconData icon; - final Widget description; + final VoidCallback? onTap; @override Widget build(BuildContext context, WidgetRef ref) { final puzzle = ref.watch(nextPuzzleProvider(angle)); + final flatOpenings = ref.watch(flatOpeningsListProvider); Widget buildPuzzlePreview(Puzzle? puzzle, {bool loading = false}) { final preview = puzzle != null ? PuzzlePreview.fromPuzzle(puzzle) : null; + return loading ? const Shimmer( child: ShimmerLoading( @@ -623,50 +655,101 @@ class PuzzleAnglePreview extends ConsumerWidget { child: SmallBoardPreview.loading(), ), ) - : SmallBoardPreview( - orientation: preview?.orientation ?? Side.white, - fen: preview?.initialFen ?? kEmptyFen, - lastMove: preview?.initialMove, - description: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, + : Slidable( + enabled: angle != const PuzzleTheme(PuzzleThemeKey.mix), + endActionPane: ActionPane( + motion: const StretchMotion(), children: [ - description, - Icon( - icon, - size: 34, - color: DefaultTextStyle.of(context) - .style - .color - ?.withValues(alpha: 0.6), + SlidableAction( + icon: Icons.delete, + onPressed: (context) async { + final service = + await ref.read(puzzleServiceProvider.future); + if (context.mounted) { + service.deleteBatch( + userId: ref.read(authSessionProvider)?.user.id, + angle: angle, + ); + } + }, + spacing: 8.0, + backgroundColor: context.lichessColors.error, + foregroundColor: Colors.white, + label: context.l10n.delete, ), - if (puzzle != null) - Text( - puzzle.puzzle.sideToMove == Side.white - ? context.l10n.whitePlays - : context.l10n.blackPlays, - ) - else - const Text( - 'No puzzles available, please go online to fetch them.', - ), ], ), - onTap: puzzle != null - ? () { - pushPlatformRoute( - context, - rootNavigator: true, - builder: (context) => PuzzleScreen(angle: angle), - ).then((_) { - if (context.mounted) { - ref.invalidate(nextPuzzleProvider(angle)); - ref.invalidate(savedThemeBatchesProvider); - ref.invalidate(savedOpeningBatchesProvider); - } - }); - } - : null, + child: SmallBoardPreview( + orientation: preview?.orientation ?? Side.white, + fen: preview?.initialFen ?? kEmptyFen, + lastMove: preview?.initialMove, + description: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: switch (angle) { + PuzzleTheme(themeKey: final themeKey) => [ + Text( + themeKey.l10n(context.l10n).name, + style: Styles.boardPreviewTitle, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + Text( + themeKey.l10n(context.l10n).description, + maxLines: 3, + overflow: TextOverflow.ellipsis, + style: TextStyle( + height: 1.2, + fontSize: 12.0, + color: DefaultTextStyle.of(context) + .style + .color + ?.withValues(alpha: 0.6), + ), + ), + ], + PuzzleOpening(key: final openingKey) => [ + Text( + flatOpenings.valueOrNull + ?.firstWhere((o) => o.key == openingKey) + .name ?? + openingKey.replaceAll('_', ' '), + style: Styles.boardPreviewTitle, + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + ], + }, + ), + Icon( + switch (angle) { + PuzzleTheme(themeKey: final themeKey) => themeKey.icon, + PuzzleOpening() => PuzzleIcons.opening, + }, + size: 34, + color: DefaultTextStyle.of(context) + .style + .color + ?.withValues(alpha: 0.6), + ), + if (puzzle != null) + Text( + puzzle.puzzle.sideToMove == Side.white + ? context.l10n.whitePlays + : context.l10n.blackPlays, + ) + else + const Text( + 'No puzzles available, please go online to fetch them.', + ), + ], + ), + onTap: puzzle != null ? onTap : null, + ), ); } diff --git a/lib/src/view/puzzle/puzzle_themes_screen.dart b/lib/src/view/puzzle/puzzle_themes_screen.dart index ca1dcbd40d..b65981abce 100644 --- a/lib/src/view/puzzle/puzzle_themes_screen.dart +++ b/lib/src/view/puzzle/puzzle_themes_screen.dart @@ -218,11 +218,7 @@ class _Category extends ConsumerWidget { builder: (context) => PuzzleScreen( angle: PuzzleTheme(theme), ), - ).then((_) { - if (context.mounted) { - ref.invalidate(savedThemeBatchesProvider); - } - }); + ); } : null, ), diff --git a/lib/src/widgets/list.dart b/lib/src/widgets/list.dart index a9c62168a7..d0f57db2f4 100644 --- a/lib/src/widgets/list.dart +++ b/lib/src/widgets/list.dart @@ -417,3 +417,56 @@ class AdaptiveListTile extends StatelessWidget { ); } } + +typedef RemovedItemBuilder = Widget Function( + T item, + BuildContext context, + Animation animation, +); + +/// Keeps a Dart [List] in sync with an [AnimatedList]. +/// +/// The [insert] and [removeAt] methods apply to both the internal list and +/// the animated list that belongs to [listKey]. +/// +/// This class only exposes as much of the Dart List API as is needed by the +/// sample app. More list methods are easily added, however methods that +/// mutate the list must make the same changes to the animated list in terms +/// of [AnimatedListState.insertItem] and [AnimatedList.removeItem]. +class AnimatedListModel { + AnimatedListModel({ + required this.listKey, + required this.removedItemBuilder, + Iterable? initialItems, + }) : _items = List.from(initialItems ?? []); + + final GlobalKey listKey; + final RemovedItemBuilder removedItemBuilder; + final List _items; + + AnimatedListState? get _animatedList => listKey.currentState; + + void insert(int index, E item) { + _items.insert(index, item); + _animatedList!.insertItem(index); + } + + E removeAt(int index) { + final E removedItem = _items.removeAt(index); + if (removedItem != null) { + _animatedList!.removeItem( + index, + (BuildContext context, Animation animation) { + return removedItemBuilder(removedItem, context, animation); + }, + ); + } + return removedItem; + } + + int get length => _items.length; + + E operator [](int index) => _items[index]; + + int indexOf(E item) => _items.indexOf(item); +} diff --git a/test/model/puzzle/puzzle_service_test.dart b/test/model/puzzle/puzzle_service_test.dart index df896a6ed2..141f851199 100644 --- a/test/model/puzzle/puzzle_service_test.dart +++ b/test/model/puzzle/puzzle_service_test.dart @@ -55,9 +55,7 @@ void main() { expect(data?.unsolved.length, equals(3)); }); - test( - 'if local queue is full, it will not download data but still try to fetch rating', - () async { + test('will not download data if local queue is full', () async { int nbReq = 0; final mockClient = MockClient((request) { nbReq++; @@ -81,7 +79,7 @@ void main() { final next = await service.nextPuzzle( userId: null, ); - expect(nbReq, equals(1)); + expect(nbReq, equals(0)); expect(next?.puzzle.puzzle.id, equals(const PuzzleId('pId3'))); final data = await storage.fetch(userId: null); expect(data?.unsolved.length, equals(1)); From 712f354aeb6372911dca6bd1b3344e1b52a7d19b Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 15 Oct 2024 11:04:52 +0200 Subject: [PATCH 482/979] Allow to delete saved puzzle batches --- lib/src/view/puzzle/puzzle_tab_screen.dart | 500 ++++++++++++------- lib/src/widgets/list.dart | 67 ++- lib/src/widgets/platform_scaffold.dart | 5 + test/view/puzzle/puzzle_tab_screen_test.dart | 172 ++++++- 4 files changed, 540 insertions(+), 204 deletions(-) diff --git a/lib/src/view/puzzle/puzzle_tab_screen.dart b/lib/src/view/puzzle/puzzle_tab_screen.dart index f3d2cc4001..5dcc68c4fb 100644 --- a/lib/src/view/puzzle/puzzle_tab_screen.dart +++ b/lib/src/view/puzzle/puzzle_tab_screen.dart @@ -27,7 +27,6 @@ import 'package:lichess_mobile/src/widgets/board_preview.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; import 'puzzle_screen.dart'; @@ -58,42 +57,212 @@ class PuzzleTabScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return ConsumerPlatformWidget( - ref: ref, - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, - ); + final savedAngles = ref.watch(savedAnglesProvider).valueOrNull; + + if (savedAngles == null) { + return const Center(child: CircularProgressIndicator()); + } + + if (Theme.of(context).platform == TargetPlatform.iOS) { + return _CupertinoTabBody(savedAngles); + } else { + return _MaterialTabBody(savedAngles); + } } +} - Widget _androidBuilder(BuildContext context, WidgetRef ref) { - return PopScope( - canPop: false, - onPopInvokedWithResult: (bool didPop, _) { - if (!didPop) { - ref.read(currentBottomTabProvider.notifier).state = BottomTab.home; - } - }, - child: Scaffold( - appBar: AppBar( - title: Text(context.l10n.puzzles), - actions: const [ - _DashboardButton(), - _HistoryButton(), - ], +Widget _buildMainListItem( + BuildContext context, + int index, + Animation animation, + PuzzleAngle Function(int index) getAngle, +) { + switch (index) { + case 0: + return const _PuzzleMenu(); + case 1: + return Padding( + padding: Styles.horizontalBodyPadding.add( + Theme.of(context).platform == TargetPlatform.iOS + ? Styles.sectionTopPadding + : EdgeInsets.zero, ), - body: const Column( - children: [ - ConnectivityBanner(), - Expanded( - child: _Body(), - ), - ], + child: Text( + context.l10n.puzzleDesc, + style: Styles.sectionTitle, ), - ), + ); + case 2: + return const DailyPuzzle(); + case 3: + return PuzzleAnglePreview( + angle: const PuzzleTheme(PuzzleThemeKey.mix), + onTap: () { + pushPlatformRoute( + context, + rootNavigator: true, + builder: (context) => const PuzzleScreen( + angle: PuzzleTheme(PuzzleThemeKey.mix), + ), + ); + }, + ); + default: + final angle = getAngle(index); + return PuzzleAnglePreview( + angle: angle, + onTap: () { + pushPlatformRoute( + context, + rootNavigator: true, + builder: (context) => PuzzleScreen(angle: angle), + ); + }, + ); + } +} + +Widget _buildMainListRemovedItem( + PuzzleAngle angle, + BuildContext context, + Animation animation, +) { + return SizeTransition( + sizeFactor: animation, + child: PuzzleAnglePreview(angle: angle), + ); +} + +// display the main body list for cupertino devices, as a workaround +// for missing type to handle both [SliverAnimatedList] and [AnimatedList]. +class _CupertinoTabBody extends ConsumerStatefulWidget { + const _CupertinoTabBody(this.savedAngles); + + final IMap savedAngles; + + @override + ConsumerState<_CupertinoTabBody> createState() => _CupertinoTabBodyState(); +} + +class _CupertinoTabBodyState extends ConsumerState<_CupertinoTabBody> { + final GlobalKey _listKey = + GlobalKey(); + late SliverAnimatedListModel _angles; + + @override + void initState() { + super.initState(); + _angles = SliverAnimatedListModel( + listKey: _listKey, + removedItemBuilder: _buildMainListRemovedItem, + initialItems: widget.savedAngles.keys, + itemsOffset: 4, + ); + } + + @override + void didUpdateWidget(covariant _CupertinoTabBody oldWidget) { + super.didUpdateWidget(oldWidget); + final oldKeys = oldWidget.savedAngles.toKeyISet(); + final newKeys = widget.savedAngles.toKeyISet(); + + if (oldKeys != newKeys) { + final missings = oldKeys.difference(newKeys); + if (missings.isNotEmpty) { + for (final missing in missings) { + final index = _angles.indexOf(missing); + if (index != -1) { + _angles.removeAt(index); + } + } + } + + final additions = newKeys.difference(oldKeys); + if (additions.isNotEmpty) { + for (final addition in additions) { + final index = _angles.length; + _angles.insert(index, addition); + } + } + } + } + + Widget _buildItem( + BuildContext context, + int index, + Animation animation, + ) { + return _buildMainListItem( + context, + index, + animation, + (index) => _angles[index], ); } - Widget _iosBuilder(BuildContext context, WidgetRef ref) { + @override + Widget build(BuildContext context) { + final isTablet = isTabletOrLarger(context); + + if (isTablet) { + return Row( + children: [ + Expanded( + child: CupertinoPageScaffold( + child: CustomScrollView( + controller: puzzlesScrollController, + slivers: [ + CupertinoSliverNavigationBar( + padding: const EdgeInsetsDirectional.only( + start: 16.0, + end: 8.0, + ), + largeTitle: Text(context.l10n.puzzles), + trailing: const Row( + mainAxisSize: MainAxisSize.min, + children: [ + _DashboardButton(), + ], + ), + ), + const SliverToBoxAdapter(child: ConnectivityBanner()), + SliverSafeArea( + top: false, + sliver: SliverAnimatedList( + key: _listKey, + initialItemCount: _angles.length, + itemBuilder: _buildItem, + ), + ), + ], + ), + ), + ), + VerticalDivider( + width: 1.0, + thickness: 1.0, + color: CupertinoColors.opaqueSeparator.resolveFrom(context), + ), + Expanded( + child: CupertinoPageScaffold( + backgroundColor: + CupertinoColors.systemBackground.resolveFrom(context), + navigationBar: CupertinoNavigationBar( + transitionBetweenRoutes: false, + middle: Text(context.l10n.puzzleHistory), + trailing: const _HistoryButton(), + ), + child: ListView( + children: const [ + PuzzleHistoryWidget(showHeader: false), + ], + ), + ), + ), + ], + ); + } + return CupertinoPageScaffold( child: CustomScrollView( controller: puzzlesScrollController, @@ -114,136 +283,46 @@ class PuzzleTabScreen extends ConsumerWidget { ), ), const SliverToBoxAdapter(child: ConnectivityBanner()), - const SliverSafeArea( + SliverSafeArea( top: false, - sliver: _Body(), - ), - ], - ), - ); - } -} - -class _Body extends ConsumerWidget { - const _Body(); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final connectivity = ref.watch(connectivityChangesProvider); - final savedAnglesAsync = ref.watch(savedAnglesProvider); - - final isTablet = isTabletOrLarger(context); - - final dailyPuzzleWidget = connectivity.whenIs( - online: () => const [ - DailyPuzzle(), - ], - offline: () => [], - ); - - // we always show the healthy mix theme - final healthyMixPreview = PuzzleAnglePreview( - angle: const PuzzleTheme(PuzzleThemeKey.mix), - onTap: () { - pushPlatformRoute( - context, - rootNavigator: true, - builder: (context) => const PuzzleScreen( - angle: PuzzleTheme(PuzzleThemeKey.mix), - ), - ); - }, - ); - - final savedAngles = savedAnglesAsync.valueOrNull; - - final tacticalTrainerTitle = Padding( - padding: Styles.horizontalBodyPadding.add( - Theme.of(context).platform == TargetPlatform.iOS - ? Styles.sectionTopPadding - : EdgeInsets.zero, - ), - child: Text( - context.l10n.puzzleDesc, - style: Styles.sectionTitle, - ), - ); - - final handsetChildren = [ - _PuzzleMenu(connectivity: connectivity), - tacticalTrainerTitle, - ...dailyPuzzleWidget, - healthyMixPreview, - if (savedAngles != null) _SavedAnglesPreviewList(savedAngles), - const SizedBox(height: 8.0), - ]; - - final tabletChildren = [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - _PuzzleMenu(connectivity: connectivity), - tacticalTrainerTitle, - ...dailyPuzzleWidget, - healthyMixPreview, - if (savedAngles != null) _SavedAnglesPreviewList(savedAngles), - const SizedBox(height: 8.0), - ], - ), - ), - Expanded( - child: Column( - children: [ - PuzzleHistoryWidget(), - ], + sliver: SliverAnimatedList( + key: _listKey, + initialItemCount: _angles.length, + itemBuilder: _buildItem, ), ), ], ), - ]; - - final children = isTablet ? tabletChildren : handsetChildren; - - return Theme.of(context).platform == TargetPlatform.iOS - ? SliverList(delegate: SliverChildListDelegate.fixed(children)) - : ListView( - controller: puzzlesScrollController, - children: children, - ); + ); } } -class _SavedAnglesPreviewList extends StatefulWidget { - const _SavedAnglesPreviewList(this.savedAngles); +class _MaterialTabBody extends ConsumerStatefulWidget { + const _MaterialTabBody(this.savedAngles); final IMap savedAngles; @override - State<_SavedAnglesPreviewList> createState() => - _SavedAnglesPreviewListState(); + ConsumerState<_MaterialTabBody> createState() => _MaterialTabBodyState(); } -class _SavedAnglesPreviewListState extends State<_SavedAnglesPreviewList> { +class _MaterialTabBodyState extends ConsumerState<_MaterialTabBody> { final GlobalKey _listKey = GlobalKey(); - late final AnimatedListModel _angles; + late AnimatedListModel _angles; @override void initState() { super.initState(); _angles = AnimatedListModel( listKey: _listKey, - removedItemBuilder: _buildRemovedItem, + removedItemBuilder: _buildMainListRemovedItem, initialItems: widget.savedAngles.keys, + itemsOffset: 4, ); } @override - void didUpdateWidget(covariant _SavedAnglesPreviewList oldWidget) { + void didUpdateWidget(covariant _MaterialTabBody oldWidget) { super.didUpdateWidget(oldWidget); final oldKeys = oldWidget.savedAngles.toKeyISet(); final newKeys = widget.savedAngles.toKeyISet(); @@ -274,39 +353,70 @@ class _SavedAnglesPreviewListState extends State<_SavedAnglesPreviewList> { int index, Animation animation, ) { - final angle = _angles[index]; - return PuzzleAnglePreview( - angle: angle, - onTap: () { - pushPlatformRoute( - context, - rootNavigator: true, - builder: (context) => PuzzleScreen(angle: angle), - ); - }, - ); - } - - Widget _buildRemovedItem( - PuzzleAngle angle, - BuildContext context, - Animation animation, - ) { - return SizeTransition( - sizeFactor: animation, - child: PuzzleAnglePreview(angle: angle), + return _buildMainListItem( + context, + index, + animation, + (index) => _angles[index], ); } @override Widget build(BuildContext context) { - return AnimatedList( - shrinkWrap: true, - padding: EdgeInsets.zero, - physics: const ClampingScrollPhysics(), - key: _listKey, - initialItemCount: _angles.length, - itemBuilder: _buildItem, + final isTablet = isTabletOrLarger(context); + + return PopScope( + canPop: false, + onPopInvokedWithResult: (bool didPop, _) { + if (!didPop) { + ref.read(currentBottomTabProvider.notifier).state = BottomTab.home; + } + }, + child: Scaffold( + appBar: AppBar( + title: Text(context.l10n.puzzles), + actions: const [ + _DashboardButton(), + _HistoryButton(), + ], + ), + body: isTablet + ? Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: AnimatedList( + shrinkWrap: true, + physics: const ClampingScrollPhysics(), + key: _listKey, + initialItemCount: _angles.length, + controller: puzzlesScrollController, + itemBuilder: _buildItem, + ), + ), + Expanded( + child: ListView( + children: const [ + PuzzleHistoryWidget(), + ], + ), + ), + ], + ) + : Column( + children: [ + const ConnectivityBanner(), + Expanded( + child: AnimatedList( + key: _listKey, + controller: puzzlesScrollController, + initialItemCount: _angles.length, + itemBuilder: _buildItem, + ), + ), + ], + ), + ), ); } } @@ -348,12 +458,11 @@ class _PuzzleMenuListTile extends StatelessWidget { } class _PuzzleMenu extends ConsumerWidget { - const _PuzzleMenu({required this.connectivity}); - - final AsyncValue connectivity; + const _PuzzleMenu(); @override Widget build(BuildContext context, WidgetRef ref) { + final connectivity = ref.watch(connectivityChangesProvider); final bool isOnline = connectivity.value?.isOnline ?? false; return ListSection( @@ -415,6 +524,10 @@ class _PuzzleMenu extends ConsumerWidget { } class PuzzleHistoryWidget extends ConsumerWidget { + const PuzzleHistoryWidget({this.showHeader = true}); + + final bool showHeader; + @override Widget build(BuildContext context, WidgetRef ref) { final asyncData = ref.watch(puzzleRecentActivityProvider); @@ -426,7 +539,7 @@ class PuzzleHistoryWidget extends ConsumerWidget { } if (recentActivity.isEmpty) { return ListSection( - header: Text(context.l10n.puzzleHistory), + header: showHeader ? Text(context.l10n.puzzleHistory) : null, children: [ Center( child: Padding( @@ -447,18 +560,20 @@ class PuzzleHistoryWidget extends ConsumerWidget { return ListSection( cupertinoBackgroundColor: - CupertinoTheme.of(context).scaffoldBackgroundColor, + CupertinoPageScaffoldBackgroundColor.maybeOf(context), cupertinoClipBehavior: Clip.none, - header: Text(context.l10n.puzzleHistory), - headerTrailing: NoPaddingTextButton( - onPressed: () => pushPlatformRoute( - context, - builder: (context) => const PuzzleHistoryScreen(), - ), - child: Text( - context.l10n.more, - ), - ), + header: showHeader ? Text(context.l10n.puzzleHistory) : null, + headerTrailing: showHeader + ? NoPaddingTextButton( + onPressed: () => pushPlatformRoute( + context, + builder: (context) => const PuzzleHistoryScreen(), + ), + child: Text( + context.l10n.more, + ), + ) + : null, children: [ Padding( padding: Theme.of(context).platform == TargetPlatform.iOS @@ -558,11 +673,14 @@ TextStyle _puzzlePreviewSubtitleStyle(BuildContext context) { /// A widget that displays the daily puzzle. class DailyPuzzle extends ConsumerWidget { - const DailyPuzzle(); + const DailyPuzzle({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { + final isOnline = + ref.watch(connectivityChangesProvider).valueOrNull?.isOnline ?? false; final puzzle = ref.watch(dailyPuzzleProvider); + return puzzle.when( data: (data) { final preview = PuzzlePreview.fromPuzzle(data); @@ -617,17 +735,21 @@ class DailyPuzzle extends ConsumerWidget { }, ); }, - loading: () => const Shimmer( - child: ShimmerLoading( - isLoading: true, - child: SmallBoardPreview.loading(), - ), - ), + loading: () => isOnline + ? const Shimmer( + child: ShimmerLoading( + isLoading: true, + child: SmallBoardPreview.loading(), + ), + ) + : const SizedBox.shrink(), error: (error, _) { - return const Padding( - padding: Styles.bodySectionPadding, - child: Text('Could not load the daily puzzle.'), - ); + return isOnline + ? const Padding( + padding: Styles.bodySectionPadding, + child: Text('Could not load the daily puzzle.'), + ) + : const SizedBox.shrink(); }, ); } @@ -635,7 +757,7 @@ class DailyPuzzle extends ConsumerWidget { /// A widget that displays a preview of a puzzle angle batch. class PuzzleAnglePreview extends ConsumerWidget { - const PuzzleAnglePreview({required this.angle, this.onTap}); + const PuzzleAnglePreview({required this.angle, this.onTap, super.key}); final PuzzleAngle angle; final VoidCallback? onTap; diff --git a/lib/src/widgets/list.dart b/lib/src/widgets/list.dart index d0f57db2f4..69cba26fad 100644 --- a/lib/src/widgets/list.dart +++ b/lib/src/widgets/list.dart @@ -424,35 +424,78 @@ typedef RemovedItemBuilder = Widget Function( Animation animation, ); -/// Keeps a Dart [List] in sync with an [AnimatedList]. +/// Keeps a Dart [List] in sync with an [AnimatedList] or [SliverAnimatedList]. /// /// The [insert] and [removeAt] methods apply to both the internal list and /// the animated list that belongs to [listKey]. -/// -/// This class only exposes as much of the Dart List API as is needed by the -/// sample app. More list methods are easily added, however methods that -/// mutate the list must make the same changes to the animated list in terms -/// of [AnimatedListState.insertItem] and [AnimatedList.removeItem]. class AnimatedListModel { AnimatedListModel({ required this.listKey, required this.removedItemBuilder, Iterable? initialItems, - }) : _items = List.from(initialItems ?? []); + int? itemsOffset, + }) : _items = List.from(initialItems ?? []), + itemsOffset = itemsOffset ?? 0; final GlobalKey listKey; final RemovedItemBuilder removedItemBuilder; final List _items; + final int itemsOffset; AnimatedListState? get _animatedList => listKey.currentState; void insert(int index, E item) { - _items.insert(index, item); + _items.insert(index - itemsOffset, item); + _animatedList!.insertItem(index); + } + + E removeAt(int index) { + final E removedItem = _items.removeAt(index - itemsOffset); + if (removedItem != null) { + _animatedList!.removeItem( + index, + (BuildContext context, Animation animation) { + return removedItemBuilder(removedItem, context, animation); + }, + ); + } + return removedItem; + } + + int get length => _items.length + itemsOffset; + + E operator [](int index) => _items[index - itemsOffset]; + + int indexOf(E item) => _items.indexOf(item) + itemsOffset; +} + +/// Keeps a Dart [List] in sync with a [SliverAnimatedList]. +/// +/// The [insert] and [removeAt] methods apply to both the internal list and +/// the animated list that belongs to [listKey]. +class SliverAnimatedListModel { + SliverAnimatedListModel({ + required this.listKey, + required this.removedItemBuilder, + Iterable? initialItems, + int? itemsOffset, + }) : _items = List.from(initialItems ?? []), + itemsOffset = itemsOffset ?? 0; + + final GlobalKey listKey; + final RemovedItemBuilder removedItemBuilder; + final List _items; + final int itemsOffset; + + SliverAnimatedListState? get _animatedList => listKey.currentState; + + void insert(int index, E item) { + _items.insert(index - itemsOffset, item); _animatedList!.insertItem(index); } E removeAt(int index) { - final E removedItem = _items.removeAt(index); + final E removedItem = _items.removeAt(index - itemsOffset); if (removedItem != null) { _animatedList!.removeItem( index, @@ -464,9 +507,9 @@ class AnimatedListModel { return removedItem; } - int get length => _items.length; + int get length => _items.length + itemsOffset; - E operator [](int index) => _items[index]; + E operator [](int index) => _items[index - itemsOffset]; - int indexOf(E item) => _items.indexOf(item); + int indexOf(E item) => _items.indexOf(item) + itemsOffset; } diff --git a/lib/src/widgets/platform_scaffold.dart b/lib/src/widgets/platform_scaffold.dart index d8c3020249..1337dddeb7 100644 --- a/lib/src/widgets/platform_scaffold.dart +++ b/lib/src/widgets/platform_scaffold.dart @@ -18,6 +18,7 @@ class PlatformAppBar extends StatelessWidget { this.leading, this.actions = const [], this.androidTitleSpacing, + this.cupertinoTransitionBetweenRoutes, }); /// Widget to place at the start of the navigation bar @@ -42,6 +43,9 @@ class PlatformAppBar extends StatelessWidget { /// Will be passed to [AppBar.titleSpacing] on Android. Has no effect on iOS. final double? androidTitleSpacing; + /// Whether to animate the transition between routes on iOS. + final bool? cupertinoTransitionBetweenRoutes; + AppBar _androidBuilder(BuildContext context) { return AppBar( titleSpacing: androidTitleSpacing, @@ -60,6 +64,7 @@ class PlatformAppBar extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: actions, ), + transitionBetweenRoutes: cupertinoTransitionBetweenRoutes ?? true, ); } diff --git a/test/view/puzzle/puzzle_tab_screen_test.dart b/test/view/puzzle/puzzle_tab_screen_test.dart index 0c96bd943c..f900bf50b6 100644 --- a/test/view/puzzle/puzzle_tab_screen_test.dart +++ b/test/view/puzzle/puzzle_tab_screen_test.dart @@ -46,6 +46,17 @@ void main() { ), ).thenAnswer((_) async => batch); + when( + () => mockBatchStorage.fetchSavedThemes( + userId: null, + ), + ).thenAnswer((_) async => const IMapConst({})); + when( + () => mockBatchStorage.fetchSavedOpenings( + userId: null, + ), + ).thenAnswer((_) async => const IMapConst({})); + final app = await makeTestProviderScopeApp( tester, home: const PuzzleTabScreen(), @@ -56,7 +67,7 @@ void main() { await tester.pumpWidget(app); - // wait for connectivity + // wait for connectivity and storage await tester.pump(const Duration(milliseconds: 100)); // wait for the puzzles to load @@ -78,6 +89,16 @@ void main() { angle: const PuzzleTheme(PuzzleThemeKey.mix), ), ).thenAnswer((_) async => batch); + when( + () => mockBatchStorage.fetchSavedThemes( + userId: null, + ), + ).thenAnswer((_) async => const IMapConst({})); + when( + () => mockBatchStorage.fetchSavedOpenings( + userId: null, + ), + ).thenAnswer((_) async => const IMapConst({})); final app = await makeTestProviderScopeApp( tester, home: const PuzzleTabScreen(), @@ -91,7 +112,7 @@ void main() { await tester.pumpWidget(app); - // wait for connectivity + // wait for connectivity and storage await tester.pumpAndSettle(const Duration(milliseconds: 100)); expect(find.text('Puzzle Themes'), findsOneWidget); @@ -106,6 +127,16 @@ void main() { angle: const PuzzleTheme(PuzzleThemeKey.mix), ), ).thenAnswer((_) async => batch); + when( + () => mockBatchStorage.fetchSavedThemes( + userId: null, + ), + ).thenAnswer((_) async => const IMapConst({})); + when( + () => mockBatchStorage.fetchSavedOpenings( + userId: null, + ), + ).thenAnswer((_) async => const IMapConst({})); final app = await makeTestProviderScopeApp( tester, @@ -120,7 +151,7 @@ void main() { await tester.pumpWidget(app); - // wait for connectivity + // wait for connectivity and storage await tester.pump(const Duration(milliseconds: 100)); // wait for the puzzles to load @@ -147,6 +178,16 @@ void main() { angle: const PuzzleTheme(PuzzleThemeKey.mix), ), ).thenAnswer((_) async => batch); + when( + () => mockBatchStorage.fetchSavedThemes( + userId: null, + ), + ).thenAnswer((_) async => const IMapConst({})); + when( + () => mockBatchStorage.fetchSavedOpenings( + userId: null, + ), + ).thenAnswer((_) async => const IMapConst({})); final app = await makeTestProviderScopeApp( tester, @@ -161,6 +202,9 @@ void main() { await tester.pumpWidget(app); + // wait for connectivity and storage + await tester.pump(const Duration(milliseconds: 100)); + // wait for the puzzle to load await tester.pump(const Duration(milliseconds: 100)); @@ -183,5 +227,127 @@ void main() { equals('4k2r/Q5pp/3bp3/4n3/1r5q/8/PP2B1PP/R1B2R1K b k - 0 21'), ); }); + + testWidgets('shows saved puzzle theme batches', + (WidgetTester tester) async { + when( + () => mockBatchStorage.fetch( + userId: null, + angle: const PuzzleTheme(PuzzleThemeKey.mix), + ), + ).thenAnswer((_) async => batch); + when( + () => mockBatchStorage.fetch( + userId: null, + angle: const PuzzleTheme(PuzzleThemeKey.advancedPawn), + ), + ).thenAnswer((_) async => batch); + when( + () => mockBatchStorage.fetchSavedThemes( + userId: null, + ), + ).thenAnswer( + (_) async => const IMapConst({ + PuzzleThemeKey.advancedPawn: 50, + }), + ); + when( + () => mockBatchStorage.fetchSavedOpenings( + userId: null, + ), + ).thenAnswer((_) async => const IMapConst({})); + + final app = await makeTestProviderScopeApp( + tester, + home: const PuzzleTabScreen(), + overrides: [ + puzzleBatchStorageProvider.overrideWith((ref) => mockBatchStorage), + httpClientFactoryProvider.overrideWith((ref) { + return FakeHttpClientFactory(() => mockClient); + }), + ], + ); + + await tester.pumpWidget(app); + + // wait for connectivity and storage + await tester.pump(const Duration(milliseconds: 100)); + + // wait for the puzzles to load + await tester.pump(const Duration(milliseconds: 100)); + + expect(find.byType(PuzzleAnglePreview), findsNWidgets(2)); + expect( + find.widgetWithText(PuzzleAnglePreview, 'Healthy mix'), + findsOneWidget, + ); + + expect( + find.widgetWithText(PuzzleAnglePreview, 'Advanced pawn'), + findsOneWidget, + ); + }); + + testWidgets('shows saved puzzle openings batches', + (WidgetTester tester) async { + when( + () => mockBatchStorage.fetch( + userId: null, + angle: const PuzzleTheme(PuzzleThemeKey.mix), + ), + ).thenAnswer((_) async => batch); + when( + () => mockBatchStorage.fetch( + userId: null, + angle: const PuzzleOpening('A00'), + ), + ).thenAnswer((_) async => batch); + when( + () => mockBatchStorage.fetchSavedThemes( + userId: null, + ), + ).thenAnswer( + (_) async => const IMapConst({}), + ); + when( + () => mockBatchStorage.fetchSavedOpenings( + userId: null, + ), + ).thenAnswer( + (_) async => const IMapConst({ + 'A00': 50, + }), + ); + + final app = await makeTestProviderScopeApp( + tester, + home: const PuzzleTabScreen(), + overrides: [ + puzzleBatchStorageProvider.overrideWith((ref) => mockBatchStorage), + httpClientFactoryProvider.overrideWith((ref) { + return FakeHttpClientFactory(() => mockClient); + }), + ], + ); + + await tester.pumpWidget(app); + + // wait for connectivity and storage + await tester.pump(const Duration(milliseconds: 100)); + + // wait for the puzzles to load + await tester.pump(const Duration(milliseconds: 100)); + + expect(find.byType(PuzzleAnglePreview), findsNWidgets(2)); + expect( + find.widgetWithText(PuzzleAnglePreview, 'Healthy mix'), + findsOneWidget, + ); + + expect( + find.widgetWithText(PuzzleAnglePreview, 'A00'), + findsOneWidget, + ); + }); }); } From 6cea0665abb252effc577c1cf8ed7f3394e323ae Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 15 Oct 2024 11:09:20 +0200 Subject: [PATCH 483/979] Restore PlatformAppBar API --- lib/src/widgets/platform_scaffold.dart | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lib/src/widgets/platform_scaffold.dart b/lib/src/widgets/platform_scaffold.dart index 1337dddeb7..d8c3020249 100644 --- a/lib/src/widgets/platform_scaffold.dart +++ b/lib/src/widgets/platform_scaffold.dart @@ -18,7 +18,6 @@ class PlatformAppBar extends StatelessWidget { this.leading, this.actions = const [], this.androidTitleSpacing, - this.cupertinoTransitionBetweenRoutes, }); /// Widget to place at the start of the navigation bar @@ -43,9 +42,6 @@ class PlatformAppBar extends StatelessWidget { /// Will be passed to [AppBar.titleSpacing] on Android. Has no effect on iOS. final double? androidTitleSpacing; - /// Whether to animate the transition between routes on iOS. - final bool? cupertinoTransitionBetweenRoutes; - AppBar _androidBuilder(BuildContext context) { return AppBar( titleSpacing: androidTitleSpacing, @@ -64,7 +60,6 @@ class PlatformAppBar extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: actions, ), - transitionBetweenRoutes: cupertinoTransitionBetweenRoutes ?? true, ); } From 575f058659696851752db079e34e8a98e3dfb6a7 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 15 Oct 2024 11:23:32 +0200 Subject: [PATCH 484/979] Upgrade dependencies --- ios/Podfile.lock | 10 +++--- pubspec.lock | 92 ++++++++++++++++++++++++++++++------------------ pubspec.yaml | 2 +- 3 files changed, 64 insertions(+), 40 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index b24aa22cfb..bba98bc619 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -131,7 +131,7 @@ PODS: - FlutterMacOS - sound_effect (0.0.2): - Flutter - - sqflite (0.0.3): + - sqflite_darwin (0.0.4): - Flutter - FlutterMacOS - stockfish (1.6.2): @@ -159,7 +159,7 @@ DEPENDENCIES: - share_plus (from `.symlinks/plugins/share_plus/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - sound_effect (from `.symlinks/plugins/sound_effect/ios`) - - sqflite (from `.symlinks/plugins/sqflite/darwin`) + - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) - stockfish (from `.symlinks/plugins/stockfish/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) @@ -217,8 +217,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" sound_effect: :path: ".symlinks/plugins/sound_effect/ios" - sqflite: - :path: ".symlinks/plugins/sqflite/darwin" + sqflite_darwin: + :path: ".symlinks/plugins/sqflite_darwin/darwin" stockfish: :path: ".symlinks/plugins/stockfish/ios" url_launcher_ios: @@ -259,7 +259,7 @@ SPEC CHECKSUMS: share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 sound_effect: 5280cfa89d4a576032186f15600dc948ca6d39ce - sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec + sqflite_darwin: a553b1fd6fe66f53bbb0fe5b4f5bab93f08d7a13 stockfish: d00cf6b95579f1d7032cbfd8e4fe874972fe2ff9 url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1 diff --git a/pubspec.lock b/pubspec.lock index b7f168906c..825baadc5c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -66,10 +66,10 @@ packages: dependency: transitive description: name: args - sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" + sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.6.0" async: dependency: "direct main" description: @@ -314,18 +314,18 @@ packages: dependency: "direct dev" description: name: custom_lint - sha256: "6e1ec47427ca968f22bce734d00028ae7084361999b41673291138945c5baca0" + sha256: "832fcdc676171205201c9cffafd6b5add19393962f6598af8472b48b413026e6" url: "https://pub.dev" source: hosted - version: "0.6.7" + version: "0.6.8" custom_lint_builder: dependency: transitive description: name: custom_lint_builder - sha256: ba2f90fff4eff71d202d097eb14b14f87087eaaef742e956208c0eb9d3a40a21 + sha256: c3d82779026f91b8e00c9ac18934595cbc9b490094ea682052beeafdb2bd50ac url: "https://pub.dev" source: hosted - version: "0.6.7" + version: "0.6.8" custom_lint_core: dependency: transitive description: @@ -370,10 +370,10 @@ packages: dependency: "direct main" description: name: device_info_plus - sha256: a7fd703482b391a87d60b6061d04dfdeab07826b96f9abd8f5ed98068acc0074 + sha256: db03b2d2a3fa466a4627709e1db58692c3f7f658e36a5942d342d86efedc4091 url: "https://pub.dev" source: hosted - version: "10.1.2" + version: "11.0.0" device_info_plus_platform_interface: dependency: transitive description: @@ -426,10 +426,10 @@ packages: dependency: transitive description: name: file - sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.1" firebase_core: dependency: "direct main" description: @@ -604,10 +604,10 @@ packages: dependency: "direct main" description: name: flutter_riverpod - sha256: "0f1974eff5bbe774bf1d870e406fc6f29e3d6f1c46bd9c58e7172ff68a785d7d" + sha256: "711d916456563f715bde1e139d7cfdca009f8264befab3ac9f8ded8b6ec26405" url: "https://pub.dev" source: hosted - version: "2.5.1" + version: "2.5.3" flutter_secure_storage: dependency: "direct main" description: @@ -919,10 +919,10 @@ packages: dependency: transitive description: name: mime - sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" url: "https://pub.dev" source: hosted - version: "1.0.6" + version: "2.0.0" mockito: dependency: transitive description: @@ -975,10 +975,10 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: a75164ade98cb7d24cfd0a13c6408927c6b217fa60dee5a7ff5c116a58f28918 + sha256: "894f37107424311bdae3e476552229476777b8752c5a2a2369c0cb9a2d5442ef" url: "https://pub.dev" source: hosted - version: "8.0.2" + version: "8.0.3" package_info_plus_platform_interface: dependency: transitive description: @@ -1015,10 +1015,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: f7544c346a0742aee1450f9e5c0f5269d7c602b9c95fdbcd9fb8f5b1df13b1cc + sha256: c464428172cb986b758c6d1724c603097febb8fb855aa265aeecc9280c294d4a url: "https://pub.dev" source: hosted - version: "2.2.11" + version: "2.2.12" path_provider_foundation: dependency: transitive description: @@ -1127,10 +1127,10 @@ packages: dependency: transitive description: name: riverpod - sha256: f21b32ffd26a36555e501b04f4a5dca43ed59e16343f1a30c13632b2351dfa4d + sha256: c86fedfb45dd1da98ee6493dd9374325cdf494e7d523ebfb0c387eecc5f7b5c9 url: "https://pub.dev" source: hosted - version: "2.5.1" + version: "2.5.3" riverpod_analyzer_utils: dependency: transitive description: @@ -1143,10 +1143,10 @@ packages: dependency: "direct main" description: name: riverpod_annotation - sha256: e5e796c0eba4030c704e9dae1b834a6541814963292839dcf9638d53eba84f5c + sha256: "77fdedb87d09344809e8b514ab864d0537b1cb580a93d09bf579b0403aa6203a" url: "https://pub.dev" source: hosted - version: "2.3.5" + version: "2.5.3" riverpod_generator: dependency: "direct dev" description: @@ -1175,18 +1175,18 @@ packages: dependency: "direct main" description: name: share_plus - sha256: "468c43f285207c84bcabf5737f33b914ceb8eb38398b91e5e3ad1698d1b72a52" + sha256: fec12c3c39f01e4df1ec6ad92b6e85503c5ca64ffd6e28d18c9ffe53fcc4cb11 url: "https://pub.dev" source: hosted - version: "10.0.2" + version: "10.0.3" share_plus_platform_interface: dependency: transitive description: name: share_plus_platform_interface - sha256: "6ababf341050edff57da8b6990f11f4e99eaba837865e2e6defe16d039619db5" + sha256: c57c0bbfec7142e3a0f55633be504b796af72e60e3c791b44d5a017b985f7a48 url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "5.0.1" shared_preferences: dependency: "direct main" description: @@ -1316,18 +1316,26 @@ packages: dependency: "direct main" description: name: sqflite - sha256: ff5a2436ef8ebdfda748fbfe957f9981524cb5ff11e7bafa8c42771840e8a788 + sha256: "79a297dc3cc137e758c6a4baf83342b039e5a6d2436fcdf3f96a00adaaf2ad62" url: "https://pub.dev" source: hosted - version: "2.3.3+2" + version: "2.4.0" + sqflite_android: + dependency: transitive + description: + name: sqflite_android + sha256: "78f489aab276260cdd26676d2169446c7ecd3484bbd5fead4ca14f3ed4dd9ee3" + url: "https://pub.dev" + source: hosted + version: "2.4.0" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: "2d8e607db72e9cb7748c9c6e739e2c9618320a5517de693d5a24609c4671b1a4" + sha256: "4468b24876d673418a7b7147e5a08a715b4998a7ae69227acafaab762e0e5490" url: "https://pub.dev" source: hosted - version: "2.5.4+4" + version: "2.5.4+5" sqflite_common_ffi: dependency: "direct dev" description: @@ -1336,6 +1344,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.3+1" + sqflite_darwin: + dependency: transitive + description: + name: sqflite_darwin + sha256: "769733dddf94622d5541c73e4ddc6aa7b252d865285914b6fcd54a63c4b4f027" + url: "https://pub.dev" + source: hosted + version: "2.4.1-1" + sqflite_platform_interface: + dependency: transitive + description: + name: sqflite_platform_interface + sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" + url: "https://pub.dev" + source: hosted + version: "2.4.0" sqlite3: dependency: transitive description: @@ -1461,10 +1485,10 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3" + sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.3.1" url_launcher_android: dependency: transitive description: @@ -1629,10 +1653,10 @@ packages: dependency: transitive description: name: win32 - sha256: "4d45dc9069dba4619dc0ebd93c7cec5e66d8482cb625a370ac806dcc8165f2ec" + sha256: e5c39a90447e7c81cfec14b041cdbd0d0916bd9ebbc7fe02ab69568be703b9bd url: "https://pub.dev" source: hosted - version: "5.5.5" + version: "5.6.0" win32_registry: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 7e2f1bd0c5..f650f57fd2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -20,7 +20,7 @@ dependencies: cupertino_icons: ^1.0.2 dartchess: ^0.9.0 deep_pick: ^1.0.0 - device_info_plus: ^10.1.0 + device_info_plus: ^11.0.0 dynamic_color: ^1.6.9 fast_immutable_collections: ^10.0.0 firebase_core: ^3.0.0 From affe2ed557ab2e7d9977a87bb6781cf7991b5173 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 15 Oct 2024 11:28:19 +0200 Subject: [PATCH 485/979] Bump version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index f650f57fd2..75e9b6d929 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: lichess_mobile description: Lichess mobile app V2 publish_to: "none" -version: 0.12.1+001201 # see README.md for details about versioning +version: 0.12.2+001202 # see README.md for details about versioning environment: sdk: ">=3.3.0 <4.0.0" From 1a6a04382153a0e16ca29ae783d29c73582b3982 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 16 Oct 2024 10:56:20 +0200 Subject: [PATCH 486/979] Remove side choice on creating custom games Fixes #1020 --- lib/src/model/lobby/game_seek.dart | 13 ------------- .../model/lobby/game_setup_preferences.dart | 7 ------- .../view/play/create_custom_game_screen.dart | 19 ++----------------- 3 files changed, 2 insertions(+), 37 deletions(-) diff --git a/lib/src/model/lobby/game_seek.dart b/lib/src/model/lobby/game_seek.dart index 6b1901f464..089250ae18 100644 --- a/lib/src/model/lobby/game_seek.dart +++ b/lib/src/model/lobby/game_seek.dart @@ -4,7 +4,6 @@ import 'package:dartchess/dartchess.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; -import 'package:lichess_mobile/src/model/common/game.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; import 'package:lichess_mobile/src/model/common/speed.dart'; import 'package:lichess_mobile/src/model/common/time_increment.dart'; @@ -37,7 +36,6 @@ class GameSeek with _$GameSeek { int? days, required bool rated, Variant? variant, - Side? side, /// Rating range (int, int)? ratingRange, @@ -66,11 +64,6 @@ class GameSeek with _$GameSeek { ), rated: account != null && setup.customRated, variant: setup.customVariant, - side: setup.customRated == true || setup.customSide == SideChoice.random - ? null - : setup.customSide == SideChoice.white - ? Side.white - : Side.black, ratingRange: account != null ? setup.ratingRangeFromCustom(account) : null, ); @@ -82,11 +75,6 @@ class GameSeek with _$GameSeek { days: setup.customDaysPerTurn, rated: account != null && setup.customRated, variant: setup.customVariant, - side: setup.customRated == true || setup.customSide == SideChoice.random - ? null - : setup.customSide == SideChoice.white - ? Side.white - : Side.black, ratingRange: account != null ? setup.ratingRangeFromCustom(account) : null, ); @@ -142,7 +130,6 @@ class GameSeek with _$GameSeek { if (days != null) 'days': days.toString(), 'rated': rated.toString(), if (variant != null) 'variant': variant!.name, - if (side != null) 'color': side!.name, if (ratingRange != null) 'ratingRange': '${ratingRange!.$1}-${ratingRange!.$2}', }; diff --git a/lib/src/model/lobby/game_setup_preferences.dart b/lib/src/model/lobby/game_setup_preferences.dart index eca0c1591a..d1c496b72f 100644 --- a/lib/src/model/lobby/game_setup_preferences.dart +++ b/lib/src/model/lobby/game_setup_preferences.dart @@ -1,7 +1,6 @@ import 'dart:math' as math; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; -import 'package:lichess_mobile/src/model/common/game.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; import 'package:lichess_mobile/src/model/common/speed.dart'; import 'package:lichess_mobile/src/model/common/time_increment.dart'; @@ -49,10 +48,6 @@ class GameSetupPreferences extends _$GameSetupPreferences return save(state.copyWith(customRated: rated)); } - Future setCustomSide(SideChoice side) { - return save(state.copyWith(customSide: side)); - } - Future setCustomRatingRange(int min, int max) { return save(state.copyWith(customRatingDelta: (min, max))); } @@ -76,7 +71,6 @@ class GameSetupPrefs with _$GameSetupPrefs implements SerializablePreferences { required int customDaysPerTurn, required Variant customVariant, required bool customRated, - required SideChoice customSide, required (int, int) customRatingDelta, }) = _GameSetupPrefs; @@ -87,7 +81,6 @@ class GameSetupPrefs with _$GameSetupPrefs implements SerializablePreferences { customIncrementSeconds: 0, customVariant: Variant.standard, customRated: false, - customSide: SideChoice.random, customRatingDelta: (-500, 500), customDaysPerTurn: 3, ); diff --git a/lib/src/view/play/create_custom_game_screen.dart b/lib/src/view/play/create_custom_game_screen.dart index a6ff680dfc..b50283fa93 100644 --- a/lib/src/view/play/create_custom_game_screen.dart +++ b/lib/src/view/play/create_custom_game_screen.dart @@ -523,23 +523,8 @@ class _CreateGameBodyState extends ConsumerState<_CreateGameBody> { harmonizeCupertinoTitleStyle: true, title: Text(context.l10n.side), trailing: AdaptiveTextButton( - onPressed: () { - showChoicePicker( - context, - choices: SideChoice.values, - selectedItem: preferences.customSide, - labelBuilder: (SideChoice side) => - Text(side.label(context.l10n)), - onSelectedItemChanged: (SideChoice side) { - ref - .read(gameSetupPreferencesProvider.notifier) - .setCustomSide(side); - }, - ); - }, - child: Text( - preferences.customSide.label(context.l10n), - ), + onPressed: null, + child: Text(SideChoice.random.label(context.l10n)), ), ), ), From b4eb5a52bcb4f7a3eaae0a500a95292a3e700ff1 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 16 Oct 2024 11:34:38 +0200 Subject: [PATCH 487/979] Don't use root nav to open puzzle theme screen --- lib/src/view/puzzle/puzzle_tab_screen.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/src/view/puzzle/puzzle_tab_screen.dart b/lib/src/view/puzzle/puzzle_tab_screen.dart index 5dcc68c4fb..c83462a20e 100644 --- a/lib/src/view/puzzle/puzzle_tab_screen.dart +++ b/lib/src/view/puzzle/puzzle_tab_screen.dart @@ -476,7 +476,6 @@ class _PuzzleMenu extends ConsumerWidget { pushPlatformRoute( context, title: context.l10n.puzzlePuzzleThemes, - rootNavigator: true, builder: (context) => const PuzzleThemesScreen(), ); }, From ea88722d7685ada88c9a99d7a3334777e97356f4 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 16 Oct 2024 11:48:55 +0200 Subject: [PATCH 488/979] Be sure to refresh account activity before accessing profile screen Closes #812 --- lib/src/view/home/home_tab_screen.dart | 1 + lib/src/view/settings/settings_tab_screen.dart | 2 ++ 2 files changed, 3 insertions(+) diff --git a/lib/src/view/home/home_tab_screen.dart b/lib/src/view/home/home_tab_screen.dart index 92be858f6f..537ba7e5a1 100644 --- a/lib/src/view/home/home_tab_screen.dart +++ b/lib/src/view/home/home_tab_screen.dart @@ -527,6 +527,7 @@ class _HelloWidget extends ConsumerWidget { ), child: GestureDetector( onTap: () { + ref.invalidate(accountActivityProvider); pushPlatformRoute( context, builder: (context) => const ProfileScreen(), diff --git a/lib/src/view/settings/settings_tab_screen.dart b/lib/src/view/settings/settings_tab_screen.dart index 03b2d566cc..ad2d25fc90 100644 --- a/lib/src/view/settings/settings_tab_screen.dart +++ b/lib/src/view/settings/settings_tab_screen.dart @@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/binding.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/db/database.dart'; +import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/auth/auth_controller.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; @@ -134,6 +135,7 @@ class _Body extends ConsumerWidget { ? const CupertinoListTileChevron() : null, onTap: () { + ref.invalidate(accountActivityProvider); pushPlatformRoute( context, title: context.l10n.profile, From c93c7f5272e742a3f30751b5bb7a9356befc3e37 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 16 Oct 2024 11:51:57 +0200 Subject: [PATCH 489/979] Remove unused import --- lib/src/model/lobby/game_seek.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/src/model/lobby/game_seek.dart b/lib/src/model/lobby/game_seek.dart index 089250ae18..b44d7ce262 100644 --- a/lib/src/model/lobby/game_seek.dart +++ b/lib/src/model/lobby/game_seek.dart @@ -1,6 +1,5 @@ import 'dart:math' as math; -import 'package:dartchess/dartchess.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; From 52eb092dc1d888ed1746c145c8d2e050821f9893 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 16 Oct 2024 14:24:21 +0200 Subject: [PATCH 490/979] Filter freezed input files to speed up build runner --- build.yaml | 2 + .../model/analysis/analysis_controller.dart | 2 +- .../analysis/server_analysis_service.dart | 1 + .../broadcast/broadcast_round_controller.dart | 1 + lib/src/model/common/socket.dart | 49 +++++++++++++++++++ .../correspondence_service.dart | 1 + lib/src/model/game/chat_controller.dart | 1 + lib/src/model/game/game_controller.dart | 1 + lib/src/model/lobby/create_game_service.dart | 1 + lib/src/model/lobby/lobby_numbers.dart | 1 + lib/src/model/relation/online_friends.dart | 1 + lib/src/model/tv/live_tv_channels.dart | 1 + lib/src/model/tv/tv_controller.dart | 1 + lib/src/network/socket.dart | 49 +------------------ lib/src/utils/connectivity.dart | 20 +++----- lib/src/utils/l10n.dart | 17 ++----- lib/src/view/engine/engine_gauge.dart | 24 ++++----- .../view/play/create_custom_game_screen.dart | 1 + lib/src/view/puzzle/puzzle_screen.dart | 2 +- lib/src/widgets/countdown_clock.dart | 30 +++++++----- 20 files changed, 104 insertions(+), 102 deletions(-) create mode 100644 lib/src/model/common/socket.dart diff --git a/build.yaml b/build.yaml index bf6e7dfc2f..88c39f2d77 100644 --- a/build.yaml +++ b/build.yaml @@ -2,6 +2,8 @@ targets: $default: builders: freezed: + generate_for: + - lib/src/model/**/*.dart options: from_json: false to_json: false diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index 5921cdaf0a..b8bdc6255c 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -749,7 +749,7 @@ class AnalysisState with _$AnalysisState { bool get canGoNext => currentNode.hasChild; bool get canGoBack => currentPath.size > UciPath.empty.size; - EngineGaugeParams get engineGaugeParams => EngineGaugeParams( + EngineGaugeParams get engineGaugeParams => ( orientation: pov, isLocalEngineAvailable: isEngineAvailable, position: position, diff --git a/lib/src/model/analysis/server_analysis_service.dart b/lib/src/model/analysis/server_analysis_service.dart index a7e740a60e..861eedebd0 100644 --- a/lib/src/model/analysis/server_analysis_service.dart +++ b/lib/src/model/analysis/server_analysis_service.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:dartchess/dartchess.dart'; import 'package:flutter/foundation.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/game/game_repository.dart'; import 'package:lichess_mobile/src/model/game/game_socket_events.dart'; import 'package:lichess_mobile/src/network/http.dart'; diff --git a/lib/src/model/broadcast/broadcast_round_controller.dart b/lib/src/model/broadcast/broadcast_round_controller.dart index 808f517267..ef09edc8ca 100644 --- a/lib/src/model/broadcast/broadcast_round_controller.dart +++ b/lib/src/model/broadcast/broadcast_round_controller.dart @@ -7,6 +7,7 @@ import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_repository.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/network/socket.dart'; import 'package:lichess_mobile/src/utils/json.dart'; diff --git a/lib/src/model/common/socket.dart b/lib/src/model/common/socket.dart new file mode 100644 index 0000000000..696d140d46 --- /dev/null +++ b/lib/src/model/common/socket.dart @@ -0,0 +1,49 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'socket.freezed.dart'; + +/// A socket event. +@freezed +class SocketEvent with _$SocketEvent { + const SocketEvent._(); + + const factory SocketEvent({ + required String topic, + dynamic data, + + /// Version of the socket event, only for versioned socket routes. + int? version, + }) = _SocketEvent; + + /// A special internal pong event that should never be accessible to the subscribers. + static const pong = SocketEvent(topic: '_pong'); + + factory SocketEvent.fromJson(Map json) { + if (json['t'] == null) { + if (json['v'] != null) { + return SocketEvent( + topic: '_version', + version: json['v'] as int, + ); + } else { + assert(false, 'Unsupported socket event json: $json'); + return pong; + } + } + final topic = json['t'] as String; + if (topic == 'n') { + return SocketEvent( + topic: topic, + data: { + 'nbPlayers': json['d'] as int, + 'nbGames': json['r'] as int, + }, + ); + } + return SocketEvent( + topic: topic, + data: json['d'], + version: json['v'] as int?, + ); + } +} diff --git a/lib/src/model/correspondence/correspondence_service.dart b/lib/src/model/correspondence/correspondence_service.dart index 634221fcbc..654cbdc4b3 100644 --- a/lib/src/model/correspondence/correspondence_service.dart +++ b/lib/src/model/correspondence/correspondence_service.dart @@ -9,6 +9,7 @@ import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/auth/bearer.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/correspondence/correspondence_game_storage.dart'; import 'package:lichess_mobile/src/model/correspondence/offline_correspondence_game.dart'; import 'package:lichess_mobile/src/model/game/game_repository.dart'; diff --git a/lib/src/model/game/chat_controller.dart b/lib/src/model/game/chat_controller.dart index ddddc8f936..6ec13b78a6 100644 --- a/lib/src/model/game/chat_controller.dart +++ b/lib/src/model/game/chat_controller.dart @@ -5,6 +5,7 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/db/database.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/game/game_controller.dart'; import 'package:lichess_mobile/src/network/socket.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; diff --git a/lib/src/model/game/game_controller.dart b/lib/src/model/game/game_controller.dart index a8f99217df..055c503d99 100644 --- a/lib/src/model/game/game_controller.dart +++ b/lib/src/model/game/game_controller.dart @@ -16,6 +16,7 @@ import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/service/move_feedback.dart'; import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; +import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/common/speed.dart'; import 'package:lichess_mobile/src/model/correspondence/correspondence_service.dart'; import 'package:lichess_mobile/src/model/game/archived_game.dart'; diff --git a/lib/src/model/lobby/create_game_service.dart b/lib/src/model/lobby/create_game_service.dart index 46fba2c6ce..c3707fd590 100644 --- a/lib/src/model/lobby/create_game_service.dart +++ b/lib/src/model/lobby/create_game_service.dart @@ -6,6 +6,7 @@ import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/challenge/challenge.dart'; import 'package:lichess_mobile/src/model/challenge/challenge_repository.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/lobby/game_seek.dart'; import 'package:lichess_mobile/src/model/lobby/lobby_repository.dart'; import 'package:lichess_mobile/src/network/http.dart'; diff --git a/lib/src/model/lobby/lobby_numbers.dart b/lib/src/model/lobby/lobby_numbers.dart index adb4bf22e3..b7bf6df761 100644 --- a/lib/src/model/lobby/lobby_numbers.dart +++ b/lib/src/model/lobby/lobby_numbers.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/network/socket.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; diff --git a/lib/src/model/relation/online_friends.dart b/lib/src/model/relation/online_friends.dart index c18466d198..8f1f4a3980 100644 --- a/lib/src/model/relation/online_friends.dart +++ b/lib/src/model/relation/online_friends.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/network/socket.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; diff --git a/lib/src/model/tv/live_tv_channels.dart b/lib/src/model/tv/live_tv_channels.dart index cdc07605b4..95c77bb3ff 100644 --- a/lib/src/model/tv/live_tv_channels.dart +++ b/lib/src/model/tv/live_tv_channels.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:dartchess/dartchess.dart'; import 'package:deep_pick/deep_pick.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/tv/tv_socket_events.dart'; import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/network/socket.dart'; diff --git a/lib/src/model/tv/tv_controller.dart b/lib/src/model/tv/tv_controller.dart index 68e0dd13b9..f53e002667 100644 --- a/lib/src/model/tv/tv_controller.dart +++ b/lib/src/model/tv/tv_controller.dart @@ -6,6 +6,7 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; +import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/game/game.dart'; import 'package:lichess_mobile/src/model/game/game_socket_events.dart'; import 'package:lichess_mobile/src/model/game/game_status.dart'; diff --git a/lib/src/network/socket.dart b/lib/src/network/socket.dart index 197c1a58ef..cf8980c715 100644 --- a/lib/src/network/socket.dart +++ b/lib/src/network/socket.dart @@ -6,11 +6,11 @@ import 'dart:math' as math; import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/binding.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/auth/bearer.dart'; +import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/network/http.dart'; import 'package:logging/logging.dart'; import 'package:package_info_plus/package_info_plus.dart'; @@ -18,7 +18,6 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:web_socket_channel/io.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; -part 'socket.freezed.dart'; part 'socket.g.dart'; const kSRIStorageKey = 'socket_random_identifier'; @@ -652,49 +651,3 @@ class WebSocketChannelFactory { return IOWebSocketChannel(socket); } } - -/// A socket event. -@freezed -class SocketEvent with _$SocketEvent { - const SocketEvent._(); - - const factory SocketEvent({ - required String topic, - dynamic data, - - /// Version of the socket event, only for versioned socket routes. - int? version, - }) = _SocketEvent; - - /// A special internal pong event that should never be accessible to the subscribers. - static const pong = SocketEvent(topic: '_pong'); - - factory SocketEvent.fromJson(Map json) { - if (json['t'] == null) { - if (json['v'] != null) { - return SocketEvent( - topic: '_version', - version: json['v'] as int, - ); - } else { - assert(false, 'Unsupported socket event json: $json'); - return pong; - } - } - final topic = json['t'] as String; - if (topic == 'n') { - return SocketEvent( - topic: topic, - data: { - 'nbPlayers': json['d'] as int, - 'nbGames': json['r'] as int, - }, - ); - } - return SocketEvent( - topic: topic, - data: json['d'], - version: json['v'] as int?, - ); - } -} diff --git a/lib/src/utils/connectivity.dart b/lib/src/utils/connectivity.dart index 9f2e76829f..5e6d4c1a15 100644 --- a/lib/src/utils/connectivity.dart +++ b/lib/src/utils/connectivity.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/widgets.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:http/http.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/network/http.dart'; @@ -10,7 +9,6 @@ import 'package:lichess_mobile/src/utils/rate_limit.dart'; import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -part 'connectivity.freezed.dart'; part 'connectivity.g.dart'; final _logger = Logger('Connectivity'); @@ -71,7 +69,8 @@ class ConnectivityChanges extends _$ConnectivityChanges { state = AsyncValue.data(newConn); } else { - state = AsyncValue.data(state.requireValue.copyWith(appState: appState)); + final (:isOnline, appState: _) = state.requireValue; + state = AsyncValue.data((isOnline: isOnline, appState: appState)); } } @@ -89,7 +88,7 @@ class ConnectivityChanges extends _$ConnectivityChanges { if (newIsOnline != wasOnline) { _logger.info('Connectivity status: $result, isOnline: $isOnline'); state = AsyncValue.data( - ConnectivityStatus( + ( isOnline: newIsOnline, appState: state.valueOrNull?.appState, ), @@ -101,7 +100,7 @@ class ConnectivityChanges extends _$ConnectivityChanges { List result, AppLifecycleState? appState, ) async { - final status = ConnectivityStatus( + final status = ( isOnline: await isOnline(_defaultClient), appState: appState, ); @@ -110,13 +109,10 @@ class ConnectivityChanges extends _$ConnectivityChanges { } } -@freezed -class ConnectivityStatus with _$ConnectivityStatus { - const factory ConnectivityStatus({ - required bool isOnline, - AppLifecycleState? appState, - }) = _ConnectivityStatus; -} +typedef ConnectivityStatus = ({ + bool isOnline, + AppLifecycleState? appState, +}); final _internetCheckUris = [ Uri.parse('https://www.gstatic.com/generate_204'), diff --git a/lib/src/utils/l10n.dart b/lib/src/utils/l10n.dart index c3857206af..77af7c6680 100644 --- a/lib/src/utils/l10n.dart +++ b/lib/src/utils/l10n.dart @@ -1,20 +1,13 @@ -import 'dart:ui' as ui; - import 'package:flutter/material.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/l10n/l10n.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -part 'l10n.freezed.dart'; part 'l10n.g.dart'; -@freezed -class L10nState with _$L10nState { - const factory L10nState({ - required Locale locale, - required AppLocalizations strings, - }) = _L10nState; -} +typedef L10nState = ({ + Locale locale, + AppLocalizations strings, +}); @Riverpod(keepAlive: true) class L10n extends _$L10n { @@ -36,7 +29,7 @@ class L10n extends _$L10n { L10nState _getLocale() { final locale = WidgetsBinding.instance.platformDispatcher.locale; - return L10nState( + return ( locale: locale, strings: lookupAppLocalizations(locale), ); diff --git a/lib/src/view/engine/engine_gauge.dart b/lib/src/view/engine/engine_gauge.dart index 22f9050545..4d35a79bb5 100644 --- a/lib/src/view/engine/engine_gauge.dart +++ b/lib/src/view/engine/engine_gauge.dart @@ -2,15 +2,12 @@ import 'package:dartchess/dartchess.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/common/eval.dart'; import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; import 'package:lichess_mobile/src/model/settings/brightness.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; -part 'engine_gauge.freezed.dart'; - const double kEvalGaugeSize = 26.0; const double kEvalGaugeFontSize = 11.0; const Color _kEvalGaugeBackgroundColor = Color(0xFF444444); @@ -22,21 +19,18 @@ enum EngineGaugeDisplayMode { horizontal, } -@freezed -class EngineGaugeParams with _$EngineGaugeParams { - const factory EngineGaugeParams({ - required bool isLocalEngineAvailable, +typedef EngineGaugeParams = ({ + bool isLocalEngineAvailable, - /// Only used for vertical display mode. - required Side orientation, + /// Only used for vertical display mode. + Side orientation, - /// Position to evaluate. - required Position position, + /// Position to evaluate. + Position position, - /// Saved evaluation to display when the current evaluation is not available. - Eval? savedEval, - }) = _EngineGaugeParams; -} + /// Saved evaluation to display when the current evaluation is not available. + Eval? savedEval, +}); class EngineGauge extends ConsumerWidget { const EngineGauge({ diff --git a/lib/src/view/play/create_custom_game_screen.dart b/lib/src/view/play/create_custom_game_screen.dart index b50283fa93..e885f19b37 100644 --- a/lib/src/view/play/create_custom_game_screen.dart +++ b/lib/src/view/play/create_custom_game_screen.dart @@ -10,6 +10,7 @@ import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/game.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; +import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/common/time_increment.dart'; import 'package:lichess_mobile/src/model/lobby/create_game_service.dart'; import 'package:lichess_mobile/src/model/lobby/game_seek.dart'; diff --git a/lib/src/view/puzzle/puzzle_screen.dart b/lib/src/view/puzzle/puzzle_screen.dart index fa701f8edf..ddd843f7f5 100644 --- a/lib/src/view/puzzle/puzzle_screen.dart +++ b/lib/src/view/puzzle/puzzle_screen.dart @@ -300,7 +300,7 @@ class _Body extends ConsumerWidget { ]) : null, engineGauge: puzzleState.isEngineEnabled - ? EngineGaugeParams( + ? ( orientation: puzzleState.pov, isLocalEngineAvailable: true, position: puzzleState.position, diff --git a/lib/src/widgets/countdown_clock.dart b/lib/src/widgets/countdown_clock.dart index 09fa81dc84..e562e0ce7a 100644 --- a/lib/src/widgets/countdown_clock.dart +++ b/lib/src/widgets/countdown_clock.dart @@ -3,13 +3,10 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; -part 'countdown_clock.freezed.dart'; - /// A simple countdown clock. /// /// The clock starts only when [active] is `true`. @@ -261,16 +258,23 @@ class Clock extends StatelessWidget { } } -@freezed -class ClockStyle with _$ClockStyle { - const factory ClockStyle({ - required Color textColor, - required Color activeTextColor, - required Color emergencyTextColor, - required Color backgroundColor, - required Color activeBackgroundColor, - required Color emergencyBackgroundColor, - }) = _ClockStyle; +@immutable +class ClockStyle { + const ClockStyle({ + required this.textColor, + required this.activeTextColor, + required this.emergencyTextColor, + required this.backgroundColor, + required this.activeBackgroundColor, + required this.emergencyBackgroundColor, + }); + + final Color textColor; + final Color activeTextColor; + final Color emergencyTextColor; + final Color backgroundColor; + final Color activeBackgroundColor; + final Color emergencyBackgroundColor; static const darkThemeStyle = ClockStyle( textColor: Colors.grey, From 132bab70958a863885963742a172b55d2879abc4 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 16 Oct 2024 15:49:57 +0200 Subject: [PATCH 491/979] Apply build runner only on needed files --- build.yaml | 6 ++ lib/src/app.dart | 2 +- lib/src/crashlytics.dart | 9 +-- lib/src/model/game/game_history.dart | 2 +- lib/src/model/lobby/create_game_service.dart | 2 + .../notifications/notification_service.dart | 2 +- .../model/user/user_repository_providers.dart | 16 +++++ lib/src/navigation.dart | 2 +- lib/src/{utils => network}/connectivity.dart | 0 lib/src/utils/l10n.dart | 17 ++--- lib/src/view/analysis/analysis_screen.dart | 2 +- lib/src/view/game/game_screen.dart | 65 +++---------------- lib/src/view/game/game_screen_providers.dart | 55 ++++++++++++++++ lib/src/view/game/ping_rating.dart | 6 +- lib/src/view/home/create_game_options.dart | 2 +- lib/src/view/home/home_tab_screen.dart | 2 +- lib/src/view/play/online_bots_screen.dart | 9 +-- lib/src/view/play/quick_game_button.dart | 2 +- lib/src/view/puzzle/opening_screen.dart | 13 ++-- lib/src/view/puzzle/puzzle_screen.dart | 3 +- lib/src/view/puzzle/puzzle_tab_screen.dart | 2 +- lib/src/view/puzzle/puzzle_themes_screen.dart | 12 ++-- lib/src/view/relation/following_screen.dart | 11 +--- lib/src/view/tools/tools_tab_screen.dart | 2 +- lib/src/view/user/recent_games.dart | 2 +- lib/src/view/user/user_activity.dart | 26 +------- lib/src/view/watch/watch_tab_screen.dart | 9 +-- lib/src/widgets/feedback.dart | 2 +- test/test_container.dart | 2 +- test/test_provider_scope.dart | 2 +- 30 files changed, 136 insertions(+), 151 deletions(-) rename lib/src/{utils => network}/connectivity.dart (100%) diff --git a/build.yaml b/build.yaml index 88c39f2d77..5a699c68cb 100644 --- a/build.yaml +++ b/build.yaml @@ -7,3 +7,9 @@ targets: options: from_json: false to_json: false + riverpod_generator: + generate_for: + - lib/src/model/**/*.dart + - lib/src/network/*.dart + - lib/src/db/*.dart + - lib/src/**/*_providers.dart diff --git a/lib/src/app.dart b/lib/src/app.dart index 24bf9d26f7..d32b2bf2b0 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -13,10 +13,10 @@ import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/model/settings/brightness.dart'; import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; import 'package:lichess_mobile/src/navigation.dart'; +import 'package:lichess_mobile/src/network/connectivity.dart'; import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/network/socket.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; /// The main application widget. diff --git a/lib/src/crashlytics.dart b/lib/src/crashlytics.dart index c87f39e270..4592e3bb6c 100644 --- a/lib/src/crashlytics.dart +++ b/lib/src/crashlytics.dart @@ -1,9 +1,6 @@ import 'package:firebase_crashlytics/firebase_crashlytics.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; -part 'crashlytics.g.dart'; - -@Riverpod(keepAlive: true) -FirebaseCrashlytics crashlytics(CrashlyticsRef ref) { +final crashlyticsProvider = Provider((ref) { return FirebaseCrashlytics.instance; -} +}); diff --git a/lib/src/model/game/game_history.dart b/lib/src/model/game/game_history.dart index dbea52b366..10516780b8 100644 --- a/lib/src/model/game/game_history.dart +++ b/lib/src/model/game/game_history.dart @@ -13,8 +13,8 @@ import 'package:lichess_mobile/src/model/game/game_repository.dart'; import 'package:lichess_mobile/src/model/game/game_storage.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/model/user/user_repository_providers.dart'; +import 'package:lichess_mobile/src/network/connectivity.dart'; import 'package:lichess_mobile/src/network/http.dart'; -import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:lichess_mobile/src/utils/riverpod.dart'; import 'package:result_extensions/result_extensions.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; diff --git a/lib/src/model/lobby/create_game_service.dart b/lib/src/model/lobby/create_game_service.dart index c3707fd590..be6a8c7a4a 100644 --- a/lib/src/model/lobby/create_game_service.dart +++ b/lib/src/model/lobby/create_game_service.dart @@ -22,6 +22,7 @@ typedef ChallengeResponse = ({ ChallengeDeclineReason? declineReason, }); +/// A provider for the [CreateGameService]. @riverpod CreateGameService createGameService(CreateGameServiceRef ref) { final service = CreateGameService(Logger('CreateGameService'), ref: ref); @@ -31,6 +32,7 @@ CreateGameService createGameService(CreateGameServiceRef ref) { return service; } +/// A service to create a new game from the lobby or from a challenge. class CreateGameService { CreateGameService(this._log, {required this.ref}); diff --git a/lib/src/model/notifications/notification_service.dart b/lib/src/model/notifications/notification_service.dart index c3c378750f..842834d025 100644 --- a/lib/src/model/notifications/notification_service.dart +++ b/lib/src/model/notifications/notification_service.dart @@ -10,9 +10,9 @@ import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/challenge/challenge_service.dart'; import 'package:lichess_mobile/src/model/correspondence/correspondence_service.dart'; import 'package:lichess_mobile/src/model/notifications/notifications.dart'; +import 'package:lichess_mobile/src/network/connectivity.dart'; import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/utils/badge_service.dart'; -import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:lichess_mobile/src/utils/l10n.dart'; import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; diff --git a/lib/src/model/user/user_repository_providers.dart b/lib/src/model/user/user_repository_providers.dart index cdd6c83240..09c2121bd4 100644 --- a/lib/src/model/user/user_repository_providers.dart +++ b/lib/src/model/user/user_repository_providers.dart @@ -20,6 +20,22 @@ Future user(UserRef ref, {required UserId id}) async { ); } +@riverpod +Future> userActivity( + UserActivityRef ref, { + required UserId id, +}) async { + return ref.withClientCacheFor( + (client) => UserRepository(client).getActivity(id), + // cache is important because the associated widget is in a [ListView] and + // the provider may be instanciated multiple times in a short period of time + // (e.g. when scrolling) + // TODO: consider debouncing the request instead of caching it, or make the + // request in the parent widget and pass the result to the child + const Duration(minutes: 1), + ); +} + @riverpod Future<(User, UserStatus)> userAndStatus( UserAndStatusRef ref, { diff --git a/lib/src/navigation.dart b/lib/src/navigation.dart index 3967d8e8ed..f5d96607f5 100644 --- a/lib/src/navigation.dart +++ b/lib/src/navigation.dart @@ -2,7 +2,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/l10n/l10n.dart'; -import 'package:lichess_mobile/src/utils/connectivity.dart'; +import 'package:lichess_mobile/src/network/connectivity.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/view/home/home_tab_screen.dart'; import 'package:lichess_mobile/src/view/puzzle/puzzle_tab_screen.dart'; diff --git a/lib/src/utils/connectivity.dart b/lib/src/network/connectivity.dart similarity index 100% rename from lib/src/utils/connectivity.dart rename to lib/src/network/connectivity.dart diff --git a/lib/src/utils/l10n.dart b/lib/src/utils/l10n.dart index 77af7c6680..6324bed96a 100644 --- a/lib/src/utils/l10n.dart +++ b/lib/src/utils/l10n.dart @@ -1,18 +1,19 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/l10n/l10n.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; -part 'l10n.g.dart'; - -typedef L10nState = ({ +typedef ActiveLocalizations = ({ Locale locale, AppLocalizations strings, }); -@Riverpod(keepAlive: true) -class L10n extends _$L10n { +/// A provider that exposes the current locale and localized strings. +final l10nProvider = + NotifierProvider(L10nNotifier.new); + +class L10nNotifier extends Notifier { @override - L10nState build() { + ActiveLocalizations build() { final observer = _LocaleObserver((locales) { _update(); }); @@ -27,7 +28,7 @@ class L10n extends _$L10n { state = _getLocale(); } - L10nState _getLocale() { + ActiveLocalizations _getLocale() { final locale = WidgetsBinding.instance.platformDispatcher.locale; return ( locale: locale, diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index 9a5c233d08..a87c4155fa 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -18,10 +18,10 @@ import 'package:lichess_mobile/src/model/engine/engine.dart'; import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; import 'package:lichess_mobile/src/model/game/game_repository_providers.dart'; import 'package:lichess_mobile/src/model/game/game_share_service.dart'; +import 'package:lichess_mobile/src/network/connectivity.dart'; import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; diff --git a/lib/src/view/game/game_screen.dart b/lib/src/view/game/game_screen.dart index d61308cfcb..584611617b 100644 --- a/lib/src/view/game/game_screen.dart +++ b/lib/src/view/game/game_screen.dart @@ -14,14 +14,12 @@ import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/game/game_loading_board.dart'; +import 'package:lichess_mobile/src/view/game/game_screen_providers.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'game_body.dart'; import 'game_common_widgets.dart'; -part 'game_screen.g.dart'; - /// Screen to play a game, or to show a challenge or to show current user's past games. /// /// The screen can be created in three ways: @@ -107,10 +105,13 @@ class _GameScreenState extends ConsumerState with RouteAware { @override Widget build(BuildContext context) { - final gameProvider = - _loadGameProvider(widget.seek, widget.challenge, widget.initialGameId); + final provider = currentGameProvider( + widget.seek, + widget.challenge, + widget.initialGameId, + ); - return ref.watch(gameProvider).when( + return ref.watch(provider).when( data: (data) { final ( gameFullId: gameId, @@ -129,11 +130,11 @@ class _GameScreenState extends ConsumerState with RouteAware { blackClockKey: _blackClockKey, boardKey: _boardKey, onLoadGameCallback: (id) { - ref.read(gameProvider.notifier).loadGame(id); + ref.read(provider.notifier).loadGame(id); }, onNewOpponentCallback: (game) { if (widget.source == _GameSource.lobby) { - ref.read(gameProvider.notifier).newOpponent(); + ref.read(provider.notifier).newOpponent(); } else { final savedSetup = ref.read(gameSetupPreferencesProvider); pushReplacementPlatformRoute( @@ -206,51 +207,3 @@ class _GameScreenState extends ConsumerState with RouteAware { ); } } - -@riverpod -class _LoadGame extends _$LoadGame { - @override - Future build( - GameSeek? seek, - ChallengeRequest? challenge, - GameFullId? gameId, - ) { - assert( - gameId != null || seek != null || challenge != null, - 'Either a seek, challenge or a game id must be provided.', - ); - - final service = ref.watch(createGameServiceProvider); - - if (seek != null) { - return service - .newLobbyGame(seek) - .then((id) => (gameFullId: id, challenge: null, declineReason: null)); - } else if (challenge != null) { - return service.newRealTimeChallenge(challenge); - } - - return Future.value( - (gameFullId: gameId!, challenge: null, declineReason: null), - ); - } - - /// Search for a new opponent (lobby only). - Future newOpponent() async { - if (seek != null) { - final service = ref.read(createGameServiceProvider); - state = const AsyncValue.loading(); - state = AsyncValue.data( - await service.newLobbyGame(seek!).then( - (id) => (gameFullId: id, challenge: null, declineReason: null), - ), - ); - } - } - - /// Load a game from its id. - void loadGame(GameFullId id) { - state = - AsyncValue.data((gameFullId: id, challenge: null, declineReason: null)); - } -} diff --git a/lib/src/view/game/game_screen_providers.dart b/lib/src/view/game/game_screen_providers.dart index 8e4fc8f449..305019f419 100644 --- a/lib/src/view/game/game_screen_providers.dart +++ b/lib/src/view/game/game_screen_providers.dart @@ -1,11 +1,66 @@ +import 'package:lichess_mobile/src/model/challenge/challenge.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/speed.dart'; import 'package:lichess_mobile/src/model/game/game.dart'; import 'package:lichess_mobile/src/model/game/game_controller.dart'; +import 'package:lichess_mobile/src/model/lobby/create_game_service.dart'; +import 'package:lichess_mobile/src/model/lobby/game_seek.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'game_screen_providers.g.dart'; +/// A provider that returns the currently loaded [GameFullId] for the [GameScreen]. +/// +/// If the [gameId] is provided, it will simply return it. +/// If not, it uses the [CreateGameService] to create a new game from the lobby or from a challenge. +@riverpod +class CurrentGame extends _$CurrentGame { + @override + Future build( + GameSeek? seek, + ChallengeRequest? challenge, + GameFullId? gameId, + ) { + assert( + gameId != null || seek != null || challenge != null, + 'Either a seek, challenge or a game id must be provided.', + ); + + final service = ref.watch(createGameServiceProvider); + + if (seek != null) { + return service + .newLobbyGame(seek) + .then((id) => (gameFullId: id, challenge: null, declineReason: null)); + } else if (challenge != null) { + return service.newRealTimeChallenge(challenge); + } + + return Future.value( + (gameFullId: gameId!, challenge: null, declineReason: null), + ); + } + + /// Search for a new opponent (lobby only). + Future newOpponent() async { + if (seek != null) { + final service = ref.read(createGameServiceProvider); + state = const AsyncValue.loading(); + state = AsyncValue.data( + await service.newLobbyGame(seek!).then( + (id) => (gameFullId: id, challenge: null, declineReason: null), + ), + ); + } + } + + /// Load a game from its id. + void loadGame(GameFullId id) { + state = + AsyncValue.data((gameFullId: id, challenge: null, declineReason: null)); + } +} + @riverpod class IsBoardTurned extends _$IsBoardTurned { @override diff --git a/lib/src/view/game/ping_rating.dart b/lib/src/view/game/ping_rating.dart index 4b1f416840..4ee011d17f 100644 --- a/lib/src/view/game/ping_rating.dart +++ b/lib/src/view/game/ping_rating.dart @@ -5,10 +5,8 @@ import 'package:lichess_mobile/src/network/socket.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -part 'ping_rating.g.dart'; - @riverpod -int pingRating(PingRatingRef ref) { +final pingRatingProvider = Provider.autoDispose((ref) { final ping = ref.watch(averageLagProvider).inMicroseconds / 1000; return ping == 0 @@ -20,7 +18,7 @@ int pingRating(PingRatingRef ref) { : ping < 500 ? 2 : 1; -} +}); class SocketPingRating extends ConsumerWidget { const SocketPingRating({ diff --git a/lib/src/view/home/create_game_options.dart b/lib/src/view/home/create_game_options.dart index c8918e4393..bbd676e4c1 100644 --- a/lib/src/view/home/create_game_options.dart +++ b/lib/src/view/home/create_game_options.dart @@ -2,9 +2,9 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; +import 'package:lichess_mobile/src/network/connectivity.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/over_the_board/over_the_board_screen.dart'; diff --git a/lib/src/view/home/home_tab_screen.dart b/lib/src/view/home/home_tab_screen.dart index 537ba7e5a1..8a1a3cdb07 100644 --- a/lib/src/view/home/home_tab_screen.dart +++ b/lib/src/view/home/home_tab_screen.dart @@ -11,9 +11,9 @@ import 'package:lichess_mobile/src/model/correspondence/correspondence_game_stor import 'package:lichess_mobile/src/model/game/game_history.dart'; import 'package:lichess_mobile/src/model/settings/home_preferences.dart'; import 'package:lichess_mobile/src/navigation.dart'; +import 'package:lichess_mobile/src/network/connectivity.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:lichess_mobile/src/utils/l10n.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; diff --git a/lib/src/view/play/online_bots_screen.dart b/lib/src/view/play/online_bots_screen.dart index 74355f23d7..356b555380 100644 --- a/lib/src/view/play/online_bots_screen.dart +++ b/lib/src/view/play/online_bots_screen.dart @@ -20,11 +20,8 @@ import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:lichess_mobile/src/widgets/user_full_name.dart'; import 'package:linkify/linkify.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:url_launcher/url_launcher.dart'; -part 'online_bots_screen.g.dart'; - // TODO(#796): remove when Leela featured bots special challenges are ready // https://github.com/lichess-org/mobile/issues/796 const _disabledBots = { @@ -34,8 +31,8 @@ const _disabledBots = { 'leelarookodds', }; -@riverpod -Future> _onlineBots(_OnlineBotsRef ref) async { +final _onlineBotsProvider = + FutureProvider.autoDispose>((ref) async { return ref.withClientCacheFor( (client) => UserRepository(client).getOnlineBots().then( (bots) => bots @@ -46,7 +43,7 @@ Future> _onlineBots(_OnlineBotsRef ref) async { ), const Duration(hours: 5), ); -} +}); class OnlineBotsScreen extends StatelessWidget { const OnlineBotsScreen(); diff --git a/lib/src/view/play/quick_game_button.dart b/lib/src/view/play/quick_game_button.dart index a22522b49e..c587b32aee 100644 --- a/lib/src/view/play/quick_game_button.dart +++ b/lib/src/view/play/quick_game_button.dart @@ -5,8 +5,8 @@ import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/lobby/game_seek.dart'; import 'package:lichess_mobile/src/model/lobby/game_setup_preferences.dart'; +import 'package:lichess_mobile/src/network/connectivity.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/game/game_screen.dart'; diff --git a/lib/src/view/puzzle/opening_screen.dart b/lib/src/view/puzzle/opening_screen.dart index 6089ce5dd9..62166de177 100644 --- a/lib/src/view/puzzle/opening_screen.dart +++ b/lib/src/view/puzzle/opening_screen.dart @@ -5,22 +5,17 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_angle.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_opening.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_providers.dart'; +import 'package:lichess_mobile/src/network/connectivity.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'puzzle_screen.dart'; -part 'opening_screen.g.dart'; - -@riverpod -Future<(bool, IMap, IList?)> _openings( - _OpeningsRef ref, -) async { +final _openingsProvider = FutureProvider.autoDispose< + (bool, IMap, IList?)>((ref) async { final connectivity = await ref.watch(connectivityChangesProvider.future); final savedOpenings = await ref.watch(savedOpeningBatchesProvider.future); IList? onlineOpenings; @@ -30,7 +25,7 @@ Future<(bool, IMap, IList?)> _openings( onlineOpenings = null; } return (connectivity.isOnline, savedOpenings, onlineOpenings); -} +}); class OpeningThemeScreen extends StatelessWidget { const OpeningThemeScreen({super.key}); diff --git a/lib/src/view/puzzle/puzzle_screen.dart b/lib/src/view/puzzle/puzzle_screen.dart index ddd843f7f5..74dddd04cc 100644 --- a/lib/src/view/puzzle/puzzle_screen.dart +++ b/lib/src/view/puzzle/puzzle_screen.dart @@ -21,15 +21,14 @@ import 'package:lichess_mobile/src/model/puzzle/puzzle_service.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_theme.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/navigation.dart'; +import 'package:lichess_mobile/src/network/connectivity.dart'; import 'package:lichess_mobile/src/network/http.dart'; -import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:lichess_mobile/src/utils/immersive_mode.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/share.dart'; import 'package:lichess_mobile/src/view/account/rating_pref_aware.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; -import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; import 'package:lichess_mobile/src/view/game/archived_game_screen.dart'; import 'package:lichess_mobile/src/view/puzzle/puzzle_settings_screen.dart'; import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; diff --git a/lib/src/view/puzzle/puzzle_tab_screen.dart b/lib/src/view/puzzle/puzzle_tab_screen.dart index c83462a20e..b844322967 100644 --- a/lib/src/view/puzzle/puzzle_tab_screen.dart +++ b/lib/src/view/puzzle/puzzle_tab_screen.dart @@ -13,10 +13,10 @@ import 'package:lichess_mobile/src/model/puzzle/puzzle_providers.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_service.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_theme.dart'; import 'package:lichess_mobile/src/navigation.dart'; +import 'package:lichess_mobile/src/network/connectivity.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/puzzle_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; diff --git a/lib/src/view/puzzle/puzzle_themes_screen.dart b/lib/src/view/puzzle/puzzle_themes_screen.dart index b65981abce..e29e793f05 100644 --- a/lib/src/view/puzzle/puzzle_themes_screen.dart +++ b/lib/src/view/puzzle/puzzle_themes_screen.dart @@ -5,8 +5,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_angle.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_providers.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_theme.dart'; +import 'package:lichess_mobile/src/network/connectivity.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/puzzle/opening_screen.dart'; @@ -16,18 +16,14 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'puzzle_screen.dart'; -part 'puzzle_themes_screen.g.dart'; - @riverpod -Future< +final _themesProvider = FutureProvider.autoDispose< ( bool, IMap, IMap?, bool, - )> _themes( - _ThemesRef ref, -) async { + )>((ref) async { final connectivity = await ref.watch(connectivityChangesProvider.future); final savedThemes = await ref.watch(savedThemeBatchesProvider.future); IMap? onlineThemes; @@ -43,7 +39,7 @@ Future< onlineThemes, savedOpenings.isNotEmpty ); -} +}); class PuzzleThemesScreen extends StatelessWidget { const PuzzleThemesScreen({super.key}); diff --git a/lib/src/view/relation/following_screen.dart b/lib/src/view/relation/following_screen.dart index 18a72bbb7f..b46870a346 100644 --- a/lib/src/view/relation/following_screen.dart +++ b/lib/src/view/relation/following_screen.dart @@ -16,18 +16,13 @@ import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:lichess_mobile/src/widgets/user_list_tile.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; -part 'following_screen.g.dart'; - -@riverpod -Future<(IList, IList)> _getFollowingAndOnlines( - _GetFollowingAndOnlinesRef ref, -) async { +final _getFollowingAndOnlinesProvider = + FutureProvider.autoDispose<(IList, IList)>((ref) async { final following = await ref.watch(followingProvider.future); final onlines = await ref.watch(onlineFriendsProvider.future); return (following, onlines); -} +}); class FollowingScreen extends StatelessWidget { const FollowingScreen({super.key}); diff --git a/lib/src/view/tools/tools_tab_screen.dart b/lib/src/view/tools/tools_tab_screen.dart index f469a5ed50..62cd402965 100644 --- a/lib/src/view/tools/tools_tab_screen.dart +++ b/lib/src/view/tools/tools_tab_screen.dart @@ -5,8 +5,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/navigation.dart'; +import 'package:lichess_mobile/src/network/connectivity.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; diff --git a/lib/src/view/user/recent_games.dart b/lib/src/view/user/recent_games.dart index 1e8d8c2c0f..7a7651e7cb 100644 --- a/lib/src/view/user/recent_games.dart +++ b/lib/src/view/user/recent_games.dart @@ -3,8 +3,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/game/game_history.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; +import 'package:lichess_mobile/src/network/connectivity.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/game/game_list_tile.dart'; diff --git a/lib/src/view/user/user_activity.dart b/lib/src/view/user/user_activity.dart index d5cd531997..99d073b7af 100644 --- a/lib/src/view/user/user_activity.dart +++ b/lib/src/view/user/user_activity.dart @@ -1,12 +1,9 @@ -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; -import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; -import 'package:lichess_mobile/src/model/user/user_repository.dart'; -import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/model/user/user_repository_providers.dart'; import 'package:lichess_mobile/src/styles/lichess_colors.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; @@ -15,28 +12,9 @@ import 'package:lichess_mobile/src/view/account/rating_pref_aware.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/rating.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'user_activity.g.dart'; final _dateFormatter = DateFormat.yMMMd(); -@riverpod -Future> _userActivity( - _UserActivityRef ref, { - required UserId id, -}) async { - return ref.withClientCacheFor( - (client) => UserRepository(client).getActivity(id), - // cache is important because the associated widget is in a [ListView] and - // the provider may be instanciated multiple times in a short period of time - // (e.g. when scrolling) - // TODO: consider debouncing the request instead of caching it, or make the - // request in the parent widget and pass the result to the child - const Duration(minutes: 1), - ); -} - class UserActivityWidget extends ConsumerWidget { const UserActivityWidget({this.user, super.key}); @@ -45,7 +23,7 @@ class UserActivityWidget extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final activity = user != null - ? ref.watch(_userActivityProvider(id: user!.id)) + ? ref.watch(userActivityProvider(id: user!.id)) : ref.watch(accountActivityProvider); return activity.when( diff --git a/lib/src/view/watch/watch_tab_screen.dart b/lib/src/view/watch/watch_tab_screen.dart index fd44e08d1b..3a881d123d 100644 --- a/lib/src/view/watch/watch_tab_screen.dart +++ b/lib/src/view/watch/watch_tab_screen.dart @@ -25,9 +25,6 @@ import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; import 'package:lichess_mobile/src/widgets/user_full_name.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'watch_tab_screen.g.dart'; const _featuredChannelsSet = ISetConst({ TvChannel.best, @@ -36,8 +33,8 @@ const _featuredChannelsSet = ISetConst({ TvChannel.rapid, }); -@riverpod -Future> featuredChannels(FeaturedChannelsRef ref) async { +final featuredChannelsProvider = + FutureProvider.autoDispose>((ref) async { return ref.withClientCacheFor( (client) async { final channels = await TvRepository(client).channels(); @@ -60,7 +57,7 @@ Future> featuredChannels(FeaturedChannelsRef ref) async { }, const Duration(minutes: 5), ); -} +}); class WatchTabScreen extends ConsumerStatefulWidget { const WatchTabScreen({super.key}); diff --git a/lib/src/widgets/feedback.dart b/lib/src/widgets/feedback.dart index 2bc129e248..c637d2b7c8 100644 --- a/lib/src/widgets/feedback.dart +++ b/lib/src/widgets/feedback.dart @@ -2,8 +2,8 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; +import 'package:lichess_mobile/src/network/connectivity.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:signal_strength_indicator/signal_strength_indicator.dart'; diff --git a/test/test_container.dart b/test/test_container.dart index bdc0e711fa..6c9ca502e9 100644 --- a/test/test_container.dart +++ b/test/test_container.dart @@ -8,9 +8,9 @@ import 'package:lichess_mobile/src/db/database.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; import 'package:lichess_mobile/src/model/notifications/notification_service.dart'; +import 'package:lichess_mobile/src/network/connectivity.dart'; import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/network/socket.dart'; -import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import './fake_crashlytics.dart'; diff --git a/test/test_provider_scope.dart b/test/test_provider_scope.dart index 088256e265..854354a1fb 100644 --- a/test/test_provider_scope.dart +++ b/test/test_provider_scope.dart @@ -17,9 +17,9 @@ import 'package:lichess_mobile/src/model/auth/session_storage.dart'; import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; import 'package:lichess_mobile/src/model/notifications/notification_service.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; +import 'package:lichess_mobile/src/network/connectivity.dart'; import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/network/socket.dart'; -import 'package:lichess_mobile/src/utils/connectivity.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:visibility_detector/visibility_detector.dart'; From 2aaffc38d01878db27e99b6f607e82b0041149fc Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Wed, 16 Oct 2024 23:04:06 +0200 Subject: [PATCH 492/979] move PGN tree view to dedicated file --- .../model/analysis/analysis_controller.dart | 2 +- lib/src/view/analysis/analysis_board.dart | 2 +- lib/src/view/analysis/tree_view.dart | 1124 +--------------- lib/src/view/game/game_result_dialog.dart | 2 +- .../view/{analysis => pgn}/annotations.dart | 0 lib/src/view/pgn/pgn_tree_view.dart | 1126 +++++++++++++++++ test/view/analysis/analysis_screen_test.dart | 2 +- 7 files changed, 1131 insertions(+), 1127 deletions(-) rename lib/src/view/{analysis => pgn}/annotations.dart (100%) create mode 100644 lib/src/view/pgn/pgn_tree_view.dart diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index 97b88bdc36..377648aebc 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -19,8 +19,8 @@ import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; import 'package:lichess_mobile/src/model/engine/work.dart'; import 'package:lichess_mobile/src/model/game/player.dart'; import 'package:lichess_mobile/src/utils/rate_limit.dart'; -import 'package:lichess_mobile/src/view/analysis/tree_view.dart'; import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; +import 'package:lichess_mobile/src/view/pgn/pgn_tree_view.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'analysis_controller.freezed.dart'; diff --git a/lib/src/view/analysis/analysis_board.dart b/lib/src/view/analysis/analysis_board.dart index 7e6e1109ae..04dbca98e6 100644 --- a/lib/src/view/analysis/analysis_board.dart +++ b/lib/src/view/analysis/analysis_board.dart @@ -15,7 +15,7 @@ import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/eval.dart'; import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; -import 'package:lichess_mobile/src/view/analysis/annotations.dart'; +import 'package:lichess_mobile/src/view/pgn/annotations.dart'; class AnalysisBoard extends ConsumerStatefulWidget { const AnalysisBoard( diff --git a/lib/src/view/analysis/tree_view.dart b/lib/src/view/analysis/tree_view.dart index af5695e03d..89bef6120c 100644 --- a/lib/src/view/analysis/tree_view.dart +++ b/lib/src/view/analysis/tree_view.dart @@ -1,30 +1,12 @@ -import 'package:collection/collection.dart'; -import 'package:dartchess/dartchess.dart'; -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lichess_mobile/src/model/account/account_preferences.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; -import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; import 'package:lichess_mobile/src/model/analysis/opening_service.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; -import 'package:lichess_mobile/src/model/common/node.dart'; -import 'package:lichess_mobile/src/model/common/uci.dart'; -import 'package:lichess_mobile/src/utils/duration.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; -import 'package:lichess_mobile/src/utils/rate_limit.dart'; -import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; -import 'package:lichess_mobile/src/widgets/buttons.dart'; -import 'package:lichess_mobile/src/widgets/list.dart'; +import 'package:lichess_mobile/src/view/pgn/pgn_tree_view.dart'; -import 'annotations.dart'; - -// fast replay debounce delay, same as piece animation duration, to avoid piece -// animation jank at the end of the replay -const kFastReplayDebounceDelay = Duration(milliseconds: 150); const kOpeningHeaderHeight = 32.0; -const kInlineMoveSpacing = 3.0; class AnalysisTreeView extends ConsumerWidget { const AnalysisTreeView( @@ -70,1110 +52,6 @@ class AnalysisTreeView extends ConsumerWidget { } } -/// Callbacks for interaction with [DebouncedPgnTreeView] -abstract class PgnTreeNotifier { - void expandVariations(UciPath path); - void collapseVariations(UciPath path); - void promoteVariation(UciPath path, bool toMainLine); - void deleteFromHere(UciPath path); - void userJump(UciPath path); -} - -/// Displays a tree-like view of a PGN game's moves. Path changes are debounced to avoid rebuilding the whole tree on every move. -/// -/// For example, the PGN 1. e4 e5 (1... d5) (1... Nc6) 2. Nf3 Nc6 (2... a5) 3. Bc4 * will be displayed as: -/// 1. e4 e5 // [_MainLinePart] -/// |- 1... d5 // [_SideLinePart] -/// |- 1... Nc6 // [_SideLinePart] -/// 2. Nf3 Nc6 (2... a5) 3. Bc4 // [_MainLinePart], with inline sideline -/// Short sidelines without any branching are displayed inline with their parent line. -/// Longer sidelines are displayed on a new line and indented. -/// The mainline is split into parts whenever a move has a non-inline sideline, this corresponds to the [_MainLinePart] widget. -/// Similarly, a [_SideLinePart] contains the moves sequence of a sideline where each node has only one child. -class DebouncedPgnTreeView extends ConsumerStatefulWidget { - const DebouncedPgnTreeView({ - required this.root, - required this.currentPath, - required this.pgnRootComments, - required this.notifier, - }); - - /// Root of the PGN tree to display - final ViewRoot root; - - /// Path to the currently selected move in the tree - final UciPath currentPath; - - /// Comments associated with the root node - final IList? pgnRootComments; - - /// Callbacks for when the user interacts with the tree view, e.g. selecting a different move or collapsing variations - final PgnTreeNotifier notifier; - - @override - ConsumerState createState() => - _DebouncedPgnTreeViewState(); -} - -class _DebouncedPgnTreeViewState extends ConsumerState { - final currentMoveKey = GlobalKey(); - final _debounce = Debouncer(kFastReplayDebounceDelay); - - /// Path to the currently selected move in the tree. When widget.currentPath changes rapidly, we debounce the change to avoid rebuilding the whole tree on every move. - late UciPath pathToCurrentMove; - - @override - void initState() { - super.initState(); - pathToCurrentMove = widget.currentPath; - WidgetsBinding.instance.addPostFrameCallback((_) { - if (currentMoveKey.currentContext != null) { - Scrollable.ensureVisible( - currentMoveKey.currentContext!, - alignment: 0.5, - alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd, - ); - } - }); - } - - @override - void dispose() { - _debounce.dispose(); - super.dispose(); - } - - @override - void didUpdateWidget(covariant DebouncedPgnTreeView oldWidget) { - super.didUpdateWidget(oldWidget); - - if (oldWidget.currentPath != widget.currentPath) { - // debouncing the current path change to avoid rebuilding when using - // the fast replay buttons - _debounce(() { - setState(() { - pathToCurrentMove = widget.currentPath; - }); - WidgetsBinding.instance.addPostFrameCallback((_) { - if (currentMoveKey.currentContext != null) { - Scrollable.ensureVisible( - currentMoveKey.currentContext!, - duration: const Duration(milliseconds: 200), - curve: Curves.easeIn, - alignment: 0.5, - alignmentPolicy: ScrollPositionAlignmentPolicy.explicit, - ); - } - }); - }); - } - } - - // This is the most expensive part of the pgn tree view because of the tree - // that may be very large. - // Great care must be taken to avoid unnecessary rebuilds. - // This should actually rebuild only when the current path changes or a new node - // is added. - // Debouncing the current path change is necessary to avoid rebuilding when - // using the fast replay buttons. - @override - Widget build(BuildContext context) { - final shouldShowComments = ref.watch( - analysisPreferencesProvider.select((value) => value.showPgnComments), - ); - - final shouldShowAnnotations = ref.watch( - analysisPreferencesProvider.select((value) => value.showAnnotations), - ); - - return _PgnTreeView( - root: widget.root, - rootComments: widget.pgnRootComments, - params: ( - shouldShowAnnotations: shouldShowAnnotations, - shouldShowComments: shouldShowComments, - currentMoveKey: currentMoveKey, - pathToCurrentMove: pathToCurrentMove, - notifier: widget.notifier, - ), - ); - } -} - -/// A group of parameters that are passed through various parts of the tree view -/// and ultimately evaluated in the [InlineMove] widget. -/// -/// Grouped in this record to improve readability. -typedef _PgnTreeViewParams = ({ - /// Path to the currently selected move in the tree. - UciPath pathToCurrentMove, - - /// Whether to show NAG annotations like '!' and '??'. - bool shouldShowAnnotations, - - /// Whether to show comments associated with the moves. - bool shouldShowComments, - - /// Key that will we assigned to the widget corresponding to [pathToCurrentMove]. - /// Can be used e.g. to ensure that the current move is visible on the screen. - GlobalKey currentMoveKey, - - /// Callbacks for when the user interacts with the tree view, e.g. selecting a different move. - PgnTreeNotifier notifier, -}); - -/// Whether to display the sideline inline. -/// -/// Sidelines are usually rendered on a new line and indented. -/// However sidelines are rendered inline (in parantheses) if the side line has no branching and is less than 6 moves deep. -bool _displaySideLineAsInline(ViewBranch node, [int depth = 0]) { - if (depth == 6) return false; - if (node.children.isEmpty) return true; - if (node.children.length > 1) return false; - return _displaySideLineAsInline(node.children.first, depth + 1); -} - -/// Returns whether this node has a sideline that should not be displayed inline. -bool _hasNonInlineSideLine(ViewNode node) => - node.children.length > 2 || - (node.children.length == 2 && !_displaySideLineAsInline(node.children[1])); - -/// Splits the mainline into parts, where each part is a sequence of moves that are displayed on the same line. -/// -/// A part ends when a mainline node has a sideline that should not be displayed inline. -Iterable> _mainlineParts(ViewRoot root) => - [root, ...root.mainline] - .splitAfter(_hasNonInlineSideLine) - .takeWhile((nodes) => nodes.firstOrNull?.children.isNotEmpty == true); - -class _PgnTreeView extends StatefulWidget { - const _PgnTreeView({ - required this.root, - required this.rootComments, - required this.params, - }); - - /// Root of the PGN tree - final ViewRoot root; - - /// Comments associated with the root node - final IList? rootComments; - - final _PgnTreeViewParams params; - - @override - State<_PgnTreeView> createState() => _PgnTreeViewState(); -} - -/// A record that holds the rendered parts of a subtree. -typedef _CachedRenderedSubtree = ({ - /// The mainline part of the subtree. - _MainLinePart mainLinePart, - - /// The sidelines part of the subtree. - /// - /// This is nullable since the very last mainline part might not have any sidelines. - _IndentedSideLines? sidelines, - - /// Whether the subtree contains the current move. - bool containsCurrentMove, -}); - -class _PgnTreeViewState extends State<_PgnTreeView> { - /// Caches the result of [_mainlineParts], it only needs to be recalculated when the root changes, - /// but not when `params.pathToCurrentMove` changes. - List> mainlineParts = []; - - /// Cache of the top-level subtrees obtained from the last `build()` method. - /// - /// Building the whole tree is expensive, so we cache the subtrees that did not change when the current move changes. - /// The framework will skip the `build()` of each subtree since the widget reference is the same. - List<_CachedRenderedSubtree> subtrees = []; - - UciPath _mainlinePartOfCurrentPath() { - var path = UciPath.empty; - for (final node in widget.root.mainline) { - if (!widget.params.pathToCurrentMove.contains(path + node.id)) { - break; - } - path = path + node.id; - } - return path; - } - - List<_CachedRenderedSubtree> _buildChangedSubtrees({ - required bool fullRebuild, - }) { - var path = UciPath.empty; - return mainlineParts.mapIndexed( - (i, mainlineNodes) { - final mainlineInitialPath = path; - - final sidelineInitialPath = UciPath.join( - path, - UciPath.fromIds( - mainlineNodes - .take(mainlineNodes.length - 1) - .map((n) => n.children.first.id), - ), - ); - - path = sidelineInitialPath; - if (mainlineNodes.last.children.isNotEmpty) { - path = path + mainlineNodes.last.children.first.id; - } - - final mainlinePartOfCurrentPath = _mainlinePartOfCurrentPath(); - final containsCurrentMove = - mainlinePartOfCurrentPath.size > mainlineInitialPath.size && - mainlinePartOfCurrentPath.size <= path.size; - - if (fullRebuild || - subtrees[i].containsCurrentMove || - containsCurrentMove) { - // Skip the first node which is the continuation of the mainline - final sidelineNodes = mainlineNodes.last.children.skip(1); - return ( - mainLinePart: _MainLinePart( - params: widget.params, - initialPath: mainlineInitialPath, - nodes: mainlineNodes, - ), - sidelines: sidelineNodes.isNotEmpty - ? _IndentedSideLines( - sidelineNodes, - parent: mainlineNodes.last, - params: widget.params, - initialPath: sidelineInitialPath, - nesting: 1, - ) - : null, - containsCurrentMove: containsCurrentMove, - ); - } else { - // Avoid expensive rebuilds ([State.build]) of the entire PGN tree by caching parts of the tree that did not change across a path change - return subtrees[i]; - } - }, - ).toList(growable: false); - } - - void _updateLines({required bool fullRebuild}) { - setState(() { - if (fullRebuild) { - mainlineParts = _mainlineParts(widget.root).toList(growable: false); - } - - subtrees = _buildChangedSubtrees(fullRebuild: fullRebuild); - }); - } - - @override - void initState() { - super.initState(); - _updateLines(fullRebuild: true); - } - - @override - void didUpdateWidget(covariant _PgnTreeView oldWidget) { - super.didUpdateWidget(oldWidget); - _updateLines(fullRebuild: oldWidget.root != widget.root); - } - - @override - Widget build(BuildContext context) { - final rootComments = widget.rootComments?.map((c) => c.text).nonNulls ?? []; - return Padding( - padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 10), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // trick to make auto-scroll work when returning to the root position - if (widget.params.pathToCurrentMove.isEmpty) - SizedBox.shrink(key: widget.params.currentMoveKey), - - if (widget.params.shouldShowComments && rootComments.isNotEmpty) - Text.rich( - TextSpan( - children: _comments( - rootComments, - textStyle: _baseTextStyle, - ), - ), - ), - ...subtrees - .map( - (part) => [ - part.mainLinePart, - if (part.sidelines != null) part.sidelines!, - ], - ) - .flattened, - ], - ), - ); - } -} - -List _buildInlineSideLine({ - required ViewBranch firstNode, - required ViewNode parent, - required UciPath initialPath, - required TextStyle textStyle, - required bool followsComment, - required _PgnTreeViewParams params, -}) { - textStyle = textStyle.copyWith( - fontSize: textStyle.fontSize != null ? textStyle.fontSize! - 2.0 : null, - ); - - final sidelineNodes = [firstNode, ...firstNode.mainline]; - - var path = initialPath; - return [ - if (followsComment) const WidgetSpan(child: SizedBox(width: 4.0)), - ...sidelineNodes.mapIndexedAndLast( - (i, node, last) { - final pathToNode = path; - path = path + node.id; - - return [ - if (i == 0) ...[ - if (followsComment) const WidgetSpan(child: SizedBox(width: 4.0)), - TextSpan( - text: '(', - style: textStyle, - ), - ], - ..._moveWithComment( - node, - lineInfo: ( - type: _LineType.inlineSideline, - startLine: i == 0 || sidelineNodes[i - 1].hasTextComment - ), - pathToNode: pathToNode, - textStyle: textStyle, - params: params, - ), - if (last) - TextSpan( - text: ')', - style: textStyle, - ), - ]; - }, - ).flattened, - const WidgetSpan(child: SizedBox(width: 4.0)), - ]; -} - -const _baseTextStyle = TextStyle( - fontSize: 16.0, - height: 1.5, -); - -/// The different types of lines (move sequences) that are displayed in the tree view. -enum _LineType { - /// (A part of) the game's main line. - mainline, - - /// A sideline branching off the main line or a parent sideline. - /// - /// Each sideline is rendered on a new line and indented. - sideline, - - /// A short sideline without any branching, displayed in parantheses inline with it's parent line. - inlineSideline, -} - -/// Metadata about a move's role in the tree view. -typedef _LineInfo = ({_LineType type, bool startLine}); - -List _moveWithComment( - ViewBranch branch, { - required TextStyle textStyle, - required _LineInfo lineInfo, - required UciPath pathToNode, - required _PgnTreeViewParams params, - - /// Optional [GlobalKey] that will be assigned to the [InlineMove] widget. - /// - /// It should only be set if it is the first move of a sideline. - /// We use this to track the position of the first move widget. See [_SideLinePart.firstMoveKey]. - GlobalKey? firstMoveKey, -}) { - return [ - WidgetSpan( - alignment: PlaceholderAlignment.middle, - child: InlineMove( - key: firstMoveKey, - branch: branch, - lineInfo: lineInfo, - path: pathToNode + branch.id, - textStyle: textStyle, - params: params, - ), - ), - if (params.shouldShowComments && branch.hasTextComment) - ..._comments(branch.textComments, textStyle: textStyle), - ]; -} - -/// A widget that renders part of a sideline, where each move is displayed on the same line without branching. -/// -/// Each node in the sideline has only one child (or two children where the second child is rendered as an inline sideline). -class _SideLinePart extends ConsumerWidget { - _SideLinePart( - this.nodes, { - required this.initialPath, - required this.firstMoveKey, - required this.params, - }) : assert(nodes.isNotEmpty); - - final List nodes; - - final UciPath initialPath; - - /// The key that will be assigned to the first move in this sideline. - /// - /// This is needed so that the indent guidelines can be drawn correctly. - final GlobalKey firstMoveKey; - - final _PgnTreeViewParams params; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final textStyle = _baseTextStyle.copyWith( - color: _textColor(context, 0.6), - fontSize: _baseTextStyle.fontSize! - 1.0, - ); - - var path = initialPath + nodes.first.id; - final moves = [ - ..._moveWithComment( - nodes.first, - lineInfo: ( - type: _LineType.sideline, - startLine: true, - ), - firstMoveKey: firstMoveKey, - pathToNode: initialPath, - textStyle: textStyle, - params: params, - ), - ...nodes.take(nodes.length - 1).map( - (node) { - final moves = [ - ..._moveWithComment( - node.children.first, - lineInfo: ( - type: _LineType.sideline, - startLine: node.hasTextComment, - ), - pathToNode: path, - textStyle: textStyle, - params: params, - ), - if (node.children.length == 2 && - _displaySideLineAsInline(node.children[1])) - ..._buildInlineSideLine( - followsComment: node.children.first.hasTextComment, - firstNode: node.children[1], - parent: node, - initialPath: path, - textStyle: textStyle, - params: params, - ), - ]; - path = path + node.children.first.id; - return moves; - }, - ).flattened, - ]; - - return Text.rich( - TextSpan( - children: moves, - ), - ); - } -} - -/// A widget that renders part of the mainline. -/// -/// A part of the mainline is rendered on a single line. See [_mainlineParts]. -class _MainLinePart extends ConsumerWidget { - const _MainLinePart({ - required this.initialPath, - required this.params, - required this.nodes, - }); - - final UciPath initialPath; - - final List nodes; - - final _PgnTreeViewParams params; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final textStyle = _baseTextStyle.copyWith( - color: _textColor(context, 0.9), - ); - - var path = initialPath; - return Text.rich( - TextSpan( - children: nodes - .takeWhile((node) => node.children.isNotEmpty) - .mapIndexed( - (i, node) { - final mainlineNode = node.children.first; - final moves = [ - _moveWithComment( - mainlineNode, - lineInfo: ( - type: _LineType.mainline, - startLine: i == 0 || (node as ViewBranch).hasTextComment, - ), - pathToNode: path, - textStyle: textStyle, - params: params, - ), - if (node.children.length == 2 && - _displaySideLineAsInline(node.children[1])) ...[ - _buildInlineSideLine( - followsComment: mainlineNode.hasTextComment, - firstNode: node.children[1], - parent: node, - initialPath: path, - textStyle: textStyle, - params: params, - ), - ], - ]; - path = path + mainlineNode.id; - return moves.flattened; - }, - ) - .flattened - .toList(growable: false), - ), - ); - } -} - -/// A widget that renders a sideline. -/// -/// The moves are rendered on the same line (see [_SideLinePart]) until further -/// branching is encountered, at which point the children sidelines are rendered -/// on new lines and indented (see [_IndentedSideLines]). -class _SideLine extends StatelessWidget { - const _SideLine({ - required this.firstNode, - required this.parent, - required this.firstMoveKey, - required this.initialPath, - required this.params, - required this.nesting, - }); - - final ViewBranch firstNode; - final ViewNode parent; - final GlobalKey firstMoveKey; - final UciPath initialPath; - final _PgnTreeViewParams params; - final int nesting; - - List _getSidelinePartNodes() { - final sidelineNodes = [firstNode]; - while (sidelineNodes.last.children.isNotEmpty && - !_hasNonInlineSideLine(sidelineNodes.last)) { - sidelineNodes.add(sidelineNodes.last.children.first); - } - return sidelineNodes.toList(growable: false); - } - - @override - Widget build(BuildContext context) { - final sidelinePartNodes = _getSidelinePartNodes(); - - final lastNodeChildren = sidelinePartNodes.last.children; - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _SideLinePart( - sidelinePartNodes, - firstMoveKey: firstMoveKey, - initialPath: initialPath, - params: params, - ), - if (lastNodeChildren.isNotEmpty) - _IndentedSideLines( - lastNodeChildren, - parent: sidelinePartNodes.last, - initialPath: UciPath.join( - initialPath, - UciPath.fromIds(sidelinePartNodes.map((node) => node.id)), - ), - params: params, - nesting: nesting + 1, - ), - ], - ); - } -} - -class _IndentPainter extends CustomPainter { - const _IndentPainter({ - required this.sideLineStartPositions, - required this.color, - required this.padding, - }); - - final List sideLineStartPositions; - - final Color color; - - final double padding; - - @override - void paint(Canvas canvas, Size size) { - if (sideLineStartPositions.isNotEmpty) { - final paint = Paint() - ..strokeWidth = 1.5 - ..color = color - ..strokeCap = StrokeCap.round - ..style = PaintingStyle.stroke; - - final origin = Offset(-padding, 0); - - final path = Path()..moveTo(origin.dx, origin.dy); - path.lineTo(origin.dx, sideLineStartPositions.last.dy); - for (final position in sideLineStartPositions) { - path.moveTo(origin.dx, position.dy); - path.lineTo(origin.dx + padding / 2, position.dy); - } - canvas.drawPath(path, paint); - } - } - - @override - bool shouldRepaint(_IndentPainter oldDelegate) { - return oldDelegate.sideLineStartPositions != sideLineStartPositions || - oldDelegate.color != color; - } -} - -/// A widget that displays indented sidelines. -/// -/// Will show one ore more sidelines indented on their own line and add indent -/// guides. -/// If there are hidden lines, a "+" button is displayed to expand them. -class _IndentedSideLines extends StatefulWidget { - const _IndentedSideLines( - this.sideLines, { - required this.parent, - required this.initialPath, - required this.params, - required this.nesting, - }); - - final Iterable sideLines; - - final ViewNode parent; - - final UciPath initialPath; - - final _PgnTreeViewParams params; - - final int nesting; - - @override - State<_IndentedSideLines> createState() => _IndentedSideLinesState(); -} - -class _IndentedSideLinesState extends State<_IndentedSideLines> { - /// Keys for the first move of each sideline. - /// - /// Used to calculate the position of the indent guidelines. The keys are - /// assigned to the first move of each sideline. The position of the keys is - /// used to calculate the position of the indent guidelines. A [GlobalKey] is - /// necessary because the exact position of the first move is not known until the - /// widget is rendered, as the vertical space can vary depending on the length of - /// the line, and if the line is wrapped. - late List _sideLinesStartKeys; - - /// The position of the first move of each sideline computed relative to the column and derived from the [GlobalKey] found in [_sideLinesStartKeys]. - List _sideLineStartPositions = []; - - /// The [GlobalKey] for the column that contains the side lines. - final GlobalKey _columnKey = GlobalKey(); - - /// Redraws the indents on demand. - /// - /// Will re-generate the [GlobalKey]s for the first move of each sideline and - /// calculate the position of the indents in a post-frame callback. - void _redrawIndents() { - _sideLinesStartKeys = List.generate( - _visibleSideLines.length + (_hasHiddenLines ? 1 : 0), - (_) => GlobalKey(), - ); - WidgetsBinding.instance.addPostFrameCallback((_) { - final RenderBox? columnBox = - _columnKey.currentContext?.findRenderObject() as RenderBox?; - final Offset rowOffset = - columnBox?.localToGlobal(Offset.zero) ?? Offset.zero; - - final positions = _sideLinesStartKeys.map((key) { - final context = key.currentContext; - final renderBox = context?.findRenderObject() as RenderBox?; - final height = renderBox?.size.height ?? 0; - final offset = renderBox?.localToGlobal(Offset.zero) ?? Offset.zero; - return Offset(offset.dx, offset.dy + height / 2) - rowOffset; - }).toList(growable: false); - - setState(() { - _sideLineStartPositions = positions; - }); - }); - } - - bool get _hasHiddenLines => widget.sideLines.any((node) => node.isHidden); - - Iterable get _visibleSideLines => - widget.sideLines.whereNot((node) => node.isHidden); - - @override - void initState() { - super.initState(); - _redrawIndents(); - } - - @override - void didUpdateWidget(covariant _IndentedSideLines oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.sideLines != widget.sideLines) { - _redrawIndents(); - } - } - - @override - Widget build(BuildContext context) { - final sideLineWidgets = _visibleSideLines - .mapIndexed( - (i, firstSidelineNode) => _SideLine( - firstNode: firstSidelineNode, - parent: widget.parent, - firstMoveKey: _sideLinesStartKeys[i], - initialPath: widget.initialPath, - params: widget.params, - nesting: widget.nesting, - ), - ) - .toList(growable: false); - - final padding = widget.nesting < 6 ? 12.0 : 0.0; - - return Padding( - padding: EdgeInsets.only(left: padding), - child: CustomPaint( - painter: _IndentPainter( - sideLineStartPositions: _sideLineStartPositions, - color: _textColor(context, 0.6)!, - padding: padding, - ), - child: Column( - key: _columnKey, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ...sideLineWidgets, - if (_hasHiddenLines) - GestureDetector( - child: Icon( - Icons.add_box, - color: _textColor(context, 0.6), - key: _sideLinesStartKeys.last, - size: _baseTextStyle.fontSize! + 5, - ), - onTap: () { - widget.params.notifier.expandVariations(widget.initialPath); - }, - ), - ], - ), - ), - ); - } -} - -Color? _textColor( - BuildContext context, - double opacity, { - int? nag, -}) { - final defaultColor = Theme.of(context).platform == TargetPlatform.android - ? Theme.of(context).textTheme.bodyLarge?.color?.withValues(alpha: opacity) - : CupertinoTheme.of(context) - .textTheme - .textStyle - .color - ?.withValues(alpha: opacity); - - return nag != null && nag > 0 ? nagColor(nag) : defaultColor; -} - -/// A widget that displays a single move in the tree view. -/// -/// The move can optionnally be preceded by an index, and followed by a nag annotation. -/// The move is displayed as a clickable button that will jump to the move when pressed. -/// The move is highlighted if it is the current move. -/// A long press on the move will display a context menu with options to promote the move to the main line, collapse variations, etc. -class InlineMove extends ConsumerWidget { - const InlineMove({ - required this.branch, - required this.path, - required this.textStyle, - required this.lineInfo, - required this.params, - super.key, - }); - - final ViewBranch branch; - final UciPath path; - - final TextStyle textStyle; - - final _LineInfo lineInfo; - - final _PgnTreeViewParams params; - - static const borderRadius = BorderRadius.all(Radius.circular(4.0)); - - bool get isCurrentMove => params.pathToCurrentMove == path; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final pieceNotation = ref.watch(pieceNotationProvider).maybeWhen( - data: (value) => value, - orElse: () => defaultAccountPreferences.pieceNotation, - ); - final moveFontFamily = - pieceNotation == PieceNotation.symbol ? 'ChessFont' : null; - - final moveTextStyle = textStyle.copyWith( - fontFamily: moveFontFamily, - fontWeight: isCurrentMove - ? FontWeight.bold - : lineInfo.type == _LineType.inlineSideline - ? FontWeight.normal - : FontWeight.w600, - ); - - final indexTextStyle = textStyle.copyWith( - color: _textColor(context, 0.6), - ); - - final indexText = branch.position.ply.isOdd - ? TextSpan( - text: '${(branch.position.ply / 2).ceil()}. ', - style: indexTextStyle, - ) - : (lineInfo.startLine - ? TextSpan( - text: '${(branch.position.ply / 2).ceil()}… ', - style: indexTextStyle, - ) - : null); - - final moveWithNag = branch.sanMove.san + - (branch.nags != null && params.shouldShowAnnotations - ? moveAnnotationChar(branch.nags!) - : ''); - - final nag = params.shouldShowAnnotations ? branch.nags?.firstOrNull : null; - - final ply = branch.position.ply; - return AdaptiveInkWell( - key: isCurrentMove ? params.currentMoveKey : null, - borderRadius: borderRadius, - onTap: () => params.notifier.userJump(path), - onLongPress: () { - showAdaptiveBottomSheet( - context: context, - isDismissible: true, - isScrollControlled: true, - showDragHandle: true, - builder: (context) => _MoveContextMenu( - notifier: params.notifier, - title: ply.isOdd - ? '${(ply / 2).ceil()}. $moveWithNag' - : '${(ply / 2).ceil()}... $moveWithNag', - path: path, - branch: branch, - isSideline: lineInfo.type != _LineType.mainline, - ), - ); - }, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 5.0, vertical: 2.0), - decoration: isCurrentMove - ? BoxDecoration( - color: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoColors.systemGrey3.resolveFrom(context) - : Theme.of(context).focusColor, - shape: BoxShape.rectangle, - borderRadius: borderRadius, - ) - : null, - child: Text.rich( - TextSpan( - children: [ - if (indexText != null) ...[ - indexText, - ], - TextSpan( - text: moveWithNag, - style: moveTextStyle.copyWith( - color: _textColor( - context, - isCurrentMove ? 1 : 0.9, - nag: nag, - ), - ), - ), - ], - ), - ), - ), - ); - } -} - -class _MoveContextMenu extends ConsumerWidget { - const _MoveContextMenu({ - required this.title, - required this.path, - required this.branch, - required this.isSideline, - required this.notifier, - }); - - final String title; - final UciPath path; - final ViewBranch branch; - final bool isSideline; - final PgnTreeNotifier notifier; - - @override - Widget build(BuildContext context, WidgetRef ref) { - return BottomSheetScrollableContainer( - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - title, - style: Theme.of(context).textTheme.titleLarge, - ), - if (branch.clock != null) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon( - Icons.punch_clock, - ), - const SizedBox(width: 4.0), - Text( - branch.clock!.toHoursMinutesSeconds( - showTenths: - branch.clock! < const Duration(minutes: 1), - ), - ), - ], - ), - if (branch.elapsedMoveTime != null) ...[ - const SizedBox(height: 4.0), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon( - Icons.hourglass_bottom, - ), - const SizedBox(width: 4.0), - Text( - branch.elapsedMoveTime! - .toHoursMinutesSeconds(showTenths: true), - ), - ], - ), - ], - ], - ), - ], - ), - ), - if (branch.hasTextComment) - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 8.0, - ), - child: Text( - branch.textComments.join(' '), - ), - ), - const PlatformDivider(indent: 0), - if (isSideline) ...[ - BottomSheetContextMenuAction( - icon: Icons.subtitles_off, - child: Text(context.l10n.collapseVariations), - onPressed: () => notifier.collapseVariations(path), - ), - BottomSheetContextMenuAction( - icon: Icons.expand_less, - child: Text(context.l10n.promoteVariation), - onPressed: () => notifier.promoteVariation(path, false), - ), - BottomSheetContextMenuAction( - icon: Icons.check, - child: Text(context.l10n.makeMainLine), - onPressed: () => notifier.promoteVariation(path, true), - ), - ], - BottomSheetContextMenuAction( - icon: Icons.delete, - child: Text(context.l10n.deleteFromHere), - onPressed: () => notifier.deleteFromHere(path), - ), - ], - ); - } -} - -List _comments( - Iterable comments, { - required TextStyle textStyle, -}) => - comments - .map( - (comment) => TextSpan( - text: comment, - style: textStyle.copyWith( - fontSize: textStyle.fontSize! - 2.0, - fontStyle: FontStyle.italic, - ), - ), - ) - .toList(growable: false); - class _OpeningHeaderDelegate extends SliverPersistentHeaderDelegate { const _OpeningHeaderDelegate( this.ctrlProvider, { diff --git a/lib/src/view/game/game_result_dialog.dart b/lib/src/view/game/game_result_dialog.dart index 029ff5af77..f1ea715ceb 100644 --- a/lib/src/view/game/game_result_dialog.dart +++ b/lib/src/view/game/game_result_dialog.dart @@ -22,7 +22,7 @@ import 'package:lichess_mobile/src/model/game/playable_game.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; -import 'package:lichess_mobile/src/view/analysis/annotations.dart'; +import 'package:lichess_mobile/src/view/pgn/annotations.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; diff --git a/lib/src/view/analysis/annotations.dart b/lib/src/view/pgn/annotations.dart similarity index 100% rename from lib/src/view/analysis/annotations.dart rename to lib/src/view/pgn/annotations.dart diff --git a/lib/src/view/pgn/pgn_tree_view.dart b/lib/src/view/pgn/pgn_tree_view.dart new file mode 100644 index 0000000000..bfe803d5e1 --- /dev/null +++ b/lib/src/view/pgn/pgn_tree_view.dart @@ -0,0 +1,1126 @@ +import 'package:collection/collection.dart'; +import 'package:dartchess/dartchess.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/account/account_preferences.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; +import 'package:lichess_mobile/src/model/common/node.dart'; +import 'package:lichess_mobile/src/model/common/uci.dart'; +import 'package:lichess_mobile/src/utils/duration.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/rate_limit.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/list.dart'; + +import 'annotations.dart'; + +// fast replay debounce delay, same as piece animation duration, to avoid piece +// animation jank at the end of the replay +const kFastReplayDebounceDelay = Duration(milliseconds: 150); + +/// Callbacks for interaction with [DebouncedPgnTreeView] +abstract class PgnTreeNotifier { + void expandVariations(UciPath path); + void collapseVariations(UciPath path); + void promoteVariation(UciPath path, bool toMainLine); + void deleteFromHere(UciPath path); + void userJump(UciPath path); +} + +/// Displays a tree-like view of a PGN game's moves. Path changes are debounced to avoid rebuilding the whole tree on every move. +/// +/// For example, the PGN 1. e4 e5 (1... d5) (1... Nc6) 2. Nf3 Nc6 (2... a5) 3. Bc4 * will be displayed as: +/// 1. e4 e5 // [_MainLinePart] +/// |- 1... d5 // [_SideLinePart] +/// |- 1... Nc6 // [_SideLinePart] +/// 2. Nf3 Nc6 (2... a5) 3. Bc4 // [_MainLinePart], with inline sideline +/// Short sidelines without any branching are displayed inline with their parent line. +/// Longer sidelines are displayed on a new line and indented. +/// The mainline is split into parts whenever a move has a non-inline sideline, this corresponds to the [_MainLinePart] widget. +/// Similarly, a [_SideLinePart] contains the moves sequence of a sideline where each node has only one child. +class DebouncedPgnTreeView extends ConsumerStatefulWidget { + const DebouncedPgnTreeView({ + required this.root, + required this.currentPath, + required this.pgnRootComments, + required this.notifier, + }); + + /// Root of the PGN tree to display + final ViewRoot root; + + /// Path to the currently selected move in the tree + final UciPath currentPath; + + /// Comments associated with the root node + final IList? pgnRootComments; + + /// Callbacks for when the user interacts with the tree view, e.g. selecting a different move or collapsing variations + final PgnTreeNotifier notifier; + + @override + ConsumerState createState() => + _DebouncedPgnTreeViewState(); +} + +class _DebouncedPgnTreeViewState extends ConsumerState { + final currentMoveKey = GlobalKey(); + final _debounce = Debouncer(kFastReplayDebounceDelay); + + /// Path to the currently selected move in the tree. When widget.currentPath changes rapidly, we debounce the change to avoid rebuilding the whole tree on every move. + late UciPath pathToCurrentMove; + + @override + void initState() { + super.initState(); + pathToCurrentMove = widget.currentPath; + WidgetsBinding.instance.addPostFrameCallback((_) { + if (currentMoveKey.currentContext != null) { + Scrollable.ensureVisible( + currentMoveKey.currentContext!, + alignment: 0.5, + alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd, + ); + } + }); + } + + @override + void dispose() { + _debounce.dispose(); + super.dispose(); + } + + @override + void didUpdateWidget(covariant DebouncedPgnTreeView oldWidget) { + super.didUpdateWidget(oldWidget); + + if (oldWidget.currentPath != widget.currentPath) { + // debouncing the current path change to avoid rebuilding when using + // the fast replay buttons + _debounce(() { + setState(() { + pathToCurrentMove = widget.currentPath; + }); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (currentMoveKey.currentContext != null) { + Scrollable.ensureVisible( + currentMoveKey.currentContext!, + duration: const Duration(milliseconds: 200), + curve: Curves.easeIn, + alignment: 0.5, + alignmentPolicy: ScrollPositionAlignmentPolicy.explicit, + ); + } + }); + }); + } + } + + // This is the most expensive part of the pgn tree view because of the tree + // that may be very large. + // Great care must be taken to avoid unnecessary rebuilds. + // This should actually rebuild only when the current path changes or a new node + // is added. + // Debouncing the current path change is necessary to avoid rebuilding when + // using the fast replay buttons. + @override + Widget build(BuildContext context) { + final shouldShowComments = ref.watch( + analysisPreferencesProvider.select((value) => value.showPgnComments), + ); + + final shouldShowAnnotations = ref.watch( + analysisPreferencesProvider.select((value) => value.showAnnotations), + ); + + return _PgnTreeView( + root: widget.root, + rootComments: widget.pgnRootComments, + params: ( + shouldShowAnnotations: shouldShowAnnotations, + shouldShowComments: shouldShowComments, + currentMoveKey: currentMoveKey, + pathToCurrentMove: pathToCurrentMove, + notifier: widget.notifier, + ), + ); + } +} + +/// A group of parameters that are passed through various parts of the tree view +/// and ultimately evaluated in the [InlineMove] widget. +/// +/// Grouped in this record to improve readability. +typedef _PgnTreeViewParams = ({ + /// Path to the currently selected move in the tree. + UciPath pathToCurrentMove, + + /// Whether to show NAG annotations like '!' and '??'. + bool shouldShowAnnotations, + + /// Whether to show comments associated with the moves. + bool shouldShowComments, + + /// Key that will we assigned to the widget corresponding to [pathToCurrentMove]. + /// Can be used e.g. to ensure that the current move is visible on the screen. + GlobalKey currentMoveKey, + + /// Callbacks for when the user interacts with the tree view, e.g. selecting a different move. + PgnTreeNotifier notifier, +}); + +/// Whether to display the sideline inline. +/// +/// Sidelines are usually rendered on a new line and indented. +/// However sidelines are rendered inline (in parantheses) if the side line has no branching and is less than 6 moves deep. +bool _displaySideLineAsInline(ViewBranch node, [int depth = 0]) { + if (depth == 6) return false; + if (node.children.isEmpty) return true; + if (node.children.length > 1) return false; + return _displaySideLineAsInline(node.children.first, depth + 1); +} + +/// Returns whether this node has a sideline that should not be displayed inline. +bool _hasNonInlineSideLine(ViewNode node) => + node.children.length > 2 || + (node.children.length == 2 && !_displaySideLineAsInline(node.children[1])); + +/// Splits the mainline into parts, where each part is a sequence of moves that are displayed on the same line. +/// +/// A part ends when a mainline node has a sideline that should not be displayed inline. +Iterable> _mainlineParts(ViewRoot root) => + [root, ...root.mainline] + .splitAfter(_hasNonInlineSideLine) + .takeWhile((nodes) => nodes.firstOrNull?.children.isNotEmpty == true); + +class _PgnTreeView extends StatefulWidget { + const _PgnTreeView({ + required this.root, + required this.rootComments, + required this.params, + }); + + /// Root of the PGN tree + final ViewRoot root; + + /// Comments associated with the root node + final IList? rootComments; + + final _PgnTreeViewParams params; + + @override + State<_PgnTreeView> createState() => _PgnTreeViewState(); +} + +/// A record that holds the rendered parts of a subtree. +typedef _CachedRenderedSubtree = ({ + /// The mainline part of the subtree. + _MainLinePart mainLinePart, + + /// The sidelines part of the subtree. + /// + /// This is nullable since the very last mainline part might not have any sidelines. + _IndentedSideLines? sidelines, + + /// Whether the subtree contains the current move. + bool containsCurrentMove, +}); + +class _PgnTreeViewState extends State<_PgnTreeView> { + /// Caches the result of [_mainlineParts], it only needs to be recalculated when the root changes, + /// but not when `params.pathToCurrentMove` changes. + List> mainlineParts = []; + + /// Cache of the top-level subtrees obtained from the last `build()` method. + /// + /// Building the whole tree is expensive, so we cache the subtrees that did not change when the current move changes. + /// The framework will skip the `build()` of each subtree since the widget reference is the same. + List<_CachedRenderedSubtree> subtrees = []; + + UciPath _mainlinePartOfCurrentPath() { + var path = UciPath.empty; + for (final node in widget.root.mainline) { + if (!widget.params.pathToCurrentMove.contains(path + node.id)) { + break; + } + path = path + node.id; + } + return path; + } + + List<_CachedRenderedSubtree> _buildChangedSubtrees({ + required bool fullRebuild, + }) { + var path = UciPath.empty; + return mainlineParts.mapIndexed( + (i, mainlineNodes) { + final mainlineInitialPath = path; + + final sidelineInitialPath = UciPath.join( + path, + UciPath.fromIds( + mainlineNodes + .take(mainlineNodes.length - 1) + .map((n) => n.children.first.id), + ), + ); + + path = sidelineInitialPath; + if (mainlineNodes.last.children.isNotEmpty) { + path = path + mainlineNodes.last.children.first.id; + } + + final mainlinePartOfCurrentPath = _mainlinePartOfCurrentPath(); + final containsCurrentMove = + mainlinePartOfCurrentPath.size > mainlineInitialPath.size && + mainlinePartOfCurrentPath.size <= path.size; + + if (fullRebuild || + subtrees[i].containsCurrentMove || + containsCurrentMove) { + // Skip the first node which is the continuation of the mainline + final sidelineNodes = mainlineNodes.last.children.skip(1); + return ( + mainLinePart: _MainLinePart( + params: widget.params, + initialPath: mainlineInitialPath, + nodes: mainlineNodes, + ), + sidelines: sidelineNodes.isNotEmpty + ? _IndentedSideLines( + sidelineNodes, + parent: mainlineNodes.last, + params: widget.params, + initialPath: sidelineInitialPath, + nesting: 1, + ) + : null, + containsCurrentMove: containsCurrentMove, + ); + } else { + // Avoid expensive rebuilds ([State.build]) of the entire PGN tree by caching parts of the tree that did not change across a path change + return subtrees[i]; + } + }, + ).toList(growable: false); + } + + void _updateLines({required bool fullRebuild}) { + setState(() { + if (fullRebuild) { + mainlineParts = _mainlineParts(widget.root).toList(growable: false); + } + + subtrees = _buildChangedSubtrees(fullRebuild: fullRebuild); + }); + } + + @override + void initState() { + super.initState(); + _updateLines(fullRebuild: true); + } + + @override + void didUpdateWidget(covariant _PgnTreeView oldWidget) { + super.didUpdateWidget(oldWidget); + _updateLines(fullRebuild: oldWidget.root != widget.root); + } + + @override + Widget build(BuildContext context) { + final rootComments = widget.rootComments?.map((c) => c.text).nonNulls ?? []; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // trick to make auto-scroll work when returning to the root position + if (widget.params.pathToCurrentMove.isEmpty) + SizedBox.shrink(key: widget.params.currentMoveKey), + + if (widget.params.shouldShowComments && rootComments.isNotEmpty) + Text.rich( + TextSpan( + children: _comments( + rootComments, + textStyle: _baseTextStyle, + ), + ), + ), + ...subtrees + .map( + (part) => [ + part.mainLinePart, + if (part.sidelines != null) part.sidelines!, + ], + ) + .flattened, + ], + ), + ); + } +} + +List _buildInlineSideLine({ + required ViewBranch firstNode, + required ViewNode parent, + required UciPath initialPath, + required TextStyle textStyle, + required bool followsComment, + required _PgnTreeViewParams params, +}) { + textStyle = textStyle.copyWith( + fontSize: textStyle.fontSize != null ? textStyle.fontSize! - 2.0 : null, + ); + + final sidelineNodes = [firstNode, ...firstNode.mainline]; + + var path = initialPath; + return [ + if (followsComment) const WidgetSpan(child: SizedBox(width: 4.0)), + ...sidelineNodes.mapIndexedAndLast( + (i, node, last) { + final pathToNode = path; + path = path + node.id; + + return [ + if (i == 0) ...[ + if (followsComment) const WidgetSpan(child: SizedBox(width: 4.0)), + TextSpan( + text: '(', + style: textStyle, + ), + ], + ..._moveWithComment( + node, + lineInfo: ( + type: _LineType.inlineSideline, + startLine: i == 0 || sidelineNodes[i - 1].hasTextComment + ), + pathToNode: pathToNode, + textStyle: textStyle, + params: params, + ), + if (last) + TextSpan( + text: ')', + style: textStyle, + ), + ]; + }, + ).flattened, + const WidgetSpan(child: SizedBox(width: 4.0)), + ]; +} + +const _baseTextStyle = TextStyle( + fontSize: 16.0, + height: 1.5, +); + +/// The different types of lines (move sequences) that are displayed in the tree view. +enum _LineType { + /// (A part of) the game's main line. + mainline, + + /// A sideline branching off the main line or a parent sideline. + /// + /// Each sideline is rendered on a new line and indented. + sideline, + + /// A short sideline without any branching, displayed in parantheses inline with it's parent line. + inlineSideline, +} + +/// Metadata about a move's role in the tree view. +typedef _LineInfo = ({_LineType type, bool startLine}); + +List _moveWithComment( + ViewBranch branch, { + required TextStyle textStyle, + required _LineInfo lineInfo, + required UciPath pathToNode, + required _PgnTreeViewParams params, + + /// Optional [GlobalKey] that will be assigned to the [InlineMove] widget. + /// + /// It should only be set if it is the first move of a sideline. + /// We use this to track the position of the first move widget. See [_SideLinePart.firstMoveKey]. + GlobalKey? firstMoveKey, +}) { + return [ + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: InlineMove( + key: firstMoveKey, + branch: branch, + lineInfo: lineInfo, + path: pathToNode + branch.id, + textStyle: textStyle, + params: params, + ), + ), + if (params.shouldShowComments && branch.hasTextComment) + ..._comments(branch.textComments, textStyle: textStyle), + ]; +} + +/// A widget that renders part of a sideline, where each move is displayed on the same line without branching. +/// +/// Each node in the sideline has only one child (or two children where the second child is rendered as an inline sideline). +class _SideLinePart extends ConsumerWidget { + _SideLinePart( + this.nodes, { + required this.initialPath, + required this.firstMoveKey, + required this.params, + }) : assert(nodes.isNotEmpty); + + final List nodes; + + final UciPath initialPath; + + /// The key that will be assigned to the first move in this sideline. + /// + /// This is needed so that the indent guidelines can be drawn correctly. + final GlobalKey firstMoveKey; + + final _PgnTreeViewParams params; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final textStyle = _baseTextStyle.copyWith( + color: _textColor(context, 0.6), + fontSize: _baseTextStyle.fontSize! - 1.0, + ); + + var path = initialPath + nodes.first.id; + final moves = [ + ..._moveWithComment( + nodes.first, + lineInfo: ( + type: _LineType.sideline, + startLine: true, + ), + firstMoveKey: firstMoveKey, + pathToNode: initialPath, + textStyle: textStyle, + params: params, + ), + ...nodes.take(nodes.length - 1).map( + (node) { + final moves = [ + ..._moveWithComment( + node.children.first, + lineInfo: ( + type: _LineType.sideline, + startLine: node.hasTextComment, + ), + pathToNode: path, + textStyle: textStyle, + params: params, + ), + if (node.children.length == 2 && + _displaySideLineAsInline(node.children[1])) + ..._buildInlineSideLine( + followsComment: node.children.first.hasTextComment, + firstNode: node.children[1], + parent: node, + initialPath: path, + textStyle: textStyle, + params: params, + ), + ]; + path = path + node.children.first.id; + return moves; + }, + ).flattened, + ]; + + return Text.rich( + TextSpan( + children: moves, + ), + ); + } +} + +/// A widget that renders part of the mainline. +/// +/// A part of the mainline is rendered on a single line. See [_mainlineParts]. +class _MainLinePart extends ConsumerWidget { + const _MainLinePart({ + required this.initialPath, + required this.params, + required this.nodes, + }); + + final UciPath initialPath; + + final List nodes; + + final _PgnTreeViewParams params; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final textStyle = _baseTextStyle.copyWith( + color: _textColor(context, 0.9), + ); + + var path = initialPath; + return Text.rich( + TextSpan( + children: nodes + .takeWhile((node) => node.children.isNotEmpty) + .mapIndexed( + (i, node) { + final mainlineNode = node.children.first; + final moves = [ + _moveWithComment( + mainlineNode, + lineInfo: ( + type: _LineType.mainline, + startLine: i == 0 || (node as ViewBranch).hasTextComment, + ), + pathToNode: path, + textStyle: textStyle, + params: params, + ), + if (node.children.length == 2 && + _displaySideLineAsInline(node.children[1])) ...[ + _buildInlineSideLine( + followsComment: mainlineNode.hasTextComment, + firstNode: node.children[1], + parent: node, + initialPath: path, + textStyle: textStyle, + params: params, + ), + ], + ]; + path = path + mainlineNode.id; + return moves.flattened; + }, + ) + .flattened + .toList(growable: false), + ), + ); + } +} + +/// A widget that renders a sideline. +/// +/// The moves are rendered on the same line (see [_SideLinePart]) until further +/// branching is encountered, at which point the children sidelines are rendered +/// on new lines and indented (see [_IndentedSideLines]). +class _SideLine extends StatelessWidget { + const _SideLine({ + required this.firstNode, + required this.parent, + required this.firstMoveKey, + required this.initialPath, + required this.params, + required this.nesting, + }); + + final ViewBranch firstNode; + final ViewNode parent; + final GlobalKey firstMoveKey; + final UciPath initialPath; + final _PgnTreeViewParams params; + final int nesting; + + List _getSidelinePartNodes() { + final sidelineNodes = [firstNode]; + while (sidelineNodes.last.children.isNotEmpty && + !_hasNonInlineSideLine(sidelineNodes.last)) { + sidelineNodes.add(sidelineNodes.last.children.first); + } + return sidelineNodes.toList(growable: false); + } + + @override + Widget build(BuildContext context) { + final sidelinePartNodes = _getSidelinePartNodes(); + + final lastNodeChildren = sidelinePartNodes.last.children; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _SideLinePart( + sidelinePartNodes, + firstMoveKey: firstMoveKey, + initialPath: initialPath, + params: params, + ), + if (lastNodeChildren.isNotEmpty) + _IndentedSideLines( + lastNodeChildren, + parent: sidelinePartNodes.last, + initialPath: UciPath.join( + initialPath, + UciPath.fromIds(sidelinePartNodes.map((node) => node.id)), + ), + params: params, + nesting: nesting + 1, + ), + ], + ); + } +} + +class _IndentPainter extends CustomPainter { + const _IndentPainter({ + required this.sideLineStartPositions, + required this.color, + required this.padding, + }); + + final List sideLineStartPositions; + + final Color color; + + final double padding; + + @override + void paint(Canvas canvas, Size size) { + if (sideLineStartPositions.isNotEmpty) { + final paint = Paint() + ..strokeWidth = 1.5 + ..color = color + ..strokeCap = StrokeCap.round + ..style = PaintingStyle.stroke; + + final origin = Offset(-padding, 0); + + final path = Path()..moveTo(origin.dx, origin.dy); + path.lineTo(origin.dx, sideLineStartPositions.last.dy); + for (final position in sideLineStartPositions) { + path.moveTo(origin.dx, position.dy); + path.lineTo(origin.dx + padding / 2, position.dy); + } + canvas.drawPath(path, paint); + } + } + + @override + bool shouldRepaint(_IndentPainter oldDelegate) { + return oldDelegate.sideLineStartPositions != sideLineStartPositions || + oldDelegate.color != color; + } +} + +/// A widget that displays indented sidelines. +/// +/// Will show one ore more sidelines indented on their own line and add indent +/// guides. +/// If there are hidden lines, a "+" button is displayed to expand them. +class _IndentedSideLines extends StatefulWidget { + const _IndentedSideLines( + this.sideLines, { + required this.parent, + required this.initialPath, + required this.params, + required this.nesting, + }); + + final Iterable sideLines; + + final ViewNode parent; + + final UciPath initialPath; + + final _PgnTreeViewParams params; + + final int nesting; + + @override + State<_IndentedSideLines> createState() => _IndentedSideLinesState(); +} + +class _IndentedSideLinesState extends State<_IndentedSideLines> { + /// Keys for the first move of each sideline. + /// + /// Used to calculate the position of the indent guidelines. The keys are + /// assigned to the first move of each sideline. The position of the keys is + /// used to calculate the position of the indent guidelines. A [GlobalKey] is + /// necessary because the exact position of the first move is not known until the + /// widget is rendered, as the vertical space can vary depending on the length of + /// the line, and if the line is wrapped. + late List _sideLinesStartKeys; + + /// The position of the first move of each sideline computed relative to the column and derived from the [GlobalKey] found in [_sideLinesStartKeys]. + List _sideLineStartPositions = []; + + /// The [GlobalKey] for the column that contains the side lines. + final GlobalKey _columnKey = GlobalKey(); + + /// Redraws the indents on demand. + /// + /// Will re-generate the [GlobalKey]s for the first move of each sideline and + /// calculate the position of the indents in a post-frame callback. + void _redrawIndents() { + _sideLinesStartKeys = List.generate( + _visibleSideLines.length + (_hasHiddenLines ? 1 : 0), + (_) => GlobalKey(), + ); + WidgetsBinding.instance.addPostFrameCallback((_) { + final RenderBox? columnBox = + _columnKey.currentContext?.findRenderObject() as RenderBox?; + final Offset rowOffset = + columnBox?.localToGlobal(Offset.zero) ?? Offset.zero; + + final positions = _sideLinesStartKeys.map((key) { + final context = key.currentContext; + final renderBox = context?.findRenderObject() as RenderBox?; + final height = renderBox?.size.height ?? 0; + final offset = renderBox?.localToGlobal(Offset.zero) ?? Offset.zero; + return Offset(offset.dx, offset.dy + height / 2) - rowOffset; + }).toList(growable: false); + + setState(() { + _sideLineStartPositions = positions; + }); + }); + } + + bool get _hasHiddenLines => widget.sideLines.any((node) => node.isHidden); + + Iterable get _visibleSideLines => + widget.sideLines.whereNot((node) => node.isHidden); + + @override + void initState() { + super.initState(); + _redrawIndents(); + } + + @override + void didUpdateWidget(covariant _IndentedSideLines oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.sideLines != widget.sideLines) { + _redrawIndents(); + } + } + + @override + Widget build(BuildContext context) { + final sideLineWidgets = _visibleSideLines + .mapIndexed( + (i, firstSidelineNode) => _SideLine( + firstNode: firstSidelineNode, + parent: widget.parent, + firstMoveKey: _sideLinesStartKeys[i], + initialPath: widget.initialPath, + params: widget.params, + nesting: widget.nesting, + ), + ) + .toList(growable: false); + + final padding = widget.nesting < 6 ? 12.0 : 0.0; + + return Padding( + padding: EdgeInsets.only(left: padding), + child: CustomPaint( + painter: _IndentPainter( + sideLineStartPositions: _sideLineStartPositions, + color: _textColor(context, 0.6)!, + padding: padding, + ), + child: Column( + key: _columnKey, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...sideLineWidgets, + if (_hasHiddenLines) + GestureDetector( + child: Icon( + Icons.add_box, + color: _textColor(context, 0.6), + key: _sideLinesStartKeys.last, + size: _baseTextStyle.fontSize! + 5, + ), + onTap: () { + widget.params.notifier.expandVariations(widget.initialPath); + }, + ), + ], + ), + ), + ); + } +} + +Color? _textColor( + BuildContext context, + double opacity, { + int? nag, +}) { + final defaultColor = Theme.of(context).platform == TargetPlatform.android + ? Theme.of(context).textTheme.bodyLarge?.color?.withValues(alpha: opacity) + : CupertinoTheme.of(context) + .textTheme + .textStyle + .color + ?.withValues(alpha: opacity); + + return nag != null && nag > 0 ? nagColor(nag) : defaultColor; +} + +/// A widget that displays a single move in the tree view. +/// +/// The move can optionnally be preceded by an index, and followed by a nag annotation. +/// The move is displayed as a clickable button that will jump to the move when pressed. +/// The move is highlighted if it is the current move. +/// A long press on the move will display a context menu with options to promote the move to the main line, collapse variations, etc. +class InlineMove extends ConsumerWidget { + const InlineMove({ + required this.branch, + required this.path, + required this.textStyle, + required this.lineInfo, + required this.params, + super.key, + }); + + final ViewBranch branch; + final UciPath path; + + final TextStyle textStyle; + + final _LineInfo lineInfo; + + final _PgnTreeViewParams params; + + static const borderRadius = BorderRadius.all(Radius.circular(4.0)); + + bool get isCurrentMove => params.pathToCurrentMove == path; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final pieceNotation = ref.watch(pieceNotationProvider).maybeWhen( + data: (value) => value, + orElse: () => defaultAccountPreferences.pieceNotation, + ); + final moveFontFamily = + pieceNotation == PieceNotation.symbol ? 'ChessFont' : null; + + final moveTextStyle = textStyle.copyWith( + fontFamily: moveFontFamily, + fontWeight: isCurrentMove + ? FontWeight.bold + : lineInfo.type == _LineType.inlineSideline + ? FontWeight.normal + : FontWeight.w600, + ); + + final indexTextStyle = textStyle.copyWith( + color: _textColor(context, 0.6), + ); + + final indexText = branch.position.ply.isOdd + ? TextSpan( + text: '${(branch.position.ply / 2).ceil()}. ', + style: indexTextStyle, + ) + : (lineInfo.startLine + ? TextSpan( + text: '${(branch.position.ply / 2).ceil()}… ', + style: indexTextStyle, + ) + : null); + + final moveWithNag = branch.sanMove.san + + (branch.nags != null && params.shouldShowAnnotations + ? moveAnnotationChar(branch.nags!) + : ''); + + final nag = params.shouldShowAnnotations ? branch.nags?.firstOrNull : null; + + final ply = branch.position.ply; + return AdaptiveInkWell( + key: isCurrentMove ? params.currentMoveKey : null, + borderRadius: borderRadius, + onTap: () => params.notifier.userJump(path), + onLongPress: () { + showAdaptiveBottomSheet( + context: context, + isDismissible: true, + isScrollControlled: true, + showDragHandle: true, + builder: (context) => _MoveContextMenu( + notifier: params.notifier, + title: ply.isOdd + ? '${(ply / 2).ceil()}. $moveWithNag' + : '${(ply / 2).ceil()}... $moveWithNag', + path: path, + branch: branch, + isSideline: lineInfo.type != _LineType.mainline, + ), + ); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 5.0, vertical: 2.0), + decoration: isCurrentMove + ? BoxDecoration( + color: Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoColors.systemGrey3.resolveFrom(context) + : Theme.of(context).focusColor, + shape: BoxShape.rectangle, + borderRadius: borderRadius, + ) + : null, + child: Text.rich( + TextSpan( + children: [ + if (indexText != null) ...[ + indexText, + ], + TextSpan( + text: moveWithNag, + style: moveTextStyle.copyWith( + color: _textColor( + context, + isCurrentMove ? 1 : 0.9, + nag: nag, + ), + ), + ), + ], + ), + ), + ), + ); + } +} + +class _MoveContextMenu extends ConsumerWidget { + const _MoveContextMenu({ + required this.title, + required this.path, + required this.branch, + required this.isSideline, + required this.notifier, + }); + + final String title; + final UciPath path; + final ViewBranch branch; + final bool isSideline; + final PgnTreeNotifier notifier; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return BottomSheetScrollableContainer( + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + title, + style: Theme.of(context).textTheme.titleLarge, + ), + if (branch.clock != null) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.punch_clock, + ), + const SizedBox(width: 4.0), + Text( + branch.clock!.toHoursMinutesSeconds( + showTenths: + branch.clock! < const Duration(minutes: 1), + ), + ), + ], + ), + if (branch.elapsedMoveTime != null) ...[ + const SizedBox(height: 4.0), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.hourglass_bottom, + ), + const SizedBox(width: 4.0), + Text( + branch.elapsedMoveTime! + .toHoursMinutesSeconds(showTenths: true), + ), + ], + ), + ], + ], + ), + ], + ), + ), + if (branch.hasTextComment) + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 8.0, + ), + child: Text( + branch.textComments.join(' '), + ), + ), + const PlatformDivider(indent: 0), + if (isSideline) ...[ + BottomSheetContextMenuAction( + icon: Icons.subtitles_off, + child: Text(context.l10n.collapseVariations), + onPressed: () => notifier.collapseVariations(path), + ), + BottomSheetContextMenuAction( + icon: Icons.expand_less, + child: Text(context.l10n.promoteVariation), + onPressed: () => notifier.promoteVariation(path, false), + ), + BottomSheetContextMenuAction( + icon: Icons.check, + child: Text(context.l10n.makeMainLine), + onPressed: () => notifier.promoteVariation(path, true), + ), + ], + BottomSheetContextMenuAction( + icon: Icons.delete, + child: Text(context.l10n.deleteFromHere), + onPressed: () => notifier.deleteFromHere(path), + ), + ], + ); + } +} + +List _comments( + Iterable comments, { + required TextStyle textStyle, +}) => + comments + .map( + (comment) => TextSpan( + text: comment, + style: textStyle.copyWith( + fontSize: textStyle.fontSize! - 2.0, + fontStyle: FontStyle.italic, + ), + ), + ) + .toList(growable: false); diff --git a/test/view/analysis/analysis_screen_test.dart b/test/view/analysis/analysis_screen_test.dart index f510ef1516..6426fc4079 100644 --- a/test/view/analysis/analysis_screen_test.dart +++ b/test/view/analysis/analysis_screen_test.dart @@ -16,7 +16,7 @@ import 'package:lichess_mobile/src/model/game/player.dart'; import 'package:lichess_mobile/src/model/settings/preferences.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; -import 'package:lichess_mobile/src/view/analysis/tree_view.dart'; +import 'package:lichess_mobile/src/view/pgn/pgn_tree_view.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import '../../test_provider_scope.dart'; From 83521cd48340dd2a18e54dff56ed711ab45408de Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Wed, 16 Oct 2024 23:15:51 +0200 Subject: [PATCH 493/979] fix: invalidate tree cache if shouldShowComments or shouldShowAnnotations changes --- lib/src/view/pgn/pgn_tree_view.dart | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/src/view/pgn/pgn_tree_view.dart b/lib/src/view/pgn/pgn_tree_view.dart index bfe803d5e1..cb69d9b98a 100644 --- a/lib/src/view/pgn/pgn_tree_view.dart +++ b/lib/src/view/pgn/pgn_tree_view.dart @@ -328,7 +328,13 @@ class _PgnTreeViewState extends State<_PgnTreeView> { @override void didUpdateWidget(covariant _PgnTreeView oldWidget) { super.didUpdateWidget(oldWidget); - _updateLines(fullRebuild: oldWidget.root != widget.root); + _updateLines( + fullRebuild: oldWidget.root != widget.root || + oldWidget.params.shouldShowComments != + widget.params.shouldShowComments || + oldWidget.params.shouldShowAnnotations != + widget.params.shouldShowAnnotations, + ); } @override From 3792ddeb1df97e2a00e2d56fbfc3a3f0f83d9ed5 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 17 Oct 2024 10:25:09 +0200 Subject: [PATCH 494/979] WIP on fixing build_runner perf issues --- build.yaml | 3 ++ lib/src/app.dart | 6 +++- lib/src/model/puzzle/puzzle_controller.dart | 2 +- lib/src/model/settings/brightness.dart | 11 ++++--- .../model/settings/general_preferences.dart | 31 +++++++++++++------ lib/src/view/game/game_screen.dart | 1 + .../settings/app_background_mode_screen.dart | 14 ++++----- .../view/settings/settings_tab_screen.dart | 6 ++-- lib/src/view/user/perf_cards.dart | 1 + 9 files changed, 49 insertions(+), 26 deletions(-) diff --git a/build.yaml b/build.yaml index 5a699c68cb..6621469741 100644 --- a/build.yaml +++ b/build.yaml @@ -1,6 +1,9 @@ targets: $default: builders: + json_serializable: + generate_for: + - lib/src/model/**/*.dart freezed: generate_for: - lib/src/model/**/*.dart diff --git a/lib/src/app.dart b/lib/src/app.dart index d32b2bf2b0..b000b4e806 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -171,7 +171,11 @@ class _AppState extends ConsumerState { lichessCustomColors.harmonized(colorScheme), ], ), - themeMode: generalPrefs.themeMode, + themeMode: switch (generalPrefs.themeMode) { + BackgroundThemeMode.light => ThemeMode.light, + BackgroundThemeMode.dark => ThemeMode.dark, + BackgroundThemeMode.system => ThemeMode.system, + }, builder: Theme.of(context).platform == TargetPlatform.iOS ? (context, child) { return CupertinoTheme( diff --git a/lib/src/model/puzzle/puzzle_controller.dart b/lib/src/model/puzzle/puzzle_controller.dart index 8d9c1d15e2..71c92b7343 100644 --- a/lib/src/model/puzzle/puzzle_controller.dart +++ b/lib/src/model/puzzle/puzzle_controller.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:async/async.dart'; -import 'package:collection/collection.dart'; +import 'package:collection/collection.dart' show IterableExtension; import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; diff --git a/lib/src/model/settings/brightness.dart b/lib/src/model/settings/brightness.dart index d3638e1123..f232bbc6b8 100644 --- a/lib/src/model/settings/brightness.dart +++ b/lib/src/model/settings/brightness.dart @@ -1,4 +1,5 @@ -import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart' show Brightness; +import 'package:flutter/widgets.dart' show WidgetsBinding; import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -17,17 +18,17 @@ class CurrentBrightness extends _$CurrentBrightness { WidgetsBinding.instance.platformDispatcher.onPlatformBrightnessChanged = () { WidgetsBinding.instance.handlePlatformBrightnessChanged(); - if (themeMode == ThemeMode.system) { + if (themeMode == BackgroundThemeMode.system) { state = WidgetsBinding.instance.platformDispatcher.platformBrightness; } }; switch (themeMode) { - case ThemeMode.dark: + case BackgroundThemeMode.dark: return Brightness.dark; - case ThemeMode.light: + case BackgroundThemeMode.light: return Brightness.light; - case ThemeMode.system: + case BackgroundThemeMode.system: return WidgetsBinding.instance.platformDispatcher.platformBrightness; } } diff --git a/lib/src/model/settings/general_preferences.dart b/lib/src/model/settings/general_preferences.dart index e63197513c..7e77435b81 100644 --- a/lib/src/model/settings/general_preferences.dart +++ b/lib/src/model/settings/general_preferences.dart @@ -1,5 +1,5 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; +import 'dart:ui' show Locale; + import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/model/settings/preferences.dart'; @@ -21,7 +21,7 @@ class GeneralPreferences extends _$GeneralPreferences return fetch(); } - Future setThemeMode(ThemeMode themeMode) { + Future setThemeMode(BackgroundThemeMode themeMode) { return save(state.copyWith(themeMode: themeMode)); } @@ -42,9 +42,6 @@ class GeneralPreferences extends _$GeneralPreferences } Future toggleSystemColors() async { - if (defaultTargetPlatform != TargetPlatform.android) { - return; - } await save(state.copyWith(systemColors: !state.systemColors)); if (state.systemColors == false) { final boardTheme = ref.read(boardPreferencesProvider).boardTheme; @@ -85,8 +82,11 @@ Locale? _localeFromJson(Map? json) { @Freezed(fromJson: true, toJson: true) class GeneralPrefs with _$GeneralPrefs implements SerializablePreferences { const factory GeneralPrefs({ - @JsonKey(unknownEnumValue: ThemeMode.system, defaultValue: ThemeMode.system) - required ThemeMode themeMode, + @JsonKey( + unknownEnumValue: BackgroundThemeMode.system, + defaultValue: BackgroundThemeMode.system, + ) + required BackgroundThemeMode themeMode, required bool isSoundEnabled, @JsonKey(unknownEnumValue: SoundTheme.standard) required SoundTheme soundTheme, @@ -100,7 +100,7 @@ class GeneralPrefs with _$GeneralPrefs implements SerializablePreferences { }) = _GeneralPrefs; static const defaults = GeneralPrefs( - themeMode: ThemeMode.system, + themeMode: BackgroundThemeMode.system, isSoundEnabled: true, soundTheme: SoundTheme.standard, masterVolume: 0.8, @@ -112,6 +112,19 @@ class GeneralPrefs with _$GeneralPrefs implements SerializablePreferences { } } +/// Describes the background theme of the app. +enum BackgroundThemeMode { + /// Use either the light or dark theme based on what the user has selected in + /// the system settings. + system, + + /// Always use the light mode regardless of system preference. + light, + + /// Always use the dark mode (if available) regardless of system preference. + dark, +} + enum SoundTheme { standard('Standard'), piano('Piano'), diff --git a/lib/src/view/game/game_screen.dart b/lib/src/view/game/game_screen.dart index 584611617b..7e394c3587 100644 --- a/lib/src/view/game/game_screen.dart +++ b/lib/src/view/game/game_screen.dart @@ -43,6 +43,7 @@ class GameScreen extends ConsumerStatefulWidget { 'Either a seek, a challenge or an initial game id must be provided.', ); + // tweak final GameSeek? seek; final GameFullId? initialGameId; diff --git a/lib/src/view/settings/app_background_mode_screen.dart b/lib/src/view/settings/app_background_mode_screen.dart index dc551a2be7..e24084c562 100644 --- a/lib/src/view/settings/app_background_mode_screen.dart +++ b/lib/src/view/settings/app_background_mode_screen.dart @@ -31,13 +31,13 @@ class AppBackgroundModeScreen extends StatelessWidget { ); } - static String themeTitle(BuildContext context, ThemeMode theme) { + static String themeTitle(BuildContext context, BackgroundThemeMode theme) { switch (theme) { - case ThemeMode.system: + case BackgroundThemeMode.system: return context.l10n.deviceTheme; - case ThemeMode.dark: + case BackgroundThemeMode.dark: return context.l10n.dark; - case ThemeMode.light: + case BackgroundThemeMode.light: return context.l10n.light; } } @@ -50,15 +50,15 @@ class _Body extends ConsumerWidget { generalPreferencesProvider.select((state) => state.themeMode), ); - void onChanged(ThemeMode? value) => ref + void onChanged(BackgroundThemeMode? value) => ref .read(generalPreferencesProvider.notifier) - .setThemeMode(value ?? ThemeMode.system); + .setThemeMode(value ?? BackgroundThemeMode.system); return SafeArea( child: ListView( children: [ ChoicePicker( - choices: ThemeMode.values, + choices: BackgroundThemeMode.values, selectedItem: themeMode, titleBuilder: (t) => Text(AppBackgroundModeScreen.themeTitle(context, t)), diff --git a/lib/src/view/settings/settings_tab_screen.dart b/lib/src/view/settings/settings_tab_screen.dart index ad2d25fc90..134757963c 100644 --- a/lib/src/view/settings/settings_tab_screen.dart +++ b/lib/src/view/settings/settings_tab_screen.dart @@ -234,13 +234,13 @@ class _Body extends ConsumerWidget { if (Theme.of(context).platform == TargetPlatform.android) { showChoicePicker( context, - choices: ThemeMode.values, + choices: BackgroundThemeMode.values, selectedItem: generalPrefs.themeMode, labelBuilder: (t) => Text(AppBackgroundModeScreen.themeTitle(context, t)), - onSelectedItemChanged: (ThemeMode? value) => ref + onSelectedItemChanged: (BackgroundThemeMode? value) => ref .read(generalPreferencesProvider.notifier) - .setThemeMode(value ?? ThemeMode.system), + .setThemeMode(value ?? BackgroundThemeMode.system), ); } else { pushPlatformRoute( diff --git a/lib/src/view/user/perf_cards.dart b/lib/src/view/user/perf_cards.dart index 51d3bffff7..3440bf796e 100644 --- a/lib/src/view/user/perf_cards.dart +++ b/lib/src/view/user/perf_cards.dart @@ -13,6 +13,7 @@ import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/rating.dart'; +/// A widget that displays the performance cards of a user. class PerfCards extends StatelessWidget { const PerfCards({ required this.user, From 85cb792f42b3e7c0bbe897501407e6969c441856 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 17 Oct 2024 10:57:44 +0200 Subject: [PATCH 495/979] Refactor preferences to avoid build_runner perf issues --- lib/src/init.dart | 2 +- lib/src/intl.dart | 2 +- .../model/analysis/analysis_preferences.dart | 11 ++- .../challenge/challenge_preferences.dart | 12 ++- .../model/common/service/sound_service.dart | 2 +- .../coordinate_training_preferences.dart | 12 ++- lib/src/model/game/game_preferences.dart | 10 ++- .../model/lobby/game_setup_preferences.dart | 10 ++- .../opening_explorer_preferences.dart | 13 ++- lib/src/model/puzzle/puzzle_preferences.dart | 10 ++- lib/src/model/settings/board_preferences.dart | 12 ++- .../model/settings/general_preferences.dart | 11 ++- lib/src/model/settings/home_preferences.dart | 12 ++- .../settings/over_the_board_preferences.dart | 15 ++-- lib/src/model/settings/preferences.dart | 88 ------------------- .../model/settings/preferences_storage.dart | 67 +++++++++----- test/view/analysis/analysis_screen_test.dart | 2 +- .../opening_explorer_screen_test.dart | 1 - 18 files changed, 145 insertions(+), 147 deletions(-) delete mode 100644 lib/src/model/settings/preferences.dart diff --git a/lib/src/init.dart b/lib/src/init.dart index 628cc86cfd..8e146691a0 100644 --- a/lib/src/init.dart +++ b/lib/src/init.dart @@ -7,7 +7,7 @@ import 'package:flutter_displaymode/flutter_displaymode.dart'; import 'package:lichess_mobile/src/binding.dart'; import 'package:lichess_mobile/src/db/secure_storage.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; -import 'package:lichess_mobile/src/model/settings/preferences.dart'; +import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; import 'package:lichess_mobile/src/network/socket.dart'; import 'package:lichess_mobile/src/utils/chessboard.dart'; import 'package:lichess_mobile/src/utils/color_palette.dart'; diff --git a/lib/src/intl.dart b/lib/src/intl.dart index ea5d78a04a..bda5051faf 100644 --- a/lib/src/intl.dart +++ b/lib/src/intl.dart @@ -4,7 +4,7 @@ import 'package:flutter/widgets.dart'; import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/binding.dart'; import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; -import 'package:lichess_mobile/src/model/settings/preferences.dart'; +import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; import 'package:timeago/timeago.dart' as timeago; /// Setup [Intl.defaultLocale] and timeago locale and messages. diff --git a/lib/src/model/analysis/analysis_preferences.dart b/lib/src/model/analysis/analysis_preferences.dart index c6d5692c1c..4b8dddd84d 100644 --- a/lib/src/model/analysis/analysis_preferences.dart +++ b/lib/src/model/analysis/analysis_preferences.dart @@ -1,6 +1,5 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; -import 'package:lichess_mobile/src/model/settings/preferences.dart'; import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -14,6 +13,14 @@ class AnalysisPreferences extends _$AnalysisPreferences @override final prefCategory = PrefCategory.analysis; + // ignore: avoid_public_notifier_properties + @override + AnalysisPrefs get defaults => AnalysisPrefs.defaults; + + @override + AnalysisPrefs fromJson(Map json) => + AnalysisPrefs.fromJson(json); + @override AnalysisPrefs build() { return fetch(); @@ -79,7 +86,7 @@ class AnalysisPreferences extends _$AnalysisPreferences } @Freezed(fromJson: true, toJson: true) -class AnalysisPrefs with _$AnalysisPrefs implements SerializablePreferences { +class AnalysisPrefs with _$AnalysisPrefs implements Serializable { const AnalysisPrefs._(); const factory AnalysisPrefs({ diff --git a/lib/src/model/challenge/challenge_preferences.dart b/lib/src/model/challenge/challenge_preferences.dart index ba1e1d7aef..76453530e9 100644 --- a/lib/src/model/challenge/challenge_preferences.dart +++ b/lib/src/model/challenge/challenge_preferences.dart @@ -4,7 +4,6 @@ import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/game.dart'; import 'package:lichess_mobile/src/model/common/speed.dart'; import 'package:lichess_mobile/src/model/common/time_increment.dart'; -import 'package:lichess_mobile/src/model/settings/preferences.dart'; import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -17,7 +16,14 @@ class ChallengePreferences extends _$ChallengePreferences with SessionPreferencesStorage { // ignore: avoid_public_notifier_properties @override - PrefCategory get prefCategory => PrefCategory.challenge; + PrefCategory get prefCategory => PrefCategory.challenge; + + @override + ChallengePrefs defaults({LightUser? user}) => ChallengePrefs.defaults; + + @override + ChallengePrefs fromJson(Map json) => + ChallengePrefs.fromJson(json); @override ChallengePrefs build() { @@ -50,7 +56,7 @@ class ChallengePreferences extends _$ChallengePreferences } @Freezed(fromJson: true, toJson: true) -class ChallengePrefs with _$ChallengePrefs implements SerializablePreferences { +class ChallengePrefs with _$ChallengePrefs implements Serializable { const ChallengePrefs._(); const factory ChallengePrefs({ diff --git a/lib/src/model/common/service/sound_service.dart b/lib/src/model/common/service/sound_service.dart index c74497b816..0a015632bb 100644 --- a/lib/src/model/common/service/sound_service.dart +++ b/lib/src/model/common/service/sound_service.dart @@ -4,7 +4,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart' show rootBundle; import 'package:lichess_mobile/src/binding.dart'; import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; -import 'package:lichess_mobile/src/model/settings/preferences.dart'; +import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:sound_effect/sound_effect.dart'; diff --git a/lib/src/model/coordinate_training/coordinate_training_preferences.dart b/lib/src/model/coordinate_training/coordinate_training_preferences.dart index 6d81a84d0f..435d97d672 100644 --- a/lib/src/model/coordinate_training/coordinate_training_preferences.dart +++ b/lib/src/model/coordinate_training/coordinate_training_preferences.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/l10n/l10n.dart'; import 'package:lichess_mobile/src/model/common/game.dart'; -import 'package:lichess_mobile/src/model/settings/preferences.dart'; import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -16,6 +15,15 @@ class CoordinateTrainingPreferences extends _$CoordinateTrainingPreferences @override final prefCategory = PrefCategory.coordinateTraining; + // ignore: avoid_public_notifier_properties + @override + CoordinateTrainingPrefs get defaults => CoordinateTrainingPrefs.defaults; + + @override + CoordinateTrainingPrefs fromJson(Map json) { + return CoordinateTrainingPrefs.fromJson(json); + } + @override CoordinateTrainingPrefs build() { return fetch(); @@ -79,7 +87,7 @@ enum TrainingMode { @Freezed(fromJson: true, toJson: true) class CoordinateTrainingPrefs with _$CoordinateTrainingPrefs - implements SerializablePreferences { + implements Serializable { const CoordinateTrainingPrefs._(); const factory CoordinateTrainingPrefs({ diff --git a/lib/src/model/game/game_preferences.dart b/lib/src/model/game/game_preferences.dart index d433c51be3..d95a1e4795 100644 --- a/lib/src/model/game/game_preferences.dart +++ b/lib/src/model/game/game_preferences.dart @@ -1,5 +1,4 @@ import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:lichess_mobile/src/model/settings/preferences.dart'; import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -14,6 +13,13 @@ class GamePreferences extends _$GamePreferences @override final prefCategory = PrefCategory.game; + // ignore: avoid_public_notifier_properties + @override + GamePrefs get defaults => GamePrefs.defaults; + + @override + GamePrefs fromJson(Map json) => GamePrefs.fromJson(json); + @override GamePrefs build() { return fetch(); @@ -32,7 +38,7 @@ class GamePreferences extends _$GamePreferences } @Freezed(fromJson: true, toJson: true) -class GamePrefs with _$GamePrefs implements SerializablePreferences { +class GamePrefs with _$GamePrefs implements Serializable { const factory GamePrefs({ bool? enableChat, bool? blindfoldMode, diff --git a/lib/src/model/lobby/game_setup_preferences.dart b/lib/src/model/lobby/game_setup_preferences.dart index d1c496b72f..6e0103195c 100644 --- a/lib/src/model/lobby/game_setup_preferences.dart +++ b/lib/src/model/lobby/game_setup_preferences.dart @@ -4,7 +4,6 @@ import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; import 'package:lichess_mobile/src/model/common/speed.dart'; import 'package:lichess_mobile/src/model/common/time_increment.dart'; -import 'package:lichess_mobile/src/model/settings/preferences.dart'; import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -19,6 +18,13 @@ class GameSetupPreferences extends _$GameSetupPreferences @override final prefCategory = PrefCategory.gameSetup; + @override + GameSetupPrefs defaults({LightUser? user}) => GameSetupPrefs.defaults; + + @override + GameSetupPrefs fromJson(Map json) => + GameSetupPrefs.fromJson(json); + @override GameSetupPrefs build() { return fetch(); @@ -60,7 +66,7 @@ class GameSetupPreferences extends _$GameSetupPreferences enum TimeControl { realTime, correspondence } @Freezed(fromJson: true, toJson: true) -class GameSetupPrefs with _$GameSetupPrefs implements SerializablePreferences { +class GameSetupPrefs with _$GameSetupPrefs implements Serializable { const GameSetupPrefs._(); const factory GameSetupPrefs({ diff --git a/lib/src/model/opening_explorer/opening_explorer_preferences.dart b/lib/src/model/opening_explorer/opening_explorer_preferences.dart index 79b22b733e..833b70af6c 100644 --- a/lib/src/model/opening_explorer/opening_explorer_preferences.dart +++ b/lib/src/model/opening_explorer/opening_explorer_preferences.dart @@ -3,7 +3,6 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/common/speed.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; -import 'package:lichess_mobile/src/model/settings/preferences.dart'; import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -18,6 +17,14 @@ class OpeningExplorerPreferences extends _$OpeningExplorerPreferences @override final prefCategory = PrefCategory.openingExplorer; + @override + OpeningExplorerPrefs defaults({LightUser? user}) => + OpeningExplorerPrefs.defaults(user: user); + + @override + OpeningExplorerPrefs fromJson(Map json) => + OpeningExplorerPrefs.fromJson(json); + @override OpeningExplorerPrefs build() { return fetch(); @@ -92,9 +99,7 @@ class OpeningExplorerPreferences extends _$OpeningExplorerPreferences } @Freezed(fromJson: true, toJson: true) -class OpeningExplorerPrefs - with _$OpeningExplorerPrefs - implements SerializablePreferences { +class OpeningExplorerPrefs with _$OpeningExplorerPrefs implements Serializable { const OpeningExplorerPrefs._(); const factory OpeningExplorerPrefs({ diff --git a/lib/src/model/puzzle/puzzle_preferences.dart b/lib/src/model/puzzle/puzzle_preferences.dart index 8af4fdbd97..25cf6be0b9 100644 --- a/lib/src/model/puzzle/puzzle_preferences.dart +++ b/lib/src/model/puzzle/puzzle_preferences.dart @@ -1,8 +1,8 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_difficulty.dart'; -import 'package:lichess_mobile/src/model/settings/preferences.dart'; import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; +import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'puzzle_preferences.freezed.dart'; @@ -15,6 +15,12 @@ class PuzzlePreferences extends _$PuzzlePreferences @override final prefCategory = PrefCategory.puzzle; + @override + PuzzlePrefs defaults({LightUser? user}) => PuzzlePrefs.defaults(id: user?.id); + + @override + PuzzlePrefs fromJson(Map json) => PuzzlePrefs.fromJson(json); + @override PuzzlePrefs build() { return fetch(); @@ -30,7 +36,7 @@ class PuzzlePreferences extends _$PuzzlePreferences } @Freezed(fromJson: true, toJson: true) -class PuzzlePrefs with _$PuzzlePrefs implements SerializablePreferences { +class PuzzlePrefs with _$PuzzlePrefs implements Serializable { const factory PuzzlePrefs({ required UserId? id, required PuzzleDifficulty difficulty, diff --git a/lib/src/model/settings/board_preferences.dart b/lib/src/model/settings/board_preferences.dart index b22b995165..ca5e4cb208 100644 --- a/lib/src/model/settings/board_preferences.dart +++ b/lib/src/model/settings/board_preferences.dart @@ -1,7 +1,6 @@ import 'package:chessground/chessground.dart'; import 'package:flutter/widgets.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:lichess_mobile/src/model/settings/preferences.dart'; import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; import 'package:lichess_mobile/src/utils/color_palette.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -14,7 +13,14 @@ class BoardPreferences extends _$BoardPreferences with PreferencesStorage { // ignore: avoid_public_notifier_properties @override - PrefCategory get prefCategory => PrefCategory.board; + PrefCategory get prefCategory => PrefCategory.board; + + // ignore: avoid_public_notifier_properties + @override + BoardPrefs get defaults => BoardPrefs.defaults; + + @override + BoardPrefs fromJson(Map json) => BoardPrefs.fromJson(json); @override BoardPrefs build() { @@ -87,7 +93,7 @@ class BoardPreferences extends _$BoardPreferences } @Freezed(fromJson: true, toJson: true) -class BoardPrefs with _$BoardPrefs implements SerializablePreferences { +class BoardPrefs with _$BoardPrefs implements Serializable { const BoardPrefs._(); const factory BoardPrefs({ diff --git a/lib/src/model/settings/general_preferences.dart b/lib/src/model/settings/general_preferences.dart index 7e77435b81..ef458d8bd6 100644 --- a/lib/src/model/settings/general_preferences.dart +++ b/lib/src/model/settings/general_preferences.dart @@ -2,7 +2,6 @@ import 'dart:ui' show Locale; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; -import 'package:lichess_mobile/src/model/settings/preferences.dart'; import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -16,6 +15,14 @@ class GeneralPreferences extends _$GeneralPreferences @override final prefCategory = PrefCategory.general; + // ignore: avoid_public_notifier_properties + @override + GeneralPrefs get defaults => GeneralPrefs.defaults; + + @override + GeneralPrefs fromJson(Map json) => + GeneralPrefs.fromJson(json); + @override GeneralPrefs build() { return fetch(); @@ -80,7 +87,7 @@ Locale? _localeFromJson(Map? json) { } @Freezed(fromJson: true, toJson: true) -class GeneralPrefs with _$GeneralPrefs implements SerializablePreferences { +class GeneralPrefs with _$GeneralPrefs implements Serializable { const factory GeneralPrefs({ @JsonKey( unknownEnumValue: BackgroundThemeMode.system, diff --git a/lib/src/model/settings/home_preferences.dart b/lib/src/model/settings/home_preferences.dart index 8431622253..31e2eefff0 100644 --- a/lib/src/model/settings/home_preferences.dart +++ b/lib/src/model/settings/home_preferences.dart @@ -1,5 +1,4 @@ import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:lichess_mobile/src/model/settings/preferences.dart'; import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -11,7 +10,14 @@ class HomePreferences extends _$HomePreferences with PreferencesStorage { // ignore: avoid_public_notifier_properties @override - PrefCategory get prefCategory => PrefCategory.home; + PrefCategory get prefCategory => PrefCategory.home; + + // ignore: avoid_public_notifier_properties + @override + HomePrefs get defaults => HomePrefs.defaults; + + @override + HomePrefs fromJson(Map json) => HomePrefs.fromJson(json); @override HomePrefs build() { @@ -35,7 +41,7 @@ enum EnabledWidget { } @Freezed(fromJson: true, toJson: true) -class HomePrefs with _$HomePrefs implements SerializablePreferences { +class HomePrefs with _$HomePrefs implements Serializable { const factory HomePrefs({ required Set enabledWidgets, }) = _HomePrefs; diff --git a/lib/src/model/settings/over_the_board_preferences.dart b/lib/src/model/settings/over_the_board_preferences.dart index fe3cd93ff9..7565119845 100644 --- a/lib/src/model/settings/over_the_board_preferences.dart +++ b/lib/src/model/settings/over_the_board_preferences.dart @@ -1,5 +1,4 @@ import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:lichess_mobile/src/model/settings/preferences.dart'; import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -11,7 +10,15 @@ class OverTheBoardPreferences extends _$OverTheBoardPreferences with PreferencesStorage { // ignore: avoid_public_notifier_properties @override - PrefCategory get prefCategory => PrefCategory.overTheBoard; + PrefCategory get prefCategory => PrefCategory.overTheBoard; + + // ignore: avoid_public_notifier_properties + @override + OverTheBoardPrefs get defaults => OverTheBoardPrefs.defaults; + + @override + OverTheBoardPrefs fromJson(Map json) => + OverTheBoardPrefs.fromJson(json); @override OverTheBoardPrefs build() { @@ -32,9 +39,7 @@ class OverTheBoardPreferences extends _$OverTheBoardPreferences } @Freezed(fromJson: true, toJson: true) -class OverTheBoardPrefs - with _$OverTheBoardPrefs - implements SerializablePreferences { +class OverTheBoardPrefs with _$OverTheBoardPrefs implements Serializable { const OverTheBoardPrefs._(); const factory OverTheBoardPrefs({ diff --git a/lib/src/model/settings/preferences.dart b/lib/src/model/settings/preferences.dart deleted file mode 100644 index 519c882c9f..0000000000 --- a/lib/src/model/settings/preferences.dart +++ /dev/null @@ -1,88 +0,0 @@ -import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; -import 'package:lichess_mobile/src/model/challenge/challenge_preferences.dart'; -import 'package:lichess_mobile/src/model/coordinate_training/coordinate_training_preferences.dart'; -import 'package:lichess_mobile/src/model/game/game_preferences.dart'; -import 'package:lichess_mobile/src/model/lobby/game_setup_preferences.dart'; -import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_preferences.dart'; -import 'package:lichess_mobile/src/model/puzzle/puzzle_preferences.dart'; -import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; -import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; -import 'package:lichess_mobile/src/model/settings/home_preferences.dart'; -import 'package:lichess_mobile/src/model/settings/over_the_board_preferences.dart'; -import 'package:lichess_mobile/src/model/user/user.dart'; - -/// A preference category with its storage key and default values. -enum PrefCategory { - general('preferences.general', GeneralPrefs.defaults), - home('preferences.home', HomePrefs.defaults), - board('preferences.board', BoardPrefs.defaults), - analysis('preferences.analysis', AnalysisPrefs.defaults), - overTheBoard('preferences.overTheBoard', OverTheBoardPrefs.defaults), - challenge('preferences.challenge', ChallengePrefs.defaults), - gameSetup('preferences.gameSetup', GameSetupPrefs.defaults), - game('preferences.game', GamePrefs.defaults), - coordinateTraining( - 'preferences.coordinateTraining', - CoordinateTrainingPrefs.defaults, - ), - openingExplorer( - 'preferences.opening_explorer', - null as OpeningExplorerPrefs?, - ), - puzzle('preferences.puzzle', null as PuzzlePrefs?); - - const PrefCategory(this.storageKey, this._defaults); - - final String storageKey; - final T? _defaults; - - T defaults({LightUser? user}) => switch (this) { - PrefCategory.general => _defaults!, - PrefCategory.home => _defaults!, - PrefCategory.board => _defaults!, - PrefCategory.analysis => _defaults!, - PrefCategory.overTheBoard => _defaults!, - PrefCategory.challenge => _defaults!, - PrefCategory.gameSetup => _defaults!, - PrefCategory.game => _defaults!, - PrefCategory.coordinateTraining => _defaults!, - PrefCategory.openingExplorer => - OpeningExplorerPrefs.defaults(user: user) as T, - PrefCategory.puzzle => PuzzlePrefs.defaults(id: user?.id) as T, - }; -} - -/// Interface for serializable preferences. -abstract class SerializablePreferences { - Map toJson(); - - static SerializablePreferences fromJson( - PrefCategory key, - Map json, - ) { - switch (key) { - case PrefCategory.general: - return GeneralPrefs.fromJson(json); - case PrefCategory.home: - return HomePrefs.fromJson(json); - case PrefCategory.board: - return BoardPrefs.fromJson(json); - case PrefCategory.analysis: - return AnalysisPrefs.fromJson(json); - case PrefCategory.overTheBoard: - return OverTheBoardPrefs.fromJson(json); - case PrefCategory.challenge: - return ChallengePrefs.fromJson(json); - case PrefCategory.gameSetup: - return GameSetupPrefs.fromJson(json); - case PrefCategory.game: - return GamePrefs.fromJson(json); - case PrefCategory.coordinateTraining: - return CoordinateTrainingPrefs.fromJson(json); - case PrefCategory.openingExplorer: - return OpeningExplorerPrefs.fromJson(json); - case PrefCategory.puzzle: - return PuzzlePrefs.fromJson(json); - } - } -} diff --git a/lib/src/model/settings/preferences_storage.dart b/lib/src/model/settings/preferences_storage.dart index 27e942e590..7a8a674f08 100644 --- a/lib/src/model/settings/preferences_storage.dart +++ b/lib/src/model/settings/preferences_storage.dart @@ -3,20 +3,43 @@ import 'dart:convert'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/binding.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; -import 'package:lichess_mobile/src/model/settings/preferences.dart'; +import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:logging/logging.dart'; final _logger = Logger('PreferencesStorage'); +abstract class Serializable { + Map toJson(); +} + +/// A preference category with its storage key +enum PrefCategory { + general('preferences.general'), + home('preferences.home'), + board('preferences.board'), + analysis('preferences.analysis'), + overTheBoard('preferences.overTheBoard'), + challenge('preferences.challenge'), + gameSetup('preferences.gameSetup'), + game('preferences.game'), + coordinateTraining('preferences.coordinateTraining'), + openingExplorer('preferences.opening_explorer'), + puzzle('preferences.puzzle'); + + const PrefCategory(this.storageKey); + + final String storageKey; +} + /// A [Notifier] mixin to provide a way to store and retrieve preferences. -/// -/// This mixin is intended to be used with a [Notifier] that holds a -/// [SerializablePreferences] object. -mixin PreferencesStorage { +mixin PreferencesStorage { AutoDisposeNotifierProviderRef get ref; abstract T state; - PrefCategory get prefCategory; + T fromJson(Map json); + T get defaults; + + PrefCategory get prefCategory; Future save(T value) async { await LichessBinding.instance.sharedPreferences @@ -29,31 +52,28 @@ mixin PreferencesStorage { final stored = LichessBinding.instance.sharedPreferences .getString(prefCategory.storageKey); if (stored == null) { - return prefCategory.defaults(); + return defaults; } try { - return SerializablePreferences.fromJson( - prefCategory, + return fromJson( jsonDecode(stored) as Map, - ) as T; + ); } catch (e) { _logger.warning('Failed to decode $prefCategory preferences: $e'); - return prefCategory.defaults(); + return defaults; } } } -/// A mixin to provide a way to store and retrieve preferences per session. -/// -/// This mixin is intended to be used with a [Notifier] that holds a -/// [SerializablePreferences] object. It provides a way to save and fetch the -/// preferences from the shared preferences, using the current session to -/// differentiate between different users. -mixin SessionPreferencesStorage { +/// A [Notifier] mixin to provide a way to store and retrieve preferences per session. +mixin SessionPreferencesStorage { AutoDisposeNotifierProviderRef get ref; abstract T state; - PrefCategory get prefCategory; + T fromJson(Map json); + T defaults({LightUser? user}); + + PrefCategory get prefCategory; Future save(T value) async { final session = ref.read(authSessionProvider); @@ -70,16 +90,15 @@ mixin SessionPreferencesStorage { final stored = LichessBinding.instance.sharedPreferences .getString(key(prefCategory.storageKey, session)); if (stored == null) { - return prefCategory.defaults(user: session?.user); + return defaults(user: session?.user); } try { - return SerializablePreferences.fromJson( - prefCategory, + return fromJson( jsonDecode(stored) as Map, - ) as T; + ); } catch (e) { _logger.warning('Failed to decode $prefCategory preferences: $e'); - return prefCategory.defaults(user: session?.user); + return defaults(user: session?.user); } } diff --git a/test/view/analysis/analysis_screen_test.dart b/test/view/analysis/analysis_screen_test.dart index b81f4f4892..489d2430a1 100644 --- a/test/view/analysis/analysis_screen_test.dart +++ b/test/view/analysis/analysis_screen_test.dart @@ -13,7 +13,7 @@ import 'package:lichess_mobile/src/model/common/speed.dart'; import 'package:lichess_mobile/src/model/game/archived_game.dart'; import 'package:lichess_mobile/src/model/game/game_status.dart'; import 'package:lichess_mobile/src/model/game/player.dart'; -import 'package:lichess_mobile/src/model/settings/preferences.dart'; +import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; import 'package:lichess_mobile/src/view/analysis/tree_view.dart'; diff --git a/test/view/opening_explorer/opening_explorer_screen_test.dart b/test/view/opening_explorer/opening_explorer_screen_test.dart index 6d0a16971e..a442d6f50e 100644 --- a/test/view/opening_explorer/opening_explorer_screen_test.dart +++ b/test/view/opening_explorer/opening_explorer_screen_test.dart @@ -10,7 +10,6 @@ import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_preferences.dart'; -import 'package:lichess_mobile/src/model/settings/preferences.dart'; import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/network/http.dart'; From f79be5044542a7df88a9dbce2c3a2c545d281c99 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 17 Oct 2024 11:29:28 +0200 Subject: [PATCH 496/979] Tweaks --- lib/src/binding.dart | 2 +- lib/src/constants.dart | 2 ++ lib/src/model/engine/evaluation_service.dart | 1 + lib/src/network/socket.dart | 1 - lib/src/view/settings/toggle_sound_button.dart | 1 + 5 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/src/binding.dart b/lib/src/binding.dart index c001f7fa87..7d80eaeb55 100644 --- a/lib/src/binding.dart +++ b/lib/src/binding.dart @@ -9,13 +9,13 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:lichess_mobile/firebase_options.dart'; import 'package:lichess_mobile/l10n/l10n.dart'; +import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/db/secure_storage.dart'; import 'package:lichess_mobile/src/log.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/auth/session_storage.dart'; import 'package:lichess_mobile/src/model/notifications/notification_service.dart'; import 'package:lichess_mobile/src/model/notifications/notifications.dart'; -import 'package:lichess_mobile/src/network/socket.dart'; import 'package:lichess_mobile/src/utils/string.dart'; import 'package:lichess_mobile/src/utils/system.dart'; import 'package:logging/logging.dart'; diff --git a/lib/src/constants.dart b/lib/src/constants.dart index 638c471c32..caad94ac7d 100644 --- a/lib/src/constants.dart +++ b/lib/src/constants.dart @@ -32,6 +32,8 @@ const kLichessDevPassword = String.fromEnvironment('LICHESS_DEV_PASSWORD'); const kLichessClientId = 'lichess_mobile'; +const kSRIStorageKey = 'socket_random_identifier'; + // lichess // https://github.com/lichess-org/lila/blob/4562a83cdb263c3ebf7e148c0f666f0ff92b91c7/modules/rating/src/main/Glicko.scala#L71 const kProvisionalDeviation = 110; diff --git a/lib/src/model/engine/evaluation_service.dart b/lib/src/model/engine/evaluation_service.dart index abeaa041a7..05ec8cc9e3 100644 --- a/lib/src/model/engine/evaluation_service.dart +++ b/lib/src/model/engine/evaluation_service.dart @@ -30,6 +30,7 @@ const engineSupportedVariants = { Variant.fromPosition, }; +/// A service to evaluate chess positions using an engine. class EvaluationService { EvaluationService(this.ref, {required this.maxMemory}); diff --git a/lib/src/network/socket.dart b/lib/src/network/socket.dart index cf8980c715..051cc107bf 100644 --- a/lib/src/network/socket.dart +++ b/lib/src/network/socket.dart @@ -20,7 +20,6 @@ import 'package:web_socket_channel/web_socket_channel.dart'; part 'socket.g.dart'; -const kSRIStorageKey = 'socket_random_identifier'; const kDefaultSocketRoute = '/socket/v5'; const _kDefaultConnectTimeout = Duration(seconds: 10); diff --git a/lib/src/view/settings/toggle_sound_button.dart b/lib/src/view/settings/toggle_sound_button.dart index 3bbf111eb6..a269e82b47 100644 --- a/lib/src/view/settings/toggle_sound_button.dart +++ b/lib/src/view/settings/toggle_sound_button.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; +/// A button that toggles the sound on and off. class ToggleSoundButton extends ConsumerWidget { const ToggleSoundButton({this.iconSize, super.key}); From 0bfbc2550f590cedb7d060adde38a7f7a56d98d5 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 17 Oct 2024 12:36:45 +0200 Subject: [PATCH 497/979] More refactoring to avoid long build_runner builds --- lib/main.dart | 7 +- lib/src/app.dart | 32 +++++- lib/src/binding.dart | 107 +----------------- lib/src/init.dart | 25 +++- lib/src/model/auth/auth_session.dart | 4 +- lib/src/model/common/preloaded_data.dart | 58 ++++++++++ lib/src/model/engine/evaluation_service.dart | 7 +- lib/src/model/lobby/create_game_service.dart | 6 +- .../notifications/notification_service.dart | 3 +- lib/src/network/http.dart | 17 ++- lib/src/network/socket.dart | 14 +-- .../view/settings/settings_tab_screen.dart | 5 +- test/binding.dart | 44 +------ test/test_container.dart | 28 ++++- test/test_provider_scope.dart | 28 ++++- 15 files changed, 201 insertions(+), 184 deletions(-) create mode 100644 lib/src/model/common/preloaded_data.dart diff --git a/lib/main.dart b/lib/main.dart index ed20196deb..c04e4e8423 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -25,7 +25,6 @@ Future main() async { await migrateSharedPreferences(); await lichessBinding.preloadSharedPreferences(); - await lichessBinding.preloadData(); await preloadPieceImages(); @@ -35,7 +34,9 @@ Future main() async { final locale = await setupIntl(widgetsBinding); - await lichessBinding.initializeNotifications(locale); + await initializeLocalNotifications(locale); + + await lichessBinding.initializeFirebase(); if (defaultTargetPlatform == TargetPlatform.android) { await androidDisplayInitialization(widgetsBinding); @@ -46,7 +47,7 @@ Future main() async { observers: [ ProviderLogger(), ], - child: const Application(), + child: const AppInitializationScreen(), ), ); } diff --git a/lib/src/app.dart b/lib/src/app.dart index b000b4e806..a0288d0b94 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -7,6 +7,7 @@ import 'package:lichess_mobile/l10n/l10n.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/challenge/challenge_service.dart'; +import 'package:lichess_mobile/src/model/common/preloaded_data.dart'; import 'package:lichess_mobile/src/model/correspondence/correspondence_service.dart'; import 'package:lichess_mobile/src/model/notifications/notification_service.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; @@ -19,6 +20,35 @@ import 'package:lichess_mobile/src/network/socket.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; +/// Application initialization and main entry point. +class AppInitializationScreen extends ConsumerWidget { + const AppInitializationScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + ref.listen>( + preloadedDataProvider, + (_, state) { + if (state.hasValue || state.hasError) { + FlutterNativeSplash.remove(); + } + }, + ); + + return ref.watch(preloadedDataProvider).when( + data: (_) => const Application(), + // loading screen is handled by the native splash screen + loading: () => const SizedBox.shrink(), + error: (err, st) { + debugPrint( + 'SEVERE: [App] could not initialize app; $err\n$st', + ); + return const SizedBox.shrink(); + }, + ); + } +} + /// The main application widget. /// /// This widget is the root of the application and is responsible for setting up @@ -38,8 +68,6 @@ class _AppState extends ConsumerState { @override void initState() { - FlutterNativeSplash.remove(); - _appLifecycleListener = AppLifecycleListener( onResume: () async { final online = await isOnline(ref.read(defaultClientProvider)); diff --git a/lib/src/binding.dart b/lib/src/binding.dart index 7d80eaeb55..6b42a43f8f 100644 --- a/lib/src/binding.dart +++ b/lib/src/binding.dart @@ -1,29 +1,12 @@ -import 'dart:convert'; - -import 'package:device_info_plus/device_info_plus.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; -import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:lichess_mobile/firebase_options.dart'; -import 'package:lichess_mobile/l10n/l10n.dart'; -import 'package:lichess_mobile/src/constants.dart'; -import 'package:lichess_mobile/src/db/secure_storage.dart'; import 'package:lichess_mobile/src/log.dart'; -import 'package:lichess_mobile/src/model/auth/auth_session.dart'; -import 'package:lichess_mobile/src/model/auth/session_storage.dart'; -import 'package:lichess_mobile/src/model/notifications/notification_service.dart'; -import 'package:lichess_mobile/src/model/notifications/notifications.dart'; -import 'package:lichess_mobile/src/utils/string.dart'; -import 'package:lichess_mobile/src/utils/system.dart'; -import 'package:logging/logging.dart'; -import 'package:package_info_plus/package_info_plus.dart'; import 'package:shared_preferences/shared_preferences.dart'; -final _logger = Logger('LichessBinding'); - /// A singleton class that provides access to plugins and external APIs. /// /// Only one instance of this class will be created during the app's lifetime. @@ -77,29 +60,12 @@ abstract class LichessBinding { /// have not yet been initialized. SharedPreferencesWithCache get sharedPreferences; - /// Application package information. - PackageInfo get packageInfo; - - /// Device information. - BaseDeviceInfo get deviceInfo; - - /// The user session read during app initialization. - AuthSessionState? get initialUserSession; - - /// Socket Random Identifier. - String get sri; - - /// Maximum memory in MB that the engine can use. - /// - /// This is 10% of the total physical memory. - int get engineMaxMemoryInMb; - - /// Initialize notifications. + /// Initialize Firebase. /// - /// This wraps [Firebase.initializeApp] and [FlutterLocalNotificationsPlugin.initialize]. + /// This wraps [Firebase.initializeApp]. /// /// This should be called only once before the app starts. - Future initializeNotifications(Locale locale); + Future initializeFirebase(); /// Wraps [FirebaseMessaging.instance]. FirebaseMessaging get firebaseMessaging; @@ -163,56 +129,8 @@ class AppLichessBinding extends LichessBinding { _syncSharedPreferencesWithCache = await _sharedPreferencesWithCache; } - late PackageInfo _syncPackageInfo; - late BaseDeviceInfo _syncDeviceInfo; - AuthSessionState? _syncInitialUserSession; - late String _syncSri; - late int _syncEngineMaxMemoryInMb; - - @override - PackageInfo get packageInfo => _syncPackageInfo; - - @override - BaseDeviceInfo get deviceInfo => _syncDeviceInfo; - - @override - AuthSessionState? get initialUserSession => _syncInitialUserSession; - - @override - String get sri => _syncSri; - - @override - int get engineMaxMemoryInMb => _syncEngineMaxMemoryInMb; - - /// Preload useful data. - /// - /// This must be called only once before the app starts. - Future preloadData() async { - _syncPackageInfo = await PackageInfo.fromPlatform(); - _syncDeviceInfo = await DeviceInfoPlugin().deviceInfo; - - final string = await SecureStorage.instance.read(key: kSessionStorageKey); - if (string != null) { - _syncInitialUserSession = AuthSessionState.fromJson( - jsonDecode(string) as Map, - ); - } - - final storedSri = await SecureStorage.instance.read(key: kSRIStorageKey); - - if (storedSri == null) { - _logger.warning('SRI not found in secure storage'); - } - - _syncSri = storedSri ?? genRandomString(12); - - final physicalMemory = await System.instance.getTotalRam() ?? 256.0; - final engineMaxMemory = (physicalMemory / 10).ceil(); - _syncEngineMaxMemoryInMb = engineMaxMemory; - } - @override - Future initializeNotifications(Locale locale) async { + Future initializeFirebase() async { await Firebase.initializeApp( options: DefaultFirebaseOptions.currentPlatform, ); @@ -225,23 +143,6 @@ class AppLichessBinding extends LichessBinding { return true; }; } - - final l10n = await AppLocalizations.delegate.load(locale); - await FlutterLocalNotificationsPlugin().initialize( - InitializationSettings( - android: const AndroidInitializationSettings('logo_black'), - iOS: DarwinInitializationSettings( - requestBadgePermission: false, - notificationCategories: [ - ChallengeNotification.darwinPlayableVariantCategory(l10n), - ChallengeNotification.darwinUnplayableVariantCategory(l10n), - ], - ), - ), - onDidReceiveNotificationResponse: - NotificationService.onDidReceiveNotificationResponse, - // onDidReceiveBackgroundNotificationResponse: notificationTapBackground, - ); } @override diff --git a/lib/src/init.dart b/lib/src/init.dart index 8e146691a0..788d8040b4 100644 --- a/lib/src/init.dart +++ b/lib/src/init.dart @@ -4,11 +4,15 @@ import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_displaymode/flutter_displaymode.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:lichess_mobile/l10n/l10n.dart'; import 'package:lichess_mobile/src/binding.dart'; +import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/db/secure_storage.dart'; +import 'package:lichess_mobile/src/model/notifications/notification_service.dart'; +import 'package:lichess_mobile/src/model/notifications/notifications.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; -import 'package:lichess_mobile/src/network/socket.dart'; import 'package:lichess_mobile/src/utils/chessboard.dart'; import 'package:lichess_mobile/src/utils/color_palette.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; @@ -45,6 +49,25 @@ Future setupFirstLaunch() async { } } +Future initializeLocalNotifications(Locale locale) async { + final l10n = await AppLocalizations.delegate.load(locale); + await FlutterLocalNotificationsPlugin().initialize( + InitializationSettings( + android: const AndroidInitializationSettings('logo_black'), + iOS: DarwinInitializationSettings( + requestBadgePermission: false, + notificationCategories: [ + ChallengeNotification.darwinPlayableVariantCategory(l10n), + ChallengeNotification.darwinUnplayableVariantCategory(l10n), + ], + ), + ), + onDidReceiveNotificationResponse: + NotificationService.onDidReceiveNotificationResponse, + // onDidReceiveBackgroundNotificationResponse: notificationTapBackground, + ); +} + Future preloadPieceImages() async { final prefs = LichessBinding.instance.sharedPreferences; final storedPrefs = prefs.getString(PrefCategory.board.storageKey); diff --git a/lib/src/model/auth/auth_session.dart b/lib/src/model/auth/auth_session.dart index c53e863607..d104df6edc 100644 --- a/lib/src/model/auth/auth_session.dart +++ b/lib/src/model/auth/auth_session.dart @@ -1,5 +1,5 @@ import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:lichess_mobile/src/binding.dart'; +import 'package:lichess_mobile/src/model/common/preloaded_data.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -12,7 +12,7 @@ part 'auth_session.g.dart'; class AuthSession extends _$AuthSession { @override AuthSessionState? build() { - return LichessBinding.instance.initialUserSession; + return ref.read(preloadedDataProvider).requireValue.userSession; } Future update(AuthSessionState session) async { diff --git a/lib/src/model/common/preloaded_data.dart b/lib/src/model/common/preloaded_data.dart new file mode 100644 index 0000000000..95ffc6975f --- /dev/null +++ b/lib/src/model/common/preloaded_data.dart @@ -0,0 +1,58 @@ +import 'package:device_info_plus/device_info_plus.dart'; +import 'package:flutter/services.dart'; +import 'package:lichess_mobile/src/constants.dart'; +import 'package:lichess_mobile/src/db/secure_storage.dart'; +import 'package:lichess_mobile/src/model/auth/auth_session.dart'; +import 'package:lichess_mobile/src/model/auth/session_storage.dart'; +import 'package:lichess_mobile/src/utils/string.dart'; +import 'package:lichess_mobile/src/utils/system.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'preloaded_data.g.dart'; + +typedef PreloadedData = ({ + PackageInfo packageInfo, + BaseDeviceInfo deviceInfo, + AuthSessionState? userSession, + String sri, + int engineMaxMemoryInMb, +}); + +@Riverpod(keepAlive: true) +Future preloadedData(PreloadedDataRef ref) async { + final sessionStorage = ref.watch(sessionStorageProvider); + + final pInfo = await PackageInfo.fromPlatform(); + final deviceInfo = await DeviceInfoPlugin().deviceInfo; + + // Generate a socket random identifier and store it for the app lifetime + String? storedSri; + try { + storedSri = await SecureStorage.instance.read(key: kSRIStorageKey); + if (storedSri == null) { + final sri = genRandomString(12); + await SecureStorage.instance.write(key: kSRIStorageKey, value: sri); + storedSri = sri; + } + } on PlatformException catch (_) { + // Clear all secure storage if an error occurs because it probably means the key has + // been lost + await SecureStorage.instance.deleteAll(); + } + + final sri = storedSri ?? genRandomString(12); + + final userSession = await sessionStorage.read(); + + final physicalMemory = await System.instance.getTotalRam() ?? 256.0; + final engineMaxMemory = (physicalMemory / 10).ceil(); + + return ( + packageInfo: pInfo, + deviceInfo: deviceInfo, + userSession: userSession, + sri: sri, + engineMaxMemoryInMb: engineMaxMemory, + ); +} diff --git a/lib/src/model/engine/evaluation_service.dart b/lib/src/model/engine/evaluation_service.dart index 05ec8cc9e3..7abb7a45dc 100644 --- a/lib/src/model/engine/evaluation_service.dart +++ b/lib/src/model/engine/evaluation_service.dart @@ -6,9 +6,9 @@ import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/foundation.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:lichess_mobile/src/binding.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/eval.dart'; +import 'package:lichess_mobile/src/model/common/preloaded_data.dart'; import 'package:lichess_mobile/src/model/common/uci.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:stream_transform/stream_transform.dart'; @@ -177,9 +177,8 @@ class EvaluationService { @Riverpod(keepAlive: true) EvaluationService evaluationService(EvaluationServiceRef ref) { - // requireValue is possible because cachedDataProvider is loaded before - // anything. See: lib/src/app.dart - final maxMemory = LichessBinding.instance.engineMaxMemoryInMb; + final maxMemory = + ref.read(preloadedDataProvider).requireValue.engineMaxMemoryInMb; final service = EvaluationService(ref, maxMemory: maxMemory); ref.onDispose(() { diff --git a/lib/src/model/lobby/create_game_service.dart b/lib/src/model/lobby/create_game_service.dart index be6a8c7a4a..96eeccd783 100644 --- a/lib/src/model/lobby/create_game_service.dart +++ b/lib/src/model/lobby/create_game_service.dart @@ -1,11 +1,11 @@ import 'dart:async'; import 'package:deep_pick/deep_pick.dart'; -import 'package:lichess_mobile/src/binding.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/challenge/challenge.dart'; import 'package:lichess_mobile/src/model/challenge/challenge_repository.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/common/preloaded_data.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/lobby/game_seek.dart'; import 'package:lichess_mobile/src/model/lobby/lobby_repository.dart'; @@ -109,7 +109,7 @@ class CreateGameService { await ref.withClient( (client) => LobbyRepository(client).createSeek( seek, - sri: LichessBinding.instance.sri, + sri: ref.read(preloadedDataProvider).requireValue.sri, ), ); } @@ -213,7 +213,7 @@ class CreateGameService { /// Cancel the current game creation. Future cancelSeek() async { _log.info('Cancelling game creation'); - final sri = LichessBinding.instance.sri; + final sri = ref.read(preloadedDataProvider).requireValue.sri; try { await LobbyRepository(lichessClient).cancelSeek(sri: sri); } catch (e) { diff --git a/lib/src/model/notifications/notification_service.dart b/lib/src/model/notifications/notification_service.dart index 842834d025..b4ad993235 100644 --- a/lib/src/model/notifications/notification_service.dart +++ b/lib/src/model/notifications/notification_service.dart @@ -8,6 +8,7 @@ import 'package:lichess_mobile/l10n/l10n.dart'; import 'package:lichess_mobile/src/binding.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/challenge/challenge_service.dart'; +import 'package:lichess_mobile/src/model/common/preloaded_data.dart'; import 'package:lichess_mobile/src/model/correspondence/correspondence_service.dart'; import 'package:lichess_mobile/src/model/notifications/notifications.dart'; import 'package:lichess_mobile/src/network/connectivity.dart'; @@ -344,7 +345,7 @@ class NotificationService { final lichessBinding = AppLichessBinding.ensureInitialized(); await lichessBinding.preloadSharedPreferences(); - await lichessBinding.preloadData(); + await ref.read(preloadedDataProvider.future); try { await ref.read(notificationServiceProvider)._processFcmMessage( diff --git a/lib/src/network/http.dart b/lib/src/network/http.dart index af20b2faa9..b82d9b7d22 100644 --- a/lib/src/network/http.dart +++ b/lib/src/network/http.dart @@ -21,10 +21,10 @@ import 'package:http/http.dart' StreamedResponse; import 'package:http/io_client.dart'; import 'package:http/retry.dart'; -import 'package:lichess_mobile/src/binding.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/auth/bearer.dart'; +import 'package:lichess_mobile/src/model/common/preloaded_data.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:logging/logging.dart'; import 'package:package_info_plus/package_info_plus.dart'; @@ -47,8 +47,7 @@ Uri lichessUri(String unencodedPath, [Map? queryParameters]) => /// Do not use directly, use [defaultClient] or [lichessClient] instead. class HttpClientFactory { Client call() { - final packageInfo = LichessBinding.instance.packageInfo; - final userAgent = 'Lichess Mobile/${packageInfo.version}'; + const userAgent = 'Lichess Mobile'; if (Platform.isAndroid) { final engine = CronetEngine.build( cacheMode: CacheMode.memory, @@ -112,9 +111,9 @@ String userAgent(UserAgentRef ref) { final session = ref.watch(authSessionProvider); return makeUserAgent( - LichessBinding.instance.packageInfo, - LichessBinding.instance.deviceInfo, - LichessBinding.instance.sri, + ref.read(preloadedDataProvider).requireValue.packageInfo, + ref.read(preloadedDataProvider).requireValue.deviceInfo, + ref.read(preloadedDataProvider).requireValue.sri, session?.user, ); } @@ -176,9 +175,9 @@ class LichessClient implements Client { request.headers['Authorization'] = 'Bearer $bearer'; } request.headers['User-Agent'] = makeUserAgent( - LichessBinding.instance.packageInfo, - LichessBinding.instance.deviceInfo, - LichessBinding.instance.sri, + _ref.read(preloadedDataProvider).requireValue.packageInfo, + _ref.read(preloadedDataProvider).requireValue.deviceInfo, + _ref.read(preloadedDataProvider).requireValue.sri, session?.user, ); diff --git a/lib/src/network/socket.dart b/lib/src/network/socket.dart index 051cc107bf..e055f29023 100644 --- a/lib/src/network/socket.dart +++ b/lib/src/network/socket.dart @@ -6,10 +6,10 @@ import 'dart:math' as math; import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; -import 'package:lichess_mobile/src/binding.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/auth/bearer.dart'; +import 'package:lichess_mobile/src/model/common/preloaded_data.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/network/http.dart'; import 'package:logging/logging.dart'; @@ -452,11 +452,11 @@ class SocketPool { // Create a default socket client. This one is never disposed. final client = SocketClient( _currentRoute, - sri: LichessBinding.instance.sri, + sri: _ref.read(preloadedDataProvider).requireValue.sri, channelFactory: _ref.read(webSocketChannelFactoryProvider), getSession: () => _ref.read(authSessionProvider), - packageInfo: LichessBinding.instance.packageInfo, - deviceInfo: LichessBinding.instance.deviceInfo, + packageInfo: _ref.read(preloadedDataProvider).requireValue.packageInfo, + deviceInfo: _ref.read(preloadedDataProvider).requireValue.deviceInfo, pingDelay: const Duration(seconds: 25), ); @@ -507,9 +507,9 @@ class SocketPool { route, channelFactory: _ref.read(webSocketChannelFactoryProvider), getSession: () => _ref.read(authSessionProvider), - packageInfo: LichessBinding.instance.packageInfo, - deviceInfo: LichessBinding.instance.deviceInfo, - sri: LichessBinding.instance.sri, + packageInfo: _ref.read(preloadedDataProvider).requireValue.packageInfo, + deviceInfo: _ref.read(preloadedDataProvider).requireValue.deviceInfo, + sri: _ref.read(preloadedDataProvider).requireValue.sri, onStreamListen: () { _disposeTimers[route]?.cancel(); }, diff --git a/lib/src/view/settings/settings_tab_screen.dart b/lib/src/view/settings/settings_tab_screen.dart index 134757963c..98b62dd96d 100644 --- a/lib/src/view/settings/settings_tab_screen.dart +++ b/lib/src/view/settings/settings_tab_screen.dart @@ -2,12 +2,12 @@ import 'package:app_settings/app_settings.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lichess_mobile/src/binding.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/db/database.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/auth/auth_controller.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; +import 'package:lichess_mobile/src/model/common/preloaded_data.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; import 'package:lichess_mobile/src/navigation.dart'; @@ -93,7 +93,8 @@ class _Body extends ConsumerWidget { final boardPrefs = ref.watch(boardPreferencesProvider); final authController = ref.watch(authControllerProvider); final userSession = ref.watch(authSessionProvider); - final packageInfo = LichessBinding.instance.packageInfo; + final packageInfo = + ref.read(preloadedDataProvider).requireValue.packageInfo; final dbSize = ref.watch(getDbSizeInBytesProvider); final androidVersionAsync = ref.watch(androidVersionProvider); diff --git a/test/binding.dart b/test/binding.dart index 606beb5822..485926ce52 100644 --- a/test/binding.dart +++ b/test/binding.dart @@ -1,14 +1,10 @@ import 'dart:async'; -import 'dart:ui'; -import 'package:device_info_plus/device_info_plus.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/binding.dart'; -import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:logging/logging.dart'; -import 'package:package_info_plus/package_info_plus.dart'; import 'package:shared_preferences/shared_preferences.dart'; /// The binding instance used in tests. @@ -50,43 +46,6 @@ class TestLichessBinding extends LichessBinding { _instance = this; } - /// Preload useful data. - /// - /// This should be called only once before the app starts. - Future preloadData(AuthSessionState? initialUserSession) async { - _initialUserSession = initialUserSession; - } - - AuthSessionState? _initialUserSession; - - @override - BaseDeviceInfo get deviceInfo => BaseDeviceInfo({ - 'name': 'test', - 'model': 'test', - 'manufacturer': 'test', - 'systemName': 'test', - 'systemVersion': 'test', - 'identifierForVendor': 'test', - 'isPhysicalDevice': true, - }); - - @override - int get engineMaxMemoryInMb => 256; - - @override - AuthSessionState? get initialUserSession => _initialUserSession; - - @override - PackageInfo get packageInfo => PackageInfo( - appName: 'lichess_mobile_test', - version: '0.0.0', - buildNumber: '0', - packageName: 'lichess_mobile_test', - ); - - @override - String get sri => 'test-sri'; - /// Set the initial values for shared preferences. Future setInitialSharedPreferencesValues( Map values, @@ -128,13 +87,12 @@ class TestLichessBinding extends LichessBinding { void reset() { _firebaseMessaging = null; _sharedPreferences = null; - _initialUserSession = null; } FakeFirebaseMessaging? _firebaseMessaging; @override - Future initializeNotifications(Locale locale) async {} + Future initializeFirebase() async {} @override FakeFirebaseMessaging get firebaseMessaging { diff --git a/test/test_container.dart b/test/test_container.dart index 6c9ca502e9..ecb340ee0a 100644 --- a/test/test_container.dart +++ b/test/test_container.dart @@ -1,16 +1,20 @@ +import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; +import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/crashlytics.dart'; import 'package:lichess_mobile/src/db/database.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; +import 'package:lichess_mobile/src/model/common/preloaded_data.dart'; import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; import 'package:lichess_mobile/src/model/notifications/notification_service.dart'; import 'package:lichess_mobile/src/network/connectivity.dart'; import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/network/socket.dart'; +import 'package:package_info_plus/package_info_plus.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import './fake_crashlytics.dart'; @@ -49,8 +53,6 @@ Future makeContainer({ kSRIStorageKey: 'test', }); - await binding.preloadData(userSession); - final container = ProviderContainer( overrides: [ connectivityPluginProvider.overrideWith((_) { @@ -78,6 +80,28 @@ Future makeContainer({ }), crashlyticsProvider.overrideWithValue(FakeCrashlytics()), soundServiceProvider.overrideWithValue(FakeSoundService()), + preloadedDataProvider.overrideWith((ref) { + return ( + sri: 'test-sri', + packageInfo: PackageInfo( + appName: 'lichess_mobile_test', + version: '0.0.0', + buildNumber: '0', + packageName: 'lichess_mobile_test', + ), + deviceInfo: BaseDeviceInfo({ + 'name': 'test', + 'model': 'test', + 'manufacturer': 'test', + 'systemName': 'test', + 'systemVersion': 'test', + 'identifierForVendor': 'test', + 'isPhysicalDevice': true, + }), + userSession: userSession, + engineMaxMemoryInMb: 256, + ); + }), ...overrides ?? [], ], ); diff --git a/test/test_provider_scope.dart b/test/test_provider_scope.dart index 854354a1fb..853f5e8738 100644 --- a/test/test_provider_scope.dart +++ b/test/test_provider_scope.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -8,18 +9,21 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; import 'package:lichess_mobile/l10n/l10n.dart'; +import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/crashlytics.dart'; import 'package:lichess_mobile/src/db/database.dart'; import 'package:lichess_mobile/src/model/account/account_preferences.dart'; import 'package:lichess_mobile/src/model/analysis/opening_service.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/auth/session_storage.dart'; +import 'package:lichess_mobile/src/model/common/preloaded_data.dart'; import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; import 'package:lichess_mobile/src/model/notifications/notification_service.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/network/connectivity.dart'; import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/network/socket.dart'; +import 'package:package_info_plus/package_info_plus.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:visibility_detector/visibility_detector.dart'; @@ -116,8 +120,6 @@ Future makeTestProviderScope( : defaultBoardPref, ); - await binding.preloadData(userSession); - FlutterSecureStorage.setMockInitialValues({ kSRIStorageKey: 'test', if (userSession != null) @@ -166,6 +168,28 @@ Future makeTestProviderScope( soundServiceProvider.overrideWithValue(FakeSoundService()), // ignore: scoped_providers_should_specify_dependencies openingServiceProvider.overrideWithValue(FakeOpeningService()), + preloadedDataProvider.overrideWith((ref) { + return ( + sri: 'test-sri', + packageInfo: PackageInfo( + appName: 'lichess_mobile_test', + version: 'test', + buildNumber: '0.0.0', + packageName: 'lichess_mobile_test', + ), + deviceInfo: BaseDeviceInfo({ + 'name': 'test', + 'model': 'test', + 'manufacturer': 'test', + 'systemName': 'test', + 'systemVersion': 'test', + 'identifierForVendor': 'test', + 'isPhysicalDevice': true, + }), + userSession: userSession, + engineMaxMemoryInMb: 256, + ); + }), ...overrides ?? [], ], child: MediaQuery( From e182047cfe0c5df4652d2f5d517f09179e2b3bfb Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 17 Oct 2024 12:53:04 +0200 Subject: [PATCH 498/979] Fix warning about generated provider --- build.yaml | 1 + lib/src/localizations.dart | 47 +++++++++++++++++++ .../notifications/notification_service.dart | 4 +- lib/src/model/puzzle/puzzle_theme.dart | 4 +- lib/src/utils/l10n.dart | 47 ------------------- test/test_provider_scope.dart | 1 + 6 files changed, 53 insertions(+), 51 deletions(-) create mode 100644 lib/src/localizations.dart diff --git a/build.yaml b/build.yaml index 6621469741..5bed70aa37 100644 --- a/build.yaml +++ b/build.yaml @@ -12,6 +12,7 @@ targets: to_json: false riverpod_generator: generate_for: + - lib/src/localizations.dart - lib/src/model/**/*.dart - lib/src/network/*.dart - lib/src/db/*.dart diff --git a/lib/src/localizations.dart b/lib/src/localizations.dart new file mode 100644 index 0000000000..830328c270 --- /dev/null +++ b/lib/src/localizations.dart @@ -0,0 +1,47 @@ +import 'package:flutter/widgets.dart'; +import 'package:lichess_mobile/l10n/l10n.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'localizations.g.dart'; + +typedef ActiveLocalizations = ({ + Locale locale, + AppLocalizations strings, +}); + +@Riverpod(keepAlive: true) +class Localizations extends _$Localizations { + @override + ActiveLocalizations build() { + final observer = _LocaleObserver((locales) { + _update(); + }); + final binding = WidgetsBinding.instance; + binding.addObserver(observer); + ref.onDispose(() => binding.removeObserver(observer)); + + return _getLocale(); + } + + void _update() { + state = _getLocale(); + } + + ActiveLocalizations _getLocale() { + final locale = WidgetsBinding.instance.platformDispatcher.locale; + return ( + locale: locale, + strings: lookupAppLocalizations(locale), + ); + } +} + +/// observer used to notify the caller when the locale changes +class _LocaleObserver extends WidgetsBindingObserver { + _LocaleObserver(this._didChangeLocales); + final void Function(List? locales) _didChangeLocales; + @override + void didChangeLocales(List? locales) { + _didChangeLocales(locales); + } +} diff --git a/lib/src/model/notifications/notification_service.dart b/lib/src/model/notifications/notification_service.dart index b4ad993235..d13624e37d 100644 --- a/lib/src/model/notifications/notification_service.dart +++ b/lib/src/model/notifications/notification_service.dart @@ -6,6 +6,7 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/l10n/l10n.dart'; import 'package:lichess_mobile/src/binding.dart'; +import 'package:lichess_mobile/src/localizations.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/challenge/challenge_service.dart'; import 'package:lichess_mobile/src/model/common/preloaded_data.dart'; @@ -14,7 +15,6 @@ import 'package:lichess_mobile/src/model/notifications/notifications.dart'; import 'package:lichess_mobile/src/network/connectivity.dart'; import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/utils/badge_service.dart'; -import 'package:lichess_mobile/src/utils/l10n.dart'; import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -66,7 +66,7 @@ class NotificationService { /// Whether the device has been registered for push notifications. bool _registeredDevice = false; - AppLocalizations get _l10n => _ref.read(l10nProvider).strings; + AppLocalizations get _l10n => _ref.read(localizationsProvider).strings; FlutterLocalNotificationsPlugin get _notificationDisplay => _ref.read(notificationDisplayProvider); diff --git a/lib/src/model/puzzle/puzzle_theme.dart b/lib/src/model/puzzle/puzzle_theme.dart index 42cda10e69..12f22021ef 100644 --- a/lib/src/model/puzzle/puzzle_theme.dart +++ b/lib/src/model/puzzle/puzzle_theme.dart @@ -2,8 +2,8 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/widgets.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/l10n/l10n.dart'; +import 'package:lichess_mobile/src/localizations.dart'; import 'package:lichess_mobile/src/styles/puzzle_icons.dart'; -import 'package:lichess_mobile/src/utils/l10n.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'puzzle_theme.freezed.dart'; @@ -410,7 +410,7 @@ typedef PuzzleThemeCategory = (String, List); IList puzzleThemeCategories( PuzzleThemeCategoriesRef ref, ) { - final l10n = ref.watch(l10nProvider); + final l10n = ref.watch(localizationsProvider); return IList([ ( diff --git a/lib/src/utils/l10n.dart b/lib/src/utils/l10n.dart index 6324bed96a..fcfe83aee0 100644 --- a/lib/src/utils/l10n.dart +++ b/lib/src/utils/l10n.dart @@ -1,51 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lichess_mobile/l10n/l10n.dart'; - -typedef ActiveLocalizations = ({ - Locale locale, - AppLocalizations strings, -}); - -/// A provider that exposes the current locale and localized strings. -final l10nProvider = - NotifierProvider(L10nNotifier.new); - -class L10nNotifier extends Notifier { - @override - ActiveLocalizations build() { - final observer = _LocaleObserver((locales) { - _update(); - }); - final binding = WidgetsBinding.instance; - binding.addObserver(observer); - ref.onDispose(() => binding.removeObserver(observer)); - - return _getLocale(); - } - - void _update() { - state = _getLocale(); - } - - ActiveLocalizations _getLocale() { - final locale = WidgetsBinding.instance.platformDispatcher.locale; - return ( - locale: locale, - strings: lookupAppLocalizations(locale), - ); - } -} - -/// observer used to notify the caller when the locale changes -class _LocaleObserver extends WidgetsBindingObserver { - _LocaleObserver(this._didChangeLocales); - final void Function(List? locales) _didChangeLocales; - @override - void didChangeLocales(List? locales) { - _didChangeLocales(locales); - } -} /// Returns a localized string with a single placeholder replaced by a widget. /// diff --git a/test/test_provider_scope.dart b/test/test_provider_scope.dart index 853f5e8738..2b87ad5523 100644 --- a/test/test_provider_scope.dart +++ b/test/test_provider_scope.dart @@ -168,6 +168,7 @@ Future makeTestProviderScope( soundServiceProvider.overrideWithValue(FakeSoundService()), // ignore: scoped_providers_should_specify_dependencies openingServiceProvider.overrideWithValue(FakeOpeningService()), + // ignore: scoped_providers_should_specify_dependencies preloadedDataProvider.overrideWith((ref) { return ( sri: 'test-sri', From e75571c45a6d950340017bbff83d4a438569f486 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 17 Oct 2024 14:22:30 +0200 Subject: [PATCH 499/979] Fix test package info --- test/test_provider_scope.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_provider_scope.dart b/test/test_provider_scope.dart index 2b87ad5523..148dfbc2e0 100644 --- a/test/test_provider_scope.dart +++ b/test/test_provider_scope.dart @@ -174,8 +174,8 @@ Future makeTestProviderScope( sri: 'test-sri', packageInfo: PackageInfo( appName: 'lichess_mobile_test', - version: 'test', - buildNumber: '0.0.0', + version: '0.0.0', + buildNumber: '0', packageName: 'lichess_mobile_test', ), deviceInfo: BaseDeviceInfo({ From 17082eb13b950b9414913b112d979bb0e1230d9e Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 17 Oct 2024 15:08:36 +0200 Subject: [PATCH 500/979] Fix token test --- lib/src/network/http.dart | 1 + test/network/http_test.dart | 105 ++++++++++++++++++++++++++---------- 2 files changed, 77 insertions(+), 29 deletions(-) diff --git a/lib/src/network/http.dart b/lib/src/network/http.dart index b82d9b7d22..7c8ba776a8 100644 --- a/lib/src/network/http.dart +++ b/lib/src/network/http.dart @@ -208,6 +208,7 @@ class LichessClient implements Client { .postReadJson( lichessUri('/api/token/test'), mapper: (json) => json, + body: session.token, ) .timeout(const Duration(seconds: 5)); if (data[session.token] == null) { diff --git a/test/network/http_test.dart b/test/network/http_test.dart index eb48167f16..488eaed8ef 100644 --- a/test/network/http_test.dart +++ b/test/network/http_test.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:fake_async/fake_async.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; +import 'package:http/testing.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/auth/bearer.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; @@ -283,11 +284,23 @@ void main() { test( 'when receiving a 401, will test session token and delete session if not valid anymore', () async { + int nbTokenTestRequests = 0; final container = await makeContainer( overrides: [ httpClientFactoryProvider.overrideWith((ref) { return FakeHttpClientFactory(() => FakeClient()); }), + defaultClientProvider.overrideWith((ref) { + return MockClient((request) async { + if (request.url.path == '/api/token/test') { + nbTokenTestRequests++; + final token = request.body.split(',')[0]; + final response = '{"$token": null}'; + return http.Response(response, 200); + } + return http.Response('', 404); + }); + }), ], userSession: const AuthSessionState( token: 'test-token', @@ -307,7 +320,7 @@ void main() { async.flushMicrotasks(); final requests = FakeClient.verifyRequests(); - expect(requests.length, 2); + expect(requests.length, 1); expect( requests.first, isA().having( @@ -317,27 +330,65 @@ void main() { ), ); + expect(nbTokenTestRequests, 1); + + expect(container.read(authSessionProvider), isNull); + }); + }); + + test( + 'when receiving a 401, will test session token and keep session if still valid', + () async { + int nbTokenTestRequests = 0; + final container = await makeContainer( + overrides: [ + httpClientFactoryProvider.overrideWith((ref) { + return FakeHttpClientFactory(() => FakeClient()); + }), + defaultClientProvider.overrideWith((ref) { + return MockClient((request) async { + if (request.url.path == '/api/token/test') { + nbTokenTestRequests++; + final token = request.body.split(',')[0]; + final response = + '{"$token": {"userId": "test-user-id","scope": "web:mobile", "expires":1760704968038}}'; + return http.Response(response, 200); + } + return http.Response('', 404); + }); + }), + ], + userSession: const AuthSessionState( + token: 'test-token', + user: LightUser(id: UserId('test-user-id'), name: 'test-username'), + ), + ); + + fakeAsync((async) { + final session = container.read(authSessionProvider); + expect(session, isNotNull); + + final client = container.read(lichessClientProvider); + try { + client.get(Uri(path: '/will/return/401')); + } on ServerException catch (_) {} + + async.flushMicrotasks(); + + final requests = FakeClient.verifyRequests(); + expect(requests.length, 1); expect( - requests.last, - isA() - .having( - (r) => r.url.host, - 'host', - 'lichess.dev', - ) - .having( - (r) => r.url.path, - 'path', - '/api/token/test', - ) - .having( - (r) => r.headers['Authorization'], - 'Authorization', - null, - ), + requests.first, + isA().having( + (r) => r.headers['Authorization'], + 'Authorization', + 'Bearer ${signBearerToken('test-token')}', + ), ); - expect(container.read(authSessionProvider), isNull); + expect(nbTokenTestRequests, 1); + + expect(container.read(authSessionProvider), equals(session)); }); }); }); @@ -359,10 +410,14 @@ class FakeClient extends http.BaseClient { @override Future send(http.BaseRequest request) { _requests.add(request); - return Future.value(_responseBasedOnPath(request)); + + return _responseBasedOnPath(request, request.finalize()); } - http.StreamedResponse _responseBasedOnPath(http.BaseRequest request) { + Future _responseBasedOnPath( + http.BaseRequest request, + http.ByteStream bodyStream, + ) async { switch (request.url.path) { case '/will/throw/socket/exception': throw const SocketException('no internet'); @@ -384,14 +439,6 @@ class FakeClient extends http.BaseClient { return http.StreamedResponse(_streamBody('204'), 204); case '/will/return/301': return http.StreamedResponse(_streamBody('301'), 301); - case '/api/token/test': - final token = request.headers['Authorization']; - final response = ''' - { - "$token": null - } -'''; - return http.StreamedResponse(_streamBody(response), 200); default: return http.StreamedResponse(_streamBody('200'), 200); } From 303414d3d9529294468a159f3d704c1601e58e4a Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 18 Oct 2024 10:20:35 +0200 Subject: [PATCH 501/979] Tweak slidable drag start behavior --- lib/src/view/play/challenge_list_item.dart | 2 ++ lib/src/view/puzzle/puzzle_tab_screen.dart | 2 ++ lib/src/view/relation/following_screen.dart | 4 +++- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/src/view/play/challenge_list_item.dart b/lib/src/view/play/challenge_list_item.dart index 3bcbaa5293..955f434d39 100644 --- a/lib/src/view/play/challenge_list_item.dart +++ b/lib/src/view/play/challenge_list_item.dart @@ -1,6 +1,7 @@ import 'dart:math'; import 'package:dartchess/dartchess.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; @@ -67,6 +68,7 @@ class ChallengeListItem extends ConsumerWidget { return Container( color: color, child: Slidable( + dragStartBehavior: DragStartBehavior.start, endActionPane: ActionPane( motion: const StretchMotion(), extentRatio: 0.6, diff --git a/lib/src/view/puzzle/puzzle_tab_screen.dart b/lib/src/view/puzzle/puzzle_tab_screen.dart index b844322967..7666ae50d1 100644 --- a/lib/src/view/puzzle/puzzle_tab_screen.dart +++ b/lib/src/view/puzzle/puzzle_tab_screen.dart @@ -1,6 +1,7 @@ import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/cupertino.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; @@ -777,6 +778,7 @@ class PuzzleAnglePreview extends ConsumerWidget { ), ) : Slidable( + dragStartBehavior: DragStartBehavior.start, enabled: angle != const PuzzleTheme(PuzzleThemeKey.mix), endActionPane: ActionPane( motion: const StretchMotion(), diff --git a/lib/src/view/relation/following_screen.dart b/lib/src/view/relation/following_screen.dart index b46870a346..b27b7ad5f3 100644 --- a/lib/src/view/relation/following_screen.dart +++ b/lib/src/view/relation/following_screen.dart @@ -1,5 +1,6 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/cupertino.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; @@ -70,8 +71,9 @@ class _Body extends ConsumerWidget { itemBuilder: (context, index) { final user = following[index]; return Slidable( + dragStartBehavior: DragStartBehavior.start, endActionPane: ActionPane( - motion: const ScrollMotion(), + motion: const StretchMotion(), extentRatio: 0.3, children: [ SlidableAction( From 00b5b872c02e2ec42eb22624c1c16e7483b12e88 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 18 Oct 2024 10:50:01 +0200 Subject: [PATCH 502/979] Fix a crash with missing opening data on puzzle tab --- lib/src/model/puzzle/puzzle_opening.dart | 38 +++++++-------------- lib/src/model/puzzle/puzzle_repository.dart | 8 +++-- lib/src/view/puzzle/puzzle_tab_screen.dart | 12 ++++++- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/lib/src/model/puzzle/puzzle_opening.dart b/lib/src/model/puzzle/puzzle_opening.dart index ed03974aa9..3e77522141 100644 --- a/lib/src/model/puzzle/puzzle_opening.dart +++ b/lib/src/model/puzzle/puzzle_opening.dart @@ -1,46 +1,34 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_providers.dart'; -import 'package:lichess_mobile/src/utils/riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -part 'puzzle_opening.freezed.dart'; part 'puzzle_opening.g.dart'; -@freezed -class PuzzleOpeningFamily with _$PuzzleOpeningFamily { - const factory PuzzleOpeningFamily({ - required String key, - required String name, - required int count, - required IList openings, - }) = _PuzzleOpeningFamily; -} +typedef PuzzleOpeningFamily = ({ + String key, + String name, + int count, + IList openings, +}); -@freezed -class PuzzleOpeningData with _$PuzzleOpeningData { - const factory PuzzleOpeningData({ - required String key, - required String name, - required int count, - }) = _PuzzleOpeningData; -} +typedef PuzzleOpeningData = ({ + String key, + String name, + int count, +}); /// Returns a flattened list of openings with their respective counts. -/// -/// The list is cached for 1 day. @riverpod Future> flatOpeningsList( FlatOpeningsListRef ref, ) async { - ref.cacheFor(const Duration(days: 1)); final families = await ref.watch(puzzleOpeningsProvider.future); return families .map( (f) => [ - PuzzleOpeningData(key: f.key, name: f.name, count: f.count), + (key: f.key, name: f.name, count: f.count), ...f.openings.map( - (o) => PuzzleOpeningData( + (o) => ( key: o.key, name: '${f.name}: ${o.name}', count: o.count, diff --git a/lib/src/model/puzzle/puzzle_repository.dart b/lib/src/model/puzzle/puzzle_repository.dart index cd47368e86..19929d9d1a 100644 --- a/lib/src/model/puzzle/puzzle_repository.dart +++ b/lib/src/model/puzzle/puzzle_repository.dart @@ -475,18 +475,20 @@ IList _puzzleOpeningFromPick(RequiredPick pick) { return pick('openings').asListOrThrow((openingPick) { final familyPick = openingPick('family'); final openings = openingPick('openings').asListOrNull( - (openPick) => PuzzleOpeningData( + (openPick) => ( key: openPick('key').asStringOrThrow(), name: openPick('name').asStringOrThrow(), count: openPick('count').asIntOrThrow(), ), ); - return PuzzleOpeningFamily( + return ( key: familyPick('key').asStringOrThrow(), name: familyPick('name').asStringOrThrow(), count: familyPick('count').asIntOrThrow(), - openings: openings != null ? openings.toIList() : IList(const []), + openings: openings != null + ? openings.toIList() + : IList(const []), ); }).toIList(); } diff --git a/lib/src/view/puzzle/puzzle_tab_screen.dart b/lib/src/view/puzzle/puzzle_tab_screen.dart index 7666ae50d1..1fb80bbc4c 100644 --- a/lib/src/view/puzzle/puzzle_tab_screen.dart +++ b/lib/src/view/puzzle/puzzle_tab_screen.dart @@ -838,7 +838,17 @@ class PuzzleAnglePreview extends ConsumerWidget { PuzzleOpening(key: final openingKey) => [ Text( flatOpenings.valueOrNull - ?.firstWhere((o) => o.key == openingKey) + ?.firstWhere( + (o) => o.key == openingKey, + orElse: () => ( + key: openingKey, + name: openingKey.replaceAll( + '_', + '', + ), + count: 0 + ), + ) .name ?? openingKey.replaceAll('_', ' '), style: Styles.boardPreviewTitle, From 33904fcfb0daa944159bc7be1013cfa0a1db7bf9 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 18 Oct 2024 12:20:43 +0200 Subject: [PATCH 503/979] Fix loading widgets in home This fixes a crash reported where ongoing game provider was disposed when still being in a loading state. --- lib/src/network/connectivity.dart | 2 + lib/src/view/home/home_tab_screen.dart | 524 +++++++++++++------------ 2 files changed, 266 insertions(+), 260 deletions(-) diff --git a/lib/src/network/connectivity.dart b/lib/src/network/connectivity.dart index 5e6d4c1a15..955d95649d 100644 --- a/lib/src/network/connectivity.dart +++ b/lib/src/network/connectivity.dart @@ -173,6 +173,7 @@ extension AsyncValueConnectivity on AsyncValue { required R Function() offline, }) { return maybeWhen( + skipLoadingOnReload: true, data: (status) => status.isOnline ? online() : offline(), orElse: offline, ); @@ -199,6 +200,7 @@ extension AsyncValueConnectivity on AsyncValue { required R Function() loading, }) { return when( + skipLoadingOnReload: true, data: (status) => status.isOnline ? online() : offline(), loading: loading, error: (error, stack) => offline(), diff --git a/lib/src/view/home/home_tab_screen.dart b/lib/src/view/home/home_tab_screen.dart index 8a1a3cdb07..9719db0f1d 100644 --- a/lib/src/view/home/home_tab_screen.dart +++ b/lib/src/view/home/home_tab_screen.dart @@ -34,7 +34,6 @@ import 'package:lichess_mobile/src/widgets/board_carousel_item.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/misc.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/user_full_name.dart'; import 'package:timeago/timeago.dart' as timeago; import 'package:url_launcher/url_launcher.dart'; @@ -63,167 +62,26 @@ class _HomeScreenState extends ConsumerState with RouteAware { if (!hasRefreshed && !wasOnline && isNowOnline) { hasRefreshed = true; - _refreshData(); + _refreshData(isOnline: isNowOnline); } wasOnline = isNowOnline; } }); - return PlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, - ); - } - - Widget _androidBuilder(BuildContext context) { - final isTablet = isTabletOrLarger(context); - final isEditing = ref.watch(editModeProvider); - return Scaffold( - appBar: AppBar( - title: const Text('lichess.org'), - actions: [ - IconButton( - onPressed: () { - ref.read(editModeProvider.notifier).state = !isEditing; - }, - icon: - Icon(isEditing ? Icons.save_outlined : Icons.app_registration), - tooltip: isEditing ? 'Save' : 'Edit', - ), - const _ChallengeScreenButton(), - const _PlayerScreenButton(), - ], - ), - body: RefreshIndicator( - key: _androidRefreshKey, - onRefresh: () => _refreshData(), - child: const Column( - children: [ - ConnectivityBanner(), - Expanded(child: _HomeBody()), - ], - ), - ), - floatingActionButton: isTablet - ? null - : FloatingActionButton.extended( - onPressed: () { - pushPlatformRoute( - context, - builder: (_) => const PlayScreen(), - ); - }, - icon: const Icon(Icons.add), - label: Text(context.l10n.play), - ), - ); - } - - Widget _iosBuilder(BuildContext context) { - final isEditing = ref.watch(editModeProvider); - return CupertinoPageScaffold( - child: Stack( - alignment: Alignment.bottomCenter, - children: [ - CustomScrollView( - controller: homeScrollController, - slivers: [ - CupertinoSliverNavigationBar( - padding: const EdgeInsetsDirectional.only( - start: 16.0, - end: 8.0, - ), - largeTitle: Text(context.l10n.mobileHomeTab), - leading: CupertinoButton( - alignment: Alignment.centerLeft, - padding: EdgeInsets.zero, - onPressed: () { - ref.read(editModeProvider.notifier).state = !isEditing; - }, - child: Text(isEditing ? 'Done' : 'Edit'), - ), - trailing: const Row( - mainAxisSize: MainAxisSize.min, - children: [ - _ChallengeScreenButton(), - _PlayerScreenButton(), - ], - ), - ), - CupertinoSliverRefreshControl( - onRefresh: () => _refreshData(), - ), - const SliverToBoxAdapter(child: ConnectivityBanner()), - const SliverSafeArea(top: false, sliver: _HomeBody()), - ], - ), - if (getScreenType(context) == ScreenType.handset) - Positioned( - bottom: MediaQuery.paddingOf(context).bottom + 16.0, - right: 8.0, - child: FloatingActionButton.extended( - backgroundColor: CupertinoTheme.of(context).primaryColor, - foregroundColor: - CupertinoTheme.of(context).primaryContrastingColor, - onPressed: () { - pushPlatformRoute( - context, - title: context.l10n.play, - builder: (_) => const PlayScreen(), - ); - }, - icon: const Icon(Icons.add), - label: Text(context.l10n.play), - ), - ), - ], - ), - ); - } - - Future _refreshData() { - return Future.wait([ - ref.refresh(accountProvider.future), - ref.refresh(myRecentGamesProvider.future), - ref.refresh(ongoingGamesProvider.future), - ]); - } -} - -class _SignInWidget extends ConsumerWidget { - const _SignInWidget(); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final authController = ref.watch(authControllerProvider); - - return SecondaryButton( - semanticsLabel: context.l10n.signIn, - onPressed: authController.isLoading - ? null - : () => ref.read(authControllerProvider.notifier).signIn(), - child: Text(context.l10n.signIn), - ); - } -} - -class _HomeBody extends ConsumerWidget { - const _HomeBody(); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isOnlineAsync = ref.watch(connectivityChangesProvider); + final connectivity = ref.watch(connectivityChangesProvider); final isEditing = ref.watch(editModeProvider); - return isOnlineAsync.when( + return connectivity.when( + skipLoadingOnReload: true, data: (status) { final session = ref.watch(authSessionProvider); - final isTablet = isTabletOrLarger(context); + final ongoingGames = ref.watch(ongoingGamesProvider); final emptyRecent = ref.watch(myRecentGamesProvider).maybeWhen( data: (data) => data.isEmpty, orElse: () => false, ); + final isTablet = isTabletOrLarger(context); // Show the welcome screen if there are no recent games and no stored games // (i.e. first installation, or the user has never played a game) @@ -232,133 +90,284 @@ class _HomeBody extends ConsumerWidget { session: session, status: status, isTablet: isTablet, - isEditing: isEditing, ); } final widgets = isTablet - ? [ - _EditableWidget( - widget: EnabledWidget.hello, - isEditing: isEditing, - shouldShow: true, - child: const _HelloWidget(), - ), - _EditableWidget( - widget: EnabledWidget.perfCards, - isEditing: isEditing, - shouldShow: session != null, - child: const AccountPerfCards( - padding: Styles.bodySectionPadding, - ), - ), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Flexible( - child: Column( + ? _tabletWidgets( + session: session, + status: status, + ongoingGames: ongoingGames, + ) + : _handsetWidgets( + session: session, + status: status, + ongoingGames: ongoingGames, + ); + + if (Theme.of(context).platform == TargetPlatform.iOS) { + return CupertinoPageScaffold( + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + CustomScrollView( + controller: homeScrollController, + slivers: [ + CupertinoSliverNavigationBar( + padding: const EdgeInsetsDirectional.only( + start: 16.0, + end: 8.0, + ), + largeTitle: Text(context.l10n.mobileHomeTab), + leading: CupertinoButton( + alignment: Alignment.centerLeft, + padding: EdgeInsets.zero, + onPressed: () { + ref.read(editModeProvider.notifier).state = + !isEditing; + }, + child: Text(isEditing ? 'Done' : 'Edit'), + ), + trailing: const Row( + mainAxisSize: MainAxisSize.min, children: [ - const SizedBox(height: 8.0), - if (status.isOnline) - _TabletCreateAGameSection(isEditing: isEditing), - if (status.isOnline) - const _OngoingGamesPreview(maxGamesToShow: 5) - else - const _OfflineCorrespondencePreview( - maxGamesToShow: 5, - ), + _ChallengeScreenButton(), + _PlayerScreenButton(), ], ), ), - const Flexible( - child: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - SizedBox(height: 8.0), - RecentGamesWidget(), - ], + CupertinoSliverRefreshControl( + onRefresh: () => _refreshData(isOnline: status.isOnline), + ), + const SliverToBoxAdapter(child: ConnectivityBanner()), + SliverSafeArea( + top: false, + sliver: SliverList( + delegate: SliverChildListDelegate(widgets), ), ), ], ), - ] - : [ - _EditableWidget( - widget: EnabledWidget.hello, - isEditing: isEditing, - shouldShow: true, - child: const _HelloWidget(), - ), - _EditableWidget( - widget: EnabledWidget.perfCards, - isEditing: isEditing, - shouldShow: session != null, - child: const AccountPerfCards( - padding: Styles.horizontalBodyPadding, + if (getScreenType(context) == ScreenType.handset) + Positioned( + bottom: MediaQuery.paddingOf(context).bottom + 16.0, + right: 8.0, + child: FloatingActionButton.extended( + backgroundColor: CupertinoTheme.of(context).primaryColor, + foregroundColor: + CupertinoTheme.of(context).primaryContrastingColor, + onPressed: () { + pushPlatformRoute( + context, + title: context.l10n.play, + builder: (_) => const PlayScreen(), + ); + }, + icon: const Icon(Icons.add), + label: Text(context.l10n.play), + ), ), - ), - _EditableWidget( - widget: EnabledWidget.quickPairing, - isEditing: isEditing, - shouldShow: status.isOnline, - child: const Padding( - padding: Styles.bodySectionPadding, - child: QuickGameMatrix(), + ], + ), + ); + } else { + return Scaffold( + appBar: AppBar( + title: const Text('lichess.org'), + actions: [ + IconButton( + onPressed: () { + ref.read(editModeProvider.notifier).state = !isEditing; + }, + icon: Icon( + isEditing ? Icons.save_outlined : Icons.app_registration, ), + tooltip: isEditing ? 'Save' : 'Edit', ), + const _ChallengeScreenButton(), + const _PlayerScreenButton(), + ], + ), + body: RefreshIndicator( + key: _androidRefreshKey, + onRefresh: () => _refreshData(isOnline: status.isOnline), + child: Column( + children: [ + const ConnectivityBanner(), + Expanded( + child: ListView( + controller: homeScrollController, + children: widgets, + ), + ), + ], + ), + ), + floatingActionButton: isTablet + ? null + : FloatingActionButton.extended( + onPressed: () { + pushPlatformRoute( + context, + builder: (_) => const PlayScreen(), + ); + }, + icon: const Icon(Icons.add), + label: Text(context.l10n.play), + ), + ); + } + }, + error: (_, __) => const CenterLoadingIndicator(), + loading: () => const CenterLoadingIndicator(), + ); + } + + List _handsetWidgets({ + required AuthSessionState? session, + required ConnectivityStatus status, + required AsyncValue> ongoingGames, + }) { + return [ + const _EditableWidget( + widget: EnabledWidget.hello, + shouldShow: true, + child: _HelloWidget(), + ), + _EditableWidget( + widget: EnabledWidget.perfCards, + shouldShow: session != null, + child: const AccountPerfCards( + padding: Styles.horizontalBodyPadding, + ), + ), + _EditableWidget( + widget: EnabledWidget.quickPairing, + shouldShow: status.isOnline, + child: const Padding( + padding: Styles.bodySectionPadding, + child: QuickGameMatrix(), + ), + ), + if (status.isOnline) + _OngoingGamesCarousel(ongoingGames, maxGamesToShow: 20) + else + const _OfflineCorrespondenceCarousel(maxGamesToShow: 20), + const RecentGamesWidget(), + if (Theme.of(context).platform == TargetPlatform.iOS) + const SizedBox(height: 70.0) + else + const SizedBox(height: 54.0), + ]; + } + + List _tabletWidgets({ + required AuthSessionState? session, + required ConnectivityStatus status, + required AsyncValue> ongoingGames, + }) { + return [ + const _EditableWidget( + widget: EnabledWidget.hello, + shouldShow: true, + child: _HelloWidget(), + ), + _EditableWidget( + widget: EnabledWidget.perfCards, + shouldShow: session != null, + child: const AccountPerfCards( + padding: Styles.bodySectionPadding, + ), + ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Flexible( + child: Column( + children: [ + const SizedBox(height: 8.0), + if (status.isOnline) const _TabletCreateAGameSection(), if (status.isOnline) - const _OngoingGamesCarousel(maxGamesToShow: 20) - else - const _OfflineCorrespondenceCarousel(maxGamesToShow: 20), - const RecentGamesWidget(), - if (Theme.of(context).platform == TargetPlatform.iOS) - const SizedBox(height: 70.0) + _OngoingGamesPreview( + ongoingGames, + maxGamesToShow: 5, + ) else - const SizedBox(height: 54.0), - ]; + const _OfflineCorrespondencePreview( + maxGamesToShow: 5, + ), + ], + ), + ), + const Flexible( + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox(height: 8.0), + RecentGamesWidget(), + ], + ), + ), + ], + ), + ]; + } - return Theme.of(context).platform == TargetPlatform.android - ? ListView( - controller: homeScrollController, - children: widgets, - ) - : SliverList( - delegate: SliverChildListDelegate(widgets), - ); - }, - loading: () { - const child = CenterLoadingIndicator(); - return Theme.of(context).platform == TargetPlatform.android - ? child - : const SliverFillRemaining(child: child); - }, - error: (error, stack) { - const child = SizedBox.shrink(); - return Theme.of(context).platform == TargetPlatform.android - ? child - : const SliverFillRemaining(child: child); - }, + Future _refreshData({required bool isOnline}) { + return Future.wait([ + ref.refresh(myRecentGamesProvider.future), + if (isOnline) ref.refresh(accountProvider.future), + if (isOnline) ref.refresh(ongoingGamesProvider.future), + ]); + } +} + +class _SignInWidget extends ConsumerWidget { + const _SignInWidget(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final authController = ref.watch(authControllerProvider); + + return SecondaryButton( + semanticsLabel: context.l10n.signIn, + onPressed: authController.isLoading + ? null + : () => ref.read(authControllerProvider.notifier).signIn(), + child: Text(context.l10n.signIn), ); } } +/// A widget that can be enabled or disabled by the user. +/// +/// This widget is used to show or hide certain sections of the home screen. +/// +/// The [homePreferencesProvider] provides a list of enabled widgets. +/// +/// * The [widget] parameter is the widget that can be enabled or disabled. +/// +/// * The [shouldShow] parameter is useful when the widget should be shown only +/// when certain conditions are met. For example, we only want to show the quick +/// pairing matrix when the user is online. +/// This parameter is only active when the user is not in edit mode, as we +/// always want to display the widget in edit mode. class _EditableWidget extends ConsumerWidget { const _EditableWidget({ required this.child, required this.widget, - required this.isEditing, required this.shouldShow, }); final Widget child; final EnabledWidget widget; - final bool isEditing; final bool shouldShow; @override Widget build(BuildContext context, WidgetRef ref) { final enabledWidgets = ref.watch(homePreferencesProvider).enabledWidgets; + final isEditing = ref.watch(editModeProvider); final isEnabled = enabledWidgets.contains(widget); if (!shouldShow) { @@ -399,13 +408,11 @@ class _WelcomeScreen extends StatelessWidget { required this.session, required this.status, required this.isTablet, - required this.isEditing, }); final AuthSessionState? session; final ConnectivityStatus status; final bool isTablet; - final bool isEditing; @override Widget build(BuildContext context) { @@ -453,8 +460,8 @@ class _WelcomeScreen extends StatelessWidget { Row( children: [ if (status.isOnline) - Flexible( - child: _TabletCreateAGameSection(isEditing: isEditing), + const Flexible( + child: _TabletCreateAGameSection(), ), Flexible( child: Column( @@ -465,11 +472,10 @@ class _WelcomeScreen extends StatelessWidget { ) else ...[ if (status.isOnline) - _EditableWidget( + const _EditableWidget( widget: EnabledWidget.quickPairing, - isEditing: isEditing, shouldShow: true, - child: const Padding( + child: Padding( padding: Styles.bodySectionPadding, child: QuickGameMatrix(), ), @@ -558,43 +564,41 @@ class _HelloWidget extends ConsumerWidget { } class _TabletCreateAGameSection extends StatelessWidget { - const _TabletCreateAGameSection({required this.isEditing}); - - final bool isEditing; + const _TabletCreateAGameSection(); @override Widget build(BuildContext context) { - return Column( + return const Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ _EditableWidget( widget: EnabledWidget.quickPairing, - isEditing: isEditing, shouldShow: true, - child: const Padding( + child: Padding( padding: Styles.bodySectionPadding, child: QuickGameMatrix(), ), ), - const Padding( + Padding( padding: Styles.bodySectionPadding, child: QuickGameButton(), ), - const CreateGameOptions(), + CreateGameOptions(), ], ); } } class _OngoingGamesCarousel extends ConsumerWidget { - const _OngoingGamesCarousel({required this.maxGamesToShow}); + const _OngoingGamesCarousel(this.games, {required this.maxGamesToShow}); + + final AsyncValue> games; final int maxGamesToShow; @override Widget build(BuildContext context, WidgetRef ref) { - final ongoingGames = ref.watch(ongoingGamesProvider); - return ongoingGames.maybeWhen( + return games.maybeWhen( data: (data) { if (data.isEmpty) { return const SizedBox.shrink(); @@ -823,14 +827,14 @@ class _GamePreviewCarouselItem extends StatelessWidget { } class _OngoingGamesPreview extends ConsumerWidget { - const _OngoingGamesPreview({required this.maxGamesToShow}); + const _OngoingGamesPreview(this.games, {required this.maxGamesToShow}); + final AsyncValue> games; final int maxGamesToShow; @override Widget build(BuildContext context, WidgetRef ref) { - final ongoingGames = ref.watch(ongoingGamesProvider); - return ongoingGames.maybeWhen( + return games.maybeWhen( data: (data) { return PreviewGameList( list: data, From 22a088a831f5e83ba3f7d5c4a533c85f54a6d674 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Sat, 19 Oct 2024 11:11:08 +0200 Subject: [PATCH 504/979] Test if variant is chess960 before generating moves --- lib/src/model/analysis/analysis_controller.dart | 6 ++++-- lib/src/model/game/game_controller.dart | 10 ++++++++++ .../over_the_board/over_the_board_game_controller.dart | 5 +++++ .../offline_correspondence_game_screen.dart | 5 ++++- lib/src/view/game/game_body.dart | 4 ++-- lib/src/view/over_the_board/over_the_board_screen.dart | 2 +- 6 files changed, 26 insertions(+), 6 deletions(-) diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index b8bdc6255c..c337b6dc77 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -715,8 +715,10 @@ class AnalysisState with _$AnalysisState { /// Whether the analysis is for a lichess game. bool get isLichessGameAnalysis => gameAnyId != null; - IMap> get validMoves => - makeLegalMoves(currentNode.position); + IMap> get validMoves => makeLegalMoves( + currentNode.position, + isChess960: variant == Variant.chess960, + ); /// Whether the user can request server analysis. /// diff --git a/lib/src/model/game/game_controller.dart b/lib/src/model/game/game_controller.dart index 055c503d99..aa1f73705f 100644 --- a/lib/src/model/game/game_controller.dart +++ b/lib/src/model/game/game_controller.dart @@ -1010,6 +1010,16 @@ class GameState with _$GameState { GameFullId? redirectGameId, }) = _GameState; + /// The [Position] and its legal moves at the current cursor. + (Position, IMap>) get currentPosition { + final position = game.positionAt(stepCursor); + final legalMoves = makeLegalMoves( + position, + isChess960: game.meta.variant == Variant.chess960, + ); + return (position, legalMoves); + } + /// Whether the zen mode is active bool get isZenModeActive => game.playable ? isZenModeEnabled : game.prefs?.zenMode == Zen.yes; diff --git a/lib/src/model/over_the_board/over_the_board_game_controller.dart b/lib/src/model/over_the_board/over_the_board_game_controller.dart index 5f333caa8f..8f7741059e 100644 --- a/lib/src/model/over_the_board/over_the_board_game_controller.dart +++ b/lib/src/model/over_the_board/over_the_board_game_controller.dart @@ -173,6 +173,11 @@ class OverTheBoardGameState with _$OverTheBoardGameState { ? NormalMove.fromUci(game.steps[stepCursor].sanMove!.move.uci) : null; + IMap> get legalMoves => makeLegalMoves( + currentPosition, + isChess960: game.meta.variant == Variant.chess960, + ); + MaterialDiffSide? currentMaterialDiff(Side side) { return game.steps[stepCursor].diff?.bySide(side); } diff --git a/lib/src/view/correspondence/offline_correspondence_game_screen.dart b/lib/src/view/correspondence/offline_correspondence_game_screen.dart index bc0477fd10..e2f181ebd3 100644 --- a/lib/src/view/correspondence/offline_correspondence_game_screen.dart +++ b/lib/src/view/correspondence/offline_correspondence_game_screen.dart @@ -219,7 +219,10 @@ class _BodyState extends ConsumerState<_Body> { : PlayerSide.none, isCheck: position.isCheck, sideToMove: sideToMove, - validMoves: makeLegalMoves(position), + validMoves: makeLegalMoves( + position, + isChess960: game.variant == Variant.chess960, + ), promotionMove: promotionMove, onMove: (move, {isDrop, captured}) { onUserMove(move); diff --git a/lib/src/view/game/game_body.dart b/lib/src/view/game/game_body.dart index ad27bb0333..af8b9fae60 100644 --- a/lib/src/view/game/game_body.dart +++ b/lib/src/view/game/game_body.dart @@ -119,7 +119,7 @@ class GameBody extends ConsumerWidget { return gameStateAsync.when( data: (gameState) { - final position = gameState.game.positionAt(gameState.stepCursor); + final (position, legalMoves) = gameState.currentPosition; final youAre = gameState.game.youAre ?? Side.white; final archivedBlackClock = gameState.game.archivedBlackClockAt(gameState.stepCursor); @@ -253,7 +253,7 @@ class GameBody extends ConsumerWidget { isCheck: boardPreferences.boardHighlights && position.isCheck, sideToMove: position.turn, - validMoves: makeLegalMoves(position), + validMoves: legalMoves, promotionMove: gameState.promotionMove, onMove: (move, {isDrop}) { ref.read(ctrlProvider.notifier).userMove( diff --git a/lib/src/view/over_the_board/over_the_board_screen.dart b/lib/src/view/over_the_board/over_the_board_screen.dart index 384091c939..c1a7ecaaec 100644 --- a/lib/src/view/over_the_board/over_the_board_screen.dart +++ b/lib/src/view/over_the_board/over_the_board_screen.dart @@ -143,7 +143,7 @@ class _BodyState extends ConsumerState<_Body> { ? PlayerSide.white : PlayerSide.black, sideToMove: gameState.turn, - validMoves: makeLegalMoves(gameState.currentPosition), + validMoves: gameState.legalMoves, onPromotionSelection: ref .read(overTheBoardGameControllerProvider.notifier) .onPromotionSelection, From 4c2a1c980f54d30b1fbad64ac6a16ad77d08b3dc Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Sat, 19 Oct 2024 15:17:21 +0200 Subject: [PATCH 505/979] Upgrade dependencies --- ios/Podfile.lock | 8 +++--- pubspec.lock | 64 ++++++++++++++++++++++++------------------------ 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index bba98bc619..313ff3a5b1 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -229,9 +229,9 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: app_settings: 017320c6a680cdc94c799949d95b84cb69389ebc AppAuth: 501c04eda8a8d11f179dbe8637b7a91bb7e5d2fa - connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db + connectivity_plus: 4c41c08fc6d7c91f63bc7aec70ffe3730b04f563 cupertino_http: 1a3a0f163c1b26e7f1a293b33d476e0fde7a64ec - device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d + device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342 Firebase: 98e6bf5278170668a7983e12971a66b2cd57fc8c firebase_core: 2bedc3136ec7c7b8561c6123ed0239387b53f2af firebase_crashlytics: 37d104d457b51760b48504a93a12b3bf70995d77 @@ -252,11 +252,11 @@ SPEC CHECKSUMS: GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 - package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c + package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 - share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad + share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 sound_effect: 5280cfa89d4a576032186f15600dc948ca6d39ce sqflite_darwin: a553b1fd6fe66f53bbb0fe5b4f5bab93f08d7a13 diff --git a/pubspec.lock b/pubspec.lock index 825baadc5c..da527005a1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -242,10 +242,10 @@ packages: dependency: "direct main" description: name: connectivity_plus - sha256: "2056db5241f96cdc0126bd94459fc4cdc13876753768fc7a31c425e50a7177d0" + sha256: "876849631b0c7dc20f8b471a2a03142841b482438e3b707955464f5ffca3e4c3" url: "https://pub.dev" source: hosted - version: "6.0.5" + version: "6.1.0" connectivity_plus_platform_interface: dependency: transitive description: @@ -258,10 +258,10 @@ packages: dependency: transitive description: name: convert - sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" cronet_http: dependency: "direct main" description: @@ -282,10 +282,10 @@ packages: dependency: "direct main" description: name: crypto - sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27 + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.6" csslib: dependency: transitive description: @@ -346,10 +346,10 @@ packages: dependency: "direct main" description: name: dartchess - sha256: "9ce59d164b5ddf87fdb3cbe118015674f2f241e33b0891cec1e9f30b68a81b26" + sha256: "6ddc173288cb63444edf0d766986c283bb1ae6d8ede2ac1fc9de857f0f84d051" url: "https://pub.dev" source: hosted - version: "0.9.0" + version: "0.9.2" dbus: dependency: transitive description: @@ -370,10 +370,10 @@ packages: dependency: "direct main" description: name: device_info_plus - sha256: db03b2d2a3fa466a4627709e1db58692c3f7f658e36a5942d342d86efedc4091 + sha256: c4af09051b4f0508f6c1dc0a5c085bf014d5c9a4a0678ce1799c2b4d716387a0 url: "https://pub.dev" source: hosted - version: "11.0.0" + version: "11.1.0" device_info_plus_platform_interface: dependency: transitive description: @@ -498,10 +498,10 @@ packages: dependency: transitive description: name: fixnum - sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" fl_chart: dependency: "direct main" description: @@ -767,10 +767,10 @@ packages: dependency: transitive description: name: http_parser - sha256: "40f592dd352890c3b60fec1b68e786cefb9603e05ff303dbc4dda49b304ecdf4" + sha256: "76d306a1c3afb33fe82e2bbacad62a61f409b5634c915fceb0d799de1a913360" url: "https://pub.dev" source: hosted - version: "4.1.0" + version: "4.1.1" http_profile: dependency: transitive description: @@ -783,10 +783,10 @@ packages: dependency: transitive description: name: image - sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8" + sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d url: "https://pub.dev" source: hosted - version: "4.2.0" + version: "4.3.0" intl: dependency: "direct main" description: @@ -879,10 +879,10 @@ packages: dependency: "direct main" description: name: logging - sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" macros: dependency: transitive description: @@ -975,10 +975,10 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: "894f37107424311bdae3e476552229476777b8752c5a2a2369c0cb9a2d5442ef" + sha256: df3eb3e0aed5c1107bb0fdb80a8e82e778114958b1c5ac5644fb1ac9cae8a998 url: "https://pub.dev" source: hosted - version: "8.0.3" + version: "8.1.0" package_info_plus_platform_interface: dependency: transitive description: @@ -1063,10 +1063,10 @@ packages: dependency: transitive description: name: platform - sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" url: "https://pub.dev" source: hosted - version: "3.1.5" + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -1175,10 +1175,10 @@ packages: dependency: "direct main" description: name: share_plus - sha256: fec12c3c39f01e4df1ec6ad92b6e85503c5ca64ffd6e28d18c9ffe53fcc4cb11 + sha256: "334fcdf0ef9c0df0e3b428faebcac9568f35c747d59831474b2fc56e156d244e" url: "https://pub.dev" source: hosted - version: "10.0.3" + version: "10.1.0" share_plus_platform_interface: dependency: transitive description: @@ -1340,10 +1340,10 @@ packages: dependency: "direct dev" description: name: sqflite_common_ffi - sha256: a6057d4c87e9260ba1ec436ebac24760a110589b9c0a859e128842eb69a7ef04 + sha256: d316908f1537725427ff2827a5c5f3b2c1bc311caed985fe3c9b10939c9e11ca url: "https://pub.dev" source: hosted - version: "2.3.3+1" + version: "2.3.4" sqflite_darwin: dependency: transitive description: @@ -1469,10 +1469,10 @@ packages: dependency: transitive description: name: typed_data - sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.4.0" universal_io: dependency: transitive description: @@ -1541,10 +1541,10 @@ packages: dependency: transitive description: name: url_launcher_windows - sha256: "49c10f879746271804767cb45551ec5592cdab00ee105c06dddde1a98f73b185" + sha256: "44cf3aabcedde30f2dba119a9dea3b0f2672fbe6fa96e85536251d678216b3c4" url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.3" uuid: dependency: transitive description: @@ -1653,10 +1653,10 @@ packages: dependency: transitive description: name: win32 - sha256: e5c39a90447e7c81cfec14b041cdbd0d0916bd9ebbc7fe02ab69568be703b9bd + sha256: "2294c64768987ea280b43a3d8357d42d5679f3e2b5b69b602be45b2abbd165b0" url: "https://pub.dev" source: hosted - version: "5.6.0" + version: "5.6.1" win32_registry: dependency: transitive description: From 84d36f44e83100905467bc959c680b064fde789b Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Sat, 19 Oct 2024 15:21:10 +0200 Subject: [PATCH 506/979] Update with new dartchess --- .../board_editor/board_editor_controller.dart | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/src/model/board_editor/board_editor_controller.dart b/lib/src/model/board_editor/board_editor_controller.dart index f0459c4c2d..5bcf1eb67a 100644 --- a/lib/src/model/board_editor/board_editor_controller.dart +++ b/lib/src/model/board_editor/board_editor_controller.dart @@ -15,7 +15,7 @@ class BoardEditorController extends _$BoardEditorController { orientation: Side.white, sideToPlay: Side.white, pieces: readFen(initialFen ?? kInitialFEN).lock, - unmovedRooks: SquareSet.corners, + castlingRights: SquareSet.corners, editorPointerMode: EditorPointerMode.drag, enPassantOptions: SquareSet.empty, enPassantSquare: null, @@ -142,9 +142,9 @@ class BoardEditorController extends _$BoardEditorController { void _setRookUnmoved(Square square, bool unmoved) { state = state.copyWith( - unmovedRooks: unmoved - ? state.unmovedRooks.withSquare(square) - : state.unmovedRooks.withoutSquare(square), + castlingRights: unmoved + ? state.castlingRights.withSquare(square) + : state.castlingRights.withoutSquare(square), ); } } @@ -157,7 +157,7 @@ class BoardEditorState with _$BoardEditorState { required Side orientation, required Side sideToPlay, required IMap pieces, - required SquareSet unmovedRooks, + required SquareSet castlingRights, required EditorPointerMode editorPointerMode, required SquareSet enPassantOptions, required Square? enPassantSquare, @@ -169,12 +169,12 @@ class BoardEditorState with _$BoardEditorState { bool isCastlingAllowed(Side side, CastlingSide castlingSide) => switch (side) { Side.white => switch (castlingSide) { - CastlingSide.king => unmovedRooks.has(Square.h1), - CastlingSide.queen => unmovedRooks.has(Square.a1), + CastlingSide.king => castlingRights.has(Square.h1), + CastlingSide.queen => castlingRights.has(Square.a1), }, Side.black => switch (castlingSide) { - CastlingSide.king => unmovedRooks.has(Square.h8), - CastlingSide.queen => unmovedRooks.has(Square.a8), + CastlingSide.king => castlingRights.has(Square.h8), + CastlingSide.queen => castlingRights.has(Square.a8), }, }; @@ -183,7 +183,7 @@ class BoardEditorState with _$BoardEditorState { final board = Board.parseFen(boardFen); return Setup( board: board, - unmovedRooks: unmovedRooks, + castlingRights: castlingRights, turn: sideToPlay == Side.white ? Side.white : Side.black, epSquare: enPassantSquare, halfmoves: 0, From 1dadbc2550f216c3f096a335017f9f8a47e3cfca Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Sun, 20 Oct 2024 16:15:21 +0200 Subject: [PATCH 507/979] tweak shimmer loading --- lib/src/widgets/list.dart | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/src/widgets/list.dart b/lib/src/widgets/list.dart index 69cba26fad..df310d851b 100644 --- a/lib/src/widgets/list.dart +++ b/lib/src/widgets/list.dart @@ -81,9 +81,11 @@ class ListSection extends StatelessWidget { child: Column( children: [ if (header != null) - // ignore: avoid-wrapping-in-padding Padding( - padding: const EdgeInsets.symmetric(vertical: 10.0), + padding: const EdgeInsets.symmetric( + vertical: 10.0, + horizontal: 16.0, + ), child: Container( width: double.infinity, height: 25, @@ -94,9 +96,11 @@ class ListSection extends StatelessWidget { ), ), for (int i = 0; i < children.length; i++) - // ignore: avoid-wrapping-in-padding Padding( - padding: const EdgeInsets.symmetric(vertical: 10.0), + padding: const EdgeInsets.symmetric( + vertical: 10.0, + horizontal: 16.0, + ), child: Container( width: double.infinity, height: 50, From c62052a521ed8d942fcf8fea86a846c14679c1a8 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Sun, 20 Oct 2024 18:22:57 +0200 Subject: [PATCH 508/979] fix broadcast live move --- .../model/broadcast/broadcast_game_clock.dart | 171 ++++++++++++++++++ lib/src/view/analysis/tree_view.dart | 5 + 2 files changed, 176 insertions(+) create mode 100644 lib/src/model/broadcast/broadcast_game_clock.dart diff --git a/lib/src/model/broadcast/broadcast_game_clock.dart b/lib/src/model/broadcast/broadcast_game_clock.dart new file mode 100644 index 0000000000..478385e988 --- /dev/null +++ b/lib/src/model/broadcast/broadcast_game_clock.dart @@ -0,0 +1,171 @@ +import 'dart:async'; + +import 'package:lichess_mobile/src/model/broadcast/broadcast_round_controller.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/network/socket.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +@riverpod +class BroadcastClockController extends _$BroadcastRoundController { + static Uri broadcastSocketUri(BroadcastRoundId broadcastRoundId) => + Uri(path: 'study/$broadcastRoundId/socket/v6'); + + StreamSubscription? _subscription; + + Timer? _timer; + + late SocketClient _socketClient; + + @override + Future build(BroadcastRoundId broadcastRoundId) async { + _socketClient = ref + .watch(socketPoolProvider) + .open(BroadcastClockController.broadcastSocketUri(broadcastRoundId)); + + _subscription = _socketClient.stream.listen(_handleSocketEvent); + + ref.onDispose(() { + _subscription?.cancel(); + _timer?.cancel(); + }); + + final games = + await ref.watch(broadcastRoundProvider(broadcastRoundId).future); + + _timer = Timer.periodic( + const Duration(seconds: 1), + (_) => _updateClocks(), + ); + + return games; + } + + void _updateClocks() { + state = AsyncData( + state.requireValue.map((gameId, game) { + if (!game.isPlaying) return MapEntry(gameId, game); + final thinkTime = game.thinkTime; + final newThinkTime = thinkTime + const Duration(seconds: 1); + return MapEntry(gameId, game.copyWith(thinkTime: newThinkTime)); + }), + ); + } + + void _handleSocketEvent(SocketEvent event) { + if (!state.hasValue) return; + + switch (event.topic) { + // Sent when a node is recevied from the broadcast + case 'addNode': + _handleAddNodeEvent(event); + // Sent when the state of games changes + case 'chapters': + _handleChaptersEvent(event); + // Sent when clocks are updated from the broadcast + case 'clock': + _handleClockEvent(event); + } + } + + void _handleAddNodeEvent(SocketEvent event) { + // The path of the last and current move of the broadcasted game + // Its value is "!" if the path is identical to one of the node that was received + final currentPath = pick(event.data, 'relayPath').asUciPathOrThrow(); + + // We check that the event we received is for the last move of the game + if (currentPath.value != '!') return; + + final broadcastGameId = + pick(event.data, 'p', 'chapterId').asBroadcastGameIdOrThrow(); + + final fen = pick(event.data, 'n', 'fen').asStringOrThrow(); + + final playingSide = Setup.parseFen(fen).turn; + + state = AsyncData( + state.requireValue.update( + broadcastGameId, + (broadcastGame) => broadcastGame.copyWith( + players: IMap( + { + playingSide: broadcastGame.players[playingSide]!, + playingSide.opposite: + broadcastGame.players[playingSide.opposite]!.copyWith( + clock: pick(event.data, 'n', 'clock') + .asDurationFromCentiSecondsOrNull(), + ), + }, + ), + fen: fen, + lastMove: pick(event.data, 'n', 'uci').asUciMoveOrThrow(), + thinkTime: Duration.zero, + ), + ), + ); + } + + void _handleChaptersEvent(SocketEvent event) { + final games = pick(event.data).asListOrThrow(gameFromPick); + state = AsyncData(IMap.fromEntries(games)); + } + + void _handleClockEvent(SocketEvent event) { + final broadcastGameId = + pick(event.data, 'p', 'chapterId').asBroadcastGameIdOrThrow(); + final whiteClock = pick(event.data, 'p', 'relayClocks', 0) + .asDurationFromCentiSecondsOrNull(); + final blackClock = pick(event.data, 'p', 'relayClocks', 1) + .asDurationFromCentiSecondsOrNull(); + state = AsyncData( + state.requireValue.update( + broadcastGameId, + (broadcastsGame) => broadcastsGame.copyWith( + players: IMap( + { + Side.white: broadcastsGame.players[Side.white]!.copyWith( + clock: whiteClock, + ), + Side.black: broadcastsGame.players[Side.black]!.copyWith( + clock: blackClock, + ), + }, + ), + ), + ), + ); + } +} + +@riverpod +Future roundClocks( + RoundClocksRef ref, BroadcastRoundId roundId) async { + final games = + await ref.watch(broadcastRoundControllerProvider(roundId).future); + + final _timer = Timer.periodic( + const Duration(seconds: 1), + (_) => _updateClocks(), + ); + + ref.onDispose(() { + _timer.cancel(); + }); + // get clock data from game controller + + // listen to socket to get clock update events + + // instantiate the local timer + + return clockData; +} + +void _updateClocks() { + state = AsyncData( + state.requireValue.map((gameId, game) { + if (!game.isPlaying) return MapEntry(gameId, game); + final thinkTime = game.thinkTime; + final newThinkTime = thinkTime + const Duration(seconds: 1); + return MapEntry(gameId, game.copyWith(thinkTime: newThinkTime)); + }), + ); +} diff --git a/lib/src/view/analysis/tree_view.dart b/lib/src/view/analysis/tree_view.dart index 5f011e1cad..4dd0b1b766 100644 --- a/lib/src/view/analysis/tree_view.dart +++ b/lib/src/view/analysis/tree_view.dart @@ -109,6 +109,11 @@ class _InlineTreeViewState extends ConsumerState { }); }); } + if (prev?.livePath != state.livePath) { + setState(() { + pathToLiveMove = state.livePath; + }); + } }, ); From b7c14934a9376d1c280ef08a9c57383a76c73c77 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Sun, 20 Oct 2024 19:13:19 +0200 Subject: [PATCH 509/979] tweak tree rebuilding with broadcast live moves --- lib/src/view/analysis/tree_view.dart | 41 +++++++++++++++------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/lib/src/view/analysis/tree_view.dart b/lib/src/view/analysis/tree_view.dart index 4dd0b1b766..74c6d3492e 100644 --- a/lib/src/view/analysis/tree_view.dart +++ b/lib/src/view/analysis/tree_view.dart @@ -89,29 +89,32 @@ class _InlineTreeViewState extends ConsumerState { ref.listen( analysisControllerProvider(widget.pgn, widget.options), (prev, state) { - if (prev?.currentPath != state.currentPath) { - // debouncing the current path change to avoid rebuilding when using - // the fast replay buttons + if (prev?.currentPath != state.currentPath || + prev?.livePath != state.livePath) { + // debouncing the current and live path change to avoid rebuilding when using + // the fast replay buttons or when receiving a lot of moves in a short time _debounce(() { setState(() { - pathToCurrentMove = state.currentPath; - }); - WidgetsBinding.instance.addPostFrameCallback((_) { - if (currentMoveKey.currentContext != null) { - Scrollable.ensureVisible( - currentMoveKey.currentContext!, - duration: const Duration(milliseconds: 200), - curve: Curves.easeIn, - alignment: 0.5, - alignmentPolicy: ScrollPositionAlignmentPolicy.explicit, - ); + if (prev?.currentPath != state.currentPath) { + pathToCurrentMove = state.currentPath; + } + if (prev?.livePath != state.livePath) { + pathToLiveMove = state.livePath; } }); - }); - } - if (prev?.livePath != state.livePath) { - setState(() { - pathToLiveMove = state.livePath; + if (prev?.currentPath != state.currentPath) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (currentMoveKey.currentContext != null) { + Scrollable.ensureVisible( + currentMoveKey.currentContext!, + duration: const Duration(milliseconds: 200), + curve: Curves.easeIn, + alignment: 0.5, + alignmentPolicy: ScrollPositionAlignmentPolicy.explicit, + ); + } + }); + } }); } }, From 1bf7445500f53d89f33097a4b031a7ea015fc3fa Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Sun, 20 Oct 2024 19:15:31 +0200 Subject: [PATCH 510/979] revert wip file added by mistake --- .../model/broadcast/broadcast_game_clock.dart | 171 ------------------ 1 file changed, 171 deletions(-) delete mode 100644 lib/src/model/broadcast/broadcast_game_clock.dart diff --git a/lib/src/model/broadcast/broadcast_game_clock.dart b/lib/src/model/broadcast/broadcast_game_clock.dart deleted file mode 100644 index 478385e988..0000000000 --- a/lib/src/model/broadcast/broadcast_game_clock.dart +++ /dev/null @@ -1,171 +0,0 @@ -import 'dart:async'; - -import 'package:lichess_mobile/src/model/broadcast/broadcast_round_controller.dart'; -import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/network/socket.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -@riverpod -class BroadcastClockController extends _$BroadcastRoundController { - static Uri broadcastSocketUri(BroadcastRoundId broadcastRoundId) => - Uri(path: 'study/$broadcastRoundId/socket/v6'); - - StreamSubscription? _subscription; - - Timer? _timer; - - late SocketClient _socketClient; - - @override - Future build(BroadcastRoundId broadcastRoundId) async { - _socketClient = ref - .watch(socketPoolProvider) - .open(BroadcastClockController.broadcastSocketUri(broadcastRoundId)); - - _subscription = _socketClient.stream.listen(_handleSocketEvent); - - ref.onDispose(() { - _subscription?.cancel(); - _timer?.cancel(); - }); - - final games = - await ref.watch(broadcastRoundProvider(broadcastRoundId).future); - - _timer = Timer.periodic( - const Duration(seconds: 1), - (_) => _updateClocks(), - ); - - return games; - } - - void _updateClocks() { - state = AsyncData( - state.requireValue.map((gameId, game) { - if (!game.isPlaying) return MapEntry(gameId, game); - final thinkTime = game.thinkTime; - final newThinkTime = thinkTime + const Duration(seconds: 1); - return MapEntry(gameId, game.copyWith(thinkTime: newThinkTime)); - }), - ); - } - - void _handleSocketEvent(SocketEvent event) { - if (!state.hasValue) return; - - switch (event.topic) { - // Sent when a node is recevied from the broadcast - case 'addNode': - _handleAddNodeEvent(event); - // Sent when the state of games changes - case 'chapters': - _handleChaptersEvent(event); - // Sent when clocks are updated from the broadcast - case 'clock': - _handleClockEvent(event); - } - } - - void _handleAddNodeEvent(SocketEvent event) { - // The path of the last and current move of the broadcasted game - // Its value is "!" if the path is identical to one of the node that was received - final currentPath = pick(event.data, 'relayPath').asUciPathOrThrow(); - - // We check that the event we received is for the last move of the game - if (currentPath.value != '!') return; - - final broadcastGameId = - pick(event.data, 'p', 'chapterId').asBroadcastGameIdOrThrow(); - - final fen = pick(event.data, 'n', 'fen').asStringOrThrow(); - - final playingSide = Setup.parseFen(fen).turn; - - state = AsyncData( - state.requireValue.update( - broadcastGameId, - (broadcastGame) => broadcastGame.copyWith( - players: IMap( - { - playingSide: broadcastGame.players[playingSide]!, - playingSide.opposite: - broadcastGame.players[playingSide.opposite]!.copyWith( - clock: pick(event.data, 'n', 'clock') - .asDurationFromCentiSecondsOrNull(), - ), - }, - ), - fen: fen, - lastMove: pick(event.data, 'n', 'uci').asUciMoveOrThrow(), - thinkTime: Duration.zero, - ), - ), - ); - } - - void _handleChaptersEvent(SocketEvent event) { - final games = pick(event.data).asListOrThrow(gameFromPick); - state = AsyncData(IMap.fromEntries(games)); - } - - void _handleClockEvent(SocketEvent event) { - final broadcastGameId = - pick(event.data, 'p', 'chapterId').asBroadcastGameIdOrThrow(); - final whiteClock = pick(event.data, 'p', 'relayClocks', 0) - .asDurationFromCentiSecondsOrNull(); - final blackClock = pick(event.data, 'p', 'relayClocks', 1) - .asDurationFromCentiSecondsOrNull(); - state = AsyncData( - state.requireValue.update( - broadcastGameId, - (broadcastsGame) => broadcastsGame.copyWith( - players: IMap( - { - Side.white: broadcastsGame.players[Side.white]!.copyWith( - clock: whiteClock, - ), - Side.black: broadcastsGame.players[Side.black]!.copyWith( - clock: blackClock, - ), - }, - ), - ), - ), - ); - } -} - -@riverpod -Future roundClocks( - RoundClocksRef ref, BroadcastRoundId roundId) async { - final games = - await ref.watch(broadcastRoundControllerProvider(roundId).future); - - final _timer = Timer.periodic( - const Duration(seconds: 1), - (_) => _updateClocks(), - ); - - ref.onDispose(() { - _timer.cancel(); - }); - // get clock data from game controller - - // listen to socket to get clock update events - - // instantiate the local timer - - return clockData; -} - -void _updateClocks() { - state = AsyncData( - state.requireValue.map((gameId, game) { - if (!game.isPlaying) return MapEntry(gameId, game); - final thinkTime = game.thinkTime; - final newThinkTime = thinkTime + const Duration(seconds: 1); - return MapEntry(gameId, game.copyWith(thinkTime: newThinkTime)); - }), - ); -} From d9eb9a44ab367fd2113d85bdf8edd7f5f2e551f5 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Sun, 20 Oct 2024 20:40:42 +0200 Subject: [PATCH 511/979] fix collapsing variations when pressing move other than 1st move of sideline --- .../model/analysis/analysis_controller.dart | 2 +- lib/src/view/pgn/pgn_tree_view.dart | 18 +++++++++++------- test/view/analysis/analysis_screen_test.dart | 17 +++++++++++++++++ 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index 377648aebc..844c15352d 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -282,7 +282,7 @@ class AnalysisController extends _$AnalysisController @override void collapseVariations(UciPath path) { - final node = _root.parentAt(path); + final node = _root.nodeAt(path); for (final child in node.children) { child.isHidden = true; diff --git a/lib/src/view/pgn/pgn_tree_view.dart b/lib/src/view/pgn/pgn_tree_view.dart index cb69d9b98a..58954d796f 100644 --- a/lib/src/view/pgn/pgn_tree_view.dart +++ b/lib/src/view/pgn/pgn_tree_view.dart @@ -406,7 +406,8 @@ List _buildInlineSideLine({ node, lineInfo: ( type: _LineType.inlineSideline, - startLine: i == 0 || sidelineNodes[i - 1].hasTextComment + startLine: i == 0 || sidelineNodes[i - 1].hasTextComment, + pathToLine: initialPath, ), pathToNode: pathToNode, textStyle: textStyle, @@ -444,7 +445,7 @@ enum _LineType { } /// Metadata about a move's role in the tree view. -typedef _LineInfo = ({_LineType type, bool startLine}); +typedef _LineInfo = ({_LineType type, bool startLine, UciPath pathToLine}); List _moveWithComment( ViewBranch branch, { @@ -512,6 +513,7 @@ class _SideLinePart extends ConsumerWidget { lineInfo: ( type: _LineType.sideline, startLine: true, + pathToLine: initialPath, ), firstMoveKey: firstMoveKey, pathToNode: initialPath, @@ -526,6 +528,7 @@ class _SideLinePart extends ConsumerWidget { lineInfo: ( type: _LineType.sideline, startLine: node.hasTextComment, + pathToLine: initialPath, ), pathToNode: path, textStyle: textStyle, @@ -592,6 +595,7 @@ class _MainLinePart extends ConsumerWidget { lineInfo: ( type: _LineType.mainline, startLine: i == 0 || (node as ViewBranch).hasTextComment, + pathToLine: initialPath, ), pathToNode: path, textStyle: textStyle, @@ -969,7 +973,7 @@ class InlineMove extends ConsumerWidget { : '${(ply / 2).ceil()}... $moveWithNag', path: path, branch: branch, - isSideline: lineInfo.type != _LineType.mainline, + lineInfo: lineInfo, ), ); }, @@ -1013,14 +1017,14 @@ class _MoveContextMenu extends ConsumerWidget { required this.title, required this.path, required this.branch, - required this.isSideline, + required this.lineInfo, required this.notifier, }); final String title; final UciPath path; final ViewBranch branch; - final bool isSideline; + final _LineInfo lineInfo; final PgnTreeNotifier notifier; @override @@ -1088,11 +1092,11 @@ class _MoveContextMenu extends ConsumerWidget { ), ), const PlatformDivider(indent: 0), - if (isSideline) ...[ + if (lineInfo.type != _LineType.mainline) ...[ BottomSheetContextMenuAction( icon: Icons.subtitles_off, child: Text(context.l10n.collapseVariations), - onPressed: () => notifier.collapseVariations(path), + onPressed: () => notifier.collapseVariations(lineInfo.pathToLine), ), BottomSheetContextMenuAction( icon: Icons.expand_less, diff --git a/test/view/analysis/analysis_screen_test.dart b/test/view/analysis/analysis_screen_test.dart index 6426fc4079..2b21286b68 100644 --- a/test/view/analysis/analysis_screen_test.dart +++ b/test/view/analysis/analysis_screen_test.dart @@ -263,6 +263,23 @@ void main() { expectSameLine(tester, ['2… h5']); expectSameLine(tester, ['2… Nc6', '3. d3']); expectSameLine(tester, ['2… Qd7']); + + final d3 = find.text('3. d3'); + await tester.longPress(d3); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Collapse variations')); + + // need to wait for current move change debounce delay + await tester.pumpAndSettle(const Duration(milliseconds: 200)); + + // Sidelines should be collapsed again + expect(find.byIcon(Icons.add_box), findsOneWidget); + + expect(find.text('2… h5'), findsNothing); + expect(find.text('2… Nc6'), findsNothing); + expect(find.text('3. d3'), findsNothing); + expect(find.text('2… Qd7'), findsNothing); }); testWidgets('subtrees not part of the current mainline part are cached', From ed5753ea2b57a98b781889bad3ea152e5e37507b Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 21 Oct 2024 09:03:43 +0200 Subject: [PATCH 512/979] Fix Chess960 start position --- lib/src/model/common/chess960.dart | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/lib/src/model/common/chess960.dart b/lib/src/model/common/chess960.dart index d16fb640fd..c1390ec352 100644 --- a/lib/src/model/common/chess960.dart +++ b/lib/src/model/common/chess960.dart @@ -7,14 +7,10 @@ final _random = Random.secure(); Position randomChess960Position() { final rank8 = _positions[_random.nextInt(_positions.length)]; - return Chess( - board: Board.parseFen( - '$rank8/pppppppp/8/8/8/8/PPPPPPPP/${rank8.toUpperCase()}', + return Chess.fromSetup( + Setup.parseFen( + '$rank8/pppppppp/8/8/8/8/PPPPPPPP/${rank8.toUpperCase()} w KQkq - 0 1', ), - turn: Side.white, - castles: Castles.standard, - halfmoves: 0, - fullmoves: 1, ); } From 092587ea5439dc6a849d8ff1e4fa45f1be1a846f Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 21 Oct 2024 11:10:58 +0200 Subject: [PATCH 513/979] Add support for chess960 in editor castling rights --- .../board_editor/board_editor_controller.dart | 129 +++++++++++++----- .../view/board_editor/board_editor_menu.dart | 49 ++++--- .../board_editor/board_editor_screen.dart | 4 + .../board_editor_screen_test.dart | 45 ++++++ 4 files changed, 172 insertions(+), 55 deletions(-) diff --git a/lib/src/model/board_editor/board_editor_controller.dart b/lib/src/model/board_editor/board_editor_controller.dart index 5bcf1eb67a..0b45be6ffb 100644 --- a/lib/src/model/board_editor/board_editor_controller.dart +++ b/lib/src/model/board_editor/board_editor_controller.dart @@ -11,15 +11,23 @@ part 'board_editor_controller.g.dart'; class BoardEditorController extends _$BoardEditorController { @override BoardEditorState build(String? initialFen) { + final setup = Setup.parseFen(initialFen ?? kInitialFEN); return BoardEditorState( orientation: Side.white, sideToPlay: Side.white, pieces: readFen(initialFen ?? kInitialFEN).lock, - castlingRights: SquareSet.corners, + castlingRights: IMap(const { + CastlingRight.whiteKing: true, + CastlingRight.whiteQueen: true, + CastlingRight.blackKing: true, + CastlingRight.blackQueen: true, + }), editorPointerMode: EditorPointerMode.drag, enPassantOptions: SquareSet.empty, enPassantSquare: null, pieceToAddOnEdit: null, + halfmoves: setup.halfmoves, + fullmoves: setup.fullmoves, ); } @@ -126,27 +134,40 @@ class BoardEditorController extends _$BoardEditorController { void setCastling(Side side, CastlingSide castlingSide, bool allowed) { switch (side) { case Side.white: - if (castlingSide == CastlingSide.king) { - _setRookUnmoved(Square.h1, allowed); - } else { - _setRookUnmoved(Square.a1, allowed); + switch (castlingSide) { + case CastlingSide.king: + state = state.copyWith( + castlingRights: + state.castlingRights.add(CastlingRight.whiteKing, allowed), + ); + case CastlingSide.queen: + state = state.copyWith( + castlingRights: + state.castlingRights.add(CastlingRight.whiteQueen, allowed), + ); } case Side.black: - if (castlingSide == CastlingSide.king) { - _setRookUnmoved(Square.h8, allowed); - } else { - _setRookUnmoved(Square.a8, allowed); + switch (castlingSide) { + case CastlingSide.king: + state = state.copyWith( + castlingRights: + state.castlingRights.add(CastlingRight.blackKing, allowed), + ); + case CastlingSide.queen: + state = state.copyWith( + castlingRights: + state.castlingRights.add(CastlingRight.blackQueen, allowed), + ); } } } +} - void _setRookUnmoved(Square square, bool unmoved) { - state = state.copyWith( - castlingRights: unmoved - ? state.castlingRights.withSquare(square) - : state.castlingRights.withoutSquare(square), - ); - } +enum CastlingRight { + whiteKing, + whiteQueen, + blackKing, + blackQueen, } @freezed @@ -157,10 +178,12 @@ class BoardEditorState with _$BoardEditorState { required Side orientation, required Side sideToPlay, required IMap pieces, - required SquareSet castlingRights, + required IMap castlingRights, required EditorPointerMode editorPointerMode, required SquareSet enPassantOptions, required Square? enPassantSquare, + required int halfmoves, + required int fullmoves, /// When null, clears squares when in edit mode. Has no effect in drag mode. required Piece? pieceToAddOnEdit, @@ -169,26 +192,61 @@ class BoardEditorState with _$BoardEditorState { bool isCastlingAllowed(Side side, CastlingSide castlingSide) => switch (side) { Side.white => switch (castlingSide) { - CastlingSide.king => castlingRights.has(Square.h1), - CastlingSide.queen => castlingRights.has(Square.a1), + CastlingSide.king => castlingRights[CastlingRight.whiteKing]!, + CastlingSide.queen => castlingRights[CastlingRight.whiteQueen]!, }, Side.black => switch (castlingSide) { - CastlingSide.king => castlingRights.has(Square.h8), - CastlingSide.queen => castlingRights.has(Square.a8), + CastlingSide.king => castlingRights[CastlingRight.blackKing]!, + CastlingSide.queen => castlingRights[CastlingRight.blackQueen]!, }, }; - Setup get _setup { - final boardFen = writeFen(pieces.unlock); - final board = Board.parseFen(boardFen); - return Setup( - board: board, - castlingRights: castlingRights, - turn: sideToPlay == Side.white ? Side.white : Side.black, - epSquare: enPassantSquare, - halfmoves: 0, - fullmoves: 1, - ); + /// Returns the castling rights part of the FEN string. + /// + /// If the rook is missing on one side of the king, or the king is missing on the + /// backrank, the castling right is removed. + String get _castlingRightsPart { + final parts = []; + final Map hasRook = {}; + final Board board = Board.parseFen(writeFen(pieces.unlock)); + for (final side in Side.values) { + final backrankKing = SquareSet.backrankOf(side) & board.kings; + final rooksAndKings = (board.bySide(side) & SquareSet.backrankOf(side)) & + (board.rooks | board.kings); + for (final castlingSide in CastlingSide.values) { + final candidate = castlingSide == CastlingSide.king + ? rooksAndKings.squares.lastOrNull + : rooksAndKings.squares.firstOrNull; + final isCastlingPossible = candidate != null && + board.rooks.has(candidate) && + backrankKing.singleSquare != null; + switch ((side, castlingSide)) { + case (Side.white, CastlingSide.king): + hasRook[CastlingRight.whiteKing] = isCastlingPossible; + case (Side.white, CastlingSide.queen): + hasRook[CastlingRight.whiteQueen] = isCastlingPossible; + case (Side.black, CastlingSide.king): + hasRook[CastlingRight.blackKing] = isCastlingPossible; + case (Side.black, CastlingSide.queen): + hasRook[CastlingRight.blackQueen] = isCastlingPossible; + } + } + } + for (final right in CastlingRight.values) { + if (hasRook[right]! && castlingRights[right]!) { + switch (right) { + case CastlingRight.whiteKing: + parts.add('K'); + case CastlingRight.whiteQueen: + parts.add('Q'); + case CastlingRight.blackKing: + parts.add('k'); + case CastlingRight.blackQueen: + parts.add('q'); + } + } + } + return parts.isEmpty ? '-' : parts.join(''); } Piece? get activePieceOnEdit => @@ -197,14 +255,17 @@ class BoardEditorState with _$BoardEditorState { bool get deletePiecesActive => editorPointerMode == EditorPointerMode.edit && pieceToAddOnEdit == null; - String get fen => _setup.fen; + String get fen { + final boardFen = writeFen(pieces.unlock); + return '$boardFen ${sideToPlay == Side.white ? 'w' : 'b'} $_castlingRightsPart ${enPassantSquare?.name ?? '-'} $halfmoves $fullmoves'; + } /// Returns the PGN representation of the current position if it is valid. /// /// Returns `null` if the position is invalid. String? get pgn { try { - final position = Chess.fromSetup(_setup); + final position = Chess.fromSetup(Setup.parseFen(fen)); return PgnGame( headers: {'FEN': position.fen}, moves: PgnNode(), diff --git a/lib/src/view/board_editor/board_editor_menu.dart b/lib/src/view/board_editor/board_editor_menu.dart index f3b641bac1..6f35d17525 100644 --- a/lib/src/view/board_editor/board_editor_menu.dart +++ b/lib/src/view/board_editor/board_editor_menu.dart @@ -42,33 +42,40 @@ class BoardEditorMenu extends ConsumerWidget { ), Padding( padding: Styles.bodySectionPadding, - child: Text(context.l10n.castling, style: Styles.subtitle), + child: Text(context.l10n.castling, style: Styles.title), ), ...Side.values.map((side) { return Padding( padding: Styles.horizontalBodyPadding, - child: Wrap( + child: Row( spacing: 8.0, - children: - [CastlingSide.king, CastlingSide.queen].map((castlingSide) { - return ChoiceChip( - label: Text( - castlingSide == CastlingSide.king - ? side == Side.white - ? context.l10n.whiteCastlingKingside - : context.l10n.blackCastlingKingside - : 'O-O-O', + children: [ + SizedBox( + width: 100.0, + child: Text( + side == Side.white + ? context.l10n.white + : context.l10n.black, + maxLines: 1, + overflow: TextOverflow.ellipsis, ), - selected: editorState.isCastlingAllowed(side, castlingSide), - onSelected: (selected) { - ref.read(editorController.notifier).setCastling( - side, - castlingSide, - selected, - ); - }, - ); - }).toList(), + ), + ...[CastlingSide.king, CastlingSide.queen].map((castlingSide) { + return ChoiceChip( + label: Text( + castlingSide == CastlingSide.king ? 'O-O' : 'O-O-O', + ), + selected: editorState.isCastlingAllowed(side, castlingSide), + onSelected: (selected) { + ref.read(editorController.notifier).setCastling( + side, + castlingSide, + selected, + ); + }, + ); + }), + ], ), ); }), diff --git a/lib/src/view/board_editor/board_editor_screen.dart b/lib/src/view/board_editor/board_editor_screen.dart index 2012ab8195..351ee35d94 100644 --- a/lib/src/view/board_editor/board_editor_screen.dart +++ b/lib/src/view/board_editor/board_editor_screen.dart @@ -311,6 +311,10 @@ class _BottomBar extends ConsumerWidget { builder: (BuildContext context) => BoardEditorMenu( initialFen: initialFen, ), + showDragHandle: true, + constraints: BoxConstraints( + minHeight: MediaQuery.sizeOf(context).height * 0.5, + ), ), icon: Icons.tune, ), diff --git a/test/view/board_editor/board_editor_screen_test.dart b/test/view/board_editor/board_editor_screen_test.dart index 1511d59c27..2e6b4381d1 100644 --- a/test/view/board_editor/board_editor_screen_test.dart +++ b/test/view/board_editor/board_editor_screen_test.dart @@ -143,6 +143,51 @@ void main() { ); }); + testWidgets('support chess960 castling rights', (tester) async { + final app = await makeTestProviderScopeApp( + tester, + home: const BoardEditorScreen(), + ); + await tester.pumpWidget(app); + + final container = ProviderScope.containerOf( + tester.element(find.byType(ChessboardEditor)), + ); + final controllerProvider = boardEditorControllerProvider(null); + + container + .read(controllerProvider.notifier) + .loadFen('rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/4RK1R'); + + expect( + container.read(controllerProvider).fen, + 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/4RK1R w KQkq - 0 1', + ); + }); + + testWidgets('Castling rights ignored when king is not in backrank', + (tester) async { + final app = await makeTestProviderScopeApp( + tester, + home: const BoardEditorScreen(), + ); + await tester.pumpWidget(app); + + final container = ProviderScope.containerOf( + tester.element(find.byType(ChessboardEditor)), + ); + final controllerProvider = boardEditorControllerProvider(null); + + container + .read(controllerProvider.notifier) + .loadFen('rnbqkbnr/pppppppp/8/8/8/5K2/PPPPPPPP/4R2R'); + + expect( + container.read(controllerProvider).fen, + 'rnbqkbnr/pppppppp/8/8/8/5K2/PPPPPPPP/4R2R w kq - 0 1', + ); + }); + testWidgets('Possible en passant squares are calculated correctly', (tester) async { final app = await makeTestProviderScopeApp( From d79575774504ef6df0ebdabacc91ccffaa32a059 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 21 Oct 2024 11:28:58 +0200 Subject: [PATCH 514/979] Disable slidable if there's no handler Fixes #1101 --- lib/src/view/play/challenge_list_item.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/src/view/play/challenge_list_item.dart b/lib/src/view/play/challenge_list_item.dart index 955f434d39..f18078c1f4 100644 --- a/lib/src/view/play/challenge_list_item.dart +++ b/lib/src/view/play/challenge_list_item.dart @@ -68,6 +68,9 @@ class ChallengeListItem extends ConsumerWidget { return Container( color: color, child: Slidable( + enabled: onAccept != null || + onDecline != null || + (isMyChallenge && onCancel != null), dragStartBehavior: DragStartBehavior.start, endActionPane: ActionPane( motion: const StretchMotion(), From cb3cecc1f8c6bf72ca114737703e6295a9c9876b Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 21 Oct 2024 12:18:12 +0200 Subject: [PATCH 515/979] Provide last version of sqlite with the app --- ios/Podfile.lock | 24 ++++++++++++++ lib/src/db/database.dart | 15 ++++++++- lib/src/db/openings_database.dart | 12 +++++-- .../correspondence_game_storage.dart | 32 +++++++++---------- pubspec.lock | 10 +++++- pubspec.yaml | 3 +- test/model/puzzle/puzzle_storage_test.dart | 1 - 7 files changed, 74 insertions(+), 23 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 313ff3a5b1..cdf89114e5 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -134,6 +134,24 @@ PODS: - sqflite_darwin (0.0.4): - Flutter - FlutterMacOS + - "sqlite3 (3.46.1+1)": + - "sqlite3/common (= 3.46.1+1)" + - "sqlite3/common (3.46.1+1)" + - "sqlite3/dbstatvtab (3.46.1+1)": + - sqlite3/common + - "sqlite3/fts5 (3.46.1+1)": + - sqlite3/common + - "sqlite3/perf-threadsafe (3.46.1+1)": + - sqlite3/common + - "sqlite3/rtree (3.46.1+1)": + - sqlite3/common + - sqlite3_flutter_libs (0.0.1): + - Flutter + - "sqlite3 (~> 3.46.0+1)" + - sqlite3/dbstatvtab + - sqlite3/fts5 + - sqlite3/perf-threadsafe + - sqlite3/rtree - stockfish (1.6.2): - Flutter - url_launcher_ios (0.0.1): @@ -160,6 +178,7 @@ DEPENDENCIES: - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - sound_effect (from `.symlinks/plugins/sound_effect/ios`) - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) + - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/ios`) - stockfish (from `.symlinks/plugins/stockfish/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) @@ -181,6 +200,7 @@ SPEC REPOS: - nanopb - PromisesObjC - PromisesSwift + - sqlite3 EXTERNAL SOURCES: app_settings: @@ -219,6 +239,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/sound_effect/ios" sqflite_darwin: :path: ".symlinks/plugins/sqflite_darwin/darwin" + sqlite3_flutter_libs: + :path: ".symlinks/plugins/sqlite3_flutter_libs/ios" stockfish: :path: ".symlinks/plugins/stockfish/ios" url_launcher_ios: @@ -260,6 +282,8 @@ SPEC CHECKSUMS: shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 sound_effect: 5280cfa89d4a576032186f15600dc948ca6d39ce sqflite_darwin: a553b1fd6fe66f53bbb0fe5b4f5bab93f08d7a13 + sqlite3: 0bb0e6389d824e40296f531b858a2a0b71c0d2fb + sqlite3_flutter_libs: c00457ebd31e59fa6bb830380ddba24d44fbcd3b stockfish: d00cf6b95579f1d7032cbfd8e4fe874972fe2ff9 url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1 diff --git a/lib/src/db/database.dart b/lib/src/db/database.dart index 410bd3247a..1e0f6f28fd 100644 --- a/lib/src/db/database.dart +++ b/lib/src/db/database.dart @@ -1,9 +1,10 @@ import 'dart:async'; import 'dart:io'; +import 'package:logging/logging.dart'; import 'package:path/path.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:sqflite/sqflite.dart'; +import 'package:sqflite_common_ffi/sqflite_ffi.dart'; part 'database.g.dart'; @@ -16,6 +17,10 @@ const chatReadMessagesTTL = Duration(days: 60); const kStorageAnonId = '**anonymous**'; +final databaseFactory = databaseFactoryFfi; + +final _logger = Logger('Database'); + @Riverpod(keepAlive: true) Future database(DatabaseRef ref) async { final dbPath = join(await getDatabasesPath(), kLichessDatabaseName); @@ -26,6 +31,10 @@ Future database(DatabaseRef ref) async { @Riverpod(keepAlive: true) Future sqliteVersion(SqliteVersionRef ref) async { final db = await ref.read(databaseProvider.future); + return _getDatabaseVersion(db); +} + +Future _getDatabaseVersion(Database db) async { try { final versionStr = (await db.rawQuery('SELECT sqlite_version()')) .first @@ -54,6 +63,10 @@ Future openAppDatabase(DatabaseFactory dbFactory, String path) async { path, options: OpenDatabaseOptions( version: 2, + onConfigure: (db) async { + final version = await _getDatabaseVersion(db); + _logger.info('SQLite version: $version'); + }, onOpen: (db) async { await Future.wait([ _deleteOldEntries(db, 'puzzle', puzzleTTL), diff --git a/lib/src/db/openings_database.dart b/lib/src/db/openings_database.dart index 23689c63a3..9a6e6ce08f 100644 --- a/lib/src/db/openings_database.dart +++ b/lib/src/db/openings_database.dart @@ -3,7 +3,7 @@ import 'dart:io'; import 'package:flutter/services.dart'; import 'package:path/path.dart' as p; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:sqflite/sqflite.dart'; +import 'package:sqflite_common_ffi/sqflite_ffi.dart'; part 'openings_database.g.dart'; @@ -19,6 +19,8 @@ part 'openings_database.g.dart'; const _kDatabaseVersion = 2; const _kDatabaseName = 'chess_openings$_kDatabaseVersion.db'; +final databaseFactory = databaseFactoryFfi; + @Riverpod(keepAlive: true) Future openingsDatabase(OpeningsDatabaseRef ref) async { final dbPath = p.join(await getDatabasesPath(), _kDatabaseName); @@ -53,5 +55,11 @@ Future _openDb(String path) async { await File(path).writeAsBytes(bytes, flush: true); } - return openDatabase(path, readOnly: true); + return databaseFactory.openDatabase( + path, + options: OpenDatabaseOptions( + version: _kDatabaseVersion, + readOnly: true, + ), + ); } diff --git a/lib/src/model/correspondence/correspondence_game_storage.dart b/lib/src/model/correspondence/correspondence_game_storage.dart index d28d4bb6d0..88883a6317 100644 --- a/lib/src/model/correspondence/correspondence_game_storage.dart +++ b/lib/src/model/correspondence/correspondence_game_storage.dart @@ -79,29 +79,27 @@ class CorrespondenceGameStorage { /// Fetches all correspondence games with a registered move. Future> fetchGamesWithRegisteredMove(UserId? userId) async { - final sqlVersion = await ref.read(sqliteVersionProvider.future); - if (sqlVersion != null && sqlVersion >= 338000) { + try { final list = await _db.query( kCorrespondenceStorageTable, where: "json_extract(data, '\$.registeredMoveAtPgn') IS NOT NULL", ); return _decodeGames(list); - } - - final list = await _db.query( - kCorrespondenceStorageTable, - // where: "json_extract(data, '\$.registeredMoveAtPgn') IS NOT NULL", - where: 'userId = ? AND data LIKE ?', - whereArgs: [ - '${userId ?? kCorrespondenceStorageAnonId}', - '%status":"started"%', - ], - ); + } catch (e) { + final list = await _db.query( + kCorrespondenceStorageTable, + where: 'userId = ? AND data LIKE ?', + whereArgs: [ + '${userId ?? kCorrespondenceStorageAnonId}', + '%status":"started"%', + ], + ); - return _decodeGames(list).where((e) { - final (_, game) = e; - return game.registeredMoveAtPgn != null; - }).toIList(); + return _decodeGames(list).where((e) { + final (_, game) = e; + return game.registeredMoveAtPgn != null; + }).toIList(); + } } Future fetch({ diff --git a/pubspec.lock b/pubspec.lock index da527005a1..24e4a8a238 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1337,7 +1337,7 @@ packages: source: hosted version: "2.5.4+5" sqflite_common_ffi: - dependency: "direct dev" + dependency: "direct main" description: name: sqflite_common_ffi sha256: d316908f1537725427ff2827a5c5f3b2c1bc311caed985fe3c9b10939c9e11ca @@ -1368,6 +1368,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.6" + sqlite3_flutter_libs: + dependency: "direct main" + description: + name: sqlite3_flutter_libs + sha256: ccd29dd6cf6fb9351fa07cd6f92895809adbf0779c1d986acf5e3d53b3250e33 + url: "https://pub.dev" + source: hosted + version: "0.5.25" stack_trace: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 75e9b6d929..97ec48e998 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -65,6 +65,8 @@ dependencies: signal_strength_indicator: ^0.4.1 sound_effect: ^0.0.2 sqflite: ^2.2.5 + sqflite_common_ffi: ^2.2.3 + sqlite3_flutter_libs: ^0.5.25 stockfish: git: url: https://github.com/lichess-org/dart-stockfish.git @@ -88,7 +90,6 @@ dev_dependencies: mocktail: ^1.0.0 riverpod_generator: ^2.1.0 riverpod_lint: ^2.3.3 - sqflite_common_ffi: ^2.2.3 stream_channel: ^2.1.2 flutter: diff --git a/test/model/puzzle/puzzle_storage_test.dart b/test/model/puzzle/puzzle_storage_test.dart index 88f6e9a9f4..888150b5c0 100644 --- a/test/model/puzzle/puzzle_storage_test.dart +++ b/test/model/puzzle/puzzle_storage_test.dart @@ -12,7 +12,6 @@ import '../../test_container.dart'; void main() { final dbFactory = databaseFactoryFfi; - sqfliteFfiInit(); group('PuzzleHistoryStorage', () { test('save and fetch data', () async { From 8c9c71d7b67c42b928a882a741a260aee8db951e Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 21 Oct 2024 12:21:51 +0200 Subject: [PATCH 516/979] Remove possibility to cancel draw offer Closes #1061 --- lib/src/view/game/game_body.dart | 12 +----------- translation/source/mobile.xml | 1 - 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/lib/src/view/game/game_body.dart b/lib/src/view/game/game_body.dart index af8b9fae60..790d2f472a 100644 --- a/lib/src/view/game/game_body.dart +++ b/lib/src/view/game/game_body.dart @@ -728,17 +728,7 @@ class _GameBottomBar extends ConsumerWidget { .cancelOrDeclineTakeback(); }, ), - if (gameState.game.me?.offeringDraw == true) - BottomSheetAction( - makeLabel: (context) => Text(context.l10n.mobileCancelDrawOffer), - isDestructiveAction: true, - onPressed: (context) { - ref - .read(gameControllerProvider(id).notifier) - .cancelOrDeclineDraw(); - }, - ) - else if (gameState.canOfferDraw) + if (gameState.canOfferDraw) BottomSheetAction( makeLabel: (context) => Text(context.l10n.offerDraw), onPressed: gameState.shouldConfirmResignAndDrawOffer diff --git a/translation/source/mobile.xml b/translation/source/mobile.xml index 8ea73baed3..2822b3ef7f 100644 --- a/translation/source/mobile.xml +++ b/translation/source/mobile.xml @@ -31,7 +31,6 @@ Do you want to end this run? Nothing to show, please change the filters Cancel takeback offer - Cancel draw offer Waiting for opponent to join... Blindfold Live streamers From d4550798ad1ad9934793a5212d7872b885f7e8ca Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 21 Oct 2024 12:23:43 +0200 Subject: [PATCH 517/979] Bump version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 97ec48e998..6d269ac910 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: lichess_mobile description: Lichess mobile app V2 publish_to: "none" -version: 0.12.2+001202 # see README.md for details about versioning +version: 0.12.3+001203 # see README.md for details about versioning environment: sdk: ">=3.3.0 <4.0.0" From bfa7557a790a8c5fe4aad108e5d1ad3962370fcc Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 21 Oct 2024 12:35:01 +0200 Subject: [PATCH 518/979] Update dependencies --- pubspec.lock | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 24e4a8a238..9b66332986 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -596,18 +596,18 @@ packages: dependency: "direct main" description: name: flutter_native_splash - sha256: aa06fec78de2190f3db4319dd60fdc8d12b2626e93ef9828633928c2dcaea840 + sha256: ee5c9bd2b74ea8676442fd4ab876b5d41681df49276488854d6c81a5377c0ef1 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" flutter_riverpod: dependency: "direct main" description: name: flutter_riverpod - sha256: "711d916456563f715bde1e139d7cfdca009f8264befab3ac9f8ded8b6ec26405" + sha256: "6eda4e247774474c715a0805a2fb8e3cd55fbae4ead641e063c95b4bd5f3b317" url: "https://pub.dev" source: hosted - version: "2.5.3" + version: "2.6.0" flutter_secure_storage: dependency: "direct main" description: @@ -1127,10 +1127,10 @@ packages: dependency: transitive description: name: riverpod - sha256: c86fedfb45dd1da98ee6493dd9374325cdf494e7d523ebfb0c387eecc5f7b5c9 + sha256: bd6e656a764e3d27f211975626e0c4f9b8d06ab16acf3c7ba7a8061e09744c75 url: "https://pub.dev" source: hosted - version: "2.5.3" + version: "2.6.0" riverpod_analyzer_utils: dependency: transitive description: @@ -1143,10 +1143,10 @@ packages: dependency: "direct main" description: name: riverpod_annotation - sha256: "77fdedb87d09344809e8b514ab864d0537b1cb580a93d09bf579b0403aa6203a" + sha256: "1e61f8e7fc360f75e3520bbb35d22213ef542dc0de574cf90d639a358965d743" url: "https://pub.dev" source: hosted - version: "2.5.3" + version: "2.6.0" riverpod_generator: dependency: "direct dev" description: @@ -1661,10 +1661,10 @@ packages: dependency: transitive description: name: win32 - sha256: "2294c64768987ea280b43a3d8357d42d5679f3e2b5b69b602be45b2abbd165b0" + sha256: "2735daae5150e8b1dfeb3eb0544b4d3af0061e9e82cef063adcd583bdae4306a" url: "https://pub.dev" source: hosted - version: "5.6.1" + version: "5.7.0" win32_registry: dependency: transitive description: From 672c0f8eab0faa6ba43718226bf2158da8501d37 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 21 Oct 2024 12:35:18 +0200 Subject: [PATCH 519/979] Fix riverpod deprecation warnings --- lib/src/model/settings/preferences_storage.dart | 11 +++-------- lib/src/network/http.dart | 2 -- lib/src/utils/riverpod.dart | 4 +--- 3 files changed, 4 insertions(+), 13 deletions(-) diff --git a/lib/src/model/settings/preferences_storage.dart b/lib/src/model/settings/preferences_storage.dart index 7a8a674f08..6531fa752c 100644 --- a/lib/src/model/settings/preferences_storage.dart +++ b/lib/src/model/settings/preferences_storage.dart @@ -32,10 +32,7 @@ enum PrefCategory { } /// A [Notifier] mixin to provide a way to store and retrieve preferences. -mixin PreferencesStorage { - AutoDisposeNotifierProviderRef get ref; - abstract T state; - +mixin PreferencesStorage on AutoDisposeNotifier { T fromJson(Map json); T get defaults; @@ -66,10 +63,8 @@ mixin PreferencesStorage { } /// A [Notifier] mixin to provide a way to store and retrieve preferences per session. -mixin SessionPreferencesStorage { - AutoDisposeNotifierProviderRef get ref; - abstract T state; - +mixin SessionPreferencesStorage + on AutoDisposeNotifier { T fromJson(Map json); T defaults({LightUser? user}); diff --git a/lib/src/network/http.dart b/lib/src/network/http.dart index 7c8ba776a8..9c5a1cd7bd 100644 --- a/lib/src/network/http.dart +++ b/lib/src/network/http.dart @@ -594,9 +594,7 @@ extension ClientRefExtension on Ref { final client = read(lichessClientProvider); return await fn(client); } -} -extension ClientAutoDisposeRefExtension on AutoDisposeRef { /// Runs [fn] with a [LichessClient] and keeps the provider alive for [duration]. /// /// This is primarily used for caching network requests in a [FutureProvider]. diff --git a/lib/src/utils/riverpod.dart b/lib/src/utils/riverpod.dart index cc09b77654..a4976c6ea0 100644 --- a/lib/src/utils/riverpod.dart +++ b/lib/src/utils/riverpod.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -extension AutoDisposeRefExtension on AutoDisposeRef { +extension RefExtension on Ref { /// Keeps the provider alive for [duration] KeepAliveLink cacheFor(Duration duration) { final link = keepAlive(); @@ -9,9 +9,7 @@ extension AutoDisposeRefExtension on AutoDisposeRef { onDispose(timer.cancel); return link; } -} -extension RefExtension on Ref { /// Delays an execution by a bit such that if a dependency changes multiple /// time rapidly, the rest of the code is only run once. Future debounce(Duration duration) { From 1c77e21c8d81347cf4b996bac55982d52d8d70e7 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 21 Oct 2024 14:52:53 +0200 Subject: [PATCH 520/979] Do not provide another sqlite for now --- ios/Podfile.lock | 24 ------------------------ lib/src/db/database.dart | 4 +--- lib/src/db/openings_database.dart | 9 ++------- pubspec.lock | 10 +--------- pubspec.yaml | 3 +-- 5 files changed, 5 insertions(+), 45 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index cdf89114e5..313ff3a5b1 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -134,24 +134,6 @@ PODS: - sqflite_darwin (0.0.4): - Flutter - FlutterMacOS - - "sqlite3 (3.46.1+1)": - - "sqlite3/common (= 3.46.1+1)" - - "sqlite3/common (3.46.1+1)" - - "sqlite3/dbstatvtab (3.46.1+1)": - - sqlite3/common - - "sqlite3/fts5 (3.46.1+1)": - - sqlite3/common - - "sqlite3/perf-threadsafe (3.46.1+1)": - - sqlite3/common - - "sqlite3/rtree (3.46.1+1)": - - sqlite3/common - - sqlite3_flutter_libs (0.0.1): - - Flutter - - "sqlite3 (~> 3.46.0+1)" - - sqlite3/dbstatvtab - - sqlite3/fts5 - - sqlite3/perf-threadsafe - - sqlite3/rtree - stockfish (1.6.2): - Flutter - url_launcher_ios (0.0.1): @@ -178,7 +160,6 @@ DEPENDENCIES: - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - sound_effect (from `.symlinks/plugins/sound_effect/ios`) - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) - - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/ios`) - stockfish (from `.symlinks/plugins/stockfish/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) @@ -200,7 +181,6 @@ SPEC REPOS: - nanopb - PromisesObjC - PromisesSwift - - sqlite3 EXTERNAL SOURCES: app_settings: @@ -239,8 +219,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/sound_effect/ios" sqflite_darwin: :path: ".symlinks/plugins/sqflite_darwin/darwin" - sqlite3_flutter_libs: - :path: ".symlinks/plugins/sqlite3_flutter_libs/ios" stockfish: :path: ".symlinks/plugins/stockfish/ios" url_launcher_ios: @@ -282,8 +260,6 @@ SPEC CHECKSUMS: shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 sound_effect: 5280cfa89d4a576032186f15600dc948ca6d39ce sqflite_darwin: a553b1fd6fe66f53bbb0fe5b4f5bab93f08d7a13 - sqlite3: 0bb0e6389d824e40296f531b858a2a0b71c0d2fb - sqlite3_flutter_libs: c00457ebd31e59fa6bb830380ddba24d44fbcd3b stockfish: d00cf6b95579f1d7032cbfd8e4fe874972fe2ff9 url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1 diff --git a/lib/src/db/database.dart b/lib/src/db/database.dart index 1e0f6f28fd..848cd777dd 100644 --- a/lib/src/db/database.dart +++ b/lib/src/db/database.dart @@ -4,7 +4,7 @@ import 'dart:io'; import 'package:logging/logging.dart'; import 'package:path/path.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:sqflite_common_ffi/sqflite_ffi.dart'; +import 'package:sqflite/sqflite.dart'; part 'database.g.dart'; @@ -17,8 +17,6 @@ const chatReadMessagesTTL = Duration(days: 60); const kStorageAnonId = '**anonymous**'; -final databaseFactory = databaseFactoryFfi; - final _logger = Logger('Database'); @Riverpod(keepAlive: true) diff --git a/lib/src/db/openings_database.dart b/lib/src/db/openings_database.dart index 9a6e6ce08f..053591386f 100644 --- a/lib/src/db/openings_database.dart +++ b/lib/src/db/openings_database.dart @@ -3,7 +3,7 @@ import 'dart:io'; import 'package:flutter/services.dart'; import 'package:path/path.dart' as p; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:sqflite_common_ffi/sqflite_ffi.dart'; +import 'package:sqflite/sqflite.dart'; part 'openings_database.g.dart'; @@ -19,8 +19,6 @@ part 'openings_database.g.dart'; const _kDatabaseVersion = 2; const _kDatabaseName = 'chess_openings$_kDatabaseVersion.db'; -final databaseFactory = databaseFactoryFfi; - @Riverpod(keepAlive: true) Future openingsDatabase(OpeningsDatabaseRef ref) async { final dbPath = p.join(await getDatabasesPath(), _kDatabaseName); @@ -57,9 +55,6 @@ Future _openDb(String path) async { return databaseFactory.openDatabase( path, - options: OpenDatabaseOptions( - version: _kDatabaseVersion, - readOnly: true, - ), + options: OpenDatabaseOptions(readOnly: true), ); } diff --git a/pubspec.lock b/pubspec.lock index 9b66332986..5705607cf8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1337,7 +1337,7 @@ packages: source: hosted version: "2.5.4+5" sqflite_common_ffi: - dependency: "direct main" + dependency: "direct dev" description: name: sqflite_common_ffi sha256: d316908f1537725427ff2827a5c5f3b2c1bc311caed985fe3c9b10939c9e11ca @@ -1368,14 +1368,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.6" - sqlite3_flutter_libs: - dependency: "direct main" - description: - name: sqlite3_flutter_libs - sha256: ccd29dd6cf6fb9351fa07cd6f92895809adbf0779c1d986acf5e3d53b3250e33 - url: "https://pub.dev" - source: hosted - version: "0.5.25" stack_trace: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 6d269ac910..247418133e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -65,8 +65,6 @@ dependencies: signal_strength_indicator: ^0.4.1 sound_effect: ^0.0.2 sqflite: ^2.2.5 - sqflite_common_ffi: ^2.2.3 - sqlite3_flutter_libs: ^0.5.25 stockfish: git: url: https://github.com/lichess-org/dart-stockfish.git @@ -90,6 +88,7 @@ dev_dependencies: mocktail: ^1.0.0 riverpod_generator: ^2.1.0 riverpod_lint: ^2.3.3 + sqflite_common_ffi: ^2.2.3 stream_channel: ^2.1.2 flutter: From 0c7800cad11e7bf8b08e8d89e26f41f1502d0a2a Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 21 Oct 2024 15:38:32 +0200 Subject: [PATCH 521/979] Only reconnect socket if not disposed --- lib/src/network/socket.dart | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/src/network/socket.dart b/lib/src/network/socket.dart index e055f29023..08e05fd402 100644 --- a/lib/src/network/socket.dart +++ b/lib/src/network/socket.dart @@ -408,8 +408,14 @@ class SocketClient { _reconnectTimer?.cancel(); _reconnectTimer = Timer(delay, () { _averageLag.value = Duration.zero; - _logger.fine('Reconnecting WebSocket.'); - connect(); + if (!isDisposed) { + _logger.fine('Reconnecting WebSocket.'); + connect(); + } else { + _logger.warning( + 'Scheduled reconnect after $delay failed since client is disposed.', + ); + } }); } From 719e8604c12dfd052c20e5e8b7da23d1dec3e450 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 21 Oct 2024 15:47:02 +0200 Subject: [PATCH 522/979] Move code to widget folder --- .../model/analysis/analysis_controller.dart | 2 +- lib/src/view/analysis/analysis_board.dart | 2 +- lib/src/view/analysis/tree_view.dart | 2 +- lib/src/view/game/game_result_dialog.dart | 2 +- lib/src/view/pgn/annotations.dart | 69 ------------------- .../pgn_tree_view.dart => widgets/pgn.dart} | 68 +++++++++++++++++- test/view/analysis/analysis_screen_test.dart | 2 +- 7 files changed, 72 insertions(+), 75 deletions(-) delete mode 100644 lib/src/view/pgn/annotations.dart rename lib/src/{view/pgn/pgn_tree_view.dart => widgets/pgn.dart} (96%) diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index 376487c1a6..f14ba20ebc 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -20,7 +20,7 @@ import 'package:lichess_mobile/src/model/engine/work.dart'; import 'package:lichess_mobile/src/model/game/player.dart'; import 'package:lichess_mobile/src/utils/rate_limit.dart'; import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; -import 'package:lichess_mobile/src/view/pgn/pgn_tree_view.dart'; +import 'package:lichess_mobile/src/widgets/pgn.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'analysis_controller.freezed.dart'; diff --git a/lib/src/view/analysis/analysis_board.dart b/lib/src/view/analysis/analysis_board.dart index 04dbca98e6..22ff79c595 100644 --- a/lib/src/view/analysis/analysis_board.dart +++ b/lib/src/view/analysis/analysis_board.dart @@ -15,7 +15,7 @@ import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/eval.dart'; import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; -import 'package:lichess_mobile/src/view/pgn/annotations.dart'; +import 'package:lichess_mobile/src/widgets/pgn.dart'; class AnalysisBoard extends ConsumerStatefulWidget { const AnalysisBoard( diff --git a/lib/src/view/analysis/tree_view.dart b/lib/src/view/analysis/tree_view.dart index 89bef6120c..1c132b65bf 100644 --- a/lib/src/view/analysis/tree_view.dart +++ b/lib/src/view/analysis/tree_view.dart @@ -4,7 +4,7 @@ import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/analysis/opening_service.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; -import 'package:lichess_mobile/src/view/pgn/pgn_tree_view.dart'; +import 'package:lichess_mobile/src/widgets/pgn.dart'; const kOpeningHeaderHeight = 32.0; diff --git a/lib/src/view/game/game_result_dialog.dart b/lib/src/view/game/game_result_dialog.dart index f1ea715ceb..8dc98cf67f 100644 --- a/lib/src/view/game/game_result_dialog.dart +++ b/lib/src/view/game/game_result_dialog.dart @@ -22,9 +22,9 @@ import 'package:lichess_mobile/src/model/game/playable_game.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; -import 'package:lichess_mobile/src/view/pgn/annotations.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; +import 'package:lichess_mobile/src/widgets/pgn.dart'; import 'status_l10n.dart'; diff --git a/lib/src/view/pgn/annotations.dart b/lib/src/view/pgn/annotations.dart deleted file mode 100644 index 79c7184eee..0000000000 --- a/lib/src/view/pgn/annotations.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'package:chessground/chessground.dart'; -import 'package:flutter/material.dart'; -import 'package:lichess_mobile/src/styles/lichess_colors.dart'; - -const innacuracyColor = LichessColors.cyan; -const mistakeColor = Color(0xFFe69f00); -const blunderColor = Color(0xFFdf5353); - -Color? nagColor(int nag) { - return switch (nag) { - 1 => Colors.lightGreen, - 2 => mistakeColor, - 3 => Colors.teal, - 4 => blunderColor, - 5 => LichessColors.purple, - 6 => LichessColors.cyan, - int() => null, - }; -} - -String moveAnnotationChar(Iterable nags) { - return nags - .map( - (nag) => switch (nag) { - 1 => '!', - 2 => '?', - 3 => '!!', - 4 => '??', - 5 => '!?', - 6 => '?!', - int() => '', - }, - ) - .join(''); -} - -Annotation? makeAnnotation(Iterable? nags) { - final nag = nags?.firstOrNull; - if (nag == null) { - return null; - } - return switch (nag) { - 1 => const Annotation( - symbol: '!', - color: Colors.lightGreen, - ), - 3 => const Annotation( - symbol: '!!', - color: Colors.teal, - ), - 5 => const Annotation( - symbol: '!?', - color: Colors.purple, - ), - 6 => const Annotation( - symbol: '?!', - color: LichessColors.cyan, - ), - 2 => const Annotation( - symbol: '?', - color: mistakeColor, - ), - 4 => const Annotation( - symbol: '??', - color: blunderColor, - ), - int() => null, - }; -} diff --git a/lib/src/view/pgn/pgn_tree_view.dart b/lib/src/widgets/pgn.dart similarity index 96% rename from lib/src/view/pgn/pgn_tree_view.dart rename to lib/src/widgets/pgn.dart index 58954d796f..f480db8883 100644 --- a/lib/src/view/pgn/pgn_tree_view.dart +++ b/lib/src/widgets/pgn.dart @@ -1,3 +1,4 @@ +import 'package:chessground/chessground.dart'; import 'package:collection/collection.dart'; import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; @@ -8,6 +9,7 @@ import 'package:lichess_mobile/src/model/account/account_preferences.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; import 'package:lichess_mobile/src/model/common/node.dart'; import 'package:lichess_mobile/src/model/common/uci.dart'; +import 'package:lichess_mobile/src/styles/lichess_colors.dart'; import 'package:lichess_mobile/src/utils/duration.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/rate_limit.dart'; @@ -15,7 +17,71 @@ import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; -import 'annotations.dart'; +const innacuracyColor = LichessColors.cyan; +const mistakeColor = Color(0xFFe69f00); +const blunderColor = Color(0xFFdf5353); + +Color? nagColor(int nag) { + return switch (nag) { + 1 => Colors.lightGreen, + 2 => mistakeColor, + 3 => Colors.teal, + 4 => blunderColor, + 5 => LichessColors.purple, + 6 => LichessColors.cyan, + int() => null, + }; +} + +String moveAnnotationChar(Iterable nags) { + return nags + .map( + (nag) => switch (nag) { + 1 => '!', + 2 => '?', + 3 => '!!', + 4 => '??', + 5 => '!?', + 6 => '?!', + int() => '', + }, + ) + .join(''); +} + +Annotation? makeAnnotation(Iterable? nags) { + final nag = nags?.firstOrNull; + if (nag == null) { + return null; + } + return switch (nag) { + 1 => const Annotation( + symbol: '!', + color: Colors.lightGreen, + ), + 3 => const Annotation( + symbol: '!!', + color: Colors.teal, + ), + 5 => const Annotation( + symbol: '!?', + color: Colors.purple, + ), + 6 => const Annotation( + symbol: '?!', + color: LichessColors.cyan, + ), + 2 => const Annotation( + symbol: '?', + color: mistakeColor, + ), + 4 => const Annotation( + symbol: '??', + color: blunderColor, + ), + int() => null, + }; +} // fast replay debounce delay, same as piece animation duration, to avoid piece // animation jank at the end of the replay diff --git a/test/view/analysis/analysis_screen_test.dart b/test/view/analysis/analysis_screen_test.dart index 9406e5eec2..c386d11011 100644 --- a/test/view/analysis/analysis_screen_test.dart +++ b/test/view/analysis/analysis_screen_test.dart @@ -16,8 +16,8 @@ import 'package:lichess_mobile/src/model/game/player.dart'; import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; -import 'package:lichess_mobile/src/view/pgn/pgn_tree_view.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; +import 'package:lichess_mobile/src/widgets/pgn.dart'; import '../../test_provider_scope.dart'; From bf92b0a488f9d733fd70f6f1670a457dca4e90e7 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 21 Oct 2024 15:49:30 +0200 Subject: [PATCH 523/979] Bump version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 247418133e..ecccfdb9a2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: lichess_mobile description: Lichess mobile app V2 publish_to: "none" -version: 0.12.3+001203 # see README.md for details about versioning +version: 0.12.4+001204 # see README.md for details about versioning environment: sdk: ">=3.3.0 <4.0.0" From decd1cd125b650c4e791dba8881aeb02fc00db36 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 21 Oct 2024 15:57:03 +0200 Subject: [PATCH 524/979] Fix disposed socket scheduled reconnect average lab --- lib/src/network/socket.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/network/socket.dart b/lib/src/network/socket.dart index 08e05fd402..557a3eb953 100644 --- a/lib/src/network/socket.dart +++ b/lib/src/network/socket.dart @@ -407,9 +407,9 @@ class SocketClient { void _scheduleReconnect(Duration delay) { _reconnectTimer?.cancel(); _reconnectTimer = Timer(delay, () { - _averageLag.value = Duration.zero; if (!isDisposed) { _logger.fine('Reconnecting WebSocket.'); + _averageLag.value = Duration.zero; connect(); } else { _logger.warning( From 391cc97a37eb58979fb7d042c1cc908c2918613b Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 21 Oct 2024 16:48:03 +0200 Subject: [PATCH 525/979] Fix home welcome screen --- lib/src/view/home/home_tab_screen.dart | 206 +++++++++++-------------- 1 file changed, 88 insertions(+), 118 deletions(-) diff --git a/lib/src/view/home/home_tab_screen.dart b/lib/src/view/home/home_tab_screen.dart index 9719db0f1d..80246e8d8b 100644 --- a/lib/src/view/home/home_tab_screen.dart +++ b/lib/src/view/home/home_tab_screen.dart @@ -85,25 +85,23 @@ class _HomeScreenState extends ConsumerState with RouteAware { // Show the welcome screen if there are no recent games and no stored games // (i.e. first installation, or the user has never played a game) - if (emptyRecent) { - return _WelcomeScreen( - session: session, - status: status, - isTablet: isTablet, - ); - } - - final widgets = isTablet - ? _tabletWidgets( + final widgets = emptyRecent + ? _welcomeScreenWidgets( session: session, status: status, - ongoingGames: ongoingGames, + isTablet: isTablet, ) - : _handsetWidgets( - session: session, - status: status, - ongoingGames: ongoingGames, - ); + : isTablet + ? _tabletWidgets( + session: session, + status: status, + ongoingGames: ongoingGames, + ) + : _handsetWidgets( + session: session, + status: status, + ongoingGames: ongoingGames, + ); if (Theme.of(context).platform == TargetPlatform.iOS) { return CupertinoPageScaffold( @@ -261,6 +259,80 @@ class _HomeScreenState extends ConsumerState with RouteAware { ]; } + List _welcomeScreenWidgets({ + required AuthSessionState? session, + required ConnectivityStatus status, + required bool isTablet, + }) { + final welcomeWidgets = [ + Padding( + padding: Styles.horizontalBodyPadding, + child: LichessMessage( + style: Theme.of(context).platform == TargetPlatform.iOS + ? const TextStyle(fontSize: 18) + : Theme.of(context).textTheme.bodyLarge, + textAlign: TextAlign.center, + ), + ), + const SizedBox(height: 24.0), + if (session == null) ...[ + const Center(child: _SignInWidget()), + const SizedBox(height: 16.0), + ], + if (Theme.of(context).platform != TargetPlatform.iOS && + (session == null || session.user.isPatron != true)) ...[ + Center( + child: SecondaryButton( + semanticsLabel: context.l10n.patronDonate, + onPressed: () { + launchUrl(Uri.parse('https://lichess.org/patron')); + }, + child: Text(context.l10n.patronDonate), + ), + ), + const SizedBox(height: 16.0), + ], + Center( + child: SecondaryButton( + semanticsLabel: context.l10n.aboutX('Lichess...'), + onPressed: () { + launchUrl(Uri.parse('https://lichess.org/about')); + }, + child: Text(context.l10n.aboutX('Lichess...')), + ), + ), + ]; + + return [ + if (isTablet) + Row( + children: [ + if (status.isOnline) + const Flexible( + child: _TabletCreateAGameSection(), + ), + Flexible( + child: Column( + children: welcomeWidgets, + ), + ), + ], + ) + else ...[ + if (status.isOnline) + const _EditableWidget( + widget: EnabledWidget.quickPairing, + shouldShow: true, + child: Padding( + padding: Styles.bodySectionPadding, + child: QuickGameMatrix(), + ), + ), + ...welcomeWidgets, + ], + ]; + } + List _tabletWidgets({ required AuthSessionState? session, required ConnectivityStatus status, @@ -403,108 +475,6 @@ class _EditableWidget extends ConsumerWidget { } } -class _WelcomeScreen extends StatelessWidget { - const _WelcomeScreen({ - required this.session, - required this.status, - required this.isTablet, - }); - - final AuthSessionState? session; - final ConnectivityStatus status; - final bool isTablet; - - @override - Widget build(BuildContext context) { - final welcomeWidgets = [ - Padding( - padding: Styles.horizontalBodyPadding, - child: LichessMessage( - style: Theme.of(context).platform == TargetPlatform.iOS - ? const TextStyle(fontSize: 18) - : Theme.of(context).textTheme.bodyLarge, - textAlign: TextAlign.center, - ), - ), - const SizedBox(height: 24.0), - if (session == null) ...[ - const Center(child: _SignInWidget()), - const SizedBox(height: 16.0), - ], - if (Theme.of(context).platform != TargetPlatform.iOS && - (session == null || session!.user.isPatron != true)) ...[ - Center( - child: SecondaryButton( - semanticsLabel: context.l10n.patronDonate, - onPressed: () { - launchUrl(Uri.parse('https://lichess.org/patron')); - }, - child: Text(context.l10n.patronDonate), - ), - ), - const SizedBox(height: 16.0), - ], - Center( - child: SecondaryButton( - semanticsLabel: context.l10n.aboutX('Lichess...'), - onPressed: () { - launchUrl(Uri.parse('https://lichess.org/about')); - }, - child: Text(context.l10n.aboutX('Lichess...')), - ), - ), - ]; - - final emptyScreenWidgets = [ - if (isTablet) - Row( - children: [ - if (status.isOnline) - const Flexible( - child: _TabletCreateAGameSection(), - ), - Flexible( - child: Column( - children: welcomeWidgets, - ), - ), - ], - ) - else ...[ - if (status.isOnline) - const _EditableWidget( - widget: EnabledWidget.quickPairing, - shouldShow: true, - child: Padding( - padding: Styles.bodySectionPadding, - child: QuickGameMatrix(), - ), - ), - ...welcomeWidgets, - ], - ]; - - return Theme.of(context).platform == TargetPlatform.android - ? Center( - child: ListView(shrinkWrap: true, children: emptyScreenWidgets), - ) - : SliverFillRemaining( - child: Padding( - padding: EdgeInsets.only( - bottom: MediaQuery.viewPaddingOf(context).vertical + 50.0, - ), - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: emptyScreenWidgets, - ), - ), - ), - ); - } -} - class _HelloWidget extends ConsumerWidget { const _HelloWidget(); From 5de21578f086b6e45d1741098c6deb73684809b4 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Mon, 21 Oct 2024 18:00:54 +0200 Subject: [PATCH 526/979] rename broadcast path variable --- .../model/analysis/analysis_controller.dart | 12 +++++----- lib/src/view/analysis/tree_view.dart | 5 +++-- .../broadcast/broadcast_analysis_screen.dart | 8 +++---- lib/src/widgets/pgn.dart | 22 +++++++++---------- 4 files changed, 24 insertions(+), 23 deletions(-) diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index 1c6263670a..ef6b3c44fd 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -154,7 +154,7 @@ class AnalysisController extends _$AnalysisController variant: options.variant, id: options.id, currentPath: currentPath, - livePath: options.isBroadcast && pgnHeaders['Result'] == '*' + broadcastLivePath: options.isBroadcast && pgnHeaders['Result'] == '*' ? currentPath : null, isOnMainline: _root.isOnMainline(currentPath), @@ -224,7 +224,7 @@ class AnalysisController extends _$AnalysisController final (newPath, isNewNode) = _root.addMoveAt(path, move, clock: clock); if (newPath != null) { - if (state.livePath == state.currentPath) { + if (state.broadcastLivePath == state.currentPath) { _setPath( newPath, shouldRecomputeRootView: isNewNode, @@ -233,7 +233,7 @@ class AnalysisController extends _$AnalysisController ); } else { _root.promoteAt(newPath, toMainline: true); - state = state.copyWith(livePath: newPath, root: _root.view); + state = state.copyWith(broadcastLivePath: newPath, root: _root.view); } } } @@ -490,7 +490,7 @@ class AnalysisController extends _$AnalysisController state = state.copyWith( currentPath: path, - livePath: isBroadcastMove ? path : state.livePath, + broadcastLivePath: isBroadcastMove ? path : state.broadcastLivePath, isOnMainline: _root.isOnMainline(path), currentNode: AnalysisCurrentNode.fromNode(currentNode), currentBranchOpening: opening, @@ -502,7 +502,7 @@ class AnalysisController extends _$AnalysisController } else { state = state.copyWith( currentPath: path, - livePath: isBroadcastMove ? path : state.livePath, + broadcastLivePath: isBroadcastMove ? path : state.broadcastLivePath, isOnMainline: _root.isOnMainline(path), currentNode: AnalysisCurrentNode.fromNode(currentNode), currentBranchOpening: opening, @@ -713,7 +713,7 @@ class AnalysisState with _$AnalysisState { required UciPath currentPath, // The path to the current broadcast live move. - required UciPath? livePath, + required UciPath? broadcastLivePath, /// Whether the current path is on the mainline. required bool isOnMainline, diff --git a/lib/src/view/analysis/tree_view.dart b/lib/src/view/analysis/tree_view.dart index 34d9135a7c..32f12d6616 100644 --- a/lib/src/view/analysis/tree_view.dart +++ b/lib/src/view/analysis/tree_view.dart @@ -26,7 +26,8 @@ class AnalysisTreeView extends ConsumerWidget { final root = ref.watch(ctrlProvider.select((value) => value.root)); final currentPath = ref.watch(ctrlProvider.select((value) => value.currentPath)); - final livePath = ref.watch(ctrlProvider.select((value) => value.livePath)); + final broadcastLivePath = + ref.watch(ctrlProvider.select((value) => value.broadcastLivePath)); final pgnRootComments = ref.watch(ctrlProvider.select((value) => value.pgnRootComments)); @@ -41,7 +42,7 @@ class AnalysisTreeView extends ConsumerWidget { DebouncedPgnTreeView( root: root, currentPath: currentPath, - livePath: livePath, + broadcastLivePath: broadcastLivePath, pgnRootComments: pgnRootComments, notifier: ref.read(ctrlProvider.notifier), ), diff --git a/lib/src/view/broadcast/broadcast_analysis_screen.dart b/lib/src/view/broadcast/broadcast_analysis_screen.dart index 1f76448a63..39770b92f9 100644 --- a/lib/src/view/broadcast/broadcast_analysis_screen.dart +++ b/lib/src/view/broadcast/broadcast_analysis_screen.dart @@ -394,8 +394,8 @@ class _AnalysisBoardPlayersAndClocks extends ConsumerWidget { final currentPath = ref.watch( ctrlProvider.select((value) => value.currentPath), ); - final livePath = ref.watch( - ctrlProvider.select((value) => value.livePath), + final broadcastLivePath = ref.watch( + ctrlProvider.select((value) => value.broadcastLivePath), ); final playingSide = ref.watch(ctrlProvider.select((value) => value.position.turn)); @@ -416,7 +416,7 @@ class _AnalysisBoardPlayersAndClocks extends ConsumerWidget { side: pov.opposite, boardSide: _PlayerWidgetSide.top, playingSide: playingSide, - playClock: currentPath == livePath, + playClock: currentPath == broadcastLivePath, ), AnalysisBoard( pgn, @@ -432,7 +432,7 @@ class _AnalysisBoardPlayersAndClocks extends ConsumerWidget { side: pov, boardSide: _PlayerWidgetSide.bottom, playingSide: playingSide, - playClock: currentPath == livePath, + playClock: currentPath == broadcastLivePath, ), ], ); diff --git a/lib/src/widgets/pgn.dart b/lib/src/widgets/pgn.dart index d872f4be31..172ea28e10 100644 --- a/lib/src/widgets/pgn.dart +++ b/lib/src/widgets/pgn.dart @@ -111,7 +111,7 @@ class DebouncedPgnTreeView extends ConsumerStatefulWidget { const DebouncedPgnTreeView({ required this.root, required this.currentPath, - required this.livePath, + required this.broadcastLivePath, required this.pgnRootComments, required this.notifier, }); @@ -123,7 +123,7 @@ class DebouncedPgnTreeView extends ConsumerStatefulWidget { final UciPath currentPath; /// Path to the last live move in the tree if it is a broadcast game - final UciPath? livePath; + final UciPath? broadcastLivePath; /// Comments associated with the root node final IList? pgnRootComments; @@ -143,14 +143,14 @@ class _DebouncedPgnTreeViewState extends ConsumerState { /// Path to the currently selected move in the tree. When widget.currentPath changes rapidly, we debounce the change to avoid rebuilding the whole tree on every played move. late UciPath pathToCurrentMove; - /// Path to the last live move in the tree if it is a broadcast game. When widget.livePath changes rapidly, we debounce the change to avoid rebuilding the whole tree on every received move. - late UciPath? pathToLiveMove; + /// Path to the last live move in the tree if it is a broadcast game. When widget.broadcastLivePath changes rapidly, we debounce the change to avoid rebuilding the whole tree on every received move. + late UciPath? pathToBroadcastLiveMove; @override void initState() { super.initState(); pathToCurrentMove = widget.currentPath; - pathToLiveMove = widget.livePath; + pathToBroadcastLiveMove = widget.broadcastLivePath; WidgetsBinding.instance.addPostFrameCallback((_) { if (currentMoveKey.currentContext != null) { Scrollable.ensureVisible( @@ -173,16 +173,16 @@ class _DebouncedPgnTreeViewState extends ConsumerState { super.didUpdateWidget(oldWidget); if (oldWidget.currentPath != widget.currentPath || - oldWidget.livePath != widget.livePath) { - // debouncing the current and live path changes to avoid rebuilding when using the - // fast replay buttons or when receiving a lot of broadcast moves in a short time + oldWidget.broadcastLivePath != widget.broadcastLivePath) { + // debouncing the current and broadcast live path changes to avoid rebuilding when using + // the fast replay buttons or when receiving a lot of broadcast moves in a short time _debounce(() { setState(() { if (oldWidget.currentPath != widget.currentPath) { pathToCurrentMove = widget.currentPath; } - if (oldWidget.livePath != widget.livePath) { - pathToLiveMove = widget.livePath; + if (oldWidget.broadcastLivePath != widget.broadcastLivePath) { + pathToBroadcastLiveMove = widget.broadcastLivePath; } }); if (oldWidget.currentPath != widget.currentPath) { @@ -225,7 +225,7 @@ class _DebouncedPgnTreeViewState extends ConsumerState { shouldShowComments: shouldShowComments, currentMoveKey: currentMoveKey, pathToCurrentMove: pathToCurrentMove, - pathToLiveMove: pathToLiveMove, + pathToLiveMove: pathToBroadcastLiveMove, notifier: widget.notifier, ), ); From 3526121315dd93b02e0707a53b49dfbaa0cbdbd6 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 22 Oct 2024 12:22:48 +0200 Subject: [PATCH 527/979] Add tests to home screen --- lib/src/view/home/home_tab_screen.dart | 2 +- .../{home => play}/create_game_options.dart | 1 + lib/src/view/play/play_screen.dart | 3 +- test/example_data.dart | 98 +++++ test/test_helpers.dart | 2 +- test/test_provider_scope.dart | 28 ++ test/view/home/home_tab_screen_test.dart | 345 ++++++++++++++++++ 7 files changed, 476 insertions(+), 3 deletions(-) rename lib/src/view/{home => play}/create_game_options.dart (98%) create mode 100644 test/example_data.dart create mode 100644 test/view/home/home_tab_screen_test.dart diff --git a/lib/src/view/home/home_tab_screen.dart b/lib/src/view/home/home_tab_screen.dart index 80246e8d8b..8f2b3a5569 100644 --- a/lib/src/view/home/home_tab_screen.dart +++ b/lib/src/view/home/home_tab_screen.dart @@ -22,7 +22,7 @@ import 'package:lichess_mobile/src/view/account/profile_screen.dart'; import 'package:lichess_mobile/src/view/correspondence/offline_correspondence_game_screen.dart'; import 'package:lichess_mobile/src/view/game/game_screen.dart'; import 'package:lichess_mobile/src/view/game/offline_correspondence_games_screen.dart'; -import 'package:lichess_mobile/src/view/home/create_game_options.dart'; +import 'package:lichess_mobile/src/view/play/create_game_options.dart'; import 'package:lichess_mobile/src/view/play/ongoing_games_screen.dart'; import 'package:lichess_mobile/src/view/play/play_screen.dart'; import 'package:lichess_mobile/src/view/play/quick_game_button.dart'; diff --git a/lib/src/view/home/create_game_options.dart b/lib/src/view/play/create_game_options.dart similarity index 98% rename from lib/src/view/home/create_game_options.dart rename to lib/src/view/play/create_game_options.dart index bbd676e4c1..c00a745c86 100644 --- a/lib/src/view/home/create_game_options.dart +++ b/lib/src/view/play/create_game_options.dart @@ -12,6 +12,7 @@ import 'package:lichess_mobile/src/view/play/create_custom_game_screen.dart'; import 'package:lichess_mobile/src/view/play/online_bots_screen.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; +/// A widget that displays the options for creating a game. class CreateGameOptions extends ConsumerWidget { const CreateGameOptions(); diff --git a/lib/src/view/play/play_screen.dart b/lib/src/view/play/play_screen.dart index 41c50050d0..5db3f41f9c 100644 --- a/lib/src/view/play/play_screen.dart +++ b/lib/src/view/play/play_screen.dart @@ -2,10 +2,11 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; -import 'package:lichess_mobile/src/view/home/create_game_options.dart'; import 'package:lichess_mobile/src/view/play/quick_game_button.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; +import 'create_game_options.dart'; + class PlayScreen extends StatelessWidget { const PlayScreen(); diff --git a/test/example_data.dart b/test/example_data.dart new file mode 100644 index 0000000000..91a85cd077 --- /dev/null +++ b/test/example_data.dart @@ -0,0 +1,98 @@ +import 'package:dartchess/dartchess.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/common/perf.dart'; +import 'package:lichess_mobile/src/model/common/speed.dart'; +import 'package:lichess_mobile/src/model/game/archived_game.dart'; +import 'package:lichess_mobile/src/model/game/game.dart'; +import 'package:lichess_mobile/src/model/game/game_status.dart'; +import 'package:lichess_mobile/src/model/game/material_diff.dart'; +import 'package:lichess_mobile/src/model/game/player.dart'; +import 'package:lichess_mobile/src/model/user/user.dart'; + +List generateArchivedGames({ + int count = 100, + String? username, +}) { + return List.generate(count, (index) { + final id = GameId('game${index.toString().padLeft(4, '0')}'); + final whitePlayer = Player( + user: username != null && index.isEven + ? LightUser( + id: UserId.fromUserName(username), + name: username, + ) + : username != null + ? const LightUser(id: UserId('whiteId'), name: 'White') + : null, + rating: username != null ? 1500 : null, + ); + final blackPlayer = Player( + user: username != null && index.isOdd + ? LightUser( + id: UserId.fromUserName(username), + name: username, + ) + : username != null + ? const LightUser(id: UserId('blackId'), name: 'Black') + : null, + rating: username != null ? 1500 : null, + ); + return ArchivedGame( + id: id, + meta: GameMeta( + createdAt: DateTime(2021, 1, 1), + rated: true, + perf: Perf.correspondence, + speed: Speed.correspondence, + variant: Variant.standard, + ), + source: GameSource.lobby, + data: LightArchivedGame( + id: id, + variant: Variant.standard, + lastMoveAt: DateTime(2021, 1, 1), + createdAt: DateTime(2021, 1, 1), + perf: Perf.blitz, + speed: Speed.blitz, + rated: true, + status: GameStatus.started, + white: whitePlayer, + black: blackPlayer, + clock: ( + initial: const Duration(minutes: 2), + increment: const Duration(seconds: 3), + ), + ), + steps: _makeSteps( + 'e4 Nc6 Bc4 e6 a3 g6 Nf3 Bg7 c3 Nge7 d3 O-O Be3 Na5 Ba2 b6 Qd2', + ), + status: GameStatus.started, + white: whitePlayer, + black: blackPlayer, + youAre: username != null + ? index.isEven + ? Side.white + : Side.black + : null, + ); + }); +} + +IList _makeSteps(String pgn) { + Position position = Chess.initial; + final steps = [GameStep(position: position)]; + for (final san in pgn.split(' ')) { + final move = position.parseSan(san)!; + position = position.play(move); + steps.add( + GameStep( + position: position, + sanMove: SanMove(san, move), + diff: MaterialDiff.fromBoard(position.board), + ), + ); + } + return steps.toIList(); +} diff --git a/test/test_helpers.dart b/test/test_helpers.dart index da5507f50b..867f114d3c 100644 --- a/test/test_helpers.dart +++ b/test/test_helpers.dart @@ -19,7 +19,7 @@ const kPlatformVariant = Matcher sameRequest(http.BaseRequest request) => _SameRequest(request); Matcher sameHeaders(Map headers) => _SameHeaders(headers); -/// Mocks an http response with a delay of 20ms. +/// Mocks an http response Future mockResponse( String body, int code, { diff --git a/test/test_provider_scope.dart b/test/test_provider_scope.dart index 148dfbc2e0..fc55c738e9 100644 --- a/test/test_provider_scope.dart +++ b/test/test_provider_scope.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:io'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/cupertino.dart'; @@ -41,6 +42,10 @@ final mockClient = MockClient((request) async { return http.Response('', 200); }); +final offlineClient = MockClient((request) async { + throw const SocketException('No internet'); +}); + /// Returns a [MaterialApp] wrapped in a [ProviderScope] and default mocks, ready for testing. /// /// The [home] widget is the widget we want to test. Typically a screen widget, to @@ -75,6 +80,29 @@ Future makeTestProviderScopeApp( ); } +/// Wraps [makeTestProviderScope] with a [FakeHttpClientFactory] that returns an offline client. +/// +/// This is useful to test the app in offline mode. +Future makeOfflineTestProviderScope( + WidgetTester tester, { + required Widget child, + List? overrides, + AuthSessionState? userSession, + Map? defaultPreferences, +}) => + makeTestProviderScope( + tester, + child: child, + overrides: [ + httpClientFactoryProvider.overrideWith((ref) { + return FakeHttpClientFactory(() => offlineClient); + }), + ...overrides ?? [], + ], + userSession: userSession, + defaultPreferences: defaultPreferences, + ); + /// Returns a [ProviderScope] and default mocks, ready for testing. /// /// The [child] widget is the widget we want to test. It will be wrapped in a diff --git a/test/view/home/home_tab_screen_test.dart b/test/view/home/home_tab_screen_test.dart new file mode 100644 index 0000000000..aa9a6a7ad2 --- /dev/null +++ b/test/view/home/home_tab_screen_test.dart @@ -0,0 +1,345 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/testing.dart'; +import 'package:lichess_mobile/src/app.dart'; +import 'package:lichess_mobile/src/model/game/game_storage.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/styles/lichess_icons.dart'; +import 'package:lichess_mobile/src/view/game/game_list_tile.dart'; +import 'package:lichess_mobile/src/view/play/quick_game_matrix.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/feedback.dart'; + +import '../../example_data.dart'; +import '../../mock_server_responses.dart'; +import '../../model/auth/fake_session_storage.dart'; +import '../../network/fake_http_client_factory.dart'; +import '../../test_helpers.dart'; +import '../../test_provider_scope.dart'; + +void main() { + group('Home online', () { + testWidgets('shows Play button', (tester) async { + final app = await makeTestProviderScope( + tester, + child: const Application(), + ); + await tester.pumpWidget(app); + + // wait for connectivity + expect(find.byType(CircularProgressIndicator), findsOneWidget); + await tester.pump(); + + expect(find.byType(FloatingActionButton), findsOneWidget); + }); + + testWidgets('shows players button', (tester) async { + final app = await makeTestProviderScope( + tester, + child: const Application(), + ); + await tester.pumpWidget(app); + + // wait for connectivity + expect(find.byType(CircularProgressIndicator), findsOneWidget); + await tester.pump(); + + expect( + tester + .widget( + find.ancestor( + of: find.byIcon(Icons.group_outlined), + matching: find.byType(AppBarIconButton), + ), + ) + .onPressed, + isNotNull, + ); + }); + + testWidgets('shows challenge button if has session', (tester) async { + final app = await makeTestProviderScope( + tester, + child: const Application(), + userSession: fakeSession, + ); + await tester.pumpWidget(app); + + // wait for connectivity + expect(find.byType(CircularProgressIndicator), findsOneWidget); + await tester.pump(); + + expect( + tester + .widget( + find.ancestor( + of: find.byIcon(LichessIcons.crossed_swords), + matching: find.byType(AppBarIconButton), + ), + ) + .onPressed, + isNotNull, + ); + }); + + testWidgets('shows quick pairing matrix', (tester) async { + final app = await makeTestProviderScope( + tester, + child: const Application(), + ); + await tester.pumpWidget(app); + + // wait for connectivity + expect(find.byType(CircularProgressIndicator), findsOneWidget); + await tester.pump(); + + expect(find.byType(QuickGameMatrix), findsOneWidget); + }); + + testWidgets('no session, no stored game: shows welcome screen ', + (tester) async { + final app = await makeTestProviderScope( + tester, + child: const Application(), + ); + await tester.pumpWidget(app); + // wait for connectivity + expect(find.byType(CircularProgressIndicator), findsOneWidget); + await tester.pumpAndSettle(); + + expect( + find.textContaining( + 'libre, no-ads, open source chess server.', + findRichText: true, + ), + findsOneWidget, + ); + expect(find.text('Sign in'), findsOneWidget); + expect(find.text('About Lichess...'), findsOneWidget); + }); + + testWidgets( + 'session, no played game: shows welcome screen but no sign in button', + (tester) async { + int nbUserGamesRequests = 0; + final mockClient = MockClient((request) async { + if (request.url.path == '/api/games/user/testuser') { + nbUserGamesRequests++; + return mockResponse('', 200); + } + return mockResponse('', 200); + }); + final app = await makeTestProviderScope( + tester, + child: const Application(), + userSession: fakeSession, + overrides: [ + httpClientFactoryProvider + .overrideWith((ref) => FakeHttpClientFactory(() => mockClient)), + ], + ); + await tester.pumpWidget(app); + // wait for connectivity + expect(find.byType(CircularProgressIndicator), findsOneWidget); + await tester.pumpAndSettle(); + + expect(nbUserGamesRequests, 1); + expect( + find.textContaining( + 'libre, no-ads, open source chess server.', + findRichText: true, + ), + findsOneWidget, + ); + expect(find.text('About Lichess...'), findsOneWidget); + }); + + testWidgets('no session, with stored games: shows list of recent games', + (tester) async { + final app = await makeTestProviderScope( + tester, + child: const Application(), + ); + await tester.pumpWidget(app); + + final container = ProviderScope.containerOf( + tester.element(find.byType(Application)), + ); + final storage = await container.read(gameStorageProvider.future); + final games = generateArchivedGames(count: 3); + for (final game in games) { + await storage.save(game); + } + + // wait for connectivity + await tester.pumpAndSettle(); + + expect(find.text('About Lichess...'), findsNothing); + expect(find.text('Recent games'), findsOneWidget); + expect(find.byType(GameListTile), findsNWidgets(3)); + expect(find.text('Anonymous'), findsNWidgets(3)); + }); + + testWidgets('session, with played games: shows recent games', + (tester) async { + int nbUserGamesRequests = 0; + final mockClient = MockClient((request) async { + if (request.url.path == '/api/games/user/testuser') { + nbUserGamesRequests++; + return mockResponse(mockUserRecentGameResponse('testUser'), 200); + } + return mockResponse('', 200); + }); + final app = await makeTestProviderScope( + tester, + child: const Application(), + userSession: fakeSession, + overrides: [ + httpClientFactoryProvider + .overrideWith((ref) => FakeHttpClientFactory(() => mockClient)), + ], + ); + await tester.pumpWidget(app); + // wait for connectivity + expect(find.byType(CircularProgressIndicator), findsOneWidget); + await tester.pumpAndSettle(); + + expect(nbUserGamesRequests, 1); + expect(find.text('About Lichess...'), findsNothing); + expect(find.text('Recent games'), findsOneWidget); + expect(find.byType(GameListTile), findsNWidgets(3)); + expect(find.text('MightyNanook'), findsOneWidget); + }); + }); + + group('Home offline', () { + testWidgets('shows offline banner', (tester) async { + final app = await makeOfflineTestProviderScope( + tester, + child: const Application(), + ); + + await tester.pumpWidget(app); + // wait for connectivity + expect(find.byType(CircularProgressIndicator), findsOneWidget); + await tester.pump(); + + expect(find.byType(ConnectivityBanner), findsOneWidget); + }); + + testWidgets('shows Play button', (tester) async { + final app = await makeOfflineTestProviderScope( + tester, + child: const Application(), + ); + + await tester.pumpWidget(app); + + // wait for connectivity + expect(find.byType(CircularProgressIndicator), findsOneWidget); + await tester.pump(); + + expect(find.byType(FloatingActionButton), findsOneWidget); + }); + + testWidgets('shows disabled players button', (tester) async { + final app = await makeOfflineTestProviderScope( + tester, + child: const Application(), + ); + + await tester.pumpWidget(app); + + // wait for connectivity + expect(find.byType(CircularProgressIndicator), findsOneWidget); + await tester.pump(); + + expect( + tester + .widget( + find.ancestor( + of: find.byIcon(Icons.group_outlined), + matching: find.byType(AppBarIconButton), + ), + ) + .onPressed, + isNull, + ); + }); + + testWidgets('no session, no stored game: shows welcome screen ', + (tester) async { + final app = await makeTestProviderScope( + tester, + child: const Application(), + ); + await tester.pumpWidget(app); + // wait for connectivity + expect(find.byType(CircularProgressIndicator), findsOneWidget); + await tester.pumpAndSettle(); + + expect( + find.textContaining( + 'libre, no-ads, open source chess server.', + findRichText: true, + ), + findsOneWidget, + ); + expect(find.text('Sign in'), findsOneWidget); + expect(find.text('About Lichess...'), findsOneWidget); + }); + + testWidgets('no session, with stored games: shows list of recent games', + (tester) async { + final app = await makeOfflineTestProviderScope( + tester, + child: const Application(), + ); + await tester.pumpWidget(app); + + final container = ProviderScope.containerOf( + tester.element(find.byType(Application)), + ); + final storage = await container.read(gameStorageProvider.future); + final games = generateArchivedGames(count: 3); + for (final game in games) { + await storage.save(game); + } + + // wait for connectivity + await tester.pumpAndSettle(); + + expect(find.text('About Lichess...'), findsNothing); + expect(find.text('Recent games'), findsOneWidget); + expect(find.byType(GameListTile), findsNWidgets(3)); + expect(find.text('Anonymous'), findsNWidgets(3)); + }); + + testWidgets('session, with stored games: shows list of recent games', + (tester) async { + final app = await makeOfflineTestProviderScope( + tester, + child: const Application(), + userSession: fakeSession, + ); + await tester.pumpWidget(app); + + final container = ProviderScope.containerOf( + tester.element(find.byType(Application)), + ); + final storage = await container.read(gameStorageProvider.future); + final games = generateArchivedGames(count: 3, username: 'testUser'); + for (final game in games) { + await storage.save(game); + } + + // wait for connectivity + await tester.pumpAndSettle(); + + expect(find.text('About Lichess...'), findsNothing); + expect(find.text('Recent games'), findsOneWidget); + expect(find.byType(GameListTile), findsNWidgets(3)); + }); + }); +} From e99f6b4c3c40cea9d5a8b5760c6ed895811c8a62 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 22 Oct 2024 12:24:31 +0200 Subject: [PATCH 528/979] Bump version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index ecccfdb9a2..500c8a098a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: lichess_mobile description: Lichess mobile app V2 publish_to: "none" -version: 0.12.4+001204 # see README.md for details about versioning +version: 0.12.5+001205 # see README.md for details about versioning environment: sdk: ">=3.3.0 <4.0.0" From fe37a55ac5d15d1ab6166655fafb2813f6886c86 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 22 Oct 2024 12:31:36 +0200 Subject: [PATCH 529/979] Fix challenge list item Fixes #1107 --- lib/src/view/play/challenge_list_item.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/src/view/play/challenge_list_item.dart b/lib/src/view/play/challenge_list_item.dart index f18078c1f4..1eff45195c 100644 --- a/lib/src/view/play/challenge_list_item.dart +++ b/lib/src/view/play/challenge_list_item.dart @@ -159,7 +159,9 @@ class CorrespondenceChallengeListItem extends StatelessWidget { status: ChallengeStatus.created, variant: challenge.variant, speed: Speed.correspondence, - timeControl: ChallengeTimeControlType.correspondence, + timeControl: challenge.days != null + ? ChallengeTimeControlType.correspondence + : ChallengeTimeControlType.unlimited, rated: challenge.rated, sideChoice: challenge.side == null ? SideChoice.random From 25c8fe8b97874d9bc43d6522e881b3699f310e73 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 22 Oct 2024 13:05:53 +0200 Subject: [PATCH 530/979] Fix player's rating not shown in challenge lists Fixes #1108 --- lib/src/view/play/challenge_list_item.dart | 27 +++++++++++++------ .../view/play/create_custom_game_screen.dart | 2 +- .../view/user/challenge_requests_screen.dart | 2 +- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/lib/src/view/play/challenge_list_item.dart b/lib/src/view/play/challenge_list_item.dart index 1eff45195c..cc17fb53b6 100644 --- a/lib/src/view/play/challenge_list_item.dart +++ b/lib/src/view/play/challenge_list_item.dart @@ -24,7 +24,7 @@ class ChallengeListItem extends ConsumerWidget { const ChallengeListItem({ super.key, required this.challenge, - required this.user, + required this.challengerUser, this.onPressed, this.onAccept, this.onDecline, @@ -32,7 +32,7 @@ class ChallengeListItem extends ConsumerWidget { }); final Challenge challenge; - final LightUser user; + final LightUser challengerUser; final VoidCallback? onPressed; final VoidCallback? onAccept; final VoidCallback? onCancel; @@ -41,7 +41,7 @@ class ChallengeListItem extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final me = ref.watch(authSessionProvider)?.user; - final isMyChallenge = me != null && me.id == user.id; + final isMyChallenge = me != null && me.id == challengerUser.id; final color = isMyChallenge ? context.lichessColors.good.withValues(alpha: 0.2) @@ -54,11 +54,16 @@ class ChallengeListItem extends ConsumerWidget { ? LagIndicator(lagRating: challenge.challenger!.lagRating!) : null; final title = isMyChallenge + // shows destUser if it exists, otherwise shows the challenger (me) + // if no destUser, it's an open challenge I sent ? UserFullNameWidget( - user: challenge.destUser != null ? challenge.destUser!.user : user, + user: challenge.destUser != null + ? challenge.destUser!.user + : challengerUser, + rating: challenge.destUser?.rating ?? challenge.challenger?.rating, ) : UserFullNameWidget( - user: user, + user: challengerUser, rating: challenge.challenger?.rating, ); final subtitle = Text(challenge.description(context.l10n)); @@ -141,13 +146,13 @@ class CorrespondenceChallengeListItem extends StatelessWidget { const CorrespondenceChallengeListItem({ super.key, required this.challenge, - required this.user, + required this.challengerUser, this.onPressed, this.onCancel, }); final CorrespondenceChallenge challenge; - final LightUser user; + final LightUser challengerUser; final VoidCallback? onPressed; final VoidCallback? onCancel; @@ -169,8 +174,14 @@ class CorrespondenceChallengeListItem extends StatelessWidget { ? SideChoice.white : SideChoice.black, days: challenge.days, + challenger: ( + user: challengerUser, + rating: challenge.rating, + provisionalRating: challenge.provisional, + lagRating: null, + ), ), - user: user, + challengerUser: challengerUser, onPressed: onPressed, onCancel: onCancel, ); diff --git a/lib/src/view/play/create_custom_game_screen.dart b/lib/src/view/play/create_custom_game_screen.dart index e885f19b37..7840de19b6 100644 --- a/lib/src/view/play/create_custom_game_screen.dart +++ b/lib/src/view/play/create_custom_game_screen.dart @@ -236,7 +236,7 @@ class _ChallengesBodyState extends ConsumerState<_ChallengesBody> { return CorrespondenceChallengeListItem( challenge: challenge, - user: LightUser( + challengerUser: LightUser( id: UserId.fromUserName(challenge.username), name: challenge.username, title: challenge.title, diff --git a/lib/src/view/user/challenge_requests_screen.dart b/lib/src/view/user/challenge_requests_screen.dart index 98ecaea174..f00783d2eb 100644 --- a/lib/src/view/user/challenge_requests_screen.dart +++ b/lib/src/view/user/challenge_requests_screen.dart @@ -121,7 +121,7 @@ class _Body extends ConsumerWidget { return ChallengeListItem( challenge: challenge, - user: user, + challengerUser: user, onPressed: challenge.direction == ChallengeDirection.inward ? session == null ? showMissingAccountMessage From dce275c15a4df3bb1ded7d202722bbafe267bc4c Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 22 Oct 2024 16:06:47 +0200 Subject: [PATCH 531/979] Sort saved puzzle batches by last modified Closes #1091 --- lib/src/db/database.dart | 28 +++++- .../model/puzzle/puzzle_batch_storage.dart | 37 ++++++++ lib/src/model/puzzle/puzzle_providers.dart | 11 +++ lib/src/view/puzzle/puzzle_tab_screen.dart | 49 ++++------- lib/src/widgets/list.dart | 10 +++ .../puzzle/puzzle_batch_storage_test.dart | 88 +++++++++++++++++++ 6 files changed, 187 insertions(+), 36 deletions(-) diff --git a/lib/src/db/database.dart b/lib/src/db/database.dart index 848cd777dd..f88489f2fc 100644 --- a/lib/src/db/database.dart +++ b/lib/src/db/database.dart @@ -60,7 +60,7 @@ Future openAppDatabase(DatabaseFactory dbFactory, String path) async { return dbFactory.openDatabase( path, options: OpenDatabaseOptions( - version: 2, + version: 3, onConfigure: (db) async { final version = await _getDatabaseVersion(db); _logger.info('SQLite version: $version'); @@ -79,7 +79,7 @@ Future openAppDatabase(DatabaseFactory dbFactory, String path) async { }, onCreate: (db, version) async { final batch = db.batch(); - _createPuzzleBatchTableV1(batch); + _createPuzzleBatchTableV3(batch); _createPuzzleTableV1(batch); _createCorrespondenceGameTableV1(batch); _createChatReadMessagesTableV1(batch); @@ -91,6 +91,9 @@ Future openAppDatabase(DatabaseFactory dbFactory, String path) async { if (oldVersion == 1) { _createGameTableV2(batch); } + if (oldVersion < 3) { + _updatePuzzleBatchTableToV3(batch); + } await batch.commit(); }, onDowngrade: onDatabaseDowngradeDelete, @@ -98,16 +101,35 @@ Future openAppDatabase(DatabaseFactory dbFactory, String path) async { ); } -void _createPuzzleBatchTableV1(Batch batch) { +void _createPuzzleBatchTableV3(Batch batch) { batch.execute('DROP TABLE IF EXISTS puzzle_batchs'); batch.execute(''' CREATE TABLE puzzle_batchs( userId TEXT NOT NULL, angle TEXT NOT NULL, + lastModified TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + data TEXT NOT NULL, + PRIMARY KEY (userId, angle) + ) + '''); +} + +void _updatePuzzleBatchTableToV3(Batch batch) { + batch.execute(''' + CREATE TABLE puzzle_batchs_new( + userId TEXT NOT NULL, + angle TEXT NOT NULL, + lastModified TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, data TEXT NOT NULL, PRIMARY KEY (userId, angle) ) '''); + batch.execute(''' + INSERT INTO puzzle_batchs_new(userId, angle, data) + SELECT userId, angle, data FROM puzzle_batchs + '''); + batch.execute('DROP TABLE puzzle_batchs'); + batch.execute('ALTER TABLE puzzle_batchs_new RENAME TO puzzle_batchs'); } void _createPuzzleTableV1(Batch batch) { diff --git a/lib/src/model/puzzle/puzzle_batch_storage.dart b/lib/src/model/puzzle/puzzle_batch_storage.dart index dca6be7e5e..df9cad3a1a 100644 --- a/lib/src/model/puzzle/puzzle_batch_storage.dart +++ b/lib/src/model/puzzle/puzzle_batch_storage.dart @@ -96,6 +96,43 @@ class PuzzleBatchStorage { _ref.invalidateSelf(); } + /// Fetches all saved puzzles batches (except mix) for the given user. + Future> fetchAll({ + required UserId? userId, + }) async { + final list = await _db.query( + _tableName, + where: 'userId = ?', + whereArgs: [ + userId ?? _anonUserKey, + ], + orderBy: 'lastModified DESC', + ); + return list + .map((entry) { + final angleStr = entry['angle'] as String?; + final raw = entry['data'] as String?; + + if (angleStr == null || raw == null) return null; + + final angle = PuzzleAngle.fromKey(angleStr); + + if (angle == const PuzzleTheme(PuzzleThemeKey.mix)) return null; + + final json = jsonDecode(raw); + if (json is! Map) { + throw const FormatException( + '[PuzzleBatchStorage] cannot fetch puzzles: expected an object', + ); + } + final data = PuzzleBatch.fromJson(json); + final count = data.unsolved.length; + return (angle, count); + }) + .nonNulls + .toIList(); + } + Future> fetchSavedThemes({ required UserId? userId, }) async { diff --git a/lib/src/model/puzzle/puzzle_providers.dart b/lib/src/model/puzzle/puzzle_providers.dart index 8487c90ce2..5c8a7ddbc3 100644 --- a/lib/src/model/puzzle/puzzle_providers.dart +++ b/lib/src/model/puzzle/puzzle_providers.dart @@ -13,6 +13,7 @@ import 'package:lichess_mobile/src/model/puzzle/puzzle_storage.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_theme.dart'; import 'package:lichess_mobile/src/model/puzzle/storm.dart'; import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/utils/riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'puzzle_providers.g.dart'; @@ -26,6 +27,9 @@ Future nextPuzzle( final puzzleService = await ref.read(puzzleServiceFactoryProvider)( queueLength: kPuzzleLocalQueueLength, ); + // useful for for preview puzzle list in puzzle tab (providers in a list can + // be invalidated multiple times when the user scrolls the list) + ref.cacheFor(const Duration(minutes: 1)); return puzzleService.nextPuzzle( userId: session?.user.id, @@ -60,6 +64,13 @@ Future dailyPuzzle(DailyPuzzleRef ref) { ); } +@riverpod +Future> savedBatches(SavedBatchesRef ref) async { + final session = ref.watch(authSessionProvider); + final storage = await ref.watch(puzzleBatchStorageProvider.future); + return storage.fetchAll(userId: session?.user.id); +} + @riverpod Future> savedThemeBatches( SavedThemeBatchesRef ref, diff --git a/lib/src/view/puzzle/puzzle_tab_screen.dart b/lib/src/view/puzzle/puzzle_tab_screen.dart index 1fb80bbc4c..4a8262283e 100644 --- a/lib/src/view/puzzle/puzzle_tab_screen.dart +++ b/lib/src/view/puzzle/puzzle_tab_screen.dart @@ -38,36 +38,21 @@ import 'streak_screen.dart'; const _kNumberOfHistoryItemsOnHandset = 8; const _kNumberOfHistoryItemsOnTablet = 16; -final savedAnglesProvider = - FutureProvider.autoDispose>((ref) async { - final savedThemes = await ref.watch(savedThemeBatchesProvider.future); - final savedOpenings = await ref.watch(savedOpeningBatchesProvider.future); - return IMap.fromEntries([ - ...savedThemes - .remove(PuzzleThemeKey.mix) - .map((themeKey, v) => MapEntry(PuzzleTheme(themeKey), v)) - .entries, - ...savedOpenings - .map((openingKey, v) => MapEntry(PuzzleOpening(openingKey), v)) - .entries, - ]); -}); - class PuzzleTabScreen extends ConsumerWidget { const PuzzleTabScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final savedAngles = ref.watch(savedAnglesProvider).valueOrNull; + final savedBatches = ref.watch(savedBatchesProvider).valueOrNull; - if (savedAngles == null) { + if (savedBatches == null) { return const Center(child: CircularProgressIndicator()); } if (Theme.of(context).platform == TargetPlatform.iOS) { - return _CupertinoTabBody(savedAngles); + return _CupertinoTabBody(savedBatches); } else { - return _MaterialTabBody(savedAngles); + return _MaterialTabBody(savedBatches); } } } @@ -137,9 +122,9 @@ Widget _buildMainListRemovedItem( // display the main body list for cupertino devices, as a workaround // for missing type to handle both [SliverAnimatedList] and [AnimatedList]. class _CupertinoTabBody extends ConsumerStatefulWidget { - const _CupertinoTabBody(this.savedAngles); + const _CupertinoTabBody(this.savedBatches); - final IMap savedAngles; + final IList<(PuzzleAngle, int)> savedBatches; @override ConsumerState<_CupertinoTabBody> createState() => _CupertinoTabBodyState(); @@ -156,7 +141,7 @@ class _CupertinoTabBodyState extends ConsumerState<_CupertinoTabBody> { _angles = SliverAnimatedListModel( listKey: _listKey, removedItemBuilder: _buildMainListRemovedItem, - initialItems: widget.savedAngles.keys, + initialItems: widget.savedBatches.map((e) => e.$1), itemsOffset: 4, ); } @@ -164,8 +149,8 @@ class _CupertinoTabBodyState extends ConsumerState<_CupertinoTabBody> { @override void didUpdateWidget(covariant _CupertinoTabBody oldWidget) { super.didUpdateWidget(oldWidget); - final oldKeys = oldWidget.savedAngles.toKeyISet(); - final newKeys = widget.savedAngles.toKeyISet(); + final oldKeys = ISet(oldWidget.savedBatches.map((e) => e.$1)); + final newKeys = ISet(widget.savedBatches.map((e) => e.$1)); if (oldKeys != newKeys) { final missings = oldKeys.difference(newKeys); @@ -181,8 +166,7 @@ class _CupertinoTabBodyState extends ConsumerState<_CupertinoTabBody> { final additions = newKeys.difference(oldKeys); if (additions.isNotEmpty) { for (final addition in additions) { - final index = _angles.length; - _angles.insert(index, addition); + _angles.prepend(addition); } } } @@ -299,9 +283,9 @@ class _CupertinoTabBodyState extends ConsumerState<_CupertinoTabBody> { } class _MaterialTabBody extends ConsumerStatefulWidget { - const _MaterialTabBody(this.savedAngles); + const _MaterialTabBody(this.savedBatches); - final IMap savedAngles; + final IList<(PuzzleAngle, int)> savedBatches; @override ConsumerState<_MaterialTabBody> createState() => _MaterialTabBodyState(); @@ -317,7 +301,7 @@ class _MaterialTabBodyState extends ConsumerState<_MaterialTabBody> { _angles = AnimatedListModel( listKey: _listKey, removedItemBuilder: _buildMainListRemovedItem, - initialItems: widget.savedAngles.keys, + initialItems: widget.savedBatches.map((e) => e.$1), itemsOffset: 4, ); } @@ -325,8 +309,8 @@ class _MaterialTabBodyState extends ConsumerState<_MaterialTabBody> { @override void didUpdateWidget(covariant _MaterialTabBody oldWidget) { super.didUpdateWidget(oldWidget); - final oldKeys = oldWidget.savedAngles.toKeyISet(); - final newKeys = widget.savedAngles.toKeyISet(); + final oldKeys = ISet(oldWidget.savedBatches.map((e) => e.$1)); + final newKeys = ISet(widget.savedBatches.map((e) => e.$1)); if (oldKeys != newKeys) { final missings = oldKeys.difference(newKeys); @@ -342,8 +326,7 @@ class _MaterialTabBodyState extends ConsumerState<_MaterialTabBody> { final additions = newKeys.difference(oldKeys); if (additions.isNotEmpty) { for (final addition in additions) { - final index = _angles.length; - _angles.insert(index, addition); + _angles.prepend(addition); } } } diff --git a/lib/src/widgets/list.dart b/lib/src/widgets/list.dart index 69cba26fad..2b93b4f15a 100644 --- a/lib/src/widgets/list.dart +++ b/lib/src/widgets/list.dart @@ -444,6 +444,11 @@ class AnimatedListModel { AnimatedListState? get _animatedList => listKey.currentState; + void prepend(E item) { + _items.insert(0, item); + _animatedList!.insertItem(itemsOffset); + } + void insert(int index, E item) { _items.insert(index - itemsOffset, item); _animatedList!.insertItem(index); @@ -489,6 +494,11 @@ class SliverAnimatedListModel { SliverAnimatedListState? get _animatedList => listKey.currentState; + void prepend(E item) { + _items.insert(0, item); + _animatedList!.insertItem(itemsOffset); + } + void insert(int index, E item) { _items.insert(index - itemsOffset, item); _animatedList!.insertItem(index); diff --git a/test/model/puzzle/puzzle_batch_storage_test.dart b/test/model/puzzle/puzzle_batch_storage_test.dart index 623304a96b..0f493f97be 100644 --- a/test/model/puzzle/puzzle_batch_storage_test.dart +++ b/test/model/puzzle/puzzle_batch_storage_test.dart @@ -1,12 +1,16 @@ +import 'dart:convert'; + import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:lichess_mobile/src/db/database.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_angle.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_batch_storage.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_theme.dart'; +import 'package:sqflite/sqflite.dart'; import '../../test_container.dart'; @@ -66,6 +70,90 @@ void main() { ), ); }); + + test('fetchSavedOpenings', () async { + final container = await makeContainer(); + + final storage = await container.read(puzzleBatchStorageProvider.future); + + await storage.save( + userId: null, + angle: const PuzzleOpening('test_opening'), + data: data, + ); + await storage.save( + userId: null, + angle: const PuzzleOpening('test_opening2'), + data: data, + ); + + expect( + storage.fetchSavedOpenings(userId: null), + completion( + equals( + IMap(const { + 'test_opening': 1, + 'test_opening2': 1, + }), + ), + ), + ); + }); + + test('fetchAll', () async { + final container = await makeContainer(); + + final database = await container.read(databaseProvider.future); + final storage = await container.read(puzzleBatchStorageProvider.future); + + Future save(PuzzleAngle angle, PuzzleBatch data, String timestamp) { + return database.insert( + 'puzzle_batchs', + { + 'userId': '**anon**', + 'angle': angle.key, + 'data': jsonEncode(data.toJson()), + 'lastModified': timestamp, + }, + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } + + await save( + const PuzzleTheme(PuzzleThemeKey.rookEndgame), + data, + '2021-01-02T00:00:00Z', + ); + await save( + const PuzzleTheme(PuzzleThemeKey.doubleBishopMate), + data, + '2021-01-03T00:00:00Z', + ); + await save( + const PuzzleOpening('test_opening'), + data, + '2021-01-04T00:00:00Z', + ); + await save( + const PuzzleOpening('test_opening2'), + data, + '2021-01-04T80:00:00Z', + ); + + expect( + storage.fetchAll(userId: null), + completion( + equals( + [ + const PuzzleOpening('test_opening2'), + const PuzzleOpening('test_opening'), + const PuzzleTheme(PuzzleThemeKey.doubleBishopMate), + const PuzzleTheme(PuzzleThemeKey.rookEndgame), + ].map((angle) => (angle, 1)).toIList(), + ), + ), + ); + }); }); } From adf81f145a8e7c0d6045d69fec23ebb4dcc70626 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 22 Oct 2024 16:51:13 +0200 Subject: [PATCH 532/979] Add a test for puzzle batch deletion --- test/view/puzzle/puzzle_tab_screen_test.dart | 193 ++++++++++++------- 1 file changed, 119 insertions(+), 74 deletions(-) diff --git a/test/view/puzzle/puzzle_tab_screen_test.dart b/test/view/puzzle/puzzle_tab_screen_test.dart index f900bf50b6..051a41fd81 100644 --- a/test/view/puzzle/puzzle_tab_screen_test.dart +++ b/test/view/puzzle/puzzle_tab_screen_test.dart @@ -1,13 +1,22 @@ +import 'dart:convert'; + import 'package:chessground/chessground.dart'; +import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; +import 'package:lichess_mobile/src/db/database.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/common/perf.dart'; +import 'package:lichess_mobile/src/model/puzzle/puzzle.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_angle.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_batch_storage.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_theme.dart'; import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/view/puzzle/puzzle_tab_screen.dart'; import 'package:mocktail/mocktail.dart'; +import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import '../../model/puzzle/mock_server_responses.dart'; import '../../network/fake_http_client_factory.dart'; @@ -45,17 +54,11 @@ void main() { angle: const PuzzleTheme(PuzzleThemeKey.mix), ), ).thenAnswer((_) async => batch); - - when( - () => mockBatchStorage.fetchSavedThemes( - userId: null, - ), - ).thenAnswer((_) async => const IMapConst({})); when( - () => mockBatchStorage.fetchSavedOpenings( + () => mockBatchStorage.fetchAll( userId: null, ), - ).thenAnswer((_) async => const IMapConst({})); + ).thenAnswer((_) async => IList(const [])); final app = await makeTestProviderScopeApp( tester, @@ -90,15 +93,10 @@ void main() { ), ).thenAnswer((_) async => batch); when( - () => mockBatchStorage.fetchSavedThemes( + () => mockBatchStorage.fetchAll( userId: null, ), - ).thenAnswer((_) async => const IMapConst({})); - when( - () => mockBatchStorage.fetchSavedOpenings( - userId: null, - ), - ).thenAnswer((_) async => const IMapConst({})); + ).thenAnswer((_) async => IList(const [])); final app = await makeTestProviderScopeApp( tester, home: const PuzzleTabScreen(), @@ -128,16 +126,10 @@ void main() { ), ).thenAnswer((_) async => batch); when( - () => mockBatchStorage.fetchSavedThemes( - userId: null, - ), - ).thenAnswer((_) async => const IMapConst({})); - when( - () => mockBatchStorage.fetchSavedOpenings( + () => mockBatchStorage.fetchAll( userId: null, ), - ).thenAnswer((_) async => const IMapConst({})); - + ).thenAnswer((_) async => IList(const [])); final app = await makeTestProviderScopeApp( tester, home: const PuzzleTabScreen(), @@ -179,15 +171,10 @@ void main() { ), ).thenAnswer((_) async => batch); when( - () => mockBatchStorage.fetchSavedThemes( + () => mockBatchStorage.fetchAll( userId: null, ), - ).thenAnswer((_) async => const IMapConst({})); - when( - () => mockBatchStorage.fetchSavedOpenings( - userId: null, - ), - ).thenAnswer((_) async => const IMapConst({})); + ).thenAnswer((_) async => IList(const [])); final app = await makeTestProviderScopeApp( tester, @@ -228,8 +215,7 @@ void main() { ); }); - testWidgets('shows saved puzzle theme batches', - (WidgetTester tester) async { + testWidgets('shows saved puzzle batches', (WidgetTester tester) async { when( () => mockBatchStorage.fetch( userId: null, @@ -243,19 +229,21 @@ void main() { ), ).thenAnswer((_) async => batch); when( - () => mockBatchStorage.fetchSavedThemes( + () => mockBatchStorage.fetch( userId: null, + angle: const PuzzleOpening('A00'), ), - ).thenAnswer( - (_) async => const IMapConst({ - PuzzleThemeKey.advancedPawn: 50, - }), - ); + ).thenAnswer((_) async => batch); when( - () => mockBatchStorage.fetchSavedOpenings( + () => mockBatchStorage.fetchAll( userId: null, ), - ).thenAnswer((_) async => const IMapConst({})); + ).thenAnswer( + (_) async => IList(const [ + (PuzzleTheme(PuzzleThemeKey.advancedPawn), 50), + (PuzzleOpening('A00'), 50), + ]), + ); final app = await makeTestProviderScopeApp( tester, @@ -276,57 +264,62 @@ void main() { // wait for the puzzles to load await tester.pump(const Duration(milliseconds: 100)); - expect(find.byType(PuzzleAnglePreview), findsNWidgets(2)); + await tester.scrollUntilVisible( + find.widgetWithText(PuzzleAnglePreview, 'A00'), + 200, + ); + expect(find.byType(PuzzleAnglePreview), findsNWidgets(3)); expect( find.widgetWithText(PuzzleAnglePreview, 'Healthy mix'), findsOneWidget, ); - expect( find.widgetWithText(PuzzleAnglePreview, 'Advanced pawn'), findsOneWidget, ); + expect( + find.widgetWithText(PuzzleAnglePreview, 'A00'), + findsOneWidget, + ); }); - testWidgets('shows saved puzzle openings batches', - (WidgetTester tester) async { - when( - () => mockBatchStorage.fetch( - userId: null, - angle: const PuzzleTheme(PuzzleThemeKey.mix), - ), - ).thenAnswer((_) async => batch); - when( - () => mockBatchStorage.fetch( - userId: null, - angle: const PuzzleOpening('A00'), - ), - ).thenAnswer((_) async => batch); - when( - () => mockBatchStorage.fetchSavedThemes( - userId: null, - ), - ).thenAnswer( - (_) async => const IMapConst({}), + testWidgets('delete a saved puzzle batch', (WidgetTester tester) async { + final testDb = await openAppDatabase( + databaseFactoryFfiNoIsolate, + inMemoryDatabasePath, ); - when( - () => mockBatchStorage.fetchSavedOpenings( - userId: null, + + for (final (angle, timestamp) in [ + (const PuzzleTheme(PuzzleThemeKey.mix), '2021-01-01T00:00:00Z'), + ( + const PuzzleTheme(PuzzleThemeKey.advancedPawn), + '2021-01-01T00:00:00Z' ), - ).thenAnswer( - (_) async => const IMapConst({ - 'A00': 50, - }), - ); + (const PuzzleOpening('A00'), '2021-01-02T00:00:00Z'), + ]) { + await testDb.insert( + 'puzzle_batchs', + { + 'userId': '**anon**', + 'angle': angle.key, + 'data': jsonEncode(onePuzzleBatch.toJson()), + 'lastModified': timestamp, + }, + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } final app = await makeTestProviderScopeApp( tester, home: const PuzzleTabScreen(), overrides: [ - puzzleBatchStorageProvider.overrideWith((ref) => mockBatchStorage), httpClientFactoryProvider.overrideWith((ref) { return FakeHttpClientFactory(() => mockClient); }), + databaseProvider.overrideWith((ref) async { + ref.onDispose(testDb.close); + return testDb; + }), ], ); @@ -338,16 +331,68 @@ void main() { // wait for the puzzles to load await tester.pump(const Duration(milliseconds: 100)); - expect(find.byType(PuzzleAnglePreview), findsNWidgets(2)); + await tester.scrollUntilVisible( + find.widgetWithText(PuzzleAnglePreview, 'Advanced pawn'), + 200, + ); + + expect(find.byType(PuzzleAnglePreview), findsNWidgets(3)); + + await tester.drag( + find.descendant( + of: find.widgetWithText(PuzzleAnglePreview, 'A00'), + matching: find.byType(Slidable), + ), + const Offset(-150, 0), + ); + await tester.pumpAndSettle(); + expect( - find.widgetWithText(PuzzleAnglePreview, 'Healthy mix'), + find.widgetWithText(SlidableAction, 'Delete'), findsOneWidget, ); + await tester.tap(find.widgetWithText(SlidableAction, 'Delete')); + + await tester.pumpAndSettle(); + expect( find.widgetWithText(PuzzleAnglePreview, 'A00'), - findsOneWidget, + findsNothing, ); }); }); } + +final onePuzzleBatch = PuzzleBatch( + solved: IList(const [ + PuzzleSolution(id: PuzzleId('pId'), win: true, rated: true), + PuzzleSolution(id: PuzzleId('pId2'), win: false, rated: true), + ]), + unsolved: IList([ + Puzzle( + puzzle: PuzzleData( + id: const PuzzleId('pId3'), + rating: 1988, + plays: 5, + initialPly: 23, + solution: IList(const ['a6a7', 'b2a2', 'c4c2']), + themes: ISet(const ['endgame', 'advantage']), + ), + game: const PuzzleGame( + id: GameId('PrlkCqOv'), + perf: Perf.blitz, + rated: true, + white: PuzzleGamePlayer( + side: Side.white, + name: 'user1', + ), + black: PuzzleGamePlayer( + side: Side.black, + name: 'user2', + ), + pgn: 'e4 Nc6 Bc4 e6 a3 g6 Nf3 Bg7 c3 Nge7 d3 O-O Be3 Na5 Ba2 b6 Qd2', + ), + ), + ]), +); From fe861af56c6d6de4c5fa0ede3d74020751855c75 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 22 Oct 2024 17:03:55 +0200 Subject: [PATCH 533/979] Upgrade dependencies --- pubspec.lock | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 5705607cf8..a4cad0cfab 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -604,10 +604,10 @@ packages: dependency: "direct main" description: name: flutter_riverpod - sha256: "6eda4e247774474c715a0805a2fb8e3cd55fbae4ead641e063c95b4bd5f3b317" + sha256: "9532ee6db4a943a1ed8383072a2e3eeda041db5657cdf6d2acecf3c21ecbe7e1" url: "https://pub.dev" source: hosted - version: "2.6.0" + version: "2.6.1" flutter_secure_storage: dependency: "direct main" description: @@ -1127,42 +1127,42 @@ packages: dependency: transitive description: name: riverpod - sha256: bd6e656a764e3d27f211975626e0c4f9b8d06ab16acf3c7ba7a8061e09744c75 + sha256: "59062512288d3056b2321804332a13ffdd1bf16df70dcc8e506e411280a72959" url: "https://pub.dev" source: hosted - version: "2.6.0" + version: "2.6.1" riverpod_analyzer_utils: dependency: transitive description: name: riverpod_analyzer_utils - sha256: ac28d7bc678471ec986b42d88e5a0893513382ff7542c7ac9634463b044ac72c + sha256: "0dcb0af32d561f8fa000c6a6d95633c9fb08ea8a8df46e3f9daca59f11218167" url: "https://pub.dev" source: hosted - version: "0.5.4" + version: "0.5.6" riverpod_annotation: dependency: "direct main" description: name: riverpod_annotation - sha256: "1e61f8e7fc360f75e3520bbb35d22213ef542dc0de574cf90d639a358965d743" + sha256: e14b0bf45b71326654e2705d462f21b958f987087be850afd60578fcd502d1b8 url: "https://pub.dev" source: hosted - version: "2.6.0" + version: "2.6.1" riverpod_generator: dependency: "direct dev" description: name: riverpod_generator - sha256: "63311e361ffc578d655dfc31b48dfa4ed3bc76fd06f9be845e9bf97c5c11a429" + sha256: "851aedac7ad52693d12af3bf6d92b1626d516ed6b764eb61bf19e968b5e0b931" url: "https://pub.dev" source: hosted - version: "2.4.3" + version: "2.6.1" riverpod_lint: dependency: "direct dev" description: name: riverpod_lint - sha256: a35a92f2c2a4b7a5d95671c96c5432b42c20f26bb3e985e83d0b186471b61a85 + sha256: "0684c21a9a4582c28c897d55c7b611fa59a351579061b43f8c92c005804e63a8" url: "https://pub.dev" source: hosted - version: "2.3.13" + version: "2.6.1" rxdart: dependency: transitive description: @@ -1364,10 +1364,10 @@ packages: dependency: transitive description: name: sqlite3 - sha256: "45f168ae2213201b54e09429ed0c593dc2c88c924a1488d6f9c523a255d567cb" + sha256: bb174b3ec2527f9c5f680f73a89af8149dd99782fbb56ea88ad0807c5638f2ed url: "https://pub.dev" source: hosted - version: "2.4.6" + version: "2.4.7" stack_trace: dependency: transitive description: @@ -1653,10 +1653,10 @@ packages: dependency: transitive description: name: win32 - sha256: "2735daae5150e8b1dfeb3eb0544b4d3af0061e9e82cef063adcd583bdae4306a" + sha256: e1d0cc62e65dc2561f5071fcbccecf58ff20c344f8f3dc7d4922df372a11df1f url: "https://pub.dev" source: hosted - version: "5.7.0" + version: "5.7.1" win32_registry: dependency: transitive description: From f02368c1747e6685789f11618e8a4523d5f273c8 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 22 Oct 2024 19:28:49 +0200 Subject: [PATCH 534/979] Fix riverpod deprecation warnings --- lib/src/db/database.dart | 7 ++-- lib/src/db/openings_database.dart | 3 +- lib/src/model/account/account_repository.dart | 9 ++--- lib/src/model/analysis/opening_service.dart | 5 +-- .../analysis/server_analysis_service.dart | 5 +-- lib/src/model/auth/auth_repository.dart | 3 +- lib/src/model/auth/session_storage.dart | 3 +- .../model/challenge/challenge_repository.dart | 3 +- .../model/challenge/challenge_service.dart | 5 +-- lib/src/model/common/preloaded_data.dart | 3 +- .../model/common/service/move_feedback.dart | 5 +-- .../model/common/service/sound_service.dart | 5 +-- .../correspondence_game_storage.dart | 9 +++-- .../correspondence_service.dart | 5 +-- lib/src/model/engine/evaluation_service.dart | 5 +-- lib/src/model/game/game_history.dart | 9 +++-- .../model/game/game_repository_providers.dart | 8 ++--- lib/src/model/game/game_share_service.dart | 5 +-- lib/src/model/game/game_storage.dart | 5 ++- lib/src/model/lobby/create_game_service.dart | 5 +-- lib/src/model/lobby/lobby_repository.dart | 5 ++- .../notifications/notification_service.dart | 6 ++-- .../model/puzzle/puzzle_batch_storage.dart | 5 +-- lib/src/model/puzzle/puzzle_opening.dart | 7 ++-- lib/src/model/puzzle/puzzle_providers.dart | 36 ++++++++----------- lib/src/model/puzzle/puzzle_service.dart | 9 ++--- lib/src/model/puzzle/puzzle_storage.dart | 3 +- lib/src/model/puzzle/puzzle_theme.dart | 5 ++- .../relation_repository_providers.dart | 3 +- .../model/user/user_repository_providers.dart | 30 ++++++---------- lib/src/network/connectivity.dart | 3 +- lib/src/network/http.dart | 11 +++--- lib/src/network/socket.dart | 9 +++-- .../game/archived_game_screen_providers.dart | 5 +-- lib/src/view/game/game_screen_providers.dart | 16 +++------ 35 files changed, 124 insertions(+), 136 deletions(-) diff --git a/lib/src/db/database.dart b/lib/src/db/database.dart index f88489f2fc..a5e17f389a 100644 --- a/lib/src/db/database.dart +++ b/lib/src/db/database.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:io'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:logging/logging.dart'; import 'package:path/path.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -20,14 +21,14 @@ const kStorageAnonId = '**anonymous**'; final _logger = Logger('Database'); @Riverpod(keepAlive: true) -Future database(DatabaseRef ref) async { +Future database(Ref ref) async { final dbPath = join(await getDatabasesPath(), kLichessDatabaseName); return openAppDatabase(databaseFactory, dbPath); } /// Returns the sqlite version as an integer. @Riverpod(keepAlive: true) -Future sqliteVersion(SqliteVersionRef ref) async { +Future sqliteVersion(Ref ref) async { final db = await ref.read(databaseProvider.future); return _getDatabaseVersion(db); } @@ -48,7 +49,7 @@ Future _getDatabaseVersion(Database db) async { } @Riverpod(keepAlive: true) -Future getDbSizeInBytes(GetDbSizeInBytesRef ref) async { +Future getDbSizeInBytes(Ref ref) async { final dbPath = join(await getDatabasesPath(), kLichessDatabaseName); final dbFile = File(dbPath); diff --git a/lib/src/db/openings_database.dart b/lib/src/db/openings_database.dart index 053591386f..5b0e72aa3c 100644 --- a/lib/src/db/openings_database.dart +++ b/lib/src/db/openings_database.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:path/path.dart' as p; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:sqflite/sqflite.dart'; @@ -20,7 +21,7 @@ const _kDatabaseVersion = 2; const _kDatabaseName = 'chess_openings$_kDatabaseVersion.db'; @Riverpod(keepAlive: true) -Future openingsDatabase(OpeningsDatabaseRef ref) async { +Future openingsDatabase(Ref ref) async { final dbPath = p.join(await getDatabasesPath(), _kDatabaseName); return _openDb(dbPath); } diff --git a/lib/src/model/account/account_repository.dart b/lib/src/model/account/account_repository.dart index be4f276dc3..3a6c83c7f6 100644 --- a/lib/src/model/account/account_repository.dart +++ b/lib/src/model/account/account_repository.dart @@ -1,5 +1,6 @@ import 'package:deep_pick/deep_pick.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:http/http.dart' as http; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; @@ -18,7 +19,7 @@ import 'ongoing_game.dart'; part 'account_repository.g.dart'; @riverpod -Future account(AccountRef ref) async { +Future account(Ref ref) async { final session = ref.watch(authSessionProvider); if (session == null) return null; @@ -29,12 +30,12 @@ Future account(AccountRef ref) async { } @riverpod -Future accountUser(AccountUserRef ref) async { +Future accountUser(Ref ref) async { return ref.watch(accountProvider.selectAsync((user) => user?.lightUser)); } @riverpod -Future> accountActivity(AccountActivityRef ref) async { +Future> accountActivity(Ref ref) async { final session = ref.watch(authSessionProvider); if (session == null) return IList(); return ref.withClientCacheFor( @@ -44,7 +45,7 @@ Future> accountActivity(AccountActivityRef ref) async { } @riverpod -Future> ongoingGames(OngoingGamesRef ref) async { +Future> ongoingGames(Ref ref) async { final session = ref.watch(authSessionProvider); if (session == null) return IList(); diff --git a/lib/src/model/analysis/opening_service.dart b/lib/src/model/analysis/opening_service.dart index ea81058abe..3d1f67a739 100644 --- a/lib/src/model/analysis/opening_service.dart +++ b/lib/src/model/analysis/opening_service.dart @@ -1,5 +1,6 @@ import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/db/openings_database.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -15,14 +16,14 @@ const kOpeningAllowedVariants = ISetConst({ }); @Riverpod(keepAlive: true) -OpeningService openingService(OpeningServiceRef ref) { +OpeningService openingService(Ref ref) { return OpeningService(ref); } class OpeningService { OpeningService(this._ref); - final OpeningServiceRef _ref; + final Ref _ref; Future get _db => _ref.read(openingsDatabaseProvider.future); diff --git a/lib/src/model/analysis/server_analysis_service.dart b/lib/src/model/analysis/server_analysis_service.dart index 861eedebd0..94665dfdac 100644 --- a/lib/src/model/analysis/server_analysis_service.dart +++ b/lib/src/model/analysis/server_analysis_service.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:dartchess/dartchess.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/game/game_repository.dart'; @@ -13,7 +14,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'server_analysis_service.g.dart'; @Riverpod(keepAlive: true) -ServerAnalysisService serverAnalysisService(ServerAnalysisServiceRef ref) { +ServerAnalysisService serverAnalysisService(Ref ref) { return ServerAnalysisService(ref); } @@ -22,7 +23,7 @@ class ServerAnalysisService { (GameAnyId, StreamSubscription)? _socketSubscription; - final ServerAnalysisServiceRef ref; + final Ref ref; final _currentAnalysis = ValueNotifier(null); diff --git a/lib/src/model/auth/auth_repository.dart b/lib/src/model/auth/auth_repository.dart index 57f2b4a79f..9d364ad31c 100644 --- a/lib/src/model/auth/auth_repository.dart +++ b/lib/src/model/auth/auth_repository.dart @@ -1,5 +1,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_appauth/flutter_appauth.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:http/http.dart' as http; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; @@ -15,7 +16,7 @@ const redirectUri = 'org.lichess.mobile://login-callback'; const oauthScopes = ['web:mobile']; @Riverpod(keepAlive: true) -FlutterAppAuth appAuth(AppAuthRef ref) { +FlutterAppAuth appAuth(Ref ref) { return const FlutterAppAuth(); } diff --git a/lib/src/model/auth/session_storage.dart b/lib/src/model/auth/session_storage.dart index 874ba169ac..0509aa3e64 100644 --- a/lib/src/model/auth/session_storage.dart +++ b/lib/src/model/auth/session_storage.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/db/secure_storage.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; @@ -10,7 +11,7 @@ part 'session_storage.g.dart'; const kSessionStorageKey = '$kLichessHost.userSession'; @Riverpod(keepAlive: true) -SessionStorage sessionStorage(SessionStorageRef ref) { +SessionStorage sessionStorage(Ref ref) { return const SessionStorage(); } diff --git a/lib/src/model/challenge/challenge_repository.dart b/lib/src/model/challenge/challenge_repository.dart index 67b091bc0e..17593a0cb5 100644 --- a/lib/src/model/challenge/challenge_repository.dart +++ b/lib/src/model/challenge/challenge_repository.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:deep_pick/deep_pick.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:http/http.dart' as http; import 'package:lichess_mobile/src/model/challenge/challenge.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; @@ -11,7 +12,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'challenge_repository.g.dart'; @Riverpod(keepAlive: true) -ChallengeRepository challengeRepository(ChallengeRepositoryRef ref) { +ChallengeRepository challengeRepository(Ref ref) { return ChallengeRepository(ref.read(lichessClientProvider)); } diff --git a/lib/src/model/challenge/challenge_service.dart b/lib/src/model/challenge/challenge_service.dart index 294aefb154..a670d924ea 100644 --- a/lib/src/model/challenge/challenge_service.dart +++ b/lib/src/model/challenge/challenge_service.dart @@ -4,6 +4,7 @@ import 'package:collection/collection.dart'; import 'package:deep_pick/deep_pick.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/challenge/challenge.dart'; import 'package:lichess_mobile/src/model/challenge/challenge_repository.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; @@ -22,7 +23,7 @@ import 'package:stream_transform/stream_transform.dart'; part 'challenge_service.g.dart'; @Riverpod(keepAlive: true) -ChallengeService challengeService(ChallengeServiceRef ref) { +ChallengeService challengeService(Ref ref) { final service = ChallengeService(ref); ref.onDispose(() => service.dispose()); return service; @@ -32,7 +33,7 @@ ChallengeService challengeService(ChallengeServiceRef ref) { class ChallengeService { ChallengeService(this.ref); - final ChallengeServiceRef ref; + final Ref ref; ChallengesList? _current; ChallengesList? _previous; diff --git a/lib/src/model/common/preloaded_data.dart b/lib/src/model/common/preloaded_data.dart index 95ffc6975f..95b3cbfc05 100644 --- a/lib/src/model/common/preloaded_data.dart +++ b/lib/src/model/common/preloaded_data.dart @@ -1,5 +1,6 @@ import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/db/secure_storage.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; @@ -20,7 +21,7 @@ typedef PreloadedData = ({ }); @Riverpod(keepAlive: true) -Future preloadedData(PreloadedDataRef ref) async { +Future preloadedData(Ref ref) async { final sessionStorage = ref.watch(sessionStorageProvider); final pInfo = await PackageInfo.fromPlatform(); diff --git a/lib/src/model/common/service/move_feedback.dart b/lib/src/model/common/service/move_feedback.dart index f441b7abc1..ff11f5d50b 100644 --- a/lib/src/model/common/service/move_feedback.dart +++ b/lib/src/model/common/service/move_feedback.dart @@ -1,4 +1,5 @@ import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -6,7 +7,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'move_feedback.g.dart'; @Riverpod(keepAlive: true) -MoveFeedbackService moveFeedbackService(MoveFeedbackServiceRef ref) { +MoveFeedbackService moveFeedbackService(Ref ref) { final soundService = ref.watch(soundServiceProvider); return MoveFeedbackService(soundService, ref); } @@ -15,7 +16,7 @@ class MoveFeedbackService { MoveFeedbackService(this._soundService, this._ref); final SoundService _soundService; - final MoveFeedbackServiceRef _ref; + final Ref _ref; void moveFeedback({bool check = false}) { _soundService.play(Sound.move); diff --git a/lib/src/model/common/service/sound_service.dart b/lib/src/model/common/service/sound_service.dart index 0a015632bb..ad398c86ea 100644 --- a/lib/src/model/common/service/sound_service.dart +++ b/lib/src/model/common/service/sound_service.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart' show rootBundle; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/binding.dart'; import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; @@ -28,7 +29,7 @@ enum Sound { } @Riverpod(keepAlive: true) -SoundService soundService(SoundServiceRef ref) { +SoundService soundService(Ref ref) { final service = SoundService(ref); ref.onDispose(() => service.release()); return service; @@ -70,7 +71,7 @@ Future _loadSound(SoundTheme theme, Sound sound) async { class SoundService { SoundService(this._ref); - final SoundServiceRef _ref; + final Ref _ref; /// Initialize the sound service. /// diff --git a/lib/src/model/correspondence/correspondence_game_storage.dart b/lib/src/model/correspondence/correspondence_game_storage.dart index 88883a6317..bfa4881f9c 100644 --- a/lib/src/model/correspondence/correspondence_game_storage.dart +++ b/lib/src/model/correspondence/correspondence_game_storage.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/db/database.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; @@ -14,7 +15,7 @@ part 'correspondence_game_storage.g.dart'; @Riverpod(keepAlive: true) Future correspondenceGameStorage( - CorrespondenceGameStorageRef ref, + Ref ref, ) async { final db = await ref.watch(databaseProvider.future); return CorrespondenceGameStorage(db, ref); @@ -22,9 +23,7 @@ Future correspondenceGameStorage( @riverpod Future> - offlineOngoingCorrespondenceGames( - OfflineOngoingCorrespondenceGamesRef ref, -) async { + offlineOngoingCorrespondenceGames(Ref ref) async { final session = ref.watch(authSessionProvider); // cannot use ref.watch because it would create a circular dependency // as we invalidate this provider in the storage save and delete methods @@ -48,7 +47,7 @@ const kCorrespondenceStorageAnonId = '**anonymous**'; class CorrespondenceGameStorage { const CorrespondenceGameStorage(this._db, this.ref); final Database _db; - final CorrespondenceGameStorageRef ref; + final Ref ref; /// Fetches all ongoing correspondence games, sorted by time left. Future> fetchOngoingGames( diff --git a/lib/src/model/correspondence/correspondence_service.dart b/lib/src/model/correspondence/correspondence_service.dart index 654cbdc4b3..05ad5f800a 100644 --- a/lib/src/model/correspondence/correspondence_service.dart +++ b/lib/src/model/correspondence/correspondence_service.dart @@ -5,6 +5,7 @@ import 'dart:io'; import 'package:collection/collection.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/auth/bearer.dart'; @@ -26,7 +27,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'correspondence_service.g.dart'; @Riverpod(keepAlive: true) -CorrespondenceService correspondenceService(CorrespondenceServiceRef ref) { +CorrespondenceService correspondenceService(Ref ref) { return CorrespondenceService( Logger('CorrespondenceService'), ref: ref, @@ -37,7 +38,7 @@ CorrespondenceService correspondenceService(CorrespondenceServiceRef ref) { class CorrespondenceService { CorrespondenceService(this._log, {required this.ref}); - final CorrespondenceServiceRef ref; + final Ref ref; final Logger _log; /// Handles a notification response that caused the app to open. diff --git a/lib/src/model/engine/evaluation_service.dart b/lib/src/model/engine/evaluation_service.dart index 7abb7a45dc..d822ce00ce 100644 --- a/lib/src/model/engine/evaluation_service.dart +++ b/lib/src/model/engine/evaluation_service.dart @@ -5,6 +5,7 @@ import 'dart:math'; import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; 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/common/chess.dart'; import 'package:lichess_mobile/src/model/common/eval.dart'; @@ -34,7 +35,7 @@ const engineSupportedVariants = { class EvaluationService { EvaluationService(this.ref, {required this.maxMemory}); - final EvaluationServiceRef ref; + final Ref ref; final int maxMemory; @@ -176,7 +177,7 @@ class EvaluationService { } @Riverpod(keepAlive: true) -EvaluationService evaluationService(EvaluationServiceRef ref) { +EvaluationService evaluationService(Ref ref) { final maxMemory = ref.read(preloadedDataProvider).requireValue.engineMaxMemoryInMb; diff --git a/lib/src/model/game/game_history.dart b/lib/src/model/game/game_history.dart index 10516780b8..50c4122e6d 100644 --- a/lib/src/model/game/game_history.dart +++ b/lib/src/model/game/game_history.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:async/async.dart'; import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; @@ -32,9 +33,7 @@ const _nbPerPage = 20; /// If the user is not logged in, or there is no connectivity, the recent games /// stored locally are fetched instead. @riverpod -Future> myRecentGames( - MyRecentGamesRef ref, -) async { +Future> myRecentGames(Ref ref) async { final online = await ref .watch(connectivityChangesProvider.selectAsync((c) => c.isOnline)); final session = ref.watch(authSessionProvider); @@ -62,7 +61,7 @@ Future> myRecentGames( /// A provider that fetches the recent games from the server for a given user. @riverpod Future> userRecentGames( - UserRecentGamesRef ref, { + Ref ref, { required UserId userId, }) { return ref.withClientCacheFor( @@ -83,7 +82,7 @@ Future> userRecentGames( /// stored locally are fetched instead. @riverpod Future userNumberOfGames( - UserNumberOfGamesRef ref, + Ref ref, LightUser? user, { required bool isOnline, }) async { diff --git a/lib/src/model/game/game_repository_providers.dart b/lib/src/model/game/game_repository_providers.dart index 26fd2d839a..fdc69944aa 100644 --- a/lib/src/model/game/game_repository_providers.dart +++ b/lib/src/model/game/game_repository_providers.dart @@ -1,4 +1,5 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/game/archived_game.dart'; import 'package:lichess_mobile/src/model/game/game_storage.dart'; @@ -11,10 +12,7 @@ part 'game_repository_providers.g.dart'; /// Fetches a game from the local storage if available, otherwise fetches it from the server. @riverpod -Future archivedGame( - ArchivedGameRef ref, { - required GameId id, -}) async { +Future archivedGame(Ref ref, {required GameId id}) async { final gameStorage = await ref.watch(gameStorageProvider.future); final game = await gameStorage.fetch(gameId: id); if (game != null) return game; @@ -26,7 +24,7 @@ Future archivedGame( @riverpod Future> gamesById( - GamesByIdRef ref, { + Ref ref, { required ISet ids, }) { return ref.withClient( diff --git a/lib/src/model/game/game_share_service.dart b/lib/src/model/game/game_share_service.dart index 8723a221a6..60f7ceb5bb 100644 --- a/lib/src/model/game/game_share_service.dart +++ b/lib/src/model/game/game_share_service.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:dartchess/dartchess.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; @@ -11,14 +12,14 @@ import 'package:share_plus/share_plus.dart'; part 'game_share_service.g.dart'; @Riverpod(keepAlive: true) -GameShareService gameShareService(GameShareServiceRef ref) { +GameShareService gameShareService(Ref ref) { return GameShareService(ref); } class GameShareService { GameShareService(this._ref); - final GameShareServiceRef _ref; + final Ref _ref; /// Fetches the raw PGN of a game and launches the share dialog. Future rawPgn(GameId id) async { diff --git a/lib/src/model/game/game_storage.dart b/lib/src/model/game/game_storage.dart index dc89b9b04b..5995a5e150 100644 --- a/lib/src/model/game/game_storage.dart +++ b/lib/src/model/game/game_storage.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/db/database.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/game/archived_game.dart'; @@ -11,9 +12,7 @@ import 'package:sqflite/sqflite.dart'; part 'game_storage.g.dart'; @Riverpod(keepAlive: true) -Future gameStorage( - GameStorageRef ref, -) async { +Future gameStorage(Ref ref) async { final db = await ref.watch(databaseProvider.future); return GameStorage(db); } diff --git a/lib/src/model/lobby/create_game_service.dart b/lib/src/model/lobby/create_game_service.dart index 96eeccd783..fea652f63a 100644 --- a/lib/src/model/lobby/create_game_service.dart +++ b/lib/src/model/lobby/create_game_service.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:deep_pick/deep_pick.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/challenge/challenge.dart'; import 'package:lichess_mobile/src/model/challenge/challenge_repository.dart'; @@ -24,7 +25,7 @@ typedef ChallengeResponse = ({ /// A provider for the [CreateGameService]. @riverpod -CreateGameService createGameService(CreateGameServiceRef ref) { +CreateGameService createGameService(Ref ref) { final service = CreateGameService(Logger('CreateGameService'), ref: ref); ref.onDispose(() { service.dispose(); @@ -36,7 +37,7 @@ CreateGameService createGameService(CreateGameServiceRef ref) { class CreateGameService { CreateGameService(this._log, {required this.ref}); - final CreateGameServiceRef ref; + final Ref ref; final Logger _log; LichessClient get lichessClient => ref.read(lichessClientProvider); diff --git a/lib/src/model/lobby/lobby_repository.dart b/lib/src/model/lobby/lobby_repository.dart index a6a95f1df3..db59a23602 100644 --- a/lib/src/model/lobby/lobby_repository.dart +++ b/lib/src/model/lobby/lobby_repository.dart @@ -1,5 +1,6 @@ import 'package:deep_pick/deep_pick.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:http/http.dart' as http; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; @@ -13,9 +14,7 @@ import 'game_seek.dart'; part 'lobby_repository.g.dart'; @riverpod -Future> correspondenceChallenges( - CorrespondenceChallengesRef ref, -) { +Future> correspondenceChallenges(Ref ref) { return ref.withClient( (client) => LobbyRepository(client).getCorrespondenceChallenges(), ); diff --git a/lib/src/model/notifications/notification_service.dart b/lib/src/model/notifications/notification_service.dart index d13624e37d..43be19c9b9 100644 --- a/lib/src/model/notifications/notification_service.dart +++ b/lib/src/model/notifications/notification_service.dart @@ -24,12 +24,12 @@ final _logger = Logger('NotificationService'); /// A provider instance of the [FlutterLocalNotificationsPlugin]. @Riverpod(keepAlive: true) -FlutterLocalNotificationsPlugin notificationDisplay(NotificationDisplayRef _) => +FlutterLocalNotificationsPlugin notificationDisplay(Ref _) => FlutterLocalNotificationsPlugin(); /// A provider instance of the [NotificationService]. @Riverpod(keepAlive: true) -NotificationService notificationService(NotificationServiceRef ref) { +NotificationService notificationService(Ref ref) { final service = NotificationService(ref); ref.onDispose(() => service._dispose()); @@ -47,7 +47,7 @@ NotificationService notificationService(NotificationServiceRef ref) { class NotificationService { NotificationService(this._ref); - final NotificationServiceRef _ref; + final Ref _ref; /// The Firebase Cloud Messaging token refresh subscription. StreamSubscription? _fcmTokenRefreshSubscription; diff --git a/lib/src/model/puzzle/puzzle_batch_storage.dart b/lib/src/model/puzzle/puzzle_batch_storage.dart index df9cad3a1a..b35969535a 100644 --- a/lib/src/model/puzzle/puzzle_batch_storage.dart +++ b/lib/src/model/puzzle/puzzle_batch_storage.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:collection/collection.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/db/database.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; @@ -16,7 +17,7 @@ part 'puzzle_batch_storage.freezed.dart'; part 'puzzle_batch_storage.g.dart'; @Riverpod(keepAlive: true) -Future puzzleBatchStorage(PuzzleBatchStorageRef ref) async { +Future puzzleBatchStorage(Ref ref) async { final database = await ref.watch(databaseProvider.future); return PuzzleBatchStorage(database, ref); } @@ -29,7 +30,7 @@ class PuzzleBatchStorage { const PuzzleBatchStorage(this._db, this._ref); final Database _db; - final PuzzleBatchStorageRef _ref; + final Ref _ref; Future fetch({ required UserId? userId, diff --git a/lib/src/model/puzzle/puzzle_opening.dart b/lib/src/model/puzzle/puzzle_opening.dart index 3e77522141..51858a28a0 100644 --- a/lib/src/model/puzzle/puzzle_opening.dart +++ b/lib/src/model/puzzle/puzzle_opening.dart @@ -1,4 +1,5 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_providers.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -19,9 +20,7 @@ typedef PuzzleOpeningData = ({ /// Returns a flattened list of openings with their respective counts. @riverpod -Future> flatOpeningsList( - FlatOpeningsListRef ref, -) async { +Future> flatOpeningsList(Ref ref) async { final families = await ref.watch(puzzleOpeningsProvider.future); return families .map( @@ -41,7 +40,7 @@ Future> flatOpeningsList( } @riverpod -Future puzzleOpeningName(PuzzleOpeningNameRef ref, String key) async { +Future puzzleOpeningName(Ref ref, String key) async { final openings = await ref.watch(flatOpeningsListProvider.future); return openings.firstWhere((element) => element.key == key).name; } diff --git a/lib/src/model/puzzle/puzzle_providers.dart b/lib/src/model/puzzle/puzzle_providers.dart index 5c8a7ddbc3..1873c9ffba 100644 --- a/lib/src/model/puzzle/puzzle_providers.dart +++ b/lib/src/model/puzzle/puzzle_providers.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle.dart'; @@ -19,10 +20,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'puzzle_providers.g.dart'; @riverpod -Future nextPuzzle( - NextPuzzleRef ref, - PuzzleAngle angle, -) async { +Future nextPuzzle(Ref ref, PuzzleAngle angle) async { final session = ref.watch(authSessionProvider); final puzzleService = await ref.read(puzzleServiceFactoryProvider)( queueLength: kPuzzleLocalQueueLength, @@ -38,18 +36,18 @@ Future nextPuzzle( } @riverpod -Future streak(StreakRef ref) { +Future streak(Ref ref) { return ref.withClient((client) => PuzzleRepository(client).streak()); } @riverpod -Future storm(StormRef ref) { +Future storm(Ref ref) { return ref.withClient((client) => PuzzleRepository(client).storm()); } /// Fetches a puzzle from the local storage if available, otherwise fetches it from the server. @riverpod -Future puzzle(PuzzleRef ref, PuzzleId id) async { +Future puzzle(Ref ref, PuzzleId id) async { final puzzleStorage = await ref.watch(puzzleStorageProvider.future); final puzzle = await puzzleStorage.fetch(puzzleId: id); if (puzzle != null) return puzzle; @@ -57,7 +55,7 @@ Future puzzle(PuzzleRef ref, PuzzleId id) async { } @riverpod -Future dailyPuzzle(DailyPuzzleRef ref) { +Future dailyPuzzle(Ref ref) { return ref.withClientCacheFor( (client) => PuzzleRepository(client).daily(), const Duration(hours: 6), @@ -65,16 +63,14 @@ Future dailyPuzzle(DailyPuzzleRef ref) { } @riverpod -Future> savedBatches(SavedBatchesRef ref) async { +Future> savedBatches(Ref ref) async { final session = ref.watch(authSessionProvider); final storage = await ref.watch(puzzleBatchStorageProvider.future); return storage.fetchAll(userId: session?.user.id); } @riverpod -Future> savedThemeBatches( - SavedThemeBatchesRef ref, -) async { +Future> savedThemeBatches(Ref ref) async { final session = ref.watch(authSessionProvider); final storage = await ref.watch(puzzleBatchStorageProvider.future); return storage.fetchSavedThemes(userId: session?.user.id); @@ -82,7 +78,7 @@ Future> savedThemeBatches( @riverpod Future> savedOpeningBatches( - SavedOpeningBatchesRef ref, + Ref ref, ) async { final session = ref.watch(authSessionProvider); final storage = await ref.watch(puzzleBatchStorageProvider.future); @@ -91,7 +87,7 @@ Future> savedOpeningBatches( @riverpod Future puzzleDashboard( - PuzzleDashboardRef ref, + Ref ref, int days, ) async { final session = ref.watch(authSessionProvider); @@ -103,9 +99,7 @@ Future puzzleDashboard( } @riverpod -Future?> puzzleRecentActivity( - PuzzleRecentActivityRef ref, -) async { +Future?> puzzleRecentActivity(Ref ref) async { final session = ref.watch(authSessionProvider); if (session == null) return null; return ref.withClientCacheFor( @@ -115,16 +109,14 @@ Future?> puzzleRecentActivity( } @riverpod -Future stormDashboard(StormDashboardRef ref, UserId id) async { +Future stormDashboard(Ref ref, UserId id) async { return ref.withClient( (client) => PuzzleRepository(client).stormDashboard(id), ); } @riverpod -Future> puzzleThemes( - PuzzleThemesRef ref, -) { +Future> puzzleThemes(Ref ref) { return ref.withClientCacheFor( (client) => PuzzleRepository(client).puzzleThemes(), const Duration(days: 1), @@ -132,7 +124,7 @@ Future> puzzleThemes( } @riverpod -Future> puzzleOpenings(PuzzleOpeningsRef ref) { +Future> puzzleOpenings(Ref ref) { return ref.withClientCacheFor( (client) => PuzzleRepository(client).puzzleOpenings(), const Duration(days: 1), diff --git a/lib/src/model/puzzle/puzzle_service.dart b/lib/src/model/puzzle/puzzle_service.dart index 4bd065e656..c5d7cb04bf 100644 --- a/lib/src/model/puzzle/puzzle_service.dart +++ b/lib/src/model/puzzle/puzzle_service.dart @@ -2,6 +2,7 @@ import 'dart:math' show max; import 'package:async/async.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_storage.dart'; @@ -24,21 +25,21 @@ part 'puzzle_service.g.dart'; const kPuzzleLocalQueueLength = 50; @Riverpod(keepAlive: true) -Future puzzleService(PuzzleServiceRef ref) { +Future puzzleService(Ref ref) { return ref.read(puzzleServiceFactoryProvider)( queueLength: kPuzzleLocalQueueLength, ); } @Riverpod(keepAlive: true) -PuzzleServiceFactory puzzleServiceFactory(PuzzleServiceFactoryRef ref) { +PuzzleServiceFactory puzzleServiceFactory(Ref ref) { return PuzzleServiceFactory(ref); } class PuzzleServiceFactory { PuzzleServiceFactory(this._ref); - final PuzzleServiceFactoryRef _ref; + final Ref _ref; Future call({required int queueLength}) async { return PuzzleService( @@ -73,7 +74,7 @@ class PuzzleService { required this.queueLength, }); - final PuzzleServiceFactoryRef _ref; + final Ref _ref; final int queueLength; final PuzzleBatchStorage batchStorage; final PuzzleStorage puzzleStorage; diff --git a/lib/src/model/puzzle/puzzle_storage.dart b/lib/src/model/puzzle/puzzle_storage.dart index ef146a1d6f..acd3426f92 100644 --- a/lib/src/model/puzzle/puzzle_storage.dart +++ b/lib/src/model/puzzle/puzzle_storage.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/db/database.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle.dart'; @@ -9,7 +10,7 @@ import 'package:sqflite/sqflite.dart'; part 'puzzle_storage.g.dart'; @Riverpod(keepAlive: true) -Future puzzleStorage(PuzzleStorageRef ref) async { +Future puzzleStorage(Ref ref) async { final db = await ref.watch(databaseProvider.future); return PuzzleStorage(db); } diff --git a/lib/src/model/puzzle/puzzle_theme.dart b/lib/src/model/puzzle/puzzle_theme.dart index 12f22021ef..5c7c791156 100644 --- a/lib/src/model/puzzle/puzzle_theme.dart +++ b/lib/src/model/puzzle/puzzle_theme.dart @@ -1,5 +1,6 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/l10n/l10n.dart'; import 'package:lichess_mobile/src/localizations.dart'; @@ -407,9 +408,7 @@ final IMap puzzleThemeNameMap = typedef PuzzleThemeCategory = (String, List); @Riverpod(keepAlive: true) -IList puzzleThemeCategories( - PuzzleThemeCategoriesRef ref, -) { +IList puzzleThemeCategories(Ref ref) { final l10n = ref.watch(localizationsProvider); return IList([ diff --git a/lib/src/model/relation/relation_repository_providers.dart b/lib/src/model/relation/relation_repository_providers.dart index 2c80f6144a..7f20842f26 100644 --- a/lib/src/model/relation/relation_repository_providers.dart +++ b/lib/src/model/relation/relation_repository_providers.dart @@ -1,4 +1,5 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/network/http.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -8,7 +9,7 @@ import 'relation_repository.dart'; part 'relation_repository_providers.g.dart'; @riverpod -Future> following(FollowingRef ref) async { +Future> following(Ref ref) async { return ref.withClientCacheFor( (client) => RelationRepository(client).getFollowing(), const Duration(hours: 1), diff --git a/lib/src/model/user/user_repository_providers.dart b/lib/src/model/user/user_repository_providers.dart index 09c2121bd4..a1221aa58a 100644 --- a/lib/src/model/user/user_repository_providers.dart +++ b/lib/src/model/user/user_repository_providers.dart @@ -1,4 +1,5 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; import 'package:lichess_mobile/src/model/user/leaderboard.dart'; @@ -14,17 +15,14 @@ part 'user_repository_providers.g.dart'; const _kAutoCompleteDebounceTimer = Duration(milliseconds: 300); @riverpod -Future user(UserRef ref, {required UserId id}) async { +Future user(Ref ref, {required UserId id}) async { return ref.withClient( (client) => UserRepository(client).getUser(id), ); } @riverpod -Future> userActivity( - UserActivityRef ref, { - required UserId id, -}) async { +Future> userActivity(Ref ref, {required UserId id}) async { return ref.withClientCacheFor( (client) => UserRepository(client).getActivity(id), // cache is important because the associated widget is in a [ListView] and @@ -37,10 +35,7 @@ Future> userActivity( } @riverpod -Future<(User, UserStatus)> userAndStatus( - UserAndStatusRef ref, { - required UserId id, -}) async { +Future<(User, UserStatus)> userAndStatus(Ref ref, {required UserId id}) async { return ref.withClient( (client) async { final repo = UserRepository(client); @@ -59,7 +54,7 @@ Future<(User, UserStatus)> userAndStatus( @riverpod Future userPerfStats( - UserPerfStatsRef ref, { + Ref ref, { required UserId id, required Perf perf, }) async { @@ -70,7 +65,7 @@ Future userPerfStats( @riverpod Future> userStatuses( - UserStatusesRef ref, { + Ref ref, { required ISet ids, }) async { return ref.withClient( @@ -79,7 +74,7 @@ Future> userStatuses( } @riverpod -Future> liveStreamers(LiveStreamersRef ref) async { +Future> liveStreamers(Ref ref) async { return ref.withClientCacheFor( (client) => UserRepository(client).getLiveStreamers(), const Duration(minutes: 1), @@ -87,7 +82,7 @@ Future> liveStreamers(LiveStreamersRef ref) async { } @riverpod -Future> top1(Top1Ref ref) async { +Future> top1(Ref ref) async { return ref.withClientCacheFor( (client) => UserRepository(client).getTop1(), const Duration(hours: 12), @@ -95,7 +90,7 @@ Future> top1(Top1Ref ref) async { } @riverpod -Future leaderboard(LeaderboardRef ref) async { +Future leaderboard(Ref ref) async { return ref.withClientCacheFor( (client) => UserRepository(client).getLeaderboard(), const Duration(hours: 2), @@ -103,10 +98,7 @@ Future leaderboard(LeaderboardRef ref) async { } @riverpod -Future> autoCompleteUser( - AutoCompleteUserRef ref, - String term, -) async { +Future> autoCompleteUser(Ref ref, String term) async { // debounce calls as user might be typing var didDispose = false; ref.onDispose(() => didDispose = true); @@ -122,7 +114,7 @@ Future> autoCompleteUser( @riverpod Future> userRatingHistory( - UserRatingHistoryRef ref, { + Ref ref, { required UserId id, }) async { return ref.withClientCacheFor( diff --git a/lib/src/network/connectivity.dart b/lib/src/network/connectivity.dart index 955d95649d..bf340e467a 100644 --- a/lib/src/network/connectivity.dart +++ b/lib/src/network/connectivity.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:http/http.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/network/http.dart'; @@ -15,7 +16,7 @@ final _logger = Logger('Connectivity'); /// A provider that exposes a [Connectivity] instance. @Riverpod(keepAlive: true) -Connectivity connectivityPlugin(ConnectivityPluginRef _) => Connectivity(); +Connectivity connectivityPlugin(Ref _) => Connectivity(); /// This provider is used to check the device's connectivity status, reacting to /// changes in connectivity and app lifecycle events. diff --git a/lib/src/network/http.dart b/lib/src/network/http.dart index 9c5a1cd7bd..4c24ab6cb5 100644 --- a/lib/src/network/http.dart +++ b/lib/src/network/http.dart @@ -69,8 +69,7 @@ class HttpClientFactory { } @Riverpod(keepAlive: true) -HttpClientFactory httpClientFactory(HttpClientFactoryRef _) => - HttpClientFactory(); +HttpClientFactory httpClientFactory(Ref _) => HttpClientFactory(); /// The default http client. /// @@ -78,7 +77,7 @@ HttpClientFactory httpClientFactory(HttpClientFactoryRef _) => /// example, requests to lichess CDN, or other APIs. /// Only one instance of this client is created and kept alive for the whole app. @Riverpod(keepAlive: true) -Client defaultClient(DefaultClientRef ref) { +Client defaultClient(Ref ref) { final client = LoggingClient(ref.read(httpClientFactoryProvider)()); ref.onDispose(() => client.close()); return client; @@ -88,7 +87,7 @@ Client defaultClient(DefaultClientRef ref) { /// /// Only one instance of this client is created and kept alive for the whole app. @Riverpod(keepAlive: true) -LichessClient lichessClient(LichessClientRef ref) { +LichessClient lichessClient(Ref ref) { final client = LichessClient( // Retry just once, after 500ms, on 429 Too Many Requests. RetryClient( @@ -107,7 +106,7 @@ Duration _defaultDelay(int retryCount) => const Duration(milliseconds: 900) * math.pow(1.5, retryCount); @Riverpod(keepAlive: true) -String userAgent(UserAgentRef ref) { +String userAgent(Ref ref) { final session = ref.watch(authSessionProvider); return makeUserAgent( @@ -163,7 +162,7 @@ class LoggingClient extends BaseClient { class LichessClient implements Client { LichessClient(this._inner, this._ref); - final LichessClientRef _ref; + final Ref _ref; final Client _inner; @override diff --git a/lib/src/network/socket.dart b/lib/src/network/socket.dart index 557a3eb953..1997cf23e2 100644 --- a/lib/src/network/socket.dart +++ b/lib/src/network/socket.dart @@ -6,6 +6,7 @@ import 'dart:math' as math; import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/auth/bearer.dart'; @@ -475,7 +476,7 @@ class SocketPool { _pool[_currentRoute] = client; } - final SocketPoolRef _ref; + final Ref _ref; /// The delay before closing the socket if idle (no subscription). final Duration idleTimeout; @@ -570,7 +571,7 @@ class SocketPool { } @Riverpod(keepAlive: true) -SocketPool socketPool(SocketPoolRef ref) { +SocketPool socketPool(Ref ref) { final pool = SocketPool(ref); Timer? closeInBackgroundTimer; @@ -629,9 +630,7 @@ class AverageLag extends _$AverageLag { } @Riverpod(keepAlive: true) -WebSocketChannelFactory webSocketChannelFactory( - WebSocketChannelFactoryRef ref, -) { +WebSocketChannelFactory webSocketChannelFactory(Ref ref) { return const WebSocketChannelFactory(); } diff --git a/lib/src/view/game/archived_game_screen_providers.dart b/lib/src/view/game/archived_game_screen_providers.dart index d3ae4fe2dd..bcbecf5721 100644 --- a/lib/src/view/game/archived_game_screen_providers.dart +++ b/lib/src/view/game/archived_game_screen_providers.dart @@ -1,3 +1,4 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; import 'package:lichess_mobile/src/model/game/archived_game.dart'; @@ -19,7 +20,7 @@ class IsBoardTurned extends _$IsBoardTurned { } @riverpod -bool canGoForward(CanGoForwardRef ref, GameId id) { +bool canGoForward(Ref ref, GameId id) { final gameCursor = ref.watch(gameCursorProvider(id)); if (gameCursor.hasValue) { final (game, cursor) = gameCursor.value!; @@ -30,7 +31,7 @@ bool canGoForward(CanGoForwardRef ref, GameId id) { } @riverpod -bool canGoBackward(CanGoBackwardRef ref, GameId id) { +bool canGoBackward(Ref ref, GameId id) { final gameCursor = ref.watch(gameCursorProvider(id)); if (gameCursor.hasValue) { final (_, cursor) = gameCursor.value!; diff --git a/lib/src/view/game/game_screen_providers.dart b/lib/src/view/game/game_screen_providers.dart index 305019f419..285750cd9f 100644 --- a/lib/src/view/game/game_screen_providers.dart +++ b/lib/src/view/game/game_screen_providers.dart @@ -1,3 +1,4 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/challenge/challenge.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/speed.dart'; @@ -74,10 +75,7 @@ class IsBoardTurned extends _$IsBoardTurned { } @riverpod -Future shouldPreventGoingBack( - ShouldPreventGoingBackRef ref, - GameFullId gameId, -) { +Future shouldPreventGoingBack(Ref ref, GameFullId gameId) { return ref.watch( gameControllerProvider(gameId).selectAsync( (state) => @@ -94,10 +92,7 @@ Future< bool shouldConfirmMove, bool isZenModeEnabled, bool canAutoQueen - })> userGamePrefs( - UserGamePrefsRef ref, - GameFullId gameId, -) async { + })> userGamePrefs(Ref ref, GameFullId gameId) async { final prefs = await ref.watch( gameControllerProvider(gameId).selectAsync((state) => state.game.prefs), ); @@ -128,10 +123,7 @@ Future< /// /// This is data that won't change during the game. @riverpod -Future gameMeta( - GameMetaRef ref, - GameFullId gameId, -) async { +Future gameMeta(Ref ref, GameFullId gameId) async { return await ref.watch( gameControllerProvider(gameId).selectAsync((state) => state.game.meta), ); From b2592c28a5dacd872638eb98916011525b0ffa71 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 22 Oct 2024 21:38:55 +0200 Subject: [PATCH 535/979] Upgrade dependencies --- pubspec.lock | 34 +++++++++++++++++----------------- pubspec.yaml | 2 +- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index a4cad0cfab..af5424b22c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "45cfa8471b89fb6643fe9bf51bd7931a76b8f5ec2d65de4fb176dba8d4f22c77" + sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" url: "https://pub.dev" source: hosted - version: "73.0.0" + version: "76.0.0" _flutterfire_internals: dependency: transitive description: @@ -21,15 +21,15 @@ packages: dependency: transitive description: dart source: sdk - version: "0.3.2" + version: "0.3.3" analyzer: dependency: transitive description: name: analyzer - sha256: "4959fec185fe70cce007c57e9ab6983101dbe593d2bf8bbfb4453aaec0cf470a" + sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" url: "https://pub.dev" source: hosted - version: "6.8.0" + version: "6.11.0" analyzer_plugin: dependency: transitive description: @@ -314,26 +314,26 @@ packages: dependency: "direct dev" description: name: custom_lint - sha256: "832fcdc676171205201c9cffafd6b5add19393962f6598af8472b48b413026e6" + sha256: "4500e88854e7581ee43586abeaf4443cb22375d6d289241a87b1aadf678d5545" url: "https://pub.dev" source: hosted - version: "0.6.8" + version: "0.6.10" custom_lint_builder: dependency: transitive description: name: custom_lint_builder - sha256: c3d82779026f91b8e00c9ac18934595cbc9b490094ea682052beeafdb2bd50ac + sha256: "5a95eff100da256fbf086b329c17c8b49058c261cdf56d3a4157d3c31c511d78" url: "https://pub.dev" source: hosted - version: "0.6.8" + version: "0.6.10" custom_lint_core: dependency: transitive description: name: custom_lint_core - sha256: "4ddbbdaa774265de44c97054dcec058a83d9081d071785ece601e348c18c267d" + sha256: "76a4046cc71d976222a078a8fd4a65e198b70545a8d690a75196dd14f08510f6" url: "https://pub.dev" source: hosted - version: "0.6.5" + version: "0.6.10" dart_style: dependency: transitive description: @@ -887,10 +887,10 @@ packages: dependency: transitive description: name: macros - sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" + sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" url: "https://pub.dev" source: hosted - version: "0.1.2-main.4" + version: "0.1.3-main.0" matcher: dependency: transitive description: @@ -1372,10 +1372,10 @@ packages: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.0" state_notifier: dependency: transitive description: @@ -1597,10 +1597,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b url: "https://pub.dev" source: hosted - version: "14.2.5" + version: "14.3.0" wakelock_plus: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 500c8a098a..0429dbb57f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,7 +5,7 @@ publish_to: "none" version: 0.12.5+001205 # see README.md for details about versioning environment: - sdk: ">=3.3.0 <4.0.0" + sdk: ">=3.5.0 <4.0.0" dependencies: app_settings: ^5.1.1 From c7a393270fed6ba2f41de170da746382197530c7 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 22 Oct 2024 21:42:44 +0200 Subject: [PATCH 536/979] Add flutter version constraint --- pubspec.lock | 2 +- pubspec.yaml | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index af5424b22c..6858978b69 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1691,4 +1691,4 @@ packages: version: "3.1.2" sdks: dart: ">=3.5.0 <4.0.0" - flutter: ">=3.24.0" + flutter: ">=3.27.0-0.1.pre" diff --git a/pubspec.yaml b/pubspec.yaml index 0429dbb57f..364c89a868 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,10 +2,12 @@ name: lichess_mobile description: Lichess mobile app V2 publish_to: "none" -version: 0.12.5+001205 # see README.md for details about versioning +version: 0.12.5+001205 # See README.md for details about versioning environment: sdk: ">=3.5.0 <4.0.0" + # We're using the beta channel for the flutter version + flutter: "3.27.0-0.1.pre" dependencies: app_settings: ^5.1.1 From 67aa3cb34474bc1c1f770fa615a49a32cf992e90 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 22 Oct 2024 22:25:09 +0200 Subject: [PATCH 537/979] Fix deprecation warning --- lib/src/navigation.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/navigation.dart b/lib/src/navigation.dart index f5d96607f5..b9239c44d7 100644 --- a/lib/src/navigation.dart +++ b/lib/src/navigation.dart @@ -490,8 +490,8 @@ class _MaterialTabViewState extends ConsumerState<_MaterialTabView> { final currentTab = ref.watch(currentBottomTabProvider); final enablePopHandler = currentTab == widget.tab; return NavigatorPopHandler( - onPop: enablePopHandler - ? () { + onPopWithResult: enablePopHandler + ? (_) { widget.navigatorKey?.currentState?.maybePop(); } : null, From 70b1fc495367fec811cb83b4b12f64d7d4782f3e Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Tue, 22 Oct 2024 18:26:48 +0200 Subject: [PATCH 538/979] refactor: make _computeBestMoveShapes() public --- lib/src/model/common/eval.dart | 72 ++++++++++++++++++++++ lib/src/view/analysis/analysis_board.dart | 75 +---------------------- 2 files changed, 73 insertions(+), 74 deletions(-) diff --git a/lib/src/model/common/eval.dart b/lib/src/model/common/eval.dart index 0691c850e7..df78dd2483 100644 --- a/lib/src/model/common/eval.dart +++ b/lib/src/model/common/eval.dart @@ -1,8 +1,11 @@ import 'dart:math' as math; +import 'package:chessground/chessground.dart'; import 'package:collection/collection.dart'; import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; @@ -158,6 +161,75 @@ class PvData with _$PvData { typedef MoveWithWinningChances = ({Move move, double winningChances}); +ISet computeBestMoveShapes( + IList moves, + Side sideToMove, + PieceAssets pieceAssets, +) { + // Scale down all moves with index > 0 based on how much worse their winning chances are compared to the best move + // (assume moves are ordered by their winning chances, so index==0 is the best move) + double scaleArrowAgainstBestMove(int index) { + const minScale = 0.15; + const maxScale = 1.0; + const winningDiffScaleFactor = 2.5; + + final bestMove = moves[0]; + final winningDiffComparedToBestMove = + bestMove.winningChances - moves[index].winningChances; + // Force minimum scale if the best move is significantly better than this move + if (winningDiffComparedToBestMove > 0.3) { + return minScale; + } + return clampDouble( + math.max( + minScale, + maxScale - winningDiffScaleFactor * winningDiffComparedToBestMove, + ), + 0, + 1, + ); + } + + return ISet( + moves.mapIndexed( + (i, m) { + final move = m.move; + // Same colors as in the Web UI with a slightly different opacity + // The best move has a different color than the other moves + final color = Color((i == 0) ? 0x66003088 : 0x664A4A4A); + switch (move) { + case NormalMove(from: _, to: _, promotion: final promRole): + return [ + Arrow( + color: color, + orig: move.from, + dest: move.to, + scale: scaleArrowAgainstBestMove(i), + ), + if (promRole != null) + PieceShape( + color: color, + orig: move.to, + pieceAssets: pieceAssets, + piece: Piece(color: sideToMove, role: promRole), + ), + ]; + case DropMove(role: final role, to: _): + return [ + PieceShape( + color: color, + orig: move.to, + pieceAssets: pieceAssets, + opacity: 0.5, + piece: Piece(color: sideToMove, role: role), + ), + ]; + } + }, + ).expand((e) => e), + ); +} + double cpToPawns(int cp) => cp / 100; int cpFromPawns(double pawns) => (pawns * 100).round(); diff --git a/lib/src/view/analysis/analysis_board.dart b/lib/src/view/analysis/analysis_board.dart index 22ff79c595..bfb74f672e 100644 --- a/lib/src/view/analysis/analysis_board.dart +++ b/lib/src/view/analysis/analysis_board.dart @@ -1,8 +1,4 @@ -import 'dart:math' as math; -import 'dart:ui'; - import 'package:chessground/chessground.dart'; -import 'package:collection/collection.dart'; import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/cupertino.dart'; @@ -68,7 +64,7 @@ class AnalysisBoardState extends ConsumerState { final ISet bestMoveShapes = showBestMoveArrow && analysisState.isEngineAvailable && bestMoves != null - ? _computeBestMoveShapes( + ? computeBestMoveShapes( bestMoves, currentNode.position.turn, boardPrefs.pieceSet.assets, @@ -119,75 +115,6 @@ class AnalysisBoardState extends ConsumerState { ); } - ISet _computeBestMoveShapes( - IList moves, - Side sideToMove, - PieceAssets pieceAssets, - ) { - // Scale down all moves with index > 0 based on how much worse their winning chances are compared to the best move - // (assume moves are ordered by their winning chances, so index==0 is the best move) - double scaleArrowAgainstBestMove(int index) { - const minScale = 0.15; - const maxScale = 1.0; - const winningDiffScaleFactor = 2.5; - - final bestMove = moves[0]; - final winningDiffComparedToBestMove = - bestMove.winningChances - moves[index].winningChances; - // Force minimum scale if the best move is significantly better than this move - if (winningDiffComparedToBestMove > 0.3) { - return minScale; - } - return clampDouble( - math.max( - minScale, - maxScale - winningDiffScaleFactor * winningDiffComparedToBestMove, - ), - 0, - 1, - ); - } - - return ISet( - moves.mapIndexed( - (i, m) { - final move = m.move; - // Same colors as in the Web UI with a slightly different opacity - // The best move has a different color than the other moves - final color = Color((i == 0) ? 0x66003088 : 0x664A4A4A); - switch (move) { - case NormalMove(from: _, to: _, promotion: final promRole): - return [ - Arrow( - color: color, - orig: move.from, - dest: move.to, - scale: scaleArrowAgainstBestMove(i), - ), - if (promRole != null) - PieceShape( - color: color, - orig: move.to, - pieceAssets: pieceAssets, - piece: Piece(color: sideToMove, role: promRole), - ), - ]; - case DropMove(role: final role, to: _): - return [ - PieceShape( - color: color, - orig: move.to, - pieceAssets: pieceAssets, - opacity: 0.5, - piece: Piece(color: sideToMove, role: role), - ), - ]; - } - }, - ).expand((e) => e), - ); - } - void _onCompleteShape(Shape shape) { if (userShapes.any((element) => element == shape)) { setState(() { From 4c3a24678625603c50d1461e98bc59aa253da4c3 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 23 Oct 2024 21:03:18 +0200 Subject: [PATCH 539/979] Don't try to show perf card if offline --- lib/src/view/home/home_tab_screen.dart | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/lib/src/view/home/home_tab_screen.dart b/lib/src/view/home/home_tab_screen.dart index 8f2b3a5569..d540ce6c5f 100644 --- a/lib/src/view/home/home_tab_screen.dart +++ b/lib/src/view/home/home_tab_screen.dart @@ -232,13 +232,14 @@ class _HomeScreenState extends ConsumerState with RouteAware { shouldShow: true, child: _HelloWidget(), ), - _EditableWidget( - widget: EnabledWidget.perfCards, - shouldShow: session != null, - child: const AccountPerfCards( - padding: Styles.horizontalBodyPadding, + if (status.isOnline) + _EditableWidget( + widget: EnabledWidget.perfCards, + shouldShow: session != null, + child: const AccountPerfCards( + padding: Styles.horizontalBodyPadding, + ), ), - ), _EditableWidget( widget: EnabledWidget.quickPairing, shouldShow: status.isOnline, @@ -344,13 +345,14 @@ class _HomeScreenState extends ConsumerState with RouteAware { shouldShow: true, child: _HelloWidget(), ), - _EditableWidget( - widget: EnabledWidget.perfCards, - shouldShow: session != null, - child: const AccountPerfCards( - padding: Styles.bodySectionPadding, + if (status.isOnline) + _EditableWidget( + widget: EnabledWidget.perfCards, + shouldShow: session != null, + child: const AccountPerfCards( + padding: Styles.bodySectionPadding, + ), ), - ), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ From cc30a898bfb0f32cc7f189d7ac7f81fcc7e4d8f2 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Tue, 8 Oct 2024 23:40:50 +0200 Subject: [PATCH 540/979] add models to get study as JSON and PGN --- lib/src/model/study/study.dart | 105 ++++++++++++++++++++++ lib/src/model/study/study_repository.dart | 30 +++++++ 2 files changed, 135 insertions(+) diff --git a/lib/src/model/study/study.dart b/lib/src/model/study/study.dart index 8ff3fa9c1a..d1de222a78 100644 --- a/lib/src/model/study/study.dart +++ b/lib/src/model/study/study.dart @@ -1,11 +1,116 @@ +import 'package:collection/collection.dart'; +import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; part 'study.freezed.dart'; part 'study.g.dart'; +@Freezed(fromJson: true) +class Study with _$Study { + const Study._(); + + const factory Study({ + required StudyId id, + required String name, + required ({StudyChapterId chapterId, String path}) position, + required bool liked, + required int likes, + required UserId? ownerId, + @JsonKey(fromJson: studyFeaturesFromJson) required StudyFeatures features, + required IList topics, + required IList chapters, + required StudyChapter chapter, + }) = _Study; + + StudyChapterMeta get currentChapterMeta => + chapters.firstWhere((c) => c.id == chapter.id); + + factory Study.fromJson(Map json) => _$StudyFromJson(json); +} + +typedef StudyFeatures = ({ + bool cloneable, + bool chat, + bool sticky, +}); + +StudyFeatures studyFeaturesFromJson(Map json) { + return ( + cloneable: json['cloneable'] as bool? ?? false, + chat: json['chat'] as bool? ?? false, + sticky: json['sticky'] as bool? ?? false, + ); +} + +@Freezed(fromJson: true) +class StudyChapter with _$StudyChapter { + const StudyChapter._(); + + const factory StudyChapter({ + required StudyChapterId id, + required StudyChapterSetup setup, + @JsonKey(defaultValue: false) required bool practise, + required int? conceal, + @JsonKey(defaultValue: false) required bool gamebook, + @JsonKey(fromJson: studyChapterFeaturesFromJson) + required StudyChapterFeatures features, + }) = _StudyChapter; + + factory StudyChapter.fromJson(Map json) => + _$StudyChapterFromJson(json); +} + +typedef StudyChapterFeatures = ({ + bool computer, + bool explorer, +}); + +StudyChapterFeatures studyChapterFeaturesFromJson(Map json) { + return ( + computer: json['computer'] as bool? ?? false, + explorer: json['explorer'] as bool? ?? false, + ); +} + +@Freezed(fromJson: true) +class StudyChapterSetup with _$StudyChapterSetup { + const StudyChapterSetup._(); + + const factory StudyChapterSetup({ + required GameId? id, + required Side orientation, + @JsonKey(fromJson: _variantFromJson) required Variant variant, + required bool? fromFen, + }) = _StudyChapterSetup; + + factory StudyChapterSetup.fromJson(Map json) => + _$StudyChapterSetupFromJson(json); +} + +Variant _variantFromJson(Map json) { + return Variant.values.firstWhereOrNull( + (v) => v.name == json['key'], + )!; +} + +@Freezed(fromJson: true) +class StudyChapterMeta with _$StudyChapterMeta { + const StudyChapterMeta._(); + + const factory StudyChapterMeta({ + required StudyChapterId id, + required String name, + required String? fen, + }) = _StudyChapterMeta; + + factory StudyChapterMeta.fromJson(Map json) => + _$StudyChapterMetaFromJson(json); +} + @Freezed(fromJson: true) class StudyPageData with _$StudyPageData { const StudyPageData._(); diff --git a/lib/src/model/study/study_repository.dart b/lib/src/model/study/study_repository.dart index b0e6b84461..5b479a7ef7 100644 --- a/lib/src/model/study/study_repository.dart +++ b/lib/src/model/study/study_repository.dart @@ -1,6 +1,9 @@ +import 'dart:convert'; + import 'package:deep_pick/deep_pick.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:http/http.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/study/study.dart'; import 'package:lichess_mobile/src/model/study/study_filter.dart'; import 'package:lichess_mobile/src/network/http.dart'; @@ -53,4 +56,31 @@ class StudyRepository { }, ); } + + Future<(Study study, String pgn)> getStudy({ + required StudyId id, + StudyChapterId? chapterId, + }) async { + final study = await client.readJson( + Uri( + path: (chapterId != null) ? '/study/$id/$chapterId' : '/study/$id', + queryParameters: { + 'chapters': '1', + }, + ), + headers: {'Accept': 'application/json'}, + mapper: (Map json) { + return Study.fromJson( + pick(json, 'study').asMapOrThrow(), + ); + }, + ); + + final pgnBytes = await client.readBytes( + Uri(path: '/api/study/$id/${chapterId ?? study.chapter.id}.pgn'), + headers: {'Accept': 'application/x-chess-pgn'}, + ); + + return (study, utf8.decode(pgnBytes)); + } } From 8b324a33ee4b485fe9cbeb2dea7a826c63cddbf8 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Sun, 20 Oct 2024 22:31:14 +0200 Subject: [PATCH 541/979] add tests for study json parsing --- test/model/study/study_repository_test.dart | 443 ++++++++++++++++++++ 1 file changed, 443 insertions(+) create mode 100644 test/model/study/study_repository_test.dart diff --git a/test/model/study/study_repository_test.dart b/test/model/study/study_repository_test.dart new file mode 100644 index 0000000000..2a120dcd5f --- /dev/null +++ b/test/model/study/study_repository_test.dart @@ -0,0 +1,443 @@ +import 'package:dartchess/dartchess.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/testing.dart'; +import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/study/study.dart'; +import 'package:lichess_mobile/src/model/study/study_repository.dart'; + +import '../../test_helpers.dart'; + +void main() { + group('StudyRepository.getStudy', () { + test('correctly parse study JSON', () async { + // curl -X GET https://lichess.org/study/JbWtuaeK/7OJXp679\?chapters\=1 -H "Accept: application/json" | sed "s/\\\\n/ /g" | jq 'del(.study.chat)' + const response = ''' +{ + "study": { + "id": "JbWtuaeK", + "name": "How to Solve Puzzles Correctly", + "members": { + "kyle-and-jess": { + "user": { + "name": "Kyle-and-Jess", + "flair": "nature.chipmunk", + "id": "kyle-and-jess" + }, + "role": "w" + }, + "jessieu726": { + "user": { + "name": "jessieu726", + "flair": "nature.duck", + "id": "jessieu726" + }, + "role": "w" + }, + "kyle11878": { + "user": { + "name": "kyle11878", + "flair": "activity.lichess-horsey", + "id": "kyle11878" + }, + "role": "w" + } + }, + "position": { + "chapterId": "EgqyeQIp", + "path": "" + }, + "ownerId": "kyle-and-jess", + "settings": { + "explorer": "contributor", + "description": false, + "computer": "contributor", + "chat": "everyone", + "sticky": false, + "shareable": "contributor", + "cloneable": "contributor" + }, + "visibility": "public", + "createdAt": 1729286237789, + "secondsSinceUpdate": 4116, + "from": "scratch", + "likes": 29, + "flair": "activity.puzzle-piece", + "liked": false, + "features": { + "cloneable": false, + "shareable": false, + "chat": true + }, + "topics": [], + "chapter": { + "id": "7OJXp679", + "ownerId": "kyle-and-jess", + "setup": { + "variant": { + "key": "standard", + "name": "Standard" + }, + "orientation": "black", + "fromFen": true + }, + "tags": [], + "features": { + "computer": false, + "explorer": false + }, + "gamebook": true + }, + "chapters": [ + { + "id": "EgqyeQIp", + "name": "Introduction" + }, + { + "id": "z6tGV47W", + "name": "Practice Your Thought Process", + "fen": "2k4r/p1p2p2/1p2b2p/1Pqn2r1/2B5/B1PP4/P4PPP/RN2Q1K1 b - - 6 20", + "orientation": "black" + }, + { + "id": "dTfxbccx", + "name": "Practice Strategic Thinking", + "fen": "r3r1k1/1b2b2p/pq4pB/1p3pN1/2p5/2P5/PPn1QPPP/3RR1K1 w - - 0 23" + }, + { + "id": "B1U4pFdG", + "name": "Calculate Fully", + "fen": "3r3r/1Rpk1p2/2p2q1p/Q2pp3/P2PP1n1/2P1B1Pp/5P2/1N3RK1 b - - 2 26", + "orientation": "black" + }, + { + "id": "NJLW7jil", + "name": "Calculate Freely", + "fen": "4k3/8/6p1/R1p1r1n1/P3Pp2/2N2r2/1PP1K1R1/8 b - - 2 39", + "orientation": "black" + }, + { + "id": "7OJXp679", + "name": "Use a Timer", + "fen": "r5k1/ppp2ppp/7r/4Nb2/3P4/1QN1PPq1/PP2B1P1/R4RK1 b - - 1 20", + "orientation": "black" + }, + { + "id": "Rgk6UlTP", + "name": "Understand Your Mistakes", + "fen": "r4rk1/1R3pb1/pR2N1p1/2q5/4p3/2P1P1Pp/Q2P1P1P/6K1 b - - 1 26", + "orientation": "black" + }, + { + "id": "VsdxmjCf", + "name": "Adjusting Difficulty", + "fen": "3r4/k1pq1p1r/pp1p2p1/8/3P4/P1P2BP1/1P1N1Pp1/R3R1K1 b - - 0 1", + "orientation": "black" + }, + { + "id": "FHU6xhYs", + "name": "Using Themes", + "fen": "r2k3N/pbpp1Bpp/1p6/2b1p3/3n3q/P7/1PPP1RPP/RNB2QK1 b - - 3 12", + "orientation": "black" + }, + { + "id": "8FhO455h", + "name": "Endurance Training", + "fen": "8/1p5k/2qPQ2p/p5p1/5r1n/2B4P/5P2/4R1K1 w - - 3 41" + }, + { + "id": "jWUEWsEf", + "name": "Final Thoughts", + "fen": "8/1PP2PP1/PppPPppP/Pp1pp1pP/Pp4pP/1Pp2pP1/2PppP2/3PP3 w - - 0 1" + } + ], + "federations": {} + }, + "analysis": { + "game": { + "id": "synthetic", + "variant": { + "key": "standard", + "name": "Standard", + "short": "Std" + }, + "opening": null, + "fen": "r5k1/ppp2ppp/7r/4Nb2/3P4/1QN1PPq1/PP2B1P1/R4RK1 b - - 1 20", + "turns": 39, + "player": "black", + "status": { + "id": 10, + "name": "created" + }, + "initialFen": "r5k1/ppp2ppp/7r/4Nb2/3P4/1QN1PPq1/PP2B1P1/R4RK1 b - - 1 20" + }, + "player": { + "id": null, + "color": "black" + }, + "opponent": { + "color": "white", + "ai": null + }, + "orientation": "black", + "pref": { + "animationDuration": 300, + "coords": 1, + "moveEvent": 2, + "showCaptured": true, + "keyboardMove": false, + "rookCastle": true, + "highlight": true, + "destination": true + }, + "userAnalysis": true, + "treeParts": [ + { + "ply": 39, + "fen": "r5k1/ppp2ppp/7r/4Nb2/3P4/1QN1PPq1/PP2B1P1/R4RK1 b - - 1 20", + "comments": [ + { + "id": "4nZ6", + "text": "Using a timer can be great during puzzle solving, and I don't mean timing yourself to solve quickly. What I mean is setting a timer that restricts when you're allowed to play a move. Start with a minute or two (for more difficult puzzles; if you're solving easy puzzles, you don't need the timer) and calculate the entire time. When you're solving even harder puzzles, set an even longer timer (5-10 minutes maybe). Practice pushing calculations further and looking at different lines during that time (for very difficult puzzles, you should have plenty to calculate). This is to train yourself to take time during important moments, instead of rushing through the position. Set a timer for one to two minutes and calculate this position as black as fully as you can.", + "by": { + "id": "kyle-and-jess", + "name": "Kyle-and-Jess" + } + } + ], + "gamebook": { + "hint": "The white king is not very safe. Can black increase the pressure on the king?" + }, + "dests": "456789 LbktxCESUZ6 wenopvxDEFKMU WGO YIQ 2MU XHP VhpxFNOPQRSTU !9?" + }, + { + "ply": 40, + "fen": "r5k1/ppp2ppp/8/4Nb2/3P4/1QN1PPq1/PP2B1Pr/R4RK1 w - - 2 21", + "id": "R2", + "uci": "h6h2", + "san": "Rh2", + "gamebook": { + "deviation": "Black has to be quick to jump on the initiative of white's king being vulnerable." + } + }, + { + "ply": 41, + "fen": "r5k1/ppp2Qpp/8/4Nb2/3P4/2N1PPq1/PP2B1Pr/R4RK1 b - - 0 21", + "id": "4X", + "uci": "b3f7", + "san": "Qxf7+", + "check": true + }, + { + "ply": 42, + "fen": "r6k/ppp2Qpp/8/4Nb2/3P4/2N1PPq1/PP2B1Pr/R4RK1 w - - 1 22", + "id": "ab", + "uci": "g8h8", + "san": "Kh8" + }, + { + "ply": 43, + "fen": "r6k/ppp2Qpp/8/4Nb2/3P4/2N1PPq1/PP2BRPr/R5K1 b - - 2 22", + "id": "(0", + "uci": "f1f2", + "san": "Rf2" + }, + { + "ply": 44, + "fen": "r6k/ppp2Qpp/8/4Nb2/3P3q/2N1PP2/PP2BRPr/R5K1 w - - 3 23", + "id": "9B", + "uci": "g3h4", + "san": "Qh4", + "gamebook": { + "deviation": "Keep the initiative going! Go for the king!" + } + }, + { + "ply": 45, + "fen": "r5Qk/ppp3pp/8/4Nb2/3P3q/2N1PP2/PP2BRPr/R5K1 b - - 4 23", + "id": "Xa", + "uci": "f7g8", + "san": "Qg8+", + "check": true, + "children": [ + { + "ply": 46, + "fen": "6rk/ppp3pp/8/4Nb2/3P3q/2N1PP2/PP2BRPr/R5K1 w - - 0 24", + "id": "[a", + "uci": "a8g8", + "san": "Rxg8", + "comments": [ + { + "id": "lq80", + "text": "This allows for Nf7#", + "by": { + "id": "kyle-and-jess", + "name": "Kyle-and-Jess" + } + } + ], + "glyphs": [ + { + "id": 4, + "symbol": "??", + "name": "Blunder" + } + ], + "children": [] + } + ] + }, + { + "ply": 46, + "fen": "r5k1/ppp3pp/8/4Nb2/3P3q/2N1PP2/PP2BRPr/R5K1 w - - 0 24", + "id": "ba", + "uci": "h8g8", + "san": "Kxg8", + "comments": [ + { + "id": "sAXm", + "text": "Good job avoiding the smothered mate!", + "by": { + "id": "kyle-and-jess", + "name": "Kyle-and-Jess" + } + } + ] + } + ] + } +} +'''; + + final mockClient = MockClient((request) { + if (request.url.path == '/study/JbWtuaeK/7OJXp679') { + expect(request.url.queryParameters['chapters'], '1'); + return mockResponse( + response, + 200, + ); + } else if (request.url.path == '/api/study/JbWtuaeK/7OJXp679.pgn') { + return mockResponse( + 'pgn', + 200, + ); + } + return mockResponse('', 404); + }); + + final repo = StudyRepository(mockClient); + + final (study, pgn) = await repo.getStudy( + id: const StudyId('JbWtuaeK'), + chapterId: const StudyChapterId('7OJXp679'), + ); + + expect(pgn, 'pgn'); + + expect( + study, + Study( + id: const StudyId('JbWtuaeK'), + name: 'How to Solve Puzzles Correctly', + position: const ( + chapterId: StudyChapterId('EgqyeQIp'), + path: '', + ), + liked: false, + likes: 29, + ownerId: const UserId('kyle-and-jess'), + features: ( + cloneable: false, + chat: true, + sticky: false, + ), + topics: const IList.empty(), + chapters: IList( + const [ + StudyChapterMeta( + id: StudyChapterId('EgqyeQIp'), + name: 'Introduction', + fen: null, + ), + StudyChapterMeta( + id: StudyChapterId('z6tGV47W'), + name: 'Practice Your Thought Process', + fen: + '2k4r/p1p2p2/1p2b2p/1Pqn2r1/2B5/B1PP4/P4PPP/RN2Q1K1 b - - 6 20', + ), + StudyChapterMeta( + id: StudyChapterId('dTfxbccx'), + name: 'Practice Strategic Thinking', + fen: + 'r3r1k1/1b2b2p/pq4pB/1p3pN1/2p5/2P5/PPn1QPPP/3RR1K1 w - - 0 23', + ), + StudyChapterMeta( + id: StudyChapterId('B1U4pFdG'), + name: 'Calculate Fully', + fen: + '3r3r/1Rpk1p2/2p2q1p/Q2pp3/P2PP1n1/2P1B1Pp/5P2/1N3RK1 b - - 2 26', + ), + StudyChapterMeta( + id: StudyChapterId('NJLW7jil'), + name: 'Calculate Freely', + fen: '4k3/8/6p1/R1p1r1n1/P3Pp2/2N2r2/1PP1K1R1/8 b - - 2 39', + ), + StudyChapterMeta( + id: StudyChapterId('7OJXp679'), + name: 'Use a Timer', + fen: + 'r5k1/ppp2ppp/7r/4Nb2/3P4/1QN1PPq1/PP2B1P1/R4RK1 b - - 1 20', + ), + StudyChapterMeta( + id: StudyChapterId('Rgk6UlTP'), + name: 'Understand Your Mistakes', + fen: + 'r4rk1/1R3pb1/pR2N1p1/2q5/4p3/2P1P1Pp/Q2P1P1P/6K1 b - - 1 26', + ), + StudyChapterMeta( + id: StudyChapterId('VsdxmjCf'), + name: 'Adjusting Difficulty', + fen: + '3r4/k1pq1p1r/pp1p2p1/8/3P4/P1P2BP1/1P1N1Pp1/R3R1K1 b - - 0 1', + ), + StudyChapterMeta( + id: StudyChapterId('FHU6xhYs'), + name: 'Using Themes', + fen: + 'r2k3N/pbpp1Bpp/1p6/2b1p3/3n3q/P7/1PPP1RPP/RNB2QK1 b - - 3 12', + ), + StudyChapterMeta( + id: StudyChapterId('8FhO455h'), + name: 'Endurance Training', + fen: '8/1p5k/2qPQ2p/p5p1/5r1n/2B4P/5P2/4R1K1 w - - 3 41', + ), + StudyChapterMeta( + id: StudyChapterId('jWUEWsEf'), + name: 'Final Thoughts', + fen: + '8/1PP2PP1/PppPPppP/Pp1pp1pP/Pp4pP/1Pp2pP1/2PppP2/3PP3 w - - 0 1', + ), + ], + ), + chapter: const StudyChapter( + id: StudyChapterId('7OJXp679'), + setup: StudyChapterSetup( + id: null, + orientation: Side.black, + variant: Variant.standard, + fromFen: true, + ), + practise: false, + conceal: null, + gamebook: true, + features: ( + computer: false, + explorer: false, + ), + ), + ), + ); + }); + }); +} From 9605fa4210d08900f1fa0e04351955146ad0a496 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Sun, 20 Oct 2024 23:27:43 +0200 Subject: [PATCH 542/979] use pick instead of generated json --- lib/src/model/common/id.dart | 10 +++++++ lib/src/model/study/study.dart | 31 ++++++++++++++++++--- lib/src/model/study/study_repository.dart | 6 +--- test/model/study/study_repository_test.dart | 4 --- 4 files changed, 38 insertions(+), 13 deletions(-) diff --git a/lib/src/model/common/id.dart b/lib/src/model/common/id.dart index 8d9b7225e7..813de7f8b4 100644 --- a/lib/src/model/common/id.dart +++ b/lib/src/model/common/id.dart @@ -196,4 +196,14 @@ extension IDPick on Pick { return null; } } + + StudyId asStudyIdOrThrow() { + final value = required().value; + if (value is String) { + return StudyId(value); + } + throw PickException( + "value $value at $debugParsingExit can't be casted to StudyId", + ); + } } diff --git a/lib/src/model/study/study.dart b/lib/src/model/study/study.dart index d1de222a78..d3eb929b99 100644 --- a/lib/src/model/study/study.dart +++ b/lib/src/model/study/study.dart @@ -1,5 +1,6 @@ import 'package:collection/collection.dart'; import 'package:dartchess/dartchess.dart'; +import 'package:deep_pick/deep_pick.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; @@ -9,18 +10,17 @@ import 'package:lichess_mobile/src/model/user/user.dart'; part 'study.freezed.dart'; part 'study.g.dart'; -@Freezed(fromJson: true) +@freezed class Study with _$Study { const Study._(); const factory Study({ required StudyId id, required String name, - required ({StudyChapterId chapterId, String path}) position, required bool liked, required int likes, required UserId? ownerId, - @JsonKey(fromJson: studyFeaturesFromJson) required StudyFeatures features, + required StudyFeatures features, required IList topics, required IList chapters, required StudyChapter chapter, @@ -29,7 +29,30 @@ class Study with _$Study { StudyChapterMeta get currentChapterMeta => chapters.firstWhere((c) => c.id == chapter.id); - factory Study.fromJson(Map json) => _$StudyFromJson(json); + factory Study.fromServerJson(Map json) => + _studyFromPick(pick(json).required()); +} + +Study _studyFromPick(RequiredPick pick) { + final study = pick('study'); + return Study( + id: study('id').asStudyIdOrThrow(), + name: study('name').asStringOrThrow(), + liked: study('liked').asBoolOrThrow(), + likes: study('likes').asIntOrThrow(), + ownerId: study('ownerId').asUserIdOrNull(), + features: ( + cloneable: study('features', 'cloneable').asBoolOrFalse(), + chat: study('features', 'chat').asBoolOrFalse(), + sticky: study('features', 'sticky').asBoolOrFalse(), + ), + topics: + study('topics').asListOrThrow((pick) => pick.asStringOrThrow()).lock, + chapters: study('chapters') + .asListOrThrow((pick) => StudyChapterMeta.fromJson(pick.asMapOrThrow())) + .lock, + chapter: StudyChapter.fromJson(study('chapter').asMapOrThrow()), + ); } typedef StudyFeatures = ({ diff --git a/lib/src/model/study/study_repository.dart b/lib/src/model/study/study_repository.dart index 5b479a7ef7..59e552d4d3 100644 --- a/lib/src/model/study/study_repository.dart +++ b/lib/src/model/study/study_repository.dart @@ -69,11 +69,7 @@ class StudyRepository { }, ), headers: {'Accept': 'application/json'}, - mapper: (Map json) { - return Study.fromJson( - pick(json, 'study').asMapOrThrow(), - ); - }, + mapper: Study.fromServerJson, ); final pgnBytes = await client.readBytes( diff --git a/test/model/study/study_repository_test.dart b/test/model/study/study_repository_test.dart index 2a120dcd5f..baa8565087 100644 --- a/test/model/study/study_repository_test.dart +++ b/test/model/study/study_repository_test.dart @@ -340,10 +340,6 @@ void main() { Study( id: const StudyId('JbWtuaeK'), name: 'How to Solve Puzzles Correctly', - position: const ( - chapterId: StudyChapterId('EgqyeQIp'), - path: '', - ), liked: false, likes: 29, ownerId: const UserId('kyle-and-jess'), From 19c946b35dd399cf6fb882063e0a5d61fd57e1c2 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Sun, 20 Oct 2024 23:34:35 +0200 Subject: [PATCH 543/979] add support for hints and deviations --- lib/src/model/study/study.dart | 19 +++++++++++++++++++ test/model/study/study_repository_test.dart | 20 ++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/lib/src/model/study/study.dart b/lib/src/model/study/study.dart index d3eb929b99..7295fa3856 100644 --- a/lib/src/model/study/study.dart +++ b/lib/src/model/study/study.dart @@ -24,6 +24,13 @@ class Study with _$Study { required IList topics, required IList chapters, required StudyChapter chapter, + // Hints to display in "gamebook"/"interactive" mode + // Index corresponds to the current ply. + required IList hints, + // Comment to display when deviating from the mainline in "gamebook" mode + // (i.e. when making a wrong move). + // Index corresponds to the current ply. + required IList deviationComments, }) = _Study; StudyChapterMeta get currentChapterMeta => @@ -34,6 +41,16 @@ class Study with _$Study { } Study _studyFromPick(RequiredPick pick) { + final treeParts = pick('analysis', 'treeParts').asListOrThrow((part) => part); + + final hints = []; + final deviationComments = []; + + for (final part in treeParts) { + hints.add(part('gamebook', 'hint').asStringOrNull()); + deviationComments.add(part('gamebook', 'deviation').asStringOrNull()); + } + final study = pick('study'); return Study( id: study('id').asStudyIdOrThrow(), @@ -52,6 +69,8 @@ Study _studyFromPick(RequiredPick pick) { .asListOrThrow((pick) => StudyChapterMeta.fromJson(pick.asMapOrThrow())) .lock, chapter: StudyChapter.fromJson(study('chapter').asMapOrThrow()), + hints: hints.lock, + deviationComments: deviationComments.lock, ); } diff --git a/test/model/study/study_repository_test.dart b/test/model/study/study_repository_test.dart index baa8565087..d3c269da6c 100644 --- a/test/model/study/study_repository_test.dart +++ b/test/model/study/study_repository_test.dart @@ -432,6 +432,26 @@ void main() { explorer: false, ), ), + hints: [ + 'The white king is not very safe. Can black increase the pressure on the king?', + null, + null, + null, + null, + null, + null, + null, + ].lock, + deviationComments: [ + null, + "Black has to be quick to jump on the initiative of white's king being vulnerable.", + null, + null, + null, + 'Keep the initiative going! Go for the king!', + null, + null, + ].lock, ), ); }); From eb3161c9e15f80c607370ca16935924e4e479a78 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Sun, 20 Oct 2024 23:37:52 +0200 Subject: [PATCH 544/979] remove now unused helper function --- lib/src/model/study/study.dart | 8 -------- 1 file changed, 8 deletions(-) diff --git a/lib/src/model/study/study.dart b/lib/src/model/study/study.dart index 7295fa3856..5a68fa7f4e 100644 --- a/lib/src/model/study/study.dart +++ b/lib/src/model/study/study.dart @@ -80,14 +80,6 @@ typedef StudyFeatures = ({ bool sticky, }); -StudyFeatures studyFeaturesFromJson(Map json) { - return ( - cloneable: json['cloneable'] as bool? ?? false, - chat: json['chat'] as bool? ?? false, - sticky: json['sticky'] as bool? ?? false, - ); -} - @Freezed(fromJson: true) class StudyChapter with _$StudyChapter { const StudyChapter._(); From 87c00d532963fa7bcc3e7a71a3b016ee21585f64 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Mon, 21 Oct 2024 09:55:43 +0200 Subject: [PATCH 545/979] fix doc comments --- lib/src/model/study/study.dart | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/src/model/study/study.dart b/lib/src/model/study/study.dart index 5a68fa7f4e..ea04c948e7 100644 --- a/lib/src/model/study/study.dart +++ b/lib/src/model/study/study.dart @@ -24,12 +24,14 @@ class Study with _$Study { required IList topics, required IList chapters, required StudyChapter chapter, - // Hints to display in "gamebook"/"interactive" mode - // Index corresponds to the current ply. + + /// Hints to display in "gamebook"/"interactive" mode + /// Index corresponds to the current ply. required IList hints, - // Comment to display when deviating from the mainline in "gamebook" mode - // (i.e. when making a wrong move). - // Index corresponds to the current ply. + + /// Comment to display when deviating from the mainline in "gamebook" mode + /// (i.e. when making a wrong move). + /// Index corresponds to the current ply. required IList deviationComments, }) = _Study; From a01ed8a7a3e4c2dd80610e0442a80b2545ef9a5f Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Fri, 25 Oct 2024 23:21:01 +0200 Subject: [PATCH 546/979] add provider for study repository (so that we can mock it in tests) --- lib/src/model/study/study_repository.dart | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/src/model/study/study_repository.dart b/lib/src/model/study/study_repository.dart index 59e552d4d3..d9032a24ba 100644 --- a/lib/src/model/study/study_repository.dart +++ b/lib/src/model/study/study_repository.dart @@ -2,11 +2,20 @@ import 'dart:convert'; import 'package:deep_pick/deep_pick.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:http/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/study/study.dart'; import 'package:lichess_mobile/src/model/study/study_filter.dart'; import 'package:lichess_mobile/src/network/http.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'study_repository.g.dart'; + +@Riverpod(keepAlive: true) +StudyRepository studyRepository(Ref ref) { + return StudyRepository(ref.read(lichessClientProvider)); +} class StudyRepository { StudyRepository(this.client); From aea420e62b1c691a5fe2197792d18412aa5377a4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Oct 2024 18:36:47 +0000 Subject: [PATCH 547/979] Bump rexml from 3.3.3 to 3.3.9 in /android Bumps [rexml](https://github.com/ruby/rexml) from 3.3.3 to 3.3.9. - [Release notes](https://github.com/ruby/rexml/releases) - [Changelog](https://github.com/ruby/rexml/blob/master/NEWS.md) - [Commits](https://github.com/ruby/rexml/compare/v3.3.3...v3.3.9) --- updated-dependencies: - dependency-name: rexml dependency-type: indirect ... Signed-off-by: dependabot[bot] --- android/Gemfile.lock | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/android/Gemfile.lock b/android/Gemfile.lock index cbd2b355ea..d7ff3604de 100644 --- a/android/Gemfile.lock +++ b/android/Gemfile.lock @@ -171,8 +171,7 @@ GEM trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (3.1.2) - rexml (3.3.3) - strscan + rexml (3.3.9) rouge (2.0.7) ruby2_keywords (0.0.5) rubyzip (2.3.2) @@ -185,7 +184,6 @@ GEM simctl (1.6.10) CFPropertyList naturally - strscan (3.1.0) terminal-notifier (2.0.0) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) From 87d307f9313e5be0efe96465c7eed0bc6907bb0d Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 29 Oct 2024 10:04:44 +0100 Subject: [PATCH 548/979] Add study and bersek icons --- assets/fonts/LichessIcons.ttf | Bin 15292 -> 16116 bytes fluttericon.json | 30 +++++++++++++++++++++++++++++- lib/src/styles/lichess_icons.dart | 2 ++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/assets/fonts/LichessIcons.ttf b/assets/fonts/LichessIcons.ttf index 4a9a94b90a29e62cb954fb4ba4c5baf0f1c02b92..faa08325e3952d59f910e45d91971eca7bfbed3a 100644 GIT binary patch delta 1451 zcmYk6e`pj(6vyA}&du&!E_ZuB_BMLSU3NFOt2yst?rv{QyqNZ`#%RU@LhMpFFFVrvS*i0I+vxd~nkJdC>>JT?e2a9UeO|0`~J60O2M;`+?Cz zgTt-;k1k;B&q#h09U@1Mq`$1kD>e(CAk|DUdYyLyN|;Lo8g0$1~5tIe}2F>;K2Q#ng^kaLy0jaHCR7iW)quhh&W(YD9^ax?dBnZG?jQ`q2#l`vBk;doImjOtvXH}q zta6*LR=y;z$}P&=r!YsY<>r5)XGnQb1*icU>c4;vu#|Lw7Sv4+kU;Hq00VWW1K{2* zz32eAWlJwPz(#<_UI%DJ4LAViUhY_92mq5WcRD}{wcr40)NTi8Lw(i(HlvmtpdEF) z1K@>N-r)dQfIsg!K(3M(b|#JnuLXY#Z3~TsJ`MdIj)ixJN90cV{YZc0vJzHClm`uk zhAZyOIf3iATUC!wSd53s& zEeU;~_;j&2Fi_+giUaQvFFBpit?HBNY(nRDKYh41qLlWLf!Yb+jb8nPzl-<72Quz? zE4%?0;9?mr!DX0(Yw#J|gZp@Z85>Jmydn6A5+fl+(9$IDiexi+J8zg86L=Pi5gau` zx*bs>`EHUojW(h&nP=IEpXjDxXKESz#7E*JM(n(;=vGeGnuxB2m8=}jX1Yigvu`Ek zZKRE41*Sw4tk6yHVdWB*X$TsN6FGuqFt>ptA+&WND%5~ZBm-N5*F+} z(c9Z4iZn&{w5A15anr-=-f&%Gnl`$dqHS+_++9=(P}d}-K_+%l|J!YOp^2rUKB=zH zA6#i_S_%4(iqvTEMw?xHfig*ode<4ect*=Sy%C}k^KfA*t*`g+F}6XS+7f4F`rL}D zYN|SsO3^#&3XS_BJS%=rwtM@U8~O+Q^Mimq>{i z;60-SyN9?QaQXh;KWL9#81*o*`d5IE`d!K77QQMzQ^&F9#w)3frbiK2_2VEfC6f~G z5zFMY098DD+UBAvJN`+>Dlj=>hdsRl=UfqbnGI2*VOdP@CotY#PEsK(9Cp&ZM{a|AMS-I3jhEB delta 624 zcmYk2O=uHA6vzLwyEOF!W41~BIE30%ZEYHBBE?9w4I;JnV7>GpRJ%4w7fI|ETdjih z){{udK1Sxn>R6P2HLR)(XDt^F9!Atz_P7gBt_|1DW@4aE%+tnL$@fimY zcK{5HmMzCF?egUG z^27Q9`>iv6T^b*?x;t`jS$~YNUACqjF)R-9d^h9ft5&)2>Dk_M@7T)#^e$)o+T^zf z_nrWmqkP2dd^O&ahTvVY@^a$KrC|0Cw&^uyur|Ns$#?x#yKke5bEn{Lr zJePB#Pe}P*+?P|@PAgbtcL{nc2gYFFJF8>Z$;^&gvvjrLJr^;8%gFnyi2s(n zqK~mbgIv}B%3gg-aJx5Ivp46bDqoGeV&+!|;DHu8p+KBIqd+U&ufTqKP=N#Vc?I}q zzuDabZFES1cEHUjaELyx0Oz?`p86`*d`ji5P>C*~y(R~UW0X9A;kdhYy z{rgIx<AK&z=?X H#L1<9k`S4b diff --git a/fluttericon.json b/fluttericon.json index 28d7a4bff0..e5c483d5b4 100644 --- a/fluttericon.json +++ b/fluttericon.json @@ -537,6 +537,34 @@ "search": [ "book_lichess" ] + }, + { + "uid": "df30e1b667ba2db43d2ad1ca361c21fa", + "css": "study", + "code": 59425, + "src": "custom_icons", + "selected": true, + "svg": { + "path": "M892.6 705.1V437.5H500V44.9H232.4Q179.7 44.9 144.5 82 107.4 117.2 107.4 169.9V437.5H500V830.1H767.6Q820.3 830.1 855.5 793 892.6 757.8 892.6 705.1ZM927.7 169.9V705.1Q927.7 771.5 880.9 818.4T767.6 865.2H232.4Q166 865.2 119.1 818.4T72.3 705.1V169.9Q72.3 103.5 119.1 56.6T232.4 9.8H767.6Q834 9.8 880.9 56.6T927.7 169.9Z", + "width": 1020 + }, + "search": [ + "study" + ] + }, + { + "uid": "5666e3cf8e8a53bb514d650488dd9dce", + "css": "body-cut", + "code": 59426, + "src": "custom_icons", + "selected": true, + "svg": { + "path": "M445.3 236.3Q453.1 236.3 472.7 232.4 480.5 232.4 482.4 226.6 486.3 218.8 486.3 214.8L439.5-48.8Q435.5-66.4 419.9-62.5 357.4-52.7 322.3-1T298.8 111.3Q306.6 166 348.6 201.2T445.3 236.3ZM410.2-25.4L449.2 201.2 445.3 203.1Q402.3 203.1 371.1 175.8T332 105.5 346.7 25.4 410.2-25.4ZM710.9 232.4Q730.5 236.3 738.3 236.3 793 236.3 835 201.2T884.8 111.3Q896.5 50.8 861.3-1T763.7-62.5Q755.9-62.5 752-58.6 748-56.6 744.1-48.8L699.2 214.8Q695.3 232.4 710.9 232.4ZM773.4-25.4Q814.5-11.7 837.9 25.4T853.5 105.5Q843.8 148.4 810.5 175.8T734.4 201.2ZM966.8 705.1H783.2L794.9 640.6Q814.5 652.3 826.2 654.3 853.5 660.2 877 643.6T904.3 599.6L931.6 423.8Q941.4 378.9 912.1 337.9 884.8 298.8 837.9 289.1L707 265.6Q697.3 265.6 693.4 269.5 687.5 273.4 687.5 279.3L627 625Q623 642.6 638.7 642.6 656.3 646.5 652.3 662.1L646.5 705.1H535.2L531.3 662.1Q527.3 646.5 544.9 642.6 550.8 642.6 554.7 636.7 558.6 632.8 558.6 625L498 279.3Q494.1 261.7 478.5 265.6L345.7 289.1Q298.8 298.8 271.5 337.9T252 423.8L281.3 597.7Q287.1 625 308.6 640.6 332 658.2 359.4 652.3 373 650.4 388.7 638.7L400.4 705.1H316.4V687.5Q316.4 669.9 300.8 669.9 283.2 669.9 283.2 687.5V705.1H33.2Q3.9 705.1 3.9 771.5T33.2 837.9H283.2V886.7Q283.2 904.3 300.8 904.3 316.4 904.3 316.4 886.7V837.9H425.8L433.6 882.8Q437.5 906.3 457 921.9T500 937.5H511.7Q539.1 931.6 554.7 909.2T566.4 859.4L562.5 837.9H623L619.1 859.4Q613.3 884.8 628.9 910.2 644.5 931.6 671.9 937.5H683.6Q709 937.5 727.5 921.9T750 882.8L757.8 837.9H900.4Q933.6 837.9 959 796.9T984.4 720.7Q984.4 705.1 966.8 705.1ZM412.1 578.1V576.2L392.6 462.9Q388.7 445.3 373 449.2 367.2 449.2 362.3 455.1T359.4 466.8L378.9 580.1V582Q382.8 595.7 375 606.4T353.5 619.1 328.1 613.3Q318.4 607.4 314.5 591.8L285.2 418Q279.3 384.8 298.8 356.4T351.6 322.3L466.8 300.8 523.4 617.2Q492.2 632.8 498 668L502 705.1H433.6ZM183.6 738.3H216.8V804.7H183.6V738.3ZM150.4 804.7H117.2V738.3H150.4V804.7ZM39.1 738.3H84V804.7H39.1Q35.2 794.9 35.2 771.5T39.1 738.3ZM250 804.7V738.3H283.2V804.7H250ZM533.2 865.2Q537.1 896.5 505.9 904.3 492.2 906.3 481.4 898.4T466.8 877L460.9 837.9H527.3ZM316.4 804.7V738.3H640.6L628.9 804.7H316.4ZM716.8 877Q709 910.2 677.7 904.3 664.1 900.4 656.3 890.6 650.4 878.9 650.4 865.2L685.5 668Q691.4 632.8 662.1 617.2L716.8 300.8 832 322.3Q867.2 328.1 884.8 355.5 904.3 384.8 898.4 418L871.1 595.7Q863.3 627 832 621.1 820.3 621.1 810.5 607.4 804.7 599.6 804.7 584V582L826.2 466.8Q826.2 451.2 812.5 447.3 796.9 443.4 793 460.9L771.5 580.1V582ZM900.4 804.7H763.7L777.3 738.3H947.3Q943.4 761.7 928.7 783.2T900.4 804.7Z", + "width": 1023 + }, + "search": [ + "body-cut" + ] } ] -} \ No newline at end of file +} diff --git a/lib/src/styles/lichess_icons.dart b/lib/src/styles/lichess_icons.dart index 628ffcc5fa..12ed2cb177 100644 --- a/lib/src/styles/lichess_icons.dart +++ b/lib/src/styles/lichess_icons.dart @@ -66,6 +66,8 @@ class LichessIcons { static const IconData radio_tower_lichess = IconData(0xe81e, fontFamily: _kFontFam, fontPackage: _kFontPkg); static const IconData crossed_swords = IconData(0xe81f, fontFamily: _kFontFam, fontPackage: _kFontPkg); static const IconData book_lichess = IconData(0xe820, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData study = IconData(0xe821, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData body_cut = IconData(0xe822, fontFamily: _kFontFam, fontPackage: _kFontPkg); static const IconData tag = IconData(0xf02b, fontFamily: _kFontFam, fontPackage: _kFontPkg); static const IconData tags = IconData(0xf02c, fontFamily: _kFontFam, fontPackage: _kFontPkg); static const IconData step_backward = IconData(0xf048, fontFamily: _kFontFam, fontPackage: _kFontPkg); From a897dff9b8de157b6ba0b80161f24c7c3dc2b09e Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 29 Oct 2024 20:43:28 +0100 Subject: [PATCH 549/979] Fix going back from puzzle history --- lib/src/model/puzzle/puzzle_activity.dart | 4 +- .../view/puzzle/puzzle_history_screen.dart | 46 ++-- lib/src/view/puzzle/puzzle_screen.dart | 1 - lib/src/view/puzzle/puzzle_tab_screen.dart | 75 ++++--- test/model/puzzle/mock_server_responses.dart | 6 + test/model/puzzle/puzzle_repository_test.dart | 18 ++ .../puzzle/puzzle_history_screen_test.dart | 204 ++++++++++++++++++ 7 files changed, 296 insertions(+), 58 deletions(-) create mode 100644 test/view/puzzle/puzzle_history_screen_test.dart diff --git a/lib/src/model/puzzle/puzzle_activity.dart b/lib/src/model/puzzle/puzzle_activity.dart index 9addb76e85..79b245488f 100644 --- a/lib/src/model/puzzle/puzzle_activity.dart +++ b/lib/src/model/puzzle/puzzle_activity.dart @@ -23,7 +23,7 @@ class PuzzleActivity extends _$PuzzleActivity { @override Future build() async { - ref.cacheFor(const Duration(minutes: 30)); + ref.cacheFor(const Duration(minutes: 5)); ref.onDispose(() { _list.clear(); }); @@ -64,7 +64,7 @@ class PuzzleActivity extends _$PuzzleActivity { if (!state.hasValue) return; final currentVal = state.requireValue; - if (_list.length < _maxPuzzles) { + if (currentVal.hasMore && _list.length < _maxPuzzles) { state = AsyncData(currentVal.copyWith(isLoading: true)); Result.capture( ref.withClient( diff --git a/lib/src/view/puzzle/puzzle_history_screen.dart b/lib/src/view/puzzle/puzzle_history_screen.dart index d2df0b6ca2..6b715d2788 100644 --- a/lib/src/view/puzzle/puzzle_history_screen.dart +++ b/lib/src/view/puzzle/puzzle_history_screen.dart @@ -20,18 +20,6 @@ import 'package:timeago/timeago.dart' as timeago; final _dateFormatter = DateFormat.yMMMd(); -class PuzzleHistoryScreen extends StatelessWidget { - const PuzzleHistoryScreen(); - - @override - Widget build(BuildContext context) { - return PlatformScaffold( - appBar: PlatformAppBar(title: Text(context.l10n.puzzleHistory)), - body: _Body(), - ); - } -} - /// Shows a short preview of the puzzle history. class PuzzleHistoryPreview extends ConsumerWidget { const PuzzleHistoryPreview(this.history, {this.maxRows, super.key}); @@ -79,6 +67,19 @@ class PuzzleHistoryPreview extends ConsumerWidget { } } +/// A screen that displays the full puzzle history. +class PuzzleHistoryScreen extends StatelessWidget { + const PuzzleHistoryScreen(); + + @override + Widget build(BuildContext context) { + return PlatformScaffold( + appBar: PlatformAppBar(title: Text(context.l10n.puzzleHistory)), + body: _Body(), + ); + } +} + class _Body extends ConsumerStatefulWidget { @override ConsumerState<_Body> createState() => _BodyState(); @@ -88,8 +89,6 @@ const _kPuzzlePadding = 10.0; class _BodyState extends ConsumerState<_Body> { final ScrollController _scrollController = ScrollController(); - bool _hasMore = true; - bool _isLoading = false; @override void initState() { @@ -105,9 +104,10 @@ class _BodyState extends ConsumerState<_Body> { } void _scrollListener() { - if (_scrollController.position.pixels == - _scrollController.position.maxScrollExtent) { - if (_hasMore && !_isLoading) { + if (_scrollController.position.pixels >= + _scrollController.position.maxScrollExtent * 0.7) { + final currentState = ref.read(puzzleActivityProvider).valueOrNull; + if (currentState != null && !currentState.isLoading) { ref.read(puzzleActivityProvider.notifier).getNext(); } } @@ -119,8 +119,6 @@ class _BodyState extends ConsumerState<_Body> { return historyState.when( data: (state) { - _hasMore = state.hasMore; - _isLoading = state.isLoading; if (state.hasError) { showPlatformSnackbar( context, @@ -162,8 +160,10 @@ class _BodyState extends ConsumerState<_Body> { child: Row( children: element .map( - (e) => - _HistoryBoard(e as PuzzleHistoryEntry, boardWidth), + (e) => PuzzleHistoryBoard( + e as PuzzleHistoryEntry, + boardWidth, + ), ) .toList(), ), @@ -200,8 +200,8 @@ class _BodyState extends ConsumerState<_Body> { } } -class _HistoryBoard extends ConsumerWidget { - const _HistoryBoard(this.puzzle, this.boardWidth); +class PuzzleHistoryBoard extends ConsumerWidget { + const PuzzleHistoryBoard(this.puzzle, this.boardWidth); final PuzzleHistoryEntry puzzle; final double boardWidth; diff --git a/lib/src/view/puzzle/puzzle_screen.dart b/lib/src/view/puzzle/puzzle_screen.dart index 74dddd04cc..6f51d20735 100644 --- a/lib/src/view/puzzle/puzzle_screen.dart +++ b/lib/src/view/puzzle/puzzle_screen.dart @@ -82,7 +82,6 @@ class _PuzzleScreenState extends ConsumerState with RouteAware { super.didPop(); if (mounted) { ref.invalidate(nextPuzzleProvider(widget.angle)); - ref.invalidate(puzzleRecentActivityProvider); } } diff --git a/lib/src/view/puzzle/puzzle_tab_screen.dart b/lib/src/view/puzzle/puzzle_tab_screen.dart index 4a8262283e..6a46d2583c 100644 --- a/lib/src/view/puzzle/puzzle_tab_screen.dart +++ b/lib/src/view/puzzle/puzzle_tab_screen.dart @@ -62,6 +62,7 @@ Widget _buildMainListItem( int index, Animation animation, PuzzleAngle Function(int index) getAngle, + VoidCallback? onGoingBackFromPuzzleScreen, ) { switch (index) { case 0: @@ -90,7 +91,7 @@ Widget _buildMainListItem( builder: (context) => const PuzzleScreen( angle: PuzzleTheme(PuzzleThemeKey.mix), ), - ); + ).then((_) => onGoingBackFromPuzzleScreen?.call()); }, ); default: @@ -102,7 +103,7 @@ Widget _buildMainListItem( context, rootNavigator: true, builder: (context) => PuzzleScreen(angle: angle), - ); + ).then((_) => onGoingBackFromPuzzleScreen?.call()); }, ); } @@ -172,23 +173,28 @@ class _CupertinoTabBodyState extends ConsumerState<_CupertinoTabBody> { } } - Widget _buildItem( - BuildContext context, - int index, - Animation animation, - ) { - return _buildMainListItem( - context, - index, - animation, - (index) => _angles[index], - ); - } - @override Widget build(BuildContext context) { final isTablet = isTabletOrLarger(context); + Widget buildItem( + BuildContext context, + int index, + Animation animation, + ) => + _buildMainListItem( + context, + index, + animation, + (index) => _angles[index], + isTablet + ? () { + ref.read(currentBottomTabProvider.notifier).state = + BottomTab.home; + } + : null, + ); + if (isTablet) { return Row( children: [ @@ -216,7 +222,7 @@ class _CupertinoTabBodyState extends ConsumerState<_CupertinoTabBody> { sliver: SliverAnimatedList( key: _listKey, initialItemCount: _angles.length, - itemBuilder: _buildItem, + itemBuilder: buildItem, ), ), ], @@ -273,7 +279,7 @@ class _CupertinoTabBodyState extends ConsumerState<_CupertinoTabBody> { sliver: SliverAnimatedList( key: _listKey, initialItemCount: _angles.length, - itemBuilder: _buildItem, + itemBuilder: buildItem, ), ), ], @@ -332,23 +338,28 @@ class _MaterialTabBodyState extends ConsumerState<_MaterialTabBody> { } } - Widget _buildItem( - BuildContext context, - int index, - Animation animation, - ) { - return _buildMainListItem( - context, - index, - animation, - (index) => _angles[index], - ); - } - @override Widget build(BuildContext context) { final isTablet = isTabletOrLarger(context); + Widget buildItem( + BuildContext context, + int index, + Animation animation, + ) => + _buildMainListItem( + context, + index, + animation, + (index) => _angles[index], + isTablet + ? () { + ref.read(currentBottomTabProvider.notifier).state = + BottomTab.home; + } + : null, + ); + return PopScope( canPop: false, onPopInvokedWithResult: (bool didPop, _) { @@ -375,7 +386,7 @@ class _MaterialTabBodyState extends ConsumerState<_MaterialTabBody> { key: _listKey, initialItemCount: _angles.length, controller: puzzlesScrollController, - itemBuilder: _buildItem, + itemBuilder: buildItem, ), ), Expanded( @@ -395,7 +406,7 @@ class _MaterialTabBodyState extends ConsumerState<_MaterialTabBody> { key: _listKey, controller: puzzlesScrollController, initialItemCount: _angles.length, - itemBuilder: _buildItem, + itemBuilder: buildItem, ), ), ], diff --git a/test/model/puzzle/mock_server_responses.dart b/test/model/puzzle/mock_server_responses.dart index 01653958f3..eca36e9cb7 100644 --- a/test/model/puzzle/mock_server_responses.dart +++ b/test/model/puzzle/mock_server_responses.dart @@ -5,3 +5,9 @@ const mockDailyPuzzleResponse = ''' const mockMixBatchResponse = ''' {"puzzles":[{"game":{"id":"PrlkCqOv","perf":{"key":"rapid","name":"Rapid"},"rated":true,"players":[{"userId":"silverjo","name":"silverjo (1777)","color":"white"},{"userId":"robyarchitetto","name":"Robyarchitetto (1742)","color":"black"}],"pgn":"e4 Nc6 Bc4 e6 a3 g6 Nf3 Bg7 c3 Nge7 d3 O-O Be3 Na5 Ba2 b6 Qd2 Bb7 Bh6 d5 e5 d4 Bxg7 Kxg7 Qf4 Bxf3 Qxf3 dxc3 Nxc3 Nac6 Qf6+ Kg8 Rd1 Nd4 O-O c5 Ne4 Nef5 Rd2 Qxf6 Nxf6+ Kg7 Re1 h5 h3 Rad8 b4 Nh4 Re3 Nhf5 Re1 a5 bxc5 bxc5 Bc4 Ra8 Rb1 Nh4 Rdb2 Nc6 Rb7 Nxe5 Bxe6 Kxf6 Bd5 Nf5 R7b6+ Kg7 Bxa8 Rxa8 R6b3 Nd4 Rb7 Nxd3 Rd1 Ne2+ Kh2 Ndf4 Rdd7 Rf8 Ra7 c4 Rxa5 c3 Rc5 Ne6 Rc4 Ra8 a4 Rb8 a5 Rb2 a6 c2","clock":"5+8"},"puzzle":{"id":"20yWT","rating":1859,"plays":551,"initialPly":93,"solution":["a6a7","b2a2","c4c2","a2a7","d7a7"],"themes":["endgame","long","advantage","advancedPawn"]}},{"game":{"id":"0lwkiJbZ","perf":{"key":"classical","name":"Classical"},"rated":true,"players":[{"userId":"nirdosh","name":"nirdosh (2035)","color":"white"},{"userId":"burn_it_down","name":"burn_it_down (2139)","color":"black"}],"pgn":"d4 Nf6 Nf3 c5 e3 g6 Bd3 Bg7 c3 Qc7 O-O O-O Nbd2 d6 Qe2 Nbd7 e4 cxd4 cxd4 e5 dxe5 dxe5 b3 Nc5 Bb2 Nh5 g3 Bh3 Rfc1 Qd6 Bc4 Rac8 Bd5 Qb8 Ng5 Bd7 Ba3 b6 Rc2 h6 Ngf3 Rfe8 Rac1 Ne6 Nc4 Bb5 Qe3 Bxc4 Bxc4 Nd4 Nxd4 exd4 Qd3 Rcd8 f4 Nf6 e5 Ng4 Qxg6 Ne3 Bxf7+ Kh8 Rc7 Qa8 Qxg7+ Kxg7 Bd5+ Kg6 Bxa8 Rxa8 Rd7 Rad8 Rc6+ Kf5 Rcd6 Rxd7 Rxd7 Ke4 Bb2 Nc2 Kf2 d3 Bc1 Nd4 h3","clock":"15+15"},"puzzle":{"id":"7H5EV","rating":1852,"plays":410,"initialPly":84,"solution":["e8c8","d7d4","e4d4"],"themes":["endgame","short","advantage"]}},{"game":{"id":"eWGRX5AI","perf":{"key":"rapid","name":"Rapid"},"rated":true,"players":[{"userId":"sacalot","name":"sacalot (2151)","color":"white"},{"userId":"landitirana","name":"landitirana (1809)","color":"black"}],"pgn":"e4 e5 Nf3 Nc6 d4 exd4 Bc4 Nf6 O-O Nxe4 Re1 d5 Bxd5 Qxd5 Nc3 Qd8 Rxe4+ Be6 Nxd4 Nxd4 Rxd4 Qf6 Ne4 Qe5 f4 Qf5 Ng3 Qa5 Bd2 Qb6 Be3 Bc5 f5 Bd5 Rxd5 Bxe3+ Kh1 O-O Rd3 Rfe8 Qf3 Qxb2 Rf1 Bd4 Nh5 Bf6 Rb3 Qd4 Rxb7 Re3 Nxf6+ gxf6 Qf2 Rae8 Rxc7 Qe5 Rc4 Re1 Rf4 Qa1 h3","clock":"10+0"},"puzzle":{"id":"1qUth","rating":1556,"plays":2661,"initialPly":60,"solution":["e1f1","f2f1","e8e1","f1e1","a1e1"],"themes":["endgame","master","advantage","fork","long","pin"]}}]} '''; + +const mockActivityResponse = ''' +{ "date": 1717460624888, "puzzle": { "fen": "6k1/3rqpp1/5b1p/p1p1pP1Q/1pB4P/1P1R1PP1/P7/6K1 w - - 1 1", "id": "BlOLL", "lastMove": "c7d7", "plays": 14703, "rating": 2018, "solution": [ "h5f7", "e7f7", "d3d7", "f7c4", "b3c4" ], "themes": [ "endgame", "crushing", "long", "sacrifice", "pin" ] }, "win": true } +{ "date": 1717460624788, "puzzle": { "fen": "6k1/3rqpp1/5b1p/p1p1pP1Q/1pB4P/1P1R1PP1/P7/6K1 w - - 1 1", "id": "BlOLK", "lastMove": "c7d7", "plays": 14703, "rating": 2018, "solution": [ "h5f7", "e7f7", "d3d7", "f7c4", "b3c4" ], "themes": [ "endgame", "crushing", "long", "sacrifice", "pin" ] }, "win": true } +{ "date": 1717460624688, "puzzle": { "fen": "6k1/3rqpp1/5b1p/p1p1pP1Q/1pB4P/1P1R1PP1/P7/6K1 w - - 1 1", "id": "BlOLG", "lastMove": "c7d7", "plays": 14703, "rating": 2018, "solution": [ "h5f7", "e7f7", "d3d7", "f7c4", "b3c4" ], "themes": [ "endgame", "crushing", "long", "sacrifice", "pin" ] }, "win": true } +'''; diff --git a/test/model/puzzle/puzzle_repository_test.dart b/test/model/puzzle/puzzle_repository_test.dart index 2002320c3a..d053bda7ac 100644 --- a/test/model/puzzle/puzzle_repository_test.dart +++ b/test/model/puzzle/puzzle_repository_test.dart @@ -1,3 +1,4 @@ +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle.dart'; @@ -116,5 +117,22 @@ void main() { expect(result, isA()); }); + + test('puzzle activity', () async { + final mockClient = MockClient((request) { + if (request.url.path == '/api/puzzle/activity') { + return mockResponse(mockActivityResponse, 200); + } + return mockResponse('', 404); + }); + + final container = await lichessClientContainer(mockClient); + final client = container.read(lichessClientProvider); + final repo = PuzzleRepository(client); + final result = await repo.puzzleActivity(3); + + expect(result, isA>()); + expect(result.length, 3); + }); }); } diff --git a/test/view/puzzle/puzzle_history_screen_test.dart b/test/view/puzzle/puzzle_history_screen_test.dart new file mode 100644 index 0000000000..caa034b73d --- /dev/null +++ b/test/view/puzzle/puzzle_history_screen_test.dart @@ -0,0 +1,204 @@ +import 'dart:math' as math; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/testing.dart'; +import 'package:intl/intl.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/view/puzzle/puzzle_history_screen.dart'; +import 'package:lichess_mobile/src/view/puzzle/puzzle_screen.dart'; +import 'package:lichess_mobile/src/widgets/board_thumbnail.dart'; + +import '../../model/auth/fake_session_storage.dart'; +import '../../model/puzzle/mock_server_responses.dart'; +import '../../test_helpers.dart'; +import '../../test_provider_scope.dart'; + +void main() { + final Map mockActivityRequestsCount = {}; + + tearDown(() { + mockActivityRequestsCount.clear(); + }); + + MockClient makeClient(int totalNumberOfPuzzles) => MockClient((request) { + if (request.url.path == '/api/puzzle/activity') { + final query = request.url.queryParameters; + final max = int.parse(query['max']!); + final beforeDateParam = query['before']; + final beforeDate = beforeDateParam != null + ? DateTime.fromMillisecondsSinceEpoch(int.parse(beforeDateParam)) + : null; + final totalAlreadyRequested = + mockActivityRequestsCount.values.fold(0, (p, e) => p + e); + + if (totalAlreadyRequested >= totalNumberOfPuzzles) { + return mockResponse('', 200); + } + + final key = + beforeDate != null ? DateFormat.yMd().format(beforeDate) : null; + + final nbPuzzles = math.min(max, totalNumberOfPuzzles); + mockActivityRequestsCount[key] = + (mockActivityRequestsCount[key] ?? 0) + nbPuzzles; + return mockResponse( + generateHistory(nbPuzzles, beforeDate), + 200, + ); + } else if (request.url.path == '/api/puzzle/batch/mix') { + return mockResponse(mockMixBatchResponse, 200); + } else if (request.url.path.startsWith('/api/puzzle')) { + return mockResponse( + ''' +{"game":{"id":"MNMYnEjm","perf":{"key":"classical","name":"Classical"},"rated":true,"players":[{"name":"Igor76","id":"igor76","color":"white","rating":2211},{"name":"dmitriy_duyun","id":"dmitriy_duyun","color":"black","rating":2180}],"pgn":"e4 c6 d4 d5 Nc3 g6 Nf3 Bg7 h3 dxe4 Nxe4 Nf6 Bd3 Nxe4 Bxe4 Nd7 O-O Nf6 Bd3 O-O Re1 Bf5 Bxf5 gxf5 c3 e6 Bg5 Qb6 Qc2 Rac8 Ne5 Qc7 Rad1 Nd7 Bf4 Nxe5 Bxe5 Bxe5 Rxe5 Rcd8 Qd2 Kh8 Rde1 Rg8 Qf4","clock":"20+15"},"puzzle":{"id":"0XqV2","rating":1929,"plays":93270,"solution":["f7f6","e5f5","c7g7","g2g3","e6f5"],"themes":["clearance","endgame","advantage","intermezzo","long"],"initialPly":44}} +''', + 200, + ); + } + return mockResponse('', 404); + }); + + testWidgets('Displays an initial list of puzzles', + (WidgetTester tester) async { + final app = await makeTestProviderScopeApp( + tester, + home: const PuzzleHistoryScreen(), + userSession: fakeSession, + overrides: [ + lichessClientProvider.overrideWith((ref) { + return LichessClient(makeClient(4), ref); + }), + ], + ); + + await tester.pumpWidget(app); + + expect(find.byType(PuzzleHistoryScreen), findsOneWidget); + + // wait for puzzles to load + await tester.pump(const Duration(milliseconds: 20)); + + expect(mockActivityRequestsCount, equals({null: 4})); + + expect(find.byType(BoardThumbnail), findsNWidgets(4)); + + expect(find.text(DateFormat.yMMMd().format(firstPageDate)), findsOneWidget); + }); + + testWidgets('Scrolling down loads next page', (WidgetTester tester) async { + final app = await makeTestProviderScopeApp( + tester, + home: const PuzzleHistoryScreen(), + userSession: fakeSession, + overrides: [ + lichessClientProvider.overrideWith((ref) { + return LichessClient(makeClient(80), ref); + }), + ], + ); + + await tester.pumpWidget(app); + + expect(find.byType(PuzzleHistoryScreen), findsOneWidget); + + // wait for puzzles to load + await tester.pump(const Duration(milliseconds: 20)); + + // first page has 20 puzzles + expect(mockActivityRequestsCount, equals({null: 20})); + + // not everything will be displayed but we should see at least first 2 rows + expect(find.byType(BoardThumbnail), findsAtLeastNWidgets(4)); + + await tester.scrollUntilVisible( + find.byWidgetPredicate( + (widget) => + widget is PuzzleHistoryBoard && widget.puzzle.id.value == 'Bnull20', + description: 'last item of 1st page', + ), + 400, + ); + + // next pages have 50 puzzles + expect( + mockActivityRequestsCount, + equals({ + null: 20, + '1/31/2024': 50, + }), + ); + + // by the time we've scrolled to the end the next puzzles are already here + await tester.scrollUntilVisible( + find.byWidgetPredicate( + (widget) => + widget is PuzzleHistoryBoard && widget.puzzle.id.value == 'B3150', + description: 'last item of 2nd page', + ), + 1000, + ); + + // one more page + expect( + mockActivityRequestsCount, + equals({ + null: 20, + '1/31/2024': 50, + '1/30/2024': 50, + }), + ); + + await tester.scrollUntilVisible( + find.byWidgetPredicate( + (widget) => + widget is PuzzleHistoryBoard && widget.puzzle.id.value == 'B3010', + description: 'last item of 3rd page', + ), + 400, + ); + + // no more items + expect(mockActivityRequestsCount.length, 3); + + // wait for the scroll to finish + await tester.pumpAndSettle(); + + await tester.tap( + find.byWidgetPredicate( + (widget) => + widget is PuzzleHistoryBoard && widget.puzzle.id.value == 'B3010', + ), + ); + + await tester.pumpAndSettle(); + expect(find.byType(PuzzleScreen), findsOneWidget); + + // go back, should be on the same page and history still loaded + await tester.pageBack(); + await tester.pumpAndSettle(); + + expect(find.byType(PuzzleHistoryScreen), findsOneWidget); + expect( + find.byWidgetPredicate( + (widget) => + widget is PuzzleHistoryBoard && widget.puzzle.id.value == 'B3010', + ), + findsOneWidget, + ); + }); +} + +// a date to use for the first page; all the puzzle with have the same date per +// page for simplification +final firstPageDate = DateTime.parse('2024-01-31 10:00:00'); + +String generateHistory(int count, DateTime? maybeDate) { + final buffer = StringBuffer(); + for (int i = 0; i < count; i++) { + final date = maybeDate?.subtract(const Duration(days: 1)) ?? firstPageDate; + final id = 'B${maybeDate?.day}${i + 1}'; + buffer.writeln(''' +{ "date": ${date.millisecondsSinceEpoch}, "puzzle": { "fen": "6k1/3rqpp1/5b1p/p1p1pP1Q/1pB4P/1P1R1PP1/P7/6K1 w - - 1 1", "id": "$id", "lastMove": "c7d7", "plays": 14703, "rating": 2018, "solution": [ "h5f7", "e7f7", "d3d7", "f7c4", "b3c4" ], "themes": [ "endgame", "crushing", "long", "sacrifice", "pin" ] }, "win": true } +'''); + } + return buffer.toString(); +} From 5b7b0b6edf8b970d1033056a7094509fb92c4fff Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 29 Oct 2024 20:54:28 +0100 Subject: [PATCH 550/979] Fix toggle sound button style --- lib/src/view/settings/toggle_sound_button.dart | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/lib/src/view/settings/toggle_sound_button.dart b/lib/src/view/settings/toggle_sound_button.dart index a269e82b47..b3abc10487 100644 --- a/lib/src/view/settings/toggle_sound_button.dart +++ b/lib/src/view/settings/toggle_sound_button.dart @@ -5,9 +5,7 @@ import 'package:lichess_mobile/src/widgets/buttons.dart'; /// A button that toggles the sound on and off. class ToggleSoundButton extends ConsumerWidget { - const ToggleSoundButton({this.iconSize, super.key}); - - final double? iconSize; + const ToggleSoundButton({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { @@ -17,13 +15,12 @@ class ToggleSoundButton extends ConsumerWidget { ), ); - return PlatformIconButton( - iconSize: iconSize, - // TODO: translate + return AppBarIconButton( + // TODO: i18n semanticsLabel: 'Toggle sound', - onTap: () => + onPressed: () => ref.read(generalPreferencesProvider.notifier).toggleSoundEnabled(), - icon: isSoundEnabled ? Icons.volume_up : Icons.volume_off, + icon: Icon(isSoundEnabled ? Icons.volume_up : Icons.volume_off), ); } } From e7c7f11b3d13f60b1d78d567f3282ddc797ace9c Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 30 Oct 2024 13:13:54 +0100 Subject: [PATCH 551/979] Don't use transparent app bars on iOS tablets I reproduce bad scroll performance on a iPad. --- lib/src/app.dart | 6 +++++- lib/src/styles/styles.dart | 5 +++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/src/app.dart b/lib/src/app.dart index a0288d0b94..0cb59123d7 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -142,6 +142,8 @@ class _AppState extends ConsumerState { ? _generateDynamicColourSchemes(lightColorScheme, darkColorScheme) : (null, null); + final isTablet = isTabletOrLarger(context); + final dynamicColorScheme = brightness == Brightness.light ? fixedLightScheme : fixedDarkScheme; @@ -173,7 +175,9 @@ class _AppState extends ConsumerState { .copyWith(color: Styles.cupertinoTitleColor), ), scaffoldBackgroundColor: Styles.cupertinoScaffoldColor, - barBackgroundColor: Styles.cupertinoAppBarColor, + barBackgroundColor: isTablet + ? Styles.cupertinoTabletAppBarColor + : Styles.cupertinoAppBarColor, ); return MaterialApp( diff --git a/lib/src/styles/styles.dart b/lib/src/styles/styles.dart index a9c45f1bc8..697c86a33d 100644 --- a/lib/src/styles/styles.dart +++ b/lib/src/styles/styles.dart @@ -77,6 +77,11 @@ abstract class Styles { color: Color(0xE6F9F9F9), darkColor: Color.fromARGB(210, 36, 36, 38), ); + static const cupertinoTabletAppBarColor = + CupertinoDynamicColor.withBrightness( + color: Color(0xFFF9F9F9), + darkColor: Color.fromARGB(255, 36, 36, 36), + ); static const cupertinoScaffoldColor = CupertinoDynamicColor.withBrightness( color: Color.fromARGB(255, 242, 242, 247), darkColor: Color.fromARGB(255, 23, 23, 23), From 6dc60fba413ea9b1c786216378a903a417e896dc Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 31 Oct 2024 14:31:40 +0100 Subject: [PATCH 552/979] Upgrade dependencies --- ios/Podfile.lock | 15 ++++++-- pubspec.lock | 96 ++++++++++++++++++++++++++++-------------------- pubspec.yaml | 6 +-- 3 files changed, 70 insertions(+), 47 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 313ff3a5b1..562392d4a9 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -12,6 +12,7 @@ PODS: - FlutterMacOS - cupertino_http (0.0.1): - Flutter + - FlutterMacOS - device_info_plus (0.0.1): - Flutter - Firebase/CoreOnly (11.2.0): @@ -116,6 +117,8 @@ PODS: - nanopb/encode (= 3.30910.0) - nanopb/decode (3.30910.0) - nanopb/encode (3.30910.0) + - objective_c (0.0.1): + - Flutter - package_info_plus (0.4.5): - Flutter - path_provider_foundation (0.0.1): @@ -144,7 +147,7 @@ PODS: DEPENDENCIES: - app_settings (from `.symlinks/plugins/app_settings/ios`) - connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`) - - cupertino_http (from `.symlinks/plugins/cupertino_http/ios`) + - cupertino_http (from `.symlinks/plugins/cupertino_http/darwin`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - firebase_core (from `.symlinks/plugins/firebase_core/ios`) - firebase_crashlytics (from `.symlinks/plugins/firebase_crashlytics/ios`) @@ -154,6 +157,7 @@ DEPENDENCIES: - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) + - objective_c (from `.symlinks/plugins/objective_c/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - share_plus (from `.symlinks/plugins/share_plus/ios`) @@ -188,7 +192,7 @@ EXTERNAL SOURCES: connectivity_plus: :path: ".symlinks/plugins/connectivity_plus/darwin" cupertino_http: - :path: ".symlinks/plugins/cupertino_http/ios" + :path: ".symlinks/plugins/cupertino_http/darwin" device_info_plus: :path: ".symlinks/plugins/device_info_plus/ios" firebase_core: @@ -207,6 +211,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/flutter_native_splash/ios" flutter_secure_storage: :path: ".symlinks/plugins/flutter_secure_storage/ios" + objective_c: + :path: ".symlinks/plugins/objective_c/ios" package_info_plus: :path: ".symlinks/plugins/package_info_plus/ios" path_provider_foundation: @@ -230,7 +236,7 @@ SPEC CHECKSUMS: app_settings: 017320c6a680cdc94c799949d95b84cb69389ebc AppAuth: 501c04eda8a8d11f179dbe8637b7a91bb7e5d2fa connectivity_plus: 4c41c08fc6d7c91f63bc7aec70ffe3730b04f563 - cupertino_http: 1a3a0f163c1b26e7f1a293b33d476e0fde7a64ec + cupertino_http: 947a233f40cfea55167a49f2facc18434ea117ba device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342 Firebase: 98e6bf5278170668a7983e12971a66b2cd57fc8c firebase_core: 2bedc3136ec7c7b8561c6123ed0239387b53f2af @@ -245,13 +251,14 @@ SPEC CHECKSUMS: FirebaseRemoteConfigInterop: c3a5c31b3c22079f41ba1dc645df889d9ce38cb9 FirebaseSessions: 655ff17f3cc1a635cbdc2d69b953878001f9e25b Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - flutter_appauth: aef998cfbcc307dff7f2fbe1f59a50323748dc25 + flutter_appauth: 408f4cda69a4ad59bdf696e04cd9e13e1449b44e flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086 flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778 flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12 GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 + objective_c: 77e887b5ba1827970907e10e832eec1683f3431d package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 diff --git a/pubspec.lock b/pubspec.lock index 6858978b69..d8cefb1c94 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -194,10 +194,10 @@ packages: dependency: "direct main" description: name: chessground - sha256: "93d7c7c8e6c049dd9ee99aa158520c6ae6e1d81aedae1dc0dbca93a5113fba9e" + sha256: "363e3c408ef360807ee2d04ee6a2caab2d63fa0f66542cda2005927300da379e" url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "5.3.0" ci: dependency: transitive description: @@ -210,10 +210,10 @@ packages: dependency: transitive description: name: cli_util - sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19 + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c url: "https://pub.dev" source: hosted - version: "0.4.1" + version: "0.4.2" clock: dependency: transitive description: @@ -226,10 +226,10 @@ packages: dependency: transitive description: name: code_builder - sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 + sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" url: "https://pub.dev" source: hosted - version: "4.10.0" + version: "4.10.1" collection: dependency: "direct main" description: @@ -290,18 +290,18 @@ packages: dependency: transitive description: name: csslib - sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb" + sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.2" cupertino_http: dependency: "direct main" description: name: cupertino_http - sha256: "7e75c45a27cc13a886ab0a1e4d8570078397057bd612de9d24fe5df0d9387717" + sha256: c13f43571ba579f3d96d959e72f6c716b2a5ff379b927dda0d7639d0bcd2c49c url: "https://pub.dev" source: hosted - version: "1.5.1" + version: "2.0.0" cupertino_icons: dependency: "direct main" description: @@ -314,26 +314,34 @@ packages: dependency: "direct dev" description: name: custom_lint - sha256: "4500e88854e7581ee43586abeaf4443cb22375d6d289241a87b1aadf678d5545" + sha256: "3486c470bb93313a9417f926c7dd694a2e349220992d7b9d14534dc49c15bba9" url: "https://pub.dev" source: hosted - version: "0.6.10" + version: "0.7.0" custom_lint_builder: dependency: transitive description: name: custom_lint_builder - sha256: "5a95eff100da256fbf086b329c17c8b49058c261cdf56d3a4157d3c31c511d78" + sha256: "42cdc41994eeeddab0d7a722c7093ec52bd0761921eeb2cbdbf33d192a234759" url: "https://pub.dev" source: hosted - version: "0.6.10" + version: "0.7.0" custom_lint_core: dependency: transitive description: name: custom_lint_core - sha256: "76a4046cc71d976222a078a8fd4a65e198b70545a8d690a75196dd14f08510f6" + sha256: "02450c3e45e2a6e8b26c4d16687596ab3c4644dd5792e3313aa9ceba5a49b7f5" url: "https://pub.dev" source: hosted - version: "0.6.10" + version: "0.7.0" + custom_lint_visitor: + dependency: transitive + description: + name: custom_lint_visitor + sha256: bfe9b7a09c4775a587b58d10ebb871d4fe618237639b1e84d5ec62d7dfef25f9 + url: "https://pub.dev" + source: hosted + version: "1.0.0+6.11.0" dart_style: dependency: transitive description: @@ -519,18 +527,18 @@ packages: dependency: "direct main" description: name: flutter_appauth - sha256: "84e8753fe20864da241892823ff7dbd252baa34f1649d6feb48118e8ae829ed1" + sha256: "6ab0e7fb2cb66db472a71c00e0f0d0888f186d308beaef4bba1a6113fa861096" url: "https://pub.dev" source: hosted - version: "7.0.1" + version: "8.0.0+1" flutter_appauth_platform_interface: dependency: transitive description: name: flutter_appauth_platform_interface - sha256: "0959824b401f3ee209c869734252bd5d4d4aab804b019c03815c56e3b9a4bc34" + sha256: ccf5e1d8c40dd35b297290b33cc1896648b4b92a2ec3f62a436c62a8eef9a9db url: "https://pub.dev" source: hosted - version: "7.0.1" + version: "8.0.0" flutter_cache_manager: dependency: transitive description: @@ -567,10 +575,10 @@ packages: dependency: "direct main" description: name: flutter_local_notifications - sha256: "49eeef364fddb71515bc78d5a8c51435a68bccd6e4d68e25a942c5e47761ae71" + sha256: "674173fd3c9eda9d4c8528da2ce0ea69f161577495a9cc835a2a4ecd7eadeb35" url: "https://pub.dev" source: hosted - version: "17.2.3" + version: "17.2.4" flutter_local_notifications_linux: dependency: transitive description: @@ -743,10 +751,10 @@ packages: dependency: transitive description: name: html - sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a" + sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec" url: "https://pub.dev" source: hosted - version: "0.15.4" + version: "0.15.5" http: dependency: "direct main" description: @@ -955,6 +963,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.0" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: c8467f3b067a3867c4876210b2fa65a57d117b5667ac158df5991804b0912673 + url: "https://pub.dev" + source: hosted + version: "3.0.0" octo_image: dependency: transitive description: @@ -999,18 +1015,18 @@ packages: dependency: transitive description: name: path_parsing - sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf + sha256: caa17e8f0b386eb190dd5b6a3b71211c76375aa8b6ffb4465b5863d019bdb334 url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.3" path_provider: dependency: transitive description: name: path_provider - sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.5" path_provider_android: dependency: transitive description: @@ -1135,10 +1151,10 @@ packages: dependency: transitive description: name: riverpod_analyzer_utils - sha256: "0dcb0af32d561f8fa000c6a6d95633c9fb08ea8a8df46e3f9daca59f11218167" + sha256: dc53a659cb543b203cdc35cd4e942ed08ea893eb6ef12029301323bdf18c5d95 url: "https://pub.dev" source: hosted - version: "0.5.6" + version: "0.5.7" riverpod_annotation: dependency: "direct main" description: @@ -1151,18 +1167,18 @@ packages: dependency: "direct dev" description: name: riverpod_generator - sha256: "851aedac7ad52693d12af3bf6d92b1626d516ed6b764eb61bf19e968b5e0b931" + sha256: "54458dac2fea976990dc9ed379060db6ae5c8790143f1963fedd0fb99980a326" url: "https://pub.dev" source: hosted - version: "2.6.1" + version: "2.6.2" riverpod_lint: dependency: "direct dev" description: name: riverpod_lint - sha256: "0684c21a9a4582c28c897d55c7b611fa59a351579061b43f8c92c005804e63a8" + sha256: "326efc199b87f21053b9a2afbf2aea26c41b3bf6f8ba346ce69126ee17d16ebd" url: "https://pub.dev" source: hosted - version: "2.6.1" + version: "2.6.2" rxdart: dependency: transitive description: @@ -1175,10 +1191,10 @@ packages: dependency: "direct main" description: name: share_plus - sha256: "334fcdf0ef9c0df0e3b428faebcac9568f35c747d59831474b2fc56e156d244e" + sha256: "3af2cda1752e5c24f2fc04b6083b40f013ffe84fb90472f30c6499a9213d5442" url: "https://pub.dev" source: hosted - version: "10.1.0" + version: "10.1.1" share_plus_platform_interface: dependency: transitive description: @@ -1493,10 +1509,10 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "8fc3bae0b68c02c47c5c86fa8bfa74471d42687b0eded01b78de87872db745e2" + sha256: "0dea215895a4d254401730ca0ba8204b29109a34a99fb06ae559a2b60988d2de" url: "https://pub.dev" source: hosted - version: "6.3.12" + version: "6.3.13" url_launcher_ios: dependency: transitive description: @@ -1653,10 +1669,10 @@ packages: dependency: transitive description: name: win32 - sha256: e1d0cc62e65dc2561f5071fcbccecf58ff20c344f8f3dc7d4922df372a11df1f + sha256: "10169d3934549017f0ae278ccb07f828f9d6ea21573bab0fb77b0e1ef0fce454" url: "https://pub.dev" source: hosted - version: "5.7.1" + version: "5.7.2" win32_registry: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 364c89a868..385aff56d0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -18,7 +18,7 @@ dependencies: connectivity_plus: ^6.0.2 cronet_http: ^1.3.1 crypto: ^3.0.3 - cupertino_http: ^1.1.0 + cupertino_http: ^2.0.0 cupertino_icons: ^1.0.2 dartchess: ^0.9.0 deep_pick: ^1.0.0 @@ -31,7 +31,7 @@ dependencies: fl_chart: ^0.69.0 flutter: sdk: flutter - flutter_appauth: ^7.0.0 + flutter_appauth: ^8.0.0+1 flutter_displaymode: ^0.6.0 flutter_layout_grid: ^2.0.1 flutter_linkify: ^6.0.0 @@ -80,7 +80,7 @@ dependencies: dev_dependencies: build_runner: ^2.3.2 - custom_lint: ^0.6.0 + custom_lint: ^0.7.0 fake_async: ^1.3.1 flutter_test: sdk: flutter From 8a0c93d1e72f320b364ba0fe160b88b827e9a7ba Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 31 Oct 2024 14:51:47 +0100 Subject: [PATCH 553/979] Remove useless params --- lib/src/view/puzzle/puzzle_tab_screen.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/src/view/puzzle/puzzle_tab_screen.dart b/lib/src/view/puzzle/puzzle_tab_screen.dart index 6a46d2583c..a8d4e61ba2 100644 --- a/lib/src/view/puzzle/puzzle_tab_screen.dart +++ b/lib/src/view/puzzle/puzzle_tab_screen.dart @@ -381,8 +381,6 @@ class _MaterialTabBodyState extends ConsumerState<_MaterialTabBody> { children: [ Expanded( child: AnimatedList( - shrinkWrap: true, - physics: const ClampingScrollPhysics(), key: _listKey, initialItemCount: _angles.length, controller: puzzlesScrollController, From 820ec0e464a388ec46c178a64cf4024a1821ce6a Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 31 Oct 2024 15:05:54 +0100 Subject: [PATCH 554/979] Improve BoardThumbnail with a board optimized for scrollables --- lib/src/widgets/board_thumbnail.dart | 47 +++++++++++++++++++--------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/lib/src/widgets/board_thumbnail.dart b/lib/src/widgets/board_thumbnail.dart index 91f1629810..2095e4d797 100644 --- a/lib/src/widgets/board_thumbnail.dart +++ b/lib/src/widgets/board_thumbnail.dart @@ -15,6 +15,7 @@ class BoardThumbnail extends ConsumerStatefulWidget { this.footer, this.lastMove, this.onTap, + this.animationDuration, }); const BoardThumbnail.loading({ @@ -24,7 +25,8 @@ class BoardThumbnail extends ConsumerStatefulWidget { }) : orientation = Side.white, fen = kInitialFEN, lastMove = null, - onTap = null; + onTap = null, + animationDuration = null; /// Size of the board. final double size; @@ -46,6 +48,9 @@ class BoardThumbnail extends ConsumerStatefulWidget { final GestureTapCallback? onTap; + /// Optionally animate changes to the board by the specified duration. + final Duration? animationDuration; + @override _BoardThumbnailState createState() => _BoardThumbnailState(); } @@ -67,20 +72,32 @@ class _BoardThumbnailState extends ConsumerState { Widget build(BuildContext context) { final boardPrefs = ref.watch(boardPreferencesProvider); - final board = Chessboard.fixed( - size: widget.size, - fen: widget.fen, - orientation: widget.orientation, - lastMove: widget.lastMove as NormalMove?, - settings: ChessboardSettings( - enableCoordinates: false, - borderRadius: const BorderRadius.all(Radius.circular(4.0)), - boxShadow: boardShadows, - animationDuration: const Duration(milliseconds: 150), - pieceAssets: boardPrefs.pieceSet.assets, - colorScheme: boardPrefs.boardTheme.colors, - ), - ); + final board = widget.animationDuration != null + ? Chessboard.fixed( + size: widget.size, + fen: widget.fen, + orientation: widget.orientation, + lastMove: widget.lastMove as NormalMove?, + settings: ChessboardSettings( + enableCoordinates: false, + borderRadius: const BorderRadius.all(Radius.circular(4.0)), + boxShadow: boardShadows, + animationDuration: widget.animationDuration!, + pieceAssets: boardPrefs.pieceSet.assets, + colorScheme: boardPrefs.boardTheme.colors, + ), + ) + : StaticChessboard( + size: widget.size, + fen: widget.fen, + orientation: widget.orientation, + lastMove: widget.lastMove as NormalMove?, + enableCoordinates: false, + borderRadius: const BorderRadius.all(Radius.circular(4.0)), + boxShadow: boardShadows, + pieceAssets: boardPrefs.pieceSet.assets, + colorScheme: boardPrefs.boardTheme.colors, + ); final maybeTappableBoard = widget.onTap != null ? GestureDetector( From f190529640b39b54353f56a94ec1ecbc14ff70aa Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 31 Oct 2024 17:14:16 +0100 Subject: [PATCH 555/979] Make the whole card semi-transp when is my turn --- lib/src/view/home/home_tab_screen.dart | 51 +++++++++++++------------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/lib/src/view/home/home_tab_screen.dart b/lib/src/view/home/home_tab_screen.dart index d540ce6c5f..6abf2eefaf 100644 --- a/lib/src/view/home/home_tab_screen.dart +++ b/lib/src/view/home/home_tab_screen.dart @@ -7,6 +7,7 @@ import 'package:lichess_mobile/src/model/account/ongoing_game.dart'; import 'package:lichess_mobile/src/model/auth/auth_controller.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/challenge/challenges.dart'; +import 'package:lichess_mobile/src/model/common/speed.dart'; import 'package:lichess_mobile/src/model/correspondence/correspondence_game_storage.dart'; import 'package:lichess_mobile/src/model/game/game_history.dart'; import 'package:lichess_mobile/src/model/settings/home_preferences.dart'; @@ -731,21 +732,21 @@ class _GamePreviewCarouselItem extends StatelessWidget { @override Widget build(BuildContext context) { - return BoardCarouselItem( - fen: game.fen, - orientation: game.orientation, - lastMove: game.lastMove, - description: Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 10.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Opacity( - opacity: game.isMyTurn ? 1.0 : 0.6, - child: Row( + return Opacity( + opacity: game.speed != Speed.correspondence || game.isMyTurn ? 1.0 : 0.6, + child: BoardCarouselItem( + fen: game.fen, + orientation: game.orientation, + lastMove: game.lastMove, + description: Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Row( children: [ if (game.isMyTurn) ...const [ Icon( @@ -781,19 +782,19 @@ class _GamePreviewCarouselItem extends StatelessWidget { ), ], ), - ), - const SizedBox(height: 4.0), - UserFullNameWidget.player( - user: game.opponent, - rating: game.opponentRating, - aiLevel: game.opponentAiLevel, - style: Styles.boardPreviewTitle, - ), - ], + const SizedBox(height: 4.0), + UserFullNameWidget.player( + user: game.opponent, + rating: game.opponentRating, + aiLevel: game.opponentAiLevel, + style: Styles.boardPreviewTitle, + ), + ], + ), ), ), + onTap: onTap, ), - onTap: onTap, ); } } From 9497c7a167835d6af8ad163c1c734c182248754e Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 31 Oct 2024 17:33:59 +0100 Subject: [PATCH 556/979] Allow 0 eval lines --- lib/src/model/analysis/analysis_preferences.dart | 4 ++-- lib/src/view/analysis/analysis_settings.dart | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/model/analysis/analysis_preferences.dart b/lib/src/model/analysis/analysis_preferences.dart index 4b8dddd84d..5633e3db30 100644 --- a/lib/src/model/analysis/analysis_preferences.dart +++ b/lib/src/model/analysis/analysis_preferences.dart @@ -67,7 +67,7 @@ class AnalysisPreferences extends _$AnalysisPreferences } Future setNumEvalLines(int numEvalLines) { - assert(numEvalLines >= 1 && numEvalLines <= 3); + assert(numEvalLines >= 0 && numEvalLines <= 3); return save( state.copyWith( numEvalLines: numEvalLines, @@ -95,7 +95,7 @@ class AnalysisPrefs with _$AnalysisPrefs implements Serializable { required bool showBestMoveArrow, required bool showAnnotations, required bool showPgnComments, - @Assert('numEvalLines >= 1 && numEvalLines <= 3') required int numEvalLines, + @Assert('numEvalLines >= 0 && numEvalLines <= 3') required int numEvalLines, @Assert('numEngineCores >= 1 && numEngineCores <= maxEngineCores') required int numEngineCores, }) = _AnalysisPrefs; diff --git a/lib/src/view/analysis/analysis_settings.dart b/lib/src/view/analysis/analysis_settings.dart index dac59b62f7..f80fb44473 100644 --- a/lib/src/view/analysis/analysis_settings.dart +++ b/lib/src/view/analysis/analysis_settings.dart @@ -61,7 +61,7 @@ class AnalysisSettings extends ConsumerWidget { ), subtitle: NonLinearSlider( value: prefs.numEvalLines, - values: const [1, 2, 3], + values: const [0, 1, 2, 3], onChangeEnd: isEngineAvailable ? (value) => ref .read(ctrlProvider.notifier) From 854798bdbdc44c1f9fd95e9579927a9ceec5aed5 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Thu, 31 Oct 2024 23:23:16 +0100 Subject: [PATCH 557/979] follow riverpod depreciation notice --- lib/src/model/broadcast/broadcast_providers.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/src/model/broadcast/broadcast_providers.dart b/lib/src/model/broadcast/broadcast_providers.dart index 02e75daf1d..4cadb62d0f 100644 --- a/lib/src/model/broadcast/broadcast_providers.dart +++ b/lib/src/model/broadcast/broadcast_providers.dart @@ -1,3 +1,4 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_repository.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; @@ -44,7 +45,7 @@ class BroadcastsPaginator extends _$BroadcastsPaginator { @riverpod Future broadcastTournament( - BroadcastTournamentRef ref, + Ref ref, BroadcastTournamentId broadcastTournamentId, ) { return ref.withClient( @@ -55,7 +56,7 @@ Future broadcastTournament( @riverpod Future broadcastRound( - BroadcastRoundRef ref, + Ref ref, BroadcastRoundId broadcastRoundId, ) { return ref.withClient( @@ -65,7 +66,7 @@ Future broadcastRound( @riverpod Future broadcastGame( - BroadcastGameRef ref, { + Ref ref, { required BroadcastRoundId roundId, required BroadcastGameId gameId, }) { From 8ea8c2ce16cbee4cfb65c6f49750bc40b5c6d905 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Fri, 1 Nov 2024 11:49:07 +0100 Subject: [PATCH 558/979] increase move list size in landscape --- lib/src/widgets/board_table.dart | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/lib/src/widgets/board_table.dart b/lib/src/widgets/board_table.dart index 1de8bc6f19..d734291b98 100644 --- a/lib/src/widgets/board_table.dart +++ b/lib/src/widgets/board_table.dart @@ -264,7 +264,7 @@ class _BoardTableState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - Flexible(child: widget.topTable), + widget.topTable, if (!widget.zenMode && slicedMoves != null) Expanded( child: Padding( @@ -279,14 +279,8 @@ class _BoardTableState extends ConsumerState { ), ) else - // same height as [MoveList] - const Expanded( - child: Padding( - padding: EdgeInsets.all(16.0), - child: SizedBox(height: 40), - ), - ), - Flexible(child: widget.bottomTable), + const Spacer(), + widget.bottomTable, ], ), ), From c44c43c92a4370758be3e43c7d4f9b28dbfb7bcd Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 1 Nov 2024 18:11:32 +0100 Subject: [PATCH 559/979] Update translations --- lib/l10n/app_en.arb | 385 +- lib/l10n/l10n.dart | 1376 +++++- lib/l10n/l10n_af.dart | 773 +++- lib/l10n/l10n_ar.dart | 801 +++- lib/l10n/l10n_az.dart | 757 +++- lib/l10n/l10n_be.dart | 807 +++- lib/l10n/l10n_bg.dart | 767 +++- lib/l10n/l10n_bn.dart | 763 +++- lib/l10n/l10n_br.dart | 769 +++- lib/l10n/l10n_bs.dart | 762 +++- lib/l10n/l10n_ca.dart | 757 +++- lib/l10n/l10n_cs.dart | 869 +++- lib/l10n/l10n_da.dart | 759 +++- lib/l10n/l10n_de.dart | 767 +++- lib/l10n/l10n_el.dart | 867 +++- lib/l10n/l10n_en.dart | 5582 ++++++++++++++--------- lib/l10n/l10n_eo.dart | 757 +++- lib/l10n/l10n_es.dart | 761 +++- lib/l10n/l10n_et.dart | 761 +++- lib/l10n/l10n_eu.dart | 875 +++- lib/l10n/l10n_fa.dart | 1013 ++++- lib/l10n/l10n_fi.dart | 761 +++- lib/l10n/l10n_fo.dart | 757 +++- lib/l10n/l10n_fr.dart | 759 +++- lib/l10n/l10n_ga.dart | 769 +++- lib/l10n/l10n_gl.dart | 789 +++- lib/l10n/l10n_gsw.dart | 937 +++- lib/l10n/l10n_he.dart | 801 +++- lib/l10n/l10n_hi.dart | 759 +++- lib/l10n/l10n_hr.dart | 762 +++- lib/l10n/l10n_hu.dart | 773 +++- lib/l10n/l10n_hy.dart | 757 +++- lib/l10n/l10n_id.dart | 787 +++- lib/l10n/l10n_it.dart | 775 +++- lib/l10n/l10n_ja.dart | 753 +++- lib/l10n/l10n_kk.dart | 763 +++- lib/l10n/l10n_ko.dart | 1161 ++++- lib/l10n/l10n_lb.dart | 759 +++- lib/l10n/l10n_lt.dart | 787 +++- lib/l10n/l10n_lv.dart | 761 +++- lib/l10n/l10n_mk.dart | 763 +++- lib/l10n/l10n_nb.dart | 765 +++- lib/l10n/l10n_nl.dart | 773 +++- lib/l10n/l10n_nn.dart | 785 +++- lib/l10n/l10n_pl.dart | 775 +++- lib/l10n/l10n_pt.dart | 5657 +++++++++++++++--------- lib/l10n/l10n_ro.dart | 849 +++- lib/l10n/l10n_ru.dart | 781 +++- lib/l10n/l10n_sk.dart | 771 +++- lib/l10n/l10n_sl.dart | 797 +++- lib/l10n/l10n_sq.dart | 761 +++- lib/l10n/l10n_sr.dart | 761 +++- lib/l10n/l10n_sv.dart | 803 +++- lib/l10n/l10n_tr.dart | 797 +++- lib/l10n/l10n_uk.dart | 771 +++- lib/l10n/l10n_vi.dart | 785 +++- lib/l10n/l10n_zh.dart | 5609 +++++++++++++++-------- lib/l10n/lila_af.arb | 200 +- lib/l10n/lila_ar.arb | 227 +- lib/l10n/lila_az.arb | 152 +- lib/l10n/lila_be.arb | 209 +- lib/l10n/lila_bg.arb | 196 +- lib/l10n/lila_bn.arb | 107 +- lib/l10n/lila_br.arb | 130 +- lib/l10n/lila_bs.arb | 185 +- lib/l10n/lila_ca.arb | 231 +- lib/l10n/lila_cs.arb | 262 +- lib/l10n/lila_da.arb | 234 +- lib/l10n/lila_de.arb | 242 +- lib/l10n/lila_el.arb | 274 +- lib/l10n/lila_en_US.arb | 269 +- lib/l10n/lila_eo.arb | 190 +- lib/l10n/lila_es.arb | 236 +- lib/l10n/lila_et.arb | 173 +- lib/l10n/lila_eu.arb | 268 +- lib/l10n/lila_fa.arb | 459 +- lib/l10n/lila_fi.arb | 234 +- lib/l10n/lila_fo.arb | 136 +- lib/l10n/lila_fr.arb | 234 +- lib/l10n/lila_ga.arb | 178 +- lib/l10n/lila_gl.arb | 264 +- lib/l10n/lila_gsw.arb | 390 +- lib/l10n/lila_he.arb | 264 +- lib/l10n/lila_hi.arb | 186 +- lib/l10n/lila_hr.arb | 181 +- lib/l10n/lila_hu.arb | 194 +- lib/l10n/lila_hy.arb | 180 +- lib/l10n/lila_id.arb | 195 +- lib/l10n/lila_it.arb | 219 +- lib/l10n/lila_ja.arb | 233 +- lib/l10n/lila_kk.arb | 190 +- lib/l10n/lila_ko.arb | 615 ++- lib/l10n/lila_lb.arb | 205 +- lib/l10n/lila_lt.arb | 223 +- lib/l10n/lila_lv.arb | 179 +- lib/l10n/lila_mk.arb | 14 +- lib/l10n/lila_nb.arb | 239 +- lib/l10n/lila_nl.arb | 241 +- lib/l10n/lila_nn.arb | 253 +- lib/l10n/lila_pl.arb | 237 +- lib/l10n/lila_pt.arb | 242 +- lib/l10n/lila_pt_BR.arb | 246 +- lib/l10n/lila_ro.arb | 255 +- lib/l10n/lila_ru.arb | 243 +- lib/l10n/lila_sk.arb | 233 +- lib/l10n/lila_sl.arb | 212 +- lib/l10n/lila_sq.arb | 230 +- lib/l10n/lila_sr.arb | 125 +- lib/l10n/lila_sv.arb | 213 +- lib/l10n/lila_tr.arb | 252 +- lib/l10n/lila_uk.arb | 213 +- lib/l10n/lila_vi.arb | 263 +- lib/l10n/lila_zh.arb | 271 +- lib/l10n/lila_zh_TW.arb | 841 ++-- lib/src/model/puzzle/puzzle_theme.dart | 4 +- 115 files changed, 64888 insertions(+), 8584 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 83000377fe..6734d91450 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -37,7 +37,6 @@ "mobilePuzzleStormConfirmEndRun": "Do you want to end this run?", "mobilePuzzleStormFilterNothingToShow": "Nothing to show, please change the filters", "mobileCancelTakebackOffer": "Cancel takeback offer", - "mobileCancelDrawOffer": "Cancel draw offer", "mobileWaitingForOpponentToJoin": "Waiting for opponent to join...", "mobileBlindfoldMode": "Blindfold", "mobileLiveStreamers": "Live streamers", @@ -147,6 +146,17 @@ } } }, + "activityCompletedNbVariantGames": "{count, plural, =1{Completed {count} {param2} correspondence game} other{Completed {count} {param2} correspondence games}}", + "@activityCompletedNbVariantGames": { + "placeholders": { + "count": { + "type": "int" + }, + "param2": { + "type": "String" + } + } + }, "activityFollowedNbPlayers": "{count, plural, =1{Started following {count} player} other{Started following {count} players}}", "@activityFollowedNbPlayers": { "placeholders": { @@ -229,7 +239,131 @@ } }, "broadcastBroadcasts": "Broadcasts", + "broadcastMyBroadcasts": "My broadcasts", "broadcastLiveBroadcasts": "Live tournament broadcasts", + "broadcastBroadcastCalendar": "Broadcast calendar", + "broadcastNewBroadcast": "New live broadcast", + "broadcastSubscribedBroadcasts": "Subscribed broadcasts", + "broadcastAboutBroadcasts": "About broadcasts", + "broadcastHowToUseLichessBroadcasts": "How to use Lichess Broadcasts.", + "broadcastTheNewRoundHelp": "The new round will have the same members and contributors as the previous one.", + "broadcastAddRound": "Add a round", + "broadcastOngoing": "Ongoing", + "broadcastUpcoming": "Upcoming", + "broadcastCompleted": "Completed", + "broadcastCompletedHelp": "Lichess detects round completion, but can get it wrong. Use this to set it manually.", + "broadcastRoundName": "Round name", + "broadcastRoundNumber": "Round number", + "broadcastTournamentName": "Tournament name", + "broadcastTournamentDescription": "Short tournament description", + "broadcastFullDescription": "Full tournament description", + "broadcastFullDescriptionHelp": "Optional long description of the tournament. {param1} is available. Length must be less than {param2} characters.", + "@broadcastFullDescriptionHelp": { + "placeholders": { + "param1": { + "type": "String" + }, + "param2": { + "type": "String" + } + } + }, + "broadcastSourceSingleUrl": "PGN Source URL", + "broadcastSourceUrlHelp": "URL that Lichess will check to get PGN updates. It must be publicly accessible from the Internet.", + "broadcastSourceGameIds": "Up to 64 Lichess game IDs, separated by spaces.", + "broadcastStartDateTimeZone": "Start date in the tournament local timezone: {param}", + "@broadcastStartDateTimeZone": { + "placeholders": { + "param": { + "type": "String" + } + } + }, + "broadcastStartDateHelp": "Optional, if you know when the event starts", + "broadcastCurrentGameUrl": "Current game URL", + "broadcastDownloadAllRounds": "Download all rounds", + "broadcastResetRound": "Reset this round", + "broadcastDeleteRound": "Delete this round", + "broadcastDefinitivelyDeleteRound": "Definitively delete the round and all its games.", + "broadcastDeleteAllGamesOfThisRound": "Delete all games of this round. The source will need to be active in order to re-create them.", + "broadcastEditRoundStudy": "Edit round study", + "broadcastDeleteTournament": "Delete this tournament", + "broadcastDefinitivelyDeleteTournament": "Definitively delete the entire tournament, all its rounds and all its games.", + "broadcastShowScores": "Show players scores based on game results", + "broadcastReplacePlayerTags": "Optional: replace player names, ratings and titles", + "broadcastFideFederations": "FIDE federations", + "broadcastTop10Rating": "Top 10 rating", + "broadcastFidePlayers": "FIDE players", + "broadcastFidePlayerNotFound": "FIDE player not found", + "broadcastFideProfile": "FIDE profile", + "broadcastFederation": "Federation", + "broadcastAgeThisYear": "Age this year", + "broadcastUnrated": "Unrated", + "broadcastRecentTournaments": "Recent tournaments", + "broadcastOpenLichess": "Open in Lichess", + "broadcastTeams": "Teams", + "broadcastBoards": "Boards", + "broadcastOverview": "Overview", + "broadcastSubscribeTitle": "Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.", + "broadcastUploadImage": "Upload tournament image", + "broadcastNoBoardsYet": "No boards yet. These will appear once games are uploaded.", + "broadcastBoardsCanBeLoaded": "Boards can be loaded with a source or via the {param}", + "@broadcastBoardsCanBeLoaded": { + "placeholders": { + "param": { + "type": "String" + } + } + }, + "broadcastStartsAfter": "Starts after {param}", + "@broadcastStartsAfter": { + "placeholders": { + "param": { + "type": "String" + } + } + }, + "broadcastStartVerySoon": "The broadcast will start very soon.", + "broadcastNotYetStarted": "The broadcast has not yet started.", + "broadcastOfficialWebsite": "Official website", + "broadcastStandings": "Standings", + "broadcastIframeHelp": "More options on the {param}", + "@broadcastIframeHelp": { + "placeholders": { + "param": { + "type": "String" + } + } + }, + "broadcastWebmastersPage": "webmasters page", + "broadcastPgnSourceHelp": "A public, real-time PGN source for this round. We also offer a {param} for faster and more efficient synchronisation.", + "@broadcastPgnSourceHelp": { + "placeholders": { + "param": { + "type": "String" + } + } + }, + "broadcastEmbedThisBroadcast": "Embed this broadcast in your website", + "broadcastEmbedThisRound": "Embed {param} in your website", + "@broadcastEmbedThisRound": { + "placeholders": { + "param": { + "type": "String" + } + } + }, + "broadcastRatingDiff": "Rating diff", + "broadcastGamesThisTournament": "Games in this tournament", + "broadcastScore": "Score", + "broadcastNbBroadcasts": "{count, plural, =1{{count} broadcast} other{{count} broadcasts}}", + "@broadcastNbBroadcasts": { + "placeholders": { + "count": { + "type": "int" + } + } + }, "challengeChallengesX": "Challenges: {param1}", "@challengeChallengesX": { "placeholders": { @@ -752,8 +886,8 @@ "puzzleThemeXRayAttackDescription": "A piece attacks or defends a square, through an enemy piece.", "puzzleThemeZugzwang": "Zugzwang", "puzzleThemeZugzwangDescription": "The opponent is limited in the moves they can make, and all moves worsen their position.", - "puzzleThemeHealthyMix": "Healthy mix", - "puzzleThemeHealthyMixDescription": "A bit of everything. You don't know what to expect, so you remain ready for anything! Just like in real games.", + "puzzleThemeMix": "Healthy mix", + "puzzleThemeMixDescription": "A bit of everything. You don't know what to expect, so you remain ready for anything! Just like in real games.", "puzzleThemePlayerGames": "Player games", "puzzleThemePlayerGamesDescription": "Lookup puzzles generated from your games, or from another player's games.", "puzzleThemePuzzleDownloadInformation": "These puzzles are in the public domain, and can be downloaded from {param}.", @@ -935,7 +1069,6 @@ "memory": "Memory", "infiniteAnalysis": "Infinite analysis", "removesTheDepthLimit": "Removes the depth limit, and keeps your computer warm", - "engineManager": "Engine manager", "blunder": "Blunder", "mistake": "Mistake", "inaccuracy": "Inaccuracy", @@ -1090,6 +1223,7 @@ } }, "gamesPlayed": "Games played", + "ok": "OK", "cancel": "Cancel", "whiteTimeOut": "White time out", "blackTimeOut": "Black time out", @@ -1483,7 +1617,9 @@ "cheat": "Cheat", "troll": "Troll", "other": "Other", - "reportDescriptionHelp": "Paste the link to the game(s) and explain what is wrong about this user's behaviour. Don't just say \"they cheat\", but tell us how you came to this conclusion. Your report will be processed faster if written in English.", + "reportCheatBoostHelp": "Paste the link to the game(s) and explain what is wrong about this user's behaviour. Don't just say \"they cheat\", but tell us how you came to this conclusion.", + "reportUsernameHelp": "Explain what about this username is offensive. Don't just say \"it's offensive/inappropriate\", but tell us how you came to this conclusion, especially if the insult is obfuscated, not in english, is in slang, or is a historical/cultural reference.", + "reportProcessedFasterInEnglish": "Your report will be processed faster if written in English.", "error_provideOneCheatedGameLink": "Please provide at least one link to a cheated game.", "by": "by {param}", "@by": { @@ -2333,6 +2469,7 @@ "showMeEverything": "Show me everything", "lichessPatronInfo": "Lichess is a charity and entirely free/libre open source software.\nAll operating costs, development, and content are funded solely by user donations.", "nothingToSeeHere": "Nothing to see here at the moment.", + "stats": "Stats", "opponentLeftCounter": "{count, plural, =1{Your opponent left the game. You can claim victory in {count} second.} other{Your opponent left the game. You can claim victory in {count} seconds.}}", "@opponentLeftCounter": { "placeholders": { @@ -2773,6 +2910,242 @@ } }, "streamerLichessStreamers": "Lichess streamers", + "studyPrivate": "Private", + "studyMyStudies": "My studies", + "studyStudiesIContributeTo": "Studies I contribute to", + "studyMyPublicStudies": "My public studies", + "studyMyPrivateStudies": "My private studies", + "studyMyFavoriteStudies": "My favourite studies", + "studyWhatAreStudies": "What are studies?", + "studyAllStudies": "All studies", + "studyStudiesCreatedByX": "Studies created by {param}", + "@studyStudiesCreatedByX": { + "placeholders": { + "param": { + "type": "String" + } + } + }, + "studyNoneYet": "None yet.", + "studyHot": "Hot", + "studyDateAddedNewest": "Date added (newest)", + "studyDateAddedOldest": "Date added (oldest)", + "studyRecentlyUpdated": "Recently updated", + "studyMostPopular": "Most popular", + "studyAlphabetical": "Alphabetical", + "studyAddNewChapter": "Add a new chapter", + "studyAddMembers": "Add members", + "studyInviteToTheStudy": "Invite to the study", + "studyPleaseOnlyInvitePeopleYouKnow": "Please only invite people who know you, and who actively want to join this study.", + "studySearchByUsername": "Search by username", + "studySpectator": "Spectator", + "studyContributor": "Contributor", + "studyKick": "Kick", + "studyLeaveTheStudy": "Leave the study", + "studyYouAreNowAContributor": "You are now a contributor", + "studyYouAreNowASpectator": "You are now a spectator", + "studyPgnTags": "PGN tags", + "studyLike": "Like", + "studyUnlike": "Unlike", + "studyNewTag": "New tag", + "studyCommentThisPosition": "Comment on this position", + "studyCommentThisMove": "Comment on this move", + "studyAnnotateWithGlyphs": "Annotate with glyphs", + "studyTheChapterIsTooShortToBeAnalysed": "The chapter is too short to be analysed.", + "studyOnlyContributorsCanRequestAnalysis": "Only the study contributors can request a computer analysis.", + "studyGetAFullComputerAnalysis": "Get a full server-side computer analysis of the mainline.", + "studyMakeSureTheChapterIsComplete": "Make sure the chapter is complete. You can only request analysis once.", + "studyAllSyncMembersRemainOnTheSamePosition": "All SYNC members remain on the same position", + "studyShareChanges": "Share changes with spectators and save them on the server", + "studyPlaying": "Playing", + "studyShowEvalBar": "Evaluation bars", + "studyFirst": "First", + "studyPrevious": "Previous", + "studyNext": "Next", + "studyLast": "Last", "studyShareAndExport": "Share & export", - "studyStart": "Start" + "studyCloneStudy": "Clone", + "studyStudyPgn": "Study PGN", + "studyDownloadAllGames": "Download all games", + "studyChapterPgn": "Chapter PGN", + "studyCopyChapterPgn": "Copy PGN", + "studyDownloadGame": "Download game", + "studyStudyUrl": "Study URL", + "studyCurrentChapterUrl": "Current chapter URL", + "studyYouCanPasteThisInTheForumToEmbed": "You can paste this in the forum or your Lichess blog to embed", + "studyStartAtInitialPosition": "Start at initial position", + "studyStartAtX": "Start at {param}", + "@studyStartAtX": { + "placeholders": { + "param": { + "type": "String" + } + } + }, + "studyEmbedInYourWebsite": "Embed in your website", + "studyReadMoreAboutEmbedding": "Read more about embedding", + "studyOnlyPublicStudiesCanBeEmbedded": "Only public studies can be embedded!", + "studyOpen": "Open", + "studyXBroughtToYouByY": "{param1}, brought to you by {param2}", + "@studyXBroughtToYouByY": { + "placeholders": { + "param1": { + "type": "String" + }, + "param2": { + "type": "String" + } + } + }, + "studyStudyNotFound": "Study not found", + "studyEditChapter": "Edit chapter", + "studyNewChapter": "New chapter", + "studyImportFromChapterX": "Import from {param}", + "@studyImportFromChapterX": { + "placeholders": { + "param": { + "type": "String" + } + } + }, + "studyOrientation": "Orientation", + "studyAnalysisMode": "Analysis mode", + "studyPinnedChapterComment": "Pinned chapter comment", + "studySaveChapter": "Save chapter", + "studyClearAnnotations": "Clear annotations", + "studyClearVariations": "Clear variations", + "studyDeleteChapter": "Delete chapter", + "studyDeleteThisChapter": "Delete this chapter. There is no going back!", + "studyClearAllCommentsInThisChapter": "Clear all comments, glyphs and drawn shapes in this chapter", + "studyRightUnderTheBoard": "Right under the board", + "studyNoPinnedComment": "None", + "studyNormalAnalysis": "Normal analysis", + "studyHideNextMoves": "Hide next moves", + "studyInteractiveLesson": "Interactive lesson", + "studyChapterX": "Chapter {param}", + "@studyChapterX": { + "placeholders": { + "param": { + "type": "String" + } + } + }, + "studyEmpty": "Empty", + "studyStartFromInitialPosition": "Start from initial position", + "studyEditor": "Editor", + "studyStartFromCustomPosition": "Start from custom position", + "studyLoadAGameByUrl": "Load games by URLs", + "studyLoadAPositionFromFen": "Load a position from FEN", + "studyLoadAGameFromPgn": "Load games from PGN", + "studyAutomatic": "Automatic", + "studyUrlOfTheGame": "URL of the games, one per line", + "studyLoadAGameFromXOrY": "Load games from {param1} or {param2}", + "@studyLoadAGameFromXOrY": { + "placeholders": { + "param1": { + "type": "String" + }, + "param2": { + "type": "String" + } + } + }, + "studyCreateChapter": "Create chapter", + "studyCreateStudy": "Create study", + "studyEditStudy": "Edit study", + "studyVisibility": "Visibility", + "studyPublic": "Public", + "studyUnlisted": "Unlisted", + "studyInviteOnly": "Invite only", + "studyAllowCloning": "Allow cloning", + "studyNobody": "Nobody", + "studyOnlyMe": "Only me", + "studyContributors": "Contributors", + "studyMembers": "Members", + "studyEveryone": "Everyone", + "studyEnableSync": "Enable sync", + "studyYesKeepEveryoneOnTheSamePosition": "Yes: keep everyone on the same position", + "studyNoLetPeopleBrowseFreely": "No: let people browse freely", + "studyPinnedStudyComment": "Pinned study comment", + "studyStart": "Start", + "studySave": "Save", + "studyClearChat": "Clear chat", + "studyDeleteTheStudyChatHistory": "Delete the study chat history? There is no going back!", + "studyDeleteStudy": "Delete study", + "studyConfirmDeleteStudy": "Delete the entire study? There is no going back! Type the name of the study to confirm: {param}", + "@studyConfirmDeleteStudy": { + "placeholders": { + "param": { + "type": "String" + } + } + }, + "studyWhereDoYouWantToStudyThat": "Where do you want to study that?", + "studyGoodMove": "Good move", + "studyMistake": "Mistake", + "studyBrilliantMove": "Brilliant move", + "studyBlunder": "Blunder", + "studyInterestingMove": "Interesting move", + "studyDubiousMove": "Dubious move", + "studyOnlyMove": "Only move", + "studyZugzwang": "Zugzwang", + "studyEqualPosition": "Equal position", + "studyUnclearPosition": "Unclear position", + "studyWhiteIsSlightlyBetter": "White is slightly better", + "studyBlackIsSlightlyBetter": "Black is slightly better", + "studyWhiteIsBetter": "White is better", + "studyBlackIsBetter": "Black is better", + "studyWhiteIsWinning": "White is winning", + "studyBlackIsWinning": "Black is winning", + "studyNovelty": "Novelty", + "studyDevelopment": "Development", + "studyInitiative": "Initiative", + "studyAttack": "Attack", + "studyCounterplay": "Counterplay", + "studyTimeTrouble": "Time trouble", + "studyWithCompensation": "With compensation", + "studyWithTheIdea": "With the idea", + "studyNextChapter": "Next chapter", + "studyPrevChapter": "Previous chapter", + "studyStudyActions": "Study actions", + "studyTopics": "Topics", + "studyMyTopics": "My topics", + "studyPopularTopics": "Popular topics", + "studyManageTopics": "Manage topics", + "studyBack": "Back", + "studyPlayAgain": "Play again", + "studyWhatWouldYouPlay": "What would you play in this position?", + "studyYouCompletedThisLesson": "Congratulations! You completed this lesson.", + "studyNbChapters": "{count, plural, =1{{count} Chapter} other{{count} Chapters}}", + "@studyNbChapters": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "studyNbGames": "{count, plural, =1{{count} Game} other{{count} Games}}", + "@studyNbGames": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "studyNbMembers": "{count, plural, =1{{count} Member} other{{count} Members}}", + "@studyNbMembers": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Paste your PGN text here, up to {count} game} other{Paste your PGN text here, up to {count} games}}", + "@studyPasteYourPgnTextHereUpToNbGames": { + "placeholders": { + "count": { + "type": "int" + } + } + } } \ No newline at end of file diff --git a/lib/l10n/l10n.dart b/lib/l10n/l10n.dart index 341eb51750..62cb2d2d20 100644 --- a/lib/l10n/l10n.dart +++ b/lib/l10n/l10n.dart @@ -390,12 +390,6 @@ abstract class AppLocalizations { /// **'Cancel takeback offer'** String get mobileCancelTakebackOffer; - /// No description provided for @mobileCancelDrawOffer. - /// - /// In en, this message translates to: - /// **'Cancel draw offer'** - String get mobileCancelDrawOffer; - /// No description provided for @mobileWaitingForOpponentToJoin. /// /// In en, this message translates to: @@ -540,6 +534,12 @@ abstract class AppLocalizations { /// **'{count, plural, =1{Completed {count} correspondence game} other{Completed {count} correspondence games}}'** String activityCompletedNbGames(int count); + /// No description provided for @activityCompletedNbVariantGames. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{Completed {count} {param2} correspondence game} other{Completed {count} {param2} correspondence games}}'** + String activityCompletedNbVariantGames(int count, String param2); + /// No description provided for @activityFollowedNbPlayers. /// /// In en, this message translates to: @@ -600,12 +600,402 @@ abstract class AppLocalizations { /// **'Broadcasts'** String get broadcastBroadcasts; + /// No description provided for @broadcastMyBroadcasts. + /// + /// In en, this message translates to: + /// **'My broadcasts'** + String get broadcastMyBroadcasts; + /// No description provided for @broadcastLiveBroadcasts. /// /// In en, this message translates to: /// **'Live tournament broadcasts'** String get broadcastLiveBroadcasts; + /// No description provided for @broadcastBroadcastCalendar. + /// + /// In en, this message translates to: + /// **'Broadcast calendar'** + String get broadcastBroadcastCalendar; + + /// No description provided for @broadcastNewBroadcast. + /// + /// In en, this message translates to: + /// **'New live broadcast'** + String get broadcastNewBroadcast; + + /// No description provided for @broadcastSubscribedBroadcasts. + /// + /// In en, this message translates to: + /// **'Subscribed broadcasts'** + String get broadcastSubscribedBroadcasts; + + /// No description provided for @broadcastAboutBroadcasts. + /// + /// In en, this message translates to: + /// **'About broadcasts'** + String get broadcastAboutBroadcasts; + + /// No description provided for @broadcastHowToUseLichessBroadcasts. + /// + /// In en, this message translates to: + /// **'How to use Lichess Broadcasts.'** + String get broadcastHowToUseLichessBroadcasts; + + /// No description provided for @broadcastTheNewRoundHelp. + /// + /// In en, this message translates to: + /// **'The new round will have the same members and contributors as the previous one.'** + String get broadcastTheNewRoundHelp; + + /// No description provided for @broadcastAddRound. + /// + /// In en, this message translates to: + /// **'Add a round'** + String get broadcastAddRound; + + /// No description provided for @broadcastOngoing. + /// + /// In en, this message translates to: + /// **'Ongoing'** + String get broadcastOngoing; + + /// No description provided for @broadcastUpcoming. + /// + /// In en, this message translates to: + /// **'Upcoming'** + String get broadcastUpcoming; + + /// No description provided for @broadcastCompleted. + /// + /// In en, this message translates to: + /// **'Completed'** + String get broadcastCompleted; + + /// No description provided for @broadcastCompletedHelp. + /// + /// In en, this message translates to: + /// **'Lichess detects round completion, but can get it wrong. Use this to set it manually.'** + String get broadcastCompletedHelp; + + /// No description provided for @broadcastRoundName. + /// + /// In en, this message translates to: + /// **'Round name'** + String get broadcastRoundName; + + /// No description provided for @broadcastRoundNumber. + /// + /// In en, this message translates to: + /// **'Round number'** + String get broadcastRoundNumber; + + /// No description provided for @broadcastTournamentName. + /// + /// In en, this message translates to: + /// **'Tournament name'** + String get broadcastTournamentName; + + /// No description provided for @broadcastTournamentDescription. + /// + /// In en, this message translates to: + /// **'Short tournament description'** + String get broadcastTournamentDescription; + + /// No description provided for @broadcastFullDescription. + /// + /// In en, this message translates to: + /// **'Full tournament description'** + String get broadcastFullDescription; + + /// No description provided for @broadcastFullDescriptionHelp. + /// + /// In en, this message translates to: + /// **'Optional long description of the tournament. {param1} is available. Length must be less than {param2} characters.'** + String broadcastFullDescriptionHelp(String param1, String param2); + + /// No description provided for @broadcastSourceSingleUrl. + /// + /// In en, this message translates to: + /// **'PGN Source URL'** + String get broadcastSourceSingleUrl; + + /// No description provided for @broadcastSourceUrlHelp. + /// + /// In en, this message translates to: + /// **'URL that Lichess will check to get PGN updates. It must be publicly accessible from the Internet.'** + String get broadcastSourceUrlHelp; + + /// No description provided for @broadcastSourceGameIds. + /// + /// In en, this message translates to: + /// **'Up to 64 Lichess game IDs, separated by spaces.'** + String get broadcastSourceGameIds; + + /// No description provided for @broadcastStartDateTimeZone. + /// + /// In en, this message translates to: + /// **'Start date in the tournament local timezone: {param}'** + String broadcastStartDateTimeZone(String param); + + /// No description provided for @broadcastStartDateHelp. + /// + /// In en, this message translates to: + /// **'Optional, if you know when the event starts'** + String get broadcastStartDateHelp; + + /// No description provided for @broadcastCurrentGameUrl. + /// + /// In en, this message translates to: + /// **'Current game URL'** + String get broadcastCurrentGameUrl; + + /// No description provided for @broadcastDownloadAllRounds. + /// + /// In en, this message translates to: + /// **'Download all rounds'** + String get broadcastDownloadAllRounds; + + /// No description provided for @broadcastResetRound. + /// + /// In en, this message translates to: + /// **'Reset this round'** + String get broadcastResetRound; + + /// No description provided for @broadcastDeleteRound. + /// + /// In en, this message translates to: + /// **'Delete this round'** + String get broadcastDeleteRound; + + /// No description provided for @broadcastDefinitivelyDeleteRound. + /// + /// In en, this message translates to: + /// **'Definitively delete the round and all its games.'** + String get broadcastDefinitivelyDeleteRound; + + /// No description provided for @broadcastDeleteAllGamesOfThisRound. + /// + /// In en, this message translates to: + /// **'Delete all games of this round. The source will need to be active in order to re-create them.'** + String get broadcastDeleteAllGamesOfThisRound; + + /// No description provided for @broadcastEditRoundStudy. + /// + /// In en, this message translates to: + /// **'Edit round study'** + String get broadcastEditRoundStudy; + + /// No description provided for @broadcastDeleteTournament. + /// + /// In en, this message translates to: + /// **'Delete this tournament'** + String get broadcastDeleteTournament; + + /// No description provided for @broadcastDefinitivelyDeleteTournament. + /// + /// In en, this message translates to: + /// **'Definitively delete the entire tournament, all its rounds and all its games.'** + String get broadcastDefinitivelyDeleteTournament; + + /// No description provided for @broadcastShowScores. + /// + /// In en, this message translates to: + /// **'Show players scores based on game results'** + String get broadcastShowScores; + + /// No description provided for @broadcastReplacePlayerTags. + /// + /// In en, this message translates to: + /// **'Optional: replace player names, ratings and titles'** + String get broadcastReplacePlayerTags; + + /// No description provided for @broadcastFideFederations. + /// + /// In en, this message translates to: + /// **'FIDE federations'** + String get broadcastFideFederations; + + /// No description provided for @broadcastTop10Rating. + /// + /// In en, this message translates to: + /// **'Top 10 rating'** + String get broadcastTop10Rating; + + /// No description provided for @broadcastFidePlayers. + /// + /// In en, this message translates to: + /// **'FIDE players'** + String get broadcastFidePlayers; + + /// No description provided for @broadcastFidePlayerNotFound. + /// + /// In en, this message translates to: + /// **'FIDE player not found'** + String get broadcastFidePlayerNotFound; + + /// No description provided for @broadcastFideProfile. + /// + /// In en, this message translates to: + /// **'FIDE profile'** + String get broadcastFideProfile; + + /// No description provided for @broadcastFederation. + /// + /// In en, this message translates to: + /// **'Federation'** + String get broadcastFederation; + + /// No description provided for @broadcastAgeThisYear. + /// + /// In en, this message translates to: + /// **'Age this year'** + String get broadcastAgeThisYear; + + /// No description provided for @broadcastUnrated. + /// + /// In en, this message translates to: + /// **'Unrated'** + String get broadcastUnrated; + + /// No description provided for @broadcastRecentTournaments. + /// + /// In en, this message translates to: + /// **'Recent tournaments'** + String get broadcastRecentTournaments; + + /// No description provided for @broadcastOpenLichess. + /// + /// In en, this message translates to: + /// **'Open in Lichess'** + String get broadcastOpenLichess; + + /// No description provided for @broadcastTeams. + /// + /// In en, this message translates to: + /// **'Teams'** + String get broadcastTeams; + + /// No description provided for @broadcastBoards. + /// + /// In en, this message translates to: + /// **'Boards'** + String get broadcastBoards; + + /// No description provided for @broadcastOverview. + /// + /// In en, this message translates to: + /// **'Overview'** + String get broadcastOverview; + + /// No description provided for @broadcastSubscribeTitle. + /// + /// In en, this message translates to: + /// **'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'** + String get broadcastSubscribeTitle; + + /// No description provided for @broadcastUploadImage. + /// + /// In en, this message translates to: + /// **'Upload tournament image'** + String get broadcastUploadImage; + + /// No description provided for @broadcastNoBoardsYet. + /// + /// In en, this message translates to: + /// **'No boards yet. These will appear once games are uploaded.'** + String get broadcastNoBoardsYet; + + /// No description provided for @broadcastBoardsCanBeLoaded. + /// + /// In en, this message translates to: + /// **'Boards can be loaded with a source or via the {param}'** + String broadcastBoardsCanBeLoaded(String param); + + /// No description provided for @broadcastStartsAfter. + /// + /// In en, this message translates to: + /// **'Starts after {param}'** + String broadcastStartsAfter(String param); + + /// No description provided for @broadcastStartVerySoon. + /// + /// In en, this message translates to: + /// **'The broadcast will start very soon.'** + String get broadcastStartVerySoon; + + /// No description provided for @broadcastNotYetStarted. + /// + /// In en, this message translates to: + /// **'The broadcast has not yet started.'** + String get broadcastNotYetStarted; + + /// No description provided for @broadcastOfficialWebsite. + /// + /// In en, this message translates to: + /// **'Official website'** + String get broadcastOfficialWebsite; + + /// No description provided for @broadcastStandings. + /// + /// In en, this message translates to: + /// **'Standings'** + String get broadcastStandings; + + /// No description provided for @broadcastIframeHelp. + /// + /// In en, this message translates to: + /// **'More options on the {param}'** + String broadcastIframeHelp(String param); + + /// No description provided for @broadcastWebmastersPage. + /// + /// In en, this message translates to: + /// **'webmasters page'** + String get broadcastWebmastersPage; + + /// No description provided for @broadcastPgnSourceHelp. + /// + /// In en, this message translates to: + /// **'A public, real-time PGN source for this round. We also offer a {param} for faster and more efficient synchronisation.'** + String broadcastPgnSourceHelp(String param); + + /// No description provided for @broadcastEmbedThisBroadcast. + /// + /// In en, this message translates to: + /// **'Embed this broadcast in your website'** + String get broadcastEmbedThisBroadcast; + + /// No description provided for @broadcastEmbedThisRound. + /// + /// In en, this message translates to: + /// **'Embed {param} in your website'** + String broadcastEmbedThisRound(String param); + + /// No description provided for @broadcastRatingDiff. + /// + /// In en, this message translates to: + /// **'Rating diff'** + String get broadcastRatingDiff; + + /// No description provided for @broadcastGamesThisTournament. + /// + /// In en, this message translates to: + /// **'Games in this tournament'** + String get broadcastGamesThisTournament; + + /// No description provided for @broadcastScore. + /// + /// In en, this message translates to: + /// **'Score'** + String get broadcastScore; + + /// No description provided for @broadcastNbBroadcasts. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{{count} broadcast} other{{count} broadcasts}}'** + String broadcastNbBroadcasts(int count); + /// No description provided for @challengeChallengesX. /// /// In en, this message translates to: @@ -2514,17 +2904,17 @@ abstract class AppLocalizations { /// **'The opponent is limited in the moves they can make, and all moves worsen their position.'** String get puzzleThemeZugzwangDescription; - /// No description provided for @puzzleThemeHealthyMix. + /// No description provided for @puzzleThemeMix. /// /// In en, this message translates to: /// **'Healthy mix'** - String get puzzleThemeHealthyMix; + String get puzzleThemeMix; - /// No description provided for @puzzleThemeHealthyMixDescription. + /// No description provided for @puzzleThemeMixDescription. /// /// In en, this message translates to: /// **'A bit of everything. You don\'t know what to expect, so you remain ready for anything! Just like in real games.'** - String get puzzleThemeHealthyMixDescription; + String get puzzleThemeMixDescription; /// No description provided for @puzzleThemePlayerGames. /// @@ -3306,12 +3696,6 @@ abstract class AppLocalizations { /// **'Removes the depth limit, and keeps your computer warm'** String get removesTheDepthLimit; - /// No description provided for @engineManager. - /// - /// In en, this message translates to: - /// **'Engine manager'** - String get engineManager; - /// No description provided for @blunder. /// /// In en, this message translates to: @@ -3798,6 +4182,12 @@ abstract class AppLocalizations { /// **'Games played'** String get gamesPlayed; + /// No description provided for @ok. + /// + /// In en, this message translates to: + /// **'OK'** + String get ok; + /// No description provided for @cancel. /// /// In en, this message translates to: @@ -5130,11 +5520,23 @@ abstract class AppLocalizations { /// **'Other'** String get other; - /// No description provided for @reportDescriptionHelp. + /// No description provided for @reportCheatBoostHelp. + /// + /// In en, this message translates to: + /// **'Paste the link to the game(s) and explain what is wrong about this user\'s behaviour. Don\'t just say \"they cheat\", but tell us how you came to this conclusion.'** + String get reportCheatBoostHelp; + + /// No description provided for @reportUsernameHelp. /// /// In en, this message translates to: - /// **'Paste the link to the game(s) and explain what is wrong about this user\'s behaviour. Don\'t just say \"they cheat\", but tell us how you came to this conclusion. Your report will be processed faster if written in English.'** - String get reportDescriptionHelp; + /// **'Explain what about this username is offensive. Don\'t just say \"it\'s offensive/inappropriate\", but tell us how you came to this conclusion, especially if the insult is obfuscated, not in english, is in slang, or is a historical/cultural reference.'** + String get reportUsernameHelp; + + /// No description provided for @reportProcessedFasterInEnglish. + /// + /// In en, this message translates to: + /// **'Your report will be processed faster if written in English.'** + String get reportProcessedFasterInEnglish; /// No description provided for @error_provideOneCheatedGameLink. /// @@ -7518,6 +7920,12 @@ abstract class AppLocalizations { /// **'Nothing to see here at the moment.'** String get nothingToSeeHere; + /// No description provided for @stats. + /// + /// In en, this message translates to: + /// **'Stats'** + String get stats; + /// No description provided for @opponentLeftCounter. /// /// In en, this message translates to: @@ -8094,17 +8502,935 @@ abstract class AppLocalizations { /// **'Lichess streamers'** String get streamerLichessStreamers; - /// No description provided for @studyShareAndExport. + /// No description provided for @studyPrivate. /// /// In en, this message translates to: - /// **'Share & export'** - String get studyShareAndExport; + /// **'Private'** + String get studyPrivate; - /// No description provided for @studyStart. + /// No description provided for @studyMyStudies. /// /// In en, this message translates to: - /// **'Start'** - String get studyStart; + /// **'My studies'** + String get studyMyStudies; + + /// No description provided for @studyStudiesIContributeTo. + /// + /// In en, this message translates to: + /// **'Studies I contribute to'** + String get studyStudiesIContributeTo; + + /// No description provided for @studyMyPublicStudies. + /// + /// In en, this message translates to: + /// **'My public studies'** + String get studyMyPublicStudies; + + /// No description provided for @studyMyPrivateStudies. + /// + /// In en, this message translates to: + /// **'My private studies'** + String get studyMyPrivateStudies; + + /// No description provided for @studyMyFavoriteStudies. + /// + /// In en, this message translates to: + /// **'My favourite studies'** + String get studyMyFavoriteStudies; + + /// No description provided for @studyWhatAreStudies. + /// + /// In en, this message translates to: + /// **'What are studies?'** + String get studyWhatAreStudies; + + /// No description provided for @studyAllStudies. + /// + /// In en, this message translates to: + /// **'All studies'** + String get studyAllStudies; + + /// No description provided for @studyStudiesCreatedByX. + /// + /// In en, this message translates to: + /// **'Studies created by {param}'** + String studyStudiesCreatedByX(String param); + + /// No description provided for @studyNoneYet. + /// + /// In en, this message translates to: + /// **'None yet.'** + String get studyNoneYet; + + /// No description provided for @studyHot. + /// + /// In en, this message translates to: + /// **'Hot'** + String get studyHot; + + /// No description provided for @studyDateAddedNewest. + /// + /// In en, this message translates to: + /// **'Date added (newest)'** + String get studyDateAddedNewest; + + /// No description provided for @studyDateAddedOldest. + /// + /// In en, this message translates to: + /// **'Date added (oldest)'** + String get studyDateAddedOldest; + + /// No description provided for @studyRecentlyUpdated. + /// + /// In en, this message translates to: + /// **'Recently updated'** + String get studyRecentlyUpdated; + + /// No description provided for @studyMostPopular. + /// + /// In en, this message translates to: + /// **'Most popular'** + String get studyMostPopular; + + /// No description provided for @studyAlphabetical. + /// + /// In en, this message translates to: + /// **'Alphabetical'** + String get studyAlphabetical; + + /// No description provided for @studyAddNewChapter. + /// + /// In en, this message translates to: + /// **'Add a new chapter'** + String get studyAddNewChapter; + + /// No description provided for @studyAddMembers. + /// + /// In en, this message translates to: + /// **'Add members'** + String get studyAddMembers; + + /// No description provided for @studyInviteToTheStudy. + /// + /// In en, this message translates to: + /// **'Invite to the study'** + String get studyInviteToTheStudy; + + /// No description provided for @studyPleaseOnlyInvitePeopleYouKnow. + /// + /// In en, this message translates to: + /// **'Please only invite people who know you, and who actively want to join this study.'** + String get studyPleaseOnlyInvitePeopleYouKnow; + + /// No description provided for @studySearchByUsername. + /// + /// In en, this message translates to: + /// **'Search by username'** + String get studySearchByUsername; + + /// No description provided for @studySpectator. + /// + /// In en, this message translates to: + /// **'Spectator'** + String get studySpectator; + + /// No description provided for @studyContributor. + /// + /// In en, this message translates to: + /// **'Contributor'** + String get studyContributor; + + /// No description provided for @studyKick. + /// + /// In en, this message translates to: + /// **'Kick'** + String get studyKick; + + /// No description provided for @studyLeaveTheStudy. + /// + /// In en, this message translates to: + /// **'Leave the study'** + String get studyLeaveTheStudy; + + /// No description provided for @studyYouAreNowAContributor. + /// + /// In en, this message translates to: + /// **'You are now a contributor'** + String get studyYouAreNowAContributor; + + /// No description provided for @studyYouAreNowASpectator. + /// + /// In en, this message translates to: + /// **'You are now a spectator'** + String get studyYouAreNowASpectator; + + /// No description provided for @studyPgnTags. + /// + /// In en, this message translates to: + /// **'PGN tags'** + String get studyPgnTags; + + /// No description provided for @studyLike. + /// + /// In en, this message translates to: + /// **'Like'** + String get studyLike; + + /// No description provided for @studyUnlike. + /// + /// In en, this message translates to: + /// **'Unlike'** + String get studyUnlike; + + /// No description provided for @studyNewTag. + /// + /// In en, this message translates to: + /// **'New tag'** + String get studyNewTag; + + /// No description provided for @studyCommentThisPosition. + /// + /// In en, this message translates to: + /// **'Comment on this position'** + String get studyCommentThisPosition; + + /// No description provided for @studyCommentThisMove. + /// + /// In en, this message translates to: + /// **'Comment on this move'** + String get studyCommentThisMove; + + /// No description provided for @studyAnnotateWithGlyphs. + /// + /// In en, this message translates to: + /// **'Annotate with glyphs'** + String get studyAnnotateWithGlyphs; + + /// No description provided for @studyTheChapterIsTooShortToBeAnalysed. + /// + /// In en, this message translates to: + /// **'The chapter is too short to be analysed.'** + String get studyTheChapterIsTooShortToBeAnalysed; + + /// No description provided for @studyOnlyContributorsCanRequestAnalysis. + /// + /// In en, this message translates to: + /// **'Only the study contributors can request a computer analysis.'** + String get studyOnlyContributorsCanRequestAnalysis; + + /// No description provided for @studyGetAFullComputerAnalysis. + /// + /// In en, this message translates to: + /// **'Get a full server-side computer analysis of the mainline.'** + String get studyGetAFullComputerAnalysis; + + /// No description provided for @studyMakeSureTheChapterIsComplete. + /// + /// In en, this message translates to: + /// **'Make sure the chapter is complete. You can only request analysis once.'** + String get studyMakeSureTheChapterIsComplete; + + /// No description provided for @studyAllSyncMembersRemainOnTheSamePosition. + /// + /// In en, this message translates to: + /// **'All SYNC members remain on the same position'** + String get studyAllSyncMembersRemainOnTheSamePosition; + + /// No description provided for @studyShareChanges. + /// + /// In en, this message translates to: + /// **'Share changes with spectators and save them on the server'** + String get studyShareChanges; + + /// No description provided for @studyPlaying. + /// + /// In en, this message translates to: + /// **'Playing'** + String get studyPlaying; + + /// No description provided for @studyShowEvalBar. + /// + /// In en, this message translates to: + /// **'Evaluation bars'** + String get studyShowEvalBar; + + /// No description provided for @studyFirst. + /// + /// In en, this message translates to: + /// **'First'** + String get studyFirst; + + /// No description provided for @studyPrevious. + /// + /// In en, this message translates to: + /// **'Previous'** + String get studyPrevious; + + /// No description provided for @studyNext. + /// + /// In en, this message translates to: + /// **'Next'** + String get studyNext; + + /// No description provided for @studyLast. + /// + /// In en, this message translates to: + /// **'Last'** + String get studyLast; + + /// No description provided for @studyShareAndExport. + /// + /// In en, this message translates to: + /// **'Share & export'** + String get studyShareAndExport; + + /// No description provided for @studyCloneStudy. + /// + /// In en, this message translates to: + /// **'Clone'** + String get studyCloneStudy; + + /// No description provided for @studyStudyPgn. + /// + /// In en, this message translates to: + /// **'Study PGN'** + String get studyStudyPgn; + + /// No description provided for @studyDownloadAllGames. + /// + /// In en, this message translates to: + /// **'Download all games'** + String get studyDownloadAllGames; + + /// No description provided for @studyChapterPgn. + /// + /// In en, this message translates to: + /// **'Chapter PGN'** + String get studyChapterPgn; + + /// No description provided for @studyCopyChapterPgn. + /// + /// In en, this message translates to: + /// **'Copy PGN'** + String get studyCopyChapterPgn; + + /// No description provided for @studyDownloadGame. + /// + /// In en, this message translates to: + /// **'Download game'** + String get studyDownloadGame; + + /// No description provided for @studyStudyUrl. + /// + /// In en, this message translates to: + /// **'Study URL'** + String get studyStudyUrl; + + /// No description provided for @studyCurrentChapterUrl. + /// + /// In en, this message translates to: + /// **'Current chapter URL'** + String get studyCurrentChapterUrl; + + /// No description provided for @studyYouCanPasteThisInTheForumToEmbed. + /// + /// In en, this message translates to: + /// **'You can paste this in the forum or your Lichess blog to embed'** + String get studyYouCanPasteThisInTheForumToEmbed; + + /// No description provided for @studyStartAtInitialPosition. + /// + /// In en, this message translates to: + /// **'Start at initial position'** + String get studyStartAtInitialPosition; + + /// No description provided for @studyStartAtX. + /// + /// In en, this message translates to: + /// **'Start at {param}'** + String studyStartAtX(String param); + + /// No description provided for @studyEmbedInYourWebsite. + /// + /// In en, this message translates to: + /// **'Embed in your website'** + String get studyEmbedInYourWebsite; + + /// No description provided for @studyReadMoreAboutEmbedding. + /// + /// In en, this message translates to: + /// **'Read more about embedding'** + String get studyReadMoreAboutEmbedding; + + /// No description provided for @studyOnlyPublicStudiesCanBeEmbedded. + /// + /// In en, this message translates to: + /// **'Only public studies can be embedded!'** + String get studyOnlyPublicStudiesCanBeEmbedded; + + /// No description provided for @studyOpen. + /// + /// In en, this message translates to: + /// **'Open'** + String get studyOpen; + + /// No description provided for @studyXBroughtToYouByY. + /// + /// In en, this message translates to: + /// **'{param1}, brought to you by {param2}'** + String studyXBroughtToYouByY(String param1, String param2); + + /// No description provided for @studyStudyNotFound. + /// + /// In en, this message translates to: + /// **'Study not found'** + String get studyStudyNotFound; + + /// No description provided for @studyEditChapter. + /// + /// In en, this message translates to: + /// **'Edit chapter'** + String get studyEditChapter; + + /// No description provided for @studyNewChapter. + /// + /// In en, this message translates to: + /// **'New chapter'** + String get studyNewChapter; + + /// No description provided for @studyImportFromChapterX. + /// + /// In en, this message translates to: + /// **'Import from {param}'** + String studyImportFromChapterX(String param); + + /// No description provided for @studyOrientation. + /// + /// In en, this message translates to: + /// **'Orientation'** + String get studyOrientation; + + /// No description provided for @studyAnalysisMode. + /// + /// In en, this message translates to: + /// **'Analysis mode'** + String get studyAnalysisMode; + + /// No description provided for @studyPinnedChapterComment. + /// + /// In en, this message translates to: + /// **'Pinned chapter comment'** + String get studyPinnedChapterComment; + + /// No description provided for @studySaveChapter. + /// + /// In en, this message translates to: + /// **'Save chapter'** + String get studySaveChapter; + + /// No description provided for @studyClearAnnotations. + /// + /// In en, this message translates to: + /// **'Clear annotations'** + String get studyClearAnnotations; + + /// No description provided for @studyClearVariations. + /// + /// In en, this message translates to: + /// **'Clear variations'** + String get studyClearVariations; + + /// No description provided for @studyDeleteChapter. + /// + /// In en, this message translates to: + /// **'Delete chapter'** + String get studyDeleteChapter; + + /// No description provided for @studyDeleteThisChapter. + /// + /// In en, this message translates to: + /// **'Delete this chapter. There is no going back!'** + String get studyDeleteThisChapter; + + /// No description provided for @studyClearAllCommentsInThisChapter. + /// + /// In en, this message translates to: + /// **'Clear all comments, glyphs and drawn shapes in this chapter'** + String get studyClearAllCommentsInThisChapter; + + /// No description provided for @studyRightUnderTheBoard. + /// + /// In en, this message translates to: + /// **'Right under the board'** + String get studyRightUnderTheBoard; + + /// No description provided for @studyNoPinnedComment. + /// + /// In en, this message translates to: + /// **'None'** + String get studyNoPinnedComment; + + /// No description provided for @studyNormalAnalysis. + /// + /// In en, this message translates to: + /// **'Normal analysis'** + String get studyNormalAnalysis; + + /// No description provided for @studyHideNextMoves. + /// + /// In en, this message translates to: + /// **'Hide next moves'** + String get studyHideNextMoves; + + /// No description provided for @studyInteractiveLesson. + /// + /// In en, this message translates to: + /// **'Interactive lesson'** + String get studyInteractiveLesson; + + /// No description provided for @studyChapterX. + /// + /// In en, this message translates to: + /// **'Chapter {param}'** + String studyChapterX(String param); + + /// No description provided for @studyEmpty. + /// + /// In en, this message translates to: + /// **'Empty'** + String get studyEmpty; + + /// No description provided for @studyStartFromInitialPosition. + /// + /// In en, this message translates to: + /// **'Start from initial position'** + String get studyStartFromInitialPosition; + + /// No description provided for @studyEditor. + /// + /// In en, this message translates to: + /// **'Editor'** + String get studyEditor; + + /// No description provided for @studyStartFromCustomPosition. + /// + /// In en, this message translates to: + /// **'Start from custom position'** + String get studyStartFromCustomPosition; + + /// No description provided for @studyLoadAGameByUrl. + /// + /// In en, this message translates to: + /// **'Load games by URLs'** + String get studyLoadAGameByUrl; + + /// No description provided for @studyLoadAPositionFromFen. + /// + /// In en, this message translates to: + /// **'Load a position from FEN'** + String get studyLoadAPositionFromFen; + + /// No description provided for @studyLoadAGameFromPgn. + /// + /// In en, this message translates to: + /// **'Load games from PGN'** + String get studyLoadAGameFromPgn; + + /// No description provided for @studyAutomatic. + /// + /// In en, this message translates to: + /// **'Automatic'** + String get studyAutomatic; + + /// No description provided for @studyUrlOfTheGame. + /// + /// In en, this message translates to: + /// **'URL of the games, one per line'** + String get studyUrlOfTheGame; + + /// No description provided for @studyLoadAGameFromXOrY. + /// + /// In en, this message translates to: + /// **'Load games from {param1} or {param2}'** + String studyLoadAGameFromXOrY(String param1, String param2); + + /// No description provided for @studyCreateChapter. + /// + /// In en, this message translates to: + /// **'Create chapter'** + String get studyCreateChapter; + + /// No description provided for @studyCreateStudy. + /// + /// In en, this message translates to: + /// **'Create study'** + String get studyCreateStudy; + + /// No description provided for @studyEditStudy. + /// + /// In en, this message translates to: + /// **'Edit study'** + String get studyEditStudy; + + /// No description provided for @studyVisibility. + /// + /// In en, this message translates to: + /// **'Visibility'** + String get studyVisibility; + + /// No description provided for @studyPublic. + /// + /// In en, this message translates to: + /// **'Public'** + String get studyPublic; + + /// No description provided for @studyUnlisted. + /// + /// In en, this message translates to: + /// **'Unlisted'** + String get studyUnlisted; + + /// No description provided for @studyInviteOnly. + /// + /// In en, this message translates to: + /// **'Invite only'** + String get studyInviteOnly; + + /// No description provided for @studyAllowCloning. + /// + /// In en, this message translates to: + /// **'Allow cloning'** + String get studyAllowCloning; + + /// No description provided for @studyNobody. + /// + /// In en, this message translates to: + /// **'Nobody'** + String get studyNobody; + + /// No description provided for @studyOnlyMe. + /// + /// In en, this message translates to: + /// **'Only me'** + String get studyOnlyMe; + + /// No description provided for @studyContributors. + /// + /// In en, this message translates to: + /// **'Contributors'** + String get studyContributors; + + /// No description provided for @studyMembers. + /// + /// In en, this message translates to: + /// **'Members'** + String get studyMembers; + + /// No description provided for @studyEveryone. + /// + /// In en, this message translates to: + /// **'Everyone'** + String get studyEveryone; + + /// No description provided for @studyEnableSync. + /// + /// In en, this message translates to: + /// **'Enable sync'** + String get studyEnableSync; + + /// No description provided for @studyYesKeepEveryoneOnTheSamePosition. + /// + /// In en, this message translates to: + /// **'Yes: keep everyone on the same position'** + String get studyYesKeepEveryoneOnTheSamePosition; + + /// No description provided for @studyNoLetPeopleBrowseFreely. + /// + /// In en, this message translates to: + /// **'No: let people browse freely'** + String get studyNoLetPeopleBrowseFreely; + + /// No description provided for @studyPinnedStudyComment. + /// + /// In en, this message translates to: + /// **'Pinned study comment'** + String get studyPinnedStudyComment; + + /// No description provided for @studyStart. + /// + /// In en, this message translates to: + /// **'Start'** + String get studyStart; + + /// No description provided for @studySave. + /// + /// In en, this message translates to: + /// **'Save'** + String get studySave; + + /// No description provided for @studyClearChat. + /// + /// In en, this message translates to: + /// **'Clear chat'** + String get studyClearChat; + + /// No description provided for @studyDeleteTheStudyChatHistory. + /// + /// In en, this message translates to: + /// **'Delete the study chat history? There is no going back!'** + String get studyDeleteTheStudyChatHistory; + + /// No description provided for @studyDeleteStudy. + /// + /// In en, this message translates to: + /// **'Delete study'** + String get studyDeleteStudy; + + /// No description provided for @studyConfirmDeleteStudy. + /// + /// In en, this message translates to: + /// **'Delete the entire study? There is no going back! Type the name of the study to confirm: {param}'** + String studyConfirmDeleteStudy(String param); + + /// No description provided for @studyWhereDoYouWantToStudyThat. + /// + /// In en, this message translates to: + /// **'Where do you want to study that?'** + String get studyWhereDoYouWantToStudyThat; + + /// No description provided for @studyGoodMove. + /// + /// In en, this message translates to: + /// **'Good move'** + String get studyGoodMove; + + /// No description provided for @studyMistake. + /// + /// In en, this message translates to: + /// **'Mistake'** + String get studyMistake; + + /// No description provided for @studyBrilliantMove. + /// + /// In en, this message translates to: + /// **'Brilliant move'** + String get studyBrilliantMove; + + /// No description provided for @studyBlunder. + /// + /// In en, this message translates to: + /// **'Blunder'** + String get studyBlunder; + + /// No description provided for @studyInterestingMove. + /// + /// In en, this message translates to: + /// **'Interesting move'** + String get studyInterestingMove; + + /// No description provided for @studyDubiousMove. + /// + /// In en, this message translates to: + /// **'Dubious move'** + String get studyDubiousMove; + + /// No description provided for @studyOnlyMove. + /// + /// In en, this message translates to: + /// **'Only move'** + String get studyOnlyMove; + + /// No description provided for @studyZugzwang. + /// + /// In en, this message translates to: + /// **'Zugzwang'** + String get studyZugzwang; + + /// No description provided for @studyEqualPosition. + /// + /// In en, this message translates to: + /// **'Equal position'** + String get studyEqualPosition; + + /// No description provided for @studyUnclearPosition. + /// + /// In en, this message translates to: + /// **'Unclear position'** + String get studyUnclearPosition; + + /// No description provided for @studyWhiteIsSlightlyBetter. + /// + /// In en, this message translates to: + /// **'White is slightly better'** + String get studyWhiteIsSlightlyBetter; + + /// No description provided for @studyBlackIsSlightlyBetter. + /// + /// In en, this message translates to: + /// **'Black is slightly better'** + String get studyBlackIsSlightlyBetter; + + /// No description provided for @studyWhiteIsBetter. + /// + /// In en, this message translates to: + /// **'White is better'** + String get studyWhiteIsBetter; + + /// No description provided for @studyBlackIsBetter. + /// + /// In en, this message translates to: + /// **'Black is better'** + String get studyBlackIsBetter; + + /// No description provided for @studyWhiteIsWinning. + /// + /// In en, this message translates to: + /// **'White is winning'** + String get studyWhiteIsWinning; + + /// No description provided for @studyBlackIsWinning. + /// + /// In en, this message translates to: + /// **'Black is winning'** + String get studyBlackIsWinning; + + /// No description provided for @studyNovelty. + /// + /// In en, this message translates to: + /// **'Novelty'** + String get studyNovelty; + + /// No description provided for @studyDevelopment. + /// + /// In en, this message translates to: + /// **'Development'** + String get studyDevelopment; + + /// No description provided for @studyInitiative. + /// + /// In en, this message translates to: + /// **'Initiative'** + String get studyInitiative; + + /// No description provided for @studyAttack. + /// + /// In en, this message translates to: + /// **'Attack'** + String get studyAttack; + + /// No description provided for @studyCounterplay. + /// + /// In en, this message translates to: + /// **'Counterplay'** + String get studyCounterplay; + + /// No description provided for @studyTimeTrouble. + /// + /// In en, this message translates to: + /// **'Time trouble'** + String get studyTimeTrouble; + + /// No description provided for @studyWithCompensation. + /// + /// In en, this message translates to: + /// **'With compensation'** + String get studyWithCompensation; + + /// No description provided for @studyWithTheIdea. + /// + /// In en, this message translates to: + /// **'With the idea'** + String get studyWithTheIdea; + + /// No description provided for @studyNextChapter. + /// + /// In en, this message translates to: + /// **'Next chapter'** + String get studyNextChapter; + + /// No description provided for @studyPrevChapter. + /// + /// In en, this message translates to: + /// **'Previous chapter'** + String get studyPrevChapter; + + /// No description provided for @studyStudyActions. + /// + /// In en, this message translates to: + /// **'Study actions'** + String get studyStudyActions; + + /// No description provided for @studyTopics. + /// + /// In en, this message translates to: + /// **'Topics'** + String get studyTopics; + + /// No description provided for @studyMyTopics. + /// + /// In en, this message translates to: + /// **'My topics'** + String get studyMyTopics; + + /// No description provided for @studyPopularTopics. + /// + /// In en, this message translates to: + /// **'Popular topics'** + String get studyPopularTopics; + + /// No description provided for @studyManageTopics. + /// + /// In en, this message translates to: + /// **'Manage topics'** + String get studyManageTopics; + + /// No description provided for @studyBack. + /// + /// In en, this message translates to: + /// **'Back'** + String get studyBack; + + /// No description provided for @studyPlayAgain. + /// + /// In en, this message translates to: + /// **'Play again'** + String get studyPlayAgain; + + /// No description provided for @studyWhatWouldYouPlay. + /// + /// In en, this message translates to: + /// **'What would you play in this position?'** + String get studyWhatWouldYouPlay; + + /// No description provided for @studyYouCompletedThisLesson. + /// + /// In en, this message translates to: + /// **'Congratulations! You completed this lesson.'** + String get studyYouCompletedThisLesson; + + /// No description provided for @studyNbChapters. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{{count} Chapter} other{{count} Chapters}}'** + String studyNbChapters(int count); + + /// No description provided for @studyNbGames. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{{count} Game} other{{count} Games}}'** + String studyNbGames(int count); + + /// No description provided for @studyNbMembers. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{{count} Member} other{{count} Members}}'** + String studyNbMembers(int count); + + /// No description provided for @studyPasteYourPgnTextHereUpToNbGames. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{Paste your PGN text here, up to {count} game} other{Paste your PGN text here, up to {count} games}}'** + String studyPasteYourPgnTextHereUpToNbGames(int count); } class _AppLocalizationsDelegate extends LocalizationsDelegate { diff --git a/lib/l10n/l10n_af.dart b/lib/l10n/l10n_af.dart index 4e5ee67e73..2a5384239d 100644 --- a/lib/l10n/l10n_af.dart +++ b/lib/l10n/l10n_af.dart @@ -103,9 +103,6 @@ class AppLocalizationsAf extends AppLocalizations { @override String get mobileCancelTakebackOffer => 'Cancel takeback offer'; - @override - String get mobileCancelDrawOffer => 'Cancel draw offer'; - @override String get mobileWaitingForOpponentToJoin => 'Wag vir opponent om aan te sluit...'; @@ -142,7 +139,7 @@ class AppLocalizationsAf extends AppLocalizations { String get mobileGreetingWithoutName => 'Hallo'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobilePrefMagnifyDraggedPiece => 'Vergroot gesleepte stuk'; @override String get activityActivity => 'Aktiwiteite'; @@ -246,6 +243,17 @@ class AppLocalizationsAf extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Completed $count $param2 correspondence games', + one: 'Completed $count $param2 correspondence game', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,226 @@ class AppLocalizationsAf extends AppLocalizations { @override String get broadcastBroadcasts => 'Uitsendings'; + @override + String get broadcastMyBroadcasts => 'My uitsendings'; + @override String get broadcastLiveBroadcasts => 'Regstreekse toernooi uitsendings'; + @override + String get broadcastBroadcastCalendar => 'Broadcast calendar'; + + @override + String get broadcastNewBroadcast => 'Nuwe regstreekse uitsendings'; + + @override + String get broadcastSubscribedBroadcasts => 'Subscribed broadcasts'; + + @override + String get broadcastAboutBroadcasts => 'About broadcasts'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'How to use Lichess Broadcasts.'; + + @override + String get broadcastTheNewRoundHelp => 'The new round will have the same members and contributors as the previous one.'; + + @override + String get broadcastAddRound => 'Voeg \'n ronde by'; + + @override + String get broadcastOngoing => 'Deurlopend'; + + @override + String get broadcastUpcoming => 'Opkomend'; + + @override + String get broadcastCompleted => 'Voltooi'; + + @override + String get broadcastCompletedHelp => 'Lichess detects round completion, but can get it wrong. Use this to set it manually.'; + + @override + String get broadcastRoundName => 'Ronde se naam'; + + @override + String get broadcastRoundNumber => 'Ronde getal'; + + @override + String get broadcastTournamentName => 'Toernooi se naam'; + + @override + String get broadcastTournamentDescription => 'Kort beskrywing van die toernooi'; + + @override + String get broadcastFullDescription => 'Volle geleentheid beskrywing'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Opsionele lang beskrywing van die uitsending. $param1 is beskikbaar. Lengte moet minder as $param2 karakters.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN-Bronskakel'; + + @override + String get broadcastSourceUrlHelp => 'URL wat Lichess sal nagaan vir PGN opdaterings. Dit moet openbaar beskikbaar wees vanaf die Internet.'; + + @override + String get broadcastSourceGameIds => 'Up to 64 Lichess game IDs, separated by spaces.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Start date in the tournament local timezone: $param'; + } + + @override + String get broadcastStartDateHelp => 'Optioneel, indien jy weet wanner die geleentheid begin'; + + @override + String get broadcastCurrentGameUrl => 'Huidige spel se bronadres'; + + @override + String get broadcastDownloadAllRounds => 'Laai al die rondes af'; + + @override + String get broadcastResetRound => 'Herstel die ronde'; + + @override + String get broadcastDeleteRound => 'Skrap die ronde'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Skrap die rondte en sy spelle beslis uit.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Skrap alle spelle van hierdie rondte. Die bron sal aktief moet wees om hulle te kan herskep.'; + + @override + String get broadcastEditRoundStudy => 'Edit round study'; + + @override + String get broadcastDeleteTournament => 'Vee hierdie toernooi uit'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Vee beslis die hele toernooi uit, met al sy rondtes en spelle.'; + + @override + String get broadcastShowScores => 'Show players scores based on game results'; + + @override + String get broadcastReplacePlayerTags => 'Opsioneel: vervang spelername, graderings en titels'; + + @override + String get broadcastFideFederations => 'FIDE-federasies'; + + @override + String get broadcastTop10Rating => 'Top 10 gradering'; + + @override + String get broadcastFidePlayers => 'FIDE-deelnemers'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE-deelnemer nie gevind nie'; + + @override + String get broadcastFideProfile => 'FIDE-profiel'; + + @override + String get broadcastFederation => 'Federasie'; + + @override + String get broadcastAgeThisYear => 'Ouderdom vanjaar'; + + @override + String get broadcastUnrated => 'Ongegradeerd'; + + @override + String get broadcastRecentTournaments => 'Onlangse toernooie'; + + @override + String get broadcastOpenLichess => 'Open in Lichess'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Overview'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Official website'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'Score'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count broadcasts', + one: '$count broadcast', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Uitdagings: $param1'; @@ -1390,10 +1615,10 @@ class AppLocalizationsAf extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Die opponent is beperk in die bewegings wat hulle kan maak, en alle bewegings vererger hul posisie.'; @override - String get puzzleThemeHealthyMix => 'Gesonde mengsel'; + String get puzzleThemeMix => 'Gesonde mengsel'; @override - String get puzzleThemeHealthyMixDescription => '\'N Bietjie van alles. Jy weet nie wat om te verwag nie, dus bly jy gereed vir enigiets! Net soos in regte speletjies.'; + String get puzzleThemeMixDescription => '\'N Bietjie van alles. Jy weet nie wat om te verwag nie, dus bly jy gereed vir enigiets! Net soos in regte speletjies.'; @override String get puzzleThemePlayerGames => 'Speler se spelle'; @@ -1797,9 +2022,6 @@ class AppLocalizationsAf extends AppLocalizations { @override String get removesTheDepthLimit => 'Verwyder dieptelimiet, en hou jou rekenaar warm'; - @override - String get engineManager => 'Enjinbestuurder'; - @override String get blunder => 'Flater'; @@ -2063,6 +2285,9 @@ class AppLocalizationsAf extends AppLocalizations { @override String get gamesPlayed => 'Spelle gespeel'; + @override + String get ok => 'OK'; + @override String get cancel => 'Kanseleer'; @@ -2121,7 +2346,7 @@ class AppLocalizationsAf extends AppLocalizations { String get standard => 'Standaard'; @override - String get customPosition => 'Custom position'; + String get customPosition => 'Gebruiklike Posisie'; @override String get unlimited => 'Oneindig'; @@ -2404,7 +2629,7 @@ class AppLocalizationsAf extends AppLocalizations { String get reconnecting => 'Konnekteer weer'; @override - String get noNetwork => 'Offline'; + String get noNetwork => 'Vanlyn af'; @override String get favoriteOpponents => 'Gunsteling opponente'; @@ -2631,13 +2856,13 @@ class AppLocalizationsAf extends AppLocalizations { String get editProfile => 'Verander profiel'; @override - String get realName => 'Real name'; + String get realName => 'Regte naam'; @override - String get setFlair => 'Set your flair'; + String get setFlair => 'Stel jou Vlam'; @override - String get flair => 'Flair'; + String get flair => 'Vlam'; @override String get youCanHideFlair => 'There is a setting to hide all user flairs across the entire site.'; @@ -2772,7 +2997,13 @@ class AppLocalizationsAf extends AppLocalizations { String get other => 'Ander'; @override - String get reportDescriptionHelp => 'Plak skakel na die spel(le) en verduidelik wat skort met die lid se gedrag. Moenie net sê hulle kroek nie, maar verduidelik hoe daardie gevolgtrekking bereik is. Jou verslag sal vinniger geantwoord word as dit in Engels geskryf is.'; + String get reportCheatBoostHelp => 'Paste the link to the game(s) and explain what is wrong about this user\'s behaviour. Don\'t just say \"they cheat\", but tell us how you came to this conclusion.'; + + @override + String get reportUsernameHelp => 'Explain what about this username is offensive. Don\'t just say \"it\'s offensive/inappropriate\", but tell us how you came to this conclusion, especially if the insult is obfuscated, not in english, is in slang, or is a historical/cultural reference.'; + + @override + String get reportProcessedFasterInEnglish => 'Your report will be processed faster if written in English.'; @override String get error_provideOneCheatedGameLink => 'Verskaf asseblief ten minste een skakel na \'n spel waar hulle gekroek het.'; @@ -4077,6 +4308,9 @@ class AppLocalizationsAf extends AppLocalizations { @override String get nothingToSeeHere => 'Nothing to see here at the moment.'; + @override + String get stats => 'Stats'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4335,8 +4569,8 @@ class AppLocalizationsAf extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count simuls', - one: '$count simul', + other: '$count simulasies', + one: '$count simulasie', ); return '$_temp0'; } @@ -4723,9 +4957,514 @@ class AppLocalizationsAf extends AppLocalizations { @override String get streamerLichessStreamers => 'Lichess aanbieders'; + @override + String get studyPrivate => 'Privaat'; + + @override + String get studyMyStudies => 'My studies'; + + @override + String get studyStudiesIContributeTo => 'Studies waartoe ek bydra'; + + @override + String get studyMyPublicStudies => 'My publieke studies'; + + @override + String get studyMyPrivateStudies => 'My privaat studies'; + + @override + String get studyMyFavoriteStudies => 'My gunsteling studies'; + + @override + String get studyWhatAreStudies => 'Wat is studies?'; + + @override + String get studyAllStudies => 'Alle studies'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Studies gemaak deur $param'; + } + + @override + String get studyNoneYet => 'Nog geen.'; + + @override + String get studyHot => 'Gewild'; + + @override + String get studyDateAddedNewest => 'Datum bygevoeg (nuutste)'; + + @override + String get studyDateAddedOldest => 'Datum bygevoeg (oudste)'; + + @override + String get studyRecentlyUpdated => 'Onlangs opgedateer'; + + @override + String get studyMostPopular => 'Mees gewilde'; + + @override + String get studyAlphabetical => 'Alfabeties'; + + @override + String get studyAddNewChapter => 'Voeg \'n nuwe hoofstuk by'; + + @override + String get studyAddMembers => 'Voeg iemand by'; + + @override + String get studyInviteToTheStudy => 'Nooi uit om deel te wees van die studie'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Nooi asseblief net mense uit wat jy ken of wat aktief wil deelneem aan die studie.'; + + @override + String get studySearchByUsername => 'Soek vir gebruikersnaam'; + + @override + String get studySpectator => 'Toeskouer'; + + @override + String get studyContributor => 'Bydraer'; + + @override + String get studyKick => 'Verwyder'; + + @override + String get studyLeaveTheStudy => 'Verlaat die studie'; + + @override + String get studyYouAreNowAContributor => 'Jy is nou \'n bydraer'; + + @override + String get studyYouAreNowASpectator => 'Jy is nou \'n toeskouer'; + + @override + String get studyPgnTags => 'PGN etikette'; + + @override + String get studyLike => 'Hou van'; + + @override + String get studyUnlike => 'Afkeur'; + + @override + String get studyNewTag => 'Nuwe etiket'; + + @override + String get studyCommentThisPosition => 'Lewer kommentaar op hierdie posisie'; + + @override + String get studyCommentThisMove => 'Lewer kommentaar op hierdie skuif'; + + @override + String get studyAnnotateWithGlyphs => 'Annoteer met karakters'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Die hoofstuk is te kort om geanaliseer te word.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Slegs die studie bydraers kan versoek om \'n rekenaar analise te doen.'; + + @override + String get studyGetAFullComputerAnalysis => 'Kry \'n vol-bediener rekenaar analise van die hooflyn.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Maak seker dat die hoofstuk volledig is. Jy kan slegs eenkeer \'n analise versoek.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Alle SYNC lede bly op dieselfde posisie'; + + @override + String get studyShareChanges => 'Deel veranderinge met toeskouers en stoor dit op die bediener'; + + @override + String get studyPlaying => 'Besig om te speel'; + + @override + String get studyShowEvalBar => 'Evaluation bars'; + + @override + String get studyFirst => 'Eerste'; + + @override + String get studyPrevious => 'Vorige'; + + @override + String get studyNext => 'Volgende'; + + @override + String get studyLast => 'Laaste'; + @override String get studyShareAndExport => 'Deel & voer uit'; + @override + String get studyCloneStudy => 'Kloneer'; + + @override + String get studyStudyPgn => 'Studie PGN'; + + @override + String get studyDownloadAllGames => 'Laai alle speletjies af'; + + @override + String get studyChapterPgn => 'Hoofstuk PGN'; + + @override + String get studyCopyChapterPgn => 'Kopieer PGN'; + + @override + String get studyDownloadGame => 'Aflaai spel'; + + @override + String get studyStudyUrl => 'Bestudeer URL'; + + @override + String get studyCurrentChapterUrl => 'Huidige hoofstuk URL'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'U kan dit in die forum plak om in te bed'; + + @override + String get studyStartAtInitialPosition => 'Begin by die oorspronklike posisie'; + + @override + String studyStartAtX(String param) { + return 'Begin by $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Bed in u webwerf of blog'; + + @override + String get studyReadMoreAboutEmbedding => 'Lees meer oor inbedding'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Slegs openbare studies kan ingebed word!'; + + @override + String get studyOpen => 'Maak oop'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1, aan jou beskikbaar gestel deur $param2'; + } + + @override + String get studyStudyNotFound => 'Studie kon nie gevind word nie'; + + @override + String get studyEditChapter => 'Verander die hoofstuk'; + + @override + String get studyNewChapter => 'Nuwe hoofstuk'; + + @override + String studyImportFromChapterX(String param) { + return 'Voer in vanaf $param'; + } + + @override + String get studyOrientation => 'Oriëntasie'; + + @override + String get studyAnalysisMode => 'Analiseer mode'; + + @override + String get studyPinnedChapterComment => 'Vasgepende hoofstuk kommentaar'; + + @override + String get studySaveChapter => 'Stoor hoofstuk'; + + @override + String get studyClearAnnotations => 'Vee annotasies uit'; + + @override + String get studyClearVariations => 'Verwyder variasies'; + + @override + String get studyDeleteChapter => 'Vee hoofstuk uit'; + + @override + String get studyDeleteThisChapter => 'Vee die hoofstuk uit? Jy gaan dit nie kan terugvat nie!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Vee al die kommentaar, karakters en getekende vorms in die hoofstuk uit?'; + + @override + String get studyRightUnderTheBoard => 'Reg onder die bord'; + + @override + String get studyNoPinnedComment => 'Geen'; + + @override + String get studyNormalAnalysis => 'Normale analise'; + + @override + String get studyHideNextMoves => 'Versteek die volgende skuiwe'; + + @override + String get studyInteractiveLesson => 'Interaktiewe les'; + + @override + String studyChapterX(String param) { + return 'Hoofstuk $param'; + } + + @override + String get studyEmpty => 'Leeg'; + + @override + String get studyStartFromInitialPosition => 'Begin vanaf oorspronklike posisie'; + + @override + String get studyEditor => 'Redakteur'; + + @override + String get studyStartFromCustomPosition => 'Begin vanaf eie posisie'; + + @override + String get studyLoadAGameByUrl => 'Laai \'n wedstryd op deur die URL'; + + @override + String get studyLoadAPositionFromFen => 'Laai posisie vanaf FEN'; + + @override + String get studyLoadAGameFromPgn => 'Laai wedstryd vanaf PGN'; + + @override + String get studyAutomatic => 'Outomaties'; + + @override + String get studyUrlOfTheGame => 'URL van die wedstryd'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Laai \'n wedstryd van $param1 of $param2'; + } + + @override + String get studyCreateChapter => 'Skep \'n hoofstuk'; + + @override + String get studyCreateStudy => 'Skep \'n studie'; + + @override + String get studyEditStudy => 'Verander studie'; + + @override + String get studyVisibility => 'Sigbaarheid'; + + @override + String get studyPublic => 'Publiek'; + + @override + String get studyUnlisted => 'Ongelys'; + + @override + String get studyInviteOnly => 'Slegs op uitnodiging'; + + @override + String get studyAllowCloning => 'Laat kloning toe'; + + @override + String get studyNobody => 'Niemand'; + + @override + String get studyOnlyMe => 'Net ek'; + + @override + String get studyContributors => 'Bydraers'; + + @override + String get studyMembers => 'Lede'; + + @override + String get studyEveryone => 'Almal'; + + @override + String get studyEnableSync => 'Maak sync beskikbaar'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Ja: hou almal op dieselfde posisie'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Nee: laat mense toe om vrylik deur te gaan'; + + @override + String get studyPinnedStudyComment => 'Vasgepende studie opmerking'; + @override String get studyStart => 'Begin'; + + @override + String get studySave => 'Stoor'; + + @override + String get studyClearChat => 'Maak die gesprek skoon'; + + @override + String get studyDeleteTheStudyChatHistory => 'Vee die gesprek uit? Onthou, jy kan dit nie terug kry nie!'; + + @override + String get studyDeleteStudy => 'Vee die studie uit'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Skrap die hele studie? Daar is geen terugkeer nie! Tik die naam van die studie om te bevesting: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Waar wil jy dit bestudeer?'; + + @override + String get studyGoodMove => 'Goeie skuif'; + + @override + String get studyMistake => 'Fout'; + + @override + String get studyBrilliantMove => 'Skitterende skuif'; + + @override + String get studyBlunder => 'Flater'; + + @override + String get studyInterestingMove => 'Interesante skuif'; + + @override + String get studyDubiousMove => 'Twyfelagte skuif'; + + @override + String get studyOnlyMove => 'Eenigste skuif'; + + @override + String get studyZugzwang => 'Zugzwang'; + + @override + String get studyEqualPosition => 'Gelyke posisie'; + + @override + String get studyUnclearPosition => 'Onduidelike posise'; + + @override + String get studyWhiteIsSlightlyBetter => 'Wit is effens beter'; + + @override + String get studyBlackIsSlightlyBetter => 'Swart is effens beter'; + + @override + String get studyWhiteIsBetter => 'Wit is beter'; + + @override + String get studyBlackIsBetter => 'Swart is beter'; + + @override + String get studyWhiteIsWinning => 'Wit is beter'; + + @override + String get studyBlackIsWinning => 'Swart is beter'; + + @override + String get studyNovelty => 'Nuwigheid'; + + @override + String get studyDevelopment => 'Ontwikkeling'; + + @override + String get studyInitiative => 'Inisiatief'; + + @override + String get studyAttack => 'Aanval'; + + @override + String get studyCounterplay => 'Teenstoot'; + + @override + String get studyTimeTrouble => 'Tydskommer'; + + @override + String get studyWithCompensation => 'Met vergoeding'; + + @override + String get studyWithTheIdea => 'Met die idee'; + + @override + String get studyNextChapter => 'Volgende hoofstuk'; + + @override + String get studyPrevChapter => 'Vorige hoofstuk'; + + @override + String get studyStudyActions => 'Studie aksie'; + + @override + String get studyTopics => 'Onderwerpe'; + + @override + String get studyMyTopics => 'My onderwerpe'; + + @override + String get studyPopularTopics => 'Gewilde onderwerpe'; + + @override + String get studyManageTopics => 'Bestuur onderwerpe'; + + @override + String get studyBack => 'Terug'; + + @override + String get studyPlayAgain => 'Speel weer'; + + @override + String get studyWhatWouldYouPlay => 'Wat sal jy in hierdie posisie speel?'; + + @override + String get studyYouCompletedThisLesson => 'Geluk! Jy het hierdie les voltooi.'; + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Hoofstukke', + one: '$count Hoofstuk', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Wedstryde', + one: '$count Wedstryd', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Lede', + one: '$count Lid', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Plak jou PGN teks hier, tot by $count spelle', + one: 'Plak jou PGN teks hier, tot by $count spel', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_ar.dart b/lib/l10n/l10n_ar.dart index 472b3a4125..54bb0da1f5 100644 --- a/lib/l10n/l10n_ar.dart +++ b/lib/l10n/l10n_ar.dart @@ -24,7 +24,7 @@ class AppLocalizationsAr extends AppLocalizations { String get mobileSettingsTab => 'الإعدادات'; @override - String get mobileMustBeLoggedIn => 'لعرض هذه الصفحة، قم بتسجيل الدخول.'; + String get mobileMustBeLoggedIn => 'سجل الدخول لعرض هذه الصفحة.'; @override String get mobileSystemColors => 'ألوان النظام'; @@ -65,7 +65,7 @@ class AppLocalizationsAr extends AppLocalizations { String get mobileNoSearchResults => 'لا توجد نتائج'; @override - String get mobileAreYouSure => 'هل أنت متأكد؟'; + String get mobileAreYouSure => 'هل أنت واثق؟'; @override String get mobilePuzzleStreakAbortWarning => 'سوف تفقد تسلقك الحالي وسيتم حفظ نتيجتك.'; @@ -98,25 +98,22 @@ class AppLocalizationsAr extends AppLocalizations { String get mobilePuzzleStormConfirmEndRun => 'هل تريد إنهاء هذا التشغيل؟'; @override - String get mobilePuzzleStormFilterNothingToShow => 'لا شيء لإظهاره، الرجاء تغيير الفلاتر'; + String get mobilePuzzleStormFilterNothingToShow => 'لا شيء لإظهاره، الرجاء تغيير المرشح'; @override String get mobileCancelTakebackOffer => 'إلغاء عرض الاسترداد'; - @override - String get mobileCancelDrawOffer => 'إلغاء عرض التعادل'; - @override String get mobileWaitingForOpponentToJoin => 'في انتظار انضمام الطرف الآخر...'; @override - String get mobileBlindfoldMode => 'عصب العينين'; + String get mobileBlindfoldMode => 'معصوب العينين'; @override - String get mobileLiveStreamers => 'البثوث المباشرة'; + String get mobileLiveStreamers => 'البث المباشر'; @override - String get mobileCustomGameJoinAGame => 'الانضمام إلى لعبة'; + String get mobileCustomGameJoinAGame => 'الانضمام إلى لُعْبَة'; @override String get mobileCorrespondenceClearSavedMove => 'مسح النقل المحفوظ'; @@ -125,24 +122,24 @@ class AppLocalizationsAr extends AppLocalizations { String get mobileSomethingWentWrong => 'لقد حدث خطأ ما.'; @override - String get mobileShowResult => 'Show result'; + String get mobileShowResult => 'إظهار النتيجة'; @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobilePuzzleThemesSubtitle => 'حُل الألغاز المتعلّقة بافتتاحاتك المفضّلة، أو اختر موضوعاً.'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobilePuzzleStormSubtitle => 'حل أكبر عدد ممكن من الألغاز في 3 دقائق.'; @override String mobileGreeting(String param) { - return 'Hello, $param'; + return 'مرحبا، $param'; } @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileGreetingWithoutName => 'مرحبا'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobilePrefMagnifyDraggedPiece => 'تكبير القطعة المسحوبة'; @override String get activityActivity => 'الأنشطة'; @@ -278,6 +275,17 @@ class AppLocalizationsAr extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Completed $count $param2 correspondence games', + one: 'Completed $count $param2 correspondence game', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -416,9 +424,230 @@ class AppLocalizationsAr extends AppLocalizations { @override String get broadcastBroadcasts => 'البثوث'; + @override + String get broadcastMyBroadcasts => 'بثي'; + @override String get broadcastLiveBroadcasts => 'بث البطولة المباشرة'; + @override + String get broadcastBroadcastCalendar => 'تقويم البث'; + + @override + String get broadcastNewBroadcast => 'بث مباشر جديد'; + + @override + String get broadcastSubscribedBroadcasts => 'البث المُشترك به'; + + @override + String get broadcastAboutBroadcasts => 'حول البثوث'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'كيفية استخدام بث ليتشيس.'; + + @override + String get broadcastTheNewRoundHelp => 'ستضم الجولة الجديدة الأعضاء والمساهمين عينهم الذين اشتركوا في الجولة السابق.'; + + @override + String get broadcastAddRound => 'إضافة جولة'; + + @override + String get broadcastOngoing => 'الجارية'; + + @override + String get broadcastUpcoming => 'القادمة'; + + @override + String get broadcastCompleted => 'المكتملة'; + + @override + String get broadcastCompletedHelp => 'يعرف ليتشيس بانتهاء الجولة استناداً إلى المصدر، استخدم هذا التبديل إذا لم يكن هناك مصدر.'; + + @override + String get broadcastRoundName => 'اسم الجولة'; + + @override + String get broadcastRoundNumber => 'رقم الجولة (الشوط)'; + + @override + String get broadcastTournamentName => 'اسم البطولة'; + + @override + String get broadcastTournamentDescription => 'وصف موجز للبطولة'; + + @override + String get broadcastFullDescription => 'الوصف الكامل'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'الوصف الاختياري الطويل للبث. $param1 متوفر. يجب أن لا يتجاوز طول النص $param2 حرفاً.'; + } + + @override + String get broadcastSourceSingleUrl => 'رابط مصدر PGN'; + + @override + String get broadcastSourceUrlHelp => 'URL الذي سيتحقق منه Lichess للحصول على تحديثات PGN. يجب أن يكون متاحًا للجميع على الإنترنت.'; + + @override + String get broadcastSourceGameIds => 'حتى 64 معرف لُعْبَة ليتشيس، مفصولة بمسافات.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'موعد البداية بتوقيت البطولة المحلي: $param'; + } + + @override + String get broadcastStartDateHelp => 'اختياري، إذا كنت تعرف متى يبدأ الحدث'; + + @override + String get broadcastCurrentGameUrl => 'رابط المباراة الحالية'; + + @override + String get broadcastDownloadAllRounds => 'تحميل جميع المباريات'; + + @override + String get broadcastResetRound => 'إعادة ضبط هذه الجولة'; + + @override + String get broadcastDeleteRound => 'حذف هذه الجولة'; + + @override + String get broadcastDefinitivelyDeleteRound => 'قم بحذف الجولة وألعابها نهائيا.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'احذف جميع ألعاب هذه الجولة. سوف يحتاج المصدر إلى أن يكون نشطا من أجل إعادة إنشائها.'; + + @override + String get broadcastEditRoundStudy => 'تعديل دراسة الجولة'; + + @override + String get broadcastDeleteTournament => 'حذف هذه المسابقة'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'قم بحذف البطولة جميعها و جميع جولاتها و جميع ألعابها.'; + + @override + String get broadcastShowScores => 'اظهر نقاط اللاعبين بناءً على نتائج اللعبة'; + + @override + String get broadcastReplacePlayerTags => 'اختياري: استبدل أسماء اللاعبين وتقييماتهم وألقابهم'; + + @override + String get broadcastFideFederations => 'الاتحاد الدولي للشطرنج'; + + @override + String get broadcastTop10Rating => 'تقييم أعلى 10'; + + @override + String get broadcastFidePlayers => 'لاعبين FIDE'; + + @override + String get broadcastFidePlayerNotFound => 'لم يتم العثور على لاعب الاتحاد الدولي (FIDE)'; + + @override + String get broadcastFideProfile => 'مِلَفّ FIDE'; + + @override + String get broadcastFederation => 'إتحاد'; + + @override + String get broadcastAgeThisYear => 'العمر هذا العام'; + + @override + String get broadcastUnrated => 'غير مقيم'; + + @override + String get broadcastRecentTournaments => 'البطولات الأخيرة'; + + @override + String get broadcastOpenLichess => 'Open in Lichess'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Overview'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Official website'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'Score'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count بثوث', + many: '$count بثوث', + few: '$count بثوث', + two: 'بثين', + one: '$count بث', + zero: '$count بث', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'التحديات: $param1'; @@ -1478,10 +1707,10 @@ class AppLocalizationsAr extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'حركات الخصم محدودة و كل الحركات تؤدي إلى تفاقم الوضع نحو الأسوء.'; @override - String get puzzleThemeHealthyMix => 'خليط'; + String get puzzleThemeMix => 'خليط'; @override - String get puzzleThemeHealthyMixDescription => 'القليل من كل نوع، لذا لا يمكنك التنبؤ باللغز القادم فابقى مستعداً لأي شيء، تماماً كالمباريات الحقيقية.'; + String get puzzleThemeMixDescription => 'القليل من كل نوع، لذا لا يمكنك التنبؤ باللغز القادم فابقى مستعداً لأي شيء، تماماً كالمباريات الحقيقية.'; @override String get puzzleThemePlayerGames => 'مبارايات اللاعب'; @@ -1885,9 +2114,6 @@ class AppLocalizationsAr extends AppLocalizations { @override String get removesTheDepthLimit => 'التحليل لأبعد عمق، وابقاء حاسوبك نشطًا'; - @override - String get engineManager => 'مدير المحركات'; - @override String get blunder => 'خطأ فادح'; @@ -2151,6 +2377,9 @@ class AppLocalizationsAr extends AppLocalizations { @override String get gamesPlayed => 'المباريات الملعوبة'; + @override + String get ok => 'OK'; + @override String get cancel => 'إلغاء'; @@ -2860,7 +3089,13 @@ class AppLocalizationsAr extends AppLocalizations { String get other => 'أخرى'; @override - String get reportDescriptionHelp => 'الصق رابط المباراة (المباريات) واشرح بالتفصيل المشكلة في تصرف هذا المستحدم. لا تقل فقط \"انهم يغشون\"، ولكن اشرح لنا سبب استنتاجك. سيكون الرد أسرع إن كتبت بالإنكليزية.'; + String get reportCheatBoostHelp => 'هذه رسالة عامية وليست مخصصة لبلاغات الغش. وهي تحاول تعليم اللاعب كيفية كتابة بلاغ مفيد لفريق لي-تشيس. و أيضا تطلب إثبات.\n\nتظهر على صفحة \"بلغ مستخدم\"\nhttps://lichess. org/report.'; + + @override + String get reportUsernameHelp => 'اشرح ما المسيء في اسم المستخدم هذا. لا تقل فقط \"إنه مسيء/غير مناسب\"، بل أخبرنا كيف توصلت إلى هذا الاستنتاج، خاصة إذا كانت الإهانة غير واضحة، أو ليست باللغة الإنجليزية، أو كانت باللغة العامية، أو كانت إشارة تاريخية/ثقافية.'; + + @override + String get reportProcessedFasterInEnglish => 'سيتم معالجة بلاغك بشكل أسرع إذا تمت كتابته باللغة الإنجليزية.'; @override String get error_provideOneCheatedGameLink => 'برجاء تقديم رابط واحد علي الأقل لمباراة حدث فيها غش.'; @@ -4165,6 +4400,9 @@ class AppLocalizationsAr extends AppLocalizations { @override String get nothingToSeeHere => 'لا يوجد شيء يمكن رؤيته هنا في الوقت الحالي.'; + @override + String get stats => 'Stats'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4987,9 +5225,530 @@ class AppLocalizationsAr extends AppLocalizations { @override String get streamerLichessStreamers => 'بثوث ليشس'; + @override + String get studyPrivate => 'خاص'; + + @override + String get studyMyStudies => 'دراستي'; + + @override + String get studyStudiesIContributeTo => 'الدراسات المساهم بها'; + + @override + String get studyMyPublicStudies => 'دراسات العامة'; + + @override + String get studyMyPrivateStudies => 'دراساتي الخاصة'; + + @override + String get studyMyFavoriteStudies => 'دراساتي المفضلة'; + + @override + String get studyWhatAreStudies => 'ما هي الدراسات؟'; + + @override + String get studyAllStudies => 'كل الدراسات'; + + @override + String studyStudiesCreatedByX(String param) { + return 'الدراسات التي أنشئها $param'; + } + + @override + String get studyNoneYet => 'لا يوجد.'; + + @override + String get studyHot => 'ذات شعبية'; + + @override + String get studyDateAddedNewest => 'تاريخ الإضافة (الأحدث)'; + + @override + String get studyDateAddedOldest => 'تاريخ الإضافة (الأقدم)'; + + @override + String get studyRecentlyUpdated => 'تم تحديثه مؤخرا'; + + @override + String get studyMostPopular => 'الاكثر شعبية'; + + @override + String get studyAlphabetical => 'أبجدي'; + + @override + String get studyAddNewChapter => 'أضف فصلاً جديدا'; + + @override + String get studyAddMembers => 'إضافة أعضاء'; + + @override + String get studyInviteToTheStudy => 'دعوة الى دراسة'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'يرجى فقط إضافة اشخاص تعرفهم، ويريدون المشاركة في هذه الدراسة'; + + @override + String get studySearchByUsername => 'البحث بواسطة اسم المستخدم'; + + @override + String get studySpectator => 'مشاهد'; + + @override + String get studyContributor => 'مساهم'; + + @override + String get studyKick => 'طرد'; + + @override + String get studyLeaveTheStudy => 'مغادرة الدراسة'; + + @override + String get studyYouAreNowAContributor => 'انت الان اصبحت مساهم'; + + @override + String get studyYouAreNowASpectator => 'انت الان اصبحت مشاهد'; + + @override + String get studyPgnTags => 'وسم PGN'; + + @override + String get studyLike => 'إعجاب'; + + @override + String get studyUnlike => 'إلغاء الإعجاب'; + + @override + String get studyNewTag => 'علامة جديدة'; + + @override + String get studyCommentThisPosition => 'التعليق على هذا الوضع'; + + @override + String get studyCommentThisMove => 'التعليق على هذه النقلة'; + + @override + String get studyAnnotateWithGlyphs => 'التعليق مع الحروف الرسومية'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'الفصل جداً قصير لكي يتم تحليله'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'فقط المساهمون في هذا الدراسة يمكنهم طلب تحليل الحاسوب'; + + @override + String get studyGetAFullComputerAnalysis => 'احصل على تحليل حاسوب كامل للتفريع الرئيسي من قبل الخادم'; + + @override + String get studyMakeSureTheChapterIsComplete => 'كن متأكداً ان الفصل مكتمل، يمكنك طلب تحليل الحاسوب مره واحده فحسب'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'يظل جميع ألاعضاء الذين تمت مزامنة معلوماتهم في نفس الترتيب'; + + @override + String get studyShareChanges => 'شارك التغيبرات مع المشاهدين وإحفظهن الى الخادم'; + + @override + String get studyPlaying => 'يلعب الان'; + + @override + String get studyShowEvalBar => 'شرائط التقييم'; + + @override + String get studyFirst => 'الأولى'; + + @override + String get studyPrevious => 'السابق'; + + @override + String get studyNext => 'التالي'; + + @override + String get studyLast => 'الأخير'; + @override String get studyShareAndExport => 'مشاركة و تصدير'; + @override + String get studyCloneStudy => 'استنساخ'; + + @override + String get studyStudyPgn => 'PGN الدراسة'; + + @override + String get studyDownloadAllGames => 'حمل جميع الألعاب'; + + @override + String get studyChapterPgn => 'PGN الفصل'; + + @override + String get studyCopyChapterPgn => 'نسخ PGN'; + + @override + String get studyDownloadGame => 'حمل لعبة'; + + @override + String get studyStudyUrl => 'رابط الدراسة'; + + @override + String get studyCurrentChapterUrl => 'رابط الفصل الحالي'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'يمكنك لصق هذا في المنتدى لتضمينه'; + + @override + String get studyStartAtInitialPosition => 'البدء من وضع البداية'; + + @override + String studyStartAtX(String param) { + return 'البدء من $param'; + } + + @override + String get studyEmbedInYourWebsite => 'ضمنه في موقع أو مدونة'; + + @override + String get studyReadMoreAboutEmbedding => 'راجع المزيد عن التضمين'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'يمكن تضمين الدراسات العامة فقط!'; + + @override + String get studyOpen => 'فتح'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1 مقدمة من $param2'; + } + + @override + String get studyStudyNotFound => 'لم يتم العثور على الدراسة'; + + @override + String get studyEditChapter => 'تحرير الفصل'; + + @override + String get studyNewChapter => 'فصل جديد'; + + @override + String studyImportFromChapterX(String param) { + return 'استيراد من $param'; + } + + @override + String get studyOrientation => 'اتجاه الرقعة'; + + @override + String get studyAnalysisMode => 'وضع التحليل'; + + @override + String get studyPinnedChapterComment => 'التعليق المثبت على الفصل'; + + @override + String get studySaveChapter => 'حفظ الفصل'; + + @override + String get studyClearAnnotations => 'مسح العلامات'; + + @override + String get studyClearVariations => 'مسح اللاينات'; + + @override + String get studyDeleteChapter => 'حذف الفصل'; + + @override + String get studyDeleteThisChapter => 'هل تريد حذف الفصل ؟ لايمكنك التراجع عن ذلك لاحقاً!'; + + @override + String get studyClearAllCommentsInThisChapter => 'مسح جميع التعليقات والغلافات والأشكال المرسومة في هذا الفصل؟'; + + @override + String get studyRightUnderTheBoard => 'تحت الرقعة مباشرة'; + + @override + String get studyNoPinnedComment => 'بدون'; + + @override + String get studyNormalAnalysis => 'تحليل عادي'; + + @override + String get studyHideNextMoves => 'أخفي النقلة التالية'; + + @override + String get studyInteractiveLesson => 'درس تفاعلي'; + + @override + String studyChapterX(String param) { + return 'الفصل $param'; + } + + @override + String get studyEmpty => 'فارغ'; + + @override + String get studyStartFromInitialPosition => 'البدء من وضعية البداية'; + + @override + String get studyEditor => 'المحرر'; + + @override + String get studyStartFromCustomPosition => 'البدء من وضع مخصص'; + + @override + String get studyLoadAGameByUrl => 'تحميل لعبة من رابط'; + + @override + String get studyLoadAPositionFromFen => 'تحميل موقف من FEN'; + + @override + String get studyLoadAGameFromPgn => 'استرد لعبة من PGN'; + + @override + String get studyAutomatic => 'تلقائي'; + + @override + String get studyUrlOfTheGame => 'رابط اللعبة'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'استيراد لعبة من $param1 او $param2'; + } + + @override + String get studyCreateChapter => 'أنشئ الفصل'; + + @override + String get studyCreateStudy => 'أنشى الدراسة'; + + @override + String get studyEditStudy => 'حرر الدراسة'; + + @override + String get studyVisibility => 'الظهور'; + + @override + String get studyPublic => 'عامة'; + + @override + String get studyUnlisted => 'غير مدرجة'; + + @override + String get studyInviteOnly => 'دعوة فقط'; + + @override + String get studyAllowCloning => 'السماح بالاستنساخ'; + + @override + String get studyNobody => 'لا أحد'; + + @override + String get studyOnlyMe => 'أنا فقط'; + + @override + String get studyContributors => 'المساهمون'; + + @override + String get studyMembers => 'اعضاء'; + + @override + String get studyEveryone => 'الجميع'; + + @override + String get studyEnableSync => 'مكن المزامنة'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'نعم: إبقاء الجميع في نفس الوضعية'; + + @override + String get studyNoLetPeopleBrowseFreely => 'لا: دع الناس يتصفحون بحرية'; + + @override + String get studyPinnedStudyComment => 'تعليق الدراسة المثبتة'; + @override String get studyStart => 'ابدأ'; + + @override + String get studySave => 'حفظ'; + + @override + String get studyClearChat => 'مسح المحادثة'; + + @override + String get studyDeleteTheStudyChatHistory => 'هل تريد حذف سجل الدردشة الدراسية؟ لا يمكن إرجاعها!'; + + @override + String get studyDeleteStudy => 'حذف الدراسة'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'حذف الدراسة بأكملها؟ لا يمكنك التراجع عن هذه الخطوة! اكتب اسم الدراسة لتأكيد عملية الحذف: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'أين تريد دراسة ذلك؟'; + + @override + String get studyGoodMove => 'نقلة جيدة'; + + @override + String get studyMistake => 'خطأ'; + + @override + String get studyBrilliantMove => 'نقلة رائعة'; + + @override + String get studyBlunder => 'غلطة'; + + @override + String get studyInterestingMove => 'نقلة مثيرة للاهتمام'; + + @override + String get studyDubiousMove => 'نقلة مشبوهة'; + + @override + String get studyOnlyMove => 'نقلة وحيدة'; + + @override + String get studyZugzwang => 'Zugzwang'; + + @override + String get studyEqualPosition => 'وضع متساوي'; + + @override + String get studyUnclearPosition => 'وضعية غير واضح'; + + @override + String get studyWhiteIsSlightlyBetter => 'الأبيض أفضل بقليل'; + + @override + String get studyBlackIsSlightlyBetter => 'الأسود أفضل بقليل'; + + @override + String get studyWhiteIsBetter => 'الأبيض أفضل'; + + @override + String get studyBlackIsBetter => 'الأسود أفضل'; + + @override + String get studyWhiteIsWinning => 'الأبيض يفوز'; + + @override + String get studyBlackIsWinning => 'الأسود يفوز'; + + @override + String get studyNovelty => 'جديد'; + + @override + String get studyDevelopment => 'تطوير'; + + @override + String get studyInitiative => 'مبادرة'; + + @override + String get studyAttack => 'هجوم'; + + @override + String get studyCounterplay => 'هجوم مضاد'; + + @override + String get studyTimeTrouble => 'مشكلة وقت'; + + @override + String get studyWithCompensation => 'مع تعويض'; + + @override + String get studyWithTheIdea => 'مع فكرة'; + + @override + String get studyNextChapter => 'الفصل التالي'; + + @override + String get studyPrevChapter => 'الفصل السابق'; + + @override + String get studyStudyActions => 'خيارات الدراسة'; + + @override + String get studyTopics => 'المواضيع'; + + @override + String get studyMyTopics => 'المواضيع الخاصة بي'; + + @override + String get studyPopularTopics => 'المواضيع الشائعة'; + + @override + String get studyManageTopics => 'إدارة المواضيع'; + + @override + String get studyBack => 'رجوع'; + + @override + String get studyPlayAgain => 'اللعب مجددا'; + + @override + String get studyWhatWouldYouPlay => 'ماذا ستلعب في هذا الموقف؟'; + + @override + String get studyYouCompletedThisLesson => 'تهانينا! لقد أكملت هذا الدرس.'; + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count فصول', + many: '$count فصل', + few: '$count فصول', + two: 'فصلان', + one: '$count فصل', + zero: '$count فصل', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count مباراة', + many: '$count مباراة', + few: '$count مبارايات', + two: 'مبارتان', + one: '$count مباراة', + zero: '$count مباراة', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count أعضاء', + many: '$count عضو', + few: '$count عضو', + two: '$count عضو', + one: '$count عضو', + zero: '$count عضو', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'الصق الPGN هنا، حتى $count العاب', + many: 'ألصق نص PGN هنا، حتى $count مباراة', + few: 'ألصق نص PGN هنا، حتى $count مباراة', + two: 'ألصق نص PGN هنا، حتى $count مباراة', + one: 'الصق نص الPGN هنا، حتى $count لعبة واحدة', + zero: 'ألصق نص PGN هنا، حتى $count مباراة', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_az.dart b/lib/l10n/l10n_az.dart index 8ab787334a..6f983e0a0b 100644 --- a/lib/l10n/l10n_az.dart +++ b/lib/l10n/l10n_az.dart @@ -103,9 +103,6 @@ class AppLocalizationsAz extends AppLocalizations { @override String get mobileCancelTakebackOffer => 'Cancel takeback offer'; - @override - String get mobileCancelDrawOffer => 'Cancel draw offer'; - @override String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @@ -246,6 +243,17 @@ class AppLocalizationsAz extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Completed $count $param2 correspondence games', + one: 'Completed $count $param2 correspondence game', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,226 @@ class AppLocalizationsAz extends AppLocalizations { @override String get broadcastBroadcasts => 'Yayım'; + @override + String get broadcastMyBroadcasts => 'My broadcasts'; + @override String get broadcastLiveBroadcasts => 'Canlı turnir yayımları'; + @override + String get broadcastBroadcastCalendar => 'Broadcast calendar'; + + @override + String get broadcastNewBroadcast => 'Yeni canlı yayım'; + + @override + String get broadcastSubscribedBroadcasts => 'Subscribed broadcasts'; + + @override + String get broadcastAboutBroadcasts => 'About broadcasts'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'How to use Lichess Broadcasts.'; + + @override + String get broadcastTheNewRoundHelp => 'The new round will have the same members and contributors as the previous one.'; + + @override + String get broadcastAddRound => 'Tur əlavə et'; + + @override + String get broadcastOngoing => 'Davam edən'; + + @override + String get broadcastUpcoming => 'Yaxınlaşan'; + + @override + String get broadcastCompleted => 'Tamamlanan'; + + @override + String get broadcastCompletedHelp => 'Lichess detects round completion, but can get it wrong. Use this to set it manually.'; + + @override + String get broadcastRoundName => 'Tur adı'; + + @override + String get broadcastRoundNumber => 'Tur sayı'; + + @override + String get broadcastTournamentName => 'Turnir adı'; + + @override + String get broadcastTournamentDescription => 'Qısa turnir açıqlaması'; + + @override + String get broadcastFullDescription => 'Tədbirin tam açıqlaması'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Tədbirin istəyə bağlı təfsilatlı açıqlaması. $param1 seçimi mövcuddur. Mətnin uzunluğu $param2 simvoldan az olmalıdır.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN Source URL'; + + @override + String get broadcastSourceUrlHelp => 'Lichess, verdiyiniz URL ilə PGN-i yeniləyəcək. Bu internetdə hamı tərəfindən əldə edilə bilən olmalıdır.'; + + @override + String get broadcastSourceGameIds => 'Up to 64 Lichess game IDs, separated by spaces.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Start date in the tournament local timezone: $param'; + } + + @override + String get broadcastStartDateHelp => 'İstəyə bağlı, tədbirin başlama vaxtını bilirsinizsə'; + + @override + String get broadcastCurrentGameUrl => 'Hazırkı oyun URL-i'; + + @override + String get broadcastDownloadAllRounds => 'Download all rounds'; + + @override + String get broadcastResetRound => 'Bu turu sıfırla'; + + @override + String get broadcastDeleteRound => 'Bu turu sil'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Definitively delete the round and all its games.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Delete all games of this round. The source will need to be active in order to re-create them.'; + + @override + String get broadcastEditRoundStudy => 'Edit round study'; + + @override + String get broadcastDeleteTournament => 'Delete this tournament'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Definitively delete the entire tournament, all its rounds and all its games.'; + + @override + String get broadcastShowScores => 'Show players scores based on game results'; + + @override + String get broadcastReplacePlayerTags => 'Optional: replace player names, ratings and titles'; + + @override + String get broadcastFideFederations => 'FIDE federations'; + + @override + String get broadcastTop10Rating => 'Top 10 rating'; + + @override + String get broadcastFidePlayers => 'FIDE players'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE player not found'; + + @override + String get broadcastFideProfile => 'FIDE profile'; + + @override + String get broadcastFederation => 'Federation'; + + @override + String get broadcastAgeThisYear => 'Age this year'; + + @override + String get broadcastUnrated => 'Unrated'; + + @override + String get broadcastRecentTournaments => 'Recent tournaments'; + + @override + String get broadcastOpenLichess => 'Open in Lichess'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Overview'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Official website'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'Score'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count broadcasts', + one: '$count broadcast', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Challenges: $param1'; @@ -1388,10 +1613,10 @@ class AppLocalizationsAz extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Rəqibin edə biləcəyi gediş sayı məhduddur və istənilən gediş vəziyyəti daha da pisləşdirir.'; @override - String get puzzleThemeHealthyMix => 'Həftəbecər'; + String get puzzleThemeMix => 'Həftəbecər'; @override - String get puzzleThemeHealthyMixDescription => 'Hər şeydən bir az. Nə gözləyəcəyini bilmirsən, ona görə hər şeyə hazır olursan! Eynilə həqiqi oyunlarda olduğu kimi.'; + String get puzzleThemeMixDescription => 'Hər şeydən bir az. Nə gözləyəcəyini bilmirsən, ona görə hər şeyə hazır olursan! Eynilə həqiqi oyunlarda olduğu kimi.'; @override String get puzzleThemePlayerGames => 'Player games'; @@ -1795,9 +2020,6 @@ class AppLocalizationsAz extends AppLocalizations { @override String get removesTheDepthLimit => 'Dərinlik limitini ləğv edir və kompüterinizi isti saxlayır'; - @override - String get engineManager => 'Mühərrik meneceri'; - @override String get blunder => 'Kobud Səhv'; @@ -2061,6 +2283,9 @@ class AppLocalizationsAz extends AppLocalizations { @override String get gamesPlayed => 'Oynadığı oyunlar'; + @override + String get ok => 'OK'; + @override String get cancel => 'Ləğv et'; @@ -2770,7 +2995,13 @@ class AppLocalizationsAz extends AppLocalizations { String get other => 'Digər'; @override - String get reportDescriptionHelp => 'Oyunun və ya oyunların linkini yapışdırın və bu istifadəçinin davranışında nəyin səhv olduğunu izah edin. Yalnız \"hiylə edirlər\" deməyin, necə bu nəticəyə gəldiyinizi bizə deyin. İngilis dilində yazıldığı təqdirdə hesabat daha sürətli işlənəcəkdir.'; + String get reportCheatBoostHelp => 'Paste the link to the game(s) and explain what is wrong about this user\'s behaviour. Don\'t just say \"they cheat\", but tell us how you came to this conclusion.'; + + @override + String get reportUsernameHelp => 'Explain what about this username is offensive. Don\'t just say \"it\'s offensive/inappropriate\", but tell us how you came to this conclusion, especially if the insult is obfuscated, not in english, is in slang, or is a historical/cultural reference.'; + + @override + String get reportProcessedFasterInEnglish => 'Your report will be processed faster if written in English.'; @override String get error_provideOneCheatedGameLink => 'Lütfən ən azı bir hiyləli oyun linki daxil edin.'; @@ -4075,6 +4306,9 @@ class AppLocalizationsAz extends AppLocalizations { @override String get nothingToSeeHere => 'Nothing to see here at the moment.'; + @override + String get stats => 'Stats'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4721,9 +4955,514 @@ class AppLocalizationsAz extends AppLocalizations { @override String get streamerLichessStreamers => 'Lichess yayımçıları'; + @override + String get studyPrivate => 'Özəl'; + + @override + String get studyMyStudies => 'Çalışmalarım'; + + @override + String get studyStudiesIContributeTo => 'Töhfə verdiyim çalışmalar'; + + @override + String get studyMyPublicStudies => 'Hərkəsə açıq çalışmalarım'; + + @override + String get studyMyPrivateStudies => 'Özəl çalışmalarım'; + + @override + String get studyMyFavoriteStudies => 'Sevimli çalışmalarım'; + + @override + String get studyWhatAreStudies => 'Çalışmalar nədir?'; + + @override + String get studyAllStudies => 'Bütün çalışmalar'; + + @override + String studyStudiesCreatedByX(String param) { + return '$param tərəfindən yaradılan çalışmalar'; + } + + @override + String get studyNoneYet => 'Hələ ki, yoxdur.'; + + @override + String get studyHot => 'Məşhur'; + + @override + String get studyDateAddedNewest => 'Əlavə edilmə tarixi (yenidən köhnəyə)'; + + @override + String get studyDateAddedOldest => 'Əlavə edilmə tarixi (köhnədən yeniyə)'; + + @override + String get studyRecentlyUpdated => 'Ən son yenilənən'; + + @override + String get studyMostPopular => 'Ən məşhur'; + + @override + String get studyAlphabetical => 'Əlifbaya görə'; + + @override + String get studyAddNewChapter => 'Yeni bir fəsil əlavə et'; + + @override + String get studyAddMembers => 'Üzv əlavə et'; + + @override + String get studyInviteToTheStudy => 'Çalışmaya dəvət et'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Zəhmət olmasa yalnız tanıdığınız və bu çalışmaya aktiv olaraq qoşulmaq istəyən insanları dəvət edin.'; + + @override + String get studySearchByUsername => 'İstifadəçi adına görə axtar'; + + @override + String get studySpectator => 'Tamaşaçı'; + + @override + String get studyContributor => 'Töhfə verən'; + + @override + String get studyKick => 'Qov'; + + @override + String get studyLeaveTheStudy => 'Çalışmanı tərk et'; + + @override + String get studyYouAreNowAContributor => 'İndi iştirakçısınız'; + + @override + String get studyYouAreNowASpectator => 'İndi tamaşaçısınız'; + + @override + String get studyPgnTags => 'PGN etiketləri'; + + @override + String get studyLike => 'Bəyən'; + + @override + String get studyUnlike => 'Unlike'; + + @override + String get studyNewTag => 'Yeni etiket'; + + @override + String get studyCommentThisPosition => 'Bu pozisiyaya rəy bildirin'; + + @override + String get studyCommentThisMove => 'Bu gedişə rəy bildirin'; + + @override + String get studyAnnotateWithGlyphs => 'Simvol ilə izah et'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Fəsil təhlil edilməsi üçün çox qısadır.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Yalnız çalışma iştirakçıları kompüter təhlili tələb edə bilər.'; + + @override + String get studyGetAFullComputerAnalysis => 'Ana variant üçün serverdən hərtərəfli kompüter təhlilini alın.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Fəslin tamamlandığına əmin olun. Yalnız bir dəfə təhlil tələbi edə bilərsiniz.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'EYNİLƏŞDİRİLMİŞ bütün üzvlər eyni pozisiyada qalır'; + + @override + String get studyShareChanges => 'Dəyişiklikləri tamaşaçılarla paylaşın və onları serverdə saxlayın'; + + @override + String get studyPlaying => 'Oynanılan'; + + @override + String get studyShowEvalBar => 'Evaluation bars'; + + @override + String get studyFirst => 'İlk'; + + @override + String get studyPrevious => 'Əvvəlki'; + + @override + String get studyNext => 'Növbəti'; + + @override + String get studyLast => 'Son'; + @override String get studyShareAndExport => 'Paylaş və yüklə'; + @override + String get studyCloneStudy => 'Klonla'; + + @override + String get studyStudyPgn => 'Çalışma PGN-i'; + + @override + String get studyDownloadAllGames => 'Bütün oyunları endir'; + + @override + String get studyChapterPgn => 'Fəsil PGN-i'; + + @override + String get studyCopyChapterPgn => 'Copy PGN'; + + @override + String get studyDownloadGame => 'Oyunu endir'; + + @override + String get studyStudyUrl => 'Çalışma URL-i'; + + @override + String get studyCurrentChapterUrl => 'Cari fəsil URL-ii'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Pərçimləmək üçün bunu forumda paylaşa bilərsiniz'; + + @override + String get studyStartAtInitialPosition => 'Başlanğıc pozisiyada başlasın'; + + @override + String studyStartAtX(String param) { + return 'buradan başla: $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Veb sayt və ya bloqunuzda pərçimləyin'; + + @override + String get studyReadMoreAboutEmbedding => 'Pərçimləmə haqqında daha ətraflı'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Yalnız hərkəsə açıq çalışmalar pərçimlənə bilər!'; + + @override + String get studyOpen => 'Aç'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param2 sizə $param1 tərəfindən gətirildi'; + } + + @override + String get studyStudyNotFound => 'Çalışma tapılmadı'; + + @override + String get studyEditChapter => 'Fəslə düzəliş et'; + + @override + String get studyNewChapter => 'Yeni fəsil'; + + @override + String studyImportFromChapterX(String param) { + return 'Import from $param'; + } + + @override + String get studyOrientation => 'İstiqamət'; + + @override + String get studyAnalysisMode => 'Təhlil rejimi'; + + @override + String get studyPinnedChapterComment => 'Sancaqlanmış fəsil rəyləri'; + + @override + String get studySaveChapter => 'Fəsli yadda saxla'; + + @override + String get studyClearAnnotations => 'İzahları təmizlə'; + + @override + String get studyClearVariations => 'Clear variations'; + + @override + String get studyDeleteChapter => 'Fəsli sil'; + + @override + String get studyDeleteThisChapter => 'Bu fəsil silinsin? Bunun geri dönüşü yoxdur!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Bu fəsildəki bütün rəylər, simvollar və çəkilmiş formalar təmizlənsin?'; + + @override + String get studyRightUnderTheBoard => 'Lövhənin altında'; + + @override + String get studyNoPinnedComment => 'Görünməsin'; + + @override + String get studyNormalAnalysis => 'Normal təhlil'; + + @override + String get studyHideNextMoves => 'Növbəti gedişləri gizlət'; + + @override + String get studyInteractiveLesson => 'İnteraktiv dərs'; + + @override + String studyChapterX(String param) { + return '$param. Fəsil'; + } + + @override + String get studyEmpty => 'Boş'; + + @override + String get studyStartFromInitialPosition => 'Başlanğıc pozisiyadan başlasın'; + + @override + String get studyEditor => 'Redaktor'; + + @override + String get studyStartFromCustomPosition => 'Özəl pozisiyadan başlasın'; + + @override + String get studyLoadAGameByUrl => 'URL ilə oyun yüklə'; + + @override + String get studyLoadAPositionFromFen => 'FEN ilə pozisiya yüklə'; + + @override + String get studyLoadAGameFromPgn => 'PGN ilə oyun yüklə'; + + @override + String get studyAutomatic => 'Avtomatik'; + + @override + String get studyUrlOfTheGame => 'Oyun URL-i'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return '$param1 və ya $param2 ilə oyun yükləyin'; + } + + @override + String get studyCreateChapter => 'Fəsil yarat'; + + @override + String get studyCreateStudy => 'Çalışma yarat'; + + @override + String get studyEditStudy => 'Çalışmaya düzəliş et'; + + @override + String get studyVisibility => 'Görünmə'; + + @override + String get studyPublic => 'Hərkəsə açıq'; + + @override + String get studyUnlisted => 'Siyahıya alınmamış'; + + @override + String get studyInviteOnly => 'Yalnız dəvətlə'; + + @override + String get studyAllowCloning => 'Klonlamağa icazə ver'; + + @override + String get studyNobody => 'Heç kim'; + + @override + String get studyOnlyMe => 'Yalnız mən'; + + @override + String get studyContributors => 'Töhfə verənlər'; + + @override + String get studyMembers => 'Üzvlər'; + + @override + String get studyEveryone => 'Hamı'; + + @override + String get studyEnableSync => 'Eyniləşdirməni aktivləşdir'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Bəli: hər kəsi eyni pozisiyada saxla'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Xeyr: sərbəst gəzməyə icazə ver'; + + @override + String get studyPinnedStudyComment => 'Sancaqlanmış çalışma rəyləri'; + @override String get studyStart => 'Başlat'; + + @override + String get studySave => 'Saxla'; + + @override + String get studyClearChat => 'Söhbəti təmizlə'; + + @override + String get studyDeleteTheStudyChatHistory => 'Çalışmanın söhbət tarixçəsi silinsin? Bunun geri dönüşü yoxdur!'; + + @override + String get studyDeleteStudy => 'Çalışmanı sil'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Delete the entire study? There is no going back! Type the name of the study to confirm: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Harada çalışmaq istəyirsən?'; + + @override + String get studyGoodMove => 'Yaxşı gediş'; + + @override + String get studyMistake => 'Səhv'; + + @override + String get studyBrilliantMove => 'Brilyant gediş'; + + @override + String get studyBlunder => 'Kobud səhv'; + + @override + String get studyInterestingMove => 'Maraqlı gediş'; + + @override + String get studyDubiousMove => 'Şübhəli gediş'; + + @override + String get studyOnlyMove => 'Tək gediş'; + + @override + String get studyZugzwang => 'Suqsvanq'; + + @override + String get studyEqualPosition => 'Bərabər mövqe'; + + @override + String get studyUnclearPosition => 'Qeyri-müəyyən mövqe'; + + @override + String get studyWhiteIsSlightlyBetter => 'Ağlar biraz öndədir'; + + @override + String get studyBlackIsSlightlyBetter => 'Qaralar biraz öndədir'; + + @override + String get studyWhiteIsBetter => 'Ağlar üstündür'; + + @override + String get studyBlackIsBetter => 'Qaralar üstündür'; + + @override + String get studyWhiteIsWinning => 'Ağlar qalib gəlir'; + + @override + String get studyBlackIsWinning => 'Qaralar qalib gəlir'; + + @override + String get studyNovelty => 'Novelty'; + + @override + String get studyDevelopment => 'Development'; + + @override + String get studyInitiative => 'Initiative'; + + @override + String get studyAttack => 'Attack'; + + @override + String get studyCounterplay => 'Counterplay'; + + @override + String get studyTimeTrouble => 'Time trouble'; + + @override + String get studyWithCompensation => 'With compensation'; + + @override + String get studyWithTheIdea => 'With the idea'; + + @override + String get studyNextChapter => 'Next chapter'; + + @override + String get studyPrevChapter => 'Previous chapter'; + + @override + String get studyStudyActions => 'Study actions'; + + @override + String get studyTopics => 'Topics'; + + @override + String get studyMyTopics => 'My topics'; + + @override + String get studyPopularTopics => 'Popular topics'; + + @override + String get studyManageTopics => 'Manage topics'; + + @override + String get studyBack => 'Back'; + + @override + String get studyPlayAgain => 'Play again'; + + @override + String get studyWhatWouldYouPlay => 'What would you play in this position?'; + + @override + String get studyYouCompletedThisLesson => 'Congratulations! You completed this lesson.'; + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Fəsil', + one: '$count Fəsil', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Oyun', + one: '$count Oyun', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Üzv', + one: '$count Üzv', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'PGN mətninizi bura yapışdırın, ən çox $count oyuna qədər', + one: 'PGN mətninizi bura yapışdırın, ən çox $count oyuna qədər', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_be.dart b/lib/l10n/l10n_be.dart index 4881fc737d..b52ca6007a 100644 --- a/lib/l10n/l10n_be.dart +++ b/lib/l10n/l10n_be.dart @@ -9,10 +9,10 @@ class AppLocalizationsBe extends AppLocalizations { AppLocalizationsBe([String locale = 'be']) : super(locale); @override - String get mobileHomeTab => 'Home'; + String get mobileHomeTab => 'Галоўная'; @override - String get mobilePuzzlesTab => 'Puzzles'; + String get mobilePuzzlesTab => 'Задачы'; @override String get mobileToolsTab => 'Tools'; @@ -21,7 +21,7 @@ class AppLocalizationsBe extends AppLocalizations { String get mobileWatchTab => 'Watch'; @override - String get mobileSettingsTab => 'Settings'; + String get mobileSettingsTab => 'Налады'; @override String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; @@ -33,13 +33,13 @@ class AppLocalizationsBe extends AppLocalizations { String get mobileFeedbackButton => 'Feedback'; @override - String get mobileOkButton => 'OK'; + String get mobileOkButton => 'Добра'; @override String get mobileSettingsHapticFeedback => 'Haptic feedback'; @override - String get mobileSettingsImmersiveMode => 'Immersive mode'; + String get mobileSettingsImmersiveMode => 'Поўнаэкранны рэжым'; @override String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; @@ -51,21 +51,21 @@ class AppLocalizationsBe extends AppLocalizations { String get mobileAllGames => 'All games'; @override - String get mobileRecentSearches => 'Recent searches'; + String get mobileRecentSearches => 'Нядаўнія пошукі'; @override - String get mobileClearButton => 'Clear'; + String get mobileClearButton => 'Ачысціць'; @override String mobilePlayersMatchingSearchTerm(String param) { - return 'Players with \"$param\"'; + return 'Гульцы з «$param»'; } @override - String get mobileNoSearchResults => 'No results'; + String get mobileNoSearchResults => 'Няма вынікаў'; @override - String get mobileAreYouSure => 'Are you sure?'; + String get mobileAreYouSure => 'Вы ўпэўнены?'; @override String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; @@ -103,9 +103,6 @@ class AppLocalizationsBe extends AppLocalizations { @override String get mobileCancelTakebackOffer => 'Cancel takeback offer'; - @override - String get mobileCancelDrawOffer => 'Cancel draw offer'; - @override String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @@ -262,6 +259,17 @@ class AppLocalizationsBe extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Completed $count $param2 correspondence games', + one: 'Completed $count $param2 correspondence game', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -382,9 +390,226 @@ class AppLocalizationsBe extends AppLocalizations { @override String get broadcastBroadcasts => 'Трансляцыі'; + @override + String get broadcastMyBroadcasts => 'Мае трансляцыі'; + @override String get broadcastLiveBroadcasts => 'Прамыя трансляцыі турніраў'; + @override + String get broadcastBroadcastCalendar => 'Broadcast calendar'; + + @override + String get broadcastNewBroadcast => 'Новая прамая трансляцыя'; + + @override + String get broadcastSubscribedBroadcasts => 'Subscribed broadcasts'; + + @override + String get broadcastAboutBroadcasts => 'Пра трансляцыіі'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Як карыстацца трансляцыямі Lichess.'; + + @override + String get broadcastTheNewRoundHelp => 'The new round will have the same members and contributors as the previous one.'; + + @override + String get broadcastAddRound => 'Дадаць тур'; + + @override + String get broadcastOngoing => 'Бягучыя'; + + @override + String get broadcastUpcoming => 'Надыходзячыя'; + + @override + String get broadcastCompleted => 'Завершаныя'; + + @override + String get broadcastCompletedHelp => 'Lichess detects round completion, but can get it wrong. Use this to set it manually.'; + + @override + String get broadcastRoundName => 'Назва туру'; + + @override + String get broadcastRoundNumber => 'Нумар туру'; + + @override + String get broadcastTournamentName => 'Назва турніру'; + + @override + String get broadcastTournamentDescription => 'Сціслае апісанне турніру'; + + @override + String get broadcastFullDescription => 'Поўнае апісанне турніру'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Неабавязковая дасканалае апісанне турніру. Даступны $param1. Даўжыня павінна быць менш за $param2 сімвалаў.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN Source URL'; + + @override + String get broadcastSourceUrlHelp => 'Спасылка, з якой Lichess паспрабуе атрымоўваць абнаўленні PGN. Яны павінна быць даступнай для кожнай ва Інтэрнэце.'; + + @override + String get broadcastSourceGameIds => 'Up to 64 Lichess game IDs, separated by spaces.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Start date in the tournament local timezone: $param'; + } + + @override + String get broadcastStartDateHelp => 'Па жаданні, калі вы ведаеце пачатак падзеі'; + + @override + String get broadcastCurrentGameUrl => 'Спасылка на бягучую гульню'; + + @override + String get broadcastDownloadAllRounds => 'Спампаваць усе туры'; + + @override + String get broadcastResetRound => 'Скасаваць гэты тур'; + + @override + String get broadcastDeleteRound => 'Выдаліць гэты тур'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Канчаткова выдаліць ​​тур і ўсе яго гульні.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Выдаліць усе гульні гэтага тура. Для іх паўторнага стварэння крыніца павінна быць актыўнай.'; + + @override + String get broadcastEditRoundStudy => 'Рэдагаваць навучанне туру'; + + @override + String get broadcastDeleteTournament => 'Выдаліць гэты турнір'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Канчаткова выдаліць увесь турнір, усе яго туры і ўсе гульні.'; + + @override + String get broadcastShowScores => 'Show players scores based on game results'; + + @override + String get broadcastReplacePlayerTags => 'Optional: replace player names, ratings and titles'; + + @override + String get broadcastFideFederations => 'FIDE federations'; + + @override + String get broadcastTop10Rating => 'Top 10 rating'; + + @override + String get broadcastFidePlayers => 'Гульцы FIDE'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE player not found'; + + @override + String get broadcastFideProfile => 'Профіль FIDE'; + + @override + String get broadcastFederation => 'Федэрацыя'; + + @override + String get broadcastAgeThisYear => 'Age this year'; + + @override + String get broadcastUnrated => 'Unrated'; + + @override + String get broadcastRecentTournaments => 'Recent tournaments'; + + @override + String get broadcastOpenLichess => 'Open in Lichess'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Overview'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Official website'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'Score'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count broadcasts', + one: '$count broadcast', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Выклікаў: $param1'; @@ -874,7 +1099,7 @@ class AppLocalizationsBe extends AppLocalizations { String get puzzleByOpenings => 'By openings'; @override - String get puzzlePuzzlesByOpenings => 'Puzzles by openings'; + String get puzzlePuzzlesByOpenings => 'Задачы за дэбютамі'; @override String get puzzleOpeningsYouPlayedTheMost => 'Openings you played the most in rated games'; @@ -1434,10 +1659,10 @@ class AppLocalizationsBe extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Супернік абмежаваны ў хадах і ўсе магчымыя хады пагаршаюць яго пазіцыю.'; @override - String get puzzleThemeHealthyMix => 'Здаровая сумесь'; + String get puzzleThemeMix => 'Здаровая сумесь'; @override - String get puzzleThemeHealthyMixDescription => 'Патрошкі ўсяго. Вы ня ведаеце чаго чакаць, таму гатовы да ўсяго! Як у сапраўдных гульнях.'; + String get puzzleThemeMixDescription => 'Патрошкі ўсяго. Вы ня ведаеце чаго чакаць, таму гатовы да ўсяго! Як у сапраўдных гульнях.'; @override String get puzzleThemePlayerGames => 'З партый гульца'; @@ -1680,7 +1905,7 @@ class AppLocalizationsBe extends AppLocalizations { String get deleteFromHere => 'Выдаліць з гэтага месца'; @override - String get collapseVariations => 'Collapse variations'; + String get collapseVariations => 'Ачысціць варыянты'; @override String get expandVariations => 'Expand variations'; @@ -1841,9 +2066,6 @@ class AppLocalizationsBe extends AppLocalizations { @override String get removesTheDepthLimit => 'Здымае абмежаванне на глыбіню. Асцярожна, ваш камп’ютар можа перагрэцца!'; - @override - String get engineManager => 'Engine manager'; - @override String get blunder => 'Грубая памылка'; @@ -2107,6 +2329,9 @@ class AppLocalizationsBe extends AppLocalizations { @override String get gamesPlayed => 'Партый згуляна'; + @override + String get ok => 'OK'; + @override String get cancel => 'Скасаваць'; @@ -2675,7 +2900,7 @@ class AppLocalizationsBe extends AppLocalizations { String get editProfile => 'Рэдагаваць профіль'; @override - String get realName => 'Real name'; + String get realName => 'Сапраўднае імя'; @override String get setFlair => 'Set your flair'; @@ -2735,7 +2960,7 @@ class AppLocalizationsBe extends AppLocalizations { String get puzzles => 'Задачы'; @override - String get onlineBots => 'Online bots'; + String get onlineBots => 'Анлайн боты'; @override String get name => 'Назва'; @@ -2756,7 +2981,7 @@ class AppLocalizationsBe extends AppLocalizations { String get yes => 'Так'; @override - String get website => 'Website'; + String get website => 'Вэб-сайт'; @override String get mobile => 'Mobile'; @@ -2816,7 +3041,13 @@ class AppLocalizationsBe extends AppLocalizations { String get other => 'Іншае'; @override - String get reportDescriptionHelp => 'Пакіньце ніжэй спасылку на гульню (ці гульні) і патлумачце, што вас непакоіць у паводзінах гэтага карыстальніка. Не пішыце нешта кшталту «ён чмут!» – патлумачце, як вы прыйшлі да гэтага выніку. Мы хутчэй разбярэмся ў сітуацыі, калі вы напішаце нам па-англійску.'; + String get reportCheatBoostHelp => 'Paste the link to the game(s) and explain what is wrong about this user\'s behaviour. Don\'t just say \"they cheat\", but tell us how you came to this conclusion.'; + + @override + String get reportUsernameHelp => 'Explain what about this username is offensive. Don\'t just say \"it\'s offensive/inappropriate\", but tell us how you came to this conclusion, especially if the insult is obfuscated, not in english, is in slang, or is a historical/cultural reference.'; + + @override + String get reportProcessedFasterInEnglish => 'Your report will be processed faster if written in English.'; @override String get error_provideOneCheatedGameLink => 'Калі ласка, дадайце спасылку хаця б на адну гульню, дзе былі парушаны правілы.'; @@ -2919,7 +3150,7 @@ class AppLocalizationsBe extends AppLocalizations { String get outsideTheBoard => 'Па-за дошкай'; @override - String get allSquaresOfTheBoard => 'All squares of the board'; + String get allSquaresOfTheBoard => 'Усе клеткі на дошцы'; @override String get onSlowGames => 'У павольных гульнях'; @@ -3515,19 +3746,19 @@ class AppLocalizationsBe extends AppLocalizations { String get backgroundImageUrl => 'Спасылка на фон:'; @override - String get board => 'Board'; + String get board => 'Дошка'; @override - String get size => 'Size'; + String get size => 'Размер'; @override - String get opacity => 'Opacity'; + String get opacity => 'Празрыстасць'; @override - String get brightness => 'Brightness'; + String get brightness => 'Яркасць'; @override - String get hue => 'Hue'; + String get hue => 'Адценне'; @override String get boardReset => 'Reset colours to default'; @@ -4121,6 +4352,9 @@ class AppLocalizationsBe extends AppLocalizations { @override String get nothingToSeeHere => 'Nothing to see here at the moment.'; + @override + String get stats => 'Stats'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4851,9 +5085,522 @@ class AppLocalizationsBe extends AppLocalizations { @override String get streamerLichessStreamers => 'Стрымеры на Lichess'; + @override + String get studyPrivate => 'Прыватны'; + + @override + String get studyMyStudies => 'Мае навучанні'; + + @override + String get studyStudiesIContributeTo => 'Навучанні, якія я рэдагую'; + + @override + String get studyMyPublicStudies => 'Мае публічныя навучанні'; + + @override + String get studyMyPrivateStudies => 'Мае прыватные навучанні'; + + @override + String get studyMyFavoriteStudies => 'Мае ўлюбленые навучанні'; + + @override + String get studyWhatAreStudies => 'Што такое навучанні?'; + + @override + String get studyAllStudies => 'Усе навучанні'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Навучанні, створаныя $param'; + } + + @override + String get studyNoneYet => 'Пакуль нічога няма.'; + + @override + String get studyHot => 'Гарачыя'; + + @override + String get studyDateAddedNewest => 'Дата дадання (навейшыя)'; + + @override + String get studyDateAddedOldest => 'Дата дадання (старэйшыя)'; + + @override + String get studyRecentlyUpdated => 'Нядаўна абноўленыя'; + + @override + String get studyMostPopular => 'Найбольш папулярныя'; + + @override + String get studyAlphabetical => 'Па алфавіце'; + + @override + String get studyAddNewChapter => 'Дадаць новы раздзел'; + + @override + String get studyAddMembers => 'Дадаць удзельнікаў'; + + @override + String get studyInviteToTheStudy => 'Закліцца да навучання'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Калі ласка, заклікайце толькі людзей, якіх вы ведаеце, та тых хто актыўна хоча далучыцца да навучання.'; + + @override + String get studySearchByUsername => 'Шукаць па імені карыстальніка'; + + @override + String get studySpectator => 'Глядач'; + + @override + String get studyContributor => 'Рэдактар'; + + @override + String get studyKick => 'Выдаліць'; + + @override + String get studyLeaveTheStudy => 'Пакінуць навучанне'; + + @override + String get studyYouAreNowAContributor => 'Вы цяпер рэдактар'; + + @override + String get studyYouAreNowASpectator => 'Вы цяпер глядач'; + + @override + String get studyPgnTags => 'Тэгі PGN'; + + @override + String get studyLike => 'Упадабаць'; + + @override + String get studyUnlike => 'Разпадабаць'; + + @override + String get studyNewTag => 'Новы тэг'; + + @override + String get studyCommentThisPosition => 'Каментаваць пазіцыю'; + + @override + String get studyCommentThisMove => 'Каментаваць гэты ход'; + + @override + String get studyAnnotateWithGlyphs => 'Дадаць знакавую анатацыю'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Раздел занадта кароткі для аналізу.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Толькі рэдактары навучання могуць запрасіць камп\'ютарны аналіз.'; + + @override + String get studyGetAFullComputerAnalysis => 'Атрымайце поўны серверны кампутарны аналіз галоўнай лініі.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Пераканайцеся, што раздзел гатоў. Вы можаце запрасіць аналіз толькі адзін раз.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Усе сінхранізаваныя ўдзельнікі застаюцца на аднолькавай пазіцыі'; + + @override + String get studyShareChanges => 'Падзяліцца зменамі з гледачамі та захаваць іх на серверы'; + + @override + String get studyPlaying => 'Гуляецца'; + + @override + String get studyShowEvalBar => 'Шкалы ацэнкі'; + + @override + String get studyFirst => 'На першую'; + + @override + String get studyPrevious => 'Папярэдняя'; + + @override + String get studyNext => 'Наступная'; + + @override + String get studyLast => 'На апошнюю'; + @override String get studyShareAndExport => 'Падзяліцца & экспартаваць'; + @override + String get studyCloneStudy => 'Кланаваць'; + + @override + String get studyStudyPgn => 'PGN навучання'; + + @override + String get studyDownloadAllGames => 'Спампаваць усе гульні'; + + @override + String get studyChapterPgn => 'PGN раздзелу'; + + @override + String get studyCopyChapterPgn => 'Скапіраваць PGN'; + + @override + String get studyDownloadGame => 'Спампаваць гульню'; + + @override + String get studyStudyUrl => 'URL навучання'; + + @override + String get studyCurrentChapterUrl => 'URL бягучага раздзелу'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Вы можаце ўставіць гэта на форум, каб убудаваць'; + + @override + String get studyStartAtInitialPosition => 'Пачынаць у пачатковай пазіцыі'; + + @override + String studyStartAtX(String param) { + return 'Пачынаць з $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Убудаваць у свой сайт або блог'; + + @override + String get studyReadMoreAboutEmbedding => 'Пачытаць больш пра ўбудаванне'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Толькі публічныя навучанні могуць быць убудаваны!'; + + @override + String get studyOpen => 'Адкрыць'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param2 зрабіў для вас $param1'; + } + + @override + String get studyStudyNotFound => 'Навучанне не знойдзена'; + + @override + String get studyEditChapter => 'Рэдагаваць раздзел'; + + @override + String get studyNewChapter => 'Новы раздзел'; + + @override + String studyImportFromChapterX(String param) { + return 'Імпартаваць з $param'; + } + + @override + String get studyOrientation => 'Арыентацыя дошкі'; + + @override + String get studyAnalysisMode => 'Рэжым аналізу'; + + @override + String get studyPinnedChapterComment => 'Замацаваны каментар раздзелу'; + + @override + String get studySaveChapter => 'Захаваць раздзел'; + + @override + String get studyClearAnnotations => 'Ачысціць анатацыі'; + + @override + String get studyClearVariations => 'Ачысціць варыянты'; + + @override + String get studyDeleteChapter => 'Выдаліць раздзел'; + + @override + String get studyDeleteThisChapter => 'Выдаліць гэты раздел? Гэта нельга будзе адмяніць!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Выдаліць усе каментары, знакавыя анатацыі і намаляваныя фігуры ў гэтым раздзеле?'; + + @override + String get studyRightUnderTheBoard => 'Адразу пад дошкай'; + + @override + String get studyNoPinnedComment => 'Ніякіх'; + + @override + String get studyNormalAnalysis => 'Звычайны аналіз'; + + @override + String get studyHideNextMoves => 'Схаваць наступныя хады'; + + @override + String get studyInteractiveLesson => 'Інтэрактыўны занятак'; + + @override + String studyChapterX(String param) { + return 'Раздзел $param'; + } + + @override + String get studyEmpty => 'Пуста'; + + @override + String get studyStartFromInitialPosition => 'Пачынаць з пачатковай пазіцыі'; + + @override + String get studyEditor => 'Рэдактар'; + + @override + String get studyStartFromCustomPosition => 'Пачынаць з абранай пазіцыі'; + + @override + String get studyLoadAGameByUrl => 'Загрузіць гульні па URLs'; + + @override + String get studyLoadAPositionFromFen => 'Загрузіць пазіцыю з FEN'; + + @override + String get studyLoadAGameFromPgn => 'Загрузіць гульні з PGN'; + + @override + String get studyAutomatic => 'Аўтаматычна'; + + @override + String get studyUrlOfTheGame => 'URL гульняў, адзін на радок'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Загрузіць партыі з $param1 або $param2'; + } + + @override + String get studyCreateChapter => 'Стварыць раздзел'; + + @override + String get studyCreateStudy => 'Стварыць навучанне'; + + @override + String get studyEditStudy => 'Рэдактаваць навучанне'; + + @override + String get studyVisibility => 'Бачнасць'; + + @override + String get studyPublic => 'Публічны'; + + @override + String get studyUnlisted => 'Нябачны'; + + @override + String get studyInviteOnly => 'Толькі па запрашэннях'; + + @override + String get studyAllowCloning => 'Дазволіць кланаванне'; + + @override + String get studyNobody => 'Ніхто'; + + @override + String get studyOnlyMe => 'Толькі я'; + + @override + String get studyContributors => 'Рэдактары'; + + @override + String get studyMembers => 'Удзельнікі'; + + @override + String get studyEveryone => 'Кожны'; + + @override + String get studyEnableSync => 'Уключыць сінхранізацыю'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Так: трымаць усіх на аднолькавай пазіцыі'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Не: хай людзі вольна праглядаюць пазіцыі'; + + @override + String get studyPinnedStudyComment => 'Замацаваць каментар да занятку'; + @override String get studyStart => 'Пачаць'; + + @override + String get studySave => 'Захаваць'; + + @override + String get studyClearChat => 'Ачысціць чат'; + + @override + String get studyDeleteTheStudyChatHistory => 'Выдаліць гісторыю чата навучання цалкам? Гэта нельга будзе адмяніць!'; + + @override + String get studyDeleteStudy => 'Выдаліць навучанне'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Выдаліць навучанне поўнасцю? Гэта нельга будзе адмяніць! Увядзіце назву навучання каб падцвердзіць: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Дзе вы жадаеце навучацца?'; + + @override + String get studyGoodMove => 'Добры ход'; + + @override + String get studyMistake => 'Памылка'; + + @override + String get studyBrilliantMove => 'Бліскучы ход'; + + @override + String get studyBlunder => 'Позех'; + + @override + String get studyInterestingMove => 'Цікавы ход'; + + @override + String get studyDubiousMove => 'Сумнеўны ход'; + + @override + String get studyOnlyMove => 'Адзіны ход'; + + @override + String get studyZugzwang => 'Цугцванг'; + + @override + String get studyEqualPosition => 'Раўная пазіцыя'; + + @override + String get studyUnclearPosition => 'Незразумелая пазіцыя'; + + @override + String get studyWhiteIsSlightlyBetter => 'У белых трошкі лепш'; + + @override + String get studyBlackIsSlightlyBetter => 'У чорных трошкі лепш'; + + @override + String get studyWhiteIsBetter => 'У белых лепш'; + + @override + String get studyBlackIsBetter => 'У чорных лепш'; + + @override + String get studyWhiteIsWinning => 'Белыя перамагаюць'; + + @override + String get studyBlackIsWinning => 'Чорныя перамагаюць'; + + @override + String get studyNovelty => 'Новаўвядзенне'; + + @override + String get studyDevelopment => 'Развіццё'; + + @override + String get studyInitiative => 'Ініцыятыва'; + + @override + String get studyAttack => 'Напад'; + + @override + String get studyCounterplay => 'Контргульня'; + + @override + String get studyTimeTrouble => 'Цэйтнот'; + + @override + String get studyWithCompensation => 'З кампенсацыяй'; + + @override + String get studyWithTheIdea => 'З ідэяй'; + + @override + String get studyNextChapter => 'Наступны раздзел'; + + @override + String get studyPrevChapter => 'Папярэдні раздзел'; + + @override + String get studyStudyActions => 'Дзеянні ў навучанні'; + + @override + String get studyTopics => 'Тэмы'; + + @override + String get studyMyTopics => 'Мае тэмы'; + + @override + String get studyPopularTopics => 'Папулярныя тэмы'; + + @override + String get studyManageTopics => 'Кіраваць тэмамі'; + + @override + String get studyBack => 'Назад'; + + @override + String get studyPlayAgain => 'Гуляць зноў'; + + @override + String get studyWhatWouldYouPlay => 'Як бы вы пахадзілі ў гэтай пазіцыі?'; + + @override + String get studyYouCompletedThisLesson => 'Віншуем! Вы прайшлі гэты ўрок.'; + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count раздзелаў', + many: '$count раздзелаў', + few: '$count раздзелы', + one: '$count раздзел', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count партый', + many: '$count партый', + few: '$count партыі', + one: '$count партыя', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count удзельнікаў', + many: '$count удзельнікаў', + few: '$count удзельніка', + one: '$count удзельнік', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Устаўце сюды ваш PGN тэкст, не больш за $count гульняў', + many: 'Устаўце сюды ваш PGN тэкст, не больш за $count гульняў', + few: 'Устаўце сюды ваш PGN тэкст, не больш за $count гульні', + one: 'Устаўце сюды ваш PGN тэкст, не больш за $count гульню', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_bg.dart b/lib/l10n/l10n_bg.dart index 4d6c9a7e35..8ef8d6d0bb 100644 --- a/lib/l10n/l10n_bg.dart +++ b/lib/l10n/l10n_bg.dart @@ -103,9 +103,6 @@ class AppLocalizationsBg extends AppLocalizations { @override String get mobileCancelTakebackOffer => 'Cancel takeback offer'; - @override - String get mobileCancelDrawOffer => 'Cancel draw offer'; - @override String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @@ -125,21 +122,21 @@ class AppLocalizationsBg extends AppLocalizations { String get mobileSomethingWentWrong => 'Възникна грешка.'; @override - String get mobileShowResult => 'Show result'; + String get mobileShowResult => 'Покажи резултат'; @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobilePuzzleThemesSubtitle => 'Решавайте задачи от любимите Ви дебюти или изберете друга тема.'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobilePuzzleStormSubtitle => 'Решете колкото можете повече задачи за 3 минути.'; @override String mobileGreeting(String param) { - return 'Hello, $param'; + return 'Здравейте, $param'; } @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileGreetingWithoutName => 'Здравейте'; @override String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; @@ -246,6 +243,17 @@ class AppLocalizationsBg extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Изиграни $count $param2 кореспондентски игри', + one: 'Изиграна $count $param2 кореспондентска игра', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,226 @@ class AppLocalizationsBg extends AppLocalizations { @override String get broadcastBroadcasts => 'Излъчване'; + @override + String get broadcastMyBroadcasts => 'Моите излъчвания'; + @override String get broadcastLiveBroadcasts => 'Излъчвания на турнир на живо'; + @override + String get broadcastBroadcastCalendar => 'Календар на излъчванията'; + + @override + String get broadcastNewBroadcast => 'Нови предавания на живо'; + + @override + String get broadcastSubscribedBroadcasts => 'Излчвания които следя'; + + @override + String get broadcastAboutBroadcasts => 'About broadcasts'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'How to use Lichess Broadcasts.'; + + @override + String get broadcastTheNewRoundHelp => 'The new round will have the same members and contributors as the previous one.'; + + @override + String get broadcastAddRound => 'Добави рунд'; + + @override + String get broadcastOngoing => 'Текущи'; + + @override + String get broadcastUpcoming => 'Предстоящи'; + + @override + String get broadcastCompleted => 'Завършени'; + + @override + String get broadcastCompletedHelp => 'Lichess detects round completion, but can get it wrong. Use this to set it manually.'; + + @override + String get broadcastRoundName => 'Име на рунда'; + + @override + String get broadcastRoundNumber => 'Номер на рунда'; + + @override + String get broadcastTournamentName => 'Име на турнира'; + + @override + String get broadcastTournamentDescription => 'Кратко описание на турнира'; + + @override + String get broadcastFullDescription => 'Пълно описание на събитието'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Незадължително дълго описание на излъчването. $param1 е налично. Дължината трябва да по-малка от $param2 знака.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN Source URL'; + + @override + String get broadcastSourceUrlHelp => 'Уебадресът, който Lichess ще проверява, за да получи осъвременявания на PGN. Той трябва да е публично достъпен от интернет.'; + + @override + String get broadcastSourceGameIds => 'Up to 64 Lichess game IDs, separated by spaces.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Start date in the tournament local timezone: $param'; + } + + @override + String get broadcastStartDateHelp => 'По избор, ако знаете, кога започва събитието'; + + @override + String get broadcastCurrentGameUrl => 'URL на настоящата партия'; + + @override + String get broadcastDownloadAllRounds => 'Изтегли всички рундове'; + + @override + String get broadcastResetRound => 'Нулирай този рунд'; + + @override + String get broadcastDeleteRound => 'Изтрий този рунд'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Окончателно изтрийте този рунд и всичките му игри.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Изтрийте този рунд и всичките му игри. Източникът трябва да е активен за да можете да ги възстановите.'; + + @override + String get broadcastEditRoundStudy => 'Edit round study'; + + @override + String get broadcastDeleteTournament => 'Изтрий този турнир'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Окончателно изтрий целия турнир, всичките му рундове и игри.'; + + @override + String get broadcastShowScores => 'Show players scores based on game results'; + + @override + String get broadcastReplacePlayerTags => 'По избор: промени имената на играчите, рейтингите и титлите'; + + @override + String get broadcastFideFederations => 'ФИДЕ федерации'; + + @override + String get broadcastTop10Rating => 'Top 10 rating'; + + @override + String get broadcastFidePlayers => 'FIDE players'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE player not found'; + + @override + String get broadcastFideProfile => 'ФИДЕ профил'; + + @override + String get broadcastFederation => 'Федерация'; + + @override + String get broadcastAgeThisYear => 'Age this year'; + + @override + String get broadcastUnrated => 'Unrated'; + + @override + String get broadcastRecentTournaments => 'Recent tournaments'; + + @override + String get broadcastOpenLichess => 'Open in Lichess'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Overview'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Official website'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'Score'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count излъчвания', + one: '$count излъчване', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Предизвикателства: $param1'; @@ -1390,10 +1615,10 @@ class AppLocalizationsBg extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Опонентът има малко възможни ходове и всеки един от тях води до влошаване на положението му.'; @override - String get puzzleThemeHealthyMix => 'От всичко по малко'; + String get puzzleThemeMix => 'От всичко по малко'; @override - String get puzzleThemeHealthyMixDescription => 'По малко от всичко. Не знаете какво да очаквате, така че бъдете готови за всичко! Точно като в истинските игри.'; + String get puzzleThemeMixDescription => 'По малко от всичко. Не знаете какво да очаквате, така че бъдете готови за всичко! Точно като в истинските игри.'; @override String get puzzleThemePlayerGames => 'Партии на играча'; @@ -1797,9 +2022,6 @@ class AppLocalizationsBg extends AppLocalizations { @override String get removesTheDepthLimit => 'Анализът ще е безкраен, а компютърът ви - топъл'; - @override - String get engineManager => 'Мениджър на двигателя'; - @override String get blunder => 'Груба грешка'; @@ -2063,6 +2285,9 @@ class AppLocalizationsBg extends AppLocalizations { @override String get gamesPlayed => 'Изиграни игри'; + @override + String get ok => 'ОК'; + @override String get cancel => 'Отказ'; @@ -2772,7 +2997,13 @@ class AppLocalizationsBg extends AppLocalizations { String get other => 'Друго'; @override - String get reportDescriptionHelp => 'Поставете линк към играта и обяснете какъв е проблемът с поведението на този потребител. Не казвайте единствено, че мами, но ни кажете как сте стигнали до този извод. Вашият доклад ще бъде обработен по-бързо, ако е написан на английски.'; + String get reportCheatBoostHelp => 'Paste the link to the game(s) and explain what is wrong about this user\'s behaviour. Don\'t just say \"they cheat\", but tell us how you came to this conclusion.'; + + @override + String get reportUsernameHelp => 'Explain what about this username is offensive. Don\'t just say \"it\'s offensive/inappropriate\", but tell us how you came to this conclusion, especially if the insult is obfuscated, not in english, is in slang, or is a historical/cultural reference.'; + + @override + String get reportProcessedFasterInEnglish => 'Your report will be processed faster if written in English.'; @override String get error_provideOneCheatedGameLink => 'Моля дай поне един линк до измамна игра.'; @@ -4077,6 +4308,9 @@ class AppLocalizationsBg extends AppLocalizations { @override String get nothingToSeeHere => 'Nothing to see here at the moment.'; + @override + String get stats => 'Stats'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4723,9 +4957,514 @@ class AppLocalizationsBg extends AppLocalizations { @override String get streamerLichessStreamers => 'Lichess стриймъри'; + @override + String get studyPrivate => 'Лични'; + + @override + String get studyMyStudies => 'Моите казуси'; + + @override + String get studyStudiesIContributeTo => 'Казуси, към които допринасям'; + + @override + String get studyMyPublicStudies => 'Моите публични казуси'; + + @override + String get studyMyPrivateStudies => 'Моите лични казуси'; + + @override + String get studyMyFavoriteStudies => 'Моите любими казуси'; + + @override + String get studyWhatAreStudies => 'Какво представляват казусите?'; + + @override + String get studyAllStudies => 'Всички казуси'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Казуси от $param'; + } + + @override + String get studyNoneYet => 'Все още няма.'; + + @override + String get studyHot => 'Популярни'; + + @override + String get studyDateAddedNewest => 'Дата на добавяне (най-нови)'; + + @override + String get studyDateAddedOldest => 'Дата на добавяне (най-стари)'; + + @override + String get studyRecentlyUpdated => 'Скоро обновени'; + + @override + String get studyMostPopular => 'Най-популярни'; + + @override + String get studyAlphabetical => 'Азбучно'; + + @override + String get studyAddNewChapter => 'Добавяне на нов раздел'; + + @override + String get studyAddMembers => 'Добави членове'; + + @override + String get studyInviteToTheStudy => 'Покани към казуса'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Моля канете само хора, които познавате и които биха искали да се присъединят.'; + + @override + String get studySearchByUsername => 'Търсене по потребителско име'; + + @override + String get studySpectator => 'Зрител'; + + @override + String get studyContributor => 'Сътрудник'; + + @override + String get studyKick => 'Изритване'; + + @override + String get studyLeaveTheStudy => 'Напусни казуса'; + + @override + String get studyYouAreNowAContributor => 'Вие сте сътрудник'; + + @override + String get studyYouAreNowASpectator => 'Вие сте зрител'; + + @override + String get studyPgnTags => 'PGN тагове'; + + @override + String get studyLike => 'Харесай'; + + @override + String get studyUnlike => 'Не харесвам'; + + @override + String get studyNewTag => 'Нов таг'; + + @override + String get studyCommentThisPosition => 'Коментирай позицията'; + + @override + String get studyCommentThisMove => 'Коментирай хода'; + + @override + String get studyAnnotateWithGlyphs => 'Анотация със специални символи'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Тази глава е твърде къса и не може да бъде анализирана.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Само сътрудници към казуса могат да пускат компютърен анализ.'; + + @override + String get studyGetAFullComputerAnalysis => 'Вземи пълен сървърен анализ на основна линия.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Уверете се, че главата е завършена. Можете да пуснете анализ само веднъж.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Всички синхронизирани членове остават на същата позиция'; + + @override + String get studyShareChanges => 'Споделете промените със зрителите и ги запазете на сървъра'; + + @override + String get studyPlaying => 'Играе се'; + + @override + String get studyShowEvalBar => 'Evaluation bars'; + + @override + String get studyFirst => 'Първа'; + + @override + String get studyPrevious => 'Предишна'; + + @override + String get studyNext => 'Следваща'; + + @override + String get studyLast => 'Последна'; + @override String get studyShareAndExport => 'Сподели'; + @override + String get studyCloneStudy => 'Клонирай'; + + @override + String get studyStudyPgn => 'PGN на казуса'; + + @override + String get studyDownloadAllGames => 'Изтегли всички партии'; + + @override + String get studyChapterPgn => 'PGN на главата'; + + @override + String get studyCopyChapterPgn => 'Копирай PGN'; + + @override + String get studyDownloadGame => 'Изтегли партия'; + + @override + String get studyStudyUrl => 'URL на казуса'; + + @override + String get studyCurrentChapterUrl => 'URL на настоящата глава'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Можете да поставите това във форум и ще бъде вградено'; + + @override + String get studyStartAtInitialPosition => 'Започни от начална позиция'; + + @override + String studyStartAtX(String param) { + return 'Започни от $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Вгради в твоя сайт или блог'; + + @override + String get studyReadMoreAboutEmbedding => 'Прочети повече за вграждането'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Само публични казуси могат да бъдат вграждани!'; + + @override + String get studyOpen => 'Отвори'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1, предоставени от $param2'; + } + + @override + String get studyStudyNotFound => 'Казусът не бе открит'; + + @override + String get studyEditChapter => 'Промени глава'; + + @override + String get studyNewChapter => 'Нова глава'; + + @override + String studyImportFromChapterX(String param) { + return 'Импортиране от $param'; + } + + @override + String get studyOrientation => 'Ориентация'; + + @override + String get studyAnalysisMode => 'Режим на анализ'; + + @override + String get studyPinnedChapterComment => 'Коментар на главата'; + + @override + String get studySaveChapter => 'Запази глава'; + + @override + String get studyClearAnnotations => 'Изтрий анотациите'; + + @override + String get studyClearVariations => 'Изчисти вариациите'; + + @override + String get studyDeleteChapter => 'Изтрий глава'; + + @override + String get studyDeleteThisChapter => 'Изтриване на главата? Това е необратимо!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Изтрий всички коментари, специални символи и нарисувани форми в главата?'; + + @override + String get studyRightUnderTheBoard => 'Точно под дъската'; + + @override + String get studyNoPinnedComment => 'Никакви'; + + @override + String get studyNormalAnalysis => 'Нормален анализ'; + + @override + String get studyHideNextMoves => 'Скриване на следващите ходове'; + + @override + String get studyInteractiveLesson => 'Интерактивен урок'; + + @override + String studyChapterX(String param) { + return 'Глава: $param'; + } + + @override + String get studyEmpty => 'Празна'; + + @override + String get studyStartFromInitialPosition => 'Започни от начална позиция'; + + @override + String get studyEditor => 'Редактор'; + + @override + String get studyStartFromCustomPosition => 'Започни от избрана позиция'; + + @override + String get studyLoadAGameByUrl => 'Зареди партии от URL'; + + @override + String get studyLoadAPositionFromFen => 'Зареди позиция от FEN'; + + @override + String get studyLoadAGameFromPgn => 'Зареди партии от PGN'; + + @override + String get studyAutomatic => 'Автоматичен'; + + @override + String get studyUrlOfTheGame => 'URL на партиите, по една на линия'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Зареди партии от $param1 или $param2'; + } + + @override + String get studyCreateChapter => 'Създай'; + + @override + String get studyCreateStudy => 'Създай казус'; + + @override + String get studyEditStudy => 'Редактирай казус'; + + @override + String get studyVisibility => 'Видимост'; + + @override + String get studyPublic => 'Публични'; + + @override + String get studyUnlisted => 'Несподелени'; + + @override + String get studyInviteOnly => 'Само с покани'; + + @override + String get studyAllowCloning => 'Позволи клониране'; + + @override + String get studyNobody => 'Никой'; + + @override + String get studyOnlyMe => 'Само за мен'; + + @override + String get studyContributors => 'Сътрудници'; + + @override + String get studyMembers => 'Членове'; + + @override + String get studyEveryone => 'Всички'; + + @override + String get studyEnableSync => 'Разреши синхронизиране'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Да: дръж всички на същата позиция'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Не: позволи свободно разглеждане'; + + @override + String get studyPinnedStudyComment => 'Коментар на казуса'; + @override String get studyStart => 'Начало'; + + @override + String get studySave => 'Запази'; + + @override + String get studyClearChat => 'Изтрий чат съобщенията'; + + @override + String get studyDeleteTheStudyChatHistory => 'Изтриване на чат историята? Това е необратимо!'; + + @override + String get studyDeleteStudy => 'Изтрий казуса'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Изтриване на целия казус? Това е необратимо! Въведете името на казуса за да потвърдите: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Къде да бъде проучено това?'; + + @override + String get studyGoodMove => 'Добър ход'; + + @override + String get studyMistake => 'Грешка'; + + @override + String get studyBrilliantMove => 'Отличен ход'; + + @override + String get studyBlunder => 'Груба грешка'; + + @override + String get studyInterestingMove => 'Интересен ход'; + + @override + String get studyDubiousMove => 'Съмнителен ход'; + + @override + String get studyOnlyMove => 'Единствен ход'; + + @override + String get studyZugzwang => 'Цугцванг'; + + @override + String get studyEqualPosition => 'Равна позиция'; + + @override + String get studyUnclearPosition => 'Неясна позиция'; + + @override + String get studyWhiteIsSlightlyBetter => 'Белите са малко по-добре'; + + @override + String get studyBlackIsSlightlyBetter => 'Черните са малко по-добре'; + + @override + String get studyWhiteIsBetter => 'Белите са по-добре'; + + @override + String get studyBlackIsBetter => 'Черните са по-добре'; + + @override + String get studyWhiteIsWinning => 'Белите печелят'; + + @override + String get studyBlackIsWinning => 'Черните печелят'; + + @override + String get studyNovelty => 'Нововъведeние'; + + @override + String get studyDevelopment => 'Развитие'; + + @override + String get studyInitiative => 'Инициатива'; + + @override + String get studyAttack => 'Атака'; + + @override + String get studyCounterplay => 'Контра атака'; + + @override + String get studyTimeTrouble => 'Проблем с времето'; + + @override + String get studyWithCompensation => 'С компенсация'; + + @override + String get studyWithTheIdea => 'С идеята'; + + @override + String get studyNextChapter => 'Следваща глава'; + + @override + String get studyPrevChapter => 'Предишна глава'; + + @override + String get studyStudyActions => 'Опции за учене'; + + @override + String get studyTopics => 'Теми'; + + @override + String get studyMyTopics => 'Моите теми'; + + @override + String get studyPopularTopics => 'Популярни теми'; + + @override + String get studyManageTopics => 'Управление на темите'; + + @override + String get studyBack => 'Обратно'; + + @override + String get studyPlayAgain => 'Играйте отново'; + + @override + String get studyWhatWouldYouPlay => 'Какво бихте играли в тази позиция?'; + + @override + String get studyYouCompletedThisLesson => 'Поздравления! Вие завършихте този урок.'; + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Глави', + one: '$count Глава', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Игри', + one: '$count Игра', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Членове', + one: '$count Член', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Постави твоя PGN текст тук, до $count партии', + one: 'Постави твоя PGN текст тук, до $count партия', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_bn.dart b/lib/l10n/l10n_bn.dart index 36ed6689cb..4cb7ef5ee3 100644 --- a/lib/l10n/l10n_bn.dart +++ b/lib/l10n/l10n_bn.dart @@ -103,9 +103,6 @@ class AppLocalizationsBn extends AppLocalizations { @override String get mobileCancelTakebackOffer => 'Cancel takeback offer'; - @override - String get mobileCancelDrawOffer => 'Cancel draw offer'; - @override String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @@ -246,6 +243,17 @@ class AppLocalizationsBn extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Completed $count $param2 correspondence games', + one: 'Completed $count $param2 correspondence game', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,226 @@ class AppLocalizationsBn extends AppLocalizations { @override String get broadcastBroadcasts => 'সম্প্রচার'; + @override + String get broadcastMyBroadcasts => 'My broadcasts'; + @override String get broadcastLiveBroadcasts => 'সরাসরি টুর্নামেন্ট সম্প্রচার'; + @override + String get broadcastBroadcastCalendar => 'Broadcast calendar'; + + @override + String get broadcastNewBroadcast => 'নতুন সরাসরি সম্প্রচার'; + + @override + String get broadcastSubscribedBroadcasts => 'Subscribed broadcasts'; + + @override + String get broadcastAboutBroadcasts => 'About broadcasts'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'How to use Lichess Broadcasts.'; + + @override + String get broadcastTheNewRoundHelp => 'The new round will have the same members and contributors as the previous one.'; + + @override + String get broadcastAddRound => 'Add a round'; + + @override + String get broadcastOngoing => 'চলমান'; + + @override + String get broadcastUpcoming => 'আসন্ন'; + + @override + String get broadcastCompleted => 'সমাপ্ত'; + + @override + String get broadcastCompletedHelp => 'Lichess detects round completion, but can get it wrong. Use this to set it manually.'; + + @override + String get broadcastRoundName => 'Round name'; + + @override + String get broadcastRoundNumber => 'গোল নম্বর'; + + @override + String get broadcastTournamentName => 'Tournament name'; + + @override + String get broadcastTournamentDescription => 'Short tournament description'; + + @override + String get broadcastFullDescription => 'ইভেন্টের সম্পূর্ণ বিবরণ'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Optional long description of the tournament. $param1 is available. Length must be less than $param2 characters.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN Source URL'; + + @override + String get broadcastSourceUrlHelp => 'ইউআরএল যা লাইসেন্সেস পিজিএন আপডেট পেতে চেক করবে। এটি অবশ্যই ইন্টারনেট থেকে সর্বজনীনভাবে অ্যাক্সেসযোগ্য।.'; + + @override + String get broadcastSourceGameIds => 'Up to 64 Lichess game IDs, separated by spaces.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Start date in the tournament local timezone: $param'; + } + + @override + String get broadcastStartDateHelp => 'Optional, if you know when the event starts'; + + @override + String get broadcastCurrentGameUrl => 'Current game URL'; + + @override + String get broadcastDownloadAllRounds => 'Download all rounds'; + + @override + String get broadcastResetRound => 'Reset this round'; + + @override + String get broadcastDeleteRound => 'Delete this round'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Definitively delete the round and all its games.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Delete all games of this round. The source will need to be active in order to re-create them.'; + + @override + String get broadcastEditRoundStudy => 'Edit round study'; + + @override + String get broadcastDeleteTournament => 'Delete this tournament'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Definitively delete the entire tournament, all its rounds and all its games.'; + + @override + String get broadcastShowScores => 'Show players scores based on game results'; + + @override + String get broadcastReplacePlayerTags => 'Optional: replace player names, ratings and titles'; + + @override + String get broadcastFideFederations => 'FIDE federations'; + + @override + String get broadcastTop10Rating => 'Top 10 rating'; + + @override + String get broadcastFidePlayers => 'FIDE players'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE player not found'; + + @override + String get broadcastFideProfile => 'FIDE profile'; + + @override + String get broadcastFederation => 'Federation'; + + @override + String get broadcastAgeThisYear => 'Age this year'; + + @override + String get broadcastUnrated => 'Unrated'; + + @override + String get broadcastRecentTournaments => 'Recent tournaments'; + + @override + String get broadcastOpenLichess => 'Open in Lichess'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Overview'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Official website'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'Score'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count broadcasts', + one: '$count broadcast', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'প্রতিদ্বন্দ্বীরা:$param1'; @@ -1390,10 +1615,10 @@ class AppLocalizationsBn extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'প্রতিপক্ষের সীমিত চাল আছে, এবং সব চাল তাদের অবস্থান আরো খারাপ করবে।'; @override - String get puzzleThemeHealthyMix => 'পরিমিত মিশ্রণ'; + String get puzzleThemeMix => 'পরিমিত মিশ্রণ'; @override - String get puzzleThemeHealthyMixDescription => 'সবকিছু একটু করে। আপনি জানবেন না কি আসতে চলেছে। অনেকটা বাস্তব খেলার মতো।'; + String get puzzleThemeMixDescription => 'সবকিছু একটু করে। আপনি জানবেন না কি আসতে চলেছে। অনেকটা বাস্তব খেলার মতো।'; @override String get puzzleThemePlayerGames => 'খেলোয়ারদের খেলা হতে'; @@ -1636,7 +1861,7 @@ class AppLocalizationsBn extends AppLocalizations { String get deleteFromHere => 'এখান থেকে মুছুন'; @override - String get collapseVariations => 'Collapse variations'; + String get collapseVariations => 'ভেরিয়েশন সঙ্কুচিত করুন'; @override String get expandVariations => 'Expand variations'; @@ -1797,9 +2022,6 @@ class AppLocalizationsBn extends AppLocalizations { @override String get removesTheDepthLimit => 'গভীরতার সীমা অপসারণ করুন এবং আপনার কম্পিউটারকে গরম রাখুন'; - @override - String get engineManager => 'ইঞ্জিন ম্যানেজার'; - @override String get blunder => 'গুরুতর ভুল'; @@ -1878,7 +2100,7 @@ class AppLocalizationsBn extends AppLocalizations { String get friends => 'বন্ধুরা'; @override - String get otherPlayers => 'other players'; + String get otherPlayers => 'অন্যান্য খেলোয়াড়'; @override String get discussions => 'বার্তাগুলি'; @@ -2063,6 +2285,9 @@ class AppLocalizationsBn extends AppLocalizations { @override String get gamesPlayed => 'খেলা খেলেছেন'; + @override + String get ok => 'OK'; + @override String get cancel => 'বাতিল করুন'; @@ -2715,7 +2940,7 @@ class AppLocalizationsBn extends AppLocalizations { String get website => 'Website'; @override - String get mobile => 'Mobile'; + String get mobile => 'মোবাইল'; @override String get help => 'সাহায্য'; @@ -2772,7 +2997,13 @@ class AppLocalizationsBn extends AppLocalizations { String get other => 'অন্য কোনো কারণ'; @override - String get reportDescriptionHelp => 'এখানে সেই খেলাটির link দেন এবং বলুন ওই ব্যক্তি ব্যবহারে কি অসুবিধা ছিল ?'; + String get reportCheatBoostHelp => 'Paste the link to the game(s) and explain what is wrong about this user\'s behaviour. Don\'t just say \"they cheat\", but tell us how you came to this conclusion.'; + + @override + String get reportUsernameHelp => 'Explain what about this username is offensive. Don\'t just say \"it\'s offensive/inappropriate\", but tell us how you came to this conclusion, especially if the insult is obfuscated, not in english, is in slang, or is a historical/cultural reference.'; + + @override + String get reportProcessedFasterInEnglish => 'Your report will be processed faster if written in English.'; @override String get error_provideOneCheatedGameLink => 'অনুগ্রহ করে একটা চিটেড গেমের লিংক দিন।'; @@ -4077,6 +4308,9 @@ class AppLocalizationsBn extends AppLocalizations { @override String get nothingToSeeHere => 'এই মুহূর্তে এখানে দেখার কিছু নেই.'; + @override + String get stats => 'Stats'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4723,9 +4957,514 @@ class AppLocalizationsBn extends AppLocalizations { @override String get streamerLichessStreamers => 'লিছেসস স্ত্রেয়ামের'; + @override + String get studyPrivate => 'ব্যাক্তিগত'; + + @override + String get studyMyStudies => 'আমার অধ্যায়ন'; + + @override + String get studyStudiesIContributeTo => 'যেসকল অধ্যায়নে আমার অবদান রয়েছে'; + + @override + String get studyMyPublicStudies => 'জনসাধারনকৃত আমার অধ্যায়নগুলো'; + + @override + String get studyMyPrivateStudies => 'আমার ব্যাক্তিগত অধ্যায়ন'; + + @override + String get studyMyFavoriteStudies => 'আমার পছন্দের অধ্যায়ন'; + + @override + String get studyWhatAreStudies => 'অধ্যায়ন কি?'; + + @override + String get studyAllStudies => 'সকল অধ্যায়নগুলি'; + + @override + String studyStudiesCreatedByX(String param) { + return 'অধ্যায়ন তৈরি করেছেন $param'; + } + + @override + String get studyNoneYet => 'আপাতত নেই।'; + + @override + String get studyHot => 'গরমাগরম'; + + @override + String get studyDateAddedNewest => 'তৈরির তারিখ (সবচেয়ে নতুন)'; + + @override + String get studyDateAddedOldest => 'তৈরির তারিখ (সবচেয়ে পুরনো)'; + + @override + String get studyRecentlyUpdated => 'সাম্প্রতিক হালনাগাদকৃত'; + + @override + String get studyMostPopular => 'সবচেয়ে জনপ্রিয়'; + + @override + String get studyAlphabetical => 'বর্ণানুক্রমিক'; + + @override + String get studyAddNewChapter => 'নতুন অধ্যায় যোগ করুন'; + + @override + String get studyAddMembers => 'সদস্য যোগ করুন'; + + @override + String get studyInviteToTheStudy => 'স্টাডিতে আমন্ত্রণ জানান'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'দয়া করে যাদের আপনি জানেন তাদের এবং যারা সক্রিয়ভাবে যোগদান করতে চায়, কেবল তাদেরকেই আমন্ত্রন জানান।'; + + @override + String get studySearchByUsername => 'ইউজারনেম দ্বারা খুঁজুন'; + + @override + String get studySpectator => 'দর্শক'; + + @override + String get studyContributor => 'অবদানকারী'; + + @override + String get studyKick => 'লাথি দিয়ে বের করুন'; + + @override + String get studyLeaveTheStudy => 'Leave the study'; + + @override + String get studyYouAreNowAContributor => 'You are now a contributor'; + + @override + String get studyYouAreNowASpectator => 'আপনি এখন দর্শক'; + + @override + String get studyPgnTags => 'PGN ট্যাগ'; + + @override + String get studyLike => 'পছন্দ করা'; + + @override + String get studyUnlike => 'পছন্দ নয়'; + + @override + String get studyNewTag => 'নতুন ট্যাগ'; + + @override + String get studyCommentThisPosition => 'Comment on this position'; + + @override + String get studyCommentThisMove => 'Comment on this move'; + + @override + String get studyAnnotateWithGlyphs => 'Annotate with glyphs'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'এনালাইসিস করার জন্য চ্যাপ্টারটা খুব ছোট'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'শুধুমাত্র স্টাডি\'টার কন্ট্রিবিউটররাই কম্পিউটার এনালাইসিস এর রিকোয়েস্ট করতে পারবে।'; + + @override + String get studyGetAFullComputerAnalysis => 'Get a full server-side computer analysis of the mainline.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Make sure the chapter is complete. You can only request analysis once.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'All SYNC members remain on the same position'; + + @override + String get studyShareChanges => 'Share changes with spectators and save them on the server'; + + @override + String get studyPlaying => 'খেলছে'; + + @override + String get studyShowEvalBar => 'Evaluation bars'; + + @override + String get studyFirst => 'সর্ব প্রথম'; + + @override + String get studyPrevious => 'আগের ধাপ'; + + @override + String get studyNext => 'পরের ধাপ'; + + @override + String get studyLast => 'সর্বশেষ'; + @override String get studyShareAndExport => 'Share & export'; + @override + String get studyCloneStudy => 'Clone'; + + @override + String get studyStudyPgn => 'অধ্যায়ন PGN আকারে'; + + @override + String get studyDownloadAllGames => 'ডাউনলোড করুন সকল গেম'; + + @override + String get studyChapterPgn => 'Chapter PGN'; + + @override + String get studyCopyChapterPgn => 'Copy PGN'; + + @override + String get studyDownloadGame => 'Download game'; + + @override + String get studyStudyUrl => 'Study URL'; + + @override + String get studyCurrentChapterUrl => 'Current chapter URL'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'You can paste this in the forum or your Lichess blog to embed'; + + @override + String get studyStartAtInitialPosition => 'Start at initial position'; + + @override + String studyStartAtX(String param) { + return 'Start at $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Embed in your website'; + + @override + String get studyReadMoreAboutEmbedding => 'Read more about embedding'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Only public studies can be embedded!'; + + @override + String get studyOpen => 'ওপেন'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1, brought to you by $param2'; + } + + @override + String get studyStudyNotFound => 'Study not found'; + + @override + String get studyEditChapter => 'Edit chapter'; + + @override + String get studyNewChapter => 'New chapter'; + + @override + String studyImportFromChapterX(String param) { + return 'Import from $param'; + } + + @override + String get studyOrientation => 'Orientation'; + + @override + String get studyAnalysisMode => 'Analysis mode'; + + @override + String get studyPinnedChapterComment => 'Pinned chapter comment'; + + @override + String get studySaveChapter => 'Save chapter'; + + @override + String get studyClearAnnotations => 'Clear annotations'; + + @override + String get studyClearVariations => 'Clear variations'; + + @override + String get studyDeleteChapter => 'Delete chapter'; + + @override + String get studyDeleteThisChapter => 'Delete this chapter. There is no going back!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Clear all comments, glyphs and drawn shapes in this chapter'; + + @override + String get studyRightUnderTheBoard => 'Right under the board'; + + @override + String get studyNoPinnedComment => 'None'; + + @override + String get studyNormalAnalysis => 'Normal analysis'; + + @override + String get studyHideNextMoves => 'Hide next moves'; + + @override + String get studyInteractiveLesson => 'Interactive lesson'; + + @override + String studyChapterX(String param) { + return 'Chapter $param'; + } + + @override + String get studyEmpty => 'Empty'; + + @override + String get studyStartFromInitialPosition => 'Start from initial position'; + + @override + String get studyEditor => 'Editor'; + + @override + String get studyStartFromCustomPosition => 'নির্দিষ্ট অবস্থান থেকে শুরু করুন'; + + @override + String get studyLoadAGameByUrl => 'URL থেকে খেলা লোড করুন'; + + @override + String get studyLoadAPositionFromFen => 'FEN থেকে একটি অবস্থান লোড করুন'; + + @override + String get studyLoadAGameFromPgn => 'PGN থেকে খেলা লোড করুন'; + + @override + String get studyAutomatic => 'স্বয়ংক্রিয়'; + + @override + String get studyUrlOfTheGame => 'খেলাগুলোর URL, লাইনপ্রতি একটি'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return '$param1 অথবা $param2 থেকে খেলাসমূহ লোড করুন'; + } + + @override + String get studyCreateChapter => 'অধ্যায় তৈরি করুন'; + + @override + String get studyCreateStudy => 'স্টাডি তৈরি করুন'; + + @override + String get studyEditStudy => 'স্টাডি সম্পাদনা করুন'; + + @override + String get studyVisibility => 'দৃশ্যমানতা'; + + @override + String get studyPublic => 'পাবলিক'; + + @override + String get studyUnlisted => 'প্রাইভেট'; + + @override + String get studyInviteOnly => 'কেবল আমন্ত্রনভিত্তিক'; + + @override + String get studyAllowCloning => 'ক্লোন করার অনুমতি দিন'; + + @override + String get studyNobody => 'কেউ না'; + + @override + String get studyOnlyMe => 'শুধু আমি'; + + @override + String get studyContributors => 'অবদানকারীরা'; + + @override + String get studyMembers => 'সদস্যবৃন্দ'; + + @override + String get studyEveryone => 'সবাই'; + + @override + String get studyEnableSync => 'সাইনক চালু করুন'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'সবাইকে একই অবস্থানে রাখুন'; + + @override + String get studyNoLetPeopleBrowseFreely => 'না: মানুষকে মুক্তভাবে ব্রাউজ করতে দিন'; + + @override + String get studyPinnedStudyComment => 'পিন করা স্টাডি মন্তব্য'; + @override String get studyStart => 'শুরু করুন'; + + @override + String get studySave => 'সংরক্ষন করুন'; + + @override + String get studyClearChat => 'চ্যাট পরিষ্কার করুন'; + + @override + String get studyDeleteTheStudyChatHistory => 'স্টাডি চ্যাটের ইতিহাস মুছে ফেলবেন? এটা কিন্তু ফিরে আসবে না!'; + + @override + String get studyDeleteStudy => 'স্টাডি মুছে ফেলুন'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Delete the entire study? There is no going back! Type the name of the study to confirm: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'আপনি কোথায় এটা চর্চা করবেন?'; + + @override + String get studyGoodMove => 'ভালো চাল'; + + @override + String get studyMistake => 'ভূল চাল'; + + @override + String get studyBrilliantMove => 'অসাধারণ চাল'; + + @override + String get studyBlunder => 'ব্লান্ডার'; + + @override + String get studyInterestingMove => 'আগ্রহোদ্দীপক চাল'; + + @override + String get studyDubiousMove => 'অনিশ্চিত চাল'; + + @override + String get studyOnlyMove => 'একমাত্র সম্ভাব্য চাল'; + + @override + String get studyZugzwang => 'যুগযোয়াং'; + + @override + String get studyEqualPosition => 'সমান অবস্থান'; + + @override + String get studyUnclearPosition => 'অনিশ্চিত অবস্থান'; + + @override + String get studyWhiteIsSlightlyBetter => 'সাদা একটু বেশি ভালো'; + + @override + String get studyBlackIsSlightlyBetter => 'কালো একটু বেশি ভালো'; + + @override + String get studyWhiteIsBetter => 'সাদা ভালো'; + + @override + String get studyBlackIsBetter => 'কালো ভালো'; + + @override + String get studyWhiteIsWinning => 'সাদা জিতছে'; + + @override + String get studyBlackIsWinning => 'কালো জিতছে'; + + @override + String get studyNovelty => 'নোভেল্টি'; + + @override + String get studyDevelopment => 'Development'; + + @override + String get studyInitiative => 'Initiative'; + + @override + String get studyAttack => 'Attack'; + + @override + String get studyCounterplay => 'Counterplay'; + + @override + String get studyTimeTrouble => 'Time trouble'; + + @override + String get studyWithCompensation => 'With compensation'; + + @override + String get studyWithTheIdea => 'With the idea'; + + @override + String get studyNextChapter => 'Next chapter'; + + @override + String get studyPrevChapter => 'Previous chapter'; + + @override + String get studyStudyActions => 'Study actions'; + + @override + String get studyTopics => 'Topics'; + + @override + String get studyMyTopics => 'My topics'; + + @override + String get studyPopularTopics => 'Popular topics'; + + @override + String get studyManageTopics => 'Manage topics'; + + @override + String get studyBack => 'Back'; + + @override + String get studyPlayAgain => 'Play again'; + + @override + String get studyWhatWouldYouPlay => 'What would you play in this position?'; + + @override + String get studyYouCompletedThisLesson => 'Congratulations! You completed this lesson.'; + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$countটি অধ্যায়', + one: '$countটি অধ্যায়', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$countটি খেলা', + one: '$countটি খেলা', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count জন সদস্য', + one: '$count জন সদস্য', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'PGN টেক্সট এখানে পেস্ট করুন, $count টি খেলা পর্যন্ত', + one: 'PGN টেক্সট এখানে পেস্ট করুন, $count টি খেলা পর্যন্ত', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_br.dart b/lib/l10n/l10n_br.dart index 859cba0240..7117adb327 100644 --- a/lib/l10n/l10n_br.dart +++ b/lib/l10n/l10n_br.dart @@ -103,9 +103,6 @@ class AppLocalizationsBr extends AppLocalizations { @override String get mobileCancelTakebackOffer => 'Cancel takeback offer'; - @override - String get mobileCancelDrawOffer => 'Cancel draw offer'; - @override String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @@ -270,6 +267,17 @@ class AppLocalizationsBr extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Completed $count $param2 correspondence games', + one: 'Completed $count $param2 correspondence game', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -399,9 +407,226 @@ class AppLocalizationsBr extends AppLocalizations { @override String get broadcastBroadcasts => 'War-eeun'; + @override + String get broadcastMyBroadcasts => 'My broadcasts'; + @override String get broadcastLiveBroadcasts => 'Tournamantoù skignet war-eeun'; + @override + String get broadcastBroadcastCalendar => 'Broadcast calendar'; + + @override + String get broadcastNewBroadcast => 'Skignañ war-eeun nevez'; + + @override + String get broadcastSubscribedBroadcasts => 'Subscribed broadcasts'; + + @override + String get broadcastAboutBroadcasts => 'About broadcasts'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'How to use Lichess Broadcasts.'; + + @override + String get broadcastTheNewRoundHelp => 'The new round will have the same members and contributors as the previous one.'; + + @override + String get broadcastAddRound => 'Add a round'; + + @override + String get broadcastOngoing => 'O ren'; + + @override + String get broadcastUpcoming => 'A-benn nebeut'; + + @override + String get broadcastCompleted => 'Tremenet'; + + @override + String get broadcastCompletedHelp => 'Lichess detects round completion, but can get it wrong. Use this to set it manually.'; + + @override + String get broadcastRoundName => 'Round name'; + + @override + String get broadcastRoundNumber => 'Niverenn ar batalm'; + + @override + String get broadcastTournamentName => 'Tournament name'; + + @override + String get broadcastTournamentDescription => 'Short tournament description'; + + @override + String get broadcastFullDescription => 'Deskrivadur an abadenn a-bezh'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Deskrivadur hir ar skignañ war-eeun ma fell deoc\'h.$param1 zo dijabl. Ne vo ket hiroc\'h evit $param2 sin.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN Source URL'; + + @override + String get broadcastSourceUrlHelp => 'An URL a ray Lichess ganti evit kaout hizivadurioù ar PGN. Ret eo dezhi bezañ digor d\'an holl war Internet.'; + + @override + String get broadcastSourceGameIds => 'Up to 64 Lichess game IDs, separated by spaces.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Start date in the tournament local timezone: $param'; + } + + @override + String get broadcastStartDateHelp => 'Diret eo, ma ouzit pegoulz e krogo'; + + @override + String get broadcastCurrentGameUrl => 'Current game URL'; + + @override + String get broadcastDownloadAllRounds => 'Download all rounds'; + + @override + String get broadcastResetRound => 'Reset this round'; + + @override + String get broadcastDeleteRound => 'Delete this round'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Definitively delete the round and all its games.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Delete all games of this round. The source will need to be active in order to re-create them.'; + + @override + String get broadcastEditRoundStudy => 'Edit round study'; + + @override + String get broadcastDeleteTournament => 'Dilemel an tournamant-mañ'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Dilemel an tournamant da viken, an holl grogadoù ha pep tra penn-da-benn.'; + + @override + String get broadcastShowScores => 'Show players scores based on game results'; + + @override + String get broadcastReplacePlayerTags => 'Optional: replace player names, ratings and titles'; + + @override + String get broadcastFideFederations => 'FIDE federations'; + + @override + String get broadcastTop10Rating => 'Top 10 rating'; + + @override + String get broadcastFidePlayers => 'FIDE players'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE player not found'; + + @override + String get broadcastFideProfile => 'FIDE profile'; + + @override + String get broadcastFederation => 'Federation'; + + @override + String get broadcastAgeThisYear => 'Age this year'; + + @override + String get broadcastUnrated => 'Unrated'; + + @override + String get broadcastRecentTournaments => 'Recent tournaments'; + + @override + String get broadcastOpenLichess => 'Open in Lichess'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Overview'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Official website'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'Score'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count broadcasts', + one: '$count broadcast', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Daeoù: $param1'; @@ -1442,10 +1667,10 @@ class AppLocalizationsBr extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'The opponent is limited in the moves they can make, and all moves worsen their position.'; @override - String get puzzleThemeHealthyMix => 'A bep seurt'; + String get puzzleThemeMix => 'A bep seurt'; @override - String get puzzleThemeHealthyMixDescription => 'A bep seurt. N\'ouzit ket petra gortoz hag e mod-se e voc\'h prest evit pep tra! Heñvel ouzh ar c\'hrogadoù gwir.'; + String get puzzleThemeMixDescription => 'A bep seurt. N\'ouzit ket petra gortoz hag e mod-se e voc\'h prest evit pep tra! Heñvel ouzh ar c\'hrogadoù gwir.'; @override String get puzzleThemePlayerGames => 'Player games'; @@ -1849,9 +2074,6 @@ class AppLocalizationsBr extends AppLocalizations { @override String get removesTheDepthLimit => 'Removes the depth limit, and keeps your computer warm'; - @override - String get engineManager => 'Merañ an urzhiataer'; - @override String get blunder => 'Bourd'; @@ -2115,6 +2337,9 @@ class AppLocalizationsBr extends AppLocalizations { @override String get gamesPlayed => 'Krogadoù c\'hoariet'; + @override + String get ok => 'OK'; + @override String get cancel => 'Nullañ'; @@ -2824,7 +3049,13 @@ class AppLocalizationsBr extends AppLocalizations { String get other => 'All'; @override - String get reportDescriptionHelp => 'Pegit liamm ar c\'hrogad(où) ha displegit ar pezh a ya a-dreuz gant emzalc\'h oc\'h enebour. Lâret \"o truchañ emañ\" ne vo ket trawalc\'h, ret eo displegañ mat. Buanoc\'h e pledimp ganti ma skrivit e saozneg.'; + String get reportCheatBoostHelp => 'Paste the link to the game(s) and explain what is wrong about this user\'s behaviour. Don\'t just say \"they cheat\", but tell us how you came to this conclusion.'; + + @override + String get reportUsernameHelp => 'Explain what about this username is offensive. Don\'t just say \"it\'s offensive/inappropriate\", but tell us how you came to this conclusion, especially if the insult is obfuscated, not in english, is in slang, or is a historical/cultural reference.'; + + @override + String get reportProcessedFasterInEnglish => 'Your report will be processed faster if written in English.'; @override String get error_provideOneCheatedGameLink => 'Roit d\'an nebeutañ ul liamm hag a gas d\'ur c\'hrogad trucherezh ennañ.'; @@ -4129,6 +4360,9 @@ class AppLocalizationsBr extends AppLocalizations { @override String get nothingToSeeHere => 'Nothing to see here at the moment.'; + @override + String get stats => 'Stats'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4901,9 +5135,526 @@ class AppLocalizationsBr extends AppLocalizations { @override String get streamerLichessStreamers => 'Streamerien Lichess'; + @override + String get studyPrivate => 'Prevez'; + + @override + String get studyMyStudies => 'Ma studiadennoù'; + + @override + String get studyStudiesIContributeTo => 'Studiadennoù am eus kemeret perzh enne'; + + @override + String get studyMyPublicStudies => 'Ma studiadennoù foran'; + + @override + String get studyMyPrivateStudies => 'Ma studiadennoù prevez'; + + @override + String get studyMyFavoriteStudies => 'Ma studiadennoù muiañ-karet'; + + @override + String get studyWhatAreStudies => 'Petra eo ar studiadennoù?'; + + @override + String get studyAllStudies => 'An holl studiadennoù'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Studiadennoù krouet gant $param'; + } + + @override + String get studyNoneYet => 'Hini ebet evit poent.'; + + @override + String get studyHot => 'Deus ar c\'hiz'; + + @override + String get studyDateAddedNewest => 'Deiziad ouzhpennet (nevesañ)'; + + @override + String get studyDateAddedOldest => 'Deiziad ouzhpennet (koshañ)'; + + @override + String get studyRecentlyUpdated => 'Hizivaet a-nevez'; + + @override + String get studyMostPopular => 'Muiañ karet'; + + @override + String get studyAlphabetical => 'Alphabetical'; + + @override + String get studyAddNewChapter => 'Ouzhpennañ ur pennad'; + + @override + String get studyAddMembers => 'Ouzhpennañ izili'; + + @override + String get studyInviteToTheStudy => 'Pediñ d\'ar studiadenn'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Na bedit nemet tud a anavezit hag o deus c\'hoant da gemer perzh da vat en ho studiadenn.'; + + @override + String get studySearchByUsername => 'Klask dre anv implijer'; + + @override + String get studySpectator => 'Arvester'; + + @override + String get studyContributor => 'Perzhiad'; + + @override + String get studyKick => 'Forbannañ'; + + @override + String get studyLeaveTheStudy => 'Kuitaat ar studiadenn'; + + @override + String get studyYouAreNowAContributor => 'Perzhiad oc\'h bremañ'; + + @override + String get studyYouAreNowASpectator => 'Un arvester oc\'h bremañ'; + + @override + String get studyPgnTags => 'Tikedennoù PGN'; + + @override + String get studyLike => 'Plijet'; + + @override + String get studyUnlike => 'Unlike'; + + @override + String get studyNewTag => 'Tikedenn nevez'; + + @override + String get studyCommentThisPosition => 'Lâret ur ger diwar-benn al lakadur-mañ'; + + @override + String get studyCommentThisMove => 'Ober un evezhiadenn diwar-benn ar fiñvadenn-mañ'; + + @override + String get studyAnnotateWithGlyphs => 'Notennaouiñ gant arouezioù'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Re verr eo ar pennad evit bezañ dielfennet.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'N\'eus nemet perzhidi ar studiadenn a c\'hall goulenn un dielfennañ urzhiataer.'; + + @override + String get studyGetAFullComputerAnalysis => 'Kaout un dielfennañ klok eus ar bennlinenn graet gant un urzhiataer.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Bezit sur eo klok ar pennad. Ne c\'hallit goulenn un dielfennañ nemet ur wech.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Er memes lec\'hiadur e chom holl izili ar SYNC'; + + @override + String get studyShareChanges => 'Rannañ cheñchamantoù gant an arvesterien ha saveteiñ anezhe war ar servor'; + + @override + String get studyPlaying => 'O c\'hoari'; + + @override + String get studyShowEvalBar => 'Evaluation bars'; + + @override + String get studyFirst => 'Kentañ'; + + @override + String get studyPrevious => 'War-gil'; + + @override + String get studyNext => 'War-lec\'h'; + + @override + String get studyLast => 'Diwezhañ'; + @override String get studyShareAndExport => 'Skignañ & ezporzhiañ'; + @override + String get studyCloneStudy => 'Eilañ'; + + @override + String get studyStudyPgn => 'PGN ar studi'; + + @override + String get studyDownloadAllGames => 'Pellgargañ an holl grogadoù'; + + @override + String get studyChapterPgn => 'PGN ar pennad'; + + @override + String get studyCopyChapterPgn => 'Copy PGN'; + + @override + String get studyDownloadGame => 'Pellgargañ ur c\'hrogad'; + + @override + String get studyStudyUrl => 'Studiañ URL'; + + @override + String get studyCurrentChapterUrl => 'URL ar pennad evit poent'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Gallout a rit pegañ se er forom evit ensoc\'hañ'; + + @override + String get studyStartAtInitialPosition => 'Kregiñ el lec\'hiadur kentañ'; + + @override + String studyStartAtX(String param) { + return 'Kregiñ e $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Enframmañ en ho lec\'hienn pe blog'; + + @override + String get studyReadMoreAboutEmbedding => 'Goût hiroc\'h diwar-benn an ensoc\'hañ'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Ar studiadennoù foran a c\'hall bezañ ensoc\'het!'; + + @override + String get studyOpen => 'Digeriñ'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1, zo kaset deoc\'h gant $param2'; + } + + @override + String get studyStudyNotFound => 'N\'eo ket bet kavet ar studiadenn'; + + @override + String get studyEditChapter => 'Aozañ ar pennad'; + + @override + String get studyNewChapter => 'Pennad nevez'; + + @override + String studyImportFromChapterX(String param) { + return 'Import from $param'; + } + + @override + String get studyOrientation => 'Tuadur'; + + @override + String get studyAnalysisMode => 'Doare dielfennañ'; + + @override + String get studyPinnedChapterComment => 'Ali war ar pennad spilhet'; + + @override + String get studySaveChapter => 'Saveteiñ pennad'; + + @override + String get studyClearAnnotations => 'Diverkañ an notennoù'; + + @override + String get studyClearVariations => 'Clear variations'; + + @override + String get studyDeleteChapter => 'Dilemel pennad'; + + @override + String get studyDeleteThisChapter => 'Dilemel ar pennad-mañ? Hep distro e vo!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Diverkañ an holl evezhiadennoù ha notennoù er pennad?'; + + @override + String get studyRightUnderTheBoard => 'Dindan an dablez'; + + @override + String get studyNoPinnedComment => 'Hini ebet'; + + @override + String get studyNormalAnalysis => 'Dielfennañ normal'; + + @override + String get studyHideNextMoves => 'Kuzhat ar fiñvadennoù da heul'; + + @override + String get studyInteractiveLesson => 'Kentel etreoberiat'; + + @override + String studyChapterX(String param) { + return 'Pennad $param'; + } + + @override + String get studyEmpty => 'Goullo'; + + @override + String get studyStartFromInitialPosition => 'Kregiñ el lec\'hiadur kentañ'; + + @override + String get studyEditor => 'Aozer'; + + @override + String get studyStartFromCustomPosition => 'Kregiñ adalek ul lakadur aozet'; + + @override + String get studyLoadAGameByUrl => 'Kargañ ur c\'hrogad dre URL'; + + @override + String get studyLoadAPositionFromFen => 'Kargañ ul lakadur dre FEN'; + + @override + String get studyLoadAGameFromPgn => 'Kargañ ul lakadur dre PGN'; + + @override + String get studyAutomatic => 'Emgefre'; + + @override + String get studyUrlOfTheGame => 'URL ar c\'hrogad'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Kargañ ur c\'hrogad eus $param1 pe $param2'; + } + + @override + String get studyCreateChapter => 'Krouiñ pennad'; + + @override + String get studyCreateStudy => 'Krouiñ ur studiadenn'; + + @override + String get studyEditStudy => 'Aozañ studiadenn'; + + @override + String get studyVisibility => 'Gwelusted'; + + @override + String get studyPublic => 'Foran'; + + @override + String get studyUnlisted => 'N\'eo ket bet listennet'; + + @override + String get studyInviteOnly => 'Kouvidi hepken'; + + @override + String get studyAllowCloning => 'Aotreañ ar c\'hlonañ'; + + @override + String get studyNobody => 'Den ebet'; + + @override + String get studyOnlyMe => 'Me hepken'; + + @override + String get studyContributors => 'Perzhidi'; + + @override + String get studyMembers => 'Izili'; + + @override + String get studyEveryone => 'An holl dud'; + + @override + String get studyEnableSync => 'Gweredekaat sync'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Ya: laoskit an traoù evel m\'emaint'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Nann: laoskit an dud merdeiñ trankilik'; + + @override + String get studyPinnedStudyComment => 'Ali war ar studiadenn spilhet'; + @override String get studyStart => 'Kregiñ'; + + @override + String get studySave => 'Saveteiñ'; + + @override + String get studyClearChat => 'Diverkañ ar flapañ'; + + @override + String get studyDeleteTheStudyChatHistory => 'Dilemel an istor-flapañ? Hep distro e vo!'; + + @override + String get studyDeleteStudy => 'Dilemel ar studiadenn'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Delete the entire study? There is no going back! Type the name of the study to confirm: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Pelec\'h ho peus c\'hoant da studiañ se?'; + + @override + String get studyGoodMove => 'Good move'; + + @override + String get studyMistake => 'Mistake'; + + @override + String get studyBrilliantMove => 'Brilliant move'; + + @override + String get studyBlunder => 'Blunder'; + + @override + String get studyInterestingMove => 'Interesting move'; + + @override + String get studyDubiousMove => 'Dubious move'; + + @override + String get studyOnlyMove => 'Only move'; + + @override + String get studyZugzwang => 'Zugzwang'; + + @override + String get studyEqualPosition => 'Equal position'; + + @override + String get studyUnclearPosition => 'Unclear position'; + + @override + String get studyWhiteIsSlightlyBetter => 'White is slightly better'; + + @override + String get studyBlackIsSlightlyBetter => 'Black is slightly better'; + + @override + String get studyWhiteIsBetter => 'White is better'; + + @override + String get studyBlackIsBetter => 'Black is better'; + + @override + String get studyWhiteIsWinning => 'White is winning'; + + @override + String get studyBlackIsWinning => 'Black is winning'; + + @override + String get studyNovelty => 'Novelty'; + + @override + String get studyDevelopment => 'Development'; + + @override + String get studyInitiative => 'Initiative'; + + @override + String get studyAttack => 'Attack'; + + @override + String get studyCounterplay => 'Counterplay'; + + @override + String get studyTimeTrouble => 'Time trouble'; + + @override + String get studyWithCompensation => 'With compensation'; + + @override + String get studyWithTheIdea => 'With the idea'; + + @override + String get studyNextChapter => 'Next chapter'; + + @override + String get studyPrevChapter => 'Previous chapter'; + + @override + String get studyStudyActions => 'Study actions'; + + @override + String get studyTopics => 'Topics'; + + @override + String get studyMyTopics => 'My topics'; + + @override + String get studyPopularTopics => 'Popular topics'; + + @override + String get studyManageTopics => 'Manage topics'; + + @override + String get studyBack => 'Back'; + + @override + String get studyPlayAgain => 'Play again'; + + @override + String get studyWhatWouldYouPlay => 'What would you play in this position?'; + + @override + String get studyYouCompletedThisLesson => 'Congratulations! You completed this lesson.'; + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count pennad', + many: '$count pennad', + few: '$count pennad', + two: '$count pennad', + one: '$count pennad', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count C\'hoariadenn', + many: '$count C\'hoariadenn', + few: '$count C\'hoariadenn', + two: '$count C\'hoariadenn', + one: '$count C\'hoariadenn', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Ezel', + many: '$count Ezel', + few: '$count Ezel', + two: '$count Ezel', + one: '$count Ezel', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Pegit testenn ho PGN amañ, betek $count krogadoù', + many: 'Pegit testenn ho PGN amañ, betek $count krogadoù', + few: 'Pegit testenn ho PGN amañ, betek $count krogadoù', + two: 'Pegit testenn ho PGN amañ, betek $count grogad', + one: 'Pegit testenn ho PGN amañ, betek $count krogad', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_bs.dart b/lib/l10n/l10n_bs.dart index 59c21e3501..92b421d2a0 100644 --- a/lib/l10n/l10n_bs.dart +++ b/lib/l10n/l10n_bs.dart @@ -103,9 +103,6 @@ class AppLocalizationsBs extends AppLocalizations { @override String get mobileCancelTakebackOffer => 'Cancel takeback offer'; - @override - String get mobileCancelDrawOffer => 'Cancel draw offer'; - @override String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @@ -254,6 +251,17 @@ class AppLocalizationsBs extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Completed $count $param2 correspondence games', + one: 'Completed $count $param2 correspondence game', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -365,9 +373,227 @@ class AppLocalizationsBs extends AppLocalizations { @override String get broadcastBroadcasts => 'Emitovanja'; + @override + String get broadcastMyBroadcasts => 'Moja emitiranja'; + @override String get broadcastLiveBroadcasts => 'Prenos turnira uživo'; + @override + String get broadcastBroadcastCalendar => 'Broadcast calendar'; + + @override + String get broadcastNewBroadcast => 'Novo emitovanje uživo'; + + @override + String get broadcastSubscribedBroadcasts => 'Subscribed broadcasts'; + + @override + String get broadcastAboutBroadcasts => 'About broadcasts'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'How to use Lichess Broadcasts.'; + + @override + String get broadcastTheNewRoundHelp => 'The new round will have the same members and contributors as the previous one.'; + + @override + String get broadcastAddRound => 'Dodajte kolo'; + + @override + String get broadcastOngoing => 'U toku'; + + @override + String get broadcastUpcoming => 'Nadolazeći'; + + @override + String get broadcastCompleted => 'Završeno'; + + @override + String get broadcastCompletedHelp => 'Lichess detects round completion, but can get it wrong. Use this to set it manually.'; + + @override + String get broadcastRoundName => 'Ime kola'; + + @override + String get broadcastRoundNumber => 'Zaokružen broj'; + + @override + String get broadcastTournamentName => 'Naziv turnira'; + + @override + String get broadcastTournamentDescription => 'Kratak opis turnira'; + + @override + String get broadcastFullDescription => 'Potpuni opis događaja'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Neobavezni dugi opis događaja koji se emituje. $param1 je dostupan. Dužina mora biti manja od $param2 slova.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN Source URL'; + + @override + String get broadcastSourceUrlHelp => 'Link koji će Lichess koristiti kako bi redovno ažurirao PGN. Mora biti javno dostupan na internetu.'; + + @override + String get broadcastSourceGameIds => 'Up to 64 Lichess game IDs, separated by spaces.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Start date in the tournament local timezone: $param'; + } + + @override + String get broadcastStartDateHelp => 'Neobavezno, ukoliko znate kada počinje događaj'; + + @override + String get broadcastCurrentGameUrl => 'Link za trenutnu partiju'; + + @override + String get broadcastDownloadAllRounds => 'Skinite sve runde'; + + @override + String get broadcastResetRound => 'Ponovo postavite ovo kolo'; + + @override + String get broadcastDeleteRound => 'Izbrišite ovo kolo'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Definitivno izbrišite ovo kolo i partije u njemu.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Izbrišite sve partije iz ovog kola. Izvor mora biti aktivan da biste ih mogli ponovo kreirati.'; + + @override + String get broadcastEditRoundStudy => 'Podesite studiju kola'; + + @override + String get broadcastDeleteTournament => 'Izbrišite ovaj turnir'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Definitivno izbrišite cijeli turnir, sva kola i sve partije.'; + + @override + String get broadcastShowScores => 'Show players scores based on game results'; + + @override + String get broadcastReplacePlayerTags => 'Optional: replace player names, ratings and titles'; + + @override + String get broadcastFideFederations => 'FIDE federations'; + + @override + String get broadcastTop10Rating => 'Top 10 rating'; + + @override + String get broadcastFidePlayers => 'FIDE players'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE player not found'; + + @override + String get broadcastFideProfile => 'FIDE profile'; + + @override + String get broadcastFederation => 'Federation'; + + @override + String get broadcastAgeThisYear => 'Age this year'; + + @override + String get broadcastUnrated => 'Unrated'; + + @override + String get broadcastRecentTournaments => 'Recent tournaments'; + + @override + String get broadcastOpenLichess => 'Open in Lichess'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Overview'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Official website'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'Score'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count emitovanja', + few: '$count emitovanja', + one: '$count emitovanje', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Izazovi: $param1'; @@ -1412,10 +1638,10 @@ class AppLocalizationsBs extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Protivnik ima ograničen broj poteza, a svaki od njih pogoršava mu poziciju.'; @override - String get puzzleThemeHealthyMix => 'Zdrava mješavina'; + String get puzzleThemeMix => 'Zdrava mješavina'; @override - String get puzzleThemeHealthyMixDescription => 'Svega pomalo. Ne znate šta možete očekivati, pa ostajete spremni na sve! Baš kao u pravim partijama.'; + String get puzzleThemeMixDescription => 'Svega pomalo. Ne znate šta možete očekivati, pa ostajete spremni na sve! Baš kao u pravim partijama.'; @override String get puzzleThemePlayerGames => 'Igračke igre'; @@ -1819,9 +2045,6 @@ class AppLocalizationsBs extends AppLocalizations { @override String get removesTheDepthLimit => 'Uklanja granicu do koje računar može analizirati, i održava toplinu računara'; - @override - String get engineManager => 'Upravitelj šahovskog programa'; - @override String get blunder => 'Grubi previd'; @@ -2085,6 +2308,9 @@ class AppLocalizationsBs extends AppLocalizations { @override String get gamesPlayed => 'Odigrane partije'; + @override + String get ok => 'OK'; + @override String get cancel => 'Odustani'; @@ -2794,7 +3020,13 @@ class AppLocalizationsBs extends AppLocalizations { String get other => 'Ostalo'; @override - String get reportDescriptionHelp => 'Zalijepite link na partiju ili partije u pitanju i objasnite što nije bilo u redu sa ponašanjem korisnika. Nemojte samo reći \"varao je\", nego objasnite kako ste došli do tog zaključka. Vaša prijava će biti brže obrađena ukoliko je napišete na engleskom jeziku.'; + String get reportCheatBoostHelp => 'Paste the link to the game(s) and explain what is wrong about this user\'s behaviour. Don\'t just say \"they cheat\", but tell us how you came to this conclusion.'; + + @override + String get reportUsernameHelp => 'Explain what about this username is offensive. Don\'t just say \"it\'s offensive/inappropriate\", but tell us how you came to this conclusion, especially if the insult is obfuscated, not in english, is in slang, or is a historical/cultural reference.'; + + @override + String get reportProcessedFasterInEnglish => 'Your report will be processed faster if written in English.'; @override String get error_provideOneCheatedGameLink => 'Molimo navedite barem jedan link na partiju u kojoj je igrač varao.'; @@ -4099,6 +4331,9 @@ class AppLocalizationsBs extends AppLocalizations { @override String get nothingToSeeHere => 'Nothing to see here at the moment.'; + @override + String get stats => 'Stats'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4789,9 +5024,518 @@ class AppLocalizationsBs extends AppLocalizations { @override String get streamerLichessStreamers => 'Lichess emiteri'; + @override + String get studyPrivate => 'Privatna'; + + @override + String get studyMyStudies => 'Moje studije'; + + @override + String get studyStudiesIContributeTo => 'Studije kojima doprinosim'; + + @override + String get studyMyPublicStudies => 'Moje javne studije'; + + @override + String get studyMyPrivateStudies => 'Moje privatne studije'; + + @override + String get studyMyFavoriteStudies => 'Moje omiljene studije'; + + @override + String get studyWhatAreStudies => 'Šta su studije?'; + + @override + String get studyAllStudies => 'Sve studije'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Studije koje je kreirao/la $param'; + } + + @override + String get studyNoneYet => 'Još nijedna.'; + + @override + String get studyHot => 'U trendu'; + + @override + String get studyDateAddedNewest => 'Datum dodavanja (najnovije)'; + + @override + String get studyDateAddedOldest => 'Datum dodavanja (najstarije)'; + + @override + String get studyRecentlyUpdated => 'Nedavno ažurirane'; + + @override + String get studyMostPopular => 'Najpopularnije'; + + @override + String get studyAlphabetical => 'Abecedno'; + + @override + String get studyAddNewChapter => 'Dodajte novo poglavlje'; + + @override + String get studyAddMembers => 'Dodajte članove'; + + @override + String get studyInviteToTheStudy => 'Pozovite na studiju'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Molimo Vas da pozovete samo ljude koje znate i koji su zainteresovani da aktivno učustvuju u ovoj studiji.'; + + @override + String get studySearchByUsername => 'Pretraga prema korisničkom imenu'; + + @override + String get studySpectator => 'Posmatrač'; + + @override + String get studyContributor => 'Saradnik'; + + @override + String get studyKick => 'Izbaci'; + + @override + String get studyLeaveTheStudy => 'Napustite studiju'; + + @override + String get studyYouAreNowAContributor => 'Sada ste saradnik'; + + @override + String get studyYouAreNowASpectator => 'Sada ste posmatrač'; + + @override + String get studyPgnTags => 'PGN oznake'; + + @override + String get studyLike => 'Sviđa mi se'; + + @override + String get studyUnlike => 'Ne sviđa mi se'; + + @override + String get studyNewTag => 'Nova oznaka'; + + @override + String get studyCommentThisPosition => 'Komentirajte ovu poziciju'; + + @override + String get studyCommentThisMove => 'Komentirajte ovaj potez'; + + @override + String get studyAnnotateWithGlyphs => 'Obilježite poteze simbolima'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Poglavlje je prekratko za analizu.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Samo saradnici u studiji mogu zahtijevati računarsku analizu.'; + + @override + String get studyGetAFullComputerAnalysis => 'Dobijte potpunu serversku analizu glavne varijacije.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Budite sigurni da je poglavlje gotovo. Računarsku analizu možete zahtjevati samo jednom.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Svi sinhronizovani članovi ostaju na istoj poziciji'; + + @override + String get studyShareChanges => 'Podijelite promjene sa posmatračima i sačuvajte ih na server'; + + @override + String get studyPlaying => 'U toku'; + + @override + String get studyShowEvalBar => 'Evaluacijske trake'; + + @override + String get studyFirst => 'Prva strana'; + + @override + String get studyPrevious => 'Prethodna strana'; + + @override + String get studyNext => 'Sljedeća strana'; + + @override + String get studyLast => 'Posljednja strana'; + @override String get studyShareAndExport => 'Podijelite i izvezite'; + @override + String get studyCloneStudy => 'Klonirajte'; + + @override + String get studyStudyPgn => 'Studirajte PGN'; + + @override + String get studyDownloadAllGames => 'Skinite sve partije'; + + @override + String get studyChapterPgn => 'PGN poglavlja'; + + @override + String get studyCopyChapterPgn => 'Kopirajte PGN'; + + @override + String get studyDownloadGame => 'Skini partiju'; + + @override + String get studyStudyUrl => 'Link studije'; + + @override + String get studyCurrentChapterUrl => 'Link trenutnog poglavlja'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Možete ovo zalijepiti na forumu ili Vašem blogu na Lichessu kako biste ugradili poglavlje'; + + @override + String get studyStartAtInitialPosition => 'Krenite sa inicijalnom pozicijom'; + + @override + String studyStartAtX(String param) { + return 'Krenite sa $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Ugradite na Vaš sajt'; + + @override + String get studyReadMoreAboutEmbedding => 'Pročitajte više o ugrađivanju'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Samo javne studije mogu biti ugrađene!'; + + @override + String get studyOpen => 'Otvorite'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1 vam je donio $param2'; + } + + @override + String get studyStudyNotFound => 'Studija nije pronađena'; + + @override + String get studyEditChapter => 'Uredite poglavlje'; + + @override + String get studyNewChapter => 'Novo poglavlje'; + + @override + String studyImportFromChapterX(String param) { + return 'Uvezite iz $param'; + } + + @override + String get studyOrientation => 'Orijentacija'; + + @override + String get studyAnalysisMode => 'Tip analize'; + + @override + String get studyPinnedChapterComment => 'Stalni komentar poglavlja'; + + @override + String get studySaveChapter => 'Sačuvajte poglavlje'; + + @override + String get studyClearAnnotations => 'Izbrišite bilješke'; + + @override + String get studyClearVariations => 'Ukloni varijante'; + + @override + String get studyDeleteChapter => 'Izbrišite poglavlje'; + + @override + String get studyDeleteThisChapter => 'Da li želite izbrisati ovo poglavlje? Nakon ove akcije, poglavlje se ne može vratiti!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Da li želite izbrisati sve komentare, simbole i nacrtane oblike u ovom poglavlju?'; + + @override + String get studyRightUnderTheBoard => 'Odmah ispod ploče'; + + @override + String get studyNoPinnedComment => 'Nijedan'; + + @override + String get studyNormalAnalysis => 'Normalna analiza'; + + @override + String get studyHideNextMoves => 'Sakrijte sljedeće poteze'; + + @override + String get studyInteractiveLesson => 'Interaktivna lekcija'; + + @override + String studyChapterX(String param) { + return 'Poglavlje $param'; + } + + @override + String get studyEmpty => 'Prazno'; + + @override + String get studyStartFromInitialPosition => 'Krenite sa inicijalnom pozicijom'; + + @override + String get studyEditor => 'Uređivač'; + + @override + String get studyStartFromCustomPosition => 'Krenite sa željenom pozicijom'; + + @override + String get studyLoadAGameByUrl => 'Učitajte partiju pomoću linka'; + + @override + String get studyLoadAPositionFromFen => 'Učitajte partiju pomoću FEN koda'; + + @override + String get studyLoadAGameFromPgn => 'Učitajte partiju pomoću PGN formata'; + + @override + String get studyAutomatic => 'Automatska'; + + @override + String get studyUrlOfTheGame => 'Link partije'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Učitajte partiju sa $param1 ili $param2'; + } + + @override + String get studyCreateChapter => 'Kreirajte poglavlje'; + + @override + String get studyCreateStudy => 'Kreirajte studiju'; + + @override + String get studyEditStudy => 'Uredite studiju'; + + @override + String get studyVisibility => 'Vidljivost'; + + @override + String get studyPublic => 'Javna'; + + @override + String get studyUnlisted => 'Neizlistane'; + + @override + String get studyInviteOnly => 'Samo po pozivu'; + + @override + String get studyAllowCloning => 'Dozvolite kloniranje'; + + @override + String get studyNobody => 'Niko'; + + @override + String get studyOnlyMe => 'Samo ja'; + + @override + String get studyContributors => 'Saradnici'; + + @override + String get studyMembers => 'Članovi'; + + @override + String get studyEveryone => 'Svi'; + + @override + String get studyEnableSync => 'Omogućite sinhronizaciju'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Da: zadržite sve na istoj poziciji'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Ne: Dozvolite ljudima da slobodno pregledaju'; + + @override + String get studyPinnedStudyComment => 'Stalni komentar studije'; + @override String get studyStart => 'Pokreni'; + + @override + String get studySave => 'Sačuvaj'; + + @override + String get studyClearChat => 'Izbrišite dopisivanje'; + + @override + String get studyDeleteTheStudyChatHistory => 'Da li želite izbrisati svo dopisivanje vezano za ovu studiju? Nakon ove akcije, obrisani tekst se ne može vratiti!'; + + @override + String get studyDeleteStudy => 'Izbrišite studiju'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Izbrisati cijelu studiju? Nema povratka! Ukucajte naziv studije da potvrdite: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Gdje želite da ovu poziciju prostudirate?'; + + @override + String get studyGoodMove => 'Dobar potez'; + + @override + String get studyMistake => 'Greška'; + + @override + String get studyBrilliantMove => 'Briljantan potez'; + + @override + String get studyBlunder => 'Grubi previd'; + + @override + String get studyInterestingMove => 'Zanimljiv potez'; + + @override + String get studyDubiousMove => 'Sumnjiv potez'; + + @override + String get studyOnlyMove => 'Jedini potez'; + + @override + String get studyZugzwang => 'Iznudica'; + + @override + String get studyEqualPosition => 'Jednaka pozicija'; + + @override + String get studyUnclearPosition => 'Nejasna pozicija'; + + @override + String get studyWhiteIsSlightlyBetter => 'Bijeli je u blagoj prednosti'; + + @override + String get studyBlackIsSlightlyBetter => 'Crni je u blagoj prednosti'; + + @override + String get studyWhiteIsBetter => 'Bijeli je bolji'; + + @override + String get studyBlackIsBetter => 'Crni je bolji'; + + @override + String get studyWhiteIsWinning => 'Bijeli dobija'; + + @override + String get studyBlackIsWinning => 'Crni dobija'; + + @override + String get studyNovelty => 'Nov potez'; + + @override + String get studyDevelopment => 'Razvoj'; + + @override + String get studyInitiative => 'Inicijativa'; + + @override + String get studyAttack => 'Napad'; + + @override + String get studyCounterplay => 'Protivnapad'; + + @override + String get studyTimeTrouble => 'Cajtnot'; + + @override + String get studyWithCompensation => 'S kompenzacijom'; + + @override + String get studyWithTheIdea => 'S idejom'; + + @override + String get studyNextChapter => 'Sljedeće poglavlje'; + + @override + String get studyPrevChapter => 'Prethodno poglavlje'; + + @override + String get studyStudyActions => 'Opcije za studiju'; + + @override + String get studyTopics => 'Teme'; + + @override + String get studyMyTopics => 'Moje teme'; + + @override + String get studyPopularTopics => 'Popularne teme'; + + @override + String get studyManageTopics => 'Upravljajte temama'; + + @override + String get studyBack => 'Nazad'; + + @override + String get studyPlayAgain => 'Igrajte ponovo'; + + @override + String get studyWhatWouldYouPlay => 'Šta biste odigrali u ovoj poziciji?'; + + @override + String get studyYouCompletedThisLesson => 'Čestitamo! Kompletirali ste ovu lekciju.'; + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Poglavlja', + few: '$count Poglavlja', + one: '$count Poglavlje', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Partija', + few: '$count Partije', + one: '$count Partija', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Članova', + few: '$count Člana', + one: '$count Član', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Ovdje zalijepite svoj PGN tekst, do $count partija', + few: 'Ovdje zalijepite svoj PGN tekst, do $count partije', + one: 'Ovdje zalijepite svoj PGN tekst, do $count partije', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_ca.dart b/lib/l10n/l10n_ca.dart index da53e6374b..9b0e1864bb 100644 --- a/lib/l10n/l10n_ca.dart +++ b/lib/l10n/l10n_ca.dart @@ -103,9 +103,6 @@ class AppLocalizationsCa extends AppLocalizations { @override String get mobileCancelTakebackOffer => 'Anul·la la petició per desfer la jugada'; - @override - String get mobileCancelDrawOffer => 'Anul·la la petició de taules'; - @override String get mobileWaitingForOpponentToJoin => 'Esperant que s\'uneixi l\'adversari...'; @@ -246,6 +243,17 @@ class AppLocalizationsCa extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Completed $count $param2 correspondence games', + one: 'Completed $count $param2 correspondence game', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,226 @@ class AppLocalizationsCa extends AppLocalizations { @override String get broadcastBroadcasts => 'Retransmissions'; + @override + String get broadcastMyBroadcasts => 'Les meves retransmissions'; + @override String get broadcastLiveBroadcasts => 'Retransmissions de tornejos en directe'; + @override + String get broadcastBroadcastCalendar => 'Calendari de retransmissions'; + + @override + String get broadcastNewBroadcast => 'Nova retransmissió en directe'; + + @override + String get broadcastSubscribedBroadcasts => 'Emissions que segueixo'; + + @override + String get broadcastAboutBroadcasts => 'Sobre les retransmissions'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Com utilitzar les retransmissions de Lichess.'; + + @override + String get broadcastTheNewRoundHelp => 'La nova ronda tindrà els mateixos membres i contribuïdors que l\'anterior.'; + + @override + String get broadcastAddRound => 'Afegir una ronda'; + + @override + String get broadcastOngoing => 'En curs'; + + @override + String get broadcastUpcoming => 'Properes'; + + @override + String get broadcastCompleted => 'Acabada'; + + @override + String get broadcastCompletedHelp => 'Lichess detecta el final de la ronda en funció de les partides de l\'origen. Utilitzeu aquesta opció si no hi ha origen.'; + + @override + String get broadcastRoundName => 'Nom de ronda'; + + @override + String get broadcastRoundNumber => 'Ronda número'; + + @override + String get broadcastTournamentName => 'Nom del torneig'; + + @override + String get broadcastTournamentDescription => 'Breu descripció del torneig'; + + @override + String get broadcastFullDescription => 'Descripció total de l\'esdeveniment'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Opció de llarga descripció de l\'esdeveniment. $param1 és disponible. Ha de tenir menys de $param2 lletres.'; + } + + @override + String get broadcastSourceSingleUrl => 'URL origen del PGN'; + + @override + String get broadcastSourceUrlHelp => 'URL que Lichess comprovarà per a obtenir actualitzacions PGN. Ha de ser públicament accessible des d\'Internet.'; + + @override + String get broadcastSourceGameIds => 'Fins a 64 identificadors de partides de Lichess, separades per espais.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Dia d\'inici a la zona horari del torneig: $param'; + } + + @override + String get broadcastStartDateHelp => 'Opcional, si saps quan comença l\'esdeveniment'; + + @override + String get broadcastCurrentGameUrl => 'URL actual de joc'; + + @override + String get broadcastDownloadAllRounds => 'Baixa totes les rondes'; + + @override + String get broadcastResetRound => 'Restablir aquesta ronda'; + + @override + String get broadcastDeleteRound => 'Eliminar aquesta ronda'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Eliminar definitivament la ronda i les seves partides.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Eliminar totes les partides d\'aquesta ronda. L\'origen ha d\'estar actiu per a recrear-les.'; + + @override + String get broadcastEditRoundStudy => 'Edita l\'estudi de la ronda'; + + @override + String get broadcastDeleteTournament => 'Elimina aquest torneig'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Elimina el torneig de forma definitiva, amb totes les seves rondes i les seves partides.'; + + @override + String get broadcastShowScores => 'Mostra les puntuacions dels jugadors en funció dels resultats de les partides'; + + @override + String get broadcastReplacePlayerTags => 'Opcional: Reemplaça noms dels jugadors, puntuacions i títols'; + + @override + String get broadcastFideFederations => 'Federacions FIDE'; + + @override + String get broadcastTop10Rating => 'Top 10 Ràting'; + + @override + String get broadcastFidePlayers => 'Jugadors FIDE'; + + @override + String get broadcastFidePlayerNotFound => 'No s\'ha trobat el jugador FIDE'; + + @override + String get broadcastFideProfile => 'Perfil FIDE'; + + @override + String get broadcastFederation => 'Federació'; + + @override + String get broadcastAgeThisYear => 'Edat aquest any'; + + @override + String get broadcastUnrated => 'Sense avaluació'; + + @override + String get broadcastRecentTournaments => 'Tornejos recents'; + + @override + String get broadcastOpenLichess => 'Obre a Lichess'; + + @override + String get broadcastTeams => 'Equips'; + + @override + String get broadcastBoards => 'Taulers'; + + @override + String get broadcastOverview => 'Visió general'; + + @override + String get broadcastSubscribeTitle => 'Subscriviu-vos per ser notificats quan comença cada ronda. Podeu activar/desactivara la campana o modificar les notificacions push a les preferències del vostre compte.'; + + @override + String get broadcastUploadImage => 'Puja una imatge del torneig'; + + @override + String get broadcastNoBoardsYet => 'Encara no hi ha taulers. Apareixeran en el moment que es carreguin les partides.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Els taulers es poden carregar per codi o a través de $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Començar a les $param'; + } + + @override + String get broadcastStartVerySoon => 'La retransmissió començarà aviat.'; + + @override + String get broadcastNotYetStarted => 'La retransmissió encara no ha començat.'; + + @override + String get broadcastOfficialWebsite => 'Lloc web oficial'; + + @override + String get broadcastStandings => 'Classificació'; + + @override + String broadcastIframeHelp(String param) { + return 'Més opcions a la $param'; + } + + @override + String get broadcastWebmastersPage => 'pàgina d\'administració'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'Un origen públic en PGN públic en temps real d\'aquesta ronda. També oferim un $param per una sincronització més ràpida i eficient.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Incrusta aquesta retransmissió al vostre lloc web'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Incrusta $param al vostre lloc web'; + } + + @override + String get broadcastRatingDiff => 'Diferència puntuació'; + + @override + String get broadcastGamesThisTournament => 'Partides en aquest torneig'; + + @override + String get broadcastScore => 'Puntuació'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count retransmissions', + one: '$count retransmissió', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Desafiaments: $param1'; @@ -1390,10 +1615,10 @@ class AppLocalizationsCa extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'El rival té els moviments limitats i cada jugada empitjora la seva posició.'; @override - String get puzzleThemeHealthyMix => 'Una mica de cada'; + String get puzzleThemeMix => 'Una mica de cada'; @override - String get puzzleThemeHealthyMixDescription => 'Una mica de tot. No sabràs el que t\'espera, així doncs estigues alerta pel que sigui! Igual que a les partides de veritat.'; + String get puzzleThemeMixDescription => 'Una mica de tot. No sabràs el que t\'espera, així doncs estigues alerta pel que sigui! Igual que a les partides de veritat.'; @override String get puzzleThemePlayerGames => 'Partides de jugadors'; @@ -1797,9 +2022,6 @@ class AppLocalizationsCa extends AppLocalizations { @override String get removesTheDepthLimit => 'Treu el límit de profunditat i escalfa el teu ordinador'; - @override - String get engineManager => 'Gestió del mòdul'; - @override String get blunder => 'Errada greu'; @@ -2063,6 +2285,9 @@ class AppLocalizationsCa extends AppLocalizations { @override String get gamesPlayed => 'Partides jugades'; + @override + String get ok => 'OK'; + @override String get cancel => 'Cancel·lar'; @@ -2772,7 +2997,13 @@ class AppLocalizationsCa extends AppLocalizations { String get other => 'Altres'; @override - String get reportDescriptionHelp => 'Enganxa l\'enllaç de la partida (o partides) i explica el comportament negatiu d\'aquest usuari. No et limitis a dir que \"fa trampes\", i explica com has arribat a aquesta conclusió. El teu informe serà processat més ràpidament si l\'escrius en anglès.'; + String get reportCheatBoostHelp => 'Enganxa l\'enllaç de la partida (o partides) i explica quin és el problema amb el comportament d\'aquest usuari. No et limitis a dir que \"fa trampes\", explica\'ns com has arribat a aquesta conclusió.'; + + @override + String get reportUsernameHelp => 'Explica quin és el comportament ofensiu d\'aquest usuari. No et limitis a dir simplement \"és ofensiu/inapropiat\", explica\'ns com has arribat a aquesta conclusió, especialment si l\'insult està ofuscat, en un idioma diferent de l\'anglès, és un barbarisme o és una referència històrica o cultural.'; + + @override + String get reportProcessedFasterInEnglish => 'El teu informe serà tractat més ràpidament si està escrit en anglès.'; @override String get error_provideOneCheatedGameLink => 'Si us plau, proporcioneu com a mínim un enllaç a un joc on s\'han fet trampes.'; @@ -4077,6 +4308,9 @@ class AppLocalizationsCa extends AppLocalizations { @override String get nothingToSeeHere => 'Res a veure per aquí de moment.'; + @override + String get stats => 'Estadístiques'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4723,9 +4957,514 @@ class AppLocalizationsCa extends AppLocalizations { @override String get streamerLichessStreamers => 'Retransmissors de Lichess'; + @override + String get studyPrivate => 'Privat'; + + @override + String get studyMyStudies => 'Els meus estudis'; + + @override + String get studyStudiesIContributeTo => 'Estudis on jo hi contribueixo'; + + @override + String get studyMyPublicStudies => 'Els meus estudis públics'; + + @override + String get studyMyPrivateStudies => 'Els meus estudis privats'; + + @override + String get studyMyFavoriteStudies => 'Els meus estudis favorits'; + + @override + String get studyWhatAreStudies => 'Què són els estudis?'; + + @override + String get studyAllStudies => 'Tots els estudis'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Estudis creats per $param'; + } + + @override + String get studyNoneYet => 'Res encara.'; + + @override + String get studyHot => 'Candent'; + + @override + String get studyDateAddedNewest => 'Data d’inclusió (més nous)'; + + @override + String get studyDateAddedOldest => 'Data d’inclusió (més antics)'; + + @override + String get studyRecentlyUpdated => 'Actualitzat darrerament'; + + @override + String get studyMostPopular => 'Més popular'; + + @override + String get studyAlphabetical => 'Alfabètic'; + + @override + String get studyAddNewChapter => 'Afegir un nou capítol'; + + @override + String get studyAddMembers => 'Afegeix membres'; + + @override + String get studyInviteToTheStudy => 'Convida a l’estudi'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Si us plau, convida gent que coneixes, i que vólen unir-se activament a l’estudi.'; + + @override + String get studySearchByUsername => 'Cerca per nom d\'usuari'; + + @override + String get studySpectator => 'Espectador'; + + @override + String get studyContributor => 'Contribuïdor'; + + @override + String get studyKick => 'Expulsa'; + + @override + String get studyLeaveTheStudy => 'Deixar l’estudi'; + + @override + String get studyYouAreNowAContributor => 'Ara ets un contribuïdor'; + + @override + String get studyYouAreNowASpectator => 'Actualment ets un espectador'; + + @override + String get studyPgnTags => 'Etiquetes PGN'; + + @override + String get studyLike => 'M’agrada'; + + @override + String get studyUnlike => 'Ja no m\'agrada'; + + @override + String get studyNewTag => 'Nova etiqueta'; + + @override + String get studyCommentThisPosition => 'Comentar en aquesta posició'; + + @override + String get studyCommentThisMove => 'Comentar en aquest moviment'; + + @override + String get studyAnnotateWithGlyphs => 'Anotar amb signes'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'El capítol és massa curt per ser analitzat.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Només els contribuïdors de l’estudi poden demanar un anàlisis computeritzat.'; + + @override + String get studyGetAFullComputerAnalysis => 'Obté un anàlisi complert desde el servidor de la línia principal.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Segura’t que el capítol és complert. Només pots requerir l’anàlisi una sola vegada.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Tots els membres sincronitzats es mantenen a la mateixa posició'; + + @override + String get studyShareChanges => 'Comparteix els canvis amb els espectadors i guarda’ls al servidor'; + + @override + String get studyPlaying => 'Jugant'; + + @override + String get studyShowEvalBar => 'Barres d\'avaluació'; + + @override + String get studyFirst => 'Primer'; + + @override + String get studyPrevious => 'Anterior'; + + @override + String get studyNext => 'Següent'; + + @override + String get studyLast => 'Últim'; + @override String get studyShareAndExport => 'Comparteix i exporta'; + @override + String get studyCloneStudy => 'Clona'; + + @override + String get studyStudyPgn => 'PGN de l’estudi'; + + @override + String get studyDownloadAllGames => 'Descarrega tots els jocs'; + + @override + String get studyChapterPgn => 'PGN del capítol'; + + @override + String get studyCopyChapterPgn => 'Copiar PGN'; + + @override + String get studyDownloadGame => 'Descarrega partida'; + + @override + String get studyStudyUrl => 'URL de l’estudi'; + + @override + String get studyCurrentChapterUrl => 'URL del capítol actual'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Pots enganxar això en el forum per insertar'; + + @override + String get studyStartAtInitialPosition => 'Comnçar a la posició inicial'; + + @override + String studyStartAtX(String param) { + return 'Començar a $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Inserta en la teva web o blog'; + + @override + String get studyReadMoreAboutEmbedding => 'Llegeix més sobre insertar'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Només els estudis públics poden ser inserits!'; + + @override + String get studyOpen => 'Obrir'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1, presentat per $param2'; + } + + @override + String get studyStudyNotFound => 'Estudi no trobat'; + + @override + String get studyEditChapter => 'Editar capítol'; + + @override + String get studyNewChapter => 'Nou capítol'; + + @override + String studyImportFromChapterX(String param) { + return 'Importar de $param'; + } + + @override + String get studyOrientation => 'Orientaciò'; + + @override + String get studyAnalysisMode => 'Mode d\'anàlisi'; + + @override + String get studyPinnedChapterComment => 'Comentari del capítol fixat'; + + @override + String get studySaveChapter => 'Guarda el capítol'; + + @override + String get studyClearAnnotations => 'Netejar anotacions'; + + @override + String get studyClearVariations => 'Netejar variacions'; + + @override + String get studyDeleteChapter => 'Eliminar capítol'; + + @override + String get studyDeleteThisChapter => 'Eliminar aquest capítol? No hi ha volta enrera!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Esborrar tots els comentaris, signes i marques en aquest capítol?'; + + @override + String get studyRightUnderTheBoard => 'Just a sota el tauler'; + + @override + String get studyNoPinnedComment => 'Cap'; + + @override + String get studyNormalAnalysis => 'Análisis normal'; + + @override + String get studyHideNextMoves => 'Oculta els següents moviments'; + + @override + String get studyInteractiveLesson => 'Lliçó interactiva'; + + @override + String studyChapterX(String param) { + return 'Capítol $param'; + } + + @override + String get studyEmpty => 'Buit'; + + @override + String get studyStartFromInitialPosition => 'Començar a la posició inicial'; + + @override + String get studyEditor => 'Editor'; + + @override + String get studyStartFromCustomPosition => 'Començar a una posició personalitzada'; + + @override + String get studyLoadAGameByUrl => 'Carregar una partida desde una URL'; + + @override + String get studyLoadAPositionFromFen => 'Carregar una posició via codi FEN'; + + @override + String get studyLoadAGameFromPgn => 'Carregar una partida PGN'; + + @override + String get studyAutomatic => 'Automàtic'; + + @override + String get studyUrlOfTheGame => 'URL del joc'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Carregar una partida desde $param1 o $param2'; + } + + @override + String get studyCreateChapter => 'Crear capítol'; + + @override + String get studyCreateStudy => 'Crear estudi'; + + @override + String get studyEditStudy => 'Editar estudi'; + + @override + String get studyVisibility => 'Visibilitat'; + + @override + String get studyPublic => 'Públic'; + + @override + String get studyUnlisted => 'No llistats'; + + @override + String get studyInviteOnly => 'Només per invitació'; + + @override + String get studyAllowCloning => 'Permitir clonat'; + + @override + String get studyNobody => 'Ningú'; + + @override + String get studyOnlyMe => 'Només jo'; + + @override + String get studyContributors => 'Col·laboradors'; + + @override + String get studyMembers => 'Membres'; + + @override + String get studyEveryone => 'Tothom'; + + @override + String get studyEnableSync => 'Habilita la sincronització'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Sí: tothom veu la mateixa posició'; + + @override + String get studyNoLetPeopleBrowseFreely => 'No: permetre que la gent navegui lliurement'; + + @override + String get studyPinnedStudyComment => 'Comentar estudi fixat'; + @override String get studyStart => 'Inici'; + + @override + String get studySave => 'Desa'; + + @override + String get studyClearChat => 'Neteja el Chat'; + + @override + String get studyDeleteTheStudyChatHistory => 'Eliminar el xat de l’estudi? No hi ha volta enrera!'; + + @override + String get studyDeleteStudy => 'Eliminar estudi'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Esteu segurs que voleu eliminar el estudi? Tingues en compte que no es pot desfer. Per a confirmar-ho escriu el nom del estudi: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'A on vols estudiar-ho?'; + + @override + String get studyGoodMove => 'Bona jugada'; + + @override + String get studyMistake => 'Errada'; + + @override + String get studyBrilliantMove => 'Jugada brillant'; + + @override + String get studyBlunder => 'Error greu'; + + @override + String get studyInterestingMove => 'Jugada interessant'; + + @override + String get studyDubiousMove => 'Jugada dubtosa'; + + @override + String get studyOnlyMove => 'Única jugada'; + + @override + String get studyZugzwang => 'Zugzwang (atzucac)'; + + @override + String get studyEqualPosition => 'Posició igualada'; + + @override + String get studyUnclearPosition => 'Posició poc clara'; + + @override + String get studyWhiteIsSlightlyBetter => 'El blanc està lleugerament millor'; + + @override + String get studyBlackIsSlightlyBetter => 'El negre està lleugerament millor'; + + @override + String get studyWhiteIsBetter => 'El blanc està millor'; + + @override + String get studyBlackIsBetter => 'El negre està millor'; + + @override + String get studyWhiteIsWinning => 'El blanc està guanyant'; + + @override + String get studyBlackIsWinning => 'El negre està guanyant'; + + @override + String get studyNovelty => 'Novetat'; + + @override + String get studyDevelopment => 'Desenvolupament'; + + @override + String get studyInitiative => 'Iniciativa'; + + @override + String get studyAttack => 'Atac'; + + @override + String get studyCounterplay => 'Contra atac'; + + @override + String get studyTimeTrouble => 'Problema de temps'; + + @override + String get studyWithCompensation => 'Amb compensació'; + + @override + String get studyWithTheIdea => 'Amb la idea'; + + @override + String get studyNextChapter => 'Capítol següent'; + + @override + String get studyPrevChapter => 'Capítol Anterior'; + + @override + String get studyStudyActions => 'Acions de l\'estudi'; + + @override + String get studyTopics => 'Temes'; + + @override + String get studyMyTopics => 'Els meus temes'; + + @override + String get studyPopularTopics => 'Temes populars'; + + @override + String get studyManageTopics => 'Gestiona els temes'; + + @override + String get studyBack => 'Enrere'; + + @override + String get studyPlayAgain => 'Torna a jugar'; + + @override + String get studyWhatWouldYouPlay => 'Que jugaríeu en aquesta posició?'; + + @override + String get studyYouCompletedThisLesson => 'Enhorabona, heu completat aquesta lliçó.'; + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Capítols', + one: '$count Capítol', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Jocs', + one: '$count Joc', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Membres', + one: '$count Membre', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Enganxa el teu PGN aquí, fins a $count partides', + one: 'Enganxa el teu PGN aquí, fins a $count partida', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_cs.dart b/lib/l10n/l10n_cs.dart index eaef3e5109..311fce67bd 100644 --- a/lib/l10n/l10n_cs.dart +++ b/lib/l10n/l10n_cs.dart @@ -54,92 +54,89 @@ class AppLocalizationsCs extends AppLocalizations { String get mobileRecentSearches => 'Recent searches'; @override - String get mobileClearButton => 'Clear'; + String get mobileClearButton => 'Vymazat'; @override String mobilePlayersMatchingSearchTerm(String param) { - return 'Players with \"$param\"'; + return 'Hráči s \"$param\"'; } @override - String get mobileNoSearchResults => 'No results'; + String get mobileNoSearchResults => 'Žádné výsledky'; @override - String get mobileAreYouSure => 'Are you sure?'; + String get mobileAreYouSure => 'Jste si jistý?'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; + String get mobilePuzzleStreakAbortWarning => 'Ztratíte aktuální sérii a vaše skóre bude uloženo.'; @override - String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; + String get mobilePuzzleStormNothingToShow => 'Nic k zobrazení. Zahrajte si nějaké běhy Bouřky úloh.'; @override - String get mobileSharePuzzle => 'Share this puzzle'; + String get mobileSharePuzzle => 'Sdílej tuto úlohu'; @override - String get mobileShareGameURL => 'Share game URL'; + String get mobileShareGameURL => 'Sdílet URL hry'; @override - String get mobileShareGamePGN => 'Share PGN'; + String get mobileShareGamePGN => 'Sdílet PGN'; @override - String get mobileSharePositionAsFEN => 'Share position as FEN'; + String get mobileSharePositionAsFEN => 'Sdílet pozici jako FEN'; @override - String get mobileShowVariations => 'Show variations'; + String get mobileShowVariations => 'Zobraz variace'; @override - String get mobileHideVariation => 'Hide variation'; + String get mobileHideVariation => 'Schovej variace'; @override - String get mobileShowComments => 'Show comments'; + String get mobileShowComments => 'Zobraz komentáře'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get mobilePuzzleStormConfirmEndRun => 'Chceš ukončit tento běh?'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobilePuzzleStormFilterNothingToShow => 'Nic k zobrazení, prosím změn filtry'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; + String get mobileCancelTakebackOffer => 'Zrušit nabídnutí vrácení tahu'; @override - String get mobileCancelDrawOffer => 'Cancel draw offer'; + String get mobileWaitingForOpponentToJoin => 'Čeká se na připojení protihráče...'; @override - String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; + String get mobileBlindfoldMode => 'Páska přes oči'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get mobileLiveStreamers => 'Živé vysílání'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileCustomGameJoinAGame => 'Připojit se ke hře'; @override - String get mobileCustomGameJoinAGame => 'Join a game'; + String get mobileCorrespondenceClearSavedMove => 'Vymazat uložené tahy'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; + String get mobileSomethingWentWrong => 'Něco se pokazilo.'; @override - String get mobileSomethingWentWrong => 'Something went wrong.'; + String get mobileShowResult => 'Zobrazit výsledky'; @override - String get mobileShowResult => 'Show result'; + String get mobilePuzzleThemesSubtitle => 'Hrej úlohy z tvých oblíbených zahájení, nebo si vyber styl.'; @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; - - @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobilePuzzleStormSubtitle => 'Vyřeš co nejvíce úloh co dokážeš za 3 minuty.'; @override String mobileGreeting(String param) { - return 'Hello, $param'; + return 'Ahoj, $param'; } @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileGreetingWithoutName => 'Ahoj'; @override String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; @@ -262,6 +259,19 @@ class AppLocalizationsCs extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Dokončeno $count $param2 korespondenčních partii', + many: 'Dokončeno $count $param2 korespondenčních partii', + few: 'Dokončeny $count $param2 korespondenční partie', + one: 'Dokončena $count $param2 korespondenční partie', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -382,9 +392,228 @@ class AppLocalizationsCs extends AppLocalizations { @override String get broadcastBroadcasts => 'Přenosy'; + @override + String get broadcastMyBroadcasts => 'Moje vysílání'; + @override String get broadcastLiveBroadcasts => 'Živé přenosy turnajů'; + @override + String get broadcastBroadcastCalendar => 'Kalendář přenosů'; + + @override + String get broadcastNewBroadcast => 'Nový živý přenos'; + + @override + String get broadcastSubscribedBroadcasts => 'Odebírané přenosy'; + + @override + String get broadcastAboutBroadcasts => 'O vysílání'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Jak používat Lichess vysílání.'; + + @override + String get broadcastTheNewRoundHelp => 'Nové kolo bude mít stejné členy a přispěvatele jako to předchozí.'; + + @override + String get broadcastAddRound => 'Přidat kolo'; + + @override + String get broadcastOngoing => 'Probíhající'; + + @override + String get broadcastUpcoming => 'Chystané'; + + @override + String get broadcastCompleted => 'Dokončené'; + + @override + String get broadcastCompletedHelp => 'Lichess detekuje dokončení kola na základě zdrojových her. Tento přepínač použijte, pokud není k dispozici žádný zdroj.'; + + @override + String get broadcastRoundName => 'Číslo kola'; + + @override + String get broadcastRoundNumber => 'Číslo kola'; + + @override + String get broadcastTournamentName => 'Název turnaje'; + + @override + String get broadcastTournamentDescription => 'Stručný popis turnaje'; + + @override + String get broadcastFullDescription => 'Úplný popis události'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Volitelný dlouhý popis přenosu. $param1 je k dispozici. Délka musí být menší než $param2 znaků.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN Zdrojová URL adresa'; + + @override + String get broadcastSourceUrlHelp => 'URL adresa, kterou bude Lichess kontrolovat pro získání PGN aktualizací. Musí být veřejně přístupná z internetu.'; + + @override + String get broadcastSourceGameIds => 'Až 64 ID Lichess her, oddělených mezerama.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Datum zahájení v lokálním čase turnaje: $param'; + } + + @override + String get broadcastStartDateHelp => 'Nepovinné, pokud víte, kdy událost začíná'; + + @override + String get broadcastCurrentGameUrl => 'URL adresa právě probíhající partie'; + + @override + String get broadcastDownloadAllRounds => 'Stáhnout hry ze všech kol'; + + @override + String get broadcastResetRound => 'Resetovat toto kolo'; + + @override + String get broadcastDeleteRound => 'Smazat toto kolo'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Definitivně smazat kolo a jeho hry.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Smazat všechny hry v tomto kole. Zdroj musí být aktivní aby bylo možno je znovu vytvořit.'; + + @override + String get broadcastEditRoundStudy => 'Upravit studie kola'; + + @override + String get broadcastDeleteTournament => 'Smazat tento turnaj'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Opravdu smazat celý turnaj, všechna kola a hry.'; + + @override + String get broadcastShowScores => 'Zobraz skóre hráču dle herních výsledků'; + + @override + String get broadcastReplacePlayerTags => 'Volitelné: nahraď jména hráčů, rating a tituly'; + + @override + String get broadcastFideFederations => 'FIDE federace'; + + @override + String get broadcastTop10Rating => 'Rating top 10'; + + @override + String get broadcastFidePlayers => 'FIDE hráči'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE hráč nenalezen'; + + @override + String get broadcastFideProfile => 'FIDE profil'; + + @override + String get broadcastFederation => 'Federace'; + + @override + String get broadcastAgeThisYear => 'Věk tento rok'; + + @override + String get broadcastUnrated => 'Nehodnocen'; + + @override + String get broadcastRecentTournaments => 'Nedávné tournamenty'; + + @override + String get broadcastOpenLichess => 'Open in Lichess'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Overview'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Official website'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'Score'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count vysílání', + many: '$count vysílání', + few: '$count vysílání', + one: '$count vysílání', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Výzvy: $param1'; @@ -1434,10 +1663,10 @@ class AppLocalizationsCs extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Soupeř musí zahrát jakýkoliv tah, přičemž všechny zhoršují jeho pozici a zlepšují naší pozici.'; @override - String get puzzleThemeHealthyMix => 'Mix úloh'; + String get puzzleThemeMix => 'Mix úloh'; @override - String get puzzleThemeHealthyMixDescription => 'Troška od všeho. Nevíte co čekat, čili jste na vše připraveni! Jako v normální partii.'; + String get puzzleThemeMixDescription => 'Troška od všeho. Nevíte co čekat, čili jste na vše připraveni! Jako v normální partii.'; @override String get puzzleThemePlayerGames => 'Z vašich her'; @@ -1680,10 +1909,10 @@ class AppLocalizationsCs extends AppLocalizations { String get deleteFromHere => 'Smazat odsud'; @override - String get collapseVariations => 'Collapse variations'; + String get collapseVariations => 'Schovat variace'; @override - String get expandVariations => 'Expand variations'; + String get expandVariations => 'Zobrazit variace'; @override String get forceVariation => 'Zobrazit jako variantu'; @@ -1841,9 +2070,6 @@ class AppLocalizationsCs extends AppLocalizations { @override String get removesTheDepthLimit => 'Zapne nekonečnou analýzu a odstraní omezení hloubky propočtu'; - @override - String get engineManager => 'Správce enginu'; - @override String get blunder => 'Hrubá chyba'; @@ -1922,7 +2148,7 @@ class AppLocalizationsCs extends AppLocalizations { String get friends => 'Přátelé'; @override - String get otherPlayers => 'other players'; + String get otherPlayers => 'ostatní hráči'; @override String get discussions => 'Konverzace'; @@ -2107,6 +2333,9 @@ class AppLocalizationsCs extends AppLocalizations { @override String get gamesPlayed => 'Odehraných partií'; + @override + String get ok => 'OK'; + @override String get cancel => 'Zrušit'; @@ -2675,16 +2904,16 @@ class AppLocalizationsCs extends AppLocalizations { String get editProfile => 'Upravit profil'; @override - String get realName => 'Real name'; + String get realName => 'Skutečné jméno'; @override String get setFlair => 'Nastav si svou ikonu za jménem'; @override - String get flair => 'Upravitelná ikona'; + String get flair => 'Ikona'; @override - String get youCanHideFlair => 'There is a setting to hide all user flairs across the entire site.'; + String get youCanHideFlair => 'Existuje nastavení které schová všechny uživatelské ikony za jménem po celém webu.'; @override String get biography => 'O mně'; @@ -2735,7 +2964,7 @@ class AppLocalizationsCs extends AppLocalizations { String get puzzles => 'Puzzle'; @override - String get onlineBots => 'Online bots'; + String get onlineBots => 'Online roboti'; @override String get name => 'Jméno'; @@ -2756,10 +2985,10 @@ class AppLocalizationsCs extends AppLocalizations { String get yes => 'Ano'; @override - String get website => 'Website'; + String get website => 'Web'; @override - String get mobile => 'Mobile'; + String get mobile => 'Mobil'; @override String get help => 'Nápověda:'; @@ -2816,7 +3045,13 @@ class AppLocalizationsCs extends AppLocalizations { String get other => 'Jiné'; @override - String get reportDescriptionHelp => 'Vložte link na hru(y) a popište, co je špatně na chování tohoto hráče. (Pokud možno anglicky.)'; + String get reportCheatBoostHelp => 'Zde vlož odkaz na hru(hry) a napiš co dělal tento uživatel. Nepiš pouze \"on podváděl\", ale napiš proč si myslíš že podváděl.'; + + @override + String get reportUsernameHelp => 'Vysvětli co je urážlivého na jeho u6ivatelském jménu. Nepiš pouze \"Je urážlivé/nevhodné\", ale řekni i důvod proč to tak je, zejména pokud je urážka zatemněná, nebo je v jiném jazyce než v angličtině, nebo je ve slangu či jde o historickou nebokulturní referenci.'; + + @override + String get reportProcessedFasterInEnglish => 'Nahlášení bude rychlejší pokud bude v angličtině.'; @override String get error_provideOneCheatedGameLink => 'Prosím, uveďte alespoň jeden link na partii, ve které se podvádělo.'; @@ -2919,7 +3154,7 @@ class AppLocalizationsCs extends AppLocalizations { String get outsideTheBoard => 'Mimo šachovnici'; @override - String get allSquaresOfTheBoard => 'All squares of the board'; + String get allSquaresOfTheBoard => 'Všechny pole na šachovnici'; @override String get onSlowGames => 'Při pomalých hrách'; @@ -3133,7 +3368,7 @@ class AppLocalizationsCs extends AppLocalizations { } @override - String get yourPendingSimuls => 'Your pending simuls'; + String get yourPendingSimuls => 'Tvoje simulace ve frontě'; @override String get createdSimuls => 'Nově vytvořené simultánky'; @@ -3142,7 +3377,7 @@ class AppLocalizationsCs extends AppLocalizations { String get hostANewSimul => 'Vytvoř novou simultánku'; @override - String get signUpToHostOrJoinASimul => 'Sign up to host or join a simul'; + String get signUpToHostOrJoinASimul => 'Zaregistruj se abys mohl založit nebo se připojit k simulaci'; @override String get noSimulFound => 'Simultánka nenalezena'; @@ -3217,7 +3452,7 @@ class AppLocalizationsCs extends AppLocalizations { String get keyGoToStartOrEnd => 'běžte na začátek/konec'; @override - String get keyCycleSelectedVariation => 'Cycle selected variation'; + String get keyCycleSelectedVariation => 'Projdi zkrze vybranou variaci'; @override String get keyShowOrHideComments => 'zobrazte/skryjte komentáře'; @@ -3241,22 +3476,22 @@ class AppLocalizationsCs extends AppLocalizations { String get keyNextInaccuracy => 'Další nepřesnost'; @override - String get keyPreviousBranch => 'Previous branch'; + String get keyPreviousBranch => 'Předchozí větev'; @override - String get keyNextBranch => 'Next branch'; + String get keyNextBranch => 'Další větev'; @override String get toggleVariationArrows => 'Přepnout šipky variant'; @override - String get cyclePreviousOrNextVariation => 'Cycle previous/next variation'; + String get cyclePreviousOrNextVariation => 'Projdi předchozí/následující variantu'; @override String get toggleGlyphAnnotations => 'Přepnout poznámky glyfů'; @override - String get togglePositionAnnotations => 'Toggle position annotations'; + String get togglePositionAnnotations => 'Přepni zvýraznění pozice'; @override String get variationArrowsInfo => 'Šipky variant umožňují navigaci bez použití seznamu tahů.'; @@ -3515,22 +3750,22 @@ class AppLocalizationsCs extends AppLocalizations { String get backgroundImageUrl => 'URL zdroj obrázku na pozadí:'; @override - String get board => 'Board'; + String get board => 'Šachovnice'; @override - String get size => 'Size'; + String get size => 'Velikost'; @override - String get opacity => 'Opacity'; + String get opacity => 'Průhlednost'; @override - String get brightness => 'Brightness'; + String get brightness => 'Jas'; @override String get hue => 'Hue'; @override - String get boardReset => 'Reset colours to default'; + String get boardReset => 'Vrátit barvy na původní nastavení'; @override String get pieceSet => 'Vzhled figur'; @@ -4119,7 +4354,10 @@ class AppLocalizationsCs extends AppLocalizations { String get lichessPatronInfo => 'Lichess je bezplatný a zcela svobodný/nezávislý softvér s otevřeným zdrojovým kódem.\nVeškeré provozní náklady, vývoj a obsah jsou financovány výhradně z příspěvků uživatelů.'; @override - String get nothingToSeeHere => 'Nothing to see here at the moment.'; + String get nothingToSeeHere => 'Momentálně zde není nic k vidění.'; + + @override + String get stats => 'Stats'; @override String opponentLeftCounter(int count) { @@ -4855,9 +5093,522 @@ class AppLocalizationsCs extends AppLocalizations { @override String get streamerLichessStreamers => 'Lichess streameři'; + @override + String get studyPrivate => 'Soukromé'; + + @override + String get studyMyStudies => 'Moje studie'; + + @override + String get studyStudiesIContributeTo => 'Studie, ke kterým přispívám'; + + @override + String get studyMyPublicStudies => 'Moje veřejné studie'; + + @override + String get studyMyPrivateStudies => 'Moje soukromé studie'; + + @override + String get studyMyFavoriteStudies => 'Moje oblíbené studie'; + + @override + String get studyWhatAreStudies => 'Co jsou studie?'; + + @override + String get studyAllStudies => 'Všechny studie'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Studie vytvořené hráčem $param'; + } + + @override + String get studyNoneYet => 'Zatím nic.'; + + @override + String get studyHot => 'Oblíbené'; + + @override + String get studyDateAddedNewest => 'Datum přidání (nejnovější)'; + + @override + String get studyDateAddedOldest => 'Datum přidání (nejstarší)'; + + @override + String get studyRecentlyUpdated => 'Nedávno aktualizované'; + + @override + String get studyMostPopular => 'Nejoblíbenější'; + + @override + String get studyAlphabetical => 'Abecedně'; + + @override + String get studyAddNewChapter => 'Přidat novou kapitolu'; + + @override + String get studyAddMembers => 'Přidat uživatele'; + + @override + String get studyInviteToTheStudy => 'Pozvat do studie'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Prosím zvěte pouze lidi, které znáte a kteří se chtějí aktivně připojit k této studii.'; + + @override + String get studySearchByUsername => 'Hledat podle uživatelského jména'; + + @override + String get studySpectator => 'Divák'; + + @override + String get studyContributor => 'Přispívající'; + + @override + String get studyKick => 'Vyhodit'; + + @override + String get studyLeaveTheStudy => 'Opustit studii'; + + @override + String get studyYouAreNowAContributor => 'Nyní jste přispívající'; + + @override + String get studyYouAreNowASpectator => 'Nyní jste divák'; + + @override + String get studyPgnTags => 'PGN tagy'; + + @override + String get studyLike => 'To se mi líbí'; + + @override + String get studyUnlike => 'Už se mi nelíbí'; + + @override + String get studyNewTag => 'Nový štítek'; + + @override + String get studyCommentThisPosition => 'Komentář k tomuto příspěvku'; + + @override + String get studyCommentThisMove => 'Komentář k tomuto tahu'; + + @override + String get studyAnnotateWithGlyphs => 'Popsat glyfy'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Kapitola je moc krátká na to, aby mohla být zanalyzována.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Pouze přispěvatelé mohou požádat o počítačovou analýzu.'; + + @override + String get studyGetAFullComputerAnalysis => 'Získejte plnou počítačovou analýzu hlavní varianty.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Ujistěte se, že je kapitola úplná. O analýzu můžete požádat pouze jednou.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Všichni SYNCHRONIZOVANÍ členové zůstávají na stejné pozici'; + + @override + String get studyShareChanges => 'Sdílet změny s diváky a uložit je na server'; + + @override + String get studyPlaying => 'Probíhající'; + + @override + String get studyShowEvalBar => 'Lišta hodnotící pozici'; + + @override + String get studyFirst => 'První'; + + @override + String get studyPrevious => 'Předchozí'; + + @override + String get studyNext => 'Další'; + + @override + String get studyLast => 'Poslední'; + @override String get studyShareAndExport => 'Sdílení a export'; + @override + String get studyCloneStudy => 'Klonovat'; + + @override + String get studyStudyPgn => 'PGN studie'; + + @override + String get studyDownloadAllGames => 'Stáhnout všechny hry'; + + @override + String get studyChapterPgn => 'PGN kapitoly'; + + @override + String get studyCopyChapterPgn => 'Kopírovat PGN'; + + @override + String get studyDownloadGame => 'Stáhnout hru'; + + @override + String get studyStudyUrl => 'URL studie'; + + @override + String get studyCurrentChapterUrl => 'URL aktuální kapitoly'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Tento odkaz můžete vložit např. do diskusního fóra'; + + @override + String get studyStartAtInitialPosition => 'Začít ve výchozí pozici'; + + @override + String studyStartAtX(String param) { + return 'Začít u tahu $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Vložte vaší stránku nebo blog'; + + @override + String get studyReadMoreAboutEmbedding => 'Přečtěte si více o vkládání'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Lze vložit pouze veřejné studie!'; + + @override + String get studyOpen => 'Otevřít'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1 vám přináší $param2'; + } + + @override + String get studyStudyNotFound => 'Studie nenalezena'; + + @override + String get studyEditChapter => 'Upravit kapitolu'; + + @override + String get studyNewChapter => 'Nová kapitola'; + + @override + String studyImportFromChapterX(String param) { + return 'Importovat z $param'; + } + + @override + String get studyOrientation => 'Orientace'; + + @override + String get studyAnalysisMode => 'Režim rozboru'; + + @override + String get studyPinnedChapterComment => 'Připnutý komentář u kapitoly'; + + @override + String get studySaveChapter => 'Uložit kapitolu'; + + @override + String get studyClearAnnotations => 'Vymazat anotace'; + + @override + String get studyClearVariations => 'Vymazat varianty'; + + @override + String get studyDeleteChapter => 'Odstranit kapitolu'; + + @override + String get studyDeleteThisChapter => 'Opravdu chcete odstranit tuto kapitolu? Kapitola bude navždy ztracena!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Vymazat všechny komentáře a výtvory v této kapitole?'; + + @override + String get studyRightUnderTheBoard => 'Přímo pod šachovnicí'; + + @override + String get studyNoPinnedComment => 'Žádný'; + + @override + String get studyNormalAnalysis => 'Normální rozbor'; + + @override + String get studyHideNextMoves => 'Skrýt následující tahy'; + + @override + String get studyInteractiveLesson => 'Interaktivní lekce'; + + @override + String studyChapterX(String param) { + return 'Kapitola: $param'; + } + + @override + String get studyEmpty => 'Prázdné'; + + @override + String get studyStartFromInitialPosition => 'Začít z původní pozice'; + + @override + String get studyEditor => 'Tvůrce'; + + @override + String get studyStartFromCustomPosition => 'Začít od vlastní pozice'; + + @override + String get studyLoadAGameByUrl => 'Načíst hru podle URL'; + + @override + String get studyLoadAPositionFromFen => 'Načíst polohu z FEN'; + + @override + String get studyLoadAGameFromPgn => 'Načíst hru z PGN'; + + @override + String get studyAutomatic => 'Automatický'; + + @override + String get studyUrlOfTheGame => 'URL hry'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Načíst hru z $param1 nebo $param2'; + } + + @override + String get studyCreateChapter => 'Vytvořit kapitolu'; + + @override + String get studyCreateStudy => 'Vytvořit studii'; + + @override + String get studyEditStudy => 'Upravit studii'; + + @override + String get studyVisibility => 'Viditelnost'; + + @override + String get studyPublic => 'Veřejná'; + + @override + String get studyUnlisted => 'Neveřejná'; + + @override + String get studyInviteOnly => 'Pouze na pozvání'; + + @override + String get studyAllowCloning => 'Povolit klonování'; + + @override + String get studyNobody => 'Nikdo'; + + @override + String get studyOnlyMe => 'Pouze já'; + + @override + String get studyContributors => 'Přispěvatelé'; + + @override + String get studyMembers => 'Členové'; + + @override + String get studyEveryone => 'Kdokoli'; + + @override + String get studyEnableSync => 'Povolit synchronizaci'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Ano, všichni zůstávají na stejné pozici'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Ne, umožnit volné procházení'; + + @override + String get studyPinnedStudyComment => 'Připnutý komentář studie'; + @override String get studyStart => 'Začít'; + + @override + String get studySave => 'Uložit'; + + @override + String get studyClearChat => 'Vyčistit chat'; + + @override + String get studyDeleteTheStudyChatHistory => 'Opravdu chcete vymazat historii chatu? Operaci nelze vrátit!'; + + @override + String get studyDeleteStudy => 'Smazat studii'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Opravdu chcete smazat celou studii? Akci nelze vrátit zpět. Zadejte název studie pro potvrzení: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Kde chcete tuto pozici studovat?'; + + @override + String get studyGoodMove => 'Dobrý tah'; + + @override + String get studyMistake => 'Chyba'; + + @override + String get studyBrilliantMove => 'Výborný tah'; + + @override + String get studyBlunder => 'Hrubá chyba'; + + @override + String get studyInterestingMove => 'Zajímavý tah'; + + @override + String get studyDubiousMove => 'Pochybný tah'; + + @override + String get studyOnlyMove => 'Jediný tah'; + + @override + String get studyZugzwang => 'Zugzwang'; + + @override + String get studyEqualPosition => 'Rovná pozice'; + + @override + String get studyUnclearPosition => 'Nejasná pozice'; + + @override + String get studyWhiteIsSlightlyBetter => 'Bílý stojí o něco lépe'; + + @override + String get studyBlackIsSlightlyBetter => 'Černý stojí o něco lépe'; + + @override + String get studyWhiteIsBetter => 'Bílý stojí lépe'; + + @override + String get studyBlackIsBetter => 'Černý stojí lépe'; + + @override + String get studyWhiteIsWinning => 'Bílý má rozhodující výhodu'; + + @override + String get studyBlackIsWinning => 'Černý má rozhodující výhodu'; + + @override + String get studyNovelty => 'Novinka'; + + @override + String get studyDevelopment => 'Vývin'; + + @override + String get studyInitiative => 'S iniciativou'; + + @override + String get studyAttack => 'S útokem'; + + @override + String get studyCounterplay => 'S protihrou'; + + @override + String get studyTimeTrouble => 'Časová tíseň'; + + @override + String get studyWithCompensation => 'S kompenzací'; + + @override + String get studyWithTheIdea => 'S ideou'; + + @override + String get studyNextChapter => 'Další kapitola'; + + @override + String get studyPrevChapter => 'Předchozí kapitola'; + + @override + String get studyStudyActions => 'Akce pro studii'; + + @override + String get studyTopics => 'Témata'; + + @override + String get studyMyTopics => 'Moje témata'; + + @override + String get studyPopularTopics => 'Oblíbená témata'; + + @override + String get studyManageTopics => 'Správa témat'; + + @override + String get studyBack => 'Zpět'; + + @override + String get studyPlayAgain => 'Hrát znovu'; + + @override + String get studyWhatWouldYouPlay => 'Co byste v této pozici hráli?'; + + @override + String get studyYouCompletedThisLesson => 'Blahopřejeme! Dokončili jste tuto lekci.'; + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count kapitol', + many: '$count kapitol', + few: '$count kapitoly', + one: '$count kapitola', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count her', + many: '$count her', + few: '$count hry', + one: '$count hra', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count členů', + many: '$count členů', + few: '$count členi', + one: '$count člen', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Vložte obsah vašeho PGN souboru (až $count her)', + many: 'Vložte obsah vašeho PGN souboru (až $count her)', + few: 'Vložte obsah vašeho PGN souboru (až $count hry)', + one: 'Vložte obsah vašeho PGN souboru (až $count hra)', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_da.dart b/lib/l10n/l10n_da.dart index e23dcef0b7..e30401ac05 100644 --- a/lib/l10n/l10n_da.dart +++ b/lib/l10n/l10n_da.dart @@ -103,9 +103,6 @@ class AppLocalizationsDa extends AppLocalizations { @override String get mobileCancelTakebackOffer => 'Annuller tilbud om tilbagetagelse'; - @override - String get mobileCancelDrawOffer => 'Træk tilbud om remis tilbage'; - @override String get mobileWaitingForOpponentToJoin => 'Venter på at modstander slutter sig til...'; @@ -142,7 +139,7 @@ class AppLocalizationsDa extends AppLocalizations { String get mobileGreetingWithoutName => 'Hej'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobilePrefMagnifyDraggedPiece => 'Forstør brik, som trækkes'; @override String get activityActivity => 'Aktivitet'; @@ -246,6 +243,17 @@ class AppLocalizationsDa extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Afsluttede $count $param2 korrespondancepartier', + one: 'Afsluttede $count $param2 korrespondanceparti', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,226 @@ class AppLocalizationsDa extends AppLocalizations { @override String get broadcastBroadcasts => 'Udsendelser'; + @override + String get broadcastMyBroadcasts => 'Mine udsendelser'; + @override String get broadcastLiveBroadcasts => 'Live turnerings-udsendelser'; + @override + String get broadcastBroadcastCalendar => 'Kaldender for udsendelser'; + + @override + String get broadcastNewBroadcast => 'Ny live-udsendelse'; + + @override + String get broadcastSubscribedBroadcasts => 'Udsendelser, du abonnerer på'; + + @override + String get broadcastAboutBroadcasts => 'Om udsendelse'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Sådan bruges Lichess-udsendelser.'; + + @override + String get broadcastTheNewRoundHelp => 'Den nye runde vil have de samme medlemmer og bidragydere som den foregående.'; + + @override + String get broadcastAddRound => 'Tilføj en runde'; + + @override + String get broadcastOngoing => 'I gang'; + + @override + String get broadcastUpcoming => 'Kommende'; + + @override + String get broadcastCompleted => 'Afsluttet'; + + @override + String get broadcastCompletedHelp => 'Lichess registrerer rund-færdiggørelse baseret på kildepartierne. Brug denne skifter, hvis der ikke er nogen kilde.'; + + @override + String get broadcastRoundName => 'Rundenavn'; + + @override + String get broadcastRoundNumber => 'Rundenummer'; + + @override + String get broadcastTournamentName => 'Turneringsnavn'; + + @override + String get broadcastTournamentDescription => 'Kort beskrivelse af turnering'; + + @override + String get broadcastFullDescription => 'Fuld beskrivelse af begivenheden'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Valgfri lang beskrivelse af transmissionen. $param1 er tilgængelig. Længde skal være mindre end $param2 tegn.'; + } + + @override + String get broadcastSourceSingleUrl => 'URL for PGN-kilde'; + + @override + String get broadcastSourceUrlHelp => 'URL som Lichess vil trække på for at få PGN updates. Den skal være offentlig tilgængelig fra internettet.'; + + @override + String get broadcastSourceGameIds => 'Op til 64 Lichess parti-ID\'er, adskilt af mellemrum.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Startdato i turneringens lokale tidszone: $param'; + } + + @override + String get broadcastStartDateHelp => 'Valgfri, hvis du ved, hvornår begivenheden starter'; + + @override + String get broadcastCurrentGameUrl => 'Nuværende parti URL'; + + @override + String get broadcastDownloadAllRounds => 'Download alle runder'; + + @override + String get broadcastResetRound => 'Nulstil denne runde'; + + @override + String get broadcastDeleteRound => 'Slet denne runde'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Slet runden og dens partier endegyldigt.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Slet alle partier i denne runde. Kilden skal være aktiv for at genskabe dem.'; + + @override + String get broadcastEditRoundStudy => 'Rediger rundestudie'; + + @override + String get broadcastDeleteTournament => 'Slet denne turnering'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Slet hele turneringen, alle dens runder og alle dens partier.'; + + @override + String get broadcastShowScores => 'Vis spilleres point baseret på resultater fra partier'; + + @override + String get broadcastReplacePlayerTags => 'Valgfrit: udskift spillernavne, ratings og titler'; + + @override + String get broadcastFideFederations => 'FIDE-føderationer'; + + @override + String get broadcastTop10Rating => 'Top 10 rating'; + + @override + String get broadcastFidePlayers => 'FIDE-spillere'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE-spiller ikke fundet'; + + @override + String get broadcastFideProfile => 'FIDE-profil'; + + @override + String get broadcastFederation => 'Føderation'; + + @override + String get broadcastAgeThisYear => 'Alder i år'; + + @override + String get broadcastUnrated => 'Uden rating'; + + @override + String get broadcastRecentTournaments => 'Seneste turneringer'; + + @override + String get broadcastOpenLichess => 'Åbn i Lichess'; + + @override + String get broadcastTeams => 'Hold'; + + @override + String get broadcastBoards => 'Brætter'; + + @override + String get broadcastOverview => 'Oversigt'; + + @override + String get broadcastSubscribeTitle => 'Abonner på at blive underrettet, når hver runde starter. Du kan skifte mellem klokke- eller push-meddelelser for udsendelser i dine kontoindstillinger.'; + + @override + String get broadcastUploadImage => 'Upload turneringsbillede'; + + @override + String get broadcastNoBoardsYet => 'Ingen brætter endnu. Disse vises når partier er uploadet.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Brætter kan indlæses med en kilde eller via $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starter efter $param'; + } + + @override + String get broadcastStartVerySoon => 'Udsendelsen starter meget snart.'; + + @override + String get broadcastNotYetStarted => 'Udsendelsen er endnu ikke startet.'; + + @override + String get broadcastOfficialWebsite => 'Officielt websted'; + + @override + String get broadcastStandings => 'Stillinger'; + + @override + String broadcastIframeHelp(String param) { + return 'Flere muligheder på $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters side'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'En offentlig, realtids PGN-kilde til denne runde. Vi tilbyder også en $param for hurtigere og mere effektiv synkronisering.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Indlejr denne udsendelse på dit website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Indlejr $param på dit website'; + } + + @override + String get broadcastRatingDiff => 'Rating-forskel'; + + @override + String get broadcastGamesThisTournament => 'Partier i denne turnering'; + + @override + String get broadcastScore => 'Score'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count udsendelser', + one: '$count udsendelse', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Udfordringer: $param1'; @@ -1390,10 +1615,10 @@ class AppLocalizationsDa extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Modstanderen har begrænsede muligheder for træk, og ethvert træk vil forværre positionen.'; @override - String get puzzleThemeHealthyMix => 'Sund blanding'; + String get puzzleThemeMix => 'Sund blanding'; @override - String get puzzleThemeHealthyMixDescription => 'Lidt af hvert. Du kan ikke vide, hvad du skal forvente, så du skal være klar til alt! Præcis som i rigtige spil.'; + String get puzzleThemeMixDescription => 'Lidt af hvert. Du kan ikke vide, hvad du skal forvente, så du skal være klar til alt! Præcis som i rigtige spil.'; @override String get puzzleThemePlayerGames => 'Spiller-partier'; @@ -1797,9 +2022,6 @@ class AppLocalizationsDa extends AppLocalizations { @override String get removesTheDepthLimit => 'Fjerner dybdegrænsen, og holder din computer varm'; - @override - String get engineManager => 'Administration af skakprogram'; - @override String get blunder => 'Brøler'; @@ -2063,6 +2285,9 @@ class AppLocalizationsDa extends AppLocalizations { @override String get gamesPlayed => 'Antal partier spillet'; + @override + String get ok => 'Ok'; + @override String get cancel => 'Annuller'; @@ -2772,7 +2997,13 @@ class AppLocalizationsDa extends AppLocalizations { String get other => 'Andet'; @override - String get reportDescriptionHelp => 'Indsæt et link til partiet (eller partierne) og forklar hvad der er i vejen med brugerens opførsel.'; + String get reportCheatBoostHelp => 'Indsæt linket til partiet (eller partierne) og forklar hvad der er i vejen med brugerens opførsel. Sig ikke blot \"de snyder\", men fortæl os, hvordan du nåede frem til den konklusion.'; + + @override + String get reportUsernameHelp => 'Forklar, hvad der er stødende ved dette brugernavn. Sig ikke blot \"det er stødende/upassende\", men fortæl os, hvordan du nåede frem til denne konklusion, især hvis fornærmelsen er sløret, ikke er på engelsk, er slang eller er en historisk/kulturel reference.'; + + @override + String get reportProcessedFasterInEnglish => 'Din indberetning vil blive behandlet hurtigere, hvis den er skrevet på engelsk.'; @override String get error_provideOneCheatedGameLink => 'Angiv mindst ét link til et parti med snyd.'; @@ -4077,6 +4308,9 @@ class AppLocalizationsDa extends AppLocalizations { @override String get nothingToSeeHere => 'Intet at se her i øjeblikket.'; + @override + String get stats => 'Statistik'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4723,9 +4957,514 @@ class AppLocalizationsDa extends AppLocalizations { @override String get streamerLichessStreamers => 'Lichess-streamere'; + @override + String get studyPrivate => 'Privat'; + + @override + String get studyMyStudies => 'Mine studier'; + + @override + String get studyStudiesIContributeTo => 'Studier jeg bidrager til'; + + @override + String get studyMyPublicStudies => 'Mine offentlige studier'; + + @override + String get studyMyPrivateStudies => 'Mine private studier'; + + @override + String get studyMyFavoriteStudies => 'Mine favoritstudier'; + + @override + String get studyWhatAreStudies => 'Hvad er studier?'; + + @override + String get studyAllStudies => 'Alle studier'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Studier oprettet af $param'; + } + + @override + String get studyNoneYet => 'Ingen endnu.'; + + @override + String get studyHot => 'Populært'; + + @override + String get studyDateAddedNewest => 'Dato tilføjet (nyeste)'; + + @override + String get studyDateAddedOldest => 'Dato tilføjet (ældste)'; + + @override + String get studyRecentlyUpdated => 'Nyligt opdateret'; + + @override + String get studyMostPopular => 'Mest populære'; + + @override + String get studyAlphabetical => 'Alfabetisk'; + + @override + String get studyAddNewChapter => 'Tilføj et nyt kapitel'; + + @override + String get studyAddMembers => 'Tilføj medlemmer'; + + @override + String get studyInviteToTheStudy => 'Inviter til studiet'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Inviter venligst kun personer du kender, og som ønsker at være en del af dette studie.'; + + @override + String get studySearchByUsername => 'Søg på brugernavn'; + + @override + String get studySpectator => 'Tilskuer'; + + @override + String get studyContributor => 'Bidragsyder'; + + @override + String get studyKick => 'Smid ud'; + + @override + String get studyLeaveTheStudy => 'Forlad dette studie'; + + @override + String get studyYouAreNowAContributor => 'Du er nu bidragsyder'; + + @override + String get studyYouAreNowASpectator => 'Du er nu tilskuer'; + + @override + String get studyPgnTags => 'PGN tags'; + + @override + String get studyLike => 'Synes godt om'; + + @override + String get studyUnlike => 'Synes ikke godt om'; + + @override + String get studyNewTag => 'Nyt tag'; + + @override + String get studyCommentThisPosition => 'Kommenter på denne stilling'; + + @override + String get studyCommentThisMove => 'Kommenter på dette træk'; + + @override + String get studyAnnotateWithGlyphs => 'Annoter med glyffer'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Dette kapitel er for kort til at blive analyseret.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Kun studiets bidragsydere kan anmode om en computeranalyse.'; + + @override + String get studyGetAFullComputerAnalysis => 'Få en fuld server-computeranalyse af hovedlinjen.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Sikr dig at kapitlet er færdigt. Du kan kun anmode om analyse én gang.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Alle SYNC medlemmer forbliver på samme stilling'; + + @override + String get studyShareChanges => 'Del ændringer med tilskuere og gem dem på serveren'; + + @override + String get studyPlaying => 'Spiller'; + + @override + String get studyShowEvalBar => 'Evalueringssøjler'; + + @override + String get studyFirst => 'Første'; + + @override + String get studyPrevious => 'Forrige'; + + @override + String get studyNext => 'Næste'; + + @override + String get studyLast => 'Sidste'; + @override String get studyShareAndExport => 'Del & eksport'; + @override + String get studyCloneStudy => 'Klon'; + + @override + String get studyStudyPgn => 'Studie PGN'; + + @override + String get studyDownloadAllGames => 'Download alle partier'; + + @override + String get studyChapterPgn => 'Kapitel PGN'; + + @override + String get studyCopyChapterPgn => 'Kopier PGN'; + + @override + String get studyDownloadGame => 'Download parti'; + + @override + String get studyStudyUrl => 'Studie URL'; + + @override + String get studyCurrentChapterUrl => 'Nuværende kapitel URL'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Du kan indsætte dette i forummet for at indlejre'; + + @override + String get studyStartAtInitialPosition => 'Start ved indledende stilling'; + + @override + String studyStartAtX(String param) { + return 'Start ved $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Indlejr på din hjemmeside eller blog'; + + @override + String get studyReadMoreAboutEmbedding => 'Læs mere om indlejring'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Kun offentlige studier kan indlejres!'; + + @override + String get studyOpen => 'Åbn'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1 bragt til dig af $param2'; + } + + @override + String get studyStudyNotFound => 'Studie ikke fundet'; + + @override + String get studyEditChapter => 'Rediger kapitel'; + + @override + String get studyNewChapter => 'Nyt kapitel'; + + @override + String studyImportFromChapterX(String param) { + return 'Import fra $param'; + } + + @override + String get studyOrientation => 'Retning'; + + @override + String get studyAnalysisMode => 'Analysetilstand'; + + @override + String get studyPinnedChapterComment => 'Fastgjort kapitelkommentar'; + + @override + String get studySaveChapter => 'Gem kapitel'; + + @override + String get studyClearAnnotations => 'Ryd annoteringer'; + + @override + String get studyClearVariations => 'Ryd varianter'; + + @override + String get studyDeleteChapter => 'Slet kapitel'; + + @override + String get studyDeleteThisChapter => 'Slet dette kapitel? Du kan ikke fortryde!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Ryd alle kommentarer og figurer i dette kapitel?'; + + @override + String get studyRightUnderTheBoard => 'Lige under brættet'; + + @override + String get studyNoPinnedComment => 'Ingen'; + + @override + String get studyNormalAnalysis => 'Normal analyse'; + + @override + String get studyHideNextMoves => 'Skjul næste træk'; + + @override + String get studyInteractiveLesson => 'Interaktiv lektion'; + + @override + String studyChapterX(String param) { + return 'Kapitel $param'; + } + + @override + String get studyEmpty => 'Tom'; + + @override + String get studyStartFromInitialPosition => 'Start ved indledende stilling'; + + @override + String get studyEditor => 'Editor'; + + @override + String get studyStartFromCustomPosition => 'Start fra brugerdefinerede stilling'; + + @override + String get studyLoadAGameByUrl => 'Indlæs et parti fra URL'; + + @override + String get studyLoadAPositionFromFen => 'Indlæs en stilling fra FEN'; + + @override + String get studyLoadAGameFromPgn => 'Indlæs et parti fra PGN'; + + @override + String get studyAutomatic => 'Automatisk'; + + @override + String get studyUrlOfTheGame => 'URL for partiet'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Indlæs et parti fra $param1 eller $param2'; + } + + @override + String get studyCreateChapter => 'Opret kapitel'; + + @override + String get studyCreateStudy => 'Opret studie'; + + @override + String get studyEditStudy => 'Rediger studie'; + + @override + String get studyVisibility => 'Synlighed'; + + @override + String get studyPublic => 'Offentlig'; + + @override + String get studyUnlisted => 'Ikke listet'; + + @override + String get studyInviteOnly => 'Kun inviterede'; + + @override + String get studyAllowCloning => 'Tillad kloning'; + + @override + String get studyNobody => 'Ingen'; + + @override + String get studyOnlyMe => 'Kun mig'; + + @override + String get studyContributors => 'Bidragydere'; + + @override + String get studyMembers => 'Medlemmer'; + + @override + String get studyEveryone => 'Enhver'; + + @override + String get studyEnableSync => 'Aktiver synk'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Ja: behold alle på den samme stilling'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Nej: lad folk gennemse frit'; + + @override + String get studyPinnedStudyComment => 'Fastgjort studie-kommentar'; + @override String get studyStart => 'Start'; + + @override + String get studySave => 'Gem'; + + @override + String get studyClearChat => 'Ryd chat'; + + @override + String get studyDeleteTheStudyChatHistory => 'Slet studiets chat-historik? Du kan ikke fortryde!'; + + @override + String get studyDeleteStudy => 'Slet studie'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Slet hele studiet? Det kan ikke fortrydes! Skriv navnet på studiet for at bekræfte: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Hvor vil du studere det?'; + + @override + String get studyGoodMove => 'Godt træk'; + + @override + String get studyMistake => 'Fejl'; + + @override + String get studyBrilliantMove => 'Fremragende træk'; + + @override + String get studyBlunder => 'Brøler'; + + @override + String get studyInterestingMove => 'Interessant træk'; + + @override + String get studyDubiousMove => 'Tvivlsomt træk'; + + @override + String get studyOnlyMove => 'Eneste mulige træk'; + + @override + String get studyZugzwang => 'Træktvang'; + + @override + String get studyEqualPosition => 'Lige stilling'; + + @override + String get studyUnclearPosition => 'Uafklaret stilling'; + + @override + String get studyWhiteIsSlightlyBetter => 'Hvid står lidt bedre'; + + @override + String get studyBlackIsSlightlyBetter => 'Sort står lidt bedre'; + + @override + String get studyWhiteIsBetter => 'Hvid står bedre'; + + @override + String get studyBlackIsBetter => 'Sort står bedre'; + + @override + String get studyWhiteIsWinning => 'Hvid vinder'; + + @override + String get studyBlackIsWinning => 'Sort vinder'; + + @override + String get studyNovelty => 'Nyfunden'; + + @override + String get studyDevelopment => 'Udvikling'; + + @override + String get studyInitiative => 'Initiativ'; + + @override + String get studyAttack => 'Angreb'; + + @override + String get studyCounterplay => 'Modspil'; + + @override + String get studyTimeTrouble => 'Tidsproblemer'; + + @override + String get studyWithCompensation => 'Med kompensation'; + + @override + String get studyWithTheIdea => 'Med ideen'; + + @override + String get studyNextChapter => 'Næste kapitel'; + + @override + String get studyPrevChapter => 'Forrige kapitel'; + + @override + String get studyStudyActions => 'Studiehandlinger'; + + @override + String get studyTopics => 'Emner'; + + @override + String get studyMyTopics => 'Mine emner'; + + @override + String get studyPopularTopics => 'Populære emner'; + + @override + String get studyManageTopics => 'Administrér emner'; + + @override + String get studyBack => 'Tilbage'; + + @override + String get studyPlayAgain => 'Spil igen'; + + @override + String get studyWhatWouldYouPlay => 'Hvad ville du spille i denne position?'; + + @override + String get studyYouCompletedThisLesson => 'Tillykke! Du har fuldført denne lektion.'; + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count kapitler', + one: '$count kapitel', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count partier', + one: '$count parti', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Medlemmer', + one: '$count Medlem', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Indsæt din PGN-tekst her, op til $count partier', + one: 'Indsæt din PGN-tekst her, op til $count parti', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_de.dart b/lib/l10n/l10n_de.dart index da502af1fd..f79bfa9af0 100644 --- a/lib/l10n/l10n_de.dart +++ b/lib/l10n/l10n_de.dart @@ -21,7 +21,7 @@ class AppLocalizationsDe extends AppLocalizations { String get mobileWatchTab => 'Zuschauen'; @override - String get mobileSettingsTab => 'Einstellungen'; + String get mobileSettingsTab => 'Optionen'; @override String get mobileMustBeLoggedIn => 'Du musst eingeloggt sein, um diese Seite anzuzeigen.'; @@ -103,9 +103,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get mobileCancelTakebackOffer => 'Zugzurücknahme-Angebot abbrechen'; - @override - String get mobileCancelDrawOffer => 'Remisangebot zurücknehmen'; - @override String get mobileWaitingForOpponentToJoin => 'Warte auf Beitritt eines Gegners...'; @@ -128,10 +125,10 @@ class AppLocalizationsDe extends AppLocalizations { String get mobileShowResult => 'Ergebnis anzeigen'; @override - String get mobilePuzzleThemesSubtitle => 'Spiele Aufgaben aus deinen Lieblings-Öffnungen oder wähle ein Thema.'; + String get mobilePuzzleThemesSubtitle => 'Spiele Aufgaben aus deinen Lieblings-Öffnungen oder wähle ein Theme.'; @override - String get mobilePuzzleStormSubtitle => 'Löse in 3 Minuten so viele Aufgaben wie möglich.'; + String get mobilePuzzleStormSubtitle => 'Löse so viele Aufgaben wie möglich in 3 Minuten.'; @override String mobileGreeting(String param) { @@ -142,7 +139,7 @@ class AppLocalizationsDe extends AppLocalizations { String get mobileGreetingWithoutName => 'Hallo'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobilePrefMagnifyDraggedPiece => 'Vergrößern der gezogenen Figur'; @override String get activityActivity => 'Verlauf'; @@ -246,6 +243,17 @@ class AppLocalizationsDe extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Hat $count $param2-Fernschachpartien gespielt', + one: 'Hat $count $param2-Fernschachpartie gespielt', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,226 @@ class AppLocalizationsDe extends AppLocalizations { @override String get broadcastBroadcasts => 'Übertragungen'; + @override + String get broadcastMyBroadcasts => 'Meine Übertragungen'; + @override String get broadcastLiveBroadcasts => 'Live-Turnierübertragungen'; + @override + String get broadcastBroadcastCalendar => 'Sendekalender'; + + @override + String get broadcastNewBroadcast => 'Neue Liveübertragung'; + + @override + String get broadcastSubscribedBroadcasts => 'Abonnierte Übertragungen'; + + @override + String get broadcastAboutBroadcasts => 'Über Übertragungen'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Wie man Lichess-Übertragungen benutzt.'; + + @override + String get broadcastTheNewRoundHelp => 'Die nächste Runde wird die gleichen Mitspieler und Mitwirkende haben wie die vorhergehende.'; + + @override + String get broadcastAddRound => 'Eine Runde hinzufügen'; + + @override + String get broadcastOngoing => 'Laufend'; + + @override + String get broadcastUpcoming => 'Demnächst'; + + @override + String get broadcastCompleted => 'Beendet'; + + @override + String get broadcastCompletedHelp => 'Lichess erkennt Rundenabschlüsse basierend auf den Quellspielen. Verwende diesen Schalter, wenn keine Quelle vorhanden ist.'; + + @override + String get broadcastRoundName => 'Rundenname'; + + @override + String get broadcastRoundNumber => 'Rundennummer'; + + @override + String get broadcastTournamentName => 'Turniername'; + + @override + String get broadcastTournamentDescription => 'Kurze Turnierbeschreibung'; + + @override + String get broadcastFullDescription => 'Vollständige Ereignisbeschreibung'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Optionale, ausführliche Beschreibung der Übertragung. $param1 ist verfügbar. Die Beschreibung muss kürzer als $param2 Zeichen sein.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN Quell-URL'; + + @override + String get broadcastSourceUrlHelp => 'URL die Lichess abfragt um PGN Aktualisierungen zu erhalten. Sie muss öffentlich aus dem Internet zugänglich sein.'; + + @override + String get broadcastSourceGameIds => 'Bis zu 64 Lichess Partie-IDs, getrennt durch Leerzeichen.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Startdatum in der Zeitzone des Tunierstandortes: $param'; + } + + @override + String get broadcastStartDateHelp => 'Optional, falls du weißt wann das Ereignis beginnt'; + + @override + String get broadcastCurrentGameUrl => 'URL der aktuellen Partie'; + + @override + String get broadcastDownloadAllRounds => 'Alle Runden herunterladen'; + + @override + String get broadcastResetRound => 'Diese Runde zurücksetzen'; + + @override + String get broadcastDeleteRound => 'Diese Runde löschen'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Lösche die Runde und ihre Partien endgültig.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Lösche alle Partien dieser Runde. Die Quelle muss aktiv sein, um sie neu zu erstellen.'; + + @override + String get broadcastEditRoundStudy => 'Rundenstudie bearbeiten'; + + @override + String get broadcastDeleteTournament => 'Dieses Turnier löschen'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Lösche definitiv das gesamte Turnier, alle seine Runden und Partien.'; + + @override + String get broadcastShowScores => 'Punktestand der Spieler basierend auf Spielergebnissen anzeigen'; + + @override + String get broadcastReplacePlayerTags => 'Optional: Spielernamen, Wertungen und Titel ersetzen'; + + @override + String get broadcastFideFederations => 'FIDE-Verbände'; + + @override + String get broadcastTop10Rating => 'Top-10-Wertung'; + + @override + String get broadcastFidePlayers => 'FIDE-Spieler'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE-Spieler nicht gefunden'; + + @override + String get broadcastFideProfile => 'FIDE-Profil'; + + @override + String get broadcastFederation => 'Verband'; + + @override + String get broadcastAgeThisYear => 'Alter in diesem Jahr'; + + @override + String get broadcastUnrated => 'Ungewertet'; + + @override + String get broadcastRecentTournaments => 'Letzte Turniere'; + + @override + String get broadcastOpenLichess => 'In Lichess öffnen'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Bretter'; + + @override + String get broadcastOverview => 'Überblick'; + + @override + String get broadcastSubscribeTitle => 'Abonnieren, um bei Rundenbeginn benachrichtigt zu werden. Du kannst in deinen Benutzereinstellungen für Übertragungen zwischen einer Benachrichtigung per Glocke oder per Push-Benachrichtigung wählen.'; + + @override + String get broadcastUploadImage => 'Turnierbild hochladen'; + + @override + String get broadcastNoBoardsYet => 'Noch keine Bretter vorhanden. Diese werden angezeigt, sobald die Partien hochgeladen werden.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Die Bretter können per Quelle oder via $param geladen werden'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Beginnt nach $param'; + } + + @override + String get broadcastStartVerySoon => 'Diese Übertragung wird in Kürze beginnen.'; + + @override + String get broadcastNotYetStarted => 'Die Übertragung hat noch nicht begonnen.'; + + @override + String get broadcastOfficialWebsite => 'Offizielle Webseite'; + + @override + String get broadcastStandings => 'Rangliste'; + + @override + String broadcastIframeHelp(String param) { + return 'Weitere Optionen auf der $param'; + } + + @override + String get broadcastWebmastersPage => 'Webmaster-Seite'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'Eine öffentliche Echtzeit-PGN-Quelle für diese Runde. Wir bieten auch eine $param für eine schnellere und effizientere Synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Bette diese Übertragung in deine Webseite ein'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Bette $param in deine Webseite ein'; + } + + @override + String get broadcastRatingDiff => 'Wertungsdifferenz'; + + @override + String get broadcastGamesThisTournament => 'Partien in diesem Turnier'; + + @override + String get broadcastScore => 'Punktestand'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Übertragungen', + one: '$count Übertragung', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Herausforderungen: $param1'; @@ -1390,10 +1615,10 @@ class AppLocalizationsDe extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Der Gegner ist in der Anzahl seiner Züge limitiert und jeder seiner Züge verschlechtert seine Stellung.'; @override - String get puzzleThemeHealthyMix => 'Gesunder Mix'; + String get puzzleThemeMix => 'Gesunder Mix'; @override - String get puzzleThemeHealthyMixDescription => 'Ein bisschen von Allem. Du weißt nicht, was dich erwartet, deshalb bleibst du auf alles vorbereitet! Genau wie in echten Partien.'; + String get puzzleThemeMixDescription => 'Ein bisschen von Allem. Du weißt nicht, was dich erwartet, deshalb bleibst du auf alles vorbereitet! Genau wie in echten Partien.'; @override String get puzzleThemePlayerGames => 'Partien von Spielern'; @@ -1797,9 +2022,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get removesTheDepthLimit => 'Entfernt die Tiefenbegrenzung und hält deinen Computer warm'; - @override - String get engineManager => 'Engineverwaltung'; - @override String get blunder => 'Grober Patzer'; @@ -2063,6 +2285,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get gamesPlayed => 'Gespielte Partien'; + @override + String get ok => 'OK'; + @override String get cancel => 'Abbrechen'; @@ -2772,7 +2997,13 @@ class AppLocalizationsDe extends AppLocalizations { String get other => 'Sonstiges'; @override - String get reportDescriptionHelp => 'Füge den Link zu einer oder mehreren Partien ein und erkläre die Auffälligkeiten bezüglich des Spielerverhaltens. Bitte schreibe nicht einfach nur „dieser Spieler betrügt“, sondern begründe auch, wie Du zu diesem Schluss kommst. Dein Bericht wird schneller bearbeitet, wenn er in englischer Sprache verfasst ist.'; + String get reportCheatBoostHelp => 'Füge den Link zu einer oder mehreren Partien ein und erkläre die Auffälligkeiten bezüglich des Verhaltens des Spielers. Bitte schreibe nicht einfach nur „Die schummeln (bzw. betrügen)“, sondern begründe auch, wie Du zu diesem Schluss kommst.'; + + @override + String get reportUsernameHelp => 'Erkläre, was an diesem Benutzernamen beleidigend oder unangemessen ist. Sage nicht einfach \"Der Name ist beleidigend/unangemessen\", sondern erkläre, wie du zu dieser Schlussfolgerung gekommen bist. Insbesondere wenn die Beleidigung verschleiert wird, nicht auf Englisch, ist in Slang, oder ist ein historischer/kultureller Bezugspunkt.'; + + @override + String get reportProcessedFasterInEnglish => 'Ihr Bericht wird schneller bearbeitet, wenn er auf Englisch verfasst ist.'; @override String get error_provideOneCheatedGameLink => 'Bitte gib mindestens einen Link zu einem Spiel an, in dem betrogen wurde.'; @@ -4057,7 +4288,7 @@ class AppLocalizationsDe extends AppLocalizations { String get lichessDbExplanation => 'Aus gewerteten Partien aller Lichess-Spieler'; @override - String get switchSides => 'andere Farbe'; + String get switchSides => 'Seitenwechsel'; @override String get closingAccountWithdrawAppeal => 'Dein Benutzerkonto zu schließen wird auch deinen Einspruch zurückziehen'; @@ -4077,6 +4308,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get nothingToSeeHere => 'Im Moment gibt es hier nichts zu sehen.'; + @override + String get stats => 'Statistiken'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4723,9 +4957,514 @@ class AppLocalizationsDe extends AppLocalizations { @override String get streamerLichessStreamers => 'Lichess Streamer'; + @override + String get studyPrivate => 'Privat'; + + @override + String get studyMyStudies => 'Meine Studien'; + + @override + String get studyStudiesIContributeTo => 'Studien, an denen ich mitwirke'; + + @override + String get studyMyPublicStudies => 'Meine öffentlichen Studien'; + + @override + String get studyMyPrivateStudies => 'Meine privaten Studien'; + + @override + String get studyMyFavoriteStudies => 'Meine Lieblingsstudien'; + + @override + String get studyWhatAreStudies => 'Was sind Studien?'; + + @override + String get studyAllStudies => 'Alle Studien'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Von $param erstellte Studien'; + } + + @override + String get studyNoneYet => 'Bisher keine.'; + + @override + String get studyHot => 'Angesagt'; + + @override + String get studyDateAddedNewest => 'Veröffentlichungsdatum (neueste)'; + + @override + String get studyDateAddedOldest => 'Veröffentlichungsdatum (älteste)'; + + @override + String get studyRecentlyUpdated => 'Kürzlich aktualisiert'; + + @override + String get studyMostPopular => 'Beliebteste'; + + @override + String get studyAlphabetical => 'Alphabetisch'; + + @override + String get studyAddNewChapter => 'Neues Kapitel hinzufügen'; + + @override + String get studyAddMembers => 'Mitglieder hinzufügen'; + + @override + String get studyInviteToTheStudy => 'Zur Studie einladen'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Bitte lade nur Leute ein, die dich kennen und die aktiv an dieser Studie teilnehmen möchten.'; + + @override + String get studySearchByUsername => 'Suche nach Benutzernamen'; + + @override + String get studySpectator => 'Zuschauer'; + + @override + String get studyContributor => 'Mitwirkender'; + + @override + String get studyKick => 'Rauswerfen'; + + @override + String get studyLeaveTheStudy => 'Studie verlassen'; + + @override + String get studyYouAreNowAContributor => 'Du bist jetzt ein Mitwirkender'; + + @override + String get studyYouAreNowASpectator => 'Du bist jetzt Zuschauer'; + + @override + String get studyPgnTags => 'PGN Tags'; + + @override + String get studyLike => 'Gefällt mir'; + + @override + String get studyUnlike => 'Gefällt mir nicht mehr'; + + @override + String get studyNewTag => 'Neuer Tag'; + + @override + String get studyCommentThisPosition => 'Kommentiere diese Stellung'; + + @override + String get studyCommentThisMove => 'Kommentiere diesen Zug'; + + @override + String get studyAnnotateWithGlyphs => 'Mit Symbolen kommentieren'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Das Kapitel ist zu kurz zum Analysieren.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Nur Mitwirkende an der Studie können eine Computeranalyse anfordern.'; + + @override + String get studyGetAFullComputerAnalysis => 'Erhalte eine vollständige serverseitige Computeranalyse der Hauptvariante.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Stelle sicher, dass das Kapitel vollständig ist. Die Analyse kann nur einmal angefordert werden.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Alle synchronisierten Mitglieder sehen die gleiche Stellung'; + + @override + String get studyShareChanges => 'Teile Änderungen mit den Zuschauern und speichere sie auf dem Server'; + + @override + String get studyPlaying => 'Laufende Partien'; + + @override + String get studyShowEvalBar => 'Stellungsbewertungs-Balken'; + + @override + String get studyFirst => 'Erste Seite'; + + @override + String get studyPrevious => 'Zurück'; + + @override + String get studyNext => 'Weiter'; + + @override + String get studyLast => 'Letzte Seite'; + @override String get studyShareAndExport => 'Teilen und exportieren'; + @override + String get studyCloneStudy => 'Klonen'; + + @override + String get studyStudyPgn => 'Studien PGN'; + + @override + String get studyDownloadAllGames => 'Lade alle Partien herunter'; + + @override + String get studyChapterPgn => 'Kapitel PGN'; + + @override + String get studyCopyChapterPgn => 'PGN kopieren'; + + @override + String get studyDownloadGame => 'Lade die Partie herunter'; + + @override + String get studyStudyUrl => 'Studien URL'; + + @override + String get studyCurrentChapterUrl => 'URL des aktuellen Kapitels'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Zum Einbinden füge dies im Forum ein'; + + @override + String get studyStartAtInitialPosition => 'Beginne mit der Anfangsstellung'; + + @override + String studyStartAtX(String param) { + return 'Beginne mit $param'; + } + + @override + String get studyEmbedInYourWebsite => 'In deine Webseite oder deinen Blog einbetten'; + + @override + String get studyReadMoreAboutEmbedding => 'Lies mehr über das Einbinden'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Nur öffentliche Studien können eingebunden werden!'; + + @override + String get studyOpen => 'Öffnen'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1 präsentiert von $param2'; + } + + @override + String get studyStudyNotFound => 'Studie nicht gefunden'; + + @override + String get studyEditChapter => 'Kapitel bearbeiten'; + + @override + String get studyNewChapter => 'Neues Kapitel'; + + @override + String studyImportFromChapterX(String param) { + return 'Importiere aus $param'; + } + + @override + String get studyOrientation => 'Ausrichtung'; + + @override + String get studyAnalysisMode => 'Analysemodus'; + + @override + String get studyPinnedChapterComment => 'Angepinnte Kapitelkommentare'; + + @override + String get studySaveChapter => 'Kapitel speichern'; + + @override + String get studyClearAnnotations => 'Anmerkungen löschen'; + + @override + String get studyClearVariations => 'Varianten löschen'; + + @override + String get studyDeleteChapter => 'Kapitel löschen'; + + @override + String get studyDeleteThisChapter => 'Kapitel löschen. Dies kann nicht rückgängig gemacht werden!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Alle Kommentare, Symbole und gezeichnete Formen in diesem Kapitel löschen'; + + @override + String get studyRightUnderTheBoard => 'Direkt unterhalb des Bretts'; + + @override + String get studyNoPinnedComment => 'Keine'; + + @override + String get studyNormalAnalysis => 'Normale Analyse'; + + @override + String get studyHideNextMoves => 'Nächste Züge ausblenden'; + + @override + String get studyInteractiveLesson => 'Interaktive Übung'; + + @override + String studyChapterX(String param) { + return 'Kapitel $param'; + } + + @override + String get studyEmpty => 'Leer'; + + @override + String get studyStartFromInitialPosition => 'Von Ausgangsstellung starten'; + + @override + String get studyEditor => 'Editor'; + + @override + String get studyStartFromCustomPosition => 'Von benutzerdefinierter Stellung starten'; + + @override + String get studyLoadAGameByUrl => 'Lade eine Partie mittels URL'; + + @override + String get studyLoadAPositionFromFen => 'Lade eine Partie mittels FEN'; + + @override + String get studyLoadAGameFromPgn => 'Lade eine Partie mittels PGN'; + + @override + String get studyAutomatic => 'Automatisch'; + + @override + String get studyUrlOfTheGame => 'URL der Partie'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Partie von $param1 oder $param2 laden'; + } + + @override + String get studyCreateChapter => 'Kapitel erstellen'; + + @override + String get studyCreateStudy => 'Studie erstellen'; + + @override + String get studyEditStudy => 'Studie bearbeiten'; + + @override + String get studyVisibility => 'Sichtbarkeit'; + + @override + String get studyPublic => 'Öffentlich'; + + @override + String get studyUnlisted => 'Ungelistet'; + + @override + String get studyInviteOnly => 'Nur mit Einladung'; + + @override + String get studyAllowCloning => 'Klonen erlaubt'; + + @override + String get studyNobody => 'Niemand'; + + @override + String get studyOnlyMe => 'Nur ich'; + + @override + String get studyContributors => 'Mitwirkende'; + + @override + String get studyMembers => 'Mitglieder'; + + @override + String get studyEveryone => 'Alle'; + + @override + String get studyEnableSync => 'Sync aktivieren'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Ja: Gleiche Stellung für alle'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Nein: Unabhängige Navigation für alle'; + + @override + String get studyPinnedStudyComment => 'Angepinnter Studienkommentar'; + @override String get studyStart => 'Start'; + + @override + String get studySave => 'Speichern'; + + @override + String get studyClearChat => 'Chat löschen'; + + @override + String get studyDeleteTheStudyChatHistory => 'Chatverlauf der Studie löschen? Dies kann nicht rückgängig gemacht werden!'; + + @override + String get studyDeleteStudy => 'Studie löschen'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Die gesamte Studie löschen? Es gibt kein Zurück! Gib zur Bestätigung den Namen der Studie ein: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Welche Studie möchtest du nutzen?'; + + @override + String get studyGoodMove => 'Guter Zug'; + + @override + String get studyMistake => 'Fehler'; + + @override + String get studyBrilliantMove => 'Brillanter Zug'; + + @override + String get studyBlunder => 'Grober Patzer'; + + @override + String get studyInterestingMove => 'Interessanter Zug'; + + @override + String get studyDubiousMove => 'Fragwürdiger Zug'; + + @override + String get studyOnlyMove => 'Einziger Zug'; + + @override + String get studyZugzwang => 'Zugzwang'; + + @override + String get studyEqualPosition => 'Ausgeglichene Stellung'; + + @override + String get studyUnclearPosition => 'Unklare Stellung'; + + @override + String get studyWhiteIsSlightlyBetter => 'Weiß steht leicht besser'; + + @override + String get studyBlackIsSlightlyBetter => 'Schwarz steht leicht besser'; + + @override + String get studyWhiteIsBetter => 'Weiß steht besser'; + + @override + String get studyBlackIsBetter => 'Schwarz steht besser'; + + @override + String get studyWhiteIsWinning => 'Weiß steht auf Gewinn'; + + @override + String get studyBlackIsWinning => 'Schwarz steht auf Gewinn'; + + @override + String get studyNovelty => 'Neuerung'; + + @override + String get studyDevelopment => 'Entwicklung'; + + @override + String get studyInitiative => 'Initiative'; + + @override + String get studyAttack => 'Angriff'; + + @override + String get studyCounterplay => 'Gegenspiel'; + + @override + String get studyTimeTrouble => 'Zeitnot'; + + @override + String get studyWithCompensation => 'Mit Kompensation'; + + @override + String get studyWithTheIdea => 'Mit der Idee'; + + @override + String get studyNextChapter => 'Nächstes Kapitel'; + + @override + String get studyPrevChapter => 'Vorheriges Kapitel'; + + @override + String get studyStudyActions => 'Studien-Aktionen'; + + @override + String get studyTopics => 'Themen'; + + @override + String get studyMyTopics => 'Meine Themen'; + + @override + String get studyPopularTopics => 'Beliebte Themen'; + + @override + String get studyManageTopics => 'Themen verwalten'; + + @override + String get studyBack => 'Zurück'; + + @override + String get studyPlayAgain => 'Erneut spielen'; + + @override + String get studyWhatWouldYouPlay => 'Was würdest du in dieser Stellung spielen?'; + + @override + String get studyYouCompletedThisLesson => 'Gratulation! Du hast diese Lektion abgeschlossen.'; + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Kapitel', + one: '$count Kapitel', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Partien', + one: '$count Partie', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Mitglieder', + one: '$count Mitglied', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Füge dein PGN Text hier ein, bis zu $count Partien', + one: 'Füge deinen PGN Text hier ein, bis zu $count Partie', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_el.dart b/lib/l10n/l10n_el.dart index d932457dea..02eda1d8ce 100644 --- a/lib/l10n/l10n_el.dart +++ b/lib/l10n/l10n_el.dart @@ -9,126 +9,123 @@ class AppLocalizationsEl extends AppLocalizations { AppLocalizationsEl([String locale = 'el']) : super(locale); @override - String get mobileHomeTab => 'Home'; + String get mobileHomeTab => 'Αρχική'; @override - String get mobilePuzzlesTab => 'Puzzles'; + String get mobilePuzzlesTab => 'Γρίφοι'; @override - String get mobileToolsTab => 'Tools'; + String get mobileToolsTab => 'Εργαλεία'; @override - String get mobileWatchTab => 'Watch'; + String get mobileWatchTab => 'Δείτε'; @override - String get mobileSettingsTab => 'Settings'; + String get mobileSettingsTab => 'Ρυθμίσεις'; @override - String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; + String get mobileMustBeLoggedIn => 'Πρέπει να συνδεθείτε για να δείτε αυτή τη σελίδα.'; @override - String get mobileSystemColors => 'System colors'; + String get mobileSystemColors => 'Χρώματα συστήματος'; @override - String get mobileFeedbackButton => 'Feedback'; + String get mobileFeedbackButton => 'Πείτε μας τη γνώμη σας'; @override - String get mobileOkButton => 'OK'; + String get mobileOkButton => 'ΟΚ'; @override - String get mobileSettingsHapticFeedback => 'Haptic feedback'; + String get mobileSettingsHapticFeedback => 'Απόκριση δόνησης'; @override String get mobileSettingsImmersiveMode => 'Immersive mode'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileSettingsImmersiveModeSubtitle => 'Αποκρύπτει τη διεπαφή του συστήματος όσο παίζεται. Ενεργοποιήστε εάν σας ενοχλούν οι χειρονομίες πλοήγησης του συστήματος στα άκρα της οθόνης. Ισχύει για την προβολή παιχνιδιού και το Puzzle Storm.'; @override - String get mobileNotFollowingAnyUser => 'You are not following any user.'; + String get mobileNotFollowingAnyUser => 'Δεν ακολουθείτε κανέναν χρήστη.'; @override - String get mobileAllGames => 'All games'; + String get mobileAllGames => 'Όλα τα παιχνίδια'; @override - String get mobileRecentSearches => 'Recent searches'; + String get mobileRecentSearches => 'Πρόσφατες αναζητήσεις'; @override - String get mobileClearButton => 'Clear'; + String get mobileClearButton => 'Εκκαθάριση'; @override String mobilePlayersMatchingSearchTerm(String param) { - return 'Players with \"$param\"'; + return 'Παίκτες με \"$param\"'; } @override - String get mobileNoSearchResults => 'No results'; + String get mobileNoSearchResults => 'Δεν βρέθηκαν αποτελέσματα'; @override - String get mobileAreYouSure => 'Are you sure?'; + String get mobileAreYouSure => 'Είστε σίγουροι;'; @override String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; @override - String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; + String get mobilePuzzleStormNothingToShow => 'Δεν υπάρχουν στοιχεία. Παίξτε κάποιους γύρους Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Share this puzzle'; + String get mobileSharePuzzle => 'Κοινοποίηση γρίφου'; @override - String get mobileShareGameURL => 'Share game URL'; + String get mobileShareGameURL => 'Κοινοποίηση URL παιχνιδιού'; @override - String get mobileShareGamePGN => 'Share PGN'; + String get mobileShareGamePGN => 'Κοινοποίηση PGN'; @override - String get mobileSharePositionAsFEN => 'Share position as FEN'; + String get mobileSharePositionAsFEN => 'Κοινοποίηση θέσης ως FEN'; @override - String get mobileShowVariations => 'Show variations'; + String get mobileShowVariations => 'Εμφάνιση παραλλαγών'; @override - String get mobileHideVariation => 'Hide variation'; + String get mobileHideVariation => 'Απόκρυψη παραλλαγής'; @override - String get mobileShowComments => 'Show comments'; + String get mobileShowComments => 'Εμφάνιση σχολίων'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get mobilePuzzleStormConfirmEndRun => 'Θέλετε να τερματίσετε αυτόν τον γύρο;'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobilePuzzleStormFilterNothingToShow => 'Δεν υπάρχουν γρίφοι για τις συγκεκριμένες επιλογές φίλτρων, παρακαλώ δοκιμάστε κάποιες άλλες'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; + String get mobileCancelTakebackOffer => 'Ακυρώστε την προσφορά αναίρεσης της κίνησης'; @override - String get mobileCancelDrawOffer => 'Cancel draw offer'; + String get mobileWaitingForOpponentToJoin => 'Αναμονή για αντίπαλο...'; @override - String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; + String get mobileBlindfoldMode => 'Τυφλό'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get mobileLiveStreamers => 'Streamers ζωντανά αυτή τη στιγμή'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileCustomGameJoinAGame => 'Συμμετοχή σε παιχνίδι'; @override - String get mobileCustomGameJoinAGame => 'Join a game'; + String get mobileCorrespondenceClearSavedMove => 'Εκκαθάριση αποθηκευμένης κίνησης'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; - - @override - String get mobileSomethingWentWrong => 'Something went wrong.'; + String get mobileSomethingWentWrong => 'Κάτι πήγε στραβά.'; @override String get mobileShowResult => 'Εμφάνιση αποτελέσματος'; @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobilePuzzleThemesSubtitle => 'Παίξτε γρίφους από τα αγαπημένα σας ανοίγματα, ή επιλέξτε θέμα.'; @override String get mobilePuzzleStormSubtitle => 'Λύστε όσους γρίφους όσο το δυνατόν, σε 3 λεπτά.'; @@ -142,7 +139,7 @@ class AppLocalizationsEl extends AppLocalizations { String get mobileGreetingWithoutName => 'Καλωσορίσατε'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobilePrefMagnifyDraggedPiece => 'Μεγέθυνση του επιλεγμένου κομματιού'; @override String get activityActivity => 'Δραστηριότητα'; @@ -246,6 +243,17 @@ class AppLocalizationsEl extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Ολοκλήρωσε $count παρτίδες αλληλογραφίας $param2', + one: 'Ολοκλήρωσε $count παρτίδα αλληλογραφίας $param2', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,226 @@ class AppLocalizationsEl extends AppLocalizations { @override String get broadcastBroadcasts => 'Αναμεταδόσεις'; + @override + String get broadcastMyBroadcasts => 'Οι αναμεταδόσεις μου'; + @override String get broadcastLiveBroadcasts => 'Αναμεταδόσεις ζωντανών τουρνούα'; + @override + String get broadcastBroadcastCalendar => 'Broadcast calendar'; + + @override + String get broadcastNewBroadcast => 'Νέα ζωντανή αναμετάδοση'; + + @override + String get broadcastSubscribedBroadcasts => 'Εγγεγραμμένες μεταδώσεις'; + + @override + String get broadcastAboutBroadcasts => 'Σχετικά με εκπομπές'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Πώς να χρησιμοποιήσετε τις εκπομπές Lichess.'; + + @override + String get broadcastTheNewRoundHelp => 'Ο νέος γύρος θα έχει τα ίδια μέλη και τους ίδιους συνεισφέροντες όπως ο προηγούμενος.'; + + @override + String get broadcastAddRound => 'Προσθήκη γύρου'; + + @override + String get broadcastOngoing => 'Σε εξέλιξη'; + + @override + String get broadcastUpcoming => 'Προσεχή'; + + @override + String get broadcastCompleted => 'Ολοκληρώθηκε'; + + @override + String get broadcastCompletedHelp => 'Το Lichess ανιχνεύει ολοκλήρωση γύρων, αλλά μπορεί να κάνει λάθος. Χρησιμοποιήστε αυτό για να το ρυθμίσετε χειροκίνητα.'; + + @override + String get broadcastRoundName => 'Όνομα γύρου'; + + @override + String get broadcastRoundNumber => 'Αριθμός γύρου'; + + @override + String get broadcastTournamentName => 'Όνομα τουρνουά'; + + @override + String get broadcastTournamentDescription => 'Σύντομη περιγραφή τουρνουά'; + + @override + String get broadcastFullDescription => 'Πλήρης περιγραφή γεγονότος'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Προαιρετική αναλυτική περιγραφή της αναμετάδοσης. Η μορφή $param1 είναι διαθέσιμη. Το μήκος πρέπει μικρότερο από $param2 χαρακτήρες.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN Source URL'; + + @override + String get broadcastSourceUrlHelp => 'URL για λήψη PGN ενημερώσεων. Πρέπει να είναι δημόσια προσβάσιμο μέσω διαδικτύου.'; + + @override + String get broadcastSourceGameIds => 'Up to 64 Lichess game IDs, separated by spaces.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Start date in the tournament local timezone: $param'; + } + + @override + String get broadcastStartDateHelp => 'Προαιρετικό, εάν γνωρίζετε πότε αρχίζει η εκδήλωση'; + + @override + String get broadcastCurrentGameUrl => 'Διεύθυνση URL αυτού του παιχνιδιού'; + + @override + String get broadcastDownloadAllRounds => 'Λήψη όλων των γύρων'; + + @override + String get broadcastResetRound => 'Επαναφορά αυτού του γύρου'; + + @override + String get broadcastDeleteRound => 'Διαγραφή αυτού του γύρου'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Σίγουρα διαγράψτε τον γύρο και όλα τα παιχνίδια του.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Διαγράψτε όλα τα παιχνίδια αυτού του γύρου. Η πηγή μετάδοσης θα πρέπει να είναι ενεργή για να τα ξαναδημιουργήσετε.'; + + @override + String get broadcastEditRoundStudy => 'Επεξεργασία μελέτης γύρου'; + + @override + String get broadcastDeleteTournament => 'Διαγραφή αυτού του τουρνουά'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Σίγουρα διαγράψτε ολόκληρο τον διαγωνισμό, όλους τους γύρους του και όλα τα παιχνίδια του.'; + + @override + String get broadcastShowScores => 'Show players scores based on game results'; + + @override + String get broadcastReplacePlayerTags => 'Optional: replace player names, ratings and titles'; + + @override + String get broadcastFideFederations => 'Ομοσπονδίες FIDE'; + + @override + String get broadcastTop10Rating => 'Top 10 rating'; + + @override + String get broadcastFidePlayers => 'Παίκτες FIDE'; + + @override + String get broadcastFidePlayerNotFound => 'Δε βρέθηκε παίκτης FIDE'; + + @override + String get broadcastFideProfile => 'Προφίλ FIDE'; + + @override + String get broadcastFederation => 'Ομοσπονδία'; + + @override + String get broadcastAgeThisYear => 'Φετινή ηλικία'; + + @override + String get broadcastUnrated => 'Unrated'; + + @override + String get broadcastRecentTournaments => 'Πρόσφατα τουρνουά'; + + @override + String get broadcastOpenLichess => 'Άνοιγμα στο Lichess'; + + @override + String get broadcastTeams => 'Ομάδες'; + + @override + String get broadcastBoards => 'Σκακιέρες'; + + @override + String get broadcastOverview => 'Επισκόπηση'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Ανεβάστε εικόνα τουρνουά'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Ξεκινάει μετά από $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Επίσημη ιστοσελίδα'; + + @override + String get broadcastStandings => 'Κατάταξη'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Διαφορά βαθμολογίας'; + + @override + String get broadcastGamesThisTournament => 'Παρτίδες σε αυτό το τουρνουά'; + + @override + String get broadcastScore => 'Βαθμολογία'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count αναμεταδόσεις', + one: '$count αναμετάδοση', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Προκλήσεις: $param1'; @@ -556,7 +781,7 @@ class AppLocalizationsEl extends AppLocalizations { String get preferencesDisplay => 'Εμφάνιση'; @override - String get preferencesPrivacy => 'Απόρρητο'; + String get preferencesPrivacy => 'Ιδιωτικότητα'; @override String get preferencesNotifications => 'Ειδοποιήσεις'; @@ -670,7 +895,7 @@ class AppLocalizationsEl extends AppLocalizations { String get preferencesMoveConfirmation => 'Επιβεβαίωση κίνησης'; @override - String get preferencesExplainCanThenBeTemporarilyDisabled => 'Μπορεί να απενεργοποιηθεί κατά τη διάρκεια ενός παιχνιδιού με το μενού του ταμπλό'; + String get preferencesExplainCanThenBeTemporarilyDisabled => 'Μπορεί να απενεργοποιηθεί κατά τη διάρκεια ενός παιχνιδιού με το μενού της σκακιέρας'; @override String get preferencesInCorrespondenceGames => 'Στις παρτίδες δι\' αλληλογραφίας'; @@ -790,10 +1015,10 @@ class AppLocalizationsEl extends AppLocalizations { String get puzzleVoteToLoadNextOne => 'Ψηφίστε για να προχωρήσετε στο επόμενο!'; @override - String get puzzleUpVote => 'Up vote puzzle'; + String get puzzleUpVote => 'Μου άρεσε ο γρίφος'; @override - String get puzzleDownVote => 'Down vote puzzle'; + String get puzzleDownVote => 'Δε μου άρεσε ο γρίφος'; @override String get puzzleYourPuzzleRatingWillNotChange => 'Οι βαθμοί αξιολόγησής σας δε θα αλλάξουν. Αυτοί οι βαθμοί χρησιμεύουν στην επιλογή γρίφων για το επίπεδό σας και όχι στον ανταγωνισμό.'; @@ -837,19 +1062,19 @@ class AppLocalizationsEl extends AppLocalizations { String get puzzlePuzzleComplete => 'Ο γρίφος ολοκληρώθηκε!'; @override - String get puzzleByOpenings => 'By openings'; + String get puzzleByOpenings => 'Ανά άνοιγμα'; @override - String get puzzlePuzzlesByOpenings => 'Puzzles by openings'; + String get puzzlePuzzlesByOpenings => 'Γρίφοι ανά άνοιγμα'; @override - String get puzzleOpeningsYouPlayedTheMost => 'Openings you played the most in rated games'; + String get puzzleOpeningsYouPlayedTheMost => 'Ανοίγματα που παίξατε πιο συχνά σε βαθμολογημένες παρτίδες σκάκι'; @override - String get puzzleUseFindInPage => 'Use \"Find in page\" in the browser menu to find your favourite opening!'; + String get puzzleUseFindInPage => 'Πατήστε «Εύρεση στη σελίδα» στο μενού του προγράμματος περιήγησης, για να βρείτε το αγαπημένο σας άνοιγμα!'; @override - String get puzzleUseCtrlF => 'Use Ctrl+f to find your favourite opening!'; + String get puzzleUseCtrlF => 'Πατήστε Ctrl+f για να βρείτε το αγαπημένο σας άνοιγμα!'; @override String get puzzleNotTheMove => 'Δεν είναι αυτή η κίνηση!'; @@ -1390,10 +1615,10 @@ class AppLocalizationsEl extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Ο αντίπαλος είναι περιορισμένος στις κινήσεις που μπορεί να κάνει και οποιαδήποτε κίνηση επιλέξει επιδεινώνει την θέση του.'; @override - String get puzzleThemeHealthyMix => 'Προτεινόμενο μίγμα'; + String get puzzleThemeMix => 'Προτεινόμενο μίγμα'; @override - String get puzzleThemeHealthyMixDescription => 'Λίγο απ\' όλα. Δεν ξέρετε τι να περιμένετε, οπότε παραμένετε σε ετοιμότητα! Όπως στα πραγματικά παιχνίδια.'; + String get puzzleThemeMixDescription => 'Λίγο απ\' όλα. Δεν ξέρετε τι να περιμένετε, οπότε παραμένετε σε ετοιμότητα! Όπως στα πραγματικά παιχνίδια.'; @override String get puzzleThemePlayerGames => 'Παιχνίδια παίκτη'; @@ -1636,10 +1861,10 @@ class AppLocalizationsEl extends AppLocalizations { String get deleteFromHere => 'Διαγραφή από εδώ'; @override - String get collapseVariations => 'Collapse variations'; + String get collapseVariations => 'Σύμπτυξη παραλλαγών'; @override - String get expandVariations => 'Expand variations'; + String get expandVariations => 'Εμφάνιση βαριάντων'; @override String get forceVariation => 'Θέσε σε βαριάντα'; @@ -1699,7 +1924,7 @@ class AppLocalizationsEl extends AppLocalizations { @override String masterDbExplanation(String param1, String param2, String param3) { - return 'Δύο εκατομμύρια OTB παρτίδες $param1 + παικτών με αξιολόγηση FIDE από $param2 έως $param3'; + return 'Παρτίδες OTB $param1 + παικτών με αξιολόγηση FIDE, από $param2 έως $param3'; } @override @@ -1797,9 +2022,6 @@ class AppLocalizationsEl extends AppLocalizations { @override String get removesTheDepthLimit => 'Καταργεί το όριο βάθους και κρατά τον υπολογιστή σας ζεστό'; - @override - String get engineManager => 'Διαχειριστής μηχανής'; - @override String get blunder => 'Σοβαρό σφάλμα'; @@ -1878,7 +2100,7 @@ class AppLocalizationsEl extends AppLocalizations { String get friends => 'Φίλοι'; @override - String get otherPlayers => 'other players'; + String get otherPlayers => 'άλλους παίκτες'; @override String get discussions => 'Συζητήσεις'; @@ -2063,6 +2285,9 @@ class AppLocalizationsEl extends AppLocalizations { @override String get gamesPlayed => 'Παιγμένα παιχνίδια'; + @override + String get ok => 'ΟΚ'; + @override String get cancel => 'Ακύρωση'; @@ -2631,7 +2856,7 @@ class AppLocalizationsEl extends AppLocalizations { String get editProfile => 'Επεξεργασία προφίλ'; @override - String get realName => 'Real name'; + String get realName => 'Πραγματικό όνομα'; @override String get setFlair => 'Ορίστε τη νιφάδα σας'; @@ -2712,10 +2937,10 @@ class AppLocalizationsEl extends AppLocalizations { String get yes => 'Ναι'; @override - String get website => 'Website'; + String get website => 'Ιστοσελίδα'; @override - String get mobile => 'Mobile'; + String get mobile => 'Εφαρμογή Κινητού'; @override String get help => 'Βοήθεια:'; @@ -2772,7 +2997,13 @@ class AppLocalizationsEl extends AppLocalizations { String get other => 'Άλλο'; @override - String get reportDescriptionHelp => 'Κάντε επικόλληση τον σύνδεσμο για το παιχνίδι(α) και εξηγήστε τι είναι παράξενο στη συμπεριφορά του χρήστη. Μην πείτε απλά «επειδή κλέβει», πείτε μας πως καταλήξατε σε αυτό το συμπέρασμα. Η αναφορά σας θα επεξεργαστεί πιο γρήγορα αν είναι γραμμένη στα αγγλικά.'; + String get reportCheatBoostHelp => 'Επικολλήστε τους συνδέσμους με τα παιχνίδια και εξηγήστε μας γιατί θεωρείτε ότι η συμπεριφορά του χρήστη είναι παράξενη σε αυτά. Μη λέτε απλώς ότι «κλέβει» (\"they cheat\"), αλλά πείτε μας πως καταλήξατε σε αυτό το συμπέρασμα.'; + + @override + String get reportUsernameHelp => 'Εξηγήστε μας γιατί είναι προσβλητικό το όνομα αυτού του χρήστη. Μη λέτε απλώς ότι \"είναι προσβλητικό/ακατάλληλο\" (\"it\'s offensive/inappropriate\"), αλλά πείτε μας πώς καταλήξατε σε αυτό το συμπέρασμα, ειδικά αν πρόκειται για προσβολή η οποία δεν είναι ιδιαίτερα εμφανής: για παράδειγμα αν δεν είναι στα αγγλικά, είναι σε κάποια αργκό ή κάνει κάποια προσβλητική ιστορική/πολιτιστική αναφορά.'; + + @override + String get reportProcessedFasterInEnglish => 'Η αναφορά σας θα επεξεργαστεί γρηγορότερα αν είναι γραμμένη στα αγγλικά.'; @override String get error_provideOneCheatedGameLink => 'Καταχωρίστε τουλάχιστον έναν σύνδεσμο σε ένα παιχνίδι εξαπάτησης.'; @@ -3152,7 +3383,7 @@ class AppLocalizationsEl extends AppLocalizations { String get lichessTournaments => 'Τουρνουά στο Lichess'; @override - String get tournamentFAQ => 'Τεκμηρίωση τουρνουά τύπου αρένας'; + String get tournamentFAQ => 'Τεκμηρίωση για τουρνουά τύπου αρένας'; @override String get timeBeforeTournamentStarts => 'Χρόνος προτού ξεκινήσει το τουρνουά'; @@ -3323,7 +3554,7 @@ class AppLocalizationsEl extends AppLocalizations { String get crosstable => 'Αποτελέσματα'; @override - String get youCanAlsoScrollOverTheBoardToMoveInTheGame => 'Μπορείτε επίσης να κινηθείτε πάνω στην σκακιέρα για να πάτε στο παιχνίδι.'; + String get youCanAlsoScrollOverTheBoardToMoveInTheGame => 'Μπορείτε επίσης να κινήσετε πάνω στην σκακιέρα για να μετακινηθείτε στο παιχνίδι.'; @override String get scrollOverComputerVariationsToPreviewThem => 'Μετακινήστε το ποντίκι σας πάνω στις βαριάντες του υπολογιστή για την προεπισκόπησή τους.'; @@ -4077,6 +4308,9 @@ class AppLocalizationsEl extends AppLocalizations { @override String get nothingToSeeHere => 'Τίποτα για να δείτε εδώ αυτή τη στιγμή.'; + @override + String get stats => 'Στατιστικά'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4723,9 +4957,514 @@ class AppLocalizationsEl extends AppLocalizations { @override String get streamerLichessStreamers => 'Lichess streamers'; + @override + String get studyPrivate => 'Ιδιωτικό'; + + @override + String get studyMyStudies => 'Οι μελέτες μου'; + + @override + String get studyStudiesIContributeTo => 'Μελέτες που συνεισφέρω'; + + @override + String get studyMyPublicStudies => 'Οι δημόσιες μελέτες μου'; + + @override + String get studyMyPrivateStudies => 'Οι ιδιωτικές μελέτες μου'; + + @override + String get studyMyFavoriteStudies => 'Οι αγαπημένες μελέτες μου'; + + @override + String get studyWhatAreStudies => 'Τι είναι οι μελέτες;'; + + @override + String get studyAllStudies => 'Όλες οι μελέτες'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Μελέτες που δημιουργήθηκαν από τον/την $param'; + } + + @override + String get studyNoneYet => 'Τίποτα ακόμη εδώ.'; + + @override + String get studyHot => 'Δημοφιλείς (hot)'; + + @override + String get studyDateAddedNewest => 'Ημερομηνία προσθήκης (νεότερες)'; + + @override + String get studyDateAddedOldest => 'Ημερομηνία προσθήκης (παλαιότερες)'; + + @override + String get studyRecentlyUpdated => 'Πρόσφατα ενημερωμένες'; + + @override + String get studyMostPopular => 'Οι πιο δημοφιλείς'; + + @override + String get studyAlphabetical => 'Αλφαβητικά'; + + @override + String get studyAddNewChapter => 'Προσθήκη νέου κεφαλαίου'; + + @override + String get studyAddMembers => 'Προσθήκη μελών'; + + @override + String get studyInviteToTheStudy => 'Προσκάλεσε στην μελέτη'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Παρακαλώ, προσκαλέστε μόνο άτομα που γνωρίζετε και που θέλουν να συμμετέχουν ενεργά σε αυτήν την μελέτη.'; + + @override + String get studySearchByUsername => 'Αναζήτηση με όνομα χρήστη'; + + @override + String get studySpectator => 'Θεατής'; + + @override + String get studyContributor => 'Συνεισφέρων'; + + @override + String get studyKick => 'Διώξε'; + + @override + String get studyLeaveTheStudy => 'Αποχώρησε από αυτήν την μελέτη'; + + @override + String get studyYouAreNowAContributor => 'Μπορείτε τώρα να συνεισφέρετε στην μελέτη'; + + @override + String get studyYouAreNowASpectator => 'Είστε πλέον θεατής'; + + @override + String get studyPgnTags => 'PGN ετικέτες'; + + @override + String get studyLike => 'Μου αρέσει'; + + @override + String get studyUnlike => 'Δε μου αρέσει'; + + @override + String get studyNewTag => 'Νέα ετικέτα'; + + @override + String get studyCommentThisPosition => 'Σχολίασε την υπάρχουσα θέση'; + + @override + String get studyCommentThisMove => 'Σχολίασε αυτήν την κίνηση'; + + @override + String get studyAnnotateWithGlyphs => 'Σχολιασμός με σύμβολα'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Το κεφάλαιο είναι πολύ μικρό για να αναλυθεί.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Μόνο αυτοί που συνεισφέρουν στην σπουδή μπορούν να ζητήσουν ανάλυση από υπολογιστή.'; + + @override + String get studyGetAFullComputerAnalysis => 'Αίτηση πλήρης ανάλυσης της κύριας γραμμής παρτίδας από μηχανή του σέρβερ.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Σιγουρευτείτε ότι το κεφάλαιο είναι ολοκληρωμένο. Μπορείτε να ζητήσετε ανάλυση μόνο μια φορά.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Όλα τα συγχρονισμένα μέλη παραμένουν στην ίδια θέση'; + + @override + String get studyShareChanges => 'Διαμοιρασμός στους θεατές των αλλαγών και αποθήκευση τους στο σέρβερ'; + + @override + String get studyPlaying => 'Παίζονται'; + + @override + String get studyShowEvalBar => 'Μπάρες αξιολόγησης'; + + @override + String get studyFirst => 'Πρώτη'; + + @override + String get studyPrevious => 'Προηγούμενη'; + + @override + String get studyNext => 'Επόμενη'; + + @override + String get studyLast => 'Τελευταία'; + @override String get studyShareAndExport => 'Διαμοιρασμός & εξαγωγή'; + @override + String get studyCloneStudy => 'Κλωνοποίησε'; + + @override + String get studyStudyPgn => 'PGN της μελέτης'; + + @override + String get studyDownloadAllGames => 'Λήψη όλων των παιχνιδιών'; + + @override + String get studyChapterPgn => 'PGN του κεφαλαίου'; + + @override + String get studyCopyChapterPgn => 'Αντιγραφή PGN'; + + @override + String get studyDownloadGame => 'Λήψη παιχνιδιού'; + + @override + String get studyStudyUrl => 'URL μελέτης'; + + @override + String get studyCurrentChapterUrl => 'Τρέχον κεφάλαιο URL'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Επικολλήστε το παρόν για ενσωμάτωση στο φόρουμ'; + + @override + String get studyStartAtInitialPosition => 'Ξεκινάει από αρχική θέση'; + + @override + String studyStartAtX(String param) { + return 'Ξεκινάει με $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Ενσωματώστε στην ιστοσελίδα σας ή το μπλογκ σας'; + + @override + String get studyReadMoreAboutEmbedding => 'Διαβάστε περισσότερα για την ενσωμάτωση'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Μόνο δημόσιες μελέτες μπορούν να ενσωματωθούν!'; + + @override + String get studyOpen => 'Άνοιξε'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1, δημιουργήθηκε από $param2'; + } + + @override + String get studyStudyNotFound => 'Η μελέτη δεν βρέθηκε'; + + @override + String get studyEditChapter => 'Επεξεργάσου το κεφάλαιο'; + + @override + String get studyNewChapter => 'Νέο κεφάλαιο'; + + @override + String studyImportFromChapterX(String param) { + return 'Εισαγωγή από $param'; + } + + @override + String get studyOrientation => 'Προσανατολισμός'; + + @override + String get studyAnalysisMode => 'Τύπος ανάλυσης'; + + @override + String get studyPinnedChapterComment => 'Καρφιτσωμένο σχόλιο κεφαλαίου'; + + @override + String get studySaveChapter => 'Αποθήκευση κεφαλαίου'; + + @override + String get studyClearAnnotations => 'Διαγραφή σχολιασμών'; + + @override + String get studyClearVariations => 'Εκκαθάριση βαριάντων'; + + @override + String get studyDeleteChapter => 'Διαγραφή κεφαλαίου'; + + @override + String get studyDeleteThisChapter => 'Διαγραφή κεφαλαίου; Μη αναιρέσιμη ενέργεια!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Καθαρισμός όλων των σχολίων, συμβόλων και σχεδίων στο τρέχον κεφάλαιο;'; + + @override + String get studyRightUnderTheBoard => 'Κάτω από την σκακιέρα'; + + @override + String get studyNoPinnedComment => 'Καμία'; + + @override + String get studyNormalAnalysis => 'Απλή ανάλυση'; + + @override + String get studyHideNextMoves => 'Απόκρυψη επόμενων κινήσεων'; + + @override + String get studyInteractiveLesson => 'Διαδραστικό μάθημα'; + + @override + String studyChapterX(String param) { + return 'Κεφάλαιο $param'; + } + + @override + String get studyEmpty => 'Κενή'; + + @override + String get studyStartFromInitialPosition => 'Έναρξη από τρέχουσα θέση'; + + @override + String get studyEditor => 'Επεξεργαστής'; + + @override + String get studyStartFromCustomPosition => 'Έναρξη από τρέχουσα θέση'; + + @override + String get studyLoadAGameByUrl => 'Φόρτωση παρτίδας με URL'; + + @override + String get studyLoadAPositionFromFen => 'Φόρτωση θέσης από FEN'; + + @override + String get studyLoadAGameFromPgn => 'Φόρτωσε μια παρτίδα από PGN'; + + @override + String get studyAutomatic => 'Αυτόματο'; + + @override + String get studyUrlOfTheGame => 'URL παρτίδων, ένα ανά γραμμή'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Φόρτωση παρτίδων από $param1 ή $param2'; + } + + @override + String get studyCreateChapter => 'Δημιουργία κεφαλαίου'; + + @override + String get studyCreateStudy => 'Δημιουργία μελέτης'; + + @override + String get studyEditStudy => 'Επεξεργασία μελέτης'; + + @override + String get studyVisibility => 'Ορατότητα'; + + @override + String get studyPublic => 'Δημόσια'; + + @override + String get studyUnlisted => 'Ακαταχώρητη'; + + @override + String get studyInviteOnly => 'Με πρόσκληση'; + + @override + String get studyAllowCloning => 'Επέτρεψε αντιγραφή'; + + @override + String get studyNobody => 'Κανένας'; + + @override + String get studyOnlyMe => 'Μόνο εγώ'; + + @override + String get studyContributors => 'Συνεισφέροντες'; + + @override + String get studyMembers => 'Μέλη'; + + @override + String get studyEveryone => 'Οποιοσδήποτε'; + + @override + String get studyEnableSync => 'Ενεργοποίηση συγχρονισμού'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Ναι: όλοι βλέπουν την ίδια θέση'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Όχι: ελεύθερη επιλογή θέσης'; + + @override + String get studyPinnedStudyComment => 'Καρφιτσωμένο σχόλιο μελέτης'; + @override String get studyStart => 'Δημιουργία'; + + @override + String get studySave => 'Αποθήκευση'; + + @override + String get studyClearChat => 'Εκκαθάριση συνομιλίας'; + + @override + String get studyDeleteTheStudyChatHistory => 'Διαγραφή συνομιλίας μελέτης; Μη αναιρέσιμη ενέργεια!'; + + @override + String get studyDeleteStudy => 'Διαγραφή μελέτης'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Να διαγραφεί όλη η μελέτη; Η ενέργεια αυτή δεν μπορεί να αναιρεθεί! Πληκτρολογήστε το όνομα της μελέτης για επιβεβαίωση: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Που θέλετε να δημιουργήσετε την μελέτη;'; + + @override + String get studyGoodMove => 'Καλή κίνηση'; + + @override + String get studyMistake => 'Λάθος'; + + @override + String get studyBrilliantMove => 'Εξαιρετική κίνηση'; + + @override + String get studyBlunder => 'Σοβαρό λάθος'; + + @override + String get studyInterestingMove => 'Ενδιαφέρουσα κίνηση'; + + @override + String get studyDubiousMove => 'Κίνηση αμφίβολης αξίας'; + + @override + String get studyOnlyMove => 'Μοναδική κίνηση'; + + @override + String get studyZugzwang => 'Τσούγκσβανγκ'; + + @override + String get studyEqualPosition => 'Ισόπαλη θέση'; + + @override + String get studyUnclearPosition => 'Ασαφής θέση'; + + @override + String get studyWhiteIsSlightlyBetter => 'Το λευκά είναι ελαφρώς καλύτερα'; + + @override + String get studyBlackIsSlightlyBetter => 'Το μαύρα είναι ελαφρώς καλύτερα'; + + @override + String get studyWhiteIsBetter => 'Τα λευκά είναι καλύτερα'; + + @override + String get studyBlackIsBetter => 'Τα μαύρα είναι καλύτερα'; + + @override + String get studyWhiteIsWinning => 'Τα λευκά κερδίζουν'; + + @override + String get studyBlackIsWinning => 'Τα μαύρα κερδίζουν'; + + @override + String get studyNovelty => 'Novelty'; + + @override + String get studyDevelopment => 'Ανάπτυξη'; + + @override + String get studyInitiative => 'Πρωτοβουλία'; + + @override + String get studyAttack => 'Επίθεση'; + + @override + String get studyCounterplay => 'Αντεπίθεση'; + + @override + String get studyTimeTrouble => 'Πίεση χρόνου'; + + @override + String get studyWithCompensation => 'Με αντάλλαγμα'; + + @override + String get studyWithTheIdea => 'Με ιδέα'; + + @override + String get studyNextChapter => 'Επόμενο κεφάλαιο'; + + @override + String get studyPrevChapter => 'Προηγούμενο κεφάλαιο'; + + @override + String get studyStudyActions => 'Ρυθμίσεις μελέτης'; + + @override + String get studyTopics => 'Θέματα'; + + @override + String get studyMyTopics => 'Τα θέματά μου'; + + @override + String get studyPopularTopics => 'Δημοφιλή θέματα'; + + @override + String get studyManageTopics => 'Διαχείριση θεμάτων'; + + @override + String get studyBack => 'Πίσω'; + + @override + String get studyPlayAgain => 'Παίξτε ξανά'; + + @override + String get studyWhatWouldYouPlay => 'Τι θα παίζατε σε αυτή τη θέση;'; + + @override + String get studyYouCompletedThisLesson => 'Συγχαρητήρια! Ολοκληρώσατε αυτό το μάθημα.'; + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Κεφάλαια', + one: '$count Κεφάλαιο', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Παρτίδες', + one: '$count Παρτίδα', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Μέλη', + one: '$count Μέλος', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Επικολλήστε το PGN εδώ, μέχρι $count παρτίδες', + one: 'Επικολλήστε το PGN εδώ, μέχρι $count παρτίδα', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_en.dart b/lib/l10n/l10n_en.dart index 949d04a151..d115d4734b 100644 --- a/lib/l10n/l10n_en.dart +++ b/lib/l10n/l10n_en.dart @@ -103,9 +103,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get mobileCancelTakebackOffer => 'Cancel takeback offer'; - @override - String get mobileCancelDrawOffer => 'Cancel draw offer'; - @override String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @@ -246,6 +243,17 @@ class AppLocalizationsEn extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Completed $count $param2 correspondence games', + one: 'Completed $count $param2 correspondence game', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,226 @@ class AppLocalizationsEn extends AppLocalizations { @override String get broadcastBroadcasts => 'Broadcasts'; + @override + String get broadcastMyBroadcasts => 'My broadcasts'; + @override String get broadcastLiveBroadcasts => 'Live tournament broadcasts'; + @override + String get broadcastBroadcastCalendar => 'Broadcast calendar'; + + @override + String get broadcastNewBroadcast => 'New live broadcast'; + + @override + String get broadcastSubscribedBroadcasts => 'Subscribed broadcasts'; + + @override + String get broadcastAboutBroadcasts => 'About broadcasts'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'How to use Lichess Broadcasts.'; + + @override + String get broadcastTheNewRoundHelp => 'The new round will have the same members and contributors as the previous one.'; + + @override + String get broadcastAddRound => 'Add a round'; + + @override + String get broadcastOngoing => 'Ongoing'; + + @override + String get broadcastUpcoming => 'Upcoming'; + + @override + String get broadcastCompleted => 'Completed'; + + @override + String get broadcastCompletedHelp => 'Lichess detects round completion, but can get it wrong. Use this to set it manually.'; + + @override + String get broadcastRoundName => 'Round name'; + + @override + String get broadcastRoundNumber => 'Round number'; + + @override + String get broadcastTournamentName => 'Tournament name'; + + @override + String get broadcastTournamentDescription => 'Short tournament description'; + + @override + String get broadcastFullDescription => 'Full tournament description'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Optional long description of the tournament. $param1 is available. Length must be less than $param2 characters.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN Source URL'; + + @override + String get broadcastSourceUrlHelp => 'URL that Lichess will check to get PGN updates. It must be publicly accessible from the Internet.'; + + @override + String get broadcastSourceGameIds => 'Up to 64 Lichess game IDs, separated by spaces.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Start date in the tournament local timezone: $param'; + } + + @override + String get broadcastStartDateHelp => 'Optional, if you know when the event starts'; + + @override + String get broadcastCurrentGameUrl => 'Current game URL'; + + @override + String get broadcastDownloadAllRounds => 'Download all rounds'; + + @override + String get broadcastResetRound => 'Reset this round'; + + @override + String get broadcastDeleteRound => 'Delete this round'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Definitively delete the round and all its games.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Delete all games of this round. The source will need to be active in order to re-create them.'; + + @override + String get broadcastEditRoundStudy => 'Edit round study'; + + @override + String get broadcastDeleteTournament => 'Delete this tournament'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Definitively delete the entire tournament, all its rounds and all its games.'; + + @override + String get broadcastShowScores => 'Show players scores based on game results'; + + @override + String get broadcastReplacePlayerTags => 'Optional: replace player names, ratings and titles'; + + @override + String get broadcastFideFederations => 'FIDE federations'; + + @override + String get broadcastTop10Rating => 'Top 10 rating'; + + @override + String get broadcastFidePlayers => 'FIDE players'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE player not found'; + + @override + String get broadcastFideProfile => 'FIDE profile'; + + @override + String get broadcastFederation => 'Federation'; + + @override + String get broadcastAgeThisYear => 'Age this year'; + + @override + String get broadcastUnrated => 'Unrated'; + + @override + String get broadcastRecentTournaments => 'Recent tournaments'; + + @override + String get broadcastOpenLichess => 'Open in Lichess'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Overview'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Official website'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'Score'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count broadcasts', + one: '$count broadcast', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Challenges: $param1'; @@ -1388,10 +1613,10 @@ class AppLocalizationsEn extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'The opponent is limited in the moves they can make, and all moves worsen their position.'; @override - String get puzzleThemeHealthyMix => 'Healthy mix'; + String get puzzleThemeMix => 'Healthy mix'; @override - String get puzzleThemeHealthyMixDescription => 'A bit of everything. You don\'t know what to expect, so you remain ready for anything! Just like in real games.'; + String get puzzleThemeMixDescription => 'A bit of everything. You don\'t know what to expect, so you remain ready for anything! Just like in real games.'; @override String get puzzleThemePlayerGames => 'Player games'; @@ -1795,9 +2020,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get removesTheDepthLimit => 'Removes the depth limit, and keeps your computer warm'; - @override - String get engineManager => 'Engine manager'; - @override String get blunder => 'Blunder'; @@ -2061,6 +2283,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get gamesPlayed => 'Games played'; + @override + String get ok => 'OK'; + @override String get cancel => 'Cancel'; @@ -2770,7 +2995,13 @@ class AppLocalizationsEn extends AppLocalizations { String get other => 'Other'; @override - String get reportDescriptionHelp => 'Paste the link to the game(s) and explain what is wrong about this user\'s behaviour. Don\'t just say \"they cheat\", but tell us how you came to this conclusion. Your report will be processed faster if written in English.'; + String get reportCheatBoostHelp => 'Paste the link to the game(s) and explain what is wrong about this user\'s behaviour. Don\'t just say \"they cheat\", but tell us how you came to this conclusion.'; + + @override + String get reportUsernameHelp => 'Explain what about this username is offensive. Don\'t just say \"it\'s offensive/inappropriate\", but tell us how you came to this conclusion, especially if the insult is obfuscated, not in english, is in slang, or is a historical/cultural reference.'; + + @override + String get reportProcessedFasterInEnglish => 'Your report will be processed faster if written in English.'; @override String get error_provideOneCheatedGameLink => 'Please provide at least one link to a cheated game.'; @@ -4075,6 +4306,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get nothingToSeeHere => 'Nothing to see here at the moment.'; + @override + String get stats => 'Stats'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4722,4726 +4956,5896 @@ class AppLocalizationsEn extends AppLocalizations { String get streamerLichessStreamers => 'Lichess streamers'; @override - String get studyShareAndExport => 'Share & export'; + String get studyPrivate => 'Private'; @override - String get studyStart => 'Start'; -} + String get studyMyStudies => 'My studies'; -/// The translations for English, as used in the United States (`en_US`). -class AppLocalizationsEnUs extends AppLocalizationsEn { - AppLocalizationsEnUs(): super('en_US'); + @override + String get studyStudiesIContributeTo => 'Studies I contribute to'; @override - String get mobileHomeTab => 'Home'; + String get studyMyPublicStudies => 'My public studies'; @override - String get mobilePuzzlesTab => 'Puzzles'; + String get studyMyPrivateStudies => 'My private studies'; @override - String get mobileToolsTab => 'Tools'; + String get studyMyFavoriteStudies => 'My favourite studies'; @override - String get mobileWatchTab => 'Watch'; + String get studyWhatAreStudies => 'What are studies?'; @override - String get mobileSettingsTab => 'Settings'; + String get studyAllStudies => 'All studies'; @override - String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; + String studyStudiesCreatedByX(String param) { + return 'Studies created by $param'; + } @override - String get mobileSystemColors => 'System colors'; + String get studyNoneYet => 'None yet.'; @override - String get mobileFeedbackButton => 'Feedback'; + String get studyHot => 'Hot'; @override - String get mobileOkButton => 'OK'; + String get studyDateAddedNewest => 'Date added (newest)'; @override - String get mobileSettingsHapticFeedback => 'Haptic feedback'; + String get studyDateAddedOldest => 'Date added (oldest)'; @override - String get mobileSettingsImmersiveMode => 'Immersive mode'; + String get studyRecentlyUpdated => 'Recently updated'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get studyMostPopular => 'Most popular'; @override - String get mobileNotFollowingAnyUser => 'You are not following any user.'; + String get studyAlphabetical => 'Alphabetical'; @override - String get mobileAllGames => 'All games'; + String get studyAddNewChapter => 'Add a new chapter'; @override - String get mobileRecentSearches => 'Recent searches'; + String get studyAddMembers => 'Add members'; @override - String get mobileClearButton => 'Clear'; + String get studyInviteToTheStudy => 'Invite to the study'; @override - String get mobileNoSearchResults => 'No results'; + String get studyPleaseOnlyInvitePeopleYouKnow => 'Please only invite people who know you, and who actively want to join this study.'; @override - String get mobileAreYouSure => 'Are you sure?'; + String get studySearchByUsername => 'Search by username'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak, but your score will be saved.'; + String get studySpectator => 'Spectator'; @override - String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; + String get studyContributor => 'Contributor'; @override - String get mobileSharePuzzle => 'Share this puzzle'; + String get studyKick => 'Kick'; @override - String get mobileShareGameURL => 'Share game URL'; + String get studyLeaveTheStudy => 'Leave the study'; @override - String get mobileShareGamePGN => 'Share PGN'; + String get studyYouAreNowAContributor => 'You are now a contributor'; @override - String get mobileSharePositionAsFEN => 'Share position as FEN'; + String get studyYouAreNowASpectator => 'You are now a spectator'; @override - String get mobileShowVariations => 'Show variations'; + String get studyPgnTags => 'PGN tags'; @override - String get mobileHideVariation => 'Hide variation'; + String get studyLike => 'Like'; @override - String get mobileShowComments => 'Show comments'; + String get studyUnlike => 'Unlike'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get studyNewTag => 'New tag'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get studyCommentThisPosition => 'Comment on this position'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; + String get studyCommentThisMove => 'Comment on this move'; @override - String get mobileCancelDrawOffer => 'Cancel draw offer'; + String get studyAnnotateWithGlyphs => 'Annotate with glyphs'; @override - String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; + String get studyTheChapterIsTooShortToBeAnalysed => 'The chapter is too short to be analysed.'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get studyOnlyContributorsCanRequestAnalysis => 'Only the study contributors can request a computer analysis.'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get studyGetAFullComputerAnalysis => 'Get a full server-side computer analysis of the mainline.'; @override - String get mobileCustomGameJoinAGame => 'Join a game'; + String get studyMakeSureTheChapterIsComplete => 'Make sure the chapter is complete. You can only request analysis once.'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; + String get studyAllSyncMembersRemainOnTheSamePosition => 'All SYNC members remain on the same position'; @override - String get mobileSomethingWentWrong => 'Something went wrong.'; + String get studyShareChanges => 'Share changes with spectators and save them on the server'; @override - String get mobileShowResult => 'Show result'; + String get studyPlaying => 'Playing'; @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get studyShowEvalBar => 'Evaluation bars'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get studyFirst => 'First'; @override - String mobileGreeting(String param) { - return 'Hello, $param'; - } + String get studyPrevious => 'Previous'; @override - String get mobileGreetingWithoutName => 'Hello'; + String get studyNext => 'Next'; @override - String get activityActivity => 'Activity'; + String get studyLast => 'Last'; @override - String get activityHostedALiveStream => 'Hosted a live stream'; + String get studyShareAndExport => 'Share & export'; @override - String activityRankedInSwissTournament(String param1, String param2) { - return 'Ranked #$param1 in $param2'; - } + String get studyCloneStudy => 'Clone'; @override - String get activitySignedUp => 'Signed up to lichess.org'; + String get studyStudyPgn => 'Study PGN'; @override - String activitySupportedNbMonths(int count, String param2) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Supported lichess.org for $count months as a $param2', - one: 'Supported lichess.org for $count month as a $param2', - ); - return '$_temp0'; - } + String get studyDownloadAllGames => 'Download all games'; @override - String activityPracticedNbPositions(int count, String param2) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Practiced $count positions on $param2', - one: 'Practiced $count position on $param2', - ); - return '$_temp0'; - } + String get studyChapterPgn => 'Chapter PGN'; @override - String activitySolvedNbPuzzles(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Solved $count tactical puzzles', - one: 'Solved $count tactical puzzle', - ); - return '$_temp0'; - } + String get studyCopyChapterPgn => 'Copy PGN'; @override - String activityPlayedNbGames(int count, String param2) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Played $count $param2 games', - one: 'Played $count $param2 game', - ); - return '$_temp0'; - } - - @override - String activityPostedNbMessages(int count, String param2) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Posted $count messages in $param2', - one: 'Posted $count message in $param2', - ); - return '$_temp0'; - } - - @override - String activityPlayedNbMoves(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Played $count moves', - one: 'Played $count move', - ); - return '$_temp0'; - } + String get studyDownloadGame => 'Download game'; @override - String activityInNbCorrespondenceGames(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'in $count correspondence games', - one: 'in $count correspondence game', - ); - return '$_temp0'; - } + String get studyStudyUrl => 'Study URL'; @override - String activityCompletedNbGames(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Completed $count correspondence games', - one: 'Completed $count correspondence game', - ); - return '$_temp0'; - } + String get studyCurrentChapterUrl => 'Current chapter URL'; @override - String activityFollowedNbPlayers(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Started following $count players', - one: 'Started following $count player', - ); - return '$_temp0'; - } + String get studyYouCanPasteThisInTheForumToEmbed => 'You can paste this in the forum or your Lichess blog to embed'; @override - String activityGainedNbFollowers(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Gained $count new followers', - one: 'Gained $count new follower', - ); - return '$_temp0'; - } + String get studyStartAtInitialPosition => 'Start at initial position'; @override - String activityHostedNbSimuls(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Hosted $count simultaneous exhibitions', - one: 'Hosted $count simultaneous exhibition', - ); - return '$_temp0'; + String studyStartAtX(String param) { + return 'Start at $param'; } @override - String activityJoinedNbSimuls(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Participated in $count simultaneous exhibitions', - one: 'Participated in $count simultaneous exhibition', - ); - return '$_temp0'; - } + String get studyEmbedInYourWebsite => 'Embed in your website'; @override - String activityCreatedNbStudies(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Created $count new studies', - one: 'Created $count new study', - ); - return '$_temp0'; - } + String get studyReadMoreAboutEmbedding => 'Read more about embedding'; @override - String activityCompetedInNbTournaments(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Competed in $count tournaments', - one: 'Competed in $count tournament', - ); - return '$_temp0'; - } + String get studyOnlyPublicStudiesCanBeEmbedded => 'Only public studies can be embedded!'; @override - String activityRankedInTournament(int count, String param2, String param3, String param4) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Ranked #$count (top $param2%) with $param3 games in $param4', - one: 'Ranked #$count (top $param2%) with $param3 game in $param4', - ); - return '$_temp0'; - } + String get studyOpen => 'Open'; @override - String activityCompetedInNbSwissTournaments(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Competed in $count Swiss tournaments', - one: 'Competed in $count Swiss tournament', - ); - return '$_temp0'; + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1, brought to you by $param2'; } @override - String activityJoinedNbTeams(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Joined $count teams', - one: 'Joined $count team', - ); - return '$_temp0'; - } + String get studyStudyNotFound => 'Study not found'; @override - String get broadcastBroadcasts => 'Broadcasts'; + String get studyEditChapter => 'Edit chapter'; @override - String get broadcastLiveBroadcasts => 'Live tournament broadcasts'; + String get studyNewChapter => 'New chapter'; @override - String challengeChallengesX(String param1) { - return 'Challenges: $param1'; + String studyImportFromChapterX(String param) { + return 'Import from $param'; } @override - String get challengeChallengeToPlay => 'Challenge to a game'; - - @override - String get challengeChallengeDeclined => 'Challenge declined'; - - @override - String get challengeChallengeAccepted => 'Challenge accepted!'; + String get studyOrientation => 'Orientation'; @override - String get challengeChallengeCanceled => 'Challenge canceled.'; + String get studyAnalysisMode => 'Analysis mode'; @override - String get challengeRegisterToSendChallenges => 'Please register to send challenges.'; + String get studyPinnedChapterComment => 'Pinned chapter comment'; @override - String challengeYouCannotChallengeX(String param) { - return 'You cannot challenge $param.'; - } + String get studySaveChapter => 'Save chapter'; @override - String challengeXDoesNotAcceptChallenges(String param) { - return '$param does not accept challenges.'; - } + String get studyClearAnnotations => 'Clear annotations'; @override - String challengeYourXRatingIsTooFarFromY(String param1, String param2) { - return 'Your $param1 rating is too far from $param2.'; - } + String get studyClearVariations => 'Clear variations'; @override - String challengeCannotChallengeDueToProvisionalXRating(String param) { - return 'Cannot challenge due to provisional $param rating.'; - } + String get studyDeleteChapter => 'Delete chapter'; @override - String challengeXOnlyAcceptsChallengesFromFriends(String param) { - return '$param only accepts challenges from friends.'; - } + String get studyDeleteThisChapter => 'Delete this chapter. There is no going back!'; @override - String get challengeDeclineGeneric => 'I\'m not accepting challenges at the moment.'; + String get studyClearAllCommentsInThisChapter => 'Clear all comments, glyphs and drawn shapes in this chapter'; @override - String get challengeDeclineLater => 'This is not the right time for me, please ask again later.'; + String get studyRightUnderTheBoard => 'Right under the board'; @override - String get challengeDeclineTooFast => 'This time control is too fast for me, please challenge again with a slower game.'; + String get studyNoPinnedComment => 'None'; @override - String get challengeDeclineTooSlow => 'This time control is too slow for me, please challenge again with a faster game.'; + String get studyNormalAnalysis => 'Normal analysis'; @override - String get challengeDeclineTimeControl => 'I\'m not accepting challenges with this time control.'; + String get studyHideNextMoves => 'Hide next moves'; @override - String get challengeDeclineRated => 'Please send me a rated challenge instead.'; + String get studyInteractiveLesson => 'Interactive lesson'; @override - String get challengeDeclineCasual => 'Please send me a casual challenge instead.'; + String studyChapterX(String param) { + return 'Chapter $param'; + } @override - String get challengeDeclineStandard => 'I\'m not accepting variant challenges right now.'; + String get studyEmpty => 'Empty'; @override - String get challengeDeclineVariant => 'I\'m not willing to play this variant right now.'; + String get studyStartFromInitialPosition => 'Start from initial position'; @override - String get challengeDeclineNoBot => 'I\'m not accepting challenges from bots.'; + String get studyEditor => 'Editor'; @override - String get challengeDeclineOnlyBot => 'I\'m only accepting challenges from bots.'; + String get studyStartFromCustomPosition => 'Start from custom position'; @override - String get challengeInviteLichessUser => 'Or invite a Lichess user:'; + String get studyLoadAGameByUrl => 'Load games by URLs'; @override - String get contactContact => 'Contact'; + String get studyLoadAPositionFromFen => 'Load a position from FEN'; @override - String get contactContactLichess => 'Contact Lichess'; + String get studyLoadAGameFromPgn => 'Load games from PGN'; @override - String get patronDonate => 'Donate'; + String get studyAutomatic => 'Automatic'; @override - String get patronLichessPatron => 'Lichess Patron'; + String get studyUrlOfTheGame => 'URL of the games, one per line'; @override - String perfStatPerfStats(String param) { - return '$param stats'; + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Load games from $param1 or $param2'; } @override - String get perfStatViewTheGames => 'View the games'; + String get studyCreateChapter => 'Create chapter'; @override - String get perfStatProvisional => 'provisional'; + String get studyCreateStudy => 'Create study'; @override - String get perfStatNotEnoughRatedGames => 'Not enough rated games have been played to establish a reliable rating.'; + String get studyEditStudy => 'Edit study'; @override - String perfStatProgressOverLastXGames(String param) { - return 'Progression over the last $param games:'; - } + String get studyVisibility => 'Visibility'; @override - String perfStatRatingDeviation(String param) { - return 'Rating deviation: $param.'; - } + String get studyPublic => 'Public'; @override - String perfStatRatingDeviationTooltip(String param1, String param2, String param3) { - return 'Lower value means the rating is more stable. Above $param1, the rating is considered provisional. To be included in the rankings, this value should be below $param2 (standard chess) or $param3 (variants).'; - } + String get studyUnlisted => 'Unlisted'; @override - String get perfStatTotalGames => 'Total games'; + String get studyInviteOnly => 'Invite only'; @override - String get perfStatRatedGames => 'Rated games'; + String get studyAllowCloning => 'Allow cloning'; @override - String get perfStatTournamentGames => 'Tournament games'; + String get studyNobody => 'Nobody'; @override - String get perfStatBerserkedGames => 'Berserked games'; + String get studyOnlyMe => 'Only me'; @override - String get perfStatTimeSpentPlaying => 'Time spent playing'; + String get studyContributors => 'Contributors'; @override - String get perfStatAverageOpponent => 'Average opponent'; + String get studyMembers => 'Members'; @override - String get perfStatVictories => 'Victories'; + String get studyEveryone => 'Everyone'; @override - String get perfStatDefeats => 'Defeats'; + String get studyEnableSync => 'Enable sync'; @override - String get perfStatDisconnections => 'Disconnections'; + String get studyYesKeepEveryoneOnTheSamePosition => 'Yes: keep everyone on the same position'; @override - String get perfStatNotEnoughGames => 'Not enough games played'; + String get studyNoLetPeopleBrowseFreely => 'No: let people browse freely'; @override - String perfStatHighestRating(String param) { - return 'Highest rating: $param'; - } + String get studyPinnedStudyComment => 'Pinned study comment'; @override - String perfStatLowestRating(String param) { - return 'Lowest rating: $param'; - } + String get studyStart => 'Start'; @override - String perfStatFromXToY(String param1, String param2) { - return 'from $param1 to $param2'; - } + String get studySave => 'Save'; @override - String get perfStatWinningStreak => 'Winning streak'; + String get studyClearChat => 'Clear chat'; @override - String get perfStatLosingStreak => 'Losing streak'; + String get studyDeleteTheStudyChatHistory => 'Delete the study chat history? There is no going back!'; @override - String perfStatLongestStreak(String param) { - return 'Longest streak: $param'; - } + String get studyDeleteStudy => 'Delete study'; @override - String perfStatCurrentStreak(String param) { - return 'Current streak: $param'; + String studyConfirmDeleteStudy(String param) { + return 'Delete the entire study? There is no going back! Type the name of the study to confirm: $param'; } @override - String get perfStatBestRated => 'Best rated victories'; - - @override - String get perfStatGamesInARow => 'Games played in a row'; + String get studyWhereDoYouWantToStudyThat => 'Where do you want to study that?'; @override - String get perfStatLessThanOneHour => 'Less than one hour between games'; + String get studyGoodMove => 'Good move'; @override - String get perfStatMaxTimePlaying => 'Max time spent playing'; + String get studyMistake => 'Mistake'; @override - String get perfStatNow => 'now'; + String get studyBrilliantMove => 'Brilliant move'; @override - String get preferencesPreferences => 'Preferences'; + String get studyBlunder => 'Blunder'; @override - String get preferencesDisplay => 'Display'; + String get studyInterestingMove => 'Interesting move'; @override - String get preferencesPrivacy => 'Privacy'; + String get studyDubiousMove => 'Dubious move'; @override - String get preferencesNotifications => 'Notifications'; + String get studyOnlyMove => 'Only move'; @override - String get preferencesPieceAnimation => 'Piece animation'; + String get studyZugzwang => 'Zugzwang'; @override - String get preferencesMaterialDifference => 'Material difference'; + String get studyEqualPosition => 'Equal position'; @override - String get preferencesBoardHighlights => 'Board highlights (last move and check)'; + String get studyUnclearPosition => 'Unclear position'; @override - String get preferencesPieceDestinations => 'Piece destinations (valid moves and premoves)'; + String get studyWhiteIsSlightlyBetter => 'White is slightly better'; @override - String get preferencesBoardCoordinates => 'Board coordinates (A-H, 1-8)'; + String get studyBlackIsSlightlyBetter => 'Black is slightly better'; @override - String get preferencesMoveListWhilePlaying => 'Move list while playing'; + String get studyWhiteIsBetter => 'White is better'; @override - String get preferencesPgnPieceNotation => 'Move notation'; + String get studyBlackIsBetter => 'Black is better'; @override - String get preferencesChessPieceSymbol => 'Chess piece symbol'; + String get studyWhiteIsWinning => 'White is winning'; @override - String get preferencesPgnLetter => 'Letter (K, Q, R, B, N)'; + String get studyBlackIsWinning => 'Black is winning'; @override - String get preferencesZenMode => 'Zen mode'; + String get studyNovelty => 'Novelty'; @override - String get preferencesShowPlayerRatings => 'Show player ratings'; + String get studyDevelopment => 'Development'; @override - String get preferencesShowFlairs => 'Show player flairs'; + String get studyInitiative => 'Initiative'; @override - String get preferencesExplainShowPlayerRatings => 'This allows hiding all ratings from the website, to help focus on the chess. Games can still be rated, this is only about what you get to see.'; + String get studyAttack => 'Attack'; @override - String get preferencesDisplayBoardResizeHandle => 'Show board resize handle'; + String get studyCounterplay => 'Counterplay'; @override - String get preferencesOnlyOnInitialPosition => 'Only on initial position'; + String get studyTimeTrouble => 'Time trouble'; @override - String get preferencesInGameOnly => 'In-game only'; + String get studyWithCompensation => 'With compensation'; @override - String get preferencesChessClock => 'Chess clock'; + String get studyWithTheIdea => 'With the idea'; @override - String get preferencesTenthsOfSeconds => 'Tenths of seconds'; + String get studyNextChapter => 'Next chapter'; @override - String get preferencesWhenTimeRemainingLessThanTenSeconds => 'When time remaining < 10 seconds'; + String get studyPrevChapter => 'Previous chapter'; @override - String get preferencesHorizontalGreenProgressBars => 'Horizontal green progress bars'; + String get studyStudyActions => 'Study actions'; @override - String get preferencesSoundWhenTimeGetsCritical => 'Sound when time gets critical'; + String get studyTopics => 'Topics'; @override - String get preferencesGiveMoreTime => 'Give more time'; + String get studyMyTopics => 'My topics'; @override - String get preferencesGameBehavior => 'Game behavior'; + String get studyPopularTopics => 'Popular topics'; @override - String get preferencesHowDoYouMovePieces => 'How do you move pieces?'; + String get studyManageTopics => 'Manage topics'; @override - String get preferencesClickTwoSquares => 'Click two squares'; + String get studyBack => 'Back'; @override - String get preferencesDragPiece => 'Drag a piece'; + String get studyPlayAgain => 'Play again'; @override - String get preferencesBothClicksAndDrag => 'Either'; + String get studyWhatWouldYouPlay => 'What would you play in this position?'; @override - String get preferencesPremovesPlayingDuringOpponentTurn => 'Premoves (playing during opponent turn)'; + String get studyYouCompletedThisLesson => 'Congratulations! You completed this lesson.'; @override - String get preferencesTakebacksWithOpponentApproval => 'Takebacks (with opponent approval)'; + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Chapters', + one: '$count Chapter', + ); + return '$_temp0'; + } @override - String get preferencesInCasualGamesOnly => 'In casual games only'; + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Games', + one: '$count Game', + ); + return '$_temp0'; + } @override - String get preferencesPromoteToQueenAutomatically => 'Promote to Queen automatically'; - - @override - String get preferencesExplainPromoteToQueenAutomatically => 'Hold the key while promoting to temporarily disable auto-promotion'; + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Members', + one: '$count Member', + ); + return '$_temp0'; + } @override - String get preferencesWhenPremoving => 'When premoving'; + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Paste your PGN text here, up to $count games', + one: 'Paste your PGN text here, up to $count game', + ); + return '$_temp0'; + } +} - @override - String get preferencesClaimDrawOnThreefoldRepetitionAutomatically => 'Claim draw on threefold repetition automatically'; +/// The translations for English, as used in the United States (`en_US`). +class AppLocalizationsEnUs extends AppLocalizationsEn { + AppLocalizationsEnUs(): super('en_US'); @override - String get preferencesWhenTimeRemainingLessThanThirtySeconds => 'When time remaining < 30 seconds'; + String get mobileHomeTab => 'Home'; @override - String get preferencesMoveConfirmation => 'Move confirmation'; + String get mobilePuzzlesTab => 'Puzzles'; @override - String get preferencesExplainCanThenBeTemporarilyDisabled => 'Can be disabled during a game with the board menu'; + String get mobileToolsTab => 'Tools'; @override - String get preferencesInCorrespondenceGames => 'Correspondence games'; + String get mobileWatchTab => 'Watch'; @override - String get preferencesCorrespondenceAndUnlimited => 'Correspondence and unlimited'; + String get mobileSettingsTab => 'Settings'; @override - String get preferencesConfirmResignationAndDrawOffers => 'Confirm resignation and draw offers'; + String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; @override - String get preferencesCastleByMovingTheKingTwoSquaresOrOntoTheRook => 'Castling method'; + String get mobileSystemColors => 'System colors'; @override - String get preferencesCastleByMovingTwoSquares => 'Move king two squares'; + String get mobileFeedbackButton => 'Feedback'; @override - String get preferencesCastleByMovingOntoTheRook => 'Move king onto rook'; + String get mobileOkButton => 'OK'; @override - String get preferencesInputMovesWithTheKeyboard => 'Input moves with the keyboard'; + String get mobileSettingsHapticFeedback => 'Haptic feedback'; @override - String get preferencesInputMovesWithVoice => 'Input moves with your voice'; + String get mobileSettingsImmersiveMode => 'Immersive mode'; @override - String get preferencesSnapArrowsToValidMoves => 'Snap arrows to valid moves'; + String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; @override - String get preferencesSayGgWpAfterLosingOrDrawing => 'Say \"Good game, well played\" upon defeat or draw'; + String get mobileNotFollowingAnyUser => 'You are not following any user.'; @override - String get preferencesYourPreferencesHaveBeenSaved => 'Your preferences have been saved.'; + String get mobileAllGames => 'All games'; @override - String get preferencesScrollOnTheBoardToReplayMoves => 'Scroll on the board to replay moves'; + String get mobileRecentSearches => 'Recent searches'; @override - String get preferencesCorrespondenceEmailNotification => 'Daily mail notification listing your correspondence games'; + String get mobileClearButton => 'Clear'; @override - String get preferencesNotifyStreamStart => 'Streamer goes live'; + String mobilePlayersMatchingSearchTerm(String param) { + return 'Players with \"$param\"'; + } @override - String get preferencesNotifyInboxMsg => 'New inbox message'; + String get mobileNoSearchResults => 'No results'; @override - String get preferencesNotifyForumMention => 'Forum comment mentions you'; + String get mobileAreYouSure => 'Are you sure?'; @override - String get preferencesNotifyInvitedStudy => 'Study invite'; + String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak, but your score will be saved.'; @override - String get preferencesNotifyGameEvent => 'Correspondence game updates'; + String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; @override - String get preferencesNotifyChallenge => 'Challenges'; + String get mobileSharePuzzle => 'Share this puzzle'; @override - String get preferencesNotifyTournamentSoon => 'Tournament starting soon'; + String get mobileShareGameURL => 'Share game URL'; @override - String get preferencesNotifyTimeAlarm => 'Correspondence time running out'; + String get mobileShareGamePGN => 'Share PGN'; @override - String get preferencesNotifyBell => 'Bell notification within Lichess'; + String get mobileSharePositionAsFEN => 'Share position as FEN'; @override - String get preferencesNotifyPush => 'Device notification when you\'re not on Lichess'; + String get mobileShowVariations => 'Show variations'; @override - String get preferencesNotifyWeb => 'Browser'; + String get mobileHideVariation => 'Hide variation'; @override - String get preferencesNotifyDevice => 'Device'; + String get mobileShowComments => 'Show comments'; @override - String get preferencesBellNotificationSound => 'Bell notification sound'; + String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; @override - String get puzzlePuzzles => 'Chess Puzzles'; + String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; @override - String get puzzlePuzzleThemes => 'Puzzle Themes'; + String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @override - String get puzzleRecommended => 'Recommended'; + String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @override - String get puzzlePhases => 'Phases'; + String get mobileBlindfoldMode => 'Blindfold'; @override - String get puzzleMotifs => 'Motifs'; + String get mobileLiveStreamers => 'Live streamers'; @override - String get puzzleAdvanced => 'Advanced'; + String get mobileCustomGameJoinAGame => 'Join a game'; @override - String get puzzleLengths => 'Lengths'; + String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; @override - String get puzzleMates => 'Mates'; + String get mobileSomethingWentWrong => 'Something went wrong.'; @override - String get puzzleGoals => 'Goals'; + String get mobileShowResult => 'Show result'; @override - String get puzzleOrigin => 'Origin'; + String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; @override - String get puzzleSpecialMoves => 'Special moves'; + String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; @override - String get puzzleDidYouLikeThisPuzzle => 'Did you like this puzzle?'; + String mobileGreeting(String param) { + return 'Hello, $param'; + } @override - String get puzzleVoteToLoadNextOne => 'Vote to load the next one!'; + String get mobileGreetingWithoutName => 'Hello'; @override - String get puzzleUpVote => 'Upvote puzzle'; + String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; @override - String get puzzleDownVote => 'Downvote puzzle'; + String get activityActivity => 'Activity'; @override - String get puzzleYourPuzzleRatingWillNotChange => 'Your puzzle rating will not change. Note that puzzles are not a competition. Ratings help select the best puzzles for your current skill.'; + String get activityHostedALiveStream => 'Hosted a live stream'; @override - String get puzzleFindTheBestMoveForWhite => 'Find the best move for white.'; + String activityRankedInSwissTournament(String param1, String param2) { + return 'Ranked #$param1 in $param2'; + } @override - String get puzzleFindTheBestMoveForBlack => 'Find the best move for black.'; + String get activitySignedUp => 'Signed up to lichess.org'; @override - String get puzzleToGetPersonalizedPuzzles => 'To get personalized puzzles:'; + String activitySupportedNbMonths(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Supported lichess.org for $count months as a $param2', + one: 'Supported lichess.org for $count month as a $param2', + ); + return '$_temp0'; + } @override - String puzzlePuzzleId(String param) { - return 'Puzzle $param'; + String activityPracticedNbPositions(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Practiced $count positions on $param2', + one: 'Practiced $count position on $param2', + ); + return '$_temp0'; } @override - String get puzzlePuzzleOfTheDay => 'Puzzle of the day'; + String activitySolvedNbPuzzles(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Solved $count tactical puzzles', + one: 'Solved $count tactical puzzle', + ); + return '$_temp0'; + } @override - String get puzzleDailyPuzzle => 'Daily Puzzle'; + String activityPlayedNbGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Played $count $param2 games', + one: 'Played $count $param2 game', + ); + return '$_temp0'; + } @override - String get puzzleClickToSolve => 'Click to solve'; + String activityPostedNbMessages(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Posted $count messages in $param2', + one: 'Posted $count message in $param2', + ); + return '$_temp0'; + } @override - String get puzzleGoodMove => 'Good move'; + String activityPlayedNbMoves(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Played $count moves', + one: 'Played $count move', + ); + return '$_temp0'; + } @override - String get puzzleBestMove => 'Best move!'; + String activityInNbCorrespondenceGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count correspondence games', + one: 'in $count correspondence game', + ); + return '$_temp0'; + } @override - String get puzzleKeepGoing => 'Keep going…'; + String activityCompletedNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Completed $count correspondence games', + one: 'Completed $count correspondence game', + ); + return '$_temp0'; + } @override - String get puzzlePuzzleSuccess => 'Success!'; + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Completed $count $param2 correspondence games', + one: 'Completed $count $param2 correspondence game', + ); + return '$_temp0'; + } @override - String get puzzlePuzzleComplete => 'Puzzle complete!'; + String activityFollowedNbPlayers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Started following $count players', + one: 'Started following $count player', + ); + return '$_temp0'; + } @override - String get puzzleByOpenings => 'By openings'; + String activityGainedNbFollowers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Gained $count new followers', + one: 'Gained $count new follower', + ); + return '$_temp0'; + } @override - String get puzzlePuzzlesByOpenings => 'Puzzles by openings'; + String activityHostedNbSimuls(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Hosted $count simultaneous exhibitions', + one: 'Hosted $count simultaneous exhibition', + ); + return '$_temp0'; + } @override - String get puzzleOpeningsYouPlayedTheMost => 'Openings you played the most in rated games'; + String activityJoinedNbSimuls(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Participated in $count simultaneous exhibitions', + one: 'Participated in $count simultaneous exhibition', + ); + return '$_temp0'; + } @override - String get puzzleUseFindInPage => 'Use \"Find in page\" in the browser menu to find your favorite opening!'; + String activityCreatedNbStudies(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Created $count new studies', + one: 'Created $count new study', + ); + return '$_temp0'; + } @override - String get puzzleUseCtrlF => 'Use Ctrl+f to find your favorite opening!'; + String activityCompetedInNbTournaments(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Competed in $count Arena tournaments', + one: 'Competed in $count Arena tournament', + ); + return '$_temp0'; + } @override - String get puzzleNotTheMove => 'That\'s not the move!'; + String activityRankedInTournament(int count, String param2, String param3, String param4) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Ranked #$count (top $param2%) with $param3 games in $param4', + one: 'Ranked #$count (top $param2%) with $param3 game in $param4', + ); + return '$_temp0'; + } @override - String get puzzleTrySomethingElse => 'Try something else.'; + String activityCompetedInNbSwissTournaments(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Competed in $count Swiss tournaments', + one: 'Competed in $count Swiss tournament', + ); + return '$_temp0'; + } @override - String puzzleRatingX(String param) { - return 'Rating: $param'; + String activityJoinedNbTeams(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Joined $count teams', + one: 'Joined $count team', + ); + return '$_temp0'; } @override - String get puzzleHidden => 'hidden'; + String get broadcastBroadcasts => 'Broadcasts'; @override - String puzzleFromGameLink(String param) { - return 'From game $param'; - } + String get broadcastMyBroadcasts => 'My broadcasts'; @override - String get puzzleContinueTraining => 'Continue training'; + String get broadcastLiveBroadcasts => 'Live tournament broadcasts'; @override - String get puzzleDifficultyLevel => 'Difficulty level'; + String get broadcastBroadcastCalendar => 'Broadcast calendar'; @override - String get puzzleNormal => 'Normal'; + String get broadcastNewBroadcast => 'New live broadcast'; @override - String get puzzleEasier => 'Easier'; + String get broadcastSubscribedBroadcasts => 'Subscribed broadcasts'; @override - String get puzzleEasiest => 'Easiest'; + String get broadcastAboutBroadcasts => 'About broadcasts'; @override - String get puzzleHarder => 'Harder'; + String get broadcastHowToUseLichessBroadcasts => 'How to use Lichess Broadcasts.'; @override - String get puzzleHardest => 'Hardest'; + String get broadcastTheNewRoundHelp => 'The new round will have the same members and contributors as the previous one.'; @override - String get puzzleExample => 'Example'; + String get broadcastAddRound => 'Add a round'; @override - String get puzzleAddAnotherTheme => 'Add another theme'; + String get broadcastOngoing => 'Ongoing'; @override - String get puzzleNextPuzzle => 'Next puzzle'; + String get broadcastUpcoming => 'Upcoming'; @override - String get puzzleJumpToNextPuzzleImmediately => 'Jump to next puzzle immediately'; + String get broadcastCompleted => 'Completed'; @override - String get puzzlePuzzleDashboard => 'Puzzle Dashboard'; + String get broadcastCompletedHelp => 'Lichess detects round completion, but can get it wrong. Use this to set it manually.'; @override - String get puzzleImprovementAreas => 'Improvement areas'; + String get broadcastRoundName => 'Round name'; @override - String get puzzleStrengths => 'Strengths'; + String get broadcastRoundNumber => 'Round number'; @override - String get puzzleHistory => 'Puzzle history'; + String get broadcastTournamentName => 'Tournament name'; @override - String get puzzleSolved => 'solved'; + String get broadcastTournamentDescription => 'Short tournament description'; @override - String get puzzleFailed => 'failed'; + String get broadcastFullDescription => 'Full tournament description'; @override - String get puzzleStreakDescription => 'Solve progressively harder puzzles and build a win streak. There is no clock, so take your time. One wrong move, and it\'s game over! But you can skip one move per session.'; + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Optional long description of the tournament. $param1 is available. Length must be less than $param2 characters.'; + } @override - String puzzleYourStreakX(String param) { - return 'Your streak: $param'; - } + String get broadcastSourceSingleUrl => 'PGN Source URL'; @override - String get puzzleStreakSkipExplanation => 'Skip this move to preserve your streak! Only works once per run.'; + String get broadcastSourceUrlHelp => 'URL that Lichess will check to get PGN updates. It must be publicly accessible from the Internet.'; @override - String get puzzleContinueTheStreak => 'Continue the streak'; + String get broadcastSourceGameIds => 'Up to 64 Lichess game IDs, separated by spaces.'; @override - String get puzzleNewStreak => 'New streak'; + String broadcastStartDateTimeZone(String param) { + return 'Start date in the tournament local timezone: $param'; + } @override - String get puzzleFromMyGames => 'From my games'; + String get broadcastStartDateHelp => 'Optional, if you know when the event starts'; @override - String get puzzleLookupOfPlayer => 'Search puzzles from a player\'s games'; + String get broadcastCurrentGameUrl => 'Current game URL'; @override - String puzzleFromXGames(String param) { - return 'Puzzles from $param\'s games'; - } + String get broadcastDownloadAllRounds => 'Download all rounds'; @override - String get puzzleSearchPuzzles => 'Search puzzles'; + String get broadcastResetRound => 'Reset this round'; @override - String get puzzleFromMyGamesNone => 'You have no puzzles in the database, but Lichess still loves you very much.\nPlay rapid and classical games to increase your chances of having a puzzle of yours added!'; + String get broadcastDeleteRound => 'Delete this round'; @override - String puzzleFromXGamesFound(String param1, String param2) { - return '$param1 puzzles found in $param2 games'; - } + String get broadcastDefinitivelyDeleteRound => 'Definitively delete the round and all its games.'; @override - String get puzzlePuzzleDashboardDescription => 'Train, analyse, improve'; + String get broadcastDeleteAllGamesOfThisRound => 'Delete all games of this round. The source will need to be active in order to re-create them.'; @override - String puzzlePercentSolved(String param) { - return '$param solved'; - } + String get broadcastEditRoundStudy => 'Edit round study'; @override - String get puzzleNoPuzzlesToShow => 'Nothing to show, go play some puzzles first!'; + String get broadcastDeleteTournament => 'Delete this tournament'; @override - String get puzzleImprovementAreasDescription => 'Train these to optimize your progress!'; + String get broadcastDefinitivelyDeleteTournament => 'Definitively delete the entire tournament, all its rounds and all its games.'; @override - String get puzzleStrengthDescription => 'You perform the best in these themes'; + String get broadcastShowScores => 'Show players\' scores based on game results'; @override - String puzzlePlayedXTimes(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Played $count times', - one: 'Played $count time', - ); - return '$_temp0'; - } + String get broadcastReplacePlayerTags => 'Optional: replace player names, ratings and titles'; @override - String puzzleNbPointsBelowYourPuzzleRating(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count points below your puzzle rating', - one: 'One point below your puzzle rating', - ); - return '$_temp0'; - } + String get broadcastFideFederations => 'FIDE federations'; @override - String puzzleNbPointsAboveYourPuzzleRating(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count points above your puzzle rating', - one: 'One point above your puzzle rating', - ); - return '$_temp0'; - } + String get broadcastTop10Rating => 'Top 10 rating'; @override - String puzzleNbPlayed(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count played', - one: '$count played', - ); - return '$_temp0'; - } + String get broadcastFidePlayers => 'FIDE players'; @override - String puzzleNbToReplay(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count to replay', - one: '$count to replay', - ); - return '$_temp0'; - } + String get broadcastFidePlayerNotFound => 'FIDE player not found'; @override - String get puzzleThemeAdvancedPawn => 'Advanced pawn'; + String get broadcastFideProfile => 'FIDE profile'; @override - String get puzzleThemeAdvancedPawnDescription => 'One of your pawns is deep into the opponent position, maybe threatening to promote.'; + String get broadcastFederation => 'Federation'; @override - String get puzzleThemeAdvantage => 'Advantage'; + String get broadcastAgeThisYear => 'Age this year'; @override - String get puzzleThemeAdvantageDescription => 'Seize your chance to get a decisive advantage. (200cp ≤ eval ≤ 600cp)'; + String get broadcastUnrated => 'Unrated'; @override - String get puzzleThemeAnastasiaMate => 'Anastasia\'s mate'; + String get broadcastRecentTournaments => 'Recent tournaments'; @override - String get puzzleThemeAnastasiaMateDescription => 'A knight and rook or queen team up to trap the opposing king between the side of the board and a friendly piece.'; + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count broadcasts', + one: '$count broadcast', + ); + return '$_temp0'; + } @override - String get puzzleThemeArabianMate => 'Arabian mate'; + String challengeChallengesX(String param1) { + return 'Challenges: $param1'; + } @override - String get puzzleThemeArabianMateDescription => 'A knight and a rook team up to trap the opposing king on a corner of the board.'; + String get challengeChallengeToPlay => 'Challenge to a game'; @override - String get puzzleThemeAttackingF2F7 => 'Attacking f2 or f7'; + String get challengeChallengeDeclined => 'Challenge declined.'; @override - String get puzzleThemeAttackingF2F7Description => 'An attack focusing on the f2 or f7 pawn, such as in the fried liver opening.'; + String get challengeChallengeAccepted => 'Challenge accepted!'; @override - String get puzzleThemeAttraction => 'Attraction'; + String get challengeChallengeCanceled => 'Challenge canceled.'; @override - String get puzzleThemeAttractionDescription => 'An exchange or sacrifice encouraging or forcing an opponent piece to a square that allows a follow-up tactic.'; + String get challengeRegisterToSendChallenges => 'Please register to send challenges to this user.'; @override - String get puzzleThemeBackRankMate => 'Back rank mate'; + String challengeYouCannotChallengeX(String param) { + return 'You cannot challenge $param.'; + } @override - String get puzzleThemeBackRankMateDescription => 'Checkmate the king on the home rank, when it is trapped there by its own pieces.'; + String challengeXDoesNotAcceptChallenges(String param) { + return '$param does not accept challenges.'; + } @override - String get puzzleThemeBishopEndgame => 'Bishop endgame'; + String challengeYourXRatingIsTooFarFromY(String param1, String param2) { + return 'Your $param1 rating is too far from $param2.'; + } @override - String get puzzleThemeBishopEndgameDescription => 'An endgame with only bishops and pawns.'; + String challengeCannotChallengeDueToProvisionalXRating(String param) { + return 'Cannot challenge due to provisional $param rating.'; + } @override - String get puzzleThemeBodenMate => 'Boden\'s mate'; + String challengeXOnlyAcceptsChallengesFromFriends(String param) { + return '$param only accepts challenges from friends.'; + } @override - String get puzzleThemeBodenMateDescription => 'Two attacking bishops on criss-crossing diagonals deliver mate to a king obstructed by friendly pieces.'; + String get challengeDeclineGeneric => 'I\'m not accepting challenges at the moment.'; @override - String get puzzleThemeCastling => 'Castling'; + String get challengeDeclineLater => 'This is not the right time for me, please ask again later.'; @override - String get puzzleThemeCastlingDescription => 'Bring the king to safety, and deploy the rook for attack.'; + String get challengeDeclineTooFast => 'This time control is too fast for me, please challenge again with a slower game.'; @override - String get puzzleThemeCapturingDefender => 'Capture the defender'; + String get challengeDeclineTooSlow => 'This time control is too slow for me, please challenge again with a faster game.'; @override - String get puzzleThemeCapturingDefenderDescription => 'Removing a piece that is critical to defense of another piece, allowing the now undefended piece to be captured on a following move.'; + String get challengeDeclineTimeControl => 'I\'m not accepting challenges with this time control.'; @override - String get puzzleThemeCrushing => 'Crushing'; + String get challengeDeclineRated => 'Please send me a rated challenge instead.'; @override - String get puzzleThemeCrushingDescription => 'Spot the opponent blunder to obtain a crushing advantage. (eval ≥ 600cp)'; + String get challengeDeclineCasual => 'Please send me a casual challenge instead.'; @override - String get puzzleThemeDoubleBishopMate => 'Double bishop mate'; + String get challengeDeclineStandard => 'I\'m not accepting variant challenges right now.'; @override - String get puzzleThemeDoubleBishopMateDescription => 'Two attacking bishops on adjacent diagonals deliver mate to a king obstructed by friendly pieces.'; + String get challengeDeclineVariant => 'I\'m not willing to play this variant right now.'; @override - String get puzzleThemeDovetailMate => 'Dovetail mate'; + String get challengeDeclineNoBot => 'I\'m not accepting challenges from bots.'; @override - String get puzzleThemeDovetailMateDescription => 'A queen delivers mate to an adjacent king, whose only two escape squares are obstructed by friendly pieces.'; + String get challengeDeclineOnlyBot => 'I\'m only accepting challenges from bots.'; @override - String get puzzleThemeEquality => 'Equality'; + String get challengeInviteLichessUser => 'Or invite a Lichess user:'; @override - String get puzzleThemeEqualityDescription => 'Come back from a losing position, and secure a draw or a balanced position. (eval ≤ 200cp)'; + String get contactContact => 'Contact'; @override - String get puzzleThemeKingsideAttack => 'Kingside attack'; + String get contactContactLichess => 'Contact Lichess'; @override - String get puzzleThemeKingsideAttackDescription => 'An attack of the opponent\'s king, after they castled on the king side.'; + String get patronDonate => 'Donate'; @override - String get puzzleThemeClearance => 'Clearance'; + String get patronLichessPatron => 'Lichess Patron'; @override - String get puzzleThemeClearanceDescription => 'A move, often with tempo, that clears a square, file or diagonal for a follow-up tactical idea.'; + String perfStatPerfStats(String param) { + return '$param stats'; + } @override - String get puzzleThemeDefensiveMove => 'Defensive move'; + String get perfStatViewTheGames => 'View the games'; @override - String get puzzleThemeDefensiveMoveDescription => 'A precise move or sequence of moves that is needed to avoid losing material or another advantage.'; + String get perfStatProvisional => 'provisional'; @override - String get puzzleThemeDeflection => 'Deflection'; + String get perfStatNotEnoughRatedGames => 'Not enough rated games have been played to establish a reliable rating.'; @override - String get puzzleThemeDeflectionDescription => 'A move that distracts an opponent piece from another duty that it performs, such as guarding a key square. Sometimes also called \"overloading\".'; + String perfStatProgressOverLastXGames(String param) { + return 'Progression over the last $param games:'; + } @override - String get puzzleThemeDiscoveredAttack => 'Discovered attack'; + String perfStatRatingDeviation(String param) { + return 'Rating deviation: $param.'; + } @override - String get puzzleThemeDiscoveredAttackDescription => 'Moving a piece that previously blocked an attack by another long range piece, such as a knight out of the way of a rook.'; + String perfStatRatingDeviationTooltip(String param1, String param2, String param3) { + return 'Lower value means the rating is more stable. Above $param1, the rating is considered provisional. To be included in the rankings, this value should be below $param2 (standard chess) or $param3 (variants).'; + } @override - String get puzzleThemeDoubleCheck => 'Double check'; + String get perfStatTotalGames => 'Total games'; @override - String get puzzleThemeDoubleCheckDescription => 'Checking with two pieces at once, as a result of a discovered attack where both the moving piece and the unveiled piece attack the opponent\'s king.'; + String get perfStatRatedGames => 'Rated games'; @override - String get puzzleThemeEndgame => 'Endgame'; + String get perfStatTournamentGames => 'Tournament games'; @override - String get puzzleThemeEndgameDescription => 'A tactic during the last phase of the game.'; + String get perfStatBerserkedGames => 'Berserked games'; @override - String get puzzleThemeEnPassantDescription => 'A tactic involving the en passant rule, where a pawn can capture an opponent pawn that has bypassed it using its initial two-square move.'; + String get perfStatTimeSpentPlaying => 'Time spent playing'; @override - String get puzzleThemeExposedKing => 'Exposed king'; + String get perfStatAverageOpponent => 'Average opponent'; @override - String get puzzleThemeExposedKingDescription => 'A tactic involving a king with few defenders around it, often leading to checkmate.'; + String get perfStatVictories => 'Victories'; @override - String get puzzleThemeFork => 'Fork'; + String get perfStatDefeats => 'Defeats'; @override - String get puzzleThemeForkDescription => 'A move where the moved piece attacks two opponent pieces at once.'; + String get perfStatDisconnections => 'Disconnections'; @override - String get puzzleThemeHangingPiece => 'Hanging piece'; + String get perfStatNotEnoughGames => 'Not enough games played'; @override - String get puzzleThemeHangingPieceDescription => 'A tactic involving an opponent piece being undefended or insufficiently defended and free to capture.'; + String perfStatHighestRating(String param) { + return 'Highest rating: $param'; + } @override - String get puzzleThemeHookMate => 'Hook mate'; + String perfStatLowestRating(String param) { + return 'Lowest rating: $param'; + } @override - String get puzzleThemeHookMateDescription => 'Checkmate with a rook, knight, and pawn along with one enemy pawn to limit the enemy king\'s escape.'; + String perfStatFromXToY(String param1, String param2) { + return 'from $param1 to $param2'; + } @override - String get puzzleThemeInterference => 'Interference'; + String get perfStatWinningStreak => 'Winning streak'; @override - String get puzzleThemeInterferenceDescription => 'Moving a piece between two opponent pieces to leave one or both opponent pieces undefended, such as a knight on a defended square between two rooks.'; + String get perfStatLosingStreak => 'Losing streak'; @override - String get puzzleThemeIntermezzo => 'Intermezzo'; + String perfStatLongestStreak(String param) { + return 'Longest streak: $param'; + } @override - String get puzzleThemeIntermezzoDescription => 'Instead of playing the expected move, first interpose another move posing an immediate threat that the opponent must answer. Also known as \"Zwischenzug\" or \"In between\".'; + String perfStatCurrentStreak(String param) { + return 'Current streak: $param'; + } @override - String get puzzleThemeKnightEndgame => 'Knight endgame'; + String get perfStatBestRated => 'Best rated victories'; @override - String get puzzleThemeKnightEndgameDescription => 'An endgame with only knights and pawns.'; + String get perfStatGamesInARow => 'Games played in a row'; @override - String get puzzleThemeLong => 'Long puzzle'; + String get perfStatLessThanOneHour => 'Less than one hour between games'; @override - String get puzzleThemeLongDescription => 'Three moves to win.'; + String get perfStatMaxTimePlaying => 'Max time spent playing'; @override - String get puzzleThemeMaster => 'Master games'; + String get perfStatNow => 'now'; @override - String get puzzleThemeMasterDescription => 'Puzzles from games played by titled players.'; + String get preferencesPreferences => 'Preferences'; @override - String get puzzleThemeMasterVsMaster => 'Master vs Master games'; + String get preferencesDisplay => 'Display'; @override - String get puzzleThemeMasterVsMasterDescription => 'Puzzles from games between two titled players.'; + String get preferencesPrivacy => 'Privacy'; @override - String get puzzleThemeMate => 'Checkmate'; + String get preferencesNotifications => 'Notifications'; @override - String get puzzleThemeMateDescription => 'Win the game with style.'; + String get preferencesPieceAnimation => 'Piece animation'; @override - String get puzzleThemeMateIn1 => 'Mate in 1'; + String get preferencesMaterialDifference => 'Material difference'; @override - String get puzzleThemeMateIn1Description => 'Deliver checkmate in one move.'; + String get preferencesBoardHighlights => 'Board highlights (last move and check)'; @override - String get puzzleThemeMateIn2 => 'Mate in 2'; + String get preferencesPieceDestinations => 'Piece destinations (valid moves and premoves)'; @override - String get puzzleThemeMateIn2Description => 'Deliver checkmate in two moves.'; + String get preferencesBoardCoordinates => 'Board coordinates (A-H, 1-8)'; @override - String get puzzleThemeMateIn3 => 'Mate in 3'; + String get preferencesMoveListWhilePlaying => 'Move list while playing'; @override - String get puzzleThemeMateIn3Description => 'Deliver checkmate in three moves.'; + String get preferencesPgnPieceNotation => 'Move notation'; @override - String get puzzleThemeMateIn4 => 'Mate in 4'; + String get preferencesChessPieceSymbol => 'Chess piece symbol'; @override - String get puzzleThemeMateIn4Description => 'Deliver checkmate in four moves.'; + String get preferencesPgnLetter => 'Letter (K, Q, R, B, N)'; @override - String get puzzleThemeMateIn5 => 'Mate in 5 or more'; + String get preferencesZenMode => 'Zen mode'; @override - String get puzzleThemeMateIn5Description => 'Figure out a long mating sequence.'; + String get preferencesShowPlayerRatings => 'Show player ratings'; @override - String get puzzleThemeMiddlegame => 'Middlegame'; + String get preferencesShowFlairs => 'Show player flairs'; @override - String get puzzleThemeMiddlegameDescription => 'A tactic during the second phase of the game.'; + String get preferencesDisplayBoardResizeHandle => 'Show board resize handle'; @override - String get puzzleThemeOneMove => 'One-move puzzle'; + String get preferencesOnlyOnInitialPosition => 'Only on initial position'; @override - String get puzzleThemeOneMoveDescription => 'A puzzle that is only one move long.'; + String get preferencesInGameOnly => 'In-game only'; @override - String get puzzleThemeOpening => 'Opening'; + String get preferencesChessClock => 'Chess clock'; @override - String get puzzleThemeOpeningDescription => 'A tactic during the first phase of the game.'; + String get preferencesTenthsOfSeconds => 'Tenths of seconds'; @override - String get puzzleThemePawnEndgame => 'Pawn endgame'; + String get preferencesWhenTimeRemainingLessThanTenSeconds => 'When time remaining < 10 seconds'; @override - String get puzzleThemePawnEndgameDescription => 'An endgame with only pawns.'; + String get preferencesHorizontalGreenProgressBars => 'Horizontal green progress bars'; @override - String get puzzleThemePin => 'Pin'; + String get preferencesSoundWhenTimeGetsCritical => 'Sound when time gets critical'; @override - String get puzzleThemePinDescription => 'A tactic involving pins, where a piece is unable to move without revealing an attack on a higher value piece.'; + String get preferencesGiveMoreTime => 'Give more time'; @override - String get puzzleThemePromotion => 'Promotion'; + String get preferencesGameBehavior => 'Game behavior'; @override - String get puzzleThemePromotionDescription => 'Promote one of your pawns to a queen or minor piece.'; + String get preferencesHowDoYouMovePieces => 'How do you move pieces?'; @override - String get puzzleThemeQueenEndgame => 'Queen endgame'; + String get preferencesClickTwoSquares => 'Click two squares'; @override - String get puzzleThemeQueenEndgameDescription => 'An endgame with only queens and pawns.'; + String get preferencesDragPiece => 'Drag a piece'; @override - String get puzzleThemeQueenRookEndgame => 'Queen and Rook'; + String get preferencesBothClicksAndDrag => 'Either'; @override - String get puzzleThemeQueenRookEndgameDescription => 'An endgame with only queens, rooks, and pawns.'; + String get preferencesPremovesPlayingDuringOpponentTurn => 'Premoves (playing during opponent turn)'; @override - String get puzzleThemeQueensideAttack => 'Queenside attack'; + String get preferencesTakebacksWithOpponentApproval => 'Takebacks (with opponent approval)'; @override - String get puzzleThemeQueensideAttackDescription => 'An attack of the opponent\'s king, after they castled on the queen side.'; + String get preferencesInCasualGamesOnly => 'In casual games only'; @override - String get puzzleThemeQuietMove => 'Quiet move'; + String get preferencesPromoteToQueenAutomatically => 'Promote to Queen automatically'; @override - String get puzzleThemeQuietMoveDescription => 'A move that does not make a check or capture, but does prepare an unavoidable threat for a later move.'; + String get preferencesExplainPromoteToQueenAutomatically => 'Hold the key while promoting to temporarily disable auto-promotion'; @override - String get puzzleThemeRookEndgame => 'Rook endgame'; + String get preferencesWhenPremoving => 'When premoving'; @override - String get puzzleThemeRookEndgameDescription => 'An endgame with only rooks and pawns.'; + String get preferencesClaimDrawOnThreefoldRepetitionAutomatically => 'Claim draw on threefold repetition automatically'; @override - String get puzzleThemeSacrifice => 'Sacrifice'; + String get preferencesWhenTimeRemainingLessThanThirtySeconds => 'When time remaining < 30 seconds'; @override - String get puzzleThemeSacrificeDescription => 'A tactic involving giving up material in the short-term, to gain an advantage again after a forced sequence of moves.'; + String get preferencesMoveConfirmation => 'Move confirmation'; @override - String get puzzleThemeShort => 'Short puzzle'; + String get preferencesExplainCanThenBeTemporarilyDisabled => 'Can be disabled during a game with the board menu'; @override - String get puzzleThemeShortDescription => 'Two moves to win.'; + String get preferencesInCorrespondenceGames => 'Correspondence games'; @override - String get puzzleThemeSkewer => 'Skewer'; + String get preferencesCorrespondenceAndUnlimited => 'Correspondence and unlimited'; @override - String get puzzleThemeSkewerDescription => 'A motif involving a high value piece being attacked, moving out the way, and allowing a lower value piece behind it to be captured or attacked, the inverse of a pin.'; + String get preferencesConfirmResignationAndDrawOffers => 'Confirm resignation and draw offers'; @override - String get puzzleThemeSmotheredMate => 'Smothered mate'; + String get preferencesCastleByMovingTheKingTwoSquaresOrOntoTheRook => 'Castling method'; @override - String get puzzleThemeSmotheredMateDescription => 'A checkmate delivered by a knight in which the mated king is unable to move because it is surrounded (or smothered) by its own pieces.'; + String get preferencesCastleByMovingTwoSquares => 'Move king two squares'; @override - String get puzzleThemeSuperGM => 'Super GM games'; + String get preferencesCastleByMovingOntoTheRook => 'Move king onto rook'; @override - String get puzzleThemeSuperGMDescription => 'Puzzles from games played by the best players in the world.'; + String get preferencesInputMovesWithTheKeyboard => 'Input moves with the keyboard'; @override - String get puzzleThemeTrappedPiece => 'Trapped piece'; + String get preferencesInputMovesWithVoice => 'Input moves with your voice'; @override - String get puzzleThemeTrappedPieceDescription => 'A piece is unable to escape capture as it has limited moves.'; + String get preferencesSnapArrowsToValidMoves => 'Snap arrows to valid moves'; @override - String get puzzleThemeUnderPromotion => 'Underpromotion'; + String get preferencesSayGgWpAfterLosingOrDrawing => 'Say \"Good game, well played\" upon defeat or draw'; @override - String get puzzleThemeUnderPromotionDescription => 'Promotion to a knight, bishop, or rook.'; + String get preferencesYourPreferencesHaveBeenSaved => 'Your preferences have been saved.'; @override - String get puzzleThemeVeryLong => 'Very long puzzle'; + String get preferencesScrollOnTheBoardToReplayMoves => 'Scroll on the board to replay moves'; @override - String get puzzleThemeVeryLongDescription => 'Four moves or more to win.'; + String get preferencesCorrespondenceEmailNotification => 'Daily mail notification listing your correspondence games'; @override - String get puzzleThemeXRayAttack => 'X-Ray attack'; + String get preferencesNotifyStreamStart => 'Streamer goes live'; @override - String get puzzleThemeXRayAttackDescription => 'A piece attacks or defends a square, through an enemy piece.'; + String get preferencesNotifyInboxMsg => 'New inbox message'; @override - String get puzzleThemeZugzwang => 'Zugzwang'; + String get preferencesNotifyForumMention => 'Forum comment mentions you'; @override - String get puzzleThemeZugzwangDescription => 'The opponent is limited in the moves they can make, and all moves worsen their position.'; + String get preferencesNotifyInvitedStudy => 'Study invite'; @override - String get puzzleThemeHealthyMix => 'Healthy mix'; + String get preferencesNotifyGameEvent => 'Correspondence game updates'; @override - String get puzzleThemeHealthyMixDescription => 'A bit of everything. You don\'t know what to expect, so you remain ready for anything! Just like in real games.'; + String get preferencesNotifyChallenge => 'Challenges'; @override - String get puzzleThemePlayerGames => 'Player games'; + String get preferencesNotifyTournamentSoon => 'Tournament starting soon'; @override - String get puzzleThemePlayerGamesDescription => 'Lookup puzzles generated from your games, or from another player\'s games.'; + String get preferencesNotifyTimeAlarm => 'Correspondence time running out'; @override - String puzzleThemePuzzleDownloadInformation(String param) { - return 'These puzzles are in the public domain, and can be downloaded from $param.'; - } + String get preferencesNotifyBell => 'Bell notification within Lichess'; @override - String get searchSearch => 'Search'; + String get preferencesNotifyPush => 'Device notification when you\'re not on Lichess'; @override - String get settingsSettings => 'Settings'; + String get preferencesNotifyWeb => 'Browser'; @override - String get settingsCloseAccount => 'Close account'; + String get preferencesNotifyDevice => 'Device'; @override - String get settingsManagedAccountCannotBeClosed => 'Your account is managed and cannot be closed.'; + String get preferencesBellNotificationSound => 'Bell notification sound'; @override - String get settingsClosingIsDefinitive => 'Closing is definitive. There is no going back. Are you sure?'; + String get puzzlePuzzles => 'Puzzles'; @override - String get settingsCantOpenSimilarAccount => 'You will not be allowed to open a new account with the same name, even if the case is different.'; + String get puzzlePuzzleThemes => 'Puzzle Themes'; @override - String get settingsChangedMindDoNotCloseAccount => 'I changed my mind, don\'t close my account'; + String get puzzleRecommended => 'Recommended'; @override - String get settingsCloseAccountExplanation => 'Are you sure you want to close your account? Closing your account is a permanent decision. You will NEVER be able to log in EVER AGAIN.'; + String get puzzlePhases => 'Phases'; @override - String get settingsThisAccountIsClosed => 'This account is closed.'; + String get puzzleMotifs => 'Motifs'; @override - String get playWithAFriend => 'Play with a friend'; + String get puzzleAdvanced => 'Advanced'; @override - String get playWithTheMachine => 'Play with the computer'; + String get puzzleLengths => 'Lengths'; @override - String get toInviteSomeoneToPlayGiveThisUrl => 'To invite someone to play, give this URL'; + String get puzzleMates => 'Mates'; @override - String get gameOver => 'Game Over'; + String get puzzleGoals => 'Goals'; @override - String get waitingForOpponent => 'Waiting for opponent'; + String get puzzleOrigin => 'Origin'; @override - String get orLetYourOpponentScanQrCode => 'Or let your opponent scan this QR code'; + String get puzzleSpecialMoves => 'Special moves'; @override - String get waiting => 'Waiting'; + String get puzzleDidYouLikeThisPuzzle => 'Did you like this puzzle?'; @override - String get yourTurn => 'Your turn'; + String get puzzleVoteToLoadNextOne => 'Vote to load the next one!'; @override - String aiNameLevelAiLevel(String param1, String param2) { - return '$param1 level $param2'; - } + String get puzzleUpVote => 'Upvote puzzle'; @override - String get level => 'Level'; + String get puzzleDownVote => 'Downvote puzzle'; @override - String get strength => 'Strength'; + String get puzzleYourPuzzleRatingWillNotChange => 'Your puzzle rating will not change. Note that puzzles are not a competition. Your rating helps selecting the best puzzles for your current skill.'; @override - String get toggleTheChat => 'Toggle the chat'; + String get puzzleFindTheBestMoveForWhite => 'Find the best move for white.'; @override - String get chat => 'Chat'; + String get puzzleFindTheBestMoveForBlack => 'Find the best move for black.'; @override - String get resign => 'Resign'; + String get puzzleToGetPersonalizedPuzzles => 'To get personalized puzzles:'; @override - String get checkmate => 'Checkmate'; + String puzzlePuzzleId(String param) { + return 'Puzzle $param'; + } @override - String get stalemate => 'Stalemate'; + String get puzzlePuzzleOfTheDay => 'Puzzle of the day'; @override - String get white => 'White'; + String get puzzleDailyPuzzle => 'Daily Puzzle'; @override - String get black => 'Black'; + String get puzzleClickToSolve => 'Click to solve'; @override - String get asWhite => 'as white'; + String get puzzleGoodMove => 'Good move'; @override - String get asBlack => 'as black'; + String get puzzleBestMove => 'Best move!'; @override - String get randomColor => 'Random side'; + String get puzzleKeepGoing => 'Keep going…'; @override - String get createAGame => 'Create a game'; + String get puzzlePuzzleSuccess => 'Success!'; @override - String get whiteIsVictorious => 'White is victorious'; + String get puzzlePuzzleComplete => 'Puzzle complete!'; @override - String get blackIsVictorious => 'Black is victorious'; + String get puzzleByOpenings => 'By openings'; @override - String get youPlayTheWhitePieces => 'You play the white pieces'; + String get puzzlePuzzlesByOpenings => 'Puzzles by openings'; @override - String get youPlayTheBlackPieces => 'You play the black pieces'; + String get puzzleOpeningsYouPlayedTheMost => 'Openings you played the most in rated games'; @override - String get itsYourTurn => 'It\'s your turn!'; + String get puzzleUseFindInPage => 'Use \"Find in page\" in the browser menu to find your favorite opening!'; @override - String get cheatDetected => 'Cheat Detected'; + String get puzzleUseCtrlF => 'Use Ctrl+f to find your favorite opening!'; @override - String get kingInTheCenter => 'King in the center'; + String get puzzleNotTheMove => 'That\'s not the move!'; @override - String get threeChecks => 'Three checks'; + String get puzzleTrySomethingElse => 'Try something else.'; @override - String get raceFinished => 'Race finished'; + String puzzleRatingX(String param) { + return 'Rating: $param'; + } @override - String get variantEnding => 'Variant ending'; + String get puzzleHidden => 'hidden'; @override - String get newOpponent => 'New opponent'; + String puzzleFromGameLink(String param) { + return 'From game $param'; + } @override - String get yourOpponentWantsToPlayANewGameWithYou => 'Your opponent wants to play a new game with you'; + String get puzzleContinueTraining => 'Continue training'; @override - String get joinTheGame => 'Join the game'; + String get puzzleDifficultyLevel => 'Difficulty level'; @override - String get whitePlays => 'White to play'; + String get puzzleNormal => 'Normal'; @override - String get blackPlays => 'Black to play'; + String get puzzleEasier => 'Easier'; @override - String get opponentLeftChoices => 'Your opponent left the game. You can claim victory, call the game a draw, or wait.'; + String get puzzleEasiest => 'Easiest'; @override - String get forceResignation => 'Claim victory'; + String get puzzleHarder => 'Harder'; @override - String get forceDraw => 'Call draw'; + String get puzzleHardest => 'Hardest'; @override - String get talkInChat => 'Please be nice in the chat!'; + String get puzzleExample => 'Example'; @override - String get theFirstPersonToComeOnThisUrlWillPlayWithYou => 'The first person to come to this URL will play with you.'; + String get puzzleAddAnotherTheme => 'Add another theme'; @override - String get whiteResigned => 'White resigned'; + String get puzzleNextPuzzle => 'Next puzzle'; @override - String get blackResigned => 'Black resigned'; + String get puzzleJumpToNextPuzzleImmediately => 'Jump to next puzzle immediately'; @override - String get whiteLeftTheGame => 'White left the game'; + String get puzzlePuzzleDashboard => 'Puzzle Dashboard'; @override - String get blackLeftTheGame => 'Black left the game'; + String get puzzleImprovementAreas => 'Improvement areas'; @override - String get whiteDidntMove => 'White didn\'t move'; + String get puzzleStrengths => 'Strengths'; @override - String get blackDidntMove => 'Black didn\'t move'; + String get puzzleHistory => 'Puzzle history'; @override - String get requestAComputerAnalysis => 'Request a computer analysis'; + String get puzzleSolved => 'solved'; @override - String get computerAnalysis => 'Computer analysis'; + String get puzzleFailed => 'incorrect'; @override - String get computerAnalysisAvailable => 'Computer analysis available'; + String get puzzleStreakDescription => 'Solve progressively harder puzzles and build a win streak. There is no clock, so take your time. One wrong move, and it\'s game over! But you can skip one move per session.'; @override - String get computerAnalysisDisabled => 'Computer analysis disabled'; + String puzzleYourStreakX(String param) { + return 'Your streak: $param'; + } @override - String get analysis => 'Analysis board'; + String get puzzleStreakSkipExplanation => 'Skip this move to preserve your streak! Only works once per run.'; @override - String depthX(String param) { - return 'Depth $param'; - } + String get puzzleContinueTheStreak => 'Continue the streak'; @override - String get usingServerAnalysis => 'Using server analysis'; + String get puzzleNewStreak => 'New streak'; @override - String get loadingEngine => 'Loading engine ...'; + String get puzzleFromMyGames => 'From my games'; @override - String get calculatingMoves => 'Calculating moves...'; + String get puzzleLookupOfPlayer => 'Search puzzles from a player\'s games'; @override - String get engineFailed => 'Error loading engine'; + String puzzleFromXGames(String param) { + return 'Puzzles from $param\'s games'; + } @override - String get cloudAnalysis => 'Cloud analysis'; + String get puzzleSearchPuzzles => 'Search puzzles'; @override - String get goDeeper => 'Go deeper'; + String get puzzleFromMyGamesNone => 'You have no puzzles in the database, but Lichess still loves you very much.\n\nPlay rapid and classical games to increase your chances of having a puzzle of yours added!'; @override - String get showThreat => 'Show threat'; + String puzzleFromXGamesFound(String param1, String param2) { + return '$param1 puzzles found in $param2 games'; + } @override - String get inLocalBrowser => 'in local browser'; + String get puzzlePuzzleDashboardDescription => 'Train, analyse, improve'; @override - String get toggleLocalEvaluation => 'Toggle local evaluation'; + String puzzlePercentSolved(String param) { + return '$param solved'; + } @override - String get promoteVariation => 'Promote variation'; + String get puzzleNoPuzzlesToShow => 'Nothing to show, go play some puzzles first!'; @override - String get makeMainLine => 'Make main line'; + String get puzzleImprovementAreasDescription => 'Train these to optimize your progress!'; @override - String get deleteFromHere => 'Delete from here'; + String get puzzleStrengthDescription => 'You perform the best in these themes'; @override - String get collapseVariations => 'Collapse variations'; + String puzzlePlayedXTimes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Played $count times', + one: 'Played $count time', + ); + return '$_temp0'; + } @override - String get expandVariations => 'Expand variations'; + String puzzleNbPointsBelowYourPuzzleRating(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count points below your puzzle rating', + one: 'One point below your puzzle rating', + ); + return '$_temp0'; + } @override - String get forceVariation => 'Force variation'; + String puzzleNbPointsAboveYourPuzzleRating(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count points above your puzzle rating', + one: 'One point above your puzzle rating', + ); + return '$_temp0'; + } @override - String get copyVariationPgn => 'Copy variation PGN'; + String puzzleNbPlayed(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count played', + one: '$count played', + ); + return '$_temp0'; + } @override - String get move => 'Move'; + String puzzleNbToReplay(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count to replay', + one: '$count to replay', + ); + return '$_temp0'; + } @override - String get variantLoss => 'Variant loss'; + String get puzzleThemeAdvancedPawn => 'Advanced pawn'; @override - String get variantWin => 'Variant win'; + String get puzzleThemeAdvancedPawnDescription => 'One of your pawns is deep into the opponent position, maybe threatening to promote.'; @override - String get insufficientMaterial => 'Insufficient material'; + String get puzzleThemeAdvantage => 'Advantage'; @override - String get pawnMove => 'Pawn move'; + String get puzzleThemeAdvantageDescription => 'Seize your chance to get a decisive advantage. (200cp ≤ eval ≤ 600cp)'; @override - String get capture => 'Capture'; + String get puzzleThemeAnastasiaMate => 'Anastasia\'s mate'; @override - String get close => 'Close'; + String get puzzleThemeAnastasiaMateDescription => 'A knight and rook or queen team up to trap the opposing king between the side of the board and a friendly piece.'; @override - String get winning => 'Winning'; + String get puzzleThemeArabianMate => 'Arabian mate'; @override - String get losing => 'Losing'; + String get puzzleThemeArabianMateDescription => 'A knight and a rook team up to trap the opposing king on a corner of the board.'; @override - String get drawn => 'Drawn'; + String get puzzleThemeAttackingF2F7 => 'Attacking f2 or f7'; @override - String get unknown => 'Unknown'; + String get puzzleThemeAttackingF2F7Description => 'An attack focusing on the f2 or f7 pawn, such as in the fried liver opening.'; @override - String get database => 'Database'; + String get puzzleThemeAttraction => 'Attraction'; @override - String get whiteDrawBlack => 'White / Draw / Black'; + String get puzzleThemeAttractionDescription => 'An exchange or sacrifice encouraging or forcing an opponent piece to a square that allows a follow-up tactic.'; @override - String averageRatingX(String param) { - return 'Average rating: $param'; - } + String get puzzleThemeBackRankMate => 'Back rank mate'; @override - String get recentGames => 'Recent games'; + String get puzzleThemeBackRankMateDescription => 'Checkmate the king on the home rank, when it is trapped there by its own pieces.'; @override - String get topGames => 'Top games'; + String get puzzleThemeBishopEndgame => 'Bishop endgame'; @override - String masterDbExplanation(String param1, String param2, String param3) { - return 'OTB games of $param1+ FIDE rated players from $param2 to $param3'; - } + String get puzzleThemeBishopEndgameDescription => 'An endgame with only bishops and pawns.'; @override - String get dtzWithRounding => 'DTZ50\'\' with rounding, based on number of half-moves until next capture or pawn move'; + String get puzzleThemeBodenMate => 'Boden\'s mate'; @override - String get noGameFound => 'No game found'; + String get puzzleThemeBodenMateDescription => 'Two attacking bishops on criss-crossing diagonals deliver mate to a king obstructed by friendly pieces.'; @override - String get maxDepthReached => 'Max depth reached!'; + String get puzzleThemeCastling => 'Castling'; @override - String get maybeIncludeMoreGamesFromThePreferencesMenu => 'Maybe include more games from the preferences menu?'; + String get puzzleThemeCastlingDescription => 'Bring the king to safety, and deploy the rook for attack.'; @override - String get openings => 'Openings'; + String get puzzleThemeCapturingDefender => 'Capture the defender'; @override - String get openingExplorer => 'Opening explorer'; + String get puzzleThemeCapturingDefenderDescription => 'Removing a piece that is critical to defense of another piece, allowing the now undefended piece to be captured on a following move.'; @override - String get openingEndgameExplorer => 'Opening/endgame explorer'; + String get puzzleThemeCrushing => 'Crushing'; @override - String xOpeningExplorer(String param) { - return '$param opening explorer'; - } + String get puzzleThemeCrushingDescription => 'Spot the opponent blunder to obtain a crushing advantage. (eval ≥ 600cp)'; @override - String get playFirstOpeningEndgameExplorerMove => 'Play first opening/endgame-explorer move'; + String get puzzleThemeDoubleBishopMate => 'Double bishop mate'; @override - String get winPreventedBy50MoveRule => 'Win prevented by 50-move rule'; + String get puzzleThemeDoubleBishopMateDescription => 'Two attacking bishops on adjacent diagonals deliver mate to a king obstructed by friendly pieces.'; @override - String get lossSavedBy50MoveRule => 'Loss prevented by 50-move rule'; + String get puzzleThemeDovetailMate => 'Dovetail mate'; @override - String get winOr50MovesByPriorMistake => 'Win or 50 moves by prior mistake'; + String get puzzleThemeDovetailMateDescription => 'A queen delivers mate to an adjacent king, whose only two escape squares are obstructed by friendly pieces.'; @override - String get lossOr50MovesByPriorMistake => 'Loss or 50 moves by prior mistake'; + String get puzzleThemeEquality => 'Equality'; @override - String get unknownDueToRounding => 'Due to possible rounding of DTZ values in Syzygy tablebases, a win/loss is only guaranteed if the recommended tablebase line has been followed since the last capture or pawn move.'; + String get puzzleThemeEqualityDescription => 'Come back from a losing position, and secure a draw or a balanced position. (eval ≤ 200cp)'; @override - String get allSet => 'All set!'; + String get puzzleThemeKingsideAttack => 'Kingside attack'; @override - String get importPgn => 'Import PGN'; + String get puzzleThemeKingsideAttackDescription => 'An attack of the opponent\'s king, after they castled on the king side.'; @override - String get delete => 'Delete'; + String get puzzleThemeClearance => 'Clearance'; @override - String get deleteThisImportedGame => 'Delete this imported game?'; + String get puzzleThemeClearanceDescription => 'A move, often with tempo, that clears a square, file or diagonal for a follow-up tactical idea.'; @override - String get replayMode => 'Replay mode'; + String get puzzleThemeDefensiveMove => 'Defensive move'; @override - String get realtimeReplay => 'Realtime'; + String get puzzleThemeDefensiveMoveDescription => 'A precise move or sequence of moves that is needed to avoid losing material or another advantage.'; @override - String get byCPL => 'By CPL'; + String get puzzleThemeDeflection => 'Deflection'; @override - String get openStudy => 'Open study'; + String get puzzleThemeDeflectionDescription => 'A move that distracts an opponent piece from another duty that it performs, such as guarding a key square. Sometimes also called \"overloading\".'; @override - String get enable => 'Enable'; + String get puzzleThemeDiscoveredAttack => 'Discovered attack'; @override - String get bestMoveArrow => 'Best move arrow'; + String get puzzleThemeDiscoveredAttackDescription => 'Moving a piece that previously blocked an attack by another long range piece, such as a knight out of the way of a rook.'; @override - String get showVariationArrows => 'Show variation arrows'; + String get puzzleThemeDoubleCheck => 'Double check'; @override - String get evaluationGauge => 'Evaluation gauge'; + String get puzzleThemeDoubleCheckDescription => 'Checking with two pieces at once, as a result of a discovered attack where both the moving piece and the unveiled piece attack the opponent\'s king.'; @override - String get multipleLines => 'Multiple lines'; + String get puzzleThemeEndgame => 'Endgame'; @override - String get cpus => 'CPUs'; + String get puzzleThemeEndgameDescription => 'A tactic during the last phase of the game.'; @override - String get memory => 'Memory'; + String get puzzleThemeEnPassantDescription => 'A tactic involving the en passant rule, where a pawn can capture an opponent pawn that has bypassed it using its initial two-square move.'; @override - String get infiniteAnalysis => 'Infinite analysis'; + String get puzzleThemeExposedKing => 'Exposed king'; @override - String get removesTheDepthLimit => 'Removes the depth limit, and keeps your computer warm'; + String get puzzleThemeExposedKingDescription => 'A tactic involving a king with few defenders around it, often leading to checkmate.'; @override - String get engineManager => 'Engine manager'; + String get puzzleThemeFork => 'Fork'; @override - String get blunder => 'Blunder'; + String get puzzleThemeForkDescription => 'A move where the moved piece attacks two opponent pieces at once.'; @override - String get mistake => 'Mistake'; + String get puzzleThemeHangingPiece => 'Hanging piece'; @override - String get inaccuracy => 'Inaccuracy'; + String get puzzleThemeHangingPieceDescription => 'A tactic involving an opponent piece being undefended or insufficiently defended and free to capture.'; @override - String get moveTimes => 'Move times'; + String get puzzleThemeHookMate => 'Hook mate'; @override - String get flipBoard => 'Flip board'; + String get puzzleThemeHookMateDescription => 'Checkmate with a rook, knight, and pawn along with one enemy pawn to limit the enemy king\'s escape.'; @override - String get threefoldRepetition => 'Threefold repetition'; + String get puzzleThemeInterference => 'Interference'; @override - String get claimADraw => 'Claim a draw'; + String get puzzleThemeInterferenceDescription => 'Moving a piece between two opponent pieces to leave one or both opponent pieces undefended, such as a knight on a defended square between two rooks.'; @override - String get offerDraw => 'Offer draw'; + String get puzzleThemeIntermezzo => 'Intermezzo'; @override - String get draw => 'Draw'; + String get puzzleThemeIntermezzoDescription => 'Instead of playing the expected move, first interpose another move posing an immediate threat that the opponent must answer. Also known as \"Zwischenzug\" or \"In between\".'; @override - String get drawByMutualAgreement => 'Draw by mutual agreement'; + String get puzzleThemeKnightEndgame => 'Knight endgame'; @override - String get fiftyMovesWithoutProgress => 'Fifty moves without progress'; + String get puzzleThemeKnightEndgameDescription => 'An endgame with only knights and pawns.'; @override - String get currentGames => 'Current games'; + String get puzzleThemeLong => 'Long puzzle'; @override - String get viewInFullSize => 'View in full size'; + String get puzzleThemeLongDescription => 'Three moves to win.'; @override - String get logOut => 'Sign out'; + String get puzzleThemeMaster => 'Master games'; @override - String get signIn => 'Sign in'; + String get puzzleThemeMasterDescription => 'Puzzles from games played by titled players.'; @override - String get rememberMe => 'Keep me logged in'; + String get puzzleThemeMasterVsMaster => 'Master vs Master games'; @override - String get youNeedAnAccountToDoThat => 'You need an account to do that'; + String get puzzleThemeMasterVsMasterDescription => 'Puzzles from games between two titled players.'; @override - String get signUp => 'Register'; + String get puzzleThemeMate => 'Checkmate'; @override - String get computersAreNotAllowedToPlay => 'Computers and computer-assisted players are not allowed to play. Please do not get assistance from chess engines, databases, or from other players while playing. Also note that making multiple accounts is strongly discouraged and excessive multi-accounting will lead to being banned.'; + String get puzzleThemeMateDescription => 'Win the game with style.'; @override - String get games => 'Games'; + String get puzzleThemeMateIn1 => 'Mate in 1'; @override - String get forum => 'Forum'; - - @override - String xPostedInForumY(String param1, String param2) { - return '$param1 posted in topic $param2'; - } + String get puzzleThemeMateIn1Description => 'Deliver checkmate in one move.'; @override - String get latestForumPosts => 'Latest forum posts'; + String get puzzleThemeMateIn2 => 'Mate in 2'; @override - String get players => 'Players'; + String get puzzleThemeMateIn2Description => 'Deliver checkmate in two moves.'; @override - String get friends => 'Friends'; + String get puzzleThemeMateIn3 => 'Mate in 3'; @override - String get otherPlayers => 'other players'; + String get puzzleThemeMateIn3Description => 'Deliver checkmate in three moves.'; @override - String get discussions => 'Conversations'; + String get puzzleThemeMateIn4 => 'Mate in 4'; @override - String get today => 'Today'; + String get puzzleThemeMateIn4Description => 'Deliver checkmate in four moves.'; @override - String get yesterday => 'Yesterday'; + String get puzzleThemeMateIn5 => 'Mate in 5 or more'; @override - String get minutesPerSide => 'Minutes per side'; + String get puzzleThemeMateIn5Description => 'Figure out a long mating sequence.'; @override - String get variant => 'Variant'; + String get puzzleThemeMiddlegame => 'Middlegame'; @override - String get variants => 'Variants'; + String get puzzleThemeMiddlegameDescription => 'A tactic during the second phase of the game.'; @override - String get timeControl => 'Time control'; + String get puzzleThemeOneMove => 'One-move puzzle'; @override - String get realTime => 'Real time'; + String get puzzleThemeOneMoveDescription => 'A puzzle that is only one move long.'; @override - String get correspondence => 'Correspondence'; + String get puzzleThemeOpening => 'Opening'; @override - String get daysPerTurn => 'Days per turn'; + String get puzzleThemeOpeningDescription => 'A tactic during the first phase of the game.'; @override - String get oneDay => 'One day'; + String get puzzleThemePawnEndgame => 'Pawn endgame'; @override - String get time => 'Time'; + String get puzzleThemePawnEndgameDescription => 'An endgame with only pawns.'; @override - String get rating => 'Rating'; + String get puzzleThemePin => 'Pin'; @override - String get ratingStats => 'Rating stats'; + String get puzzleThemePinDescription => 'A tactic involving pins, where a piece is unable to move without revealing an attack on a higher value piece.'; @override - String get username => 'User name'; + String get puzzleThemePromotion => 'Promotion'; @override - String get usernameOrEmail => 'User name or email'; + String get puzzleThemePromotionDescription => 'Promote one of your pawns to a queen or minor piece.'; @override - String get changeUsername => 'Change username'; + String get puzzleThemeQueenEndgame => 'Queen endgame'; @override - String get changeUsernameNotSame => 'Only the case of the letters can change. For example \"johndoe\" to \"JohnDoe\".'; + String get puzzleThemeQueenEndgameDescription => 'An endgame with only queens and pawns.'; @override - String get changeUsernameDescription => 'Change your username. This can only be done once and you are only allowed to change the case of the letters in your username.'; + String get puzzleThemeQueenRookEndgame => 'Queen and Rook'; @override - String get signupUsernameHint => 'Be sure to choose a family-friendly username. You cannot change it later, and any accounts with inappropriate usernames will be closed!'; + String get puzzleThemeQueenRookEndgameDescription => 'An endgame with only queens, rooks, and pawns.'; @override - String get signupEmailHint => 'We will only use it for password reset.'; + String get puzzleThemeQueensideAttack => 'Queenside attack'; @override - String get password => 'Password'; + String get puzzleThemeQueensideAttackDescription => 'An attack of the opponent\'s king, after they castled on the queen side.'; @override - String get changePassword => 'Change password'; + String get puzzleThemeQuietMove => 'Quiet move'; @override - String get changeEmail => 'Change email'; + String get puzzleThemeQuietMoveDescription => 'A move that does not make a check or capture, but does prepare an unavoidable threat for a later move.'; @override - String get email => 'Email'; + String get puzzleThemeRookEndgame => 'Rook endgame'; @override - String get passwordReset => 'Password reset'; + String get puzzleThemeRookEndgameDescription => 'An endgame with only rooks and pawns.'; @override - String get forgotPassword => 'Forgot password?'; + String get puzzleThemeSacrifice => 'Sacrifice'; @override - String get error_weakPassword => 'This password is extremely common and too easy to guess.'; + String get puzzleThemeSacrificeDescription => 'A tactic involving giving up material in the short-term, to gain an advantage again after a forced sequence of moves.'; @override - String get error_namePassword => 'Please don\'t use your username as your password.'; + String get puzzleThemeShort => 'Short puzzle'; @override - String get blankedPassword => 'You have used the same password on another site, and that site has been compromised. To ensure the safety of your Lichess account, we need you to set a new password. Thank you for your understanding.'; + String get puzzleThemeShortDescription => 'Two moves to win.'; @override - String get youAreLeavingLichess => 'You are leaving Lichess'; + String get puzzleThemeSkewer => 'Skewer'; @override - String get neverTypeYourPassword => 'Never type your Lichess password on another site!'; + String get puzzleThemeSkewerDescription => 'A motif involving a high value piece being attacked, moving out the way, and allowing a lower value piece behind it to be captured or attacked, the inverse of a pin.'; @override - String proceedToX(String param) { - return 'Proceed to $param'; - } + String get puzzleThemeSmotheredMate => 'Smothered mate'; @override - String get passwordSuggestion => 'Do not set a password suggested by someone else. They will use it to steal your account.'; + String get puzzleThemeSmotheredMateDescription => 'A checkmate delivered by a knight in which the mated king is unable to move because it is surrounded (or smothered) by its own pieces.'; @override - String get emailSuggestion => 'Do not set an email address suggested by someone else. They will use it to steal your account.'; + String get puzzleThemeSuperGM => 'Super GM games'; @override - String get emailConfirmHelp => 'Help with email confirmation'; + String get puzzleThemeSuperGMDescription => 'Puzzles from games played by the best players in the world.'; @override - String get emailConfirmNotReceived => 'Didn\'t receive your confirmation email after signing up?'; + String get puzzleThemeTrappedPiece => 'Trapped piece'; @override - String get whatSignupUsername => 'What username did you use to sign up?'; + String get puzzleThemeTrappedPieceDescription => 'A piece is unable to escape capture as it has limited moves.'; @override - String usernameNotFound(String param) { - return 'We couldn\'t find any user by this name: $param.'; - } + String get puzzleThemeUnderPromotion => 'Underpromotion'; @override - String get usernameCanBeUsedForNewAccount => 'You can use this username to create a new account'; + String get puzzleThemeUnderPromotionDescription => 'Promotion to a knight, bishop, or rook.'; @override - String emailSent(String param) { - return 'We have sent an email to $param.'; - } + String get puzzleThemeVeryLong => 'Very long puzzle'; @override - String get emailCanTakeSomeTime => 'It can take some time to arrive.'; + String get puzzleThemeVeryLongDescription => 'Four moves or more to win.'; @override - String get refreshInboxAfterFiveMinutes => 'Wait 5 minutes and refresh your email inbox.'; + String get puzzleThemeXRayAttack => 'X-Ray attack'; @override - String get checkSpamFolder => 'Also check your spam folder, it might end up there. If so, mark it as not spam.'; + String get puzzleThemeXRayAttackDescription => 'A piece attacks or defends a square, through an enemy piece.'; @override - String get emailForSignupHelp => 'If you still have questions, please send us an email:'; + String get puzzleThemeZugzwang => 'Zugzwang'; @override - String copyTextToEmail(String param) { - return 'Copy and paste the above text and send it to $param'; - } + String get puzzleThemeZugzwangDescription => 'The opponent is limited in the moves they can make, and all moves worsen their position.'; @override - String get waitForSignupHelp => 'We will come back to you shortly to help you complete your signup.'; + String get puzzleThemeMix => 'Healthy mix'; @override - String accountConfirmed(String param) { - return 'The user $param is successfully confirmed.'; - } + String get puzzleThemeMixDescription => 'A bit of everything. You don\'t know what to expect, so you remain ready for anything! Just like in real games.'; @override - String accountCanLogin(String param) { - return 'You can login right now as $param.'; - } + String get puzzleThemePlayerGames => 'Player games'; @override - String get accountConfirmationEmailNotNeeded => 'You do not need a confirmation email.'; + String get puzzleThemePlayerGamesDescription => 'Lookup puzzles generated from your games, or from another player\'s games.'; @override - String accountClosed(String param) { - return 'The account $param is closed.'; + String puzzleThemePuzzleDownloadInformation(String param) { + return 'These puzzles are in the public domain, and can be downloaded from $param.'; } @override - String accountRegisteredWithoutEmail(String param) { - return 'The account $param was registered without an email.'; - } + String get searchSearch => 'Search'; @override - String get rank => 'Rank'; + String get settingsSettings => 'Settings'; @override - String rankX(String param) { - return 'Rank: $param'; - } + String get settingsCloseAccount => 'Close account'; @override - String get gamesPlayed => 'Games played'; + String get settingsManagedAccountCannotBeClosed => 'Your account is managed and cannot be closed.'; @override - String get cancel => 'Cancel'; + String get settingsClosingIsDefinitive => 'Closing is definitive. There is no going back. Are you sure?'; @override - String get whiteTimeOut => 'White time out'; + String get settingsCantOpenSimilarAccount => 'You will not be allowed to open a new account with the same name, even if the case is different.'; @override - String get blackTimeOut => 'Black time out'; + String get settingsChangedMindDoNotCloseAccount => 'I changed my mind, don\'t close my account'; @override - String get drawOfferSent => 'Draw offer sent'; + String get settingsCloseAccountExplanation => 'Are you sure you want to close your account? Closing your account is a permanent decision. You will NEVER be able to log in EVER AGAIN.'; @override - String get drawOfferAccepted => 'Draw offer accepted'; + String get settingsThisAccountIsClosed => 'This account is closed.'; @override - String get drawOfferCanceled => 'Draw offer canceled'; + String get playWithAFriend => 'Play with a friend'; @override - String get whiteOffersDraw => 'White offers draw'; + String get playWithTheMachine => 'Play with the computer'; @override - String get blackOffersDraw => 'Black offers draw'; + String get toInviteSomeoneToPlayGiveThisUrl => 'To invite someone to play, give this URL'; @override - String get whiteDeclinesDraw => 'White declines draw'; + String get gameOver => 'Game Over'; @override - String get blackDeclinesDraw => 'Black declines draw'; + String get waitingForOpponent => 'Waiting for opponent'; @override - String get yourOpponentOffersADraw => 'Your opponent offers a draw'; + String get orLetYourOpponentScanQrCode => 'Or let your opponent scan this QR code'; @override - String get accept => 'Accept'; + String get waiting => 'Waiting'; @override - String get decline => 'Decline'; + String get yourTurn => 'Your turn'; @override - String get playingRightNow => 'Playing right now'; + String aiNameLevelAiLevel(String param1, String param2) { + return '$param1 level $param2'; + } @override - String get eventInProgress => 'Playing right now'; + String get level => 'Level'; @override - String get finished => 'Finished'; + String get strength => 'Strength'; @override - String get abortGame => 'Abort game'; + String get toggleTheChat => 'Toggle the chat'; @override - String get gameAborted => 'Game aborted'; + String get chat => 'Chat'; @override - String get standard => 'Standard'; + String get resign => 'Resign'; @override - String get customPosition => 'Custom position'; + String get checkmate => 'Checkmate'; @override - String get unlimited => 'Unlimited'; + String get stalemate => 'Stalemate'; @override - String get mode => 'Mode'; + String get white => 'White'; @override - String get casual => 'Casual'; + String get black => 'Black'; @override - String get rated => 'Rated'; + String get asWhite => 'as white'; @override - String get casualTournament => 'Casual'; + String get asBlack => 'as black'; @override - String get ratedTournament => 'Rated'; + String get randomColor => 'Random side'; @override - String get thisGameIsRated => 'This game is rated'; + String get createAGame => 'Create a game'; @override - String get rematch => 'Rematch'; + String get whiteIsVictorious => 'White is victorious'; @override - String get rematchOfferSent => 'Rematch offer sent'; + String get blackIsVictorious => 'Black is victorious'; @override - String get rematchOfferAccepted => 'Rematch offer accepted'; + String get youPlayTheWhitePieces => 'You play the white pieces'; @override - String get rematchOfferCanceled => 'Rematch offer canceled'; + String get youPlayTheBlackPieces => 'You play the black pieces'; @override - String get rematchOfferDeclined => 'Rematch offer declined'; + String get itsYourTurn => 'It\'s your turn!'; @override - String get cancelRematchOffer => 'Cancel rematch offer'; + String get cheatDetected => 'Cheat Detected'; @override - String get viewRematch => 'View rematch'; + String get kingInTheCenter => 'King in the center'; @override - String get confirmMove => 'Confirm move'; + String get threeChecks => 'Three checks'; @override - String get play => 'Play'; + String get raceFinished => 'Race finished'; @override - String get inbox => 'Inbox'; + String get variantEnding => 'Variant ending'; @override - String get chatRoom => 'Chat room'; + String get newOpponent => 'New opponent'; @override - String get loginToChat => 'Sign in to chat'; + String get yourOpponentWantsToPlayANewGameWithYou => 'Your opponent wants to play a new game with you'; @override - String get youHaveBeenTimedOut => 'You have been timed out.'; + String get joinTheGame => 'Join the game'; @override - String get spectatorRoom => 'Spectator room'; + String get whitePlays => 'White to play'; @override - String get composeMessage => 'Compose message'; + String get blackPlays => 'Black to play'; @override - String get subject => 'Subject'; + String get opponentLeftChoices => 'Your opponent left the game. You can claim victory, call the game a draw, or wait.'; @override - String get send => 'Send'; + String get forceResignation => 'Claim victory'; @override - String get incrementInSeconds => 'Increment in seconds'; + String get forceDraw => 'Call draw'; @override - String get freeOnlineChess => 'Free Online Chess'; + String get talkInChat => 'Please be nice in the chat!'; @override - String get exportGames => 'Export games'; + String get theFirstPersonToComeOnThisUrlWillPlayWithYou => 'The first person to come to this URL will play with you.'; @override - String get ratingRange => 'Rating range'; + String get whiteResigned => 'White resigned'; @override - String get thisAccountViolatedTos => 'This account violated the Lichess Terms of Service'; + String get blackResigned => 'Black resigned'; @override - String get openingExplorerAndTablebase => 'Opening explorer & tablebase'; + String get whiteLeftTheGame => 'White left the game'; @override - String get takeback => 'Takeback'; + String get blackLeftTheGame => 'Black left the game'; @override - String get proposeATakeback => 'Propose a takeback'; + String get whiteDidntMove => 'White didn\'t move'; @override - String get takebackPropositionSent => 'Takeback sent'; + String get blackDidntMove => 'Black didn\'t move'; @override - String get takebackPropositionDeclined => 'Takeback declined'; + String get requestAComputerAnalysis => 'Request a computer analysis'; @override - String get takebackPropositionAccepted => 'Takeback accepted'; + String get computerAnalysis => 'Computer analysis'; @override - String get takebackPropositionCanceled => 'Takeback canceled'; + String get computerAnalysisAvailable => 'Computer analysis available'; @override - String get yourOpponentProposesATakeback => 'Your opponent proposes a takeback'; + String get computerAnalysisDisabled => 'Computer analysis disabled'; @override - String get bookmarkThisGame => 'Bookmark this game'; - - @override - String get tournament => 'Tournament'; + String get analysis => 'Analysis board'; @override - String get tournaments => 'Tournaments'; + String depthX(String param) { + return 'Depth $param'; + } @override - String get tournamentPoints => 'Tournament points'; + String get usingServerAnalysis => 'Using server analysis'; @override - String get viewTournament => 'View tournament'; + String get loadingEngine => 'Loading engine...'; @override - String get backToTournament => 'Back to tournament'; + String get calculatingMoves => 'Calculating moves...'; @override - String get noDrawBeforeSwissLimit => 'You cannot draw before 30 moves are played in a Swiss tournament.'; + String get engineFailed => 'Error loading engine'; @override - String get thematic => 'Thematic'; + String get cloudAnalysis => 'Cloud analysis'; @override - String yourPerfRatingIsProvisional(String param) { - return 'Your $param rating is provisional'; - } + String get goDeeper => 'Go deeper'; @override - String yourPerfRatingIsTooHigh(String param1, String param2) { - return 'Your $param1 rating ($param2) is too high'; - } + String get showThreat => 'Show threat'; @override - String yourTopWeeklyPerfRatingIsTooHigh(String param1, String param2) { - return 'Your top weekly $param1 rating ($param2) is too high'; - } + String get inLocalBrowser => 'in local browser'; @override - String yourPerfRatingIsTooLow(String param1, String param2) { - return 'Your $param1 rating ($param2) is too low'; - } + String get toggleLocalEvaluation => 'Toggle local evaluation'; @override - String ratedMoreThanInPerf(String param1, String param2) { - return 'Rated ≥ $param1 in $param2'; - } + String get promoteVariation => 'Promote variation'; @override - String ratedLessThanInPerf(String param1, String param2) { - return 'Rated ≤ $param1 in $param2 for the last week'; - } + String get makeMainLine => 'Make main line'; @override - String mustBeInTeam(String param) { - return 'Must be in team $param'; - } + String get deleteFromHere => 'Delete from here'; @override - String youAreNotInTeam(String param) { - return 'You are not in the team $param'; - } + String get collapseVariations => 'Collapse variations'; @override - String get backToGame => 'Back to game'; + String get expandVariations => 'Expand variations'; @override - String get siteDescription => 'Free online chess server. Play chess in a clean interface. No registration, no ads, no plugin required. Play chess with the computer, friends or random opponents.'; + String get forceVariation => 'Force variation'; @override - String xJoinedTeamY(String param1, String param2) { - return '$param1 joined team $param2'; - } + String get copyVariationPgn => 'Copy variation PGN'; @override - String xCreatedTeamY(String param1, String param2) { - return '$param1 created team $param2'; - } + String get move => 'Move'; @override - String get startedStreaming => 'started streaming'; + String get variantLoss => 'Variant loss'; @override - String xStartedStreaming(String param) { - return '$param started streaming'; - } + String get variantWin => 'Variant win'; @override - String get averageElo => 'Average rating'; + String get insufficientMaterial => 'Insufficient material'; @override - String get location => 'Location'; + String get pawnMove => 'Pawn move'; @override - String get filterGames => 'Filter games'; + String get capture => 'Capture'; @override - String get reset => 'Reset'; + String get close => 'Close'; @override - String get apply => 'Submit'; + String get winning => 'Winning'; @override - String get save => 'Save'; + String get losing => 'Losing'; @override - String get leaderboard => 'Leaderboard'; + String get drawn => 'Drawn'; @override - String get screenshotCurrentPosition => 'Screenshot current position'; + String get unknown => 'Unknown'; @override - String get gameAsGIF => 'Game as GIF'; + String get database => 'Database'; @override - String get pasteTheFenStringHere => 'Paste the FEN text here'; + String get whiteDrawBlack => 'White / Draw / Black'; @override - String get pasteThePgnStringHere => 'Paste the PGN text here'; + String averageRatingX(String param) { + return 'Average rating: $param'; + } @override - String get orUploadPgnFile => 'Or upload a PGN file'; + String get recentGames => 'Recent games'; @override - String get fromPosition => 'From position'; + String get topGames => 'Top games'; @override - String get continueFromHere => 'Continue from here'; + String masterDbExplanation(String param1, String param2, String param3) { + return 'OTB games of $param1+ FIDE rated players from $param2 to $param3'; + } @override - String get toStudy => 'Study'; + String get dtzWithRounding => 'DTZ50\'\' with rounding, based on number of half-moves until next capture or pawn move'; @override - String get importGame => 'Import game'; + String get noGameFound => 'No game found'; @override - String get importGameExplanation => 'Paste a game PGN to get a browsable replay,\ncomputer analysis, game chat and shareable URL.'; + String get maxDepthReached => 'Max depth reached!'; @override - String get importGameCaveat => 'Variations will be erased. To keep them, import the PGN via a study.'; + String get maybeIncludeMoreGamesFromThePreferencesMenu => 'Maybe include more games from the preferences menu?'; @override - String get importGameDataPrivacyWarning => 'This PGN can be accessed by the public. To import a game privately, use a study.'; + String get openings => 'Openings'; @override - String get thisIsAChessCaptcha => 'This is a chess CAPTCHA.'; + String get openingExplorer => 'Opening explorer'; @override - String get clickOnTheBoardToMakeYourMove => 'Click on the board to make your move, and prove you are human.'; + String get openingEndgameExplorer => 'Opening/endgame explorer'; @override - String get captcha_fail => 'Please solve the chess captcha.'; + String xOpeningExplorer(String param) { + return '$param opening explorer'; + } @override - String get notACheckmate => 'Not a checkmate'; + String get playFirstOpeningEndgameExplorerMove => 'Play first opening/endgame-explorer move'; @override - String get whiteCheckmatesInOneMove => 'White to checkmate in one move'; + String get winPreventedBy50MoveRule => 'Win prevented by 50-move rule'; @override - String get blackCheckmatesInOneMove => 'Black to checkmate in one move'; + String get lossSavedBy50MoveRule => 'Loss prevented by 50-move rule'; @override - String get retry => 'Retry'; + String get winOr50MovesByPriorMistake => 'Win or 50 moves by prior mistake'; @override - String get reconnecting => 'Reconnecting'; + String get lossOr50MovesByPriorMistake => 'Loss or 50 moves by prior mistake'; @override - String get noNetwork => 'Offline'; + String get unknownDueToRounding => 'Due to possible rounding of DTZ values in Syzygy tablebases, a win/loss is only guaranteed if the recommended tablebase line has been followed since the last capture or pawn move.'; @override - String get favoriteOpponents => 'Favorite opponents'; + String get allSet => 'All set!'; @override - String get follow => 'Follow'; + String get importPgn => 'Import PGN'; @override - String get following => 'Following'; + String get delete => 'Delete'; @override - String get unfollow => 'Unfollow'; + String get deleteThisImportedGame => 'Delete this imported game?'; @override - String followX(String param) { - return 'Follow $param'; - } + String get replayMode => 'Replay mode'; @override - String unfollowX(String param) { - return 'Unfollow $param'; - } + String get realtimeReplay => 'Realtime'; @override - String get block => 'Block'; + String get byCPL => 'By CPL'; @override - String get blocked => 'Blocked'; + String get openStudy => 'Open study'; @override - String get unblock => 'Unblock'; + String get enable => 'Enable'; @override - String get followsYou => 'Follows you'; + String get bestMoveArrow => 'Best move arrow'; @override - String xStartedFollowingY(String param1, String param2) { - return '$param1 started following $param2'; - } + String get showVariationArrows => 'Show variation arrows'; @override - String get more => 'More'; + String get evaluationGauge => 'Evaluation gauge'; @override - String get memberSince => 'Member since'; + String get multipleLines => 'Multiple lines'; @override - String lastSeenActive(String param) { - return 'Active $param'; - } + String get cpus => 'CPUs'; @override - String get player => 'Player'; + String get memory => 'Memory'; @override - String get list => 'List'; + String get infiniteAnalysis => 'Infinite analysis'; @override - String get graph => 'Graph'; + String get removesTheDepthLimit => 'Removes the depth limit, and keeps your computer warm'; @override - String get required => 'Required.'; + String get blunder => 'Blunder'; @override - String get openTournaments => 'Open tournaments'; + String get mistake => 'Mistake'; @override - String get duration => 'Duration'; + String get inaccuracy => 'Inaccuracy'; @override - String get winner => 'Winner'; + String get moveTimes => 'Move times'; @override - String get standing => 'Standing'; + String get flipBoard => 'Flip board'; @override - String get createANewTournament => 'Create a new tournament'; + String get threefoldRepetition => 'Threefold repetition'; @override - String get tournamentCalendar => 'Tournament calendar'; + String get claimADraw => 'Claim a draw'; @override - String get conditionOfEntry => 'Entry requirements:'; + String get offerDraw => 'Offer draw'; @override - String get advancedSettings => 'Advanced settings'; + String get draw => 'Draw'; @override - String get safeTournamentName => 'Pick a very safe name for the tournament.'; + String get drawByMutualAgreement => 'Draw by mutual agreement'; @override - String get inappropriateNameWarning => 'Anything even slightly inappropriate could get your account closed.'; + String get fiftyMovesWithoutProgress => 'Fifty moves without progress'; @override - String get emptyTournamentName => 'Leave empty to name the tournament after a notable chess player.'; + String get currentGames => 'Current games'; @override - String get makePrivateTournament => 'Make the tournament private, and restrict access with a password'; + String get viewInFullSize => 'View in full size'; @override - String get join => 'Join'; + String get logOut => 'Sign out'; @override - String get withdraw => 'Withdraw'; + String get signIn => 'Sign in'; @override - String get points => 'Points'; + String get rememberMe => 'Keep me logged in'; @override - String get wins => 'Wins'; + String get youNeedAnAccountToDoThat => 'You need an account to do that'; @override - String get losses => 'Losses'; + String get signUp => 'Register'; @override - String get createdBy => 'Created by'; + String get computersAreNotAllowedToPlay => 'Computers and computer-assisted players are not allowed to play. Please do not get assistance from chess engines, databases, or from other players while playing. Also note that making multiple accounts is strongly discouraged and excessive multi-accounting will lead to being banned.'; @override - String get tournamentIsStarting => 'The tournament is starting'; + String get games => 'Games'; @override - String get tournamentPairingsAreNowClosed => 'The tournament pairings are now closed.'; + String get forum => 'Forum'; @override - String standByX(String param) { - return 'Stand by $param, pairing players, get ready!'; + String xPostedInForumY(String param1, String param2) { + return '$param1 posted in topic $param2'; } @override - String get pause => 'Pause'; + String get latestForumPosts => 'Latest forum posts'; @override - String get resume => 'Resume'; + String get players => 'Players'; @override - String get youArePlaying => 'You are playing!'; + String get friends => 'Friends'; @override - String get winRate => 'Win rate'; + String get otherPlayers => 'other players'; @override - String get berserkRate => 'Berserk rate'; + String get discussions => 'Conversations'; @override - String get performance => 'Performance'; + String get today => 'Today'; @override - String get tournamentComplete => 'Tournament complete'; + String get yesterday => 'Yesterday'; @override - String get movesPlayed => 'Moves played'; + String get minutesPerSide => 'Minutes per side'; @override - String get whiteWins => 'White wins'; + String get variant => 'Variant'; @override - String get blackWins => 'Black wins'; + String get variants => 'Variants'; @override - String get drawRate => 'Draw rate'; + String get timeControl => 'Time control'; @override - String get draws => 'Draws'; + String get realTime => 'Real time'; @override - String nextXTournament(String param) { - return 'Next $param tournament:'; - } + String get correspondence => 'Correspondence'; @override - String get averageOpponent => 'Average opponent'; + String get daysPerTurn => 'Days per turn'; @override - String get boardEditor => 'Board editor'; + String get oneDay => 'One day'; @override - String get setTheBoard => 'Set the board'; + String get time => 'Time'; @override - String get popularOpenings => 'Popular openings'; + String get rating => 'Rating'; @override - String get endgamePositions => 'Endgame positions'; + String get ratingStats => 'Rating stats'; @override - String chess960StartPosition(String param) { - return 'Chess960 start position: $param'; - } + String get username => 'User name'; @override - String get startPosition => 'Starting position'; + String get usernameOrEmail => 'User name or email'; @override - String get clearBoard => 'Clear board'; + String get changeUsername => 'Change username'; @override - String get loadPosition => 'Load position'; + String get changeUsernameNotSame => 'Only the case of the letters can change. For example \"johndoe\" to \"JohnDoe\".'; @override - String get isPrivate => 'Private'; + String get changeUsernameDescription => 'Change your username. This can only be done once and you are only allowed to change the case of the letters in your username.'; @override - String reportXToModerators(String param) { - return 'Report $param to moderators'; - } + String get signupUsernameHint => 'Be sure to choose a family-friendly username. You cannot change it later, and any accounts with inappropriate usernames will be closed!'; @override - String profileCompletion(String param) { - return 'Profile completion: $param'; - } + String get signupEmailHint => 'We will only use it for password reset.'; @override - String xRating(String param) { - return '$param rating'; - } + String get password => 'Password'; @override - String get ifNoneLeaveEmpty => 'If none, leave empty'; + String get changePassword => 'Change password'; @override - String get profile => 'Profile'; + String get changeEmail => 'Change email'; @override - String get editProfile => 'Edit profile'; + String get email => 'Email'; @override - String get realName => 'Real name'; + String get passwordReset => 'Password reset'; @override - String get setFlair => 'Set your flair'; + String get forgotPassword => 'Forgot password?'; @override - String get flair => 'Flair'; + String get error_weakPassword => 'This password is extremely common and too easy to guess.'; @override - String get youCanHideFlair => 'There is a setting to hide all user flairs across the entire site.'; + String get error_namePassword => 'Please don\'t use your username as your password.'; @override - String get biography => 'Biography'; + String get blankedPassword => 'You have used the same password on another site, and that site has been compromised. To ensure the safety of your Lichess account, we need you to set a new password. Thank you for your understanding.'; @override - String get countryRegion => 'Region or country'; + String get youAreLeavingLichess => 'You are leaving Lichess'; @override - String get thankYou => 'Thank you!'; + String get neverTypeYourPassword => 'Never type your Lichess password on another site!'; @override - String get socialMediaLinks => 'Social media links'; + String proceedToX(String param) { + return 'Proceed to $param'; + } @override - String get oneUrlPerLine => 'One URL per line.'; + String get passwordSuggestion => 'Do not set a password suggested by someone else. They will use it to steal your account.'; @override - String get inlineNotation => 'Inline notation'; + String get emailSuggestion => 'Do not set an email address suggested by someone else. They will use it to steal your account.'; @override - String get makeAStudy => 'For safekeeping and sharing, consider making a study.'; + String get emailConfirmHelp => 'Help with email confirmation'; @override - String get clearSavedMoves => 'Clear moves'; + String get emailConfirmNotReceived => 'Didn\'t receive your confirmation email after signing up?'; @override - String get previouslyOnLichessTV => 'Previously on Lichess TV'; + String get whatSignupUsername => 'What username did you use to sign up?'; @override - String get onlinePlayers => 'Online players'; + String usernameNotFound(String param) { + return 'We couldn\'t find any user by this name: $param.'; + } @override - String get activePlayers => 'Active players'; + String get usernameCanBeUsedForNewAccount => 'You can use this username to create a new account'; @override - String get bewareTheGameIsRatedButHasNoClock => 'Beware, the game is rated but has no clock!'; + String emailSent(String param) { + return 'We have sent an email to $param.'; + } @override - String get success => 'Success'; + String get emailCanTakeSomeTime => 'It can take some time to arrive.'; @override - String get automaticallyProceedToNextGameAfterMoving => 'Automatically proceed to next game after moving'; + String get refreshInboxAfterFiveMinutes => 'Wait 5 minutes and refresh your email inbox.'; @override - String get autoSwitch => 'Auto switch'; + String get checkSpamFolder => 'Also check your spam folder, it might end up there. If so, mark it as not spam.'; @override - String get puzzles => 'Puzzles'; + String get emailForSignupHelp => 'If you still have questions, please send us an email:'; @override - String get onlineBots => 'Online bots'; + String copyTextToEmail(String param) { + return 'Copy and paste the above text and send it to $param'; + } @override - String get name => 'Name'; + String get waitForSignupHelp => 'We will come back to you shortly to help you complete your signup.'; @override - String get description => 'Description'; + String accountConfirmed(String param) { + return 'The user $param is successfully confirmed.'; + } @override - String get descPrivate => 'Private description'; + String accountCanLogin(String param) { + return 'You can login right now as $param.'; + } @override - String get descPrivateHelp => 'Text that only the team members will see. If set, replaces the public description for team members.'; + String get accountConfirmationEmailNotNeeded => 'You do not need a confirmation email.'; @override - String get no => 'No'; + String accountClosed(String param) { + return 'The account $param is closed.'; + } @override - String get yes => 'Yes'; + String accountRegisteredWithoutEmail(String param) { + return 'The account $param was registered without an email.'; + } @override - String get website => 'Website'; + String get rank => 'Rank'; @override - String get mobile => 'Mobile'; + String rankX(String param) { + return 'Rank: $param'; + } @override - String get help => 'Help:'; + String get gamesPlayed => 'Games played'; @override - String get createANewTopic => 'Create a new topic'; + String get cancel => 'Cancel'; @override - String get topics => 'Topics'; + String get whiteTimeOut => 'White time out'; @override - String get posts => 'Posts'; + String get blackTimeOut => 'Black time out'; @override - String get lastPost => 'Last post'; + String get drawOfferSent => 'Draw offer sent'; @override - String get views => 'Views'; + String get drawOfferAccepted => 'Draw offer accepted'; @override - String get replies => 'Replies'; + String get drawOfferCanceled => 'Draw offer canceled'; @override - String get replyToThisTopic => 'Reply to this topic'; + String get whiteOffersDraw => 'White offers draw'; @override - String get reply => 'Reply'; + String get blackOffersDraw => 'Black offers draw'; @override - String get message => 'Message'; + String get whiteDeclinesDraw => 'White declines draw'; @override - String get createTheTopic => 'Create the topic'; + String get blackDeclinesDraw => 'Black declines draw'; @override - String get reportAUser => 'Report a user'; + String get yourOpponentOffersADraw => 'Your opponent offers a draw'; @override - String get user => 'User'; + String get accept => 'Accept'; @override - String get reason => 'Reason'; + String get decline => 'Decline'; @override - String get whatIsIheMatter => 'What\'s the matter?'; + String get playingRightNow => 'Playing right now'; @override - String get cheat => 'Cheat'; + String get eventInProgress => 'Playing now'; @override - String get troll => 'Troll'; + String get finished => 'Finished'; @override - String get other => 'Other'; + String get abortGame => 'Abort game'; @override - String get reportDescriptionHelp => 'Paste the link to the game(s) and explain what is wrong about this user behavior. Don\'t just say \"they cheat\", but tell us how you came to this conclusion. Your report will be processed faster if written in English.'; + String get gameAborted => 'Game aborted'; @override - String get error_provideOneCheatedGameLink => 'Please provide at least one link to a cheated game.'; + String get standard => 'Standard'; @override - String by(String param) { - return 'by $param'; - } + String get customPosition => 'Custom position'; @override - String importedByX(String param) { - return 'Imported by $param'; - } + String get unlimited => 'Unlimited'; @override - String get thisTopicIsNowClosed => 'This topic is now closed.'; + String get mode => 'Mode'; @override - String get blog => 'Blog'; + String get casual => 'Casual'; @override - String get notes => 'Notes'; + String get rated => 'Rated'; @override - String get typePrivateNotesHere => 'Type private notes here'; + String get casualTournament => 'Casual'; @override - String get writeAPrivateNoteAboutThisUser => 'Write a private note about this user'; + String get ratedTournament => 'Rated'; @override - String get noNoteYet => 'No note yet'; + String get thisGameIsRated => 'This game is rated'; @override - String get invalidUsernameOrPassword => 'Invalid username or password'; + String get rematch => 'Rematch'; @override - String get incorrectPassword => 'Incorrect password'; + String get rematchOfferSent => 'Rematch offer sent'; @override - String get invalidAuthenticationCode => 'Invalid authentication code'; + String get rematchOfferAccepted => 'Rematch offer accepted'; @override - String get emailMeALink => 'Email me a link'; + String get rematchOfferCanceled => 'Rematch offer canceled'; @override - String get currentPassword => 'Current password'; + String get rematchOfferDeclined => 'Rematch offer declined'; @override - String get newPassword => 'New password'; + String get cancelRematchOffer => 'Cancel rematch offer'; @override - String get newPasswordAgain => 'New password (again)'; + String get viewRematch => 'View rematch'; @override - String get newPasswordsDontMatch => 'The new passwords don\'t match'; + String get confirmMove => 'Confirm move'; @override - String get newPasswordStrength => 'Password strength'; + String get play => 'Play'; @override - String get clockInitialTime => 'Clock initial time'; + String get inbox => 'Inbox'; @override - String get clockIncrement => 'Clock increment'; + String get chatRoom => 'Chat room'; @override - String get privacy => 'Privacy'; + String get loginToChat => 'Sign in to chat'; @override - String get privacyPolicy => 'Privacy policy'; + String get youHaveBeenTimedOut => 'You have been timed out.'; @override - String get letOtherPlayersFollowYou => 'Let other players follow you'; + String get spectatorRoom => 'Spectator room'; @override - String get letOtherPlayersChallengeYou => 'Let other players challenge you'; + String get composeMessage => 'Compose message'; @override - String get letOtherPlayersInviteYouToStudy => 'Let other players invite you to study'; + String get subject => 'Subject'; @override - String get sound => 'Sound'; + String get send => 'Send'; @override - String get none => 'None'; + String get incrementInSeconds => 'Increment in seconds'; @override - String get fast => 'Fast'; + String get freeOnlineChess => 'Free Online Chess'; @override - String get normal => 'Normal'; + String get exportGames => 'Export games'; @override - String get slow => 'Slow'; + String get ratingRange => 'Rating range'; @override - String get insideTheBoard => 'Inside the board'; + String get thisAccountViolatedTos => 'This account violated the Lichess Terms of Service'; @override - String get outsideTheBoard => 'Outside the board'; + String get openingExplorerAndTablebase => 'Opening explorer & tablebase'; @override - String get allSquaresOfTheBoard => 'All squares on the board'; + String get takeback => 'Takeback'; @override - String get onSlowGames => 'On slow games'; + String get proposeATakeback => 'Propose a takeback'; @override - String get always => 'Always'; + String get takebackPropositionSent => 'Takeback sent'; @override - String get never => 'Never'; + String get takebackPropositionDeclined => 'Takeback declined'; @override - String xCompetesInY(String param1, String param2) { - return '$param1 competes in $param2'; - } + String get takebackPropositionAccepted => 'Takeback accepted'; @override - String get victory => 'Victory'; + String get takebackPropositionCanceled => 'Takeback canceled'; @override - String get defeat => 'Defeat'; + String get yourOpponentProposesATakeback => 'Your opponent proposes a takeback'; @override - String victoryVsYInZ(String param1, String param2, String param3) { - return '$param1 vs $param2 in $param3'; - } + String get bookmarkThisGame => 'Bookmark this game'; @override - String defeatVsYInZ(String param1, String param2, String param3) { - return '$param1 vs $param2 in $param3'; - } + String get tournament => 'Tournament'; @override - String drawVsYInZ(String param1, String param2, String param3) { - return '$param1 vs $param2 in $param3'; - } + String get tournaments => 'Tournaments'; @override - String get timeline => 'Timeline'; + String get tournamentPoints => 'Tournament points'; @override - String get starting => 'Starting:'; + String get viewTournament => 'View tournament'; @override - String get allInformationIsPublicAndOptional => 'All information is public and optional.'; + String get backToTournament => 'Back to tournament'; @override - String get biographyDescription => 'Talk about yourself, your interests, what you like in chess, your favorite openings, players, ...'; + String get noDrawBeforeSwissLimit => 'You cannot draw before 30 moves are played in a Swiss tournament.'; @override - String get listBlockedPlayers => 'List players you have blocked'; + String get thematic => 'Thematic'; @override - String get human => 'Human'; + String yourPerfRatingIsProvisional(String param) { + return 'Your $param rating is provisional'; + } @override - String get computer => 'Computer'; + String yourPerfRatingIsTooHigh(String param1, String param2) { + return 'Your $param1 rating ($param2) is too high'; + } @override - String get side => 'Side'; + String yourTopWeeklyPerfRatingIsTooHigh(String param1, String param2) { + return 'Your top weekly $param1 rating ($param2) is too high'; + } @override - String get clock => 'Clock'; + String yourPerfRatingIsTooLow(String param1, String param2) { + return 'Your $param1 rating ($param2) is too low'; + } @override - String get opponent => 'Opponent'; + String ratedMoreThanInPerf(String param1, String param2) { + return 'Rated ≥ $param1 in $param2'; + } @override - String get learnMenu => 'Learn'; + String ratedLessThanInPerf(String param1, String param2) { + return 'Rated ≤ $param1 in $param2 for the last week'; + } @override - String get studyMenu => 'Study'; + String mustBeInTeam(String param) { + return 'Must be in team $param'; + } @override - String get practice => 'Practice'; + String youAreNotInTeam(String param) { + return 'You are not in the team $param'; + } @override - String get community => 'Community'; + String get backToGame => 'Back to game'; @override - String get tools => 'Tools'; + String get siteDescription => 'Free online chess server. Play chess in a clean interface. No registration, no ads, no plugin required. Play chess with the computer, friends or random opponents.'; @override - String get increment => 'Increment'; + String xJoinedTeamY(String param1, String param2) { + return '$param1 joined team $param2'; + } @override - String get error_unknown => 'Invalid value'; + String xCreatedTeamY(String param1, String param2) { + return '$param1 created team $param2'; + } @override - String get error_required => 'This field is required'; + String get startedStreaming => 'started streaming'; @override - String get error_email => 'This email address is invalid'; + String xStartedStreaming(String param) { + return '$param started streaming'; + } @override - String get error_email_acceptable => 'This email address is not acceptable. Please double-check it, and try again.'; + String get averageElo => 'Average rating'; @override - String get error_email_unique => 'Email address invalid or already taken'; + String get location => 'Location'; @override - String get error_email_different => 'This is already your email address'; + String get filterGames => 'Filter games'; @override - String error_minLength(String param) { - return 'Must be at least $param characters long'; - } + String get reset => 'Reset'; @override - String error_maxLength(String param) { - return 'Maximum length is $param'; - } + String get apply => 'Submit'; @override - String error_min(String param) { - return 'Must be at least $param'; - } + String get save => 'Save'; @override - String error_max(String param) { - return 'Must be at most $param'; - } + String get leaderboard => 'Leaderboard'; @override - String ifRatingIsPlusMinusX(String param) { - return 'If rating is ± $param'; - } + String get screenshotCurrentPosition => 'Screenshot current position'; @override - String get ifRegistered => 'If registered'; + String get gameAsGIF => 'Game as GIF'; @override - String get onlyExistingConversations => 'Only existing conversations'; + String get pasteTheFenStringHere => 'Paste the FEN text here'; @override - String get onlyFriends => 'Only friends'; + String get pasteThePgnStringHere => 'Paste the PGN text here'; @override - String get menu => 'Menu'; + String get orUploadPgnFile => 'Or upload a PGN file'; @override - String get castling => 'Castling'; + String get fromPosition => 'From position'; @override - String get whiteCastlingKingside => 'White O-O'; + String get continueFromHere => 'Continue from here'; @override - String get blackCastlingKingside => 'Black O-O'; + String get toStudy => 'Study'; @override - String tpTimeSpentPlaying(String param) { - return 'Time spent playing: $param'; - } + String get importGame => 'Import game'; @override - String get watchGames => 'Watch games'; + String get importGameExplanation => 'Paste a game PGN to get a browsable replay, computer analysis, game chat and public shareable URL.'; @override - String tpTimeSpentOnTV(String param) { - return 'Time on TV: $param'; - } + String get importGameCaveat => 'Variations will be erased. To keep them, import the PGN via a study.'; @override - String get watch => 'Watch'; + String get importGameDataPrivacyWarning => 'This PGN can be accessed by the public. To import a game privately, use a study.'; @override - String get videoLibrary => 'Video library'; + String get thisIsAChessCaptcha => 'This is a chess CAPTCHA.'; @override - String get streamersMenu => 'Streamers'; + String get clickOnTheBoardToMakeYourMove => 'Click on the board to make your move, and prove you are human.'; @override - String get mobileApp => 'Mobile App'; + String get captcha_fail => 'Please solve the chess captcha.'; @override - String get webmasters => 'Webmasters'; + String get notACheckmate => 'Not a checkmate'; @override - String get about => 'About'; + String get whiteCheckmatesInOneMove => 'White to checkmate in one move'; @override - String aboutX(String param) { - return 'About $param'; - } + String get blackCheckmatesInOneMove => 'Black to checkmate in one move'; @override - String xIsAFreeYLibreOpenSourceChessServer(String param1, String param2) { - return '$param1 is a free ($param2), libre, no-ads, open source chess server.'; - } + String get retry => 'Retry'; @override - String get really => 'really'; + String get reconnecting => 'Reconnecting'; @override - String get contribute => 'Contribute'; + String get noNetwork => 'Offline'; @override - String get termsOfService => 'Terms of Service'; + String get favoriteOpponents => 'Favorite opponents'; @override - String get sourceCode => 'Source Code'; + String get follow => 'Follow'; @override - String get simultaneousExhibitions => 'Simultaneous exhibitions'; + String get following => 'Following'; @override - String get host => 'Host'; + String get unfollow => 'Unfollow'; @override - String hostColorX(String param) { - return 'Host color: $param'; + String followX(String param) { + return 'Follow $param'; } @override - String get yourPendingSimuls => 'Your pending simuls'; + String unfollowX(String param) { + return 'Unfollow $param'; + } @override - String get createdSimuls => 'Newly created simuls'; + String get block => 'Block'; @override - String get hostANewSimul => 'Host a new simul'; + String get blocked => 'Blocked'; @override - String get signUpToHostOrJoinASimul => 'Sign up to join or host a simul'; + String get unblock => 'Unblock'; @override - String get noSimulFound => 'Simul not found'; + String get followsYou => 'Follows you'; @override - String get noSimulExplanation => 'This simultaneous exhibition does not exist.'; + String xStartedFollowingY(String param1, String param2) { + return '$param1 started following $param2'; + } @override - String get returnToSimulHomepage => 'Return to simul homepage'; + String get more => 'More'; @override - String get aboutSimul => 'Simuls involve a single player facing several players at once.'; + String get memberSince => 'Member since'; @override - String get aboutSimulImage => 'Out of 50 opponents, Fischer won 47 games, drew 2 and lost 1.'; + String lastSeenActive(String param) { + return 'Active $param'; + } @override - String get aboutSimulRealLife => 'The concept is taken from real world events. In real life, this involves the simul host moving from table to table to play a single move.'; + String get player => 'Player'; @override - String get aboutSimulRules => 'When the simul starts, every player starts a game with the host, who gets to play the white pieces. The simul ends when all games are complete.'; + String get list => 'List'; @override - String get aboutSimulSettings => 'Simuls are always casual. Rematches, takebacks and adding time are disabled.'; + String get graph => 'Graph'; @override - String get create => 'Create'; + String get required => 'Required.'; @override - String get whenCreateSimul => 'When you create a Simul, you get to play several players at once.'; + String get openTournaments => 'Open tournaments'; @override - String get simulVariantsHint => 'If you select several variants, each player gets to choose which one to play.'; + String get duration => 'Duration'; @override - String get simulClockHint => 'Fischer Clock setup. The more players you take on, the more time you may need.'; + String get winner => 'Winner'; @override - String get simulAddExtraTime => 'You may add extra time to your clock to help cope with the simul.'; + String get standing => 'Standing'; @override - String get simulHostExtraTime => 'Host extra clock time'; + String get createANewTournament => 'Create a new tournament'; @override - String get simulAddExtraTimePerPlayer => 'Add initial time to your clock for each player joining the simul.'; + String get tournamentCalendar => 'Tournament calendar'; @override - String get simulHostExtraTimePerPlayer => 'Host extra clock time per player'; + String get conditionOfEntry => 'Entry requirements:'; @override - String get lichessTournaments => 'Lichess tournaments'; + String get advancedSettings => 'Advanced settings'; @override - String get tournamentFAQ => 'Arena tournament FAQ'; + String get safeTournamentName => 'Pick a very safe name for the tournament.'; @override - String get timeBeforeTournamentStarts => 'Time before tournament starts'; + String get inappropriateNameWarning => 'Anything even slightly inappropriate could get your account closed.'; @override - String get averageCentipawnLoss => 'Average centipawn loss'; + String get emptyTournamentName => 'Leave empty to name the tournament after a notable chess player.'; @override - String get accuracy => 'Accuracy'; + String get makePrivateTournament => 'Make the tournament private, and restrict access with a password'; @override - String get keyboardShortcuts => 'Keyboard shortcuts'; + String get join => 'Join'; @override - String get keyMoveBackwardOrForward => 'move backward/forward'; + String get withdraw => 'Withdraw'; @override - String get keyGoToStartOrEnd => 'go to start/end'; + String get points => 'Points'; @override - String get keyCycleSelectedVariation => 'Cycle selected variation'; + String get wins => 'Wins'; @override - String get keyShowOrHideComments => 'show/hide comments'; + String get losses => 'Losses'; @override - String get keyEnterOrExitVariation => 'enter/exit variation'; + String get createdBy => 'Created by'; @override - String get keyRequestComputerAnalysis => 'Request computer analysis, learn from your mistakes'; + String get tournamentIsStarting => 'The tournament is starting'; @override - String get keyNextLearnFromYourMistakes => 'Next (learn from your mistakes)'; + String get tournamentPairingsAreNowClosed => 'The tournament pairings are now closed.'; @override - String get keyNextBlunder => 'Next blunder'; + String standByX(String param) { + return 'Stand by $param, pairing players, get ready!'; + } @override - String get keyNextMistake => 'Next mistake'; + String get pause => 'Pause'; @override - String get keyNextInaccuracy => 'Next inaccuracy'; + String get resume => 'Resume'; @override - String get keyPreviousBranch => 'Previous branch'; + String get youArePlaying => 'You are playing!'; @override - String get keyNextBranch => 'Next branch'; + String get winRate => 'Win rate'; @override - String get toggleVariationArrows => 'Toggle variation arrows'; + String get berserkRate => 'Berserk rate'; @override - String get cyclePreviousOrNextVariation => 'Cycle previous/next variation'; + String get performance => 'Performance'; @override - String get toggleGlyphAnnotations => 'Toggle move annotations'; + String get tournamentComplete => 'Tournament complete'; @override - String get togglePositionAnnotations => 'Toggle position annotations'; + String get movesPlayed => 'Moves played'; @override - String get variationArrowsInfo => 'Variation arrows let you navigate without using the move list.'; + String get whiteWins => 'White wins'; @override - String get playSelectedMove => 'play selected move'; + String get blackWins => 'Black wins'; @override - String get newTournament => 'New tournament'; + String get drawRate => 'Draw rate'; @override - String get tournamentHomeTitle => 'Chess tournaments featuring various time controls and variants'; + String get draws => 'Draws'; @override - String get tournamentHomeDescription => 'Play fast-paced chess tournaments! Join an official scheduled tournament, or create your own. Bullet, Blitz, Classical, Chess960, King of the Hill, Threecheck, and more options available for endless chess fun.'; + String nextXTournament(String param) { + return 'Next $param tournament:'; + } @override - String get tournamentNotFound => 'Tournament not found'; + String get averageOpponent => 'Average opponent'; @override - String get tournamentDoesNotExist => 'This tournament does not exist.'; + String get boardEditor => 'Board editor'; @override - String get tournamentMayHaveBeenCanceled => 'The tournament may have been canceled if all players left before it started.'; + String get setTheBoard => 'Set the board'; @override - String get returnToTournamentsHomepage => 'Return to tournaments homepage'; + String get popularOpenings => 'Popular openings'; @override - String weeklyPerfTypeRatingDistribution(String param) { - return 'Weekly $param rating distribution'; - } + String get endgamePositions => 'Endgame positions'; @override - String yourPerfTypeRatingIsRating(String param1, String param2) { - return 'Your $param1 rating is $param2.'; + String chess960StartPosition(String param) { + return 'Chess960 start position: $param'; } @override - String youAreBetterThanPercentOfPerfTypePlayers(String param1, String param2) { - return 'You are better than $param1 of $param2 players.'; - } + String get startPosition => 'Starting position'; @override - String userIsBetterThanPercentOfPerfTypePlayers(String param1, String param2, String param3) { - return '$param1 is better than $param2 of $param3 players.'; - } + String get clearBoard => 'Clear board'; @override - String betterThanPercentPlayers(String param1, String param2) { - return 'Better than $param1 of $param2 players'; - } + String get loadPosition => 'Load position'; @override - String youDoNotHaveAnEstablishedPerfTypeRating(String param) { - return 'You do not have an established $param rating.'; - } + String get isPrivate => 'Private'; @override - String get yourRating => 'Your rating'; + String reportXToModerators(String param) { + return 'Report $param to moderators'; + } @override - String get cumulative => 'Cumulative'; + String profileCompletion(String param) { + return 'Profile completion: $param'; + } @override - String get glicko2Rating => 'Glicko-2 rating'; + String xRating(String param) { + return '$param rating'; + } @override - String get checkYourEmail => 'Check your Email'; + String get ifNoneLeaveEmpty => 'If none, leave empty'; @override - String get weHaveSentYouAnEmailClickTheLink => 'We\'ve sent you an email. Click the link in the email to activate your account.'; + String get profile => 'Profile'; @override - String get ifYouDoNotSeeTheEmailCheckOtherPlaces => 'If you don\'t see the email, check other places it might be, like your junk, spam, social, or other folders.'; + String get editProfile => 'Edit profile'; @override - String weHaveSentYouAnEmailTo(String param) { - return 'We\'ve sent an email to $param. Click the link in the email to reset your password.'; - } + String get realName => 'Real name'; @override - String byRegisteringYouAgreeToBeBoundByOur(String param) { - return 'By registering, you agree to be bound by our $param.'; - } + String get setFlair => 'Set your flair'; @override - String readAboutOur(String param) { - return 'Read about our $param.'; - } + String get flair => 'Flair'; @override - String get networkLagBetweenYouAndLichess => 'Network lag between you and lichess'; + String get youCanHideFlair => 'There is a setting to hide all user flairs across the entire site.'; @override - String get timeToProcessAMoveOnLichessServer => 'Time to process a move on lichess server'; + String get biography => 'Biography'; @override - String get downloadAnnotated => 'Download annotated'; + String get countryRegion => 'Country or region'; @override - String get downloadRaw => 'Download raw'; + String get thankYou => 'Thank you!'; @override - String get downloadImported => 'Download imported'; + String get socialMediaLinks => 'Social media links'; @override - String get crosstable => 'Crosstable'; + String get oneUrlPerLine => 'One URL per line.'; @override - String get youCanAlsoScrollOverTheBoardToMoveInTheGame => 'You can also scroll over the board to move in the game.'; + String get inlineNotation => 'Inline notation'; @override - String get scrollOverComputerVariationsToPreviewThem => 'Scroll over computer variations to preview them.'; + String get makeAStudy => 'For safekeeping and sharing, consider making a study.'; @override - String get analysisShapesHowTo => 'Press shift+click or right-click to draw circles and arrows on the board.'; + String get clearSavedMoves => 'Clear moves'; @override - String get letOtherPlayersMessageYou => 'Let other players message you'; + String get previouslyOnLichessTV => 'Previously on Lichess TV'; @override - String get receiveForumNotifications => 'Receive notifications when mentioned in the forum'; + String get onlinePlayers => 'Online players'; @override - String get shareYourInsightsData => 'Share your chess insights data'; + String get activePlayers => 'Active players'; @override - String get withNobody => 'With nobody'; + String get bewareTheGameIsRatedButHasNoClock => 'Beware, the game is rated but has no clock!'; @override - String get withFriends => 'With friends'; + String get success => 'Success'; @override - String get withEverybody => 'With everybody'; + String get automaticallyProceedToNextGameAfterMoving => 'Automatically proceed to next game after moving'; @override - String get kidMode => 'Kid mode'; + String get autoSwitch => 'Auto switch'; @override - String get kidModeIsEnabled => 'Child-mode is enabled.'; + String get puzzles => 'Puzzles'; @override - String get kidModeExplanation => 'This is about safety. In kid mode, all site communications are disabled. Enable this for your children and school students, to protect them from other internet users.'; + String get onlineBots => 'Online bots'; @override - String inKidModeTheLichessLogoGetsIconX(String param) { - return 'In kid mode, the lichess logo gets a $param icon, so you know your kids are safe.'; - } + String get name => 'Name'; @override - String get askYourChessTeacherAboutLiftingKidMode => 'Your account is managed. Ask your chess teacher about lifting kid mode.'; + String get description => 'Description'; @override - String get enableKidMode => 'Enable Kid mode'; + String get descPrivate => 'Private description'; @override - String get disableKidMode => 'Disable Kid mode'; + String get descPrivateHelp => 'Text that only the team members will see. If set, replaces the public description for team members.'; @override - String get security => 'Security'; + String get no => 'No'; @override - String get sessions => 'Sessions'; + String get yes => 'Yes'; @override - String get revokeAllSessions => 'revoke all sessions'; + String get website => 'Website'; @override - String get playChessEverywhere => 'Play chess everywhere'; + String get mobile => 'Mobile'; @override - String get asFreeAsLichess => 'As free as lichess'; + String get help => 'Help:'; @override - String get builtForTheLoveOfChessNotMoney => 'Built for the love of chess, not money'; + String get createANewTopic => 'Create a new topic'; @override - String get everybodyGetsAllFeaturesForFree => 'Everybody gets all features for free'; + String get topics => 'Topics'; @override - String get zeroAdvertisement => 'Zero advertisement'; + String get posts => 'Posts'; @override - String get fullFeatured => 'Full featured'; + String get lastPost => 'Last post'; @override - String get phoneAndTablet => 'Phone and tablet'; + String get views => 'Views'; @override - String get bulletBlitzClassical => 'Bullet, blitz, classical'; + String get replies => 'Replies'; @override - String get correspondenceChess => 'Correspondence chess'; + String get replyToThisTopic => 'Reply to this topic'; @override - String get onlineAndOfflinePlay => 'Online and offline play'; + String get reply => 'Reply'; @override - String get viewTheSolution => 'View the solution'; + String get message => 'Message'; @override - String get followAndChallengeFriends => 'Follow and challenge friends'; + String get createTheTopic => 'Create the topic'; @override - String get gameAnalysis => 'Game analysis'; + String get reportAUser => 'Report a user'; @override - String xHostsY(String param1, String param2) { - return '$param1 hosts $param2'; - } + String get user => 'User'; @override - String xJoinsY(String param1, String param2) { - return '$param1 joins $param2'; - } + String get reason => 'Reason'; @override - String xLikesY(String param1, String param2) { - return '$param1 likes $param2'; - } + String get whatIsIheMatter => 'What\'s the matter?'; @override - String get quickPairing => 'Quick pairing'; + String get cheat => 'Cheat'; @override - String get lobby => 'Lobby'; + String get troll => 'Troll'; @override - String get anonymous => 'Anonymous'; + String get other => 'Other'; @override - String yourScore(String param) { - return 'Your score: $param'; - } + String get reportCheatBoostHelp => 'Paste a link to the game(s) and explain what is wrong with this user\'s behavior. Don\'t just say \"they cheat,\" but tell us how you came to this conclusion.'; @override - String get language => 'Language'; + String get reportUsernameHelp => 'Explain why this username is offensive. Don\'t just say \"it\'s offensive/inappropriate,\" but tell us how you came to this conclusion, especially if the offense is obscure, not in English, in slang, or a historical/cultural reference.'; @override - String get background => 'Background'; + String get reportProcessedFasterInEnglish => 'Your report will be processed faster if written in English.'; @override - String get light => 'Light'; + String get error_provideOneCheatedGameLink => 'Please provide at least one link to a cheated game.'; @override - String get dark => 'Dark'; + String by(String param) { + return 'by $param'; + } @override - String get transparent => 'Transparent'; + String importedByX(String param) { + return 'Imported by $param'; + } @override - String get deviceTheme => 'Device theme'; + String get thisTopicIsNowClosed => 'This topic is now closed.'; @override - String get backgroundImageUrl => 'Background image URL:'; + String get blog => 'Blog'; @override - String get board => 'Board'; + String get notes => 'Notes'; @override - String get size => 'Size'; + String get typePrivateNotesHere => 'Type private notes here'; @override - String get opacity => 'Opacity'; + String get writeAPrivateNoteAboutThisUser => 'Write a private note about this user'; @override - String get brightness => 'Brightness'; + String get noNoteYet => 'No note yet'; @override - String get hue => 'Hue'; + String get invalidUsernameOrPassword => 'Invalid username or password'; @override - String get boardReset => 'Reset colors to default'; + String get incorrectPassword => 'Incorrect password'; @override - String get pieceSet => 'Piece set'; + String get invalidAuthenticationCode => 'Invalid authentication code'; @override - String get embedInYourWebsite => 'Embed in your website'; + String get emailMeALink => 'Email me a link'; @override - String get usernameAlreadyUsed => 'This username is already in use, please try another one.'; + String get currentPassword => 'Current password'; @override - String get usernamePrefixInvalid => 'The username must start with a letter.'; + String get newPassword => 'New password'; @override - String get usernameSuffixInvalid => 'The username must end with a letter or a number.'; + String get newPasswordAgain => 'New password (again)'; @override - String get usernameCharsInvalid => 'The username must only contain letters, numbers, underscores, and hyphens. Consecutive underscores and hyphens are not allowed.'; + String get newPasswordsDontMatch => 'The new passwords don\'t match'; @override - String get usernameUnacceptable => 'This username is not acceptable.'; + String get newPasswordStrength => 'Password strength'; @override - String get playChessInStyle => 'Play chess in style'; + String get clockInitialTime => 'Clock initial time'; @override - String get chessBasics => 'Chess basics'; + String get clockIncrement => 'Clock increment'; @override - String get coaches => 'Coaches'; + String get privacy => 'Privacy'; @override - String get invalidPgn => 'Invalid PGN'; + String get privacyPolicy => 'Privacy policy'; @override - String get invalidFen => 'Invalid FEN'; + String get letOtherPlayersFollowYou => 'Let other players follow you'; @override - String get custom => 'Custom'; + String get letOtherPlayersChallengeYou => 'Let other players challenge you'; @override - String get notifications => 'Notifications'; + String get letOtherPlayersInviteYouToStudy => 'Let other players invite you to study'; @override - String notificationsX(String param1) { - return 'Notifications: $param1'; - } + String get sound => 'Sound'; @override - String perfRatingX(String param) { - return 'Rating: $param'; - } + String get none => 'None'; @override - String get practiceWithComputer => 'Practice with computer'; + String get fast => 'Fast'; @override - String anotherWasX(String param) { - return 'Another was $param'; - } + String get normal => 'Normal'; @override - String bestWasX(String param) { - return 'Best was $param'; - } + String get slow => 'Slow'; @override - String get youBrowsedAway => 'You browsed away'; + String get insideTheBoard => 'Inside the board'; @override - String get resumePractice => 'Resume practice'; + String get outsideTheBoard => 'Outside the board'; @override - String get drawByFiftyMoves => 'The game has been drawn by the fifty-move rule.'; + String get allSquaresOfTheBoard => 'All squares on the board'; @override - String get theGameIsADraw => 'The game is a draw.'; + String get onSlowGames => 'On slow games'; @override - String get computerThinking => 'Computer thinking ...'; + String get always => 'Always'; @override - String get seeBestMove => 'See best move'; + String get never => 'Never'; @override - String get hideBestMove => 'Hide the best move'; + String xCompetesInY(String param1, String param2) { + return '$param1 competes in $param2'; + } @override - String get getAHint => 'Get a hint'; + String get victory => 'Victory'; @override - String get evaluatingYourMove => 'Evaluating your move ...'; + String get defeat => 'Defeat'; @override - String get whiteWinsGame => 'White wins'; + String victoryVsYInZ(String param1, String param2, String param3) { + return '$param1 vs $param2 in $param3'; + } @override - String get blackWinsGame => 'Black wins'; + String defeatVsYInZ(String param1, String param2, String param3) { + return '$param1 vs $param2 in $param3'; + } @override - String get learnFromYourMistakes => 'Learn from your mistakes'; + String drawVsYInZ(String param1, String param2, String param3) { + return '$param1 vs $param2 in $param3'; + } @override - String get learnFromThisMistake => 'Learn from this mistake'; + String get timeline => 'Timeline'; @override - String get skipThisMove => 'Skip this move'; + String get starting => 'Starting:'; @override - String get next => 'Next'; + String get allInformationIsPublicAndOptional => 'All information is public and optional.'; @override - String xWasPlayed(String param) { - return '$param was played'; - } + String get biographyDescription => 'Talk about yourself, your interests, what you like in chess, your favorite openings, players, ...'; @override - String get findBetterMoveForWhite => 'Find a better move for white'; + String get listBlockedPlayers => 'List players you have blocked'; @override - String get findBetterMoveForBlack => 'Find a better move for black'; + String get human => 'Human'; @override - String get resumeLearning => 'Resume learning'; + String get computer => 'Computer'; @override - String get youCanDoBetter => 'You can do better'; + String get side => 'Side'; @override - String get tryAnotherMoveForWhite => 'Try another move for white'; + String get clock => 'Clock'; @override - String get tryAnotherMoveForBlack => 'Try another move for black'; + String get opponent => 'Opponent'; @override - String get solution => 'Solution'; + String get learnMenu => 'Learn'; @override - String get waitingForAnalysis => 'Waiting for analysis'; + String get studyMenu => 'Study'; + + @override + String get practice => 'Practice'; + + @override + String get community => 'Community'; + + @override + String get tools => 'Tools'; + + @override + String get increment => 'Increment'; + + @override + String get error_unknown => 'Invalid value'; + + @override + String get error_required => 'This field is required'; + + @override + String get error_email => 'This email address is invalid'; + + @override + String get error_email_acceptable => 'This email address is not acceptable. Please double-check it, and try again.'; + + @override + String get error_email_unique => 'Email address invalid or already taken'; + + @override + String get error_email_different => 'This is already your email address'; + + @override + String error_minLength(String param) { + return 'Must be at least $param characters long'; + } + + @override + String error_maxLength(String param) { + return 'Must be at most $param characters long'; + } + + @override + String error_min(String param) { + return 'Must be at least $param'; + } + + @override + String error_max(String param) { + return 'Must be at most $param'; + } + + @override + String ifRatingIsPlusMinusX(String param) { + return 'If rating is ± $param'; + } + + @override + String get ifRegistered => 'If registered'; + + @override + String get onlyExistingConversations => 'Only existing conversations'; + + @override + String get onlyFriends => 'Only friends'; + + @override + String get menu => 'Menu'; + + @override + String get castling => 'Castling'; + + @override + String get whiteCastlingKingside => 'White O-O'; + + @override + String get blackCastlingKingside => 'Black O-O'; + + @override + String tpTimeSpentPlaying(String param) { + return 'Time spent playing: $param'; + } + + @override + String get watchGames => 'Watch games'; + + @override + String tpTimeSpentOnTV(String param) { + return 'Time featured on TV: $param'; + } + + @override + String get watch => 'Watch'; + + @override + String get videoLibrary => 'Video library'; + + @override + String get streamersMenu => 'Streamers'; + + @override + String get mobileApp => 'Mobile App'; + + @override + String get webmasters => 'Webmasters'; + + @override + String get about => 'About'; + + @override + String aboutX(String param) { + return 'About $param'; + } + + @override + String xIsAFreeYLibreOpenSourceChessServer(String param1, String param2) { + return '$param1 is a free ($param2), libre, no-ads, open source chess server.'; + } + + @override + String get really => 'really'; + + @override + String get contribute => 'Contribute'; + + @override + String get termsOfService => 'Terms of Service'; + + @override + String get sourceCode => 'Source Code'; + + @override + String get simultaneousExhibitions => 'Simultaneous exhibitions'; + + @override + String get host => 'Host'; + + @override + String hostColorX(String param) { + return 'Host color: $param'; + } + + @override + String get yourPendingSimuls => 'Your pending simuls'; + + @override + String get createdSimuls => 'Newly created simuls'; + + @override + String get hostANewSimul => 'Host a new simul'; + + @override + String get signUpToHostOrJoinASimul => 'Sign up to join or host a simul'; + + @override + String get noSimulFound => 'Simul not found'; + + @override + String get noSimulExplanation => 'This simultaneous exhibition does not exist.'; + + @override + String get returnToSimulHomepage => 'Return to simul homepage'; + + @override + String get aboutSimul => 'Simuls involve a single player facing several players at once.'; + + @override + String get aboutSimulImage => 'Out of 50 opponents, Fischer won 47 games, drew 2 and lost 1.'; + + @override + String get aboutSimulRealLife => 'The concept is taken from real world events. In real life, this involves the simul host moving from table to table to play a single move.'; + + @override + String get aboutSimulRules => 'When the simul starts, every player starts a game with the host. The simul ends when all games are complete.'; + + @override + String get aboutSimulSettings => 'Simuls are always casual. Rematches, takebacks and adding time are disabled.'; + + @override + String get create => 'Create'; + + @override + String get whenCreateSimul => 'When you create a Simul, you get to play several players at once.'; + + @override + String get simulVariantsHint => 'If you select several variants, each player gets to choose which one to play.'; + + @override + String get simulClockHint => 'Fischer Clock setup. The more players you take on, the more time you may need.'; + + @override + String get simulAddExtraTime => 'You may add extra initial time to your clock to help you cope with the simul.'; + + @override + String get simulHostExtraTime => 'Host extra initial clock time'; + + @override + String get simulAddExtraTimePerPlayer => 'Add initial time to your clock for each player joining the simul.'; + + @override + String get simulHostExtraTimePerPlayer => 'Host extra clock time per player'; + + @override + String get lichessTournaments => 'Lichess tournaments'; + + @override + String get tournamentFAQ => 'Arena tournament FAQ'; + + @override + String get timeBeforeTournamentStarts => 'Time before tournament starts'; + + @override + String get averageCentipawnLoss => 'Average centipawn loss'; + + @override + String get accuracy => 'Accuracy'; + + @override + String get keyboardShortcuts => 'Keyboard shortcuts'; + + @override + String get keyMoveBackwardOrForward => 'move backward/forward'; + + @override + String get keyGoToStartOrEnd => 'go to start/end'; + + @override + String get keyCycleSelectedVariation => 'Cycle selected variation'; + + @override + String get keyShowOrHideComments => 'show/hide comments'; + + @override + String get keyEnterOrExitVariation => 'enter/exit variation'; + + @override + String get keyRequestComputerAnalysis => 'Request computer analysis, Learn from your mistakes'; + + @override + String get keyNextLearnFromYourMistakes => 'Next (Learn from your mistakes)'; + + @override + String get keyNextBlunder => 'Next blunder'; + + @override + String get keyNextMistake => 'Next mistake'; + + @override + String get keyNextInaccuracy => 'Next inaccuracy'; + + @override + String get keyPreviousBranch => 'Previous branch'; + + @override + String get keyNextBranch => 'Next branch'; + + @override + String get toggleVariationArrows => 'Toggle variation arrows'; + + @override + String get cyclePreviousOrNextVariation => 'Cycle previous/next variation'; + + @override + String get toggleGlyphAnnotations => 'Toggle move annotations'; + + @override + String get togglePositionAnnotations => 'Toggle position annotations'; + + @override + String get variationArrowsInfo => 'Variation arrows let you navigate without using the move list.'; + + @override + String get playSelectedMove => 'play selected move'; + + @override + String get newTournament => 'New tournament'; + + @override + String get tournamentHomeTitle => 'Chess tournaments featuring various time controls and variants'; + + @override + String get tournamentHomeDescription => 'Play fast-paced chess tournaments! Join an official scheduled tournament, or create your own. Bullet, Blitz, Classical, Chess960, King of the Hill, Threecheck, and more options available for endless chess fun.'; + + @override + String get tournamentNotFound => 'Tournament not found'; + + @override + String get tournamentDoesNotExist => 'This tournament does not exist.'; + + @override + String get tournamentMayHaveBeenCanceled => 'The tournament may have been canceled if all players left before it started.'; + + @override + String get returnToTournamentsHomepage => 'Return to tournaments homepage'; + + @override + String weeklyPerfTypeRatingDistribution(String param) { + return 'Weekly $param rating distribution'; + } + + @override + String yourPerfTypeRatingIsRating(String param1, String param2) { + return 'Your $param1 rating is $param2.'; + } + + @override + String youAreBetterThanPercentOfPerfTypePlayers(String param1, String param2) { + return 'You are better than $param1 of $param2 players.'; + } + + @override + String userIsBetterThanPercentOfPerfTypePlayers(String param1, String param2, String param3) { + return '$param1 is better than $param2 of $param3 players.'; + } + + @override + String betterThanPercentPlayers(String param1, String param2) { + return 'Better than $param1 of $param2 players'; + } + + @override + String youDoNotHaveAnEstablishedPerfTypeRating(String param) { + return 'You do not have an established $param rating.'; + } + + @override + String get yourRating => 'Your rating'; + + @override + String get cumulative => 'Cumulative'; + + @override + String get glicko2Rating => 'Glicko-2 rating'; + + @override + String get checkYourEmail => 'Check your Email'; + + @override + String get weHaveSentYouAnEmailClickTheLink => 'We\'ve sent you an email. Click the link in the email to activate your account.'; + + @override + String get ifYouDoNotSeeTheEmailCheckOtherPlaces => 'If you don\'t see the email, check other places it might be, like your junk, spam, social, or other folders.'; + + @override + String weHaveSentYouAnEmailTo(String param) { + return 'We\'ve sent an email to $param. Click the link in the email to reset your password.'; + } + + @override + String byRegisteringYouAgreeToBeBoundByOur(String param) { + return 'By registering, you agree to the $param.'; + } + + @override + String readAboutOur(String param) { + return 'Read about our $param.'; + } + + @override + String get networkLagBetweenYouAndLichess => 'Network lag between you and Lichess'; + + @override + String get timeToProcessAMoveOnLichessServer => 'Time to process a move on Lichess\'s server'; + + @override + String get downloadAnnotated => 'Download annotated'; + + @override + String get downloadRaw => 'Download raw'; + + @override + String get downloadImported => 'Download imported'; + + @override + String get crosstable => 'Crosstable'; + + @override + String get youCanAlsoScrollOverTheBoardToMoveInTheGame => 'You can also scroll over the board to move in the game.'; + + @override + String get scrollOverComputerVariationsToPreviewThem => 'Scroll over computer variations to preview them.'; + + @override + String get analysisShapesHowTo => 'Press shift+click or right-click to draw circles and arrows on the board.'; + + @override + String get letOtherPlayersMessageYou => 'Let other players message you'; + + @override + String get receiveForumNotifications => 'Receive notifications when mentioned in the forum'; + + @override + String get shareYourInsightsData => 'Share your chess insights data'; + + @override + String get withNobody => 'With nobody'; + + @override + String get withFriends => 'With friends'; + + @override + String get withEverybody => 'With everybody'; + + @override + String get kidMode => 'Kid mode'; + + @override + String get kidModeIsEnabled => 'Kid mode is enabled.'; + + @override + String get kidModeExplanation => 'This is about safety. In kid mode, all site communications are disabled. Enable this for your children and school students, to protect them from other internet users.'; + + @override + String inKidModeTheLichessLogoGetsIconX(String param) { + return 'In kid mode, the Lichess logo gets a $param icon, so you know your kids are safe.'; + } + + @override + String get askYourChessTeacherAboutLiftingKidMode => 'Your account is managed. Ask your chess teacher about lifting kid mode.'; + + @override + String get enableKidMode => 'Enable Kid mode'; + + @override + String get disableKidMode => 'Disable Kid mode'; + + @override + String get security => 'Security'; + + @override + String get sessions => 'Sessions'; + + @override + String get revokeAllSessions => 'revoke all sessions'; + + @override + String get playChessEverywhere => 'Play chess everywhere'; + + @override + String get asFreeAsLichess => 'As free as Lichess'; + + @override + String get builtForTheLoveOfChessNotMoney => 'Built for the love of chess, not money'; + + @override + String get everybodyGetsAllFeaturesForFree => 'Everybody gets all features for free'; + + @override + String get zeroAdvertisement => 'Zero advertisement'; + + @override + String get fullFeatured => 'Full featured'; + + @override + String get phoneAndTablet => 'Phone and tablet'; + + @override + String get bulletBlitzClassical => 'Bullet, blitz, classical'; + + @override + String get correspondenceChess => 'Correspondence chess'; + + @override + String get onlineAndOfflinePlay => 'Online and offline play'; + + @override + String get viewTheSolution => 'View the solution'; + + @override + String get followAndChallengeFriends => 'Follow and challenge friends'; + + @override + String get gameAnalysis => 'Game analysis'; + + @override + String xHostsY(String param1, String param2) { + return '$param1 hosts $param2'; + } + + @override + String xJoinsY(String param1, String param2) { + return '$param1 joins $param2'; + } + + @override + String xLikesY(String param1, String param2) { + return '$param1 likes $param2'; + } + + @override + String get quickPairing => 'Quick pairing'; + + @override + String get lobby => 'Lobby'; + + @override + String get anonymous => 'Anonymous'; + + @override + String yourScore(String param) { + return 'Your score: $param'; + } + + @override + String get language => 'Language'; + + @override + String get background => 'Background'; + + @override + String get light => 'Light'; + + @override + String get dark => 'Dark'; + + @override + String get transparent => 'Transparent'; + + @override + String get deviceTheme => 'Device theme'; + + @override + String get backgroundImageUrl => 'Background image URL:'; + + @override + String get board => 'Board'; + + @override + String get size => 'Size'; + + @override + String get opacity => 'Opacity'; + + @override + String get brightness => 'Brightness'; + + @override + String get hue => 'Hue'; + + @override + String get boardReset => 'Reset colors to default'; + + @override + String get pieceSet => 'Piece set'; + + @override + String get embedInYourWebsite => 'Embed in your website'; + + @override + String get usernameAlreadyUsed => 'This username is already in use, please try another one.'; + + @override + String get usernamePrefixInvalid => 'The username must start with a letter.'; + + @override + String get usernameSuffixInvalid => 'The username must end with a letter or a number.'; + + @override + String get usernameCharsInvalid => 'The username must only contain letters, numbers, underscores, and hyphens. Consecutive underscores and hyphens are not allowed.'; + + @override + String get usernameUnacceptable => 'This username is not acceptable.'; + + @override + String get playChessInStyle => 'Play chess in style'; + + @override + String get chessBasics => 'Chess basics'; + + @override + String get coaches => 'Coaches'; + + @override + String get invalidPgn => 'Invalid PGN'; + + @override + String get invalidFen => 'Invalid FEN'; + + @override + String get custom => 'Custom'; + + @override + String get notifications => 'Notifications'; + + @override + String notificationsX(String param1) { + return 'Notifications: $param1'; + } + + @override + String perfRatingX(String param) { + return 'Rating: $param'; + } + + @override + String get practiceWithComputer => 'Practice with computer'; + + @override + String anotherWasX(String param) { + return 'Another was $param'; + } + + @override + String bestWasX(String param) { + return 'Best was $param'; + } + + @override + String get youBrowsedAway => 'You browsed away'; + + @override + String get resumePractice => 'Resume practice'; + + @override + String get drawByFiftyMoves => 'The game has been drawn by the fifty-move rule.'; + + @override + String get theGameIsADraw => 'The game is a draw.'; + + @override + String get computerThinking => 'Computer thinking ...'; + + @override + String get seeBestMove => 'See best move'; + + @override + String get hideBestMove => 'Hide the best move'; + + @override + String get getAHint => 'Get a hint'; + + @override + String get evaluatingYourMove => 'Evaluating your move ...'; + + @override + String get whiteWinsGame => 'White wins'; + + @override + String get blackWinsGame => 'Black wins'; + + @override + String get learnFromYourMistakes => 'Learn from your mistakes'; + + @override + String get learnFromThisMistake => 'Learn from this mistake'; + + @override + String get skipThisMove => 'Skip this move'; + + @override + String get next => 'Next'; + + @override + String xWasPlayed(String param) { + return '$param was played'; + } + + @override + String get findBetterMoveForWhite => 'Find a better move for white'; + + @override + String get findBetterMoveForBlack => 'Find a better move for black'; + + @override + String get resumeLearning => 'Resume learning'; + + @override + String get youCanDoBetter => 'You can do better'; + + @override + String get tryAnotherMoveForWhite => 'Try another move for white'; + + @override + String get tryAnotherMoveForBlack => 'Try another move for black'; + + @override + String get solution => 'Solution'; + + @override + String get waitingForAnalysis => 'Waiting for analysis'; @override String get noMistakesFoundForWhite => 'No significant mistakes found for White'; @override - String get noMistakesFoundForBlack => 'No significant mistakes found for Black'; + String get noMistakesFoundForBlack => 'No significant mistakes found for Black'; + + @override + String get doneReviewingWhiteMistakes => 'Done reviewing White mistakes'; + + @override + String get doneReviewingBlackMistakes => 'Done reviewing Black mistakes'; + + @override + String get doItAgain => 'Do it again'; + + @override + String get reviewWhiteMistakes => 'Review White mistakes'; + + @override + String get reviewBlackMistakes => 'Review Black mistakes'; + + @override + String get advantage => 'Advantage'; + + @override + String get opening => 'Opening'; + + @override + String get middlegame => 'Middlegame'; + + @override + String get endgame => 'Endgame'; + + @override + String get conditionalPremoves => 'Conditional premoves'; + + @override + String get addCurrentVariation => 'Add current variation'; + + @override + String get playVariationToCreateConditionalPremoves => 'Play a variation to create conditional premoves'; + + @override + String get noConditionalPremoves => 'No conditional premoves'; + + @override + String playX(String param) { + return 'Play $param'; + } + + @override + String get showUnreadLichessMessage => 'You have received a private message from Lichess.'; + + @override + String get clickHereToReadIt => 'Click here to read it'; + + @override + String get sorry => 'Sorry :('; + + @override + String get weHadToTimeYouOutForAWhile => 'We had to time you out for a while.'; + + @override + String get why => 'Why?'; + + @override + String get pleasantChessExperience => 'We aim to provide a pleasant chess experience for everyone.'; + + @override + String get goodPractice => 'To that effect, we must ensure that all players follow good practice.'; + + @override + String get potentialProblem => 'When a potential problem is detected, we display this message.'; + + @override + String get howToAvoidThis => 'How to avoid this?'; + + @override + String get playEveryGame => 'Play every game you start.'; + + @override + String get tryToWin => 'Try to win (or at least draw) every game you play.'; + + @override + String get resignLostGames => 'Resign lost games (don\'t let the clock run down).'; + + @override + String get temporaryInconvenience => 'We apologize for the temporary inconvenience,'; + + @override + String get wishYouGreatGames => 'and wish you great games on lichess.org.'; + + @override + String get thankYouForReading => 'Thank you for reading!'; + + @override + String get lifetimeScore => 'Lifetime score'; + + @override + String get currentMatchScore => 'Current match score'; + + @override + String get agreementAssistance => 'I agree that I will at no time receive assistance during my games (from a chess computer, book, database or another person).'; + + @override + String get agreementNice => 'I agree that I will always be respectful to other players.'; + + @override + String agreementMultipleAccounts(String param) { + return 'I agree that I will not create multiple accounts (except for the reasons stated in the $param).'; + } + + @override + String get agreementPolicy => 'I agree that I will follow all Lichess policies.'; + + @override + String get searchOrStartNewDiscussion => 'Search or start new conversation'; + + @override + String get edit => 'Edit'; + + @override + String get bullet => 'Bullet'; + + @override + String get blitz => 'Blitz'; + + @override + String get rapid => 'Rapid'; + + @override + String get classical => 'Classical'; + + @override + String get ultraBulletDesc => 'Insanely fast games: less than 30 seconds'; + + @override + String get bulletDesc => 'Very fast games: less than 3 minutes'; + + @override + String get blitzDesc => 'Fast games: 3 to 8 minutes'; + + @override + String get rapidDesc => 'Rapid games: 8 to 25 minutes'; + + @override + String get classicalDesc => 'Classical games: 25 minutes and more'; + + @override + String get correspondenceDesc => 'Correspondence games: one or several days per move'; + + @override + String get puzzleDesc => 'Chess tactics trainer'; + + @override + String get important => 'Important'; + + @override + String yourQuestionMayHaveBeenAnswered(String param1) { + return 'Your question may already have an answer $param1'; + } + + @override + String get inTheFAQ => 'in the F.A.Q.'; + + @override + String toReportSomeoneForCheatingOrBadBehavior(String param1) { + return 'To report a user for cheating or bad behavior, $param1'; + } + + @override + String get useTheReportForm => 'use the report form'; + + @override + String toRequestSupport(String param1) { + return 'To request support, $param1'; + } + + @override + String get tryTheContactPage => 'try the contact page'; + + @override + String makeSureToRead(String param1) { + return 'Make sure to read $param1'; + } + + @override + String get theForumEtiquette => 'the forum etiquette'; + + @override + String get thisTopicIsArchived => 'This topic has been archived and can no longer be replied to.'; + + @override + String joinTheTeamXToPost(String param1) { + return 'Join the $param1, to post in this forum'; + } + + @override + String teamNamedX(String param1) { + return '$param1 team'; + } + + @override + String get youCannotPostYetPlaySomeGames => 'You can\'t post in the forums yet. Play some games!'; + + @override + String get subscribe => 'Subscribe'; + + @override + String get unsubscribe => 'Unsubscribe'; + + @override + String mentionedYouInX(String param1) { + return 'mentioned you in \"$param1\".'; + } + + @override + String xMentionedYouInY(String param1, String param2) { + return '$param1 mentioned you in \"$param2\".'; + } + + @override + String invitedYouToX(String param1) { + return 'invited you to \"$param1\".'; + } + + @override + String xInvitedYouToY(String param1, String param2) { + return '$param1 invited you to \"$param2\".'; + } + + @override + String get youAreNowPartOfTeam => 'You are now part of the team.'; + + @override + String youHaveJoinedTeamX(String param1) { + return 'You have joined \"$param1\".'; + } + + @override + String get someoneYouReportedWasBanned => 'Someone you reported was banned'; + + @override + String get congratsYouWon => 'Congratulations, you won!'; + + @override + String gameVsX(String param1) { + return 'Game vs $param1'; + } + + @override + String resVsX(String param1, String param2) { + return '$param1 vs $param2'; + } + + @override + String get lostAgainstTOSViolator => 'You lost rating points to someone who violated the Lichess TOS'; + + @override + String refundXpointsTimeControlY(String param1, String param2) { + return 'Refund: $param1 $param2 rating points.'; + } + + @override + String get timeAlmostUp => 'Time is almost up!'; + + @override + String get clickToRevealEmailAddress => '[Click to reveal email address]'; + + @override + String get download => 'Download'; + + @override + String get coachManager => 'Coach manager'; + + @override + String get streamerManager => 'Streamer manager'; + + @override + String get cancelTournament => 'Cancel the tournament'; + + @override + String get tournDescription => 'Tournament description'; + + @override + String get tournDescriptionHelp => 'Anything special you want to tell the participants? Try to keep it short. Markdown links are available: [name](https://url)'; + + @override + String get ratedFormHelp => 'Games are rated and impact players ratings'; + + @override + String get onlyMembersOfTeam => 'Only members of team'; + + @override + String get noRestriction => 'No restriction'; + + @override + String get minimumRatedGames => 'Minimum rated games'; + + @override + String get minimumRating => 'Minimum rating'; + + @override + String get maximumWeeklyRating => 'Maximum weekly rating'; + + @override + String positionInputHelp(String param) { + return 'Paste a valid FEN to start every game from a given position.\nIt only works for standard games, not with variants.\nYou can use the $param to generate a FEN position, then paste it here.\nLeave empty to start games from the normal initial position.'; + } + + @override + String get cancelSimul => 'Cancel the simul'; + + @override + String get simulHostcolor => 'Host color for each game'; + + @override + String get estimatedStart => 'Estimated start time'; + + @override + String simulFeatured(String param) { + return 'Feature on $param'; + } + + @override + String simulFeaturedHelp(String param) { + return 'Show your simul to everyone on $param. Disable for private simuls.'; + } + + @override + String get simulDescription => 'Simul description'; + + @override + String get simulDescriptionHelp => 'Anything you want to tell the participants?'; + + @override + String markdownAvailable(String param) { + return '$param is available for more advanced syntax.'; + } + + @override + String get embedsAvailable => 'Paste a game URL or a study chapter URL to embed it.'; + + @override + String get inYourLocalTimezone => 'In your own local timezone'; + + @override + String get tournChat => 'Tournament chat'; + + @override + String get noChat => 'No chat'; + + @override + String get onlyTeamLeaders => 'Only team leaders'; + + @override + String get onlyTeamMembers => 'Only team members'; + + @override + String get navigateMoveTree => 'Navigate the move tree'; + + @override + String get mouseTricks => 'Mouse tricks'; + + @override + String get toggleLocalAnalysis => 'Toggle local computer analysis'; + + @override + String get toggleAllAnalysis => 'Toggle all computer analysis'; + + @override + String get playComputerMove => 'Play best computer move'; + + @override + String get analysisOptions => 'Analysis options'; + + @override + String get focusChat => 'Focus chat'; + + @override + String get showHelpDialog => 'Show this help dialog'; + + @override + String get reopenYourAccount => 'Reopen your account'; + + @override + String get closedAccountChangedMind => 'If you closed your account, but have since changed your mind, you get one chance of getting your account back.'; + + @override + String get onlyWorksOnce => 'This will only work once.'; + + @override + String get cantDoThisTwice => 'If you close your account a second time, there will be no way of recovering it.'; + + @override + String get emailAssociatedToaccount => 'Email address associated to the account'; + + @override + String get sentEmailWithLink => 'We\'ve sent you an email with a link.'; + + @override + String get tournamentEntryCode => 'Tournament entry code'; + + @override + String get hangOn => 'Hang on!'; + + @override + String gameInProgress(String param) { + return 'You have a game in progress with $param.'; + } + + @override + String get abortTheGame => 'Abort the game'; + + @override + String get resignTheGame => 'Resign the game'; + + @override + String get youCantStartNewGame => 'You can\'t start a new game until this one is finished.'; + + @override + String get since => 'Since'; + + @override + String get until => 'Until'; + + @override + String get lichessDbExplanation => 'Rated games sampled from all Lichess players'; + + @override + String get switchSides => 'Switch sides'; + + @override + String get closingAccountWithdrawAppeal => 'Closing your account will withdraw your appeal'; + + @override + String get ourEventTips => 'Our tips for organizing events'; + + @override + String get instructions => 'Instructions'; + + @override + String get showMeEverything => 'Show me everything'; + + @override + String get lichessPatronInfo => 'Lichess is a charity and entirely free/libre open source software.\nAll operating costs, development, and content are funded solely by user donations.'; + + @override + String get nothingToSeeHere => 'Nothing to see here at the moment.'; + + @override + String opponentLeftCounter(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Your opponent left the game. You can claim victory in $count seconds.', + one: 'Your opponent left the game. You can claim victory in $count second.', + ); + return '$_temp0'; + } + + @override + String mateInXHalfMoves(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Mate in $count half-moves', + one: 'Mate in $count half-move', + ); + return '$_temp0'; + } + + @override + String nbBlunders(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count blunders', + one: '$count blunder', + ); + return '$_temp0'; + } + + @override + String nbMistakes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count mistakes', + one: '$count mistake', + ); + return '$_temp0'; + } + + @override + String nbInaccuracies(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count inaccuracies', + one: '$count inaccuracy', + ); + return '$_temp0'; + } + + @override + String nbPlayers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count players', + one: '$count player', + ); + return '$_temp0'; + } + + @override + String nbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count games', + one: '$count game', + ); + return '$_temp0'; + } + + @override + String ratingXOverYGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count rating over $param2 games', + one: '$count rating over $param2 game', + ); + return '$_temp0'; + } + + @override + String nbBookmarks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count bookmarks', + one: '$count bookmark', + ); + return '$_temp0'; + } + + @override + String nbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count days', + one: '$count day', + ); + return '$_temp0'; + } + + @override + String nbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hours', + one: '$count hour', + ); + return '$_temp0'; + } @override - String get doneReviewingWhiteMistakes => 'Done reviewing White mistakes'; + String nbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutes', + one: '$count minute', + ); + return '$_temp0'; + } @override - String get doneReviewingBlackMistakes => 'Done reviewing Black mistakes'; + String rankIsUpdatedEveryNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Rank is updated every $count minutes', + one: 'Rank is updated every minute', + ); + return '$_temp0'; + } @override - String get doItAgain => 'Do it again'; + String nbPuzzles(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count puzzles', + one: '$count puzzle', + ); + return '$_temp0'; + } @override - String get reviewWhiteMistakes => 'Review White mistakes'; + String nbGamesWithYou(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count games with you', + one: '$count game with you', + ); + return '$_temp0'; + } @override - String get reviewBlackMistakes => 'Review Black mistakes'; + String nbRated(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count rated', + one: '$count rated', + ); + return '$_temp0'; + } @override - String get advantage => 'Advantage'; + String nbWins(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count wins', + one: '$count win', + ); + return '$_temp0'; + } @override - String get opening => 'Opening'; + String nbLosses(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count losses', + one: '$count loss', + ); + return '$_temp0'; + } @override - String get middlegame => 'Middlegame'; + String nbDraws(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count draws', + one: '$count draw', + ); + return '$_temp0'; + } @override - String get endgame => 'Endgame'; + String nbPlaying(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count playing', + one: '$count playing', + ); + return '$_temp0'; + } @override - String get conditionalPremoves => 'Conditional premoves'; + String giveNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Give $count seconds', + one: 'Give $count second', + ); + return '$_temp0'; + } @override - String get addCurrentVariation => 'Add current variation'; + String nbTournamentPoints(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tournament points', + one: '$count tournament point', + ); + return '$_temp0'; + } @override - String get playVariationToCreateConditionalPremoves => 'Play a variation to create conditional premoves'; + String nbStudies(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count studies', + one: '$count study', + ); + return '$_temp0'; + } @override - String get noConditionalPremoves => 'No conditional premoves'; + String nbSimuls(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count simuls', + one: '$count simul', + ); + return '$_temp0'; + } @override - String playX(String param) { - return 'Play $param'; + String moreThanNbRatedGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '≥ $count rated games', + one: '≥ $count rated game', + ); + return '$_temp0'; } @override - String get showUnreadLichessMessage => 'You have received a private message from Lichess.'; + String moreThanNbPerfRatedGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '≥ $count $param2 rated games', + one: '≥ $count $param2 rated game', + ); + return '$_temp0'; + } + + @override + String needNbMorePerfGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'You need to play $count more $param2 rated games', + one: 'You need to play $count more $param2 rated game', + ); + return '$_temp0'; + } + + @override + String needNbMoreGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'You need to play $count more rated games', + one: 'You need to play $count more rated game', + ); + return '$_temp0'; + } + + @override + String nbImportedGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count imported games', + one: '$count imported game', + ); + return '$_temp0'; + } + + @override + String nbFriendsOnline(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count friends online', + one: '$count friend online', + ); + return '$_temp0'; + } + + @override + String nbFollowers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count followers', + one: '$count follower', + ); + return '$_temp0'; + } + + @override + String nbFollowing(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count following', + one: '$count following', + ); + return '$_temp0'; + } + + @override + String lessThanNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Less than $count minutes', + one: 'Less than $count minute', + ); + return '$_temp0'; + } + + @override + String nbGamesInPlay(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count games in play', + one: '$count game in play', + ); + return '$_temp0'; + } @override - String get clickHereToReadIt => 'Click here to read it'; + String maximumNbCharacters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Maximum: $count characters.', + one: 'Maximum: $count character.', + ); + return '$_temp0'; + } @override - String get sorry => 'Sorry :('; + String blocks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count blocks', + one: '$count block', + ); + return '$_temp0'; + } @override - String get weHadToTimeYouOutForAWhile => 'We had to time you out for a while.'; + String nbForumPosts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count forum posts', + one: '$count forum post', + ); + return '$_temp0'; + } @override - String get why => 'Why?'; + String nbPerfTypePlayersThisWeek(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count $param2 players this week.', + one: '$count $param2 player this week.', + ); + return '$_temp0'; + } @override - String get pleasantChessExperience => 'We aim to provide a pleasant chess experience for everyone.'; + String availableInNbLanguages(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Available in $count languages!', + one: 'Available in $count language!', + ); + return '$_temp0'; + } @override - String get goodPractice => 'To that effect, we must ensure that all players follow good practice.'; + String nbSecondsToPlayTheFirstMove(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count seconds to play the first move', + one: '$count second to play the first move', + ); + return '$_temp0'; + } @override - String get potentialProblem => 'When a potential problem is detected, we display this message.'; + String nbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count seconds', + one: '$count second', + ); + return '$_temp0'; + } @override - String get howToAvoidThis => 'How to avoid this?'; + String andSaveNbPremoveLines(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'and save $count premove lines', + one: 'and save $count premove line', + ); + return '$_temp0'; + } @override - String get playEveryGame => 'Play every game you start.'; + String get stormMoveToStart => 'Move to start'; @override - String get tryToWin => 'Try to win (or at least draw) every game you play.'; + String get stormYouPlayTheWhitePiecesInAllPuzzles => 'You play the white pieces in all puzzles'; @override - String get resignLostGames => 'Resign lost games (don\'t let the clock run down).'; + String get stormYouPlayTheBlackPiecesInAllPuzzles => 'You play the black pieces in all puzzles'; @override - String get temporaryInconvenience => 'We apologize for the temporary inconvenience,'; + String get stormPuzzlesSolved => 'puzzles solved'; @override - String get wishYouGreatGames => 'and wish you great games on lichess.org.'; + String get stormNewDailyHighscore => 'New daily highscore!'; @override - String get thankYouForReading => 'Thank you for reading!'; + String get stormNewWeeklyHighscore => 'New weekly highscore!'; @override - String get lifetimeScore => 'Lifetime score'; + String get stormNewMonthlyHighscore => 'New monthly highscore!'; @override - String get currentMatchScore => 'Current match score'; + String get stormNewAllTimeHighscore => 'New all-time highscore!'; @override - String get agreementAssistance => 'I agree that I will at no time receive assistance during my games (from a chess computer, book, database or another person).'; + String stormPreviousHighscoreWasX(String param) { + return 'Previous highscore was $param'; + } @override - String get agreementNice => 'I agree that I will always be nice to other players.'; + String get stormPlayAgain => 'Play again'; @override - String agreementMultipleAccounts(String param) { - return 'I agree that I will not create multiple accounts (except for the reasons stated in the $param).'; + String stormHighscoreX(String param) { + return 'Highscore: $param'; } @override - String get agreementPolicy => 'I agree that I will follow all Lichess policies.'; - - @override - String get searchOrStartNewDiscussion => 'Search or start new discussion'; + String get stormScore => 'Score'; @override - String get edit => 'Edit'; + String get stormMoves => 'Moves'; @override - String get bullet => 'Bullet'; + String get stormAccuracy => 'Accuracy'; @override - String get blitz => 'Blitz'; + String get stormCombo => 'Combo'; @override - String get rapid => 'Rapid'; + String get stormTime => 'Time'; @override - String get classical => 'Classical'; + String get stormTimePerMove => 'Time per move'; @override - String get ultraBulletDesc => 'Insanely fast games: less than 30 seconds'; + String get stormHighestSolved => 'Highest solved'; @override - String get bulletDesc => 'Very fast games: less than 3 minutes'; + String get stormPuzzlesPlayed => 'Puzzles played'; @override - String get blitzDesc => 'Fast games: 3 to 8 minutes'; + String get stormNewRun => 'New run (hotkey: Space)'; @override - String get rapidDesc => 'Rapid games: 8 to 25 minutes'; + String get stormEndRun => 'End run (hotkey: Enter)'; @override - String get classicalDesc => 'Classical games: 25 minutes and more'; + String get stormHighscores => 'Highscores'; @override - String get correspondenceDesc => 'Correspondence games: one or several days per move'; + String get stormViewBestRuns => 'View best runs'; @override - String get puzzleDesc => 'Chess tactics trainer'; + String get stormBestRunOfDay => 'Best run of day'; @override - String get important => 'Important'; + String get stormRuns => 'Runs'; @override - String yourQuestionMayHaveBeenAnswered(String param1) { - return 'Your question may already have an answer $param1'; - } + String get stormGetReady => 'Get ready!'; @override - String get inTheFAQ => 'in the F.A.Q.'; + String get stormWaitingForMorePlayers => 'Waiting for more players to join...'; @override - String toReportSomeoneForCheatingOrBadBehavior(String param1) { - return 'To report a user for cheating or bad behavior, $param1'; - } + String get stormRaceComplete => 'Race complete!'; @override - String get useTheReportForm => 'use the report form'; + String get stormSpectating => 'Spectating'; @override - String toRequestSupport(String param1) { - return 'To request support, $param1'; - } + String get stormJoinTheRace => 'Join the race!'; @override - String get tryTheContactPage => 'try the contact page'; + String get stormStartTheRace => 'Start the race'; @override - String makeSureToRead(String param1) { - return 'Make sure to read $param1'; + String stormYourRankX(String param) { + return 'Your rank: $param'; } @override - String get theForumEtiquette => 'the forum etiquette'; - - @override - String get thisTopicIsArchived => 'This topic has been archived and can no longer be replied to.'; + String get stormWaitForRematch => 'Wait for rematch'; @override - String joinTheTeamXToPost(String param1) { - return 'Join the $param1, to post in this forum'; - } + String get stormNextRace => 'Next race'; @override - String teamNamedX(String param1) { - return '$param1 team'; - } + String get stormJoinRematch => 'Join rematch'; @override - String get youCannotPostYetPlaySomeGames => 'You can\'t post in the forums yet. Play some games!'; + String get stormWaitingToStart => 'Waiting to start'; @override - String get subscribe => 'Subscribe'; + String get stormCreateNewGame => 'Create a new game'; @override - String get unsubscribe => 'Unsubscribe'; + String get stormJoinPublicRace => 'Join a public race'; @override - String mentionedYouInX(String param1) { - return 'mentioned you in \"$param1\".'; - } + String get stormRaceYourFriends => 'Race your friends'; @override - String xMentionedYouInY(String param1, String param2) { - return '$param1 mentioned you in \"$param2\".'; - } + String get stormSkip => 'skip'; @override - String invitedYouToX(String param1) { - return 'invited you to \"$param1\".'; - } + String get stormSkipHelp => 'You can skip one move per race:'; @override - String xInvitedYouToY(String param1, String param2) { - return '$param1 invited you to \"$param2\".'; - } + String get stormSkipExplanation => 'Skip this move to preserve your combo! Only works once per race.'; @override - String get youAreNowPartOfTeam => 'You are now part of the team.'; + String get stormFailedPuzzles => 'Failed puzzles'; @override - String youHaveJoinedTeamX(String param1) { - return 'You have joined \"$param1\".'; - } + String get stormSlowPuzzles => 'Slow puzzles'; @override - String get someoneYouReportedWasBanned => 'Someone you reported was banned'; + String get stormSkippedPuzzle => 'Skipped puzzle'; @override - String get congratsYouWon => 'Congratulations, you won!'; + String get stormThisWeek => 'This week'; @override - String gameVsX(String param1) { - return 'Game vs $param1'; - } + String get stormThisMonth => 'This month'; @override - String resVsX(String param1, String param2) { - return '$param1 vs $param2'; - } + String get stormAllTime => 'All-time'; @override - String get lostAgainstTOSViolator => 'You lost rating points to someone who violated the Lichess TOS'; + String get stormClickToReload => 'Click to reload'; @override - String refundXpointsTimeControlY(String param1, String param2) { - return 'Refund: $param1 $param2 rating points.'; - } + String get stormThisRunHasExpired => 'This run has expired!'; @override - String get timeAlmostUp => 'Time is almost up!'; + String get stormThisRunWasOpenedInAnotherTab => 'This run was opened in another tab!'; @override - String get clickToRevealEmailAddress => '[Click to reveal email address.]'; + String stormXRuns(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count runs', + one: '1 run', + ); + return '$_temp0'; + } @override - String get download => 'Download'; + String stormPlayedNbRunsOfPuzzleStorm(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Played $count runs of $param2', + one: 'Played one run of $param2', + ); + return '$_temp0'; + } @override - String get coachManager => 'Coach manager'; + String get streamerLichessStreamers => 'Lichess streamers'; @override - String get streamerManager => 'Streamer manager'; + String get studyPrivate => 'Private'; @override - String get cancelTournament => 'Cancel the tournament'; + String get studyMyStudies => 'My studies'; @override - String get tournDescription => 'Tournament description'; + String get studyStudiesIContributeTo => 'Studies I contribute to'; @override - String get tournDescriptionHelp => 'Anything special you want to tell the participants? Try to keep it short. Markdown links are available: [name](https://url)'; + String get studyMyPublicStudies => 'My public studies'; @override - String get ratedFormHelp => 'Games are rated\nand impact players ratings'; + String get studyMyPrivateStudies => 'My private studies'; @override - String get onlyMembersOfTeam => 'Only members of team'; + String get studyMyFavoriteStudies => 'My favorite studies'; @override - String get noRestriction => 'No restriction'; + String get studyWhatAreStudies => 'What are studies?'; @override - String get minimumRatedGames => 'Minimum rated games'; + String get studyAllStudies => 'All studies'; @override - String get minimumRating => 'Minimum rating'; + String studyStudiesCreatedByX(String param) { + return 'Studies created by $param'; + } @override - String get maximumWeeklyRating => 'Maximum weekly rating'; + String get studyNoneYet => 'None yet.'; @override - String positionInputHelp(String param) { - return 'Paste a valid FEN to start every game from a given position.\nIt only works for standard games, not with variants.\nYou can use the $param to generate a FEN position, then paste it here.\nLeave empty to start games from the normal initial position.'; - } + String get studyHot => 'Hot'; @override - String get cancelSimul => 'Cancel the simul'; + String get studyDateAddedNewest => 'Date added (newest)'; @override - String get simulHostcolor => 'Host color for each game'; + String get studyDateAddedOldest => 'Date added (oldest)'; @override - String get estimatedStart => 'Estimated start time'; + String get studyRecentlyUpdated => 'Recently updated'; @override - String simulFeatured(String param) { - return 'Feature on $param'; - } + String get studyMostPopular => 'Most popular'; @override - String simulFeaturedHelp(String param) { - return 'Show your simul to everyone on $param. Disable for private simuls.'; - } + String get studyAlphabetical => 'Alphabetical'; @override - String get simulDescription => 'Simul description'; + String get studyAddNewChapter => 'Add a new chapter'; @override - String get simulDescriptionHelp => 'Anything you want to tell the participants?'; + String get studyAddMembers => 'Add members'; @override - String markdownAvailable(String param) { - return '$param is available for more advanced syntax.'; - } + String get studyInviteToTheStudy => 'Invite to the study'; @override - String get embedsAvailable => 'Paste a game URL or a study chapter URL to embed it.'; + String get studyPleaseOnlyInvitePeopleYouKnow => 'Please only invite people who know you, and who actively want to join this study.'; @override - String get inYourLocalTimezone => 'In your own local timezone'; + String get studySearchByUsername => 'Search by username'; @override - String get tournChat => 'Tournament chat'; + String get studySpectator => 'Spectator'; @override - String get noChat => 'No chat'; + String get studyContributor => 'Contributor'; @override - String get onlyTeamLeaders => 'Only team leaders'; + String get studyKick => 'Kick'; @override - String get onlyTeamMembers => 'Only team members'; + String get studyLeaveTheStudy => 'Leave the study'; @override - String get navigateMoveTree => 'Navigate the move tree'; + String get studyYouAreNowAContributor => 'You are now a contributor'; @override - String get mouseTricks => 'Mouse tricks'; + String get studyYouAreNowASpectator => 'You are now a spectator'; @override - String get toggleLocalAnalysis => 'Toggle local computer analysis'; + String get studyPgnTags => 'PGN tags'; @override - String get toggleAllAnalysis => 'Toggle all computer analysis'; + String get studyLike => 'Like'; @override - String get playComputerMove => 'Play best computer move'; + String get studyUnlike => 'Unlike'; @override - String get analysisOptions => 'Analysis options'; + String get studyNewTag => 'New tag'; @override - String get focusChat => 'Focus chat'; + String get studyCommentThisPosition => 'Comment on this position'; @override - String get showHelpDialog => 'Show this help dialog'; + String get studyCommentThisMove => 'Comment on this move'; @override - String get reopenYourAccount => 'Reopen your account'; + String get studyAnnotateWithGlyphs => 'Annotate with glyphs'; @override - String get closedAccountChangedMind => 'If you closed your account, but have since changed your mind, you get one chance of getting your account back.'; + String get studyTheChapterIsTooShortToBeAnalysed => 'The chapter is too short to be analyzed.'; @override - String get onlyWorksOnce => 'This will only work once.'; + String get studyOnlyContributorsCanRequestAnalysis => 'Only the study contributors can request a computer analysis.'; @override - String get cantDoThisTwice => 'If you close your account a second time, there will be no way of recovering it.'; + String get studyGetAFullComputerAnalysis => 'Get a full server-side computer analysis of the mainline.'; @override - String get emailAssociatedToaccount => 'Email address associated to the account'; + String get studyMakeSureTheChapterIsComplete => 'Make sure the chapter is complete. You can only request analysis once.'; @override - String get sentEmailWithLink => 'We\'ve sent you an email with a link.'; + String get studyAllSyncMembersRemainOnTheSamePosition => 'All SYNC members remain on the same position'; @override - String get tournamentEntryCode => 'Tournament entry code'; + String get studyShareChanges => 'Share changes with spectators and save them on the server'; @override - String get hangOn => 'Hang on!'; + String get studyPlaying => 'Playing'; @override - String gameInProgress(String param) { - return 'You have a game in progress with $param.'; - } + String get studyShowEvalBar => 'Evaluation gauge'; @override - String get abortTheGame => 'Abort the game'; + String get studyFirst => 'First'; @override - String get resignTheGame => 'Resign the game'; + String get studyPrevious => 'Previous'; @override - String get youCantStartNewGame => 'You can\'t start a new game until this one is finished.'; + String get studyNext => 'Next'; @override - String get since => 'Since'; + String get studyLast => 'Last'; @override - String get until => 'Until'; + String get studyShareAndExport => 'Share & export'; @override - String get lichessDbExplanation => 'Rated games sampled from all Lichess players'; + String get studyCloneStudy => 'Clone'; @override - String get switchSides => 'Switch sides'; + String get studyStudyPgn => 'Study PGN'; @override - String get closingAccountWithdrawAppeal => 'Closing your account will withdraw your appeal'; + String get studyDownloadAllGames => 'Download all games'; @override - String get ourEventTips => 'Our tips for organizing events'; + String get studyChapterPgn => 'Chapter PGN'; @override - String get instructions => 'Instructions'; + String get studyCopyChapterPgn => 'Copy PGN'; @override - String get showMeEverything => 'Show me everything'; + String get studyDownloadGame => 'Download game'; @override - String get lichessPatronInfo => 'Lichess is a charity and entirely free/libre open source software.\nAll operating costs, development, and content are funded solely by user donations.'; + String get studyStudyUrl => 'Study URL'; @override - String get nothingToSeeHere => 'Nothing to see here at the moment.'; + String get studyCurrentChapterUrl => 'Current chapter URL'; @override - String opponentLeftCounter(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Your opponent left the game. You can claim victory in $count seconds.', - one: 'Your opponent left the game. You can claim victory in $count second.', - ); - return '$_temp0'; - } + String get studyYouCanPasteThisInTheForumToEmbed => 'You can paste this in the forum or your Lichess blog to embed'; @override - String mateInXHalfMoves(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Mate in $count half-moves', - one: 'Mate in $count half-move', - ); - return '$_temp0'; - } + String get studyStartAtInitialPosition => 'Start at initial position'; @override - String nbBlunders(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count blunders', - one: '$count blunder', - ); - return '$_temp0'; + String studyStartAtX(String param) { + return 'Start at $param'; } @override - String nbMistakes(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count mistakes', - one: '$count mistake', - ); - return '$_temp0'; - } + String get studyEmbedInYourWebsite => 'Embed in your website'; @override - String nbInaccuracies(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count inaccuracies', - one: '$count inaccuracy', - ); - return '$_temp0'; - } + String get studyReadMoreAboutEmbedding => 'Read more about embedding'; @override - String nbPlayers(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count players', - one: '$count player', - ); - return '$_temp0'; - } + String get studyOnlyPublicStudiesCanBeEmbedded => 'Only public studies can be embedded!'; @override - String nbGames(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count games', - one: '$count game', - ); - return '$_temp0'; - } + String get studyOpen => 'Open'; @override - String ratingXOverYGames(int count, String param2) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count rating over $param2 games', - one: '$count rating over $param2 game', - ); - return '$_temp0'; + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1, brought to you by $param2'; } @override - String nbBookmarks(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count bookmarks', - one: '$count bookmark', - ); - return '$_temp0'; - } + String get studyStudyNotFound => 'Study not found'; @override - String nbDays(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count days', - one: '$count day', - ); - return '$_temp0'; - } + String get studyEditChapter => 'Edit chapter'; @override - String nbHours(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count hours', - one: '$count hour', - ); - return '$_temp0'; - } + String get studyNewChapter => 'New chapter'; @override - String nbMinutes(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count minutes', - one: '$count minute', - ); - return '$_temp0'; + String studyImportFromChapterX(String param) { + return 'Import from $param'; } @override - String rankIsUpdatedEveryNbMinutes(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Rank is updated every $count minutes', - one: 'Rank is updated every minute', - ); - return '$_temp0'; - } + String get studyOrientation => 'Orientation'; @override - String nbPuzzles(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count puzzles', - one: '$count puzzle', - ); - return '$_temp0'; - } + String get studyAnalysisMode => 'Analysis mode'; @override - String nbGamesWithYou(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count games with you', - one: '$count game with you', - ); - return '$_temp0'; - } + String get studyPinnedChapterComment => 'Pinned chapter comment'; @override - String nbRated(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count rated', - one: '$count rated', - ); - return '$_temp0'; - } + String get studySaveChapter => 'Save chapter'; @override - String nbWins(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count wins', - one: '$count win', - ); - return '$_temp0'; - } + String get studyClearAnnotations => 'Clear annotations'; @override - String nbLosses(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count losses', - one: '$count loss', - ); - return '$_temp0'; - } + String get studyClearVariations => 'Clear variations'; @override - String nbDraws(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count draws', - one: '$count draw', - ); - return '$_temp0'; - } + String get studyDeleteChapter => 'Delete chapter'; @override - String nbPlaying(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count playing', - one: '$count playing', - ); - return '$_temp0'; - } + String get studyDeleteThisChapter => 'Delete this chapter? There is no going back!'; @override - String giveNbSeconds(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Give $count seconds', - one: 'Give $count second', - ); - return '$_temp0'; - } + String get studyClearAllCommentsInThisChapter => 'Clear all comments, glyphs and drawn shapes in this chapter?'; @override - String nbTournamentPoints(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count tournament points', - one: '$count tournament point', - ); - return '$_temp0'; - } + String get studyRightUnderTheBoard => 'Right under the board'; @override - String nbStudies(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count studies', - one: '$count study', - ); - return '$_temp0'; - } + String get studyNoPinnedComment => 'None'; + + @override + String get studyNormalAnalysis => 'Normal analysis'; @override - String nbSimuls(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count simuls', - one: '$count simul', - ); - return '$_temp0'; - } + String get studyHideNextMoves => 'Hide next moves'; @override - String moreThanNbRatedGames(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '≥ $count rated games', - one: '≥ $count rated game', - ); - return '$_temp0'; - } + String get studyInteractiveLesson => 'Interactive lesson'; @override - String moreThanNbPerfRatedGames(int count, String param2) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '≥ $count $param2 rated games', - one: '≥ $count $param2 rated game', - ); - return '$_temp0'; + String studyChapterX(String param) { + return 'Chapter $param'; } @override - String needNbMorePerfGames(int count, String param2) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'You need to play $count more $param2 rated games', - one: 'You need to play $count more $param2 rated game', - ); - return '$_temp0'; - } + String get studyEmpty => 'Empty'; @override - String needNbMoreGames(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'You need to play $count more rated games', - one: 'You need to play $count more rated game', - ); - return '$_temp0'; - } + String get studyStartFromInitialPosition => 'Start from initial position'; @override - String nbImportedGames(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count imported games', - one: '$count imported game', - ); - return '$_temp0'; - } + String get studyEditor => 'Editor'; @override - String nbFriendsOnline(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count friends online', - one: '$count friend online', - ); - return '$_temp0'; - } + String get studyStartFromCustomPosition => 'Start from custom position'; @override - String nbFollowers(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count followers', - one: '$count follower', - ); - return '$_temp0'; - } + String get studyLoadAGameByUrl => 'Load games by URL'; @override - String nbFollowing(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count following', - one: '$count following', - ); - return '$_temp0'; - } + String get studyLoadAPositionFromFen => 'Load a position from FEN'; @override - String lessThanNbMinutes(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Less than $count minutes', - one: 'Less than $count minute', - ); - return '$_temp0'; - } + String get studyLoadAGameFromPgn => 'Load games from PGN'; @override - String nbGamesInPlay(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count games in play', - one: '$count game in play', - ); - return '$_temp0'; - } + String get studyAutomatic => 'Automatic'; @override - String maximumNbCharacters(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Maximum: $count characters.', - one: 'Maximum: $count character.', - ); - return '$_temp0'; - } + String get studyUrlOfTheGame => 'URL of the games, one per line'; @override - String blocks(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count blocks', - one: '$count block', - ); - return '$_temp0'; + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Load games from $param1 or $param2'; } @override - String nbForumPosts(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count forum posts', - one: '$count forum post', - ); - return '$_temp0'; - } + String get studyCreateChapter => 'Create chapter'; @override - String nbPerfTypePlayersThisWeek(int count, String param2) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count $param2 players this week.', - one: '$count $param2 player this week.', - ); - return '$_temp0'; - } + String get studyCreateStudy => 'Create study'; @override - String availableInNbLanguages(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Available in $count languages!', - one: 'Available in $count language!', - ); - return '$_temp0'; - } + String get studyEditStudy => 'Edit study'; @override - String nbSecondsToPlayTheFirstMove(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count seconds to play the first move', - one: '$count second to play the first move', - ); - return '$_temp0'; - } + String get studyVisibility => 'Visibility'; @override - String nbSeconds(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count seconds', - one: '$count second', - ); - return '$_temp0'; - } + String get studyPublic => 'Public'; @override - String andSaveNbPremoveLines(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'and save $count premove lines', - one: 'and save $count premove line', - ); - return '$_temp0'; - } + String get studyUnlisted => 'Unlisted'; @override - String get stormMoveToStart => 'Move to start'; + String get studyInviteOnly => 'Invite only'; @override - String get stormYouPlayTheWhitePiecesInAllPuzzles => 'You play the white pieces in all puzzles'; + String get studyAllowCloning => 'Allow cloning'; @override - String get stormYouPlayTheBlackPiecesInAllPuzzles => 'You play the black pieces in all puzzles'; + String get studyNobody => 'Nobody'; @override - String get stormPuzzlesSolved => 'puzzles solved'; + String get studyOnlyMe => 'Only me'; @override - String get stormNewDailyHighscore => 'New daily highscore!'; + String get studyContributors => 'Contributors'; @override - String get stormNewWeeklyHighscore => 'New weekly highscore!'; + String get studyMembers => 'Members'; @override - String get stormNewMonthlyHighscore => 'New monthly highscore!'; + String get studyEveryone => 'Everyone'; @override - String get stormNewAllTimeHighscore => 'New all-time highscore!'; + String get studyEnableSync => 'Enable sync'; @override - String stormPreviousHighscoreWasX(String param) { - return 'Previous highscore was $param'; - } + String get studyYesKeepEveryoneOnTheSamePosition => 'Yes: keep everyone on the same position'; @override - String get stormPlayAgain => 'Play again'; + String get studyNoLetPeopleBrowseFreely => 'No: let people browse freely'; @override - String stormHighscoreX(String param) { - return 'Highscore: $param'; - } + String get studyPinnedStudyComment => 'Pinned study comment'; @override - String get stormScore => 'Score'; + String get studyStart => 'Start'; @override - String get stormMoves => 'Moves'; + String get studySave => 'Save'; @override - String get stormAccuracy => 'Accuracy'; + String get studyClearChat => 'Clear chat'; @override - String get stormCombo => 'Combo'; + String get studyDeleteTheStudyChatHistory => 'Delete the study chat history? There is no going back!'; @override - String get stormTime => 'Time'; + String get studyDeleteStudy => 'Delete study'; @override - String get stormTimePerMove => 'Time per move'; + String studyConfirmDeleteStudy(String param) { + return 'Delete the entire study? There is no going back! Type the name of the study to confirm: $param'; + } @override - String get stormHighestSolved => 'Highest solved'; + String get studyWhereDoYouWantToStudyThat => 'Where do you want to study that?'; @override - String get stormPuzzlesPlayed => 'Puzzles played'; + String get studyGoodMove => 'Good move'; @override - String get stormNewRun => 'New run (hotkey: Space)'; + String get studyMistake => 'Mistake'; @override - String get stormEndRun => 'End run (hotkey: Enter)'; + String get studyBrilliantMove => 'Brilliant move'; @override - String get stormHighscores => 'Highscores'; + String get studyBlunder => 'Blunder'; @override - String get stormViewBestRuns => 'View best runs'; + String get studyInterestingMove => 'Interesting move'; @override - String get stormBestRunOfDay => 'Best run of day'; + String get studyDubiousMove => 'Dubious move'; @override - String get stormRuns => 'Runs'; + String get studyOnlyMove => 'Only move'; @override - String get stormGetReady => 'Get ready!'; + String get studyZugzwang => 'Zugzwang'; @override - String get stormWaitingForMorePlayers => 'Waiting for more players to join...'; + String get studyEqualPosition => 'Equal position'; @override - String get stormRaceComplete => 'Race complete!'; + String get studyUnclearPosition => 'Unclear position'; @override - String get stormSpectating => 'Spectating'; + String get studyWhiteIsSlightlyBetter => 'White is slightly better'; @override - String get stormJoinTheRace => 'Join the race!'; + String get studyBlackIsSlightlyBetter => 'Black is slightly better'; @override - String get stormStartTheRace => 'Start the race'; + String get studyWhiteIsBetter => 'White is better'; @override - String stormYourRankX(String param) { - return 'Your rank: $param'; - } + String get studyBlackIsBetter => 'Black is better'; @override - String get stormWaitForRematch => 'Wait for rematch'; + String get studyWhiteIsWinning => 'White is winning'; @override - String get stormNextRace => 'Next race'; + String get studyBlackIsWinning => 'Black is winning'; @override - String get stormJoinRematch => 'Join rematch'; + String get studyNovelty => 'Novelty'; @override - String get stormWaitingToStart => 'Waiting to start'; + String get studyDevelopment => 'Development'; @override - String get stormCreateNewGame => 'Create a new game'; + String get studyInitiative => 'Initiative'; @override - String get stormJoinPublicRace => 'Join a public race'; + String get studyAttack => 'Attack'; @override - String get stormRaceYourFriends => 'Race your friends'; + String get studyCounterplay => 'Counterplay'; @override - String get stormSkip => 'skip'; + String get studyTimeTrouble => 'Time trouble'; @override - String get stormSkipHelp => 'You can skip one move per race:'; + String get studyWithCompensation => 'With compensation'; @override - String get stormSkipExplanation => 'Skip this move to preserve your combo! Only works once per race.'; + String get studyWithTheIdea => 'With the idea'; @override - String get stormFailedPuzzles => 'Failed puzzles'; + String get studyNextChapter => 'Next chapter'; @override - String get stormSlowPuzzles => 'Slow puzzles'; + String get studyPrevChapter => 'Previous chapter'; @override - String get stormSkippedPuzzle => 'Skipped puzzle'; + String get studyStudyActions => 'Study actions'; @override - String get stormThisWeek => 'This week'; + String get studyTopics => 'Topics'; @override - String get stormThisMonth => 'This month'; + String get studyMyTopics => 'My topics'; @override - String get stormAllTime => 'All-time'; + String get studyPopularTopics => 'Popular topics'; @override - String get stormClickToReload => 'Click to reload'; + String get studyManageTopics => 'Manage topics'; @override - String get stormThisRunHasExpired => 'This run has expired!'; + String get studyBack => 'Back'; @override - String get stormThisRunWasOpenedInAnotherTab => 'This run was opened in another tab!'; + String get studyPlayAgain => 'Play again'; @override - String stormXRuns(int count) { + String get studyWhatWouldYouPlay => 'What would you play in this position?'; + + @override + String get studyYouCompletedThisLesson => 'Congratulations! You completed this lesson.'; + + @override + String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count runs', - one: '1 run', + other: '$count Chapters', + one: '$count Chapter', ); return '$_temp0'; } @override - String stormPlayedNbRunsOfPuzzleStorm(int count, String param2) { + String studyNbGames(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Played $count runs of $param2', - one: 'Played one run of $param2', + other: '$count Games', + one: '$count Game', ); return '$_temp0'; } @override - String get streamerLichessStreamers => 'Lichess streamers'; - - @override - String get studyShareAndExport => 'Share & export'; + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Members', + one: '$count Member', + ); + return '$_temp0'; + } @override - String get studyStart => 'Start'; + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Paste your PGN text here, up to $count games', + one: 'Paste your PGN text here, up to $count game', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_eo.dart b/lib/l10n/l10n_eo.dart index e1f48e2589..8dc62f7fc9 100644 --- a/lib/l10n/l10n_eo.dart +++ b/lib/l10n/l10n_eo.dart @@ -103,9 +103,6 @@ class AppLocalizationsEo extends AppLocalizations { @override String get mobileCancelTakebackOffer => 'Cancel takeback offer'; - @override - String get mobileCancelDrawOffer => 'Cancel draw offer'; - @override String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @@ -246,6 +243,17 @@ class AppLocalizationsEo extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Completed $count $param2 correspondence games', + one: 'Completed $count $param2 correspondence game', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,226 @@ class AppLocalizationsEo extends AppLocalizations { @override String get broadcastBroadcasts => 'Elsendoj'; + @override + String get broadcastMyBroadcasts => 'Miaj elsendoj'; + @override String get broadcastLiveBroadcasts => 'Vivaj turniraj elsendoj'; + @override + String get broadcastBroadcastCalendar => 'Broadcast calendar'; + + @override + String get broadcastNewBroadcast => 'Nova viva elsendo'; + + @override + String get broadcastSubscribedBroadcasts => 'Abonitaj elsendoj'; + + @override + String get broadcastAboutBroadcasts => 'Pri elsendoj'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Kiel uzi Lichess Elsendojn.'; + + @override + String get broadcastTheNewRoundHelp => 'La nova raŭndo havos la samajn membrojn kaj kontribuantojn, kiom la antaŭa.'; + + @override + String get broadcastAddRound => 'Aldoni raŭndon'; + + @override + String get broadcastOngoing => 'Nun funkcianta'; + + @override + String get broadcastUpcoming => 'Baldaŭ'; + + @override + String get broadcastCompleted => 'Kompletigita'; + + @override + String get broadcastCompletedHelp => 'Lichess detektas raŭndan finiĝon baze sur la fontaj ludoj. Uzu ĉi tiun baskuligo, se ne estas fonto.'; + + @override + String get broadcastRoundName => 'Raŭndnomo'; + + @override + String get broadcastRoundNumber => 'Rondnumero'; + + @override + String get broadcastTournamentName => 'Nomo de la turniro'; + + @override + String get broadcastTournamentDescription => 'Mallonga turnira priskribo'; + + @override + String get broadcastFullDescription => 'Plena eventa priskribo'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Laŭvola longa priskribo de la elsendo. $param1 haveblas. Longeco devas esti malpli ol $param2 literoj.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN Source URL'; + + @override + String get broadcastSourceUrlHelp => 'URL kiun Lichess kontrolos por akiri PGN ĝisdatigojn. Ĝi devas esti publike alirebla en interreto.'; + + @override + String get broadcastSourceGameIds => 'Up to 64 Lichess game IDs, separated by spaces.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Start date in the tournament local timezone: $param'; + } + + @override + String get broadcastStartDateHelp => 'Laŭvola, se vi scias, kiam komenciĝas la evento'; + + @override + String get broadcastCurrentGameUrl => 'Nuna luda URL'; + + @override + String get broadcastDownloadAllRounds => 'Elŝuti ĉiujn raŭndojn'; + + @override + String get broadcastResetRound => 'Restarigi ĉi tiun raŭndon'; + + @override + String get broadcastDeleteRound => 'Forigi ĉi tiun raŭndon'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Sendube forigi la raŭndon kaj ĉiujn ĝiajn ludojn.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Forigi ĉiujn ludojn de ĉi tiu raŭndo. La fonto devos esti aktiva por rekrei ilin.'; + + @override + String get broadcastEditRoundStudy => 'Redakti raŭndan studon'; + + @override + String get broadcastDeleteTournament => 'Forigi ĉi tiun turniron'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Sendube forigi la tuta turniro, kaj ĝiajn raŭndojn kaj ĉiujn ĝiajn ludojn.'; + + @override + String get broadcastShowScores => 'Show players scores based on game results'; + + @override + String get broadcastReplacePlayerTags => 'Optional: replace player names, ratings and titles'; + + @override + String get broadcastFideFederations => 'FIDE federations'; + + @override + String get broadcastTop10Rating => 'Top 10 rating'; + + @override + String get broadcastFidePlayers => 'FIDE players'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE player not found'; + + @override + String get broadcastFideProfile => 'FIDE profile'; + + @override + String get broadcastFederation => 'Federation'; + + @override + String get broadcastAgeThisYear => 'Age this year'; + + @override + String get broadcastUnrated => 'Unrated'; + + @override + String get broadcastRecentTournaments => 'Recent tournaments'; + + @override + String get broadcastOpenLichess => 'Open in Lichess'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Overview'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Official website'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'Score'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count elsendoj', + one: '$count elsendo', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Defioj: $param1'; @@ -1390,10 +1615,10 @@ class AppLocalizationsEo extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'La opcioj de la kontraŭulo por moviĝi estas limigitaj, kaj ĉiuj movoj plimalbonigas rian pozicion.'; @override - String get puzzleThemeHealthyMix => 'Sana miksaĵo'; + String get puzzleThemeMix => 'Sana miksaĵo'; @override - String get puzzleThemeHealthyMixDescription => 'Iom de ĉio. Vi ne scias kion atendi, do vi restas preta por io ajn! Same kiel en realaj ludoj.'; + String get puzzleThemeMixDescription => 'Iom de ĉio. Vi ne scias kion atendi, do vi restas preta por io ajn! Same kiel en realaj ludoj.'; @override String get puzzleThemePlayerGames => 'Ludantaj ludoj'; @@ -1797,9 +2022,6 @@ class AppLocalizationsEo extends AppLocalizations { @override String get removesTheDepthLimit => 'Forigas la profundlimon, kaj tenas vian komputilon varma'; - @override - String get engineManager => 'Motora administranto'; - @override String get blunder => 'Erarego'; @@ -2063,6 +2285,9 @@ class AppLocalizationsEo extends AppLocalizations { @override String get gamesPlayed => 'Luditaj ludoj'; + @override + String get ok => 'OK'; + @override String get cancel => 'Nuligi'; @@ -2772,7 +2997,13 @@ class AppLocalizationsEo extends AppLocalizations { String get other => 'Io alia'; @override - String get reportDescriptionHelp => 'Inkluzivu la ligilon al la ludo(j) kaj ekspliku tion, kio malbonas pri la konduto de ĉi tiu uzanto.'; + String get reportCheatBoostHelp => 'Paste the link to the game(s) and explain what is wrong about this user\'s behaviour. Don\'t just say \"they cheat\", but tell us how you came to this conclusion.'; + + @override + String get reportUsernameHelp => 'Explain what about this username is offensive. Don\'t just say \"it\'s offensive/inappropriate\", but tell us how you came to this conclusion, especially if the insult is obfuscated, not in english, is in slang, or is a historical/cultural reference.'; + + @override + String get reportProcessedFasterInEnglish => 'Your report will be processed faster if written in English.'; @override String get error_provideOneCheatedGameLink => 'Bonvolu doni almenaŭ unu ligilon al ludo en kiu oni friponis.'; @@ -4077,6 +4308,9 @@ class AppLocalizationsEo extends AppLocalizations { @override String get nothingToSeeHere => 'Nenio videbla ĉi tie nuntempe.'; + @override + String get stats => 'Stats'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4723,9 +4957,514 @@ class AppLocalizationsEo extends AppLocalizations { @override String get streamerLichessStreamers => 'Lichess filmprezentistoj'; + @override + String get studyPrivate => 'Privata'; + + @override + String get studyMyStudies => 'Miaj studoj'; + + @override + String get studyStudiesIContributeTo => 'Studoj en kiuj mi kontribuas'; + + @override + String get studyMyPublicStudies => 'Miaj publikaj studoj'; + + @override + String get studyMyPrivateStudies => 'Miaj privataj studoj'; + + @override + String get studyMyFavoriteStudies => 'Miaj preferataj studoj'; + + @override + String get studyWhatAreStudies => 'Kio estas la studoj?'; + + @override + String get studyAllStudies => 'Ĉiuj studoj'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Studoj kreitaj de $param'; + } + + @override + String get studyNoneYet => 'Neniu ankoraŭ.'; + + @override + String get studyHot => 'Tendenca'; + + @override + String get studyDateAddedNewest => 'Dato aldonita (plej novaj)'; + + @override + String get studyDateAddedOldest => 'Dato aldonita (plej malnovaj)'; + + @override + String get studyRecentlyUpdated => 'Lastatempe ĝisdatigita'; + + @override + String get studyMostPopular => 'Plej popularaj'; + + @override + String get studyAlphabetical => 'Alfabete'; + + @override + String get studyAddNewChapter => 'Aldoni novan ĉapitron'; + + @override + String get studyAddMembers => 'Aldoni membrojn'; + + @override + String get studyInviteToTheStudy => 'Inviti al la studo'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Bonvolu inviti nur homojn, kiujn vi konas kaj kiuj aktive volas aliĝi al tiu ĉi studo.'; + + @override + String get studySearchByUsername => 'Serĉi laŭ uzantnomo'; + + @override + String get studySpectator => 'Spektanto'; + + @override + String get studyContributor => 'Kontribuanto'; + + @override + String get studyKick => 'Forpuŝi'; + + @override + String get studyLeaveTheStudy => 'Forlasi la studon'; + + @override + String get studyYouAreNowAContributor => 'Nun vi estas kunlaboranto'; + + @override + String get studyYouAreNowASpectator => 'Nun vi estas spektanto'; + + @override + String get studyPgnTags => 'PGN etikedoj'; + + @override + String get studyLike => 'Ŝati'; + + @override + String get studyUnlike => 'Malŝati'; + + @override + String get studyNewTag => 'Nova etikedo'; + + @override + String get studyCommentThisPosition => 'Komenti tiun posicion'; + + @override + String get studyCommentThisMove => 'Komenti tiun movon'; + + @override + String get studyAnnotateWithGlyphs => 'Komenti per signobildo'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'La ĉapitro estas tro mallonga por esti analizita.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Nur la kontribuantoj de la studo povas peti komputilan analizon.'; + + @override + String get studyGetAFullComputerAnalysis => 'Akiru kompletan servilan komputilan analizon de la ĉefa linio.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Certiĝu, ke la ĉapitro estas kompleta. Vi nur povas peti analizon unu foje.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Ĉiuj sinkronigitaj membroj restas ĉe la sama pozicio'; + + @override + String get studyShareChanges => 'Diskonigi ŝanĝojn al spektantoj kaj konservi tiujn ĉe la servilo'; + + @override + String get studyPlaying => 'Ludanta'; + + @override + String get studyShowEvalBar => 'Taksaj stangoj'; + + @override + String get studyFirst => 'Al la unua'; + + @override + String get studyPrevious => 'Antaŭa'; + + @override + String get studyNext => 'Sekva'; + + @override + String get studyLast => 'Al la lasta'; + @override String get studyShareAndExport => 'Konigi & eksporti'; + @override + String get studyCloneStudy => 'Kloni'; + + @override + String get studyStudyPgn => 'PGN de la studo'; + + @override + String get studyDownloadAllGames => 'Elŝuti ĉiujn ludojn'; + + @override + String get studyChapterPgn => 'PGN de la ĉapitro'; + + @override + String get studyCopyChapterPgn => 'Kopii PGN'; + + @override + String get studyDownloadGame => 'Elŝuti ludon'; + + @override + String get studyStudyUrl => 'URL de la studo'; + + @override + String get studyCurrentChapterUrl => 'URL de tiu ĉi ĉapitro'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Vi povas alglui ĉi tiun en la forumo aŭ via Lichess blogo por enkorpigi'; + + @override + String get studyStartAtInitialPosition => 'Starti ekde komenca pozicio'; + + @override + String studyStartAtX(String param) { + return 'Komenci je $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Enkorpigi en via retejo'; + + @override + String get studyReadMoreAboutEmbedding => 'Legi pli pri enkorpigo'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Nur publikaj studoj eblas enkorpiĝi!'; + + @override + String get studyOpen => 'Malfermi'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1, provizia al vi de $param2'; + } + + @override + String get studyStudyNotFound => 'Studo ne trovita'; + + @override + String get studyEditChapter => 'Redakti ĉapitron'; + + @override + String get studyNewChapter => 'Nova ĉapitro'; + + @override + String studyImportFromChapterX(String param) { + return 'Importi el $param'; + } + + @override + String get studyOrientation => 'Orientiĝo'; + + @override + String get studyAnalysisMode => 'Analiza modo'; + + @override + String get studyPinnedChapterComment => 'Alpinglita ĉapitra komento'; + + @override + String get studySaveChapter => 'Konservi ĉapitron'; + + @override + String get studyClearAnnotations => 'Forigi notojn'; + + @override + String get studyClearVariations => 'Forigi variaĵojn'; + + @override + String get studyDeleteChapter => 'Forigi ĉapitron'; + + @override + String get studyDeleteThisChapter => 'Ĉu forigi ĉi tiun ĉapitron? Tiun agon vi ne povos malfari!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Forigi ĉiujn komentojn, signobildoj, kaj skribintaj formoj en ĉi tiu ĉapitro'; + + @override + String get studyRightUnderTheBoard => 'Ĝuste sub la tabulo'; + + @override + String get studyNoPinnedComment => 'Neniu'; + + @override + String get studyNormalAnalysis => 'Normala analizo'; + + @override + String get studyHideNextMoves => 'Kaŝi la sekvajn movojn'; + + @override + String get studyInteractiveLesson => 'Interaga leciono'; + + @override + String studyChapterX(String param) { + return 'Ĉapitro $param'; + } + + @override + String get studyEmpty => 'Malplena'; + + @override + String get studyStartFromInitialPosition => 'Starti el la komenca pozicio'; + + @override + String get studyEditor => 'Redaktanto'; + + @override + String get studyStartFromCustomPosition => 'Starti el propra pozicio'; + + @override + String get studyLoadAGameByUrl => 'Ŝarĝi ludon el URL'; + + @override + String get studyLoadAPositionFromFen => 'Ŝarĝi posicion el FEN kodo'; + + @override + String get studyLoadAGameFromPgn => 'Ŝarĝi ludon el PGN'; + + @override + String get studyAutomatic => 'Aŭtomata'; + + @override + String get studyUrlOfTheGame => 'URL de la ludo'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Ŝarĝu ludon el $param1 aŭ $param2'; + } + + @override + String get studyCreateChapter => 'Krei ĉapitron'; + + @override + String get studyCreateStudy => 'Krei studon'; + + @override + String get studyEditStudy => 'Redakti studon'; + + @override + String get studyVisibility => 'Videbleco'; + + @override + String get studyPublic => 'Publika'; + + @override + String get studyUnlisted => 'Nelistigita'; + + @override + String get studyInviteOnly => 'Per invito'; + + @override + String get studyAllowCloning => 'Permesi klonadon'; + + @override + String get studyNobody => 'Neniu'; + + @override + String get studyOnlyMe => 'Nur mi'; + + @override + String get studyContributors => 'Kontribuantoj'; + + @override + String get studyMembers => 'Membroj'; + + @override + String get studyEveryone => 'Ĉiuj'; + + @override + String get studyEnableSync => 'Ebligi sinkronigon'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Jes: ĉiuj vidas la saman pozicion'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Ne: lasu homojn esplori libere'; + + @override + String get studyPinnedStudyComment => 'Komento alpinglita al la studo'; + @override String get studyStart => 'Komenci'; + + @override + String get studySave => 'Konservi'; + + @override + String get studyClearChat => 'Vakigi babiladon'; + + @override + String get studyDeleteTheStudyChatHistory => 'Ĉu forigi la historian babilejon de la ĉapitro? Tiun agon vi ne povos malfari!'; + + @override + String get studyDeleteStudy => 'Forigi studon'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Ĉu forigi la tuta studo? Ne estas reiro! Tajpi la nomon de la studo por konfirmi: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Kie vi volas studi tion?'; + + @override + String get studyGoodMove => 'Bona movo'; + + @override + String get studyMistake => 'Eraro'; + + @override + String get studyBrilliantMove => 'Brilianta movo'; + + @override + String get studyBlunder => 'Erarego'; + + @override + String get studyInterestingMove => 'Interesa movo'; + + @override + String get studyDubiousMove => 'Dubinda movo'; + + @override + String get studyOnlyMove => 'Nura movo'; + + @override + String get studyZugzwang => 'Movdevigo'; + + @override + String get studyEqualPosition => 'Egala aranĝo'; + + @override + String get studyUnclearPosition => 'Malklara aranĝo'; + + @override + String get studyWhiteIsSlightlyBetter => 'Blanko estas iomete pli bona'; + + @override + String get studyBlackIsSlightlyBetter => 'Nigro estas iomete pli bona'; + + @override + String get studyWhiteIsBetter => 'Blanko estas pli bona'; + + @override + String get studyBlackIsBetter => 'Nigro estas pli bona'; + + @override + String get studyWhiteIsWinning => 'Blanko estas gajnanta'; + + @override + String get studyBlackIsWinning => 'Nigro estas gajnanta'; + + @override + String get studyNovelty => 'Novaĵo'; + + @override + String get studyDevelopment => 'Programado'; + + @override + String get studyInitiative => 'Iniciato'; + + @override + String get studyAttack => 'Atako'; + + @override + String get studyCounterplay => 'Kontraŭludo'; + + @override + String get studyTimeTrouble => 'Tempa ĝeno'; + + @override + String get studyWithCompensation => 'Kun kompenso'; + + @override + String get studyWithTheIdea => 'Kun la ideo'; + + @override + String get studyNextChapter => 'Sekva ĉapitro'; + + @override + String get studyPrevChapter => 'Antaŭa ĉapitro'; + + @override + String get studyStudyActions => 'Studaj agoj'; + + @override + String get studyTopics => 'Temoj'; + + @override + String get studyMyTopics => 'Miaj temoj'; + + @override + String get studyPopularTopics => 'Popularaj temoj'; + + @override + String get studyManageTopics => 'Administri temojn'; + + @override + String get studyBack => 'Reen'; + + @override + String get studyPlayAgain => 'Reludi'; + + @override + String get studyWhatWouldYouPlay => 'Kion vi ludus en ĉi tiu pozicio?'; + + @override + String get studyYouCompletedThisLesson => 'Gratulon! Vi kompletigis la lecionon.'; + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Ĉapitroj', + one: '$count Ĉapitro', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Ludoj', + one: '$count Ludo', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Membroj', + one: '$count Membro', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Algluu ĉi tie vian PGN kodon, ĝis maksimume $count ludoj', + one: 'Algluu ĉi tie vian PGN kodon, maksimume ĝis $count ludo', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_es.dart b/lib/l10n/l10n_es.dart index 7f65a608d6..a5e5bff96b 100644 --- a/lib/l10n/l10n_es.dart +++ b/lib/l10n/l10n_es.dart @@ -103,9 +103,6 @@ class AppLocalizationsEs extends AppLocalizations { @override String get mobileCancelTakebackOffer => 'Cancelar oferta de deshacer movimiento'; - @override - String get mobileCancelDrawOffer => 'Cancelar ofertas de tablas'; - @override String get mobileWaitingForOpponentToJoin => 'Esperando a que se una un oponente...'; @@ -142,7 +139,7 @@ class AppLocalizationsEs extends AppLocalizations { String get mobileGreetingWithoutName => 'Hola'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobilePrefMagnifyDraggedPiece => 'Aumentar la pieza arrastrada'; @override String get activityActivity => 'Actividad'; @@ -246,6 +243,17 @@ class AppLocalizationsEs extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Ha completado $count $param2 partidas por correspondencia', + one: 'Ha completado $count $param2 partida por correspondencia', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,226 @@ class AppLocalizationsEs extends AppLocalizations { @override String get broadcastBroadcasts => 'Emisiones'; + @override + String get broadcastMyBroadcasts => 'Mis transmisiones'; + @override String get broadcastLiveBroadcasts => 'Emisiones de torneos en directo'; + @override + String get broadcastBroadcastCalendar => 'Calendario de transmisiones'; + + @override + String get broadcastNewBroadcast => 'Nueva emisión en directo'; + + @override + String get broadcastSubscribedBroadcasts => 'Transmisiones suscritas'; + + @override + String get broadcastAboutBroadcasts => 'Acerca de las transmisiones'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Como utilizar las transmisiones de Lichess.'; + + @override + String get broadcastTheNewRoundHelp => 'La nueva ronda tendrá los mismos miembros y contribuyentes que la anterior.'; + + @override + String get broadcastAddRound => 'Añadir una ronda'; + + @override + String get broadcastOngoing => 'En curso'; + + @override + String get broadcastUpcoming => 'Próximamente'; + + @override + String get broadcastCompleted => 'Completadas'; + + @override + String get broadcastCompletedHelp => 'Lichess detecta la terminación de la ronda según las partidas de origen. Usa este interruptor si no hay ninguna.'; + + @override + String get broadcastRoundName => 'Nombre de la ronda'; + + @override + String get broadcastRoundNumber => 'Número de ronda'; + + @override + String get broadcastTournamentName => 'Nombre del torneo'; + + @override + String get broadcastTournamentDescription => 'Breve descripción del torneo'; + + @override + String get broadcastFullDescription => 'Descripción completa del evento'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Descripción larga opcional de la emisión. $param1 está disponible. La longitud debe ser inferior a $param2 caracteres.'; + } + + @override + String get broadcastSourceSingleUrl => 'URL origen del archivo PGN'; + + @override + String get broadcastSourceUrlHelp => 'URL que Lichess comprobará para obtener actualizaciones PGN. Debe ser públicamente accesible desde Internet.'; + + @override + String get broadcastSourceGameIds => 'Hasta 64 identificadores de partidas de Lichess, separados por espacios.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Fecha de inicio en la zona horaria local del torneo: $param'; + } + + @override + String get broadcastStartDateHelp => 'Opcional, si sabes cuando comienza el evento'; + + @override + String get broadcastCurrentGameUrl => 'Enlace de la partida actual'; + + @override + String get broadcastDownloadAllRounds => 'Descargar todas las rondas'; + + @override + String get broadcastResetRound => 'Restablecer esta ronda'; + + @override + String get broadcastDeleteRound => 'Eliminar esta ronda'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Eliminar definitivamente la ronda y sus partidas.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Eliminar todas las partidas de esta ronda. La fuente tendrá que estar activa para volver a crearlos.'; + + @override + String get broadcastEditRoundStudy => 'Editar estudio de ronda'; + + @override + String get broadcastDeleteTournament => 'Elimina este torneo'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Elimina definitivamente todo el torneo, rondas y partidas incluidas.'; + + @override + String get broadcastShowScores => 'Mostrar las puntuaciones de los jugadores según los resultados de las partidas'; + + @override + String get broadcastReplacePlayerTags => 'Opcional: reemplazar nombres de jugadores, puntuaciones y títulos'; + + @override + String get broadcastFideFederations => 'Federaciones FIDE'; + + @override + String get broadcastTop10Rating => 'Los 10 mejores'; + + @override + String get broadcastFidePlayers => 'Jugadores FIDE'; + + @override + String get broadcastFidePlayerNotFound => 'Jugador FIDE no encontrado'; + + @override + String get broadcastFideProfile => 'Perfil FIDE'; + + @override + String get broadcastFederation => 'Federación'; + + @override + String get broadcastAgeThisYear => 'Edad actual'; + + @override + String get broadcastUnrated => 'Sin puntuación'; + + @override + String get broadcastRecentTournaments => 'Torneos recientes'; + + @override + String get broadcastOpenLichess => 'Abrir en Lichess'; + + @override + String get broadcastTeams => 'Equipos'; + + @override + String get broadcastBoards => 'Tableros'; + + @override + String get broadcastOverview => 'Resumen'; + + @override + String get broadcastSubscribeTitle => 'Suscríbete para ser notificado cuando comience cada ronda. Puedes alternar entre notificaciones de campana o de dispositivo para emisiones en las preferencias de tu cuenta.'; + + @override + String get broadcastUploadImage => 'Subir imagen del torneo'; + + @override + String get broadcastNoBoardsYet => 'Aún no hay tableros. Estos aparecerán una vez se suban las partidas.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Los tableros pueden cargarse gracias a una fuente o a través de $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Comienza en $param'; + } + + @override + String get broadcastStartVerySoon => 'La transmisión comenzará muy pronto.'; + + @override + String get broadcastNotYetStarted => 'La transmisión aún no ha comenzado.'; + + @override + String get broadcastOfficialWebsite => 'Sitio oficial'; + + @override + String get broadcastStandings => 'Clasificación'; + + @override + String broadcastIframeHelp(String param) { + return 'Más opciones en $param'; + } + + @override + String get broadcastWebmastersPage => 'la página del webmaster'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'Una fuente PGN pública en tiempo real para esta ronda. También ofrecemos $param para una sincronización más rápida y eficiente.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Inserta esta transmisión en tu sitio web'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Inserta la $param en tu sitio web'; + } + + @override + String get broadcastRatingDiff => 'Diferencia de valoración'; + + @override + String get broadcastGamesThisTournament => 'Partidas en este torneo'; + + @override + String get broadcastScore => 'Resultado'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count retransmisiones', + one: '$count retransmisión', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Desafíos: $param1'; @@ -1390,10 +1615,10 @@ class AppLocalizationsEs extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'El oponente está limitado en los movimientos que puede realizar, y todos los movimientos empeoran su posición.'; @override - String get puzzleThemeHealthyMix => 'Mezcla equilibrada'; + String get puzzleThemeMix => 'Mezcla equilibrada'; @override - String get puzzleThemeHealthyMixDescription => 'Un poco de todo. No sabes lo que te espera, así que estate listo para cualquier cosa, como en las partidas reales.'; + String get puzzleThemeMixDescription => 'Un poco de todo. No sabes lo que te espera, así que estate listo para cualquier cosa, como en las partidas reales.'; @override String get puzzleThemePlayerGames => 'Partidas de jugadores'; @@ -1797,9 +2022,6 @@ class AppLocalizationsEs extends AppLocalizations { @override String get removesTheDepthLimit => 'Elimina el límite de profundidad del análisis y hace trabajar a tu ordenador'; - @override - String get engineManager => 'Gestor de motores'; - @override String get blunder => 'Error grave'; @@ -2063,6 +2285,9 @@ class AppLocalizationsEs extends AppLocalizations { @override String get gamesPlayed => 'Partidas jugadas'; + @override + String get ok => 'Aceptar'; + @override String get cancel => 'Cancelar'; @@ -2772,7 +2997,13 @@ class AppLocalizationsEs extends AppLocalizations { String get other => 'Otro'; @override - String get reportDescriptionHelp => 'Pega el enlace a la(s) partida(s) y explícanos qué hay de malo en el comportamiento de este usuario. No digas simplemente \"hace trampa\"; explícanos cómo has llegado a esta conclusión. Tu informe será procesado más rápido si está escrito en inglés.'; + String get reportCheatBoostHelp => 'Pega el enlace a la(s) partida(s) y explícanos qué hay de malo en el comportamiento de este usuario. No digas simplemente \"hace trampa\", sino cómo has llegado a esta conclusión.'; + + @override + String get reportUsernameHelp => 'Explica qué es lo que te resulta ofensivo de este nombre de usuario. No digas solo \"es ofensivo\" o \"inapropiado\", sino cómo llegaste a esta conclusión, sobre todo si el insulto no es tan obvio, no está en inglés, es jerga o una referencia histórica o cultural.'; + + @override + String get reportProcessedFasterInEnglish => 'Tu informe será procesado más rápido si está escrito en inglés.'; @override String get error_provideOneCheatedGameLink => 'Por favor, proporciona al menos un enlace a una partida en la que se hicieron trampas.'; @@ -4077,6 +4308,9 @@ class AppLocalizationsEs extends AppLocalizations { @override String get nothingToSeeHere => 'Nada que ver aquí por ahora.'; + @override + String get stats => 'Estadísticas'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4160,7 +4394,7 @@ class AppLocalizationsEs extends AppLocalizations { count, locale: localeName, other: 'Puntuación de $count en $param2 partidas', - one: 'puntuación $count en $param2 partida', + one: 'Puntuación $count en $param2 partida', ); return '$_temp0'; } @@ -4723,9 +4957,514 @@ class AppLocalizationsEs extends AppLocalizations { @override String get streamerLichessStreamers => 'Presentadores de Lichess'; + @override + String get studyPrivate => 'Privado'; + + @override + String get studyMyStudies => 'Mis estudios'; + + @override + String get studyStudiesIContributeTo => 'Estudios en los que colaboro'; + + @override + String get studyMyPublicStudies => 'Mis estudios públicos'; + + @override + String get studyMyPrivateStudies => 'Mis estudios privados'; + + @override + String get studyMyFavoriteStudies => 'Mis estudios favoritos'; + + @override + String get studyWhatAreStudies => '¿Qué son los estudios?'; + + @override + String get studyAllStudies => 'Todos los estudios'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Estudios creados por $param'; + } + + @override + String get studyNoneYet => 'Ninguno por ahora.'; + + @override + String get studyHot => 'De interés actualmente'; + + @override + String get studyDateAddedNewest => 'Fecha (más recientes)'; + + @override + String get studyDateAddedOldest => 'Fecha (más antiguos)'; + + @override + String get studyRecentlyUpdated => 'Actualizados recientemente'; + + @override + String get studyMostPopular => 'Más populares'; + + @override + String get studyAlphabetical => 'Alfabético'; + + @override + String get studyAddNewChapter => 'Añadir nuevo capítulo'; + + @override + String get studyAddMembers => 'Añadir miembros'; + + @override + String get studyInviteToTheStudy => 'Invitar al estudio'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Por favor, invita sólo a personas que conozcas y que deseen unirse a este estudio.'; + + @override + String get studySearchByUsername => 'Buscar por nombre de usuario'; + + @override + String get studySpectator => 'Espectador'; + + @override + String get studyContributor => 'Colaborador'; + + @override + String get studyKick => 'Expulsar'; + + @override + String get studyLeaveTheStudy => 'Dejar el estudio'; + + @override + String get studyYouAreNowAContributor => 'Ahora eres un colaborador'; + + @override + String get studyYouAreNowASpectator => 'Ahora eres un espectador'; + + @override + String get studyPgnTags => 'Etiquetas PGN'; + + @override + String get studyLike => 'Me gusta'; + + @override + String get studyUnlike => 'No me gusta'; + + @override + String get studyNewTag => 'Nueva etiqueta'; + + @override + String get studyCommentThisPosition => 'Comentar esta posición'; + + @override + String get studyCommentThisMove => 'Comentar este movimiento'; + + @override + String get studyAnnotateWithGlyphs => 'Anotar con iconos'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'El capítulo es demasiado corto para analizarlo.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Sólo los colaboradores del estudio pueden solicitar un análisis por ordenador.'; + + @override + String get studyGetAFullComputerAnalysis => 'Obtén un análisis completo de la línea principal en el servidor.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Asegúrate de que el capítulo está completo. Sólo puede solicitar el análisis una vez.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Todos los miembros de SYNC permanecen en la misma posición'; + + @override + String get studyShareChanges => 'Comparte cambios con los espectadores y guárdalos en el servidor'; + + @override + String get studyPlaying => 'Jugando'; + + @override + String get studyShowEvalBar => 'Barras de evaluación'; + + @override + String get studyFirst => 'Primero'; + + @override + String get studyPrevious => 'Anterior'; + + @override + String get studyNext => 'Siguiente'; + + @override + String get studyLast => 'Último'; + @override String get studyShareAndExport => 'Compartir y exportar'; + @override + String get studyCloneStudy => 'Clonar'; + + @override + String get studyStudyPgn => 'PGN del estudio'; + + @override + String get studyDownloadAllGames => 'Descargar todas las partidas'; + + @override + String get studyChapterPgn => 'PGN del capítulo'; + + @override + String get studyCopyChapterPgn => 'Copiar PGN'; + + @override + String get studyDownloadGame => 'Descargar partida'; + + @override + String get studyStudyUrl => 'URL del estudio'; + + @override + String get studyCurrentChapterUrl => 'URL del capítulo actual'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Puedes pegar esto en el foro para insertar la partida'; + + @override + String get studyStartAtInitialPosition => 'Comenzar desde la posición inicial'; + + @override + String studyStartAtX(String param) { + return 'Comenzar en $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Insértalo en tu página o blog'; + + @override + String get studyReadMoreAboutEmbedding => 'Leer más sobre insertar contenido'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => '¡Solo los estudios públicos pueden ser insertados!'; + + @override + String get studyOpen => 'Abrir'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1, proporcionado por $param2'; + } + + @override + String get studyStudyNotFound => 'No se encontró el estudio'; + + @override + String get studyEditChapter => 'Editar capítulo'; + + @override + String get studyNewChapter => 'Capítulo nuevo'; + + @override + String studyImportFromChapterX(String param) { + return 'Importar de $param'; + } + + @override + String get studyOrientation => 'Orientación'; + + @override + String get studyAnalysisMode => 'Modo de análisis'; + + @override + String get studyPinnedChapterComment => 'Comentario fijo para el capítulo'; + + @override + String get studySaveChapter => 'Guardar capítulo'; + + @override + String get studyClearAnnotations => 'Borrar anotaciones'; + + @override + String get studyClearVariations => 'Borrar variantes'; + + @override + String get studyDeleteChapter => 'Borrar capítulo'; + + @override + String get studyDeleteThisChapter => '¿Realmente quieres borrar el capítulo? ¡Esta acción no se puede deshacer!'; + + @override + String get studyClearAllCommentsInThisChapter => '¿Borrar todos los comentarios, iconos y marcas de este capítulo?'; + + @override + String get studyRightUnderTheBoard => 'Justo debajo del tablero'; + + @override + String get studyNoPinnedComment => 'Ninguno'; + + @override + String get studyNormalAnalysis => 'Análisis normal'; + + @override + String get studyHideNextMoves => 'Ocultar los siguientes movimientos'; + + @override + String get studyInteractiveLesson => 'Lección interactiva'; + + @override + String studyChapterX(String param) { + return 'Capítulo $param'; + } + + @override + String get studyEmpty => 'Vacío'; + + @override + String get studyStartFromInitialPosition => 'Comenzar desde la posición inicial'; + + @override + String get studyEditor => 'Editor'; + + @override + String get studyStartFromCustomPosition => 'Comenzar desde una posición personalizada'; + + @override + String get studyLoadAGameByUrl => 'Cargar una partida desde una URL'; + + @override + String get studyLoadAPositionFromFen => 'Cargar una posición vía código FEN'; + + @override + String get studyLoadAGameFromPgn => 'Cargar una partida vía código PGN'; + + @override + String get studyAutomatic => 'Automática'; + + @override + String get studyUrlOfTheGame => 'URL de la partida'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Cargar una partida desde $param1 o $param2'; + } + + @override + String get studyCreateChapter => 'Crear capítulo'; + + @override + String get studyCreateStudy => 'Crear estudio'; + + @override + String get studyEditStudy => 'Editar estudio'; + + @override + String get studyVisibility => 'Visibilidad'; + + @override + String get studyPublic => 'Público'; + + @override + String get studyUnlisted => 'Sin listar'; + + @override + String get studyInviteOnly => 'Acceso mediante invitación'; + + @override + String get studyAllowCloning => 'Permitir clonado'; + + @override + String get studyNobody => 'Nadie'; + + @override + String get studyOnlyMe => 'Sólo yo'; + + @override + String get studyContributors => 'Colaboradores'; + + @override + String get studyMembers => 'Miembros'; + + @override + String get studyEveryone => 'Todo el mundo'; + + @override + String get studyEnableSync => 'Habilitar sincronización'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Sí: todo el mundo ve la misma posición'; + + @override + String get studyNoLetPeopleBrowseFreely => 'No: permitir que la gente navegue libremente'; + + @override + String get studyPinnedStudyComment => 'Comentario fijado del estudio'; + @override String get studyStart => 'Comenzar'; + + @override + String get studySave => 'Guardar'; + + @override + String get studyClearChat => 'Limpiar el chat'; + + @override + String get studyDeleteTheStudyChatHistory => '¿Realmente quieres borrar el historial de chat? ¡Esta acción no se puede deshacer!'; + + @override + String get studyDeleteStudy => 'Borrar estudio'; + + @override + String studyConfirmDeleteStudy(String param) { + return '¿Seguro que quieres eliminar el estudio? Ten en cuenta que esta acción no se puede deshacer. Para confirmar, escribe el nombre del estudio: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => '¿Dónde quieres estudiar eso?'; + + @override + String get studyGoodMove => 'Jugada buena'; + + @override + String get studyMistake => 'Error'; + + @override + String get studyBrilliantMove => 'Jugada muy buena'; + + @override + String get studyBlunder => 'Error grave'; + + @override + String get studyInterestingMove => 'Jugada interesante'; + + @override + String get studyDubiousMove => 'Jugada dudosa'; + + @override + String get studyOnlyMove => 'Movimiento único'; + + @override + String get studyZugzwang => 'Zugzwang'; + + @override + String get studyEqualPosition => 'Posición igualada'; + + @override + String get studyUnclearPosition => 'Posición poco clara'; + + @override + String get studyWhiteIsSlightlyBetter => 'Las blancas están ligeramente mejor'; + + @override + String get studyBlackIsSlightlyBetter => 'Las negras están ligeramente mejor'; + + @override + String get studyWhiteIsBetter => 'Las blancas están mejor'; + + @override + String get studyBlackIsBetter => 'Las negras están mejor'; + + @override + String get studyWhiteIsWinning => 'Las blancas están ganando'; + + @override + String get studyBlackIsWinning => 'Las negras están ganando'; + + @override + String get studyNovelty => 'Novedad'; + + @override + String get studyDevelopment => 'Desarrollo'; + + @override + String get studyInitiative => 'Iniciativa'; + + @override + String get studyAttack => 'Ataque'; + + @override + String get studyCounterplay => 'Contrajuego'; + + @override + String get studyTimeTrouble => 'Problema de tiempo'; + + @override + String get studyWithCompensation => 'Con compensación'; + + @override + String get studyWithTheIdea => 'Con la idea'; + + @override + String get studyNextChapter => 'Capítulo siguiente'; + + @override + String get studyPrevChapter => 'Capítulo anterior'; + + @override + String get studyStudyActions => 'Acciones de estudio'; + + @override + String get studyTopics => 'Temas'; + + @override + String get studyMyTopics => 'Mis temas'; + + @override + String get studyPopularTopics => 'Temas populares'; + + @override + String get studyManageTopics => 'Administrar temas'; + + @override + String get studyBack => 'Volver'; + + @override + String get studyPlayAgain => 'Jugar de nuevo'; + + @override + String get studyWhatWouldYouPlay => '¿Qué jugarías en esta posición?'; + + @override + String get studyYouCompletedThisLesson => '¡Felicidades! Has completado esta lección.'; + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Capítulos', + one: '$count Capítulo', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Partidas', + one: '$count Partida', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Miembros', + one: '$count Miembro', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Pega aquí el código PGN, $count partidas como máximo', + one: 'Pega aquí el código PGN, $count partida como máximo', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_et.dart b/lib/l10n/l10n_et.dart index fe1ec9d9f3..892e85dded 100644 --- a/lib/l10n/l10n_et.dart +++ b/lib/l10n/l10n_et.dart @@ -103,9 +103,6 @@ class AppLocalizationsEt extends AppLocalizations { @override String get mobileCancelTakebackOffer => 'Cancel takeback offer'; - @override - String get mobileCancelDrawOffer => 'Cancel draw offer'; - @override String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @@ -246,6 +243,17 @@ class AppLocalizationsEt extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Completed $count $param2 correspondence games', + one: 'Completed $count $param2 correspondence game', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,226 @@ class AppLocalizationsEt extends AppLocalizations { @override String get broadcastBroadcasts => 'Otseülekanded'; + @override + String get broadcastMyBroadcasts => 'My broadcasts'; + @override String get broadcastLiveBroadcasts => 'Otseülekanded turniirilt'; + @override + String get broadcastBroadcastCalendar => 'Broadcast calendar'; + + @override + String get broadcastNewBroadcast => 'Uus otseülekanne'; + + @override + String get broadcastSubscribedBroadcasts => 'Subscribed broadcasts'; + + @override + String get broadcastAboutBroadcasts => 'About broadcasts'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'How to use Lichess Broadcasts.'; + + @override + String get broadcastTheNewRoundHelp => 'The new round will have the same members and contributors as the previous one.'; + + @override + String get broadcastAddRound => 'Lisa voor'; + + @override + String get broadcastOngoing => 'Käimas'; + + @override + String get broadcastUpcoming => 'Tulemas'; + + @override + String get broadcastCompleted => 'Lõppenud'; + + @override + String get broadcastCompletedHelp => 'Lichess detects round completion, but can get it wrong. Use this to set it manually.'; + + @override + String get broadcastRoundName => 'Vooru nimi'; + + @override + String get broadcastRoundNumber => 'Vooru number'; + + @override + String get broadcastTournamentName => 'Turniiri nimi'; + + @override + String get broadcastTournamentDescription => 'Lühike turniiri kirjeldus'; + + @override + String get broadcastFullDescription => 'Sündmuse täielik kirjeldus'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Valikuline otseülekande kirjeldus. $param1 on saadaval. Pikkus peab olema maksimaalselt $param2 tähemärki.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN Source URL'; + + @override + String get broadcastSourceUrlHelp => 'URL, kust Lichess saab PGN-i värskenduse. See peab olema Internetist kättesaadav.'; + + @override + String get broadcastSourceGameIds => 'Up to 64 Lichess game IDs, separated by spaces.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Start date in the tournament local timezone: $param'; + } + + @override + String get broadcastStartDateHelp => 'Valikuline, kui tead millal sündmus algab'; + + @override + String get broadcastCurrentGameUrl => 'Praeguse mängu URL'; + + @override + String get broadcastDownloadAllRounds => 'Lae alla kõik voorud'; + + @override + String get broadcastResetRound => 'Lähtesta see voor'; + + @override + String get broadcastDeleteRound => 'Kustuta see voor'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Kustuta lõplikult voor ja selle mängud.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Kustuta kõik mängud sellest voorust. Allikas peab olema aktiveeritud nende taastamiseks.'; + + @override + String get broadcastEditRoundStudy => 'Edit round study'; + + @override + String get broadcastDeleteTournament => 'Delete this tournament'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Definitively delete the entire tournament, all its rounds and all its games.'; + + @override + String get broadcastShowScores => 'Show players scores based on game results'; + + @override + String get broadcastReplacePlayerTags => 'Optional: replace player names, ratings and titles'; + + @override + String get broadcastFideFederations => 'FIDE federations'; + + @override + String get broadcastTop10Rating => 'Top 10 rating'; + + @override + String get broadcastFidePlayers => 'FIDE players'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE player not found'; + + @override + String get broadcastFideProfile => 'FIDE profile'; + + @override + String get broadcastFederation => 'Federation'; + + @override + String get broadcastAgeThisYear => 'Age this year'; + + @override + String get broadcastUnrated => 'Unrated'; + + @override + String get broadcastRecentTournaments => 'Recent tournaments'; + + @override + String get broadcastOpenLichess => 'Open in Lichess'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Overview'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Official website'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'Score'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count broadcasts', + one: '$count broadcast', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Challenges: $param1'; @@ -1390,10 +1615,10 @@ class AppLocalizationsEt extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Vastasel on piiratud võimalused teha lubatud käike ja kõik halvendavad vastase olukorda.'; @override - String get puzzleThemeHealthyMix => 'Tervislik segu'; + String get puzzleThemeMix => 'Tervislik segu'; @override - String get puzzleThemeHealthyMixDescription => 'Natuke kõike. Kunagi ei tea mida oodata ehk ole valmis kõigeks! Täpselt nagu päris mängudes.'; + String get puzzleThemeMixDescription => 'Natuke kõike. Kunagi ei tea mida oodata ehk ole valmis kõigeks! Täpselt nagu päris mängudes.'; @override String get puzzleThemePlayerGames => 'Player games'; @@ -1797,9 +2022,6 @@ class AppLocalizationsEt extends AppLocalizations { @override String get removesTheDepthLimit => 'Eemaldab sügavuslimiidi ja hoiab arvuti soojana'; - @override - String get engineManager => 'Engine manager'; - @override String get blunder => 'Viga'; @@ -2063,6 +2285,9 @@ class AppLocalizationsEt extends AppLocalizations { @override String get gamesPlayed => 'Mänge mängitud'; + @override + String get ok => 'OK'; + @override String get cancel => 'Katkesta'; @@ -2772,7 +2997,13 @@ class AppLocalizationsEt extends AppLocalizations { String get other => 'Muu'; @override - String get reportDescriptionHelp => 'Kleebi link mängu(de)st ja selgita, mis on valesti selle kasutaja käitumises. Ära ütle lihtsalt \"ta teeb sohki\", vaid seleta, kuidas sa selle järelduseni jõudsid. Sõnum käsitletakse kiiremini kui see on kirjutatud inglise keeles.'; + String get reportCheatBoostHelp => 'Paste the link to the game(s) and explain what is wrong about this user\'s behaviour. Don\'t just say \"they cheat\", but tell us how you came to this conclusion.'; + + @override + String get reportUsernameHelp => 'Explain what about this username is offensive. Don\'t just say \"it\'s offensive/inappropriate\", but tell us how you came to this conclusion, especially if the insult is obfuscated, not in english, is in slang, or is a historical/cultural reference.'; + + @override + String get reportProcessedFasterInEnglish => 'Your report will be processed faster if written in English.'; @override String get error_provideOneCheatedGameLink => 'Palun andke vähemalt üks link pettust sisaldavale mängule.'; @@ -4077,6 +4308,9 @@ class AppLocalizationsEt extends AppLocalizations { @override String get nothingToSeeHere => 'Nothing to see here at the moment.'; + @override + String get stats => 'Stats'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4093,8 +4327,8 @@ class AppLocalizationsEt extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Šahh ja matt $count käiguga', - one: 'Šahh ja Matt $count käiguga', + other: 'Matt $count käiguga', + one: 'Matt $count käiguga', ); return '$_temp0'; } @@ -4723,9 +4957,514 @@ class AppLocalizationsEt extends AppLocalizations { @override String get streamerLichessStreamers => 'Lichessi striimijad'; + @override + String get studyPrivate => 'Privaatne'; + + @override + String get studyMyStudies => 'Minu uuringud'; + + @override + String get studyStudiesIContributeTo => 'Uuringud, milles osalen'; + + @override + String get studyMyPublicStudies => 'Minu avalikud uuringud'; + + @override + String get studyMyPrivateStudies => 'Minu privaatsed uuringud'; + + @override + String get studyMyFavoriteStudies => 'Minu lemmikuuringud'; + + @override + String get studyWhatAreStudies => 'Mis on uuringud?'; + + @override + String get studyAllStudies => 'Kõik uuringud'; + + @override + String studyStudiesCreatedByX(String param) { + return '$param loodud uuringud'; + } + + @override + String get studyNoneYet => 'Veel mitte ühtegi.'; + + @override + String get studyHot => 'Kuum'; + + @override + String get studyDateAddedNewest => 'Lisamisaeg (uusimad)'; + + @override + String get studyDateAddedOldest => 'Lisamisaeg (vanimad)'; + + @override + String get studyRecentlyUpdated => 'Hiljuti uuendatud'; + + @override + String get studyMostPopular => 'Kõige populaarsemad'; + + @override + String get studyAlphabetical => 'Tähestikuline'; + + @override + String get studyAddNewChapter => 'Lisa uus peatükk'; + + @override + String get studyAddMembers => 'Lisa liikmeid'; + + @override + String get studyInviteToTheStudy => 'Kutsu uuringule'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Palun kutsuge ainult inimesi keda te teate ning kes soovivad aktiivselt selle uuringuga liituda.'; + + @override + String get studySearchByUsername => 'Otsi kasutajanime järgi'; + + @override + String get studySpectator => 'Vaatleja'; + + @override + String get studyContributor => 'Panustaja'; + + @override + String get studyKick => 'Viska välja'; + + @override + String get studyLeaveTheStudy => 'Lahku uuringust'; + + @override + String get studyYouAreNowAContributor => 'Te olete nüüd panustaja'; + + @override + String get studyYouAreNowASpectator => 'Te olete nüüd vaatleja'; + + @override + String get studyPgnTags => 'PGN sildid'; + + @override + String get studyLike => 'Meeldib'; + + @override + String get studyUnlike => 'Eemalda meeldimine'; + + @override + String get studyNewTag => 'Uus silt'; + + @override + String get studyCommentThisPosition => 'Kommenteeri seda seisu'; + + @override + String get studyCommentThisMove => 'Kommenteeri seda käiku'; + + @override + String get studyAnnotateWithGlyphs => 'Annoteerige glüüfidega'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'See peatükk on liiga lühike analüüsimiseks.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Ainult selle uuringu panustajad saavad taotleda arvuti analüüsi.'; + + @override + String get studyGetAFullComputerAnalysis => 'Taotle täielikku serveripoolset arvuti analüüsi põhiliinist.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Make sure the chapter is complete. You can only request analysis once.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'All SYNC members remain on the same position'; + + @override + String get studyShareChanges => 'Share changes with spectators and save them on the server'; + + @override + String get studyPlaying => 'Mängimas'; + + @override + String get studyShowEvalBar => 'Evaluation bars'; + + @override + String get studyFirst => 'Esimene'; + + @override + String get studyPrevious => 'Eelmine'; + + @override + String get studyNext => 'Järgmine'; + + @override + String get studyLast => 'Viimane'; + @override String get studyShareAndExport => 'Jaga & ekspordi'; + @override + String get studyCloneStudy => 'Klooni'; + + @override + String get studyStudyPgn => 'Uuringu PGN'; + + @override + String get studyDownloadAllGames => 'Lae alla kõik mängud'; + + @override + String get studyChapterPgn => 'Peatüki PGN'; + + @override + String get studyCopyChapterPgn => 'Kopeeri PGN'; + + @override + String get studyDownloadGame => 'Lae alla mäng'; + + @override + String get studyStudyUrl => 'Uuringu URL'; + + @override + String get studyCurrentChapterUrl => 'Praeguse peatüki URL'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Te saate selle asetada foorumisse või oma Lichessi blogisse sängitamiseks'; + + @override + String get studyStartAtInitialPosition => 'Alusta algseisus'; + + @override + String studyStartAtX(String param) { + return 'Alusta $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Sängita oma veebilehele'; + + @override + String get studyReadMoreAboutEmbedding => 'Loe rohkem sängitamisest'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Ainult avalikud uurimused on sängitatavad!'; + + @override + String get studyOpen => 'Ava'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1, leheküljelt $param2'; + } + + @override + String get studyStudyNotFound => 'Uuringut ei leitud'; + + @override + String get studyEditChapter => 'Muuda peatükki'; + + @override + String get studyNewChapter => 'Uus peatükk'; + + @override + String studyImportFromChapterX(String param) { + return 'Too peatükist $param'; + } + + @override + String get studyOrientation => 'Suund'; + + @override + String get studyAnalysisMode => 'Analüüsirežiim'; + + @override + String get studyPinnedChapterComment => 'Kinnitatud peatüki kommentaar'; + + @override + String get studySaveChapter => 'Salvesta peatükk'; + + @override + String get studyClearAnnotations => 'Eemalda kommentaarid'; + + @override + String get studyClearVariations => 'Eemalda variatsioonid'; + + @override + String get studyDeleteChapter => 'Kustuta peatükk'; + + @override + String get studyDeleteThisChapter => 'Kustuta see peatükk? Seda ei saa tühistada!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Puhasta kõik kommentaarid, glüüfid ja joonistatud kujundid sellest peatükist'; + + @override + String get studyRightUnderTheBoard => 'Otse laua all'; + + @override + String get studyNoPinnedComment => 'Puudub'; + + @override + String get studyNormalAnalysis => 'Tavaline analüüs'; + + @override + String get studyHideNextMoves => 'Peida järgmised käigud'; + + @override + String get studyInteractiveLesson => 'Interaktiivne õppetund'; + + @override + String studyChapterX(String param) { + return 'Peatükk $param'; + } + + @override + String get studyEmpty => 'Tühi'; + + @override + String get studyStartFromInitialPosition => 'Alusta algsest positsioonist'; + + @override + String get studyEditor => 'Muuda'; + + @override + String get studyStartFromCustomPosition => 'Alusta kohandatud positsioonist'; + + @override + String get studyLoadAGameByUrl => 'Lae mäng alla URL-ist'; + + @override + String get studyLoadAPositionFromFen => 'Laadi alla positsioon FEN-ist'; + + @override + String get studyLoadAGameFromPgn => 'Lae mänge PGN-ist'; + + @override + String get studyAutomatic => 'Automaatne'; + + @override + String get studyUrlOfTheGame => 'URL mängu'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Lae mäng alla $param1 või $param2'; + } + + @override + String get studyCreateChapter => 'Alusta peatükk'; + + @override + String get studyCreateStudy => 'Koosta uuring'; + + @override + String get studyEditStudy => 'Muuda uuringut'; + + @override + String get studyVisibility => 'Nähtavus'; + + @override + String get studyPublic => 'Avalik'; + + @override + String get studyUnlisted => 'Mitte avalik'; + + @override + String get studyInviteOnly => 'Ainult kutsega'; + + @override + String get studyAllowCloning => 'Luba kloneerimine'; + + @override + String get studyNobody => 'Mitte keegi'; + + @override + String get studyOnlyMe => 'Ainult mina'; + + @override + String get studyContributors => 'Panustajad'; + + @override + String get studyMembers => 'Liikmed'; + + @override + String get studyEveryone => 'Kõik'; + + @override + String get studyEnableSync => 'Luba sünkroneerimine'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Jah: hoia kõik samal positsioonil'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Ei: lase inimestel sirvida vabalt'; + + @override + String get studyPinnedStudyComment => 'Kinnitatud uuringu kommentaar'; + @override String get studyStart => 'Alusta'; + + @override + String get studySave => 'Salvesta'; + + @override + String get studyClearChat => 'Clear chat'; + + @override + String get studyDeleteTheStudyChatHistory => 'Kas soovite kustutada uuringu vestluse ajaloo? Seda otsust ei saa tagasi võtta!'; + + @override + String get studyDeleteStudy => 'Kustuta uuring'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Kas soovite kustutada terve uuringu? Seda otsust ei saa tagasi võtta! Kirjutage uuringu nimi otsuse kinnitamiseks: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Kus te seda lauda soovite uurida?'; + + @override + String get studyGoodMove => 'Hea käik'; + + @override + String get studyMistake => 'Viga'; + + @override + String get studyBrilliantMove => 'Suurepärane käik'; + + @override + String get studyBlunder => 'Tõsine viga'; + + @override + String get studyInterestingMove => 'Huvitav käik'; + + @override + String get studyDubiousMove => 'Kahtlane käik'; + + @override + String get studyOnlyMove => 'Ainus käik'; + + @override + String get studyZugzwang => 'Sundkäik'; + + @override + String get studyEqualPosition => 'Võrdne positsioon'; + + @override + String get studyUnclearPosition => 'Ebaselge positsioon'; + + @override + String get studyWhiteIsSlightlyBetter => 'Valgel on kerge eelis'; + + @override + String get studyBlackIsSlightlyBetter => 'Mustal on kerge eelis'; + + @override + String get studyWhiteIsBetter => 'Valgel on eelis'; + + @override + String get studyBlackIsBetter => 'Mustal on eelis'; + + @override + String get studyWhiteIsWinning => 'Valge on võitmas'; + + @override + String get studyBlackIsWinning => 'Must on võitmas'; + + @override + String get studyNovelty => 'Uudsus'; + + @override + String get studyDevelopment => 'Arendus'; + + @override + String get studyInitiative => 'Algatus'; + + @override + String get studyAttack => 'Rünnak'; + + @override + String get studyCounterplay => 'Vastumäng'; + + @override + String get studyTimeTrouble => 'Time trouble'; + + @override + String get studyWithCompensation => 'With compensation'; + + @override + String get studyWithTheIdea => 'With the idea'; + + @override + String get studyNextChapter => 'Järgmine peatükk'; + + @override + String get studyPrevChapter => 'Eelmine peatükk'; + + @override + String get studyStudyActions => 'Uuringu toimingud'; + + @override + String get studyTopics => 'Teemad'; + + @override + String get studyMyTopics => 'Minu teemad'; + + @override + String get studyPopularTopics => 'Populaarsed teemad'; + + @override + String get studyManageTopics => 'Halda teemasid'; + + @override + String get studyBack => 'Tagasi'; + + @override + String get studyPlayAgain => 'Mängi uuesti'; + + @override + String get studyWhatWouldYouPlay => 'Mis sa mängiksid selles positsioonis?'; + + @override + String get studyYouCompletedThisLesson => 'Palju õnne! Oled läbinud selle õppetunni.'; + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count peatükki', + one: '$count peatükk', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count mängu', + one: '$count mäng', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count liiget', + one: '$count liige', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Aseta oma PGN tekst siia, kuni $count mängu', + one: 'Aseta oma PGN tekst siia, kuni $count mäng', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_eu.dart b/lib/l10n/l10n_eu.dart index f3e070aab1..7a405058d6 100644 --- a/lib/l10n/l10n_eu.dart +++ b/lib/l10n/l10n_eu.dart @@ -9,140 +9,137 @@ class AppLocalizationsEu extends AppLocalizations { AppLocalizationsEu([String locale = 'eu']) : super(locale); @override - String get mobileHomeTab => 'Home'; + String get mobileHomeTab => 'Hasiera'; @override - String get mobilePuzzlesTab => 'Puzzles'; + String get mobilePuzzlesTab => 'Ariketak'; @override - String get mobileToolsTab => 'Tools'; + String get mobileToolsTab => 'Tresnak'; @override - String get mobileWatchTab => 'Watch'; + String get mobileWatchTab => 'Ikusi'; @override - String get mobileSettingsTab => 'Settings'; + String get mobileSettingsTab => 'Ezarpenak'; @override - String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; + String get mobileMustBeLoggedIn => 'Sartu egin behar zara orri hau ikusteko.'; @override - String get mobileSystemColors => 'System colors'; + String get mobileSystemColors => 'Sistemaren koloreak'; @override - String get mobileFeedbackButton => 'Feedback'; + String get mobileFeedbackButton => 'Iritzia'; @override - String get mobileOkButton => 'OK'; + String get mobileOkButton => 'Ados'; @override - String get mobileSettingsHapticFeedback => 'Haptic feedback'; + String get mobileSettingsHapticFeedback => 'Ukipen-erantzuna'; @override - String get mobileSettingsImmersiveMode => 'Immersive mode'; + String get mobileSettingsImmersiveMode => 'Murgiltze modua'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileSettingsImmersiveModeSubtitle => 'Ezkutatu sistemaren menuak jokatzen ari zaren artean. Erabili hau zure telefonoaren nabigatzeko aukerek traba egiten badizute. Partida bati eta ariketen zaparradan aplikatu daiteke.'; @override - String get mobileNotFollowingAnyUser => 'You are not following any user.'; + String get mobileNotFollowingAnyUser => 'Ez zaude erabiltzailerik jarraitzen.'; @override - String get mobileAllGames => 'All games'; + String get mobileAllGames => 'Partida guztiak'; @override - String get mobileRecentSearches => 'Recent searches'; + String get mobileRecentSearches => 'Azken bilaketak'; @override - String get mobileClearButton => 'Clear'; + String get mobileClearButton => 'Garbitu'; @override String mobilePlayersMatchingSearchTerm(String param) { - return 'Players with \"$param\"'; + return '\"$param\" duten jokalariak'; } @override - String get mobileNoSearchResults => 'No results'; + String get mobileNoSearchResults => 'Emaitzarik ez'; @override - String get mobileAreYouSure => 'Are you sure?'; + String get mobileAreYouSure => 'Ziur zaude?'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; + String get mobilePuzzleStreakAbortWarning => 'Zure uneko bolada galduko duzu eta zure puntuazioa gorde egingo da.'; @override - String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; + String get mobilePuzzleStormNothingToShow => 'Ez dago ezer erakusteko. Jokatu Ariketa zaparrada batzuk.'; @override - String get mobileSharePuzzle => 'Share this puzzle'; + String get mobileSharePuzzle => 'Partekatu ariketa hau'; @override - String get mobileShareGameURL => 'Share game URL'; + String get mobileShareGameURL => 'Partekatu partidaren URLa'; @override - String get mobileShareGamePGN => 'Share PGN'; + String get mobileShareGamePGN => 'Partekatu PGNa'; @override - String get mobileSharePositionAsFEN => 'Share position as FEN'; + String get mobileSharePositionAsFEN => 'Partekatu posizioa FEN gisa'; @override - String get mobileShowVariations => 'Show variations'; + String get mobileShowVariations => 'Erakutsi aukerak'; @override - String get mobileHideVariation => 'Hide variation'; + String get mobileHideVariation => 'Ezkutatu aukera'; @override - String get mobileShowComments => 'Show comments'; + String get mobileShowComments => 'Erakutsi iruzkinak'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get mobilePuzzleStormConfirmEndRun => 'Saiakera hau amaitu nahi duzu?'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobilePuzzleStormFilterNothingToShow => 'Ez dago erakusteko ezer, aldatu filtroak'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; + String get mobileCancelTakebackOffer => 'Bertan behera utzi atzera-egite eskaera'; @override - String get mobileCancelDrawOffer => 'Cancel draw offer'; + String get mobileWaitingForOpponentToJoin => 'Aurkaria sartzeko zain...'; @override - String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; + String get mobileBlindfoldMode => 'Itsuka'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get mobileLiveStreamers => 'Zuzeneko streamerrak'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileCustomGameJoinAGame => 'Sartu partida baten'; @override - String get mobileCustomGameJoinAGame => 'Join a game'; + String get mobileCorrespondenceClearSavedMove => 'Garbitu gordetako jokaldia'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; + String get mobileSomethingWentWrong => 'Zerbait gaizki joan da.'; @override - String get mobileSomethingWentWrong => 'Something went wrong.'; + String get mobileShowResult => 'Erakutsi emaitza'; @override - String get mobileShowResult => 'Show result'; + String get mobilePuzzleThemesSubtitle => 'Jokatu zure irekiera gogokoenen ariketak, edo aukeratu gai bat.'; @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; - - @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobilePuzzleStormSubtitle => 'Ebatzi ahalik eta ariketa gehien 3 minututan.'; @override String mobileGreeting(String param) { - return 'Hello, $param'; + return 'Kaixo $param'; } @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileGreetingWithoutName => 'Kaixo'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobilePrefMagnifyDraggedPiece => 'Handitu arrastatutako pieza'; @override String get activityActivity => 'Jarduera'; @@ -246,6 +243,17 @@ class AppLocalizationsEu extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$param2 posta bidezko $count partida osatuta', + one: '$param2 posta bidezko partida $count osatuta', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,226 @@ class AppLocalizationsEu extends AppLocalizations { @override String get broadcastBroadcasts => 'Emanaldiak'; + @override + String get broadcastMyBroadcasts => 'Nire zuzenekoak'; + @override String get broadcastLiveBroadcasts => 'Txapelketen zuzeneko emanaldiak'; + @override + String get broadcastBroadcastCalendar => 'Emanaldien egutegia'; + + @override + String get broadcastNewBroadcast => 'Zuzeneko emanaldi berria'; + + @override + String get broadcastSubscribedBroadcasts => 'Harpidetutako emanaldiak'; + + @override + String get broadcastAboutBroadcasts => 'Zuzeneko emanaldiei buruz'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Nola erabili Lichessen Zuzenekoak.'; + + @override + String get broadcastTheNewRoundHelp => 'Txanda berriak aurrekoak beste kide eta laguntzaile izango ditu.'; + + @override + String get broadcastAddRound => 'Gehitu txanda bat'; + + @override + String get broadcastOngoing => 'Orain martxan'; + + @override + String get broadcastUpcoming => 'Hurrengo emanaldiak'; + + @override + String get broadcastCompleted => 'Amaitutako emanaldiak'; + + @override + String get broadcastCompletedHelp => 'Txanda amaitu dela jatorrizko partidekin detektatzen du Lichessek. Erabili aukera hau jatorririk ez badago.'; + + @override + String get broadcastRoundName => 'Txandaren izena'; + + @override + String get broadcastRoundNumber => 'Txanda zenbaki'; + + @override + String get broadcastTournamentName => 'Txapelketaren izena'; + + @override + String get broadcastTournamentDescription => 'Txapelketaren deskribapen laburra'; + + @override + String get broadcastFullDescription => 'Ekitaldiaren deskribapen osoa'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Emanaldiaren azalpen luzea, hautazkoa da. $param1 badago. Luzera $param2 karaktere edo laburragoa izan behar da.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGNaren jatorrizko URLa'; + + @override + String get broadcastSourceUrlHelp => 'Lichessek PGNaren eguneraketak jasoko dituen URLa. Interneteko helbide bat izan behar da.'; + + @override + String get broadcastSourceGameIds => 'Gehienez ere Lichesseko 64 partidren idak, espazioekin banatuta.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Txapelketaren hasiera ordua ordu-zona lokalean: $param'; + } + + @override + String get broadcastStartDateHelp => 'Hautazkoa, ekitaldia noiz hasten den baldin badakizu'; + + @override + String get broadcastCurrentGameUrl => 'Uneko partidaren URL helbidea'; + + @override + String get broadcastDownloadAllRounds => 'Deskargatu txanda guztiak'; + + @override + String get broadcastResetRound => 'Berrezarri txanda hau'; + + @override + String get broadcastDeleteRound => 'Ezabatu txanda hau'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Betiko ezabatu txanda eta bere partida guztiak.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Ezabatu txanda honetako partida guztiak. Jatorria aktibo egon behar da berriz sortzeko.'; + + @override + String get broadcastEditRoundStudy => 'Editatu txandako azterlana'; + + @override + String get broadcastDeleteTournament => 'Ezabatu txapelketa hau'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Txapelketa behin betiko ezabatu, bere txanda eta partida guztiak barne.'; + + @override + String get broadcastShowScores => 'Erakutsi jokalarien puntuazioak partiden emaitzen arabera'; + + @override + String get broadcastReplacePlayerTags => 'Hautazkoa: aldatu jokalarien izen, puntuazio eta tituluak'; + + @override + String get broadcastFideFederations => 'FIDE federazioak'; + + @override + String get broadcastTop10Rating => '10 onenak'; + + @override + String get broadcastFidePlayers => 'FIDE jokalariak'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE jokalaria ez da aurkitu'; + + @override + String get broadcastFideProfile => 'FIDE profila'; + + @override + String get broadcastFederation => 'Federazioa'; + + @override + String get broadcastAgeThisYear => 'Adina'; + + @override + String get broadcastUnrated => 'Ez du sailkapenik'; + + @override + String get broadcastRecentTournaments => 'Azken txapelketak'; + + @override + String get broadcastOpenLichess => 'Open in Lichess'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Overview'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Official website'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'Score'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count zuzeneko', + one: 'Zuzeneko $count', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Erronkak: $param1'; @@ -1390,10 +1615,10 @@ class AppLocalizationsEu extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Aurkariak jokaldi mugatuak ditu eta jokaldi guztien bere posizioa okertu egiten dute.'; @override - String get puzzleThemeHealthyMix => 'Denetik pixkat'; + String get puzzleThemeMix => 'Denetik pixkat'; @override - String get puzzleThemeHealthyMixDescription => 'Denetatik. Ez dakizu zer espero, beraz prestatu zure burua edozertarako! Benetako partidetan bezala.'; + String get puzzleThemeMixDescription => 'Denetatik. Ez dakizu zer espero, beraz prestatu zure burua edozertarako! Benetako partidetan bezala.'; @override String get puzzleThemePlayerGames => 'Jokalarien partidak'; @@ -1636,10 +1861,10 @@ class AppLocalizationsEu extends AppLocalizations { String get deleteFromHere => 'Ezabatu hemendik aurrera'; @override - String get collapseVariations => 'Collapse variations'; + String get collapseVariations => 'Ezkutatu aldaerak'; @override - String get expandVariations => 'Expand variations'; + String get expandVariations => 'Erakutsi aldaerak'; @override String get forceVariation => 'Aldaera derrigortu'; @@ -1797,9 +2022,6 @@ class AppLocalizationsEu extends AppLocalizations { @override String get removesTheDepthLimit => 'Sakonera muga ezabatzendu eta zure ordenagailua epel mantentzen du'; - @override - String get engineManager => 'Motore kudeatzailea'; - @override String get blunder => 'Hanka-sartzea'; @@ -1878,7 +2100,7 @@ class AppLocalizationsEu extends AppLocalizations { String get friends => 'Lagunak'; @override - String get otherPlayers => 'other players'; + String get otherPlayers => 'beste jokalariak'; @override String get discussions => 'Eztabaidak'; @@ -2063,6 +2285,9 @@ class AppLocalizationsEu extends AppLocalizations { @override String get gamesPlayed => 'Partida jokaturik'; + @override + String get ok => 'OK'; + @override String get cancel => 'Ezeztatu'; @@ -2121,7 +2346,7 @@ class AppLocalizationsEu extends AppLocalizations { String get standard => 'Ohikoa'; @override - String get customPosition => 'Custom position'; + String get customPosition => 'Posizio pertsonalizatua'; @override String get unlimited => 'Mugagabea'; @@ -2631,7 +2856,7 @@ class AppLocalizationsEu extends AppLocalizations { String get editProfile => 'Nire profila editatu'; @override - String get realName => 'Real name'; + String get realName => 'Benetako izena'; @override String get setFlair => 'Ezarri zure iruditxoa'; @@ -2691,7 +2916,7 @@ class AppLocalizationsEu extends AppLocalizations { String get puzzles => 'Ariketak'; @override - String get onlineBots => 'Online bots'; + String get onlineBots => 'Online dauden botak'; @override String get name => 'Izena'; @@ -2712,10 +2937,10 @@ class AppLocalizationsEu extends AppLocalizations { String get yes => 'Bai'; @override - String get website => 'Website'; + String get website => 'Webgunea'; @override - String get mobile => 'Mobile'; + String get mobile => 'Mobila'; @override String get help => 'Laguntza:'; @@ -2772,7 +2997,13 @@ class AppLocalizationsEu extends AppLocalizations { String get other => 'Bestelakoak'; @override - String get reportDescriptionHelp => 'Partidaren esteka itsasi, eta azaldu zer egin duen gaizki erabiltzaileak. Ez esan \"tranpak egiten ditu\" bakarrik, eman horren arrazoiak. Zure mezua azkarrago begiratuko dugu ingelesez idazten baduzu.'; + String get reportCheatBoostHelp => 'Partidaren esteka itsasi, eta azaldu zer egin duen gaizki erabiltzaileak. Ez esan \"tranpak egiten ditu\" bakarrik, eman horren arrazoiak.'; + + @override + String get reportUsernameHelp => 'Azaldu erabiltzaile-izen honek zer duen iraingarria. Ez esan \"iraingarria da\" soilik, eman arrazoiak, batez ere iraina ezkutatuta badago, ez bada ingelesezko hitz bat edo errefererantzia historiko edo kulturala bada.'; + + @override + String get reportProcessedFasterInEnglish => 'Zure mezua azkarrago kudeatuko dugu ingelesez idazten baduzu.'; @override String get error_provideOneCheatedGameLink => 'Iruzurra izandako partida baten lotura bidali gutxienez.'; @@ -2875,7 +3106,7 @@ class AppLocalizationsEu extends AppLocalizations { String get outsideTheBoard => 'Taulatik kanpo'; @override - String get allSquaresOfTheBoard => 'All squares of the board'; + String get allSquaresOfTheBoard => 'Taulako lauki guztiak'; @override String get onSlowGames => 'Partida moteletan'; @@ -3471,22 +3702,22 @@ class AppLocalizationsEu extends AppLocalizations { String get backgroundImageUrl => 'Atzeko-planoko irudia:'; @override - String get board => 'Board'; + String get board => 'Taula'; @override - String get size => 'Size'; + String get size => 'Tamaina'; @override - String get opacity => 'Opacity'; + String get opacity => 'Gardentasuna'; @override - String get brightness => 'Brightness'; + String get brightness => 'Argitasuna'; @override - String get hue => 'Hue'; + String get hue => 'Ñabardura'; @override - String get boardReset => 'Reset colours to default'; + String get boardReset => 'Berrezarri koloreak defektuzkoetara'; @override String get pieceSet => 'Pieza formatua'; @@ -4075,7 +4306,10 @@ class AppLocalizationsEu extends AppLocalizations { String get lichessPatronInfo => 'Lichess software librea da.\nGarapen eta mantentze-kostu guztiak erabiltzaileen dohaintzekin ordaintzen dira.'; @override - String get nothingToSeeHere => 'Nothing to see here at the moment.'; + String get nothingToSeeHere => 'Hemen ez dago ezer zuretzat.'; + + @override + String get stats => 'Stats'; @override String opponentLeftCounter(int count) { @@ -4723,9 +4957,514 @@ class AppLocalizationsEu extends AppLocalizations { @override String get streamerLichessStreamers => 'Lichess streamerrak'; + @override + String get studyPrivate => 'Pribatua'; + + @override + String get studyMyStudies => 'Nire azterlanak'; + + @override + String get studyStudiesIContributeTo => 'Nik parte hartzen dudan azterlanak'; + + @override + String get studyMyPublicStudies => 'Nire azterlan publikoak'; + + @override + String get studyMyPrivateStudies => 'Nire azterlan pribatuak'; + + @override + String get studyMyFavoriteStudies => 'Nire azterlan gogokoenak'; + + @override + String get studyWhatAreStudies => 'Zer dira azterlanak?'; + + @override + String get studyAllStudies => 'Azterlan guztiak'; + + @override + String studyStudiesCreatedByX(String param) { + return '$param erabiltzaileak sortutako azterlanak'; + } + + @override + String get studyNoneYet => 'Bat ere ez.'; + + @override + String get studyHot => 'Nabarmendutakoak'; + + @override + String get studyDateAddedNewest => 'Sorrera-data (berriena)'; + + @override + String get studyDateAddedOldest => 'Sorrera-data (zaharrena)'; + + @override + String get studyRecentlyUpdated => 'Eguneratutako azkenak'; + + @override + String get studyMostPopular => 'Arrakasta gehien duena'; + + @override + String get studyAlphabetical => 'Alfabetikoa'; + + @override + String get studyAddNewChapter => 'Kapitulu berria gehitu'; + + @override + String get studyAddMembers => 'Kideak gehitu'; + + @override + String get studyInviteToTheStudy => 'Azterlanera gonbidatu'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Ezagutzen duzun eta benetan azterlanean interesa duen jendea gonbidatu bakarrik.'; + + @override + String get studySearchByUsername => 'Erabiltzaile izenaren arabera bilatu'; + + @override + String get studySpectator => 'Ikuslea'; + + @override + String get studyContributor => 'Laguntzailea'; + + @override + String get studyKick => 'Kanporatu'; + + @override + String get studyLeaveTheStudy => 'Azterlana utzi'; + + @override + String get studyYouAreNowAContributor => 'Laguntzailea zara orain'; + + @override + String get studyYouAreNowASpectator => 'Ikuslea zara orain'; + + @override + String get studyPgnTags => 'PGN etiketak'; + + @override + String get studyLike => 'Datsegit'; + + @override + String get studyUnlike => 'Ez dut atsegin'; + + @override + String get studyNewTag => 'Etiketa berria'; + + @override + String get studyCommentThisPosition => 'Posizio hau komentatu'; + + @override + String get studyCommentThisMove => 'Jokaldi hau komentatu'; + + @override + String get studyAnnotateWithGlyphs => 'Ikonoekin komentatu'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Komentatzeko laburregia da kapitulua.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Azterlanaren laguntzaileek bakarrik eskatu dezakete ordenagailu bidezko analisia.'; + + @override + String get studyGetAFullComputerAnalysis => 'Linea nagusiaren ordenagailu bidezko analisia lortu.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Ziurtatu kapitulua guztiz osatu duzula. Analisia behin bakarrik eskatu dezakezu.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Kide sinkronizatu guztiak posizio berean jarraitzen dute'; + + @override + String get studyShareChanges => 'Aldaketak ikusleekin partekatu eta zerbitzarian gorde'; + + @override + String get studyPlaying => 'Jokatzen'; + + @override + String get studyShowEvalBar => 'Ebaluazio barrak'; + + @override + String get studyFirst => 'Lehenengoa'; + + @override + String get studyPrevious => 'Aurrekoa'; + + @override + String get studyNext => 'Hurrengoa'; + + @override + String get studyLast => 'Azkena'; + @override String get studyShareAndExport => 'Partekatu & esportatu'; + @override + String get studyCloneStudy => 'Klonatu'; + + @override + String get studyStudyPgn => 'Azterlanaren PGNa'; + + @override + String get studyDownloadAllGames => 'Partida guztiak deskargatu'; + + @override + String get studyChapterPgn => 'Kapituluaren PGNa'; + + @override + String get studyCopyChapterPgn => 'Kopiatu PGNa'; + + @override + String get studyDownloadGame => 'Partida deskargatu'; + + @override + String get studyStudyUrl => 'Azterlanaren helbidea'; + + @override + String get studyCurrentChapterUrl => 'Uneko kapituluaren helbidea'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Hau foroan itsatsi dezakezu'; + + @override + String get studyStartAtInitialPosition => 'Hasierako posizioan hasi'; + + @override + String studyStartAtX(String param) { + return 'Hemen asi $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Zure webgunean itsatsi'; + + @override + String get studyReadMoreAboutEmbedding => 'Itsasteari buruz gehiago irakurri'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Azterlan publikoak bakarrik txertatu daitezke beste webguneetan!'; + + @override + String get studyOpen => 'Ireki'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1 azterlana $param2 erabiltzaileak prestatu du'; + } + + @override + String get studyStudyNotFound => 'Azterlana ez da aurkitu'; + + @override + String get studyEditChapter => 'Kapitulua aldatu'; + + @override + String get studyNewChapter => 'Kapitulu berria'; + + @override + String studyImportFromChapterX(String param) { + return 'Inportatu $param kapitulotik'; + } + + @override + String get studyOrientation => 'Kokapena'; + + @override + String get studyAnalysisMode => 'Analisi modua'; + + @override + String get studyPinnedChapterComment => 'Kapituluaren iltzatutako iruzkina'; + + @override + String get studySaveChapter => 'Kapitulua gorde'; + + @override + String get studyClearAnnotations => 'Iruzkinak garbitu'; + + @override + String get studyClearVariations => 'Garbitu aldaerak'; + + @override + String get studyDeleteChapter => 'Kapitulua ezabatu'; + + @override + String get studyDeleteThisChapter => 'Kapitulu hau ezabatu egin nahi duzu? Ez dago atzera egiterik!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Kapitulu honetako iruzkin guztiak ezabatu?'; + + @override + String get studyRightUnderTheBoard => 'Xake-taularen azpian'; + + @override + String get studyNoPinnedComment => 'Ez erakutsi'; + + @override + String get studyNormalAnalysis => 'Analisi arrunta'; + + @override + String get studyHideNextMoves => 'Hurrengo jokaldiak ezkutatu'; + + @override + String get studyInteractiveLesson => 'Ikasgai interaktiboa'; + + @override + String studyChapterX(String param) { + return '$param kapitulua'; + } + + @override + String get studyEmpty => 'Hutsa'; + + @override + String get studyStartFromInitialPosition => 'Hasierako posiziotik hasi'; + + @override + String get studyEditor => 'Editorea'; + + @override + String get studyStartFromCustomPosition => 'Pertsonalizatutako posiziotik hasi'; + + @override + String get studyLoadAGameByUrl => 'Partida interneteko helbide batetik kargatu'; + + @override + String get studyLoadAPositionFromFen => 'Posizioa FEN batetik kargatu'; + + @override + String get studyLoadAGameFromPgn => 'Partida PGN batetik kargatu'; + + @override + String get studyAutomatic => 'Automatikoa'; + + @override + String get studyUrlOfTheGame => 'Partidaren URLa'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Hemendik kargatu partida bat: $param1 edo $param2'; + } + + @override + String get studyCreateChapter => 'Kapitulua sortu'; + + @override + String get studyCreateStudy => 'Azterlana sortu'; + + @override + String get studyEditStudy => 'Azterlana aldatu'; + + @override + String get studyVisibility => 'Ikusgaitasuna'; + + @override + String get studyPublic => 'Publikoa'; + + @override + String get studyUnlisted => 'Ez zerrendatu'; + + @override + String get studyInviteOnly => 'Gonbidatuentzat bakarrik'; + + @override + String get studyAllowCloning => 'Kopiatzea utzi'; + + @override + String get studyNobody => 'Inor ere ez'; + + @override + String get studyOnlyMe => 'Ni bakarrik'; + + @override + String get studyContributors => 'Laguntzaileak'; + + @override + String get studyMembers => 'Kideak'; + + @override + String get studyEveryone => 'Guztiak'; + + @override + String get studyEnableSync => 'Sinkronizazioa aktibatu'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Bai: guztiak posizio berean mantendu'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Ez: erabiltzaileei nahi dutena egiten utzi'; + + @override + String get studyPinnedStudyComment => 'Azterlanaren iltzatutako iruzkina'; + @override String get studyStart => 'Hasi'; + + @override + String get studySave => 'Gorde'; + + @override + String get studyClearChat => 'Txata garbitu'; + + @override + String get studyDeleteTheStudyChatHistory => 'Azterlaneko txata ezabatu? Ez dago atzera egiterik!'; + + @override + String get studyDeleteStudy => 'Azterlana ezabatu'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Azterlan osoa ezabatu? Ez dago atzera egiterik! Idatzi azterlanaren izena baieztapena emateko: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Non nahi duzu hori aztertu?'; + + @override + String get studyGoodMove => 'Jokaldi ona'; + + @override + String get studyMistake => 'Akatsa'; + + @override + String get studyBrilliantMove => 'Jokaldi bikaina'; + + @override + String get studyBlunder => 'Akats larria'; + + @override + String get studyInterestingMove => 'Jokaldi interesgarria'; + + @override + String get studyDubiousMove => 'Zalantzazko jokaldia'; + + @override + String get studyOnlyMove => 'Jokaldi bakarra'; + + @override + String get studyZugzwang => 'Zugzwang'; + + @override + String get studyEqualPosition => 'Berdindutako posizioa'; + + @override + String get studyUnclearPosition => 'Posizioa ez da argia'; + + @override + String get studyWhiteIsSlightlyBetter => 'Zuria hobetoxeago'; + + @override + String get studyBlackIsSlightlyBetter => 'Beltza hobetoxeago'; + + @override + String get studyWhiteIsBetter => 'Zuria hobeto'; + + @override + String get studyBlackIsBetter => 'Beltza hobeto'; + + @override + String get studyWhiteIsWinning => 'Zuria irabazten ari da'; + + @override + String get studyBlackIsWinning => 'Beltza irabazten ari da'; + + @override + String get studyNovelty => 'Berritasuna'; + + @override + String get studyDevelopment => 'Garapena'; + + @override + String get studyInitiative => 'Iniziatiba'; + + @override + String get studyAttack => 'Erasoa'; + + @override + String get studyCounterplay => 'Kontraerasoa'; + + @override + String get studyTimeTrouble => 'Denbora-arazoak'; + + @override + String get studyWithCompensation => 'Konepntsazioarekin'; + + @override + String get studyWithTheIdea => 'Ideiarekin'; + + @override + String get studyNextChapter => 'Hurrengo kapitulua'; + + @override + String get studyPrevChapter => 'Aurreko kapitulua'; + + @override + String get studyStudyActions => 'Azterlanen akzioak'; + + @override + String get studyTopics => 'Gaiak'; + + @override + String get studyMyTopics => 'Nire gaiak'; + + @override + String get studyPopularTopics => 'Gai arrakastatsuak'; + + @override + String get studyManageTopics => 'Kudeatu gaiak'; + + @override + String get studyBack => 'Atzera joan'; + + @override + String get studyPlayAgain => 'Jokatu berriz'; + + @override + String get studyWhatWouldYouPlay => 'Zer jokatuko zenuke posizio honetan?'; + + @override + String get studyYouCompletedThisLesson => 'Zorionak! Ikasgai hau osatu duzu.'; + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count kapitulu', + one: 'Kapitulu $count', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count partida', + one: 'Partida $count', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count kide', + one: 'Kide $count', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Itsatsi hemen zure PGNa, gehienez $count partida', + one: 'Itsatsi hemen zure PGNa, gehienez partida $count', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_fa.dart b/lib/l10n/l10n_fa.dart index f250133286..7567dddbc7 100644 --- a/lib/l10n/l10n_fa.dart +++ b/lib/l10n/l10n_fa.dart @@ -24,45 +24,45 @@ class AppLocalizationsFa extends AppLocalizations { String get mobileSettingsTab => 'تنظیمات'; @override - String get mobileMustBeLoggedIn => 'برای دیدن این صفحه باید وارد حساب‌تان شده باشید.'; + String get mobileMustBeLoggedIn => 'برای دیدن این برگه باید وارد شده باشید.'; @override String get mobileSystemColors => 'رنگ‌های دستگاه'; @override - String get mobileFeedbackButton => 'بازخوراند'; + String get mobileFeedbackButton => 'بازخورد'; @override String get mobileOkButton => 'باشه'; @override - String get mobileSettingsHapticFeedback => 'بازخوراند لمسی'; + String get mobileSettingsHapticFeedback => 'بازخورد لمسی'; @override - String get mobileSettingsImmersiveMode => 'حالت غوطه‌ور'; + String get mobileSettingsImmersiveMode => 'حالت فراگیر'; @override - String get mobileSettingsImmersiveModeSubtitle => 'هنگام بازی، میانای کاربری دستگاه را پنهان کنید. اگر حرکت‌های ناوبری دستگاه در لبه‌های پرده آزارتان می‌دهد، از این استفاده کنید. برای پرده‌های بازی و معماباران (Puzzle Storm) کاربرد دارد.'; + String get mobileSettingsImmersiveModeSubtitle => 'رابط کاربری را هنگام بازی پنهان کنید. اگر ناوبری لمسی در لبه‌های دستگاه اذیتتان می‌کند از این استفاده کنید. کارساز برای برگه‌های بازی و معماباران.'; @override - String get mobileNotFollowingAnyUser => 'شما هیچ کاربری را نمی‌دنبالید.'; + String get mobileNotFollowingAnyUser => 'شما هیچ کاربری را دنبال نمی‌کنید.'; @override String get mobileAllGames => 'همه بازی‌ها'; @override - String get mobileRecentSearches => 'جستجوهای اخیر'; + String get mobileRecentSearches => 'واپسین جستجوها'; @override String get mobileClearButton => 'پاکسازی'; @override String mobilePlayersMatchingSearchTerm(String param) { - return 'بازیکنانِ «$param»'; + return 'کاربران با پیوند «$param»'; } @override - String get mobileNoSearchResults => 'بدون نتیجه'; + String get mobileNoSearchResults => 'بدون پیامد'; @override String get mobileAreYouSure => 'مطمئنید؟'; @@ -92,22 +92,19 @@ class AppLocalizationsFa extends AppLocalizations { String get mobileHideVariation => 'بستن شاخه‌ها'; @override - String get mobileShowComments => 'نمایش نظرها'; + String get mobileShowComments => 'نمایش دیدگاه‌ها'; @override - String get mobilePuzzleStormConfirmEndRun => 'می‌خواهید این دور را پایان دهید؟'; + String get mobilePuzzleStormConfirmEndRun => 'می‌خواهید این دور را به پایان برسانید؟'; @override - String get mobilePuzzleStormFilterNothingToShow => 'چیزی برای نمایش نیست، لطفا پالاب‌گرها را تغییر دهید'; + String get mobilePuzzleStormFilterNothingToShow => 'چیزی برای نمایش نیست، خواهشمندیم پالایه‌ها را دگرسان کنید.'; @override String get mobileCancelTakebackOffer => 'رد درخواست برگرداندن'; @override - String get mobileCancelDrawOffer => 'رد پیشنهاد تساوی'; - - @override - String get mobileWaitingForOpponentToJoin => 'در انتظار آمدن حریف...'; + String get mobileWaitingForOpponentToJoin => 'شکیبا برای پیوستن حریف...'; @override String get mobileBlindfoldMode => 'چشم‌بسته'; @@ -119,19 +116,19 @@ class AppLocalizationsFa extends AppLocalizations { String get mobileCustomGameJoinAGame => 'به بازی بپیوندید'; @override - String get mobileCorrespondenceClearSavedMove => 'پاکیدن حرکت ذخیره‌شده'; + String get mobileCorrespondenceClearSavedMove => 'پاک کردن حرکت ذخیره شده'; @override String get mobileSomethingWentWrong => 'مشکلی پیش آمد.'; @override - String get mobileShowResult => 'نمایش نتیجه'; + String get mobileShowResult => 'نمایش پیامد'; @override - String get mobilePuzzleThemesSubtitle => 'معماهایی را از گشایش دلخواه‌تان بازی کنید، یا موضوعی را برگزینید.'; + String get mobilePuzzleThemesSubtitle => 'معماهایی را از گشایش دلخواه‌تان بازی کنید، یا جستاری را برگزینید.'; @override - String get mobilePuzzleStormSubtitle => 'در ۳ دقیقه، هر چندتا معما که می‌توانید، حل کنید.'; + String get mobilePuzzleStormSubtitle => 'هر چند تا معما را که می‌توانید در ۳ دقیقه حل کنید.'; @override String mobileGreeting(String param) { @@ -139,10 +136,10 @@ class AppLocalizationsFa extends AppLocalizations { } @override - String get mobileGreetingWithoutName => 'سلام'; + String get mobileGreetingWithoutName => 'درود'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobilePrefMagnifyDraggedPiece => 'بزرگ‌نمودن مهره‌ی کشیده'; @override String get activityActivity => 'فعالیت'; @@ -207,8 +204,8 @@ class AppLocalizationsFa extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count پیام را در $param2 ارسال کرد', - one: '$count پیام را در $param2 ارسال کرد', + other: '$count پیام را در $param2 فرستاد', + one: '$count پیام را در $param2 فرستاد', ); return '$_temp0'; } @@ -246,12 +243,23 @@ class AppLocalizationsFa extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'تکمیل $count بازی مکاتبه‌ای $param2', + one: 'تکمیل $count بازی مکاتبه‌ای $param2', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count بازیکن را دنبال کرد', + other: 'شروع به دنبالیدن $count بازیکن کرد', one: '$count بازیکن را دنبال کرد', ); return '$_temp0'; @@ -262,8 +270,8 @@ class AppLocalizationsFa extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count دنبال کننده جدید به دست آورد', - one: '$count دنبال کننده جدید به دست آورد', + other: '$count دنبال‌گر جدید به‌دست آورد', + one: '$count دنبال‌گر جدید به‌دست آورد', ); return '$_temp0'; } @@ -348,9 +356,226 @@ class AppLocalizationsFa extends AppLocalizations { @override String get broadcastBroadcasts => 'پخش همگانی'; + @override + String get broadcastMyBroadcasts => 'پخش همگانی من'; + @override String get broadcastLiveBroadcasts => 'پخش زنده مسابقات'; + @override + String get broadcastBroadcastCalendar => 'تقویم پخش'; + + @override + String get broadcastNewBroadcast => 'پخش زنده جدید'; + + @override + String get broadcastSubscribedBroadcasts => 'پخش‌های دنبال‌شده'; + + @override + String get broadcastAboutBroadcasts => 'درباره پخش‌های همگانی'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'نحوه استفاده از پخش همگانی Lichess.'; + + @override + String get broadcastTheNewRoundHelp => 'دور جدید، همان اعضا و مشارکت‌کنندگان دور قبلی را خواهد داشت.'; + + @override + String get broadcastAddRound => 'اضافه کردن یک دور'; + + @override + String get broadcastOngoing => 'ادامه‌دار'; + + @override + String get broadcastUpcoming => 'آینده'; + + @override + String get broadcastCompleted => 'کامل‌شده'; + + @override + String get broadcastCompletedHelp => 'Lichess تکمیل دور را بر اساس بازی‌های منبع تشخیص می‌دهد. اگر منبعی وجود ندارد، از این کلید استفاده کنید.'; + + @override + String get broadcastRoundName => 'نام دور'; + + @override + String get broadcastRoundNumber => 'شماره دور'; + + @override + String get broadcastTournamentName => 'نام مسابقات'; + + @override + String get broadcastTournamentDescription => 'توضیحات کوتاه مسابقات'; + + @override + String get broadcastFullDescription => 'توضیحات کامل مسابقات'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'توضیحات بلند و اختیاری پخش همگانی. $param1 قابل‌استفاده است. طول متن باید کمتر از $param2 نویسه باشد.'; + } + + @override + String get broadcastSourceSingleUrl => 'وب‌نشانیِ PGN'; + + @override + String get broadcastSourceUrlHelp => 'وب‌نشانی‌ای که Lichess برای دریافت به‌روزرسانی‌های PGN می‌بررسد. آن باید از راه اینترنت در دسترس همگان باشد.'; + + @override + String get broadcastSourceGameIds => 'تا ۶۴ شناسه بازی لیچس٬ جداشده با فاصله.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'تاریخ آغاز در زمان-یانه محلی مسابقات: $param'; + } + + @override + String get broadcastStartDateHelp => 'اختیاری است، اگر می‌دانید چه زمانی رویداد شروع می‌شود'; + + @override + String get broadcastCurrentGameUrl => 'نشانی بازی کنونی'; + + @override + String get broadcastDownloadAllRounds => 'بارگیری همه دورها'; + + @override + String get broadcastResetRound => 'ازنوکردن این دور'; + + @override + String get broadcastDeleteRound => 'حذف این دور'; + + @override + String get broadcastDefinitivelyDeleteRound => 'این دور و همه بازی‌هایش را به طور کامل حذف کن.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'همه بازی‌های این دور را حذف کن. منبع باید فعال باشد تا بتوان آنها را بازساخت.'; + + @override + String get broadcastEditRoundStudy => 'ویرایش مطالعه دور'; + + @override + String get broadcastDeleteTournament => 'حذف این مسابقات'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'کل مسابقات، شامل همه دورها و بازی‌هایش را به طور کامل حذف کن.'; + + @override + String get broadcastShowScores => 'نمایش امتیاز بازیکنان بر پایه نتیجه بازی‌ها'; + + @override + String get broadcastReplacePlayerTags => 'اختیاری: عوض کردن نام، درجه‌بندی و عنوان بازیکنان'; + + @override + String get broadcastFideFederations => 'کشورگان‌های فیده'; + + @override + String get broadcastTop10Rating => 'ده درجه‌بندی برتر'; + + @override + String get broadcastFidePlayers => 'بازیکنان فیده'; + + @override + String get broadcastFidePlayerNotFound => 'بازیکن فیده پیدا نشد'; + + @override + String get broadcastFideProfile => 'رُخ‌نمای فیده'; + + @override + String get broadcastFederation => 'کشورگان'; + + @override + String get broadcastAgeThisYear => 'سنِ امسال'; + + @override + String get broadcastUnrated => 'بی‌درجه‌بندی'; + + @override + String get broadcastRecentTournaments => 'مسابقاتِ اخیر'; + + @override + String get broadcastOpenLichess => 'Open in Lichess'; + + @override + String get broadcastTeams => 'تیم‌ها'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Overview'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'پخش زنده به زودی آغاز خواهد شد.'; + + @override + String get broadcastNotYetStarted => 'پخش زنده هنوز آغاز نشده است.'; + + @override + String get broadcastOfficialWebsite => 'تارنمای رسمی'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'ناسانی امتیازات'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'امتیاز'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count پخش همگانی', + one: '$count پخش همگانی', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'پیشنهاد بازی: $param1'; @@ -450,7 +675,7 @@ class AppLocalizationsFa extends AppLocalizations { } @override - String get perfStatViewTheGames => 'بازی ها را تماشا کنید'; + String get perfStatViewTheGames => 'دیدن بازی‌ها'; @override String get perfStatProvisional => 'موقت'; @@ -706,7 +931,7 @@ class AppLocalizationsFa extends AppLocalizations { String get preferencesYourPreferencesHaveBeenSaved => 'تغییرات شما ذخیره شده است'; @override - String get preferencesScrollOnTheBoardToReplayMoves => 'اسکرول کردن روی صفحه برای مشاهده مجدد حرکت‌ها'; + String get preferencesScrollOnTheBoardToReplayMoves => 'برای بازپخش حرکت‌ها، روی صفحه بازی بِنَوَردید'; @override String get preferencesCorrespondenceEmailNotification => 'ایمیل های روزانه که بازی های شبیه شما را به صورت لیست درمی‌آورند'; @@ -718,7 +943,7 @@ class AppLocalizationsFa extends AppLocalizations { String get preferencesNotifyInboxMsg => 'پیام جدید'; @override - String get preferencesNotifyForumMention => 'در کامنتی نام شما ذکر شده است'; + String get preferencesNotifyForumMention => 'در انجمن از شما نام‌بُرده‌اند'; @override String get preferencesNotifyInvitedStudy => 'دعوت به مطالعه'; @@ -754,7 +979,7 @@ class AppLocalizationsFa extends AppLocalizations { String get puzzlePuzzles => 'معماها'; @override - String get puzzlePuzzleThemes => 'موضوع معماها'; + String get puzzlePuzzleThemes => 'موضوع معما'; @override String get puzzleRecommended => 'توصیه شده'; @@ -855,7 +1080,7 @@ class AppLocalizationsFa extends AppLocalizations { String get puzzleNotTheMove => 'این حرکت نیست!'; @override - String get puzzleTrySomethingElse => 'چیز دیگری پیدا کنید'; + String get puzzleTrySomethingElse => 'چیز دیگری بیابید.'; @override String puzzleRatingX(String param) { @@ -904,7 +1129,7 @@ class AppLocalizationsFa extends AppLocalizations { String get puzzleJumpToNextPuzzleImmediately => 'فوراً به معمای بعدی بروید'; @override - String get puzzlePuzzleDashboard => 'پیشخوان معماها'; + String get puzzlePuzzleDashboard => 'پیشخوان معما'; @override String get puzzleImprovementAreas => 'نقاط ضعف'; @@ -913,7 +1138,7 @@ class AppLocalizationsFa extends AppLocalizations { String get puzzleStrengths => 'نقاط قوت'; @override - String get puzzleHistory => 'پیشینه معماها'; + String get puzzleHistory => 'پیشینهٔ معما'; @override String get puzzleSolved => 'حل شده'; @@ -950,10 +1175,10 @@ class AppLocalizationsFa extends AppLocalizations { } @override - String get puzzleSearchPuzzles => 'جستجوی معماها'; + String get puzzleSearchPuzzles => 'جستجوی معما'; @override - String get puzzleFromMyGamesNone => 'شما هیچ معمایی در پایگاه‌داده ندارید، اما Lichess همچنان شما را بسیار دوست دارد.\n\nبازی‌های سریع و مرسوم را انجام دهید تا بَختِتان را برای افزودن معمایی از خودتان بیفزایید!'; + String get puzzleFromMyGamesNone => 'شما هیچ معمایی در دادگان ندارید، اما Lichess همچنان شما را بسیار دوست دارد.\n\nبازی‌های سریع و مرسوم را انجام دهید تا بخت‌تان را برای افزودن معمایی از خودتان بیفزایید!'; @override String puzzleFromXGamesFound(String param1, String param2) { @@ -1120,7 +1345,7 @@ class AppLocalizationsFa extends AppLocalizations { String get puzzleThemeEquality => 'برابری'; @override - String get puzzleThemeEqualityDescription => 'از وضعیت باخت در‌آیید و به وضعیت تساوی یا ترازمند برسید. (ارزیابی ≤ ۲۰۰ ص‌پ)'; + String get puzzleThemeEqualityDescription => 'از وضعیت باخت در‌آیید و به وضعیت تساوی یا تعادل برسید. (ارزیابی ≤ ۲۰۰ ص‌پ)'; @override String get puzzleThemeKingsideAttack => 'حمله به جناح شاه'; @@ -1375,7 +1600,7 @@ class AppLocalizationsFa extends AppLocalizations { String get puzzleThemeVeryLong => 'معمای خیلی طولانی'; @override - String get puzzleThemeVeryLongDescription => 'چهار حرکت یا بیشتر برای برنده شدن.'; + String get puzzleThemeVeryLongDescription => 'بُردن با چهار حرکت یا بیشتر.'; @override String get puzzleThemeXRayAttack => 'حمله پیکانی'; @@ -1390,10 +1615,10 @@ class AppLocalizationsFa extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'حریف در حرکت‌هایش محدود است و همه‌شان وضعیتش را بدتر می‌کند.'; @override - String get puzzleThemeHealthyMix => 'ترکیب سالم'; + String get puzzleThemeMix => 'آمیزهٔ همگن'; @override - String get puzzleThemeHealthyMixDescription => 'یک ذره از همه چیز. شما نمی دانید چه چیزی پیش روی شماست، بنابراین شما باید برای هر چیزی آماده باشید! دقیقا مثل بازی های واقعی.'; + String get puzzleThemeMixDescription => 'ذره‌ای از هر چیزی. شما نمی‌دانید چه چیزی پیش روی شماست، بنابراین برای هر چیزی آماده می‌مانید! درست مانند بازی‌های واقعی.'; @override String get puzzleThemePlayerGames => 'بازی‌های بازیکن'; @@ -1416,7 +1641,7 @@ class AppLocalizationsFa extends AppLocalizations { String get settingsCloseAccount => 'بستن حساب'; @override - String get settingsManagedAccountCannotBeClosed => 'اکانت شما مدیریت شده است و نمی تواند بسته شود.'; + String get settingsManagedAccountCannotBeClosed => 'حساب‌تان مدیریت می‌شود و نمی‌توان آن را بست.'; @override String get settingsClosingIsDefinitive => 'بعد از بستن حسابتان دیگر نمی توانید به آن دسترسی پیدا کنید. آیا مطمئن هستید؟'; @@ -1425,7 +1650,7 @@ class AppLocalizationsFa extends AppLocalizations { String get settingsCantOpenSimilarAccount => 'شما نمی توانید حساب جدیدی با این نام کاربری باز کنید، حتی اگر با دستگاه دیگری وارد شوید.'; @override - String get settingsChangedMindDoNotCloseAccount => 'نظرم را عوض کردم اکانتم را نمی بندم'; + String get settingsChangedMindDoNotCloseAccount => 'نظرم عوض شد، حسابم را نبند'; @override String get settingsCloseAccountExplanation => 'آیا مطمئنید که می خواهید حساب خود را ببندید؟ بستن حساب یک تصمیم دائمی است. شما هرگز نمی توانید دوباره وارد حساب خود شوید.'; @@ -1440,7 +1665,7 @@ class AppLocalizationsFa extends AppLocalizations { String get playWithTheMachine => 'بازی با رایانه'; @override - String get toInviteSomeoneToPlayGiveThisUrl => 'برای دعوت کردن حریف این لینک را برای او بفرستید'; + String get toInviteSomeoneToPlayGiveThisUrl => 'برای دعوت کسی به بازی، این وب‌نشانی را دهید'; @override String get gameOver => 'پایان بازی'; @@ -1574,10 +1799,10 @@ class AppLocalizationsFa extends AppLocalizations { String get blackLeftTheGame => 'سیاه بازی را ترک کرد'; @override - String get whiteDidntMove => 'سفید تکان نخورد'; + String get whiteDidntMove => 'سفید بازی نکرد'; @override - String get blackDidntMove => 'مشکی تکان نخورد'; + String get blackDidntMove => 'سیاه بازی نکرد'; @override String get requestAComputerAnalysis => 'درخواست تحلیل رایانه‌ای'; @@ -1609,7 +1834,7 @@ class AppLocalizationsFa extends AppLocalizations { String get calculatingMoves => 'در حال محاسبه حرکات...'; @override - String get engineFailed => 'خطا در بارگذاری پردازشگر شطرنج'; + String get engineFailed => 'خطا در بارگذاری پردازشگر'; @override String get cloudAnalysis => 'تحلیل ابری'; @@ -1654,7 +1879,7 @@ class AppLocalizationsFa extends AppLocalizations { String get variantLoss => 'حرکت بازنده'; @override - String get variantWin => 'حرکت برنده'; + String get variantWin => 'بُردِ شطرنج‌گونه'; @override String get insufficientMaterial => 'مُهره ناکافی برای مات'; @@ -1669,7 +1894,7 @@ class AppLocalizationsFa extends AppLocalizations { String get close => 'بستن'; @override - String get winning => 'حرکت پیروزی‌بخش'; + String get winning => 'حرکت برنده'; @override String get losing => 'حرکت بازنده'; @@ -1712,7 +1937,7 @@ class AppLocalizationsFa extends AppLocalizations { String get maxDepthReached => 'عمق به حداکثر رسیده!'; @override - String get maybeIncludeMoreGamesFromThePreferencesMenu => 'ممکنه بازی‌های بیشتری با توجه به گزینگان ترجیح‌ها، وجود داشته باشد؟'; + String get maybeIncludeMoreGamesFromThePreferencesMenu => 'شاید بازی‌های بیشتری با توجه به نام‌چین تنظیمات، وجود داشته باشد.'; @override String get openings => 'گشایش‌ها'; @@ -1732,7 +1957,7 @@ class AppLocalizationsFa extends AppLocalizations { String get playFirstOpeningEndgameExplorerMove => 'نخستین حرکت گشایش/آخربازی پویشگر را برو'; @override - String get winPreventedBy50MoveRule => 'قانون پنجاه حرکت از پیروزی جلوگیری کرد'; + String get winPreventedBy50MoveRule => 'قانون پنجاه حرکت جلوی پیروزی را گرفت'; @override String get lossSavedBy50MoveRule => 'قانون ۵۰ حرکت از شکست جلوگیری کرد'; @@ -1765,7 +1990,7 @@ class AppLocalizationsFa extends AppLocalizations { String get realtimeReplay => 'مشابه بازی'; @override - String get byCPL => 'درنگ حین اشتباهات'; + String get byCPL => 'درنگ هنگام اشتباه'; @override String get openStudy => 'گشودن مطالعه'; @@ -1797,9 +2022,6 @@ class AppLocalizationsFa extends AppLocalizations { @override String get removesTheDepthLimit => 'محدودیت عمق را برمی‌دارد و رایانه‌تان داغ می‌ماند'; - @override - String get engineManager => 'مدیر موتور شطرنج'; - @override String get blunder => 'اشتباه فاحش'; @@ -1849,13 +2071,13 @@ class AppLocalizationsFa extends AppLocalizations { String get rememberMe => 'مرا به خاطر بسپار'; @override - String get youNeedAnAccountToDoThat => 'شما برای انجام این کار به یک حساب کاربری نیاز دارید.'; + String get youNeedAnAccountToDoThat => 'برای انجام آن به یک حساب نیازمندید'; @override String get signUp => 'نام نویسی'; @override - String get computersAreNotAllowedToPlay => 'كامپيوتر و بازيكناني كه از كامپيوتر كمك مي گيرند،مجاز به بازی نیستند.لطفا از انجین شطرنج يا دیتابیس شطرنج و يا بازيكنان ديگر كمک نگيريد. همچنین توجه کنید که داشتن چند حساب کاربری به شدت نهی شده است. استفاده فزاینده از چند حساب منجر به مسدود شدن حساب شما خواهد شد.'; + String get computersAreNotAllowedToPlay => 'رایانه ها و بازیکنان رایانه-یاریده، مجاز به بازی نیستند. لطفا هنگام بازی از موتورهای شطرنج، دادگان‌ها یا دیگر بازیکنان کمک نگیرید. همچنین توجه کنید که ساخت چندین حساب به شدت ممنوع است و چند حسابی افزاینده، منجر به بستن‌تان می‌شود.'; @override String get games => 'بازی ها'; @@ -1865,11 +2087,11 @@ class AppLocalizationsFa extends AppLocalizations { @override String xPostedInForumY(String param1, String param2) { - return '$param1 در انجمن،موضوع $param2 را پست کرد.'; + return '$param1 در موضوع $param2، پیامی نوشت'; } @override - String get latestForumPosts => 'آخرین پست های انجمن'; + String get latestForumPosts => 'آخرین فرسته‌های انجمن'; @override String get players => 'بازیکنان'; @@ -1941,7 +2163,7 @@ class AppLocalizationsFa extends AppLocalizations { String get signupUsernameHint => 'مطمئن شوید که یک نام کاربری مناسب انتخاب میکنید. بعداً نمی توانید آن را تغییر دهید و هر حسابی با نام کاربری نامناسب بسته می شود!'; @override - String get signupEmailHint => 'ما فقط از آن برای تنظیم مجدد رمز عبور استفاده خواهیم کرد.'; + String get signupEmailHint => 'ما فقط برای بازنشاندن گذرواژه، از آن استفاده خواهیم کرد.'; @override String get password => 'رمز عبور'; @@ -1991,7 +2213,7 @@ class AppLocalizationsFa extends AppLocalizations { String get emailConfirmHelp => 'کمک با تائید ایمیل'; @override - String get emailConfirmNotReceived => 'آیا ایمیل تائید بعد از ثبت نام را دریافت نکرده اید؟'; + String get emailConfirmNotReceived => 'آیا رایانامهٔ تاییدتان را پس از نام‌نویسی دریافت نکردید؟'; @override String get whatSignupUsername => 'از چه نام کاربری برای ثبت نام استفاده کردید؟'; @@ -2061,7 +2283,10 @@ class AppLocalizationsFa extends AppLocalizations { } @override - String get gamesPlayed => 'تعداد بازی های انجام شده'; + String get gamesPlayed => 'بازی‌های انجامیده'; + + @override + String get ok => 'بسیار خب'; @override String get cancel => 'لغو'; @@ -2184,7 +2409,7 @@ class AppLocalizationsFa extends AppLocalizations { String get youHaveBeenTimedOut => 'زمان شما به پایان رسید.'; @override - String get spectatorRoom => 'اتاق تماشاچیان'; + String get spectatorRoom => 'اتاق تماشاگران'; @override String get composeMessage => 'نوشتن پیام'; @@ -2235,7 +2460,7 @@ class AppLocalizationsFa extends AppLocalizations { String get yourOpponentProposesATakeback => 'حریف پیشنهاد پس گرفتن حرکت می دهد'; @override - String get bookmarkThisGame => 'نشان گذاری بازی'; + String get bookmarkThisGame => 'نشانک‌گذاری'; @override String get tournament => 'مسابقه'; @@ -2247,7 +2472,7 @@ class AppLocalizationsFa extends AppLocalizations { String get tournamentPoints => 'مجموع امتیازات مسابقات'; @override - String get viewTournament => 'مشاهده مسابقات'; + String get viewTournament => 'دیدن مسابقات'; @override String get backToTournament => 'برگشت به مسابقه'; @@ -2302,7 +2527,7 @@ class AppLocalizationsFa extends AppLocalizations { String get backToGame => 'بازگشت به بازی'; @override - String get siteDescription => 'کارساز برخط و رایگان شطرنج. با میانایی روان شطرنج بازی کنید. بدون ثبت‌نام، بدون تبلیغ، بدون نیاز به افزونه. با رایانه، دوستان یا حریفان تصادفی شطرنج بازی کنید.'; + String get siteDescription => 'کارساز برخط و رایگان شطرنج. با میانایی روان، شطرنج بازی کنید. بدون نام‌نویسی، بدون تبلیغ، بدون نیاز به افزونه. با رایانه، دوستان یا حریفان تصادفی شطرنج بازی کنید.'; @override String xJoinedTeamY(String param1, String param2) { @@ -2401,7 +2626,7 @@ class AppLocalizationsFa extends AppLocalizations { String get retry => 'تلاش دوباره'; @override - String get reconnecting => 'در حال اتصال دوباره'; + String get reconnecting => 'در حال بازاتصال'; @override String get noNetwork => 'بُرون‌خط'; @@ -2410,22 +2635,22 @@ class AppLocalizationsFa extends AppLocalizations { String get favoriteOpponents => 'رقبای مورد علاقه'; @override - String get follow => 'دنبال کردن'; + String get follow => 'دنبالیدن'; @override - String get following => 'افرادی که دنبال می کنید'; + String get following => 'دنبالنده'; @override - String get unfollow => 'لغو دنبال کردن'; + String get unfollow => 'وادنبالیدن'; @override String followX(String param) { - return 'دنبال کردن $param'; + return 'دنبالیدن $param'; } @override String unfollowX(String param) { - return 'لغو دنبال کردن $param'; + return 'وادنبالیدن $param'; } @override @@ -2438,11 +2663,11 @@ class AppLocalizationsFa extends AppLocalizations { String get unblock => 'لغو انسداد'; @override - String get followsYou => 'افرادی که شما را دنبال می کنند'; + String get followsYou => 'شما را می‌دنبالد'; @override String xStartedFollowingY(String param1, String param2) { - return '$param1 $param2 را فالو کرد'; + return '$param1 دنبالیدن $param2 را آغازید'; } @override @@ -2561,7 +2786,7 @@ class AppLocalizationsFa extends AppLocalizations { String get whiteWins => 'پیروزی با مهره سفید'; @override - String get blackWins => 'سیاه برنده شد'; + String get blackWins => 'سیاه می‌برد'; @override String get drawRate => 'نرخ تساوی'; @@ -2682,13 +2907,13 @@ class AppLocalizationsFa extends AppLocalizations { String get success => 'موفق شدید'; @override - String get automaticallyProceedToNextGameAfterMoving => 'حرکت کردن اتوماتیک برای بازی بعدی بعد از حرکت کردن'; + String get automaticallyProceedToNextGameAfterMoving => 'پس از حرکت، خودکار به بازی بعدی روید'; @override String get autoSwitch => 'تعویض خودکار'; @override - String get puzzles => 'معماها'; + String get puzzles => 'معما'; @override String get onlineBots => 'ربات‌های بَرخط'; @@ -2703,7 +2928,7 @@ class AppLocalizationsFa extends AppLocalizations { String get descPrivate => 'توضیحات خصوصی'; @override - String get descPrivateHelp => 'متنی که فقط اعضای تیم مشاهده خواهند کرد. در صورت تعیین، جایگزین توضیحات عمومی برای اعضای تیم خواهد شد.'; + String get descPrivateHelp => 'متنی که فقط هم‌تیمی‌ها خواهند دید. در صورت تعیین، جایگزین وصف همگانی برای هم‌تیمی‌ها می‌شود خواهد شد.'; @override String get no => 'نه'; @@ -2727,10 +2952,10 @@ class AppLocalizationsFa extends AppLocalizations { String get topics => 'مباحث'; @override - String get posts => 'پست ها'; + String get posts => 'فرسته‌ها'; @override - String get lastPost => 'آخرین ارسال'; + String get lastPost => 'آخرین فرسته'; @override String get views => 'نمایش ها'; @@ -2769,17 +2994,23 @@ class AppLocalizationsFa extends AppLocalizations { String get troll => 'وِزُل'; @override - String get other => 'موضوعات دیگر'; + String get other => 'دیگر'; @override - String get reportDescriptionHelp => 'لینک بازی های این کاربر را قرار دهید و توضیع دهید خطای رفتار این بازیکن چه بوده است'; + String get reportCheatBoostHelp => 'پیوند بازی(ها) را جای‌گذارید و بشرحید که چه رفتاری از این کاربر مشکل دارد. فقط نگویید «آنها تقلب‌کارند»، بلکه به ما بگویید چطور به این نتیجه رسیده‌اید.'; + + @override + String get reportUsernameHelp => 'بشرحید چه چیز این نام‌کاربری آزارنده است. فقط نگویید «آزارنده/نامناسب است»، بلکه به ما بگویید چطور به این نتیجه رسیده‌اید، به‌ویژه اگر توهین: گنگ است، انگلیسی نیست، کوچه‌بازاری است، یا یک ارجاع تاریخی/فرهنگی است.'; + + @override + String get reportProcessedFasterInEnglish => 'اگر انگلیسی بنویسید، زودتر به گزارش‌تان رسیدگی خواهد شد.'; @override String get error_provideOneCheatedGameLink => 'لطفآ حداقل یک نمونه تقلب در بازی را مطرح کنید.'; @override String by(String param) { - return 'توسط $param'; + return 'به‌دستِ $param'; } @override @@ -2791,25 +3022,25 @@ class AppLocalizationsFa extends AppLocalizations { String get thisTopicIsNowClosed => 'این موضوع بسته شده است'; @override - String get blog => 'بلاگ'; + String get blog => 'وبنوشت'; @override - String get notes => 'یادداشت ها'; + String get notes => 'یادداشت‌ها'; @override - String get typePrivateNotesHere => 'یادداشت خصوصی را اینجا وارد کنید'; + String get typePrivateNotesHere => 'یادداشت‌های خصوصی را اینجا بنویسید'; @override String get writeAPrivateNoteAboutThisUser => 'یک یادداشت خصوصی درباره این کاربر بنویسید'; @override - String get noNoteYet => 'تا الان، بدون یادداشت'; + String get noNoteYet => 'تاکنون، بدون یادداشت'; @override String get invalidUsernameOrPassword => 'نام کاربری یا رمز عبور نادرست است'; @override - String get incorrectPassword => 'رمزعبور اشتباه'; + String get incorrectPassword => 'گذرواژه‌ی نادرست'; @override String get invalidAuthenticationCode => 'کد اصالت سنجی نامعتبر'; @@ -2845,7 +3076,7 @@ class AppLocalizationsFa extends AppLocalizations { String get privacyPolicy => 'سیاست حریم شخصی'; @override - String get letOtherPlayersFollowYou => 'بقیه بازیکنان شما را دنبال کنند'; + String get letOtherPlayersFollowYou => 'اجازه دهید دیگر بازیکنان شما را بدنبالند'; @override String get letOtherPlayersChallengeYou => 'اجازه دهید بازیکنان دیگر به شما پیشنهاد بازی دهند'; @@ -2916,7 +3147,7 @@ class AppLocalizationsFa extends AppLocalizations { String get timeline => 'جدول زمانی'; @override - String get starting => 'شروع'; + String get starting => 'آغاز:'; @override String get allInformationIsPublicAndOptional => 'تمامی اطلاعات عمومی و اختیاری است.'; @@ -2952,7 +3183,7 @@ class AppLocalizationsFa extends AppLocalizations { String get practice => 'تمرین کردن'; @override - String get community => 'انجمن'; + String get community => 'همدارگان'; @override String get tools => 'ابزارها'; @@ -3013,7 +3244,7 @@ class AppLocalizationsFa extends AppLocalizations { String get onlyFriends => 'فقط دوستان'; @override - String get menu => 'فهرست'; + String get menu => 'نام‌چین'; @override String get castling => 'قلعه‌روی'; @@ -3030,7 +3261,7 @@ class AppLocalizationsFa extends AppLocalizations { } @override - String get watchGames => 'تماشای بازی ها'; + String get watchGames => 'تماشای بازی‌ها'; @override String tpTimeSpentOnTV(String param) { @@ -3038,7 +3269,7 @@ class AppLocalizationsFa extends AppLocalizations { } @override - String get watch => 'نگاه کردن'; + String get watch => 'تماشا'; @override String get videoLibrary => 'فیلم ها'; @@ -3075,7 +3306,7 @@ class AppLocalizationsFa extends AppLocalizations { String get termsOfService => 'قوانین'; @override - String get sourceCode => 'منبع کد لایچس'; + String get sourceCode => 'کد منبع'; @override String get simultaneousExhibitions => 'نمایش هم زمان'; @@ -3176,7 +3407,7 @@ class AppLocalizationsFa extends AppLocalizations { String get keyCycleSelectedVariation => 'چرخه شاخه اصلی انتخاب‌شده'; @override - String get keyShowOrHideComments => 'نمایش/ پنهان کردن نظرات'; + String get keyShowOrHideComments => 'نمایش/پنهان کردن نظرها'; @override String get keyEnterOrExitVariation => 'ورود / خروج به شاخه'; @@ -3185,7 +3416,7 @@ class AppLocalizationsFa extends AppLocalizations { String get keyRequestComputerAnalysis => 'درخواست تحلیل رایانه‌ای، از اشتباه‌های‌تان بیاموزید'; @override - String get keyNextLearnFromYourMistakes => 'بعدی (از اشتباهات خود درس بگیرید)'; + String get keyNextLearnFromYourMistakes => 'بعدی (از اشتباه‌های‌تان بیاموزید)'; @override String get keyNextBlunder => 'اشتباه فاحش بعدی'; @@ -3258,7 +3489,7 @@ class AppLocalizationsFa extends AppLocalizations { @override String userIsBetterThanPercentOfPerfTypePlayers(String param1, String param2, String param3) { - return '$param1 بهتر از $param2 از بازیکنان $param3 میباشد.'; + return '$param1 بهتر از $param2 بازیکنان $param3 است.'; } @override @@ -3323,10 +3554,10 @@ class AppLocalizationsFa extends AppLocalizations { String get crosstable => 'رودررو'; @override - String get youCanAlsoScrollOverTheBoardToMoveInTheGame => 'شما می توانید برای حرکت در بازی از صفحه استفاده کنید'; + String get youCanAlsoScrollOverTheBoardToMoveInTheGame => 'برای حرکت، روی صفحه بازی بِنَوَردید.'; @override - String get scrollOverComputerVariationsToPreviewThem => 'برای مشاهده آن ها اسکرول کنید.'; + String get scrollOverComputerVariationsToPreviewThem => 'برای پیش‌نمایش آن‌ها، روی شاخه‌های رایانه‌ای بِنَوَردید.'; @override String get analysisShapesHowTo => 'و کلیک کنید یا راست کلیک کنید تا دایره یا فلش در صفحه بکشید shift'; @@ -3364,7 +3595,7 @@ class AppLocalizationsFa extends AppLocalizations { } @override - String get askYourChessTeacherAboutLiftingKidMode => 'اکانت شما مدیریت شده است. برای برداشتن حالت کودک از معلم شطرنج خود درخواست کنید.'; + String get askYourChessTeacherAboutLiftingKidMode => 'حسابتان مدیریت می‌شود. از آموزگار شطرنج‌تان درباره برداشتن حالت کودک بپرسید.'; @override String get enableKidMode => 'فعال کردن حالت کودکانه'; @@ -3400,7 +3631,7 @@ class AppLocalizationsFa extends AppLocalizations { String get fullFeatured => 'با تمامی امکانات'; @override - String get phoneAndTablet => 'موبایل و تبلت'; + String get phoneAndTablet => 'گوشی و رایانک'; @override String get bulletBlitzClassical => 'گلوله‌ای، برق‌آسا، مرسوم'; @@ -3415,24 +3646,24 @@ class AppLocalizationsFa extends AppLocalizations { String get viewTheSolution => 'دیدن راه‌حل'; @override - String get followAndChallengeFriends => 'دنبال کردن و پیشنهاد بازی دادن به دوستان'; + String get followAndChallengeFriends => 'دنبالیدن و پیشنهاد بازی دادن به دوستان'; @override String get gameAnalysis => 'تجزیه و تحلیلِ بازی'; @override String xHostsY(String param1, String param2) { - return '$param1 میزبان ها $param2'; + return '$param1 میزبان $param2 است'; } @override String xJoinsY(String param1, String param2) { - return '$param1 وارد می شود $param2'; + return '$param1 به $param2 می‌پیوندد'; } @override String xLikesY(String param1, String param2) { - return '$param1 می پسندد $param2'; + return '$param1، $param2 را می‌پسندد'; } @override @@ -3510,10 +3741,10 @@ class AppLocalizationsFa extends AppLocalizations { String get usernameUnacceptable => 'این نام کاربری قابل قبول نیست.'; @override - String get playChessInStyle => 'استایل شطرنج باز داشته باشید!'; + String get playChessInStyle => 'شطرنج‌بازیِ نوگارانه'; @override - String get chessBasics => 'اصول شطرنج'; + String get chessBasics => 'پایه‌های شطرنج'; @override String get coaches => 'مربی ها'; @@ -3569,7 +3800,7 @@ class AppLocalizationsFa extends AppLocalizations { String get computerThinking => 'محاسبه رایانه‌ای ...'; @override - String get seeBestMove => 'مشاهده بهترین حرکت'; + String get seeBestMove => 'دیدن بهترین حرکت'; @override String get hideBestMove => 'پنهان کردن بهترین حرکت'; @@ -3581,13 +3812,13 @@ class AppLocalizationsFa extends AppLocalizations { String get evaluatingYourMove => 'در حال بررسی حرکت شما...'; @override - String get whiteWinsGame => 'سفید برنده شد'; + String get whiteWinsGame => 'سفید می‌برد'; @override - String get blackWinsGame => 'سیاه برنده شد'; + String get blackWinsGame => 'سیاه می‌برد'; @override - String get learnFromYourMistakes => 'از اشتباهات خود درس بگیرید'; + String get learnFromYourMistakes => 'از اشتباه‌های‌تان بیاموزید'; @override String get learnFromThisMistake => 'از این اشتباه درس بگیرید'; @@ -3616,22 +3847,22 @@ class AppLocalizationsFa extends AppLocalizations { String get youCanDoBetter => 'می‌توانید بهتر انجامش دهید'; @override - String get tryAnotherMoveForWhite => 'برای سفید،حرکت دیگری را امتحان کنید'; + String get tryAnotherMoveForWhite => 'حرکت دیگری را برای سفید بیابید'; @override - String get tryAnotherMoveForBlack => 'برای سیاه،حرکت دیگری را امتحان کنید'; + String get tryAnotherMoveForBlack => 'حرکت دیگری را برای سیاه بیابید'; @override String get solution => 'راه‌حل'; @override - String get waitingForAnalysis => 'در انتظار برای آنالیز'; + String get waitingForAnalysis => 'در انتظار تحلیل'; @override - String get noMistakesFoundForWhite => 'هیچ اشتباهی برای سفید مشاهده نشد'; + String get noMistakesFoundForWhite => 'هیچی اشتباهی از سفید یافت نشد'; @override - String get noMistakesFoundForBlack => 'هیچ اشتباهی برای سیاه مشاهده نشد'; + String get noMistakesFoundForBlack => 'هیچی اشتباهی از سیاه یافت نشد'; @override String get doneReviewingWhiteMistakes => 'اشتباهات سفید بررسی شد'; @@ -3702,7 +3933,7 @@ class AppLocalizationsFa extends AppLocalizations { String get potentialProblem => 'زمانی که مشکلی احتمالی شناسایی شد ، این پیام را نمایش می دهیم.'; @override - String get howToAvoidThis => 'چگونه از این امر جلوگیری کنیم؟'; + String get howToAvoidThis => 'چگونه از آن بپرهیزیم؟'; @override String get playEveryGame => 'هر بازی‌ای که آغازیدید را، بازی کنید.'; @@ -3729,18 +3960,18 @@ class AppLocalizationsFa extends AppLocalizations { String get currentMatchScore => 'امتیاز بازی فعلی'; @override - String get agreementAssistance => 'من تضمین میکنم که در حین بازی ها کمک نگیرم ( از انجین ، کتاب ، پایگاه داده یا شخصی دیگر)'; + String get agreementAssistance => 'من موافقم که در طول بازی‌هایم هیچگاه کمکی نخواهم گرفت (از یک رایانه شطرنج، کتاب، دادگان یا شخص دیگری).'; @override - String get agreementNice => 'من تضمین میکنم که همیشه به بازیکن های دیگر احترام بگذارم.'; + String get agreementNice => 'می‌پذیرم که همواره به بازیکنان دیگر احترام گزارم.'; @override String agreementMultipleAccounts(String param) { - return 'من موافقت می‌کنم که چندین اکانت برای خودم ایجاد نکنم(به جز دلایلی که در $param اشاره شده).'; + return 'موافقم که چندین حساب نخواهم ساخت (جز به دلیل‌های ذکر شده در $param).'; } @override - String get agreementPolicy => 'من تضمین میکنم که به تمام قوانین و خط مشی های لیچس پایبند باشم .'; + String get agreementPolicy => 'با پیروی از همهٔ خط‌مشی‌های Lichess، موافقم.'; @override String get searchOrStartNewDiscussion => 'جستجو یا شروع کردن مکالمه جدید'; @@ -3821,7 +4052,7 @@ class AppLocalizationsFa extends AppLocalizations { @override String joinTheTeamXToPost(String param1) { - return 'به $param1 ملحق شوید تا در این انجمن پست بگذارید'; + return 'برای فرسته گذاشتن در این انجمن، به $param1 بپیوندید'; } @override @@ -3830,7 +4061,7 @@ class AppLocalizationsFa extends AppLocalizations { } @override - String get youCannotPostYetPlaySomeGames => 'شما هنوز قادر به پست گذاشتن در انجمن نیستید. چند بازی انجام دهید!'; + String get youCannotPostYetPlaySomeGames => 'هنوز نمی‌توانید در انجمن‌ها فرسته گذارید. چند بازی کنید!'; @override String get subscribe => 'مشترک شدن'; @@ -3845,7 +4076,7 @@ class AppLocalizationsFa extends AppLocalizations { @override String xMentionedYouInY(String param1, String param2) { - return '$param1 اسم شما را در \"$param2\" ذکر کرده است.'; + return '$param1 از شما در \"$param2\" نام برد.'; } @override @@ -3870,7 +4101,7 @@ class AppLocalizationsFa extends AppLocalizations { String get someoneYouReportedWasBanned => 'شخصی که گزارش کردید مسدود شد'; @override - String get congratsYouWon => 'تبریک، شما برنده شدید!'; + String get congratsYouWon => 'شادباش، شما بُردید!'; @override String gameVsX(String param1) { @@ -3883,7 +4114,7 @@ class AppLocalizationsFa extends AppLocalizations { } @override - String get lostAgainstTOSViolator => 'شما در برابر کسی که قانون‌های Lichess را نقض کرده، امتیاز درجه‌بندی از دست دادید'; + String get lostAgainstTOSViolator => 'شما برابر کسی که قانون‌های Lichess را نقض کرده، امتیاز درجه‌بندی از دست دادید'; @override String refundXpointsTimeControlY(String param1, String param2) { @@ -4077,6 +4308,9 @@ class AppLocalizationsFa extends AppLocalizations { @override String get nothingToSeeHere => 'فعلا هیچی اینجا نیست.'; + @override + String get stats => 'آمار'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4126,8 +4360,8 @@ class AppLocalizationsFa extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count غیردقیق', - one: '$count غیردقیق', + other: '$count نادقیق', + one: '$count نادقیق', ); return '$_temp0'; } @@ -4170,8 +4404,8 @@ class AppLocalizationsFa extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count بازی مورد علاقه', - one: '$count بازی مورد علاقه', + other: '$count نشانک', + one: '$count نشانک', ); return '$_temp0'; } @@ -4412,8 +4646,8 @@ class AppLocalizationsFa extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count دنبال کننده‌', - one: '$count دنبال کننده‌', + other: '$count دنبال‌گر', + one: '$count دنبال‌گر', ); return '$_temp0'; } @@ -4423,8 +4657,8 @@ class AppLocalizationsFa extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count دنبال میکند', - one: '$count دنبال می کند', + other: '$count دنبالنده', + one: '$count دنبالنده', ); return '$_temp0'; } @@ -4478,8 +4712,8 @@ class AppLocalizationsFa extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count وبنوشته در انجمن', - one: '$count وبنوشته در انجمن', + other: '$count فرسته در انجمن', + one: '$count فرسته در انجمن', ); return '$_temp0'; } @@ -4610,7 +4844,7 @@ class AppLocalizationsFa extends AppLocalizations { String get stormHighscores => 'بالاترین امتیازها'; @override - String get stormViewBestRuns => 'مشاهده بهترین دورها'; + String get stormViewBestRuns => 'دیدن بهترین دورها'; @override String get stormBestRunOfDay => 'بهترین دور روز'; @@ -4723,9 +4957,514 @@ class AppLocalizationsFa extends AppLocalizations { @override String get streamerLichessStreamers => 'بَرخَط-محتواسازان Lichess'; + @override + String get studyPrivate => 'خصوصی'; + + @override + String get studyMyStudies => 'مطالعه‌های من'; + + @override + String get studyStudiesIContributeTo => 'مطالعه‌هایی که در آن شرکت دارم'; + + @override + String get studyMyPublicStudies => 'مطالعه‌های همگانی من'; + + @override + String get studyMyPrivateStudies => 'مطالعه‌های خصوصی من'; + + @override + String get studyMyFavoriteStudies => 'مطالعه‌های دلخواه من'; + + @override + String get studyWhatAreStudies => 'مطالعه‌ها چه هستند؟'; + + @override + String get studyAllStudies => 'همه مطالعه‌ها'; + + @override + String studyStudiesCreatedByX(String param) { + return 'مطالعه‌هایی که $param ساخته است'; + } + + @override + String get studyNoneYet => 'هنوز، هیچ.'; + + @override + String get studyHot => 'رواجیده'; + + @override + String get studyDateAddedNewest => 'تاریخ افزوده شدن (نوترین)'; + + @override + String get studyDateAddedOldest => 'تاریخ افزوده شدن (کهنه‌ترین)'; + + @override + String get studyRecentlyUpdated => 'تازگی به‌روزشده'; + + @override + String get studyMostPopular => 'محبوب‌ترین‌'; + + @override + String get studyAlphabetical => 'براساس حروف الفبا'; + + @override + String get studyAddNewChapter => 'افزودن بخش جدید'; + + @override + String get studyAddMembers => 'افزودن اعضا'; + + @override + String get studyInviteToTheStudy => 'دعوت به این مطالعه'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'لطفا تنها کسانی را دعوت کنید که شما را می‌شناسند و کنشگرانه می‌خواهند به این مطالعه بپیوندند.'; + + @override + String get studySearchByUsername => 'جستجو بر اساس نام کاربری'; + + @override + String get studySpectator => 'تماشاگر'; + + @override + String get studyContributor => 'مشارکت کننده'; + + @override + String get studyKick => 'اخراج'; + + @override + String get studyLeaveTheStudy => 'ترک مطالعه'; + + @override + String get studyYouAreNowAContributor => 'شما یک مشارکت کننده جدید هستید'; + + @override + String get studyYouAreNowASpectator => 'شما اکنون یک تماشاگرید'; + + @override + String get studyPgnTags => 'نشان های PGN'; + + @override + String get studyLike => 'پسندیدن'; + + @override + String get studyUnlike => 'نمی‌پسندم'; + + @override + String get studyNewTag => 'برچسب جدید'; + + @override + String get studyCommentThisPosition => 'یادداشت‌نویسی برای این وضعیت'; + + @override + String get studyCommentThisMove => 'یادداشت‌نویسی برای این حرکت'; + + @override + String get studyAnnotateWithGlyphs => 'حرکت‌نویسی به‌همراه علامت‌ها'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'این بخش برای تحلیل، بسیار کوتاه است.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'تنها مشارکت‌گران این مطالعه، می‌توانند درخواست تحلیل رایانه‌ای دهند.'; + + @override + String get studyGetAFullComputerAnalysis => 'یک تحلیل کامل رایانه‌ای کارساز-سو از شاخه اصلی بگیرید.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'مطمئن شوید که بخش کامل است. شما فقط یک بار می‌توانید درخواست تحلیل دهید.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'همه‌ی عضوهای همگام در وضعیت یکسانی باقی می‌مانند'; + + @override + String get studyShareChanges => 'تغییرها را در کارساز ذخیره کنید و با تماشاگران به اشتراک گذارید'; + + @override + String get studyPlaying => 'در حال انجام'; + + @override + String get studyShowEvalBar => 'نوار ارزیابی'; + + @override + String get studyFirst => 'اولین'; + + @override + String get studyPrevious => 'پیشین'; + + @override + String get studyNext => 'بعدی'; + + @override + String get studyLast => 'آخرین'; + @override String get studyShareAndExport => 'همرسانی و برون‏بُرد'; + @override + String get studyCloneStudy => 'همسانیدن'; + + @override + String get studyStudyPgn => 'PGN مطالعه'; + + @override + String get studyDownloadAllGames => 'بارگیری تمام بازی ها'; + + @override + String get studyChapterPgn => 'PGN ِ بخش'; + + @override + String get studyCopyChapterPgn => 'رونوشت‌گیری PGN'; + + @override + String get studyDownloadGame => 'بارگیری بازی'; + + @override + String get studyStudyUrl => 'وب‌نشانی مطالعه'; + + @override + String get studyCurrentChapterUrl => 'وب‌نشانی بخش جاری'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'می‌توانید این را در انجمن یا وبنوشت Lichessتان برای جاسازی قرار دهید'; + + @override + String get studyStartAtInitialPosition => 'در وضعیت نخستین بیاغازید'; + + @override + String studyStartAtX(String param) { + return 'آغاز از $param'; + } + + @override + String get studyEmbedInYourWebsite => 'در وبگاهتان قرار دهید'; + + @override + String get studyReadMoreAboutEmbedding => 'درباره قرار دادن (در سایت) بیشتر بخوانید'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'فقط مطالعه‌های همگانی می‌توانند جایگذاری شوند!'; + + @override + String get studyOpen => 'بگشایید'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1، به دست $param2 برای شما آورده شده است'; + } + + @override + String get studyStudyNotFound => 'مطالعه یافت نشد'; + + @override + String get studyEditChapter => 'ویرایش بخش'; + + @override + String get studyNewChapter => 'بخش نو'; + + @override + String studyImportFromChapterX(String param) { + return 'درونبُرد از $param'; + } + + @override + String get studyOrientation => 'جهت'; + + @override + String get studyAnalysisMode => 'حالت تجزیه تحلیل'; + + @override + String get studyPinnedChapterComment => 'یادداشت سنجاقیده‌ٔ بخش'; + + @override + String get studySaveChapter => 'ذخیره بخش'; + + @override + String get studyClearAnnotations => 'پاک کردن حرکت‌نویسی'; + + @override + String get studyClearVariations => 'پاک کردن تغییرات'; + + @override + String get studyDeleteChapter => 'حذف بخش'; + + @override + String get studyDeleteThisChapter => 'حذف این بخش. بازگشت وجود ندارد!'; + + @override + String get studyClearAllCommentsInThisChapter => 'همه دیدگاه‌ها، نمادها و شکل‌های ترسیم شده در این بخش، پاک شوند'; + + @override + String get studyRightUnderTheBoard => 'درست زیر صفحهٔ بازی'; + + @override + String get studyNoPinnedComment => 'هیچ'; + + @override + String get studyNormalAnalysis => 'تحلیل ساده'; + + @override + String get studyHideNextMoves => 'پنهان کردن حرکت بعدی'; + + @override + String get studyInteractiveLesson => 'درس میان‌کنشی'; + + @override + String studyChapterX(String param) { + return 'بخش $param'; + } + + @override + String get studyEmpty => 'خالی'; + + @override + String get studyStartFromInitialPosition => 'از وضعیت نخستین بیاغازید'; + + @override + String get studyEditor => 'ویرایشگر'; + + @override + String get studyStartFromCustomPosition => 'از وضعیت دلخواه بیاغازید'; + + @override + String get studyLoadAGameByUrl => 'بارگذاری بازی از وب‌نشانی‌ها'; + + @override + String get studyLoadAPositionFromFen => 'بار کردن وضعیت از FEN'; + + @override + String get studyLoadAGameFromPgn => 'باگذاری بازی با استفاده از فایل PGN'; + + @override + String get studyAutomatic => 'خودکار'; + + @override + String get studyUrlOfTheGame => 'وب‌نشانی بازی‌ها، یکی در هر خط'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'بازی‌ها را از $param1 یا $param2 بارگذاری نمایید'; + } + + @override + String get studyCreateChapter => 'ساخت بخش'; + + @override + String get studyCreateStudy => 'ساخت مطالعه'; + + @override + String get studyEditStudy => 'ویرایش مطالعه'; + + @override + String get studyVisibility => 'دیدگی'; + + @override + String get studyPublic => 'همگانی'; + + @override + String get studyUnlisted => 'فهرست‌نشده'; + + @override + String get studyInviteOnly => 'فقط توسط دعوتنامه'; + + @override + String get studyAllowCloning => 'اجازه همسانِش'; + + @override + String get studyNobody => 'هیچ کس'; + + @override + String get studyOnlyMe => 'تنها من'; + + @override + String get studyContributors => 'مشارکت‌کنندگان'; + + @override + String get studyMembers => 'اعضا'; + + @override + String get studyEveryone => 'همه'; + + @override + String get studyEnableSync => 'فعال کردن همگام سازی'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'بله: همه را در وضعیت یکسانی نگه دار'; + + @override + String get studyNoLetPeopleBrowseFreely => 'خیر: به مردم اجازه جستجوی آزادانه بده'; + + @override + String get studyPinnedStudyComment => 'یادداشت سنجاقیده به مطالعه'; + @override String get studyStart => 'آغاز'; + + @override + String get studySave => 'ذخیره'; + + @override + String get studyClearChat => 'پاک کردن گفتگو'; + + @override + String get studyDeleteTheStudyChatHistory => 'پیشینه گپِ مطالعه پاک شود؟ بازگشت وجود ندارد!'; + + @override + String get studyDeleteStudy => 'پاکیدن مطالعه'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'کل مطالعه پاک شود؟ بازگشت وجود ندارد! برای تایید، نام مطالعه را بنویسید: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'کجا می‌خواهید آنرا مطالعه کنید؟'; + + @override + String get studyGoodMove => 'حرکت خوب'; + + @override + String get studyMistake => 'اشتباه'; + + @override + String get studyBrilliantMove => 'حرکت درخشان'; + + @override + String get studyBlunder => 'اشتباه فاحش'; + + @override + String get studyInterestingMove => 'حرکت جالب'; + + @override + String get studyDubiousMove => 'حرکت مشکوک'; + + @override + String get studyOnlyMove => 'تک‌حرکت'; + + @override + String get studyZugzwang => 'اکراهی'; + + @override + String get studyEqualPosition => 'وضعیت برابر'; + + @override + String get studyUnclearPosition => 'وضعیت ناروشن'; + + @override + String get studyWhiteIsSlightlyBetter => 'سفید کمی بهتر است'; + + @override + String get studyBlackIsSlightlyBetter => 'سیاه کمی بهتر است'; + + @override + String get studyWhiteIsBetter => 'سفید بهتر است'; + + @override + String get studyBlackIsBetter => 'سیاه بهتر است'; + + @override + String get studyWhiteIsWinning => 'سفید می‌برد'; + + @override + String get studyBlackIsWinning => 'سیاه می‌برد'; + + @override + String get studyNovelty => 'روش و ایده‌ای نو در شروع بازی'; + + @override + String get studyDevelopment => 'گسترش'; + + @override + String get studyInitiative => 'ابتکار عمل'; + + @override + String get studyAttack => 'حمله'; + + @override + String get studyCounterplay => 'بازی‌متقابل'; + + @override + String get studyTimeTrouble => 'تنگی زمان'; + + @override + String get studyWithCompensation => 'دارای مزیت و برتری'; + + @override + String get studyWithTheIdea => 'با طرح'; + + @override + String get studyNextChapter => 'بخش بعدی'; + + @override + String get studyPrevChapter => 'بخش پیشین'; + + @override + String get studyStudyActions => 'عملگرهای مطالعه'; + + @override + String get studyTopics => 'موضوع‌ها'; + + @override + String get studyMyTopics => 'موضوع‌های من'; + + @override + String get studyPopularTopics => 'موضوع‌های محبوب'; + + @override + String get studyManageTopics => 'مدیریت موضوع‌ها'; + + @override + String get studyBack => 'بازگشت'; + + @override + String get studyPlayAgain => 'دوباره بازی کنید'; + + @override + String get studyWhatWouldYouPlay => 'در این وضعیت چطور بازی می‌کنید؟'; + + @override + String get studyYouCompletedThisLesson => 'تبریک! شما این درس را کامل کردید.'; + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count بخش', + one: '$count بخش', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count بازی', + one: '$count بازی', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count عضو', + one: '$count عضو', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'متن PGN خود را در اینجا بچسبانید، تا $count بازی', + one: 'متن PGN خود را در اینجا بچسبانید، تا $count بازی', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_fi.dart b/lib/l10n/l10n_fi.dart index ff4644cfd5..0228d6ce2d 100644 --- a/lib/l10n/l10n_fi.dart +++ b/lib/l10n/l10n_fi.dart @@ -58,7 +58,7 @@ class AppLocalizationsFi extends AppLocalizations { @override String mobilePlayersMatchingSearchTerm(String param) { - return 'Players with \"$param\"'; + return 'Pelaajat, joiden tunnuksesta löytyy \"$param\"'; } @override @@ -103,9 +103,6 @@ class AppLocalizationsFi extends AppLocalizations { @override String get mobileCancelTakebackOffer => 'Peruuta siirron peruutuspyyntö'; - @override - String get mobileCancelDrawOffer => 'Peruuta tasapeliehdotus'; - @override String get mobileWaitingForOpponentToJoin => 'Odotetaan vastustajan löytymistä...'; @@ -142,7 +139,7 @@ class AppLocalizationsFi extends AppLocalizations { String get mobileGreetingWithoutName => 'Hei'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobilePrefMagnifyDraggedPiece => 'Suurenna vedettävä nappula'; @override String get activityActivity => 'Toiminta'; @@ -246,6 +243,17 @@ class AppLocalizationsFi extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Pelasi $count $param2-kirjeshakkipeliä', + one: 'Pelasi $count $param2-kirjeshakkipelin', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,226 @@ class AppLocalizationsFi extends AppLocalizations { @override String get broadcastBroadcasts => 'Lähetykset'; + @override + String get broadcastMyBroadcasts => 'Omat lähetykset'; + @override String get broadcastLiveBroadcasts => 'Suorat lähetykset turnauksista'; + @override + String get broadcastBroadcastCalendar => 'Lähetyskalenteri'; + + @override + String get broadcastNewBroadcast => 'Uusi livelähetys'; + + @override + String get broadcastSubscribedBroadcasts => 'Tilatut lähetykset'; + + @override + String get broadcastAboutBroadcasts => 'Lähetyksistä'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Kuinka Lichess-lähetyksiä käytetään.'; + + @override + String get broadcastTheNewRoundHelp => 'Uudella kierroksella on samat jäsenet ja osallistujat kuin edellisellä.'; + + @override + String get broadcastAddRound => 'Lisää kierros'; + + @override + String get broadcastOngoing => 'Käynnissä'; + + @override + String get broadcastUpcoming => 'Tulossa'; + + @override + String get broadcastCompleted => 'Päättyneet'; + + @override + String get broadcastCompletedHelp => 'Lichess tunnistaa lähteenä olevista peleistä, milloin kierros on viety päätökseen. Lähteen puuttuessa voit käyttää tätä asetusta.'; + + @override + String get broadcastRoundName => 'Kierroksen nimi'; + + @override + String get broadcastRoundNumber => 'Kierroksen numero'; + + @override + String get broadcastTournamentName => 'Turnauksen nimi'; + + @override + String get broadcastTournamentDescription => 'Turnauksen lyhyt kuvaus'; + + @override + String get broadcastFullDescription => 'Täysimittainen kuvaus tapahtumasta'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Ei-pakollinen pitkä kuvaus lähetyksestä. $param1-muotoiluja voi käyttää. Pituus voi olla enintään $param2 merkkiä.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN:n lähde-URL'; + + @override + String get broadcastSourceUrlHelp => 'URL, josta Lichess hakee PGN-päivitykset. Sen täytyy olla julkisesti saatavilla internetissä.'; + + @override + String get broadcastSourceGameIds => 'Korkeintaan 64 Lichess-pelin tunnistenumeroa välilyönneillä eroteltuna.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Alkamisajankohta turnauksen paikallisella aikavyöhykkeellä: $param'; + } + + @override + String get broadcastStartDateHelp => 'Ei-pakollinen, laita jos tiedät milloin tapahtuma alkaa'; + + @override + String get broadcastCurrentGameUrl => 'Tämän pelin URL'; + + @override + String get broadcastDownloadAllRounds => 'Lataa kaikki kierrokset'; + + @override + String get broadcastResetRound => 'Nollaa tämä kierros'; + + @override + String get broadcastDeleteRound => 'Poista tämä kierros'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Poista kierros ja sen pelit lopullisesti.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Poista kaikki tämän kierroksen pelit. Lähteen on oltava aktiivinen, jotta pelit voidaan luoda uudelleen.'; + + @override + String get broadcastEditRoundStudy => 'Kierrostutkielman muokkaus'; + + @override + String get broadcastDeleteTournament => 'Poista tämä turnaus'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Poista lopullisesti koko turnaus, sen kaikki kierrokset ja kaikki pelit.'; + + @override + String get broadcastShowScores => 'Näytä pelaajien pisteet pelien tulosten pohjalta'; + + @override + String get broadcastReplacePlayerTags => 'Valinnainen: korvaa pelaajien nimet, vahvuusluvut ja arvonimet'; + + @override + String get broadcastFideFederations => 'FIDEn liitot'; + + @override + String get broadcastTop10Rating => 'Top 10 -vahvuuslukulista'; + + @override + String get broadcastFidePlayers => 'FIDE-pelaajat'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE-pelaajaa ei löytynyt'; + + @override + String get broadcastFideProfile => 'FIDE-profiili'; + + @override + String get broadcastFederation => 'Kansallinen liitto'; + + @override + String get broadcastAgeThisYear => 'Ikä tänä vuonna'; + + @override + String get broadcastUnrated => 'Pisteyttämätön'; + + @override + String get broadcastRecentTournaments => 'Viimeisimmät turnaukset'; + + @override + String get broadcastOpenLichess => 'Avaa Lichessissä'; + + @override + String get broadcastTeams => 'Joukkueet'; + + @override + String get broadcastBoards => 'Laudat'; + + @override + String get broadcastOverview => 'Pääsivu'; + + @override + String get broadcastSubscribeTitle => 'Tilaa ilmoitukset kunkin kierroksen alkamisesta. Käyttäjätunnuksesi asetuksista voit kytkeä ääni- ja puskuilmoitukset päälle tai pois.'; + + @override + String get broadcastUploadImage => 'Lisää turnauksen kuva'; + + @override + String get broadcastNoBoardsYet => 'Pelilautoja ei vielä ole. Ne tulevat näkyviin sitä mukaa, kun pelit ladataan tänne.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Alkuun on aikaa $param'; + } + + @override + String get broadcastStartVerySoon => 'Lähetys alkaa aivan pian.'; + + @override + String get broadcastNotYetStarted => 'Lähetys ei ole vielä alkanut.'; + + @override + String get broadcastOfficialWebsite => 'Virallinen verkkosivu'; + + @override + String get broadcastStandings => 'Tulostaulu'; + + @override + String broadcastIframeHelp(String param) { + return 'Lisäasetuksia löytyy $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasterin sivulta'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'Tämän kierroksen julkinen ja reaaliaikainen PGN-tiedosto. Nopeampaan ja tehokkaampaan synkronisointiin on tarjolla myös $param.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Upota tämä lähetys sivustoosi'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Upota $param sivustoosi'; + } + + @override + String get broadcastRatingDiff => 'Vahvuuslukujen erotus'; + + @override + String get broadcastGamesThisTournament => 'Pelit tässä turnauksessa'; + + @override + String get broadcastScore => 'Pisteet'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count lähetystä', + one: '$count lähetys', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Haasteet: $param1'; @@ -1390,10 +1615,10 @@ class AppLocalizationsFi extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Vastustajalla on rajoitettu määrä mahdollisia siirtoja, ja niistä kaikki heikentävät hänen asemaansa.'; @override - String get puzzleThemeHealthyMix => 'Terve sekoitus'; + String get puzzleThemeMix => 'Terve sekoitus'; @override - String get puzzleThemeHealthyMixDescription => 'Vähän kaikkea. Et tiedä mitä tuleman pitää, joten olet valmiina mihin tahansa! Aivan kuten oikeissa peleissäkin.'; + String get puzzleThemeMixDescription => 'Vähän kaikkea. Et tiedä mitä tuleman pitää, joten olet valmiina mihin tahansa! Aivan kuten oikeissa peleissäkin.'; @override String get puzzleThemePlayerGames => 'Pelaajan peleistä'; @@ -1797,9 +2022,6 @@ class AppLocalizationsFi extends AppLocalizations { @override String get removesTheDepthLimit => 'Poistaa syvyysrajoituksen ja pitää koneesi lämpöisenä'; - @override - String get engineManager => 'Moottorin hallinta'; - @override String get blunder => 'Vakava virhe'; @@ -2063,6 +2285,9 @@ class AppLocalizationsFi extends AppLocalizations { @override String get gamesPlayed => 'Pelattuja pelejä'; + @override + String get ok => 'OK'; + @override String get cancel => 'Peruuta'; @@ -2772,7 +2997,13 @@ class AppLocalizationsFi extends AppLocalizations { String get other => 'Muu'; @override - String get reportDescriptionHelp => 'Liitä linkki peliin/peleihin ja kerro, mikä on pielessä tämän käyttäjän käytöksessä. Älä vain sano että \"hän huijaa\", vaan kerro meille miksi ajattelet näin. Raporttisi käydään läpi nopeammin, jos se on kirjoitettu englanniksi.'; + String get reportCheatBoostHelp => 'Liitä linkki peliin/peleihin ja kerro, mikä tämän käyttäjän toiminnassa on pielessä. Älä vain sano hänen huijaavan, vaan kerro meille, miksi olet päätellyt niin.'; + + @override + String get reportUsernameHelp => 'Selitä, mikä tässä käyttäjätunnuksessa on loukkaavaa. Älä vain sano sen olevan loukkaava tai sopimaton, vaan kerro meille, mihin näkemyksesi perustuu, varsinkin jos loukkaus on epäsuora, muun kuin englanninkielinen, slangia, tai jos siinä viitataan kulttuuriin tai historiaan.'; + + @override + String get reportProcessedFasterInEnglish => 'Ilmoituksesi käsitellään nopeammin, jos se on kirjoitettu englanniksi.'; @override String get error_provideOneCheatedGameLink => 'Anna ainakin yksi linkki peliin, jossa epäilet huijaamista.'; @@ -4077,6 +4308,9 @@ class AppLocalizationsFi extends AppLocalizations { @override String get nothingToSeeHere => 'Täällä ei ole tällä hetkellä mitään nähtävää.'; + @override + String get stats => 'Tilastot'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4723,9 +4957,514 @@ class AppLocalizationsFi extends AppLocalizations { @override String get streamerLichessStreamers => 'Lichess-striimaajat'; + @override + String get studyPrivate => 'Yksityinen'; + + @override + String get studyMyStudies => 'Tutkielmani'; + + @override + String get studyStudiesIContributeTo => 'Tutkielmat joihin olen osallisena'; + + @override + String get studyMyPublicStudies => 'Julkiset tutkielmani'; + + @override + String get studyMyPrivateStudies => 'Yksityiset tutkielmani'; + + @override + String get studyMyFavoriteStudies => 'Suosikkitutkielmani'; + + @override + String get studyWhatAreStudies => 'Mitä ovat tutkielmat?'; + + @override + String get studyAllStudies => 'Kaikki tutkielmat'; + + @override + String studyStudiesCreatedByX(String param) { + return '$param luomat tutkielmat'; + } + + @override + String get studyNoneYet => 'Ei mitään.'; + + @override + String get studyHot => 'Suositut juuri nyt'; + + @override + String get studyDateAddedNewest => 'Julkaisupäivä (uusimmat)'; + + @override + String get studyDateAddedOldest => 'Julkaisupäivä (vanhimmat)'; + + @override + String get studyRecentlyUpdated => 'Viimeksi päivitetyt'; + + @override + String get studyMostPopular => 'Suosituimmat'; + + @override + String get studyAlphabetical => 'Aakkosjärjestyksessä'; + + @override + String get studyAddNewChapter => 'Lisää uusi luku'; + + @override + String get studyAddMembers => 'Lisää jäseniä'; + + @override + String get studyInviteToTheStudy => 'Kutsu tutkielmaan'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Kutsu vain ihmisiä, jotka tunnet ja jotka haluavat osallistua aktiivisesti.'; + + @override + String get studySearchByUsername => 'Hae käyttäjätunnuksella'; + + @override + String get studySpectator => 'Katsoja'; + + @override + String get studyContributor => 'Osallistuja'; + + @override + String get studyKick => 'Poista'; + + @override + String get studyLeaveTheStudy => 'Jätä tutkielma'; + + @override + String get studyYouAreNowAContributor => 'Olet nyt osallistuja'; + + @override + String get studyYouAreNowASpectator => 'Olet nyt katsoja'; + + @override + String get studyPgnTags => 'PGN-tunnisteet'; + + @override + String get studyLike => 'Tykkää'; + + @override + String get studyUnlike => 'Poista tykkäys'; + + @override + String get studyNewTag => 'Uusi tunniste'; + + @override + String get studyCommentThisPosition => 'Kommentoi asemaa'; + + @override + String get studyCommentThisMove => 'Kommentoi siirtoa'; + + @override + String get studyAnnotateWithGlyphs => 'Arvioi symbolein'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Luku on liian lyhyt analysoitavaksi.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Vain tutkielman osallistujat voivat pyytää tietokoneanalyysin.'; + + @override + String get studyGetAFullComputerAnalysis => 'Hanki palvelimelta täysi tietokoneanalyysi päälinjasta.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Varmista, että luku on valmis. Voit pyytää analyysiä vain kerran.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Kaikki SYNC-jäsenet pysyvät samassa asemassa'; + + @override + String get studyShareChanges => 'Anna katsojien nähdä muutokset ja tallenna ne palvelimelle'; + + @override + String get studyPlaying => 'Meneillään'; + + @override + String get studyShowEvalBar => 'Arviopalkit'; + + @override + String get studyFirst => 'Alkuun'; + + @override + String get studyPrevious => 'Edellinen'; + + @override + String get studyNext => 'Seuraava'; + + @override + String get studyLast => 'Loppuun'; + @override String get studyShareAndExport => 'Jaa & vie'; + @override + String get studyCloneStudy => 'Kloonaa'; + + @override + String get studyStudyPgn => 'Tutkielman PGN'; + + @override + String get studyDownloadAllGames => 'Lataa kaikki pelit'; + + @override + String get studyChapterPgn => 'Luvun PGN'; + + @override + String get studyCopyChapterPgn => 'Kopioi PGN'; + + @override + String get studyDownloadGame => 'Lataa peli'; + + @override + String get studyStudyUrl => 'Tutkielman URL'; + + @override + String get studyCurrentChapterUrl => 'Tämän luvun URL'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Voit upottaa tämän foorumiin liittämällä'; + + @override + String get studyStartAtInitialPosition => 'Aloita alkuperäisestä asemasta'; + + @override + String studyStartAtX(String param) { + return 'Aloita siirrosta $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Upota sivustoosi tai blogiisi'; + + @override + String get studyReadMoreAboutEmbedding => 'Lue lisää upottamisesta'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Vain julkiset tutkielmat voidaan upottaa!'; + + @override + String get studyOpen => 'Avaa'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1, sivustolta $param2'; + } + + @override + String get studyStudyNotFound => 'Tutkielmaa ei löydy'; + + @override + String get studyEditChapter => 'Muokkaa lukua'; + + @override + String get studyNewChapter => 'Uusi luku'; + + @override + String studyImportFromChapterX(String param) { + return 'Tuo luvusta $param'; + } + + @override + String get studyOrientation => 'Suunta'; + + @override + String get studyAnalysisMode => 'Analyysitila'; + + @override + String get studyPinnedChapterComment => 'Kiinnitetty lukukommentti'; + + @override + String get studySaveChapter => 'Tallenna luku'; + + @override + String get studyClearAnnotations => 'Poista kommentit'; + + @override + String get studyClearVariations => 'Tyhjennä muunnelmat'; + + @override + String get studyDeleteChapter => 'Poista luku'; + + @override + String get studyDeleteThisChapter => 'Poistetaanko tämä luku? Et voi palauttaa sitä enää!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Poista kaikki kommentit, symbolit ja piirtokuviot tästä luvusta?'; + + @override + String get studyRightUnderTheBoard => 'Heti laudan alla'; + + @override + String get studyNoPinnedComment => 'Ei'; + + @override + String get studyNormalAnalysis => 'Tavallinen analyysi'; + + @override + String get studyHideNextMoves => 'Piilota tulevat siirrot'; + + @override + String get studyInteractiveLesson => 'Interaktiivinen oppitunti'; + + @override + String studyChapterX(String param) { + return 'Luku $param'; + } + + @override + String get studyEmpty => 'Tyhjä'; + + @override + String get studyStartFromInitialPosition => 'Aloita alkuasemasta'; + + @override + String get studyEditor => 'Editori'; + + @override + String get studyStartFromCustomPosition => 'Aloita haluamastasi asemasta'; + + @override + String get studyLoadAGameByUrl => 'Lataa peli URL:stä'; + + @override + String get studyLoadAPositionFromFen => 'Lataa asema FEN:istä'; + + @override + String get studyLoadAGameFromPgn => 'Ota peli PGN:stä'; + + @override + String get studyAutomatic => 'Automaattinen'; + + @override + String get studyUrlOfTheGame => 'URL peliin'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Lataa peli lähteestä $param1 tai $param2'; + } + + @override + String get studyCreateChapter => 'Aloita luku'; + + @override + String get studyCreateStudy => 'Luo tutkielma'; + + @override + String get studyEditStudy => 'Muokkaa tutkielmaa'; + + @override + String get studyVisibility => 'Näkyvyys'; + + @override + String get studyPublic => 'Julkinen'; + + @override + String get studyUnlisted => 'Listaamaton'; + + @override + String get studyInviteOnly => 'Vain kutsutut'; + + @override + String get studyAllowCloning => 'Salli kloonaus'; + + @override + String get studyNobody => 'Ei kukaan'; + + @override + String get studyOnlyMe => 'Vain minä'; + + @override + String get studyContributors => 'Osallistujat'; + + @override + String get studyMembers => 'Jäsenet'; + + @override + String get studyEveryone => 'Kaikki'; + + @override + String get studyEnableSync => 'Synkronointi käyttöön'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Kyllä: pidä kaikki samassa asemassa'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Ei: anna ihmisten selata vapaasti'; + + @override + String get studyPinnedStudyComment => 'Kiinnitetty tutkielmakommentti'; + @override String get studyStart => 'Aloita'; + + @override + String get studySave => 'Tallenna'; + + @override + String get studyClearChat => 'Tyhjennä keskustelu'; + + @override + String get studyDeleteTheStudyChatHistory => 'Haluatko poistaa tutkielman keskusteluhistorian? Et voi palauttaa sitä enää!'; + + @override + String get studyDeleteStudy => 'Poista tutkielma'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Poistetaanko koko tutkielma? Et voi palauttaa sitä enää. Vahvista poisto kirjoittamalla tutkielman nimen: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Missä haluat tutkia tätä?'; + + @override + String get studyGoodMove => 'Hyvä siirto'; + + @override + String get studyMistake => 'Virhe'; + + @override + String get studyBrilliantMove => 'Loistava siirto'; + + @override + String get studyBlunder => 'Vakava virhe'; + + @override + String get studyInterestingMove => 'Mielenkiintoinen siirto'; + + @override + String get studyDubiousMove => 'Kyseenalainen siirto'; + + @override + String get studyOnlyMove => 'Ainoa siirto'; + + @override + String get studyZugzwang => 'Siirtopakko'; + + @override + String get studyEqualPosition => 'Tasainen asema'; + + @override + String get studyUnclearPosition => 'Epäselvä asema'; + + @override + String get studyWhiteIsSlightlyBetter => 'Valkealla on pieni etu'; + + @override + String get studyBlackIsSlightlyBetter => 'Mustalla on pieni etu'; + + @override + String get studyWhiteIsBetter => 'Valkealla on etu'; + + @override + String get studyBlackIsBetter => 'Mustalla on etu'; + + @override + String get studyWhiteIsWinning => 'Valkea on voitolla'; + + @override + String get studyBlackIsWinning => 'Musta on voitolla'; + + @override + String get studyNovelty => 'Uutuus'; + + @override + String get studyDevelopment => 'Kehitys'; + + @override + String get studyInitiative => 'Aloite'; + + @override + String get studyAttack => 'Hyökkäys'; + + @override + String get studyCounterplay => 'Vastapeli'; + + @override + String get studyTimeTrouble => 'Aikapula'; + + @override + String get studyWithCompensation => 'Kompensaatio'; + + @override + String get studyWithTheIdea => 'Ideana'; + + @override + String get studyNextChapter => 'Seuraava luku'; + + @override + String get studyPrevChapter => 'Edellinen luku'; + + @override + String get studyStudyActions => 'Tutkielmatoiminnot'; + + @override + String get studyTopics => 'Aiheet'; + + @override + String get studyMyTopics => 'Omat aiheeni'; + + @override + String get studyPopularTopics => 'Suositut aiheet'; + + @override + String get studyManageTopics => 'Aiheiden hallinta'; + + @override + String get studyBack => 'Takaisin'; + + @override + String get studyPlayAgain => 'Pelaa uudelleen'; + + @override + String get studyWhatWouldYouPlay => 'Mitä pelaisit tässä asemassa?'; + + @override + String get studyYouCompletedThisLesson => 'Onnittelut! Olet suorittanut tämän oppiaiheen.'; + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count lukua', + one: '$count luku', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count peliä', + one: '$count peli', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count jäsentä', + one: '$count jäsen', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Liitä PGN tähän, enintään $count peliä', + one: 'Liitä PGN tähän, enintään $count peli', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_fo.dart b/lib/l10n/l10n_fo.dart index cc6dc465d9..bdf2bcc58f 100644 --- a/lib/l10n/l10n_fo.dart +++ b/lib/l10n/l10n_fo.dart @@ -103,9 +103,6 @@ class AppLocalizationsFo extends AppLocalizations { @override String get mobileCancelTakebackOffer => 'Cancel takeback offer'; - @override - String get mobileCancelDrawOffer => 'Cancel draw offer'; - @override String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @@ -246,6 +243,17 @@ class AppLocalizationsFo extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Completed $count $param2 correspondence games', + one: 'Completed $count $param2 correspondence game', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,226 @@ class AppLocalizationsFo extends AppLocalizations { @override String get broadcastBroadcasts => 'Sendingar'; + @override + String get broadcastMyBroadcasts => 'My broadcasts'; + @override String get broadcastLiveBroadcasts => 'Beinleiðis sendingar frá kappingum'; + @override + String get broadcastBroadcastCalendar => 'Broadcast calendar'; + + @override + String get broadcastNewBroadcast => 'Nýggj beinleiðis sending'; + + @override + String get broadcastSubscribedBroadcasts => 'Subscribed broadcasts'; + + @override + String get broadcastAboutBroadcasts => 'About broadcasts'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'How to use Lichess Broadcasts.'; + + @override + String get broadcastTheNewRoundHelp => 'The new round will have the same members and contributors as the previous one.'; + + @override + String get broadcastAddRound => 'Add a round'; + + @override + String get broadcastOngoing => 'Í gongd'; + + @override + String get broadcastUpcoming => 'Komandi'; + + @override + String get broadcastCompleted => 'Liðug sending'; + + @override + String get broadcastCompletedHelp => 'Lichess detects round completion, but can get it wrong. Use this to set it manually.'; + + @override + String get broadcastRoundName => 'Round name'; + + @override + String get broadcastRoundNumber => 'Nummar á umfari'; + + @override + String get broadcastTournamentName => 'Tournament name'; + + @override + String get broadcastTournamentDescription => 'Short tournament description'; + + @override + String get broadcastFullDescription => 'Fullfíggjað lýsing av tiltaki'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Valfrí long lýsing av sending. $param1 er tøkt. Longdin má vera styttri enn $param2 bókstavir.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN Source URL'; + + @override + String get broadcastSourceUrlHelp => 'URL-leinki, ið Lichess fer at kanna til tess at fáa PGN dagføringar. Leinkið nýtist at vera alment atkomiligt á alnetinum.'; + + @override + String get broadcastSourceGameIds => 'Up to 64 Lichess game IDs, separated by spaces.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Start date in the tournament local timezone: $param'; + } + + @override + String get broadcastStartDateHelp => 'Valfrítt, um tú veitst, nær tiltakið byrjar'; + + @override + String get broadcastCurrentGameUrl => 'Current game URL'; + + @override + String get broadcastDownloadAllRounds => 'Download all rounds'; + + @override + String get broadcastResetRound => 'Reset this round'; + + @override + String get broadcastDeleteRound => 'Delete this round'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Definitively delete the round and all its games.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Delete all games of this round. The source will need to be active in order to re-create them.'; + + @override + String get broadcastEditRoundStudy => 'Edit round study'; + + @override + String get broadcastDeleteTournament => 'Delete this tournament'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Definitively delete the entire tournament, all its rounds and all its games.'; + + @override + String get broadcastShowScores => 'Show players scores based on game results'; + + @override + String get broadcastReplacePlayerTags => 'Optional: replace player names, ratings and titles'; + + @override + String get broadcastFideFederations => 'FIDE federations'; + + @override + String get broadcastTop10Rating => 'Top 10 rating'; + + @override + String get broadcastFidePlayers => 'FIDE players'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE player not found'; + + @override + String get broadcastFideProfile => 'FIDE profile'; + + @override + String get broadcastFederation => 'Federation'; + + @override + String get broadcastAgeThisYear => 'Age this year'; + + @override + String get broadcastUnrated => 'Unrated'; + + @override + String get broadcastRecentTournaments => 'Recent tournaments'; + + @override + String get broadcastOpenLichess => 'Open in Lichess'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Overview'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Official website'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'Score'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count broadcasts', + one: '$count broadcast', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Challenges: $param1'; @@ -1388,10 +1613,10 @@ class AppLocalizationsFo extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Mótleikarin hevur avmarkaðar møguleikar at flyta, og allir leikir gera støðu hansara verri.'; @override - String get puzzleThemeHealthyMix => 'Sunt bland'; + String get puzzleThemeMix => 'Sunt bland'; @override - String get puzzleThemeHealthyMixDescription => 'Eitt sindur av øllum. Tú veitst ikki, hvat tú kanst vænta tær, so ver til reiðar til alt! Júst sum í veruligum talvum.'; + String get puzzleThemeMixDescription => 'Eitt sindur av øllum. Tú veitst ikki, hvat tú kanst vænta tær, so ver til reiðar til alt! Júst sum í veruligum talvum.'; @override String get puzzleThemePlayerGames => 'Player games'; @@ -1795,9 +2020,6 @@ class AppLocalizationsFo extends AppLocalizations { @override String get removesTheDepthLimit => 'Tekur dýpdaravmarkingar burtur, og heldur telduna hjá tær heita'; - @override - String get engineManager => 'Engine manager'; - @override String get blunder => 'Bukkur'; @@ -2061,6 +2283,9 @@ class AppLocalizationsFo extends AppLocalizations { @override String get gamesPlayed => 'Talv telvað'; + @override + String get ok => 'OK'; + @override String get cancel => 'Ógilda'; @@ -2770,7 +2995,13 @@ class AppLocalizationsFo extends AppLocalizations { String get other => 'Annað'; @override - String get reportDescriptionHelp => 'Flyt leinkið til talvið ella talvini higar, og greið frá, hvat bagir atburðinum hjá brúkaranum. Skriva ikki bert \"hann snýtir\", men sig okkum, hvussu tú komst til hesa niðurstøðu. Fráboðan tín verður skjótari viðgjørd, um hon verður skrivað á enskum.'; + String get reportCheatBoostHelp => 'Paste the link to the game(s) and explain what is wrong about this user\'s behaviour. Don\'t just say \"they cheat\", but tell us how you came to this conclusion.'; + + @override + String get reportUsernameHelp => 'Explain what about this username is offensive. Don\'t just say \"it\'s offensive/inappropriate\", but tell us how you came to this conclusion, especially if the insult is obfuscated, not in english, is in slang, or is a historical/cultural reference.'; + + @override + String get reportProcessedFasterInEnglish => 'Your report will be processed faster if written in English.'; @override String get error_provideOneCheatedGameLink => 'Útvega leinki til í minsta lagi eitt talv, har snýtt varð.'; @@ -4075,6 +4306,9 @@ class AppLocalizationsFo extends AppLocalizations { @override String get nothingToSeeHere => 'Nothing to see here at the moment.'; + @override + String get stats => 'Stats'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4721,9 +4955,514 @@ class AppLocalizationsFo extends AppLocalizations { @override String get streamerLichessStreamers => 'Lichess stroymarar'; + @override + String get studyPrivate => 'Egin (privat)'; + + @override + String get studyMyStudies => 'Mínar rannsóknir'; + + @override + String get studyStudiesIContributeTo => 'Rannsóknir, eg gevi mítt íkast til'; + + @override + String get studyMyPublicStudies => 'Mínar almennu rannsóknir'; + + @override + String get studyMyPrivateStudies => 'Mínar egnu rannsóknir'; + + @override + String get studyMyFavoriteStudies => 'Mínar yndisrannsóknir'; + + @override + String get studyWhatAreStudies => 'Hvat eru rannsóknir?'; + + @override + String get studyAllStudies => 'Allar rannsóknir'; + + @override + String studyStudiesCreatedByX(String param) { + return '$param stovnaði hesar rannsóknir'; + } + + @override + String get studyNoneYet => 'Ongar enn.'; + + @override + String get studyHot => 'Heitar'; + + @override + String get studyDateAddedNewest => 'Eftir dagfesting (nýggjastu)'; + + @override + String get studyDateAddedOldest => 'Eftir dagfesting (eldstu)'; + + @override + String get studyRecentlyUpdated => 'Nýliga dagførdar'; + + @override + String get studyMostPopular => 'Best dámdu'; + + @override + String get studyAlphabetical => 'Alphabetical'; + + @override + String get studyAddNewChapter => 'Skoyt nýggjan kapittul upp í'; + + @override + String get studyAddMembers => 'Legg limir aftrat'; + + @override + String get studyInviteToTheStudy => 'Bjóða uppí rannsóknina'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Bjóða vinaliga bert fólki, tú kennir, og sum vilja taka virknan lut í rannsóknini.'; + + @override + String get studySearchByUsername => 'Leita eftir brúkaranavni'; + + @override + String get studySpectator => 'Áskoðari'; + + @override + String get studyContributor => 'Gevur íkast'; + + @override + String get studyKick => 'Koyr úr'; + + @override + String get studyLeaveTheStudy => 'Far úr rannsóknini'; + + @override + String get studyYouAreNowAContributor => 'Tú ert nú ein, ið leggur aftrat rannsóknini'; + + @override + String get studyYouAreNowASpectator => 'Tú ert nú áskoðari'; + + @override + String get studyPgnTags => 'PGN-frámerki'; + + @override + String get studyLike => 'Dáma'; + + @override + String get studyUnlike => 'Unlike'; + + @override + String get studyNewTag => 'Nýtt frámerki'; + + @override + String get studyCommentThisPosition => 'Viðmerk hesa støðuna'; + + @override + String get studyCommentThisMove => 'Viðmerk henda leikin'; + + @override + String get studyAnnotateWithGlyphs => 'Skriva við teknum'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Kapittulin er ov stuttur til at verða greinaður.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Bert tey, ið geva sítt íkast til rannsóknina, kunnu biðja um eina teldugreining.'; + + @override + String get studyGetAFullComputerAnalysis => 'Fá eina fullfíggjaða teldugreining av høvuðsbrigdinum frá ambætaranum.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Tryggja tær, at kapittulin er fullfíggjaður. Tú kanst bert biðja um greining eina ferð.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Allir SYNC-limir verða verandi í somu støðu'; + + @override + String get studyShareChanges => 'Deil broytingar við áskoðarar, og goym tær á ambætaranum'; + + @override + String get studyPlaying => 'Í gongd'; + + @override + String get studyShowEvalBar => 'Evaluation bars'; + + @override + String get studyFirst => 'Fyrsta'; + + @override + String get studyPrevious => 'Undanfarna'; + + @override + String get studyNext => 'Næsta'; + + @override + String get studyLast => 'Síðsta'; + @override String get studyShareAndExport => 'Deil & flyt út'; + @override + String get studyCloneStudy => 'Klona'; + + @override + String get studyStudyPgn => 'PGN rannsókn'; + + @override + String get studyDownloadAllGames => 'Tak øll talv niður'; + + @override + String get studyChapterPgn => 'PGN kapittul'; + + @override + String get studyCopyChapterPgn => 'Copy PGN'; + + @override + String get studyDownloadGame => 'Tak talv niður'; + + @override + String get studyStudyUrl => 'URL rannsókn'; + + @override + String get studyCurrentChapterUrl => 'Núverandi URL partur'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Tú kanst seta hetta inn í torgið at sýna tað har'; + + @override + String get studyStartAtInitialPosition => 'Byrja við byrjanarstøðuni'; + + @override + String studyStartAtX(String param) { + return 'Byrja við $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Fell inn í heimasíðu tína ella blogg tín'; + + @override + String get studyReadMoreAboutEmbedding => 'Les meira um at fella inn í'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Bert almennar rannsóknir kunnu verða feldar inn í!'; + + @override + String get studyOpen => 'Lat upp'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param2 fekk tær $param1 til vegar'; + } + + @override + String get studyStudyNotFound => 'Rannsókn ikki funnin'; + + @override + String get studyEditChapter => 'Broyt kapittul'; + + @override + String get studyNewChapter => 'Nýggjur kapittul'; + + @override + String studyImportFromChapterX(String param) { + return 'Import from $param'; + } + + @override + String get studyOrientation => 'Helling'; + + @override + String get studyAnalysisMode => 'Greiningarstøða'; + + @override + String get studyPinnedChapterComment => 'Føst viðmerking til kapittulin'; + + @override + String get studySaveChapter => 'Goym kapittulin'; + + @override + String get studyClearAnnotations => 'Strika viðmerkingar'; + + @override + String get studyClearVariations => 'Clear variations'; + + @override + String get studyDeleteChapter => 'Strika kapittul'; + + @override + String get studyDeleteThisChapter => 'Strika henda kapittulin? Til ber ikki at angra!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Skulu allar viðmerkingar, øll tekn og teknað skap strikast úr hesum kapitli?'; + + @override + String get studyRightUnderTheBoard => 'Beint undir talvborðinum'; + + @override + String get studyNoPinnedComment => 'Einki'; + + @override + String get studyNormalAnalysis => 'Vanlig greining'; + + @override + String get studyHideNextMoves => 'Fjal næstu leikirnar'; + + @override + String get studyInteractiveLesson => 'Samvirkin frálæra'; + + @override + String studyChapterX(String param) { + return 'Kapittul $param'; + } + + @override + String get studyEmpty => 'Tómur'; + + @override + String get studyStartFromInitialPosition => 'Byrja við byrjanarstøðuni'; + + @override + String get studyEditor => 'Ritstjóri'; + + @override + String get studyStartFromCustomPosition => 'Byrja við støðu, ið brúkari ger av'; + + @override + String get studyLoadAGameByUrl => 'Les inn talv frá URL'; + + @override + String get studyLoadAPositionFromFen => 'Les inn talvstøðu frá FEN'; + + @override + String get studyLoadAGameFromPgn => 'Les inn talv frá PGN'; + + @override + String get studyAutomatic => 'Sjálvvirkið'; + + @override + String get studyUrlOfTheGame => 'URL fyri talvini'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Les talv inn frá $param1 ella $param2'; + } + + @override + String get studyCreateChapter => 'Stovna kapittul'; + + @override + String get studyCreateStudy => 'Stovna rannsókn'; + + @override + String get studyEditStudy => 'Ritstjórna rannsókn'; + + @override + String get studyVisibility => 'Sýni'; + + @override + String get studyPublic => 'Almen'; + + @override + String get studyUnlisted => 'Ikki skrásett'; + + @override + String get studyInviteOnly => 'Bert innboðin'; + + @override + String get studyAllowCloning => 'Loyv kloning'; + + @override + String get studyNobody => 'Eingin'; + + @override + String get studyOnlyMe => 'Bert eg'; + + @override + String get studyContributors => 'Luttakarar'; + + @override + String get studyMembers => 'Limir'; + + @override + String get studyEveryone => 'Øll'; + + @override + String get studyEnableSync => 'Samstilling møgulig'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Ja: varðveit øll í somu støðu'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Nei: lat fólk kaga frítt'; + + @override + String get studyPinnedStudyComment => 'Føst rannsóknarviðmerking'; + @override String get studyStart => 'Byrja'; + + @override + String get studySave => 'Goym'; + + @override + String get studyClearChat => 'Rudda kjatt'; + + @override + String get studyDeleteTheStudyChatHistory => 'Skal kjattsøgan í rannsóknini strikast? Til ber ikki at angra!'; + + @override + String get studyDeleteStudy => 'Burturbein rannsókn'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Delete the entire study? There is no going back! Type the name of the study to confirm: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Hvar vilt tú rannsaka hatta?'; + + @override + String get studyGoodMove => 'Góður leikur'; + + @override + String get studyMistake => 'Mistak'; + + @override + String get studyBrilliantMove => 'Framúrskarandi leikur'; + + @override + String get studyBlunder => 'Bukkur'; + + @override + String get studyInterestingMove => 'Áhugaverdur leikur'; + + @override + String get studyDubiousMove => 'Ivasamur leikur'; + + @override + String get studyOnlyMove => 'Einasti leikur'; + + @override + String get studyZugzwang => 'Zugzwang'; + + @override + String get studyEqualPosition => 'Equal position'; + + @override + String get studyUnclearPosition => 'Unclear position'; + + @override + String get studyWhiteIsSlightlyBetter => 'White is slightly better'; + + @override + String get studyBlackIsSlightlyBetter => 'Black is slightly better'; + + @override + String get studyWhiteIsBetter => 'White is better'; + + @override + String get studyBlackIsBetter => 'Black is better'; + + @override + String get studyWhiteIsWinning => 'Hvítur stendur til at vinna'; + + @override + String get studyBlackIsWinning => 'Svartur stendur til at vinna'; + + @override + String get studyNovelty => 'Novelty'; + + @override + String get studyDevelopment => 'Development'; + + @override + String get studyInitiative => 'Initiative'; + + @override + String get studyAttack => 'Attack'; + + @override + String get studyCounterplay => 'Counterplay'; + + @override + String get studyTimeTrouble => 'Time trouble'; + + @override + String get studyWithCompensation => 'With compensation'; + + @override + String get studyWithTheIdea => 'With the idea'; + + @override + String get studyNextChapter => 'Next chapter'; + + @override + String get studyPrevChapter => 'Previous chapter'; + + @override + String get studyStudyActions => 'Study actions'; + + @override + String get studyTopics => 'Topics'; + + @override + String get studyMyTopics => 'My topics'; + + @override + String get studyPopularTopics => 'Popular topics'; + + @override + String get studyManageTopics => 'Manage topics'; + + @override + String get studyBack => 'Back'; + + @override + String get studyPlayAgain => 'Play again'; + + @override + String get studyWhatWouldYouPlay => 'What would you play in this position?'; + + @override + String get studyYouCompletedThisLesson => 'Congratulations! You completed this lesson.'; + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count kapitlar', + one: '$count kapittul', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count talv', + one: '$count talv', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count limir', + one: '$count limur', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Set PGN tekstin hjá tær inn her, upp til $count talv', + one: 'Set PGN tekstin hjá tær inn her, upp til $count talv', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_fr.dart b/lib/l10n/l10n_fr.dart index 30bfa85b63..9955f385e1 100644 --- a/lib/l10n/l10n_fr.dart +++ b/lib/l10n/l10n_fr.dart @@ -103,9 +103,6 @@ class AppLocalizationsFr extends AppLocalizations { @override String get mobileCancelTakebackOffer => 'Annuler la proposition de reprise du coup'; - @override - String get mobileCancelDrawOffer => 'Annuler la proposition de nulle'; - @override String get mobileWaitingForOpponentToJoin => 'En attente d\'un adversaire...'; @@ -142,7 +139,7 @@ class AppLocalizationsFr extends AppLocalizations { String get mobileGreetingWithoutName => 'Bonjour'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobilePrefMagnifyDraggedPiece => 'Grossir la pièce déplacée'; @override String get activityActivity => 'Activité'; @@ -246,6 +243,17 @@ class AppLocalizationsFr extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count parties $param2 par correspondance terminées', + one: '$count partie $param2 par correspondance terminée', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,226 @@ class AppLocalizationsFr extends AppLocalizations { @override String get broadcastBroadcasts => 'Diffusions'; + @override + String get broadcastMyBroadcasts => 'Ma diffusion'; + @override String get broadcastLiveBroadcasts => 'Diffusions de tournois en direct'; + @override + String get broadcastBroadcastCalendar => 'Calendrier des diffusions'; + + @override + String get broadcastNewBroadcast => 'Nouvelle diffusion en direct'; + + @override + String get broadcastSubscribedBroadcasts => 'Diffusions suivies'; + + @override + String get broadcastAboutBroadcasts => 'À propos des diffusions'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Comment utiliser les diffusions dans Lichess.'; + + @override + String get broadcastTheNewRoundHelp => 'La nouvelle ronde aura les mêmes participants et contributeurs que la précédente.'; + + @override + String get broadcastAddRound => 'Ajouter une ronde'; + + @override + String get broadcastOngoing => 'En cours'; + + @override + String get broadcastUpcoming => 'À venir'; + + @override + String get broadcastCompleted => 'Terminé'; + + @override + String get broadcastCompletedHelp => 'Lichess détecte la fin des rondes en fonction des parties sources. Utilisez cette option s\'il n\'y a pas de source.'; + + @override + String get broadcastRoundName => 'Nom de la ronde'; + + @override + String get broadcastRoundNumber => 'Numéro de la ronde'; + + @override + String get broadcastTournamentName => 'Nom du tournoi'; + + @override + String get broadcastTournamentDescription => 'Brève description du tournoi'; + + @override + String get broadcastFullDescription => 'Description complète de l\'événement'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Description détaillée et optionnelle de la diffusion. $param1 est disponible. La longueur doit être inférieure à $param2 caractères.'; + } + + @override + String get broadcastSourceSingleUrl => 'URL source de la partie en PGN'; + + @override + String get broadcastSourceUrlHelp => 'URL que Lichess interrogera pour obtenir les mises à jour du PGN. Elle doit être accessible publiquement depuis Internet.'; + + @override + String get broadcastSourceGameIds => 'Jusqu\'à 64 ID de partie Lichess séparés par des espaces.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Date de début du tournoi (fuseau horaire local) : $param'; + } + + @override + String get broadcastStartDateHelp => 'Facultatif, si vous savez quand l\'événement commence'; + + @override + String get broadcastCurrentGameUrl => 'URL de la partie en cours'; + + @override + String get broadcastDownloadAllRounds => 'Télécharger toutes les rondes'; + + @override + String get broadcastResetRound => 'Réinitialiser cette ronde'; + + @override + String get broadcastDeleteRound => 'Supprimer cette ronde'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Supprimer définitivement la ronde et ses parties.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Supprimer toutes les parties de la ronde. La source doit être active pour recréer les parties.'; + + @override + String get broadcastEditRoundStudy => 'Modifier l\'étude de la ronde'; + + @override + String get broadcastDeleteTournament => 'Supprimer ce tournoi'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Supprimer définitivement le tournoi, toutes ses rondes et toutes ses parties.'; + + @override + String get broadcastShowScores => 'Afficher les résultats des joueurs en fonction des résultats des parties'; + + @override + String get broadcastReplacePlayerTags => 'Facultatif : remplacer les noms des joueurs, les classements et les titres'; + + @override + String get broadcastFideFederations => 'Fédérations FIDE'; + + @override + String get broadcastTop10Rating => '10 plus hauts classements'; + + @override + String get broadcastFidePlayers => 'Joueurs FIDE'; + + @override + String get broadcastFidePlayerNotFound => 'Joueur FIDE introuvable'; + + @override + String get broadcastFideProfile => 'Profil FIDE'; + + @override + String get broadcastFederation => 'Fédération'; + + @override + String get broadcastAgeThisYear => 'Âge cette année'; + + @override + String get broadcastUnrated => 'Non classé'; + + @override + String get broadcastRecentTournaments => 'Tournois récents'; + + @override + String get broadcastOpenLichess => 'Ouvrir dans Lichess'; + + @override + String get broadcastTeams => 'Équipes'; + + @override + String get broadcastBoards => 'Échiquiers'; + + @override + String get broadcastOverview => 'Survol'; + + @override + String get broadcastSubscribeTitle => 'Abonnez-vous pour être averti du début de chaque ronde. Vous pouvez basculer entre une sonnerie ou une notification poussée pour les diffusions dans les préférences de votre compte.'; + + @override + String get broadcastUploadImage => 'Téléverser une image pour le tournoi'; + + @override + String get broadcastNoBoardsYet => 'Pas d\'échiquiers pour le moment. Ils s\'afficheront lorsque les parties seront téléversées.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Les échiquiers sont chargés à partir d\'une source ou de l\'$param.'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Commence après $param'; + } + + @override + String get broadcastStartVerySoon => 'La diffusion commencera très bientôt.'; + + @override + String get broadcastNotYetStarted => 'La diffusion n\'a pas encore commencé.'; + + @override + String get broadcastOfficialWebsite => 'Site Web officiel'; + + @override + String get broadcastStandings => 'Classement'; + + @override + String broadcastIframeHelp(String param) { + return 'Plus d\'options sur la $param'; + } + + @override + String get broadcastWebmastersPage => 'page des webmestres'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'Source PGN publique en temps réel pour cette ronde. Nous offrons également un $param pour permettre une synchronisation rapide et efficace.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Intégrer cette diffusion dans votre site Web'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Intégrer la $param dans votre site Web'; + } + + @override + String get broadcastRatingDiff => 'Différence de cote'; + + @override + String get broadcastGamesThisTournament => 'Partie de ce tournoi'; + + @override + String get broadcastScore => 'Résultat'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count diffusions', + one: '$count diffusion', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Défis : $param1'; @@ -1390,10 +1615,10 @@ class AppLocalizationsFr extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'L\'adversaire est limité dans les mouvements qu\'il peut effectuer, et tous les coups aggravent sa position.'; @override - String get puzzleThemeHealthyMix => 'Divers'; + String get puzzleThemeMix => 'Problèmes variés'; @override - String get puzzleThemeHealthyMixDescription => 'Un peu de tout. Vous ne savez pas à quoi vous attendre ! Comme dans une vraie partie.'; + String get puzzleThemeMixDescription => 'Un peu de tout. Vous ne savez pas à quoi vous attendre! Comme dans une vraie partie.'; @override String get puzzleThemePlayerGames => 'Parties de joueurs'; @@ -1797,9 +2022,6 @@ class AppLocalizationsFr extends AppLocalizations { @override String get removesTheDepthLimit => 'Désactive la profondeur limitée et fait chauffer votre ordinateur'; - @override - String get engineManager => 'Gestionnaire de moteur d\'analyse'; - @override String get blunder => 'Gaffe'; @@ -2063,6 +2285,9 @@ class AppLocalizationsFr extends AppLocalizations { @override String get gamesPlayed => 'Parties jouées'; + @override + String get ok => 'OK'; + @override String get cancel => 'Annuler'; @@ -2772,7 +2997,13 @@ class AppLocalizationsFr extends AppLocalizations { String get other => 'Autre'; @override - String get reportDescriptionHelp => 'Copiez le(s) lien(s) vers les parties et expliquez en quoi le comportement de cet utilisateur est inapproprié. Ne dites pas juste \"il triche\", mais expliquez comment vous êtes arrivé à cette conclusion. Votre rapport sera traité plus vite s\'il est écrit en anglais.'; + String get reportCheatBoostHelp => 'Collez le lien vers la ou les parties et expliquez pourquoi le comportement de l\'utilisateur est inapproprié. Ne dites pas juste « il triche »; expliquez comment vous êtes arrivé à cette conclusion.'; + + @override + String get reportUsernameHelp => 'Expliquez pourquoi ce nom d\'utilisateur est offensant. Ne dites pas simplement qu\'il est choquant ou inapproprié; expliquez comment vous êtes arrivé à cette conclusion, surtout si l\'insulte n\'est pas claire, n\'est pas en anglais, est en argot ou a une connotation historique ou culturelle.'; + + @override + String get reportProcessedFasterInEnglish => 'Votre rapport sera traité plus rapidement s\'il est rédigé en anglais.'; @override String get error_provideOneCheatedGameLink => 'Merci de fournir au moins un lien vers une partie où il y a eu triche.'; @@ -4077,6 +4308,9 @@ class AppLocalizationsFr extends AppLocalizations { @override String get nothingToSeeHere => 'Rien à voir ici pour le moment.'; + @override + String get stats => 'Statistiques'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4723,9 +4957,514 @@ class AppLocalizationsFr extends AppLocalizations { @override String get streamerLichessStreamers => 'Streamers sur Lichess'; + @override + String get studyPrivate => 'Étude(s) privée(s)'; + + @override + String get studyMyStudies => 'Mes études'; + + @override + String get studyStudiesIContributeTo => 'Études auxquelles je participe'; + + @override + String get studyMyPublicStudies => 'Mes études publiques'; + + @override + String get studyMyPrivateStudies => 'Mes études privées'; + + @override + String get studyMyFavoriteStudies => 'Mes études favorites'; + + @override + String get studyWhatAreStudies => 'Qu\'est-ce qu\'une étude ?'; + + @override + String get studyAllStudies => 'Toutes les études'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Études créées par $param'; + } + + @override + String get studyNoneYet => 'Aucune étude.'; + + @override + String get studyHot => 'Populaire(s)'; + + @override + String get studyDateAddedNewest => 'Date d\'ajout (dernier ajout)'; + + @override + String get studyDateAddedOldest => 'Date d\'ajout (premier ajout)'; + + @override + String get studyRecentlyUpdated => 'Récemment mis à jour'; + + @override + String get studyMostPopular => 'Études les plus populaires'; + + @override + String get studyAlphabetical => 'Alphabétique'; + + @override + String get studyAddNewChapter => 'Ajouter un nouveau chapitre'; + + @override + String get studyAddMembers => 'Ajouter des membres'; + + @override + String get studyInviteToTheStudy => 'Inviter à l\'étude'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Veuillez n\'inviter que des personnes qui vous connaissent et qui souhaitent activement participer à cette étude.'; + + @override + String get studySearchByUsername => 'Rechercher par nom d\'utilisateur'; + + @override + String get studySpectator => 'Spectateur'; + + @override + String get studyContributor => 'Contributeur'; + + @override + String get studyKick => 'Éjecter'; + + @override + String get studyLeaveTheStudy => 'Quitter l\'étude'; + + @override + String get studyYouAreNowAContributor => 'Vous êtes maintenant un contributeur'; + + @override + String get studyYouAreNowASpectator => 'Vous êtes maintenant un spectateur'; + + @override + String get studyPgnTags => 'Étiquettes PGN'; + + @override + String get studyLike => 'Aimer'; + + @override + String get studyUnlike => 'Je n’aime pas'; + + @override + String get studyNewTag => 'Nouvelle étiquette'; + + @override + String get studyCommentThisPosition => 'Commenter la position'; + + @override + String get studyCommentThisMove => 'Commenter ce coup'; + + @override + String get studyAnnotateWithGlyphs => 'Annoter avec des symboles'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Le chapitre est trop court pour être analysé.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Seuls les contributeurs de l\'étude peuvent demander une analyse informatique.'; + + @override + String get studyGetAFullComputerAnalysis => 'Obtenez une analyse en ligne complète de la ligne principale.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Assurez-vous que le chapitre est terminé. Vous ne pouvez demander l\'analyse qu\'une seule fois.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Tous les membres SYNC demeurent sur la même position'; + + @override + String get studyShareChanges => 'Partager les changements avec les spectateurs et les enregistrer sur le serveur'; + + @override + String get studyPlaying => 'En cours'; + + @override + String get studyShowEvalBar => 'Barre d’évaluation'; + + @override + String get studyFirst => 'Premier'; + + @override + String get studyPrevious => 'Précédent'; + + @override + String get studyNext => 'Suivant'; + + @override + String get studyLast => 'Dernier'; + @override String get studyShareAndExport => 'Partager & exporter'; + @override + String get studyCloneStudy => 'Dupliquer'; + + @override + String get studyStudyPgn => 'PGN de l\'étude'; + + @override + String get studyDownloadAllGames => 'Télécharger toutes les parties'; + + @override + String get studyChapterPgn => 'PGN du chapitre'; + + @override + String get studyCopyChapterPgn => 'Copier le fichier PGN'; + + @override + String get studyDownloadGame => 'Télécharger la partie'; + + @override + String get studyStudyUrl => 'URL de l\'étude'; + + @override + String get studyCurrentChapterUrl => 'URL du chapitre actuel'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Vous pouvez collez ce lien dans le forum afin de l’insérer'; + + @override + String get studyStartAtInitialPosition => 'Commencer à partir du début'; + + @override + String studyStartAtX(String param) { + return 'Débuter à $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Intégrer dans votre site ou blog'; + + @override + String get studyReadMoreAboutEmbedding => 'En savoir plus sur l\'intégration'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Seules les études publiques peuvent être intégrées !'; + + @override + String get studyOpen => 'Ouvrir'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1 vous est apporté par $param2'; + } + + @override + String get studyStudyNotFound => 'Étude introuvable'; + + @override + String get studyEditChapter => 'Modifier le chapitre'; + + @override + String get studyNewChapter => 'Nouveau chapitre'; + + @override + String studyImportFromChapterX(String param) { + return 'Importer depuis $param'; + } + + @override + String get studyOrientation => 'Orientation'; + + @override + String get studyAnalysisMode => 'Mode analyse'; + + @override + String get studyPinnedChapterComment => 'Commentaire du chapitre épinglé'; + + @override + String get studySaveChapter => 'Enregistrer le chapitre'; + + @override + String get studyClearAnnotations => 'Effacer les annotations'; + + @override + String get studyClearVariations => 'Supprimer les variantes'; + + @override + String get studyDeleteChapter => 'Supprimer le chapitre'; + + @override + String get studyDeleteThisChapter => 'Supprimer ce chapitre ? Cette action est irréversible !'; + + @override + String get studyClearAllCommentsInThisChapter => 'Effacer tous les commentaires et annotations dans ce chapitre ?'; + + @override + String get studyRightUnderTheBoard => 'Juste sous l\'échiquier'; + + @override + String get studyNoPinnedComment => 'Aucun'; + + @override + String get studyNormalAnalysis => 'Analyse normale'; + + @override + String get studyHideNextMoves => 'Cacher les coups suivants'; + + @override + String get studyInteractiveLesson => 'Leçon interactive'; + + @override + String studyChapterX(String param) { + return 'Chapitre : $param'; + } + + @override + String get studyEmpty => 'Par défaut'; + + @override + String get studyStartFromInitialPosition => 'Commencer à partir du début'; + + @override + String get studyEditor => 'Editeur'; + + @override + String get studyStartFromCustomPosition => 'Commencer à partir d\'une position personnalisée'; + + @override + String get studyLoadAGameByUrl => 'Charger des parties à partir d\'une URL'; + + @override + String get studyLoadAPositionFromFen => 'Charger une position par FEN'; + + @override + String get studyLoadAGameFromPgn => 'Charger des parties par PGN'; + + @override + String get studyAutomatic => 'Automatique'; + + @override + String get studyUrlOfTheGame => 'URL des parties, une par ligne'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Charger des parties de $param1 ou $param2'; + } + + @override + String get studyCreateChapter => 'Créer un chapitre'; + + @override + String get studyCreateStudy => 'Créer une étude'; + + @override + String get studyEditStudy => 'Modifier l\'étude'; + + @override + String get studyVisibility => 'Visibilité'; + + @override + String get studyPublic => 'Publique'; + + @override + String get studyUnlisted => 'Non répertorié'; + + @override + String get studyInviteOnly => 'Sur invitation seulement'; + + @override + String get studyAllowCloning => 'Autoriser la duplication'; + + @override + String get studyNobody => 'Personne'; + + @override + String get studyOnlyMe => 'Seulement moi'; + + @override + String get studyContributors => 'Contributeurs'; + + @override + String get studyMembers => 'Membres'; + + @override + String get studyEveryone => 'Tout le monde'; + + @override + String get studyEnableSync => 'Activer la synchronisation'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Oui : garder tout le monde sur la même position'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Non : laisser les gens naviguer librement'; + + @override + String get studyPinnedStudyComment => 'Commentaire d\'étude épinglé'; + @override String get studyStart => 'Commencer'; + + @override + String get studySave => 'Enregistrer'; + + @override + String get studyClearChat => 'Effacer le tchat'; + + @override + String get studyDeleteTheStudyChatHistory => 'Supprimer l\'historique du tchat de l\'étude ? Cette action est irréversible !'; + + @override + String get studyDeleteStudy => 'Supprimer l\'étude'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Supprimer toute l’étude? Aucun retour en arrière possible! Taper le nom de l’étude pour confirmer : $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Où voulez-vous étudier cela ?'; + + @override + String get studyGoodMove => 'Bon coup'; + + @override + String get studyMistake => 'Erreur'; + + @override + String get studyBrilliantMove => 'Excellent coup'; + + @override + String get studyBlunder => 'Gaffe'; + + @override + String get studyInterestingMove => 'Coup intéressant'; + + @override + String get studyDubiousMove => 'Coup douteux'; + + @override + String get studyOnlyMove => 'Seul coup'; + + @override + String get studyZugzwang => 'Zugzwang'; + + @override + String get studyEqualPosition => 'Position égale'; + + @override + String get studyUnclearPosition => 'Position incertaine'; + + @override + String get studyWhiteIsSlightlyBetter => 'Les Blancs sont un peu mieux'; + + @override + String get studyBlackIsSlightlyBetter => 'Les Noirs sont un peu mieux'; + + @override + String get studyWhiteIsBetter => 'Les Blancs sont mieux'; + + @override + String get studyBlackIsBetter => 'Les Noirs sont mieux'; + + @override + String get studyWhiteIsWinning => 'Les Blancs gagnent'; + + @override + String get studyBlackIsWinning => 'Les Noirs gagnent'; + + @override + String get studyNovelty => 'Nouveauté'; + + @override + String get studyDevelopment => 'Développement'; + + @override + String get studyInitiative => 'Initiative'; + + @override + String get studyAttack => 'Attaque'; + + @override + String get studyCounterplay => 'Contre-jeu'; + + @override + String get studyTimeTrouble => 'Pression de temps'; + + @override + String get studyWithCompensation => 'Avec compensation'; + + @override + String get studyWithTheIdea => 'Avec l\'idée'; + + @override + String get studyNextChapter => 'Chapitre suivant'; + + @override + String get studyPrevChapter => 'Chapitre précédent'; + + @override + String get studyStudyActions => 'Options pour les études'; + + @override + String get studyTopics => 'Thèmes'; + + @override + String get studyMyTopics => 'Mes thèmes'; + + @override + String get studyPopularTopics => 'Thèmes populaires'; + + @override + String get studyManageTopics => 'Gérer les thèmes'; + + @override + String get studyBack => 'Retour'; + + @override + String get studyPlayAgain => 'Jouer à nouveau'; + + @override + String get studyWhatWouldYouPlay => 'Que joueriez-vous dans cette position ?'; + + @override + String get studyYouCompletedThisLesson => 'Félicitations ! Vous avez terminé ce cours.'; + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count chapitres', + one: '$count chapitre', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count parties', + one: '$count partie', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count membres', + one: '$count membre', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Collez votre texte PGN ici, jusqu\'à $count parties', + one: 'Collez votre texte PGN ici, jusqu\'à $count partie', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_ga.dart b/lib/l10n/l10n_ga.dart index 4aece38474..9bb52bbb8e 100644 --- a/lib/l10n/l10n_ga.dart +++ b/lib/l10n/l10n_ga.dart @@ -103,9 +103,6 @@ class AppLocalizationsGa extends AppLocalizations { @override String get mobileCancelTakebackOffer => 'Cancel takeback offer'; - @override - String get mobileCancelDrawOffer => 'Cancel draw offer'; - @override String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @@ -270,6 +267,17 @@ class AppLocalizationsGa extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Completed $count $param2 correspondence games', + one: 'Completed $count $param2 correspondence game', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -399,9 +407,226 @@ class AppLocalizationsGa extends AppLocalizations { @override String get broadcastBroadcasts => 'Craoltaí'; + @override + String get broadcastMyBroadcasts => 'My broadcasts'; + @override String get broadcastLiveBroadcasts => 'Craoltaí beo comórtais'; + @override + String get broadcastBroadcastCalendar => 'Broadcast calendar'; + + @override + String get broadcastNewBroadcast => 'Craoladh beo nua'; + + @override + String get broadcastSubscribedBroadcasts => 'Subscribed broadcasts'; + + @override + String get broadcastAboutBroadcasts => 'About broadcasts'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'How to use Lichess Broadcasts.'; + + @override + String get broadcastTheNewRoundHelp => 'The new round will have the same members and contributors as the previous one.'; + + @override + String get broadcastAddRound => 'Cuir babhta leis'; + + @override + String get broadcastOngoing => 'Leanúnach'; + + @override + String get broadcastUpcoming => 'Le teacht'; + + @override + String get broadcastCompleted => 'Críochnaithe'; + + @override + String get broadcastCompletedHelp => 'Lichess detects round completion, but can get it wrong. Use this to set it manually.'; + + @override + String get broadcastRoundName => 'Ainm babhta'; + + @override + String get broadcastRoundNumber => 'Uimhir bhabhta'; + + @override + String get broadcastTournamentName => 'Ainm comórtas'; + + @override + String get broadcastTournamentDescription => 'Cur síos gairid ar an gcomórtas'; + + @override + String get broadcastFullDescription => 'Cur síos iomlán ar an ócáid'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Cur síos fada roghnach ar an craoladh. Tá $param1 ar fáil. Caithfidh an fad a bheith níos lú ná $param2 carachtar.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN Source URL'; + + @override + String get broadcastSourceUrlHelp => 'URL a seiceálfaidh Lichess chun PGN nuashonruithe a fháil. Caithfidh sé a bheith le féiceáil go poiblí ón Idirlíon.'; + + @override + String get broadcastSourceGameIds => 'Up to 64 Lichess game IDs, separated by spaces.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Start date in the tournament local timezone: $param'; + } + + @override + String get broadcastStartDateHelp => 'Roghnach, má tá a fhios agat cathain a thosóidh an ócáid'; + + @override + String get broadcastCurrentGameUrl => 'URL cluiche reatha'; + + @override + String get broadcastDownloadAllRounds => 'Íoslódáil gach babhta'; + + @override + String get broadcastResetRound => 'Athshocraigh an babhta seo'; + + @override + String get broadcastDeleteRound => 'Scrios an babhta seo'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Scrios go cinntitheach an babhta agus a chuid cluichí.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Scrios gach cluiche den bhabhta seo. Caithfidh an fhoinse a bheith gníomhach chun iad a athchruthú.'; + + @override + String get broadcastEditRoundStudy => 'Edit round study'; + + @override + String get broadcastDeleteTournament => 'Delete this tournament'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Definitively delete the entire tournament, all its rounds and all its games.'; + + @override + String get broadcastShowScores => 'Show players scores based on game results'; + + @override + String get broadcastReplacePlayerTags => 'Optional: replace player names, ratings and titles'; + + @override + String get broadcastFideFederations => 'FIDE federations'; + + @override + String get broadcastTop10Rating => 'Top 10 rating'; + + @override + String get broadcastFidePlayers => 'FIDE players'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE player not found'; + + @override + String get broadcastFideProfile => 'FIDE profile'; + + @override + String get broadcastFederation => 'Federation'; + + @override + String get broadcastAgeThisYear => 'Age this year'; + + @override + String get broadcastUnrated => 'Unrated'; + + @override + String get broadcastRecentTournaments => 'Recent tournaments'; + + @override + String get broadcastOpenLichess => 'Open in Lichess'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Overview'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Official website'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'Score'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count broadcasts', + one: '$count broadcast', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Challenges: $param1'; @@ -1456,10 +1681,10 @@ class AppLocalizationsGa extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Ciallaíonn Zugzwang gur gá le himreoir a s(h) eans a \nthógáil cé nár mhaith leis nó léi toisc gur laige a bheith a s(h) uíomh cibé beart a dhéanfaidh sé/sí. Ba mhaith leis / léi \"háram\" a rá ach níl sé sin ceadaithe.'; @override - String get puzzleThemeHealthyMix => 'Meascán sláintiúil'; + String get puzzleThemeMix => 'Meascán sláintiúil'; @override - String get puzzleThemeHealthyMixDescription => 'Giota de gach rud. Níl a fhios agat cad tá os do comhair, mar sin fanann tú réidh le haghaidh athan bith! Díreach mar atá i gcluichí fíor.'; + String get puzzleThemeMixDescription => 'Giota de gach rud. Níl a fhios agat cad tá os do comhair, mar sin fanann tú réidh le haghaidh athan bith! Díreach mar atá i gcluichí fíor.'; @override String get puzzleThemePlayerGames => 'Cluichí imreoir'; @@ -1863,9 +2088,6 @@ class AppLocalizationsGa extends AppLocalizations { @override String get removesTheDepthLimit => 'Faigheann sé réidh leis an teorainn doimhneachta, agus coinníonn sé do ríomhaire te'; - @override - String get engineManager => 'Engine manager'; - @override String get blunder => 'Meancóg'; @@ -2129,6 +2351,9 @@ class AppLocalizationsGa extends AppLocalizations { @override String get gamesPlayed => 'Cluichí imeartha'; + @override + String get ok => 'OK'; + @override String get cancel => 'Cealaigh'; @@ -2838,7 +3063,13 @@ class AppLocalizationsGa extends AppLocalizations { String get other => 'Eile'; @override - String get reportDescriptionHelp => 'Greamaigh an nasc chuig an gcluiche/na cluichí agus mínigh cad atá cearr le hiompar an úsáideora. Ná habair go díreach go mbíonn \"caimiléireacht\" ar bun acu, ach inis dúinn faoin dóigh a fuair tú amach faoi. Faraor, déanfar do thuairisc a phróiseáil níos tapúla más i mBéarla atá sé.'; + String get reportCheatBoostHelp => 'Paste the link to the game(s) and explain what is wrong about this user\'s behaviour. Don\'t just say \"they cheat\", but tell us how you came to this conclusion.'; + + @override + String get reportUsernameHelp => 'Explain what about this username is offensive. Don\'t just say \"it\'s offensive/inappropriate\", but tell us how you came to this conclusion, especially if the insult is obfuscated, not in english, is in slang, or is a historical/cultural reference.'; + + @override + String get reportProcessedFasterInEnglish => 'Your report will be processed faster if written in English.'; @override String get error_provideOneCheatedGameLink => 'Cuir nasc ar fáil chuig cluiche amháin ar a laghad ar tharla caimiléireacht ann le do thoil.'; @@ -4143,6 +4374,9 @@ class AppLocalizationsGa extends AppLocalizations { @override String get nothingToSeeHere => 'Nothing to see here at the moment.'; + @override + String get stats => 'Stats'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4915,9 +5149,526 @@ class AppLocalizationsGa extends AppLocalizations { @override String get streamerLichessStreamers => 'Sruthaithe Lichess'; + @override + String get studyPrivate => 'Cé na daoine! Tá an leathanach seo príobháideach, ní féidir leat é a rochtain'; + + @override + String get studyMyStudies => 'Mo chuid staidéir'; + + @override + String get studyStudiesIContributeTo => 'Staidéir atá á n-iarraidh agam'; + + @override + String get studyMyPublicStudies => 'Mo chuid staidéir phoiblí'; + + @override + String get studyMyPrivateStudies => 'Mo chuid staidéir phríobháideacha'; + + @override + String get studyMyFavoriteStudies => 'Na staidéir is fearr liom'; + + @override + String get studyWhatAreStudies => 'Cad is staidéir ann?'; + + @override + String get studyAllStudies => 'Gach staidéar'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Staidéir a chruthaigh $param'; + } + + @override + String get studyNoneYet => 'Níl aon cheann fós.'; + + @override + String get studyHot => 'Te'; + + @override + String get studyDateAddedNewest => 'Dáta curtha leis (dáta is déanaí)'; + + @override + String get studyDateAddedOldest => 'Dáta curtha leis (dáta is sinne)'; + + @override + String get studyRecentlyUpdated => 'Faisnéis nuashonraithe le déanaí'; + + @override + String get studyMostPopular => 'Móréilimh'; + + @override + String get studyAlphabetical => 'Aibítre'; + + @override + String get studyAddNewChapter => 'Cuir caibidil nua leis'; + + @override + String get studyAddMembers => 'Cuir baill leis'; + + @override + String get studyInviteToTheStudy => 'Tabhair cuireadh don staidéar'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Ná tabhair cuireadh ach do dhaoine a bhfuil aithne agat orthu, agus ar mian leo go gníomhach a bheith páirteach sa staidéar seo.'; + + @override + String get studySearchByUsername => 'Cuardaigh de réir ainm úsáideora'; + + @override + String get studySpectator => 'Breathnóir'; + + @override + String get studyContributor => 'Rannpháirtí'; + + @override + String get studyKick => 'Ciceáil'; + + @override + String get studyLeaveTheStudy => 'Fág an staidéar'; + + @override + String get studyYouAreNowAContributor => 'Is ranníocóir anois tú'; + + @override + String get studyYouAreNowASpectator => 'Is lucht féachana anois tú'; + + @override + String get studyPgnTags => 'Clibeanna PGN'; + + @override + String get studyLike => 'Is maith liom'; + + @override + String get studyUnlike => 'Díthogh'; + + @override + String get studyNewTag => 'Clib nua'; + + @override + String get studyCommentThisPosition => 'Déan trácht ar an suíomh seo'; + + @override + String get studyCommentThisMove => 'Déan trácht ar an mbeart seo'; + + @override + String get studyAnnotateWithGlyphs => 'Nodaireacht le glifeanna'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Tá an chaibidil ró-ghearr le hanailís a dhéanamh uirthi.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Ní féidir ach le rannpháirtithe an staidéir anailís ríomhaire a iarraidh.'; + + @override + String get studyGetAFullComputerAnalysis => 'Faigh anailís ríomhaire iomlán ón freastalaí ar an bpríomhlíne.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Bí cinnte go bhfuil an chaibidil críochnaithe. Ní féidir leat iarr ar anailís ach uair amháin.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Fanann gach ball SYNC sa suíomh céanna'; + + @override + String get studyShareChanges => 'Roinn athruithe le lucht féachana agus sábháil iad ar an freastalaí'; + + @override + String get studyPlaying => 'Ag imirt'; + + @override + String get studyShowEvalBar => 'Evaluation bars'; + + @override + String get studyFirst => 'Céad'; + + @override + String get studyPrevious => 'Roimhe'; + + @override + String get studyNext => 'Ar aghaidh'; + + @override + String get studyLast => 'Deiridh'; + @override String get studyShareAndExport => 'Comhroinn & easpórtáil'; + @override + String get studyCloneStudy => 'Déan cóip'; + + @override + String get studyStudyPgn => 'Déan staidéar ar PGN'; + + @override + String get studyDownloadAllGames => 'Íoslódáil gach cluiche'; + + @override + String get studyChapterPgn => 'PGN caibidle'; + + @override + String get studyCopyChapterPgn => 'Cóipeáil PGN'; + + @override + String get studyDownloadGame => 'Íoslódáil cluiche'; + + @override + String get studyStudyUrl => 'URL an staidéir'; + + @override + String get studyCurrentChapterUrl => 'URL caibidil reatha'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Is féidir é seo a ghreamú san fhóram chun leabú'; + + @override + String get studyStartAtInitialPosition => 'Tosaigh ag an suíomh tosaigh'; + + @override + String studyStartAtX(String param) { + return 'Tosú ag $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Leabaithe i do shuíomh Gréasáin nó i do bhlag'; + + @override + String get studyReadMoreAboutEmbedding => 'Léigh tuilleadh faoi leabú'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Ní féidir ach staidéir phoiblí a leabú!'; + + @override + String get studyOpen => 'Oscailte'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1, a thugann $param2 chugat'; + } + + @override + String get studyStudyNotFound => 'Níor aimsíodh staidéar'; + + @override + String get studyEditChapter => 'Cuir caibidil in eagar'; + + @override + String get studyNewChapter => 'Caibidil nua'; + + @override + String studyImportFromChapterX(String param) { + return 'Iompórtáil ó $param'; + } + + @override + String get studyOrientation => 'Treoshuíomh'; + + @override + String get studyAnalysisMode => 'Modh anailíse'; + + @override + String get studyPinnedChapterComment => 'Trácht caibidil greamaithe'; + + @override + String get studySaveChapter => 'Sábháil caibidil'; + + @override + String get studyClearAnnotations => 'Glan anótála'; + + @override + String get studyClearVariations => 'Glan éagsúlachtaí'; + + @override + String get studyDeleteChapter => 'Scrios caibidil'; + + @override + String get studyDeleteThisChapter => 'Scrios an chaibidil seo? Níl aon dul ar ais!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Glan gach trácht, glif agus cruthanna tarraingthe sa chaibidil seo?'; + + @override + String get studyRightUnderTheBoard => 'Díreach faoin gclár'; + + @override + String get studyNoPinnedComment => 'Faic'; + + @override + String get studyNormalAnalysis => 'Gnáth-anailís'; + + @override + String get studyHideNextMoves => 'Folaigh na bearta ina dhiaidh seo'; + + @override + String get studyInteractiveLesson => 'Ceacht idirghníomhach'; + + @override + String studyChapterX(String param) { + return 'Caibidil $param'; + } + + @override + String get studyEmpty => 'Folamh'; + + @override + String get studyStartFromInitialPosition => 'Tosaigh ón suíomh tosaigh'; + + @override + String get studyEditor => 'Eagarthóir'; + + @override + String get studyStartFromCustomPosition => 'Tosaigh ón suíomh saincheaptha'; + + @override + String get studyLoadAGameByUrl => 'Lód cluichí le URLanna'; + + @override + String get studyLoadAPositionFromFen => 'Luchtaigh suíomh ó FEN'; + + @override + String get studyLoadAGameFromPgn => 'Lódáil cluichí ó PGN'; + + @override + String get studyAutomatic => 'Uathoibríoch'; + + @override + String get studyUrlOfTheGame => 'URL na gcluichí, ceann amháin an líne'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Lódáil cluichí ó $param1 nó $param2'; + } + + @override + String get studyCreateChapter => 'Cruthaigh caibidil'; + + @override + String get studyCreateStudy => 'Cruthaigh staidéar'; + + @override + String get studyEditStudy => 'Cuir staidéar in eagar'; + + @override + String get studyVisibility => 'Infheictheacht'; + + @override + String get studyPublic => 'Poiblí'; + + @override + String get studyUnlisted => 'Neamhliostaithe'; + + @override + String get studyInviteOnly => 'Tabhair cuireadh amháin'; + + @override + String get studyAllowCloning => 'Lig clónáil'; + + @override + String get studyNobody => 'Níl einne'; + + @override + String get studyOnlyMe => 'Mise amháin'; + + @override + String get studyContributors => 'Rannpháirtithe'; + + @override + String get studyMembers => 'Baill'; + + @override + String get studyEveryone => 'Gach duine'; + + @override + String get studyEnableSync => 'Cuir sinc ar chumas'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Cinnte: coinnigh gach duine ar an suíomh céanna'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Na déan: lig do dhaoine brabhsáil go saor'; + + @override + String get studyPinnedStudyComment => 'Trácht staidéir greamaithe'; + @override String get studyStart => 'Tosú'; + + @override + String get studySave => 'Sábháil'; + + @override + String get studyClearChat => 'Glan comhrá'; + + @override + String get studyDeleteTheStudyChatHistory => 'Scrios an stair comhrá staidéir? Níl aon dul ar ais!'; + + @override + String get studyDeleteStudy => 'Scrios an staidéar'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Scrios an staidéar iomlán? Níl aon dul ar ais! Clóscríobh ainm an staidéar le deimhniú: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Cá háit ar mhaith leat staidéar a dhéanamh air sin?'; + + @override + String get studyGoodMove => 'Beart maith'; + + @override + String get studyMistake => 'Botún'; + + @override + String get studyBrilliantMove => 'Beart iontach'; + + @override + String get studyBlunder => 'Botún'; + + @override + String get studyInterestingMove => 'Beart suimiúil'; + + @override + String get studyDubiousMove => 'Beart amhrasach'; + + @override + String get studyOnlyMove => 'Beart dleathach'; + + @override + String get studyZugzwang => 'Zugzwang'; + + @override + String get studyEqualPosition => 'Suíomh cothrom'; + + @override + String get studyUnclearPosition => 'Suíomh doiléir'; + + @override + String get studyWhiteIsSlightlyBetter => 'Tá bán píosa beag níos fearr'; + + @override + String get studyBlackIsSlightlyBetter => 'Tá dubh píosa beag níos fearr'; + + @override + String get studyWhiteIsBetter => 'Tá bán níos fearr'; + + @override + String get studyBlackIsBetter => 'Tá dubh níos fearr'; + + @override + String get studyWhiteIsWinning => 'Bán ag bua'; + + @override + String get studyBlackIsWinning => 'Dubh ag bua'; + + @override + String get studyNovelty => 'Nuaga'; + + @override + String get studyDevelopment => 'Forbairt'; + + @override + String get studyInitiative => 'Tionscnamh'; + + @override + String get studyAttack => 'Ionsaí'; + + @override + String get studyCounterplay => 'Frithimirt'; + + @override + String get studyTimeTrouble => 'Trioblóid ama'; + + @override + String get studyWithCompensation => 'Le cúiteamh'; + + @override + String get studyWithTheIdea => 'Le smaoineamh'; + + @override + String get studyNextChapter => 'Céad chaibidil eile'; + + @override + String get studyPrevChapter => 'Caibidil roimhe seo'; + + @override + String get studyStudyActions => 'Déan staidéar ar ghníomhartha'; + + @override + String get studyTopics => 'Topaicí'; + + @override + String get studyMyTopics => 'Mo thopaicí'; + + @override + String get studyPopularTopics => 'Topaicí choitianta'; + + @override + String get studyManageTopics => 'Bainistigh topaicí'; + + @override + String get studyBack => 'Siar'; + + @override + String get studyPlayAgain => 'Imir arís'; + + @override + String get studyWhatWouldYouPlay => 'Cad a dhéanfá sa suíomh seo?'; + + @override + String get studyYouCompletedThisLesson => 'Comhghairdeas! Chríochnaigh tú an ceacht seo.'; + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Caibidil', + many: '$count Caibidil', + few: '$count gCaibidil', + two: '$count Chaibidil', + one: '$count Caibidil', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Cluiche', + many: '$count Cluiche', + few: '$count gCluiche', + two: '$count Chluiche', + one: '$count Cluiche', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Comhalta', + many: '$count Comhalta', + few: '$count gComhalta', + two: '$count Chomhalta', + one: '$count Comhalta', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Greamaigh do théacs PGN anseo, suas le $count cluiche', + many: 'Greamaigh do théacs PGN anseo, suas le $count cluiche', + few: 'Greamaigh do théacs PGN anseo, suas le $count gcluiche', + two: 'Greamaigh do théacs PGN anseo, suas le $count chluiche', + one: 'Greamaigh do théacs PGN anseo, suas le $count cluiche', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_gl.dart b/lib/l10n/l10n_gl.dart index 9630689049..a35f0b7875 100644 --- a/lib/l10n/l10n_gl.dart +++ b/lib/l10n/l10n_gl.dart @@ -58,7 +58,7 @@ class AppLocalizationsGl extends AppLocalizations { @override String mobilePlayersMatchingSearchTerm(String param) { - return 'Xogadores con \"$param\"'; + return 'O nome de usuario contén \"$param\"'; } @override @@ -103,9 +103,6 @@ class AppLocalizationsGl extends AppLocalizations { @override String get mobileCancelTakebackOffer => 'Cancelar a proposta de cambio'; - @override - String get mobileCancelDrawOffer => 'Cancelar a oferta de táboas'; - @override String get mobileWaitingForOpponentToJoin => 'Agardando un rival...'; @@ -142,7 +139,7 @@ class AppLocalizationsGl extends AppLocalizations { String get mobileGreetingWithoutName => 'Ola'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobilePrefMagnifyDraggedPiece => 'Ampliar a peza arrastrada'; @override String get activityActivity => 'Actividade'; @@ -246,6 +243,17 @@ class AppLocalizationsGl extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Finalizou $count partidas $param2 por correspondencia', + one: 'Finalizou $count partida $param2 por correspondencia', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,226 @@ class AppLocalizationsGl extends AppLocalizations { @override String get broadcastBroadcasts => 'Emisións en directo'; + @override + String get broadcastMyBroadcasts => 'As miñas emisións'; + @override String get broadcastLiveBroadcasts => 'Emisións de torneos en directo'; + @override + String get broadcastBroadcastCalendar => 'Calendario de emisións'; + + @override + String get broadcastNewBroadcast => 'Nova emisión en directo'; + + @override + String get broadcastSubscribedBroadcasts => 'Emisións subscritas'; + + @override + String get broadcastAboutBroadcasts => 'Sobre as retransmisións'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Como usar as Retransmisións de Lichess.'; + + @override + String get broadcastTheNewRoundHelp => 'A nova rolda terá os mesmos membros e colaboradores cá rolda anterior.'; + + @override + String get broadcastAddRound => 'Engadir unha rolda'; + + @override + String get broadcastOngoing => 'En curso'; + + @override + String get broadcastUpcoming => 'Proximamente'; + + @override + String get broadcastCompleted => 'Completadas'; + + @override + String get broadcastCompletedHelp => 'Malia que Lichess detecta o final das roldas, pódese equivocar. Usa esta opción para facelo manualmente.'; + + @override + String get broadcastRoundName => 'Nome da rolda'; + + @override + String get broadcastRoundNumber => 'Número de rolda'; + + @override + String get broadcastTournamentName => 'Nome do torneo'; + + @override + String get broadcastTournamentDescription => 'Breve descrición do torneo'; + + @override + String get broadcastFullDescription => 'Descrición completa do torneo'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Descrición longa opcional do torneo. $param1 está dispoñíbel. A lonxitude debe ser menor de $param2 caracteres.'; + } + + @override + String get broadcastSourceSingleUrl => 'URL de orixe do arquivo PGN'; + + @override + String get broadcastSourceUrlHelp => 'Ligazón que Lichess comprobará para obter actualizacións dos PGN. Debe ser publicamente accesíbel desde a Internet.'; + + @override + String get broadcastSourceGameIds => 'Até 64 identificadores de partidas de Lichess, separados por espazos.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Data de inicio do torneo (na zona horaria local): $param'; + } + + @override + String get broadcastStartDateHelp => 'Opcional, se sabes cando comeza o evento'; + + @override + String get broadcastCurrentGameUrl => 'Ligazón da partida actual'; + + @override + String get broadcastDownloadAllRounds => 'Descargar todas as roldas'; + + @override + String get broadcastResetRound => 'Restablecer esta rolda'; + + @override + String get broadcastDeleteRound => 'Borrar esta rolda'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Eliminar definitivamente a rolda e todas as súas partidas.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Eliminar todas as partidas desta rolda. A transmisión en orixe terá que estar activa para volver crealas.'; + + @override + String get broadcastEditRoundStudy => 'Editar o estudo da rolda'; + + @override + String get broadcastDeleteTournament => 'Eliminar este torneo'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Eliminar o torneo de forma definitiva, con todas as súas roldas e partidas.'; + + @override + String get broadcastShowScores => 'Amosar os puntos dos xogadores en función dos resultados das partidas'; + + @override + String get broadcastReplacePlayerTags => 'Opcional: substituír os nomes dos xogadores, as puntuacións e os títulos'; + + @override + String get broadcastFideFederations => 'Federacións FIDE'; + + @override + String get broadcastTop10Rating => 'Media do top 10'; + + @override + String get broadcastFidePlayers => 'Xogadores FIDE'; + + @override + String get broadcastFidePlayerNotFound => 'Xogador FIDE non atopado'; + + @override + String get broadcastFideProfile => 'Perfil FIDE'; + + @override + String get broadcastFederation => 'Federación'; + + @override + String get broadcastAgeThisYear => 'Idade actual'; + + @override + String get broadcastUnrated => 'Sen puntuar'; + + @override + String get broadcastRecentTournaments => 'Torneos recentes'; + + @override + String get broadcastOpenLichess => 'Abrir en Lichess'; + + @override + String get broadcastTeams => 'Equipos'; + + @override + String get broadcastBoards => 'Taboleiros'; + + @override + String get broadcastOverview => 'Visión de conxunto'; + + @override + String get broadcastSubscribeTitle => 'Subscríbete para ser notificado ó comezo de cada rolda. Podes activar/desactivar o son das notificacións ou as notificacións emerxentes para as emisións en directo nas preferencias da túa conta.'; + + @override + String get broadcastUploadImage => 'Subir a imaxe do torneo'; + + @override + String get broadcastNoBoardsYet => 'Aínda non hai taboleiros. Aparecerán cando se suban as partidas.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Os taboleiros pódense cargar dende a fonte ou a través da $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Comeza en $param'; + } + + @override + String get broadcastStartVerySoon => 'A emisión comeza decontado.'; + + @override + String get broadcastNotYetStarted => 'A emisión aínda non comezou.'; + + @override + String get broadcastOfficialWebsite => 'Páxina web oficial'; + + @override + String get broadcastStandings => 'Clasificación'; + + @override + String broadcastIframeHelp(String param) { + return 'Máis opcións na $param'; + } + + @override + String get broadcastWebmastersPage => 'páxina do administrador web'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'Unha fonte dos PGN pública e en tempo real para esta rolda. Tamén ofrecemos unha $param para unha sincronización máis rápida e eficiente.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Incrustar esta emisión na túa páxina web'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Incrustar a $param na túa páxina web'; + } + + @override + String get broadcastRatingDiff => 'Diferenza de puntuación'; + + @override + String get broadcastGamesThisTournament => 'Partidas neste torneo'; + + @override + String get broadcastScore => 'Resultado'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count emisións', + one: '$count emisión', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Desafíos: $param1'; @@ -598,7 +823,7 @@ class AppLocalizationsGl extends AppLocalizations { String get preferencesShowFlairs => 'Amosar as habelencias dos xogadores'; @override - String get preferencesExplainShowPlayerRatings => 'Isto permite ocultar todas as puntuacións do sitio, para axudar a centrarse no xadrez. As partidas poden ser puntuadas, isto só afecta ó que podes ver.'; + String get preferencesExplainShowPlayerRatings => 'Oculta todas as puntuacións de Lichess para axudar a centrarse no xadrez. As partidas poden ser puntuadas, isto só afecta ó que podes ver.'; @override String get preferencesDisplayBoardResizeHandle => 'Mostrar o control de redimensionamento do taboleiro'; @@ -628,7 +853,7 @@ class AppLocalizationsGl extends AppLocalizations { String get preferencesGiveMoreTime => 'Dar máis tempo'; @override - String get preferencesGameBehavior => 'Comportamento do xogo'; + String get preferencesGameBehavior => 'Comportamento durante a partida'; @override String get preferencesHowDoYouMovePieces => 'Como moves as pezas?'; @@ -688,7 +913,7 @@ class AppLocalizationsGl extends AppLocalizations { String get preferencesCastleByMovingTwoSquares => 'Movendo o rei dúas casas'; @override - String get preferencesCastleByMovingOntoTheRook => 'Movendo rei ata a torre'; + String get preferencesCastleByMovingOntoTheRook => 'Movendo o rei cara a torre'; @override String get preferencesInputMovesWithTheKeyboard => 'Introdución de movementos co teclado'; @@ -919,7 +1144,7 @@ class AppLocalizationsGl extends AppLocalizations { String get puzzleSolved => 'resoltos'; @override - String get puzzleFailed => 'fallado'; + String get puzzleFailed => 'incorrecto'; @override String get puzzleStreakDescription => 'Soluciona exercicios cada vez máis difíciles e consigue unha secuencia de vitorias. Non hai conta atrás, así que podes ir amodo. Se te equivocas nun só movemento, acabouse! Pero lembra que podes omitir unha xogada por sesión.'; @@ -1150,7 +1375,7 @@ class AppLocalizationsGl extends AppLocalizations { String get puzzleThemeDiscoveredAttack => 'Ataque descuberto'; @override - String get puzzleThemeDiscoveredAttackDescription => 'Apartar unha peza que previamente bloqueaba o ataque doutra peza de longo alcance (por exemplo un cabalo fóra do camiño dunha torre).'; + String get puzzleThemeDiscoveredAttackDescription => 'Apartar unha peza (por exemplo un cabalo) que previamente bloqueaba o ataque doutra peza de longo alcance (por exemplo unha torre).'; @override String get puzzleThemeDoubleCheck => 'Xaque dobre'; @@ -1390,10 +1615,10 @@ class AppLocalizationsGl extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'O rival ten os movementos limitados e calquera xogada que faga empeora a súa posición.'; @override - String get puzzleThemeHealthyMix => 'Mestura equilibrada'; + String get puzzleThemeMix => 'Mestura equilibrada'; @override - String get puzzleThemeHealthyMixDescription => 'Un pouco de todo. Non sabes que vai vir, así que prepárate para calquera cousa! Coma nas partidas de verdade.'; + String get puzzleThemeMixDescription => 'Un pouco de todo. Non sabes que vai vir, así que prepárate para calquera cousa! Coma nas partidas de verdade.'; @override String get puzzleThemePlayerGames => 'Partidas de xogadores'; @@ -1797,9 +2022,6 @@ class AppLocalizationsGl extends AppLocalizations { @override String get removesTheDepthLimit => 'Elimina o límite de profundidade, e mantén o teu ordenador quente'; - @override - String get engineManager => 'Administrador do motor de análise'; - @override String get blunder => 'Metida de zoca'; @@ -1956,7 +2178,7 @@ class AppLocalizationsGl extends AppLocalizations { String get email => 'Correo electrónico'; @override - String get passwordReset => 'Cambiar contrasinal'; + String get passwordReset => 'restablecer o contrasinal'; @override String get forgotPassword => 'Esqueciches o teu contrasinal?'; @@ -2063,6 +2285,9 @@ class AppLocalizationsGl extends AppLocalizations { @override String get gamesPlayed => 'Partidas xogadas'; + @override + String get ok => 'Ok'; + @override String get cancel => 'Cancelar'; @@ -2772,7 +2997,13 @@ class AppLocalizationsGl extends AppLocalizations { String get other => 'Outro'; @override - String get reportDescriptionHelp => 'Pega a ligazón á(s) partida(s) e explica o que é incorrecto no comportamento deste usuario. Non digas só \"fai trampas\", cóntanos como chegaches a esa conclusión. A túa denuncia será procesada máis rapidamente se está escrita en inglés.'; + String get reportCheatBoostHelp => 'Pega a ligazón á(s) partida(s) e explica que ten de malo o comportamento deste usuario. Non digas soamente \"fai trampas\", mais cóntanos como chegaches a esta conclusión.'; + + @override + String get reportUsernameHelp => 'Explica que ten de ofensivo este nome de usuario. Non digas soamente \"é ofensivo/inadecuado\". Cóntanos como chegaches a esta conclusión, especialmente se o insulto está camuflado, non está en inglés, é xerga ou é unha referencia histórica ou cultural.'; + + @override + String get reportProcessedFasterInEnglish => 'A túa denuncia será procesada máis rápido se está escrita en Inglés.'; @override String get error_provideOneCheatedGameLink => 'Por favor, incorpora cando menos unha ligazón a unha partida na que se fixeron trampas.'; @@ -2985,7 +3216,7 @@ class AppLocalizationsGl extends AppLocalizations { @override String error_maxLength(String param) { - return 'A lonxitude máxima é de $param caracteres'; + return 'Debe conter polo menos $param caracteres'; } @override @@ -3034,7 +3265,7 @@ class AppLocalizationsGl extends AppLocalizations { @override String tpTimeSpentOnTV(String param) { - return 'Tempo en TV: $param'; + return 'Tempo saíndo en TV: $param'; } @override @@ -3119,7 +3350,7 @@ class AppLocalizationsGl extends AppLocalizations { String get aboutSimulRealLife => 'O concepto tómase de eventos reais. Nestes, o anfitrión das simultáneas móvese de mesa en mesa e fai unha xogada de cada vez.'; @override - String get aboutSimulRules => 'Cando as simultáneas comezan, cada xogador inicia unha partida contra o anfitrión. As simultáneas finalizan cando todas as partidas rematan.'; + String get aboutSimulRules => 'Cando comezan as simultáneas, cada xogador inicia unha partida co anfitrión. As simultáneas finalizan cando se completan todas as partidas.'; @override String get aboutSimulSettings => 'As simultáneas son sempre amigables. As opcións de desquite, de desfacer a xogada e de engadir tempo non están activadas.'; @@ -3140,7 +3371,7 @@ class AppLocalizationsGl extends AppLocalizations { String get simulAddExtraTime => 'Podes engadir tempo extra ó teu reloxo para axudarte coas simultáneas.'; @override - String get simulHostExtraTime => 'Tempo extra para o anfitrión'; + String get simulHostExtraTime => 'Tempo inicial extra para o anfitrión'; @override String get simulAddExtraTimePerPlayer => 'Engadir tempo inicial ao teu reloxo por cada xogador que se una ás simultáneas.'; @@ -3212,7 +3443,7 @@ class AppLocalizationsGl extends AppLocalizations { String get toggleGlyphAnnotations => 'Activar/desactivar as anotacións con símbolos'; @override - String get togglePositionAnnotations => 'Alternar anotaciones de posición'; + String get togglePositionAnnotations => 'Activar/desactivar as anotacións'; @override String get variationArrowsInfo => 'As frechas das variantes permítenche navegar sen usar a lista de movementos.'; @@ -3323,7 +3554,7 @@ class AppLocalizationsGl extends AppLocalizations { String get crosstable => 'Táboa de cruces'; @override - String get youCanAlsoScrollOverTheBoardToMoveInTheGame => 'Tamén podes usar a roda do rato sobre o taboleiro para moverte pola partida.'; + String get youCanAlsoScrollOverTheBoardToMoveInTheGame => 'Usa a roda do rato sobre o taboleiro para moverte pola partida.'; @override String get scrollOverComputerVariationsToPreviewThem => 'Pasa o punteiro sobre as variantes da computadora para visualizalas.'; @@ -3915,7 +4146,7 @@ class AppLocalizationsGl extends AppLocalizations { String get tournDescriptionHelp => 'Queres dicirlles algo en especial ós participantes? Tenta ser breve. Hai dispoñibles ligazóns de Markdown: [name](https://url)'; @override - String get ratedFormHelp => 'As partidas son puntuadas\ne afectan ás puntuacións dos xogadores'; + String get ratedFormHelp => 'As partidas son puntuadas e afectan ás puntuacións dos xogadores'; @override String get onlyMembersOfTeam => 'Só membros do equipo'; @@ -4054,7 +4285,7 @@ class AppLocalizationsGl extends AppLocalizations { String get until => 'Ata'; @override - String get lichessDbExplanation => 'Partidas puntuadas de todos os xogadores de Lichess'; + String get lichessDbExplanation => 'Partidas puntuadas xogadas en Lichess'; @override String get switchSides => 'Cambiar de cor'; @@ -4077,6 +4308,9 @@ class AppLocalizationsGl extends AppLocalizations { @override String get nothingToSeeHere => 'Nada que ver aquí polo de agora.'; + @override + String get stats => 'Estatísticas'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4723,9 +4957,514 @@ class AppLocalizationsGl extends AppLocalizations { @override String get streamerLichessStreamers => 'Presentadores de Lichess'; + @override + String get studyPrivate => 'Privado'; + + @override + String get studyMyStudies => 'Os meus estudos'; + + @override + String get studyStudiesIContributeTo => 'Estudos nos que contribúo'; + + @override + String get studyMyPublicStudies => 'Os meus estudos públicos'; + + @override + String get studyMyPrivateStudies => 'Os meus estudos privados'; + + @override + String get studyMyFavoriteStudies => 'Os meus estudos favoritos'; + + @override + String get studyWhatAreStudies => 'Que son os estudos?'; + + @override + String get studyAllStudies => 'Todos os estudos'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Estudos creados por $param'; + } + + @override + String get studyNoneYet => 'Aínda non hai.'; + + @override + String get studyHot => 'Candentes'; + + @override + String get studyDateAddedNewest => 'Data engadida (máis novos)'; + + @override + String get studyDateAddedOldest => 'Data engadida (máis antigos)'; + + @override + String get studyRecentlyUpdated => 'Actualizados recentemente'; + + @override + String get studyMostPopular => 'Máis populares'; + + @override + String get studyAlphabetical => 'Alfabeticamente'; + + @override + String get studyAddNewChapter => 'Engadir un novo capítulo'; + + @override + String get studyAddMembers => 'Engadir membros'; + + @override + String get studyInviteToTheStudy => 'Invitar ao estudo'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Por favor, convida só a persoas que te coñezan e que desexen activamente unirse a este estudo.'; + + @override + String get studySearchByUsername => 'Buscar por nome de usuario'; + + @override + String get studySpectator => 'Espectador'; + + @override + String get studyContributor => 'Colaborador'; + + @override + String get studyKick => 'Expulsar'; + + @override + String get studyLeaveTheStudy => 'Deixar o estudo'; + + @override + String get studyYouAreNowAContributor => 'Agora es un colaborador'; + + @override + String get studyYouAreNowASpectator => 'Agora es un espectador'; + + @override + String get studyPgnTags => 'Etiquetas PGN'; + + @override + String get studyLike => 'Gústame'; + + @override + String get studyUnlike => 'Xa non me gusta'; + + @override + String get studyNewTag => 'Nova etiqueta'; + + @override + String get studyCommentThisPosition => 'Comentar nesta posición'; + + @override + String get studyCommentThisMove => 'Comentar este movemento'; + + @override + String get studyAnnotateWithGlyphs => 'Anotar con símbolos'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'O capítulo é demasiado curto para analizalo.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Só os colaboradores do estudo poden solicitar unha análise por ordenador.'; + + @override + String get studyGetAFullComputerAnalysis => 'Obtén unha análise completa da liña principal dende o servidor.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Asegúrate de que o capítulo está completo. Só podes solicitar a análise unha vez.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Todos os membros sincronizados permanecen na mesma posición'; + + @override + String get studyShareChanges => 'Comparte os cambios cos espectadores e gárdaos no servidor'; + + @override + String get studyPlaying => 'En xogo'; + + @override + String get studyShowEvalBar => 'Indicadores de avaliación'; + + @override + String get studyFirst => 'Primeiro'; + + @override + String get studyPrevious => 'Anterior'; + + @override + String get studyNext => 'Seguinte'; + + @override + String get studyLast => 'Último'; + @override String get studyShareAndExport => 'Compartir e exportar'; + @override + String get studyCloneStudy => 'Clonar'; + + @override + String get studyStudyPgn => 'PGN do estudo'; + + @override + String get studyDownloadAllGames => 'Descargar todas as partidas'; + + @override + String get studyChapterPgn => 'PGN do capítulo'; + + @override + String get studyCopyChapterPgn => 'Copiar PGN'; + + @override + String get studyDownloadGame => 'Descargar partida'; + + @override + String get studyStudyUrl => 'URL do estudo'; + + @override + String get studyCurrentChapterUrl => 'Ligazón do capítulo actual'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Podes pegar esta URL no foro ou no teu blog de Lichess para incrustala'; + + @override + String get studyStartAtInitialPosition => 'Comezar desde a posición inicial do estudo'; + + @override + String studyStartAtX(String param) { + return 'Comezar en $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Incrustar na túa páxina web ou blog'; + + @override + String get studyReadMoreAboutEmbedding => 'Ler máis sobre como inserir contido'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Só se poden inserir estudos públicos!'; + + @override + String get studyOpen => 'Abrir'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1 traído para ti por $param2'; + } + + @override + String get studyStudyNotFound => 'Estudo non atopado'; + + @override + String get studyEditChapter => 'Editar capítulo'; + + @override + String get studyNewChapter => 'Novo capítulo'; + + @override + String studyImportFromChapterX(String param) { + return 'Importar de $param'; + } + + @override + String get studyOrientation => 'Orientación'; + + @override + String get studyAnalysisMode => 'Modo de análise'; + + @override + String get studyPinnedChapterComment => 'Comentario do capítulo fixado'; + + @override + String get studySaveChapter => 'Gardar capítulo'; + + @override + String get studyClearAnnotations => 'Borrar anotacións'; + + @override + String get studyClearVariations => 'Borrar variantes'; + + @override + String get studyDeleteChapter => 'Borrar capítulo'; + + @override + String get studyDeleteThisChapter => 'Realmente queres borrar o capítulo? Non hai volta atrás!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Borrar todos os comentarios, símbolos e marcas do capítulo'; + + @override + String get studyRightUnderTheBoard => 'Xusto debaixo do taboleiro'; + + @override + String get studyNoPinnedComment => 'Ningún'; + + @override + String get studyNormalAnalysis => 'Análise normal'; + + @override + String get studyHideNextMoves => 'Ocultar os seguintes movementos'; + + @override + String get studyInteractiveLesson => 'Lección interactiva'; + + @override + String studyChapterX(String param) { + return 'Capítulo $param'; + } + + @override + String get studyEmpty => 'Baleiro'; + + @override + String get studyStartFromInitialPosition => 'Comezar desde a posición inicial'; + + @override + String get studyEditor => 'Editor'; + + @override + String get studyStartFromCustomPosition => 'Comezar dende unha posición personalizada'; + + @override + String get studyLoadAGameByUrl => 'Cargar as partidas dende unha URL'; + + @override + String get studyLoadAPositionFromFen => 'Cargar unha posición dende o FEN'; + + @override + String get studyLoadAGameFromPgn => 'Cargar as partidas dende o PGN'; + + @override + String get studyAutomatic => 'Automática'; + + @override + String get studyUrlOfTheGame => 'Ligazóns das partidas, unha por liña'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Cargar partidas dende $param1 ou $param2'; + } + + @override + String get studyCreateChapter => 'Crear capítulo'; + + @override + String get studyCreateStudy => 'Crear estudo'; + + @override + String get studyEditStudy => 'Editar estudo'; + + @override + String get studyVisibility => 'Visibilidade'; + + @override + String get studyPublic => 'Público'; + + @override + String get studyUnlisted => 'Sen listar'; + + @override + String get studyInviteOnly => 'Acceso só mediante invitación'; + + @override + String get studyAllowCloning => 'Permitir clonado'; + + @override + String get studyNobody => 'Ninguén'; + + @override + String get studyOnlyMe => 'Só eu'; + + @override + String get studyContributors => 'Colaboradores'; + + @override + String get studyMembers => 'Membros'; + + @override + String get studyEveryone => 'Todo o mundo'; + + @override + String get studyEnableSync => 'Activar sincronización'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Si: todos verán a mesma posición'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Non: permitir que a xente navegue libremente'; + + @override + String get studyPinnedStudyComment => 'Comentario fixado do estudo'; + @override String get studyStart => 'Comezar'; + + @override + String get studySave => 'Gardar'; + + @override + String get studyClearChat => 'Borrar a sala de conversa'; + + @override + String get studyDeleteTheStudyChatHistory => 'Borrar o historial da sala de conversa? Esta acción non se pode desfacer!'; + + @override + String get studyDeleteStudy => 'Borrar estudo'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Borrar todo o estudo? Non se poderá recuperar! Teclea o nome do estudo para confirmar: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Onde queres estudar isto?'; + + @override + String get studyGoodMove => 'Bo movemento'; + + @override + String get studyMistake => 'Erro'; + + @override + String get studyBrilliantMove => 'Movemento brillante'; + + @override + String get studyBlunder => 'Metida de zoca'; + + @override + String get studyInterestingMove => 'Movemento interesante'; + + @override + String get studyDubiousMove => 'Movemento dubidoso'; + + @override + String get studyOnlyMove => 'Movemento único'; + + @override + String get studyZugzwang => 'Zugzwang'; + + @override + String get studyEqualPosition => 'Posición igualada'; + + @override + String get studyUnclearPosition => 'Posición pouco clara'; + + @override + String get studyWhiteIsSlightlyBetter => 'As brancas están lixeiramente mellor'; + + @override + String get studyBlackIsSlightlyBetter => 'As negras están lixeiramente mellor'; + + @override + String get studyWhiteIsBetter => 'As brancas están mellor'; + + @override + String get studyBlackIsBetter => 'As negras están mellor'; + + @override + String get studyWhiteIsWinning => 'As brancas están gañando'; + + @override + String get studyBlackIsWinning => 'As negras están gañando'; + + @override + String get studyNovelty => 'Novidade'; + + @override + String get studyDevelopment => 'Desenvolvemento'; + + @override + String get studyInitiative => 'Iniciativa'; + + @override + String get studyAttack => 'Ataque'; + + @override + String get studyCounterplay => 'Contraataque'; + + @override + String get studyTimeTrouble => 'Apuros de tempo'; + + @override + String get studyWithCompensation => 'Con compensación'; + + @override + String get studyWithTheIdea => 'Coa idea'; + + @override + String get studyNextChapter => 'Capítulo seguinte'; + + @override + String get studyPrevChapter => 'Capítulo anterior'; + + @override + String get studyStudyActions => 'Accións de estudo'; + + @override + String get studyTopics => 'Temas'; + + @override + String get studyMyTopics => 'Os meus temas'; + + @override + String get studyPopularTopics => 'Temas populares'; + + @override + String get studyManageTopics => 'Administrar temas'; + + @override + String get studyBack => 'Voltar'; + + @override + String get studyPlayAgain => 'Xogar de novo'; + + @override + String get studyWhatWouldYouPlay => 'Que xogarías nesta posición?'; + + @override + String get studyYouCompletedThisLesson => 'Parabéns! Completaches esta lección.'; + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Capítulos', + one: '$count Capítulo', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Partidas', + one: '$count Partida', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Membros', + one: '$count Membro', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Pega o teu texto PGN aquí, ata $count partidas', + one: 'Pega o teu texto PGN aquí, ata $count partida', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_gsw.dart b/lib/l10n/l10n_gsw.dart index 80685dbfce..76a82a010b 100644 --- a/lib/l10n/l10n_gsw.dart +++ b/lib/l10n/l10n_gsw.dart @@ -48,7 +48,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get mobileNotFollowingAnyUser => 'Du folgsch keim Schpiller.'; @override - String get mobileAllGames => 'Alli Partie'; + String get mobileAllGames => 'All Schpiel'; @override String get mobileRecentSearches => 'Kürzlich Gsuechts'; @@ -103,9 +103,6 @@ class AppLocalizationsGsw extends AppLocalizations { @override String get mobileCancelTakebackOffer => 'Zugsrücknam-Offerte zruggzieh'; - @override - String get mobileCancelDrawOffer => 'Remis-Agebot zruggzieh'; - @override String get mobileWaitingForOpponentToJoin => 'Warte bis en Gegner erschint...'; @@ -116,7 +113,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get mobileLiveStreamers => 'Live Streamer'; @override - String get mobileCustomGameJoinAGame => 'Bi ere Partie mitschpille'; + String get mobileCustomGameJoinAGame => 'Es Schpiel mitschpille'; @override String get mobileCorrespondenceClearSavedMove => 'Lösch die gschpeicherete Züg'; @@ -142,7 +139,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get mobileGreetingWithoutName => 'Hoi'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobilePrefMagnifyDraggedPiece => 'Vegrösserig vu de zogene Figur'; @override String get activityActivity => 'Aktivitäte'; @@ -196,8 +193,8 @@ class AppLocalizationsGsw extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Hät $count Partie $param2 gschpillt', - one: 'Hät $count Partie $param2 gschpillt', + other: 'Hät $count Schpiel $param2 gschpillt', + one: 'Hät $count Schpiel $param2 gschpillt', ); return '$_temp0'; } @@ -229,8 +226,8 @@ class AppLocalizationsGsw extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'i $count Fernschachpartie', - one: 'i $count Fernschachpartie', + other: 'in $count Korrespondänz-Schpiel', + one: 'in $count Korrespondänz-Schpiel', ); return '$_temp0'; } @@ -240,8 +237,19 @@ class AppLocalizationsGsw extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Hät $count Fernschachpartie gschpillt', - one: 'Hät $count Fernschachpartie gschpillt', + other: 'Hät $count Korrespondänz-Schpiel gschpillt', + one: 'Hät $count Korrespondänz-Schpiel gschpillt', + ); + return '$_temp0'; + } + + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count $param2 Korrespondänz-Schpiel gschpillt', + one: '$count $param2 Korrespondänz-Schpiel gschpillt', ); return '$_temp0'; } @@ -348,16 +356,233 @@ class AppLocalizationsGsw extends AppLocalizations { @override String get broadcastBroadcasts => 'Überträgige'; + @override + String get broadcastMyBroadcasts => 'Eigeni Überträgige'; + @override String get broadcastLiveBroadcasts => 'Live Turnier-Überträgige'; + @override + String get broadcastBroadcastCalendar => 'Überträgigs-Kaländer'; + + @override + String get broadcastNewBroadcast => 'Neui Live-Überträgige'; + + @override + String get broadcastSubscribedBroadcasts => 'Abonnierti Überträgige'; + + @override + String get broadcastAboutBroadcasts => 'Über Überträgige'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Wie mer Lichess-Überträgige benutzt.'; + + @override + String get broadcastTheNewRoundHelp => 'Die neu Runde wird us de gliche Mitglieder und Mitwürkende beschtah, wie die Vorherig.'; + + @override + String get broadcastAddRound => 'E Rundi zuefüege'; + + @override + String get broadcastOngoing => 'Laufend'; + + @override + String get broadcastUpcoming => 'Demnächscht'; + + @override + String get broadcastCompleted => 'Beändet'; + + @override + String get broadcastCompletedHelp => 'Lichess erkännt de Rundeschluss oder au nöd! De Schalter setz das uf \"manuell\".'; + + @override + String get broadcastRoundName => 'Runde Name'; + + @override + String get broadcastRoundNumber => 'Runde Nummere'; + + @override + String get broadcastTournamentName => 'Turnier Name'; + + @override + String get broadcastTournamentDescription => 'Churzi Turnier Beschribig'; + + @override + String get broadcastFullDescription => 'Vollschtändigi Ereignisbeschribig'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Optionali, usfüehrlichi Beschribig vu de Überträgig. $param1 isch verfügbar. Die Beschribig muess chürzer als $param2 Zeiche si.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN Quälle URL'; + + @override + String get broadcastSourceUrlHelp => 'URL wo Lichess abfrögt, für PGN Aktualisierige z\'erhalte. Sie muess öffentlich im Internet zuegänglich si.'; + + @override + String get broadcastSourceGameIds => 'Bis zu 64 Lichess Schpiel-IDs, trännt dur en Leerschlag.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Startdatum i de lokale Zitzone vum Turnier: $param'; + } + + @override + String get broadcastStartDateHelp => 'Optional, falls du weisch, wänn das Ereignis afangt'; + + @override + String get broadcastCurrentGameUrl => 'URL vom laufende Schpiel'; + + @override + String get broadcastDownloadAllRounds => 'Alli Runde abelade'; + + @override + String get broadcastResetRound => 'Die Rundi zruggsetze'; + + @override + String get broadcastDeleteRound => 'Die Rundi lösche'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Die Rundi, mit allne Schpiel, definitiv lösche.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Lösch alli Schpiel vu dere Rundi. D\'Quälle muess aktiv si, dass sie neu erschtellt werde chönd.'; + + @override + String get broadcastEditRoundStudy => 'Runde-Schtudie bearbeite'; + + @override + String get broadcastDeleteTournament => 'Lösch das Turnier'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Das ganze Turnier, alli Runde und alli Schpiel definitiv lösche.'; + + @override + String get broadcastShowScores => 'Zeigt d\'Erfolg vu de Schpiller, anhand vu Schpiel-Ergäbnis'; + + @override + String get broadcastReplacePlayerTags => 'Optional: Schpillernäme, Wertige und Titel weg lah'; + + @override + String get broadcastFideFederations => 'FIDE Wältschachverband'; + + @override + String get broadcastTop10Rating => 'Top 10 Ratings'; + + @override + String get broadcastFidePlayers => 'FIDE Schpiller'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE Schpiller nöd g\'funde'; + + @override + String get broadcastFideProfile => 'FIDE Profil'; + + @override + String get broadcastFederation => 'Verband'; + + @override + String get broadcastAgeThisYear => 'Alter i dem Jahr'; + + @override + String get broadcastUnrated => 'Ungwertet'; + + @override + String get broadcastRecentTournaments => 'Aktuellschti Turnier'; + + @override + String get broadcastOpenLichess => 'In Lichess öffne'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Brätter'; + + @override + String get broadcastOverview => 'Überblick'; + + @override + String get broadcastSubscribeTitle => 'Mäld dich a, zum über jede Rundeschtart informiert z\'werde. Du chasch de Alarm- oder d\'Push-Benachrichtigung, für Überträgige, i dine Kontoischtellige umschalte.'; + + @override + String get broadcastUploadImage => 'Turnier-Bild ufelade'; + + @override + String get broadcastNoBoardsYet => 'No kei Brätter. Die erschined, sobald Schpiel ufeglade sind.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Brätter chönd mit ere Quälle oder via $param ufeglade werde'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Schtartet nach $param'; + } + + @override + String get broadcastStartVerySoon => 'Die Überträgig schtartet sehr bald.'; + + @override + String get broadcastNotYetStarted => 'Die Überträgig hät nonig agfange.'; + + @override + String get broadcastOfficialWebsite => 'Offizielli Website'; + + @override + String get broadcastStandings => 'Tabälle'; + + @override + String broadcastIframeHelp(String param) { + return 'Meh Optionen uf $param'; + } + + @override + String get broadcastWebmastersPage => 'Webmaster Site'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'Öffentlichi, real-time PGN Quälle, für die Rundi. Mir offeriered au $param für e schnälleri und effiziänteri Synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Nimm die Überträgig uf dini Website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Nimm $param uf dini Website'; + } + + @override + String get broadcastRatingDiff => 'Wertigs Differänz'; + + @override + String get broadcastGamesThisTournament => 'Schpiel i dem Turnier'; + + @override + String get broadcastScore => 'Resultat'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Überträgige', + one: '$count Überträgige', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Useforderige: $param1'; } @override - String get challengeChallengeToPlay => 'Zunere Partie usefordere'; + String get challengeChallengeToPlay => 'Zum Schpiel fordere'; @override String get challengeChallengeDeclined => 'Useforderig abglehnt'; @@ -456,7 +681,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get perfStatProvisional => 'provisorisch'; @override - String get perfStatNotEnoughRatedGames => 'Nöd gnueg gwerteti Schpil, für e verlässlichi Wertig z\'errächne.'; + String get perfStatNotEnoughRatedGames => 'Nöd gnueg g\'werteti Schpil, zum e verlässlichi Wertig z\'errächne.'; @override String perfStatProgressOverLastXGames(String param) { @@ -477,7 +702,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get perfStatTotalGames => 'Alli Schpil'; @override - String get perfStatRatedGames => 'Gwerteti Schpil'; + String get perfStatRatedGames => 'G\'werteti Schpiel'; @override String get perfStatTournamentGames => 'Turnier Schpil'; @@ -538,7 +763,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get perfStatBestRated => 'die beschte Sieg'; @override - String get perfStatGamesInARow => 'In Serie gschpillti Partie'; + String get perfStatGamesInARow => 'Nachenand g\'schpillti Schpiel'; @override String get perfStatLessThanOneHour => 'Weniger als 1 Schtund zwüsche de Schpil'; @@ -598,7 +823,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get preferencesShowFlairs => 'Benutzer-Emojis azeige'; @override - String get preferencesExplainShowPlayerRatings => 'Das erlaubt s\'Usblände vu allne Wertige uf de Site und hilft, sich ufs Schach z\'konzentriere. Partie chönd immer no bewertet werde, es gaht nur um das, was du gsesch.'; + String get preferencesExplainShowPlayerRatings => 'Das erlaubt s\'Usblände vu allne Wertige uf de Site und hilft, sich ufs Schach z\'konzentriere. s\'Schpiel wird immer no bewertet werde, es gaht nur drum, was mer gseht.'; @override String get preferencesDisplayBoardResizeHandle => 'Zeig de Brättgrössi Regler'; @@ -649,7 +874,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get preferencesTakebacksWithOpponentApproval => 'Zugsrücknahm (mit Erlaubnis vom Gägner)'; @override - String get preferencesInCasualGamesOnly => 'Nur in ungwertete Schpiel'; + String get preferencesInCasualGamesOnly => 'Nur in nöd g\'wertete Schpiel'; @override String get preferencesPromoteToQueenAutomatically => 'Automatischi Umwandlig zur Dame'; @@ -709,7 +934,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get preferencesScrollOnTheBoardToReplayMoves => 'Mit em Muszeiger uf em Brätt, chasch mit em Musrad all Züg vor- und zrugg scrolle'; @override - String get preferencesCorrespondenceEmailNotification => 'Täglichi E-Mail-Benachrichtigung, wo Dini Fernschachpartie uflischtet'; + String get preferencesCorrespondenceEmailNotification => 'Täglichi E-Mail-Benachrichtigung, wo dini Korrespondänz-Schpiel uflischtet'; @override String get preferencesNotifyStreamStart => 'De Streamer gaht live'; @@ -843,7 +1068,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get puzzlePuzzlesByOpenings => 'Ufgabe nach Eröffnige'; @override - String get puzzleOpeningsYouPlayedTheMost => 'Dini meischt-gschpillte Eröffnige, i gwertete Partie'; + String get puzzleOpeningsYouPlayedTheMost => 'Dini meischt-gschpillte Eröffnige, i g\'wertete Schpiel'; @override String get puzzleUseFindInPage => 'Benutz im Browser \"Suchen...\", um dini bevorzugti Eröffnig z\'finde!'; @@ -867,7 +1092,7 @@ class AppLocalizationsGsw extends AppLocalizations { @override String puzzleFromGameLink(String param) { - return 'Us de Partie $param'; + return 'Vom Schpiel $param'; } @override @@ -942,22 +1167,22 @@ class AppLocalizationsGsw extends AppLocalizations { String get puzzleFromMyGames => 'Us eigene Schpil'; @override - String get puzzleLookupOfPlayer => 'Suech Ufgabe us de Partie vu me Schpiller'; + String get puzzleLookupOfPlayer => 'Suech Ufgabe us de Schpiel vu me Schpiller'; @override String puzzleFromXGames(String param) { - return 'Ufgabe us Partie vu $param'; + return 'Ufgabe us Schpiel vu $param'; } @override String get puzzleSearchPuzzles => 'Suech Ufgabe'; @override - String get puzzleFromMyGamesNone => 'Eu häsch kei Ufgabe i de Datäbank, aber Lichess schätzt dich immerno sehr.\n\nSchpill schnälli und klassischi Partie, so erhöht sich d\'Chance, dass au Ufgabe us dine eigene Schpil zuegfüegt werded!'; + String get puzzleFromMyGamesNone => 'Du häsch kei Ufgabe i de Datäbank, aber Lichess schätzt dich immerno sehr.\n\nSchpill schnälli und au klassischi Schpiel, will so erhöht sich d\'Chance, dass Ufgabe vu dine eigene Schpiel zuegfüegt werded!'; @override String puzzleFromXGamesFound(String param1, String param2) { - return '$param1 Ufgabe in $param2 Partie gfunde'; + return '$param1 Ufgabe in Schpiel vu $param2 gfunde'; } @override @@ -1162,7 +1387,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get puzzleThemeEndgame => 'Ändschpil'; @override - String get puzzleThemeEndgameDescription => 'E Taktik für die letscht Fase vu de Partie.'; + String get puzzleThemeEndgameDescription => 'E Taktik für die letscht Fase vum Schpiel.'; @override String get puzzleThemeEnPassantDescription => 'E Taktik wo \"En-Passant\" beinhaltet - e Regle wo en Pur cha en gägnerische Pur schlaa, wänn de ihn mit em \"Zwei-Fälder-Zug\" übergange hät.'; @@ -1216,16 +1441,16 @@ class AppLocalizationsGsw extends AppLocalizations { String get puzzleThemeLongDescription => '3 Züg zum Sieg.'; @override - String get puzzleThemeMaster => 'Meischter Partie'; + String get puzzleThemeMaster => 'Meischter Schpiel'; @override - String get puzzleThemeMasterDescription => 'Ufgabe us Partie vu Schpiller mit Titel.'; + String get puzzleThemeMasterDescription => 'Ufgabe us Schpiel vu Schpiller mit Titel.'; @override - String get puzzleThemeMasterVsMaster => 'Meischter gäge Meischter Partie'; + String get puzzleThemeMasterVsMaster => 'Meischter gäge Meischter Schpiel'; @override - String get puzzleThemeMasterVsMasterDescription => 'Ufgabe us Partie vu 2 Schpiller mit Titel.'; + String get puzzleThemeMasterVsMasterDescription => 'Ufgabe us Schpiel vu 2 Schpiller mit Titel.'; @override String get puzzleThemeMate => 'Schachmatt'; @@ -1267,7 +1492,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get puzzleThemeMiddlegame => 'Mittelschpiel'; @override - String get puzzleThemeMiddlegameDescription => 'E Taktik für die zweit Fase vu de Partie.'; + String get puzzleThemeMiddlegameDescription => 'E Taktik für die zweit Fase vum Schpiel.'; @override String get puzzleThemeOneMove => '1-zügigi Ufgab'; @@ -1279,7 +1504,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get puzzleThemeOpening => 'Eröffnig'; @override - String get puzzleThemeOpeningDescription => 'E Taktik für die erscht Fase vu de Partie.'; + String get puzzleThemeOpeningDescription => 'E Taktik für die erscht Fase vum Schpiel.'; @override String get puzzleThemePawnEndgame => 'Pure Ändschpiel'; @@ -1354,10 +1579,10 @@ class AppLocalizationsGsw extends AppLocalizations { String get puzzleThemeSmotheredMateDescription => 'Es Schachmatt mit em Springer, wo sich de König nöd bewege cha, will er vu sine eigene Figuren umstellt-, also vollkomme igschlosse, wird.'; @override - String get puzzleThemeSuperGM => 'Super-Grossmeischter-Partie'; + String get puzzleThemeSuperGM => 'Super-Grossmeischter-Schpiel'; @override - String get puzzleThemeSuperGMDescription => 'Ufgabe us Partie, vu de beschte Schpiller uf de Wält.'; + String get puzzleThemeSuperGMDescription => 'Ufgabe us Schpiel, vu de beschte Schpiller uf de Wält.'; @override String get puzzleThemeTrappedPiece => 'G\'fangeni Figur'; @@ -1390,13 +1615,13 @@ class AppLocalizationsGsw extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'De Gägner hät nur e limitierti Azahl Züg und Jede verschlächteret sini Schtellig.'; @override - String get puzzleThemeHealthyMix => 'En gsunde Mix'; + String get puzzleThemeMix => 'En gsunde Mix'; @override - String get puzzleThemeHealthyMixDescription => 'Es bitzli vu Allem, me weiss nöd was eim erwartet, drum isch mer uf alles g\'fasst - genau wie bi richtige Schachschpiel.'; + String get puzzleThemeMixDescription => 'Es bitzli vu Allem, me weiss nöd was eim erwartet, drum isch mer uf alles g\'fasst - genau wie bi richtige Schachschpiel.'; @override - String get puzzleThemePlayerGames => 'Schpiller Partie'; + String get puzzleThemePlayerGames => 'Schpiller-Schpiel'; @override String get puzzleThemePlayerGamesDescription => 'Suech nach Ufgabe us dine Schpiel oder Ufgabe us Schpiel vu Andere.'; @@ -1538,7 +1763,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get yourOpponentWantsToPlayANewGameWithYou => 'Din Gägner wett es neus Schpil mit dir schpille'; @override - String get joinTheGame => 'Tritt de Partie bi'; + String get joinTheGame => 'Gang is Schpiel'; @override String get whitePlays => 'Wiss am Zug'; @@ -1571,7 +1796,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get whiteLeftTheGame => 'Wiss hät d\'Partie verlah'; @override - String get blackLeftTheGame => 'Schwarz hät d\'Partie verlah'; + String get blackLeftTheGame => 'Schwarz hät s\'Schpiel verlah'; @override String get whiteDidntMove => 'Wiss hät nöd zoge'; @@ -1692,14 +1917,14 @@ class AppLocalizationsGsw extends AppLocalizations { } @override - String get recentGames => 'Aktuelli Partie'; + String get recentGames => 'Aktuelli Schpiel'; @override - String get topGames => 'Beschti-Partie'; + String get topGames => 'Schpitzeschpiel'; @override String masterDbExplanation(String param1, String param2, String param3) { - return '2 Millione OTB Schpil vu $param1+ FIDE-gwertete Schpiller vu $param2 bis $param3'; + return '2 Millione OTB Schpiel vu $param1+ FIDE-g\'wertete Schpiller vu $param2 bis $param3'; } @override @@ -1797,9 +2022,6 @@ class AppLocalizationsGsw extends AppLocalizations { @override String get removesTheDepthLimit => 'Entfernt d\'Tüfebegränzig und haltet din Computer warm'; - @override - String get engineManager => 'Engine Betreuer'; - @override String get blunder => 'En Patzer'; @@ -1834,7 +2056,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get fiftyMovesWithoutProgress => '50 Züg ohni Fortschritt'; @override - String get currentGames => 'Laufendi Partie'; + String get currentGames => 'Laufendi Schpiel'; @override String get viewInFullSize => 'In voller Grössi azeige'; @@ -1858,7 +2080,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get computersAreNotAllowedToPlay => 'Computer und Computer-Understützig isch nöd erlaubt. Bitte lass dir bim Schpille nöd vu Schachengines, Datäbanke oder andere Schpiller hälfe. \nAu vum Erschtelle vu mehrere Konte wird dringend abgrate - übermässigs Multikonteverhalte chann zume Usschluss fühere.'; @override - String get games => 'Partie'; + String get games => 'Schpiel'; @override String get forum => 'Forum'; @@ -2061,7 +2283,10 @@ class AppLocalizationsGsw extends AppLocalizations { } @override - String get gamesPlayed => 'Gschpillti Partie'; + String get gamesPlayed => 'Schpiel g\'macht'; + + @override + String get ok => 'OK'; @override String get cancel => 'Abbräche'; @@ -2103,7 +2328,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get decline => 'Ablehnä'; @override - String get playingRightNow => 'Partie isch am laufe'; + String get playingRightNow => 'Schpiel lauft'; @override String get eventInProgress => 'Lauft jetzt'; @@ -2112,10 +2337,10 @@ class AppLocalizationsGsw extends AppLocalizations { String get finished => 'Beändet'; @override - String get abortGame => 'Partie abbräche'; + String get abortGame => 'Schpiel abbräche'; @override - String get gameAborted => 'Partie abbroche'; + String get gameAborted => 'Schpiel abbroche'; @override String get standard => 'Standard'; @@ -2142,7 +2367,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get ratedTournament => 'Gwertet'; @override - String get thisGameIsRated => 'Das Schpiel isch gwertet'; + String get thisGameIsRated => 'Das Schpiel isch g\'wertet'; @override String get rematch => 'Revanche'; @@ -2202,7 +2427,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get freeOnlineChess => 'Gratis Online-Schach'; @override - String get exportGames => 'Partie exportiere'; + String get exportGames => 'Schpiel exportiere'; @override String get ratingRange => 'Wertigsbereich'; @@ -2280,12 +2505,12 @@ class AppLocalizationsGsw extends AppLocalizations { @override String ratedMoreThanInPerf(String param1, String param2) { - return 'Gwertet ≥ $param1 in $param2'; + return 'G\'wertet ≥ $param1 in $param2'; } @override String ratedLessThanInPerf(String param1, String param2) { - return 'Wertig ≤ $param1 im $param2 die letschte 7 Täg'; + return '≤ $param1 im $param2 die letscht Wuche'; } @override @@ -2329,7 +2554,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get location => 'Ortschaft/Land'; @override - String get filterGames => 'Partie filtere'; + String get filterGames => 'Schpiel filtere'; @override String get reset => 'Zruggsetze'; @@ -2368,7 +2593,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get toStudy => 'Schtudie'; @override - String get importGame => 'Partie importiere'; + String get importGame => 'Schpiel importiere'; @override String get importGameExplanation => 'Füeg e Schpiel-PGN i, für Zuegriff uf Schpielwiderholig, Computeranalyse, Chat und e teilbari URL.'; @@ -2505,10 +2730,10 @@ class AppLocalizationsGsw extends AppLocalizations { String get makePrivateTournament => 'Mach das Turnier privat und beschränk de Zuegang mit Passwort'; @override - String get join => 'Mach mit'; + String get join => 'Mitmache'; @override - String get withdraw => 'Usstige'; + String get withdraw => 'Usschtige'; @override String get points => 'Pünkt'; @@ -2534,7 +2759,7 @@ class AppLocalizationsGsw extends AppLocalizations { } @override - String get pause => 'underbräche'; + String get pause => 'Pausiere'; @override String get resume => 'wieder ischtige'; @@ -2676,13 +2901,13 @@ class AppLocalizationsGsw extends AppLocalizations { String get activePlayers => 'Aktivi Schpiller'; @override - String get bewareTheGameIsRatedButHasNoClock => 'Achtung, das Schpiel isch gwertet, aber ohni Zitlimit!'; + String get bewareTheGameIsRatedButHasNoClock => 'Achtung, das Schpiel isch g\'wertet, aber ohni Zitlimit!'; @override String get success => 'Korräkt'; @override - String get automaticallyProceedToNextGameAfterMoving => 'Nach em Zug automatisch zur nächschte Partie'; + String get automaticallyProceedToNextGameAfterMoving => 'Nach em Zug automatisch zum nächschte Schpiel'; @override String get autoSwitch => 'Automatische Wächsel'; @@ -2772,7 +2997,13 @@ class AppLocalizationsGsw extends AppLocalizations { String get other => 'Suschtigs'; @override - String get reportDescriptionHelp => 'Füeg de Link dere/dene Partie bi und erchlär, wie sich de Benutzer falsch benah hät. Säg nöd nur \"de bschisst\", schrib eus wie du da druf chunnsch. (änglisch gschribeni Mäldige, werded schnäller behandlet).'; + String get reportCheatBoostHelp => 'Füeg de Link dem/dene Schpiel bi und erchlär, wie sich de Benutzer falsch benah hät. Säg nöd nur \"de bschisst\", schrib eus wie du da druf chunnsch. (änglisch gschribeni Mäldige, werded schnäller behandlet).'; + + @override + String get reportUsernameHelp => 'Erchlär, was am Benutzername nöd in Ornig isch: Säg nöd eifach \"er isch beleidigend/unagmässe\", sondern schrib eus, wie du zu dere Folgerig cho bisch. B\'sunders wänn die Beleidigung verschleieret-, nöd uf Änglisch- oder in Dialäkt isch oder wänn sie en historische oder kulturelle Bezug hät.'; + + @override + String get reportProcessedFasterInEnglish => 'Änglisch g\'schribeni Mäldige werded schnäller behandlet.'; @override String get error_provideOneCheatedGameLink => 'Bitte gib mindeschtens 1 Link zume Schpiel a, wo bschisse worde isch.'; @@ -2878,7 +3109,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get allSquaresOfTheBoard => 'Uf jedem Fäld'; @override - String get onSlowGames => 'Bi langsame Partie'; + String get onSlowGames => 'Bi langsame Schpiel'; @override String get always => 'Immer'; @@ -3030,7 +3261,7 @@ class AppLocalizationsGsw extends AppLocalizations { } @override - String get watchGames => 'Partie zueluege'; + String get watchGames => 'Schpiel beobachte'; @override String tpTimeSpentOnTV(String param) { @@ -3113,16 +3344,16 @@ class AppLocalizationsGsw extends AppLocalizations { String get aboutSimul => 'Bim Simultanschach schpillt 1 Simultanschpiller \nglichzitig gäge beliebig vill Simultangägner.'; @override - String get aboutSimulImage => 'Hät de Bobby Fischer, bi 50 Gägner, 47 Sieg \nund 2 Remis gschafft; nur 1 Partie hät er verlore.'; + String get aboutSimulImage => 'Hät de Bobby Fischer, bi 50 Gägner, 47 Sieg \nund 2 Remis gschafft - nu 1 Schpiel hät er verlore.'; @override - String get aboutSimulRealLife => 'Bim Simultanschach schpillt 1 Simultanschpiller \nglichzitig gäge beliebig vill Simultangägner, \nso lang, bis alli Partie fertig gschpillt sind.'; + String get aboutSimulRealLife => 'Bim Simultanschach schpillt 1 Simultanschpiller \nglichzitig gäge beliebig vill Simultangägner, \nso lang, bis alli Schpiel fertig gschpillt sind.'; @override String get aboutSimulRules => 'Wie bi reale Simultanschach Veraschtaltige, gaht \nde Simultanschpiller vu eim Simultangägner zum \nNächschte und macht bi jedem Brätt 1 Zug.'; @override - String get aboutSimulSettings => 'Zugsrücknahme, zuesätzlichi Zit oder Revanche \ngits nöd und es isch immer ungwertet.'; + String get aboutSimulSettings => 'Zugsrücknahme, zuesätzlichi Zit oder Revanche \ngits nöd und es isch nie g\'wertet.'; @override String get create => 'Erschtelle'; @@ -3578,7 +3809,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get getAHint => 'Lass dir en Tipp geh'; @override - String get evaluatingYourMove => 'Din Zug wird gwertet ...'; + String get evaluatingYourMove => 'Din Zug wird g\'wertet ...'; @override String get whiteWinsGame => 'Wiss günnt'; @@ -3705,7 +3936,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get howToAvoidThis => 'Wie chasch das verhindere?'; @override - String get playEveryGame => 'Schpill jedi Partie, wo du afangsch, au fertig.'; + String get playEveryGame => 'Schpill jedes Schpiel, wo du afangsch, au fertig.'; @override String get tryToWin => 'Probier jedes Schpil z\'günne (oder mindeschtens es Remis z\'erreiche).'; @@ -3776,7 +4007,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get classicalDesc => 'Klassischi Schpiel: 25 Minute und meh'; @override - String get correspondenceDesc => 'Fernschach Partie: 1 oder mehreri Täg pro Zug'; + String get correspondenceDesc => 'Korrespondänz-Schpiel: 1 oder mehreri Täg pro Zug'; @override String get puzzleDesc => 'Schach-Taktik Trainer'; @@ -3830,7 +4061,7 @@ class AppLocalizationsGsw extends AppLocalizations { } @override - String get youCannotPostYetPlaySomeGames => 'Du chasch nonig im Forum schribe: Schpill zerscht no es paar Partie!'; + String get youCannotPostYetPlaySomeGames => 'Du chasch no nüt im Forum schribe: Schpill vorher es paar Schpiel!'; @override String get subscribe => 'Abonniere'; @@ -3874,7 +4105,7 @@ class AppLocalizationsGsw extends AppLocalizations { @override String gameVsX(String param1) { - return 'Partie gäge $param1'; + return 'Schpiel gäge $param1'; } @override @@ -3915,7 +4146,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get tournDescriptionHelp => 'Wottsch de Teilnehmer öppis Speziells mitteile? Probier dich churz z\'fasse. Url mit Name sind möglich: [name](https://url)'; @override - String get ratedFormHelp => 'Alli Partie sind gwertet\nund beiflussed dini Wertig'; + String get ratedFormHelp => 'Alli Schpiel sind g\'wertet\nund beiflussed dini Wertig'; @override String get onlyMembersOfTeam => 'Nur Mitglider vum Team'; @@ -3924,7 +4155,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get noRestriction => 'Kei Ischränkige'; @override - String get minimumRatedGames => 'Minimum gwerteti Partie'; + String get minimumRatedGames => 'Minimum g\'werteti Schpiel'; @override String get minimumRating => 'Minimali Wertig'; @@ -3941,7 +4172,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get cancelSimul => 'Simultanschach abbräche'; @override - String get simulHostcolor => 'Farb vom Simultanschpiller, für jedi Partie'; + String get simulHostcolor => 'Farb vom Simultanschpiller, für jedes Schpiel'; @override String get estimatedStart => 'Vorussichtlichi Schtartzit'; @@ -4035,17 +4266,17 @@ class AppLocalizationsGsw extends AppLocalizations { @override String gameInProgress(String param) { - return 'Du häsch no e laufedi Partie mit $param.'; + return 'Du häsch no es laufeds Schpiel mit $param.'; } @override - String get abortTheGame => 'Partie abbräche'; + String get abortTheGame => 'Schpiel abbräche'; @override - String get resignTheGame => 'Partie ufgeh'; + String get resignTheGame => 'Schpiel ufgeh'; @override - String get youCantStartNewGame => 'Du chasch kei neui Partie starte, bevor die no Laufendi nöd fertig gschpillt isch.'; + String get youCantStartNewGame => 'Du chasch kei neus Schpiel schtarte, bevor s\'Laufende nöd fertig isch.'; @override String get since => 'Sit'; @@ -4054,7 +4285,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get until => 'Bis'; @override - String get lichessDbExplanation => 'Gwerteti Schpiel vu allne Lichess Schpiller'; + String get lichessDbExplanation => 'G\'werteti Schpiel vu allne Lichess Schpiller'; @override String get switchSides => 'Farb wächsle'; @@ -4077,13 +4308,16 @@ class AppLocalizationsGsw extends AppLocalizations { @override String get nothingToSeeHere => 'Da gits im monumäntan nüt z\'gseh.'; + @override + String get stats => 'Schtatistike'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, other: 'Din Gägner hät d\'Partie verlah, du chasch in $count Sekunde din Sieg beaschpruche', - one: 'Din Gägner hät d\'Partie verlah. Du chasch in $count Sekunde din Sieg beaschpruche', + one: 'Din Gägner hät s\'Schpiel verlah. Du chasch in $count Sekunde de Sieg beaschpruche', ); return '$_temp0'; } @@ -4148,8 +4382,8 @@ class AppLocalizationsGsw extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count Partie', - one: '$count Partie', + other: '$count Schpiel', + one: '$count Schpiel', ); return '$_temp0'; } @@ -4236,8 +4470,8 @@ class AppLocalizationsGsw extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count Partie mit dir', - one: '$count Partie mit dir', + other: '$count Schpiel mit dir', + one: '$count Schpiel mit dir', ); return '$_temp0'; } @@ -4248,7 +4482,7 @@ class AppLocalizationsGsw extends AppLocalizations { count, locale: localeName, other: '$count gwerteti', - one: '$count gwertet', + one: '$count g\'wertet', ); return '$_temp0'; } @@ -4291,8 +4525,8 @@ class AppLocalizationsGsw extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count laufendi Partie', - one: '$count laufendi Partie', + other: '$count am Schpille', + one: '$count am Schpille', ); return '$_temp0'; } @@ -4346,8 +4580,8 @@ class AppLocalizationsGsw extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '≥ $count gwerteti Schpiel', - one: '≥ $count gwertets Schpiel', + other: '≥ $count g\'werteti Schpiel', + one: '≥ $count g\'wertets Schpiel', ); return '$_temp0'; } @@ -4357,8 +4591,8 @@ class AppLocalizationsGsw extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '≥ $count gwerteti $param2 Schpiel', - one: '≥ $count gwertets $param2 Schpiel', + other: '≥ $count g\'werteti $param2 Schpiel', + one: '≥ $count g\'wertets $param2 Schpiel', ); return '$_temp0'; } @@ -4368,8 +4602,8 @@ class AppLocalizationsGsw extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Du muesch no $count gwerteti Partie meh $param2 schpille', - one: 'Du muesch no $count gwerteti Partie meh $param2 schpille', + other: 'Du muesch no $count g\'werteti Schpiel meh $param2 schpille', + one: 'Du muesch no $count g\'wertets Schpiel meh $param2 schpille', ); return '$_temp0'; } @@ -4390,8 +4624,8 @@ class AppLocalizationsGsw extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count importierti Partie', - one: '$count importierti Partie', + other: '$count importierti Schpiel', + one: '$count importierts Schpiel', ); return '$_temp0'; } @@ -4445,8 +4679,8 @@ class AppLocalizationsGsw extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count laufendi Partie', - one: '$count laufendi Partie', + other: '$count laufendi Schpiel', + one: '$count laufends Schpiel', ); return '$_temp0'; } @@ -4723,9 +4957,514 @@ class AppLocalizationsGsw extends AppLocalizations { @override String get streamerLichessStreamers => 'Lichess Streamer/-in'; + @override + String get studyPrivate => 'Privat'; + + @override + String get studyMyStudies => 'Mini Schtudie'; + + @override + String get studyStudiesIContributeTo => 'Schtudie, wo ich mitwürke'; + + @override + String get studyMyPublicStudies => 'Mini öffentliche Schtudie'; + + @override + String get studyMyPrivateStudies => 'Mini private Schtudie'; + + @override + String get studyMyFavoriteStudies => 'Mini liebschte Schtudie'; + + @override + String get studyWhatAreStudies => 'Was sind Schtudie?'; + + @override + String get studyAllStudies => 'All Schtudie'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Vu $param erschtellte Schtudie'; + } + + @override + String get studyNoneYet => 'No kei.'; + + @override + String get studyHot => 'agseit'; + + @override + String get studyDateAddedNewest => 'wänn zuegfüegt (neui)'; + + @override + String get studyDateAddedOldest => 'wänn zuegfüegt (älti)'; + + @override + String get studyRecentlyUpdated => 'frisch aktualisiert'; + + @override + String get studyMostPopular => 'beliebtschti'; + + @override + String get studyAlphabetical => 'alphabetisch'; + + @override + String get studyAddNewChapter => 'Neus Kapitel zuefüege'; + + @override + String get studyAddMembers => 'Mitglider zuefüege'; + + @override + String get studyInviteToTheStudy => 'Lad zu de Schtudie i'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Bitte lad nur Lüt i, wo du kännsch und wo wänd aktiv a dere Schtudie teilneh.'; + + @override + String get studySearchByUsername => 'Suech noch Benutzername'; + + @override + String get studySpectator => 'Zueschauer'; + + @override + String get studyContributor => 'Mitwürkende'; + + @override + String get studyKick => 'Userüere'; + + @override + String get studyLeaveTheStudy => 'Schtudie verlah'; + + @override + String get studyYouAreNowAContributor => 'Du bisch jetzt en Mitwürkende'; + + @override + String get studyYouAreNowASpectator => 'Du bisch jetzt Zueschauer'; + + @override + String get studyPgnTags => 'PGN Tags'; + + @override + String get studyLike => 'Gfallt mir'; + + @override + String get studyUnlike => 'Gfallt mir nümme'; + + @override + String get studyNewTag => 'Neue Tag'; + + @override + String get studyCommentThisPosition => 'Kommentier die Schtellig'; + + @override + String get studyCommentThisMove => 'Kommentier de Zug'; + + @override + String get studyAnnotateWithGlyphs => 'Mit Symbol kommentiere'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Das Kapitel isch z\'churz zum analysiere.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Nur wer a dere Schtudie mit macht, chann e Coputeranalyse afordere.'; + + @override + String get studyGetAFullComputerAnalysis => 'Erhalt e vollschtändigi, serversitigi Computeranalyse vu de Hauptvariante.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Schtell sicher, dass das Kapitel vollschtändig isch. Die Analyse chann nur eimal agforderet werde.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Alli synchronisierte Mitglider gsehnd die glich Schtellig'; + + @override + String get studyShareChanges => 'Teil Änderige mit de Zueschauer und speicher sie uf em Server'; + + @override + String get studyPlaying => 'Laufend'; + + @override + String get studyShowEvalBar => 'Bewertigs-Skala'; + + @override + String get studyFirst => 'zur erschte Site'; + + @override + String get studyPrevious => 'zrugg'; + + @override + String get studyNext => 'nächschti'; + + @override + String get studyLast => 'zur letschte Site'; + @override String get studyShareAndExport => 'Teile & exportiere'; + @override + String get studyCloneStudy => 'Klone'; + + @override + String get studyStudyPgn => 'Schtudie PGN'; + + @override + String get studyDownloadAllGames => 'All Schpiel abelade'; + + @override + String get studyChapterPgn => 'Kapitel PGN'; + + @override + String get studyCopyChapterPgn => 'PGN kopiere'; + + @override + String get studyDownloadGame => 'Das Schpiel abelade'; + + @override + String get studyStudyUrl => 'Schtudie URL'; + + @override + String get studyCurrentChapterUrl => 'URL aktuells Kapitel'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Du chasch das, zum ibinde, im Forum oder i dim Liches Tagebuech ifüege'; + + @override + String get studyStartAtInitialPosition => 'Fang ab de Grundschtellig a'; + + @override + String studyStartAtX(String param) { + return 'Fang mit $param a'; + } + + @override + String get studyEmbedInYourWebsite => 'I dini Website ibinde'; + + @override + String get studyReadMoreAboutEmbedding => 'Lies meh über s\'Ibinde'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Mer chann nur öffentlichi Schtudie ibinde!'; + + @override + String get studyOpen => 'Öffne'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1 präsentiert vu $param2'; + } + + @override + String get studyStudyNotFound => 'Schtudie nöd gfunde'; + + @override + String get studyEditChapter => 'Kapitel bearbeite'; + + @override + String get studyNewChapter => 'Neus Kapitel'; + + @override + String studyImportFromChapterX(String param) { + return 'Importiers us $param'; + } + + @override + String get studyOrientation => 'Orientierig'; + + @override + String get studyAnalysisMode => 'Analyse Modus'; + + @override + String get studyPinnedChapterComment => 'Aghänkte Kapitel Kommentar'; + + @override + String get studySaveChapter => 'Kapitel schpeichere'; + + @override + String get studyClearAnnotations => 'Amerkige lösche'; + + @override + String get studyClearVariations => 'Variante lösche'; + + @override + String get studyDeleteChapter => 'Kapitel lösche'; + + @override + String get studyDeleteThisChapter => 'Kapitel lösche? Das chann nöd rückgängig gmacht werde!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Alli Kommentär, Symbol und Zeichnigsforme i dem Kapitel lösche?'; + + @override + String get studyRightUnderTheBoard => 'Diräkt underhalb vom Brätt'; + + @override + String get studyNoPinnedComment => 'Kei'; + + @override + String get studyNormalAnalysis => 'Normali Analyse'; + + @override + String get studyHideNextMoves => 'Nögschti Züg uusbländä'; + + @override + String get studyInteractiveLesson => 'Interaktivi Üäbig'; + + @override + String studyChapterX(String param) { + return 'Kapitäl $param'; + } + + @override + String get studyEmpty => 'Läär'; + + @override + String get studyStartFromInitialPosition => 'Fang vu de Usgangsschtellig a'; + + @override + String get studyEditor => 'Ändärä'; + + @override + String get studyStartFromCustomPosition => 'Fang vunere benutzerdefinierte Schtellig a'; + + @override + String get studyLoadAGameByUrl => 'Lad es Schpiel mit ere URL'; + + @override + String get studyLoadAPositionFromFen => 'Lad e Schtellig mit ere FEN'; + + @override + String get studyLoadAGameFromPgn => 'Lad Schpiel mit eme PGN'; + + @override + String get studyAutomatic => 'Automatisch'; + + @override + String get studyUrlOfTheGame => 'URL vu de Schpiel'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Lad es Schpiel vo $param1 oder $param2'; + } + + @override + String get studyCreateChapter => 'Kapitäl ärschtelä'; + + @override + String get studyCreateStudy => 'Schtudie erschtelle'; + + @override + String get studyEditStudy => 'Schtudie bearbeite'; + + @override + String get studyVisibility => 'Sichtbarkeit'; + + @override + String get studyPublic => 'Öffentlich'; + + @override + String get studyUnlisted => 'Unglischtet'; + + @override + String get studyInviteOnly => 'Nur mit Iladig'; + + @override + String get studyAllowCloning => 'Chlone erlaube'; + + @override + String get studyNobody => 'Niemer'; + + @override + String get studyOnlyMe => 'Nur ich'; + + @override + String get studyContributors => 'Mitwirkändi'; + + @override + String get studyMembers => 'Mitglider'; + + @override + String get studyEveryone => 'Alli'; + + @override + String get studyEnableSync => 'Sync aktiviärä'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Jawoll: Glichi Schtellig für alli'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Nei: Unabhängigi Navigation für alli'; + + @override + String get studyPinnedStudyComment => 'Agheftete Schtudiekommentar'; + @override String get studyStart => 'Schtart'; + + @override + String get studySave => 'Schpeichärä'; + + @override + String get studyClearChat => 'Tschätt löschä'; + + @override + String get studyDeleteTheStudyChatHistory => 'Chatverlauf vu de Schtudie lösche? Das chann nüme rückgängig gmacht werde!'; + + @override + String get studyDeleteStudy => 'Schtudie lösche'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Die ganz Schtudie lösche? Es git keis Zrugg! Gib zur Beschtätigung de Name vu de Schtudie i: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Welli Schtudie wottsch bruche?'; + + @override + String get studyGoodMove => 'Guete Zug'; + + @override + String get studyMistake => 'Fähler'; + + @override + String get studyBrilliantMove => 'Briliantä Zug'; + + @override + String get studyBlunder => 'Grobä Patzer'; + + @override + String get studyInterestingMove => 'Intressantä Zug'; + + @override + String get studyDubiousMove => 'Frogwürdigä Zug'; + + @override + String get studyOnlyMove => 'Einzigä Zug'; + + @override + String get studyZugzwang => 'Zugzwang'; + + @override + String get studyEqualPosition => 'Usglicheni Schtellig'; + + @override + String get studyUnclearPosition => 'Unklari Schtellig'; + + @override + String get studyWhiteIsSlightlyBetter => 'Wiss schtaht es bitzli besser'; + + @override + String get studyBlackIsSlightlyBetter => 'Schwarz schtaht es bitzli besser'; + + @override + String get studyWhiteIsBetter => 'Wiss schtaht besser'; + + @override + String get studyBlackIsBetter => 'Schwarz schtoht besser'; + + @override + String get studyWhiteIsWinning => 'Wiss schtaht uf Gwünn'; + + @override + String get studyBlackIsWinning => 'Schwarz schtoht uf Gwünn'; + + @override + String get studyNovelty => 'Neuerig'; + + @override + String get studyDevelopment => 'Entwicklig'; + + @override + String get studyInitiative => 'Initiativä'; + + @override + String get studyAttack => 'Agriff'; + + @override + String get studyCounterplay => 'Gägeschpiel'; + + @override + String get studyTimeTrouble => 'Zitnot'; + + @override + String get studyWithCompensation => 'Mit Kompänsation'; + + @override + String get studyWithTheIdea => 'Mit dä Idee'; + + @override + String get studyNextChapter => 'Nögschts Kapitäl'; + + @override + String get studyPrevChapter => 'Vorhärigs Kapitäl'; + + @override + String get studyStudyActions => 'Lärnaktionä'; + + @override + String get studyTopics => 'Theme'; + + @override + String get studyMyTopics => 'Mini Theme'; + + @override + String get studyPopularTopics => 'Beliebti Theme'; + + @override + String get studyManageTopics => 'Theme verwalte'; + + @override + String get studyBack => 'Zrugg'; + + @override + String get studyPlayAgain => 'Vo vornä'; + + @override + String get studyWhatWouldYouPlay => 'Was würdisch du ih derä Stellig spiele?'; + + @override + String get studyYouCompletedThisLesson => 'Gratulation! Du häsch die Lektion abgschlosse.'; + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Kapitäl', + one: '$count Kapitel', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Schpiel', + one: '$count Schpiel', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Mitglider', + one: '$count Mitglid', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Füeg din PGN Tegscht da i, bis zu $count Schpiel', + one: 'Füeg din PGN Tegscht da i, bis zu $count Schpiel', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_he.dart b/lib/l10n/l10n_he.dart index 03f10af8d1..86118c87c4 100644 --- a/lib/l10n/l10n_he.dart +++ b/lib/l10n/l10n_he.dart @@ -103,9 +103,6 @@ class AppLocalizationsHe extends AppLocalizations { @override String get mobileCancelTakebackOffer => 'ביטול ההצעה להחזיר את המהלך האחרון'; - @override - String get mobileCancelDrawOffer => 'ביטול הצעת התיקו'; - @override String get mobileWaitingForOpponentToJoin => 'ממתין שיריב יצטרף...'; @@ -142,13 +139,13 @@ class AppLocalizationsHe extends AppLocalizations { String get mobileGreetingWithoutName => 'שלום'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobilePrefMagnifyDraggedPiece => 'הגדלת הכלי הנגרר'; @override String get activityActivity => 'פעילות'; @override - String get activityHostedALiveStream => 'עלה (או עלתה) לשידור חי'; + String get activityHostedALiveStream => 'על\\תה לשידור חי'; @override String activityRankedInSwissTournament(String param1, String param2) { @@ -166,7 +163,7 @@ class AppLocalizationsHe extends AppLocalizations { other: 'תמכ/ה בליצ\'ס במשך $count חודשים כ$param2', many: 'תמכ/ה בליצ\'ס במשך $count חודשים כ$param2', two: 'תמכ/ה בליצ\'ס במשך $count חודשים כ$param2', - one: 'תמכ/ה בליצ\'ס במשך חודש $count כ$param2', + one: 'תמכ/ה ב-lichess במשך חודש $count כ$param2', ); return '$_temp0'; } @@ -262,6 +259,19 @@ class AppLocalizationsHe extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'השלים/ה $count משחקי התכתבות מסוג $param2', + many: 'השלים/ה $count משחקי התכתבות מסוג $param2', + two: 'השלים/ה $count משחקי התכתבות מסוג $param2', + one: 'השלים/ה משחק התכתבות $count מסוג $param2', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -382,9 +392,228 @@ class AppLocalizationsHe extends AppLocalizations { @override String get broadcastBroadcasts => 'הקרנות'; + @override + String get broadcastMyBroadcasts => 'ההקרנות שלי'; + @override String get broadcastLiveBroadcasts => 'צפייה ישירה בטורנירים'; + @override + String get broadcastBroadcastCalendar => 'לוח השידורים'; + + @override + String get broadcastNewBroadcast => 'הקרנה ישירה חדשה'; + + @override + String get broadcastSubscribedBroadcasts => 'הקרנות שנרשמת אליהן'; + + @override + String get broadcastAboutBroadcasts => 'הסבר על הקרנות'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'איך להשתמש בהקרנות ב־Lichess.'; + + @override + String get broadcastTheNewRoundHelp => 'הסבב החדש יכלול את אותם התורמים והחברים כמו בסבב הקודם.'; + + @override + String get broadcastAddRound => 'הוספת סבב'; + + @override + String get broadcastOngoing => 'כרגע'; + + @override + String get broadcastUpcoming => 'בקרוב'; + + @override + String get broadcastCompleted => 'שהושלמו'; + + @override + String get broadcastCompletedHelp => 'Lichess מאתר מתי הושלם הסבב על פי המשחקים שבקישור למהלכים בשידור חי (המקור). הפעילו את האפשרות הזאת אם אין מקור שממנו נשאבים המשחקים.'; + + @override + String get broadcastRoundName => 'שם סבב'; + + @override + String get broadcastRoundNumber => 'מספר סבב'; + + @override + String get broadcastTournamentName => 'שם הטורניר'; + + @override + String get broadcastTournamentDescription => 'תיאור הטורניר בקצרה'; + + @override + String get broadcastFullDescription => 'תיאור מלא של הטורניר'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'תיאור מפורט של הטורניר (אופציונאלי). $param1 זמין. אורך התיאור לא יעלה על $param2 תווים.'; + } + + @override + String get broadcastSourceSingleUrl => 'קישור המקור של ה־PGN'; + + @override + String get broadcastSourceUrlHelp => 'הקישור ש־Lichess יבדוק כדי לקלוט עדכונים ב־PGN. הוא חייב להיות פומבי ונגיש דרך האינטרנט.'; + + @override + String get broadcastSourceGameIds => 'עד 64 מזהי משחק של Lichess, מופרדים ברווחים.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'שעת ההתחלה באזור הזמן המקומי של הטורניר: $param'; + } + + @override + String get broadcastStartDateHelp => 'אופציונאלי, אם את/ה יודע/ת מתי האירוע צפוי להתחיל'; + + @override + String get broadcastCurrentGameUrl => 'הקישור למשחק הנוכחי'; + + @override + String get broadcastDownloadAllRounds => 'הורדת כל הסבבים'; + + @override + String get broadcastResetRound => 'אפס את הסיבוב הזה'; + + @override + String get broadcastDeleteRound => 'מחיקת הסבב הזה'; + + @override + String get broadcastDefinitivelyDeleteRound => 'מחיקת הסבב הזה והמשחקים שבו לצמיתות'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'מחיקת כל המשחקים בסבב הזה. כדי ליצור אותם מחדש, קישור המקור צריך להיות פעיל.'; + + @override + String get broadcastEditRoundStudy => 'עריכת לוח הלמידה של הסבב'; + + @override + String get broadcastDeleteTournament => 'מחיקת הטורניר הזה'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'מחיקה לצמיתות של הטורניר הזה, על כל סבביו והמשחקים שבו.'; + + @override + String get broadcastShowScores => 'הצגת הניקוד של השחקנים בהתבסס על תוצאות המשחקים'; + + @override + String get broadcastReplacePlayerTags => 'אופציונאלי: החלפה של שמות השחקנים, דירוגיהם ותאריהם'; + + @override + String get broadcastFideFederations => 'איגודי FIDE'; + + @override + String get broadcastTop10Rating => 'דירוג עשרת המובילים'; + + @override + String get broadcastFidePlayers => 'שחקני FIDE'; + + @override + String get broadcastFidePlayerNotFound => 'לא נמצא שחקן FIDE'; + + @override + String get broadcastFideProfile => 'פרופיל FIDE'; + + @override + String get broadcastFederation => 'איגוד'; + + @override + String get broadcastAgeThisYear => 'גיל השנה'; + + @override + String get broadcastUnrated => 'לא מדורג'; + + @override + String get broadcastRecentTournaments => 'טורנירים אחרונים'; + + @override + String get broadcastOpenLichess => 'פתיחה ב־Lichess'; + + @override + String get broadcastTeams => 'קבוצות'; + + @override + String get broadcastBoards => 'לוחות'; + + @override + String get broadcastOverview => 'מידע כללי'; + + @override + String get broadcastSubscribeTitle => 'הירשמו כדי לקבל התראה בתחילת כל סבב. ניתן להפעיל או לבטל התראות קופצות או התראות ״פעמון״ בהגדרות החשבון שלך.'; + + @override + String get broadcastUploadImage => 'העלאת תמונה עבור הטורניר'; + + @override + String get broadcastNoBoardsYet => 'אין עדיין לוחות. הם יופיעו כשיעלו המשחקים.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'ניתן להעלות לוחות באמצעות קישור מקור או דרך $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'מתחיל אחרי $param'; + } + + @override + String get broadcastStartVerySoon => 'ההקרנה תחל ממש בקרוב.'; + + @override + String get broadcastNotYetStarted => 'ההקרנה טרם החלה.'; + + @override + String get broadcastOfficialWebsite => 'האתר הרשמי'; + + @override + String get broadcastStandings => 'תוצאות'; + + @override + String broadcastIframeHelp(String param) { + return 'ישנן אפשרויות נוספות ב$param'; + } + + @override + String get broadcastWebmastersPage => 'עמוד המתכנתים'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'קישור ל־PGN פומבי המתעדכן בשידור חי. אנו מציעים גם $param לסנכרון מיטבי ומהיר.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'הטמעת ההקרנה באתר האינטרנט שלך'; + + @override + String broadcastEmbedThisRound(String param) { + return 'הטמעת $param באתר האינטרנט שלך'; + } + + @override + String get broadcastRatingDiff => 'הפרש הדירוג'; + + @override + String get broadcastGamesThisTournament => 'משחקים בטורניר זה'; + + @override + String get broadcastScore => 'ניקוד'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count הקרנות', + many: '$count הקרנות', + two: '$count הקרנות', + one: 'הקרנה $count', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'הזמנות למשחק: $param1'; @@ -1434,10 +1663,10 @@ class AppLocalizationsHe extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'היריב מוגבל במסעים שביכולתו לבצע, וכל אחד מחמיר את מצבו.'; @override - String get puzzleThemeHealthyMix => 'שילוב בריא'; + String get puzzleThemeMix => 'שילוב בריא'; @override - String get puzzleThemeHealthyMixDescription => 'קצת מהכל. לא תדעו למה לצפות. עליכם להיות מוכנים להכל! בדיוק כמו משחקים אמיתיים.'; + String get puzzleThemeMixDescription => 'קצת מהכול. לא תדעו למה לצפות. עליכם להיות מוכנים להכול! בדיוק כמו במשחקים אמיתיים.'; @override String get puzzleThemePlayerGames => 'המשחקים שלי'; @@ -1466,7 +1695,7 @@ class AppLocalizationsHe extends AppLocalizations { String get settingsClosingIsDefinitive => 'הסגירה היא סופית. אין דרך חזרה. האם את/ה בטוח/ה?'; @override - String get settingsCantOpenSimilarAccount => 'לא תוכל/י לפתוח חשבון חדש עם אותו השם, אפילו בשינוי אותיות קטנות לגדולות ולהיפך.'; + String get settingsCantOpenSimilarAccount => 'לא תוכל/י לפתוח חשבון חדש עם אותו השם, אפילו בשינוי אותיות קטנות לגדולות והפוך. '; @override String get settingsChangedMindDoNotCloseAccount => 'שיניתי את דעתי, אל תסגרו את החשבון שלי'; @@ -1841,9 +2070,6 @@ class AppLocalizationsHe extends AppLocalizations { @override String get removesTheDepthLimit => 'מסיר את מגבלת העומק ו\"מחמם\" את המחשב'; - @override - String get engineManager => 'מנהל המנועים'; - @override String get blunder => 'טעות גסה'; @@ -2018,7 +2244,7 @@ class AppLocalizationsHe extends AppLocalizations { String get youAreLeavingLichess => 'את/ה עוזב/ת את Lichess'; @override - String get neverTypeYourPassword => 'לעולם אל תקלידו את סיסמתכם ב־Lichessבאף אתר אחר!'; + String get neverTypeYourPassword => 'לעולם אל תקלידו את סיסמתכם ב־Lichess באף אתר אחר!'; @override String proceedToX(String param) { @@ -2107,6 +2333,9 @@ class AppLocalizationsHe extends AppLocalizations { @override String get gamesPlayed => 'משחקים בטורניר'; + @override + String get ok => 'אוקיי'; + @override String get cancel => 'ביטול'; @@ -2252,7 +2481,7 @@ class AppLocalizationsHe extends AppLocalizations { String get ratingRange => 'טווח דירוג'; @override - String get thisAccountViolatedTos => 'החשבון הזה הפר את תנאי השימוש של ליצ\'ס'; + String get thisAccountViolatedTos => 'החשבון הזה הפר את תנאי השימוש של Lichess'; @override String get openingExplorerAndTablebase => 'סייר הפתיחות וטבלאות סיומים'; @@ -2652,7 +2881,7 @@ class AppLocalizationsHe extends AppLocalizations { @override String reportXToModerators(String param) { - return 'דווח/י על $param למנהלים'; + return 'דווח על $param למנהלים'; } @override @@ -2711,7 +2940,7 @@ class AppLocalizationsHe extends AppLocalizations { String get clearSavedMoves => 'הסרת המהלכים'; @override - String get previouslyOnLichessTV => 'לאחרונה בטלוויזיה של ליצ\'ס'; + String get previouslyOnLichessTV => 'לאחרונה בטלוויזיה של Lichess'; @override String get onlinePlayers => 'שחקנים מחוברים'; @@ -2795,10 +3024,10 @@ class AppLocalizationsHe extends AppLocalizations { String get createTheTopic => 'צור אשכול'; @override - String get reportAUser => 'דווח/י על משתמש/ת'; + String get reportAUser => 'דיווח על משתמש/ת'; @override - String get user => 'משתמש/ת'; + String get user => 'משתמש'; @override String get reason => 'סיבה'; @@ -2816,7 +3045,13 @@ class AppLocalizationsHe extends AppLocalizations { String get other => 'אחר'; @override - String get reportDescriptionHelp => 'הדביקו את הקישור למשחק(ים) והסבירו מה לא בסדר בהתנהגות המשתמש. אל תכתבו סתם ״השחקן/ית מרמה״, הסבירו לנו כיצד הגעתם למסקנה הזו. הדיווח יטופל מהר יותר אם ייכתב באנגלית.'; + String get reportCheatBoostHelp => 'הדביקו את הקישור למשחקים שעליהם תרצו לדווח והסבירו מה הבעיה בהתנהגות המשתמש כפי שהיא משתקפת בהם. אל תכתבו סתם ״השחקן מרמה״. הסבירו לנו כיצד הגעתם למסקנה הזו.'; + + @override + String get reportUsernameHelp => 'הסבירו מה פוגעני בשם המשתמש הזה. אל תכתבו סתם ״שם המשתמש פוגעני״. הסבירו לנו כיצד הגעתם למסקנה הזו, במיוחד אם ההעלבה מוסווית, בשפה זרה (שאינה אנגלית), בלשון סלנג או תלויית תרבות והיסטוריה.'; + + @override + String get reportProcessedFasterInEnglish => 'הדיווח שלך יטופל מהר יותר אם ייכתב באנגלית.'; @override String get error_provideOneCheatedGameLink => 'בבקשה לספק לפחות קישור אחד למשחק עם רמאות.'; @@ -2987,7 +3222,7 @@ class AppLocalizationsHe extends AppLocalizations { String get opponent => 'יריב'; @override - String get learnMenu => 'למד/י'; + String get learnMenu => 'למדו'; @override String get studyMenu => 'לוחות למידה'; @@ -3193,7 +3428,7 @@ class AppLocalizationsHe extends AppLocalizations { String get simulHostExtraTimePerPlayer => 'זמן נוסף למארח/ת עבור כל שחקן/ית שמצטרף/ת'; @override - String get lichessTournaments => 'טורנירים של ליצ\'ס'; + String get lichessTournaments => 'טורנירים של Lichess'; @override String get tournamentFAQ => 'שאלות נפוצות לגבי טורנירי הזירה'; @@ -3349,7 +3584,7 @@ class AppLocalizationsHe extends AppLocalizations { } @override - String get networkLagBetweenYouAndLichess => 'עיכוב הרשת בינך לבין ליצ\'ס'; + String get networkLagBetweenYouAndLichess => 'עיכוב הרשת בינך לבין Lichess'; @override String get timeToProcessAMoveOnLichessServer => 'זמן לעיבוד מהלך בשרת ליצ\'ס'; @@ -3404,7 +3639,7 @@ class AppLocalizationsHe extends AppLocalizations { @override String inKidModeTheLichessLogoGetsIconX(String param) { - return 'במצב ילדים הסמל של ליצ\'ס מקבל אייקון $param, כדי שתדעו שילדיכם מוגנים.'; + return 'במצב ילדים הסמל של Lichess מקבל אייקון $param, כדי שתדעו שילדיכם מוגנים.'; } @override @@ -3796,10 +4031,10 @@ class AppLocalizationsHe extends AppLocalizations { String get bullet => 'Bullet'; @override - String get blitz => 'Blitz'; + String get blitz => 'בזק'; @override - String get rapid => 'Rapid'; + String get rapid => 'זריז'; @override String get classical => 'Classical'; @@ -4121,6 +4356,9 @@ class AppLocalizationsHe extends AppLocalizations { @override String get nothingToSeeHere => 'אין כלום להצגה כאן, בינתיים.'; + @override + String get stats => 'סטטיסטיקות'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4855,9 +5093,522 @@ class AppLocalizationsHe extends AppLocalizations { @override String get streamerLichessStreamers => 'שדרני Lichess'; + @override + String get studyPrivate => 'פרטי'; + + @override + String get studyMyStudies => 'לוחות הלמידה שלי'; + + @override + String get studyStudiesIContributeTo => 'לוחות למידה שתרמתי להם'; + + @override + String get studyMyPublicStudies => 'לוחות הלמידה הפומביים שלי'; + + @override + String get studyMyPrivateStudies => 'לוחות הלמידה הפרטיים שלי'; + + @override + String get studyMyFavoriteStudies => 'לוחות הלמידה המועדפים שלי'; + + @override + String get studyWhatAreStudies => 'מה הם לוחות למידה?'; + + @override + String get studyAllStudies => 'כל לוחות הלמידה'; + + @override + String studyStudiesCreatedByX(String param) { + return 'לוחות למידה שנוצרו על ידי $param'; + } + + @override + String get studyNoneYet => 'אין עדיין.'; + + @override + String get studyHot => 'כוכבים עולים'; + + @override + String get studyDateAddedNewest => 'תאריך הוספה (החדש ביותר)'; + + @override + String get studyDateAddedOldest => 'תאריך הוספה (הישן ביותר)'; + + @override + String get studyRecentlyUpdated => 'עודכן לאחרונה'; + + @override + String get studyMostPopular => 'הכי פופולריים'; + + @override + String get studyAlphabetical => 'בסדר האלפבית'; + + @override + String get studyAddNewChapter => 'הוסיפו פרק חדש'; + + @override + String get studyAddMembers => 'הוספת משתמשים'; + + @override + String get studyInviteToTheStudy => 'הזמינו ללוח הלמידה'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'אנא הזמינו רק שחקנים שאתם מכירים המעוניינים להצטרף ללוח הלמידה הזה.'; + + @override + String get studySearchByUsername => 'חיפוש לפי שם משתמש'; + + @override + String get studySpectator => 'צופה'; + + @override + String get studyContributor => 'תורם'; + + @override + String get studyKick => 'הסרה'; + + @override + String get studyLeaveTheStudy => 'צא/י מלוח הלמידה'; + + @override + String get studyYouAreNowAContributor => 'כעת את/ה תורם/ת'; + + @override + String get studyYouAreNowASpectator => 'כעת את/ת צופה'; + + @override + String get studyPgnTags => 'תוויות PGN'; + + @override + String get studyLike => 'אהבתי'; + + @override + String get studyUnlike => 'ביטול \"אהבתי\"'; + + @override + String get studyNewTag => 'תג חדש'; + + @override + String get studyCommentThisPosition => 'הערה לגבי העמדה'; + + @override + String get studyCommentThisMove => 'הערה לגבי המסע'; + + @override + String get studyAnnotateWithGlyphs => 'השתמשו בסימנים מוסכמים כדי להגיב על מהלכים'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'פרק זה קצר מכדי להצדיק ניתוח.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'רק תורמי לוח הלמידה יכולים לבקש ניתוח ממוחשב.'; + + @override + String get studyGetAFullComputerAnalysis => 'קבל/י ניתוח צד־שרת מלא של המסעים העיקריים (mainline).'; + + @override + String get studyMakeSureTheChapterIsComplete => 'ניתן לבקש ניתוח ממוחשב רק פעם אחת, ולכן ודאו שהפרק הושלם.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'כולם צופים באותה העמדה'; + + @override + String get studyShareChanges => 'שתפו שינויים עם הצופים ושמרו אותם על השרת'; + + @override + String get studyPlaying => 'מתקיים כעת'; + + @override + String get studyShowEvalBar => 'מדי הערכה'; + + @override + String get studyFirst => 'ראשון'; + + @override + String get studyPrevious => 'הקודם'; + + @override + String get studyNext => 'הבא'; + + @override + String get studyLast => 'אחרון'; + @override String get studyShareAndExport => 'שיתוף & ייצוא'; + @override + String get studyCloneStudy => 'שכפול'; + + @override + String get studyStudyPgn => 'ה-PGN של לוח הלמידה'; + + @override + String get studyDownloadAllGames => 'הורדת כל המשחקים'; + + @override + String get studyChapterPgn => 'ה-PGN של הפרק'; + + @override + String get studyCopyChapterPgn => 'העתקת ה־PGN'; + + @override + String get studyDownloadGame => 'הורדת המשחק'; + + @override + String get studyStudyUrl => 'כתובת לוח הלמידה'; + + @override + String get studyCurrentChapterUrl => 'כתובת האינטרנט של הפרק הנוכחי'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'את/ה יכול/ה לפרסם את זה בפורום כדי להטמיע'; + + @override + String get studyStartAtInitialPosition => 'התחילו בעמדת הפתיחה'; + + @override + String studyStartAtX(String param) { + return 'התחילו ב$param'; + } + + @override + String get studyEmbedInYourWebsite => 'הטמעה באתר שלך'; + + @override + String get studyReadMoreAboutEmbedding => 'קראו עוד על הטמעה'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'ניתן להטמיע אך ורק לוחות למידה פומביים!'; + + @override + String get studyOpen => 'פתח'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1, מוגש על ידי $param2'; + } + + @override + String get studyStudyNotFound => 'לוח הלמידה לא נמצא'; + + @override + String get studyEditChapter => 'עריכת הפרק'; + + @override + String get studyNewChapter => 'פרק חדש'; + + @override + String studyImportFromChapterX(String param) { + return 'ייבא מתוך $param'; + } + + @override + String get studyOrientation => 'כיוון הלוח'; + + @override + String get studyAnalysisMode => 'מצב ניתוח'; + + @override + String get studyPinnedChapterComment => 'תגובה מוצמדת לפרק'; + + @override + String get studySaveChapter => 'שמור פרק'; + + @override + String get studyClearAnnotations => 'נקה הערות'; + + @override + String get studyClearVariations => 'נקה וריאציות'; + + @override + String get studyDeleteChapter => 'מחיקת הפרק'; + + @override + String get studyDeleteThisChapter => 'למחוק את הפרק? אין דרך חזרה!'; + + @override + String get studyClearAllCommentsInThisChapter => 'ניקוי כל ההערות, הרישומים והציורים בפרק זה'; + + @override + String get studyRightUnderTheBoard => 'ממש מתחת ללוח'; + + @override + String get studyNoPinnedComment => 'ללא'; + + @override + String get studyNormalAnalysis => 'ניתוח רגיל'; + + @override + String get studyHideNextMoves => 'הסתרת המסעים הבאים'; + + @override + String get studyInteractiveLesson => 'שיעור אינטראקטיבי'; + + @override + String studyChapterX(String param) { + return 'פרק $param'; + } + + @override + String get studyEmpty => 'ריק'; + + @override + String get studyStartFromInitialPosition => 'התחילו מהעמדה ההתחלתית'; + + @override + String get studyEditor => 'עורך'; + + @override + String get studyStartFromCustomPosition => 'התחילו מעמדה מותאמת אישית'; + + @override + String get studyLoadAGameByUrl => 'טען משחק ע\"י כתובת אינטרנט'; + + @override + String get studyLoadAPositionFromFen => 'טען עמדה מFEN'; + + @override + String get studyLoadAGameFromPgn => 'טען משחק מPGN'; + + @override + String get studyAutomatic => 'אוטומטי'; + + @override + String get studyUrlOfTheGame => 'כתובת אינטרנטית של משחק'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'טען משחק מ$param1 או מ$param2'; + } + + @override + String get studyCreateChapter => 'צור פרק'; + + @override + String get studyCreateStudy => 'יצירת לוח למידה'; + + @override + String get studyEditStudy => 'עריכת לוח למידה'; + + @override + String get studyVisibility => 'חשיפה'; + + @override + String get studyPublic => 'פומבי'; + + @override + String get studyUnlisted => 'באמצעות קישור'; + + @override + String get studyInviteOnly => 'מוזמנים בלבד'; + + @override + String get studyAllowCloning => 'אפשרו יצירת עותקים'; + + @override + String get studyNobody => 'אף אחד'; + + @override + String get studyOnlyMe => 'רק אני'; + + @override + String get studyContributors => 'תורמים'; + + @override + String get studyMembers => 'חברים'; + + @override + String get studyEveryone => 'כולם'; + + @override + String get studyEnableSync => 'הפעל סנכרון'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'כן: שמור את כולם באותה העמדה'; + + @override + String get studyNoLetPeopleBrowseFreely => 'לא: תן לאנשים לדפדף בחופשיות'; + + @override + String get studyPinnedStudyComment => 'תגובה מוצמדת ללוח הלמידה'; + @override String get studyStart => 'שמירה'; + + @override + String get studySave => 'שמירה'; + + @override + String get studyClearChat => 'ניקוי הצ\'אט'; + + @override + String get studyDeleteTheStudyChatHistory => 'למחוק את היסטוריית הצ\'אט של לוח הלמידה? אין דרך חזרה!'; + + @override + String get studyDeleteStudy => 'מחיקת לוח למידה'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'האם למחוק את כל לוח הלמידה? אין דרך חזרה! הקלידו את שם לוח הלמידה לאישור: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'היכן ליצור את לוח הלמידה?'; + + @override + String get studyGoodMove => 'מסע טוב'; + + @override + String get studyMistake => 'טעות'; + + @override + String get studyBrilliantMove => 'מסע מבריק'; + + @override + String get studyBlunder => 'טעות חמורה'; + + @override + String get studyInterestingMove => 'מסע מעניין'; + + @override + String get studyDubiousMove => 'מסע מפוקפק'; + + @override + String get studyOnlyMove => 'המסע היחיד'; + + @override + String get studyZugzwang => 'כפאי'; + + @override + String get studyEqualPosition => 'עמדה מאוזנת'; + + @override + String get studyUnclearPosition => 'עמדה לא ברורה'; + + @override + String get studyWhiteIsSlightlyBetter => 'יתרון קל ללבן'; + + @override + String get studyBlackIsSlightlyBetter => 'יתרון קל לשחור'; + + @override + String get studyWhiteIsBetter => 'יתרון ללבן'; + + @override + String get studyBlackIsBetter => 'יתרון לשחור'; + + @override + String get studyWhiteIsWinning => 'יתרון מכריע ללבן'; + + @override + String get studyBlackIsWinning => 'יתרון מכריע לשחור'; + + @override + String get studyNovelty => 'חידוש'; + + @override + String get studyDevelopment => 'פיתוח'; + + @override + String get studyInitiative => 'יוזמה'; + + @override + String get studyAttack => 'התקפה'; + + @override + String get studyCounterplay => 'מתקפת נגד'; + + @override + String get studyTimeTrouble => 'מצוקת זמן'; + + @override + String get studyWithCompensation => 'עם פיצוי'; + + @override + String get studyWithTheIdea => 'עם הרעיון'; + + @override + String get studyNextChapter => 'הפרק הבא'; + + @override + String get studyPrevChapter => 'הפרק הקודם'; + + @override + String get studyStudyActions => 'פעולות לוח למידה'; + + @override + String get studyTopics => 'נושאים'; + + @override + String get studyMyTopics => 'הנושאים שלי'; + + @override + String get studyPopularTopics => 'נושאים פופולריים'; + + @override + String get studyManageTopics => 'עריכת נושאים'; + + @override + String get studyBack => 'חזרה'; + + @override + String get studyPlayAgain => 'הפעל שוב'; + + @override + String get studyWhatWouldYouPlay => 'מה הייתם משחקים בעמדה הזו?'; + + @override + String get studyYouCompletedThisLesson => 'מזל טוב! סיימתם את השיעור.'; + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count פרקים', + many: '$count פרקים', + two: '$count פרקים', + one: 'פרק $count', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count משחקים', + many: '$count משחקים', + two: '$count משחקים', + one: '$count משחק', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count משתמשים', + many: '$count משתמשים', + two: '$count משתמשים', + one: 'משתמש אחד', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'הדבק את טקסט הPGN שלך כאן, עד ל$count משחקים', + many: 'הדבק את טקסט הPGN שלך כאן, עד ל$count משחקים', + two: 'הדבק את טקסט הPGN שלך כאן, עד ל$count משחקים', + one: 'הדבק את טקסט הPGN שלך כאן, עד למשחק $count', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_hi.dart b/lib/l10n/l10n_hi.dart index 338aa918c6..f058ddd5e7 100644 --- a/lib/l10n/l10n_hi.dart +++ b/lib/l10n/l10n_hi.dart @@ -103,9 +103,6 @@ class AppLocalizationsHi extends AppLocalizations { @override String get mobileCancelTakebackOffer => 'Takeback प्रस्ताव रद्द करें'; - @override - String get mobileCancelDrawOffer => 'Draw प्रस्ताव रद्द करें'; - @override String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @@ -246,6 +243,17 @@ class AppLocalizationsHi extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Completed $count $param2 correspondence games', + one: 'Completed $count $param2 correspondence game', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,226 @@ class AppLocalizationsHi extends AppLocalizations { @override String get broadcastBroadcasts => 'प्रसारण'; + @override + String get broadcastMyBroadcasts => 'मेरा प्रसारण'; + @override String get broadcastLiveBroadcasts => 'लाइव टूर्नामेंट प्रसारण'; + @override + String get broadcastBroadcastCalendar => 'Broadcast calendar'; + + @override + String get broadcastNewBroadcast => 'नया लाइव प्रसारण'; + + @override + String get broadcastSubscribedBroadcasts => 'Subscribed broadcasts'; + + @override + String get broadcastAboutBroadcasts => 'About broadcasts'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'How to use Lichess Broadcasts.'; + + @override + String get broadcastTheNewRoundHelp => 'The new round will have the same members and contributors as the previous one.'; + + @override + String get broadcastAddRound => 'एक दौर जोड़ें'; + + @override + String get broadcastOngoing => 'चल रही है'; + + @override + String get broadcastUpcoming => 'आगामी'; + + @override + String get broadcastCompleted => 'पूर्ण'; + + @override + String get broadcastCompletedHelp => 'Lichess detects round completion, but can get it wrong. Use this to set it manually.'; + + @override + String get broadcastRoundName => 'दौर का नाम'; + + @override + String get broadcastRoundNumber => 'दौर संख्या'; + + @override + String get broadcastTournamentName => 'प्रतियोगिता का नाम'; + + @override + String get broadcastTournamentDescription => 'संक्षिप्त प्रतियोगिता वर्णन'; + + @override + String get broadcastFullDescription => 'संक्षिप्त वर्णन'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'प्रसारण का वैकल्पिक लंबा विवरण. $param1 उपलब्ध है. लंबाई $param2 से कम होना चाहिए'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN Source URL'; + + @override + String get broadcastSourceUrlHelp => 'URL जो Lichess PGN अपडेट प्राप्त करने के लिए जाँच करेगा। यह सार्वजनिक रूप से इंटरनेट पर सुलभ होना चाहिए।'; + + @override + String get broadcastSourceGameIds => 'Up to 64 Lichess game IDs, separated by spaces.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Start date in the tournament local timezone: $param'; + } + + @override + String get broadcastStartDateHelp => 'वैकल्पिक, यदि आप जानना चाहते हो की प्रतिस्प्रधा कब शुरू होगी'; + + @override + String get broadcastCurrentGameUrl => 'वर्तमान अध्याय URL'; + + @override + String get broadcastDownloadAllRounds => 'सभी राउंड डाउनलोड करें'; + + @override + String get broadcastResetRound => 'इस फॉर्म को रीसेट करें'; + + @override + String get broadcastDeleteRound => 'इस राउंड को डिलीट करें'; + + @override + String get broadcastDefinitivelyDeleteRound => 'राउंड और उसके सभी गेम को निश्चित रूप से हटा दें।'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'इस दौर के सभी गेम हटाएं. उन्हें पुनः बनाने के लिए स्रोत को सक्रिय होने की आवश्यकता होगी।'; + + @override + String get broadcastEditRoundStudy => 'राउंड स्टडी संपादित करें'; + + @override + String get broadcastDeleteTournament => 'इस टूर्नामेंट को हटाएं'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'संपूर्ण टूर्नामेंट, उसके सभी राउंड और उसके सभी गेम को निश्चित रूप से हटा दें।'; + + @override + String get broadcastShowScores => 'Show players scores based on game results'; + + @override + String get broadcastReplacePlayerTags => 'Optional: replace player names, ratings and titles'; + + @override + String get broadcastFideFederations => 'FIDE federations'; + + @override + String get broadcastTop10Rating => 'Top 10 rating'; + + @override + String get broadcastFidePlayers => 'FIDE players'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE player not found'; + + @override + String get broadcastFideProfile => 'FIDE profile'; + + @override + String get broadcastFederation => 'Federation'; + + @override + String get broadcastAgeThisYear => 'Age this year'; + + @override + String get broadcastUnrated => 'Unrated'; + + @override + String get broadcastRecentTournaments => 'Recent tournaments'; + + @override + String get broadcastOpenLichess => 'Open in Lichess'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Overview'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Official website'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'Score'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count प्रसारण', + one: '$count प्रसारण', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Challenges: $param1'; @@ -1388,10 +1613,10 @@ class AppLocalizationsHi extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'प्रतिद्वंद्वी उन चालों में सीमित है जो वे कर सकते हैं, और सभी चालें उनकी स्थिति को खराब करती हैं।'; @override - String get puzzleThemeHealthyMix => 'स्वस्थ मिश्रण'; + String get puzzleThemeMix => 'स्वस्थ मिश्रण'; @override - String get puzzleThemeHealthyMixDescription => 'सब का कुछ कुछ। आप नहीं जानते कि क्या उम्मीद है, इसलिए आप किसी भी चीज़ के लिए तैयार रहें! बिल्कुल असली खेल की तरह।'; + String get puzzleThemeMixDescription => 'सब का कुछ कुछ। आप नहीं जानते कि क्या उम्मीद है, इसलिए आप किसी भी चीज़ के लिए तैयार रहें! बिल्कुल असली खेल की तरह।'; @override String get puzzleThemePlayerGames => 'खिलाड़ियों के खेल'; @@ -1795,9 +2020,6 @@ class AppLocalizationsHi extends AppLocalizations { @override String get removesTheDepthLimit => 'गहराई सीमा को निकालता है, और आपके कंप्यूटर को गर्म रखता है'; - @override - String get engineManager => 'इंजन प्रबंधक'; - @override String get blunder => 'भयंकर गलती'; @@ -2061,6 +2283,9 @@ class AppLocalizationsHi extends AppLocalizations { @override String get gamesPlayed => 'खेले हुए खेल'; + @override + String get ok => 'OK'; + @override String get cancel => 'रद्द करें'; @@ -2770,7 +2995,13 @@ class AppLocalizationsHi extends AppLocalizations { String get other => 'दूसरा'; @override - String get reportDescriptionHelp => 'खेल/खेलों के लिंक को लगाएं (paste) और बताएँ की यूज़र के व्यवहार में क्या खराबी है|'; + String get reportCheatBoostHelp => 'Paste the link to the game(s) and explain what is wrong about this user\'s behaviour. Don\'t just say \"they cheat\", but tell us how you came to this conclusion.'; + + @override + String get reportUsernameHelp => 'Explain what about this username is offensive. Don\'t just say \"it\'s offensive/inappropriate\", but tell us how you came to this conclusion, especially if the insult is obfuscated, not in english, is in slang, or is a historical/cultural reference.'; + + @override + String get reportProcessedFasterInEnglish => 'Your report will be processed faster if written in English.'; @override String get error_provideOneCheatedGameLink => 'कृपया ठगे गए खेल के लिए कम से कम एक लिंक प्रदान करें।'; @@ -4073,7 +4304,10 @@ class AppLocalizationsHi extends AppLocalizations { String get lichessPatronInfo => 'Lichess एक चैरिटी और पूरी तरह से फ्री/लिबर ओपन सोर्स सॉफ्टवेयर है।\nसभी परिचालन लागत, विकास और सामग्री पूरी तरह से उपयोगकर्ता दान द्वारा वित्त पोषित हैं।'; @override - String get nothingToSeeHere => 'Nothing to see here at the moment.'; + String get nothingToSeeHere => 'इस समय यहां देखने को कुछ भी नहीं है।'; + + @override + String get stats => 'Stats'; @override String opponentLeftCounter(int count) { @@ -4721,9 +4955,514 @@ class AppLocalizationsHi extends AppLocalizations { @override String get streamerLichessStreamers => 'लिचेस स्ट्रीमर'; + @override + String get studyPrivate => 'गोपनीय'; + + @override + String get studyMyStudies => 'मेरे अध्ययन'; + + @override + String get studyStudiesIContributeTo => 'मेरे योगदान वाले अध्ययन'; + + @override + String get studyMyPublicStudies => 'मेरे सार्वजनिक अध्ययन'; + + @override + String get studyMyPrivateStudies => 'मेरे निजी अध्ययन'; + + @override + String get studyMyFavoriteStudies => 'मेरे पसंदीदा अध्ययन'; + + @override + String get studyWhatAreStudies => 'अध्ययन सामग्री क्या है'; + + @override + String get studyAllStudies => 'सभी अध्ययन'; + + @override + String studyStudiesCreatedByX(String param) { + return '$param द्वारा बनाए गए अध्ययन'; + } + + @override + String get studyNoneYet => 'अभी तक नहीं।'; + + @override + String get studyHot => 'लोकप्रिय'; + + @override + String get studyDateAddedNewest => 'जोड़ा गया (नवीनतम)'; + + @override + String get studyDateAddedOldest => 'जोड़ा गया (सबसे पुराना)'; + + @override + String get studyRecentlyUpdated => 'हाल ही में अद्यतित'; + + @override + String get studyMostPopular => 'सबसे लोकप्रिय'; + + @override + String get studyAlphabetical => 'वर्णक्रमानुसार'; + + @override + String get studyAddNewChapter => 'एक नया अध्याय जोड़ें'; + + @override + String get studyAddMembers => 'सदस्य जोड़ें'; + + @override + String get studyInviteToTheStudy => 'अध्ययन के लिए आमंत्रित करें'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'कृपया केवल उन लोगों को आमंत्रित करें जिन्हें आप जानते हैं, और जो इस अध्ययन में सक्रिय रूप से शामिल होना चाहते हैं।'; + + @override + String get studySearchByUsername => 'यूज़रनेम से खोजें'; + + @override + String get studySpectator => 'दर्शक'; + + @override + String get studyContributor => 'योगदानकर्ता'; + + @override + String get studyKick => 'बाहर निकालें'; + + @override + String get studyLeaveTheStudy => 'अध्ययन छोड़े'; + + @override + String get studyYouAreNowAContributor => 'अब आप एक योगदानकर्ता हैं'; + + @override + String get studyYouAreNowASpectator => 'अब आप एक दर्शक हैं'; + + @override + String get studyPgnTags => 'PGN टैग'; + + @override + String get studyLike => 'लाइक'; + + @override + String get studyUnlike => 'नापसन्द करे'; + + @override + String get studyNewTag => 'नया टैग'; + + @override + String get studyCommentThisPosition => 'इस स्थिति पर टिप्पणी करें'; + + @override + String get studyCommentThisMove => 'इस चाल पर टिप्पणी करें'; + + @override + String get studyAnnotateWithGlyphs => 'प्रतीक के साथ टिप्पणी करें'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'यह अध्याय विश्लेषण के लिए बहुत छोटा है'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'केवल अध्ययन योगदानकर्ता ही कंप्यूटर विश्लेषण का अनुरोध कर सकते हैं।'; + + @override + String get studyGetAFullComputerAnalysis => 'मेनलाइन का पूर्ण सर्वर-साइड कंप्यूटर विश्लेषण प्राप्त करें।'; + + @override + String get studyMakeSureTheChapterIsComplete => 'सुनिश्चित करें कि अध्याय पूरा हो गया है। आप केवल एक बार विश्लेषण का अनुरोध कर सकते हैं'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'सभी SYNC सदस्य एक ही स्थिति पर रहेंगे'; + + @override + String get studyShareChanges => 'दर्शकों के साथ परिवर्तन साझा करें और उन्हें सर्वर पर सहेजें'; + + @override + String get studyPlaying => 'वर्तमान खेल'; + + @override + String get studyShowEvalBar => 'Evaluation bars'; + + @override + String get studyFirst => 'प्रथम'; + + @override + String get studyPrevious => 'पिछला'; + + @override + String get studyNext => 'अगला'; + + @override + String get studyLast => 'अंतिम'; + @override String get studyShareAndExport => 'शेयर & एक्सपोर्ट करें'; + @override + String get studyCloneStudy => 'प्रतिलिपि'; + + @override + String get studyStudyPgn => 'PGN का अध्ययन करें'; + + @override + String get studyDownloadAllGames => 'सभी खेल नीचे लादें'; + + @override + String get studyChapterPgn => 'अध्याय PGN'; + + @override + String get studyCopyChapterPgn => 'पीजीएन की नकल लें'; + + @override + String get studyDownloadGame => 'खेल नीचे लादें'; + + @override + String get studyStudyUrl => 'अध्ययन का URL'; + + @override + String get studyCurrentChapterUrl => 'वर्तमान अध्याय URL'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'आप अध्याय को जोड़ने के लिए इसे फ़ोरम में जोर सकते हैं'; + + @override + String get studyStartAtInitialPosition => 'प्रारंभिक स्थिति में शुरू करें'; + + @override + String studyStartAtX(String param) { + return '$param से प्रारंभ करें'; + } + + @override + String get studyEmbedInYourWebsite => 'अपनी वेबसाइट अथवा ब्लॉग पर प्रकाशित करें'; + + @override + String get studyReadMoreAboutEmbedding => 'एम्बेड करने के बारे में और पढ़ें'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'केवल सार्वजनिक अध्ययनों को एम्बेड किया जा सकता है!'; + + @override + String get studyOpen => 'खोलें'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1, $param2 द्वारा आपके लिए'; + } + + @override + String get studyStudyNotFound => 'अध्ययन नहीं मिला'; + + @override + String get studyEditChapter => 'अध्याय संपादित करें'; + + @override + String get studyNewChapter => 'नया अध्याय'; + + @override + String studyImportFromChapterX(String param) { + return '$param से आयात करें'; + } + + @override + String get studyOrientation => 'अभिविन्यास'; + + @override + String get studyAnalysisMode => 'विश्लेषण प्रणाली'; + + @override + String get studyPinnedChapterComment => 'अध्याय पर की गयी महत्वपूर्ण टिप्पणी'; + + @override + String get studySaveChapter => 'अध्याय सहेजें'; + + @override + String get studyClearAnnotations => 'टिप्पणी मिटाएँ'; + + @override + String get studyClearVariations => 'विविधताओं को मिटाये'; + + @override + String get studyDeleteChapter => 'अध्याय हटाएं'; + + @override + String get studyDeleteThisChapter => 'इस अध्याय को हटाएं? हटाने के पश्चात वापसी नहीं होगी!'; + + @override + String get studyClearAllCommentsInThisChapter => 'इस अध्याय में सभी टिप्पणियाँ, प्रतीक, और आकृतियाँ साफ़ करें?'; + + @override + String get studyRightUnderTheBoard => 'बोर्ड के ठीक नीचे'; + + @override + String get studyNoPinnedComment => 'खाली'; + + @override + String get studyNormalAnalysis => 'सामान्य विश्लेषण'; + + @override + String get studyHideNextMoves => 'अगली चालें छिपाएँ'; + + @override + String get studyInteractiveLesson => 'संवादमूलक सबक'; + + @override + String studyChapterX(String param) { + return 'अध्याय $param'; + } + + @override + String get studyEmpty => 'खाली'; + + @override + String get studyStartFromInitialPosition => 'प्रारंभिक स्थिति से शुरू करें'; + + @override + String get studyEditor => 'संपादक'; + + @override + String get studyStartFromCustomPosition => 'कृत्रिम स्थिति से शुरू करें'; + + @override + String get studyLoadAGameByUrl => 'URL द्वारा एक गेम लोड करें'; + + @override + String get studyLoadAPositionFromFen => 'FEN द्वारा स्थिति लोड करें'; + + @override + String get studyLoadAGameFromPgn => 'PGN से एक गेम लोड करें'; + + @override + String get studyAutomatic => 'स्वचालित'; + + @override + String get studyUrlOfTheGame => 'खेल का URL'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return '$param1 या $param2 से एक गेम लोड करें'; + } + + @override + String get studyCreateChapter => 'अध्याय बनाएँ'; + + @override + String get studyCreateStudy => 'अध्ययन बनाएँ'; + + @override + String get studyEditStudy => 'अध्ययन संपादित करें'; + + @override + String get studyVisibility => 'दृश्यता'; + + @override + String get studyPublic => 'सार्वजनिक'; + + @override + String get studyUnlisted => 'असूचीबद्ध'; + + @override + String get studyInviteOnly => 'केवल आमंत्रित'; + + @override + String get studyAllowCloning => 'नकल की अनुमति दें'; + + @override + String get studyNobody => 'कोई भी नहीं'; + + @override + String get studyOnlyMe => 'केवल मैं'; + + @override + String get studyContributors => 'योगदानकर्ता'; + + @override + String get studyMembers => 'सदस्य'; + + @override + String get studyEveryone => 'सभी'; + + @override + String get studyEnableSync => 'Sync चालू'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'जी हां सभी को एक ही स्थान पर रखे'; + + @override + String get studyNoLetPeopleBrowseFreely => 'नहीं सभी लोगो को अपनी इच्छा से ब्राउज करने दें'; + + @override + String get studyPinnedStudyComment => 'रुकिए पढ़िए विचार रखिए'; + @override String get studyStart => 'शुरू करिए'; + + @override + String get studySave => 'बचा कर रखिए'; + + @override + String get studyClearChat => 'बातें मिटा दे'; + + @override + String get studyDeleteTheStudyChatHistory => 'क्या इस पड़ाई से सम्बन्धित बातों को मिटा देना चाहिए? इससे पीछे जाने का कोई रास्ता शेष नहीं है!'; + + @override + String get studyDeleteStudy => 'अध्याय को मिटा दे'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'संपूर्ण अध्ययन हटाएं? वहां से कोई वापसी नहीं है! पुष्टि करने के लिए अध्ययन का नाम टाइप करें:$param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'आप इसको खा से पड़ना चाहते है'; + + @override + String get studyGoodMove => 'अच्छी चाल!'; + + @override + String get studyMistake => 'ग़लती'; + + @override + String get studyBrilliantMove => 'अद्भुत चाल​।'; + + @override + String get studyBlunder => 'भयंकर गलती'; + + @override + String get studyInterestingMove => 'दिलचस्प चाल​ |'; + + @override + String get studyDubiousMove => 'संदिग्ध चाल'; + + @override + String get studyOnlyMove => 'इकलौता चाल'; + + @override + String get studyZugzwang => 'जबरन चाल'; + + @override + String get studyEqualPosition => 'बराबर स्थिति'; + + @override + String get studyUnclearPosition => 'अस्पष्ट स्थिति'; + + @override + String get studyWhiteIsSlightlyBetter => 'सफेद थोड़ा सा बेहतर है'; + + @override + String get studyBlackIsSlightlyBetter => 'काला थोड़ा बेहतर है'; + + @override + String get studyWhiteIsBetter => 'सफेद बेहतर है!'; + + @override + String get studyBlackIsBetter => 'काला बेहतर है।'; + + @override + String get studyWhiteIsWinning => 'सफेद जीत रहा है'; + + @override + String get studyBlackIsWinning => 'काला जीत रहा है'; + + @override + String get studyNovelty => 'नवीनता'; + + @override + String get studyDevelopment => 'विकास'; + + @override + String get studyInitiative => 'पहल'; + + @override + String get studyAttack => 'आक्रमण'; + + @override + String get studyCounterplay => 'काउंटरप्ले'; + + @override + String get studyTimeTrouble => 'समय की समस्या'; + + @override + String get studyWithCompensation => 'लग मुआवजा।'; + + @override + String get studyWithTheIdea => 'विचीर के साथ।'; + + @override + String get studyNextChapter => 'अगला अध्याय।'; + + @override + String get studyPrevChapter => 'पिछला अध्याय।'; + + @override + String get studyStudyActions => 'अध्ययन क्रिया'; + + @override + String get studyTopics => 'विषय'; + + @override + String get studyMyTopics => 'मेरे विषय'; + + @override + String get studyPopularTopics => 'लोकप्रिय विषय'; + + @override + String get studyManageTopics => 'विषय प्रबंधन'; + + @override + String get studyBack => 'पीछे'; + + @override + String get studyPlayAgain => 'फिर से खेलेंगे?'; + + @override + String get studyWhatWouldYouPlay => 'आप इस स्थिति में क्या खेलेंगे?'; + + @override + String get studyYouCompletedThisLesson => 'बधाई हो! आपने यह सबक पूरा कर लिया है।'; + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count अध्याय', + one: '$count अध्याय', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count खेल', + one: '$count खेल', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count सदस्य', + one: '$count सदस्य', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'यहां अपना PGN टेक्स्ट डाले,$count खेल तक', + one: 'यहां अपना PGN टेक्स्ट डाले,$count खेल तक', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_hr.dart b/lib/l10n/l10n_hr.dart index 3270304cf5..a87cd54149 100644 --- a/lib/l10n/l10n_hr.dart +++ b/lib/l10n/l10n_hr.dart @@ -103,9 +103,6 @@ class AppLocalizationsHr extends AppLocalizations { @override String get mobileCancelTakebackOffer => 'Cancel takeback offer'; - @override - String get mobileCancelDrawOffer => 'Cancel draw offer'; - @override String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @@ -254,6 +251,17 @@ class AppLocalizationsHr extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Completed $count $param2 correspondence games', + one: 'Completed $count $param2 correspondence game', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -365,9 +373,227 @@ class AppLocalizationsHr extends AppLocalizations { @override String get broadcastBroadcasts => 'Prijenosi'; + @override + String get broadcastMyBroadcasts => 'My broadcasts'; + @override String get broadcastLiveBroadcasts => 'Prijenosi turnira uživo'; + @override + String get broadcastBroadcastCalendar => 'Broadcast calendar'; + + @override + String get broadcastNewBroadcast => 'Novi prijenos uživo'; + + @override + String get broadcastSubscribedBroadcasts => 'Subscribed broadcasts'; + + @override + String get broadcastAboutBroadcasts => 'About broadcasts'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'How to use Lichess Broadcasts.'; + + @override + String get broadcastTheNewRoundHelp => 'The new round will have the same members and contributors as the previous one.'; + + @override + String get broadcastAddRound => 'Dodajte rundu'; + + @override + String get broadcastOngoing => 'U tijeku'; + + @override + String get broadcastUpcoming => 'Nadolazi'; + + @override + String get broadcastCompleted => 'Završeno'; + + @override + String get broadcastCompletedHelp => 'Lichess detects round completion, but can get it wrong. Use this to set it manually.'; + + @override + String get broadcastRoundName => 'Ime runde'; + + @override + String get broadcastRoundNumber => 'Broj runde'; + + @override + String get broadcastTournamentName => 'Ime turnira'; + + @override + String get broadcastTournamentDescription => 'Kratak opis turnira'; + + @override + String get broadcastFullDescription => 'Potpuni opis događaja'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Neobavezni dugi opis prijenosa. $param1 je dostupno. Duljina mora biti manja od $param2 znakova.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN Source URL'; + + @override + String get broadcastSourceUrlHelp => 'Link koji će Lichess ispitavati kako bi dobio PGN ažuriranja. Mora biti javno dostupan s interneta.'; + + @override + String get broadcastSourceGameIds => 'Up to 64 Lichess game IDs, separated by spaces.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Start date in the tournament local timezone: $param'; + } + + @override + String get broadcastStartDateHelp => 'Neobavezno, ako znaš kada događaj počinje'; + + @override + String get broadcastCurrentGameUrl => 'URL trenutne igre'; + + @override + String get broadcastDownloadAllRounds => 'Preuzmite sve igre'; + + @override + String get broadcastResetRound => 'Resetiraj ovu rundu'; + + @override + String get broadcastDeleteRound => 'Izbriši ovu rundu'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Definitivno izbrišite rundu i njezine igre.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Izbriši sve igre ovog kola. Izvor mora biti aktivan kako bi ih se ponovno stvorilo.'; + + @override + String get broadcastEditRoundStudy => 'Edit round study'; + + @override + String get broadcastDeleteTournament => 'Izbriši ovaj turnir'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Definitively delete the entire tournament, all its rounds and all its games.'; + + @override + String get broadcastShowScores => 'Show players scores based on game results'; + + @override + String get broadcastReplacePlayerTags => 'Optional: replace player names, ratings and titles'; + + @override + String get broadcastFideFederations => 'FIDE federations'; + + @override + String get broadcastTop10Rating => 'Top 10 rating'; + + @override + String get broadcastFidePlayers => 'FIDE players'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE player not found'; + + @override + String get broadcastFideProfile => 'FIDE profile'; + + @override + String get broadcastFederation => 'Federation'; + + @override + String get broadcastAgeThisYear => 'Age this year'; + + @override + String get broadcastUnrated => 'Unrated'; + + @override + String get broadcastRecentTournaments => 'Recent tournaments'; + + @override + String get broadcastOpenLichess => 'Open in Lichess'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Overview'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Official website'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'Score'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count prijenosa', + few: '$count prijenosa', + one: '$count prijenos', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Challenges: $param1'; @@ -1412,10 +1638,10 @@ class AppLocalizationsHr extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Protivnik je prisiljen odigrati potez koji mu pogoršava poziciju.'; @override - String get puzzleThemeHealthyMix => 'Pomalo svega'; + String get puzzleThemeMix => 'Pomalo svega'; @override - String get puzzleThemeHealthyMixDescription => 'Kao i u pravim partijama - budi spreman i očekuj bilo što! Kombinacija svih navedenih vrsta zadataka.'; + String get puzzleThemeMixDescription => 'Kao i u pravim partijama - budi spreman i očekuj bilo što! Kombinacija svih navedenih vrsta zadataka.'; @override String get puzzleThemePlayerGames => 'Igračeve partije'; @@ -1819,9 +2045,6 @@ class AppLocalizationsHr extends AppLocalizations { @override String get removesTheDepthLimit => 'Uklanja granicu do koje računalo može analizirati, i održava tvoje računalo toplim'; - @override - String get engineManager => 'Upravitelj enginea'; - @override String get blunder => 'Gruba greška'; @@ -2085,6 +2308,9 @@ class AppLocalizationsHr extends AppLocalizations { @override String get gamesPlayed => 'Broj odigranih partija'; + @override + String get ok => 'OK'; + @override String get cancel => 'Odustani'; @@ -2794,7 +3020,13 @@ class AppLocalizationsHr extends AppLocalizations { String get other => 'Ostalo'; @override - String get reportDescriptionHelp => 'Zalijepi link na partiju/e u pitanju i objasni što nije u redu s ponašanjem korisnika. Nemoj samo reći \"varao je\", nego reci kako si došao/la do tog zaključka. Tvoja prijava bit će obrađena brže ako ju napišeš na engleskom jeziku.'; + String get reportCheatBoostHelp => 'Paste the link to the game(s) and explain what is wrong about this user\'s behaviour. Don\'t just say \"they cheat\", but tell us how you came to this conclusion.'; + + @override + String get reportUsernameHelp => 'Explain what about this username is offensive. Don\'t just say \"it\'s offensive/inappropriate\", but tell us how you came to this conclusion, especially if the insult is obfuscated, not in english, is in slang, or is a historical/cultural reference.'; + + @override + String get reportProcessedFasterInEnglish => 'Your report will be processed faster if written in English.'; @override String get error_provideOneCheatedGameLink => 'Molimo navedite barem jedan link igre u kojoj je igrač varao.'; @@ -4099,6 +4331,9 @@ class AppLocalizationsHr extends AppLocalizations { @override String get nothingToSeeHere => 'Nothing to see here at the moment.'; + @override + String get stats => 'Stats'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4787,9 +5022,518 @@ class AppLocalizationsHr extends AppLocalizations { @override String get streamerLichessStreamers => 'Lichess emiteri'; + @override + String get studyPrivate => 'Privatno'; + + @override + String get studyMyStudies => 'Moje studije'; + + @override + String get studyStudiesIContributeTo => 'Studije kojima pridonosim'; + + @override + String get studyMyPublicStudies => 'Moje javne studije'; + + @override + String get studyMyPrivateStudies => 'Moje privatne studije'; + + @override + String get studyMyFavoriteStudies => 'Moje omiljene studije'; + + @override + String get studyWhatAreStudies => 'Što su studije?'; + + @override + String get studyAllStudies => 'Sve studije'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Studije koje je stvorio $param'; + } + + @override + String get studyNoneYet => 'Još niti jedna.'; + + @override + String get studyHot => 'Aktualno'; + + @override + String get studyDateAddedNewest => 'Po datumu (najnovije)'; + + @override + String get studyDateAddedOldest => 'Po datumu (najstarije)'; + + @override + String get studyRecentlyUpdated => 'Nedavno objavljene'; + + @override + String get studyMostPopular => 'Najpopularnije'; + + @override + String get studyAlphabetical => 'Abecednim redom'; + + @override + String get studyAddNewChapter => 'Dodaj novo poglavlje'; + + @override + String get studyAddMembers => 'Dodaj članove'; + + @override + String get studyInviteToTheStudy => 'Pozovi na učenje'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Molimo da pozovete ljude koje znate i koji su voljni sudjelovati u ovoj studiji.'; + + @override + String get studySearchByUsername => 'Traži prema korisničkom imenu'; + + @override + String get studySpectator => 'Gledatelj'; + + @override + String get studyContributor => 'Suradnik'; + + @override + String get studyKick => 'Izbaci'; + + @override + String get studyLeaveTheStudy => 'Napusti studiju'; + + @override + String get studyYouAreNowAContributor => 'Postao si suradnik'; + + @override + String get studyYouAreNowASpectator => 'Postao si gledatelj'; + + @override + String get studyPgnTags => 'PGN oznake'; + + @override + String get studyLike => 'Sviđa mi se'; + + @override + String get studyUnlike => 'Ne sviđa mi se'; + + @override + String get studyNewTag => 'Nova oznaka'; + + @override + String get studyCommentThisPosition => 'Komentiraj ovu poziciju'; + + @override + String get studyCommentThisMove => 'Komentiraj ovaj potez'; + + @override + String get studyAnnotateWithGlyphs => 'Pribilježi glifovima'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Poglavlje je prekratko za analizu.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Samo suradnici u studiji mogu zahtijevati računalnu analizu.'; + + @override + String get studyGetAFullComputerAnalysis => 'Dobi potpunu analizu \"main-line\" od servera.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Budite sigurni da je poglavlje gotovo. Zahtjev za računalnom analizom se može dobiti samo jednom.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Svi sinkronizirani članovi ostaju na istoj poziciji'; + + @override + String get studyShareChanges => 'Podijeli promjene sa gledateljima i pohrani ih na server'; + + @override + String get studyPlaying => 'U tijeku'; + + @override + String get studyShowEvalBar => 'Evaluation bars'; + + @override + String get studyFirst => 'Prvi'; + + @override + String get studyPrevious => 'Prethodno'; + + @override + String get studyNext => 'Sljedeće'; + + @override + String get studyLast => 'Posljednja'; + @override String get studyShareAndExport => 'Podijeli & izvozi'; + @override + String get studyCloneStudy => 'Kloniraj'; + + @override + String get studyStudyPgn => 'Studiraj PGN'; + + @override + String get studyDownloadAllGames => 'Preuzmite sve igre'; + + @override + String get studyChapterPgn => 'PGN poglavlja'; + + @override + String get studyCopyChapterPgn => 'Kopiraj PGN'; + + @override + String get studyDownloadGame => 'Preuzmi igru'; + + @override + String get studyStudyUrl => 'Studiraj URL'; + + @override + String get studyCurrentChapterUrl => 'URL trenutnog poglavlja'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Možete zaljepiti ovo u forum da ugradite poglavlje'; + + @override + String get studyStartAtInitialPosition => 'Kreni s početne pozicije'; + + @override + String studyStartAtX(String param) { + return 'Započni na $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Ugradi u svoju stranicu ili blog'; + + @override + String get studyReadMoreAboutEmbedding => 'Pročitajte više o ugradnji'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Samo javne studije mogu biti uključene!'; + + @override + String get studyOpen => 'Otvori'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1 vam je donio $param2'; + } + + @override + String get studyStudyNotFound => 'Studija nije pronađena'; + + @override + String get studyEditChapter => 'Uredi poglavlje'; + + @override + String get studyNewChapter => 'Novo poglavlje'; + + @override + String studyImportFromChapterX(String param) { + return 'Unesi iz $param'; + } + + @override + String get studyOrientation => 'Orijentacija'; + + @override + String get studyAnalysisMode => 'Tip analize'; + + @override + String get studyPinnedChapterComment => 'Stalni komentar na poglavlje'; + + @override + String get studySaveChapter => 'Spremi poglavlje'; + + @override + String get studyClearAnnotations => 'Očisti pribilješke'; + + @override + String get studyClearVariations => 'Očistiti varijacije'; + + @override + String get studyDeleteChapter => 'Obriši poglavlje'; + + @override + String get studyDeleteThisChapter => 'Dali želite obrisati ovo poglavlje? Nakon ovoga nema povratka!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Želite li očistiti sve komentare, glifove i nacrtane oblike u ovom poglavlju?'; + + @override + String get studyRightUnderTheBoard => 'Točno ispod table'; + + @override + String get studyNoPinnedComment => 'Ništa'; + + @override + String get studyNormalAnalysis => 'Normalna analiza'; + + @override + String get studyHideNextMoves => 'Sakrij sljedeći potez'; + + @override + String get studyInteractiveLesson => 'Interaktivna poduka'; + + @override + String studyChapterX(String param) { + return 'Poglavlje $param'; + } + + @override + String get studyEmpty => 'Prazno'; + + @override + String get studyStartFromInitialPosition => 'Kreni s početne pozicije'; + + @override + String get studyEditor => 'Uređivač'; + + @override + String get studyStartFromCustomPosition => 'Kreni s prilagođene pozicije'; + + @override + String get studyLoadAGameByUrl => 'Učitaj igru prema URL'; + + @override + String get studyLoadAPositionFromFen => 'Učitaj poziciju od FENa'; + + @override + String get studyLoadAGameFromPgn => 'Učitaj igru od PGNa'; + + @override + String get studyAutomatic => 'Automatski'; + + @override + String get studyUrlOfTheGame => 'URL igre'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Učitaj igru sa $param1 ili $param2'; + } + + @override + String get studyCreateChapter => 'Stvori poglavlje'; + + @override + String get studyCreateStudy => 'Stvori studiju'; + + @override + String get studyEditStudy => 'Uredi studiju'; + + @override + String get studyVisibility => 'Vidljivost'; + + @override + String get studyPublic => 'Javno'; + + @override + String get studyUnlisted => 'Neizlistane'; + + @override + String get studyInviteOnly => 'Samo na poziv'; + + @override + String get studyAllowCloning => 'Dopusti kloniranje'; + + @override + String get studyNobody => 'Nitko'; + + @override + String get studyOnlyMe => 'Samo ja'; + + @override + String get studyContributors => 'Suradnici'; + + @override + String get studyMembers => 'Članovi'; + + @override + String get studyEveryone => 'Svi'; + + @override + String get studyEnableSync => 'Aktiviraj sinkronizaciju'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Da: drži sve u istoj poziciji'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Ne: neka ljudi slobodno pregledavaju'; + + @override + String get studyPinnedStudyComment => 'Stalni komentar na studije'; + @override String get studyStart => 'Start'; + + @override + String get studySave => 'Spremi'; + + @override + String get studyClearChat => 'Očistite razgovor'; + + @override + String get studyDeleteTheStudyChatHistory => 'Dali želite obrisati povijest razgovora? Nakon ovoga nema povratka!'; + + @override + String get studyDeleteStudy => 'Izbriši studiju'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Izbrisati cijelu studiju? Nema povratka! Ukucajte naziv studije da potvrdite: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Gdje želiš to studirati?'; + + @override + String get studyGoodMove => 'Dobar potez'; + + @override + String get studyMistake => 'Greška'; + + @override + String get studyBrilliantMove => 'Briljantan potez'; + + @override + String get studyBlunder => 'Gruba greška'; + + @override + String get studyInterestingMove => 'Zanimljiv potez'; + + @override + String get studyDubiousMove => 'Sumnjiv potez'; + + @override + String get studyOnlyMove => 'Jedini potez'; + + @override + String get studyZugzwang => 'Iznudica'; + + @override + String get studyEqualPosition => 'Jednaka pozicija'; + + @override + String get studyUnclearPosition => 'Nejasna pozicija'; + + @override + String get studyWhiteIsSlightlyBetter => 'Bijeli je u blagoj prednosti'; + + @override + String get studyBlackIsSlightlyBetter => 'Crni je u blagoj prednosti'; + + @override + String get studyWhiteIsBetter => 'Bijeli je bolji'; + + @override + String get studyBlackIsBetter => 'Crni je bolji'; + + @override + String get studyWhiteIsWinning => 'Bijeli dobija'; + + @override + String get studyBlackIsWinning => 'Crni dobija'; + + @override + String get studyNovelty => 'Nov potez'; + + @override + String get studyDevelopment => 'Razvoj'; + + @override + String get studyInitiative => 'Inicijativa'; + + @override + String get studyAttack => 'Napad'; + + @override + String get studyCounterplay => 'Protunapad'; + + @override + String get studyTimeTrouble => 'Vremenska nevolja'; + + @override + String get studyWithCompensation => 'S kompenzacijom'; + + @override + String get studyWithTheIdea => 'S idejom'; + + @override + String get studyNextChapter => 'Sljedeće poglavlje'; + + @override + String get studyPrevChapter => 'Prethodno poglavlje'; + + @override + String get studyStudyActions => 'Studijske radnje'; + + @override + String get studyTopics => 'Teme'; + + @override + String get studyMyTopics => 'Moje teme'; + + @override + String get studyPopularTopics => 'Popularne teme'; + + @override + String get studyManageTopics => 'Upravljaj temama'; + + @override + String get studyBack => 'Nazad'; + + @override + String get studyPlayAgain => 'Igraj ponovno'; + + @override + String get studyWhatWouldYouPlay => 'Što bi igrali u ovoj poziciji?'; + + @override + String get studyYouCompletedThisLesson => 'Čestitamo! Završili ste lekciju.'; + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Poglavlja', + few: '$count Poglavlja', + one: '$count Poglavlje', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Partije', + few: '$count Partije', + one: '$count Partija', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Članova', + few: '$count Član', + one: '$count Član', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Ovdje zalijepite svoj PGN tekst, do $count igara', + few: 'Ovdje zalijepite svoj PGN tekst, do $count igri', + one: 'Ovdje zalijepite svoj PGN tekst, do $count igre', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_hu.dart b/lib/l10n/l10n_hu.dart index 7eec643fcf..aa70650fe0 100644 --- a/lib/l10n/l10n_hu.dart +++ b/lib/l10n/l10n_hu.dart @@ -36,7 +36,7 @@ class AppLocalizationsHu extends AppLocalizations { String get mobileOkButton => 'OK'; @override - String get mobileSettingsHapticFeedback => 'Haptikus visszajelzés'; + String get mobileSettingsHapticFeedback => 'Érintésalapú visszajelzés'; @override String get mobileSettingsImmersiveMode => 'Teljes képernyős mód'; @@ -48,7 +48,7 @@ class AppLocalizationsHu extends AppLocalizations { String get mobileNotFollowingAnyUser => 'Jelenleg nem követsz senkit.'; @override - String get mobileAllGames => 'Az összes játszma'; + String get mobileAllGames => 'Összes játszma'; @override String get mobileRecentSearches => 'Keresési előzmények'; @@ -103,9 +103,6 @@ class AppLocalizationsHu extends AppLocalizations { @override String get mobileCancelTakebackOffer => 'Visszalépés kérésének visszavonása'; - @override - String get mobileCancelDrawOffer => 'Döntetlenkérés visszavonása'; - @override String get mobileWaitingForOpponentToJoin => 'Várakozás az ellenfél csatlakozására...'; @@ -125,24 +122,24 @@ class AppLocalizationsHu extends AppLocalizations { String get mobileSomethingWentWrong => 'Hiba történt.'; @override - String get mobileShowResult => 'Show result'; + String get mobileShowResult => 'Eredmény mutatása'; @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobilePuzzleThemesSubtitle => 'Oldj feladványokat kedvenc megnyitásaid kapcsán vagy válassz egy tematikát.'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobilePuzzleStormSubtitle => 'Oldd meg a lehető legtöbb feladványt 3 perc alatt.'; @override String mobileGreeting(String param) { - return 'Hello, $param'; + return 'Üdv $param!'; } @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileGreetingWithoutName => 'Üdv'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobilePrefMagnifyDraggedPiece => 'Mozdított bábu nagyítása'; @override String get activityActivity => 'Aktivitás'; @@ -246,6 +243,17 @@ class AppLocalizationsHu extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Completed $count $param2 correspondence games', + one: 'Completed $count $param2 correspondence game', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,226 @@ class AppLocalizationsHu extends AppLocalizations { @override String get broadcastBroadcasts => 'Versenyközvetítések'; + @override + String get broadcastMyBroadcasts => 'My broadcasts'; + @override String get broadcastLiveBroadcasts => 'Közvetítések élő versenyekről'; + @override + String get broadcastBroadcastCalendar => 'Broadcast calendar'; + + @override + String get broadcastNewBroadcast => 'Új élő versenyközvetítés'; + + @override + String get broadcastSubscribedBroadcasts => 'Subscribed broadcasts'; + + @override + String get broadcastAboutBroadcasts => 'About broadcasts'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'How to use Lichess Broadcasts.'; + + @override + String get broadcastTheNewRoundHelp => 'The new round will have the same members and contributors as the previous one.'; + + @override + String get broadcastAddRound => 'Forduló hozzáadása'; + + @override + String get broadcastOngoing => 'Folyamatban'; + + @override + String get broadcastUpcoming => 'Közelgő'; + + @override + String get broadcastCompleted => 'Befejeződött'; + + @override + String get broadcastCompletedHelp => 'Lichess detects round completion, but can get it wrong. Use this to set it manually.'; + + @override + String get broadcastRoundName => 'Forduló neve'; + + @override + String get broadcastRoundNumber => 'Forduló száma'; + + @override + String get broadcastTournamentName => 'Verseny neve'; + + @override + String get broadcastTournamentDescription => 'Verseny rövid leírása'; + + @override + String get broadcastFullDescription => 'Esemény teljes leírása'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Opcionális a közvetítés még bővebb leírása. $param1 használható. A hossz nem lehet több, mint $param2 karakter.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN Source URL'; + + @override + String get broadcastSourceUrlHelp => 'URL amit a Lichess időnként PGN frissítésekért ellenőriz. Ennek nyilvános internetcímnek kell lennie.'; + + @override + String get broadcastSourceGameIds => 'Up to 64 Lichess game IDs, separated by spaces.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Start date in the tournament local timezone: $param'; + } + + @override + String get broadcastStartDateHelp => 'Opcionális, ha tudod mikor kezdődik az esemény'; + + @override + String get broadcastCurrentGameUrl => 'Jelenlegi játszma URL'; + + @override + String get broadcastDownloadAllRounds => 'Összes játszma letöltése'; + + @override + String get broadcastResetRound => 'A forduló újrakezdése'; + + @override + String get broadcastDeleteRound => 'A forduló törlése'; + + @override + String get broadcastDefinitivelyDeleteRound => 'A forduló és játszmáinak végleges törlése.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Minden játék törlése ebben a fordulóban. A forrásnak aktívnak kell lennie, hogy újra létre lehessen hozni őket.'; + + @override + String get broadcastEditRoundStudy => 'Forduló tanulmányának szerkesztése'; + + @override + String get broadcastDeleteTournament => 'Verseny törlése'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Az egész verseny végleges törlése az összes fordulóval és játszmával együtt.'; + + @override + String get broadcastShowScores => 'Show players scores based on game results'; + + @override + String get broadcastReplacePlayerTags => 'Optional: replace player names, ratings and titles'; + + @override + String get broadcastFideFederations => 'FIDE federations'; + + @override + String get broadcastTop10Rating => 'Top 10 rating'; + + @override + String get broadcastFidePlayers => 'FIDE players'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE player not found'; + + @override + String get broadcastFideProfile => 'FIDE profile'; + + @override + String get broadcastFederation => 'Federation'; + + @override + String get broadcastAgeThisYear => 'Age this year'; + + @override + String get broadcastUnrated => 'Unrated'; + + @override + String get broadcastRecentTournaments => 'Recent tournaments'; + + @override + String get broadcastOpenLichess => 'Open in Lichess'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Overview'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Official website'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'Score'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count versenyközvetítés', + one: '$count versenyközvetítés', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Kihívások: $param1'; @@ -1390,10 +1615,10 @@ class AppLocalizationsHu extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Az ellenfélnek kevés lehetséges lépése van, és mind csak tovább rontja a pozícióját.'; @override - String get puzzleThemeHealthyMix => 'Vegyes mix'; + String get puzzleThemeMix => 'Vegyes mix'; @override - String get puzzleThemeHealthyMixDescription => 'Egy kicsit mindenből. Nem tudod mire számíthatsz, ezért állj készen bármire! Akár egy valódi játszmában.'; + String get puzzleThemeMixDescription => 'Egy kicsit mindenből. Nem tudod mire számíthatsz, ezért állj készen bármire! Akár egy valódi játszmában.'; @override String get puzzleThemePlayerGames => 'Felhasználók játszmái'; @@ -1797,9 +2022,6 @@ class AppLocalizationsHu extends AppLocalizations { @override String get removesTheDepthLimit => 'Feloldja a mélységi korlátot, és melegen tartja a számítógéped'; - @override - String get engineManager => 'Motor menedzser'; - @override String get blunder => 'Baklövés'; @@ -2063,6 +2285,9 @@ class AppLocalizationsHu extends AppLocalizations { @override String get gamesPlayed => 'Lejátszott játszmák'; + @override + String get ok => 'OK'; + @override String get cancel => 'Mégse'; @@ -2772,7 +2997,13 @@ class AppLocalizationsHu extends AppLocalizations { String get other => 'Egyéb'; @override - String get reportDescriptionHelp => 'Másold be a játék(ok) linkjét, és mondd el, mi a gond a játékos viselkedésével. Ne csak annyit írj, hogy \"csalt\", hanem próbáld elmondani, miből gondolod ezt. A jelentésedet hamarabb feldolgozzák, ha angolul írod.'; + String get reportCheatBoostHelp => 'Paste the link to the game(s) and explain what is wrong about this user\'s behaviour. Don\'t just say \"they cheat\", but tell us how you came to this conclusion.'; + + @override + String get reportUsernameHelp => 'Explain what about this username is offensive. Don\'t just say \"it\'s offensive/inappropriate\", but tell us how you came to this conclusion, especially if the insult is obfuscated, not in english, is in slang, or is a historical/cultural reference.'; + + @override + String get reportProcessedFasterInEnglish => 'Your report will be processed faster if written in English.'; @override String get error_provideOneCheatedGameLink => 'Kérünk, legalább adj meg linket legalább egy csalt játszmához.'; @@ -4077,6 +4308,9 @@ class AppLocalizationsHu extends AppLocalizations { @override String get nothingToSeeHere => 'Itt nincs semmi látnivaló jelenleg.'; + @override + String get stats => 'Stats'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4723,9 +4957,514 @@ class AppLocalizationsHu extends AppLocalizations { @override String get streamerLichessStreamers => 'Lichess streamerek'; + @override + String get studyPrivate => 'Privát'; + + @override + String get studyMyStudies => 'Tanulmányaim'; + + @override + String get studyStudiesIContributeTo => 'Tanulmányaim szerkesztőként'; + + @override + String get studyMyPublicStudies => 'Nyilvános tanulmányaim'; + + @override + String get studyMyPrivateStudies => 'Saját tanulmányaim'; + + @override + String get studyMyFavoriteStudies => 'Kedvenc tanulmányaim'; + + @override + String get studyWhatAreStudies => 'Mik azok a tanulmányok?'; + + @override + String get studyAllStudies => 'Összes tanulmány'; + + @override + String studyStudiesCreatedByX(String param) { + return '$param tanulmányai'; + } + + @override + String get studyNoneYet => 'Nincs még ilyen tanulmány.'; + + @override + String get studyHot => 'Felkapott'; + + @override + String get studyDateAddedNewest => 'Újabbak elöl'; + + @override + String get studyDateAddedOldest => 'Hozzáadva (legrégebbi)'; + + @override + String get studyRecentlyUpdated => 'Nemrégiben frissítve'; + + @override + String get studyMostPopular => 'Legnépszerűbb'; + + @override + String get studyAlphabetical => 'Betűrendben'; + + @override + String get studyAddNewChapter => 'Új fejezet hozzáadása'; + + @override + String get studyAddMembers => 'Tagok hozzáadása'; + + @override + String get studyInviteToTheStudy => 'Meghívás a tanulmányba'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Csak olyan ismerőst hívj meg, aki szeretne részt venni a tanulmány készítésében.'; + + @override + String get studySearchByUsername => 'Keresés felhasználónév alapján'; + + @override + String get studySpectator => 'Néző'; + + @override + String get studyContributor => 'Szerkesztő'; + + @override + String get studyKick => 'Eltávolítás'; + + @override + String get studyLeaveTheStudy => 'Tanulmány elhagyása'; + + @override + String get studyYouAreNowAContributor => 'Szerkesztő lettél'; + + @override + String get studyYouAreNowASpectator => 'Néző lettél'; + + @override + String get studyPgnTags => 'PGN címkék'; + + @override + String get studyLike => 'Kedvel'; + + @override + String get studyUnlike => 'Mégse tetszik'; + + @override + String get studyNewTag => 'Új címke'; + + @override + String get studyCommentThisPosition => 'Megjegyzés ehhez az álláshoz'; + + @override + String get studyCommentThisMove => 'Megjegyzés ehhez a lépéshez'; + + @override + String get studyAnnotateWithGlyphs => 'Lépések megjelölése'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'A fejezet túl rövid számítógépes elemzéshez.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Csak a tanulmány szerkesztői kérhetnek számítógépes elemzést.'; + + @override + String get studyGetAFullComputerAnalysis => 'Teljes szerveroldali számítógépes elemzés kérése a főváltozatról.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Ellenőrizd, hogy a fejezet elkészült-e. Csak egyszer kérhető számítógépes elemzés.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Minden szinkronizált tag ugyanazt az állást látja'; + + @override + String get studyShareChanges => 'A módosítások láthatóak a nézők számára, és mentésre kerülnek a szerveren'; + + @override + String get studyPlaying => 'Folyamatban'; + + @override + String get studyShowEvalBar => 'Evaluation bars'; + + @override + String get studyFirst => 'Első'; + + @override + String get studyPrevious => 'Előző'; + + @override + String get studyNext => 'Következő'; + + @override + String get studyLast => 'Utolsó'; + @override String get studyShareAndExport => 'Megosztás és exportálás'; + @override + String get studyCloneStudy => 'Klónozás'; + + @override + String get studyStudyPgn => 'PGN a tanulmányról'; + + @override + String get studyDownloadAllGames => 'Az összes játszma letöltése'; + + @override + String get studyChapterPgn => 'PGN a fejezetről'; + + @override + String get studyCopyChapterPgn => 'PGN másolása'; + + @override + String get studyDownloadGame => 'Játszma letöltése'; + + @override + String get studyStudyUrl => 'Tanulmány URL'; + + @override + String get studyCurrentChapterUrl => 'URL erre a fejezetre'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Ezzel a linkkel beágyazhatod a fejezetet a Lichess blogodban vagy a fórumon'; + + @override + String get studyStartAtInitialPosition => 'Kezdés a kiinduló állásból'; + + @override + String studyStartAtX(String param) { + return 'Kezdés innen: $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Beágyazás saját weboldalba'; + + @override + String get studyReadMoreAboutEmbedding => 'A beágyazásról bővebben'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Csak nyilvános tanulmányokat lehet beágyazni!'; + + @override + String get studyOpen => 'Megnyitás'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1, a $param2 jóvoltából'; + } + + @override + String get studyStudyNotFound => 'Tanulmány nem található'; + + @override + String get studyEditChapter => 'Fejezet szerkesztése'; + + @override + String get studyNewChapter => 'Új fejezet'; + + @override + String studyImportFromChapterX(String param) { + return 'Importálás innen: $param'; + } + + @override + String get studyOrientation => 'Szemszög'; + + @override + String get studyAnalysisMode => 'Elemzés típusa'; + + @override + String get studyPinnedChapterComment => 'Rögzített megjegyzés a fejezethez'; + + @override + String get studySaveChapter => 'Fejezet mentése'; + + @override + String get studyClearAnnotations => 'Megjegyzések törlése'; + + @override + String get studyClearVariations => 'Változatok törlése'; + + @override + String get studyDeleteChapter => 'Fejezet törlése'; + + @override + String get studyDeleteThisChapter => 'Törlöd a fejezetet? Ezt nem lehet visszavonni!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Minden megjegyzés, lépésjelölés és rajz törlése a fejezetből'; + + @override + String get studyRightUnderTheBoard => 'Közvetlenül a tábla alatt'; + + @override + String get studyNoPinnedComment => 'Nincs'; + + @override + String get studyNormalAnalysis => 'Normál elemzés'; + + @override + String get studyHideNextMoves => 'Következő lépések elrejtése'; + + @override + String get studyInteractiveLesson => 'Interaktív lecke'; + + @override + String studyChapterX(String param) { + return '$param. fejezet'; + } + + @override + String get studyEmpty => 'Üres'; + + @override + String get studyStartFromInitialPosition => 'Kezdés az alapállásból'; + + @override + String get studyEditor => 'Szerkesztő'; + + @override + String get studyStartFromCustomPosition => 'Kezdés tetszőleges állásból'; + + @override + String get studyLoadAGameByUrl => 'Játszmák betöltése linkkel'; + + @override + String get studyLoadAPositionFromFen => 'Állás betöltése FEN-ből'; + + @override + String get studyLoadAGameFromPgn => 'Játszmák betöltése PGN-ből'; + + @override + String get studyAutomatic => 'Automatikus'; + + @override + String get studyUrlOfTheGame => 'Játszmák linkje, soronként egy'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Játszmák betöltése $param1 vagy $param2 szerverről'; + } + + @override + String get studyCreateChapter => 'Fejezet létrehozása'; + + @override + String get studyCreateStudy => 'Tanulmány létrehozása'; + + @override + String get studyEditStudy => 'Tanulmány szerkesztése'; + + @override + String get studyVisibility => 'Láthatóság'; + + @override + String get studyPublic => 'Nyilvános'; + + @override + String get studyUnlisted => 'Nincs listázva'; + + @override + String get studyInviteOnly => 'Csak meghívással'; + + @override + String get studyAllowCloning => 'Klónozható'; + + @override + String get studyNobody => 'Senki'; + + @override + String get studyOnlyMe => 'Csak én'; + + @override + String get studyContributors => 'Szerkesztők'; + + @override + String get studyMembers => 'Tagok'; + + @override + String get studyEveryone => 'Mindenki'; + + @override + String get studyEnableSync => 'Sync engedélyezése'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Igen: mindenki ugyanazt az állást látja'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Nem: szabadon böngészhető'; + + @override + String get studyPinnedStudyComment => 'Rögzített megjegyzés a tanulmányhoz'; + @override String get studyStart => 'Mehet'; + + @override + String get studySave => 'Mentés'; + + @override + String get studyClearChat => 'Chat törlése'; + + @override + String get studyDeleteTheStudyChatHistory => 'Biztosan törlöd a chat előzményeket a tanulmányból? Ezt nem lehet visszavonni!'; + + @override + String get studyDeleteStudy => 'Tanulmány törlése'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Törlöd a teljes tanulmányt? Ezt nem lehet visszavonni! Gépeld be a tanulmány nevét a megerősítéshez: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Melyik tanulmányba kerüljön?'; + + @override + String get studyGoodMove => 'Jó lépés'; + + @override + String get studyMistake => 'Hiba'; + + @override + String get studyBrilliantMove => 'Kiváló lépés'; + + @override + String get studyBlunder => 'Durva hiba'; + + @override + String get studyInterestingMove => 'Érdekes lépés'; + + @override + String get studyDubiousMove => 'Szokatlan lépés'; + + @override + String get studyOnlyMove => 'Egyetlen lépés'; + + @override + String get studyZugzwang => 'Lépéskényszer'; + + @override + String get studyEqualPosition => 'Egyenlő állás'; + + @override + String get studyUnclearPosition => 'Zavaros állás'; + + @override + String get studyWhiteIsSlightlyBetter => 'Világos kicsit jobban áll'; + + @override + String get studyBlackIsSlightlyBetter => 'Sötét kicsit jobban áll'; + + @override + String get studyWhiteIsBetter => 'Világos jobban áll'; + + @override + String get studyBlackIsBetter => 'Sötét jobban áll'; + + @override + String get studyWhiteIsWinning => 'Világos nyerésre áll'; + + @override + String get studyBlackIsWinning => 'Sötét nyerésre áll'; + + @override + String get studyNovelty => 'Újítás'; + + @override + String get studyDevelopment => 'Fejlődés'; + + @override + String get studyInitiative => 'Kezdeményezés'; + + @override + String get studyAttack => 'Támadás'; + + @override + String get studyCounterplay => 'Ellenjáték'; + + @override + String get studyTimeTrouble => 'Időzavar'; + + @override + String get studyWithCompensation => 'Kompenzáció'; + + @override + String get studyWithTheIdea => 'Elképzelés'; + + @override + String get studyNextChapter => 'Következő fejezet'; + + @override + String get studyPrevChapter => 'Előző fejezet'; + + @override + String get studyStudyActions => 'Műveletek a tanulmányban'; + + @override + String get studyTopics => 'Témakörök'; + + @override + String get studyMyTopics => 'Témaköreim'; + + @override + String get studyPopularTopics => 'Népszerű témakörök'; + + @override + String get studyManageTopics => 'Témakörök kezelése'; + + @override + String get studyBack => 'Vissza'; + + @override + String get studyPlayAgain => 'Újra'; + + @override + String get studyWhatWouldYouPlay => 'Mit lépnél ebben az állásban?'; + + @override + String get studyYouCompletedThisLesson => 'Gratulálok! A fejezet végére értél.'; + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Fejezet', + one: '$count Fejezet', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Játszma', + one: '$count Játszma', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Tag', + one: '$count Tag', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Illeszd be a PGN szövegét (legfeljebb $count játszma)', + one: 'Illeszd be a PGN szövegét legfeljebb $count játszmáig', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_hy.dart b/lib/l10n/l10n_hy.dart index e67f7d9221..6075423a15 100644 --- a/lib/l10n/l10n_hy.dart +++ b/lib/l10n/l10n_hy.dart @@ -103,9 +103,6 @@ class AppLocalizationsHy extends AppLocalizations { @override String get mobileCancelTakebackOffer => 'Cancel takeback offer'; - @override - String get mobileCancelDrawOffer => 'Cancel draw offer'; - @override String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @@ -246,6 +243,17 @@ class AppLocalizationsHy extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Completed $count $param2 correspondence games', + one: 'Completed $count $param2 correspondence game', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,226 @@ class AppLocalizationsHy extends AppLocalizations { @override String get broadcastBroadcasts => 'Հեռարձակումներ'; + @override + String get broadcastMyBroadcasts => 'Իմ հեռարձակումները'; + @override String get broadcastLiveBroadcasts => 'Մրցաշարի ուղիղ հեռարձակումներ'; + @override + String get broadcastBroadcastCalendar => 'Broadcast calendar'; + + @override + String get broadcastNewBroadcast => 'Նոր ուղիղ հեռարձակում'; + + @override + String get broadcastSubscribedBroadcasts => 'Բաժանորդագրված հեռարձակումներ'; + + @override + String get broadcastAboutBroadcasts => 'About broadcasts'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'How to use Lichess Broadcasts.'; + + @override + String get broadcastTheNewRoundHelp => 'The new round will have the same members and contributors as the previous one.'; + + @override + String get broadcastAddRound => 'Ավելացնել խաղափուլ'; + + @override + String get broadcastOngoing => 'Ընթացիկ'; + + @override + String get broadcastUpcoming => 'Առաջիկայում սպասվող'; + + @override + String get broadcastCompleted => 'Ավարտված'; + + @override + String get broadcastCompletedHelp => 'Lichess detects round completion, but can get it wrong. Use this to set it manually.'; + + @override + String get broadcastRoundName => 'Խաղափուլի անվանում'; + + @override + String get broadcastRoundNumber => 'Խաղափուլի համար'; + + @override + String get broadcastTournamentName => 'Մրցաշարի անվանում'; + + @override + String get broadcastTournamentDescription => 'Իրադարձության համառոտ նկարագրություն'; + + @override + String get broadcastFullDescription => 'Իրադարձության ամբողջական նկարագրություն'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Optional long description of the tournament. $param1 is available. Length must be less than $param2 characters.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN Source URL'; + + @override + String get broadcastSourceUrlHelp => 'URL that Lichess will check to get PGN updates. It must be publicly accessible from the Internet.'; + + @override + String get broadcastSourceGameIds => 'Up to 64 Lichess game IDs, separated by spaces.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Start date in the tournament local timezone: $param'; + } + + @override + String get broadcastStartDateHelp => 'Լրացուցիչ, եթե գիտեք, թե երբ է սկսվելու իրադարձությունը'; + + @override + String get broadcastCurrentGameUrl => 'Ընթացիկ պարտիայի URL-հասցեն'; + + @override + String get broadcastDownloadAllRounds => 'Բեռնել բոլոր խաղափուլերը'; + + @override + String get broadcastResetRound => 'Հեռացնել այս խաղափուլը'; + + @override + String get broadcastDeleteRound => 'Հեռացնել այս խաղափուլը'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Definitively delete the round and all its games.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Delete all games of this round. The source will need to be active in order to re-create them.'; + + @override + String get broadcastEditRoundStudy => 'Խմբագրել խաղափուլի ստուդիան'; + + @override + String get broadcastDeleteTournament => 'Հեռացնել այս մրցաշարը'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Վերջնականապես հեռացնել ամբողջ մրցաշարը, նրա խաղափուլերը և պարտիաները։'; + + @override + String get broadcastShowScores => 'Show players scores based on game results'; + + @override + String get broadcastReplacePlayerTags => 'Optional: replace player names, ratings and titles'; + + @override + String get broadcastFideFederations => 'FIDE federations'; + + @override + String get broadcastTop10Rating => 'Top 10 rating'; + + @override + String get broadcastFidePlayers => 'FIDE players'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE player not found'; + + @override + String get broadcastFideProfile => 'FIDE profile'; + + @override + String get broadcastFederation => 'Federation'; + + @override + String get broadcastAgeThisYear => 'Age this year'; + + @override + String get broadcastUnrated => 'Unrated'; + + @override + String get broadcastRecentTournaments => 'Recent tournaments'; + + @override + String get broadcastOpenLichess => 'Open in Lichess'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Overview'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Official website'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'Score'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count broadcasts', + one: '$count broadcast', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Մարտահրավերներ: $param1'; @@ -1390,10 +1615,10 @@ class AppLocalizationsHy extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Մրցակիցը ստիպված է անել հնարավոր փոքրաթիվ քայլերից մեկը, բայց քայլերից ցանկացածը տանում է դիրքի վատացման։'; @override - String get puzzleThemeHealthyMix => 'Խառը խնդիրներ'; + String get puzzleThemeMix => 'Խառը խնդիրներ'; @override - String get puzzleThemeHealthyMixDescription => 'Ամեն ինչից` քիչ-քիչ։ Դուք չգիտեք` ինչ է սպասվում, այնպես որ, պատրաստ եղեք ամեն ինչի։ Ինչպես իսկական պարտիայում։'; + String get puzzleThemeMixDescription => 'Ամեն ինչից` քիչ-քիչ։ Դուք չգիտեք` ինչ է սպասվում, այնպես որ, պատրաստ եղեք ամեն ինչի։ Ինչպես իսկական պարտիայում։'; @override String get puzzleThemePlayerGames => 'Խաղացողի պարտիաները'; @@ -1797,9 +2022,6 @@ class AppLocalizationsHy extends AppLocalizations { @override String get removesTheDepthLimit => 'Վերացնում է խորության սահմանափակումը և տաք պահում ձեր համակարգիչը'; - @override - String get engineManager => 'Շարժիչի մենեջեր'; - @override String get blunder => 'Վրիպում'; @@ -2063,6 +2285,9 @@ class AppLocalizationsHy extends AppLocalizations { @override String get gamesPlayed => 'Խաղացած խաղեր'; + @override + String get ok => 'OK'; + @override String get cancel => 'Հերքել'; @@ -2772,7 +2997,13 @@ class AppLocalizationsHy extends AppLocalizations { String get other => 'այլ'; @override - String get reportDescriptionHelp => 'Կիսվեք մեզ հետ Այն խաղերի հղումներով, որտեղ կարծում եք, որ կանոնները խախտվել են և նկարագրեք, թե ինչն է սխալ: Բավական չէ պարզապես գրել \"Նա խարդախում է\", խնդրում ենք նկարագրել, թե ինչպես եք եկել այս եզրակացության: Մենք ավելի արագ կաշխատենք, եթե գրեք անգլերեն:'; + String get reportCheatBoostHelp => 'Paste the link to the game(s) and explain what is wrong about this user\'s behaviour. Don\'t just say \"they cheat\", but tell us how you came to this conclusion.'; + + @override + String get reportUsernameHelp => 'Explain what about this username is offensive. Don\'t just say \"it\'s offensive/inappropriate\", but tell us how you came to this conclusion, especially if the insult is obfuscated, not in english, is in slang, or is a historical/cultural reference.'; + + @override + String get reportProcessedFasterInEnglish => 'Your report will be processed faster if written in English.'; @override String get error_provideOneCheatedGameLink => 'Խնդրում ենք ավելացնել առնվազն մեկ խաղի հղում, որտեղ ձեր կարծիքով խախտվել են կանոնները:'; @@ -4077,6 +4308,9 @@ class AppLocalizationsHy extends AppLocalizations { @override String get nothingToSeeHere => 'Nothing to see here at the moment.'; + @override + String get stats => 'Stats'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4723,9 +4957,514 @@ class AppLocalizationsHy extends AppLocalizations { @override String get streamerLichessStreamers => 'Lichess-ի հեռարձակողներ'; + @override + String get studyPrivate => 'Անձնական'; + + @override + String get studyMyStudies => 'Իմ ստուդիաները'; + + @override + String get studyStudiesIContributeTo => 'Իմ մասնակցությամբ ստուդիաները'; + + @override + String get studyMyPublicStudies => 'Իմ հանրային ստուդիաները'; + + @override + String get studyMyPrivateStudies => 'Իմ անձնական ստուդիաները'; + + @override + String get studyMyFavoriteStudies => 'Իմ սիրելի ստուդիաները'; + + @override + String get studyWhatAreStudies => 'Ի՞նչ են «ստուդիաները»'; + + @override + String get studyAllStudies => 'Բոլոր ստուդիաները'; + + @override + String studyStudiesCreatedByX(String param) { + return '$param-ի ստեղծած ստուդիաները'; + } + + @override + String get studyNoneYet => 'Առայժմ ոչինչ։'; + + @override + String get studyHot => 'Ամենաակտիվները'; + + @override + String get studyDateAddedNewest => 'Վերջերս ավելացվածները'; + + @override + String get studyDateAddedOldest => 'Վաղուց ավելացվածները'; + + @override + String get studyRecentlyUpdated => 'Վերջերս թարմացվածները'; + + @override + String get studyMostPopular => 'Ամենահայտնիները'; + + @override + String get studyAlphabetical => 'Այբբենական կարգով'; + + @override + String get studyAddNewChapter => 'Ավելացնել նոր գլուխ'; + + @override + String get studyAddMembers => 'Ավելացնել մասնակիցների'; + + @override + String get studyInviteToTheStudy => 'Հրավիրել ստուդիա'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Հրավիրեք միայն այն մասնակիցներին, որոնց ճանաչում եք, և որոնք ակտիվորեն ցանկանում են միանալ այս ստուդիային։'; + + @override + String get studySearchByUsername => 'Որոնում ըստ մասնակցային անվան'; + + @override + String get studySpectator => 'Հանդիսատես'; + + @override + String get studyContributor => 'Խմբագիր'; + + @override + String get studyKick => 'Վռնդել'; + + @override + String get studyLeaveTheStudy => 'Լքել ստուդիան'; + + @override + String get studyYouAreNowAContributor => 'Այժմ Դուք խմբագիր եք'; + + @override + String get studyYouAreNowASpectator => 'Այժմ Դուք հանդիսական եք'; + + @override + String get studyPgnTags => 'PGN-ի թեգերը'; + + @override + String get studyLike => 'Հավանել'; + + @override + String get studyUnlike => 'Չեմ հավանում'; + + @override + String get studyNewTag => 'Նոր թեգ'; + + @override + String get studyCommentThisPosition => 'Մեկնաբանել այս դիրքը'; + + @override + String get studyCommentThisMove => 'Մեկնաբանել այս քայլը'; + + @override + String get studyAnnotateWithGlyphs => 'Ավելացնել սիմվոլներով անոտացիա'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Վերլուծության համար գլուխը չափազանց կարճ է։'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Միայն ստուդիայի խմբագիրները կարող են խնդրել համակարգչային վերլուծություն։'; + + @override + String get studyGetAFullComputerAnalysis => 'Սերվերից ստանալ գլխավոր գծի ամբողջական համակարգչային վերլուծություն։'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Համոզվեք, որ գլուխն ավարտված է։ Համակարգչային վերլուծություն կարող եք խնդրել միայն մեկ անգամ։'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Բոլոր սինքրոնիզացված մասնակիցները մնում են նույն դիրքում'; + + @override + String get studyShareChanges => 'Փոփոխությունները տարածել հանդիսականների շրջանում և դրանք պահպանել սերվերում'; + + @override + String get studyPlaying => 'Ակտիվ'; + + @override + String get studyShowEvalBar => 'Evaluation bars'; + + @override + String get studyFirst => 'Առաջինը'; + + @override + String get studyPrevious => 'Նախորդը'; + + @override + String get studyNext => 'Հաջորդը'; + + @override + String get studyLast => 'Վերջինը'; + @override String get studyShareAndExport => 'Տարածել & և արտահանել'; + @override + String get studyCloneStudy => 'Կլոնավորել'; + + @override + String get studyStudyPgn => 'Ստուդիայի PGN-ն'; + + @override + String get studyDownloadAllGames => 'Ներբեռնել բոլոր պարտիաները'; + + @override + String get studyChapterPgn => 'Գլխի PGN-ը'; + + @override + String get studyCopyChapterPgn => 'Պատճենել PGN-ը'; + + @override + String get studyDownloadGame => 'Ներբեռնել պարտիան'; + + @override + String get studyStudyUrl => 'Ստուդիայի հղումը'; + + @override + String get studyCurrentChapterUrl => 'Այս գլխի հղումը'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Ֆորումում կամ Lichess-ի բլոգում ներդնելու համար տեղադրեք այս կոդը'; + + @override + String get studyStartAtInitialPosition => 'Բացել սկզբնական դիրքում'; + + @override + String studyStartAtX(String param) { + return 'Սկսել $param-ից'; + } + + @override + String get studyEmbedInYourWebsite => 'Ներդնել սեփական կայքում կամ բլոգում'; + + @override + String get studyReadMoreAboutEmbedding => 'Մանրամասն կայքում ներդնելու մասին'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Կայքում կարելի է ներդնել միայն հրապարակային ստուդիաները։'; + + @override + String get studyOpen => 'Բացել'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1-ը $param2-ից'; + } + + @override + String get studyStudyNotFound => 'Ստուդիան չի գտնվել'; + + @override + String get studyEditChapter => 'Խմբագրել գլուխը'; + + @override + String get studyNewChapter => 'Նոր գլուխ'; + + @override + String studyImportFromChapterX(String param) { + return 'Ներդնել $param-ից'; + } + + @override + String get studyOrientation => 'Կողմնորոշում'; + + @override + String get studyAnalysisMode => 'Վերլուծության ռեժիմ'; + + @override + String get studyPinnedChapterComment => 'Գլխի ամրակցված մեկնաբանություն'; + + @override + String get studySaveChapter => 'Պահպանել գլուխը'; + + @override + String get studyClearAnnotations => 'Հեռացնել անոտացիան'; + + @override + String get studyClearVariations => 'Հեռացնել տարբերակները'; + + @override + String get studyDeleteChapter => 'Հեռացնել գլուխը'; + + @override + String get studyDeleteThisChapter => 'Հեռացնե՞լ գլուխը։ Վերականգնել հնարավոր չի լինի։'; + + @override + String get studyClearAllCommentsInThisChapter => 'Մաքրե՞լ այս գլխի բոլոր մեկնաբանություններն ու նշումները'; + + @override + String get studyRightUnderTheBoard => 'Անմիջապես տախտակի տակ'; + + @override + String get studyNoPinnedComment => 'Ոչ'; + + @override + String get studyNormalAnalysis => 'Սովորական վերլուծություն'; + + @override + String get studyHideNextMoves => 'Թաքցնել հետագա քայլերը'; + + @override + String get studyInteractiveLesson => 'Ինտերակտիվ դասընթաց'; + + @override + String studyChapterX(String param) { + return 'Գլուխ $param'; + } + + @override + String get studyEmpty => 'Դատարկ է'; + + @override + String get studyStartFromInitialPosition => 'Սկսել նախնական դիրքից'; + + @override + String get studyEditor => 'Խմբագիր'; + + @override + String get studyStartFromCustomPosition => 'Սկսել սեփական դիրքից'; + + @override + String get studyLoadAGameByUrl => 'Բեռնել պարտիան ըստ URL-ի'; + + @override + String get studyLoadAPositionFromFen => 'Բեռնել դիրքը FEN-ով'; + + @override + String get studyLoadAGameFromPgn => 'Բեռնել դիրքն ըստ PGN-ի'; + + @override + String get studyAutomatic => 'Ինքնաբերաբար'; + + @override + String get studyUrlOfTheGame => 'Պարտիայի URL-ը, մեկ տողով'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Ներբեռնել խաղը $param1-ից կամ $param2-ից'; + } + + @override + String get studyCreateChapter => 'Ստեղծել գլուխը'; + + @override + String get studyCreateStudy => 'Ստեղծել ստուդիա'; + + @override + String get studyEditStudy => 'Խմբագրել ստուդիան'; + + @override + String get studyVisibility => 'Հասանելի է դիտման համար'; + + @override + String get studyPublic => 'Հրապարակային'; + + @override + String get studyUnlisted => 'Հղումով'; + + @override + String get studyInviteOnly => 'Միայն հրավերով'; + + @override + String get studyAllowCloning => 'Թույլատրել պատճենումը'; + + @override + String get studyNobody => 'Ոչ ոք'; + + @override + String get studyOnlyMe => 'Միայն ես'; + + @override + String get studyContributors => 'Համահեղինակներ'; + + @override + String get studyMembers => 'Անդամները'; + + @override + String get studyEveryone => 'Բոլորը'; + + @override + String get studyEnableSync => 'Միացնել սինքրոնացումը'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Այո. բոլորի համար դնել միևնույն դիրքը'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Ոչ. թույլատրել մասնակիցներին ազատ ուսումնասիրել բոլոր դիրքերը'; + + @override + String get studyPinnedStudyComment => 'Ստուդիայի ամրակցված մեկնաբանություն'; + @override String get studyStart => 'Սկսել'; + + @override + String get studySave => 'Պահպանել'; + + @override + String get studyClearChat => 'Մաքրել զրուցարանը'; + + @override + String get studyDeleteTheStudyChatHistory => 'Հեռացնե՞լ ստուդիայի զրուցարանը։ Վերականգնել հնարավոր չի լինի։'; + + @override + String get studyDeleteStudy => 'Հեռացնել ստուդիան'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Հեռացնե՞լ ամբողջ ստուդիան։ Հեռացումն անդառնալի կլինի։ Հաստատելու համար մուտքագրեք ստուդիայի անվանումը՝ $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Որտե՞ղ եք ցանկանում ստեղծել ստուդիան։'; + + @override + String get studyGoodMove => 'Լավ քայլ է'; + + @override + String get studyMistake => 'Սխալ'; + + @override + String get studyBrilliantMove => 'Գերազանց քայլ է'; + + @override + String get studyBlunder => 'Վրիպում'; + + @override + String get studyInterestingMove => 'Հետաքրքիր քայլ է'; + + @override + String get studyDubiousMove => 'Կասկածելի քայլ'; + + @override + String get studyOnlyMove => 'Միակ քայլ'; + + @override + String get studyZugzwang => 'Ցուգցվանգ'; + + @override + String get studyEqualPosition => 'Հավասար դիրք'; + + @override + String get studyUnclearPosition => 'Անորոշ դիրք'; + + @override + String get studyWhiteIsSlightlyBetter => 'Սպիտակները մի քիչ լավ են'; + + @override + String get studyBlackIsSlightlyBetter => 'Սևերը մի քիչ լավ են'; + + @override + String get studyWhiteIsBetter => 'Սպիտակները լավ են'; + + @override + String get studyBlackIsBetter => 'Սևերը լավ են'; + + @override + String get studyWhiteIsWinning => 'Սպիտակները հաղթում են'; + + @override + String get studyBlackIsWinning => 'Սևերը հաղթում են'; + + @override + String get studyNovelty => 'Նորույթ'; + + @override + String get studyDevelopment => 'Զարգացում'; + + @override + String get studyInitiative => 'Նախաձեռնություն'; + + @override + String get studyAttack => 'Գրոհ'; + + @override + String get studyCounterplay => 'Հակախաղ'; + + @override + String get studyTimeTrouble => 'Ցայտնոտ'; + + @override + String get studyWithCompensation => 'Փոխհատուցմամբ'; + + @override + String get studyWithTheIdea => 'Մտահղացմամբ'; + + @override + String get studyNextChapter => 'Հաջորդ գլուխը'; + + @override + String get studyPrevChapter => 'Նախորդ գլուխը'; + + @override + String get studyStudyActions => 'Գործողությունները ստուդիայում'; + + @override + String get studyTopics => 'Թեմաներ'; + + @override + String get studyMyTopics => 'Իմ թեմաները'; + + @override + String get studyPopularTopics => 'Շատ դիտվող թեմաներ'; + + @override + String get studyManageTopics => 'Թեմաների կառավարում'; + + @override + String get studyBack => 'Հետ'; + + @override + String get studyPlayAgain => 'Կրկին խաղալ'; + + @override + String get studyWhatWouldYouPlay => 'Ինչպե՞ս կխաղայիք այս դիրքում'; + + @override + String get studyYouCompletedThisLesson => 'Շնորհավորո՜ւմ ենք։ Դուք ավարեցիք այս դասը։'; + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count գլուխ', + one: '$count գլուխ', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count պարտիա', + one: '$count պարտիա', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count մասնակից', + one: '$count մասնակից', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Տեղադրեք տեսքտը PGN ձևաչափով, $count պարտիայից ոչ ավելի', + one: 'Տեղադրեք տեսքտը PGN ձևաչափով, $count պարտիայից ոչ ավելի', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_id.dart b/lib/l10n/l10n_id.dart index 45042ee484..700c873747 100644 --- a/lib/l10n/l10n_id.dart +++ b/lib/l10n/l10n_id.dart @@ -103,9 +103,6 @@ class AppLocalizationsId extends AppLocalizations { @override String get mobileCancelTakebackOffer => 'Cancel takeback offer'; - @override - String get mobileCancelDrawOffer => 'Cancel draw offer'; - @override String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @@ -238,6 +235,17 @@ class AppLocalizationsId extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Completed $count $param2 correspondence games', + one: 'Completed $count $param2 correspondence game', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -331,9 +339,226 @@ class AppLocalizationsId extends AppLocalizations { @override String get broadcastBroadcasts => 'Siaran'; + @override + String get broadcastMyBroadcasts => 'My broadcasts'; + @override String get broadcastLiveBroadcasts => 'Siaran turnamen langsung'; + @override + String get broadcastBroadcastCalendar => 'Broadcast calendar'; + + @override + String get broadcastNewBroadcast => 'Siaran langsung baru'; + + @override + String get broadcastSubscribedBroadcasts => 'Subscribed broadcasts'; + + @override + String get broadcastAboutBroadcasts => 'About broadcasts'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'How to use Lichess Broadcasts.'; + + @override + String get broadcastTheNewRoundHelp => 'The new round will have the same members and contributors as the previous one.'; + + @override + String get broadcastAddRound => 'Tambakan ronde'; + + @override + String get broadcastOngoing => 'Sedang berlangsung'; + + @override + String get broadcastUpcoming => 'Akan datang'; + + @override + String get broadcastCompleted => 'Telah selesai'; + + @override + String get broadcastCompletedHelp => 'Lichess detects round completion, but can get it wrong. Use this to set it manually.'; + + @override + String get broadcastRoundName => 'Nama ronde'; + + @override + String get broadcastRoundNumber => 'Babak ronde'; + + @override + String get broadcastTournamentName => 'Nama turnamen'; + + @override + String get broadcastTournamentDescription => 'Deskripsi singkat turnamen'; + + @override + String get broadcastFullDescription => 'Keterangan acara secara penuh'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Deskripsi panjang opsional dari siaran. $param1 tersedia. Panjangnya harus kurang dari $param2 karakter.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN Source URL'; + + @override + String get broadcastSourceUrlHelp => 'URL yang akan di-polling oleh Lichess untuk mendapatkan pembaruan PGN. Itu harus dapat diakses publik dari Internet.'; + + @override + String get broadcastSourceGameIds => 'Up to 64 Lichess game IDs, separated by spaces.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Start date in the tournament local timezone: $param'; + } + + @override + String get broadcastStartDateHelp => 'Opsional, jika Anda tahu kapan acara dimulai'; + + @override + String get broadcastCurrentGameUrl => 'Tautan permainan ini'; + + @override + String get broadcastDownloadAllRounds => 'Unduh semua ronde'; + + @override + String get broadcastResetRound => 'Atur ulang ronde ini'; + + @override + String get broadcastDeleteRound => 'Hapus ronde ini'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Definitively delete the round and all its games.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Delete all games of this round. The source will need to be active in order to re-create them.'; + + @override + String get broadcastEditRoundStudy => 'Edit round study'; + + @override + String get broadcastDeleteTournament => 'Delete this tournament'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Definitively delete the entire tournament, all its rounds and all its games.'; + + @override + String get broadcastShowScores => 'Show players scores based on game results'; + + @override + String get broadcastReplacePlayerTags => 'Optional: replace player names, ratings and titles'; + + @override + String get broadcastFideFederations => 'FIDE federations'; + + @override + String get broadcastTop10Rating => 'Top 10 rating'; + + @override + String get broadcastFidePlayers => 'FIDE players'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE player not found'; + + @override + String get broadcastFideProfile => 'FIDE profile'; + + @override + String get broadcastFederation => 'Federation'; + + @override + String get broadcastAgeThisYear => 'Age this year'; + + @override + String get broadcastUnrated => 'Unrated'; + + @override + String get broadcastRecentTournaments => 'Recent tournaments'; + + @override + String get broadcastOpenLichess => 'Open in Lichess'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Overview'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Official website'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'Score'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count broadcasts', + one: '$count broadcast', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Challenges: $param1'; @@ -590,7 +815,7 @@ class AppLocalizationsId extends AppLocalizations { String get preferencesOnlyOnInitialPosition => 'Hanya di posisi awal'; @override - String get preferencesInGameOnly => 'In-game only'; + String get preferencesInGameOnly => 'Hanya di dalam permainan'; @override String get preferencesChessClock => 'Jam catur'; @@ -1368,10 +1593,10 @@ class AppLocalizationsId extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Musuh dibatasi gerakan yang dapat mereka lakukan, dan semua gerakan memperburuk posisi mereka.'; @override - String get puzzleThemeHealthyMix => 'Campuran baik'; + String get puzzleThemeMix => 'Campuran baik'; @override - String get puzzleThemeHealthyMixDescription => 'Sedikit dari segalanya. Anda tidak tahu apa yang akan terjadi, jadi Anda tetap siap untuk apapun! Sama seperti permainan sebenarnya.'; + String get puzzleThemeMixDescription => 'Sedikit dari segalanya. Anda tidak tahu apa yang akan terjadi, jadi Anda tetap siap untuk apapun! Sama seperti permainan sebenarnya.'; @override String get puzzleThemePlayerGames => 'Permainan pemain'; @@ -1510,7 +1735,7 @@ class AppLocalizationsId extends AppLocalizations { String get variantEnding => 'Akhir sesuai aturan variasi'; @override - String get newOpponent => 'Penantang baru'; + String get newOpponent => 'Permainan yang baru'; @override String get yourOpponentWantsToPlayANewGameWithYou => 'Lawan Anda ingin bermain lagi dengan Anda'; @@ -1775,9 +2000,6 @@ class AppLocalizationsId extends AppLocalizations { @override String get removesTheDepthLimit => 'Menghapus batas kedalaman, dan membuat komputer Anda hangat'; - @override - String get engineManager => 'Pengaturan komputer'; - @override String get blunder => 'Blunder'; @@ -1856,7 +2078,7 @@ class AppLocalizationsId extends AppLocalizations { String get friends => 'Teman'; @override - String get otherPlayers => 'other players'; + String get otherPlayers => 'pemain lainnya'; @override String get discussions => 'Diskusi'; @@ -2041,6 +2263,9 @@ class AppLocalizationsId extends AppLocalizations { @override String get gamesPlayed => 'Permainan yang telah dimainkan'; + @override + String get ok => 'OK'; + @override String get cancel => 'Batal'; @@ -2609,7 +2834,7 @@ class AppLocalizationsId extends AppLocalizations { String get editProfile => 'Ubah profil'; @override - String get realName => 'Real name'; + String get realName => 'Nama asli'; @override String get setFlair => 'Sunting flair anda'; @@ -2690,7 +2915,7 @@ class AppLocalizationsId extends AppLocalizations { String get yes => 'Ya'; @override - String get website => 'Website'; + String get website => 'Situs Web'; @override String get mobile => 'Mobile'; @@ -2750,7 +2975,13 @@ class AppLocalizationsId extends AppLocalizations { String get other => 'Lainnya'; @override - String get reportDescriptionHelp => 'Paste link berikut ke dalam permainan dan jelaskan apa masalah tentang pengguna ini.'; + String get reportCheatBoostHelp => 'Paste the link to the game(s) and explain what is wrong about this user\'s behaviour. Don\'t just say \"they cheat\", but tell us how you came to this conclusion.'; + + @override + String get reportUsernameHelp => 'Explain what about this username is offensive. Don\'t just say \"it\'s offensive/inappropriate\", but tell us how you came to this conclusion, especially if the insult is obfuscated, not in english, is in slang, or is a historical/cultural reference.'; + + @override + String get reportProcessedFasterInEnglish => 'Your report will be processed faster if written in English.'; @override String get error_provideOneCheatedGameLink => 'Harap berikan setidaknya satu tautan ke permainan yang curang.'; @@ -3449,22 +3680,22 @@ class AppLocalizationsId extends AppLocalizations { String get backgroundImageUrl => 'URL gambar latar belakang:'; @override - String get board => 'Board'; + String get board => 'Papan'; @override - String get size => 'Size'; + String get size => 'Ukuran'; @override - String get opacity => 'Opacity'; + String get opacity => 'Transparansi'; @override - String get brightness => 'Brightness'; + String get brightness => 'Kecerahan'; @override - String get hue => 'Hue'; + String get hue => 'Rona'; @override - String get boardReset => 'Reset colours to default'; + String get boardReset => 'Kembalikan warna ke pengaturan semula'; @override String get pieceSet => 'Susunan buah catur'; @@ -3656,10 +3887,10 @@ class AppLocalizationsId extends AppLocalizations { } @override - String get showUnreadLichessMessage => 'You have received a private message from Lichess.'; + String get showUnreadLichessMessage => 'Anda telah menerima pesan pribadi dari Lichess.'; @override - String get clickHereToReadIt => 'Click here to read it'; + String get clickHereToReadIt => 'Klik di sini untuk membaca'; @override String get sorry => 'Maaf :('; @@ -3727,7 +3958,7 @@ class AppLocalizationsId extends AppLocalizations { String get edit => 'Ubah'; @override - String get bullet => 'Bullet'; + String get bullet => 'Peluru'; @override String get blitz => 'Blitz'; @@ -4044,16 +4275,19 @@ class AppLocalizationsId extends AppLocalizations { String get ourEventTips => 'Tips dari kami terkait penyelenggaraan acara'; @override - String get instructions => 'Instructions'; + String get instructions => 'Instruksi'; @override - String get showMeEverything => 'Show me everything'; + String get showMeEverything => 'Tunjukkan semuanya'; @override String get lichessPatronInfo => 'Lichess adalah sebuah amal dan semuanya merupakan perangkat lunak sumber terbuka yang gratis/bebas.\nSemua biaya operasi, pengembangan, dan konten didanai sepenuhnya oleh donasi pengguna.'; @override - String get nothingToSeeHere => 'Nothing to see here at the moment.'; + String get nothingToSeeHere => 'Tidak ada yang bisa dilihat untuk saat ini.'; + + @override + String get stats => 'Stats'; @override String opponentLeftCounter(int count) { @@ -4657,9 +4891,510 @@ class AppLocalizationsId extends AppLocalizations { @override String get streamerLichessStreamers => 'Streamer Lichess'; + @override + String get studyPrivate => 'Pribadi'; + + @override + String get studyMyStudies => 'Studi saya'; + + @override + String get studyStudiesIContributeTo => 'Studi yang saya ikut berkontribusi'; + + @override + String get studyMyPublicStudies => 'Studi publik saya'; + + @override + String get studyMyPrivateStudies => 'Studi pribadi saya'; + + @override + String get studyMyFavoriteStudies => 'Studi favorit saya'; + + @override + String get studyWhatAreStudies => 'Apa itu studi?'; + + @override + String get studyAllStudies => 'Semua studi'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Studi dibuat oleh $param'; + } + + @override + String get studyNoneYet => 'Tidak ada.'; + + @override + String get studyHot => 'Terhangat'; + + @override + String get studyDateAddedNewest => 'Tanggal ditambahkan (terbaru)'; + + @override + String get studyDateAddedOldest => 'Tanggal ditambahkan (terlama)'; + + @override + String get studyRecentlyUpdated => 'Baru saja diperbarui'; + + @override + String get studyMostPopular => 'Paling populer'; + + @override + String get studyAlphabetical => 'Menurut abjad'; + + @override + String get studyAddNewChapter => 'Tambahkan bab baru'; + + @override + String get studyAddMembers => 'Tambahkan anggota'; + + @override + String get studyInviteToTheStudy => 'Ajak untuk studi'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Harap hanya mengundang orang yang Anda kenal, dan yang secara aktif ingin bergabung dengan studi ini.'; + + @override + String get studySearchByUsername => 'Cari berdasarkan nama pengguna'; + + @override + String get studySpectator => 'Penonton'; + + @override + String get studyContributor => 'Kontributor'; + + @override + String get studyKick => 'Diusir'; + + @override + String get studyLeaveTheStudy => 'Tinggalkan studi'; + + @override + String get studyYouAreNowAContributor => 'Sekarang Anda menjadi kontributor'; + + @override + String get studyYouAreNowASpectator => 'Sekarang Anda adalah penonton'; + + @override + String get studyPgnTags => 'Tagar PGN'; + + @override + String get studyLike => 'Suka'; + + @override + String get studyUnlike => 'Batal Suka'; + + @override + String get studyNewTag => 'Tagar baru'; + + @override + String get studyCommentThisPosition => 'Komentar di posisi ini'; + + @override + String get studyCommentThisMove => 'Komentari langkah ini'; + + @override + String get studyAnnotateWithGlyphs => 'Anotasikan dengan glif'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Bab ini terlalu pendek untuk di analisa.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Hanya kontributor yang dapat meminta analisa komputer.'; + + @override + String get studyGetAFullComputerAnalysis => 'Dapatkan analisis komputer penuh di pihak server dari jalur utama.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Pastikan bab ini selesai. Anda hanya dapat meminta analisis satu kali.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Semua anggota yang ter-sinkron tetap pada posisi yang sama'; + + @override + String get studyShareChanges => 'Bagikan perubahan dengan penonton dan simpan di server'; + + @override + String get studyPlaying => 'Memainkan'; + + @override + String get studyShowEvalBar => 'Evaluation bars'; + + @override + String get studyFirst => 'Pertama'; + + @override + String get studyPrevious => 'Sebelumnya'; + + @override + String get studyNext => 'Berikutnya'; + + @override + String get studyLast => 'Terakhir'; + @override String get studyShareAndExport => 'Bagikan & ekspor'; + @override + String get studyCloneStudy => 'Gandakan'; + + @override + String get studyStudyPgn => 'Studi PGN'; + + @override + String get studyDownloadAllGames => 'Unduh semua permainan'; + + @override + String get studyChapterPgn => 'Bab PGN'; + + @override + String get studyCopyChapterPgn => 'Salin PGN'; + + @override + String get studyDownloadGame => 'Unduh permainan'; + + @override + String get studyStudyUrl => 'URL studi'; + + @override + String get studyCurrentChapterUrl => 'URL Bab saat ini'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Anda dapat menempelkan ini di forum untuk disematkan'; + + @override + String get studyStartAtInitialPosition => 'Mulai saat posisi awal'; + + @override + String studyStartAtX(String param) { + return 'Mulai dari $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Sematkan di blog atau website Anda'; + + @override + String get studyReadMoreAboutEmbedding => 'Baca lebih tentang penyematan'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Hanya pelajaran publik yang dapat di sematkan!'; + + @override + String get studyOpen => 'Buka'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1 dibawakan kepadamu dari $param2'; + } + + @override + String get studyStudyNotFound => 'Studi tidak ditemukan'; + + @override + String get studyEditChapter => 'Ubah bab'; + + @override + String get studyNewChapter => 'Bab baru'; + + @override + String studyImportFromChapterX(String param) { + return 'Impor dari $param'; + } + + @override + String get studyOrientation => 'Orientasi'; + + @override + String get studyAnalysisMode => 'Mode analisa'; + + @override + String get studyPinnedChapterComment => 'Sematkan komentar bagian bab'; + + @override + String get studySaveChapter => 'Simpan bab'; + + @override + String get studyClearAnnotations => 'Hapus anotasi'; + + @override + String get studyClearVariations => 'Hapus variasi'; + + @override + String get studyDeleteChapter => 'Hapus bab'; + + @override + String get studyDeleteThisChapter => 'Hapus bab ini? Ini tidak akan dapat mengulangkan kembali!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Hapus semua komentar dan bentuk di bab ini?'; + + @override + String get studyRightUnderTheBoard => 'Kanan dibawah papan'; + + @override + String get studyNoPinnedComment => 'Tidak ada'; + + @override + String get studyNormalAnalysis => 'Analisa biasa'; + + @override + String get studyHideNextMoves => 'Sembunyikan langkah selanjutnya'; + + @override + String get studyInteractiveLesson => 'Pelajaran interaktif'; + + @override + String studyChapterX(String param) { + return 'Bab $param'; + } + + @override + String get studyEmpty => 'Kosong'; + + @override + String get studyStartFromInitialPosition => 'Mulai dari posisi awal'; + + @override + String get studyEditor => 'Penyunting'; + + @override + String get studyStartFromCustomPosition => 'Mulai dari posisi yang disesuaikan'; + + @override + String get studyLoadAGameByUrl => 'Muat permainan dari URL'; + + @override + String get studyLoadAPositionFromFen => 'Muat posisi dari FEN'; + + @override + String get studyLoadAGameFromPgn => 'Muat permainan dari PGN'; + + @override + String get studyAutomatic => 'Otomatis'; + + @override + String get studyUrlOfTheGame => 'URL permainan'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Muat permainan dari $param1 atau $param2'; + } + + @override + String get studyCreateChapter => 'Buat bab'; + + @override + String get studyCreateStudy => 'Buat studi'; + + @override + String get studyEditStudy => 'Ubah studi'; + + @override + String get studyVisibility => 'Visibilitas'; + + @override + String get studyPublic => 'Publik'; + + @override + String get studyUnlisted => 'Tidak terdaftar'; + + @override + String get studyInviteOnly => 'Hanya yang diundang'; + + @override + String get studyAllowCloning => 'Perbolehkan kloning'; + + @override + String get studyNobody => 'Tidak ada seorangpun'; + + @override + String get studyOnlyMe => 'Hanya saya'; + + @override + String get studyContributors => 'Kontributor'; + + @override + String get studyMembers => 'Anggota'; + + @override + String get studyEveryone => 'Semua orang'; + + @override + String get studyEnableSync => 'Aktifkan sinkronisasi'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Ya: atur semua orang dalam posisi yang sama'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Tidak: Bolehkan untuk menjelajah dengan bebas'; + + @override + String get studyPinnedStudyComment => 'Sematkan komentar studi'; + @override String get studyStart => 'Mulai'; + + @override + String get studySave => 'Simpan'; + + @override + String get studyClearChat => 'Bersihkan obrolan'; + + @override + String get studyDeleteTheStudyChatHistory => 'Hapus riwayat obrolan studi? Ini tidak akan dapat mengulangkan kembali!'; + + @override + String get studyDeleteStudy => 'Hapus studi'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Hapus seluruh studi? Tidak dapat kembal lagi! Tuliskan nama studi untuk konfirmasi: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Dimana Anda ingin mempelajarinya?'; + + @override + String get studyGoodMove => 'Langkah bagus'; + + @override + String get studyMistake => 'Kesalahan'; + + @override + String get studyBrilliantMove => 'Langkah Brilian'; + + @override + String get studyBlunder => 'Blunder'; + + @override + String get studyInterestingMove => 'Langkah menarik'; + + @override + String get studyDubiousMove => 'Langkah meragukan'; + + @override + String get studyOnlyMove => 'Langkah satu-satunya'; + + @override + String get studyZugzwang => 'Zugzwang'; + + @override + String get studyEqualPosition => 'Posisi imbang'; + + @override + String get studyUnclearPosition => 'Posisi tidak jelas'; + + @override + String get studyWhiteIsSlightlyBetter => 'Putih sedikit lebih unggul'; + + @override + String get studyBlackIsSlightlyBetter => 'Hitam sedikit lebih unggul'; + + @override + String get studyWhiteIsBetter => 'Putih lebih unggul'; + + @override + String get studyBlackIsBetter => 'Hitam lebih unggul'; + + @override + String get studyWhiteIsWinning => 'Putih menang telak'; + + @override + String get studyBlackIsWinning => 'Hitam menang telak'; + + @override + String get studyNovelty => 'Langkah baru'; + + @override + String get studyDevelopment => 'Pengembangan'; + + @override + String get studyInitiative => 'Inisiatif'; + + @override + String get studyAttack => 'Serangan'; + + @override + String get studyCounterplay => 'Serangan balik'; + + @override + String get studyTimeTrouble => 'Tekanan waktu'; + + @override + String get studyWithCompensation => 'Dengan kompensasi'; + + @override + String get studyWithTheIdea => 'Dengan ide'; + + @override + String get studyNextChapter => 'Bab selanjutnya'; + + @override + String get studyPrevChapter => 'Bab sebelumnya'; + + @override + String get studyStudyActions => 'Pembelajaran'; + + @override + String get studyTopics => 'Topik'; + + @override + String get studyMyTopics => 'Topik saya'; + + @override + String get studyPopularTopics => 'Topik populer'; + + @override + String get studyManageTopics => 'Kelola topik'; + + @override + String get studyBack => 'Kembali'; + + @override + String get studyPlayAgain => 'Main lagi'; + + @override + String get studyWhatWouldYouPlay => 'What would you play in this position?'; + + @override + String get studyYouCompletedThisLesson => 'Selamat. Anda telah menyelesaikan pelajaran ini.'; + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Bab', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Permainan', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Anggota', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Tempelkan PGN kamu disini, lebih dari $count permainan', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_it.dart b/lib/l10n/l10n_it.dart index 84551045ec..997afd9ccb 100644 --- a/lib/l10n/l10n_it.dart +++ b/lib/l10n/l10n_it.dart @@ -103,9 +103,6 @@ class AppLocalizationsIt extends AppLocalizations { @override String get mobileCancelTakebackOffer => 'Annulla richiesta di ritiro mossa'; - @override - String get mobileCancelDrawOffer => 'Annulla richiesta di patta'; - @override String get mobileWaitingForOpponentToJoin => 'In attesa dell\'avversario...'; @@ -125,24 +122,24 @@ class AppLocalizationsIt extends AppLocalizations { String get mobileSomethingWentWrong => 'Si è verificato un errore.'; @override - String get mobileShowResult => 'Show result'; + String get mobileShowResult => 'Mostra il risultato'; @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobilePuzzleThemesSubtitle => '.'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobilePuzzleStormSubtitle => 'Risolvi il maggior numero di puzzle in tre minuti.'; @override String mobileGreeting(String param) { - return 'Hello, $param'; + return 'Ciao, $param'; } @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileGreetingWithoutName => 'Ciao'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobilePrefMagnifyDraggedPiece => 'Ingrandisci il pezzo trascinato'; @override String get activityActivity => 'Attività'; @@ -246,6 +243,17 @@ class AppLocalizationsIt extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Partite $count $param2 per corrispondenza completate', + one: 'Partita $count $param2 per corrispondenza completata', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,226 @@ class AppLocalizationsIt extends AppLocalizations { @override String get broadcastBroadcasts => 'Dirette'; + @override + String get broadcastMyBroadcasts => 'Le mie trasmissioni'; + @override String get broadcastLiveBroadcasts => 'Tornei in diretta'; + @override + String get broadcastBroadcastCalendar => 'Calendario trasmissioni'; + + @override + String get broadcastNewBroadcast => 'Nuova diretta'; + + @override + String get broadcastSubscribedBroadcasts => 'Trasmissioni abbonate'; + + @override + String get broadcastAboutBroadcasts => 'Informazioni sulle trasmissioni'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Istruzioni delle trasmissioni Lichess.'; + + @override + String get broadcastTheNewRoundHelp => 'Il nuovo turno avrà gli stessi membri e contributori del precedente.'; + + @override + String get broadcastAddRound => 'Aggiungi un turno'; + + @override + String get broadcastOngoing => 'In corso'; + + @override + String get broadcastUpcoming => 'Prossimamente'; + + @override + String get broadcastCompleted => 'Conclusa'; + + @override + String get broadcastCompletedHelp => 'Lichess rileva il completamento del turno a seconda delle partite di origine. Utilizza questo interruttore se non è presente alcuna origine.'; + + @override + String get broadcastRoundName => 'Nome turno'; + + @override + String get broadcastRoundNumber => 'Turno numero'; + + @override + String get broadcastTournamentName => 'Nome del torneo'; + + @override + String get broadcastTournamentDescription => 'Breve descrizione dell\'evento'; + + @override + String get broadcastFullDescription => 'Descrizione completa dell\'evento'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return '(Facoltativo) Descrizione completa dell\'evento. $param1 è disponibile. La lunghezza deve essere inferiore a $param2 caratteri.'; + } + + @override + String get broadcastSourceSingleUrl => 'Sorgente URL PGN'; + + @override + String get broadcastSourceUrlHelp => 'L\'URL che Lichess utilizzerà per ottenere gli aggiornamenti dei PGN. Deve essere accessibile pubblicamente su Internet.'; + + @override + String get broadcastSourceGameIds => 'Fino a 64 ID di partite Lichess, separati da spazi.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Data d\'inizio nel fuso orario locale del torneo: $param'; + } + + @override + String get broadcastStartDateHelp => 'Facoltativo, se sai quando inizia l\'evento'; + + @override + String get broadcastCurrentGameUrl => 'URL della partita corrente'; + + @override + String get broadcastDownloadAllRounds => 'Scarica tutti i round'; + + @override + String get broadcastResetRound => 'Reimposta questo turno'; + + @override + String get broadcastDeleteRound => 'Elimina questo turno'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Elimina definitivamente il turno e le sue partite.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Elimina tutte le partite di questo turno. L\'emittente dovrà essere attiva per poterli ricreare.'; + + @override + String get broadcastEditRoundStudy => 'Modifica lo studio del turno'; + + @override + String get broadcastDeleteTournament => 'Elimina questo torneo'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Elimina definitivamente l\'intero torneo, tutti i turni e tutte le partite.'; + + @override + String get broadcastShowScores => 'Mostra i punteggi dei giocatori in base ai risultati del gioco'; + + @override + String get broadcastReplacePlayerTags => 'Facoltativo: sostituisci i nomi dei giocatori, i punteggi e i titoli'; + + @override + String get broadcastFideFederations => 'Federazioni FIDE'; + + @override + String get broadcastTop10Rating => 'Migliori 10 punteggi'; + + @override + String get broadcastFidePlayers => 'Giocatori FIDE'; + + @override + String get broadcastFidePlayerNotFound => 'Giocatore FIDE non trovato'; + + @override + String get broadcastFideProfile => 'Profilo FIDE'; + + @override + String get broadcastFederation => 'Federazione'; + + @override + String get broadcastAgeThisYear => 'Età quest\'anno'; + + @override + String get broadcastUnrated => 'Non classificato'; + + @override + String get broadcastRecentTournaments => 'Tornei recenti'; + + @override + String get broadcastOpenLichess => 'Open in Lichess'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Overview'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Official website'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'Score'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count dirette', + one: '$count diretta', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Sfide: $param1'; @@ -1390,10 +1615,10 @@ class AppLocalizationsIt extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'L\'avversario è limitato nella sua scelta della mossa, e tutte le mosse possibili peggiorano la sua posizione.'; @override - String get puzzleThemeHealthyMix => 'Mix generale'; + String get puzzleThemeMix => 'Mix generale'; @override - String get puzzleThemeHealthyMixDescription => 'Un po\' di tutto. Nessuna aspettativa, affinché si possa rimanere pronti a qualsiasi cosa! Proprio come nelle partite vere.'; + String get puzzleThemeMixDescription => 'Un po\' di tutto. Nessuna aspettativa, affinché si possa rimanere pronti a qualsiasi cosa! Proprio come nelle partite vere.'; @override String get puzzleThemePlayerGames => 'Partite tra giocatori'; @@ -1797,9 +2022,6 @@ class AppLocalizationsIt extends AppLocalizations { @override String get removesTheDepthLimit => 'Rimuove il limite di profondità di analisi, ma può surriscaldare il tuo computer'; - @override - String get engineManager => 'Gestore del motore'; - @override String get blunder => 'Errore grave'; @@ -1878,7 +2100,7 @@ class AppLocalizationsIt extends AppLocalizations { String get friends => 'Amici'; @override - String get otherPlayers => 'other players'; + String get otherPlayers => 'altri giocatori'; @override String get discussions => 'Conversazioni'; @@ -2063,6 +2285,9 @@ class AppLocalizationsIt extends AppLocalizations { @override String get gamesPlayed => 'Partite giocate'; + @override + String get ok => 'OK'; + @override String get cancel => 'Annulla'; @@ -2712,10 +2937,10 @@ class AppLocalizationsIt extends AppLocalizations { String get yes => 'Sì'; @override - String get website => 'Website'; + String get website => 'Sito'; @override - String get mobile => 'Mobile'; + String get mobile => 'Cellulare'; @override String get help => 'Aiuto:'; @@ -2772,7 +2997,13 @@ class AppLocalizationsIt extends AppLocalizations { String get other => 'Altro'; @override - String get reportDescriptionHelp => 'Incolla il link della partita/e e spiega cosa non va con questo giocatore. Non dire soltanto \"ha imbrogliato\", ma specifica come sei arrivato a questa conclusione. Il tuo report verrà processato più velocemente se scritto in lingua inglese.'; + String get reportCheatBoostHelp => 'Incolla il link della partita(o partite) e spiega cosa non va sul comportamento di questo utente. Non dire solamente \"ha barato\", ma invece dici come sei arrivato a questa conclusione.'; + + @override + String get reportUsernameHelp => 'Spiegaci cosa vi è di offensivo in questo nome utente. Non dire solamente \"è offensivo/inappopriato\", ma invece dici come sei arrivato a questa conclusione, soprattutto se l\'insulto è offuscato, non in inglese, in linguaggio giovanile, oppure se è un riferimento storico/culturale.'; + + @override + String get reportProcessedFasterInEnglish => 'La tua segnalazione sarà processata più velocemente se scritta in Inglese.'; @override String get error_provideOneCheatedGameLink => 'Si prega di fornire almeno un collegamento link di una partita in cui il giocatore ha imbrogliato.'; @@ -4077,6 +4308,9 @@ class AppLocalizationsIt extends AppLocalizations { @override String get nothingToSeeHere => 'Niente da vedere qui al momento.'; + @override + String get stats => 'Stats'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4723,9 +4957,514 @@ class AppLocalizationsIt extends AppLocalizations { @override String get streamerLichessStreamers => 'Lichess streamer'; + @override + String get studyPrivate => 'Privato'; + + @override + String get studyMyStudies => 'I miei studi'; + + @override + String get studyStudiesIContributeTo => 'Studi a cui collaboro'; + + @override + String get studyMyPublicStudies => 'I miei studi pubblici'; + + @override + String get studyMyPrivateStudies => 'I miei studi privati'; + + @override + String get studyMyFavoriteStudies => 'I miei studi preferiti'; + + @override + String get studyWhatAreStudies => 'Cosa sono gli \"studi\"?'; + + @override + String get studyAllStudies => 'Tutti gli studi'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Studi creati da $param'; + } + + @override + String get studyNoneYet => 'Vuoto.'; + + @override + String get studyHot => 'Hot'; + + @override + String get studyDateAddedNewest => 'Data di pubblicazione (dalla più recente)'; + + @override + String get studyDateAddedOldest => 'Data di pubblicazione (dalla meno recente)'; + + @override + String get studyRecentlyUpdated => 'Data di aggiornamento (dalla più recente)'; + + @override + String get studyMostPopular => 'Più popolari'; + + @override + String get studyAlphabetical => 'Alfabetico'; + + @override + String get studyAddNewChapter => 'Aggiungi un nuovo capitolo'; + + @override + String get studyAddMembers => 'Aggiungi membri'; + + @override + String get studyInviteToTheStudy => 'Invita allo studio'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Invita solo persone che conosci e che desiderano partecipare attivamente a questo studio.'; + + @override + String get studySearchByUsername => 'Cerca per nome utente'; + + @override + String get studySpectator => 'Spettatore'; + + @override + String get studyContributor => 'Partecipante'; + + @override + String get studyKick => 'Espelli'; + + @override + String get studyLeaveTheStudy => 'Abbandona lo studio'; + + @override + String get studyYouAreNowAContributor => 'Ora sei un partecipante'; + + @override + String get studyYouAreNowASpectator => 'Ora sei uno spettatore'; + + @override + String get studyPgnTags => 'Tag PGN'; + + @override + String get studyLike => 'Mi piace'; + + @override + String get studyUnlike => 'Non mi Piace'; + + @override + String get studyNewTag => 'Nuovo tag'; + + @override + String get studyCommentThisPosition => 'Commenta questa posizione'; + + @override + String get studyCommentThisMove => 'Commenta questa mossa'; + + @override + String get studyAnnotateWithGlyphs => 'Commenta con segni convenzionali'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Il capitolo è troppo breve per essere analizzato.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Solo i partecipanti allo studio possono richiedere un\'analisi del computer.'; + + @override + String get studyGetAFullComputerAnalysis => 'Richiedi un\'analisi completa del computer della variante principale.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Assicurati che il capitolo sia completo. Puoi richiedere l\'analisi solo una volta.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Tutti i membri in SYNC rimangono sulla stessa posizione'; + + @override + String get studyShareChanges => 'Condividi le modifiche con gli spettatori e salvale sul server'; + + @override + String get studyPlaying => 'In corso'; + + @override + String get studyShowEvalBar => 'Barre di valutazione'; + + @override + String get studyFirst => 'Primo'; + + @override + String get studyPrevious => 'Precedente'; + + @override + String get studyNext => 'Successivo'; + + @override + String get studyLast => 'Ultimo'; + @override String get studyShareAndExport => 'Condividi & esporta'; + @override + String get studyCloneStudy => 'Duplica'; + + @override + String get studyStudyPgn => 'PGN dello studio'; + + @override + String get studyDownloadAllGames => 'Scarica tutte le partite'; + + @override + String get studyChapterPgn => 'PGN del capitolo'; + + @override + String get studyCopyChapterPgn => 'Copia in PGN'; + + @override + String get studyDownloadGame => 'Scarica partita'; + + @override + String get studyStudyUrl => 'URL dello studio'; + + @override + String get studyCurrentChapterUrl => 'URL del capitolo corrente'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Puoi incollare questo URL nel forum per creare un rimando'; + + @override + String get studyStartAtInitialPosition => 'Inizia dalla prima mossa'; + + @override + String studyStartAtX(String param) { + return 'Inizia a: $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Incorpora nel tuo sito Web o Blog'; + + @override + String get studyReadMoreAboutEmbedding => 'Per saperne di più su come incorporare'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Solo gli studi pubblici possono essere incorporati!'; + + @override + String get studyOpen => 'Apri'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1 fornito da $param2'; + } + + @override + String get studyStudyNotFound => 'Studio non trovato'; + + @override + String get studyEditChapter => 'Modifica il capitolo'; + + @override + String get studyNewChapter => 'Nuovo capitolo'; + + @override + String studyImportFromChapterX(String param) { + return 'Importa da $param'; + } + + @override + String get studyOrientation => 'Orientamento'; + + @override + String get studyAnalysisMode => 'Modalità analisi'; + + @override + String get studyPinnedChapterComment => 'Commento del capitolo'; + + @override + String get studySaveChapter => 'Salva capitolo'; + + @override + String get studyClearAnnotations => 'Cancella annotazioni'; + + @override + String get studyClearVariations => 'Elimina le varianti'; + + @override + String get studyDeleteChapter => 'Elimina capitolo'; + + @override + String get studyDeleteThisChapter => 'Vuoi davvero eliminare questo capitolo? Sarà perso per sempre!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Cancellare tutti i commenti, le annotazioni e i disegni in questo capitolo?'; + + @override + String get studyRightUnderTheBoard => 'Sotto la scacchiera'; + + @override + String get studyNoPinnedComment => 'Nessun commento'; + + @override + String get studyNormalAnalysis => 'Analisi normale'; + + @override + String get studyHideNextMoves => 'Nascondi le mosse successive'; + + @override + String get studyInteractiveLesson => 'Lezione interattiva'; + + @override + String studyChapterX(String param) { + return 'Capitolo $param'; + } + + @override + String get studyEmpty => 'Semplice'; + + @override + String get studyStartFromInitialPosition => 'Parti dalla posizione iniziale'; + + @override + String get studyEditor => 'Editor'; + + @override + String get studyStartFromCustomPosition => 'Inizia da una posizione personalizzata'; + + @override + String get studyLoadAGameByUrl => 'Carica una partita da URL'; + + @override + String get studyLoadAPositionFromFen => 'Carica una posizione da FEN'; + + @override + String get studyLoadAGameFromPgn => 'Carica una partita da PGN'; + + @override + String get studyAutomatic => 'Automatica'; + + @override + String get studyUrlOfTheGame => 'URL della partita'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Carica una partita da $param1 o $param2'; + } + + @override + String get studyCreateChapter => 'Crea capitolo'; + + @override + String get studyCreateStudy => 'Crea studio'; + + @override + String get studyEditStudy => 'Modifica studio'; + + @override + String get studyVisibility => 'Visibilità'; + + @override + String get studyPublic => 'Pubblico'; + + @override + String get studyUnlisted => 'Non elencato'; + + @override + String get studyInviteOnly => 'Solo su invito'; + + @override + String get studyAllowCloning => 'Permetti la clonazione'; + + @override + String get studyNobody => 'Nessuno'; + + @override + String get studyOnlyMe => 'Solo io'; + + @override + String get studyContributors => 'Collaboratori'; + + @override + String get studyMembers => 'Membri'; + + @override + String get studyEveryone => 'Tutti'; + + @override + String get studyEnableSync => 'Abilita sincronizzazione'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Sì: tutti vedranno la stessa posizione'; + + @override + String get studyNoLetPeopleBrowseFreely => 'No: ognuno potrà scorrere i capitoli indipendentemente'; + + @override + String get studyPinnedStudyComment => 'Commento dello studio'; + @override String get studyStart => 'Inizia'; + + @override + String get studySave => 'Salva'; + + @override + String get studyClearChat => 'Cancella chat'; + + @override + String get studyDeleteTheStudyChatHistory => 'Vuoi davvero eliminare la cronologia della chat? Sarà persa per sempre!'; + + @override + String get studyDeleteStudy => 'Elimina studio'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Eliminare l\'intero studio? Non sarà possibile annullare l\'operazione! Digitare il nome dello studio per confermare: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Dove vuoi creare lo studio?'; + + @override + String get studyGoodMove => 'Bella mossa'; + + @override + String get studyMistake => 'Errore'; + + @override + String get studyBrilliantMove => 'Mossa geniale'; + + @override + String get studyBlunder => 'Errore grave'; + + @override + String get studyInterestingMove => 'Mossa interessante'; + + @override + String get studyDubiousMove => 'Mossa dubbia'; + + @override + String get studyOnlyMove => 'Unica mossa'; + + @override + String get studyZugzwang => 'Zugzwang'; + + @override + String get studyEqualPosition => 'Posizione equivalente'; + + @override + String get studyUnclearPosition => 'Posizione non chiara'; + + @override + String get studyWhiteIsSlightlyBetter => 'Il bianco è in lieve vantaggio'; + + @override + String get studyBlackIsSlightlyBetter => 'Il nero è in lieve vantaggio'; + + @override + String get studyWhiteIsBetter => 'Il bianco è in vantaggio'; + + @override + String get studyBlackIsBetter => 'Il nero è in vantaggio'; + + @override + String get studyWhiteIsWinning => 'Il bianco sta vincendo'; + + @override + String get studyBlackIsWinning => 'Il nero sta vincendo'; + + @override + String get studyNovelty => 'Novità'; + + @override + String get studyDevelopment => 'Sviluppo'; + + @override + String get studyInitiative => 'Iniziativa'; + + @override + String get studyAttack => 'Attacco'; + + @override + String get studyCounterplay => 'Contrattacco'; + + @override + String get studyTimeTrouble => 'Prolemi di tempo'; + + @override + String get studyWithCompensation => 'Con compenso'; + + @override + String get studyWithTheIdea => 'Con l\'idea'; + + @override + String get studyNextChapter => 'Prossimo capitolo'; + + @override + String get studyPrevChapter => 'Capitolo precedente'; + + @override + String get studyStudyActions => 'Studia azioni'; + + @override + String get studyTopics => 'Discussioni'; + + @override + String get studyMyTopics => 'Le mie discussioni'; + + @override + String get studyPopularTopics => 'Argomenti popolari'; + + @override + String get studyManageTopics => 'Gestisci discussioni'; + + @override + String get studyBack => 'Indietro'; + + @override + String get studyPlayAgain => 'Gioca di nuovo'; + + @override + String get studyWhatWouldYouPlay => 'Cosa giocheresti in questa posizione?'; + + @override + String get studyYouCompletedThisLesson => 'Congratulazioni! Hai completato questa lezione.'; + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count capitoli', + one: '$count capitolo', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count partite', + one: '$count partita', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count membri', + one: '$count membro', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Incolla qui i testi PGN, massimo $count partite', + one: 'Incolla qui il testo PGN, massimo $count partita', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_ja.dart b/lib/l10n/l10n_ja.dart index c64040946d..65c193783a 100644 --- a/lib/l10n/l10n_ja.dart +++ b/lib/l10n/l10n_ja.dart @@ -103,9 +103,6 @@ class AppLocalizationsJa extends AppLocalizations { @override String get mobileCancelTakebackOffer => '待ったをキャンセル'; - @override - String get mobileCancelDrawOffer => 'ドロー提案をキャンセル'; - @override String get mobileWaitingForOpponentToJoin => '対戦相手の参加を待っています…'; @@ -142,7 +139,7 @@ class AppLocalizationsJa extends AppLocalizations { String get mobileGreetingWithoutName => 'こんにちは'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobilePrefMagnifyDraggedPiece => 'ドラッグ中の駒を拡大'; @override String get activityActivity => '活動'; @@ -238,6 +235,16 @@ class AppLocalizationsJa extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 局の $param2 通信戦を完了しました', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -331,9 +338,225 @@ class AppLocalizationsJa extends AppLocalizations { @override String get broadcastBroadcasts => 'イベント中継'; + @override + String get broadcastMyBroadcasts => '自分の配信'; + @override String get broadcastLiveBroadcasts => '実戦トーナメントのライブ中継'; + @override + String get broadcastBroadcastCalendar => '中継カレンダー'; + + @override + String get broadcastNewBroadcast => '新しいライブ中継'; + + @override + String get broadcastSubscribedBroadcasts => '登録した配信'; + + @override + String get broadcastAboutBroadcasts => '中継について'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Lichess 中継の使い方。'; + + @override + String get broadcastTheNewRoundHelp => '新ラウンドには前回と同じメンバーと投稿者が参加します。'; + + @override + String get broadcastAddRound => 'ラウンドを追加'; + + @override + String get broadcastOngoing => '配信中'; + + @override + String get broadcastUpcoming => '予定'; + + @override + String get broadcastCompleted => '終了'; + + @override + String get broadcastCompletedHelp => 'Lichess は元になる対局に基づいてラウンド終了を検出します。元になる対局がない時はこのトグルを使ってください。'; + + @override + String get broadcastRoundName => 'ラウンド名'; + + @override + String get broadcastRoundNumber => 'ラウンド'; + + @override + String get broadcastTournamentName => '大会名'; + + @override + String get broadcastTournamentDescription => '大会の短い説明'; + + @override + String get broadcastFullDescription => '長い説明'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return '内容の詳しい説明(オプション)。$param1 が利用できます。長さは [欧文換算で] $param2 字まで。'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN のソース URL'; + + @override + String get broadcastSourceUrlHelp => 'Lichess が PGN を取得するための URL。インターネット上に公表されているもののみ。'; + + @override + String get broadcastSourceGameIds => 'Lichess ゲーム ID、半角スペースで区切って最大 64 個まで。'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Start date in the tournament local timezone: $param'; + } + + @override + String get broadcastStartDateHelp => 'イベント開始時刻(オプション)'; + + @override + String get broadcastCurrentGameUrl => '現在のゲームの URL'; + + @override + String get broadcastDownloadAllRounds => '全ラウンドをダウンロード'; + + @override + String get broadcastResetRound => 'このラウンドをリセット'; + + @override + String get broadcastDeleteRound => 'このラウンドを削除'; + + @override + String get broadcastDefinitivelyDeleteRound => 'このラウンドのゲームをすべて削除する。'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'このラウンドのすべてのゲームを削除します。復活させるには情報源がアクティブでなくてはなりません。'; + + @override + String get broadcastEditRoundStudy => 'ラウンドの研究を編集'; + + @override + String get broadcastDeleteTournament => 'このトーナメントを削除'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'トーナメント全体(全ラウンド、全ゲーム)を削除する。'; + + @override + String get broadcastShowScores => 'ゲーム結果に応じてプレイヤーのスコアを表示'; + + @override + String get broadcastReplacePlayerTags => 'オプション:プレイヤーの名前、レーティング、タイトルの変更'; + + @override + String get broadcastFideFederations => 'FIDE 加盟協会'; + + @override + String get broadcastTop10Rating => 'レーティング トップ10'; + + @override + String get broadcastFidePlayers => 'FIDE 選手'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE 選手が見つかりません'; + + @override + String get broadcastFideProfile => 'FIDE プロフィール'; + + @override + String get broadcastFederation => '所属協会'; + + @override + String get broadcastAgeThisYear => '今年時点の年齢'; + + @override + String get broadcastUnrated => 'レーティングなし'; + + @override + String get broadcastRecentTournaments => '最近のトーナメント'; + + @override + String get broadcastOpenLichess => 'Lichess で開く'; + + @override + String get broadcastTeams => 'チーム'; + + @override + String get broadcastBoards => 'ボード'; + + @override + String get broadcastOverview => '概要'; + + @override + String get broadcastSubscribeTitle => '登録しておくと各ラウンドの開始時に通知が来ます。アカウント設定でベルやプッシュ通知の切り替えができます。'; + + @override + String get broadcastUploadImage => 'トーナメントの画像をアップロード'; + + @override + String get broadcastNoBoardsYet => 'ボードはまだありません。棋譜がアップロードされると表示されます。'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'ボードはソースまたは $param 経由で読み込めます'; + } + + @override + String broadcastStartsAfter(String param) { + return '$param 後に開始'; + } + + @override + String get broadcastStartVerySoon => '中継はまもなく始まります。'; + + @override + String get broadcastNotYetStarted => '中継はまだ始まっていません。'; + + @override + String get broadcastOfficialWebsite => '公式サイト'; + + @override + String get broadcastStandings => '順位'; + + @override + String broadcastIframeHelp(String param) { + return '他のオプションは $param にあります'; + } + + @override + String get broadcastWebmastersPage => 'ウェブ管理者のページ'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'このラウンドについて公表されたリアルタイムの PGN です。$param も利用でき、高速かつ高効率の同期が行なえます。'; + } + + @override + String get broadcastEmbedThisBroadcast => 'この中継をウェブサイトに埋め込む'; + + @override + String broadcastEmbedThisRound(String param) { + return '$param をウェブサイトに埋め込む'; + } + + @override + String get broadcastRatingDiff => 'レーティングの差'; + + @override + String get broadcastGamesThisTournament => 'このトーナメントの対局'; + + @override + String get broadcastScore => 'スコア'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count ブロードキャスト', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'チャレンジ:$param1'; @@ -1368,10 +1591,10 @@ class AppLocalizationsJa extends AppLocalizations { String get puzzleThemeZugzwangDescription => '相手の指せる手が、どれを選んでも局面を悪くしてしまう形。'; @override - String get puzzleThemeHealthyMix => '混合'; + String get puzzleThemeMix => '混合'; @override - String get puzzleThemeHealthyMixDescription => 'いろいろな問題を少しずつ。どんな問題が来るかわからないので油断しないで! 実戦と同じです。'; + String get puzzleThemeMixDescription => 'いろいろな問題を少しずつ。どんな問題が来るかわからないので油断しないで! 実戦と同じです。'; @override String get puzzleThemePlayerGames => 'プレイヤーの対局'; @@ -1775,9 +1998,6 @@ class AppLocalizationsJa extends AppLocalizations { @override String get removesTheDepthLimit => '探索手数の制限をなくし最大限の解析を行なう'; - @override - String get engineManager => '解析エンジンの管理'; - @override String get blunder => '大悪手'; @@ -2041,6 +2261,9 @@ class AppLocalizationsJa extends AppLocalizations { @override String get gamesPlayed => '対局数'; + @override + String get ok => 'OK'; + @override String get cancel => 'キャンセル'; @@ -2750,7 +2973,13 @@ class AppLocalizationsJa extends AppLocalizations { String get other => 'その他'; @override - String get reportDescriptionHelp => '問題のゲームへのリンクを貼って、相手ユーザーの問題点を説明してください。ただ「イカサマだ」と言うのではなく、なぜそう思うか理由を書いてください。英語で書くと対応が早くできます。'; + String get reportCheatBoostHelp => 'ゲームへのリンクを張って、このユーザーの行動のどこが問題かを説明してください。ただ「チート」と言うのではなく、あなたがなぜそう思ったのか教えてください。'; + + @override + String get reportUsernameHelp => 'このユーザー名のどこが攻撃的かを説明してください。ただ「攻撃的」「不適切」と言うのではなく、あなたがなぜそう思ったのか教えてください。中でも綴りの変更、英語以外の言語、俗語、歴史・文化的要因に関係した場合は特に説明が必要です。'; + + @override + String get reportProcessedFasterInEnglish => '英語で書いていただくと通報への対応が早くなります。'; @override String get error_provideOneCheatedGameLink => '不正のあった対局 1 局以上へのリンクを添えてください。'; @@ -4055,6 +4284,9 @@ class AppLocalizationsJa extends AppLocalizations { @override String get nothingToSeeHere => '今は何もありません。'; + @override + String get stats => '統計'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4657,9 +4889,510 @@ class AppLocalizationsJa extends AppLocalizations { @override String get streamerLichessStreamers => 'Lichess 配信者'; + @override + String get studyPrivate => '非公開'; + + @override + String get studyMyStudies => '自分の研究'; + + @override + String get studyStudiesIContributeTo => '参加した研究'; + + @override + String get studyMyPublicStudies => '自分の公開研究'; + + @override + String get studyMyPrivateStudies => '自分の非公開研究'; + + @override + String get studyMyFavoriteStudies => 'お気に入りの研究'; + + @override + String get studyWhatAreStudies => '研究(study)とは?'; + + @override + String get studyAllStudies => 'すべての研究'; + + @override + String studyStudiesCreatedByX(String param) { + return '$param による研究'; + } + + @override + String get studyNoneYet => 'まだなし'; + + @override + String get studyHot => '注目'; + + @override + String get studyDateAddedNewest => '投稿日(新しい順)'; + + @override + String get studyDateAddedOldest => '投稿日(古い順)'; + + @override + String get studyRecentlyUpdated => '更新順'; + + @override + String get studyMostPopular => '人気順'; + + @override + String get studyAlphabetical => 'アルファベット順'; + + @override + String get studyAddNewChapter => '新たな章を追加'; + + @override + String get studyAddMembers => 'メンバーを追加する'; + + @override + String get studyInviteToTheStudy => 'この研究に招待する'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => '招待する相手は、あなたが知っていて参加したい人だけにしてください。'; + + @override + String get studySearchByUsername => 'ユーザー名で検索'; + + @override + String get studySpectator => '観戦者'; + + @override + String get studyContributor => '投稿参加者'; + + @override + String get studyKick => '追放'; + + @override + String get studyLeaveTheStudy => 'この研究から出る'; + + @override + String get studyYouAreNowAContributor => '投稿参加者になりました'; + + @override + String get studyYouAreNowASpectator => '観戦者になりました'; + + @override + String get studyPgnTags => 'PGN タグ'; + + @override + String get studyLike => 'いいね'; + + @override + String get studyUnlike => 'いいね解除'; + + @override + String get studyNewTag => '新しいタグ'; + + @override + String get studyCommentThisPosition => 'この局面にコメントする'; + + @override + String get studyCommentThisMove => 'この手にコメント'; + + @override + String get studyAnnotateWithGlyphs => '解説記号を入れる'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => '章が短すぎて解析できません。'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'コンピュータ解析を要請できるのは投稿参加者だけです。'; + + @override + String get studyGetAFullComputerAnalysis => '主手順についてサーバ上でのコンピュータ解析を行なう。'; + + @override + String get studyMakeSureTheChapterIsComplete => '章が完成したか確認してください。解析の要請は 1 回だけです。'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => '同期したメンバーは同じ局面に留まります'; + + @override + String get studyShareChanges => '変更を観戦者と共有し、サーバに保存する'; + + @override + String get studyPlaying => 'プレイ中'; + + @override + String get studyShowEvalBar => '評価値バー'; + + @override + String get studyFirst => '最初'; + + @override + String get studyPrevious => '前'; + + @override + String get studyNext => '次'; + + @override + String get studyLast => '最後'; + @override String get studyShareAndExport => '共有とエクスポート'; + @override + String get studyCloneStudy => '研究をコピー'; + + @override + String get studyStudyPgn => '研究の PGN'; + + @override + String get studyDownloadAllGames => '全局をダウンロード'; + + @override + String get studyChapterPgn => '章の PGN'; + + @override + String get studyCopyChapterPgn => 'PGN をコピー'; + + @override + String get studyDownloadGame => '1 局をダウンロード'; + + @override + String get studyStudyUrl => '研究の URL'; + + @override + String get studyCurrentChapterUrl => '現在の章の URL'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'これをフォーラムにペーストすれば埋め込み表示できます'; + + @override + String get studyStartAtInitialPosition => '開始局面から'; + + @override + String studyStartAtX(String param) { + return '$param に開始'; + } + + @override + String get studyEmbedInYourWebsite => '自分のウェブサイト/ブログに埋め込む'; + + @override + String get studyReadMoreAboutEmbedding => '埋め込み(embedding)の説明'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => '埋め込みできるのは公開研究だけです!'; + + @override + String get studyOpen => '開く'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1 を $param2 がお届けします'; + } + + @override + String get studyStudyNotFound => '研究が見つかりません'; + + @override + String get studyEditChapter => '章を編集'; + + @override + String get studyNewChapter => '新しい章'; + + @override + String studyImportFromChapterX(String param) { + return '$param からインポート'; + } + + @override + String get studyOrientation => '盤の上下'; + + @override + String get studyAnalysisMode => '解析モード'; + + @override + String get studyPinnedChapterComment => '章の優先表示コメント'; + + @override + String get studySaveChapter => '章を保存'; + + @override + String get studyClearAnnotations => '注釈をクリア'; + + @override + String get studyClearVariations => '手順をクリア'; + + @override + String get studyDeleteChapter => '章を削除'; + + @override + String get studyDeleteThisChapter => 'ほんとうに削除しますか? 戻せませんよ!'; + + @override + String get studyClearAllCommentsInThisChapter => 'この章のコメントと図形をすべて削除しますか?'; + + @override + String get studyRightUnderTheBoard => '盤のすぐ下に'; + + @override + String get studyNoPinnedComment => 'なし'; + + @override + String get studyNormalAnalysis => '通常解析'; + + @override + String get studyHideNextMoves => '次の手順をかくす'; + + @override + String get studyInteractiveLesson => '対話形式のレッスン'; + + @override + String studyChapterX(String param) { + return '章 $param'; + } + + @override + String get studyEmpty => '空白'; + + @override + String get studyStartFromInitialPosition => '開始局面から'; + + @override + String get studyEditor => 'エディタ'; + + @override + String get studyStartFromCustomPosition => '指定した局面から'; + + @override + String get studyLoadAGameByUrl => '棋譜を URL で読み込み'; + + @override + String get studyLoadAPositionFromFen => '局面を FEN で読み込み'; + + @override + String get studyLoadAGameFromPgn => '棋譜を PGN で読み込み'; + + @override + String get studyAutomatic => '自動'; + + @override + String get studyUrlOfTheGame => '棋譜の URL'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return '$param1 か $param2 から棋譜を読み込み'; + } + + @override + String get studyCreateChapter => '章を作成'; + + @override + String get studyCreateStudy => '研究を作成'; + + @override + String get studyEditStudy => '研究を編集'; + + @override + String get studyVisibility => '公開範囲'; + + @override + String get studyPublic => '公開'; + + @override + String get studyUnlisted => '非公開'; + + @override + String get studyInviteOnly => '招待のみ'; + + @override + String get studyAllowCloning => 'コピーの許可'; + + @override + String get studyNobody => '不許可'; + + @override + String get studyOnlyMe => '自分のみ'; + + @override + String get studyContributors => '参加者のみ'; + + @override + String get studyMembers => 'メンバー'; + + @override + String get studyEveryone => '全員'; + + @override + String get studyEnableSync => '同期'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => '同期する=全員が同じ局面を見る'; + + @override + String get studyNoLetPeopleBrowseFreely => '同期しない=各人が自由に閲覧'; + + @override + String get studyPinnedStudyComment => '優先表示コメント'; + @override String get studyStart => '開始'; + + @override + String get studySave => '保存'; + + @override + String get studyClearChat => 'チャットを消去'; + + @override + String get studyDeleteTheStudyChatHistory => 'ほんとうに削除しますか? 戻せませんよ!'; + + @override + String get studyDeleteStudy => '研究を削除'; + + @override + String studyConfirmDeleteStudy(String param) { + return '研究全体を削除しますか? 戻せませんよ! 削除なら研究の名称を入力: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'どこで研究しますか?'; + + @override + String get studyGoodMove => '好手'; + + @override + String get studyMistake => '悪手'; + + @override + String get studyBrilliantMove => '妙手'; + + @override + String get studyBlunder => '大悪手'; + + @override + String get studyInterestingMove => '面白い手'; + + @override + String get studyDubiousMove => '疑問手'; + + @override + String get studyOnlyMove => '絶対手'; + + @override + String get studyZugzwang => 'ツークツワンク'; + + @override + String get studyEqualPosition => '互角'; + + @override + String get studyUnclearPosition => '形勢不明'; + + @override + String get studyWhiteIsSlightlyBetter => '白やや優勢'; + + @override + String get studyBlackIsSlightlyBetter => '黒やや優勢'; + + @override + String get studyWhiteIsBetter => '白優勢'; + + @override + String get studyBlackIsBetter => '黒優勢'; + + @override + String get studyWhiteIsWinning => '白勝勢'; + + @override + String get studyBlackIsWinning => '黒勝勢'; + + @override + String get studyNovelty => '新手'; + + @override + String get studyDevelopment => '展開'; + + @override + String get studyInitiative => '主導権'; + + @override + String get studyAttack => '攻撃'; + + @override + String get studyCounterplay => '反撃'; + + @override + String get studyTimeTrouble => '時間切迫'; + + @override + String get studyWithCompensation => '駒損だが代償あり'; + + @override + String get studyWithTheIdea => '狙い'; + + @override + String get studyNextChapter => '次の章'; + + @override + String get studyPrevChapter => '前の章'; + + @override + String get studyStudyActions => '研究の操作'; + + @override + String get studyTopics => 'トピック'; + + @override + String get studyMyTopics => '自分のトピック'; + + @override + String get studyPopularTopics => '人気のトピック'; + + @override + String get studyManageTopics => 'トピックの管理'; + + @override + String get studyBack => '戻る'; + + @override + String get studyPlayAgain => 'もう一度プレイ'; + + @override + String get studyWhatWouldYouPlay => 'この局面、あなたならどう指す?'; + + @override + String get studyYouCompletedThisLesson => 'おめでとう ! このレッスンを修了しました。'; + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 章', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 局', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count メンバー', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'ここに PGN をペースト($count 局まで)', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_kk.dart b/lib/l10n/l10n_kk.dart index 8dbeda53d5..8794975fa0 100644 --- a/lib/l10n/l10n_kk.dart +++ b/lib/l10n/l10n_kk.dart @@ -103,9 +103,6 @@ class AppLocalizationsKk extends AppLocalizations { @override String get mobileCancelTakebackOffer => 'Cancel takeback offer'; - @override - String get mobileCancelDrawOffer => 'Cancel draw offer'; - @override String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @@ -246,6 +243,17 @@ class AppLocalizationsKk extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Completed $count $param2 correspondence games', + one: 'Completed $count $param2 correspondence game', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,226 @@ class AppLocalizationsKk extends AppLocalizations { @override String get broadcastBroadcasts => 'Көрсетілімдер'; + @override + String get broadcastMyBroadcasts => 'Менің көрсетілімдерім'; + @override String get broadcastLiveBroadcasts => 'Жарыстың тікелей көрсетілімдері'; + @override + String get broadcastBroadcastCalendar => 'Broadcast calendar'; + + @override + String get broadcastNewBroadcast => 'Жаңа тікелей көрсетілім'; + + @override + String get broadcastSubscribedBroadcasts => 'Subscribed broadcasts'; + + @override + String get broadcastAboutBroadcasts => 'About broadcasts'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'How to use Lichess Broadcasts.'; + + @override + String get broadcastTheNewRoundHelp => 'The new round will have the same members and contributors as the previous one.'; + + @override + String get broadcastAddRound => 'Айналым қосу'; + + @override + String get broadcastOngoing => 'Болып жатқан'; + + @override + String get broadcastUpcoming => 'Келе жатқан'; + + @override + String get broadcastCompleted => 'Аяқталған'; + + @override + String get broadcastCompletedHelp => 'Lichess detects round completion, but can get it wrong. Use this to set it manually.'; + + @override + String get broadcastRoundName => 'Айналым атауы'; + + @override + String get broadcastRoundNumber => 'Раунд нөмірі'; + + @override + String get broadcastTournamentName => 'Жарыс атауы'; + + @override + String get broadcastTournamentDescription => 'Жарыстың қысқа сипаттамасы'; + + @override + String get broadcastFullDescription => 'Оқиғаның толық сипаттамасы'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Көрсетілімнің қосымша үлкен сипаттамасы. $param1 қолданысқа ашық. Ұзындығы $param2 таңбадан кем болуы керек.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN Source URL'; + + @override + String get broadcastSourceUrlHelp => 'PGN жаңартуларын алу үшін Личес тексеретін сілтеме. Ол интернетте баршалыққа ашық болуы керек.'; + + @override + String get broadcastSourceGameIds => 'Up to 64 Lichess game IDs, separated by spaces.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Start date in the tournament local timezone: $param'; + } + + @override + String get broadcastStartDateHelp => 'Міндетті емес, егер күнін біліп тұрсаңыз'; + + @override + String get broadcastCurrentGameUrl => 'Қазіргі ойын сілтемесі'; + + @override + String get broadcastDownloadAllRounds => 'Барлық айналымдарды жүктеп алу'; + + @override + String get broadcastResetRound => 'Бұл айналымды жаңарту'; + + @override + String get broadcastDeleteRound => 'Бұл айналымды жою'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Айналым мен оның ойындарын толығымен жою.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Айналымның бүкіл ойындарын жою. Оларды қайта құру үшін қайнар көзі белсенді болуы керек.'; + + @override + String get broadcastEditRoundStudy => 'Айналымның зертханасын өзгерту'; + + @override + String get broadcastDeleteTournament => 'Бұл жарысты жою'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Жарысты айналым мен ойындарымен бірге толығымен жою.'; + + @override + String get broadcastShowScores => 'Show players scores based on game results'; + + @override + String get broadcastReplacePlayerTags => 'Optional: replace player names, ratings and titles'; + + @override + String get broadcastFideFederations => 'FIDE federations'; + + @override + String get broadcastTop10Rating => 'Top 10 rating'; + + @override + String get broadcastFidePlayers => 'FIDE players'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE player not found'; + + @override + String get broadcastFideProfile => 'FIDE profile'; + + @override + String get broadcastFederation => 'Federation'; + + @override + String get broadcastAgeThisYear => 'Age this year'; + + @override + String get broadcastUnrated => 'Unrated'; + + @override + String get broadcastRecentTournaments => 'Recent tournaments'; + + @override + String get broadcastOpenLichess => 'Open in Lichess'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Overview'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Official website'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'Score'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count көрсетілім', + one: '$count көрсетілім', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Шақырулар: $param1'; @@ -1390,10 +1615,10 @@ class AppLocalizationsKk extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Жүрісі шектелген тастардың әр жүрісі жалпы жағдайдың нашарлауына әкеп соқтыратын кез.'; @override - String get puzzleThemeHealthyMix => 'Аралас дастархан'; + String get puzzleThemeMix => 'Аралас дастархан'; @override - String get puzzleThemeHealthyMixDescription => 'Барлығынан аз-аздан. Күтпеген жағдайларға бейім болыңыз! Дәл нағыз шахматтағыдай!'; + String get puzzleThemeMixDescription => 'Барлығынан аз-аздан. Күтпеген жағдайларға бейім болыңыз! Дәл нағыз шахматтағыдай!'; @override String get puzzleThemePlayerGames => 'Ойыншылардан'; @@ -1797,9 +2022,6 @@ class AppLocalizationsKk extends AppLocalizations { @override String get removesTheDepthLimit => 'Тереңдік шектеулерін жояды, әрі компьютеріңізді қыздырады'; - @override - String get engineManager => 'Есептеуіш басқарушысы'; - @override String get blunder => 'Өрескел қателік'; @@ -2063,6 +2285,9 @@ class AppLocalizationsKk extends AppLocalizations { @override String get gamesPlayed => 'Ойындар саны'; + @override + String get ok => 'OK'; + @override String get cancel => 'Болдыртпау'; @@ -2772,7 +2997,13 @@ class AppLocalizationsKk extends AppLocalizations { String get other => 'Басқа'; @override - String get reportDescriptionHelp => 'Ойынның (-дардың) сілтемесін қойып, осы ойыншының қай әрекеті орынсыз болғанын түсіндіріп беріңіз. Жай ғана \"ол алдап ойнады\" деп жаза салмай, осы ойға қалай келгеніңізді айтып беріңіз. Сіздің шағымыңыз ағылшын тілінде жазылса, тезірек тексеруден өтеді.'; + String get reportCheatBoostHelp => 'Paste the link to the game(s) and explain what is wrong about this user\'s behaviour. Don\'t just say \"they cheat\", but tell us how you came to this conclusion.'; + + @override + String get reportUsernameHelp => 'Explain what about this username is offensive. Don\'t just say \"it\'s offensive/inappropriate\", but tell us how you came to this conclusion, especially if the insult is obfuscated, not in english, is in slang, or is a historical/cultural reference.'; + + @override + String get reportProcessedFasterInEnglish => 'Your report will be processed faster if written in English.'; @override String get error_provideOneCheatedGameLink => 'Кемі бір ойынның сілтемесін беруіңізді сұраймыз.'; @@ -3388,10 +3619,10 @@ class AppLocalizationsKk extends AppLocalizations { String get asFreeAsLichess => 'Личес-тей тегін'; @override - String get builtForTheLoveOfChessNotMoney => 'Ақша қуып емес, шахматты қызыққаннан жасап отырмыз'; + String get builtForTheLoveOfChessNotMoney => 'Ақша қуып емес, шахматты жақсы көргеннен жасап отырмыз'; @override - String get everybodyGetsAllFeaturesForFree => 'Барлық құралдары бүкіл адам үшін тегін'; + String get everybodyGetsAllFeaturesForFree => 'Барлық құралдары барлық адам үшін тегін'; @override String get zeroAdvertisement => 'Еш жарнамасыз'; @@ -4077,6 +4308,9 @@ class AppLocalizationsKk extends AppLocalizations { @override String get nothingToSeeHere => 'Nothing to see here at the moment.'; + @override + String get stats => 'Stats'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4501,7 +4735,7 @@ class AppLocalizationsKk extends AppLocalizations { count, locale: localeName, other: '$count тіліндегі нұсқасы бар!', - one: '$count тіліндегі нұсқасы бар!', + one: '$count тілде нұсқасы бар!', ); return '$_temp0'; } @@ -4723,9 +4957,514 @@ class AppLocalizationsKk extends AppLocalizations { @override String get streamerLichessStreamers => 'Личес стримерлері'; + @override + String get studyPrivate => 'Жеке'; + + @override + String get studyMyStudies => 'Менің зерттеулерім'; + + @override + String get studyStudiesIContributeTo => 'Қолдауымдағы зерттеулер'; + + @override + String get studyMyPublicStudies => 'Жалпыға ашық зерттеулерім'; + + @override + String get studyMyPrivateStudies => 'Жеке зерттеулерім'; + + @override + String get studyMyFavoriteStudies => 'Қалаулы зерттеулерім'; + + @override + String get studyWhatAreStudies => 'Зерттеулер деген не?'; + + @override + String get studyAllStudies => 'Бүкіл зерттеулер'; + + @override + String studyStudiesCreatedByX(String param) { + return '$param жасаған зерттеулер'; + } + + @override + String get studyNoneYet => 'Әзірге жоқ.'; + + @override + String get studyHot => 'Тренд'; + + @override + String get studyDateAddedNewest => 'Құрылған күні (жаңадан)'; + + @override + String get studyDateAddedOldest => 'Құрылған күні (ескіден)'; + + @override + String get studyRecentlyUpdated => 'Жақында құрылған'; + + @override + String get studyMostPopular => 'Ең танымалдары'; + + @override + String get studyAlphabetical => 'Әліппе ретімен'; + + @override + String get studyAddNewChapter => 'Жаңа бөлім құру'; + + @override + String get studyAddMembers => 'Мүшелерді қосу'; + + @override + String get studyInviteToTheStudy => 'Зерттеуге шақыру'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Тек таныстарды әрі зерттеуге қосылуға шын ниетті адамдарды ғана шықырыңыз.'; + + @override + String get studySearchByUsername => 'Тіркеулі атымен іздеу'; + + @override + String get studySpectator => 'Көрермен'; + + @override + String get studyContributor => 'Қолдаушы'; + + @override + String get studyKick => 'Шығару'; + + @override + String get studyLeaveTheStudy => 'Зерттеуден шығу'; + + @override + String get studyYouAreNowAContributor => 'Сіз енді қолдаушысыз'; + + @override + String get studyYouAreNowASpectator => 'Сіз енді көрерменсіз'; + + @override + String get studyPgnTags => 'PGN тэгтері'; + + @override + String get studyLike => 'Ұнату'; + + @override + String get studyUnlike => 'Ұнатпаймын'; + + @override + String get studyNewTag => 'Жаңа тэг'; + + @override + String get studyCommentThisPosition => 'Осы тақта күйі туралы пікір қалдыру'; + + @override + String get studyCommentThisMove => 'Осы жүріс туралы пікір қалдыру'; + + @override + String get studyAnnotateWithGlyphs => 'Глифтермен түсіндірме жазуу'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Бөлім талдауға жарамды болу үшін тым қысқа.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Зерттеу қолдаушылары ғана компьютерлік талдауды сұрай алады.'; + + @override + String get studyGetAFullComputerAnalysis => 'Сервер-жақты компьютер осы негізгі жолға толық талдау жасайтын болады.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Талдауды бір рет қана сұрай аласыз, сондықтан бөлімді аяқтауды ұмытпаңыз.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Барлық үйлескен мүшелер өз күйінде қалады'; + + @override + String get studyShareChanges => 'Көрермендермен өзгертулерді бөлісіңіз әрі серверде сақтап қойыңыз'; + + @override + String get studyPlaying => 'Қазір ойында'; + + @override + String get studyShowEvalBar => 'Evaluation bars'; + + @override + String get studyFirst => 'Бірінші'; + + @override + String get studyPrevious => 'Алдыңғы'; + + @override + String get studyNext => 'Келесі'; + + @override + String get studyLast => 'Соңғы'; + @override String get studyShareAndExport => 'Бөлісу мен Жүктеп алу'; + @override + String get studyCloneStudy => 'Көшірме'; + + @override + String get studyStudyPgn => 'Зерттеудің PGN'; + + @override + String get studyDownloadAllGames => 'Барлық ойындарды жүктеп алу'; + + @override + String get studyChapterPgn => 'Бөлімнің PGN'; + + @override + String get studyCopyChapterPgn => 'PGN-ді көшіру'; + + @override + String get studyDownloadGame => 'Ойынды жүктеп алу'; + + @override + String get studyStudyUrl => 'Зерттеудің сілтемесі'; + + @override + String get studyCurrentChapterUrl => 'Қазіргі бөлімнің сілтемесі'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Сіз бұны форумға не Личес блогыңызға қоя аласыз'; + + @override + String get studyStartAtInitialPosition => 'Басталуы: бастапқы күйден'; + + @override + String studyStartAtX(String param) { + return 'Басталуы: $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Сіздің сайт не блогыңызға арналған енгізу сілтемесі'; + + @override + String get studyReadMoreAboutEmbedding => 'Енгізу туралы оқыңыз'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Тек жалпыға ашық зерттеулер енгізуге жарамды!'; + + @override + String get studyOpen => 'Ашу'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1, оны сізге $param2 ұсынды'; + } + + @override + String get studyStudyNotFound => 'Зерттеу табылмады'; + + @override + String get studyEditChapter => 'Бөлімді өңдеу'; + + @override + String get studyNewChapter => 'Жаңа бөлім'; + + @override + String studyImportFromChapterX(String param) { + return '$param-нан жүктеп алу'; + } + + @override + String get studyOrientation => 'Бағыты'; + + @override + String get studyAnalysisMode => 'Талдау нұсқасы'; + + @override + String get studyPinnedChapterComment => 'Қадаулы бөлім пікірі'; + + @override + String get studySaveChapter => 'Бөлімді сақтау'; + + @override + String get studyClearAnnotations => 'Түсіндірмені өшіру'; + + @override + String get studyClearVariations => 'Тармақты өшіру'; + + @override + String get studyDeleteChapter => 'Бөлімді жою'; + + @override + String get studyDeleteThisChapter => 'Бөлімді жоясыз ба? Кері жол жоқ!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Бөлімдегі бүкіл пікір, глиф пен сызбаларды өшіресіз бе?'; + + @override + String get studyRightUnderTheBoard => 'Тура тақтаның астына'; + + @override + String get studyNoPinnedComment => 'Жоқ'; + + @override + String get studyNormalAnalysis => 'Қалыпты талдау'; + + @override + String get studyHideNextMoves => 'Келесі жүрістерді жасыру'; + + @override + String get studyInteractiveLesson => 'Интерактивті сабақ'; + + @override + String studyChapterX(String param) { + return '$param-ші бөлім'; + } + + @override + String get studyEmpty => 'Бос'; + + @override + String get studyStartFromInitialPosition => 'Басталуы: бастапқы күйден'; + + @override + String get studyEditor => 'Өңдеуші'; + + @override + String get studyStartFromCustomPosition => 'Басталуы: белгілі күйден'; + + @override + String get studyLoadAGameByUrl => 'Сілтеме арқылы ойындарды жүктеп салу'; + + @override + String get studyLoadAPositionFromFen => 'FEN арқылы ойындарды жүктеп салу'; + + @override + String get studyLoadAGameFromPgn => 'PGN арқылы ойындарды жүктеп салу'; + + @override + String get studyAutomatic => 'Автоматты түрде'; + + @override + String get studyUrlOfTheGame => 'Ойындардың сілтемесі, әр жолға бір-бірден'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return '$param1 не $param2 ойындарын жүктеп салу'; + } + + @override + String get studyCreateChapter => 'Бөлім құру'; + + @override + String get studyCreateStudy => 'Зерттеуді құру'; + + @override + String get studyEditStudy => 'Зерттеуді өңдеу'; + + @override + String get studyVisibility => 'Көрінуі'; + + @override + String get studyPublic => 'Жалпыға ашық'; + + @override + String get studyUnlisted => 'Жасырын'; + + @override + String get studyInviteOnly => 'Шақырумен ғана'; + + @override + String get studyAllowCloning => 'Көшірмеге рұқсат беру'; + + @override + String get studyNobody => 'Ешкім'; + + @override + String get studyOnlyMe => 'Өзім ғана'; + + @override + String get studyContributors => 'Қолдаушылар'; + + @override + String get studyMembers => 'Мүшелер'; + + @override + String get studyEveryone => 'Барлығы'; + + @override + String get studyEnableSync => 'Үйлесуді қосу'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Иә: бәрі бірдей күйде болады'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Жоқ: бәріне еркін шолуға рұқсат ету'; + + @override + String get studyPinnedStudyComment => 'Қадаулы зерттеу пікірі'; + @override String get studyStart => 'Бастау'; + + @override + String get studySave => 'Сақтау'; + + @override + String get studyClearChat => 'Чатты өшіру'; + + @override + String get studyDeleteTheStudyChatHistory => 'Зерттеудің чат тарихын өшіресіз бе? Кері жол жоқ!'; + + @override + String get studyDeleteStudy => 'Зерттеуді жою'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Бүкіл зерттеуді жоясыз ба? Қайтар жол жоқ. Растау үшін зерттеу атауын жазыңыз: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Бұл күйдің зерттеуін қай жерде бастайсыз?'; + + @override + String get studyGoodMove => 'Жақсы жүріс'; + + @override + String get studyMistake => 'Қате'; + + @override + String get studyBrilliantMove => 'Әдемі жүріс'; + + @override + String get studyBlunder => 'Өрескел қателік'; + + @override + String get studyInterestingMove => 'Қызық жүріс'; + + @override + String get studyDubiousMove => 'Күмәнді жүріс'; + + @override + String get studyOnlyMove => 'Жалғыз жүріс'; + + @override + String get studyZugzwang => 'Цугцванг'; + + @override + String get studyEqualPosition => 'Күйлері шамалас'; + + @override + String get studyUnclearPosition => 'Күйі анық емес'; + + @override + String get studyWhiteIsSlightlyBetter => 'Ақ сәл күштірек'; + + @override + String get studyBlackIsSlightlyBetter => 'Қара сәл күштірек'; + + @override + String get studyWhiteIsBetter => 'Ақтың жағдайы жақсы'; + + @override + String get studyBlackIsBetter => 'Қараның жағдайы жақсы'; + + @override + String get studyWhiteIsWinning => 'Ақ жеңеді'; + + @override + String get studyBlackIsWinning => 'Қара жеңеді'; + + @override + String get studyNovelty => 'Жаңашылдық'; + + @override + String get studyDevelopment => 'Дамыту'; + + @override + String get studyInitiative => 'Белсенді'; + + @override + String get studyAttack => 'Шабуыл'; + + @override + String get studyCounterplay => 'Қарсы шабуыл'; + + @override + String get studyTimeTrouble => 'Уақыт қаупі'; + + @override + String get studyWithCompensation => 'Өтеумен'; + + @override + String get studyWithTheIdea => 'Бір оймен'; + + @override + String get studyNextChapter => 'Келесі бөлім'; + + @override + String get studyPrevChapter => 'Алдыңғы бөлім'; + + @override + String get studyStudyActions => 'Зерттеу әрекеттері'; + + @override + String get studyTopics => 'Тақырыптар'; + + @override + String get studyMyTopics => 'Менің тақырыптарым'; + + @override + String get studyPopularTopics => 'Белгілі тақырыптар'; + + @override + String get studyManageTopics => 'Тақырыптарды басқару'; + + @override + String get studyBack => 'Кері қайту'; + + @override + String get studyPlayAgain => 'Қайта ойнау'; + + @override + String get studyWhatWouldYouPlay => 'Осы күйде не ойнамақсыз?'; + + @override + String get studyYouCompletedThisLesson => 'Құтты болсын! Сіз бұл сабақты бітірдіңіз.'; + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count бөлім', + one: '$count бөлім', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count ойын', + one: '$count ойын', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count мүше', + one: '$count мүше', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'PGN мәтінін осында қойыңыз, $count ойынға дейін', + one: 'PGN мәтінін осында қойыңыз, $count ойын ғана', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_ko.dart b/lib/l10n/l10n_ko.dart index df44e81b5d..d2613ec56a 100644 --- a/lib/l10n/l10n_ko.dart +++ b/lib/l10n/l10n_ko.dart @@ -86,10 +86,10 @@ class AppLocalizationsKo extends AppLocalizations { String get mobileSharePositionAsFEN => 'FEN으로 공유'; @override - String get mobileShowVariations => 'Show variations'; + String get mobileShowVariations => '바리에이션 보이기'; @override - String get mobileHideVariation => 'Hide variation'; + String get mobileHideVariation => '바리에이션 숨기기'; @override String get mobileShowComments => '댓글 보기'; @@ -103,14 +103,11 @@ class AppLocalizationsKo extends AppLocalizations { @override String get mobileCancelTakebackOffer => '무르기 요청 취소'; - @override - String get mobileCancelDrawOffer => '무승부 요청 취소'; - @override String get mobileWaitingForOpponentToJoin => '상대 참가를 기다리는 중...'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get mobileBlindfoldMode => '기물 가리기'; @override String get mobileLiveStreamers => '방송 중인 스트리머'; @@ -125,24 +122,24 @@ class AppLocalizationsKo extends AppLocalizations { String get mobileSomethingWentWrong => '문제가 발생했습니다.'; @override - String get mobileShowResult => 'Show result'; + String get mobileShowResult => '결과 표시'; @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobilePuzzleThemesSubtitle => '당신이 가장 좋아하는 오프닝으로부터의 퍼즐을 플레이하거나, 테마를 선택하십시오.'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobilePuzzleStormSubtitle => '3분 이내에 최대한 많은 퍼즐을 해결하십시오.'; @override String mobileGreeting(String param) { - return 'Hello, $param'; + return '안녕하세요, $param'; } @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileGreetingWithoutName => '안녕하세요'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobilePrefMagnifyDraggedPiece => '드래그한 기물 확대하기'; @override String get activityActivity => '활동'; @@ -163,7 +160,7 @@ class AppLocalizationsKo extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count 개월 동안 $param2 에서 lichess.org 을 후원하였습니다.', + other: '$count개월 동안 $param2으로 lichess.org를 후원함', ); return '$_temp0'; } @@ -173,7 +170,7 @@ class AppLocalizationsKo extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$param2 에서 총 $count 개의 포지션을 연습하였습니다.', + other: '$param2에서 총 $count개의 포지션을 연습함', ); return '$_temp0'; } @@ -183,7 +180,7 @@ class AppLocalizationsKo extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '전술 문제 $count 개를 해결하였습니다.', + other: '전술 문제 $count개를 해결함', ); return '$_temp0'; } @@ -213,7 +210,7 @@ class AppLocalizationsKo extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count 수를 둠', + other: '수 $count개를 둠', ); return '$_temp0'; } @@ -238,6 +235,16 @@ class AppLocalizationsKo extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count $param2 긴 대국전을 완료함', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -283,7 +290,7 @@ class AppLocalizationsKo extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count 건의 연구를 작성함', + other: '공부 $count개 작성함', ); return '$_temp0'; } @@ -331,9 +338,225 @@ class AppLocalizationsKo extends AppLocalizations { @override String get broadcastBroadcasts => '방송'; + @override + String get broadcastMyBroadcasts => '내 방송'; + @override String get broadcastLiveBroadcasts => '실시간 대회 방송'; + @override + String get broadcastBroadcastCalendar => '방송 달력'; + + @override + String get broadcastNewBroadcast => '새 실시간 방송'; + + @override + String get broadcastSubscribedBroadcasts => '구독 중인 방송'; + + @override + String get broadcastAboutBroadcasts => '방송에 대해서'; + + @override + String get broadcastHowToUseLichessBroadcasts => '리체스 방송을 사용하는 방법.'; + + @override + String get broadcastTheNewRoundHelp => '새로운 라운드에는 이전 라운드와 동일한 구성원과 기여자가 있을 것입니다.'; + + @override + String get broadcastAddRound => '라운드 추가'; + + @override + String get broadcastOngoing => '진행중'; + + @override + String get broadcastUpcoming => '방영 예정'; + + @override + String get broadcastCompleted => '종료됨'; + + @override + String get broadcastCompletedHelp => 'Lichess는 경기 완료를 감지하지만, 잘못될 때가 있을 수 있습니다. 수동으로 설정하기 위해 이걸 사용하세요.'; + + @override + String get broadcastRoundName => '라운드 이름'; + + @override + String get broadcastRoundNumber => '라운드 숫자'; + + @override + String get broadcastTournamentName => '토너먼트 이름'; + + @override + String get broadcastTournamentDescription => '짧은 토너먼트 설명'; + + @override + String get broadcastFullDescription => '전체 이벤트 설명'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return '(선택) 방송에 대한 긴 설명입니다. $param1 사용이 가능합니다. 길이는 $param2 글자보다 짧아야 합니다.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN Source URL'; + + @override + String get broadcastSourceUrlHelp => 'Lichess가 PGN 업데이트를 받기 위해 확인할 URL입니다. 인터넷에서 공개적으로 액세스 할 수 있어야 합니다.'; + + @override + String get broadcastSourceGameIds => '공간으로 나눠진 64개까지의 Lichess 경기 ID.'; + + @override + String broadcastStartDateTimeZone(String param) { + return '내 시간대의 토너먼트 시작 날짜: $param'; + } + + @override + String get broadcastStartDateHelp => '선택 사항, 언제 이벤트가 시작되는지 알고 있는 경우'; + + @override + String get broadcastCurrentGameUrl => '현재 게임 URL'; + + @override + String get broadcastDownloadAllRounds => '모든 라운드 다운로드받기'; + + @override + String get broadcastResetRound => '라운드 초기화'; + + @override + String get broadcastDeleteRound => '라운드 삭제'; + + @override + String get broadcastDefinitivelyDeleteRound => '라운드와 해당 게임을 완전히 삭제합니다.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => '이 라운드의 모든 게임을 삭제합니다. 다시 생성하려면 소스가 활성화되어 있어야 합니다.'; + + @override + String get broadcastEditRoundStudy => '경기 공부 편집'; + + @override + String get broadcastDeleteTournament => '이 토너먼트 삭제'; + + @override + String get broadcastDefinitivelyDeleteTournament => '토너먼트 전체의 모든 라운드와 게임을 완전히 삭제합니다.'; + + @override + String get broadcastShowScores => '게임 결과에 따라 플레이어 점수 표시'; + + @override + String get broadcastReplacePlayerTags => '선택 사항: 플레이어 이름, 레이팅 및 타이틀 바꾸기'; + + @override + String get broadcastFideFederations => 'FIDE 연맹'; + + @override + String get broadcastTop10Rating => 'Top 10 레이팅'; + + @override + String get broadcastFidePlayers => 'FIDE 선수들'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE 선수 찾지 못함'; + + @override + String get broadcastFideProfile => 'FIDE 프로필'; + + @override + String get broadcastFederation => '연맹'; + + @override + String get broadcastAgeThisYear => '올해 나이'; + + @override + String get broadcastUnrated => '비레이팅'; + + @override + String get broadcastRecentTournaments => '최근 토너먼트'; + + @override + String get broadcastOpenLichess => 'Lichess에서 열기'; + + @override + String get broadcastTeams => '팀'; + + @override + String get broadcastBoards => '보드'; + + @override + String get broadcastOverview => '개요'; + + @override + String get broadcastSubscribeTitle => '라운드가 시작될 때 알림을 받으려면 구독하세요. 계정 설정에서 방송을 위한 벨이나 알림 푸시를 토글할 수 있습니다.'; + + @override + String get broadcastUploadImage => '토너먼트 사진 업로드'; + + @override + String get broadcastNoBoardsYet => '아직 보드가 없습니다. 게임들이 업로드되면 나타납니다.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return '보드들은 소스나 $param(으)로 로드될 수 있습니다'; + } + + @override + String broadcastStartsAfter(String param) { + return '$param 후 시작'; + } + + @override + String get broadcastStartVerySoon => '방송이 곧 시작됩니다.'; + + @override + String get broadcastNotYetStarted => '아직 방송이 시작을 하지 않았습니다.'; + + @override + String get broadcastOfficialWebsite => '공식 웹사이트'; + + @override + String get broadcastStandings => '순위'; + + @override + String broadcastIframeHelp(String param) { + return '$param에서 더 많은 정보를 확인하실 수 있습니다'; + } + + @override + String get broadcastWebmastersPage => '웹마스터 페이지'; + + @override + String broadcastPgnSourceHelp(String param) { + return '이 라운드의 공개된, 실시간 PGN 소스 입니다. 보다 더 빠르고 효율적인 동기화를 위해 $param도 제공됩니다.'; + } + + @override + String get broadcastEmbedThisBroadcast => '이 방송을 웹사이트에 삽입하세요'; + + @override + String broadcastEmbedThisRound(String param) { + return '$param을(를) 웹사이트에 삼입하세요'; + } + + @override + String get broadcastRatingDiff => '레이팅 차이'; + + @override + String get broadcastGamesThisTournament => '이 토너먼트의 게임들'; + + @override + String get broadcastScore => '점수'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 방송', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return '도전: $param1'; @@ -457,7 +680,7 @@ class AppLocalizationsKo extends AppLocalizations { } @override - String get perfStatTotalGames => '총 게임'; + String get perfStatTotalGames => '총 대국'; @override String get perfStatRatedGames => '레이팅 게임'; @@ -472,13 +695,13 @@ class AppLocalizationsKo extends AppLocalizations { String get perfStatTimeSpentPlaying => '플레이한 시간'; @override - String get perfStatAverageOpponent => '상대의 평균 레이팅'; + String get perfStatAverageOpponent => '상대 평균'; @override - String get perfStatVictories => '승리'; + String get perfStatVictories => '승'; @override - String get perfStatDefeats => '패배'; + String get perfStatDefeats => '패'; @override String get perfStatDisconnections => '연결 끊김'; @@ -498,7 +721,7 @@ class AppLocalizationsKo extends AppLocalizations { @override String perfStatFromXToY(String param1, String param2) { - return '$param1에서 $param2'; + return '$param1에서 $param2까지'; } @override @@ -518,10 +741,10 @@ class AppLocalizationsKo extends AppLocalizations { } @override - String get perfStatBestRated => '승리한 최고 레이팅'; + String get perfStatBestRated => '최고 레이팅 승리'; @override - String get perfStatGamesInARow => '연속 게임 플레이'; + String get perfStatGamesInARow => '연속 대국'; @override String get perfStatLessThanOneHour => '게임 사이가 1시간 미만인 경우'; @@ -653,13 +876,13 @@ class AppLocalizationsKo extends AppLocalizations { String get preferencesMoveConfirmation => '피스를 움직이기 전에 물음'; @override - String get preferencesExplainCanThenBeTemporarilyDisabled => 'Can be disabled during a game with the board menu'; + String get preferencesExplainCanThenBeTemporarilyDisabled => '경기 도중 보드 메뉴에서 비활성화될 수 있습니다.'; @override - String get preferencesInCorrespondenceGames => '긴 대국에서만'; + String get preferencesInCorrespondenceGames => '통신전'; @override - String get preferencesCorrespondenceAndUnlimited => '긴 대국과 무제한'; + String get preferencesCorrespondenceAndUnlimited => '통신과 무제한'; @override String get preferencesConfirmResignationAndDrawOffers => '기권 또는 무승부 제안시 물음'; @@ -707,7 +930,7 @@ class AppLocalizationsKo extends AppLocalizations { String get preferencesNotifyInvitedStudy => '스터디 초대'; @override - String get preferencesNotifyGameEvent => '긴 대국 업데이트'; + String get preferencesNotifyGameEvent => '통신전 업데이트'; @override String get preferencesNotifyChallenge => '도전 과제'; @@ -716,7 +939,7 @@ class AppLocalizationsKo extends AppLocalizations { String get preferencesNotifyTournamentSoon => '곧 토너먼트 시작할 때'; @override - String get preferencesNotifyTimeAlarm => '긴 대국 시간 초과'; + String get preferencesNotifyTimeAlarm => '통신전 시간 곧 만료됨'; @override String get preferencesNotifyBell => '리체스 내에서 벨 알림'; @@ -779,13 +1002,13 @@ class AppLocalizationsKo extends AppLocalizations { String get puzzleDownVote => '퍼즐 비추천'; @override - String get puzzleYourPuzzleRatingWillNotChange => '당신의 퍼즐 레이팅은 바뀌지 않을 것입니다. 퍼즐은 경쟁이 아니라는 걸 기억하세요. 레이팅은 당신의 현재 스킬에 맞는 퍼즐을 선택하도록 돕습니다.'; + String get puzzleYourPuzzleRatingWillNotChange => '당신의 퍼즐 레이팅은 바뀌지 않을 것입니다. 퍼즐은 경쟁이 아니라는 걸 기억하세요. 레이팅은 당신의 현재 수준에 맞는 퍼즐을 선택하도록 돕습니다.'; @override String get puzzleFindTheBestMoveForWhite => '백의 최고의 수를 찾아보세요.'; @override - String get puzzleFindTheBestMoveForBlack => '흑의 최고의 수를 찾아보세요.'; + String get puzzleFindTheBestMoveForBlack => '흑의 최선 수를 찾아보세요.'; @override String get puzzleToGetPersonalizedPuzzles => '개인화된 퍼즐을 위해선:'; @@ -835,7 +1058,7 @@ class AppLocalizationsKo extends AppLocalizations { String get puzzleUseCtrlF => 'Ctrl+f를 사용해서 가장 좋아하는 오프닝을 찾으세요!'; @override - String get puzzleNotTheMove => '답이 아닙니다!'; + String get puzzleNotTheMove => '그 수가 아닙니다!'; @override String get puzzleTrySomethingElse => '다른 것 시도하기'; @@ -1119,7 +1342,7 @@ class AppLocalizationsKo extends AppLocalizations { String get puzzleThemeDefensiveMoveDescription => '기물을 잃거나 다른 손실을 피하기 위해 필요한 정확한 수입니다.'; @override - String get puzzleThemeDeflection => '유인'; + String get puzzleThemeDeflection => '굴절'; @override String get puzzleThemeDeflectionDescription => '중요한 칸을 수비하는 등 다른 역할을 수행하는 상대 기물의 주의를 분산시키는 수입니다. \"과부하\"라고도 불립니다.'; @@ -1368,10 +1591,10 @@ class AppLocalizationsKo extends AppLocalizations { String get puzzleThemeZugzwangDescription => '상대가 둘 수 있는 수는 제한되어 있으며, 모든 수가 포지션을 악화시킵니다.'; @override - String get puzzleThemeHealthyMix => '골고루 섞기'; + String get puzzleThemeMix => '골고루 섞기'; @override - String get puzzleThemeHealthyMixDescription => '전부 다. 무엇이 나올지 모르기 때문에 모든 것에 준비되어 있어야 합니다. 마치 진짜 게임처럼요.'; + String get puzzleThemeMixDescription => '전부 다. 무엇이 나올지 모르기 때문에 모든 것에 준비되어 있어야 합니다. 마치 진짜 게임처럼요.'; @override String get puzzleThemePlayerGames => '플레이어 게임'; @@ -1415,13 +1638,13 @@ class AppLocalizationsKo extends AppLocalizations { String get playWithAFriend => '친구와 게임하기'; @override - String get playWithTheMachine => '체스 엔진과 게임하기'; + String get playWithTheMachine => '체스 엔진과 붙기'; @override String get toInviteSomeoneToPlayGiveThisUrl => '이 URL로 친구를 초대하세요'; @override - String get gameOver => '게임 종료'; + String get gameOver => '게임 오버'; @override String get waitingForOpponent => '상대를 기다리는 중'; @@ -1462,10 +1685,10 @@ class AppLocalizationsKo extends AppLocalizations { String get stalemate => '스테일메이트'; @override - String get white => '백색'; + String get white => '백'; @override - String get black => '흑색'; + String get black => '흑'; @override String get asWhite => '백일때'; @@ -1516,22 +1739,22 @@ class AppLocalizationsKo extends AppLocalizations { String get yourOpponentWantsToPlayANewGameWithYou => '상대가 재대결을 원합니다'; @override - String get joinTheGame => '게임 참가'; + String get joinTheGame => '대국 참가'; @override - String get whitePlays => '백색 차례'; + String get whitePlays => '백 차례'; @override - String get blackPlays => '흑색 차례'; + String get blackPlays => '흑 차례'; @override String get opponentLeftChoices => '당신의 상대가 게임을 나갔습니다. 상대를 기다리거나 승리 또는 무승부 처리할 수 있습니다.'; @override - String get forceResignation => '승리 처리'; + String get forceResignation => '승리 취하기'; @override - String get forceDraw => '무승부 처리'; + String get forceDraw => '무승부 선언'; @override String get talkInChat => '건전한 채팅을 해주세요!'; @@ -1540,25 +1763,25 @@ class AppLocalizationsKo extends AppLocalizations { String get theFirstPersonToComeOnThisUrlWillPlayWithYou => '이 URL로 가장 먼저 들어온 사람과 체스를 두게 됩니다.'; @override - String get whiteResigned => '백색 기권'; + String get whiteResigned => '백 기권함'; @override - String get blackResigned => '흑색 기권'; + String get blackResigned => '흑 기권함'; @override - String get whiteLeftTheGame => '백색이 게임을 나갔습니다'; + String get whiteLeftTheGame => '백이 게임을 나갔습니다'; @override - String get blackLeftTheGame => '흑색이 게임을 나갔습니다'; + String get blackLeftTheGame => '흑이 게임을 나갔습니다'; @override String get whiteDidntMove => '백이 두지 않음'; @override - String get blackDidntMove => '흑이 두지 않음'; + String get blackDidntMove => '흑이 수를 두지 않음'; @override - String get requestAComputerAnalysis => '컴퓨터 분석 요청'; + String get requestAComputerAnalysis => '컴퓨터 분석 요청하기'; @override String get computerAnalysis => '컴퓨터 분석'; @@ -1567,7 +1790,7 @@ class AppLocalizationsKo extends AppLocalizations { String get computerAnalysisAvailable => '컴퓨터 분석이 가능합니다.'; @override - String get computerAnalysisDisabled => '컴퓨터 분석 꺼짐'; + String get computerAnalysisDisabled => '컴퓨터 분석 비활성화됨'; @override String get analysis => '분석'; @@ -1581,13 +1804,13 @@ class AppLocalizationsKo extends AppLocalizations { String get usingServerAnalysis => '서버 분석 사용하기'; @override - String get loadingEngine => '엔진 로드 중 ...'; + String get loadingEngine => '엔진 로드 중...'; @override String get calculatingMoves => '수 계산 중...'; @override - String get engineFailed => '엔진 로딩 에러'; + String get engineFailed => '엔진 불러오는 도중 오류 발생'; @override String get cloudAnalysis => '클라우드 분석'; @@ -1596,16 +1819,16 @@ class AppLocalizationsKo extends AppLocalizations { String get goDeeper => '더 깊게 분석하기'; @override - String get showThreat => '위험요소 표시하기'; + String get showThreat => '위험요소 표시'; @override String get inLocalBrowser => '브라우저에서'; @override - String get toggleLocalEvaluation => '개인 컴퓨터에서 분석하기'; + String get toggleLocalEvaluation => '로컬 분석 토글'; @override - String get promoteVariation => '게임 분석 후에 어떤 수에 대한 예상결과들을 확인하고 싶다면'; + String get promoteVariation => '변형 승격'; @override String get makeMainLine => '주 라인으로 하기'; @@ -1614,10 +1837,10 @@ class AppLocalizationsKo extends AppLocalizations { String get deleteFromHere => '여기서부터 삭제'; @override - String get collapseVariations => 'Collapse variations'; + String get collapseVariations => '바리에이션 축소하기'; @override - String get expandVariations => 'Expand variations'; + String get expandVariations => '바리에이션 확장하기'; @override String get forceVariation => '변화 강제하기'; @@ -1635,13 +1858,13 @@ class AppLocalizationsKo extends AppLocalizations { String get variantWin => '변형 체스에서 승리'; @override - String get insufficientMaterial => '기물 부족으로 무승부입니다.'; + String get insufficientMaterial => '기물 부족'; @override String get pawnMove => '폰 이동'; @override - String get capture => 'Capture'; + String get capture => '기물 잡기'; @override String get close => '닫기'; @@ -1662,7 +1885,7 @@ class AppLocalizationsKo extends AppLocalizations { String get database => '데이터베이스'; @override - String get whiteDrawBlack => '백 : 무승부 : 흑'; + String get whiteDrawBlack => '백 / 무승부 / 흑'; @override String averageRatingX(String param) { @@ -1684,7 +1907,7 @@ class AppLocalizationsKo extends AppLocalizations { String get dtzWithRounding => '다음 포획 혹은 폰 수까지 남은 반수를 반올림후 나타낸 DTZ50\" 수치'; @override - String get noGameFound => '게임을 찾을 수 없습니다.'; + String get noGameFound => '게임을 찾을 수 없습니다'; @override String get maxDepthReached => '최대 깊이 도달!'; @@ -1710,10 +1933,10 @@ class AppLocalizationsKo extends AppLocalizations { String get playFirstOpeningEndgameExplorerMove => '첫 번째 오프닝/엔드게임 탐색기 수 두기'; @override - String get winPreventedBy50MoveRule => '50수 규칙에 의하여 승리가 불가능합니다.'; + String get winPreventedBy50MoveRule => '50수 규칙에 의하여 승리가 불가능합니다'; @override - String get lossSavedBy50MoveRule => '50수 규칙에 의하여 패배가 불가능합니다.'; + String get lossSavedBy50MoveRule => '50수 규칙에 의하여 패배가 불가능합니다'; @override String get winOr50MovesByPriorMistake => '승리 혹은 이전의 실수로 인한 50수 규칙 무승부'; @@ -1725,7 +1948,7 @@ class AppLocalizationsKo extends AppLocalizations { String get unknownDueToRounding => 'DTZ 수치의 반올림 때문에 추천된 테이블베이스 라인을 따라야만 승리 및 패배가 보장됩니다.'; @override - String get allSet => '모든 설정 완료!'; + String get allSet => '모두 완료!'; @override String get importPgn => 'PGN 가져오기'; @@ -1743,7 +1966,7 @@ class AppLocalizationsKo extends AppLocalizations { String get realtimeReplay => '실시간'; @override - String get byCPL => '평가치변화'; + String get byCPL => '센티폰 손실'; @override String get openStudy => '연구를 시작하기'; @@ -1758,10 +1981,10 @@ class AppLocalizationsKo extends AppLocalizations { String get showVariationArrows => '바리에이션 화살표 표시하기'; @override - String get evaluationGauge => '평가치 게이지'; + String get evaluationGauge => '평가 게이지'; @override - String get multipleLines => '다중 분석 수'; + String get multipleLines => '다중 라인 수'; @override String get cpus => 'CPU 수'; @@ -1776,16 +1999,13 @@ class AppLocalizationsKo extends AppLocalizations { String get removesTheDepthLimit => '탐색 깊이 제한을 없애고 컴퓨터를 따뜻하게 해줍니다'; @override - String get engineManager => '엔진 매니저'; - - @override - String get blunder => '심각한 실수'; + String get blunder => '블런더'; @override String get mistake => '실수'; @override - String get inaccuracy => '사소한 실수'; + String get inaccuracy => '부정확한 수'; @override String get moveTimes => '이동 시간'; @@ -1809,10 +2029,10 @@ class AppLocalizationsKo extends AppLocalizations { String get drawByMutualAgreement => '상호 동의에 의한 무승부'; @override - String get fiftyMovesWithoutProgress => '진전이 없이 50수 소모'; + String get fiftyMovesWithoutProgress => '진전 없이 50수 소모'; @override - String get currentGames => '진행 중인 게임'; + String get currentGames => '진행 중인 게임들'; @override String get viewInFullSize => '크게 보기'; @@ -1827,13 +2047,13 @@ class AppLocalizationsKo extends AppLocalizations { String get rememberMe => '로그인 유지'; @override - String get youNeedAnAccountToDoThat => '회원만이 접근할 수 있습니다.'; + String get youNeedAnAccountToDoThat => '회원이어야 가능합니다'; @override String get signUp => '회원 가입'; @override - String get computersAreNotAllowedToPlay => '컴퓨터나 컴퓨터의 도움을 받는 플레이어는 대국이 금지되어 있습니다. 대국할 때 체스 엔진이나 관련 자료, 또는 주변 플레이어로부터 도움을 받지 마십시오. 또한, 다중 계정 사용은 권장하지 않으며 지나치게 많은 다중 계정을 사용할 시 계정이 차단될 수 있습니다.'; + String get computersAreNotAllowedToPlay => '컴퓨터나 컴퓨터 지원을 받는 플레이어들은 게임 참가가 금지되어 있습니다. 게임 중 체스 엔진이나, 데이터베이스나, 주변 플레이어들로부터 도움을 받지 마십시오. 이와 더불어 다중 계정 소유는 권장하지 않으며 지나치게 많은 계정들을 사용할 시 계정들이 차단될 수 있습니다.'; @override String get games => '게임'; @@ -1856,10 +2076,10 @@ class AppLocalizationsKo extends AppLocalizations { String get friends => '친구들'; @override - String get otherPlayers => 'other players'; + String get otherPlayers => '다른 플레이어들'; @override - String get discussions => '토론'; + String get discussions => '대화'; @override String get today => '오늘'; @@ -1868,7 +2088,7 @@ class AppLocalizationsKo extends AppLocalizations { String get yesterday => '어제'; @override - String get minutesPerSide => '주어진 시간(분)'; + String get minutesPerSide => '양쪽 시간(분)'; @override String get variant => '게임 종류'; @@ -1877,16 +2097,16 @@ class AppLocalizationsKo extends AppLocalizations { String get variants => '변형'; @override - String get timeControl => '시간 제한'; + String get timeControl => '제한 시간(분)'; @override - String get realTime => '짧은 대국'; + String get realTime => '차례 없음'; @override String get correspondence => '긴 대국'; @override - String get daysPerTurn => '한 수에 걸리는 일수'; + String get daysPerTurn => '수당 일수'; @override String get oneDay => '1일'; @@ -1901,19 +2121,19 @@ class AppLocalizationsKo extends AppLocalizations { String get ratingStats => '레이팅 통계'; @override - String get username => '아이디'; + String get username => '유저네임'; @override String get usernameOrEmail => '사용자 이름이나 이메일 주소'; @override - String get changeUsername => '사용자명 변경'; + String get changeUsername => '사용자 이름 변경'; @override - String get changeUsernameNotSame => '글자의 대소문자 변경만 가능합니다 예: \"johndoe\" to \"JohnDoe\".'; + String get changeUsernameNotSame => '글자의 대소문자 변경만 가능합니다 예: \"chulsoo\"에서 \"ChulSoo\"로.'; @override - String get changeUsernameDescription => '닉네임 변경하기: 대/소문자의 변경만이 허용되며, 단 한번만 가능한 작업입니다.'; + String get changeUsernameDescription => '사용자명 변경하기: 대/소문자만의 변경이 허용되며, 단 한번만 가능합니다.'; @override String get signupUsernameHint => '사용자 이름이 어린이를 포함해 모두에게 적절한지 확인하세요. 나중에 변경할 수 없으며 부적절한 사용자 이름을 가진 계정은 폐쇄됩니다!'; @@ -1928,10 +2148,10 @@ class AppLocalizationsKo extends AppLocalizations { String get changePassword => '비밀번호 변경'; @override - String get changeEmail => '메일 주소 변경'; + String get changeEmail => '이메일 주소 변경'; @override - String get email => '메일'; + String get email => '이메일'; @override String get passwordReset => '비밀번호 초기화'; @@ -1940,23 +2160,23 @@ class AppLocalizationsKo extends AppLocalizations { String get forgotPassword => '비밀번호를 잊어버리셨나요?'; @override - String get error_weakPassword => '이 비밀번호는 매우 일반적이고 추측하기 쉽습니다.'; + String get error_weakPassword => '이 비밀번호는 매우 흔하며 추측하기 쉽습니다.'; @override - String get error_namePassword => '사용자 아이디를 비밀번호로 사용하지 마세요.'; + String get error_namePassword => '사용자 이름을 비밀번호로 사용하지 마세요.'; @override - String get blankedPassword => '다른 사이트에서 동일한 비밀번호를 사용했으며 해당 사이트가 유출된 경우. 라이선스 계정의 안전을 위해 새 비밀번호를 설정해 주셔야 합니다. 양해해 주셔서 감사합니다.'; + String get blankedPassword => '당신이 다른 사이트에서 동일한 비밀번호를 사용했으며, 해당 사이트가 유출되었습니다. Lichess 계정의 안전을 위해 새 비밀번호를 설정해 주세요. 양해해 주셔서 감사합니다.'; @override - String get youAreLeavingLichess => '리체스에서 나갑니다'; + String get youAreLeavingLichess => 'Lichess에서 나갑니다'; @override - String get neverTypeYourPassword => '다른 사이트에서는 절대로 리체스 비밀번호를 입력하지 마세요!'; + String get neverTypeYourPassword => '다른 사이트에서는 절대로 Lichess 비밀번호를 입력하지 마세요!'; @override String proceedToX(String param) { - return '$param 진행'; + return '$param로 진행'; } @override @@ -1984,24 +2204,24 @@ class AppLocalizationsKo extends AppLocalizations { @override String emailSent(String param) { - return '$param로 이메일을 전송했습니다.'; + return '$param(으)로 이메일을 전송했습니다.'; } @override - String get emailCanTakeSomeTime => '도착하는데 시간이 걸릴 수 있습니다.'; + String get emailCanTakeSomeTime => '이메일이 도착하는데 시간이 좀 걸릴 수 있습니다.'; @override - String get refreshInboxAfterFiveMinutes => '5분 가량 기다린 후 이메일 수신함을 새로고침하세요.'; + String get refreshInboxAfterFiveMinutes => '5분 가량 기다린 후 이메일 수신함을 새로고침 해주세요.'; @override - String get checkSpamFolder => '또한 스펨메일함을 확인해주시고 스펨을 해제해주세요.'; + String get checkSpamFolder => '또한 스팸 메일함에 들어가 있을 수 있습니다. 만약 그런 경우, 스팸이 아님으로 표시해 두세요.'; @override - String get emailForSignupHelp => '모두 실패했다면 이곳으로 메일을 보내주세요:'; + String get emailForSignupHelp => '모두 실패했다면, 이곳으로 메일을 보내주세요:'; @override String copyTextToEmail(String param) { - return '위의 텍스트를 복사해서 $param로 보내주세요.'; + return '위의 텍스트를 복사해서 $param(으)로 보내주세요'; } @override @@ -2009,12 +2229,12 @@ class AppLocalizationsKo extends AppLocalizations { @override String accountConfirmed(String param) { - return '$param 사용자가 성공적으로 확인되었습니다.'; + return '유저 $param(이)가 성공적으로 확인되었습니다.'; } @override String accountCanLogin(String param) { - return '이제 $param로 로그인할 수 있습니다.'; + return '이제 $param(으)로 로그인할 수 있습니다.'; } @override @@ -2022,7 +2242,7 @@ class AppLocalizationsKo extends AppLocalizations { @override String accountClosed(String param) { - return '$param 계정은 폐쇄되었습니다.'; + return '계정 $param(은)는 폐쇄되었습니다.'; } @override @@ -2041,53 +2261,56 @@ class AppLocalizationsKo extends AppLocalizations { @override String get gamesPlayed => '게임'; + @override + String get ok => 'OK'; + @override String get cancel => '취소'; @override - String get whiteTimeOut => '백색 시간 초과'; + String get whiteTimeOut => '백 시간 초과'; @override - String get blackTimeOut => '흑색 시간 초과'; + String get blackTimeOut => '흑 시간 초과'; @override - String get drawOfferSent => '무승부를 요청했습니다'; + String get drawOfferSent => '무승부 요청함'; @override - String get drawOfferAccepted => '무승부 요청이 승낙됐습니다'; + String get drawOfferAccepted => '무승부 요청 수락됨'; @override - String get drawOfferCanceled => '무승부 요청을 취소했습니다'; + String get drawOfferCanceled => '무승부 요청 취소함'; @override - String get whiteOffersDraw => '백이 무승부를 제안했습니다'; + String get whiteOffersDraw => '백이 무승부를 제안합니다'; @override - String get blackOffersDraw => '흑이 무승부를 제안했습니다'; + String get blackOffersDraw => '흑이 무승부를 제안합니다'; @override - String get whiteDeclinesDraw => '백이 무승부 제안을 거절했습니다'; + String get whiteDeclinesDraw => '백이 무승부 제안을 거절하였습니다'; @override - String get blackDeclinesDraw => '흑이 무승부 제안을 거절했습니다'; + String get blackDeclinesDraw => '흑이 무승부 제안을 거절하였습니다'; @override - String get yourOpponentOffersADraw => '상대가 무승부를 요청했습니다'; + String get yourOpponentOffersADraw => '상대가 무승부를 요청합니다'; @override - String get accept => '승낙'; + String get accept => '수락'; @override String get decline => '거절'; @override - String get playingRightNow => '대국 중'; + String get playingRightNow => '지금 대국 중'; @override String get eventInProgress => '지금 대국 중'; @override - String get finished => '종료'; + String get finished => '종료됨'; @override String get abortGame => '게임 중단'; @@ -2096,10 +2319,10 @@ class AppLocalizationsKo extends AppLocalizations { String get gameAborted => '게임 중단됨'; @override - String get standard => '표준'; + String get standard => '스탠다드'; @override - String get customPosition => '사용자 지정 포지션'; + String get customPosition => '커스텀 포지션'; @override String get unlimited => '무제한'; @@ -2114,7 +2337,7 @@ class AppLocalizationsKo extends AppLocalizations { String get rated => '레이팅'; @override - String get casualTournament => '일반'; + String get casualTournament => '캐주얼'; @override String get ratedTournament => '레이팅'; @@ -2126,13 +2349,13 @@ class AppLocalizationsKo extends AppLocalizations { String get rematch => '재대결'; @override - String get rematchOfferSent => '재대결 요청을 보냈습니다'; + String get rematchOfferSent => '재대결 요청 전송됨'; @override - String get rematchOfferAccepted => '재대결 요청이 승낙됐습니다'; + String get rematchOfferAccepted => '재대결 요청 승낙됨'; @override - String get rematchOfferCanceled => '재대결 요청이 취소됐습니다'; + String get rematchOfferCanceled => '재대결 요청 취소됨'; @override String get rematchOfferDeclined => '재대결 요청이 거절됐습니다'; @@ -2141,7 +2364,7 @@ class AppLocalizationsKo extends AppLocalizations { String get cancelRematchOffer => '재대결 요청 취소'; @override - String get viewRematch => '재대결 보러 가기'; + String get viewRematch => '재대결 보기'; @override String get confirmMove => '수 확인'; @@ -2150,16 +2373,16 @@ class AppLocalizationsKo extends AppLocalizations { String get play => '플레이'; @override - String get inbox => '받은편지함'; + String get inbox => '편지함'; @override String get chatRoom => '채팅'; @override - String get loginToChat => '채팅에 로그인하기'; + String get loginToChat => '채팅하려면 로그인하세요'; @override - String get youHaveBeenTimedOut => '채팅에서 로그아웃 되었습니다.'; + String get youHaveBeenTimedOut => '채팅에서 타임아웃 되었습니다.'; @override String get spectatorRoom => '관전자 채팅'; @@ -2174,7 +2397,7 @@ class AppLocalizationsKo extends AppLocalizations { String get send => '전송'; @override - String get incrementInSeconds => '턴 당 추가 시간(초)'; + String get incrementInSeconds => '수 당 추가 시간(초)'; @override String get freeOnlineChess => '무료 온라인 체스'; @@ -2183,10 +2406,10 @@ class AppLocalizationsKo extends AppLocalizations { String get exportGames => '게임 내보내기'; @override - String get ratingRange => 'ELO 범위'; + String get ratingRange => '레이팅 범위'; @override - String get thisAccountViolatedTos => '이 계정은 Lichess 이용 약관을 위반했습니다.'; + String get thisAccountViolatedTos => '이 계정은 Lichess 이용 약관을 위반하였습니다'; @override String get openingExplorerAndTablebase => '오프닝 탐색 & 테이블베이스'; @@ -2195,19 +2418,19 @@ class AppLocalizationsKo extends AppLocalizations { String get takeback => '무르기'; @override - String get proposeATakeback => '무르기를 요청합니다'; + String get proposeATakeback => '무르기 요청'; @override - String get takebackPropositionSent => '무르기 요청을 보냈습니다'; + String get takebackPropositionSent => '무르기 요청 전송됨'; @override - String get takebackPropositionDeclined => '무르기 요청이 거절됐습니다'; + String get takebackPropositionDeclined => '무르기 요청 거절됨'; @override - String get takebackPropositionAccepted => '무르기 요청이 승낙됐습니다'; + String get takebackPropositionAccepted => '무르기 요청 승낙됨'; @override - String get takebackPropositionCanceled => '무르기 요청이 취소됐습니다'; + String get takebackPropositionCanceled => '무르기 요청 취소됨'; @override String get yourOpponentProposesATakeback => '상대가 무르기를 요청합니다'; @@ -2304,16 +2527,16 @@ class AppLocalizationsKo extends AppLocalizations { String get averageElo => '평균 레이팅'; @override - String get location => '주소'; + String get location => '위치'; @override - String get filterGames => '필터'; + String get filterGames => '대국 필터'; @override String get reset => '초기화'; @override - String get apply => '적용'; + String get apply => '저장'; @override String get save => '저장하기'; @@ -2379,7 +2602,7 @@ class AppLocalizationsKo extends AppLocalizations { String get retry => '재시도'; @override - String get reconnecting => '연결 재시도 중'; + String get reconnecting => '연결 중'; @override String get noNetwork => '오프라인'; @@ -2524,7 +2747,7 @@ class AppLocalizationsKo extends AppLocalizations { String get winRate => '승률'; @override - String get berserkRate => '버서크율'; + String get berserkRate => '버서크 비율'; @override String get performance => '퍼포먼스 레이팅'; @@ -2600,7 +2823,7 @@ class AppLocalizationsKo extends AppLocalizations { } @override - String get ifNoneLeaveEmpty => '없으면 무시하세요'; + String get ifNoneLeaveEmpty => '없다면 비워두세요'; @override String get profile => '프로필'; @@ -2609,22 +2832,22 @@ class AppLocalizationsKo extends AppLocalizations { String get editProfile => '프로필 수정'; @override - String get realName => '본명'; + String get realName => '실명'; @override - String get setFlair => 'Set your flair'; + String get setFlair => '아이콘을 선택하세요'; @override - String get flair => 'Flair'; + String get flair => '아이콘'; @override - String get youCanHideFlair => 'There is a setting to hide all user flairs across the entire site.'; + String get youCanHideFlair => '전체 사이트에서 모든 유저의 아이콘을 숨기는 설정이 있습니다.'; @override String get biography => '소개'; @override - String get countryRegion => '국가/지역'; + String get countryRegion => '국가 또는 지역'; @override String get thankYou => '감사합니다!'; @@ -2633,13 +2856,13 @@ class AppLocalizationsKo extends AppLocalizations { String get socialMediaLinks => '소셜 미디어 링크'; @override - String get oneUrlPerLine => '한 줄에 1개 URL'; + String get oneUrlPerLine => '한 줄에 당 URL 1개'; @override - String get inlineNotation => '기보를 가로쓰기'; + String get inlineNotation => '기보법 가로쓰기'; @override - String get makeAStudy => '안전하게 보관하고 공유하려면 스터디를 만들어 보세요.'; + String get makeAStudy => '안전하게 보관하고 공유하려면 공부를 만들어 보세요.'; @override String get clearSavedMoves => '저장된 움직임 삭제'; @@ -2669,7 +2892,7 @@ class AppLocalizationsKo extends AppLocalizations { String get puzzles => '퍼즐'; @override - String get onlineBots => 'Online bots'; + String get onlineBots => '온라인 봇'; @override String get name => '이름'; @@ -2690,19 +2913,19 @@ class AppLocalizationsKo extends AppLocalizations { String get yes => '예'; @override - String get website => 'Website'; + String get website => '웹사이트'; @override - String get mobile => 'Mobile'; + String get mobile => '모바일'; @override String get help => '힌트:'; @override - String get createANewTopic => '새 토픽'; + String get createANewTopic => '새 주제 만들기'; @override - String get topics => '토픽'; + String get topics => '주제'; @override String get posts => '글'; @@ -2726,7 +2949,7 @@ class AppLocalizationsKo extends AppLocalizations { String get message => '내용'; @override - String get createTheTopic => '새 토픽 생성'; + String get createTheTopic => '새 주제 생성'; @override String get reportAUser => '사용자 신고'; @@ -2750,7 +2973,13 @@ class AppLocalizationsKo extends AppLocalizations { String get other => '기타'; @override - String get reportDescriptionHelp => '게임 URL 주소를 붙여넣으시고 해당 사용자가 무엇을 잘못했는지 설명해 주세요.'; + String get reportCheatBoostHelp => '게임 URL 주소를 붙여넣으시고 해당 사용자가 무엇을 잘못했는지 설명해 주세요. 그냥 \"그들이 부정행위를 했어요\" 라고만 말하지 말고, 어떻게 당신이 이 결론에 도달하게 됐는지 알려주세요.'; + + @override + String get reportUsernameHelp => '왜 이 사용자의 이름이 불쾌한지 설명해주세요. 그저 \"불쾌해요/부적절해요\"라고만 말하지 마세요, 대신 왜 이런 결론에 도달했는지 말씀해 주세요. 단어가 난해하거나, 영어가 아니거나, 은어이거나, 문화적/역사적 배경이 있는 경우 특히 중요합니다.'; + + @override + String get reportProcessedFasterInEnglish => '귀하의 신고가 영어로 적혀있을 경우 빠르게 처리될 것입니다.'; @override String get error_provideOneCheatedGameLink => '부정행위가 존재하는 게임의 링크를 적어도 하나는 적어주세요.'; @@ -2766,7 +2995,7 @@ class AppLocalizationsKo extends AppLocalizations { } @override - String get thisTopicIsNowClosed => '이 토픽은 닫혔습니다.'; + String get thisTopicIsNowClosed => '이 주제는 닫혔습니다.'; @override String get blog => '블로그'; @@ -2814,7 +3043,7 @@ class AppLocalizationsKo extends AppLocalizations { String get clockInitialTime => '기본 시간'; @override - String get clockIncrement => '한 수당 증가하는 시간'; + String get clockIncrement => '수 당 추가 시간'; @override String get privacy => '보안'; @@ -2853,7 +3082,7 @@ class AppLocalizationsKo extends AppLocalizations { String get outsideTheBoard => '보드 바깥쪽에'; @override - String get allSquaresOfTheBoard => 'All squares of the board'; + String get allSquaresOfTheBoard => '보드의 모든 칸'; @override String get onSlowGames => '느린 게임에서만'; @@ -2909,10 +3138,10 @@ class AppLocalizationsKo extends AppLocalizations { String get human => '인간'; @override - String get computer => '인공지능'; + String get computer => '컴퓨터'; @override - String get side => '진영'; + String get side => '색'; @override String get clock => '시계'; @@ -2924,7 +3153,7 @@ class AppLocalizationsKo extends AppLocalizations { String get learnMenu => '배우기'; @override - String get studyMenu => '연구'; + String get studyMenu => '공부'; @override String get practice => '연습'; @@ -2936,7 +3165,7 @@ class AppLocalizationsKo extends AppLocalizations { String get tools => '도구'; @override - String get increment => '시간 증가'; + String get increment => '추가 시간'; @override String get error_unknown => '잘못된 값'; @@ -2948,7 +3177,7 @@ class AppLocalizationsKo extends AppLocalizations { String get error_email => '이메일 주소가 유효하지 않습니다'; @override - String get error_email_acceptable => '이 이메일 주소는 받을 수 없습니다. 다시 확인후 시도해주세요.'; + String get error_email_acceptable => '이 이메일 주소는 수용 불가합니다. 확인후 다시 시도해주세요.'; @override String get error_email_unique => '이메일 주소가 유효하지 않거나 이미 등록되었습니다'; @@ -3056,51 +3285,51 @@ class AppLocalizationsKo extends AppLocalizations { String get sourceCode => '소스 코드'; @override - String get simultaneousExhibitions => '동시대국'; + String get simultaneousExhibitions => '다면기'; @override - String get host => '호스트'; + String get host => '주최자'; @override String hostColorX(String param) { - return '호스트의 색: $param'; + return '주최자 색: $param'; } @override - String get yourPendingSimuls => '대기 중인 동시대국'; + String get yourPendingSimuls => '대기 중인 다면기'; @override - String get createdSimuls => '새롭게 생성된 동시대국'; + String get createdSimuls => '새롭게 생성된 다면기'; @override - String get hostANewSimul => '새 동시대국을 생성하기'; + String get hostANewSimul => '새 다면기 주최하기'; @override - String get signUpToHostOrJoinASimul => '동시대국을 생성/참가하려면 로그인하세요'; + String get signUpToHostOrJoinASimul => '다면기를 주최/참가하려면 로그인하세요'; @override - String get noSimulFound => '동시대국을 찾을 수 없습니다'; + String get noSimulFound => '다면기를 찾을 수 없습니다'; @override - String get noSimulExplanation => '존재하지 않는 동시대국입니다.'; + String get noSimulExplanation => '존재하지 않는 다면기입니다.'; @override - String get returnToSimulHomepage => '동시대국 홈으로 돌아가기'; + String get returnToSimulHomepage => '다면기 홈으로 돌아가기'; @override - String get aboutSimul => '동시대국에서는 1인의 플레이어가 여러 플레이어와 대국을 벌입니다.'; + String get aboutSimul => '다면기에서는 1인의 플레이어가 여러 플레이어와 대국을 벌입니다.'; @override - String get aboutSimulImage => '50명의 상대 중, 피셔는 47국을 승리하였고, 2국은 무승부였으며 1국만을 패배하였습니다.'; + String get aboutSimulImage => '피셔는 50명의 상대 중, 47국을 승리하였고, 2국은 무승부였으며 1국만 패하였습니다.'; @override - String get aboutSimulRealLife => '이 동시대국의 개념은 실제 동시대국과 동일합니다. 실제로 1인 플레이어는 테이블을 넘기며 한 수씩 둡니다.'; + String get aboutSimulRealLife => '이 컨셉은 실제 이벤트들을 본딴 것입니다. 실제 경기에서는 다면기 주최자가 테이블을 돌아다니며 한 수씩 둡니다.'; @override - String get aboutSimulRules => '동시대국이 시작되면 모든 플레이어가 호스트와 게임을 합니다. 동시대국은 모든 플레이어와 게임이 끝나면 종료됩니다.'; + String get aboutSimulRules => '다면기가 시작되면, 모든 플레이어가 주최자와 대국을 합니다. 다면기는 모든 플레이어와 게임이 끝나면 종료됩니다.'; @override - String get aboutSimulSettings => '동시대국은 캐주얼 전입니다. 재대결, 무르기, 시간추가를 할 수 없습니다.'; + String get aboutSimulSettings => '다면기는 항상 캐주얼전입니다. 재대결, 무르기, 시간추가를 할 수 없습니다.'; @override String get create => '생성'; @@ -3151,7 +3380,7 @@ class AppLocalizationsKo extends AppLocalizations { String get keyGoToStartOrEnd => '처음/끝으로 가기'; @override - String get keyCycleSelectedVariation => 'Cycle selected variation'; + String get keyCycleSelectedVariation => '선택된 바리에이션 순환하기'; @override String get keyShowOrHideComments => '댓글 표시/숨기기'; @@ -3175,22 +3404,22 @@ class AppLocalizationsKo extends AppLocalizations { String get keyNextInaccuracy => '다음 부정확한 수'; @override - String get keyPreviousBranch => 'Previous branch'; + String get keyPreviousBranch => '이전 부'; @override - String get keyNextBranch => 'Next branch'; + String get keyNextBranch => '다음 부'; @override - String get toggleVariationArrows => 'Toggle variation arrows'; + String get toggleVariationArrows => '바리에이션 화살표 표시하기'; @override - String get cyclePreviousOrNextVariation => 'Cycle previous/next variation'; + String get cyclePreviousOrNextVariation => '이전/다음 바리에이션 순환하기'; @override - String get toggleGlyphAnnotations => 'Toggle move annotations'; + String get toggleGlyphAnnotations => '이동 주석 토글하기'; @override - String get togglePositionAnnotations => 'Toggle position annotations'; + String get togglePositionAnnotations => '위치 주석 토글하기'; @override String get variationArrowsInfo => '변형 화살표를 사용하면 이동 목록을 사용하지 않고 탐색이 가능합니다.'; @@ -3363,7 +3592,7 @@ class AppLocalizationsKo extends AppLocalizations { String get playChessEverywhere => '어디에서나 체스를 즐기세요'; @override - String get asFreeAsLichess => 'lichess처럼 무료입니다'; + String get asFreeAsLichess => 'Lichess처럼 무료예요'; @override String get builtForTheLoveOfChessNotMoney => '오직 체스에 대한 열정으로 만들어졌습니다'; @@ -3384,13 +3613,13 @@ class AppLocalizationsKo extends AppLocalizations { String get bulletBlitzClassical => '불릿, 블리츠, 클래식 방식 지원'; @override - String get correspondenceChess => '우편 체스 지원'; + String get correspondenceChess => '긴 대국 체스'; @override String get onlineAndOfflinePlay => '온라인/오프라인 게임 모두 지원'; @override - String get viewTheSolution => '해답 보기'; + String get viewTheSolution => '정답 보기'; @override String get followAndChallengeFriends => '친구를 팔로우하고 도전하기'; @@ -3491,7 +3720,7 @@ class AppLocalizationsKo extends AppLocalizations { String get playChessInStyle => '스타일리시하게 체스하기'; @override - String get chessBasics => '체스 기본'; + String get chessBasics => '체스의 기본'; @override String get coaches => '코치'; @@ -3594,10 +3823,10 @@ class AppLocalizationsKo extends AppLocalizations { String get youCanDoBetter => '더 잘할 수 있어요'; @override - String get tryAnotherMoveForWhite => '흰색에게 또 다른 수를 찾아보세요'; + String get tryAnotherMoveForWhite => '백의 또 다른 수를 찾아보세요'; @override - String get tryAnotherMoveForBlack => '검은색에게 또 다른 수를 찾아보세요'; + String get tryAnotherMoveForBlack => '흑의 또 다른 수를 찾아보세요'; @override String get solution => '해답'; @@ -3609,13 +3838,13 @@ class AppLocalizationsKo extends AppLocalizations { String get noMistakesFoundForWhite => '백에게 악수는 없었습니다'; @override - String get noMistakesFoundForBlack => '흑에게 악수는 없었습니다'; + String get noMistakesFoundForBlack => '흑에게 실수는 없었습니다'; @override String get doneReviewingWhiteMistakes => '백의 악수 체크가 종료됨'; @override - String get doneReviewingBlackMistakes => '흑의 악수 체크가 종료됨'; + String get doneReviewingBlackMistakes => '흑의 실수 탐색이 종료됨'; @override String get doItAgain => '다시 하기'; @@ -3624,10 +3853,10 @@ class AppLocalizationsKo extends AppLocalizations { String get reviewWhiteMistakes => '백의 악수를 체크'; @override - String get reviewBlackMistakes => '흑의 악수를 체크'; + String get reviewBlackMistakes => '흑의 실수 리뷰'; @override - String get advantage => '이득'; + String get advantage => '이점'; @override String get opening => '오프닝'; @@ -3869,7 +4098,7 @@ class AppLocalizationsKo extends AppLocalizations { } @override - String get timeAlmostUp => '시간이 거의 다 되었습니다!'; + String get timeAlmostUp => '시간이 거의 다 되었어요!'; @override String get clickToRevealEmailAddress => '[이메일 주소를 보려면 클릭]'; @@ -3884,7 +4113,7 @@ class AppLocalizationsKo extends AppLocalizations { String get streamerManager => '스트리머 설정'; @override - String get cancelTournament => '토너먼트 취소'; + String get cancelTournament => '토너먼트 취소하기'; @override String get tournDescription => '토너먼트 설명'; @@ -3896,7 +4125,7 @@ class AppLocalizationsKo extends AppLocalizations { String get ratedFormHelp => '레이팅 게임을 합니다\n플레이어 레이팅에 영향을 줍니다'; @override - String get onlyMembersOfTeam => '팀 멤버만'; + String get onlyMembersOfTeam => '팀 멤버들만'; @override String get noRestriction => '제한 없음'; @@ -3964,7 +4193,7 @@ class AppLocalizationsKo extends AppLocalizations { String get onlyTeamMembers => '팀 멤버만'; @override - String get navigateMoveTree => '수 탐색'; + String get navigateMoveTree => '수의 나무 탐색'; @override String get mouseTricks => '마우스 기능'; @@ -3988,7 +4217,7 @@ class AppLocalizationsKo extends AppLocalizations { String get showHelpDialog => '이 도움말 보기'; @override - String get reopenYourAccount => '계정 다시 활성화'; + String get reopenYourAccount => '계정 재활성화'; @override String get closedAccountChangedMind => '계정을 폐쇄한 후 마음이 바뀌었다면, 계정을 다시 활성화할 수 있는 기회가 한 번 있습니다.'; @@ -4009,11 +4238,11 @@ class AppLocalizationsKo extends AppLocalizations { String get tournamentEntryCode => '토너먼트 입장 코드'; @override - String get hangOn => '잠깐!'; + String get hangOn => '잠깐만요!'; @override String gameInProgress(String param) { - return '$param와 진행중인 게임이 있습니다.'; + return '$param와(과) 진행중인 대국이 있습니다.'; } @override @@ -4055,12 +4284,15 @@ class AppLocalizationsKo extends AppLocalizations { @override String get nothingToSeeHere => '지금은 여기에 볼 것이 없습니다.'; + @override + String get stats => '통계'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '당신의 상대가 게임을 나갔습니다. $count 초 후에 승리를 주장할 수 있습니다.', + other: '상대방이 게임을 나갔습니다. $count초 후에 승리를 취할 수 있습니다.', ); return '$_temp0'; } @@ -4070,7 +4302,7 @@ class AppLocalizationsKo extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count반수만에 체크메이트', + other: '$count개의 반수 후 체크메이트', ); return '$_temp0'; } @@ -4100,7 +4332,7 @@ class AppLocalizationsKo extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count 사소한 실수', + other: '$count 부정확한 수', ); return '$_temp0'; } @@ -4110,7 +4342,7 @@ class AppLocalizationsKo extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count명의 플레이어', + other: '플레이어 $count명', ); return '$_temp0'; } @@ -4120,7 +4352,7 @@ class AppLocalizationsKo extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count개의 게임', + other: '게임 $count개', ); return '$_temp0'; } @@ -4180,7 +4412,7 @@ class AppLocalizationsKo extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '순위는 매 $count분마다 갱신됩니다.', + other: '순위는 매 $count분마다 갱신됩니다', ); return '$_temp0'; } @@ -4210,7 +4442,7 @@ class AppLocalizationsKo extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count번의 레이팅 대국', + other: '$count번의 레이팅 게임', ); return '$_temp0'; } @@ -4270,7 +4502,7 @@ class AppLocalizationsKo extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count 토너먼트 포인트', + other: '$count 토너먼트 점수', ); return '$_temp0'; } @@ -4280,7 +4512,7 @@ class AppLocalizationsKo extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count 연구', + other: '$count 공부', ); return '$_temp0'; } @@ -4290,7 +4522,7 @@ class AppLocalizationsKo extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count 동시대국', + other: '$count 다면기', ); return '$_temp0'; } @@ -4440,7 +4672,7 @@ class AppLocalizationsKo extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count개의 언어 지원!', + other: '$count개의 언어를 지원합니다!', ); return '$_temp0'; } @@ -4657,9 +4889,510 @@ class AppLocalizationsKo extends AppLocalizations { @override String get streamerLichessStreamers => 'Lichess 스트리머'; + @override + String get studyPrivate => '비공개'; + + @override + String get studyMyStudies => '내 공부'; + + @override + String get studyStudiesIContributeTo => '내가 기여한 공부'; + + @override + String get studyMyPublicStudies => '내 공개 공부'; + + @override + String get studyMyPrivateStudies => '내 개인 공부'; + + @override + String get studyMyFavoriteStudies => '내가 즐겨찾는 공부'; + + @override + String get studyWhatAreStudies => '공부가 무엇인가요?'; + + @override + String get studyAllStudies => '모든 공부'; + + @override + String studyStudiesCreatedByX(String param) { + return '$param이(가) 만든 공부'; + } + + @override + String get studyNoneYet => '아직 없음'; + + @override + String get studyHot => '인기있는'; + + @override + String get studyDateAddedNewest => '추가된 날짜(새로운 순)'; + + @override + String get studyDateAddedOldest => '추가된 날짜(오래된 순)'; + + @override + String get studyRecentlyUpdated => '최근에 업데이트된 순'; + + @override + String get studyMostPopular => '인기 많은 순'; + + @override + String get studyAlphabetical => '알파벳 순'; + + @override + String get studyAddNewChapter => '새 챕터 추가하기'; + + @override + String get studyAddMembers => '멤버 추가'; + + @override + String get studyInviteToTheStudy => '공부에 초대'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => '당신이 아는 사람들이나 공부에 적극적으로 참여하고 싶은 사람들만 초대하세요.'; + + @override + String get studySearchByUsername => '사용자 이름으로 검색'; + + @override + String get studySpectator => '관전자'; + + @override + String get studyContributor => '기여자'; + + @override + String get studyKick => '강제 퇴장'; + + @override + String get studyLeaveTheStudy => '공부 나가기'; + + @override + String get studyYouAreNowAContributor => '당신은 이제 기여자입니다'; + + @override + String get studyYouAreNowASpectator => '당신은 이제 관전자입니다'; + + @override + String get studyPgnTags => 'PGN 태그'; + + @override + String get studyLike => '좋아요'; + + @override + String get studyUnlike => '좋아요 취소'; + + @override + String get studyNewTag => '새 태그'; + + @override + String get studyCommentThisPosition => '이 포지션에 댓글 달기'; + + @override + String get studyCommentThisMove => '이 수에 댓글 달기'; + + @override + String get studyAnnotateWithGlyphs => '기호로 주석 달기'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => '분석되기 너무 짧은 챕터입니다.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => '공부 기여자들만이 컴퓨터 분석을 요청할 수 있습니다.'; + + @override + String get studyGetAFullComputerAnalysis => '메인라인에 대한 전체 서버 컴퓨터 분석을 가져옵니다.'; + + @override + String get studyMakeSureTheChapterIsComplete => '챕터가 완료되었는지 확인하세요. 분석은 한번만 요청할 수 있습니다.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => '동기화된 모든 멤버들은 같은 포지션을 공유합니다'; + + @override + String get studyShareChanges => '관전자와 변경 사항을 공유하고 서버에 저장'; + + @override + String get studyPlaying => '대국 중'; + + @override + String get studyShowEvalBar => '평가 막대'; + + @override + String get studyFirst => '처음'; + + @override + String get studyPrevious => '이전'; + + @override + String get studyNext => '다음'; + + @override + String get studyLast => '마지막'; + @override String get studyShareAndExport => '공유 및 내보내기'; + @override + String get studyCloneStudy => '복제'; + + @override + String get studyStudyPgn => '공부 PGN'; + + @override + String get studyDownloadAllGames => '모든 게임 다운로드'; + + @override + String get studyChapterPgn => '챕터 PGN'; + + @override + String get studyCopyChapterPgn => 'PGN 복사'; + + @override + String get studyDownloadGame => '게임 다운로드'; + + @override + String get studyStudyUrl => '공부 URL'; + + @override + String get studyCurrentChapterUrl => '현재 챕터 URL'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => '포럼에 공유하려면 이 주소를 붙여넣으세요'; + + @override + String get studyStartAtInitialPosition => '처음 포지션에서 시작'; + + @override + String studyStartAtX(String param) { + return '$param에서 시작'; + } + + @override + String get studyEmbedInYourWebsite => '웹사이트 또는 블로그에 공유하기'; + + @override + String get studyReadMoreAboutEmbedding => '공유에 대한 상세 정보'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => '공개 공부들만 공유할 수 있습니다!'; + + @override + String get studyOpen => '열기'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1. $param2에서 가져옴'; + } + + @override + String get studyStudyNotFound => '공부를 찾을 수 없습니다'; + + @override + String get studyEditChapter => '챕터 편집하기'; + + @override + String get studyNewChapter => '새 챕터'; + + @override + String studyImportFromChapterX(String param) { + return '$param에서 가져오기'; + } + + @override + String get studyOrientation => '방향'; + + @override + String get studyAnalysisMode => '분석 모드'; + + @override + String get studyPinnedChapterComment => '챕터 댓글 고정하기'; + + @override + String get studySaveChapter => '챕터 저장'; + + @override + String get studyClearAnnotations => '주석 지우기'; + + @override + String get studyClearVariations => '파생 초기화'; + + @override + String get studyDeleteChapter => '챕터 지우기'; + + @override + String get studyDeleteThisChapter => '이 챕터를 지울까요? 되돌릴 수 없습니다!'; + + @override + String get studyClearAllCommentsInThisChapter => '이 챕터의 모든 코멘트와 기호를 지울까요?'; + + @override + String get studyRightUnderTheBoard => '보드 우하단에'; + + @override + String get studyNoPinnedComment => '없음'; + + @override + String get studyNormalAnalysis => '일반 분석'; + + @override + String get studyHideNextMoves => '다음 수 숨기기'; + + @override + String get studyInteractiveLesson => '상호 대화형 레슨'; + + @override + String studyChapterX(String param) { + return '챕터 $param'; + } + + @override + String get studyEmpty => '비어있음'; + + @override + String get studyStartFromInitialPosition => '초기 포지션에서 시작'; + + @override + String get studyEditor => '편집기'; + + @override + String get studyStartFromCustomPosition => '커스텀 포지션에서 시작'; + + @override + String get studyLoadAGameByUrl => 'URL로 게임 가져오기'; + + @override + String get studyLoadAPositionFromFen => 'FEN으로 포지션 가져오기'; + + @override + String get studyLoadAGameFromPgn => 'PGN으로 게임 가져오기'; + + @override + String get studyAutomatic => '자동'; + + @override + String get studyUrlOfTheGame => '한 줄에 하나씩, 게임의 URL'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return '$param1 또는 $param2에서 게임 로드'; + } + + @override + String get studyCreateChapter => '챕터 만들기'; + + @override + String get studyCreateStudy => '공부 만들기'; + + @override + String get studyEditStudy => '공부 편집하기'; + + @override + String get studyVisibility => '공개 설정'; + + @override + String get studyPublic => '공개'; + + @override + String get studyUnlisted => '비공개'; + + @override + String get studyInviteOnly => '초대만'; + + @override + String get studyAllowCloning => '복제 허용'; + + @override + String get studyNobody => '아무도'; + + @override + String get studyOnlyMe => '나만'; + + @override + String get studyContributors => '기여자만'; + + @override + String get studyMembers => '멤버만'; + + @override + String get studyEveryone => '모두'; + + @override + String get studyEnableSync => '동기화 사용'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => '예: 모두가 같은 위치를 봅니다'; + + @override + String get studyNoLetPeopleBrowseFreely => '아니요: 사람들이 자유롭게 이동할 수 있습니다'; + + @override + String get studyPinnedStudyComment => '고정된 댓글'; + @override String get studyStart => '시작'; + + @override + String get studySave => '저장'; + + @override + String get studyClearChat => '채팅 기록 지우기'; + + @override + String get studyDeleteTheStudyChatHistory => '공부 채팅 히스토리를 지울까요? 되돌릴 수 없습니다!'; + + @override + String get studyDeleteStudy => '공부 삭제'; + + @override + String studyConfirmDeleteStudy(String param) { + return '모든 공부를 삭제할까요? 복구할 수 없습니다! 확인을 위해서 공부의 이름을 입력하세요: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => '어디에서 공부하시겠습니까?'; + + @override + String get studyGoodMove => '좋은 수'; + + @override + String get studyMistake => '실수'; + + @override + String get studyBrilliantMove => '매우 좋은 수'; + + @override + String get studyBlunder => '블런더'; + + @override + String get studyInterestingMove => '흥미로운 수'; + + @override + String get studyDubiousMove => '애매한 수'; + + @override + String get studyOnlyMove => '유일한 수'; + + @override + String get studyZugzwang => '추크추방'; + + @override + String get studyEqualPosition => '동등한 포지션'; + + @override + String get studyUnclearPosition => '불확실한 포지션'; + + @override + String get studyWhiteIsSlightlyBetter => '백이 미세하게 좋음'; + + @override + String get studyBlackIsSlightlyBetter => '흑이 미세하게 좋음'; + + @override + String get studyWhiteIsBetter => '백이 유리함'; + + @override + String get studyBlackIsBetter => '흑이 유리함'; + + @override + String get studyWhiteIsWinning => '백이 이기고 있음'; + + @override + String get studyBlackIsWinning => '흑이 이기고 있음'; + + @override + String get studyNovelty => '새로운 수'; + + @override + String get studyDevelopment => '발전'; + + @override + String get studyInitiative => '주도권'; + + @override + String get studyAttack => '공격'; + + @override + String get studyCounterplay => '카운터플레이'; + + @override + String get studyTimeTrouble => '시간이 부족함'; + + @override + String get studyWithCompensation => '보상이 있음'; + + @override + String get studyWithTheIdea => '아이디어'; + + @override + String get studyNextChapter => '다음 챕터'; + + @override + String get studyPrevChapter => '이전 챕터'; + + @override + String get studyStudyActions => '공부 액션'; + + @override + String get studyTopics => '주제'; + + @override + String get studyMyTopics => '내 주제'; + + @override + String get studyPopularTopics => '인기 주제'; + + @override + String get studyManageTopics => '주제 관리'; + + @override + String get studyBack => '뒤로'; + + @override + String get studyPlayAgain => '다시 플레이'; + + @override + String get studyWhatWouldYouPlay => '이 포지션에서 무엇을 하시겠습니까?'; + + @override + String get studyYouCompletedThisLesson => '축하합니다! 이 레슨을 완료했습니다.'; + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 챕터', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 게임', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '멤버 $count명', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'PGN을 여기에 붙여넣으세요. 최대 $count 게임까지 가능합니다.', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_lb.dart b/lib/l10n/l10n_lb.dart index 26186fd760..e7c01e58c0 100644 --- a/lib/l10n/l10n_lb.dart +++ b/lib/l10n/l10n_lb.dart @@ -103,9 +103,6 @@ class AppLocalizationsLb extends AppLocalizations { @override String get mobileCancelTakebackOffer => 'Cancel takeback offer'; - @override - String get mobileCancelDrawOffer => 'Cancel draw offer'; - @override String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @@ -142,7 +139,7 @@ class AppLocalizationsLb extends AppLocalizations { String get mobileGreetingWithoutName => 'Moien'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobilePrefMagnifyDraggedPiece => 'Gezunne Figur vergréisseren'; @override String get activityActivity => 'Verlaf'; @@ -246,6 +243,17 @@ class AppLocalizationsLb extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Huet $count $param2-Fernschachpartië gespillt', + one: 'Huet $count $param2-Fernschachpartie gespillt', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,226 @@ class AppLocalizationsLb extends AppLocalizations { @override String get broadcastBroadcasts => 'Iwwerdroungen'; + @override + String get broadcastMyBroadcasts => 'My broadcasts'; + @override String get broadcastLiveBroadcasts => 'Live Turnéier Iwwerdroungen'; + @override + String get broadcastBroadcastCalendar => 'Broadcast calendar'; + + @override + String get broadcastNewBroadcast => 'Nei Live Iwwerdroung'; + + @override + String get broadcastSubscribedBroadcasts => 'Subscribed broadcasts'; + + @override + String get broadcastAboutBroadcasts => 'About broadcasts'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'How to use Lichess Broadcasts.'; + + @override + String get broadcastTheNewRoundHelp => 'The new round will have the same members and contributors as the previous one.'; + + @override + String get broadcastAddRound => 'Ronn hinzufügen'; + + @override + String get broadcastOngoing => 'Am Gaang'; + + @override + String get broadcastUpcoming => 'Demnächst'; + + @override + String get broadcastCompleted => 'Eriwwer'; + + @override + String get broadcastCompletedHelp => 'Lichess detects round completion, but can get it wrong. Use this to set it manually.'; + + @override + String get broadcastRoundName => 'Ronnennumm'; + + @override + String get broadcastRoundNumber => 'Ronnennummer'; + + @override + String get broadcastTournamentName => 'Turnéiernumm'; + + @override + String get broadcastTournamentDescription => 'Kuerz Turnéierbeschreiwung'; + + @override + String get broadcastFullDescription => 'Komplett Turnéierbeschreiwung'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Optional laang Beschreiwung vum Turnéier. $param1 ass disponibel. Längt muss manner wéi $param2 Buschtawen sinn.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN Source URL'; + + @override + String get broadcastSourceUrlHelp => 'URL déi Lichess checkt fir PGN à jour ze halen. Muss ëffentlech iwwer Internet zougänglech sinn.'; + + @override + String get broadcastSourceGameIds => 'Bis zu 64 Lichess-Partie-IDen, duerch Espacë getrennt.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Startdatum vum Turnéier an der lokaler Zäitzon: $param'; + } + + @override + String get broadcastStartDateHelp => 'Optional, wann du wees wéini den Turnéier ufänkt'; + + @override + String get broadcastCurrentGameUrl => 'URL vun der aktueller Partie'; + + @override + String get broadcastDownloadAllRounds => 'All Ronnen eroflueden'; + + @override + String get broadcastResetRound => 'Ronn zerécksetzen'; + + @override + String get broadcastDeleteRound => 'Ronn läschen'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Dës Ronn an hir Partien endgülteg läschen.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'All Partien vun dëser Ronn läschen. D\'Quell muss aktiv sinn fir se ze rekreéieren.'; + + @override + String get broadcastEditRoundStudy => 'Ronnen-Etüd modifiéieren'; + + @override + String get broadcastDeleteTournament => 'Dësen Turnéier läschen'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'De ganzen Turnéier definitiv läschen, all seng Ronnen an all seng Partien.'; + + @override + String get broadcastShowScores => 'Show players scores based on game results'; + + @override + String get broadcastReplacePlayerTags => 'Optional: Spillernimm, Wäertungen an Titelen ersetzen'; + + @override + String get broadcastFideFederations => 'FIDE-Federatiounen'; + + @override + String get broadcastTop10Rating => 'Top 10 rating'; + + @override + String get broadcastFidePlayers => 'FIDE-Spiller'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE-Spiller net tfonnt'; + + @override + String get broadcastFideProfile => 'FIDE-Profil'; + + @override + String get broadcastFederation => 'Federatioun'; + + @override + String get broadcastAgeThisYear => 'Alter dëst Joer'; + + @override + String get broadcastUnrated => 'Ongewäert'; + + @override + String get broadcastRecentTournaments => 'Rezent Turnéieren'; + + @override + String get broadcastOpenLichess => 'Open in Lichess'; + + @override + String get broadcastTeams => 'Ekippen'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Iwwersiicht'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Turnéierbild eroplueden'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Fänkt no $param un'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Offiziell Websäit'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'Méi Optiounen op der $param'; + } + + @override + String get broadcastWebmastersPage => 'Webmaster-Säit'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Partien an dësem Turnéier'; + + @override + String get broadcastScore => 'Score'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Iwwerdroungen', + one: '$count Iwwerdroung', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Erausfuerderungen: $param1'; @@ -1390,10 +1615,10 @@ class AppLocalizationsLb extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'De Géigner huet eng begrenzten Unzuel un Zich an all Zuch verschlechtert seng Positioun.'; @override - String get puzzleThemeHealthyMix => 'Gesonde Mix'; + String get puzzleThemeMix => 'Gesonde Mix'; @override - String get puzzleThemeHealthyMixDescription => 'E bësse vun allem. Du weess net wat dech erwaart, dowéinst muss op alles preparéiert sinn! Genau wéi bei echte Partien.'; + String get puzzleThemeMixDescription => 'E bësse vun allem. Du weess net wat dech erwaart, dowéinst muss op alles preparéiert sinn! Genau wéi bei echte Partien.'; @override String get puzzleThemePlayerGames => 'Partie vu Spiller'; @@ -1797,9 +2022,6 @@ class AppLocalizationsLb extends AppLocalizations { @override String get removesTheDepthLimit => 'Entfernt d\'Déifenbegrenzung an hält däin Computer waarm'; - @override - String get engineManager => 'Engineverwaltung'; - @override String get blunder => 'Gaffe'; @@ -2063,6 +2285,9 @@ class AppLocalizationsLb extends AppLocalizations { @override String get gamesPlayed => 'Partien gespillt'; + @override + String get ok => 'OK'; + @override String get cancel => 'Annuléieren'; @@ -2772,7 +2997,13 @@ class AppLocalizationsLb extends AppLocalizations { String get other => 'Aner'; @override - String get reportDescriptionHelp => 'Post den Link vun Partie(n) and erklär wat den Problem mat dësem Benotzer sengem Verhalen ass. So net just \"Hien fuddelt\", mee so eis wéi du zu dëser Konklusioun komm bass. Däin Rapport gëtt méi schnell veraarbecht wann en op Englesch ass.'; + String get reportCheatBoostHelp => 'Paste the link to the game(s) and explain what is wrong about this user\'s behaviour. Don\'t just say \"they cheat\", but tell us how you came to this conclusion.'; + + @override + String get reportUsernameHelp => 'Explain what about this username is offensive. Don\'t just say \"it\'s offensive/inappropriate\", but tell us how you came to this conclusion, especially if the insult is obfuscated, not in english, is in slang, or is a historical/cultural reference.'; + + @override + String get reportProcessedFasterInEnglish => 'Your report will be processed faster if written in English.'; @override String get error_provideOneCheatedGameLink => 'Wannechgelift gëff eis op mannst een Link zu enger Partie mat Bedruch.'; @@ -4077,6 +4308,9 @@ class AppLocalizationsLb extends AppLocalizations { @override String get nothingToSeeHere => 'Fir de Moment gëtt et hei näischt ze gesinn.'; + @override + String get stats => 'Statistiken'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4723,9 +4957,514 @@ class AppLocalizationsLb extends AppLocalizations { @override String get streamerLichessStreamers => 'Lichess Streamer'; + @override + String get studyPrivate => 'Privat'; + + @override + String get studyMyStudies => 'Meng Etüden'; + + @override + String get studyStudiesIContributeTo => 'Etüden, un deenen ech matwierken'; + + @override + String get studyMyPublicStudies => 'Meng öffentlech Etüden'; + + @override + String get studyMyPrivateStudies => 'Meng privat Etüden'; + + @override + String get studyMyFavoriteStudies => 'Meng Lieblingsetüden'; + + @override + String get studyWhatAreStudies => 'Wat sinn Etüden?'; + + @override + String get studyAllStudies => 'All Etüden'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Etüden kreéiert vun $param'; + } + + @override + String get studyNoneYet => 'Nach keng.'; + + @override + String get studyHot => 'Ugesot'; + + @override + String get studyDateAddedNewest => 'Veröffentlechungsdatum (am neisten)'; + + @override + String get studyDateAddedOldest => 'Veröffentlechungsdatum (am aalsten)'; + + @override + String get studyRecentlyUpdated => 'Rezent aktualiséiert'; + + @override + String get studyMostPopular => 'Am Beléiftsten'; + + @override + String get studyAlphabetical => 'Alphabetesch'; + + @override + String get studyAddNewChapter => 'Neit Kapitel bäifügen'; + + @override + String get studyAddMembers => 'Memberen hinzufügen'; + + @override + String get studyInviteToTheStudy => 'An d\'Etüd alueden'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Wannechgelift invitéier just Leit déi du kenns an déi aktiv un der Etüd matwierken wëllen.'; + + @override + String get studySearchByUsername => 'No Benotzernumm sichen'; + + @override + String get studySpectator => 'Zuschauer'; + + @override + String get studyContributor => 'Matwierkenden'; + + @override + String get studyKick => 'Rausgehéien'; + + @override + String get studyLeaveTheStudy => 'Etüd verloossen'; + + @override + String get studyYouAreNowAContributor => 'Du bass elo e Contributeur'; + + @override + String get studyYouAreNowASpectator => 'Du bass elo en Zuschauer'; + + @override + String get studyPgnTags => 'PGN Tags'; + + @override + String get studyLike => 'Gefällt mir'; + + @override + String get studyUnlike => 'Gefällt mer net méi'; + + @override + String get studyNewTag => 'Néien Tag'; + + @override + String get studyCommentThisPosition => 'Kommentéier des Positioun'; + + @override + String get studyCommentThisMove => 'Kommentéier dësen Zuch'; + + @override + String get studyAnnotateWithGlyphs => 'Mat Symboler kommentéieren'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'D\'Kapitel ass ze kuerz fir analyséiert ze ginn.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Just Etüden Matwierkender kënnen eng Computer Analyse ufroen.'; + + @override + String get studyGetAFullComputerAnalysis => 'Vollstänneg serversäiteg Computeranalyse vun der Haaptvariant erhalen.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Stell sécher dass d\'Kapitel vollstänneg ass. Du kanns eng Analyse just eemol ufroen.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'All SYNC Memberen gesinn déi selwecht Positioun'; + + @override + String get studyShareChanges => 'Deel Ännerungen mat den Zuschauer an späicher se um Server'; + + @override + String get studyPlaying => 'Lafend Partie'; + + @override + String get studyShowEvalBar => 'Evaluation bars'; + + @override + String get studyFirst => 'Éischt Säit'; + + @override + String get studyPrevious => 'Zeréck'; + + @override + String get studyNext => 'Weider'; + + @override + String get studyLast => 'Lescht Säit'; + @override String get studyShareAndExport => 'Deelen & exportéieren'; + @override + String get studyCloneStudy => 'Klonen'; + + @override + String get studyStudyPgn => 'Etüden PGN'; + + @override + String get studyDownloadAllGames => 'All Partien eroflueden'; + + @override + String get studyChapterPgn => 'Kapitel PGN'; + + @override + String get studyCopyChapterPgn => 'PGN kopéieren'; + + @override + String get studyDownloadGame => 'Partie eroflueden'; + + @override + String get studyStudyUrl => 'Etüden URL'; + + @override + String get studyCurrentChapterUrl => 'Aktuellt Kapitel URL'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Zum Anbetten an een Forum oder Blog afügen'; + + @override + String get studyStartAtInitialPosition => 'Mat Startpositioun ufänken'; + + @override + String studyStartAtX(String param) { + return 'Bei $param ufänken'; + } + + @override + String get studyEmbedInYourWebsite => 'An Websäit anbetten'; + + @override + String get studyReadMoreAboutEmbedding => 'Méi iwwer Anbetten liesen'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Just ëffentlech Etüden kënnen angebett ginn!'; + + @override + String get studyOpen => 'Opmaachen'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1, presentéiert vum $param2'; + } + + @override + String get studyStudyNotFound => 'Etüd net fonnt'; + + @override + String get studyEditChapter => 'Kapitel editéieren'; + + @override + String get studyNewChapter => 'Neit Kapitel'; + + @override + String studyImportFromChapterX(String param) { + return 'Importéieren aus $param'; + } + + @override + String get studyOrientation => 'Orientatioun'; + + @override + String get studyAnalysisMode => 'Analysemodus'; + + @override + String get studyPinnedChapterComment => 'Ugepinnten Kapitelkommentar'; + + @override + String get studySaveChapter => 'Kapitel späicheren'; + + @override + String get studyClearAnnotations => 'Annotatiounen läschen'; + + @override + String get studyClearVariations => 'Variante läschen'; + + @override + String get studyDeleteChapter => 'Kapitel läschen'; + + @override + String get studyDeleteThisChapter => 'Kapitel läschen? Et gëtt keen zeréck!'; + + @override + String get studyClearAllCommentsInThisChapter => 'All Kommentarer, Symboler an Zeechnungsformen an dësem Kapitel läschen?'; + + @override + String get studyRightUnderTheBoard => 'Direkt ënnert dem Briet'; + + @override + String get studyNoPinnedComment => 'Keng'; + + @override + String get studyNormalAnalysis => 'Normal Analyse'; + + @override + String get studyHideNextMoves => 'Nächst Zich verstoppen'; + + @override + String get studyInteractiveLesson => 'Interaktiv Übung'; + + @override + String studyChapterX(String param) { + return 'Kapitel $param'; + } + + @override + String get studyEmpty => 'Eidel'; + + @override + String get studyStartFromInitialPosition => 'Aus Startpositioun ufänken'; + + @override + String get studyEditor => 'Editor'; + + @override + String get studyStartFromCustomPosition => 'Aus benotzerdefinéierter Positioun ufänken'; + + @override + String get studyLoadAGameByUrl => 'Partien mat URL lueden'; + + @override + String get studyLoadAPositionFromFen => 'Positioun aus FEN lueden'; + + @override + String get studyLoadAGameFromPgn => 'Partien aus PGN lueden'; + + @override + String get studyAutomatic => 'Automatesch'; + + @override + String get studyUrlOfTheGame => 'URL vun den Partien, eng pro Zeil'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Partien vun $param1 oder $param2 lueden'; + } + + @override + String get studyCreateChapter => 'Kapitel kréieren'; + + @override + String get studyCreateStudy => 'Etüd kreéieren'; + + @override + String get studyEditStudy => 'Etüd änneren'; + + @override + String get studyVisibility => 'Visibilitéit'; + + @override + String get studyPublic => 'Ëffentlech'; + + @override + String get studyUnlisted => 'Ongelëscht'; + + @override + String get studyInviteOnly => 'Just mat Invitatioun'; + + @override + String get studyAllowCloning => 'Klonen erlaaben'; + + @override + String get studyNobody => 'Keen'; + + @override + String get studyOnlyMe => 'Just ech'; + + @override + String get studyContributors => 'Matwierkendender'; + + @override + String get studyMembers => 'Memberen'; + + @override + String get studyEveryone => 'Jiddereen'; + + @override + String get studyEnableSync => 'Synchronisatioun aktivéieren'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Jo: Jiddereen op der selwechter Positioun halen'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Nee: Leit individuell browsen loossen'; + + @override + String get studyPinnedStudyComment => 'Ugepinnten Etüdenkommentar'; + @override String get studyStart => 'Lass'; + + @override + String get studySave => 'Späicheren'; + + @override + String get studyClearChat => 'Chat läschen'; + + @override + String get studyDeleteTheStudyChatHistory => 'Etüdenchat läschen? Et gëtt keen zeréck!'; + + @override + String get studyDeleteStudy => 'Etüd läschen'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Komplett Etüd läschen? Et gëett keen zeréck! Tipp den Numm vun der Etüd an fir ze konfirméieren: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Wéieng Etüd wëlls du benotzen?'; + + @override + String get studyGoodMove => 'Gudden Zuch'; + + @override + String get studyMistake => 'Feeler'; + + @override + String get studyBrilliantMove => 'Brillianten Zuch'; + + @override + String get studyBlunder => 'Gaffe'; + + @override + String get studyInterestingMove => 'Interessanten Zuch'; + + @override + String get studyDubiousMove => 'Dubiosen Zuch'; + + @override + String get studyOnlyMove => 'Eenzegen Zuch'; + + @override + String get studyZugzwang => 'Zugzwang'; + + @override + String get studyEqualPosition => 'Ausgeglach Positioun'; + + @override + String get studyUnclearPosition => 'Onkloer Positioun'; + + @override + String get studyWhiteIsSlightlyBetter => 'Wäiss steet liicht besser'; + + @override + String get studyBlackIsSlightlyBetter => 'Schwaarz steet liicht besser'; + + @override + String get studyWhiteIsBetter => 'Wäiss ass besser'; + + @override + String get studyBlackIsBetter => 'Schwaarz ass besser'; + + @override + String get studyWhiteIsWinning => 'Wéiss steet op Gewënn'; + + @override + String get studyBlackIsWinning => 'Schwaarz steet op Gewënn'; + + @override + String get studyNovelty => 'Neiheet'; + + @override + String get studyDevelopment => 'Entwécklung'; + + @override + String get studyInitiative => 'Initiativ'; + + @override + String get studyAttack => 'Ugrëff'; + + @override + String get studyCounterplay => 'Géigespill'; + + @override + String get studyTimeTrouble => 'Zäitdrock'; + + @override + String get studyWithCompensation => 'Mat Kompensatioun'; + + @override + String get studyWithTheIdea => 'Mat der Iddi'; + + @override + String get studyNextChapter => 'Nächst Kapitel'; + + @override + String get studyPrevChapter => 'Kapitel virdrun'; + + @override + String get studyStudyActions => 'Etüden-Aktiounen'; + + @override + String get studyTopics => 'Themen'; + + @override + String get studyMyTopics => 'Meng Themen'; + + @override + String get studyPopularTopics => 'Beléift Themen'; + + @override + String get studyManageTopics => 'Themen managen'; + + @override + String get studyBack => 'Zeréck'; + + @override + String get studyPlayAgain => 'Nach eng Kéier spillen'; + + @override + String get studyWhatWouldYouPlay => 'Wat géifs du an dëser Positioun spillen?'; + + @override + String get studyYouCompletedThisLesson => 'Gudd gemaach! Du hues dës Übung ofgeschloss.'; + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Kapitel', + one: '$count Kapitel', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Partien', + one: '$count Partie', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Memberen', + one: '$count Member', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'PGN Text hei asetzen, bis zu $count Partien', + one: 'PGN Text hei asetzen, bis zu $count Partie', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_lt.dart b/lib/l10n/l10n_lt.dart index 37288a6162..df24d46e02 100644 --- a/lib/l10n/l10n_lt.dart +++ b/lib/l10n/l10n_lt.dart @@ -103,9 +103,6 @@ class AppLocalizationsLt extends AppLocalizations { @override String get mobileCancelTakebackOffer => 'Cancel takeback offer'; - @override - String get mobileCancelDrawOffer => 'Cancel draw offer'; - @override String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @@ -262,6 +259,17 @@ class AppLocalizationsLt extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Completed $count $param2 correspondence games', + one: 'Completed $count $param2 correspondence game', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -382,9 +390,228 @@ class AppLocalizationsLt extends AppLocalizations { @override String get broadcastBroadcasts => 'Transliacijos'; + @override + String get broadcastMyBroadcasts => 'Mano transliacijos'; + @override String get broadcastLiveBroadcasts => 'Vykstančios turnyrų transliacijos'; + @override + String get broadcastBroadcastCalendar => 'Broadcast calendar'; + + @override + String get broadcastNewBroadcast => 'Nauja transliacija'; + + @override + String get broadcastSubscribedBroadcasts => 'Prenumeruojamos transliacijos'; + + @override + String get broadcastAboutBroadcasts => 'Apie transliacijas'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Kaip naudotis Lichess transliacijomis.'; + + @override + String get broadcastTheNewRoundHelp => 'Naujajame ture bus tie patys nariai ir bendradarbiai, kaip ir ankstesniame.'; + + @override + String get broadcastAddRound => 'Pridėti raundą'; + + @override + String get broadcastOngoing => 'Vykstančios'; + + @override + String get broadcastUpcoming => 'Artėjančios'; + + @override + String get broadcastCompleted => 'Pasibaigę'; + + @override + String get broadcastCompletedHelp => 'Lichess aptiko turo užbaigimą, bet galimai klaidingai. Naudokite tai, norėdami nustatyti rankiniu būdu.'; + + @override + String get broadcastRoundName => 'Raundo pavadinimas'; + + @override + String get broadcastRoundNumber => 'Raundo numeris'; + + @override + String get broadcastTournamentName => 'Turnyro pavadinimas'; + + @override + String get broadcastTournamentDescription => 'Trumpas turnyro aprašymas'; + + @override + String get broadcastFullDescription => 'Pilnas renginio aprašymas'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Neprivalomas pilnas transliacijos aprašymas. Galima naudoti $param1. Ilgis negali viršyti $param2 simbolių.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN šaltinio URL'; + + @override + String get broadcastSourceUrlHelp => 'URL, į kurį „Lichess“ kreipsis gauti PGN atnaujinimus. Privalo būti viešai pasiekiamas internete.'; + + @override + String get broadcastSourceGameIds => 'Iki 64 Lichess žaidimo ID, atskirtų tarpais.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Start date in the tournament local timezone: $param'; + } + + @override + String get broadcastStartDateHelp => 'Neprivaloma; tik jeigu žinote, kada prasideda renginys'; + + @override + String get broadcastCurrentGameUrl => 'Dabartinio žaidimo adresas'; + + @override + String get broadcastDownloadAllRounds => 'Atsisiųsti visus raundus'; + + @override + String get broadcastResetRound => 'Atstatyti raundą'; + + @override + String get broadcastDeleteRound => 'Ištrinti raundą'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Užtikrintai ištrinti raundą ir jo partijas.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Ištrinti visas partijas šiame raunde. Norint jas perkurti reikės aktyvaus šaltinio.'; + + @override + String get broadcastEditRoundStudy => 'Keisti raundo studiją'; + + @override + String get broadcastDeleteTournament => 'Ištrinti šį turnyrą'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Užtikrintai ištrinti visą turnyrą, visus raundus ir visas jų partijas.'; + + @override + String get broadcastShowScores => 'Rodyti žaidėjų balus pagal partijų rezultatus'; + + @override + String get broadcastReplacePlayerTags => 'Pasirenkama: pakeiskite žaidėjų vardus, reitingus ir titulus'; + + @override + String get broadcastFideFederations => 'FIDE federacijos'; + + @override + String get broadcastTop10Rating => '10 aukščiausių reitingų'; + + @override + String get broadcastFidePlayers => 'FIDE žaidėjai'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE žaidėjas nerastas'; + + @override + String get broadcastFideProfile => 'FIDE profilis'; + + @override + String get broadcastFederation => 'Federacija'; + + @override + String get broadcastAgeThisYear => 'Amžius šiemet'; + + @override + String get broadcastUnrated => 'Nereitinguota(s)'; + + @override + String get broadcastRecentTournaments => 'Neseniai sukurti turnyrai'; + + @override + String get broadcastOpenLichess => 'Open in Lichess'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Overview'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Official website'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'Score'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count transliacijų', + many: '$count transliacijos', + few: '$count transliacijos', + one: '$count transliacija', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Iššūkiai: $param1'; @@ -1434,10 +1661,10 @@ class AppLocalizationsLt extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Priešininkas apribotas ėjimais, kuriuos gali padaryti, ir visi jo ėjimai tik pabloginą jo poziciją.'; @override - String get puzzleThemeHealthyMix => 'Visko po truputį'; + String get puzzleThemeMix => 'Visko po truputį'; @override - String get puzzleThemeHealthyMixDescription => 'Nežinote ko tikėtis, todėl būkite pasiruošę bet kam! Visai kaip tikruose žaidimuose.'; + String get puzzleThemeMixDescription => 'Nežinote ko tikėtis, todėl būkite pasiruošę bet kam! Visai kaip tikruose žaidimuose.'; @override String get puzzleThemePlayerGames => 'Žaidėjų žaidimai'; @@ -1493,7 +1720,7 @@ class AppLocalizationsLt extends AppLocalizations { String get waitingForOpponent => 'Laukiama varžovo'; @override - String get orLetYourOpponentScanQrCode => 'Arba leiskite priešininkui nuskanuoti šį QR kodą'; + String get orLetYourOpponentScanQrCode => 'Arba leiskite priešininkui nuskenuoti šį QR kodą'; @override String get waiting => 'Laukiama'; @@ -1510,7 +1737,7 @@ class AppLocalizationsLt extends AppLocalizations { String get level => 'Lygis'; @override - String get strength => 'Stiprumas'; + String get strength => 'Pasipriešinimo stiprumas'; @override String get toggleTheChat => 'Įjungti / išjungti pokalbį'; @@ -1534,10 +1761,10 @@ class AppLocalizationsLt extends AppLocalizations { String get black => 'Juodieji'; @override - String get asWhite => 'kaip baltieji'; + String get asWhite => 'už baltuosius'; @override - String get asBlack => 'kaip juodieji'; + String get asBlack => 'už juoduosius'; @override String get randomColor => 'Atsitiktinė spalva'; @@ -1552,10 +1779,10 @@ class AppLocalizationsLt extends AppLocalizations { String get blackIsVictorious => 'Juodieji laimėjo'; @override - String get youPlayTheWhitePieces => 'Žaidžiate baltomis figūromis'; + String get youPlayTheWhitePieces => 'Jūs žaidžiate baltosiomis figūromis'; @override - String get youPlayTheBlackPieces => 'Žaidžiate juodomis figūromis'; + String get youPlayTheBlackPieces => 'Jūs žaidžiate juodosiomis figūromis'; @override String get itsYourTurn => 'Jūsų ėjimas!'; @@ -1841,9 +2068,6 @@ class AppLocalizationsLt extends AppLocalizations { @override String get removesTheDepthLimit => 'Panaikina gylio limitą ir neleidžia kompiuteriui atvėsti'; - @override - String get engineManager => 'Variklių valdymas'; - @override String get blunder => 'Šiurkšti klaida'; @@ -1922,7 +2146,7 @@ class AppLocalizationsLt extends AppLocalizations { String get friends => 'Draugai'; @override - String get otherPlayers => 'other players'; + String get otherPlayers => 'kiti žaidėjai'; @override String get discussions => 'Diskusijos'; @@ -2107,6 +2331,9 @@ class AppLocalizationsLt extends AppLocalizations { @override String get gamesPlayed => 'sužaistos partijos'; + @override + String get ok => 'OK'; + @override String get cancel => 'Atšaukti'; @@ -2756,10 +2983,10 @@ class AppLocalizationsLt extends AppLocalizations { String get yes => 'Taip'; @override - String get website => 'Website'; + String get website => 'Tinklapis'; @override - String get mobile => 'Mobile'; + String get mobile => 'Mobilus'; @override String get help => 'Pagalba:'; @@ -2816,7 +3043,13 @@ class AppLocalizationsLt extends AppLocalizations { String get other => 'Kita'; @override - String get reportDescriptionHelp => 'Įdėkite nuorodą į partiją(-as) ir paaiškinkite, kas netinkamo yra šio vartotojo elgsenoje. Paminėkite, kaip priėjote prie tokios išvados. Jūsų pranešimas bus apdorotas greičiau, jei bus pateiktas anglų kalba.'; + String get reportCheatBoostHelp => 'Įdėkite nuorodą į partiją(-as) ir paaiškinkite, kas netinkamo yra šio vartotojo elgsenoje. Paminėkite, kaip priėjote prie tokios išvados. Jūsų pranešimas bus apdorotas greičiau, jei bus pateiktas anglų kalba.'; + + @override + String get reportUsernameHelp => 'Paaiškinkite, kuo šis vartotojo vardas yra įžeidžiantis. Nesakykite tiesiog „tai įžeidžia/netinkama“, bet papasakokite, kaip priėjote prie šios išvados, ypač jei įžeidimas yra užmaskuotas, ne anglų kalba, yra slengas arba yra istorinė / kultūrinė nuoroda.'; + + @override + String get reportProcessedFasterInEnglish => 'Jūsų pranešimas bus apdorotas greičiau, jei jis bus parašytas anglų kalba.'; @override String get error_provideOneCheatedGameLink => 'Pateikite bent vieną nuorodą į partiją, kurioje buvo sukčiauta.'; @@ -4121,6 +4354,9 @@ class AppLocalizationsLt extends AppLocalizations { @override String get nothingToSeeHere => 'Nieko naujo.'; + @override + String get stats => 'Stats'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4129,7 +4365,7 @@ class AppLocalizationsLt extends AppLocalizations { other: 'Jūsų varžovas paliko partiją. Galėsite prisiimti pergalę už $count sekundžių.', many: 'Jūsų varžovas paliko partiją. Galėsite prisiimti pergalę už $count sekundžių.', few: 'Jūsų varžovas paliko partiją. Galėsite prisiimti pergalę už $count sekundžių.', - one: 'Jūsų varžovas paliko partiją. Galėsite prisiimti pergalę už $count sekundės.', + one: 'Jūsų varžovas paliko partiją. Galite reikalauti pergalės už $count sekundės.', ); return '$_temp0'; } @@ -4855,9 +5091,522 @@ class AppLocalizationsLt extends AppLocalizations { @override String get streamerLichessStreamers => 'Lichess transliuotojai'; + @override + String get studyPrivate => 'Privati'; + + @override + String get studyMyStudies => 'Mano studijos'; + + @override + String get studyStudiesIContributeTo => 'Studijos, kuriose prisidedu'; + + @override + String get studyMyPublicStudies => 'Mano viešos studijos'; + + @override + String get studyMyPrivateStudies => 'Mano privačios studijos'; + + @override + String get studyMyFavoriteStudies => 'Mano mėgstamiausios studijos'; + + @override + String get studyWhatAreStudies => 'Kas yra studijos?'; + + @override + String get studyAllStudies => 'Visos studijos'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Studijos, sukurtos $param'; + } + + @override + String get studyNoneYet => 'Dar nėra.'; + + @override + String get studyHot => 'Populiaru dabar'; + + @override + String get studyDateAddedNewest => 'Sukūrimo data (naujausios)'; + + @override + String get studyDateAddedOldest => 'Sukūrimo data (seniausios)'; + + @override + String get studyRecentlyUpdated => 'Neseniai atnaujintos'; + + @override + String get studyMostPopular => 'Populiariausios'; + + @override + String get studyAlphabetical => 'Abėcėlės tvarka'; + + @override + String get studyAddNewChapter => 'Pridėti naują skyrių'; + + @override + String get studyAddMembers => 'Pridėti narių'; + + @override + String get studyInviteToTheStudy => 'Pakviesti į studiją'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Kvieskite tik pažįstamus žmones, ir tik norinčius dalyvauti šioje studijoje.'; + + @override + String get studySearchByUsername => 'Ieškoti pagal naudotojo vardą'; + + @override + String get studySpectator => 'Žiūrovas'; + + @override + String get studyContributor => 'Talkininkas'; + + @override + String get studyKick => 'Išmesti'; + + @override + String get studyLeaveTheStudy => 'Palikti studiją'; + + @override + String get studyYouAreNowAContributor => 'Dabar esate talkininkas'; + + @override + String get studyYouAreNowASpectator => 'Dabar esate žiūrovas'; + + @override + String get studyPgnTags => 'PGN žymos'; + + @override + String get studyLike => 'Mėgti'; + + @override + String get studyUnlike => 'Nebemėgti'; + + @override + String get studyNewTag => 'Nauja žyma'; + + @override + String get studyCommentThisPosition => 'Komentuoti šią poziciją'; + + @override + String get studyCommentThisMove => 'Komentuoti šį ėjimą'; + + @override + String get studyAnnotateWithGlyphs => 'Komentuoti su glifais'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Skyrius yra per trumpas analizei.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Tik studijos talkininkai gali prašyti kompiuterio analizės.'; + + @override + String get studyGetAFullComputerAnalysis => 'Gaukite pilną pagrindinės linijos kompiuterio analizę.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Įsitikinkite, kad skyrius užbaigtas. Analizės galite prašyti tik kartą.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Visi SYNC nariai lieka toje pačioje pozicijoje'; + + @override + String get studyShareChanges => 'Dalinkitės pakeitimais su žiūrovais ir saugokite juos serveryje'; + + @override + String get studyPlaying => 'Žaidžiama'; + + @override + String get studyShowEvalBar => 'Vertinimo skalė'; + + @override + String get studyFirst => 'Pirmas'; + + @override + String get studyPrevious => 'Ankstesnis'; + + @override + String get studyNext => 'Kitas'; + + @override + String get studyLast => 'Paskutinis'; + @override String get studyShareAndExport => 'Dalintis ir eksportuoti'; + @override + String get studyCloneStudy => 'Klonuoti'; + + @override + String get studyStudyPgn => 'Studijos PGN'; + + @override + String get studyDownloadAllGames => 'Atsisiųsti visus žaidimus'; + + @override + String get studyChapterPgn => 'Skyriaus PGN'; + + @override + String get studyCopyChapterPgn => 'Kopijuoti PGN'; + + @override + String get studyDownloadGame => 'Atsisiųsti žaidimą'; + + @override + String get studyStudyUrl => 'Studijos URL'; + + @override + String get studyCurrentChapterUrl => 'Dabartinio skyriaus URL'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Galite įklijuoti šį forume norėdami įterpti'; + + @override + String get studyStartAtInitialPosition => 'Pradėti pradinėje pozicijoje'; + + @override + String studyStartAtX(String param) { + return 'Pradėti nuo $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Įterpti savo svetainėje ar tinklaraštyje'; + + @override + String get studyReadMoreAboutEmbedding => 'Skaitykite daugiau apie įterpimą'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Gali būti įterptos tik viešos studijos!'; + + @override + String get studyOpen => 'Atverti'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1 iš $param2'; + } + + @override + String get studyStudyNotFound => 'Studija nerasta'; + + @override + String get studyEditChapter => 'Redaguoti skyrių'; + + @override + String get studyNewChapter => 'Naujas skyrius'; + + @override + String studyImportFromChapterX(String param) { + return 'Importuoti iš $param'; + } + + @override + String get studyOrientation => 'Kryptis'; + + @override + String get studyAnalysisMode => 'Analizės režimas'; + + @override + String get studyPinnedChapterComment => 'Prisegtas skyriaus komentaras'; + + @override + String get studySaveChapter => 'Išsaugoti skyrių'; + + @override + String get studyClearAnnotations => 'Pašalinti anotacijas'; + + @override + String get studyClearVariations => 'Išvalyti variacijas'; + + @override + String get studyDeleteChapter => 'Ištrinti skyrių'; + + @override + String get studyDeleteThisChapter => 'Ištrinti šį skyrių? Nėra kelio atgal!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Išvalyti visus komentarus, ženklus ir figūras šiame skyriuje?'; + + @override + String get studyRightUnderTheBoard => 'Iš karto po lenta'; + + @override + String get studyNoPinnedComment => 'Jokio'; + + @override + String get studyNormalAnalysis => 'Įprasta analizė'; + + @override + String get studyHideNextMoves => 'Slėpti kitus ėjimus'; + + @override + String get studyInteractiveLesson => 'Interaktyvi pamoka'; + + @override + String studyChapterX(String param) { + return 'Skyrius $param'; + } + + @override + String get studyEmpty => 'Tuščia'; + + @override + String get studyStartFromInitialPosition => 'Pradėti nuo pirminės pozicijos'; + + @override + String get studyEditor => 'Redaktorius'; + + @override + String get studyStartFromCustomPosition => 'Pradėti nuo tinkintos pozicijos'; + + @override + String get studyLoadAGameByUrl => 'Pakrauti partijas iš adresų'; + + @override + String get studyLoadAPositionFromFen => 'Pakrauti poziciją iš FEN'; + + @override + String get studyLoadAGameFromPgn => 'Pakrauti partijas iš PGN'; + + @override + String get studyAutomatic => 'Automatinis'; + + @override + String get studyUrlOfTheGame => 'Partijų adresai, vienas per eilutę'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Pakrauti partijas iš $param1 arba $param2'; + } + + @override + String get studyCreateChapter => 'Sukurti skyrių'; + + @override + String get studyCreateStudy => 'Sukurti studiją'; + + @override + String get studyEditStudy => 'Redaguoti studiją'; + + @override + String get studyVisibility => 'Matomumas'; + + @override + String get studyPublic => 'Viešas'; + + @override + String get studyUnlisted => 'Nėra sąraše'; + + @override + String get studyInviteOnly => 'Tik su pakvietimu'; + + @override + String get studyAllowCloning => 'Leisti kopijuoti'; + + @override + String get studyNobody => 'Niekam'; + + @override + String get studyOnlyMe => 'Tik man'; + + @override + String get studyContributors => 'Dalyviams'; + + @override + String get studyMembers => 'Nariams'; + + @override + String get studyEveryone => 'Visiems'; + + @override + String get studyEnableSync => 'Įgalinti sinchronizaciją'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Taip: visiems rodyti tą pačią poziciją'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Ne: leisti žmonėms naršyti laisvai'; + + @override + String get studyPinnedStudyComment => 'Prisegtas studijos komentaras'; + @override String get studyStart => 'Pradėti'; + + @override + String get studySave => 'Išsaugoti'; + + @override + String get studyClearChat => 'Išvalyti pokalbį'; + + @override + String get studyDeleteTheStudyChatHistory => 'Ištrinti studijos pokalbių istoriją? Nėra kelio atgal!'; + + @override + String get studyDeleteStudy => 'Ištrinti studiją'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Ištrinti visą studiją? Ištrynimas negrįžtamas. Norėdami tęsti įrašykite studijos pavadinimą: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Kur norite tai studijuoti?'; + + @override + String get studyGoodMove => 'Geras ėjimas'; + + @override + String get studyMistake => 'Klaida'; + + @override + String get studyBrilliantMove => 'Puikus ėjimas'; + + @override + String get studyBlunder => 'Šiurkšti klaida'; + + @override + String get studyInterestingMove => 'Įdomus ėjimas'; + + @override + String get studyDubiousMove => 'Abejotinas ėjimas'; + + @override + String get studyOnlyMove => 'Vienintelis ėjimas'; + + @override + String get studyZugzwang => 'Cugcvangas'; + + @override + String get studyEqualPosition => 'Lygi pozicija'; + + @override + String get studyUnclearPosition => 'Neaiški pozicija'; + + @override + String get studyWhiteIsSlightlyBetter => 'Šiek tiek geriau baltiesiems'; + + @override + String get studyBlackIsSlightlyBetter => 'Šiek tiek geriau juodiesiems'; + + @override + String get studyWhiteIsBetter => 'Geriau baltiesiems'; + + @override + String get studyBlackIsBetter => 'Geriau juodiesiems'; + + @override + String get studyWhiteIsWinning => 'Laimi baltieji'; + + @override + String get studyBlackIsWinning => 'Laimi juodieji'; + + @override + String get studyNovelty => 'Naujovė'; + + @override + String get studyDevelopment => 'Plėtojimas'; + + @override + String get studyInitiative => 'Iniciatyva'; + + @override + String get studyAttack => 'Ataka'; + + @override + String get studyCounterplay => 'Kontraėjimas'; + + @override + String get studyTimeTrouble => 'Laiko problemos'; + + @override + String get studyWithCompensation => 'Su kompensacija'; + + @override + String get studyWithTheIdea => 'Su mintimi'; + + @override + String get studyNextChapter => 'Kitas skyrius'; + + @override + String get studyPrevChapter => 'Ankstenis skyrius'; + + @override + String get studyStudyActions => 'Studijos veiksmai'; + + @override + String get studyTopics => 'Temos'; + + @override + String get studyMyTopics => 'Mano temos'; + + @override + String get studyPopularTopics => 'Populiarios temos'; + + @override + String get studyManageTopics => 'Valdyti temas'; + + @override + String get studyBack => 'Atgal'; + + @override + String get studyPlayAgain => 'Žaisti dar kartą'; + + @override + String get studyWhatWouldYouPlay => 'Ar norėtumėte žaisti nuo šios pozicijos?'; + + @override + String get studyYouCompletedThisLesson => 'Sveikiname! Jūs pabaigėte šią pamoką.'; + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count skyrių', + many: '$count skyrių', + few: '$count skyriai', + one: '$count skyrius', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count partijų', + many: '$count partijų', + few: '$count partijos', + one: '$count partija', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count narių', + many: '$count narių', + few: '$count nariai', + one: '$count narys', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Įklijuokite savo PGN tekstą čia, iki $count žaidimų', + many: 'Įklijuokite savo PGN tekstą čia, iki $count žaidimo', + few: 'Įklijuokite savo PGN tekstą čia, iki $count žaidimų', + one: 'Įklijuokite savo PGN tekstą čia, iki $count žaidimo', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_lv.dart b/lib/l10n/l10n_lv.dart index 4111b6e32d..cc7a53e80c 100644 --- a/lib/l10n/l10n_lv.dart +++ b/lib/l10n/l10n_lv.dart @@ -103,9 +103,6 @@ class AppLocalizationsLv extends AppLocalizations { @override String get mobileCancelTakebackOffer => 'Cancel takeback offer'; - @override - String get mobileCancelDrawOffer => 'Cancel draw offer'; - @override String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @@ -254,6 +251,17 @@ class AppLocalizationsLv extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Completed $count $param2 correspondence games', + one: 'Completed $count $param2 correspondence game', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -365,9 +373,226 @@ class AppLocalizationsLv extends AppLocalizations { @override String get broadcastBroadcasts => 'Raidījumi'; + @override + String get broadcastMyBroadcasts => 'My broadcasts'; + @override String get broadcastLiveBroadcasts => 'Reāllaika turnīru raidījumi'; + @override + String get broadcastBroadcastCalendar => 'Broadcast calendar'; + + @override + String get broadcastNewBroadcast => 'Jauns reāllaika raidījums'; + + @override + String get broadcastSubscribedBroadcasts => 'Subscribed broadcasts'; + + @override + String get broadcastAboutBroadcasts => 'About broadcasts'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'How to use Lichess Broadcasts.'; + + @override + String get broadcastTheNewRoundHelp => 'The new round will have the same members and contributors as the previous one.'; + + @override + String get broadcastAddRound => 'Pievienot raundu'; + + @override + String get broadcastOngoing => 'Notiekošie'; + + @override + String get broadcastUpcoming => 'Gaidāmie'; + + @override + String get broadcastCompleted => 'Notikušie'; + + @override + String get broadcastCompletedHelp => 'Lichess detects round completion, but can get it wrong. Use this to set it manually.'; + + @override + String get broadcastRoundName => 'Raunda nosaukums'; + + @override + String get broadcastRoundNumber => 'Raunda skaitlis'; + + @override + String get broadcastTournamentName => 'Turnīra nosaukums'; + + @override + String get broadcastTournamentDescription => 'Īss turnīra apraksts'; + + @override + String get broadcastFullDescription => 'Pilns pasākuma apraksts'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Neobligāts garš raidījuma apraksts. Pieejams $param1. Garumam jābūt mazāk kā $param2 rakstzīmēm.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN Source URL'; + + @override + String get broadcastSourceUrlHelp => 'URL, ko Lichess aptaujās, lai iegūtu PGN atjauninājumus. Tam jābūt publiski piekļūstamam no interneta.'; + + @override + String get broadcastSourceGameIds => 'Up to 64 Lichess game IDs, separated by spaces.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Start date in the tournament local timezone: $param'; + } + + @override + String get broadcastStartDateHelp => 'Neobligāts, ja zināt, kad pasākums sākas'; + + @override + String get broadcastCurrentGameUrl => 'Pašreizējās spēles URL'; + + @override + String get broadcastDownloadAllRounds => 'Lejupielādēt visus raundus'; + + @override + String get broadcastResetRound => 'Atiestatīt šo raundu'; + + @override + String get broadcastDeleteRound => 'Dzēst šo raundu'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Neatgriezeniski dzēst raundu un tā spēles.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Izdzēst visas šī raunda spēles. To atjaunošanai būs nepieciešams aktīvs avots.'; + + @override + String get broadcastEditRoundStudy => 'Edit round study'; + + @override + String get broadcastDeleteTournament => 'Delete this tournament'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Definitively delete the entire tournament, all its rounds and all its games.'; + + @override + String get broadcastShowScores => 'Show players scores based on game results'; + + @override + String get broadcastReplacePlayerTags => 'Optional: replace player names, ratings and titles'; + + @override + String get broadcastFideFederations => 'FIDE federations'; + + @override + String get broadcastTop10Rating => 'Top 10 rating'; + + @override + String get broadcastFidePlayers => 'FIDE players'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE player not found'; + + @override + String get broadcastFideProfile => 'FIDE profile'; + + @override + String get broadcastFederation => 'Federation'; + + @override + String get broadcastAgeThisYear => 'Age this year'; + + @override + String get broadcastUnrated => 'Unrated'; + + @override + String get broadcastRecentTournaments => 'Recent tournaments'; + + @override + String get broadcastOpenLichess => 'Open in Lichess'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Overview'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Official website'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'Score'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count broadcasts', + one: '$count broadcast', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Izaicinājumi: $param1'; @@ -1412,10 +1637,10 @@ class AppLocalizationsLv extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Pretiniekam ir ierobežoti iespējamie gājieni, un visi no tiem pasliktina pretinieka pozīciju.'; @override - String get puzzleThemeHealthyMix => 'Veselīgs sajaukums'; + String get puzzleThemeMix => 'Veselīgs sajaukums'; @override - String get puzzleThemeHealthyMixDescription => 'Mazliet no visa kā. Nezināsiet, ko sagaidīt, tāpēc paliksiet gatavs jebkam! Tieši kā īstās spēlēs.'; + String get puzzleThemeMixDescription => 'Mazliet no visa kā. Nezināsiet, ko sagaidīt, tāpēc paliksiet gatavs jebkam! Tieši kā īstās spēlēs.'; @override String get puzzleThemePlayerGames => 'Spēlētāja spēles'; @@ -1819,9 +2044,6 @@ class AppLocalizationsLv extends AppLocalizations { @override String get removesTheDepthLimit => 'Noņem dziļuma ierobežojumu un uztur tavu datoru siltu'; - @override - String get engineManager => 'Dzinēja pārvaldnieks'; - @override String get blunder => 'Rupja kļūda'; @@ -2085,6 +2307,9 @@ class AppLocalizationsLv extends AppLocalizations { @override String get gamesPlayed => 'Izspēlētās spēles'; + @override + String get ok => 'OK'; + @override String get cancel => 'Atcelt'; @@ -2794,7 +3019,13 @@ class AppLocalizationsLv extends AppLocalizations { String get other => 'Cits'; @override - String get reportDescriptionHelp => 'Ielīmējiet spēles saiti un paskaidrojiet, kas nav kārtībā ar lietotāja uzvedību. Nepietiks, ja tikai norādīsiet, ka \"lietotājs krāpjas\" — lūdzu, pastāstiet, kā nonācāt pie šī secinājuma. Ja jūsu ziņojums būs rakstīts angliski, par to varēsim parūpēties ātrāk.'; + String get reportCheatBoostHelp => 'Paste the link to the game(s) and explain what is wrong about this user\'s behaviour. Don\'t just say \"they cheat\", but tell us how you came to this conclusion.'; + + @override + String get reportUsernameHelp => 'Explain what about this username is offensive. Don\'t just say \"it\'s offensive/inappropriate\", but tell us how you came to this conclusion, especially if the insult is obfuscated, not in english, is in slang, or is a historical/cultural reference.'; + + @override + String get reportProcessedFasterInEnglish => 'Your report will be processed faster if written in English.'; @override String get error_provideOneCheatedGameLink => 'Lūdzu, norādiet vismaz vienu saiti uz spēli, kurā pretinieks ir krāpies.'; @@ -4099,6 +4330,9 @@ class AppLocalizationsLv extends AppLocalizations { @override String get nothingToSeeHere => 'Nothing to see here at the moment.'; + @override + String get stats => 'Stats'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4789,9 +5023,518 @@ class AppLocalizationsLv extends AppLocalizations { @override String get streamerLichessStreamers => 'Lichess straumētāji'; + @override + String get studyPrivate => 'Privāta'; + + @override + String get studyMyStudies => 'Manas izpētes'; + + @override + String get studyStudiesIContributeTo => 'Izpētes, kurās piedalos'; + + @override + String get studyMyPublicStudies => 'Manas publiskās izpētes'; + + @override + String get studyMyPrivateStudies => 'Manas privātās izpētes'; + + @override + String get studyMyFavoriteStudies => 'Mana izpēšu izlase'; + + @override + String get studyWhatAreStudies => 'Kas ir izpētes?'; + + @override + String get studyAllStudies => 'Visas izpētes'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Izpētes, ko izveidoja $param'; + } + + @override + String get studyNoneYet => 'Pagaidām nevienas.'; + + @override + String get studyHot => 'Nesen populārās'; + + @override + String get studyDateAddedNewest => 'Pievienošanas datums (jaunākās)'; + + @override + String get studyDateAddedOldest => 'Pievienošanas datums (vecākās)'; + + @override + String get studyRecentlyUpdated => 'Nesen atjaunotās'; + + @override + String get studyMostPopular => 'Populārākās'; + + @override + String get studyAlphabetical => 'Alfabētiskā secībā'; + + @override + String get studyAddNewChapter => 'Pievienot nodaļu'; + + @override + String get studyAddMembers => 'Pievienot dalībniekus'; + + @override + String get studyInviteToTheStudy => 'Ielūgt uz izpēti'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Lūdzu, ielūdziet tikai cilvēkus, kurus pazīstat un kuri vēlas pievienoties izpētei.'; + + @override + String get studySearchByUsername => 'Meklēt pēc lietotājvārda'; + + @override + String get studySpectator => 'Skatītājs'; + + @override + String get studyContributor => 'Ieguldītājs'; + + @override + String get studyKick => 'Izmest'; + + @override + String get studyLeaveTheStudy => 'Pamest izpēti'; + + @override + String get studyYouAreNowAContributor => 'Tagad esat ieguldītājs'; + + @override + String get studyYouAreNowASpectator => 'Tagad esat skatītājs'; + + @override + String get studyPgnTags => 'PGN birkas'; + + @override + String get studyLike => 'Patīk'; + + @override + String get studyUnlike => 'Noņemt atzīmi \"patīk\"'; + + @override + String get studyNewTag => 'Jauna birka'; + + @override + String get studyCommentThisPosition => 'Komentēt šo pozīciju'; + + @override + String get studyCommentThisMove => 'Komentēt šo gājienu'; + + @override + String get studyAnnotateWithGlyphs => 'Anotēt ar glifiem'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Šī nodaļa ir par īsu lai to analizētu.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Tikai izpētes ieguldītāji var pieprasīt datoranalīzi.'; + + @override + String get studyGetAFullComputerAnalysis => 'Iegūstiet pilnu servera puses pamatvarianta datoranalīzi.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Pārliecinieties, ka nodaļa ir pabeigta. Datoranalīzi var pieprasīt tikai vienreiz.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Visi SYNC dalībnieki paliek vienā pozīcijā'; + + @override + String get studyShareChanges => 'Koplietot izmaiņas ar skatītājiem un saglabāt tās serverī'; + + @override + String get studyPlaying => 'Notiek'; + + @override + String get studyShowEvalBar => 'Evaluation bars'; + + @override + String get studyFirst => 'Pirmais'; + + @override + String get studyPrevious => 'Iepriekšējais'; + + @override + String get studyNext => 'Nākamais'; + + @override + String get studyLast => 'Pēdējais'; + @override String get studyShareAndExport => 'Koplietot & eksportēt'; + @override + String get studyCloneStudy => 'Klonēt'; + + @override + String get studyStudyPgn => 'Izpētes PGN'; + + @override + String get studyDownloadAllGames => 'Lejupielādēt visas spēles'; + + @override + String get studyChapterPgn => 'Nodaļas PGN'; + + @override + String get studyCopyChapterPgn => 'Kopēt PGN'; + + @override + String get studyDownloadGame => 'Lejupielādēt spēli'; + + @override + String get studyStudyUrl => 'Izpētes URL'; + + @override + String get studyCurrentChapterUrl => 'Pašreizējās nodaļas URL'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Šo varat ielīmēt forumā, lai iegultu'; + + @override + String get studyStartAtInitialPosition => 'Sākt no sākotnējās pozīcijas'; + + @override + String studyStartAtX(String param) { + return 'Sākt ar $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Iegult savā mājaslapā vai blogā'; + + @override + String get studyReadMoreAboutEmbedding => 'Lasīt vairāk par iegulšanu'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Iegult var tikai publiskas izpētes!'; + + @override + String get studyOpen => 'Atvērt'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param2 piedāvā \"$param1\"'; + } + + @override + String get studyStudyNotFound => 'Izpēte nav atrasta'; + + @override + String get studyEditChapter => 'Rediģēt nodaļu'; + + @override + String get studyNewChapter => 'Jauna nodaļa'; + + @override + String studyImportFromChapterX(String param) { + return 'Importēt no $param'; + } + + @override + String get studyOrientation => 'Orientācija'; + + @override + String get studyAnalysisMode => 'Analīzes režīms'; + + @override + String get studyPinnedChapterComment => 'Piesprausts nodaļas komentārs'; + + @override + String get studySaveChapter => 'Saglabāt nodaļu'; + + @override + String get studyClearAnnotations => 'Notīrīt piezīmes'; + + @override + String get studyClearVariations => 'Notīrīt variantus'; + + @override + String get studyDeleteChapter => 'Dzēst nodaļu'; + + @override + String get studyDeleteThisChapter => 'Vai dzēst šo nodaļu? Atpakaļceļa nav!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Notīrīt visus komentārus un figūras šajā nodaļā?'; + + @override + String get studyRightUnderTheBoard => 'Tieši zem galdiņa'; + + @override + String get studyNoPinnedComment => 'Neviens'; + + @override + String get studyNormalAnalysis => 'Parasta analīze'; + + @override + String get studyHideNextMoves => 'Slēpt turpmākos gājienus'; + + @override + String get studyInteractiveLesson => 'Interaktīva nodarbība'; + + @override + String studyChapterX(String param) { + return '$param. nodaļa'; + } + + @override + String get studyEmpty => 'Tukšs'; + + @override + String get studyStartFromInitialPosition => 'Sākt no sākotnējās pozīcijas'; + + @override + String get studyEditor => 'Redaktors'; + + @override + String get studyStartFromCustomPosition => 'Sākt no pielāgotas pozīcijas'; + + @override + String get studyLoadAGameByUrl => 'Ielādēt spēli, norādot URL'; + + @override + String get studyLoadAPositionFromFen => 'Ielādēt pozīciju no FEN'; + + @override + String get studyLoadAGameFromPgn => 'Ielādēt spēli no PGN'; + + @override + String get studyAutomatic => 'Automātisks'; + + @override + String get studyUrlOfTheGame => 'Spēles URL'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Ielādēt spēli no $param1 vai $param2'; + } + + @override + String get studyCreateChapter => 'Izveidot nodaļu'; + + @override + String get studyCreateStudy => 'Izveidot izpēti'; + + @override + String get studyEditStudy => 'Rediģēt izpēti'; + + @override + String get studyVisibility => 'Redzamība'; + + @override + String get studyPublic => 'Publiska'; + + @override + String get studyUnlisted => 'Nerindota'; + + @override + String get studyInviteOnly => 'Tikai ar ielūgumu'; + + @override + String get studyAllowCloning => 'Atļaut dublēšanu'; + + @override + String get studyNobody => 'Neviens'; + + @override + String get studyOnlyMe => 'Tikai es'; + + @override + String get studyContributors => 'Ieguldītāji'; + + @override + String get studyMembers => 'Dalībnieki'; + + @override + String get studyEveryone => 'Visi'; + + @override + String get studyEnableSync => 'Iespējot sinhronizāciju'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Jā: paturēt visus vienā pozīcijā'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Nē: ļaut katram brīvi pārlūkot'; + + @override + String get studyPinnedStudyComment => 'Piesprausts izpētes komentārs'; + @override String get studyStart => 'Sākt'; + + @override + String get studySave => 'Saglabāt'; + + @override + String get studyClearChat => 'Notīrīt saraksti'; + + @override + String get studyDeleteTheStudyChatHistory => 'Vai dzēst izpētes sarakstes vēsturi? Atpakaļceļa nav!'; + + @override + String get studyDeleteStudy => 'Dzēst izpēti'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Dzēst visu izpēti? Atpakaļceļa nav! Ievadiet izpētes nosaukumu, lai apstiprinātu: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Kur vēlaties to izpētīt?'; + + @override + String get studyGoodMove => 'Labs gājiens'; + + @override + String get studyMistake => 'Kļūda'; + + @override + String get studyBrilliantMove => 'Izcils gājiens'; + + @override + String get studyBlunder => 'Rupja kļūda'; + + @override + String get studyInterestingMove => 'Interesants gājiens'; + + @override + String get studyDubiousMove => 'Apšaubāms gājiens'; + + @override + String get studyOnlyMove => 'Vienīgais gājiens'; + + @override + String get studyZugzwang => 'Gājiena spaids'; + + @override + String get studyEqualPosition => 'Vienlīdzīga pozīcija'; + + @override + String get studyUnclearPosition => 'Neskaidra pozīcija'; + + @override + String get studyWhiteIsSlightlyBetter => 'Baltajiem nedaudz labāka pozīcija'; + + @override + String get studyBlackIsSlightlyBetter => 'Melnajiem nedaudz labāka pozīcija'; + + @override + String get studyWhiteIsBetter => 'Baltajiem labāka pozīcija'; + + @override + String get studyBlackIsBetter => 'Melnajiem labāka pozīcija'; + + @override + String get studyWhiteIsWinning => 'Baltie tuvojas uzvarai'; + + @override + String get studyBlackIsWinning => 'Melnie tuvojas uzvarai'; + + @override + String get studyNovelty => 'Oriģināls gājiens'; + + @override + String get studyDevelopment => 'Attīstība'; + + @override + String get studyInitiative => 'Iniciatīva'; + + @override + String get studyAttack => 'Uzbrukums'; + + @override + String get studyCounterplay => 'Pretspēle'; + + @override + String get studyTimeTrouble => 'Laika trūkuma grūtības'; + + @override + String get studyWithCompensation => 'Ar atlīdzinājumu'; + + @override + String get studyWithTheIdea => 'Ar domu'; + + @override + String get studyNextChapter => 'Nākamā nodaļa'; + + @override + String get studyPrevChapter => 'Iepriekšējā nodaļa'; + + @override + String get studyStudyActions => 'Izpētes darbības'; + + @override + String get studyTopics => 'Temati'; + + @override + String get studyMyTopics => 'Mani temati'; + + @override + String get studyPopularTopics => 'Populāri temati'; + + @override + String get studyManageTopics => 'Pārvaldīt tematus'; + + @override + String get studyBack => 'Atpakaļ'; + + @override + String get studyPlayAgain => 'Spēlēt vēlreiz'; + + @override + String get studyWhatWouldYouPlay => 'Kā jūs spēlētu šādā pozīcijā?'; + + @override + String get studyYouCompletedThisLesson => 'Apsveicam! Pabeidzāt šo nodarbību.'; + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Nodaļas', + one: '$count Nodaļa', + zero: '$count Nodaļas', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Spēles', + one: '$count Spēle', + zero: '$count Spēles', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Dalībnieki', + one: '$count Dalībnieks', + zero: '$count Dalībnieki', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Ielīmējiet PGN tekstu šeit, ne vairāk kā $count spēles', + one: 'Ielīmējiet PGN tekstu šeit, ne vairāk kā $count spēli', + zero: 'Ielīmējiet PGN tekstu šeit, ne vairāk kā $count spēles', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_mk.dart b/lib/l10n/l10n_mk.dart index 91912a2485..2473af76e7 100644 --- a/lib/l10n/l10n_mk.dart +++ b/lib/l10n/l10n_mk.dart @@ -27,16 +27,16 @@ class AppLocalizationsMk extends AppLocalizations { String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; @override - String get mobileSystemColors => 'System colors'; + String get mobileSystemColors => 'Системски бои'; @override - String get mobileFeedbackButton => 'Feedback'; + String get mobileFeedbackButton => 'Повратна информација'; @override String get mobileOkButton => 'OK'; @override - String get mobileSettingsHapticFeedback => 'Haptic feedback'; + String get mobileSettingsHapticFeedback => 'Тактилен фидбек'; @override String get mobileSettingsImmersiveMode => 'Immersive mode'; @@ -103,9 +103,6 @@ class AppLocalizationsMk extends AppLocalizations { @override String get mobileCancelTakebackOffer => 'Cancel takeback offer'; - @override - String get mobileCancelDrawOffer => 'Cancel draw offer'; - @override String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @@ -246,6 +243,17 @@ class AppLocalizationsMk extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Completed $count $param2 correspondence games', + one: 'Completed $count $param2 correspondence game', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,226 @@ class AppLocalizationsMk extends AppLocalizations { @override String get broadcastBroadcasts => 'Емитувања'; + @override + String get broadcastMyBroadcasts => 'My broadcasts'; + @override String get broadcastLiveBroadcasts => 'Пренос на турнири во живо'; + @override + String get broadcastBroadcastCalendar => 'Broadcast calendar'; + + @override + String get broadcastNewBroadcast => 'Ново емитување во живо'; + + @override + String get broadcastSubscribedBroadcasts => 'Subscribed broadcasts'; + + @override + String get broadcastAboutBroadcasts => 'About broadcasts'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'How to use Lichess Broadcasts.'; + + @override + String get broadcastTheNewRoundHelp => 'The new round will have the same members and contributors as the previous one.'; + + @override + String get broadcastAddRound => 'Add a round'; + + @override + String get broadcastOngoing => 'Во тек'; + + @override + String get broadcastUpcoming => 'Претстојни'; + + @override + String get broadcastCompleted => 'Завршени'; + + @override + String get broadcastCompletedHelp => 'Lichess detects round completion, but can get it wrong. Use this to set it manually.'; + + @override + String get broadcastRoundName => 'Round name'; + + @override + String get broadcastRoundNumber => 'Заокружен број'; + + @override + String get broadcastTournamentName => 'Tournament name'; + + @override + String get broadcastTournamentDescription => 'Short tournament description'; + + @override + String get broadcastFullDescription => 'Цел опис на настанот'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Незадолжителен, долг опис на емитуваниот настан. $param1 е достапен. Должината мора да е пократка од $param2 знаци.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN Source URL'; + + @override + String get broadcastSourceUrlHelp => 'URL кое Lichess ќе го користи за ажурирање на PGN датотеката. Мора да биде јавно достапно на интернет.'; + + @override + String get broadcastSourceGameIds => 'Up to 64 Lichess game IDs, separated by spaces.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Start date in the tournament local timezone: $param'; + } + + @override + String get broadcastStartDateHelp => 'Незадолжително, ако знаете кога почнува настанот'; + + @override + String get broadcastCurrentGameUrl => 'Current game URL'; + + @override + String get broadcastDownloadAllRounds => 'Download all rounds'; + + @override + String get broadcastResetRound => 'Reset this round'; + + @override + String get broadcastDeleteRound => 'Delete this round'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Definitively delete the round and all its games.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Delete all games of this round. The source will need to be active in order to re-create them.'; + + @override + String get broadcastEditRoundStudy => 'Edit round study'; + + @override + String get broadcastDeleteTournament => 'Delete this tournament'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Definitively delete the entire tournament, all its rounds and all its games.'; + + @override + String get broadcastShowScores => 'Show players scores based on game results'; + + @override + String get broadcastReplacePlayerTags => 'Optional: replace player names, ratings and titles'; + + @override + String get broadcastFideFederations => 'FIDE federations'; + + @override + String get broadcastTop10Rating => 'Top 10 rating'; + + @override + String get broadcastFidePlayers => 'FIDE players'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE player not found'; + + @override + String get broadcastFideProfile => 'FIDE profile'; + + @override + String get broadcastFederation => 'Federation'; + + @override + String get broadcastAgeThisYear => 'Age this year'; + + @override + String get broadcastUnrated => 'Unrated'; + + @override + String get broadcastRecentTournaments => 'Recent tournaments'; + + @override + String get broadcastOpenLichess => 'Open in Lichess'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Overview'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Official website'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'Score'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count broadcasts', + one: '$count broadcast', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Challenges: $param1'; @@ -1390,10 +1615,10 @@ class AppLocalizationsMk extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'The opponent is limited in the moves they can make, and all moves worsen their position.'; @override - String get puzzleThemeHealthyMix => 'Healthy mix'; + String get puzzleThemeMix => 'Healthy mix'; @override - String get puzzleThemeHealthyMixDescription => 'A bit of everything. You don\'t know what to expect, so you remain ready for anything! Just like in real games.'; + String get puzzleThemeMixDescription => 'A bit of everything. You don\'t know what to expect, so you remain ready for anything! Just like in real games.'; @override String get puzzleThemePlayerGames => 'Player games'; @@ -1797,9 +2022,6 @@ class AppLocalizationsMk extends AppLocalizations { @override String get removesTheDepthLimit => 'Неограничена длабочина на анализа, го загрева вашиот компјутер'; - @override - String get engineManager => 'Менаџер на компјутерот'; - @override String get blunder => 'Глупа грешка'; @@ -2063,6 +2285,9 @@ class AppLocalizationsMk extends AppLocalizations { @override String get gamesPlayed => 'Одиграни партии'; + @override + String get ok => 'OK'; + @override String get cancel => 'Откажи'; @@ -2772,7 +2997,13 @@ class AppLocalizationsMk extends AppLocalizations { String get other => 'Друго'; @override - String get reportDescriptionHelp => 'Внесете линк од играта/игрите и објаснете каде е проблемот во однесувањето на овој корисник. Немојте само да обвините за мамење, туку објаснете како дојдовте до тој заклучок. Вашата пријава ќе биди разгледана побрзо ако е напишана на англиски јазик.'; + String get reportCheatBoostHelp => 'Paste the link to the game(s) and explain what is wrong about this user\'s behaviour. Don\'t just say \"they cheat\", but tell us how you came to this conclusion.'; + + @override + String get reportUsernameHelp => 'Explain what about this username is offensive. Don\'t just say \"it\'s offensive/inappropriate\", but tell us how you came to this conclusion, especially if the insult is obfuscated, not in english, is in slang, or is a historical/cultural reference.'; + + @override + String get reportProcessedFasterInEnglish => 'Your report will be processed faster if written in English.'; @override String get error_provideOneCheatedGameLink => 'Ве молиме доставете барем една врска до партија со мамење.'; @@ -4077,6 +4308,9 @@ class AppLocalizationsMk extends AppLocalizations { @override String get nothingToSeeHere => 'Nothing to see here at the moment.'; + @override + String get stats => 'Stats'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4723,9 +4957,514 @@ class AppLocalizationsMk extends AppLocalizations { @override String get streamerLichessStreamers => 'Lichess streamers'; + @override + String get studyPrivate => 'Private'; + + @override + String get studyMyStudies => 'My studies'; + + @override + String get studyStudiesIContributeTo => 'Studies I contribute to'; + + @override + String get studyMyPublicStudies => 'My public studies'; + + @override + String get studyMyPrivateStudies => 'My private studies'; + + @override + String get studyMyFavoriteStudies => 'My favourite studies'; + + @override + String get studyWhatAreStudies => 'What are studies?'; + + @override + String get studyAllStudies => 'All studies'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Studies created by $param'; + } + + @override + String get studyNoneYet => 'None yet.'; + + @override + String get studyHot => 'Hot'; + + @override + String get studyDateAddedNewest => 'Date added (newest)'; + + @override + String get studyDateAddedOldest => 'Date added (oldest)'; + + @override + String get studyRecentlyUpdated => 'Recently updated'; + + @override + String get studyMostPopular => 'Most popular'; + + @override + String get studyAlphabetical => 'Alphabetical'; + + @override + String get studyAddNewChapter => 'Add a new chapter'; + + @override + String get studyAddMembers => 'Add members'; + + @override + String get studyInviteToTheStudy => 'Invite to the study'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Please only invite people who know you, and who actively want to join this study.'; + + @override + String get studySearchByUsername => 'Search by username'; + + @override + String get studySpectator => 'Spectator'; + + @override + String get studyContributor => 'Contributor'; + + @override + String get studyKick => 'Kick'; + + @override + String get studyLeaveTheStudy => 'Leave the study'; + + @override + String get studyYouAreNowAContributor => 'You are now a contributor'; + + @override + String get studyYouAreNowASpectator => 'You are now a spectator'; + + @override + String get studyPgnTags => 'PGN tags'; + + @override + String get studyLike => 'Like'; + + @override + String get studyUnlike => 'Unlike'; + + @override + String get studyNewTag => 'New tag'; + + @override + String get studyCommentThisPosition => 'Comment on this position'; + + @override + String get studyCommentThisMove => 'Comment on this move'; + + @override + String get studyAnnotateWithGlyphs => 'Annotate with glyphs'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'The chapter is too short to be analysed.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Only the study contributors can request a computer analysis.'; + + @override + String get studyGetAFullComputerAnalysis => 'Get a full server-side computer analysis of the mainline.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Make sure the chapter is complete. You can only request analysis once.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'All SYNC members remain on the same position'; + + @override + String get studyShareChanges => 'Share changes with spectators and save them on the server'; + + @override + String get studyPlaying => 'Playing'; + + @override + String get studyShowEvalBar => 'Evaluation bars'; + + @override + String get studyFirst => 'First'; + + @override + String get studyPrevious => 'Previous'; + + @override + String get studyNext => 'Next'; + + @override + String get studyLast => 'Last'; + @override String get studyShareAndExport => 'Share & export'; + @override + String get studyCloneStudy => 'Clone'; + + @override + String get studyStudyPgn => 'Study PGN'; + + @override + String get studyDownloadAllGames => 'Download all games'; + + @override + String get studyChapterPgn => 'Chapter PGN'; + + @override + String get studyCopyChapterPgn => 'Copy PGN'; + + @override + String get studyDownloadGame => 'Download game'; + + @override + String get studyStudyUrl => 'Study URL'; + + @override + String get studyCurrentChapterUrl => 'Current chapter URL'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'You can paste this in the forum or your Lichess blog to embed'; + + @override + String get studyStartAtInitialPosition => 'Start at initial position'; + + @override + String studyStartAtX(String param) { + return 'Start at $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Embed in your website'; + + @override + String get studyReadMoreAboutEmbedding => 'Read more about embedding'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Only public studies can be embedded!'; + + @override + String get studyOpen => 'Open'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1, brought to you by $param2'; + } + + @override + String get studyStudyNotFound => 'Study not found'; + + @override + String get studyEditChapter => 'Edit chapter'; + + @override + String get studyNewChapter => 'New chapter'; + + @override + String studyImportFromChapterX(String param) { + return 'Import from $param'; + } + + @override + String get studyOrientation => 'Orientation'; + + @override + String get studyAnalysisMode => 'Analysis mode'; + + @override + String get studyPinnedChapterComment => 'Pinned chapter comment'; + + @override + String get studySaveChapter => 'Save chapter'; + + @override + String get studyClearAnnotations => 'Clear annotations'; + + @override + String get studyClearVariations => 'Clear variations'; + + @override + String get studyDeleteChapter => 'Delete chapter'; + + @override + String get studyDeleteThisChapter => 'Delete this chapter. There is no going back!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Clear all comments, glyphs and drawn shapes in this chapter'; + + @override + String get studyRightUnderTheBoard => 'Right under the board'; + + @override + String get studyNoPinnedComment => 'None'; + + @override + String get studyNormalAnalysis => 'Normal analysis'; + + @override + String get studyHideNextMoves => 'Hide next moves'; + + @override + String get studyInteractiveLesson => 'Interactive lesson'; + + @override + String studyChapterX(String param) { + return 'Chapter $param'; + } + + @override + String get studyEmpty => 'Empty'; + + @override + String get studyStartFromInitialPosition => 'Start from initial position'; + + @override + String get studyEditor => 'Editor'; + + @override + String get studyStartFromCustomPosition => 'Start from custom position'; + + @override + String get studyLoadAGameByUrl => 'Load games by URLs'; + + @override + String get studyLoadAPositionFromFen => 'Load a position from FEN'; + + @override + String get studyLoadAGameFromPgn => 'Load games from PGN'; + + @override + String get studyAutomatic => 'Automatic'; + + @override + String get studyUrlOfTheGame => 'URL of the games, one per line'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Load games from $param1 or $param2'; + } + + @override + String get studyCreateChapter => 'Create chapter'; + + @override + String get studyCreateStudy => 'Create study'; + + @override + String get studyEditStudy => 'Edit study'; + + @override + String get studyVisibility => 'Visibility'; + + @override + String get studyPublic => 'Public'; + + @override + String get studyUnlisted => 'Unlisted'; + + @override + String get studyInviteOnly => 'Invite only'; + + @override + String get studyAllowCloning => 'Allow cloning'; + + @override + String get studyNobody => 'Nobody'; + + @override + String get studyOnlyMe => 'Only me'; + + @override + String get studyContributors => 'Contributors'; + + @override + String get studyMembers => 'Members'; + + @override + String get studyEveryone => 'Everyone'; + + @override + String get studyEnableSync => 'Enable sync'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Yes: keep everyone on the same position'; + + @override + String get studyNoLetPeopleBrowseFreely => 'No: let people browse freely'; + + @override + String get studyPinnedStudyComment => 'Pinned study comment'; + @override String get studyStart => 'Start'; + + @override + String get studySave => 'Save'; + + @override + String get studyClearChat => 'Clear chat'; + + @override + String get studyDeleteTheStudyChatHistory => 'Delete the study chat history? There is no going back!'; + + @override + String get studyDeleteStudy => 'Delete study'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Delete the entire study? There is no going back! Type the name of the study to confirm: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Where do you want to study that?'; + + @override + String get studyGoodMove => 'Good move'; + + @override + String get studyMistake => 'Mistake'; + + @override + String get studyBrilliantMove => 'Brilliant move'; + + @override + String get studyBlunder => 'Blunder'; + + @override + String get studyInterestingMove => 'Interesting move'; + + @override + String get studyDubiousMove => 'Dubious move'; + + @override + String get studyOnlyMove => 'Only move'; + + @override + String get studyZugzwang => 'Zugzwang'; + + @override + String get studyEqualPosition => 'Equal position'; + + @override + String get studyUnclearPosition => 'Unclear position'; + + @override + String get studyWhiteIsSlightlyBetter => 'White is slightly better'; + + @override + String get studyBlackIsSlightlyBetter => 'Black is slightly better'; + + @override + String get studyWhiteIsBetter => 'White is better'; + + @override + String get studyBlackIsBetter => 'Black is better'; + + @override + String get studyWhiteIsWinning => 'White is winning'; + + @override + String get studyBlackIsWinning => 'Black is winning'; + + @override + String get studyNovelty => 'Novelty'; + + @override + String get studyDevelopment => 'Development'; + + @override + String get studyInitiative => 'Initiative'; + + @override + String get studyAttack => 'Attack'; + + @override + String get studyCounterplay => 'Counterplay'; + + @override + String get studyTimeTrouble => 'Time trouble'; + + @override + String get studyWithCompensation => 'With compensation'; + + @override + String get studyWithTheIdea => 'With the idea'; + + @override + String get studyNextChapter => 'Next chapter'; + + @override + String get studyPrevChapter => 'Previous chapter'; + + @override + String get studyStudyActions => 'Study actions'; + + @override + String get studyTopics => 'Topics'; + + @override + String get studyMyTopics => 'My topics'; + + @override + String get studyPopularTopics => 'Popular topics'; + + @override + String get studyManageTopics => 'Manage topics'; + + @override + String get studyBack => 'Back'; + + @override + String get studyPlayAgain => 'Play again'; + + @override + String get studyWhatWouldYouPlay => 'What would you play in this position?'; + + @override + String get studyYouCompletedThisLesson => 'Congratulations! You completed this lesson.'; + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Chapters', + one: '$count Chapter', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Games', + one: '$count Game', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Members', + one: '$count Member', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Paste your PGN text here, up to $count games', + one: 'Paste your PGN text here, up to $count game', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_nb.dart b/lib/l10n/l10n_nb.dart index ee123d534b..4afa372d7a 100644 --- a/lib/l10n/l10n_nb.dart +++ b/lib/l10n/l10n_nb.dart @@ -103,9 +103,6 @@ class AppLocalizationsNb extends AppLocalizations { @override String get mobileCancelTakebackOffer => 'Avbryt tilbud om å angre'; - @override - String get mobileCancelDrawOffer => 'Avbryt remistilbud'; - @override String get mobileWaitingForOpponentToJoin => 'Venter på motstanderen ...'; @@ -131,18 +128,18 @@ class AppLocalizationsNb extends AppLocalizations { String get mobilePuzzleThemesSubtitle => 'Spill sjakknøtter fra favorittåpningene dine, eller velg et tema.'; @override - String get mobilePuzzleStormSubtitle => 'Løs så mange sjakknøtter som mulig i løpet av 3 minutter.'; + String get mobilePuzzleStormSubtitle => 'Løs så mange sjakknøtter du klarer i løpet av 3 minutter.'; @override String mobileGreeting(String param) { - return 'Hallo, $param'; + return 'Hei, $param'; } @override - String get mobileGreetingWithoutName => 'Hallo'; + String get mobileGreetingWithoutName => 'Hei'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobilePrefMagnifyDraggedPiece => 'Forstørr brikker når de dras'; @override String get activityActivity => 'Aktivitet'; @@ -246,6 +243,17 @@ class AppLocalizationsNb extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Har spilt ferdig $count fjernsjakkpartier i $param2', + one: 'Har spilt ferdig $count fjernsjakkparti i $param2', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,226 @@ class AppLocalizationsNb extends AppLocalizations { @override String get broadcastBroadcasts => 'Overføringer'; + @override + String get broadcastMyBroadcasts => 'Mine overføringer'; + @override String get broadcastLiveBroadcasts => 'Direkteoverføringer av turneringer'; + @override + String get broadcastBroadcastCalendar => 'Kalender for overføringer'; + + @override + String get broadcastNewBroadcast => 'Ny direkteoverføring'; + + @override + String get broadcastSubscribedBroadcasts => 'Overføringer som du abonnerer på'; + + @override + String get broadcastAboutBroadcasts => 'Om overføringer'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Hvordan bruke overføringer hos Lichess.'; + + @override + String get broadcastTheNewRoundHelp => 'Den nye runden vil ha de samme medlemmene og bidragsyterne som den forrige.'; + + @override + String get broadcastAddRound => 'Legg til runde'; + + @override + String get broadcastOngoing => 'Pågående'; + + @override + String get broadcastUpcoming => 'Kommende'; + + @override + String get broadcastCompleted => 'Fullført'; + + @override + String get broadcastCompletedHelp => 'Lichess oppdager fullførte runder basert på kildepartiene. Bruk denne knappen hvis det ikke finnes noen kilde.'; + + @override + String get broadcastRoundName => 'Rundenavn'; + + @override + String get broadcastRoundNumber => 'Rundenummer'; + + @override + String get broadcastTournamentName => 'Turneringsnavn'; + + @override + String get broadcastTournamentDescription => 'Kort beskrivelse av turneringen'; + + @override + String get broadcastFullDescription => 'Full beskrivelse av turneringen'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Valgfri lang beskrivelse av turneringen. $param1 er tilgjengelig. Beskrivelsen må være kortere enn $param2 tegn.'; + } + + @override + String get broadcastSourceSingleUrl => 'URL til PGN-kilden'; + + @override + String get broadcastSourceUrlHelp => 'Lenke som Lichess vil hente PGN-oppdateringer fra. Den må være offentlig tilgjengelig på internett.'; + + @override + String get broadcastSourceGameIds => 'Opptil 64 ID-er for partier hos Lichess. De må være adskilt med mellomrom.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Startdato i turneringens lokale tidssone: $param'; + } + + @override + String get broadcastStartDateHelp => 'Valgfritt, hvis du vet når arrangementet starter'; + + @override + String get broadcastCurrentGameUrl => 'URL for dette partiet'; + + @override + String get broadcastDownloadAllRounds => 'Last ned alle rundene'; + + @override + String get broadcastResetRound => 'Nullstill denne runden'; + + @override + String get broadcastDeleteRound => 'Slett denne runden'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Slett runden og tilhørende partier ugjenkallelig.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Slett alle partiene i denne runden. Kilden må være aktiv for å gjenopprette dem.'; + + @override + String get broadcastEditRoundStudy => 'Rediger rundestudie'; + + @override + String get broadcastDeleteTournament => 'Slett denne turneringen'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Slett hele turneringen for godt, sammen med alle rundene og alle partiene.'; + + @override + String get broadcastShowScores => 'Vis poeng for spillerne basert på resultater av partiene'; + + @override + String get broadcastReplacePlayerTags => 'Valgfritt: erstatt spillernavn, ratinger og titler'; + + @override + String get broadcastFideFederations => 'FIDE-forbund'; + + @override + String get broadcastTop10Rating => 'Topp 10 rating'; + + @override + String get broadcastFidePlayers => 'FIDE-spillere'; + + @override + String get broadcastFidePlayerNotFound => 'Fant ikke FIDE-spiller'; + + @override + String get broadcastFideProfile => 'FIDE-profil'; + + @override + String get broadcastFederation => 'Forbund'; + + @override + String get broadcastAgeThisYear => 'Alder i år'; + + @override + String get broadcastUnrated => 'Uratet'; + + @override + String get broadcastRecentTournaments => 'Nylige turneringer'; + + @override + String get broadcastOpenLichess => 'Åpne i Lichess'; + + @override + String get broadcastTeams => 'Lag'; + + @override + String get broadcastBoards => 'Brett'; + + @override + String get broadcastOverview => 'Oversikt'; + + @override + String get broadcastSubscribeTitle => 'Abonner for å bli varslet når hver runde starter. Du kan velge varselform i kontoinnstillingene dine.'; + + @override + String get broadcastUploadImage => 'Last opp bilde for turneringen'; + + @override + String get broadcastNoBoardsYet => 'Ingen brett. De vises når partiene er lastet opp.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Brett kan lastes med en kilde eller via $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starter etter $param'; + } + + @override + String get broadcastStartVerySoon => 'Overføringen starter straks.'; + + @override + String get broadcastNotYetStarted => 'Overføringen har ikke startet.'; + + @override + String get broadcastOfficialWebsite => 'Offisiell nettside'; + + @override + String get broadcastStandings => 'Resultatliste'; + + @override + String broadcastIframeHelp(String param) { + return 'Flere alternativer på $param'; + } + + @override + String get broadcastWebmastersPage => 'administratorens side'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'En offentlig PGN-kilde i sanntid for denne runden. Vi tilbyr også en $param for raskere og mer effektiv synkronisering.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Bygg inn denne overføringen på nettstedet ditt'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Bygg inn $param på nettstedet ditt'; + } + + @override + String get broadcastRatingDiff => 'Ratingdifferanse'; + + @override + String get broadcastGamesThisTournament => 'Partier i denne turneringen'; + + @override + String get broadcastScore => 'Poengsum'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count overføringer', + one: '$count overføring', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Utfordringer: $param1'; @@ -1390,10 +1615,10 @@ class AppLocalizationsNb extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Motstanderen kan bare utføre trekk som forverrer egen stilling.'; @override - String get puzzleThemeHealthyMix => 'Frisk blanding'; + String get puzzleThemeMix => 'Frisk blanding'; @override - String get puzzleThemeHealthyMixDescription => 'Litt av alt. Du vet ikke hva du får, så du er klar for alt! Akkurat som i virkelige partier.'; + String get puzzleThemeMixDescription => 'Litt av alt. Du vet ikke hva du får, så du er klar for alt! Akkurat som i virkelige partier.'; @override String get puzzleThemePlayerGames => 'Spillerpartier'; @@ -1797,9 +2022,6 @@ class AppLocalizationsNb extends AppLocalizations { @override String get removesTheDepthLimit => 'Fjerner dybdebegrensning, og holder maskinen din varm'; - @override - String get engineManager => 'Innstillinger for sjakkmotorer'; - @override String get blunder => 'Bukk'; @@ -2063,6 +2285,9 @@ class AppLocalizationsNb extends AppLocalizations { @override String get gamesPlayed => 'partier spilt'; + @override + String get ok => 'OK'; + @override String get cancel => 'Avbryt'; @@ -2772,7 +2997,13 @@ class AppLocalizationsNb extends AppLocalizations { String get other => 'Annet'; @override - String get reportDescriptionHelp => 'Kopier lenken til partiet/partiene og forklar hva som er galt med denne brukerens oppførsel.'; + String get reportCheatBoostHelp => 'Kopier lenken til partiet/partiene og forklar hva som er galt med denne brukerens oppførsel. Skriv en utdypende begrunnelse, ikke bare «vedkommende jukser».'; + + @override + String get reportUsernameHelp => 'Forklar hvorfor brukernavnet er støtende. Skriv en utdypende begrunnelse, ikke bare «det er støtende/upassende». Dette gjelder særlig hvis fornærmelsen er tilslørt, ikke er på engelsk, er et slanguttrykk eller er en historisk/kulturell referanse.'; + + @override + String get reportProcessedFasterInEnglish => 'Rapporten din blir behandlet raskere hvis den er skrevet på engelsk.'; @override String get error_provideOneCheatedGameLink => 'Oppgi minst én lenke til et jukseparti.'; @@ -4077,6 +4308,9 @@ class AppLocalizationsNb extends AppLocalizations { @override String get nothingToSeeHere => 'Ingenting her for nå.'; + @override + String get stats => 'Statistikk'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4723,9 +4957,514 @@ class AppLocalizationsNb extends AppLocalizations { @override String get streamerLichessStreamers => 'Lichess-strømmere'; + @override + String get studyPrivate => 'Privat'; + + @override + String get studyMyStudies => 'Mine studier'; + + @override + String get studyStudiesIContributeTo => 'Studier jeg bidrar til'; + + @override + String get studyMyPublicStudies => 'Mine offentlige studier'; + + @override + String get studyMyPrivateStudies => 'Mine private studier'; + + @override + String get studyMyFavoriteStudies => 'Mine favorittstudier'; + + @override + String get studyWhatAreStudies => 'Hva er studier?'; + + @override + String get studyAllStudies => 'Alle studier'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Studier opprettet av $param'; + } + + @override + String get studyNoneYet => 'Ingen så langt.'; + + @override + String get studyHot => 'Hett'; + + @override + String get studyDateAddedNewest => 'Dato tilføyd (nyeste)'; + + @override + String get studyDateAddedOldest => 'Dato tilføyd (eldste)'; + + @override + String get studyRecentlyUpdated => 'Nylig oppdatert'; + + @override + String get studyMostPopular => 'Mest populære'; + + @override + String get studyAlphabetical => 'Alfabetisk'; + + @override + String get studyAddNewChapter => 'Legg til kapittel'; + + @override + String get studyAddMembers => 'Legg til medlemmer'; + + @override + String get studyInviteToTheStudy => 'Inviter til studien'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Inviter bare folk du kjenner som ønsker å delta i studien.'; + + @override + String get studySearchByUsername => 'Søk på brukernavn'; + + @override + String get studySpectator => 'Tilskuer'; + + @override + String get studyContributor => 'Bidragsyter'; + + @override + String get studyKick => 'Kast ut'; + + @override + String get studyLeaveTheStudy => 'Forlat studien'; + + @override + String get studyYouAreNowAContributor => 'Du er nå bidragsyter'; + + @override + String get studyYouAreNowASpectator => 'Du er nå tilskuer'; + + @override + String get studyPgnTags => 'PGN-merkelapper'; + + @override + String get studyLike => 'Lik'; + + @override + String get studyUnlike => 'Slutt å like'; + + @override + String get studyNewTag => 'Ny merkelapp'; + + @override + String get studyCommentThisPosition => 'Kommenter denne stillingen'; + + @override + String get studyCommentThisMove => 'Kommenter dette trekket'; + + @override + String get studyAnnotateWithGlyphs => 'Kommenter med symboler'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Kapittelet er for kort for analyse.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Bare bidragsyterne til studien kan be om maskinanalyse.'; + + @override + String get studyGetAFullComputerAnalysis => 'Få full maskinanalyse av hovedvarianten fra serveren.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Sørg for at kapittelet er fullført. Du kan bare be om analyse én gang.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Alle synkroniserte medlemmer ser den samme stillingen'; + + @override + String get studyShareChanges => 'Del endringer med tilskuere og lagre dem på serveren'; + + @override + String get studyPlaying => 'Pågår'; + + @override + String get studyShowEvalBar => 'Evalueringssøyler'; + + @override + String get studyFirst => 'Første'; + + @override + String get studyPrevious => 'Forrige'; + + @override + String get studyNext => 'Neste'; + + @override + String get studyLast => 'Siste'; + @override String get studyShareAndExport => 'Del og eksporter'; + @override + String get studyCloneStudy => 'Klon'; + + @override + String get studyStudyPgn => 'Studie-PGN'; + + @override + String get studyDownloadAllGames => 'Last ned alle partiene'; + + @override + String get studyChapterPgn => 'Kapittel-PGN'; + + @override + String get studyCopyChapterPgn => 'Kopier PGN'; + + @override + String get studyDownloadGame => 'Last ned partiet'; + + @override + String get studyStudyUrl => 'Studie-URL'; + + @override + String get studyCurrentChapterUrl => 'Kapittel-URL'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Du kan lime inn dette i forumet for å bygge det inn der'; + + @override + String get studyStartAtInitialPosition => 'Start ved innledende stilling'; + + @override + String studyStartAtX(String param) { + return 'Start ved $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Bygg inn på nettstedet ditt eller bloggen din'; + + @override + String get studyReadMoreAboutEmbedding => 'Les mer om å bygge inn'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Bare offentlige studier kan bygges inn!'; + + @override + String get studyOpen => 'Åpne'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1 presentert av $param2'; + } + + @override + String get studyStudyNotFound => 'Fant ikke studien'; + + @override + String get studyEditChapter => 'Rediger kapittel'; + + @override + String get studyNewChapter => 'Nytt kapittel'; + + @override + String studyImportFromChapterX(String param) { + return 'Importer fra $param'; + } + + @override + String get studyOrientation => 'Retning'; + + @override + String get studyAnalysisMode => 'Analysemodus'; + + @override + String get studyPinnedChapterComment => 'Fastspikrede kapittelkommenter'; + + @override + String get studySaveChapter => 'Lagre kapittelet'; + + @override + String get studyClearAnnotations => 'Fjern notater'; + + @override + String get studyClearVariations => 'Fjern varianter'; + + @override + String get studyDeleteChapter => 'Slett kapittel'; + + @override + String get studyDeleteThisChapter => 'Slette dette kapittelet? Du kan ikke angre!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Fjern alle kommentarer og figurer i dette kapittelet?'; + + @override + String get studyRightUnderTheBoard => 'Rett under brettet'; + + @override + String get studyNoPinnedComment => 'Ingen'; + + @override + String get studyNormalAnalysis => 'Normal analyse'; + + @override + String get studyHideNextMoves => 'Skjul neste trekk'; + + @override + String get studyInteractiveLesson => 'Interaktiv leksjon'; + + @override + String studyChapterX(String param) { + return 'Kapittel $param'; + } + + @override + String get studyEmpty => 'Tom'; + + @override + String get studyStartFromInitialPosition => 'Start ved innledende stilling'; + + @override + String get studyEditor => 'Editor'; + + @override + String get studyStartFromCustomPosition => 'Start fra innledende stilling'; + + @override + String get studyLoadAGameByUrl => 'Last inn partier fra URL-er'; + + @override + String get studyLoadAPositionFromFen => 'Last inn en stilling fra FEN'; + + @override + String get studyLoadAGameFromPgn => 'Last inn partier fra PGN'; + + @override + String get studyAutomatic => 'Automatisk'; + + @override + String get studyUrlOfTheGame => 'URL for partiene, én pr. linje'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Last inn partier fra $param1 eller $param2'; + } + + @override + String get studyCreateChapter => 'Opprett kapittel'; + + @override + String get studyCreateStudy => 'Opprett en studie'; + + @override + String get studyEditStudy => 'Rediger studie'; + + @override + String get studyVisibility => 'Synlighet'; + + @override + String get studyPublic => 'Offentlig'; + + @override + String get studyUnlisted => 'Ikke listet'; + + @override + String get studyInviteOnly => 'Bare etter invitasjon'; + + @override + String get studyAllowCloning => 'Tillat kloning'; + + @override + String get studyNobody => 'Ingen'; + + @override + String get studyOnlyMe => 'Bare meg'; + + @override + String get studyContributors => 'Bidragsytere'; + + @override + String get studyMembers => 'Medlemmer'; + + @override + String get studyEveryone => 'Alle'; + + @override + String get studyEnableSync => 'Aktiver synkronisering'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Ja: behold alle i samme stilling'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Nei: la folk se fritt gjennom'; + + @override + String get studyPinnedStudyComment => 'Fastspikrede studiekommentarer'; + @override String get studyStart => 'Start'; + + @override + String get studySave => 'Lagre'; + + @override + String get studyClearChat => 'Fjern samtalen'; + + @override + String get studyDeleteTheStudyChatHistory => 'Slette studiens samtalehistorikk? Du kan ikke angre!'; + + @override + String get studyDeleteStudy => 'Slett studie'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Slette hele studien? Du kan ikke angre! Bekreft ved å skrive inn navnet på studien: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Hvilken studie vil du bruke?'; + + @override + String get studyGoodMove => 'Godt trekk'; + + @override + String get studyMistake => 'Feil'; + + @override + String get studyBrilliantMove => 'Strålende trekk'; + + @override + String get studyBlunder => 'Bukk'; + + @override + String get studyInterestingMove => 'Interessant trekk'; + + @override + String get studyDubiousMove => 'Tvilsomt trekk'; + + @override + String get studyOnlyMove => 'Eneste trekk'; + + @override + String get studyZugzwang => 'Trekktvang'; + + @override + String get studyEqualPosition => 'Lik stilling'; + + @override + String get studyUnclearPosition => 'Uavklart stilling'; + + @override + String get studyWhiteIsSlightlyBetter => 'Hvit står litt bedre'; + + @override + String get studyBlackIsSlightlyBetter => 'Svart står litt bedre'; + + @override + String get studyWhiteIsBetter => 'Hvit står bedre'; + + @override + String get studyBlackIsBetter => 'Svart står bedre'; + + @override + String get studyWhiteIsWinning => 'Hvit står til vinst'; + + @override + String get studyBlackIsWinning => 'Svart står til vinst'; + + @override + String get studyNovelty => 'Nyvinning'; + + @override + String get studyDevelopment => 'Utvikling'; + + @override + String get studyInitiative => 'Initiativ'; + + @override + String get studyAttack => 'Angrep'; + + @override + String get studyCounterplay => 'Motspill'; + + @override + String get studyTimeTrouble => 'Tidsnød'; + + @override + String get studyWithCompensation => 'Med kompensasjon'; + + @override + String get studyWithTheIdea => 'Med ideen'; + + @override + String get studyNextChapter => 'Neste kapittel'; + + @override + String get studyPrevChapter => 'Forrige kapittel'; + + @override + String get studyStudyActions => 'Studiehandlinger'; + + @override + String get studyTopics => 'Emner'; + + @override + String get studyMyTopics => 'Mine emner'; + + @override + String get studyPopularTopics => 'Populære emner'; + + @override + String get studyManageTopics => 'Administrer emner'; + + @override + String get studyBack => 'Tilbake'; + + @override + String get studyPlayAgain => 'Spill igjen'; + + @override + String get studyWhatWouldYouPlay => 'Hva vil du spille i denne stillingen?'; + + @override + String get studyYouCompletedThisLesson => 'Gratulerer! Du har fullført denne leksjonen.'; + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count kapitler', + one: '$count kapittel', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count partier', + one: '$count parti', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count medlemmer', + one: '$count medlem', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Sett inn PGN-teksten din her, maksimum $count partier', + one: 'Sett inn PGN-teksten din her, maksimum $count parti', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_nl.dart b/lib/l10n/l10n_nl.dart index 9781f805ca..ff2469fcf7 100644 --- a/lib/l10n/l10n_nl.dart +++ b/lib/l10n/l10n_nl.dart @@ -42,7 +42,7 @@ class AppLocalizationsNl extends AppLocalizations { String get mobileSettingsImmersiveMode => 'Volledig scherm-modus'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileSettingsImmersiveModeSubtitle => 'Systeem-UI verbergen tijdens het spelen. Gebruik dit als je last hebt van de navigatiegebaren aan de randen van het scherm. Dit is van toepassing op spel- en Puzzle Storm schermen.'; @override String get mobileNotFollowingAnyUser => 'U volgt geen gebruiker.'; @@ -95,22 +95,19 @@ class AppLocalizationsNl extends AppLocalizations { String get mobileShowComments => 'Opmerkingen weergeven'; @override - String get mobilePuzzleStormConfirmEndRun => 'Wil je dit uitvoeren beëindigen?'; + String get mobilePuzzleStormConfirmEndRun => 'Wil je deze reeks beëindigen?'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobilePuzzleStormFilterNothingToShow => 'Niets te tonen, wijzig de filters'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; - - @override - String get mobileCancelDrawOffer => 'Remiseaanbod intrekken'; + String get mobileCancelTakebackOffer => 'Terugnameaanbod annuleren'; @override String get mobileWaitingForOpponentToJoin => 'Wachten op een tegenstander...'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get mobileBlindfoldMode => 'Geblinddoekt'; @override String get mobileLiveStreamers => 'Live streamers'; @@ -142,7 +139,7 @@ class AppLocalizationsNl extends AppLocalizations { String get mobileGreetingWithoutName => 'Hallo'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobilePrefMagnifyDraggedPiece => 'Versleept stuk vergroot weergeven'; @override String get activityActivity => 'Activiteit'; @@ -174,8 +171,8 @@ class AppLocalizationsNl extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Beoefende $count posities van $param2', - one: 'Beoefende $count positie van $param2', + other: 'Oefende $count posities van $param2', + one: 'Oefende $count positie van $param2', ); return '$_temp0'; } @@ -246,6 +243,17 @@ class AppLocalizationsNl extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Voltooide $count $param2 correspondentiepartijen', + one: 'Voltooide $count $param2 correspondentiepartijen', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,226 @@ class AppLocalizationsNl extends AppLocalizations { @override String get broadcastBroadcasts => 'Uitzendingen'; + @override + String get broadcastMyBroadcasts => 'Mijn uitzendingen'; + @override String get broadcastLiveBroadcasts => 'Live toernooi uitzendingen'; + @override + String get broadcastBroadcastCalendar => 'Uitzendkalender'; + + @override + String get broadcastNewBroadcast => 'Nieuwe live uitzending'; + + @override + String get broadcastSubscribedBroadcasts => 'Subscribed broadcasts'; + + @override + String get broadcastAboutBroadcasts => 'Over uitzending'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Hoe Lichess Uitzendingen te gebruiken.'; + + @override + String get broadcastTheNewRoundHelp => 'De nieuwe ronde zal dezelfde leden en bijdragers hebben als de vorige.'; + + @override + String get broadcastAddRound => 'Ronde toevoegen'; + + @override + String get broadcastOngoing => 'Lopend'; + + @override + String get broadcastUpcoming => 'Aankomend'; + + @override + String get broadcastCompleted => 'Voltooid'; + + @override + String get broadcastCompletedHelp => 'Lichess detecteert voltooiing van de ronde op basis van de bronpartijen. Gebruik deze schakelaar als er geen bron is.'; + + @override + String get broadcastRoundName => 'Naam ronde'; + + @override + String get broadcastRoundNumber => 'Ronde'; + + @override + String get broadcastTournamentName => 'Naam toernooi'; + + @override + String get broadcastTournamentDescription => 'Korte toernooibeschrijving'; + + @override + String get broadcastFullDescription => 'Volledige beschrijving evenement'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Optionele lange beschrijving van de uitzending. $param1 is beschikbaar. Totale lengte moet minder zijn dan $param2 tekens.'; + } + + @override + String get broadcastSourceSingleUrl => 'URL van PGN-bron'; + + @override + String get broadcastSourceUrlHelp => 'Link die Lichess gebruikt om PGN updates te krijgen. Deze moet openbaar toegankelijk zijn via internet.'; + + @override + String get broadcastSourceGameIds => 'Tot 64 Lichess partij-ID\'\'s, gescheiden door spaties.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Startdatum in de lokale tijdzone van het tornooi: $param'; + } + + @override + String get broadcastStartDateHelp => 'Optioneel, als je weet wanneer het evenement start'; + + @override + String get broadcastCurrentGameUrl => 'Huidige partij-link'; + + @override + String get broadcastDownloadAllRounds => 'Alle rondes downloaden'; + + @override + String get broadcastResetRound => 'Deze ronde opnieuw instellen'; + + @override + String get broadcastDeleteRound => 'Deze ronde verwijderen'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Deze ronde en bijbehorende partijen definitief verwijderen.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Alle partijen van deze ronde verwijderen. De bron zal actief moeten zijn om ze opnieuw te maken.'; + + @override + String get broadcastEditRoundStudy => 'Studieronde bewerken'; + + @override + String get broadcastDeleteTournament => 'Verwijder dit toernooi'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Verwijder definitief het hele toernooi, inclusief alle rondes en partijen.'; + + @override + String get broadcastShowScores => 'Toon scores van spelers op basis van partij-uitslagen'; + + @override + String get broadcastReplacePlayerTags => 'Optioneel: vervang spelersnamen, beoordelingen en titels'; + + @override + String get broadcastFideFederations => 'FIDE-federaties'; + + @override + String get broadcastTop10Rating => 'Top 10-rating'; + + @override + String get broadcastFidePlayers => 'FIDE-spelers'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE-speler niet gevonden'; + + @override + String get broadcastFideProfile => 'FIDE-profiel'; + + @override + String get broadcastFederation => 'Federatie'; + + @override + String get broadcastAgeThisYear => 'Leeftijd dit jaar'; + + @override + String get broadcastUnrated => 'Zonder rating'; + + @override + String get broadcastRecentTournaments => 'Recente toernooien'; + + @override + String get broadcastOpenLichess => 'Openen in Lichess'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Borden'; + + @override + String get broadcastOverview => 'Overzicht'; + + @override + String get broadcastSubscribeTitle => 'Krijg een melding wanneer elke ronde start. Je kunt bel- of pushmeldingen voor uitzendingen in je accountvoorkeuren in-/uitschakelen.'; + + @override + String get broadcastUploadImage => 'Toernooifoto uploaden'; + + @override + String get broadcastNoBoardsYet => 'Nog geen borden. Deze zullen verschijnen van zodra er partijen worden geüpload.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Borden kunnen geladen worden met een bron of via de $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Start na $param'; + } + + @override + String get broadcastStartVerySoon => 'De uitzending begint binnenkort.'; + + @override + String get broadcastNotYetStarted => 'De uitzending is nog niet begonnen.'; + + @override + String get broadcastOfficialWebsite => 'Officiële website'; + + @override + String get broadcastStandings => 'Klassement'; + + @override + String broadcastIframeHelp(String param) { + return 'Meer opties voor de $param'; + } + + @override + String get broadcastWebmastersPage => 'pagina van de webmaster'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'Een publieke real-time PGN-bron voor deze ronde. We bieden ook een $param aan voor een snellere en efficiëntere synchronisatie.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Deze uitzending insluiten in je website'; + + @override + String broadcastEmbedThisRound(String param) { + return '$param insluiten in je website'; + } + + @override + String get broadcastRatingDiff => 'Ratingverschil'; + + @override + String get broadcastGamesThisTournament => 'Partijen in dit toernooi'; + + @override + String get broadcastScore => 'Score'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count uitzendingen', + one: '$count uitzending', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Uitdagingen: $param1'; @@ -1390,10 +1615,10 @@ class AppLocalizationsNl extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'De tegenstander is beperkt in de zetten die hij kan doen, en elke zet verslechtert zijn stelling.'; @override - String get puzzleThemeHealthyMix => 'Gezonde mix'; + String get puzzleThemeMix => 'Gezonde mix'; @override - String get puzzleThemeHealthyMixDescription => 'Van alles wat. Je weet niet wat je te wachten staat, je moet dus op alles voorbereid zijn! Net als in echte partijen.'; + String get puzzleThemeMixDescription => 'Van alles wat. Je weet niet wat je te wachten staat, je moet dus op alles voorbereid zijn! Net als in echte partijen.'; @override String get puzzleThemePlayerGames => 'Eigen partijen'; @@ -1797,9 +2022,6 @@ class AppLocalizationsNl extends AppLocalizations { @override String get removesTheDepthLimit => 'Verwijdert de dieptelimiet, en houdt je computer warm'; - @override - String get engineManager => 'Engine-beheer'; - @override String get blunder => 'Blunder'; @@ -2063,6 +2285,9 @@ class AppLocalizationsNl extends AppLocalizations { @override String get gamesPlayed => 'Gespeelde partijen'; + @override + String get ok => 'Oké'; + @override String get cancel => 'Annuleren'; @@ -2772,7 +2997,13 @@ class AppLocalizationsNl extends AppLocalizations { String get other => 'Anders'; @override - String get reportDescriptionHelp => 'Plak de link naar de partij(en) en leg uit wat er mis is met het gedrag van de gebruiker. Zeg niet alleen \'hij speelt vals\', maar vertel ons hoe u bent gekomen op deze conclusie. Uw rapportage zal sneller worden verwerkt als het in het Engels is geschreven.'; + String get reportCheatBoostHelp => 'Plak de link naar de partij(en) en leg uit wat er mis is met het gedrag van de gebruiker. Zeg niet alleen \'hij speelt vals\', maar leg ook uit hoe je tot deze conclusie komt.'; + + @override + String get reportUsernameHelp => 'Leg uit wat er aan deze gebruikersnaam beledigend is. Zeg niet gewoon \"het is aanstootgevend/ongepast\", maar vertel ons hoe je tot deze conclusie komt, vooral als de belediging verhuld wordt, niet in het Engels is, in dialect is, of een historische of culturele verwijzing is.'; + + @override + String get reportProcessedFasterInEnglish => 'Je melding wordt sneller verwerkt als deze in het Engels is geschreven.'; @override String get error_provideOneCheatedGameLink => 'Geef ten minste één link naar een partij waarin vals gespeeld is.'; @@ -4077,6 +4308,9 @@ class AppLocalizationsNl extends AppLocalizations { @override String get nothingToSeeHere => 'Hier is momenteel niets te zien.'; + @override + String get stats => 'Statistieken'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4723,9 +4957,514 @@ class AppLocalizationsNl extends AppLocalizations { @override String get streamerLichessStreamers => 'Lichess streamers'; + @override + String get studyPrivate => 'Privé'; + + @override + String get studyMyStudies => 'Mijn Studies'; + + @override + String get studyStudiesIContributeTo => 'Studies waaraan ik bijdraag'; + + @override + String get studyMyPublicStudies => 'Mijn openbare studies'; + + @override + String get studyMyPrivateStudies => 'Mijn privé studies'; + + @override + String get studyMyFavoriteStudies => 'Mijn favoriete studies'; + + @override + String get studyWhatAreStudies => 'Wat zijn studies?'; + + @override + String get studyAllStudies => 'Alle studies'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Studies gemaakt door $param'; + } + + @override + String get studyNoneYet => 'Nog geen...'; + + @override + String get studyHot => 'Populair'; + + @override + String get studyDateAddedNewest => 'Datum toegevoegd (nieuwste)'; + + @override + String get studyDateAddedOldest => 'Datum toegevoegd (oudste)'; + + @override + String get studyRecentlyUpdated => 'Recent bijgewerkt'; + + @override + String get studyMostPopular => 'Meest populair'; + + @override + String get studyAlphabetical => 'Alfabetisch'; + + @override + String get studyAddNewChapter => 'Nieuw hoofdstuk toevoegen'; + + @override + String get studyAddMembers => 'Deelnemers toevoegen'; + + @override + String get studyInviteToTheStudy => 'Uitnodigen voor de studie'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Nodig alleen deelnemers uit die jou kennen en actief mee willen doen aan deze studie.'; + + @override + String get studySearchByUsername => 'Zoeken op gebruikersnaam'; + + @override + String get studySpectator => 'Kijker'; + + @override + String get studyContributor => 'Bijdrager'; + + @override + String get studyKick => 'Verwijder'; + + @override + String get studyLeaveTheStudy => 'Verlaat de studie'; + + @override + String get studyYouAreNowAContributor => 'Je bent nu een bijdrager'; + + @override + String get studyYouAreNowASpectator => 'Je bent nu een toeschouwer'; + + @override + String get studyPgnTags => 'PGN labels'; + + @override + String get studyLike => 'Vind ik leuk'; + + @override + String get studyUnlike => 'Vind ik niet meer leuk'; + + @override + String get studyNewTag => 'Nieuw label'; + + @override + String get studyCommentThisPosition => 'Reageer op deze positie'; + + @override + String get studyCommentThisMove => 'Reageer op deze zet'; + + @override + String get studyAnnotateWithGlyphs => 'Maak aantekeningen met symbolen'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Dit hoofdstuk is te kort om geanalyseerd te worden.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Alleen de bijdragers kunnen een computer analyse aanvragen.'; + + @override + String get studyGetAFullComputerAnalysis => 'Krijg een volledige computer analyse van de hoofdlijn.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Zorg ervoor dat het hoofdstuk voltooid is. Je kunt slechts één keer een analyse aanvragen.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Alle SYNC leden blijven op dezelfde positie'; + + @override + String get studyShareChanges => 'Deel veranderingen met toeschouwers en sla deze op op de server'; + + @override + String get studyPlaying => 'Spelend'; + + @override + String get studyShowEvalBar => 'Evaluatiebalk'; + + @override + String get studyFirst => 'Eerste'; + + @override + String get studyPrevious => 'Vorige'; + + @override + String get studyNext => 'Volgende'; + + @override + String get studyLast => 'Laatste'; + @override String get studyShareAndExport => 'Deel & exporteer'; + @override + String get studyCloneStudy => 'Kopiëren'; + + @override + String get studyStudyPgn => 'PGN bestuderen'; + + @override + String get studyDownloadAllGames => 'Download alle partijen'; + + @override + String get studyChapterPgn => 'Hoofdstuk PGN'; + + @override + String get studyCopyChapterPgn => 'PGN kopiëren'; + + @override + String get studyDownloadGame => 'Partij downloaden'; + + @override + String get studyStudyUrl => 'Studie URL'; + + @override + String get studyCurrentChapterUrl => 'Huidige hoofdstuk URL'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Je kunt deze link plakken wanneer je een bericht schrijft op het forum om de partij interactief weer te geven'; + + @override + String get studyStartAtInitialPosition => 'Begin bij de startpositie'; + + @override + String studyStartAtX(String param) { + return 'Beginnen bij $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Insluiten in blog of website'; + + @override + String get studyReadMoreAboutEmbedding => 'Lees meer over insluiten'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Alleen openbare studies kunnen worden ingevoegd!'; + + @override + String get studyOpen => 'Open'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1 aangeboden door $param2'; + } + + @override + String get studyStudyNotFound => 'Studie niet gevonden'; + + @override + String get studyEditChapter => 'Hoofdstuk bewerken'; + + @override + String get studyNewChapter => 'Nieuw hoofdstuk'; + + @override + String studyImportFromChapterX(String param) { + return 'Importeren van $param'; + } + + @override + String get studyOrientation => 'Oriëntatie'; + + @override + String get studyAnalysisMode => 'Analysemodus'; + + @override + String get studyPinnedChapterComment => 'Vastgezet commentaar van het hoofdstuk'; + + @override + String get studySaveChapter => 'Hoofdstuk opslaan'; + + @override + String get studyClearAnnotations => 'Wis aantekeningen'; + + @override + String get studyClearVariations => 'Verwijder variaties'; + + @override + String get studyDeleteChapter => 'Verwijder hoofdstuk'; + + @override + String get studyDeleteThisChapter => 'Wil je dit hoofdstuk verwijderen? Je kan dit niet ongedaan maken!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Verwijder alle aantekeningen, tekens en getekende figuren in dit hoofdstuk?'; + + @override + String get studyRightUnderTheBoard => 'Recht onder het bord'; + + @override + String get studyNoPinnedComment => 'Geen'; + + @override + String get studyNormalAnalysis => 'Normale analyse'; + + @override + String get studyHideNextMoves => 'Verberg volgende zetten'; + + @override + String get studyInteractiveLesson => 'Interactieve les'; + + @override + String studyChapterX(String param) { + return 'Hoofdstuk $param'; + } + + @override + String get studyEmpty => 'Leeg'; + + @override + String get studyStartFromInitialPosition => 'Start bij de initiële positie'; + + @override + String get studyEditor => 'Editor'; + + @override + String get studyStartFromCustomPosition => 'Start bij een aangepaste positie'; + + @override + String get studyLoadAGameByUrl => 'Laad partijen via een URL'; + + @override + String get studyLoadAPositionFromFen => 'Laad een spel via een FEN'; + + @override + String get studyLoadAGameFromPgn => 'Laad partijen via een PGN'; + + @override + String get studyAutomatic => 'Automatisch'; + + @override + String get studyUrlOfTheGame => 'URL van de partijen, één per regel'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Laad partijen van $param1 of $param2'; + } + + @override + String get studyCreateChapter => 'Creëer hoofdstuk'; + + @override + String get studyCreateStudy => 'Maak studie'; + + @override + String get studyEditStudy => 'Bewerk studie'; + + @override + String get studyVisibility => 'Zichtbaarheid'; + + @override + String get studyPublic => 'Openbaar'; + + @override + String get studyUnlisted => 'Niet openbaar'; + + @override + String get studyInviteOnly => 'Alleen op uitnodiging'; + + @override + String get studyAllowCloning => 'Klonen toestaan'; + + @override + String get studyNobody => 'Niemand'; + + @override + String get studyOnlyMe => 'Alleen ik'; + + @override + String get studyContributors => 'Bijdragers'; + + @override + String get studyMembers => 'Deelnemers'; + + @override + String get studyEveryone => 'Iedereen'; + + @override + String get studyEnableSync => 'Synchronisatie inschakelen'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Ja: houd iedereen op dezelfde stelling'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Nee: laat mensen vrij bladeren'; + + @override + String get studyPinnedStudyComment => 'Vastgezette studie reactie'; + @override String get studyStart => 'Start'; + + @override + String get studySave => 'Opslaan'; + + @override + String get studyClearChat => 'Maak de chat leeg'; + + @override + String get studyDeleteTheStudyChatHistory => 'Verwijder de studiechat geschiedenis? Er is geen weg terug!'; + + @override + String get studyDeleteStudy => 'Studie verwijderen'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'De hele studie verwijderen? Er is geen weg terug! Type de naam van de studie om te bevestigen dat je de studie wilt verwijderen: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Waar wil je dat bestuderen?'; + + @override + String get studyGoodMove => 'Goede zet'; + + @override + String get studyMistake => 'Fout'; + + @override + String get studyBrilliantMove => 'Briljante zet'; + + @override + String get studyBlunder => 'Blunder'; + + @override + String get studyInterestingMove => 'Interessante zet'; + + @override + String get studyDubiousMove => 'Dubieuze zet'; + + @override + String get studyOnlyMove => 'Enig mogelijke zet'; + + @override + String get studyZugzwang => 'Zetdwang'; + + @override + String get studyEqualPosition => 'Stelling in evenwicht'; + + @override + String get studyUnclearPosition => 'Onduidelijke stelling'; + + @override + String get studyWhiteIsSlightlyBetter => 'Wit staat iets beter'; + + @override + String get studyBlackIsSlightlyBetter => 'Zwart staat iets beter'; + + @override + String get studyWhiteIsBetter => 'Wit staat beter'; + + @override + String get studyBlackIsBetter => 'Zwart staat beter'; + + @override + String get studyWhiteIsWinning => 'Wit staat gewonnen'; + + @override + String get studyBlackIsWinning => 'Zwart staat gewonnen'; + + @override + String get studyNovelty => 'Noviteit'; + + @override + String get studyDevelopment => 'Ontwikkeling'; + + @override + String get studyInitiative => 'Initiatief'; + + @override + String get studyAttack => 'Aanval'; + + @override + String get studyCounterplay => 'Tegenspel'; + + @override + String get studyTimeTrouble => 'Tijdnood'; + + @override + String get studyWithCompensation => 'Met compensatie'; + + @override + String get studyWithTheIdea => 'Met het idee'; + + @override + String get studyNextChapter => 'Volgende hoofdstuk'; + + @override + String get studyPrevChapter => 'Vorige hoofdstuk'; + + @override + String get studyStudyActions => 'Studie sneltoetsen'; + + @override + String get studyTopics => 'Onderwerpen'; + + @override + String get studyMyTopics => 'Mijn onderwerpen'; + + @override + String get studyPopularTopics => 'Populaire onderwerpen'; + + @override + String get studyManageTopics => 'Onderwerpen beheren'; + + @override + String get studyBack => 'Terug'; + + @override + String get studyPlayAgain => 'Opnieuw spelen'; + + @override + String get studyWhatWouldYouPlay => 'Wat zou je in deze stelling spelen?'; + + @override + String get studyYouCompletedThisLesson => 'Gefeliciteerd! Je hebt deze les voltooid.'; + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hoofdstukken', + one: '$count hoofdstuk', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Partijen', + one: '$count Partij', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Deelnemers', + one: '$count Deelnemer', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Plak je PGN tekst hier, tot $count spellen mogelijk', + one: 'Plak je PGN tekst hier, tot $count spel mogelijk', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_nn.dart b/lib/l10n/l10n_nn.dart index ac94f6395c..003c052e73 100644 --- a/lib/l10n/l10n_nn.dart +++ b/lib/l10n/l10n_nn.dart @@ -103,9 +103,6 @@ class AppLocalizationsNn extends AppLocalizations { @override String get mobileCancelTakebackOffer => 'Avbryt tilbud om angrerett'; - @override - String get mobileCancelDrawOffer => 'Avbryt remistilbud'; - @override String get mobileWaitingForOpponentToJoin => 'Ventar på motspelar...'; @@ -142,7 +139,7 @@ class AppLocalizationsNn extends AppLocalizations { String get mobileGreetingWithoutName => 'Hei'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobilePrefMagnifyDraggedPiece => 'Forstørr brikke som vert trekt'; @override String get activityActivity => 'Aktivitet'; @@ -246,13 +243,24 @@ class AppLocalizationsNn extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Har spelt $count $param2-fjernsjakkparti', + one: 'Har spelt $count $param2-fjernsjakkparti', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Fylgjer $count spelarar', - one: 'Fylgjer $count spelar', + other: 'Følgjer $count spelarar', + one: 'Følgjer $count spelar', ); return '$_temp0'; } @@ -262,8 +270,8 @@ class AppLocalizationsNn extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Har $count nye fylgjarar', - one: 'Har $count nye fylgjarar', + other: 'Har $count nye følgjarar', + one: 'Har $count ny følgjar', ); return '$_temp0'; } @@ -348,9 +356,226 @@ class AppLocalizationsNn extends AppLocalizations { @override String get broadcastBroadcasts => 'Overføringar'; + @override + String get broadcastMyBroadcasts => 'Mine sendingar'; + @override String get broadcastLiveBroadcasts => 'Direktesende turneringar'; + @override + String get broadcastBroadcastCalendar => 'Kaldender for sendingar'; + + @override + String get broadcastNewBroadcast => 'Ny direktesending'; + + @override + String get broadcastSubscribedBroadcasts => 'Sendingar du abonnerar på'; + + @override + String get broadcastAboutBroadcasts => 'Om sending'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Korleis bruke Lichess-sendingar.'; + + @override + String get broadcastTheNewRoundHelp => 'Den nye runden vil ha same medlemar og bidragsytarar som den førre.'; + + @override + String get broadcastAddRound => 'Legg til ein runde'; + + @override + String get broadcastOngoing => 'Pågåande'; + + @override + String get broadcastUpcoming => 'Kommande'; + + @override + String get broadcastCompleted => 'Fullførde'; + + @override + String get broadcastCompletedHelp => 'Lichess detekterer ferdigspela rundar basert på kjeldeparita. Bruk denne innstillinga om det ikkje finst ei kjelde.'; + + @override + String get broadcastRoundName => 'Rundenamn'; + + @override + String get broadcastRoundNumber => 'Rundenummer'; + + @override + String get broadcastTournamentName => 'Turneringsnamn'; + + @override + String get broadcastTournamentDescription => 'Kortfatta skildring av turneringa'; + + @override + String get broadcastFullDescription => 'Full omtale av arrangementet'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Valfri lang omtale av overføringa. $param1 er tilgjengeleg. Omtalen må vera kortare enn $param2 teikn.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN kjelde-URL'; + + @override + String get broadcastSourceUrlHelp => 'Lenke som Lichess vil hente PGN-oppdateringar frå. Den må vera offentleg tilgjengeleg på internett.'; + + @override + String get broadcastSourceGameIds => 'Opp til 64 Lichess spel-ID\'ar, skilde med mellomrom.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Startdato i turneringas lokale tidssone: $param'; + } + + @override + String get broadcastStartDateHelp => 'Valfritt, om du veit når arrangementet startar'; + + @override + String get broadcastCurrentGameUrl => 'URL til pågåande parti'; + + @override + String get broadcastDownloadAllRounds => 'Last ned alle rundene'; + + @override + String get broadcastResetRound => 'Tilbakestill denne runden'; + + @override + String get broadcastDeleteRound => 'Slett denne runden'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Slett runden og tilhøyrande parti ugjenkalleleg.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Fjern alle parti frå denne runden. Kjelda må vera aktiv om dei skal kunne rettast opp att.'; + + @override + String get broadcastEditRoundStudy => 'Rediger rundestudie'; + + @override + String get broadcastDeleteTournament => 'Slett denne turneringa'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Slett heile turneringa med alle rundene og alle partia.'; + + @override + String get broadcastShowScores => 'Vis poengsummane til spelarar basert på spelresultatet deira'; + + @override + String get broadcastReplacePlayerTags => 'Valfritt: bytt ut spelarnamn, rangeringar og titlar'; + + @override + String get broadcastFideFederations => 'FIDE-forbund'; + + @override + String get broadcastTop10Rating => 'Topp 10 rating'; + + @override + String get broadcastFidePlayers => 'FIDE-spelarar'; + + @override + String get broadcastFidePlayerNotFound => 'Fann ikkje FIDE-spelar'; + + @override + String get broadcastFideProfile => 'FIDE-profil'; + + @override + String get broadcastFederation => 'Forbund'; + + @override + String get broadcastAgeThisYear => 'Alder i år'; + + @override + String get broadcastUnrated => 'Urangert'; + + @override + String get broadcastRecentTournaments => 'Nylegaste turneringar'; + + @override + String get broadcastOpenLichess => 'Opne i Lichess'; + + @override + String get broadcastTeams => 'Lag'; + + @override + String get broadcastBoards => 'Brett'; + + @override + String get broadcastOverview => 'Oversikt'; + + @override + String get broadcastSubscribeTitle => 'Abonner for å få melding når kvarr runde startar. I konto-innstillingane dine kan du velje kva form varslane skal sendas som.'; + + @override + String get broadcastUploadImage => 'Last opp turneringsbilete'; + + @override + String get broadcastNoBoardsYet => 'Førebels er det ikkje brett å syne. Desse vert først vist når spel er lasta opp.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Brett kan lastas med ei kjelde eller via $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Startar etter $param'; + } + + @override + String get broadcastStartVerySoon => 'Sending vil starte om ikkje lenge.'; + + @override + String get broadcastNotYetStarted => 'Sendinga har førebels ikkje starta.'; + + @override + String get broadcastOfficialWebsite => 'Offisiell nettside'; + + @override + String get broadcastStandings => 'Resultat'; + + @override + String broadcastIframeHelp(String param) { + return 'Fleire alternativ på $param'; + } + + @override + String get broadcastWebmastersPage => 'administratoren si side'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'Ei offentleg PGN-kjelde i sanntid for denne runden. Vi tilbyr og ei $param for raskare og meir effektiv synkronisering.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Bygg inn denne sendinga på nettstaden din'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Bygg inn $param på nettstaden din'; + } + + @override + String get broadcastRatingDiff => 'Rangeringsdiff'; + + @override + String get broadcastGamesThisTournament => 'Spel i denne turneringa'; + + @override + String get broadcastScore => 'Poengskår'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count sendingar', + one: '$count sending', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Utfordringar: $param1'; @@ -1390,10 +1615,10 @@ class AppLocalizationsNn extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Ei stilling der alle moglege trekk skadar stillinga.'; @override - String get puzzleThemeHealthyMix => 'Blanda drops'; + String get puzzleThemeMix => 'Blanda drops'; @override - String get puzzleThemeHealthyMixDescription => 'Litt av alt. Du veit ikkje kva du blir møtt med, så du må vera førebudd på det meste. Nett som i verkelege parti.'; + String get puzzleThemeMixDescription => 'Litt av alt. Du veit ikkje kva du blir møtt med, så du må vera førebudd på det meste. Nett som i verkelege parti.'; @override String get puzzleThemePlayerGames => 'Spelar parti'; @@ -1797,9 +2022,6 @@ class AppLocalizationsNn extends AppLocalizations { @override String get removesTheDepthLimit => 'Tar bort avgrensing i søke-djupna, og varmar opp maskina'; - @override - String get engineManager => 'Innstillingar for sjakkprogram'; - @override String get blunder => 'Bukk'; @@ -2063,6 +2285,9 @@ class AppLocalizationsNn extends AppLocalizations { @override String get gamesPlayed => 'Spelte parti'; + @override + String get ok => 'OK'; + @override String get cancel => 'Avbryt'; @@ -2410,13 +2635,13 @@ class AppLocalizationsNn extends AppLocalizations { String get favoriteOpponents => 'Favorittmotstandarar'; @override - String get follow => 'Fylgj'; + String get follow => 'Følg'; @override - String get following => 'Fylgjer'; + String get following => 'Følgjer'; @override - String get unfollow => 'Slutt å fylgja'; + String get unfollow => 'Slutt å følgja'; @override String followX(String param) { @@ -2438,11 +2663,11 @@ class AppLocalizationsNn extends AppLocalizations { String get unblock => 'Fjern blokkering'; @override - String get followsYou => 'Fylgjer deg'; + String get followsYou => 'Følgjer deg'; @override String xStartedFollowingY(String param1, String param2) { - return '$param1 byrja å fylgja $param2'; + return '$param1 byrja å følgja $param2'; } @override @@ -2772,7 +2997,13 @@ class AppLocalizationsNn extends AppLocalizations { String get other => 'Anna'; @override - String get reportDescriptionHelp => 'Lim inn link til partiet/partia og forklar kva som er gale med åtferda til denne brukaren.'; + String get reportCheatBoostHelp => 'Legg ved lenke til partiet/partia og forklar kva som er gale med åtferda til denne brukaren. Å berre påstå at brukaren juksar er ikkje nok, men gje ei nærare forklaring på korleis du kom til denne konklusjonen.'; + + @override + String get reportUsernameHelp => 'Forklår kva som gjer brukarnamnet er støytande. Det held ikkje med å påstå at \"namnet er støytande/upassande\", men fortell oss korleis du kom til denne konklusjonen, spesielt om tydinga er uklår, ikkje er på engelsk, er eit slanguttrykk, eller har ein historisk/kulturell referanse.'; + + @override + String get reportProcessedFasterInEnglish => 'Rapporten din blir raskare behandla om du skriv på engelsk.'; @override String get error_provideOneCheatedGameLink => 'Oppgje minst ei lenke til eit jukseparti.'; @@ -2845,7 +3076,7 @@ class AppLocalizationsNn extends AppLocalizations { String get privacyPolicy => 'Personvernpolitikk'; @override - String get letOtherPlayersFollowYou => 'Lat andre spelarar fylgja deg'; + String get letOtherPlayersFollowYou => 'Lat andre spelarar følgja deg'; @override String get letOtherPlayersChallengeYou => 'Lat andre spelarar utfordra deg'; @@ -4077,6 +4308,9 @@ class AppLocalizationsNn extends AppLocalizations { @override String get nothingToSeeHere => 'Ikkje noko å sjå nett no.'; + @override + String get stats => 'Statistikk'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4412,8 +4646,8 @@ class AppLocalizationsNn extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count fylgjarar', - one: '$count fylgjar', + other: '$count følgjarar', + one: '$count følgjar', ); return '$_temp0'; } @@ -4424,7 +4658,7 @@ class AppLocalizationsNn extends AppLocalizations { count, locale: localeName, other: '$count fylgjer', - one: '$count fylgjer', + one: '$count følgjer', ); return '$_temp0'; } @@ -4723,9 +4957,514 @@ class AppLocalizationsNn extends AppLocalizations { @override String get streamerLichessStreamers => 'Lichess-strøymarar'; + @override + String get studyPrivate => 'Privat'; + + @override + String get studyMyStudies => 'Mine studiar'; + + @override + String get studyStudiesIContributeTo => 'Studiar eg bidreg til'; + + @override + String get studyMyPublicStudies => 'Mine offentlege studiar'; + + @override + String get studyMyPrivateStudies => 'Mine private studiar'; + + @override + String get studyMyFavoriteStudies => 'Mine favorittstudiar'; + + @override + String get studyWhatAreStudies => 'Kva er studiar?'; + + @override + String get studyAllStudies => 'Alle studiar'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Studiar oppretta av $param'; + } + + @override + String get studyNoneYet => 'Ingen så langt.'; + + @override + String get studyHot => 'Omtykt'; + + @override + String get studyDateAddedNewest => 'Dato tilføydd (siste)'; + + @override + String get studyDateAddedOldest => 'Dato tilføydd (første)'; + + @override + String get studyRecentlyUpdated => 'Nyleg oppdatert'; + + @override + String get studyMostPopular => 'Mest omtykt'; + + @override + String get studyAlphabetical => 'Alfabetisk'; + + @override + String get studyAddNewChapter => 'Føy til eit nytt kapittel'; + + @override + String get studyAddMembers => 'Legg til medlemar'; + + @override + String get studyInviteToTheStudy => 'Inviter til studien'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Inviter berre folk du kjenner og som aktivt ynskjer å delta i studien.'; + + @override + String get studySearchByUsername => 'Søk på brukarnamn'; + + @override + String get studySpectator => 'Tilskodar'; + + @override + String get studyContributor => 'Bidragsytar'; + + @override + String get studyKick => 'Kast ut'; + + @override + String get studyLeaveTheStudy => 'Forlat studien'; + + @override + String get studyYouAreNowAContributor => 'Du er no bidragsytar'; + + @override + String get studyYouAreNowASpectator => 'Du er no tilskodar'; + + @override + String get studyPgnTags => 'PGN-merkelappar'; + + @override + String get studyLike => 'Lik'; + + @override + String get studyUnlike => 'Slutt å lika'; + + @override + String get studyNewTag => 'Ny merkelapp'; + + @override + String get studyCommentThisPosition => 'Kommenter denne stillinga'; + + @override + String get studyCommentThisMove => 'Kommenter dette trekket'; + + @override + String get studyAnnotateWithGlyphs => 'Kommenter med symbol'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Kapittelet er for kort for å analyserast.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Berre bidragsytarar til studien kan be om maskinanalyse.'; + + @override + String get studyGetAFullComputerAnalysis => 'Få full maskinanalyse av hovedvarianten frå serveren.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Sørg for at kapittelet er fullført. Du kan berre be om analyse ein gong.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Alle SYNC-medlemene ser den same stillingen'; + + @override + String get studyShareChanges => 'Lagre endringar på serveren og del dei med tilskodarar'; + + @override + String get studyPlaying => 'Spelar no'; + + @override + String get studyShowEvalBar => 'Evalueringssøyler'; + + @override + String get studyFirst => 'Første'; + + @override + String get studyPrevious => 'Attende'; + + @override + String get studyNext => 'Neste'; + + @override + String get studyLast => 'Siste'; + @override String get studyShareAndExport => 'Del & eksporter'; + @override + String get studyCloneStudy => 'Klon'; + + @override + String get studyStudyPgn => 'Studie-PGN'; + + @override + String get studyDownloadAllGames => 'Last ned alle spel'; + + @override + String get studyChapterPgn => 'Kapittel-PGN'; + + @override + String get studyCopyChapterPgn => 'Kopier PGN'; + + @override + String get studyDownloadGame => 'Last ned spel'; + + @override + String get studyStudyUrl => 'Studie-URL'; + + @override + String get studyCurrentChapterUrl => 'Kapittel-URL'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Du kan lime inn dette i forumet for å syna det der'; + + @override + String get studyStartAtInitialPosition => 'Start ved innleiande stilling'; + + @override + String studyStartAtX(String param) { + return 'Start ved $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Inkorporer i websida eller bloggen din'; + + @override + String get studyReadMoreAboutEmbedding => 'Les meir om innbygging'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Berre offentlege studiar kan byggast inn!'; + + @override + String get studyOpen => 'Opne'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1 presentert av $param2'; + } + + @override + String get studyStudyNotFound => 'Fann ikkje studien'; + + @override + String get studyEditChapter => 'Rediger kapittel'; + + @override + String get studyNewChapter => 'Nytt kapittel'; + + @override + String studyImportFromChapterX(String param) { + return 'Importer frå $param'; + } + + @override + String get studyOrientation => 'Retning'; + + @override + String get studyAnalysisMode => 'Analysemodus'; + + @override + String get studyPinnedChapterComment => 'Fastspikra kapittelkommentar'; + + @override + String get studySaveChapter => 'Lagre kapittelet'; + + @override + String get studyClearAnnotations => 'Fjern notat'; + + @override + String get studyClearVariations => 'Fjern variantar'; + + @override + String get studyDeleteChapter => 'Slett kapittel'; + + @override + String get studyDeleteThisChapter => 'Slette dette kapittelet? Avgjerda er endeleg og kan ikkje angrast!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Fjern alle kommentarar og figurar i dette kapittelet?'; + + @override + String get studyRightUnderTheBoard => 'Rett under brettet'; + + @override + String get studyNoPinnedComment => 'Ingen'; + + @override + String get studyNormalAnalysis => 'Normal analyse'; + + @override + String get studyHideNextMoves => 'Skjul neste trekk'; + + @override + String get studyInteractiveLesson => 'Interaktiv leksjon'; + + @override + String studyChapterX(String param) { + return 'Kapittel $param'; + } + + @override + String get studyEmpty => 'Tom'; + + @override + String get studyStartFromInitialPosition => 'Start ved innleiande stilling'; + + @override + String get studyEditor => 'Editor'; + + @override + String get studyStartFromCustomPosition => 'Start frå innleiande stilling'; + + @override + String get studyLoadAGameByUrl => 'Last opp eit parti frå URL'; + + @override + String get studyLoadAPositionFromFen => 'Last opp ein stilling frå FEN'; + + @override + String get studyLoadAGameFromPgn => 'Last opp eit parti frå PGN'; + + @override + String get studyAutomatic => 'Automatisk'; + + @override + String get studyUrlOfTheGame => 'URL for partiet'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Last opp eit parti frå $param1 eller $param2'; + } + + @override + String get studyCreateChapter => 'Opprett kapittel'; + + @override + String get studyCreateStudy => 'Opprett ein studie'; + + @override + String get studyEditStudy => 'Rediger studie'; + + @override + String get studyVisibility => 'Synlegheit'; + + @override + String get studyPublic => 'Offentleg'; + + @override + String get studyUnlisted => 'Ikkje opplista'; + + @override + String get studyInviteOnly => 'Berre etter invitasjon'; + + @override + String get studyAllowCloning => 'Tillat kloning'; + + @override + String get studyNobody => 'Ingen'; + + @override + String get studyOnlyMe => 'Berre meg'; + + @override + String get studyContributors => 'Bidragsytarar'; + + @override + String get studyMembers => 'Medlemar'; + + @override + String get studyEveryone => 'Alle'; + + @override + String get studyEnableSync => 'Aktiver synk'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Ja: behald alle i den same stilllinga'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Nei: lat folk sjå fritt gjennom'; + + @override + String get studyPinnedStudyComment => 'Fastspikra studiekommentar'; + @override String get studyStart => 'Start'; + + @override + String get studySave => 'Lagre'; + + @override + String get studyClearChat => 'Fjern teksten frå kommentarfeltet'; + + @override + String get studyDeleteTheStudyChatHistory => 'Slette studiens kommentar-historikk? Du kan ikkje angre!'; + + @override + String get studyDeleteStudy => 'Slett studie'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Slette heile studien? Avgjerda er endeleg og kan ikke gjeras om! Skriv namnet på studien som skal stadfestast: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Kva for ein studie vil du bruke?'; + + @override + String get studyGoodMove => 'Godt trekk'; + + @override + String get studyMistake => 'Mistak'; + + @override + String get studyBrilliantMove => 'Strålande trekk'; + + @override + String get studyBlunder => 'Bukk'; + + @override + String get studyInterestingMove => 'Interessant trekk'; + + @override + String get studyDubiousMove => 'Tvilsamt trekk'; + + @override + String get studyOnlyMove => 'Einaste moglege trekk'; + + @override + String get studyZugzwang => 'Trekktvang'; + + @override + String get studyEqualPosition => 'Lik stilling'; + + @override + String get studyUnclearPosition => 'Uavklart stilling'; + + @override + String get studyWhiteIsSlightlyBetter => 'Kvit står litt betre'; + + @override + String get studyBlackIsSlightlyBetter => 'Svart står litt betre'; + + @override + String get studyWhiteIsBetter => 'Kvit står betre'; + + @override + String get studyBlackIsBetter => 'Svart står betre'; + + @override + String get studyWhiteIsWinning => 'Kvit står til vinst'; + + @override + String get studyBlackIsWinning => 'Svart står til vinst'; + + @override + String get studyNovelty => 'Nyskapning'; + + @override + String get studyDevelopment => 'Utvikling'; + + @override + String get studyInitiative => 'Initiativ'; + + @override + String get studyAttack => 'Åtak'; + + @override + String get studyCounterplay => 'Motspel'; + + @override + String get studyTimeTrouble => 'Tidsnaud'; + + @override + String get studyWithCompensation => 'Med kompensasjon'; + + @override + String get studyWithTheIdea => 'Med ideen'; + + @override + String get studyNextChapter => 'Neste kapittel'; + + @override + String get studyPrevChapter => 'Førre kapittel'; + + @override + String get studyStudyActions => 'Studiehandlingar'; + + @override + String get studyTopics => 'Tema'; + + @override + String get studyMyTopics => 'Mine tema'; + + @override + String get studyPopularTopics => 'Omtykte tema'; + + @override + String get studyManageTopics => 'Administrer tema'; + + @override + String get studyBack => 'Tilbake'; + + @override + String get studyPlayAgain => 'Spel på ny'; + + @override + String get studyWhatWouldYouPlay => 'Kva vil du spela i denne stillinga?'; + + @override + String get studyYouCompletedThisLesson => 'Gratulerar! Du har fullført denne leksjonen.'; + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count kapittel', + one: '$count kapittel', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count parti', + one: '$count parti', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count medlemar', + one: '$count medlem', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Sett inn PGN-teksten din her, maksimum $count parti', + one: 'Sett inn PGN-teksten din her, maksimum $count parti', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_pl.dart b/lib/l10n/l10n_pl.dart index ac34b50c46..d1148a26d7 100644 --- a/lib/l10n/l10n_pl.dart +++ b/lib/l10n/l10n_pl.dart @@ -103,9 +103,6 @@ class AppLocalizationsPl extends AppLocalizations { @override String get mobileCancelTakebackOffer => 'Anuluj prośbę cofnięcia ruchu'; - @override - String get mobileCancelDrawOffer => 'Anuluj propozycję remisu'; - @override String get mobileWaitingForOpponentToJoin => 'Oczekiwanie na dołączenie przeciwnika...'; @@ -142,7 +139,7 @@ class AppLocalizationsPl extends AppLocalizations { String get mobileGreetingWithoutName => 'Witaj'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobilePrefMagnifyDraggedPiece => 'Powiększ przeciąganą bierkę'; @override String get activityActivity => 'Aktywność'; @@ -262,6 +259,19 @@ class AppLocalizationsPl extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Zakończone $count $param2 partii korespondencyjnych', + many: 'Zakończone $count $param2 partii korespondencyjnych', + few: 'Zakończone $count $param2 partie korespondencyjne', + one: 'Zakończona $count $param2 partia korespondencyjna', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -382,9 +392,228 @@ class AppLocalizationsPl extends AppLocalizations { @override String get broadcastBroadcasts => 'Transmisje'; + @override + String get broadcastMyBroadcasts => 'Moje transmisje'; + @override String get broadcastLiveBroadcasts => 'Transmisje turniejów na żywo'; + @override + String get broadcastBroadcastCalendar => 'Kalendarz transmisji'; + + @override + String get broadcastNewBroadcast => 'Nowa transmisja na żywo'; + + @override + String get broadcastSubscribedBroadcasts => 'Subskrybowane transmisje'; + + @override + String get broadcastAboutBroadcasts => 'O transmisji'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Jak korzystać z transmisji na Lichess.'; + + @override + String get broadcastTheNewRoundHelp => 'Nowa runda będzie miała tych samych uczestników co poprzednia.'; + + @override + String get broadcastAddRound => 'Dodaj rundę'; + + @override + String get broadcastOngoing => 'Trwające'; + + @override + String get broadcastUpcoming => 'Nadchodzące'; + + @override + String get broadcastCompleted => 'Zakończone'; + + @override + String get broadcastCompletedHelp => 'Lichess wykrywa ukończenie rundy w oparciu o śledzone partie. Użyj tego przełącznika, jeśli nie ma takich partii.'; + + @override + String get broadcastRoundName => 'Nazwa rundy'; + + @override + String get broadcastRoundNumber => 'Numer rundy'; + + @override + String get broadcastTournamentName => 'Nazwa turnieju'; + + @override + String get broadcastTournamentDescription => 'Krótki opis turnieju'; + + @override + String get broadcastFullDescription => 'Pełny opis wydarzenia'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Opcjonalny długi opis transmisji. $param1 jest dostępny. Długość musi być mniejsza niż $param2 znaków.'; + } + + @override + String get broadcastSourceSingleUrl => 'Adres URL zapisu PGN'; + + @override + String get broadcastSourceUrlHelp => 'Adres URL, który Lichess będzie udostępniał, aby można było uzyskać aktualizacje PGN. Musi być publicznie dostępny z internetu.'; + + @override + String get broadcastSourceGameIds => 'Do 64 identyfikatorów partii, oddzielonych spacjami.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Data rozpoczęcia w lokalnej strefie czasowej turnieju: $param'; + } + + @override + String get broadcastStartDateHelp => 'Opcjonalne, jeśli wiesz kiedy wydarzenie się rozpocznie'; + + @override + String get broadcastCurrentGameUrl => 'Adres URL bieżącej partii'; + + @override + String get broadcastDownloadAllRounds => 'Pobierz wszystkie rundy'; + + @override + String get broadcastResetRound => 'Zresetuj tę rundę'; + + @override + String get broadcastDeleteRound => 'Usuń tę rundę'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Ostatecznie usuń rundę i jej wszystkie partie.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Usuń wszystkie partie w tej rundzie. Źródło będzie musiało być aktywne, aby je odtworzyć.'; + + @override + String get broadcastEditRoundStudy => 'Edytuj opracowanie rundy'; + + @override + String get broadcastDeleteTournament => 'Usuń ten turniej'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Ostatecznie usuń cały turniej, jego wszystkie rundy i partie.'; + + @override + String get broadcastShowScores => 'Pokaż wyniki graczy na podstawie wyników gry'; + + @override + String get broadcastReplacePlayerTags => 'Opcjonalnie: zmień nazwy, rankingi oraz tytuły gracza'; + + @override + String get broadcastFideFederations => 'Federacje FIDE'; + + @override + String get broadcastTop10Rating => '10 najlepszych rankingów'; + + @override + String get broadcastFidePlayers => 'Zawodnicy FIDE'; + + @override + String get broadcastFidePlayerNotFound => 'Nie znaleziono zawodnika FIDE'; + + @override + String get broadcastFideProfile => 'Profil FIDE'; + + @override + String get broadcastFederation => 'Federacja'; + + @override + String get broadcastAgeThisYear => 'Wiek w tym roku'; + + @override + String get broadcastUnrated => 'Bez rankingu'; + + @override + String get broadcastRecentTournaments => 'Najnowsze turnieje'; + + @override + String get broadcastOpenLichess => 'Otwórz w Lichess'; + + @override + String get broadcastTeams => 'Drużyny'; + + @override + String get broadcastBoards => 'Szachownice'; + + @override + String get broadcastOverview => 'Podgląd'; + + @override + String get broadcastSubscribeTitle => 'Subskrybuj, aby dostawać powiadomienia o każdej rozpoczętej rundzie. W preferencjach konta możesz przełączać czy chcesz powiadomienia dźwiękowe czy wyskakujące notyfikacje tekstowe.'; + + @override + String get broadcastUploadImage => 'Prześlij logo turnieju'; + + @override + String get broadcastNoBoardsYet => 'Szachownice pojawią się jak tylko załadują się partie.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Szachownice mogą być załadowane bezpośrednio ze źródła lub przez $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Rozpoczyna się po $param'; + } + + @override + String get broadcastStartVerySoon => 'Transmisja wkrótce się rozpocznie.'; + + @override + String get broadcastNotYetStarted => 'Transmisja jeszcze się nie rozpoczęła.'; + + @override + String get broadcastOfficialWebsite => 'Oficjalna strona'; + + @override + String get broadcastStandings => 'Klasyfikacja'; + + @override + String broadcastIframeHelp(String param) { + return 'Więcej opcji na $param'; + } + + @override + String get broadcastWebmastersPage => 'stronie webmasterów'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'Publiczne źródło PGN w czasie rzeczywistym dla tej rundy. Oferujemy również $param dla szybszej i skuteczniejszej synchronizacji.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Umieść tę transmisję na swojej stronie internetowej'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Osadź $param na swojej stronie internetowej'; + } + + @override + String get broadcastRatingDiff => 'Różnica rankingu'; + + @override + String get broadcastGamesThisTournament => 'Partie w tym turnieju'; + + @override + String get broadcastScore => 'Wynik'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count transmisji', + many: '$count transmisji', + few: '$count transmisje', + one: '$count transmisja', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Wyzwania: $param1'; @@ -490,7 +719,7 @@ class AppLocalizationsPl extends AppLocalizations { String get perfStatProvisional => 'prowizoryczny'; @override - String get perfStatNotEnoughRatedGames => 'Nie zagrano wystarczająco dużo rankingowych gier, aby ustalić wiarygodną ranking.'; + String get perfStatNotEnoughRatedGames => 'Nie zagrano wystarczająco dużo rankingowych gier, aby ustalić wiarygodny ranking.'; @override String perfStatProgressOverLastXGames(String param) { @@ -1434,10 +1663,10 @@ class AppLocalizationsPl extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Ograniczone ruchy przeciwnika powodują, że każde posunięcie pogarsza jego pozycję.'; @override - String get puzzleThemeHealthyMix => 'Miszmasz'; + String get puzzleThemeMix => 'Miszmasz'; @override - String get puzzleThemeHealthyMixDescription => 'Bądź gotów na wszystko! Jak podczas prawdziwej partii.'; + String get puzzleThemeMixDescription => 'Po trochu wszystkiego. Nie wiesz czego się spodziewać, więc bądź gotów na wszystko! Tak jak w prawdziwej partii.'; @override String get puzzleThemePlayerGames => 'Partie gracza'; @@ -1841,9 +2070,6 @@ class AppLocalizationsPl extends AppLocalizations { @override String get removesTheDepthLimit => 'Usuwa limit głębokości analizy i rozgrzewa Twój komputer do czerwoności ;)'; - @override - String get engineManager => 'Ustawienia silnika'; - @override String get blunder => 'Błąd'; @@ -2107,6 +2333,9 @@ class AppLocalizationsPl extends AppLocalizations { @override String get gamesPlayed => 'Rozegranych partii'; + @override + String get ok => 'OK'; + @override String get cancel => 'Anuluj'; @@ -2759,7 +2988,7 @@ class AppLocalizationsPl extends AppLocalizations { String get website => 'Strona internetowa'; @override - String get mobile => 'Mobile'; + String get mobile => 'Aplikacja mobilna'; @override String get help => 'Porada:'; @@ -2816,7 +3045,13 @@ class AppLocalizationsPl extends AppLocalizations { String get other => 'Inne'; @override - String get reportDescriptionHelp => 'Wklej odnośnik do partii i wyjaśnij, co złego jest w zachowaniu tego użytkownika. Nie pisz tylko, że „oszukuje”, ale wytłumacz nam, na jakiej podstawie doszedłeś/aś do takiego wniosku. Odniesiemy się do twojego zgłoszenia szybciej, jeżeli napiszesz je w języku angielskim.'; + String get reportCheatBoostHelp => 'Wklej link do partii i wytłumacz, co złego jest w zachowaniu tego użytkownika. Nie pisz tylko \"on oszukiwał\", lecz napisz jak doszedłeś/aś do tego wniosku.'; + + @override + String get reportUsernameHelp => 'Wytłumacz, co w nazwie użytkownika jest obraźliwe. Nie pisz tylko \"nazwa jest obraźliwa\", lecz napisz jak doszedłeś/aś do tego wniosku, zwłaszcza jeśli jest mało znany, nie po angielsku, slangowy lub odniesieniem do kultury/historii.'; + + @override + String get reportProcessedFasterInEnglish => 'Twoje zgłoszenie będzie sprawdzone szybciej, jeśli zostanie napisane po angielsku.'; @override String get error_provideOneCheatedGameLink => 'Podaj przynajmniej jeden odnośnik do gry, w której oszukiwano.'; @@ -4121,6 +4356,9 @@ class AppLocalizationsPl extends AppLocalizations { @override String get nothingToSeeHere => 'W tej chwili nie ma nic do zobaczenia.'; + @override + String get stats => 'Statystyki'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4855,9 +5093,522 @@ class AppLocalizationsPl extends AppLocalizations { @override String get streamerLichessStreamers => 'Streamerzy Lichess'; + @override + String get studyPrivate => 'Prywatne'; + + @override + String get studyMyStudies => 'Moje opracowania'; + + @override + String get studyStudiesIContributeTo => 'Opracowania, które współtworzę'; + + @override + String get studyMyPublicStudies => 'Moje publiczne opracowania'; + + @override + String get studyMyPrivateStudies => 'Moje prywatne opracowania'; + + @override + String get studyMyFavoriteStudies => 'Moje ulubione opracowania'; + + @override + String get studyWhatAreStudies => 'Czym są opracowania?'; + + @override + String get studyAllStudies => 'Wszystkie opracowania'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Opracowanie stworzone przez $param'; + } + + @override + String get studyNoneYet => 'Jeszcze brak.'; + + @override + String get studyHot => 'Hity'; + + @override + String get studyDateAddedNewest => 'Data dodania (od najnowszych)'; + + @override + String get studyDateAddedOldest => 'Data dodania (od najstarszych)'; + + @override + String get studyRecentlyUpdated => 'Ostatnio aktualizowane'; + + @override + String get studyMostPopular => 'Najpopularniejsze'; + + @override + String get studyAlphabetical => 'Alfabetycznie'; + + @override + String get studyAddNewChapter => 'Dodaj nowy rozdział'; + + @override + String get studyAddMembers => 'Dodaj uczestników'; + + @override + String get studyInviteToTheStudy => 'Zaproś do opracowania'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Zapraszaj do opracowania tylko znajomych, którzy chcą w nim aktywnie uczestniczyć.'; + + @override + String get studySearchByUsername => 'Szukaj wg nazwy użytkownika'; + + @override + String get studySpectator => 'Obserwator'; + + @override + String get studyContributor => 'Współautor'; + + @override + String get studyKick => 'Wyrzuć'; + + @override + String get studyLeaveTheStudy => 'Opuść opracowanie'; + + @override + String get studyYouAreNowAContributor => 'Jesteś teraz współautorem'; + + @override + String get studyYouAreNowASpectator => 'Jesteś teraz obserwatorem'; + + @override + String get studyPgnTags => 'Znaczniki PGN'; + + @override + String get studyLike => 'Lubię to'; + + @override + String get studyUnlike => 'Cofnij polubienie'; + + @override + String get studyNewTag => 'Nowy znacznik'; + + @override + String get studyCommentThisPosition => 'Skomentuj tę pozycję'; + + @override + String get studyCommentThisMove => 'Skomentuj ten ruch'; + + @override + String get studyAnnotateWithGlyphs => 'Dodaj adnotacje symbolami'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Rozdział jest zbyt krótki do analizy.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Tylko współautorzy opracowania mogą prosić o analizę komputerową.'; + + @override + String get studyGetAFullComputerAnalysis => 'Uzyskaj pełną, zdalną analizę komputerową głównego wariantu.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Upewnij się, że rozdział jest kompletny. O jego analizę możesz poprosić tylko raz.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Wszyscy zsynchronizowani uczestnicy pozostają na tej samej pozycji'; + + @override + String get studyShareChanges => 'Współdzielenie zmian z obserwatorami i ich zapis na serwerze'; + + @override + String get studyPlaying => 'W toku'; + + @override + String get studyShowEvalBar => 'Paski ewaluacji'; + + @override + String get studyFirst => 'Pierwszy'; + + @override + String get studyPrevious => 'Poprzedni'; + + @override + String get studyNext => 'Następny'; + + @override + String get studyLast => 'Ostatni'; + @override String get studyShareAndExport => 'Udostępnianie i eksport'; + @override + String get studyCloneStudy => 'Powiel'; + + @override + String get studyStudyPgn => 'PGN opracowania'; + + @override + String get studyDownloadAllGames => 'Pobierz wszystkie partie'; + + @override + String get studyChapterPgn => 'PGN rozdziału'; + + @override + String get studyCopyChapterPgn => 'Kopiuj PGN'; + + @override + String get studyDownloadGame => 'Pobierz partię'; + + @override + String get studyStudyUrl => 'Link do opracowania'; + + @override + String get studyCurrentChapterUrl => 'URL bieżącego rozdziału'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Możesz wkleić to, aby osadzić na forum'; + + @override + String get studyStartAtInitialPosition => 'Rozpocznij z pozycji początkowej'; + + @override + String studyStartAtX(String param) { + return 'Rozpocznij od $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Udostępnij na swojej stronie lub na blogu'; + + @override + String get studyReadMoreAboutEmbedding => 'Dowiedz się więcej o osadzaniu'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Tylko publiczne opracowania mogą być osadzane!'; + + @override + String get studyOpen => 'Otwórz'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1 przygotowane przez $param2'; + } + + @override + String get studyStudyNotFound => 'Nie znaleziono opracowania'; + + @override + String get studyEditChapter => 'Edytuj rozdział'; + + @override + String get studyNewChapter => 'Nowy rozdział'; + + @override + String studyImportFromChapterX(String param) { + return 'Zaimportuj z $param'; + } + + @override + String get studyOrientation => 'Orientacja'; + + @override + String get studyAnalysisMode => 'Rodzaj analizy'; + + @override + String get studyPinnedChapterComment => 'Przypięty komentarz'; + + @override + String get studySaveChapter => 'Zapisz rozdział'; + + @override + String get studyClearAnnotations => 'Usuń adnotacje'; + + @override + String get studyClearVariations => 'Wyczyść warianty'; + + @override + String get studyDeleteChapter => 'Usuń rozdział'; + + @override + String get studyDeleteThisChapter => 'Usunąć ten rozdział? Nie będzie można tego cofnąć!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Usunąć wszystkie komentarze i oznaczenia w tym rozdziale?'; + + @override + String get studyRightUnderTheBoard => 'Pod szachownicą, po prawej stronie'; + + @override + String get studyNoPinnedComment => 'Brak'; + + @override + String get studyNormalAnalysis => 'Normalna'; + + @override + String get studyHideNextMoves => 'Ukryj następne posunięcia'; + + @override + String get studyInteractiveLesson => 'Lekcja interaktywna'; + + @override + String studyChapterX(String param) { + return 'Rozdział $param'; + } + + @override + String get studyEmpty => 'Pusty'; + + @override + String get studyStartFromInitialPosition => 'Rozpocznij z pozycji początkowej'; + + @override + String get studyEditor => 'Edytor'; + + @override + String get studyStartFromCustomPosition => 'Rozpocznij z ustawionej pozycji'; + + @override + String get studyLoadAGameByUrl => 'Zaimportuj partię z linku'; + + @override + String get studyLoadAPositionFromFen => 'Zaimportuj partię z FEN'; + + @override + String get studyLoadAGameFromPgn => 'Zaimportuj partię z PGN'; + + @override + String get studyAutomatic => 'Automatycznie'; + + @override + String get studyUrlOfTheGame => 'Link do partii'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Zaimportuj partię z $param1 lub $param2'; + } + + @override + String get studyCreateChapter => 'Stwórz rozdział'; + + @override + String get studyCreateStudy => 'Stwórz opracowanie'; + + @override + String get studyEditStudy => 'Edytuj opracowanie'; + + @override + String get studyVisibility => 'Widoczność'; + + @override + String get studyPublic => 'Publiczne'; + + @override + String get studyUnlisted => 'Niepubliczne'; + + @override + String get studyInviteOnly => 'Tylko zaproszeni'; + + @override + String get studyAllowCloning => 'Pozwól kopiować'; + + @override + String get studyNobody => 'Nikt'; + + @override + String get studyOnlyMe => 'Tylko ja'; + + @override + String get studyContributors => 'Współautorzy'; + + @override + String get studyMembers => 'Uczestnicy'; + + @override + String get studyEveryone => 'Każdy'; + + @override + String get studyEnableSync => 'Włącz synchronizację'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Tak: utrzymaj wszystkich w tej samej pozycji'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Nie: pozwól oglądać wszystkim'; + + @override + String get studyPinnedStudyComment => 'Przypięte komentarze'; + @override String get studyStart => 'Rozpocznij'; + + @override + String get studySave => 'Zapisz'; + + @override + String get studyClearChat => 'Wyczyść czat'; + + @override + String get studyDeleteTheStudyChatHistory => 'Usunąć historię czatu opracowania? Nie będzie można tego cofnąć!'; + + @override + String get studyDeleteStudy => 'Usuń opracowanie'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Usunąć opracowanie? Nie będzie można go odzyskać! Wpisz nazwę opracowania, aby potwierdzić operację: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Gdzie chcesz się tego uczyć?'; + + @override + String get studyGoodMove => 'Dobry ruch'; + + @override + String get studyMistake => 'Pomyłka'; + + @override + String get studyBrilliantMove => 'Świetny ruch'; + + @override + String get studyBlunder => 'Błąd'; + + @override + String get studyInterestingMove => 'Interesujący ruch'; + + @override + String get studyDubiousMove => 'Wątpliwy ruch'; + + @override + String get studyOnlyMove => 'Jedyny ruch'; + + @override + String get studyZugzwang => 'Zugzwang'; + + @override + String get studyEqualPosition => 'Równa pozycja'; + + @override + String get studyUnclearPosition => 'Niejasna pozycja'; + + @override + String get studyWhiteIsSlightlyBetter => 'Białe stoją nieznacznie lepiej'; + + @override + String get studyBlackIsSlightlyBetter => 'Czarne stoją nieznacznie lepiej'; + + @override + String get studyWhiteIsBetter => 'Białe stoją lepiej'; + + @override + String get studyBlackIsBetter => 'Czarne stoją lepiej'; + + @override + String get studyWhiteIsWinning => 'Białe wygrywają'; + + @override + String get studyBlackIsWinning => 'Czarne wygrywają'; + + @override + String get studyNovelty => 'Nowość'; + + @override + String get studyDevelopment => 'Rozwój'; + + @override + String get studyInitiative => 'Inicjatywa'; + + @override + String get studyAttack => 'Atak'; + + @override + String get studyCounterplay => 'Przeciwdziałanie'; + + @override + String get studyTimeTrouble => 'Problem z czasem'; + + @override + String get studyWithCompensation => 'Z rekompensatą'; + + @override + String get studyWithTheIdea => 'Z pomysłem'; + + @override + String get studyNextChapter => 'Następny rozdział'; + + @override + String get studyPrevChapter => 'Poprzedni rozdział'; + + @override + String get studyStudyActions => 'Opcje opracowań'; + + @override + String get studyTopics => 'Tematy'; + + @override + String get studyMyTopics => 'Moje tematy'; + + @override + String get studyPopularTopics => 'Popularne tematy'; + + @override + String get studyManageTopics => 'Zarządzaj tematami'; + + @override + String get studyBack => 'Powrót'; + + @override + String get studyPlayAgain => 'Odtwórz ponownie'; + + @override + String get studyWhatWouldYouPlay => 'Co byś zagrał w tej pozycji?'; + + @override + String get studyYouCompletedThisLesson => 'Gratulacje! Ukończono tę lekcję.'; + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count rozdziałów', + many: '$count rozdziałów', + few: '$count rozdziały', + one: '$count rozdział', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count partii', + many: '$count partii', + few: '$count partie', + one: '$count partia', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count uczestników', + many: '$count uczestników', + few: '$count uczestników', + one: '$count uczestnik', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Wklej tutaj swój PGN, max $count partii', + many: 'Wklej tutaj swój PGN, max $count partii', + few: 'Wklej tutaj swój PGN, max $count partie', + one: 'Wklej tutaj swój PGN, max $count partię', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_pt.dart b/lib/l10n/l10n_pt.dart index fc5c158b3a..e933087200 100644 --- a/lib/l10n/l10n_pt.dart +++ b/lib/l10n/l10n_pt.dart @@ -103,9 +103,6 @@ class AppLocalizationsPt extends AppLocalizations { @override String get mobileCancelTakebackOffer => 'Cancelar pedido de voltar'; - @override - String get mobileCancelDrawOffer => 'Cancel draw offer'; - @override String get mobileWaitingForOpponentToJoin => 'À espera do adversário entrar...'; @@ -125,24 +122,24 @@ class AppLocalizationsPt extends AppLocalizations { String get mobileSomethingWentWrong => 'Algo deu errado.'; @override - String get mobileShowResult => 'Show result'; + String get mobileShowResult => 'Mostrar resultado'; @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobilePuzzleThemesSubtitle => 'Joga problemas das tuas aberturas favoritas, ou escolhe um tema.'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobilePuzzleStormSubtitle => 'Resolve quantos problemas for possível em 3 minutos.'; @override String mobileGreeting(String param) { - return 'Hello, $param'; + return 'Olá, $param'; } @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileGreetingWithoutName => 'Olá'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobilePrefMagnifyDraggedPiece => 'Ampliar peça arrastada'; @override String get activityActivity => 'Atividade'; @@ -246,6 +243,17 @@ class AppLocalizationsPt extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Completou $count jogos $param2 por correspondência', + one: 'Completou $count jogo $param2 por correspondência', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,226 @@ class AppLocalizationsPt extends AppLocalizations { @override String get broadcastBroadcasts => 'Transmissões'; + @override + String get broadcastMyBroadcasts => 'As minhas transmissões'; + @override String get broadcastLiveBroadcasts => 'Transmissões do torneio em direto'; + @override + String get broadcastBroadcastCalendar => 'Calendário de transmissão'; + + @override + String get broadcastNewBroadcast => 'Nova transmissão em direto'; + + @override + String get broadcastSubscribedBroadcasts => 'Transmissões subscritas'; + + @override + String get broadcastAboutBroadcasts => 'Sobre Transmissões'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Como usar as Transmissões do Lichess.'; + + @override + String get broadcastTheNewRoundHelp => 'A nova ronda terá os mesmos membros e contribuidores que a anterior.'; + + @override + String get broadcastAddRound => 'Adicionar uma ronda'; + + @override + String get broadcastOngoing => 'A decorrer'; + + @override + String get broadcastUpcoming => 'Brevemente'; + + @override + String get broadcastCompleted => 'Concluído'; + + @override + String get broadcastCompletedHelp => 'Lichess deteta a conclusão da ronda baseada nos jogos da fonte. Use essa opção se não houver fonte.'; + + @override + String get broadcastRoundName => 'Nome da ronda'; + + @override + String get broadcastRoundNumber => 'Número da ronda'; + + @override + String get broadcastTournamentName => 'Nome do torneio'; + + @override + String get broadcastTournamentDescription => 'Breve descrição do torneio'; + + @override + String get broadcastFullDescription => 'Descrição completa do evento'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Descrição longa do evento opcional da transmissão. $param1 está disponível. Tem de ter menos que $param2 carácteres.'; + } + + @override + String get broadcastSourceSingleUrl => 'URL da fonte PGN'; + + @override + String get broadcastSourceUrlHelp => 'Link que o Lichess vai verificar para obter atualizações da PGN. Deve ser acessível ao público a partir da internet.'; + + @override + String get broadcastSourceGameIds => 'Até 64 IDs de jogo Lichess, separados por espaços.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Data de início no fuso horário local do torneio: $param'; + } + + @override + String get broadcastStartDateHelp => 'Opcional, se souberes quando começa o evento'; + + @override + String get broadcastCurrentGameUrl => 'Link da partida atual'; + + @override + String get broadcastDownloadAllRounds => 'Transferir todas as rondas'; + + @override + String get broadcastResetRound => 'Reiniciar esta ronda'; + + @override + String get broadcastDeleteRound => 'Apagar esta ronda'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Eliminar definitivamente a ronda e os seus jogos.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Eliminar todos os jogos desta ronda. A fonte deverá estar ativa para poder recriá-los.'; + + @override + String get broadcastEditRoundStudy => 'Editar estudo da ronda'; + + @override + String get broadcastDeleteTournament => 'Eliminar este torneio'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Excluir definitivamente todo o torneio, todas as rondas e todos os jogos.'; + + @override + String get broadcastShowScores => 'Mostra as pontuações dos jogadores com base nos resultados dos jogos'; + + @override + String get broadcastReplacePlayerTags => 'Opcional: substituir nomes de jogadores, avaliações e títulos'; + + @override + String get broadcastFideFederations => 'Federações FIDE'; + + @override + String get broadcastTop10Rating => '10 melhores classificações'; + + @override + String get broadcastFidePlayers => 'Jogadores FIDE'; + + @override + String get broadcastFidePlayerNotFound => 'Jogador FIDE não encontrado'; + + @override + String get broadcastFideProfile => 'Perfil FIDE'; + + @override + String get broadcastFederation => 'Federação'; + + @override + String get broadcastAgeThisYear => 'Idade neste ano'; + + @override + String get broadcastUnrated => 'Sem classificação'; + + @override + String get broadcastRecentTournaments => 'Torneio recentes'; + + @override + String get broadcastOpenLichess => 'Abrir no Lichess'; + + @override + String get broadcastTeams => 'Equipas'; + + @override + String get broadcastBoards => 'Tabuleiros'; + + @override + String get broadcastOverview => 'Visão geral'; + + @override + String get broadcastSubscribeTitle => 'Subscreva para ser notificado quando cada ronda começar. Podes ativar o sino ou as notificações push para transmissões nas preferências da tua conta.'; + + @override + String get broadcastUploadImage => 'Carregar imagem do torneio'; + + @override + String get broadcastNoBoardsYet => 'Ainda não há tabuleiros. Estes aparecerão assim que os jogos forem carregados.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Os tabuleiros podem ser carregados com uma fonte ou através do $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Começa após $param'; + } + + @override + String get broadcastStartVerySoon => 'A transmissão terá início muito em breve.'; + + @override + String get broadcastNotYetStarted => 'A transmissão ainda não começou.'; + + @override + String get broadcastOfficialWebsite => 'Website oficial'; + + @override + String get broadcastStandings => 'Classificações'; + + @override + String broadcastIframeHelp(String param) { + return 'Mais opções na $param'; + } + + @override + String get broadcastWebmastersPage => 'página webmasters'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'Uma fonte PGN pública em tempo real para esta ronda. Oferecemos também a $param para uma sincronização mais rápida e eficiente.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Incorporar esta transmissão no teu website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Incorporar $param no teu website'; + } + + @override + String get broadcastRatingDiff => 'Diferença de Elo'; + + @override + String get broadcastGamesThisTournament => 'Jogos deste torneio'; + + @override + String get broadcastScore => 'Pontuação'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count transmissões', + one: '$count transmissão', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Desafios: $param1'; @@ -1390,10 +1615,10 @@ class AppLocalizationsPt extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'O adversário está limitado quanto aos seus movimentos, e todas as jogadas pioram a sua posição.'; @override - String get puzzleThemeHealthyMix => 'Mistura saudável'; + String get puzzleThemeMix => 'Mistura saudável'; @override - String get puzzleThemeHealthyMixDescription => 'Um pouco de tudo. Não sabes o que esperar, então ficas pronto para qualquer coisa! Exatamente como em jogos de verdade.'; + String get puzzleThemeMixDescription => 'Um pouco de tudo. Não sabes o que esperar, então ficas pronto para qualquer coisa! Exatamente como em jogos de verdade.'; @override String get puzzleThemePlayerGames => 'Jogos de jogadores'; @@ -1797,9 +2022,6 @@ class AppLocalizationsPt extends AppLocalizations { @override String get removesTheDepthLimit => 'Remove o limite de profundidade e mantém o teu computador quente'; - @override - String get engineManager => 'Gestão do motor'; - @override String get blunder => 'Erro grave'; @@ -1878,7 +2100,7 @@ class AppLocalizationsPt extends AppLocalizations { String get friends => 'Amigos'; @override - String get otherPlayers => 'other players'; + String get otherPlayers => 'outros jogadores'; @override String get discussions => 'Conversas'; @@ -2063,6 +2285,9 @@ class AppLocalizationsPt extends AppLocalizations { @override String get gamesPlayed => 'Partidas jogadas'; + @override + String get ok => 'OK'; + @override String get cancel => 'Cancelar'; @@ -2712,10 +2937,10 @@ class AppLocalizationsPt extends AppLocalizations { String get yes => 'Sim'; @override - String get website => 'Website'; + String get website => 'Site'; @override - String get mobile => 'Mobile'; + String get mobile => 'Telemóvel'; @override String get help => 'Ajuda:'; @@ -2772,7 +2997,13 @@ class AppLocalizationsPt extends AppLocalizations { String get other => 'Outro'; @override - String get reportDescriptionHelp => 'Inclui o link do(s) jogo(s) e explica o que há de errado com o comportamento deste utilizador. Não digas apenas \"ele faz batota\"; informa-nos como chegaste a essa conclusão. A tua denúncia será processada mais rapidamente se for escrita em inglês.'; + String get reportCheatBoostHelp => 'Cola o(s) link(s) do(s) jogo(s) e explica o que está errado no comportamento deste utilizador. Não digas apenas “eles fazem batota”, mas diz-nos como chegaste a essa conclusão.'; + + @override + String get reportUsernameHelp => 'Explica o que este nome de utilizador tem de ofensivo. Não digas apenas “é ofensivo/inapropriado”, mas diz-nos como chegaste a essa conclusão, especialmente se o insulto for ofuscado, não estiver em inglês, estiver em calão ou for uma referência histórica/cultural.'; + + @override + String get reportProcessedFasterInEnglish => 'A tua denúncia será processado mais rapidamente se estiver escrito em inglês.'; @override String get error_provideOneCheatedGameLink => 'Por favor, fornece-nos pelo menos um link para um jogo onde tenha havido batota.'; @@ -3403,7 +3634,7 @@ class AppLocalizationsPt extends AppLocalizations { String get phoneAndTablet => 'Telemóvel e tablet'; @override - String get bulletBlitzClassical => 'Bullet, blitz, clássico'; + String get bulletBlitzClassical => 'Bullet, rápida, clássica'; @override String get correspondenceChess => 'Xadrez por correspondência'; @@ -4077,6 +4308,9 @@ class AppLocalizationsPt extends AppLocalizations { @override String get nothingToSeeHere => 'Nada para ver aqui no momento.'; + @override + String get stats => 'Estatísticas'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4724,4731 +4958,5978 @@ class AppLocalizationsPt extends AppLocalizations { String get streamerLichessStreamers => 'Streamers no Lichess'; @override - String get studyShareAndExport => 'Partilhar & exportar'; + String get studyPrivate => 'Privado'; @override - String get studyStart => 'Iniciar'; -} + String get studyMyStudies => 'Os meus estudos'; -/// The translations for Portuguese, as used in Brazil (`pt_BR`). -class AppLocalizationsPtBr extends AppLocalizationsPt { - AppLocalizationsPtBr(): super('pt_BR'); + @override + String get studyStudiesIContributeTo => 'Estudos para os quais contribui'; @override - String get mobileHomeTab => 'Início'; + String get studyMyPublicStudies => 'Os meus estudos públicos'; @override - String get mobilePuzzlesTab => 'Problemas'; + String get studyMyPrivateStudies => 'Os meus estudos privados'; @override - String get mobileToolsTab => 'Ferramentas'; + String get studyMyFavoriteStudies => 'Os meus estudos favoritos'; @override - String get mobileWatchTab => 'Assistir'; + String get studyWhatAreStudies => 'O que são estudos?'; @override - String get mobileSettingsTab => 'Ajustes'; + String get studyAllStudies => 'Todos os estudos'; @override - String get mobileMustBeLoggedIn => 'Você precisa estar logado para ver essa pagina.'; + String studyStudiesCreatedByX(String param) { + return 'Estudos criados por $param'; + } @override - String get mobileSystemColors => 'Cores do sistema'; + String get studyNoneYet => 'Nenhum ainda.'; @override - String get mobileFeedbackButton => 'Comentários'; + String get studyHot => 'Destaques'; @override - String get mobileOkButton => 'Ok'; + String get studyDateAddedNewest => 'Data em que foi adicionado (mais recente)'; @override - String get mobileSettingsHapticFeedback => 'Vibrar ao trocar'; + String get studyDateAddedOldest => 'Data em que foi adicionado (mais antigo)'; @override - String get mobileSettingsImmersiveMode => 'Modo imerssivo'; + String get studyRecentlyUpdated => 'Atualizado recentemente'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Ocultar a “interface” do sistema durante a reprodução. Use isto se você estiver incomodado com gestor de navegação do sistema nas bordas da tela. Aplica-se as telas dos jogos e desafios.'; + String get studyMostPopular => 'Mais popular'; @override - String get mobileNotFollowingAnyUser => 'Você não estar seguindo nenhum usuário.'; + String get studyAlphabetical => 'Ordem alfabética'; @override - String get mobileAllGames => 'Todos os jogos'; + String get studyAddNewChapter => 'Adicionar um novo capítulo'; @override - String get mobileRecentSearches => 'Pesquisas recentes'; + String get studyAddMembers => 'Adicionar membros'; @override - String get mobileClearButton => 'Limpar'; + String get studyInviteToTheStudy => 'Convidar para o estudo'; @override - String mobilePlayersMatchingSearchTerm(String param) { - return 'Usuários com \"$param\"'; - } + String get studyPleaseOnlyInvitePeopleYouKnow => 'Por favor, convida apenas pessoas que conheças e que querem participar ativamente neste estudo.'; @override - String get mobileNoSearchResults => 'Sem Resultados'; + String get studySearchByUsername => 'Pesquisar por nome de utilizador'; @override - String get mobileAreYouSure => 'Você tem certeza?'; + String get studySpectator => 'Espectador'; @override - String get mobilePuzzleStreakAbortWarning => 'Você perderá a sua sequência atual e sua pontuação será salva.'; + String get studyContributor => 'Colaborador'; @override - String get mobilePuzzleStormNothingToShow => 'Nada para mostrar aqui. Jogue algumas rodadas da Puzzle Storm.'; + String get studyKick => 'Expulsar'; @override - String get mobileSharePuzzle => 'Tentar novamente este quebra-cabeça'; + String get studyLeaveTheStudy => 'Sair do estudo'; @override - String get mobileShareGameURL => 'Compartilhar URL do jogo'; + String get studyYouAreNowAContributor => 'Agora és um colaborador'; @override - String get mobileShareGamePGN => 'Compartilhar PGN'; + String get studyYouAreNowASpectator => 'Agora és um espectador'; @override - String get mobileSharePositionAsFEN => 'Compartilhar posição como FEN'; + String get studyPgnTags => 'Etiquetas PGN'; @override - String get mobileShowVariations => 'Mostrar setas da variantes'; + String get studyLike => 'Gostar'; @override - String get mobileHideVariation => 'Ocultar variante forçada'; + String get studyUnlike => 'Remover gosto'; @override - String get mobileShowComments => 'Mostrar comentários'; + String get studyNewTag => 'Nova etiqueta'; @override - String get mobilePuzzleStormConfirmEndRun => 'Você quer terminar o turno?'; + String get studyCommentThisPosition => 'Comentar esta posição'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nada para mostrar aqui, por favor, altere os filtros'; + String get studyCommentThisMove => 'Comentar este lance'; @override - String get mobileCancelTakebackOffer => 'Cancelar oferta de revanche'; + String get studyAnnotateWithGlyphs => 'Anotar com símbolos'; @override - String get mobileCancelDrawOffer => 'Cancelar oferta de revanche'; + String get studyTheChapterIsTooShortToBeAnalysed => 'O capítulo é demasiado curto para ser analisado.'; @override - String get mobileWaitingForOpponentToJoin => 'Esperando por um oponente...'; + String get studyOnlyContributorsCanRequestAnalysis => 'Apenas os colaboradores de estudo podem solicitar uma análise de computador.'; @override - String get mobileBlindfoldMode => 'Venda'; + String get studyGetAFullComputerAnalysis => 'Obtém uma análise completa da linha principal pelo servidor.'; @override - String get mobileLiveStreamers => 'Streamers do Lichess'; + String get studyMakeSureTheChapterIsComplete => 'Certifica-te que o capítulo está completo. Só podes solicitar a análise uma vez.'; @override - String get mobileCustomGameJoinAGame => 'Entrar em um jogo'; + String get studyAllSyncMembersRemainOnTheSamePosition => 'Todos os membros do SYNC permanecem na mesma posição'; @override - String get mobileCorrespondenceClearSavedMove => 'Limpar movimento salvos'; + String get studyShareChanges => 'Partilha as alterações com espectadores e guarda-as no servidor'; @override - String get mobileSomethingWentWrong => 'Houve algum problema.'; + String get studyPlaying => 'A ser jogado'; @override - String get mobileShowResult => 'Mostrar resultado'; + String get studyShowEvalBar => 'Barras de avaliação'; @override - String get mobilePuzzleThemesSubtitle => 'Jogue quebra-cabeças de suas aberturas favoritas, ou escolha um tema.'; + String get studyFirst => 'Primeira'; @override - String get mobilePuzzleStormSubtitle => 'Resolva quantos quebra-cabeças for possível em 3 minutos.'; + String get studyPrevious => 'Anterior'; @override - String mobileGreeting(String param) { - return 'Olá, $param'; - } + String get studyNext => 'Seguinte'; @override - String get mobileGreetingWithoutName => 'Olá'; + String get studyLast => 'Última'; @override - String get activityActivity => 'Atividade'; + String get studyShareAndExport => 'Partilhar & exportar'; @override - String get activityHostedALiveStream => 'Iniciou uma transmissão ao vivo'; + String get studyCloneStudy => 'Clonar'; @override - String activityRankedInSwissTournament(String param1, String param2) { - return 'Classificado #$param1 entre $param2'; - } + String get studyStudyPgn => 'PGN do estudo'; @override - String get activitySignedUp => 'Registrou-se no lichess'; + String get studyDownloadAllGames => 'Transferir todas as partidas'; @override - String activitySupportedNbMonths(int count, String param2) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Contribuiu para o lichess.org por $count meses como $param2', - one: 'Contribuiu para o lichess.org por $count mês como $param2', - ); - return '$_temp0'; - } + String get studyChapterPgn => 'PGN do capítulo'; @override - String activityPracticedNbPositions(int count, String param2) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Praticou $count posições em $param2', - one: 'Praticou $count posição em $param2', - ); - return '$_temp0'; - } + String get studyCopyChapterPgn => 'Copiar PGN'; @override - String activitySolvedNbPuzzles(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Resolveu $count quebra-cabeças táticos', - one: 'Resolveu $count quebra-cabeça tático', - ); - return '$_temp0'; - } + String get studyDownloadGame => 'Transferir partida'; @override - String activityPlayedNbGames(int count, String param2) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Jogou $count partidas de $param2', - one: 'Jogou $count partida de $param2', - ); - return '$_temp0'; - } + String get studyStudyUrl => 'URL do estudo'; @override - String activityPostedNbMessages(int count, String param2) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Publicou $count mensagens em $param2', - one: 'Publicou $count mensagem em $param2', - ); - return '$_temp0'; - } + String get studyCurrentChapterUrl => 'URL do capítulo atual'; @override - String activityPlayedNbMoves(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Jogou $count movimentos', - one: 'Jogou $count movimento', - ); - return '$_temp0'; - } + String get studyYouCanPasteThisInTheForumToEmbed => 'Podes colocar isto no fórum para o incorporares'; @override - String activityInNbCorrespondenceGames(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'em $count jogos por correspondência', - one: 'em $count jogo por correspondência', - ); - return '$_temp0'; - } + String get studyStartAtInitialPosition => 'Começar na posição inicial'; @override - String activityCompletedNbGames(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Completou $count jogos por correspondência', - one: 'Completou $count jogo por correspondência', - ); - return '$_temp0'; + String studyStartAtX(String param) { + return 'Começar em $param'; } @override - String activityFollowedNbPlayers(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Começou a seguir $count jogadores', - one: 'Começou a seguir $count jogador', - ); - return '$_temp0'; - } + String get studyEmbedInYourWebsite => 'Incorporar no teu site ou blog'; @override - String activityGainedNbFollowers(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Ganhou $count novos seguidores', - one: 'Ganhou $count novo seguidor', - ); - return '$_temp0'; - } + String get studyReadMoreAboutEmbedding => 'Ler mais sobre incorporação'; @override - String activityHostedNbSimuls(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Hospedou $count exibições simultâneas', - one: 'Hospedou $count exibição simultânea', - ); - return '$_temp0'; - } + String get studyOnlyPublicStudiesCanBeEmbedded => 'Só estudos públicos é que podem ser incorporados!'; @override - String activityJoinedNbSimuls(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Participou em $count exibições simultâneas', - one: 'Participou em $count exibição simultânea', - ); - return '$_temp0'; - } + String get studyOpen => 'Abrir'; @override - String activityCreatedNbStudies(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Criou $count novos estudos', - one: 'Criou $count novo estudo', - ); - return '$_temp0'; + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1, trazido a si pelo $param2'; } @override - String activityCompetedInNbTournaments(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Competiu em $count torneios arena', - one: 'Competiu em $count torneio arena', - ); - return '$_temp0'; - } + String get studyStudyNotFound => 'Estudo não encontrado'; @override - String activityRankedInTournament(int count, String param2, String param3, String param4) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Classificado #$count (top $param2%) com $param3 jogos em $param4', - one: 'Classificado #$count (top $param2%) com $param3 jogo em $param4', - ); - return '$_temp0'; - } + String get studyEditChapter => 'Editar capítulo'; @override - String activityCompetedInNbSwissTournaments(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Competiu em $count torneios suíços', - one: 'Competiu em $count torneio suíço', - ); - return '$_temp0'; - } + String get studyNewChapter => 'Novo capítulo'; @override - String activityJoinedNbTeams(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Entrou nas $count equipes', - one: 'Entrou na $count equipe', - ); - return '$_temp0'; + String studyImportFromChapterX(String param) { + return 'Importar de $param'; } @override - String get broadcastBroadcasts => 'Transmissões'; + String get studyOrientation => 'Orientação'; @override - String get broadcastLiveBroadcasts => 'Transmissões ao vivo do torneio'; + String get studyAnalysisMode => 'Modo de análise'; @override - String challengeChallengesX(String param1) { - return 'Desafios: $param1'; - } + String get studyPinnedChapterComment => 'Comentário de capítulo afixado'; @override - String get challengeChallengeToPlay => 'Desafiar para jogar'; + String get studySaveChapter => 'Guardar capítulo'; @override - String get challengeChallengeDeclined => 'Desafio recusado'; + String get studyClearAnnotations => 'Limpar anotações'; @override - String get challengeChallengeAccepted => 'Desafio aceito!'; + String get studyClearVariations => 'Limpar variações'; @override - String get challengeChallengeCanceled => 'Desafio cancelado.'; + String get studyDeleteChapter => 'Eliminar capítulo'; @override - String get challengeRegisterToSendChallenges => 'Por favor, registre-se para enviar desafios.'; + String get studyDeleteThisChapter => 'Eliminar este capítulo? Não há volta atrás!'; @override - String challengeYouCannotChallengeX(String param) { - return 'Você não pode desafiar $param.'; - } + String get studyClearAllCommentsInThisChapter => 'Apagar todos os comentários e símbolos após lances neste capítulo?'; @override - String challengeXDoesNotAcceptChallenges(String param) { - return '$param não aceita desafios.'; - } + String get studyRightUnderTheBoard => 'Mesmo por baixo do tabuleiro'; @override - String challengeYourXRatingIsTooFarFromY(String param1, String param2) { - return 'O seu rating $param1 é muito diferente de $param2.'; - } + String get studyNoPinnedComment => 'Nenhum'; @override - String challengeCannotChallengeDueToProvisionalXRating(String param) { - return 'Não pode desafiar devido ao rating provisório de $param.'; - } + String get studyNormalAnalysis => 'Análise normal'; @override - String challengeXOnlyAcceptsChallengesFromFriends(String param) { - return '$param só aceita desafios de amigos.'; - } + String get studyHideNextMoves => 'Ocultar os próximos movimentos'; @override - String get challengeDeclineGeneric => 'Não estou aceitando desafios no momento.'; + String get studyInteractiveLesson => 'Lição interativa'; @override - String get challengeDeclineLater => 'Este não é o momento certo para mim, por favor pergunte novamente mais tarde.'; + String studyChapterX(String param) { + return 'Capítulo $param'; + } @override - String get challengeDeclineTooFast => 'Este controle de tempo é muito rápido para mim, por favor, desafie novamente com um jogo mais lento.'; + String get studyEmpty => 'Vazio'; @override - String get challengeDeclineTooSlow => 'Este controle de tempo é muito lento para mim, por favor, desafie novamente com um jogo mais rápido.'; + String get studyStartFromInitialPosition => 'Começar da posição inicial'; @override - String get challengeDeclineTimeControl => 'Não estou aceitando desafios com estes controles de tempo.'; + String get studyEditor => 'Editor'; @override - String get challengeDeclineRated => 'Por favor, envie-me um desafio ranqueado.'; + String get studyStartFromCustomPosition => 'Iniciar de uma posição personalizada'; @override - String get challengeDeclineCasual => 'Por favor, envie-me um desafio amigável.'; + String get studyLoadAGameByUrl => 'Carregar um jogo por URL'; @override - String get challengeDeclineStandard => 'Não estou aceitando desafios de variantes no momento.'; + String get studyLoadAPositionFromFen => 'Carregar uma posição por FEN'; @override - String get challengeDeclineVariant => 'Não estou a fim de jogar esta variante no momento.'; + String get studyLoadAGameFromPgn => 'Carregar um jogo por PGN'; @override - String get challengeDeclineNoBot => 'Não estou aceitando desafios de robôs.'; + String get studyAutomatic => 'Automática'; @override - String get challengeDeclineOnlyBot => 'Estou aceitando apenas desafios de robôs.'; + String get studyUrlOfTheGame => 'URL do jogo'; @override - String get challengeInviteLichessUser => 'Ou convide um usuário Lichess:'; + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Carregar um jogo do $param1 ou de $param2'; + } @override - String get contactContact => 'Contato'; + String get studyCreateChapter => 'Criar capítulo'; @override - String get contactContactLichess => 'Entrar em contato com Lichess'; + String get studyCreateStudy => 'Criar estudo'; @override - String get patronDonate => 'Doação'; + String get studyEditStudy => 'Editar estudo'; @override - String get patronLichessPatron => 'Apoie o Lichess'; + String get studyVisibility => 'Visibilidade'; @override - String perfStatPerfStats(String param) { - return 'Estatísticas de $param'; - } + String get studyPublic => 'Público'; @override - String get perfStatViewTheGames => 'Ver os jogos'; + String get studyUnlisted => 'Não listado'; @override - String get perfStatProvisional => 'provisório'; + String get studyInviteOnly => 'Apenas por convite'; @override - String get perfStatNotEnoughRatedGames => 'Não foram jogadas partidas suficientes valendo rating para estabelecer uma classificação confiável.'; + String get studyAllowCloning => 'Permitir clonagem'; @override - String perfStatProgressOverLastXGames(String param) { - return 'Progresso nos últimos $param jogos:'; - } + String get studyNobody => 'Ninguém'; @override - String perfStatRatingDeviation(String param) { - return 'Desvio de pontuação: $param.'; - } + String get studyOnlyMe => 'Apenas eu'; @override - String perfStatRatingDeviationTooltip(String param1, String param2, String param3) { - return 'Um valor inferior indica que a pontuação é mais estável. Superior a $param1, a pontuação é classificada como provisória. Para ser incluída nas classificações, esse valor deve ser inferior a $param2 (xadrez padrão) ou $param3 (variantes).'; - } + String get studyContributors => 'Contribuidores'; @override - String get perfStatTotalGames => 'Total de partidas'; + String get studyMembers => 'Membros'; @override - String get perfStatRatedGames => 'Partidas valendo pontos'; + String get studyEveryone => 'Toda a gente'; @override - String get perfStatTournamentGames => 'Jogos de torneio'; + String get studyEnableSync => 'Ativar sincronização'; @override - String get perfStatBerserkedGames => 'Partidas Berserked'; + String get studyYesKeepEveryoneOnTheSamePosition => 'Sim: mantenha toda a gente na mesma posição'; @override - String get perfStatTimeSpentPlaying => 'Tempo jogando'; + String get studyNoLetPeopleBrowseFreely => 'Não: deixa as pessoas navegarem livremente'; @override - String get perfStatAverageOpponent => 'Pontuação média do adversário'; + String get studyPinnedStudyComment => 'Comentário de estudo fixo'; @override - String get perfStatVictories => 'Vitórias'; + String get studyStart => 'Iniciar'; @override - String get perfStatDefeats => 'Derrotas'; + String get studySave => 'Guardar'; @override - String get perfStatDisconnections => 'Desconexões'; + String get studyClearChat => 'Limpar o chat'; @override - String get perfStatNotEnoughGames => 'Jogos insuficientes jogados'; + String get studyDeleteTheStudyChatHistory => 'Apagar o histórico do chat do estudo? Não há volta atrás!'; @override - String perfStatHighestRating(String param) { - return 'Pontuação mais alta: $param'; - } + String get studyDeleteStudy => 'Eliminar estudo'; @override - String perfStatLowestRating(String param) { - return 'Rating mais baixo: $param'; + String studyConfirmDeleteStudy(String param) { + return 'Eliminar todo o estudo? Não há volta atrás! Digite o nome do estudo para confirmar: $param'; } @override - String perfStatFromXToY(String param1, String param2) { - return 'de $param1 para $param2'; - } + String get studyWhereDoYouWantToStudyThat => 'Onde queres estudar isso?'; @override - String get perfStatWinningStreak => 'Série de Vitórias'; + String get studyGoodMove => 'Boa jogada'; @override - String get perfStatLosingStreak => 'Série de derrotas'; + String get studyMistake => 'Erro'; @override - String perfStatLongestStreak(String param) { - return 'Sequência mais longa: $param'; - } + String get studyBrilliantMove => 'Jogada brilhante'; @override - String perfStatCurrentStreak(String param) { - return 'Sequência atual: $param'; - } + String get studyBlunder => 'Erro grave'; @override - String get perfStatBestRated => 'Melhores vitórias valendo pontuação'; + String get studyInterestingMove => 'Lance interessante'; @override - String get perfStatGamesInARow => 'Partidas jogadas seguidas'; + String get studyDubiousMove => 'Lance duvidoso'; @override - String get perfStatLessThanOneHour => 'Menos de uma hora entre partidas'; + String get studyOnlyMove => 'Lance único'; @override - String get perfStatMaxTimePlaying => 'Tempo máximo jogando'; + String get studyZugzwang => 'Zugzwang'; @override - String get perfStatNow => 'agora'; + String get studyEqualPosition => 'Posição igual'; @override - String get preferencesPreferences => 'Preferências'; + String get studyUnclearPosition => 'Posição não clara'; @override - String get preferencesDisplay => 'Exibição'; + String get studyWhiteIsSlightlyBetter => 'As brancas estão ligeiramente melhor'; @override - String get preferencesPrivacy => 'Privacidade'; + String get studyBlackIsSlightlyBetter => 'As pretas estão ligeiramente melhor'; @override - String get preferencesNotifications => 'Notificações'; + String get studyWhiteIsBetter => 'As brancas estão melhor'; @override - String get preferencesPieceAnimation => 'Animação das peças'; + String get studyBlackIsBetter => 'As pretas estão melhor'; @override - String get preferencesMaterialDifference => 'Diferença material'; + String get studyWhiteIsWinning => 'Brancas estão ganhando'; @override - String get preferencesBoardHighlights => 'Destacar casas do tabuleiro (último movimento e xeque)'; + String get studyBlackIsWinning => 'Pretas estão ganhando'; @override - String get preferencesPieceDestinations => 'Destino das peças (movimentos válidos e pré-movimentos)'; + String get studyNovelty => 'Novidade teórica'; @override - String get preferencesBoardCoordinates => 'Coordenadas do tabuleiro (A-H, 1-8)'; + String get studyDevelopment => 'Desenvolvimento'; @override - String get preferencesMoveListWhilePlaying => 'Lista de movimentos durante a partida'; + String get studyInitiative => 'Iniciativa'; @override - String get preferencesPgnPieceNotation => 'Modo de notação das jogadas'; + String get studyAttack => 'Ataque'; @override - String get preferencesChessPieceSymbol => 'Símbolo da peça'; + String get studyCounterplay => 'Contra-jogo'; @override - String get preferencesPgnLetter => 'Letra (K, Q, R, B, N)'; + String get studyTimeTrouble => 'Pouco tempo'; @override - String get preferencesZenMode => 'Modo Zen'; + String get studyWithCompensation => 'Com compensação'; @override - String get preferencesShowPlayerRatings => 'Mostrar rating dos jogadores'; + String get studyWithTheIdea => 'Com a ideia'; @override - String get preferencesShowFlairs => 'Mostrar emotes de usuário'; + String get studyNextChapter => 'Próximo capítulo'; @override - String get preferencesExplainShowPlayerRatings => 'Permite ocultar todas os ratings do site, para ajudar a se concentrar no jogo. As partidas continuam valendo rating.'; + String get studyPrevChapter => 'Capítulo anterior'; @override - String get preferencesDisplayBoardResizeHandle => 'Mostrar cursor de redimensionamento do tabuleiro'; + String get studyStudyActions => 'Opções de estudo'; @override - String get preferencesOnlyOnInitialPosition => 'Apenas na posição inicial'; + String get studyTopics => 'Tópicos'; @override - String get preferencesInGameOnly => 'Durante partidas'; + String get studyMyTopics => 'Os meus tópicos'; @override - String get preferencesChessClock => 'Relógio'; + String get studyPopularTopics => 'Tópicos populares'; @override - String get preferencesTenthsOfSeconds => 'Décimos de segundo'; + String get studyManageTopics => 'Gerir tópicos'; @override - String get preferencesWhenTimeRemainingLessThanTenSeconds => 'Quando o tempo restante < 10 segundos'; + String get studyBack => 'Voltar'; @override - String get preferencesHorizontalGreenProgressBars => 'Barras de progresso verdes horizontais'; + String get studyPlayAgain => 'Jogar novamente'; @override - String get preferencesSoundWhenTimeGetsCritical => 'Som ao atingir tempo crítico'; + String get studyWhatWouldYouPlay => 'O que jogaria nessa situação?'; @override - String get preferencesGiveMoreTime => 'Dar mais tempo'; + String get studyYouCompletedThisLesson => 'Parabéns! Completou esta lição.'; @override - String get preferencesGameBehavior => 'Comportamento do jogo'; + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count capítulos', + one: '$count capítulo', + ); + return '$_temp0'; + } @override - String get preferencesHowDoYouMovePieces => 'Como você move as peças?'; + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Jogos', + one: '$count Jogo', + ); + return '$_temp0'; + } @override - String get preferencesClickTwoSquares => 'Clicar em duas casas'; + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count membros', + one: '$count membro', + ); + return '$_temp0'; + } @override - String get preferencesDragPiece => 'Arrastar a peça'; + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Cole seu texto PGN aqui, até $count jogos', + one: 'Cole seu texto PGN aqui, até $count jogo', + ); + return '$_temp0'; + } +} - @override - String get preferencesBothClicksAndDrag => 'Ambas'; +/// The translations for Portuguese, as used in Brazil (`pt_BR`). +class AppLocalizationsPtBr extends AppLocalizationsPt { + AppLocalizationsPtBr(): super('pt_BR'); @override - String get preferencesPremovesPlayingDuringOpponentTurn => 'Pré-movimentos (jogadas durante o turno do oponente)'; + String get mobileHomeTab => 'Início'; @override - String get preferencesTakebacksWithOpponentApproval => 'Voltar jogada (com aprovação do oponente)'; + String get mobilePuzzlesTab => 'Problemas'; @override - String get preferencesInCasualGamesOnly => 'Somente em jogos casuais'; + String get mobileToolsTab => 'Ferramentas'; @override - String get preferencesPromoteToQueenAutomatically => 'Promover a Dama automaticamente'; + String get mobileWatchTab => 'Assistir'; @override - String get preferencesExplainPromoteToQueenAutomatically => 'Mantenha a tecla pressionada enquanto promove para desativar temporariamente a autopromoção'; + String get mobileSettingsTab => 'Ajustes'; @override - String get preferencesWhenPremoving => 'Quando pré-mover'; + String get mobileMustBeLoggedIn => 'Você precisa estar logado para ver essa pagina.'; @override - String get preferencesClaimDrawOnThreefoldRepetitionAutomatically => 'Reivindicar empate sobre a repetição tripla automaticamente'; + String get mobileSystemColors => 'Cores do sistema'; @override - String get preferencesWhenTimeRemainingLessThanThirtySeconds => 'Quando o tempo restante < 30 segundos'; + String get mobileFeedbackButton => 'Comentários'; @override - String get preferencesMoveConfirmation => 'Confirmação de movimento'; + String get mobileOkButton => 'Ok'; @override - String get preferencesExplainCanThenBeTemporarilyDisabled => 'Pode ser desativado durante a partida no menu do tabuleiro'; + String get mobileSettingsHapticFeedback => 'Vibrar ao trocar'; @override - String get preferencesInCorrespondenceGames => 'Jogos por correspondência'; + String get mobileSettingsImmersiveMode => 'Modo imersivo'; @override - String get preferencesCorrespondenceAndUnlimited => 'Por correspondência e sem limites'; + String get mobileSettingsImmersiveModeSubtitle => 'Ocultar a “interface” do sistema durante a reprodução. Use isto se você estiver incomodado com gestor de navegação do sistema nas bordas da tela. Aplica-se às telas dos jogos e desafios.'; @override - String get preferencesConfirmResignationAndDrawOffers => 'Confirmar abandono e oferta de empate'; + String get mobileNotFollowingAnyUser => 'Você não está seguindo nenhum usuário.'; @override - String get preferencesCastleByMovingTheKingTwoSquaresOrOntoTheRook => 'Maneira de rocar'; + String get mobileAllGames => 'Todos os jogos'; @override - String get preferencesCastleByMovingTwoSquares => 'Mover o rei duas casas'; + String get mobileRecentSearches => 'Pesquisas recentes'; @override - String get preferencesCastleByMovingOntoTheRook => 'Mover o rei em direção à torre'; + String get mobileClearButton => 'Limpar'; @override - String get preferencesInputMovesWithTheKeyboard => 'Fazer lances com escrita do teclado'; + String mobilePlayersMatchingSearchTerm(String param) { + return 'Usuários com \"$param\"'; + } @override - String get preferencesInputMovesWithVoice => 'Mova as peças com sua voz'; + String get mobileNoSearchResults => 'Sem Resultados'; @override - String get preferencesSnapArrowsToValidMoves => 'Insira setas para movimentos válidos'; + String get mobileAreYouSure => 'Você tem certeza?'; @override - String get preferencesSayGgWpAfterLosingOrDrawing => 'Diga \"Bom jogo, bem jogado\" após a derrota ou empate'; + String get mobilePuzzleStreakAbortWarning => 'Você perderá a sua sequência atual e sua pontuação será salva.'; @override - String get preferencesYourPreferencesHaveBeenSaved => 'Suas preferências foram salvas.'; + String get mobilePuzzleStormNothingToShow => 'Nada para mostrar aqui. Jogue algumas rodadas da Puzzle Storm.'; @override - String get preferencesScrollOnTheBoardToReplayMoves => 'Use o scroll do mouse no tabuleiro para ir passando as jogadas'; + String get mobileSharePuzzle => 'Compartilhar este quebra-cabeça'; @override - String get preferencesCorrespondenceEmailNotification => 'Email diário listando seus jogos por correspondência'; + String get mobileShareGameURL => 'Compartilhar URL do jogo'; @override - String get preferencesNotifyStreamStart => 'Streamer começou uma transmissão ao vivo'; + String get mobileShareGamePGN => 'Compartilhar PGN'; @override - String get preferencesNotifyInboxMsg => 'Nova mensagem na caixa de entrada'; + String get mobileSharePositionAsFEN => 'Compartilhar posição como FEN'; @override - String get preferencesNotifyForumMention => 'Você foi mencionado em um comentário do fórum'; + String get mobileShowVariations => 'Mostrar setas de variantes'; @override - String get preferencesNotifyInvitedStudy => 'Convite para um estudo'; + String get mobileHideVariation => 'Ocultar variante forçada'; @override - String get preferencesNotifyGameEvent => 'Jogo por correspondência atualizado'; + String get mobileShowComments => 'Mostrar comentários'; @override - String get preferencesNotifyChallenge => 'Desafios'; + String get mobilePuzzleStormConfirmEndRun => 'Você quer terminar o turno?'; @override - String get preferencesNotifyTournamentSoon => 'O torneio vai começar em breve'; + String get mobilePuzzleStormFilterNothingToShow => 'Nada para mostrar aqui, por favor, altere os filtros'; @override - String get preferencesNotifyTimeAlarm => 'Está acabando o tempo no jogo por correspondência'; + String get mobileCancelTakebackOffer => 'Cancelar oferta de revanche'; @override - String get preferencesNotifyBell => 'Notificação no Lichess'; + String get mobileWaitingForOpponentToJoin => 'Esperando por um oponente...'; @override - String get preferencesNotifyPush => 'Notificação no dispositivo fora do Lichess'; + String get mobileBlindfoldMode => 'Venda'; @override - String get preferencesNotifyWeb => 'Navegador'; + String get mobileLiveStreamers => 'Streamers do Lichess'; @override - String get preferencesNotifyDevice => 'Dispositivo'; + String get mobileCustomGameJoinAGame => 'Entrar em um jogo'; @override - String get preferencesBellNotificationSound => 'Som da notificação'; + String get mobileCorrespondenceClearSavedMove => 'Limpar movimento salvos'; @override - String get puzzlePuzzles => 'Quebra-cabeças'; + String get mobileSomethingWentWrong => 'Houve algum problema.'; @override - String get puzzlePuzzleThemes => 'Temas de quebra-cabeça'; + String get mobileShowResult => 'Mostrar resultado'; @override - String get puzzleRecommended => 'Recomendado'; + String get mobilePuzzleThemesSubtitle => 'Jogue quebra-cabeças de suas aberturas favoritas, ou escolha um tema.'; @override - String get puzzlePhases => 'Fases'; + String get mobilePuzzleStormSubtitle => 'Resolva quantos quebra-cabeças for possível em 3 minutos.'; @override - String get puzzleMotifs => 'Motivos táticos'; + String mobileGreeting(String param) { + return 'Olá, $param'; + } @override - String get puzzleAdvanced => 'Avançado'; + String get mobileGreetingWithoutName => 'Olá'; @override - String get puzzleLengths => 'Distância'; + String get mobilePrefMagnifyDraggedPiece => 'Ampliar peça segurada'; @override - String get puzzleMates => 'Xeque-mates'; + String get activityActivity => 'Atividade'; @override - String get puzzleGoals => 'Objetivos'; + String get activityHostedALiveStream => 'Iniciou uma transmissão ao vivo'; @override - String get puzzleOrigin => 'Origem'; + String activityRankedInSwissTournament(String param1, String param2) { + return 'Classificado #$param1 entre $param2'; + } @override - String get puzzleSpecialMoves => 'Movimentos especiais'; + String get activitySignedUp => 'Registrou-se no lichess'; @override - String get puzzleDidYouLikeThisPuzzle => 'Você gostou deste quebra-cabeças?'; + String activitySupportedNbMonths(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Contribuiu para o lichess.org por $count meses como $param2', + one: 'Contribuiu para o lichess.org por $count mês como $param2', + ); + return '$_temp0'; + } @override - String get puzzleVoteToLoadNextOne => 'Vote para carregar o próximo!'; + String activityPracticedNbPositions(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Praticou $count posições em $param2', + one: 'Praticou $count posição em $param2', + ); + return '$_temp0'; + } @override - String get puzzleUpVote => 'Votar a favor do quebra-cabeça'; + String activitySolvedNbPuzzles(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Resolveu $count quebra-cabeças táticos', + one: 'Resolveu $count quebra-cabeça tático', + ); + return '$_temp0'; + } @override - String get puzzleDownVote => 'Votar contra o quebra-cabeça'; + String activityPlayedNbGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Jogou $count partidas de $param2', + one: 'Jogou $count partida de $param2', + ); + return '$_temp0'; + } @override - String get puzzleYourPuzzleRatingWillNotChange => 'Sua pontuação de quebra-cabeças não mudará. Note que os quebra-cabeças não são uma competição. A pontuação indica os quebra-cabeças que se adequam às suas habilidades.'; + String activityPostedNbMessages(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Publicou $count mensagens em $param2', + one: 'Publicou $count mensagem em $param2', + ); + return '$_temp0'; + } @override - String get puzzleFindTheBestMoveForWhite => 'Encontre o melhor lance para as brancas.'; + String activityPlayedNbMoves(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Jogou $count movimentos', + one: 'Jogou $count movimento', + ); + return '$_temp0'; + } @override - String get puzzleFindTheBestMoveForBlack => 'Encontre a melhor jogada para as pretas.'; + String activityInNbCorrespondenceGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'em $count jogos por correspondência', + one: 'em $count jogo por correspondência', + ); + return '$_temp0'; + } @override - String get puzzleToGetPersonalizedPuzzles => 'Para obter desafios personalizados:'; + String activityCompletedNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Completou $count jogos por correspondência', + one: 'Completou $count jogo por correspondência', + ); + return '$_temp0'; + } @override - String puzzlePuzzleId(String param) { - return 'Quebra-cabeça $param'; + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count $param2 partidas por correspondência finalizadas', + one: 'Completou $count $param2 partida por correspondência', + ); + return '$_temp0'; } @override - String get puzzlePuzzleOfTheDay => 'Quebra-cabeça do dia'; + String activityFollowedNbPlayers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Começou a seguir $count jogadores', + one: 'Começou a seguir $count jogador', + ); + return '$_temp0'; + } @override - String get puzzleDailyPuzzle => 'Quebra-cabeça diário'; + String activityGainedNbFollowers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Ganhou $count novos seguidores', + one: 'Ganhou $count novo seguidor', + ); + return '$_temp0'; + } @override - String get puzzleClickToSolve => 'Clique para resolver'; + String activityHostedNbSimuls(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Hospedou $count exibições simultâneas', + one: 'Hospedou $count exibição simultânea', + ); + return '$_temp0'; + } @override - String get puzzleGoodMove => 'Boa jogada'; + String activityJoinedNbSimuls(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Participou em $count exibições simultâneas', + one: 'Participou em $count exibição simultânea', + ); + return '$_temp0'; + } @override - String get puzzleBestMove => 'Melhor jogada!'; + String activityCreatedNbStudies(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Criou $count novos estudos', + one: 'Criou $count novo estudo', + ); + return '$_temp0'; + } @override - String get puzzleKeepGoing => 'Continue…'; + String activityCompetedInNbTournaments(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Competiu em $count torneios arena', + one: 'Competiu em $count torneio arena', + ); + return '$_temp0'; + } @override - String get puzzlePuzzleSuccess => 'Sucesso!'; + String activityRankedInTournament(int count, String param2, String param3, String param4) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Classificado #$count (top $param2%) com $param3 jogos em $param4', + one: 'Classificado #$count (top $param2%) com $param3 jogo em $param4', + ); + return '$_temp0'; + } @override - String get puzzlePuzzleComplete => 'Quebra-cabeças concluído!'; + String activityCompetedInNbSwissTournaments(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Competiu em $count torneios suíços', + one: 'Competiu em $count torneio suíço', + ); + return '$_temp0'; + } @override - String get puzzleByOpenings => 'Por abertura'; + String activityJoinedNbTeams(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Entrou nas $count equipes', + one: 'Entrou na $count equipe', + ); + return '$_temp0'; + } @override - String get puzzlePuzzlesByOpenings => 'Quebra-cabeças por abertura'; + String get broadcastBroadcasts => 'Transmissões'; @override - String get puzzleOpeningsYouPlayedTheMost => 'Aberturas que você mais jogou em partidas valendo pontos'; + String get broadcastMyBroadcasts => 'Minhas transmissões'; @override - String get puzzleUseFindInPage => 'Use a ferramenta \"Encontrar na página\" do navegador para encontrar sua abertura favorita!'; + String get broadcastLiveBroadcasts => 'Transmissões ao vivo do torneio'; @override - String get puzzleUseCtrlF => 'Aperte Ctrl + F para encontrar sua abertura favorita!'; + String get broadcastBroadcastCalendar => 'Calendário das transmissões'; @override - String get puzzleNotTheMove => 'O movimento não é este!'; + String get broadcastNewBroadcast => 'Nova transmissão ao vivo'; @override - String get puzzleTrySomethingElse => 'Tente algo diferente.'; + String get broadcastSubscribedBroadcasts => 'Transmissões em que você se inscreveu'; @override - String puzzleRatingX(String param) { - return 'Rating: $param'; - } + String get broadcastAboutBroadcasts => 'Sobre as transmissões'; @override - String get puzzleHidden => 'oculto'; + String get broadcastHowToUseLichessBroadcasts => 'Como usar as transmissões do Lichess.'; @override - String puzzleFromGameLink(String param) { - return 'Do jogo $param'; - } + String get broadcastTheNewRoundHelp => 'A nova rodada terá os mesmos membros e colaboradores que a anterior.'; @override - String get puzzleContinueTraining => 'Continue treinando'; + String get broadcastAddRound => 'Adicionar uma rodada'; @override - String get puzzleDifficultyLevel => 'Nível de dificuldade'; + String get broadcastOngoing => 'Em andamento'; @override - String get puzzleNormal => 'Normal'; + String get broadcastUpcoming => 'Próximos'; @override - String get puzzleEasier => 'Fácil'; + String get broadcastCompleted => 'Concluído'; @override - String get puzzleEasiest => 'Muito fácil'; + String get broadcastCompletedHelp => 'O Lichess detecta o fim da rodada baseado nos jogos fonte. Use essa opção se não houver fonte.'; @override - String get puzzleHarder => 'Difícil'; + String get broadcastRoundName => 'Nome da rodada'; @override - String get puzzleHardest => 'Muito difícil'; + String get broadcastRoundNumber => 'Número da rodada'; @override - String get puzzleExample => 'Exemplo'; + String get broadcastTournamentName => 'Nome do torneio'; @override - String get puzzleAddAnotherTheme => 'Adicionar um outro tema'; + String get broadcastTournamentDescription => 'Descrição curta do torneio'; @override - String get puzzleNextPuzzle => 'Próximo quebra-cabeça'; + String get broadcastFullDescription => 'Descrição completa do evento'; @override - String get puzzleJumpToNextPuzzleImmediately => 'Ir para o próximo problema automaticamente'; + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Descrição longa e opcional da transmissão. $param1 está disponível. O tamanho deve ser menor que $param2 caracteres.'; + } @override - String get puzzlePuzzleDashboard => 'Painel do quebra-cabeças'; + String get broadcastSourceSingleUrl => 'URL de origem de PGN'; @override - String get puzzleImprovementAreas => 'Áreas de aprimoramento'; + String get broadcastSourceUrlHelp => 'URL que Lichess irá verificar para obter atualizações PGN. Deve ser acessível ao público a partir da Internet.'; @override - String get puzzleStrengths => 'Pontos fortes'; + String get broadcastSourceGameIds => 'Até 64 IDs de partidas do Lichess, separados por espaços.'; @override - String get puzzleHistory => 'Histórico de quebra-cabeças'; + String broadcastStartDateTimeZone(String param) { + return 'Data de início no horário local do torneio: $param'; + } @override - String get puzzleSolved => 'resolvido'; + String get broadcastStartDateHelp => 'Opcional, se você sabe quando o evento começa'; @override - String get puzzleFailed => 'falhou'; + String get broadcastCurrentGameUrl => 'URL da partida atual'; @override - String get puzzleStreakDescription => 'Resolva quebra-cabeças progressivamente mais difíceis e construa uma sequência de vitórias. Não há relógio, então tome seu tempo. Um movimento errado e o jogo acaba! Porém, você pode pular um movimento por sessão.'; + String get broadcastDownloadAllRounds => 'Baixar todas as rodadas'; @override - String puzzleYourStreakX(String param) { - return 'Sua sequência: $param'; - } + String get broadcastResetRound => 'Reiniciar esta rodada'; @override - String get puzzleStreakSkipExplanation => 'Pule este lance para preservar a sua sequência! Funciona apenas uma vez por corrida.'; + String get broadcastDeleteRound => 'Excluir esta rodada'; @override - String get puzzleContinueTheStreak => 'Continuar a sequência'; + String get broadcastDefinitivelyDeleteRound => 'Deletar permanentemente todas as partidas desta rodada.'; @override - String get puzzleNewStreak => 'Nova sequência'; + String get broadcastDeleteAllGamesOfThisRound => 'Deletar todas as partidas desta rodada. A fonte deverá estar ativa para criá-las novamente.'; @override - String get puzzleFromMyGames => 'Dos meus jogos'; + String get broadcastEditRoundStudy => 'Editar estudo da rodada'; @override - String get puzzleLookupOfPlayer => 'Pesquise quebra-cabeças de um jogador específico'; + String get broadcastDeleteTournament => 'Excluir este torneio'; @override - String puzzleFromXGames(String param) { - return 'Problemas de $param\' jogos'; - } + String get broadcastDefinitivelyDeleteTournament => 'Excluir permanentemente todo o torneio, incluindo todas as rodadas e jogos.'; @override - String get puzzleSearchPuzzles => 'Procurar quebra-cabeças'; + String get broadcastShowScores => 'Mostrar pontuações dos jogadores com base nos resultados das partidas'; @override - String get puzzleFromMyGamesNone => 'Você não tem nenhum quebra-cabeça no banco de dados, mas o Lichess ainda te ama muito.\nJogue partidas rápidas e clássicas para aumentar suas chances de ter um desafio seu adicionado!'; + String get broadcastReplacePlayerTags => 'Opcional: substituir nomes de jogador, ratings e títulos'; @override - String puzzleFromXGamesFound(String param1, String param2) { - return '$param1 quebra-cabeças encontrados em $param2 partidas'; - } + String get broadcastFideFederations => 'Federações FIDE'; @override - String get puzzlePuzzleDashboardDescription => 'Treine, analise, melhore'; + String get broadcastTop10Rating => 'Classificação top 10'; @override - String puzzlePercentSolved(String param) { - return '$param resolvido'; - } + String get broadcastFidePlayers => 'Jogadores FIDE'; @override - String get puzzleNoPuzzlesToShow => 'Não há nada para mostrar aqui, jogue alguns quebra-cabeças primeiro!'; + String get broadcastFidePlayerNotFound => 'Jogador não encontrando na FIDE'; @override - String get puzzleImprovementAreasDescription => 'Treine estes para otimizar o seu progresso!'; + String get broadcastFideProfile => 'Perfil FIDE'; @override - String get puzzleStrengthDescription => 'Sua perfomance é melhor nesses temas'; + String get broadcastFederation => 'Federação'; @override - String puzzlePlayedXTimes(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Jogado $count vezes', - one: 'Jogado $count vezes', - ); - return '$_temp0'; - } + String get broadcastAgeThisYear => 'Idade atual'; @override - String puzzleNbPointsBelowYourPuzzleRating(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count pontos abaixo da sua classificação de quebra-cabeças', - one: 'Um ponto abaixo da sua classificação de quebra-cabeças', - ); - return '$_temp0'; - } + String get broadcastUnrated => 'Sem rating'; @override - String puzzleNbPointsAboveYourPuzzleRating(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count pontos acima da sua classificação de quebra-cabeças', - one: 'Um ponto acima da sua classificação de quebra-cabeças', - ); - return '$_temp0'; - } + String get broadcastRecentTournaments => 'Torneios recentes'; @override - String puzzleNbPlayed(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count jogados', - one: '$count jogado', - ); - return '$_temp0'; - } + String get broadcastOpenLichess => 'Abrir no Lichess'; @override - String puzzleNbToReplay(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count a serem repetidos', - one: '$count a ser repetido', - ); - return '$_temp0'; - } + String get broadcastTeams => 'Equipes'; @override - String get puzzleThemeAdvancedPawn => 'Peão avançado'; + String get broadcastBoards => 'Tabuleiros'; @override - String get puzzleThemeAdvancedPawnDescription => 'Um peão prestes a ser promovido ou à beira da promoção é um tema tático.'; + String get broadcastOverview => 'Visão geral'; @override - String get puzzleThemeAdvantage => 'Vantagem'; + String get broadcastSubscribeTitle => 'Inscreva-se para ser notificado no início de cada rodada. Você pode configurar as notificações de transmissões nas suas preferências.'; @override - String get puzzleThemeAdvantageDescription => 'Aproveite a sua chance de ter uma vantagem decisiva. (200cp ≤ eval ≤ 600cp)'; + String get broadcastUploadImage => 'Enviar imagem de torneio'; @override - String get puzzleThemeAnastasiaMate => 'Mate Anastasia'; + String get broadcastNoBoardsYet => 'Sem tabuleiros ainda. Eles vão aparecer quando os jogos forem enviados.'; @override - String get puzzleThemeAnastasiaMateDescription => 'Um cavalo e uma torre se unem para prender o rei do oponente entre a lateral do tabuleiro e uma peça amiga.'; + String broadcastBoardsCanBeLoaded(String param) { + return 'Tabuleiros são carregados com uma fonte ou pelo $param'; + } @override - String get puzzleThemeArabianMate => 'Mate árabe'; + String broadcastStartsAfter(String param) { + return 'Começa após $param'; + } @override - String get puzzleThemeArabianMateDescription => 'Um cavalo e uma torre se unem para prender o rei inimigo em um canto do tabuleiro.'; + String get broadcastStartVerySoon => 'A transmissão começará em breve.'; @override - String get puzzleThemeAttackingF2F7 => 'Atacando f2 ou f7'; + String get broadcastNotYetStarted => 'A transmissão ainda não começou.'; @override - String get puzzleThemeAttackingF2F7Description => 'Um ataque focado no peão de f2 e no peão de f7, como na abertura frango frito.'; + String get broadcastOfficialWebsite => 'Site oficial'; @override - String get puzzleThemeAttraction => 'Atração'; + String get broadcastStandings => 'Classificação'; @override - String get puzzleThemeAttractionDescription => 'Uma troca ou sacrifício encorajando ou forçando uma peça do oponente a uma casa que permite uma sequência tática.'; + String broadcastIframeHelp(String param) { + return 'Mais opções na $param'; + } @override - String get puzzleThemeBackRankMate => 'Mate do corredor'; + String get broadcastWebmastersPage => 'página dos webmasters'; @override - String get puzzleThemeBackRankMateDescription => 'Dê o xeque-mate no rei na última fileira, quando ele estiver bloqueado pelas próprias peças.'; + String broadcastPgnSourceHelp(String param) { + return 'Uma fonte PGN pública ao vivo desta rodada. Há também a $param para uma sincronização mais rápida e eficiente.'; + } @override - String get puzzleThemeBishopEndgame => 'Finais de bispo'; + String get broadcastEmbedThisBroadcast => 'Incorporar essa transmissão em seu site'; @override - String get puzzleThemeBishopEndgameDescription => 'Final com somente bispos e peões.'; + String broadcastEmbedThisRound(String param) { + return 'Incorporar $param em seu site'; + } @override - String get puzzleThemeBodenMate => 'Mate de Boden'; + String get broadcastRatingDiff => 'Diferência de pontos'; @override - String get puzzleThemeBodenMateDescription => 'Dois bispos atacantes em diagonais cruzadas dão um mate em um rei obstruído por peças amigas.'; + String get broadcastGamesThisTournament => 'Jogos neste torneio'; @override - String get puzzleThemeCastling => 'Roque'; + String get broadcastScore => 'Pontuação'; @override - String get puzzleThemeCastlingDescription => 'Traga o seu rei para a segurança, e prepare sua torre para o ataque.'; + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count transmissões', + one: '$count transmissão', + ); + return '$_temp0'; + } @override - String get puzzleThemeCapturingDefender => 'Capture o defensor'; + String challengeChallengesX(String param1) { + return 'Desafios: $param1'; + } @override - String get puzzleThemeCapturingDefenderDescription => 'Remover uma peça que seja importante na defesa de outra, permitindo que agora a peça indefesa seja capturada na jogada seguinte.'; + String get challengeChallengeToPlay => 'Desafiar para jogar'; @override - String get puzzleThemeCrushing => 'Punindo'; + String get challengeChallengeDeclined => 'Desafio recusado'; @override - String get puzzleThemeCrushingDescription => 'Perceba a capivarada do oponente para obter uma vantagem decisiva. (vantagem ≥ 600cp)'; + String get challengeChallengeAccepted => 'Desafio aceito!'; @override - String get puzzleThemeDoubleBishopMate => 'Mate de dois bispos'; + String get challengeChallengeCanceled => 'Desafio cancelado.'; @override - String get puzzleThemeDoubleBishopMateDescription => 'Dois bispos atacantes em diagonais adjacentes dão um mate em um rei obstruído por peças amigas.'; + String get challengeRegisterToSendChallenges => 'Por favor, registre-se para enviar desafios.'; @override - String get puzzleThemeDovetailMate => 'Mate da cauda de andorinha'; + String challengeYouCannotChallengeX(String param) { + return 'Você não pode desafiar $param.'; + } @override - String get puzzleThemeDovetailMateDescription => 'Uma dama dá um mate em um rei adjacente, cujos únicos dois quadrados de fuga estão obstruídos por peças amigas.'; + String challengeXDoesNotAcceptChallenges(String param) { + return '$param não aceita desafios.'; + } @override - String get puzzleThemeEquality => 'Igualdade'; + String challengeYourXRatingIsTooFarFromY(String param1, String param2) { + return 'O seu rating $param1 é muito diferente de $param2.'; + } @override - String get puzzleThemeEqualityDescription => 'Saia de uma posição perdida, e assegure um empate ou uma posição equilibrada. (aval ≤ 200cp)'; + String challengeCannotChallengeDueToProvisionalXRating(String param) { + return 'Não pode desafiar devido ao rating provisório de $param.'; + } @override - String get puzzleThemeKingsideAttack => 'Ataque na ala do Rei'; + String challengeXOnlyAcceptsChallengesFromFriends(String param) { + return '$param só aceita desafios de amigos.'; + } @override - String get puzzleThemeKingsideAttackDescription => 'Um ataque ao rei do oponente, após ele ter efetuado o roque curto.'; + String get challengeDeclineGeneric => 'Não estou aceitando desafios no momento.'; @override - String get puzzleThemeClearance => 'Lance útil'; + String get challengeDeclineLater => 'Este não é o momento certo para mim, por favor pergunte novamente mais tarde.'; @override - String get puzzleThemeClearanceDescription => 'Um lance, às vezes consumindo tempos, que libera uma casa, fileira ou diagonal para uma ideia tática em seguida.'; + String get challengeDeclineTooFast => 'Este controle de tempo é muito rápido para mim, por favor, desafie novamente com um jogo mais lento.'; @override - String get puzzleThemeDefensiveMove => 'Movimento defensivo'; + String get challengeDeclineTooSlow => 'Este controle de tempo é muito lento para mim, por favor, desafie novamente com um jogo mais rápido.'; @override - String get puzzleThemeDefensiveMoveDescription => 'Um movimento preciso ou sequência de movimentos que são necessários para evitar perda de material ou outra vantagem.'; + String get challengeDeclineTimeControl => 'Não estou aceitando desafios com estes controles de tempo.'; @override - String get puzzleThemeDeflection => 'Desvio'; + String get challengeDeclineRated => 'Por favor, envie-me um desafio ranqueado.'; @override - String get puzzleThemeDeflectionDescription => 'Um movimento que desvia a peça do oponente da sua função, por exemplo a de defesa de outra peça ou a defesa de uma casa importante.'; + String get challengeDeclineCasual => 'Por favor, envie-me um desafio amigável.'; @override - String get puzzleThemeDiscoveredAttack => 'Ataque descoberto'; + String get challengeDeclineStandard => 'Não estou aceitando desafios de variantes no momento.'; @override - String get puzzleThemeDiscoveredAttackDescription => 'Mover uma peça que anteriormente bloqueava um ataque de uma peça de longo alcance, como por exemplo um cavalo liberando a coluna de uma torre.'; + String get challengeDeclineVariant => 'Não estou a fim de jogar esta variante no momento.'; @override - String get puzzleThemeDoubleCheck => 'Xeque duplo'; + String get challengeDeclineNoBot => 'Não estou aceitando desafios de robôs.'; @override - String get puzzleThemeDoubleCheckDescription => 'Dar Xeque com duas peças ao mesmo tempo, como resultado de um ataque descoberto onde tanto a peça que se move quanto a peça que estava sendo obstruída atacam o rei do oponente.'; + String get challengeDeclineOnlyBot => 'Estou aceitando apenas desafios de robôs.'; @override - String get puzzleThemeEndgame => 'Finais'; + String get challengeInviteLichessUser => 'Ou convide um usuário Lichess:'; @override - String get puzzleThemeEndgameDescription => 'Tática durante a última fase do jogo.'; + String get contactContact => 'Contato'; @override - String get puzzleThemeEnPassantDescription => 'Uma tática envolvendo a regra do en passant, onde um peão pode capturar um peão do oponente que passou por ele usando seu movimento inicial de duas casas.'; + String get contactContactLichess => 'Entrar em contato com Lichess'; @override - String get puzzleThemeExposedKing => 'Rei exposto'; + String get patronDonate => 'Doação'; @override - String get puzzleThemeExposedKingDescription => 'Uma tática que envolve um rei com poucos defensores ao seu redor, muitas vezes levando a xeque-mate.'; + String get patronLichessPatron => 'Apoie o Lichess'; @override - String get puzzleThemeFork => 'Garfo (ou duplo)'; + String perfStatPerfStats(String param) { + return 'Estatísticas de $param'; + } @override - String get puzzleThemeForkDescription => 'Um movimento onde a peça movida ataca duas peças de oponente de uma só vez.'; + String get perfStatViewTheGames => 'Ver os jogos'; @override - String get puzzleThemeHangingPiece => 'Peça pendurada'; + String get perfStatProvisional => 'provisório'; @override - String get puzzleThemeHangingPieceDescription => 'Uma táctica que envolve uma peça indefesa do oponente ou insuficientemente defendida e livre para ser capturada.'; + String get perfStatNotEnoughRatedGames => 'Não foram jogadas partidas suficientes valendo rating para estabelecer uma classificação confiável.'; @override - String get puzzleThemeHookMate => 'Xeque gancho'; + String perfStatProgressOverLastXGames(String param) { + return 'Progresso nos últimos $param jogos:'; + } @override - String get puzzleThemeHookMateDescription => 'Xeque-mate com uma torre, um cavalo e um peão, juntamente com um peão inimigo, para limitar a fuga do rei.'; + String perfStatRatingDeviation(String param) { + return 'Desvio de pontuação: $param.'; + } @override - String get puzzleThemeInterference => 'Interferência'; + String perfStatRatingDeviationTooltip(String param1, String param2, String param3) { + return 'Um valor inferior indica que a pontuação é mais estável. Superior a $param1, a pontuação é classificada como provisória. Para ser incluída nas classificações, esse valor deve ser inferior a $param2 (xadrez padrão) ou $param3 (variantes).'; + } @override - String get puzzleThemeInterferenceDescription => 'Mover uma peça entre duas peças do oponente para deixar uma ou duas peças do oponente indefesas, como um cavalo em uma casa defendida por duas torres.'; + String get perfStatTotalGames => 'Total de partidas'; @override - String get puzzleThemeIntermezzo => 'Lance intermediário'; + String get perfStatRatedGames => 'Partidas valendo pontos'; @override - String get puzzleThemeIntermezzoDescription => 'Em vez de jogar o movimento esperado, primeiro realiza outro movimento criando uma ameaça imediata a que o oponente deve responder. Também conhecido como \"Zwischenzug\" ou \"In between\".'; + String get perfStatTournamentGames => 'Jogos de torneio'; @override - String get puzzleThemeKnightEndgame => 'Finais de Cavalo'; + String get perfStatBerserkedGames => 'Partidas Berserked'; @override - String get puzzleThemeKnightEndgameDescription => 'Um final jogado apenas com cavalos e peões.'; + String get perfStatTimeSpentPlaying => 'Tempo jogando'; @override - String get puzzleThemeLong => 'Quebra-cabeças longo'; + String get perfStatAverageOpponent => 'Pontuação média do adversário'; @override - String get puzzleThemeLongDescription => 'Vitória em três movimentos.'; + String get perfStatVictories => 'Vitórias'; @override - String get puzzleThemeMaster => 'Partidas de mestres'; + String get perfStatDefeats => 'Derrotas'; @override - String get puzzleThemeMasterDescription => 'Quebra-cabeças de partidas jogadas por jogadores titulados.'; + String get perfStatDisconnections => 'Desconexões'; @override - String get puzzleThemeMasterVsMaster => 'Partidas de Mestre vs Mestre'; + String get perfStatNotEnoughGames => 'Jogos insuficientes jogados'; @override - String get puzzleThemeMasterVsMasterDescription => 'Quebra-cabeças de partidas entre dois jogadores titulados.'; + String perfStatHighestRating(String param) { + return 'Pontuação mais alta: $param'; + } @override - String get puzzleThemeMate => 'Xeque-mate'; + String perfStatLowestRating(String param) { + return 'Rating mais baixo: $param'; + } @override - String get puzzleThemeMateDescription => 'Vença o jogo com estilo.'; + String perfStatFromXToY(String param1, String param2) { + return 'de $param1 para $param2'; + } @override - String get puzzleThemeMateIn1 => 'Mate em 1'; + String get perfStatWinningStreak => 'Série de Vitórias'; @override - String get puzzleThemeMateIn1Description => 'Dar xeque-mate em um movimento.'; + String get perfStatLosingStreak => 'Série de derrotas'; @override - String get puzzleThemeMateIn2 => 'Mate em 2'; + String perfStatLongestStreak(String param) { + return 'Sequência mais longa: $param'; + } @override - String get puzzleThemeMateIn2Description => 'Dar xeque-mate em dois movimentos.'; + String perfStatCurrentStreak(String param) { + return 'Sequência atual: $param'; + } @override - String get puzzleThemeMateIn3 => 'Mate em 3'; + String get perfStatBestRated => 'Melhores vitórias valendo pontuação'; @override - String get puzzleThemeMateIn3Description => 'Dar xeque-mate em três movimentos.'; + String get perfStatGamesInARow => 'Partidas jogadas seguidas'; @override - String get puzzleThemeMateIn4 => 'Mate em 4'; + String get perfStatLessThanOneHour => 'Menos de uma hora entre partidas'; @override - String get puzzleThemeMateIn4Description => 'Dar xeque-mate em 4 movimentos.'; + String get perfStatMaxTimePlaying => 'Tempo máximo jogando'; @override - String get puzzleThemeMateIn5 => 'Mate em 5 ou mais'; + String get perfStatNow => 'agora'; @override - String get puzzleThemeMateIn5Description => 'Descubra uma longa sequência de mate.'; + String get preferencesPreferences => 'Preferências'; @override - String get puzzleThemeMiddlegame => 'Meio-jogo'; + String get preferencesDisplay => 'Exibição'; @override - String get puzzleThemeMiddlegameDescription => 'Tática durante a segunda fase do jogo.'; + String get preferencesPrivacy => 'Privacidade'; @override - String get puzzleThemeOneMove => 'Quebra-cabeças de um movimento'; + String get preferencesNotifications => 'Notificações'; @override - String get puzzleThemeOneMoveDescription => 'Quebra-cabeças de um movimento.'; + String get preferencesPieceAnimation => 'Animação das peças'; @override - String get puzzleThemeOpening => 'Abertura'; + String get preferencesMaterialDifference => 'Diferença material'; @override - String get puzzleThemeOpeningDescription => 'Tática durante a primeira fase do jogo.'; + String get preferencesBoardHighlights => 'Destacar casas do tabuleiro (último movimento e xeque)'; @override - String get puzzleThemePawnEndgame => 'Finais de peões'; + String get preferencesPieceDestinations => 'Destino das peças (movimentos válidos e pré-movimentos)'; @override - String get puzzleThemePawnEndgameDescription => 'Um final apenas com peões.'; + String get preferencesBoardCoordinates => 'Coordenadas do tabuleiro (A-H, 1-8)'; @override - String get puzzleThemePin => 'Cravada'; + String get preferencesMoveListWhilePlaying => 'Lista de movimentos durante a partida'; @override - String get puzzleThemePinDescription => 'Uma tática envolvendo cravada, onde uma peça é incapaz de mover-se sem abrir um descoberto em uma peça de maior valor.'; + String get preferencesPgnPieceNotation => 'Modo de notação das jogadas'; @override - String get puzzleThemePromotion => 'Promoção'; + String get preferencesChessPieceSymbol => 'Símbolo da peça'; @override - String get puzzleThemePromotionDescription => 'Promova um peão para uma dama ou a uma peça menor.'; + String get preferencesPgnLetter => 'Letra (K, Q, R, B, N)'; @override - String get puzzleThemeQueenEndgame => 'Finais de Dama'; + String get preferencesZenMode => 'Modo Zen'; @override - String get puzzleThemeQueenEndgameDescription => 'Um final com apenas damas e peões.'; + String get preferencesShowPlayerRatings => 'Mostrar rating dos jogadores'; @override - String get puzzleThemeQueenRookEndgame => 'Finais de Dama e Torre'; + String get preferencesShowFlairs => 'Mostrar emotes de usuário'; @override - String get puzzleThemeQueenRookEndgameDescription => 'Finais com apenas Dama, Torre e Peões.'; + String get preferencesExplainShowPlayerRatings => 'Permite ocultar todas os ratings do site, para ajudar a se concentrar no jogo. As partidas continuam valendo rating.'; @override - String get puzzleThemeQueensideAttack => 'Ataque na ala da dama'; + String get preferencesDisplayBoardResizeHandle => 'Mostrar cursor de redimensionamento do tabuleiro'; @override - String get puzzleThemeQueensideAttackDescription => 'Um ataque ao rei adversário, após ter efetuado o roque na ala da Dama.'; + String get preferencesOnlyOnInitialPosition => 'Apenas na posição inicial'; @override - String get puzzleThemeQuietMove => 'Lance de preparação'; + String get preferencesInGameOnly => 'Durante partidas'; @override - String get puzzleThemeQuietMoveDescription => 'Um lance que não dá xeque nem realiza uma captura, mas prepara uma ameaça inevitável para a jogada seguinte.'; + String get preferencesChessClock => 'Relógio'; @override - String get puzzleThemeRookEndgame => 'Finais de Torres'; + String get preferencesTenthsOfSeconds => 'Décimos de segundo'; @override - String get puzzleThemeRookEndgameDescription => 'Um final com apenas torres e peões.'; + String get preferencesWhenTimeRemainingLessThanTenSeconds => 'Quando o tempo restante < 10 segundos'; @override - String get puzzleThemeSacrifice => 'Sacrifício'; + String get preferencesHorizontalGreenProgressBars => 'Barras de progresso verdes horizontais'; @override - String get puzzleThemeSacrificeDescription => 'Uma tática envolvendo a entrega de material no curto prazo, com o objetivo de se obter uma vantagem após uma sequência forçada de movimentos.'; + String get preferencesSoundWhenTimeGetsCritical => 'Som ao atingir tempo crítico'; @override - String get puzzleThemeShort => 'Quebra-cabeças curto'; + String get preferencesGiveMoreTime => 'Dar mais tempo'; @override - String get puzzleThemeShortDescription => 'Vitória em dois lances.'; + String get preferencesGameBehavior => 'Comportamento do jogo'; @override - String get puzzleThemeSkewer => 'Raio X'; + String get preferencesHowDoYouMovePieces => 'Como você move as peças?'; @override - String get puzzleThemeSkewerDescription => 'Um movimento que envolve uma peça de alto valor sendo atacada fugindo do ataque e permitindo que uma peça de menor valor seja capturada ou atacada, o inverso de cravada.'; + String get preferencesClickTwoSquares => 'Clicar em duas casas'; @override - String get puzzleThemeSmotheredMate => 'Mate de Philidor (mate sufocado)'; + String get preferencesDragPiece => 'Arrastar a peça'; @override - String get puzzleThemeSmotheredMateDescription => 'Um xeque-mate dado por um cavalo onde o rei é incapaz de mover-se porque está cercado (ou sufocado) pelas próprias peças.'; + String get preferencesBothClicksAndDrag => 'Ambas'; @override - String get puzzleThemeSuperGM => 'Super partidas de GMs'; + String get preferencesPremovesPlayingDuringOpponentTurn => 'Pré-movimentos (jogadas durante o turno do oponente)'; @override - String get puzzleThemeSuperGMDescription => 'Quebra-cabeças de partidas jogadas pelos melhores jogadores do mundo.'; + String get preferencesTakebacksWithOpponentApproval => 'Voltar jogada (com aprovação do oponente)'; @override - String get puzzleThemeTrappedPiece => 'Peça presa'; + String get preferencesInCasualGamesOnly => 'Somente em jogos casuais'; @override - String get puzzleThemeTrappedPieceDescription => 'Uma peça é incapaz de escapar da captura, pois tem movimentos limitados.'; + String get preferencesPromoteToQueenAutomatically => 'Promover a Dama automaticamente'; @override - String get puzzleThemeUnderPromotion => 'Subpromoção'; + String get preferencesExplainPromoteToQueenAutomatically => 'Mantenha a tecla pressionada enquanto promove para desativar temporariamente a autopromoção'; @override - String get puzzleThemeUnderPromotionDescription => 'Promover para cavalo, bispo ou torre.'; + String get preferencesWhenPremoving => 'Quando pré-mover'; @override - String get puzzleThemeVeryLong => 'Quebra-cabeças muito longo'; + String get preferencesClaimDrawOnThreefoldRepetitionAutomatically => 'Reivindicar empate sobre a repetição tripla automaticamente'; @override - String get puzzleThemeVeryLongDescription => 'Quatro movimentos ou mais para vencer.'; + String get preferencesWhenTimeRemainingLessThanThirtySeconds => 'Quando o tempo restante < 30 segundos'; @override - String get puzzleThemeXRayAttack => 'Ataque em raio X'; + String get preferencesMoveConfirmation => 'Confirmação de movimento'; @override - String get puzzleThemeXRayAttackDescription => 'Uma peça ataca ou defende uma casa indiretamente, através de uma peça adversária.'; + String get preferencesExplainCanThenBeTemporarilyDisabled => 'Pode ser desativado durante a partida no menu do tabuleiro'; @override - String get puzzleThemeZugzwang => 'Zugzwang'; + String get preferencesInCorrespondenceGames => 'Jogos por correspondência'; @override - String get puzzleThemeZugzwangDescription => 'O adversário tem os seus movimentos limitados, e qualquer movimento que ele faça vai enfraquecer sua própria posição.'; + String get preferencesCorrespondenceAndUnlimited => 'Por correspondência e sem limites'; @override - String get puzzleThemeHealthyMix => 'Combinação saudável'; + String get preferencesConfirmResignationAndDrawOffers => 'Confirmar abandono e oferta de empate'; @override - String get puzzleThemeHealthyMixDescription => 'Um pouco de tudo. Você nunca sabe o que vai encontrar, então esteja pronto para tudo! Igualzinho aos jogos em tabuleiros reais.'; + String get preferencesCastleByMovingTheKingTwoSquaresOrOntoTheRook => 'Maneira de rocar'; @override - String get puzzleThemePlayerGames => 'Partidas de jogadores'; + String get preferencesCastleByMovingTwoSquares => 'Mover o rei duas casas'; @override - String get puzzleThemePlayerGamesDescription => 'Procure quebra-cabeças gerados a partir de suas partidas ou das de outro jogador.'; + String get preferencesCastleByMovingOntoTheRook => 'Mover o rei em direção à torre'; @override - String puzzleThemePuzzleDownloadInformation(String param) { - return 'Esses quebra-cabeças estão em domínio público, e você pode baixá-los em $param.'; - } + String get preferencesInputMovesWithTheKeyboard => 'Fazer lances com escrita do teclado'; @override - String get searchSearch => 'Buscar'; + String get preferencesInputMovesWithVoice => 'Mova as peças com sua voz'; @override - String get settingsSettings => 'Configurações'; + String get preferencesSnapArrowsToValidMoves => 'Insira setas para movimentos válidos'; @override - String get settingsCloseAccount => 'Encerrar conta'; + String get preferencesSayGgWpAfterLosingOrDrawing => 'Diga \"Bom jogo, bem jogado\" após a derrota ou empate'; @override - String get settingsManagedAccountCannotBeClosed => 'Sua conta é gerenciada, e não pode ser encerrada.'; + String get preferencesYourPreferencesHaveBeenSaved => 'Suas preferências foram salvas.'; @override - String get settingsClosingIsDefinitive => 'O encerramento é definitivo. Não há como desfazer. Tem certeza?'; + String get preferencesScrollOnTheBoardToReplayMoves => 'Use o scroll do mouse no tabuleiro para ir passando as jogadas'; @override - String get settingsCantOpenSimilarAccount => 'Você não poderá abrir uma nova conta com o mesmo nome, mesmo que alterne entre maiúsculas e minúsculas.'; + String get preferencesCorrespondenceEmailNotification => 'Email diário listando seus jogos por correspondência'; @override - String get settingsChangedMindDoNotCloseAccount => 'Eu mudei de ideia, não encerre minha conta'; + String get preferencesNotifyStreamStart => 'Streamer começou uma transmissão ao vivo'; @override - String get settingsCloseAccountExplanation => 'Tem certeza de que deseja encerrar sua conta? Encerrar sua conta é uma decisão permanente. Você NUNCA MAIS será capaz de entrar com ela novamente.'; + String get preferencesNotifyInboxMsg => 'Nova mensagem na caixa de entrada'; @override - String get settingsThisAccountIsClosed => 'Esta conta foi encerrada.'; + String get preferencesNotifyForumMention => 'Você foi mencionado em um comentário do fórum'; @override - String get playWithAFriend => 'Jogar contra um amigo'; + String get preferencesNotifyInvitedStudy => 'Convite para um estudo'; @override - String get playWithTheMachine => 'Jogar contra o computador'; + String get preferencesNotifyGameEvent => 'Jogo por correspondência atualizado'; @override - String get toInviteSomeoneToPlayGiveThisUrl => 'Para convidar alguém para jogar, envie este URL'; + String get preferencesNotifyChallenge => 'Desafios'; @override - String get gameOver => 'Fim da partida'; + String get preferencesNotifyTournamentSoon => 'O torneio vai começar em breve'; @override - String get waitingForOpponent => 'Aguardando oponente'; + String get preferencesNotifyTimeAlarm => 'Está acabando o tempo no jogo por correspondência'; @override - String get orLetYourOpponentScanQrCode => 'Ou deixe seu oponente ler este QR Code'; + String get preferencesNotifyBell => 'Notificação no Lichess'; @override - String get waiting => 'Aguardando'; + String get preferencesNotifyPush => 'Notificação no dispositivo fora do Lichess'; @override - String get yourTurn => 'Sua vez'; + String get preferencesNotifyWeb => 'Navegador'; @override - String aiNameLevelAiLevel(String param1, String param2) { - return '$param1 nível $param2'; - } + String get preferencesNotifyDevice => 'Dispositivo'; @override - String get level => 'Nível'; + String get preferencesBellNotificationSound => 'Som da notificação'; @override - String get strength => 'Nível'; + String get puzzlePuzzles => 'Quebra-cabeças'; @override - String get toggleTheChat => 'Ativar/Desativar chat'; + String get puzzlePuzzleThemes => 'Temas de quebra-cabeça'; @override - String get chat => 'Chat'; + String get puzzleRecommended => 'Recomendado'; @override - String get resign => 'Desistir'; + String get puzzlePhases => 'Fases'; @override - String get checkmate => 'Xeque-mate'; + String get puzzleMotifs => 'Motivos táticos'; @override - String get stalemate => 'Rei afogado'; + String get puzzleAdvanced => 'Avançado'; @override - String get white => 'Brancas'; + String get puzzleLengths => 'Distância'; @override - String get black => 'Pretas'; + String get puzzleMates => 'Xeque-mates'; @override - String get asWhite => 'de brancas'; + String get puzzleGoals => 'Objetivos'; @override - String get asBlack => 'de pretas'; + String get puzzleOrigin => 'Origem'; @override - String get randomColor => 'Cor aleatória'; + String get puzzleSpecialMoves => 'Movimentos especiais'; @override - String get createAGame => 'Criar uma partida'; + String get puzzleDidYouLikeThisPuzzle => 'Você gostou deste quebra-cabeças?'; @override - String get whiteIsVictorious => 'Brancas vencem'; + String get puzzleVoteToLoadNextOne => 'Vote para carregar o próximo!'; @override - String get blackIsVictorious => 'Pretas vencem'; + String get puzzleUpVote => 'Votar a favor do quebra-cabeça'; @override - String get youPlayTheWhitePieces => 'Você joga com as peças brancas'; + String get puzzleDownVote => 'Votar contra o quebra-cabeça'; @override - String get youPlayTheBlackPieces => 'Você joga com as peças pretas'; + String get puzzleYourPuzzleRatingWillNotChange => 'Sua pontuação de quebra-cabeças não mudará. Note que os quebra-cabeças não são uma competição. A pontuação indica os quebra-cabeças que se adequam às suas habilidades.'; @override - String get itsYourTurn => 'É a sua vez!'; + String get puzzleFindTheBestMoveForWhite => 'Encontre o melhor lance para as brancas.'; @override - String get cheatDetected => 'Trapaça Detectada'; + String get puzzleFindTheBestMoveForBlack => 'Encontre a melhor jogada para as pretas.'; @override - String get kingInTheCenter => 'Rei no centro'; + String get puzzleToGetPersonalizedPuzzles => 'Para obter desafios personalizados:'; @override - String get threeChecks => 'Três xeques'; + String puzzlePuzzleId(String param) { + return 'Quebra-cabeça $param'; + } @override - String get raceFinished => 'Corrida terminada'; + String get puzzlePuzzleOfTheDay => 'Quebra-cabeça do dia'; @override - String get variantEnding => 'Fim da variante'; + String get puzzleDailyPuzzle => 'Quebra-cabeça diário'; @override - String get newOpponent => 'Novo oponente'; + String get puzzleClickToSolve => 'Clique para resolver'; @override - String get yourOpponentWantsToPlayANewGameWithYou => 'Seu oponente quer jogar uma nova partida contra você'; + String get puzzleGoodMove => 'Boa jogada'; @override - String get joinTheGame => 'Entrar no jogo'; + String get puzzleBestMove => 'Melhor jogada!'; @override - String get whitePlays => 'Brancas jogam'; + String get puzzleKeepGoing => 'Continue…'; @override - String get blackPlays => 'Pretas jogam'; + String get puzzlePuzzleSuccess => 'Sucesso!'; @override - String get opponentLeftChoices => 'O seu oponente deixou a partida. Você pode reivindicar vitória, declarar empate ou aguardar.'; + String get puzzlePuzzleComplete => 'Quebra-cabeças concluído!'; @override - String get forceResignation => 'Reivindicar vitória'; + String get puzzleByOpenings => 'Por abertura'; @override - String get forceDraw => 'Reivindicar empate'; + String get puzzlePuzzlesByOpenings => 'Quebra-cabeças por abertura'; @override - String get talkInChat => 'Por favor, seja gentil no chat!'; + String get puzzleOpeningsYouPlayedTheMost => 'Aberturas que você mais jogou em partidas valendo pontos'; @override - String get theFirstPersonToComeOnThisUrlWillPlayWithYou => 'A primeira pessoa que acessar esta URL jogará contigo.'; + String get puzzleUseFindInPage => 'Use a ferramenta \"Encontrar na página\" do navegador para encontrar sua abertura favorita!'; @override - String get whiteResigned => 'Brancas desistiram'; + String get puzzleUseCtrlF => 'Aperte Ctrl + F para encontrar sua abertura favorita!'; @override - String get blackResigned => 'Pretas desistiram'; + String get puzzleNotTheMove => 'O movimento não é este!'; @override - String get whiteLeftTheGame => 'Brancas deixaram a partida'; + String get puzzleTrySomethingElse => 'Tente algo diferente.'; @override - String get blackLeftTheGame => 'Pretas deixaram a partida'; + String puzzleRatingX(String param) { + return 'Rating: $param'; + } @override - String get whiteDidntMove => 'As brancas não se moveram'; + String get puzzleHidden => 'oculto'; @override - String get blackDidntMove => 'As pretas não se moveram'; + String puzzleFromGameLink(String param) { + return 'Do jogo $param'; + } @override - String get requestAComputerAnalysis => 'Solicitar uma análise do computador'; + String get puzzleContinueTraining => 'Continue treinando'; @override - String get computerAnalysis => 'Análise do computador'; + String get puzzleDifficultyLevel => 'Nível de dificuldade'; @override - String get computerAnalysisAvailable => 'Análise de computador disponível'; + String get puzzleNormal => 'Normal'; @override - String get computerAnalysisDisabled => 'Análise de computador desativada'; + String get puzzleEasier => 'Fácil'; @override - String get analysis => 'Análise'; + String get puzzleEasiest => 'Muito fácil'; @override - String depthX(String param) { - return 'Profundidade $param'; - } + String get puzzleHarder => 'Difícil'; @override - String get usingServerAnalysis => 'Análise de servidor em uso'; + String get puzzleHardest => 'Muito difícil'; @override - String get loadingEngine => 'Carregando ...'; + String get puzzleExample => 'Exemplo'; @override - String get calculatingMoves => 'Calculando jogadas...'; + String get puzzleAddAnotherTheme => 'Adicionar um outro tema'; @override - String get engineFailed => 'Erro ao carregar o engine'; + String get puzzleNextPuzzle => 'Próximo quebra-cabeça'; @override - String get cloudAnalysis => 'Análise na nuvem'; + String get puzzleJumpToNextPuzzleImmediately => 'Ir para o próximo problema automaticamente'; @override - String get goDeeper => 'Detalhar'; + String get puzzlePuzzleDashboard => 'Painel do quebra-cabeças'; @override - String get showThreat => 'Mostrar ameaça'; + String get puzzleImprovementAreas => 'Áreas de aprimoramento'; @override - String get inLocalBrowser => 'no navegador local'; + String get puzzleStrengths => 'Pontos fortes'; @override - String get toggleLocalEvaluation => 'Ativar/Desativar análise local'; + String get puzzleHistory => 'Histórico de quebra-cabeças'; @override - String get promoteVariation => 'Promover variante'; + String get puzzleSolved => 'resolvido'; @override - String get makeMainLine => 'Transformar em linha principal'; + String get puzzleFailed => 'falhou'; @override - String get deleteFromHere => 'Excluir a partir daqui'; + String get puzzleStreakDescription => 'Resolva quebra-cabeças progressivamente mais difíceis e construa uma sequência de vitórias. Não há relógio, então tome seu tempo. Um movimento errado e o jogo acaba! Porém, você pode pular um movimento por sessão.'; @override - String get collapseVariations => 'Esconder variantes'; + String puzzleYourStreakX(String param) { + return 'Sua sequência: $param'; + } @override - String get expandVariations => 'Mostrar variantes'; + String get puzzleStreakSkipExplanation => 'Pule este lance para preservar a sua sequência! Funciona apenas uma vez por corrida.'; @override - String get forceVariation => 'Variante forçada'; + String get puzzleContinueTheStreak => 'Continuar a sequência'; @override - String get copyVariationPgn => 'Copiar PGN da variante'; + String get puzzleNewStreak => 'Nova sequência'; @override - String get move => 'Movimentos'; + String get puzzleFromMyGames => 'Dos meus jogos'; @override - String get variantLoss => 'Derrota da variante'; + String get puzzleLookupOfPlayer => 'Pesquise quebra-cabeças de um jogador específico'; @override - String get variantWin => 'Vitória da variante'; + String puzzleFromXGames(String param) { + return 'Problemas de $param\' jogos'; + } @override - String get insufficientMaterial => 'Material insuficiente'; + String get puzzleSearchPuzzles => 'Procurar quebra-cabeças'; @override - String get pawnMove => 'Movimento de peão'; + String get puzzleFromMyGamesNone => 'Você não tem nenhum quebra-cabeça no banco de dados, mas o Lichess ainda te ama muito.\nJogue partidas rápidas e clássicas para aumentar suas chances de ter um desafio seu adicionado!'; @override - String get capture => 'Captura'; + String puzzleFromXGamesFound(String param1, String param2) { + return '$param1 quebra-cabeças encontrados em $param2 partidas'; + } @override - String get close => 'Fechar'; + String get puzzlePuzzleDashboardDescription => 'Treine, analise, melhore'; @override - String get winning => 'Vencendo'; + String puzzlePercentSolved(String param) { + return '$param resolvido'; + } @override - String get losing => 'Perdendo'; + String get puzzleNoPuzzlesToShow => 'Não há nada para mostrar aqui, jogue alguns quebra-cabeças primeiro!'; @override - String get drawn => 'Empate'; + String get puzzleImprovementAreasDescription => 'Treine estes para otimizar o seu progresso!'; @override - String get unknown => 'Posição desconhecida'; + String get puzzleStrengthDescription => 'Sua perfomance é melhor nesses temas'; @override - String get database => 'Banco de Dados'; + String puzzlePlayedXTimes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Jogado $count vezes', + one: 'Jogado $count vezes', + ); + return '$_temp0'; + } @override - String get whiteDrawBlack => 'Brancas / Empate / Pretas'; + String puzzleNbPointsBelowYourPuzzleRating(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count pontos abaixo da sua classificação de quebra-cabeças', + one: 'Um ponto abaixo da sua classificação de quebra-cabeças', + ); + return '$_temp0'; + } @override - String averageRatingX(String param) { - return 'Classificação média: $param'; + String puzzleNbPointsAboveYourPuzzleRating(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count pontos acima da sua classificação de quebra-cabeças', + one: 'Um ponto acima da sua classificação de quebra-cabeças', + ); + return '$_temp0'; } @override - String get recentGames => 'Partidas recentes'; + String puzzleNbPlayed(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count jogados', + one: '$count jogado', + ); + return '$_temp0'; + } @override - String get topGames => 'Melhores partidas'; + String puzzleNbToReplay(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count a serem repetidos', + one: '$count a ser repetido', + ); + return '$_temp0'; + } @override - String masterDbExplanation(String param1, String param2, String param3) { - return 'Duas milhões de partidas de jogadores com pontuação FIDE acima de $param1, desde $param2 a $param3'; - } + String get puzzleThemeAdvancedPawn => 'Peão avançado'; @override - String get dtzWithRounding => 'DTZ50\" com arredondamento, baseado no número de meias-jogadas até a próxima captura ou jogada de peão'; + String get puzzleThemeAdvancedPawnDescription => 'Um peão prestes a ser promovido ou à beira da promoção é um tema tático.'; @override - String get noGameFound => 'Nenhuma partida encontrada'; + String get puzzleThemeAdvantage => 'Vantagem'; @override - String get maxDepthReached => 'Profundidade máxima alcançada!'; + String get puzzleThemeAdvantageDescription => 'Aproveite a sua chance de ter uma vantagem decisiva. (200cp ≤ eval ≤ 600cp)'; @override - String get maybeIncludeMoreGamesFromThePreferencesMenu => 'Talvez você queira incluir mais jogos a partir do menu de preferências'; + String get puzzleThemeAnastasiaMate => 'Mate Anastasia'; @override - String get openings => 'Aberturas'; + String get puzzleThemeAnastasiaMateDescription => 'Um cavalo e uma torre se unem para prender o rei do oponente entre a lateral do tabuleiro e uma peça amiga.'; @override - String get openingExplorer => 'Explorador de aberturas'; + String get puzzleThemeArabianMate => 'Mate árabe'; @override - String get openingEndgameExplorer => 'Explorador de Aberturas/Finais'; + String get puzzleThemeArabianMateDescription => 'Um cavalo e uma torre se unem para prender o rei inimigo em um canto do tabuleiro.'; @override - String xOpeningExplorer(String param) { - return '$param Explorador de aberturas'; - } + String get puzzleThemeAttackingF2F7 => 'Atacando f2 ou f7'; @override - String get playFirstOpeningEndgameExplorerMove => 'Jogue o primeiro lance do explorador de aberturas/finais'; + String get puzzleThemeAttackingF2F7Description => 'Um ataque focado no peão de f2 e no peão de f7, como na abertura frango frito.'; @override - String get winPreventedBy50MoveRule => 'Vitória impedida pela regra dos 50 movimentos'; + String get puzzleThemeAttraction => 'Atração'; @override - String get lossSavedBy50MoveRule => 'Derrota impedida pela regra dos 50 movimentos'; + String get puzzleThemeAttractionDescription => 'Uma troca ou sacrifício encorajando ou forçando uma peça do oponente a uma casa que permite uma sequência tática.'; @override - String get winOr50MovesByPriorMistake => 'Vitória ou 50 movimentos por erro anterior'; + String get puzzleThemeBackRankMate => 'Mate do corredor'; @override - String get lossOr50MovesByPriorMistake => 'Derrota ou 50 movimentos por erro anterior'; + String get puzzleThemeBackRankMateDescription => 'Dê o xeque-mate no rei na última fileira, quando ele estiver bloqueado pelas próprias peças.'; @override - String get unknownDueToRounding => 'Vitória/derrota garantida somente se a variante recomendada tiver sido seguida desde o último movimento de captura ou de peão, devido ao possível arredondamento.'; + String get puzzleThemeBishopEndgame => 'Finais de bispo'; @override - String get allSet => 'Tudo pronto!'; + String get puzzleThemeBishopEndgameDescription => 'Final com somente bispos e peões.'; @override - String get importPgn => 'Importar PGN'; + String get puzzleThemeBodenMate => 'Mate de Boden'; @override - String get delete => 'Excluir'; + String get puzzleThemeBodenMateDescription => 'Dois bispos atacantes em diagonais cruzadas dão um mate em um rei obstruído por peças amigas.'; @override - String get deleteThisImportedGame => 'Excluir este jogo importado?'; + String get puzzleThemeCastling => 'Roque'; @override - String get replayMode => 'Rever a partida'; + String get puzzleThemeCastlingDescription => 'Traga o seu rei para a segurança, e prepare sua torre para o ataque.'; @override - String get realtimeReplay => 'Tempo Real'; + String get puzzleThemeCapturingDefender => 'Capture o defensor'; @override - String get byCPL => 'Por erros'; + String get puzzleThemeCapturingDefenderDescription => 'Remover uma peça que seja importante na defesa de outra, permitindo que agora a peça indefesa seja capturada na jogada seguinte.'; @override - String get openStudy => 'Abrir estudo'; + String get puzzleThemeCrushing => 'Punindo'; @override - String get enable => 'Ativar'; + String get puzzleThemeCrushingDescription => 'Perceba a capivarada do oponente para obter uma vantagem decisiva. (vantagem ≥ 600cp)'; @override - String get bestMoveArrow => 'Seta de melhor movimento'; + String get puzzleThemeDoubleBishopMate => 'Mate de dois bispos'; @override - String get showVariationArrows => 'Mostrar setas das variantes'; + String get puzzleThemeDoubleBishopMateDescription => 'Dois bispos atacantes em diagonais adjacentes dão um mate em um rei obstruído por peças amigas.'; @override - String get evaluationGauge => 'Escala de avaliação'; + String get puzzleThemeDovetailMate => 'Mate da cauda de andorinha'; @override - String get multipleLines => 'Linhas de análise'; + String get puzzleThemeDovetailMateDescription => 'Uma dama dá um mate em um rei adjacente, cujos únicos dois quadrados de fuga estão obstruídos por peças amigas.'; @override - String get cpus => 'CPUs'; + String get puzzleThemeEquality => 'Igualdade'; @override - String get memory => 'Memória'; + String get puzzleThemeEqualityDescription => 'Saia de uma posição perdida, e assegure um empate ou uma posição equilibrada. (aval ≤ 200cp)'; @override - String get infiniteAnalysis => 'Análise infinita'; + String get puzzleThemeKingsideAttack => 'Ataque na ala do Rei'; @override - String get removesTheDepthLimit => 'Remove o limite de profundidade, o que aquece seu computador'; + String get puzzleThemeKingsideAttackDescription => 'Um ataque ao rei do oponente, após ele ter efetuado o roque curto.'; @override - String get engineManager => 'Gerenciador de engine'; + String get puzzleThemeClearance => 'Lance útil'; @override - String get blunder => 'Capivarada'; + String get puzzleThemeClearanceDescription => 'Um lance, às vezes consumindo tempos, que libera uma casa, fileira ou diagonal para uma ideia tática em seguida.'; @override - String get mistake => 'Erro'; + String get puzzleThemeDefensiveMove => 'Movimento defensivo'; @override - String get inaccuracy => 'Imprecisão'; + String get puzzleThemeDefensiveMoveDescription => 'Um movimento preciso ou sequência de movimentos que são necessários para evitar perda de material ou outra vantagem.'; @override - String get moveTimes => 'Tempo por movimento'; + String get puzzleThemeDeflection => 'Desvio'; @override - String get flipBoard => 'Girar o tabuleiro'; + String get puzzleThemeDeflectionDescription => 'Um movimento que desvia a peça do oponente da sua função, por exemplo a de defesa de outra peça ou a defesa de uma casa importante.'; @override - String get threefoldRepetition => 'Tripla repetição'; + String get puzzleThemeDiscoveredAttack => 'Ataque descoberto'; @override - String get claimADraw => 'Reivindicar empate'; + String get puzzleThemeDiscoveredAttackDescription => 'Mover uma peça que anteriormente bloqueava um ataque de uma peça de longo alcance, como por exemplo um cavalo liberando a coluna de uma torre.'; @override - String get offerDraw => 'Propor empate'; + String get puzzleThemeDoubleCheck => 'Xeque duplo'; @override - String get draw => 'Empate'; + String get puzzleThemeDoubleCheckDescription => 'Dar Xeque com duas peças ao mesmo tempo, como resultado de um ataque descoberto onde tanto a peça que se move quanto a peça que estava sendo obstruída atacam o rei do oponente.'; @override - String get drawByMutualAgreement => 'Empate por acordo mútuo'; + String get puzzleThemeEndgame => 'Finais'; @override - String get fiftyMovesWithoutProgress => 'Cinquenta jogadas sem progresso'; + String get puzzleThemeEndgameDescription => 'Tática durante a última fase do jogo.'; @override - String get currentGames => 'Partidas atuais'; + String get puzzleThemeEnPassantDescription => 'Uma tática envolvendo a regra do en passant, onde um peão pode capturar um peão do oponente que passou por ele usando seu movimento inicial de duas casas.'; @override - String get viewInFullSize => 'Ver em tela cheia'; + String get puzzleThemeExposedKing => 'Rei exposto'; @override - String get logOut => 'Sair'; + String get puzzleThemeExposedKingDescription => 'Uma tática que envolve um rei com poucos defensores ao seu redor, muitas vezes levando a xeque-mate.'; @override - String get signIn => 'Entrar'; + String get puzzleThemeFork => 'Garfo (ou duplo)'; @override - String get rememberMe => 'Lembrar de mim'; + String get puzzleThemeForkDescription => 'Um movimento onde a peça movida ataca duas peças de oponente de uma só vez.'; @override - String get youNeedAnAccountToDoThat => 'Você precisa de uma conta para fazer isso'; + String get puzzleThemeHangingPiece => 'Peça pendurada'; @override - String get signUp => 'Registrar'; + String get puzzleThemeHangingPieceDescription => 'Uma táctica que envolve uma peça indefesa do oponente ou insuficientemente defendida e livre para ser capturada.'; @override - String get computersAreNotAllowedToPlay => 'A ajuda de software não é permitida. Por favor, não utilize programas de xadrez, bancos de dados ou o auxilio de outros jogadores durante a partida. Além disso, a criação de múltiplas contas é fortemente desaconselhada e sua prática excessiva acarretará em banimento.'; + String get puzzleThemeHookMate => 'Xeque gancho'; @override - String get games => 'Partidas'; + String get puzzleThemeHookMateDescription => 'Xeque-mate com uma torre, um cavalo e um peão, juntamente com um peão inimigo, para limitar a fuga do rei.'; @override - String get forum => 'Fórum'; + String get puzzleThemeInterference => 'Interferência'; @override - String xPostedInForumY(String param1, String param2) { - return '$param1 publicou no tópico $param2'; - } + String get puzzleThemeInterferenceDescription => 'Mover uma peça entre duas peças do oponente para deixar uma ou duas peças do oponente indefesas, como um cavalo em uma casa defendida por duas torres.'; @override - String get latestForumPosts => 'Últimas publicações no fórum'; + String get puzzleThemeIntermezzo => 'Lance intermediário'; @override - String get players => 'Jogadores'; + String get puzzleThemeIntermezzoDescription => 'Em vez de jogar o movimento esperado, primeiro realiza outro movimento criando uma ameaça imediata a que o oponente deve responder. Também conhecido como \"Zwischenzug\" ou \"In between\".'; @override - String get friends => 'Amigos'; + String get puzzleThemeKnightEndgame => 'Finais de Cavalo'; @override - String get otherPlayers => 'outros jogadores'; + String get puzzleThemeKnightEndgameDescription => 'Um final jogado apenas com cavalos e peões.'; @override - String get discussions => 'Discussões'; + String get puzzleThemeLong => 'Quebra-cabeças longo'; @override - String get today => 'Hoje'; + String get puzzleThemeLongDescription => 'Vitória em três movimentos.'; @override - String get yesterday => 'Ontem'; + String get puzzleThemeMaster => 'Partidas de mestres'; @override - String get minutesPerSide => 'Minutos por jogador'; + String get puzzleThemeMasterDescription => 'Quebra-cabeças de partidas jogadas por jogadores titulados.'; @override - String get variant => 'Variante'; + String get puzzleThemeMasterVsMaster => 'Partidas de Mestre vs Mestre'; @override - String get variants => 'Variantes'; + String get puzzleThemeMasterVsMasterDescription => 'Quebra-cabeças de partidas entre dois jogadores titulados.'; @override - String get timeControl => 'Ritmo'; + String get puzzleThemeMate => 'Xeque-mate'; @override - String get realTime => 'Tempo real'; + String get puzzleThemeMateDescription => 'Vença o jogo com estilo.'; @override - String get correspondence => 'Correspondência'; + String get puzzleThemeMateIn1 => 'Mate em 1'; @override - String get daysPerTurn => 'Dias por lance'; + String get puzzleThemeMateIn1Description => 'Dar xeque-mate em um movimento.'; @override - String get oneDay => 'Um dia'; + String get puzzleThemeMateIn2 => 'Mate em 2'; @override - String get time => 'Tempo'; + String get puzzleThemeMateIn2Description => 'Dar xeque-mate em dois movimentos.'; @override - String get rating => 'Rating'; + String get puzzleThemeMateIn3 => 'Mate em 3'; @override - String get ratingStats => 'Estatísticas de classificação'; + String get puzzleThemeMateIn3Description => 'Dar xeque-mate em três movimentos.'; @override - String get username => 'Nome de usuário'; + String get puzzleThemeMateIn4 => 'Mate em 4'; @override - String get usernameOrEmail => 'Nome ou email do usuário'; + String get puzzleThemeMateIn4Description => 'Dar xeque-mate em 4 movimentos.'; @override - String get changeUsername => 'Alterar nome de usuário'; + String get puzzleThemeMateIn5 => 'Mate em 5 ou mais'; @override - String get changeUsernameNotSame => 'Pode-se apenas trocar as letras de minúscula para maiúscula e vice-versa. Por exemplo, \"fulanodetal\" para \"FulanoDeTal\".'; + String get puzzleThemeMateIn5Description => 'Descubra uma longa sequência de mate.'; @override - String get changeUsernameDescription => 'Altere seu nome de usuário. Isso só pode ser feito uma vez e você poderá apenas trocar as letras de minúscula para maiúscula e vice-versa.'; + String get puzzleThemeMiddlegame => 'Meio-jogo'; @override - String get signupUsernameHint => 'Escolha um nome de usuário apropriado. Não será possível mudá-lo, e qualquer conta que tiver um nome ofensivo ou inapropriado será excluída!'; + String get puzzleThemeMiddlegameDescription => 'Tática durante a segunda fase do jogo.'; @override - String get signupEmailHint => 'Vamos usar apenas para redefinir a sua senha.'; + String get puzzleThemeOneMove => 'Quebra-cabeças de um movimento'; @override - String get password => 'Senha'; + String get puzzleThemeOneMoveDescription => 'Quebra-cabeças de um movimento.'; @override - String get changePassword => 'Alterar senha'; + String get puzzleThemeOpening => 'Abertura'; @override - String get changeEmail => 'Alterar email'; + String get puzzleThemeOpeningDescription => 'Tática durante a primeira fase do jogo.'; @override - String get email => 'E-mail'; + String get puzzleThemePawnEndgame => 'Finais de peões'; @override - String get passwordReset => 'Redefinição de senha'; + String get puzzleThemePawnEndgameDescription => 'Um final apenas com peões.'; @override - String get forgotPassword => 'Esqueceu sua senha?'; + String get puzzleThemePin => 'Cravada'; @override - String get error_weakPassword => 'A senha é extremamente comum e fácil de adivinhar.'; + String get puzzleThemePinDescription => 'Uma tática envolvendo cravada, onde uma peça é incapaz de mover-se sem abrir um descoberto em uma peça de maior valor.'; @override - String get error_namePassword => 'Não utilize seu nome de usuário como senha.'; + String get puzzleThemePromotion => 'Promoção'; @override - String get blankedPassword => 'Você usou a mesma senha em outro site, e esse site foi comprometido. Para garantir a segurança da sua conta no Lichess, você precisa criar uma nova senha. Agradecemos sua compreensão.'; + String get puzzleThemePromotionDescription => 'Promova um peão para uma dama ou a uma peça menor.'; @override - String get youAreLeavingLichess => 'Você está saindo do Lichess'; + String get puzzleThemeQueenEndgame => 'Finais de Dama'; @override - String get neverTypeYourPassword => 'Nunca digite sua senha do Lichess em outro site!'; + String get puzzleThemeQueenEndgameDescription => 'Um final com apenas damas e peões.'; @override - String proceedToX(String param) { - return 'Ir para $param'; - } + String get puzzleThemeQueenRookEndgame => 'Finais de Dama e Torre'; @override - String get passwordSuggestion => 'Não coloque uma senha sugerida por outra pessoa, porque ela poderá roubar sua conta.'; + String get puzzleThemeQueenRookEndgameDescription => 'Finais com apenas Dama, Torre e Peões.'; @override - String get emailSuggestion => 'Não coloque um endereço de email sugerido por outra pessoa, porque ela poderá roubar sua conta.'; + String get puzzleThemeQueensideAttack => 'Ataque na ala da dama'; @override - String get emailConfirmHelp => 'Ajuda com confirmação por e-mail'; + String get puzzleThemeQueensideAttackDescription => 'Um ataque ao rei adversário, após ter efetuado o roque na ala da Dama.'; @override - String get emailConfirmNotReceived => 'Não recebeu seu e-mail de confirmação após o registro?'; + String get puzzleThemeQuietMove => 'Lance de preparação'; @override - String get whatSignupUsername => 'Qual nome de usuário você usou para se registrar?'; + String get puzzleThemeQuietMoveDescription => 'Um lance que não dá xeque nem realiza uma captura, mas prepara uma ameaça inevitável para a jogada seguinte.'; @override - String usernameNotFound(String param) { - return 'Não foi possível encontrar nenhum usuário com este nome: $param.'; - } + String get puzzleThemeRookEndgame => 'Finais de Torres'; @override - String get usernameCanBeUsedForNewAccount => 'Você pode usar esse nome de usuário para criar uma nova conta'; + String get puzzleThemeRookEndgameDescription => 'Um final com apenas torres e peões.'; @override - String emailSent(String param) { - return 'Enviamos um e-mail para $param.'; - } + String get puzzleThemeSacrifice => 'Sacrifício'; @override - String get emailCanTakeSomeTime => 'Pode levar algum tempo para chegar.'; + String get puzzleThemeSacrificeDescription => 'Uma tática envolvendo a entrega de material no curto prazo, com o objetivo de se obter uma vantagem após uma sequência forçada de movimentos.'; @override - String get refreshInboxAfterFiveMinutes => 'Aguarde 5 minutos e atualize sua caixa de entrada.'; + String get puzzleThemeShort => 'Quebra-cabeças curto'; @override - String get checkSpamFolder => 'Verifique também a sua caixa de spam. Caso esteja lá, marque como não é spam.'; + String get puzzleThemeShortDescription => 'Vitória em dois lances.'; @override - String get emailForSignupHelp => 'Se todo o resto falhar, envie-nos este e-mail:'; + String get puzzleThemeSkewer => 'Raio X'; @override - String copyTextToEmail(String param) { - return 'Copie e cole o texto acima e envie-o para $param'; - } + String get puzzleThemeSkewerDescription => 'Um movimento que envolve uma peça de alto valor sendo atacada fugindo do ataque e permitindo que uma peça de menor valor seja capturada ou atacada, o inverso de cravada.'; @override - String get waitForSignupHelp => 'Entraremos em contato em breve para ajudá-lo a completar seu registro.'; + String get puzzleThemeSmotheredMate => 'Mate de Philidor (mate sufocado)'; @override - String accountConfirmed(String param) { - return 'O usuário $param foi confirmado com sucesso.'; - } + String get puzzleThemeSmotheredMateDescription => 'Um xeque-mate dado por um cavalo onde o rei é incapaz de mover-se porque está cercado (ou sufocado) pelas próprias peças.'; @override - String accountCanLogin(String param) { - return 'Você pode acessar agora como $param.'; - } + String get puzzleThemeSuperGM => 'Super partidas de GMs'; @override - String get accountConfirmationEmailNotNeeded => 'Você não precisa de um e-mail de confirmação.'; + String get puzzleThemeSuperGMDescription => 'Quebra-cabeças de partidas jogadas pelos melhores jogadores do mundo.'; @override - String accountClosed(String param) { - return 'A conta $param está encerrada.'; - } + String get puzzleThemeTrappedPiece => 'Peça presa'; @override - String accountRegisteredWithoutEmail(String param) { - return 'A conta $param foi registrada sem um e-mail.'; - } + String get puzzleThemeTrappedPieceDescription => 'Uma peça é incapaz de escapar da captura, pois tem movimentos limitados.'; @override - String get rank => 'Rank'; + String get puzzleThemeUnderPromotion => 'Subpromoção'; @override - String rankX(String param) { - return 'Classificação: $param'; - } + String get puzzleThemeUnderPromotionDescription => 'Promover para cavalo, bispo ou torre.'; @override - String get gamesPlayed => 'Partidas realizadas'; + String get puzzleThemeVeryLong => 'Quebra-cabeças muito longo'; @override - String get cancel => 'Cancelar'; + String get puzzleThemeVeryLongDescription => 'Quatro movimentos ou mais para vencer.'; @override - String get whiteTimeOut => 'Tempo das brancas esgotado'; + String get puzzleThemeXRayAttack => 'Ataque em raio X'; @override - String get blackTimeOut => 'Tempo das pretas esgotado'; + String get puzzleThemeXRayAttackDescription => 'Uma peça ataca ou defende uma casa indiretamente, através de uma peça adversária.'; @override - String get drawOfferSent => 'Proposta de empate enviada'; + String get puzzleThemeZugzwang => 'Zugzwang'; @override - String get drawOfferAccepted => 'Proposta de empate aceita'; + String get puzzleThemeZugzwangDescription => 'O adversário tem os seus movimentos limitados, e qualquer movimento que ele faça vai enfraquecer sua própria posição.'; @override - String get drawOfferCanceled => 'Proposta de empate cancelada'; + String get puzzleThemeMix => 'Combinação saudável'; @override - String get whiteOffersDraw => 'Brancas oferecem empate'; + String get puzzleThemeMixDescription => 'Um pouco de tudo. Você nunca sabe o que vai encontrar, então esteja pronto para tudo! Igualzinho aos jogos em tabuleiros reais.'; @override - String get blackOffersDraw => 'Pretas oferecem empate'; + String get puzzleThemePlayerGames => 'Partidas de jogadores'; @override - String get whiteDeclinesDraw => 'Brancas recusam empate'; + String get puzzleThemePlayerGamesDescription => 'Procure quebra-cabeças gerados a partir de suas partidas ou das de outro jogador.'; @override - String get blackDeclinesDraw => 'Pretas recusam empate'; + String puzzleThemePuzzleDownloadInformation(String param) { + return 'Esses quebra-cabeças estão em domínio público, e você pode baixá-los em $param.'; + } @override - String get yourOpponentOffersADraw => 'Seu adversário oferece empate'; + String get searchSearch => 'Buscar'; @override - String get accept => 'Aceitar'; + String get settingsSettings => 'Configurações'; @override - String get decline => 'Recusar'; + String get settingsCloseAccount => 'Encerrar conta'; @override - String get playingRightNow => 'Jogando agora'; + String get settingsManagedAccountCannotBeClosed => 'Sua conta é gerenciada, e não pode ser encerrada.'; @override - String get eventInProgress => 'Jogando agora'; + String get settingsClosingIsDefinitive => 'O encerramento é definitivo. Não há como desfazer. Tem certeza?'; @override - String get finished => 'Terminado'; + String get settingsCantOpenSimilarAccount => 'Você não poderá abrir uma nova conta com o mesmo nome, mesmo que alterne entre maiúsculas e minúsculas.'; @override - String get abortGame => 'Cancelar partida'; + String get settingsChangedMindDoNotCloseAccount => 'Eu mudei de ideia, não encerre minha conta'; @override - String get gameAborted => 'Partida cancelada'; + String get settingsCloseAccountExplanation => 'Tem certeza de que deseja encerrar sua conta? Encerrar sua conta é uma decisão permanente. Você NUNCA MAIS será capaz de entrar com ela novamente.'; @override - String get standard => 'Padrão'; + String get settingsThisAccountIsClosed => 'Esta conta foi encerrada.'; @override - String get customPosition => 'Posição personalizada'; + String get playWithAFriend => 'Jogar contra um amigo'; @override - String get unlimited => 'Ilimitado'; + String get playWithTheMachine => 'Jogar contra o computador'; @override - String get mode => 'Modo'; + String get toInviteSomeoneToPlayGiveThisUrl => 'Para convidar alguém para jogar, envie este URL'; @override - String get casual => 'Amistosa'; + String get gameOver => 'Fim da partida'; @override - String get rated => 'Ranqueada'; + String get waitingForOpponent => 'Aguardando oponente'; @override - String get casualTournament => 'Amistoso'; + String get orLetYourOpponentScanQrCode => 'Ou deixe seu oponente ler este QR Code'; @override - String get ratedTournament => 'Valendo pontos'; + String get waiting => 'Aguardando'; @override - String get thisGameIsRated => 'Esta partida vale pontos'; + String get yourTurn => 'Sua vez'; @override - String get rematch => 'Revanche'; + String aiNameLevelAiLevel(String param1, String param2) { + return '$param1 nível $param2'; + } @override - String get rematchOfferSent => 'Oferta de revanche enviada'; + String get level => 'Nível'; @override - String get rematchOfferAccepted => 'Oferta de revanche aceita'; + String get strength => 'Nível'; @override - String get rematchOfferCanceled => 'Oferta de revanche cancelada'; + String get toggleTheChat => 'Ativar/Desativar chat'; @override - String get rematchOfferDeclined => 'Oferta de revanche recusada'; + String get chat => 'Chat'; @override - String get cancelRematchOffer => 'Cancelar oferta de revanche'; + String get resign => 'Desistir'; @override - String get viewRematch => 'Ver revanche'; + String get checkmate => 'Xeque-mate'; @override - String get confirmMove => 'Confirmar lance'; + String get stalemate => 'Rei afogado'; @override - String get play => 'Jogar'; + String get white => 'Brancas'; @override - String get inbox => 'Mensagens'; + String get black => 'Pretas'; @override - String get chatRoom => 'Sala de chat'; + String get asWhite => 'de brancas'; @override - String get loginToChat => 'Faça login para conversar'; + String get asBlack => 'de pretas'; @override - String get youHaveBeenTimedOut => 'Sua sessão expirou.'; + String get randomColor => 'Cor aleatória'; @override - String get spectatorRoom => 'Sala do espectador'; + String get createAGame => 'Criar uma partida'; @override - String get composeMessage => 'Escrever mensagem'; + String get whiteIsVictorious => 'Brancas vencem'; @override - String get subject => 'Assunto'; + String get blackIsVictorious => 'Pretas vencem'; @override - String get send => 'Enviar'; + String get youPlayTheWhitePieces => 'Você joga com as peças brancas'; @override - String get incrementInSeconds => 'Acréscimo em segundos'; + String get youPlayTheBlackPieces => 'Você joga com as peças pretas'; @override - String get freeOnlineChess => 'Xadrez Online Gratuito'; + String get itsYourTurn => 'É a sua vez!'; @override - String get exportGames => 'Exportar partidas'; + String get cheatDetected => 'Trapaça Detectada'; @override - String get ratingRange => 'Rating entre'; + String get kingInTheCenter => 'Rei no centro'; @override - String get thisAccountViolatedTos => 'Esta conta violou os Termos de Serviço do Lichess'; + String get threeChecks => 'Três xeques'; @override - String get openingExplorerAndTablebase => 'Explorador de abertura & tabela de finais'; + String get raceFinished => 'Corrida terminada'; @override - String get takeback => 'Voltar jogada'; + String get variantEnding => 'Fim da variante'; @override - String get proposeATakeback => 'Propor voltar jogada'; + String get newOpponent => 'Novo oponente'; @override - String get takebackPropositionSent => 'Proposta de voltar jogada enviada'; + String get yourOpponentWantsToPlayANewGameWithYou => 'Seu oponente quer jogar uma nova partida contra você'; @override - String get takebackPropositionDeclined => 'Proposta de voltar jogada recusada'; + String get joinTheGame => 'Entrar no jogo'; @override - String get takebackPropositionAccepted => 'Proposta de voltar jogada aceita'; + String get whitePlays => 'Brancas jogam'; @override - String get takebackPropositionCanceled => 'Proposta de voltar jogada cancelada'; + String get blackPlays => 'Pretas jogam'; @override - String get yourOpponentProposesATakeback => 'Seu oponente propõe voltar jogada'; + String get opponentLeftChoices => 'O seu oponente deixou a partida. Você pode reivindicar vitória, declarar empate ou aguardar.'; @override - String get bookmarkThisGame => 'Adicionar esta partida às favoritas'; + String get forceResignation => 'Reivindicar vitória'; @override - String get tournament => 'Torneio'; + String get forceDraw => 'Reivindicar empate'; @override - String get tournaments => 'Torneios'; + String get talkInChat => 'Por favor, seja gentil no chat!'; @override - String get tournamentPoints => 'Pontos de torneios'; + String get theFirstPersonToComeOnThisUrlWillPlayWithYou => 'A primeira pessoa que acessar esta URL jogará contigo.'; @override - String get viewTournament => 'Ver torneio'; + String get whiteResigned => 'Brancas desistiram'; @override - String get backToTournament => 'Voltar ao torneio'; + String get blackResigned => 'Pretas desistiram'; @override - String get noDrawBeforeSwissLimit => 'Não é possível empatar antes de 30 lances em um torneio suíço.'; + String get whiteLeftTheGame => 'Brancas deixaram a partida'; @override - String get thematic => 'Temático'; + String get blackLeftTheGame => 'Pretas deixaram a partida'; @override - String yourPerfRatingIsProvisional(String param) { - return 'Seu rating $param é provisório'; - } + String get whiteDidntMove => 'As brancas não se moveram'; @override - String yourPerfRatingIsTooHigh(String param1, String param2) { - return 'Seu $param1 rating ($param2) é muito alta'; - } + String get blackDidntMove => 'As pretas não se moveram'; @override - String yourTopWeeklyPerfRatingIsTooHigh(String param1, String param2) { - return 'Seu melhor rating $param1 da semana ($param2) é muito alto'; - } + String get requestAComputerAnalysis => 'Solicitar uma análise do computador'; @override - String yourPerfRatingIsTooLow(String param1, String param2) { - return 'Sua $param1 pontuação ($param2) é muito baixa'; - } + String get computerAnalysis => 'Análise do computador'; @override - String ratedMoreThanInPerf(String param1, String param2) { - return 'Pontuação ≥ $param1 em $param2'; - } + String get computerAnalysisAvailable => 'Análise de computador disponível'; @override - String ratedLessThanInPerf(String param1, String param2) { - return 'Pontuação ≤ $param1 em $param2'; - } + String get computerAnalysisDisabled => 'Análise de computador desativada'; @override - String mustBeInTeam(String param) { - return 'Precisa estar na equipe $param'; - } + String get analysis => 'Análise'; @override - String youAreNotInTeam(String param) { - return 'Você não está na equipe $param'; + String depthX(String param) { + return 'Profundidade $param'; } @override - String get backToGame => 'Retorne à partida'; + String get usingServerAnalysis => 'Análise de servidor em uso'; @override - String get siteDescription => 'Xadrez online gratuito. Jogue xadrez agora numa interface simples. Sem registro, sem anúncios, sem plugins. Jogue xadrez contra computador, amigos ou adversários aleatórios.'; + String get loadingEngine => 'Carregando ...'; @override - String xJoinedTeamY(String param1, String param2) { - return '$param1 juntou-se à equipe $param2'; - } + String get calculatingMoves => 'Calculando jogadas...'; @override - String xCreatedTeamY(String param1, String param2) { - return '$param1 criou a equipe $param2'; - } + String get engineFailed => 'Erro ao carregar o engine'; @override - String get startedStreaming => 'começou uma transmissão ao vivo'; + String get cloudAnalysis => 'Análise na nuvem'; @override - String xStartedStreaming(String param) { - return '$param começou a transmitir'; - } + String get goDeeper => 'Detalhar'; @override - String get averageElo => 'Média de rating'; + String get showThreat => 'Mostrar ameaça'; @override - String get location => 'Localização'; + String get inLocalBrowser => 'no navegador local'; @override - String get filterGames => 'Filtrar partidas'; + String get toggleLocalEvaluation => 'Ativar/Desativar análise local'; @override - String get reset => 'Reiniciar'; + String get promoteVariation => 'Promover variante'; @override - String get apply => 'Aplicar'; + String get makeMainLine => 'Transformar em linha principal'; @override - String get save => 'Salvar'; + String get deleteFromHere => 'Excluir a partir daqui'; @override - String get leaderboard => 'Classificação'; + String get collapseVariations => 'Esconder variantes'; @override - String get screenshotCurrentPosition => 'Captura de tela da posição atual'; + String get expandVariations => 'Mostrar variantes'; @override - String get gameAsGIF => 'Salvar a partida como GIF'; + String get forceVariation => 'Variante forçada'; @override - String get pasteTheFenStringHere => 'Cole a notação FEN aqui'; + String get copyVariationPgn => 'Copiar PGN da variante'; @override - String get pasteThePgnStringHere => 'Cole a notação PGN aqui'; + String get move => 'Movimentos'; @override - String get orUploadPgnFile => 'Ou carregue um arquivo PGN'; + String get variantLoss => 'Derrota da variante'; @override - String get fromPosition => 'A partir da posição'; + String get variantWin => 'Vitória da variante'; @override - String get continueFromHere => 'Continuar daqui'; + String get insufficientMaterial => 'Material insuficiente'; @override - String get toStudy => 'Estudo'; + String get pawnMove => 'Movimento de peão'; @override - String get importGame => 'Importar partida'; + String get capture => 'Captura'; @override - String get importGameExplanation => 'Após colar uma partida em PGN você poderá revisá-la interativamente, consultar uma análise de computador, utilizar o chat e compartilhar um link.'; + String get close => 'Fechar'; @override - String get importGameCaveat => 'As variantes serão apagadas. Para salvá-las, importe o PGN em um estudo.'; + String get winning => 'Vencendo'; @override - String get importGameDataPrivacyWarning => 'Este PGN pode ser acessado publicamente. Use um estudo para importar um jogo privado.'; + String get losing => 'Perdendo'; @override - String get thisIsAChessCaptcha => 'Este é um CAPTCHA enxadrístico.'; + String get drawn => 'Empate'; @override - String get clickOnTheBoardToMakeYourMove => 'Clique no tabuleiro para fazer seu lance, provando que é humano.'; + String get unknown => 'Posição desconhecida'; @override - String get captcha_fail => 'Por favor, resolva o captcha enxadrístico.'; + String get database => 'Banco de Dados'; @override - String get notACheckmate => 'Não é xeque-mate'; + String get whiteDrawBlack => 'Brancas / Empate / Pretas'; @override - String get whiteCheckmatesInOneMove => 'As brancas dão mate em um lance'; + String averageRatingX(String param) { + return 'Classificação média: $param'; + } @override - String get blackCheckmatesInOneMove => 'As pretas dão mate em um lance'; + String get recentGames => 'Partidas recentes'; @override - String get retry => 'Tentar novamente'; + String get topGames => 'Melhores partidas'; @override - String get reconnecting => 'Reconectando'; + String masterDbExplanation(String param1, String param2, String param3) { + return 'Duas milhões de partidas de jogadores com pontuação FIDE acima de $param1, desde $param2 a $param3'; + } @override - String get noNetwork => 'Sem conexão'; + String get dtzWithRounding => 'DTZ50\" com arredondamento, baseado no número de meias-jogadas até a próxima captura ou jogada de peão'; @override - String get favoriteOpponents => 'Adversários favoritos'; + String get noGameFound => 'Nenhuma partida encontrada'; @override - String get follow => 'Seguir'; + String get maxDepthReached => 'Profundidade máxima alcançada!'; @override - String get following => 'Seguindo'; + String get maybeIncludeMoreGamesFromThePreferencesMenu => 'Talvez você queira incluir mais jogos a partir do menu de preferências'; @override - String get unfollow => 'Parar de seguir'; + String get openings => 'Aberturas'; @override - String followX(String param) { - return 'Seguir $param'; - } + String get openingExplorer => 'Explorador de aberturas'; @override - String unfollowX(String param) { - return 'Deixar de seguir $param'; - } + String get openingEndgameExplorer => 'Explorador de Aberturas/Finais'; @override - String get block => 'Bloquear'; + String xOpeningExplorer(String param) { + return '$param Explorador de aberturas'; + } @override - String get blocked => 'Bloqueado'; + String get playFirstOpeningEndgameExplorerMove => 'Jogue o primeiro lance do explorador de aberturas/finais'; @override - String get unblock => 'Desbloquear'; + String get winPreventedBy50MoveRule => 'Vitória impedida pela regra dos 50 movimentos'; @override - String get followsYou => 'Segue você'; + String get lossSavedBy50MoveRule => 'Derrota impedida pela regra dos 50 movimentos'; @override - String xStartedFollowingY(String param1, String param2) { - return '$param1 começou a seguir $param2'; - } + String get winOr50MovesByPriorMistake => 'Vitória ou 50 movimentos por erro anterior'; @override - String get more => 'Mais'; + String get lossOr50MovesByPriorMistake => 'Derrota ou 50 movimentos por erro anterior'; @override - String get memberSince => 'Membro desde'; + String get unknownDueToRounding => 'Vitória/derrota garantida somente se a variante recomendada tiver sido seguida desde o último movimento de captura ou de peão, devido ao possível arredondamento.'; @override - String lastSeenActive(String param) { - return 'Ativo $param'; - } + String get allSet => 'Tudo pronto!'; @override - String get player => 'Jogador'; + String get importPgn => 'Importar PGN'; @override - String get list => 'Lista'; + String get delete => 'Excluir'; @override - String get graph => 'Gráfico'; + String get deleteThisImportedGame => 'Excluir este jogo importado?'; @override - String get required => 'Obrigatório.'; + String get replayMode => 'Rever a partida'; @override - String get openTournaments => 'Torneios abertos'; + String get realtimeReplay => 'Tempo Real'; @override - String get duration => 'Duração'; + String get byCPL => 'Por erros'; @override - String get winner => 'Vencedor'; + String get openStudy => 'Abrir estudo'; @override - String get standing => 'Colocação'; + String get enable => 'Ativar'; @override - String get createANewTournament => 'Criar novo torneio'; + String get bestMoveArrow => 'Seta de melhor movimento'; @override - String get tournamentCalendar => 'Calendário do torneio'; + String get showVariationArrows => 'Mostrar setas das variantes'; @override - String get conditionOfEntry => 'Condições de participação:'; + String get evaluationGauge => 'Escala de avaliação'; @override - String get advancedSettings => 'Configurações avançadas'; + String get multipleLines => 'Linhas de análise'; @override - String get safeTournamentName => 'Escolha um nome seguro para o torneio.'; + String get cpus => 'CPUs'; @override - String get inappropriateNameWarning => 'Até mesmo a menor indecência poderia ensejar o encerramento de sua conta.'; + String get memory => 'Memória'; @override - String get emptyTournamentName => 'Deixe em branco para dar ao torneio o nome de um grande mestre aleatório.'; + String get infiniteAnalysis => 'Análise infinita'; @override - String get makePrivateTournament => 'Faça o torneio privado e restrinja o acesso com uma senha'; + String get removesTheDepthLimit => 'Remove o limite de profundidade, o que aquece seu computador'; @override - String get join => 'Entrar'; + String get blunder => 'Capivarada'; @override - String get withdraw => 'Sair'; + String get mistake => 'Erro'; @override - String get points => 'Pontos'; + String get inaccuracy => 'Imprecisão'; @override - String get wins => 'Vitórias'; + String get moveTimes => 'Tempo por movimento'; @override - String get losses => 'Derrotas'; + String get flipBoard => 'Girar o tabuleiro'; @override - String get createdBy => 'Criado por'; + String get threefoldRepetition => 'Tripla repetição'; @override - String get tournamentIsStarting => 'O torneio está começando'; + String get claimADraw => 'Reivindicar empate'; @override - String get tournamentPairingsAreNowClosed => 'Os pareamentos do torneio estão fechados agora.'; + String get offerDraw => 'Propor empate'; @override - String standByX(String param) { - return '$param, aguarde: o pareamento está em andamento, prepare-se!'; - } + String get draw => 'Empate'; @override - String get pause => 'Pausar'; + String get drawByMutualAgreement => 'Empate por acordo mútuo'; @override - String get resume => 'Continuar'; + String get fiftyMovesWithoutProgress => 'Cinquenta jogadas sem progresso'; @override - String get youArePlaying => 'Você está participando!'; + String get currentGames => 'Partidas atuais'; @override - String get winRate => 'Taxa de vitórias'; + String get viewInFullSize => 'Ver em tela cheia'; @override - String get berserkRate => 'Taxa Berserk'; + String get logOut => 'Sair'; @override - String get performance => 'Desempenho'; + String get signIn => 'Entrar'; @override - String get tournamentComplete => 'Torneio completo'; + String get rememberMe => 'Lembrar de mim'; @override - String get movesPlayed => 'Movimentos realizados'; + String get youNeedAnAccountToDoThat => 'Você precisa de uma conta para fazer isso'; @override - String get whiteWins => 'Brancas venceram'; + String get signUp => 'Registrar'; @override - String get blackWins => 'Pretas venceram'; + String get computersAreNotAllowedToPlay => 'A ajuda de software não é permitida. Por favor, não utilize programas de xadrez, bancos de dados ou o auxilio de outros jogadores durante a partida. Além disso, a criação de múltiplas contas é fortemente desaconselhada e sua prática excessiva acarretará em banimento.'; @override - String get drawRate => 'Taxa de empates'; + String get games => 'Partidas'; @override - String get draws => 'Empates'; + String get forum => 'Fórum'; @override - String nextXTournament(String param) { - return 'Próximo torneio $param:'; + String xPostedInForumY(String param1, String param2) { + return '$param1 publicou no tópico $param2'; } @override - String get averageOpponent => 'Pontuação média adversários'; + String get latestForumPosts => 'Últimas publicações no fórum'; @override - String get boardEditor => 'Editor de tabuleiro'; + String get players => 'Jogadores'; @override - String get setTheBoard => 'Defina a posição'; + String get friends => 'Amigos'; @override - String get popularOpenings => 'Aberturas populares'; + String get otherPlayers => 'outros jogadores'; @override - String get endgamePositions => 'Posições de final'; + String get discussions => 'Discussões'; @override - String chess960StartPosition(String param) { - return 'Posição inicial do Xadrez960: $param'; - } + String get today => 'Hoje'; @override - String get startPosition => 'Posição inicial'; + String get yesterday => 'Ontem'; @override - String get clearBoard => 'Limpar tabuleiro'; + String get minutesPerSide => 'Minutos por jogador'; @override - String get loadPosition => 'Carregar posição'; + String get variant => 'Variante'; @override - String get isPrivate => 'Privado'; + String get variants => 'Variantes'; @override - String reportXToModerators(String param) { - return 'Reportar $param aos moderadores'; - } + String get timeControl => 'Ritmo'; @override - String profileCompletion(String param) { - return 'Conclusão do perfil: $param'; - } + String get realTime => 'Tempo real'; @override - String xRating(String param) { - return 'Rating $param'; - } + String get correspondence => 'Correspondência'; @override - String get ifNoneLeaveEmpty => 'Se nenhuma, deixe vazio'; + String get daysPerTurn => 'Dias por lance'; @override - String get profile => 'Perfil'; + String get oneDay => 'Um dia'; @override - String get editProfile => 'Editar perfil'; + String get time => 'Tempo'; @override - String get realName => 'Nome real'; + String get rating => 'Rating'; @override - String get setFlair => 'Escolha seu emote'; + String get ratingStats => 'Estatísticas de classificação'; @override - String get flair => 'Estilo'; + String get username => 'Nome de usuário'; @override - String get youCanHideFlair => 'Você pode esconder todos os emotes de usuário no site.'; + String get usernameOrEmail => 'Nome ou email do usuário'; @override - String get biography => 'Biografia'; + String get changeUsername => 'Alterar nome de usuário'; @override - String get countryRegion => 'País ou região'; + String get changeUsernameNotSame => 'Pode-se apenas trocar as letras de minúscula para maiúscula e vice-versa. Por exemplo, \"fulanodetal\" para \"FulanoDeTal\".'; @override - String get thankYou => 'Obrigado!'; + String get changeUsernameDescription => 'Altere seu nome de usuário. Isso só pode ser feito uma vez e você poderá apenas trocar as letras de minúscula para maiúscula e vice-versa.'; @override - String get socialMediaLinks => 'Links de mídia social'; + String get signupUsernameHint => 'Escolha um nome de usuário apropriado. Não será possível mudá-lo, e qualquer conta que tiver um nome ofensivo ou inapropriado será excluída!'; @override - String get oneUrlPerLine => 'Uma URL por linha.'; + String get signupEmailHint => 'Vamos usar apenas para redefinir a sua senha.'; @override - String get inlineNotation => 'Notação em linha'; + String get password => 'Senha'; @override - String get makeAStudy => 'Para salvar e compartilhar uma análise, crie um estudo.'; + String get changePassword => 'Alterar senha'; @override - String get clearSavedMoves => 'Limpar lances'; + String get changeEmail => 'Alterar email'; @override - String get previouslyOnLichessTV => 'Anteriormente em Lichess TV'; + String get email => 'E-mail'; @override - String get onlinePlayers => 'Jogadores online'; + String get passwordReset => 'Redefinição de senha'; @override - String get activePlayers => 'Jogadores ativos'; + String get forgotPassword => 'Esqueceu sua senha?'; @override - String get bewareTheGameIsRatedButHasNoClock => 'Cuidado, o jogo vale rating, mas não há controle de tempo!'; + String get error_weakPassword => 'A senha é extremamente comum e fácil de adivinhar.'; @override - String get success => 'Sucesso'; + String get error_namePassword => 'Não utilize seu nome de usuário como senha.'; @override - String get automaticallyProceedToNextGameAfterMoving => 'Passar automaticamente ao jogo seguinte após o lance'; + String get blankedPassword => 'Você usou a mesma senha em outro site, e esse site foi comprometido. Para garantir a segurança da sua conta no Lichess, você precisa criar uma nova senha. Agradecemos sua compreensão.'; @override - String get autoSwitch => 'Alternar automaticamente'; + String get youAreLeavingLichess => 'Você está saindo do Lichess'; @override - String get puzzles => 'Quebra-cabeças'; + String get neverTypeYourPassword => 'Nunca digite sua senha do Lichess em outro site!'; @override - String get onlineBots => 'Bots online'; + String proceedToX(String param) { + return 'Ir para $param'; + } @override - String get name => 'Nome'; + String get passwordSuggestion => 'Não coloque uma senha sugerida por outra pessoa, porque ela poderá roubar sua conta.'; @override - String get description => 'Descrição'; + String get emailSuggestion => 'Não coloque um endereço de email sugerido por outra pessoa, porque ela poderá roubar sua conta.'; @override - String get descPrivate => 'Descrição privada'; + String get emailConfirmHelp => 'Ajuda com confirmação por e-mail'; @override - String get descPrivateHelp => 'Texto que apenas os membros da equipe verão. Se definido, substitui a descrição pública para os membros da equipe.'; + String get emailConfirmNotReceived => 'Não recebeu seu e-mail de confirmação após o registro?'; @override - String get no => 'Não'; + String get whatSignupUsername => 'Qual nome de usuário você usou para se registrar?'; @override - String get yes => 'Sim'; + String usernameNotFound(String param) { + return 'Não foi possível encontrar nenhum usuário com este nome: $param.'; + } @override - String get website => 'Site'; + String get usernameCanBeUsedForNewAccount => 'Você pode usar esse nome de usuário para criar uma nova conta'; @override - String get mobile => 'Celular'; + String emailSent(String param) { + return 'Enviamos um e-mail para $param.'; + } @override - String get help => 'Ajuda:'; + String get emailCanTakeSomeTime => 'Pode levar algum tempo para chegar.'; @override - String get createANewTopic => 'Criar novo tópico'; + String get refreshInboxAfterFiveMinutes => 'Aguarde 5 minutos e atualize sua caixa de entrada.'; @override - String get topics => 'Tópicos'; + String get checkSpamFolder => 'Verifique também a sua caixa de spam. Caso esteja lá, marque como não é spam.'; @override - String get posts => 'Publicações'; + String get emailForSignupHelp => 'Se todo o resto falhar, envie-nos este e-mail:'; @override - String get lastPost => 'Última postagem'; + String copyTextToEmail(String param) { + return 'Copie e cole o texto acima e envie-o para $param'; + } @override - String get views => 'Visualizações'; + String get waitForSignupHelp => 'Entraremos em contato em breve para ajudá-lo a completar seu registro.'; @override - String get replies => 'Respostas'; + String accountConfirmed(String param) { + return 'O usuário $param foi confirmado com sucesso.'; + } @override - String get replyToThisTopic => 'Responder a este tópico'; + String accountCanLogin(String param) { + return 'Você pode acessar agora como $param.'; + } @override - String get reply => 'Responder'; + String get accountConfirmationEmailNotNeeded => 'Você não precisa de um e-mail de confirmação.'; @override - String get message => 'Mensagem'; + String accountClosed(String param) { + return 'A conta $param está encerrada.'; + } @override - String get createTheTopic => 'Criar tópico'; + String accountRegisteredWithoutEmail(String param) { + return 'A conta $param foi registrada sem um e-mail.'; + } @override - String get reportAUser => 'Reportar um usuário'; + String get rank => 'Rank'; @override - String get user => 'Usuário'; + String rankX(String param) { + return 'Classificação: $param'; + } @override - String get reason => 'Motivo'; + String get gamesPlayed => 'Partidas realizadas'; @override - String get whatIsIheMatter => 'Qual é o motivo?'; + String get ok => 'OK'; @override - String get cheat => 'Trapaça'; + String get cancel => 'Cancelar'; @override - String get troll => 'Troll'; + String get whiteTimeOut => 'Tempo das brancas esgotado'; @override - String get other => 'Outro'; + String get blackTimeOut => 'Tempo das pretas esgotado'; @override - String get reportDescriptionHelp => 'Cole o link do(s) jogo(s) e explique o que há de errado com o comportamento do usuário. Não diga apenas \"ele trapaceia\", informe-nos como chegou a esta conclusão. Sua denúncia será processada mais rapidamente se escrita em inglês.'; + String get drawOfferSent => 'Proposta de empate enviada'; @override - String get error_provideOneCheatedGameLink => 'Por favor forneça ao menos um link para um jogo com suspeita de trapaça.'; + String get drawOfferAccepted => 'Proposta de empate aceita'; @override - String by(String param) { - return 'por $param'; - } + String get drawOfferCanceled => 'Proposta de empate cancelada'; @override - String importedByX(String param) { - return 'Importado por $param'; - } + String get whiteOffersDraw => 'Brancas oferecem empate'; @override - String get thisTopicIsNowClosed => 'O tópico foi fechado.'; + String get blackOffersDraw => 'Pretas oferecem empate'; @override - String get blog => 'Blog'; + String get whiteDeclinesDraw => 'Brancas recusam empate'; @override - String get notes => 'Notas'; + String get blackDeclinesDraw => 'Pretas recusam empate'; @override - String get typePrivateNotesHere => 'Digite notas pessoais aqui'; + String get yourOpponentOffersADraw => 'Seu adversário oferece empate'; @override - String get writeAPrivateNoteAboutThisUser => 'Escreva uma nota pessoal sobre este usuário'; + String get accept => 'Aceitar'; @override - String get noNoteYet => 'Nenhuma nota'; + String get decline => 'Recusar'; @override - String get invalidUsernameOrPassword => 'Nome de usuário ou senha incorretos'; + String get playingRightNow => 'Jogando agora'; @override - String get incorrectPassword => 'Senha incorreta'; + String get eventInProgress => 'Jogando agora'; @override - String get invalidAuthenticationCode => 'Código de verificação inválido'; + String get finished => 'Terminado'; @override - String get emailMeALink => 'Me envie um link'; + String get abortGame => 'Cancelar partida'; @override - String get currentPassword => 'Senha atual'; + String get gameAborted => 'Partida cancelada'; @override - String get newPassword => 'Nova senha'; + String get standard => 'Padrão'; @override - String get newPasswordAgain => 'Nova senha (novamente)'; + String get customPosition => 'Posição personalizada'; @override - String get newPasswordsDontMatch => 'As novas senhas não correspondem'; + String get unlimited => 'Ilimitado'; @override - String get newPasswordStrength => 'Senha forte'; + String get mode => 'Modo'; @override - String get clockInitialTime => 'Tempo de relógio'; + String get casual => 'Amistosa'; @override - String get clockIncrement => 'Incremento do relógio'; + String get rated => 'Ranqueada'; @override - String get privacy => 'Privacidade'; + String get casualTournament => 'Amistoso'; @override - String get privacyPolicy => 'Política de privacidade'; + String get ratedTournament => 'Valendo pontos'; @override - String get letOtherPlayersFollowYou => 'Permitir que outros jogadores sigam você'; + String get thisGameIsRated => 'Esta partida vale pontos'; @override - String get letOtherPlayersChallengeYou => 'Permitir que outros jogadores desafiem você'; + String get rematch => 'Revanche'; @override - String get letOtherPlayersInviteYouToStudy => 'Deixe outros jogadores convidá-lo para um estudo'; + String get rematchOfferSent => 'Oferta de revanche enviada'; @override - String get sound => 'Som'; + String get rematchOfferAccepted => 'Oferta de revanche aceita'; @override - String get none => 'Nenhum'; + String get rematchOfferCanceled => 'Oferta de revanche cancelada'; @override - String get fast => 'Rápido'; + String get rematchOfferDeclined => 'Oferta de revanche recusada'; @override - String get normal => 'Normal'; + String get cancelRematchOffer => 'Cancelar oferta de revanche'; @override - String get slow => 'Lento'; + String get viewRematch => 'Ver revanche'; @override - String get insideTheBoard => 'Dentro do tabuleiro'; + String get confirmMove => 'Confirmar lance'; @override - String get outsideTheBoard => 'Fora do tabuleiro'; + String get play => 'Jogar'; @override - String get allSquaresOfTheBoard => 'Todas as casas do tabuleiro'; + String get inbox => 'Mensagens'; @override - String get onSlowGames => 'Em partidas lentas'; + String get chatRoom => 'Sala de chat'; @override - String get always => 'Sempre'; + String get loginToChat => 'Faça login para conversar'; @override - String get never => 'Nunca'; + String get youHaveBeenTimedOut => 'Sua sessão expirou.'; @override - String xCompetesInY(String param1, String param2) { - return '$param1 compete em $param2'; - } + String get spectatorRoom => 'Sala do espectador'; @override - String get victory => 'Vitória'; + String get composeMessage => 'Escrever mensagem'; @override - String get defeat => 'Derrota'; + String get subject => 'Assunto'; @override - String victoryVsYInZ(String param1, String param2, String param3) { - return '$param1 vs $param2 em $param3'; - } + String get send => 'Enviar'; @override - String defeatVsYInZ(String param1, String param2, String param3) { - return '$param1 vs $param2 em $param3'; - } + String get incrementInSeconds => 'Acréscimo em segundos'; @override - String drawVsYInZ(String param1, String param2, String param3) { - return '$param1 vs $param2 em $param3'; - } + String get freeOnlineChess => 'Xadrez Online Gratuito'; @override - String get timeline => 'Linha do tempo'; + String get exportGames => 'Exportar partidas'; @override - String get starting => 'Iniciando:'; + String get ratingRange => 'Rating entre'; @override - String get allInformationIsPublicAndOptional => 'Todas as informações são públicas e opcionais.'; + String get thisAccountViolatedTos => 'Esta conta violou os Termos de Serviço do Lichess'; @override - String get biographyDescription => 'Fale sobre você, seus interesses, o que você gosta no xadrez, suas aberturas favoritas, jogadores...'; + String get openingExplorerAndTablebase => 'Explorador de abertura & tabela de finais'; @override - String get listBlockedPlayers => 'Sua lista de jogadores bloqueados'; + String get takeback => 'Voltar jogada'; @override - String get human => 'Humano'; + String get proposeATakeback => 'Propor voltar jogada'; @override - String get computer => 'Computador'; + String get takebackPropositionSent => 'Proposta de voltar jogada enviada'; @override - String get side => 'Cor'; + String get takebackPropositionDeclined => 'Proposta de voltar jogada recusada'; @override - String get clock => 'Relógio'; + String get takebackPropositionAccepted => 'Proposta de voltar jogada aceita'; @override - String get opponent => 'Adversário'; + String get takebackPropositionCanceled => 'Proposta de voltar jogada cancelada'; @override - String get learnMenu => 'Aprender'; + String get yourOpponentProposesATakeback => 'Seu oponente propõe voltar jogada'; @override - String get studyMenu => 'Estudar'; + String get bookmarkThisGame => 'Adicionar esta partida às favoritas'; @override - String get practice => 'Praticar'; + String get tournament => 'Torneio'; @override - String get community => 'Comunidade'; + String get tournaments => 'Torneios'; @override - String get tools => 'Ferramentas'; + String get tournamentPoints => 'Pontos de torneios'; @override - String get increment => 'Incremento'; + String get viewTournament => 'Ver torneio'; @override - String get error_unknown => 'Valor inválido'; + String get backToTournament => 'Voltar ao torneio'; @override - String get error_required => 'Este campo deve ser preenchido'; + String get noDrawBeforeSwissLimit => 'Não é possível empatar antes de 30 lances em um torneio suíço.'; @override - String get error_email => 'Este endereço de e-mail é inválido'; + String get thematic => 'Temático'; @override - String get error_email_acceptable => 'Este endereço de e-mail não é válido. Verifique e tente novamente.'; + String yourPerfRatingIsProvisional(String param) { + return 'Seu rating $param é provisório'; + } @override - String get error_email_unique => 'Endereço de e-mail é inválido ou já está sendo utilizado'; + String yourPerfRatingIsTooHigh(String param1, String param2) { + return 'Seu $param1 rating ($param2) é muito alta'; + } @override - String get error_email_different => 'Este já é o seu endereço de e-mail'; + String yourTopWeeklyPerfRatingIsTooHigh(String param1, String param2) { + return 'Seu melhor rating $param1 da semana ($param2) é muito alto'; + } @override - String error_minLength(String param) { - return 'O mínimo de caracteres é $param'; + String yourPerfRatingIsTooLow(String param1, String param2) { + return 'Sua $param1 pontuação ($param2) é muito baixa'; } @override - String error_maxLength(String param) { - return 'O máximo de caracteres é $param'; + String ratedMoreThanInPerf(String param1, String param2) { + return 'Pontuação ≥ $param1 em $param2'; } @override - String error_min(String param) { - return 'Deve ser maior ou igual a $param'; + String ratedLessThanInPerf(String param1, String param2) { + return 'Pontuação ≤ $param1 em $param2'; } @override - String error_max(String param) { - return 'Deve ser menor ou igual a $param'; + String mustBeInTeam(String param) { + return 'Precisa estar na equipe $param'; } @override - String ifRatingIsPlusMinusX(String param) { - return 'Se o rating for ± $param'; + String youAreNotInTeam(String param) { + return 'Você não está na equipe $param'; } @override - String get ifRegistered => 'Se registrado'; + String get backToGame => 'Retorne à partida'; @override - String get onlyExistingConversations => 'Apenas conversas iniciadas'; + String get siteDescription => 'Xadrez online gratuito. Jogue xadrez agora numa interface simples. Sem registro, sem anúncios, sem plugins. Jogue xadrez contra computador, amigos ou adversários aleatórios.'; @override - String get onlyFriends => 'Apenas amigos'; + String xJoinedTeamY(String param1, String param2) { + return '$param1 juntou-se à equipe $param2'; + } @override - String get menu => 'Menu'; + String xCreatedTeamY(String param1, String param2) { + return '$param1 criou a equipe $param2'; + } @override - String get castling => 'Roque'; + String get startedStreaming => 'começou uma transmissão ao vivo'; @override - String get whiteCastlingKingside => 'O-O das brancas'; + String xStartedStreaming(String param) { + return '$param começou a transmitir'; + } @override - String get blackCastlingKingside => 'O-O das pretas'; + String get averageElo => 'Média de rating'; @override - String tpTimeSpentPlaying(String param) { - return 'Tempo jogando: $param'; - } + String get location => 'Localização'; @override - String get watchGames => 'Assistir partidas'; + String get filterGames => 'Filtrar partidas'; @override - String tpTimeSpentOnTV(String param) { - return 'Tempo na TV: $param'; - } + String get reset => 'Reiniciar'; @override - String get watch => 'Assistir'; + String get apply => 'Aplicar'; @override - String get videoLibrary => 'Vídeos'; + String get save => 'Salvar'; @override - String get streamersMenu => 'Streamers'; + String get leaderboard => 'Classificação'; @override - String get mobileApp => 'Aplicativo Móvel'; + String get screenshotCurrentPosition => 'Captura de tela da posição atual'; @override - String get webmasters => 'Webmasters'; + String get gameAsGIF => 'Salvar a partida como GIF'; @override - String get about => 'Sobre'; + String get pasteTheFenStringHere => 'Cole a notação FEN aqui'; @override - String aboutX(String param) { - return 'Sobre o $param'; - } + String get pasteThePgnStringHere => 'Cole a notação PGN aqui'; @override - String xIsAFreeYLibreOpenSourceChessServer(String param1, String param2) { - return '$param1 é um servidor de xadrez gratuito ($param2), livre, sem anúncios e código aberto.'; - } + String get orUploadPgnFile => 'Ou carregue um arquivo PGN'; @override - String get really => 'realmente'; + String get fromPosition => 'A partir da posição'; @override - String get contribute => 'Contribuir'; + String get continueFromHere => 'Continuar daqui'; @override - String get termsOfService => 'Termos de serviço'; + String get toStudy => 'Estudo'; @override - String get sourceCode => 'Código-fonte'; + String get importGame => 'Importar partida'; @override - String get simultaneousExhibitions => 'Exibição simultânea'; + String get importGameExplanation => 'Após colar uma partida em PGN você poderá revisá-la interativamente, consultar uma análise de computador, utilizar o chat e compartilhar um link.'; @override - String get host => 'Simultanista'; + String get importGameCaveat => 'As variantes serão apagadas. Para salvá-las, importe o PGN em um estudo.'; @override - String hostColorX(String param) { - return 'Cor do simultanista: $param'; - } + String get importGameDataPrivacyWarning => 'Este PGN pode ser acessado publicamente. Use um estudo para importar um jogo privado.'; @override - String get yourPendingSimuls => 'Suas simultâneas pendentes'; + String get thisIsAChessCaptcha => 'Este é um CAPTCHA enxadrístico.'; @override - String get createdSimuls => 'Simultâneas criadas recentemente'; + String get clickOnTheBoardToMakeYourMove => 'Clique no tabuleiro para fazer seu lance, provando que é humano.'; @override - String get hostANewSimul => 'Iniciar nova simultânea'; + String get captcha_fail => 'Por favor, resolva o captcha enxadrístico.'; @override - String get signUpToHostOrJoinASimul => 'Entre em uma ou crie uma conta para hospedar'; + String get notACheckmate => 'Não é xeque-mate'; @override - String get noSimulFound => 'Simultânea não encontrada'; + String get whiteCheckmatesInOneMove => 'As brancas dão mate em um lance'; @override - String get noSimulExplanation => 'Esta exibição simultânea não existe.'; + String get blackCheckmatesInOneMove => 'As pretas dão mate em um lance'; @override - String get returnToSimulHomepage => 'Retornar à página inicial da simultânea'; + String get retry => 'Tentar novamente'; @override - String get aboutSimul => 'A simultânea envolve um único jogador contra vários oponentes ao mesmo tempo.'; + String get reconnecting => 'Reconectando'; @override - String get aboutSimulImage => 'Contra 50 oponentes, Fischer ganhou 47 jogos, empatou 2 e perdeu 1.'; + String get noNetwork => 'Sem conexão'; @override - String get aboutSimulRealLife => 'O conceito provém de eventos reais, nos quais o simultanista se move de mesa em mesa, executando um movimento por vez.'; + String get favoriteOpponents => 'Adversários favoritos'; @override - String get aboutSimulRules => 'Quando a simultânea começa, cada jogador começa sua partida contra o simultanista, o qual sempre tem as brancas. A simultânea termina quando todas as partidas são finalizadas.'; + String get follow => 'Seguir'; @override - String get aboutSimulSettings => 'As simultâneas sempre são partidas amigáveis. Revanches, voltar jogadas e tempo adicional estão desativados.'; + String get following => 'Seguindo'; @override - String get create => 'Criar'; + String get unfollow => 'Parar de seguir'; @override - String get whenCreateSimul => 'Quando cria uma simultânea, você joga com vários adversários ao mesmo tempo.'; + String followX(String param) { + return 'Seguir $param'; + } @override - String get simulVariantsHint => 'Se você selecionar diversas variantes, cada jogador poderá escolher qual delas jogar.'; + String unfollowX(String param) { + return 'Deixar de seguir $param'; + } @override - String get simulClockHint => 'Configuração de acréscimos no relógio. Quanto mais jogadores admitir, mais tempo pode necessitar.'; + String get block => 'Bloquear'; @override - String get simulAddExtraTime => 'Você pode acrescentar tempo adicional a seu relógio, para ajudá-lo a lidar com a simultânea.'; + String get blocked => 'Bloqueado'; @override - String get simulHostExtraTime => 'Tempo adicional do simultanista'; + String get unblock => 'Desbloquear'; @override - String get simulAddExtraTimePerPlayer => 'Adicionar tempo inicial ao seu relógio por cada jogador adversário que entrar na simultânea.'; + String get followsYou => 'Segue você'; @override - String get simulHostExtraTimePerPlayer => 'Tempo adicional do simultanista por jogador'; + String xStartedFollowingY(String param1, String param2) { + return '$param1 começou a seguir $param2'; + } @override - String get lichessTournaments => 'Torneios do Lichess'; + String get more => 'Mais'; @override - String get tournamentFAQ => 'Perguntas Frequentes sobre torneios no estilo Arena'; + String get memberSince => 'Membro desde'; @override - String get timeBeforeTournamentStarts => 'Contagem regressiva para início do torneio'; + String lastSeenActive(String param) { + return 'Ativo $param'; + } @override - String get averageCentipawnLoss => 'Perda média em centipeões'; + String get player => 'Jogador'; @override - String get accuracy => 'Precisão'; + String get list => 'Lista'; @override - String get keyboardShortcuts => 'Atalhos de teclado'; + String get graph => 'Gráfico'; @override - String get keyMoveBackwardOrForward => 'retroceder/avançar lance'; + String get required => 'Obrigatório.'; @override - String get keyGoToStartOrEnd => 'ir para início/fim'; + String get openTournaments => 'Torneios abertos'; @override - String get keyCycleSelectedVariation => 'Alternar entre as variantes'; + String get duration => 'Duração'; @override - String get keyShowOrHideComments => 'mostrar/ocultar comentários'; + String get winner => 'Vencedor'; @override - String get keyEnterOrExitVariation => 'entrar/sair da variante'; + String get standing => 'Colocação'; @override - String get keyRequestComputerAnalysis => 'Solicite análise do computador, aprenda com seus erros'; + String get createANewTournament => 'Criar novo torneio'; @override - String get keyNextLearnFromYourMistakes => 'Próximo (Aprenda com seus erros)'; + String get tournamentCalendar => 'Calendário do torneio'; @override - String get keyNextBlunder => 'Próximo erro grave'; + String get conditionOfEntry => 'Condições de participação:'; @override - String get keyNextMistake => 'Próximo erro'; + String get advancedSettings => 'Configurações avançadas'; @override - String get keyNextInaccuracy => 'Próxima imprecisão'; + String get safeTournamentName => 'Escolha um nome seguro para o torneio.'; @override - String get keyPreviousBranch => 'Branch anterior'; + String get inappropriateNameWarning => 'Até mesmo a menor indecência poderia ensejar o encerramento de sua conta.'; @override - String get keyNextBranch => 'Próximo branch'; + String get emptyTournamentName => 'Deixe em branco para dar ao torneio o nome de um grande mestre aleatório.'; @override - String get toggleVariationArrows => 'Ativar/desativar setas'; + String get makePrivateTournament => 'Faça o torneio privado e restrinja o acesso com uma senha'; @override - String get cyclePreviousOrNextVariation => 'Variante seguinte/anterior'; + String get join => 'Entrar'; @override - String get toggleGlyphAnnotations => 'Ativar/desativar anotações'; + String get withdraw => 'Sair'; @override - String get togglePositionAnnotations => 'Ativar/desativar anotações de posição'; + String get points => 'Pontos'; @override - String get variationArrowsInfo => 'Setas de variação permitem navegar sem usar a lista de movimentos.'; + String get wins => 'Vitórias'; @override - String get playSelectedMove => 'jogar movimento selecionado'; + String get losses => 'Derrotas'; @override - String get newTournament => 'Novo torneio'; + String get createdBy => 'Criado por'; @override - String get tournamentHomeTitle => 'Torneios de xadrez com diversos controles de tempo e variantes'; + String get tournamentIsStarting => 'O torneio está começando'; @override - String get tournamentHomeDescription => 'Jogue xadrez em ritmo acelerado! Entre em um torneio oficial agendado ou crie seu próprio. Bullet, Blitz, Clássico, Chess960, King of the Hill, Três Xeques e outras modalidades disponíveis para uma ilimitada diversão enxadrística.'; + String get tournamentPairingsAreNowClosed => 'Os pareamentos do torneio estão fechados agora.'; @override - String get tournamentNotFound => 'Torneio não encontrado'; + String standByX(String param) { + return '$param, aguarde: o pareamento está em andamento, prepare-se!'; + } @override - String get tournamentDoesNotExist => 'Este torneio não existe.'; + String get pause => 'Pausar'; @override - String get tournamentMayHaveBeenCanceled => 'O evento pode ter sido cancelado, se todos os jogadores saíram antes de seu início.'; + String get resume => 'Continuar'; @override - String get returnToTournamentsHomepage => 'Volte à página inicial de torneios'; + String get youArePlaying => 'Você está participando!'; @override - String weeklyPerfTypeRatingDistribution(String param) { - return 'Distribuição mensal de rating em $param'; - } + String get winRate => 'Taxa de vitórias'; @override - String yourPerfTypeRatingIsRating(String param1, String param2) { - return 'Seu rating em $param1 é $param2.'; - } + String get berserkRate => 'Taxa Berserk'; @override - String youAreBetterThanPercentOfPerfTypePlayers(String param1, String param2) { - return 'Você é melhor que $param1 dos jogadores de $param2.'; - } + String get performance => 'Desempenho'; @override - String userIsBetterThanPercentOfPerfTypePlayers(String param1, String param2, String param3) { - return '$param1 é melhor que $param2 dos $param3 jogadores.'; - } + String get tournamentComplete => 'Torneio completo'; @override - String betterThanPercentPlayers(String param1, String param2) { - return 'Melhor que $param1 dos jogadores de $param2'; - } + String get movesPlayed => 'Movimentos realizados'; @override - String youDoNotHaveAnEstablishedPerfTypeRating(String param) { - return 'Você não tem rating definido em $param.'; - } + String get whiteWins => 'Brancas venceram'; @override - String get yourRating => 'Seu rating'; + String get blackWins => 'Pretas venceram'; @override - String get cumulative => 'Cumulativo'; + String get drawRate => 'Taxa de empates'; @override - String get glicko2Rating => 'Rating Glicko-2'; + String get draws => 'Empates'; @override - String get checkYourEmail => 'Verifique seu e-mail'; + String nextXTournament(String param) { + return 'Próximo torneio $param:'; + } @override - String get weHaveSentYouAnEmailClickTheLink => 'Enviamos um e-mail. Clique no link do e-mail para ativar sua conta.'; + String get averageOpponent => 'Pontuação média adversários'; @override - String get ifYouDoNotSeeTheEmailCheckOtherPlaces => 'Se você não vir o e-mail, verifique outros locais onde possa estar, como lixeira, spam ou outras pastas.'; + String get boardEditor => 'Editor de tabuleiro'; @override - String weHaveSentYouAnEmailTo(String param) { - return 'Enviamos um e-mail para $param. Clique no link do e-mail para redefinir sua senha.'; - } + String get setTheBoard => 'Defina a posição'; @override - String byRegisteringYouAgreeToBeBoundByOur(String param) { - return 'Ao registrar, você concorda em se comprometer com nossa $param.'; - } + String get popularOpenings => 'Aberturas populares'; @override - String readAboutOur(String param) { - return 'Leia sobre a nossa $param.'; + String get endgamePositions => 'Posições de final'; + + @override + String chess960StartPosition(String param) { + return 'Posição inicial do Xadrez960: $param'; } @override - String get networkLagBetweenYouAndLichess => 'Atraso na rede'; + String get startPosition => 'Posição inicial'; @override - String get timeToProcessAMoveOnLichessServer => 'Tempo para processar um movimento no servidor do Lichess'; + String get clearBoard => 'Limpar tabuleiro'; @override - String get downloadAnnotated => 'Baixar anotação'; + String get loadPosition => 'Carregar posição'; @override - String get downloadRaw => 'Baixar texto'; + String get isPrivate => 'Privado'; @override - String get downloadImported => 'Baixar partida importada'; + String reportXToModerators(String param) { + return 'Reportar $param aos moderadores'; + } @override - String get crosstable => 'Tabela'; + String profileCompletion(String param) { + return 'Conclusão do perfil: $param'; + } @override - String get youCanAlsoScrollOverTheBoardToMoveInTheGame => 'Você também pode rolar sobre o tabuleiro para percorrer as jogadas.'; + String xRating(String param) { + return 'Rating $param'; + } @override - String get scrollOverComputerVariationsToPreviewThem => 'Passe o mouse pelas variações do computador para visualizá-las.'; + String get ifNoneLeaveEmpty => 'Se nenhuma, deixe vazio'; @override - String get analysisShapesHowTo => 'Pressione Shift+Clique ou clique com o botão direito do mouse para desenhar círculos e setas no tabuleiro.'; + String get profile => 'Perfil'; @override - String get letOtherPlayersMessageYou => 'Permitir que outros jogadores lhe enviem mensagem'; + String get editProfile => 'Editar perfil'; @override - String get receiveForumNotifications => 'Receba notificações quando você for mencionado no fórum'; + String get realName => 'Nome real'; @override - String get shareYourInsightsData => 'Compartilhe seus dados da análise'; + String get setFlair => 'Escolha seu emote'; @override - String get withNobody => 'Com ninguém'; + String get flair => 'Estilo'; @override - String get withFriends => 'Com amigos'; + String get youCanHideFlair => 'Você pode esconder todos os emotes de usuário no site.'; @override - String get withEverybody => 'Com todos'; + String get biography => 'Biografia'; @override - String get kidMode => 'Modo infantil'; + String get countryRegion => 'País ou região'; @override - String get kidModeIsEnabled => 'O modo infantil está ativado.'; + String get thankYou => 'Obrigado!'; @override - String get kidModeExplanation => 'Isto diz respeito à segurança. No modo infantil, todas as comunicações do site são desabilitadas. Habilite isso para seus filhos e alunos, para protegê-los de outros usuários da Internet.'; + String get socialMediaLinks => 'Links de mídia social'; @override - String inKidModeTheLichessLogoGetsIconX(String param) { - return 'No modo infantil, a logo do lichess tem um ícone $param, para que você saiba que suas crianças estão seguras.'; - } + String get oneUrlPerLine => 'Uma URL por linha.'; @override - String get askYourChessTeacherAboutLiftingKidMode => 'Sua conta é gerenciada. Para desativar o modo infantil, peça ao seu professor.'; + String get inlineNotation => 'Notação em linha'; @override - String get enableKidMode => 'Habilitar o modo infantil'; + String get makeAStudy => 'Para salvar e compartilhar uma análise, crie um estudo.'; @override - String get disableKidMode => 'Desabilitar o modo infantil'; + String get clearSavedMoves => 'Limpar lances'; @override - String get security => 'Segurança'; + String get previouslyOnLichessTV => 'Anteriormente em Lichess TV'; @override - String get sessions => 'Sessões'; + String get onlinePlayers => 'Jogadores online'; @override - String get revokeAllSessions => 'revogar todas as sessões'; + String get activePlayers => 'Jogadores ativos'; @override - String get playChessEverywhere => 'Jogue xadrez em qualquer lugar'; + String get bewareTheGameIsRatedButHasNoClock => 'Cuidado, o jogo vale rating, mas não há controle de tempo!'; @override - String get asFreeAsLichess => 'Tão gratuito quanto o Lichess'; + String get success => 'Sucesso'; @override - String get builtForTheLoveOfChessNotMoney => 'Desenvolvido pelo amor ao xadrez, não pelo dinheiro'; + String get automaticallyProceedToNextGameAfterMoving => 'Passar automaticamente ao jogo seguinte após o lance'; @override - String get everybodyGetsAllFeaturesForFree => 'Todos têm todos os recursos de graça'; + String get autoSwitch => 'Alternar automaticamente'; @override - String get zeroAdvertisement => 'Zero anúncios'; + String get puzzles => 'Quebra-cabeças'; @override - String get fullFeatured => 'Cheio de recursos'; + String get onlineBots => 'Bots online'; @override - String get phoneAndTablet => 'Celular e tablet'; + String get name => 'Nome'; @override - String get bulletBlitzClassical => 'Bullet, blitz, clássico'; + String get description => 'Descrição'; @override - String get correspondenceChess => 'Xadrez por correspondência'; + String get descPrivate => 'Descrição privada'; @override - String get onlineAndOfflinePlay => 'Jogue online e offline'; + String get descPrivateHelp => 'Texto que apenas os membros da equipe verão. Se definido, substitui a descrição pública para os membros da equipe.'; @override - String get viewTheSolution => 'Ver solução'; + String get no => 'Não'; @override - String get followAndChallengeFriends => 'Siga e desafie amigos'; + String get yes => 'Sim'; @override - String get gameAnalysis => 'Análise da partida'; + String get website => 'Site'; @override - String xHostsY(String param1, String param2) { - return '$param1 criou $param2'; - } + String get mobile => 'Celular'; @override - String xJoinsY(String param1, String param2) { - return '$param1 entrou em $param2'; - } + String get help => 'Ajuda:'; @override - String xLikesY(String param1, String param2) { - return '$param1 gostou de $param2'; - } + String get createANewTopic => 'Criar novo tópico'; @override - String get quickPairing => 'Pareamento rápido'; + String get topics => 'Tópicos'; @override - String get lobby => 'Salão'; + String get posts => 'Publicações'; @override - String get anonymous => 'Anônimo'; + String get lastPost => 'Última postagem'; @override - String yourScore(String param) { - return 'Sua pontuação:$param'; - } + String get views => 'Visualizações'; @override - String get language => 'Idioma'; + String get replies => 'Respostas'; @override - String get background => 'Cor tema'; + String get replyToThisTopic => 'Responder a este tópico'; @override - String get light => 'Claro'; + String get reply => 'Responder'; @override - String get dark => 'Escuro'; + String get message => 'Mensagem'; @override - String get transparent => 'Transparente'; + String get createTheTopic => 'Criar tópico'; @override - String get deviceTheme => 'Tema do dispositivo'; + String get reportAUser => 'Reportar um usuário'; @override - String get backgroundImageUrl => 'URL da imagem de fundo:'; + String get user => 'Usuário'; @override - String get board => 'Tabuleiro'; + String get reason => 'Motivo'; @override - String get size => 'Tamanho'; + String get whatIsIheMatter => 'Qual é o motivo?'; @override - String get opacity => 'Opacidade'; + String get cheat => 'Trapaça'; @override - String get brightness => 'Brilho'; + String get troll => 'Troll'; @override - String get hue => 'Tom'; + String get other => 'Outro'; @override - String get boardReset => 'Restaurar as cores padrão'; + String get reportCheatBoostHelp => 'Cole o link do(s) jogo(s) e explique o que há de errado com o comportamento do usuário. Não diga apenas \"ele trapaceia\", informe-nos como chegou a esta conclusão.'; @override - String get pieceSet => 'Estilo das peças'; + String get reportUsernameHelp => 'Explique porque este nome de usuário é ofensivo. Não diga apenas \"é ofensivo/inapropriado\", mas nos diga como chegou a essa conclusão especialmente se o insulto for ofuscado, não estiver em inglês, for uma gíria, ou for uma referência histórica/cultural.'; @override - String get embedInYourWebsite => 'Incorporar no seu site'; + String get reportProcessedFasterInEnglish => 'A sua denúncia será processada mais rápido se for escrita em inglês.'; @override - String get usernameAlreadyUsed => 'Este nome de usuário já está registado, por favor, escolha outro.'; + String get error_provideOneCheatedGameLink => 'Por favor forneça ao menos um link para um jogo com suspeita de trapaça.'; @override - String get usernamePrefixInvalid => 'O nome de usuário deve começar com uma letra.'; + String by(String param) { + return 'por $param'; + } @override - String get usernameSuffixInvalid => 'O nome de usuário deve terminar com uma letra ou um número.'; + String importedByX(String param) { + return 'Importado por $param'; + } @override - String get usernameCharsInvalid => 'Nomes de usuário só podem conter letras, números, sublinhados e hifens.'; + String get thisTopicIsNowClosed => 'O tópico foi fechado.'; @override - String get usernameUnacceptable => 'Este nome de usuário não é aceitável.'; + String get blog => 'Blog'; @override - String get playChessInStyle => 'Jogue xadrez com estilo'; + String get notes => 'Notas'; @override - String get chessBasics => 'Básicos do xadrez'; + String get typePrivateNotesHere => 'Digite notas pessoais aqui'; @override - String get coaches => 'Treinadores'; + String get writeAPrivateNoteAboutThisUser => 'Escreva uma nota pessoal sobre este usuário'; @override - String get invalidPgn => 'PGN inválido'; + String get noNoteYet => 'Nenhuma nota'; @override - String get invalidFen => 'FEN inválido'; + String get invalidUsernameOrPassword => 'Nome de usuário ou senha incorretos'; @override - String get custom => 'Personalizado'; + String get incorrectPassword => 'Senha incorreta'; @override - String get notifications => 'Notificações'; + String get invalidAuthenticationCode => 'Código de verificação inválido'; @override - String notificationsX(String param1) { - return 'Notificações: $param1'; - } + String get emailMeALink => 'Me envie um link'; @override - String perfRatingX(String param) { - return 'Rating: $param'; - } + String get currentPassword => 'Senha atual'; @override - String get practiceWithComputer => 'Pratique com o computador'; + String get newPassword => 'Nova senha'; @override - String anotherWasX(String param) { - return 'Um outro lance seria $param'; - } + String get newPasswordAgain => 'Nova senha (novamente)'; @override - String bestWasX(String param) { - return 'Melhor seria $param'; - } + String get newPasswordsDontMatch => 'As novas senhas não correspondem'; @override - String get youBrowsedAway => 'Você navegou para longe'; + String get newPasswordStrength => 'Senha forte'; @override - String get resumePractice => 'Retornar à prática'; + String get clockInitialTime => 'Tempo de relógio'; @override - String get drawByFiftyMoves => 'O jogo empatou pela regra dos cinquenta movimentos.'; + String get clockIncrement => 'Incremento do relógio'; @override - String get theGameIsADraw => 'A partida terminou em empate.'; + String get privacy => 'Privacidade'; @override - String get computerThinking => 'Computador pensando ...'; + String get privacyPolicy => 'Política de privacidade'; @override - String get seeBestMove => 'Veja o melhor lance'; + String get letOtherPlayersFollowYou => 'Permitir que outros jogadores sigam você'; @override - String get hideBestMove => 'Esconder o melhor lance'; + String get letOtherPlayersChallengeYou => 'Permitir que outros jogadores desafiem você'; @override - String get getAHint => 'Obter uma dica'; + String get letOtherPlayersInviteYouToStudy => 'Deixe outros jogadores convidá-lo para um estudo'; @override - String get evaluatingYourMove => 'Avaliando o seu movimento ...'; + String get sound => 'Som'; @override - String get whiteWinsGame => 'Brancas vencem'; + String get none => 'Nenhum'; @override - String get blackWinsGame => 'Pretas vencem'; + String get fast => 'Rápido'; @override - String get learnFromYourMistakes => 'Aprenda com seus erros'; + String get normal => 'Normal'; @override - String get learnFromThisMistake => 'Aprenda com este erro'; + String get slow => 'Lento'; @override - String get skipThisMove => 'Pular esse lance'; + String get insideTheBoard => 'Dentro do tabuleiro'; @override - String get next => 'Próximo'; + String get outsideTheBoard => 'Fora do tabuleiro'; @override - String xWasPlayed(String param) { - return '$param foi jogado'; - } + String get allSquaresOfTheBoard => 'Todas as casas do tabuleiro'; @override - String get findBetterMoveForWhite => 'Encontrar o melhor lance para as Brancas'; + String get onSlowGames => 'Em partidas lentas'; @override - String get findBetterMoveForBlack => 'Encontre o melhor lance para as Pretas'; + String get always => 'Sempre'; @override - String get resumeLearning => 'Continuar a aprendizagem'; + String get never => 'Nunca'; @override - String get youCanDoBetter => 'Você pode fazer melhor'; + String xCompetesInY(String param1, String param2) { + return '$param1 compete em $param2'; + } @override - String get tryAnotherMoveForWhite => 'Tente um outro lance para as Brancas'; + String get victory => 'Vitória'; @override - String get tryAnotherMoveForBlack => 'Tente um outro lance para as Pretas'; + String get defeat => 'Derrota'; @override - String get solution => 'Solução'; + String victoryVsYInZ(String param1, String param2, String param3) { + return '$param1 vs $param2 em $param3'; + } @override - String get waitingForAnalysis => 'Aguardando análise'; + String defeatVsYInZ(String param1, String param2, String param3) { + return '$param1 vs $param2 em $param3'; + } @override - String get noMistakesFoundForWhite => 'Nenhum erro encontrado para as Brancas'; + String drawVsYInZ(String param1, String param2, String param3) { + return '$param1 vs $param2 em $param3'; + } + + @override + String get timeline => 'Linha do tempo'; + + @override + String get starting => 'Iniciando:'; + + @override + String get allInformationIsPublicAndOptional => 'Todas as informações são públicas e opcionais.'; + + @override + String get biographyDescription => 'Fale sobre você, seus interesses, o que você gosta no xadrez, suas aberturas favoritas, jogadores...'; + + @override + String get listBlockedPlayers => 'Sua lista de jogadores bloqueados'; + + @override + String get human => 'Humano'; + + @override + String get computer => 'Computador'; + + @override + String get side => 'Cor'; + + @override + String get clock => 'Relógio'; + + @override + String get opponent => 'Adversário'; + + @override + String get learnMenu => 'Aprender'; + + @override + String get studyMenu => 'Estudar'; + + @override + String get practice => 'Praticar'; + + @override + String get community => 'Comunidade'; + + @override + String get tools => 'Ferramentas'; + + @override + String get increment => 'Incremento'; + + @override + String get error_unknown => 'Valor inválido'; + + @override + String get error_required => 'Este campo deve ser preenchido'; + + @override + String get error_email => 'Este endereço de e-mail é inválido'; + + @override + String get error_email_acceptable => 'Este endereço de e-mail não é válido. Verifique e tente novamente.'; + + @override + String get error_email_unique => 'Endereço de e-mail é inválido ou já está sendo utilizado'; + + @override + String get error_email_different => 'Este já é o seu endereço de e-mail'; + + @override + String error_minLength(String param) { + return 'O mínimo de caracteres é $param'; + } + + @override + String error_maxLength(String param) { + return 'O máximo de caracteres é $param'; + } + + @override + String error_min(String param) { + return 'Deve ser maior ou igual a $param'; + } + + @override + String error_max(String param) { + return 'Deve ser menor ou igual a $param'; + } + + @override + String ifRatingIsPlusMinusX(String param) { + return 'Se o rating for ± $param'; + } + + @override + String get ifRegistered => 'Se registrado'; + + @override + String get onlyExistingConversations => 'Apenas conversas iniciadas'; + + @override + String get onlyFriends => 'Apenas amigos'; + + @override + String get menu => 'Menu'; + + @override + String get castling => 'Roque'; + + @override + String get whiteCastlingKingside => 'O-O das brancas'; + + @override + String get blackCastlingKingside => 'O-O das pretas'; + + @override + String tpTimeSpentPlaying(String param) { + return 'Tempo jogando: $param'; + } + + @override + String get watchGames => 'Assistir partidas'; + + @override + String tpTimeSpentOnTV(String param) { + return 'Tempo na TV: $param'; + } + + @override + String get watch => 'Assistir'; + + @override + String get videoLibrary => 'Vídeos'; + + @override + String get streamersMenu => 'Streamers'; + + @override + String get mobileApp => 'Aplicativo Móvel'; + + @override + String get webmasters => 'Webmasters'; + + @override + String get about => 'Sobre'; + + @override + String aboutX(String param) { + return 'Sobre o $param'; + } + + @override + String xIsAFreeYLibreOpenSourceChessServer(String param1, String param2) { + return '$param1 é um servidor de xadrez gratuito ($param2), livre, sem anúncios e código aberto.'; + } + + @override + String get really => 'realmente'; + + @override + String get contribute => 'Contribuir'; + + @override + String get termsOfService => 'Termos de serviço'; + + @override + String get sourceCode => 'Código-fonte'; + + @override + String get simultaneousExhibitions => 'Exibição simultânea'; + + @override + String get host => 'Simultanista'; + + @override + String hostColorX(String param) { + return 'Cor do simultanista: $param'; + } + + @override + String get yourPendingSimuls => 'Suas simultâneas pendentes'; + + @override + String get createdSimuls => 'Simultâneas criadas recentemente'; + + @override + String get hostANewSimul => 'Iniciar nova simultânea'; + + @override + String get signUpToHostOrJoinASimul => 'Entre em uma ou crie uma conta para hospedar'; + + @override + String get noSimulFound => 'Simultânea não encontrada'; + + @override + String get noSimulExplanation => 'Esta exibição simultânea não existe.'; + + @override + String get returnToSimulHomepage => 'Retornar à página inicial da simultânea'; + + @override + String get aboutSimul => 'A simultânea envolve um único jogador contra vários oponentes ao mesmo tempo.'; + + @override + String get aboutSimulImage => 'Contra 50 oponentes, Fischer ganhou 47 jogos, empatou 2 e perdeu 1.'; + + @override + String get aboutSimulRealLife => 'O conceito provém de eventos reais, nos quais o simultanista se move de mesa em mesa, executando um movimento por vez.'; + + @override + String get aboutSimulRules => 'Quando a simultânea começa, cada jogador começa sua partida contra o simultanista, o qual sempre tem as brancas. A simultânea termina quando todas as partidas são finalizadas.'; + + @override + String get aboutSimulSettings => 'As simultâneas sempre são partidas amigáveis. Revanches, voltar jogadas e tempo adicional estão desativados.'; + + @override + String get create => 'Criar'; + + @override + String get whenCreateSimul => 'Quando cria uma simultânea, você joga com vários adversários ao mesmo tempo.'; + + @override + String get simulVariantsHint => 'Se você selecionar diversas variantes, cada jogador poderá escolher qual delas jogar.'; + + @override + String get simulClockHint => 'Configuração de acréscimos no relógio. Quanto mais jogadores admitir, mais tempo pode necessitar.'; + + @override + String get simulAddExtraTime => 'Você pode acrescentar tempo adicional a seu relógio, para ajudá-lo a lidar com a simultânea.'; + + @override + String get simulHostExtraTime => 'Tempo adicional do simultanista'; + + @override + String get simulAddExtraTimePerPlayer => 'Adicionar tempo inicial ao seu relógio por cada jogador adversário que entrar na simultânea.'; + + @override + String get simulHostExtraTimePerPlayer => 'Tempo adicional do simultanista por jogador'; + + @override + String get lichessTournaments => 'Torneios do Lichess'; + + @override + String get tournamentFAQ => 'Perguntas Frequentes sobre torneios no estilo Arena'; + + @override + String get timeBeforeTournamentStarts => 'Contagem regressiva para início do torneio'; + + @override + String get averageCentipawnLoss => 'Perda média em centipeões'; + + @override + String get accuracy => 'Precisão'; + + @override + String get keyboardShortcuts => 'Atalhos de teclado'; + + @override + String get keyMoveBackwardOrForward => 'retroceder/avançar lance'; + + @override + String get keyGoToStartOrEnd => 'ir para início/fim'; + + @override + String get keyCycleSelectedVariation => 'Alternar entre as variantes'; + + @override + String get keyShowOrHideComments => 'mostrar/ocultar comentários'; + + @override + String get keyEnterOrExitVariation => 'entrar/sair da variante'; + + @override + String get keyRequestComputerAnalysis => 'Solicite análise do computador, aprenda com seus erros'; + + @override + String get keyNextLearnFromYourMistakes => 'Próximo (Aprenda com seus erros)'; + + @override + String get keyNextBlunder => 'Próximo erro grave'; + + @override + String get keyNextMistake => 'Próximo erro'; + + @override + String get keyNextInaccuracy => 'Próxima imprecisão'; + + @override + String get keyPreviousBranch => 'Branch anterior'; + + @override + String get keyNextBranch => 'Próximo branch'; + + @override + String get toggleVariationArrows => 'Ativar/desativar setas'; + + @override + String get cyclePreviousOrNextVariation => 'Variante seguinte/anterior'; + + @override + String get toggleGlyphAnnotations => 'Ativar/desativar anotações'; + + @override + String get togglePositionAnnotations => 'Ativar/desativar anotações de posição'; + + @override + String get variationArrowsInfo => 'Setas de variação permitem navegar sem usar a lista de movimentos.'; + + @override + String get playSelectedMove => 'jogar movimento selecionado'; + + @override + String get newTournament => 'Novo torneio'; + + @override + String get tournamentHomeTitle => 'Torneios de xadrez com diversos controles de tempo e variantes'; + + @override + String get tournamentHomeDescription => 'Jogue xadrez em ritmo acelerado! Entre em um torneio oficial agendado ou crie seu próprio. Bullet, Blitz, Clássico, Chess960, King of the Hill, Três Xeques e outras modalidades disponíveis para uma ilimitada diversão enxadrística.'; + + @override + String get tournamentNotFound => 'Torneio não encontrado'; + + @override + String get tournamentDoesNotExist => 'Este torneio não existe.'; + + @override + String get tournamentMayHaveBeenCanceled => 'O evento pode ter sido cancelado, se todos os jogadores saíram antes de seu início.'; + + @override + String get returnToTournamentsHomepage => 'Volte à página inicial de torneios'; + + @override + String weeklyPerfTypeRatingDistribution(String param) { + return 'Distribuição mensal de rating em $param'; + } + + @override + String yourPerfTypeRatingIsRating(String param1, String param2) { + return 'Seu rating em $param1 é $param2.'; + } + + @override + String youAreBetterThanPercentOfPerfTypePlayers(String param1, String param2) { + return 'Você é melhor que $param1 dos jogadores de $param2.'; + } + + @override + String userIsBetterThanPercentOfPerfTypePlayers(String param1, String param2, String param3) { + return '$param1 é melhor que $param2 dos $param3 jogadores.'; + } + + @override + String betterThanPercentPlayers(String param1, String param2) { + return 'Melhor que $param1 dos jogadores de $param2'; + } + + @override + String youDoNotHaveAnEstablishedPerfTypeRating(String param) { + return 'Você não tem rating definido em $param.'; + } + + @override + String get yourRating => 'Seu rating'; + + @override + String get cumulative => 'Cumulativo'; + + @override + String get glicko2Rating => 'Rating Glicko-2'; + + @override + String get checkYourEmail => 'Verifique seu e-mail'; + + @override + String get weHaveSentYouAnEmailClickTheLink => 'Enviamos um e-mail. Clique no link do e-mail para ativar sua conta.'; + + @override + String get ifYouDoNotSeeTheEmailCheckOtherPlaces => 'Se você não vir o e-mail, verifique outros locais onde possa estar, como lixeira, spam ou outras pastas.'; + + @override + String weHaveSentYouAnEmailTo(String param) { + return 'Enviamos um e-mail para $param. Clique no link do e-mail para redefinir sua senha.'; + } + + @override + String byRegisteringYouAgreeToBeBoundByOur(String param) { + return 'Ao registrar, você concorda em se comprometer com nossa $param.'; + } + + @override + String readAboutOur(String param) { + return 'Leia sobre a nossa $param.'; + } + + @override + String get networkLagBetweenYouAndLichess => 'Atraso na rede'; + + @override + String get timeToProcessAMoveOnLichessServer => 'Tempo para processar um movimento no servidor do Lichess'; + + @override + String get downloadAnnotated => 'Baixar anotação'; + + @override + String get downloadRaw => 'Baixar texto'; + + @override + String get downloadImported => 'Baixar partida importada'; + + @override + String get crosstable => 'Tabela'; + + @override + String get youCanAlsoScrollOverTheBoardToMoveInTheGame => 'Você também pode rolar sobre o tabuleiro para percorrer as jogadas.'; + + @override + String get scrollOverComputerVariationsToPreviewThem => 'Passe o mouse pelas variações do computador para visualizá-las.'; + + @override + String get analysisShapesHowTo => 'Pressione Shift+Clique ou clique com o botão direito do mouse para desenhar círculos e setas no tabuleiro.'; + + @override + String get letOtherPlayersMessageYou => 'Permitir que outros jogadores lhe enviem mensagem'; + + @override + String get receiveForumNotifications => 'Receba notificações quando você for mencionado no fórum'; + + @override + String get shareYourInsightsData => 'Compartilhe seus dados da análise'; + + @override + String get withNobody => 'Com ninguém'; + + @override + String get withFriends => 'Com amigos'; + + @override + String get withEverybody => 'Com todos'; + + @override + String get kidMode => 'Modo infantil'; + + @override + String get kidModeIsEnabled => 'O modo infantil está ativado.'; + + @override + String get kidModeExplanation => 'Isto diz respeito à segurança. No modo infantil, todas as comunicações do site são desabilitadas. Habilite isso para seus filhos e alunos, para protegê-los de outros usuários da Internet.'; + + @override + String inKidModeTheLichessLogoGetsIconX(String param) { + return 'No modo infantil, a logo do lichess tem um ícone $param, para que você saiba que suas crianças estão seguras.'; + } + + @override + String get askYourChessTeacherAboutLiftingKidMode => 'Sua conta é gerenciada. Para desativar o modo infantil, peça ao seu professor.'; + + @override + String get enableKidMode => 'Habilitar o modo infantil'; + + @override + String get disableKidMode => 'Desabilitar o modo infantil'; + + @override + String get security => 'Segurança'; + + @override + String get sessions => 'Sessões'; + + @override + String get revokeAllSessions => 'revogar todas as sessões'; + + @override + String get playChessEverywhere => 'Jogue xadrez em qualquer lugar'; + + @override + String get asFreeAsLichess => 'Tão gratuito quanto o Lichess'; + + @override + String get builtForTheLoveOfChessNotMoney => 'Desenvolvido pelo amor ao xadrez, não pelo dinheiro'; + + @override + String get everybodyGetsAllFeaturesForFree => 'Todos têm todos os recursos de graça'; + + @override + String get zeroAdvertisement => 'Zero anúncios'; + + @override + String get fullFeatured => 'Cheio de recursos'; + + @override + String get phoneAndTablet => 'Celular e tablet'; + + @override + String get bulletBlitzClassical => 'Bullet, blitz, clássico'; + + @override + String get correspondenceChess => 'Xadrez por correspondência'; + + @override + String get onlineAndOfflinePlay => 'Jogue online e offline'; + + @override + String get viewTheSolution => 'Ver solução'; + + @override + String get followAndChallengeFriends => 'Siga e desafie amigos'; + + @override + String get gameAnalysis => 'Análise da partida'; + + @override + String xHostsY(String param1, String param2) { + return '$param1 criou $param2'; + } + + @override + String xJoinsY(String param1, String param2) { + return '$param1 entrou em $param2'; + } + + @override + String xLikesY(String param1, String param2) { + return '$param1 gostou de $param2'; + } + + @override + String get quickPairing => 'Pareamento rápido'; + + @override + String get lobby => 'Salão'; + + @override + String get anonymous => 'Anônimo'; + + @override + String yourScore(String param) { + return 'Sua pontuação:$param'; + } + + @override + String get language => 'Idioma'; + + @override + String get background => 'Cor tema'; + + @override + String get light => 'Claro'; + + @override + String get dark => 'Escuro'; + + @override + String get transparent => 'Transparente'; + + @override + String get deviceTheme => 'Tema do dispositivo'; + + @override + String get backgroundImageUrl => 'URL da imagem de fundo:'; + + @override + String get board => 'Tabuleiro'; + + @override + String get size => 'Tamanho'; + + @override + String get opacity => 'Opacidade'; + + @override + String get brightness => 'Brilho'; + + @override + String get hue => 'Tom'; + + @override + String get boardReset => 'Restaurar as cores padrão'; + + @override + String get pieceSet => 'Estilo das peças'; + + @override + String get embedInYourWebsite => 'Incorporar no seu site'; + + @override + String get usernameAlreadyUsed => 'Este nome de usuário já está registado, por favor, escolha outro.'; + + @override + String get usernamePrefixInvalid => 'O nome de usuário deve começar com uma letra.'; + + @override + String get usernameSuffixInvalid => 'O nome de usuário deve terminar com uma letra ou um número.'; + + @override + String get usernameCharsInvalid => 'Nomes de usuário só podem conter letras, números, sublinhados e hifens.'; + + @override + String get usernameUnacceptable => 'Este nome de usuário não é aceitável.'; + + @override + String get playChessInStyle => 'Jogue xadrez com estilo'; + + @override + String get chessBasics => 'Básicos do xadrez'; + + @override + String get coaches => 'Treinadores'; + + @override + String get invalidPgn => 'PGN inválido'; + + @override + String get invalidFen => 'FEN inválido'; + + @override + String get custom => 'Personalizado'; + + @override + String get notifications => 'Notificações'; + + @override + String notificationsX(String param1) { + return 'Notificações: $param1'; + } + + @override + String perfRatingX(String param) { + return 'Rating: $param'; + } + + @override + String get practiceWithComputer => 'Pratique com o computador'; + + @override + String anotherWasX(String param) { + return 'Um outro lance seria $param'; + } + + @override + String bestWasX(String param) { + return 'Melhor seria $param'; + } + + @override + String get youBrowsedAway => 'Você navegou para longe'; + + @override + String get resumePractice => 'Retornar à prática'; + + @override + String get drawByFiftyMoves => 'O jogo empatou pela regra dos cinquenta movimentos.'; + + @override + String get theGameIsADraw => 'A partida terminou em empate.'; + + @override + String get computerThinking => 'Computador pensando ...'; + + @override + String get seeBestMove => 'Veja o melhor lance'; + + @override + String get hideBestMove => 'Esconder o melhor lance'; + + @override + String get getAHint => 'Obter uma dica'; + + @override + String get evaluatingYourMove => 'Avaliando o seu movimento ...'; + + @override + String get whiteWinsGame => 'Brancas vencem'; + + @override + String get blackWinsGame => 'Pretas vencem'; + + @override + String get learnFromYourMistakes => 'Aprenda com seus erros'; + + @override + String get learnFromThisMistake => 'Aprenda com este erro'; + + @override + String get skipThisMove => 'Pular esse lance'; + + @override + String get next => 'Próximo'; + + @override + String xWasPlayed(String param) { + return '$param foi jogado'; + } + + @override + String get findBetterMoveForWhite => 'Encontrar o melhor lance para as Brancas'; + + @override + String get findBetterMoveForBlack => 'Encontre o melhor lance para as Pretas'; + + @override + String get resumeLearning => 'Continuar a aprendizagem'; + + @override + String get youCanDoBetter => 'Você pode fazer melhor'; + + @override + String get tryAnotherMoveForWhite => 'Tente um outro lance para as Brancas'; + + @override + String get tryAnotherMoveForBlack => 'Tente um outro lance para as Pretas'; + + @override + String get solution => 'Solução'; + + @override + String get waitingForAnalysis => 'Aguardando análise'; + + @override + String get noMistakesFoundForWhite => 'Nenhum erro encontrado para as Brancas'; @override String get noMistakesFoundForBlack => 'Nenhum erro encontrado para as Pretas'; @override - String get doneReviewingWhiteMistakes => 'Erros das brancas já revistos'; + String get doneReviewingWhiteMistakes => 'Erros das brancas já revistos'; + + @override + String get doneReviewingBlackMistakes => 'Erros das pretas já revistos'; + + @override + String get doItAgain => 'Faça novamente'; + + @override + String get reviewWhiteMistakes => 'Rever erros das Brancas'; + + @override + String get reviewBlackMistakes => 'Rever erros das Pretas'; + + @override + String get advantage => 'Vantagem'; + + @override + String get opening => 'Abertura'; + + @override + String get middlegame => 'Meio-jogo'; + + @override + String get endgame => 'Finais'; + + @override + String get conditionalPremoves => 'Pré-lances condicionais'; + + @override + String get addCurrentVariation => 'Adicionar a variação atual'; + + @override + String get playVariationToCreateConditionalPremoves => 'Jogar uma variação para criar pré-lances condicionais'; + + @override + String get noConditionalPremoves => 'Sem pré-lances condicionais'; + + @override + String playX(String param) { + return 'Jogar $param'; + } + + @override + String get showUnreadLichessMessage => 'Você recebeu uma mensagem privada do Lichess.'; + + @override + String get clickHereToReadIt => 'Clique aqui para ler'; + + @override + String get sorry => 'Desculpa :('; + + @override + String get weHadToTimeYouOutForAWhile => 'Tivemos de bloqueá-lo por um tempo.'; + + @override + String get why => 'Por quê?'; + + @override + String get pleasantChessExperience => 'Buscamos oferecer uma experiência agradável de xadrez para todos.'; + + @override + String get goodPractice => 'Para isso, precisamos assegurar que nossos jogadores sigam boas práticas.'; + + @override + String get potentialProblem => 'Quando um problema em potencial é detectado, nós mostramos esta mensagem.'; + + @override + String get howToAvoidThis => 'Como evitar isso?'; + + @override + String get playEveryGame => 'Jogue todos os jogos que inicia.'; + + @override + String get tryToWin => 'Tente vencer (ou pelo menos empatar) todos os jogos que jogar.'; + + @override + String get resignLostGames => 'Conceda partidas perdidas (não deixe o relógio ir até ao fim).'; + + @override + String get temporaryInconvenience => 'Pedimos desculpa pelo incômodo temporário,'; + + @override + String get wishYouGreatGames => 'e desejamos-lhe grandes jogos em lichess.org.'; + + @override + String get thankYouForReading => 'Obrigado pela leitura!'; + + @override + String get lifetimeScore => 'Pontuação de todo o período'; + + @override + String get currentMatchScore => 'Pontuação da partida atual'; + + @override + String get agreementAssistance => 'Eu concordo que em momento algum receberei assistência durante os meus jogos (seja de um computador, livro, banco de dados ou outra pessoa).'; + + @override + String get agreementNice => 'Eu concordo que serei sempre cortês com outros jogadores.'; + + @override + String agreementMultipleAccounts(String param) { + return 'Eu concordo que não criarei múltiplas contas (exceto pelas razões indicadas em $param).'; + } + + @override + String get agreementPolicy => 'Eu concordo que seguirei todas as normas do Lichess.'; + + @override + String get searchOrStartNewDiscussion => 'Procurar ou iniciar nova conversa'; + + @override + String get edit => 'Editar'; + + @override + String get bullet => 'Bullet'; + + @override + String get blitz => 'Blitz'; + + @override + String get rapid => 'Rápida'; + + @override + String get classical => 'Clássico'; + + @override + String get ultraBulletDesc => 'Jogos insanamente rápidos: menos de 30 segundos'; + + @override + String get bulletDesc => 'Jogos muito rápidos: menos de 3 minutos'; + + @override + String get blitzDesc => 'Jogos rápidos: 3 a 8 minutos'; + + @override + String get rapidDesc => 'Jogos rápidos: 8 a 25 minutos'; + + @override + String get classicalDesc => 'Jogos clássicos: 25 minutos ou mais'; + + @override + String get correspondenceDesc => 'Jogos por correspondência: um ou vários dias por lance'; + + @override + String get puzzleDesc => 'Treinador de táticas de xadrez'; + + @override + String get important => 'Importante'; + + @override + String yourQuestionMayHaveBeenAnswered(String param1) { + return 'A sua pergunta pode já ter sido respondida $param1'; + } + + @override + String get inTheFAQ => 'no F.A.Q.'; + + @override + String toReportSomeoneForCheatingOrBadBehavior(String param1) { + return 'Para denunciar um usuário por trapaças ou mau comportamento, $param1'; + } + + @override + String get useTheReportForm => 'use o formulário de denúncia'; + + @override + String toRequestSupport(String param1) { + return 'Para solicitar ajuda, $param1'; + } + + @override + String get tryTheContactPage => 'tente a página de contato'; + + @override + String makeSureToRead(String param1) { + return 'Certifique-se de ler $param1'; + } + + @override + String get theForumEtiquette => 'as regras do fórum'; + + @override + String get thisTopicIsArchived => 'Este tópico foi arquivado e não pode mais ser respondido.'; + + @override + String joinTheTeamXToPost(String param1) { + return 'Junte-se a $param1 para publicar neste fórum'; + } + + @override + String teamNamedX(String param1) { + return 'Equipe $param1'; + } + + @override + String get youCannotPostYetPlaySomeGames => 'Você não pode publicar nos fóruns ainda. Jogue algumas partidas!'; + + @override + String get subscribe => 'Seguir publicações'; + + @override + String get unsubscribe => 'Deixar de seguir publicações'; + + @override + String mentionedYouInX(String param1) { + return 'mencionou você em \"$param1\".'; + } + + @override + String xMentionedYouInY(String param1, String param2) { + return '$param1 mencionou você em \"$param2\".'; + } + + @override + String invitedYouToX(String param1) { + return 'convidou você para \"$param1\".'; + } + + @override + String xInvitedYouToY(String param1, String param2) { + return '$param1 convidou você para \"$param2\".'; + } + + @override + String get youAreNowPartOfTeam => 'Você agora faz parte da equipe.'; + + @override + String youHaveJoinedTeamX(String param1) { + return 'Você ingressou em \"$param1\".'; + } + + @override + String get someoneYouReportedWasBanned => 'Alguém que você denunciou foi banido'; + + @override + String get congratsYouWon => 'Parabéns, você venceu!'; + + @override + String gameVsX(String param1) { + return 'Jogo vs $param1'; + } + + @override + String resVsX(String param1, String param2) { + return '$param1 vs $param2'; + } + + @override + String get lostAgainstTOSViolator => 'Você perdeu rating para alguém que violou os termos de serviço do Lichess'; + + @override + String refundXpointsTimeControlY(String param1, String param2) { + return 'Reembolso: $param1 $param2 pontos de rating.'; + } + + @override + String get timeAlmostUp => 'O tempo está quase acabando!'; + + @override + String get clickToRevealEmailAddress => '[Clique para revelar o endereço de e-mail]'; + + @override + String get download => 'Baixar'; + + @override + String get coachManager => 'Configurações para professores'; + + @override + String get streamerManager => 'Configurações para streamers'; + + @override + String get cancelTournament => 'Cancelar o torneio'; + + @override + String get tournDescription => 'Descrição do torneio'; + + @override + String get tournDescriptionHelp => 'Algo especial que você queira dizer aos participantes? Tente ser breve. Links em Markdown disponíveis: [name](https://url)'; + + @override + String get ratedFormHelp => 'Os jogos valem classificação\ne afetam o rating dos jogadores'; + + @override + String get onlyMembersOfTeam => 'Apenas membros da equipe'; + + @override + String get noRestriction => 'Sem restrição'; + + @override + String get minimumRatedGames => 'Mínimo de partidas ranqueadas'; + + @override + String get minimumRating => 'Rating mínimo'; + + @override + String get maximumWeeklyRating => 'Rating máxima da semana'; + + @override + String positionInputHelp(String param) { + return 'Cole um FEN válido para iniciar as partidas a partir de uma posição específica.\nSó funciona com jogos padrão, e não com variantes.\nUse o $param para gerar uma posição FEN, e depois cole-a aqui.\nDeixe em branco para começar as partidas na posição inicial padrão.'; + } + + @override + String get cancelSimul => 'Cancelar a simultânea'; + + @override + String get simulHostcolor => 'Cor do simultanista em cada jogo'; + + @override + String get estimatedStart => 'Tempo de início estimado'; + + @override + String simulFeatured(String param) { + return 'Compartilhar em $param'; + } + + @override + String simulFeaturedHelp(String param) { + return 'Compartilhar a simultânia com todos em $param. Desative para jogos privados.'; + } + + @override + String get simulDescription => 'Descrição da simultânea'; + + @override + String get simulDescriptionHelp => 'Você gostaria de dizer algo aos participantes?'; + + @override + String markdownAvailable(String param) { + return '$param está disponível para opções de formatação adicionais.'; + } + + @override + String get embedsAvailable => 'Cole a URL de uma partida ou de um capítulo de estudo para incorporá-lo.'; + + @override + String get inYourLocalTimezone => 'No seu próprio fuso horário'; + + @override + String get tournChat => 'Chat do torneio'; + + @override + String get noChat => 'Sem chat'; + + @override + String get onlyTeamLeaders => 'Apenas líderes de equipe'; + + @override + String get onlyTeamMembers => 'Apenas membros da equipe'; + + @override + String get navigateMoveTree => 'Navegar pela notação de movimentos'; + + @override + String get mouseTricks => 'Funcionalidades do mouse'; + + @override + String get toggleLocalAnalysis => 'Ativar/desativar análise local do computador'; + + @override + String get toggleAllAnalysis => 'Ativar/desativar todas análises locais do computador'; + + @override + String get playComputerMove => 'Jogar o melhor lance de computador'; + + @override + String get analysisOptions => 'Opções de análise'; + + @override + String get focusChat => 'Focar texto'; + + @override + String get showHelpDialog => 'Mostrar esta mensagem de ajuda'; + + @override + String get reopenYourAccount => 'Reabra sua conta'; + + @override + String get closedAccountChangedMind => 'Caso você tenha encerrado sua conta, mas mudou de opinião, você tem ainda uma chance de recuperá-la.'; + + @override + String get onlyWorksOnce => 'Isso só vai funcionar uma vez.'; + + @override + String get cantDoThisTwice => 'Caso você encerre sua conta pela segunda vez, será impossível recuperá-la.'; + + @override + String get emailAssociatedToaccount => 'Endereço de e-mail associado à conta'; + + @override + String get sentEmailWithLink => 'Enviamos um e-mail pra você com um link.'; + + @override + String get tournamentEntryCode => 'Código de entrada do torneio'; + + @override + String get hangOn => 'Espere!'; + + @override + String gameInProgress(String param) { + return 'Você tem uma partida em andamento com $param.'; + } + + @override + String get abortTheGame => 'Cancelar a partida'; + + @override + String get resignTheGame => 'Abandonar a partida'; + + @override + String get youCantStartNewGame => 'Você não pode iniciar um novo jogo até que este acabe.'; + + @override + String get since => 'Desde'; + + @override + String get until => 'Até'; + + @override + String get lichessDbExplanation => 'Amostra de partidas rankeadas de todos os jogadores do Lichess'; + + @override + String get switchSides => 'Trocar de lado'; + + @override + String get closingAccountWithdrawAppeal => 'Encerrar sua conta anulará seu apelo'; + + @override + String get ourEventTips => 'Nossas dicas para organização de eventos'; + + @override + String get instructions => 'Instruções'; + + @override + String get showMeEverything => 'Mostrar tudo'; + + @override + String get lichessPatronInfo => 'Lichess é um software de código aberto, totalmente grátis e sem fins lucrativos. Todos os custos operacionais, de desenvolvimento, e os conteúdos são financiados unicamente através de doações de usuários.'; + + @override + String get nothingToSeeHere => 'Nada para ver aqui no momento.'; + + @override + String get stats => 'Estatísticas'; + + @override + String opponentLeftCounter(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'O seu adversário deixou a partida. Você pode reivindicar vitória em $count segundos.', + one: 'O seu adversário deixou a partida. Você pode reivindicar vitória em $count segundo.', + ); + return '$_temp0'; + } + + @override + String mateInXHalfMoves(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Mate em $count lances', + one: 'Mate em $count lance', + ); + return '$_temp0'; + } + + @override + String nbBlunders(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count capivaradas', + one: '$count capivarada', + ); + return '$_temp0'; + } + + @override + String nbMistakes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count erros', + one: '$count erro', + ); + return '$_temp0'; + } + + @override + String nbInaccuracies(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count imprecisões', + one: '$count imprecisão', + ); + return '$_temp0'; + } + + @override + String nbPlayers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count jogadores conectados', + one: '$count jogadores conectados', + ); + return '$_temp0'; + } + + @override + String nbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count partidas', + one: '$count partida', + ); + return '$_temp0'; + } + + @override + String ratingXOverYGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Rating $count após $param2 partidas', + one: 'Rating $count após $param2 partida', + ); + return '$_temp0'; + } + + @override + String nbBookmarks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Favoritos', + one: '$count Favoritos', + ); + return '$_temp0'; + } + + @override + String nbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count dias', + one: '$count dias', + ); + return '$_temp0'; + } + + @override + String nbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count horas', + one: '$count horas', + ); + return '$_temp0'; + } + + @override + String nbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutos', + one: '$count minuto', + ); + return '$_temp0'; + } @override - String get doneReviewingBlackMistakes => 'Erros das pretas já revistos'; + String rankIsUpdatedEveryNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'O ranking é atualizado a cada $count minutos', + one: 'O ranking é atualizado a cada $count minutos', + ); + return '$_temp0'; + } @override - String get doItAgain => 'Faça novamente'; + String nbPuzzles(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count problemas', + one: '$count quebra-cabeça', + ); + return '$_temp0'; + } @override - String get reviewWhiteMistakes => 'Rever erros das Brancas'; + String nbGamesWithYou(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count partidas contra você', + one: '$count partidas contra você', + ); + return '$_temp0'; + } @override - String get reviewBlackMistakes => 'Rever erros das Pretas'; + String nbRated(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count valendo pontos', + one: '$count valendo pontos', + ); + return '$_temp0'; + } @override - String get advantage => 'Vantagem'; + String nbWins(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count vitórias', + one: '$count vitória', + ); + return '$_temp0'; + } @override - String get opening => 'Abertura'; + String nbLosses(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count derrotas', + one: '$count derrota', + ); + return '$_temp0'; + } @override - String get middlegame => 'Meio-jogo'; + String nbDraws(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count empates', + one: '$count empates', + ); + return '$_temp0'; + } @override - String get endgame => 'Finais'; + String nbPlaying(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count jogando', + one: '$count jogando', + ); + return '$_temp0'; + } @override - String get conditionalPremoves => 'Pré-lances condicionais'; + String giveNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Dar $count segundos', + one: 'Dar $count segundos', + ); + return '$_temp0'; + } @override - String get addCurrentVariation => 'Adicionar a variação atual'; + String nbTournamentPoints(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count pontos de torneio', + one: '$count ponto de torneio', + ); + return '$_temp0'; + } @override - String get playVariationToCreateConditionalPremoves => 'Jogar uma variação para criar pré-lances condicionais'; + String nbStudies(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count estudos', + one: '$count estudo', + ); + return '$_temp0'; + } @override - String get noConditionalPremoves => 'Sem pré-lances condicionais'; + String nbSimuls(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count simultâneas', + one: '$count simultânea', + ); + return '$_temp0'; + } + + @override + String moreThanNbRatedGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '≥ $count jogos valendo pontos', + one: '≥ $count jogos valendo pontos', + ); + return '$_temp0'; + } + + @override + String moreThanNbPerfRatedGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '≥ $count $param2 partidas valendo pontos', + one: '≥ $count partida $param2 valendo pontos', + ); + return '$_temp0'; + } + + @override + String needNbMorePerfGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Você precisa jogar mais $count partidas de $param2 valendo pontos', + one: 'Você precisa jogar mais $count partida de $param2 valendo pontos', + ); + return '$_temp0'; + } + + @override + String needNbMoreGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Você precisa jogar ainda $count partidas valendo pontos', + one: 'Você precisa jogar ainda $count partidas valendo pontos', + ); + return '$_temp0'; + } + + @override + String nbImportedGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count de partidas importadas', + one: '$count de partidas importadas', + ); + return '$_temp0'; + } + + @override + String nbFriendsOnline(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count amigos online', + one: '$count amigo online', + ); + return '$_temp0'; + } + + @override + String nbFollowers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count seguidores', + one: '$count seguidores', + ); + return '$_temp0'; + } + + @override + String nbFollowing(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count seguidos', + one: '$count seguidos', + ); + return '$_temp0'; + } + + @override + String lessThanNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Menos que $count minutos', + one: 'Menos que $count minutos', + ); + return '$_temp0'; + } + + @override + String nbGamesInPlay(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count partidas em andamento', + one: '$count partidas em andamento', + ); + return '$_temp0'; + } + + @override + String maximumNbCharacters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Máximo: $count caracteres.', + one: 'Máximo: $count caractere.', + ); + return '$_temp0'; + } @override - String playX(String param) { - return 'Jogar $param'; + String blocks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count bloqueados', + one: '$count bloqueado', + ); + return '$_temp0'; } @override - String get showUnreadLichessMessage => 'Você recebeu uma mensagem privada do Lichess.'; + String nbForumPosts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count publicações no fórum', + one: '$count publicação no fórum', + ); + return '$_temp0'; + } @override - String get clickHereToReadIt => 'Clique aqui para ler'; + String nbPerfTypePlayersThisWeek(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count $param2 jogadores nesta semana.', + one: '$count $param2 jogador nesta semana.', + ); + return '$_temp0'; + } @override - String get sorry => 'Desculpa :('; + String availableInNbLanguages(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Disponível em $count idiomas!', + one: 'Disponível em $count idiomas!', + ); + return '$_temp0'; + } @override - String get weHadToTimeYouOutForAWhile => 'Tivemos de bloqueá-lo por um tempo.'; + String nbSecondsToPlayTheFirstMove(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count segundos para fazer o primeiro lance', + one: '$count segundo para fazer o primeiro lance', + ); + return '$_temp0'; + } @override - String get why => 'Por quê?'; + String nbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count segundos', + one: '$count segundo', + ); + return '$_temp0'; + } @override - String get pleasantChessExperience => 'Buscamos oferecer uma experiência agradável de xadrez para todos.'; + String andSaveNbPremoveLines(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'e salvar as linhas de pré-lance de $count', + one: 'e salvar a linha de pré-lance de $count', + ); + return '$_temp0'; + } @override - String get goodPractice => 'Para isso, precisamos assegurar que nossos jogadores sigam boas práticas.'; + String get stormMoveToStart => 'Mova para começar'; @override - String get potentialProblem => 'Quando um problema em potencial é detectado, nós mostramos esta mensagem.'; + String get stormYouPlayTheWhitePiecesInAllPuzzles => 'Você joga com as peças brancas em todos os quebra-cabeças'; @override - String get howToAvoidThis => 'Como evitar isso?'; + String get stormYouPlayTheBlackPiecesInAllPuzzles => 'Você joga com as peças pretas em todos os quebra-cabeças'; @override - String get playEveryGame => 'Jogue todos os jogos que inicia.'; + String get stormPuzzlesSolved => 'quebra-cabeças resolvidos'; @override - String get tryToWin => 'Tente vencer (ou pelo menos empatar) todos os jogos que jogar.'; + String get stormNewDailyHighscore => 'Novo recorde diário!'; @override - String get resignLostGames => 'Conceda partidas perdidas (não deixe o relógio ir até ao fim).'; + String get stormNewWeeklyHighscore => 'Novo recorde semanal!'; @override - String get temporaryInconvenience => 'Pedimos desculpa pelo incômodo temporário,'; + String get stormNewMonthlyHighscore => 'Novo recorde mensal!'; @override - String get wishYouGreatGames => 'e desejamos-lhe grandes jogos em lichess.org.'; + String get stormNewAllTimeHighscore => 'Novo recorde de todos os tempos!'; @override - String get thankYouForReading => 'Obrigado pela leitura!'; + String stormPreviousHighscoreWasX(String param) { + return 'Recorde anterior era $param'; + } @override - String get lifetimeScore => 'Pontuação de todo o período'; + String get stormPlayAgain => 'Jogar novamente'; @override - String get currentMatchScore => 'Pontuação da partida atual'; + String stormHighscoreX(String param) { + return 'Recorde: $param'; + } @override - String get agreementAssistance => 'Eu concordo que em momento algum receberei assistência durante os meus jogos (seja de um computador, livro, banco de dados ou outra pessoa).'; + String get stormScore => 'Pontuação'; @override - String get agreementNice => 'Eu concordo que serei sempre cortês com outros jogadores.'; + String get stormMoves => 'Lances'; @override - String agreementMultipleAccounts(String param) { - return 'Eu concordo que não criarei múltiplas contas (exceto pelas razões indicadas em $param).'; - } + String get stormAccuracy => 'Precisão'; @override - String get agreementPolicy => 'Eu concordo que seguirei todas as normas do Lichess.'; + String get stormCombo => 'Combo'; @override - String get searchOrStartNewDiscussion => 'Procurar ou iniciar nova conversa'; + String get stormTime => 'Tempo'; @override - String get edit => 'Editar'; + String get stormTimePerMove => 'Tempo por lance'; @override - String get bullet => 'Bullet'; + String get stormHighestSolved => 'Classificação mais alta'; @override - String get blitz => 'Blitz'; + String get stormPuzzlesPlayed => 'Quebra-cabeças jogados'; @override - String get rapid => 'Rápida'; + String get stormNewRun => 'Nova série (tecla de atalho: espaço)'; @override - String get classical => 'Clássico'; + String get stormEndRun => 'Finalizar série (tecla de atalho: Enter)'; @override - String get ultraBulletDesc => 'Jogos insanamente rápidos: menos de 30 segundos'; + String get stormHighscores => 'Melhores pontuações'; @override - String get bulletDesc => 'Jogos muito rápidos: menos de 3 minutos'; + String get stormViewBestRuns => 'Ver melhores séries'; @override - String get blitzDesc => 'Jogos rápidos: 3 a 8 minutos'; + String get stormBestRunOfDay => 'Melhor série do dia'; @override - String get rapidDesc => 'Jogos rápidos: 8 a 25 minutos'; + String get stormRuns => 'Séries'; @override - String get classicalDesc => 'Jogos clássicos: 25 minutos ou mais'; + String get stormGetReady => 'Prepare-se!'; @override - String get correspondenceDesc => 'Jogos por correspondência: um ou vários dias por lance'; + String get stormWaitingForMorePlayers => 'Esperando mais jogadores entrarem...'; @override - String get puzzleDesc => 'Treinador de táticas de xadrez'; + String get stormRaceComplete => 'Corrida concluída!'; @override - String get important => 'Importante'; + String get stormSpectating => 'Espectando'; @override - String yourQuestionMayHaveBeenAnswered(String param1) { - return 'A sua pergunta pode já ter sido respondida $param1'; - } + String get stormJoinTheRace => 'Entre na corrida!'; @override - String get inTheFAQ => 'no F.A.Q.'; + String get stormStartTheRace => 'Começar a corrida'; @override - String toReportSomeoneForCheatingOrBadBehavior(String param1) { - return 'Para denunciar um usuário por trapaças ou mau comportamento, $param1'; + String stormYourRankX(String param) { + return 'Sua classificação: $param'; } @override - String get useTheReportForm => 'use o formulário de denúncia'; - - @override - String toRequestSupport(String param1) { - return 'Para solicitar ajuda, $param1'; - } + String get stormWaitForRematch => 'Esperando por revanche'; @override - String get tryTheContactPage => 'tente a página de contato'; + String get stormNextRace => 'Próxima corrida'; @override - String makeSureToRead(String param1) { - return 'Certifique-se de ler $param1'; - } + String get stormJoinRematch => 'Junte-se a revanche'; @override - String get theForumEtiquette => 'as regras do fórum'; + String get stormWaitingToStart => 'Esperando para começar'; @override - String get thisTopicIsArchived => 'Este tópico foi arquivado e não pode mais ser respondido.'; + String get stormCreateNewGame => 'Criar um novo jogo'; @override - String joinTheTeamXToPost(String param1) { - return 'Junte-se a $param1 para publicar neste fórum'; - } + String get stormJoinPublicRace => 'Junte-se a uma corrida pública'; @override - String teamNamedX(String param1) { - return 'Equipe $param1'; - } + String get stormRaceYourFriends => 'Corra contra seus amigos'; @override - String get youCannotPostYetPlaySomeGames => 'Você não pode publicar nos fóruns ainda. Jogue algumas partidas!'; + String get stormSkip => 'pular'; @override - String get subscribe => 'Seguir publicações'; + String get stormSkipHelp => 'Você pode pular um movimento por corrida:'; @override - String get unsubscribe => 'Deixar de seguir publicações'; + String get stormSkipExplanation => 'Pule este lance para preservar o seu combo! Funciona apenas uma vez por corrida.'; @override - String mentionedYouInX(String param1) { - return 'mencionou você em \"$param1\".'; - } + String get stormFailedPuzzles => 'Quebra-cabeças falhados'; @override - String xMentionedYouInY(String param1, String param2) { - return '$param1 mencionou você em \"$param2\".'; - } + String get stormSlowPuzzles => 'Quebra-cabeças lentos'; @override - String invitedYouToX(String param1) { - return 'convidou você para \"$param1\".'; - } + String get stormSkippedPuzzle => 'Quebra-cabeça pulado'; @override - String xInvitedYouToY(String param1, String param2) { - return '$param1 convidou você para \"$param2\".'; - } + String get stormThisWeek => 'Essa semana'; @override - String get youAreNowPartOfTeam => 'Você agora faz parte da equipe.'; + String get stormThisMonth => 'Esse mês'; @override - String youHaveJoinedTeamX(String param1) { - return 'Você ingressou em \"$param1\".'; - } + String get stormAllTime => 'Desde o início'; @override - String get someoneYouReportedWasBanned => 'Alguém que você denunciou foi banido'; + String get stormClickToReload => 'Clique para recarregar'; @override - String get congratsYouWon => 'Parabéns, você venceu!'; + String get stormThisRunHasExpired => 'Esta corrida acabou!'; @override - String gameVsX(String param1) { - return 'Jogo vs $param1'; - } + String get stormThisRunWasOpenedInAnotherTab => 'Esta corrida foi aberta em outra aba!'; @override - String resVsX(String param1, String param2) { - return '$param1 vs $param2'; + String stormXRuns(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count séries', + one: '1 tentativa', + ); + return '$_temp0'; } @override - String get lostAgainstTOSViolator => 'Você perdeu rating para alguém que violou os termos de serviço do Lichess'; - - @override - String refundXpointsTimeControlY(String param1, String param2) { - return 'Reembolso: $param1 $param2 pontos de rating.'; + String stormPlayedNbRunsOfPuzzleStorm(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Jogou $count tentativas de $param2', + one: 'Jogou uma tentativa de $param2', + ); + return '$_temp0'; } @override - String get timeAlmostUp => 'O tempo está quase acabando!'; - - @override - String get clickToRevealEmailAddress => '[Clique para revelar o endereço de e-mail]'; - - @override - String get download => 'Baixar'; - - @override - String get coachManager => 'Configurações para professores'; + String get streamerLichessStreamers => 'Streamers do Lichess'; @override - String get streamerManager => 'Configurações para streamers'; + String get studyPrivate => 'Privado'; @override - String get cancelTournament => 'Cancelar o torneio'; + String get studyMyStudies => 'Meus estudos'; @override - String get tournDescription => 'Descrição do torneio'; + String get studyStudiesIContributeTo => 'Estudos para os quais contribuí'; @override - String get tournDescriptionHelp => 'Algo especial que você queira dizer aos participantes? Tente ser breve. Links em Markdown disponíveis: [name](https://url)'; + String get studyMyPublicStudies => 'Meus estudos públicos'; @override - String get ratedFormHelp => 'Os jogos valem classificação\ne afetam o rating dos jogadores'; + String get studyMyPrivateStudies => 'Meus estudos privados'; @override - String get onlyMembersOfTeam => 'Apenas membros da equipe'; + String get studyMyFavoriteStudies => 'Meus estudos favoritos'; @override - String get noRestriction => 'Sem restrição'; + String get studyWhatAreStudies => 'O que são estudos?'; @override - String get minimumRatedGames => 'Mínimo de partidas ranqueadas'; + String get studyAllStudies => 'Todos os estudos'; @override - String get minimumRating => 'Rating mínimo'; + String studyStudiesCreatedByX(String param) { + return 'Estudos criados por $param'; + } @override - String get maximumWeeklyRating => 'Rating máxima da semana'; + String get studyNoneYet => 'Nenhum ainda.'; @override - String positionInputHelp(String param) { - return 'Cole um FEN válido para iniciar as partidas a partir de uma posição específica.\nSó funciona com jogos padrão, e não com variantes.\nUse o $param para gerar uma posição FEN, e depois cole-a aqui.\nDeixe em branco para começar as partidas na posição inicial padrão.'; - } + String get studyHot => 'Em alta'; @override - String get cancelSimul => 'Cancelar a simultânea'; + String get studyDateAddedNewest => 'Data de criação (mais recente)'; @override - String get simulHostcolor => 'Cor do simultanista em cada jogo'; + String get studyDateAddedOldest => 'Data de criação (mais antiga)'; @override - String get estimatedStart => 'Tempo de início estimado'; + String get studyRecentlyUpdated => 'Atualizado recentemente'; @override - String simulFeatured(String param) { - return 'Compartilhar em $param'; - } + String get studyMostPopular => 'Mais populares'; @override - String simulFeaturedHelp(String param) { - return 'Compartilhar a simultânia com todos em $param. Desative para jogos privados.'; - } + String get studyAlphabetical => 'Em ordem alfabética'; @override - String get simulDescription => 'Descrição da simultânea'; + String get studyAddNewChapter => 'Adicionar um novo capítulo'; @override - String get simulDescriptionHelp => 'Você gostaria de dizer algo aos participantes?'; + String get studyAddMembers => 'Adicionar membros'; @override - String markdownAvailable(String param) { - return '$param está disponível para opções de formatação adicionais.'; - } + String get studyInviteToTheStudy => 'Convidar para o estudo'; @override - String get embedsAvailable => 'Cole a URL de uma partida ou de um capítulo de estudo para incorporá-lo.'; + String get studyPleaseOnlyInvitePeopleYouKnow => 'Por favor, convide apenas pessoas que você conhece e que queiram participar efetivamente deste estudo.'; @override - String get inYourLocalTimezone => 'No seu próprio fuso horário'; + String get studySearchByUsername => 'Pesquisar por nome de usuário'; @override - String get tournChat => 'Chat do torneio'; + String get studySpectator => 'Espectador'; @override - String get noChat => 'Sem chat'; + String get studyContributor => 'Colaborador'; @override - String get onlyTeamLeaders => 'Apenas líderes de equipe'; + String get studyKick => 'Expulsar'; @override - String get onlyTeamMembers => 'Apenas membros da equipe'; + String get studyLeaveTheStudy => 'Sair deste estudo'; @override - String get navigateMoveTree => 'Navegar pela notação de movimentos'; + String get studyYouAreNowAContributor => 'Agora você é um(a) colaborador(a)'; @override - String get mouseTricks => 'Funcionalidades do mouse'; + String get studyYouAreNowASpectator => 'Você agora é um(a) espectador(a)'; @override - String get toggleLocalAnalysis => 'Ativar/desativar análise local do computador'; + String get studyPgnTags => 'Etiquetas PGN'; @override - String get toggleAllAnalysis => 'Ativar/desativar todas análises locais do computador'; + String get studyLike => 'Gostei'; @override - String get playComputerMove => 'Jogar o melhor lance de computador'; + String get studyUnlike => 'Não gostei'; @override - String get analysisOptions => 'Opções de análise'; + String get studyNewTag => 'Nova etiqueta'; @override - String get focusChat => 'Focar texto'; + String get studyCommentThisPosition => 'Comente sobre esta posição'; @override - String get showHelpDialog => 'Mostrar esta mensagem de ajuda'; + String get studyCommentThisMove => 'Comente sobre este lance'; @override - String get reopenYourAccount => 'Reabra sua conta'; + String get studyAnnotateWithGlyphs => 'Anotar com símbolos'; @override - String get closedAccountChangedMind => 'Caso você tenha encerrado sua conta, mas mudou de opinião, você tem ainda uma chance de recuperá-la.'; + String get studyTheChapterIsTooShortToBeAnalysed => 'O capítulo é muito curto para ser analisado.'; @override - String get onlyWorksOnce => 'Isso só vai funcionar uma vez.'; + String get studyOnlyContributorsCanRequestAnalysis => 'Apenas os colaboradores de um estudo podem solicitar uma análise de computador.'; @override - String get cantDoThisTwice => 'Caso você encerre sua conta pela segunda vez, será impossível recuperá-la.'; + String get studyGetAFullComputerAnalysis => 'Obter uma análise completa dos movimentos pelo servidor.'; @override - String get emailAssociatedToaccount => 'Endereço de e-mail associado à conta'; + String get studyMakeSureTheChapterIsComplete => 'Certifique-se de que o capítulo está completo. Você só pode solicitar análise uma vez.'; @override - String get sentEmailWithLink => 'Enviamos um e-mail pra você com um link.'; + String get studyAllSyncMembersRemainOnTheSamePosition => 'Todos os membros em SYNC veem a mesma posição'; @override - String get tournamentEntryCode => 'Código de entrada do torneio'; + String get studyShareChanges => 'Compartilhar as alterações com os espectadores e salvá-las no servidor'; @override - String get hangOn => 'Espere!'; + String get studyPlaying => 'Jogando'; @override - String gameInProgress(String param) { - return 'Você tem uma partida em andamento com $param.'; - } + String get studyShowEvalBar => 'Barras de avaliação'; @override - String get abortTheGame => 'Cancelar a partida'; + String get studyFirst => 'Primeiro'; @override - String get resignTheGame => 'Abandonar a partida'; + String get studyPrevious => 'Anterior'; @override - String get youCantStartNewGame => 'Você não pode iniciar um novo jogo até que este acabe.'; + String get studyNext => 'Próximo'; @override - String get since => 'Desde'; + String get studyLast => 'Último'; @override - String get until => 'Até'; + String get studyShareAndExport => 'Compartilhar & exportar'; @override - String get lichessDbExplanation => 'Amostra de partidas rankeadas de todos os jogadores do Lichess'; + String get studyCloneStudy => 'Duplicar'; @override - String get switchSides => 'Trocar de lado'; + String get studyStudyPgn => 'PGN de estudo'; @override - String get closingAccountWithdrawAppeal => 'Encerrar sua conta anulará seu apelo'; + String get studyDownloadAllGames => 'Baixar todas as partidas'; @override - String get ourEventTips => 'Nossas dicas para organização de eventos'; + String get studyChapterPgn => 'PGN do capítulo'; @override - String get instructions => 'Instruções'; + String get studyCopyChapterPgn => 'Copiar PGN'; @override - String get showMeEverything => 'Mostrar tudo'; + String get studyDownloadGame => 'Baixar partida'; @override - String get lichessPatronInfo => 'Lichess é um software de código aberto, totalmente grátis e sem fins lucrativos. Todos os custos operacionais, de desenvolvimento, e os conteúdos são financiados unicamente através de doações de usuários.'; + String get studyStudyUrl => 'URL de estudo'; @override - String get nothingToSeeHere => 'Nada para ver aqui no momento.'; + String get studyCurrentChapterUrl => 'URL do capítulo atual'; @override - String opponentLeftCounter(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'O seu adversário deixou a partida. Você pode reivindicar vitória em $count segundos.', - one: 'O seu adversário deixou a partida. Você pode reivindicar vitória em $count segundo.', - ); - return '$_temp0'; - } + String get studyYouCanPasteThisInTheForumToEmbed => 'Você pode colar isso no fórum para incluir o estudo na publicação'; @override - String mateInXHalfMoves(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Mate em $count lances', - one: 'Mate em $count lance', - ); - return '$_temp0'; - } + String get studyStartAtInitialPosition => 'Começar na posição inicial'; @override - String nbBlunders(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count capivaradas', - one: '$count capivarada', - ); - return '$_temp0'; + String studyStartAtX(String param) { + return 'Começar em $param'; } @override - String nbMistakes(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count erros', - one: '$count erro', - ); - return '$_temp0'; - } + String get studyEmbedInYourWebsite => 'Incorporar em seu site ou blog'; @override - String nbInaccuracies(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count imprecisões', - one: '$count imprecisão', - ); - return '$_temp0'; - } + String get studyReadMoreAboutEmbedding => 'Leia mais sobre como incorporar'; @override - String nbPlayers(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count jogadores conectados', - one: '$count jogadores conectados', - ); - return '$_temp0'; - } + String get studyOnlyPublicStudiesCanBeEmbedded => 'Apenas os estudos públicos podem ser incorporados!'; @override - String nbGames(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count partidas', - one: '$count partida', - ); - return '$_temp0'; - } + String get studyOpen => 'Abertura'; @override - String ratingXOverYGames(int count, String param2) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Rating $count após $param2 partidas', - one: 'Rating $count em $param2 jogo', - ); - return '$_temp0'; + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1, disponibilizado por $param2'; } @override - String nbBookmarks(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count Favoritos', - one: '$count Favoritos', - ); - return '$_temp0'; - } + String get studyStudyNotFound => 'Estudo não encontrado'; @override - String nbDays(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count dias', - one: '$count dias', - ); - return '$_temp0'; - } + String get studyEditChapter => 'Editar capítulo'; @override - String nbHours(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count horas', - one: '$count horas', - ); - return '$_temp0'; - } + String get studyNewChapter => 'Novo capítulo'; @override - String nbMinutes(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count minutos', - one: '$count minuto', - ); - return '$_temp0'; + String studyImportFromChapterX(String param) { + return 'Importar de $param'; } @override - String rankIsUpdatedEveryNbMinutes(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'O ranking é atualizado a cada $count minutos', - one: 'O ranking é atualizado a cada $count minutos', - ); - return '$_temp0'; - } + String get studyOrientation => 'Orientação'; @override - String nbPuzzles(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count problemas', - one: '$count quebra-cabeça', - ); - return '$_temp0'; - } + String get studyAnalysisMode => 'Modo de análise'; @override - String nbGamesWithYou(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count partidas contra você', - one: '$count partidas contra você', - ); - return '$_temp0'; - } + String get studyPinnedChapterComment => 'Comentário de capítulo afixado'; @override - String nbRated(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count valendo pontos', - one: '$count valendo pontos', - ); - return '$_temp0'; - } + String get studySaveChapter => 'Salvar capítulo'; @override - String nbWins(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count vitórias', - one: '$count vitória', - ); - return '$_temp0'; - } + String get studyClearAnnotations => 'Remover anotações'; @override - String nbLosses(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count derrotas', - one: '$count derrota', - ); - return '$_temp0'; - } + String get studyClearVariations => 'Limpar variantes'; @override - String nbDraws(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count empates', - one: '$count empates', - ); - return '$_temp0'; - } + String get studyDeleteChapter => 'Excluir capítulo'; @override - String nbPlaying(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count jogando', - one: '$count jogando', - ); - return '$_temp0'; - } + String get studyDeleteThisChapter => 'Excluir este capítulo? Não há volta!'; @override - String giveNbSeconds(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Dar $count segundos', - one: 'Dar $count segundos', - ); - return '$_temp0'; - } + String get studyClearAllCommentsInThisChapter => 'Remover todos os comentários e formas deste capítulo?'; @override - String nbTournamentPoints(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count pontos de torneio', - one: '$count ponto de torneio', - ); - return '$_temp0'; - } + String get studyRightUnderTheBoard => 'Logo abaixo do tabuleiro'; @override - String nbStudies(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count estudos', - one: '$count estudo', - ); - return '$_temp0'; - } + String get studyNoPinnedComment => 'Nenhum'; + + @override + String get studyNormalAnalysis => 'Análise normal'; @override - String nbSimuls(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count simultâneas', - one: '$count simultânea', - ); - return '$_temp0'; - } + String get studyHideNextMoves => 'Ocultar próximos movimentos'; @override - String moreThanNbRatedGames(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '≥ $count jogos valendo pontos', - one: '≥ $count jogos valendo pontos', - ); - return '$_temp0'; - } + String get studyInteractiveLesson => 'Lição interativa'; @override - String moreThanNbPerfRatedGames(int count, String param2) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '≥ $count $param2 partidas valendo pontos', - one: '≥ $count partida $param2 valendo pontos', - ); - return '$_temp0'; + String studyChapterX(String param) { + return 'Capítulo $param'; } @override - String needNbMorePerfGames(int count, String param2) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Você precisa jogar mais $count partidas de $param2 valendo pontos', - one: 'Você precisa jogar mais $count partida de $param2 valendo pontos', - ); - return '$_temp0'; - } + String get studyEmpty => 'Vazio'; @override - String needNbMoreGames(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Você precisa jogar ainda $count partidas valendo pontos', - one: 'Você precisa jogar ainda $count partidas valendo pontos', - ); - return '$_temp0'; - } + String get studyStartFromInitialPosition => 'Reiniciar para posição inicial'; @override - String nbImportedGames(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count de partidas importadas', - one: '$count de partidas importadas', - ); - return '$_temp0'; - } + String get studyEditor => 'Editor'; @override - String nbFriendsOnline(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count amigos online', - one: '$count amigo online', - ); - return '$_temp0'; - } + String get studyStartFromCustomPosition => 'Iniciar com posição personalizada'; @override - String nbFollowers(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count seguidores', - one: '$count seguidores', - ); - return '$_temp0'; - } + String get studyLoadAGameByUrl => 'Carregar um jogo por URL'; @override - String nbFollowing(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count seguidos', - one: '$count seguidos', - ); - return '$_temp0'; - } + String get studyLoadAPositionFromFen => 'Carregar uma posição com FEN'; @override - String lessThanNbMinutes(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Menos que $count minutos', - one: 'Menos que $count minutos', - ); - return '$_temp0'; - } + String get studyLoadAGameFromPgn => 'Carregar um jogo com PGN'; @override - String nbGamesInPlay(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count partidas em andamento', - one: '$count partidas em andamento', - ); - return '$_temp0'; - } + String get studyAutomatic => 'Automático'; @override - String maximumNbCharacters(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Máximo: $count caracteres.', - one: 'Máximo: $count caractere.', - ); - return '$_temp0'; - } + String get studyUrlOfTheGame => 'URL do jogo'; @override - String blocks(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count bloqueados', - one: '$count bloqueado', - ); - return '$_temp0'; + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Carregar um jogo de $param1 ou $param2'; } @override - String nbForumPosts(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count publicações no fórum', - one: '$count publicação no fórum', - ); - return '$_temp0'; - } + String get studyCreateChapter => 'Criar capítulo'; @override - String nbPerfTypePlayersThisWeek(int count, String param2) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count $param2 jogadores nesta semana.', - one: '$count $param2 jogador nesta semana.', - ); - return '$_temp0'; - } + String get studyCreateStudy => 'Criar estudo'; @override - String availableInNbLanguages(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'Disponível em $count idiomas!', - one: 'Disponível em $count idiomas!', - ); - return '$_temp0'; - } + String get studyEditStudy => 'Editar estudo'; @override - String nbSecondsToPlayTheFirstMove(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count segundos para fazer o primeiro lance', - one: '$count segundo para fazer o primeiro lance', - ); - return '$_temp0'; - } + String get studyVisibility => 'Visibilidade'; @override - String nbSeconds(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count segundos', - one: '$count segundo', - ); - return '$_temp0'; - } + String get studyPublic => 'Público'; @override - String andSaveNbPremoveLines(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: 'e salvar as linhas de pré-lance de $count', - one: 'e salvar a linha de pré-lance de $count', - ); - return '$_temp0'; - } + String get studyUnlisted => 'Não listado'; @override - String get stormMoveToStart => 'Mova para começar'; + String get studyInviteOnly => 'Apenas por convite'; @override - String get stormYouPlayTheWhitePiecesInAllPuzzles => 'Você joga com as peças brancas em todos os quebra-cabeças'; + String get studyAllowCloning => 'Permitir clonagem'; @override - String get stormYouPlayTheBlackPiecesInAllPuzzles => 'Você joga com as peças pretas em todos os quebra-cabeças'; + String get studyNobody => 'Ninguém'; @override - String get stormPuzzlesSolved => 'quebra-cabeças resolvidos'; + String get studyOnlyMe => 'Apenas eu'; @override - String get stormNewDailyHighscore => 'Novo recorde diário!'; + String get studyContributors => 'Colaboradores'; @override - String get stormNewWeeklyHighscore => 'Novo recorde semanal!'; + String get studyMembers => 'Membros'; @override - String get stormNewMonthlyHighscore => 'Novo recorde mensal!'; + String get studyEveryone => 'Todos'; @override - String get stormNewAllTimeHighscore => 'Novo recorde de todos os tempos!'; + String get studyEnableSync => 'Ativar sincronização'; @override - String stormPreviousHighscoreWasX(String param) { - return 'Recorde anterior era $param'; - } + String get studyYesKeepEveryoneOnTheSamePosition => 'Sim: mantenha todos na mesma posição'; @override - String get stormPlayAgain => 'Jogar novamente'; + String get studyNoLetPeopleBrowseFreely => 'Não: deixe as pessoas navegarem livremente'; @override - String stormHighscoreX(String param) { - return 'Recorde: $param'; - } + String get studyPinnedStudyComment => 'Comentário de estudo afixado'; @override - String get stormScore => 'Pontuação'; + String get studyStart => 'Iniciar'; @override - String get stormMoves => 'Lances'; + String get studySave => 'Salvar'; @override - String get stormAccuracy => 'Precisão'; + String get studyClearChat => 'Limpar conversação'; @override - String get stormCombo => 'Combo'; + String get studyDeleteTheStudyChatHistory => 'Excluir o histórico de conversação do estudo? Não há volta!'; @override - String get stormTime => 'Tempo'; + String get studyDeleteStudy => 'Excluir estudo'; @override - String get stormTimePerMove => 'Tempo por lance'; + String studyConfirmDeleteStudy(String param) { + return 'Excluir todo o estudo? Não há volta! Digite o nome do estudo para confirmar: $param'; + } @override - String get stormHighestSolved => 'Classificação mais alta'; + String get studyWhereDoYouWantToStudyThat => 'Onde você quer estudar?'; @override - String get stormPuzzlesPlayed => 'Quebra-cabeças jogados'; + String get studyGoodMove => 'Boa jogada'; @override - String get stormNewRun => 'Nova série (tecla de atalho: espaço)'; + String get studyMistake => 'Erro'; @override - String get stormEndRun => 'Finalizar série (tecla de atalho: Enter)'; + String get studyBrilliantMove => 'Jogada excelente'; @override - String get stormHighscores => 'Melhores pontuações'; + String get studyBlunder => 'Erro grave'; @override - String get stormViewBestRuns => 'Ver melhores séries'; + String get studyInterestingMove => 'Jogada interessante'; @override - String get stormBestRunOfDay => 'Melhor série do dia'; + String get studyDubiousMove => 'Lance questionável'; @override - String get stormRuns => 'Séries'; + String get studyOnlyMove => 'Única jogada'; @override - String get stormGetReady => 'Prepare-se!'; + String get studyZugzwang => 'Zugzwang'; @override - String get stormWaitingForMorePlayers => 'Esperando mais jogadores entrarem...'; + String get studyEqualPosition => 'Posição igual'; @override - String get stormRaceComplete => 'Corrida concluída!'; + String get studyUnclearPosition => 'Posição incerta'; @override - String get stormSpectating => 'Espectando'; + String get studyWhiteIsSlightlyBetter => 'Brancas estão um pouco melhor'; @override - String get stormJoinTheRace => 'Entre na corrida!'; + String get studyBlackIsSlightlyBetter => 'Pretas estão um pouco melhor'; @override - String get stormStartTheRace => 'Começar a corrida'; + String get studyWhiteIsBetter => 'Brancas estão melhor'; @override - String stormYourRankX(String param) { - return 'Sua classificação: $param'; - } + String get studyBlackIsBetter => 'Pretas estão melhor'; @override - String get stormWaitForRematch => 'Esperando por revanche'; + String get studyWhiteIsWinning => 'Brancas estão ganhando'; @override - String get stormNextRace => 'Próxima corrida'; + String get studyBlackIsWinning => 'Pretas estão ganhando'; @override - String get stormJoinRematch => 'Junte-se a revanche'; + String get studyNovelty => 'Novidade'; @override - String get stormWaitingToStart => 'Esperando para começar'; + String get studyDevelopment => 'Desenvolvimento'; @override - String get stormCreateNewGame => 'Criar um novo jogo'; + String get studyInitiative => 'Iniciativa'; @override - String get stormJoinPublicRace => 'Junte-se a uma corrida pública'; + String get studyAttack => 'Ataque'; @override - String get stormRaceYourFriends => 'Corra contra seus amigos'; + String get studyCounterplay => 'Contra-ataque'; @override - String get stormSkip => 'pular'; + String get studyTimeTrouble => 'Problema de tempo'; @override - String get stormSkipHelp => 'Você pode pular um movimento por corrida:'; + String get studyWithCompensation => 'Com compensação'; @override - String get stormSkipExplanation => 'Pule este lance para preservar o seu combo! Funciona apenas uma vez por corrida.'; + String get studyWithTheIdea => 'Com a ideia'; @override - String get stormFailedPuzzles => 'Quebra-cabeças falhados'; + String get studyNextChapter => 'Próximo capítulo'; @override - String get stormSlowPuzzles => 'Quebra-cabeças lentos'; + String get studyPrevChapter => 'Capítulo anterior'; @override - String get stormSkippedPuzzle => 'Quebra-cabeça pulado'; + String get studyStudyActions => 'Opções de estudo'; @override - String get stormThisWeek => 'Essa semana'; + String get studyTopics => 'Tópicos'; @override - String get stormThisMonth => 'Esse mês'; + String get studyMyTopics => 'Meus tópicos'; @override - String get stormAllTime => 'Desde o início'; + String get studyPopularTopics => 'Tópicos populares'; @override - String get stormClickToReload => 'Clique para recarregar'; + String get studyManageTopics => 'Gerenciar tópicos'; @override - String get stormThisRunHasExpired => 'Esta corrida acabou!'; + String get studyBack => 'Voltar'; @override - String get stormThisRunWasOpenedInAnotherTab => 'Esta corrida foi aberta em outra aba!'; + String get studyPlayAgain => 'Jogar novamente'; @override - String stormXRuns(int count) { + String get studyWhatWouldYouPlay => 'O que você jogaria nessa posição?'; + + @override + String get studyYouCompletedThisLesson => 'Parabéns! Você completou essa lição.'; + + @override + String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count séries', - one: '1 tentativa', + other: '$count Capítulos', + one: '$count Capítulo', ); return '$_temp0'; } @override - String stormPlayedNbRunsOfPuzzleStorm(int count, String param2) { + String studyNbGames(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Jogou $count tentativas de $param2', - one: 'Jogou uma tentativa de $param2', + other: '$count Jogos', + one: '$count Jogo', ); return '$_temp0'; } @override - String get streamerLichessStreamers => 'Streamers do Lichess'; - - @override - String get studyShareAndExport => 'Compartilhar & exportar'; + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Membros', + one: '$count Membro', + ); + return '$_temp0'; + } @override - String get studyStart => 'Iniciar'; + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Cole seu texto PGN aqui, até $count jogos', + one: 'Cole seu texto PGN aqui, até $count jogo', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_ro.dart b/lib/l10n/l10n_ro.dart index 6fc00078fb..cf82a7509e 100644 --- a/lib/l10n/l10n_ro.dart +++ b/lib/l10n/l10n_ro.dart @@ -12,13 +12,13 @@ class AppLocalizationsRo extends AppLocalizations { String get mobileHomeTab => 'Acasă'; @override - String get mobilePuzzlesTab => 'Puzzles'; + String get mobilePuzzlesTab => 'Puzzle-uri'; @override - String get mobileToolsTab => 'Tools'; + String get mobileToolsTab => 'Unelte'; @override - String get mobileWatchTab => 'Watch'; + String get mobileWatchTab => 'Vizionează'; @override String get mobileSettingsTab => 'Setări'; @@ -27,7 +27,7 @@ class AppLocalizationsRo extends AppLocalizations { String get mobileMustBeLoggedIn => 'Trebuie să te autentifici pentru a accesa această pagină.'; @override - String get mobileSystemColors => 'System colors'; + String get mobileSystemColors => 'Culori sistem'; @override String get mobileFeedbackButton => 'Feedback'; @@ -36,99 +36,96 @@ class AppLocalizationsRo extends AppLocalizations { String get mobileOkButton => 'OK'; @override - String get mobileSettingsHapticFeedback => 'Haptic feedback'; + String get mobileSettingsHapticFeedback => 'Control tactil'; @override - String get mobileSettingsImmersiveMode => 'Immersive mode'; + String get mobileSettingsImmersiveMode => 'Mod imersiv'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileSettingsImmersiveModeSubtitle => 'Ascunde interfața de utilizator a sistemului în timpul jocului. Folosește această opțiune dacă ești deranjat de gesturile de navigare ale sistemului la marginile ecranului. Se aplică pentru ecranele de joc și Puzzle Storm.'; @override - String get mobileNotFollowingAnyUser => 'You are not following any user.'; + String get mobileNotFollowingAnyUser => 'Nu urmărești niciun utilizator.'; @override String get mobileAllGames => 'Toate jocurile'; @override - String get mobileRecentSearches => 'Recent searches'; + String get mobileRecentSearches => 'Căutări recente'; @override - String get mobileClearButton => 'Clear'; + String get mobileClearButton => 'Resetare'; @override String mobilePlayersMatchingSearchTerm(String param) { - return 'Players with \"$param\"'; + return 'Jucători cu \"$param\"'; } @override - String get mobileNoSearchResults => 'No results'; + String get mobileNoSearchResults => 'Niciun rezultat'; @override - String get mobileAreYouSure => 'Are you sure?'; + String get mobileAreYouSure => 'Ești sigur?'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; + String get mobilePuzzleStreakAbortWarning => 'Îți vei pierde streak-ul actual iar scorul va fi salvat.'; @override - String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; + String get mobilePuzzleStormNothingToShow => 'Nimic de arătat. Jucați câteva partide de Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Share this puzzle'; + String get mobileSharePuzzle => 'Distribuie acest puzzle'; @override - String get mobileShareGameURL => 'Share game URL'; + String get mobileShareGameURL => 'Distribuie URL-ul jocului'; @override - String get mobileShareGamePGN => 'Share PGN'; + String get mobileShareGamePGN => 'Distribuie PGN'; @override - String get mobileSharePositionAsFEN => 'Share position as FEN'; + String get mobileSharePositionAsFEN => 'Distribuie poziția ca FEN'; @override - String get mobileShowVariations => 'Show variations'; + String get mobileShowVariations => 'Arată variațiile'; @override - String get mobileHideVariation => 'Hide variation'; + String get mobileHideVariation => 'Ascunde variațiile'; @override String get mobileShowComments => 'Afişează сomentarii'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get mobilePuzzleStormConfirmEndRun => 'Vrei să termini acest run?'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobilePuzzleStormFilterNothingToShow => 'Nimic de afișat, vă rugăm să schimbați filtrele'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; + String get mobileCancelTakebackOffer => 'Anulați propunerea de revanșă'; @override - String get mobileCancelDrawOffer => 'Cancel draw offer'; + String get mobileWaitingForOpponentToJoin => 'În așteptarea unui jucător...'; @override - String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; + String get mobileBlindfoldMode => 'Legat la ochi'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get mobileLiveStreamers => 'Fluxuri live'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileCustomGameJoinAGame => 'Alătură-te unui joc'; @override - String get mobileCustomGameJoinAGame => 'Join a game'; + String get mobileCorrespondenceClearSavedMove => 'Șterge mutarea salvată'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; - - @override - String get mobileSomethingWentWrong => 'Something went wrong.'; + String get mobileSomethingWentWrong => 'Ceva nu a mers bine. :('; @override String get mobileShowResult => 'Arată rezultatul'; @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobilePuzzleThemesSubtitle => 'Joacă puzzle-uri din deschiderile tale preferate sau alege o temă.'; @override String get mobilePuzzleStormSubtitle => 'Rezolvă cât mai multe puzzle-uri în 3 minute.'; @@ -142,7 +139,7 @@ class AppLocalizationsRo extends AppLocalizations { String get mobileGreetingWithoutName => 'Salut'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobilePrefMagnifyDraggedPiece => 'Mărește piesa trasă'; @override String get activityActivity => 'Activitate'; @@ -254,6 +251,18 @@ class AppLocalizationsRo extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Terminat $count jocuri de tip $param2 de corespondență', + few: 'Terminat $count jocuri de tip $param2 de corespondență', + one: 'Terminat $count joc de tip $param2 de corespondență', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -365,9 +374,227 @@ class AppLocalizationsRo extends AppLocalizations { @override String get broadcastBroadcasts => 'Transmisiuni'; + @override + String get broadcastMyBroadcasts => 'Transmisiile mele'; + @override String get broadcastLiveBroadcasts => 'Difuzări de turnee în direct'; + @override + String get broadcastBroadcastCalendar => 'Broadcast calendar'; + + @override + String get broadcastNewBroadcast => 'O nouă difuzare în direct'; + + @override + String get broadcastSubscribedBroadcasts => 'Transmisii abonate'; + + @override + String get broadcastAboutBroadcasts => 'Despre emisiuni'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Cum să utilizați emisiunile Lichess.'; + + @override + String get broadcastTheNewRoundHelp => 'Runda noua va avea aceiași membri și contribuitori ca cea anterioară.'; + + @override + String get broadcastAddRound => 'Adaugă o rundă'; + + @override + String get broadcastOngoing => 'În desfășurare'; + + @override + String get broadcastUpcoming => 'Următoare'; + + @override + String get broadcastCompleted => 'Terminate'; + + @override + String get broadcastCompletedHelp => 'Lichess detectează finalizarea rundei pe baza jocurilor sursă. Utilizați această comutare dacă nu există nicio sursă.'; + + @override + String get broadcastRoundName => 'Numele rundei'; + + @override + String get broadcastRoundNumber => 'Număr rotund'; + + @override + String get broadcastTournamentName => 'Numele turneului'; + + @override + String get broadcastTournamentDescription => 'O descriere scurtă a turneului'; + + @override + String get broadcastFullDescription => 'Întreaga descriere a evenimentului'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Descriere lungă, opțională, a difuzării. $param1 este disponibil. Lungimea trebuie să fie mai mică decât $param2 caractere.'; + } + + @override + String get broadcastSourceSingleUrl => 'URL sursă PGN'; + + @override + String get broadcastSourceUrlHelp => 'URL-ul pe care Lichess îl va verifica pentru a obține actualizări al PGN-ului. Trebuie să fie public accesibil pe Internet.'; + + @override + String get broadcastSourceGameIds => 'Până la 64 de ID-uri de joc Lichess, separate prin spații.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Start date in the tournament local timezone: $param'; + } + + @override + String get broadcastStartDateHelp => 'Opțional, dacă știi când va începe evenimentul'; + + @override + String get broadcastCurrentGameUrl => 'URL-ul partidei curente'; + + @override + String get broadcastDownloadAllRounds => 'Descarcă toate rundele'; + + @override + String get broadcastResetRound => 'Resetează această rundă'; + + @override + String get broadcastDeleteRound => 'Șterge această rundă'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Șterge definitiv runda și jocurile sale.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Șterge toate jocurile din această rundă. Sursa va trebui să fie activă pentru a le recrea.'; + + @override + String get broadcastEditRoundStudy => 'Editare rundă de studiu'; + + @override + String get broadcastDeleteTournament => 'Șterge acest turneu'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Sigur doresc să ștergeți întregul turneu, toate rundele și toate jocurile sale.'; + + @override + String get broadcastShowScores => 'Arată scorurile jucătorilor pe baza rezultatelor jocului'; + + @override + String get broadcastReplacePlayerTags => 'Opțional: înlocuiește numele jucătorilor, ratingurile și titlurile'; + + @override + String get broadcastFideFederations => 'Federații FIDE'; + + @override + String get broadcastTop10Rating => 'Top 10 evaluări'; + + @override + String get broadcastFidePlayers => 'Jucători FIDE'; + + @override + String get broadcastFidePlayerNotFound => 'Jucătorul FIDE nu a fost găsit'; + + @override + String get broadcastFideProfile => 'Profil FIDE'; + + @override + String get broadcastFederation => 'Federație'; + + @override + String get broadcastAgeThisYear => 'Vârsta în acest an'; + + @override + String get broadcastUnrated => 'Fără rating'; + + @override + String get broadcastRecentTournaments => 'Turnee recente'; + + @override + String get broadcastOpenLichess => 'Deschide în Lichess'; + + @override + String get broadcastTeams => 'Echipe'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Overview'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Official website'; + + @override + String get broadcastStandings => 'Clasament'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'Scor'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count de transmisiuni', + few: '$count transmisiuni', + one: '$count transmisiune', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Provocări: $param1'; @@ -1412,10 +1639,10 @@ class AppLocalizationsRo extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Adversarul este limitat în mișcările pe care le poate face, iar toate mișcările îi înrăutățesc poziția.'; @override - String get puzzleThemeHealthyMix => 'Amestec sănătos'; + String get puzzleThemeMix => 'Amestec sănătos'; @override - String get puzzleThemeHealthyMixDescription => 'Un pic din toate. Nu știi la ce să te aștepți, așa că rămâi gata pentru orice! La fel ca în jocurile reale.'; + String get puzzleThemeMixDescription => 'Un pic din toate. Nu știi la ce să te aștepți, așa că rămâi gata pentru orice! La fel ca în jocurile reale.'; @override String get puzzleThemePlayerGames => 'Partide jucători'; @@ -1658,10 +1885,10 @@ class AppLocalizationsRo extends AppLocalizations { String get deleteFromHere => 'Șterge de aici'; @override - String get collapseVariations => 'Collapse variations'; + String get collapseVariations => 'Restrânge variațiile'; @override - String get expandVariations => 'Expand variations'; + String get expandVariations => 'Extinde variațiile'; @override String get forceVariation => 'Forțează variația'; @@ -1819,9 +2046,6 @@ class AppLocalizationsRo extends AppLocalizations { @override String get removesTheDepthLimit => 'Elimină limita de adâncime (și încălzește computerul)'; - @override - String get engineManager => 'Manager de motor'; - @override String get blunder => 'Gafă'; @@ -2085,6 +2309,9 @@ class AppLocalizationsRo extends AppLocalizations { @override String get gamesPlayed => 'Partide jucate'; + @override + String get ok => 'OK'; + @override String get cancel => 'Anulare'; @@ -2656,13 +2883,13 @@ class AppLocalizationsRo extends AppLocalizations { String get realName => 'Nume real'; @override - String get setFlair => 'Set your flair'; + String get setFlair => 'Arată-ți stilul'; @override String get flair => 'Pictograma personalizată'; @override - String get youCanHideFlair => 'There is a setting to hide all user flairs across the entire site.'; + String get youCanHideFlair => 'Există o setare pentru a ascunde flair-ul utilizatorilor pe întregului site.'; @override String get biography => 'Biografie'; @@ -2713,7 +2940,7 @@ class AppLocalizationsRo extends AppLocalizations { String get puzzles => 'Probleme de șah'; @override - String get onlineBots => 'Online bots'; + String get onlineBots => 'Boți online'; @override String get name => 'Nume'; @@ -2734,10 +2961,10 @@ class AppLocalizationsRo extends AppLocalizations { String get yes => 'Da'; @override - String get website => 'Website'; + String get website => 'Pagină web'; @override - String get mobile => 'Mobile'; + String get mobile => 'Mobil'; @override String get help => 'Ajutor:'; @@ -2794,7 +3021,13 @@ class AppLocalizationsRo extends AppLocalizations { String get other => 'Altceva'; @override - String get reportDescriptionHelp => 'Adaugă link-ul de la joc(uri) și arată ce este greșit cu privire la acest comportament al utilizatorului. Nu preciza doar ”trișează”, ci spune-ne cum ai ajuns la această concluzie. Raportul tău va fi procesat mai rapid dacă este scris în engleză.'; + String get reportCheatBoostHelp => 'Adaugă link-ul de la joc(uri) și arată ce este greșit cu privire la acest comportament al utilizatorului. Nu preciza doar \"trișează\", ci spune-ne cum ai ajuns la această concluzie.'; + + @override + String get reportUsernameHelp => 'Explică de ce acest nume de utilizator este jignitor. Nu spune doar \"e ofensiv/inadecvat\", ci spune-ne cum ai ajuns la această concluzie, mai ales în cazul în care insulta este obscură, nu este în engleză, este jargon sau este o referință istorică/culturală.'; + + @override + String get reportProcessedFasterInEnglish => 'Raportul tău va fi procesat mai rapid dacă este scris în engleză.'; @override String get error_provideOneCheatedGameLink => 'Te rugăm să furnizezi cel puțin un link către un joc în care s-a trișat.'; @@ -2897,7 +3130,7 @@ class AppLocalizationsRo extends AppLocalizations { String get outsideTheBoard => 'În afara tablei'; @override - String get allSquaresOfTheBoard => 'All squares of the board'; + String get allSquaresOfTheBoard => 'Toate pătratele de pe tablă'; @override String get onSlowGames => 'În jocurile lente'; @@ -3228,13 +3461,13 @@ class AppLocalizationsRo extends AppLocalizations { String get toggleVariationArrows => 'Comută săgețile de variație'; @override - String get cyclePreviousOrNextVariation => 'Cycle previous/next variation'; + String get cyclePreviousOrNextVariation => 'Ciclu de variație precedentă/următoare'; @override String get toggleGlyphAnnotations => 'Comută adnotările gilfelor'; @override - String get togglePositionAnnotations => 'Toggle position annotations'; + String get togglePositionAnnotations => 'Activează/Dezactivează adnotările pozițiilor'; @override String get variationArrowsInfo => 'Săgețile de variație vă permit să navigați fără a utiliza lista de mutare.'; @@ -4097,7 +4330,10 @@ class AppLocalizationsRo extends AppLocalizations { String get lichessPatronInfo => 'Lichess este o asociație non-profit și un software gratuit și open-source.\nToate costurile de operare și de dezvoltare sunt finanțate doar din donațiile utilizatorilor.'; @override - String get nothingToSeeHere => 'Nothing to see here at the moment.'; + String get nothingToSeeHere => 'Nimic de văzut aici momentan.'; + + @override + String get stats => 'Statistici'; @override String opponentLeftCounter(int count) { @@ -4789,9 +5025,518 @@ class AppLocalizationsRo extends AppLocalizations { @override String get streamerLichessStreamers => 'Lichess streameri'; + @override + String get studyPrivate => 'Privat'; + + @override + String get studyMyStudies => 'Studiile mele'; + + @override + String get studyStudiesIContributeTo => 'Studiile la care contribui'; + + @override + String get studyMyPublicStudies => 'Studiile mele publice'; + + @override + String get studyMyPrivateStudies => 'Studiile mele private'; + + @override + String get studyMyFavoriteStudies => 'Studiile mele preferate'; + + @override + String get studyWhatAreStudies => 'Ce sunt studiile?'; + + @override + String get studyAllStudies => 'Toate studiile'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Studii create de $param'; + } + + @override + String get studyNoneYet => 'Niciunul încă.'; + + @override + String get studyHot => 'Populare'; + + @override + String get studyDateAddedNewest => 'Data adăugată (cele mai noi)'; + + @override + String get studyDateAddedOldest => 'Data adăugată (cele mai vechi)'; + + @override + String get studyRecentlyUpdated => 'Încărcate recent'; + + @override + String get studyMostPopular => 'Cele mai populare'; + + @override + String get studyAlphabetical => 'Alfabetic'; + + @override + String get studyAddNewChapter => 'Adaugă un nou capitol'; + + @override + String get studyAddMembers => 'Adaugă membri'; + + @override + String get studyInviteToTheStudy => 'Invită la studiu'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Vă rugăm să invitați doar persoanele pe care le cunoașteți și care vor în mod activ să se alăture studiului.'; + + @override + String get studySearchByUsername => 'Caută după numele de utilizator'; + + @override + String get studySpectator => 'Spectator'; + + @override + String get studyContributor => 'Contribuitor'; + + @override + String get studyKick => 'Înlătură'; + + @override + String get studyLeaveTheStudy => 'Părăsește studiul'; + + @override + String get studyYouAreNowAContributor => 'Acum ești un contribuitor'; + + @override + String get studyYouAreNowASpectator => 'Acum ești un spectator'; + + @override + String get studyPgnTags => 'Etichete PGN'; + + @override + String get studyLike => 'Apreciază'; + + @override + String get studyUnlike => 'Nu îmi mai place'; + + @override + String get studyNewTag => 'Etichetă nouă'; + + @override + String get studyCommentThisPosition => 'Comentează această poziție'; + + @override + String get studyCommentThisMove => 'Comentează această mutare'; + + @override + String get studyAnnotateWithGlyphs => 'Adnotează cu simboluri'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Capitolul este prea mic pentru a fi analizat.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Numai contribuitorii studiului pot solicita o analiză a computerului.'; + + @override + String get studyGetAFullComputerAnalysis => 'Obțineți o întreagă analiză server-side a computerului a variației principale.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Asigurați-vă că acest capitol este complet. Puteți solicita o analiză doar o singură dată.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Toți membri sincronizați rămân la aceeași poziție'; + + @override + String get studyShareChanges => 'Împărtășește modificările cu spectatorii și salvează-le pe server'; + + @override + String get studyPlaying => 'În desfășurare'; + + @override + String get studyShowEvalBar => 'Bară de evaluare'; + + @override + String get studyFirst => 'Prima'; + + @override + String get studyPrevious => 'Precedentă'; + + @override + String get studyNext => 'Următoarea'; + + @override + String get studyLast => 'Ultima'; + @override String get studyShareAndExport => 'Împărtășește & exportă'; + @override + String get studyCloneStudy => 'Clonează'; + + @override + String get studyStudyPgn => 'PGN-ul studiului'; + + @override + String get studyDownloadAllGames => 'Descarcă toate partidele'; + + @override + String get studyChapterPgn => 'PGN-ul capitolului'; + + @override + String get studyCopyChapterPgn => 'Copiază PGN'; + + @override + String get studyDownloadGame => 'Descarcă partida'; + + @override + String get studyStudyUrl => 'URL-ul studiului'; + + @override + String get studyCurrentChapterUrl => 'URL-ul capitolului curent'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Poți lipi acest cod în forum pentru a îngloba'; + + @override + String get studyStartAtInitialPosition => 'Începeți de la poziția inițială'; + + @override + String studyStartAtX(String param) { + return 'Începeți la $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Înglobează pe site-ul sau blog-ul tău'; + + @override + String get studyReadMoreAboutEmbedding => 'Citește mai multe despre înglobare'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Numai studii publice pot fi înglobate!'; + + @override + String get studyOpen => 'Deschideți'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1, oferit pentru dvs. de $param2'; + } + + @override + String get studyStudyNotFound => 'Studiul nu a fost găsit'; + + @override + String get studyEditChapter => 'Editează capitolul'; + + @override + String get studyNewChapter => 'Capitol nou'; + + @override + String studyImportFromChapterX(String param) { + return 'Importă din $param'; + } + + @override + String get studyOrientation => 'Orientare'; + + @override + String get studyAnalysisMode => 'Tip de analiză'; + + @override + String get studyPinnedChapterComment => 'Comentariu fixat'; + + @override + String get studySaveChapter => 'Salvează capitolul'; + + @override + String get studyClearAnnotations => 'Curățați adnotările'; + + @override + String get studyClearVariations => 'Curățați variațiile'; + + @override + String get studyDeleteChapter => 'Ștergeți capitolul'; + + @override + String get studyDeleteThisChapter => 'Ștergeți acest capitol? Nu există cale de întoarcere!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Ștergeți toate comentariile, simbolurile și figurile desenate din acest capitol?'; + + @override + String get studyRightUnderTheBoard => 'Fix sub tablă'; + + @override + String get studyNoPinnedComment => 'Niciunul'; + + @override + String get studyNormalAnalysis => 'Analiză normală'; + + @override + String get studyHideNextMoves => 'Ascunde următoarele mutări'; + + @override + String get studyInteractiveLesson => 'Lecție interactivă'; + + @override + String studyChapterX(String param) { + return 'Capitolul $param'; + } + + @override + String get studyEmpty => 'Gol'; + + @override + String get studyStartFromInitialPosition => 'Începeți de la poziția inițială'; + + @override + String get studyEditor => 'Editor'; + + @override + String get studyStartFromCustomPosition => 'Începeți de la o poziție personalizată'; + + @override + String get studyLoadAGameByUrl => 'Încărcați meciul din URL'; + + @override + String get studyLoadAPositionFromFen => 'Încărcați o poziție din FEN'; + + @override + String get studyLoadAGameFromPgn => 'Încărcați un joc din PGN'; + + @override + String get studyAutomatic => 'Automată'; + + @override + String get studyUrlOfTheGame => 'URL-ul jocului'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Încărcați un joc de pe $param1 sau $param2'; + } + + @override + String get studyCreateChapter => 'Creați capitolul'; + + @override + String get studyCreateStudy => 'Creați studiul'; + + @override + String get studyEditStudy => 'Editați studiul'; + + @override + String get studyVisibility => 'Vizibilitate'; + + @override + String get studyPublic => 'Public'; + + @override + String get studyUnlisted => 'Nelistat'; + + @override + String get studyInviteOnly => 'Doar invitați'; + + @override + String get studyAllowCloning => 'Permiteți clonarea'; + + @override + String get studyNobody => 'Nimeni'; + + @override + String get studyOnlyMe => 'Doar eu'; + + @override + String get studyContributors => 'Contribuitori'; + + @override + String get studyMembers => 'Membri'; + + @override + String get studyEveryone => 'Toată lumea'; + + @override + String get studyEnableSync => 'Activați sincronizarea'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Da: menține-i pe toți la aceeași poziție'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Nu: permite navigarea liberă'; + + @override + String get studyPinnedStudyComment => 'Comentariu fixat'; + @override String get studyStart => 'Începe'; + + @override + String get studySave => 'Salvează'; + + @override + String get studyClearChat => 'Șterge conversația'; + + @override + String get studyDeleteTheStudyChatHistory => 'Ștergeți istoricul chatului? Nu există cale de întoarcere!'; + + @override + String get studyDeleteStudy => 'Ștergeți studiul'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Ștergeți întregul studiu? Nu există cale de întoarcere! Introduceți numele studiului pentru a confirma: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Unde vreți s-o studiați?'; + + @override + String get studyGoodMove => 'Mutare bună'; + + @override + String get studyMistake => 'Greșeală'; + + @override + String get studyBrilliantMove => 'Mișcare genială'; + + @override + String get studyBlunder => 'Gafă'; + + @override + String get studyInterestingMove => 'Mișcare interesantă'; + + @override + String get studyDubiousMove => 'Mutare dubioasă'; + + @override + String get studyOnlyMove => 'Singura mișcare posibilă'; + + @override + String get studyZugzwang => 'Zugzwang'; + + @override + String get studyEqualPosition => 'Poziție egală'; + + @override + String get studyUnclearPosition => 'Poziție neclară'; + + @override + String get studyWhiteIsSlightlyBetter => 'Albul este puțin mai bun'; + + @override + String get studyBlackIsSlightlyBetter => 'Negrul este puțin mai bun'; + + @override + String get studyWhiteIsBetter => 'Albul este mai bun'; + + @override + String get studyBlackIsBetter => 'Negrul este mai bun'; + + @override + String get studyWhiteIsWinning => 'Albul câștigă'; + + @override + String get studyBlackIsWinning => 'Negrul câștigă'; + + @override + String get studyNovelty => 'Noutate'; + + @override + String get studyDevelopment => 'Dezvoltare'; + + @override + String get studyInitiative => 'Inițiativă'; + + @override + String get studyAttack => 'Atac'; + + @override + String get studyCounterplay => 'Contraatac'; + + @override + String get studyTimeTrouble => 'Probleme de timp'; + + @override + String get studyWithCompensation => 'Cu compensații'; + + @override + String get studyWithTheIdea => 'Cu ideea'; + + @override + String get studyNextChapter => 'Capitolul următor'; + + @override + String get studyPrevChapter => 'Capitolul precedent'; + + @override + String get studyStudyActions => 'Acţiuni de studiu'; + + @override + String get studyTopics => 'Subiecte'; + + @override + String get studyMyTopics => 'Subiectele mele'; + + @override + String get studyPopularTopics => 'Subiecte populare'; + + @override + String get studyManageTopics => 'Gestionează subiecte'; + + @override + String get studyBack => 'Înapoi'; + + @override + String get studyPlayAgain => 'Joacă din nou'; + + @override + String get studyWhatWouldYouPlay => 'Ce ai juca în această poziție?'; + + @override + String get studyYouCompletedThisLesson => 'Felicitări! Ai terminat această lecție.'; + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count capitole', + few: '$count capitole', + one: '$count capitol', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count partide', + few: '$count partide', + one: '$count partidă', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count membri', + few: '$count membri', + one: '$count membru', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Lipiți textul PGN aici, până la $count meciuri', + few: 'Lipiți textul PGN aici, până la $count meciuri', + one: 'Lipiți textul PGN aici, până la $count meci', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_ru.dart b/lib/l10n/l10n_ru.dart index cda5bef248..9908cf5b0a 100644 --- a/lib/l10n/l10n_ru.dart +++ b/lib/l10n/l10n_ru.dart @@ -103,9 +103,6 @@ class AppLocalizationsRu extends AppLocalizations { @override String get mobileCancelTakebackOffer => 'Отменить предложение о возврате хода'; - @override - String get mobileCancelDrawOffer => 'Отменить предложение ничьей'; - @override String get mobileWaitingForOpponentToJoin => 'Ожидание соперника...'; @@ -142,7 +139,7 @@ class AppLocalizationsRu extends AppLocalizations { String get mobileGreetingWithoutName => 'Привет'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobilePrefMagnifyDraggedPiece => 'Увеличивать перетаскиваемую фигуру'; @override String get activityActivity => 'Активность'; @@ -262,6 +259,19 @@ class AppLocalizationsRu extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Завершены $count $param2 игр по переписке', + many: 'Завершены $count $param2 игр по переписке', + few: 'Завершены $count $param2 игры по переписке', + one: 'Завершена $count $param2 игра по переписке', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -382,9 +392,228 @@ class AppLocalizationsRu extends AppLocalizations { @override String get broadcastBroadcasts => 'Трансляции'; + @override + String get broadcastMyBroadcasts => 'Мои трансляции'; + @override String get broadcastLiveBroadcasts => 'Прямые трансляции турнира'; + @override + String get broadcastBroadcastCalendar => 'Календарь трансляций'; + + @override + String get broadcastNewBroadcast => 'Новая прямая трансляция'; + + @override + String get broadcastSubscribedBroadcasts => 'Подписанные рассылки'; + + @override + String get broadcastAboutBroadcasts => 'О трансляции'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Как пользоваться трансляциями Lichess.'; + + @override + String get broadcastTheNewRoundHelp => 'В новом туре примут участие те же участники и редакторы, что и в предыдущем туре.'; + + @override + String get broadcastAddRound => 'Добавить тур'; + + @override + String get broadcastOngoing => 'Текущие'; + + @override + String get broadcastUpcoming => 'Предстоящие'; + + @override + String get broadcastCompleted => 'Завершённые'; + + @override + String get broadcastCompletedHelp => 'Lichess определяет завершение тура на основе источника партий. Используйте этот переключатель, если нет источника.'; + + @override + String get broadcastRoundName => 'Название тура'; + + @override + String get broadcastRoundNumber => 'Номер тура'; + + @override + String get broadcastTournamentName => 'Название турнира'; + + @override + String get broadcastTournamentDescription => 'Краткое описание турнира'; + + @override + String get broadcastFullDescription => 'Полное описание события'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Необязательное полное описание трансляции. Доступна разметка $param1. Длина должна быть меньше $param2 символов.'; + } + + @override + String get broadcastSourceSingleUrl => 'Исходный URL PGN'; + + @override + String get broadcastSourceUrlHelp => 'URL-адрес, с которого Lichess будет получать обновление PGN. Он должен быть доступен для получения из Интернета.'; + + @override + String get broadcastSourceGameIds => 'До 64 идентификаторов (ID) игр Lichess, разделённых пробелами.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Дата начала турнира в местном часовом поясе: $param'; + } + + @override + String get broadcastStartDateHelp => 'Дополнительно, если вы знаете, когда событие начнётся'; + + @override + String get broadcastCurrentGameUrl => 'URL-адрес текущей партии'; + + @override + String get broadcastDownloadAllRounds => 'Скачать все туры'; + + @override + String get broadcastResetRound => 'Сбросить тур'; + + @override + String get broadcastDeleteRound => 'Удалить этот тур'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Определенно удалить тур и его партии.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Удалить все партии этого тура. Для их пересоздания потребуется активный источник.'; + + @override + String get broadcastEditRoundStudy => 'Редактировать студию тура'; + + @override + String get broadcastDeleteTournament => 'Удалить этот турнир'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Окончательно удалить весь турнир, его туры и партии.'; + + @override + String get broadcastShowScores => 'Показать очки игроков по результатам партий'; + + @override + String get broadcastReplacePlayerTags => 'Необязательно: заменить имена игроков, рейтинги и звания'; + + @override + String get broadcastFideFederations => 'Федерации FIDE'; + + @override + String get broadcastTop10Rating => 'Топ-10'; + + @override + String get broadcastFidePlayers => 'Игроки FIDE'; + + @override + String get broadcastFidePlayerNotFound => 'Профиль FIDE не найден'; + + @override + String get broadcastFideProfile => 'Профиль FIDE'; + + @override + String get broadcastFederation => 'Федерация'; + + @override + String get broadcastAgeThisYear => 'Возраст в этом году'; + + @override + String get broadcastUnrated => 'Без рейтинга'; + + @override + String get broadcastRecentTournaments => 'Недавние турниры'; + + @override + String get broadcastOpenLichess => 'Открыть в Lichess'; + + @override + String get broadcastTeams => 'Клубы'; + + @override + String get broadcastBoards => 'Доски'; + + @override + String get broadcastOverview => 'Обзор'; + + @override + String get broadcastSubscribeTitle => 'Подпишитесь, чтобы получать уведомления о начале каждого раунда. Вы можете включить звуковое или пуш-уведомление для трансляций в своих настройках.'; + + @override + String get broadcastUploadImage => 'Загрузить изображение турнира'; + + @override + String get broadcastNoBoardsYet => 'Пока нет досок. Они появятся после загрузки партий.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Доски могут быть загружены из источника или с помощью $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Начало после $param'; + } + + @override + String get broadcastStartVerySoon => 'Трансляция начнётся совсем скоро.'; + + @override + String get broadcastNotYetStarted => 'Трансляция ещё не началась.'; + + @override + String get broadcastOfficialWebsite => 'Официальный веб-сайт'; + + @override + String get broadcastStandings => 'Турнирная таблица'; + + @override + String broadcastIframeHelp(String param) { + return 'Больше опций на $param'; + } + + @override + String get broadcastWebmastersPage => 'странице веб-мастера'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'Публичный PGN-источник для этого раунда в реальном времени. Мы также предлагаем $param для более быстрой и эффективной синхронизации.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Встройте эту трансляцию на ваш сайт'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Встроить $param на свой сайт'; + } + + @override + String get broadcastRatingDiff => 'Разница в рейтингах'; + + @override + String get broadcastGamesThisTournament => 'Партии этого турнира'; + + @override + String get broadcastScore => 'Очки'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count трансляций', + many: '$count трансляций', + few: '$count трансляции', + one: '$count трансляция', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Вызовов: $param1'; @@ -773,7 +1002,7 @@ class AppLocalizationsRu extends AppLocalizations { String get preferencesNotifyBell => 'Звуковое оповещение на Личесс'; @override - String get preferencesNotifyPush => 'Оповещение на устройстве, когда Вы не находитесь на сайте Личесс'; + String get preferencesNotifyPush => 'Оповещение на устройстве, когда вы не находитесь на сайте Lichess'; @override String get preferencesNotifyWeb => 'Браузер'; @@ -1434,10 +1663,10 @@ class AppLocalizationsRu extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Противник вынужден сделать один из немногих возможных ходов, но любой ход ведёт к ухудшению его положения.'; @override - String get puzzleThemeHealthyMix => 'Сборная солянка'; + String get puzzleThemeMix => 'Сборная солянка'; @override - String get puzzleThemeHealthyMixDescription => 'Всего понемногу. Вы не знаете, чего ожидать, так что будьте готовы ко всему! Прямо как в настоящей партии.'; + String get puzzleThemeMixDescription => 'Всего понемногу. Вы не знаете, чего ожидать, так что будьте готовы ко всему! Прямо как в настоящей партии.'; @override String get puzzleThemePlayerGames => 'Партии игрока'; @@ -1773,7 +2002,7 @@ class AppLocalizationsRu extends AppLocalizations { } @override - String get playFirstOpeningEndgameExplorerMove => 'Играть первый ход изучателя дебютов/эндшпилей'; + String get playFirstOpeningEndgameExplorerMove => 'Играть первый ход изучения дебютов/эндшпилей'; @override String get winPreventedBy50MoveRule => 'Не удаётся победить из-за правила 50 ходов'; @@ -1788,7 +2017,7 @@ class AppLocalizationsRu extends AppLocalizations { String get lossOr50MovesByPriorMistake => 'Поражение или 50 ходов после последней ошибки'; @override - String get unknownDueToRounding => 'Победа/поражение гарантируется только если рекомендуемая последовательность ходов была выполнена с момента последнего взятия фигуры или хода пешки из-за возможного округления значений DTZ в базах Syzygy.'; + String get unknownDueToRounding => 'Победа/поражение гарантируется, только если рекомендуемая последовательность ходов была выполнена с момента последнего взятия фигуры или хода пешки из-за возможного округления значений DTZ в базах Syzygy.'; @override String get allSet => 'Готово!'; @@ -1841,9 +2070,6 @@ class AppLocalizationsRu extends AppLocalizations { @override String get removesTheDepthLimit => 'Снимает ограничение на глубину анализа, но заставляет поработать ваш компьютер'; - @override - String get engineManager => 'Менеджер движка'; - @override String get blunder => 'Зевок'; @@ -2107,6 +2333,9 @@ class AppLocalizationsRu extends AppLocalizations { @override String get gamesPlayed => 'Сыграно партий'; + @override + String get ok => 'OK'; + @override String get cancel => 'Отменить'; @@ -2747,7 +2976,7 @@ class AppLocalizationsRu extends AppLocalizations { String get descPrivate => 'Описание для членов команды'; @override - String get descPrivateHelp => 'Текст, который будут видеть только члены команды(добавленный текст заменит публичное описание для членов команды).'; + String get descPrivateHelp => 'Описание, которое будут видеть только члены клуба. Если установлено, то заменяет публичное описание для всех членов клуба.'; @override String get no => 'Нет'; @@ -2816,7 +3045,13 @@ class AppLocalizationsRu extends AppLocalizations { String get other => 'Другое'; @override - String get reportDescriptionHelp => 'Поделитесь с нами ссылками на игры, где, как вам кажется, были нарушены правила, и опишите, в чём дело. Недостаточно просто написать «он мухлюет», пожалуйста, опишите, как вы пришли к такому выводу. Мы сработаем оперативнее, если вы напишете на английском языке.'; + String get reportCheatBoostHelp => 'Вставьте ссылку на игру (или несколько игр) и объясните, что не так в поведении этого пользователя. Не надо просто писать «он жульничал», лучше распишите, как вы пришли к такому выводу.'; + + @override + String get reportUsernameHelp => 'Объясните, что в этом имени пользователя является оскорбительным. Не надо просто писать «оно оскорбительно или неподобающе», лучше расскажите, как вы пришли к такому выводу, особенно если оскорбление завуалировано, не на английском языке, является сленгом, или же является исторической или культурной отсылкой.'; + + @override + String get reportProcessedFasterInEnglish => 'Ваша жалоба будет рассмотрена быстрее, если она будет написана на английском языке.'; @override String get error_provideOneCheatedGameLink => 'Пожалуйста, добавьте ссылку хотя бы на одну игру, где по вашему мнению были нарушены правила.'; @@ -4121,6 +4356,9 @@ class AppLocalizationsRu extends AppLocalizations { @override String get nothingToSeeHere => 'Здесь ничего нет пока.'; + @override + String get stats => 'Статистика'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4272,7 +4510,7 @@ class AppLocalizationsRu extends AppLocalizations { other: '$count минут', many: '$count минут', few: '$count минуты', - one: '$count минута', + one: '$count одна минута', ); return '$_temp0'; } @@ -4855,9 +5093,522 @@ class AppLocalizationsRu extends AppLocalizations { @override String get streamerLichessStreamers => 'Стримеры Lichess'; + @override + String get studyPrivate => 'Частная'; + + @override + String get studyMyStudies => 'Мои студии'; + + @override + String get studyStudiesIContributeTo => 'Студии с моим участием'; + + @override + String get studyMyPublicStudies => 'Мои публичные студии'; + + @override + String get studyMyPrivateStudies => 'Мои частные студии'; + + @override + String get studyMyFavoriteStudies => 'Мои отмеченные студии'; + + @override + String get studyWhatAreStudies => 'Что такое «студии»?'; + + @override + String get studyAllStudies => 'Все студии'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Студии, созданные $param'; + } + + @override + String get studyNoneYet => 'Пока ничего.'; + + @override + String get studyHot => 'Самые активные'; + + @override + String get studyDateAddedNewest => 'Недавно добавленные'; + + @override + String get studyDateAddedOldest => 'Давно добавленные'; + + @override + String get studyRecentlyUpdated => 'Недавно обновлённые'; + + @override + String get studyMostPopular => 'Самые популярные'; + + @override + String get studyAlphabetical => 'По алфавиту'; + + @override + String get studyAddNewChapter => 'Добавить новую главу'; + + @override + String get studyAddMembers => 'Добавить участников'; + + @override + String get studyInviteToTheStudy => 'Пригласить в студию'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Приглашайте только тех участников, которых вы знаете, и кто активно желает участвовать в этой студии.'; + + @override + String get studySearchByUsername => 'Поиск по имени'; + + @override + String get studySpectator => 'Зритель'; + + @override + String get studyContributor => 'Редактор'; + + @override + String get studyKick => 'Выгнать'; + + @override + String get studyLeaveTheStudy => 'Покинуть студию'; + + @override + String get studyYouAreNowAContributor => 'Теперь вы редактор'; + + @override + String get studyYouAreNowASpectator => 'Теперь вы зритель'; + + @override + String get studyPgnTags => 'Теги PGN'; + + @override + String get studyLike => 'Нравится'; + + @override + String get studyUnlike => 'Не нравится'; + + @override + String get studyNewTag => 'Новый тег'; + + @override + String get studyCommentThisPosition => 'Комментировать эту позицию'; + + @override + String get studyCommentThisMove => 'Комментировать этот ход'; + + @override + String get studyAnnotateWithGlyphs => 'Добавить символьную аннотацию'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Глава слишком короткая для анализа.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Только редакторы студии могут запросить компьютерный анализ.'; + + @override + String get studyGetAFullComputerAnalysis => 'Получить с сервера полный компьютерный анализ главной линии.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Убедитесь, что глава завершена. Вы можете запросить анализ только один раз.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Все синхронизированные участники остаются на той же позиции'; + + @override + String get studyShareChanges => 'Поделиться изменениями со зрителями и сохранить их на сервере'; + + @override + String get studyPlaying => 'Активные'; + + @override + String get studyShowEvalBar => 'Шкалы оценки'; + + @override + String get studyFirst => 'Первая'; + + @override + String get studyPrevious => 'Назад'; + + @override + String get studyNext => 'Дальше'; + + @override + String get studyLast => 'Последняя'; + @override String get studyShareAndExport => 'Поделиться и экспортировать'; + @override + String get studyCloneStudy => 'Клонировать'; + + @override + String get studyStudyPgn => 'PGN студии'; + + @override + String get studyDownloadAllGames => 'Скачать все партии'; + + @override + String get studyChapterPgn => 'PGN главы'; + + @override + String get studyCopyChapterPgn => 'Копировать PGN'; + + @override + String get studyDownloadGame => 'Скачать партию'; + + @override + String get studyStudyUrl => 'Ссылка на студию'; + + @override + String get studyCurrentChapterUrl => 'Ссылка на эту главу'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Вставьте этот код на форум для вставки'; + + @override + String get studyStartAtInitialPosition => 'Открыть в начальной позиции'; + + @override + String studyStartAtX(String param) { + return 'Начать с $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Вставить в свой сайт или блог'; + + @override + String get studyReadMoreAboutEmbedding => 'Подробнее о вставке на сайт'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Вставлять на сайт можно только публичные студии!'; + + @override + String get studyOpen => 'Открыть'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1 на $param2'; + } + + @override + String get studyStudyNotFound => 'Студия не найдена'; + + @override + String get studyEditChapter => 'Редактировать главу'; + + @override + String get studyNewChapter => 'Новая глава'; + + @override + String studyImportFromChapterX(String param) { + return 'Импорт из $param'; + } + + @override + String get studyOrientation => 'Ориентация'; + + @override + String get studyAnalysisMode => 'Режим анализа'; + + @override + String get studyPinnedChapterComment => 'Закреплённый комментарий главы'; + + @override + String get studySaveChapter => 'Сохранить главу'; + + @override + String get studyClearAnnotations => 'Очистить аннотацию'; + + @override + String get studyClearVariations => 'Очистить варианты'; + + @override + String get studyDeleteChapter => 'Удалить главу'; + + @override + String get studyDeleteThisChapter => 'Удалить эту главу? Её нельзя будет вернуть!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Очистить все комментарии и обозначения этой главы?'; + + @override + String get studyRightUnderTheBoard => 'Прямо под доской'; + + @override + String get studyNoPinnedComment => 'Нет'; + + @override + String get studyNormalAnalysis => 'Обычный анализ'; + + @override + String get studyHideNextMoves => 'Скрыть последующие ходы'; + + @override + String get studyInteractiveLesson => 'Интерактивный урок'; + + @override + String studyChapterX(String param) { + return 'Глава $param'; + } + + @override + String get studyEmpty => 'Пусто'; + + @override + String get studyStartFromInitialPosition => 'Начать с исходной позиции'; + + @override + String get studyEditor => 'Редактор'; + + @override + String get studyStartFromCustomPosition => 'Начать со своей позиции'; + + @override + String get studyLoadAGameByUrl => 'Загрузить игру по URL'; + + @override + String get studyLoadAPositionFromFen => 'Загрузить позицию из FEN'; + + @override + String get studyLoadAGameFromPgn => 'Загрузить игру из PGN'; + + @override + String get studyAutomatic => 'Автоматически'; + + @override + String get studyUrlOfTheGame => 'URL игры'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Загрузить игру из $param1 или $param2'; + } + + @override + String get studyCreateChapter => 'Создать главу'; + + @override + String get studyCreateStudy => 'Создать студию'; + + @override + String get studyEditStudy => 'Изменить студию'; + + @override + String get studyVisibility => 'Доступно к просмотру'; + + @override + String get studyPublic => 'Публичная'; + + @override + String get studyUnlisted => 'Доступ по ссылке'; + + @override + String get studyInviteOnly => 'Только по приглашению'; + + @override + String get studyAllowCloning => 'Разрешить копирование'; + + @override + String get studyNobody => 'Никто'; + + @override + String get studyOnlyMe => 'Только я'; + + @override + String get studyContributors => 'Соавторы'; + + @override + String get studyMembers => 'Участники'; + + @override + String get studyEveryone => 'Все'; + + @override + String get studyEnableSync => 'Включить синхронизацию'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Да: устанавливать всем одинаковую позицию'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Нет: позволить участникам свободно изучать все позиции'; + + @override + String get studyPinnedStudyComment => 'Закреплённый комментарий студии'; + @override String get studyStart => 'Начать'; + + @override + String get studySave => 'Сохранить'; + + @override + String get studyClearChat => 'Очистить чат'; + + @override + String get studyDeleteTheStudyChatHistory => 'Удалить чат студии? Восстановить будет невозможно!'; + + @override + String get studyDeleteStudy => 'Удалить студию'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Удалить всю студию? Удаление необратимо! Введите название студии для подтверждения: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Где вы хотите создать студию?'; + + @override + String get studyGoodMove => 'Хороший ход'; + + @override + String get studyMistake => 'Ошибка'; + + @override + String get studyBrilliantMove => 'Отличный ход'; + + @override + String get studyBlunder => 'Зевок'; + + @override + String get studyInterestingMove => 'Интересный ход'; + + @override + String get studyDubiousMove => 'Сомнительный ход'; + + @override + String get studyOnlyMove => 'Единственный ход'; + + @override + String get studyZugzwang => 'Цугцванг'; + + @override + String get studyEqualPosition => 'Равная позиция'; + + @override + String get studyUnclearPosition => 'Неясная позиция'; + + @override + String get studyWhiteIsSlightlyBetter => 'У белых немного лучше'; + + @override + String get studyBlackIsSlightlyBetter => 'У чёрных немного лучше'; + + @override + String get studyWhiteIsBetter => 'У белых лучше'; + + @override + String get studyBlackIsBetter => 'У чёрных лучше'; + + @override + String get studyWhiteIsWinning => 'Белые побеждают'; + + @override + String get studyBlackIsWinning => 'Чёрные побеждают'; + + @override + String get studyNovelty => 'Новинка'; + + @override + String get studyDevelopment => 'Развитие'; + + @override + String get studyInitiative => 'Инициатива'; + + @override + String get studyAttack => 'Атака'; + + @override + String get studyCounterplay => 'Контригра'; + + @override + String get studyTimeTrouble => 'Цейтнот'; + + @override + String get studyWithCompensation => 'С компенсацией'; + + @override + String get studyWithTheIdea => 'С идеей'; + + @override + String get studyNextChapter => 'Следующая глава'; + + @override + String get studyPrevChapter => 'Предыдущая глава'; + + @override + String get studyStudyActions => 'Действия в студии'; + + @override + String get studyTopics => 'Темы'; + + @override + String get studyMyTopics => 'Мои темы'; + + @override + String get studyPopularTopics => 'Популярные темы'; + + @override + String get studyManageTopics => 'Управление темами'; + + @override + String get studyBack => 'Назад'; + + @override + String get studyPlayAgain => 'Сыграть снова'; + + @override + String get studyWhatWouldYouPlay => 'Как бы вы сыграли в этой позиции?'; + + @override + String get studyYouCompletedThisLesson => 'Поздравляем! Вы прошли этот урок.'; + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count глав', + many: '$count глав', + few: '$count главы', + one: '$count глава', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count партий', + many: '$count партий', + few: '$count партии', + one: '$count партия', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count участников', + many: '$count участников', + few: '$count участника', + one: '$count участник', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Вставьте текст в формате PGN, не больше $count игр', + many: 'Вставьте текст в формате PGN, не больше $count игр', + few: 'Вставьте текст в формате PGN, не больше $count игр', + one: 'Вставьте текст в формате PGN, не больше $count игры', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_sk.dart b/lib/l10n/l10n_sk.dart index ef0e4eec53..51330c0e2a 100644 --- a/lib/l10n/l10n_sk.dart +++ b/lib/l10n/l10n_sk.dart @@ -103,9 +103,6 @@ class AppLocalizationsSk extends AppLocalizations { @override String get mobileCancelTakebackOffer => 'Zrušiť žiadosť o vrátenie ťahu'; - @override - String get mobileCancelDrawOffer => 'Zrušiť navrhnutie remízy'; - @override String get mobileWaitingForOpponentToJoin => 'Čaká sa na pripojenie súpera...'; @@ -142,7 +139,7 @@ class AppLocalizationsSk extends AppLocalizations { String get mobileGreetingWithoutName => 'Ahoj'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobilePrefMagnifyDraggedPiece => 'Zväčšiť uchopenú figúrku'; @override String get activityActivity => 'Aktivita'; @@ -262,6 +259,19 @@ class AppLocalizationsSk extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Odohraných $count $param2 korešpondenčných partií', + many: 'Odohraných $count $param2 korešpondenčných partií', + few: 'Odohrané $count $param2 korešpondenčné partie', + one: 'Odohraná $count $param2 korešpondenčná partia', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -382,9 +392,228 @@ class AppLocalizationsSk extends AppLocalizations { @override String get broadcastBroadcasts => 'Vysielanie'; + @override + String get broadcastMyBroadcasts => 'Moje vysielania'; + @override String get broadcastLiveBroadcasts => 'Živé vysielanie turnaja'; + @override + String get broadcastBroadcastCalendar => 'Kalendár vysielaní'; + + @override + String get broadcastNewBroadcast => 'Nové živé vysielanie'; + + @override + String get broadcastSubscribedBroadcasts => 'Odoberané vysielania'; + + @override + String get broadcastAboutBroadcasts => 'O vysielaní'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Ako používať Lichess vysielanie.'; + + @override + String get broadcastTheNewRoundHelp => 'Nové kolo bude mať tých istých členov a prispievateľov ako to predchádzajúce.'; + + @override + String get broadcastAddRound => 'Pridať kolo'; + + @override + String get broadcastOngoing => 'Prebiehajúci'; + + @override + String get broadcastUpcoming => 'Blížiace sa'; + + @override + String get broadcastCompleted => 'Ukončené'; + + @override + String get broadcastCompletedHelp => 'Lichess rozpozná dokončenie kola, ale môže sa pomýliť. Pomocou tejto funkcie ho môžete nastaviť ručne.'; + + @override + String get broadcastRoundName => 'Názov kola'; + + @override + String get broadcastRoundNumber => 'Číslo kola'; + + @override + String get broadcastTournamentName => 'Názov turnaja'; + + @override + String get broadcastTournamentDescription => 'Krátky popis turnaja'; + + @override + String get broadcastFullDescription => 'Úplný popis turnaja'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Voliteľný dlhý popis vysielania. $param1 je dostupný. Dĺžka musí byť menej ako $param2 znakov.'; + } + + @override + String get broadcastSourceSingleUrl => 'Zdrojová URL pre PGN súbor'; + + @override + String get broadcastSourceUrlHelp => 'URL, ktorú bude Lichess kontrolovať, aby získal aktualizácie PGN. Musí byť verejne prístupná z internetu.'; + + @override + String get broadcastSourceGameIds => 'Až do 64 identifikátorov Lichess partií oddelených medzerami.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Dátum začiatku v miestnej časovej zóne turnaja: $param'; + } + + @override + String get broadcastStartDateHelp => 'Voliteľné, ak viete kedy sa udalosť začne'; + + @override + String get broadcastCurrentGameUrl => 'Adresa URL aktuálnej partie'; + + @override + String get broadcastDownloadAllRounds => 'Stiahnuť všetky kolá'; + + @override + String get broadcastResetRound => 'Resetovať toto kolo'; + + @override + String get broadcastDeleteRound => 'Vymazať toto kolo'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Definitívne vymazať kolo a partie tohto kola.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Vymazať všetky partie tohto kola. K opätovnému vytvoreniu partií bude potrebné aby bol zdroj aktívny.'; + + @override + String get broadcastEditRoundStudy => 'Upraviť kolo štúdií'; + + @override + String get broadcastDeleteTournament => 'Vymazať tento turnaj'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Definitívne odstrániť celý turnaj so všetkými kolami a všetkými partiami.'; + + @override + String get broadcastShowScores => 'Zobraziť skóre hráčov na základe výsledkov partií'; + + @override + String get broadcastReplacePlayerTags => 'Voliteľné: nahradiť mená hráčov, hodnotenia a tituly'; + + @override + String get broadcastFideFederations => 'FIDE federácie'; + + @override + String get broadcastTop10Rating => '10 najlepšie hodnotených'; + + @override + String get broadcastFidePlayers => 'FIDE šachisti'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE šachista sa nenašiel'; + + @override + String get broadcastFideProfile => 'FIDE profil'; + + @override + String get broadcastFederation => 'Federácia'; + + @override + String get broadcastAgeThisYear => 'Vek tento rok'; + + @override + String get broadcastUnrated => 'Bez hodnotenia'; + + @override + String get broadcastRecentTournaments => 'Posledné turnaje'; + + @override + String get broadcastOpenLichess => 'Otvoriť na Lichess'; + + @override + String get broadcastTeams => 'Tímy'; + + @override + String get broadcastBoards => 'Šachovnice'; + + @override + String get broadcastOverview => 'Prehľad'; + + @override + String get broadcastSubscribeTitle => 'Prihláste sa, aby ste boli informovaní o začiatku každého kola. V nastaveniach účtu môžete prepnúť zvončekové alebo push upozornenia na vysielanie.'; + + @override + String get broadcastUploadImage => 'Nahrať obrázok pre turnaj'; + + @override + String get broadcastNoBoardsYet => 'Zatiaľ žiadne šachovnice. Objavia sa po nahratí partií.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Šachovnice možno načítať pomocou zdroja alebo pomocou $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'Vysielanie sa začne čoskoro.'; + + @override + String get broadcastNotYetStarted => 'Vysielanie sa ešte nezačalo.'; + + @override + String get broadcastOfficialWebsite => 'Oficiálna webstránka'; + + @override + String get broadcastStandings => 'Poradie'; + + @override + String broadcastIframeHelp(String param) { + return 'Viac možností nájdete na $param'; + } + + @override + String get broadcastWebmastersPage => 'stránke tvorcu'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'Verejný zdroj PGN v reálnom čase pre toto kolo. Ponúkame tiež $param na rýchlejšiu a efektívnejšiu synchronizáciu.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Vložiť toto vysielanie na webovú stránku'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Vložiť $param na webovú stránku'; + } + + @override + String get broadcastRatingDiff => 'Ratingový rozdiel'; + + @override + String get broadcastGamesThisTournament => 'Partie tohto turnaja'; + + @override + String get broadcastScore => 'Skóre'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count vysielaní', + many: '$count vysielaní', + few: '$count vysielania', + one: '$count vysielanie', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Výzvy: $param1'; @@ -1434,10 +1663,10 @@ class AppLocalizationsSk extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Súper je limitovaný vo svojich ťahoch a každý ťah zhorší jeho pozíciu.'; @override - String get puzzleThemeHealthyMix => 'Zdravý mix'; + String get puzzleThemeMix => 'Zdravá zmes'; @override - String get puzzleThemeHealthyMixDescription => 'Zmes úloh. Neviete čo očakávať, a tak ste neustále pripravení na všetko! Presne ako v skutočných partiách.'; + String get puzzleThemeMixDescription => 'Od všetkého trochu. Neviete, čo môžete očakávať, a tak ste neustále pripravení na všetko! Presne ako v skutočných partiách.'; @override String get puzzleThemePlayerGames => 'Vaše partie'; @@ -1841,9 +2070,6 @@ class AppLocalizationsSk extends AppLocalizations { @override String get removesTheDepthLimit => 'Odstráni obmedzenie hĺbky analýzy a spôsobí zahrievanie Vášho počítača'; - @override - String get engineManager => 'Správa motorov'; - @override String get blunder => 'Hrubá chyba'; @@ -2107,6 +2333,9 @@ class AppLocalizationsSk extends AppLocalizations { @override String get gamesPlayed => 'Odohraných partií'; + @override + String get ok => 'OK'; + @override String get cancel => 'Zrušiť'; @@ -2816,7 +3045,13 @@ class AppLocalizationsSk extends AppLocalizations { String get other => 'Iné'; @override - String get reportDescriptionHelp => 'Vložte odkaz na hru/y, a vysvetlite, čo je zlé na tomto správaní používateľa.'; + String get reportCheatBoostHelp => 'Vložte odkaz na partiu(/e) a vysvetlite, čo je na správaní tohto používateľa zlé. Nehovorte len „podvádza“, ale povedzte, ako ste k tomuto záveru dospeli.'; + + @override + String get reportUsernameHelp => 'Vysvetlite, čo je na tomto používateľskom mene urážlivé. Nehovorte len „je to urážlivé/nevhodné“, ale povedzte nám, ako ste k tomuto záveru dospeli, najmä ak je urážka významovo zahmlená, nie je v angličtine, je v slangu alebo odkazuje na niečo z historie/kultúry.'; + + @override + String get reportProcessedFasterInEnglish => 'Vaša správa bude spracovaná rýchlejšie, ak bude napísaná v angličtine.'; @override String get error_provideOneCheatedGameLink => 'Prosím, uveďte aspoň jeden odkaz na partiu, v ktorej sa podvádzalo.'; @@ -4121,6 +4356,9 @@ class AppLocalizationsSk extends AppLocalizations { @override String get nothingToSeeHere => 'Momentálne tu nie je nič k zobrazeniu.'; + @override + String get stats => 'Štatistiky'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4855,9 +5093,522 @@ class AppLocalizationsSk extends AppLocalizations { @override String get streamerLichessStreamers => 'Lichess streameri'; + @override + String get studyPrivate => 'Súkromné'; + + @override + String get studyMyStudies => 'Moje štúdie'; + + @override + String get studyStudiesIContributeTo => 'Učivo, ku ktorému prispievam'; + + @override + String get studyMyPublicStudies => 'Moje verejné štúdie'; + + @override + String get studyMyPrivateStudies => 'Moje súkromné učivo'; + + @override + String get studyMyFavoriteStudies => 'Moje obľúbené štúdie'; + + @override + String get studyWhatAreStudies => 'Čo sú štúdie?'; + + @override + String get studyAllStudies => 'Všetko učivo'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Štúdie vytvorené $param'; + } + + @override + String get studyNoneYet => 'Zatiaľ žiadne.'; + + @override + String get studyHot => 'Teraz populárne'; + + @override + String get studyDateAddedNewest => 'Dátum pridania (najnovšie)'; + + @override + String get studyDateAddedOldest => 'Dátum pridania (najstaršie)'; + + @override + String get studyRecentlyUpdated => 'Nedávno aktualizované'; + + @override + String get studyMostPopular => 'Najpopulárnejšie'; + + @override + String get studyAlphabetical => 'Abecedne'; + + @override + String get studyAddNewChapter => 'Pridať novú kapitolu'; + + @override + String get studyAddMembers => 'Pridať členov'; + + @override + String get studyInviteToTheStudy => 'Pozvať k štúdii'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Prosím pozývajte iba ľudí ktorých poznáte a o ktorých viete, že chcú túto štúdiu vidieť.'; + + @override + String get studySearchByUsername => 'Hľadať podľa použív. mena'; + + @override + String get studySpectator => 'Divák'; + + @override + String get studyContributor => 'Prispievateľ'; + + @override + String get studyKick => 'Vyhodiť'; + + @override + String get studyLeaveTheStudy => 'Opustiť štúdiu'; + + @override + String get studyYouAreNowAContributor => 'Od teraz ste prispievateľom'; + + @override + String get studyYouAreNowASpectator => 'Od teraz ste divákom'; + + @override + String get studyPgnTags => 'PGN značka'; + + @override + String get studyLike => 'Páči sa mi'; + + @override + String get studyUnlike => 'Nepáči sa mi'; + + @override + String get studyNewTag => 'Nová značka'; + + @override + String get studyCommentThisPosition => 'Komentovať túto pozíciu'; + + @override + String get studyCommentThisMove => 'Komentovať tento ťah'; + + @override + String get studyAnnotateWithGlyphs => 'Anotovať pomocou glyphov'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Kapitola je príliš krátka na analýzu.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Oba prispievatelia k tejto štúdii môžu požiadať a počítačovú analýzu.'; + + @override + String get studyGetAFullComputerAnalysis => 'Získajte úplnú počítačovú analýzu hlavného variantu na strane servera.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Uistite sa, že kapitola je kompletná. Požiadať o analýzu môžete iba raz.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Všetci zosynchronizovaní členovia uvidia rovnakú pozíciu'; + + @override + String get studyShareChanges => 'Zdieľajte zmeny s divákmi a uložte ich na server'; + + @override + String get studyPlaying => 'Práve sa hrá'; + + @override + String get studyShowEvalBar => 'Ukazovatele hodnotenia'; + + @override + String get studyFirst => 'Prvá'; + + @override + String get studyPrevious => 'Späť'; + + @override + String get studyNext => 'Ďalej'; + + @override + String get studyLast => 'Posledná'; + @override String get studyShareAndExport => 'Zdielať & export'; + @override + String get studyCloneStudy => 'Naklonovať'; + + @override + String get studyStudyPgn => 'PGN štúdie'; + + @override + String get studyDownloadAllGames => 'Stiahnuť všetky partie'; + + @override + String get studyChapterPgn => 'PGN kapitoly'; + + @override + String get studyCopyChapterPgn => 'Kopírovať PGN'; + + @override + String get studyDownloadGame => 'Stiahnúť hru'; + + @override + String get studyStudyUrl => 'URL štúdie'; + + @override + String get studyCurrentChapterUrl => 'URL aktuálnej kapitoly'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Vložte pre zobrazenie vo fóre'; + + @override + String get studyStartAtInitialPosition => 'Začať zo základného postavenia'; + + @override + String studyStartAtX(String param) { + return 'Začať na $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Vložte na svoju webstránku alebo blog'; + + @override + String get studyReadMoreAboutEmbedding => 'Prečítajte si viac o vkladaní'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Vložené môžu byť iba verejné štúdie!'; + + @override + String get studyOpen => 'Otvoriť'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1 Vám priniesol $param2'; + } + + @override + String get studyStudyNotFound => 'Štúdia sa nenašla'; + + @override + String get studyEditChapter => 'Upraviť kapitolu'; + + @override + String get studyNewChapter => 'Nová kapitola'; + + @override + String studyImportFromChapterX(String param) { + return 'Importovať z $param'; + } + + @override + String get studyOrientation => 'Orientácia'; + + @override + String get studyAnalysisMode => 'Mód analýzy'; + + @override + String get studyPinnedChapterComment => 'Pripnutý komentár ku kapitole'; + + @override + String get studySaveChapter => 'Uložiť kapitolu'; + + @override + String get studyClearAnnotations => 'Vymazať anotácie'; + + @override + String get studyClearVariations => 'Vymazať varianty'; + + @override + String get studyDeleteChapter => 'Vymazať kapitolu'; + + @override + String get studyDeleteThisChapter => 'Chcete vymazať túto kapitolu? Táto akcia sa nedá vratit späť!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Vymazať všetky komentáre, piktogramy a nakreslené tvary v tejto kapitole?'; + + @override + String get studyRightUnderTheBoard => 'Pod hraciu dosku'; + + @override + String get studyNoPinnedComment => 'Nikam'; + + @override + String get studyNormalAnalysis => 'Normálna analýza'; + + @override + String get studyHideNextMoves => 'Skryť nasledujuce ťahy'; + + @override + String get studyInteractiveLesson => 'Interaktívna lekcia'; + + @override + String studyChapterX(String param) { + return 'Kapitola $param'; + } + + @override + String get studyEmpty => 'Prázdne'; + + @override + String get studyStartFromInitialPosition => 'Začať z počiatočnej pozície'; + + @override + String get studyEditor => 'Editor'; + + @override + String get studyStartFromCustomPosition => 'Začať z vlastnej pozície'; + + @override + String get studyLoadAGameByUrl => 'Načítať hru z URL'; + + @override + String get studyLoadAPositionFromFen => 'Načítať pozíciu z FEN'; + + @override + String get studyLoadAGameFromPgn => 'Načítať hru z PGN'; + + @override + String get studyAutomatic => 'Automatická'; + + @override + String get studyUrlOfTheGame => 'URL hry'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Načítať hru z $param1 alebo z $param2'; + } + + @override + String get studyCreateChapter => 'Vytvoriť kapitolu'; + + @override + String get studyCreateStudy => 'Vytvoriť štúdiu'; + + @override + String get studyEditStudy => 'Upraviť učivo'; + + @override + String get studyVisibility => 'Viditeľnosť'; + + @override + String get studyPublic => 'Verejné'; + + @override + String get studyUnlisted => 'Nezapísané'; + + @override + String get studyInviteOnly => 'Iba na pozvanie'; + + @override + String get studyAllowCloning => 'Povoliť klonovanie'; + + @override + String get studyNobody => 'Nikto'; + + @override + String get studyOnlyMe => 'Iba ja'; + + @override + String get studyContributors => 'Prispievatelia'; + + @override + String get studyMembers => 'Členovia'; + + @override + String get studyEveryone => 'Všetci'; + + @override + String get studyEnableSync => 'Povoliť synchronizáciu'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Ano: ponechajte každého na tej istej pozícii'; + + @override + String get studyNoLetPeopleBrowseFreely => 'No: povoľte ľudom voľne prehľadávať'; + + @override + String get studyPinnedStudyComment => 'Pripnutý komentár k učivu'; + @override String get studyStart => 'Štart'; + + @override + String get studySave => 'Uložiť'; + + @override + String get studyClearChat => 'Vymazať čet'; + + @override + String get studyDeleteTheStudyChatHistory => 'Definitívne vymazať históriu četu k tejto štúdii? Táto akcia sa nedá vrátit späť!'; + + @override + String get studyDeleteStudy => 'Vymazať štúdiu'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Definitívne vymazať štúdiu? Táto akcia sa nedá vrátit späť! Napíšte názov štúdie pre potvrdenie: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Kde to chcete študovať?'; + + @override + String get studyGoodMove => 'Dobrý ťah'; + + @override + String get studyMistake => 'Chyba'; + + @override + String get studyBrilliantMove => 'Veľmi dobrý ťah'; + + @override + String get studyBlunder => 'Hrubá chyba'; + + @override + String get studyInterestingMove => 'Zaujímavý ťah'; + + @override + String get studyDubiousMove => 'Pochybný ťah'; + + @override + String get studyOnlyMove => 'Jediný možný ťah'; + + @override + String get studyZugzwang => 'Nevýhoda ťahu'; + + @override + String get studyEqualPosition => 'Rovnocenná pozícia'; + + @override + String get studyUnclearPosition => 'Nejasná pozícia'; + + @override + String get studyWhiteIsSlightlyBetter => 'Biely stojí o trochu lepšie'; + + @override + String get studyBlackIsSlightlyBetter => 'Čierny stojí o trochu lepšie'; + + @override + String get studyWhiteIsBetter => 'Biely stojí lepšie'; + + @override + String get studyBlackIsBetter => 'Čierny stojí lepšie'; + + @override + String get studyWhiteIsWinning => 'Biely stojí na výhru'; + + @override + String get studyBlackIsWinning => 'Čierny stojí na výhru'; + + @override + String get studyNovelty => 'Novinka'; + + @override + String get studyDevelopment => 'Vývin'; + + @override + String get studyInitiative => 'Iniciatíva'; + + @override + String get studyAttack => 'Útok'; + + @override + String get studyCounterplay => 'Protiútok'; + + @override + String get studyTimeTrouble => 'Časová tieseň'; + + @override + String get studyWithCompensation => 'S výhodou'; + + @override + String get studyWithTheIdea => 'S myšlienkou'; + + @override + String get studyNextChapter => 'Ďalšia kapitola'; + + @override + String get studyPrevChapter => 'Predchádzajúca kapitola'; + + @override + String get studyStudyActions => 'Úkony pri štúdii'; + + @override + String get studyTopics => 'Témy'; + + @override + String get studyMyTopics => 'Moje témy'; + + @override + String get studyPopularTopics => 'Populárne témy'; + + @override + String get studyManageTopics => 'Spravovať témy'; + + @override + String get studyBack => 'Späť'; + + @override + String get studyPlayAgain => 'Hrať znova'; + + @override + String get studyWhatWouldYouPlay => 'Čo by ste hrali v tejto pozícii?'; + + @override + String get studyYouCompletedThisLesson => 'Gratulujeme! Túto lekciu ste ukončili.'; + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Kapitol', + many: '$count Kapitol', + few: '$count Kapitoly', + one: '$count Kapitola', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Partií', + many: '$count Partií', + few: '$count Partie', + one: '$count Partia', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Členov', + many: '$count Členov', + few: '$count Členovia', + one: '$count Člen', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Váš PGN text vložte sem, maximálne $count partií', + many: 'Váš PGN text vložte sem, maximálne $count partií', + few: 'Váš PGN text vložte sem, maximálne $count partie', + one: 'Váš PGN text vložte sem, maximálne $count partiu', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_sl.dart b/lib/l10n/l10n_sl.dart index dfc348e8b8..bcc5e32413 100644 --- a/lib/l10n/l10n_sl.dart +++ b/lib/l10n/l10n_sl.dart @@ -9,28 +9,28 @@ class AppLocalizationsSl extends AppLocalizations { AppLocalizationsSl([String locale = 'sl']) : super(locale); @override - String get mobileHomeTab => 'Home'; + String get mobileHomeTab => 'Domov'; @override - String get mobilePuzzlesTab => 'Puzzles'; + String get mobilePuzzlesTab => 'Problemi'; @override - String get mobileToolsTab => 'Tools'; + String get mobileToolsTab => 'Orodja'; @override - String get mobileWatchTab => 'Watch'; + String get mobileWatchTab => 'Glej'; @override - String get mobileSettingsTab => 'Settings'; + String get mobileSettingsTab => 'Nastavitve'; @override - String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; + String get mobileMustBeLoggedIn => 'Predenj lahko dostopaš do te strani, se je potrebno prijaviti.'; @override - String get mobileSystemColors => 'System colors'; + String get mobileSystemColors => 'Barve sistema'; @override - String get mobileFeedbackButton => 'Feedback'; + String get mobileFeedbackButton => 'Povratne informacije'; @override String get mobileOkButton => 'OK'; @@ -103,9 +103,6 @@ class AppLocalizationsSl extends AppLocalizations { @override String get mobileCancelTakebackOffer => 'Cancel takeback offer'; - @override - String get mobileCancelDrawOffer => 'Cancel draw offer'; - @override String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @@ -142,7 +139,7 @@ class AppLocalizationsSl extends AppLocalizations { String get mobileGreetingWithoutName => 'Živjo'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobilePrefMagnifyDraggedPiece => 'Povečaj vlečeno figuro'; @override String get activityActivity => 'Aktivnost'; @@ -262,6 +259,19 @@ class AppLocalizationsSl extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Dokončanih $count $param2 korespondenčnih iger', + few: 'Dokončane $count $param2 korespondenčne igre', + two: 'Dokončani $count $param2 korespondenčni igri', + one: 'Dokončana $count $param2 korespondenčna igra', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -382,9 +392,228 @@ class AppLocalizationsSl extends AppLocalizations { @override String get broadcastBroadcasts => 'Prenosi'; + @override + String get broadcastMyBroadcasts => 'Moje oddajanja'; + @override String get broadcastLiveBroadcasts => 'Prenos turnirjev v živo'; + @override + String get broadcastBroadcastCalendar => 'Koledar oddaj'; + + @override + String get broadcastNewBroadcast => 'Nov prenos v živo'; + + @override + String get broadcastSubscribedBroadcasts => 'Naročene oddaje'; + + @override + String get broadcastAboutBroadcasts => 'O oddaji'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Kako uporabljati Lichess Broadcasts.'; + + @override + String get broadcastTheNewRoundHelp => 'Novi krog bo imel iste člane in sodelavce kot prejšnji.'; + + @override + String get broadcastAddRound => 'Dodajte krog'; + + @override + String get broadcastOngoing => 'V teku'; + + @override + String get broadcastUpcoming => 'Prihajajoči'; + + @override + String get broadcastCompleted => 'Zaključeno'; + + @override + String get broadcastCompletedHelp => 'Lichess zazna zaključek kroga na podlagi izvornih iger. Uporabite ta preklop, če ni vira.'; + + @override + String get broadcastRoundName => 'Ime kroga'; + + @override + String get broadcastRoundNumber => 'Številka kroga'; + + @override + String get broadcastTournamentName => 'Turnirsko ime'; + + @override + String get broadcastTournamentDescription => 'Kratek opis turnirja'; + + @override + String get broadcastFullDescription => 'Polni opis dogodka'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Neobvezen dolg opis prenosa. $param1 je na voljo. Dolžina mora biti manjša od $param2 znakov.'; + } + + @override + String get broadcastSourceSingleUrl => 'Vir partije v PGN formatu'; + + @override + String get broadcastSourceUrlHelp => 'URL, ki ga bo Lichess preveril, da bo prejel PGN posodobitve. Javno mora biti dostopen preko interneta.'; + + @override + String get broadcastSourceGameIds => 'Up to 64 Lichess game IDs, separated by spaces.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Začetni datum v lokalnem časovnem pasu turnirja: $param'; + } + + @override + String get broadcastStartDateHelp => 'Izbirno, če veste, kdaj se dogodek začne'; + + @override + String get broadcastCurrentGameUrl => 'URL trenutno igrane igre'; + + @override + String get broadcastDownloadAllRounds => 'Prenesite vse kroge'; + + @override + String get broadcastResetRound => 'Ponastavi ta krog'; + + @override + String get broadcastDeleteRound => 'Izbriši ta krog'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Dokončno izbrišite krog in njegove igre.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Izbriši vse igre tega kroga. Vir bo moral biti aktiven, da jih lahko znova ustvarite.'; + + @override + String get broadcastEditRoundStudy => 'Uredi krog študije'; + + @override + String get broadcastDeleteTournament => 'Zbrišite ta turnir'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Dokončno izbrišite celoten turnir, vse njegove kroge in vse njegove igre.'; + + @override + String get broadcastShowScores => 'Prikaži rezultate igralcev na podlagi rezultatov igre'; + + @override + String get broadcastReplacePlayerTags => 'Izbirno: zamenjajte imena igralcev, ratinge in nazive'; + + @override + String get broadcastFideFederations => 'FIDE federations'; + + @override + String get broadcastTop10Rating => 'Top 10 rating'; + + @override + String get broadcastFidePlayers => 'FIDE players'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE player not found'; + + @override + String get broadcastFideProfile => 'FIDE profile'; + + @override + String get broadcastFederation => 'Federation'; + + @override + String get broadcastAgeThisYear => 'Age this year'; + + @override + String get broadcastUnrated => 'Unrated'; + + @override + String get broadcastRecentTournaments => 'Recent tournaments'; + + @override + String get broadcastOpenLichess => 'Open in Lichess'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Overview'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Official website'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'Score'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count oddaj', + few: '$count oddaje', + two: '$count oddaji', + one: '$count oddaja', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Izzivi:$param1'; @@ -1434,10 +1663,10 @@ class AppLocalizationsSl extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Nasprotnik ima omejene poteze in vsaka poslabša njegovo pozicijo.'; @override - String get puzzleThemeHealthyMix => 'Zdrava mešanica'; + String get puzzleThemeMix => 'Zdrava mešanica'; @override - String get puzzleThemeHealthyMixDescription => 'Vsega po malo. Ne veste, kaj pričakovati, zato bodite pripravljeni na vse! Kot pri resničnih partijah.'; + String get puzzleThemeMixDescription => 'Vsega po malo. Ne veste, kaj pričakovati, zato bodite pripravljeni na vse! Kot pri resničnih partijah.'; @override String get puzzleThemePlayerGames => 'Igralske igre'; @@ -1821,7 +2050,7 @@ class AppLocalizationsSl extends AppLocalizations { String get bestMoveArrow => 'Puščica najboljše poteze'; @override - String get showVariationArrows => 'Show variation arrows'; + String get showVariationArrows => 'Prikaži puščice z variacijami'; @override String get evaluationGauge => 'Kazalnik ocene'; @@ -1841,9 +2070,6 @@ class AppLocalizationsSl extends AppLocalizations { @override String get removesTheDepthLimit => 'Odstrani omejitev globine in ohrani računalnik topel'; - @override - String get engineManager => 'Vodja motorja'; - @override String get blunder => 'Spodrsljaj'; @@ -1922,7 +2148,7 @@ class AppLocalizationsSl extends AppLocalizations { String get friends => 'Prijatelji'; @override - String get otherPlayers => 'other players'; + String get otherPlayers => 'drugi igralci'; @override String get discussions => 'Pogovori'; @@ -2107,6 +2333,9 @@ class AppLocalizationsSl extends AppLocalizations { @override String get gamesPlayed => 'Odigranih iger'; + @override + String get ok => 'V redu'; + @override String get cancel => 'Prekliči'; @@ -2675,7 +2904,7 @@ class AppLocalizationsSl extends AppLocalizations { String get editProfile => 'Uredi profil'; @override - String get realName => 'Real name'; + String get realName => 'Pravo ime'; @override String get setFlair => 'Določite svoj okus'; @@ -2756,10 +2985,10 @@ class AppLocalizationsSl extends AppLocalizations { String get yes => 'Da'; @override - String get website => 'Website'; + String get website => 'Spletna stran'; @override - String get mobile => 'Mobile'; + String get mobile => 'Mobilna aplikacija'; @override String get help => 'Pomoč:'; @@ -2816,7 +3045,13 @@ class AppLocalizationsSl extends AppLocalizations { String get other => 'Drugo'; @override - String get reportDescriptionHelp => 'Prilepite povezave do igre (ali iger) in pojasnite kaj je narobe z obnašanjem uporabnika. Ne napišite samo \"uporabnik goljufa\" temveč pojasnite zakaj mislite tako. Prijava bo obdelana hitreje če bo napisana v angleščini.'; + String get reportCheatBoostHelp => 'Prilepite povezavo do igre (ali iger) in pojasnite, kaj je narobe z nasprotnikovim načinom igranja. Ne napišite le, da \"nasprotnik goljufa\", ampak pojasnite, kako ste prišli do te ugotovitve.'; + + @override + String get reportUsernameHelp => 'Explain what about this username is offensive. Don\'t just say \"it\'s offensive/inappropriate\", but tell us how you came to this conclusion, especially if the insult is obfuscated, not in english, is in slang, or is a historical/cultural reference.'; + + @override + String get reportProcessedFasterInEnglish => 'Your report will be processed faster if written in English.'; @override String get error_provideOneCheatedGameLink => 'Navedite vsaj eno povezavo do igre s primerom goljufanja.'; @@ -4121,6 +4356,9 @@ class AppLocalizationsSl extends AppLocalizations { @override String get nothingToSeeHere => 'Tukaj trenutno ni ničesar za videti.'; + @override + String get stats => 'Stats'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4855,9 +5093,522 @@ class AppLocalizationsSl extends AppLocalizations { @override String get streamerLichessStreamers => 'Lichess voditelji prenosa'; + @override + String get studyPrivate => 'Zasebno'; + + @override + String get studyMyStudies => 'Moje študije'; + + @override + String get studyStudiesIContributeTo => 'Študije h katerim prispevam'; + + @override + String get studyMyPublicStudies => 'Moje javne študije'; + + @override + String get studyMyPrivateStudies => 'Moje zasebne študije'; + + @override + String get studyMyFavoriteStudies => 'Moje najljubše študije'; + + @override + String get studyWhatAreStudies => 'Kaj so študije?'; + + @override + String get studyAllStudies => 'Vse študije'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Študije, ki jih je ustvaril $param'; + } + + @override + String get studyNoneYet => 'Še nič.'; + + @override + String get studyHot => 'Vroče'; + + @override + String get studyDateAddedNewest => 'Dodano (novejše)'; + + @override + String get studyDateAddedOldest => 'Dodano (starejše)'; + + @override + String get studyRecentlyUpdated => 'Nazadnje objavljeno'; + + @override + String get studyMostPopular => 'Najbolj popularno'; + + @override + String get studyAlphabetical => 'Po abecednem redu'; + + @override + String get studyAddNewChapter => 'Dodaj poglavje'; + + @override + String get studyAddMembers => 'Dodaj člane'; + + @override + String get studyInviteToTheStudy => 'Povabi na študijo'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Prosimo, povabite samo tiste ljudi, ki jih poznate in ki bi se želeli pridružiti tej študiji.'; + + @override + String get studySearchByUsername => 'Iskanje po uporabniškem imenu'; + + @override + String get studySpectator => 'Opazovalec'; + + @override + String get studyContributor => 'Sodelovalec'; + + @override + String get studyKick => 'Odstrani'; + + @override + String get studyLeaveTheStudy => 'Zapusti študijo'; + + @override + String get studyYouAreNowAContributor => 'Ste nov sodelovalec'; + + @override + String get studyYouAreNowASpectator => 'Sedaj ste opazovalec'; + + @override + String get studyPgnTags => 'PGN oznake'; + + @override + String get studyLike => 'Všečkaj'; + + @override + String get studyUnlike => 'Ni mi všeč'; + + @override + String get studyNewTag => 'Nova oznaka'; + + @override + String get studyCommentThisPosition => 'Komentiraj to pozicijo'; + + @override + String get studyCommentThisMove => 'Komentiraj to potezo'; + + @override + String get studyAnnotateWithGlyphs => 'Označi s simbolom'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'To poglavje je prekratko, da bi se analiziralo.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Samo sodelovalci v študiji lahko zahtevajo računalniško analizo.'; + + @override + String get studyGetAFullComputerAnalysis => 'Pridobi na računalniškem strežniku izvedeno računalniško analizo glavne varjante.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Poskrbite, da bo poglavje zaključeno. Analizo lahko zahtevate samo enkrat.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Vsi sinhronizirani člani so v isti poziciji'; + + @override + String get studyShareChanges => 'Deli spremembe z gledalci in jih shrani na strežnik'; + + @override + String get studyPlaying => 'V teku'; + + @override + String get studyShowEvalBar => 'Evaluation bars'; + + @override + String get studyFirst => 'Prva stran'; + + @override + String get studyPrevious => 'Prejšnja stran'; + + @override + String get studyNext => 'Naslednja stran'; + + @override + String get studyLast => 'Zadnja stran'; + @override String get studyShareAndExport => 'Deli in Izvozi podatke'; + @override + String get studyCloneStudy => 'Kloniraj'; + + @override + String get studyStudyPgn => 'PGN študije'; + + @override + String get studyDownloadAllGames => 'Prenesi vse igre'; + + @override + String get studyChapterPgn => 'PGN poglavja'; + + @override + String get studyCopyChapterPgn => 'Kopiraj PGN'; + + @override + String get studyDownloadGame => 'Prenesi igro'; + + @override + String get studyStudyUrl => 'URL študije'; + + @override + String get studyCurrentChapterUrl => 'URL trenutnega poglavja'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'To lahko prilepite na forum, da vstavite'; + + @override + String get studyStartAtInitialPosition => 'Začni v začetni poziciji'; + + @override + String studyStartAtX(String param) { + return 'Začni z $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Vstavite v vašo spletno stran ali blog'; + + @override + String get studyReadMoreAboutEmbedding => 'Preberite več o vstavljanju'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Vdelati je mogoče le javni študij!'; + + @override + String get studyOpen => 'Odpri'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1 vam ponuja $param2'; + } + + @override + String get studyStudyNotFound => 'Študije nismo našli'; + + @override + String get studyEditChapter => 'Uredi poglavje'; + + @override + String get studyNewChapter => 'Novo poglavje'; + + @override + String studyImportFromChapterX(String param) { + return 'Uvozi iz $param'; + } + + @override + String get studyOrientation => 'Smer'; + + @override + String get studyAnalysisMode => 'Analizni način'; + + @override + String get studyPinnedChapterComment => 'Pripet komentar poglavja'; + + @override + String get studySaveChapter => 'Shrani poglavje'; + + @override + String get studyClearAnnotations => 'Zbriši oznake'; + + @override + String get studyClearVariations => 'Izbriši variante'; + + @override + String get studyDeleteChapter => 'Izbriši poglavje'; + + @override + String get studyDeleteThisChapter => 'Izbriši to poglavje? Poti nazaj ni več!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Izbriši vse komentarje in oblike v tem poglavju?'; + + @override + String get studyRightUnderTheBoard => 'Takoj pod šahovnico'; + + @override + String get studyNoPinnedComment => 'Brez'; + + @override + String get studyNormalAnalysis => 'Običajna analiza'; + + @override + String get studyHideNextMoves => 'Skrij naslednje poteze'; + + @override + String get studyInteractiveLesson => 'Interaktivne lekcije'; + + @override + String studyChapterX(String param) { + return 'Poglavje: $param'; + } + + @override + String get studyEmpty => 'Prazno'; + + @override + String get studyStartFromInitialPosition => 'Začni v začetni poziciji'; + + @override + String get studyEditor => 'Urejevalnik'; + + @override + String get studyStartFromCustomPosition => 'Začni v prilagojeni poziciji'; + + @override + String get studyLoadAGameByUrl => 'Naloži partijo iz URL'; + + @override + String get studyLoadAPositionFromFen => 'Naloži pozicijo iz FEN'; + + @override + String get studyLoadAGameFromPgn => 'Naloži partijo iz PGN'; + + @override + String get studyAutomatic => 'Samodejno'; + + @override + String get studyUrlOfTheGame => 'URL igre'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Naloži partijo iz $param1 ali $param2'; + } + + @override + String get studyCreateChapter => 'Ustvari poglavje'; + + @override + String get studyCreateStudy => 'Ustvarite študijo'; + + @override + String get studyEditStudy => 'Uredite študijo'; + + @override + String get studyVisibility => 'Vidnost'; + + @override + String get studyPublic => 'Javno'; + + @override + String get studyUnlisted => 'Ni na seznamu'; + + @override + String get studyInviteOnly => 'Samo na povabilo'; + + @override + String get studyAllowCloning => 'Dovoli kloniranje'; + + @override + String get studyNobody => 'Nihče'; + + @override + String get studyOnlyMe => 'Samo jaz'; + + @override + String get studyContributors => 'Prispevali so'; + + @override + String get studyMembers => 'Člani'; + + @override + String get studyEveryone => 'Kdorkoli'; + + @override + String get studyEnableSync => 'Omogoči sinhronizacijo'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Da: vse obdrži v isti poziciji'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Ne: naj uporabniki prosto raziskujejo'; + + @override + String get studyPinnedStudyComment => 'Označen komentar študije'; + @override String get studyStart => 'Začni'; + + @override + String get studySave => 'Shrani'; + + @override + String get studyClearChat => 'Počisti klepet'; + + @override + String get studyDeleteTheStudyChatHistory => 'Brisanje zgodovine klepeta? Poti nazaj več ni!'; + + @override + String get studyDeleteStudy => 'Izbriši študijo'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Želite izbrisati celotno študijo? Ni poti nazaj! Za potrditev vnesite ime študije: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Kje želite to študirati?'; + + @override + String get studyGoodMove => 'Dobra poteza'; + + @override + String get studyMistake => 'Napakica'; + + @override + String get studyBrilliantMove => 'Briljantna poteza'; + + @override + String get studyBlunder => 'Napaka'; + + @override + String get studyInterestingMove => 'Zanimiva poteza'; + + @override + String get studyDubiousMove => 'Dvomljiva poteza'; + + @override + String get studyOnlyMove => 'Edina poteza'; + + @override + String get studyZugzwang => 'Nujnica'; + + @override + String get studyEqualPosition => 'Enaka pozicija'; + + @override + String get studyUnclearPosition => 'Nejasna pozicija'; + + @override + String get studyWhiteIsSlightlyBetter => 'Beli je nekoliko boljši'; + + @override + String get studyBlackIsSlightlyBetter => 'Črni je nekoliko boljši'; + + @override + String get studyWhiteIsBetter => 'Beli je boljši'; + + @override + String get studyBlackIsBetter => 'Črni je boljši'; + + @override + String get studyWhiteIsWinning => 'Beli zmaguje'; + + @override + String get studyBlackIsWinning => 'Črni zmaguje'; + + @override + String get studyNovelty => 'Novost'; + + @override + String get studyDevelopment => 'Razvoj'; + + @override + String get studyInitiative => 'Iniciativa'; + + @override + String get studyAttack => 'Napad'; + + @override + String get studyCounterplay => 'Protinapad'; + + @override + String get studyTimeTrouble => 'Časovna stiska'; + + @override + String get studyWithCompensation => 'S kompenzacijo'; + + @override + String get studyWithTheIdea => 'Z idejo'; + + @override + String get studyNextChapter => 'Naslednje poglavje'; + + @override + String get studyPrevChapter => 'Prejšnje poglavje'; + + @override + String get studyStudyActions => 'Študijske akcije'; + + @override + String get studyTopics => 'Teme'; + + @override + String get studyMyTopics => 'Moje teme'; + + @override + String get studyPopularTopics => 'Priljubljene teme'; + + @override + String get studyManageTopics => 'Upravljaj teme'; + + @override + String get studyBack => 'Nazaj'; + + @override + String get studyPlayAgain => 'Igrajte ponovno'; + + @override + String get studyWhatWouldYouPlay => 'Kaj bi igrali v tem položaju?'; + + @override + String get studyYouCompletedThisLesson => 'Čestitke! Končali ste to lekcijo.'; + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count poglavij', + few: '$count Poglavja', + two: '$count Poglavji', + one: '$count Poglavje', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Partij', + few: '$count Partije', + two: '$count Partiji', + one: '$count Partija', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Članov', + few: '$count Člani', + two: '$count Člana', + one: '$count Član', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Prilepite PGN besedilo, z največ $count partijami', + few: 'Prilepite PGN besedilo, z največ $count partijami', + two: 'Prilepite PGN besedilo, z največ $count partijama', + one: 'Prilepite PGN besedilo, z največ $count partijo', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_sq.dart b/lib/l10n/l10n_sq.dart index ee6e39f9de..28bd224dab 100644 --- a/lib/l10n/l10n_sq.dart +++ b/lib/l10n/l10n_sq.dart @@ -103,9 +103,6 @@ class AppLocalizationsSq extends AppLocalizations { @override String get mobileCancelTakebackOffer => 'Anulojeni ofertën për prapakthim'; - @override - String get mobileCancelDrawOffer => 'Anulojeni ofertën për barazim'; - @override String get mobileWaitingForOpponentToJoin => 'Po pritet që të vijë kundërshtari…'; @@ -142,7 +139,7 @@ class AppLocalizationsSq extends AppLocalizations { String get mobileGreetingWithoutName => 'Tungjatjeta'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobilePrefMagnifyDraggedPiece => 'Zmadho gurin e tërhequr'; @override String get activityActivity => 'Aktiviteti'; @@ -246,6 +243,17 @@ class AppLocalizationsSq extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Completed $count $param2 correspondence games', + one: 'Completed $count $param2 correspondence game', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,226 @@ class AppLocalizationsSq extends AppLocalizations { @override String get broadcastBroadcasts => 'Transmetime'; + @override + String get broadcastMyBroadcasts => 'Transmetimet e mia'; + @override String get broadcastLiveBroadcasts => 'Transmetime të drejtpërdrejta turnesh'; + @override + String get broadcastBroadcastCalendar => 'Kalendar transmetimesh'; + + @override + String get broadcastNewBroadcast => 'Transmetim i ri i drejtpërdrejtë'; + + @override + String get broadcastSubscribedBroadcasts => 'Transmetime me pajtim'; + + @override + String get broadcastAboutBroadcasts => 'Rreth transmetimeve'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Si të përdoren Transmetimet Lichess.'; + + @override + String get broadcastTheNewRoundHelp => 'Raundi i ri do të ketë të njëjtën anëtarë dhe kontribues si i mëparshmi.'; + + @override + String get broadcastAddRound => 'Shtoni një raund'; + + @override + String get broadcastOngoing => 'Në zhvillim'; + + @override + String get broadcastUpcoming => 'I ardhshëm'; + + @override + String get broadcastCompleted => 'I mbaruar'; + + @override + String get broadcastCompletedHelp => 'Lichess-i e pikas plotësimin e raundit bazuar në lojërat burim. Përdoreni këtë buton, nëse s’ka burim.'; + + @override + String get broadcastRoundName => 'Emër raundi'; + + @override + String get broadcastRoundNumber => 'Numër raundi'; + + @override + String get broadcastTournamentName => 'Emër turneu'; + + @override + String get broadcastTournamentDescription => 'Përshkrim i shkurtër i turneut'; + + @override + String get broadcastFullDescription => 'Përshkrim i plotë i turneut'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Përshkrim i gjatë opsional i turneut. $param1 është e disponueshme. Gjatësia duhet të jetë më pak se $param2 shenja.'; + } + + @override + String get broadcastSourceSingleUrl => 'URL Burimi PGN-je'; + + @override + String get broadcastSourceUrlHelp => 'URL-ja që do të kontrollojë Lichess-i për të marrë përditësime PGN-sh. Duhet të jetë e përdorshme publikisht që nga Interneti.'; + + @override + String get broadcastSourceGameIds => 'Deri në 64 ID lojërash Lichess, ndarë me hapësira.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Datë fillimi në zonën kohore vendore të turneut: $param'; + } + + @override + String get broadcastStartDateHelp => 'Opsionale, nëse e dini kur fillon veprimtaria'; + + @override + String get broadcastCurrentGameUrl => 'URL e lojës së tanishme'; + + @override + String get broadcastDownloadAllRounds => 'Shkarko krejt raundet'; + + @override + String get broadcastResetRound => 'Reset this round'; + + @override + String get broadcastDeleteRound => 'Fshije këtë raund'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Fshije përfundimisht raundin dhe lojërat e tij.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Fshi krejt lojërat e këtij raundi. Burimi do të duhet të jetë aktiv, që të mund të rikrijohen ato.'; + + @override + String get broadcastEditRoundStudy => 'Përpunoni analizë raundi'; + + @override + String get broadcastDeleteTournament => 'Fshije këtë turne'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Fshihe përfundimisht krejt turneun, krejt raundet e tij dhe krejt lojërat në të.'; + + @override + String get broadcastShowScores => 'Shfaq pikë lojtatësh bazuar në përfundime lojërash'; + + @override + String get broadcastReplacePlayerTags => 'Opsionale: zëvendësoni emra lojëtarësh, vlerësime dhe tituj'; + + @override + String get broadcastFideFederations => 'Federata FIDE'; + + @override + String get broadcastTop10Rating => '10 vlerësimet kryesuese'; + + @override + String get broadcastFidePlayers => 'Lojtarë FIDE'; + + @override + String get broadcastFidePlayerNotFound => 'S’u gjet lojtar FIDE'; + + @override + String get broadcastFideProfile => 'Profil FIDE'; + + @override + String get broadcastFederation => 'Federim'; + + @override + String get broadcastAgeThisYear => 'Moshë këtë vit'; + + @override + String get broadcastUnrated => 'Pa pikë'; + + @override + String get broadcastRecentTournaments => 'Turne së fundi'; + + @override + String get broadcastOpenLichess => 'Hape në Lichess'; + + @override + String get broadcastTeams => 'Ekipe'; + + @override + String get broadcastBoards => 'Fusha'; + + @override + String get broadcastOverview => 'Përmbledhje'; + + @override + String get broadcastSubscribeTitle => 'Pajtohuni, që të noftoheni se kur fillon çdo raund. Mund të aktivizoni/çaktivizoni zilen, ose njoftimet “push” për transmetime, që nga parapëlqimet për llogarinë tuaj.'; + + @override + String get broadcastUploadImage => 'Ngarkoni figurë turneu'; + + @override + String get broadcastNoBoardsYet => 'Ende pa fusha. Këto do të shfaqen sapo të ngrkohen lojërat.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Fushat mund të ngarkohen me një burim, ose përmes $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Fillon pas $param'; + } + + @override + String get broadcastStartVerySoon => 'Transmetimi do të fillojë shumë shpejt.'; + + @override + String get broadcastNotYetStarted => 'Transmetimi s’ka filluar ende.'; + + @override + String get broadcastOfficialWebsite => 'Sajti zyrtar'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'Më tepër mundësi te $param'; + } + + @override + String get broadcastWebmastersPage => 'faqe webmaster-ësh'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'Një burim publik,, PGN, i atypëratyshëm për këtë raund. Ofrojmë gjithashtu edhe një $param, për njëkohësim më të shpejtë dhe më efikas.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Trupëzojeni këtë transmetim në sajtin tuaj'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Trupëzojeni $param në sajtin tuaj'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Lojëra në këtë turne'; + + @override + String get broadcastScore => 'Përfundim'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count transmetime', + one: '$count transmetim', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Challenges: $param1'; @@ -727,7 +952,7 @@ class AppLocalizationsSq extends AppLocalizations { String get preferencesNotifyGameEvent => 'Përditësime loje me korrespondencë'; @override - String get preferencesNotifyChallenge => 'Challenges'; + String get preferencesNotifyChallenge => 'Sfida'; @override String get preferencesNotifyTournamentSoon => 'Turne që fillon së shpejti'; @@ -1390,10 +1615,10 @@ class AppLocalizationsSq extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Kundërshtari është i kufizuar në lëvizjet që mund të bëjë dhe krejt lëvizjet përkeqësojnë pozicionin e tij.'; @override - String get puzzleThemeHealthyMix => 'Përzierje e ushtrimeve'; + String get puzzleThemeMix => 'Ushtrime të përzierë'; @override - String get puzzleThemeHealthyMixDescription => 'Pak nga të gjitha. S’dini ç’të prisni, ndaj mbeteni gati për gjithçka! Mu si në lojëra të njëmendta.'; + String get puzzleThemeMixDescription => 'Pak nga të gjitha. S’dini ç’të prisni, ndaj mbeteni gati për gjithçka! Mu si në lojëra të njëmendta.'; @override String get puzzleThemePlayerGames => 'Lojëra të lojëtarit'; @@ -1797,9 +2022,6 @@ class AppLocalizationsSq extends AppLocalizations { @override String get removesTheDepthLimit => 'Heq kufirin e thellësisë dhe e mban të ngrohtë kompjuterin tuaj'; - @override - String get engineManager => 'Engine manager'; - @override String get blunder => 'Gafë'; @@ -2063,6 +2285,9 @@ class AppLocalizationsSq extends AppLocalizations { @override String get gamesPlayed => 'Lojëra të luajtura'; + @override + String get ok => 'OK'; + @override String get cancel => 'Anuloje'; @@ -2772,7 +2997,13 @@ class AppLocalizationsSq extends AppLocalizations { String get other => 'Tjetër'; @override - String get reportDescriptionHelp => 'Ngjitni lidhjen për te loja(ra) dhe shpjegoni çfarë nuk shkon me sjelljen e këtij përdoruesi. Mos shkruani thjesht “mashtrojnë”, por na tregoni si mbërritët në këtë përfundim. Raportimi juaj do të përpunohet më shpejt, nëse shkruhet në anglisht.'; + String get reportCheatBoostHelp => 'Ngjitni lidhjen për te loja(rat) dhe shpjegoni se ç’nuk shkon me sjelljen e këtij përdoruesi. Mos thoni thjesht “bën me hile”, por na tregoni se si arritët në këtë konkluzion.'; + + @override + String get reportUsernameHelp => 'Shpjegoni pse ky emër përdoruesi është fyes. Mos thoni thjesht “është fyes/i papërshtatshëm”, por na tregoni se si arritët në këtë konkluzion, veçanërisht nëse fyerja është e hollë, jo në anglisht, është në një slang, ose është një referencë historike/kulturore.'; + + @override + String get reportProcessedFasterInEnglish => 'Raportimi juaj do të shqyrtohet më shpejt, nëse është shkruar në anglisht.'; @override String get error_provideOneCheatedGameLink => 'Ju lutemi, jepni të paktën një lidhje te një lojë me hile.'; @@ -4077,6 +4308,9 @@ class AppLocalizationsSq extends AppLocalizations { @override String get nothingToSeeHere => 'S’ka ç’shihet këtu tani.'; + @override + String get stats => 'Statistika'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4723,9 +4957,514 @@ class AppLocalizationsSq extends AppLocalizations { @override String get streamerLichessStreamers => 'Transmetues Lichess-i'; + @override + String get studyPrivate => 'Privat'; + + @override + String get studyMyStudies => 'Mësimet e mia'; + + @override + String get studyStudiesIContributeTo => 'Mësimet në të cilat kam kontribuar'; + + @override + String get studyMyPublicStudies => 'Mësimet e mia publike'; + + @override + String get studyMyPrivateStudies => 'Mësimet e mia private'; + + @override + String get studyMyFavoriteStudies => 'Mësimet e mia të parapëlqyera'; + + @override + String get studyWhatAreStudies => 'Ç’janë mësimet?'; + + @override + String get studyAllStudies => 'Krejt mësimet'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Mësime të krijuara nga $param'; + } + + @override + String get studyNoneYet => 'Ende asnjë.'; + + @override + String get studyHot => 'Më aktualet'; + + @override + String get studyDateAddedNewest => 'Data e krijimit (nga më e reja)'; + + @override + String get studyDateAddedOldest => 'Data e krijimit (nga më e vjetra)'; + + @override + String get studyRecentlyUpdated => 'E përditësuar së fundmi'; + + @override + String get studyMostPopular => 'Më populloret'; + + @override + String get studyAlphabetical => 'Alfabetik'; + + @override + String get studyAddNewChapter => 'Shto një kapitull të ri'; + + @override + String get studyAddMembers => 'Shto anëtarë'; + + @override + String get studyInviteToTheStudy => 'Ftoni në mësim'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Ju lutemi, ftoni vetëm njerëzit që i njihni dhe që duan vërtet të marrin pjesë në këtë mësim.'; + + @override + String get studySearchByUsername => 'Kërkoni sipas emrit të përdoruesit'; + + @override + String get studySpectator => 'Shikues'; + + @override + String get studyContributor => 'Kontribues'; + + @override + String get studyKick => 'Përjashtoje'; + + @override + String get studyLeaveTheStudy => 'Braktisni mësimin'; + + @override + String get studyYouAreNowAContributor => 'Tani jeni një kontribues'; + + @override + String get studyYouAreNowASpectator => 'Tani jeni shikues'; + + @override + String get studyPgnTags => 'Etiketa PGN'; + + @override + String get studyLike => 'Pëlqejeni'; + + @override + String get studyUnlike => 'Shpëlqejeni'; + + @override + String get studyNewTag => 'Etiketë e re'; + + @override + String get studyCommentThisPosition => 'Komentoni këtë pozicion'; + + @override + String get studyCommentThisMove => 'Komentoni këtë lëvizje'; + + @override + String get studyAnnotateWithGlyphs => 'Shenjoni me karaktere'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Ky kapitull është shumë i shkurtë për t’u analizuar.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Analizë kompjuterike mund të kërkohet vetëm nga kontribuesit e këtij mësimi.'; + + @override + String get studyGetAFullComputerAnalysis => 'Merrni nga shërbyesi një analizë të plotë kompjuterike të variantit kryesor.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Sigurohuni që kapitulli të jetë i plotë. Mund të kërkoni analizë vetëm një herë.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Krejt anëtarët SYNC mbesin në të njëjtin pozicion'; + + @override + String get studyShareChanges => 'Ndani ndryshimet me shikuesit dhe ruajini ato në shërbyes'; + + @override + String get studyPlaying => 'Po luhet'; + + @override + String get studyShowEvalBar => 'Shtylla vlerësimi'; + + @override + String get studyFirst => 'E para'; + + @override + String get studyPrevious => 'E mëparshmja'; + + @override + String get studyNext => 'Pasuesja'; + + @override + String get studyLast => 'E fundit'; + @override String get studyShareAndExport => 'Ndajeni me të tjerë & eksportoni'; + @override + String get studyCloneStudy => 'Klonoje'; + + @override + String get studyStudyPgn => 'Studioni PGN'; + + @override + String get studyDownloadAllGames => 'Shkarkoji krejt lojërat'; + + @override + String get studyChapterPgn => 'PGN e kapitullit'; + + @override + String get studyCopyChapterPgn => 'Kopjo PGN'; + + @override + String get studyDownloadGame => 'Shkarko lojën'; + + @override + String get studyStudyUrl => 'URL Mësimi'; + + @override + String get studyCurrentChapterUrl => 'URL e Kapitullit Aktual'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Këtë mund ta ngjitni te forumi ose blogu juaj Lichess, për ta trupëzuar'; + + @override + String get studyStartAtInitialPosition => 'Fillo në pozicionin fillestar'; + + @override + String studyStartAtX(String param) { + return 'Fillo tek $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Trupëzojeni te sajti juaj'; + + @override + String get studyReadMoreAboutEmbedding => 'Lexoni më tepër rreth trupëzimit'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Mund të trupëzoni vetëm mësime publike!'; + + @override + String get studyOpen => 'Hap'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1, sjellë për ju nga $param2'; + } + + @override + String get studyStudyNotFound => 'Mësimi s’u gjet'; + + @override + String get studyEditChapter => 'Përpunoni kapitullin'; + + @override + String get studyNewChapter => 'Kapitull i ri'; + + @override + String studyImportFromChapterX(String param) { + return 'Importo prej $param'; + } + + @override + String get studyOrientation => 'Drejtimi'; + + @override + String get studyAnalysisMode => 'Mënyra Analizim'; + + @override + String get studyPinnedChapterComment => 'Koment kapitulli i fiksuar'; + + @override + String get studySaveChapter => 'Ruaje kapitullin'; + + @override + String get studyClearAnnotations => 'Spastro shënimet'; + + @override + String get studyClearVariations => 'Spastroji variantet'; + + @override + String get studyDeleteChapter => 'Fshije kapitullin'; + + @override + String get studyDeleteThisChapter => 'Të fshihet ky kapitull? S’ka kthim mbrapa!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Të spastrohen krejt komentet, glifet dhe format e vizatuara në këtë kapitull?'; + + @override + String get studyRightUnderTheBoard => 'Mu nën fushë'; + + @override + String get studyNoPinnedComment => 'Asnjë'; + + @override + String get studyNormalAnalysis => 'Analizë normale'; + + @override + String get studyHideNextMoves => 'Fshih lëvizjen e radhës'; + + @override + String get studyInteractiveLesson => 'Mësim me ndërveprim'; + + @override + String studyChapterX(String param) { + return 'Kapitulli $param'; + } + + @override + String get studyEmpty => 'E zbrazët'; + + @override + String get studyStartFromInitialPosition => 'Fillo nga pozicioni fillestar'; + + @override + String get studyEditor => 'Përpunues'; + + @override + String get studyStartFromCustomPosition => 'Fillo nga pozicion vetjak'; + + @override + String get studyLoadAGameByUrl => 'Ngarko lojëra nga URL'; + + @override + String get studyLoadAPositionFromFen => 'Ngarko pozicionin nga FEN'; + + @override + String get studyLoadAGameFromPgn => 'Ngarko lojëra nga PGN'; + + @override + String get studyAutomatic => 'Automatik'; + + @override + String get studyUrlOfTheGame => 'URL e lojërave, një për rresht'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Ngarko lojëra nga $param1 ose $param2'; + } + + @override + String get studyCreateChapter => 'Krijo kapitull'; + + @override + String get studyCreateStudy => 'Krijoni mësim'; + + @override + String get studyEditStudy => 'Përpunoni mësimin'; + + @override + String get studyVisibility => 'Dukshmëri'; + + @override + String get studyPublic => 'Publike'; + + @override + String get studyUnlisted => 'Jo në listë'; + + @override + String get studyInviteOnly => 'Vetëm me ftesa'; + + @override + String get studyAllowCloning => 'Lejo klonimin'; + + @override + String get studyNobody => 'Askush'; + + @override + String get studyOnlyMe => 'Vetëm unë'; + + @override + String get studyContributors => 'Kontribues'; + + @override + String get studyMembers => 'Anëtarë'; + + @override + String get studyEveryone => 'Cilido'; + + @override + String get studyEnableSync => 'Lejo njëkohësim'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Po: mbaje këdo në të njëjtin pozicion'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Jo: lejoji njerëzit të shfletojnë lirisht'; + + @override + String get studyPinnedStudyComment => 'Koment studimi i fiksuar'; + @override String get studyStart => 'Fillo'; + + @override + String get studySave => 'Ruaje'; + + @override + String get studyClearChat => 'Spastroje bisedën'; + + @override + String get studyDeleteTheStudyChatHistory => 'Të fshihet historiku i fjalosjeve të mësimit? S’ka kthim mbrapa!'; + + @override + String get studyDeleteStudy => 'Fshije mësimin'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Të fshihet krejt mësimi? S’ka kthim mbrapa! Për ta ripohuar, shtypni emrin e mësimit: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Ku doni ta studioni atë?'; + + @override + String get studyGoodMove => 'Lëvizje e mirë'; + + @override + String get studyMistake => 'Gabim'; + + @override + String get studyBrilliantMove => 'Lëvizje e shkëlqyer'; + + @override + String get studyBlunder => 'Gafë'; + + @override + String get studyInterestingMove => 'Lëvizje me interes'; + + @override + String get studyDubiousMove => 'Lëvizje e dyshimtë'; + + @override + String get studyOnlyMove => 'Lëvizja e vetme'; + + @override + String get studyZugzwang => 'Zugzwang'; + + @override + String get studyEqualPosition => 'është baras me'; + + @override + String get studyUnclearPosition => 'Shenjë gishti e paqartë'; + + @override + String get studyWhiteIsSlightlyBetter => 'I bardhi është pakëz më mirë'; + + @override + String get studyBlackIsSlightlyBetter => 'I ziu është pakëz më mirë'; + + @override + String get studyWhiteIsBetter => 'I bardhi është më mirë'; + + @override + String get studyBlackIsBetter => 'I ziu është më mirë'; + + @override + String get studyWhiteIsWinning => 'I bardhi po fiton'; + + @override + String get studyBlackIsWinning => 'I ziu po fiton'; + + @override + String get studyNovelty => 'Risi'; + + @override + String get studyDevelopment => 'Zhvillim'; + + @override + String get studyInitiative => 'Nismë'; + + @override + String get studyAttack => 'Sulm'; + + @override + String get studyCounterplay => 'Kundërsulm'; + + @override + String get studyTimeTrouble => 'Probleme me këtë instalim?'; + + @override + String get studyWithCompensation => 'Me kompesim'; + + @override + String get studyWithTheIdea => 'Me idenë'; + + @override + String get studyNextChapter => 'Kapitulli pasues'; + + @override + String get studyPrevChapter => 'Kapitulli i mëparshëm'; + + @override + String get studyStudyActions => 'Studioni veprimet'; + + @override + String get studyTopics => 'Tema'; + + @override + String get studyMyTopics => 'Temat e mia'; + + @override + String get studyPopularTopics => 'Tema popullore'; + + @override + String get studyManageTopics => 'Administroni tema'; + + @override + String get studyBack => 'Mbrapsht'; + + @override + String get studyPlayAgain => 'Riluaje'; + + @override + String get studyWhatWouldYouPlay => 'Ç’lëvizje do të bënit në këtë pozicion?'; + + @override + String get studyYouCompletedThisLesson => 'Përgëzime! E mbaruat këtë mësim.'; + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Kapituj', + one: '$count Kapitull', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Lojëra', + one: '$count Lojë', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Anëtarë', + one: '$count Anëtar', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Hidhni këtu tekstin e PGN-s tuaj, deri në $count lojëra', + one: 'Hidhni këtu tekstin e PGN-s tuaj, deri në $count lojë', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_sr.dart b/lib/l10n/l10n_sr.dart index 8fdff22f73..8977a23734 100644 --- a/lib/l10n/l10n_sr.dart +++ b/lib/l10n/l10n_sr.dart @@ -103,9 +103,6 @@ class AppLocalizationsSr extends AppLocalizations { @override String get mobileCancelTakebackOffer => 'Cancel takeback offer'; - @override - String get mobileCancelDrawOffer => 'Cancel draw offer'; - @override String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @@ -254,6 +251,17 @@ class AppLocalizationsSr extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Completed $count $param2 correspondence games', + one: 'Completed $count $param2 correspondence game', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -364,9 +372,226 @@ class AppLocalizationsSr extends AppLocalizations { @override String get broadcastBroadcasts => 'Емитовања'; + @override + String get broadcastMyBroadcasts => 'My broadcasts'; + @override String get broadcastLiveBroadcasts => 'Уживо емитовање турнира'; + @override + String get broadcastBroadcastCalendar => 'Broadcast calendar'; + + @override + String get broadcastNewBroadcast => 'Нова ужива емитовања'; + + @override + String get broadcastSubscribedBroadcasts => 'Subscribed broadcasts'; + + @override + String get broadcastAboutBroadcasts => 'About broadcasts'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'How to use Lichess Broadcasts.'; + + @override + String get broadcastTheNewRoundHelp => 'The new round will have the same members and contributors as the previous one.'; + + @override + String get broadcastAddRound => 'Add a round'; + + @override + String get broadcastOngoing => 'Текућа'; + + @override + String get broadcastUpcoming => 'Предстојећа'; + + @override + String get broadcastCompleted => 'Завршена'; + + @override + String get broadcastCompletedHelp => 'Lichess detects round completion, but can get it wrong. Use this to set it manually.'; + + @override + String get broadcastRoundName => 'Round name'; + + @override + String get broadcastRoundNumber => 'Број рунде'; + + @override + String get broadcastTournamentName => 'Tournament name'; + + @override + String get broadcastTournamentDescription => 'Short tournament description'; + + @override + String get broadcastFullDescription => 'Цео опис догађаја'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Optional long description of the tournament. $param1 is available. Length must be less than $param2 characters.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN Source URL'; + + @override + String get broadcastSourceUrlHelp => 'URL that Lichess will check to get PGN updates. It must be publicly accessible from the Internet.'; + + @override + String get broadcastSourceGameIds => 'Up to 64 Lichess game IDs, separated by spaces.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Start date in the tournament local timezone: $param'; + } + + @override + String get broadcastStartDateHelp => 'Optional, if you know when the event starts'; + + @override + String get broadcastCurrentGameUrl => 'Current game URL'; + + @override + String get broadcastDownloadAllRounds => 'Download all rounds'; + + @override + String get broadcastResetRound => 'Reset this round'; + + @override + String get broadcastDeleteRound => 'Delete this round'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Definitively delete the round and all its games.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Delete all games of this round. The source will need to be active in order to re-create them.'; + + @override + String get broadcastEditRoundStudy => 'Edit round study'; + + @override + String get broadcastDeleteTournament => 'Delete this tournament'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Definitively delete the entire tournament, all its rounds and all its games.'; + + @override + String get broadcastShowScores => 'Show players scores based on game results'; + + @override + String get broadcastReplacePlayerTags => 'Optional: replace player names, ratings and titles'; + + @override + String get broadcastFideFederations => 'FIDE federations'; + + @override + String get broadcastTop10Rating => 'Top 10 rating'; + + @override + String get broadcastFidePlayers => 'FIDE players'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE player not found'; + + @override + String get broadcastFideProfile => 'FIDE profile'; + + @override + String get broadcastFederation => 'Federation'; + + @override + String get broadcastAgeThisYear => 'Age this year'; + + @override + String get broadcastUnrated => 'Unrated'; + + @override + String get broadcastRecentTournaments => 'Recent tournaments'; + + @override + String get broadcastOpenLichess => 'Open in Lichess'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Overview'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Official website'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'Score'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count broadcasts', + one: '$count broadcast', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Challenges: $param1'; @@ -1405,10 +1630,10 @@ class AppLocalizationsSr extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Противник има ограничен избор потеза и сваким потезом погоршава своју позицију.'; @override - String get puzzleThemeHealthyMix => 'Здрава мешавина'; + String get puzzleThemeMix => 'Здрава мешавина'; @override - String get puzzleThemeHealthyMixDescription => 'Свега по мало. Не знаш шта да очекујеш, па остајеш спреман за све! Баш као у правим партијама.'; + String get puzzleThemeMixDescription => 'Свега по мало. Не знаш шта да очекујеш, па остајеш спреман за све! Баш као у правим партијама.'; @override String get puzzleThemePlayerGames => 'Играчеве партије'; @@ -1812,9 +2037,6 @@ class AppLocalizationsSr extends AppLocalizations { @override String get removesTheDepthLimit => 'Уклања ограничење дубине и греје рачунар'; - @override - String get engineManager => 'Менаџер машине'; - @override String get blunder => 'Груба грешка'; @@ -2078,6 +2300,9 @@ class AppLocalizationsSr extends AppLocalizations { @override String get gamesPlayed => 'Број одиграних партија'; + @override + String get ok => 'OK'; + @override String get cancel => 'Откажи'; @@ -2787,7 +3012,13 @@ class AppLocalizationsSr extends AppLocalizations { String get other => 'Остало'; @override - String get reportDescriptionHelp => 'Залијепите везу до игре и објасните шта није у реду са понашањем корисника. Немојте само рећи \"варао\", али реците како сте дошли до тог закључка. Ваша пријава ће бити обрађена брже ако је напишете на енглеском језику.'; + String get reportCheatBoostHelp => 'Paste the link to the game(s) and explain what is wrong about this user\'s behaviour. Don\'t just say \"they cheat\", but tell us how you came to this conclusion.'; + + @override + String get reportUsernameHelp => 'Explain what about this username is offensive. Don\'t just say \"it\'s offensive/inappropriate\", but tell us how you came to this conclusion, especially if the insult is obfuscated, not in english, is in slang, or is a historical/cultural reference.'; + + @override + String get reportProcessedFasterInEnglish => 'Your report will be processed faster if written in English.'; @override String get error_provideOneCheatedGameLink => 'Наведите барем једну везу игре у којој је играч варао.'; @@ -4092,6 +4323,9 @@ class AppLocalizationsSr extends AppLocalizations { @override String get nothingToSeeHere => 'Nothing to see here at the moment.'; + @override + String get stats => 'Stats'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4777,9 +5011,518 @@ class AppLocalizationsSr extends AppLocalizations { @override String get streamerLichessStreamers => 'Личес стримери'; + @override + String get studyPrivate => 'Приватна'; + + @override + String get studyMyStudies => 'Моје студије'; + + @override + String get studyStudiesIContributeTo => 'Студије којима доприносим'; + + @override + String get studyMyPublicStudies => 'Моје јавне студије'; + + @override + String get studyMyPrivateStudies => 'Моје приватне студије'; + + @override + String get studyMyFavoriteStudies => 'Моје омиљене студије'; + + @override + String get studyWhatAreStudies => 'Шта су студије?'; + + @override + String get studyAllStudies => 'Све студије'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Студије које је $param направио/ла'; + } + + @override + String get studyNoneYet => 'Ниједна за сад.'; + + @override + String get studyHot => 'У тренду'; + + @override + String get studyDateAddedNewest => 'Датум додавања (најновије)'; + + @override + String get studyDateAddedOldest => 'Датум додавања (најстарије)'; + + @override + String get studyRecentlyUpdated => 'Недавно ажуриране'; + + @override + String get studyMostPopular => 'Најпопуларније'; + + @override + String get studyAlphabetical => 'Alphabetical'; + + @override + String get studyAddNewChapter => 'Додајте ново поглавље'; + + @override + String get studyAddMembers => 'Додај чланове'; + + @override + String get studyInviteToTheStudy => 'Позовите у студију'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Молимо вас да само позивате људе које познајете и који активно желе да се придруже овој студији.'; + + @override + String get studySearchByUsername => 'Претражујте по корисничком имену'; + + @override + String get studySpectator => 'Посматрач'; + + @override + String get studyContributor => 'Cарадник'; + + @override + String get studyKick => 'Избаци'; + + @override + String get studyLeaveTheStudy => 'Напусти студију'; + + @override + String get studyYouAreNowAContributor => 'Сада сте сарадник'; + + @override + String get studyYouAreNowASpectator => 'Сада сте посматрач'; + + @override + String get studyPgnTags => 'PGN ознаке'; + + @override + String get studyLike => 'Свиђа ми се'; + + @override + String get studyUnlike => 'Unlike'; + + @override + String get studyNewTag => 'Нова ознака'; + + @override + String get studyCommentThisPosition => 'Прокоментаришите ову позицију'; + + @override + String get studyCommentThisMove => 'Прокоментаришите овај потез'; + + @override + String get studyAnnotateWithGlyphs => 'Прибележите глифовима'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Поглавље је прекратко за анализу.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Само сарадници у студији могу захтевати рачунарску анализу.'; + + @override + String get studyGetAFullComputerAnalysis => 'Добијте потпуну рачунарску анализу главне варијације од стране сервера.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Побрините се да је поглавље завршено. Само једном можете захтевати анализу.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Сви SYNC чланови остају на истој позицији'; + + @override + String get studyShareChanges => 'Делите измене са посматрачима и сачувајте их на сервер'; + + @override + String get studyPlaying => 'У току'; + + @override + String get studyShowEvalBar => 'Evaluation bars'; + + @override + String get studyFirst => 'Прва'; + + @override + String get studyPrevious => 'Претходна'; + + @override + String get studyNext => 'Следећа'; + + @override + String get studyLast => 'Последња'; + @override String get studyShareAndExport => 'Подели и извези'; + @override + String get studyCloneStudy => 'Клонирај'; + + @override + String get studyStudyPgn => 'PGN студије'; + + @override + String get studyDownloadAllGames => 'Преузми све партије'; + + @override + String get studyChapterPgn => 'PGN поглавља'; + + @override + String get studyCopyChapterPgn => 'Copy PGN'; + + @override + String get studyDownloadGame => 'Преузми партију'; + + @override + String get studyStudyUrl => 'Линк студије'; + + @override + String get studyCurrentChapterUrl => 'Линк тренутног поглавља'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Ово можете налепити у форум да уградите'; + + @override + String get studyStartAtInitialPosition => 'Започни на иницијалној позицији'; + + @override + String studyStartAtX(String param) { + return 'Започни на $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Угради у свој сајт или блог'; + + @override + String get studyReadMoreAboutEmbedding => 'Прочитај више о уграђивању'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Само јавне студије могу бити уграђене!'; + + @override + String get studyOpen => 'Отворите'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param2 Вам доноси $param1'; + } + + @override + String get studyStudyNotFound => 'Студија није пронађена'; + + @override + String get studyEditChapter => 'Измени поглавље'; + + @override + String get studyNewChapter => 'Ново поглавље'; + + @override + String studyImportFromChapterX(String param) { + return 'Import from $param'; + } + + @override + String get studyOrientation => 'Оријентација'; + + @override + String get studyAnalysisMode => 'Врста анализе'; + + @override + String get studyPinnedChapterComment => 'Закачен коментар поглавља'; + + @override + String get studySaveChapter => 'Сачувај поглавље'; + + @override + String get studyClearAnnotations => 'Избриши анотације'; + + @override + String get studyClearVariations => 'Clear variations'; + + @override + String get studyDeleteChapter => 'Избриши поглавље'; + + @override + String get studyDeleteThisChapter => 'Избриши ово поглавље? Нема повратка назад!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Избриши све коментаре, глифове и нацртане облике у овом поглављу?'; + + @override + String get studyRightUnderTheBoard => 'Одмах испод табле'; + + @override + String get studyNoPinnedComment => 'Ниједан'; + + @override + String get studyNormalAnalysis => 'Нормална анализа'; + + @override + String get studyHideNextMoves => 'Сакриј следеће потезе'; + + @override + String get studyInteractiveLesson => 'Интерактивна лекција'; + + @override + String studyChapterX(String param) { + return 'Поглавље $param'; + } + + @override + String get studyEmpty => 'Празно'; + + @override + String get studyStartFromInitialPosition => 'Започните од иницијалне позиције'; + + @override + String get studyEditor => 'Уређивач'; + + @override + String get studyStartFromCustomPosition => 'Започните од жељене позиције'; + + @override + String get studyLoadAGameByUrl => 'Учитајте партије преко линкова'; + + @override + String get studyLoadAPositionFromFen => 'Учитајте позицију из FEN-а'; + + @override + String get studyLoadAGameFromPgn => 'Учитајте партију из PGN-а'; + + @override + String get studyAutomatic => 'Аутоматски'; + + @override + String get studyUrlOfTheGame => 'Линкови партија, једна по реду'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Учитајте партије са $param1 или $param2'; + } + + @override + String get studyCreateChapter => 'Направи поглавље'; + + @override + String get studyCreateStudy => 'Направи студију'; + + @override + String get studyEditStudy => 'Измени студију'; + + @override + String get studyVisibility => 'Видљивост'; + + @override + String get studyPublic => 'Јавно'; + + @override + String get studyUnlisted => 'Неприказано'; + + @override + String get studyInviteOnly => 'Само по позиву'; + + @override + String get studyAllowCloning => 'Дозволите клонирање'; + + @override + String get studyNobody => 'Нико'; + + @override + String get studyOnlyMe => 'Само ја'; + + @override + String get studyContributors => 'Сарадници'; + + @override + String get studyMembers => 'Чланови'; + + @override + String get studyEveryone => 'Сви'; + + @override + String get studyEnableSync => 'Омогући синхронизацију'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Да: задржи све на истој позицији'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Не: дозволи људима да слободно прегледају'; + + @override + String get studyPinnedStudyComment => 'Закачен коментар студије'; + @override String get studyStart => 'Започни'; + + @override + String get studySave => 'Сачувај'; + + @override + String get studyClearChat => 'Очисти ћаскање'; + + @override + String get studyDeleteTheStudyChatHistory => 'Избриши историју ћаскања студије? Нема повратка назад!'; + + @override + String get studyDeleteStudy => 'Избриши студију'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Delete the entire study? There is no going back! Type the name of the study to confirm: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Где желите то проучити?'; + + @override + String get studyGoodMove => 'Good move'; + + @override + String get studyMistake => 'Mistake'; + + @override + String get studyBrilliantMove => 'Brilliant move'; + + @override + String get studyBlunder => 'Blunder'; + + @override + String get studyInterestingMove => 'Interesting move'; + + @override + String get studyDubiousMove => 'Dubious move'; + + @override + String get studyOnlyMove => 'Only move'; + + @override + String get studyZugzwang => 'Zugzwang'; + + @override + String get studyEqualPosition => 'Equal position'; + + @override + String get studyUnclearPosition => 'Unclear position'; + + @override + String get studyWhiteIsSlightlyBetter => 'White is slightly better'; + + @override + String get studyBlackIsSlightlyBetter => 'Black is slightly better'; + + @override + String get studyWhiteIsBetter => 'White is better'; + + @override + String get studyBlackIsBetter => 'Black is better'; + + @override + String get studyWhiteIsWinning => 'White is winning'; + + @override + String get studyBlackIsWinning => 'Black is winning'; + + @override + String get studyNovelty => 'Novelty'; + + @override + String get studyDevelopment => 'Development'; + + @override + String get studyInitiative => 'Initiative'; + + @override + String get studyAttack => 'Attack'; + + @override + String get studyCounterplay => 'Counterplay'; + + @override + String get studyTimeTrouble => 'Time trouble'; + + @override + String get studyWithCompensation => 'With compensation'; + + @override + String get studyWithTheIdea => 'With the idea'; + + @override + String get studyNextChapter => 'Next chapter'; + + @override + String get studyPrevChapter => 'Previous chapter'; + + @override + String get studyStudyActions => 'Study actions'; + + @override + String get studyTopics => 'Topics'; + + @override + String get studyMyTopics => 'My topics'; + + @override + String get studyPopularTopics => 'Popular topics'; + + @override + String get studyManageTopics => 'Manage topics'; + + @override + String get studyBack => 'Back'; + + @override + String get studyPlayAgain => 'Play again'; + + @override + String get studyWhatWouldYouPlay => 'What would you play in this position?'; + + @override + String get studyYouCompletedThisLesson => 'Congratulations! You completed this lesson.'; + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Поглављa', + few: '$count Поглављa', + one: '$count Поглавље', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Партија', + few: '$count Партијe', + one: '$count Партија', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Чланова', + few: '$count Чланa', + one: '$count Члан', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Налепите свој PGN текст овде, до $count партија', + few: 'Налепите свој PGN текст овде, до $count партије', + one: 'Налепите свој PGN текст овде, до $count партије', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_sv.dart b/lib/l10n/l10n_sv.dart index 592ce18f9a..0012a34527 100644 --- a/lib/l10n/l10n_sv.dart +++ b/lib/l10n/l10n_sv.dart @@ -9,16 +9,16 @@ class AppLocalizationsSv extends AppLocalizations { AppLocalizationsSv([String locale = 'sv']) : super(locale); @override - String get mobileHomeTab => 'Home'; + String get mobileHomeTab => 'Hem'; @override - String get mobilePuzzlesTab => 'Puzzles'; + String get mobilePuzzlesTab => 'Problem'; @override - String get mobileToolsTab => 'Tools'; + String get mobileToolsTab => 'Verktyg'; @override - String get mobileWatchTab => 'Watch'; + String get mobileWatchTab => 'Titta'; @override String get mobileSettingsTab => 'Settings'; @@ -27,7 +27,7 @@ class AppLocalizationsSv extends AppLocalizations { String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; @override - String get mobileSystemColors => 'System colors'; + String get mobileSystemColors => 'Systemets färger'; @override String get mobileFeedbackButton => 'Feedback'; @@ -48,24 +48,24 @@ class AppLocalizationsSv extends AppLocalizations { String get mobileNotFollowingAnyUser => 'You are not following any user.'; @override - String get mobileAllGames => 'All games'; + String get mobileAllGames => 'Alla spel'; @override - String get mobileRecentSearches => 'Recent searches'; + String get mobileRecentSearches => 'Senaste sökningar'; @override - String get mobileClearButton => 'Clear'; + String get mobileClearButton => 'Rensa'; @override String mobilePlayersMatchingSearchTerm(String param) { - return 'Players with \"$param\"'; + return 'Spelare med \"$param\"'; } @override - String get mobileNoSearchResults => 'No results'; + String get mobileNoSearchResults => 'Inga resultat'; @override - String get mobileAreYouSure => 'Are you sure?'; + String get mobileAreYouSure => 'Är du säker?'; @override String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; @@ -74,25 +74,25 @@ class AppLocalizationsSv extends AppLocalizations { String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Share this puzzle'; + String get mobileSharePuzzle => 'Dela detta schackproblem'; @override - String get mobileShareGameURL => 'Share game URL'; + String get mobileShareGameURL => 'Dela parti-URL'; @override - String get mobileShareGamePGN => 'Share PGN'; + String get mobileShareGamePGN => 'Dela PGN'; @override String get mobileSharePositionAsFEN => 'Share position as FEN'; @override - String get mobileShowVariations => 'Show variations'; + String get mobileShowVariations => 'Visa variationer'; @override - String get mobileHideVariation => 'Hide variation'; + String get mobileHideVariation => 'Dölj variationer'; @override - String get mobileShowComments => 'Show comments'; + String get mobileShowComments => 'Visa kommentarer'; @override String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; @@ -103,29 +103,26 @@ class AppLocalizationsSv extends AppLocalizations { @override String get mobileCancelTakebackOffer => 'Cancel takeback offer'; - @override - String get mobileCancelDrawOffer => 'Cancel draw offer'; - @override String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get mobileBlindfoldMode => 'I blindo'; @override String get mobileLiveStreamers => 'Live streamers'; @override - String get mobileCustomGameJoinAGame => 'Join a game'; + String get mobileCustomGameJoinAGame => 'Gå med i spel'; @override String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; @override - String get mobileSomethingWentWrong => 'Something went wrong.'; + String get mobileSomethingWentWrong => 'Något gick fel.'; @override - String get mobileShowResult => 'Show result'; + String get mobileShowResult => 'Visa resultat'; @override String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; @@ -135,11 +132,11 @@ class AppLocalizationsSv extends AppLocalizations { @override String mobileGreeting(String param) { - return 'Hello, $param'; + return 'Hej $param'; } @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileGreetingWithoutName => 'Hej'; @override String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; @@ -246,6 +243,17 @@ class AppLocalizationsSv extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Completed $count $param2 correspondence games', + one: 'Completed $count $param2 correspondence game', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,226 @@ class AppLocalizationsSv extends AppLocalizations { @override String get broadcastBroadcasts => 'Sändningar'; + @override + String get broadcastMyBroadcasts => 'Mina sändningar'; + @override String get broadcastLiveBroadcasts => 'Direktsända turneringar'; + @override + String get broadcastBroadcastCalendar => 'Broadcast calendar'; + + @override + String get broadcastNewBroadcast => 'Ny direktsändning'; + + @override + String get broadcastSubscribedBroadcasts => 'Subscribed broadcasts'; + + @override + String get broadcastAboutBroadcasts => 'Om sändningar'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Hur man använder Lichess-Sändningar.'; + + @override + String get broadcastTheNewRoundHelp => 'Den nya rundan kommer att ha samma medlemmar och bidragsgivare som den föregående.'; + + @override + String get broadcastAddRound => 'Lägg till en omgång'; + + @override + String get broadcastOngoing => 'Pågående'; + + @override + String get broadcastUpcoming => 'Kommande'; + + @override + String get broadcastCompleted => 'Slutförda'; + + @override + String get broadcastCompletedHelp => 'Lichess upptäcker slutförandet av rundor baserat på källspelen. Använd detta alternativ om det inte finns någon källa.'; + + @override + String get broadcastRoundName => 'Omgångens namn'; + + @override + String get broadcastRoundNumber => 'Omgångens nummer'; + + @override + String get broadcastTournamentName => 'Turneringens namn'; + + @override + String get broadcastTournamentDescription => 'Kort beskrivning av turneringen'; + + @override + String get broadcastFullDescription => 'Fullständig beskrivning'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Valfri längre beskrivning av sändningen. $param1 är tillgänglig. Längden måste vara mindre än $param2 tecken.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN Source URL'; + + @override + String get broadcastSourceUrlHelp => 'URL som Lichess kan använda för att få PGN-uppdateringar. Den måste vara publikt tillgänglig från Internet.'; + + @override + String get broadcastSourceGameIds => 'Up to 64 Lichess game IDs, separated by spaces.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Start date in the tournament local timezone: $param'; + } + + @override + String get broadcastStartDateHelp => 'Valfritt, om du vet när händelsen startar'; + + @override + String get broadcastCurrentGameUrl => 'Länk till aktuellt parti (URL)'; + + @override + String get broadcastDownloadAllRounds => 'Ladda ner alla omgångar'; + + @override + String get broadcastResetRound => 'Återställ den här omgången'; + + @override + String get broadcastDeleteRound => 'Ta bort den här omgången'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Ta bort denna runda och dess partier definitivt.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Radera alla partier i denna runda. Källan kommer behöva vara aktiv för att återskapa dem.'; + + @override + String get broadcastEditRoundStudy => 'Redigera studie för ronden'; + + @override + String get broadcastDeleteTournament => 'Radera turnering'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Definitivt radera turnering.'; + + @override + String get broadcastShowScores => 'Show players scores based on game results'; + + @override + String get broadcastReplacePlayerTags => 'Optional: replace player names, ratings and titles'; + + @override + String get broadcastFideFederations => 'FIDE federations'; + + @override + String get broadcastTop10Rating => 'Top 10 rating'; + + @override + String get broadcastFidePlayers => 'FIDE players'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE player not found'; + + @override + String get broadcastFideProfile => 'FIDE profile'; + + @override + String get broadcastFederation => 'Federation'; + + @override + String get broadcastAgeThisYear => 'Age this year'; + + @override + String get broadcastUnrated => 'Unrated'; + + @override + String get broadcastRecentTournaments => 'Recent tournaments'; + + @override + String get broadcastOpenLichess => 'Open in Lichess'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Overview'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Official website'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'Score'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count sändningar', + one: '$count sändning', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Utmaningar: $param1'; @@ -1390,10 +1615,10 @@ class AppLocalizationsSv extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Motspelaren har begränsat antal möjliga drag, och alla möjliga drag förvärrar motspelarens position.'; @override - String get puzzleThemeHealthyMix => 'Blandad kompott'; + String get puzzleThemeMix => 'Blandad kompott'; @override - String get puzzleThemeHealthyMixDescription => 'Lite av varje. Du vet inte vad som kommer, så du behöver vara redo för allt! Precis som i riktiga partier.'; + String get puzzleThemeMixDescription => 'Lite av varje. Du vet inte vad som kommer, så du behöver vara redo för allt! Precis som i riktiga partier.'; @override String get puzzleThemePlayerGames => 'Spelarspel'; @@ -1797,9 +2022,6 @@ class AppLocalizationsSv extends AppLocalizations { @override String get removesTheDepthLimit => 'Tar bort sökdjupsbegränsningen och håller datorn varm'; - @override - String get engineManager => 'Hantera analysmotor'; - @override String get blunder => 'Blunder'; @@ -2063,6 +2285,9 @@ class AppLocalizationsSv extends AppLocalizations { @override String get gamesPlayed => 'Partier spelade'; + @override + String get ok => 'OK'; + @override String get cancel => 'Avbryt'; @@ -2772,7 +2997,13 @@ class AppLocalizationsSv extends AppLocalizations { String get other => 'Annat'; @override - String get reportDescriptionHelp => 'Klistra in länken till partiet och förklara vad som är fel med den här användarens beteende. Säg inte bara \"de fuskar\", utan förklara hur du dragit denna slutsats. Din rapport kommer att behandlas fortare om den är skriven på engelska.'; + String get reportCheatBoostHelp => 'Paste the link to the game(s) and explain what is wrong about this user\'s behaviour. Don\'t just say \"they cheat\", but tell us how you came to this conclusion.'; + + @override + String get reportUsernameHelp => 'Explain what about this username is offensive. Don\'t just say \"it\'s offensive/inappropriate\", but tell us how you came to this conclusion, especially if the insult is obfuscated, not in english, is in slang, or is a historical/cultural reference.'; + + @override + String get reportProcessedFasterInEnglish => 'Your report will be processed faster if written in English.'; @override String get error_provideOneCheatedGameLink => 'Ange minst en länk till ett spel där användaren fuskade.'; @@ -4077,6 +4308,9 @@ class AppLocalizationsSv extends AppLocalizations { @override String get nothingToSeeHere => 'Nothing to see here at the moment.'; + @override + String get stats => 'Stats'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4723,9 +4957,514 @@ class AppLocalizationsSv extends AppLocalizations { @override String get streamerLichessStreamers => 'Videokanaler från Lichess'; + @override + String get studyPrivate => 'Privat'; + + @override + String get studyMyStudies => 'Mina studier'; + + @override + String get studyStudiesIContributeTo => 'Studier som jag bidrar till'; + + @override + String get studyMyPublicStudies => 'Mina offentliga studier'; + + @override + String get studyMyPrivateStudies => 'Mina privata studier'; + + @override + String get studyMyFavoriteStudies => 'Mina favoritstudier'; + + @override + String get studyWhatAreStudies => 'Vad är studier?'; + + @override + String get studyAllStudies => 'Alla studier'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Studier skapade av $param'; + } + + @override + String get studyNoneYet => 'Inga ännu.'; + + @override + String get studyHot => 'Populära'; + + @override + String get studyDateAddedNewest => 'Datum tillagd (nyaste)'; + + @override + String get studyDateAddedOldest => 'Datum tillagd (nyaste)'; + + @override + String get studyRecentlyUpdated => 'Nyligen uppdaterade'; + + @override + String get studyMostPopular => 'Mest populära'; + + @override + String get studyAlphabetical => 'Alfabetisk'; + + @override + String get studyAddNewChapter => 'Lägg till ett nytt kapitel'; + + @override + String get studyAddMembers => 'Lägg till medlemmar'; + + @override + String get studyInviteToTheStudy => 'Bjud in till studien'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Viktigt: bjud bara in människor du känner och som aktivt vill gå med i studien.'; + + @override + String get studySearchByUsername => 'Sök efter användarnamn'; + + @override + String get studySpectator => 'Åskådare'; + + @override + String get studyContributor => 'Bidragsgivare'; + + @override + String get studyKick => 'Sparka'; + + @override + String get studyLeaveTheStudy => 'Lämna studien'; + + @override + String get studyYouAreNowAContributor => 'Du är nu bidragsgivare'; + + @override + String get studyYouAreNowASpectator => 'Du är nu en åskådare'; + + @override + String get studyPgnTags => 'PGN taggar'; + + @override + String get studyLike => 'Gilla'; + + @override + String get studyUnlike => 'Sluta gilla'; + + @override + String get studyNewTag => 'Ny tag'; + + @override + String get studyCommentThisPosition => 'Kommentera denna position'; + + @override + String get studyCommentThisMove => 'Kommentera detta drag'; + + @override + String get studyAnnotateWithGlyphs => 'Kommentera med glyfer'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Kapitlet är för kort för att analyseras.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Endast studiens bidragsgivare kan begära en datoranalys.'; + + @override + String get studyGetAFullComputerAnalysis => 'Hämta en fullständig serveranalys av huvudlinjen.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Försäkra dig om att kapitlet är färdigt. Du kan bara begära analysen en gång.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Alla SYNC-medlemmar är kvar på samma position'; + + @override + String get studyShareChanges => 'Dela ändringar med åskådare och spara dem på servern'; + + @override + String get studyPlaying => 'Spelar'; + + @override + String get studyShowEvalBar => 'Värderingsfält'; + + @override + String get studyFirst => 'Första'; + + @override + String get studyPrevious => 'Föregående'; + + @override + String get studyNext => 'Nästa'; + + @override + String get studyLast => 'Sista'; + @override String get studyShareAndExport => 'Dela & exportera'; + @override + String get studyCloneStudy => 'Klona'; + + @override + String get studyStudyPgn => 'Studiens PGN'; + + @override + String get studyDownloadAllGames => 'Ladda ner alla partier'; + + @override + String get studyChapterPgn => 'Kapitel PGN'; + + @override + String get studyCopyChapterPgn => 'Kopiera PGN'; + + @override + String get studyDownloadGame => 'Ladda ner parti'; + + @override + String get studyStudyUrl => 'Studiens URL'; + + @override + String get studyCurrentChapterUrl => 'Aktuell kapitel URL'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Du kan klistra in detta i forumet för att infoga'; + + @override + String get studyStartAtInitialPosition => 'Start vid ursprunglig position'; + + @override + String studyStartAtX(String param) { + return 'Börja på $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Infoga på din hemsida eller blogg'; + + @override + String get studyReadMoreAboutEmbedding => 'Läs mer om att infoga'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Endast offentliga studier kan läggas till!'; + + @override + String get studyOpen => 'Öppna'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1 gjord av $param2'; + } + + @override + String get studyStudyNotFound => 'Studien kan inte hittas'; + + @override + String get studyEditChapter => 'Redigera kapitel'; + + @override + String get studyNewChapter => 'Nytt kapitel'; + + @override + String studyImportFromChapterX(String param) { + return 'Importera från $param'; + } + + @override + String get studyOrientation => 'Orientering'; + + @override + String get studyAnalysisMode => 'Analysläge'; + + @override + String get studyPinnedChapterComment => 'Fastnålad kommentar till kapitlet'; + + @override + String get studySaveChapter => 'Spara kapitlet'; + + @override + String get studyClearAnnotations => 'Rensa kommentarer'; + + @override + String get studyClearVariations => 'Rensa variationer'; + + @override + String get studyDeleteChapter => 'Ta bort kapitel'; + + @override + String get studyDeleteThisChapter => 'Ta bort detta kapitel. Det går inte att ångra!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Rensa alla kommentarer, symboler och former i detta kapitel?'; + + @override + String get studyRightUnderTheBoard => 'Direkt under brädet'; + + @override + String get studyNoPinnedComment => 'Ingen'; + + @override + String get studyNormalAnalysis => 'Normal analys'; + + @override + String get studyHideNextMoves => 'Dölj nästa drag'; + + @override + String get studyInteractiveLesson => 'Interaktiv lektion'; + + @override + String studyChapterX(String param) { + return 'Kapitel $param'; + } + + @override + String get studyEmpty => 'Tom'; + + @override + String get studyStartFromInitialPosition => 'Starta från ursprunglig position'; + + @override + String get studyEditor => 'Redigeringsverktyg'; + + @override + String get studyStartFromCustomPosition => 'Starta från anpassad position'; + + @override + String get studyLoadAGameByUrl => 'Importera ett spel med URL'; + + @override + String get studyLoadAPositionFromFen => 'Importera en position med FEN-kod'; + + @override + String get studyLoadAGameFromPgn => 'Importera ett spel med PGN-kod'; + + @override + String get studyAutomatic => 'Automatisk'; + + @override + String get studyUrlOfTheGame => 'URL till partiet'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Importera ett parti från $param1 eller $param2'; + } + + @override + String get studyCreateChapter => 'Skapa kapitel'; + + @override + String get studyCreateStudy => 'Skapa en studie'; + + @override + String get studyEditStudy => 'Redigera studie'; + + @override + String get studyVisibility => 'Synlighet'; + + @override + String get studyPublic => 'Offentlig'; + + @override + String get studyUnlisted => 'Ej listad'; + + @override + String get studyInviteOnly => 'Endast inbjudna'; + + @override + String get studyAllowCloning => 'Tillåt kloning'; + + @override + String get studyNobody => 'Ingen'; + + @override + String get studyOnlyMe => 'Bara mig'; + + @override + String get studyContributors => 'Medhjälpare'; + + @override + String get studyMembers => 'Medlemmar'; + + @override + String get studyEveryone => 'Alla'; + + @override + String get studyEnableSync => 'Aktivera synkronisering'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Ja: håll alla på samma position'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Nej: låt alla bläddra fritt'; + + @override + String get studyPinnedStudyComment => 'Ständigt synlig studiekommentar'; + @override String get studyStart => 'Starta'; + + @override + String get studySave => 'Spara'; + + @override + String get studyClearChat => 'Rensa Chatten'; + + @override + String get studyDeleteTheStudyChatHistory => 'Radera studiens chatthistorik? Detta går inte att ångra!'; + + @override + String get studyDeleteStudy => 'Ta bort studie'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Radera hela studien? Detta går inte att ångra! Skriv namnet på studien för att bekräfta: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Var vill du studera detta?'; + + @override + String get studyGoodMove => 'Bra drag'; + + @override + String get studyMistake => 'Misstag'; + + @override + String get studyBrilliantMove => 'Lysande drag'; + + @override + String get studyBlunder => 'Blunder'; + + @override + String get studyInterestingMove => 'Intressant drag'; + + @override + String get studyDubiousMove => 'Tvivelaktigt drag'; + + @override + String get studyOnlyMove => 'Enda draget'; + + @override + String get studyZugzwang => 'Zugzwang'; + + @override + String get studyEqualPosition => 'Likvärdig position'; + + @override + String get studyUnclearPosition => 'Oklar position'; + + @override + String get studyWhiteIsSlightlyBetter => 'Vit är något bättre'; + + @override + String get studyBlackIsSlightlyBetter => 'Svart är något bättre'; + + @override + String get studyWhiteIsBetter => 'Vit är bättre'; + + @override + String get studyBlackIsBetter => 'Svart är bättre'; + + @override + String get studyWhiteIsWinning => 'Vit vinner'; + + @override + String get studyBlackIsWinning => 'Svart vinner'; + + @override + String get studyNovelty => 'Ny variant'; + + @override + String get studyDevelopment => 'Utveckling'; + + @override + String get studyInitiative => 'Initiativ'; + + @override + String get studyAttack => 'Attack'; + + @override + String get studyCounterplay => 'Motspel'; + + @override + String get studyTimeTrouble => 'Tidsproblem'; + + @override + String get studyWithCompensation => 'Med kompensation'; + + @override + String get studyWithTheIdea => 'Med idén'; + + @override + String get studyNextChapter => 'Nästa kapitel'; + + @override + String get studyPrevChapter => 'Föregående kapitel'; + + @override + String get studyStudyActions => 'Studie-alternativ'; + + @override + String get studyTopics => 'Ämnen'; + + @override + String get studyMyTopics => 'Mina ämnen'; + + @override + String get studyPopularTopics => 'Populära ämnen'; + + @override + String get studyManageTopics => 'Hantera ämnen'; + + @override + String get studyBack => 'Tillbaka'; + + @override + String get studyPlayAgain => 'Spela igen'; + + @override + String get studyWhatWouldYouPlay => 'Vad skulle du spela i denna position?'; + + @override + String get studyYouCompletedThisLesson => 'Grattis! Du har slutfört denna lektionen.'; + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Kapitel', + one: '$count Kapitel', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count partier', + one: '$count partier', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Medlemmar', + one: '$count Medlem', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Klistra in din PGN-kod här, upp till $count partier', + one: 'Klistra in din PGN-kod här, upp till $count parti', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_tr.dart b/lib/l10n/l10n_tr.dart index 24e4ffa09d..6756739357 100644 --- a/lib/l10n/l10n_tr.dart +++ b/lib/l10n/l10n_tr.dart @@ -39,10 +39,10 @@ class AppLocalizationsTr extends AppLocalizations { String get mobileSettingsHapticFeedback => 'Titreşimli geri bildirim'; @override - String get mobileSettingsImmersiveMode => 'Immersive mode'; + String get mobileSettingsImmersiveMode => 'Sürükleyici mod'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileSettingsImmersiveModeSubtitle => 'Oynarken sistem arayüzünü gizle. Ekranın kenarlarındaki sistemin gezinme hareketlerinden rahatsızsan bunu kullan. Bu ayar, oyun ve Bulmaca Fırtınası ekranlarına uygulanır.'; @override String get mobileNotFollowingAnyUser => 'Hiçbir kullanıcıyı takip etmiyorsunuz.'; @@ -58,7 +58,7 @@ class AppLocalizationsTr extends AppLocalizations { @override String mobilePlayersMatchingSearchTerm(String param) { - return 'Players with \"$param\"'; + return '\"$param\" ile başlayan oyuncularla'; } @override @@ -68,10 +68,10 @@ class AppLocalizationsTr extends AppLocalizations { String get mobileAreYouSure => 'Emin misiniz?'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; + String get mobilePuzzleStreakAbortWarning => 'Mevcut serinizi kaybedeceksiniz ve puanınız kaydedilecektir.'; @override - String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; + String get mobilePuzzleStormNothingToShow => 'Gösterilcek bir şey yok. Birkaç kez Bulmaca Fırtınası oyunu oynayın.'; @override String get mobileSharePuzzle => 'Bulmacayı paylaş'; @@ -95,17 +95,14 @@ class AppLocalizationsTr extends AppLocalizations { String get mobileShowComments => 'Yorumları göster'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get mobilePuzzleStormConfirmEndRun => 'Bu oyunu bitirmek istiyor musun?'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobilePuzzleStormFilterNothingToShow => 'Gösterilecek bir şey yok, lütfen filtreleri değiştirin'; @override String get mobileCancelTakebackOffer => 'Geri alma teklifini iptal et'; - @override - String get mobileCancelDrawOffer => 'Berabere teklifini iptal et'; - @override String get mobileWaitingForOpponentToJoin => 'Rakip bekleniyor...'; @@ -113,36 +110,36 @@ class AppLocalizationsTr extends AppLocalizations { String get mobileBlindfoldMode => 'Körleme modu'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileLiveStreamers => 'Canlı yayıncılar'; @override String get mobileCustomGameJoinAGame => 'Bir oyuna katıl'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; + String get mobileCorrespondenceClearSavedMove => 'Kayıtlı hamleyi sil'; @override String get mobileSomethingWentWrong => 'Birşeyler ters gitti.'; @override - String get mobileShowResult => 'Show result'; + String get mobileShowResult => 'Sonucu göster'; @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobilePuzzleThemesSubtitle => 'En sevdiğiniz açılışlardan bulmacalar oynayın veya bir tema seçin.'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobilePuzzleStormSubtitle => '3 dakika içerisinde mümkün olduğunca çok bulmaca çözün.'; @override String mobileGreeting(String param) { - return 'Hello, $param'; + return 'Merhaba, $param'; } @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileGreetingWithoutName => 'Merhaba'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobilePrefMagnifyDraggedPiece => 'Sürüklenen parçayı büyüt'; @override String get activityActivity => 'Son Etkinlikler'; @@ -246,6 +243,17 @@ class AppLocalizationsTr extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count $param2 yazışmalı oyunu tamamladı', + one: '$count $param2 yazışmalı oyunu tamamladı', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -348,9 +356,226 @@ class AppLocalizationsTr extends AppLocalizations { @override String get broadcastBroadcasts => 'Canlı Turnuvalar'; + @override + String get broadcastMyBroadcasts => 'Canlı Turnuvalarım'; + @override String get broadcastLiveBroadcasts => 'Canlı Turnuva Yayınları'; + @override + String get broadcastBroadcastCalendar => 'Turnuva takvimi'; + + @override + String get broadcastNewBroadcast => 'Canlı Turnuva Ekle'; + + @override + String get broadcastSubscribedBroadcasts => 'Abone olduğunuz yayınlar'; + + @override + String get broadcastAboutBroadcasts => 'Canlı Turnuvalar hakkında'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Lichess Canlı Turnuvaları nasıl kullanılır.'; + + @override + String get broadcastTheNewRoundHelp => 'Yeni tur, önceki turdaki üyeler ve katkıda bulunanlarla aynı olacak.'; + + @override + String get broadcastAddRound => 'Bir tur ekle'; + + @override + String get broadcastOngoing => 'Devam eden turnuvalar'; + + @override + String get broadcastUpcoming => 'Yaklaşan turnuvalar'; + + @override + String get broadcastCompleted => 'Tamamlanan turnuvalar'; + + @override + String get broadcastCompletedHelp => 'Lichess, tur tamamlanmasını kaynak oyunlara dayanarak algılar. Kaynak yoksa bu anahtarı kullanın.'; + + @override + String get broadcastRoundName => 'Tur ismi'; + + @override + String get broadcastRoundNumber => 'Tur sayısı'; + + @override + String get broadcastTournamentName => 'Turnuva ismi'; + + @override + String get broadcastTournamentDescription => 'Turnuvanın kısa tanımı'; + + @override + String get broadcastFullDescription => 'Etkinliğin detaylıca açıklaması'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Etkinliğin isteğe bağlı detaylı açıklaması. $param1 seçeneği mevcuttur. Metnin uzunluğu azami $param2 karakter olmalıdır.'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN Kaynak URL\'si'; + + @override + String get broadcastSourceUrlHelp => 'Lichess, sağladığınız URL yardımıyla PGN\'yi güncelleyecektir. İnternet üzerinden herkese açık bir URL yazmalısınız.'; + + @override + String get broadcastSourceGameIds => 'Boşluklarla ayrılmış 64 adede kadar Lichess oyun ID\'si.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Turnuva yerel saati ile başlama zamanı: $param'; + } + + @override + String get broadcastStartDateHelp => 'İsteğe bağlı, etkinliğin ne zaman başladığını biliyorsanız ekleyebilirsiniz.'; + + @override + String get broadcastCurrentGameUrl => 'Şu anki oyunun linki'; + + @override + String get broadcastDownloadAllRounds => 'Bütün maçları indir'; + + @override + String get broadcastResetRound => 'Bu turu sıfırla'; + + @override + String get broadcastDeleteRound => 'Bu turu sil'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Turu ve oyunlarını tamamen sil.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Bu turdaki tüm oyunları sil. Tekrardan yapılabilmesi için kaynağın aktif olması gerekir.'; + + @override + String get broadcastEditRoundStudy => 'Tur çalışmasını düzenle'; + + @override + String get broadcastDeleteTournament => 'Bu turnuvayı sil'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Bütün turnuvayı, turlarını ve oyunlarını kalıcı olarak sil.'; + + @override + String get broadcastShowScores => 'Oyuncuların puanlarını oyun sonuçlarına göre göster'; + + @override + String get broadcastReplacePlayerTags => 'İsteğe bağlı: Oyuncu adlarını, derecelendirmelerini ve unvanlarını değiştirin'; + + @override + String get broadcastFideFederations => 'FIDE federasyonları'; + + @override + String get broadcastTop10Rating => 'İlk 10 rating'; + + @override + String get broadcastFidePlayers => 'FIDE oyuncuları'; + + @override + String get broadcastFidePlayerNotFound => 'FIDE oyuncusu bulunamadı'; + + @override + String get broadcastFideProfile => 'FIDE profili'; + + @override + String get broadcastFederation => 'Federasyon'; + + @override + String get broadcastAgeThisYear => 'Bu yılki yaşı'; + + @override + String get broadcastUnrated => 'Derecelendirilmemiş'; + + @override + String get broadcastRecentTournaments => 'Son Turnuvalar'; + + @override + String get broadcastOpenLichess => 'Lichess\'te aç'; + + @override + String get broadcastTeams => 'Takımlar'; + + @override + String get broadcastBoards => 'Tahtalar'; + + @override + String get broadcastOverview => 'Genel Bakış'; + + @override + String get broadcastSubscribeTitle => 'Tur başladığında bildirim almak için abone olun. Hesap tercihlerinizden anlık ya da çan bildirimi tercihinizi hesap tercihlerinizden belirleyebilirsiniz.'; + + @override + String get broadcastUploadImage => 'Turnuva görseli yükleyin'; + + @override + String get broadcastNoBoardsYet => 'Henüz tahta bulunmamaktadır. Oyunlar yüklendikçe tahtalar ortaya çıkacaktır.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Tahtalar bir kaynaktan ya da ${param}ndan yüklenebilir'; + } + + @override + String broadcastStartsAfter(String param) { + return '$param\'ten sonra başlar'; + } + + @override + String get broadcastStartVerySoon => 'Yayın az sonra başlayacak.'; + + @override + String get broadcastNotYetStarted => 'Yayın henüz başlamadı.'; + + @override + String get broadcastOfficialWebsite => 'Resmî site'; + + @override + String get broadcastStandings => 'Sıralamalar'; + + @override + String broadcastIframeHelp(String param) { + return '${param}nda daha fazla seçenek'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'Bu turun açık, gerçek zamanlı PGN kaynağı. Daha hızlı ve verimli senkronizasyon için $param\'ımız da bulunmaktadır.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'İnternet sitenizde bu yayını gömülü paylaşın'; + + @override + String broadcastEmbedThisRound(String param) { + return '${param}u İnternet sitenizde gömülü paylaşın'; + } + + @override + String get broadcastRatingDiff => 'Puan farkı'; + + @override + String get broadcastGamesThisTournament => 'Bu turnuvadaki maçlar'; + + @override + String get broadcastScore => 'Skor'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count canlı turnuva', + one: '$count canlı turnuva', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return '$param1 karşılaşmaları'; @@ -1390,10 +1615,10 @@ class AppLocalizationsTr extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Rakibin iyi bir hamlesinin kalmadığı, yapabileceği bütün hamlelerin kendisine zarar vereceği taktikler.'; @override - String get puzzleThemeHealthyMix => 'Bir ondan bir bundan'; + String get puzzleThemeMix => 'Bir ondan bir bundan'; @override - String get puzzleThemeHealthyMixDescription => 'Ortaya karışık bulmacalar. Karşına ne tür bir pozisyonun çıkacağı tam bir muamma. Tıpkı gerçek maçlardaki gibi her şeye hazırlıklı olmakta fayda var.'; + String get puzzleThemeMixDescription => 'Ortaya karışık bulmacalar. Karşına ne tür bir pozisyonun çıkacağı tam bir muamma. Tıpkı gerçek maçlardaki gibi her şeye hazırlıklı olmakta fayda var.'; @override String get puzzleThemePlayerGames => 'Bireysel oyunlar'; @@ -1639,7 +1864,7 @@ class AppLocalizationsTr extends AppLocalizations { String get collapseVariations => 'Varyasyonları daralt'; @override - String get expandVariations => 'Varyasyonları genişler'; + String get expandVariations => 'Varyasyonları genişlet'; @override String get forceVariation => 'Varyant olarak göster'; @@ -1797,9 +2022,6 @@ class AppLocalizationsTr extends AppLocalizations { @override String get removesTheDepthLimit => 'Derinlik sınırını kaldırır ve bilgisayarınızı sıcacık tutar'; - @override - String get engineManager => 'Motor yöneticisi'; - @override String get blunder => 'Vahim hata'; @@ -1878,7 +2100,7 @@ class AppLocalizationsTr extends AppLocalizations { String get friends => 'Arkadaşlar'; @override - String get otherPlayers => 'other players'; + String get otherPlayers => 'diğer oyuncular'; @override String get discussions => 'Sohbetler'; @@ -2063,6 +2285,9 @@ class AppLocalizationsTr extends AppLocalizations { @override String get gamesPlayed => 'Oynanmış oyunlar'; + @override + String get ok => 'OK'; + @override String get cancel => 'İptal et'; @@ -2712,10 +2937,10 @@ class AppLocalizationsTr extends AppLocalizations { String get yes => 'Evet'; @override - String get website => 'Website'; + String get website => 'Web sitesi'; @override - String get mobile => 'Mobile'; + String get mobile => 'Mobil'; @override String get help => 'Yardım:'; @@ -2772,7 +2997,13 @@ class AppLocalizationsTr extends AppLocalizations { String get other => 'Diğer'; @override - String get reportDescriptionHelp => 'Raporlamak istediğiniz oyunun linkini yapıştırın ve sorununuzu açıklayın. Lütfen sadece \"hile yapıyor\" gibisinden açıklama yazmayın, hile olduğunu nasıl anladığınızı açıklayın. Rapor edeceğiniz kişiyi ya da oyunu \"İngilizce\" açıklarsanız, daha hızlı sonuca ulaşırsınız.'; + String get reportCheatBoostHelp => 'Oyunun (oyunların) bağlantısını yapıştırın ve kullanıcının hangi davranışı yanlış açıklayın. Sadece \"bu hile\" demeyin, bize bu sonuca nasıl vardığınızı söyleyin.'; + + @override + String get reportUsernameHelp => 'Bu kullanıcı adı hakkında neyin saldırganca olduğunu açıklayın. Sadece \"bu saldırganca/uygunsuz\" demeyin, bu sonuca nasıl vardığınızı söyleyin bize, özellikle de hakaret gizlenmiş, ingilizce olmayan, argo, veya tarihsel/kültürel referansalar varsa.'; + + @override + String get reportProcessedFasterInEnglish => 'Eğer İngilizce yazarsanız raporunuz daha hızlı işleme alınır.'; @override String get error_provideOneCheatedGameLink => 'Lütfen hileli gördüğünüz en az 1 adet oyun linki verin.'; @@ -2875,7 +3106,7 @@ class AppLocalizationsTr extends AppLocalizations { String get outsideTheBoard => 'Karelerin dışında'; @override - String get allSquaresOfTheBoard => 'All squares of the board'; + String get allSquaresOfTheBoard => 'Tahtadaki tüm karelerde'; @override String get onSlowGames => 'Yavaş oyunlarda'; @@ -4077,6 +4308,9 @@ class AppLocalizationsTr extends AppLocalizations { @override String get nothingToSeeHere => 'Şu anda görülebilecek bir şey yok.'; + @override + String get stats => 'İstatistikler'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4723,9 +4957,514 @@ class AppLocalizationsTr extends AppLocalizations { @override String get streamerLichessStreamers => 'Lichess yayıncıları'; + @override + String get studyPrivate => 'Gizli'; + + @override + String get studyMyStudies => 'Çalışmalarım'; + + @override + String get studyStudiesIContributeTo => 'Katkıda bulunduğum çalışmalar'; + + @override + String get studyMyPublicStudies => 'Herkese açık çalışmalarım'; + + @override + String get studyMyPrivateStudies => 'Gizli çalışmalarım'; + + @override + String get studyMyFavoriteStudies => 'En sevdiğim çalışmalar'; + + @override + String get studyWhatAreStudies => 'Çalışmalar nedir?'; + + @override + String get studyAllStudies => 'Bütün çalışmalar'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Çalışmalar $param tarafından oluşturulmuştur'; + } + + @override + String get studyNoneYet => 'Henüz yok.'; + + @override + String get studyHot => 'Popüler'; + + @override + String get studyDateAddedNewest => 'Eklenme tarihi (en yeni)'; + + @override + String get studyDateAddedOldest => 'Eklenme tarihi (en eski)'; + + @override + String get studyRecentlyUpdated => 'Yeni güncellenmiş'; + + @override + String get studyMostPopular => 'En popüler'; + + @override + String get studyAlphabetical => 'Alfabetik'; + + @override + String get studyAddNewChapter => 'Yeni bir bölüm ekle'; + + @override + String get studyAddMembers => 'Üye ekle'; + + @override + String get studyInviteToTheStudy => 'Bu çalışmaya davet edin'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Lütfen sadece tanıdığınız ve katkı sağlayacağını düşündüğünüz kişileri davet ediniz.'; + + @override + String get studySearchByUsername => 'Kullanıcı adına göre ara'; + + @override + String get studySpectator => 'İzleyici'; + + @override + String get studyContributor => 'Katılımcı'; + + @override + String get studyKick => 'Çıkar'; + + @override + String get studyLeaveTheStudy => 'Çalışmadan ayrıl'; + + @override + String get studyYouAreNowAContributor => 'Artık bir katılımcısınız'; + + @override + String get studyYouAreNowASpectator => 'Artık bir izleyicisiniz'; + + @override + String get studyPgnTags => 'PGN etiketleri'; + + @override + String get studyLike => 'Beğen'; + + @override + String get studyUnlike => 'Beğenmekten Vazgeç'; + + @override + String get studyNewTag => 'Yeni etiket'; + + @override + String get studyCommentThisPosition => 'Bu pozisyon için yorum yap'; + + @override + String get studyCommentThisMove => 'Bu hamle için yorum yap'; + + @override + String get studyAnnotateWithGlyphs => 'Glifler ile açıkla'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Bu bölüm analiz için çok kısa.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Sadece katılımcılar bilgisayar analizi isteyebilir.'; + + @override + String get studyGetAFullComputerAnalysis => 'Bu varyant için ayrıntılı bir sunucu analizi yapın.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Bu bölümü tamamladığınızdan emin olun. Sadece bir kez bilgisayar analizi talep edebilirsiniz.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Senkronize edilen bütün üyeler aynı pozisyonda kalır'; + + @override + String get studyShareChanges => 'Değişiklikleri izleyiciler ile paylaşın ve sunucuya kaydedin'; + + @override + String get studyPlaying => 'Oynanıyor'; + + @override + String get studyShowEvalBar => 'Değerlendirme çubuğu'; + + @override + String get studyFirst => 'İlk'; + + @override + String get studyPrevious => 'Önceki'; + + @override + String get studyNext => 'Sonraki'; + + @override + String get studyLast => 'Son'; + @override String get studyShareAndExport => 'Paylaş ve dışa aktar'; + @override + String get studyCloneStudy => 'Klon'; + + @override + String get studyStudyPgn => 'Çalışma PGN\'si'; + + @override + String get studyDownloadAllGames => 'Bütün oyunları indir'; + + @override + String get studyChapterPgn => 'Bölüm PGN\'si'; + + @override + String get studyCopyChapterPgn => 'PGN \'yi kopyala'; + + @override + String get studyDownloadGame => 'Oyunu indir'; + + @override + String get studyStudyUrl => 'Çalışma Adresi'; + + @override + String get studyCurrentChapterUrl => 'Mevcut Bölümün Adresi'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Forumda gömülü olarak paylaşmak için yukarıdaki bağlantıyı kullanabilirsiniz'; + + @override + String get studyStartAtInitialPosition => 'İlk pozisyondan başlasın'; + + @override + String studyStartAtX(String param) { + return '$param pozisyonundan başlasın'; + } + + @override + String get studyEmbedInYourWebsite => 'İnternet sitenizde ya da blogunuzda gömülü olarak paylaşın'; + + @override + String get studyReadMoreAboutEmbedding => 'Gömülü paylaşma hakkında'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Yalnızca herkese açık çalışmalar gömülü paylaşılabilir!'; + + @override + String get studyOpen => 'Aç'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param2 sana $param1 getirdi'; + } + + @override + String get studyStudyNotFound => 'Böyle bir çalışma bulunamadı'; + + @override + String get studyEditChapter => 'Bölümü düzenle'; + + @override + String get studyNewChapter => 'Yeni bölüm'; + + @override + String studyImportFromChapterX(String param) { + return '$param çalışmasından içe aktar'; + } + + @override + String get studyOrientation => 'Tahta yönü'; + + @override + String get studyAnalysisMode => 'Analiz modu'; + + @override + String get studyPinnedChapterComment => 'Bölüm üzerine yorumlar'; + + @override + String get studySaveChapter => 'Bölümü kaydet'; + + @override + String get studyClearAnnotations => 'Açıklamaları sil'; + + @override + String get studyClearVariations => 'Varyasyonları sil'; + + @override + String get studyDeleteChapter => 'Bölümü sil'; + + @override + String get studyDeleteThisChapter => 'Bu bölüm silinsin mi? Bunun geri dönüşü yok!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Bu bölümdeki bütün yorumlar ve işaretler temizlensin mi?'; + + @override + String get studyRightUnderTheBoard => 'Tahtanın hemen altında görünsün'; + + @override + String get studyNoPinnedComment => 'Yok'; + + @override + String get studyNormalAnalysis => 'Normal analiz'; + + @override + String get studyHideNextMoves => 'Sonraki hamleleri gizle'; + + @override + String get studyInteractiveLesson => 'Etkileşimli ders'; + + @override + String studyChapterX(String param) { + return 'Bölüm $param'; + } + + @override + String get studyEmpty => 'Boş'; + + @override + String get studyStartFromInitialPosition => 'İlk pozisyondan başlasın'; + + @override + String get studyEditor => 'Editör'; + + @override + String get studyStartFromCustomPosition => 'Özel bir pozisyondan başlasın'; + + @override + String get studyLoadAGameByUrl => 'URL ile oyun yükle'; + + @override + String get studyLoadAPositionFromFen => 'FEN kullanarak pozisyon yükle'; + + @override + String get studyLoadAGameFromPgn => 'PGN ile oyun yükle'; + + @override + String get studyAutomatic => 'Otomatik'; + + @override + String get studyUrlOfTheGame => 'Oyunun bağlantısı'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return '$param1 veya $param2 kullanarak oyun yükle'; + } + + @override + String get studyCreateChapter => 'Bölüm oluştur'; + + @override + String get studyCreateStudy => 'Çalışma oluştur'; + + @override + String get studyEditStudy => 'Çalışmayı düzenle'; + + @override + String get studyVisibility => 'Görünürlük'; + + @override + String get studyPublic => 'Herkese açık'; + + @override + String get studyUnlisted => 'Liste dışı'; + + @override + String get studyInviteOnly => 'Sadece davet edilenler'; + + @override + String get studyAllowCloning => 'Klonlamaya izni olanlar'; + + @override + String get studyNobody => 'Hiç kimse'; + + @override + String get studyOnlyMe => 'Sadece ben'; + + @override + String get studyContributors => 'Katkıda Bulunanlar'; + + @override + String get studyMembers => 'Üyeler'; + + @override + String get studyEveryone => 'Herkes'; + + @override + String get studyEnableSync => 'Senkronizasyonu etkinleştir'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Evet: herkes aynı pozisyonda kalsın'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Hayır: herkes dilediğince gezinebilsin'; + + @override + String get studyPinnedStudyComment => 'Çalışma üzerine yorumlar'; + @override String get studyStart => 'Başlat'; + + @override + String get studySave => 'Kaydet'; + + @override + String get studyClearChat => 'Sohbeti temizle'; + + @override + String get studyDeleteTheStudyChatHistory => 'Çalışmanın sohbet geçmişi silinsin mi? Bunun geri dönüşü yok!'; + + @override + String get studyDeleteStudy => 'Çalışmayı sil'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Tüm çalışma silinsin mi? Bunun geri dönüşü yok! İşlemi onaylamak için çalışmanın adını yazın: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Bu çalışmayı nerede sürdürmek istersiniz?'; + + @override + String get studyGoodMove => 'İyi hamle'; + + @override + String get studyMistake => 'Hata'; + + @override + String get studyBrilliantMove => 'Muhteşem hamle'; + + @override + String get studyBlunder => 'Vahim hata'; + + @override + String get studyInterestingMove => 'İlginç hamle'; + + @override + String get studyDubiousMove => 'Şüpheli hamle'; + + @override + String get studyOnlyMove => 'Tek hamle'; + + @override + String get studyZugzwang => 'Zugzwang'; + + @override + String get studyEqualPosition => 'Eşit pozisyon'; + + @override + String get studyUnclearPosition => 'Belirsiz pozisyon'; + + @override + String get studyWhiteIsSlightlyBetter => 'Beyaz biraz önde'; + + @override + String get studyBlackIsSlightlyBetter => 'Siyah biraz önde'; + + @override + String get studyWhiteIsBetter => 'Beyaz daha üstün'; + + @override + String get studyBlackIsBetter => 'Siyah daha üstün'; + + @override + String get studyWhiteIsWinning => 'Beyaz kazanıyor'; + + @override + String get studyBlackIsWinning => 'Siyah kazanıyor'; + + @override + String get studyNovelty => 'Farklı bir açılış'; + + @override + String get studyDevelopment => 'Gelişim hamlesi'; + + @override + String get studyInitiative => 'Girişim'; + + @override + String get studyAttack => 'Saldırı'; + + @override + String get studyCounterplay => 'Karşı saldırı'; + + @override + String get studyTimeTrouble => 'Vakit sıkıntısı'; + + @override + String get studyWithCompensation => 'Pozisyon avantajı ile'; + + @override + String get studyWithTheIdea => 'Plan doğrultusunda'; + + @override + String get studyNextChapter => 'Sonraki bölüm'; + + @override + String get studyPrevChapter => 'Önceki bölüm'; + + @override + String get studyStudyActions => 'Çalışma seçenekleri'; + + @override + String get studyTopics => 'Konular'; + + @override + String get studyMyTopics => 'Konularım'; + + @override + String get studyPopularTopics => 'Popüler konular'; + + @override + String get studyManageTopics => 'Konuları yönet'; + + @override + String get studyBack => 'Baştan başlat'; + + @override + String get studyPlayAgain => 'Tekrar oyna'; + + @override + String get studyWhatWouldYouPlay => 'Burada hangi hamleyi yapardınız?'; + + @override + String get studyYouCompletedThisLesson => 'Tebrikler! Bu dersi tamamlandınız.'; + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Bölüm', + one: '$count Bölüm', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Oyun', + one: '$count oyun', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Üye', + one: '$count Üye', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'PGN metninizi buraya yapıştırın, en fazla $count oyuna kadar', + one: 'PGN metninizi buraya yapıştırın, en fazla $count oyuna kadar', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_uk.dart b/lib/l10n/l10n_uk.dart index fb35d69ab9..c6ebba1edf 100644 --- a/lib/l10n/l10n_uk.dart +++ b/lib/l10n/l10n_uk.dart @@ -103,9 +103,6 @@ class AppLocalizationsUk extends AppLocalizations { @override String get mobileCancelTakebackOffer => 'Скасувати пропозицію повернення ходу'; - @override - String get mobileCancelDrawOffer => 'Скасувати пропозицію нічиєї'; - @override String get mobileWaitingForOpponentToJoin => 'Очікування на суперника...'; @@ -142,7 +139,7 @@ class AppLocalizationsUk extends AppLocalizations { String get mobileGreetingWithoutName => 'Привіт'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobilePrefMagnifyDraggedPiece => 'Збільшувати розмір фігури при перетягуванні'; @override String get activityActivity => 'Активність'; @@ -262,6 +259,19 @@ class AppLocalizationsUk extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Зіграно $count $param2 заочних ігор', + many: 'Зіграно $count $param2 заочних ігор', + few: 'Зіграно $count $param2 заочні гри', + one: 'Зіграно $count $param2 заочну гру', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -382,9 +392,228 @@ class AppLocalizationsUk extends AppLocalizations { @override String get broadcastBroadcasts => 'Трансляції'; + @override + String get broadcastMyBroadcasts => 'Мої трансляції'; + @override String get broadcastLiveBroadcasts => 'Онлайн трансляції турнірів'; + @override + String get broadcastBroadcastCalendar => 'Календар трансляцій'; + + @override + String get broadcastNewBroadcast => 'Нова трансляція'; + + @override + String get broadcastSubscribedBroadcasts => 'Обрані трансляції'; + + @override + String get broadcastAboutBroadcasts => 'Про трансляцію'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Як користуватися Lichess трансляціями.'; + + @override + String get broadcastTheNewRoundHelp => 'У новому раунді будуть ті самі учасники та редактори, що й у попередньому.'; + + @override + String get broadcastAddRound => 'Додати раунд'; + + @override + String get broadcastOngoing => 'Поточні'; + + @override + String get broadcastUpcoming => 'Майбутні'; + + @override + String get broadcastCompleted => 'Завершені'; + + @override + String get broadcastCompletedHelp => 'Lichess виявляє завершення раунду на основі ігор. Використовуйте цей перемикач якщо немає джерела.'; + + @override + String get broadcastRoundName => 'Назва раунду'; + + @override + String get broadcastRoundNumber => 'Номер раунду'; + + @override + String get broadcastTournamentName => 'Назва турніру'; + + @override + String get broadcastTournamentDescription => 'Короткий опис турніру'; + + @override + String get broadcastFullDescription => 'Повний опис події'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Необов\'язковий довгий опис трансляції. Наявна розмітка $param1. Довжина має бути менша ніж $param2 символів.'; + } + + @override + String get broadcastSourceSingleUrl => 'Адреса джерела PGN'; + + @override + String get broadcastSourceUrlHelp => 'Посилання, яке Lichess перевірятиме, щоб отримати оновлення PGN. Воно має бути загальнодоступним в Інтернеті.'; + + @override + String get broadcastSourceGameIds => 'До 64 ігрових ID Lichess, відокремлені пробілами.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Start date in the tournament local timezone: $param'; + } + + @override + String get broadcastStartDateHelp => 'За бажанням, якщо ви знаєте, коли починається подія'; + + @override + String get broadcastCurrentGameUrl => 'Посилання на поточну гру'; + + @override + String get broadcastDownloadAllRounds => 'Завантажити всі тури'; + + @override + String get broadcastResetRound => 'Скинути цей раунд'; + + @override + String get broadcastDeleteRound => 'Видалити цей раунд'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Видалити всі ігри цього раунду.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Видалити всі ігри цього раунду. Джерело має бути активним для того, щоб повторно відтворити його.'; + + @override + String get broadcastEditRoundStudy => 'Редагувати дослідження раунду'; + + @override + String get broadcastDeleteTournament => 'Видалити турнір'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Остаточно видалити весь турнір, всі його раунди та всі його ігри.'; + + @override + String get broadcastShowScores => 'Показувати результати гравців за результатами гри'; + + @override + String get broadcastReplacePlayerTags => 'За бажанням: замінити імена, рейтинги та титули гравців'; + + @override + String get broadcastFideFederations => 'Федерації FIDE'; + + @override + String get broadcastTop10Rating => 'Топ 10 рейтингу'; + + @override + String get broadcastFidePlayers => 'Гравці FIDE'; + + @override + String get broadcastFidePlayerNotFound => 'Гравця FIDE не знайдено'; + + @override + String get broadcastFideProfile => 'Профіль FIDE'; + + @override + String get broadcastFederation => 'Федерація'; + + @override + String get broadcastAgeThisYear => 'Вік цього року'; + + @override + String get broadcastUnrated => 'Без рейтингу'; + + @override + String get broadcastRecentTournaments => 'Нещодавні турніри'; + + @override + String get broadcastOpenLichess => 'Open in Lichess'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Overview'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Офіційний вебсайт'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'Score'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count трансляцій', + many: '$count трансляцій', + few: '$count трансляції', + one: '$count трансляція', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Виклики: $param1'; @@ -1434,10 +1663,10 @@ class AppLocalizationsUk extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Суперник обмежений в своїх ходах, а кожен хід погіршує його позицію.'; @override - String get puzzleThemeHealthyMix => 'Здорова суміш'; + String get puzzleThemeMix => 'Усього потрохи'; @override - String get puzzleThemeHealthyMixDescription => 'Всього потроху. Ви не знаєте, чого очікувати, тому готуйтесь до всього! Як у справжніх партіях.'; + String get puzzleThemeMixDescription => 'Усього потрохи. Ви не знатимете, чого очікувати, тому готуйтеся до всього! Як у справжніх партіях.'; @override String get puzzleThemePlayerGames => 'Ігри гравця'; @@ -1841,9 +2070,6 @@ class AppLocalizationsUk extends AppLocalizations { @override String get removesTheDepthLimit => 'Знімає обмеження на глибину аналізу - ваш комп’ютер стане теплішим'; - @override - String get engineManager => 'Менеджер рушія'; - @override String get blunder => 'Груба помилка'; @@ -2107,6 +2333,9 @@ class AppLocalizationsUk extends AppLocalizations { @override String get gamesPlayed => 'Ігор зіграно'; + @override + String get ok => 'Гаразд'; + @override String get cancel => 'Скасувати'; @@ -2816,7 +3045,13 @@ class AppLocalizationsUk extends AppLocalizations { String get other => 'Інше'; @override - String get reportDescriptionHelp => 'Вставте посилання на гру (ігри) та поясніть, що не так із поведінкою цього користувача. Не пишіть просто \"він шахраює\", а розкажіть, як ви дійшли до такого висновку. Вашу скаргу розглянуть швидше, якщо ви напишете її англійською.'; + String get reportCheatBoostHelp => 'Вставте посилання на гру або ігри та поясніть, що не так з поведінкою цього користувача. Не кажіть «він нечесно грав», а поясніть, як ви прийшли до такого висновку.'; + + @override + String get reportUsernameHelp => 'Поясніть, що саме в цьому імені користувача є образливе. Не кажіть «воно образливе/неприйнятне», а поясніть, чому ви так вважаєте, особливо коли образа заплутана, не англійською, на сленгу, чи є посиланням на щось історичне/культурне.'; + + @override + String get reportProcessedFasterInEnglish => 'Ваша скарга оброблятиметься швидше, якщо буде написана англійською.'; @override String get error_provideOneCheatedGameLink => 'Будь ласка, додайте посилання на хоча б одну нечесну гру.'; @@ -4121,6 +4356,9 @@ class AppLocalizationsUk extends AppLocalizations { @override String get nothingToSeeHere => 'Поки тут нічого немає.'; + @override + String get stats => 'Статистика'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4855,9 +5093,522 @@ class AppLocalizationsUk extends AppLocalizations { @override String get streamerLichessStreamers => 'Стримери Lichess'; + @override + String get studyPrivate => 'Приватне'; + + @override + String get studyMyStudies => 'Мої дослідження'; + + @override + String get studyStudiesIContributeTo => 'Дослідження, яким я сприяю'; + + @override + String get studyMyPublicStudies => 'Мої публічні дослідження'; + + @override + String get studyMyPrivateStudies => 'Мої приватні дослідження'; + + @override + String get studyMyFavoriteStudies => 'Мої улюблені дослідження'; + + @override + String get studyWhatAreStudies => 'Що таке дослідження?'; + + @override + String get studyAllStudies => 'Усі дослідження'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Дослідження, створені $param'; + } + + @override + String get studyNoneYet => 'Ще немає.'; + + @override + String get studyHot => 'Активні'; + + @override + String get studyDateAddedNewest => 'Дата додавання (старіші)'; + + @override + String get studyDateAddedOldest => 'Дата додавання (старіші)'; + + @override + String get studyRecentlyUpdated => 'Нещодавно оновлені'; + + @override + String get studyMostPopular => 'Найпопулярніші'; + + @override + String get studyAlphabetical => 'За алфавітом'; + + @override + String get studyAddNewChapter => 'Додати новий розділ'; + + @override + String get studyAddMembers => 'Додати учасників'; + + @override + String get studyInviteToTheStudy => 'Запросити до дослідження'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Будь ласка запрошуйте лише людей, яких ви знаєте, і які хочуть активно долучитися до цього дослідження.'; + + @override + String get studySearchByUsername => 'Пошук за іменем користувача'; + + @override + String get studySpectator => 'Глядач'; + + @override + String get studyContributor => 'Співавтор'; + + @override + String get studyKick => 'Вигнати'; + + @override + String get studyLeaveTheStudy => 'Покинути дослідження'; + + @override + String get studyYouAreNowAContributor => 'Тепер ви співавтор'; + + @override + String get studyYouAreNowASpectator => 'Тепер ви глядач'; + + @override + String get studyPgnTags => 'Теги PGN'; + + @override + String get studyLike => 'Подобається'; + + @override + String get studyUnlike => 'Не подобається'; + + @override + String get studyNewTag => 'Новий тег'; + + @override + String get studyCommentThisPosition => 'Коментувати цю позицію'; + + @override + String get studyCommentThisMove => 'Коментувати цей хід'; + + @override + String get studyAnnotateWithGlyphs => 'Додати символьну анотацію'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Розділ занадто короткий для аналізу.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Лише співавтори дослідження можуть дати запит на комп\'ютерний аналіз.'; + + @override + String get studyGetAFullComputerAnalysis => 'Отримати повний серверний комп\'ютерний аналіз головної лінії.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Переконайтесь, що розділ завершено. Ви можете дати запит на аналіз лише один раз.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Усі синхронізовані учасники залишаються на тій же позиції'; + + @override + String get studyShareChanges => 'Поділитися змінами з глядачами та зберегти їх на сервері'; + + @override + String get studyPlaying => 'Активні'; + + @override + String get studyShowEvalBar => 'Шкала оцінки'; + + @override + String get studyFirst => 'Перша'; + + @override + String get studyPrevious => 'Попередня'; + + @override + String get studyNext => 'Наступна'; + + @override + String get studyLast => 'Остання'; + @override String get studyShareAndExport => 'Надсилання та експорт'; + @override + String get studyCloneStudy => 'Клонувати'; + + @override + String get studyStudyPgn => 'PGN дослідження'; + + @override + String get studyDownloadAllGames => 'Завантажити всі партії'; + + @override + String get studyChapterPgn => 'PGN розділу'; + + @override + String get studyCopyChapterPgn => 'Скопіювати PGN'; + + @override + String get studyDownloadGame => 'Завантажити гру'; + + @override + String get studyStudyUrl => 'Посилання на дослідження'; + + @override + String get studyCurrentChapterUrl => 'Посилання на цей розділ'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Ви можете вставити цей код на форумі для вбудування'; + + @override + String get studyStartAtInitialPosition => 'Старт з початкової позиції'; + + @override + String studyStartAtX(String param) { + return 'Почати з $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Вбудувати на своєму сайті'; + + @override + String get studyReadMoreAboutEmbedding => 'Докладніше про вбудовування'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Лише публічні дослідження можна вбудовувати!'; + + @override + String get studyOpen => 'Відкрити'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1 надано вам $param2'; + } + + @override + String get studyStudyNotFound => 'Дослідження не знайдено'; + + @override + String get studyEditChapter => 'Редагувати розділ'; + + @override + String get studyNewChapter => 'Новий розділ'; + + @override + String studyImportFromChapterX(String param) { + return 'Імпортувати з $param'; + } + + @override + String get studyOrientation => 'Орієнтація'; + + @override + String get studyAnalysisMode => 'Режим аналізу'; + + @override + String get studyPinnedChapterComment => 'Закріплений коментар розділу'; + + @override + String get studySaveChapter => 'Зберегти розділ'; + + @override + String get studyClearAnnotations => 'Очистити анотацію'; + + @override + String get studyClearVariations => 'Очистити анотацію'; + + @override + String get studyDeleteChapter => 'Видалити розділ'; + + @override + String get studyDeleteThisChapter => 'Видалити цей розділ? Відновити буде неможливо!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Очистити всі коментарі та позначки з цього розділу?'; + + @override + String get studyRightUnderTheBoard => 'Відразу під шахівницею'; + + @override + String get studyNoPinnedComment => 'Немає'; + + @override + String get studyNormalAnalysis => 'Звичайний аналіз'; + + @override + String get studyHideNextMoves => 'Приховати наступні ходи'; + + @override + String get studyInteractiveLesson => 'Інтерактивний урок'; + + @override + String studyChapterX(String param) { + return 'Розділ $param'; + } + + @override + String get studyEmpty => 'Порожній'; + + @override + String get studyStartFromInitialPosition => 'Старт з початкової позиції'; + + @override + String get studyEditor => 'Редактор'; + + @override + String get studyStartFromCustomPosition => 'Почати з обраної позиції'; + + @override + String get studyLoadAGameByUrl => 'Завантажте гру за посиланням'; + + @override + String get studyLoadAPositionFromFen => 'Завантажити позицію з FEN'; + + @override + String get studyLoadAGameFromPgn => 'Завантажити гру з PGN'; + + @override + String get studyAutomatic => 'Автоматично'; + + @override + String get studyUrlOfTheGame => 'Посилання на гру'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Завантажити гру з $param1 або $param2'; + } + + @override + String get studyCreateChapter => 'Створити розділ'; + + @override + String get studyCreateStudy => 'Створити дослідження'; + + @override + String get studyEditStudy => 'Редагування дослідження'; + + @override + String get studyVisibility => 'Видимість'; + + @override + String get studyPublic => 'Публічне'; + + @override + String get studyUnlisted => 'Поза списком'; + + @override + String get studyInviteOnly => 'Лише за запрошенням'; + + @override + String get studyAllowCloning => 'Дозволити копіювання'; + + @override + String get studyNobody => 'Ніхто'; + + @override + String get studyOnlyMe => 'Лише я'; + + @override + String get studyContributors => 'Співавтори'; + + @override + String get studyMembers => 'Учасники'; + + @override + String get studyEveryone => 'Всі'; + + @override + String get studyEnableSync => 'Увімкнути синхронізацію'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Так: однакова позиція для всіх'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Ні: дозволити вільний перегляд'; + + @override + String get studyPinnedStudyComment => 'Закріплений коментар дослідження'; + @override String get studyStart => 'Почати'; + + @override + String get studySave => 'Зберегти'; + + @override + String get studyClearChat => 'Очистити чат'; + + @override + String get studyDeleteTheStudyChatHistory => 'Видалити історію чату дослідження? Відновити буде неможливо!'; + + @override + String get studyDeleteStudy => 'Видалити дослідження'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Ви дійсно бажаєте видалити все дослідження? Назад дороги немає! Введіть назву дослідження для підтвердження: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Де ви хочете це дослідити?'; + + @override + String get studyGoodMove => 'Хороший хід'; + + @override + String get studyMistake => 'Помилка'; + + @override + String get studyBrilliantMove => 'Блискучий хід'; + + @override + String get studyBlunder => 'Груба помилка'; + + @override + String get studyInterestingMove => 'Цікавий хід'; + + @override + String get studyDubiousMove => 'Сумнівний хід'; + + @override + String get studyOnlyMove => 'Єдиний хід'; + + @override + String get studyZugzwang => 'Цугцванг'; + + @override + String get studyEqualPosition => 'Рівна позиція'; + + @override + String get studyUnclearPosition => 'Незрозуміла позиція'; + + @override + String get studyWhiteIsSlightlyBetter => 'Позиція білих трохи краще'; + + @override + String get studyBlackIsSlightlyBetter => 'Позиція чорних трохи краще'; + + @override + String get studyWhiteIsBetter => 'Позиція білих краще'; + + @override + String get studyBlackIsBetter => 'Позиція чорних краще'; + + @override + String get studyWhiteIsWinning => 'Білі перемагають'; + + @override + String get studyBlackIsWinning => 'Чорні перемагають'; + + @override + String get studyNovelty => 'Новинка'; + + @override + String get studyDevelopment => 'Розвиток'; + + @override + String get studyInitiative => 'Ініціатива'; + + @override + String get studyAttack => 'Атака'; + + @override + String get studyCounterplay => 'Контргра'; + + @override + String get studyTimeTrouble => 'Цейтнот'; + + @override + String get studyWithCompensation => 'З компенсацією'; + + @override + String get studyWithTheIdea => 'З ідеєю'; + + @override + String get studyNextChapter => 'Наступний розділ'; + + @override + String get studyPrevChapter => 'Попередній розділ'; + + @override + String get studyStudyActions => 'Команди дослідження'; + + @override + String get studyTopics => 'Теми'; + + @override + String get studyMyTopics => 'Мої теми'; + + @override + String get studyPopularTopics => 'Популярні теми'; + + @override + String get studyManageTopics => 'Управління темами'; + + @override + String get studyBack => 'Назад'; + + @override + String get studyPlayAgain => 'Грати знову'; + + @override + String get studyWhatWouldYouPlay => 'Що б ви грали в цій позиції?'; + + @override + String get studyYouCompletedThisLesson => 'Вітаємо! Ви завершили цей урок.'; + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count розділи', + many: '$count розділів', + few: '$count розділи', + one: '$count розділ', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Партій', + many: '$count Партій', + few: '$count Партії', + one: '$count Партія', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count учасників', + many: '$count учасників', + few: '$count учасники', + one: '$count учасник', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Вставте ваш PGN текст тут, до $count ігор', + many: 'Вставте ваш PGN текст тут, до $count ігор', + few: 'Вставте ваш PGN текст тут, до $count ігор', + one: 'Вставте ваш PGN текст тут, до $count гри', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_vi.dart b/lib/l10n/l10n_vi.dart index 700900240c..585607d0ff 100644 --- a/lib/l10n/l10n_vi.dart +++ b/lib/l10n/l10n_vi.dart @@ -103,9 +103,6 @@ class AppLocalizationsVi extends AppLocalizations { @override String get mobileCancelTakebackOffer => 'Hủy đề nghị đi lại'; - @override - String get mobileCancelDrawOffer => 'Hủy đề nghị hòa'; - @override String get mobileWaitingForOpponentToJoin => 'Đang chờ đối thủ tham gia...'; @@ -131,18 +128,18 @@ class AppLocalizationsVi extends AppLocalizations { String get mobilePuzzleThemesSubtitle => 'Giải câu đố từ những khai cuộc yêu thích của bạn hoặc chọn một chủ đề.'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobilePuzzleStormSubtitle => 'Giải càng nhiều câu đố càng tốt trong 3 phút.'; @override String mobileGreeting(String param) { - return 'Hello, $param'; + return 'Xin chào, $param'; } @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileGreetingWithoutName => 'Xin chào'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobilePrefMagnifyDraggedPiece => 'Phóng to quân cờ được kéo'; @override String get activityActivity => 'Hoạt động'; @@ -156,7 +153,7 @@ class AppLocalizationsVi extends AppLocalizations { } @override - String get activitySignedUp => 'Đã ghi danh ở lichess.org'; + String get activitySignedUp => 'Đã ghi danh tại lichess.org'; @override String activitySupportedNbMonths(int count, String param2) { @@ -238,12 +235,22 @@ class AppLocalizationsVi extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Đã hoàn thành $count ván cờ qua thư $param2', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Đã theo dõi $count người chơi', + other: 'Đã theo dõi $count kỳ thủ', ); return '$_temp0'; } @@ -331,9 +338,225 @@ class AppLocalizationsVi extends AppLocalizations { @override String get broadcastBroadcasts => 'Các phát sóng'; + @override + String get broadcastMyBroadcasts => 'Các phát sóng của tôi'; + @override String get broadcastLiveBroadcasts => 'Các giải đấu phát sóng trực tiếp'; + @override + String get broadcastBroadcastCalendar => 'Lịch phát sóng'; + + @override + String get broadcastNewBroadcast => 'Phát sóng trực tiếp mới'; + + @override + String get broadcastSubscribedBroadcasts => 'Các phát sóng đã đăng ký theo dõi'; + + @override + String get broadcastAboutBroadcasts => 'Giới thiệu về phát sóng'; + + @override + String get broadcastHowToUseLichessBroadcasts => 'Cách sử dụng Phát sóng của Lichess.'; + + @override + String get broadcastTheNewRoundHelp => 'Vòng mới sẽ có các thành viên và cộng tác viên giống như vòng trước.'; + + @override + String get broadcastAddRound => 'Thêm vòng'; + + @override + String get broadcastOngoing => 'Đang diễn ra'; + + @override + String get broadcastUpcoming => 'Sắp diễn ra'; + + @override + String get broadcastCompleted => 'Đã hoàn thành'; + + @override + String get broadcastCompletedHelp => 'Lichess phát hiện việc hoàn thành vòng đấu dựa trên các ván đấu nguồn. Sử dụng nút chuyển đổi này nếu không có nguồn.'; + + @override + String get broadcastRoundName => 'Tên vòng'; + + @override + String get broadcastRoundNumber => 'Vòng đấu số'; + + @override + String get broadcastTournamentName => 'Tên giải đấu'; + + @override + String get broadcastTournamentDescription => 'Mô tả ngắn giải đấu'; + + @override + String get broadcastFullDescription => 'Mô tả đầy đủ giải đấu'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return 'Tùy chọn mô tả dài về giải đấu. Có thể sử dụng $param1. Độ dài phải nhỏ hơn $param2 ký tự.'; + } + + @override + String get broadcastSourceSingleUrl => 'URL Nguồn PGN'; + + @override + String get broadcastSourceUrlHelp => 'URL mà Lichess sẽ khảo sát để nhận cập nhật PGN. Nó phải được truy cập công khai từ Internet.'; + + @override + String get broadcastSourceGameIds => 'Tối đa 64 ID ván cờ trên Lichess, phân tách bằng dấu cách.'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Thời gian bắt đầu của giải theo múi giờ địa phương: $param'; + } + + @override + String get broadcastStartDateHelp => 'Tùy chọn, nếu bạn biết khi nào sự kiện bắt đầu'; + + @override + String get broadcastCurrentGameUrl => 'URL ván đấu hiện tại'; + + @override + String get broadcastDownloadAllRounds => 'Tải về tất cả ván đấu'; + + @override + String get broadcastResetRound => 'Đặt lại vòng này'; + + @override + String get broadcastDeleteRound => 'Xóa vòng này'; + + @override + String get broadcastDefinitivelyDeleteRound => 'Dứt khoát xóa tất cả vòng đấu và các ván đấu trong đó.'; + + @override + String get broadcastDeleteAllGamesOfThisRound => 'Xóa toàn bộ ván cờ trong vòng này. Để tạo lại chúng bạn cần thêm lại nguồn.'; + + @override + String get broadcastEditRoundStudy => 'Chỉnh sửa vòng nghiên cứu'; + + @override + String get broadcastDeleteTournament => 'Xóa giải đấu này'; + + @override + String get broadcastDefinitivelyDeleteTournament => 'Xóa dứt khoát toàn bộ giải đấu, tất cả các vòng và tất cả ván cờ trong đó.'; + + @override + String get broadcastShowScores => 'Hiển thị điểm số của người chơi dựa trên kết quả ván đấu'; + + @override + String get broadcastReplacePlayerTags => 'Tùy chọn: biệt danh, hệ số Elo và danh hiệu'; + + @override + String get broadcastFideFederations => 'Các liên đoàn FIDE'; + + @override + String get broadcastTop10Rating => 'Hệ số Elo top 10'; + + @override + String get broadcastFidePlayers => 'Các kỳ thủ FIDE'; + + @override + String get broadcastFidePlayerNotFound => 'Không tìm thấy kỳ thủ FIDE'; + + @override + String get broadcastFideProfile => 'Hồ sơ FIDE'; + + @override + String get broadcastFederation => 'Liên đoàn'; + + @override + String get broadcastAgeThisYear => 'Tuổi năm nay'; + + @override + String get broadcastUnrated => 'Chưa xếp hạng'; + + @override + String get broadcastRecentTournaments => 'Các giải đấu tham gia gần đây'; + + @override + String get broadcastOpenLichess => 'Mở trên Lichess'; + + @override + String get broadcastTeams => 'Các đội'; + + @override + String get broadcastBoards => 'Các bàn đấu'; + + @override + String get broadcastOverview => 'Tổng quan'; + + @override + String get broadcastSubscribeTitle => 'Đăng ký để được thông báo khi mỗi vòng bắt đầu. Bạn có thể chuyển đổi chuông hoặc thông báo đẩy cho các chương trình phát sóng trong tùy chọn tài khoản của mình.'; + + @override + String get broadcastUploadImage => 'Tải hình ảnh giải đấu lên'; + + @override + String get broadcastNoBoardsYet => 'Chưa có bàn nào. Chúng sẽ xuất hiện khi ván đấu được tải lên.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Bàn đấu có thể được tải bằng nguồn hoặc thông qua $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Bắt đầu sau $param'; + } + + @override + String get broadcastStartVerySoon => 'Chương trình phát sóng sẽ sớm bắt đầu.'; + + @override + String get broadcastNotYetStarted => 'Chương trình phát sóng vẫn chưa bắt đầu.'; + + @override + String get broadcastOfficialWebsite => 'Website chính thức'; + + @override + String get broadcastStandings => 'Bảng xếp hạng'; + + @override + String broadcastIframeHelp(String param) { + return 'Thêm tùy chọn trên $param'; + } + + @override + String get broadcastWebmastersPage => 'trang nhà phát triển web'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'Nguồn PGN công khai, thời gian thực cho vòng này. Chúng tôi cũng cung cấp $param để đồng bộ hóa nhanh hơn và hiệu quả hơn.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Nhúng chương trình phát sóng này vào trang web của bạn'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Nhúng $param vào trang web của bạn'; + } + + @override + String get broadcastRatingDiff => 'Độ thay đổi hệ số'; + + @override + String get broadcastGamesThisTournament => 'Các ván đấu trong giải này'; + + @override + String get broadcastScore => 'Điểm số'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count phát sóng', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return 'Số thách đấu: $param1'; @@ -1203,7 +1426,7 @@ class AppLocalizationsVi extends AppLocalizations { String get puzzleThemeMasterVsMaster => 'Ván đấu giữa 2 kiện tướng'; @override - String get puzzleThemeMasterVsMasterDescription => 'Câu đố từ các ván đấu giữa hai người chơi có danh hiệu.'; + String get puzzleThemeMasterVsMasterDescription => 'Câu đố từ các ván đấu giữa hai kiện tướng.'; @override String get puzzleThemeMate => 'Chiếu hết'; @@ -1368,10 +1591,10 @@ class AppLocalizationsVi extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Đối phương bị giới hạn các nước mà họ có thể đi và tất cả các nước đi ấy đều hại họ.'; @override - String get puzzleThemeHealthyMix => 'Phối hợp nhịp nhàng'; + String get puzzleThemeMix => 'Phối hợp nhịp nhàng'; @override - String get puzzleThemeHealthyMixDescription => 'Mỗi thứ một chút. Bạn không biết được thứ gì đang chờ mình, vậy nên bạn cần phải sẵn sàng cho mọi thứ! Như một ván cờ thật vậy!'; + String get puzzleThemeMixDescription => 'Mỗi thứ một chút. Bạn không biết được thứ gì đang chờ mình, vậy nên bạn cần phải sẵn sàng cho mọi thứ! Như một ván cờ thật vậy!'; @override String get puzzleThemePlayerGames => 'Các ván đấu của người chơi'; @@ -1421,7 +1644,7 @@ class AppLocalizationsVi extends AppLocalizations { String get toInviteSomeoneToPlayGiveThisUrl => 'Để mời ai đó chơi, hãy gửi URL này'; @override - String get gameOver => 'Kết thúc ván cờ'; + String get gameOver => 'Ván cờ kết thúc'; @override String get waitingForOpponent => 'Đang chờ đối thủ'; @@ -1543,7 +1766,7 @@ class AppLocalizationsVi extends AppLocalizations { String get whiteResigned => 'Bên trắng chịu thua'; @override - String get blackResigned => 'Đen chịu thua'; + String get blackResigned => 'Bên đen chịu thua'; @override String get whiteLeftTheGame => 'Bên trắng đã rời khỏi ván cờ'; @@ -1561,13 +1784,13 @@ class AppLocalizationsVi extends AppLocalizations { String get requestAComputerAnalysis => 'Yêu cầu máy tính phân tích'; @override - String get computerAnalysis => 'Máy tính phân tích'; + String get computerAnalysis => 'Phân tích từ máy tính'; @override - String get computerAnalysisAvailable => 'Có sẵn máy tính phân tích'; + String get computerAnalysisAvailable => 'Có sẵn phân tích từ máy tính'; @override - String get computerAnalysisDisabled => 'Phân tích máy tính bị vô hiệu hóa'; + String get computerAnalysisDisabled => 'Phân tích từ máy tính bị vô hiệu hóa'; @override String get analysis => 'Bàn cờ phân tích'; @@ -1677,7 +1900,7 @@ class AppLocalizationsVi extends AppLocalizations { @override String masterDbExplanation(String param1, String param2, String param3) { - return 'Các ván đấu OTB của các kỳ thủ có hệ số Elo FIDE $param1+ từ năm $param2 đến $param3'; + return 'Các ván đấu OTB của các kỳ thủ có hệ số Rating FIDE $param1+ từ năm $param2 đến $param3'; } @override @@ -1775,9 +1998,6 @@ class AppLocalizationsVi extends AppLocalizations { @override String get removesTheDepthLimit => 'Bỏ giới hạn độ sâu và giữ máy tính của bạn mượt hơn'; - @override - String get engineManager => 'Quản lý động cơ'; - @override String get blunder => 'Sai lầm nghiêm trọng'; @@ -1910,7 +2130,7 @@ class AppLocalizationsVi extends AppLocalizations { String get changeUsername => 'Thay đổi tên đăng nhập'; @override - String get changeUsernameNotSame => 'Bạn chỉ có thể thay đổi cách viết hoa/thường. Ví dụ \"johndoe\" thành \"JohnDoe\".'; + String get changeUsernameNotSame => 'Bạn chỉ có thể thay đổi cách viết hoa/thường. Ví dụ \"dotrongkhanh04032012\" thành \"DoTrongKhanh04032012\".'; @override String get changeUsernameDescription => 'Thay đổi tên người dùng của bạn. Điều này chỉ có thể thực hiện một lần và bạn chỉ được thay đổi cách viết hoa/viết thường các chữ trong tên người dùng của bạn.'; @@ -2041,6 +2261,9 @@ class AppLocalizationsVi extends AppLocalizations { @override String get gamesPlayed => 'Số ván đã chơi'; + @override + String get ok => 'OK'; + @override String get cancel => 'Hủy'; @@ -2361,7 +2584,7 @@ class AppLocalizationsVi extends AppLocalizations { String get thisIsAChessCaptcha => 'Đây là mã CAPTCHA cờ vua.'; @override - String get clickOnTheBoardToMakeYourMove => 'Nhấn vào bàn cờ để di chuyển và chứng minh bạn là con người.'; + String get clickOnTheBoardToMakeYourMove => 'Nhấn vào bàn cờ để di chuyển, và chứng minh bạn là con người.'; @override String get captcha_fail => 'Hãy giải mã captcha cờ vua.'; @@ -2750,7 +2973,13 @@ class AppLocalizationsVi extends AppLocalizations { String get other => 'Khác'; @override - String get reportDescriptionHelp => 'Dán đường dẫn đến (các) ván cờ và giải thích về vấn đề của kỳ thủ này. Đừng chỉ nói \"họ gian lận\" mà hãy miêu tả chi tiết nhất có thể. Vấn đề sẽ được giải quyết nhanh hơn nếu bạn viết bằng tiếng Anh.'; + String get reportCheatBoostHelp => 'Dán đường dẫn đến (các) ván cờ và giải thích hành vi sai của kỳ thủ này. Đừng chỉ nói \"họ gian lận\", nhưng hãy cho chúng tôi biết bạn đã đi đến kết luận này như thế nào.'; + + @override + String get reportUsernameHelp => 'Giải thích những gì về tên người dùng này là xúc phạm. Đừng chỉ nói \"nó gây khó chịu/không phù hợp\", nhưng hãy cho chúng tôi biết bạn đã đi đến kết luận này như thế nào, đặc biệt nếu sự xúc phạm bị che giấu, không phải bằng tiếng Anh, là tiếng lóng, hoặc là một tài liệu tham khảo lịch sử/văn hóa.'; + + @override + String get reportProcessedFasterInEnglish => 'Báo cáo của bạn sẽ được xử lý nhanh hơn nếu được viết bằng tiếng Anh.'; @override String get error_provideOneCheatedGameLink => 'Hãy cung cấp ít nhất một đường dẫn đến ván cờ bị gian lận.'; @@ -2921,7 +3150,7 @@ class AppLocalizationsVi extends AppLocalizations { String get opponent => 'Đối thủ'; @override - String get learnMenu => 'Học'; + String get learnMenu => 'Học tập'; @override String get studyMenu => 'Nghiên cứu'; @@ -3205,7 +3434,7 @@ class AppLocalizationsVi extends AppLocalizations { String get tournamentHomeTitle => 'Giải đấu cờ vua với nhiều thiết lập thời gian và biến thể phong phú'; @override - String get tournamentHomeDescription => 'Chơi các giải đấu cờ vua nhịp độ nhanh! Tham gia một giải đấu chính thức hoặc tự tạo giải đấu của bạn. Cờ Đạn, cờ Chớp, cờ Nhanh, cờ Chậm, Chess960, King of the Hill, Threecheck và nhiều lựa chọn khác cho niềm vui đánh cờ vô tận.'; + String get tournamentHomeDescription => 'Chơi các giải đấu cờ vua nhịp độ nhanh! Tham gia một giải đấu chính thức hoặc tự tạo giải đấu của bạn. Cờ đạn, Cờ chớp, Cờ nhanh, Cờ chậm, Chess960, King of the Hill, Threecheck và nhiều lựa chọn khác cho niềm vui đánh cờ vô tận.'; @override String get tournamentNotFound => 'Không tìm thấy giải đấu'; @@ -4055,6 +4284,9 @@ class AppLocalizationsVi extends AppLocalizations { @override String get nothingToSeeHere => 'Không có gì để xem ở đây vào lúc này.'; + @override + String get stats => 'Thống kê'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4657,9 +4889,510 @@ class AppLocalizationsVi extends AppLocalizations { @override String get streamerLichessStreamers => 'Các Streamer của Lichess'; + @override + String get studyPrivate => 'Riêng tư'; + + @override + String get studyMyStudies => 'Các nghiên cứu của tôi'; + + @override + String get studyStudiesIContributeTo => 'Các nghiên cứu tôi đóng góp'; + + @override + String get studyMyPublicStudies => 'Nghiên cứu công khai của tôi'; + + @override + String get studyMyPrivateStudies => 'Nghiên cứu riêng tư của tôi'; + + @override + String get studyMyFavoriteStudies => 'Các nghiên cứu yêu thích của tôi'; + + @override + String get studyWhatAreStudies => 'Nghiên cứu là gì?'; + + @override + String get studyAllStudies => 'Tất cả các nghiên cứu'; + + @override + String studyStudiesCreatedByX(String param) { + return 'Các nghiên cứu được tạo bởi $param'; + } + + @override + String get studyNoneYet => 'Chưa có gì cả.'; + + @override + String get studyHot => 'Thịnh hành'; + + @override + String get studyDateAddedNewest => 'Ngày được thêm (mới nhất)'; + + @override + String get studyDateAddedOldest => 'Ngày được thêm (cũ nhất)'; + + @override + String get studyRecentlyUpdated => 'Được cập nhật gần đây'; + + @override + String get studyMostPopular => 'Phổ biến nhất'; + + @override + String get studyAlphabetical => 'Theo thứ tự chữ cái'; + + @override + String get studyAddNewChapter => 'Thêm một chương mới'; + + @override + String get studyAddMembers => 'Thêm thành viên'; + + @override + String get studyInviteToTheStudy => 'Mời vào nghiên cứu'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => 'Vui lòng chỉ mời những người bạn biết và những người tích cực muốn tham gia nghiên cứu này.'; + + @override + String get studySearchByUsername => 'Tìm kiếm theo tên người dùng'; + + @override + String get studySpectator => 'Khán giả'; + + @override + String get studyContributor => 'Người đóng góp'; + + @override + String get studyKick => 'Đuổi'; + + @override + String get studyLeaveTheStudy => 'Rời khỏi nghiên cứu'; + + @override + String get studyYouAreNowAContributor => 'Bây giờ bạn là một người đóng góp'; + + @override + String get studyYouAreNowASpectator => 'Bây giờ bạn là một khán giả'; + + @override + String get studyPgnTags => 'Nhãn PGN'; + + @override + String get studyLike => 'Thích'; + + @override + String get studyUnlike => 'Bỏ thích'; + + @override + String get studyNewTag => 'Nhãn mới'; + + @override + String get studyCommentThisPosition => 'Bình luận về thế cờ này'; + + @override + String get studyCommentThisMove => 'Bình luận về nước cờ này'; + + @override + String get studyAnnotateWithGlyphs => 'Chú thích bằng dấu'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => 'Chương này quá ngắn để có thể được phân tích.'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => 'Chỉ những người đóng góp nghiên cứu mới có thể yêu cầu máy tính phân tích.'; + + @override + String get studyGetAFullComputerAnalysis => 'Nhận phân tích máy tính phía máy chủ đầy đủ về biến chính.'; + + @override + String get studyMakeSureTheChapterIsComplete => 'Hãy chắc chắn chương đã hoàn thành. Bạn chỉ có thể yêu cầu phân tích 1 lần.'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => 'Đồng bộ hóa tất cả các thành viên trên cùng một thế cờ'; + + @override + String get studyShareChanges => 'Chia sẻ các thay đổi với khán giả và lưu chúng trên máy chủ'; + + @override + String get studyPlaying => 'Đang chơi'; + + @override + String get studyShowEvalBar => 'Thanh lợi thế'; + + @override + String get studyFirst => 'Trang đầu'; + + @override + String get studyPrevious => 'Trang trước'; + + @override + String get studyNext => 'Trang tiếp theo'; + + @override + String get studyLast => 'Trang cuối'; + @override String get studyShareAndExport => 'Chia sẻ & xuất'; + @override + String get studyCloneStudy => 'Nhân bản'; + + @override + String get studyStudyPgn => 'PGN nghiên cứu'; + + @override + String get studyDownloadAllGames => 'Tải về tất cả ván đấu'; + + @override + String get studyChapterPgn => 'PGN chương'; + + @override + String get studyCopyChapterPgn => 'Sao chép PGN'; + + @override + String get studyDownloadGame => 'Tải về ván cờ'; + + @override + String get studyStudyUrl => 'URL nghiên cứu'; + + @override + String get studyCurrentChapterUrl => 'URL chương hiện tại'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => 'Bạn có thể dán cái này để nhúng vào diễn đàn hoặc blog Lichess cá nhân của bạn'; + + @override + String get studyStartAtInitialPosition => 'Bắt đầu từ thế cờ ban đầu'; + + @override + String studyStartAtX(String param) { + return 'Bắt đầu tại nước $param'; + } + + @override + String get studyEmbedInYourWebsite => 'Nhúng vào trang web của bạn'; + + @override + String get studyReadMoreAboutEmbedding => 'Đọc thêm về việc nhúng'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => 'Chỉ các nghiên cứu công khai mới được nhúng!'; + + @override + String get studyOpen => 'Mở'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1 được lấy từ $param2'; + } + + @override + String get studyStudyNotFound => 'Không tìm thấy nghiên cứu nào'; + + @override + String get studyEditChapter => 'Sửa chương'; + + @override + String get studyNewChapter => 'Chương mới'; + + @override + String studyImportFromChapterX(String param) { + return 'Nhập từ chương $param'; + } + + @override + String get studyOrientation => 'Nghiên cứu cho bên'; + + @override + String get studyAnalysisMode => 'Chế độ phân tích'; + + @override + String get studyPinnedChapterComment => 'Đã ghim bình luận chương'; + + @override + String get studySaveChapter => 'Lưu chương'; + + @override + String get studyClearAnnotations => 'Xóa chú thích'; + + @override + String get studyClearVariations => 'Xóa các biến'; + + @override + String get studyDeleteChapter => 'Xóa chương'; + + @override + String get studyDeleteThisChapter => 'Xóa chương này. Sẽ không có cách nào để có thể khôi phục lại!'; + + @override + String get studyClearAllCommentsInThisChapter => 'Xóa tất cả bình luận, dấu chú thích và hình vẽ trong chương này'; + + @override + String get studyRightUnderTheBoard => 'Ngay dưới bàn cờ'; + + @override + String get studyNoPinnedComment => 'Không có'; + + @override + String get studyNormalAnalysis => 'Phân tích thường'; + + @override + String get studyHideNextMoves => 'Ẩn các nước tiếp theo'; + + @override + String get studyInteractiveLesson => 'Bài học tương tác'; + + @override + String studyChapterX(String param) { + return 'Chương $param'; + } + + @override + String get studyEmpty => 'Trống'; + + @override + String get studyStartFromInitialPosition => 'Bắt đầu từ thế cờ ban đầu'; + + @override + String get studyEditor => 'Chỉnh sửa bàn cờ'; + + @override + String get studyStartFromCustomPosition => 'Bắt đầu từ thế cờ tùy chỉnh'; + + @override + String get studyLoadAGameByUrl => 'Tải ván cờ bằng URL'; + + @override + String get studyLoadAPositionFromFen => 'Tải thế cờ từ chuỗi FEN'; + + @override + String get studyLoadAGameFromPgn => 'Tải ván cờ từ PGN'; + + @override + String get studyAutomatic => 'Tự động'; + + @override + String get studyUrlOfTheGame => 'URL của các ván, một URL mỗi dòng'; + + @override + String studyLoadAGameFromXOrY(String param1, String param2) { + return 'Tải ván cờ từ $param1 hoặc $param2'; + } + + @override + String get studyCreateChapter => 'Tạo chương'; + + @override + String get studyCreateStudy => 'Tạo nghiên cứu'; + + @override + String get studyEditStudy => 'Chỉnh sửa nghiên cứu'; + + @override + String get studyVisibility => 'Khả năng hiển thị'; + + @override + String get studyPublic => 'Công khai'; + + @override + String get studyUnlisted => 'Không công khai'; + + @override + String get studyInviteOnly => 'Chỉ những người được mời'; + + @override + String get studyAllowCloning => 'Cho phép tạo bản sao'; + + @override + String get studyNobody => 'Không ai cả'; + + @override + String get studyOnlyMe => 'Chỉ mình tôi'; + + @override + String get studyContributors => 'Những người đóng góp'; + + @override + String get studyMembers => 'Thành viên'; + + @override + String get studyEveryone => 'Mọi người'; + + @override + String get studyEnableSync => 'Kích hoạt tính năng đồng bộ hóa'; + + @override + String get studyYesKeepEveryoneOnTheSamePosition => 'Có: giữ tất cả mọi người trên 1 thế cờ'; + + @override + String get studyNoLetPeopleBrowseFreely => 'Không: để mọi người tự do xem xét'; + + @override + String get studyPinnedStudyComment => 'Bình luận nghiên cứu được ghim'; + @override String get studyStart => 'Bắt đầu'; + + @override + String get studySave => 'Lưu'; + + @override + String get studyClearChat => 'Xóa trò chuyện'; + + @override + String get studyDeleteTheStudyChatHistory => 'Xóa lịch sử trò chuyện nghiên cứu? Không thể khôi phục lại!'; + + @override + String get studyDeleteStudy => 'Xóa nghiên cứu'; + + @override + String studyConfirmDeleteStudy(String param) { + return 'Xóa toàn bộ nghiên cứu? Không có cách nào để khôi phục lại! Nhập tên của nghiên cứu để xác nhận: $param'; + } + + @override + String get studyWhereDoYouWantToStudyThat => 'Bạn muốn nghiên cứu ở đâu?'; + + @override + String get studyGoodMove => 'Nước tốt'; + + @override + String get studyMistake => 'Sai lầm'; + + @override + String get studyBrilliantMove => 'Nước đi thiên tài'; + + @override + String get studyBlunder => 'Sai lầm nghiêm trọng'; + + @override + String get studyInterestingMove => 'Nước đi hay'; + + @override + String get studyDubiousMove => 'Nước đi mơ hồ'; + + @override + String get studyOnlyMove => 'Nước duy nhất'; + + @override + String get studyZugzwang => 'Zugzwang'; + + @override + String get studyEqualPosition => 'Thế trận cân bằng'; + + @override + String get studyUnclearPosition => 'Thế cờ không rõ ràng'; + + @override + String get studyWhiteIsSlightlyBetter => 'Bên trắng có một chút lợi thế'; + + @override + String get studyBlackIsSlightlyBetter => 'Bên đen có một chút lợi thế'; + + @override + String get studyWhiteIsBetter => 'Bên trắng lợi thế hơn'; + + @override + String get studyBlackIsBetter => 'Bên đen lợi thế hơn'; + + @override + String get studyWhiteIsWinning => 'Bên trắng đang thắng dần'; + + @override + String get studyBlackIsWinning => 'Bên đen đang thắng dần'; + + @override + String get studyNovelty => 'Mới lạ'; + + @override + String get studyDevelopment => 'Phát triển'; + + @override + String get studyInitiative => 'Chủ động'; + + @override + String get studyAttack => 'Tấn công'; + + @override + String get studyCounterplay => 'Phản công'; + + @override + String get studyTimeTrouble => 'Sắp hết thời gian'; + + @override + String get studyWithCompensation => 'Có bù đắp'; + + @override + String get studyWithTheIdea => 'Với ý tưởng'; + + @override + String get studyNextChapter => 'Chương tiếp theo'; + + @override + String get studyPrevChapter => 'Chương trước'; + + @override + String get studyStudyActions => 'Các thao tác trong nghiên cứu'; + + @override + String get studyTopics => 'Chủ đề'; + + @override + String get studyMyTopics => 'Chủ đề của tôi'; + + @override + String get studyPopularTopics => 'Chủ đề phổ biến'; + + @override + String get studyManageTopics => 'Quản lý chủ đề'; + + @override + String get studyBack => 'Quay Lại'; + + @override + String get studyPlayAgain => 'Chơi lại'; + + @override + String get studyWhatWouldYouPlay => 'Bạn sẽ làm gì ở thế cờ này?'; + + @override + String get studyYouCompletedThisLesson => 'Chúc mừng! Bạn đã hoàn thành bài học này.'; + + @override + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Chương', + ); + return '$_temp0'; + } + + @override + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Ván cờ', + ); + return '$_temp0'; + } + + @override + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Thành viên', + ); + return '$_temp0'; + } + + @override + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Dán PGN ở đây, tối đa $count ván', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_zh.dart b/lib/l10n/l10n_zh.dart index 75b678ce81..74aaa3f0ea 100644 --- a/lib/l10n/l10n_zh.dart +++ b/lib/l10n/l10n_zh.dart @@ -42,7 +42,7 @@ class AppLocalizationsZh extends AppLocalizations { String get mobileSettingsImmersiveMode => '沉浸模式'; @override - String get mobileSettingsImmersiveModeSubtitle => '播放时隐藏系统UI。 如果您对屏幕边缘的系统导航手势感到困扰,请使用此功能。 适用于游戏和益智风暴屏幕。'; + String get mobileSettingsImmersiveModeSubtitle => '下棋时隐藏系统界面。 如果您的操作受到屏幕边缘的系统导航手势干扰,请使用此功能。 适用于棋局和 Puzzle Storm 界面。'; @override String get mobileNotFollowingAnyUser => '你没有关注任何用户。'; @@ -58,7 +58,7 @@ class AppLocalizationsZh extends AppLocalizations { @override String mobilePlayersMatchingSearchTerm(String param) { - return '拥有\"$param\"的玩家'; + return '包含\"$param\"名称的棋手'; } @override @@ -71,7 +71,7 @@ class AppLocalizationsZh extends AppLocalizations { String get mobilePuzzleStreakAbortWarning => '你将失去你目前的连胜,你的分数将被保存。'; @override - String get mobilePuzzleStormNothingToShow => '没什么好表现的。 玩拼图风暴的一些运行。'; + String get mobilePuzzleStormNothingToShow => '没有记录。 请下几组 Puzzle Storm。'; @override String get mobileSharePuzzle => '分享这个谜题'; @@ -86,16 +86,16 @@ class AppLocalizationsZh extends AppLocalizations { String get mobileSharePositionAsFEN => '保存局面为 FEN'; @override - String get mobileShowVariations => '显示变化'; + String get mobileShowVariations => '显示变着'; @override - String get mobileHideVariation => '隐藏变异'; + String get mobileHideVariation => '隐藏变着'; @override String get mobileShowComments => '显示评论'; @override - String get mobilePuzzleStormConfirmEndRun => '你想结束这次跑步吗?'; + String get mobilePuzzleStormConfirmEndRun => '你想结束这组吗?'; @override String get mobilePuzzleStormFilterNothingToShow => '没有显示,请更改过滤器'; @@ -103,9 +103,6 @@ class AppLocalizationsZh extends AppLocalizations { @override String get mobileCancelTakebackOffer => '取消悔棋请求'; - @override - String get mobileCancelDrawOffer => '取消和棋请求'; - @override String get mobileWaitingForOpponentToJoin => '正在等待对手加入...'; @@ -119,19 +116,19 @@ class AppLocalizationsZh extends AppLocalizations { String get mobileCustomGameJoinAGame => '加入一局游戏'; @override - String get mobileCorrespondenceClearSavedMove => '清除已保存的移动'; + String get mobileCorrespondenceClearSavedMove => '清除已保存的着法'; @override - String get mobileSomethingWentWrong => '发生一些错误。'; + String get mobileSomethingWentWrong => '出了一些问题。'; @override String get mobileShowResult => '显示结果'; @override - String get mobilePuzzleThemesSubtitle => '从你最喜欢的开口玩拼图,或选择一个主题。'; + String get mobilePuzzleThemesSubtitle => '从你最喜欢的开局解决谜题,或选择一个主题。'; @override - String get mobilePuzzleStormSubtitle => '在3分钟内尽可能多地解决谜题'; + String get mobilePuzzleStormSubtitle => '在3分钟内解决尽可能多的谜题。'; @override String mobileGreeting(String param) { @@ -142,7 +139,7 @@ class AppLocalizationsZh extends AppLocalizations { String get mobileGreetingWithoutName => '你好!'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobilePrefMagnifyDraggedPiece => '放大正在拖动的棋子'; @override String get activityActivity => '动态'; @@ -238,6 +235,17 @@ class AppLocalizationsZh extends AppLocalizations { return '$_temp0'; } + @override + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Completed $count $param2 correspondence games', + one: 'Completed $count $param2 correspondence game', + ); + return '$_temp0'; + } + @override String activityFollowedNbPlayers(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -331,9 +339,225 @@ class AppLocalizationsZh extends AppLocalizations { @override String get broadcastBroadcasts => '转播'; + @override + String get broadcastMyBroadcasts => '我的直播'; + @override String get broadcastLiveBroadcasts => '赛事转播'; + @override + String get broadcastBroadcastCalendar => '转播日程表'; + + @override + String get broadcastNewBroadcast => '新建实况转播'; + + @override + String get broadcastSubscribedBroadcasts => '已订阅的转播'; + + @override + String get broadcastAboutBroadcasts => '关于转播'; + + @override + String get broadcastHowToUseLichessBroadcasts => '如何使用Lichess转播'; + + @override + String get broadcastTheNewRoundHelp => '新一轮的成员和贡献者将与前一轮相同。'; + + @override + String get broadcastAddRound => '添加一轮'; + + @override + String get broadcastOngoing => '进行中'; + + @override + String get broadcastUpcoming => '即将举行'; + + @override + String get broadcastCompleted => '已完成'; + + @override + String get broadcastCompletedHelp => 'Lichess基于源游戏检测游戏的完成状态。如果没有源,请使用此选项。'; + + @override + String get broadcastRoundName => '轮次名称'; + + @override + String get broadcastRoundNumber => '轮数'; + + @override + String get broadcastTournamentName => '锦标赛名称'; + + @override + String get broadcastTournamentDescription => '锦标赛简短描述'; + + @override + String get broadcastFullDescription => '赛事详情'; + + @override + String broadcastFullDescriptionHelp(String param1, String param2) { + return '转播内容的详细描述 (可选)。可以使用 $param1,字数少于 $param2 个。'; + } + + @override + String get broadcastSourceSingleUrl => 'PGN的URL源'; + + @override + String get broadcastSourceUrlHelp => 'Lichess 将从该网址搜查 PGN 的更新。它必须是公开的。'; + + @override + String get broadcastSourceGameIds => '多达64个 Lichess 棋局Id,用空格隔开。'; + + @override + String broadcastStartDateTimeZone(String param) { + return 'Start date in the tournament local timezone: $param'; + } + + @override + String get broadcastStartDateHelp => '如果你知道比赛开始时间 (可选)'; + + @override + String get broadcastCurrentGameUrl => '当前棋局链接'; + + @override + String get broadcastDownloadAllRounds => '下载所有棋局'; + + @override + String get broadcastResetRound => '重置此轮'; + + @override + String get broadcastDeleteRound => '删除此轮'; + + @override + String get broadcastDefinitivelyDeleteRound => '确定删除该回合及其游戏。'; + + @override + String get broadcastDeleteAllGamesOfThisRound => '删除此回合的所有游戏。源需要激活才能重新创建。'; + + @override + String get broadcastEditRoundStudy => '编辑该轮次的棋局研究'; + + @override + String get broadcastDeleteTournament => '删除该锦标赛'; + + @override + String get broadcastDefinitivelyDeleteTournament => '确定删除整个锦标赛、所有轮次和其中所有比赛。'; + + @override + String get broadcastShowScores => '根据比赛结果显示棋手分数'; + + @override + String get broadcastReplacePlayerTags => '可选项:替换选手的名字、等级分和头衔'; + + @override + String get broadcastFideFederations => 'FIDE 成员国'; + + @override + String get broadcastTop10Rating => '前10名等级分'; + + @override + String get broadcastFidePlayers => 'FIDE 棋手'; + + @override + String get broadcastFidePlayerNotFound => '未找到 FIDE 棋手'; + + @override + String get broadcastFideProfile => 'FIDE个人资料'; + + @override + String get broadcastFederation => '棋联'; + + @override + String get broadcastAgeThisYear => '今年的年龄'; + + @override + String get broadcastUnrated => '未评级'; + + @override + String get broadcastRecentTournaments => '最近的比赛'; + + @override + String get broadcastOpenLichess => 'Open in Lichess'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Overview'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Official website'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'Score'; + + @override + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 直播', + ); + return '$_temp0'; + } + @override String challengeChallengesX(String param1) { return '挑战: $param1'; @@ -425,7 +649,7 @@ class AppLocalizationsZh extends AppLocalizations { String get patronDonate => '捐赠'; @override - String get patronLichessPatron => '赞助 Lichess'; + String get patronLichessPatron => 'Lichess赞助者账号'; @override String perfStatPerfStats(String param) { @@ -536,7 +760,7 @@ class AppLocalizationsZh extends AppLocalizations { String get preferencesPreferences => '偏好设置'; @override - String get preferencesDisplay => '显示'; + String get preferencesDisplay => '界面设置'; @override String get preferencesPrivacy => '隐私设置'; @@ -1368,10 +1592,10 @@ class AppLocalizationsZh extends AppLocalizations { String get puzzleThemeZugzwangDescription => '对手可选的着法是有限的,并且所有着法都会使其局面更加恶化。'; @override - String get puzzleThemeHealthyMix => '健康搭配'; + String get puzzleThemeMix => '健康搭配'; @override - String get puzzleThemeHealthyMixDescription => '每个主题中选取一些。你不知道会出现什么,因此得时刻打起精神! 就像在真实对局中一样。'; + String get puzzleThemeMixDescription => '每个主题中选取一些。你不知道会出现什么,因此得时刻打起精神! 就像在真实对局中一样。'; @override String get puzzleThemePlayerGames => '玩家对局'; @@ -1444,7 +1668,7 @@ class AppLocalizationsZh extends AppLocalizations { String get level => '级别'; @override - String get strength => '电脑的难度'; + String get strength => '强度'; @override String get toggleTheChat => '聊天开关'; @@ -1480,10 +1704,10 @@ class AppLocalizationsZh extends AppLocalizations { String get createAGame => '创建对局'; @override - String get whiteIsVictorious => '白方胜'; + String get whiteIsVictorious => '白方胜利'; @override - String get blackIsVictorious => '黑方胜'; + String get blackIsVictorious => '黑方胜利'; @override String get youPlayTheWhitePieces => '你执白棋'; @@ -1492,7 +1716,7 @@ class AppLocalizationsZh extends AppLocalizations { String get youPlayTheBlackPieces => '你执黑棋'; @override - String get itsYourTurn => '轮到你了!'; + String get itsYourTurn => '你的回合!'; @override String get cheatDetected => '检测到作弊'; @@ -1501,10 +1725,10 @@ class AppLocalizationsZh extends AppLocalizations { String get kingInTheCenter => '王占中'; @override - String get threeChecks => '三次将军'; + String get threeChecks => '三次将军胜'; @override - String get raceFinished => '比赛结束'; + String get raceFinished => '竞王结束'; @override String get variantEnding => '变种结束'; @@ -1525,16 +1749,16 @@ class AppLocalizationsZh extends AppLocalizations { String get blackPlays => '黑方走棋'; @override - String get opponentLeftChoices => '您的对手可能已离开棋局。您可以宣布胜利,和棋,或继续等待。'; + String get opponentLeftChoices => '你的对手已离开棋局。你可以宣布胜利、和棋或继续等待。'; @override String get forceResignation => '宣布胜利'; @override - String get forceDraw => '和棋'; + String get forceDraw => '宣布和棋'; @override - String get talkInChat => '聊天请注意文明用语。'; + String get talkInChat => '聊天请注意文明用语!'; @override String get theFirstPersonToComeOnThisUrlWillPlayWithYou => '第一个访问此网址的人将与你下棋。'; @@ -1599,7 +1823,7 @@ class AppLocalizationsZh extends AppLocalizations { String get showThreat => '显示威胁'; @override - String get inLocalBrowser => '在本地浏览器'; + String get inLocalBrowser => '本地浏览器'; @override String get toggleLocalEvaluation => '切换到本地分析'; @@ -1629,7 +1853,7 @@ class AppLocalizationsZh extends AppLocalizations { String get move => '着法'; @override - String get variantLoss => '变体输了'; + String get variantLoss => '变体输棋'; @override String get variantWin => '变体胜利'; @@ -1647,7 +1871,7 @@ class AppLocalizationsZh extends AppLocalizations { String get close => '关闭'; @override - String get winning => '赢棋'; + String get winning => '胜棋'; @override String get losing => '输棋'; @@ -1656,7 +1880,7 @@ class AppLocalizationsZh extends AppLocalizations { String get drawn => '和棋'; @override - String get unknown => '结局未知'; + String get unknown => '未知'; @override String get database => '数据库'; @@ -1670,24 +1894,24 @@ class AppLocalizationsZh extends AppLocalizations { } @override - String get recentGames => '最近对局'; + String get recentGames => '最近棋局'; @override String get topGames => '名局'; @override String masterDbExplanation(String param1, String param2, String param3) { - return '$param2-$param3年国际棋联等级分$param1以上棋手的两百万局棋谱'; + return '$param2-$param3年国际棋联等级分$param1以上棋手的棋谱'; } @override String get dtzWithRounding => '经过四舍五入的DTZ50\'\',是基于到下次吃子或兵动的半步数目。'; @override - String get noGameFound => '没找到符合要求的棋局'; + String get noGameFound => '未找到棋局'; @override - String get maxDepthReached => '已达最大深度!'; + String get maxDepthReached => '已达到最大深度!'; @override String get maybeIncludeMoreGamesFromThePreferencesMenu => '请尝试在“选择”菜单内包括更多棋局。'; @@ -1775,9 +1999,6 @@ class AppLocalizationsZh extends AppLocalizations { @override String get removesTheDepthLimit => '取消深度限制(会提升电脑温度)'; - @override - String get engineManager => '引擎管理'; - @override String get blunder => '漏着'; @@ -2041,6 +2262,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get gamesPlayed => '棋局'; + @override + String get ok => 'OK'; + @override String get cancel => '取消'; @@ -2102,7 +2326,7 @@ class AppLocalizationsZh extends AppLocalizations { String get customPosition => '自定义位置'; @override - String get unlimited => '无限时间'; + String get unlimited => '无限制'; @override String get mode => '模式'; @@ -2693,7 +2917,7 @@ class AppLocalizationsZh extends AppLocalizations { String get website => '网站'; @override - String get mobile => '流动电话'; + String get mobile => '移动端'; @override String get help => '帮助:'; @@ -2750,7 +2974,13 @@ class AppLocalizationsZh extends AppLocalizations { String get other => '其他'; @override - String get reportDescriptionHelp => '请附上棋局链接解释该用户的行为问题。例如如果你怀疑某用户作弊,请不要只说 “对手作弊”。请解释为什么你认为对手作弊。如果你用英语举报,我们将会更快作出答复。'; + String get reportCheatBoostHelp => '请附上棋局链接解释该用户的行为问题。请不要只说 “对手作弊”,而是解释为什么你认为对手作弊。'; + + @override + String get reportUsernameHelp => '解释这个用户名为何具有冒犯性。不要只说“它具有冒犯性/不恰当”,而是要告诉我们你是如何得出这个结论的,特别是如果侮辱性内容是隐晦的、非英语的、俚语或有历史/文化参考。'; + + @override + String get reportProcessedFasterInEnglish => '如果您使用英语举报,我们将会更快作出答复。'; @override String get error_provideOneCheatedGameLink => '请提供至少一局作弊的棋局的链接。'; @@ -4055,6 +4285,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get nothingToSeeHere => '此刻没有什么可看的。'; + @override + String get stats => 'Stats'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -4658,4347 +4891,5889 @@ class AppLocalizationsZh extends AppLocalizations { String get streamerLichessStreamers => 'Lichess 主播'; @override - String get studyShareAndExport => '分享并导出'; + String get studyPrivate => '私人'; @override - String get studyStart => '开始'; -} + String get studyMyStudies => '我的研讨'; -/// The translations for Chinese, as used in Taiwan (`zh_TW`). -class AppLocalizationsZhTw extends AppLocalizationsZh { - AppLocalizationsZhTw(): super('zh_TW'); + @override + String get studyStudiesIContributeTo => '我贡献的研讨'; @override - String get mobileHomeTab => '主頁'; + String get studyMyPublicStudies => '我的公开研讨'; @override - String get mobilePuzzlesTab => '謎題'; + String get studyMyPrivateStudies => '我的私有研讨'; @override - String get mobileToolsTab => '工具'; + String get studyMyFavoriteStudies => '我收藏的研讨'; @override - String get mobileWatchTab => '觀看'; + String get studyWhatAreStudies => '什么是研讨?'; @override - String get mobileSettingsTab => '設置'; + String get studyAllStudies => '所有研讨'; @override - String get mobileMustBeLoggedIn => '你必須登入才能查看此頁面。'; + String studyStudiesCreatedByX(String param) { + return '由 $param 创建的研讨'; + } @override - String get mobileSystemColors => '系统颜色'; + String get studyNoneYet => '暂无。'; @override - String get mobileFeedbackButton => '問題反饋'; + String get studyHot => '热门'; @override - String get mobileOkButton => '確認'; + String get studyDateAddedNewest => '添加时间 (最新)'; @override - String get mobileSettingsHapticFeedback => '震動回饋'; + String get studyDateAddedOldest => '添加时间 (最早)'; @override - String get mobileSettingsImmersiveMode => '沉浸模式'; + String get studyRecentlyUpdated => '最近更新'; @override - String get mobileAllGames => '所有遊戲'; + String get studyMostPopular => '最受欢迎'; @override - String get mobileRecentSearches => '最近搜尋'; + String get studyAlphabetical => '按字母顺序'; @override - String get mobileClearButton => '清除'; + String get studyAddNewChapter => '添加一个新章节'; @override - String get mobileNoSearchResults => '無結果'; + String get studyAddMembers => '添加成员'; @override - String get mobileAreYouSure => '您確定嗎?'; + String get studyInviteToTheStudy => '邀请参加研讨'; @override - String get mobileShareGamePGN => '分享 PGN'; + String get studyPleaseOnlyInvitePeopleYouKnow => '请仅邀请你认识的并且积极希望参与这个研讨的成员'; @override - String get mobileCustomGameJoinAGame => '加入遊戲'; + String get studySearchByUsername => '按用户名搜索'; @override - String get activityActivity => '活動'; + String get studySpectator => '旁观者'; @override - String get activityHostedALiveStream => '主持一個現場直播'; + String get studyContributor => '贡献者'; @override - String activityRankedInSwissTournament(String param1, String param2) { - return '在$param2中排名$param1'; - } + String get studyKick => '踢出'; @override - String get activitySignedUp => '在lichess.org中註冊'; + String get studyLeaveTheStudy => '离开研讨'; @override - String activitySupportedNbMonths(int count, String param2) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '以$param2的身分支持lichess.org$count個月', - ); - return '$_temp0'; - } + String get studyYouAreNowAContributor => '你现在是一位贡献者'; @override - String activityPracticedNbPositions(int count, String param2) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '在$param2練習了$count個棋局', - ); - return '$_temp0'; - } + String get studyYouAreNowASpectator => '你现在是一位旁观者'; @override - String activitySolvedNbPuzzles(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '解決了$count個戰術題目', - ); - return '$_temp0'; - } + String get studyPgnTags => 'PGN 标签'; @override - String activityPlayedNbGames(int count, String param2) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '下了$count場$param2類型的棋局', - ); - return '$_temp0'; - } + String get studyLike => '赞'; @override - String activityPostedNbMessages(int count, String param2) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '在$param2發表了$count則訊息', - ); - return '$_temp0'; - } + String get studyUnlike => '取消赞'; @override - String activityPlayedNbMoves(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '下了$count步', - ); - return '$_temp0'; - } + String get studyNewTag => '新建标签'; @override - String activityInNbCorrespondenceGames(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '在$count場長時間棋局中', - ); - return '$_temp0'; - } + String get studyCommentThisPosition => '评论当前局面'; @override - String activityCompletedNbGames(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '完成了$count場長時間棋局', - ); - return '$_temp0'; - } + String get studyCommentThisMove => '评论这步走法'; @override - String activityFollowedNbPlayers(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '開始關注$count個玩家', - ); - return '$_temp0'; - } + String get studyAnnotateWithGlyphs => '用符号标注'; @override - String activityGainedNbFollowers(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '增加了$count個追蹤者', - ); - return '$_temp0'; - } + String get studyTheChapterIsTooShortToBeAnalysed => '本章节太短,无法进行分析。'; @override - String activityHostedNbSimuls(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '主持了$count場車輪戰', - ); - return '$_temp0'; - } + String get studyOnlyContributorsCanRequestAnalysis => '只有贡献者可以请求服务器分析。'; @override - String activityJoinedNbSimuls(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '加入了$count場車輪戰', - ); - return '$_temp0'; - } + String get studyGetAFullComputerAnalysis => '请求服务器完整地分析主线走法。'; @override - String activityCreatedNbStudies(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '創造了$count個新的研究', - ); - return '$_temp0'; - } + String get studyMakeSureTheChapterIsComplete => '请确保章节已完成。你只能请求分析一次。'; @override - String activityCompetedInNbTournaments(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '完成了$count場錦標賽', - ); - return '$_temp0'; - } + String get studyAllSyncMembersRemainOnTheSamePosition => 'SYNC 中所有成员处于相同局面'; @override - String activityRankedInTournament(int count, String param2, String param3, String param4) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '在$param4錦標賽中下了$param3盤棋局,排名第$count(前$param2%)', - ); - return '$_temp0'; - } + String get studyShareChanges => '与旁观者共享更改并云端保存'; @override - String activityCompetedInNbSwissTournaments(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '參與過$count\'場瑞士制錦標賽', - ); - return '$_temp0'; - } + String get studyPlaying => '正在对局'; @override - String activityJoinedNbTeams(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '加入$count團隊', - ); - return '$_temp0'; - } + String get studyShowEvalBar => '评估条'; @override - String get broadcastBroadcasts => '比賽直播'; + String get studyFirst => '首页'; @override - String get broadcastLiveBroadcasts => '錦標賽直播'; + String get studyPrevious => '上一页'; @override - String challengeChallengesX(String param1) { - return '挑戰: $param1'; - } + String get studyNext => '下一页'; @override - String get challengeChallengeToPlay => '邀請對弈'; + String get studyLast => '末页'; @override - String get challengeChallengeDeclined => '對弈邀請已拒絕'; + String get studyShareAndExport => '分享并导出'; @override - String get challengeChallengeAccepted => '對弈邀請已接受'; + String get studyCloneStudy => '复制棋局'; @override - String get challengeChallengeCanceled => '對弈邀請已撤銷'; + String get studyStudyPgn => '研究 PGN'; @override - String get challengeRegisterToSendChallenges => '請登入以向其他人發出對弈邀請'; + String get studyDownloadAllGames => '下载所有棋局'; @override - String challengeYouCannotChallengeX(String param) { - return '你無法向$param發出對弈邀請'; - } + String get studyChapterPgn => '章节PGN'; @override - String challengeXDoesNotAcceptChallenges(String param) { - return '$param沒有接受對弈邀請'; - } + String get studyCopyChapterPgn => '复制PGN'; @override - String challengeYourXRatingIsTooFarFromY(String param1, String param2) { - return '您的$param1積分與$param2相差太多'; - } + String get studyDownloadGame => '下载棋局'; @override - String challengeCannotChallengeDueToProvisionalXRating(String param) { - return '由於您的$param積分不夠穩定,無法發出挑戰。'; - } + String get studyStudyUrl => '研究链接'; @override - String challengeXOnlyAcceptsChallengesFromFriends(String param) { - return '$param只接受好友的對弈邀請'; - } + String get studyCurrentChapterUrl => '当前章节链接'; @override - String get challengeDeclineGeneric => '我目前不接受對弈'; + String get studyYouCanPasteThisInTheForumToEmbed => '你可以将此粘贴到论坛以嵌入章节'; @override - String get challengeDeclineLater => '我現在不接受對弈,請晚點再詢問'; + String get studyStartAtInitialPosition => '从初始局面开始'; @override - String get challengeDeclineTooFast => '這個時間控制對我來說太快了,請用慢一點的遊戲再次挑戰。'; + String studyStartAtX(String param) { + return '从 $param 开始'; + } @override - String get challengeDeclineTooSlow => '這個時間控制對我來說太慢了,請用快一點的遊戲再次挑戰。'; + String get studyEmbedInYourWebsite => '嵌入到你的网站上'; @override - String get challengeDeclineTimeControl => '我不接受這個挑戰的時間控制。'; + String get studyReadMoreAboutEmbedding => '阅读更多关于嵌入的信息'; @override - String get challengeDeclineRated => '請向我發送積分對弈。'; + String get studyOnlyPublicStudiesCanBeEmbedded => '只能嵌入隐私设置为公开的研究!'; @override - String get challengeDeclineCasual => '請向我發送休閒對弈。'; + String get studyOpen => '打开'; @override - String get challengeDeclineStandard => '我不接受變體對弈。'; + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1 由 $param2 提供'; + } @override - String get challengeDeclineVariant => '我現在不想玩這個變體。'; + String get studyStudyNotFound => '找不到研究'; @override - String get challengeDeclineNoBot => '我不接受機器人的對弈。'; + String get studyEditChapter => '编辑章节'; @override - String get challengeDeclineOnlyBot => '我目前只接受機器人的對弈。'; + String get studyNewChapter => '新章节'; @override - String get challengeInviteLichessUser => '或邀請一位 Lichess 用户:'; + String studyImportFromChapterX(String param) { + return '从 $param 导入'; + } @override - String get contactContact => '聯繫我們'; + String get studyOrientation => '视角'; @override - String get contactContactLichess => '聯繫 Lichess'; + String get studyAnalysisMode => '分析模式'; @override - String get patronDonate => '捐款'; + String get studyPinnedChapterComment => '置顶评论'; @override - String get patronLichessPatron => 'Lichess 贊助者'; + String get studySaveChapter => '保存章节'; @override - String perfStatPerfStats(String param) { - return '$param戰績'; - } + String get studyClearAnnotations => '清除注释'; @override - String get perfStatViewTheGames => '查看遊戲紀錄'; + String get studyClearVariations => '清除变着'; @override - String get perfStatProvisional => '臨時'; + String get studyDeleteChapter => '删除章节'; @override - String get perfStatNotEnoughRatedGames => '積分賽場次太少,無法計算準確積分。'; + String get studyDeleteThisChapter => '删除本章节?本操作无法撤销!'; @override - String perfStatProgressOverLastXGames(String param) { - return '最近$param場棋局之積分變化:'; - } + String get studyClearAllCommentsInThisChapter => '清除章节中所有信息?'; @override - String perfStatRatingDeviation(String param) { - return '積分誤差: $param'; - } + String get studyRightUnderTheBoard => '正下方'; @override - String perfStatRatingDeviationTooltip(String param1, String param2, String param3) { - return '越低的數值代表積分越穩定。 數值高於$param1時的積分會被判定為浮動積分。\n要被列入排名之中,該數值需低於$param2(標準西洋棋) 或是$param3(西洋棋變體)。'; - } + String get studyNoPinnedComment => '不需要'; @override - String get perfStatTotalGames => '總計棋局'; + String get studyNormalAnalysis => '普通模式'; @override - String get perfStatRatedGames => '積分棋局'; + String get studyHideNextMoves => '隐藏下一步'; @override - String get perfStatTournamentGames => '聯賽棋局'; + String get studyInteractiveLesson => '互动课'; @override - String get perfStatBerserkedGames => '狂暴模式棋局'; + String studyChapterX(String param) { + return '章节 $param'; + } @override - String get perfStatTimeSpentPlaying => '奕棋時間'; + String get studyEmpty => '空白'; @override - String get perfStatAverageOpponent => '對手平均積分'; + String get studyStartFromInitialPosition => '从初始局面开始'; @override - String get perfStatVictories => '勝場'; + String get studyEditor => '编辑器'; @override - String get perfStatDefeats => '敗場'; + String get studyStartFromCustomPosition => '从自定义局面开始'; @override - String get perfStatDisconnections => '斷線場次'; + String get studyLoadAGameByUrl => '通过 URL 加载游戏'; @override - String get perfStatNotEnoughGames => '棋局數不夠多'; + String get studyLoadAPositionFromFen => '从 FEN 加载一个局面'; @override - String perfStatHighestRating(String param) { - return '最高積分:$param'; - } + String get studyLoadAGameFromPgn => '从 PGN 文件加载游戏'; @override - String perfStatLowestRating(String param) { - return '最低積分:$param'; - } + String get studyAutomatic => '自动'; @override - String perfStatFromXToY(String param1, String param2) { - return '從$param1到$param2'; - } + String get studyUrlOfTheGame => '游戏的 URL'; @override - String get perfStatWinningStreak => '連勝場數'; + String studyLoadAGameFromXOrY(String param1, String param2) { + return '从 $param1 或 $param2 加载游戏'; + } @override - String get perfStatLosingStreak => '連敗場數'; + String get studyCreateChapter => '创建章节'; @override - String perfStatLongestStreak(String param) { - return '最長紀錄:$param'; - } + String get studyCreateStudy => '创建课程'; @override - String perfStatCurrentStreak(String param) { - return '目前記錄:$param'; - } + String get studyEditStudy => '编辑课程'; @override - String get perfStatBestRated => '積分賽勝場之最強對手'; + String get studyVisibility => '权限'; @override - String get perfStatGamesInARow => '連續奕棋場數'; + String get studyPublic => '公开'; @override - String get perfStatLessThanOneHour => '兩場間距不到一小時'; + String get studyUnlisted => '未列出'; @override - String get perfStatMaxTimePlaying => '最高奕棋時間'; + String get studyInviteOnly => '仅限邀请'; @override - String get perfStatNow => '現在'; + String get studyAllowCloning => '允许复制'; @override - String get preferencesPreferences => '偏好設置'; + String get studyNobody => '没人'; @override - String get preferencesDisplay => '顯示'; + String get studyOnlyMe => '仅自己'; @override - String get preferencesPrivacy => '隱私'; + String get studyContributors => '贡献者'; @override - String get preferencesNotifications => '通知'; + String get studyMembers => '成员'; @override - String get preferencesPieceAnimation => '棋子動畫'; + String get studyEveryone => '所有人'; @override - String get preferencesMaterialDifference => '子力差距'; + String get studyEnableSync => '允许同步'; @override - String get preferencesBoardHighlights => '棋盤高亮 (最後一步與將軍)'; + String get studyYesKeepEveryoneOnTheSamePosition => '确认:每个人都处于同样的局面'; @override - String get preferencesPieceDestinations => '棋子目的地(有效走法與預先走棋)'; + String get studyNoLetPeopleBrowseFreely => '取消:让玩家自由选择'; @override - String get preferencesBoardCoordinates => '棋盤座標(A-H, 1-8)'; + String get studyPinnedStudyComment => '置顶评论'; @override - String get preferencesMoveListWhilePlaying => '遊戲進行時顯示棋譜'; + String get studyStart => '开始'; @override - String get preferencesPgnPieceNotation => '棋譜記法'; + String get studySave => '保存'; @override - String get preferencesChessPieceSymbol => '棋子符號'; + String get studyClearChat => '清空对话'; @override - String get preferencesPgnLetter => '字母 (K, Q, R, B, N)'; + String get studyDeleteTheStudyChatHistory => '删除课程聊天记录?本操作无法撤销!'; @override - String get preferencesZenMode => '專注模式'; + String get studyDeleteStudy => '删除课程'; @override - String get preferencesShowPlayerRatings => '顯示玩家等級分'; + String studyConfirmDeleteStudy(String param) { + return '确定删除整个研讨?该操作不可恢复,输入研讨名以确认:$param'; + } @override - String get preferencesExplainShowPlayerRatings => '這允許隱藏本網站上的所有等級分,以輔助專心下棋。每局遊戲仍可以計算及改變等級分,這個設定只會影響到你是否看得到此分數。'; + String get studyWhereDoYouWantToStudyThat => '你想从哪里开始此项研究?'; @override - String get preferencesDisplayBoardResizeHandle => '顯示盤面大小調整區塊'; + String get studyGoodMove => '好棋'; @override - String get preferencesOnlyOnInitialPosition => '只在起始局面'; + String get studyMistake => '错着'; @override - String get preferencesInGameOnly => '只在遊戲中'; + String get studyBrilliantMove => '极好'; @override - String get preferencesChessClock => '棋鐘'; + String get studyBlunder => '漏着'; @override - String get preferencesTenthsOfSeconds => '十分之一秒'; + String get studyInterestingMove => '略好'; @override - String get preferencesWhenTimeRemainingLessThanTenSeconds => '當剩餘時間小於10秒'; + String get studyDubiousMove => '略坏'; @override - String get preferencesHorizontalGreenProgressBars => '綠色橫進度條'; + String get studyOnlyMove => '唯一着法'; @override - String get preferencesSoundWhenTimeGetsCritical => '時間不足時聲音提醒'; + String get studyZugzwang => 'Zugzwang'; @override - String get preferencesGiveMoreTime => '給對方更多時間'; + String get studyEqualPosition => '均势'; @override - String get preferencesGameBehavior => '對局行為'; + String get studyUnclearPosition => '局势不明'; @override - String get preferencesHowDoYouMovePieces => '移動棋子方式?'; + String get studyWhiteIsSlightlyBetter => '白方略优'; @override - String get preferencesClickTwoSquares => '點擊棋子及目標位置'; + String get studyBlackIsSlightlyBetter => '黑方略优'; @override - String get preferencesDragPiece => '拖曳棋子'; + String get studyWhiteIsBetter => '白方占优'; @override - String get preferencesBothClicksAndDrag => '兩者都行'; + String get studyBlackIsBetter => '黑方占优'; @override - String get preferencesPremovesPlayingDuringOpponentTurn => '預先走棋(在對手的回合走棋)'; + String get studyWhiteIsWinning => '白方即胜'; @override - String get preferencesTakebacksWithOpponentApproval => '悔棋(經過對手同意)'; + String get studyBlackIsWinning => '黑方即胜'; @override - String get preferencesInCasualGamesOnly => '僅限非正式遊戲'; + String get studyNovelty => '新奇的'; @override - String get preferencesPromoteToQueenAutomatically => '兵自動升為后'; + String get studyDevelopment => '发展'; @override - String get preferencesExplainPromoteToQueenAutomatically => '升變的同時按住以暫時取消自動升變'; + String get studyInitiative => '占据主动'; @override - String get preferencesWhenPremoving => '預先走棋時'; + String get studyAttack => '攻击'; @override - String get preferencesClaimDrawOnThreefoldRepetitionAutomatically => '在三次重覆局面時自動要求和局'; + String get studyCounterplay => '反击'; @override - String get preferencesWhenTimeRemainingLessThanThirtySeconds => '當剩餘時間小於30秒'; + String get studyTimeTrouble => '无暇多虑'; @override - String get preferencesMoveConfirmation => '走棋確認'; + String get studyWithCompensation => '优势补偿'; @override - String get preferencesExplainCanThenBeTemporarilyDisabled => '可以在遊戲中用棋盤選單中關閉此功能'; + String get studyWithTheIdea => '教科书式的'; @override - String get preferencesInCorrespondenceGames => '在長期對局中'; + String get studyNextChapter => '下一章节'; @override - String get preferencesCorrespondenceAndUnlimited => '通信和無限'; + String get studyPrevChapter => '上一章节'; @override - String get preferencesConfirmResignationAndDrawOffers => '確認投降或和局請求'; + String get studyStudyActions => '研讨操作'; @override - String get preferencesCastleByMovingTheKingTwoSquaresOrOntoTheRook => '入堡方法'; + String get studyTopics => '主题'; @override - String get preferencesCastleByMovingTwoSquares => '移動國王兩格'; + String get studyMyTopics => '我的主题'; @override - String get preferencesCastleByMovingOntoTheRook => '移動國王到城堡上'; + String get studyPopularTopics => '热门主题'; @override - String get preferencesInputMovesWithTheKeyboard => '使用鍵盤輸入著法'; + String get studyManageTopics => '管理主题'; @override - String get preferencesInputMovesWithVoice => '用語音輸入著法'; + String get studyBack => '回到起始'; @override - String get preferencesSnapArrowsToValidMoves => '將右鍵標示箭頭鎖定到合法棋步'; + String get studyPlayAgain => '重玩'; @override - String get preferencesSayGgWpAfterLosingOrDrawing => '輸棋或和棋後自動發送 \"Good game, well played\"。'; + String get studyWhatWouldYouPlay => '你会在这个位置上怎么走?'; @override - String get preferencesYourPreferencesHaveBeenSaved => '已儲存您的設定。'; + String get studyYouCompletedThisLesson => '恭喜!你完成了这个课程!'; @override - String get preferencesScrollOnTheBoardToReplayMoves => '在騎盤上使用滑鼠滾輪以重新顯示過去棋步'; + String studyNbChapters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '共 $count 章', + ); + return '$_temp0'; + } @override - String get preferencesCorrespondenceEmailNotification => '每日以電郵列出您當前的長期對局'; + String studyNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '共 $count 盘棋', + ); + return '$_temp0'; + } @override - String get preferencesNotifyStreamStart => '追蹤的直播主開始直播'; + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 位成员', + ); + return '$_temp0'; + } @override - String get preferencesNotifyInboxMsg => '收件夾有新訊息'; + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '在此粘贴你的 PGN 文本,最多支持 $count 个游戏', + ); + return '$_temp0'; + } +} + +/// The translations for Chinese, as used in Taiwan (`zh_TW`). +class AppLocalizationsZhTw extends AppLocalizationsZh { + AppLocalizationsZhTw(): super('zh_TW'); @override - String get preferencesNotifyForumMention => '論壇評論中提到您'; + String get mobileHomeTab => '首頁'; @override - String get preferencesNotifyInvitedStudy => '研究邀請'; + String get mobilePuzzlesTab => '謎題'; @override - String get preferencesNotifyGameEvent => '長期對局更新訊息'; + String get mobileToolsTab => '工具'; @override - String get preferencesNotifyChallenge => '挑戰'; + String get mobileWatchTab => '觀戰'; @override - String get preferencesNotifyTournamentSoon => '比賽即將開始'; + String get mobileSettingsTab => '設定'; @override - String get preferencesNotifyTimeAlarm => '長期對局的時間即將耗盡'; + String get mobileMustBeLoggedIn => '你必須登入才能查看此頁面。'; @override - String get preferencesNotifyBell => 'Lichess 內的鈴聲通知'; + String get mobileSystemColors => '系統顏色'; @override - String get preferencesNotifyPush => 'Lichess 外的設備通知'; + String get mobileFeedbackButton => '問題反饋'; @override - String get preferencesNotifyWeb => '瀏覽器通知'; + String get mobileOkButton => '確認'; @override - String get preferencesNotifyDevice => '設備通知'; + String get mobileSettingsHapticFeedback => '震動回饋'; @override - String get preferencesBellNotificationSound => '通知鈴聲'; + String get mobileSettingsImmersiveMode => '沉浸模式'; @override - String get puzzlePuzzles => '謎題'; + String get mobileSettingsImmersiveModeSubtitle => '在下棋和 Puzzle Storm 時隱藏系統界面。如果您受到螢幕邊緣的系統導航手勢干擾,可以使用此功能。'; @override - String get puzzlePuzzleThemes => '謎題主題'; + String get mobileNotFollowingAnyUser => '您未被任何使用者追蹤。'; @override - String get puzzleRecommended => '推薦'; + String get mobileAllGames => '所有棋局'; @override - String get puzzlePhases => '分類'; + String get mobileRecentSearches => '搜尋紀錄'; @override - String get puzzleMotifs => '主題'; - - @override - String get puzzleAdvanced => '高級'; - - @override - String get puzzleLengths => '長度'; - - @override - String get puzzleMates => '將軍'; - - @override - String get puzzleGoals => '目標'; - - @override - String get puzzleOrigin => '來源'; - - @override - String get puzzleSpecialMoves => '特殊移動'; - - @override - String get puzzleDidYouLikeThisPuzzle => '您喜歡這道謎題嗎?'; - - @override - String get puzzleVoteToLoadNextOne => '告訴我們加載下一題!'; - - @override - String get puzzleYourPuzzleRatingWillNotChange => '您的謎題評級不會改變。請注意,謎題不是比賽。您的評分有助於選擇最適合您當前技能的謎題。'; - - @override - String get puzzleFindTheBestMoveForWhite => '為白方找出最佳移動'; - - @override - String get puzzleFindTheBestMoveForBlack => '為黑方找出最佳移動'; - - @override - String get puzzleToGetPersonalizedPuzzles => '得到個人推薦題目:'; + String get mobileClearButton => '清除'; @override - String puzzlePuzzleId(String param) { - return '謎題 $param'; + String mobilePlayersMatchingSearchTerm(String param) { + return '名稱包含「$param」的玩家'; } @override - String get puzzlePuzzleOfTheDay => '每日一題'; - - @override - String get puzzleClickToSolve => '點擊解題'; + String get mobileNoSearchResults => '沒有任何搜尋結果'; @override - String get puzzleGoodMove => '好棋'; + String get mobileAreYouSure => '您確定嗎?'; @override - String get puzzleBestMove => '最佳走法!'; + String get mobilePuzzleStreakAbortWarning => '這將失去目前的連勝並且將儲存目前成績。'; @override - String get puzzleKeepGoing => '加油!'; + String get mobilePuzzleStormNothingToShow => '沒有內容可顯示。您可以進行一些 Puzzle Storm 。'; @override - String get puzzlePuzzleSuccess => '成功!'; + String get mobileSharePuzzle => '分享這個謎題'; @override - String get puzzlePuzzleComplete => '解題完成!'; + String get mobileShareGameURL => '分享對局網址'; @override - String get puzzleNotTheMove => '不是這步!'; + String get mobileShareGamePGN => '分享 PGN'; @override - String get puzzleTrySomethingElse => '試試其他的移動'; + String get mobileSharePositionAsFEN => '以 FEN 分享棋局位置'; @override - String puzzleRatingX(String param) { - return '評級:$param'; - } + String get mobileShowVariations => '顯示變體'; @override - String get puzzleHidden => '隱藏'; + String get mobileHideVariation => '隱藏變體'; @override - String puzzleFromGameLink(String param) { - return '來自對局 $param'; - } + String get mobileShowComments => '顯示留言'; @override - String get puzzleContinueTraining => '繼續訓練'; + String get mobilePuzzleStormConfirmEndRun => '是否中斷於此?'; @override - String get puzzleDifficultyLevel => '困難度'; + String get mobilePuzzleStormFilterNothingToShow => '沒有內容可顯示,請更改篩選條件'; @override - String get puzzleNormal => '一般'; + String get mobileCancelTakebackOffer => '取消悔棋請求'; @override - String get puzzleEasier => '簡單'; + String get mobileWaitingForOpponentToJoin => '正在等待對手加入...'; @override - String get puzzleEasiest => '超簡單'; + String get mobileBlindfoldMode => '盲棋'; @override - String get puzzleHarder => '困難'; + String get mobileLiveStreamers => 'Lichess 實況主'; @override - String get puzzleHardest => '超困難'; + String get mobileCustomGameJoinAGame => '加入棋局'; @override - String get puzzleExample => '範例'; + String get mobileCorrespondenceClearSavedMove => '清除已儲存移動'; @override - String get puzzleAddAnotherTheme => '加入其他主題'; + String get mobileSomethingWentWrong => '發生了一些問題。'; @override - String get puzzleJumpToNextPuzzleImmediately => '立即跳到下一個謎題'; + String get mobileShowResult => '顯示結果'; @override - String get puzzlePuzzleDashboard => '謎題能力分析'; + String get mobilePuzzleThemesSubtitle => '從您喜歡的開局進行謎題,或選擇一個主題。'; @override - String get puzzleImprovementAreas => '弱點'; + String get mobilePuzzleStormSubtitle => '在三分鐘內解開盡可能多的謎題'; @override - String get puzzleStrengths => '強項'; + String mobileGreeting(String param) { + return '您好, $param'; + } @override - String get puzzleHistory => '解題紀錄'; + String get mobileGreetingWithoutName => '您好'; @override - String get puzzleSolved => '解決'; + String get mobilePrefMagnifyDraggedPiece => '放大被拖曳的棋子'; @override - String get puzzleFailed => '失敗'; + String get activityActivity => '活動'; @override - String get puzzleStreakDescription => '累積你的連勝,解著漸漸變難的題目。 沒有時間限制,不要急。走錯一步,將會是遊戲結束!\n不過每一局中你都有跳過一步棋的機會。'; + String get activityHostedALiveStream => '主持一個現場直播'; @override - String puzzleYourStreakX(String param) { - return '您的連勝場數:$param'; + String activityRankedInSwissTournament(String param1, String param2) { + return '在$param2中排名 $param1'; } @override - String get puzzleStreakSkipExplanation => '跳過這一步來維持您的連勝紀錄!每次遊玩只能使用一次。'; + String get activitySignedUp => '在 lichess.org 中註冊'; @override - String get puzzleContinueTheStreak => '繼續遊玩'; + String activitySupportedNbMonths(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '以 $param2 身分贊助 lichess.org $count 個月', + ); + return '$_temp0'; + } @override - String get puzzleNewStreak => '新的連勝紀錄'; + String activityPracticedNbPositions(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '在 $param2 練習了 $count 個棋局', + ); + return '$_temp0'; + } @override - String get puzzleFromMyGames => '來自我的棋局'; + String activitySolvedNbPuzzles(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '解決了 $count 個戰術題目', + ); + return '$_temp0'; + } @override - String get puzzleLookupOfPlayer => '尋找其他棋手的棋局謎題'; + String activityPlayedNbGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '下了 $count 場$param2類型的棋局', + ); + return '$_temp0'; + } @override - String puzzleFromXGames(String param) { - return '來自$param棋局的謎題'; + String activityPostedNbMessages(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '在「$param2」發表了 $count 則訊息', + ); + return '$_temp0'; } @override - String get puzzleSearchPuzzles => '尋找謎題'; + String activityPlayedNbMoves(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '下了 $count 步', + ); + return '$_temp0'; + } @override - String get puzzleFromMyGamesNone => '你在數據庫中沒有謎題,但 Lichess 仍然非常愛你。\n遊玩一些快速和經典遊戲,以增加添加拼圖的機會!'; + String activityInNbCorrespondenceGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '在 $count 場通信棋局中', + ); + return '$_temp0'; + } @override - String puzzleFromXGamesFound(String param1, String param2) { - return '在$param2中找到$param1個謎題'; + String activityCompletedNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '完成了 $count 場通信棋局', + ); + return '$_temp0'; } @override - String get puzzlePuzzleDashboardDescription => '訓練、分析、改進'; + String activityCompletedNbVariantGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '完成了 $count $param2 場通信棋局', + ); + return '$_temp0'; + } @override - String puzzlePercentSolved(String param) { - return '$param 已解決'; + String activityFollowedNbPlayers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '開始關注 $count 個玩家', + ); + return '$_temp0'; } @override - String get puzzleNoPuzzlesToShow => '沒有什麼可展示的,先去玩一些謎題吧!'; + String activityGainedNbFollowers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '增加了 $count 個追蹤者', + ); + return '$_temp0'; + } @override - String get puzzleImprovementAreasDescription => '訓練這些類型的謎題來優化你的進步!'; + String activityHostedNbSimuls(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '主持了$count場車輪戰', + ); + return '$_temp0'; + } @override - String get puzzleStrengthDescription => '你在這些主題中表現最好'; + String activityJoinedNbSimuls(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '加入了$count場車輪戰', + ); + return '$_temp0'; + } @override - String puzzlePlayedXTimes(int count) { + String activityCreatedNbStudies(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '已被嘗試$count次', + other: '創造了$count個新的研究', ); return '$_temp0'; } @override - String puzzleNbPointsBelowYourPuzzleRating(int count) { + String activityCompetedInNbTournaments(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '低於你的謎題積分$count點', + other: '完成了$count場錦標賽', ); return '$_temp0'; } @override - String puzzleNbPointsAboveYourPuzzleRating(int count) { + String activityRankedInTournament(int count, String param2, String param3, String param4) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '高於你的謎題積分$count點', + other: '在$param4錦標賽中下了$param3盤棋局,排名第$count(前$param2%)', ); return '$_temp0'; } @override - String puzzleNbPlayed(int count) { + String activityCompetedInNbSwissTournaments(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count 已遊玩', + other: '參與過 $count 場瑞士制錦標賽', ); return '$_temp0'; } @override - String puzzleNbToReplay(int count) { + String activityJoinedNbTeams(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count 重玩', + other: '加入 $count 團隊', ); return '$_temp0'; } @override - String get puzzleThemeAdvancedPawn => '升變兵'; + String get broadcastBroadcasts => '比賽直播'; @override - String get puzzleThemeAdvancedPawnDescription => '你的其中一個兵已經深入了對方的棋位,或許要威脅升變。'; + String get broadcastMyBroadcasts => '我的直播'; @override - String get puzzleThemeAdvantage => '擁有優勢'; + String get broadcastLiveBroadcasts => '錦標賽直播'; @override - String get puzzleThemeAnastasiaMate => '阿納斯塔西亞殺法'; + String get broadcastBroadcastCalendar => '直播時程表'; @override - String get puzzleThemeArabianMate => '阿拉伯殺法'; + String get broadcastNewBroadcast => '新的現場直播'; @override - String get puzzleThemeArabianMateDescription => '馬和車聯手把對方的王困住在角落的位置'; + String get broadcastSubscribedBroadcasts => '已訂閱的直播'; @override - String get puzzleThemeAttackingF2F7 => '攻擊f2或f7'; + String get broadcastAboutBroadcasts => '關於直播'; @override - String get puzzleThemeAttraction => '吸引'; + String get broadcastHowToUseLichessBroadcasts => '如何使用 Lichess 比賽直播'; @override - String get puzzleThemeBackRankMate => '後排將死'; + String get broadcastTheNewRoundHelp => '新的一局會有跟上一局相同的成員與貢獻者'; @override - String get puzzleThemeBackRankMateDescription => '在對方的王在底線被自身的棋子困住時,將殺對方的王'; + String get broadcastAddRound => '新增回合'; @override - String get puzzleThemeBishopEndgame => '象殘局'; + String get broadcastOngoing => '進行中'; @override - String get puzzleThemeBishopEndgameDescription => '只剩象和兵的殘局'; + String get broadcastUpcoming => '即將舉行'; @override - String get puzzleThemeBodenMate => '波登殺法'; + String get broadcastCompleted => '已結束'; @override - String get puzzleThemeCastling => '易位'; + String get broadcastCompletedHelp => 'Lichess 偵測棋局的結束,但有可能會偵測錯誤。請在這自行設定。'; @override - String get puzzleThemeCapturingDefender => '吃子 - 防守者'; + String get broadcastRoundName => '回合名稱'; @override - String get puzzleThemeCrushing => '壓倒性優勢'; + String get broadcastRoundNumber => '回合數'; @override - String get puzzleThemeCrushingDescription => '察覺對方的漏著並藉此取得巨大優勢。(大於600百分兵)'; + String get broadcastTournamentName => '錦標賽名稱'; @override - String get puzzleThemeDoubleBishopMate => '雙主教將死'; + String get broadcastTournamentDescription => '簡短比賽說明'; @override - String get puzzleThemeEquality => '均勢'; + String get broadcastFullDescription => '完整比賽說明'; @override - String get puzzleThemeKingsideAttack => '王翼攻擊'; + String broadcastFullDescriptionHelp(String param1, String param2) { + return '直播內容的詳細描述 。可以利用 $param1。字數限於$param2個字。'; + } @override - String get puzzleThemeClearance => '騰挪'; + String get broadcastSourceSingleUrl => 'PGN 來源網址'; @override - String get puzzleThemeDefensiveMove => '加強防守'; + String get broadcastSourceUrlHelp => 'Lichess 將以該網址更新PGN數據,網址必須公開'; @override - String get puzzleThemeDeflection => '引離'; + String get broadcastSourceGameIds => '最多 64 個以空格分開的 Lichess 棋局序號。'; @override - String get puzzleThemeDiscoveredAttack => '閃擊'; + String broadcastStartDateTimeZone(String param) { + return '當地時區的錦標賽起始日期:$param'; + } @override - String get puzzleThemeDoubleCheck => '雙將'; + String get broadcastStartDateHelp => '可選,如果知道比賽開始時間'; @override - String get puzzleThemeEndgame => '殘局'; + String get broadcastCurrentGameUrl => '目前棋局連結'; @override - String get puzzleThemeEndgameDescription => '棋局中最後階段的戰術'; + String get broadcastDownloadAllRounds => '下載所有棋局'; @override - String get puzzleThemeFork => '捉雙'; + String get broadcastResetRound => '重設此回合'; @override - String get puzzleThemeKnightEndgame => '馬殘局'; + String get broadcastDeleteRound => '刪除此回合'; @override - String get puzzleThemeKnightEndgameDescription => '只剩馬和兵的殘局'; + String get broadcastDefinitivelyDeleteRound => '刪除這局以及其所有棋局'; @override - String get puzzleThemeLong => '長謎題'; + String get broadcastDeleteAllGamesOfThisRound => '刪除所有此輪的棋局。直播來源必須是開啟的以成功重新建立棋局。'; @override - String get puzzleThemeLongDescription => '三步獲勝'; + String get broadcastEditRoundStudy => '編輯此輪研究'; @override - String get puzzleThemeMaster => '大師棋局'; + String get broadcastDeleteTournament => '刪除此錦標賽'; @override - String get puzzleThemeMasterVsMaster => '大師對局'; + String get broadcastDefinitivelyDeleteTournament => '刪除錦標賽以及所有棋局'; @override - String get puzzleThemeMate => '將軍'; + String get broadcastShowScores => '根據比賽結果顯示玩家分數'; @override - String get puzzleThemeMateIn1 => '一步殺棋'; + String get broadcastReplacePlayerTags => '取代玩家名字、評級、以及頭銜(選填)'; @override - String get puzzleThemeMateIn1Description => '一步將軍'; + String get broadcastFideFederations => 'FIDE 國別'; @override - String get puzzleThemeMateIn2 => '兩步殺棋'; + String get broadcastTop10Rating => '前 10 名平均評級'; @override - String get puzzleThemeMateIn2Description => '走兩步以達到將軍'; + String get broadcastFidePlayers => 'FIDE 玩家'; @override - String get puzzleThemeMateIn3 => '三步殺棋'; + String get broadcastFidePlayerNotFound => '找不到 FIDE 玩家'; @override - String get puzzleThemeMateIn3Description => '走三步以達到將軍'; + String get broadcastFideProfile => 'FIDE 序號'; @override - String get puzzleThemeMateIn4 => '四步殺棋'; + String get broadcastFederation => '國籍'; @override - String get puzzleThemeMateIn4Description => '走四步以達到將軍'; + String get broadcastAgeThisYear => '年齡'; @override - String get puzzleThemeMateIn5 => '五步或更高 將軍'; + String get broadcastUnrated => '未評級'; @override - String get puzzleThemeMiddlegame => '中局'; + String get broadcastRecentTournaments => '最近錦標賽'; @override - String get puzzleThemeMiddlegameDescription => '棋局中第二階段的戰術'; + String get broadcastOpenLichess => '在 lichess 中開啟'; @override - String get puzzleThemeOneMove => '一步題'; + String get broadcastTeams => '團隊'; @override - String get puzzleThemeOneMoveDescription => '只有一步長的題目'; - - @override - String get puzzleThemeOpening => '開局'; + String get broadcastBoards => '棋局'; @override - String get puzzleThemeOpeningDescription => '棋局中起始階段的戰術'; + String get broadcastOverview => '概覽'; @override - String get puzzleThemePawnEndgame => '兵殘局'; + String get broadcastSubscribeTitle => '訂閱以在每輪開始時獲得通知。您可以在帳戶設定中切換直播的鈴聲或推播通知。'; @override - String get puzzleThemePawnEndgameDescription => '只剩兵的殘局'; + String get broadcastUploadImage => '上傳錦標賽圖片'; @override - String get puzzleThemePin => '牽制'; + String get broadcastNoBoardsYet => '尚無棋局。這些棋局將在對局上傳後顯示。'; @override - String get puzzleThemePromotion => '升變'; + String get broadcastStartVerySoon => '直播即將開始。'; @override - String get puzzleThemeQueenEndgame => '后殘局'; + String get broadcastNotYetStarted => '直播尚未開始。'; @override - String get puzzleThemeQueenEndgameDescription => '只剩后和兵的殘局'; + String get broadcastOfficialWebsite => '官網'; @override - String get puzzleThemeQueenRookEndgame => '后與車'; + String get broadcastStandings => '排行榜'; @override - String get puzzleThemeQueenRookEndgameDescription => '只剩后、車和兵的殘局'; + String broadcastIframeHelp(String param) { + return '更多選項在$param'; + } @override - String get puzzleThemeQueensideAttack => '后翼攻擊'; + String get broadcastWebmastersPage => '網頁管理員頁面'; @override - String get puzzleThemeQuietMove => '安靜的一着'; + String broadcastPgnSourceHelp(String param) { + return '這一輪的公開實時 PGN。我們還提供$param以實現更快和更高效的同步。'; + } @override - String get puzzleThemeRookEndgame => '車殘局'; + String get broadcastEmbedThisBroadcast => '將此直播嵌入您的網站'; @override - String get puzzleThemeRookEndgameDescription => '只剩車和兵的殘局'; + String broadcastEmbedThisRound(String param) { + return '將$param嵌入您的網站'; + } @override - String get puzzleThemeSacrifice => '棄子'; + String get broadcastRatingDiff => '評級差異'; @override - String get puzzleThemeShort => '短謎題'; + String get broadcastGamesThisTournament => '此比賽的對局'; @override - String get puzzleThemeShortDescription => '兩步獲勝'; + String get broadcastScore => '分數'; @override - String get puzzleThemeSkewer => '串擊'; + String broadcastNbBroadcasts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 個直播', + ); + return '$_temp0'; + } @override - String get puzzleThemeSmotheredMate => '悶殺'; + String challengeChallengesX(String param1) { + return '挑戰: $param1'; + } @override - String get puzzleThemeSuperGM => '超級大師賽局'; + String get challengeChallengeToPlay => '邀請對弈'; @override - String get puzzleThemeSuperGMDescription => '來自世界各地優秀玩家對局的戰術題'; + String get challengeChallengeDeclined => '對弈邀請已拒絕'; @override - String get puzzleThemeTrappedPiece => '被困的棋子'; + String get challengeChallengeAccepted => '對弈邀請已接受'; @override - String get puzzleThemeUnderPromotion => '升變'; + String get challengeChallengeCanceled => '對弈邀請已撤銷'; @override - String get puzzleThemeUnderPromotionDescription => '升變成騎士、象或車'; + String get challengeRegisterToSendChallenges => '請登入以向其他人發出對弈邀請'; @override - String get puzzleThemeVeryLong => '非常長的謎題'; + String challengeYouCannotChallengeX(String param) { + return '你無法向$param發出對弈邀請'; + } @override - String get puzzleThemeVeryLongDescription => '四步或以上獲勝'; + String challengeXDoesNotAcceptChallenges(String param) { + return '$param沒有接受對弈邀請'; + } @override - String get puzzleThemeXRayAttack => '穿透攻擊'; + String challengeYourXRatingIsTooFarFromY(String param1, String param2) { + return '您的$param1積分與$param2相差太多'; + } @override - String get puzzleThemeZugzwang => '等著'; + String challengeCannotChallengeDueToProvisionalXRating(String param) { + return '由於您的$param積分不夠穩定,無法發出挑戰。'; + } @override - String get puzzleThemeHealthyMix => '綜合'; + String challengeXOnlyAcceptsChallengesFromFriends(String param) { + return '$param只接受好友的對弈邀請'; + } @override - String get puzzleThemeHealthyMixDescription => '所有類型都有!你不知道會遇到什麼題型,所以請做好準備,就像在實戰一樣。'; + String get challengeDeclineGeneric => '我目前不接受對弈'; @override - String get searchSearch => '搜尋'; + String get challengeDeclineLater => '我現在不接受對弈,請晚點再詢問'; @override - String get settingsSettings => '設定'; + String get challengeDeclineTooFast => '這個時間控制對我來說太快了,請用慢一點的遊戲再次挑戰。'; @override - String get settingsCloseAccount => '關閉帳戶'; + String get challengeDeclineTooSlow => '這個時間控制對我來說太慢了,請用快一點的遊戲再次挑戰。'; @override - String get settingsClosingIsDefinitive => '您確定要刪除帳號嗎?這是不能挽回的'; + String get challengeDeclineTimeControl => '我不接受這個挑戰的時間控制。'; @override - String get settingsCantOpenSimilarAccount => '即使名稱大小寫不同,您也不能使用相同的名稱開設新帳戶'; + String get challengeDeclineRated => '請向我發送積分對弈。'; @override - String get settingsChangedMindDoNotCloseAccount => '我改變主意了,不要關閉我的帳號'; + String get challengeDeclineCasual => '請向我發送休閒對弈。'; @override - String get settingsCloseAccountExplanation => '您真的確定要刪除帳戶嗎? 關閉帳戶是永久性的決定, 您將「永遠無法」再次登錄。'; + String get challengeDeclineStandard => '我不接受變體對弈。'; @override - String get settingsThisAccountIsClosed => '此帳號已被關閉。'; + String get challengeDeclineVariant => '我現在不想玩這個變體。'; @override - String get playWithAFriend => '和好友下棋'; + String get challengeDeclineNoBot => '我不接受機器人的對弈。'; @override - String get playWithTheMachine => '和電腦下棋'; + String get challengeDeclineOnlyBot => '我目前只接受機器人的對弈。'; @override - String get toInviteSomeoneToPlayGiveThisUrl => '邀人下棋,請分享這個網址'; + String get challengeInviteLichessUser => '或邀請一位 Lichess 用户:'; @override - String get gameOver => '遊戲結束'; + String get contactContact => '聯繫我們'; @override - String get waitingForOpponent => '等待對手'; + String get contactContactLichess => '聯繫 Lichess'; @override - String get waiting => '請稍等'; + String get patronDonate => '捐款'; @override - String get yourTurn => '該你走'; + String get patronLichessPatron => 'Lichess 贊助者'; @override - String aiNameLevelAiLevel(String param1, String param2) { - return '$param1等級 $param2'; + String perfStatPerfStats(String param) { + return '$param戰績'; } @override - String get level => '難度'; + String get perfStatViewTheGames => '查看遊戲紀錄'; @override - String get strength => '強度'; + String get perfStatProvisional => '臨時'; @override - String get toggleTheChat => '聊天開關'; + String get perfStatNotEnoughRatedGames => '積分賽場次太少,無法計算準確積分。'; @override - String get chat => '聊天'; + String perfStatProgressOverLastXGames(String param) { + return '最近$param場棋局之積分變化:'; + } @override - String get resign => '認輸'; + String perfStatRatingDeviation(String param) { + return '積分誤差: $param'; + } @override - String get checkmate => '將死'; + String perfStatRatingDeviationTooltip(String param1, String param2, String param3) { + return '越低的數值代表積分越穩定。 數值高於$param1時的積分會被判定為浮動積分。\n要被列入排名之中,該數值需低於$param2(標準西洋棋) 或是$param3(西洋棋變體)。'; + } @override - String get stalemate => '逼和'; + String get perfStatTotalGames => '總計棋局'; @override - String get white => '白方'; + String get perfStatRatedGames => '積分棋局'; @override - String get black => '黑方'; + String get perfStatTournamentGames => '聯賽棋局'; @override - String get asWhite => '使用白棋'; + String get perfStatBerserkedGames => '狂暴模式棋局'; @override - String get asBlack => '使用黑棋'; + String get perfStatTimeSpentPlaying => '奕棋時間'; @override - String get randomColor => '隨機選色'; + String get perfStatAverageOpponent => '對手平均積分'; @override - String get createAGame => '開始對局'; + String get perfStatVictories => '勝場'; @override - String get whiteIsVictorious => '白方勝'; + String get perfStatDefeats => '敗場'; @override - String get blackIsVictorious => '黑方勝'; + String get perfStatDisconnections => '斷線場次'; @override - String get youPlayTheWhitePieces => '您執白棋'; + String get perfStatNotEnoughGames => '棋局數不夠多'; @override - String get youPlayTheBlackPieces => '您執黑棋'; + String perfStatHighestRating(String param) { + return '最高積分:$param'; + } @override - String get itsYourTurn => '輪到你了!'; + String perfStatLowestRating(String param) { + return '最低積分:$param'; + } @override - String get cheatDetected => '偵測到作弊行為'; + String perfStatFromXToY(String param1, String param2) { + return '從$param1到$param2'; + } @override - String get kingInTheCenter => '王居中'; + String get perfStatWinningStreak => '連勝場數'; @override - String get threeChecks => '三次將軍'; + String get perfStatLosingStreak => '連敗場數'; @override - String get raceFinished => '競王結束'; + String perfStatLongestStreak(String param) { + return '最長紀錄:$param'; + } @override - String get variantEnding => '另類終局'; + String perfStatCurrentStreak(String param) { + return '目前記錄:$param'; + } @override - String get newOpponent => '換個對手'; + String get perfStatBestRated => '積分賽勝場之最強對手'; @override - String get yourOpponentWantsToPlayANewGameWithYou => '你的對手想和你複賽'; + String get perfStatGamesInARow => '連續奕棋場數'; @override - String get joinTheGame => '加入這盤棋'; + String get perfStatLessThanOneHour => '兩場間距不到一小時'; @override - String get whitePlays => '白方走棋'; + String get perfStatMaxTimePlaying => '最高奕棋時間'; @override - String get blackPlays => '黑方走棋'; + String get perfStatNow => '現在'; @override - String get opponentLeftChoices => '對方可能已經離開遊戲。您可以選擇:取勝、和棋或等待對方走棋。'; + String get preferencesPreferences => '偏好設定'; @override - String get forceResignation => '取勝'; + String get preferencesDisplay => '顯示'; @override - String get forceDraw => '和棋'; + String get preferencesPrivacy => '隱私'; @override - String get talkInChat => '請在聊天室裡文明一點'; + String get preferencesNotifications => '通知'; @override - String get theFirstPersonToComeOnThisUrlWillPlayWithYou => '第一個訪問該網址的人將與您下棋。'; + String get preferencesPieceAnimation => '棋子動畫'; @override - String get whiteResigned => '白方認輸'; + String get preferencesMaterialDifference => '子力差距'; @override - String get blackResigned => '黑方認輸'; + String get preferencesBoardHighlights => '國王紅色亮光(最後一步與將軍)'; @override - String get whiteLeftTheGame => '白方棄局'; + String get preferencesPieceDestinations => '棋子目的地(有效走法與預先走棋)'; @override - String get blackLeftTheGame => '黑方棄局'; + String get preferencesBoardCoordinates => '棋盤座標(A-H, 1-8)'; @override - String get whiteDidntMove => '白方沒有走棋'; + String get preferencesMoveListWhilePlaying => '遊戲進行時顯示棋譜'; @override - String get blackDidntMove => '黑方沒有走棋'; + String get preferencesPgnPieceNotation => '棋譜記法'; @override - String get requestAComputerAnalysis => '請求電腦分析'; + String get preferencesChessPieceSymbol => '棋子符號'; @override - String get computerAnalysis => '電腦分析'; + String get preferencesPgnLetter => '字母 (K, Q, R, B, N)'; @override - String get computerAnalysisAvailable => '電腦分析可用'; + String get preferencesZenMode => '專注模式'; @override - String get computerAnalysisDisabled => '電腦分析未啟用'; + String get preferencesShowPlayerRatings => '顯示玩家等級分'; @override - String get analysis => '分析棋局'; + String get preferencesShowFlairs => '顯示玩家身分'; @override - String depthX(String param) { - return '深度 $param'; - } + String get preferencesExplainShowPlayerRatings => '這允許隱藏本網站上的所有等級分,以輔助專心下棋。每局遊戲仍可以計算及改變等級分,這個設定只會影響到你是否看得到此分數。'; @override - String get usingServerAnalysis => '正在使用伺服器分析'; + String get preferencesDisplayBoardResizeHandle => '顯示盤面大小調整區塊'; @override - String get loadingEngine => '正在載入引擎 ...'; + String get preferencesOnlyOnInitialPosition => '只在起始局面'; @override - String get calculatingMoves => '計算著法中。。。'; + String get preferencesInGameOnly => '只在遊戲中'; @override - String get engineFailed => '加載引擎出錯'; + String get preferencesChessClock => '棋鐘'; @override - String get cloudAnalysis => '雲端分析'; + String get preferencesTenthsOfSeconds => '十分之一秒'; @override - String get goDeeper => '深入分析'; + String get preferencesWhenTimeRemainingLessThanTenSeconds => '當剩餘時間小於10秒'; @override - String get showThreat => '顯示敵方威脅'; + String get preferencesHorizontalGreenProgressBars => '綠色橫進度條'; @override - String get inLocalBrowser => '在本地瀏覽器'; + String get preferencesSoundWhenTimeGetsCritical => '時間不足時聲音提醒'; @override - String get toggleLocalEvaluation => '使用您當地的伺服器分析'; + String get preferencesGiveMoreTime => '給對方更多時間'; @override - String get promoteVariation => '增加變化'; + String get preferencesGameBehavior => '對局行為'; @override - String get makeMainLine => '將這步棋導入主要流程中'; + String get preferencesHowDoYouMovePieces => '移動棋子方式?'; @override - String get deleteFromHere => '從這處開始刪除'; + String get preferencesClickTwoSquares => '點擊棋子及目標位置'; @override - String get forceVariation => '移除變化'; + String get preferencesDragPiece => '拖曳棋子'; @override - String get copyVariationPgn => '複製變體 PGN'; + String get preferencesBothClicksAndDrag => '兩者都行'; @override - String get move => '走棋'; + String get preferencesPremovesPlayingDuringOpponentTurn => '預先走棋(在對手的回合走棋)'; @override - String get variantLoss => '您因特殊規則而輸了'; + String get preferencesTakebacksWithOpponentApproval => '悔棋(經過對手同意)'; @override - String get variantWin => '您因特殊規則而贏了'; + String get preferencesInCasualGamesOnly => '僅限非正式遊戲'; @override - String get insufficientMaterial => '由於棋子不足而導致平局'; + String get preferencesPromoteToQueenAutomatically => '兵自動升為后'; @override - String get pawnMove => '小兵移動'; + String get preferencesExplainPromoteToQueenAutomatically => '升變的同時按住以暫時取消自動升變'; @override - String get capture => '吃子'; + String get preferencesWhenPremoving => '預先走棋時'; @override - String get close => '關閉'; + String get preferencesClaimDrawOnThreefoldRepetitionAutomatically => '在三次重覆局面時自動要求和局'; @override - String get winning => '贏棋'; + String get preferencesWhenTimeRemainingLessThanThirtySeconds => '當剩餘時間小於30秒'; @override - String get losing => '輸棋'; + String get preferencesMoveConfirmation => '走棋確認'; @override - String get drawn => '平手'; + String get preferencesExplainCanThenBeTemporarilyDisabled => '可以在遊戲中用棋盤選單中關閉此功能'; @override - String get unknown => '未知'; + String get preferencesInCorrespondenceGames => '在長期對局中'; @override - String get database => '資料庫'; + String get preferencesCorrespondenceAndUnlimited => '通信和無限'; @override - String get whiteDrawBlack => '白棋獲勝 / 平局 / 黑棋獲勝'; + String get preferencesConfirmResignationAndDrawOffers => '確認投降或和局請求'; @override - String averageRatingX(String param) { - return '平均評分: $param'; - } + String get preferencesCastleByMovingTheKingTwoSquaresOrOntoTheRook => '入堡方法'; @override - String get recentGames => '最近的棋局'; + String get preferencesCastleByMovingTwoSquares => '移動國王兩格'; @override - String get topGames => '評分最高的棋局'; + String get preferencesCastleByMovingOntoTheRook => '移動國王到城堡上'; @override - String masterDbExplanation(String param1, String param2, String param3) { - return '來自$param2到$param3年國際棋聯積分$param1以上的棋手對局棋譜'; - } + String get preferencesInputMovesWithTheKeyboard => '使用鍵盤輸入著法'; @override - String get dtzWithRounding => '經過四捨五入的DTZ50\'\',是基於到下次吃子或兵動的半步數目。'; + String get preferencesInputMovesWithVoice => '用語音輸入著法'; @override - String get noGameFound => '未找到遊戲'; + String get preferencesSnapArrowsToValidMoves => '將右鍵標示箭頭鎖定到合法棋步'; @override - String get maxDepthReached => '已達到最大深度!'; + String get preferencesSayGgWpAfterLosingOrDrawing => '輸棋或和棋後自動發送 \"Good game, well played\"。'; @override - String get maybeIncludeMoreGamesFromThePreferencesMenu => '試著從偏好設置中加入更多棋局'; + String get preferencesYourPreferencesHaveBeenSaved => '已儲存您的設定。'; @override - String get openings => '開局'; + String get preferencesScrollOnTheBoardToReplayMoves => '在騎盤上使用滑鼠滾輪以重新顯示過去棋步'; @override - String get openingExplorer => '開局瀏覽器'; + String get preferencesCorrespondenceEmailNotification => '每日以電郵列出您當前的長期對局'; @override - String get openingEndgameExplorer => '開局與終局瀏覽器'; + String get preferencesNotifyStreamStart => '追蹤的直播主開始直播'; @override - String xOpeningExplorer(String param) { - return '$param開局瀏覽器'; - } + String get preferencesNotifyInboxMsg => '收件夾有新訊息'; @override - String get playFirstOpeningEndgameExplorerMove => '在開局/殘局瀏覽器走第一步棋'; + String get preferencesNotifyForumMention => '論壇評論中提到您'; @override - String get winPreventedBy50MoveRule => '在不違反50步和局規則下贏得這局棋'; + String get preferencesNotifyInvitedStudy => '研究邀請'; @override - String get lossSavedBy50MoveRule => '藉由50步和局規則來避免輸掉棋局'; + String get preferencesNotifyGameEvent => '長期對局更新訊息'; @override - String get winOr50MovesByPriorMistake => '贏棋或因先前錯誤50步作和'; + String get preferencesNotifyChallenge => '挑戰'; @override - String get lossOr50MovesByPriorMistake => '輸棋或因先前錯誤50步作和'; + String get preferencesNotifyTournamentSoon => '比賽即將開始'; @override - String get unknownDueToRounding => '由上次吃子或兵動開始按殘局庫建議走法走才能保證勝敗的判斷正確。這是因為Syzygy殘局庫的DTZ數值可能經過四捨五入。'; + String get preferencesNotifyTimeAlarm => '長期對局的時間即將耗盡'; @override - String get allSet => '一切就緒!'; + String get preferencesNotifyBell => 'Lichess 內的鈴聲通知'; @override - String get importPgn => '匯入 PGN'; + String get preferencesNotifyPush => 'Lichess 外的設備通知'; @override - String get delete => '刪除'; + String get preferencesNotifyWeb => '瀏覽器通知'; @override - String get deleteThisImportedGame => '刪除此匯入的棋局?'; + String get preferencesNotifyDevice => '設備通知'; @override - String get replayMode => '重播模式'; + String get preferencesBellNotificationSound => '通知鈴聲'; @override - String get realtimeReplay => '實時'; + String get puzzlePuzzles => '謎題'; @override - String get byCPL => 'CPL'; + String get puzzlePuzzleThemes => '謎題主題'; @override - String get openStudy => '打開研究視窗'; + String get puzzleRecommended => '推薦'; @override - String get enable => '開啟'; + String get puzzlePhases => '分類'; @override - String get bestMoveArrow => '最佳移動的箭頭'; + String get puzzleMotifs => '主題'; @override - String get showVariationArrows => '顯示變體箭頭'; + String get puzzleAdvanced => '高級'; @override - String get evaluationGauge => '棋力估計表'; + String get puzzleLengths => '長度'; @override - String get multipleLines => '路線分析線'; + String get puzzleMates => '將軍'; @override - String get cpus => 'CPU'; + String get puzzleGoals => '目標'; @override - String get memory => '記憶體'; + String get puzzleOrigin => '來源'; @override - String get infiniteAnalysis => '無限分析'; + String get puzzleSpecialMoves => '特殊移動'; @override - String get removesTheDepthLimit => '取消深度限制,使您的電腦發熱。'; + String get puzzleDidYouLikeThisPuzzle => '您喜歡這道謎題嗎?'; @override - String get engineManager => '引擎管理'; + String get puzzleVoteToLoadNextOne => '告訴我們加載下一題!'; @override - String get blunder => '嚴重錯誤'; + String get puzzleUpVote => '投票為好謎題'; @override - String get mistake => '錯誤'; + String get puzzleDownVote => '投票為壞謎題'; @override - String get inaccuracy => '輕微失誤'; + String get puzzleYourPuzzleRatingWillNotChange => '您的謎題評級不會改變。請注意,謎題不是比賽。您的評分有助於選擇最適合您當前技能的謎題。'; @override - String get moveTimes => '走棋時間'; + String get puzzleFindTheBestMoveForWhite => '為白方找出最佳移動'; @override - String get flipBoard => '翻轉棋盤'; + String get puzzleFindTheBestMoveForBlack => '為黑方找出最佳移動'; @override - String get threefoldRepetition => '三次重複局面'; + String get puzzleToGetPersonalizedPuzzles => '得到個人推薦題目:'; @override - String get claimADraw => '要求和棋'; + String puzzlePuzzleId(String param) { + return '謎題 $param'; + } @override - String get offerDraw => '提出和棋'; + String get puzzlePuzzleOfTheDay => '每日一題'; @override - String get draw => '和棋'; + String get puzzleDailyPuzzle => '每日謎題'; @override - String get drawByMutualAgreement => '雙方同意和局'; + String get puzzleClickToSolve => '點擊解題'; @override - String get fiftyMovesWithoutProgress => '50步規則和局'; + String get puzzleGoodMove => '好棋'; @override - String get currentGames => '當前對局'; + String get puzzleBestMove => '最佳走法!'; @override - String get viewInFullSize => '在整個網頁裡觀看棋局'; + String get puzzleKeepGoing => '加油!'; @override - String get logOut => '登出'; + String get puzzlePuzzleSuccess => '成功!'; @override - String get signIn => '登入'; + String get puzzlePuzzleComplete => '解題完成!'; @override - String get rememberMe => '保持登入狀態'; + String get puzzleByOpenings => '以開局區分'; @override - String get youNeedAnAccountToDoThat => '請註冊以完成該操作'; + String get puzzlePuzzlesByOpenings => '以開局區分謎題'; @override - String get signUp => '註冊'; + String get puzzleOpeningsYouPlayedTheMost => '您最常使用的開局'; @override - String get computersAreNotAllowedToPlay => '電腦與電腦輔助棋手不允許參加對弈。對弈時,請勿從國際象棋引擎、資料庫以及其他棋手那裡獲取幫助。另外,強烈建議不要創建多個帳號;過分地使用多個帳號將導致封號。'; + String get puzzleUseFindInPage => '在瀏覽器中使用「在頁面中尋找」以尋找你最喜歡的開局!'; @override - String get games => '棋局'; + String get puzzleUseCtrlF => '按下 Ctrl+f 以找出您最喜歡的開局方式!'; @override - String get forum => '論壇'; + String get puzzleNotTheMove => '不是這步!'; @override - String xPostedInForumY(String param1, String param2) { - return '$param1發帖:$param2'; + String get puzzleTrySomethingElse => '試試其他的移動'; + + @override + String puzzleRatingX(String param) { + return '評級:$param'; } @override - String get latestForumPosts => '最新論壇貼文'; + String get puzzleHidden => '隱藏'; @override - String get players => '棋手'; + String puzzleFromGameLink(String param) { + return '來自對局 $param'; + } @override - String get friends => '朋友'; + String get puzzleContinueTraining => '繼續訓練'; @override - String get discussions => '對話'; + String get puzzleDifficultyLevel => '困難度'; @override - String get today => '今天'; + String get puzzleNormal => '一般'; @override - String get yesterday => '昨天'; + String get puzzleEasier => '簡單'; @override - String get minutesPerSide => '每方分鐘數'; + String get puzzleEasiest => '超簡單'; @override - String get variant => '變體'; + String get puzzleHarder => '困難'; @override - String get variants => '變體'; + String get puzzleHardest => '超困難'; @override - String get timeControl => '時間限制'; + String get puzzleExample => '範例'; @override - String get realTime => '實時棋局'; + String get puzzleAddAnotherTheme => '加入其他主題'; @override - String get correspondence => '通信棋局'; + String get puzzleNextPuzzle => '下個謎題'; @override - String get daysPerTurn => '每步允許天數'; + String get puzzleJumpToNextPuzzleImmediately => '立即跳到下一個謎題'; @override - String get oneDay => '一天'; + String get puzzlePuzzleDashboard => '謎題能力分析'; @override - String get time => '時間'; + String get puzzleImprovementAreas => '弱點'; @override - String get rating => '評級'; + String get puzzleStrengths => '強項'; @override - String get ratingStats => '評分數據'; + String get puzzleHistory => '解題紀錄'; @override - String get username => '用戶名'; + String get puzzleSolved => '解決'; @override - String get usernameOrEmail => '用戶名或電郵地址'; + String get puzzleFailed => '失敗'; @override - String get changeUsername => '更改用戶名'; + String get puzzleStreakDescription => '累積你的連勝,解著漸漸變難的題目。 沒有時間限制,不要急。走錯一步,將會是遊戲結束!\n不過每一局中你都有跳過一步棋的機會。'; @override - String get changeUsernameNotSame => '只能更改字母大小字。例如,將「johndoe」變成「JohnDoe」。'; + String puzzleYourStreakX(String param) { + return '您的連勝場數:$param'; + } @override - String get changeUsernameDescription => '更改用戶名。您最多可以更改一次字母大小寫。'; + String get puzzleStreakSkipExplanation => '跳過這一步來維持您的連勝紀錄!每次遊玩只能使用一次。'; @override - String get signupUsernameHint => '請選擇一個和諧的用戶名,用戶名無法再次更改,並且不合規的用戶名會導致帳戶被封禁!'; + String get puzzleContinueTheStreak => '繼續遊玩'; @override - String get signupEmailHint => '僅用於密碼重置'; + String get puzzleNewStreak => '新的連勝紀錄'; @override - String get password => '密碼'; + String get puzzleFromMyGames => '來自我的棋局'; @override - String get changePassword => '更改密碼'; + String get puzzleLookupOfPlayer => '尋找其他棋手的棋局謎題'; @override - String get changeEmail => '更改電郵地址'; + String puzzleFromXGames(String param) { + return '來自$param棋局的謎題'; + } @override - String get email => '電郵地址'; + String get puzzleSearchPuzzles => '尋找謎題'; @override - String get passwordReset => '重置密碼'; + String get puzzleFromMyGamesNone => '你在資料庫中沒有謎題,但 Lichess 仍然非常愛你。\n遊玩一些快速和經典遊戲,以增加從你的棋局中生成謎題的機會!'; @override - String get forgotPassword => '忘記密碼?'; + String puzzleFromXGamesFound(String param1, String param2) { + return '在$param2中找到$param1個謎題'; + } @override - String get error_weakPassword => '此密碼太常見,且很容易被猜到。'; + String get puzzlePuzzleDashboardDescription => '訓練、分析、改進'; @override - String get error_namePassword => '請不要把密碼設為用戶名。'; + String puzzlePercentSolved(String param) { + return '$param 已解決'; + } @override - String get blankedPassword => '你在其他站點使用過相同的密碼,並且這些站點已經失效。為確保你的 Lichess 帳戶安全,你需要設置新密碼。感謝你的理解。'; + String get puzzleNoPuzzlesToShow => '沒有什麼可展示的,先去玩一些謎題吧!'; @override - String get youAreLeavingLichess => '你正在離開 Lichess'; + String get puzzleImprovementAreasDescription => '訓練這些類型的謎題來優化你的進步!'; @override - String get neverTypeYourPassword => '不要在其他網站輸入你的 Lichess 密碼!'; + String get puzzleStrengthDescription => '你在這些主題中表現最好'; @override - String proceedToX(String param) { - return '前往 $param'; + String puzzlePlayedXTimes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '已被嘗試$count次', + ); + return '$_temp0'; } @override - String get passwordSuggestion => '不要使用他人建議的密碼,他們會用此密碼盜取你的帳戶。'; + String puzzleNbPointsBelowYourPuzzleRating(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '低於你的謎題積分$count點', + ); + return '$_temp0'; + } @override - String get emailSuggestion => '不要使用他人提供的郵箱地址,他們會用它盜取你的帳戶。'; + String puzzleNbPointsAboveYourPuzzleRating(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '高於你的謎題積分$count點', + ); + return '$_temp0'; + } @override - String get emailConfirmHelp => '協助郵件確認'; + String puzzleNbPlayed(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 已遊玩', + ); + return '$_temp0'; + } @override - String get emailConfirmNotReceived => '註冊後沒有收到確認郵件?'; + String puzzleNbToReplay(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 重玩', + ); + return '$_temp0'; + } @override - String get whatSignupUsername => '你用了什麼用戶名註冊?'; + String get puzzleThemeAdvancedPawn => '升變兵'; @override - String usernameNotFound(String param) { - return '找不到用戶 $param。'; - } + String get puzzleThemeAdvancedPawnDescription => '你的其中一個兵已經深入了對方的棋位,或許要威脅升變。'; @override - String get usernameCanBeUsedForNewAccount => '你可以使用這個用戶名創建帳戶'; + String get puzzleThemeAdvantage => '取得優勢'; @override - String emailSent(String param) { - return '我們向 $param 發送了電子郵件。'; - } + String get puzzleThemeAdvantageDescription => '把握機會以取得決定性優勢。(200 厘兵 ≤ 評估值 ≤ 600 厘兵)'; @override - String get emailCanTakeSomeTime => '可能需要一些時間才能收到。'; + String get puzzleThemeAnastasiaMate => '阿納斯塔西亞殺法'; @override - String get refreshInboxAfterFiveMinutes => '等待5分鐘並刷新你的收件箱。'; + String get puzzleThemeAnastasiaMateDescription => '馬與車或后聯手在棋盤邊困住對手國王以及另一對手棋子'; @override - String get checkSpamFolder => '嘗試檢查你的垃圾郵件收件匣,它可能在那裡。 如果在,請將其標記為非垃圾郵件。'; + String get puzzleThemeArabianMate => '阿拉伯殺法'; @override - String get emailForSignupHelp => '如果其他所有的方法都失敗了,給我們發這條短信:'; + String get puzzleThemeArabianMateDescription => '馬和車聯手把對方的王困住在角落的位置'; @override - String copyTextToEmail(String param) { - return '複製並粘貼上面的文本然後把它發給$param'; - } + String get puzzleThemeAttackingF2F7 => '攻擊 f2 或 f7'; @override - String get waitForSignupHelp => '我們很快就會給你回復,説明你完成註冊。'; + String get puzzleThemeAttackingF2F7Description => '專注於 f2 或 f7 兵的攻擊,像是 fried liver 攻擊'; @override - String accountConfirmed(String param) { - return '這個使用者 $param 成功地確認了'; - } + String get puzzleThemeAttraction => '吸引'; @override - String accountCanLogin(String param) { - return '你可以做為 $param 登入了。'; - } + String get puzzleThemeAttractionDescription => '一種換子或犧牲強迫對手旗子到某格以好進行接下來的戰術。'; @override - String get accountConfirmationEmailNotNeeded => '你不需要確認電子郵件。'; + String get puzzleThemeBackRankMate => '後排將死'; @override - String accountClosed(String param) { - return '帳戶 $param 被關閉。'; - } + String get puzzleThemeBackRankMateDescription => '在對方的王於底線被自身的棋子困住時,將死對方的王'; @override - String accountRegisteredWithoutEmail(String param) { - return '帳戶 $param 未使用電子郵箱註冊。'; - } + String get puzzleThemeBishopEndgame => '主教殘局'; @override - String get rank => '排名'; + String get puzzleThemeBishopEndgameDescription => '只剩象和兵的殘局'; @override - String rankX(String param) { - return '排名:$param'; - } + String get puzzleThemeBodenMate => '波登殺法'; @override - String get gamesPlayed => '盤棋已結束'; + String get puzzleThemeBodenMateDescription => '以在對角線上的兩個主教將死被自身棋子困住的王。'; @override - String get cancel => '取消'; + String get puzzleThemeCastling => '易位'; @override - String get whiteTimeOut => '白方時間到'; + String get puzzleThemeCastlingDescription => '讓國王回到安全,並讓車發動攻擊。'; @override - String get blackTimeOut => '黑方時間到'; + String get puzzleThemeCapturingDefender => '吃子 - 防守者'; @override - String get drawOfferSent => '和棋請求已發送'; + String get puzzleThemeCapturingDefenderDescription => '移除防守其他棋子的防守者以攻擊未被保護的棋子'; @override - String get drawOfferAccepted => '同意和棋'; + String get puzzleThemeCrushing => '壓倒性優勢'; @override - String get drawOfferCanceled => '和棋取消'; + String get puzzleThemeCrushingDescription => '察覺對方的漏著並藉此取得巨大優勢。(大於600百分兵)'; @override - String get whiteOffersDraw => '白方提出和棋'; + String get puzzleThemeDoubleBishopMate => '雙主教將死'; @override - String get blackOffersDraw => '黑方提出和棋'; + String get puzzleThemeDoubleBishopMateDescription => '相鄰對角線上的兩個主教將將死被自身棋子困住的王。'; @override - String get whiteDeclinesDraw => '白方拒絕和棋'; + String get puzzleThemeDovetailMate => '柯齊奧將死'; @override - String get blackDeclinesDraw => '黑方拒絕和棋'; + String get puzzleThemeDovetailMateDescription => '以皇后將死被自身棋子困住的國王'; @override - String get yourOpponentOffersADraw => '您的對手提出和棋'; + String get puzzleThemeEquality => '均勢'; @override - String get accept => '接受'; + String get puzzleThemeEqualityDescription => '從劣勢中反敗為和。(分析值 ≤ 200厘兵)'; @override - String get decline => '拒絕'; + String get puzzleThemeKingsideAttack => '王翼攻擊'; @override - String get playingRightNow => '正在對局'; + String get puzzleThemeKingsideAttackDescription => '在對方於王翼易位後的攻擊。'; @override - String get eventInProgress => '正在進行'; + String get puzzleThemeClearance => '騰挪'; @override - String get finished => '已結束'; + String get puzzleThemeClearanceDescription => '為了施展戰術而清除我方攻擊格上的障礙物。'; @override - String get abortGame => '中止本局'; + String get puzzleThemeDefensiveMove => '加強防守'; @override - String get gameAborted => '棋局已中止'; + String get puzzleThemeDefensiveMoveDescription => '一種為了避免遺失棋子或優勢而採取的必要行動。'; @override - String get standard => '標準'; + String get puzzleThemeDeflection => '引離'; @override - String get unlimited => '無限'; + String get puzzleThemeDeflectionDescription => '為了分散敵人專注力所採取的戰術,容易搗亂敵人原本的計畫。'; @override - String get mode => '模式'; + String get puzzleThemeDiscoveredAttack => '閃擊'; @override - String get casual => '休閒'; + String get puzzleThemeDiscoveredAttackDescription => '將一子(例如騎士)移開長程攻擊格(例如城堡)。'; @override - String get rated => '排位賽'; + String get puzzleThemeDoubleCheck => '雙將'; @override - String get casualTournament => '休閒'; + String get puzzleThemeDoubleCheckDescription => '雙重將軍,讓我方能攻擊敵人的他子。'; @override - String get ratedTournament => '排位'; + String get puzzleThemeEndgame => '殘局'; @override - String get thisGameIsRated => '此對局是排位賽'; + String get puzzleThemeEndgameDescription => '棋局中最後階段的戰術'; @override - String get rematch => '重賽'; + String get puzzleThemeEnPassantDescription => '一種食敵方過路兵的戰略。'; @override - String get rematchOfferSent => '重賽請求已發送'; + String get puzzleThemeExposedKing => '未被保護的國王'; @override - String get rematchOfferAccepted => '重賽請求被接受'; + String get puzzleThemeExposedKingDescription => '攻擊未被保護的國王之戰術,常常導致將死。'; @override - String get rematchOfferCanceled => '重賽請求被取消'; + String get puzzleThemeFork => '捉雙'; @override - String get rematchOfferDeclined => '重賽請求被拒絕'; + String get puzzleThemeForkDescription => '一種同時攻擊敵方多個子,使敵方只能犧牲一子的戰術。'; @override - String get cancelRematchOffer => '取消重賽請求'; + String get puzzleThemeHangingPiece => '懸子'; @override - String get viewRematch => '觀看重賽'; + String get puzzleThemeHangingPieceDescription => '「免費」取得他子的戰術'; @override - String get confirmMove => '確認移動'; + String get puzzleThemeHookMate => '鉤將死'; @override - String get play => '下棋'; + String get puzzleThemeHookMateDescription => '利用車馬兵與一敵方兵以限制敵方國王的逃生路線。'; @override - String get inbox => '收件箱'; + String get puzzleThemeInterference => '干擾'; @override - String get chatRoom => '聊天室'; + String get puzzleThemeInterferenceDescription => '將一子擋在兩個敵方子之間以切斷防護,例如以騎士在兩車之間阻擋。'; @override - String get loginToChat => '登入以聊天'; + String get puzzleThemeIntermezzo => 'Intermezzo'; @override - String get youHaveBeenTimedOut => '由於時間原因您不能發言'; + String get puzzleThemeIntermezzoDescription => '與其走正常的棋譜,不如威脅敵方子吧!這樣不但可以破壞敵方原先計畫,還可以讓敵人必須對威脅採取對應的動作。這種戰術又稱為「Zwischenzug」或「In between」。'; @override - String get spectatorRoom => '觀眾室'; + String get puzzleThemeKnightEndgame => '馬殘局'; @override - String get composeMessage => '寫信息'; + String get puzzleThemeKnightEndgameDescription => '只剩馬和兵的殘局'; @override - String get subject => '主題'; + String get puzzleThemeLong => '長謎題'; @override - String get send => '發送'; + String get puzzleThemeLongDescription => '三步獲勝'; @override - String get incrementInSeconds => '增加秒數'; + String get puzzleThemeMaster => '大師棋局'; @override - String get freeOnlineChess => '免費線上國際象棋'; + String get puzzleThemeMasterDescription => '從頭銜玩家的棋局中生成的謎題。'; @override - String get exportGames => '導出棋局'; + String get puzzleThemeMasterVsMaster => '大師對局'; @override - String get ratingRange => '對方級別範圍'; + String get puzzleThemeMasterVsMasterDescription => '從兩位頭銜玩家的棋局中生成的謎題。'; @override - String get thisAccountViolatedTos => '此帳號違反了Lichess的使用規定'; + String get puzzleThemeMate => '將軍'; @override - String get openingExplorerAndTablebase => '開局瀏覽器 & 總局資料庫'; + String get puzzleThemeMateDescription => '以你的技能贏得勝利'; @override - String get takeback => '悔棋'; + String get puzzleThemeMateIn1 => '一步殺棋'; @override - String get proposeATakeback => '請求悔棋'; + String get puzzleThemeMateIn1Description => '一步將軍'; @override - String get takebackPropositionSent => '悔棋請求已發送'; + String get puzzleThemeMateIn2 => '兩步殺棋'; @override - String get takebackPropositionDeclined => '悔棋請求被拒絕'; + String get puzzleThemeMateIn2Description => '走兩步以達到將軍'; @override - String get takebackPropositionAccepted => '同意悔棋'; + String get puzzleThemeMateIn3 => '三步殺棋'; @override - String get takebackPropositionCanceled => '悔棋請求已取消'; + String get puzzleThemeMateIn3Description => '走三步以達到將軍'; @override - String get yourOpponentProposesATakeback => '對手請求悔棋'; + String get puzzleThemeMateIn4 => '四步殺棋'; @override - String get bookmarkThisGame => '收藏該棋局'; + String get puzzleThemeMateIn4Description => '走四步以達到將軍'; @override - String get tournament => '錦標賽'; + String get puzzleThemeMateIn5 => '五步或更高 將軍'; @override - String get tournaments => '錦標賽'; + String get puzzleThemeMateIn5Description => '看出較長的將死步驟。'; @override - String get tournamentPoints => '錦標賽得分'; + String get puzzleThemeMiddlegame => '中局'; @override - String get viewTournament => '觀看錦標賽'; + String get puzzleThemeMiddlegameDescription => '棋局中第二階段的戰術'; @override - String get backToTournament => '返回錦標賽主頁'; + String get puzzleThemeOneMove => '一步題'; @override - String get noDrawBeforeSwissLimit => '在瑞士錦標賽中,在下三十步棋前你不能提和.'; + String get puzzleThemeOneMoveDescription => '只有一步長的題目'; @override - String get thematic => '特殊開局'; + String get puzzleThemeOpening => '開局'; @override - String yourPerfRatingIsProvisional(String param) { - return '您目前的評分$param為臨時評分'; - } + String get puzzleThemeOpeningDescription => '棋局中起始階段的戰術'; @override - String yourPerfRatingIsTooHigh(String param1, String param2) { - return '您的 $param1 積分 ($param2) 過高'; - } + String get puzzleThemePawnEndgame => '兵殘局'; @override - String yourTopWeeklyPerfRatingIsTooHigh(String param1, String param2) { - return '您本週最高 $param1 積分 ($param2) 過高'; - } + String get puzzleThemePawnEndgameDescription => '只剩兵的殘局'; @override - String yourPerfRatingIsTooLow(String param1, String param2) { - return '您的 $param1 積分 ($param2) 過低'; - } + String get puzzleThemePin => '牽制'; @override - String ratedMoreThanInPerf(String param1, String param2) { - return '在$param2模式下的評分大於$param1'; - } + String get puzzleThemePinDescription => '一種涉及「牽制」,讓一敵方子無法在讓其他更高價值的子不被受到攻擊下移動的戰術。'; @override - String ratedLessThanInPerf(String param1, String param2) { - return '在$param2模式下的評分小於$param1'; - } + String get puzzleThemePromotion => '升變'; @override - String mustBeInTeam(String param) { - return '需要在$param團隊內'; - } + String get puzzleThemePromotionDescription => '讓兵走到後排升變為皇后或其他高價值的子。'; @override - String youAreNotInTeam(String param) { - return '您不在$param團隊中'; - } + String get puzzleThemeQueenEndgame => '后殘局'; @override - String get backToGame => '返回棋局'; + String get puzzleThemeQueenEndgameDescription => '只剩后和兵的殘局'; @override - String get siteDescription => '界面清新的免費線上國際象棋平台。不用註冊,沒有廣告,無需插件。快來與電腦、朋友和陌生人一起對戰吧!'; + String get puzzleThemeQueenRookEndgame => '后與車'; @override - String xJoinedTeamY(String param1, String param2) { - return '$param1加入$param2隊'; - } + String get puzzleThemeQueenRookEndgameDescription => '只剩后、車和兵的殘局'; @override - String xCreatedTeamY(String param1, String param2) { - return '$param1組建$param2隊'; - } + String get puzzleThemeQueensideAttack => '后翼攻擊'; @override - String get startedStreaming => '開始直播'; + String get puzzleThemeQueensideAttackDescription => '在對方於后翼易位後的攻擊。'; @override - String xStartedStreaming(String param) { - return '$param開始直播'; - } + String get puzzleThemeQuietMove => '安靜的一着'; @override - String get averageElo => '平均級別'; + String get puzzleThemeQuietMoveDescription => '隱藏在未來敵方無法避免的攻擊。'; @override - String get location => '所在地'; + String get puzzleThemeRookEndgame => '車殘局'; @override - String get filterGames => '篩選棋局'; + String get puzzleThemeRookEndgameDescription => '只剩車和兵的殘局'; @override - String get reset => '重置'; + String get puzzleThemeSacrifice => '棄子'; @override - String get apply => '套用'; + String get puzzleThemeSacrificeDescription => '犧牲我方子以在一系列的移動後得到優勢。'; @override - String get save => '儲存'; + String get puzzleThemeShort => '短謎題'; @override - String get leaderboard => '排行榜'; + String get puzzleThemeShortDescription => '兩步獲勝'; @override - String get screenshotCurrentPosition => '截圖當前頁面'; + String get puzzleThemeSkewer => '串擊'; @override - String get gameAsGIF => '保存棋局為 GIF'; + String get puzzleThemeSkewerDescription => '攻擊敵方高價值的子以讓敵方移開,以攻擊背後較為低價值未受保護的他子。為一種反向的「牽制」。'; @override - String get pasteTheFenStringHere => '在此處黏貼FEN棋譜'; + String get puzzleThemeSmotheredMate => '悶殺'; @override - String get pasteThePgnStringHere => '在此處黏貼PGN棋譜'; + String get puzzleThemeSmotheredMateDescription => '一種以馬將死被自身棋子所圍困的國王。'; @override - String get orUploadPgnFile => '或者上傳一個PGN文件'; + String get puzzleThemeSuperGM => '超級大師賽局'; @override - String get fromPosition => '自定義局面'; + String get puzzleThemeSuperGMDescription => '來自世界各地優秀玩家對局的戰術題'; @override - String get continueFromHere => '从此處繼續'; + String get puzzleThemeTrappedPiece => '被困的棋子'; @override - String get toStudy => '研究'; + String get puzzleThemeTrappedPieceDescription => '一子因為被限制逃生路線而無法逃離被犧牲的命運。'; @override - String get importGame => '導入棋局'; + String get puzzleThemeUnderPromotion => '升變'; @override - String get importGameExplanation => '貼上PGN棋譜後可以重播棋局,使用電腦分析、對局聊天室及取得此棋局的分享連結。'; + String get puzzleThemeUnderPromotionDescription => '升變成騎士、象或車'; @override - String get importGameCaveat => '變著分支將被刪除。 若要保存這些變著,請通過導入PGN棋譜創建一個研究。'; + String get puzzleThemeVeryLong => '非常長的謎題'; @override - String get thisIsAChessCaptcha => '這是一個國際象棋驗證碼。'; + String get puzzleThemeVeryLongDescription => '四步或以上獲勝'; @override - String get clickOnTheBoardToMakeYourMove => '點擊棋盤走棋以證明您是人類。'; + String get puzzleThemeXRayAttack => '穿透攻擊'; @override - String get captcha_fail => '請完成驗證。'; + String get puzzleThemeXRayAttackDescription => '以敵方子攻擊或防守的戰術。'; @override - String get notACheckmate => '沒有將死'; + String get puzzleThemeZugzwang => '等著'; @override - String get whiteCheckmatesInOneMove => '白方一步將死'; + String get puzzleThemeZugzwangDescription => '對方的棋子因為所移動的空間有限所以所到之處都會增加對方劣勢'; @override - String get blackCheckmatesInOneMove => '黑方一步棋將死對手'; + String get puzzleThemeMix => '綜合'; @override - String get retry => '重試'; + String get puzzleThemeMixDescription => '所有類型都有!你不知道會遇到什麼題型,所以請做好準備,就像在實戰一樣。'; @override - String get reconnecting => '重新連接中'; + String get puzzleThemePlayerGames => '玩家謎題'; @override - String get favoriteOpponents => '最喜歡的對手'; + String get puzzleThemePlayerGamesDescription => '查詢從你或其他玩家的對奕所生成的謎題。'; @override - String get follow => '關注'; + String puzzleThemePuzzleDownloadInformation(String param) { + return '這些為公開謎題,並且在 $param 提供下載管道。'; + } @override - String get following => '已關注'; + String get searchSearch => '搜尋'; @override - String get unfollow => '取消關注'; + String get settingsSettings => '設定'; @override - String followX(String param) { - return '追蹤$param'; - } + String get settingsCloseAccount => '關閉帳戶'; @override - String unfollowX(String param) { - return '取消追蹤$param'; - } + String get settingsManagedAccountCannotBeClosed => '您的帳號已被管理並且無法關閉。'; @override - String get block => '加入黑名單'; + String get settingsClosingIsDefinitive => '您確定要刪除帳號嗎?這是無法挽回的。'; @override - String get blocked => '已加入黑名單'; + String get settingsCantOpenSimilarAccount => '即使名稱大小寫不同,您也不能使用相同的名稱開設新帳戶'; @override - String get unblock => '移除出黑名單'; + String get settingsChangedMindDoNotCloseAccount => '我改變主意了,不要關閉我的帳號'; @override - String get followsYou => '關注您'; + String get settingsCloseAccountExplanation => '您真的確定要刪除帳戶嗎? 關閉帳戶是永久性的決定, 您將「永遠無法」再次登入。'; @override - String xStartedFollowingY(String param1, String param2) { - return '$param1開始關注$param2'; - } + String get settingsThisAccountIsClosed => '此帳號已被關閉。'; @override - String get more => '更多'; + String get playWithAFriend => '和好友下棋'; @override - String get memberSince => '註冊日期'; + String get playWithTheMachine => '和電腦下棋'; @override - String lastSeenActive(String param) { - return '最近登入 $param'; - } + String get toInviteSomeoneToPlayGiveThisUrl => '請分享此網址以邀人下棋'; @override - String get player => '棋手'; + String get gameOver => '遊戲結束'; @override - String get list => '列表'; + String get waitingForOpponent => '等待對手'; @override - String get graph => '圖表'; + String get orLetYourOpponentScanQrCode => '或是讓對手掃描這個 QR code'; @override - String get required => '必填項目。'; + String get waiting => '等待對手確認中'; @override - String get openTournaments => '公開錦標賽'; + String get yourTurn => '該您走'; @override - String get duration => '持續時間'; + String aiNameLevelAiLevel(String param1, String param2) { + return '$param1等級 $param2'; + } @override - String get winner => '勝利者'; + String get level => '難度'; @override - String get standing => '名次'; + String get strength => '強度'; @override - String get createANewTournament => '建立新的錦標賽'; + String get toggleTheChat => '聊天開關'; @override - String get tournamentCalendar => '錦標賽日程'; + String get chat => '聊天'; @override - String get conditionOfEntry => '加入限制:'; + String get resign => '認輸'; @override - String get advancedSettings => '高級設定'; + String get checkmate => '將死'; @override - String get safeTournamentName => '幫錦標賽挑選一個適合的名字'; + String get stalemate => '逼和'; @override - String get inappropriateNameWarning => '即便只是一點點的違規都有可能導致您的帳號被封鎖。'; + String get white => '執白'; @override - String get emptyTournamentName => '若不填入錦標賽的名稱,將會用一位著名的棋手名字來做為錦標賽名稱。'; + String get black => '執黑'; @override - String get makePrivateTournament => '把錦標賽設定為私人,並設定密碼來限制進入。'; + String get asWhite => '作為白方'; @override - String get join => '加入'; + String get asBlack => '作為黑方'; @override - String get withdraw => '離開'; + String get randomColor => '隨機選色'; @override - String get points => '分數'; + String get createAGame => '開始對局'; @override - String get wins => '勝'; + String get whiteIsVictorious => '白方勝'; @override - String get losses => '負'; + String get blackIsVictorious => '黑方勝'; @override - String get createdBy => '創建者:'; + String get youPlayTheWhitePieces => '您執白棋'; @override - String get tournamentIsStarting => '錦標賽即將開始'; + String get youPlayTheBlackPieces => '您執黑棋'; @override - String get tournamentPairingsAreNowClosed => '此錦標賽的對手配對已結束。'; + String get itsYourTurn => '該您走!'; @override - String standByX(String param) { - return '$param準備好,你馬上要開始對棋了!'; - } + String get cheatDetected => '偵測到作弊行為'; @override - String get pause => '暫停'; + String get kingInTheCenter => '王居中'; @override - String get resume => '繼續'; + String get threeChecks => '三次將軍'; @override - String get youArePlaying => '等待對手中'; + String get raceFinished => '王至第八排'; @override - String get winRate => '勝率'; + String get variantEnding => '變體終局'; @override - String get berserkRate => '快棋率'; + String get newOpponent => '換個對手'; @override - String get performance => '表現'; + String get yourOpponentWantsToPlayANewGameWithYou => '您的對手想和你複賽'; @override - String get tournamentComplete => '錦標賽結束'; + String get joinTheGame => '加入這盤棋'; @override - String get movesPlayed => '步數'; + String get whitePlays => '白方走棋'; @override - String get whiteWins => '白方獲勝'; + String get blackPlays => '黑方走棋'; @override - String get blackWins => '黑方獲勝'; + String get opponentLeftChoices => '對方可能已經離開遊戲。您可以選擇:取勝、和棋或等待對方走棋。'; @override - String get drawRate => '和棋率'; + String get forceResignation => '取勝'; @override - String get draws => '和棋'; + String get forceDraw => '和棋'; @override - String nextXTournament(String param) { - return '下一個$param錦標賽'; - } + String get talkInChat => '請在聊天室裡文明一點'; @override - String get averageOpponent => '平均對手評分'; + String get theFirstPersonToComeOnThisUrlWillPlayWithYou => '第一個訪問該網址的人將與您下棋。'; @override - String get boardEditor => '棋盤編輯器'; + String get whiteResigned => '白方認輸'; @override - String get setTheBoard => '設定版型'; + String get blackResigned => '黑方認輸'; @override - String get popularOpenings => '使用率最高的開局'; + String get whiteLeftTheGame => '白方棄賽'; @override - String get endgamePositions => '殘局局面'; + String get blackLeftTheGame => '黑方棄賽'; @override - String chess960StartPosition(String param) { - return '960棋局開局位置: $param'; - } + String get whiteDidntMove => '白方沒有走棋'; @override - String get startPosition => '初始佈局'; + String get blackDidntMove => '黑方沒有走棋'; @override - String get clearBoard => '清空棋盤'; + String get requestAComputerAnalysis => '請求電腦分析'; @override - String get loadPosition => '裝入佈局'; + String get computerAnalysis => '電腦分析'; @override - String get isPrivate => '私人'; + String get computerAnalysisAvailable => '電腦分析可用'; @override - String reportXToModerators(String param) { - return '將$param報告給管理人員'; - } + String get computerAnalysisDisabled => '未啟用電腦分析'; @override - String profileCompletion(String param) { - return '個人檔案完成度:$param'; - } + String get analysis => '分析棋局'; @override - String xRating(String param) { - return '$param評分'; + String depthX(String param) { + return '深度 $param'; } @override - String get ifNoneLeaveEmpty => '如果沒有,請留空'; + String get usingServerAnalysis => '正在使用伺服器分析'; @override - String get profile => '資料'; + String get loadingEngine => '正在載入引擎 ...'; @override - String get editProfile => '編輯資料'; + String get calculatingMoves => '計算著法中...'; @override - String get setFlair => '設置你的圖標'; + String get engineFailed => '加載引擎出錯'; @override - String get flair => '圖標'; + String get cloudAnalysis => '雲端分析'; @override - String get youCanHideFlair => '有一個設置可以隱藏整個網站上所有用户圖標。'; + String get goDeeper => '深入分析'; @override - String get biography => '個人簡介'; + String get showThreat => '顯示敵方威脅'; @override - String get countryRegion => '國家或地區'; + String get inLocalBrowser => '在本地瀏覽器'; @override - String get thankYou => '謝謝!'; + String get toggleLocalEvaluation => '使用您當地的伺服器分析'; @override - String get socialMediaLinks => '官方社群連結'; + String get promoteVariation => '增加變化'; @override - String get oneUrlPerLine => '每行一個網址'; + String get makeMainLine => '將這步棋導入主要流程中'; @override - String get inlineNotation => '棋譜集中顯示'; + String get deleteFromHere => '從這處開始刪除'; @override - String get makeAStudy => '為了安全保管和分享,考慮創建一項研討.'; + String get collapseVariations => '隱藏變體'; @override - String get clearSavedMoves => '清空著法儲存'; + String get expandVariations => '顯示變體'; @override - String get previouslyOnLichessTV => '過去的Lichess TV直播'; + String get forceVariation => '移除變化'; @override - String get onlinePlayers => '在線棋手'; + String get copyVariationPgn => '複製變體 PGN'; @override - String get activePlayers => '活躍棋手'; + String get move => '走棋'; @override - String get bewareTheGameIsRatedButHasNoClock => '注意,這棋局是排位賽,但是不計時!'; + String get variantLoss => '您因特殊規則而輸了'; @override - String get success => '大功告成!'; + String get variantWin => '您因特殊規則而贏了'; @override - String get automaticallyProceedToNextGameAfterMoving => '移动棋子后自动进入下一盘棋'; + String get insufficientMaterial => '由於棋子不足而導致平局'; @override - String get autoSwitch => '自动更换'; + String get pawnMove => '小兵移動'; @override - String get puzzles => '謎題'; + String get capture => '吃子'; @override - String get name => '名'; + String get close => '關閉'; @override - String get description => '描述'; + String get winning => '贏棋'; @override - String get descPrivate => '內部簡介'; + String get losing => '輸棋'; @override - String get descPrivateHelp => '僅團隊成員可見,設置後將覆蓋公開簡介為團隊成員展示。'; + String get drawn => '平手'; @override - String get no => '否'; + String get unknown => '未知'; @override - String get yes => '是'; + String get database => '資料庫'; @override - String get help => '幫助:'; + String get whiteDrawBlack => '白棋獲勝 / 平局 / 黑棋獲勝'; @override - String get createANewTopic => '新话题'; + String averageRatingX(String param) { + return '平均評分: $param'; + } @override - String get topics => '话题'; + String get recentGames => '最近的棋局'; @override - String get posts => '貼文'; + String get topGames => '評分最高的棋局'; @override - String get lastPost => '最近貼文'; + String masterDbExplanation(String param1, String param2, String param3) { + return '來自$param2到$param3年國際棋聯積分$param1以上的棋手對局棋譜'; + } @override - String get views => '浏览'; + String get dtzWithRounding => '經過四捨五入的DTZ50\'\',是基於到下次吃子或兵動的半步數目。'; @override - String get replies => '回复'; + String get noGameFound => '未找到遊戲'; @override - String get replyToThisTopic => '回复此话题'; + String get maxDepthReached => '已達到最大深度!'; @override - String get reply => '回复'; + String get maybeIncludeMoreGamesFromThePreferencesMenu => '試著從設定中加入更多棋局'; @override - String get message => '信息'; + String get openings => '開局'; @override - String get createTheTopic => '创建话题'; + String get openingExplorer => '開局瀏覽器'; @override - String get reportAUser => '举报用户'; + String get openingEndgameExplorer => '開局與終局瀏覽器'; @override - String get user => '用户'; + String xOpeningExplorer(String param) { + return '$param開局瀏覽器'; + } @override - String get reason => '原因'; + String get playFirstOpeningEndgameExplorerMove => '在開局/殘局瀏覽器走第一步棋'; @override - String get whatIsIheMatter => '举报原因?'; + String get winPreventedBy50MoveRule => '在不違反50步和局規則下贏得這局棋'; @override - String get cheat => '作弊'; + String get lossSavedBy50MoveRule => '藉由50步和局規則來避免輸掉棋局'; @override - String get troll => '钓鱼'; + String get winOr50MovesByPriorMistake => '贏棋或因先前錯誤50步作和'; @override - String get other => '其他'; + String get lossOr50MovesByPriorMistake => '輸棋或因先前錯誤50步作和'; @override - String get reportDescriptionHelp => '附上游戏的网址解释该用户的行为问题'; + String get unknownDueToRounding => '由上次吃子或兵動開始按殘局庫建議走法走才能保證勝敗的判斷正確。這是因為Syzygy殘局庫的DTZ數值可能經過四捨五入。'; @override - String get error_provideOneCheatedGameLink => '請提供至少一局作弊棋局的連結。'; + String get allSet => '一切就緒!'; @override - String by(String param) { - return '$param作'; - } + String get importPgn => '匯入 PGN'; @override - String importedByX(String param) { - return '由$param滙入'; - } + String get delete => '刪除'; @override - String get thisTopicIsNowClosed => '本话题已关闭。'; + String get deleteThisImportedGame => '刪除此匯入的棋局?'; @override - String get blog => '博客'; + String get replayMode => '重播模式'; @override - String get notes => '笔记'; + String get realtimeReplay => '實時'; @override - String get typePrivateNotesHere => '在此輸入私人筆記'; + String get byCPL => '以厘兵損失'; @override - String get writeAPrivateNoteAboutThisUser => '備註用戶資訊'; + String get openStudy => '開啟研究'; @override - String get noNoteYet => '尚無筆記'; + String get enable => '啟用'; @override - String get invalidUsernameOrPassword => '用户名或密碼錯誤'; + String get bestMoveArrow => '最佳移動的箭頭'; @override - String get incorrectPassword => '舊密碼錯誤'; + String get showVariationArrows => '顯示變體箭頭'; @override - String get invalidAuthenticationCode => '驗證碼無效'; + String get evaluationGauge => '評估條'; @override - String get emailMeALink => '通過電郵發送連結給我'; + String get multipleLines => '路線分析線'; @override - String get currentPassword => '目前密碼'; + String get cpus => 'CPU 數量'; @override - String get newPassword => '新密碼'; + String get memory => '記憶體'; @override - String get newPasswordAgain => '重複新密碼'; + String get infiniteAnalysis => '無限分析'; @override - String get newPasswordsDontMatch => '新密碼不符合'; + String get removesTheDepthLimit => '取消深度限制,可能會使您的電腦發熱。'; @override - String get newPasswordStrength => '密碼強度'; + String get blunder => '漏著'; @override - String get clockInitialTime => '棋鐘起始時間'; + String get mistake => '錯誤'; @override - String get clockIncrement => '加秒'; + String get inaccuracy => '輕微失誤'; @override - String get privacy => '隱私'; + String get moveTimes => '走棋時間'; @override - String get privacyPolicy => '隱私條款'; + String get flipBoard => '翻轉棋盤'; @override - String get letOtherPlayersFollowYou => '允许其他玩家关注'; + String get threefoldRepetition => '三次重複局面'; @override - String get letOtherPlayersChallengeYou => '允许其他玩家挑战'; + String get claimADraw => '要求和棋'; @override - String get letOtherPlayersInviteYouToStudy => '允許其他棋手邀請你參加研討'; + String get offerDraw => '提出和棋'; @override - String get sound => '聲音'; + String get draw => '和棋'; @override - String get none => '無'; + String get drawByMutualAgreement => '雙方同意和局'; @override - String get fast => '快'; + String get fiftyMovesWithoutProgress => '50步規則和局'; @override - String get normal => '普通'; + String get currentGames => '當前對局'; @override - String get slow => '慢'; + String get viewInFullSize => '在整個網頁裡觀看棋局'; @override - String get insideTheBoard => '棋盤內'; + String get logOut => '登出'; @override - String get outsideTheBoard => '棋盤外'; + String get signIn => '登入'; @override - String get onSlowGames => '慢棋時'; + String get rememberMe => '保持登入狀態'; @override - String get always => '總是'; + String get youNeedAnAccountToDoThat => '請註冊以完成該操作'; @override - String get never => '永不'; + String get signUp => '註冊'; @override - String xCompetesInY(String param1, String param2) { - return '$param1参加$param2'; - } + String get computersAreNotAllowedToPlay => '電腦與電腦輔助棋手不允許參加對弈。對弈時,請勿從國際象棋引擎、資料庫以及其他棋手那裡獲取幫助。另外,強烈建議不要創建多個帳號;過分地使用多個帳號將導致封號。'; @override - String get victory => '成功!'; + String get games => '棋局'; @override - String get defeat => '戰敗'; + String get forum => '論壇'; @override - String victoryVsYInZ(String param1, String param2, String param3) { - return '$param1在$param3模式下贏了$param2'; + String xPostedInForumY(String param1, String param2) { + return '$param1發帖:$param2'; } @override - String defeatVsYInZ(String param1, String param2, String param3) { - return '$param1在$param3模式下輸給了$param2'; - } + String get latestForumPosts => '最新論壇貼文'; @override - String drawVsYInZ(String param1, String param2, String param3) { - return '$param1在$param3模式下和$param2平手'; - } + String get players => '棋手'; @override - String get timeline => '时间线'; + String get friends => '朋友'; @override - String get starting => '开始时间:'; + String get otherPlayers => '其他玩家'; @override - String get allInformationIsPublicAndOptional => '所有資料是公開的,同時是可選的。'; + String get discussions => '對話'; @override - String get biographyDescription => '給我們一個您的自我介紹,像是您的興趣、您喜愛的選手等'; + String get today => '今天'; @override - String get listBlockedPlayers => '显示黑名单用户列表'; + String get yesterday => '昨天'; @override - String get human => '人类'; + String get minutesPerSide => '每方分鐘數'; @override - String get computer => '電腦'; + String get variant => '變體'; @override - String get side => '方'; + String get variants => '變體'; @override - String get clock => '鐘'; + String get timeControl => '時間限制'; @override - String get opponent => '对手'; + String get realTime => '實時棋局'; @override - String get learnMenu => '學棋'; + String get correspondence => '通信棋局'; @override - String get studyMenu => '研究'; + String get daysPerTurn => '每步允許天數'; @override - String get practice => '練習'; + String get oneDay => '一天'; @override - String get community => '社區'; + String get time => '時間'; @override - String get tools => '工具'; + String get rating => '評級'; @override - String get increment => '加秒'; + String get ratingStats => '評分數據'; @override - String get error_unknown => '無效值'; + String get username => '使用者名稱'; @override - String get error_required => '本项必填'; + String get usernameOrEmail => '使用者名稱或電郵地址'; @override - String get error_email => '這個電子郵件地址無效'; + String get changeUsername => '更改使用者名稱'; @override - String get error_email_acceptable => '該電子郵件地址是不可用。請重新檢查後重試。'; + String get changeUsernameNotSame => '只能更改字母大小字。例如,將「johndoe」變成「JohnDoe」。'; @override - String get error_email_unique => '電子郵件地址無效或已被使用'; + String get changeUsernameDescription => '更改使用者名稱。您最多可以更改一次字母大小寫。'; @override - String get error_email_different => '這已經是您的電子郵件地址'; + String get signupUsernameHint => '請選擇一個妥當的使用者名稱。請注意使用者名稱無法再次更改,並且不妥當的名稱會導致帳號被封禁!'; @override - String error_minLength(String param) { - return '至少應有 $param 個字元長'; - } + String get signupEmailHint => '僅用於密碼重置'; @override - String error_maxLength(String param) { - return '最多不能超過 $param 個字元長'; - } + String get password => '密碼'; @override - String error_min(String param) { - return '最少 $param 個字符'; - } + String get changePassword => '更改密碼'; @override - String error_max(String param) { - return '最大不能超過 $param'; - } + String get changeEmail => '更改電子郵件'; @override - String ifRatingIsPlusMinusX(String param) { - return '允许评级范围±$param'; - } + String get email => '電子郵件'; @override - String get ifRegistered => '如已註冊'; + String get passwordReset => '重置密碼'; @override - String get onlyExistingConversations => '僅目前對話'; + String get forgotPassword => '忘記密碼?'; @override - String get onlyFriends => '只允許好友'; + String get error_weakPassword => '此密碼太常見,且很容易被猜到。'; @override - String get menu => '菜单'; + String get error_namePassword => '請不要把密碼設為使用者名稱。'; @override - String get castling => '王车易位'; + String get blankedPassword => '你在其他站點使用過相同的密碼,並且這些站點已經失效。為確保你的 Lichess 帳戶安全,你需要設置新密碼。感謝你的理解。'; @override - String get whiteCastlingKingside => '白方短易位'; + String get youAreLeavingLichess => '你正要離開 Lichess'; @override - String get blackCastlingKingside => '黑方短易位'; + String get neverTypeYourPassword => '不要在其他網站輸入你的 Lichess 密碼!'; @override - String tpTimeSpentPlaying(String param) { - return '花在下棋上的時間:$param'; + String proceedToX(String param) { + return '前往 $param'; } @override - String get watchGames => '觀看對局直播'; - - @override - String tpTimeSpentOnTV(String param) { - return '花在Lichess TV觀看直播的時間:$param'; - } + String get passwordSuggestion => '不要使用他人建議的密碼,他們會用此密碼盜取你的帳戶。'; @override - String get watch => '觀看'; + String get emailSuggestion => '不要使用他人提供的電子郵件,他們會用它盜取您的帳號。'; @override - String get videoLibrary => '影片庫'; + String get emailConfirmHelp => '協助電郵確認'; @override - String get streamersMenu => '實況主'; + String get emailConfirmNotReceived => '註冊後沒有收到確認郵件?'; @override - String get mobileApp => '行動應用程式'; + String get whatSignupUsername => '你用了什麼使用者名稱註冊?'; @override - String get webmasters => '網站管理員'; + String usernameNotFound(String param) { + return '找不到使用者名稱 $param。'; + } @override - String get about => '關於'; + String get usernameCanBeUsedForNewAccount => '你可以使用這個用戶名創建帳戶'; @override - String aboutX(String param) { - return '關於 $param'; + String emailSent(String param) { + return '我們向 $param 發送了電子郵件。'; } @override - String xIsAFreeYLibreOpenSourceChessServer(String param1, String param2) { - return '$param1是一個免費的($param2),開放性的,無廣告,開放資源的網站'; - } + String get emailCanTakeSomeTime => '可能需要一些時間才能收到。'; @override - String get really => '真的'; + String get refreshInboxAfterFiveMinutes => '等待5分鐘並刷新你的收件箱。'; @override - String get contribute => '協助'; + String get checkSpamFolder => '嘗試檢查你的垃圾郵件收件匣,它可能在那裡。 如果在,請將其標記為非垃圾郵件。'; @override - String get termsOfService => '服務條款'; + String get emailForSignupHelp => '如果其他所有的方法都失敗了,給我們發這條短信:'; @override - String get sourceCode => '原始碼'; + String copyTextToEmail(String param) { + return '複製並貼上上面的文字然後把它發給$param'; + } @override - String get simultaneousExhibitions => '車輪戰'; + String get waitForSignupHelp => '我們很快就會給你回覆,説明你完成註冊。'; @override - String get host => '主持'; + String accountConfirmed(String param) { + return '使用者 $param 認證成功'; + } @override - String hostColorX(String param) { - return '主持者使用旗子顏色:$param'; + String accountCanLogin(String param) { + return '你可以做為 $param 登入了。'; } @override - String get yourPendingSimuls => '你待處理的車輪戰'; + String get accountConfirmationEmailNotNeeded => '你不需要確認電子郵件。'; @override - String get createdSimuls => '最近开始的同步赛'; + String accountClosed(String param) { + return '帳戶 $param 被關閉。'; + } @override - String get hostANewSimul => '主持新同步赛'; + String accountRegisteredWithoutEmail(String param) { + return '帳戶 $param 未使用電子郵箱註冊。'; + } @override - String get signUpToHostOrJoinASimul => '註冊以舉辦或參與車輪戰'; + String get rank => '排名'; @override - String get noSimulFound => '找不到该同步赛'; + String rankX(String param) { + return '排名:$param'; + } @override - String get noSimulExplanation => '此車輪戰不存在。'; + String get gamesPlayed => '下過局數'; @override - String get returnToSimulHomepage => '返回表演赛主页'; + String get cancel => '取消'; @override - String get aboutSimul => '車輪戰涉及到一個人同時和幾位棋手下棋。'; + String get whiteTimeOut => '白方時間到'; @override - String get aboutSimulImage => '在50个对手中,菲舍尔赢了47局,和了2局,输了1局。'; + String get blackTimeOut => '黑方時間到'; @override - String get aboutSimulRealLife => '这个概念来自真实的国际赛事。 在现实中,这涉及到主持在桌与桌之间来回穿梭走棋。'; + String get drawOfferSent => '和棋請求已發送'; @override - String get aboutSimulRules => '当表演赛开始的时候, 每个玩家都与主持开始对弈, 而主持用白方。 当所有的对局都结束时,表演赛就结束了。'; + String get drawOfferAccepted => '同意和棋'; @override - String get aboutSimulSettings => '表演赛总是不定级的。 复赛、悔棋和\"加时\"功能将被禁用。'; + String get drawOfferCanceled => '和棋取消'; @override - String get create => '创建'; + String get whiteOffersDraw => '白方提出和棋'; @override - String get whenCreateSimul => '當您創建車輪戰時,您要同時跟幾個棋手一起下棋。'; + String get blackOffersDraw => '黑方提出和棋'; @override - String get simulVariantsHint => '如果您选择几个变体,每个玩家都要选择下哪一种。'; + String get whiteDeclinesDraw => '白方拒絕和棋'; @override - String get simulClockHint => '菲舍爾時鐘設定。棋手越多,您需要的時間可能就越多。'; + String get blackDeclinesDraw => '黑方拒絕和棋'; @override - String get simulAddExtraTime => '您可以給您的時鍾多加點時間以幫助您應對車輪戰。'; + String get yourOpponentOffersADraw => '您的對手提出和棋'; @override - String get simulHostExtraTime => '主持人的额外时间'; + String get accept => '接受'; @override - String get simulAddExtraTimePerPlayer => '每有一個玩家加入車輪戰,您棋鐘的初始時間都將增加。'; + String get decline => '拒絕'; @override - String get simulHostExtraTimePerPlayer => '每個玩家加入后棋鐘增加的額外時間'; + String get playingRightNow => '正在對局'; @override - String get lichessTournaments => 'Lichess比赛'; + String get eventInProgress => '正在進行'; @override - String get tournamentFAQ => '比赛常见问题'; + String get finished => '已結束'; @override - String get timeBeforeTournamentStarts => '比赛准备时间'; + String get abortGame => '中止本局'; @override - String get averageCentipawnLoss => '平均厘兵损失'; + String get gameAborted => '棋局已中止'; @override - String get accuracy => '精準度'; + String get standard => '標準'; @override - String get keyboardShortcuts => '快捷键'; + String get customPosition => '自定義局面'; @override - String get keyMoveBackwardOrForward => '后退/前进'; + String get unlimited => '無限'; @override - String get keyGoToStartOrEnd => '跳到开始/结束'; + String get mode => '模式'; @override - String get keyCycleSelectedVariation => '循環已選取的變體'; + String get casual => '休閒'; + + @override + String get rated => '排位賽'; @override - String get keyShowOrHideComments => '显示/隐藏评论'; + String get casualTournament => '休閒'; @override - String get keyEnterOrExitVariation => '进入/退出变体'; + String get ratedTournament => '排位'; @override - String get keyRequestComputerAnalysis => '請求引擎分析,從你的失誤中學習'; + String get thisGameIsRated => '此對局是排位賽'; @override - String get keyNextLearnFromYourMistakes => '下一個 (從你的失誤中學習)'; + String get rematch => '重賽'; @override - String get keyNextBlunder => '下一個漏著'; + String get rematchOfferSent => '重賽請求已發送'; @override - String get keyNextMistake => '下一個錯著'; + String get rematchOfferAccepted => '重賽請求被接受'; @override - String get keyNextInaccuracy => '下一個疑著'; + String get rematchOfferCanceled => '重賽請求被取消'; @override - String get keyPreviousBranch => '上一個分支'; + String get rematchOfferDeclined => '重賽請求被拒絕'; @override - String get keyNextBranch => '下一個分支'; + String get cancelRematchOffer => '取消重賽請求'; @override - String get toggleVariationArrows => '切換變體箭頭'; + String get viewRematch => '觀看重賽'; @override - String get cyclePreviousOrNextVariation => '循環上一個/下一個變體'; + String get confirmMove => '確認移動'; @override - String get toggleGlyphAnnotations => '切換圖形標註'; + String get play => '下棋'; @override - String get variationArrowsInfo => '變體箭頭讓你不需棋步列表導航'; + String get inbox => '收件箱'; @override - String get playSelectedMove => '走已選的棋步'; + String get chatRoom => '聊天室'; @override - String get newTournament => '新比赛'; + String get loginToChat => '登入以聊天'; @override - String get tournamentHomeTitle => '国际象棋赛事均设有不同的时间控制和变体'; + String get youHaveBeenTimedOut => '您已被禁言'; @override - String get tournamentHomeDescription => '加入快節奏的國際象棋比賽!加入定時賽事,或創建自己的。子彈,閃電,經典,菲舍爾任意制,王到中心,三次將軍,並提供更多的選擇為無盡的國際象棋樂趣。'; + String get spectatorRoom => '觀眾室'; @override - String get tournamentNotFound => '找不到该比赛'; + String get composeMessage => '寫信息'; @override - String get tournamentDoesNotExist => '这个比赛不存在。'; + String get subject => '主題'; @override - String get tournamentMayHaveBeenCanceled => '它可能已被取消,假如所有的对手在比赛开始之前离开。'; + String get send => '發送'; @override - String get returnToTournamentsHomepage => '返回比赛主页'; + String get incrementInSeconds => '增加秒數'; @override - String weeklyPerfTypeRatingDistribution(String param) { - return '本月$param的分数分布'; - } + String get freeOnlineChess => '免費線上西洋棋'; @override - String yourPerfTypeRatingIsRating(String param1, String param2) { - return '您的$param1分数是$param2分。'; - } + String get exportGames => '導出棋局'; @override - String youAreBetterThanPercentOfPerfTypePlayers(String param1, String param2) { - return '您比$param1的$param2棋手更强。'; - } + String get ratingRange => '對方級別範圍'; @override - String userIsBetterThanPercentOfPerfTypePlayers(String param1, String param2, String param3) { - return '$param1比$param3之中的$param2棋手強。'; - } + String get thisAccountViolatedTos => '此帳號違反了Lichess的使用規定'; @override - String betterThanPercentPlayers(String param1, String param2) { - return '您比$param1的$param2棋手更強。'; - } + String get openingExplorerAndTablebase => '開局瀏覽器 & 總局資料庫'; @override - String youDoNotHaveAnEstablishedPerfTypeRating(String param) { - return '您没有准确的$param评级。'; - } + String get takeback => '悔棋'; @override - String get yourRating => '您的評分'; + String get proposeATakeback => '請求悔棋'; @override - String get cumulative => '平均累積'; + String get takebackPropositionSent => '悔棋請求已發送'; @override - String get glicko2Rating => 'Glicko-2 積分'; + String get takebackPropositionDeclined => '悔棋請求被拒絕'; @override - String get checkYourEmail => '請檢查您的電子郵件'; + String get takebackPropositionAccepted => '悔棋請求被接受'; @override - String get weHaveSentYouAnEmailClickTheLink => '我們已經發送了一封電子郵件到你的郵箱. 點擊郵件中的連結以激活您的賬號.'; + String get takebackPropositionCanceled => '悔棋請求已取消'; @override - String get ifYouDoNotSeeTheEmailCheckOtherPlaces => '若您沒收到郵件,請檢查您的其他收件箱,例如垃圾箱、促銷、社交等。'; + String get yourOpponentProposesATakeback => '對手請求悔棋'; @override - String weHaveSentYouAnEmailTo(String param) { - return '我們發送了一封郵件到 $param。點擊郵件中的連結來重置您的密碼。'; - } + String get bookmarkThisGame => '收藏該棋局'; @override - String byRegisteringYouAgreeToBeBoundByOur(String param) { - return '您一登记,我们就假设您同意尊重我们的使用规则($param)。'; - } + String get tournament => '錦標賽'; @override - String readAboutOur(String param) { - return '閱讀我們的$param'; - } + String get tournaments => '錦標賽'; @override - String get networkLagBetweenYouAndLichess => '您和 lichess 之間的網絡時滯'; + String get tournamentPoints => '錦標賽得分'; @override - String get timeToProcessAMoveOnLichessServer => 'lichess 伺服器上處理走棋的時間'; + String get viewTournament => '觀看錦標賽'; @override - String get downloadAnnotated => '下载带笔记的记录'; + String get backToTournament => '返回錦標賽主頁'; @override - String get downloadRaw => '下载无笔记的记录'; + String get noDrawBeforeSwissLimit => '在積分循環制錦標賽中,在下三十步棋前無法和局。'; @override - String get downloadImported => '下载已导入棋局'; + String get thematic => '特殊開局'; @override - String get crosstable => '历史表'; + String yourPerfRatingIsProvisional(String param) { + return '您目前的評分$param為臨時評分'; + } @override - String get youCanAlsoScrollOverTheBoardToMoveInTheGame => '您也可以用滚动键在棋盘游戏中移动。'; + String yourPerfRatingIsTooHigh(String param1, String param2) { + return '您的 $param1 積分 ($param2) 過高'; + } @override - String get scrollOverComputerVariationsToPreviewThem => '將鼠標移到電腦分析變招上進行預覽'; + String yourTopWeeklyPerfRatingIsTooHigh(String param1, String param2) { + return '您本週最高 $param1 積分 ($param2) 過高'; + } @override - String get analysisShapesHowTo => '按shift点击或右键棋盘上绘制圆圈和箭头。'; + String yourPerfRatingIsTooLow(String param1, String param2) { + return '您的 $param1 積分 ($param2) 過低'; + } @override - String get letOtherPlayersMessageYou => '允許其他人發送私訊給您'; + String ratedMoreThanInPerf(String param1, String param2) { + return '在$param2模式下的評分大於$param1'; + } @override - String get receiveForumNotifications => '在論壇中被提及時接收通知'; + String ratedLessThanInPerf(String param1, String param2) { + return '在$param2模式下的評分小於$param1'; + } @override - String get shareYourInsightsData => '分享您的慧眼数据'; + String mustBeInTeam(String param) { + return '需要在$param團隊內'; + } @override - String get withNobody => '不分享'; + String youAreNotInTeam(String param) { + return '您不在$param團隊中'; + } @override - String get withFriends => '與好友分享'; + String get backToGame => '返回棋局'; @override - String get withEverybody => '與所有人分享'; + String get siteDescription => '界面清新的免費線上國際象棋平台。不用註冊,沒有廣告,無需插件。快來與電腦、朋友和陌生人一起對戰吧!'; @override - String get kidMode => '兒童模式'; + String xJoinedTeamY(String param1, String param2) { + return '$param1加入$param2隊'; + } @override - String get kidModeIsEnabled => '已啓用兒童模式'; + String xCreatedTeamY(String param1, String param2) { + return '$param1組建$param2隊'; + } @override - String get kidModeExplanation => '考量安全,在兒童模式中,網站上全部的文字交流將會被關閉。開啟此模式來保護你的孩子及學生不被網路上的人傷害。'; + String get startedStreaming => '開始直播'; @override - String inKidModeTheLichessLogoGetsIconX(String param) { - return '在兒童模式下,Lichess的標誌會有一個$param圖示,讓你知道你的孩子是安全的。'; + String xStartedStreaming(String param) { + return '$param開始直播'; } @override - String get askYourChessTeacherAboutLiftingKidMode => '你的帳戶被管理,詢問你的老師解除兒童模式。'; + String get averageElo => '平均級別'; @override - String get enableKidMode => '啟用兒童模式'; + String get location => '所在地'; @override - String get disableKidMode => '停用兒童模式'; + String get filterGames => '篩選棋局'; @override - String get security => '資訊安全相關設定'; + String get reset => '重置'; @override - String get sessions => '會話'; + String get apply => '套用'; @override - String get revokeAllSessions => '登出所有裝置'; + String get save => '儲存'; @override - String get playChessEverywhere => '随处下棋!'; + String get leaderboard => '排行榜'; @override - String get asFreeAsLichess => '完全又永遠的免費。'; + String get screenshotCurrentPosition => '截圖當前頁面'; @override - String get builtForTheLoveOfChessNotMoney => '不是為了錢,是為了國際象棋所創建。'; + String get gameAsGIF => '保存棋局為 GIF'; @override - String get everybodyGetsAllFeaturesForFree => '每個人都能免費使用所有功能'; + String get pasteTheFenStringHere => '在此處貼上 FEN 棋譜'; @override - String get zeroAdvertisement => '沒有廣告'; + String get pasteThePgnStringHere => '在此處貼上 PGN 棋譜'; @override - String get fullFeatured => '功能全面'; + String get orUploadPgnFile => '或者上傳一個PGN文件'; @override - String get phoneAndTablet => '手機和平板電腦'; + String get fromPosition => '自定義局面'; @override - String get bulletBlitzClassical => '子彈,閃電,經典'; + String get continueFromHere => '從此處繼續'; @override - String get correspondenceChess => '通訊賽'; + String get toStudy => '研究'; @override - String get onlineAndOfflinePlay => '線上或離線下棋'; + String get importGame => '導入棋局'; @override - String get viewTheSolution => '看解答'; + String get importGameExplanation => '貼上PGN棋譜後可以重播棋局,使用電腦分析、對局聊天室及取得此棋局的分享連結。'; @override - String get followAndChallengeFriends => '添加好友並與他們對戰'; + String get importGameCaveat => '變種分支將被刪除。 若要保存這些變種,請透過導入 PGN 棋譜建立一個研究。'; @override - String get gameAnalysis => '棋局分析研究'; + String get importGameDataPrivacyWarning => '此為公開 PGN。若要導入私人棋局,請使用研究。'; @override - String xHostsY(String param1, String param2) { - return '$param1主持$param2'; - } + String get thisIsAChessCaptcha => '此為西洋棋驗證碼。'; @override - String xJoinsY(String param1, String param2) { - return '$param1加入$param2'; - } + String get clickOnTheBoardToMakeYourMove => '點擊棋盤走棋以證明您是人類。'; @override - String xLikesY(String param1, String param2) { - return '$param1對$param2按讚'; - } - - @override - String get quickPairing => '快速配對'; + String get captcha_fail => '請完成驗證。'; @override - String get lobby => '大廳'; + String get notACheckmate => '沒有將死'; @override - String get anonymous => '匿名用户'; + String get whiteCheckmatesInOneMove => '白方一步將死'; @override - String yourScore(String param) { - return '您的分數:$param'; - } + String get blackCheckmatesInOneMove => '黑方一步棋將死對手'; @override - String get language => '語言'; + String get retry => '重試'; @override - String get background => '背景'; + String get reconnecting => '重新連接中'; @override - String get light => '亮'; + String get noNetwork => '離線'; @override - String get dark => '暗'; + String get favoriteOpponents => '最喜歡的對手'; @override - String get transparent => '透明度'; + String get follow => '關注'; @override - String get deviceTheme => '設備主題'; + String get following => '已關注'; @override - String get backgroundImageUrl => '背景圖片網址:'; + String get unfollow => '取消關注'; @override - String get pieceSet => '棋子外觀設定'; + String followX(String param) { + return '追蹤$param'; + } @override - String get embedInYourWebsite => '嵌入您的網站'; + String unfollowX(String param) { + return '取消追蹤$param'; + } @override - String get usernameAlreadyUsed => '此用戶名已經有人在使用,請嘗試使用別的'; + String get block => '加入黑名單'; @override - String get usernamePrefixInvalid => '使用者名稱必須以字母開頭'; + String get blocked => '已加入黑名單'; @override - String get usernameSuffixInvalid => '使用者名稱的結尾必須為字母或數字'; + String get unblock => '移除出黑名單'; @override - String get usernameCharsInvalid => '使用者名稱只能包含字母、 數字、 底線和短劃線。'; + String get followsYou => '關注您'; @override - String get usernameUnacceptable => '此使用者名稱不可用'; + String xStartedFollowingY(String param1, String param2) { + return '$param1開始關注$param2'; + } @override - String get playChessInStyle => '下棋也要穿得好看'; + String get more => '更多'; @override - String get chessBasics => '基本知識'; + String get memberSince => '註冊日期'; @override - String get coaches => '教練'; + String lastSeenActive(String param) { + return '最近登入 $param'; + } @override - String get invalidPgn => '無效的PGN'; + String get player => '棋手'; @override - String get invalidFen => '無效的FEN'; + String get list => '列表'; @override - String get custom => '自訂設定'; + String get graph => '圖表'; @override - String get notifications => '通知'; + String get required => '必填項目。'; @override - String notificationsX(String param1) { - return '通知: $param1'; - } + String get openTournaments => '公開錦標賽'; @override - String perfRatingX(String param) { - return '評分:$param'; - } + String get duration => '持續時間'; @override - String get practiceWithComputer => '和電腦練習'; + String get winner => '贏家'; @override - String anotherWasX(String param) { - return '另一個是$param'; - } + String get standing => '名次'; @override - String bestWasX(String param) { - return '最好的一步是$param'; - } + String get createANewTournament => '建立新的錦標賽'; @override - String get youBrowsedAway => '您暫停了剛剛的進度'; + String get tournamentCalendar => '錦標賽日程'; @override - String get resumePractice => '繼續練習'; + String get conditionOfEntry => '加入限制:'; @override - String get drawByFiftyMoves => '對局因 50 步規則判和。'; + String get advancedSettings => '進階設定'; @override - String get theGameIsADraw => '這是一場平局'; + String get safeTournamentName => '幫錦標賽挑選一個適合的名字'; @override - String get computerThinking => '電腦運算中...'; + String get inappropriateNameWarning => '即便只是一點點的違規都有可能導致您的帳號被封鎖。'; @override - String get seeBestMove => '觀看最佳移動'; + String get emptyTournamentName => '若不填入錦標賽的名稱,將會用一位著名的棋手名字來做為錦標賽名稱。'; @override - String get hideBestMove => '隱藏最佳移動'; + String get makePrivateTournament => '把錦標賽設定為私人,並設定密碼來限制進入。'; @override - String get getAHint => '得到提示'; + String get join => '加入'; @override - String get evaluatingYourMove => '分析您的移動'; + String get withdraw => '離開'; @override - String get whiteWinsGame => '白方獲勝'; + String get points => '分數'; @override - String get blackWinsGame => '黑方獲勝'; + String get wins => '勝'; @override - String get learnFromYourMistakes => '從您的失誤中學習'; + String get losses => '負'; @override - String get learnFromThisMistake => '從您的失誤中學習'; + String get createdBy => '創建者:'; @override - String get skipThisMove => '跳過這一步'; + String get tournamentIsStarting => '錦標賽即將開始'; @override - String get next => '下一個'; + String get tournamentPairingsAreNowClosed => '此錦標賽的對手配對已結束。'; @override - String xWasPlayed(String param) { - return '走了$param'; + String standByX(String param) { + return '$param準備好,你馬上要開始對棋了!'; } @override - String get findBetterMoveForWhite => '找出白方的最佳著法'; + String get pause => '暫停'; @override - String get findBetterMoveForBlack => '找出黑方的最佳著法'; + String get resume => '繼續'; @override - String get resumeLearning => '回復學習'; + String get youArePlaying => '等待對手中'; @override - String get youCanDoBetter => '您還可以做得更好'; + String get winRate => '勝率'; @override - String get tryAnotherMoveForWhite => '嘗試白方更好其他的著法'; + String get berserkRate => '快棋率'; @override - String get tryAnotherMoveForBlack => '嘗試黑方更好其他的著法'; + String get performance => '表現'; @override - String get solution => '解決方案'; + String get tournamentComplete => '錦標賽結束'; @override - String get waitingForAnalysis => '等待分析'; + String get movesPlayed => '步數'; @override - String get noMistakesFoundForWhite => '沒有找到白方的失誤'; + String get whiteWins => '白方獲勝'; @override - String get noMistakesFoundForBlack => '沒有找到黑方的失誤'; + String get blackWins => '黑方獲勝'; @override - String get doneReviewingWhiteMistakes => '已完成觀看白方的失誤'; + String get drawRate => '和棋率'; @override - String get doneReviewingBlackMistakes => '已完成觀看黑方的失誤'; + String get draws => '和棋'; @override - String get doItAgain => '重作一次'; + String nextXTournament(String param) { + return '下一個$param錦標賽'; + } @override - String get reviewWhiteMistakes => '複習白方失誤'; + String get averageOpponent => '平均對手評分'; @override - String get reviewBlackMistakes => '複習黑方失誤'; + String get boardEditor => '棋盤編輯器'; @override - String get advantage => '優勢'; + String get setTheBoard => '設定版型'; @override - String get opening => '開局'; + String get popularOpenings => '使用率最高的開局'; @override - String get middlegame => '中場'; + String get endgamePositions => '殘局局面'; @override - String get endgame => '殘局'; + String chess960StartPosition(String param) { + return '960棋局開局位置: $param'; + } @override - String get conditionalPremoves => '預設棋譜'; + String get startPosition => '初始佈局'; @override - String get addCurrentVariation => '加入現有變化'; + String get clearBoard => '清空棋盤'; @override - String get playVariationToCreateConditionalPremoves => '著一步不同的位置以創建預估走位'; + String get loadPosition => '載入佈局'; @override - String get noConditionalPremoves => '無預設棋譜'; + String get isPrivate => '私人'; @override - String playX(String param) { - return '移動至$param'; + String reportXToModerators(String param) { + return '將$param報告給管理人員'; } @override - String get showUnreadLichessMessage => '你收到一個來自 Lichess 的私人信息。'; + String profileCompletion(String param) { + return '個人檔案完成度:$param'; + } @override - String get clickHereToReadIt => '點擊閱讀'; + String xRating(String param) { + return '$param評分'; + } @override - String get sorry => '抱歉:('; + String get ifNoneLeaveEmpty => '如果沒有,請留空'; @override - String get weHadToTimeYouOutForAWhile => '您被封鎖了,在一陣子的時間內將不能下棋'; + String get profile => '資料'; @override - String get why => '為什麼?'; + String get editProfile => '編輯資料'; @override - String get pleasantChessExperience => '我們的目的在於為所有人提供愉快的國際象棋體驗'; + String get realName => '真實名稱'; @override - String get goodPractice => '為此,我們必須確保所有參與者都遵循良好做法'; + String get setFlair => '設置你的身分'; @override - String get potentialProblem => '當檢測到不良行為時,我們將顯示此消息'; + String get flair => '身分'; @override - String get howToAvoidThis => '如何避免這件事發生?'; + String get youCanHideFlair => '你可以在設定中隱藏使用者身分。'; @override - String get playEveryGame => '下好每一盤您加入的棋'; + String get biography => '個人簡介'; @override - String get tryToWin => '試著在每個棋局裡獲勝(或至少平手)'; + String get countryRegion => '國家或地區'; @override - String get resignLostGames => '棄權(不要讓時間耗盡)'; + String get thankYou => '謝謝!'; @override - String get temporaryInconvenience => '對於給您帶來的不便,我們深表歉意'; + String get socialMediaLinks => '官方社群連結'; @override - String get wishYouGreatGames => '並祝您在lichess.org上玩得開心。'; + String get oneUrlPerLine => '每行一個網址'; @override - String get thankYouForReading => '感謝您的閱讀!'; + String get inlineNotation => '棋譜集中顯示'; @override - String get lifetimeScore => '帳戶總分'; + String get makeAStudy => '為了安全保管和分享,考慮創建一項研討.'; @override - String get currentMatchScore => '現時的對局分數'; + String get clearSavedMoves => '清空著法儲存'; @override - String get agreementAssistance => '我同意我不會在比賽期間使用支援(從書籍、電腦運算、資料庫等等)'; + String get previouslyOnLichessTV => '過去的Lichess TV直播'; @override - String get agreementNice => '我會一直尊重其他的玩家'; + String get onlinePlayers => '在線棋手'; @override - String agreementMultipleAccounts(String param) { - return '我同意我不會開設多個帳號(除了於$param列明的原因以外)'; - } + String get activePlayers => '活躍棋手'; @override - String get agreementPolicy => '我同意我將會遵守Lichess的規則'; + String get bewareTheGameIsRatedButHasNoClock => '注意,這棋局是排位賽,但是不計時!'; @override - String get searchOrStartNewDiscussion => '尋找或開始聊天'; + String get success => '大功告成!'; @override - String get edit => '編輯'; + String get automaticallyProceedToNextGameAfterMoving => '移动棋子后自动进入下一盘棋'; @override - String get blitz => '快棋'; + String get autoSwitch => '自动更换'; @override - String get rapid => '快速模式'; + String get puzzles => '謎題'; @override - String get classical => '經典'; + String get onlineBots => '線上機器人'; @override - String get ultraBulletDesc => '瘋狂速度模式: 低於30秒'; + String get name => '名稱'; @override - String get bulletDesc => '非常速度模式:低於3分鐘'; + String get description => '描述'; @override - String get blitzDesc => '快速模式:3到8分鐘'; + String get descPrivate => '內部簡介'; @override - String get rapidDesc => '一般模式:8到25分鐘'; + String get descPrivateHelp => '僅團隊成員可見,設置後將覆蓋公開簡介為團隊成員展示。'; @override - String get classicalDesc => '經典模式:25分鐘以上'; + String get no => '否'; @override - String get correspondenceDesc => '長期模式:一天或好幾天一步'; + String get yes => '是'; @override - String get puzzleDesc => '西洋棋戰術教練'; + String get website => '網頁版'; @override - String get important => '重要'; + String get mobile => '行動裝置'; @override - String yourQuestionMayHaveBeenAnswered(String param1) { - return '您的問題可能已經有答案了$param1'; - } + String get help => '幫助:'; @override - String get inTheFAQ => '在F.A.Q裡'; + String get createANewTopic => '新話題'; @override - String toReportSomeoneForCheatingOrBadBehavior(String param1) { - return '舉報一位作弊或者是不良行為的玩家,$param1'; - } + String get topics => '話題'; @override - String get useTheReportForm => '請造訪回報頁面'; + String get posts => '貼文'; @override - String toRequestSupport(String param1) { - return '需要請求協助,$param1'; - } + String get lastPost => '最近貼文'; @override - String get tryTheContactPage => '請到協助頁面'; + String get views => '瀏覽'; @override - String makeSureToRead(String param1) { - return '確保你已閱讀 $param1'; - } + String get replies => '回覆'; @override - String get theForumEtiquette => '論壇禮儀'; + String get replyToThisTopic => '回覆此話題'; @override - String get thisTopicIsArchived => '該討論已封存,不能再留言'; + String get reply => '回覆'; @override - String joinTheTeamXToPost(String param1) { - return '請先加入$param1團隊,才能在這則討論裡發表留言'; - } + String get message => '訊息'; @override - String teamNamedX(String param1) { - return '$param1團隊'; - } + String get createTheTopic => '建立話題'; @override - String get youCannotPostYetPlaySomeGames => '您目前不能發表文章在論壇裡,先下幾盤棋吧!'; + String get reportAUser => '舉報使用者'; @override - String get subscribe => '訂閱'; + String get user => '使用者'; @override - String get unsubscribe => '取消訂閱'; + String get reason => '原因'; @override - String mentionedYouInX(String param1) { - return '在 \"$param1\" 中提到了您。'; - } + String get whatIsIheMatter => '舉報原因?'; @override - String xMentionedYouInY(String param1, String param2) { - return '$param1 在 \"$param2\" 中提到了您。'; - } + String get cheat => '作弊'; @override - String invitedYouToX(String param1) { - return '邀請您至\"$param1\"。'; - } + String get troll => '搗亂'; @override - String xInvitedYouToY(String param1, String param2) { - return '$param1 邀請您至\"$param2\"。'; - } + String get other => '其他'; @override - String get youAreNowPartOfTeam => '您現在是團隊的成員了。'; + String get reportCheatBoostHelp => '請詳細說明你舉報此使用者的具體原因並貼上遊戲連結。「他作弊」等簡短說明是不被接受的。'; @override - String youHaveJoinedTeamX(String param1) { - return '您已加入 \"$param1\"。'; - } + String get reportUsernameHelp => '請詳細說明你舉報此使用者的具體原因。若必要請解釋其名詞的歷史意義、網路用語、或是此使用者名稱如何指桑罵槐。「他的使用者名稱不妥」等簡短說明是不被接受的。'; @override - String get someoneYouReportedWasBanned => '您檢舉的玩家已被封鎖帳號'; + String get reportProcessedFasterInEnglish => '若舉報內容為英文將會更快的被處理。'; @override - String get congratsYouWon => '恭喜,您贏了!'; + String get error_provideOneCheatedGameLink => '請提供至少一局作弊棋局的連結。'; @override - String gameVsX(String param1) { - return '與$param1對局'; + String by(String param) { + return '作者:$param'; } @override - String resVsX(String param1, String param2) { - return '$param1 vs $param2'; + String importedByX(String param) { + return '由$param導入'; } @override - String get lostAgainstTOSViolator => '你輸給了違反了服務挑款的棋手'; + String get thisTopicIsNowClosed => '此話題已關閉'; @override - String refundXpointsTimeControlY(String param1, String param2) { - return '退回 $param1 $param2 等級分。'; - } + String get blog => '部落格'; @override - String get timeAlmostUp => '時間快到了!'; + String get notes => '備註'; @override - String get clickToRevealEmailAddress => '[按下展示電郵位置]'; + String get typePrivateNotesHere => '在此輸入私人備註'; @override - String get download => '下載'; + String get writeAPrivateNoteAboutThisUser => '備註用戶資訊'; @override - String get coachManager => '教練管理'; + String get noNoteYet => '尚無備註'; @override - String get streamerManager => '直播管理'; + String get invalidUsernameOrPassword => '使用者名稱或密碼錯誤'; @override - String get cancelTournament => '取消錦標賽'; + String get incorrectPassword => '舊密碼錯誤'; @override - String get tournDescription => '錦標賽敘述'; + String get invalidAuthenticationCode => '驗證碼無效'; @override - String get tournDescriptionHelp => '有甚麼特別要告訴參賽者的嗎?盡量不要太長。可以使用Markdown網址 [name](https://url)。'; + String get emailMeALink => '通過電郵發送連結給我'; @override - String get ratedFormHelp => '比賽為積分賽\n會影響到棋手的積分'; + String get currentPassword => '目前密碼'; @override - String get onlyMembersOfTeam => '只限隊員'; + String get newPassword => '新密碼'; @override - String get noRestriction => '沒有限制'; + String get newPasswordAgain => '重複新密碼'; @override - String get minimumRatedGames => '評分局遊玩次數下限'; + String get newPasswordsDontMatch => '新密碼不符合'; @override - String get minimumRating => '評分下限'; + String get newPasswordStrength => '密碼強度'; @override - String get maximumWeeklyRating => '每週最高評分'; + String get clockInitialTime => '棋鐘起始時間'; @override - String positionInputHelp(String param) { - return '將一個有效的 FEN 粘貼於此作為所有對局的起始位置。\n僅適用於標準國際象棋,對變體無效。\n你可以試用 $param 來生成 FEN,然後將其粘貼到這裡。\n置空表示以標準位置開始比賽。'; - } + String get clockIncrement => '加秒'; @override - String get cancelSimul => '取消車輪戰'; + String get privacy => '隱私'; @override - String get simulHostcolor => '主持所執方'; + String get privacyPolicy => '隱私條款'; @override - String get estimatedStart => '預計開始時間'; + String get letOtherPlayersFollowYou => '允許其他玩家關注'; @override - String simulFeatured(String param) { - return '展示在 $param'; - } + String get letOtherPlayersChallengeYou => '允許其他玩家發起挑戰'; @override - String simulFeaturedHelp(String param) { - return '在 $param 上向所有人展示您主持的車輪戰,對私人車輪戰無效。'; - } + String get letOtherPlayersInviteYouToStudy => '允許其他棋手邀請你參加研討'; @override - String get simulDescription => '車輪戰描述'; + String get sound => '音效'; @override - String get simulDescriptionHelp => '有甚麼要告訴參賽者的嗎?'; + String get none => '無'; @override - String markdownAvailable(String param) { - return '$param 可用於更高級的格式。'; - } + String get fast => '快'; @override - String get embedsAvailable => '粘貼對局URL或學習章節URL來嵌入。'; + String get normal => '普通'; @override - String get inYourLocalTimezone => '在你的時區內'; + String get slow => '慢'; @override - String get tournChat => '錦標賽聊天室'; + String get insideTheBoard => '棋盤內'; @override - String get noChat => '無聊天室'; + String get outsideTheBoard => '棋盤外'; @override - String get onlyTeamLeaders => '僅限各隊隊長'; + String get allSquaresOfTheBoard => '包括所有棋盤內的格子'; @override - String get onlyTeamMembers => '僅限各隊伍'; + String get onSlowGames => '慢棋時'; @override - String get navigateMoveTree => '定位'; + String get always => '總是'; @override - String get mouseTricks => '滑鼠功能'; + String get never => '永不'; @override - String get toggleLocalAnalysis => '切換本地計算機分析'; + String xCompetesInY(String param1, String param2) { + return '$param1在$param2參加'; + } @override - String get toggleAllAnalysis => '切換所有(本地+服務器) 的電腦分析'; + String get victory => '勝利'; @override - String get playComputerMove => '走電腦推薦的最佳著法'; + String get defeat => '戰敗'; @override - String get analysisOptions => '分析局面'; + String victoryVsYInZ(String param1, String param2, String param3) { + return '$param1在$param3模式下贏了$param2'; + } @override - String get focusChat => '聚焦聊天'; + String defeatVsYInZ(String param1, String param2, String param3) { + return '$param1在$param3模式下輸給了$param2'; + } @override - String get showHelpDialog => '顯示此說明欄'; + String drawVsYInZ(String param1, String param2, String param3) { + return '$param1在$param3模式下和$param2和棋'; + } @override - String get reopenYourAccount => '重新開啟帳戶'; + String get timeline => '時間軸'; @override - String get closedAccountChangedMind => '如果你停用了自己的帳號,但是改變了心意,你有一次的機會可以拿回帳號。'; + String get starting => '起始時間:'; @override - String get onlyWorksOnce => '這只能復原一次'; + String get allInformationIsPublicAndOptional => '所有資料為公開並且可被隱藏。'; @override - String get cantDoThisTwice => '如果你決定再次停用你的帳號,則不會有任何方式去復原。'; + String get biographyDescription => '給一個自我介紹,例如興趣或您喜愛的選手等'; @override - String get emailAssociatedToaccount => '和此帳號相關的電子信箱'; + String get listBlockedPlayers => '顯示黑名單'; @override - String get sentEmailWithLink => '我們已將網址寄送至你的信箱'; + String get human => '人類'; @override - String get tournamentEntryCode => '錦標賽參賽碼'; + String get computer => '電腦'; @override - String get hangOn => '等一下!'; + String get side => '方'; @override - String gameInProgress(String param) { - return '您正在與 $param 進行對局。'; - } + String get clock => '棋鐘'; @override - String get abortTheGame => '中止本局'; + String get opponent => '對手'; @override - String get resignTheGame => '認輸'; + String get learnMenu => '學習'; @override - String get youCantStartNewGame => '直到當下這局下完之前,你無法開始新的棋局'; + String get studyMenu => '研究'; @override - String get since => '自'; + String get practice => '練習'; @override - String get until => '直到'; + String get community => '社群'; @override - String get lichessDbExplanation => '來自 Lichess 用戶的所有評分遊戲'; + String get tools => '工具'; @override - String get switchSides => '更換所持顏色'; + String get increment => '加秒'; @override - String get closingAccountWithdrawAppeal => '關閉帳戶將會收回你的上訴'; + String get error_unknown => '無效值'; @override - String get ourEventTips => '舉辦賽事的小建議'; + String get error_required => '本項必填'; @override - String get instructions => '說明'; + String get error_email => '無效電子郵件'; @override - String get showMeEverything => '全部顯示'; + String get error_email_acceptable => '該電子郵件地址無效。請重新檢查後重試。'; @override - String get lichessPatronInfo => 'Lichess是個慈善、完全免費之開源軟件。\n一切營運成本、開發和內容皆來自用戶之捐贈。'; + String get error_email_unique => '電子郵件地址無效或已被使用'; @override - String opponentLeftCounter(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '您的對手已經離開了遊戲。您將在 $count 秒後獲勝。', - ); - return '$_temp0'; - } + String get error_email_different => '這已經是您的電子郵件地址'; @override - String mateInXHalfMoves(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '在$count步內將死對手', - ); - return '$_temp0'; + String error_minLength(String param) { + return '至少包含 $param 個字元'; } @override - String nbBlunders(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count 次漏著', - ); - return '$_temp0'; + String error_maxLength(String param) { + return '最多包含 $param 個字元'; } @override - String nbMistakes(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count 次失誤', - ); - return '$_temp0'; + String error_min(String param) { + return '最少包含 $param 個字符'; } @override - String nbInaccuracies(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count 次輕微失誤', - ); - return '$_temp0'; + String error_max(String param) { + return '最多不能超過 $param'; } @override - String nbPlayers(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count位棋手目前在線', - ); - return '$_temp0'; + String ifRatingIsPlusMinusX(String param) { + return '允許評級範圍±$param'; } @override - String nbGames(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '查看所有$count盤棋', - ); - return '$_temp0'; - } + String get ifRegistered => '已登入者'; @override - String ratingXOverYGames(int count, String param2) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$param2 場對局後的 $count 等級分', - ); - return '$_temp0'; - } + String get onlyExistingConversations => '僅目前對話'; @override - String nbBookmarks(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count個收藏', - ); - return '$_temp0'; - } + String get onlyFriends => '只允許好友'; @override - String nbDays(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count天', - ); - return '$_temp0'; - } + String get menu => '選單'; @override - String nbHours(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count小時', - ); - return '$_temp0'; - } + String get castling => '王車易位'; @override - String nbMinutes(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count 分鐘', - ); - return '$_temp0'; - } + String get whiteCastlingKingside => '白方短易位'; @override - String rankIsUpdatedEveryNbMinutes(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '評分每 $count 分鐘更新一次', - ); - return '$_temp0'; - } + String get blackCastlingKingside => '黑方短易位'; @override - String nbPuzzles(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count個題目', - ); - return '$_temp0'; + String tpTimeSpentPlaying(String param) { + return '花在下棋上的時間:$param'; } @override - String nbGamesWithYou(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '與您下過$count盤棋', - ); - return '$_temp0'; - } + String get watchGames => '觀看對局直播'; @override - String nbRated(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count 場排位賽', - ); - return '$_temp0'; + String tpTimeSpentOnTV(String param) { + return '花在Lichess TV的時間:$param'; } @override - String nbWins(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count局勝', - ); - return '$_temp0'; - } + String get watch => '觀看'; @override - String nbLosses(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count局負', - ); - return '$_temp0'; - } + String get videoLibrary => '影片庫'; @override - String nbDraws(int count) { - String _temp0 = intl.Intl.pluralLogic( + String get streamersMenu => '實況主'; + + @override + String get mobileApp => '行動應用程式'; + + @override + String get webmasters => '網站管理員'; + + @override + String get about => '關於'; + + @override + String aboutX(String param) { + return '關於 $param'; + } + + @override + String xIsAFreeYLibreOpenSourceChessServer(String param1, String param2) { + return '$param1是一個完全免費($param2)、開放性、無廣告、並且開源的網站'; + } + + @override + String get really => '真的'; + + @override + String get contribute => '協助'; + + @override + String get termsOfService => '服務條款'; + + @override + String get sourceCode => '原始碼'; + + @override + String get simultaneousExhibitions => '車輪戰'; + + @override + String get host => '主持'; + + @override + String hostColorX(String param) { + return '主持人所使用旗子顏色:$param'; + } + + @override + String get yourPendingSimuls => '正在載入比賽'; + + @override + String get createdSimuls => '觀看最近開始的車輪戰'; + + @override + String get hostANewSimul => '主持車輪戰'; + + @override + String get signUpToHostOrJoinASimul => '註冊以舉辦或參與車輪戰'; + + @override + String get noSimulFound => '找不到該車輪戰'; + + @override + String get noSimulExplanation => '此車輪戰不存在。'; + + @override + String get returnToSimulHomepage => '返回車輪戰首頁'; + + @override + String get aboutSimul => '車輪戰涉及到一個人同時和幾位棋手下棋。'; + + @override + String get aboutSimulImage => '在50位對手中,費雪贏了47局、和了2局、並輸了1局。'; + + @override + String get aboutSimulRealLife => '這種賽制來自於真實的國際賽事。 在現實中,這涉及到主持人在棋局與棋局之間來回走棋。'; + + @override + String get aboutSimulRules => '當車輪賽開始時,每個玩家都會與主持人對奕,主持人持白。當所有對局結束表示車輪賽也一併結束。'; + + @override + String get aboutSimulSettings => '車輪賽事較為非正式的賽制。重賽、悔棋、以及加時功能皆會被禁用。'; + + @override + String get create => '建立'; + + @override + String get whenCreateSimul => '當您創建車輪戰時,您要同時跟幾個棋手一起下棋。'; + + @override + String get simulVariantsHint => '如果您選擇多個變體,每個玩家可以選擇自己所好的變體。'; + + @override + String get simulClockHint => '費雪棋鐘設定。棋手越多,您所需的時間可能就越多。'; + + @override + String get simulAddExtraTime => '您可以給您的時鍾多加點時間以幫助您應對車輪戰。'; + + @override + String get simulHostExtraTime => '主持人的額外時間'; + + @override + String get simulAddExtraTimePerPlayer => '每有一個玩家加入車輪戰,您棋鐘的初始時間都將增加。'; + + @override + String get simulHostExtraTimePerPlayer => '於每位玩家加入後棋鐘增加的額外時間'; + + @override + String get lichessTournaments => 'Lichess 錦標賽'; + + @override + String get tournamentFAQ => '競技場錦標賽常見問題'; + + @override + String get timeBeforeTournamentStarts => '錦標賽準備時間'; + + @override + String get averageCentipawnLoss => '平均厘兵損失'; + + @override + String get accuracy => '精準度'; + + @override + String get keyboardShortcuts => '快捷鍵'; + + @override + String get keyMoveBackwardOrForward => '後退/前進'; + + @override + String get keyGoToStartOrEnd => '跳轉到開始/結束'; + + @override + String get keyCycleSelectedVariation => '循環已選取的變體'; + + @override + String get keyShowOrHideComments => '顯示/隱藏評論'; + + @override + String get keyEnterOrExitVariation => '進入/退出變體'; + + @override + String get keyRequestComputerAnalysis => '請求引擎分析,從你的失誤中學習'; + + @override + String get keyNextLearnFromYourMistakes => '下一個 (從你的失誤中學習)'; + + @override + String get keyNextBlunder => '下一個漏著'; + + @override + String get keyNextMistake => '下一個錯誤'; + + @override + String get keyNextInaccuracy => '下一個輕微失誤'; + + @override + String get keyPreviousBranch => '上一個分支'; + + @override + String get keyNextBranch => '下一個分支'; + + @override + String get toggleVariationArrows => '顯示變體箭頭'; + + @override + String get cyclePreviousOrNextVariation => '循環上一個/下一個變體'; + + @override + String get toggleGlyphAnnotations => '顯示圖形標註'; + + @override + String get togglePositionAnnotations => '顯示位置標註'; + + @override + String get variationArrowsInfo => '變體箭頭讓你不需棋步列表導航'; + + @override + String get playSelectedMove => '走已選取的棋步'; + + @override + String get newTournament => '新比賽'; + + @override + String get tournamentHomeTitle => '富有各種時間以及變體的西洋棋錦標賽'; + + @override + String get tournamentHomeDescription => '加入快節奏的國際象棋比賽!加入定時賽事,或創建自己的。子彈,閃電,經典,菲舍爾任意制,王到中心,三次將軍,並提供更多的選擇為無盡的國際象棋樂趣。'; + + @override + String get tournamentNotFound => '找不到該錦標賽'; + + @override + String get tournamentDoesNotExist => '這個錦標賽不存在。'; + + @override + String get tournamentMayHaveBeenCanceled => '錦標賽可能因為沒有其他玩家而取消。'; + + @override + String get returnToTournamentsHomepage => '返回錦標賽首頁'; + + @override + String weeklyPerfTypeRatingDistribution(String param) { + return '本月$param的分數分布'; + } + + @override + String yourPerfTypeRatingIsRating(String param1, String param2) { + return '您的$param1目前$param2分。'; + } + + @override + String youAreBetterThanPercentOfPerfTypePlayers(String param1, String param2) { + return '您比$param1的$param2棋手更強。'; + } + + @override + String userIsBetterThanPercentOfPerfTypePlayers(String param1, String param2, String param3) { + return '$param1比$param3之中的$param2棋手強。'; + } + + @override + String betterThanPercentPlayers(String param1, String param2) { + return '您比$param1的$param2棋手更強。'; + } + + @override + String youDoNotHaveAnEstablishedPerfTypeRating(String param) { + return '您沒有準確的$param評級。'; + } + + @override + String get yourRating => '您的評分'; + + @override + String get cumulative => '平均累積'; + + @override + String get glicko2Rating => 'Glicko-2 積分'; + + @override + String get checkYourEmail => '請檢查您的電子郵件'; + + @override + String get weHaveSentYouAnEmailClickTheLink => '我們已經發送了一封電子郵件到你的郵箱。點擊郵件中的連結以啟用帳號。'; + + @override + String get ifYouDoNotSeeTheEmailCheckOtherPlaces => '若您沒收到郵件,請檢查您的其他收件箱,例如垃圾箱、促銷、社交等。'; + + @override + String weHaveSentYouAnEmailTo(String param) { + return '我們發送了一封郵件到 $param。點擊郵件中的連結來重置您的密碼。'; + } + + @override + String byRegisteringYouAgreeToBeBoundByOur(String param) { + return '註冊帳號表示同意並且遵守 $param'; + } + + @override + String readAboutOur(String param) { + return '閱讀我們的$param'; + } + + @override + String get networkLagBetweenYouAndLichess => '您和 Lichess 之間的網路停滯'; + + @override + String get timeToProcessAMoveOnLichessServer => 'lichess 伺服器上處理走棋的時間'; + + @override + String get downloadAnnotated => '下載含有棋子走動方向的棋局'; + + @override + String get downloadRaw => '下載純文字'; + + @override + String get downloadImported => '下載導入的棋局'; + + @override + String get crosstable => '歷程表'; + + @override + String get youCanAlsoScrollOverTheBoardToMoveInTheGame => '您也可以捲動棋盤以移動。'; + + @override + String get scrollOverComputerVariationsToPreviewThem => '將鼠標移到電腦分析變種上進行預覽'; + + @override + String get analysisShapesHowTo => '按 shift 點及或右鍵棋盤上以繪製圓圈與箭頭。'; + + @override + String get letOtherPlayersMessageYou => '允許其他人發送私訊給您'; + + @override + String get receiveForumNotifications => '在論壇中被提及時接收通知'; + + @override + String get shareYourInsightsData => '顯示您的洞察數據'; + + @override + String get withNobody => '不顯示'; + + @override + String get withFriends => '好友'; + + @override + String get withEverybody => '所有人'; + + @override + String get kidMode => '兒童模式'; + + @override + String get kidModeIsEnabled => '已啟用兒童模式'; + + @override + String get kidModeExplanation => '考量安全,在兒童模式中,網站上全部的文字交流將會被關閉。開啟此模式來保護你的孩子及學生不被網路上的人傷害。'; + + @override + String inKidModeTheLichessLogoGetsIconX(String param) { + return '在兒童模式下,Lichess的標誌會有一個$param圖示,讓你知道你的孩子是安全的。'; + } + + @override + String get askYourChessTeacherAboutLiftingKidMode => '你的帳戶被管理,詢問你的老師解除兒童模式。'; + + @override + String get enableKidMode => '啟用兒童模式'; + + @override + String get disableKidMode => '停用兒童模式'; + + @override + String get security => '資訊安全相關設定'; + + @override + String get sessions => '裝置'; + + @override + String get revokeAllSessions => '登出所有裝置'; + + @override + String get playChessEverywhere => '隨處下棋!'; + + @override + String get asFreeAsLichess => '完全、永遠免費。'; + + @override + String get builtForTheLoveOfChessNotMoney => '不是為了錢,是為了西洋棋所創建。'; + + @override + String get everybodyGetsAllFeaturesForFree => '每個人都能免費使用所有功能'; + + @override + String get zeroAdvertisement => '沒有廣告'; + + @override + String get fullFeatured => '功能全面'; + + @override + String get phoneAndTablet => '手機和平板電腦'; + + @override + String get bulletBlitzClassical => '快或慢都隨你!'; + + @override + String get correspondenceChess => '通訊賽'; + + @override + String get onlineAndOfflinePlay => '線上或離線下棋'; + + @override + String get viewTheSolution => '看解答'; + + @override + String get followAndChallengeFriends => '添加好友並與他們對戰'; + + @override + String get gameAnalysis => '棋局分析研究'; + + @override + String xHostsY(String param1, String param2) { + return '$param1主持$param2'; + } + + @override + String xJoinsY(String param1, String param2) { + return '$param1加入$param2'; + } + + @override + String xLikesY(String param1, String param2) { + return '$param1對$param2按讚'; + } + + @override + String get quickPairing => '快速配對'; + + @override + String get lobby => '大廳'; + + @override + String get anonymous => '匿名用户'; + + @override + String yourScore(String param) { + return '您的分數:$param'; + } + + @override + String get language => '語言'; + + @override + String get background => '背景'; + + @override + String get light => '亮'; + + @override + String get dark => '暗'; + + @override + String get transparent => '透明度'; + + @override + String get deviceTheme => '設備主題'; + + @override + String get backgroundImageUrl => '背景圖片網址:'; + + @override + String get board => '棋盤外觀'; + + @override + String get size => '大小'; + + @override + String get opacity => '透明度'; + + @override + String get brightness => '亮度'; + + @override + String get hue => '色調'; + + @override + String get boardReset => '回復預設顏色設定'; + + @override + String get pieceSet => '棋子外觀設定'; + + @override + String get embedInYourWebsite => '嵌入您的網站'; + + @override + String get usernameAlreadyUsed => '該使用者名稱已被使用,請換一個試試!'; + + @override + String get usernamePrefixInvalid => '使用者名稱必須以字母開頭'; + + @override + String get usernameSuffixInvalid => '使用者名稱的結尾必須為字母或數字'; + + @override + String get usernameCharsInvalid => '使用者名稱只能包含字母、 數字、 底線和短劃線。'; + + @override + String get usernameUnacceptable => '無法套用此使用者名稱'; + + @override + String get playChessInStyle => '下棋也要穿得好看'; + + @override + String get chessBasics => '基本常識'; + + @override + String get coaches => '教練'; + + @override + String get invalidPgn => '無效的 PGN'; + + @override + String get invalidFen => '無效的 FEN'; + + @override + String get custom => '自訂設定'; + + @override + String get notifications => '通知'; + + @override + String notificationsX(String param1) { + return '通知:$param1'; + } + + @override + String perfRatingX(String param) { + return '評分:$param'; + } + + @override + String get practiceWithComputer => '和電腦練習'; + + @override + String anotherWasX(String param) { + return '另一個是$param'; + } + + @override + String bestWasX(String param) { + return '最好的一步是$param'; + } + + @override + String get youBrowsedAway => '您暫停了剛剛的進度'; + + @override + String get resumePractice => '繼續練習'; + + @override + String get drawByFiftyMoves => '對局因 50 步規則判和。'; + + @override + String get theGameIsADraw => '這是一場平局'; + + @override + String get computerThinking => '電腦運算中...'; + + @override + String get seeBestMove => '觀看最佳移動'; + + @override + String get hideBestMove => '隱藏最佳移動'; + + @override + String get getAHint => '得到提示'; + + @override + String get evaluatingYourMove => '分析您的移動'; + + @override + String get whiteWinsGame => '白方獲勝'; + + @override + String get blackWinsGame => '黑方獲勝'; + + @override + String get learnFromYourMistakes => '從您的失誤中學習'; + + @override + String get learnFromThisMistake => '從您的失誤中學習'; + + @override + String get skipThisMove => '跳過這一步'; + + @override + String get next => '下一個'; + + @override + String xWasPlayed(String param) { + return '走了$param'; + } + + @override + String get findBetterMoveForWhite => '找出白方的最佳著法'; + + @override + String get findBetterMoveForBlack => '找出黑方的最佳著法'; + + @override + String get resumeLearning => '繼續學習'; + + @override + String get youCanDoBetter => '還有更好的移動'; + + @override + String get tryAnotherMoveForWhite => '嘗試白方更好其他的著法'; + + @override + String get tryAnotherMoveForBlack => '嘗試黑方更好其他的著法'; + + @override + String get solution => '解決方案'; + + @override + String get waitingForAnalysis => '等待分析'; + + @override + String get noMistakesFoundForWhite => '沒有找到白方的失誤'; + + @override + String get noMistakesFoundForBlack => '沒有找到黑方的失誤'; + + @override + String get doneReviewingWhiteMistakes => '已完成觀看白方的失誤'; + + @override + String get doneReviewingBlackMistakes => '已完成觀看黑方的失誤'; + + @override + String get doItAgain => '再試一次'; + + @override + String get reviewWhiteMistakes => '複習白方失誤'; + + @override + String get reviewBlackMistakes => '複習黑方失誤'; + + @override + String get advantage => '優勢'; + + @override + String get opening => '開局'; + + @override + String get middlegame => '中場'; + + @override + String get endgame => '殘局'; + + @override + String get conditionalPremoves => '預設棋譜'; + + @override + String get addCurrentVariation => '加入現有變種'; + + @override + String get playVariationToCreateConditionalPremoves => '走一種變種以建立棋譜'; + + @override + String get noConditionalPremoves => '無預設棋譜'; + + @override + String playX(String param) { + return '移動至$param'; + } + + @override + String get showUnreadLichessMessage => '你收到一個來自 Lichess 的私訊。'; + + @override + String get clickHereToReadIt => '點擊以閱讀'; + + @override + String get sorry => '抱歉:('; + + @override + String get weHadToTimeYouOutForAWhile => '我們必須將您暫時封鎖'; + + @override + String get why => '為什麼?'; + + @override + String get pleasantChessExperience => '我們的目的在於維持良好的下棋環境'; + + @override + String get goodPractice => '為此,我們必須確保所有參與者都遵循良好做法'; + + @override + String get potentialProblem => '當檢測到不良行為時,我們將顯示此消息'; + + @override + String get howToAvoidThis => '如何避免這件事發生?'; + + @override + String get playEveryGame => '避免在棋局中任意退出'; + + @override + String get tryToWin => '試著在每個棋局裡獲勝(或至少平手)'; + + @override + String get resignLostGames => '投降(不要讓時間耗盡)'; + + @override + String get temporaryInconvenience => '我們對於給您帶來的不便深表歉意'; + + @override + String get wishYouGreatGames => '並祝您在 lichess.org 上玩得開心。'; + + @override + String get thankYouForReading => '感謝您的閱讀!'; + + @override + String get lifetimeScore => '帳戶總分'; + + @override + String get currentMatchScore => '現時的對局分數'; + + @override + String get agreementAssistance => '我同意我不會在比賽期間使用支援(從書籍、電腦運算、資料庫等等)'; + + @override + String get agreementNice => '我會一直尊重其他的玩家'; + + @override + String agreementMultipleAccounts(String param) { + return '我同意我不會開設多個帳號(除了於$param列明的原因以外)'; + } + + @override + String get agreementPolicy => '我同意我將會遵守Lichess的規則'; + + @override + String get searchOrStartNewDiscussion => '尋找或開始聊天'; + + @override + String get edit => '編輯'; + + @override + String get bullet => 'Bullet'; + + @override + String get blitz => 'Blitz'; + + @override + String get rapid => '快速模式'; + + @override + String get classical => '經典'; + + @override + String get ultraBulletDesc => '瘋狂速度模式:低於30秒'; + + @override + String get bulletDesc => '極快速模式:低於3分鐘'; + + @override + String get blitzDesc => '快速模式:3到8分鐘'; + + @override + String get rapidDesc => '一般模式:8到25分鐘'; + + @override + String get classicalDesc => '經典模式:25分鐘以上'; + + @override + String get correspondenceDesc => '通信模式:一天或好幾天一步'; + + @override + String get puzzleDesc => '西洋棋戰術教練'; + + @override + String get important => '重要'; + + @override + String yourQuestionMayHaveBeenAnswered(String param1) { + return '您的問題可能已經有答案了$param1'; + } + + @override + String get inTheFAQ => '在常見問答內'; + + @override + String toReportSomeoneForCheatingOrBadBehavior(String param1) { + return '舉報一位作弊或違反善良風俗的玩家,$param1'; + } + + @override + String get useTheReportForm => '請填寫回報表單'; + + @override + String toRequestSupport(String param1) { + return '$param1以獲取協助'; + } + + @override + String get tryTheContactPage => '請到協助頁面'; + + @override + String makeSureToRead(String param1) { + return '確保你已閱讀 $param1'; + } + + @override + String get theForumEtiquette => '論壇禮儀'; + + @override + String get thisTopicIsArchived => '該討論已封存,不能再留言'; + + @override + String joinTheTeamXToPost(String param1) { + return '請先加入$param1團隊,才能在這則討論裡發表留言'; + } + + @override + String teamNamedX(String param1) { + return '$param1團隊'; + } + + @override + String get youCannotPostYetPlaySomeGames => '您目前不能發表文章在論壇裡,先下幾盤棋吧!'; + + @override + String get subscribe => '訂閱'; + + @override + String get unsubscribe => '取消訂閱'; + + @override + String mentionedYouInX(String param1) { + return '在「$param1」中提到了您。'; + } + + @override + String xMentionedYouInY(String param1, String param2) { + return '$param1 在「$param2」中提到了您。'; + } + + @override + String invitedYouToX(String param1) { + return '邀請您至「$param1」。'; + } + + @override + String xInvitedYouToY(String param1, String param2) { + return '$param1 邀請您至「$param2」。'; + } + + @override + String get youAreNowPartOfTeam => '您現在是團隊的成員了。'; + + @override + String youHaveJoinedTeamX(String param1) { + return '您已加入「$param1」。'; + } + + @override + String get someoneYouReportedWasBanned => '您檢舉的玩家已被封鎖帳號'; + + @override + String get congratsYouWon => '恭喜,您贏了!'; + + @override + String gameVsX(String param1) { + return '與$param1對局'; + } + + @override + String resVsX(String param1, String param2) { + return '$param1 vs $param2'; + } + + @override + String get lostAgainstTOSViolator => '你輸給了違反了服務條款的棋手'; + + @override + String refundXpointsTimeControlY(String param1, String param2) { + return '退回 $param1 $param2 等級分。'; + } + + @override + String get timeAlmostUp => '時間快到了!'; + + @override + String get clickToRevealEmailAddress => '[點擊以顯示電子郵件]'; + + @override + String get download => '下載'; + + @override + String get coachManager => '教練管理'; + + @override + String get streamerManager => '直播管理'; + + @override + String get cancelTournament => '取消錦標賽'; + + @override + String get tournDescription => '錦標賽敘述'; + + @override + String get tournDescriptionHelp => '有甚麼特別要告訴參賽者的嗎?盡量不要太長。可以使用 Markdown 網址 [name](https://url)。'; + + @override + String get ratedFormHelp => '比賽為積分賽\n會影響到棋手的積分'; + + @override + String get onlyMembersOfTeam => '只限隊員'; + + @override + String get noRestriction => '沒有限制'; + + @override + String get minimumRatedGames => '評分局遊玩次數下限'; + + @override + String get minimumRating => '評分下限'; + + @override + String get maximumWeeklyRating => '每週最高評分'; + + @override + String positionInputHelp(String param) { + return '將一個有效的 FEN 貼上於此作為所有對局的起始位置。\n僅適用於標準西洋棋,對變種無效。\n你可以試用 $param 來生成 FEN,然後將其貼上到這裡。\n置空表示以預設位置開始比賽。'; + } + + @override + String get cancelSimul => '取消車輪戰'; + + @override + String get simulHostcolor => '主持所執方'; + + @override + String get estimatedStart => '預計開始時間'; + + @override + String simulFeatured(String param) { + return '展示在 $param'; + } + + @override + String simulFeaturedHelp(String param) { + return '在 $param 上向所有人展示您主持的車輪戰,對私人車輪戰無效。'; + } + + @override + String get simulDescription => '車輪戰描述'; + + @override + String get simulDescriptionHelp => '有甚麼要告訴參賽者的嗎?'; + + @override + String markdownAvailable(String param) { + return '$param 可用於更高級的格式。'; + } + + @override + String get embedsAvailable => '貼上對局或學習章節網址來嵌入。'; + + @override + String get inYourLocalTimezone => '在您的時區內'; + + @override + String get tournChat => '錦標賽聊天室'; + + @override + String get noChat => '無聊天室'; + + @override + String get onlyTeamLeaders => '僅限各隊隊長'; + + @override + String get onlyTeamMembers => '僅限各隊伍'; + + @override + String get navigateMoveTree => '定位'; + + @override + String get mouseTricks => '滑鼠功能'; + + @override + String get toggleLocalAnalysis => '切換本地計算機分析'; + + @override + String get toggleAllAnalysis => '切換所有(本地+服務器) 的電腦分析'; + + @override + String get playComputerMove => '走電腦推薦的最佳著法'; + + @override + String get analysisOptions => '分析局面'; + + @override + String get focusChat => '聚焦聊天'; + + @override + String get showHelpDialog => '顯示此說明欄'; + + @override + String get reopenYourAccount => '重新開啟帳戶'; + + @override + String get closedAccountChangedMind => '如果你停用了自己的帳號,但是改變了心意,你有一次的機會可以拿回帳號。'; + + @override + String get onlyWorksOnce => '這只能復原一次。'; + + @override + String get cantDoThisTwice => '如果你決定再次停用你的帳號,則不會有任何方式去復原。'; + + @override + String get emailAssociatedToaccount => '和此帳號相關的電子信箱'; + + @override + String get sentEmailWithLink => '我們已將網址寄送至你的信箱'; + + @override + String get tournamentEntryCode => '錦標賽參賽碼'; + + @override + String get hangOn => '等一下!'; + + @override + String gameInProgress(String param) { + return '您正在與 $param 進行對局。'; + } + + @override + String get abortTheGame => '中止本局'; + + @override + String get resignTheGame => '認輸'; + + @override + String get youCantStartNewGame => '直到當下這局下完之前,你無法開始新的棋局'; + + @override + String get since => '自'; + + @override + String get until => '直到'; + + @override + String get lichessDbExplanation => '來自 Lichess 用戶的所有評分遊戲'; + + @override + String get switchSides => '更換所持顏色'; + + @override + String get closingAccountWithdrawAppeal => '關閉帳戶將會收回你的上訴'; + + @override + String get ourEventTips => '舉辦賽事的小建議'; + + @override + String get instructions => '說明'; + + @override + String get showMeEverything => '全部顯示'; + + @override + String get lichessPatronInfo => 'Lichess是個慈善、完全免費且開源的軟體。\n一切營運成本、開發和內容皆來自用戶之捐贈。'; + + @override + String get nothingToSeeHere => '目前這裡沒有什麼好看的。'; + + @override + String get stats => '統計'; + + @override + String opponentLeftCounter(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '您的對手已經離開了遊戲。您將在 $count 秒後獲勝。', + ); + return '$_temp0'; + } + + @override + String mateInXHalfMoves(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '在$count步內將死對手', + ); + return '$_temp0'; + } + + @override + String nbBlunders(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 次漏著', + ); + return '$_temp0'; + } + + @override + String nbMistakes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 次失誤', + ); + return '$_temp0'; + } + + @override + String nbInaccuracies(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 次輕微失誤', + ); + return '$_temp0'; + } + + @override + String nbPlayers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count位棋手目前在線', + ); + return '$_temp0'; + } + + @override + String nbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '查看所有$count盤棋', + ); + return '$_temp0'; + } + + @override + String ratingXOverYGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$param2 場對局後的 $count 等級分', + ); + return '$_temp0'; + } + + @override + String nbBookmarks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count個收藏', + ); + return '$_temp0'; + } + + @override + String nbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count天', + ); + return '$_temp0'; + } + + @override + String nbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count小時', + ); + return '$_temp0'; + } + + @override + String nbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 分鐘', + ); + return '$_temp0'; + } + + @override + String rankIsUpdatedEveryNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '評分每 $count 分鐘更新一次', + ); + return '$_temp0'; + } + + @override + String nbPuzzles(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count個題目', + ); + return '$_temp0'; + } + + @override + String nbGamesWithYou(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '與您下過$count盤棋', + ); + return '$_temp0'; + } + + @override + String nbRated(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 場排位賽', + ); + return '$_temp0'; + } + + @override + String nbWins(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count局勝', + ); + return '$_temp0'; + } + + @override + String nbLosses(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count局負', + ); + return '$_temp0'; + } + + @override + String nbDraws(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count局和', + ); + return '$_temp0'; + } + + @override + String nbPlaying(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count下棋中', + ); + return '$_temp0'; + } + + @override + String giveNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '給對方加$count秒', + ); + return '$_temp0'; + } + + @override + String nbTournamentPoints(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count錦標賽得分', + ); + return '$_temp0'; + } + + @override + String nbStudies(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count個研究', + ); + return '$_temp0'; + } + + @override + String nbSimuls(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 個進行的車輪戰棋局', + ); + return '$_temp0'; + } + + @override + String moreThanNbRatedGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '完成至少 $count 場排位賽', + ); + return '$_temp0'; + } + + @override + String moreThanNbPerfRatedGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '≥ $count $param2 排位賽', + ); + return '$_temp0'; + } + + @override + String needNbMorePerfGames(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '你需要再下 $count 局 $param2 變體的排位賽', + ); + return '$_temp0'; + } + + @override + String needNbMoreGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '您需要再完成 $count 場排位賽', + ); + return '$_temp0'; + } + + @override + String nbImportedGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '已導入$count盤棋局', + ); + return '$_temp0'; + } + + @override + String nbFriendsOnline(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 位好友在線', + ); + return '$_temp0'; + } + + @override + String nbFollowers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count個關注者', + ); + return '$_temp0'; + } + + @override + String nbFollowing(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '關注$count人', + ); + return '$_temp0'; + } + + @override + String lessThanNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '小於$count分鐘', + ); + return '$_temp0'; + } + + @override + String nbGamesInPlay(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count場對局正在進行中', + ); + return '$_temp0'; + } + + @override + String maximumNbCharacters(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '最多包含 $count 個字符', + ); + return '$_temp0'; + } + + @override + String blocks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count位黑名單使用者', + ); + return '$_temp0'; + } + + @override + String nbForumPosts(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count個論壇貼文', + ); + return '$_temp0'; + } + + @override + String nbPerfTypePlayersThisWeek(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '本周$count位棋手下了$param2模式的棋局', + ); + return '$_temp0'; + } + + @override + String availableInNbLanguages(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '支援$count種語言!', + ); + return '$_temp0'; + } + + @override + String nbSecondsToPlayTheFirstMove(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '在$count秒前須下出第一步', + ); + return '$_temp0'; + } + + @override + String nbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 秒', + ); + return '$_temp0'; + } + + @override + String andSaveNbPremoveLines(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '以省略$count個預走的棋步', + ); + return '$_temp0'; + } + + @override + String get stormMoveToStart => '移動以開始'; + + @override + String get stormYouPlayTheWhitePiecesInAllPuzzles => '您將在所有謎題中執白'; + + @override + String get stormYouPlayTheBlackPiecesInAllPuzzles => '您將在所有謎題中執黑'; + + @override + String get stormPuzzlesSolved => '已解決題目!'; + + @override + String get stormNewDailyHighscore => '新的每日紀錄!'; + + @override + String get stormNewWeeklyHighscore => '新的每周紀錄!'; + + @override + String get stormNewMonthlyHighscore => '新的每月紀錄!'; + + @override + String get stormNewAllTimeHighscore => '新歷史紀錄!'; + + @override + String stormPreviousHighscoreWasX(String param) { + return '之前的最高紀錄:$param'; + } + + @override + String get stormPlayAgain => '再玩一次'; + + @override + String stormHighscoreX(String param) { + return '最高紀錄:$param'; + } + + @override + String get stormScore => '得分'; + + @override + String get stormMoves => '走棋'; + + @override + String get stormAccuracy => '精準度'; + + @override + String get stormCombo => '連擊'; + + @override + String get stormTime => '時間'; + + @override + String get stormTimePerMove => '平均走棋時間'; + + @override + String get stormHighestSolved => '最難解決的題目'; + + @override + String get stormPuzzlesPlayed => '解決過的題目'; + + @override + String get stormNewRun => '新的一輪 (快捷鍵:空白鍵)'; + + @override + String get stormEndRun => '結束此輪 (快捷鍵:Enter 鍵)'; + + @override + String get stormHighscores => '最高紀錄'; + + @override + String get stormViewBestRuns => '顯示最佳的一輪'; + + @override + String get stormBestRunOfDay => '今日最佳的一輪'; + + @override + String get stormRuns => '輪'; + + @override + String get stormGetReady => '做好準備!'; + + @override + String get stormWaitingForMorePlayers => '等待更多玩家加入...'; + + @override + String get stormRaceComplete => '完賽!'; + + @override + String get stormSpectating => '觀戰中'; + + @override + String get stormJoinTheRace => '加入競賽!'; + + @override + String get stormStartTheRace => '開始比賽'; + + @override + String stormYourRankX(String param) { + return '你的排名: $param'; + } + + @override + String get stormWaitForRematch => '等候重賽'; + + @override + String get stormNextRace => '下一場競賽'; + + @override + String get stormJoinRematch => '再來一局'; + + @override + String get stormWaitingToStart => '等待開始'; + + @override + String get stormCreateNewGame => '開始新遊戲'; + + @override + String get stormJoinPublicRace => '加入公開比賽'; + + @override + String get stormRaceYourFriends => '和好友比賽'; + + @override + String get stormSkip => '跳過'; + + @override + String get stormSkipHelp => '每場賽可略一手棋'; + + @override + String get stormSkipExplanation => '跳過這一步來維持您的連擊紀錄!每次遊玩只能使用一次。'; + + @override + String get stormFailedPuzzles => '失敗的謎題'; + + @override + String get stormSlowPuzzles => '耗時謎題'; + + @override + String get stormSkippedPuzzle => '已跳過的謎題'; + + @override + String get stormThisWeek => '本星期'; + + @override + String get stormThisMonth => '本月'; + + @override + String get stormAllTime => '總計'; + + @override + String get stormClickToReload => '點擊以重新加載'; + + @override + String get stormThisRunHasExpired => '本輪已過期!'; + + @override + String get stormThisRunWasOpenedInAnotherTab => '本輪已經在另一個分頁中打開!'; + + @override + String stormXRuns(int count) { + String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count局和', + other: '$count輪', + ); + return '$_temp0'; + } + + @override + String stormPlayedNbRunsOfPuzzleStorm(int count, String param2) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '玩了$count輪的$param2', ); return '$_temp0'; } @override - String nbPlaying(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count下棋中', - ); - return '$_temp0'; - } + String get streamerLichessStreamers => 'Lichess 實況主'; + + @override + String get studyPrivate => '私人的'; + + @override + String get studyMyStudies => '我的研究'; + + @override + String get studyStudiesIContributeTo => '我有貢獻的研究'; + + @override + String get studyMyPublicStudies => '我的公開研究'; + + @override + String get studyMyPrivateStudies => '我的私人研究'; + + @override + String get studyMyFavoriteStudies => '我最愛的研究'; + + @override + String get studyWhatAreStudies => '研究是什麼?'; + + @override + String get studyAllStudies => '所有研究'; + + @override + String studyStudiesCreatedByX(String param) { + return '$param創建的研究'; + } + + @override + String get studyNoneYet => '暫時沒有...'; + + @override + String get studyHot => '熱門的'; + + @override + String get studyDateAddedNewest => '新增日期(由新到舊)'; + + @override + String get studyDateAddedOldest => '新增日期(由舊到新)'; + + @override + String get studyRecentlyUpdated => '最近更新'; + + @override + String get studyMostPopular => '最受歡迎'; + + @override + String get studyAlphabetical => '按字母順序'; + + @override + String get studyAddNewChapter => '加入新章節'; + + @override + String get studyAddMembers => '新增成員'; + + @override + String get studyInviteToTheStudy => '邀請加入研究'; + + @override + String get studyPleaseOnlyInvitePeopleYouKnow => '只邀請你所認識的人,以及願意積極投入的人來共同研究'; + + @override + String get studySearchByUsername => '透過使用者名稱搜尋'; + + @override + String get studySpectator => '觀眾'; + + @override + String get studyContributor => '共同研究者'; + + @override + String get studyKick => '踢出'; + + @override + String get studyLeaveTheStudy => '退出研究'; + + @override + String get studyYouAreNowAContributor => '你現在是一位研究者了'; + + @override + String get studyYouAreNowASpectator => '你現在是觀眾'; + + @override + String get studyPgnTags => 'PGN 標籤'; + + @override + String get studyLike => '喜歡'; + + @override + String get studyUnlike => '取消喜歡'; + + @override + String get studyNewTag => '新標籤'; + + @override + String get studyCommentThisPosition => '對於目前局面的評論'; + + @override + String get studyCommentThisMove => '對於此棋步的評論'; + + @override + String get studyAnnotateWithGlyphs => '以圖形標註'; + + @override + String get studyTheChapterIsTooShortToBeAnalysed => '因為太短,所以此章節無法被分析'; + + @override + String get studyOnlyContributorsCanRequestAnalysis => '只有研究專案編輯者才能要求電腦分析'; + + @override + String get studyGetAFullComputerAnalysis => '請求伺服器完整的分析主要走法'; + + @override + String get studyMakeSureTheChapterIsComplete => '確認此章節已完成,您只能要求分析一次'; + + @override + String get studyAllSyncMembersRemainOnTheSamePosition => '所有的SYNC成員處於相同局面'; + + @override + String get studyShareChanges => '向旁觀者分享這些變動並將其保留在伺服器中'; + + @override + String get studyPlaying => '下棋中'; + + @override + String get studyShowEvalBar => '評估條'; + + @override + String get studyFirst => '第一頁'; + + @override + String get studyPrevious => '上一頁'; + + @override + String get studyNext => '下一頁'; + + @override + String get studyLast => '最後一頁'; + + @override + String get studyShareAndExport => '分享 & 導出'; + + @override + String get studyCloneStudy => '複製'; + + @override + String get studyStudyPgn => '研究 PGN'; + + @override + String get studyDownloadAllGames => '下載所有棋局'; + + @override + String get studyChapterPgn => '章節PGN'; + + @override + String get studyCopyChapterPgn => '複製PGN'; + + @override + String get studyDownloadGame => '下載棋局'; + + @override + String get studyStudyUrl => '研究連結'; + + @override + String get studyCurrentChapterUrl => '目前章節連結'; + + @override + String get studyYouCanPasteThisInTheForumToEmbed => '您可以將此複製到論壇以嵌入'; + + @override + String get studyStartAtInitialPosition => '從起始局面開始'; + + @override + String studyStartAtX(String param) { + return '從$param開始'; + } + + @override + String get studyEmbedInYourWebsite => '嵌入到您的網站或部落格'; + + @override + String get studyReadMoreAboutEmbedding => '閱讀更多與嵌入有關的內容'; + + @override + String get studyOnlyPublicStudiesCanBeEmbedded => '只有公開的研究可以嵌入!'; + + @override + String get studyOpen => '打開'; + + @override + String studyXBroughtToYouByY(String param1, String param2) { + return '$param1,由$param2提供'; + } + + @override + String get studyStudyNotFound => '找無此研究'; + + @override + String get studyEditChapter => '編輯章節'; + + @override + String get studyNewChapter => '建立新章節'; + + @override + String studyImportFromChapterX(String param) { + return '從 $param 導入'; + } + + @override + String get studyOrientation => '視角'; + + @override + String get studyAnalysisMode => '分析模式'; + + @override + String get studyPinnedChapterComment => '置頂留言'; + + @override + String get studySaveChapter => '儲存章節'; + + @override + String get studyClearAnnotations => '清除註記'; + + @override + String get studyClearVariations => '清除變化'; + + @override + String get studyDeleteChapter => '刪除章節'; + + @override + String get studyDeleteThisChapter => '刪除此章節? 此動作將無法取消!'; + + @override + String get studyClearAllCommentsInThisChapter => '清除此章節中的所有註釋和圖形嗎?'; @override - String giveNbSeconds(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '給對方加$count秒', - ); - return '$_temp0'; - } + String get studyRightUnderTheBoard => '棋盤下方'; @override - String nbTournamentPoints(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count錦標賽得分', - ); - return '$_temp0'; - } + String get studyNoPinnedComment => '無'; @override - String nbStudies(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count個研究', - ); - return '$_temp0'; - } + String get studyNormalAnalysis => '一般分析'; @override - String nbSimuls(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count 個進行的車輪戰棋局', - ); - return '$_temp0'; - } + String get studyHideNextMoves => '隱藏下一步'; @override - String moreThanNbRatedGames(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '完成至少 $count 場排位賽', - ); - return '$_temp0'; - } + String get studyInteractiveLesson => '互動課程'; @override - String moreThanNbPerfRatedGames(int count, String param2) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '≥ $count $param2 排位賽', - ); - return '$_temp0'; + String studyChapterX(String param) { + return '章節$param'; } @override - String needNbMorePerfGames(int count, String param2) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '你需要再下 $count 局 $param2 變體的排位賽', - ); - return '$_temp0'; - } + String get studyEmpty => '空的'; @override - String needNbMoreGames(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '您需要再完成 $count 場排位賽', - ); - return '$_temp0'; - } + String get studyStartFromInitialPosition => '從起始局面開始'; @override - String nbImportedGames(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '已導入$count盤棋局', - ); - return '$_temp0'; - } + String get studyEditor => '編輯器'; @override - String nbFriendsOnline(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count 位好友在線', - ); - return '$_temp0'; - } + String get studyStartFromCustomPosition => '從自定的局面開始'; @override - String nbFollowers(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count個關注者', - ); - return '$_temp0'; - } + String get studyLoadAGameByUrl => '以連結導入棋局'; @override - String nbFollowing(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '關注$count人', - ); - return '$_temp0'; - } + String get studyLoadAPositionFromFen => '透過FEN讀取局面'; @override - String lessThanNbMinutes(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '小於$count分鐘', - ); - return '$_temp0'; - } + String get studyLoadAGameFromPgn => '以PGN文件導入棋局'; @override - String nbGamesInPlay(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count場對局正在進行中', - ); - return '$_temp0'; - } + String get studyAutomatic => '自動'; @override - String maximumNbCharacters(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '最多$count个字符', - ); - return '$_temp0'; - } + String get studyUrlOfTheGame => '棋局連結,一行一個'; @override - String blocks(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count位黑名单用户', - ); - return '$_temp0'; + String studyLoadAGameFromXOrY(String param1, String param2) { + return '從$param1或$param2載入棋局'; } @override - String nbForumPosts(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count個論壇貼文', - ); - return '$_temp0'; - } + String get studyCreateChapter => '建立章節'; @override - String nbPerfTypePlayersThisWeek(int count, String param2) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '本周$count位棋手下了$param2模式的棋局', - ); - return '$_temp0'; - } + String get studyCreateStudy => '建立研究'; @override - String availableInNbLanguages(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '支援$count種語言!', - ); - return '$_temp0'; - } + String get studyEditStudy => '編輯此研究'; @override - String nbSecondsToPlayTheFirstMove(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '在$count秒前須下出第一步', - ); - return '$_temp0'; - } + String get studyVisibility => '權限'; @override - String nbSeconds(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '$count 秒', - ); - return '$_temp0'; - } + String get studyPublic => '公開的'; @override - String andSaveNbPremoveLines(int count) { - String _temp0 = intl.Intl.pluralLogic( - count, - locale: localeName, - other: '以儲存$count列預走的棋步', - ); - return '$_temp0'; - } + String get studyUnlisted => '不公開'; @override - String get stormMoveToStart => '移動以開始'; + String get studyInviteOnly => '僅限邀請'; @override - String get stormYouPlayTheWhitePiecesInAllPuzzles => '您將在所有謎題中執白'; + String get studyAllowCloning => '可以複製'; @override - String get stormYouPlayTheBlackPiecesInAllPuzzles => '您將在所有謎題中執黑'; + String get studyNobody => '没有人'; @override - String get stormPuzzlesSolved => '已解決題目!'; + String get studyOnlyMe => '僅自己'; @override - String get stormNewDailyHighscore => '新的每日紀錄!'; + String get studyContributors => '貢獻者'; @override - String get stormNewWeeklyHighscore => '新的每周紀錄!'; + String get studyMembers => '成員'; @override - String get stormNewMonthlyHighscore => '新的每月紀錄!'; + String get studyEveryone => '所有人'; @override - String get stormNewAllTimeHighscore => '新歷史紀錄!'; + String get studyEnableSync => '允許同步'; @override - String stormPreviousHighscoreWasX(String param) { - return '之前的紀錄:$param'; - } + String get studyYesKeepEveryoneOnTheSamePosition => '同步:讓所有人停留在同一個局面'; @override - String get stormPlayAgain => '再玩一次'; + String get studyNoLetPeopleBrowseFreely => '不同步:允許所有人自由進行瀏覽'; @override - String stormHighscoreX(String param) { - return '最高紀錄:$param'; - } + String get studyPinnedStudyComment => '置頂研究留言'; @override - String get stormScore => '得分'; + String get studyStart => '開始'; @override - String get stormMoves => '走棋'; + String get studySave => '存檔'; @override - String get stormAccuracy => '精準度'; + String get studyClearChat => '清空對話紀錄'; @override - String get stormCombo => '連擊'; + String get studyDeleteTheStudyChatHistory => '確定要清空課程對話紀錄嗎?此操作無法還原!'; @override - String get stormTime => '時間'; + String get studyDeleteStudy => '刪除此研究'; @override - String get stormTimePerMove => '平均走棋時間'; + String studyConfirmDeleteStudy(String param) { + return '你確定要刪除整個研究?此動作無法反悔。輸入研究名稱確認:$param'; + } @override - String get stormHighestSolved => '最難解決的題目'; + String get studyWhereDoYouWantToStudyThat => '要從哪裡開始研究呢?'; @override - String get stormPuzzlesPlayed => '解決過的題目'; + String get studyGoodMove => '好棋'; @override - String get stormNewRun => '新的一輪 (快捷鍵:空白鍵)'; + String get studyMistake => '失誤'; @override - String get stormEndRun => '結束此輪 (快捷鍵:Enter鍵)'; + String get studyBrilliantMove => '妙着'; @override - String get stormHighscores => '最高紀錄'; + String get studyBlunder => '嚴重失誤'; @override - String get stormViewBestRuns => '顯示最佳的一輪'; + String get studyInterestingMove => '有趣的一着'; @override - String get stormBestRunOfDay => '今日最佳的一輪'; + String get studyDubiousMove => '值得商榷的一着'; @override - String get stormRuns => '輪'; + String get studyOnlyMove => '唯一著法'; @override - String get stormGetReady => '做好準備!'; + String get studyZugzwang => '等著'; @override - String get stormWaitingForMorePlayers => '等待更多玩家加入...'; + String get studyEqualPosition => '勢均力敵'; @override - String get stormRaceComplete => '完賽!'; + String get studyUnclearPosition => '局勢不明'; @override - String get stormSpectating => '觀戰中'; + String get studyWhiteIsSlightlyBetter => '白方稍占優勢'; @override - String get stormJoinTheRace => '加入競賽!'; + String get studyBlackIsSlightlyBetter => '黑方稍占優勢'; @override - String get stormStartTheRace => '開始比賽'; + String get studyWhiteIsBetter => '白方占優勢'; @override - String stormYourRankX(String param) { - return '你的排名: $param'; - } + String get studyBlackIsBetter => '黑方占優勢'; @override - String get stormWaitForRematch => '等候重賽'; + String get studyWhiteIsWinning => '白方要取得勝利了'; @override - String get stormNextRace => '下一場競賽'; + String get studyBlackIsWinning => '黑方要取得勝利了'; @override - String get stormJoinRematch => '再來一局'; + String get studyNovelty => '新奇的'; @override - String get stormWaitingToStart => '等待開始'; + String get studyDevelopment => '發展'; @override - String get stormCreateNewGame => '開始新遊戲'; + String get studyInitiative => '佔據主動'; @override - String get stormJoinPublicRace => '加入公開比賽'; + String get studyAttack => '攻擊'; @override - String get stormRaceYourFriends => '和好友比賽'; + String get studyCounterplay => '反擊'; @override - String get stormSkip => '跳過'; + String get studyTimeTrouble => '時間壓力'; @override - String get stormSkipHelp => '每場賽可略一手棋'; + String get studyWithCompensation => '優勢補償'; @override - String get stormSkipExplanation => '跳過這一步來維持您的連擊紀錄!每次遊玩只能使用一次。'; + String get studyWithTheIdea => '教科書式的'; @override - String get stormFailedPuzzles => '失敗了的謎題'; + String get studyNextChapter => '下一章'; @override - String get stormSlowPuzzles => '慢 謎題'; + String get studyPrevChapter => '上一章'; @override - String get stormThisWeek => '本星期'; + String get studyStudyActions => '研討操作'; @override - String get stormThisMonth => '本月'; + String get studyTopics => '主題'; @override - String get stormAllTime => '總計'; + String get studyMyTopics => '我的主題'; @override - String get stormClickToReload => '點擊以重新加載'; + String get studyPopularTopics => '熱門主題'; @override - String get stormThisRunHasExpired => '本次比賽已過期!'; + String get studyManageTopics => '管理主題'; @override - String get stormThisRunWasOpenedInAnotherTab => '本次沖刺已經在另一個標籤頁中打開!'; + String get studyBack => '返回'; @override - String stormXRuns(int count) { + String get studyPlayAgain => '再玩一次'; + + @override + String get studyWhatWouldYouPlay => '你會在這個位置上怎麼走?'; + + @override + String get studyYouCompletedThisLesson => '恭喜!您完成了這個課程。'; + + @override + String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count輪', + other: '第$count章', ); return '$_temp0'; } @override - String stormPlayedNbRunsOfPuzzleStorm(int count, String param2) { + String studyNbGames(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '玩了$count輪的$param2', + other: '$count對局', ); return '$_temp0'; } @override - String get streamerLichessStreamers => 'Lichess實況主'; - - @override - String get studyShareAndExport => '分享 & 導出'; + String studyNbMembers(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count位成員', + ); + return '$_temp0'; + } @override - String get studyStart => '開始'; + String studyPasteYourPgnTextHereUpToNbGames(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '在此貼上PGN文本,最多可導入$count個棋局', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/lila_af.arb b/lib/l10n/lila_af.arb index 8f6f276407..1f85ff390f 100644 --- a/lib/l10n/lila_af.arb +++ b/lib/l10n/lila_af.arb @@ -35,6 +35,7 @@ "mobilePuzzleStormSubtitle": "Los soveel kopkrappers moontlik op in 3 minute.", "mobileGreeting": "Hallo, {param}", "mobileGreetingWithoutName": "Hallo", + "mobilePrefMagnifyDraggedPiece": "Vergroot gesleepte stuk", "activityActivity": "Aktiwiteite", "activityHostedALiveStream": "Het 'n lewendige uitsending aangebied", "activityRankedInSwissTournament": "Rang van #{param1} uit {param2}", @@ -57,7 +58,40 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{Het aan {count} swiss toernooi deelgeneem} other{Het aan {count} swiss toernooie deelgeneem}}", "activityJoinedNbTeams": "{count, plural, =1{Het aangesluit by {count} span} other{Het aangesluit by {count} spanne}}", "broadcastBroadcasts": "Uitsendings", + "broadcastMyBroadcasts": "My uitsendings", "broadcastLiveBroadcasts": "Regstreekse toernooi uitsendings", + "broadcastNewBroadcast": "Nuwe regstreekse uitsendings", + "broadcastAddRound": "Voeg 'n ronde by", + "broadcastOngoing": "Deurlopend", + "broadcastUpcoming": "Opkomend", + "broadcastCompleted": "Voltooi", + "broadcastRoundName": "Ronde se naam", + "broadcastRoundNumber": "Ronde getal", + "broadcastTournamentName": "Toernooi se naam", + "broadcastTournamentDescription": "Kort beskrywing van die toernooi", + "broadcastFullDescription": "Volle geleentheid beskrywing", + "broadcastFullDescriptionHelp": "Opsionele lang beskrywing van die uitsending. {param1} is beskikbaar. Lengte moet minder as {param2} karakters.", + "broadcastSourceSingleUrl": "PGN-Bronskakel", + "broadcastSourceUrlHelp": "URL wat Lichess sal nagaan vir PGN opdaterings. Dit moet openbaar beskikbaar wees vanaf die Internet.", + "broadcastStartDateHelp": "Optioneel, indien jy weet wanner die geleentheid begin", + "broadcastCurrentGameUrl": "Huidige spel se bronadres", + "broadcastDownloadAllRounds": "Laai al die rondes af", + "broadcastResetRound": "Herstel die ronde", + "broadcastDeleteRound": "Skrap die ronde", + "broadcastDefinitivelyDeleteRound": "Skrap die rondte en sy spelle beslis uit.", + "broadcastDeleteAllGamesOfThisRound": "Skrap alle spelle van hierdie rondte. Die bron sal aktief moet wees om hulle te kan herskep.", + "broadcastDeleteTournament": "Vee hierdie toernooi uit", + "broadcastDefinitivelyDeleteTournament": "Vee beslis die hele toernooi uit, met al sy rondtes en spelle.", + "broadcastReplacePlayerTags": "Opsioneel: vervang spelername, graderings en titels", + "broadcastFideFederations": "FIDE-federasies", + "broadcastTop10Rating": "Top 10 gradering", + "broadcastFidePlayers": "FIDE-deelnemers", + "broadcastFidePlayerNotFound": "FIDE-deelnemer nie gevind nie", + "broadcastFideProfile": "FIDE-profiel", + "broadcastFederation": "Federasie", + "broadcastAgeThisYear": "Ouderdom vanjaar", + "broadcastUnrated": "Ongegradeerd", + "broadcastRecentTournaments": "Onlangse toernooie", "challengeChallengesX": "Uitdagings: {param1}", "challengeChallengeToPlay": "Daag uit tot 'n spel", "challengeChallengeDeclined": "Uitdaging afgewys.", @@ -373,8 +407,8 @@ "puzzleThemeXRayAttackDescription": "'N Stuk val of verdedig 'n vierkant deur 'n vyandige stuk.", "puzzleThemeZugzwang": "Zugzwang", "puzzleThemeZugzwangDescription": "Die opponent is beperk in die bewegings wat hulle kan maak, en alle bewegings vererger hul posisie.", - "puzzleThemeHealthyMix": "Gesonde mengsel", - "puzzleThemeHealthyMixDescription": "'N Bietjie van alles. Jy weet nie wat om te verwag nie, dus bly jy gereed vir enigiets! Net soos in regte speletjies.", + "puzzleThemeMix": "Gesonde mengsel", + "puzzleThemeMixDescription": "'N Bietjie van alles. Jy weet nie wat om te verwag nie, dus bly jy gereed vir enigiets! Net soos in regte speletjies.", "puzzleThemePlayerGames": "Speler se spelle", "puzzleThemePlayerGamesDescription": "Beloer kopkrappers wat ontstaan van jou spelle, of van ander se spelle af.", "puzzleThemePuzzleDownloadInformation": "Die raaisels is in openbare domain en kan afgelaai word vanaf {param}.", @@ -505,7 +539,6 @@ "memory": "Geheue", "infiniteAnalysis": "Oneindige analise", "removesTheDepthLimit": "Verwyder dieptelimiet, en hou jou rekenaar warm", - "engineManager": "Enjinbestuurder", "blunder": "Flater", "mistake": "Fout", "inaccuracy": "Onakkuraatheid", @@ -606,6 +639,7 @@ "abortGame": "Staak spel", "gameAborted": "Spel gestaak", "standard": "Standaard", + "customPosition": "Gebruiklike Posisie", "unlimited": "Oneindig", "mode": "Modus", "casual": "Vriendskaplik", @@ -692,6 +726,7 @@ "blackCheckmatesInOneMove": "Swart om te skaakmat in een skuif", "retry": "Probeer weer", "reconnecting": "Konnekteer weer", + "noNetwork": "Vanlyn af", "favoriteOpponents": "Gunsteling opponente", "follow": "Volg", "following": "Besig om te volg", @@ -760,6 +795,9 @@ "ifNoneLeaveEmpty": "As geen, los oop", "profile": "Profiel", "editProfile": "Verander profiel", + "realName": "Regte naam", + "setFlair": "Stel jou Vlam", + "flair": "Vlam", "biography": "Biografie", "countryRegion": "Land of streek", "thankYou": "Dankie!", @@ -801,7 +839,6 @@ "cheat": "Kul", "troll": "Boelie", "other": "Ander", - "reportDescriptionHelp": "Plak skakel na die spel(le) en verduidelik wat skort met die lid se gedrag. Moenie net sê hulle kroek nie, maar verduidelik hoe daardie gevolgtrekking bereik is. Jou verslag sal vinniger geantwoord word as dit in Engels geskryf is.", "error_provideOneCheatedGameLink": "Verskaf asseblief ten minste een skakel na 'n spel waar hulle gekroek het.", "by": "deur {param}", "importedByX": "Ingevoer deur {param}", @@ -1212,6 +1249,7 @@ "giveNbSeconds": "{count, plural, =1{Gee {count} sekonde} other{Gee {count} sekondes}}", "nbTournamentPoints": "{count, plural, =1{{count} toernooipunt} other{{count} toernooipunte}}", "nbStudies": "{count, plural, =1{{count} studie} other{{count} studies}}", + "nbSimuls": "{count, plural, =1{{count} simulasie} other{{count} simulasies}}", "moreThanNbRatedGames": "{count, plural, =1{≥ {count} gegradeerde spel} other{≥ {count} gegradeerde spelle}}", "moreThanNbPerfRatedGames": "{count, plural, =1{≥ {count} {param2} gegradeerde spel} other{≥ {count} {param2} gegradeerde spelle}}", "needNbMorePerfGames": "{count, plural, =1{Jy het nodig om {count} meer gegradeerde {param2} spel te speel} other{Jy het nodig om {count} meer gegradeerde {param2} spelle te speel}}", @@ -1284,6 +1322,158 @@ "stormXRuns": "{count, plural, =1{1 lopie} other{{count} lopies}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Speel een lopie van {param2}} other{Speel {count} lopies van {param2}}}", "streamerLichessStreamers": "Lichess aanbieders", + "studyPrivate": "Privaat", + "studyMyStudies": "My studies", + "studyStudiesIContributeTo": "Studies waartoe ek bydra", + "studyMyPublicStudies": "My publieke studies", + "studyMyPrivateStudies": "My privaat studies", + "studyMyFavoriteStudies": "My gunsteling studies", + "studyWhatAreStudies": "Wat is studies?", + "studyAllStudies": "Alle studies", + "studyStudiesCreatedByX": "Studies gemaak deur {param}", + "studyNoneYet": "Nog geen.", + "studyHot": "Gewild", + "studyDateAddedNewest": "Datum bygevoeg (nuutste)", + "studyDateAddedOldest": "Datum bygevoeg (oudste)", + "studyRecentlyUpdated": "Onlangs opgedateer", + "studyMostPopular": "Mees gewilde", + "studyAlphabetical": "Alfabeties", + "studyAddNewChapter": "Voeg 'n nuwe hoofstuk by", + "studyAddMembers": "Voeg iemand by", + "studyInviteToTheStudy": "Nooi uit om deel te wees van die studie", + "studyPleaseOnlyInvitePeopleYouKnow": "Nooi asseblief net mense uit wat jy ken of wat aktief wil deelneem aan die studie.", + "studySearchByUsername": "Soek vir gebruikersnaam", + "studySpectator": "Toeskouer", + "studyContributor": "Bydraer", + "studyKick": "Verwyder", + "studyLeaveTheStudy": "Verlaat die studie", + "studyYouAreNowAContributor": "Jy is nou 'n bydraer", + "studyYouAreNowASpectator": "Jy is nou 'n toeskouer", + "studyPgnTags": "PGN etikette", + "studyLike": "Hou van", + "studyUnlike": "Afkeur", + "studyNewTag": "Nuwe etiket", + "studyCommentThisPosition": "Lewer kommentaar op hierdie posisie", + "studyCommentThisMove": "Lewer kommentaar op hierdie skuif", + "studyAnnotateWithGlyphs": "Annoteer met karakters", + "studyTheChapterIsTooShortToBeAnalysed": "Die hoofstuk is te kort om geanaliseer te word.", + "studyOnlyContributorsCanRequestAnalysis": "Slegs die studie bydraers kan versoek om 'n rekenaar analise te doen.", + "studyGetAFullComputerAnalysis": "Kry 'n vol-bediener rekenaar analise van die hooflyn.", + "studyMakeSureTheChapterIsComplete": "Maak seker dat die hoofstuk volledig is. Jy kan slegs eenkeer 'n analise versoek.", + "studyAllSyncMembersRemainOnTheSamePosition": "Alle SYNC lede bly op dieselfde posisie", + "studyShareChanges": "Deel veranderinge met toeskouers en stoor dit op die bediener", + "studyPlaying": "Besig om te speel", + "studyFirst": "Eerste", + "studyPrevious": "Vorige", + "studyNext": "Volgende", + "studyLast": "Laaste", "studyShareAndExport": "Deel & voer uit", - "studyStart": "Begin" + "studyCloneStudy": "Kloneer", + "studyStudyPgn": "Studie PGN", + "studyDownloadAllGames": "Laai alle speletjies af", + "studyChapterPgn": "Hoofstuk PGN", + "studyCopyChapterPgn": "Kopieer PGN", + "studyDownloadGame": "Aflaai spel", + "studyStudyUrl": "Bestudeer URL", + "studyCurrentChapterUrl": "Huidige hoofstuk URL", + "studyYouCanPasteThisInTheForumToEmbed": "U kan dit in die forum plak om in te bed", + "studyStartAtInitialPosition": "Begin by die oorspronklike posisie", + "studyStartAtX": "Begin by {param}", + "studyEmbedInYourWebsite": "Bed in u webwerf of blog", + "studyReadMoreAboutEmbedding": "Lees meer oor inbedding", + "studyOnlyPublicStudiesCanBeEmbedded": "Slegs openbare studies kan ingebed word!", + "studyOpen": "Maak oop", + "studyXBroughtToYouByY": "{param1}, aan jou beskikbaar gestel deur {param2}", + "studyStudyNotFound": "Studie kon nie gevind word nie", + "studyEditChapter": "Verander die hoofstuk", + "studyNewChapter": "Nuwe hoofstuk", + "studyImportFromChapterX": "Voer in vanaf {param}", + "studyOrientation": "Oriëntasie", + "studyAnalysisMode": "Analiseer mode", + "studyPinnedChapterComment": "Vasgepende hoofstuk kommentaar", + "studySaveChapter": "Stoor hoofstuk", + "studyClearAnnotations": "Vee annotasies uit", + "studyClearVariations": "Verwyder variasies", + "studyDeleteChapter": "Vee hoofstuk uit", + "studyDeleteThisChapter": "Vee die hoofstuk uit? Jy gaan dit nie kan terugvat nie!", + "studyClearAllCommentsInThisChapter": "Vee al die kommentaar, karakters en getekende vorms in die hoofstuk uit?", + "studyRightUnderTheBoard": "Reg onder die bord", + "studyNoPinnedComment": "Geen", + "studyNormalAnalysis": "Normale analise", + "studyHideNextMoves": "Versteek die volgende skuiwe", + "studyInteractiveLesson": "Interaktiewe les", + "studyChapterX": "Hoofstuk {param}", + "studyEmpty": "Leeg", + "studyStartFromInitialPosition": "Begin vanaf oorspronklike posisie", + "studyEditor": "Redakteur", + "studyStartFromCustomPosition": "Begin vanaf eie posisie", + "studyLoadAGameByUrl": "Laai 'n wedstryd op deur die URL", + "studyLoadAPositionFromFen": "Laai posisie vanaf FEN", + "studyLoadAGameFromPgn": "Laai wedstryd vanaf PGN", + "studyAutomatic": "Outomaties", + "studyUrlOfTheGame": "URL van die wedstryd", + "studyLoadAGameFromXOrY": "Laai 'n wedstryd van {param1} of {param2}", + "studyCreateChapter": "Skep 'n hoofstuk", + "studyCreateStudy": "Skep 'n studie", + "studyEditStudy": "Verander studie", + "studyVisibility": "Sigbaarheid", + "studyPublic": "Publiek", + "studyUnlisted": "Ongelys", + "studyInviteOnly": "Slegs op uitnodiging", + "studyAllowCloning": "Laat kloning toe", + "studyNobody": "Niemand", + "studyOnlyMe": "Net ek", + "studyContributors": "Bydraers", + "studyMembers": "Lede", + "studyEveryone": "Almal", + "studyEnableSync": "Maak sync beskikbaar", + "studyYesKeepEveryoneOnTheSamePosition": "Ja: hou almal op dieselfde posisie", + "studyNoLetPeopleBrowseFreely": "Nee: laat mense toe om vrylik deur te gaan", + "studyPinnedStudyComment": "Vasgepende studie opmerking", + "studyStart": "Begin", + "studySave": "Stoor", + "studyClearChat": "Maak die gesprek skoon", + "studyDeleteTheStudyChatHistory": "Vee die gesprek uit? Onthou, jy kan dit nie terug kry nie!", + "studyDeleteStudy": "Vee die studie uit", + "studyConfirmDeleteStudy": "Skrap die hele studie? Daar is geen terugkeer nie! Tik die naam van die studie om te bevesting: {param}", + "studyWhereDoYouWantToStudyThat": "Waar wil jy dit bestudeer?", + "studyGoodMove": "Goeie skuif", + "studyMistake": "Fout", + "studyBrilliantMove": "Skitterende skuif", + "studyBlunder": "Flater", + "studyInterestingMove": "Interesante skuif", + "studyDubiousMove": "Twyfelagte skuif", + "studyOnlyMove": "Eenigste skuif", + "studyZugzwang": "Zugzwang", + "studyEqualPosition": "Gelyke posisie", + "studyUnclearPosition": "Onduidelike posise", + "studyWhiteIsSlightlyBetter": "Wit is effens beter", + "studyBlackIsSlightlyBetter": "Swart is effens beter", + "studyWhiteIsBetter": "Wit is beter", + "studyBlackIsBetter": "Swart is beter", + "studyWhiteIsWinning": "Wit is beter", + "studyBlackIsWinning": "Swart is beter", + "studyNovelty": "Nuwigheid", + "studyDevelopment": "Ontwikkeling", + "studyInitiative": "Inisiatief", + "studyAttack": "Aanval", + "studyCounterplay": "Teenstoot", + "studyTimeTrouble": "Tydskommer", + "studyWithCompensation": "Met vergoeding", + "studyWithTheIdea": "Met die idee", + "studyNextChapter": "Volgende hoofstuk", + "studyPrevChapter": "Vorige hoofstuk", + "studyStudyActions": "Studie aksie", + "studyTopics": "Onderwerpe", + "studyMyTopics": "My onderwerpe", + "studyPopularTopics": "Gewilde onderwerpe", + "studyManageTopics": "Bestuur onderwerpe", + "studyBack": "Terug", + "studyPlayAgain": "Speel weer", + "studyWhatWouldYouPlay": "Wat sal jy in hierdie posisie speel?", + "studyYouCompletedThisLesson": "Geluk! Jy het hierdie les voltooi.", + "studyNbChapters": "{count, plural, =1{{count} Hoofstuk} other{{count} Hoofstukke}}", + "studyNbGames": "{count, plural, =1{{count} Wedstryd} other{{count} Wedstryde}}", + "studyNbMembers": "{count, plural, =1{{count} Lid} other{{count} Lede}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Plak jou PGN teks hier, tot by {count} spel} other{Plak jou PGN teks hier, tot by {count} spelle}}" } \ No newline at end of file diff --git a/lib/l10n/lila_ar.arb b/lib/l10n/lila_ar.arb index 1e090d3489..6c5d274d87 100644 --- a/lib/l10n/lila_ar.arb +++ b/lib/l10n/lila_ar.arb @@ -4,7 +4,7 @@ "mobileToolsTab": "أدوات", "mobileWatchTab": "شاهد", "mobileSettingsTab": "الإعدادات", - "mobileMustBeLoggedIn": "لعرض هذه الصفحة، قم بتسجيل الدخول.", + "mobileMustBeLoggedIn": "سجل الدخول لعرض هذه الصفحة.", "mobileSystemColors": "ألوان النظام", "mobileFeedbackButton": "الملاحظات", "mobileOkButton": "موافق", @@ -17,7 +17,7 @@ "mobileClearButton": "مسح", "mobilePlayersMatchingSearchTerm": "لاعبين مع \"{param}\"", "mobileNoSearchResults": "لا توجد نتائج", - "mobileAreYouSure": "هل أنت متأكد؟", + "mobileAreYouSure": "هل أنت واثق؟", "mobilePuzzleStreakAbortWarning": "سوف تفقد تسلقك الحالي وسيتم حفظ نتيجتك.", "mobilePuzzleStormNothingToShow": "لا شيء لإظهاره. العب بعض الألغاز.", "mobileSharePuzzle": "شارك هذا اللغز", @@ -28,15 +28,20 @@ "mobileHideVariation": "إخفاء سلسلة النقلات المرشحة", "mobileShowComments": "عرض التعليقات", "mobilePuzzleStormConfirmEndRun": "هل تريد إنهاء هذا التشغيل؟", - "mobilePuzzleStormFilterNothingToShow": "لا شيء لإظهاره، الرجاء تغيير الفلاتر", + "mobilePuzzleStormFilterNothingToShow": "لا شيء لإظهاره، الرجاء تغيير المرشح", "mobileCancelTakebackOffer": "إلغاء عرض الاسترداد", - "mobileCancelDrawOffer": "إلغاء عرض التعادل", "mobileWaitingForOpponentToJoin": "في انتظار انضمام الطرف الآخر...", - "mobileBlindfoldMode": "عصب العينين", - "mobileLiveStreamers": "البثوث المباشرة", - "mobileCustomGameJoinAGame": "الانضمام إلى لعبة", + "mobileBlindfoldMode": "معصوب العينين", + "mobileLiveStreamers": "البث المباشر", + "mobileCustomGameJoinAGame": "الانضمام إلى لُعْبَة", "mobileCorrespondenceClearSavedMove": "مسح النقل المحفوظ", "mobileSomethingWentWrong": "لقد حدث خطأ ما.", + "mobileShowResult": "إظهار النتيجة", + "mobilePuzzleThemesSubtitle": "حُل الألغاز المتعلّقة بافتتاحاتك المفضّلة، أو اختر موضوعاً.", + "mobilePuzzleStormSubtitle": "حل أكبر عدد ممكن من الألغاز في 3 دقائق.", + "mobileGreeting": "مرحبا، {param}", + "mobileGreetingWithoutName": "مرحبا", + "mobilePrefMagnifyDraggedPiece": "تكبير القطعة المسحوبة", "activityActivity": "الأنشطة", "activityHostedALiveStream": "بدأ بث مباشر", "activityRankedInSwissTournament": "حائز على تصنيف #{param1} في {param2}", @@ -59,7 +64,51 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =0{تنافس في {count} بطولة سويسرية} =1{تنافس في {count} بطولة سويسرية} =2{تنافس في {count} بطولة سويسرية} few{تنافس في {count} بطولة سويسرية} many{تنافس في {count} بطولة سويسرية} other{تنافس في {count} بطولة سويسرية}}", "activityJoinedNbTeams": "{count, plural, =0{إنضم ل {count} فريق} =1{إنضم ل {count} فريق} =2{إنضم لفريقين {count}} few{إنضم ل {count} فرق} many{إنضم ل {count} فرقة} other{إنضم ل {count} فريقًا}}", "broadcastBroadcasts": "البثوث", + "broadcastMyBroadcasts": "بثي", "broadcastLiveBroadcasts": "بث البطولة المباشرة", + "broadcastBroadcastCalendar": "تقويم البث", + "broadcastNewBroadcast": "بث مباشر جديد", + "broadcastSubscribedBroadcasts": "البث المُشترك به", + "broadcastAboutBroadcasts": "حول البثوث", + "broadcastHowToUseLichessBroadcasts": "كيفية استخدام بث ليتشيس.", + "broadcastTheNewRoundHelp": "ستضم الجولة الجديدة الأعضاء والمساهمين عينهم الذين اشتركوا في الجولة السابق.", + "broadcastAddRound": "إضافة جولة", + "broadcastOngoing": "الجارية", + "broadcastUpcoming": "القادمة", + "broadcastCompleted": "المكتملة", + "broadcastCompletedHelp": "يعرف ليتشيس بانتهاء الجولة استناداً إلى المصدر، استخدم هذا التبديل إذا لم يكن هناك مصدر.", + "broadcastRoundName": "اسم الجولة", + "broadcastRoundNumber": "رقم الجولة (الشوط)", + "broadcastTournamentName": "اسم البطولة", + "broadcastTournamentDescription": "وصف موجز للبطولة", + "broadcastFullDescription": "الوصف الكامل", + "broadcastFullDescriptionHelp": "الوصف الاختياري الطويل للبث. {param1} متوفر. يجب أن لا يتجاوز طول النص {param2} حرفاً.", + "broadcastSourceSingleUrl": "رابط مصدر PGN", + "broadcastSourceUrlHelp": "URL الذي سيتحقق منه Lichess للحصول على تحديثات PGN. يجب أن يكون متاحًا للجميع على الإنترنت.", + "broadcastSourceGameIds": "حتى 64 معرف لُعْبَة ليتشيس، مفصولة بمسافات.", + "broadcastStartDateTimeZone": "موعد البداية بتوقيت البطولة المحلي: {param}", + "broadcastStartDateHelp": "اختياري، إذا كنت تعرف متى يبدأ الحدث", + "broadcastCurrentGameUrl": "رابط المباراة الحالية", + "broadcastDownloadAllRounds": "تحميل جميع المباريات", + "broadcastResetRound": "إعادة ضبط هذه الجولة", + "broadcastDeleteRound": "حذف هذه الجولة", + "broadcastDefinitivelyDeleteRound": "قم بحذف الجولة وألعابها نهائيا.", + "broadcastDeleteAllGamesOfThisRound": "احذف جميع ألعاب هذه الجولة. سوف يحتاج المصدر إلى أن يكون نشطا من أجل إعادة إنشائها.", + "broadcastEditRoundStudy": "تعديل دراسة الجولة", + "broadcastDeleteTournament": "حذف هذه المسابقة", + "broadcastDefinitivelyDeleteTournament": "قم بحذف البطولة جميعها و جميع جولاتها و جميع ألعابها.", + "broadcastShowScores": "اظهر نقاط اللاعبين بناءً على نتائج اللعبة", + "broadcastReplacePlayerTags": "اختياري: استبدل أسماء اللاعبين وتقييماتهم وألقابهم", + "broadcastFideFederations": "الاتحاد الدولي للشطرنج", + "broadcastTop10Rating": "تقييم أعلى 10", + "broadcastFidePlayers": "لاعبين FIDE", + "broadcastFidePlayerNotFound": "لم يتم العثور على لاعب الاتحاد الدولي (FIDE)", + "broadcastFideProfile": "مِلَفّ FIDE", + "broadcastFederation": "إتحاد", + "broadcastAgeThisYear": "العمر هذا العام", + "broadcastUnrated": "غير مقيم", + "broadcastRecentTournaments": "البطولات الأخيرة", + "broadcastNbBroadcasts": "{count, plural, =0{{count} بث} =1{{count} بث} =2{بثين} few{{count} بثوث} many{{count} بثوث} other{{count} بثوث}}", "challengeChallengesX": "التحديات: {param1}", "challengeChallengeToPlay": "تحدى في مباراة", "challengeChallengeDeclined": "تم رفض التحدي", @@ -378,8 +427,8 @@ "puzzleThemeXRayAttackDescription": "القطعة تهاجم أو تدافع عن مربع, من خلال قطعة عدو.", "puzzleThemeZugzwang": "Zugzwang", "puzzleThemeZugzwangDescription": "حركات الخصم محدودة و كل الحركات تؤدي إلى تفاقم الوضع نحو الأسوء.", - "puzzleThemeHealthyMix": "خليط", - "puzzleThemeHealthyMixDescription": "القليل من كل نوع، لذا لا يمكنك التنبؤ باللغز القادم فابقى مستعداً لأي شيء، تماماً كالمباريات الحقيقية.", + "puzzleThemeMix": "خليط", + "puzzleThemeMixDescription": "القليل من كل نوع، لذا لا يمكنك التنبؤ باللغز القادم فابقى مستعداً لأي شيء، تماماً كالمباريات الحقيقية.", "puzzleThemePlayerGames": "مبارايات اللاعب", "puzzleThemePlayerGamesDescription": "ابحث عن ألغاز من مبارياتك أو من مباريات لاعبين آخرين.", "puzzleThemePuzzleDownloadInformation": "هذه الألغاز موجودة للعامة بإمكانك تحميلها من هنا{param}.", @@ -510,7 +559,6 @@ "memory": "الذاكرة", "infiniteAnalysis": "تحليل لانهائي", "removesTheDepthLimit": "التحليل لأبعد عمق، وابقاء حاسوبك نشطًا", - "engineManager": "مدير المحركات", "blunder": "خطأ فادح", "mistake": "خطأ", "inaccuracy": "غير دقيق", @@ -814,7 +862,9 @@ "cheat": "غش", "troll": "إزعاج", "other": "أخرى", - "reportDescriptionHelp": "الصق رابط المباراة (المباريات) واشرح بالتفصيل المشكلة في تصرف هذا المستحدم. لا تقل فقط \"انهم يغشون\"، ولكن اشرح لنا سبب استنتاجك. سيكون الرد أسرع إن كتبت بالإنكليزية.", + "reportCheatBoostHelp": "هذه رسالة عامية وليست مخصصة لبلاغات الغش. وهي تحاول تعليم اللاعب كيفية كتابة بلاغ مفيد لفريق لي-تشيس. و أيضا تطلب إثبات.\n\nتظهر على صفحة \"بلغ مستخدم\"\nhttps://lichess. org/report.", + "reportUsernameHelp": "اشرح ما المسيء في اسم المستخدم هذا. لا تقل فقط \"إنه مسيء/غير مناسب\"، بل أخبرنا كيف توصلت إلى هذا الاستنتاج، خاصة إذا كانت الإهانة غير واضحة، أو ليست باللغة الإنجليزية، أو كانت باللغة العامية، أو كانت إشارة تاريخية/ثقافية.", + "reportProcessedFasterInEnglish": "سيتم معالجة بلاغك بشكل أسرع إذا تمت كتابته باللغة الإنجليزية.", "error_provideOneCheatedGameLink": "برجاء تقديم رابط واحد علي الأقل لمباراة حدث فيها غش.", "by": "كتبها {param}", "importedByX": "استيراد '{param}'", @@ -1308,6 +1358,159 @@ "stormXRuns": "{count, plural, =0{لا جولات} =1{جولة واحدة} =2{جولتان} few{{count} جولات} many{{count} جولة} other{{count} جولة}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =0{لم تلعب أي جولة {param2}} =1{لعبت جولة واحدة من {param2}} =2{لعبت جولتين من {param2}} few{لعبت {count} جولات من {param2}} many{لعبت {count} جولة من {param2}} other{لعبت {count} جولة من {param2}}}", "streamerLichessStreamers": "بثوث ليشس", + "studyPrivate": "خاص", + "studyMyStudies": "دراستي", + "studyStudiesIContributeTo": "الدراسات المساهم بها", + "studyMyPublicStudies": "دراسات العامة", + "studyMyPrivateStudies": "دراساتي الخاصة", + "studyMyFavoriteStudies": "دراساتي المفضلة", + "studyWhatAreStudies": "ما هي الدراسات؟", + "studyAllStudies": "كل الدراسات", + "studyStudiesCreatedByX": "الدراسات التي أنشئها {param}", + "studyNoneYet": "لا يوجد.", + "studyHot": "ذات شعبية", + "studyDateAddedNewest": "تاريخ الإضافة (الأحدث)", + "studyDateAddedOldest": "تاريخ الإضافة (الأقدم)", + "studyRecentlyUpdated": "تم تحديثه مؤخرا", + "studyMostPopular": "الاكثر شعبية", + "studyAlphabetical": "أبجدي", + "studyAddNewChapter": "أضف فصلاً جديدا", + "studyAddMembers": "إضافة أعضاء", + "studyInviteToTheStudy": "دعوة الى دراسة", + "studyPleaseOnlyInvitePeopleYouKnow": "يرجى فقط إضافة اشخاص تعرفهم، ويريدون المشاركة في هذه الدراسة", + "studySearchByUsername": "البحث بواسطة اسم المستخدم", + "studySpectator": "مشاهد", + "studyContributor": "مساهم", + "studyKick": "طرد", + "studyLeaveTheStudy": "مغادرة الدراسة", + "studyYouAreNowAContributor": "انت الان اصبحت مساهم", + "studyYouAreNowASpectator": "انت الان اصبحت مشاهد", + "studyPgnTags": "وسم PGN", + "studyLike": "إعجاب", + "studyUnlike": "إلغاء الإعجاب", + "studyNewTag": "علامة جديدة", + "studyCommentThisPosition": "التعليق على هذا الوضع", + "studyCommentThisMove": "التعليق على هذه النقلة", + "studyAnnotateWithGlyphs": "التعليق مع الحروف الرسومية", + "studyTheChapterIsTooShortToBeAnalysed": "الفصل جداً قصير لكي يتم تحليله", + "studyOnlyContributorsCanRequestAnalysis": "فقط المساهمون في هذا الدراسة يمكنهم طلب تحليل الحاسوب", + "studyGetAFullComputerAnalysis": "احصل على تحليل حاسوب كامل للتفريع الرئيسي من قبل الخادم", + "studyMakeSureTheChapterIsComplete": "كن متأكداً ان الفصل مكتمل، يمكنك طلب تحليل الحاسوب مره واحده فحسب", + "studyAllSyncMembersRemainOnTheSamePosition": "يظل جميع ألاعضاء الذين تمت مزامنة معلوماتهم في نفس الترتيب", + "studyShareChanges": "شارك التغيبرات مع المشاهدين وإحفظهن الى الخادم", + "studyPlaying": "يلعب الان", + "studyShowEvalBar": "شرائط التقييم", + "studyFirst": "الأولى", + "studyPrevious": "السابق", + "studyNext": "التالي", + "studyLast": "الأخير", "studyShareAndExport": "مشاركة و تصدير", - "studyStart": "ابدأ" + "studyCloneStudy": "استنساخ", + "studyStudyPgn": "PGN الدراسة", + "studyDownloadAllGames": "حمل جميع الألعاب", + "studyChapterPgn": "PGN الفصل", + "studyCopyChapterPgn": "نسخ PGN", + "studyDownloadGame": "حمل لعبة", + "studyStudyUrl": "رابط الدراسة", + "studyCurrentChapterUrl": "رابط الفصل الحالي", + "studyYouCanPasteThisInTheForumToEmbed": "يمكنك لصق هذا في المنتدى لتضمينه", + "studyStartAtInitialPosition": "البدء من وضع البداية", + "studyStartAtX": "البدء من {param}", + "studyEmbedInYourWebsite": "ضمنه في موقع أو مدونة", + "studyReadMoreAboutEmbedding": "راجع المزيد عن التضمين", + "studyOnlyPublicStudiesCanBeEmbedded": "يمكن تضمين الدراسات العامة فقط!", + "studyOpen": "فتح", + "studyXBroughtToYouByY": "{param1} مقدمة من {param2}", + "studyStudyNotFound": "لم يتم العثور على الدراسة", + "studyEditChapter": "تحرير الفصل", + "studyNewChapter": "فصل جديد", + "studyImportFromChapterX": "استيراد من {param}", + "studyOrientation": "اتجاه الرقعة", + "studyAnalysisMode": "وضع التحليل", + "studyPinnedChapterComment": "التعليق المثبت على الفصل", + "studySaveChapter": "حفظ الفصل", + "studyClearAnnotations": "مسح العلامات", + "studyClearVariations": "مسح اللاينات", + "studyDeleteChapter": "حذف الفصل", + "studyDeleteThisChapter": "هل تريد حذف الفصل ؟ لايمكنك التراجع عن ذلك لاحقاً!", + "studyClearAllCommentsInThisChapter": "مسح جميع التعليقات والغلافات والأشكال المرسومة في هذا الفصل؟", + "studyRightUnderTheBoard": "تحت الرقعة مباشرة", + "studyNoPinnedComment": "بدون", + "studyNormalAnalysis": "تحليل عادي", + "studyHideNextMoves": "أخفي النقلة التالية", + "studyInteractiveLesson": "درس تفاعلي", + "studyChapterX": "الفصل {param}", + "studyEmpty": "فارغ", + "studyStartFromInitialPosition": "البدء من وضعية البداية", + "studyEditor": "المحرر", + "studyStartFromCustomPosition": "البدء من وضع مخصص", + "studyLoadAGameByUrl": "تحميل لعبة من رابط", + "studyLoadAPositionFromFen": "تحميل موقف من FEN", + "studyLoadAGameFromPgn": "استرد لعبة من PGN", + "studyAutomatic": "تلقائي", + "studyUrlOfTheGame": "رابط اللعبة", + "studyLoadAGameFromXOrY": "استيراد لعبة من {param1} او {param2}", + "studyCreateChapter": "أنشئ الفصل", + "studyCreateStudy": "أنشى الدراسة", + "studyEditStudy": "حرر الدراسة", + "studyVisibility": "الظهور", + "studyPublic": "عامة", + "studyUnlisted": "غير مدرجة", + "studyInviteOnly": "دعوة فقط", + "studyAllowCloning": "السماح بالاستنساخ", + "studyNobody": "لا أحد", + "studyOnlyMe": "أنا فقط", + "studyContributors": "المساهمون", + "studyMembers": "اعضاء", + "studyEveryone": "الجميع", + "studyEnableSync": "مكن المزامنة", + "studyYesKeepEveryoneOnTheSamePosition": "نعم: إبقاء الجميع في نفس الوضعية", + "studyNoLetPeopleBrowseFreely": "لا: دع الناس يتصفحون بحرية", + "studyPinnedStudyComment": "تعليق الدراسة المثبتة", + "studyStart": "ابدأ", + "studySave": "حفظ", + "studyClearChat": "مسح المحادثة", + "studyDeleteTheStudyChatHistory": "هل تريد حذف سجل الدردشة الدراسية؟ لا يمكن إرجاعها!", + "studyDeleteStudy": "حذف الدراسة", + "studyConfirmDeleteStudy": "حذف الدراسة بأكملها؟ لا يمكنك التراجع عن هذه الخطوة! اكتب اسم الدراسة لتأكيد عملية الحذف: {param}", + "studyWhereDoYouWantToStudyThat": "أين تريد دراسة ذلك؟", + "studyGoodMove": "نقلة جيدة", + "studyMistake": "خطأ", + "studyBrilliantMove": "نقلة رائعة", + "studyBlunder": "غلطة", + "studyInterestingMove": "نقلة مثيرة للاهتمام", + "studyDubiousMove": "نقلة مشبوهة", + "studyOnlyMove": "نقلة وحيدة", + "studyZugzwang": "Zugzwang", + "studyEqualPosition": "وضع متساوي", + "studyUnclearPosition": "وضعية غير واضح", + "studyWhiteIsSlightlyBetter": "الأبيض أفضل بقليل", + "studyBlackIsSlightlyBetter": "الأسود أفضل بقليل", + "studyWhiteIsBetter": "الأبيض أفضل", + "studyBlackIsBetter": "الأسود أفضل", + "studyWhiteIsWinning": "الأبيض يفوز", + "studyBlackIsWinning": "الأسود يفوز", + "studyNovelty": "جديد", + "studyDevelopment": "تطوير", + "studyInitiative": "مبادرة", + "studyAttack": "هجوم", + "studyCounterplay": "هجوم مضاد", + "studyTimeTrouble": "مشكلة وقت", + "studyWithCompensation": "مع تعويض", + "studyWithTheIdea": "مع فكرة", + "studyNextChapter": "الفصل التالي", + "studyPrevChapter": "الفصل السابق", + "studyStudyActions": "خيارات الدراسة", + "studyTopics": "المواضيع", + "studyMyTopics": "المواضيع الخاصة بي", + "studyPopularTopics": "المواضيع الشائعة", + "studyManageTopics": "إدارة المواضيع", + "studyBack": "رجوع", + "studyPlayAgain": "اللعب مجددا", + "studyWhatWouldYouPlay": "ماذا ستلعب في هذا الموقف؟", + "studyYouCompletedThisLesson": "تهانينا! لقد أكملت هذا الدرس.", + "studyNbChapters": "{count, plural, =0{{count} فصل} =1{{count} فصل} =2{فصلان} few{{count} فصول} many{{count} فصل} other{{count} فصول}}", + "studyNbGames": "{count, plural, =0{{count} مباراة} =1{{count} مباراة} =2{مبارتان} few{{count} مبارايات} many{{count} مباراة} other{{count} مباراة}}", + "studyNbMembers": "{count, plural, =0{{count} عضو} =1{{count} عضو} =2{{count} عضو} few{{count} عضو} many{{count} عضو} other{{count} أعضاء}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =0{ألصق نص PGN هنا، حتى {count} مباراة} =1{الصق نص الPGN هنا، حتى {count} لعبة واحدة} =2{ألصق نص PGN هنا، حتى {count} مباراة} few{ألصق نص PGN هنا، حتى {count} مباراة} many{ألصق نص PGN هنا، حتى {count} مباراة} other{الصق الPGN هنا، حتى {count} العاب}}" } \ No newline at end of file diff --git a/lib/l10n/lila_az.arb b/lib/l10n/lila_az.arb index e8769b9b78..5b3bb5686c 100644 --- a/lib/l10n/lila_az.arb +++ b/lib/l10n/lila_az.arb @@ -22,6 +22,22 @@ "activityJoinedNbTeams": "{count, plural, =1{{count} komandasına qatıldı} other{{count} komandasına qatıldı}}", "broadcastBroadcasts": "Yayım", "broadcastLiveBroadcasts": "Canlı turnir yayımları", + "broadcastNewBroadcast": "Yeni canlı yayım", + "broadcastAddRound": "Tur əlavə et", + "broadcastOngoing": "Davam edən", + "broadcastUpcoming": "Yaxınlaşan", + "broadcastCompleted": "Tamamlanan", + "broadcastRoundName": "Tur adı", + "broadcastRoundNumber": "Tur sayı", + "broadcastTournamentName": "Turnir adı", + "broadcastTournamentDescription": "Qısa turnir açıqlaması", + "broadcastFullDescription": "Tədbirin tam açıqlaması", + "broadcastFullDescriptionHelp": "Tədbirin istəyə bağlı təfsilatlı açıqlaması. {param1} seçimi mövcuddur. Mətnin uzunluğu {param2} simvoldan az olmalıdır.", + "broadcastSourceUrlHelp": "Lichess, verdiyiniz URL ilə PGN-i yeniləyəcək. Bu internetdə hamı tərəfindən əldə edilə bilən olmalıdır.", + "broadcastStartDateHelp": "İstəyə bağlı, tədbirin başlama vaxtını bilirsinizsə", + "broadcastCurrentGameUrl": "Hazırkı oyun URL-i", + "broadcastResetRound": "Bu turu sıfırla", + "broadcastDeleteRound": "Bu turu sil", "challengeChallengeToPlay": "Oyuna çağırış", "challengeChallengeDeclined": "Çağırış rədd edildi.", "challengeChallengeAccepted": "Çağırış qəbul edildi!", @@ -301,8 +317,8 @@ "puzzleThemeXRayAttackDescription": "Fiqur, bir xanaya rəqib fiquru üzərindən hücum edir və ya müdafiə edir.", "puzzleThemeZugzwang": "Suqsvanq", "puzzleThemeZugzwangDescription": "Rəqibin edə biləcəyi gediş sayı məhduddur və istənilən gediş vəziyyəti daha da pisləşdirir.", - "puzzleThemeHealthyMix": "Həftəbecər", - "puzzleThemeHealthyMixDescription": "Hər şeydən bir az. Nə gözləyəcəyini bilmirsən, ona görə hər şeyə hazır olursan! Eynilə həqiqi oyunlarda olduğu kimi.", + "puzzleThemeMix": "Həftəbecər", + "puzzleThemeMixDescription": "Hər şeydən bir az. Nə gözləyəcəyini bilmirsən, ona görə hər şeyə hazır olursan! Eynilə həqiqi oyunlarda olduğu kimi.", "searchSearch": "Axtar", "settingsSettings": "Tənzimləmələr", "settingsCloseAccount": "Hesabı bağla", @@ -426,7 +442,6 @@ "memory": "RAM", "infiniteAnalysis": "Sonsuz təhlil", "removesTheDepthLimit": "Dərinlik limitini ləğv edir və kompüterinizi isti saxlayır", - "engineManager": "Mühərrik meneceri", "blunder": "Kobud Səhv", "mistake": "Səhv", "inaccuracy": "Qeyri-dəqiqlik", @@ -716,7 +731,6 @@ "cheat": "Hiylə", "troll": "Trol", "other": "Digər", - "reportDescriptionHelp": "Oyunun və ya oyunların linkini yapışdırın və bu istifadəçinin davranışında nəyin səhv olduğunu izah edin. Yalnız \"hiylə edirlər\" deməyin, necə bu nəticəyə gəldiyinizi bizə deyin. İngilis dilində yazıldığı təqdirdə hesabat daha sürətli işlənəcəkdir.", "error_provideOneCheatedGameLink": "Lütfən ən azı bir hiyləli oyun linki daxil edin.", "by": "{param} tərəfindən", "thisTopicIsNowClosed": "Mövzu bağlandı.", @@ -1106,6 +1120,134 @@ "stormXRuns": "{count, plural, =1{1 cəhd} other{{count} cəhd}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Bir dəfə {param2} oynadı} other{{count} dəfə {param2} oynadı}}", "streamerLichessStreamers": "Lichess yayımçıları", + "studyPrivate": "Özəl", + "studyMyStudies": "Çalışmalarım", + "studyStudiesIContributeTo": "Töhfə verdiyim çalışmalar", + "studyMyPublicStudies": "Hərkəsə açıq çalışmalarım", + "studyMyPrivateStudies": "Özəl çalışmalarım", + "studyMyFavoriteStudies": "Sevimli çalışmalarım", + "studyWhatAreStudies": "Çalışmalar nədir?", + "studyAllStudies": "Bütün çalışmalar", + "studyStudiesCreatedByX": "{param} tərəfindən yaradılan çalışmalar", + "studyNoneYet": "Hələ ki, yoxdur.", + "studyHot": "Məşhur", + "studyDateAddedNewest": "Əlavə edilmə tarixi (yenidən köhnəyə)", + "studyDateAddedOldest": "Əlavə edilmə tarixi (köhnədən yeniyə)", + "studyRecentlyUpdated": "Ən son yenilənən", + "studyMostPopular": "Ən məşhur", + "studyAlphabetical": "Əlifbaya görə", + "studyAddNewChapter": "Yeni bir fəsil əlavə et", + "studyAddMembers": "Üzv əlavə et", + "studyInviteToTheStudy": "Çalışmaya dəvət et", + "studyPleaseOnlyInvitePeopleYouKnow": "Zəhmət olmasa yalnız tanıdığınız və bu çalışmaya aktiv olaraq qoşulmaq istəyən insanları dəvət edin.", + "studySearchByUsername": "İstifadəçi adına görə axtar", + "studySpectator": "Tamaşaçı", + "studyContributor": "Töhfə verən", + "studyKick": "Qov", + "studyLeaveTheStudy": "Çalışmanı tərk et", + "studyYouAreNowAContributor": "İndi iştirakçısınız", + "studyYouAreNowASpectator": "İndi tamaşaçısınız", + "studyPgnTags": "PGN etiketləri", + "studyLike": "Bəyən", + "studyNewTag": "Yeni etiket", + "studyCommentThisPosition": "Bu pozisiyaya rəy bildirin", + "studyCommentThisMove": "Bu gedişə rəy bildirin", + "studyAnnotateWithGlyphs": "Simvol ilə izah et", + "studyTheChapterIsTooShortToBeAnalysed": "Fəsil təhlil edilməsi üçün çox qısadır.", + "studyOnlyContributorsCanRequestAnalysis": "Yalnız çalışma iştirakçıları kompüter təhlili tələb edə bilər.", + "studyGetAFullComputerAnalysis": "Ana variant üçün serverdən hərtərəfli kompüter təhlilini alın.", + "studyMakeSureTheChapterIsComplete": "Fəslin tamamlandığına əmin olun. Yalnız bir dəfə təhlil tələbi edə bilərsiniz.", + "studyAllSyncMembersRemainOnTheSamePosition": "EYNİLƏŞDİRİLMİŞ bütün üzvlər eyni pozisiyada qalır", + "studyShareChanges": "Dəyişiklikləri tamaşaçılarla paylaşın və onları serverdə saxlayın", + "studyPlaying": "Oynanılan", + "studyFirst": "İlk", + "studyPrevious": "Əvvəlki", + "studyNext": "Növbəti", + "studyLast": "Son", "studyShareAndExport": "Paylaş və yüklə", - "studyStart": "Başlat" + "studyCloneStudy": "Klonla", + "studyStudyPgn": "Çalışma PGN-i", + "studyDownloadAllGames": "Bütün oyunları endir", + "studyChapterPgn": "Fəsil PGN-i", + "studyDownloadGame": "Oyunu endir", + "studyStudyUrl": "Çalışma URL-i", + "studyCurrentChapterUrl": "Cari fəsil URL-ii", + "studyYouCanPasteThisInTheForumToEmbed": "Pərçimləmək üçün bunu forumda paylaşa bilərsiniz", + "studyStartAtInitialPosition": "Başlanğıc pozisiyada başlasın", + "studyStartAtX": "buradan başla: {param}", + "studyEmbedInYourWebsite": "Veb sayt və ya bloqunuzda pərçimləyin", + "studyReadMoreAboutEmbedding": "Pərçimləmə haqqında daha ətraflı", + "studyOnlyPublicStudiesCanBeEmbedded": "Yalnız hərkəsə açıq çalışmalar pərçimlənə bilər!", + "studyOpen": "Aç", + "studyXBroughtToYouByY": "{param2} sizə {param1} tərəfindən gətirildi", + "studyStudyNotFound": "Çalışma tapılmadı", + "studyEditChapter": "Fəslə düzəliş et", + "studyNewChapter": "Yeni fəsil", + "studyOrientation": "İstiqamət", + "studyAnalysisMode": "Təhlil rejimi", + "studyPinnedChapterComment": "Sancaqlanmış fəsil rəyləri", + "studySaveChapter": "Fəsli yadda saxla", + "studyClearAnnotations": "İzahları təmizlə", + "studyDeleteChapter": "Fəsli sil", + "studyDeleteThisChapter": "Bu fəsil silinsin? Bunun geri dönüşü yoxdur!", + "studyClearAllCommentsInThisChapter": "Bu fəsildəki bütün rəylər, simvollar və çəkilmiş formalar təmizlənsin?", + "studyRightUnderTheBoard": "Lövhənin altında", + "studyNoPinnedComment": "Görünməsin", + "studyNormalAnalysis": "Normal təhlil", + "studyHideNextMoves": "Növbəti gedişləri gizlət", + "studyInteractiveLesson": "İnteraktiv dərs", + "studyChapterX": "{param}. Fəsil", + "studyEmpty": "Boş", + "studyStartFromInitialPosition": "Başlanğıc pozisiyadan başlasın", + "studyEditor": "Redaktor", + "studyStartFromCustomPosition": "Özəl pozisiyadan başlasın", + "studyLoadAGameByUrl": "URL ilə oyun yüklə", + "studyLoadAPositionFromFen": "FEN ilə pozisiya yüklə", + "studyLoadAGameFromPgn": "PGN ilə oyun yüklə", + "studyAutomatic": "Avtomatik", + "studyUrlOfTheGame": "Oyun URL-i", + "studyLoadAGameFromXOrY": "{param1} və ya {param2} ilə oyun yükləyin", + "studyCreateChapter": "Fəsil yarat", + "studyCreateStudy": "Çalışma yarat", + "studyEditStudy": "Çalışmaya düzəliş et", + "studyVisibility": "Görünmə", + "studyPublic": "Hərkəsə açıq", + "studyUnlisted": "Siyahıya alınmamış", + "studyInviteOnly": "Yalnız dəvətlə", + "studyAllowCloning": "Klonlamağa icazə ver", + "studyNobody": "Heç kim", + "studyOnlyMe": "Yalnız mən", + "studyContributors": "Töhfə verənlər", + "studyMembers": "Üzvlər", + "studyEveryone": "Hamı", + "studyEnableSync": "Eyniləşdirməni aktivləşdir", + "studyYesKeepEveryoneOnTheSamePosition": "Bəli: hər kəsi eyni pozisiyada saxla", + "studyNoLetPeopleBrowseFreely": "Xeyr: sərbəst gəzməyə icazə ver", + "studyPinnedStudyComment": "Sancaqlanmış çalışma rəyləri", + "studyStart": "Başlat", + "studySave": "Saxla", + "studyClearChat": "Söhbəti təmizlə", + "studyDeleteTheStudyChatHistory": "Çalışmanın söhbət tarixçəsi silinsin? Bunun geri dönüşü yoxdur!", + "studyDeleteStudy": "Çalışmanı sil", + "studyWhereDoYouWantToStudyThat": "Harada çalışmaq istəyirsən?", + "studyGoodMove": "Yaxşı gediş", + "studyMistake": "Səhv", + "studyBrilliantMove": "Brilyant gediş", + "studyBlunder": "Kobud səhv", + "studyInterestingMove": "Maraqlı gediş", + "studyDubiousMove": "Şübhəli gediş", + "studyOnlyMove": "Tək gediş", + "studyZugzwang": "Suqsvanq", + "studyEqualPosition": "Bərabər mövqe", + "studyUnclearPosition": "Qeyri-müəyyən mövqe", + "studyWhiteIsSlightlyBetter": "Ağlar biraz öndədir", + "studyBlackIsSlightlyBetter": "Qaralar biraz öndədir", + "studyWhiteIsBetter": "Ağlar üstündür", + "studyBlackIsBetter": "Qaralar üstündür", + "studyWhiteIsWinning": "Ağlar qalib gəlir", + "studyBlackIsWinning": "Qaralar qalib gəlir", + "studyNbChapters": "{count, plural, =1{{count} Fəsil} other{{count} Fəsil}}", + "studyNbGames": "{count, plural, =1{{count} Oyun} other{{count} Oyun}}", + "studyNbMembers": "{count, plural, =1{{count} Üzv} other{{count} Üzv}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{PGN mətninizi bura yapışdırın, ən çox {count} oyuna qədər} other{PGN mətninizi bura yapışdırın, ən çox {count} oyuna qədər}}" } \ No newline at end of file diff --git a/lib/l10n/lila_be.arb b/lib/l10n/lila_be.arb index ef86112a2f..245123ff66 100644 --- a/lib/l10n/lila_be.arb +++ b/lib/l10n/lila_be.arb @@ -1,4 +1,14 @@ { + "mobileHomeTab": "Галоўная", + "mobilePuzzlesTab": "Задачы", + "mobileSettingsTab": "Налады", + "mobileOkButton": "Добра", + "mobileSettingsImmersiveMode": "Поўнаэкранны рэжым", + "mobileRecentSearches": "Нядаўнія пошукі", + "mobileClearButton": "Ачысціць", + "mobilePlayersMatchingSearchTerm": "Гульцы з «{param}»", + "mobileNoSearchResults": "Няма вынікаў", + "mobileAreYouSure": "Вы ўпэўнены?", "activityActivity": "Актыўнасць", "activityHostedALiveStream": "Правялі прамую трансляцыю", "activityRankedInSwissTournament": "Скончыў на {param1} месцы ў {param2}", @@ -21,7 +31,35 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{Паўдзельнічаў(-ла) у {count} турніры па швейцарскай сістэме} few{Паўдзельнічаў(-ла) у {count} турнірах па швейцырскай сістэме} many{Паўдзельнічаў(-ла) у {count} турнірах па швейцырскай сістэме} other{Паўдзельнічаў(-ла) у {count} турнірах па швейцырскай сістэме}}", "activityJoinedNbTeams": "{count, plural, =1{Далучыўся да {count} каманды} few{Далучыўся да {count} каманд} many{Далучыўся да {count} каманд} other{Далучыўся да {count} каманд}}", "broadcastBroadcasts": "Трансляцыі", + "broadcastMyBroadcasts": "Мае трансляцыі", "broadcastLiveBroadcasts": "Прамыя трансляцыі турніраў", + "broadcastNewBroadcast": "Новая прамая трансляцыя", + "broadcastAboutBroadcasts": "Пра трансляцыіі", + "broadcastHowToUseLichessBroadcasts": "Як карыстацца трансляцыямі Lichess.", + "broadcastAddRound": "Дадаць тур", + "broadcastOngoing": "Бягучыя", + "broadcastUpcoming": "Надыходзячыя", + "broadcastCompleted": "Завершаныя", + "broadcastRoundName": "Назва туру", + "broadcastRoundNumber": "Нумар туру", + "broadcastTournamentName": "Назва турніру", + "broadcastTournamentDescription": "Сціслае апісанне турніру", + "broadcastFullDescription": "Поўнае апісанне турніру", + "broadcastFullDescriptionHelp": "Неабавязковая дасканалае апісанне турніру. Даступны {param1}. Даўжыня павінна быць менш за {param2} сімвалаў.", + "broadcastSourceUrlHelp": "Спасылка, з якой Lichess паспрабуе атрымоўваць абнаўленні PGN. Яны павінна быць даступнай для кожнай ва Інтэрнэце.", + "broadcastStartDateHelp": "Па жаданні, калі вы ведаеце пачатак падзеі", + "broadcastCurrentGameUrl": "Спасылка на бягучую гульню", + "broadcastDownloadAllRounds": "Спампаваць усе туры", + "broadcastResetRound": "Скасаваць гэты тур", + "broadcastDeleteRound": "Выдаліць гэты тур", + "broadcastDefinitivelyDeleteRound": "Канчаткова выдаліць ​​тур і ўсе яго гульні.", + "broadcastDeleteAllGamesOfThisRound": "Выдаліць усе гульні гэтага тура. Для іх паўторнага стварэння крыніца павінна быць актыўнай.", + "broadcastEditRoundStudy": "Рэдагаваць навучанне туру", + "broadcastDeleteTournament": "Выдаліць гэты турнір", + "broadcastDefinitivelyDeleteTournament": "Канчаткова выдаліць увесь турнір, усе яго туры і ўсе гульні.", + "broadcastFidePlayers": "Гульцы FIDE", + "broadcastFideProfile": "Профіль FIDE", + "broadcastFederation": "Федэрацыя", "challengeChallengesX": "Выклікаў: {param1}", "challengeChallengeToPlay": "Выклікаць на гульню", "challengeChallengeDeclined": "Выклік адхілены", @@ -172,6 +210,7 @@ "puzzleKeepGoing": "Працягвайце…", "puzzlePuzzleSuccess": "Поспех!", "puzzlePuzzleComplete": "Задача вырашана!", + "puzzlePuzzlesByOpenings": "Задачы за дэбютамі", "puzzleNotTheMove": "Гэта не той ход!", "puzzleTrySomethingElse": "Паспрабуйце нешта іншае.", "puzzleRatingX": "Рэйтынг: {param}", @@ -334,8 +373,8 @@ "puzzleThemeXRayAttackDescription": "Фігара нападае або бараніць поле праз варожую фігуру.", "puzzleThemeZugzwang": "Цугцванг", "puzzleThemeZugzwangDescription": "Супернік абмежаваны ў хадах і ўсе магчымыя хады пагаршаюць яго пазіцыю.", - "puzzleThemeHealthyMix": "Здаровая сумесь", - "puzzleThemeHealthyMixDescription": "Патрошкі ўсяго. Вы ня ведаеце чаго чакаць, таму гатовы да ўсяго! Як у сапраўдных гульнях.", + "puzzleThemeMix": "Здаровая сумесь", + "puzzleThemeMixDescription": "Патрошкі ўсяго. Вы ня ведаеце чаго чакаць, таму гатовы да ўсяго! Як у сапраўдных гульнях.", "puzzleThemePlayerGames": "З партый гульца", "puzzleThemePlayerGamesDescription": "Праглядзіце задачы ўзятыя з вашых гульняў, ці з партый іншага гульца.", "puzzleThemePuzzleDownloadInformation": "Гэта публічныя задачы, іх магчыма спампаваць з {param}.", @@ -413,6 +452,7 @@ "promoteVariation": "Прасунуць варыянт", "makeMainLine": "Зрабіць асноўным варыянтам", "deleteFromHere": "Выдаліць з гэтага месца", + "collapseVariations": "Ачысціць варыянты", "forceVariation": "Прасунуць варыянт", "copyVariationPgn": "Скап'яваць варыянт у фармаце PGN", "move": "Ход", @@ -712,6 +752,7 @@ "ifNoneLeaveEmpty": "Калі няма, пакіньце пустым", "profile": "Профіль", "editProfile": "Рэдагаваць профіль", + "realName": "Сапраўднае імя", "biography": "Біяграфія", "countryRegion": "Краіна або рэгіён", "thankYou": "Дзякуй!", @@ -728,12 +769,14 @@ "automaticallyProceedToNextGameAfterMoving": "Аўтаматычна пераходзіць да наступнай гульні пасля ходу", "autoSwitch": "Аўтапераключэнне", "puzzles": "Задачы", + "onlineBots": "Анлайн боты", "name": "Назва", "description": "Апісанне", "descPrivate": "Прыватнае апісанне", "descPrivateHelp": "Дапамагчы зрабіць \"прыватнае апісанне\".", "no": "Не", "yes": "Так", + "website": "Вэб-сайт", "help": "Дапамога:", "createANewTopic": "Стварыць новую тэму", "topics": "Тэмы", @@ -752,7 +795,6 @@ "cheat": "Несумленная гульня", "troll": "Троль", "other": "Іншае", - "reportDescriptionHelp": "Пакіньце ніжэй спасылку на гульню (ці гульні) і патлумачце, што вас непакоіць у паводзінах гэтага карыстальніка. Не пішыце нешта кшталту «ён чмут!» – патлумачце, як вы прыйшлі да гэтага выніку. Мы хутчэй разбярэмся ў сітуацыі, калі вы напішаце нам па-англійску.", "error_provideOneCheatedGameLink": "Калі ласка, дадайце спасылку хаця б на адну гульню, дзе былі парушаны правілы.", "by": "аўтар: {param}", "importedByX": "Імпартваў/-ла {param}", @@ -785,6 +827,7 @@ "slow": "Павольная", "insideTheBoard": "Унутры дошкі", "outsideTheBoard": "Па-за дошкай", + "allSquaresOfTheBoard": "Усе клеткі на дошцы", "onSlowGames": "У павольных гульнях", "always": "Заўседы", "never": "Ніколі", @@ -948,6 +991,11 @@ "transparent": "Празрысты", "deviceTheme": "Тэма прылады", "backgroundImageUrl": "Спасылка на фон:", + "board": "Дошка", + "size": "Размер", + "opacity": "Празрыстасць", + "brightness": "Яркасць", + "hue": "Адценне", "pieceSet": "Набор фігур", "embedInYourWebsite": "Убудаваць у свой сайт", "usernameAlreadyUsed": "Гэтае імя карыстальніка ўжо занятае. Калі ласка, паспрабуйце іншае.", @@ -1214,6 +1262,159 @@ "stormXRuns": "{count, plural, =1{1 спроба} few{{count} спробы} many{{count} спробаў} other{{count} спробаў}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Выкарастана адна з {param2} спроб} few{Выкарастана {count} з {param2} спроб} many{Выкарастана {count} з {param2} спробаў} other{Выкарастана {count} з {param2} спробаў}}", "streamerLichessStreamers": "Стрымеры на Lichess", + "studyPrivate": "Прыватны", + "studyMyStudies": "Мае навучанні", + "studyStudiesIContributeTo": "Навучанні, якія я рэдагую", + "studyMyPublicStudies": "Мае публічныя навучанні", + "studyMyPrivateStudies": "Мае прыватные навучанні", + "studyMyFavoriteStudies": "Мае ўлюбленые навучанні", + "studyWhatAreStudies": "Што такое навучанні?", + "studyAllStudies": "Усе навучанні", + "studyStudiesCreatedByX": "Навучанні, створаныя {param}", + "studyNoneYet": "Пакуль нічога няма.", + "studyHot": "Гарачыя", + "studyDateAddedNewest": "Дата дадання (навейшыя)", + "studyDateAddedOldest": "Дата дадання (старэйшыя)", + "studyRecentlyUpdated": "Нядаўна абноўленыя", + "studyMostPopular": "Найбольш папулярныя", + "studyAlphabetical": "Па алфавіце", + "studyAddNewChapter": "Дадаць новы раздзел", + "studyAddMembers": "Дадаць удзельнікаў", + "studyInviteToTheStudy": "Закліцца да навучання", + "studyPleaseOnlyInvitePeopleYouKnow": "Калі ласка, заклікайце толькі людзей, якіх вы ведаеце, та тых хто актыўна хоча далучыцца да навучання.", + "studySearchByUsername": "Шукаць па імені карыстальніка", + "studySpectator": "Глядач", + "studyContributor": "Рэдактар", + "studyKick": "Выдаліць", + "studyLeaveTheStudy": "Пакінуць навучанне", + "studyYouAreNowAContributor": "Вы цяпер рэдактар", + "studyYouAreNowASpectator": "Вы цяпер глядач", + "studyPgnTags": "Тэгі PGN", + "studyLike": "Упадабаць", + "studyUnlike": "Разпадабаць", + "studyNewTag": "Новы тэг", + "studyCommentThisPosition": "Каментаваць пазіцыю", + "studyCommentThisMove": "Каментаваць гэты ход", + "studyAnnotateWithGlyphs": "Дадаць знакавую анатацыю", + "studyTheChapterIsTooShortToBeAnalysed": "Раздел занадта кароткі для аналізу.", + "studyOnlyContributorsCanRequestAnalysis": "Толькі рэдактары навучання могуць запрасіць камп'ютарны аналіз.", + "studyGetAFullComputerAnalysis": "Атрымайце поўны серверны кампутарны аналіз галоўнай лініі.", + "studyMakeSureTheChapterIsComplete": "Пераканайцеся, што раздзел гатоў. Вы можаце запрасіць аналіз толькі адзін раз.", + "studyAllSyncMembersRemainOnTheSamePosition": "Усе сінхранізаваныя ўдзельнікі застаюцца на аднолькавай пазіцыі", + "studyShareChanges": "Падзяліцца зменамі з гледачамі та захаваць іх на серверы", + "studyPlaying": "Гуляецца", + "studyShowEvalBar": "Шкалы ацэнкі", + "studyFirst": "На першую", + "studyPrevious": "Папярэдняя", + "studyNext": "Наступная", + "studyLast": "На апошнюю", "studyShareAndExport": "Падзяліцца & экспартаваць", - "studyStart": "Пачаць" + "studyCloneStudy": "Кланаваць", + "studyStudyPgn": "PGN навучання", + "studyDownloadAllGames": "Спампаваць усе гульні", + "studyChapterPgn": "PGN раздзелу", + "studyCopyChapterPgn": "Скапіраваць PGN", + "studyDownloadGame": "Спампаваць гульню", + "studyStudyUrl": "URL навучання", + "studyCurrentChapterUrl": "URL бягучага раздзелу", + "studyYouCanPasteThisInTheForumToEmbed": "Вы можаце ўставіць гэта на форум, каб убудаваць", + "studyStartAtInitialPosition": "Пачынаць у пачатковай пазіцыі", + "studyStartAtX": "Пачынаць з {param}", + "studyEmbedInYourWebsite": "Убудаваць у свой сайт або блог", + "studyReadMoreAboutEmbedding": "Пачытаць больш пра ўбудаванне", + "studyOnlyPublicStudiesCanBeEmbedded": "Толькі публічныя навучанні могуць быць убудаваны!", + "studyOpen": "Адкрыць", + "studyXBroughtToYouByY": "{param2} зрабіў для вас {param1}", + "studyStudyNotFound": "Навучанне не знойдзена", + "studyEditChapter": "Рэдагаваць раздзел", + "studyNewChapter": "Новы раздзел", + "studyImportFromChapterX": "Імпартаваць з {param}", + "studyOrientation": "Арыентацыя дошкі", + "studyAnalysisMode": "Рэжым аналізу", + "studyPinnedChapterComment": "Замацаваны каментар раздзелу", + "studySaveChapter": "Захаваць раздзел", + "studyClearAnnotations": "Ачысціць анатацыі", + "studyClearVariations": "Ачысціць варыянты", + "studyDeleteChapter": "Выдаліць раздзел", + "studyDeleteThisChapter": "Выдаліць гэты раздел? Гэта нельга будзе адмяніць!", + "studyClearAllCommentsInThisChapter": "Выдаліць усе каментары, знакавыя анатацыі і намаляваныя фігуры ў гэтым раздзеле?", + "studyRightUnderTheBoard": "Адразу пад дошкай", + "studyNoPinnedComment": "Ніякіх", + "studyNormalAnalysis": "Звычайны аналіз", + "studyHideNextMoves": "Схаваць наступныя хады", + "studyInteractiveLesson": "Інтэрактыўны занятак", + "studyChapterX": "Раздзел {param}", + "studyEmpty": "Пуста", + "studyStartFromInitialPosition": "Пачынаць з пачатковай пазіцыі", + "studyEditor": "Рэдактар", + "studyStartFromCustomPosition": "Пачынаць з абранай пазіцыі", + "studyLoadAGameByUrl": "Загрузіць гульні па URLs", + "studyLoadAPositionFromFen": "Загрузіць пазіцыю з FEN", + "studyLoadAGameFromPgn": "Загрузіць гульні з PGN", + "studyAutomatic": "Аўтаматычна", + "studyUrlOfTheGame": "URL гульняў, адзін на радок", + "studyLoadAGameFromXOrY": "Загрузіць партыі з {param1} або {param2}", + "studyCreateChapter": "Стварыць раздзел", + "studyCreateStudy": "Стварыць навучанне", + "studyEditStudy": "Рэдактаваць навучанне", + "studyVisibility": "Бачнасць", + "studyPublic": "Публічны", + "studyUnlisted": "Нябачны", + "studyInviteOnly": "Толькі па запрашэннях", + "studyAllowCloning": "Дазволіць кланаванне", + "studyNobody": "Ніхто", + "studyOnlyMe": "Толькі я", + "studyContributors": "Рэдактары", + "studyMembers": "Удзельнікі", + "studyEveryone": "Кожны", + "studyEnableSync": "Уключыць сінхранізацыю", + "studyYesKeepEveryoneOnTheSamePosition": "Так: трымаць усіх на аднолькавай пазіцыі", + "studyNoLetPeopleBrowseFreely": "Не: хай людзі вольна праглядаюць пазіцыі", + "studyPinnedStudyComment": "Замацаваць каментар да занятку", + "studyStart": "Пачаць", + "studySave": "Захаваць", + "studyClearChat": "Ачысціць чат", + "studyDeleteTheStudyChatHistory": "Выдаліць гісторыю чата навучання цалкам? Гэта нельга будзе адмяніць!", + "studyDeleteStudy": "Выдаліць навучанне", + "studyConfirmDeleteStudy": "Выдаліць навучанне поўнасцю? Гэта нельга будзе адмяніць! Увядзіце назву навучання каб падцвердзіць: {param}", + "studyWhereDoYouWantToStudyThat": "Дзе вы жадаеце навучацца?", + "studyGoodMove": "Добры ход", + "studyMistake": "Памылка", + "studyBrilliantMove": "Бліскучы ход", + "studyBlunder": "Позех", + "studyInterestingMove": "Цікавы ход", + "studyDubiousMove": "Сумнеўны ход", + "studyOnlyMove": "Адзіны ход", + "studyZugzwang": "Цугцванг", + "studyEqualPosition": "Раўная пазіцыя", + "studyUnclearPosition": "Незразумелая пазіцыя", + "studyWhiteIsSlightlyBetter": "У белых трошкі лепш", + "studyBlackIsSlightlyBetter": "У чорных трошкі лепш", + "studyWhiteIsBetter": "У белых лепш", + "studyBlackIsBetter": "У чорных лепш", + "studyWhiteIsWinning": "Белыя перамагаюць", + "studyBlackIsWinning": "Чорныя перамагаюць", + "studyNovelty": "Новаўвядзенне", + "studyDevelopment": "Развіццё", + "studyInitiative": "Ініцыятыва", + "studyAttack": "Напад", + "studyCounterplay": "Контргульня", + "studyTimeTrouble": "Цэйтнот", + "studyWithCompensation": "З кампенсацыяй", + "studyWithTheIdea": "З ідэяй", + "studyNextChapter": "Наступны раздзел", + "studyPrevChapter": "Папярэдні раздзел", + "studyStudyActions": "Дзеянні ў навучанні", + "studyTopics": "Тэмы", + "studyMyTopics": "Мае тэмы", + "studyPopularTopics": "Папулярныя тэмы", + "studyManageTopics": "Кіраваць тэмамі", + "studyBack": "Назад", + "studyPlayAgain": "Гуляць зноў", + "studyWhatWouldYouPlay": "Як бы вы пахадзілі ў гэтай пазіцыі?", + "studyYouCompletedThisLesson": "Віншуем! Вы прайшлі гэты ўрок.", + "studyNbChapters": "{count, plural, =1{{count} раздзел} few{{count} раздзелы} many{{count} раздзелаў} other{{count} раздзелаў}}", + "studyNbGames": "{count, plural, =1{{count} партыя} few{{count} партыі} many{{count} партый} other{{count} партый}}", + "studyNbMembers": "{count, plural, =1{{count} удзельнік} few{{count} удзельніка} many{{count} удзельнікаў} other{{count} удзельнікаў}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Устаўце сюды ваш PGN тэкст, не больш за {count} гульню} few{Устаўце сюды ваш PGN тэкст, не больш за {count} гульні} many{Устаўце сюды ваш PGN тэкст, не больш за {count} гульняў} other{Устаўце сюды ваш PGN тэкст, не больш за {count} гульняў}}" } \ No newline at end of file diff --git a/lib/l10n/lila_bg.arb b/lib/l10n/lila_bg.arb index 18d2310883..903fbdb4bf 100644 --- a/lib/l10n/lila_bg.arb +++ b/lib/l10n/lila_bg.arb @@ -23,6 +23,11 @@ "mobileHideVariation": "Скрий вариацията", "mobileShowComments": "Покажи коментарите", "mobileSomethingWentWrong": "Възникна грешка.", + "mobileShowResult": "Покажи резултат", + "mobilePuzzleThemesSubtitle": "Решавайте задачи от любимите Ви дебюти или изберете друга тема.", + "mobilePuzzleStormSubtitle": "Решете колкото можете повече задачи за 3 минути.", + "mobileGreeting": "Здравейте, {param}", + "mobileGreetingWithoutName": "Здравейте", "activityActivity": "Дейност", "activityHostedALiveStream": "Стартира предаване на живо", "activityRankedInSwissTournament": "Рейтинг #{param1} от {param2}", @@ -35,6 +40,7 @@ "activityPlayedNbMoves": "{count, plural, =1{Изигра {count} ход} other{Изигра {count} хода}}", "activityInNbCorrespondenceGames": "{count, plural, =1{в {count} кореспондентска игра} other{в {count} кореспондентски игри}}", "activityCompletedNbGames": "{count, plural, =1{Завърши {count} кореспондентна игра} other{Завършил {count} кореспондентни игри}}", + "activityCompletedNbVariantGames": "{count, plural, =1{Изиграна {count} {param2} кореспондентска игра} other{Изиграни {count} {param2} кореспондентски игри}}", "activityFollowedNbPlayers": "{count, plural, =1{Последва {count} играч} other{Следва {count} играчи}}", "activityGainedNbFollowers": "{count, plural, =1{Прибави {count} нов последовател} other{Прибави {count} нови последователи}}", "activityHostedNbSimuls": "{count, plural, =1{Бе домакин на {count} шахматен сеанс} other{Домакин на {count} шахматни сеанса}}", @@ -45,7 +51,36 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{Участва в {count} турнир по швейцарската система} other{Участва в {count} турнира по швейцарската система}}", "activityJoinedNbTeams": "{count, plural, =1{Присъедини се в {count} отбор} other{Присъедини се в {count} отбори}}", "broadcastBroadcasts": "Излъчване", + "broadcastMyBroadcasts": "Моите излъчвания", "broadcastLiveBroadcasts": "Излъчвания на турнир на живо", + "broadcastBroadcastCalendar": "Календар на излъчванията", + "broadcastNewBroadcast": "Нови предавания на живо", + "broadcastSubscribedBroadcasts": "Излчвания които следя", + "broadcastAddRound": "Добави рунд", + "broadcastOngoing": "Текущи", + "broadcastUpcoming": "Предстоящи", + "broadcastCompleted": "Завършени", + "broadcastRoundName": "Име на рунда", + "broadcastRoundNumber": "Номер на рунда", + "broadcastTournamentName": "Име на турнира", + "broadcastTournamentDescription": "Кратко описание на турнира", + "broadcastFullDescription": "Пълно описание на събитието", + "broadcastFullDescriptionHelp": "Незадължително дълго описание на излъчването. {param1} е налично. Дължината трябва да по-малка от {param2} знака.", + "broadcastSourceUrlHelp": "Уебадресът, който Lichess ще проверява, за да получи осъвременявания на PGN. Той трябва да е публично достъпен от интернет.", + "broadcastStartDateHelp": "По избор, ако знаете, кога започва събитието", + "broadcastCurrentGameUrl": "URL на настоящата партия", + "broadcastDownloadAllRounds": "Изтегли всички рундове", + "broadcastResetRound": "Нулирай този рунд", + "broadcastDeleteRound": "Изтрий този рунд", + "broadcastDefinitivelyDeleteRound": "Окончателно изтрийте този рунд и всичките му игри.", + "broadcastDeleteAllGamesOfThisRound": "Изтрийте този рунд и всичките му игри. Източникът трябва да е активен за да можете да ги възстановите.", + "broadcastDeleteTournament": "Изтрий този турнир", + "broadcastDefinitivelyDeleteTournament": "Окончателно изтрий целия турнир, всичките му рундове и игри.", + "broadcastReplacePlayerTags": "По избор: промени имената на играчите, рейтингите и титлите", + "broadcastFideFederations": "ФИДЕ федерации", + "broadcastFideProfile": "ФИДЕ профил", + "broadcastFederation": "Федерация", + "broadcastNbBroadcasts": "{count, plural, =1{{count} излъчване} other{{count} излъчвания}}", "challengeChallengesX": "Предизвикателства: {param1}", "challengeChallengeToPlay": "Предизвикайте на партия", "challengeChallengeDeclined": "Предизвикателството е отказано", @@ -364,8 +399,8 @@ "puzzleThemeXRayAttackDescription": "Фигура атакува или защитава поле зад противникова фигура.", "puzzleThemeZugzwang": "Цугцванг", "puzzleThemeZugzwangDescription": "Опонентът има малко възможни ходове и всеки един от тях води до влошаване на положението му.", - "puzzleThemeHealthyMix": "От всичко по малко", - "puzzleThemeHealthyMixDescription": "По малко от всичко. Не знаете какво да очаквате, така че бъдете готови за всичко! Точно като в истинските игри.", + "puzzleThemeMix": "От всичко по малко", + "puzzleThemeMixDescription": "По малко от всичко. Не знаете какво да очаквате, така че бъдете готови за всичко! Точно като в истинските игри.", "puzzleThemePlayerGames": "Партии на играча", "puzzleThemePlayerGamesDescription": "Разгледайте пъзели генерирани от вашите игри, или игрите на други играчи.", "puzzleThemePuzzleDownloadInformation": "Тези пъзели са публични и могат да бъдат изтеглени от {param}.", @@ -496,7 +531,6 @@ "memory": "Памет", "infiniteAnalysis": "Неограничен анализ", "removesTheDepthLimit": "Анализът ще е безкраен, а компютърът ви - топъл", - "engineManager": "Мениджър на двигателя", "blunder": "Груба грешка", "mistake": "Грешка", "inaccuracy": "Неточност", @@ -578,6 +612,7 @@ "rank": "Класация", "rankX": "Класация: {param}", "gamesPlayed": "Изиграни игри", + "ok": "ОК", "cancel": "Отказ", "whiteTimeOut": "Времето на белите изтече", "blackTimeOut": "Времето на черните изтече", @@ -795,7 +830,6 @@ "cheat": "Измама", "troll": "Вредител", "other": "Друго", - "reportDescriptionHelp": "Поставете линк към играта и обяснете какъв е проблемът с поведението на този потребител. Не казвайте единствено, че мами, но ни кажете как сте стигнали до този извод. Вашият доклад ще бъде обработен по-бързо, ако е написан на английски.", "error_provideOneCheatedGameLink": "Моля дай поне един линк до измамна игра.", "by": "от {param}", "importedByX": "Внесени от {param}", @@ -1280,6 +1314,158 @@ "stormXRuns": "{count, plural, =1{1 опит} other{{count} опита}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Изигран един опит от {param2}} other{Изиграни {count} опита от {param2}}}", "streamerLichessStreamers": "Lichess стриймъри", + "studyPrivate": "Лични", + "studyMyStudies": "Моите казуси", + "studyStudiesIContributeTo": "Казуси, към които допринасям", + "studyMyPublicStudies": "Моите публични казуси", + "studyMyPrivateStudies": "Моите лични казуси", + "studyMyFavoriteStudies": "Моите любими казуси", + "studyWhatAreStudies": "Какво представляват казусите?", + "studyAllStudies": "Всички казуси", + "studyStudiesCreatedByX": "Казуси от {param}", + "studyNoneYet": "Все още няма.", + "studyHot": "Популярни", + "studyDateAddedNewest": "Дата на добавяне (най-нови)", + "studyDateAddedOldest": "Дата на добавяне (най-стари)", + "studyRecentlyUpdated": "Скоро обновени", + "studyMostPopular": "Най-популярни", + "studyAlphabetical": "Азбучно", + "studyAddNewChapter": "Добавяне на нов раздел", + "studyAddMembers": "Добави членове", + "studyInviteToTheStudy": "Покани към казуса", + "studyPleaseOnlyInvitePeopleYouKnow": "Моля канете само хора, които познавате и които биха искали да се присъединят.", + "studySearchByUsername": "Търсене по потребителско име", + "studySpectator": "Зрител", + "studyContributor": "Сътрудник", + "studyKick": "Изритване", + "studyLeaveTheStudy": "Напусни казуса", + "studyYouAreNowAContributor": "Вие сте сътрудник", + "studyYouAreNowASpectator": "Вие сте зрител", + "studyPgnTags": "PGN тагове", + "studyLike": "Харесай", + "studyUnlike": "Не харесвам", + "studyNewTag": "Нов таг", + "studyCommentThisPosition": "Коментирай позицията", + "studyCommentThisMove": "Коментирай хода", + "studyAnnotateWithGlyphs": "Анотация със специални символи", + "studyTheChapterIsTooShortToBeAnalysed": "Тази глава е твърде къса и не може да бъде анализирана.", + "studyOnlyContributorsCanRequestAnalysis": "Само сътрудници към казуса могат да пускат компютърен анализ.", + "studyGetAFullComputerAnalysis": "Вземи пълен сървърен анализ на основна линия.", + "studyMakeSureTheChapterIsComplete": "Уверете се, че главата е завършена. Можете да пуснете анализ само веднъж.", + "studyAllSyncMembersRemainOnTheSamePosition": "Всички синхронизирани членове остават на същата позиция", + "studyShareChanges": "Споделете промените със зрителите и ги запазете на сървъра", + "studyPlaying": "Играе се", + "studyFirst": "Първа", + "studyPrevious": "Предишна", + "studyNext": "Следваща", + "studyLast": "Последна", "studyShareAndExport": "Сподели", - "studyStart": "Начало" + "studyCloneStudy": "Клонирай", + "studyStudyPgn": "PGN на казуса", + "studyDownloadAllGames": "Изтегли всички партии", + "studyChapterPgn": "PGN на главата", + "studyCopyChapterPgn": "Копирай PGN", + "studyDownloadGame": "Изтегли партия", + "studyStudyUrl": "URL на казуса", + "studyCurrentChapterUrl": "URL на настоящата глава", + "studyYouCanPasteThisInTheForumToEmbed": "Можете да поставите това във форум и ще бъде вградено", + "studyStartAtInitialPosition": "Започни от начална позиция", + "studyStartAtX": "Започни от {param}", + "studyEmbedInYourWebsite": "Вгради в твоя сайт или блог", + "studyReadMoreAboutEmbedding": "Прочети повече за вграждането", + "studyOnlyPublicStudiesCanBeEmbedded": "Само публични казуси могат да бъдат вграждани!", + "studyOpen": "Отвори", + "studyXBroughtToYouByY": "{param1}, предоставени от {param2}", + "studyStudyNotFound": "Казусът не бе открит", + "studyEditChapter": "Промени глава", + "studyNewChapter": "Нова глава", + "studyImportFromChapterX": "Импортиране от {param}", + "studyOrientation": "Ориентация", + "studyAnalysisMode": "Режим на анализ", + "studyPinnedChapterComment": "Коментар на главата", + "studySaveChapter": "Запази глава", + "studyClearAnnotations": "Изтрий анотациите", + "studyClearVariations": "Изчисти вариациите", + "studyDeleteChapter": "Изтрий глава", + "studyDeleteThisChapter": "Изтриване на главата? Това е необратимо!", + "studyClearAllCommentsInThisChapter": "Изтрий всички коментари, специални символи и нарисувани форми в главата?", + "studyRightUnderTheBoard": "Точно под дъската", + "studyNoPinnedComment": "Никакви", + "studyNormalAnalysis": "Нормален анализ", + "studyHideNextMoves": "Скриване на следващите ходове", + "studyInteractiveLesson": "Интерактивен урок", + "studyChapterX": "Глава: {param}", + "studyEmpty": "Празна", + "studyStartFromInitialPosition": "Започни от начална позиция", + "studyEditor": "Редактор", + "studyStartFromCustomPosition": "Започни от избрана позиция", + "studyLoadAGameByUrl": "Зареди партии от URL", + "studyLoadAPositionFromFen": "Зареди позиция от FEN", + "studyLoadAGameFromPgn": "Зареди партии от PGN", + "studyAutomatic": "Автоматичен", + "studyUrlOfTheGame": "URL на партиите, по една на линия", + "studyLoadAGameFromXOrY": "Зареди партии от {param1} или {param2}", + "studyCreateChapter": "Създай", + "studyCreateStudy": "Създай казус", + "studyEditStudy": "Редактирай казус", + "studyVisibility": "Видимост", + "studyPublic": "Публични", + "studyUnlisted": "Несподелени", + "studyInviteOnly": "Само с покани", + "studyAllowCloning": "Позволи клониране", + "studyNobody": "Никой", + "studyOnlyMe": "Само за мен", + "studyContributors": "Сътрудници", + "studyMembers": "Членове", + "studyEveryone": "Всички", + "studyEnableSync": "Разреши синхронизиране", + "studyYesKeepEveryoneOnTheSamePosition": "Да: дръж всички на същата позиция", + "studyNoLetPeopleBrowseFreely": "Не: позволи свободно разглеждане", + "studyPinnedStudyComment": "Коментар на казуса", + "studyStart": "Начало", + "studySave": "Запази", + "studyClearChat": "Изтрий чат съобщенията", + "studyDeleteTheStudyChatHistory": "Изтриване на чат историята? Това е необратимо!", + "studyDeleteStudy": "Изтрий казуса", + "studyConfirmDeleteStudy": "Изтриване на целия казус? Това е необратимо! Въведете името на казуса за да потвърдите: {param}", + "studyWhereDoYouWantToStudyThat": "Къде да бъде проучено това?", + "studyGoodMove": "Добър ход", + "studyMistake": "Грешка", + "studyBrilliantMove": "Отличен ход", + "studyBlunder": "Груба грешка", + "studyInterestingMove": "Интересен ход", + "studyDubiousMove": "Съмнителен ход", + "studyOnlyMove": "Единствен ход", + "studyZugzwang": "Цугцванг", + "studyEqualPosition": "Равна позиция", + "studyUnclearPosition": "Неясна позиция", + "studyWhiteIsSlightlyBetter": "Белите са малко по-добре", + "studyBlackIsSlightlyBetter": "Черните са малко по-добре", + "studyWhiteIsBetter": "Белите са по-добре", + "studyBlackIsBetter": "Черните са по-добре", + "studyWhiteIsWinning": "Белите печелят", + "studyBlackIsWinning": "Черните печелят", + "studyNovelty": "Нововъведeние", + "studyDevelopment": "Развитие", + "studyInitiative": "Инициатива", + "studyAttack": "Атака", + "studyCounterplay": "Контра атака", + "studyTimeTrouble": "Проблем с времето", + "studyWithCompensation": "С компенсация", + "studyWithTheIdea": "С идеята", + "studyNextChapter": "Следваща глава", + "studyPrevChapter": "Предишна глава", + "studyStudyActions": "Опции за учене", + "studyTopics": "Теми", + "studyMyTopics": "Моите теми", + "studyPopularTopics": "Популярни теми", + "studyManageTopics": "Управление на темите", + "studyBack": "Обратно", + "studyPlayAgain": "Играйте отново", + "studyWhatWouldYouPlay": "Какво бихте играли в тази позиция?", + "studyYouCompletedThisLesson": "Поздравления! Вие завършихте този урок.", + "studyNbChapters": "{count, plural, =1{{count} Глава} other{{count} Глави}}", + "studyNbGames": "{count, plural, =1{{count} Игра} other{{count} Игри}}", + "studyNbMembers": "{count, plural, =1{{count} Член} other{{count} Членове}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Постави твоя PGN текст тук, до {count} партия} other{Постави твоя PGN текст тук, до {count} партии}}" } \ No newline at end of file diff --git a/lib/l10n/lila_bn.arb b/lib/l10n/lila_bn.arb index 2773f19bca..34b1ccc246 100644 --- a/lib/l10n/lila_bn.arb +++ b/lib/l10n/lila_bn.arb @@ -22,6 +22,13 @@ "activityJoinedNbTeams": "{count, plural, =1{যুক্ত হয়েছে {count} দল} other{যুক্ত হয়েছে {count} দল}}", "broadcastBroadcasts": "সম্প্রচার", "broadcastLiveBroadcasts": "সরাসরি টুর্নামেন্ট সম্প্রচার", + "broadcastNewBroadcast": "নতুন সরাসরি সম্প্রচার", + "broadcastOngoing": "চলমান", + "broadcastUpcoming": "আসন্ন", + "broadcastCompleted": "সমাপ্ত", + "broadcastRoundNumber": "গোল নম্বর", + "broadcastFullDescription": "ইভেন্টের সম্পূর্ণ বিবরণ", + "broadcastSourceUrlHelp": "ইউআরএল যা লাইসেন্সেস পিজিএন আপডেট পেতে চেক করবে। এটি অবশ্যই ইন্টারনেট থেকে সর্বজনীনভাবে অ্যাক্সেসযোগ্য।.", "challengeChallengesX": "প্রতিদ্বন্দ্বীরা:{param1}", "challengeChallengeToPlay": "খেলার জন্য চ্যালেঞ্জ করুন", "challengeChallengeDeclined": "চ্যালেঞ্জ প্রত্যাখ্যান করা হয়েছে", @@ -285,8 +292,8 @@ "puzzleThemeXRayAttackDescription": "একটি গুটি যখন প্রতিপক্ষের গুটির ভিতর দিয়ে কোনো ঘরকে রক্ষা বা আক্রমন করে।", "puzzleThemeZugzwang": "জুগজওয়াং(Zugzwang)", "puzzleThemeZugzwangDescription": "প্রতিপক্ষের সীমিত চাল আছে, এবং সব চাল তাদের অবস্থান আরো খারাপ করবে।", - "puzzleThemeHealthyMix": "পরিমিত মিশ্রণ", - "puzzleThemeHealthyMixDescription": "সবকিছু একটু করে। আপনি জানবেন না কি আসতে চলেছে। অনেকটা বাস্তব খেলার মতো।", + "puzzleThemeMix": "পরিমিত মিশ্রণ", + "puzzleThemeMixDescription": "সবকিছু একটু করে। আপনি জানবেন না কি আসতে চলেছে। অনেকটা বাস্তব খেলার মতো।", "puzzleThemePlayerGames": "খেলোয়ারদের খেলা হতে", "puzzleThemePlayerGamesDescription": "খেলোয়ারদের খেলা থেকে বাছাই করে তৈরি করা ধাঁধা।", "puzzleThemePuzzleDownloadInformation": "এই ধাঁধা গুলো সবার জন্য উন্মুক্ত এবং {param} থেকে নামিয়ে নেয়া যাবে।", @@ -365,6 +372,7 @@ "promoteVariation": "বিজ্ঞাপন", "makeMainLine": "প্রাধান সীমা করা", "deleteFromHere": "এখান থেকে মুছুন", + "collapseVariations": "ভেরিয়েশন সঙ্কুচিত করুন", "forceVariation": "জোর করে পরিবর্তন", "move": "চাল", "variantLoss": "ভিন্ন ক্ষতি", @@ -412,7 +420,6 @@ "memory": "মেমরি", "infiniteAnalysis": "অনন্ত বিশ্লেষণ", "removesTheDepthLimit": "গভীরতার সীমা অপসারণ করুন এবং আপনার কম্পিউটারকে গরম রাখুন", - "engineManager": "ইঞ্জিন ম্যানেজার", "blunder": "গুরুতর ভুল", "mistake": "ভুল", "inaccuracy": "অশুদ্ধি", @@ -438,6 +445,7 @@ "latestForumPosts": "সর্বশেষ ফোরাম বার্তা", "players": "খেলোয়াড়", "friends": "বন্ধুরা", + "otherPlayers": "অন্যান্য খেলোয়াড়", "discussions": "বার্তাগুলি", "today": "আজ", "yesterday": "গতকাল", @@ -693,6 +701,7 @@ "descPrivateHelp": "যে লেখা শুধু দলের সদস্য দেখতে পাবে.", "no": "না", "yes": "হ্যা", + "mobile": "মোবাইল", "help": "সাহায্য", "createANewTopic": "নতুন বিষয় তৈরি করুন", "topics": "বিষয়", @@ -711,7 +720,6 @@ "cheat": "চিটিং করছে", "troll": "ব্যঙ্গ করছে", "other": "অন্য কোনো কারণ", - "reportDescriptionHelp": "এখানে সেই খেলাটির link দেন এবং বলুন ওই ব্যক্তি ব্যবহারে কি অসুবিধা ছিল ?", "error_provideOneCheatedGameLink": "অনুগ্রহ করে একটা চিটেড গেমের লিংক দিন।", "by": "{param} এর দ্বারা", "importedByX": "খেলাটি এনেছেন {param}", @@ -1169,5 +1177,94 @@ "stormClickToReload": "পুনঃলোড করতে ক্লিক করুন", "stormXRuns": "{count, plural, =1{১ দউর} other{{count} দউর}}", "streamerLichessStreamers": "লিছেসস স্ত্রেয়ামের", - "studyStart": "শুরু করুন" + "studyPrivate": "ব্যাক্তিগত", + "studyMyStudies": "আমার অধ্যায়ন", + "studyStudiesIContributeTo": "যেসকল অধ্যায়নে আমার অবদান রয়েছে", + "studyMyPublicStudies": "জনসাধারনকৃত আমার অধ্যায়নগুলো", + "studyMyPrivateStudies": "আমার ব্যাক্তিগত অধ্যায়ন", + "studyMyFavoriteStudies": "আমার পছন্দের অধ্যায়ন", + "studyWhatAreStudies": "অধ্যায়ন কি?", + "studyAllStudies": "সকল অধ্যায়নগুলি", + "studyStudiesCreatedByX": "অধ্যায়ন তৈরি করেছেন {param}", + "studyNoneYet": "আপাতত নেই।", + "studyHot": "গরমাগরম", + "studyDateAddedNewest": "তৈরির তারিখ (সবচেয়ে নতুন)", + "studyDateAddedOldest": "তৈরির তারিখ (সবচেয়ে পুরনো)", + "studyRecentlyUpdated": "সাম্প্রতিক হালনাগাদকৃত", + "studyMostPopular": "সবচেয়ে জনপ্রিয়", + "studyAlphabetical": "বর্ণানুক্রমিক", + "studyAddNewChapter": "নতুন অধ্যায় যোগ করুন", + "studyAddMembers": "সদস্য যোগ করুন", + "studyInviteToTheStudy": "স্টাডিতে আমন্ত্রণ জানান", + "studyPleaseOnlyInvitePeopleYouKnow": "দয়া করে যাদের আপনি জানেন তাদের এবং যারা সক্রিয়ভাবে যোগদান করতে চায়, কেবল তাদেরকেই আমন্ত্রন জানান।", + "studySearchByUsername": "ইউজারনেম দ্বারা খুঁজুন", + "studySpectator": "দর্শক", + "studyContributor": "অবদানকারী", + "studyKick": "লাথি দিয়ে বের করুন", + "studyYouAreNowASpectator": "আপনি এখন দর্শক", + "studyPgnTags": "PGN ট্যাগ", + "studyLike": "পছন্দ করা", + "studyUnlike": "পছন্দ নয়", + "studyNewTag": "নতুন ট্যাগ", + "studyTheChapterIsTooShortToBeAnalysed": "এনালাইসিস করার জন্য চ্যাপ্টারটা খুব ছোট", + "studyOnlyContributorsCanRequestAnalysis": "শুধুমাত্র স্টাডি'টার কন্ট্রিবিউটররাই কম্পিউটার এনালাইসিস এর রিকোয়েস্ট করতে পারবে।", + "studyPlaying": "খেলছে", + "studyFirst": "সর্ব প্রথম", + "studyPrevious": "আগের ধাপ", + "studyNext": "পরের ধাপ", + "studyLast": "সর্বশেষ", + "studyStudyPgn": "অধ্যায়ন PGN আকারে", + "studyDownloadAllGames": "ডাউনলোড করুন সকল গেম", + "studyOpen": "ওপেন", + "studyStartFromCustomPosition": "নির্দিষ্ট অবস্থান থেকে শুরু করুন", + "studyLoadAGameByUrl": "URL থেকে খেলা লোড করুন", + "studyLoadAPositionFromFen": "FEN থেকে একটি অবস্থান লোড করুন", + "studyLoadAGameFromPgn": "PGN থেকে খেলা লোড করুন", + "studyAutomatic": "স্বয়ংক্রিয়", + "studyUrlOfTheGame": "খেলাগুলোর URL, লাইনপ্রতি একটি", + "studyLoadAGameFromXOrY": "{param1} অথবা {param2} থেকে খেলাসমূহ লোড করুন", + "studyCreateChapter": "অধ্যায় তৈরি করুন", + "studyCreateStudy": "স্টাডি তৈরি করুন", + "studyEditStudy": "স্টাডি সম্পাদনা করুন", + "studyVisibility": "দৃশ্যমানতা", + "studyPublic": "পাবলিক", + "studyUnlisted": "প্রাইভেট", + "studyInviteOnly": "কেবল আমন্ত্রনভিত্তিক", + "studyAllowCloning": "ক্লোন করার অনুমতি দিন", + "studyNobody": "কেউ না", + "studyOnlyMe": "শুধু আমি", + "studyContributors": "অবদানকারীরা", + "studyMembers": "সদস্যবৃন্দ", + "studyEveryone": "সবাই", + "studyEnableSync": "সাইনক চালু করুন", + "studyYesKeepEveryoneOnTheSamePosition": "সবাইকে একই অবস্থানে রাখুন", + "studyNoLetPeopleBrowseFreely": "না: মানুষকে মুক্তভাবে ব্রাউজ করতে দিন", + "studyPinnedStudyComment": "পিন করা স্টাডি মন্তব্য", + "studyStart": "শুরু করুন", + "studySave": "সংরক্ষন করুন", + "studyClearChat": "চ্যাট পরিষ্কার করুন", + "studyDeleteTheStudyChatHistory": "স্টাডি চ্যাটের ইতিহাস মুছে ফেলবেন? এটা কিন্তু ফিরে আসবে না!", + "studyDeleteStudy": "স্টাডি মুছে ফেলুন", + "studyWhereDoYouWantToStudyThat": "আপনি কোথায় এটা চর্চা করবেন?", + "studyGoodMove": "ভালো চাল", + "studyMistake": "ভূল চাল", + "studyBrilliantMove": "অসাধারণ চাল", + "studyBlunder": "ব্লান্ডার", + "studyInterestingMove": "আগ্রহোদ্দীপক চাল", + "studyDubiousMove": "অনিশ্চিত চাল", + "studyOnlyMove": "একমাত্র সম্ভাব্য চাল", + "studyZugzwang": "যুগযোয়াং", + "studyEqualPosition": "সমান অবস্থান", + "studyUnclearPosition": "অনিশ্চিত অবস্থান", + "studyWhiteIsSlightlyBetter": "সাদা একটু বেশি ভালো", + "studyBlackIsSlightlyBetter": "কালো একটু বেশি ভালো", + "studyWhiteIsBetter": "সাদা ভালো", + "studyBlackIsBetter": "কালো ভালো", + "studyWhiteIsWinning": "সাদা জিতছে", + "studyBlackIsWinning": "কালো জিতছে", + "studyNovelty": "নোভেল্টি", + "studyNbChapters": "{count, plural, =1{{count}টি অধ্যায়} other{{count}টি অধ্যায়}}", + "studyNbGames": "{count, plural, =1{{count}টি খেলা} other{{count}টি খেলা}}", + "studyNbMembers": "{count, plural, =1{{count} জন সদস্য} other{{count} জন সদস্য}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{PGN টেক্সট এখানে পেস্ট করুন, {count} টি খেলা পর্যন্ত} other{PGN টেক্সট এখানে পেস্ট করুন, {count} টি খেলা পর্যন্ত}}" } \ No newline at end of file diff --git a/lib/l10n/lila_br.arb b/lib/l10n/lila_br.arb index a3f3cbed35..4ef366adb4 100644 --- a/lib/l10n/lila_br.arb +++ b/lib/l10n/lila_br.arb @@ -22,6 +22,17 @@ "activityJoinedNbTeams": "{count, plural, =1{Ezel eus {count} skipailh} =2{Ezel eus {count} skipailh} few{Ezel eus {count} skipailh} many{Ezel eus {count} skipailh} other{Ezel eus {count} skipailh}}", "broadcastBroadcasts": "War-eeun", "broadcastLiveBroadcasts": "Tournamantoù skignet war-eeun", + "broadcastNewBroadcast": "Skignañ war-eeun nevez", + "broadcastOngoing": "O ren", + "broadcastUpcoming": "A-benn nebeut", + "broadcastCompleted": "Tremenet", + "broadcastRoundNumber": "Niverenn ar batalm", + "broadcastFullDescription": "Deskrivadur an abadenn a-bezh", + "broadcastFullDescriptionHelp": "Deskrivadur hir ar skignañ war-eeun ma fell deoc'h.{param1} zo dijabl. Ne vo ket hiroc'h evit {param2} sin.", + "broadcastSourceUrlHelp": "An URL a ray Lichess ganti evit kaout hizivadurioù ar PGN. Ret eo dezhi bezañ digor d'an holl war Internet.", + "broadcastStartDateHelp": "Diret eo, ma ouzit pegoulz e krogo", + "broadcastDeleteTournament": "Dilemel an tournamant-mañ", + "broadcastDefinitivelyDeleteTournament": "Dilemel an tournamant da viken, an holl grogadoù ha pep tra penn-da-benn.", "challengeChallengesX": "Daeoù: {param1}", "challengeChallengeToPlay": "Daeañ ar c'hoarier-mañ", "challengeChallengeDeclined": "Dae nac’het", @@ -174,8 +185,8 @@ "puzzleThemeShort": "Poelladenn verr", "puzzleThemeVeryLong": "Poelladenn hir-tre", "puzzleThemeZugzwang": "Zugzwang", - "puzzleThemeHealthyMix": "A bep seurt", - "puzzleThemeHealthyMixDescription": "A bep seurt. N'ouzit ket petra gortoz hag e mod-se e voc'h prest evit pep tra! Heñvel ouzh ar c'hrogadoù gwir.", + "puzzleThemeMix": "A bep seurt", + "puzzleThemeMixDescription": "A bep seurt. N'ouzit ket petra gortoz hag e mod-se e voc'h prest evit pep tra! Heñvel ouzh ar c'hrogadoù gwir.", "searchSearch": "Klask", "settingsSettings": "Arventennoù", "settingsCloseAccount": "Serriñ ar gont", @@ -291,7 +302,6 @@ "cpus": "CPUs", "memory": "Memor", "infiniteAnalysis": "Dielfennañ didermen", - "engineManager": "Merañ an urzhiataer", "blunder": "Bourd", "mistake": "Fazi", "inaccuracy": "Diresisted", @@ -575,7 +585,6 @@ "cheat": "Trucherezh", "troll": "Troll", "other": "All", - "reportDescriptionHelp": "Pegit liamm ar c'hrogad(où) ha displegit ar pezh a ya a-dreuz gant emzalc'h oc'h enebour. Lâret \"o truchañ emañ\" ne vo ket trawalc'h, ret eo displegañ mat. Buanoc'h e pledimp ganti ma skrivit e saozneg.", "error_provideOneCheatedGameLink": "Roit d'an nebeutañ ul liamm hag a gas d'ur c'hrogad trucherezh ennañ.", "by": "gant {param}", "importedByX": "Enportzhiet gant {param}", @@ -1010,6 +1019,117 @@ "stormAllTime": "A-viskoazh", "stormXRuns": "{count, plural, =1{1 frapad} =2{{count} frapad} few{{count} frapad} many{{count} frapad} other{{count} frapad}}", "streamerLichessStreamers": "Streamerien Lichess", + "studyPrivate": "Prevez", + "studyMyStudies": "Ma studiadennoù", + "studyStudiesIContributeTo": "Studiadennoù am eus kemeret perzh enne", + "studyMyPublicStudies": "Ma studiadennoù foran", + "studyMyPrivateStudies": "Ma studiadennoù prevez", + "studyMyFavoriteStudies": "Ma studiadennoù muiañ-karet", + "studyWhatAreStudies": "Petra eo ar studiadennoù?", + "studyAllStudies": "An holl studiadennoù", + "studyStudiesCreatedByX": "Studiadennoù krouet gant {param}", + "studyNoneYet": "Hini ebet evit poent.", + "studyHot": "Deus ar c'hiz", + "studyDateAddedNewest": "Deiziad ouzhpennet (nevesañ)", + "studyDateAddedOldest": "Deiziad ouzhpennet (koshañ)", + "studyRecentlyUpdated": "Hizivaet a-nevez", + "studyMostPopular": "Muiañ karet", + "studyAddNewChapter": "Ouzhpennañ ur pennad", + "studyAddMembers": "Ouzhpennañ izili", + "studyInviteToTheStudy": "Pediñ d'ar studiadenn", + "studyPleaseOnlyInvitePeopleYouKnow": "Na bedit nemet tud a anavezit hag o deus c'hoant da gemer perzh da vat en ho studiadenn.", + "studySearchByUsername": "Klask dre anv implijer", + "studySpectator": "Arvester", + "studyContributor": "Perzhiad", + "studyKick": "Forbannañ", + "studyLeaveTheStudy": "Kuitaat ar studiadenn", + "studyYouAreNowAContributor": "Perzhiad oc'h bremañ", + "studyYouAreNowASpectator": "Un arvester oc'h bremañ", + "studyPgnTags": "Tikedennoù PGN", + "studyLike": "Plijet", + "studyNewTag": "Tikedenn nevez", + "studyCommentThisPosition": "Lâret ur ger diwar-benn al lakadur-mañ", + "studyCommentThisMove": "Ober un evezhiadenn diwar-benn ar fiñvadenn-mañ", + "studyAnnotateWithGlyphs": "Notennaouiñ gant arouezioù", + "studyTheChapterIsTooShortToBeAnalysed": "Re verr eo ar pennad evit bezañ dielfennet.", + "studyOnlyContributorsCanRequestAnalysis": "N'eus nemet perzhidi ar studiadenn a c'hall goulenn un dielfennañ urzhiataer.", + "studyGetAFullComputerAnalysis": "Kaout un dielfennañ klok eus ar bennlinenn graet gant un urzhiataer.", + "studyMakeSureTheChapterIsComplete": "Bezit sur eo klok ar pennad. Ne c'hallit goulenn un dielfennañ nemet ur wech.", + "studyAllSyncMembersRemainOnTheSamePosition": "Er memes lec'hiadur e chom holl izili ar SYNC", + "studyShareChanges": "Rannañ cheñchamantoù gant an arvesterien ha saveteiñ anezhe war ar servor", + "studyPlaying": "O c'hoari", + "studyFirst": "Kentañ", + "studyPrevious": "War-gil", + "studyNext": "War-lec'h", + "studyLast": "Diwezhañ", "studyShareAndExport": "Skignañ & ezporzhiañ", - "studyStart": "Kregiñ" + "studyCloneStudy": "Eilañ", + "studyStudyPgn": "PGN ar studi", + "studyDownloadAllGames": "Pellgargañ an holl grogadoù", + "studyChapterPgn": "PGN ar pennad", + "studyDownloadGame": "Pellgargañ ur c'hrogad", + "studyStudyUrl": "Studiañ URL", + "studyCurrentChapterUrl": "URL ar pennad evit poent", + "studyYouCanPasteThisInTheForumToEmbed": "Gallout a rit pegañ se er forom evit ensoc'hañ", + "studyStartAtInitialPosition": "Kregiñ el lec'hiadur kentañ", + "studyStartAtX": "Kregiñ e {param}", + "studyEmbedInYourWebsite": "Enframmañ en ho lec'hienn pe blog", + "studyReadMoreAboutEmbedding": "Goût hiroc'h diwar-benn an ensoc'hañ", + "studyOnlyPublicStudiesCanBeEmbedded": "Ar studiadennoù foran a c'hall bezañ ensoc'het!", + "studyOpen": "Digeriñ", + "studyXBroughtToYouByY": "{param1}, zo kaset deoc'h gant {param2}", + "studyStudyNotFound": "N'eo ket bet kavet ar studiadenn", + "studyEditChapter": "Aozañ ar pennad", + "studyNewChapter": "Pennad nevez", + "studyOrientation": "Tuadur", + "studyAnalysisMode": "Doare dielfennañ", + "studyPinnedChapterComment": "Ali war ar pennad spilhet", + "studySaveChapter": "Saveteiñ pennad", + "studyClearAnnotations": "Diverkañ an notennoù", + "studyDeleteChapter": "Dilemel pennad", + "studyDeleteThisChapter": "Dilemel ar pennad-mañ? Hep distro e vo!", + "studyClearAllCommentsInThisChapter": "Diverkañ an holl evezhiadennoù ha notennoù er pennad?", + "studyRightUnderTheBoard": "Dindan an dablez", + "studyNoPinnedComment": "Hini ebet", + "studyNormalAnalysis": "Dielfennañ normal", + "studyHideNextMoves": "Kuzhat ar fiñvadennoù da heul", + "studyInteractiveLesson": "Kentel etreoberiat", + "studyChapterX": "Pennad {param}", + "studyEmpty": "Goullo", + "studyStartFromInitialPosition": "Kregiñ el lec'hiadur kentañ", + "studyEditor": "Aozer", + "studyStartFromCustomPosition": "Kregiñ adalek ul lakadur aozet", + "studyLoadAGameByUrl": "Kargañ ur c'hrogad dre URL", + "studyLoadAPositionFromFen": "Kargañ ul lakadur dre FEN", + "studyLoadAGameFromPgn": "Kargañ ul lakadur dre PGN", + "studyAutomatic": "Emgefre", + "studyUrlOfTheGame": "URL ar c'hrogad", + "studyLoadAGameFromXOrY": "Kargañ ur c'hrogad eus {param1} pe {param2}", + "studyCreateChapter": "Krouiñ pennad", + "studyCreateStudy": "Krouiñ ur studiadenn", + "studyEditStudy": "Aozañ studiadenn", + "studyVisibility": "Gwelusted", + "studyPublic": "Foran", + "studyUnlisted": "N'eo ket bet listennet", + "studyInviteOnly": "Kouvidi hepken", + "studyAllowCloning": "Aotreañ ar c'hlonañ", + "studyNobody": "Den ebet", + "studyOnlyMe": "Me hepken", + "studyContributors": "Perzhidi", + "studyMembers": "Izili", + "studyEveryone": "An holl dud", + "studyEnableSync": "Gweredekaat sync", + "studyYesKeepEveryoneOnTheSamePosition": "Ya: laoskit an traoù evel m'emaint", + "studyNoLetPeopleBrowseFreely": "Nann: laoskit an dud merdeiñ trankilik", + "studyPinnedStudyComment": "Ali war ar studiadenn spilhet", + "studyStart": "Kregiñ", + "studySave": "Saveteiñ", + "studyClearChat": "Diverkañ ar flapañ", + "studyDeleteTheStudyChatHistory": "Dilemel an istor-flapañ? Hep distro e vo!", + "studyDeleteStudy": "Dilemel ar studiadenn", + "studyWhereDoYouWantToStudyThat": "Pelec'h ho peus c'hoant da studiañ se?", + "studyNbChapters": "{count, plural, =1{{count} pennad} =2{{count} pennad} few{{count} pennad} many{{count} pennad} other{{count} pennad}}", + "studyNbGames": "{count, plural, =1{{count} C'hoariadenn} =2{{count} C'hoariadenn} few{{count} C'hoariadenn} many{{count} C'hoariadenn} other{{count} C'hoariadenn}}", + "studyNbMembers": "{count, plural, =1{{count} Ezel} =2{{count} Ezel} few{{count} Ezel} many{{count} Ezel} other{{count} Ezel}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Pegit testenn ho PGN amañ, betek {count} krogad} =2{Pegit testenn ho PGN amañ, betek {count} grogad} few{Pegit testenn ho PGN amañ, betek {count} krogadoù} many{Pegit testenn ho PGN amañ, betek {count} krogadoù} other{Pegit testenn ho PGN amañ, betek {count} krogadoù}}" } \ No newline at end of file diff --git a/lib/l10n/lila_bs.arb b/lib/l10n/lila_bs.arb index d3dcea6446..ad2c1392df 100644 --- a/lib/l10n/lila_bs.arb +++ b/lib/l10n/lila_bs.arb @@ -21,7 +21,31 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{Takmičio se u {count} turnirima po švicarskom sistemu} few{Takmičili se u {count} turnirima po švicarskom sistemu} other{Takmičili se u {count} turnirima po švicarskom sistemu}}", "activityJoinedNbTeams": "{count, plural, =1{Pridružio/la se {count} timu} few{Pridružio/la se {count} tima} other{Pridružio/la se {count} timova}}", "broadcastBroadcasts": "Emitovanja", + "broadcastMyBroadcasts": "Moja emitiranja", "broadcastLiveBroadcasts": "Prenos turnira uživo", + "broadcastNewBroadcast": "Novo emitovanje uživo", + "broadcastAddRound": "Dodajte kolo", + "broadcastOngoing": "U toku", + "broadcastUpcoming": "Nadolazeći", + "broadcastCompleted": "Završeno", + "broadcastRoundName": "Ime kola", + "broadcastRoundNumber": "Zaokružen broj", + "broadcastTournamentName": "Naziv turnira", + "broadcastTournamentDescription": "Kratak opis turnira", + "broadcastFullDescription": "Potpuni opis događaja", + "broadcastFullDescriptionHelp": "Neobavezni dugi opis događaja koji se emituje. {param1} je dostupan. Dužina mora biti manja od {param2} slova.", + "broadcastSourceUrlHelp": "Link koji će Lichess koristiti kako bi redovno ažurirao PGN. Mora biti javno dostupan na internetu.", + "broadcastStartDateHelp": "Neobavezno, ukoliko znate kada počinje događaj", + "broadcastCurrentGameUrl": "Link za trenutnu partiju", + "broadcastDownloadAllRounds": "Skinite sve runde", + "broadcastResetRound": "Ponovo postavite ovo kolo", + "broadcastDeleteRound": "Izbrišite ovo kolo", + "broadcastDefinitivelyDeleteRound": "Definitivno izbrišite ovo kolo i partije u njemu.", + "broadcastDeleteAllGamesOfThisRound": "Izbrišite sve partije iz ovog kola. Izvor mora biti aktivan da biste ih mogli ponovo kreirati.", + "broadcastEditRoundStudy": "Podesite studiju kola", + "broadcastDeleteTournament": "Izbrišite ovaj turnir", + "broadcastDefinitivelyDeleteTournament": "Definitivno izbrišite cijeli turnir, sva kola i sve partije.", + "broadcastNbBroadcasts": "{count, plural, =1{{count} emitovanje} few{{count} emitovanja} other{{count} emitovanja}}", "challengeChallengesX": "Izazovi: {param1}", "challengeChallengeToPlay": "Izazov na partiju", "challengeChallengeDeclined": "Izazov odbijen", @@ -339,8 +363,8 @@ "puzzleThemeXRayAttackDescription": "Figura napada ili brani polje kroz protivničku figuru.", "puzzleThemeZugzwang": "Iznudica", "puzzleThemeZugzwangDescription": "Protivnik ima ograničen broj poteza, a svaki od njih pogoršava mu poziciju.", - "puzzleThemeHealthyMix": "Zdrava mješavina", - "puzzleThemeHealthyMixDescription": "Svega pomalo. Ne znate šta možete očekivati, pa ostajete spremni na sve! Baš kao u pravim partijama.", + "puzzleThemeMix": "Zdrava mješavina", + "puzzleThemeMixDescription": "Svega pomalo. Ne znate šta možete očekivati, pa ostajete spremni na sve! Baš kao u pravim partijama.", "puzzleThemePlayerGames": "Igračke igre", "puzzleThemePlayerGamesDescription": "Potražite zagonetke stvorene iz vaših igara ili iz igara drugog igrača.", "puzzleThemePuzzleDownloadInformation": "Ove zagonetke su u javnoj domeni i mogu se preuzeti sa {param}.", @@ -467,7 +491,6 @@ "memory": "Memorija", "infiniteAnalysis": "Neprekidna analiza", "removesTheDepthLimit": "Uklanja granicu do koje računar može analizirati, i održava toplinu računara", - "engineManager": "Upravitelj šahovskog programa", "blunder": "Grubi previd", "mistake": "Greška", "inaccuracy": "Nepreciznost", @@ -758,7 +781,6 @@ "cheat": "Varanje", "troll": "Provokacija", "other": "Ostalo", - "reportDescriptionHelp": "Zalijepite link na partiju ili partije u pitanju i objasnite što nije bilo u redu sa ponašanjem korisnika. Nemojte samo reći \"varao je\", nego objasnite kako ste došli do tog zaključka. Vaša prijava će biti brže obrađena ukoliko je napišete na engleskom jeziku.", "error_provideOneCheatedGameLink": "Molimo navedite barem jedan link na partiju u kojoj je igrač varao.", "by": "od {param}", "importedByX": "Uvezao {param}", @@ -1227,6 +1249,159 @@ "stormXRuns": "{count, plural, =1{Jedna runda} few{{count} rundi} other{{count} rundi}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Odigrao jednu rundu od {param2}} few{Odigrao {count} rundi od {param2}} other{Odigrao {count} rundi {param2}}}", "streamerLichessStreamers": "Lichess emiteri", + "studyPrivate": "Privatna", + "studyMyStudies": "Moje studije", + "studyStudiesIContributeTo": "Studije kojima doprinosim", + "studyMyPublicStudies": "Moje javne studije", + "studyMyPrivateStudies": "Moje privatne studije", + "studyMyFavoriteStudies": "Moje omiljene studije", + "studyWhatAreStudies": "Šta su studije?", + "studyAllStudies": "Sve studije", + "studyStudiesCreatedByX": "Studije koje je kreirao/la {param}", + "studyNoneYet": "Još nijedna.", + "studyHot": "U trendu", + "studyDateAddedNewest": "Datum dodavanja (najnovije)", + "studyDateAddedOldest": "Datum dodavanja (najstarije)", + "studyRecentlyUpdated": "Nedavno ažurirane", + "studyMostPopular": "Najpopularnije", + "studyAlphabetical": "Abecedno", + "studyAddNewChapter": "Dodajte novo poglavlje", + "studyAddMembers": "Dodajte članove", + "studyInviteToTheStudy": "Pozovite na studiju", + "studyPleaseOnlyInvitePeopleYouKnow": "Molimo Vas da pozovete samo ljude koje znate i koji su zainteresovani da aktivno učustvuju u ovoj studiji.", + "studySearchByUsername": "Pretraga prema korisničkom imenu", + "studySpectator": "Posmatrač", + "studyContributor": "Saradnik", + "studyKick": "Izbaci", + "studyLeaveTheStudy": "Napustite studiju", + "studyYouAreNowAContributor": "Sada ste saradnik", + "studyYouAreNowASpectator": "Sada ste posmatrač", + "studyPgnTags": "PGN oznake", + "studyLike": "Sviđa mi se", + "studyUnlike": "Ne sviđa mi se", + "studyNewTag": "Nova oznaka", + "studyCommentThisPosition": "Komentirajte ovu poziciju", + "studyCommentThisMove": "Komentirajte ovaj potez", + "studyAnnotateWithGlyphs": "Obilježite poteze simbolima", + "studyTheChapterIsTooShortToBeAnalysed": "Poglavlje je prekratko za analizu.", + "studyOnlyContributorsCanRequestAnalysis": "Samo saradnici u studiji mogu zahtijevati računarsku analizu.", + "studyGetAFullComputerAnalysis": "Dobijte potpunu serversku analizu glavne varijacije.", + "studyMakeSureTheChapterIsComplete": "Budite sigurni da je poglavlje gotovo. Računarsku analizu možete zahtjevati samo jednom.", + "studyAllSyncMembersRemainOnTheSamePosition": "Svi sinhronizovani članovi ostaju na istoj poziciji", + "studyShareChanges": "Podijelite promjene sa posmatračima i sačuvajte ih na server", + "studyPlaying": "U toku", + "studyShowEvalBar": "Evaluacijske trake", + "studyFirst": "Prva strana", + "studyPrevious": "Prethodna strana", + "studyNext": "Sljedeća strana", + "studyLast": "Posljednja strana", "studyShareAndExport": "Podijelite i izvezite", - "studyStart": "Pokreni" + "studyCloneStudy": "Klonirajte", + "studyStudyPgn": "Studirajte PGN", + "studyDownloadAllGames": "Skinite sve partije", + "studyChapterPgn": "PGN poglavlja", + "studyCopyChapterPgn": "Kopirajte PGN", + "studyDownloadGame": "Skini partiju", + "studyStudyUrl": "Link studije", + "studyCurrentChapterUrl": "Link trenutnog poglavlja", + "studyYouCanPasteThisInTheForumToEmbed": "Možete ovo zalijepiti na forumu ili Vašem blogu na Lichessu kako biste ugradili poglavlje", + "studyStartAtInitialPosition": "Krenite sa inicijalnom pozicijom", + "studyStartAtX": "Krenite sa {param}", + "studyEmbedInYourWebsite": "Ugradite na Vaš sajt", + "studyReadMoreAboutEmbedding": "Pročitajte više o ugrađivanju", + "studyOnlyPublicStudiesCanBeEmbedded": "Samo javne studije mogu biti ugrađene!", + "studyOpen": "Otvorite", + "studyXBroughtToYouByY": "{param1} vam je donio {param2}", + "studyStudyNotFound": "Studija nije pronađena", + "studyEditChapter": "Uredite poglavlje", + "studyNewChapter": "Novo poglavlje", + "studyImportFromChapterX": "Uvezite iz {param}", + "studyOrientation": "Orijentacija", + "studyAnalysisMode": "Tip analize", + "studyPinnedChapterComment": "Stalni komentar poglavlja", + "studySaveChapter": "Sačuvajte poglavlje", + "studyClearAnnotations": "Izbrišite bilješke", + "studyClearVariations": "Ukloni varijante", + "studyDeleteChapter": "Izbrišite poglavlje", + "studyDeleteThisChapter": "Da li želite izbrisati ovo poglavlje? Nakon ove akcije, poglavlje se ne može vratiti!", + "studyClearAllCommentsInThisChapter": "Da li želite izbrisati sve komentare, simbole i nacrtane oblike u ovom poglavlju?", + "studyRightUnderTheBoard": "Odmah ispod ploče", + "studyNoPinnedComment": "Nijedan", + "studyNormalAnalysis": "Normalna analiza", + "studyHideNextMoves": "Sakrijte sljedeće poteze", + "studyInteractiveLesson": "Interaktivna lekcija", + "studyChapterX": "Poglavlje {param}", + "studyEmpty": "Prazno", + "studyStartFromInitialPosition": "Krenite sa inicijalnom pozicijom", + "studyEditor": "Uređivač", + "studyStartFromCustomPosition": "Krenite sa željenom pozicijom", + "studyLoadAGameByUrl": "Učitajte partiju pomoću linka", + "studyLoadAPositionFromFen": "Učitajte partiju pomoću FEN koda", + "studyLoadAGameFromPgn": "Učitajte partiju pomoću PGN formata", + "studyAutomatic": "Automatska", + "studyUrlOfTheGame": "Link partije", + "studyLoadAGameFromXOrY": "Učitajte partiju sa {param1} ili {param2}", + "studyCreateChapter": "Kreirajte poglavlje", + "studyCreateStudy": "Kreirajte studiju", + "studyEditStudy": "Uredite studiju", + "studyVisibility": "Vidljivost", + "studyPublic": "Javna", + "studyUnlisted": "Neizlistane", + "studyInviteOnly": "Samo po pozivu", + "studyAllowCloning": "Dozvolite kloniranje", + "studyNobody": "Niko", + "studyOnlyMe": "Samo ja", + "studyContributors": "Saradnici", + "studyMembers": "Članovi", + "studyEveryone": "Svi", + "studyEnableSync": "Omogućite sinhronizaciju", + "studyYesKeepEveryoneOnTheSamePosition": "Da: zadržite sve na istoj poziciji", + "studyNoLetPeopleBrowseFreely": "Ne: Dozvolite ljudima da slobodno pregledaju", + "studyPinnedStudyComment": "Stalni komentar studije", + "studyStart": "Pokreni", + "studySave": "Sačuvaj", + "studyClearChat": "Izbrišite dopisivanje", + "studyDeleteTheStudyChatHistory": "Da li želite izbrisati svo dopisivanje vezano za ovu studiju? Nakon ove akcije, obrisani tekst se ne može vratiti!", + "studyDeleteStudy": "Izbrišite studiju", + "studyConfirmDeleteStudy": "Izbrisati cijelu studiju? Nema povratka! Ukucajte naziv studije da potvrdite: {param}", + "studyWhereDoYouWantToStudyThat": "Gdje želite da ovu poziciju prostudirate?", + "studyGoodMove": "Dobar potez", + "studyMistake": "Greška", + "studyBrilliantMove": "Briljantan potez", + "studyBlunder": "Grubi previd", + "studyInterestingMove": "Zanimljiv potez", + "studyDubiousMove": "Sumnjiv potez", + "studyOnlyMove": "Jedini potez", + "studyZugzwang": "Iznudica", + "studyEqualPosition": "Jednaka pozicija", + "studyUnclearPosition": "Nejasna pozicija", + "studyWhiteIsSlightlyBetter": "Bijeli je u blagoj prednosti", + "studyBlackIsSlightlyBetter": "Crni je u blagoj prednosti", + "studyWhiteIsBetter": "Bijeli je bolji", + "studyBlackIsBetter": "Crni je bolji", + "studyWhiteIsWinning": "Bijeli dobija", + "studyBlackIsWinning": "Crni dobija", + "studyNovelty": "Nov potez", + "studyDevelopment": "Razvoj", + "studyInitiative": "Inicijativa", + "studyAttack": "Napad", + "studyCounterplay": "Protivnapad", + "studyTimeTrouble": "Cajtnot", + "studyWithCompensation": "S kompenzacijom", + "studyWithTheIdea": "S idejom", + "studyNextChapter": "Sljedeće poglavlje", + "studyPrevChapter": "Prethodno poglavlje", + "studyStudyActions": "Opcije za studiju", + "studyTopics": "Teme", + "studyMyTopics": "Moje teme", + "studyPopularTopics": "Popularne teme", + "studyManageTopics": "Upravljajte temama", + "studyBack": "Nazad", + "studyPlayAgain": "Igrajte ponovo", + "studyWhatWouldYouPlay": "Šta biste odigrali u ovoj poziciji?", + "studyYouCompletedThisLesson": "Čestitamo! Kompletirali ste ovu lekciju.", + "studyNbChapters": "{count, plural, =1{{count} Poglavlje} few{{count} Poglavlja} other{{count} Poglavlja}}", + "studyNbGames": "{count, plural, =1{{count} Partija} few{{count} Partije} other{{count} Partija}}", + "studyNbMembers": "{count, plural, =1{{count} Član} few{{count} Člana} other{{count} Članova}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Ovdje zalijepite svoj PGN tekst, do {count} partije} few{Ovdje zalijepite svoj PGN tekst, do {count} partije} other{Ovdje zalijepite svoj PGN tekst, do {count} partija}}" } \ No newline at end of file diff --git a/lib/l10n/lila_ca.arb b/lib/l10n/lila_ca.arb index 214dc06cbd..b9f9d21d81 100644 --- a/lib/l10n/lila_ca.arb +++ b/lib/l10n/lila_ca.arb @@ -29,7 +29,6 @@ "mobilePuzzleStormConfirmEndRun": "Voleu acabar aquesta ronda?", "mobilePuzzleStormFilterNothingToShow": "Res a mostrar, si us plau canvieu els filtres", "mobileCancelTakebackOffer": "Anul·la la petició per desfer la jugada", - "mobileCancelDrawOffer": "Anul·la la petició de taules", "mobileWaitingForOpponentToJoin": "Esperant que s'uneixi l'adversari...", "mobileBlindfoldMode": "A la cega", "mobileLiveStreamers": "Retransmissors en directe", @@ -63,7 +62,72 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{Ha jugat en {count} tornejos suïssos} other{Ha jugat en {count} tornejos suïssos}}", "activityJoinedNbTeams": "{count, plural, =1{Membre de {count} equip} other{T'has unit a {count} equips}}", "broadcastBroadcasts": "Retransmissions", + "broadcastMyBroadcasts": "Les meves retransmissions", "broadcastLiveBroadcasts": "Retransmissions de tornejos en directe", + "broadcastBroadcastCalendar": "Calendari de retransmissions", + "broadcastNewBroadcast": "Nova retransmissió en directe", + "broadcastSubscribedBroadcasts": "Emissions que segueixo", + "broadcastAboutBroadcasts": "Sobre les retransmissions", + "broadcastHowToUseLichessBroadcasts": "Com utilitzar les retransmissions de Lichess.", + "broadcastTheNewRoundHelp": "La nova ronda tindrà els mateixos membres i contribuïdors que l'anterior.", + "broadcastAddRound": "Afegir una ronda", + "broadcastOngoing": "En curs", + "broadcastUpcoming": "Properes", + "broadcastCompleted": "Acabada", + "broadcastCompletedHelp": "Lichess detecta el final de la ronda en funció de les partides de l'origen. Utilitzeu aquesta opció si no hi ha origen.", + "broadcastRoundName": "Nom de ronda", + "broadcastRoundNumber": "Ronda número", + "broadcastTournamentName": "Nom del torneig", + "broadcastTournamentDescription": "Breu descripció del torneig", + "broadcastFullDescription": "Descripció total de l'esdeveniment", + "broadcastFullDescriptionHelp": "Opció de llarga descripció de l'esdeveniment. {param1} és disponible. Ha de tenir menys de {param2} lletres.", + "broadcastSourceSingleUrl": "URL origen del PGN", + "broadcastSourceUrlHelp": "URL que Lichess comprovarà per a obtenir actualitzacions PGN. Ha de ser públicament accessible des d'Internet.", + "broadcastSourceGameIds": "Fins a 64 identificadors de partides de Lichess, separades per espais.", + "broadcastStartDateTimeZone": "Dia d'inici a la zona horari del torneig: {param}", + "broadcastStartDateHelp": "Opcional, si saps quan comença l'esdeveniment", + "broadcastCurrentGameUrl": "URL actual de joc", + "broadcastDownloadAllRounds": "Baixa totes les rondes", + "broadcastResetRound": "Restablir aquesta ronda", + "broadcastDeleteRound": "Eliminar aquesta ronda", + "broadcastDefinitivelyDeleteRound": "Eliminar definitivament la ronda i les seves partides.", + "broadcastDeleteAllGamesOfThisRound": "Eliminar totes les partides d'aquesta ronda. L'origen ha d'estar actiu per a recrear-les.", + "broadcastEditRoundStudy": "Edita l'estudi de la ronda", + "broadcastDeleteTournament": "Elimina aquest torneig", + "broadcastDefinitivelyDeleteTournament": "Elimina el torneig de forma definitiva, amb totes les seves rondes i les seves partides.", + "broadcastShowScores": "Mostra les puntuacions dels jugadors en funció dels resultats de les partides", + "broadcastReplacePlayerTags": "Opcional: Reemplaça noms dels jugadors, puntuacions i títols", + "broadcastFideFederations": "Federacions FIDE", + "broadcastTop10Rating": "Top 10 Ràting", + "broadcastFidePlayers": "Jugadors FIDE", + "broadcastFidePlayerNotFound": "No s'ha trobat el jugador FIDE", + "broadcastFideProfile": "Perfil FIDE", + "broadcastFederation": "Federació", + "broadcastAgeThisYear": "Edat aquest any", + "broadcastUnrated": "Sense avaluació", + "broadcastRecentTournaments": "Tornejos recents", + "broadcastOpenLichess": "Obre a Lichess", + "broadcastTeams": "Equips", + "broadcastBoards": "Taulers", + "broadcastOverview": "Visió general", + "broadcastSubscribeTitle": "Subscriviu-vos per ser notificats quan comença cada ronda. Podeu activar/desactivara la campana o modificar les notificacions push a les preferències del vostre compte.", + "broadcastUploadImage": "Puja una imatge del torneig", + "broadcastNoBoardsYet": "Encara no hi ha taulers. Apareixeran en el moment que es carreguin les partides.", + "broadcastBoardsCanBeLoaded": "Els taulers es poden carregar per codi o a través de {param}", + "broadcastStartsAfter": "Començar a les {param}", + "broadcastStartVerySoon": "La retransmissió començarà aviat.", + "broadcastNotYetStarted": "La retransmissió encara no ha començat.", + "broadcastOfficialWebsite": "Lloc web oficial", + "broadcastStandings": "Classificació", + "broadcastIframeHelp": "Més opcions a la {param}", + "broadcastWebmastersPage": "pàgina d'administració", + "broadcastPgnSourceHelp": "Un origen públic en PGN públic en temps real d'aquesta ronda. També oferim un {param} per una sincronització més ràpida i eficient.", + "broadcastEmbedThisBroadcast": "Incrusta aquesta retransmissió al vostre lloc web", + "broadcastEmbedThisRound": "Incrusta {param} al vostre lloc web", + "broadcastRatingDiff": "Diferència puntuació", + "broadcastGamesThisTournament": "Partides en aquest torneig", + "broadcastScore": "Puntuació", + "broadcastNbBroadcasts": "{count, plural, =1{{count} retransmissió} other{{count} retransmissions}}", "challengeChallengesX": "Desafiaments: {param1}", "challengeChallengeToPlay": "Desafia a una partida", "challengeChallengeDeclined": "Desafiament rebutjat", @@ -382,8 +446,8 @@ "puzzleThemeXRayAttackDescription": "Una peça ataca o defensa una casella, a través d'una peça rival.", "puzzleThemeZugzwang": "Atzucac", "puzzleThemeZugzwangDescription": "El rival té els moviments limitats i cada jugada empitjora la seva posició.", - "puzzleThemeHealthyMix": "Una mica de cada", - "puzzleThemeHealthyMixDescription": "Una mica de tot. No sabràs el que t'espera, així doncs estigues alerta pel que sigui! Igual que a les partides de veritat.", + "puzzleThemeMix": "Una mica de cada", + "puzzleThemeMixDescription": "Una mica de tot. No sabràs el que t'espera, així doncs estigues alerta pel que sigui! Igual que a les partides de veritat.", "puzzleThemePlayerGames": "Partides de jugadors", "puzzleThemePlayerGamesDescription": "Problemes generats a partir de les teves partides o de les partides d'altres jugadors.", "puzzleThemePuzzleDownloadInformation": "Aquests problemes són de domini públic i es poden descarregar des de {param}.", @@ -514,7 +578,6 @@ "memory": "Memòria", "infiniteAnalysis": "Anàlisi il·limitada", "removesTheDepthLimit": "Treu el límit de profunditat i escalfa el teu ordinador", - "engineManager": "Gestió del mòdul", "blunder": "Errada greu", "mistake": "Errada", "inaccuracy": "Imprecisió", @@ -818,7 +881,9 @@ "cheat": "Trampós", "troll": "Troll", "other": "Altres", - "reportDescriptionHelp": "Enganxa l'enllaç de la partida (o partides) i explica el comportament negatiu d'aquest usuari. No et limitis a dir que \"fa trampes\", i explica com has arribat a aquesta conclusió. El teu informe serà processat més ràpidament si l'escrius en anglès.", + "reportCheatBoostHelp": "Enganxa l'enllaç de la partida (o partides) i explica quin és el problema amb el comportament d'aquest usuari. No et limitis a dir que \"fa trampes\", explica'ns com has arribat a aquesta conclusió.", + "reportUsernameHelp": "Explica quin és el comportament ofensiu d'aquest usuari. No et limitis a dir simplement \"és ofensiu/inapropiat\", explica'ns com has arribat a aquesta conclusió, especialment si l'insult està ofuscat, en un idioma diferent de l'anglès, és un barbarisme o és una referència històrica o cultural.", + "reportProcessedFasterInEnglish": "El teu informe serà tractat més ràpidament si està escrit en anglès.", "error_provideOneCheatedGameLink": "Si us plau, proporcioneu com a mínim un enllaç a un joc on s'han fet trampes.", "by": "per {param}", "importedByX": "Importat per {param}", @@ -1216,6 +1281,7 @@ "showMeEverything": "Mostrar tot", "lichessPatronInfo": "Lichess és una entitat sense ànim de lucre i un programari totalment lliure i de codi obert.\nLes despeses de funcionament, desenvolupament i continguts es financen exclusivament amb donacions d'usuaris.", "nothingToSeeHere": "Res a veure per aquí de moment.", + "stats": "Estadístiques", "opponentLeftCounter": "{count, plural, =1{El teu contrincant ha abandonat la partida. Pots reclamar la victòria en {count} segon.} other{El teu contrincant ha abandonat la partida. Pots reclamar la victòria en {count} segons.}}", "mateInXHalfMoves": "{count, plural, =1{Mat en {count} mig-moviment} other{Mat en {count} jugades}}", "nbBlunders": "{count, plural, =1{{count} errada greu} other{{count} errades greus}}", @@ -1312,6 +1378,159 @@ "stormXRuns": "{count, plural, =1{Un intent} other{{count} intents}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Ha jugat una ronda de {param2}} other{Ha jugat {count} rondes de {param2}}}", "streamerLichessStreamers": "Retransmissors de Lichess", + "studyPrivate": "Privat", + "studyMyStudies": "Els meus estudis", + "studyStudiesIContributeTo": "Estudis on jo hi contribueixo", + "studyMyPublicStudies": "Els meus estudis públics", + "studyMyPrivateStudies": "Els meus estudis privats", + "studyMyFavoriteStudies": "Els meus estudis favorits", + "studyWhatAreStudies": "Què són els estudis?", + "studyAllStudies": "Tots els estudis", + "studyStudiesCreatedByX": "Estudis creats per {param}", + "studyNoneYet": "Res encara.", + "studyHot": "Candent", + "studyDateAddedNewest": "Data d’inclusió (més nous)", + "studyDateAddedOldest": "Data d’inclusió (més antics)", + "studyRecentlyUpdated": "Actualitzat darrerament", + "studyMostPopular": "Més popular", + "studyAlphabetical": "Alfabètic", + "studyAddNewChapter": "Afegir un nou capítol", + "studyAddMembers": "Afegeix membres", + "studyInviteToTheStudy": "Convida a l’estudi", + "studyPleaseOnlyInvitePeopleYouKnow": "Si us plau, convida gent que coneixes, i que vólen unir-se activament a l’estudi.", + "studySearchByUsername": "Cerca per nom d'usuari", + "studySpectator": "Espectador", + "studyContributor": "Contribuïdor", + "studyKick": "Expulsa", + "studyLeaveTheStudy": "Deixar l’estudi", + "studyYouAreNowAContributor": "Ara ets un contribuïdor", + "studyYouAreNowASpectator": "Actualment ets un espectador", + "studyPgnTags": "Etiquetes PGN", + "studyLike": "M’agrada", + "studyUnlike": "Ja no m'agrada", + "studyNewTag": "Nova etiqueta", + "studyCommentThisPosition": "Comentar en aquesta posició", + "studyCommentThisMove": "Comentar en aquest moviment", + "studyAnnotateWithGlyphs": "Anotar amb signes", + "studyTheChapterIsTooShortToBeAnalysed": "El capítol és massa curt per ser analitzat.", + "studyOnlyContributorsCanRequestAnalysis": "Només els contribuïdors de l’estudi poden demanar un anàlisis computeritzat.", + "studyGetAFullComputerAnalysis": "Obté un anàlisi complert desde el servidor de la línia principal.", + "studyMakeSureTheChapterIsComplete": "Segura’t que el capítol és complert. Només pots requerir l’anàlisi una sola vegada.", + "studyAllSyncMembersRemainOnTheSamePosition": "Tots els membres sincronitzats es mantenen a la mateixa posició", + "studyShareChanges": "Comparteix els canvis amb els espectadors i guarda’ls al servidor", + "studyPlaying": "Jugant", + "studyShowEvalBar": "Barres d'avaluació", + "studyFirst": "Primer", + "studyPrevious": "Anterior", + "studyNext": "Següent", + "studyLast": "Últim", "studyShareAndExport": "Comparteix i exporta", - "studyStart": "Inici" + "studyCloneStudy": "Clona", + "studyStudyPgn": "PGN de l’estudi", + "studyDownloadAllGames": "Descarrega tots els jocs", + "studyChapterPgn": "PGN del capítol", + "studyCopyChapterPgn": "Copiar PGN", + "studyDownloadGame": "Descarrega partida", + "studyStudyUrl": "URL de l’estudi", + "studyCurrentChapterUrl": "URL del capítol actual", + "studyYouCanPasteThisInTheForumToEmbed": "Pots enganxar això en el forum per insertar", + "studyStartAtInitialPosition": "Comnçar a la posició inicial", + "studyStartAtX": "Començar a {param}", + "studyEmbedInYourWebsite": "Inserta en la teva web o blog", + "studyReadMoreAboutEmbedding": "Llegeix més sobre insertar", + "studyOnlyPublicStudiesCanBeEmbedded": "Només els estudis públics poden ser inserits!", + "studyOpen": "Obrir", + "studyXBroughtToYouByY": "{param1}, presentat per {param2}", + "studyStudyNotFound": "Estudi no trobat", + "studyEditChapter": "Editar capítol", + "studyNewChapter": "Nou capítol", + "studyImportFromChapterX": "Importar de {param}", + "studyOrientation": "Orientaciò", + "studyAnalysisMode": "Mode d'anàlisi", + "studyPinnedChapterComment": "Comentari del capítol fixat", + "studySaveChapter": "Guarda el capítol", + "studyClearAnnotations": "Netejar anotacions", + "studyClearVariations": "Netejar variacions", + "studyDeleteChapter": "Eliminar capítol", + "studyDeleteThisChapter": "Eliminar aquest capítol? No hi ha volta enrera!", + "studyClearAllCommentsInThisChapter": "Esborrar tots els comentaris, signes i marques en aquest capítol?", + "studyRightUnderTheBoard": "Just a sota el tauler", + "studyNoPinnedComment": "Cap", + "studyNormalAnalysis": "Análisis normal", + "studyHideNextMoves": "Oculta els següents moviments", + "studyInteractiveLesson": "Lliçó interactiva", + "studyChapterX": "Capítol {param}", + "studyEmpty": "Buit", + "studyStartFromInitialPosition": "Començar a la posició inicial", + "studyEditor": "Editor", + "studyStartFromCustomPosition": "Començar a una posició personalitzada", + "studyLoadAGameByUrl": "Carregar una partida desde una URL", + "studyLoadAPositionFromFen": "Carregar una posició via codi FEN", + "studyLoadAGameFromPgn": "Carregar una partida PGN", + "studyAutomatic": "Automàtic", + "studyUrlOfTheGame": "URL del joc", + "studyLoadAGameFromXOrY": "Carregar una partida desde {param1} o {param2}", + "studyCreateChapter": "Crear capítol", + "studyCreateStudy": "Crear estudi", + "studyEditStudy": "Editar estudi", + "studyVisibility": "Visibilitat", + "studyPublic": "Públic", + "studyUnlisted": "No llistats", + "studyInviteOnly": "Només per invitació", + "studyAllowCloning": "Permitir clonat", + "studyNobody": "Ningú", + "studyOnlyMe": "Només jo", + "studyContributors": "Col·laboradors", + "studyMembers": "Membres", + "studyEveryone": "Tothom", + "studyEnableSync": "Habilita la sincronització", + "studyYesKeepEveryoneOnTheSamePosition": "Sí: tothom veu la mateixa posició", + "studyNoLetPeopleBrowseFreely": "No: permetre que la gent navegui lliurement", + "studyPinnedStudyComment": "Comentar estudi fixat", + "studyStart": "Inici", + "studySave": "Desa", + "studyClearChat": "Neteja el Chat", + "studyDeleteTheStudyChatHistory": "Eliminar el xat de l’estudi? No hi ha volta enrera!", + "studyDeleteStudy": "Eliminar estudi", + "studyConfirmDeleteStudy": "Esteu segurs que voleu eliminar el estudi? Tingues en compte que no es pot desfer. Per a confirmar-ho escriu el nom del estudi: {param}", + "studyWhereDoYouWantToStudyThat": "A on vols estudiar-ho?", + "studyGoodMove": "Bona jugada", + "studyMistake": "Errada", + "studyBrilliantMove": "Jugada brillant", + "studyBlunder": "Error greu", + "studyInterestingMove": "Jugada interessant", + "studyDubiousMove": "Jugada dubtosa", + "studyOnlyMove": "Única jugada", + "studyZugzwang": "Zugzwang (atzucac)", + "studyEqualPosition": "Posició igualada", + "studyUnclearPosition": "Posició poc clara", + "studyWhiteIsSlightlyBetter": "El blanc està lleugerament millor", + "studyBlackIsSlightlyBetter": "El negre està lleugerament millor", + "studyWhiteIsBetter": "El blanc està millor", + "studyBlackIsBetter": "El negre està millor", + "studyWhiteIsWinning": "El blanc està guanyant", + "studyBlackIsWinning": "El negre està guanyant", + "studyNovelty": "Novetat", + "studyDevelopment": "Desenvolupament", + "studyInitiative": "Iniciativa", + "studyAttack": "Atac", + "studyCounterplay": "Contra atac", + "studyTimeTrouble": "Problema de temps", + "studyWithCompensation": "Amb compensació", + "studyWithTheIdea": "Amb la idea", + "studyNextChapter": "Capítol següent", + "studyPrevChapter": "Capítol Anterior", + "studyStudyActions": "Acions de l'estudi", + "studyTopics": "Temes", + "studyMyTopics": "Els meus temes", + "studyPopularTopics": "Temes populars", + "studyManageTopics": "Gestiona els temes", + "studyBack": "Enrere", + "studyPlayAgain": "Torna a jugar", + "studyWhatWouldYouPlay": "Que jugaríeu en aquesta posició?", + "studyYouCompletedThisLesson": "Enhorabona, heu completat aquesta lliçó.", + "studyNbChapters": "{count, plural, =1{{count} Capítol} other{{count} Capítols}}", + "studyNbGames": "{count, plural, =1{{count} Joc} other{{count} Jocs}}", + "studyNbMembers": "{count, plural, =1{{count} Membre} other{{count} Membres}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Enganxa el teu PGN aquí, fins a {count} partida} other{Enganxa el teu PGN aquí, fins a {count} partides}}" } \ No newline at end of file diff --git a/lib/l10n/lila_cs.arb b/lib/l10n/lila_cs.arb index 7add3d2f1f..6743885eca 100644 --- a/lib/l10n/lila_cs.arb +++ b/lib/l10n/lila_cs.arb @@ -1,4 +1,31 @@ { + "mobileClearButton": "Vymazat", + "mobilePlayersMatchingSearchTerm": "Hráči s \"{param}\"", + "mobileNoSearchResults": "Žádné výsledky", + "mobileAreYouSure": "Jste si jistý?", + "mobilePuzzleStreakAbortWarning": "Ztratíte aktuální sérii a vaše skóre bude uloženo.", + "mobilePuzzleStormNothingToShow": "Nic k zobrazení. Zahrajte si nějaké běhy Bouřky úloh.", + "mobileSharePuzzle": "Sdílej tuto úlohu", + "mobileShareGameURL": "Sdílet URL hry", + "mobileShareGamePGN": "Sdílet PGN", + "mobileSharePositionAsFEN": "Sdílet pozici jako FEN", + "mobileShowVariations": "Zobraz variace", + "mobileHideVariation": "Schovej variace", + "mobileShowComments": "Zobraz komentáře", + "mobilePuzzleStormConfirmEndRun": "Chceš ukončit tento běh?", + "mobilePuzzleStormFilterNothingToShow": "Nic k zobrazení, prosím změn filtry", + "mobileCancelTakebackOffer": "Zrušit nabídnutí vrácení tahu", + "mobileWaitingForOpponentToJoin": "Čeká se na připojení protihráče...", + "mobileBlindfoldMode": "Páska přes oči", + "mobileLiveStreamers": "Živé vysílání", + "mobileCustomGameJoinAGame": "Připojit se ke hře", + "mobileCorrespondenceClearSavedMove": "Vymazat uložené tahy", + "mobileSomethingWentWrong": "Něco se pokazilo.", + "mobileShowResult": "Zobrazit výsledky", + "mobilePuzzleThemesSubtitle": "Hrej úlohy z tvých oblíbených zahájení, nebo si vyber styl.", + "mobilePuzzleStormSubtitle": "Vyřeš co nejvíce úloh co dokážeš za 3 minuty.", + "mobileGreeting": "Ahoj, {param}", + "mobileGreetingWithoutName": "Ahoj", "activityActivity": "Aktivita", "activityHostedALiveStream": "Hostoval živý stream", "activityRankedInSwissTournament": "{param1}. místo v turnaji {param2}", @@ -11,6 +38,7 @@ "activityPlayedNbMoves": "{count, plural, =1{Hrán {count} tah} few{Hrány {count} tahy} many{Hráno {count} tahů} other{Hráno {count} tahů}}", "activityInNbCorrespondenceGames": "{count, plural, =1{v {count} korespondenční partii} few{v {count} korespondenčních partiích} many{v {count} korespondenčních partiích} other{v {count} korespondenčních partiích}}", "activityCompletedNbGames": "{count, plural, =1{Dokončena {count} korespondenční partie} few{Dokončeny {count} korespondenční partie} many{Dokončeno {count} korespondenčních partií} other{Dokončeno {count} korespondenčních partií}}", + "activityCompletedNbVariantGames": "{count, plural, =1{Dokončena {count} {param2} korespondenční partie} few{Dokončeny {count} {param2} korespondenční partie} many{Dokončeno {count} {param2} korespondenčních partii} other{Dokončeno {count} {param2} korespondenčních partii}}", "activityFollowedNbPlayers": "{count, plural, =1{Začali jste sledovat {count} hráče} few{Začali jste sledovat {count} hráče} many{Začali jste sledovat {count} hráčů} other{Začali jste sledovat {count} hráčů}}", "activityGainedNbFollowers": "{count, plural, =1{Získán {count} nový následovník} few{Získáni {count} noví následovníci} many{Získáno {count} nových následovníků} other{Získáno {count} nových následovníků}}", "activityHostedNbSimuls": "{count, plural, =1{Hostili jste {count} simultánku} few{Hostili jste {count} simultánky} many{Hostili jste {count} simultánek} other{Hostili jste {count} simultánek}}", @@ -21,7 +49,51 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{Účast v {count} švýcarském turnaji} few{Účast ve {count} švýcarských turnajích} many{Účast v {count} švýcarských turnajích} other{Účast v {count} švýcarských turnajích}}", "activityJoinedNbTeams": "{count, plural, =1{Přidal se k {count} týmu} few{Přidal se k {count} týmům} many{Přidal se k {count} týmům} other{Přidal se k {count} týmům}}", "broadcastBroadcasts": "Přenosy", + "broadcastMyBroadcasts": "Moje vysílání", "broadcastLiveBroadcasts": "Živé přenosy turnajů", + "broadcastBroadcastCalendar": "Kalendář přenosů", + "broadcastNewBroadcast": "Nový živý přenos", + "broadcastSubscribedBroadcasts": "Odebírané přenosy", + "broadcastAboutBroadcasts": "O vysílání", + "broadcastHowToUseLichessBroadcasts": "Jak používat Lichess vysílání.", + "broadcastTheNewRoundHelp": "Nové kolo bude mít stejné členy a přispěvatele jako to předchozí.", + "broadcastAddRound": "Přidat kolo", + "broadcastOngoing": "Probíhající", + "broadcastUpcoming": "Chystané", + "broadcastCompleted": "Dokončené", + "broadcastCompletedHelp": "Lichess detekuje dokončení kola na základě zdrojových her. Tento přepínač použijte, pokud není k dispozici žádný zdroj.", + "broadcastRoundName": "Číslo kola", + "broadcastRoundNumber": "Číslo kola", + "broadcastTournamentName": "Název turnaje", + "broadcastTournamentDescription": "Stručný popis turnaje", + "broadcastFullDescription": "Úplný popis události", + "broadcastFullDescriptionHelp": "Volitelný dlouhý popis přenosu. {param1} je k dispozici. Délka musí být menší než {param2} znaků.", + "broadcastSourceSingleUrl": "PGN Zdrojová URL adresa", + "broadcastSourceUrlHelp": "URL adresa, kterou bude Lichess kontrolovat pro získání PGN aktualizací. Musí být veřejně přístupná z internetu.", + "broadcastSourceGameIds": "Až 64 ID Lichess her, oddělených mezerama.", + "broadcastStartDateTimeZone": "Datum zahájení v lokálním čase turnaje: {param}", + "broadcastStartDateHelp": "Nepovinné, pokud víte, kdy událost začíná", + "broadcastCurrentGameUrl": "URL adresa právě probíhající partie", + "broadcastDownloadAllRounds": "Stáhnout hry ze všech kol", + "broadcastResetRound": "Resetovat toto kolo", + "broadcastDeleteRound": "Smazat toto kolo", + "broadcastDefinitivelyDeleteRound": "Definitivně smazat kolo a jeho hry.", + "broadcastDeleteAllGamesOfThisRound": "Smazat všechny hry v tomto kole. Zdroj musí být aktivní aby bylo možno je znovu vytvořit.", + "broadcastEditRoundStudy": "Upravit studie kola", + "broadcastDeleteTournament": "Smazat tento turnaj", + "broadcastDefinitivelyDeleteTournament": "Opravdu smazat celý turnaj, všechna kola a hry.", + "broadcastShowScores": "Zobraz skóre hráču dle herních výsledků", + "broadcastReplacePlayerTags": "Volitelné: nahraď jména hráčů, rating a tituly", + "broadcastFideFederations": "FIDE federace", + "broadcastTop10Rating": "Rating top 10", + "broadcastFidePlayers": "FIDE hráči", + "broadcastFidePlayerNotFound": "FIDE hráč nenalezen", + "broadcastFideProfile": "FIDE profil", + "broadcastFederation": "Federace", + "broadcastAgeThisYear": "Věk tento rok", + "broadcastUnrated": "Nehodnocen", + "broadcastRecentTournaments": "Nedávné tournamenty", + "broadcastNbBroadcasts": "{count, plural, =1{{count} vysílání} few{{count} vysílání} many{{count} vysílání} other{{count} vysílání}}", "challengeChallengesX": "Výzvy: {param1}", "challengeChallengeToPlay": "Vyzvat k partii", "challengeChallengeDeclined": "Výzva odmítnuta", @@ -340,8 +412,8 @@ "puzzleThemeXRayAttackDescription": "Figura útočí nebo chrání pole skrze nepřátelskou figuru.", "puzzleThemeZugzwang": "Zugzwang", "puzzleThemeZugzwangDescription": "Soupeř musí zahrát jakýkoliv tah, přičemž všechny zhoršují jeho pozici a zlepšují naší pozici.", - "puzzleThemeHealthyMix": "Mix úloh", - "puzzleThemeHealthyMixDescription": "Troška od všeho. Nevíte co čekat, čili jste na vše připraveni! Jako v normální partii.", + "puzzleThemeMix": "Mix úloh", + "puzzleThemeMixDescription": "Troška od všeho. Nevíte co čekat, čili jste na vše připraveni! Jako v normální partii.", "puzzleThemePlayerGames": "Z vašich her", "puzzleThemePlayerGamesDescription": "Vyhledejte úlohy vygenerované z vašich her, nebo z her jiného hráče.", "puzzleThemePuzzleDownloadInformation": "Tyto hádanky jsou ve veřejné doméně a lze je stáhnout z {param}.", @@ -420,6 +492,8 @@ "promoteVariation": "Povýšit variantu", "makeMainLine": "Povýšit na hlavní variantu", "deleteFromHere": "Smazat odsud", + "collapseVariations": "Schovat variace", + "expandVariations": "Zobrazit variace", "forceVariation": "Zobrazit jako variantu", "copyVariationPgn": "Zkopírovat PGN varianty", "move": "Tah", @@ -470,7 +544,6 @@ "memory": "Paměť", "infiniteAnalysis": "Nekonečná analýza", "removesTheDepthLimit": "Zapne nekonečnou analýzu a odstraní omezení hloubky propočtu", - "engineManager": "Správce enginu", "blunder": "Hrubá chyba", "mistake": "Chyba", "inaccuracy": "Nepřesnost", @@ -496,6 +569,7 @@ "latestForumPosts": "Poslední příspěvky", "players": "Hráči", "friends": "Přátelé", + "otherPlayers": "ostatní hráči", "discussions": "Konverzace", "today": "Dnes", "yesterday": "Včera", @@ -726,8 +800,10 @@ "ifNoneLeaveEmpty": "Pokud nemáte, nechte pole volné", "profile": "Profil", "editProfile": "Upravit profil", + "realName": "Skutečné jméno", "setFlair": "Nastav si svou ikonu za jménem", - "flair": "Upravitelná ikona", + "flair": "Ikona", + "youCanHideFlair": "Existuje nastavení které schová všechny uživatelské ikony za jménem po celém webu.", "biography": "O mně", "countryRegion": "Země nebo region", "thankYou": "Děkujeme!", @@ -744,12 +820,15 @@ "automaticallyProceedToNextGameAfterMoving": "Automaticky přejdi k další hře po tahu", "autoSwitch": "Přepnout automaticky", "puzzles": "Puzzle", + "onlineBots": "Online roboti", "name": "Jméno", "description": "Popis", "descPrivate": "Soukromý popis", "descPrivateHelp": "Text, který uvidí pouze členové týmu, kteří poté uvidí jenom tento text.", "no": "Ne", "yes": "Ano", + "website": "Web", + "mobile": "Mobil", "help": "Nápověda:", "createANewTopic": "Vytvořit nové téma", "topics": "Témata", @@ -768,7 +847,9 @@ "cheat": "Podvod", "troll": "Troll", "other": "Jiné", - "reportDescriptionHelp": "Vložte link na hru(y) a popište, co je špatně na chování tohoto hráče. (Pokud možno anglicky.)", + "reportCheatBoostHelp": "Zde vlož odkaz na hru(hry) a napiš co dělal tento uživatel. Nepiš pouze \"on podváděl\", ale napiš proč si myslíš že podváděl.", + "reportUsernameHelp": "Vysvětli co je urážlivého na jeho u6ivatelském jménu. Nepiš pouze \"Je urážlivé/nevhodné\", ale řekni i důvod proč to tak je, zejména pokud je urážka zatemněná, nebo je v jiném jazyce než v angličtině, nebo je ve slangu či jde o historickou nebokulturní referenci.", + "reportProcessedFasterInEnglish": "Nahlášení bude rychlejší pokud bude v angličtině.", "error_provideOneCheatedGameLink": "Prosím, uveďte alespoň jeden link na partii, ve které se podvádělo.", "by": "od {param}", "importedByX": "Importováno uživatelem {param}", @@ -801,6 +882,7 @@ "slow": "Pomalé", "insideTheBoard": "Na šachovnici", "outsideTheBoard": "Mimo šachovnici", + "allSquaresOfTheBoard": "Všechny pole na šachovnici", "onSlowGames": "Při pomalých hrách", "always": "Vždy", "never": "Nikdy", @@ -862,8 +944,10 @@ "simultaneousExhibitions": "Simultánky", "host": "Host", "hostColorX": "Barva zakladatele: {param}", + "yourPendingSimuls": "Tvoje simulace ve frontě", "createdSimuls": "Nově vytvořené simultánky", "hostANewSimul": "Vytvoř novou simultánku", + "signUpToHostOrJoinASimul": "Zaregistruj se abys mohl založit nebo se připojit k simulaci", "noSimulFound": "Simultánka nenalezena", "noSimulExplanation": "Tato simultánka neexistuje", "returnToSimulHomepage": "Vrať se na domovskou sránku simultánek", @@ -888,6 +972,7 @@ "keyboardShortcuts": "Klávesové zkratky", "keyMoveBackwardOrForward": "O tah vpřed/vzad", "keyGoToStartOrEnd": "běžte na začátek/konec", + "keyCycleSelectedVariation": "Projdi zkrze vybranou variaci", "keyShowOrHideComments": "zobrazte/skryjte komentáře", "keyEnterOrExitVariation": "Zobraz variantu", "keyRequestComputerAnalysis": "Vyžádejte si počítačovou analýzu, poučte se ze svých chyb", @@ -895,8 +980,12 @@ "keyNextBlunder": "Další hrubá chyba", "keyNextMistake": "Další chyba", "keyNextInaccuracy": "Další nepřesnost", + "keyPreviousBranch": "Předchozí větev", + "keyNextBranch": "Další větev", "toggleVariationArrows": "Přepnout šipky variant", + "cyclePreviousOrNextVariation": "Projdi předchozí/následující variantu", "toggleGlyphAnnotations": "Přepnout poznámky glyfů", + "togglePositionAnnotations": "Přepni zvýraznění pozice", "variationArrowsInfo": "Šipky variant umožňují navigaci bez použití seznamu tahů.", "playSelectedMove": "zahrát vybraný tah", "newTournament": "Nový turnaj", @@ -973,6 +1062,12 @@ "transparent": "Průhledné", "deviceTheme": "Motiv podle zařízení", "backgroundImageUrl": "URL zdroj obrázku na pozadí:", + "board": "Šachovnice", + "size": "Velikost", + "opacity": "Průhlednost", + "brightness": "Jas", + "hue": "Hue", + "boardReset": "Vrátit barvy na původní nastavení", "pieceSet": "Vzhled figur", "embedInYourWebsite": "Vložit na web", "usernameAlreadyUsed": "Toto uživatelské jméno již existuje. Zvol, prosím, jiné.", @@ -1055,6 +1150,7 @@ "agreementPolicy": "Souhlasím, že se budu řídit všemi pravidly Lichessu.", "searchOrStartNewDiscussion": "Začněte nebo vyhledejte konverzaci", "edit": "Upravit", + "bullet": "Bullet", "blitz": "Bleskové šachy", "rapid": "Rapid", "classical": "Klasické", @@ -1150,6 +1246,7 @@ "instructions": "Pokyny", "showMeEverything": "Ukaž mi všechno", "lichessPatronInfo": "Lichess je bezplatný a zcela svobodný/nezávislý softvér s otevřeným zdrojovým kódem.\nVeškeré provozní náklady, vývoj a obsah jsou financovány výhradně z příspěvků uživatelů.", + "nothingToSeeHere": "Momentálně zde není nic k vidění.", "opponentLeftCounter": "{count, plural, =1{Tvůj soupeř opustil hru. Můžeš si vyžádat vítězství za {count} sekundu.} few{Tvůj soupeř opustil hru. Můžeš si vyžádat vítězství za {count} sekundy.} many{Tvůj soupeř opustil hru. Můžeš si vyžádat vítězství za {count} sekund.} other{Tvůj soupeř opustil hru. Můžeš si vyžádat vítězství za {count} sekund.}}", "mateInXHalfMoves": "{count, plural, =1{Mat v {count} půltahu} few{Mat v {count} půltazích} many{Mat v {count} půltazích} other{Mat v {count} půltazích}}", "nbBlunders": "{count, plural, =1{{count} hrubá chyba} few{{count} hrubé chyby} many{{count} hrubých chyb} other{{count} hrubých chyb}}", @@ -1246,6 +1343,159 @@ "stormXRuns": "{count, plural, =1{1 pokus} few{{count} pokusy} many{{count} pokusů} other{{count} pokusů}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Odehrán jeden {param2}} few{Odehrány {count} pokusy {param2}} many{Odehráno {count} her {param2}} other{Odehráno {count} běhů z {param2}}}", "streamerLichessStreamers": "Lichess streameři", + "studyPrivate": "Soukromé", + "studyMyStudies": "Moje studie", + "studyStudiesIContributeTo": "Studie, ke kterým přispívám", + "studyMyPublicStudies": "Moje veřejné studie", + "studyMyPrivateStudies": "Moje soukromé studie", + "studyMyFavoriteStudies": "Moje oblíbené studie", + "studyWhatAreStudies": "Co jsou studie?", + "studyAllStudies": "Všechny studie", + "studyStudiesCreatedByX": "Studie vytvořené hráčem {param}", + "studyNoneYet": "Zatím nic.", + "studyHot": "Oblíbené", + "studyDateAddedNewest": "Datum přidání (nejnovější)", + "studyDateAddedOldest": "Datum přidání (nejstarší)", + "studyRecentlyUpdated": "Nedávno aktualizované", + "studyMostPopular": "Nejoblíbenější", + "studyAlphabetical": "Abecedně", + "studyAddNewChapter": "Přidat novou kapitolu", + "studyAddMembers": "Přidat uživatele", + "studyInviteToTheStudy": "Pozvat do studie", + "studyPleaseOnlyInvitePeopleYouKnow": "Prosím zvěte pouze lidi, které znáte a kteří se chtějí aktivně připojit k této studii.", + "studySearchByUsername": "Hledat podle uživatelského jména", + "studySpectator": "Divák", + "studyContributor": "Přispívající", + "studyKick": "Vyhodit", + "studyLeaveTheStudy": "Opustit studii", + "studyYouAreNowAContributor": "Nyní jste přispívající", + "studyYouAreNowASpectator": "Nyní jste divák", + "studyPgnTags": "PGN tagy", + "studyLike": "To se mi líbí", + "studyUnlike": "Už se mi nelíbí", + "studyNewTag": "Nový štítek", + "studyCommentThisPosition": "Komentář k tomuto příspěvku", + "studyCommentThisMove": "Komentář k tomuto tahu", + "studyAnnotateWithGlyphs": "Popsat glyfy", + "studyTheChapterIsTooShortToBeAnalysed": "Kapitola je moc krátká na to, aby mohla být zanalyzována.", + "studyOnlyContributorsCanRequestAnalysis": "Pouze přispěvatelé mohou požádat o počítačovou analýzu.", + "studyGetAFullComputerAnalysis": "Získejte plnou počítačovou analýzu hlavní varianty.", + "studyMakeSureTheChapterIsComplete": "Ujistěte se, že je kapitola úplná. O analýzu můžete požádat pouze jednou.", + "studyAllSyncMembersRemainOnTheSamePosition": "Všichni SYNCHRONIZOVANÍ členové zůstávají na stejné pozici", + "studyShareChanges": "Sdílet změny s diváky a uložit je na server", + "studyPlaying": "Probíhající", + "studyShowEvalBar": "Lišta hodnotící pozici", + "studyFirst": "První", + "studyPrevious": "Předchozí", + "studyNext": "Další", + "studyLast": "Poslední", "studyShareAndExport": "Sdílení a export", - "studyStart": "Začít" + "studyCloneStudy": "Klonovat", + "studyStudyPgn": "PGN studie", + "studyDownloadAllGames": "Stáhnout všechny hry", + "studyChapterPgn": "PGN kapitoly", + "studyCopyChapterPgn": "Kopírovat PGN", + "studyDownloadGame": "Stáhnout hru", + "studyStudyUrl": "URL studie", + "studyCurrentChapterUrl": "URL aktuální kapitoly", + "studyYouCanPasteThisInTheForumToEmbed": "Tento odkaz můžete vložit např. do diskusního fóra", + "studyStartAtInitialPosition": "Začít ve výchozí pozici", + "studyStartAtX": "Začít u tahu {param}", + "studyEmbedInYourWebsite": "Vložte vaší stránku nebo blog", + "studyReadMoreAboutEmbedding": "Přečtěte si více o vkládání", + "studyOnlyPublicStudiesCanBeEmbedded": "Lze vložit pouze veřejné studie!", + "studyOpen": "Otevřít", + "studyXBroughtToYouByY": "{param1} vám přináší {param2}", + "studyStudyNotFound": "Studie nenalezena", + "studyEditChapter": "Upravit kapitolu", + "studyNewChapter": "Nová kapitola", + "studyImportFromChapterX": "Importovat z {param}", + "studyOrientation": "Orientace", + "studyAnalysisMode": "Režim rozboru", + "studyPinnedChapterComment": "Připnutý komentář u kapitoly", + "studySaveChapter": "Uložit kapitolu", + "studyClearAnnotations": "Vymazat anotace", + "studyClearVariations": "Vymazat varianty", + "studyDeleteChapter": "Odstranit kapitolu", + "studyDeleteThisChapter": "Opravdu chcete odstranit tuto kapitolu? Kapitola bude navždy ztracena!", + "studyClearAllCommentsInThisChapter": "Vymazat všechny komentáře a výtvory v této kapitole?", + "studyRightUnderTheBoard": "Přímo pod šachovnicí", + "studyNoPinnedComment": "Žádný", + "studyNormalAnalysis": "Normální rozbor", + "studyHideNextMoves": "Skrýt následující tahy", + "studyInteractiveLesson": "Interaktivní lekce", + "studyChapterX": "Kapitola: {param}", + "studyEmpty": "Prázdné", + "studyStartFromInitialPosition": "Začít z původní pozice", + "studyEditor": "Tvůrce", + "studyStartFromCustomPosition": "Začít od vlastní pozice", + "studyLoadAGameByUrl": "Načíst hru podle URL", + "studyLoadAPositionFromFen": "Načíst polohu z FEN", + "studyLoadAGameFromPgn": "Načíst hru z PGN", + "studyAutomatic": "Automatický", + "studyUrlOfTheGame": "URL hry", + "studyLoadAGameFromXOrY": "Načíst hru z {param1} nebo {param2}", + "studyCreateChapter": "Vytvořit kapitolu", + "studyCreateStudy": "Vytvořit studii", + "studyEditStudy": "Upravit studii", + "studyVisibility": "Viditelnost", + "studyPublic": "Veřejná", + "studyUnlisted": "Neveřejná", + "studyInviteOnly": "Pouze na pozvání", + "studyAllowCloning": "Povolit klonování", + "studyNobody": "Nikdo", + "studyOnlyMe": "Pouze já", + "studyContributors": "Přispěvatelé", + "studyMembers": "Členové", + "studyEveryone": "Kdokoli", + "studyEnableSync": "Povolit synchronizaci", + "studyYesKeepEveryoneOnTheSamePosition": "Ano, všichni zůstávají na stejné pozici", + "studyNoLetPeopleBrowseFreely": "Ne, umožnit volné procházení", + "studyPinnedStudyComment": "Připnutý komentář studie", + "studyStart": "Začít", + "studySave": "Uložit", + "studyClearChat": "Vyčistit chat", + "studyDeleteTheStudyChatHistory": "Opravdu chcete vymazat historii chatu? Operaci nelze vrátit!", + "studyDeleteStudy": "Smazat studii", + "studyConfirmDeleteStudy": "Opravdu chcete smazat celou studii? Akci nelze vrátit zpět. Zadejte název studie pro potvrzení: {param}", + "studyWhereDoYouWantToStudyThat": "Kde chcete tuto pozici studovat?", + "studyGoodMove": "Dobrý tah", + "studyMistake": "Chyba", + "studyBrilliantMove": "Výborný tah", + "studyBlunder": "Hrubá chyba", + "studyInterestingMove": "Zajímavý tah", + "studyDubiousMove": "Pochybný tah", + "studyOnlyMove": "Jediný tah", + "studyZugzwang": "Zugzwang", + "studyEqualPosition": "Rovná pozice", + "studyUnclearPosition": "Nejasná pozice", + "studyWhiteIsSlightlyBetter": "Bílý stojí o něco lépe", + "studyBlackIsSlightlyBetter": "Černý stojí o něco lépe", + "studyWhiteIsBetter": "Bílý stojí lépe", + "studyBlackIsBetter": "Černý stojí lépe", + "studyWhiteIsWinning": "Bílý má rozhodující výhodu", + "studyBlackIsWinning": "Černý má rozhodující výhodu", + "studyNovelty": "Novinka", + "studyDevelopment": "Vývin", + "studyInitiative": "S iniciativou", + "studyAttack": "S útokem", + "studyCounterplay": "S protihrou", + "studyTimeTrouble": "Časová tíseň", + "studyWithCompensation": "S kompenzací", + "studyWithTheIdea": "S ideou", + "studyNextChapter": "Další kapitola", + "studyPrevChapter": "Předchozí kapitola", + "studyStudyActions": "Akce pro studii", + "studyTopics": "Témata", + "studyMyTopics": "Moje témata", + "studyPopularTopics": "Oblíbená témata", + "studyManageTopics": "Správa témat", + "studyBack": "Zpět", + "studyPlayAgain": "Hrát znovu", + "studyWhatWouldYouPlay": "Co byste v této pozici hráli?", + "studyYouCompletedThisLesson": "Blahopřejeme! Dokončili jste tuto lekci.", + "studyNbChapters": "{count, plural, =1{{count} kapitola} few{{count} kapitoly} many{{count} kapitol} other{{count} kapitol}}", + "studyNbGames": "{count, plural, =1{{count} hra} few{{count} hry} many{{count} her} other{{count} her}}", + "studyNbMembers": "{count, plural, =1{{count} člen} few{{count} členi} many{{count} členů} other{{count} členů}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Vložte obsah vašeho PGN souboru (až {count} hra)} few{Vložte obsah vašeho PGN souboru (až {count} hry)} many{Vložte obsah vašeho PGN souboru (až {count} her)} other{Vložte obsah vašeho PGN souboru (až {count} her)}}" } \ No newline at end of file diff --git a/lib/l10n/lila_da.arb b/lib/l10n/lila_da.arb index 95a9dd0689..7233f2c3af 100644 --- a/lib/l10n/lila_da.arb +++ b/lib/l10n/lila_da.arb @@ -30,7 +30,6 @@ "mobilePuzzleStormConfirmEndRun": "Vil du afslutte dette løb?", "mobilePuzzleStormFilterNothingToShow": "Intet at vise, ændr venligst filtre", "mobileCancelTakebackOffer": "Annuller tilbud om tilbagetagelse", - "mobileCancelDrawOffer": "Træk tilbud om remis tilbage", "mobileWaitingForOpponentToJoin": "Venter på at modstander slutter sig til...", "mobileBlindfoldMode": "Bind for øjnene", "mobileLiveStreamers": "Live-streamere", @@ -42,6 +41,7 @@ "mobilePuzzleStormSubtitle": "Løs så mange opgaver som muligt på 3 minutter.", "mobileGreeting": "Hej, {param}", "mobileGreetingWithoutName": "Hej", + "mobilePrefMagnifyDraggedPiece": "Forstør brik, som trækkes", "activityActivity": "Aktivitet", "activityHostedALiveStream": "Hostede en livestream", "activityRankedInSwissTournament": "Rangeret #{param1} i {param2}", @@ -54,6 +54,7 @@ "activityPlayedNbMoves": "{count, plural, =1{Spillede {count} træk} other{Spillede {count} træk}}", "activityInNbCorrespondenceGames": "{count, plural, =1{i {count} korrespondanceparti} other{i {count} korrespondancepartier}}", "activityCompletedNbGames": "{count, plural, =1{Afsluttede {count} korrespondanceparti} other{Afsluttede {count} korrespondancepartier}}", + "activityCompletedNbVariantGames": "{count, plural, =1{Afsluttede {count} {param2} korrespondanceparti} other{Afsluttede {count} {param2} korrespondancepartier}}", "activityFollowedNbPlayers": "{count, plural, =1{Begyndte at følge {count} spiller} other{Begyndte at følge {count} spillere}}", "activityGainedNbFollowers": "{count, plural, =1{Fik {count} ny følger} other{Fik {count} nye følgere}}", "activityHostedNbSimuls": "{count, plural, =1{Var vært for {count} simultanskakarrangement} other{Var vært for {count} simultanskakarrangementer}}", @@ -64,7 +65,72 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{Deltog i {count} schweizerturnering} other{Deltog i {count} schweizerturneringer}}", "activityJoinedNbTeams": "{count, plural, =1{Blev medlem af {count} hold} other{Blev medlem af {count} hold}}", "broadcastBroadcasts": "Udsendelser", + "broadcastMyBroadcasts": "Mine udsendelser", "broadcastLiveBroadcasts": "Live turnerings-udsendelser", + "broadcastBroadcastCalendar": "Kaldender for udsendelser", + "broadcastNewBroadcast": "Ny live-udsendelse", + "broadcastSubscribedBroadcasts": "Udsendelser, du abonnerer på", + "broadcastAboutBroadcasts": "Om udsendelse", + "broadcastHowToUseLichessBroadcasts": "Sådan bruges Lichess-udsendelser.", + "broadcastTheNewRoundHelp": "Den nye runde vil have de samme medlemmer og bidragydere som den foregående.", + "broadcastAddRound": "Tilføj en runde", + "broadcastOngoing": "I gang", + "broadcastUpcoming": "Kommende", + "broadcastCompleted": "Afsluttet", + "broadcastCompletedHelp": "Lichess registrerer rund-færdiggørelse baseret på kildepartierne. Brug denne skifter, hvis der ikke er nogen kilde.", + "broadcastRoundName": "Rundenavn", + "broadcastRoundNumber": "Rundenummer", + "broadcastTournamentName": "Turneringsnavn", + "broadcastTournamentDescription": "Kort beskrivelse af turnering", + "broadcastFullDescription": "Fuld beskrivelse af begivenheden", + "broadcastFullDescriptionHelp": "Valgfri lang beskrivelse af transmissionen. {param1} er tilgængelig. Længde skal være mindre end {param2} tegn.", + "broadcastSourceSingleUrl": "URL for PGN-kilde", + "broadcastSourceUrlHelp": "URL som Lichess vil trække på for at få PGN updates. Den skal være offentlig tilgængelig fra internettet.", + "broadcastSourceGameIds": "Op til 64 Lichess parti-ID'er, adskilt af mellemrum.", + "broadcastStartDateTimeZone": "Startdato i turneringens lokale tidszone: {param}", + "broadcastStartDateHelp": "Valgfri, hvis du ved, hvornår begivenheden starter", + "broadcastCurrentGameUrl": "Nuværende parti URL", + "broadcastDownloadAllRounds": "Download alle runder", + "broadcastResetRound": "Nulstil denne runde", + "broadcastDeleteRound": "Slet denne runde", + "broadcastDefinitivelyDeleteRound": "Slet runden og dens partier endegyldigt.", + "broadcastDeleteAllGamesOfThisRound": "Slet alle partier i denne runde. Kilden skal være aktiv for at genskabe dem.", + "broadcastEditRoundStudy": "Rediger rundestudie", + "broadcastDeleteTournament": "Slet denne turnering", + "broadcastDefinitivelyDeleteTournament": "Slet hele turneringen, alle dens runder og alle dens partier.", + "broadcastShowScores": "Vis spilleres point baseret på resultater fra partier", + "broadcastReplacePlayerTags": "Valgfrit: udskift spillernavne, ratings og titler", + "broadcastFideFederations": "FIDE-føderationer", + "broadcastTop10Rating": "Top 10 rating", + "broadcastFidePlayers": "FIDE-spillere", + "broadcastFidePlayerNotFound": "FIDE-spiller ikke fundet", + "broadcastFideProfile": "FIDE-profil", + "broadcastFederation": "Føderation", + "broadcastAgeThisYear": "Alder i år", + "broadcastUnrated": "Uden rating", + "broadcastRecentTournaments": "Seneste turneringer", + "broadcastOpenLichess": "Åbn i Lichess", + "broadcastTeams": "Hold", + "broadcastBoards": "Brætter", + "broadcastOverview": "Oversigt", + "broadcastSubscribeTitle": "Abonner på at blive underrettet, når hver runde starter. Du kan skifte mellem klokke- eller push-meddelelser for udsendelser i dine kontoindstillinger.", + "broadcastUploadImage": "Upload turneringsbillede", + "broadcastNoBoardsYet": "Ingen brætter endnu. Disse vises når partier er uploadet.", + "broadcastBoardsCanBeLoaded": "Brætter kan indlæses med en kilde eller via {param}", + "broadcastStartsAfter": "Starter efter {param}", + "broadcastStartVerySoon": "Udsendelsen starter meget snart.", + "broadcastNotYetStarted": "Udsendelsen er endnu ikke startet.", + "broadcastOfficialWebsite": "Officielt websted", + "broadcastStandings": "Stillinger", + "broadcastIframeHelp": "Flere muligheder på {param}", + "broadcastWebmastersPage": "webmasters side", + "broadcastPgnSourceHelp": "En offentlig, realtids PGN-kilde til denne runde. Vi tilbyder også en {param} for hurtigere og mere effektiv synkronisering.", + "broadcastEmbedThisBroadcast": "Indlejr denne udsendelse på dit website", + "broadcastEmbedThisRound": "Indlejr {param} på dit website", + "broadcastRatingDiff": "Rating-forskel", + "broadcastGamesThisTournament": "Partier i denne turnering", + "broadcastScore": "Score", + "broadcastNbBroadcasts": "{count, plural, =1{{count} udsendelse} other{{count} udsendelser}}", "challengeChallengesX": "Udfordringer: {param1}", "challengeChallengeToPlay": "Udfordr til et spil", "challengeChallengeDeclined": "Udfordring afvist", @@ -383,8 +449,8 @@ "puzzleThemeXRayAttackDescription": "En brik angriber eller forsvarer et felt gennem en af modstanderens brikker.", "puzzleThemeZugzwang": "Træktvang", "puzzleThemeZugzwangDescription": "Modstanderen har begrænsede muligheder for træk, og ethvert træk vil forværre positionen.", - "puzzleThemeHealthyMix": "Sund blanding", - "puzzleThemeHealthyMixDescription": "Lidt af hvert. Du kan ikke vide, hvad du skal forvente, så du skal være klar til alt! Præcis som i rigtige spil.", + "puzzleThemeMix": "Sund blanding", + "puzzleThemeMixDescription": "Lidt af hvert. Du kan ikke vide, hvad du skal forvente, så du skal være klar til alt! Præcis som i rigtige spil.", "puzzleThemePlayerGames": "Spiller-partier", "puzzleThemePlayerGamesDescription": "Find taktikopgaver lavet ud fra dine egne partier eller fra en anden spillers partier.", "puzzleThemePuzzleDownloadInformation": "Disse opgaver er i offentligt domæne og kan downloades fra {param}.", @@ -515,7 +581,6 @@ "memory": "Hukommelse", "infiniteAnalysis": "Uendelig analyse", "removesTheDepthLimit": "Fjerner dybdegrænsen, og holder din computer varm", - "engineManager": "Administration af skakprogram", "blunder": "Brøler", "mistake": "Fejl", "inaccuracy": "Unøjagtighed", @@ -597,6 +662,7 @@ "rank": "Rang", "rankX": "Rangering: {param}", "gamesPlayed": "Antal partier spillet", + "ok": "Ok", "cancel": "Annuller", "whiteTimeOut": "Tid udløbet for hvid", "blackTimeOut": "Tid udløbet for sort", @@ -819,7 +885,9 @@ "cheat": "Snyd", "troll": "Troll", "other": "Andet", - "reportDescriptionHelp": "Indsæt et link til partiet (eller partierne) og forklar hvad der er i vejen med brugerens opførsel.", + "reportCheatBoostHelp": "Indsæt linket til partiet (eller partierne) og forklar hvad der er i vejen med brugerens opførsel. Sig ikke blot \"de snyder\", men fortæl os, hvordan du nåede frem til den konklusion.", + "reportUsernameHelp": "Forklar, hvad der er stødende ved dette brugernavn. Sig ikke blot \"det er stødende/upassende\", men fortæl os, hvordan du nåede frem til denne konklusion, især hvis fornærmelsen er sløret, ikke er på engelsk, er slang eller er en historisk/kulturel reference.", + "reportProcessedFasterInEnglish": "Din indberetning vil blive behandlet hurtigere, hvis den er skrevet på engelsk.", "error_provideOneCheatedGameLink": "Angiv mindst ét link til et parti med snyd.", "by": "Af {param}", "importedByX": "Importeret af {param}", @@ -1217,6 +1285,7 @@ "showMeEverything": "Vis mig alt", "lichessPatronInfo": "Lichess er en velgørenhedsorganisation og helt gratis/libre open source-software.\nAlle driftsomkostninger, udvikling og indhold finansieres udelukkende af brugerdonationer.", "nothingToSeeHere": "Intet at se her i øjeblikket.", + "stats": "Statistik", "opponentLeftCounter": "{count, plural, =1{Din modstander har forladt partiet. Du kan kræve at få tildelt sejren om {count} sekund.} other{Din modstander har forladt partiet. Du kan kræve at få tildelt sejren om {count} sekunder.}}", "mateInXHalfMoves": "{count, plural, =1{Mat i {count} halv-træk} other{Mat i {count} halv-træk}}", "nbBlunders": "{count, plural, =1{{count} brøler} other{{count} brølere}}", @@ -1313,6 +1382,159 @@ "stormXRuns": "{count, plural, =1{Første runde} other{{count} runder}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Spillede en runde af {param2}} other{Spillede {count} runder af {param2}}}", "streamerLichessStreamers": "Lichess-streamere", + "studyPrivate": "Privat", + "studyMyStudies": "Mine studier", + "studyStudiesIContributeTo": "Studier jeg bidrager til", + "studyMyPublicStudies": "Mine offentlige studier", + "studyMyPrivateStudies": "Mine private studier", + "studyMyFavoriteStudies": "Mine favoritstudier", + "studyWhatAreStudies": "Hvad er studier?", + "studyAllStudies": "Alle studier", + "studyStudiesCreatedByX": "Studier oprettet af {param}", + "studyNoneYet": "Ingen endnu.", + "studyHot": "Populært", + "studyDateAddedNewest": "Dato tilføjet (nyeste)", + "studyDateAddedOldest": "Dato tilføjet (ældste)", + "studyRecentlyUpdated": "Nyligt opdateret", + "studyMostPopular": "Mest populære", + "studyAlphabetical": "Alfabetisk", + "studyAddNewChapter": "Tilføj et nyt kapitel", + "studyAddMembers": "Tilføj medlemmer", + "studyInviteToTheStudy": "Inviter til studiet", + "studyPleaseOnlyInvitePeopleYouKnow": "Inviter venligst kun personer du kender, og som ønsker at være en del af dette studie.", + "studySearchByUsername": "Søg på brugernavn", + "studySpectator": "Tilskuer", + "studyContributor": "Bidragsyder", + "studyKick": "Smid ud", + "studyLeaveTheStudy": "Forlad dette studie", + "studyYouAreNowAContributor": "Du er nu bidragsyder", + "studyYouAreNowASpectator": "Du er nu tilskuer", + "studyPgnTags": "PGN tags", + "studyLike": "Synes godt om", + "studyUnlike": "Synes ikke godt om", + "studyNewTag": "Nyt tag", + "studyCommentThisPosition": "Kommenter på denne stilling", + "studyCommentThisMove": "Kommenter på dette træk", + "studyAnnotateWithGlyphs": "Annoter med glyffer", + "studyTheChapterIsTooShortToBeAnalysed": "Dette kapitel er for kort til at blive analyseret.", + "studyOnlyContributorsCanRequestAnalysis": "Kun studiets bidragsydere kan anmode om en computeranalyse.", + "studyGetAFullComputerAnalysis": "Få en fuld server-computeranalyse af hovedlinjen.", + "studyMakeSureTheChapterIsComplete": "Sikr dig at kapitlet er færdigt. Du kan kun anmode om analyse én gang.", + "studyAllSyncMembersRemainOnTheSamePosition": "Alle SYNC medlemmer forbliver på samme stilling", + "studyShareChanges": "Del ændringer med tilskuere og gem dem på serveren", + "studyPlaying": "Spiller", + "studyShowEvalBar": "Evalueringssøjler", + "studyFirst": "Første", + "studyPrevious": "Forrige", + "studyNext": "Næste", + "studyLast": "Sidste", "studyShareAndExport": "Del & eksport", - "studyStart": "Start" + "studyCloneStudy": "Klon", + "studyStudyPgn": "Studie PGN", + "studyDownloadAllGames": "Download alle partier", + "studyChapterPgn": "Kapitel PGN", + "studyCopyChapterPgn": "Kopier PGN", + "studyDownloadGame": "Download parti", + "studyStudyUrl": "Studie URL", + "studyCurrentChapterUrl": "Nuværende kapitel URL", + "studyYouCanPasteThisInTheForumToEmbed": "Du kan indsætte dette i forummet for at indlejre", + "studyStartAtInitialPosition": "Start ved indledende stilling", + "studyStartAtX": "Start ved {param}", + "studyEmbedInYourWebsite": "Indlejr på din hjemmeside eller blog", + "studyReadMoreAboutEmbedding": "Læs mere om indlejring", + "studyOnlyPublicStudiesCanBeEmbedded": "Kun offentlige studier kan indlejres!", + "studyOpen": "Åbn", + "studyXBroughtToYouByY": "{param1} bragt til dig af {param2}", + "studyStudyNotFound": "Studie ikke fundet", + "studyEditChapter": "Rediger kapitel", + "studyNewChapter": "Nyt kapitel", + "studyImportFromChapterX": "Import fra {param}", + "studyOrientation": "Retning", + "studyAnalysisMode": "Analysetilstand", + "studyPinnedChapterComment": "Fastgjort kapitelkommentar", + "studySaveChapter": "Gem kapitel", + "studyClearAnnotations": "Ryd annoteringer", + "studyClearVariations": "Ryd varianter", + "studyDeleteChapter": "Slet kapitel", + "studyDeleteThisChapter": "Slet dette kapitel? Du kan ikke fortryde!", + "studyClearAllCommentsInThisChapter": "Ryd alle kommentarer og figurer i dette kapitel?", + "studyRightUnderTheBoard": "Lige under brættet", + "studyNoPinnedComment": "Ingen", + "studyNormalAnalysis": "Normal analyse", + "studyHideNextMoves": "Skjul næste træk", + "studyInteractiveLesson": "Interaktiv lektion", + "studyChapterX": "Kapitel {param}", + "studyEmpty": "Tom", + "studyStartFromInitialPosition": "Start ved indledende stilling", + "studyEditor": "Editor", + "studyStartFromCustomPosition": "Start fra brugerdefinerede stilling", + "studyLoadAGameByUrl": "Indlæs et parti fra URL", + "studyLoadAPositionFromFen": "Indlæs en stilling fra FEN", + "studyLoadAGameFromPgn": "Indlæs et parti fra PGN", + "studyAutomatic": "Automatisk", + "studyUrlOfTheGame": "URL for partiet", + "studyLoadAGameFromXOrY": "Indlæs et parti fra {param1} eller {param2}", + "studyCreateChapter": "Opret kapitel", + "studyCreateStudy": "Opret studie", + "studyEditStudy": "Rediger studie", + "studyVisibility": "Synlighed", + "studyPublic": "Offentlig", + "studyUnlisted": "Ikke listet", + "studyInviteOnly": "Kun inviterede", + "studyAllowCloning": "Tillad kloning", + "studyNobody": "Ingen", + "studyOnlyMe": "Kun mig", + "studyContributors": "Bidragydere", + "studyMembers": "Medlemmer", + "studyEveryone": "Enhver", + "studyEnableSync": "Aktiver synk", + "studyYesKeepEveryoneOnTheSamePosition": "Ja: behold alle på den samme stilling", + "studyNoLetPeopleBrowseFreely": "Nej: lad folk gennemse frit", + "studyPinnedStudyComment": "Fastgjort studie-kommentar", + "studyStart": "Start", + "studySave": "Gem", + "studyClearChat": "Ryd chat", + "studyDeleteTheStudyChatHistory": "Slet studiets chat-historik? Du kan ikke fortryde!", + "studyDeleteStudy": "Slet studie", + "studyConfirmDeleteStudy": "Slet hele studiet? Det kan ikke fortrydes! Skriv navnet på studiet for at bekræfte: {param}", + "studyWhereDoYouWantToStudyThat": "Hvor vil du studere det?", + "studyGoodMove": "Godt træk", + "studyMistake": "Fejl", + "studyBrilliantMove": "Fremragende træk", + "studyBlunder": "Brøler", + "studyInterestingMove": "Interessant træk", + "studyDubiousMove": "Tvivlsomt træk", + "studyOnlyMove": "Eneste mulige træk", + "studyZugzwang": "Træktvang", + "studyEqualPosition": "Lige stilling", + "studyUnclearPosition": "Uafklaret stilling", + "studyWhiteIsSlightlyBetter": "Hvid står lidt bedre", + "studyBlackIsSlightlyBetter": "Sort står lidt bedre", + "studyWhiteIsBetter": "Hvid står bedre", + "studyBlackIsBetter": "Sort står bedre", + "studyWhiteIsWinning": "Hvid vinder", + "studyBlackIsWinning": "Sort vinder", + "studyNovelty": "Nyfunden", + "studyDevelopment": "Udvikling", + "studyInitiative": "Initiativ", + "studyAttack": "Angreb", + "studyCounterplay": "Modspil", + "studyTimeTrouble": "Tidsproblemer", + "studyWithCompensation": "Med kompensation", + "studyWithTheIdea": "Med ideen", + "studyNextChapter": "Næste kapitel", + "studyPrevChapter": "Forrige kapitel", + "studyStudyActions": "Studiehandlinger", + "studyTopics": "Emner", + "studyMyTopics": "Mine emner", + "studyPopularTopics": "Populære emner", + "studyManageTopics": "Administrér emner", + "studyBack": "Tilbage", + "studyPlayAgain": "Spil igen", + "studyWhatWouldYouPlay": "Hvad ville du spille i denne position?", + "studyYouCompletedThisLesson": "Tillykke! Du har fuldført denne lektion.", + "studyNbChapters": "{count, plural, =1{{count} kapitel} other{{count} kapitler}}", + "studyNbGames": "{count, plural, =1{{count} parti} other{{count} partier}}", + "studyNbMembers": "{count, plural, =1{{count} Medlem} other{{count} Medlemmer}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Indsæt din PGN-tekst her, op til {count} parti} other{Indsæt din PGN-tekst her, op til {count} partier}}" } \ No newline at end of file diff --git a/lib/l10n/lila_de.arb b/lib/l10n/lila_de.arb index 81b1798b89..076be42dde 100644 --- a/lib/l10n/lila_de.arb +++ b/lib/l10n/lila_de.arb @@ -3,7 +3,7 @@ "mobilePuzzlesTab": "Aufgaben", "mobileToolsTab": "Werkzeuge", "mobileWatchTab": "Zuschauen", - "mobileSettingsTab": "Einstellungen", + "mobileSettingsTab": "Optionen", "mobileMustBeLoggedIn": "Du musst eingeloggt sein, um diese Seite anzuzeigen.", "mobileSystemColors": "Systemfarben", "mobileFeedbackButton": "Feedback", @@ -30,7 +30,6 @@ "mobilePuzzleStormConfirmEndRun": "Möchtest du diesen Durchlauf beenden?", "mobilePuzzleStormFilterNothingToShow": "Nichts anzuzeigen, bitte passe deine Filter an", "mobileCancelTakebackOffer": "Zugzurücknahme-Angebot abbrechen", - "mobileCancelDrawOffer": "Remisangebot zurücknehmen", "mobileWaitingForOpponentToJoin": "Warte auf Beitritt eines Gegners...", "mobileBlindfoldMode": "Blind spielen", "mobileLiveStreamers": "Livestreamer", @@ -38,10 +37,11 @@ "mobileCorrespondenceClearSavedMove": "Gespeicherten Zug löschen", "mobileSomethingWentWrong": "Etwas ist schiefgelaufen.", "mobileShowResult": "Ergebnis anzeigen", - "mobilePuzzleThemesSubtitle": "Spiele Aufgaben aus deinen Lieblings-Öffnungen oder wähle ein Thema.", - "mobilePuzzleStormSubtitle": "Löse in 3 Minuten so viele Aufgaben wie möglich.", + "mobilePuzzleThemesSubtitle": "Spiele Aufgaben aus deinen Lieblings-Öffnungen oder wähle ein Theme.", + "mobilePuzzleStormSubtitle": "Löse so viele Aufgaben wie möglich in 3 Minuten.", "mobileGreeting": "Hallo, {param}", "mobileGreetingWithoutName": "Hallo", + "mobilePrefMagnifyDraggedPiece": "Vergrößern der gezogenen Figur", "activityActivity": "Verlauf", "activityHostedALiveStream": "Hat live gestreamt", "activityRankedInSwissTournament": "Hat Platz #{param1} im Turnier {param2} belegt", @@ -54,6 +54,7 @@ "activityPlayedNbMoves": "{count, plural, =1{Spielte {count} Zug} other{Spielte {count} Züge}}", "activityInNbCorrespondenceGames": "{count, plural, =1{in {count} Fernschachpartie} other{in {count} Fernschachpartien}}", "activityCompletedNbGames": "{count, plural, =1{Hat {count} Fernschachpartie gespielt} other{Hat {count} Fernschachpartien gespielt}}", + "activityCompletedNbVariantGames": "{count, plural, =1{Hat {count} {param2}-Fernschachpartie gespielt} other{Hat {count} {param2}-Fernschachpartien gespielt}}", "activityFollowedNbPlayers": "{count, plural, =1{Folgt {count} Spieler} other{Folgt {count} Spielern}}", "activityGainedNbFollowers": "{count, plural, =1{Hat {count} neuen Follower} other{Hat {count} neue Follower}}", "activityHostedNbSimuls": "{count, plural, =1{Hat {count} Simultanvorstellung gegeben} other{Hat {count} Simultanvorstellungen gegeben}}", @@ -64,7 +65,72 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{Hat an {count} Turnier nach Schweizer System teilgenommen} other{Hat an {count} Turnieren nach Schweizer System teilgenommen}}", "activityJoinedNbTeams": "{count, plural, =1{Ist {count} Team beigetreten} other{Ist {count} Teams beigetreten}}", "broadcastBroadcasts": "Übertragungen", + "broadcastMyBroadcasts": "Meine Übertragungen", "broadcastLiveBroadcasts": "Live-Turnierübertragungen", + "broadcastBroadcastCalendar": "Sendekalender", + "broadcastNewBroadcast": "Neue Liveübertragung", + "broadcastSubscribedBroadcasts": "Abonnierte Übertragungen", + "broadcastAboutBroadcasts": "Über Übertragungen", + "broadcastHowToUseLichessBroadcasts": "Wie man Lichess-Übertragungen benutzt.", + "broadcastTheNewRoundHelp": "Die nächste Runde wird die gleichen Mitspieler und Mitwirkende haben wie die vorhergehende.", + "broadcastAddRound": "Eine Runde hinzufügen", + "broadcastOngoing": "Laufend", + "broadcastUpcoming": "Demnächst", + "broadcastCompleted": "Beendet", + "broadcastCompletedHelp": "Lichess erkennt Rundenabschlüsse basierend auf den Quellspielen. Verwende diesen Schalter, wenn keine Quelle vorhanden ist.", + "broadcastRoundName": "Rundenname", + "broadcastRoundNumber": "Rundennummer", + "broadcastTournamentName": "Turniername", + "broadcastTournamentDescription": "Kurze Turnierbeschreibung", + "broadcastFullDescription": "Vollständige Ereignisbeschreibung", + "broadcastFullDescriptionHelp": "Optionale, ausführliche Beschreibung der Übertragung. {param1} ist verfügbar. Die Beschreibung muss kürzer als {param2} Zeichen sein.", + "broadcastSourceSingleUrl": "PGN Quell-URL", + "broadcastSourceUrlHelp": "URL die Lichess abfragt um PGN Aktualisierungen zu erhalten. Sie muss öffentlich aus dem Internet zugänglich sein.", + "broadcastSourceGameIds": "Bis zu 64 Lichess Partie-IDs, getrennt durch Leerzeichen.", + "broadcastStartDateTimeZone": "Startdatum in der Zeitzone des Tunierstandortes: {param}", + "broadcastStartDateHelp": "Optional, falls du weißt wann das Ereignis beginnt", + "broadcastCurrentGameUrl": "URL der aktuellen Partie", + "broadcastDownloadAllRounds": "Alle Runden herunterladen", + "broadcastResetRound": "Diese Runde zurücksetzen", + "broadcastDeleteRound": "Diese Runde löschen", + "broadcastDefinitivelyDeleteRound": "Lösche die Runde und ihre Partien endgültig.", + "broadcastDeleteAllGamesOfThisRound": "Lösche alle Partien dieser Runde. Die Quelle muss aktiv sein, um sie neu zu erstellen.", + "broadcastEditRoundStudy": "Rundenstudie bearbeiten", + "broadcastDeleteTournament": "Dieses Turnier löschen", + "broadcastDefinitivelyDeleteTournament": "Lösche definitiv das gesamte Turnier, alle seine Runden und Partien.", + "broadcastShowScores": "Punktestand der Spieler basierend auf Spielergebnissen anzeigen", + "broadcastReplacePlayerTags": "Optional: Spielernamen, Wertungen und Titel ersetzen", + "broadcastFideFederations": "FIDE-Verbände", + "broadcastTop10Rating": "Top-10-Wertung", + "broadcastFidePlayers": "FIDE-Spieler", + "broadcastFidePlayerNotFound": "FIDE-Spieler nicht gefunden", + "broadcastFideProfile": "FIDE-Profil", + "broadcastFederation": "Verband", + "broadcastAgeThisYear": "Alter in diesem Jahr", + "broadcastUnrated": "Ungewertet", + "broadcastRecentTournaments": "Letzte Turniere", + "broadcastOpenLichess": "In Lichess öffnen", + "broadcastTeams": "Teams", + "broadcastBoards": "Bretter", + "broadcastOverview": "Überblick", + "broadcastSubscribeTitle": "Abonnieren, um bei Rundenbeginn benachrichtigt zu werden. Du kannst in deinen Benutzereinstellungen für Übertragungen zwischen einer Benachrichtigung per Glocke oder per Push-Benachrichtigung wählen.", + "broadcastUploadImage": "Turnierbild hochladen", + "broadcastNoBoardsYet": "Noch keine Bretter vorhanden. Diese werden angezeigt, sobald die Partien hochgeladen werden.", + "broadcastBoardsCanBeLoaded": "Die Bretter können per Quelle oder via {param} geladen werden", + "broadcastStartsAfter": "Beginnt nach {param}", + "broadcastStartVerySoon": "Diese Übertragung wird in Kürze beginnen.", + "broadcastNotYetStarted": "Die Übertragung hat noch nicht begonnen.", + "broadcastOfficialWebsite": "Offizielle Webseite", + "broadcastStandings": "Rangliste", + "broadcastIframeHelp": "Weitere Optionen auf der {param}", + "broadcastWebmastersPage": "Webmaster-Seite", + "broadcastPgnSourceHelp": "Eine öffentliche Echtzeit-PGN-Quelle für diese Runde. Wir bieten auch eine {param} für eine schnellere und effizientere Synchronisation.", + "broadcastEmbedThisBroadcast": "Bette diese Übertragung in deine Webseite ein", + "broadcastEmbedThisRound": "Bette {param} in deine Webseite ein", + "broadcastRatingDiff": "Wertungsdifferenz", + "broadcastGamesThisTournament": "Partien in diesem Turnier", + "broadcastScore": "Punktestand", + "broadcastNbBroadcasts": "{count, plural, =1{{count} Übertragung} other{{count} Übertragungen}}", "challengeChallengesX": "Herausforderungen: {param1}", "challengeChallengeToPlay": "Zu einer Partie herausfordern", "challengeChallengeDeclined": "Herausforderung abgelehnt", @@ -383,8 +449,8 @@ "puzzleThemeXRayAttackDescription": "Eine Figur attackiert oder verteidigt ein Feld durch eine gegnerische Figur hindurch.", "puzzleThemeZugzwang": "Zugzwang", "puzzleThemeZugzwangDescription": "Der Gegner ist in der Anzahl seiner Züge limitiert und jeder seiner Züge verschlechtert seine Stellung.", - "puzzleThemeHealthyMix": "Gesunder Mix", - "puzzleThemeHealthyMixDescription": "Ein bisschen von Allem. Du weißt nicht, was dich erwartet, deshalb bleibst du auf alles vorbereitet! Genau wie in echten Partien.", + "puzzleThemeMix": "Gesunder Mix", + "puzzleThemeMixDescription": "Ein bisschen von Allem. Du weißt nicht, was dich erwartet, deshalb bleibst du auf alles vorbereitet! Genau wie in echten Partien.", "puzzleThemePlayerGames": "Partien von Spielern", "puzzleThemePlayerGamesDescription": "Suche Aufgaben, die aus deinen Partien, oder den Partien eines anderen Spielers generiert wurden.", "puzzleThemePuzzleDownloadInformation": "Diese Aufgaben sind öffentlich zugänglich und können unter {param} heruntergeladen werden.", @@ -515,7 +581,6 @@ "memory": "Arbeitsspeicher", "infiniteAnalysis": "Endlose Analyse", "removesTheDepthLimit": "Entfernt die Tiefenbegrenzung und hält deinen Computer warm", - "engineManager": "Engineverwaltung", "blunder": "Grober Patzer", "mistake": "Fehler", "inaccuracy": "Ungenauigkeit", @@ -597,6 +662,7 @@ "rank": "Rang", "rankX": "Platz: {param}", "gamesPlayed": "Gespielte Partien", + "ok": "OK", "cancel": "Abbrechen", "whiteTimeOut": "Zeitüberschreitung von Weiß", "blackTimeOut": "Zeitüberschreitung von Schwarz", @@ -819,7 +885,9 @@ "cheat": "Betrug", "troll": "Troll", "other": "Sonstiges", - "reportDescriptionHelp": "Füge den Link zu einer oder mehreren Partien ein und erkläre die Auffälligkeiten bezüglich des Spielerverhaltens. Bitte schreibe nicht einfach nur „dieser Spieler betrügt“, sondern begründe auch, wie Du zu diesem Schluss kommst. Dein Bericht wird schneller bearbeitet, wenn er in englischer Sprache verfasst ist.", + "reportCheatBoostHelp": "Füge den Link zu einer oder mehreren Partien ein und erkläre die Auffälligkeiten bezüglich des Verhaltens des Spielers. Bitte schreibe nicht einfach nur „Die schummeln (bzw. betrügen)“, sondern begründe auch, wie Du zu diesem Schluss kommst.", + "reportUsernameHelp": "Erkläre, was an diesem Benutzernamen beleidigend oder unangemessen ist. Sage nicht einfach \"Der Name ist beleidigend/unangemessen\", sondern erkläre, wie du zu dieser Schlussfolgerung gekommen bist. Insbesondere wenn die Beleidigung verschleiert wird, nicht auf Englisch, ist in Slang, oder ist ein historischer/kultureller Bezugspunkt.", + "reportProcessedFasterInEnglish": "Ihr Bericht wird schneller bearbeitet, wenn er auf Englisch verfasst ist.", "error_provideOneCheatedGameLink": "Bitte gib mindestens einen Link zu einem Spiel an, in dem betrogen wurde.", "by": "von {param}", "importedByX": "Importiert von {param}", @@ -1210,13 +1278,14 @@ "since": "Seit", "until": "Bis", "lichessDbExplanation": "Aus gewerteten Partien aller Lichess-Spieler", - "switchSides": "andere Farbe", + "switchSides": "Seitenwechsel", "closingAccountWithdrawAppeal": "Dein Benutzerkonto zu schließen wird auch deinen Einspruch zurückziehen", "ourEventTips": "Unsere Tipps für die Organisation von Veranstaltungen", "instructions": "Anleitung", "showMeEverything": "Alles zeigen", "lichessPatronInfo": "Lichess ist eine Wohltätigkeitsorganisation und eine völlig kostenlose/freie Open-Source-Software.\nAlle Betriebskosten, Entwicklung und Inhalte werden ausschließlich durch Benutzerspenden finanziert.", "nothingToSeeHere": "Im Moment gibt es hier nichts zu sehen.", + "stats": "Statistiken", "opponentLeftCounter": "{count, plural, =1{Dein Gegner hat die Partie verlassen. Du kannst in {count} Sekunde den Sieg beanspruchen.} other{Dein Gegner hat die Partie verlassen. Du kannst in {count} Sekunden den Sieg beanspruchen.}}", "mateInXHalfMoves": "{count, plural, =1{Matt in {count} Halbzug} other{Matt in {count} Halbzügen}}", "nbBlunders": "{count, plural, =1{{count} grober Patzer} other{{count} grobe Patzer}}", @@ -1313,6 +1382,159 @@ "stormXRuns": "{count, plural, =1{1 Durchlauf} other{{count} Durchläufe}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Hat einen Durchlauf von {param2} gespielt} other{Hat {count} Durchläufe von {param2} gespielt}}", "streamerLichessStreamers": "Lichess Streamer", + "studyPrivate": "Privat", + "studyMyStudies": "Meine Studien", + "studyStudiesIContributeTo": "Studien, an denen ich mitwirke", + "studyMyPublicStudies": "Meine öffentlichen Studien", + "studyMyPrivateStudies": "Meine privaten Studien", + "studyMyFavoriteStudies": "Meine Lieblingsstudien", + "studyWhatAreStudies": "Was sind Studien?", + "studyAllStudies": "Alle Studien", + "studyStudiesCreatedByX": "Von {param} erstellte Studien", + "studyNoneYet": "Bisher keine.", + "studyHot": "Angesagt", + "studyDateAddedNewest": "Veröffentlichungsdatum (neueste)", + "studyDateAddedOldest": "Veröffentlichungsdatum (älteste)", + "studyRecentlyUpdated": "Kürzlich aktualisiert", + "studyMostPopular": "Beliebteste", + "studyAlphabetical": "Alphabetisch", + "studyAddNewChapter": "Neues Kapitel hinzufügen", + "studyAddMembers": "Mitglieder hinzufügen", + "studyInviteToTheStudy": "Zur Studie einladen", + "studyPleaseOnlyInvitePeopleYouKnow": "Bitte lade nur Leute ein, die dich kennen und die aktiv an dieser Studie teilnehmen möchten.", + "studySearchByUsername": "Suche nach Benutzernamen", + "studySpectator": "Zuschauer", + "studyContributor": "Mitwirkender", + "studyKick": "Rauswerfen", + "studyLeaveTheStudy": "Studie verlassen", + "studyYouAreNowAContributor": "Du bist jetzt ein Mitwirkender", + "studyYouAreNowASpectator": "Du bist jetzt Zuschauer", + "studyPgnTags": "PGN Tags", + "studyLike": "Gefällt mir", + "studyUnlike": "Gefällt mir nicht mehr", + "studyNewTag": "Neuer Tag", + "studyCommentThisPosition": "Kommentiere diese Stellung", + "studyCommentThisMove": "Kommentiere diesen Zug", + "studyAnnotateWithGlyphs": "Mit Symbolen kommentieren", + "studyTheChapterIsTooShortToBeAnalysed": "Das Kapitel ist zu kurz zum Analysieren.", + "studyOnlyContributorsCanRequestAnalysis": "Nur Mitwirkende an der Studie können eine Computeranalyse anfordern.", + "studyGetAFullComputerAnalysis": "Erhalte eine vollständige serverseitige Computeranalyse der Hauptvariante.", + "studyMakeSureTheChapterIsComplete": "Stelle sicher, dass das Kapitel vollständig ist. Die Analyse kann nur einmal angefordert werden.", + "studyAllSyncMembersRemainOnTheSamePosition": "Alle synchronisierten Mitglieder sehen die gleiche Stellung", + "studyShareChanges": "Teile Änderungen mit den Zuschauern und speichere sie auf dem Server", + "studyPlaying": "Laufende Partien", + "studyShowEvalBar": "Stellungsbewertungs-Balken", + "studyFirst": "Erste Seite", + "studyPrevious": "Zurück", + "studyNext": "Weiter", + "studyLast": "Letzte Seite", "studyShareAndExport": "Teilen und exportieren", - "studyStart": "Start" + "studyCloneStudy": "Klonen", + "studyStudyPgn": "Studien PGN", + "studyDownloadAllGames": "Lade alle Partien herunter", + "studyChapterPgn": "Kapitel PGN", + "studyCopyChapterPgn": "PGN kopieren", + "studyDownloadGame": "Lade die Partie herunter", + "studyStudyUrl": "Studien URL", + "studyCurrentChapterUrl": "URL des aktuellen Kapitels", + "studyYouCanPasteThisInTheForumToEmbed": "Zum Einbinden füge dies im Forum ein", + "studyStartAtInitialPosition": "Beginne mit der Anfangsstellung", + "studyStartAtX": "Beginne mit {param}", + "studyEmbedInYourWebsite": "In deine Webseite oder deinen Blog einbetten", + "studyReadMoreAboutEmbedding": "Lies mehr über das Einbinden", + "studyOnlyPublicStudiesCanBeEmbedded": "Nur öffentliche Studien können eingebunden werden!", + "studyOpen": "Öffnen", + "studyXBroughtToYouByY": "{param1} präsentiert von {param2}", + "studyStudyNotFound": "Studie nicht gefunden", + "studyEditChapter": "Kapitel bearbeiten", + "studyNewChapter": "Neues Kapitel", + "studyImportFromChapterX": "Importiere aus {param}", + "studyOrientation": "Ausrichtung", + "studyAnalysisMode": "Analysemodus", + "studyPinnedChapterComment": "Angepinnte Kapitelkommentare", + "studySaveChapter": "Kapitel speichern", + "studyClearAnnotations": "Anmerkungen löschen", + "studyClearVariations": "Varianten löschen", + "studyDeleteChapter": "Kapitel löschen", + "studyDeleteThisChapter": "Kapitel löschen. Dies kann nicht rückgängig gemacht werden!", + "studyClearAllCommentsInThisChapter": "Alle Kommentare, Symbole und gezeichnete Formen in diesem Kapitel löschen", + "studyRightUnderTheBoard": "Direkt unterhalb des Bretts", + "studyNoPinnedComment": "Keine", + "studyNormalAnalysis": "Normale Analyse", + "studyHideNextMoves": "Nächste Züge ausblenden", + "studyInteractiveLesson": "Interaktive Übung", + "studyChapterX": "Kapitel {param}", + "studyEmpty": "Leer", + "studyStartFromInitialPosition": "Von Ausgangsstellung starten", + "studyEditor": "Editor", + "studyStartFromCustomPosition": "Von benutzerdefinierter Stellung starten", + "studyLoadAGameByUrl": "Lade eine Partie mittels URL", + "studyLoadAPositionFromFen": "Lade eine Partie mittels FEN", + "studyLoadAGameFromPgn": "Lade eine Partie mittels PGN", + "studyAutomatic": "Automatisch", + "studyUrlOfTheGame": "URL der Partie", + "studyLoadAGameFromXOrY": "Partie von {param1} oder {param2} laden", + "studyCreateChapter": "Kapitel erstellen", + "studyCreateStudy": "Studie erstellen", + "studyEditStudy": "Studie bearbeiten", + "studyVisibility": "Sichtbarkeit", + "studyPublic": "Öffentlich", + "studyUnlisted": "Ungelistet", + "studyInviteOnly": "Nur mit Einladung", + "studyAllowCloning": "Klonen erlaubt", + "studyNobody": "Niemand", + "studyOnlyMe": "Nur ich", + "studyContributors": "Mitwirkende", + "studyMembers": "Mitglieder", + "studyEveryone": "Alle", + "studyEnableSync": "Sync aktivieren", + "studyYesKeepEveryoneOnTheSamePosition": "Ja: Gleiche Stellung für alle", + "studyNoLetPeopleBrowseFreely": "Nein: Unabhängige Navigation für alle", + "studyPinnedStudyComment": "Angepinnter Studienkommentar", + "studyStart": "Start", + "studySave": "Speichern", + "studyClearChat": "Chat löschen", + "studyDeleteTheStudyChatHistory": "Chatverlauf der Studie löschen? Dies kann nicht rückgängig gemacht werden!", + "studyDeleteStudy": "Studie löschen", + "studyConfirmDeleteStudy": "Die gesamte Studie löschen? Es gibt kein Zurück! Gib zur Bestätigung den Namen der Studie ein: {param}", + "studyWhereDoYouWantToStudyThat": "Welche Studie möchtest du nutzen?", + "studyGoodMove": "Guter Zug", + "studyMistake": "Fehler", + "studyBrilliantMove": "Brillanter Zug", + "studyBlunder": "Grober Patzer", + "studyInterestingMove": "Interessanter Zug", + "studyDubiousMove": "Fragwürdiger Zug", + "studyOnlyMove": "Einziger Zug", + "studyZugzwang": "Zugzwang", + "studyEqualPosition": "Ausgeglichene Stellung", + "studyUnclearPosition": "Unklare Stellung", + "studyWhiteIsSlightlyBetter": "Weiß steht leicht besser", + "studyBlackIsSlightlyBetter": "Schwarz steht leicht besser", + "studyWhiteIsBetter": "Weiß steht besser", + "studyBlackIsBetter": "Schwarz steht besser", + "studyWhiteIsWinning": "Weiß steht auf Gewinn", + "studyBlackIsWinning": "Schwarz steht auf Gewinn", + "studyNovelty": "Neuerung", + "studyDevelopment": "Entwicklung", + "studyInitiative": "Initiative", + "studyAttack": "Angriff", + "studyCounterplay": "Gegenspiel", + "studyTimeTrouble": "Zeitnot", + "studyWithCompensation": "Mit Kompensation", + "studyWithTheIdea": "Mit der Idee", + "studyNextChapter": "Nächstes Kapitel", + "studyPrevChapter": "Vorheriges Kapitel", + "studyStudyActions": "Studien-Aktionen", + "studyTopics": "Themen", + "studyMyTopics": "Meine Themen", + "studyPopularTopics": "Beliebte Themen", + "studyManageTopics": "Themen verwalten", + "studyBack": "Zurück", + "studyPlayAgain": "Erneut spielen", + "studyWhatWouldYouPlay": "Was würdest du in dieser Stellung spielen?", + "studyYouCompletedThisLesson": "Gratulation! Du hast diese Lektion abgeschlossen.", + "studyNbChapters": "{count, plural, =1{{count} Kapitel} other{{count} Kapitel}}", + "studyNbGames": "{count, plural, =1{{count} Partie} other{{count} Partien}}", + "studyNbMembers": "{count, plural, =1{{count} Mitglied} other{{count} Mitglieder}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Füge deinen PGN Text hier ein, bis zu {count} Partie} other{Füge dein PGN Text hier ein, bis zu {count} Partien}}" } \ No newline at end of file diff --git a/lib/l10n/lila_el.arb b/lib/l10n/lila_el.arb index b7974e8820..1645d6ff90 100644 --- a/lib/l10n/lila_el.arb +++ b/lib/l10n/lila_el.arb @@ -1,8 +1,45 @@ { + "mobileHomeTab": "Αρχική", + "mobilePuzzlesTab": "Γρίφοι", + "mobileToolsTab": "Εργαλεία", + "mobileWatchTab": "Δείτε", + "mobileSettingsTab": "Ρυθμίσεις", + "mobileMustBeLoggedIn": "Πρέπει να συνδεθείτε για να δείτε αυτή τη σελίδα.", + "mobileSystemColors": "Χρώματα συστήματος", + "mobileFeedbackButton": "Πείτε μας τη γνώμη σας", + "mobileOkButton": "ΟΚ", + "mobileSettingsHapticFeedback": "Απόκριση δόνησης", + "mobileSettingsImmersiveModeSubtitle": "Αποκρύπτει τη διεπαφή του συστήματος όσο παίζεται. Ενεργοποιήστε εάν σας ενοχλούν οι χειρονομίες πλοήγησης του συστήματος στα άκρα της οθόνης. Ισχύει για την προβολή παιχνιδιού και το Puzzle Storm.", + "mobileNotFollowingAnyUser": "Δεν ακολουθείτε κανέναν χρήστη.", + "mobileAllGames": "Όλα τα παιχνίδια", + "mobileRecentSearches": "Πρόσφατες αναζητήσεις", + "mobileClearButton": "Εκκαθάριση", + "mobilePlayersMatchingSearchTerm": "Παίκτες με \"{param}\"", + "mobileNoSearchResults": "Δεν βρέθηκαν αποτελέσματα", + "mobileAreYouSure": "Είστε σίγουροι;", + "mobilePuzzleStormNothingToShow": "Δεν υπάρχουν στοιχεία. Παίξτε κάποιους γύρους Puzzle Storm.", + "mobileSharePuzzle": "Κοινοποίηση γρίφου", + "mobileShareGameURL": "Κοινοποίηση URL παιχνιδιού", + "mobileShareGamePGN": "Κοινοποίηση PGN", + "mobileSharePositionAsFEN": "Κοινοποίηση θέσης ως FEN", + "mobileShowVariations": "Εμφάνιση παραλλαγών", + "mobileHideVariation": "Απόκρυψη παραλλαγής", + "mobileShowComments": "Εμφάνιση σχολίων", + "mobilePuzzleStormConfirmEndRun": "Θέλετε να τερματίσετε αυτόν τον γύρο;", + "mobilePuzzleStormFilterNothingToShow": "Δεν υπάρχουν γρίφοι για τις συγκεκριμένες επιλογές φίλτρων, παρακαλώ δοκιμάστε κάποιες άλλες", + "mobileCancelTakebackOffer": "Ακυρώστε την προσφορά αναίρεσης της κίνησης", + "mobileWaitingForOpponentToJoin": "Αναμονή για αντίπαλο...", + "mobileBlindfoldMode": "Τυφλό", + "mobileLiveStreamers": "Streamers ζωντανά αυτή τη στιγμή", + "mobileCustomGameJoinAGame": "Συμμετοχή σε παιχνίδι", + "mobileCorrespondenceClearSavedMove": "Εκκαθάριση αποθηκευμένης κίνησης", + "mobileSomethingWentWrong": "Κάτι πήγε στραβά.", "mobileShowResult": "Εμφάνιση αποτελέσματος", + "mobilePuzzleThemesSubtitle": "Παίξτε γρίφους από τα αγαπημένα σας ανοίγματα, ή επιλέξτε θέμα.", "mobilePuzzleStormSubtitle": "Λύστε όσους γρίφους όσο το δυνατόν, σε 3 λεπτά.", "mobileGreeting": "Καλωσορίσατε, {param}", "mobileGreetingWithoutName": "Καλωσορίσατε", + "mobilePrefMagnifyDraggedPiece": "Μεγέθυνση του επιλεγμένου κομματιού", "activityActivity": "Δραστηριότητα", "activityHostedALiveStream": "Μεταδίδει ζωντανά", "activityRankedInSwissTournament": "Κατατάχθηκε #{param1} στο {param2}", @@ -15,6 +52,7 @@ "activityPlayedNbMoves": "{count, plural, =1{Έπαιξε {count} κίνηση} other{Έπαιξε {count} κινήσεις}}", "activityInNbCorrespondenceGames": "{count, plural, =1{σε {count} παρτίδα αλληλογραφίας} other{σε {count} παρτίδες αλληλογραφίας}}", "activityCompletedNbGames": "{count, plural, =1{Ολοκλήρωσε {count} παρτίδα αλληλογραφίας} other{Ολοκλήρωσε {count} παρτίδες αλληλογραφίας}}", + "activityCompletedNbVariantGames": "{count, plural, =1{Ολοκλήρωσε {count} παρτίδα αλληλογραφίας {param2}} other{Ολοκλήρωσε {count} παρτίδες αλληλογραφίας {param2}}}", "activityFollowedNbPlayers": "{count, plural, =1{Άρχισε να ακολουθεί {count} παίκτη} other{Άρχισε να ακολουθεί {count} παίκτες}}", "activityGainedNbFollowers": "{count, plural, =1{Απέκτησε {count} νέο ακόλουθο} other{Απέκτησε {count} νέους ακόλουθους}}", "activityHostedNbSimuls": "{count, plural, =1{Φιλοξένησε {count} σιμουλτανέ} other{Φιλοξένησε {count} σιμουλτανέ}}", @@ -25,7 +63,54 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{Αγωνίστηκε σε {count} ελβετικό τουρνουά} other{Αγωνίστηκε σε {count} ελβετικά τουρνουά}}", "activityJoinedNbTeams": "{count, plural, =1{Έγινε μέλος {count} ομάδας} other{Έγινε μέλος {count} ομαδών}}", "broadcastBroadcasts": "Αναμεταδόσεις", + "broadcastMyBroadcasts": "Οι αναμεταδόσεις μου", "broadcastLiveBroadcasts": "Αναμεταδόσεις ζωντανών τουρνούα", + "broadcastNewBroadcast": "Νέα ζωντανή αναμετάδοση", + "broadcastSubscribedBroadcasts": "Εγγεγραμμένες μεταδώσεις", + "broadcastAboutBroadcasts": "Σχετικά με εκπομπές", + "broadcastHowToUseLichessBroadcasts": "Πώς να χρησιμοποιήσετε τις εκπομπές Lichess.", + "broadcastTheNewRoundHelp": "Ο νέος γύρος θα έχει τα ίδια μέλη και τους ίδιους συνεισφέροντες όπως ο προηγούμενος.", + "broadcastAddRound": "Προσθήκη γύρου", + "broadcastOngoing": "Σε εξέλιξη", + "broadcastUpcoming": "Προσεχή", + "broadcastCompleted": "Ολοκληρώθηκε", + "broadcastCompletedHelp": "Το Lichess ανιχνεύει ολοκλήρωση γύρων, αλλά μπορεί να κάνει λάθος. Χρησιμοποιήστε αυτό για να το ρυθμίσετε χειροκίνητα.", + "broadcastRoundName": "Όνομα γύρου", + "broadcastRoundNumber": "Αριθμός γύρου", + "broadcastTournamentName": "Όνομα τουρνουά", + "broadcastTournamentDescription": "Σύντομη περιγραφή τουρνουά", + "broadcastFullDescription": "Πλήρης περιγραφή γεγονότος", + "broadcastFullDescriptionHelp": "Προαιρετική αναλυτική περιγραφή της αναμετάδοσης. Η μορφή {param1} είναι διαθέσιμη. Το μήκος πρέπει μικρότερο από {param2} χαρακτήρες.", + "broadcastSourceUrlHelp": "URL για λήψη PGN ενημερώσεων. Πρέπει να είναι δημόσια προσβάσιμο μέσω διαδικτύου.", + "broadcastStartDateHelp": "Προαιρετικό, εάν γνωρίζετε πότε αρχίζει η εκδήλωση", + "broadcastCurrentGameUrl": "Διεύθυνση URL αυτού του παιχνιδιού", + "broadcastDownloadAllRounds": "Λήψη όλων των γύρων", + "broadcastResetRound": "Επαναφορά αυτού του γύρου", + "broadcastDeleteRound": "Διαγραφή αυτού του γύρου", + "broadcastDefinitivelyDeleteRound": "Σίγουρα διαγράψτε τον γύρο και όλα τα παιχνίδια του.", + "broadcastDeleteAllGamesOfThisRound": "Διαγράψτε όλα τα παιχνίδια αυτού του γύρου. Η πηγή μετάδοσης θα πρέπει να είναι ενεργή για να τα ξαναδημιουργήσετε.", + "broadcastEditRoundStudy": "Επεξεργασία μελέτης γύρου", + "broadcastDeleteTournament": "Διαγραφή αυτού του τουρνουά", + "broadcastDefinitivelyDeleteTournament": "Σίγουρα διαγράψτε ολόκληρο τον διαγωνισμό, όλους τους γύρους του και όλα τα παιχνίδια του.", + "broadcastFideFederations": "Ομοσπονδίες FIDE", + "broadcastFidePlayers": "Παίκτες FIDE", + "broadcastFidePlayerNotFound": "Δε βρέθηκε παίκτης FIDE", + "broadcastFideProfile": "Προφίλ FIDE", + "broadcastFederation": "Ομοσπονδία", + "broadcastAgeThisYear": "Φετινή ηλικία", + "broadcastRecentTournaments": "Πρόσφατα τουρνουά", + "broadcastOpenLichess": "Άνοιγμα στο Lichess", + "broadcastTeams": "Ομάδες", + "broadcastBoards": "Σκακιέρες", + "broadcastOverview": "Επισκόπηση", + "broadcastUploadImage": "Ανεβάστε εικόνα τουρνουά", + "broadcastStartsAfter": "Ξεκινάει μετά από {param}", + "broadcastOfficialWebsite": "Επίσημη ιστοσελίδα", + "broadcastStandings": "Κατάταξη", + "broadcastRatingDiff": "Διαφορά βαθμολογίας", + "broadcastGamesThisTournament": "Παρτίδες σε αυτό το τουρνουά", + "broadcastScore": "Βαθμολογία", + "broadcastNbBroadcasts": "{count, plural, =1{{count} αναμετάδοση} other{{count} αναμεταδόσεις}}", "challengeChallengesX": "Προκλήσεις: {param1}", "challengeChallengeToPlay": "Προκαλέστε σε παιχνίδι", "challengeChallengeDeclined": "Η πρόκληση απορρίφθηκε", @@ -84,7 +169,7 @@ "perfStatNow": "τώρα", "preferencesPreferences": "Προτιμήσεις", "preferencesDisplay": "Εμφάνιση", - "preferencesPrivacy": "Απόρρητο", + "preferencesPrivacy": "Ιδιωτικότητα", "preferencesNotifications": "Ειδοποιήσεις", "preferencesPieceAnimation": "Κίνηση πιονιών", "preferencesMaterialDifference": "Διαφορά υλικού", @@ -121,7 +206,7 @@ "preferencesClaimDrawOnThreefoldRepetitionAutomatically": "Διεκδικήστε ισοπαλία αυτόματα σε τριπλή επανάληψη", "preferencesWhenTimeRemainingLessThanThirtySeconds": "Όταν απομένουν < 30 δευτερόλεπτα", "preferencesMoveConfirmation": "Επιβεβαίωση κίνησης", - "preferencesExplainCanThenBeTemporarilyDisabled": "Μπορεί να απενεργοποιηθεί κατά τη διάρκεια ενός παιχνιδιού με το μενού του ταμπλό", + "preferencesExplainCanThenBeTemporarilyDisabled": "Μπορεί να απενεργοποιηθεί κατά τη διάρκεια ενός παιχνιδιού με το μενού της σκακιέρας", "preferencesInCorrespondenceGames": "Στις παρτίδες δι' αλληλογραφίας", "preferencesCorrespondenceAndUnlimited": "Δι' αλληλογραφίας και απεριορίστου χρόνου", "preferencesConfirmResignationAndDrawOffers": "Επιβεβαίωση παραίτησης και προσφοράς ισοπαλίας", @@ -161,6 +246,8 @@ "puzzleSpecialMoves": "Ειδικές κινήσεις", "puzzleDidYouLikeThisPuzzle": "Σας άρεσε αυτός ο γρίφος;", "puzzleVoteToLoadNextOne": "Ψηφίστε για να προχωρήσετε στο επόμενο!", + "puzzleUpVote": "Μου άρεσε ο γρίφος", + "puzzleDownVote": "Δε μου άρεσε ο γρίφος", "puzzleYourPuzzleRatingWillNotChange": "Οι βαθμοί αξιολόγησής σας δε θα αλλάξουν. Αυτοί οι βαθμοί χρησιμεύουν στην επιλογή γρίφων για το επίπεδό σας και όχι στον ανταγωνισμό.", "puzzleFindTheBestMoveForWhite": "Βρείτε την καλύτερη κίνηση για τα λευκά.", "puzzleFindTheBestMoveForBlack": "Βρείτε την καλύτερη κίνηση για τα μαύρα.", @@ -174,6 +261,11 @@ "puzzleKeepGoing": "Συνεχίστε…", "puzzlePuzzleSuccess": "Επιτυχία!", "puzzlePuzzleComplete": "Ο γρίφος ολοκληρώθηκε!", + "puzzleByOpenings": "Ανά άνοιγμα", + "puzzlePuzzlesByOpenings": "Γρίφοι ανά άνοιγμα", + "puzzleOpeningsYouPlayedTheMost": "Ανοίγματα που παίξατε πιο συχνά σε βαθμολογημένες παρτίδες σκάκι", + "puzzleUseFindInPage": "Πατήστε «Εύρεση στη σελίδα» στο μενού του προγράμματος περιήγησης, για να βρείτε το αγαπημένο σας άνοιγμα!", + "puzzleUseCtrlF": "Πατήστε Ctrl+f για να βρείτε το αγαπημένο σας άνοιγμα!", "puzzleNotTheMove": "Δεν είναι αυτή η κίνηση!", "puzzleTrySomethingElse": "Δοκιμάστε κάτι άλλο.", "puzzleRatingX": "Βαθμολογία: {param}", @@ -336,8 +428,8 @@ "puzzleThemeXRayAttackDescription": "Κομμάτι που επιτίθεται ή αμύνεται ένα τετράγωνο, πίσω από εχθρικό κομμάτι.", "puzzleThemeZugzwang": "Τσούγκτσβανγκ", "puzzleThemeZugzwangDescription": "Ο αντίπαλος είναι περιορισμένος στις κινήσεις που μπορεί να κάνει και οποιαδήποτε κίνηση επιλέξει επιδεινώνει την θέση του.", - "puzzleThemeHealthyMix": "Προτεινόμενο μίγμα", - "puzzleThemeHealthyMixDescription": "Λίγο απ' όλα. Δεν ξέρετε τι να περιμένετε, οπότε παραμένετε σε ετοιμότητα! Όπως στα πραγματικά παιχνίδια.", + "puzzleThemeMix": "Προτεινόμενο μίγμα", + "puzzleThemeMixDescription": "Λίγο απ' όλα. Δεν ξέρετε τι να περιμένετε, οπότε παραμένετε σε ετοιμότητα! Όπως στα πραγματικά παιχνίδια.", "puzzleThemePlayerGames": "Παιχνίδια παίκτη", "puzzleThemePlayerGamesDescription": "Αναζητήστε γρίφους που δημιουργήθηκαν από παιχνίδια είτε δικά σας είτε άλλων παικτών.", "puzzleThemePuzzleDownloadInformation": "Αυτοί οι γρίφοι είναι δημόσιοι και μπορείτε να τους κατεβάσετε εδώ {param}.", @@ -416,6 +508,8 @@ "promoteVariation": "Προώθηση βαριάντας", "makeMainLine": "Δημιουργία κύριας γραμμής", "deleteFromHere": "Διαγραφή από εδώ", + "collapseVariations": "Σύμπτυξη παραλλαγών", + "expandVariations": "Εμφάνιση βαριάντων", "forceVariation": "Θέσε σε βαριάντα", "copyVariationPgn": "Αντιγραφή PGN αρχείου κινήσεων", "move": "Κίνηση", @@ -434,7 +528,7 @@ "averageRatingX": "Μέση βαθμολογία: {param}", "recentGames": "Πρόσφατα παιχνίδια", "topGames": "Κορυφαίες παρτίδες", - "masterDbExplanation": "Δύο εκατομμύρια OTB παρτίδες {param1} + παικτών με αξιολόγηση FIDE από {param2} έως {param3}", + "masterDbExplanation": "Παρτίδες OTB {param1} + παικτών με αξιολόγηση FIDE, από {param2} έως {param3}", "dtzWithRounding": "DTZ50'' με στρογγυλοποίηση, βάσει του αριθμού των κινήσεων μέχρι την επόμενη κίνηση πιονιού ή την επόμενη αλλαγή", "noGameFound": "Δεν βρέθηκε παρτίδα", "maxDepthReached": "Έχετε φτάσει το μέγιστο βάθος!", @@ -466,7 +560,6 @@ "memory": "Μνήμη", "infiniteAnalysis": "Άπειρη ανάλυση", "removesTheDepthLimit": "Καταργεί το όριο βάθους και κρατά τον υπολογιστή σας ζεστό", - "engineManager": "Διαχειριστής μηχανής", "blunder": "Σοβαρό σφάλμα", "mistake": "Λάθος", "inaccuracy": "Ανακρίβεια", @@ -492,6 +585,7 @@ "latestForumPosts": "Τελευταίες δημοσιεύσεις στο φόρουμ", "players": "Παίκτες", "friends": "Φίλοι", + "otherPlayers": "άλλους παίκτες", "discussions": "Συζητήσεις", "today": "Σήμερα", "yesterday": "Χθες", @@ -547,6 +641,7 @@ "rank": "Κατάταξη", "rankX": "Κατάταξη: {param}", "gamesPlayed": "Παιγμένα παιχνίδια", + "ok": "ΟΚ", "cancel": "Ακύρωση", "whiteTimeOut": "Τέλος χρόνου για τα λευκά", "blackTimeOut": "Τέλος χρόνου για τα μαύρα", @@ -722,6 +817,7 @@ "ifNoneLeaveEmpty": "Αν δεν υπάρχει, αφήστε κενό", "profile": "Προφίλ", "editProfile": "Επεξεργασία προφίλ", + "realName": "Πραγματικό όνομα", "setFlair": "Ορίστε τη νιφάδα σας", "flair": "Νιφάδα", "youCanHideFlair": "Υπάρχει μια ρύθμιση για να κρύψει όλες τις νιφάδες χρήστη σε ολόκληρη την ιστοσελίδα.", @@ -748,6 +844,8 @@ "descPrivateHelp": "Κείμενο που θα δουν μόνο τα μέλη της ομάδας. Εάν οριστεί, αντικαθιστά τη δημόσια περιγραφή για τα μέλη της ομάδας.", "no": "Όχι", "yes": "Ναι", + "website": "Ιστοσελίδα", + "mobile": "Εφαρμογή Κινητού", "help": "Βοήθεια:", "createANewTopic": "Δημιουργήστε καινούριο θέμα", "topics": "Θέματα", @@ -766,7 +864,9 @@ "cheat": "Απάτη", "troll": "Εμπαιγμός", "other": "Άλλο", - "reportDescriptionHelp": "Κάντε επικόλληση τον σύνδεσμο για το παιχνίδι(α) και εξηγήστε τι είναι παράξενο στη συμπεριφορά του χρήστη. Μην πείτε απλά «επειδή κλέβει», πείτε μας πως καταλήξατε σε αυτό το συμπέρασμα. Η αναφορά σας θα επεξεργαστεί πιο γρήγορα αν είναι γραμμένη στα αγγλικά.", + "reportCheatBoostHelp": "Επικολλήστε τους συνδέσμους με τα παιχνίδια και εξηγήστε μας γιατί θεωρείτε ότι η συμπεριφορά του χρήστη είναι παράξενη σε αυτά. Μη λέτε απλώς ότι «κλέβει» (\"they cheat\"), αλλά πείτε μας πως καταλήξατε σε αυτό το συμπέρασμα.", + "reportUsernameHelp": "Εξηγήστε μας γιατί είναι προσβλητικό το όνομα αυτού του χρήστη. Μη λέτε απλώς ότι \"είναι προσβλητικό/ακατάλληλο\" (\"it's offensive/inappropriate\"), αλλά πείτε μας πώς καταλήξατε σε αυτό το συμπέρασμα, ειδικά αν πρόκειται για προσβολή η οποία δεν είναι ιδιαίτερα εμφανής: για παράδειγμα αν δεν είναι στα αγγλικά, είναι σε κάποια αργκό ή κάνει κάποια προσβλητική ιστορική/πολιτιστική αναφορά.", + "reportProcessedFasterInEnglish": "Η αναφορά σας θα επεξεργαστεί γρηγορότερα αν είναι γραμμένη στα αγγλικά.", "error_provideOneCheatedGameLink": "Καταχωρίστε τουλάχιστον έναν σύνδεσμο σε ένα παιχνίδι εξαπάτησης.", "by": "από τον {param}", "importedByX": "Εισήχθη από τον χρήστη {param}", @@ -882,7 +982,7 @@ "simulAddExtraTimePerPlayer": "Προσθήκη χρόνου στο ρολόι κάθε παίκτη που συνδέεται στο σιμουλτανέ.", "simulHostExtraTimePerPlayer": "Προσθήκη επιπλέον χρόνου ανά παίκτη", "lichessTournaments": "Τουρνουά στο Lichess", - "tournamentFAQ": "Τεκμηρίωση τουρνουά τύπου αρένας", + "tournamentFAQ": "Τεκμηρίωση για τουρνουά τύπου αρένας", "timeBeforeTournamentStarts": "Χρόνος προτού ξεκινήσει το τουρνουά", "averageCentipawnLoss": "Μέση απώλεια εκατοστοπιονιού", "accuracy": "Ακρίβεια", @@ -933,7 +1033,7 @@ "downloadRaw": "Λήψη ακατέργαστο", "downloadImported": "Λήψη εισαγόμενου", "crosstable": "Αποτελέσματα", - "youCanAlsoScrollOverTheBoardToMoveInTheGame": "Μπορείτε επίσης να κινηθείτε πάνω στην σκακιέρα για να πάτε στο παιχνίδι.", + "youCanAlsoScrollOverTheBoardToMoveInTheGame": "Μπορείτε επίσης να κινήσετε πάνω στην σκακιέρα για να μετακινηθείτε στο παιχνίδι.", "scrollOverComputerVariationsToPreviewThem": "Μετακινήστε το ποντίκι σας πάνω στις βαριάντες του υπολογιστή για την προεπισκόπησή τους.", "analysisShapesHowTo": "Πατήστε Shift + κλικ ή δεξί κλικ για να σχεδιάσετε κύκλους και βέλη στην σκακιέρα.", "letOtherPlayersMessageYou": "Επιτρέψτε άλλους παίκτες να σας στέλνουν μηνύματα", @@ -1164,6 +1264,7 @@ "showMeEverything": "Εμφάνιση όλων", "lichessPatronInfo": "Το Lichess είναι ένα φιλανθρωπικό και εντελώς ελεύθερο λογισμικό ανοιχτού κώδικα.\nΌλα τα εξοδα λειτουργίας, ανάπτυξης και περιεχομένου καλύπτοναι αποκλειστικά από δωρεές χρηστών.", "nothingToSeeHere": "Τίποτα για να δείτε εδώ αυτή τη στιγμή.", + "stats": "Στατιστικά", "opponentLeftCounter": "{count, plural, =1{Ο αντίπαλός σας έφυγε από το παιχνίδι. Διεκδίκηση νίκης σε {count} δευτερόλεπτο.} other{Ο αντίπαλος έφυγε από το παιχνίδι. Διεκδίκηση νίκης σε {count} δευτερόλεπτα.}}", "mateInXHalfMoves": "{count, plural, =1{Ματ σε {count} μισή κίνηση} other{Ματ σε {count} μισές κινήσεις}}", "nbBlunders": "{count, plural, =1{{count} σοβαρό σφάλμα} other{{count} σοβαρά σφάλματα}}", @@ -1260,6 +1361,159 @@ "stormXRuns": "{count, plural, =1{1 γύρος} other{{count} γύροι}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Έπαιξε έναν γύρο {param2}} other{Έπαιξε {count} γύρους {param2}}}", "streamerLichessStreamers": "Lichess streamers", + "studyPrivate": "Ιδιωτικό", + "studyMyStudies": "Οι μελέτες μου", + "studyStudiesIContributeTo": "Μελέτες που συνεισφέρω", + "studyMyPublicStudies": "Οι δημόσιες μελέτες μου", + "studyMyPrivateStudies": "Οι ιδιωτικές μελέτες μου", + "studyMyFavoriteStudies": "Οι αγαπημένες μελέτες μου", + "studyWhatAreStudies": "Τι είναι οι μελέτες;", + "studyAllStudies": "Όλες οι μελέτες", + "studyStudiesCreatedByX": "Μελέτες που δημιουργήθηκαν από τον/την {param}", + "studyNoneYet": "Τίποτα ακόμη εδώ.", + "studyHot": "Δημοφιλείς (hot)", + "studyDateAddedNewest": "Ημερομηνία προσθήκης (νεότερες)", + "studyDateAddedOldest": "Ημερομηνία προσθήκης (παλαιότερες)", + "studyRecentlyUpdated": "Πρόσφατα ενημερωμένες", + "studyMostPopular": "Οι πιο δημοφιλείς", + "studyAlphabetical": "Αλφαβητικά", + "studyAddNewChapter": "Προσθήκη νέου κεφαλαίου", + "studyAddMembers": "Προσθήκη μελών", + "studyInviteToTheStudy": "Προσκάλεσε στην μελέτη", + "studyPleaseOnlyInvitePeopleYouKnow": "Παρακαλώ, προσκαλέστε μόνο άτομα που γνωρίζετε και που θέλουν να συμμετέχουν ενεργά σε αυτήν την μελέτη.", + "studySearchByUsername": "Αναζήτηση με όνομα χρήστη", + "studySpectator": "Θεατής", + "studyContributor": "Συνεισφέρων", + "studyKick": "Διώξε", + "studyLeaveTheStudy": "Αποχώρησε από αυτήν την μελέτη", + "studyYouAreNowAContributor": "Μπορείτε τώρα να συνεισφέρετε στην μελέτη", + "studyYouAreNowASpectator": "Είστε πλέον θεατής", + "studyPgnTags": "PGN ετικέτες", + "studyLike": "Μου αρέσει", + "studyUnlike": "Δε μου αρέσει", + "studyNewTag": "Νέα ετικέτα", + "studyCommentThisPosition": "Σχολίασε την υπάρχουσα θέση", + "studyCommentThisMove": "Σχολίασε αυτήν την κίνηση", + "studyAnnotateWithGlyphs": "Σχολιασμός με σύμβολα", + "studyTheChapterIsTooShortToBeAnalysed": "Το κεφάλαιο είναι πολύ μικρό για να αναλυθεί.", + "studyOnlyContributorsCanRequestAnalysis": "Μόνο αυτοί που συνεισφέρουν στην σπουδή μπορούν να ζητήσουν ανάλυση από υπολογιστή.", + "studyGetAFullComputerAnalysis": "Αίτηση πλήρης ανάλυσης της κύριας γραμμής παρτίδας από μηχανή του σέρβερ.", + "studyMakeSureTheChapterIsComplete": "Σιγουρευτείτε ότι το κεφάλαιο είναι ολοκληρωμένο. Μπορείτε να ζητήσετε ανάλυση μόνο μια φορά.", + "studyAllSyncMembersRemainOnTheSamePosition": "Όλα τα συγχρονισμένα μέλη παραμένουν στην ίδια θέση", + "studyShareChanges": "Διαμοιρασμός στους θεατές των αλλαγών και αποθήκευση τους στο σέρβερ", + "studyPlaying": "Παίζονται", + "studyShowEvalBar": "Μπάρες αξιολόγησης", + "studyFirst": "Πρώτη", + "studyPrevious": "Προηγούμενη", + "studyNext": "Επόμενη", + "studyLast": "Τελευταία", "studyShareAndExport": "Διαμοιρασμός & εξαγωγή", - "studyStart": "Δημιουργία" + "studyCloneStudy": "Κλωνοποίησε", + "studyStudyPgn": "PGN της μελέτης", + "studyDownloadAllGames": "Λήψη όλων των παιχνιδιών", + "studyChapterPgn": "PGN του κεφαλαίου", + "studyCopyChapterPgn": "Αντιγραφή PGN", + "studyDownloadGame": "Λήψη παιχνιδιού", + "studyStudyUrl": "URL μελέτης", + "studyCurrentChapterUrl": "Τρέχον κεφάλαιο URL", + "studyYouCanPasteThisInTheForumToEmbed": "Επικολλήστε το παρόν για ενσωμάτωση στο φόρουμ", + "studyStartAtInitialPosition": "Ξεκινάει από αρχική θέση", + "studyStartAtX": "Ξεκινάει με {param}", + "studyEmbedInYourWebsite": "Ενσωματώστε στην ιστοσελίδα σας ή το μπλογκ σας", + "studyReadMoreAboutEmbedding": "Διαβάστε περισσότερα για την ενσωμάτωση", + "studyOnlyPublicStudiesCanBeEmbedded": "Μόνο δημόσιες μελέτες μπορούν να ενσωματωθούν!", + "studyOpen": "Άνοιξε", + "studyXBroughtToYouByY": "{param1}, δημιουργήθηκε από {param2}", + "studyStudyNotFound": "Η μελέτη δεν βρέθηκε", + "studyEditChapter": "Επεξεργάσου το κεφάλαιο", + "studyNewChapter": "Νέο κεφάλαιο", + "studyImportFromChapterX": "Εισαγωγή από {param}", + "studyOrientation": "Προσανατολισμός", + "studyAnalysisMode": "Τύπος ανάλυσης", + "studyPinnedChapterComment": "Καρφιτσωμένο σχόλιο κεφαλαίου", + "studySaveChapter": "Αποθήκευση κεφαλαίου", + "studyClearAnnotations": "Διαγραφή σχολιασμών", + "studyClearVariations": "Εκκαθάριση βαριάντων", + "studyDeleteChapter": "Διαγραφή κεφαλαίου", + "studyDeleteThisChapter": "Διαγραφή κεφαλαίου; Μη αναιρέσιμη ενέργεια!", + "studyClearAllCommentsInThisChapter": "Καθαρισμός όλων των σχολίων, συμβόλων και σχεδίων στο τρέχον κεφάλαιο;", + "studyRightUnderTheBoard": "Κάτω από την σκακιέρα", + "studyNoPinnedComment": "Καμία", + "studyNormalAnalysis": "Απλή ανάλυση", + "studyHideNextMoves": "Απόκρυψη επόμενων κινήσεων", + "studyInteractiveLesson": "Διαδραστικό μάθημα", + "studyChapterX": "Κεφάλαιο {param}", + "studyEmpty": "Κενή", + "studyStartFromInitialPosition": "Έναρξη από τρέχουσα θέση", + "studyEditor": "Επεξεργαστής", + "studyStartFromCustomPosition": "Έναρξη από τρέχουσα θέση", + "studyLoadAGameByUrl": "Φόρτωση παρτίδας με URL", + "studyLoadAPositionFromFen": "Φόρτωση θέσης από FEN", + "studyLoadAGameFromPgn": "Φόρτωσε μια παρτίδα από PGN", + "studyAutomatic": "Αυτόματο", + "studyUrlOfTheGame": "URL παρτίδων, ένα ανά γραμμή", + "studyLoadAGameFromXOrY": "Φόρτωση παρτίδων από {param1} ή {param2}", + "studyCreateChapter": "Δημιουργία κεφαλαίου", + "studyCreateStudy": "Δημιουργία μελέτης", + "studyEditStudy": "Επεξεργασία μελέτης", + "studyVisibility": "Ορατότητα", + "studyPublic": "Δημόσια", + "studyUnlisted": "Ακαταχώρητη", + "studyInviteOnly": "Με πρόσκληση", + "studyAllowCloning": "Επέτρεψε αντιγραφή", + "studyNobody": "Κανένας", + "studyOnlyMe": "Μόνο εγώ", + "studyContributors": "Συνεισφέροντες", + "studyMembers": "Μέλη", + "studyEveryone": "Οποιοσδήποτε", + "studyEnableSync": "Ενεργοποίηση συγχρονισμού", + "studyYesKeepEveryoneOnTheSamePosition": "Ναι: όλοι βλέπουν την ίδια θέση", + "studyNoLetPeopleBrowseFreely": "Όχι: ελεύθερη επιλογή θέσης", + "studyPinnedStudyComment": "Καρφιτσωμένο σχόλιο μελέτης", + "studyStart": "Δημιουργία", + "studySave": "Αποθήκευση", + "studyClearChat": "Εκκαθάριση συνομιλίας", + "studyDeleteTheStudyChatHistory": "Διαγραφή συνομιλίας μελέτης; Μη αναιρέσιμη ενέργεια!", + "studyDeleteStudy": "Διαγραφή μελέτης", + "studyConfirmDeleteStudy": "Να διαγραφεί όλη η μελέτη; Η ενέργεια αυτή δεν μπορεί να αναιρεθεί! Πληκτρολογήστε το όνομα της μελέτης για επιβεβαίωση: {param}", + "studyWhereDoYouWantToStudyThat": "Που θέλετε να δημιουργήσετε την μελέτη;", + "studyGoodMove": "Καλή κίνηση", + "studyMistake": "Λάθος", + "studyBrilliantMove": "Εξαιρετική κίνηση", + "studyBlunder": "Σοβαρό λάθος", + "studyInterestingMove": "Ενδιαφέρουσα κίνηση", + "studyDubiousMove": "Κίνηση αμφίβολης αξίας", + "studyOnlyMove": "Μοναδική κίνηση", + "studyZugzwang": "Τσούγκσβανγκ", + "studyEqualPosition": "Ισόπαλη θέση", + "studyUnclearPosition": "Ασαφής θέση", + "studyWhiteIsSlightlyBetter": "Το λευκά είναι ελαφρώς καλύτερα", + "studyBlackIsSlightlyBetter": "Το μαύρα είναι ελαφρώς καλύτερα", + "studyWhiteIsBetter": "Τα λευκά είναι καλύτερα", + "studyBlackIsBetter": "Τα μαύρα είναι καλύτερα", + "studyWhiteIsWinning": "Τα λευκά κερδίζουν", + "studyBlackIsWinning": "Τα μαύρα κερδίζουν", + "studyNovelty": "Novelty", + "studyDevelopment": "Ανάπτυξη", + "studyInitiative": "Πρωτοβουλία", + "studyAttack": "Επίθεση", + "studyCounterplay": "Αντεπίθεση", + "studyTimeTrouble": "Πίεση χρόνου", + "studyWithCompensation": "Με αντάλλαγμα", + "studyWithTheIdea": "Με ιδέα", + "studyNextChapter": "Επόμενο κεφάλαιο", + "studyPrevChapter": "Προηγούμενο κεφάλαιο", + "studyStudyActions": "Ρυθμίσεις μελέτης", + "studyTopics": "Θέματα", + "studyMyTopics": "Τα θέματά μου", + "studyPopularTopics": "Δημοφιλή θέματα", + "studyManageTopics": "Διαχείριση θεμάτων", + "studyBack": "Πίσω", + "studyPlayAgain": "Παίξτε ξανά", + "studyWhatWouldYouPlay": "Τι θα παίζατε σε αυτή τη θέση;", + "studyYouCompletedThisLesson": "Συγχαρητήρια! Ολοκληρώσατε αυτό το μάθημα.", + "studyNbChapters": "{count, plural, =1{{count} Κεφάλαιο} other{{count} Κεφάλαια}}", + "studyNbGames": "{count, plural, =1{{count} Παρτίδα} other{{count} Παρτίδες}}", + "studyNbMembers": "{count, plural, =1{{count} Μέλος} other{{count} Μέλη}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Επικολλήστε το PGN εδώ, μέχρι {count} παρτίδα} other{Επικολλήστε το PGN εδώ, μέχρι {count} παρτίδες}}" } \ No newline at end of file diff --git a/lib/l10n/lila_en_US.arb b/lib/l10n/lila_en_US.arb index 1dcaf2ee9a..119151ae45 100644 --- a/lib/l10n/lila_en_US.arb +++ b/lib/l10n/lila_en_US.arb @@ -15,6 +15,7 @@ "mobileAllGames": "All games", "mobileRecentSearches": "Recent searches", "mobileClearButton": "Clear", + "mobilePlayersMatchingSearchTerm": "Players with \"{param}\"", "mobileNoSearchResults": "No results", "mobileAreYouSure": "Are you sure?", "mobilePuzzleStreakAbortWarning": "You will lose your current streak, but your score will be saved.", @@ -29,7 +30,6 @@ "mobilePuzzleStormConfirmEndRun": "Do you want to end this run?", "mobilePuzzleStormFilterNothingToShow": "Nothing to show, please change the filters", "mobileCancelTakebackOffer": "Cancel takeback offer", - "mobileCancelDrawOffer": "Cancel draw offer", "mobileWaitingForOpponentToJoin": "Waiting for opponent to join...", "mobileBlindfoldMode": "Blindfold", "mobileLiveStreamers": "Live streamers", @@ -41,6 +41,7 @@ "mobilePuzzleStormSubtitle": "Solve as many puzzles as possible in 3 minutes.", "mobileGreeting": "Hello, {param}", "mobileGreetingWithoutName": "Hello", + "mobilePrefMagnifyDraggedPiece": "Magnify dragged piece", "activityActivity": "Activity", "activityHostedALiveStream": "Hosted a live stream", "activityRankedInSwissTournament": "Ranked #{param1} in {param2}", @@ -53,23 +54,68 @@ "activityPlayedNbMoves": "{count, plural, =1{Played {count} move} other{Played {count} moves}}", "activityInNbCorrespondenceGames": "{count, plural, =1{in {count} correspondence game} other{in {count} correspondence games}}", "activityCompletedNbGames": "{count, plural, =1{Completed {count} correspondence game} other{Completed {count} correspondence games}}", + "activityCompletedNbVariantGames": "{count, plural, =1{Completed {count} {param2} correspondence game} other{Completed {count} {param2} correspondence games}}", "activityFollowedNbPlayers": "{count, plural, =1{Started following {count} player} other{Started following {count} players}}", "activityGainedNbFollowers": "{count, plural, =1{Gained {count} new follower} other{Gained {count} new followers}}", "activityHostedNbSimuls": "{count, plural, =1{Hosted {count} simultaneous exhibition} other{Hosted {count} simultaneous exhibitions}}", "activityJoinedNbSimuls": "{count, plural, =1{Participated in {count} simultaneous exhibition} other{Participated in {count} simultaneous exhibitions}}", "activityCreatedNbStudies": "{count, plural, =1{Created {count} new study} other{Created {count} new studies}}", - "activityCompetedInNbTournaments": "{count, plural, =1{Competed in {count} tournament} other{Competed in {count} tournaments}}", + "activityCompetedInNbTournaments": "{count, plural, =1{Competed in {count} Arena tournament} other{Competed in {count} Arena tournaments}}", "activityRankedInTournament": "{count, plural, =1{Ranked #{count} (top {param2}%) with {param3} game in {param4}} other{Ranked #{count} (top {param2}%) with {param3} games in {param4}}}", "activityCompetedInNbSwissTournaments": "{count, plural, =1{Competed in {count} Swiss tournament} other{Competed in {count} Swiss tournaments}}", "activityJoinedNbTeams": "{count, plural, =1{Joined {count} team} other{Joined {count} teams}}", "broadcastBroadcasts": "Broadcasts", + "broadcastMyBroadcasts": "My broadcasts", "broadcastLiveBroadcasts": "Live tournament broadcasts", + "broadcastBroadcastCalendar": "Broadcast calendar", + "broadcastNewBroadcast": "New live broadcast", + "broadcastSubscribedBroadcasts": "Subscribed broadcasts", + "broadcastAboutBroadcasts": "About broadcasts", + "broadcastHowToUseLichessBroadcasts": "How to use Lichess Broadcasts.", + "broadcastTheNewRoundHelp": "The new round will have the same members and contributors as the previous one.", + "broadcastAddRound": "Add a round", + "broadcastOngoing": "Ongoing", + "broadcastUpcoming": "Upcoming", + "broadcastCompleted": "Completed", + "broadcastCompletedHelp": "Lichess detects round completion, but can get it wrong. Use this to set it manually.", + "broadcastRoundName": "Round name", + "broadcastRoundNumber": "Round number", + "broadcastTournamentName": "Tournament name", + "broadcastTournamentDescription": "Short tournament description", + "broadcastFullDescription": "Full tournament description", + "broadcastFullDescriptionHelp": "Optional long description of the tournament. {param1} is available. Length must be less than {param2} characters.", + "broadcastSourceSingleUrl": "PGN Source URL", + "broadcastSourceUrlHelp": "URL that Lichess will check to get PGN updates. It must be publicly accessible from the Internet.", + "broadcastSourceGameIds": "Up to 64 Lichess game IDs, separated by spaces.", + "broadcastStartDateTimeZone": "Start date in the tournament local timezone: {param}", + "broadcastStartDateHelp": "Optional, if you know when the event starts", + "broadcastCurrentGameUrl": "Current game URL", + "broadcastDownloadAllRounds": "Download all rounds", + "broadcastResetRound": "Reset this round", + "broadcastDeleteRound": "Delete this round", + "broadcastDefinitivelyDeleteRound": "Definitively delete the round and all its games.", + "broadcastDeleteAllGamesOfThisRound": "Delete all games of this round. The source will need to be active in order to re-create them.", + "broadcastEditRoundStudy": "Edit round study", + "broadcastDeleteTournament": "Delete this tournament", + "broadcastDefinitivelyDeleteTournament": "Definitively delete the entire tournament, all its rounds and all its games.", + "broadcastShowScores": "Show players' scores based on game results", + "broadcastReplacePlayerTags": "Optional: replace player names, ratings and titles", + "broadcastFideFederations": "FIDE federations", + "broadcastTop10Rating": "Top 10 rating", + "broadcastFidePlayers": "FIDE players", + "broadcastFidePlayerNotFound": "FIDE player not found", + "broadcastFideProfile": "FIDE profile", + "broadcastFederation": "Federation", + "broadcastAgeThisYear": "Age this year", + "broadcastUnrated": "Unrated", + "broadcastRecentTournaments": "Recent tournaments", + "broadcastNbBroadcasts": "{count, plural, =1{{count} broadcast} other{{count} broadcasts}}", "challengeChallengesX": "Challenges: {param1}", "challengeChallengeToPlay": "Challenge to a game", - "challengeChallengeDeclined": "Challenge declined", + "challengeChallengeDeclined": "Challenge declined.", "challengeChallengeAccepted": "Challenge accepted!", "challengeChallengeCanceled": "Challenge canceled.", - "challengeRegisterToSendChallenges": "Please register to send challenges.", + "challengeRegisterToSendChallenges": "Please register to send challenges to this user.", "challengeYouCannotChallengeX": "You cannot challenge {param}.", "challengeXDoesNotAcceptChallenges": "{param} does not accept challenges.", "challengeYourXRatingIsTooFarFromY": "Your {param1} rating is too far from {param2}.", @@ -136,7 +182,6 @@ "preferencesZenMode": "Zen mode", "preferencesShowPlayerRatings": "Show player ratings", "preferencesShowFlairs": "Show player flairs", - "preferencesExplainShowPlayerRatings": "This allows hiding all ratings from the website, to help focus on the chess. Games can still be rated, this is only about what you get to see.", "preferencesDisplayBoardResizeHandle": "Show board resize handle", "preferencesOnlyOnInitialPosition": "Only on initial position", "preferencesInGameOnly": "In-game only", @@ -187,7 +232,7 @@ "preferencesNotifyWeb": "Browser", "preferencesNotifyDevice": "Device", "preferencesBellNotificationSound": "Bell notification sound", - "puzzlePuzzles": "Chess Puzzles", + "puzzlePuzzles": "Puzzles", "puzzlePuzzleThemes": "Puzzle Themes", "puzzleRecommended": "Recommended", "puzzlePhases": "Phases", @@ -202,7 +247,7 @@ "puzzleVoteToLoadNextOne": "Vote to load the next one!", "puzzleUpVote": "Upvote puzzle", "puzzleDownVote": "Downvote puzzle", - "puzzleYourPuzzleRatingWillNotChange": "Your puzzle rating will not change. Note that puzzles are not a competition. Ratings help select the best puzzles for your current skill.", + "puzzleYourPuzzleRatingWillNotChange": "Your puzzle rating will not change. Note that puzzles are not a competition. Your rating helps selecting the best puzzles for your current skill.", "puzzleFindTheBestMoveForWhite": "Find the best move for white.", "puzzleFindTheBestMoveForBlack": "Find the best move for black.", "puzzleToGetPersonalizedPuzzles": "To get personalized puzzles:", @@ -241,7 +286,7 @@ "puzzleStrengths": "Strengths", "puzzleHistory": "Puzzle history", "puzzleSolved": "solved", - "puzzleFailed": "failed", + "puzzleFailed": "incorrect", "puzzleStreakDescription": "Solve progressively harder puzzles and build a win streak. There is no clock, so take your time. One wrong move, and it's game over! But you can skip one move per session.", "puzzleYourStreakX": "Your streak: {param}", "puzzleStreakSkipExplanation": "Skip this move to preserve your streak! Only works once per run.", @@ -251,7 +296,7 @@ "puzzleLookupOfPlayer": "Search puzzles from a player's games", "puzzleFromXGames": "Puzzles from {param}'s games", "puzzleSearchPuzzles": "Search puzzles", - "puzzleFromMyGamesNone": "You have no puzzles in the database, but Lichess still loves you very much.\nPlay rapid and classical games to increase your chances of having a puzzle of yours added!", + "puzzleFromMyGamesNone": "You have no puzzles in the database, but Lichess still loves you very much.\n\nPlay rapid and classical games to increase your chances of having a puzzle of yours added!", "puzzleFromXGamesFound": "{param1} puzzles found in {param2} games", "puzzlePuzzleDashboardDescription": "Train, analyse, improve", "puzzlePercentSolved": "{param} solved", @@ -382,8 +427,8 @@ "puzzleThemeXRayAttackDescription": "A piece attacks or defends a square, through an enemy piece.", "puzzleThemeZugzwang": "Zugzwang", "puzzleThemeZugzwangDescription": "The opponent is limited in the moves they can make, and all moves worsen their position.", - "puzzleThemeHealthyMix": "Healthy mix", - "puzzleThemeHealthyMixDescription": "A bit of everything. You don't know what to expect, so you remain ready for anything! Just like in real games.", + "puzzleThemeMix": "Healthy mix", + "puzzleThemeMixDescription": "A bit of everything. You don't know what to expect, so you remain ready for anything! Just like in real games.", "puzzleThemePlayerGames": "Player games", "puzzleThemePlayerGamesDescription": "Lookup puzzles generated from your games, or from another player's games.", "puzzleThemePuzzleDownloadInformation": "These puzzles are in the public domain, and can be downloaded from {param}.", @@ -451,7 +496,7 @@ "analysis": "Analysis board", "depthX": "Depth {param}", "usingServerAnalysis": "Using server analysis", - "loadingEngine": "Loading engine ...", + "loadingEngine": "Loading engine...", "calculatingMoves": "Calculating moves...", "engineFailed": "Error loading engine", "cloudAnalysis": "Cloud analysis", @@ -514,7 +559,6 @@ "memory": "Memory", "infiniteAnalysis": "Infinite analysis", "removesTheDepthLimit": "Removes the depth limit, and keeps your computer warm", - "engineManager": "Engine manager", "blunder": "Blunder", "mistake": "Mistake", "inaccuracy": "Inaccuracy", @@ -610,7 +654,7 @@ "accept": "Accept", "decline": "Decline", "playingRightNow": "Playing right now", - "eventInProgress": "Playing right now", + "eventInProgress": "Playing now", "finished": "Finished", "abortGame": "Abort game", "gameAborted": "Game aborted", @@ -691,7 +735,7 @@ "continueFromHere": "Continue from here", "toStudy": "Study", "importGame": "Import game", - "importGameExplanation": "Paste a game PGN to get a browsable replay,\ncomputer analysis, game chat and shareable URL.", + "importGameExplanation": "Paste a game PGN to get a browsable replay, computer analysis, game chat and public shareable URL.", "importGameCaveat": "Variations will be erased. To keep them, import the PGN via a study.", "importGameDataPrivacyWarning": "This PGN can be accessed by the public. To import a game privately, use a study.", "thisIsAChessCaptcha": "This is a chess CAPTCHA.", @@ -776,7 +820,7 @@ "flair": "Flair", "youCanHideFlair": "There is a setting to hide all user flairs across the entire site.", "biography": "Biography", - "countryRegion": "Region or country", + "countryRegion": "Country or region", "thankYou": "Thank you!", "socialMediaLinks": "Social media links", "oneUrlPerLine": "One URL per line.", @@ -818,7 +862,9 @@ "cheat": "Cheat", "troll": "Troll", "other": "Other", - "reportDescriptionHelp": "Paste the link to the game(s) and explain what is wrong about this user behavior. Don't just say \"they cheat\", but tell us how you came to this conclusion. Your report will be processed faster if written in English.", + "reportCheatBoostHelp": "Paste a link to the game(s) and explain what is wrong with this user's behavior. Don't just say \"they cheat,\" but tell us how you came to this conclusion.", + "reportUsernameHelp": "Explain why this username is offensive. Don't just say \"it's offensive/inappropriate,\" but tell us how you came to this conclusion, especially if the offense is obscure, not in English, in slang, or a historical/cultural reference.", + "reportProcessedFasterInEnglish": "Your report will be processed faster if written in English.", "error_provideOneCheatedGameLink": "Please provide at least one link to a cheated game.", "by": "by {param}", "importedByX": "Imported by {param}", @@ -884,7 +930,7 @@ "error_email_unique": "Email address invalid or already taken", "error_email_different": "This is already your email address", "error_minLength": "Must be at least {param} characters long", - "error_maxLength": "Maximum length is {param}", + "error_maxLength": "Must be at most {param} characters long", "error_min": "Must be at least {param}", "error_max": "Must be at most {param}", "ifRatingIsPlusMinusX": "If rating is ± {param}", @@ -897,7 +943,7 @@ "blackCastlingKingside": "Black O-O", "tpTimeSpentPlaying": "Time spent playing: {param}", "watchGames": "Watch games", - "tpTimeSpentOnTV": "Time on TV: {param}", + "tpTimeSpentOnTV": "Time featured on TV: {param}", "watch": "Watch", "videoLibrary": "Video library", "streamersMenu": "Streamers", @@ -923,14 +969,14 @@ "aboutSimul": "Simuls involve a single player facing several players at once.", "aboutSimulImage": "Out of 50 opponents, Fischer won 47 games, drew 2 and lost 1.", "aboutSimulRealLife": "The concept is taken from real world events. In real life, this involves the simul host moving from table to table to play a single move.", - "aboutSimulRules": "When the simul starts, every player starts a game with the host, who gets to play the white pieces. The simul ends when all games are complete.", + "aboutSimulRules": "When the simul starts, every player starts a game with the host. The simul ends when all games are complete.", "aboutSimulSettings": "Simuls are always casual. Rematches, takebacks and adding time are disabled.", "create": "Create", "whenCreateSimul": "When you create a Simul, you get to play several players at once.", "simulVariantsHint": "If you select several variants, each player gets to choose which one to play.", "simulClockHint": "Fischer Clock setup. The more players you take on, the more time you may need.", - "simulAddExtraTime": "You may add extra time to your clock to help cope with the simul.", - "simulHostExtraTime": "Host extra clock time", + "simulAddExtraTime": "You may add extra initial time to your clock to help you cope with the simul.", + "simulHostExtraTime": "Host extra initial clock time", "simulAddExtraTimePerPlayer": "Add initial time to your clock for each player joining the simul.", "simulHostExtraTimePerPlayer": "Host extra clock time per player", "lichessTournaments": "Lichess tournaments", @@ -944,8 +990,8 @@ "keyCycleSelectedVariation": "Cycle selected variation", "keyShowOrHideComments": "show/hide comments", "keyEnterOrExitVariation": "enter/exit variation", - "keyRequestComputerAnalysis": "Request computer analysis, learn from your mistakes", - "keyNextLearnFromYourMistakes": "Next (learn from your mistakes)", + "keyRequestComputerAnalysis": "Request computer analysis, Learn from your mistakes", + "keyNextLearnFromYourMistakes": "Next (Learn from your mistakes)", "keyNextBlunder": "Next blunder", "keyNextMistake": "Next mistake", "keyNextInaccuracy": "Next inaccuracy", @@ -977,10 +1023,10 @@ "weHaveSentYouAnEmailClickTheLink": "We've sent you an email. Click the link in the email to activate your account.", "ifYouDoNotSeeTheEmailCheckOtherPlaces": "If you don't see the email, check other places it might be, like your junk, spam, social, or other folders.", "weHaveSentYouAnEmailTo": "We've sent an email to {param}. Click the link in the email to reset your password.", - "byRegisteringYouAgreeToBeBoundByOur": "By registering, you agree to be bound by our {param}.", + "byRegisteringYouAgreeToBeBoundByOur": "By registering, you agree to the {param}.", "readAboutOur": "Read about our {param}.", - "networkLagBetweenYouAndLichess": "Network lag between you and lichess", - "timeToProcessAMoveOnLichessServer": "Time to process a move on lichess server", + "networkLagBetweenYouAndLichess": "Network lag between you and Lichess", + "timeToProcessAMoveOnLichessServer": "Time to process a move on Lichess's server", "downloadAnnotated": "Download annotated", "downloadRaw": "Download raw", "downloadImported": "Download imported", @@ -995,9 +1041,9 @@ "withFriends": "With friends", "withEverybody": "With everybody", "kidMode": "Kid mode", - "kidModeIsEnabled": "Child-mode is enabled.", + "kidModeIsEnabled": "Kid mode is enabled.", "kidModeExplanation": "This is about safety. In kid mode, all site communications are disabled. Enable this for your children and school students, to protect them from other internet users.", - "inKidModeTheLichessLogoGetsIconX": "In kid mode, the lichess logo gets a {param} icon, so you know your kids are safe.", + "inKidModeTheLichessLogoGetsIconX": "In kid mode, the Lichess logo gets a {param} icon, so you know your kids are safe.", "askYourChessTeacherAboutLiftingKidMode": "Your account is managed. Ask your chess teacher about lifting kid mode.", "enableKidMode": "Enable Kid mode", "disableKidMode": "Disable Kid mode", @@ -1005,7 +1051,7 @@ "sessions": "Sessions", "revokeAllSessions": "revoke all sessions", "playChessEverywhere": "Play chess everywhere", - "asFreeAsLichess": "As free as lichess", + "asFreeAsLichess": "As free as Lichess", "builtForTheLoveOfChessNotMoney": "Built for the love of chess, not money", "everybodyGetsAllFeaturesForFree": "Everybody gets all features for free", "zeroAdvertisement": "Zero advertisement", @@ -1114,10 +1160,10 @@ "lifetimeScore": "Lifetime score", "currentMatchScore": "Current match score", "agreementAssistance": "I agree that I will at no time receive assistance during my games (from a chess computer, book, database or another person).", - "agreementNice": "I agree that I will always be nice to other players.", + "agreementNice": "I agree that I will always be respectful to other players.", "agreementMultipleAccounts": "I agree that I will not create multiple accounts (except for the reasons stated in the {param}).", "agreementPolicy": "I agree that I will follow all Lichess policies.", - "searchOrStartNewDiscussion": "Search or start new discussion", + "searchOrStartNewDiscussion": "Search or start new conversation", "edit": "Edit", "bullet": "Bullet", "blitz": "Blitz", @@ -1158,14 +1204,14 @@ "lostAgainstTOSViolator": "You lost rating points to someone who violated the Lichess TOS", "refundXpointsTimeControlY": "Refund: {param1} {param2} rating points.", "timeAlmostUp": "Time is almost up!", - "clickToRevealEmailAddress": "[Click to reveal email address.]", + "clickToRevealEmailAddress": "[Click to reveal email address]", "download": "Download", "coachManager": "Coach manager", "streamerManager": "Streamer manager", "cancelTournament": "Cancel the tournament", "tournDescription": "Tournament description", "tournDescriptionHelp": "Anything special you want to tell the participants? Try to keep it short. Markdown links are available: [name](https://url)", - "ratedFormHelp": "Games are rated\nand impact players ratings", + "ratedFormHelp": "Games are rated and impact players ratings", "onlyMembersOfTeam": "Only members of team", "noRestriction": "No restriction", "minimumRatedGames": "Minimum rated games", @@ -1312,6 +1358,159 @@ "stormXRuns": "{count, plural, =1{1 run} other{{count} runs}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Played one run of {param2}} other{Played {count} runs of {param2}}}", "streamerLichessStreamers": "Lichess streamers", + "studyPrivate": "Private", + "studyMyStudies": "My studies", + "studyStudiesIContributeTo": "Studies I contribute to", + "studyMyPublicStudies": "My public studies", + "studyMyPrivateStudies": "My private studies", + "studyMyFavoriteStudies": "My favorite studies", + "studyWhatAreStudies": "What are studies?", + "studyAllStudies": "All studies", + "studyStudiesCreatedByX": "Studies created by {param}", + "studyNoneYet": "None yet.", + "studyHot": "Hot", + "studyDateAddedNewest": "Date added (newest)", + "studyDateAddedOldest": "Date added (oldest)", + "studyRecentlyUpdated": "Recently updated", + "studyMostPopular": "Most popular", + "studyAlphabetical": "Alphabetical", + "studyAddNewChapter": "Add a new chapter", + "studyAddMembers": "Add members", + "studyInviteToTheStudy": "Invite to the study", + "studyPleaseOnlyInvitePeopleYouKnow": "Please only invite people who know you, and who actively want to join this study.", + "studySearchByUsername": "Search by username", + "studySpectator": "Spectator", + "studyContributor": "Contributor", + "studyKick": "Kick", + "studyLeaveTheStudy": "Leave the study", + "studyYouAreNowAContributor": "You are now a contributor", + "studyYouAreNowASpectator": "You are now a spectator", + "studyPgnTags": "PGN tags", + "studyLike": "Like", + "studyUnlike": "Unlike", + "studyNewTag": "New tag", + "studyCommentThisPosition": "Comment on this position", + "studyCommentThisMove": "Comment on this move", + "studyAnnotateWithGlyphs": "Annotate with glyphs", + "studyTheChapterIsTooShortToBeAnalysed": "The chapter is too short to be analyzed.", + "studyOnlyContributorsCanRequestAnalysis": "Only the study contributors can request a computer analysis.", + "studyGetAFullComputerAnalysis": "Get a full server-side computer analysis of the mainline.", + "studyMakeSureTheChapterIsComplete": "Make sure the chapter is complete. You can only request analysis once.", + "studyAllSyncMembersRemainOnTheSamePosition": "All SYNC members remain on the same position", + "studyShareChanges": "Share changes with spectators and save them on the server", + "studyPlaying": "Playing", + "studyShowEvalBar": "Evaluation gauge", + "studyFirst": "First", + "studyPrevious": "Previous", + "studyNext": "Next", + "studyLast": "Last", "studyShareAndExport": "Share & export", - "studyStart": "Start" + "studyCloneStudy": "Clone", + "studyStudyPgn": "Study PGN", + "studyDownloadAllGames": "Download all games", + "studyChapterPgn": "Chapter PGN", + "studyCopyChapterPgn": "Copy PGN", + "studyDownloadGame": "Download game", + "studyStudyUrl": "Study URL", + "studyCurrentChapterUrl": "Current chapter URL", + "studyYouCanPasteThisInTheForumToEmbed": "You can paste this in the forum or your Lichess blog to embed", + "studyStartAtInitialPosition": "Start at initial position", + "studyStartAtX": "Start at {param}", + "studyEmbedInYourWebsite": "Embed in your website", + "studyReadMoreAboutEmbedding": "Read more about embedding", + "studyOnlyPublicStudiesCanBeEmbedded": "Only public studies can be embedded!", + "studyOpen": "Open", + "studyXBroughtToYouByY": "{param1}, brought to you by {param2}", + "studyStudyNotFound": "Study not found", + "studyEditChapter": "Edit chapter", + "studyNewChapter": "New chapter", + "studyImportFromChapterX": "Import from {param}", + "studyOrientation": "Orientation", + "studyAnalysisMode": "Analysis mode", + "studyPinnedChapterComment": "Pinned chapter comment", + "studySaveChapter": "Save chapter", + "studyClearAnnotations": "Clear annotations", + "studyClearVariations": "Clear variations", + "studyDeleteChapter": "Delete chapter", + "studyDeleteThisChapter": "Delete this chapter? There is no going back!", + "studyClearAllCommentsInThisChapter": "Clear all comments, glyphs and drawn shapes in this chapter?", + "studyRightUnderTheBoard": "Right under the board", + "studyNoPinnedComment": "None", + "studyNormalAnalysis": "Normal analysis", + "studyHideNextMoves": "Hide next moves", + "studyInteractiveLesson": "Interactive lesson", + "studyChapterX": "Chapter {param}", + "studyEmpty": "Empty", + "studyStartFromInitialPosition": "Start from initial position", + "studyEditor": "Editor", + "studyStartFromCustomPosition": "Start from custom position", + "studyLoadAGameByUrl": "Load games by URL", + "studyLoadAPositionFromFen": "Load a position from FEN", + "studyLoadAGameFromPgn": "Load games from PGN", + "studyAutomatic": "Automatic", + "studyUrlOfTheGame": "URL of the games, one per line", + "studyLoadAGameFromXOrY": "Load games from {param1} or {param2}", + "studyCreateChapter": "Create chapter", + "studyCreateStudy": "Create study", + "studyEditStudy": "Edit study", + "studyVisibility": "Visibility", + "studyPublic": "Public", + "studyUnlisted": "Unlisted", + "studyInviteOnly": "Invite only", + "studyAllowCloning": "Allow cloning", + "studyNobody": "Nobody", + "studyOnlyMe": "Only me", + "studyContributors": "Contributors", + "studyMembers": "Members", + "studyEveryone": "Everyone", + "studyEnableSync": "Enable sync", + "studyYesKeepEveryoneOnTheSamePosition": "Yes: keep everyone on the same position", + "studyNoLetPeopleBrowseFreely": "No: let people browse freely", + "studyPinnedStudyComment": "Pinned study comment", + "studyStart": "Start", + "studySave": "Save", + "studyClearChat": "Clear chat", + "studyDeleteTheStudyChatHistory": "Delete the study chat history? There is no going back!", + "studyDeleteStudy": "Delete study", + "studyConfirmDeleteStudy": "Delete the entire study? There is no going back! Type the name of the study to confirm: {param}", + "studyWhereDoYouWantToStudyThat": "Where do you want to study that?", + "studyGoodMove": "Good move", + "studyMistake": "Mistake", + "studyBrilliantMove": "Brilliant move", + "studyBlunder": "Blunder", + "studyInterestingMove": "Interesting move", + "studyDubiousMove": "Dubious move", + "studyOnlyMove": "Only move", + "studyZugzwang": "Zugzwang", + "studyEqualPosition": "Equal position", + "studyUnclearPosition": "Unclear position", + "studyWhiteIsSlightlyBetter": "White is slightly better", + "studyBlackIsSlightlyBetter": "Black is slightly better", + "studyWhiteIsBetter": "White is better", + "studyBlackIsBetter": "Black is better", + "studyWhiteIsWinning": "White is winning", + "studyBlackIsWinning": "Black is winning", + "studyNovelty": "Novelty", + "studyDevelopment": "Development", + "studyInitiative": "Initiative", + "studyAttack": "Attack", + "studyCounterplay": "Counterplay", + "studyTimeTrouble": "Time trouble", + "studyWithCompensation": "With compensation", + "studyWithTheIdea": "With the idea", + "studyNextChapter": "Next chapter", + "studyPrevChapter": "Previous chapter", + "studyStudyActions": "Study actions", + "studyTopics": "Topics", + "studyMyTopics": "My topics", + "studyPopularTopics": "Popular topics", + "studyManageTopics": "Manage topics", + "studyBack": "Back", + "studyPlayAgain": "Play again", + "studyWhatWouldYouPlay": "What would you play in this position?", + "studyYouCompletedThisLesson": "Congratulations! You completed this lesson.", + "studyNbChapters": "{count, plural, =1{{count} Chapter} other{{count} Chapters}}", + "studyNbGames": "{count, plural, =1{{count} Game} other{{count} Games}}", + "studyNbMembers": "{count, plural, =1{{count} Member} other{{count} Members}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Paste your PGN text here, up to {count} game} other{Paste your PGN text here, up to {count} games}}" } \ No newline at end of file diff --git a/lib/l10n/lila_eo.arb b/lib/l10n/lila_eo.arb index f5e8c19877..9cc737237d 100644 --- a/lib/l10n/lila_eo.arb +++ b/lib/l10n/lila_eo.arb @@ -39,7 +39,36 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{Konkuris en {count} svisa turniro} other{Konkuris en {count} svisaj turniroj}}", "activityJoinedNbTeams": "{count, plural, =1{Aliĝis {count} teamo} other{Aliĝis {count} teamoj}}", "broadcastBroadcasts": "Elsendoj", + "broadcastMyBroadcasts": "Miaj elsendoj", "broadcastLiveBroadcasts": "Vivaj turniraj elsendoj", + "broadcastNewBroadcast": "Nova viva elsendo", + "broadcastSubscribedBroadcasts": "Abonitaj elsendoj", + "broadcastAboutBroadcasts": "Pri elsendoj", + "broadcastHowToUseLichessBroadcasts": "Kiel uzi Lichess Elsendojn.", + "broadcastTheNewRoundHelp": "La nova raŭndo havos la samajn membrojn kaj kontribuantojn, kiom la antaŭa.", + "broadcastAddRound": "Aldoni raŭndon", + "broadcastOngoing": "Nun funkcianta", + "broadcastUpcoming": "Baldaŭ", + "broadcastCompleted": "Kompletigita", + "broadcastCompletedHelp": "Lichess detektas raŭndan finiĝon baze sur la fontaj ludoj. Uzu ĉi tiun baskuligo, se ne estas fonto.", + "broadcastRoundName": "Raŭndnomo", + "broadcastRoundNumber": "Rondnumero", + "broadcastTournamentName": "Nomo de la turniro", + "broadcastTournamentDescription": "Mallonga turnira priskribo", + "broadcastFullDescription": "Plena eventa priskribo", + "broadcastFullDescriptionHelp": "Laŭvola longa priskribo de la elsendo. {param1} haveblas. Longeco devas esti malpli ol {param2} literoj.", + "broadcastSourceUrlHelp": "URL kiun Lichess kontrolos por akiri PGN ĝisdatigojn. Ĝi devas esti publike alirebla en interreto.", + "broadcastStartDateHelp": "Laŭvola, se vi scias, kiam komenciĝas la evento", + "broadcastCurrentGameUrl": "Nuna luda URL", + "broadcastDownloadAllRounds": "Elŝuti ĉiujn raŭndojn", + "broadcastResetRound": "Restarigi ĉi tiun raŭndon", + "broadcastDeleteRound": "Forigi ĉi tiun raŭndon", + "broadcastDefinitivelyDeleteRound": "Sendube forigi la raŭndon kaj ĉiujn ĝiajn ludojn.", + "broadcastDeleteAllGamesOfThisRound": "Forigi ĉiujn ludojn de ĉi tiu raŭndo. La fonto devos esti aktiva por rekrei ilin.", + "broadcastEditRoundStudy": "Redakti raŭndan studon", + "broadcastDeleteTournament": "Forigi ĉi tiun turniron", + "broadcastDefinitivelyDeleteTournament": "Sendube forigi la tuta turniro, kaj ĝiajn raŭndojn kaj ĉiujn ĝiajn ludojn.", + "broadcastNbBroadcasts": "{count, plural, =1{{count} elsendo} other{{count} elsendoj}}", "challengeChallengesX": "Defioj: {param1}", "challengeChallengeToPlay": "Defii al nova ludo", "challengeChallengeDeclined": "Defio malakceptita", @@ -358,8 +387,8 @@ "puzzleThemeXRayAttackDescription": "Peco atakas aŭ defendas kvadraton, tra malamika peco.", "puzzleThemeZugzwang": "Movdevigo", "puzzleThemeZugzwangDescription": "La opcioj de la kontraŭulo por moviĝi estas limigitaj, kaj ĉiuj movoj plimalbonigas rian pozicion.", - "puzzleThemeHealthyMix": "Sana miksaĵo", - "puzzleThemeHealthyMixDescription": "Iom de ĉio. Vi ne scias kion atendi, do vi restas preta por io ajn! Same kiel en realaj ludoj.", + "puzzleThemeMix": "Sana miksaĵo", + "puzzleThemeMixDescription": "Iom de ĉio. Vi ne scias kion atendi, do vi restas preta por io ajn! Same kiel en realaj ludoj.", "puzzleThemePlayerGames": "Ludantaj ludoj", "puzzleThemePlayerGamesDescription": "Serĉu enigmojn generitajn de viaj ludoj, aŭ de la ludoj de alia ludanto.", "puzzleThemePuzzleDownloadInformation": "Ĉi tiuj enigmoj estas en la publika domeno, kaj povas esti elŝutitaj de {param}.", @@ -490,7 +519,6 @@ "memory": "Memoro", "infiniteAnalysis": "Senfina analizo", "removesTheDepthLimit": "Forigas la profundlimon, kaj tenas vian komputilon varma", - "engineManager": "Motora administranto", "blunder": "Erarego", "mistake": "Eraro", "inaccuracy": "Erareto", @@ -791,7 +819,6 @@ "cheat": "Trompo", "troll": "Trolo", "other": "Io alia", - "reportDescriptionHelp": "Inkluzivu la ligilon al la ludo(j) kaj ekspliku tion, kio malbonas pri la konduto de ĉi tiu uzanto.", "error_provideOneCheatedGameLink": "Bonvolu doni almenaŭ unu ligilon al ludo en kiu oni friponis.", "by": "de {param}", "importedByX": "Importita de {param}", @@ -1285,6 +1312,159 @@ "stormXRuns": "{count, plural, =1{1 kuro} other{{count} kuroj}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Ludis unu kuron de {param2}} other{Ludis {count} kurojn de {param2}}}", "streamerLichessStreamers": "Lichess filmprezentistoj", + "studyPrivate": "Privata", + "studyMyStudies": "Miaj studoj", + "studyStudiesIContributeTo": "Studoj en kiuj mi kontribuas", + "studyMyPublicStudies": "Miaj publikaj studoj", + "studyMyPrivateStudies": "Miaj privataj studoj", + "studyMyFavoriteStudies": "Miaj preferataj studoj", + "studyWhatAreStudies": "Kio estas la studoj?", + "studyAllStudies": "Ĉiuj studoj", + "studyStudiesCreatedByX": "Studoj kreitaj de {param}", + "studyNoneYet": "Neniu ankoraŭ.", + "studyHot": "Tendenca", + "studyDateAddedNewest": "Dato aldonita (plej novaj)", + "studyDateAddedOldest": "Dato aldonita (plej malnovaj)", + "studyRecentlyUpdated": "Lastatempe ĝisdatigita", + "studyMostPopular": "Plej popularaj", + "studyAlphabetical": "Alfabete", + "studyAddNewChapter": "Aldoni novan ĉapitron", + "studyAddMembers": "Aldoni membrojn", + "studyInviteToTheStudy": "Inviti al la studo", + "studyPleaseOnlyInvitePeopleYouKnow": "Bonvolu inviti nur homojn, kiujn vi konas kaj kiuj aktive volas aliĝi al tiu ĉi studo.", + "studySearchByUsername": "Serĉi laŭ uzantnomo", + "studySpectator": "Spektanto", + "studyContributor": "Kontribuanto", + "studyKick": "Forpuŝi", + "studyLeaveTheStudy": "Forlasi la studon", + "studyYouAreNowAContributor": "Nun vi estas kunlaboranto", + "studyYouAreNowASpectator": "Nun vi estas spektanto", + "studyPgnTags": "PGN etikedoj", + "studyLike": "Ŝati", + "studyUnlike": "Malŝati", + "studyNewTag": "Nova etikedo", + "studyCommentThisPosition": "Komenti tiun posicion", + "studyCommentThisMove": "Komenti tiun movon", + "studyAnnotateWithGlyphs": "Komenti per signobildo", + "studyTheChapterIsTooShortToBeAnalysed": "La ĉapitro estas tro mallonga por esti analizita.", + "studyOnlyContributorsCanRequestAnalysis": "Nur la kontribuantoj de la studo povas peti komputilan analizon.", + "studyGetAFullComputerAnalysis": "Akiru kompletan servilan komputilan analizon de la ĉefa linio.", + "studyMakeSureTheChapterIsComplete": "Certiĝu, ke la ĉapitro estas kompleta. Vi nur povas peti analizon unu foje.", + "studyAllSyncMembersRemainOnTheSamePosition": "Ĉiuj sinkronigitaj membroj restas ĉe la sama pozicio", + "studyShareChanges": "Diskonigi ŝanĝojn al spektantoj kaj konservi tiujn ĉe la servilo", + "studyPlaying": "Ludanta", + "studyShowEvalBar": "Taksaj stangoj", + "studyFirst": "Al la unua", + "studyPrevious": "Antaŭa", + "studyNext": "Sekva", + "studyLast": "Al la lasta", "studyShareAndExport": "Konigi & eksporti", - "studyStart": "Komenci" + "studyCloneStudy": "Kloni", + "studyStudyPgn": "PGN de la studo", + "studyDownloadAllGames": "Elŝuti ĉiujn ludojn", + "studyChapterPgn": "PGN de la ĉapitro", + "studyCopyChapterPgn": "Kopii PGN", + "studyDownloadGame": "Elŝuti ludon", + "studyStudyUrl": "URL de la studo", + "studyCurrentChapterUrl": "URL de tiu ĉi ĉapitro", + "studyYouCanPasteThisInTheForumToEmbed": "Vi povas alglui ĉi tiun en la forumo aŭ via Lichess blogo por enkorpigi", + "studyStartAtInitialPosition": "Starti ekde komenca pozicio", + "studyStartAtX": "Komenci je {param}", + "studyEmbedInYourWebsite": "Enkorpigi en via retejo", + "studyReadMoreAboutEmbedding": "Legi pli pri enkorpigo", + "studyOnlyPublicStudiesCanBeEmbedded": "Nur publikaj studoj eblas enkorpiĝi!", + "studyOpen": "Malfermi", + "studyXBroughtToYouByY": "{param1}, provizia al vi de {param2}", + "studyStudyNotFound": "Studo ne trovita", + "studyEditChapter": "Redakti ĉapitron", + "studyNewChapter": "Nova ĉapitro", + "studyImportFromChapterX": "Importi el {param}", + "studyOrientation": "Orientiĝo", + "studyAnalysisMode": "Analiza modo", + "studyPinnedChapterComment": "Alpinglita ĉapitra komento", + "studySaveChapter": "Konservi ĉapitron", + "studyClearAnnotations": "Forigi notojn", + "studyClearVariations": "Forigi variaĵojn", + "studyDeleteChapter": "Forigi ĉapitron", + "studyDeleteThisChapter": "Ĉu forigi ĉi tiun ĉapitron? Tiun agon vi ne povos malfari!", + "studyClearAllCommentsInThisChapter": "Forigi ĉiujn komentojn, signobildoj, kaj skribintaj formoj en ĉi tiu ĉapitro", + "studyRightUnderTheBoard": "Ĝuste sub la tabulo", + "studyNoPinnedComment": "Neniu", + "studyNormalAnalysis": "Normala analizo", + "studyHideNextMoves": "Kaŝi la sekvajn movojn", + "studyInteractiveLesson": "Interaga leciono", + "studyChapterX": "Ĉapitro {param}", + "studyEmpty": "Malplena", + "studyStartFromInitialPosition": "Starti el la komenca pozicio", + "studyEditor": "Redaktanto", + "studyStartFromCustomPosition": "Starti el propra pozicio", + "studyLoadAGameByUrl": "Ŝarĝi ludon el URL", + "studyLoadAPositionFromFen": "Ŝarĝi posicion el FEN kodo", + "studyLoadAGameFromPgn": "Ŝarĝi ludon el PGN", + "studyAutomatic": "Aŭtomata", + "studyUrlOfTheGame": "URL de la ludo", + "studyLoadAGameFromXOrY": "Ŝarĝu ludon el {param1} aŭ {param2}", + "studyCreateChapter": "Krei ĉapitron", + "studyCreateStudy": "Krei studon", + "studyEditStudy": "Redakti studon", + "studyVisibility": "Videbleco", + "studyPublic": "Publika", + "studyUnlisted": "Nelistigita", + "studyInviteOnly": "Per invito", + "studyAllowCloning": "Permesi klonadon", + "studyNobody": "Neniu", + "studyOnlyMe": "Nur mi", + "studyContributors": "Kontribuantoj", + "studyMembers": "Membroj", + "studyEveryone": "Ĉiuj", + "studyEnableSync": "Ebligi sinkronigon", + "studyYesKeepEveryoneOnTheSamePosition": "Jes: ĉiuj vidas la saman pozicion", + "studyNoLetPeopleBrowseFreely": "Ne: lasu homojn esplori libere", + "studyPinnedStudyComment": "Komento alpinglita al la studo", + "studyStart": "Komenci", + "studySave": "Konservi", + "studyClearChat": "Vakigi babiladon", + "studyDeleteTheStudyChatHistory": "Ĉu forigi la historian babilejon de la ĉapitro? Tiun agon vi ne povos malfari!", + "studyDeleteStudy": "Forigi studon", + "studyConfirmDeleteStudy": "Ĉu forigi la tuta studo? Ne estas reiro! Tajpi la nomon de la studo por konfirmi: {param}", + "studyWhereDoYouWantToStudyThat": "Kie vi volas studi tion?", + "studyGoodMove": "Bona movo", + "studyMistake": "Eraro", + "studyBrilliantMove": "Brilianta movo", + "studyBlunder": "Erarego", + "studyInterestingMove": "Interesa movo", + "studyDubiousMove": "Dubinda movo", + "studyOnlyMove": "Nura movo", + "studyZugzwang": "Movdevigo", + "studyEqualPosition": "Egala aranĝo", + "studyUnclearPosition": "Malklara aranĝo", + "studyWhiteIsSlightlyBetter": "Blanko estas iomete pli bona", + "studyBlackIsSlightlyBetter": "Nigro estas iomete pli bona", + "studyWhiteIsBetter": "Blanko estas pli bona", + "studyBlackIsBetter": "Nigro estas pli bona", + "studyWhiteIsWinning": "Blanko estas gajnanta", + "studyBlackIsWinning": "Nigro estas gajnanta", + "studyNovelty": "Novaĵo", + "studyDevelopment": "Programado", + "studyInitiative": "Iniciato", + "studyAttack": "Atako", + "studyCounterplay": "Kontraŭludo", + "studyTimeTrouble": "Tempa ĝeno", + "studyWithCompensation": "Kun kompenso", + "studyWithTheIdea": "Kun la ideo", + "studyNextChapter": "Sekva ĉapitro", + "studyPrevChapter": "Antaŭa ĉapitro", + "studyStudyActions": "Studaj agoj", + "studyTopics": "Temoj", + "studyMyTopics": "Miaj temoj", + "studyPopularTopics": "Popularaj temoj", + "studyManageTopics": "Administri temojn", + "studyBack": "Reen", + "studyPlayAgain": "Reludi", + "studyWhatWouldYouPlay": "Kion vi ludus en ĉi tiu pozicio?", + "studyYouCompletedThisLesson": "Gratulon! Vi kompletigis la lecionon.", + "studyNbChapters": "{count, plural, =1{{count} Ĉapitro} other{{count} Ĉapitroj}}", + "studyNbGames": "{count, plural, =1{{count} Ludo} other{{count} Ludoj}}", + "studyNbMembers": "{count, plural, =1{{count} Membro} other{{count} Membroj}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Algluu ĉi tie vian PGN kodon, maksimume ĝis {count} ludo} other{Algluu ĉi tie vian PGN kodon, ĝis maksimume {count} ludoj}}" } \ No newline at end of file diff --git a/lib/l10n/lila_es.arb b/lib/l10n/lila_es.arb index 403728c5af..4c83c59b27 100644 --- a/lib/l10n/lila_es.arb +++ b/lib/l10n/lila_es.arb @@ -30,7 +30,6 @@ "mobilePuzzleStormConfirmEndRun": "¿Quieres finalizar esta ronda?", "mobilePuzzleStormFilterNothingToShow": "Nada que mostrar, por favor cambia los filtros", "mobileCancelTakebackOffer": "Cancelar oferta de deshacer movimiento", - "mobileCancelDrawOffer": "Cancelar ofertas de tablas", "mobileWaitingForOpponentToJoin": "Esperando a que se una un oponente...", "mobileBlindfoldMode": "A ciegas", "mobileLiveStreamers": "Presentadores en vivo", @@ -42,6 +41,7 @@ "mobilePuzzleStormSubtitle": "Resuelve tantos ejercicios como puedas en 3 minutos.", "mobileGreeting": "Hola {param}", "mobileGreetingWithoutName": "Hola", + "mobilePrefMagnifyDraggedPiece": "Aumentar la pieza arrastrada", "activityActivity": "Actividad", "activityHostedALiveStream": "Emitió en directo", "activityRankedInSwissTournament": "#{param1} En la Clasificatoria de {param2}", @@ -54,6 +54,7 @@ "activityPlayedNbMoves": "{count, plural, =1{Ha hecho {count} movimiento} other{Ha hecho {count} movimientos}}", "activityInNbCorrespondenceGames": "{count, plural, =1{en {count} partida por correspondencia} other{en {count} partidas por correspondencia}}", "activityCompletedNbGames": "{count, plural, =1{Ha jugado {count} partida por correspondencia} other{Ha jugado {count} partidas por correspondencia}}", + "activityCompletedNbVariantGames": "{count, plural, =1{Ha completado {count} {param2} partida por correspondencia} other{Ha completado {count} {param2} partidas por correspondencia}}", "activityFollowedNbPlayers": "{count, plural, =1{Sigue a {count} jugador} other{Sigue a {count} jugadores}}", "activityGainedNbFollowers": "{count, plural, =1{Tiene {count} seguidor nuevo} other{Tiene {count} seguidores nuevos}}", "activityHostedNbSimuls": "{count, plural, =1{Ha ofrecido {count} exhibición simultánea} other{Ha ofrecido {count} exhibiciones simultáneas}}", @@ -64,7 +65,72 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{Ha competido en {count} torneo suizo} other{Ha competido en {count} torneos suizos}}", "activityJoinedNbTeams": "{count, plural, =1{Miembro de {count} equipo} other{Miembro de {count} equipos}}", "broadcastBroadcasts": "Emisiones", + "broadcastMyBroadcasts": "Mis transmisiones", "broadcastLiveBroadcasts": "Emisiones de torneos en directo", + "broadcastBroadcastCalendar": "Calendario de transmisiones", + "broadcastNewBroadcast": "Nueva emisión en directo", + "broadcastSubscribedBroadcasts": "Transmisiones suscritas", + "broadcastAboutBroadcasts": "Acerca de las transmisiones", + "broadcastHowToUseLichessBroadcasts": "Como utilizar las transmisiones de Lichess.", + "broadcastTheNewRoundHelp": "La nueva ronda tendrá los mismos miembros y contribuyentes que la anterior.", + "broadcastAddRound": "Añadir una ronda", + "broadcastOngoing": "En curso", + "broadcastUpcoming": "Próximamente", + "broadcastCompleted": "Completadas", + "broadcastCompletedHelp": "Lichess detecta la terminación de la ronda según las partidas de origen. Usa este interruptor si no hay ninguna.", + "broadcastRoundName": "Nombre de la ronda", + "broadcastRoundNumber": "Número de ronda", + "broadcastTournamentName": "Nombre del torneo", + "broadcastTournamentDescription": "Breve descripción del torneo", + "broadcastFullDescription": "Descripción completa del evento", + "broadcastFullDescriptionHelp": "Descripción larga opcional de la emisión. {param1} está disponible. La longitud debe ser inferior a {param2} caracteres.", + "broadcastSourceSingleUrl": "URL origen del archivo PGN", + "broadcastSourceUrlHelp": "URL que Lichess comprobará para obtener actualizaciones PGN. Debe ser públicamente accesible desde Internet.", + "broadcastSourceGameIds": "Hasta 64 identificadores de partidas de Lichess, separados por espacios.", + "broadcastStartDateTimeZone": "Fecha de inicio en la zona horaria local del torneo: {param}", + "broadcastStartDateHelp": "Opcional, si sabes cuando comienza el evento", + "broadcastCurrentGameUrl": "Enlace de la partida actual", + "broadcastDownloadAllRounds": "Descargar todas las rondas", + "broadcastResetRound": "Restablecer esta ronda", + "broadcastDeleteRound": "Eliminar esta ronda", + "broadcastDefinitivelyDeleteRound": "Eliminar definitivamente la ronda y sus partidas.", + "broadcastDeleteAllGamesOfThisRound": "Eliminar todas las partidas de esta ronda. La fuente tendrá que estar activa para volver a crearlos.", + "broadcastEditRoundStudy": "Editar estudio de ronda", + "broadcastDeleteTournament": "Elimina este torneo", + "broadcastDefinitivelyDeleteTournament": "Elimina definitivamente todo el torneo, rondas y partidas incluidas.", + "broadcastShowScores": "Mostrar las puntuaciones de los jugadores según los resultados de las partidas", + "broadcastReplacePlayerTags": "Opcional: reemplazar nombres de jugadores, puntuaciones y títulos", + "broadcastFideFederations": "Federaciones FIDE", + "broadcastTop10Rating": "Los 10 mejores", + "broadcastFidePlayers": "Jugadores FIDE", + "broadcastFidePlayerNotFound": "Jugador FIDE no encontrado", + "broadcastFideProfile": "Perfil FIDE", + "broadcastFederation": "Federación", + "broadcastAgeThisYear": "Edad actual", + "broadcastUnrated": "Sin puntuación", + "broadcastRecentTournaments": "Torneos recientes", + "broadcastOpenLichess": "Abrir en Lichess", + "broadcastTeams": "Equipos", + "broadcastBoards": "Tableros", + "broadcastOverview": "Resumen", + "broadcastSubscribeTitle": "Suscríbete para ser notificado cuando comience cada ronda. Puedes alternar entre notificaciones de campana o de dispositivo para emisiones en las preferencias de tu cuenta.", + "broadcastUploadImage": "Subir imagen del torneo", + "broadcastNoBoardsYet": "Aún no hay tableros. Estos aparecerán una vez se suban las partidas.", + "broadcastBoardsCanBeLoaded": "Los tableros pueden cargarse gracias a una fuente o a través de {param}", + "broadcastStartsAfter": "Comienza en {param}", + "broadcastStartVerySoon": "La transmisión comenzará muy pronto.", + "broadcastNotYetStarted": "La transmisión aún no ha comenzado.", + "broadcastOfficialWebsite": "Sitio oficial", + "broadcastStandings": "Clasificación", + "broadcastIframeHelp": "Más opciones en {param}", + "broadcastWebmastersPage": "la página del webmaster", + "broadcastPgnSourceHelp": "Una fuente PGN pública en tiempo real para esta ronda. También ofrecemos {param} para una sincronización más rápida y eficiente.", + "broadcastEmbedThisBroadcast": "Inserta esta transmisión en tu sitio web", + "broadcastEmbedThisRound": "Inserta la {param} en tu sitio web", + "broadcastRatingDiff": "Diferencia de valoración", + "broadcastGamesThisTournament": "Partidas en este torneo", + "broadcastScore": "Resultado", + "broadcastNbBroadcasts": "{count, plural, =1{{count} retransmisión} other{{count} retransmisiones}}", "challengeChallengesX": "Desafíos: {param1}", "challengeChallengeToPlay": "Desafiar a una partida", "challengeChallengeDeclined": "Desafío rechazado", @@ -383,8 +449,8 @@ "puzzleThemeXRayAttackDescription": "Una pieza ataca o defiende una casilla, a través de una pieza del oponente.", "puzzleThemeZugzwang": "Zugzwang", "puzzleThemeZugzwangDescription": "El oponente está limitado en los movimientos que puede realizar, y todos los movimientos empeoran su posición.", - "puzzleThemeHealthyMix": "Mezcla equilibrada", - "puzzleThemeHealthyMixDescription": "Un poco de todo. No sabes lo que te espera, así que estate listo para cualquier cosa, como en las partidas reales.", + "puzzleThemeMix": "Mezcla equilibrada", + "puzzleThemeMixDescription": "Un poco de todo. No sabes lo que te espera, así que estate listo para cualquier cosa, como en las partidas reales.", "puzzleThemePlayerGames": "Partidas de jugadores", "puzzleThemePlayerGamesDescription": "Busca ejercicios generados a partir de tus partidas o de las de otros jugadores.", "puzzleThemePuzzleDownloadInformation": "Estos ejercicios son de dominio público y pueden descargarse desde {param}.", @@ -515,7 +581,6 @@ "memory": "Memoria", "infiniteAnalysis": "Análisis infinito", "removesTheDepthLimit": "Elimina el límite de profundidad del análisis y hace trabajar a tu ordenador", - "engineManager": "Gestor de motores", "blunder": "Error grave", "mistake": "Error", "inaccuracy": "Imprecisión", @@ -597,6 +662,7 @@ "rank": "Posición", "rankX": "Clasificación: {param}", "gamesPlayed": "Partidas jugadas", + "ok": "Aceptar", "cancel": "Cancelar", "whiteTimeOut": "Las blancas agotaron su tiempo", "blackTimeOut": "Las negras agotaron su tiempo", @@ -819,7 +885,9 @@ "cheat": "Trampa", "troll": "Acoso", "other": "Otro", - "reportDescriptionHelp": "Pega el enlace a la(s) partida(s) y explícanos qué hay de malo en el comportamiento de este usuario. No digas simplemente \"hace trampa\"; explícanos cómo has llegado a esta conclusión. Tu informe será procesado más rápido si está escrito en inglés.", + "reportCheatBoostHelp": "Pega el enlace a la(s) partida(s) y explícanos qué hay de malo en el comportamiento de este usuario. No digas simplemente \"hace trampa\", sino cómo has llegado a esta conclusión.", + "reportUsernameHelp": "Explica qué es lo que te resulta ofensivo de este nombre de usuario. No digas solo \"es ofensivo\" o \"inapropiado\", sino cómo llegaste a esta conclusión, sobre todo si el insulto no es tan obvio, no está en inglés, es jerga o una referencia histórica o cultural.", + "reportProcessedFasterInEnglish": "Tu informe será procesado más rápido si está escrito en inglés.", "error_provideOneCheatedGameLink": "Por favor, proporciona al menos un enlace a una partida en la que se hicieron trampas.", "by": "por {param}", "importedByX": "Importado por {param}", @@ -1217,6 +1285,7 @@ "showMeEverything": "Mostrarme todo", "lichessPatronInfo": "Lichess es una organización benéfica y un software totalmente libre y de código abierto.\nTodos los gastos de funcionamiento, desarrollo y contenidos se financian exclusivamente mediante las donaciones de sus usuarios.", "nothingToSeeHere": "Nada que ver aquí por ahora.", + "stats": "Estadísticas", "opponentLeftCounter": "{count, plural, =1{Tu oponente ha salido de la partida. Podrás reclamar la victoria en {count} segundo.} other{Tu oponente ha salido de la partida. Podrás reclamar la victoria en {count} segundos.}}", "mateInXHalfMoves": "{count, plural, =1{Mate en {count} medio movimiento} other{Mate en {count} medios movimientos}}", "nbBlunders": "{count, plural, =1{{count} error grave} other{{count} errores graves}}", @@ -1224,7 +1293,7 @@ "nbInaccuracies": "{count, plural, =1{{count} imprecisión} other{{count} imprecisiones}}", "nbPlayers": "{count, plural, =1{{count} jugador} other{{count} jugadores}}", "nbGames": "{count, plural, =1{{count} Partida} other{{count} Partidas}}", - "ratingXOverYGames": "{count, plural, =1{puntuación {count} en {param2} partida} other{Puntuación de {count} en {param2} partidas}}", + "ratingXOverYGames": "{count, plural, =1{Puntuación {count} en {param2} partida} other{Puntuación de {count} en {param2} partidas}}", "nbBookmarks": "{count, plural, =1{{count} partida favorita} other{{count} partidas favoritas}}", "nbDays": "{count, plural, =1{{count} día} other{{count} días}}", "nbHours": "{count, plural, =1{{count} hora} other{{count} horas}}", @@ -1313,6 +1382,159 @@ "stormXRuns": "{count, plural, =1{1 ronda} other{{count} rondas}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Has jugado una ronda de {param2}} other{Has jugado {count} rondas de {param2}}}", "streamerLichessStreamers": "Presentadores de Lichess", + "studyPrivate": "Privado", + "studyMyStudies": "Mis estudios", + "studyStudiesIContributeTo": "Estudios en los que colaboro", + "studyMyPublicStudies": "Mis estudios públicos", + "studyMyPrivateStudies": "Mis estudios privados", + "studyMyFavoriteStudies": "Mis estudios favoritos", + "studyWhatAreStudies": "¿Qué son los estudios?", + "studyAllStudies": "Todos los estudios", + "studyStudiesCreatedByX": "Estudios creados por {param}", + "studyNoneYet": "Ninguno por ahora.", + "studyHot": "De interés actualmente", + "studyDateAddedNewest": "Fecha (más recientes)", + "studyDateAddedOldest": "Fecha (más antiguos)", + "studyRecentlyUpdated": "Actualizados recientemente", + "studyMostPopular": "Más populares", + "studyAlphabetical": "Alfabético", + "studyAddNewChapter": "Añadir nuevo capítulo", + "studyAddMembers": "Añadir miembros", + "studyInviteToTheStudy": "Invitar al estudio", + "studyPleaseOnlyInvitePeopleYouKnow": "Por favor, invita sólo a personas que conozcas y que deseen unirse a este estudio.", + "studySearchByUsername": "Buscar por nombre de usuario", + "studySpectator": "Espectador", + "studyContributor": "Colaborador", + "studyKick": "Expulsar", + "studyLeaveTheStudy": "Dejar el estudio", + "studyYouAreNowAContributor": "Ahora eres un colaborador", + "studyYouAreNowASpectator": "Ahora eres un espectador", + "studyPgnTags": "Etiquetas PGN", + "studyLike": "Me gusta", + "studyUnlike": "No me gusta", + "studyNewTag": "Nueva etiqueta", + "studyCommentThisPosition": "Comentar esta posición", + "studyCommentThisMove": "Comentar este movimiento", + "studyAnnotateWithGlyphs": "Anotar con iconos", + "studyTheChapterIsTooShortToBeAnalysed": "El capítulo es demasiado corto para analizarlo.", + "studyOnlyContributorsCanRequestAnalysis": "Sólo los colaboradores del estudio pueden solicitar un análisis por ordenador.", + "studyGetAFullComputerAnalysis": "Obtén un análisis completo de la línea principal en el servidor.", + "studyMakeSureTheChapterIsComplete": "Asegúrate de que el capítulo está completo. Sólo puede solicitar el análisis una vez.", + "studyAllSyncMembersRemainOnTheSamePosition": "Todos los miembros de SYNC permanecen en la misma posición", + "studyShareChanges": "Comparte cambios con los espectadores y guárdalos en el servidor", + "studyPlaying": "Jugando", + "studyShowEvalBar": "Barras de evaluación", + "studyFirst": "Primero", + "studyPrevious": "Anterior", + "studyNext": "Siguiente", + "studyLast": "Último", "studyShareAndExport": "Compartir y exportar", - "studyStart": "Comenzar" + "studyCloneStudy": "Clonar", + "studyStudyPgn": "PGN del estudio", + "studyDownloadAllGames": "Descargar todas las partidas", + "studyChapterPgn": "PGN del capítulo", + "studyCopyChapterPgn": "Copiar PGN", + "studyDownloadGame": "Descargar partida", + "studyStudyUrl": "URL del estudio", + "studyCurrentChapterUrl": "URL del capítulo actual", + "studyYouCanPasteThisInTheForumToEmbed": "Puedes pegar esto en el foro para insertar la partida", + "studyStartAtInitialPosition": "Comenzar desde la posición inicial", + "studyStartAtX": "Comenzar en {param}", + "studyEmbedInYourWebsite": "Insértalo en tu página o blog", + "studyReadMoreAboutEmbedding": "Leer más sobre insertar contenido", + "studyOnlyPublicStudiesCanBeEmbedded": "¡Solo los estudios públicos pueden ser insertados!", + "studyOpen": "Abrir", + "studyXBroughtToYouByY": "{param1}, proporcionado por {param2}", + "studyStudyNotFound": "No se encontró el estudio", + "studyEditChapter": "Editar capítulo", + "studyNewChapter": "Capítulo nuevo", + "studyImportFromChapterX": "Importar de {param}", + "studyOrientation": "Orientación", + "studyAnalysisMode": "Modo de análisis", + "studyPinnedChapterComment": "Comentario fijo para el capítulo", + "studySaveChapter": "Guardar capítulo", + "studyClearAnnotations": "Borrar anotaciones", + "studyClearVariations": "Borrar variantes", + "studyDeleteChapter": "Borrar capítulo", + "studyDeleteThisChapter": "¿Realmente quieres borrar el capítulo? ¡Esta acción no se puede deshacer!", + "studyClearAllCommentsInThisChapter": "¿Borrar todos los comentarios, iconos y marcas de este capítulo?", + "studyRightUnderTheBoard": "Justo debajo del tablero", + "studyNoPinnedComment": "Ninguno", + "studyNormalAnalysis": "Análisis normal", + "studyHideNextMoves": "Ocultar los siguientes movimientos", + "studyInteractiveLesson": "Lección interactiva", + "studyChapterX": "Capítulo {param}", + "studyEmpty": "Vacío", + "studyStartFromInitialPosition": "Comenzar desde la posición inicial", + "studyEditor": "Editor", + "studyStartFromCustomPosition": "Comenzar desde una posición personalizada", + "studyLoadAGameByUrl": "Cargar una partida desde una URL", + "studyLoadAPositionFromFen": "Cargar una posición vía código FEN", + "studyLoadAGameFromPgn": "Cargar una partida vía código PGN", + "studyAutomatic": "Automática", + "studyUrlOfTheGame": "URL de la partida", + "studyLoadAGameFromXOrY": "Cargar una partida desde {param1} o {param2}", + "studyCreateChapter": "Crear capítulo", + "studyCreateStudy": "Crear estudio", + "studyEditStudy": "Editar estudio", + "studyVisibility": "Visibilidad", + "studyPublic": "Público", + "studyUnlisted": "Sin listar", + "studyInviteOnly": "Acceso mediante invitación", + "studyAllowCloning": "Permitir clonado", + "studyNobody": "Nadie", + "studyOnlyMe": "Sólo yo", + "studyContributors": "Colaboradores", + "studyMembers": "Miembros", + "studyEveryone": "Todo el mundo", + "studyEnableSync": "Habilitar sincronización", + "studyYesKeepEveryoneOnTheSamePosition": "Sí: todo el mundo ve la misma posición", + "studyNoLetPeopleBrowseFreely": "No: permitir que la gente navegue libremente", + "studyPinnedStudyComment": "Comentario fijado del estudio", + "studyStart": "Comenzar", + "studySave": "Guardar", + "studyClearChat": "Limpiar el chat", + "studyDeleteTheStudyChatHistory": "¿Realmente quieres borrar el historial de chat? ¡Esta acción no se puede deshacer!", + "studyDeleteStudy": "Borrar estudio", + "studyConfirmDeleteStudy": "¿Seguro que quieres eliminar el estudio? Ten en cuenta que esta acción no se puede deshacer. Para confirmar, escribe el nombre del estudio: {param}", + "studyWhereDoYouWantToStudyThat": "¿Dónde quieres estudiar eso?", + "studyGoodMove": "Jugada buena", + "studyMistake": "Error", + "studyBrilliantMove": "Jugada muy buena", + "studyBlunder": "Error grave", + "studyInterestingMove": "Jugada interesante", + "studyDubiousMove": "Jugada dudosa", + "studyOnlyMove": "Movimiento único", + "studyZugzwang": "Zugzwang", + "studyEqualPosition": "Posición igualada", + "studyUnclearPosition": "Posición poco clara", + "studyWhiteIsSlightlyBetter": "Las blancas están ligeramente mejor", + "studyBlackIsSlightlyBetter": "Las negras están ligeramente mejor", + "studyWhiteIsBetter": "Las blancas están mejor", + "studyBlackIsBetter": "Las negras están mejor", + "studyWhiteIsWinning": "Las blancas están ganando", + "studyBlackIsWinning": "Las negras están ganando", + "studyNovelty": "Novedad", + "studyDevelopment": "Desarrollo", + "studyInitiative": "Iniciativa", + "studyAttack": "Ataque", + "studyCounterplay": "Contrajuego", + "studyTimeTrouble": "Problema de tiempo", + "studyWithCompensation": "Con compensación", + "studyWithTheIdea": "Con la idea", + "studyNextChapter": "Capítulo siguiente", + "studyPrevChapter": "Capítulo anterior", + "studyStudyActions": "Acciones de estudio", + "studyTopics": "Temas", + "studyMyTopics": "Mis temas", + "studyPopularTopics": "Temas populares", + "studyManageTopics": "Administrar temas", + "studyBack": "Volver", + "studyPlayAgain": "Jugar de nuevo", + "studyWhatWouldYouPlay": "¿Qué jugarías en esta posición?", + "studyYouCompletedThisLesson": "¡Felicidades! Has completado esta lección.", + "studyNbChapters": "{count, plural, =1{{count} Capítulo} other{{count} Capítulos}}", + "studyNbGames": "{count, plural, =1{{count} Partida} other{{count} Partidas}}", + "studyNbMembers": "{count, plural, =1{{count} Miembro} other{{count} Miembros}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Pega aquí el código PGN, {count} partida como máximo} other{Pega aquí el código PGN, {count} partidas como máximo}}" } \ No newline at end of file diff --git a/lib/l10n/lila_et.arb b/lib/l10n/lila_et.arb index ea099f7fa3..4da5e3acf2 100644 --- a/lib/l10n/lila_et.arb +++ b/lib/l10n/lila_et.arb @@ -22,6 +22,25 @@ "activityJoinedNbTeams": "{count, plural, =1{Liitus {count} rühmaga} other{Liitus {count} rühmaga}}", "broadcastBroadcasts": "Otseülekanded", "broadcastLiveBroadcasts": "Otseülekanded turniirilt", + "broadcastNewBroadcast": "Uus otseülekanne", + "broadcastAddRound": "Lisa voor", + "broadcastOngoing": "Käimas", + "broadcastUpcoming": "Tulemas", + "broadcastCompleted": "Lõppenud", + "broadcastRoundName": "Vooru nimi", + "broadcastRoundNumber": "Vooru number", + "broadcastTournamentName": "Turniiri nimi", + "broadcastTournamentDescription": "Lühike turniiri kirjeldus", + "broadcastFullDescription": "Sündmuse täielik kirjeldus", + "broadcastFullDescriptionHelp": "Valikuline otseülekande kirjeldus. {param1} on saadaval. Pikkus peab olema maksimaalselt {param2} tähemärki.", + "broadcastSourceUrlHelp": "URL, kust Lichess saab PGN-i värskenduse. See peab olema Internetist kättesaadav.", + "broadcastStartDateHelp": "Valikuline, kui tead millal sündmus algab", + "broadcastCurrentGameUrl": "Praeguse mängu URL", + "broadcastDownloadAllRounds": "Lae alla kõik voorud", + "broadcastResetRound": "Lähtesta see voor", + "broadcastDeleteRound": "Kustuta see voor", + "broadcastDefinitivelyDeleteRound": "Kustuta lõplikult voor ja selle mängud.", + "broadcastDeleteAllGamesOfThisRound": "Kustuta kõik mängud sellest voorust. Allikas peab olema aktiveeritud nende taastamiseks.", "challengeChallengeToPlay": "Väljakutse mängule", "challengeChallengeDeclined": "Väljakutse tagasi lükatud", "challengeChallengeAccepted": "Väljakutse vastu võetud!", @@ -297,8 +316,8 @@ "puzzleThemeVeryLong": "Väga pikk ülesanne", "puzzleThemeZugzwang": "Vahekäik", "puzzleThemeZugzwangDescription": "Vastasel on piiratud võimalused teha lubatud käike ja kõik halvendavad vastase olukorda.", - "puzzleThemeHealthyMix": "Tervislik segu", - "puzzleThemeHealthyMixDescription": "Natuke kõike. Kunagi ei tea mida oodata ehk ole valmis kõigeks! Täpselt nagu päris mängudes.", + "puzzleThemeMix": "Tervislik segu", + "puzzleThemeMixDescription": "Natuke kõike. Kunagi ei tea mida oodata ehk ole valmis kõigeks! Täpselt nagu päris mängudes.", "searchSearch": "Otsi", "settingsSettings": "Seaded", "settingsCloseAccount": "Sulge konto", @@ -706,7 +725,6 @@ "cheat": "Sohk", "troll": "Troll", "other": "Muu", - "reportDescriptionHelp": "Kleebi link mängu(de)st ja selgita, mis on valesti selle kasutaja käitumises. Ära ütle lihtsalt \"ta teeb sohki\", vaid seleta, kuidas sa selle järelduseni jõudsid. Sõnum käsitletakse kiiremini kui see on kirjutatud inglise keeles.", "error_provideOneCheatedGameLink": "Palun andke vähemalt üks link pettust sisaldavale mängule.", "by": "autor {param}", "importedByX": "Importis {param}", @@ -1074,7 +1092,7 @@ "ourEventTips": "Meie nõuanded ürituste korraldamiseks", "lichessPatronInfo": "Lichess on heategevuslik ja täiesti tasuta avatud lähtekoodiga tarkvara.\nKõik tegevuskulud, arendus ja sisu rahastatakse ainult kasutajate annetustest.", "opponentLeftCounter": "{count, plural, =1{Vastane lahkus mängust. Saate panna vastase alistuma {count} sekundi pärast.} other{Vastane lahkus mängust. Saad kuulutada ennast võitjaks {count} sekundi pärast.}}", - "mateInXHalfMoves": "{count, plural, =1{Šahh ja Matt {count} käiguga} other{Šahh ja matt {count} käiguga}}", + "mateInXHalfMoves": "{count, plural, =1{Matt {count} käiguga} other{Matt {count} käiguga}}", "nbBlunders": "{count, plural, =1{{count} prohmakas} other{{count} prohmakat}}", "nbMistakes": "{count, plural, =1{{count} viga} other{{count} viga}}", "nbInaccuracies": "{count, plural, =1{{count} ebatäpsus} other{{count} ebatäpsust}}", @@ -1167,6 +1185,151 @@ "stormXRuns": "{count, plural, =1{1 mäng} other{{count} mängu}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Mängis ühe mängu {param2}i} other{Mängis {count} mängu {param2}i}}", "streamerLichessStreamers": "Lichessi striimijad", + "studyPrivate": "Privaatne", + "studyMyStudies": "Minu uuringud", + "studyStudiesIContributeTo": "Uuringud, milles osalen", + "studyMyPublicStudies": "Minu avalikud uuringud", + "studyMyPrivateStudies": "Minu privaatsed uuringud", + "studyMyFavoriteStudies": "Minu lemmikuuringud", + "studyWhatAreStudies": "Mis on uuringud?", + "studyAllStudies": "Kõik uuringud", + "studyStudiesCreatedByX": "{param} loodud uuringud", + "studyNoneYet": "Veel mitte ühtegi.", + "studyHot": "Kuum", + "studyDateAddedNewest": "Lisamisaeg (uusimad)", + "studyDateAddedOldest": "Lisamisaeg (vanimad)", + "studyRecentlyUpdated": "Hiljuti uuendatud", + "studyMostPopular": "Kõige populaarsemad", + "studyAlphabetical": "Tähestikuline", + "studyAddNewChapter": "Lisa uus peatükk", + "studyAddMembers": "Lisa liikmeid", + "studyInviteToTheStudy": "Kutsu uuringule", + "studyPleaseOnlyInvitePeopleYouKnow": "Palun kutsuge ainult inimesi keda te teate ning kes soovivad aktiivselt selle uuringuga liituda.", + "studySearchByUsername": "Otsi kasutajanime järgi", + "studySpectator": "Vaatleja", + "studyContributor": "Panustaja", + "studyKick": "Viska välja", + "studyLeaveTheStudy": "Lahku uuringust", + "studyYouAreNowAContributor": "Te olete nüüd panustaja", + "studyYouAreNowASpectator": "Te olete nüüd vaatleja", + "studyPgnTags": "PGN sildid", + "studyLike": "Meeldib", + "studyUnlike": "Eemalda meeldimine", + "studyNewTag": "Uus silt", + "studyCommentThisPosition": "Kommenteeri seda seisu", + "studyCommentThisMove": "Kommenteeri seda käiku", + "studyAnnotateWithGlyphs": "Annoteerige glüüfidega", + "studyTheChapterIsTooShortToBeAnalysed": "See peatükk on liiga lühike analüüsimiseks.", + "studyOnlyContributorsCanRequestAnalysis": "Ainult selle uuringu panustajad saavad taotleda arvuti analüüsi.", + "studyGetAFullComputerAnalysis": "Taotle täielikku serveripoolset arvuti analüüsi põhiliinist.", + "studyPlaying": "Mängimas", + "studyFirst": "Esimene", + "studyPrevious": "Eelmine", + "studyNext": "Järgmine", + "studyLast": "Viimane", "studyShareAndExport": "Jaga & ekspordi", - "studyStart": "Alusta" + "studyCloneStudy": "Klooni", + "studyStudyPgn": "Uuringu PGN", + "studyDownloadAllGames": "Lae alla kõik mängud", + "studyChapterPgn": "Peatüki PGN", + "studyCopyChapterPgn": "Kopeeri PGN", + "studyDownloadGame": "Lae alla mäng", + "studyStudyUrl": "Uuringu URL", + "studyCurrentChapterUrl": "Praeguse peatüki URL", + "studyYouCanPasteThisInTheForumToEmbed": "Te saate selle asetada foorumisse või oma Lichessi blogisse sängitamiseks", + "studyStartAtInitialPosition": "Alusta algseisus", + "studyStartAtX": "Alusta {param}", + "studyEmbedInYourWebsite": "Sängita oma veebilehele", + "studyReadMoreAboutEmbedding": "Loe rohkem sängitamisest", + "studyOnlyPublicStudiesCanBeEmbedded": "Ainult avalikud uurimused on sängitatavad!", + "studyOpen": "Ava", + "studyXBroughtToYouByY": "{param1}, leheküljelt {param2}", + "studyStudyNotFound": "Uuringut ei leitud", + "studyEditChapter": "Muuda peatükki", + "studyNewChapter": "Uus peatükk", + "studyImportFromChapterX": "Too peatükist {param}", + "studyOrientation": "Suund", + "studyAnalysisMode": "Analüüsirežiim", + "studyPinnedChapterComment": "Kinnitatud peatüki kommentaar", + "studySaveChapter": "Salvesta peatükk", + "studyClearAnnotations": "Eemalda kommentaarid", + "studyClearVariations": "Eemalda variatsioonid", + "studyDeleteChapter": "Kustuta peatükk", + "studyDeleteThisChapter": "Kustuta see peatükk? Seda ei saa tühistada!", + "studyClearAllCommentsInThisChapter": "Puhasta kõik kommentaarid, glüüfid ja joonistatud kujundid sellest peatükist", + "studyRightUnderTheBoard": "Otse laua all", + "studyNoPinnedComment": "Puudub", + "studyNormalAnalysis": "Tavaline analüüs", + "studyHideNextMoves": "Peida järgmised käigud", + "studyInteractiveLesson": "Interaktiivne õppetund", + "studyChapterX": "Peatükk {param}", + "studyEmpty": "Tühi", + "studyStartFromInitialPosition": "Alusta algsest positsioonist", + "studyEditor": "Muuda", + "studyStartFromCustomPosition": "Alusta kohandatud positsioonist", + "studyLoadAGameByUrl": "Lae mäng alla URL-ist", + "studyLoadAPositionFromFen": "Laadi alla positsioon FEN-ist", + "studyLoadAGameFromPgn": "Lae mänge PGN-ist", + "studyAutomatic": "Automaatne", + "studyUrlOfTheGame": "URL mängu", + "studyLoadAGameFromXOrY": "Lae mäng alla {param1} või {param2}", + "studyCreateChapter": "Alusta peatükk", + "studyCreateStudy": "Koosta uuring", + "studyEditStudy": "Muuda uuringut", + "studyVisibility": "Nähtavus", + "studyPublic": "Avalik", + "studyUnlisted": "Mitte avalik", + "studyInviteOnly": "Ainult kutsega", + "studyAllowCloning": "Luba kloneerimine", + "studyNobody": "Mitte keegi", + "studyOnlyMe": "Ainult mina", + "studyContributors": "Panustajad", + "studyMembers": "Liikmed", + "studyEveryone": "Kõik", + "studyEnableSync": "Luba sünkroneerimine", + "studyYesKeepEveryoneOnTheSamePosition": "Jah: hoia kõik samal positsioonil", + "studyNoLetPeopleBrowseFreely": "Ei: lase inimestel sirvida vabalt", + "studyPinnedStudyComment": "Kinnitatud uuringu kommentaar", + "studyStart": "Alusta", + "studySave": "Salvesta", + "studyDeleteTheStudyChatHistory": "Kas soovite kustutada uuringu vestluse ajaloo? Seda otsust ei saa tagasi võtta!", + "studyDeleteStudy": "Kustuta uuring", + "studyConfirmDeleteStudy": "Kas soovite kustutada terve uuringu? Seda otsust ei saa tagasi võtta! Kirjutage uuringu nimi otsuse kinnitamiseks: {param}", + "studyWhereDoYouWantToStudyThat": "Kus te seda lauda soovite uurida?", + "studyGoodMove": "Hea käik", + "studyMistake": "Viga", + "studyBrilliantMove": "Suurepärane käik", + "studyBlunder": "Tõsine viga", + "studyInterestingMove": "Huvitav käik", + "studyDubiousMove": "Kahtlane käik", + "studyOnlyMove": "Ainus käik", + "studyZugzwang": "Sundkäik", + "studyEqualPosition": "Võrdne positsioon", + "studyUnclearPosition": "Ebaselge positsioon", + "studyWhiteIsSlightlyBetter": "Valgel on kerge eelis", + "studyBlackIsSlightlyBetter": "Mustal on kerge eelis", + "studyWhiteIsBetter": "Valgel on eelis", + "studyBlackIsBetter": "Mustal on eelis", + "studyWhiteIsWinning": "Valge on võitmas", + "studyBlackIsWinning": "Must on võitmas", + "studyNovelty": "Uudsus", + "studyDevelopment": "Arendus", + "studyInitiative": "Algatus", + "studyAttack": "Rünnak", + "studyCounterplay": "Vastumäng", + "studyNextChapter": "Järgmine peatükk", + "studyPrevChapter": "Eelmine peatükk", + "studyStudyActions": "Uuringu toimingud", + "studyTopics": "Teemad", + "studyMyTopics": "Minu teemad", + "studyPopularTopics": "Populaarsed teemad", + "studyManageTopics": "Halda teemasid", + "studyBack": "Tagasi", + "studyPlayAgain": "Mängi uuesti", + "studyWhatWouldYouPlay": "Mis sa mängiksid selles positsioonis?", + "studyYouCompletedThisLesson": "Palju õnne! Oled läbinud selle õppetunni.", + "studyNbChapters": "{count, plural, =1{{count} peatükk} other{{count} peatükki}}", + "studyNbGames": "{count, plural, =1{{count} mäng} other{{count} mängu}}", + "studyNbMembers": "{count, plural, =1{{count} liige} other{{count} liiget}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Aseta oma PGN tekst siia, kuni {count} mäng} other{Aseta oma PGN tekst siia, kuni {count} mängu}}" } \ No newline at end of file diff --git a/lib/l10n/lila_eu.arb b/lib/l10n/lila_eu.arb index b5b98311c3..2fc816570f 100644 --- a/lib/l10n/lila_eu.arb +++ b/lib/l10n/lila_eu.arb @@ -1,4 +1,47 @@ { + "mobileHomeTab": "Hasiera", + "mobilePuzzlesTab": "Ariketak", + "mobileToolsTab": "Tresnak", + "mobileWatchTab": "Ikusi", + "mobileSettingsTab": "Ezarpenak", + "mobileMustBeLoggedIn": "Sartu egin behar zara orri hau ikusteko.", + "mobileSystemColors": "Sistemaren koloreak", + "mobileFeedbackButton": "Iritzia", + "mobileOkButton": "Ados", + "mobileSettingsHapticFeedback": "Ukipen-erantzuna", + "mobileSettingsImmersiveMode": "Murgiltze modua", + "mobileSettingsImmersiveModeSubtitle": "Ezkutatu sistemaren menuak jokatzen ari zaren artean. Erabili hau zure telefonoaren nabigatzeko aukerek traba egiten badizute. Partida bati eta ariketen zaparradan aplikatu daiteke.", + "mobileNotFollowingAnyUser": "Ez zaude erabiltzailerik jarraitzen.", + "mobileAllGames": "Partida guztiak", + "mobileRecentSearches": "Azken bilaketak", + "mobileClearButton": "Garbitu", + "mobilePlayersMatchingSearchTerm": "\"{param}\" duten jokalariak", + "mobileNoSearchResults": "Emaitzarik ez", + "mobileAreYouSure": "Ziur zaude?", + "mobilePuzzleStreakAbortWarning": "Zure uneko bolada galduko duzu eta zure puntuazioa gorde egingo da.", + "mobilePuzzleStormNothingToShow": "Ez dago ezer erakusteko. Jokatu Ariketa zaparrada batzuk.", + "mobileSharePuzzle": "Partekatu ariketa hau", + "mobileShareGameURL": "Partekatu partidaren URLa", + "mobileShareGamePGN": "Partekatu PGNa", + "mobileSharePositionAsFEN": "Partekatu posizioa FEN gisa", + "mobileShowVariations": "Erakutsi aukerak", + "mobileHideVariation": "Ezkutatu aukera", + "mobileShowComments": "Erakutsi iruzkinak", + "mobilePuzzleStormConfirmEndRun": "Saiakera hau amaitu nahi duzu?", + "mobilePuzzleStormFilterNothingToShow": "Ez dago erakusteko ezer, aldatu filtroak", + "mobileCancelTakebackOffer": "Bertan behera utzi atzera-egite eskaera", + "mobileWaitingForOpponentToJoin": "Aurkaria sartzeko zain...", + "mobileBlindfoldMode": "Itsuka", + "mobileLiveStreamers": "Zuzeneko streamerrak", + "mobileCustomGameJoinAGame": "Sartu partida baten", + "mobileCorrespondenceClearSavedMove": "Garbitu gordetako jokaldia", + "mobileSomethingWentWrong": "Zerbait gaizki joan da.", + "mobileShowResult": "Erakutsi emaitza", + "mobilePuzzleThemesSubtitle": "Jokatu zure irekiera gogokoenen ariketak, edo aukeratu gai bat.", + "mobilePuzzleStormSubtitle": "Ebatzi ahalik eta ariketa gehien 3 minututan.", + "mobileGreeting": "Kaixo {param}", + "mobileGreetingWithoutName": "Kaixo", + "mobilePrefMagnifyDraggedPiece": "Handitu arrastatutako pieza", "activityActivity": "Jarduera", "activityHostedALiveStream": "Zuzeneko emanaldi bat egin du", "activityRankedInSwissTournament": "Sailkapena {param1}/{param2}", @@ -11,6 +54,7 @@ "activityPlayedNbMoves": "{count, plural, =1{Jokaldi {count} egin du} other{Jokaldi {count} egin du}}", "activityInNbCorrespondenceGames": "{count, plural, =1{posta bidezko partida {count}-en} other{posta bidezko partida {count}-en}}", "activityCompletedNbGames": "{count, plural, =1{Posta bidezko partida {count} jokatu du} other{Posta bidezko {count} partida jokatu ditu}}", + "activityCompletedNbVariantGames": "{count, plural, =1{{param2} posta bidezko partida {count} osatuta} other{{param2} posta bidezko {count} partida osatuta}}", "activityFollowedNbPlayers": "{count, plural, =1{Jokalari {count} jarraitzen hasi da} other{{count} jokalari jarraitzen hasi da}}", "activityGainedNbFollowers": "{count, plural, =1{Jarraitzaile berri {count} lortu du} other{{count} jarraitzaile berri lortu ditu}}", "activityHostedNbSimuls": "{count, plural, =1{Aldibereko partiden saio {count} antolatu du} other{Aldibereko partiden {count} saio antolatu ditu}}", @@ -21,7 +65,51 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{Txapelketa suitzar {count}en hartu du parte} other{{count} txapelketa suitzarretan hartu du parte}}", "activityJoinedNbTeams": "{count, plural, =1{Talde {count}era sartu da} other{{count} taldetara sartu da}}", "broadcastBroadcasts": "Emanaldiak", + "broadcastMyBroadcasts": "Nire zuzenekoak", "broadcastLiveBroadcasts": "Txapelketen zuzeneko emanaldiak", + "broadcastBroadcastCalendar": "Emanaldien egutegia", + "broadcastNewBroadcast": "Zuzeneko emanaldi berria", + "broadcastSubscribedBroadcasts": "Harpidetutako emanaldiak", + "broadcastAboutBroadcasts": "Zuzeneko emanaldiei buruz", + "broadcastHowToUseLichessBroadcasts": "Nola erabili Lichessen Zuzenekoak.", + "broadcastTheNewRoundHelp": "Txanda berriak aurrekoak beste kide eta laguntzaile izango ditu.", + "broadcastAddRound": "Gehitu txanda bat", + "broadcastOngoing": "Orain martxan", + "broadcastUpcoming": "Hurrengo emanaldiak", + "broadcastCompleted": "Amaitutako emanaldiak", + "broadcastCompletedHelp": "Txanda amaitu dela jatorrizko partidekin detektatzen du Lichessek. Erabili aukera hau jatorririk ez badago.", + "broadcastRoundName": "Txandaren izena", + "broadcastRoundNumber": "Txanda zenbaki", + "broadcastTournamentName": "Txapelketaren izena", + "broadcastTournamentDescription": "Txapelketaren deskribapen laburra", + "broadcastFullDescription": "Ekitaldiaren deskribapen osoa", + "broadcastFullDescriptionHelp": "Emanaldiaren azalpen luzea, hautazkoa da. {param1} badago. Luzera {param2} karaktere edo laburragoa izan behar da.", + "broadcastSourceSingleUrl": "PGNaren jatorrizko URLa", + "broadcastSourceUrlHelp": "Lichessek PGNaren eguneraketak jasoko dituen URLa. Interneteko helbide bat izan behar da.", + "broadcastSourceGameIds": "Gehienez ere Lichesseko 64 partidren idak, espazioekin banatuta.", + "broadcastStartDateTimeZone": "Txapelketaren hasiera ordua ordu-zona lokalean: {param}", + "broadcastStartDateHelp": "Hautazkoa, ekitaldia noiz hasten den baldin badakizu", + "broadcastCurrentGameUrl": "Uneko partidaren URL helbidea", + "broadcastDownloadAllRounds": "Deskargatu txanda guztiak", + "broadcastResetRound": "Berrezarri txanda hau", + "broadcastDeleteRound": "Ezabatu txanda hau", + "broadcastDefinitivelyDeleteRound": "Betiko ezabatu txanda eta bere partida guztiak.", + "broadcastDeleteAllGamesOfThisRound": "Ezabatu txanda honetako partida guztiak. Jatorria aktibo egon behar da berriz sortzeko.", + "broadcastEditRoundStudy": "Editatu txandako azterlana", + "broadcastDeleteTournament": "Ezabatu txapelketa hau", + "broadcastDefinitivelyDeleteTournament": "Txapelketa behin betiko ezabatu, bere txanda eta partida guztiak barne.", + "broadcastShowScores": "Erakutsi jokalarien puntuazioak partiden emaitzen arabera", + "broadcastReplacePlayerTags": "Hautazkoa: aldatu jokalarien izen, puntuazio eta tituluak", + "broadcastFideFederations": "FIDE federazioak", + "broadcastTop10Rating": "10 onenak", + "broadcastFidePlayers": "FIDE jokalariak", + "broadcastFidePlayerNotFound": "FIDE jokalaria ez da aurkitu", + "broadcastFideProfile": "FIDE profila", + "broadcastFederation": "Federazioa", + "broadcastAgeThisYear": "Adina", + "broadcastUnrated": "Ez du sailkapenik", + "broadcastRecentTournaments": "Azken txapelketak", + "broadcastNbBroadcasts": "{count, plural, =1{Zuzeneko {count}} other{{count} zuzeneko}}", "challengeChallengesX": "Erronkak: {param1}", "challengeChallengeToPlay": "Partida baterako erronka egin", "challengeChallengeDeclined": "Erronka baztertuta", @@ -340,8 +428,8 @@ "puzzleThemeXRayAttackDescription": "Aurkariaren pieza baten artetik, pieza batek lauki bat erasotu edo defendatzen duenean.", "puzzleThemeZugzwang": "Zugzwang", "puzzleThemeZugzwangDescription": "Aurkariak jokaldi mugatuak ditu eta jokaldi guztien bere posizioa okertu egiten dute.", - "puzzleThemeHealthyMix": "Denetik pixkat", - "puzzleThemeHealthyMixDescription": "Denetatik. Ez dakizu zer espero, beraz prestatu zure burua edozertarako! Benetako partidetan bezala.", + "puzzleThemeMix": "Denetik pixkat", + "puzzleThemeMixDescription": "Denetatik. Ez dakizu zer espero, beraz prestatu zure burua edozertarako! Benetako partidetan bezala.", "puzzleThemePlayerGames": "Jokalarien partidak", "puzzleThemePlayerGamesDescription": "Ikusi zure edo beste jokalarien partidetatik sortutako ariketak.", "puzzleThemePuzzleDownloadInformation": "Ariketa hauek publikoak dira, {param} helbidetik deskargatu daitezke.", @@ -420,6 +508,8 @@ "promoteVariation": "Aldaera nagusi bihurtu", "makeMainLine": "Linea nagusi bihurtu", "deleteFromHere": "Ezabatu hemendik aurrera", + "collapseVariations": "Ezkutatu aldaerak", + "expandVariations": "Erakutsi aldaerak", "forceVariation": "Aldaera derrigortu", "copyVariationPgn": "Kopiatu ingurabidearen PGNa", "move": "Jokaldia", @@ -470,7 +560,6 @@ "memory": "Memoria", "infiniteAnalysis": "Analisi infinitua", "removesTheDepthLimit": "Sakonera muga ezabatzendu eta zure ordenagailua epel mantentzen du", - "engineManager": "Motore kudeatzailea", "blunder": "Hanka-sartzea", "mistake": "Akatsa", "inaccuracy": "Akats txikia", @@ -496,6 +585,7 @@ "latestForumPosts": "Foroko azken mezuak", "players": "Jokalariak", "friends": "Lagunak", + "otherPlayers": "beste jokalariak", "discussions": "Eztabaidak", "today": "Gaur", "yesterday": "Atzo", @@ -570,6 +660,7 @@ "abortGame": "Partida geldiarazi", "gameAborted": "Geldiarazitako partida", "standard": "Ohikoa", + "customPosition": "Posizio pertsonalizatua", "unlimited": "Mugagabea", "mode": "Modua", "casual": "Lagunartekoa", @@ -725,6 +816,7 @@ "ifNoneLeaveEmpty": "Ez baduzu, hutsik utzi", "profile": "Profila", "editProfile": "Nire profila editatu", + "realName": "Benetako izena", "setFlair": "Ezarri zure iruditxoa", "flair": "Iruditxoa", "youCanHideFlair": "Webgune guztian zehar erabiltzaile guztien iruditxoak ezkutatzeko ezarpen bat dago.", @@ -744,12 +836,15 @@ "automaticallyProceedToNextGameAfterMoving": "Mugitu ondoren, hurrengo partidara joan", "autoSwitch": "Hurrengo partidara", "puzzles": "Ariketak", + "onlineBots": "Online dauden botak", "name": "Izena", "description": "Deskribapena", "descPrivate": "Deskribapen pribatua", "descPrivateHelp": "Taldekideek bakarrik ikusiko duten testua. Ezarrita badago, taldekideei taldearen deskribapenaren ordez agertuko zaie testu hau.", "no": "Ez", "yes": "Bai", + "website": "Webgunea", + "mobile": "Mobila", "help": "Laguntza:", "createANewTopic": "Gai berria sortu", "topics": "Gaiak", @@ -768,7 +863,9 @@ "cheat": "Tranpak", "troll": "Trolla", "other": "Bestelakoak", - "reportDescriptionHelp": "Partidaren esteka itsasi, eta azaldu zer egin duen gaizki erabiltzaileak. Ez esan \"tranpak egiten ditu\" bakarrik, eman horren arrazoiak. Zure mezua azkarrago begiratuko dugu ingelesez idazten baduzu.", + "reportCheatBoostHelp": "Partidaren esteka itsasi, eta azaldu zer egin duen gaizki erabiltzaileak. Ez esan \"tranpak egiten ditu\" bakarrik, eman horren arrazoiak.", + "reportUsernameHelp": "Azaldu erabiltzaile-izen honek zer duen iraingarria. Ez esan \"iraingarria da\" soilik, eman arrazoiak, batez ere iraina ezkutatuta badago, ez bada ingelesezko hitz bat edo errefererantzia historiko edo kulturala bada.", + "reportProcessedFasterInEnglish": "Zure mezua azkarrago kudeatuko dugu ingelesez idazten baduzu.", "error_provideOneCheatedGameLink": "Iruzurra izandako partida baten lotura bidali gutxienez.", "by": "egilea {param}", "importedByX": "{param} erabiltzaileak inportatuta", @@ -801,6 +898,7 @@ "slow": "Geldoa", "insideTheBoard": "Taula barruan", "outsideTheBoard": "Taulatik kanpo", + "allSquaresOfTheBoard": "Taulako lauki guztiak", "onSlowGames": "Partida moteletan", "always": "Beti", "never": "Inoiz ere ez", @@ -980,6 +1078,12 @@ "transparent": "Gardena", "deviceTheme": "Gailuaren gaia", "backgroundImageUrl": "Atzeko-planoko irudia:", + "board": "Taula", + "size": "Tamaina", + "opacity": "Gardentasuna", + "brightness": "Argitasuna", + "hue": "Ñabardura", + "boardReset": "Berrezarri koloreak defektuzkoetara", "pieceSet": "Pieza formatua", "embedInYourWebsite": "Zure webgunean txertatu", "usernameAlreadyUsed": "Erabiltzaile izen hori hartuta dago, erabili beste bat.", @@ -1158,6 +1262,7 @@ "instructions": "Jarraibideak", "showMeEverything": "Erakutsi guztia", "lichessPatronInfo": "Lichess software librea da.\nGarapen eta mantentze-kostu guztiak erabiltzaileen dohaintzekin ordaintzen dira.", + "nothingToSeeHere": "Hemen ez dago ezer zuretzat.", "opponentLeftCounter": "{count, plural, =1{Zure aurkariak partida utzi egin du. Partida irabaztea eskatu dezakezu segundo {count}en.} other{Zure aurkariak partida utzi egin du. Partida irabaztea eskatu dezakezu {count} segundotan.}}", "mateInXHalfMoves": "{count, plural, =1{Mate jokaldi erdi {count}n} other{Mate {count} jokaldi erditan}}", "nbBlunders": "{count, plural, =1{Hanka-sartze {count}} other{{count} hanka-sartze}}", @@ -1254,6 +1359,159 @@ "stormXRuns": "{count, plural, =1{Saiakera 1} other{{count} saiakera}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{{param2} ariketaren saiakera bat egin duzu} other{{param2} ariketaren {count} saiakera egin dituzu}}", "streamerLichessStreamers": "Lichess streamerrak", + "studyPrivate": "Pribatua", + "studyMyStudies": "Nire azterlanak", + "studyStudiesIContributeTo": "Nik parte hartzen dudan azterlanak", + "studyMyPublicStudies": "Nire azterlan publikoak", + "studyMyPrivateStudies": "Nire azterlan pribatuak", + "studyMyFavoriteStudies": "Nire azterlan gogokoenak", + "studyWhatAreStudies": "Zer dira azterlanak?", + "studyAllStudies": "Azterlan guztiak", + "studyStudiesCreatedByX": "{param} erabiltzaileak sortutako azterlanak", + "studyNoneYet": "Bat ere ez.", + "studyHot": "Nabarmendutakoak", + "studyDateAddedNewest": "Sorrera-data (berriena)", + "studyDateAddedOldest": "Sorrera-data (zaharrena)", + "studyRecentlyUpdated": "Eguneratutako azkenak", + "studyMostPopular": "Arrakasta gehien duena", + "studyAlphabetical": "Alfabetikoa", + "studyAddNewChapter": "Kapitulu berria gehitu", + "studyAddMembers": "Kideak gehitu", + "studyInviteToTheStudy": "Azterlanera gonbidatu", + "studyPleaseOnlyInvitePeopleYouKnow": "Ezagutzen duzun eta benetan azterlanean interesa duen jendea gonbidatu bakarrik.", + "studySearchByUsername": "Erabiltzaile izenaren arabera bilatu", + "studySpectator": "Ikuslea", + "studyContributor": "Laguntzailea", + "studyKick": "Kanporatu", + "studyLeaveTheStudy": "Azterlana utzi", + "studyYouAreNowAContributor": "Laguntzailea zara orain", + "studyYouAreNowASpectator": "Ikuslea zara orain", + "studyPgnTags": "PGN etiketak", + "studyLike": "Datsegit", + "studyUnlike": "Ez dut atsegin", + "studyNewTag": "Etiketa berria", + "studyCommentThisPosition": "Posizio hau komentatu", + "studyCommentThisMove": "Jokaldi hau komentatu", + "studyAnnotateWithGlyphs": "Ikonoekin komentatu", + "studyTheChapterIsTooShortToBeAnalysed": "Komentatzeko laburregia da kapitulua.", + "studyOnlyContributorsCanRequestAnalysis": "Azterlanaren laguntzaileek bakarrik eskatu dezakete ordenagailu bidezko analisia.", + "studyGetAFullComputerAnalysis": "Linea nagusiaren ordenagailu bidezko analisia lortu.", + "studyMakeSureTheChapterIsComplete": "Ziurtatu kapitulua guztiz osatu duzula. Analisia behin bakarrik eskatu dezakezu.", + "studyAllSyncMembersRemainOnTheSamePosition": "Kide sinkronizatu guztiak posizio berean jarraitzen dute", + "studyShareChanges": "Aldaketak ikusleekin partekatu eta zerbitzarian gorde", + "studyPlaying": "Jokatzen", + "studyShowEvalBar": "Ebaluazio barrak", + "studyFirst": "Lehenengoa", + "studyPrevious": "Aurrekoa", + "studyNext": "Hurrengoa", + "studyLast": "Azkena", "studyShareAndExport": "Partekatu & esportatu", - "studyStart": "Hasi" + "studyCloneStudy": "Klonatu", + "studyStudyPgn": "Azterlanaren PGNa", + "studyDownloadAllGames": "Partida guztiak deskargatu", + "studyChapterPgn": "Kapituluaren PGNa", + "studyCopyChapterPgn": "Kopiatu PGNa", + "studyDownloadGame": "Partida deskargatu", + "studyStudyUrl": "Azterlanaren helbidea", + "studyCurrentChapterUrl": "Uneko kapituluaren helbidea", + "studyYouCanPasteThisInTheForumToEmbed": "Hau foroan itsatsi dezakezu", + "studyStartAtInitialPosition": "Hasierako posizioan hasi", + "studyStartAtX": "Hemen asi {param}", + "studyEmbedInYourWebsite": "Zure webgunean itsatsi", + "studyReadMoreAboutEmbedding": "Itsasteari buruz gehiago irakurri", + "studyOnlyPublicStudiesCanBeEmbedded": "Azterlan publikoak bakarrik txertatu daitezke beste webguneetan!", + "studyOpen": "Ireki", + "studyXBroughtToYouByY": "{param1} azterlana {param2} erabiltzaileak prestatu du", + "studyStudyNotFound": "Azterlana ez da aurkitu", + "studyEditChapter": "Kapitulua aldatu", + "studyNewChapter": "Kapitulu berria", + "studyImportFromChapterX": "Inportatu {param} kapitulotik", + "studyOrientation": "Kokapena", + "studyAnalysisMode": "Analisi modua", + "studyPinnedChapterComment": "Kapituluaren iltzatutako iruzkina", + "studySaveChapter": "Kapitulua gorde", + "studyClearAnnotations": "Iruzkinak garbitu", + "studyClearVariations": "Garbitu aldaerak", + "studyDeleteChapter": "Kapitulua ezabatu", + "studyDeleteThisChapter": "Kapitulu hau ezabatu egin nahi duzu? Ez dago atzera egiterik!", + "studyClearAllCommentsInThisChapter": "Kapitulu honetako iruzkin guztiak ezabatu?", + "studyRightUnderTheBoard": "Xake-taularen azpian", + "studyNoPinnedComment": "Ez erakutsi", + "studyNormalAnalysis": "Analisi arrunta", + "studyHideNextMoves": "Hurrengo jokaldiak ezkutatu", + "studyInteractiveLesson": "Ikasgai interaktiboa", + "studyChapterX": "{param} kapitulua", + "studyEmpty": "Hutsa", + "studyStartFromInitialPosition": "Hasierako posiziotik hasi", + "studyEditor": "Editorea", + "studyStartFromCustomPosition": "Pertsonalizatutako posiziotik hasi", + "studyLoadAGameByUrl": "Partida interneteko helbide batetik kargatu", + "studyLoadAPositionFromFen": "Posizioa FEN batetik kargatu", + "studyLoadAGameFromPgn": "Partida PGN batetik kargatu", + "studyAutomatic": "Automatikoa", + "studyUrlOfTheGame": "Partidaren URLa", + "studyLoadAGameFromXOrY": "Hemendik kargatu partida bat: {param1} edo {param2}", + "studyCreateChapter": "Kapitulua sortu", + "studyCreateStudy": "Azterlana sortu", + "studyEditStudy": "Azterlana aldatu", + "studyVisibility": "Ikusgaitasuna", + "studyPublic": "Publikoa", + "studyUnlisted": "Ez zerrendatu", + "studyInviteOnly": "Gonbidatuentzat bakarrik", + "studyAllowCloning": "Kopiatzea utzi", + "studyNobody": "Inor ere ez", + "studyOnlyMe": "Ni bakarrik", + "studyContributors": "Laguntzaileak", + "studyMembers": "Kideak", + "studyEveryone": "Guztiak", + "studyEnableSync": "Sinkronizazioa aktibatu", + "studyYesKeepEveryoneOnTheSamePosition": "Bai: guztiak posizio berean mantendu", + "studyNoLetPeopleBrowseFreely": "Ez: erabiltzaileei nahi dutena egiten utzi", + "studyPinnedStudyComment": "Azterlanaren iltzatutako iruzkina", + "studyStart": "Hasi", + "studySave": "Gorde", + "studyClearChat": "Txata garbitu", + "studyDeleteTheStudyChatHistory": "Azterlaneko txata ezabatu? Ez dago atzera egiterik!", + "studyDeleteStudy": "Azterlana ezabatu", + "studyConfirmDeleteStudy": "Azterlan osoa ezabatu? Ez dago atzera egiterik! Idatzi azterlanaren izena baieztapena emateko: {param}", + "studyWhereDoYouWantToStudyThat": "Non nahi duzu hori aztertu?", + "studyGoodMove": "Jokaldi ona", + "studyMistake": "Akatsa", + "studyBrilliantMove": "Jokaldi bikaina", + "studyBlunder": "Akats larria", + "studyInterestingMove": "Jokaldi interesgarria", + "studyDubiousMove": "Zalantzazko jokaldia", + "studyOnlyMove": "Jokaldi bakarra", + "studyZugzwang": "Zugzwang", + "studyEqualPosition": "Berdindutako posizioa", + "studyUnclearPosition": "Posizioa ez da argia", + "studyWhiteIsSlightlyBetter": "Zuria hobetoxeago", + "studyBlackIsSlightlyBetter": "Beltza hobetoxeago", + "studyWhiteIsBetter": "Zuria hobeto", + "studyBlackIsBetter": "Beltza hobeto", + "studyWhiteIsWinning": "Zuria irabazten ari da", + "studyBlackIsWinning": "Beltza irabazten ari da", + "studyNovelty": "Berritasuna", + "studyDevelopment": "Garapena", + "studyInitiative": "Iniziatiba", + "studyAttack": "Erasoa", + "studyCounterplay": "Kontraerasoa", + "studyTimeTrouble": "Denbora-arazoak", + "studyWithCompensation": "Konepntsazioarekin", + "studyWithTheIdea": "Ideiarekin", + "studyNextChapter": "Hurrengo kapitulua", + "studyPrevChapter": "Aurreko kapitulua", + "studyStudyActions": "Azterlanen akzioak", + "studyTopics": "Gaiak", + "studyMyTopics": "Nire gaiak", + "studyPopularTopics": "Gai arrakastatsuak", + "studyManageTopics": "Kudeatu gaiak", + "studyBack": "Atzera joan", + "studyPlayAgain": "Jokatu berriz", + "studyWhatWouldYouPlay": "Zer jokatuko zenuke posizio honetan?", + "studyYouCompletedThisLesson": "Zorionak! Ikasgai hau osatu duzu.", + "studyNbChapters": "{count, plural, =1{Kapitulu {count}} other{{count} kapitulu}}", + "studyNbGames": "{count, plural, =1{Partida {count}} other{{count} partida}}", + "studyNbMembers": "{count, plural, =1{Kide {count}} other{{count} kide}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Itsatsi hemen zure PGNa, gehienez partida {count}} other{Itsatsi hemen zure PGNa, gehienez {count} partida}}" } \ No newline at end of file diff --git a/lib/l10n/lila_fa.arb b/lib/l10n/lila_fa.arb index 535edad601..eca15d7121 100644 --- a/lib/l10n/lila_fa.arb +++ b/lib/l10n/lila_fa.arb @@ -4,19 +4,19 @@ "mobileToolsTab": "ابزارها", "mobileWatchTab": "تماشا", "mobileSettingsTab": "تنظیمات", - "mobileMustBeLoggedIn": "برای دیدن این صفحه باید وارد حساب‌تان شده باشید.", + "mobileMustBeLoggedIn": "برای دیدن این برگه باید وارد شده باشید.", "mobileSystemColors": "رنگ‌های دستگاه", - "mobileFeedbackButton": "بازخوراند", + "mobileFeedbackButton": "بازخورد", "mobileOkButton": "باشه", - "mobileSettingsHapticFeedback": "بازخوراند لمسی", - "mobileSettingsImmersiveMode": "حالت غوطه‌ور", - "mobileSettingsImmersiveModeSubtitle": "هنگام بازی، میانای کاربری دستگاه را پنهان کنید. اگر حرکت‌های ناوبری دستگاه در لبه‌های پرده آزارتان می‌دهد، از این استفاده کنید. برای پرده‌های بازی و معماباران (Puzzle Storm) کاربرد دارد.", - "mobileNotFollowingAnyUser": "شما هیچ کاربری را نمی‌دنبالید.", + "mobileSettingsHapticFeedback": "بازخورد لمسی", + "mobileSettingsImmersiveMode": "حالت فراگیر", + "mobileSettingsImmersiveModeSubtitle": "رابط کاربری را هنگام بازی پنهان کنید. اگر ناوبری لمسی در لبه‌های دستگاه اذیتتان می‌کند از این استفاده کنید. کارساز برای برگه‌های بازی و معماباران.", + "mobileNotFollowingAnyUser": "شما هیچ کاربری را دنبال نمی‌کنید.", "mobileAllGames": "همه بازی‌ها", - "mobileRecentSearches": "جستجوهای اخیر", + "mobileRecentSearches": "واپسین جستجوها", "mobileClearButton": "پاکسازی", - "mobilePlayersMatchingSearchTerm": "بازیکنانِ «{param}»", - "mobileNoSearchResults": "بدون نتیجه", + "mobilePlayersMatchingSearchTerm": "کاربران با پیوند «{param}»", + "mobileNoSearchResults": "بدون پیامد", "mobileAreYouSure": "مطمئنید؟", "mobilePuzzleStreakAbortWarning": "شما ریسه فعلی‌تان را خواهید باخت و امتیازتان ذخیره خواهد شد.", "mobilePuzzleStormNothingToShow": "چیزی برای نمایش نیست، چند دور معماباران بازی کنید.", @@ -26,22 +26,22 @@ "mobileSharePositionAsFEN": "همرسانی وضعیت، به شکل FEN", "mobileShowVariations": "باز کردن شاخه‌ها", "mobileHideVariation": "بستن شاخه‌ها", - "mobileShowComments": "نمایش نظرها", - "mobilePuzzleStormConfirmEndRun": "می‌خواهید این دور را پایان دهید؟", - "mobilePuzzleStormFilterNothingToShow": "چیزی برای نمایش نیست، لطفا پالاب‌گرها را تغییر دهید", + "mobileShowComments": "نمایش دیدگاه‌ها", + "mobilePuzzleStormConfirmEndRun": "می‌خواهید این دور را به پایان برسانید؟", + "mobilePuzzleStormFilterNothingToShow": "چیزی برای نمایش نیست، خواهشمندیم پالایه‌ها را دگرسان کنید.", "mobileCancelTakebackOffer": "رد درخواست برگرداندن", - "mobileCancelDrawOffer": "رد پیشنهاد تساوی", - "mobileWaitingForOpponentToJoin": "در انتظار آمدن حریف...", + "mobileWaitingForOpponentToJoin": "شکیبا برای پیوستن حریف...", "mobileBlindfoldMode": "چشم‌بسته", "mobileLiveStreamers": "بَرخَط-محتواسازان زنده", "mobileCustomGameJoinAGame": "به بازی بپیوندید", - "mobileCorrespondenceClearSavedMove": "پاکیدن حرکت ذخیره‌شده", + "mobileCorrespondenceClearSavedMove": "پاک کردن حرکت ذخیره شده", "mobileSomethingWentWrong": "مشکلی پیش آمد.", - "mobileShowResult": "نمایش نتیجه", - "mobilePuzzleThemesSubtitle": "معماهایی را از گشایش دلخواه‌تان بازی کنید، یا موضوعی را برگزینید.", - "mobilePuzzleStormSubtitle": "در ۳ دقیقه، هر چندتا معما که می‌توانید، حل کنید.", + "mobileShowResult": "نمایش پیامد", + "mobilePuzzleThemesSubtitle": "معماهایی را از گشایش دلخواه‌تان بازی کنید، یا جستاری را برگزینید.", + "mobilePuzzleStormSubtitle": "هر چند تا معما را که می‌توانید در ۳ دقیقه حل کنید.", "mobileGreeting": "درود، {param}", - "mobileGreetingWithoutName": "سلام", + "mobileGreetingWithoutName": "درود", + "mobilePrefMagnifyDraggedPiece": "بزرگ‌نمودن مهره‌ی کشیده", "activityActivity": "فعالیت", "activityHostedALiveStream": "میزبان پخش زنده بود", "activityRankedInSwissTournament": "رتبه #{param1} را در {param2} به دست آورد", @@ -50,12 +50,13 @@ "activityPracticedNbPositions": "{count, plural, =1{{count} وضعیت تمرین‌شده در {param2}} other{{count} وضعیت تمرین‌شده در {param2}}}", "activitySolvedNbPuzzles": "{count, plural, =1{{count} معمای آموزشی را حل کرد} other{{count} مساله تاکتیکی را حل کرد}}", "activityPlayedNbGames": "{count, plural, =1{{count} بازی {param2} را انجام داد} other{{count} بازی {param2} را انجام داد}}", - "activityPostedNbMessages": "{count, plural, =1{{count} پیام را در {param2} ارسال کرد} other{{count} پیام را در {param2} ارسال کرد}}", + "activityPostedNbMessages": "{count, plural, =1{{count} پیام را در {param2} فرستاد} other{{count} پیام را در {param2} فرستاد}}", "activityPlayedNbMoves": "{count, plural, =1{{count} حرکت انجام داد} other{{count} حرکت انجام داد}}", "activityInNbCorrespondenceGames": "{count, plural, =1{در {count} بازی مکاتبه‌ای} other{در {count} بازی مکاتبه‌ای}}", "activityCompletedNbGames": "{count, plural, =1{{count} بازی مکاتبه‌ای را به پایان رساند} other{{count} بازی مکاتبه‌ای را به پایان رساند}}", - "activityFollowedNbPlayers": "{count, plural, =1{{count} بازیکن را دنبال کرد} other{{count} بازیکن را دنبال کرد}}", - "activityGainedNbFollowers": "{count, plural, =1{{count} دنبال کننده جدید به دست آورد} other{{count} دنبال کننده جدید به دست آورد}}", + "activityCompletedNbVariantGames": "{count, plural, =1{تکمیل {count} بازی مکاتبه‌ای {param2}} other{تکمیل {count} بازی مکاتبه‌ای {param2}}}", + "activityFollowedNbPlayers": "{count, plural, =1{{count} بازیکن را دنبال کرد} other{شروع به دنبالیدن {count} بازیکن کرد}}", + "activityGainedNbFollowers": "{count, plural, =1{{count} دنبال‌گر جدید به‌دست آورد} other{{count} دنبال‌گر جدید به‌دست آورد}}", "activityHostedNbSimuls": "{count, plural, =1{{count} مسابقه هم‌زمان برگزار کرد} other{{count} مسابقه هم‌زمان برگزار کرد}}", "activityJoinedNbSimuls": "{count, plural, =1{در {count} مسابقه هم‌زمان شرکت کرد} other{در {count} مسابقه هم‌زمان شرکت کرد}}", "activityCreatedNbStudies": "{count, plural, =1{{count} درس جدید ساخت} other{{count} درس جدید ساخت}}", @@ -64,7 +65,57 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{در {count} مسابقه سوئیسی رقابت کرد} other{در {count} مسابقه سوئیسی رقابت کرد}}", "activityJoinedNbTeams": "{count, plural, =1{به {count} تیم پیوست} other{به {count} تیم پیوست}}", "broadcastBroadcasts": "پخش همگانی", + "broadcastMyBroadcasts": "پخش همگانی من", "broadcastLiveBroadcasts": "پخش زنده مسابقات", + "broadcastBroadcastCalendar": "تقویم پخش", + "broadcastNewBroadcast": "پخش زنده جدید", + "broadcastSubscribedBroadcasts": "پخش‌های دنبال‌شده", + "broadcastAboutBroadcasts": "درباره پخش‌های همگانی", + "broadcastHowToUseLichessBroadcasts": "نحوه استفاده از پخش همگانی Lichess.", + "broadcastTheNewRoundHelp": "دور جدید، همان اعضا و مشارکت‌کنندگان دور قبلی را خواهد داشت.", + "broadcastAddRound": "اضافه کردن یک دور", + "broadcastOngoing": "ادامه‌دار", + "broadcastUpcoming": "آینده", + "broadcastCompleted": "کامل‌شده", + "broadcastCompletedHelp": "Lichess تکمیل دور را بر اساس بازی‌های منبع تشخیص می‌دهد. اگر منبعی وجود ندارد، از این کلید استفاده کنید.", + "broadcastRoundName": "نام دور", + "broadcastRoundNumber": "شماره دور", + "broadcastTournamentName": "نام مسابقات", + "broadcastTournamentDescription": "توضیحات کوتاه مسابقات", + "broadcastFullDescription": "توضیحات کامل مسابقات", + "broadcastFullDescriptionHelp": "توضیحات بلند و اختیاری پخش همگانی. {param1} قابل‌استفاده است. طول متن باید کمتر از {param2} نویسه باشد.", + "broadcastSourceSingleUrl": "وب‌نشانیِ PGN", + "broadcastSourceUrlHelp": "وب‌نشانی‌ای که Lichess برای دریافت به‌روزرسانی‌های PGN می‌بررسد. آن باید از راه اینترنت در دسترس همگان باشد.", + "broadcastSourceGameIds": "تا ۶۴ شناسه بازی لیچس٬ جداشده با فاصله.", + "broadcastStartDateTimeZone": "تاریخ آغاز در زمان-یانه محلی مسابقات: {param}", + "broadcastStartDateHelp": "اختیاری است، اگر می‌دانید چه زمانی رویداد شروع می‌شود", + "broadcastCurrentGameUrl": "نشانی بازی کنونی", + "broadcastDownloadAllRounds": "بارگیری همه دورها", + "broadcastResetRound": "ازنوکردن این دور", + "broadcastDeleteRound": "حذف این دور", + "broadcastDefinitivelyDeleteRound": "این دور و همه بازی‌هایش را به طور کامل حذف کن.", + "broadcastDeleteAllGamesOfThisRound": "همه بازی‌های این دور را حذف کن. منبع باید فعال باشد تا بتوان آنها را بازساخت.", + "broadcastEditRoundStudy": "ویرایش مطالعه دور", + "broadcastDeleteTournament": "حذف این مسابقات", + "broadcastDefinitivelyDeleteTournament": "کل مسابقات، شامل همه دورها و بازی‌هایش را به طور کامل حذف کن.", + "broadcastShowScores": "نمایش امتیاز بازیکنان بر پایه نتیجه بازی‌ها", + "broadcastReplacePlayerTags": "اختیاری: عوض کردن نام، درجه‌بندی و عنوان بازیکنان", + "broadcastFideFederations": "کشورگان‌های فیده", + "broadcastTop10Rating": "ده درجه‌بندی برتر", + "broadcastFidePlayers": "بازیکنان فیده", + "broadcastFidePlayerNotFound": "بازیکن فیده پیدا نشد", + "broadcastFideProfile": "رُخ‌نمای فیده", + "broadcastFederation": "کشورگان", + "broadcastAgeThisYear": "سنِ امسال", + "broadcastUnrated": "بی‌درجه‌بندی", + "broadcastRecentTournaments": "مسابقاتِ اخیر", + "broadcastTeams": "تیم‌ها", + "broadcastStartVerySoon": "پخش زنده به زودی آغاز خواهد شد.", + "broadcastNotYetStarted": "پخش زنده هنوز آغاز نشده است.", + "broadcastOfficialWebsite": "تارنمای رسمی", + "broadcastRatingDiff": "ناسانی امتیازات", + "broadcastScore": "امتیاز", + "broadcastNbBroadcasts": "{count, plural, =1{{count} پخش همگانی} other{{count} پخش همگانی}}", "challengeChallengesX": "پیشنهاد بازی: {param1}", "challengeChallengeToPlay": "پیشنهاد بازی دادن", "challengeChallengeDeclined": "پیشنهاد بازی رد شد.", @@ -93,7 +144,7 @@ "patronDonate": "کمک مالی", "patronLichessPatron": "یاورِ Lichess", "perfStatPerfStats": "وضعیت {param}", - "perfStatViewTheGames": "بازی ها را تماشا کنید", + "perfStatViewTheGames": "دیدن بازی‌ها", "perfStatProvisional": "موقت", "perfStatNotEnoughRatedGames": "بازی های رسمی کافی برای تعیین کردن یک درجه‌بندی قابل‌اتکا انجام نشده است.", "perfStatProgressOverLastXGames": "پیشرفت در آخرین {param} بازی ها:", @@ -173,11 +224,11 @@ "preferencesSnapArrowsToValidMoves": "چسبیدن پیکان‌ها به حرکت‌های ممکن", "preferencesSayGgWpAfterLosingOrDrawing": "گفتن \"بازی خوبی بود، خوب بازی کردی\" در هنگام باخت یا تساوی", "preferencesYourPreferencesHaveBeenSaved": "تغییرات شما ذخیره شده است", - "preferencesScrollOnTheBoardToReplayMoves": "اسکرول کردن روی صفحه برای مشاهده مجدد حرکت‌ها", + "preferencesScrollOnTheBoardToReplayMoves": "برای بازپخش حرکت‌ها، روی صفحه بازی بِنَوَردید", "preferencesCorrespondenceEmailNotification": "ایمیل های روزانه که بازی های شبیه شما را به صورت لیست درمی‌آورند", "preferencesNotifyStreamStart": "استریمر شروع به فعالیت کرد", "preferencesNotifyInboxMsg": "پیام جدید", - "preferencesNotifyForumMention": "در کامنتی نام شما ذکر شده است", + "preferencesNotifyForumMention": "در انجمن از شما نام‌بُرده‌اند", "preferencesNotifyInvitedStudy": "دعوت به مطالعه", "preferencesNotifyGameEvent": "اعلان به روزرسانی بازی", "preferencesNotifyChallenge": "پیشنهاد بازی", @@ -189,7 +240,7 @@ "preferencesNotifyDevice": "دستگاه", "preferencesBellNotificationSound": "زنگ اعلان", "puzzlePuzzles": "معماها", - "puzzlePuzzleThemes": "موضوع معماها", + "puzzlePuzzleThemes": "موضوع معما", "puzzleRecommended": "توصیه شده", "puzzlePhases": "مرحله‌ها", "puzzleMotifs": "موضوعات", @@ -222,7 +273,7 @@ "puzzleUseFindInPage": "از گزینه «جستجو در صفحه» مرورگر استفاده کنید تا گشایش دلخواه‌تان را بیابید!", "puzzleUseCtrlF": "از Ctrl+f برای یابیدن گشایش دلخواه‌تان استفاده کنید!", "puzzleNotTheMove": "این حرکت نیست!", - "puzzleTrySomethingElse": "چیز دیگری پیدا کنید", + "puzzleTrySomethingElse": "چیز دیگری بیابید.", "puzzleRatingX": "درجه‌بندی: {param}", "puzzleHidden": "پنهان", "puzzleFromGameLink": "برگرفته از بازی {param}", @@ -237,10 +288,10 @@ "puzzleAddAnotherTheme": "افزودن موضوعی دیگر", "puzzleNextPuzzle": "معمای بعدی", "puzzleJumpToNextPuzzleImmediately": "فوراً به معمای بعدی بروید", - "puzzlePuzzleDashboard": "پیشخوان معماها", + "puzzlePuzzleDashboard": "پیشخوان معما", "puzzleImprovementAreas": "نقاط ضعف", "puzzleStrengths": "نقاط قوت", - "puzzleHistory": "پیشینه معماها", + "puzzleHistory": "پیشینهٔ معما", "puzzleSolved": "حل شده", "puzzleFailed": "شکست!", "puzzleStreakDescription": "به تدریج معماهای سخت‌تری را حل کنید و یک دنباله بُرد بسازید. محدویت زمانی وجود ندارد، پس عجله نکنید. با یک حرکت اشتباه، بازی تمام می‌شود! در هر دور، می‌توانید یک حرکت را رَد کنید.", @@ -251,8 +302,8 @@ "puzzleFromMyGames": "از بازی های من", "puzzleLookupOfPlayer": "به دنبال معماهای برگرفته از بازی‌های یک بازیکن مشخص، بگردید", "puzzleFromXGames": "معماهای برگرفته از بازی‌های {param}", - "puzzleSearchPuzzles": "جستجوی معماها", - "puzzleFromMyGamesNone": "شما هیچ معمایی در پایگاه‌داده ندارید، اما Lichess همچنان شما را بسیار دوست دارد.\n\nبازی‌های سریع و مرسوم را انجام دهید تا بَختِتان را برای افزودن معمایی از خودتان بیفزایید!", + "puzzleSearchPuzzles": "جستجوی معما", + "puzzleFromMyGamesNone": "شما هیچ معمایی در دادگان ندارید، اما Lichess همچنان شما را بسیار دوست دارد.\n\nبازی‌های سریع و مرسوم را انجام دهید تا بخت‌تان را برای افزودن معمایی از خودتان بیفزایید!", "puzzleFromXGamesFound": "{param1} معما در بازی‌های {param2} یافت شد", "puzzlePuzzleDashboardDescription": "تمرین کن، تحلیل کن، پیشرفت کن", "puzzlePercentSolved": "{param} حل‌شده", @@ -293,7 +344,7 @@ "puzzleThemeDovetailMate": "مات بوسه ای", "puzzleThemeDovetailMateDescription": "وزیری که شاه مجاور خودش را که تنها دو خانه ای که برای فرارش باقی مانده توسط مهره های خودش سد شده، مات می کند.", "puzzleThemeEquality": "برابری", - "puzzleThemeEqualityDescription": "از وضعیت باخت در‌آیید و به وضعیت تساوی یا ترازمند برسید. (ارزیابی ≤ ۲۰۰ ص‌پ)", + "puzzleThemeEqualityDescription": "از وضعیت باخت در‌آیید و به وضعیت تساوی یا تعادل برسید. (ارزیابی ≤ ۲۰۰ ص‌پ)", "puzzleThemeKingsideAttack": "حمله به جناح شاه", "puzzleThemeKingsideAttackDescription": "حمله به شاه حریف، پس از آنکه آنها قلعه کوچک رفتند.", "puzzleThemeClearance": "آزادسازی", @@ -378,28 +429,28 @@ "puzzleThemeUnderPromotion": "فرو-ارتقا", "puzzleThemeUnderPromotionDescription": "ارتقا به اسب، فیل یا رخ.", "puzzleThemeVeryLong": "معمای خیلی طولانی", - "puzzleThemeVeryLongDescription": "چهار حرکت یا بیشتر برای برنده شدن.", + "puzzleThemeVeryLongDescription": "بُردن با چهار حرکت یا بیشتر.", "puzzleThemeXRayAttack": "حمله پیکانی", "puzzleThemeXRayAttackDescription": "یک مهره از طریق مهره حریف به یک خانه حمله میکند یا از آن دفاع می کند.", "puzzleThemeZugzwang": "زوگزوانگ", "puzzleThemeZugzwangDescription": "حریف در حرکت‌هایش محدود است و همه‌شان وضعیتش را بدتر می‌کند.", - "puzzleThemeHealthyMix": "ترکیب سالم", - "puzzleThemeHealthyMixDescription": "یک ذره از همه چیز. شما نمی دانید چه چیزی پیش روی شماست، بنابراین شما باید برای هر چیزی آماده باشید! دقیقا مثل بازی های واقعی.", + "puzzleThemeMix": "آمیزهٔ همگن", + "puzzleThemeMixDescription": "ذره‌ای از هر چیزی. شما نمی‌دانید چه چیزی پیش روی شماست، بنابراین برای هر چیزی آماده می‌مانید! درست مانند بازی‌های واقعی.", "puzzleThemePlayerGames": "بازی‌های بازیکن", "puzzleThemePlayerGamesDescription": "دنبال معماهای ایجادشده از بازی‌های خودتان یا بازی‌های سایر بازیکنان، بگردید.", "puzzleThemePuzzleDownloadInformation": "این معماها به صورت همگانی هستند و می‌توانید از {param} بارگیریدشان.", "searchSearch": "جستجو", "settingsSettings": "تنظیمات", "settingsCloseAccount": "بستن حساب", - "settingsManagedAccountCannotBeClosed": "اکانت شما مدیریت شده است و نمی تواند بسته شود.", + "settingsManagedAccountCannotBeClosed": "حساب‌تان مدیریت می‌شود و نمی‌توان آن را بست.", "settingsClosingIsDefinitive": "بعد از بستن حسابتان دیگر نمی توانید به آن دسترسی پیدا کنید. آیا مطمئن هستید؟", "settingsCantOpenSimilarAccount": "شما نمی توانید حساب جدیدی با این نام کاربری باز کنید، حتی اگر با دستگاه دیگری وارد شوید.", - "settingsChangedMindDoNotCloseAccount": "نظرم را عوض کردم اکانتم را نمی بندم", + "settingsChangedMindDoNotCloseAccount": "نظرم عوض شد، حسابم را نبند", "settingsCloseAccountExplanation": "آیا مطمئنید که می خواهید حساب خود را ببندید؟ بستن حساب یک تصمیم دائمی است. شما هرگز نمی توانید دوباره وارد حساب خود شوید.", "settingsThisAccountIsClosed": "این حساب بسته شده است", "playWithAFriend": "بازی با دوستان", "playWithTheMachine": "بازی با رایانه", - "toInviteSomeoneToPlayGiveThisUrl": "برای دعوت کردن حریف این لینک را برای او بفرستید", + "toInviteSomeoneToPlayGiveThisUrl": "برای دعوت کسی به بازی، این وب‌نشانی را دهید", "gameOver": "پایان بازی", "waitingForOpponent": "انتطار برای حریف", "orLetYourOpponentScanQrCode": "یا اجازه دهید حریف شما این QR کد را پویش کند", @@ -443,8 +494,8 @@ "blackResigned": "سیاه تسلیم شد", "whiteLeftTheGame": "سفید بازی را ترک کرد", "blackLeftTheGame": "سیاه بازی را ترک کرد", - "whiteDidntMove": "سفید تکان نخورد", - "blackDidntMove": "مشکی تکان نخورد", + "whiteDidntMove": "سفید بازی نکرد", + "blackDidntMove": "سیاه بازی نکرد", "requestAComputerAnalysis": "درخواست تحلیل رایانه‌ای", "computerAnalysis": "تحليل رایانه‌ای", "computerAnalysisAvailable": "تحلیل رایانه‌ای موجود است", @@ -454,7 +505,7 @@ "usingServerAnalysis": "با استفاده از کارسازِ تحلیل", "loadingEngine": "پردازشگر بارمی‌گذارد...", "calculatingMoves": "در حال محاسبه حرکات...", - "engineFailed": "خطا در بارگذاری پردازشگر شطرنج", + "engineFailed": "خطا در بارگذاری پردازشگر", "cloudAnalysis": "تحلیل ابری", "goDeeper": "بررسی عمیق‌تر", "showThreat": "نمایش تهدید", @@ -469,12 +520,12 @@ "copyVariationPgn": "کپی PGN این شاخه", "move": "حرکت", "variantLoss": "حرکت بازنده", - "variantWin": "حرکت برنده", + "variantWin": "بُردِ شطرنج‌گونه", "insufficientMaterial": "مُهره ناکافی برای مات", "pawnMove": "حرکت پیاده", "capture": "گرفتن مهره", "close": "بستن", - "winning": "حرکت پیروزی‌بخش", + "winning": "حرکت برنده", "losing": "حرکت بازنده", "drawn": "حرکت تساوی‌دهنده", "unknown": "ناشناخته", @@ -487,13 +538,13 @@ "dtzWithRounding": "DTZ50'' با گرد کردن، بر اساس تعداد حرکات نیمه تا زمان دستگیری یا حرکت پیاده بعدی", "noGameFound": "هیچ بازی یافت نشد", "maxDepthReached": "عمق به حداکثر رسیده!", - "maybeIncludeMoreGamesFromThePreferencesMenu": "ممکنه بازی‌های بیشتری با توجه به گزینگان ترجیح‌ها، وجود داشته باشد؟", + "maybeIncludeMoreGamesFromThePreferencesMenu": "شاید بازی‌های بیشتری با توجه به نام‌چین تنظیمات، وجود داشته باشد.", "openings": "گشایش‌ها", "openingExplorer": "پویشگر گشایش‌", "openingEndgameExplorer": "پویشگر گشایش/آخربازی", "xOpeningExplorer": "جستجوگر گشایش {param}", "playFirstOpeningEndgameExplorerMove": "نخستین حرکت گشایش/آخربازی پویشگر را برو", - "winPreventedBy50MoveRule": "قانون پنجاه حرکت از پیروزی جلوگیری کرد", + "winPreventedBy50MoveRule": "قانون پنجاه حرکت جلوی پیروزی را گرفت", "lossSavedBy50MoveRule": "قانون ۵۰ حرکت از شکست جلوگیری کرد", "winOr50MovesByPriorMistake": "برد یا ۵٠ حرکت بعد از اشتباه قبلی", "lossOr50MovesByPriorMistake": "باخت یا ۵٠ حرکت از اشتباه قبلی", @@ -504,7 +555,7 @@ "deleteThisImportedGame": "آیا این بازیِ درونبُرده پاک شود؟", "replayMode": "حالت بازپخش", "realtimeReplay": "مشابه بازی", - "byCPL": "درنگ حین اشتباهات", + "byCPL": "درنگ هنگام اشتباه", "openStudy": "گشودن مطالعه", "enable": "فعال سازی", "bestMoveArrow": "فلش نشان دهنده بهترین حرکت", @@ -515,7 +566,6 @@ "memory": "حافظه", "infiniteAnalysis": "آنالیز بی پایان", "removesTheDepthLimit": "محدودیت عمق را برمی‌دارد و رایانه‌تان داغ می‌ماند", - "engineManager": "مدیر موتور شطرنج", "blunder": "اشتباه فاحش", "mistake": "اشتباه", "inaccuracy": "بی دقتی", @@ -532,13 +582,13 @@ "logOut": "خروج", "signIn": "ورود", "rememberMe": "مرا به خاطر بسپار", - "youNeedAnAccountToDoThat": "شما برای انجام این کار به یک حساب کاربری نیاز دارید.", + "youNeedAnAccountToDoThat": "برای انجام آن به یک حساب نیازمندید", "signUp": "نام نویسی", - "computersAreNotAllowedToPlay": "كامپيوتر و بازيكناني كه از كامپيوتر كمك مي گيرند،مجاز به بازی نیستند.لطفا از انجین شطرنج يا دیتابیس شطرنج و يا بازيكنان ديگر كمک نگيريد. همچنین توجه کنید که داشتن چند حساب کاربری به شدت نهی شده است. استفاده فزاینده از چند حساب منجر به مسدود شدن حساب شما خواهد شد.", + "computersAreNotAllowedToPlay": "رایانه ها و بازیکنان رایانه-یاریده، مجاز به بازی نیستند. لطفا هنگام بازی از موتورهای شطرنج، دادگان‌ها یا دیگر بازیکنان کمک نگیرید. همچنین توجه کنید که ساخت چندین حساب به شدت ممنوع است و چند حسابی افزاینده، منجر به بستن‌تان می‌شود.", "games": "بازی ها", "forum": "انجمن", - "xPostedInForumY": "{param1} در انجمن،موضوع {param2} را پست کرد.", - "latestForumPosts": "آخرین پست های انجمن", + "xPostedInForumY": "{param1} در موضوع {param2}، پیامی نوشت", + "latestForumPosts": "آخرین فرسته‌های انجمن", "players": "بازیکنان", "friends": "دوستان", "otherPlayers": "بازیکنان دیگر", @@ -562,7 +612,7 @@ "changeUsernameNotSame": "تنها اندازه حروف میتوانند تغییر کنند. برای مثال \"johndoe\" به \"JohnDoe\".", "changeUsernameDescription": "نام کاربری خود را تغییر دهید. این تنها یک بار انجام پذیر است و شما تنها مجازید اندازه حروف نام کاربری‌تان را تغییر دهید.", "signupUsernameHint": "مطمئن شوید که یک نام کاربری مناسب انتخاب میکنید. بعداً نمی توانید آن را تغییر دهید و هر حسابی با نام کاربری نامناسب بسته می شود!", - "signupEmailHint": "ما فقط از آن برای تنظیم مجدد رمز عبور استفاده خواهیم کرد.", + "signupEmailHint": "ما فقط برای بازنشاندن گذرواژه، از آن استفاده خواهیم کرد.", "password": "رمز عبور", "changePassword": "تغییر کلمه عبور", "changeEmail": "تغییر ایمیل", @@ -578,7 +628,7 @@ "passwordSuggestion": "از رمز عبور پیشنهاد شده از شخص دیگر استفاده نکنید. در این صورت احتمال سرقت حساب شما وجود دارد.", "emailSuggestion": "از ایمیلی که از شخص دیگر به شما پیشنهاد داده است استفاده نکنید. در این صورت احتمال سرقت حساب شما وجود دارد.", "emailConfirmHelp": "کمک با تائید ایمیل", - "emailConfirmNotReceived": "آیا ایمیل تائید بعد از ثبت نام را دریافت نکرده اید؟", + "emailConfirmNotReceived": "آیا رایانامهٔ تاییدتان را پس از نام‌نویسی دریافت نکردید؟", "whatSignupUsername": "از چه نام کاربری برای ثبت نام استفاده کردید؟", "usernameNotFound": "ما هیچ کابری با این نام نیافتیم: {param}.", "usernameCanBeUsedForNewAccount": "شما می توانید از این نام کاربری برای ایجاد یک حساب کاربری جدید استفاده کنید", @@ -596,7 +646,8 @@ "accountRegisteredWithoutEmail": "حساب کاربری {param} بدون ایمیل ثبت نام شده بود.", "rank": "رتبه", "rankX": "رتبه:{param}", - "gamesPlayed": "تعداد بازی های انجام شده", + "gamesPlayed": "بازی‌های انجامیده", + "ok": "بسیار خب", "cancel": "لغو", "whiteTimeOut": "زمان سفید تمام شد", "blackTimeOut": "زمان سیاه تمام شد", @@ -637,7 +688,7 @@ "chatRoom": "گپ‌سرا", "loginToChat": "برای گپ زدن، وارد حساب‌تان شوید", "youHaveBeenTimedOut": "زمان شما به پایان رسید.", - "spectatorRoom": "اتاق تماشاچیان", + "spectatorRoom": "اتاق تماشاگران", "composeMessage": "نوشتن پیام", "subject": "عنوان", "send": "ارسال", @@ -654,11 +705,11 @@ "takebackPropositionAccepted": "پیشنهاد پس گرفتن حرکت پذیرفته شد", "takebackPropositionCanceled": "پیشنهاد پس گرفتن حرکت لغو شد", "yourOpponentProposesATakeback": "حریف پیشنهاد پس گرفتن حرکت می دهد", - "bookmarkThisGame": "نشان گذاری بازی", + "bookmarkThisGame": "نشانک‌گذاری", "tournament": "مسابقه", "tournaments": "مسابقات", "tournamentPoints": "مجموع امتیازات مسابقات", - "viewTournament": "مشاهده مسابقات", + "viewTournament": "دیدن مسابقات", "backToTournament": "برگشت به مسابقه", "noDrawBeforeSwissLimit": "شما نمی‌توانید در مسابقات سوییس تا قبل از حرکت ۳۰ام بازی را مساوی کنید.", "thematic": "موضوعی", @@ -671,7 +722,7 @@ "mustBeInTeam": "باید در تیم {param} باشید", "youAreNotInTeam": "شما در تیم {param} نیستید", "backToGame": "بازگشت به بازی", - "siteDescription": "کارساز برخط و رایگان شطرنج. با میانایی روان شطرنج بازی کنید. بدون ثبت‌نام، بدون تبلیغ، بدون نیاز به افزونه. با رایانه، دوستان یا حریفان تصادفی شطرنج بازی کنید.", + "siteDescription": "کارساز برخط و رایگان شطرنج. با میانایی روان، شطرنج بازی کنید. بدون نام‌نویسی، بدون تبلیغ، بدون نیاز به افزونه. با رایانه، دوستان یا حریفان تصادفی شطرنج بازی کنید.", "xJoinedTeamY": "{param1} به تیم {param2} پیوست", "xCreatedTeamY": "{param1} تیم {param2} را ایجاد کرد", "startedStreaming": "پخش را آغازید", @@ -702,19 +753,19 @@ "whiteCheckmatesInOneMove": "سفید در یک حرکت مات می‌کند", "blackCheckmatesInOneMove": "سیاه در یک حرکت مات می‌کند", "retry": "تلاش دوباره", - "reconnecting": "در حال اتصال دوباره", + "reconnecting": "در حال بازاتصال", "noNetwork": "بُرون‌خط", "favoriteOpponents": "رقبای مورد علاقه", - "follow": "دنبال کردن", - "following": "افرادی که دنبال می کنید", - "unfollow": "لغو دنبال کردن", - "followX": "دنبال کردن {param}", - "unfollowX": "لغو دنبال کردن {param}", + "follow": "دنبالیدن", + "following": "دنبالنده", + "unfollow": "وادنبالیدن", + "followX": "دنبالیدن {param}", + "unfollowX": "وادنبالیدن {param}", "block": "مسدود کن", "blocked": "مسدود شده", "unblock": "لغو انسداد", - "followsYou": "افرادی که شما را دنبال می کنند", - "xStartedFollowingY": "{param1} {param2} را فالو کرد", + "followsYou": "شما را می‌دنبالد", + "xStartedFollowingY": "{param1} دنبالیدن {param2} را آغازید", "more": "بیشتر", "memberSince": "عضویت از تاریخ", "lastSeenActive": "آخرین ورود {param}", @@ -752,7 +803,7 @@ "tournamentComplete": "مسابقات به پایان رسید", "movesPlayed": "حرکات انجام شده", "whiteWins": "پیروزی با مهره سفید", - "blackWins": "سیاه برنده شد", + "blackWins": "سیاه می‌برد", "drawRate": "نرخ تساوی", "draws": "مساوی", "nextXTournament": "مسابقه ی {param} بعدی:", @@ -789,14 +840,14 @@ "activePlayers": "بازیکنان فعال", "bewareTheGameIsRatedButHasNoClock": "مراقب باشید،این بازی رتبه بندی میشود اما بدون ساعت!", "success": "موفق شدید", - "automaticallyProceedToNextGameAfterMoving": "حرکت کردن اتوماتیک برای بازی بعدی بعد از حرکت کردن", + "automaticallyProceedToNextGameAfterMoving": "پس از حرکت، خودکار به بازی بعدی روید", "autoSwitch": "تعویض خودکار", - "puzzles": "معماها", + "puzzles": "معما", "onlineBots": "ربات‌های بَرخط", "name": "نام", "description": "شرح", "descPrivate": "توضیحات خصوصی", - "descPrivateHelp": "متنی که فقط اعضای تیم مشاهده خواهند کرد. در صورت تعیین، جایگزین توضیحات عمومی برای اعضای تیم خواهد شد.", + "descPrivateHelp": "متنی که فقط هم‌تیمی‌ها خواهند دید. در صورت تعیین، جایگزین وصف همگانی برای هم‌تیمی‌ها می‌شود خواهد شد.", "no": "نه", "yes": "بله", "website": "وبگاه", @@ -804,8 +855,8 @@ "help": "راهنما:", "createANewTopic": "ایجاد یک موضوع جدید", "topics": "مباحث", - "posts": "پست ها", - "lastPost": "آخرین ارسال", + "posts": "فرسته‌ها", + "lastPost": "آخرین فرسته", "views": "نمایش ها", "replies": "پاسخ ها", "replyToThisTopic": "پاسخ به این موضوع", @@ -818,19 +869,21 @@ "whatIsIheMatter": "موضوع", "cheat": "تقلب", "troll": "وِزُل", - "other": "موضوعات دیگر", - "reportDescriptionHelp": "لینک بازی های این کاربر را قرار دهید و توضیع دهید خطای رفتار این بازیکن چه بوده است", + "other": "دیگر", + "reportCheatBoostHelp": "پیوند بازی(ها) را جای‌گذارید و بشرحید که چه رفتاری از این کاربر مشکل دارد. فقط نگویید «آنها تقلب‌کارند»، بلکه به ما بگویید چطور به این نتیجه رسیده‌اید.", + "reportUsernameHelp": "بشرحید چه چیز این نام‌کاربری آزارنده است. فقط نگویید «آزارنده/نامناسب است»، بلکه به ما بگویید چطور به این نتیجه رسیده‌اید، به‌ویژه اگر توهین: گنگ است، انگلیسی نیست، کوچه‌بازاری است، یا یک ارجاع تاریخی/فرهنگی است.", + "reportProcessedFasterInEnglish": "اگر انگلیسی بنویسید، زودتر به گزارش‌تان رسیدگی خواهد شد.", "error_provideOneCheatedGameLink": "لطفآ حداقل یک نمونه تقلب در بازی را مطرح کنید.", - "by": "توسط {param}", + "by": "به‌دستِ {param}", "importedByX": "{param} آن را وارد کرده", "thisTopicIsNowClosed": "این موضوع بسته شده است", - "blog": "بلاگ", - "notes": "یادداشت ها", - "typePrivateNotesHere": "یادداشت خصوصی را اینجا وارد کنید", + "blog": "وبنوشت", + "notes": "یادداشت‌ها", + "typePrivateNotesHere": "یادداشت‌های خصوصی را اینجا بنویسید", "writeAPrivateNoteAboutThisUser": "یک یادداشت خصوصی درباره این کاربر بنویسید", - "noNoteYet": "تا الان، بدون یادداشت", + "noNoteYet": "تاکنون، بدون یادداشت", "invalidUsernameOrPassword": "نام کاربری یا رمز عبور نادرست است", - "incorrectPassword": "رمزعبور اشتباه", + "incorrectPassword": "گذرواژه‌ی نادرست", "invalidAuthenticationCode": "کد اصالت سنجی نامعتبر", "emailMeALink": "یک لینک به من ایمیل کنید", "currentPassword": "رمز جاری", @@ -842,7 +895,7 @@ "clockIncrement": "مقدار زمان اضافی به ازای هر حرکت", "privacy": "حریم شخصی", "privacyPolicy": "سیاست حریم شخصی", - "letOtherPlayersFollowYou": "بقیه بازیکنان شما را دنبال کنند", + "letOtherPlayersFollowYou": "اجازه دهید دیگر بازیکنان شما را بدنبالند", "letOtherPlayersChallengeYou": "اجازه دهید بازیکنان دیگر به شما پیشنهاد بازی دهند", "letOtherPlayersInviteYouToStudy": "بگذارید دیگر بازیکنان، شما را به مطالعه دعوت کنند", "sound": "صدا", @@ -863,7 +916,7 @@ "defeatVsYInZ": "{param1} vs {param2} in {param3}", "drawVsYInZ": "{param1} vs {param2} in {param3}", "timeline": "جدول زمانی", - "starting": "شروع", + "starting": "آغاز:", "allInformationIsPublicAndOptional": "تمامی اطلاعات عمومی و اختیاری است.", "biographyDescription": "درباره ی خودتان بگویید - به چه چیزی در شطرنج علاقه داریدو گشایش ها - بازی ها و بازیکنان مورد علاقه تان…", "listBlockedPlayers": "فهرست بازیکنانی که مسدود کرده‌اید", @@ -875,7 +928,7 @@ "learnMenu": "یادگیری", "studyMenu": "مطالعه‌ها", "practice": "تمرین کردن", - "community": "انجمن", + "community": "همدارگان", "tools": "ابزارها", "increment": "افزایش زمان", "error_unknown": "مقدار نامعتبر", @@ -892,14 +945,14 @@ "ifRegistered": "اگر نام‌نویسی‌کرده", "onlyExistingConversations": "تنها مکالمات موجود", "onlyFriends": "فقط دوستان", - "menu": "فهرست", + "menu": "نام‌چین", "castling": "قلعه‌روی", "whiteCastlingKingside": "O-O سفید", "blackCastlingKingside": "O-O سیاه", "tpTimeSpentPlaying": "زمان بازی کردن: {param}", - "watchGames": "تماشای بازی ها", + "watchGames": "تماشای بازی‌ها", "tpTimeSpentOnTV": "مدت گذرانده در تلویزیون: {param}", - "watch": "نگاه کردن", + "watch": "تماشا", "videoLibrary": "فیلم ها", "streamersMenu": "بَرخَط-محتواسازها", "mobileApp": "گوشی‌افزار", @@ -910,7 +963,7 @@ "really": "واقعاً", "contribute": "مشارکت", "termsOfService": "قوانین", - "sourceCode": "منبع کد لایچس", + "sourceCode": "کد منبع", "simultaneousExhibitions": "نمایش هم زمان", "host": "میزبان", "hostColorX": "رنگ میزبان: {param}", @@ -943,10 +996,10 @@ "keyMoveBackwardOrForward": "حرکت به عقب/جلو", "keyGoToStartOrEnd": "رفتن به آغاز/پایان", "keyCycleSelectedVariation": "چرخه شاخه اصلی انتخاب‌شده", - "keyShowOrHideComments": "نمایش/ پنهان کردن نظرات", + "keyShowOrHideComments": "نمایش/پنهان کردن نظرها", "keyEnterOrExitVariation": "ورود / خروج به شاخه", "keyRequestComputerAnalysis": "درخواست تحلیل رایانه‌ای، از اشتباه‌های‌تان بیاموزید", - "keyNextLearnFromYourMistakes": "بعدی (از اشتباهات خود درس بگیرید)", + "keyNextLearnFromYourMistakes": "بعدی (از اشتباه‌های‌تان بیاموزید)", "keyNextBlunder": "اشتباه فاحش بعدی", "keyNextMistake": "اشتباه بعدی", "keyNextInaccuracy": "بی‌دقتی بعدی", @@ -968,7 +1021,7 @@ "weeklyPerfTypeRatingDistribution": "توزیع درجه‌بندی {param} هفتگی", "yourPerfTypeRatingIsRating": "درجه‌بندی {param1} شما {param2} است.", "youAreBetterThanPercentOfPerfTypePlayers": "شما بهتر از {param1} بازیکن ها در {param2} هستید.", - "userIsBetterThanPercentOfPerfTypePlayers": "{param1} بهتر از {param2} از بازیکنان {param3} میباشد.", + "userIsBetterThanPercentOfPerfTypePlayers": "{param1} بهتر از {param2} بازیکنان {param3} است.", "betterThanPercentPlayers": "بهتر از {param1} بازیکنان در {param2}", "youDoNotHaveAnEstablishedPerfTypeRating": "شما درجه‌بندی {param} تثبیت‌شده‌ای ندارید.", "yourRating": "درجه‌بندی شما", @@ -986,8 +1039,8 @@ "downloadRaw": "بارگیری خام", "downloadImported": "بارگیری درونبُرد", "crosstable": "رودررو", - "youCanAlsoScrollOverTheBoardToMoveInTheGame": "شما می توانید برای حرکت در بازی از صفحه استفاده کنید", - "scrollOverComputerVariationsToPreviewThem": "برای مشاهده آن ها اسکرول کنید.", + "youCanAlsoScrollOverTheBoardToMoveInTheGame": "برای حرکت، روی صفحه بازی بِنَوَردید.", + "scrollOverComputerVariationsToPreviewThem": "برای پیش‌نمایش آن‌ها، روی شاخه‌های رایانه‌ای بِنَوَردید.", "analysisShapesHowTo": "و کلیک کنید یا راست کلیک کنید تا دایره یا فلش در صفحه بکشید shift", "letOtherPlayersMessageYou": "ارسال پیام توسط بقیه به شما", "receiveForumNotifications": "دریافت اعلان در هنگام ذکر شدن در انجمن", @@ -999,7 +1052,7 @@ "kidModeIsEnabled": "حالت کودک فعال است.", "kidModeExplanation": "این گزینه،امنیتی است.با فعال کردن حالت ((کودکانه))،همه ی ارتباطات(چت کردن و...)غیر فعال می شوند.با فعال کردن این گزینه،کودکان خود را محافطت کنید.", "inKidModeTheLichessLogoGetsIconX": "در حالت کودکانه،به نماد لیچس،یک {param} اضافه می شود تا شما از فعال بودن آن مطلع شوید.", - "askYourChessTeacherAboutLiftingKidMode": "اکانت شما مدیریت شده است. برای برداشتن حالت کودک از معلم شطرنج خود درخواست کنید.", + "askYourChessTeacherAboutLiftingKidMode": "حسابتان مدیریت می‌شود. از آموزگار شطرنج‌تان درباره برداشتن حالت کودک بپرسید.", "enableKidMode": "فعال کردن حالت کودکانه", "disableKidMode": "غیر فعال کردن حالت کودکانه", "security": "امنیت", @@ -1011,16 +1064,16 @@ "everybodyGetsAllFeaturesForFree": "همگی از مزایا بصورت رایگان استفاده می کنند", "zeroAdvertisement": "بدون تبلیغات", "fullFeatured": "با تمامی امکانات", - "phoneAndTablet": "موبایل و تبلت", + "phoneAndTablet": "گوشی و رایانک", "bulletBlitzClassical": "گلوله‌ای، برق‌آسا، مرسوم", "correspondenceChess": "شطرنج مکاتبه ای", "onlineAndOfflinePlay": "بازی بَرخط و بُرون‌خط", "viewTheSolution": "دیدن راه‌حل", - "followAndChallengeFriends": "دنبال کردن و پیشنهاد بازی دادن به دوستان", + "followAndChallengeFriends": "دنبالیدن و پیشنهاد بازی دادن به دوستان", "gameAnalysis": "تجزیه و تحلیلِ بازی", - "xHostsY": "{param1} میزبان ها {param2}", - "xJoinsY": "{param1} وارد می شود {param2}", - "xLikesY": "{param1} می پسندد {param2}", + "xHostsY": "{param1} میزبان {param2} است", + "xJoinsY": "{param1} به {param2} می‌پیوندد", + "xLikesY": "{param1}، {param2} را می‌پسندد", "quickPairing": "رویارویی سریع", "lobby": "سَرسَرا", "anonymous": "ناشناس", @@ -1045,8 +1098,8 @@ "usernameSuffixInvalid": "نام کاربری باید با حرف یا شماره خاتمه یابد.", "usernameCharsInvalid": "نام کاربری فقط می تواند شامل حروف،اعداد،خط فاصله یا زیر خط(under line) باشد.", "usernameUnacceptable": "این نام کاربری قابل قبول نیست.", - "playChessInStyle": "استایل شطرنج باز داشته باشید!", - "chessBasics": "اصول شطرنج", + "playChessInStyle": "شطرنج‌بازیِ نوگارانه", + "chessBasics": "پایه‌های شطرنج", "coaches": "مربی ها", "invalidPgn": "فایل PGN نامعتبر است", "invalidFen": "وضعیت نامعتبر", @@ -1062,13 +1115,13 @@ "drawByFiftyMoves": "بازی با قانون پنجاه حرکت مساوی شده است.", "theGameIsADraw": "بازی مساوی است.", "computerThinking": "محاسبه رایانه‌ای ...", - "seeBestMove": "مشاهده بهترین حرکت", + "seeBestMove": "دیدن بهترین حرکت", "hideBestMove": "پنهان کردن بهترین حرکت", "getAHint": "راهنمایی", "evaluatingYourMove": "در حال بررسی حرکت شما...", - "whiteWinsGame": "سفید برنده شد", - "blackWinsGame": "سیاه برنده شد", - "learnFromYourMistakes": "از اشتباهات خود درس بگیرید", + "whiteWinsGame": "سفید می‌برد", + "blackWinsGame": "سیاه می‌برد", + "learnFromYourMistakes": "از اشتباه‌های‌تان بیاموزید", "learnFromThisMistake": "از این اشتباه درس بگیرید", "skipThisMove": "رد کردن این حرکت", "next": "بعدی", @@ -1077,12 +1130,12 @@ "findBetterMoveForBlack": "حرکت بهتری برای سیاه بیابید", "resumeLearning": "ادامه یادگیری", "youCanDoBetter": "می‌توانید بهتر انجامش دهید", - "tryAnotherMoveForWhite": "برای سفید،حرکت دیگری را امتحان کنید", - "tryAnotherMoveForBlack": "برای سیاه،حرکت دیگری را امتحان کنید", + "tryAnotherMoveForWhite": "حرکت دیگری را برای سفید بیابید", + "tryAnotherMoveForBlack": "حرکت دیگری را برای سیاه بیابید", "solution": "راه‌حل", - "waitingForAnalysis": "در انتظار برای آنالیز", - "noMistakesFoundForWhite": "هیچ اشتباهی برای سفید مشاهده نشد", - "noMistakesFoundForBlack": "هیچ اشتباهی برای سیاه مشاهده نشد", + "waitingForAnalysis": "در انتظار تحلیل", + "noMistakesFoundForWhite": "هیچی اشتباهی از سفید یافت نشد", + "noMistakesFoundForBlack": "هیچی اشتباهی از سیاه یافت نشد", "doneReviewingWhiteMistakes": "اشتباهات سفید بررسی شد", "doneReviewingBlackMistakes": "اشتباهات سیاه بررسی شد.", "doItAgain": "دوباره", @@ -1105,7 +1158,7 @@ "pleasantChessExperience": "هدف ما مهیا ساختن تجربه لذت بخش شطرنج به همه افراد است.", "goodPractice": "به همین منظور، ما باید اطمینان حاصل کنیم که تمام بازیکنان تمرین خوب را دنبال میکنند.", "potentialProblem": "زمانی که مشکلی احتمالی شناسایی شد ، این پیام را نمایش می دهیم.", - "howToAvoidThis": "چگونه از این امر جلوگیری کنیم؟", + "howToAvoidThis": "چگونه از آن بپرهیزیم؟", "playEveryGame": "هر بازی‌ای که آغازیدید را، بازی کنید.", "tryToWin": "در هر بازی برای پیروزی (یا حداقل تساوی) تلاش کنید.", "resignLostGames": "بازی های از دست رفته را انصراف دهید(نگذارید زمان تمام شود).", @@ -1114,10 +1167,10 @@ "thankYouForReading": "از اینکه متن را خواندید متشکریم!", "lifetimeScore": "امتیاز کل", "currentMatchScore": "امتیاز بازی فعلی", - "agreementAssistance": "من تضمین میکنم که در حین بازی ها کمک نگیرم ( از انجین ، کتاب ، پایگاه داده یا شخصی دیگر)", - "agreementNice": "من تضمین میکنم که همیشه به بازیکن های دیگر احترام بگذارم.", - "agreementMultipleAccounts": "من موافقت می‌کنم که چندین اکانت برای خودم ایجاد نکنم(به جز دلایلی که در {param} اشاره شده).", - "agreementPolicy": "من تضمین میکنم که به تمام قوانین و خط مشی های لیچس پایبند باشم .", + "agreementAssistance": "من موافقم که در طول بازی‌هایم هیچگاه کمکی نخواهم گرفت (از یک رایانه شطرنج، کتاب، دادگان یا شخص دیگری).", + "agreementNice": "می‌پذیرم که همواره به بازیکنان دیگر احترام گزارم.", + "agreementMultipleAccounts": "موافقم که چندین حساب نخواهم ساخت (جز به دلیل‌های ذکر شده در {param}).", + "agreementPolicy": "با پیروی از همهٔ خط‌مشی‌های Lichess، موافقم.", "searchOrStartNewDiscussion": "جستجو یا شروع کردن مکالمه جدید", "edit": "ویرایش", "bullet": "گلوله‌ای", @@ -1141,22 +1194,22 @@ "makeSureToRead": "حتما {param1} را مطالعه کنید", "theForumEtiquette": "آداب انجمن", "thisTopicIsArchived": "این موضوع بایگانی شده است و دیگر نمی توان به آن پاسخ داد.", - "joinTheTeamXToPost": "به {param1} ملحق شوید تا در این انجمن پست بگذارید", + "joinTheTeamXToPost": "برای فرسته گذاشتن در این انجمن، به {param1} بپیوندید", "teamNamedX": "تیم {param1}", - "youCannotPostYetPlaySomeGames": "شما هنوز قادر به پست گذاشتن در انجمن نیستید. چند بازی انجام دهید!", + "youCannotPostYetPlaySomeGames": "هنوز نمی‌توانید در انجمن‌ها فرسته گذارید. چند بازی کنید!", "subscribe": "مشترک شدن", "unsubscribe": "لغو اشتراک", "mentionedYouInX": "از شما در {param1} نام برده شد.", - "xMentionedYouInY": "{param1} اسم شما را در \"{param2}\" ذکر کرده است.", + "xMentionedYouInY": "{param1} از شما در \"{param2}\" نام برد.", "invitedYouToX": "به «{param1}» دعوت شده‌اید.", "xInvitedYouToY": "{param1} شما را به «{param2}» دعوت کرده است.", "youAreNowPartOfTeam": "شما در حال حاضر عضوی از تیم هستید.", "youHaveJoinedTeamX": "شما به \"{param1}\" پیوستید.", "someoneYouReportedWasBanned": "شخصی که گزارش کردید مسدود شد", - "congratsYouWon": "تبریک، شما برنده شدید!", + "congratsYouWon": "شادباش، شما بُردید!", "gameVsX": "بازی در برابر {param1}", "resVsX": "{param1} در برابر {param2}", - "lostAgainstTOSViolator": "شما در برابر کسی که قانون‌های Lichess را نقض کرده، امتیاز درجه‌بندی از دست دادید", + "lostAgainstTOSViolator": "شما برابر کسی که قانون‌های Lichess را نقض کرده، امتیاز درجه‌بندی از دست دادید", "refundXpointsTimeControlY": "پس‌دادن: {param1} امتیاز به درجه‌بندی {param2}.", "timeAlmostUp": "زمان تقریباً تمام شده است!", "clickToRevealEmailAddress": "[برای آشکارسازی نشانی رایانامه بتلیکید]", @@ -1217,15 +1270,16 @@ "showMeEverything": "همه چیز را به من نشان بده", "lichessPatronInfo": "لایچس یک خیریه و کاملا رایگان و نرم افزاری متن باز است. تمام هزینه های اجرا، توسعه و محتوا تنها بر پایه هدایای کاربران بنا شده است.", "nothingToSeeHere": "فعلا هیچی اینجا نیست.", + "stats": "آمار", "opponentLeftCounter": "{count, plural, =1{رقیب شما بازی را ترک کرده است. شما میتوانید بعد از {count} ثانیه اعلام پیروزی کنید.} other{رقیب شما بازی را ترک کرده است. شما میتوانید بعد از {count} ثانیه اعلام پیروزی کنید.}}", "mateInXHalfMoves": "{count, plural, =1{در {count} نیم‌حرکت مات می‌شود} other{در {count} نیم‌حرکت مات می‌شود}}", "nbBlunders": "{count, plural, =1{{count} اشتباه بزرگ} other{{count} اشتباه بزرگ}}", "nbMistakes": "{count, plural, =1{{count} اشتباه} other{{count} اشتباه}}", - "nbInaccuracies": "{count, plural, =1{{count} غیردقیق} other{{count} غیردقیق}}", + "nbInaccuracies": "{count, plural, =1{{count} نادقیق} other{{count} نادقیق}}", "nbPlayers": "{count, plural, =1{{count} بازیکن} other{{count} بازیکن}}", "nbGames": "{count, plural, =1{{count} بازی} other{{count} بازی}}", "ratingXOverYGames": "{count, plural, =1{درجه‌بندی {count} در {param2} بازی} other{{count} ریتینگ در {param2} بازی}}", - "nbBookmarks": "{count, plural, =1{{count} بازی مورد علاقه} other{{count} بازی مورد علاقه}}", + "nbBookmarks": "{count, plural, =1{{count} نشانک} other{{count} نشانک}}", "nbDays": "{count, plural, =1{{count} روز} other{{count} روز}}", "nbHours": "{count, plural, =1{{count} ساعت} other{{count} ساعت}}", "nbMinutes": "{count, plural, =1{{count} دقیقه} other{{count} دقیقه}}", @@ -1247,13 +1301,13 @@ "needNbMoreGames": "{count, plural, =1{شما باید{count} بازی رسمی دیگر انجام دهید.} other{شما باید{count} بازی رسمی دیگر انجام دهید.}}", "nbImportedGames": "{count, plural, =1{{count} بارگذاری شده} other{{count} بارگذاری شده}}", "nbFriendsOnline": "{count, plural, =1{{count} دوست بَرخط} other{{count} دوست بَرخط}}", - "nbFollowers": "{count, plural, =1{{count} دنبال کننده‌} other{{count} دنبال کننده‌}}", - "nbFollowing": "{count, plural, =1{{count} دنبال می کند} other{{count} دنبال میکند}}", + "nbFollowers": "{count, plural, =1{{count} دنبال‌گر} other{{count} دنبال‌گر}}", + "nbFollowing": "{count, plural, =1{{count} دنبالنده} other{{count} دنبالنده}}", "lessThanNbMinutes": "{count, plural, =1{کمتر از {count} دقیقه} other{کمتر از {count} دقیقه}}", "nbGamesInPlay": "{count, plural, =1{{count} بازی در حال انجام است} other{{count} بازی در حال انجام است}}", "maximumNbCharacters": "{count, plural, =1{حداکثر: {count} حرف} other{حداکثر: {count} حرف}}", "blocks": "{count, plural, =1{{count} مسدود شده} other{{count} مسدود شده}}", - "nbForumPosts": "{count, plural, =1{{count} وبنوشته در انجمن} other{{count} وبنوشته در انجمن}}", + "nbForumPosts": "{count, plural, =1{{count} فرسته در انجمن} other{{count} فرسته در انجمن}}", "nbPerfTypePlayersThisWeek": "{count, plural, =1{{count} بازیکن {param2} این هفته فعالیت داشته‌ است.} other{{count} بازیکن {param2} این هفته فعالیت داشته‌اند.}}", "availableInNbLanguages": "{count, plural, =1{در {count} زبان موجود است!} other{در {count} زبان موجود است!}}", "nbSecondsToPlayTheFirstMove": "{count, plural, =1{{count} ثانیه برای شروع اولین حرکت} other{{count} ثانیه برای شروع اولین حرکت}}", @@ -1281,7 +1335,7 @@ "stormNewRun": "دور جدید (میانبر: Space)", "stormEndRun": "پایان‌دهی دور (میانبر: Enter)", "stormHighscores": "بالاترین امتیازها", - "stormViewBestRuns": "مشاهده بهترین دورها", + "stormViewBestRuns": "دیدن بهترین دورها", "stormBestRunOfDay": "بهترین دور روز", "stormRuns": "دورها", "stormGetReady": "آماده شوید!", @@ -1313,6 +1367,159 @@ "stormXRuns": "{count, plural, =1{یک دور} other{{count} دور}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{یک دور از {param2} بازی شد} other{{count} دور از {param2} بازی شد}}", "streamerLichessStreamers": "بَرخَط-محتواسازان Lichess", + "studyPrivate": "خصوصی", + "studyMyStudies": "مطالعه‌های من", + "studyStudiesIContributeTo": "مطالعه‌هایی که در آن شرکت دارم", + "studyMyPublicStudies": "مطالعه‌های همگانی من", + "studyMyPrivateStudies": "مطالعه‌های خصوصی من", + "studyMyFavoriteStudies": "مطالعه‌های دلخواه من", + "studyWhatAreStudies": "مطالعه‌ها چه هستند؟", + "studyAllStudies": "همه مطالعه‌ها", + "studyStudiesCreatedByX": "مطالعه‌هایی که {param} ساخته است", + "studyNoneYet": "هنوز، هیچ.", + "studyHot": "رواجیده", + "studyDateAddedNewest": "تاریخ افزوده شدن (نوترین)", + "studyDateAddedOldest": "تاریخ افزوده شدن (کهنه‌ترین)", + "studyRecentlyUpdated": "تازگی به‌روزشده", + "studyMostPopular": "محبوب‌ترین‌", + "studyAlphabetical": "براساس حروف الفبا", + "studyAddNewChapter": "افزودن بخش جدید", + "studyAddMembers": "افزودن اعضا", + "studyInviteToTheStudy": "دعوت به این مطالعه", + "studyPleaseOnlyInvitePeopleYouKnow": "لطفا تنها کسانی را دعوت کنید که شما را می‌شناسند و کنشگرانه می‌خواهند به این مطالعه بپیوندند.", + "studySearchByUsername": "جستجو بر اساس نام کاربری", + "studySpectator": "تماشاگر", + "studyContributor": "مشارکت کننده", + "studyKick": "اخراج", + "studyLeaveTheStudy": "ترک مطالعه", + "studyYouAreNowAContributor": "شما یک مشارکت کننده جدید هستید", + "studyYouAreNowASpectator": "شما اکنون یک تماشاگرید", + "studyPgnTags": "نشان های PGN", + "studyLike": "پسندیدن", + "studyUnlike": "نمی‌پسندم", + "studyNewTag": "برچسب جدید", + "studyCommentThisPosition": "یادداشت‌نویسی برای این وضعیت", + "studyCommentThisMove": "یادداشت‌نویسی برای این حرکت", + "studyAnnotateWithGlyphs": "حرکت‌نویسی به‌همراه علامت‌ها", + "studyTheChapterIsTooShortToBeAnalysed": "این بخش برای تحلیل، بسیار کوتاه است.", + "studyOnlyContributorsCanRequestAnalysis": "تنها مشارکت‌گران این مطالعه، می‌توانند درخواست تحلیل رایانه‌ای دهند.", + "studyGetAFullComputerAnalysis": "یک تحلیل کامل رایانه‌ای کارساز-سو از شاخه اصلی بگیرید.", + "studyMakeSureTheChapterIsComplete": "مطمئن شوید که بخش کامل است. شما فقط یک بار می‌توانید درخواست تحلیل دهید.", + "studyAllSyncMembersRemainOnTheSamePosition": "همه‌ی عضوهای همگام در وضعیت یکسانی باقی می‌مانند", + "studyShareChanges": "تغییرها را در کارساز ذخیره کنید و با تماشاگران به اشتراک گذارید", + "studyPlaying": "در حال انجام", + "studyShowEvalBar": "نوار ارزیابی", + "studyFirst": "اولین", + "studyPrevious": "پیشین", + "studyNext": "بعدی", + "studyLast": "آخرین", "studyShareAndExport": "همرسانی و برون‏بُرد", - "studyStart": "آغاز" + "studyCloneStudy": "همسانیدن", + "studyStudyPgn": "PGN مطالعه", + "studyDownloadAllGames": "بارگیری تمام بازی ها", + "studyChapterPgn": "PGN ِ بخش", + "studyCopyChapterPgn": "رونوشت‌گیری PGN", + "studyDownloadGame": "بارگیری بازی", + "studyStudyUrl": "وب‌نشانی مطالعه", + "studyCurrentChapterUrl": "وب‌نشانی بخش جاری", + "studyYouCanPasteThisInTheForumToEmbed": "می‌توانید این را در انجمن یا وبنوشت Lichessتان برای جاسازی قرار دهید", + "studyStartAtInitialPosition": "در وضعیت نخستین بیاغازید", + "studyStartAtX": "آغاز از {param}", + "studyEmbedInYourWebsite": "در وبگاهتان قرار دهید", + "studyReadMoreAboutEmbedding": "درباره قرار دادن (در سایت) بیشتر بخوانید", + "studyOnlyPublicStudiesCanBeEmbedded": "فقط مطالعه‌های همگانی می‌توانند جایگذاری شوند!", + "studyOpen": "بگشایید", + "studyXBroughtToYouByY": "{param1}، به دست {param2} برای شما آورده شده است", + "studyStudyNotFound": "مطالعه یافت نشد", + "studyEditChapter": "ویرایش بخش", + "studyNewChapter": "بخش نو", + "studyImportFromChapterX": "درونبُرد از {param}", + "studyOrientation": "جهت", + "studyAnalysisMode": "حالت تجزیه تحلیل", + "studyPinnedChapterComment": "یادداشت سنجاقیده‌ٔ بخش", + "studySaveChapter": "ذخیره بخش", + "studyClearAnnotations": "پاک کردن حرکت‌نویسی", + "studyClearVariations": "پاک کردن تغییرات", + "studyDeleteChapter": "حذف بخش", + "studyDeleteThisChapter": "حذف این بخش. بازگشت وجود ندارد!", + "studyClearAllCommentsInThisChapter": "همه دیدگاه‌ها، نمادها و شکل‌های ترسیم شده در این بخش، پاک شوند", + "studyRightUnderTheBoard": "درست زیر صفحهٔ بازی", + "studyNoPinnedComment": "هیچ", + "studyNormalAnalysis": "تحلیل ساده", + "studyHideNextMoves": "پنهان کردن حرکت بعدی", + "studyInteractiveLesson": "درس میان‌کنشی", + "studyChapterX": "بخش {param}", + "studyEmpty": "خالی", + "studyStartFromInitialPosition": "از وضعیت نخستین بیاغازید", + "studyEditor": "ویرایشگر", + "studyStartFromCustomPosition": "از وضعیت دلخواه بیاغازید", + "studyLoadAGameByUrl": "بارگذاری بازی از وب‌نشانی‌ها", + "studyLoadAPositionFromFen": "بار کردن وضعیت از FEN", + "studyLoadAGameFromPgn": "باگذاری بازی با استفاده از فایل PGN", + "studyAutomatic": "خودکار", + "studyUrlOfTheGame": "وب‌نشانی بازی‌ها، یکی در هر خط", + "studyLoadAGameFromXOrY": "بازی‌ها را از {param1} یا {param2} بارگذاری نمایید", + "studyCreateChapter": "ساخت بخش", + "studyCreateStudy": "ساخت مطالعه", + "studyEditStudy": "ویرایش مطالعه", + "studyVisibility": "دیدگی", + "studyPublic": "همگانی", + "studyUnlisted": "فهرست‌نشده", + "studyInviteOnly": "فقط توسط دعوتنامه", + "studyAllowCloning": "اجازه همسانِش", + "studyNobody": "هیچ کس", + "studyOnlyMe": "تنها من", + "studyContributors": "مشارکت‌کنندگان", + "studyMembers": "اعضا", + "studyEveryone": "همه", + "studyEnableSync": "فعال کردن همگام سازی", + "studyYesKeepEveryoneOnTheSamePosition": "بله: همه را در وضعیت یکسانی نگه دار", + "studyNoLetPeopleBrowseFreely": "خیر: به مردم اجازه جستجوی آزادانه بده", + "studyPinnedStudyComment": "یادداشت سنجاقیده به مطالعه", + "studyStart": "آغاز", + "studySave": "ذخیره", + "studyClearChat": "پاک کردن گفتگو", + "studyDeleteTheStudyChatHistory": "پیشینه گپِ مطالعه پاک شود؟ بازگشت وجود ندارد!", + "studyDeleteStudy": "پاکیدن مطالعه", + "studyConfirmDeleteStudy": "کل مطالعه پاک شود؟ بازگشت وجود ندارد! برای تایید، نام مطالعه را بنویسید: {param}", + "studyWhereDoYouWantToStudyThat": "کجا می‌خواهید آنرا مطالعه کنید؟", + "studyGoodMove": "حرکت خوب", + "studyMistake": "اشتباه", + "studyBrilliantMove": "حرکت درخشان", + "studyBlunder": "اشتباه فاحش", + "studyInterestingMove": "حرکت جالب", + "studyDubiousMove": "حرکت مشکوک", + "studyOnlyMove": "تک‌حرکت", + "studyZugzwang": "اکراهی", + "studyEqualPosition": "وضعیت برابر", + "studyUnclearPosition": "وضعیت ناروشن", + "studyWhiteIsSlightlyBetter": "سفید کمی بهتر است", + "studyBlackIsSlightlyBetter": "سیاه کمی بهتر است", + "studyWhiteIsBetter": "سفید بهتر است", + "studyBlackIsBetter": "سیاه بهتر است", + "studyWhiteIsWinning": "سفید می‌برد", + "studyBlackIsWinning": "سیاه می‌برد", + "studyNovelty": "روش و ایده‌ای نو در شروع بازی", + "studyDevelopment": "گسترش", + "studyInitiative": "ابتکار عمل", + "studyAttack": "حمله", + "studyCounterplay": "بازی‌متقابل", + "studyTimeTrouble": "تنگی زمان", + "studyWithCompensation": "دارای مزیت و برتری", + "studyWithTheIdea": "با طرح", + "studyNextChapter": "بخش بعدی", + "studyPrevChapter": "بخش پیشین", + "studyStudyActions": "عملگرهای مطالعه", + "studyTopics": "موضوع‌ها", + "studyMyTopics": "موضوع‌های من", + "studyPopularTopics": "موضوع‌های محبوب", + "studyManageTopics": "مدیریت موضوع‌ها", + "studyBack": "بازگشت", + "studyPlayAgain": "دوباره بازی کنید", + "studyWhatWouldYouPlay": "در این وضعیت چطور بازی می‌کنید؟", + "studyYouCompletedThisLesson": "تبریک! شما این درس را کامل کردید.", + "studyNbChapters": "{count, plural, =1{{count} بخش} other{{count} بخش}}", + "studyNbGames": "{count, plural, =1{{count} بازی} other{{count} بازی}}", + "studyNbMembers": "{count, plural, =1{{count} عضو} other{{count} عضو}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{متن PGN خود را در اینجا بچسبانید، تا {count} بازی} other{متن PGN خود را در اینجا بچسبانید، تا {count} بازی}}" } \ No newline at end of file diff --git a/lib/l10n/lila_fi.arb b/lib/l10n/lila_fi.arb index 4ddb845b0a..ad423bdeab 100644 --- a/lib/l10n/lila_fi.arb +++ b/lib/l10n/lila_fi.arb @@ -15,6 +15,7 @@ "mobileAllGames": "Kaikki pelit", "mobileRecentSearches": "Viimeisimmät haut", "mobileClearButton": "Tyhjennä", + "mobilePlayersMatchingSearchTerm": "Pelaajat, joiden tunnuksesta löytyy \"{param}\"", "mobileNoSearchResults": "Ei hakutuloksia", "mobileAreYouSure": "Oletko varma?", "mobilePuzzleStreakAbortWarning": "Parhaillaan menossa oleva putkesi päättyy, ja pistemääräsi tallennetaan.", @@ -29,7 +30,6 @@ "mobilePuzzleStormConfirmEndRun": "Haluatko lopettaa tämän sarjan?", "mobilePuzzleStormFilterNothingToShow": "Ei näytettävää, muuta suodatusehtoja", "mobileCancelTakebackOffer": "Peruuta siirron peruutuspyyntö", - "mobileCancelDrawOffer": "Peruuta tasapeliehdotus", "mobileWaitingForOpponentToJoin": "Odotetaan vastustajan löytymistä...", "mobileBlindfoldMode": "Sokko", "mobileCustomGameJoinAGame": "Liity peliin", @@ -40,6 +40,7 @@ "mobilePuzzleStormSubtitle": "Ratkaise mahdollisimman monta tehtävää 3 minuutissa.", "mobileGreeting": "Hei {param}", "mobileGreetingWithoutName": "Hei", + "mobilePrefMagnifyDraggedPiece": "Suurenna vedettävä nappula", "activityActivity": "Toiminta", "activityHostedALiveStream": "Piti livestreamin", "activityRankedInSwissTournament": "Tuli {param1}. sijalle turnauksessa {param2}", @@ -52,6 +53,7 @@ "activityPlayedNbMoves": "{count, plural, =1{Pelasi {count} siirtoa} other{Pelasi {count} siirtoa}}", "activityInNbCorrespondenceGames": "{count, plural, =1{{count} kirjeshakkipelissä} other{{count} kirjeshakkipelissä}}", "activityCompletedNbGames": "{count, plural, =1{Pelasi {count} kirjeshakkipelin} other{Pelasi {count} kirjeshakkipeliä}}", + "activityCompletedNbVariantGames": "{count, plural, =1{Pelasi {count} {param2}-kirjeshakkipelin} other{Pelasi {count} {param2}-kirjeshakkipeliä}}", "activityFollowedNbPlayers": "{count, plural, =1{Alkoi seuraamaan {count} pelaajaa} other{Alkoi seuraamaan {count} pelaajaa}}", "activityGainedNbFollowers": "{count, plural, =1{Sai {count} uuden seuraajan} other{Sai {count} uutta seuraajaa}}", "activityHostedNbSimuls": "{count, plural, =1{Piti {count} simultaanin} other{Piti {count} simultaania}}", @@ -62,7 +64,71 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{Osallistui {count} sveitsiläiseen turnaukseen} other{Osallistui {count} sveitsiläiseen turnaukseen}}", "activityJoinedNbTeams": "{count, plural, =1{Liittyi {count} joukkueeseen} other{Liittyi {count} joukkueeseen}}", "broadcastBroadcasts": "Lähetykset", + "broadcastMyBroadcasts": "Omat lähetykset", "broadcastLiveBroadcasts": "Suorat lähetykset turnauksista", + "broadcastBroadcastCalendar": "Lähetyskalenteri", + "broadcastNewBroadcast": "Uusi livelähetys", + "broadcastSubscribedBroadcasts": "Tilatut lähetykset", + "broadcastAboutBroadcasts": "Lähetyksistä", + "broadcastHowToUseLichessBroadcasts": "Kuinka Lichess-lähetyksiä käytetään.", + "broadcastTheNewRoundHelp": "Uudella kierroksella on samat jäsenet ja osallistujat kuin edellisellä.", + "broadcastAddRound": "Lisää kierros", + "broadcastOngoing": "Käynnissä", + "broadcastUpcoming": "Tulossa", + "broadcastCompleted": "Päättyneet", + "broadcastCompletedHelp": "Lichess tunnistaa lähteenä olevista peleistä, milloin kierros on viety päätökseen. Lähteen puuttuessa voit käyttää tätä asetusta.", + "broadcastRoundName": "Kierroksen nimi", + "broadcastRoundNumber": "Kierroksen numero", + "broadcastTournamentName": "Turnauksen nimi", + "broadcastTournamentDescription": "Turnauksen lyhyt kuvaus", + "broadcastFullDescription": "Täysimittainen kuvaus tapahtumasta", + "broadcastFullDescriptionHelp": "Ei-pakollinen pitkä kuvaus lähetyksestä. {param1}-muotoiluja voi käyttää. Pituus voi olla enintään {param2} merkkiä.", + "broadcastSourceSingleUrl": "PGN:n lähde-URL", + "broadcastSourceUrlHelp": "URL, josta Lichess hakee PGN-päivitykset. Sen täytyy olla julkisesti saatavilla internetissä.", + "broadcastSourceGameIds": "Korkeintaan 64 Lichess-pelin tunnistenumeroa välilyönneillä eroteltuna.", + "broadcastStartDateTimeZone": "Alkamisajankohta turnauksen paikallisella aikavyöhykkeellä: {param}", + "broadcastStartDateHelp": "Ei-pakollinen, laita jos tiedät milloin tapahtuma alkaa", + "broadcastCurrentGameUrl": "Tämän pelin URL", + "broadcastDownloadAllRounds": "Lataa kaikki kierrokset", + "broadcastResetRound": "Nollaa tämä kierros", + "broadcastDeleteRound": "Poista tämä kierros", + "broadcastDefinitivelyDeleteRound": "Poista kierros ja sen pelit lopullisesti.", + "broadcastDeleteAllGamesOfThisRound": "Poista kaikki tämän kierroksen pelit. Lähteen on oltava aktiivinen, jotta pelit voidaan luoda uudelleen.", + "broadcastEditRoundStudy": "Kierrostutkielman muokkaus", + "broadcastDeleteTournament": "Poista tämä turnaus", + "broadcastDefinitivelyDeleteTournament": "Poista lopullisesti koko turnaus, sen kaikki kierrokset ja kaikki pelit.", + "broadcastShowScores": "Näytä pelaajien pisteet pelien tulosten pohjalta", + "broadcastReplacePlayerTags": "Valinnainen: korvaa pelaajien nimet, vahvuusluvut ja arvonimet", + "broadcastFideFederations": "FIDEn liitot", + "broadcastTop10Rating": "Top 10 -vahvuuslukulista", + "broadcastFidePlayers": "FIDE-pelaajat", + "broadcastFidePlayerNotFound": "FIDE-pelaajaa ei löytynyt", + "broadcastFideProfile": "FIDE-profiili", + "broadcastFederation": "Kansallinen liitto", + "broadcastAgeThisYear": "Ikä tänä vuonna", + "broadcastUnrated": "Pisteyttämätön", + "broadcastRecentTournaments": "Viimeisimmät turnaukset", + "broadcastOpenLichess": "Avaa Lichessissä", + "broadcastTeams": "Joukkueet", + "broadcastBoards": "Laudat", + "broadcastOverview": "Pääsivu", + "broadcastSubscribeTitle": "Tilaa ilmoitukset kunkin kierroksen alkamisesta. Käyttäjätunnuksesi asetuksista voit kytkeä ääni- ja puskuilmoitukset päälle tai pois.", + "broadcastUploadImage": "Lisää turnauksen kuva", + "broadcastNoBoardsYet": "Pelilautoja ei vielä ole. Ne tulevat näkyviin sitä mukaa, kun pelit ladataan tänne.", + "broadcastStartsAfter": "Alkuun on aikaa {param}", + "broadcastStartVerySoon": "Lähetys alkaa aivan pian.", + "broadcastNotYetStarted": "Lähetys ei ole vielä alkanut.", + "broadcastOfficialWebsite": "Virallinen verkkosivu", + "broadcastStandings": "Tulostaulu", + "broadcastIframeHelp": "Lisäasetuksia löytyy {param}", + "broadcastWebmastersPage": "webmasterin sivulta", + "broadcastPgnSourceHelp": "Tämän kierroksen julkinen ja reaaliaikainen PGN-tiedosto. Nopeampaan ja tehokkaampaan synkronisointiin on tarjolla myös {param}.", + "broadcastEmbedThisBroadcast": "Upota tämä lähetys sivustoosi", + "broadcastEmbedThisRound": "Upota {param} sivustoosi", + "broadcastRatingDiff": "Vahvuuslukujen erotus", + "broadcastGamesThisTournament": "Pelit tässä turnauksessa", + "broadcastScore": "Pisteet", + "broadcastNbBroadcasts": "{count, plural, =1{{count} lähetys} other{{count} lähetystä}}", "challengeChallengesX": "Haasteet: {param1}", "challengeChallengeToPlay": "Haasta peliin", "challengeChallengeDeclined": "Haasteesta kieltäydyttiin", @@ -381,8 +447,8 @@ "puzzleThemeXRayAttackDescription": "Nappula uhkaa tai puolustaa ruutua vastustajan nappulan läpi.", "puzzleThemeZugzwang": "Siirtopakko", "puzzleThemeZugzwangDescription": "Vastustajalla on rajoitettu määrä mahdollisia siirtoja, ja niistä kaikki heikentävät hänen asemaansa.", - "puzzleThemeHealthyMix": "Terve sekoitus", - "puzzleThemeHealthyMixDescription": "Vähän kaikkea. Et tiedä mitä tuleman pitää, joten olet valmiina mihin tahansa! Aivan kuten oikeissa peleissäkin.", + "puzzleThemeMix": "Terve sekoitus", + "puzzleThemeMixDescription": "Vähän kaikkea. Et tiedä mitä tuleman pitää, joten olet valmiina mihin tahansa! Aivan kuten oikeissa peleissäkin.", "puzzleThemePlayerGames": "Pelaajan peleistä", "puzzleThemePlayerGamesDescription": "Tehtäviä sinun tai jonkun toisen yksittäisen pelaajan peleistä.", "puzzleThemePuzzleDownloadInformation": "Nämä tehtävät ovat vapaasti käytettävissä ja ladattavissa osoitteesta {param}.", @@ -513,7 +579,6 @@ "memory": "Muistia", "infiniteAnalysis": "Loputon analyysi", "removesTheDepthLimit": "Poistaa syvyysrajoituksen ja pitää koneesi lämpöisenä", - "engineManager": "Moottorin hallinta", "blunder": "Vakava virhe", "mistake": "Virhe", "inaccuracy": "Epätarkkuus", @@ -595,6 +660,7 @@ "rank": "Sijoitus", "rankX": "Sijoitus: {param}", "gamesPlayed": "Pelattuja pelejä", + "ok": "OK", "cancel": "Peruuta", "whiteTimeOut": "Valkealta loppui aika", "blackTimeOut": "Mustalta loppui aika", @@ -817,7 +883,9 @@ "cheat": "Huijaus", "troll": "Trolli", "other": "Muu", - "reportDescriptionHelp": "Liitä linkki peliin/peleihin ja kerro, mikä on pielessä tämän käyttäjän käytöksessä. Älä vain sano että \"hän huijaa\", vaan kerro meille miksi ajattelet näin. Raporttisi käydään läpi nopeammin, jos se on kirjoitettu englanniksi.", + "reportCheatBoostHelp": "Liitä linkki peliin/peleihin ja kerro, mikä tämän käyttäjän toiminnassa on pielessä. Älä vain sano hänen huijaavan, vaan kerro meille, miksi olet päätellyt niin.", + "reportUsernameHelp": "Selitä, mikä tässä käyttäjätunnuksessa on loukkaavaa. Älä vain sano sen olevan loukkaava tai sopimaton, vaan kerro meille, mihin näkemyksesi perustuu, varsinkin jos loukkaus on epäsuora, muun kuin englanninkielinen, slangia, tai jos siinä viitataan kulttuuriin tai historiaan.", + "reportProcessedFasterInEnglish": "Ilmoituksesi käsitellään nopeammin, jos se on kirjoitettu englanniksi.", "error_provideOneCheatedGameLink": "Anna ainakin yksi linkki peliin, jossa epäilet huijaamista.", "by": "{param}", "importedByX": "Käyttäjän {param} tuoma", @@ -1215,6 +1283,7 @@ "showMeEverything": "Näytä kaikki", "lichessPatronInfo": "Lichess on hyväntekeväisyysjärjestö ja täysin ilmainen avoimen lähdekoodin ohjelmisto.\nKaikki toimintakustannukset, kehitystyö ja sisältö rahoitetaan yksinomaan käyttäjien lahjoituksilla.", "nothingToSeeHere": "Täällä ei ole tällä hetkellä mitään nähtävää.", + "stats": "Tilastot", "opponentLeftCounter": "{count, plural, =1{Vastustajasi on poistunut pelistä. Voit julistautua voittajaksi {count} sekunnin kuluttua.} other{Vastustajasi on poistunut pelistä. Voit julistautua voittajaksi {count} sekunnin kuluttua.}}", "mateInXHalfMoves": "{count, plural, =1{Matti {count} puolisiirrolla} other{Matti {count} puolisiirrolla}}", "nbBlunders": "{count, plural, =1{{count} vakava virhe} other{{count} vakavaa virhettä}}", @@ -1311,6 +1380,159 @@ "stormXRuns": "{count, plural, =1{1 sarja} other{{count} sarjaa}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Pelasi yhden sarjan {param2}ia} other{Pelasi {count} sarjaa {param2}ia}}", "streamerLichessStreamers": "Lichess-striimaajat", + "studyPrivate": "Yksityinen", + "studyMyStudies": "Tutkielmani", + "studyStudiesIContributeTo": "Tutkielmat joihin olen osallisena", + "studyMyPublicStudies": "Julkiset tutkielmani", + "studyMyPrivateStudies": "Yksityiset tutkielmani", + "studyMyFavoriteStudies": "Suosikkitutkielmani", + "studyWhatAreStudies": "Mitä ovat tutkielmat?", + "studyAllStudies": "Kaikki tutkielmat", + "studyStudiesCreatedByX": "{param} luomat tutkielmat", + "studyNoneYet": "Ei mitään.", + "studyHot": "Suositut juuri nyt", + "studyDateAddedNewest": "Julkaisupäivä (uusimmat)", + "studyDateAddedOldest": "Julkaisupäivä (vanhimmat)", + "studyRecentlyUpdated": "Viimeksi päivitetyt", + "studyMostPopular": "Suosituimmat", + "studyAlphabetical": "Aakkosjärjestyksessä", + "studyAddNewChapter": "Lisää uusi luku", + "studyAddMembers": "Lisää jäseniä", + "studyInviteToTheStudy": "Kutsu tutkielmaan", + "studyPleaseOnlyInvitePeopleYouKnow": "Kutsu vain ihmisiä, jotka tunnet ja jotka haluavat osallistua aktiivisesti.", + "studySearchByUsername": "Hae käyttäjätunnuksella", + "studySpectator": "Katsoja", + "studyContributor": "Osallistuja", + "studyKick": "Poista", + "studyLeaveTheStudy": "Jätä tutkielma", + "studyYouAreNowAContributor": "Olet nyt osallistuja", + "studyYouAreNowASpectator": "Olet nyt katsoja", + "studyPgnTags": "PGN-tunnisteet", + "studyLike": "Tykkää", + "studyUnlike": "Poista tykkäys", + "studyNewTag": "Uusi tunniste", + "studyCommentThisPosition": "Kommentoi asemaa", + "studyCommentThisMove": "Kommentoi siirtoa", + "studyAnnotateWithGlyphs": "Arvioi symbolein", + "studyTheChapterIsTooShortToBeAnalysed": "Luku on liian lyhyt analysoitavaksi.", + "studyOnlyContributorsCanRequestAnalysis": "Vain tutkielman osallistujat voivat pyytää tietokoneanalyysin.", + "studyGetAFullComputerAnalysis": "Hanki palvelimelta täysi tietokoneanalyysi päälinjasta.", + "studyMakeSureTheChapterIsComplete": "Varmista, että luku on valmis. Voit pyytää analyysiä vain kerran.", + "studyAllSyncMembersRemainOnTheSamePosition": "Kaikki SYNC-jäsenet pysyvät samassa asemassa", + "studyShareChanges": "Anna katsojien nähdä muutokset ja tallenna ne palvelimelle", + "studyPlaying": "Meneillään", + "studyShowEvalBar": "Arviopalkit", + "studyFirst": "Alkuun", + "studyPrevious": "Edellinen", + "studyNext": "Seuraava", + "studyLast": "Loppuun", "studyShareAndExport": "Jaa & vie", - "studyStart": "Aloita" + "studyCloneStudy": "Kloonaa", + "studyStudyPgn": "Tutkielman PGN", + "studyDownloadAllGames": "Lataa kaikki pelit", + "studyChapterPgn": "Luvun PGN", + "studyCopyChapterPgn": "Kopioi PGN", + "studyDownloadGame": "Lataa peli", + "studyStudyUrl": "Tutkielman URL", + "studyCurrentChapterUrl": "Tämän luvun URL", + "studyYouCanPasteThisInTheForumToEmbed": "Voit upottaa tämän foorumiin liittämällä", + "studyStartAtInitialPosition": "Aloita alkuperäisestä asemasta", + "studyStartAtX": "Aloita siirrosta {param}", + "studyEmbedInYourWebsite": "Upota sivustoosi tai blogiisi", + "studyReadMoreAboutEmbedding": "Lue lisää upottamisesta", + "studyOnlyPublicStudiesCanBeEmbedded": "Vain julkiset tutkielmat voidaan upottaa!", + "studyOpen": "Avaa", + "studyXBroughtToYouByY": "{param1}, sivustolta {param2}", + "studyStudyNotFound": "Tutkielmaa ei löydy", + "studyEditChapter": "Muokkaa lukua", + "studyNewChapter": "Uusi luku", + "studyImportFromChapterX": "Tuo luvusta {param}", + "studyOrientation": "Suunta", + "studyAnalysisMode": "Analyysitila", + "studyPinnedChapterComment": "Kiinnitetty lukukommentti", + "studySaveChapter": "Tallenna luku", + "studyClearAnnotations": "Poista kommentit", + "studyClearVariations": "Tyhjennä muunnelmat", + "studyDeleteChapter": "Poista luku", + "studyDeleteThisChapter": "Poistetaanko tämä luku? Et voi palauttaa sitä enää!", + "studyClearAllCommentsInThisChapter": "Poista kaikki kommentit, symbolit ja piirtokuviot tästä luvusta?", + "studyRightUnderTheBoard": "Heti laudan alla", + "studyNoPinnedComment": "Ei", + "studyNormalAnalysis": "Tavallinen analyysi", + "studyHideNextMoves": "Piilota tulevat siirrot", + "studyInteractiveLesson": "Interaktiivinen oppitunti", + "studyChapterX": "Luku {param}", + "studyEmpty": "Tyhjä", + "studyStartFromInitialPosition": "Aloita alkuasemasta", + "studyEditor": "Editori", + "studyStartFromCustomPosition": "Aloita haluamastasi asemasta", + "studyLoadAGameByUrl": "Lataa peli URL:stä", + "studyLoadAPositionFromFen": "Lataa asema FEN:istä", + "studyLoadAGameFromPgn": "Ota peli PGN:stä", + "studyAutomatic": "Automaattinen", + "studyUrlOfTheGame": "URL peliin", + "studyLoadAGameFromXOrY": "Lataa peli lähteestä {param1} tai {param2}", + "studyCreateChapter": "Aloita luku", + "studyCreateStudy": "Luo tutkielma", + "studyEditStudy": "Muokkaa tutkielmaa", + "studyVisibility": "Näkyvyys", + "studyPublic": "Julkinen", + "studyUnlisted": "Listaamaton", + "studyInviteOnly": "Vain kutsutut", + "studyAllowCloning": "Salli kloonaus", + "studyNobody": "Ei kukaan", + "studyOnlyMe": "Vain minä", + "studyContributors": "Osallistujat", + "studyMembers": "Jäsenet", + "studyEveryone": "Kaikki", + "studyEnableSync": "Synkronointi käyttöön", + "studyYesKeepEveryoneOnTheSamePosition": "Kyllä: pidä kaikki samassa asemassa", + "studyNoLetPeopleBrowseFreely": "Ei: anna ihmisten selata vapaasti", + "studyPinnedStudyComment": "Kiinnitetty tutkielmakommentti", + "studyStart": "Aloita", + "studySave": "Tallenna", + "studyClearChat": "Tyhjennä keskustelu", + "studyDeleteTheStudyChatHistory": "Haluatko poistaa tutkielman keskusteluhistorian? Et voi palauttaa sitä enää!", + "studyDeleteStudy": "Poista tutkielma", + "studyConfirmDeleteStudy": "Poistetaanko koko tutkielma? Et voi palauttaa sitä enää. Vahvista poisto kirjoittamalla tutkielman nimen: {param}", + "studyWhereDoYouWantToStudyThat": "Missä haluat tutkia tätä?", + "studyGoodMove": "Hyvä siirto", + "studyMistake": "Virhe", + "studyBrilliantMove": "Loistava siirto", + "studyBlunder": "Vakava virhe", + "studyInterestingMove": "Mielenkiintoinen siirto", + "studyDubiousMove": "Kyseenalainen siirto", + "studyOnlyMove": "Ainoa siirto", + "studyZugzwang": "Siirtopakko", + "studyEqualPosition": "Tasainen asema", + "studyUnclearPosition": "Epäselvä asema", + "studyWhiteIsSlightlyBetter": "Valkealla on pieni etu", + "studyBlackIsSlightlyBetter": "Mustalla on pieni etu", + "studyWhiteIsBetter": "Valkealla on etu", + "studyBlackIsBetter": "Mustalla on etu", + "studyWhiteIsWinning": "Valkea on voitolla", + "studyBlackIsWinning": "Musta on voitolla", + "studyNovelty": "Uutuus", + "studyDevelopment": "Kehitys", + "studyInitiative": "Aloite", + "studyAttack": "Hyökkäys", + "studyCounterplay": "Vastapeli", + "studyTimeTrouble": "Aikapula", + "studyWithCompensation": "Kompensaatio", + "studyWithTheIdea": "Ideana", + "studyNextChapter": "Seuraava luku", + "studyPrevChapter": "Edellinen luku", + "studyStudyActions": "Tutkielmatoiminnot", + "studyTopics": "Aiheet", + "studyMyTopics": "Omat aiheeni", + "studyPopularTopics": "Suositut aiheet", + "studyManageTopics": "Aiheiden hallinta", + "studyBack": "Takaisin", + "studyPlayAgain": "Pelaa uudelleen", + "studyWhatWouldYouPlay": "Mitä pelaisit tässä asemassa?", + "studyYouCompletedThisLesson": "Onnittelut! Olet suorittanut tämän oppiaiheen.", + "studyNbChapters": "{count, plural, =1{{count} luku} other{{count} lukua}}", + "studyNbGames": "{count, plural, =1{{count} peli} other{{count} peliä}}", + "studyNbMembers": "{count, plural, =1{{count} jäsen} other{{count} jäsentä}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Liitä PGN tähän, enintään {count} peli} other{Liitä PGN tähän, enintään {count} peliä}}" } \ No newline at end of file diff --git a/lib/l10n/lila_fo.arb b/lib/l10n/lila_fo.arb index 68a210975a..f8a1ad671e 100644 --- a/lib/l10n/lila_fo.arb +++ b/lib/l10n/lila_fo.arb @@ -22,6 +22,15 @@ "activityJoinedNbTeams": "{count, plural, =1{Fór upp í {count} lið} other{Fór upp í {count} lið}}", "broadcastBroadcasts": "Sendingar", "broadcastLiveBroadcasts": "Beinleiðis sendingar frá kappingum", + "broadcastNewBroadcast": "Nýggj beinleiðis sending", + "broadcastOngoing": "Í gongd", + "broadcastUpcoming": "Komandi", + "broadcastCompleted": "Liðug sending", + "broadcastRoundNumber": "Nummar á umfari", + "broadcastFullDescription": "Fullfíggjað lýsing av tiltaki", + "broadcastFullDescriptionHelp": "Valfrí long lýsing av sending. {param1} er tøkt. Longdin má vera styttri enn {param2} bókstavir.", + "broadcastSourceUrlHelp": "URL-leinki, ið Lichess fer at kanna til tess at fáa PGN dagføringar. Leinkið nýtist at vera alment atkomiligt á alnetinum.", + "broadcastStartDateHelp": "Valfrítt, um tú veitst, nær tiltakið byrjar", "challengeChallengeToPlay": "Bjóða av at telva", "challengeChallengeDeclined": "Avbjóðing avvíst", "challengeChallengeAccepted": "Avbjóðing góðtikin!", @@ -255,8 +264,8 @@ "puzzleThemeXRayAttackDescription": "Eitt fólk loypur á ella verjir ein punt gjøgnum eitt mótstøðufólk.", "puzzleThemeZugzwang": "Leiktvingsil", "puzzleThemeZugzwangDescription": "Mótleikarin hevur avmarkaðar møguleikar at flyta, og allir leikir gera støðu hansara verri.", - "puzzleThemeHealthyMix": "Sunt bland", - "puzzleThemeHealthyMixDescription": "Eitt sindur av øllum. Tú veitst ikki, hvat tú kanst vænta tær, so ver til reiðar til alt! Júst sum í veruligum talvum.", + "puzzleThemeMix": "Sunt bland", + "puzzleThemeMixDescription": "Eitt sindur av øllum. Tú veitst ikki, hvat tú kanst vænta tær, so ver til reiðar til alt! Júst sum í veruligum talvum.", "searchSearch": "Leita", "settingsSettings": "Stillingar", "settingsCloseAccount": "Lat kontu aftur", @@ -624,7 +633,6 @@ "cheat": "Snýt", "troll": "Trøll", "other": "Annað", - "reportDescriptionHelp": "Flyt leinkið til talvið ella talvini higar, og greið frá, hvat bagir atburðinum hjá brúkaranum. Skriva ikki bert \"hann snýtir\", men sig okkum, hvussu tú komst til hesa niðurstøðu. Fráboðan tín verður skjótari viðgjørd, um hon verður skrivað á enskum.", "error_provideOneCheatedGameLink": "Útvega leinki til í minsta lagi eitt talv, har snýtt varð.", "by": "eftir {param}", "importedByX": "{param} las inn", @@ -1002,6 +1010,126 @@ "stormXRuns": "{count, plural, =1{1 umfar} other{{count} umfør}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Telvaði eitt umfar av {param2}} other{Telvaði {count} umfør av {param2}}}", "streamerLichessStreamers": "Lichess stroymarar", + "studyPrivate": "Egin (privat)", + "studyMyStudies": "Mínar rannsóknir", + "studyStudiesIContributeTo": "Rannsóknir, eg gevi mítt íkast til", + "studyMyPublicStudies": "Mínar almennu rannsóknir", + "studyMyPrivateStudies": "Mínar egnu rannsóknir", + "studyMyFavoriteStudies": "Mínar yndisrannsóknir", + "studyWhatAreStudies": "Hvat eru rannsóknir?", + "studyAllStudies": "Allar rannsóknir", + "studyStudiesCreatedByX": "{param} stovnaði hesar rannsóknir", + "studyNoneYet": "Ongar enn.", + "studyHot": "Heitar", + "studyDateAddedNewest": "Eftir dagfesting (nýggjastu)", + "studyDateAddedOldest": "Eftir dagfesting (eldstu)", + "studyRecentlyUpdated": "Nýliga dagførdar", + "studyMostPopular": "Best dámdu", + "studyAddNewChapter": "Skoyt nýggjan kapittul upp í", + "studyAddMembers": "Legg limir aftrat", + "studyInviteToTheStudy": "Bjóða uppí rannsóknina", + "studyPleaseOnlyInvitePeopleYouKnow": "Bjóða vinaliga bert fólki, tú kennir, og sum vilja taka virknan lut í rannsóknini.", + "studySearchByUsername": "Leita eftir brúkaranavni", + "studySpectator": "Áskoðari", + "studyContributor": "Gevur íkast", + "studyKick": "Koyr úr", + "studyLeaveTheStudy": "Far úr rannsóknini", + "studyYouAreNowAContributor": "Tú ert nú ein, ið leggur aftrat rannsóknini", + "studyYouAreNowASpectator": "Tú ert nú áskoðari", + "studyPgnTags": "PGN-frámerki", + "studyLike": "Dáma", + "studyNewTag": "Nýtt frámerki", + "studyCommentThisPosition": "Viðmerk hesa støðuna", + "studyCommentThisMove": "Viðmerk henda leikin", + "studyAnnotateWithGlyphs": "Skriva við teknum", + "studyTheChapterIsTooShortToBeAnalysed": "Kapittulin er ov stuttur til at verða greinaður.", + "studyOnlyContributorsCanRequestAnalysis": "Bert tey, ið geva sítt íkast til rannsóknina, kunnu biðja um eina teldugreining.", + "studyGetAFullComputerAnalysis": "Fá eina fullfíggjaða teldugreining av høvuðsbrigdinum frá ambætaranum.", + "studyMakeSureTheChapterIsComplete": "Tryggja tær, at kapittulin er fullfíggjaður. Tú kanst bert biðja um greining eina ferð.", + "studyAllSyncMembersRemainOnTheSamePosition": "Allir SYNC-limir verða verandi í somu støðu", + "studyShareChanges": "Deil broytingar við áskoðarar, og goym tær á ambætaranum", + "studyPlaying": "Í gongd", + "studyFirst": "Fyrsta", + "studyPrevious": "Undanfarna", + "studyNext": "Næsta", + "studyLast": "Síðsta", "studyShareAndExport": "Deil & flyt út", - "studyStart": "Byrja" + "studyCloneStudy": "Klona", + "studyStudyPgn": "PGN rannsókn", + "studyDownloadAllGames": "Tak øll talv niður", + "studyChapterPgn": "PGN kapittul", + "studyDownloadGame": "Tak talv niður", + "studyStudyUrl": "URL rannsókn", + "studyCurrentChapterUrl": "Núverandi URL partur", + "studyYouCanPasteThisInTheForumToEmbed": "Tú kanst seta hetta inn í torgið at sýna tað har", + "studyStartAtInitialPosition": "Byrja við byrjanarstøðuni", + "studyStartAtX": "Byrja við {param}", + "studyEmbedInYourWebsite": "Fell inn í heimasíðu tína ella blogg tín", + "studyReadMoreAboutEmbedding": "Les meira um at fella inn í", + "studyOnlyPublicStudiesCanBeEmbedded": "Bert almennar rannsóknir kunnu verða feldar inn í!", + "studyOpen": "Lat upp", + "studyXBroughtToYouByY": "{param2} fekk tær {param1} til vegar", + "studyStudyNotFound": "Rannsókn ikki funnin", + "studyEditChapter": "Broyt kapittul", + "studyNewChapter": "Nýggjur kapittul", + "studyOrientation": "Helling", + "studyAnalysisMode": "Greiningarstøða", + "studyPinnedChapterComment": "Føst viðmerking til kapittulin", + "studySaveChapter": "Goym kapittulin", + "studyClearAnnotations": "Strika viðmerkingar", + "studyDeleteChapter": "Strika kapittul", + "studyDeleteThisChapter": "Strika henda kapittulin? Til ber ikki at angra!", + "studyClearAllCommentsInThisChapter": "Skulu allar viðmerkingar, øll tekn og teknað skap strikast úr hesum kapitli?", + "studyRightUnderTheBoard": "Beint undir talvborðinum", + "studyNoPinnedComment": "Einki", + "studyNormalAnalysis": "Vanlig greining", + "studyHideNextMoves": "Fjal næstu leikirnar", + "studyInteractiveLesson": "Samvirkin frálæra", + "studyChapterX": "Kapittul {param}", + "studyEmpty": "Tómur", + "studyStartFromInitialPosition": "Byrja við byrjanarstøðuni", + "studyEditor": "Ritstjóri", + "studyStartFromCustomPosition": "Byrja við støðu, ið brúkari ger av", + "studyLoadAGameByUrl": "Les inn talv frá URL", + "studyLoadAPositionFromFen": "Les inn talvstøðu frá FEN", + "studyLoadAGameFromPgn": "Les inn talv frá PGN", + "studyAutomatic": "Sjálvvirkið", + "studyUrlOfTheGame": "URL fyri talvini", + "studyLoadAGameFromXOrY": "Les talv inn frá {param1} ella {param2}", + "studyCreateChapter": "Stovna kapittul", + "studyCreateStudy": "Stovna rannsókn", + "studyEditStudy": "Ritstjórna rannsókn", + "studyVisibility": "Sýni", + "studyPublic": "Almen", + "studyUnlisted": "Ikki skrásett", + "studyInviteOnly": "Bert innboðin", + "studyAllowCloning": "Loyv kloning", + "studyNobody": "Eingin", + "studyOnlyMe": "Bert eg", + "studyContributors": "Luttakarar", + "studyMembers": "Limir", + "studyEveryone": "Øll", + "studyEnableSync": "Samstilling møgulig", + "studyYesKeepEveryoneOnTheSamePosition": "Ja: varðveit øll í somu støðu", + "studyNoLetPeopleBrowseFreely": "Nei: lat fólk kaga frítt", + "studyPinnedStudyComment": "Føst rannsóknarviðmerking", + "studyStart": "Byrja", + "studySave": "Goym", + "studyClearChat": "Rudda kjatt", + "studyDeleteTheStudyChatHistory": "Skal kjattsøgan í rannsóknini strikast? Til ber ikki at angra!", + "studyDeleteStudy": "Burturbein rannsókn", + "studyWhereDoYouWantToStudyThat": "Hvar vilt tú rannsaka hatta?", + "studyGoodMove": "Góður leikur", + "studyMistake": "Mistak", + "studyBrilliantMove": "Framúrskarandi leikur", + "studyBlunder": "Bukkur", + "studyInterestingMove": "Áhugaverdur leikur", + "studyDubiousMove": "Ivasamur leikur", + "studyOnlyMove": "Einasti leikur", + "studyWhiteIsWinning": "Hvítur stendur til at vinna", + "studyBlackIsWinning": "Svartur stendur til at vinna", + "studyNbChapters": "{count, plural, =1{{count} kapittul} other{{count} kapitlar}}", + "studyNbGames": "{count, plural, =1{{count} talv} other{{count} talv}}", + "studyNbMembers": "{count, plural, =1{{count} limur} other{{count} limir}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Set PGN tekstin hjá tær inn her, upp til {count} talv} other{Set PGN tekstin hjá tær inn her, upp til {count} talv}}" } \ No newline at end of file diff --git a/lib/l10n/lila_fr.arb b/lib/l10n/lila_fr.arb index c68f898a26..be964e37d9 100644 --- a/lib/l10n/lila_fr.arb +++ b/lib/l10n/lila_fr.arb @@ -30,7 +30,6 @@ "mobilePuzzleStormConfirmEndRun": "Voulez-vous mettre fin à cette série?", "mobilePuzzleStormFilterNothingToShow": "Rien à afficher. Veuillez changer les filtres.", "mobileCancelTakebackOffer": "Annuler la proposition de reprise du coup", - "mobileCancelDrawOffer": "Annuler la proposition de nulle", "mobileWaitingForOpponentToJoin": "En attente d'un adversaire...", "mobileBlindfoldMode": "Partie à l'aveugle", "mobileLiveStreamers": "Diffuseurs en direct", @@ -42,6 +41,7 @@ "mobilePuzzleStormSubtitle": "Faites un maximum de problèmes en 3 minutes.", "mobileGreeting": "Bonjour {param}", "mobileGreetingWithoutName": "Bonjour", + "mobilePrefMagnifyDraggedPiece": "Grossir la pièce déplacée", "activityActivity": "Activité", "activityHostedALiveStream": "A hébergé une diffusion en direct", "activityRankedInSwissTournament": "Classé {param1} dans le tournoi {param2}", @@ -54,6 +54,7 @@ "activityPlayedNbMoves": "{count, plural, =1{A joué {count} coup} other{A joué {count} coups}}", "activityInNbCorrespondenceGames": "{count, plural, =1{dans {count} partie par correspondance} other{dans {count} parties par correspondance}}", "activityCompletedNbGames": "{count, plural, =1{{count} partie par correspondance terminée} other{{count} parties par correspondance terminées}}", + "activityCompletedNbVariantGames": "{count, plural, =1{{count} partie {param2} par correspondance terminée} other{{count} parties {param2} par correspondance terminées}}", "activityFollowedNbPlayers": "{count, plural, =1{A commencé à suivre {count} joueur} other{A commencé à suivre {count} joueurs}}", "activityGainedNbFollowers": "{count, plural, =1{A gagné {count} nouveau suiveur} other{A gagné {count} nouveaux suiveurs}}", "activityHostedNbSimuls": "{count, plural, =1{A hébergé {count} simultanée} other{A hébergé {count} simultanées}}", @@ -64,7 +65,72 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{A participé à {count} tournoi(s) suisse(s)} other{A participé à {count} tournois suisses}}", "activityJoinedNbTeams": "{count, plural, =1{A rejoint {count} équipe} other{A rejoint {count} équipes}}", "broadcastBroadcasts": "Diffusions", + "broadcastMyBroadcasts": "Ma diffusion", "broadcastLiveBroadcasts": "Diffusions de tournois en direct", + "broadcastBroadcastCalendar": "Calendrier des diffusions", + "broadcastNewBroadcast": "Nouvelle diffusion en direct", + "broadcastSubscribedBroadcasts": "Diffusions suivies", + "broadcastAboutBroadcasts": "À propos des diffusions", + "broadcastHowToUseLichessBroadcasts": "Comment utiliser les diffusions dans Lichess.", + "broadcastTheNewRoundHelp": "La nouvelle ronde aura les mêmes participants et contributeurs que la précédente.", + "broadcastAddRound": "Ajouter une ronde", + "broadcastOngoing": "En cours", + "broadcastUpcoming": "À venir", + "broadcastCompleted": "Terminé", + "broadcastCompletedHelp": "Lichess détecte la fin des rondes en fonction des parties sources. Utilisez cette option s'il n'y a pas de source.", + "broadcastRoundName": "Nom de la ronde", + "broadcastRoundNumber": "Numéro de la ronde", + "broadcastTournamentName": "Nom du tournoi", + "broadcastTournamentDescription": "Brève description du tournoi", + "broadcastFullDescription": "Description complète de l'événement", + "broadcastFullDescriptionHelp": "Description détaillée et optionnelle de la diffusion. {param1} est disponible. La longueur doit être inférieure à {param2} caractères.", + "broadcastSourceSingleUrl": "URL source de la partie en PGN", + "broadcastSourceUrlHelp": "URL que Lichess interrogera pour obtenir les mises à jour du PGN. Elle doit être accessible publiquement depuis Internet.", + "broadcastSourceGameIds": "Jusqu'à 64 ID de partie Lichess séparés par des espaces.", + "broadcastStartDateTimeZone": "Date de début du tournoi (fuseau horaire local) : {param}", + "broadcastStartDateHelp": "Facultatif, si vous savez quand l'événement commence", + "broadcastCurrentGameUrl": "URL de la partie en cours", + "broadcastDownloadAllRounds": "Télécharger toutes les rondes", + "broadcastResetRound": "Réinitialiser cette ronde", + "broadcastDeleteRound": "Supprimer cette ronde", + "broadcastDefinitivelyDeleteRound": "Supprimer définitivement la ronde et ses parties.", + "broadcastDeleteAllGamesOfThisRound": "Supprimer toutes les parties de la ronde. La source doit être active pour recréer les parties.", + "broadcastEditRoundStudy": "Modifier l'étude de la ronde", + "broadcastDeleteTournament": "Supprimer ce tournoi", + "broadcastDefinitivelyDeleteTournament": "Supprimer définitivement le tournoi, toutes ses rondes et toutes ses parties.", + "broadcastShowScores": "Afficher les résultats des joueurs en fonction des résultats des parties", + "broadcastReplacePlayerTags": "Facultatif : remplacer les noms des joueurs, les classements et les titres", + "broadcastFideFederations": "Fédérations FIDE", + "broadcastTop10Rating": "10 plus hauts classements", + "broadcastFidePlayers": "Joueurs FIDE", + "broadcastFidePlayerNotFound": "Joueur FIDE introuvable", + "broadcastFideProfile": "Profil FIDE", + "broadcastFederation": "Fédération", + "broadcastAgeThisYear": "Âge cette année", + "broadcastUnrated": "Non classé", + "broadcastRecentTournaments": "Tournois récents", + "broadcastOpenLichess": "Ouvrir dans Lichess", + "broadcastTeams": "Équipes", + "broadcastBoards": "Échiquiers", + "broadcastOverview": "Survol", + "broadcastSubscribeTitle": "Abonnez-vous pour être averti du début de chaque ronde. Vous pouvez basculer entre une sonnerie ou une notification poussée pour les diffusions dans les préférences de votre compte.", + "broadcastUploadImage": "Téléverser une image pour le tournoi", + "broadcastNoBoardsYet": "Pas d'échiquiers pour le moment. Ils s'afficheront lorsque les parties seront téléversées.", + "broadcastBoardsCanBeLoaded": "Les échiquiers sont chargés à partir d'une source ou de l'{param}.", + "broadcastStartsAfter": "Commence après {param}", + "broadcastStartVerySoon": "La diffusion commencera très bientôt.", + "broadcastNotYetStarted": "La diffusion n'a pas encore commencé.", + "broadcastOfficialWebsite": "Site Web officiel", + "broadcastStandings": "Classement", + "broadcastIframeHelp": "Plus d'options sur la {param}", + "broadcastWebmastersPage": "page des webmestres", + "broadcastPgnSourceHelp": "Source PGN publique en temps réel pour cette ronde. Nous offrons également un {param} pour permettre une synchronisation rapide et efficace.", + "broadcastEmbedThisBroadcast": "Intégrer cette diffusion dans votre site Web", + "broadcastEmbedThisRound": "Intégrer la {param} dans votre site Web", + "broadcastRatingDiff": "Différence de cote", + "broadcastGamesThisTournament": "Partie de ce tournoi", + "broadcastScore": "Résultat", + "broadcastNbBroadcasts": "{count, plural, =1{{count} diffusion} other{{count} diffusions}}", "challengeChallengesX": "Défis : {param1}", "challengeChallengeToPlay": "Défier ce joueur", "challengeChallengeDeclined": "Défi refusé", @@ -383,8 +449,8 @@ "puzzleThemeXRayAttackDescription": "Une pièce attaque ou défend une case, à travers une pièce ennemie.", "puzzleThemeZugzwang": "Zugzwang", "puzzleThemeZugzwangDescription": "L'adversaire est limité dans les mouvements qu'il peut effectuer, et tous les coups aggravent sa position.", - "puzzleThemeHealthyMix": "Divers", - "puzzleThemeHealthyMixDescription": "Un peu de tout. Vous ne savez pas à quoi vous attendre ! Comme dans une vraie partie.", + "puzzleThemeMix": "Problèmes variés", + "puzzleThemeMixDescription": "Un peu de tout. Vous ne savez pas à quoi vous attendre! Comme dans une vraie partie.", "puzzleThemePlayerGames": "Parties de joueurs", "puzzleThemePlayerGamesDescription": "Problèmes tirés de vos parties ou de celles d'autres joueurs.", "puzzleThemePuzzleDownloadInformation": "Ces problèmes sont du domaine public et peuvent être téléchargés sur {param}.", @@ -515,7 +581,6 @@ "memory": "Mémoire", "infiniteAnalysis": "Analyse infinie", "removesTheDepthLimit": "Désactive la profondeur limitée et fait chauffer votre ordinateur", - "engineManager": "Gestionnaire de moteur d'analyse", "blunder": "Gaffe", "mistake": "Erreur", "inaccuracy": "Imprécision", @@ -597,6 +662,7 @@ "rank": "Rang", "rankX": "Classement : {param}", "gamesPlayed": "Parties jouées", + "ok": "OK", "cancel": "Annuler", "whiteTimeOut": "Temps blanc écoulé", "blackTimeOut": "Temps noir écoulé", @@ -819,7 +885,9 @@ "cheat": "Triche", "troll": "Troll", "other": "Autre", - "reportDescriptionHelp": "Copiez le(s) lien(s) vers les parties et expliquez en quoi le comportement de cet utilisateur est inapproprié. Ne dites pas juste \"il triche\", mais expliquez comment vous êtes arrivé à cette conclusion. Votre rapport sera traité plus vite s'il est écrit en anglais.", + "reportCheatBoostHelp": "Collez le lien vers la ou les parties et expliquez pourquoi le comportement de l'utilisateur est inapproprié. Ne dites pas juste « il triche »; expliquez comment vous êtes arrivé à cette conclusion.", + "reportUsernameHelp": "Expliquez pourquoi ce nom d'utilisateur est offensant. Ne dites pas simplement qu'il est choquant ou inapproprié; expliquez comment vous êtes arrivé à cette conclusion, surtout si l'insulte n'est pas claire, n'est pas en anglais, est en argot ou a une connotation historique ou culturelle.", + "reportProcessedFasterInEnglish": "Votre rapport sera traité plus rapidement s'il est rédigé en anglais.", "error_provideOneCheatedGameLink": "Merci de fournir au moins un lien vers une partie où il y a eu triche.", "by": "par {param}", "importedByX": "Importée par {param}", @@ -1217,6 +1285,7 @@ "showMeEverything": "Tout afficher", "lichessPatronInfo": "Lichess est une association à but non lucratif et un logiciel open source entièrement libre.\nTous les coûts d'exploitation, le développement et le contenu sont financés uniquement par les dons des utilisateurs.", "nothingToSeeHere": "Rien à voir ici pour le moment.", + "stats": "Statistiques", "opponentLeftCounter": "{count, plural, =1{Votre adversaire a quitté la partie. Vous pourrez revendiquer la victoire dans {count} seconde.} other{Votre adversaire a quitté la partie. Vous pourrez revendiquer la victoire dans {count} secondes.}}", "mateInXHalfMoves": "{count, plural, =1{Mate en {count} demi-coup} other{Mate en {count} demi-coups}}", "nbBlunders": "{count, plural, =1{{count} gaffe} other{{count} gaffes}}", @@ -1313,6 +1382,159 @@ "stormXRuns": "{count, plural, =1{1 essai} other{{count} essais}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{A fait un essai de {param2}} other{A fait {count} essais de {param2}}}", "streamerLichessStreamers": "Streamers sur Lichess", + "studyPrivate": "Étude(s) privée(s)", + "studyMyStudies": "Mes études", + "studyStudiesIContributeTo": "Études auxquelles je participe", + "studyMyPublicStudies": "Mes études publiques", + "studyMyPrivateStudies": "Mes études privées", + "studyMyFavoriteStudies": "Mes études favorites", + "studyWhatAreStudies": "Qu'est-ce qu'une étude ?", + "studyAllStudies": "Toutes les études", + "studyStudiesCreatedByX": "Études créées par {param}", + "studyNoneYet": "Aucune étude.", + "studyHot": "Populaire(s)", + "studyDateAddedNewest": "Date d'ajout (dernier ajout)", + "studyDateAddedOldest": "Date d'ajout (premier ajout)", + "studyRecentlyUpdated": "Récemment mis à jour", + "studyMostPopular": "Études les plus populaires", + "studyAlphabetical": "Alphabétique", + "studyAddNewChapter": "Ajouter un nouveau chapitre", + "studyAddMembers": "Ajouter des membres", + "studyInviteToTheStudy": "Inviter à l'étude", + "studyPleaseOnlyInvitePeopleYouKnow": "Veuillez n'inviter que des personnes qui vous connaissent et qui souhaitent activement participer à cette étude.", + "studySearchByUsername": "Rechercher par nom d'utilisateur", + "studySpectator": "Spectateur", + "studyContributor": "Contributeur", + "studyKick": "Éjecter", + "studyLeaveTheStudy": "Quitter l'étude", + "studyYouAreNowAContributor": "Vous êtes maintenant un contributeur", + "studyYouAreNowASpectator": "Vous êtes maintenant un spectateur", + "studyPgnTags": "Étiquettes PGN", + "studyLike": "Aimer", + "studyUnlike": "Je n’aime pas", + "studyNewTag": "Nouvelle étiquette", + "studyCommentThisPosition": "Commenter la position", + "studyCommentThisMove": "Commenter ce coup", + "studyAnnotateWithGlyphs": "Annoter avec des symboles", + "studyTheChapterIsTooShortToBeAnalysed": "Le chapitre est trop court pour être analysé.", + "studyOnlyContributorsCanRequestAnalysis": "Seuls les contributeurs de l'étude peuvent demander une analyse informatique.", + "studyGetAFullComputerAnalysis": "Obtenez une analyse en ligne complète de la ligne principale.", + "studyMakeSureTheChapterIsComplete": "Assurez-vous que le chapitre est terminé. Vous ne pouvez demander l'analyse qu'une seule fois.", + "studyAllSyncMembersRemainOnTheSamePosition": "Tous les membres SYNC demeurent sur la même position", + "studyShareChanges": "Partager les changements avec les spectateurs et les enregistrer sur le serveur", + "studyPlaying": "En cours", + "studyShowEvalBar": "Barre d’évaluation", + "studyFirst": "Premier", + "studyPrevious": "Précédent", + "studyNext": "Suivant", + "studyLast": "Dernier", "studyShareAndExport": "Partager & exporter", - "studyStart": "Commencer" + "studyCloneStudy": "Dupliquer", + "studyStudyPgn": "PGN de l'étude", + "studyDownloadAllGames": "Télécharger toutes les parties", + "studyChapterPgn": "PGN du chapitre", + "studyCopyChapterPgn": "Copier le fichier PGN", + "studyDownloadGame": "Télécharger la partie", + "studyStudyUrl": "URL de l'étude", + "studyCurrentChapterUrl": "URL du chapitre actuel", + "studyYouCanPasteThisInTheForumToEmbed": "Vous pouvez collez ce lien dans le forum afin de l’insérer", + "studyStartAtInitialPosition": "Commencer à partir du début", + "studyStartAtX": "Débuter à {param}", + "studyEmbedInYourWebsite": "Intégrer dans votre site ou blog", + "studyReadMoreAboutEmbedding": "En savoir plus sur l'intégration", + "studyOnlyPublicStudiesCanBeEmbedded": "Seules les études publiques peuvent être intégrées !", + "studyOpen": "Ouvrir", + "studyXBroughtToYouByY": "{param1} vous est apporté par {param2}", + "studyStudyNotFound": "Étude introuvable", + "studyEditChapter": "Modifier le chapitre", + "studyNewChapter": "Nouveau chapitre", + "studyImportFromChapterX": "Importer depuis {param}", + "studyOrientation": "Orientation", + "studyAnalysisMode": "Mode analyse", + "studyPinnedChapterComment": "Commentaire du chapitre épinglé", + "studySaveChapter": "Enregistrer le chapitre", + "studyClearAnnotations": "Effacer les annotations", + "studyClearVariations": "Supprimer les variantes", + "studyDeleteChapter": "Supprimer le chapitre", + "studyDeleteThisChapter": "Supprimer ce chapitre ? Cette action est irréversible !", + "studyClearAllCommentsInThisChapter": "Effacer tous les commentaires et annotations dans ce chapitre ?", + "studyRightUnderTheBoard": "Juste sous l'échiquier", + "studyNoPinnedComment": "Aucun", + "studyNormalAnalysis": "Analyse normale", + "studyHideNextMoves": "Cacher les coups suivants", + "studyInteractiveLesson": "Leçon interactive", + "studyChapterX": "Chapitre : {param}", + "studyEmpty": "Par défaut", + "studyStartFromInitialPosition": "Commencer à partir du début", + "studyEditor": "Editeur", + "studyStartFromCustomPosition": "Commencer à partir d'une position personnalisée", + "studyLoadAGameByUrl": "Charger des parties à partir d'une URL", + "studyLoadAPositionFromFen": "Charger une position par FEN", + "studyLoadAGameFromPgn": "Charger des parties par PGN", + "studyAutomatic": "Automatique", + "studyUrlOfTheGame": "URL des parties, une par ligne", + "studyLoadAGameFromXOrY": "Charger des parties de {param1} ou {param2}", + "studyCreateChapter": "Créer un chapitre", + "studyCreateStudy": "Créer une étude", + "studyEditStudy": "Modifier l'étude", + "studyVisibility": "Visibilité", + "studyPublic": "Publique", + "studyUnlisted": "Non répertorié", + "studyInviteOnly": "Sur invitation seulement", + "studyAllowCloning": "Autoriser la duplication", + "studyNobody": "Personne", + "studyOnlyMe": "Seulement moi", + "studyContributors": "Contributeurs", + "studyMembers": "Membres", + "studyEveryone": "Tout le monde", + "studyEnableSync": "Activer la synchronisation", + "studyYesKeepEveryoneOnTheSamePosition": "Oui : garder tout le monde sur la même position", + "studyNoLetPeopleBrowseFreely": "Non : laisser les gens naviguer librement", + "studyPinnedStudyComment": "Commentaire d'étude épinglé", + "studyStart": "Commencer", + "studySave": "Enregistrer", + "studyClearChat": "Effacer le tchat", + "studyDeleteTheStudyChatHistory": "Supprimer l'historique du tchat de l'étude ? Cette action est irréversible !", + "studyDeleteStudy": "Supprimer l'étude", + "studyConfirmDeleteStudy": "Supprimer toute l’étude? Aucun retour en arrière possible! Taper le nom de l’étude pour confirmer : {param}", + "studyWhereDoYouWantToStudyThat": "Où voulez-vous étudier cela ?", + "studyGoodMove": "Bon coup", + "studyMistake": "Erreur", + "studyBrilliantMove": "Excellent coup", + "studyBlunder": "Gaffe", + "studyInterestingMove": "Coup intéressant", + "studyDubiousMove": "Coup douteux", + "studyOnlyMove": "Seul coup", + "studyZugzwang": "Zugzwang", + "studyEqualPosition": "Position égale", + "studyUnclearPosition": "Position incertaine", + "studyWhiteIsSlightlyBetter": "Les Blancs sont un peu mieux", + "studyBlackIsSlightlyBetter": "Les Noirs sont un peu mieux", + "studyWhiteIsBetter": "Les Blancs sont mieux", + "studyBlackIsBetter": "Les Noirs sont mieux", + "studyWhiteIsWinning": "Les Blancs gagnent", + "studyBlackIsWinning": "Les Noirs gagnent", + "studyNovelty": "Nouveauté", + "studyDevelopment": "Développement", + "studyInitiative": "Initiative", + "studyAttack": "Attaque", + "studyCounterplay": "Contre-jeu", + "studyTimeTrouble": "Pression de temps", + "studyWithCompensation": "Avec compensation", + "studyWithTheIdea": "Avec l'idée", + "studyNextChapter": "Chapitre suivant", + "studyPrevChapter": "Chapitre précédent", + "studyStudyActions": "Options pour les études", + "studyTopics": "Thèmes", + "studyMyTopics": "Mes thèmes", + "studyPopularTopics": "Thèmes populaires", + "studyManageTopics": "Gérer les thèmes", + "studyBack": "Retour", + "studyPlayAgain": "Jouer à nouveau", + "studyWhatWouldYouPlay": "Que joueriez-vous dans cette position ?", + "studyYouCompletedThisLesson": "Félicitations ! Vous avez terminé ce cours.", + "studyNbChapters": "{count, plural, =1{{count} chapitre} other{{count} chapitres}}", + "studyNbGames": "{count, plural, =1{{count} partie} other{{count} parties}}", + "studyNbMembers": "{count, plural, =1{{count} membre} other{{count} membres}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Collez votre texte PGN ici, jusqu'à {count} partie} other{Collez votre texte PGN ici, jusqu'à {count} parties}}" } \ No newline at end of file diff --git a/lib/l10n/lila_ga.arb b/lib/l10n/lila_ga.arb index 4ec0db6c6b..d765fa7f66 100644 --- a/lib/l10n/lila_ga.arb +++ b/lib/l10n/lila_ga.arb @@ -22,6 +22,25 @@ "activityJoinedNbTeams": "{count, plural, =1{Isteach i bhfoireann amháin} =2{Isteach i {count} fhoireann} few{Isteach i {count} bhfoireann} many{Isteach i {count} foireann} other{Isteach i {count} foireann}}", "broadcastBroadcasts": "Craoltaí", "broadcastLiveBroadcasts": "Craoltaí beo comórtais", + "broadcastNewBroadcast": "Craoladh beo nua", + "broadcastAddRound": "Cuir babhta leis", + "broadcastOngoing": "Leanúnach", + "broadcastUpcoming": "Le teacht", + "broadcastCompleted": "Críochnaithe", + "broadcastRoundName": "Ainm babhta", + "broadcastRoundNumber": "Uimhir bhabhta", + "broadcastTournamentName": "Ainm comórtas", + "broadcastTournamentDescription": "Cur síos gairid ar an gcomórtas", + "broadcastFullDescription": "Cur síos iomlán ar an ócáid", + "broadcastFullDescriptionHelp": "Cur síos fada roghnach ar an craoladh. Tá {param1} ar fáil. Caithfidh an fad a bheith níos lú ná {param2} carachtar.", + "broadcastSourceUrlHelp": "URL a seiceálfaidh Lichess chun PGN nuashonruithe a fháil. Caithfidh sé a bheith le féiceáil go poiblí ón Idirlíon.", + "broadcastStartDateHelp": "Roghnach, má tá a fhios agat cathain a thosóidh an ócáid", + "broadcastCurrentGameUrl": "URL cluiche reatha", + "broadcastDownloadAllRounds": "Íoslódáil gach babhta", + "broadcastResetRound": "Athshocraigh an babhta seo", + "broadcastDeleteRound": "Scrios an babhta seo", + "broadcastDefinitivelyDeleteRound": "Scrios go cinntitheach an babhta agus a chuid cluichí.", + "broadcastDeleteAllGamesOfThisRound": "Scrios gach cluiche den bhabhta seo. Caithfidh an fhoinse a bheith gníomhach chun iad a athchruthú.", "challengeChallengeToPlay": "Dúshlán cluiche", "challengeChallengeDeclined": "Dúshlán diúltaithe", "challengeChallengeAccepted": "Dúshlán glactha!", @@ -328,8 +347,8 @@ "puzzleThemeXRayAttackDescription": "Déanann píosa ionsaí nó cosaint ar chearnóg, trí phíosa namhaid.", "puzzleThemeZugzwang": "Zugzwang", "puzzleThemeZugzwangDescription": "Ciallaíonn Zugzwang gur gá le himreoir a s(h) eans a \nthógáil cé nár mhaith leis nó léi toisc gur laige a bheith a s(h) uíomh cibé beart a dhéanfaidh sé/sí. Ba mhaith leis / léi \"háram\" a rá ach níl sé sin ceadaithe.", - "puzzleThemeHealthyMix": "Meascán sláintiúil", - "puzzleThemeHealthyMixDescription": "Giota de gach rud. Níl a fhios agat cad tá os do comhair, mar sin fanann tú réidh le haghaidh athan bith! Díreach mar atá i gcluichí fíor.", + "puzzleThemeMix": "Meascán sláintiúil", + "puzzleThemeMixDescription": "Giota de gach rud. Níl a fhios agat cad tá os do comhair, mar sin fanann tú réidh le haghaidh athan bith! Díreach mar atá i gcluichí fíor.", "puzzleThemePlayerGames": "Cluichí imreoir", "puzzleThemePlayerGamesDescription": "Cuardaigh fadhbanna a ghintear ó do chluichí, nó ó chluichí imreoir eile.", "puzzleThemePuzzleDownloadInformation": "Tá na fadhbanna seo i mbéal an phobail, agus is féidir iad a íoslódáil ó {param}.", @@ -744,7 +763,6 @@ "cheat": "Caimiléir", "troll": "Troll", "other": "Eile", - "reportDescriptionHelp": "Greamaigh an nasc chuig an gcluiche/na cluichí agus mínigh cad atá cearr le hiompar an úsáideora. Ná habair go díreach go mbíonn \"caimiléireacht\" ar bun acu, ach inis dúinn faoin dóigh a fuair tú amach faoi. Faraor, déanfar do thuairisc a phróiseáil níos tapúla más i mBéarla atá sé.", "error_provideOneCheatedGameLink": "Cuir nasc ar fáil chuig cluiche amháin ar a laghad ar tharla caimiléireacht ann le do thoil.", "by": "ó{param}", "importedByX": "Iompórtáilte ag {param}", @@ -1203,6 +1221,158 @@ "stormXRuns": "{count, plural, =1{1 stríocáin} =2{{count} stríocáin} few{{count} stríocáin} many{{count} stríocáin} other{{count} stríocáin}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{D'imir stríocáin amháin de {param2}} =2{D'imir {count} stríocáin de {param2}} few{D'imir {count} stríocáin de {param2}} many{D'imir {count} stríocáin de {param2}} other{D'imir {count} stríocáin de {param2}}}", "streamerLichessStreamers": "Sruthaithe Lichess", + "studyPrivate": "Cé na daoine! Tá an leathanach seo príobháideach, ní féidir leat é a rochtain", + "studyMyStudies": "Mo chuid staidéir", + "studyStudiesIContributeTo": "Staidéir atá á n-iarraidh agam", + "studyMyPublicStudies": "Mo chuid staidéir phoiblí", + "studyMyPrivateStudies": "Mo chuid staidéir phríobháideacha", + "studyMyFavoriteStudies": "Na staidéir is fearr liom", + "studyWhatAreStudies": "Cad is staidéir ann?", + "studyAllStudies": "Gach staidéar", + "studyStudiesCreatedByX": "Staidéir a chruthaigh {param}", + "studyNoneYet": "Níl aon cheann fós.", + "studyHot": "Te", + "studyDateAddedNewest": "Dáta curtha leis (dáta is déanaí)", + "studyDateAddedOldest": "Dáta curtha leis (dáta is sinne)", + "studyRecentlyUpdated": "Faisnéis nuashonraithe le déanaí", + "studyMostPopular": "Móréilimh", + "studyAlphabetical": "Aibítre", + "studyAddNewChapter": "Cuir caibidil nua leis", + "studyAddMembers": "Cuir baill leis", + "studyInviteToTheStudy": "Tabhair cuireadh don staidéar", + "studyPleaseOnlyInvitePeopleYouKnow": "Ná tabhair cuireadh ach do dhaoine a bhfuil aithne agat orthu, agus ar mian leo go gníomhach a bheith páirteach sa staidéar seo.", + "studySearchByUsername": "Cuardaigh de réir ainm úsáideora", + "studySpectator": "Breathnóir", + "studyContributor": "Rannpháirtí", + "studyKick": "Ciceáil", + "studyLeaveTheStudy": "Fág an staidéar", + "studyYouAreNowAContributor": "Is ranníocóir anois tú", + "studyYouAreNowASpectator": "Is lucht féachana anois tú", + "studyPgnTags": "Clibeanna PGN", + "studyLike": "Is maith liom", + "studyUnlike": "Díthogh", + "studyNewTag": "Clib nua", + "studyCommentThisPosition": "Déan trácht ar an suíomh seo", + "studyCommentThisMove": "Déan trácht ar an mbeart seo", + "studyAnnotateWithGlyphs": "Nodaireacht le glifeanna", + "studyTheChapterIsTooShortToBeAnalysed": "Tá an chaibidil ró-ghearr le hanailís a dhéanamh uirthi.", + "studyOnlyContributorsCanRequestAnalysis": "Ní féidir ach le rannpháirtithe an staidéir anailís ríomhaire a iarraidh.", + "studyGetAFullComputerAnalysis": "Faigh anailís ríomhaire iomlán ón freastalaí ar an bpríomhlíne.", + "studyMakeSureTheChapterIsComplete": "Bí cinnte go bhfuil an chaibidil críochnaithe. Ní féidir leat iarr ar anailís ach uair amháin.", + "studyAllSyncMembersRemainOnTheSamePosition": "Fanann gach ball SYNC sa suíomh céanna", + "studyShareChanges": "Roinn athruithe le lucht féachana agus sábháil iad ar an freastalaí", + "studyPlaying": "Ag imirt", + "studyFirst": "Céad", + "studyPrevious": "Roimhe", + "studyNext": "Ar aghaidh", + "studyLast": "Deiridh", "studyShareAndExport": "Comhroinn & easpórtáil", - "studyStart": "Tosú" + "studyCloneStudy": "Déan cóip", + "studyStudyPgn": "Déan staidéar ar PGN", + "studyDownloadAllGames": "Íoslódáil gach cluiche", + "studyChapterPgn": "PGN caibidle", + "studyCopyChapterPgn": "Cóipeáil PGN", + "studyDownloadGame": "Íoslódáil cluiche", + "studyStudyUrl": "URL an staidéir", + "studyCurrentChapterUrl": "URL caibidil reatha", + "studyYouCanPasteThisInTheForumToEmbed": "Is féidir é seo a ghreamú san fhóram chun leabú", + "studyStartAtInitialPosition": "Tosaigh ag an suíomh tosaigh", + "studyStartAtX": "Tosú ag {param}", + "studyEmbedInYourWebsite": "Leabaithe i do shuíomh Gréasáin nó i do bhlag", + "studyReadMoreAboutEmbedding": "Léigh tuilleadh faoi leabú", + "studyOnlyPublicStudiesCanBeEmbedded": "Ní féidir ach staidéir phoiblí a leabú!", + "studyOpen": "Oscailte", + "studyXBroughtToYouByY": "{param1}, a thugann {param2} chugat", + "studyStudyNotFound": "Níor aimsíodh staidéar", + "studyEditChapter": "Cuir caibidil in eagar", + "studyNewChapter": "Caibidil nua", + "studyImportFromChapterX": "Iompórtáil ó {param}", + "studyOrientation": "Treoshuíomh", + "studyAnalysisMode": "Modh anailíse", + "studyPinnedChapterComment": "Trácht caibidil greamaithe", + "studySaveChapter": "Sábháil caibidil", + "studyClearAnnotations": "Glan anótála", + "studyClearVariations": "Glan éagsúlachtaí", + "studyDeleteChapter": "Scrios caibidil", + "studyDeleteThisChapter": "Scrios an chaibidil seo? Níl aon dul ar ais!", + "studyClearAllCommentsInThisChapter": "Glan gach trácht, glif agus cruthanna tarraingthe sa chaibidil seo?", + "studyRightUnderTheBoard": "Díreach faoin gclár", + "studyNoPinnedComment": "Faic", + "studyNormalAnalysis": "Gnáth-anailís", + "studyHideNextMoves": "Folaigh na bearta ina dhiaidh seo", + "studyInteractiveLesson": "Ceacht idirghníomhach", + "studyChapterX": "Caibidil {param}", + "studyEmpty": "Folamh", + "studyStartFromInitialPosition": "Tosaigh ón suíomh tosaigh", + "studyEditor": "Eagarthóir", + "studyStartFromCustomPosition": "Tosaigh ón suíomh saincheaptha", + "studyLoadAGameByUrl": "Lód cluichí le URLanna", + "studyLoadAPositionFromFen": "Luchtaigh suíomh ó FEN", + "studyLoadAGameFromPgn": "Lódáil cluichí ó PGN", + "studyAutomatic": "Uathoibríoch", + "studyUrlOfTheGame": "URL na gcluichí, ceann amháin an líne", + "studyLoadAGameFromXOrY": "Lódáil cluichí ó {param1} nó {param2}", + "studyCreateChapter": "Cruthaigh caibidil", + "studyCreateStudy": "Cruthaigh staidéar", + "studyEditStudy": "Cuir staidéar in eagar", + "studyVisibility": "Infheictheacht", + "studyPublic": "Poiblí", + "studyUnlisted": "Neamhliostaithe", + "studyInviteOnly": "Tabhair cuireadh amháin", + "studyAllowCloning": "Lig clónáil", + "studyNobody": "Níl einne", + "studyOnlyMe": "Mise amháin", + "studyContributors": "Rannpháirtithe", + "studyMembers": "Baill", + "studyEveryone": "Gach duine", + "studyEnableSync": "Cuir sinc ar chumas", + "studyYesKeepEveryoneOnTheSamePosition": "Cinnte: coinnigh gach duine ar an suíomh céanna", + "studyNoLetPeopleBrowseFreely": "Na déan: lig do dhaoine brabhsáil go saor", + "studyPinnedStudyComment": "Trácht staidéir greamaithe", + "studyStart": "Tosú", + "studySave": "Sábháil", + "studyClearChat": "Glan comhrá", + "studyDeleteTheStudyChatHistory": "Scrios an stair comhrá staidéir? Níl aon dul ar ais!", + "studyDeleteStudy": "Scrios an staidéar", + "studyConfirmDeleteStudy": "Scrios an staidéar iomlán? Níl aon dul ar ais! Clóscríobh ainm an staidéar le deimhniú: {param}", + "studyWhereDoYouWantToStudyThat": "Cá háit ar mhaith leat staidéar a dhéanamh air sin?", + "studyGoodMove": "Beart maith", + "studyMistake": "Botún", + "studyBrilliantMove": "Beart iontach", + "studyBlunder": "Botún", + "studyInterestingMove": "Beart suimiúil", + "studyDubiousMove": "Beart amhrasach", + "studyOnlyMove": "Beart dleathach", + "studyZugzwang": "Zugzwang", + "studyEqualPosition": "Suíomh cothrom", + "studyUnclearPosition": "Suíomh doiléir", + "studyWhiteIsSlightlyBetter": "Tá bán píosa beag níos fearr", + "studyBlackIsSlightlyBetter": "Tá dubh píosa beag níos fearr", + "studyWhiteIsBetter": "Tá bán níos fearr", + "studyBlackIsBetter": "Tá dubh níos fearr", + "studyWhiteIsWinning": "Bán ag bua", + "studyBlackIsWinning": "Dubh ag bua", + "studyNovelty": "Nuaga", + "studyDevelopment": "Forbairt", + "studyInitiative": "Tionscnamh", + "studyAttack": "Ionsaí", + "studyCounterplay": "Frithimirt", + "studyTimeTrouble": "Trioblóid ama", + "studyWithCompensation": "Le cúiteamh", + "studyWithTheIdea": "Le smaoineamh", + "studyNextChapter": "Céad chaibidil eile", + "studyPrevChapter": "Caibidil roimhe seo", + "studyStudyActions": "Déan staidéar ar ghníomhartha", + "studyTopics": "Topaicí", + "studyMyTopics": "Mo thopaicí", + "studyPopularTopics": "Topaicí choitianta", + "studyManageTopics": "Bainistigh topaicí", + "studyBack": "Siar", + "studyPlayAgain": "Imir arís", + "studyWhatWouldYouPlay": "Cad a dhéanfá sa suíomh seo?", + "studyYouCompletedThisLesson": "Comhghairdeas! Chríochnaigh tú an ceacht seo.", + "studyNbChapters": "{count, plural, =1{{count} Caibidil} =2{{count} Chaibidil} few{{count} gCaibidil} many{{count} Caibidil} other{{count} Caibidil}}", + "studyNbGames": "{count, plural, =1{{count} Cluiche} =2{{count} Chluiche} few{{count} gCluiche} many{{count} Cluiche} other{{count} Cluiche}}", + "studyNbMembers": "{count, plural, =1{{count} Comhalta} =2{{count} Chomhalta} few{{count} gComhalta} many{{count} Comhalta} other{{count} Comhalta}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Greamaigh do théacs PGN anseo, suas le {count} cluiche} =2{Greamaigh do théacs PGN anseo, suas le {count} chluiche} few{Greamaigh do théacs PGN anseo, suas le {count} gcluiche} many{Greamaigh do théacs PGN anseo, suas le {count} cluiche} other{Greamaigh do théacs PGN anseo, suas le {count} cluiche}}" } \ No newline at end of file diff --git a/lib/l10n/lila_gl.arb b/lib/l10n/lila_gl.arb index fd63ec9811..62c07d440e 100644 --- a/lib/l10n/lila_gl.arb +++ b/lib/l10n/lila_gl.arb @@ -15,7 +15,7 @@ "mobileAllGames": "Todas as partidas", "mobileRecentSearches": "Procuras recentes", "mobileClearButton": "Borrar", - "mobilePlayersMatchingSearchTerm": "Xogadores con \"{param}\"", + "mobilePlayersMatchingSearchTerm": "O nome de usuario contén \"{param}\"", "mobileNoSearchResults": "Sen resultados", "mobileAreYouSure": "Estás seguro?", "mobilePuzzleStreakAbortWarning": "Perderás a túa secuencia actual e o teu resultado gardarase.", @@ -30,7 +30,6 @@ "mobilePuzzleStormConfirmEndRun": "Queres rematar esta quenda?", "mobilePuzzleStormFilterNothingToShow": "Non aparece nada. Por favor, cambia os filtros", "mobileCancelTakebackOffer": "Cancelar a proposta de cambio", - "mobileCancelDrawOffer": "Cancelar a oferta de táboas", "mobileWaitingForOpponentToJoin": "Agardando un rival...", "mobileBlindfoldMode": "Á cega", "mobileLiveStreamers": "Presentadores en directo", @@ -42,6 +41,7 @@ "mobilePuzzleStormSubtitle": "Resolve tantos crebacabezas como sexa posible en 3 minutos.", "mobileGreeting": "Ola, {param}", "mobileGreetingWithoutName": "Ola", + "mobilePrefMagnifyDraggedPiece": "Ampliar a peza arrastrada", "activityActivity": "Actividade", "activityHostedALiveStream": "Emitiu en directo", "activityRankedInSwissTournament": "{param1}º na clasificación de {param2}", @@ -54,6 +54,7 @@ "activityPlayedNbMoves": "{count, plural, =1{Xogou {count} movemento} other{Xogou {count} movementos}}", "activityInNbCorrespondenceGames": "{count, plural, =1{en {count} partida por correspondencia} other{en {count} partidas por correspondencia}}", "activityCompletedNbGames": "{count, plural, =1{Xogou {count} partida por correspondencia} other{Xogou {count} partidas por correspondencia}}", + "activityCompletedNbVariantGames": "{count, plural, =1{Finalizou {count} partida {param2} por correspondencia} other{Finalizou {count} partidas {param2} por correspondencia}}", "activityFollowedNbPlayers": "{count, plural, =1{Comezou a seguir a {count} xogador} other{Comezou a seguir a {count} xogadores}}", "activityGainedNbFollowers": "{count, plural, =1{Gañou {count} novo seguidor} other{Gañou {count} novos seguidores}}", "activityHostedNbSimuls": "{count, plural, =1{Ofreceu {count} exhibición simultánea} other{Ofreceu {count} exhibicións simultáneas}}", @@ -64,7 +65,72 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{Competiu en {count} torneo suízo} other{Competiu en {count} torneos suízos}}", "activityJoinedNbTeams": "{count, plural, =1{Uniuse a {count} equipo} other{Uniuse a {count} equipos}}", "broadcastBroadcasts": "Emisións en directo", + "broadcastMyBroadcasts": "As miñas emisións", "broadcastLiveBroadcasts": "Emisións de torneos en directo", + "broadcastBroadcastCalendar": "Calendario de emisións", + "broadcastNewBroadcast": "Nova emisión en directo", + "broadcastSubscribedBroadcasts": "Emisións subscritas", + "broadcastAboutBroadcasts": "Sobre as retransmisións", + "broadcastHowToUseLichessBroadcasts": "Como usar as Retransmisións de Lichess.", + "broadcastTheNewRoundHelp": "A nova rolda terá os mesmos membros e colaboradores cá rolda anterior.", + "broadcastAddRound": "Engadir unha rolda", + "broadcastOngoing": "En curso", + "broadcastUpcoming": "Proximamente", + "broadcastCompleted": "Completadas", + "broadcastCompletedHelp": "Malia que Lichess detecta o final das roldas, pódese equivocar. Usa esta opción para facelo manualmente.", + "broadcastRoundName": "Nome da rolda", + "broadcastRoundNumber": "Número de rolda", + "broadcastTournamentName": "Nome do torneo", + "broadcastTournamentDescription": "Breve descrición do torneo", + "broadcastFullDescription": "Descrición completa do torneo", + "broadcastFullDescriptionHelp": "Descrición longa opcional do torneo. {param1} está dispoñíbel. A lonxitude debe ser menor de {param2} caracteres.", + "broadcastSourceSingleUrl": "URL de orixe do arquivo PGN", + "broadcastSourceUrlHelp": "Ligazón que Lichess comprobará para obter actualizacións dos PGN. Debe ser publicamente accesíbel desde a Internet.", + "broadcastSourceGameIds": "Até 64 identificadores de partidas de Lichess, separados por espazos.", + "broadcastStartDateTimeZone": "Data de inicio do torneo (na zona horaria local): {param}", + "broadcastStartDateHelp": "Opcional, se sabes cando comeza o evento", + "broadcastCurrentGameUrl": "Ligazón da partida actual", + "broadcastDownloadAllRounds": "Descargar todas as roldas", + "broadcastResetRound": "Restablecer esta rolda", + "broadcastDeleteRound": "Borrar esta rolda", + "broadcastDefinitivelyDeleteRound": "Eliminar definitivamente a rolda e todas as súas partidas.", + "broadcastDeleteAllGamesOfThisRound": "Eliminar todas as partidas desta rolda. A transmisión en orixe terá que estar activa para volver crealas.", + "broadcastEditRoundStudy": "Editar o estudo da rolda", + "broadcastDeleteTournament": "Eliminar este torneo", + "broadcastDefinitivelyDeleteTournament": "Eliminar o torneo de forma definitiva, con todas as súas roldas e partidas.", + "broadcastShowScores": "Amosar os puntos dos xogadores en función dos resultados das partidas", + "broadcastReplacePlayerTags": "Opcional: substituír os nomes dos xogadores, as puntuacións e os títulos", + "broadcastFideFederations": "Federacións FIDE", + "broadcastTop10Rating": "Media do top 10", + "broadcastFidePlayers": "Xogadores FIDE", + "broadcastFidePlayerNotFound": "Xogador FIDE non atopado", + "broadcastFideProfile": "Perfil FIDE", + "broadcastFederation": "Federación", + "broadcastAgeThisYear": "Idade actual", + "broadcastUnrated": "Sen puntuar", + "broadcastRecentTournaments": "Torneos recentes", + "broadcastOpenLichess": "Abrir en Lichess", + "broadcastTeams": "Equipos", + "broadcastBoards": "Taboleiros", + "broadcastOverview": "Visión de conxunto", + "broadcastSubscribeTitle": "Subscríbete para ser notificado ó comezo de cada rolda. Podes activar/desactivar o son das notificacións ou as notificacións emerxentes para as emisións en directo nas preferencias da túa conta.", + "broadcastUploadImage": "Subir a imaxe do torneo", + "broadcastNoBoardsYet": "Aínda non hai taboleiros. Aparecerán cando se suban as partidas.", + "broadcastBoardsCanBeLoaded": "Os taboleiros pódense cargar dende a fonte ou a través da {param}", + "broadcastStartsAfter": "Comeza en {param}", + "broadcastStartVerySoon": "A emisión comeza decontado.", + "broadcastNotYetStarted": "A emisión aínda non comezou.", + "broadcastOfficialWebsite": "Páxina web oficial", + "broadcastStandings": "Clasificación", + "broadcastIframeHelp": "Máis opcións na {param}", + "broadcastWebmastersPage": "páxina do administrador web", + "broadcastPgnSourceHelp": "Unha fonte dos PGN pública e en tempo real para esta rolda. Tamén ofrecemos unha {param} para unha sincronización máis rápida e eficiente.", + "broadcastEmbedThisBroadcast": "Incrustar esta emisión na túa páxina web", + "broadcastEmbedThisRound": "Incrustar a {param} na túa páxina web", + "broadcastRatingDiff": "Diferenza de puntuación", + "broadcastGamesThisTournament": "Partidas neste torneo", + "broadcastScore": "Resultado", + "broadcastNbBroadcasts": "{count, plural, =1{{count} emisión} other{{count} emisións}}", "challengeChallengesX": "Desafíos: {param1}", "challengeChallengeToPlay": "Desafía a unha partida", "challengeChallengeDeclined": "Desafío rexeitado", @@ -137,7 +203,7 @@ "preferencesZenMode": "Modo zen", "preferencesShowPlayerRatings": "Amosar a puntuación dos xogadores", "preferencesShowFlairs": "Amosar as habelencias dos xogadores", - "preferencesExplainShowPlayerRatings": "Isto permite ocultar todas as puntuacións do sitio, para axudar a centrarse no xadrez. As partidas poden ser puntuadas, isto só afecta ó que podes ver.", + "preferencesExplainShowPlayerRatings": "Oculta todas as puntuacións de Lichess para axudar a centrarse no xadrez. As partidas poden ser puntuadas, isto só afecta ó que podes ver.", "preferencesDisplayBoardResizeHandle": "Mostrar o control de redimensionamento do taboleiro", "preferencesOnlyOnInitialPosition": "Só na posición inicial", "preferencesInGameOnly": "Só durante a partida", @@ -147,7 +213,7 @@ "preferencesHorizontalGreenProgressBars": "Barras horizontais de progreso verdes", "preferencesSoundWhenTimeGetsCritical": "Aviso cando se esgota o tempo", "preferencesGiveMoreTime": "Dar máis tempo", - "preferencesGameBehavior": "Comportamento do xogo", + "preferencesGameBehavior": "Comportamento durante a partida", "preferencesHowDoYouMovePieces": "Como moves as pezas?", "preferencesClickTwoSquares": "Premendo na casa de orixe e despois na de destino", "preferencesDragPiece": "Arrastrando a peza", @@ -167,7 +233,7 @@ "preferencesConfirmResignationAndDrawOffers": "Confirmar abandono e ofertas de táboas", "preferencesCastleByMovingTheKingTwoSquaresOrOntoTheRook": "Método de enroque", "preferencesCastleByMovingTwoSquares": "Movendo o rei dúas casas", - "preferencesCastleByMovingOntoTheRook": "Movendo rei ata a torre", + "preferencesCastleByMovingOntoTheRook": "Movendo o rei cara a torre", "preferencesInputMovesWithTheKeyboard": "Introdución de movementos co teclado", "preferencesInputMovesWithVoice": "Introdución de xogadas coa voz", "preferencesSnapArrowsToValidMoves": "Adherir frechas a movementos válidos", @@ -242,7 +308,7 @@ "puzzleStrengths": "Puntos fortes", "puzzleHistory": "Historial de crebacabezas", "puzzleSolved": "resoltos", - "puzzleFailed": "fallado", + "puzzleFailed": "incorrecto", "puzzleStreakDescription": "Soluciona exercicios cada vez máis difíciles e consigue unha secuencia de vitorias. Non hai conta atrás, así que podes ir amodo. Se te equivocas nun só movemento, acabouse! Pero lembra que podes omitir unha xogada por sesión.", "puzzleYourStreakX": "A túa secuencia de vitorias: {param}", "puzzleStreakSkipExplanation": "Omite este movemento para conservar a túa secuencia. Só se pode facer unha vez por sesión.", @@ -303,7 +369,7 @@ "puzzleThemeDeflection": "Desviación", "puzzleThemeDeflectionDescription": "Un movemento que distrae unha peza rival dunha tarefa que desempeña, como a protección dunha casa chave. Ás veces denomínase \"sobrecarga\".", "puzzleThemeDiscoveredAttack": "Ataque descuberto", - "puzzleThemeDiscoveredAttackDescription": "Apartar unha peza que previamente bloqueaba o ataque doutra peza de longo alcance (por exemplo un cabalo fóra do camiño dunha torre).", + "puzzleThemeDiscoveredAttackDescription": "Apartar unha peza (por exemplo un cabalo) que previamente bloqueaba o ataque doutra peza de longo alcance (por exemplo unha torre).", "puzzleThemeDoubleCheck": "Xaque dobre", "puzzleThemeDoubleCheckDescription": "Xaque con dúas pezas á vez, como resultado dun ataque descuberto onde tanto a peza en movemento como a desvelada atacan ao rei do opoñente.", "puzzleThemeEndgame": "Final", @@ -383,8 +449,8 @@ "puzzleThemeXRayAttackDescription": "Unha peza ataca ou defende un escaque a través dunha peza do opoñente.", "puzzleThemeZugzwang": "Zugzwang", "puzzleThemeZugzwangDescription": "O rival ten os movementos limitados e calquera xogada que faga empeora a súa posición.", - "puzzleThemeHealthyMix": "Mestura equilibrada", - "puzzleThemeHealthyMixDescription": "Un pouco de todo. Non sabes que vai vir, así que prepárate para calquera cousa! Coma nas partidas de verdade.", + "puzzleThemeMix": "Mestura equilibrada", + "puzzleThemeMixDescription": "Un pouco de todo. Non sabes que vai vir, así que prepárate para calquera cousa! Coma nas partidas de verdade.", "puzzleThemePlayerGames": "Partidas de xogadores", "puzzleThemePlayerGamesDescription": "Busca crebacabezas xerados a partir das túas partidas ou das doutros xogadores.", "puzzleThemePuzzleDownloadInformation": "Estes problemas son de dominio público e poden ser descargados en {param}.", @@ -515,7 +581,6 @@ "memory": "Memoria", "infiniteAnalysis": "Análise infinita", "removesTheDepthLimit": "Elimina o límite de profundidade, e mantén o teu ordenador quente", - "engineManager": "Administrador do motor de análise", "blunder": "Metida de zoca", "mistake": "Erro", "inaccuracy": "Imprecisión", @@ -567,7 +632,7 @@ "changePassword": "Cambiar contrasinal", "changeEmail": "Cambiar correo", "email": "Correo electrónico", - "passwordReset": "Cambiar contrasinal", + "passwordReset": "restablecer o contrasinal", "forgotPassword": "Esqueciches o teu contrasinal?", "error_weakPassword": "Ese contrasinal é extremadamente común e demasiado doado de adiviñar.", "error_namePassword": "Por favor, non uses o teu usuario como contrasinal.", @@ -597,6 +662,7 @@ "rank": "Posición", "rankX": "Posición: {param}", "gamesPlayed": "Partidas xogadas", + "ok": "Ok", "cancel": "Cancelar", "whiteTimeOut": "Acabou o tempo das brancas", "blackTimeOut": "Acabou o tempo das negras", @@ -819,7 +885,9 @@ "cheat": "Trampa", "troll": "Troll", "other": "Outro", - "reportDescriptionHelp": "Pega a ligazón á(s) partida(s) e explica o que é incorrecto no comportamento deste usuario. Non digas só \"fai trampas\", cóntanos como chegaches a esa conclusión. A túa denuncia será procesada máis rapidamente se está escrita en inglés.", + "reportCheatBoostHelp": "Pega a ligazón á(s) partida(s) e explica que ten de malo o comportamento deste usuario. Non digas soamente \"fai trampas\", mais cóntanos como chegaches a esta conclusión.", + "reportUsernameHelp": "Explica que ten de ofensivo este nome de usuario. Non digas soamente \"é ofensivo/inadecuado\". Cóntanos como chegaches a esta conclusión, especialmente se o insulto está camuflado, non está en inglés, é xerga ou é unha referencia histórica ou cultural.", + "reportProcessedFasterInEnglish": "A túa denuncia será procesada máis rápido se está escrita en Inglés.", "error_provideOneCheatedGameLink": "Por favor, incorpora cando menos unha ligazón a unha partida na que se fixeron trampas.", "by": "por {param}", "importedByX": "Importado por {param}", @@ -885,7 +953,7 @@ "error_email_unique": "Este enderezo de correo non é válido ou xa foi empregado", "error_email_different": "Este xa é o teu enderezo de correo electrónico", "error_minLength": "A lonxitude mínima é de {param} caracteres", - "error_maxLength": "A lonxitude máxima é de {param} caracteres", + "error_maxLength": "Debe conter polo menos {param} caracteres", "error_min": "Debe ser maior ou igual que {param}", "error_max": "Debe ser menor ou igual que {param}", "ifRatingIsPlusMinusX": "Se a puntuación é ± {param}", @@ -898,7 +966,7 @@ "blackCastlingKingside": "Negras O-O", "tpTimeSpentPlaying": "Tempo xogando: {param}", "watchGames": "Ver partidas", - "tpTimeSpentOnTV": "Tempo en TV: {param}", + "tpTimeSpentOnTV": "Tempo saíndo en TV: {param}", "watch": "Ver", "videoLibrary": "Videoteca", "streamersMenu": "Presentadores", @@ -924,14 +992,14 @@ "aboutSimul": "As simultáneas son partidas dun xogador contra varios ó mesmo tempo.", "aboutSimulImage": "De 50 rivais, Fischer gañou 47 partidas, empatou 2 e perdeu 1.", "aboutSimulRealLife": "O concepto tómase de eventos reais. Nestes, o anfitrión das simultáneas móvese de mesa en mesa e fai unha xogada de cada vez.", - "aboutSimulRules": "Cando as simultáneas comezan, cada xogador inicia unha partida contra o anfitrión. As simultáneas finalizan cando todas as partidas rematan.", + "aboutSimulRules": "Cando comezan as simultáneas, cada xogador inicia unha partida co anfitrión. As simultáneas finalizan cando se completan todas as partidas.", "aboutSimulSettings": "As simultáneas son sempre amigables. As opcións de desquite, de desfacer a xogada e de engadir tempo non están activadas.", "create": "Crear", "whenCreateSimul": "Cando creas unha exhibición de simultáneas, tes que enfrontarte a varios xogadores ó mesmo tempo.", "simulVariantsHint": "Se seleccionas distintas variantes, cada xogador pode escoller cal xogar.", "simulClockHint": "Modo Fischer de reloxo. Cantos máis xogadores, máis tempo necesitas.", "simulAddExtraTime": "Podes engadir tempo extra ó teu reloxo para axudarte coas simultáneas.", - "simulHostExtraTime": "Tempo extra para o anfitrión", + "simulHostExtraTime": "Tempo inicial extra para o anfitrión", "simulAddExtraTimePerPlayer": "Engadir tempo inicial ao teu reloxo por cada xogador que se una ás simultáneas.", "simulHostExtraTimePerPlayer": "Tempo extra do anfitrión por cada xogador", "lichessTournaments": "Torneos de Lichess", @@ -955,7 +1023,7 @@ "toggleVariationArrows": "Activar/desactivar as frechas das variantes", "cyclePreviousOrNextVariation": "Variante anterior/seguinte", "toggleGlyphAnnotations": "Activar/desactivar as anotacións con símbolos", - "togglePositionAnnotations": "Alternar anotaciones de posición", + "togglePositionAnnotations": "Activar/desactivar as anotacións", "variationArrowsInfo": "As frechas das variantes permítenche navegar sen usar a lista de movementos.", "playSelectedMove": "facer a xogada seleccionada", "newTournament": "Novo torneo", @@ -986,7 +1054,7 @@ "downloadRaw": "Descargar sen anotar", "downloadImported": "Descargar importadas", "crosstable": "Táboa de cruces", - "youCanAlsoScrollOverTheBoardToMoveInTheGame": "Tamén podes usar a roda do rato sobre o taboleiro para moverte pola partida.", + "youCanAlsoScrollOverTheBoardToMoveInTheGame": "Usa a roda do rato sobre o taboleiro para moverte pola partida.", "scrollOverComputerVariationsToPreviewThem": "Pasa o punteiro sobre as variantes da computadora para visualizalas.", "analysisShapesHowTo": "Pulsa Maiúsculas+clic ou preme o botón dereito do rato para debuxar círculos e frechas no taboleiro.", "letOtherPlayersMessageYou": "Permitir que outros xogadores che envíen mensaxes", @@ -1166,7 +1234,7 @@ "cancelTournament": "Cancelar o torneo", "tournDescription": "Descrición do torneo", "tournDescriptionHelp": "Queres dicirlles algo en especial ós participantes? Tenta ser breve. Hai dispoñibles ligazóns de Markdown: [name](https://url)", - "ratedFormHelp": "As partidas son puntuadas\ne afectan ás puntuacións dos xogadores", + "ratedFormHelp": "As partidas son puntuadas e afectan ás puntuacións dos xogadores", "onlyMembersOfTeam": "Só membros do equipo", "noRestriction": "Sen restrición", "minimumRatedGames": "Mínimo de partidas puntuadas", @@ -1209,7 +1277,7 @@ "youCantStartNewGame": "Non podes comezar unha nova partida ata que esta remate.", "since": "Desde", "until": "Ata", - "lichessDbExplanation": "Partidas puntuadas de todos os xogadores de Lichess", + "lichessDbExplanation": "Partidas puntuadas xogadas en Lichess", "switchSides": "Cambiar de cor", "closingAccountWithdrawAppeal": "Se pechas a túa conta, retirarás a túa apelación", "ourEventTips": "Os nosos consellos para organizar eventos", @@ -1217,6 +1285,7 @@ "showMeEverything": "Amósamo todo", "lichessPatronInfo": "Lichess é unha organización benéfica e un programa totalmente libre e de código aberto.\nTodos os custos de funcionamento, desenvolvemento e contidos fináncianse unicamente mediante as doazóns dos usuarios.", "nothingToSeeHere": "Nada que ver aquí polo de agora.", + "stats": "Estatísticas", "opponentLeftCounter": "{count, plural, =1{O teu opoñente saíu da partida. Poderás reclamar a vitoria en {count} segundo.} other{O teu opoñente saíu da partida. Poderás reclamar a vitoria en {count} segundos.}}", "mateInXHalfMoves": "{count, plural, =1{Mate en {count} media xogada} other{Mate en {count} medias xogadas}}", "nbBlunders": "{count, plural, =1{{count} metida de zoca} other{{count} metidas de zoca}}", @@ -1313,6 +1382,159 @@ "stormXRuns": "{count, plural, =1{1 quenda} other{{count} quendas}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Xogou unha quenda de {param2}} other{Xogou {count} quendas de {param2}}}", "streamerLichessStreamers": "Presentadores de Lichess", + "studyPrivate": "Privado", + "studyMyStudies": "Os meus estudos", + "studyStudiesIContributeTo": "Estudos nos que contribúo", + "studyMyPublicStudies": "Os meus estudos públicos", + "studyMyPrivateStudies": "Os meus estudos privados", + "studyMyFavoriteStudies": "Os meus estudos favoritos", + "studyWhatAreStudies": "Que son os estudos?", + "studyAllStudies": "Todos os estudos", + "studyStudiesCreatedByX": "Estudos creados por {param}", + "studyNoneYet": "Aínda non hai.", + "studyHot": "Candentes", + "studyDateAddedNewest": "Data engadida (máis novos)", + "studyDateAddedOldest": "Data engadida (máis antigos)", + "studyRecentlyUpdated": "Actualizados recentemente", + "studyMostPopular": "Máis populares", + "studyAlphabetical": "Alfabeticamente", + "studyAddNewChapter": "Engadir un novo capítulo", + "studyAddMembers": "Engadir membros", + "studyInviteToTheStudy": "Invitar ao estudo", + "studyPleaseOnlyInvitePeopleYouKnow": "Por favor, convida só a persoas que te coñezan e que desexen activamente unirse a este estudo.", + "studySearchByUsername": "Buscar por nome de usuario", + "studySpectator": "Espectador", + "studyContributor": "Colaborador", + "studyKick": "Expulsar", + "studyLeaveTheStudy": "Deixar o estudo", + "studyYouAreNowAContributor": "Agora es un colaborador", + "studyYouAreNowASpectator": "Agora es un espectador", + "studyPgnTags": "Etiquetas PGN", + "studyLike": "Gústame", + "studyUnlike": "Xa non me gusta", + "studyNewTag": "Nova etiqueta", + "studyCommentThisPosition": "Comentar nesta posición", + "studyCommentThisMove": "Comentar este movemento", + "studyAnnotateWithGlyphs": "Anotar con símbolos", + "studyTheChapterIsTooShortToBeAnalysed": "O capítulo é demasiado curto para analizalo.", + "studyOnlyContributorsCanRequestAnalysis": "Só os colaboradores do estudo poden solicitar unha análise por ordenador.", + "studyGetAFullComputerAnalysis": "Obtén unha análise completa da liña principal dende o servidor.", + "studyMakeSureTheChapterIsComplete": "Asegúrate de que o capítulo está completo. Só podes solicitar a análise unha vez.", + "studyAllSyncMembersRemainOnTheSamePosition": "Todos os membros sincronizados permanecen na mesma posición", + "studyShareChanges": "Comparte os cambios cos espectadores e gárdaos no servidor", + "studyPlaying": "En xogo", + "studyShowEvalBar": "Indicadores de avaliación", + "studyFirst": "Primeiro", + "studyPrevious": "Anterior", + "studyNext": "Seguinte", + "studyLast": "Último", "studyShareAndExport": "Compartir e exportar", - "studyStart": "Comezar" + "studyCloneStudy": "Clonar", + "studyStudyPgn": "PGN do estudo", + "studyDownloadAllGames": "Descargar todas as partidas", + "studyChapterPgn": "PGN do capítulo", + "studyCopyChapterPgn": "Copiar PGN", + "studyDownloadGame": "Descargar partida", + "studyStudyUrl": "URL do estudo", + "studyCurrentChapterUrl": "Ligazón do capítulo actual", + "studyYouCanPasteThisInTheForumToEmbed": "Podes pegar esta URL no foro ou no teu blog de Lichess para incrustala", + "studyStartAtInitialPosition": "Comezar desde a posición inicial do estudo", + "studyStartAtX": "Comezar en {param}", + "studyEmbedInYourWebsite": "Incrustar na túa páxina web ou blog", + "studyReadMoreAboutEmbedding": "Ler máis sobre como inserir contido", + "studyOnlyPublicStudiesCanBeEmbedded": "Só se poden inserir estudos públicos!", + "studyOpen": "Abrir", + "studyXBroughtToYouByY": "{param1} traído para ti por {param2}", + "studyStudyNotFound": "Estudo non atopado", + "studyEditChapter": "Editar capítulo", + "studyNewChapter": "Novo capítulo", + "studyImportFromChapterX": "Importar de {param}", + "studyOrientation": "Orientación", + "studyAnalysisMode": "Modo de análise", + "studyPinnedChapterComment": "Comentario do capítulo fixado", + "studySaveChapter": "Gardar capítulo", + "studyClearAnnotations": "Borrar anotacións", + "studyClearVariations": "Borrar variantes", + "studyDeleteChapter": "Borrar capítulo", + "studyDeleteThisChapter": "Realmente queres borrar o capítulo? Non hai volta atrás!", + "studyClearAllCommentsInThisChapter": "Borrar todos os comentarios, símbolos e marcas do capítulo", + "studyRightUnderTheBoard": "Xusto debaixo do taboleiro", + "studyNoPinnedComment": "Ningún", + "studyNormalAnalysis": "Análise normal", + "studyHideNextMoves": "Ocultar os seguintes movementos", + "studyInteractiveLesson": "Lección interactiva", + "studyChapterX": "Capítulo {param}", + "studyEmpty": "Baleiro", + "studyStartFromInitialPosition": "Comezar desde a posición inicial", + "studyEditor": "Editor", + "studyStartFromCustomPosition": "Comezar dende unha posición personalizada", + "studyLoadAGameByUrl": "Cargar as partidas dende unha URL", + "studyLoadAPositionFromFen": "Cargar unha posición dende o FEN", + "studyLoadAGameFromPgn": "Cargar as partidas dende o PGN", + "studyAutomatic": "Automática", + "studyUrlOfTheGame": "Ligazóns das partidas, unha por liña", + "studyLoadAGameFromXOrY": "Cargar partidas dende {param1} ou {param2}", + "studyCreateChapter": "Crear capítulo", + "studyCreateStudy": "Crear estudo", + "studyEditStudy": "Editar estudo", + "studyVisibility": "Visibilidade", + "studyPublic": "Público", + "studyUnlisted": "Sen listar", + "studyInviteOnly": "Acceso só mediante invitación", + "studyAllowCloning": "Permitir clonado", + "studyNobody": "Ninguén", + "studyOnlyMe": "Só eu", + "studyContributors": "Colaboradores", + "studyMembers": "Membros", + "studyEveryone": "Todo o mundo", + "studyEnableSync": "Activar sincronización", + "studyYesKeepEveryoneOnTheSamePosition": "Si: todos verán a mesma posición", + "studyNoLetPeopleBrowseFreely": "Non: permitir que a xente navegue libremente", + "studyPinnedStudyComment": "Comentario fixado do estudo", + "studyStart": "Comezar", + "studySave": "Gardar", + "studyClearChat": "Borrar a sala de conversa", + "studyDeleteTheStudyChatHistory": "Borrar o historial da sala de conversa? Esta acción non se pode desfacer!", + "studyDeleteStudy": "Borrar estudo", + "studyConfirmDeleteStudy": "Borrar todo o estudo? Non se poderá recuperar! Teclea o nome do estudo para confirmar: {param}", + "studyWhereDoYouWantToStudyThat": "Onde queres estudar isto?", + "studyGoodMove": "Bo movemento", + "studyMistake": "Erro", + "studyBrilliantMove": "Movemento brillante", + "studyBlunder": "Metida de zoca", + "studyInterestingMove": "Movemento interesante", + "studyDubiousMove": "Movemento dubidoso", + "studyOnlyMove": "Movemento único", + "studyZugzwang": "Zugzwang", + "studyEqualPosition": "Posición igualada", + "studyUnclearPosition": "Posición pouco clara", + "studyWhiteIsSlightlyBetter": "As brancas están lixeiramente mellor", + "studyBlackIsSlightlyBetter": "As negras están lixeiramente mellor", + "studyWhiteIsBetter": "As brancas están mellor", + "studyBlackIsBetter": "As negras están mellor", + "studyWhiteIsWinning": "As brancas están gañando", + "studyBlackIsWinning": "As negras están gañando", + "studyNovelty": "Novidade", + "studyDevelopment": "Desenvolvemento", + "studyInitiative": "Iniciativa", + "studyAttack": "Ataque", + "studyCounterplay": "Contraataque", + "studyTimeTrouble": "Apuros de tempo", + "studyWithCompensation": "Con compensación", + "studyWithTheIdea": "Coa idea", + "studyNextChapter": "Capítulo seguinte", + "studyPrevChapter": "Capítulo anterior", + "studyStudyActions": "Accións de estudo", + "studyTopics": "Temas", + "studyMyTopics": "Os meus temas", + "studyPopularTopics": "Temas populares", + "studyManageTopics": "Administrar temas", + "studyBack": "Voltar", + "studyPlayAgain": "Xogar de novo", + "studyWhatWouldYouPlay": "Que xogarías nesta posición?", + "studyYouCompletedThisLesson": "Parabéns! Completaches esta lección.", + "studyNbChapters": "{count, plural, =1{{count} Capítulo} other{{count} Capítulos}}", + "studyNbGames": "{count, plural, =1{{count} Partida} other{{count} Partidas}}", + "studyNbMembers": "{count, plural, =1{{count} Membro} other{{count} Membros}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Pega o teu texto PGN aquí, ata {count} partida} other{Pega o teu texto PGN aquí, ata {count} partidas}}" } \ No newline at end of file diff --git a/lib/l10n/lila_gsw.arb b/lib/l10n/lila_gsw.arb index eb1c30da21..2bac00b7c3 100644 --- a/lib/l10n/lila_gsw.arb +++ b/lib/l10n/lila_gsw.arb @@ -12,7 +12,7 @@ "mobileSettingsImmersiveMode": "Ibettete Modus", "mobileSettingsImmersiveModeSubtitle": "UI-Syschtem während em schpille usblände. Benutz die Option, wänn dich d'Navigationsgeschte, vum Sysychtem, am Bildschirmrand störed. Das gilt für Schpiel- und Puzzle Storm-Bildschirm.", "mobileNotFollowingAnyUser": "Du folgsch keim Schpiller.", - "mobileAllGames": "Alli Partie", + "mobileAllGames": "All Schpiel", "mobileRecentSearches": "Kürzlich Gsuechts", "mobileClearButton": "Leere", "mobilePlayersMatchingSearchTerm": "Schpiller mit \"{param}%", @@ -30,11 +30,10 @@ "mobilePuzzleStormConfirmEndRun": "Wottsch de Lauf beände?", "mobilePuzzleStormFilterNothingToShow": "Nüt zum Zeige, bitte d'Filter ändere", "mobileCancelTakebackOffer": "Zugsrücknam-Offerte zruggzieh", - "mobileCancelDrawOffer": "Remis-Agebot zruggzieh", "mobileWaitingForOpponentToJoin": "Warte bis en Gegner erschint...", "mobileBlindfoldMode": "Blind schpille", "mobileLiveStreamers": "Live Streamer", - "mobileCustomGameJoinAGame": "Bi ere Partie mitschpille", + "mobileCustomGameJoinAGame": "Es Schpiel mitschpille", "mobileCorrespondenceClearSavedMove": "Lösch die gschpeicherete Züg", "mobileSomethingWentWrong": "Es isch öppis schief gange.", "mobileShowResult": "Resultat zeige", @@ -42,6 +41,7 @@ "mobilePuzzleStormSubtitle": "Lös i 3 Minute so vill Ufgabe wie möglich.", "mobileGreeting": "Hoi, {param}", "mobileGreetingWithoutName": "Hoi", + "mobilePrefMagnifyDraggedPiece": "Vegrösserig vu de zogene Figur", "activityActivity": "Aktivitäte", "activityHostedALiveStream": "Hät en Live Stream gmacht", "activityRankedInSwissTournament": "Hät Rang #{param1} im Turnier {param2} erreicht", @@ -49,11 +49,12 @@ "activitySupportedNbMonths": "{count, plural, =1{Underschtüzt lichess.org sit {count} Monet als {param2}} other{Underschtützt lichess.org sit {count} Mönet als {param2}}}", "activityPracticedNbPositions": "{count, plural, =1{Hät {count} Schtellig bi {param2} güebt} other{Hät {count} Schtellige bi {param2} güebt}}", "activitySolvedNbPuzzles": "{count, plural, =1{Hät {count} Taktikufgab glöst} other{Hät {count} Taktikufgabe glöst}}", - "activityPlayedNbGames": "{count, plural, =1{Hät {count} Partie {param2} gschpillt} other{Hät {count} Partie {param2} gschpillt}}", + "activityPlayedNbGames": "{count, plural, =1{Hät {count} Schpiel {param2} gschpillt} other{Hät {count} Schpiel {param2} gschpillt}}", "activityPostedNbMessages": "{count, plural, =1{Hät {count} Nachricht in {param2} gschribe} other{Hät {count} Nachrichte in {param2} gschribe}}", "activityPlayedNbMoves": "{count, plural, =1{Hät {count} Zug gschpillt} other{Hät {count} Züg gschpillt}}", - "activityInNbCorrespondenceGames": "{count, plural, =1{i {count} Fernschachpartie} other{i {count} Fernschachpartie}}", - "activityCompletedNbGames": "{count, plural, =1{Hät {count} Fernschachpartie gschpillt} other{Hät {count} Fernschachpartie gschpillt}}", + "activityInNbCorrespondenceGames": "{count, plural, =1{in {count} Korrespondänz-Schpiel} other{in {count} Korrespondänz-Schpiel}}", + "activityCompletedNbGames": "{count, plural, =1{Hät {count} Korrespondänz-Schpiel gschpillt} other{Hät {count} Korrespondänz-Schpiel gschpillt}}", + "activityCompletedNbVariantGames": "{count, plural, =1{{count} {param2} Korrespondänz-Schpiel gschpillt} other{{count} {param2} Korrespondänz-Schpiel gschpillt}}", "activityFollowedNbPlayers": "{count, plural, =1{Folgt {count} Schpiller} other{Folgt {count} Schpiller}}", "activityGainedNbFollowers": "{count, plural, =1{Hät {count} neui Folgendi} other{Hät {count} neui Folgendi}}", "activityHostedNbSimuls": "{count, plural, =1{Hät {count} Simultanschach gmacht} other{Hät {count} Simultanschachs gmacht}}", @@ -64,9 +65,74 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{Hät a {count} Turnier nach \"Schweizer System\" teilgnah} other{Hät a {count} Turnier nach \"Schweizer System\" teilgnah}}", "activityJoinedNbTeams": "{count, plural, =1{Isch {count} Team biträte} other{Isch {count} Teams biträte}}", "broadcastBroadcasts": "Überträgige", + "broadcastMyBroadcasts": "Eigeni Überträgige", "broadcastLiveBroadcasts": "Live Turnier-Überträgige", + "broadcastBroadcastCalendar": "Überträgigs-Kaländer", + "broadcastNewBroadcast": "Neui Live-Überträgige", + "broadcastSubscribedBroadcasts": "Abonnierti Überträgige", + "broadcastAboutBroadcasts": "Über Überträgige", + "broadcastHowToUseLichessBroadcasts": "Wie mer Lichess-Überträgige benutzt.", + "broadcastTheNewRoundHelp": "Die neu Runde wird us de gliche Mitglieder und Mitwürkende beschtah, wie die Vorherig.", + "broadcastAddRound": "E Rundi zuefüege", + "broadcastOngoing": "Laufend", + "broadcastUpcoming": "Demnächscht", + "broadcastCompleted": "Beändet", + "broadcastCompletedHelp": "Lichess erkännt de Rundeschluss oder au nöd! De Schalter setz das uf \"manuell\".", + "broadcastRoundName": "Runde Name", + "broadcastRoundNumber": "Runde Nummere", + "broadcastTournamentName": "Turnier Name", + "broadcastTournamentDescription": "Churzi Turnier Beschribig", + "broadcastFullDescription": "Vollschtändigi Ereignisbeschribig", + "broadcastFullDescriptionHelp": "Optionali, usfüehrlichi Beschribig vu de Überträgig. {param1} isch verfügbar. Die Beschribig muess chürzer als {param2} Zeiche si.", + "broadcastSourceSingleUrl": "PGN Quälle URL", + "broadcastSourceUrlHelp": "URL wo Lichess abfrögt, für PGN Aktualisierige z'erhalte. Sie muess öffentlich im Internet zuegänglich si.", + "broadcastSourceGameIds": "Bis zu 64 Lichess Schpiel-IDs, trännt dur en Leerschlag.", + "broadcastStartDateTimeZone": "Startdatum i de lokale Zitzone vum Turnier: {param}", + "broadcastStartDateHelp": "Optional, falls du weisch, wänn das Ereignis afangt", + "broadcastCurrentGameUrl": "URL vom laufende Schpiel", + "broadcastDownloadAllRounds": "Alli Runde abelade", + "broadcastResetRound": "Die Rundi zruggsetze", + "broadcastDeleteRound": "Die Rundi lösche", + "broadcastDefinitivelyDeleteRound": "Die Rundi, mit allne Schpiel, definitiv lösche.", + "broadcastDeleteAllGamesOfThisRound": "Lösch alli Schpiel vu dere Rundi. D'Quälle muess aktiv si, dass sie neu erschtellt werde chönd.", + "broadcastEditRoundStudy": "Runde-Schtudie bearbeite", + "broadcastDeleteTournament": "Lösch das Turnier", + "broadcastDefinitivelyDeleteTournament": "Das ganze Turnier, alli Runde und alli Schpiel definitiv lösche.", + "broadcastShowScores": "Zeigt d'Erfolg vu de Schpiller, anhand vu Schpiel-Ergäbnis", + "broadcastReplacePlayerTags": "Optional: Schpillernäme, Wertige und Titel weg lah", + "broadcastFideFederations": "FIDE Wältschachverband", + "broadcastTop10Rating": "Top 10 Ratings", + "broadcastFidePlayers": "FIDE Schpiller", + "broadcastFidePlayerNotFound": "FIDE Schpiller nöd g'funde", + "broadcastFideProfile": "FIDE Profil", + "broadcastFederation": "Verband", + "broadcastAgeThisYear": "Alter i dem Jahr", + "broadcastUnrated": "Ungwertet", + "broadcastRecentTournaments": "Aktuellschti Turnier", + "broadcastOpenLichess": "In Lichess öffne", + "broadcastTeams": "Teams", + "broadcastBoards": "Brätter", + "broadcastOverview": "Überblick", + "broadcastSubscribeTitle": "Mäld dich a, zum über jede Rundeschtart informiert z'werde. Du chasch de Alarm- oder d'Push-Benachrichtigung, für Überträgige, i dine Kontoischtellige umschalte.", + "broadcastUploadImage": "Turnier-Bild ufelade", + "broadcastNoBoardsYet": "No kei Brätter. Die erschined, sobald Schpiel ufeglade sind.", + "broadcastBoardsCanBeLoaded": "Brätter chönd mit ere Quälle oder via {param} ufeglade werde", + "broadcastStartsAfter": "Schtartet nach {param}", + "broadcastStartVerySoon": "Die Überträgig schtartet sehr bald.", + "broadcastNotYetStarted": "Die Überträgig hät nonig agfange.", + "broadcastOfficialWebsite": "Offizielli Website", + "broadcastStandings": "Tabälle", + "broadcastIframeHelp": "Meh Optionen uf {param}", + "broadcastWebmastersPage": "Webmaster Site", + "broadcastPgnSourceHelp": "Öffentlichi, real-time PGN Quälle, für die Rundi. Mir offeriered au {param} für e schnälleri und effiziänteri Synchronisation.", + "broadcastEmbedThisBroadcast": "Nimm die Überträgig uf dini Website", + "broadcastEmbedThisRound": "Nimm {param} uf dini Website", + "broadcastRatingDiff": "Wertigs Differänz", + "broadcastGamesThisTournament": "Schpiel i dem Turnier", + "broadcastScore": "Resultat", + "broadcastNbBroadcasts": "{count, plural, =1{{count} Überträgige} other{{count} Überträgige}}", "challengeChallengesX": "Useforderige: {param1}", - "challengeChallengeToPlay": "Zunere Partie usefordere", + "challengeChallengeToPlay": "Zum Schpiel fordere", "challengeChallengeDeclined": "Useforderig abglehnt", "challengeChallengeAccepted": "Useforderig agnah!", "challengeChallengeCanceled": "Useforderig zrugg'zoge.", @@ -95,12 +161,12 @@ "perfStatPerfStats": "{param}-Schtatischtike", "perfStatViewTheGames": "Schpil azeige", "perfStatProvisional": "provisorisch", - "perfStatNotEnoughRatedGames": "Nöd gnueg gwerteti Schpil, für e verlässlichi Wertig z'errächne.", + "perfStatNotEnoughRatedGames": "Nöd gnueg g'werteti Schpil, zum e verlässlichi Wertig z'errächne.", "perfStatProgressOverLastXGames": "Fortschritt über di letschte {param} Schpil:", "perfStatRatingDeviation": "Wertigssabwichig: {param}.", "perfStatRatingDeviationTooltip": "En niedrige Wert bedütet, dass d'Wertig schtabiler isch. Über {param1} wird d'Wertig als provisorisch betrachtet. In Ranglischtene chunnt mer, wänn de Wert under {param2} (Standard) oder {param3} (Variante) isch.", "perfStatTotalGames": "Alli Schpil", - "perfStatRatedGames": "Gwerteti Schpil", + "perfStatRatedGames": "G'werteti Schpiel", "perfStatTournamentGames": "Turnier Schpil", "perfStatBerserkedGames": "Berserk Schpil", "perfStatTimeSpentPlaying": "Gsamti Schpillzit", @@ -117,7 +183,7 @@ "perfStatLongestStreak": "Längschti Serie: {param}", "perfStatCurrentStreak": "Aktuelli Serie: {param}", "perfStatBestRated": "die beschte Sieg", - "perfStatGamesInARow": "In Serie gschpillti Partie", + "perfStatGamesInARow": "Nachenand g'schpillti Schpiel", "perfStatLessThanOneHour": "Weniger als 1 Schtund zwüsche de Schpil", "perfStatMaxTimePlaying": "Maximali Schpillzit", "perfStatNow": "jetzt", @@ -137,7 +203,7 @@ "preferencesZenMode": "Zen Modus", "preferencesShowPlayerRatings": "Zeig Schpiller Wertige", "preferencesShowFlairs": "Benutzer-Emojis azeige", - "preferencesExplainShowPlayerRatings": "Das erlaubt s'Usblände vu allne Wertige uf de Site und hilft, sich ufs Schach z'konzentriere. Partie chönd immer no bewertet werde, es gaht nur um das, was du gsesch.", + "preferencesExplainShowPlayerRatings": "Das erlaubt s'Usblände vu allne Wertige uf de Site und hilft, sich ufs Schach z'konzentriere. s'Schpiel wird immer no bewertet werde, es gaht nur drum, was mer gseht.", "preferencesDisplayBoardResizeHandle": "Zeig de Brättgrössi Regler", "preferencesOnlyOnInitialPosition": "Nur bi de Afangsschtellig", "preferencesInGameOnly": "Nur im Schpiel", @@ -154,7 +220,7 @@ "preferencesBothClicksAndDrag": "Beides", "preferencesPremovesPlayingDuringOpponentTurn": "Voruszüg (Premoves), während de Gägner am Zug isch", "preferencesTakebacksWithOpponentApproval": "Zugsrücknahm (mit Erlaubnis vom Gägner)", - "preferencesInCasualGamesOnly": "Nur in ungwertete Schpiel", + "preferencesInCasualGamesOnly": "Nur in nöd g'wertete Schpiel", "preferencesPromoteToQueenAutomatically": "Automatischi Umwandlig zur Dame", "preferencesExplainPromoteToQueenAutomatically": "Druck bi de Umwandlig, so wird sie vorübergehend abgschtellt", "preferencesWhenPremoving": "Bim Voruszug", @@ -174,7 +240,7 @@ "preferencesSayGgWpAfterLosingOrDrawing": "Säg \"guet gschpillt\" nach ere Niderlag oder bi me Remis", "preferencesYourPreferencesHaveBeenSaved": "Dini Ischtellige sind gschpeicheret.", "preferencesScrollOnTheBoardToReplayMoves": "Mit em Muszeiger uf em Brätt, chasch mit em Musrad all Züg vor- und zrugg scrolle", - "preferencesCorrespondenceEmailNotification": "Täglichi E-Mail-Benachrichtigung, wo Dini Fernschachpartie uflischtet", + "preferencesCorrespondenceEmailNotification": "Täglichi E-Mail-Benachrichtigung, wo dini Korrespondänz-Schpiel uflischtet", "preferencesNotifyStreamStart": "De Streamer gaht live", "preferencesNotifyInboxMsg": "Neui Nachricht im Poschtigang", "preferencesNotifyForumMention": "En Forumkommentar erwähnt dich", @@ -218,14 +284,14 @@ "puzzlePuzzleComplete": "Ufgab abgschlosse!", "puzzleByOpenings": "Nach Eröffnige", "puzzlePuzzlesByOpenings": "Ufgabe nach Eröffnige", - "puzzleOpeningsYouPlayedTheMost": "Dini meischt-gschpillte Eröffnige, i gwertete Partie", + "puzzleOpeningsYouPlayedTheMost": "Dini meischt-gschpillte Eröffnige, i g'wertete Schpiel", "puzzleUseFindInPage": "Benutz im Browser \"Suchen...\", um dini bevorzugti Eröffnig z'finde!", "puzzleUseCtrlF": "Find dini bevorzugti Eröffnig mit Ctrl+F !", "puzzleNotTheMove": "Das isch nöd de Zug!", "puzzleTrySomethingElse": "Probier öppis Anders.", "puzzleRatingX": "Wertig: {param}", "puzzleHidden": "verschteckt", - "puzzleFromGameLink": "Us de Partie {param}", + "puzzleFromGameLink": "Vom Schpiel {param}", "puzzleContinueTraining": "Witer trainiere", "puzzleDifficultyLevel": "Schwierigi Stufe", "puzzleNormal": "Normal", @@ -249,11 +315,11 @@ "puzzleContinueTheStreak": "Erfolgsserie witerfüehre", "puzzleNewStreak": "Neui Erfolgsserie", "puzzleFromMyGames": "Us eigene Schpil", - "puzzleLookupOfPlayer": "Suech Ufgabe us de Partie vu me Schpiller", - "puzzleFromXGames": "Ufgabe us Partie vu {param}", + "puzzleLookupOfPlayer": "Suech Ufgabe us de Schpiel vu me Schpiller", + "puzzleFromXGames": "Ufgabe us Schpiel vu {param}", "puzzleSearchPuzzles": "Suech Ufgabe", - "puzzleFromMyGamesNone": "Eu häsch kei Ufgabe i de Datäbank, aber Lichess schätzt dich immerno sehr.\n\nSchpill schnälli und klassischi Partie, so erhöht sich d'Chance, dass au Ufgabe us dine eigene Schpil zuegfüegt werded!", - "puzzleFromXGamesFound": "{param1} Ufgabe in {param2} Partie gfunde", + "puzzleFromMyGamesNone": "Du häsch kei Ufgabe i de Datäbank, aber Lichess schätzt dich immerno sehr.\n\nSchpill schnälli und au klassischi Schpiel, will so erhöht sich d'Chance, dass Ufgabe vu dine eigene Schpiel zuegfüegt werded!", + "puzzleFromXGamesFound": "{param1} Ufgabe in Schpiel vu {param2} gfunde", "puzzlePuzzleDashboardDescription": "Trainiere analysiere und besser werde", "puzzlePercentSolved": "{param} glöst", "puzzleNoPuzzlesToShow": "Es git no nüt zum Zeige, lös zerscht es paar Ufgabe!", @@ -307,7 +373,7 @@ "puzzleThemeDoubleCheck": "Doppelschach", "puzzleThemeDoubleCheckDescription": "Abzug mit doppletem Schachgebot, wobi die vorher verdeckti Figur- und die Abzogeni, de König glichzitig agrifed.", "puzzleThemeEndgame": "Ändschpil", - "puzzleThemeEndgameDescription": "E Taktik für die letscht Fase vu de Partie.", + "puzzleThemeEndgameDescription": "E Taktik für die letscht Fase vum Schpiel.", "puzzleThemeEnPassantDescription": "E Taktik wo \"En-Passant\" beinhaltet - e Regle wo en Pur cha en gägnerische Pur schlaa, wänn de ihn mit em \"Zwei-Fälder-Zug\" übergange hät.", "puzzleThemeExposedKing": "Exponierte König", "puzzleThemeExposedKingDescription": "E Taktik wo de König nu vu wenige Figure verteidigt wird und oft zu Schachmatt fühert.", @@ -325,10 +391,10 @@ "puzzleThemeKnightEndgameDescription": "Es Ändschpiel, nur mit Schpringer und Pure.", "puzzleThemeLong": "Mehrzügigi Ufgab", "puzzleThemeLongDescription": "3 Züg zum Sieg.", - "puzzleThemeMaster": "Meischter Partie", - "puzzleThemeMasterDescription": "Ufgabe us Partie vu Schpiller mit Titel.", - "puzzleThemeMasterVsMaster": "Meischter gäge Meischter Partie", - "puzzleThemeMasterVsMasterDescription": "Ufgabe us Partie vu 2 Schpiller mit Titel.", + "puzzleThemeMaster": "Meischter Schpiel", + "puzzleThemeMasterDescription": "Ufgabe us Schpiel vu Schpiller mit Titel.", + "puzzleThemeMasterVsMaster": "Meischter gäge Meischter Schpiel", + "puzzleThemeMasterVsMasterDescription": "Ufgabe us Schpiel vu 2 Schpiller mit Titel.", "puzzleThemeMate": "Schachmatt", "puzzleThemeMateDescription": "Günn das Schpiel mit Schtil.", "puzzleThemeMateIn1": "Matt in 1", @@ -342,11 +408,11 @@ "puzzleThemeMateIn5": "Matt in 5 oder meh", "puzzleThemeMateIn5Description": "Find e langi Mattfüherig.", "puzzleThemeMiddlegame": "Mittelschpiel", - "puzzleThemeMiddlegameDescription": "E Taktik für die zweit Fase vu de Partie.", + "puzzleThemeMiddlegameDescription": "E Taktik für die zweit Fase vum Schpiel.", "puzzleThemeOneMove": "1-zügigi Ufgab", "puzzleThemeOneMoveDescription": "E Ufgab, mit nur 1 Zug.", "puzzleThemeOpening": "Eröffnig", - "puzzleThemeOpeningDescription": "E Taktik für die erscht Fase vu de Partie.", + "puzzleThemeOpeningDescription": "E Taktik für die erscht Fase vum Schpiel.", "puzzleThemePawnEndgame": "Pure Ändschpiel", "puzzleThemePawnEndgameDescription": "Es Ändschpiel nur mit Pure.", "puzzleThemePin": "Fesslig", @@ -371,8 +437,8 @@ "puzzleThemeSkewerDescription": "Bim \"Schpiess\" wird e Figur, wo vor ere Andere staht - oft wird sie au dezue zwunge, sich vor die Ander z'schtelle - agriffe, so dass sie us em Wäg muess und ermöglicht, die Hinder z'schlah. Quasi umgekehrti Fesslig.", "puzzleThemeSmotheredMate": "Erschtickigs Matt", "puzzleThemeSmotheredMateDescription": "Es Schachmatt mit em Springer, wo sich de König nöd bewege cha, will er vu sine eigene Figuren umstellt-, also vollkomme igschlosse, wird.", - "puzzleThemeSuperGM": "Super-Grossmeischter-Partie", - "puzzleThemeSuperGMDescription": "Ufgabe us Partie, vu de beschte Schpiller uf de Wält.", + "puzzleThemeSuperGM": "Super-Grossmeischter-Schpiel", + "puzzleThemeSuperGMDescription": "Ufgabe us Schpiel, vu de beschte Schpiller uf de Wält.", "puzzleThemeTrappedPiece": "G'fangeni Figur", "puzzleThemeTrappedPieceDescription": "E Figur cha em Schlah nöd entgah, will sie nur begränzt Züg mache cha.", "puzzleThemeUnderPromotion": "Underverwandlig", @@ -383,9 +449,9 @@ "puzzleThemeXRayAttackDescription": "E Figur attackiert oder verteidigt es Fäld dur e gägnerischi Figur.", "puzzleThemeZugzwang": "Zugszwang", "puzzleThemeZugzwangDescription": "De Gägner hät nur e limitierti Azahl Züg und Jede verschlächteret sini Schtellig.", - "puzzleThemeHealthyMix": "En gsunde Mix", - "puzzleThemeHealthyMixDescription": "Es bitzli vu Allem, me weiss nöd was eim erwartet, drum isch mer uf alles g'fasst - genau wie bi richtige Schachschpiel.", - "puzzleThemePlayerGames": "Schpiller Partie", + "puzzleThemeMix": "En gsunde Mix", + "puzzleThemeMixDescription": "Es bitzli vu Allem, me weiss nöd was eim erwartet, drum isch mer uf alles g'fasst - genau wie bi richtige Schachschpiel.", + "puzzleThemePlayerGames": "Schpiller-Schpiel", "puzzleThemePlayerGamesDescription": "Suech nach Ufgabe us dine Schpiel oder Ufgabe us Schpiel vu Andere.", "puzzleThemePuzzleDownloadInformation": "Die Ufgabe sind öffentlich, mer channs abelade under {param}.", "searchSearch": "Suechi", @@ -431,7 +497,7 @@ "variantEnding": "Variante ändet", "newOpponent": "En neue Gägner", "yourOpponentWantsToPlayANewGameWithYou": "Din Gägner wett es neus Schpil mit dir schpille", - "joinTheGame": "Tritt de Partie bi", + "joinTheGame": "Gang is Schpiel", "whitePlays": "Wiss am Zug", "blackPlays": "Schwarz am Zug", "opponentLeftChoices": "Din Gägner hät s'Schpiel verlah. Du chasch de Sieg beaschpruche, es Remis mache oder warte.", @@ -442,7 +508,7 @@ "whiteResigned": "Wiss hät ufgeh", "blackResigned": "Schwarz hät ufgeh", "whiteLeftTheGame": "Wiss hät d'Partie verlah", - "blackLeftTheGame": "Schwarz hät d'Partie verlah", + "blackLeftTheGame": "Schwarz hät s'Schpiel verlah", "whiteDidntMove": "Wiss hät nöd zoge", "blackDidntMove": "Schwarz hät nöd zoge", "requestAComputerAnalysis": "Computer-Analyse afordere", @@ -481,9 +547,9 @@ "database": "Datäbank", "whiteDrawBlack": "Wiss / Remis / Schwarz", "averageRatingX": "Durchschnittlichi Wertigszahl: {param}", - "recentGames": "Aktuelli Partie", - "topGames": "Beschti-Partie", - "masterDbExplanation": "2 Millione OTB Schpil vu {param1}+ FIDE-gwertete Schpiller vu {param2} bis {param3}", + "recentGames": "Aktuelli Schpiel", + "topGames": "Schpitzeschpiel", + "masterDbExplanation": "2 Millione OTB Schpiel vu {param1}+ FIDE-g'wertete Schpiller vu {param2} bis {param3}", "dtzWithRounding": "DTZ50'' mit Rundig, basierend uf de Azahl Halbzüg bis zum nächschte Schlag- oder Pure-Zug", "noGameFound": "Keis Schpiel gfunde", "maxDepthReached": "Die maximali Tüfi isch erreicht!", @@ -515,7 +581,6 @@ "memory": "Arbetsschpeicher", "infiniteAnalysis": "Unändlichi Analyse", "removesTheDepthLimit": "Entfernt d'Tüfebegränzig und haltet din Computer warm", - "engineManager": "Engine Betreuer", "blunder": "En Patzer", "mistake": "Fähler", "inaccuracy": "Ungnauigkeit", @@ -527,7 +592,7 @@ "draw": "Remis", "drawByMutualAgreement": "Remis dur Einigung", "fiftyMovesWithoutProgress": "50 Züg ohni Fortschritt", - "currentGames": "Laufendi Partie", + "currentGames": "Laufendi Schpiel", "viewInFullSize": "In voller Grössi azeige", "logOut": "Abmälde", "signIn": "Ilogge", @@ -535,7 +600,7 @@ "youNeedAnAccountToDoThat": "Für das bruchsch du es Benutzerkonto", "signUp": "Regischtriere", "computersAreNotAllowedToPlay": "Computer und Computer-Understützig isch nöd erlaubt. Bitte lass dir bim Schpille nöd vu Schachengines, Datäbanke oder andere Schpiller hälfe. \nAu vum Erschtelle vu mehrere Konte wird dringend abgrate - übermässigs Multikonteverhalte chann zume Usschluss fühere.", - "games": "Partie", + "games": "Schpiel", "forum": "Forum", "xPostedInForumY": "{param1} hät zum Thema {param2} öppis gschribä", "latestForumPosts": "Neuschti Forum-Biträg", @@ -596,7 +661,8 @@ "accountRegisteredWithoutEmail": "s'Konto {param} isch ohni E-Mail-Adrässe regischtriert worde.", "rank": "Rang", "rankX": "Platz: {param}", - "gamesPlayed": "Gschpillti Partie", + "gamesPlayed": "Schpiel g'macht", + "ok": "OK", "cancel": "Abbräche", "whiteTimeOut": "Wiss hät d'Zit überschritte", "blackTimeOut": "Schwarz hät d'Zit überschritte", @@ -610,11 +676,11 @@ "yourOpponentOffersADraw": "Din Gägner offeriert es Remis", "accept": "Akzeptiere", "decline": "Ablehnä", - "playingRightNow": "Partie isch am laufe", + "playingRightNow": "Schpiel lauft", "eventInProgress": "Lauft jetzt", "finished": "Beändet", - "abortGame": "Partie abbräche", - "gameAborted": "Partie abbroche", + "abortGame": "Schpiel abbräche", + "gameAborted": "Schpiel abbroche", "standard": "Standard", "customPosition": "Benutzerdefinierti Schtellig", "unlimited": "Unbegränzt", @@ -623,7 +689,7 @@ "rated": "Gwertet", "casualTournament": "Ungwertet", "ratedTournament": "Gwertet", - "thisGameIsRated": "Das Schpiel isch gwertet", + "thisGameIsRated": "Das Schpiel isch g'wertet", "rematch": "Revanche", "rematchOfferSent": "Agebot zur Revanche gsändet", "rematchOfferAccepted": "Revanche-Agebot aktzeptiert", @@ -643,7 +709,7 @@ "send": "Schicke", "incrementInSeconds": "Inkrement in Sekunde", "freeOnlineChess": "Gratis Online-Schach", - "exportGames": "Partie exportiere", + "exportGames": "Schpiel exportiere", "ratingRange": "Wertigsbereich", "thisAccountViolatedTos": "Das Konto hät gäge d'Lichess-Nutzigsbedingige verschtosse", "openingExplorerAndTablebase": "Eröffnigsbuech & Ändschpiel-Datäbank", @@ -666,8 +732,8 @@ "yourPerfRatingIsTooHigh": "Dini {param1} Wertig ({param2}) isch z'höch", "yourTopWeeklyPerfRatingIsTooHigh": "Dini höchschti {param1} Wertig ({param2}) isch z'höch", "yourPerfRatingIsTooLow": "Dini {param1} Wertig ({param2}) isch z'tüüf", - "ratedMoreThanInPerf": "Gwertet ≥ {param1} in {param2}", - "ratedLessThanInPerf": "Wertig ≤ {param1} im {param2} die letschte 7 Täg", + "ratedMoreThanInPerf": "G'wertet ≥ {param1} in {param2}", + "ratedLessThanInPerf": "≤ {param1} im {param2} die letscht Wuche", "mustBeInTeam": "Du muesch im Team {param} si", "youAreNotInTeam": "Du bisch nöd im Team {param}", "backToGame": "Zrugg zum Schpiel", @@ -678,7 +744,7 @@ "xStartedStreaming": "{param} hät mit Streame agfange", "averageElo": "Durchschnittswertig", "location": "Ortschaft/Land", - "filterGames": "Partie filtere", + "filterGames": "Schpiel filtere", "reset": "Zruggsetze", "apply": "Awände", "save": "Schpeichere", @@ -691,7 +757,7 @@ "fromPosition": "Ab Schtellig", "continueFromHere": "Vu da us witer schpille", "toStudy": "Schtudie", - "importGame": "Partie importiere", + "importGame": "Schpiel importiere", "importGameExplanation": "Füeg e Schpiel-PGN i, für Zuegriff uf Schpielwiderholig, Computeranalyse, Chat und e teilbari URL.", "importGameCaveat": "d'Variazione werded glöscht. Zums b'halte, muesch d'PGN mit ere Schtudie importiere.", "importGameDataPrivacyWarning": "De PGN isch öffentlich zuegänglich. Zum es Schpiel privat importiere, nimmsch e Schtudie.", @@ -734,8 +800,8 @@ "inappropriateNameWarning": "Alles - au liecht Unagmässes - cha zur Schlüssig vu dim Konto fühere.", "emptyTournamentName": "Frei la zum s'Turnier nach eme namhafte Schachschpiller z'benänne.", "makePrivateTournament": "Mach das Turnier privat und beschränk de Zuegang mit Passwort", - "join": "Mach mit", - "withdraw": "Usstige", + "join": "Mitmache", + "withdraw": "Usschtige", "points": "Pünkt", "wins": "Sieg", "losses": "Niederlage", @@ -743,7 +809,7 @@ "tournamentIsStarting": "s'Turnier fangt a", "tournamentPairingsAreNowClosed": "Es werded kei Turnierschpiel meh gschpillt.", "standByX": "Achtung {param}, es git neui Paarige, mach dich parat!", - "pause": "underbräche", + "pause": "Pausiere", "resume": "wieder ischtige", "youArePlaying": "Dis Schpiel fangt a!", "winRate": "Gwünn Rate", @@ -787,9 +853,9 @@ "previouslyOnLichessTV": "Zletscht uf Lichess TV", "onlinePlayers": "Online Schpiller", "activePlayers": "Aktivi Schpiller", - "bewareTheGameIsRatedButHasNoClock": "Achtung, das Schpiel isch gwertet, aber ohni Zitlimit!", + "bewareTheGameIsRatedButHasNoClock": "Achtung, das Schpiel isch g'wertet, aber ohni Zitlimit!", "success": "Korräkt", - "automaticallyProceedToNextGameAfterMoving": "Nach em Zug automatisch zur nächschte Partie", + "automaticallyProceedToNextGameAfterMoving": "Nach em Zug automatisch zum nächschte Schpiel", "autoSwitch": "Automatische Wächsel", "puzzles": "Ufgabe", "onlineBots": "Online Roboter", @@ -819,7 +885,9 @@ "cheat": "Bschiss", "troll": "Troll", "other": "Suschtigs", - "reportDescriptionHelp": "Füeg de Link dere/dene Partie bi und erchlär, wie sich de Benutzer falsch benah hät. Säg nöd nur \"de bschisst\", schrib eus wie du da druf chunnsch. (änglisch gschribeni Mäldige, werded schnäller behandlet).", + "reportCheatBoostHelp": "Füeg de Link dem/dene Schpiel bi und erchlär, wie sich de Benutzer falsch benah hät. Säg nöd nur \"de bschisst\", schrib eus wie du da druf chunnsch. (änglisch gschribeni Mäldige, werded schnäller behandlet).", + "reportUsernameHelp": "Erchlär, was am Benutzername nöd in Ornig isch: Säg nöd eifach \"er isch beleidigend/unagmässe\", sondern schrib eus, wie du zu dere Folgerig cho bisch. B'sunders wänn die Beleidigung verschleieret-, nöd uf Änglisch- oder in Dialäkt isch oder wänn sie en historische oder kulturelle Bezug hät.", + "reportProcessedFasterInEnglish": "Änglisch g'schribeni Mäldige werded schnäller behandlet.", "error_provideOneCheatedGameLink": "Bitte gib mindeschtens 1 Link zume Schpiel a, wo bschisse worde isch.", "by": "vu {param}", "importedByX": "Importiert vu {param}", @@ -853,7 +921,7 @@ "insideTheBoard": "Uf em Brätt", "outsideTheBoard": "Usse vum Brätt", "allSquaresOfTheBoard": "Uf jedem Fäld", - "onSlowGames": "Bi langsame Partie", + "onSlowGames": "Bi langsame Schpiel", "always": "Immer", "never": "Nie", "xCompetesInY": "{param1} nimmt bi {param2} teil", @@ -897,7 +965,7 @@ "whiteCastlingKingside": "Wiss 0-0", "blackCastlingKingside": "Schwarz 0-0", "tpTimeSpentPlaying": "Gsamti-Schpillzit: {param}", - "watchGames": "Partie zueluege", + "watchGames": "Schpiel beobachte", "tpTimeSpentOnTV": "Gsamtzit uf Lichess TV: {param}", "watch": "Zueluege", "videoLibrary": "Video-Bibliothek", @@ -922,10 +990,10 @@ "noSimulExplanation": "Das Simultanschach exischtiert nöd.", "returnToSimulHomepage": "Zrugg zur Simultanschach Startsite", "aboutSimul": "Bim Simultanschach schpillt 1 Simultanschpiller \nglichzitig gäge beliebig vill Simultangägner.", - "aboutSimulImage": "Hät de Bobby Fischer, bi 50 Gägner, 47 Sieg \nund 2 Remis gschafft; nur 1 Partie hät er verlore.", - "aboutSimulRealLife": "Bim Simultanschach schpillt 1 Simultanschpiller \nglichzitig gäge beliebig vill Simultangägner, \nso lang, bis alli Partie fertig gschpillt sind.", + "aboutSimulImage": "Hät de Bobby Fischer, bi 50 Gägner, 47 Sieg \nund 2 Remis gschafft - nu 1 Schpiel hät er verlore.", + "aboutSimulRealLife": "Bim Simultanschach schpillt 1 Simultanschpiller \nglichzitig gäge beliebig vill Simultangägner, \nso lang, bis alli Schpiel fertig gschpillt sind.", "aboutSimulRules": "Wie bi reale Simultanschach Veraschtaltige, gaht \nde Simultanschpiller vu eim Simultangägner zum \nNächschte und macht bi jedem Brätt 1 Zug.", - "aboutSimulSettings": "Zugsrücknahme, zuesätzlichi Zit oder Revanche \ngits nöd und es isch immer ungwertet.", + "aboutSimulSettings": "Zugsrücknahme, zuesätzlichi Zit oder Revanche \ngits nöd und es isch nie g'wertet.", "create": "Erschtelle", "whenCreateSimul": "Wänn du dir es Simultanschach machsch, chasch du glichzitig gäge mehreri Gägner schpille.", "simulVariantsHint": "Wänn du mehreri Variante wählsch, chann jede Simultangägner ussueche, was er schpille will.", @@ -1065,7 +1133,7 @@ "seeBestMove": "Zeig de bescht Zug", "hideBestMove": "Verschteck de bescht Zug", "getAHint": "Lass dir en Tipp geh", - "evaluatingYourMove": "Din Zug wird gwertet ...", + "evaluatingYourMove": "Din Zug wird g'wertet ...", "whiteWinsGame": "Wiss günnt", "blackWinsGame": "Schwarz günnt", "learnFromYourMistakes": "Lern us dine Fähler", @@ -1106,7 +1174,7 @@ "goodPractice": "Zum dem Zwäck müend mir sicher si, dass sich all Schpiller korräkt verhalted.", "potentialProblem": "Wänn es möglichs Problem entdeckt wird, zeiged mir die Mäldig.", "howToAvoidThis": "Wie chasch das verhindere?", - "playEveryGame": "Schpill jedi Partie, wo du afangsch, au fertig.", + "playEveryGame": "Schpill jedes Schpiel, wo du afangsch, au fertig.", "tryToWin": "Probier jedes Schpil z'günne (oder mindeschtens es Remis z'erreiche).", "resignLostGames": "Gib es verlores Schpiel uf (lass nöd eifach d'Uhr ablaufe).", "temporaryInconvenience": "Mir entschuldiged eus für die vorübergehende Unannehmlichkeite,", @@ -1129,7 +1197,7 @@ "blitzDesc": "Schnälli Schpiel: 3 bis 8 Minute", "rapidDesc": "Schnällschach: 8 bis 25 Minute", "classicalDesc": "Klassischi Schpiel: 25 Minute und meh", - "correspondenceDesc": "Fernschach Partie: 1 oder mehreri Täg pro Zug", + "correspondenceDesc": "Korrespondänz-Schpiel: 1 oder mehreri Täg pro Zug", "puzzleDesc": "Schach-Taktik Trainer", "important": "Wichtig", "yourQuestionMayHaveBeenAnswered": "Dini Frag chönnt bereits {param1} beantwortet worde si", @@ -1143,7 +1211,7 @@ "thisTopicIsArchived": "Das Thema isch archiviert und chann nüme beantwortet werde.", "joinTheTeamXToPost": "Tritt {param1} bi, um i dem Forum Biträg z'schribe", "teamNamedX": "{param1} Team", - "youCannotPostYetPlaySomeGames": "Du chasch nonig im Forum schribe: Schpill zerscht no es paar Partie!", + "youCannotPostYetPlaySomeGames": "Du chasch no nüt im Forum schribe: Schpill vorher es paar Schpiel!", "subscribe": "Abonniere", "unsubscribe": "Abmälde", "mentionedYouInX": "hät dich in \"{param1}\" erwähnt.", @@ -1154,7 +1222,7 @@ "youHaveJoinedTeamX": "Du bisch \"{param1}\" biträte.", "someoneYouReportedWasBanned": "Öpper wo du gmäldet häsch, isch bannt worde", "congratsYouWon": "Gratuliere, du häsch gunne!", - "gameVsX": "Partie gäge {param1}", + "gameVsX": "Schpiel gäge {param1}", "resVsX": "{param1} gäge {param2}", "lostAgainstTOSViolator": "Du häsch Wertigspünkt verlore, gäge öpper, wo d'Lichess-Regle verletzt hät", "refundXpointsTimeControlY": "Rückerschtattig: {param1} {param2} Wertigspünkt.", @@ -1166,15 +1234,15 @@ "cancelTournament": "Brich das Turnier ab", "tournDescription": "Turnier Beschribig", "tournDescriptionHelp": "Wottsch de Teilnehmer öppis Speziells mitteile? Probier dich churz z'fasse. Url mit Name sind möglich: [name](https://url)", - "ratedFormHelp": "Alli Partie sind gwertet\nund beiflussed dini Wertig", + "ratedFormHelp": "Alli Schpiel sind g'wertet\nund beiflussed dini Wertig", "onlyMembersOfTeam": "Nur Mitglider vum Team", "noRestriction": "Kei Ischränkige", - "minimumRatedGames": "Minimum gwerteti Partie", + "minimumRatedGames": "Minimum g'werteti Schpiel", "minimumRating": "Minimali Wertig", "maximumWeeklyRating": "Maximali wüchentlichi Wertig", "positionInputHelp": "E gültigi FEN schtartet jedes Schpiel ab ere beschtimmte Schtellig. \nDas funktioniert nur für Schtandardschpiel und nöd für Variante!\nDu chasch mit {param} so e FEN-Schtellig generiere und \nsie dänn im Fäld \"Afangsposition\" ifüege oder das \nFäld leer lah und all Schpiel normal schtarte.", "cancelSimul": "Simultanschach abbräche", - "simulHostcolor": "Farb vom Simultanschpiller, für jedi Partie", + "simulHostcolor": "Farb vom Simultanschpiller, für jedes Schpiel", "estimatedStart": "Vorussichtlichi Schtartzit", "simulFeatured": "Uf {param} zeige", "simulFeaturedHelp": "Zeig allne uf {param} dis Simultanschach. Privats Simultanschach deaktiviert.", @@ -1203,13 +1271,13 @@ "sentEmailWithLink": "Mir hän dir e E-Mail mit eme Link gschickt.", "tournamentEntryCode": "Turnier-Bitrittscode", "hangOn": "En Momänt!", - "gameInProgress": "Du häsch no e laufedi Partie mit {param}.", - "abortTheGame": "Partie abbräche", - "resignTheGame": "Partie ufgeh", - "youCantStartNewGame": "Du chasch kei neui Partie starte, bevor die no Laufendi nöd fertig gschpillt isch.", + "gameInProgress": "Du häsch no es laufeds Schpiel mit {param}.", + "abortTheGame": "Schpiel abbräche", + "resignTheGame": "Schpiel ufgeh", + "youCantStartNewGame": "Du chasch kei neus Schpiel schtarte, bevor s'Laufende nöd fertig isch.", "since": "Sit", "until": "Bis", - "lichessDbExplanation": "Gwerteti Schpiel vu allne Lichess Schpiller", + "lichessDbExplanation": "G'werteti Schpiel vu allne Lichess Schpiller", "switchSides": "Farb wächsle", "closingAccountWithdrawAppeal": "Wänn du dis Konto schlüssisch, ziehsch du au din Ischpruch zrugg", "ourEventTips": "Eusi Tipps, fürs Organisiere vu Events", @@ -1217,13 +1285,14 @@ "showMeEverything": "Zeig mer alles", "lichessPatronInfo": "Lichess isch e Wohltätigkeitsorganisation und e völlig choschtelosi/freii Open-Source-Software.\nAlli Betriebs-Choschte, d'Entwicklig und d'Inhält werded usschliesslich dur Benutzerschpände finanziert.", "nothingToSeeHere": "Da gits im monumäntan nüt z'gseh.", - "opponentLeftCounter": "{count, plural, =1{Din Gägner hät d'Partie verlah. Du chasch in {count} Sekunde din Sieg beaschpruche} other{Din Gägner hät d'Partie verlah, du chasch in {count} Sekunde din Sieg beaschpruche}}", + "stats": "Schtatistike", + "opponentLeftCounter": "{count, plural, =1{Din Gägner hät s'Schpiel verlah. Du chasch in {count} Sekunde de Sieg beaschpruche} other{Din Gägner hät d'Partie verlah, du chasch in {count} Sekunde din Sieg beaschpruche}}", "mateInXHalfMoves": "{count, plural, =1{Matt i {count} Halbzug} other{Matt i {count} Halbzüg}}", "nbBlunders": "{count, plural, =1{{count} Patzer} other{{count} Patzer}}", "nbMistakes": "{count, plural, =1{{count} Fähler} other{{count} Fähler}}", "nbInaccuracies": "{count, plural, =1{{count} Ungnauigkeit} other{{count} Ungnauigkeite}}", "nbPlayers": "{count, plural, =1{{count} Schpiller} other{{count} Schpiller}}", - "nbGames": "{count, plural, =1{{count} Partie} other{{count} Partie}}", + "nbGames": "{count, plural, =1{{count} Schpiel} other{{count} Schpiel}}", "ratingXOverYGames": "{count, plural, =1{{count} Wertig vu {param2} Spiel} other{{count} Wertig vu {param2} Spiel}}", "nbBookmarks": "{count, plural, =1{{count} Läsezeiche} other{{count} Läsezeiche}}", "nbDays": "{count, plural, =1{{count} Tag} other{{count} Täg}}", @@ -1231,26 +1300,26 @@ "nbMinutes": "{count, plural, =1{{count} Minute} other{{count} Minute}}", "rankIsUpdatedEveryNbMinutes": "{count, plural, =1{De Rang wird jedi Minute aktualisiert} other{De Rang wird alli {count} Minute aktualisiert}}", "nbPuzzles": "{count, plural, =1{{count} Ufgab} other{{count} Ufgabe}}", - "nbGamesWithYou": "{count, plural, =1{{count} Partie mit dir} other{{count} Partie mit dir}}", - "nbRated": "{count, plural, =1{{count} gwertet} other{{count} gwerteti}}", + "nbGamesWithYou": "{count, plural, =1{{count} Schpiel mit dir} other{{count} Schpiel mit dir}}", + "nbRated": "{count, plural, =1{{count} g'wertet} other{{count} gwerteti}}", "nbWins": "{count, plural, =1{{count} Sieg} other{{count} Sieg}}", "nbLosses": "{count, plural, =1{{count} Niederlag} other{{count} Niderlage}}", "nbDraws": "{count, plural, =1{{count} Remis} other{{count} Remis}}", - "nbPlaying": "{count, plural, =1{{count} laufendi Partie} other{{count} laufendi Partie}}", + "nbPlaying": "{count, plural, =1{{count} am Schpille} other{{count} am Schpille}}", "giveNbSeconds": "{count, plural, =1{Gib dim Gägner {count} Sekunde} other{Gib dim Gägner {count} Sekunde}}", "nbTournamentPoints": "{count, plural, =1{{count} Turnierpunkt} other{{count} Turnierpünkt}}", "nbStudies": "{count, plural, =1{{count} Schtudie} other{{count} Schtudie}}", "nbSimuls": "{count, plural, =1{{count} Simultan} other{{count} Simultan}}", - "moreThanNbRatedGames": "{count, plural, =1{≥ {count} gwertets Schpiel} other{≥ {count} gwerteti Schpiel}}", - "moreThanNbPerfRatedGames": "{count, plural, =1{≥ {count} gwertets {param2} Schpiel} other{≥ {count} gwerteti {param2} Schpiel}}", - "needNbMorePerfGames": "{count, plural, =1{Du muesch no {count} gwerteti Partie meh {param2} schpille} other{Du muesch no {count} gwerteti Partie meh {param2} schpille}}", + "moreThanNbRatedGames": "{count, plural, =1{≥ {count} g'wertets Schpiel} other{≥ {count} g'werteti Schpiel}}", + "moreThanNbPerfRatedGames": "{count, plural, =1{≥ {count} g'wertets {param2} Schpiel} other{≥ {count} g'werteti {param2} Schpiel}}", + "needNbMorePerfGames": "{count, plural, =1{Du muesch no {count} g'wertets Schpiel meh {param2} schpille} other{Du muesch no {count} g'werteti Schpiel meh {param2} schpille}}", "needNbMoreGames": "{count, plural, =1{Du bruchsch no {count} Wertigs-Schpiel meh} other{Du bruchsch no {count} Wertigs-Schpiel meh}}", - "nbImportedGames": "{count, plural, =1{{count} importierti Partie} other{{count} importierti Partie}}", + "nbImportedGames": "{count, plural, =1{{count} importierts Schpiel} other{{count} importierti Schpiel}}", "nbFriendsOnline": "{count, plural, =1{{count} Fründ online} other{{count} Fründe online}}", "nbFollowers": "{count, plural, =1{{count} Follower} other{{count} Follower}}", "nbFollowing": "{count, plural, =1{{count} folgänd} other{{count} folgänd}}", "lessThanNbMinutes": "{count, plural, =1{Weniger als {count} Minute} other{Weniger als {count} Minute}}", - "nbGamesInPlay": "{count, plural, =1{{count} laufendi Partie} other{{count} laufendi Partie}}", + "nbGamesInPlay": "{count, plural, =1{{count} laufends Schpiel} other{{count} laufendi Schpiel}}", "maximumNbCharacters": "{count, plural, =1{Höchschtens {count} Buechstabe.} other{Höchschtens {count} Buechstabe.}}", "blocks": "{count, plural, =1{{count} blockt} other{{count} Blockti}}", "nbForumPosts": "{count, plural, =1{{count} Forum-Bitrag} other{{count} Forum-Biträg}}", @@ -1313,6 +1382,159 @@ "stormXRuns": "{count, plural, =1{1 Lauf} other{{count} Läuf}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Het en Lauf vo {param2} gschpillt} other{Hät {count} Läuf vu {param2} gschpillt}}", "streamerLichessStreamers": "Lichess Streamer/-in", + "studyPrivate": "Privat", + "studyMyStudies": "Mini Schtudie", + "studyStudiesIContributeTo": "Schtudie, wo ich mitwürke", + "studyMyPublicStudies": "Mini öffentliche Schtudie", + "studyMyPrivateStudies": "Mini private Schtudie", + "studyMyFavoriteStudies": "Mini liebschte Schtudie", + "studyWhatAreStudies": "Was sind Schtudie?", + "studyAllStudies": "All Schtudie", + "studyStudiesCreatedByX": "Vu {param} erschtellte Schtudie", + "studyNoneYet": "No kei.", + "studyHot": "agseit", + "studyDateAddedNewest": "wänn zuegfüegt (neui)", + "studyDateAddedOldest": "wänn zuegfüegt (älti)", + "studyRecentlyUpdated": "frisch aktualisiert", + "studyMostPopular": "beliebtschti", + "studyAlphabetical": "alphabetisch", + "studyAddNewChapter": "Neus Kapitel zuefüege", + "studyAddMembers": "Mitglider zuefüege", + "studyInviteToTheStudy": "Lad zu de Schtudie i", + "studyPleaseOnlyInvitePeopleYouKnow": "Bitte lad nur Lüt i, wo du kännsch und wo wänd aktiv a dere Schtudie teilneh.", + "studySearchByUsername": "Suech noch Benutzername", + "studySpectator": "Zueschauer", + "studyContributor": "Mitwürkende", + "studyKick": "Userüere", + "studyLeaveTheStudy": "Schtudie verlah", + "studyYouAreNowAContributor": "Du bisch jetzt en Mitwürkende", + "studyYouAreNowASpectator": "Du bisch jetzt Zueschauer", + "studyPgnTags": "PGN Tags", + "studyLike": "Gfallt mir", + "studyUnlike": "Gfallt mir nümme", + "studyNewTag": "Neue Tag", + "studyCommentThisPosition": "Kommentier die Schtellig", + "studyCommentThisMove": "Kommentier de Zug", + "studyAnnotateWithGlyphs": "Mit Symbol kommentiere", + "studyTheChapterIsTooShortToBeAnalysed": "Das Kapitel isch z'churz zum analysiere.", + "studyOnlyContributorsCanRequestAnalysis": "Nur wer a dere Schtudie mit macht, chann e Coputeranalyse afordere.", + "studyGetAFullComputerAnalysis": "Erhalt e vollschtändigi, serversitigi Computeranalyse vu de Hauptvariante.", + "studyMakeSureTheChapterIsComplete": "Schtell sicher, dass das Kapitel vollschtändig isch. Die Analyse chann nur eimal agforderet werde.", + "studyAllSyncMembersRemainOnTheSamePosition": "Alli synchronisierte Mitglider gsehnd die glich Schtellig", + "studyShareChanges": "Teil Änderige mit de Zueschauer und speicher sie uf em Server", + "studyPlaying": "Laufend", + "studyShowEvalBar": "Bewertigs-Skala", + "studyFirst": "zur erschte Site", + "studyPrevious": "zrugg", + "studyNext": "nächschti", + "studyLast": "zur letschte Site", "studyShareAndExport": "Teile & exportiere", - "studyStart": "Schtart" + "studyCloneStudy": "Klone", + "studyStudyPgn": "Schtudie PGN", + "studyDownloadAllGames": "All Schpiel abelade", + "studyChapterPgn": "Kapitel PGN", + "studyCopyChapterPgn": "PGN kopiere", + "studyDownloadGame": "Das Schpiel abelade", + "studyStudyUrl": "Schtudie URL", + "studyCurrentChapterUrl": "URL aktuells Kapitel", + "studyYouCanPasteThisInTheForumToEmbed": "Du chasch das, zum ibinde, im Forum oder i dim Liches Tagebuech ifüege", + "studyStartAtInitialPosition": "Fang ab de Grundschtellig a", + "studyStartAtX": "Fang mit {param} a", + "studyEmbedInYourWebsite": "I dini Website ibinde", + "studyReadMoreAboutEmbedding": "Lies meh über s'Ibinde", + "studyOnlyPublicStudiesCanBeEmbedded": "Mer chann nur öffentlichi Schtudie ibinde!", + "studyOpen": "Öffne", + "studyXBroughtToYouByY": "{param1} präsentiert vu {param2}", + "studyStudyNotFound": "Schtudie nöd gfunde", + "studyEditChapter": "Kapitel bearbeite", + "studyNewChapter": "Neus Kapitel", + "studyImportFromChapterX": "Importiers us {param}", + "studyOrientation": "Orientierig", + "studyAnalysisMode": "Analyse Modus", + "studyPinnedChapterComment": "Aghänkte Kapitel Kommentar", + "studySaveChapter": "Kapitel schpeichere", + "studyClearAnnotations": "Amerkige lösche", + "studyClearVariations": "Variante lösche", + "studyDeleteChapter": "Kapitel lösche", + "studyDeleteThisChapter": "Kapitel lösche? Das chann nöd rückgängig gmacht werde!", + "studyClearAllCommentsInThisChapter": "Alli Kommentär, Symbol und Zeichnigsforme i dem Kapitel lösche?", + "studyRightUnderTheBoard": "Diräkt underhalb vom Brätt", + "studyNoPinnedComment": "Kei", + "studyNormalAnalysis": "Normali Analyse", + "studyHideNextMoves": "Nögschti Züg uusbländä", + "studyInteractiveLesson": "Interaktivi Üäbig", + "studyChapterX": "Kapitäl {param}", + "studyEmpty": "Läär", + "studyStartFromInitialPosition": "Fang vu de Usgangsschtellig a", + "studyEditor": "Ändärä", + "studyStartFromCustomPosition": "Fang vunere benutzerdefinierte Schtellig a", + "studyLoadAGameByUrl": "Lad es Schpiel mit ere URL", + "studyLoadAPositionFromFen": "Lad e Schtellig mit ere FEN", + "studyLoadAGameFromPgn": "Lad Schpiel mit eme PGN", + "studyAutomatic": "Automatisch", + "studyUrlOfTheGame": "URL vu de Schpiel", + "studyLoadAGameFromXOrY": "Lad es Schpiel vo {param1} oder {param2}", + "studyCreateChapter": "Kapitäl ärschtelä", + "studyCreateStudy": "Schtudie erschtelle", + "studyEditStudy": "Schtudie bearbeite", + "studyVisibility": "Sichtbarkeit", + "studyPublic": "Öffentlich", + "studyUnlisted": "Unglischtet", + "studyInviteOnly": "Nur mit Iladig", + "studyAllowCloning": "Chlone erlaube", + "studyNobody": "Niemer", + "studyOnlyMe": "Nur ich", + "studyContributors": "Mitwirkändi", + "studyMembers": "Mitglider", + "studyEveryone": "Alli", + "studyEnableSync": "Sync aktiviärä", + "studyYesKeepEveryoneOnTheSamePosition": "Jawoll: Glichi Schtellig für alli", + "studyNoLetPeopleBrowseFreely": "Nei: Unabhängigi Navigation für alli", + "studyPinnedStudyComment": "Agheftete Schtudiekommentar", + "studyStart": "Schtart", + "studySave": "Schpeichärä", + "studyClearChat": "Tschätt löschä", + "studyDeleteTheStudyChatHistory": "Chatverlauf vu de Schtudie lösche? Das chann nüme rückgängig gmacht werde!", + "studyDeleteStudy": "Schtudie lösche", + "studyConfirmDeleteStudy": "Die ganz Schtudie lösche? Es git keis Zrugg! Gib zur Beschtätigung de Name vu de Schtudie i: {param}", + "studyWhereDoYouWantToStudyThat": "Welli Schtudie wottsch bruche?", + "studyGoodMove": "Guete Zug", + "studyMistake": "Fähler", + "studyBrilliantMove": "Briliantä Zug", + "studyBlunder": "Grobä Patzer", + "studyInterestingMove": "Intressantä Zug", + "studyDubiousMove": "Frogwürdigä Zug", + "studyOnlyMove": "Einzigä Zug", + "studyZugzwang": "Zugzwang", + "studyEqualPosition": "Usglicheni Schtellig", + "studyUnclearPosition": "Unklari Schtellig", + "studyWhiteIsSlightlyBetter": "Wiss schtaht es bitzli besser", + "studyBlackIsSlightlyBetter": "Schwarz schtaht es bitzli besser", + "studyWhiteIsBetter": "Wiss schtaht besser", + "studyBlackIsBetter": "Schwarz schtoht besser", + "studyWhiteIsWinning": "Wiss schtaht uf Gwünn", + "studyBlackIsWinning": "Schwarz schtoht uf Gwünn", + "studyNovelty": "Neuerig", + "studyDevelopment": "Entwicklig", + "studyInitiative": "Initiativä", + "studyAttack": "Agriff", + "studyCounterplay": "Gägeschpiel", + "studyTimeTrouble": "Zitnot", + "studyWithCompensation": "Mit Kompänsation", + "studyWithTheIdea": "Mit dä Idee", + "studyNextChapter": "Nögschts Kapitäl", + "studyPrevChapter": "Vorhärigs Kapitäl", + "studyStudyActions": "Lärnaktionä", + "studyTopics": "Theme", + "studyMyTopics": "Mini Theme", + "studyPopularTopics": "Beliebti Theme", + "studyManageTopics": "Theme verwalte", + "studyBack": "Zrugg", + "studyPlayAgain": "Vo vornä", + "studyWhatWouldYouPlay": "Was würdisch du ih derä Stellig spiele?", + "studyYouCompletedThisLesson": "Gratulation! Du häsch die Lektion abgschlosse.", + "studyNbChapters": "{count, plural, =1{{count} Kapitel} other{{count} Kapitäl}}", + "studyNbGames": "{count, plural, =1{{count} Schpiel} other{{count} Schpiel}}", + "studyNbMembers": "{count, plural, =1{{count} Mitglid} other{{count} Mitglider}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Füeg din PGN Tegscht da i, bis zu {count} Schpiel} other{Füeg din PGN Tegscht da i, bis zu {count} Schpiel}}" } \ No newline at end of file diff --git a/lib/l10n/lila_he.arb b/lib/l10n/lila_he.arb index 3b559ed481..7569832308 100644 --- a/lib/l10n/lila_he.arb +++ b/lib/l10n/lila_he.arb @@ -30,7 +30,6 @@ "mobilePuzzleStormConfirmEndRun": "האם לסיים את הסבב?", "mobilePuzzleStormFilterNothingToShow": "אין מה להראות. ניתן לשנות את חתכי הסינון", "mobileCancelTakebackOffer": "ביטול ההצעה להחזיר את המהלך האחרון", - "mobileCancelDrawOffer": "ביטול הצעת התיקו", "mobileWaitingForOpponentToJoin": "ממתין שיריב יצטרף...", "mobileBlindfoldMode": "משחק עיוור", "mobileLiveStreamers": "שדרנים בשידור חי", @@ -42,11 +41,12 @@ "mobilePuzzleStormSubtitle": "פתרו כמה שיותר חידות ב־3 דקות.", "mobileGreeting": "שלום, {param}", "mobileGreetingWithoutName": "שלום", + "mobilePrefMagnifyDraggedPiece": "הגדלת הכלי הנגרר", "activityActivity": "פעילות", - "activityHostedALiveStream": "עלה (או עלתה) לשידור חי", + "activityHostedALiveStream": "על\\תה לשידור חי", "activityRankedInSwissTournament": "סיים/ה במקום {param1} ב־{param2}", "activitySignedUp": "נרשם/ה לlichess.org", - "activitySupportedNbMonths": "{count, plural, =1{תמכ/ה בליצ'ס במשך חודש {count} כ{param2}} =2{תמכ/ה בליצ'ס במשך {count} חודשים כ{param2}} many{תמכ/ה בליצ'ס במשך {count} חודשים כ{param2}} other{תמכ/ה בליצ'ס במשך {count} חודשים כ{param2}}}", + "activitySupportedNbMonths": "{count, plural, =1{תמכ/ה ב-lichess במשך חודש {count} כ{param2}} =2{תמכ/ה בליצ'ס במשך {count} חודשים כ{param2}} many{תמכ/ה בליצ'ס במשך {count} חודשים כ{param2}} other{תמכ/ה בליצ'ס במשך {count} חודשים כ{param2}}}", "activityPracticedNbPositions": "{count, plural, =1{התאמן/ה על עמדה {count} ב{param2}} =2{התאמן/ה על {count} עמדות ב{param2}} many{התאמן/ה על {count} עמדות ב{param2}} other{התאמן/ה על {count} עמדות ב{param2}}}", "activitySolvedNbPuzzles": "{count, plural, =1{פתר/ה חידה טקטית {count}} =2{פתר/ה {count} חידות טקטיות} many{פתר/ה {count} חידות טקטיות} other{פתר/ה {count} חידות טקטיות}}", "activityPlayedNbGames": "{count, plural, =1{שיחק/ה משחק {param2} {count}} =2{שיחק/ה {count} משחקי {param2}} many{שיחק/ה {count} משחקי {param2}} other{שיחק/ה {count} משחקי {param2}}}", @@ -54,6 +54,7 @@ "activityPlayedNbMoves": "{count, plural, =1{שיחק/ה מהלך {count}} =2{שיחק/ה {count} מהלכים} many{שיחק/ה {count} מהלכים} other{שיחק/ה {count} מהלכים}}", "activityInNbCorrespondenceGames": "{count, plural, =1{במשחק {count} בהתכתבות} =2{ב{count} משחקים בהתכתבות} many{ב{count} משחקים בהתכתבות} other{ב{count} משחקים בהתכתבות}}", "activityCompletedNbGames": "{count, plural, =1{השלים/ה משחק התכתבות {count}} =2{השלים/ה {count} משחקי התכתבות} many{השלים/ה {count} משחקי התכתבות} other{השלים/ה {count} משחקי התכתבות}}", + "activityCompletedNbVariantGames": "{count, plural, =1{השלים/ה משחק התכתבות {count} מסוג {param2}} =2{השלים/ה {count} משחקי התכתבות מסוג {param2}} many{השלים/ה {count} משחקי התכתבות מסוג {param2}} other{השלים/ה {count} משחקי התכתבות מסוג {param2}}}", "activityFollowedNbPlayers": "{count, plural, =1{התחיל/ה לעקוב אחר שחקן {count}} =2{התחיל/ה לעקוב אחר {count} שחקנים} many{התחיל/ה לעקוב אחר {count} שחקנים} other{התחיל/ה לעקוב אחר {count} שחקנים}}", "activityGainedNbFollowers": "{count, plural, =1{השיג/ה עוקב/ת {count} חדש/ה} =2{השיג/ה {count} עוקבים חדשים} many{השיג/ה {count} עוקבים חדשים} other{השיג/ה {count} עוקבים חדשים}}", "activityHostedNbSimuls": "{count, plural, =1{אירח/ה משחק סימולטני {count}} =2{אירח/ה {count} משחקים סימולטניים} many{אירח/ה {count} משחקים סימולטניים} other{אירח/ה {count} משחקים סימולטניים}}", @@ -64,7 +65,72 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{השתתף/ה בטורניר שוויצרי {count}} =2{השתתף/ה ב־{count} טורנירים שוויצריים} many{השתתף/ה ב־{count} טורנירים שוויצריים} other{השתתף/ה ב־{count} טורנירים שוויצריים}}", "activityJoinedNbTeams": "{count, plural, =1{הצטרף/ה לקבוצה {count}} =2{הצטרף/ה ל־{count} קבוצות} many{הצטרף/ה ל־{count} קבוצות} other{הצטרף/ה ל־{count} קבוצות}}", "broadcastBroadcasts": "הקרנות", + "broadcastMyBroadcasts": "ההקרנות שלי", "broadcastLiveBroadcasts": "צפייה ישירה בטורנירים", + "broadcastBroadcastCalendar": "לוח השידורים", + "broadcastNewBroadcast": "הקרנה ישירה חדשה", + "broadcastSubscribedBroadcasts": "הקרנות שנרשמת אליהן", + "broadcastAboutBroadcasts": "הסבר על הקרנות", + "broadcastHowToUseLichessBroadcasts": "איך להשתמש בהקרנות ב־Lichess.", + "broadcastTheNewRoundHelp": "הסבב החדש יכלול את אותם התורמים והחברים כמו בסבב הקודם.", + "broadcastAddRound": "הוספת סבב", + "broadcastOngoing": "כרגע", + "broadcastUpcoming": "בקרוב", + "broadcastCompleted": "שהושלמו", + "broadcastCompletedHelp": "Lichess מאתר מתי הושלם הסבב על פי המשחקים שבקישור למהלכים בשידור חי (המקור). הפעילו את האפשרות הזאת אם אין מקור שממנו נשאבים המשחקים.", + "broadcastRoundName": "שם סבב", + "broadcastRoundNumber": "מספר סבב", + "broadcastTournamentName": "שם הטורניר", + "broadcastTournamentDescription": "תיאור הטורניר בקצרה", + "broadcastFullDescription": "תיאור מלא של הטורניר", + "broadcastFullDescriptionHelp": "תיאור מפורט של הטורניר (אופציונאלי). {param1} זמין. אורך התיאור לא יעלה על {param2} תווים.", + "broadcastSourceSingleUrl": "קישור המקור של ה־PGN", + "broadcastSourceUrlHelp": "הקישור ש־Lichess יבדוק כדי לקלוט עדכונים ב־PGN. הוא חייב להיות פומבי ונגיש דרך האינטרנט.", + "broadcastSourceGameIds": "עד 64 מזהי משחק של Lichess, מופרדים ברווחים.", + "broadcastStartDateTimeZone": "שעת ההתחלה באזור הזמן המקומי של הטורניר: {param}", + "broadcastStartDateHelp": "אופציונאלי, אם את/ה יודע/ת מתי האירוע צפוי להתחיל", + "broadcastCurrentGameUrl": "הקישור למשחק הנוכחי", + "broadcastDownloadAllRounds": "הורדת כל הסבבים", + "broadcastResetRound": "אפס את הסיבוב הזה", + "broadcastDeleteRound": "מחיקת הסבב הזה", + "broadcastDefinitivelyDeleteRound": "מחיקת הסבב הזה והמשחקים שבו לצמיתות", + "broadcastDeleteAllGamesOfThisRound": "מחיקת כל המשחקים בסבב הזה. כדי ליצור אותם מחדש, קישור המקור צריך להיות פעיל.", + "broadcastEditRoundStudy": "עריכת לוח הלמידה של הסבב", + "broadcastDeleteTournament": "מחיקת הטורניר הזה", + "broadcastDefinitivelyDeleteTournament": "מחיקה לצמיתות של הטורניר הזה, על כל סבביו והמשחקים שבו.", + "broadcastShowScores": "הצגת הניקוד של השחקנים בהתבסס על תוצאות המשחקים", + "broadcastReplacePlayerTags": "אופציונאלי: החלפה של שמות השחקנים, דירוגיהם ותאריהם", + "broadcastFideFederations": "איגודי FIDE", + "broadcastTop10Rating": "דירוג עשרת המובילים", + "broadcastFidePlayers": "שחקני FIDE", + "broadcastFidePlayerNotFound": "לא נמצא שחקן FIDE", + "broadcastFideProfile": "פרופיל FIDE", + "broadcastFederation": "איגוד", + "broadcastAgeThisYear": "גיל השנה", + "broadcastUnrated": "לא מדורג", + "broadcastRecentTournaments": "טורנירים אחרונים", + "broadcastOpenLichess": "פתיחה ב־Lichess", + "broadcastTeams": "קבוצות", + "broadcastBoards": "לוחות", + "broadcastOverview": "מידע כללי", + "broadcastSubscribeTitle": "הירשמו כדי לקבל התראה בתחילת כל סבב. ניתן להפעיל או לבטל התראות קופצות או התראות ״פעמון״ בהגדרות החשבון שלך.", + "broadcastUploadImage": "העלאת תמונה עבור הטורניר", + "broadcastNoBoardsYet": "אין עדיין לוחות. הם יופיעו כשיעלו המשחקים.", + "broadcastBoardsCanBeLoaded": "ניתן להעלות לוחות באמצעות קישור מקור או דרך {param}", + "broadcastStartsAfter": "מתחיל אחרי {param}", + "broadcastStartVerySoon": "ההקרנה תחל ממש בקרוב.", + "broadcastNotYetStarted": "ההקרנה טרם החלה.", + "broadcastOfficialWebsite": "האתר הרשמי", + "broadcastStandings": "תוצאות", + "broadcastIframeHelp": "ישנן אפשרויות נוספות ב{param}", + "broadcastWebmastersPage": "עמוד המתכנתים", + "broadcastPgnSourceHelp": "קישור ל־PGN פומבי המתעדכן בשידור חי. אנו מציעים גם {param} לסנכרון מיטבי ומהיר.", + "broadcastEmbedThisBroadcast": "הטמעת ההקרנה באתר האינטרנט שלך", + "broadcastEmbedThisRound": "הטמעת {param} באתר האינטרנט שלך", + "broadcastRatingDiff": "הפרש הדירוג", + "broadcastGamesThisTournament": "משחקים בטורניר זה", + "broadcastScore": "ניקוד", + "broadcastNbBroadcasts": "{count, plural, =1{הקרנה {count}} =2{{count} הקרנות} many{{count} הקרנות} other{{count} הקרנות}}", "challengeChallengesX": "הזמנות למשחק: {param1}", "challengeChallengeToPlay": "הזמינו למשחק", "challengeChallengeDeclined": "ההזמנה למשחק נדחתה", @@ -383,8 +449,8 @@ "puzzleThemeXRayAttackDescription": "כלי המאיים או מגן על משבצת דרך כלי יריב.", "puzzleThemeZugzwang": "כפאי", "puzzleThemeZugzwangDescription": "היריב מוגבל במסעים שביכולתו לבצע, וכל אחד מחמיר את מצבו.", - "puzzleThemeHealthyMix": "שילוב בריא", - "puzzleThemeHealthyMixDescription": "קצת מהכל. לא תדעו למה לצפות. עליכם להיות מוכנים להכל! בדיוק כמו משחקים אמיתיים.", + "puzzleThemeMix": "שילוב בריא", + "puzzleThemeMixDescription": "קצת מהכול. לא תדעו למה לצפות. עליכם להיות מוכנים להכול! בדיוק כמו במשחקים אמיתיים.", "puzzleThemePlayerGames": "המשחקים שלי", "puzzleThemePlayerGamesDescription": "חפשו חידות אשר נוצרו ממשחקים שלכם או של שחקנים אחרים.", "puzzleThemePuzzleDownloadInformation": "החידות האלו הן נחלת הכלל, וניתן להוריד אותן מ־{param}.", @@ -393,7 +459,7 @@ "settingsCloseAccount": "סגירת החשבון", "settingsManagedAccountCannotBeClosed": "חשבונך מנוהל, ולכן לא ניתן לסגור אותו.", "settingsClosingIsDefinitive": "הסגירה היא סופית. אין דרך חזרה. האם את/ה בטוח/ה?", - "settingsCantOpenSimilarAccount": "לא תוכל/י לפתוח חשבון חדש עם אותו השם, אפילו בשינוי אותיות קטנות לגדולות ולהיפך.", + "settingsCantOpenSimilarAccount": "לא תוכל/י לפתוח חשבון חדש עם אותו השם, אפילו בשינוי אותיות קטנות לגדולות והפוך. ", "settingsChangedMindDoNotCloseAccount": "שיניתי את דעתי, אל תסגרו את החשבון שלי", "settingsCloseAccountExplanation": "האם אכן ברצונך לסגור את חשבונך? סגירת חשבונך היא החלטה סופית. לעולם לא יהיה אפשר להתחבר לחשבון הזה שוב.", "settingsThisAccountIsClosed": "החשבון הזה סגור.", @@ -515,7 +581,6 @@ "memory": "זיכרון", "infiniteAnalysis": "ניתוח אינסופי", "removesTheDepthLimit": "מסיר את מגבלת העומק ו\"מחמם\" את המחשב", - "engineManager": "מנהל המנועים", "blunder": "טעות גסה", "mistake": "שגיאה", "inaccuracy": "אי־דיוק", @@ -573,7 +638,7 @@ "error_namePassword": "נא לא להשתמש בשם המשתמש בתור הסיסמה.", "blankedPassword": "השתמשת בסיסמה שלך באתר אחר, ויתכן שהיא מועדת לפריצה. כדי להגן על חשבונך ב־Lichess, עליך להגדיר סיסמה חדשה. תודה על ההבנה.", "youAreLeavingLichess": "את/ה עוזב/ת את Lichess", - "neverTypeYourPassword": "לעולם אל תקלידו את סיסמתכם ב־Lichessבאף אתר אחר!", + "neverTypeYourPassword": "לעולם אל תקלידו את סיסמתכם ב־Lichess באף אתר אחר!", "proceedToX": "מעבר ל־{param}", "passwordSuggestion": "אל תשתמשו בסיסמה שהציע לכם אדם אחר. הוא ישתמש בה כדי לגנוב את חשבונכם!", "emailSuggestion": "אל תשמשו בכתובת מייל שהציע אדם אחר. הוא ישתמש בה כדי לגנוב את חשבונכם.", @@ -597,6 +662,7 @@ "rank": "מיקום", "rankX": "מיקום: {param}", "gamesPlayed": "משחקים בטורניר", + "ok": "אוקיי", "cancel": "ביטול", "whiteTimeOut": "נגמר הזמן ללבן", "blackTimeOut": "נגמר הזמן לשחור", @@ -645,7 +711,7 @@ "freeOnlineChess": "שחמט חינמי ברשת", "exportGames": "ייצוא משחקים", "ratingRange": "טווח דירוג", - "thisAccountViolatedTos": "החשבון הזה הפר את תנאי השימוש של ליצ'ס", + "thisAccountViolatedTos": "החשבון הזה הפר את תנאי השימוש של Lichess", "openingExplorerAndTablebase": "סייר הפתיחות וטבלאות סיומים", "takeback": "החזרת מהלך", "proposeATakeback": "הצע החזרת המהלך האחרון", @@ -766,7 +832,7 @@ "clearBoard": "ניקוי הלוח", "loadPosition": "טעינת עמדה", "isPrivate": "פרטי", - "reportXToModerators": "דווח/י על {param} למנהלים", + "reportXToModerators": "דווח על {param} למנהלים", "profileCompletion": "השלמת הפרופיל: {param}", "xRating": "דירוג {param}", "ifNoneLeaveEmpty": "אם אין, השאירו ריק", @@ -784,7 +850,7 @@ "inlineNotation": "תיאור מהלכים בשורה", "makeAStudy": "כדי לשמור ולשתף, תוכל/י ליצור לוח למידה.", "clearSavedMoves": "הסרת המהלכים", - "previouslyOnLichessTV": "לאחרונה בטלוויזיה של ליצ'ס", + "previouslyOnLichessTV": "לאחרונה בטלוויזיה של Lichess", "onlinePlayers": "שחקנים מחוברים", "activePlayers": "הכי פעילים", "bewareTheGameIsRatedButHasNoClock": "שימו לב! המשחק מדורג אך אין שעון.", @@ -812,14 +878,16 @@ "reply": "תגובה", "message": "הודעה", "createTheTopic": "צור אשכול", - "reportAUser": "דווח/י על משתמש/ת", - "user": "משתמש/ת", + "reportAUser": "דיווח על משתמש/ת", + "user": "משתמש", "reason": "סיבה", "whatIsIheMatter": "מה הבעיה?", "cheat": "רמאות", "troll": "הטרלה", "other": "אחר", - "reportDescriptionHelp": "הדביקו את הקישור למשחק(ים) והסבירו מה לא בסדר בהתנהגות המשתמש. אל תכתבו סתם ״השחקן/ית מרמה״, הסבירו לנו כיצד הגעתם למסקנה הזו. הדיווח יטופל מהר יותר אם ייכתב באנגלית.", + "reportCheatBoostHelp": "הדביקו את הקישור למשחקים שעליהם תרצו לדווח והסבירו מה הבעיה בהתנהגות המשתמש כפי שהיא משתקפת בהם. אל תכתבו סתם ״השחקן מרמה״. הסבירו לנו כיצד הגעתם למסקנה הזו.", + "reportUsernameHelp": "הסבירו מה פוגעני בשם המשתמש הזה. אל תכתבו סתם ״שם המשתמש פוגעני״. הסבירו לנו כיצד הגעתם למסקנה הזו, במיוחד אם ההעלבה מוסווית, בשפה זרה (שאינה אנגלית), בלשון סלנג או תלויית תרבות והיסטוריה.", + "reportProcessedFasterInEnglish": "הדיווח שלך יטופל מהר יותר אם ייכתב באנגלית.", "error_provideOneCheatedGameLink": "בבקשה לספק לפחות קישור אחד למשחק עם רמאות.", "by": "על־ידי {param}", "importedByX": "יובא ע\"י {param}", @@ -872,7 +940,7 @@ "side": "צד", "clock": "שעון", "opponent": "יריב", - "learnMenu": "למד/י", + "learnMenu": "למדו", "studyMenu": "לוחות למידה", "practice": "תרגול", "community": "קהילה", @@ -934,7 +1002,7 @@ "simulHostExtraTime": "זמן נוסף למארח/ת", "simulAddExtraTimePerPlayer": "הוספת זמן לשעון שלך בכל פעם שמצטרף/ת שחקן/ית למשחק הסימולטני.", "simulHostExtraTimePerPlayer": "זמן נוסף למארח/ת עבור כל שחקן/ית שמצטרף/ת", - "lichessTournaments": "טורנירים של ליצ'ס", + "lichessTournaments": "טורנירים של Lichess", "tournamentFAQ": "שאלות נפוצות לגבי טורנירי הזירה", "timeBeforeTournamentStarts": "זמן לתחילת הטורניר", "averageCentipawnLoss": "אובדן מאית־חייל ממוצע", @@ -980,7 +1048,7 @@ "weHaveSentYouAnEmailTo": "שלחנו אימייל ל{param}. לחץ על הלינק באימייל כדי לאפס את הסיסמה.", "byRegisteringYouAgreeToBeBoundByOur": "על ידי הרשמה, את/ה מסכים/ה ל{param}.", "readAboutOur": "קראו את {param}.", - "networkLagBetweenYouAndLichess": "עיכוב הרשת בינך לבין ליצ'ס", + "networkLagBetweenYouAndLichess": "עיכוב הרשת בינך לבין Lichess", "timeToProcessAMoveOnLichessServer": "זמן לעיבוד מהלך בשרת ליצ'ס", "downloadAnnotated": "הורדה עם הערות", "downloadRaw": "הורדה ללא הערות", @@ -998,7 +1066,7 @@ "kidMode": "מצב ילדים", "kidModeIsEnabled": "מצב ילדים מופעל.", "kidModeExplanation": "בשביל הבטיחות. במצב ילדים, כל אמצעי התקשורת באתר מבוטלים. הפעילו אופציה זו עבור ילדיכם ועבור תלמידי בית ספר. זאת כדי להגן עליהם מפני משתמשים אחרים.", - "inKidModeTheLichessLogoGetsIconX": "במצב ילדים הסמל של ליצ'ס מקבל אייקון {param}, כדי שתדעו שילדיכם מוגנים.", + "inKidModeTheLichessLogoGetsIconX": "במצב ילדים הסמל של Lichess מקבל אייקון {param}, כדי שתדעו שילדיכם מוגנים.", "askYourChessTeacherAboutLiftingKidMode": "החשבון שלך מנוהל. תוכל/י לבקש מהמורה שלך לשחמט להסיר את מצב הילדים.", "enableKidMode": "הפעילו מצב ילדים", "disableKidMode": "בטל מצב ילדים", @@ -1121,8 +1189,8 @@ "searchOrStartNewDiscussion": "חפשו את התחילו שיחה חדשה", "edit": "עריכה", "bullet": "Bullet", - "blitz": "Blitz", - "rapid": "Rapid", + "blitz": "בזק", + "rapid": "זריז", "classical": "Classical", "ultraBulletDesc": "משחקים מהירים בטירוף: פחות מ־30 שניות על השעון", "bulletDesc": "משחקים מהירים מאוד: פחות מ3 דקות על השעון", @@ -1217,6 +1285,7 @@ "showMeEverything": "הראו לי הכל", "lichessPatronInfo": "ליצ'ס הוא ארגון לטובת הכלל ותוכנת קוד פתוח חינמית.\nכל עלויות התפעול, הפיתוח והתוכן ממומנות אך ורק על ידי תרומות משתמשים.", "nothingToSeeHere": "אין כלום להצגה כאן, בינתיים.", + "stats": "סטטיסטיקות", "opponentLeftCounter": "{count, plural, =1{יריבך עזב את המשחק. תוכל/י להכריז על נצחון בעוד שנייה {count}.} =2{יריבך עזב את המשחק. תוכל/י להכריז על ניצחון בעוד {count} שניות.} many{יריבך עזב את המשחק. תוכל/י לדרוש ניצחון בעוד {count} שניות.} other{יריבך עזב את המשחק. תוכל/י לדרוש ניצחון בעוד {count} שניות.}}", "mateInXHalfMoves": "{count, plural, =1{מט בעוד חצי מהלך {count}} =2{מט בעוד {count} חצאי מהלכים} many{מט בעוד {count} חצאי מהלכים} other{מט בעוד {count} חצאי מהלכים}}", "nbBlunders": "{count, plural, =1{{count} טעות גסה} =2{{count} טעויות גסות} many{{count} טעויות גסות} other{{count} טעויות גסות}}", @@ -1313,6 +1382,159 @@ "stormXRuns": "{count, plural, =1{ניסיון אחד} =2{{count} ניסיונות} many{{count} ניסיונות} other{{count} נסיונות}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{שוחקה ריצה אחת של {param2}} =2{שוחקו {count} ריצות של {param2}} many{שוחקו {count} ריצות של {param2}} other{שוחקו {count} ריצות של {param2}}}", "streamerLichessStreamers": "שדרני Lichess", + "studyPrivate": "פרטי", + "studyMyStudies": "לוחות הלמידה שלי", + "studyStudiesIContributeTo": "לוחות למידה שתרמתי להם", + "studyMyPublicStudies": "לוחות הלמידה הפומביים שלי", + "studyMyPrivateStudies": "לוחות הלמידה הפרטיים שלי", + "studyMyFavoriteStudies": "לוחות הלמידה המועדפים שלי", + "studyWhatAreStudies": "מה הם לוחות למידה?", + "studyAllStudies": "כל לוחות הלמידה", + "studyStudiesCreatedByX": "לוחות למידה שנוצרו על ידי {param}", + "studyNoneYet": "אין עדיין.", + "studyHot": "כוכבים עולים", + "studyDateAddedNewest": "תאריך הוספה (החדש ביותר)", + "studyDateAddedOldest": "תאריך הוספה (הישן ביותר)", + "studyRecentlyUpdated": "עודכן לאחרונה", + "studyMostPopular": "הכי פופולריים", + "studyAlphabetical": "בסדר האלפבית", + "studyAddNewChapter": "הוסיפו פרק חדש", + "studyAddMembers": "הוספת משתמשים", + "studyInviteToTheStudy": "הזמינו ללוח הלמידה", + "studyPleaseOnlyInvitePeopleYouKnow": "אנא הזמינו רק שחקנים שאתם מכירים המעוניינים להצטרף ללוח הלמידה הזה.", + "studySearchByUsername": "חיפוש לפי שם משתמש", + "studySpectator": "צופה", + "studyContributor": "תורם", + "studyKick": "הסרה", + "studyLeaveTheStudy": "צא/י מלוח הלמידה", + "studyYouAreNowAContributor": "כעת את/ה תורם/ת", + "studyYouAreNowASpectator": "כעת את/ת צופה", + "studyPgnTags": "תוויות PGN", + "studyLike": "אהבתי", + "studyUnlike": "ביטול \"אהבתי\"", + "studyNewTag": "תג חדש", + "studyCommentThisPosition": "הערה לגבי העמדה", + "studyCommentThisMove": "הערה לגבי המסע", + "studyAnnotateWithGlyphs": "השתמשו בסימנים מוסכמים כדי להגיב על מהלכים", + "studyTheChapterIsTooShortToBeAnalysed": "פרק זה קצר מכדי להצדיק ניתוח.", + "studyOnlyContributorsCanRequestAnalysis": "רק תורמי לוח הלמידה יכולים לבקש ניתוח ממוחשב.", + "studyGetAFullComputerAnalysis": "קבל/י ניתוח צד־שרת מלא של המסעים העיקריים (mainline).", + "studyMakeSureTheChapterIsComplete": "ניתן לבקש ניתוח ממוחשב רק פעם אחת, ולכן ודאו שהפרק הושלם.", + "studyAllSyncMembersRemainOnTheSamePosition": "כולם צופים באותה העמדה", + "studyShareChanges": "שתפו שינויים עם הצופים ושמרו אותם על השרת", + "studyPlaying": "מתקיים כעת", + "studyShowEvalBar": "מדי הערכה", + "studyFirst": "ראשון", + "studyPrevious": "הקודם", + "studyNext": "הבא", + "studyLast": "אחרון", "studyShareAndExport": "שיתוף & ייצוא", - "studyStart": "שמירה" + "studyCloneStudy": "שכפול", + "studyStudyPgn": "ה-PGN של לוח הלמידה", + "studyDownloadAllGames": "הורדת כל המשחקים", + "studyChapterPgn": "ה-PGN של הפרק", + "studyCopyChapterPgn": "העתקת ה־PGN", + "studyDownloadGame": "הורדת המשחק", + "studyStudyUrl": "כתובת לוח הלמידה", + "studyCurrentChapterUrl": "כתובת האינטרנט של הפרק הנוכחי", + "studyYouCanPasteThisInTheForumToEmbed": "את/ה יכול/ה לפרסם את זה בפורום כדי להטמיע", + "studyStartAtInitialPosition": "התחילו בעמדת הפתיחה", + "studyStartAtX": "התחילו ב{param}", + "studyEmbedInYourWebsite": "הטמעה באתר שלך", + "studyReadMoreAboutEmbedding": "קראו עוד על הטמעה", + "studyOnlyPublicStudiesCanBeEmbedded": "ניתן להטמיע אך ורק לוחות למידה פומביים!", + "studyOpen": "פתח", + "studyXBroughtToYouByY": "{param1}, מוגש על ידי {param2}", + "studyStudyNotFound": "לוח הלמידה לא נמצא", + "studyEditChapter": "עריכת הפרק", + "studyNewChapter": "פרק חדש", + "studyImportFromChapterX": "ייבא מתוך {param}", + "studyOrientation": "כיוון הלוח", + "studyAnalysisMode": "מצב ניתוח", + "studyPinnedChapterComment": "תגובה מוצמדת לפרק", + "studySaveChapter": "שמור פרק", + "studyClearAnnotations": "נקה הערות", + "studyClearVariations": "נקה וריאציות", + "studyDeleteChapter": "מחיקת הפרק", + "studyDeleteThisChapter": "למחוק את הפרק? אין דרך חזרה!", + "studyClearAllCommentsInThisChapter": "ניקוי כל ההערות, הרישומים והציורים בפרק זה", + "studyRightUnderTheBoard": "ממש מתחת ללוח", + "studyNoPinnedComment": "ללא", + "studyNormalAnalysis": "ניתוח רגיל", + "studyHideNextMoves": "הסתרת המסעים הבאים", + "studyInteractiveLesson": "שיעור אינטראקטיבי", + "studyChapterX": "פרק {param}", + "studyEmpty": "ריק", + "studyStartFromInitialPosition": "התחילו מהעמדה ההתחלתית", + "studyEditor": "עורך", + "studyStartFromCustomPosition": "התחילו מעמדה מותאמת אישית", + "studyLoadAGameByUrl": "טען משחק ע\"י כתובת אינטרנט", + "studyLoadAPositionFromFen": "טען עמדה מFEN", + "studyLoadAGameFromPgn": "טען משחק מPGN", + "studyAutomatic": "אוטומטי", + "studyUrlOfTheGame": "כתובת אינטרנטית של משחק", + "studyLoadAGameFromXOrY": "טען משחק מ{param1} או מ{param2}", + "studyCreateChapter": "צור פרק", + "studyCreateStudy": "יצירת לוח למידה", + "studyEditStudy": "עריכת לוח למידה", + "studyVisibility": "חשיפה", + "studyPublic": "פומבי", + "studyUnlisted": "באמצעות קישור", + "studyInviteOnly": "מוזמנים בלבד", + "studyAllowCloning": "אפשרו יצירת עותקים", + "studyNobody": "אף אחד", + "studyOnlyMe": "רק אני", + "studyContributors": "תורמים", + "studyMembers": "חברים", + "studyEveryone": "כולם", + "studyEnableSync": "הפעל סנכרון", + "studyYesKeepEveryoneOnTheSamePosition": "כן: שמור את כולם באותה העמדה", + "studyNoLetPeopleBrowseFreely": "לא: תן לאנשים לדפדף בחופשיות", + "studyPinnedStudyComment": "תגובה מוצמדת ללוח הלמידה", + "studyStart": "שמירה", + "studySave": "שמירה", + "studyClearChat": "ניקוי הצ'אט", + "studyDeleteTheStudyChatHistory": "למחוק את היסטוריית הצ'אט של לוח הלמידה? אין דרך חזרה!", + "studyDeleteStudy": "מחיקת לוח למידה", + "studyConfirmDeleteStudy": "האם למחוק את כל לוח הלמידה? אין דרך חזרה! הקלידו את שם לוח הלמידה לאישור: {param}", + "studyWhereDoYouWantToStudyThat": "היכן ליצור את לוח הלמידה?", + "studyGoodMove": "מסע טוב", + "studyMistake": "טעות", + "studyBrilliantMove": "מסע מבריק", + "studyBlunder": "טעות חמורה", + "studyInterestingMove": "מסע מעניין", + "studyDubiousMove": "מסע מפוקפק", + "studyOnlyMove": "המסע היחיד", + "studyZugzwang": "כפאי", + "studyEqualPosition": "עמדה מאוזנת", + "studyUnclearPosition": "עמדה לא ברורה", + "studyWhiteIsSlightlyBetter": "יתרון קל ללבן", + "studyBlackIsSlightlyBetter": "יתרון קל לשחור", + "studyWhiteIsBetter": "יתרון ללבן", + "studyBlackIsBetter": "יתרון לשחור", + "studyWhiteIsWinning": "יתרון מכריע ללבן", + "studyBlackIsWinning": "יתרון מכריע לשחור", + "studyNovelty": "חידוש", + "studyDevelopment": "פיתוח", + "studyInitiative": "יוזמה", + "studyAttack": "התקפה", + "studyCounterplay": "מתקפת נגד", + "studyTimeTrouble": "מצוקת זמן", + "studyWithCompensation": "עם פיצוי", + "studyWithTheIdea": "עם הרעיון", + "studyNextChapter": "הפרק הבא", + "studyPrevChapter": "הפרק הקודם", + "studyStudyActions": "פעולות לוח למידה", + "studyTopics": "נושאים", + "studyMyTopics": "הנושאים שלי", + "studyPopularTopics": "נושאים פופולריים", + "studyManageTopics": "עריכת נושאים", + "studyBack": "חזרה", + "studyPlayAgain": "הפעל שוב", + "studyWhatWouldYouPlay": "מה הייתם משחקים בעמדה הזו?", + "studyYouCompletedThisLesson": "מזל טוב! סיימתם את השיעור.", + "studyNbChapters": "{count, plural, =1{פרק {count}} =2{{count} פרקים} many{{count} פרקים} other{{count} פרקים}}", + "studyNbGames": "{count, plural, =1{{count} משחק} =2{{count} משחקים} many{{count} משחקים} other{{count} משחקים}}", + "studyNbMembers": "{count, plural, =1{משתמש אחד} =2{{count} משתמשים} many{{count} משתמשים} other{{count} משתמשים}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{הדבק את טקסט הPGN שלך כאן, עד למשחק {count}} =2{הדבק את טקסט הPGN שלך כאן, עד ל{count} משחקים} many{הדבק את טקסט הPGN שלך כאן, עד ל{count} משחקים} other{הדבק את טקסט הPGN שלך כאן, עד ל{count} משחקים}}" } \ No newline at end of file diff --git a/lib/l10n/lila_hi.arb b/lib/l10n/lila_hi.arb index 6639517c23..e003d2369a 100644 --- a/lib/l10n/lila_hi.arb +++ b/lib/l10n/lila_hi.arb @@ -20,7 +20,6 @@ "mobileHideVariation": "वेरिएशन छुपाए", "mobileShowComments": "कमेंट्स देखें", "mobileCancelTakebackOffer": "Takeback प्रस्ताव रद्द करें", - "mobileCancelDrawOffer": "Draw प्रस्ताव रद्द करें", "mobileLiveStreamers": "लाइव स्ट्रीमर्स", "activityActivity": "कार्यकलाप", "activityHostedALiveStream": "एक लाइव स्ट्रीम होस्ट किया गया", @@ -44,7 +43,31 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{{count} स्विस टूर्नामेंट में भाग लिया} other{{count} स्विस टुर्नामेंटों में भाग लिया}}", "activityJoinedNbTeams": "{count, plural, =1{{count} टीम में शामिल हुए} other{{count} टीमों में शामिल हुए}}", "broadcastBroadcasts": "प्रसारण", + "broadcastMyBroadcasts": "मेरा प्रसारण", "broadcastLiveBroadcasts": "लाइव टूर्नामेंट प्रसारण", + "broadcastNewBroadcast": "नया लाइव प्रसारण", + "broadcastAddRound": "एक दौर जोड़ें", + "broadcastOngoing": "चल रही है", + "broadcastUpcoming": "आगामी", + "broadcastCompleted": "पूर्ण", + "broadcastRoundName": "दौर का नाम", + "broadcastRoundNumber": "दौर संख्या", + "broadcastTournamentName": "प्रतियोगिता का नाम", + "broadcastTournamentDescription": "संक्षिप्त प्रतियोगिता वर्णन", + "broadcastFullDescription": "संक्षिप्त वर्णन", + "broadcastFullDescriptionHelp": "प्रसारण का वैकल्पिक लंबा विवरण. {param1} उपलब्ध है. लंबाई {param2} से कम होना चाहिए", + "broadcastSourceUrlHelp": "URL जो Lichess PGN अपडेट प्राप्त करने के लिए जाँच करेगा। यह सार्वजनिक रूप से इंटरनेट पर सुलभ होना चाहिए।", + "broadcastStartDateHelp": "वैकल्पिक, यदि आप जानना चाहते हो की प्रतिस्प्रधा कब शुरू होगी", + "broadcastCurrentGameUrl": "वर्तमान अध्याय URL", + "broadcastDownloadAllRounds": "सभी राउंड डाउनलोड करें", + "broadcastResetRound": "इस फॉर्म को रीसेट करें", + "broadcastDeleteRound": "इस राउंड को डिलीट करें", + "broadcastDefinitivelyDeleteRound": "राउंड और उसके सभी गेम को निश्चित रूप से हटा दें।", + "broadcastDeleteAllGamesOfThisRound": "इस दौर के सभी गेम हटाएं. उन्हें पुनः बनाने के लिए स्रोत को सक्रिय होने की आवश्यकता होगी।", + "broadcastEditRoundStudy": "राउंड स्टडी संपादित करें", + "broadcastDeleteTournament": "इस टूर्नामेंट को हटाएं", + "broadcastDefinitivelyDeleteTournament": "संपूर्ण टूर्नामेंट, उसके सभी राउंड और उसके सभी गेम को निश्चित रूप से हटा दें।", + "broadcastNbBroadcasts": "{count, plural, =1{{count} प्रसारण} other{{count} प्रसारण}}", "challengeChallengeToPlay": "एक खेल के लिए चुनौती", "challengeChallengeDeclined": "चुनौती इंकार कर दिया", "challengeChallengeAccepted": "चुनौती स्वीकार की गई!", @@ -338,8 +361,8 @@ "puzzleThemeXRayAttackDescription": "एक टुकड़ा एक दुश्मन के टुकड़े के माध्यम से एक वर्ग पर हमला करता है या बचाव करता है।", "puzzleThemeZugzwang": "ज़ुग्ज्वांग", "puzzleThemeZugzwangDescription": "प्रतिद्वंद्वी उन चालों में सीमित है जो वे कर सकते हैं, और सभी चालें उनकी स्थिति को खराब करती हैं।", - "puzzleThemeHealthyMix": "स्वस्थ मिश्रण", - "puzzleThemeHealthyMixDescription": "सब का कुछ कुछ। आप नहीं जानते कि क्या उम्मीद है, इसलिए आप किसी भी चीज़ के लिए तैयार रहें! बिल्कुल असली खेल की तरह।", + "puzzleThemeMix": "स्वस्थ मिश्रण", + "puzzleThemeMixDescription": "सब का कुछ कुछ। आप नहीं जानते कि क्या उम्मीद है, इसलिए आप किसी भी चीज़ के लिए तैयार रहें! बिल्कुल असली खेल की तरह।", "puzzleThemePlayerGames": "खिलाड़ियों के खेल", "searchSearch": "खोजें", "settingsSettings": "व्यवस्था (सेटिंग्स)", @@ -466,7 +489,6 @@ "memory": "स्मृति (मेमोरी)", "infiniteAnalysis": "अनंत विश्लेषण", "removesTheDepthLimit": "गहराई सीमा को निकालता है, और आपके कंप्यूटर को गर्म रखता है", - "engineManager": "इंजन प्रबंधक", "blunder": "भयंकर गलती", "mistake": "ग़लती", "inaccuracy": "गलती", @@ -760,7 +782,6 @@ "cheat": "धोखेबाज़ी", "troll": "ट्रोल", "other": "दूसरा", - "reportDescriptionHelp": "खेल/खेलों के लिंक को लगाएं (paste) और बताएँ की यूज़र के व्यवहार में क्या खराबी है|", "error_provideOneCheatedGameLink": "कृपया ठगे गए खेल के लिए कम से कम एक लिंक प्रदान करें।", "by": "{param} द्वारा", "importedByX": "{param} द्वारा आयातित", @@ -1124,6 +1145,7 @@ "closingAccountWithdrawAppeal": "आपका खाता बंद करने से आपकी अपील वापस ले ली जाएगी", "ourEventTips": "कार्यक्रम आयोजित करने कि सलाह", "lichessPatronInfo": "Lichess एक चैरिटी और पूरी तरह से फ्री/लिबर ओपन सोर्स सॉफ्टवेयर है।\nसभी परिचालन लागत, विकास और सामग्री पूरी तरह से उपयोगकर्ता दान द्वारा वित्त पोषित हैं।", + "nothingToSeeHere": "इस समय यहां देखने को कुछ भी नहीं है।", "opponentLeftCounter": "{count, plural, =1{आपके प्रतिद्वंद्वी ने खेल छोड़ दिया। आप {count} सेकंड में जीत का दावा कर सकते हैं।} other{आपके प्रतिद्वंद्वी ने खेल छोड़ दिया। आप {count} सेकंड में जीत का दावा कर सकते हैं।}}", "mateInXHalfMoves": "{count, plural, =1{{count} हाफ मूव में मेट} other{{count} आधे-कदम में चेकमैट}}", "nbBlunders": "{count, plural, =1{{count} गंभीर गलती} other{{count} गंभीर गल्तियां}}", @@ -1212,6 +1234,158 @@ "stormAllTime": "सब समय", "stormXRuns": "{count, plural, =1{एक प्रयास} other{{count} रन}}", "streamerLichessStreamers": "लिचेस स्ट्रीमर", + "studyPrivate": "गोपनीय", + "studyMyStudies": "मेरे अध्ययन", + "studyStudiesIContributeTo": "मेरे योगदान वाले अध्ययन", + "studyMyPublicStudies": "मेरे सार्वजनिक अध्ययन", + "studyMyPrivateStudies": "मेरे निजी अध्ययन", + "studyMyFavoriteStudies": "मेरे पसंदीदा अध्ययन", + "studyWhatAreStudies": "अध्ययन सामग्री क्या है", + "studyAllStudies": "सभी अध्ययन", + "studyStudiesCreatedByX": "{param} द्वारा बनाए गए अध्ययन", + "studyNoneYet": "अभी तक नहीं।", + "studyHot": "लोकप्रिय", + "studyDateAddedNewest": "जोड़ा गया (नवीनतम)", + "studyDateAddedOldest": "जोड़ा गया (सबसे पुराना)", + "studyRecentlyUpdated": "हाल ही में अद्यतित", + "studyMostPopular": "सबसे लोकप्रिय", + "studyAlphabetical": "वर्णक्रमानुसार", + "studyAddNewChapter": "एक नया अध्याय जोड़ें", + "studyAddMembers": "सदस्य जोड़ें", + "studyInviteToTheStudy": "अध्ययन के लिए आमंत्रित करें", + "studyPleaseOnlyInvitePeopleYouKnow": "कृपया केवल उन लोगों को आमंत्रित करें जिन्हें आप जानते हैं, और जो इस अध्ययन में सक्रिय रूप से शामिल होना चाहते हैं।", + "studySearchByUsername": "यूज़रनेम से खोजें", + "studySpectator": "दर्शक", + "studyContributor": "योगदानकर्ता", + "studyKick": "बाहर निकालें", + "studyLeaveTheStudy": "अध्ययन छोड़े", + "studyYouAreNowAContributor": "अब आप एक योगदानकर्ता हैं", + "studyYouAreNowASpectator": "अब आप एक दर्शक हैं", + "studyPgnTags": "PGN टैग", + "studyLike": "लाइक", + "studyUnlike": "नापसन्द करे", + "studyNewTag": "नया टैग", + "studyCommentThisPosition": "इस स्थिति पर टिप्पणी करें", + "studyCommentThisMove": "इस चाल पर टिप्पणी करें", + "studyAnnotateWithGlyphs": "प्रतीक के साथ टिप्पणी करें", + "studyTheChapterIsTooShortToBeAnalysed": "यह अध्याय विश्लेषण के लिए बहुत छोटा है", + "studyOnlyContributorsCanRequestAnalysis": "केवल अध्ययन योगदानकर्ता ही कंप्यूटर विश्लेषण का अनुरोध कर सकते हैं।", + "studyGetAFullComputerAnalysis": "मेनलाइन का पूर्ण सर्वर-साइड कंप्यूटर विश्लेषण प्राप्त करें।", + "studyMakeSureTheChapterIsComplete": "सुनिश्चित करें कि अध्याय पूरा हो गया है। आप केवल एक बार विश्लेषण का अनुरोध कर सकते हैं", + "studyAllSyncMembersRemainOnTheSamePosition": "सभी SYNC सदस्य एक ही स्थिति पर रहेंगे", + "studyShareChanges": "दर्शकों के साथ परिवर्तन साझा करें और उन्हें सर्वर पर सहेजें", + "studyPlaying": "वर्तमान खेल", + "studyFirst": "प्रथम", + "studyPrevious": "पिछला", + "studyNext": "अगला", + "studyLast": "अंतिम", "studyShareAndExport": "शेयर & एक्सपोर्ट करें", - "studyStart": "शुरू करिए" + "studyCloneStudy": "प्रतिलिपि", + "studyStudyPgn": "PGN का अध्ययन करें", + "studyDownloadAllGames": "सभी खेल नीचे लादें", + "studyChapterPgn": "अध्याय PGN", + "studyCopyChapterPgn": "पीजीएन की नकल लें", + "studyDownloadGame": "खेल नीचे लादें", + "studyStudyUrl": "अध्ययन का URL", + "studyCurrentChapterUrl": "वर्तमान अध्याय URL", + "studyYouCanPasteThisInTheForumToEmbed": "आप अध्याय को जोड़ने के लिए इसे फ़ोरम में जोर सकते हैं", + "studyStartAtInitialPosition": "प्रारंभिक स्थिति में शुरू करें", + "studyStartAtX": "{param} से प्रारंभ करें", + "studyEmbedInYourWebsite": "अपनी वेबसाइट अथवा ब्लॉग पर प्रकाशित करें", + "studyReadMoreAboutEmbedding": "एम्बेड करने के बारे में और पढ़ें", + "studyOnlyPublicStudiesCanBeEmbedded": "केवल सार्वजनिक अध्ययनों को एम्बेड किया जा सकता है!", + "studyOpen": "खोलें", + "studyXBroughtToYouByY": "{param1}, {param2} द्वारा आपके लिए", + "studyStudyNotFound": "अध्ययन नहीं मिला", + "studyEditChapter": "अध्याय संपादित करें", + "studyNewChapter": "नया अध्याय", + "studyImportFromChapterX": "{param} से आयात करें", + "studyOrientation": "अभिविन्यास", + "studyAnalysisMode": "विश्लेषण प्रणाली", + "studyPinnedChapterComment": "अध्याय पर की गयी महत्वपूर्ण टिप्पणी", + "studySaveChapter": "अध्याय सहेजें", + "studyClearAnnotations": "टिप्पणी मिटाएँ", + "studyClearVariations": "विविधताओं को मिटाये", + "studyDeleteChapter": "अध्याय हटाएं", + "studyDeleteThisChapter": "इस अध्याय को हटाएं? हटाने के पश्चात वापसी नहीं होगी!", + "studyClearAllCommentsInThisChapter": "इस अध्याय में सभी टिप्पणियाँ, प्रतीक, और आकृतियाँ साफ़ करें?", + "studyRightUnderTheBoard": "बोर्ड के ठीक नीचे", + "studyNoPinnedComment": "खाली", + "studyNormalAnalysis": "सामान्य विश्लेषण", + "studyHideNextMoves": "अगली चालें छिपाएँ", + "studyInteractiveLesson": "संवादमूलक सबक", + "studyChapterX": "अध्याय {param}", + "studyEmpty": "खाली", + "studyStartFromInitialPosition": "प्रारंभिक स्थिति से शुरू करें", + "studyEditor": "संपादक", + "studyStartFromCustomPosition": "कृत्रिम स्थिति से शुरू करें", + "studyLoadAGameByUrl": "URL द्वारा एक गेम लोड करें", + "studyLoadAPositionFromFen": "FEN द्वारा स्थिति लोड करें", + "studyLoadAGameFromPgn": "PGN से एक गेम लोड करें", + "studyAutomatic": "स्वचालित", + "studyUrlOfTheGame": "खेल का URL", + "studyLoadAGameFromXOrY": "{param1} या {param2} से एक गेम लोड करें", + "studyCreateChapter": "अध्याय बनाएँ", + "studyCreateStudy": "अध्ययन बनाएँ", + "studyEditStudy": "अध्ययन संपादित करें", + "studyVisibility": "दृश्यता", + "studyPublic": "सार्वजनिक", + "studyUnlisted": "असूचीबद्ध", + "studyInviteOnly": "केवल आमंत्रित", + "studyAllowCloning": "नकल की अनुमति दें", + "studyNobody": "कोई भी नहीं", + "studyOnlyMe": "केवल मैं", + "studyContributors": "योगदानकर्ता", + "studyMembers": "सदस्य", + "studyEveryone": "सभी", + "studyEnableSync": "Sync चालू", + "studyYesKeepEveryoneOnTheSamePosition": "जी हां सभी को एक ही स्थान पर रखे", + "studyNoLetPeopleBrowseFreely": "नहीं सभी लोगो को अपनी इच्छा से ब्राउज करने दें", + "studyPinnedStudyComment": "रुकिए पढ़िए विचार रखिए", + "studyStart": "शुरू करिए", + "studySave": "बचा कर रखिए", + "studyClearChat": "बातें मिटा दे", + "studyDeleteTheStudyChatHistory": "क्या इस पड़ाई से सम्बन्धित बातों को मिटा देना चाहिए? इससे पीछे जाने का कोई रास्ता शेष नहीं है!", + "studyDeleteStudy": "अध्याय को मिटा दे", + "studyConfirmDeleteStudy": "संपूर्ण अध्ययन हटाएं? वहां से कोई वापसी नहीं है! पुष्टि करने के लिए अध्ययन का नाम टाइप करें:{param}", + "studyWhereDoYouWantToStudyThat": "आप इसको खा से पड़ना चाहते है", + "studyGoodMove": "अच्छी चाल!", + "studyMistake": "ग़लती", + "studyBrilliantMove": "अद्भुत चाल​।", + "studyBlunder": "भयंकर गलती", + "studyInterestingMove": "दिलचस्प चाल​ |", + "studyDubiousMove": "संदिग्ध चाल", + "studyOnlyMove": "इकलौता चाल", + "studyZugzwang": "जबरन चाल", + "studyEqualPosition": "बराबर स्थिति", + "studyUnclearPosition": "अस्पष्ट स्थिति", + "studyWhiteIsSlightlyBetter": "सफेद थोड़ा सा बेहतर है", + "studyBlackIsSlightlyBetter": "काला थोड़ा बेहतर है", + "studyWhiteIsBetter": "सफेद बेहतर है!", + "studyBlackIsBetter": "काला बेहतर है।", + "studyWhiteIsWinning": "सफेद जीत रहा है", + "studyBlackIsWinning": "काला जीत रहा है", + "studyNovelty": "नवीनता", + "studyDevelopment": "विकास", + "studyInitiative": "पहल", + "studyAttack": "आक्रमण", + "studyCounterplay": "काउंटरप्ले", + "studyTimeTrouble": "समय की समस्या", + "studyWithCompensation": "लग मुआवजा।", + "studyWithTheIdea": "विचीर के साथ।", + "studyNextChapter": "अगला अध्याय।", + "studyPrevChapter": "पिछला अध्याय।", + "studyStudyActions": "अध्ययन क्रिया", + "studyTopics": "विषय", + "studyMyTopics": "मेरे विषय", + "studyPopularTopics": "लोकप्रिय विषय", + "studyManageTopics": "विषय प्रबंधन", + "studyBack": "पीछे", + "studyPlayAgain": "फिर से खेलेंगे?", + "studyWhatWouldYouPlay": "आप इस स्थिति में क्या खेलेंगे?", + "studyYouCompletedThisLesson": "बधाई हो! आपने यह सबक पूरा कर लिया है।", + "studyNbChapters": "{count, plural, =1{{count} अध्याय} other{{count} अध्याय}}", + "studyNbGames": "{count, plural, =1{{count} खेल} other{{count} खेल}}", + "studyNbMembers": "{count, plural, =1{{count} सदस्य} other{{count} सदस्य}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{यहां अपना PGN टेक्स्ट डाले,{count} खेल तक} other{यहां अपना PGN टेक्स्ट डाले,{count} खेल तक}}" } \ No newline at end of file diff --git a/lib/l10n/lila_hr.arb b/lib/l10n/lila_hr.arb index 50e1c14efa..2e5ceee98b 100644 --- a/lib/l10n/lila_hr.arb +++ b/lib/l10n/lila_hr.arb @@ -22,6 +22,27 @@ "activityJoinedNbTeams": "{count, plural, =1{Pridružio/la se {count} timu} few{Pridružio/la se {count} tima} other{Pridružio/la se {count} timova}}", "broadcastBroadcasts": "Prijenosi", "broadcastLiveBroadcasts": "Prijenosi turnira uživo", + "broadcastNewBroadcast": "Novi prijenos uživo", + "broadcastAddRound": "Dodajte rundu", + "broadcastOngoing": "U tijeku", + "broadcastUpcoming": "Nadolazi", + "broadcastCompleted": "Završeno", + "broadcastRoundName": "Ime runde", + "broadcastRoundNumber": "Broj runde", + "broadcastTournamentName": "Ime turnira", + "broadcastTournamentDescription": "Kratak opis turnira", + "broadcastFullDescription": "Potpuni opis događaja", + "broadcastFullDescriptionHelp": "Neobavezni dugi opis prijenosa. {param1} je dostupno. Duljina mora biti manja od {param2} znakova.", + "broadcastSourceUrlHelp": "Link koji će Lichess ispitavati kako bi dobio PGN ažuriranja. Mora biti javno dostupan s interneta.", + "broadcastStartDateHelp": "Neobavezno, ako znaš kada događaj počinje", + "broadcastCurrentGameUrl": "URL trenutne igre", + "broadcastDownloadAllRounds": "Preuzmite sve igre", + "broadcastResetRound": "Resetiraj ovu rundu", + "broadcastDeleteRound": "Izbriši ovu rundu", + "broadcastDefinitivelyDeleteRound": "Definitivno izbrišite rundu i njezine igre.", + "broadcastDeleteAllGamesOfThisRound": "Izbriši sve igre ovog kola. Izvor mora biti aktivan kako bi ih se ponovno stvorilo.", + "broadcastDeleteTournament": "Izbriši ovaj turnir", + "broadcastNbBroadcasts": "{count, plural, =1{{count} prijenos} few{{count} prijenosa} other{{count} prijenosa}}", "challengeChallengeToPlay": "Poziv na partiju", "challengeChallengeDeclined": "Izazov odbijen", "challengeChallengeAccepted": "Izazov prihvaćen!", @@ -336,8 +357,8 @@ "puzzleThemeXRayAttackDescription": "Figura napada ili brani polje kroz protivničku figuru.", "puzzleThemeZugzwang": "Iznuđeni potez (Zugzwang)", "puzzleThemeZugzwangDescription": "Protivnik je prisiljen odigrati potez koji mu pogoršava poziciju.", - "puzzleThemeHealthyMix": "Pomalo svega", - "puzzleThemeHealthyMixDescription": "Kao i u pravim partijama - budi spreman i očekuj bilo što! Kombinacija svih navedenih vrsta zadataka.", + "puzzleThemeMix": "Pomalo svega", + "puzzleThemeMixDescription": "Kao i u pravim partijama - budi spreman i očekuj bilo što! Kombinacija svih navedenih vrsta zadataka.", "puzzleThemePlayerGames": "Igračeve partije", "puzzleThemePlayerGamesDescription": "Pogledaj zadatke generirate iz vlastitih partija ili iz partija određenog igrača.", "puzzleThemePuzzleDownloadInformation": "Ovi zadaci su u javnom vlasništvu i mogu biti preuzeti sa {param}.", @@ -466,7 +487,6 @@ "memory": "Memorija", "infiniteAnalysis": "Neprekidna analiza", "removesTheDepthLimit": "Uklanja granicu do koje računalo može analizirati, i održava tvoje računalo toplim", - "engineManager": "Upravitelj enginea", "blunder": "Gruba greška", "mistake": "Greška", "inaccuracy": "Nepreciznost", @@ -756,7 +776,6 @@ "cheat": "Varanje", "troll": "Provokacija", "other": "Ostalo", - "reportDescriptionHelp": "Zalijepi link na partiju/e u pitanju i objasni što nije u redu s ponašanjem korisnika. Nemoj samo reći \"varao je\", nego reci kako si došao/la do tog zaključka. Tvoja prijava bit će obrađena brže ako ju napišeš na engleskom jeziku.", "error_provideOneCheatedGameLink": "Molimo navedite barem jedan link igre u kojoj je igrač varao.", "by": "od {param}", "importedByX": "Uvezao {param}", @@ -1216,6 +1235,158 @@ "stormXRuns": "{count, plural, =1{Jedna runda} few{{count} rundi} other{{count} rundi}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Odigrao jednu rundu od {param2}} few{Odigrao {count} rundi od {param2}} other{Odigrao {count} rundi {param2}}}", "streamerLichessStreamers": "Lichess emiteri", + "studyPrivate": "Privatno", + "studyMyStudies": "Moje studije", + "studyStudiesIContributeTo": "Studije kojima pridonosim", + "studyMyPublicStudies": "Moje javne studije", + "studyMyPrivateStudies": "Moje privatne studije", + "studyMyFavoriteStudies": "Moje omiljene studije", + "studyWhatAreStudies": "Što su studije?", + "studyAllStudies": "Sve studije", + "studyStudiesCreatedByX": "Studije koje je stvorio {param}", + "studyNoneYet": "Još niti jedna.", + "studyHot": "Aktualno", + "studyDateAddedNewest": "Po datumu (najnovije)", + "studyDateAddedOldest": "Po datumu (najstarije)", + "studyRecentlyUpdated": "Nedavno objavljene", + "studyMostPopular": "Najpopularnije", + "studyAlphabetical": "Abecednim redom", + "studyAddNewChapter": "Dodaj novo poglavlje", + "studyAddMembers": "Dodaj članove", + "studyInviteToTheStudy": "Pozovi na učenje", + "studyPleaseOnlyInvitePeopleYouKnow": "Molimo da pozovete ljude koje znate i koji su voljni sudjelovati u ovoj studiji.", + "studySearchByUsername": "Traži prema korisničkom imenu", + "studySpectator": "Gledatelj", + "studyContributor": "Suradnik", + "studyKick": "Izbaci", + "studyLeaveTheStudy": "Napusti studiju", + "studyYouAreNowAContributor": "Postao si suradnik", + "studyYouAreNowASpectator": "Postao si gledatelj", + "studyPgnTags": "PGN oznake", + "studyLike": "Sviđa mi se", + "studyUnlike": "Ne sviđa mi se", + "studyNewTag": "Nova oznaka", + "studyCommentThisPosition": "Komentiraj ovu poziciju", + "studyCommentThisMove": "Komentiraj ovaj potez", + "studyAnnotateWithGlyphs": "Pribilježi glifovima", + "studyTheChapterIsTooShortToBeAnalysed": "Poglavlje je prekratko za analizu.", + "studyOnlyContributorsCanRequestAnalysis": "Samo suradnici u studiji mogu zahtijevati računalnu analizu.", + "studyGetAFullComputerAnalysis": "Dobi potpunu analizu \"main-line\" od servera.", + "studyMakeSureTheChapterIsComplete": "Budite sigurni da je poglavlje gotovo. Zahtjev za računalnom analizom se može dobiti samo jednom.", + "studyAllSyncMembersRemainOnTheSamePosition": "Svi sinkronizirani članovi ostaju na istoj poziciji", + "studyShareChanges": "Podijeli promjene sa gledateljima i pohrani ih na server", + "studyPlaying": "U tijeku", + "studyFirst": "Prvi", + "studyPrevious": "Prethodno", + "studyNext": "Sljedeće", + "studyLast": "Posljednja", "studyShareAndExport": "Podijeli & izvozi", - "studyStart": "Start" + "studyCloneStudy": "Kloniraj", + "studyStudyPgn": "Studiraj PGN", + "studyDownloadAllGames": "Preuzmite sve igre", + "studyChapterPgn": "PGN poglavlja", + "studyCopyChapterPgn": "Kopiraj PGN", + "studyDownloadGame": "Preuzmi igru", + "studyStudyUrl": "Studiraj URL", + "studyCurrentChapterUrl": "URL trenutnog poglavlja", + "studyYouCanPasteThisInTheForumToEmbed": "Možete zaljepiti ovo u forum da ugradite poglavlje", + "studyStartAtInitialPosition": "Kreni s početne pozicije", + "studyStartAtX": "Započni na {param}", + "studyEmbedInYourWebsite": "Ugradi u svoju stranicu ili blog", + "studyReadMoreAboutEmbedding": "Pročitajte više o ugradnji", + "studyOnlyPublicStudiesCanBeEmbedded": "Samo javne studije mogu biti uključene!", + "studyOpen": "Otvori", + "studyXBroughtToYouByY": "{param1} vam je donio {param2}", + "studyStudyNotFound": "Studija nije pronađena", + "studyEditChapter": "Uredi poglavlje", + "studyNewChapter": "Novo poglavlje", + "studyImportFromChapterX": "Unesi iz {param}", + "studyOrientation": "Orijentacija", + "studyAnalysisMode": "Tip analize", + "studyPinnedChapterComment": "Stalni komentar na poglavlje", + "studySaveChapter": "Spremi poglavlje", + "studyClearAnnotations": "Očisti pribilješke", + "studyClearVariations": "Očistiti varijacije", + "studyDeleteChapter": "Obriši poglavlje", + "studyDeleteThisChapter": "Dali želite obrisati ovo poglavlje? Nakon ovoga nema povratka!", + "studyClearAllCommentsInThisChapter": "Želite li očistiti sve komentare, glifove i nacrtane oblike u ovom poglavlju?", + "studyRightUnderTheBoard": "Točno ispod table", + "studyNoPinnedComment": "Ništa", + "studyNormalAnalysis": "Normalna analiza", + "studyHideNextMoves": "Sakrij sljedeći potez", + "studyInteractiveLesson": "Interaktivna poduka", + "studyChapterX": "Poglavlje {param}", + "studyEmpty": "Prazno", + "studyStartFromInitialPosition": "Kreni s početne pozicije", + "studyEditor": "Uređivač", + "studyStartFromCustomPosition": "Kreni s prilagođene pozicije", + "studyLoadAGameByUrl": "Učitaj igru prema URL", + "studyLoadAPositionFromFen": "Učitaj poziciju od FENa", + "studyLoadAGameFromPgn": "Učitaj igru od PGNa", + "studyAutomatic": "Automatski", + "studyUrlOfTheGame": "URL igre", + "studyLoadAGameFromXOrY": "Učitaj igru sa {param1} ili {param2}", + "studyCreateChapter": "Stvori poglavlje", + "studyCreateStudy": "Stvori studiju", + "studyEditStudy": "Uredi studiju", + "studyVisibility": "Vidljivost", + "studyPublic": "Javno", + "studyUnlisted": "Neizlistane", + "studyInviteOnly": "Samo na poziv", + "studyAllowCloning": "Dopusti kloniranje", + "studyNobody": "Nitko", + "studyOnlyMe": "Samo ja", + "studyContributors": "Suradnici", + "studyMembers": "Članovi", + "studyEveryone": "Svi", + "studyEnableSync": "Aktiviraj sinkronizaciju", + "studyYesKeepEveryoneOnTheSamePosition": "Da: drži sve u istoj poziciji", + "studyNoLetPeopleBrowseFreely": "Ne: neka ljudi slobodno pregledavaju", + "studyPinnedStudyComment": "Stalni komentar na studije", + "studyStart": "Start", + "studySave": "Spremi", + "studyClearChat": "Očistite razgovor", + "studyDeleteTheStudyChatHistory": "Dali želite obrisati povijest razgovora? Nakon ovoga nema povratka!", + "studyDeleteStudy": "Izbriši studiju", + "studyConfirmDeleteStudy": "Izbrisati cijelu studiju? Nema povratka! Ukucajte naziv studije da potvrdite: {param}", + "studyWhereDoYouWantToStudyThat": "Gdje želiš to studirati?", + "studyGoodMove": "Dobar potez", + "studyMistake": "Greška", + "studyBrilliantMove": "Briljantan potez", + "studyBlunder": "Gruba greška", + "studyInterestingMove": "Zanimljiv potez", + "studyDubiousMove": "Sumnjiv potez", + "studyOnlyMove": "Jedini potez", + "studyZugzwang": "Iznudica", + "studyEqualPosition": "Jednaka pozicija", + "studyUnclearPosition": "Nejasna pozicija", + "studyWhiteIsSlightlyBetter": "Bijeli je u blagoj prednosti", + "studyBlackIsSlightlyBetter": "Crni je u blagoj prednosti", + "studyWhiteIsBetter": "Bijeli je bolji", + "studyBlackIsBetter": "Crni je bolji", + "studyWhiteIsWinning": "Bijeli dobija", + "studyBlackIsWinning": "Crni dobija", + "studyNovelty": "Nov potez", + "studyDevelopment": "Razvoj", + "studyInitiative": "Inicijativa", + "studyAttack": "Napad", + "studyCounterplay": "Protunapad", + "studyTimeTrouble": "Vremenska nevolja", + "studyWithCompensation": "S kompenzacijom", + "studyWithTheIdea": "S idejom", + "studyNextChapter": "Sljedeće poglavlje", + "studyPrevChapter": "Prethodno poglavlje", + "studyStudyActions": "Studijske radnje", + "studyTopics": "Teme", + "studyMyTopics": "Moje teme", + "studyPopularTopics": "Popularne teme", + "studyManageTopics": "Upravljaj temama", + "studyBack": "Nazad", + "studyPlayAgain": "Igraj ponovno", + "studyWhatWouldYouPlay": "Što bi igrali u ovoj poziciji?", + "studyYouCompletedThisLesson": "Čestitamo! Završili ste lekciju.", + "studyNbChapters": "{count, plural, =1{{count} Poglavlje} few{{count} Poglavlja} other{{count} Poglavlja}}", + "studyNbGames": "{count, plural, =1{{count} Partija} few{{count} Partije} other{{count} Partije}}", + "studyNbMembers": "{count, plural, =1{{count} Član} few{{count} Član} other{{count} Članova}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Ovdje zalijepite svoj PGN tekst, do {count} igre} few{Ovdje zalijepite svoj PGN tekst, do {count} igri} other{Ovdje zalijepite svoj PGN tekst, do {count} igara}}" } \ No newline at end of file diff --git a/lib/l10n/lila_hu.arb b/lib/l10n/lila_hu.arb index 8a7fbce817..008dd28309 100644 --- a/lib/l10n/lila_hu.arb +++ b/lib/l10n/lila_hu.arb @@ -8,11 +8,11 @@ "mobileSystemColors": "Rendszerszínek", "mobileFeedbackButton": "Visszajelzés", "mobileOkButton": "OK", - "mobileSettingsHapticFeedback": "Haptikus visszajelzés", + "mobileSettingsHapticFeedback": "Érintésalapú visszajelzés", "mobileSettingsImmersiveMode": "Teljes képernyős mód", "mobileSettingsImmersiveModeSubtitle": "A rendszer gombjainak elrejtése játék közben. Kapcsold be, ha zavarnak a rendszer navigációs mozdulatai a képernyő sarkainál. A játszmaképernyőn és a Puzzle Storm képernyőjén működik.", "mobileNotFollowingAnyUser": "Jelenleg nem követsz senkit.", - "mobileAllGames": "Az összes játszma", + "mobileAllGames": "Összes játszma", "mobileRecentSearches": "Keresési előzmények", "mobileClearButton": "Törlés", "mobilePlayersMatchingSearchTerm": "Játékosok {param} felhasználónévvel", @@ -29,13 +29,18 @@ "mobilePuzzleStormConfirmEndRun": "Befejezed a futamot?", "mobilePuzzleStormFilterNothingToShow": "Nincs megjeleníthető elem, változtasd meg a szűrőket", "mobileCancelTakebackOffer": "Visszalépés kérésének visszavonása", - "mobileCancelDrawOffer": "Döntetlenkérés visszavonása", "mobileWaitingForOpponentToJoin": "Várakozás az ellenfél csatlakozására...", "mobileBlindfoldMode": "Vakjátszma mód", "mobileLiveStreamers": "Lichess streamerek", "mobileCustomGameJoinAGame": "Csatlakozás játszmához", "mobileCorrespondenceClearSavedMove": "Mentett lépés törlése", "mobileSomethingWentWrong": "Hiba történt.", + "mobileShowResult": "Eredmény mutatása", + "mobilePuzzleThemesSubtitle": "Oldj feladványokat kedvenc megnyitásaid kapcsán vagy válassz egy tematikát.", + "mobilePuzzleStormSubtitle": "Oldd meg a lehető legtöbb feladványt 3 perc alatt.", + "mobileGreeting": "Üdv {param}!", + "mobileGreetingWithoutName": "Üdv", + "mobilePrefMagnifyDraggedPiece": "Mozdított bábu nagyítása", "activityActivity": "Aktivitás", "activityHostedALiveStream": "Élőben közvetített", "activityRankedInSwissTournament": "Helyezés: {param1} / {param2}", @@ -59,6 +64,29 @@ "activityJoinedNbTeams": "{count, plural, =1{{count} csapathoz csatlakozott} other{{count} csapathoz csatlakozott}}", "broadcastBroadcasts": "Versenyközvetítések", "broadcastLiveBroadcasts": "Közvetítések élő versenyekről", + "broadcastNewBroadcast": "Új élő versenyközvetítés", + "broadcastAddRound": "Forduló hozzáadása", + "broadcastOngoing": "Folyamatban", + "broadcastUpcoming": "Közelgő", + "broadcastCompleted": "Befejeződött", + "broadcastRoundName": "Forduló neve", + "broadcastRoundNumber": "Forduló száma", + "broadcastTournamentName": "Verseny neve", + "broadcastTournamentDescription": "Verseny rövid leírása", + "broadcastFullDescription": "Esemény teljes leírása", + "broadcastFullDescriptionHelp": "Opcionális a közvetítés még bővebb leírása. {param1} használható. A hossz nem lehet több, mint {param2} karakter.", + "broadcastSourceUrlHelp": "URL amit a Lichess időnként PGN frissítésekért ellenőriz. Ennek nyilvános internetcímnek kell lennie.", + "broadcastStartDateHelp": "Opcionális, ha tudod mikor kezdődik az esemény", + "broadcastCurrentGameUrl": "Jelenlegi játszma URL", + "broadcastDownloadAllRounds": "Összes játszma letöltése", + "broadcastResetRound": "A forduló újrakezdése", + "broadcastDeleteRound": "A forduló törlése", + "broadcastDefinitivelyDeleteRound": "A forduló és játszmáinak végleges törlése.", + "broadcastDeleteAllGamesOfThisRound": "Minden játék törlése ebben a fordulóban. A forrásnak aktívnak kell lennie, hogy újra létre lehessen hozni őket.", + "broadcastEditRoundStudy": "Forduló tanulmányának szerkesztése", + "broadcastDeleteTournament": "Verseny törlése", + "broadcastDefinitivelyDeleteTournament": "Az egész verseny végleges törlése az összes fordulóval és játszmával együtt.", + "broadcastNbBroadcasts": "{count, plural, =1{{count} versenyközvetítés} other{{count} versenyközvetítés}}", "challengeChallengesX": "Kihívások: {param1}", "challengeChallengeToPlay": "Kihívás játszmára", "challengeChallengeDeclined": "Kihívás elutasítva", @@ -376,8 +404,8 @@ "puzzleThemeXRayAttackDescription": "Figura ami egy ellenséges figurán áthatolva véd vagy támad egy mezőt.", "puzzleThemeZugzwang": "Lépéskényszer", "puzzleThemeZugzwangDescription": "Az ellenfélnek kevés lehetséges lépése van, és mind csak tovább rontja a pozícióját.", - "puzzleThemeHealthyMix": "Vegyes mix", - "puzzleThemeHealthyMixDescription": "Egy kicsit mindenből. Nem tudod mire számíthatsz, ezért állj készen bármire! Akár egy valódi játszmában.", + "puzzleThemeMix": "Vegyes mix", + "puzzleThemeMixDescription": "Egy kicsit mindenből. Nem tudod mire számíthatsz, ezért állj készen bármire! Akár egy valódi játszmában.", "puzzleThemePlayerGames": "Felhasználók játszmái", "puzzleThemePlayerGamesDescription": "A saját vagy mások játszmáiból generált feladványok keresése.", "puzzleThemePuzzleDownloadInformation": "Ezek a feladványok közkincsnek minősülnek és innen letölthetők: {param}.", @@ -506,7 +534,6 @@ "memory": "Memória", "infiniteAnalysis": "Végtelen elemzés", "removesTheDepthLimit": "Feloldja a mélységi korlátot, és melegen tartja a számítógéped", - "engineManager": "Motor menedzser", "blunder": "Baklövés", "mistake": "Hiba", "inaccuracy": "Pontatlanság", @@ -806,7 +833,6 @@ "cheat": "Csalás", "troll": "Trollkodás", "other": "Egyéb", - "reportDescriptionHelp": "Másold be a játék(ok) linkjét, és mondd el, mi a gond a játékos viselkedésével. Ne csak annyit írj, hogy \"csalt\", hanem próbáld elmondani, miből gondolod ezt. A jelentésedet hamarabb feldolgozzák, ha angolul írod.", "error_provideOneCheatedGameLink": "Kérünk, legalább adj meg linket legalább egy csalt játszmához.", "by": "Létrehozta: {param}", "importedByX": "Importálva {param} által", @@ -1289,6 +1315,158 @@ "stormXRuns": "{count, plural, =1{1 futam} other{{count} futam}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Egy {param2} futamot játszott} other{{count} {param2} futamot játszott}}", "streamerLichessStreamers": "Lichess streamerek", + "studyPrivate": "Privát", + "studyMyStudies": "Tanulmányaim", + "studyStudiesIContributeTo": "Tanulmányaim szerkesztőként", + "studyMyPublicStudies": "Nyilvános tanulmányaim", + "studyMyPrivateStudies": "Saját tanulmányaim", + "studyMyFavoriteStudies": "Kedvenc tanulmányaim", + "studyWhatAreStudies": "Mik azok a tanulmányok?", + "studyAllStudies": "Összes tanulmány", + "studyStudiesCreatedByX": "{param} tanulmányai", + "studyNoneYet": "Nincs még ilyen tanulmány.", + "studyHot": "Felkapott", + "studyDateAddedNewest": "Újabbak elöl", + "studyDateAddedOldest": "Hozzáadva (legrégebbi)", + "studyRecentlyUpdated": "Nemrégiben frissítve", + "studyMostPopular": "Legnépszerűbb", + "studyAlphabetical": "Betűrendben", + "studyAddNewChapter": "Új fejezet hozzáadása", + "studyAddMembers": "Tagok hozzáadása", + "studyInviteToTheStudy": "Meghívás a tanulmányba", + "studyPleaseOnlyInvitePeopleYouKnow": "Csak olyan ismerőst hívj meg, aki szeretne részt venni a tanulmány készítésében.", + "studySearchByUsername": "Keresés felhasználónév alapján", + "studySpectator": "Néző", + "studyContributor": "Szerkesztő", + "studyKick": "Eltávolítás", + "studyLeaveTheStudy": "Tanulmány elhagyása", + "studyYouAreNowAContributor": "Szerkesztő lettél", + "studyYouAreNowASpectator": "Néző lettél", + "studyPgnTags": "PGN címkék", + "studyLike": "Kedvel", + "studyUnlike": "Mégse tetszik", + "studyNewTag": "Új címke", + "studyCommentThisPosition": "Megjegyzés ehhez az álláshoz", + "studyCommentThisMove": "Megjegyzés ehhez a lépéshez", + "studyAnnotateWithGlyphs": "Lépések megjelölése", + "studyTheChapterIsTooShortToBeAnalysed": "A fejezet túl rövid számítógépes elemzéshez.", + "studyOnlyContributorsCanRequestAnalysis": "Csak a tanulmány szerkesztői kérhetnek számítógépes elemzést.", + "studyGetAFullComputerAnalysis": "Teljes szerveroldali számítógépes elemzés kérése a főváltozatról.", + "studyMakeSureTheChapterIsComplete": "Ellenőrizd, hogy a fejezet elkészült-e. Csak egyszer kérhető számítógépes elemzés.", + "studyAllSyncMembersRemainOnTheSamePosition": "Minden szinkronizált tag ugyanazt az állást látja", + "studyShareChanges": "A módosítások láthatóak a nézők számára, és mentésre kerülnek a szerveren", + "studyPlaying": "Folyamatban", + "studyFirst": "Első", + "studyPrevious": "Előző", + "studyNext": "Következő", + "studyLast": "Utolsó", "studyShareAndExport": "Megosztás és exportálás", - "studyStart": "Mehet" + "studyCloneStudy": "Klónozás", + "studyStudyPgn": "PGN a tanulmányról", + "studyDownloadAllGames": "Az összes játszma letöltése", + "studyChapterPgn": "PGN a fejezetről", + "studyCopyChapterPgn": "PGN másolása", + "studyDownloadGame": "Játszma letöltése", + "studyStudyUrl": "Tanulmány URL", + "studyCurrentChapterUrl": "URL erre a fejezetre", + "studyYouCanPasteThisInTheForumToEmbed": "Ezzel a linkkel beágyazhatod a fejezetet a Lichess blogodban vagy a fórumon", + "studyStartAtInitialPosition": "Kezdés a kiinduló állásból", + "studyStartAtX": "Kezdés innen: {param}", + "studyEmbedInYourWebsite": "Beágyazás saját weboldalba", + "studyReadMoreAboutEmbedding": "A beágyazásról bővebben", + "studyOnlyPublicStudiesCanBeEmbedded": "Csak nyilvános tanulmányokat lehet beágyazni!", + "studyOpen": "Megnyitás", + "studyXBroughtToYouByY": "{param1}, a {param2} jóvoltából", + "studyStudyNotFound": "Tanulmány nem található", + "studyEditChapter": "Fejezet szerkesztése", + "studyNewChapter": "Új fejezet", + "studyImportFromChapterX": "Importálás innen: {param}", + "studyOrientation": "Szemszög", + "studyAnalysisMode": "Elemzés típusa", + "studyPinnedChapterComment": "Rögzített megjegyzés a fejezethez", + "studySaveChapter": "Fejezet mentése", + "studyClearAnnotations": "Megjegyzések törlése", + "studyClearVariations": "Változatok törlése", + "studyDeleteChapter": "Fejezet törlése", + "studyDeleteThisChapter": "Törlöd a fejezetet? Ezt nem lehet visszavonni!", + "studyClearAllCommentsInThisChapter": "Minden megjegyzés, lépésjelölés és rajz törlése a fejezetből", + "studyRightUnderTheBoard": "Közvetlenül a tábla alatt", + "studyNoPinnedComment": "Nincs", + "studyNormalAnalysis": "Normál elemzés", + "studyHideNextMoves": "Következő lépések elrejtése", + "studyInteractiveLesson": "Interaktív lecke", + "studyChapterX": "{param}. fejezet", + "studyEmpty": "Üres", + "studyStartFromInitialPosition": "Kezdés az alapállásból", + "studyEditor": "Szerkesztő", + "studyStartFromCustomPosition": "Kezdés tetszőleges állásból", + "studyLoadAGameByUrl": "Játszmák betöltése linkkel", + "studyLoadAPositionFromFen": "Állás betöltése FEN-ből", + "studyLoadAGameFromPgn": "Játszmák betöltése PGN-ből", + "studyAutomatic": "Automatikus", + "studyUrlOfTheGame": "Játszmák linkje, soronként egy", + "studyLoadAGameFromXOrY": "Játszmák betöltése {param1} vagy {param2} szerverről", + "studyCreateChapter": "Fejezet létrehozása", + "studyCreateStudy": "Tanulmány létrehozása", + "studyEditStudy": "Tanulmány szerkesztése", + "studyVisibility": "Láthatóság", + "studyPublic": "Nyilvános", + "studyUnlisted": "Nincs listázva", + "studyInviteOnly": "Csak meghívással", + "studyAllowCloning": "Klónozható", + "studyNobody": "Senki", + "studyOnlyMe": "Csak én", + "studyContributors": "Szerkesztők", + "studyMembers": "Tagok", + "studyEveryone": "Mindenki", + "studyEnableSync": "Sync engedélyezése", + "studyYesKeepEveryoneOnTheSamePosition": "Igen: mindenki ugyanazt az állást látja", + "studyNoLetPeopleBrowseFreely": "Nem: szabadon böngészhető", + "studyPinnedStudyComment": "Rögzített megjegyzés a tanulmányhoz", + "studyStart": "Mehet", + "studySave": "Mentés", + "studyClearChat": "Chat törlése", + "studyDeleteTheStudyChatHistory": "Biztosan törlöd a chat előzményeket a tanulmányból? Ezt nem lehet visszavonni!", + "studyDeleteStudy": "Tanulmány törlése", + "studyConfirmDeleteStudy": "Törlöd a teljes tanulmányt? Ezt nem lehet visszavonni! Gépeld be a tanulmány nevét a megerősítéshez: {param}", + "studyWhereDoYouWantToStudyThat": "Melyik tanulmányba kerüljön?", + "studyGoodMove": "Jó lépés", + "studyMistake": "Hiba", + "studyBrilliantMove": "Kiváló lépés", + "studyBlunder": "Durva hiba", + "studyInterestingMove": "Érdekes lépés", + "studyDubiousMove": "Szokatlan lépés", + "studyOnlyMove": "Egyetlen lépés", + "studyZugzwang": "Lépéskényszer", + "studyEqualPosition": "Egyenlő állás", + "studyUnclearPosition": "Zavaros állás", + "studyWhiteIsSlightlyBetter": "Világos kicsit jobban áll", + "studyBlackIsSlightlyBetter": "Sötét kicsit jobban áll", + "studyWhiteIsBetter": "Világos jobban áll", + "studyBlackIsBetter": "Sötét jobban áll", + "studyWhiteIsWinning": "Világos nyerésre áll", + "studyBlackIsWinning": "Sötét nyerésre áll", + "studyNovelty": "Újítás", + "studyDevelopment": "Fejlődés", + "studyInitiative": "Kezdeményezés", + "studyAttack": "Támadás", + "studyCounterplay": "Ellenjáték", + "studyTimeTrouble": "Időzavar", + "studyWithCompensation": "Kompenzáció", + "studyWithTheIdea": "Elképzelés", + "studyNextChapter": "Következő fejezet", + "studyPrevChapter": "Előző fejezet", + "studyStudyActions": "Műveletek a tanulmányban", + "studyTopics": "Témakörök", + "studyMyTopics": "Témaköreim", + "studyPopularTopics": "Népszerű témakörök", + "studyManageTopics": "Témakörök kezelése", + "studyBack": "Vissza", + "studyPlayAgain": "Újra", + "studyWhatWouldYouPlay": "Mit lépnél ebben az állásban?", + "studyYouCompletedThisLesson": "Gratulálok! A fejezet végére értél.", + "studyNbChapters": "{count, plural, =1{{count} Fejezet} other{{count} Fejezet}}", + "studyNbGames": "{count, plural, =1{{count} Játszma} other{{count} Játszma}}", + "studyNbMembers": "{count, plural, =1{{count} Tag} other{{count} Tag}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Illeszd be a PGN szövegét legfeljebb {count} játszmáig} other{Illeszd be a PGN szövegét (legfeljebb {count} játszma)}}" } \ No newline at end of file diff --git a/lib/l10n/lila_hy.arb b/lib/l10n/lila_hy.arb index 29fe4d3dc0..0cc0a39ab0 100644 --- a/lib/l10n/lila_hy.arb +++ b/lib/l10n/lila_hy.arb @@ -12,7 +12,27 @@ "activityRankedInTournament": "{count, plural, =1{{count} տեղ ({param2} % լավագույն)՝ {param4} մրցաշարում {param3} խաղերի արդյունքով} other{{count} տեղ ({param2} % լավագույն)՝ {param4} մրցաշարում {param3} խաղերի արդյունքով}}", "activityJoinedNbTeams": "{count, plural, =1{Ընդունվել է {count} թիմ} other{Ընդունվել է {count} թիմ}}", "broadcastBroadcasts": "Հեռարձակումներ", + "broadcastMyBroadcasts": "Իմ հեռարձակումները", "broadcastLiveBroadcasts": "Մրցաշարի ուղիղ հեռարձակումներ", + "broadcastNewBroadcast": "Նոր ուղիղ հեռարձակում", + "broadcastSubscribedBroadcasts": "Բաժանորդագրված հեռարձակումներ", + "broadcastAddRound": "Ավելացնել խաղափուլ", + "broadcastOngoing": "Ընթացիկ", + "broadcastUpcoming": "Առաջիկայում սպասվող", + "broadcastCompleted": "Ավարտված", + "broadcastRoundName": "Խաղափուլի անվանում", + "broadcastRoundNumber": "Խաղափուլի համար", + "broadcastTournamentName": "Մրցաշարի անվանում", + "broadcastTournamentDescription": "Իրադարձության համառոտ նկարագրություն", + "broadcastFullDescription": "Իրադարձության ամբողջական նկարագրություն", + "broadcastStartDateHelp": "Լրացուցիչ, եթե գիտեք, թե երբ է սկսվելու իրադարձությունը", + "broadcastCurrentGameUrl": "Ընթացիկ պարտիայի URL-հասցեն", + "broadcastDownloadAllRounds": "Բեռնել բոլոր խաղափուլերը", + "broadcastResetRound": "Հեռացնել այս խաղափուլը", + "broadcastDeleteRound": "Հեռացնել այս խաղափուլը", + "broadcastEditRoundStudy": "Խմբագրել խաղափուլի ստուդիան", + "broadcastDeleteTournament": "Հեռացնել այս մրցաշարը", + "broadcastDefinitivelyDeleteTournament": "Վերջնականապես հեռացնել ամբողջ մրցաշարը, նրա խաղափուլերը և պարտիաները։", "challengeChallengesX": "Մարտահրավերներ: {param1}", "challengeChallengeToPlay": "Ուղարկել խաղի մարտահրավեր", "challengeChallengeDeclined": "Մարտահրավերը մերժված է", @@ -322,8 +342,8 @@ "puzzleThemeXRayAttackDescription": "Իրավիճակ, երբ հեռահար խաղաքարի հարձակման կամ պաշտպանության գծին կանգնած է մրցակցի խաղաքարը։", "puzzleThemeZugzwang": "Ցուգցվանգ", "puzzleThemeZugzwangDescription": "Մրցակիցը ստիպված է անել հնարավոր փոքրաթիվ քայլերից մեկը, բայց քայլերից ցանկացածը տանում է դիրքի վատացման։", - "puzzleThemeHealthyMix": "Խառը խնդիրներ", - "puzzleThemeHealthyMixDescription": "Ամեն ինչից` քիչ-քիչ։ Դուք չգիտեք` ինչ է սպասվում, այնպես որ, պատրաստ եղեք ամեն ինչի։ Ինչպես իսկական պարտիայում։", + "puzzleThemeMix": "Խառը խնդիրներ", + "puzzleThemeMixDescription": "Ամեն ինչից` քիչ-քիչ։ Դուք չգիտեք` ինչ է սպասվում, այնպես որ, պատրաստ եղեք ամեն ինչի։ Ինչպես իսկական պարտիայում։", "puzzleThemePlayerGames": "Խաղացողի պարտիաները", "puzzleThemePlayerGamesDescription": "Գտնել խնդիրներ, որոնք ստեղծվել են Ձեր պարտիաներից, կամ այլ խաղացողների պարտիաներից։", "puzzleThemePuzzleDownloadInformation": "Այս խնդիրները հանրության սեփականությունն են, և Դուք կարող եք ներբեռնել դրանք՝ {param}։", @@ -450,7 +470,6 @@ "memory": "Հիշողություն", "infiniteAnalysis": "Անվերջ վերլուծություն", "removesTheDepthLimit": "Վերացնում է խորության սահմանափակումը և տաք պահում ձեր համակարգիչը", - "engineManager": "Շարժիչի մենեջեր", "blunder": "Վրիպում", "mistake": "Սխալ", "inaccuracy": "Անճշտություն", @@ -742,7 +761,6 @@ "cheat": "խաբեբա", "troll": "Թրոլինգ", "other": "այլ", - "reportDescriptionHelp": "Կիսվեք մեզ հետ Այն խաղերի հղումներով, որտեղ կարծում եք, որ կանոնները խախտվել են և նկարագրեք, թե ինչն է սխալ: Բավական չէ պարզապես գրել \"Նա խարդախում է\", խնդրում ենք նկարագրել, թե ինչպես եք եկել այս եզրակացության: Մենք ավելի արագ կաշխատենք, եթե գրեք անգլերեն:", "error_provideOneCheatedGameLink": "Խնդրում ենք ավելացնել առնվազն մեկ խաղի հղում, որտեղ ձեր կարծիքով խախտվել են կանոնները:", "by": "ըստ {param}", "importedByX": "Ներմուծվել է {param}-ի կողմից", @@ -1210,6 +1228,158 @@ "stormXRuns": "{count, plural, =1{1 փորձ} other{{count} փորձ}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Խաղացվել է մեկ շարք {param2}-ում} other{Խաղացվել է {count} շարք {param2}-ում}}", "streamerLichessStreamers": "Lichess-ի հեռարձակողներ", + "studyPrivate": "Անձնական", + "studyMyStudies": "Իմ ստուդիաները", + "studyStudiesIContributeTo": "Իմ մասնակցությամբ ստուդիաները", + "studyMyPublicStudies": "Իմ հանրային ստուդիաները", + "studyMyPrivateStudies": "Իմ անձնական ստուդիաները", + "studyMyFavoriteStudies": "Իմ սիրելի ստուդիաները", + "studyWhatAreStudies": "Ի՞նչ են «ստուդիաները»", + "studyAllStudies": "Բոլոր ստուդիաները", + "studyStudiesCreatedByX": "{param}-ի ստեղծած ստուդիաները", + "studyNoneYet": "Առայժմ ոչինչ։", + "studyHot": "Ամենաակտիվները", + "studyDateAddedNewest": "Վերջերս ավելացվածները", + "studyDateAddedOldest": "Վաղուց ավելացվածները", + "studyRecentlyUpdated": "Վերջերս թարմացվածները", + "studyMostPopular": "Ամենահայտնիները", + "studyAlphabetical": "Այբբենական կարգով", + "studyAddNewChapter": "Ավելացնել նոր գլուխ", + "studyAddMembers": "Ավելացնել մասնակիցների", + "studyInviteToTheStudy": "Հրավիրել ստուդիա", + "studyPleaseOnlyInvitePeopleYouKnow": "Հրավիրեք միայն այն մասնակիցներին, որոնց ճանաչում եք, և որոնք ակտիվորեն ցանկանում են միանալ այս ստուդիային։", + "studySearchByUsername": "Որոնում ըստ մասնակցային անվան", + "studySpectator": "Հանդիսատես", + "studyContributor": "Խմբագիր", + "studyKick": "Վռնդել", + "studyLeaveTheStudy": "Լքել ստուդիան", + "studyYouAreNowAContributor": "Այժմ Դուք խմբագիր եք", + "studyYouAreNowASpectator": "Այժմ Դուք հանդիսական եք", + "studyPgnTags": "PGN-ի թեգերը", + "studyLike": "Հավանել", + "studyUnlike": "Չեմ հավանում", + "studyNewTag": "Նոր թեգ", + "studyCommentThisPosition": "Մեկնաբանել այս դիրքը", + "studyCommentThisMove": "Մեկնաբանել այս քայլը", + "studyAnnotateWithGlyphs": "Ավելացնել սիմվոլներով անոտացիա", + "studyTheChapterIsTooShortToBeAnalysed": "Վերլուծության համար գլուխը չափազանց կարճ է։", + "studyOnlyContributorsCanRequestAnalysis": "Միայն ստուդիայի խմբագիրները կարող են խնդրել համակարգչային վերլուծություն։", + "studyGetAFullComputerAnalysis": "Սերվերից ստանալ գլխավոր գծի ամբողջական համակարգչային վերլուծություն։", + "studyMakeSureTheChapterIsComplete": "Համոզվեք, որ գլուխն ավարտված է։ Համակարգչային վերլուծություն կարող եք խնդրել միայն մեկ անգամ։", + "studyAllSyncMembersRemainOnTheSamePosition": "Բոլոր սինքրոնիզացված մասնակիցները մնում են նույն դիրքում", + "studyShareChanges": "Փոփոխությունները տարածել հանդիսականների շրջանում և դրանք պահպանել սերվերում", + "studyPlaying": "Ակտիվ", + "studyFirst": "Առաջինը", + "studyPrevious": "Նախորդը", + "studyNext": "Հաջորդը", + "studyLast": "Վերջինը", "studyShareAndExport": "Տարածել & և արտահանել", - "studyStart": "Սկսել" + "studyCloneStudy": "Կլոնավորել", + "studyStudyPgn": "Ստուդիայի PGN-ն", + "studyDownloadAllGames": "Ներբեռնել բոլոր պարտիաները", + "studyChapterPgn": "Գլխի PGN-ը", + "studyCopyChapterPgn": "Պատճենել PGN-ը", + "studyDownloadGame": "Ներբեռնել պարտիան", + "studyStudyUrl": "Ստուդիայի հղումը", + "studyCurrentChapterUrl": "Այս գլխի հղումը", + "studyYouCanPasteThisInTheForumToEmbed": "Ֆորումում կամ Lichess-ի բլոգում ներդնելու համար տեղադրեք այս կոդը", + "studyStartAtInitialPosition": "Բացել սկզբնական դիրքում", + "studyStartAtX": "Սկսել {param}-ից", + "studyEmbedInYourWebsite": "Ներդնել սեփական կայքում կամ բլոգում", + "studyReadMoreAboutEmbedding": "Մանրամասն կայքում ներդնելու մասին", + "studyOnlyPublicStudiesCanBeEmbedded": "Կայքում կարելի է ներդնել միայն հրապարակային ստուդիաները։", + "studyOpen": "Բացել", + "studyXBroughtToYouByY": "{param1}-ը {param2}-ից", + "studyStudyNotFound": "Ստուդիան չի գտնվել", + "studyEditChapter": "Խմբագրել գլուխը", + "studyNewChapter": "Նոր գլուխ", + "studyImportFromChapterX": "Ներդնել {param}-ից", + "studyOrientation": "Կողմնորոշում", + "studyAnalysisMode": "Վերլուծության ռեժիմ", + "studyPinnedChapterComment": "Գլխի ամրակցված մեկնաբանություն", + "studySaveChapter": "Պահպանել գլուխը", + "studyClearAnnotations": "Հեռացնել անոտացիան", + "studyClearVariations": "Հեռացնել տարբերակները", + "studyDeleteChapter": "Հեռացնել գլուխը", + "studyDeleteThisChapter": "Հեռացնե՞լ գլուխը։ Վերականգնել հնարավոր չի լինի։", + "studyClearAllCommentsInThisChapter": "Մաքրե՞լ այս գլխի բոլոր մեկնաբանություններն ու նշումները", + "studyRightUnderTheBoard": "Անմիջապես տախտակի տակ", + "studyNoPinnedComment": "Ոչ", + "studyNormalAnalysis": "Սովորական վերլուծություն", + "studyHideNextMoves": "Թաքցնել հետագա քայլերը", + "studyInteractiveLesson": "Ինտերակտիվ դասընթաց", + "studyChapterX": "Գլուխ {param}", + "studyEmpty": "Դատարկ է", + "studyStartFromInitialPosition": "Սկսել նախնական դիրքից", + "studyEditor": "Խմբագիր", + "studyStartFromCustomPosition": "Սկսել սեփական դիրքից", + "studyLoadAGameByUrl": "Բեռնել պարտիան ըստ URL-ի", + "studyLoadAPositionFromFen": "Բեռնել դիրքը FEN-ով", + "studyLoadAGameFromPgn": "Բեռնել դիրքն ըստ PGN-ի", + "studyAutomatic": "Ինքնաբերաբար", + "studyUrlOfTheGame": "Պարտիայի URL-ը, մեկ տողով", + "studyLoadAGameFromXOrY": "Ներբեռնել խաղը {param1}-ից կամ {param2}-ից", + "studyCreateChapter": "Ստեղծել գլուխը", + "studyCreateStudy": "Ստեղծել ստուդիա", + "studyEditStudy": "Խմբագրել ստուդիան", + "studyVisibility": "Հասանելի է դիտման համար", + "studyPublic": "Հրապարակային", + "studyUnlisted": "Հղումով", + "studyInviteOnly": "Միայն հրավերով", + "studyAllowCloning": "Թույլատրել պատճենումը", + "studyNobody": "Ոչ ոք", + "studyOnlyMe": "Միայն ես", + "studyContributors": "Համահեղինակներ", + "studyMembers": "Անդամները", + "studyEveryone": "Բոլորը", + "studyEnableSync": "Միացնել սինքրոնացումը", + "studyYesKeepEveryoneOnTheSamePosition": "Այո. բոլորի համար դնել միևնույն դիրքը", + "studyNoLetPeopleBrowseFreely": "Ոչ. թույլատրել մասնակիցներին ազատ ուսումնասիրել բոլոր դիրքերը", + "studyPinnedStudyComment": "Ստուդիայի ամրակցված մեկնաբանություն", + "studyStart": "Սկսել", + "studySave": "Պահպանել", + "studyClearChat": "Մաքրել զրուցարանը", + "studyDeleteTheStudyChatHistory": "Հեռացնե՞լ ստուդիայի զրուցարանը։ Վերականգնել հնարավոր չի լինի։", + "studyDeleteStudy": "Հեռացնել ստուդիան", + "studyConfirmDeleteStudy": "Հեռացնե՞լ ամբողջ ստուդիան։ Հեռացումն անդառնալի կլինի։ Հաստատելու համար մուտքագրեք ստուդիայի անվանումը՝ {param}", + "studyWhereDoYouWantToStudyThat": "Որտե՞ղ եք ցանկանում ստեղծել ստուդիան։", + "studyGoodMove": "Լավ քայլ է", + "studyMistake": "Սխալ", + "studyBrilliantMove": "Գերազանց քայլ է", + "studyBlunder": "Վրիպում", + "studyInterestingMove": "Հետաքրքիր քայլ է", + "studyDubiousMove": "Կասկածելի քայլ", + "studyOnlyMove": "Միակ քայլ", + "studyZugzwang": "Ցուգցվանգ", + "studyEqualPosition": "Հավասար դիրք", + "studyUnclearPosition": "Անորոշ դիրք", + "studyWhiteIsSlightlyBetter": "Սպիտակները մի քիչ լավ են", + "studyBlackIsSlightlyBetter": "Սևերը մի քիչ լավ են", + "studyWhiteIsBetter": "Սպիտակները լավ են", + "studyBlackIsBetter": "Սևերը լավ են", + "studyWhiteIsWinning": "Սպիտակները հաղթում են", + "studyBlackIsWinning": "Սևերը հաղթում են", + "studyNovelty": "Նորույթ", + "studyDevelopment": "Զարգացում", + "studyInitiative": "Նախաձեռնություն", + "studyAttack": "Գրոհ", + "studyCounterplay": "Հակախաղ", + "studyTimeTrouble": "Ցայտնոտ", + "studyWithCompensation": "Փոխհատուցմամբ", + "studyWithTheIdea": "Մտահղացմամբ", + "studyNextChapter": "Հաջորդ գլուխը", + "studyPrevChapter": "Նախորդ գլուխը", + "studyStudyActions": "Գործողությունները ստուդիայում", + "studyTopics": "Թեմաներ", + "studyMyTopics": "Իմ թեմաները", + "studyPopularTopics": "Շատ դիտվող թեմաներ", + "studyManageTopics": "Թեմաների կառավարում", + "studyBack": "Հետ", + "studyPlayAgain": "Կրկին խաղալ", + "studyWhatWouldYouPlay": "Ինչպե՞ս կխաղայիք այս դիրքում", + "studyYouCompletedThisLesson": "Շնորհավորո՜ւմ ենք։ Դուք ավարեցիք այս դասը։", + "studyNbChapters": "{count, plural, =1{{count} գլուխ} other{{count} գլուխ}}", + "studyNbGames": "{count, plural, =1{{count} պարտիա} other{{count} պարտիա}}", + "studyNbMembers": "{count, plural, =1{{count} մասնակից} other{{count} մասնակից}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Տեղադրեք տեսքտը PGN ձևաչափով, {count} պարտիայից ոչ ավելի} other{Տեղադրեք տեսքտը PGN ձևաչափով, {count} պարտիայից ոչ ավելի}}" } \ No newline at end of file diff --git a/lib/l10n/lila_id.arb b/lib/l10n/lila_id.arb index 56bf104148..3af7438340 100644 --- a/lib/l10n/lila_id.arb +++ b/lib/l10n/lila_id.arb @@ -22,6 +22,23 @@ "activityJoinedNbTeams": "{count, plural, other{Bergabung {count} tim}}", "broadcastBroadcasts": "Siaran", "broadcastLiveBroadcasts": "Siaran turnamen langsung", + "broadcastNewBroadcast": "Siaran langsung baru", + "broadcastAddRound": "Tambakan ronde", + "broadcastOngoing": "Sedang berlangsung", + "broadcastUpcoming": "Akan datang", + "broadcastCompleted": "Telah selesai", + "broadcastRoundName": "Nama ronde", + "broadcastRoundNumber": "Babak ronde", + "broadcastTournamentName": "Nama turnamen", + "broadcastTournamentDescription": "Deskripsi singkat turnamen", + "broadcastFullDescription": "Keterangan acara secara penuh", + "broadcastFullDescriptionHelp": "Deskripsi panjang opsional dari siaran. {param1} tersedia. Panjangnya harus kurang dari {param2} karakter.", + "broadcastSourceUrlHelp": "URL yang akan di-polling oleh Lichess untuk mendapatkan pembaruan PGN. Itu harus dapat diakses publik dari Internet.", + "broadcastStartDateHelp": "Opsional, jika Anda tahu kapan acara dimulai", + "broadcastCurrentGameUrl": "Tautan permainan ini", + "broadcastDownloadAllRounds": "Unduh semua ronde", + "broadcastResetRound": "Atur ulang ronde ini", + "broadcastDeleteRound": "Hapus ronde ini", "challengeChallengeToPlay": "Menantang ke permainan", "challengeChallengeDeclined": "Tantangan ditolak", "challengeChallengeAccepted": "Tantangan diterima!", @@ -95,6 +112,7 @@ "preferencesExplainShowPlayerRatings": "Ini dapat menyembunyikan rating dari website, agar membantu fokus ke catur. Permainan tetap dapat dinilai, ini hanya untuk apa yang Anda lihat.", "preferencesDisplayBoardResizeHandle": "Tampilkan pengubah ukuran papan catur", "preferencesOnlyOnInitialPosition": "Hanya di posisi awal", + "preferencesInGameOnly": "Hanya di dalam permainan", "preferencesChessClock": "Jam catur", "preferencesTenthsOfSeconds": "Sepersepuluh detik", "preferencesWhenTimeRemainingLessThanTenSeconds": "Ketika waktu tersisa < 10 detik", @@ -328,8 +346,8 @@ "puzzleThemeXRayAttackDescription": "Bidak menyerang atau mempertahankan kotak, melalui bidak musuh.", "puzzleThemeZugzwang": "Zugzwang", "puzzleThemeZugzwangDescription": "Musuh dibatasi gerakan yang dapat mereka lakukan, dan semua gerakan memperburuk posisi mereka.", - "puzzleThemeHealthyMix": "Campuran baik", - "puzzleThemeHealthyMixDescription": "Sedikit dari segalanya. Anda tidak tahu apa yang akan terjadi, jadi Anda tetap siap untuk apapun! Sama seperti permainan sebenarnya.", + "puzzleThemeMix": "Campuran baik", + "puzzleThemeMixDescription": "Sedikit dari segalanya. Anda tidak tahu apa yang akan terjadi, jadi Anda tetap siap untuk apapun! Sama seperti permainan sebenarnya.", "puzzleThemePlayerGames": "Permainan pemain", "puzzleThemePlayerGamesDescription": "Mengambil taktik yang dihasilkan dari permainan anda, atau pemain lain.", "puzzleThemePuzzleDownloadInformation": "Taktik-taktik ini ada di domain publik, dan dapat di download dari {param}.", @@ -374,7 +392,7 @@ "threeChecks": "Tiga kali skak", "raceFinished": "Balapan telah berakhir", "variantEnding": "Akhir sesuai aturan variasi", - "newOpponent": "Penantang baru", + "newOpponent": "Permainan yang baru", "yourOpponentWantsToPlayANewGameWithYou": "Lawan Anda ingin bermain lagi dengan Anda", "joinTheGame": "Ikuti permainan", "whitePlays": "Putih melangkah", @@ -454,7 +472,6 @@ "memory": "Memori", "infiniteAnalysis": "Menganalisa tanpa ada batasan", "removesTheDepthLimit": "Menghapus batas kedalaman, dan membuat komputer Anda hangat", - "engineManager": "Pengaturan komputer", "blunder": "Blunder", "mistake": "Kesalahan", "inaccuracy": "Ketidaktelitian", @@ -480,6 +497,7 @@ "latestForumPosts": "Pesan forum terbaru", "players": "Pemain", "friends": "Teman", + "otherPlayers": "pemain lainnya", "discussions": "Diskusi", "today": "Hari ini", "yesterday": "Kemarin", @@ -710,6 +728,7 @@ "ifNoneLeaveEmpty": "Jika tidak ada, biarkan kosong", "profile": "Profil", "editProfile": "Ubah profil", + "realName": "Nama asli", "setFlair": "Sunting flair anda", "flair": "Flair", "youCanHideFlair": "Terdapat setting untuk menyembunyikan semua flair pengguna pada seluruh situs.", @@ -735,6 +754,7 @@ "descPrivateHelp": "Teks yang hanya bisa dilihat oleh anggota tim. Jika ditetapkan, akan menggantikan deskripsi publik untuk anggota tim.", "no": "Tidak", "yes": "Ya", + "website": "Situs Web", "help": "Bantuan:", "createANewTopic": "Buat topik baru", "topics": "Topik", @@ -753,7 +773,6 @@ "cheat": "Cheat", "troll": "Jebakan", "other": "Lainnya", - "reportDescriptionHelp": "Paste link berikut ke dalam permainan dan jelaskan apa masalah tentang pengguna ini.", "error_provideOneCheatedGameLink": "Harap berikan setidaknya satu tautan ke permainan yang curang.", "by": "oleh {param}", "importedByX": "Diimpor oleh {param}", @@ -956,6 +975,12 @@ "transparent": "Tembus pandang", "deviceTheme": "Tema perangkat", "backgroundImageUrl": "URL gambar latar belakang:", + "board": "Papan", + "size": "Ukuran", + "opacity": "Transparansi", + "brightness": "Kecerahan", + "hue": "Rona", + "boardReset": "Kembalikan warna ke pengaturan semula", "pieceSet": "Susunan buah catur", "embedInYourWebsite": "Salin dalam website Anda", "usernameAlreadyUsed": "Nama pengguna ini sudah digunakan, coba yang lain.", @@ -1015,6 +1040,8 @@ "playVariationToCreateConditionalPremoves": "Memainkan sebuah variasi untuk membuat syarat pra-langkah", "noConditionalPremoves": "Tidak ada syarat pra-langkah", "playX": "Memainkan {param}", + "showUnreadLichessMessage": "Anda telah menerima pesan pribadi dari Lichess.", + "clickHereToReadIt": "Klik di sini untuk membaca", "sorry": "Maaf :(", "weHadToTimeYouOutForAWhile": "Kami harus mengatur waktu Anda untuk sementara waktu.", "why": "Mengapa?", @@ -1036,6 +1063,8 @@ "agreementPolicy": "Saya menyetujui bahwa saya akan mengikuti semua kebijakan yang ada di Lichess.", "searchOrStartNewDiscussion": "Cari atau mulai diskusi baru", "edit": "Ubah", + "bullet": "Peluru", + "blitz": "Blitz", "rapid": "Cepat", "classical": "Klasikal", "ultraBulletDesc": "Sangat-sangat cepat: kurang dari 30 detik", @@ -1126,7 +1155,10 @@ "switchSides": "Tukar Sisi", "closingAccountWithdrawAppeal": "Menutup akun anda akan menarik permohonan banding", "ourEventTips": "Tips dari kami terkait penyelenggaraan acara", + "instructions": "Instruksi", + "showMeEverything": "Tunjukkan semuanya", "lichessPatronInfo": "Lichess adalah sebuah amal dan semuanya merupakan perangkat lunak sumber terbuka yang gratis/bebas.\nSemua biaya operasi, pengembangan, dan konten didanai sepenuhnya oleh donasi pengguna.", + "nothingToSeeHere": "Tidak ada yang bisa dilihat untuk saat ini.", "opponentLeftCounter": "{count, plural, other{Lawanmu telah meninggalkan permainan. Anda dapat mengklaim kemenangan dalam {count} detik.}}", "mateInXHalfMoves": "{count, plural, other{Mati {count} di pertengahan langkah}}", "nbBlunders": "{count, plural, other{{count} blunder}}", @@ -1223,6 +1255,157 @@ "stormXRuns": "{count, plural, other{{count} berjalan}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, other{Memainkan {count} kali {param2}}}", "streamerLichessStreamers": "Streamer Lichess", + "studyPrivate": "Pribadi", + "studyMyStudies": "Studi saya", + "studyStudiesIContributeTo": "Studi yang saya ikut berkontribusi", + "studyMyPublicStudies": "Studi publik saya", + "studyMyPrivateStudies": "Studi pribadi saya", + "studyMyFavoriteStudies": "Studi favorit saya", + "studyWhatAreStudies": "Apa itu studi?", + "studyAllStudies": "Semua studi", + "studyStudiesCreatedByX": "Studi dibuat oleh {param}", + "studyNoneYet": "Tidak ada.", + "studyHot": "Terhangat", + "studyDateAddedNewest": "Tanggal ditambahkan (terbaru)", + "studyDateAddedOldest": "Tanggal ditambahkan (terlama)", + "studyRecentlyUpdated": "Baru saja diperbarui", + "studyMostPopular": "Paling populer", + "studyAlphabetical": "Menurut abjad", + "studyAddNewChapter": "Tambahkan bab baru", + "studyAddMembers": "Tambahkan anggota", + "studyInviteToTheStudy": "Ajak untuk studi", + "studyPleaseOnlyInvitePeopleYouKnow": "Harap hanya mengundang orang yang Anda kenal, dan yang secara aktif ingin bergabung dengan studi ini.", + "studySearchByUsername": "Cari berdasarkan nama pengguna", + "studySpectator": "Penonton", + "studyContributor": "Kontributor", + "studyKick": "Diusir", + "studyLeaveTheStudy": "Tinggalkan studi", + "studyYouAreNowAContributor": "Sekarang Anda menjadi kontributor", + "studyYouAreNowASpectator": "Sekarang Anda adalah penonton", + "studyPgnTags": "Tagar PGN", + "studyLike": "Suka", + "studyUnlike": "Batal Suka", + "studyNewTag": "Tagar baru", + "studyCommentThisPosition": "Komentar di posisi ini", + "studyCommentThisMove": "Komentari langkah ini", + "studyAnnotateWithGlyphs": "Anotasikan dengan glif", + "studyTheChapterIsTooShortToBeAnalysed": "Bab ini terlalu pendek untuk di analisa.", + "studyOnlyContributorsCanRequestAnalysis": "Hanya kontributor yang dapat meminta analisa komputer.", + "studyGetAFullComputerAnalysis": "Dapatkan analisis komputer penuh di pihak server dari jalur utama.", + "studyMakeSureTheChapterIsComplete": "Pastikan bab ini selesai. Anda hanya dapat meminta analisis satu kali.", + "studyAllSyncMembersRemainOnTheSamePosition": "Semua anggota yang ter-sinkron tetap pada posisi yang sama", + "studyShareChanges": "Bagikan perubahan dengan penonton dan simpan di server", + "studyPlaying": "Memainkan", + "studyFirst": "Pertama", + "studyPrevious": "Sebelumnya", + "studyNext": "Berikutnya", + "studyLast": "Terakhir", "studyShareAndExport": "Bagikan & ekspor", - "studyStart": "Mulai" + "studyCloneStudy": "Gandakan", + "studyStudyPgn": "Studi PGN", + "studyDownloadAllGames": "Unduh semua permainan", + "studyChapterPgn": "Bab PGN", + "studyCopyChapterPgn": "Salin PGN", + "studyDownloadGame": "Unduh permainan", + "studyStudyUrl": "URL studi", + "studyCurrentChapterUrl": "URL Bab saat ini", + "studyYouCanPasteThisInTheForumToEmbed": "Anda dapat menempelkan ini di forum untuk disematkan", + "studyStartAtInitialPosition": "Mulai saat posisi awal", + "studyStartAtX": "Mulai dari {param}", + "studyEmbedInYourWebsite": "Sematkan di blog atau website Anda", + "studyReadMoreAboutEmbedding": "Baca lebih tentang penyematan", + "studyOnlyPublicStudiesCanBeEmbedded": "Hanya pelajaran publik yang dapat di sematkan!", + "studyOpen": "Buka", + "studyXBroughtToYouByY": "{param1} dibawakan kepadamu dari {param2}", + "studyStudyNotFound": "Studi tidak ditemukan", + "studyEditChapter": "Ubah bab", + "studyNewChapter": "Bab baru", + "studyImportFromChapterX": "Impor dari {param}", + "studyOrientation": "Orientasi", + "studyAnalysisMode": "Mode analisa", + "studyPinnedChapterComment": "Sematkan komentar bagian bab", + "studySaveChapter": "Simpan bab", + "studyClearAnnotations": "Hapus anotasi", + "studyClearVariations": "Hapus variasi", + "studyDeleteChapter": "Hapus bab", + "studyDeleteThisChapter": "Hapus bab ini? Ini tidak akan dapat mengulangkan kembali!", + "studyClearAllCommentsInThisChapter": "Hapus semua komentar dan bentuk di bab ini?", + "studyRightUnderTheBoard": "Kanan dibawah papan", + "studyNoPinnedComment": "Tidak ada", + "studyNormalAnalysis": "Analisa biasa", + "studyHideNextMoves": "Sembunyikan langkah selanjutnya", + "studyInteractiveLesson": "Pelajaran interaktif", + "studyChapterX": "Bab {param}", + "studyEmpty": "Kosong", + "studyStartFromInitialPosition": "Mulai dari posisi awal", + "studyEditor": "Penyunting", + "studyStartFromCustomPosition": "Mulai dari posisi yang disesuaikan", + "studyLoadAGameByUrl": "Muat permainan dari URL", + "studyLoadAPositionFromFen": "Muat posisi dari FEN", + "studyLoadAGameFromPgn": "Muat permainan dari PGN", + "studyAutomatic": "Otomatis", + "studyUrlOfTheGame": "URL permainan", + "studyLoadAGameFromXOrY": "Muat permainan dari {param1} atau {param2}", + "studyCreateChapter": "Buat bab", + "studyCreateStudy": "Buat studi", + "studyEditStudy": "Ubah studi", + "studyVisibility": "Visibilitas", + "studyPublic": "Publik", + "studyUnlisted": "Tidak terdaftar", + "studyInviteOnly": "Hanya yang diundang", + "studyAllowCloning": "Perbolehkan kloning", + "studyNobody": "Tidak ada seorangpun", + "studyOnlyMe": "Hanya saya", + "studyContributors": "Kontributor", + "studyMembers": "Anggota", + "studyEveryone": "Semua orang", + "studyEnableSync": "Aktifkan sinkronisasi", + "studyYesKeepEveryoneOnTheSamePosition": "Ya: atur semua orang dalam posisi yang sama", + "studyNoLetPeopleBrowseFreely": "Tidak: Bolehkan untuk menjelajah dengan bebas", + "studyPinnedStudyComment": "Sematkan komentar studi", + "studyStart": "Mulai", + "studySave": "Simpan", + "studyClearChat": "Bersihkan obrolan", + "studyDeleteTheStudyChatHistory": "Hapus riwayat obrolan studi? Ini tidak akan dapat mengulangkan kembali!", + "studyDeleteStudy": "Hapus studi", + "studyConfirmDeleteStudy": "Hapus seluruh studi? Tidak dapat kembal lagi! Tuliskan nama studi untuk konfirmasi: {param}", + "studyWhereDoYouWantToStudyThat": "Dimana Anda ingin mempelajarinya?", + "studyGoodMove": "Langkah bagus", + "studyMistake": "Kesalahan", + "studyBrilliantMove": "Langkah Brilian", + "studyBlunder": "Blunder", + "studyInterestingMove": "Langkah menarik", + "studyDubiousMove": "Langkah meragukan", + "studyOnlyMove": "Langkah satu-satunya", + "studyZugzwang": "Zugzwang", + "studyEqualPosition": "Posisi imbang", + "studyUnclearPosition": "Posisi tidak jelas", + "studyWhiteIsSlightlyBetter": "Putih sedikit lebih unggul", + "studyBlackIsSlightlyBetter": "Hitam sedikit lebih unggul", + "studyWhiteIsBetter": "Putih lebih unggul", + "studyBlackIsBetter": "Hitam lebih unggul", + "studyWhiteIsWinning": "Putih menang telak", + "studyBlackIsWinning": "Hitam menang telak", + "studyNovelty": "Langkah baru", + "studyDevelopment": "Pengembangan", + "studyInitiative": "Inisiatif", + "studyAttack": "Serangan", + "studyCounterplay": "Serangan balik", + "studyTimeTrouble": "Tekanan waktu", + "studyWithCompensation": "Dengan kompensasi", + "studyWithTheIdea": "Dengan ide", + "studyNextChapter": "Bab selanjutnya", + "studyPrevChapter": "Bab sebelumnya", + "studyStudyActions": "Pembelajaran", + "studyTopics": "Topik", + "studyMyTopics": "Topik saya", + "studyPopularTopics": "Topik populer", + "studyManageTopics": "Kelola topik", + "studyBack": "Kembali", + "studyPlayAgain": "Main lagi", + "studyYouCompletedThisLesson": "Selamat. Anda telah menyelesaikan pelajaran ini.", + "studyNbChapters": "{count, plural, other{{count} Bab}}", + "studyNbGames": "{count, plural, other{{count} Permainan}}", + "studyNbMembers": "{count, plural, other{{count} Anggota}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, other{Tempelkan PGN kamu disini, lebih dari {count} permainan}}" } \ No newline at end of file diff --git a/lib/l10n/lila_it.arb b/lib/l10n/lila_it.arb index 820667bdd2..7bced24990 100644 --- a/lib/l10n/lila_it.arb +++ b/lib/l10n/lila_it.arb @@ -30,13 +30,18 @@ "mobilePuzzleStormConfirmEndRun": "Vuoi terminare questa serie?", "mobilePuzzleStormFilterNothingToShow": "Nessun risultato, per favore modifica i filtri", "mobileCancelTakebackOffer": "Annulla richiesta di ritiro mossa", - "mobileCancelDrawOffer": "Annulla richiesta di patta", "mobileWaitingForOpponentToJoin": "In attesa dell'avversario...", "mobileBlindfoldMode": "Alla cieca", "mobileLiveStreamers": "Streamer in diretta", "mobileCustomGameJoinAGame": "Unisciti a una partita", "mobileCorrespondenceClearSavedMove": "Cancella mossa salvata", "mobileSomethingWentWrong": "Si è verificato un errore.", + "mobileShowResult": "Mostra il risultato", + "mobilePuzzleThemesSubtitle": ".", + "mobilePuzzleStormSubtitle": "Risolvi il maggior numero di puzzle in tre minuti.", + "mobileGreeting": "Ciao, {param}", + "mobileGreetingWithoutName": "Ciao", + "mobilePrefMagnifyDraggedPiece": "Ingrandisci il pezzo trascinato", "activityActivity": "Attività", "activityHostedALiveStream": "Ha ospitato una diretta", "activityRankedInSwissTournament": "Classificato #{param1} in {param2}", @@ -49,6 +54,7 @@ "activityPlayedNbMoves": "{count, plural, =1{Ha giocato {count} mossa} other{Ha giocato {count} mosse}}", "activityInNbCorrespondenceGames": "{count, plural, =1{in {count} partita per corrispondenza} other{in {count} partite per corrispondenza}}", "activityCompletedNbGames": "{count, plural, =1{Ha concluso {count} partita per corrispondenza} other{Ha concluso {count} partite per corrispondenza}}", + "activityCompletedNbVariantGames": "{count, plural, =1{Partita {count} {param2} per corrispondenza completata} other{Partite {count} {param2} per corrispondenza completate}}", "activityFollowedNbPlayers": "{count, plural, =1{Ha iniziato a seguire {count} giocatore} other{Ha iniziato a seguire {count} giocatori}}", "activityGainedNbFollowers": "{count, plural, =1{Ha guadagnato {count} nuovo follower} other{Ha guadagnato {count} nuovi follower}}", "activityHostedNbSimuls": "{count, plural, =1{Ha ospitato {count} esibizione simultanea} other{Ha ospitato {count} esibizioni simultanee}}", @@ -59,7 +65,51 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{Ha gareggiato in {count} torneo Svizzero} other{Ha gareggiato in {count} tornei Svizzeri}}", "activityJoinedNbTeams": "{count, plural, =1{Si è unito ad {count} squadra} other{Si è unito a {count} squadre}}", "broadcastBroadcasts": "Dirette", + "broadcastMyBroadcasts": "Le mie trasmissioni", "broadcastLiveBroadcasts": "Tornei in diretta", + "broadcastBroadcastCalendar": "Calendario trasmissioni", + "broadcastNewBroadcast": "Nuova diretta", + "broadcastSubscribedBroadcasts": "Trasmissioni abbonate", + "broadcastAboutBroadcasts": "Informazioni sulle trasmissioni", + "broadcastHowToUseLichessBroadcasts": "Istruzioni delle trasmissioni Lichess.", + "broadcastTheNewRoundHelp": "Il nuovo turno avrà gli stessi membri e contributori del precedente.", + "broadcastAddRound": "Aggiungi un turno", + "broadcastOngoing": "In corso", + "broadcastUpcoming": "Prossimamente", + "broadcastCompleted": "Conclusa", + "broadcastCompletedHelp": "Lichess rileva il completamento del turno a seconda delle partite di origine. Utilizza questo interruttore se non è presente alcuna origine.", + "broadcastRoundName": "Nome turno", + "broadcastRoundNumber": "Turno numero", + "broadcastTournamentName": "Nome del torneo", + "broadcastTournamentDescription": "Breve descrizione dell'evento", + "broadcastFullDescription": "Descrizione completa dell'evento", + "broadcastFullDescriptionHelp": "(Facoltativo) Descrizione completa dell'evento. {param1} è disponibile. La lunghezza deve essere inferiore a {param2} caratteri.", + "broadcastSourceSingleUrl": "Sorgente URL PGN", + "broadcastSourceUrlHelp": "L'URL che Lichess utilizzerà per ottenere gli aggiornamenti dei PGN. Deve essere accessibile pubblicamente su Internet.", + "broadcastSourceGameIds": "Fino a 64 ID di partite Lichess, separati da spazi.", + "broadcastStartDateTimeZone": "Data d'inizio nel fuso orario locale del torneo: {param}", + "broadcastStartDateHelp": "Facoltativo, se sai quando inizia l'evento", + "broadcastCurrentGameUrl": "URL della partita corrente", + "broadcastDownloadAllRounds": "Scarica tutti i round", + "broadcastResetRound": "Reimposta questo turno", + "broadcastDeleteRound": "Elimina questo turno", + "broadcastDefinitivelyDeleteRound": "Elimina definitivamente il turno e le sue partite.", + "broadcastDeleteAllGamesOfThisRound": "Elimina tutte le partite di questo turno. L'emittente dovrà essere attiva per poterli ricreare.", + "broadcastEditRoundStudy": "Modifica lo studio del turno", + "broadcastDeleteTournament": "Elimina questo torneo", + "broadcastDefinitivelyDeleteTournament": "Elimina definitivamente l'intero torneo, tutti i turni e tutte le partite.", + "broadcastShowScores": "Mostra i punteggi dei giocatori in base ai risultati del gioco", + "broadcastReplacePlayerTags": "Facoltativo: sostituisci i nomi dei giocatori, i punteggi e i titoli", + "broadcastFideFederations": "Federazioni FIDE", + "broadcastTop10Rating": "Migliori 10 punteggi", + "broadcastFidePlayers": "Giocatori FIDE", + "broadcastFidePlayerNotFound": "Giocatore FIDE non trovato", + "broadcastFideProfile": "Profilo FIDE", + "broadcastFederation": "Federazione", + "broadcastAgeThisYear": "Età quest'anno", + "broadcastUnrated": "Non classificato", + "broadcastRecentTournaments": "Tornei recenti", + "broadcastNbBroadcasts": "{count, plural, =1{{count} diretta} other{{count} dirette}}", "challengeChallengesX": "Sfide: {param1}", "challengeChallengeToPlay": "Sfida a una partita", "challengeChallengeDeclined": "Sfida rifiutata", @@ -378,8 +428,8 @@ "puzzleThemeXRayAttackDescription": "Un pezzo che attacca o difende una casa attraverso un pezzo nemico.", "puzzleThemeZugzwang": "Zugzwang", "puzzleThemeZugzwangDescription": "L'avversario è limitato nella sua scelta della mossa, e tutte le mosse possibili peggiorano la sua posizione.", - "puzzleThemeHealthyMix": "Mix generale", - "puzzleThemeHealthyMixDescription": "Un po' di tutto. Nessuna aspettativa, affinché si possa rimanere pronti a qualsiasi cosa! Proprio come nelle partite vere.", + "puzzleThemeMix": "Mix generale", + "puzzleThemeMixDescription": "Un po' di tutto. Nessuna aspettativa, affinché si possa rimanere pronti a qualsiasi cosa! Proprio come nelle partite vere.", "puzzleThemePlayerGames": "Partite tra giocatori", "puzzleThemePlayerGamesDescription": "Trova problemi tratti dalle tue partite o dalle partite di altri giocatori.", "puzzleThemePuzzleDownloadInformation": "Questi problemi sono nel pubblico dominio e possono essere scaricati da {param}.", @@ -510,7 +560,6 @@ "memory": "Memoria", "infiniteAnalysis": "Analisi infinita", "removesTheDepthLimit": "Rimuove il limite di profondità di analisi, ma può surriscaldare il tuo computer", - "engineManager": "Gestore del motore", "blunder": "Errore grave", "mistake": "Errore", "inaccuracy": "Imprecisione", @@ -536,6 +585,7 @@ "latestForumPosts": "Ultimi interventi nel forum", "players": "Giocatori", "friends": "Amici", + "otherPlayers": "altri giocatori", "discussions": "Conversazioni", "today": "Oggi", "yesterday": "Ieri", @@ -793,6 +843,8 @@ "descPrivateHelp": "Il testo che solo i membri del team vedranno. Se impostato sostituisce la descrizione pubblica per i membri del team.", "no": "No", "yes": "Sì", + "website": "Sito", + "mobile": "Cellulare", "help": "Aiuto:", "createANewTopic": "Crea una nuova discussione", "topics": "Discussioni", @@ -811,7 +863,9 @@ "cheat": "Imbrogli", "troll": "Provocazioni", "other": "Altro", - "reportDescriptionHelp": "Incolla il link della partita/e e spiega cosa non va con questo giocatore. Non dire soltanto \"ha imbrogliato\", ma specifica come sei arrivato a questa conclusione. Il tuo report verrà processato più velocemente se scritto in lingua inglese.", + "reportCheatBoostHelp": "Incolla il link della partita(o partite) e spiega cosa non va sul comportamento di questo utente. Non dire solamente \"ha barato\", ma invece dici come sei arrivato a questa conclusione.", + "reportUsernameHelp": "Spiegaci cosa vi è di offensivo in questo nome utente. Non dire solamente \"è offensivo/inappopriato\", ma invece dici come sei arrivato a questa conclusione, soprattutto se l'insulto è offuscato, non in inglese, in linguaggio giovanile, oppure se è un riferimento storico/culturale.", + "reportProcessedFasterInEnglish": "La tua segnalazione sarà processata più velocemente se scritta in Inglese.", "error_provideOneCheatedGameLink": "Si prega di fornire almeno un collegamento link di una partita in cui il giocatore ha imbrogliato.", "by": "di {param}", "importedByX": "Importato da {param}", @@ -1305,6 +1359,159 @@ "stormXRuns": "{count, plural, =1{1 tentativo} other{{count} tentativi}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Ha fatto un tentativo di {param2}} other{Ha fatto {count} tentativi di {param2}}}", "streamerLichessStreamers": "Lichess streamer", + "studyPrivate": "Privato", + "studyMyStudies": "I miei studi", + "studyStudiesIContributeTo": "Studi a cui collaboro", + "studyMyPublicStudies": "I miei studi pubblici", + "studyMyPrivateStudies": "I miei studi privati", + "studyMyFavoriteStudies": "I miei studi preferiti", + "studyWhatAreStudies": "Cosa sono gli \"studi\"?", + "studyAllStudies": "Tutti gli studi", + "studyStudiesCreatedByX": "Studi creati da {param}", + "studyNoneYet": "Vuoto.", + "studyHot": "Hot", + "studyDateAddedNewest": "Data di pubblicazione (dalla più recente)", + "studyDateAddedOldest": "Data di pubblicazione (dalla meno recente)", + "studyRecentlyUpdated": "Data di aggiornamento (dalla più recente)", + "studyMostPopular": "Più popolari", + "studyAlphabetical": "Alfabetico", + "studyAddNewChapter": "Aggiungi un nuovo capitolo", + "studyAddMembers": "Aggiungi membri", + "studyInviteToTheStudy": "Invita allo studio", + "studyPleaseOnlyInvitePeopleYouKnow": "Invita solo persone che conosci e che desiderano partecipare attivamente a questo studio.", + "studySearchByUsername": "Cerca per nome utente", + "studySpectator": "Spettatore", + "studyContributor": "Partecipante", + "studyKick": "Espelli", + "studyLeaveTheStudy": "Abbandona lo studio", + "studyYouAreNowAContributor": "Ora sei un partecipante", + "studyYouAreNowASpectator": "Ora sei uno spettatore", + "studyPgnTags": "Tag PGN", + "studyLike": "Mi piace", + "studyUnlike": "Non mi Piace", + "studyNewTag": "Nuovo tag", + "studyCommentThisPosition": "Commenta questa posizione", + "studyCommentThisMove": "Commenta questa mossa", + "studyAnnotateWithGlyphs": "Commenta con segni convenzionali", + "studyTheChapterIsTooShortToBeAnalysed": "Il capitolo è troppo breve per essere analizzato.", + "studyOnlyContributorsCanRequestAnalysis": "Solo i partecipanti allo studio possono richiedere un'analisi del computer.", + "studyGetAFullComputerAnalysis": "Richiedi un'analisi completa del computer della variante principale.", + "studyMakeSureTheChapterIsComplete": "Assicurati che il capitolo sia completo. Puoi richiedere l'analisi solo una volta.", + "studyAllSyncMembersRemainOnTheSamePosition": "Tutti i membri in SYNC rimangono sulla stessa posizione", + "studyShareChanges": "Condividi le modifiche con gli spettatori e salvale sul server", + "studyPlaying": "In corso", + "studyShowEvalBar": "Barre di valutazione", + "studyFirst": "Primo", + "studyPrevious": "Precedente", + "studyNext": "Successivo", + "studyLast": "Ultimo", "studyShareAndExport": "Condividi & esporta", - "studyStart": "Inizia" + "studyCloneStudy": "Duplica", + "studyStudyPgn": "PGN dello studio", + "studyDownloadAllGames": "Scarica tutte le partite", + "studyChapterPgn": "PGN del capitolo", + "studyCopyChapterPgn": "Copia in PGN", + "studyDownloadGame": "Scarica partita", + "studyStudyUrl": "URL dello studio", + "studyCurrentChapterUrl": "URL del capitolo corrente", + "studyYouCanPasteThisInTheForumToEmbed": "Puoi incollare questo URL nel forum per creare un rimando", + "studyStartAtInitialPosition": "Inizia dalla prima mossa", + "studyStartAtX": "Inizia a: {param}", + "studyEmbedInYourWebsite": "Incorpora nel tuo sito Web o Blog", + "studyReadMoreAboutEmbedding": "Per saperne di più su come incorporare", + "studyOnlyPublicStudiesCanBeEmbedded": "Solo gli studi pubblici possono essere incorporati!", + "studyOpen": "Apri", + "studyXBroughtToYouByY": "{param1} fornito da {param2}", + "studyStudyNotFound": "Studio non trovato", + "studyEditChapter": "Modifica il capitolo", + "studyNewChapter": "Nuovo capitolo", + "studyImportFromChapterX": "Importa da {param}", + "studyOrientation": "Orientamento", + "studyAnalysisMode": "Modalità analisi", + "studyPinnedChapterComment": "Commento del capitolo", + "studySaveChapter": "Salva capitolo", + "studyClearAnnotations": "Cancella annotazioni", + "studyClearVariations": "Elimina le varianti", + "studyDeleteChapter": "Elimina capitolo", + "studyDeleteThisChapter": "Vuoi davvero eliminare questo capitolo? Sarà perso per sempre!", + "studyClearAllCommentsInThisChapter": "Cancellare tutti i commenti, le annotazioni e i disegni in questo capitolo?", + "studyRightUnderTheBoard": "Sotto la scacchiera", + "studyNoPinnedComment": "Nessun commento", + "studyNormalAnalysis": "Analisi normale", + "studyHideNextMoves": "Nascondi le mosse successive", + "studyInteractiveLesson": "Lezione interattiva", + "studyChapterX": "Capitolo {param}", + "studyEmpty": "Semplice", + "studyStartFromInitialPosition": "Parti dalla posizione iniziale", + "studyEditor": "Editor", + "studyStartFromCustomPosition": "Inizia da una posizione personalizzata", + "studyLoadAGameByUrl": "Carica una partita da URL", + "studyLoadAPositionFromFen": "Carica una posizione da FEN", + "studyLoadAGameFromPgn": "Carica una partita da PGN", + "studyAutomatic": "Automatica", + "studyUrlOfTheGame": "URL della partita", + "studyLoadAGameFromXOrY": "Carica una partita da {param1} o {param2}", + "studyCreateChapter": "Crea capitolo", + "studyCreateStudy": "Crea studio", + "studyEditStudy": "Modifica studio", + "studyVisibility": "Visibilità", + "studyPublic": "Pubblico", + "studyUnlisted": "Non elencato", + "studyInviteOnly": "Solo su invito", + "studyAllowCloning": "Permetti la clonazione", + "studyNobody": "Nessuno", + "studyOnlyMe": "Solo io", + "studyContributors": "Collaboratori", + "studyMembers": "Membri", + "studyEveryone": "Tutti", + "studyEnableSync": "Abilita sincronizzazione", + "studyYesKeepEveryoneOnTheSamePosition": "Sì: tutti vedranno la stessa posizione", + "studyNoLetPeopleBrowseFreely": "No: ognuno potrà scorrere i capitoli indipendentemente", + "studyPinnedStudyComment": "Commento dello studio", + "studyStart": "Inizia", + "studySave": "Salva", + "studyClearChat": "Cancella chat", + "studyDeleteTheStudyChatHistory": "Vuoi davvero eliminare la cronologia della chat? Sarà persa per sempre!", + "studyDeleteStudy": "Elimina studio", + "studyConfirmDeleteStudy": "Eliminare l'intero studio? Non sarà possibile annullare l'operazione! Digitare il nome dello studio per confermare: {param}", + "studyWhereDoYouWantToStudyThat": "Dove vuoi creare lo studio?", + "studyGoodMove": "Bella mossa", + "studyMistake": "Errore", + "studyBrilliantMove": "Mossa geniale", + "studyBlunder": "Errore grave", + "studyInterestingMove": "Mossa interessante", + "studyDubiousMove": "Mossa dubbia", + "studyOnlyMove": "Unica mossa", + "studyZugzwang": "Zugzwang", + "studyEqualPosition": "Posizione equivalente", + "studyUnclearPosition": "Posizione non chiara", + "studyWhiteIsSlightlyBetter": "Il bianco è in lieve vantaggio", + "studyBlackIsSlightlyBetter": "Il nero è in lieve vantaggio", + "studyWhiteIsBetter": "Il bianco è in vantaggio", + "studyBlackIsBetter": "Il nero è in vantaggio", + "studyWhiteIsWinning": "Il bianco sta vincendo", + "studyBlackIsWinning": "Il nero sta vincendo", + "studyNovelty": "Novità", + "studyDevelopment": "Sviluppo", + "studyInitiative": "Iniziativa", + "studyAttack": "Attacco", + "studyCounterplay": "Contrattacco", + "studyTimeTrouble": "Prolemi di tempo", + "studyWithCompensation": "Con compenso", + "studyWithTheIdea": "Con l'idea", + "studyNextChapter": "Prossimo capitolo", + "studyPrevChapter": "Capitolo precedente", + "studyStudyActions": "Studia azioni", + "studyTopics": "Discussioni", + "studyMyTopics": "Le mie discussioni", + "studyPopularTopics": "Argomenti popolari", + "studyManageTopics": "Gestisci discussioni", + "studyBack": "Indietro", + "studyPlayAgain": "Gioca di nuovo", + "studyWhatWouldYouPlay": "Cosa giocheresti in questa posizione?", + "studyYouCompletedThisLesson": "Congratulazioni! Hai completato questa lezione.", + "studyNbChapters": "{count, plural, =1{{count} capitolo} other{{count} capitoli}}", + "studyNbGames": "{count, plural, =1{{count} partita} other{{count} partite}}", + "studyNbMembers": "{count, plural, =1{{count} membro} other{{count} membri}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Incolla qui il testo PGN, massimo {count} partita} other{Incolla qui i testi PGN, massimo {count} partite}}" } \ No newline at end of file diff --git a/lib/l10n/lila_ja.arb b/lib/l10n/lila_ja.arb index 8e717358c8..3aa1a04e0e 100644 --- a/lib/l10n/lila_ja.arb +++ b/lib/l10n/lila_ja.arb @@ -30,7 +30,6 @@ "mobilePuzzleStormConfirmEndRun": "このストームを終了しますか?", "mobilePuzzleStormFilterNothingToShow": "条件に合う問題がありません。フィルターを変更してください", "mobileCancelTakebackOffer": "待ったをキャンセル", - "mobileCancelDrawOffer": "ドロー提案をキャンセル", "mobileWaitingForOpponentToJoin": "対戦相手の参加を待っています…", "mobileBlindfoldMode": "めかくしモード", "mobileLiveStreamers": "ライブ配信者", @@ -42,6 +41,7 @@ "mobilePuzzleStormSubtitle": "3 分間でできるだけ多くの問題を解いてください。", "mobileGreeting": "こんにちは {param} さん", "mobileGreetingWithoutName": "こんにちは", + "mobilePrefMagnifyDraggedPiece": "ドラッグ中の駒を拡大", "activityActivity": "活動", "activityHostedALiveStream": "ライブ配信", "activityRankedInSwissTournament": "{param1} 位({param2})", @@ -54,6 +54,7 @@ "activityPlayedNbMoves": "{count, plural, other{{count} 手をプレイ}}", "activityInNbCorrespondenceGames": "{count, plural, other{(通信戦 {count} 局で)}}", "activityCompletedNbGames": "{count, plural, other{{count} 局の通信戦を完了}}", + "activityCompletedNbVariantGames": "{count, plural, other{{count} 局の {param2} 通信戦を完了しました}}", "activityFollowedNbPlayers": "{count, plural, other{{count} 人をフォロー開始}}", "activityGainedNbFollowers": "{count, plural, other{{count} 人の新規フォロワーを獲得}}", "activityHostedNbSimuls": "{count, plural, other{{count} 回の同時対局を主催}}", @@ -64,7 +65,71 @@ "activityCompetedInNbSwissTournaments": "{count, plural, other{{count} 回のスイス式トーナメントに参加}}", "activityJoinedNbTeams": "{count, plural, other{{count} チームに参加}}", "broadcastBroadcasts": "イベント中継", + "broadcastMyBroadcasts": "自分の配信", "broadcastLiveBroadcasts": "実戦トーナメントのライブ中継", + "broadcastBroadcastCalendar": "中継カレンダー", + "broadcastNewBroadcast": "新しいライブ中継", + "broadcastSubscribedBroadcasts": "登録した配信", + "broadcastAboutBroadcasts": "中継について", + "broadcastHowToUseLichessBroadcasts": "Lichess 中継の使い方。", + "broadcastTheNewRoundHelp": "新ラウンドには前回と同じメンバーと投稿者が参加します。", + "broadcastAddRound": "ラウンドを追加", + "broadcastOngoing": "配信中", + "broadcastUpcoming": "予定", + "broadcastCompleted": "終了", + "broadcastCompletedHelp": "Lichess は元になる対局に基づいてラウンド終了を検出します。元になる対局がない時はこのトグルを使ってください。", + "broadcastRoundName": "ラウンド名", + "broadcastRoundNumber": "ラウンド", + "broadcastTournamentName": "大会名", + "broadcastTournamentDescription": "大会の短い説明", + "broadcastFullDescription": "長い説明", + "broadcastFullDescriptionHelp": "内容の詳しい説明(オプション)。{param1} が利用できます。長さは [欧文換算で] {param2} 字まで。", + "broadcastSourceSingleUrl": "PGN のソース URL", + "broadcastSourceUrlHelp": "Lichess が PGN を取得するための URL。インターネット上に公表されているもののみ。", + "broadcastSourceGameIds": "Lichess ゲーム ID、半角スペースで区切って最大 64 個まで。", + "broadcastStartDateHelp": "イベント開始時刻(オプション)", + "broadcastCurrentGameUrl": "現在のゲームの URL", + "broadcastDownloadAllRounds": "全ラウンドをダウンロード", + "broadcastResetRound": "このラウンドをリセット", + "broadcastDeleteRound": "このラウンドを削除", + "broadcastDefinitivelyDeleteRound": "このラウンドのゲームをすべて削除する。", + "broadcastDeleteAllGamesOfThisRound": "このラウンドのすべてのゲームを削除します。復活させるには情報源がアクティブでなくてはなりません。", + "broadcastEditRoundStudy": "ラウンドの研究を編集", + "broadcastDeleteTournament": "このトーナメントを削除", + "broadcastDefinitivelyDeleteTournament": "トーナメント全体(全ラウンド、全ゲーム)を削除する。", + "broadcastShowScores": "ゲーム結果に応じてプレイヤーのスコアを表示", + "broadcastReplacePlayerTags": "オプション:プレイヤーの名前、レーティング、タイトルの変更", + "broadcastFideFederations": "FIDE 加盟協会", + "broadcastTop10Rating": "レーティング トップ10", + "broadcastFidePlayers": "FIDE 選手", + "broadcastFidePlayerNotFound": "FIDE 選手が見つかりません", + "broadcastFideProfile": "FIDE プロフィール", + "broadcastFederation": "所属協会", + "broadcastAgeThisYear": "今年時点の年齢", + "broadcastUnrated": "レーティングなし", + "broadcastRecentTournaments": "最近のトーナメント", + "broadcastOpenLichess": "Lichess で開く", + "broadcastTeams": "チーム", + "broadcastBoards": "ボード", + "broadcastOverview": "概要", + "broadcastSubscribeTitle": "登録しておくと各ラウンドの開始時に通知が来ます。アカウント設定でベルやプッシュ通知の切り替えができます。", + "broadcastUploadImage": "トーナメントの画像をアップロード", + "broadcastNoBoardsYet": "ボードはまだありません。棋譜がアップロードされると表示されます。", + "broadcastBoardsCanBeLoaded": "ボードはソースまたは {param} 経由で読み込めます", + "broadcastStartsAfter": "{param} 後に開始", + "broadcastStartVerySoon": "中継はまもなく始まります。", + "broadcastNotYetStarted": "中継はまだ始まっていません。", + "broadcastOfficialWebsite": "公式サイト", + "broadcastStandings": "順位", + "broadcastIframeHelp": "他のオプションは {param} にあります", + "broadcastWebmastersPage": "ウェブ管理者のページ", + "broadcastPgnSourceHelp": "このラウンドについて公表されたリアルタイムの PGN です。{param} も利用でき、高速かつ高効率の同期が行なえます。", + "broadcastEmbedThisBroadcast": "この中継をウェブサイトに埋め込む", + "broadcastEmbedThisRound": "{param} をウェブサイトに埋め込む", + "broadcastRatingDiff": "レーティングの差", + "broadcastGamesThisTournament": "このトーナメントの対局", + "broadcastScore": "スコア", + "broadcastNbBroadcasts": "{count, plural, other{{count} ブロードキャスト}}", "challengeChallengesX": "チャレンジ:{param1}", "challengeChallengeToPlay": "対局を申し込む", "challengeChallengeDeclined": "挑戦が拒否されました。", @@ -383,8 +448,8 @@ "puzzleThemeXRayAttackDescription": "相手の駒の向こうにあるマスを間接的に攻撃(または防御)する。", "puzzleThemeZugzwang": "ツークツワンク", "puzzleThemeZugzwangDescription": "相手の指せる手が、どれを選んでも局面を悪くしてしまう形。", - "puzzleThemeHealthyMix": "混合", - "puzzleThemeHealthyMixDescription": "いろいろな問題を少しずつ。どんな問題が来るかわからないので油断しないで! 実戦と同じです。", + "puzzleThemeMix": "混合", + "puzzleThemeMixDescription": "いろいろな問題を少しずつ。どんな問題が来るかわからないので油断しないで! 実戦と同じです。", "puzzleThemePlayerGames": "プレイヤーの対局", "puzzleThemePlayerGamesDescription": "自分の対局、他のプレイヤーの対局から取られた問題を検索します。", "puzzleThemePuzzleDownloadInformation": "これらの問題はパブリックドメインにあり、{param} でダウンロードできます。", @@ -515,7 +580,6 @@ "memory": "メモリ", "infiniteAnalysis": "無限解析", "removesTheDepthLimit": "探索手数の制限をなくし最大限の解析を行なう", - "engineManager": "解析エンジンの管理", "blunder": "大悪手", "mistake": "悪手", "inaccuracy": "疑問手", @@ -597,6 +661,7 @@ "rank": "ランク", "rankX": "ランク: {param}", "gamesPlayed": "対局数", + "ok": "OK", "cancel": "キャンセル", "whiteTimeOut": "白時間切れ", "blackTimeOut": "黒時間切れ", @@ -819,7 +884,9 @@ "cheat": "ソフト使用", "troll": "荒らし", "other": "その他", - "reportDescriptionHelp": "問題のゲームへのリンクを貼って、相手ユーザーの問題点を説明してください。ただ「イカサマだ」と言うのではなく、なぜそう思うか理由を書いてください。英語で書くと対応が早くできます。", + "reportCheatBoostHelp": "ゲームへのリンクを張って、このユーザーの行動のどこが問題かを説明してください。ただ「チート」と言うのではなく、あなたがなぜそう思ったのか教えてください。", + "reportUsernameHelp": "このユーザー名のどこが攻撃的かを説明してください。ただ「攻撃的」「不適切」と言うのではなく、あなたがなぜそう思ったのか教えてください。中でも綴りの変更、英語以外の言語、俗語、歴史・文化的要因に関係した場合は特に説明が必要です。", + "reportProcessedFasterInEnglish": "英語で書いていただくと通報への対応が早くなります。", "error_provideOneCheatedGameLink": "不正のあった対局 1 局以上へのリンクを添えてください。", "by": "{param} によって", "importedByX": "{param} がインポート", @@ -1216,6 +1283,7 @@ "showMeEverything": "すべてを表示", "lichessPatronInfo": "Lichess は非営利組織であり、完全に無料/自由なオープンソースソフトウェアです。\n運営費、開発、コンテンツを支えているのはすべてユーザーの寄付です。", "nothingToSeeHere": "今は何もありません。", + "stats": "統計", "opponentLeftCounter": "{count, plural, other{相手がいなくなりました。後 {count} 秒で勝ちにできます。}}", "mateInXHalfMoves": "{count, plural, other{{count} プライでメイト}}", "nbBlunders": "{count, plural, other{{count} 大悪手}}", @@ -1312,6 +1380,159 @@ "stormXRuns": "{count, plural, other{{count} 回}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, other{{param2} を {count} 回プレイ}}", "streamerLichessStreamers": "Lichess 配信者", + "studyPrivate": "非公開", + "studyMyStudies": "自分の研究", + "studyStudiesIContributeTo": "参加した研究", + "studyMyPublicStudies": "自分の公開研究", + "studyMyPrivateStudies": "自分の非公開研究", + "studyMyFavoriteStudies": "お気に入りの研究", + "studyWhatAreStudies": "研究(study)とは?", + "studyAllStudies": "すべての研究", + "studyStudiesCreatedByX": "{param} による研究", + "studyNoneYet": "まだなし", + "studyHot": "注目", + "studyDateAddedNewest": "投稿日(新しい順)", + "studyDateAddedOldest": "投稿日(古い順)", + "studyRecentlyUpdated": "更新順", + "studyMostPopular": "人気順", + "studyAlphabetical": "アルファベット順", + "studyAddNewChapter": "新たな章を追加", + "studyAddMembers": "メンバーを追加する", + "studyInviteToTheStudy": "この研究に招待する", + "studyPleaseOnlyInvitePeopleYouKnow": "招待する相手は、あなたが知っていて参加したい人だけにしてください。", + "studySearchByUsername": "ユーザー名で検索", + "studySpectator": "観戦者", + "studyContributor": "投稿参加者", + "studyKick": "追放", + "studyLeaveTheStudy": "この研究から出る", + "studyYouAreNowAContributor": "投稿参加者になりました", + "studyYouAreNowASpectator": "観戦者になりました", + "studyPgnTags": "PGN タグ", + "studyLike": "いいね", + "studyUnlike": "いいね解除", + "studyNewTag": "新しいタグ", + "studyCommentThisPosition": "この局面にコメントする", + "studyCommentThisMove": "この手にコメント", + "studyAnnotateWithGlyphs": "解説記号を入れる", + "studyTheChapterIsTooShortToBeAnalysed": "章が短すぎて解析できません。", + "studyOnlyContributorsCanRequestAnalysis": "コンピュータ解析を要請できるのは投稿参加者だけです。", + "studyGetAFullComputerAnalysis": "主手順についてサーバ上でのコンピュータ解析を行なう。", + "studyMakeSureTheChapterIsComplete": "章が完成したか確認してください。解析の要請は 1 回だけです。", + "studyAllSyncMembersRemainOnTheSamePosition": "同期したメンバーは同じ局面に留まります", + "studyShareChanges": "変更を観戦者と共有し、サーバに保存する", + "studyPlaying": "プレイ中", + "studyShowEvalBar": "評価値バー", + "studyFirst": "最初", + "studyPrevious": "前", + "studyNext": "次", + "studyLast": "最後", "studyShareAndExport": "共有とエクスポート", - "studyStart": "開始" + "studyCloneStudy": "研究をコピー", + "studyStudyPgn": "研究の PGN", + "studyDownloadAllGames": "全局をダウンロード", + "studyChapterPgn": "章の PGN", + "studyCopyChapterPgn": "PGN をコピー", + "studyDownloadGame": "1 局をダウンロード", + "studyStudyUrl": "研究の URL", + "studyCurrentChapterUrl": "現在の章の URL", + "studyYouCanPasteThisInTheForumToEmbed": "これをフォーラムにペーストすれば埋め込み表示できます", + "studyStartAtInitialPosition": "開始局面から", + "studyStartAtX": "{param} に開始", + "studyEmbedInYourWebsite": "自分のウェブサイト/ブログに埋め込む", + "studyReadMoreAboutEmbedding": "埋め込み(embedding)の説明", + "studyOnlyPublicStudiesCanBeEmbedded": "埋め込みできるのは公開研究だけです!", + "studyOpen": "開く", + "studyXBroughtToYouByY": "{param1} を {param2} がお届けします", + "studyStudyNotFound": "研究が見つかりません", + "studyEditChapter": "章を編集", + "studyNewChapter": "新しい章", + "studyImportFromChapterX": "{param} からインポート", + "studyOrientation": "盤の上下", + "studyAnalysisMode": "解析モード", + "studyPinnedChapterComment": "章の優先表示コメント", + "studySaveChapter": "章を保存", + "studyClearAnnotations": "注釈をクリア", + "studyClearVariations": "手順をクリア", + "studyDeleteChapter": "章を削除", + "studyDeleteThisChapter": "ほんとうに削除しますか? 戻せませんよ!", + "studyClearAllCommentsInThisChapter": "この章のコメントと図形をすべて削除しますか?", + "studyRightUnderTheBoard": "盤のすぐ下に", + "studyNoPinnedComment": "なし", + "studyNormalAnalysis": "通常解析", + "studyHideNextMoves": "次の手順をかくす", + "studyInteractiveLesson": "対話形式のレッスン", + "studyChapterX": "章 {param}", + "studyEmpty": "空白", + "studyStartFromInitialPosition": "開始局面から", + "studyEditor": "エディタ", + "studyStartFromCustomPosition": "指定した局面から", + "studyLoadAGameByUrl": "棋譜を URL で読み込み", + "studyLoadAPositionFromFen": "局面を FEN で読み込み", + "studyLoadAGameFromPgn": "棋譜を PGN で読み込み", + "studyAutomatic": "自動", + "studyUrlOfTheGame": "棋譜の URL", + "studyLoadAGameFromXOrY": "{param1} か {param2} から棋譜を読み込み", + "studyCreateChapter": "章を作成", + "studyCreateStudy": "研究を作成", + "studyEditStudy": "研究を編集", + "studyVisibility": "公開範囲", + "studyPublic": "公開", + "studyUnlisted": "非公開", + "studyInviteOnly": "招待のみ", + "studyAllowCloning": "コピーの許可", + "studyNobody": "不許可", + "studyOnlyMe": "自分のみ", + "studyContributors": "参加者のみ", + "studyMembers": "メンバー", + "studyEveryone": "全員", + "studyEnableSync": "同期", + "studyYesKeepEveryoneOnTheSamePosition": "同期する=全員が同じ局面を見る", + "studyNoLetPeopleBrowseFreely": "同期しない=各人が自由に閲覧", + "studyPinnedStudyComment": "優先表示コメント", + "studyStart": "開始", + "studySave": "保存", + "studyClearChat": "チャットを消去", + "studyDeleteTheStudyChatHistory": "ほんとうに削除しますか? 戻せませんよ!", + "studyDeleteStudy": "研究を削除", + "studyConfirmDeleteStudy": "研究全体を削除しますか? 戻せませんよ! 削除なら研究の名称を入力: {param}", + "studyWhereDoYouWantToStudyThat": "どこで研究しますか?", + "studyGoodMove": "好手", + "studyMistake": "悪手", + "studyBrilliantMove": "妙手", + "studyBlunder": "大悪手", + "studyInterestingMove": "面白い手", + "studyDubiousMove": "疑問手", + "studyOnlyMove": "絶対手", + "studyZugzwang": "ツークツワンク", + "studyEqualPosition": "互角", + "studyUnclearPosition": "形勢不明", + "studyWhiteIsSlightlyBetter": "白やや優勢", + "studyBlackIsSlightlyBetter": "黒やや優勢", + "studyWhiteIsBetter": "白優勢", + "studyBlackIsBetter": "黒優勢", + "studyWhiteIsWinning": "白勝勢", + "studyBlackIsWinning": "黒勝勢", + "studyNovelty": "新手", + "studyDevelopment": "展開", + "studyInitiative": "主導権", + "studyAttack": "攻撃", + "studyCounterplay": "反撃", + "studyTimeTrouble": "時間切迫", + "studyWithCompensation": "駒損だが代償あり", + "studyWithTheIdea": "狙い", + "studyNextChapter": "次の章", + "studyPrevChapter": "前の章", + "studyStudyActions": "研究の操作", + "studyTopics": "トピック", + "studyMyTopics": "自分のトピック", + "studyPopularTopics": "人気のトピック", + "studyManageTopics": "トピックの管理", + "studyBack": "戻る", + "studyPlayAgain": "もう一度プレイ", + "studyWhatWouldYouPlay": "この局面、あなたならどう指す?", + "studyYouCompletedThisLesson": "おめでとう ! このレッスンを修了しました。", + "studyNbChapters": "{count, plural, other{{count} 章}}", + "studyNbGames": "{count, plural, other{{count} 局}}", + "studyNbMembers": "{count, plural, other{{count} メンバー}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, other{ここに PGN をペースト({count} 局まで)}}" } \ No newline at end of file diff --git a/lib/l10n/lila_kk.arb b/lib/l10n/lila_kk.arb index d8767323ef..590624babb 100644 --- a/lib/l10n/lila_kk.arb +++ b/lib/l10n/lila_kk.arb @@ -50,7 +50,31 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{{count} швейцарлық жарысқа қатысты} other{{count} швейцарлық жарысқа қатысты}}", "activityJoinedNbTeams": "{count, plural, =1{{count} топқа қосылды} other{{count} топқа қосылды}}", "broadcastBroadcasts": "Көрсетілімдер", + "broadcastMyBroadcasts": "Менің көрсетілімдерім", "broadcastLiveBroadcasts": "Жарыстың тікелей көрсетілімдері", + "broadcastNewBroadcast": "Жаңа тікелей көрсетілім", + "broadcastAddRound": "Айналым қосу", + "broadcastOngoing": "Болып жатқан", + "broadcastUpcoming": "Келе жатқан", + "broadcastCompleted": "Аяқталған", + "broadcastRoundName": "Айналым атауы", + "broadcastRoundNumber": "Раунд нөмірі", + "broadcastTournamentName": "Жарыс атауы", + "broadcastTournamentDescription": "Жарыстың қысқа сипаттамасы", + "broadcastFullDescription": "Оқиғаның толық сипаттамасы", + "broadcastFullDescriptionHelp": "Көрсетілімнің қосымша үлкен сипаттамасы. {param1} қолданысқа ашық. Ұзындығы {param2} таңбадан кем болуы керек.", + "broadcastSourceUrlHelp": "PGN жаңартуларын алу үшін Личес тексеретін сілтеме. Ол интернетте баршалыққа ашық болуы керек.", + "broadcastStartDateHelp": "Міндетті емес, егер күнін біліп тұрсаңыз", + "broadcastCurrentGameUrl": "Қазіргі ойын сілтемесі", + "broadcastDownloadAllRounds": "Барлық айналымдарды жүктеп алу", + "broadcastResetRound": "Бұл айналымды жаңарту", + "broadcastDeleteRound": "Бұл айналымды жою", + "broadcastDefinitivelyDeleteRound": "Айналым мен оның ойындарын толығымен жою.", + "broadcastDeleteAllGamesOfThisRound": "Айналымның бүкіл ойындарын жою. Оларды қайта құру үшін қайнар көзі белсенді болуы керек.", + "broadcastEditRoundStudy": "Айналымның зертханасын өзгерту", + "broadcastDeleteTournament": "Бұл жарысты жою", + "broadcastDefinitivelyDeleteTournament": "Жарысты айналым мен ойындарымен бірге толығымен жою.", + "broadcastNbBroadcasts": "{count, plural, =1{{count} көрсетілім} other{{count} көрсетілім}}", "challengeChallengesX": "Шақырулар: {param1}", "challengeChallengeToPlay": "Ойынға шақыру", "challengeChallengeDeclined": "Шақыруды қабылдамады", @@ -368,8 +392,8 @@ "puzzleThemeXRayAttackDescription": "Бір тастың қарсылас тасын аттай бір шаршыны қорғауы не шабуылдауы.", "puzzleThemeZugzwang": "Цугцванг", "puzzleThemeZugzwangDescription": "Жүрісі шектелген тастардың әр жүрісі жалпы жағдайдың нашарлауына әкеп соқтыратын кез.", - "puzzleThemeHealthyMix": "Аралас дастархан", - "puzzleThemeHealthyMixDescription": "Барлығынан аз-аздан. Күтпеген жағдайларға бейім болыңыз! Дәл нағыз шахматтағыдай!", + "puzzleThemeMix": "Аралас дастархан", + "puzzleThemeMixDescription": "Барлығынан аз-аздан. Күтпеген жағдайларға бейім болыңыз! Дәл нағыз шахматтағыдай!", "puzzleThemePlayerGames": "Ойыншылардан", "puzzleThemePlayerGamesDescription": "Сіздің не басқаның ойындарынан құрылған жұмбақтарды табу.", "puzzleThemePuzzleDownloadInformation": "Осы жұмбақтар көпшілікке ашық доменде сақтаулы, оларды осы жерден жүктеп алуға болады: {param}", @@ -497,7 +521,6 @@ "memory": "Жад", "infiniteAnalysis": "Шектеусіз талдау", "removesTheDepthLimit": "Тереңдік шектеулерін жояды, әрі компьютеріңізді қыздырады", - "engineManager": "Есептеуіш басқарушысы", "blunder": "Өрескел қателік", "mistake": "Қателік", "inaccuracy": "Жеңіл қате", @@ -788,7 +811,6 @@ "cheat": "Чит, алдап ойнау", "troll": "Троль, кемсіту", "other": "Басқа", - "reportDescriptionHelp": "Ойынның (-дардың) сілтемесін қойып, осы ойыншының қай әрекеті орынсыз болғанын түсіндіріп беріңіз. Жай ғана \"ол алдап ойнады\" деп жаза салмай, осы ойға қалай келгеніңізді айтып беріңіз. Сіздің шағымыңыз ағылшын тілінде жазылса, тезірек тексеруден өтеді.", "error_provideOneCheatedGameLink": "Кемі бір ойынның сілтемесін беруіңізді сұраймыз.", "by": "жасаған – {param}", "importedByX": "Жүктеп салған – {param}", @@ -965,8 +987,8 @@ "revokeAllSessions": "бәрін ажырату", "playChessEverywhere": "Шахматты кез-келген жерде ойнаңыз", "asFreeAsLichess": "Личес-тей тегін", - "builtForTheLoveOfChessNotMoney": "Ақша қуып емес, шахматты қызыққаннан жасап отырмыз", - "everybodyGetsAllFeaturesForFree": "Барлық құралдары бүкіл адам үшін тегін", + "builtForTheLoveOfChessNotMoney": "Ақша қуып емес, шахматты жақсы көргеннен жасап отырмыз", + "everybodyGetsAllFeaturesForFree": "Барлық құралдары барлық адам үшін тегін", "zeroAdvertisement": "Еш жарнамасыз", "fullFeatured": "Толыққанды жабдықталған", "phoneAndTablet": "Телепон мен планшет", @@ -1201,7 +1223,7 @@ "blocks": "{count, plural, =1{{count} бұғатталды} other{{count} бұғатталды}}", "nbForumPosts": "{count, plural, =1{{count} форумдағы жазба} other{{count} форумдағы жазба}}", "nbPerfTypePlayersThisWeek": "{count, plural, =1{Осы аптада {count} {param2}-ойыншы.} other{Осы аптада {count} {param2}-ойыншы.}}", - "availableInNbLanguages": "{count, plural, =1{{count} тіліндегі нұсқасы бар!} other{{count} тіліндегі нұсқасы бар!}}", + "availableInNbLanguages": "{count, plural, =1{{count} тілде нұсқасы бар!} other{{count} тіліндегі нұсқасы бар!}}", "nbSecondsToPlayTheFirstMove": "{count, plural, =1{Бірінші жүріске {count} секунд бар} other{Бірінші жүріске {count} секунд бар}}", "nbSeconds": "{count, plural, =1{{count} секунд} other{{count} секунд}}", "andSaveNbPremoveLines": "{count, plural, =1{әрі ертелі жүрістердің {count} жолын сақтау} other{әрі ертелі жүрістердің {count} жолын сақтау}}", @@ -1259,6 +1281,158 @@ "stormXRuns": "{count, plural, =1{1 кезең} other{{count} кезең}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{{param2} кезеңнің бірі ойналды} other{{param2} кезеңнің {count} ойналды}}", "streamerLichessStreamers": "Личес стримерлері", + "studyPrivate": "Жеке", + "studyMyStudies": "Менің зерттеулерім", + "studyStudiesIContributeTo": "Қолдауымдағы зерттеулер", + "studyMyPublicStudies": "Жалпыға ашық зерттеулерім", + "studyMyPrivateStudies": "Жеке зерттеулерім", + "studyMyFavoriteStudies": "Қалаулы зерттеулерім", + "studyWhatAreStudies": "Зерттеулер деген не?", + "studyAllStudies": "Бүкіл зерттеулер", + "studyStudiesCreatedByX": "{param} жасаған зерттеулер", + "studyNoneYet": "Әзірге жоқ.", + "studyHot": "Тренд", + "studyDateAddedNewest": "Құрылған күні (жаңадан)", + "studyDateAddedOldest": "Құрылған күні (ескіден)", + "studyRecentlyUpdated": "Жақында құрылған", + "studyMostPopular": "Ең танымалдары", + "studyAlphabetical": "Әліппе ретімен", + "studyAddNewChapter": "Жаңа бөлім құру", + "studyAddMembers": "Мүшелерді қосу", + "studyInviteToTheStudy": "Зерттеуге шақыру", + "studyPleaseOnlyInvitePeopleYouKnow": "Тек таныстарды әрі зерттеуге қосылуға шын ниетті адамдарды ғана шықырыңыз.", + "studySearchByUsername": "Тіркеулі атымен іздеу", + "studySpectator": "Көрермен", + "studyContributor": "Қолдаушы", + "studyKick": "Шығару", + "studyLeaveTheStudy": "Зерттеуден шығу", + "studyYouAreNowAContributor": "Сіз енді қолдаушысыз", + "studyYouAreNowASpectator": "Сіз енді көрерменсіз", + "studyPgnTags": "PGN тэгтері", + "studyLike": "Ұнату", + "studyUnlike": "Ұнатпаймын", + "studyNewTag": "Жаңа тэг", + "studyCommentThisPosition": "Осы тақта күйі туралы пікір қалдыру", + "studyCommentThisMove": "Осы жүріс туралы пікір қалдыру", + "studyAnnotateWithGlyphs": "Глифтермен түсіндірме жазуу", + "studyTheChapterIsTooShortToBeAnalysed": "Бөлім талдауға жарамды болу үшін тым қысқа.", + "studyOnlyContributorsCanRequestAnalysis": "Зерттеу қолдаушылары ғана компьютерлік талдауды сұрай алады.", + "studyGetAFullComputerAnalysis": "Сервер-жақты компьютер осы негізгі жолға толық талдау жасайтын болады.", + "studyMakeSureTheChapterIsComplete": "Талдауды бір рет қана сұрай аласыз, сондықтан бөлімді аяқтауды ұмытпаңыз.", + "studyAllSyncMembersRemainOnTheSamePosition": "Барлық үйлескен мүшелер өз күйінде қалады", + "studyShareChanges": "Көрермендермен өзгертулерді бөлісіңіз әрі серверде сақтап қойыңыз", + "studyPlaying": "Қазір ойында", + "studyFirst": "Бірінші", + "studyPrevious": "Алдыңғы", + "studyNext": "Келесі", + "studyLast": "Соңғы", "studyShareAndExport": "Бөлісу мен Жүктеп алу", - "studyStart": "Бастау" + "studyCloneStudy": "Көшірме", + "studyStudyPgn": "Зерттеудің PGN", + "studyDownloadAllGames": "Барлық ойындарды жүктеп алу", + "studyChapterPgn": "Бөлімнің PGN", + "studyCopyChapterPgn": "PGN-ді көшіру", + "studyDownloadGame": "Ойынды жүктеп алу", + "studyStudyUrl": "Зерттеудің сілтемесі", + "studyCurrentChapterUrl": "Қазіргі бөлімнің сілтемесі", + "studyYouCanPasteThisInTheForumToEmbed": "Сіз бұны форумға не Личес блогыңызға қоя аласыз", + "studyStartAtInitialPosition": "Басталуы: бастапқы күйден", + "studyStartAtX": "Басталуы: {param}", + "studyEmbedInYourWebsite": "Сіздің сайт не блогыңызға арналған енгізу сілтемесі", + "studyReadMoreAboutEmbedding": "Енгізу туралы оқыңыз", + "studyOnlyPublicStudiesCanBeEmbedded": "Тек жалпыға ашық зерттеулер енгізуге жарамды!", + "studyOpen": "Ашу", + "studyXBroughtToYouByY": "{param1}, оны сізге {param2} ұсынды", + "studyStudyNotFound": "Зерттеу табылмады", + "studyEditChapter": "Бөлімді өңдеу", + "studyNewChapter": "Жаңа бөлім", + "studyImportFromChapterX": "{param}-нан жүктеп алу", + "studyOrientation": "Бағыты", + "studyAnalysisMode": "Талдау нұсқасы", + "studyPinnedChapterComment": "Қадаулы бөлім пікірі", + "studySaveChapter": "Бөлімді сақтау", + "studyClearAnnotations": "Түсіндірмені өшіру", + "studyClearVariations": "Тармақты өшіру", + "studyDeleteChapter": "Бөлімді жою", + "studyDeleteThisChapter": "Бөлімді жоясыз ба? Кері жол жоқ!", + "studyClearAllCommentsInThisChapter": "Бөлімдегі бүкіл пікір, глиф пен сызбаларды өшіресіз бе?", + "studyRightUnderTheBoard": "Тура тақтаның астына", + "studyNoPinnedComment": "Жоқ", + "studyNormalAnalysis": "Қалыпты талдау", + "studyHideNextMoves": "Келесі жүрістерді жасыру", + "studyInteractiveLesson": "Интерактивті сабақ", + "studyChapterX": "{param}-ші бөлім", + "studyEmpty": "Бос", + "studyStartFromInitialPosition": "Басталуы: бастапқы күйден", + "studyEditor": "Өңдеуші", + "studyStartFromCustomPosition": "Басталуы: белгілі күйден", + "studyLoadAGameByUrl": "Сілтеме арқылы ойындарды жүктеп салу", + "studyLoadAPositionFromFen": "FEN арқылы ойындарды жүктеп салу", + "studyLoadAGameFromPgn": "PGN арқылы ойындарды жүктеп салу", + "studyAutomatic": "Автоматты түрде", + "studyUrlOfTheGame": "Ойындардың сілтемесі, әр жолға бір-бірден", + "studyLoadAGameFromXOrY": "{param1} не {param2} ойындарын жүктеп салу", + "studyCreateChapter": "Бөлім құру", + "studyCreateStudy": "Зерттеуді құру", + "studyEditStudy": "Зерттеуді өңдеу", + "studyVisibility": "Көрінуі", + "studyPublic": "Жалпыға ашық", + "studyUnlisted": "Жасырын", + "studyInviteOnly": "Шақырумен ғана", + "studyAllowCloning": "Көшірмеге рұқсат беру", + "studyNobody": "Ешкім", + "studyOnlyMe": "Өзім ғана", + "studyContributors": "Қолдаушылар", + "studyMembers": "Мүшелер", + "studyEveryone": "Барлығы", + "studyEnableSync": "Үйлесуді қосу", + "studyYesKeepEveryoneOnTheSamePosition": "Иә: бәрі бірдей күйде болады", + "studyNoLetPeopleBrowseFreely": "Жоқ: бәріне еркін шолуға рұқсат ету", + "studyPinnedStudyComment": "Қадаулы зерттеу пікірі", + "studyStart": "Бастау", + "studySave": "Сақтау", + "studyClearChat": "Чатты өшіру", + "studyDeleteTheStudyChatHistory": "Зерттеудің чат тарихын өшіресіз бе? Кері жол жоқ!", + "studyDeleteStudy": "Зерттеуді жою", + "studyConfirmDeleteStudy": "Бүкіл зерттеуді жоясыз ба? Қайтар жол жоқ. Растау үшін зерттеу атауын жазыңыз: {param}", + "studyWhereDoYouWantToStudyThat": "Бұл күйдің зерттеуін қай жерде бастайсыз?", + "studyGoodMove": "Жақсы жүріс", + "studyMistake": "Қате", + "studyBrilliantMove": "Әдемі жүріс", + "studyBlunder": "Өрескел қателік", + "studyInterestingMove": "Қызық жүріс", + "studyDubiousMove": "Күмәнді жүріс", + "studyOnlyMove": "Жалғыз жүріс", + "studyZugzwang": "Цугцванг", + "studyEqualPosition": "Күйлері шамалас", + "studyUnclearPosition": "Күйі анық емес", + "studyWhiteIsSlightlyBetter": "Ақ сәл күштірек", + "studyBlackIsSlightlyBetter": "Қара сәл күштірек", + "studyWhiteIsBetter": "Ақтың жағдайы жақсы", + "studyBlackIsBetter": "Қараның жағдайы жақсы", + "studyWhiteIsWinning": "Ақ жеңеді", + "studyBlackIsWinning": "Қара жеңеді", + "studyNovelty": "Жаңашылдық", + "studyDevelopment": "Дамыту", + "studyInitiative": "Белсенді", + "studyAttack": "Шабуыл", + "studyCounterplay": "Қарсы шабуыл", + "studyTimeTrouble": "Уақыт қаупі", + "studyWithCompensation": "Өтеумен", + "studyWithTheIdea": "Бір оймен", + "studyNextChapter": "Келесі бөлім", + "studyPrevChapter": "Алдыңғы бөлім", + "studyStudyActions": "Зерттеу әрекеттері", + "studyTopics": "Тақырыптар", + "studyMyTopics": "Менің тақырыптарым", + "studyPopularTopics": "Белгілі тақырыптар", + "studyManageTopics": "Тақырыптарды басқару", + "studyBack": "Кері қайту", + "studyPlayAgain": "Қайта ойнау", + "studyWhatWouldYouPlay": "Осы күйде не ойнамақсыз?", + "studyYouCompletedThisLesson": "Құтты болсын! Сіз бұл сабақты бітірдіңіз.", + "studyNbChapters": "{count, plural, =1{{count} бөлім} other{{count} бөлім}}", + "studyNbGames": "{count, plural, =1{{count} ойын} other{{count} ойын}}", + "studyNbMembers": "{count, plural, =1{{count} мүше} other{{count} мүше}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{PGN мәтінін осында қойыңыз, {count} ойын ғана} other{PGN мәтінін осында қойыңыз, {count} ойынға дейін}}" } \ No newline at end of file diff --git a/lib/l10n/lila_ko.arb b/lib/l10n/lila_ko.arb index 704fad8b14..969f8a6181 100644 --- a/lib/l10n/lila_ko.arb +++ b/lib/l10n/lila_ko.arb @@ -24,39 +24,113 @@ "mobileShareGameURL": "게임 URL 공유", "mobileShareGamePGN": "PGN 공유", "mobileSharePositionAsFEN": "FEN으로 공유", + "mobileShowVariations": "바리에이션 보이기", + "mobileHideVariation": "바리에이션 숨기기", "mobileShowComments": "댓글 보기", "mobilePuzzleStormConfirmEndRun": "이 도전을 종료하시겠습니까?", "mobilePuzzleStormFilterNothingToShow": "표시할 것이 없습니다. 필터를 변경해 주세요", "mobileCancelTakebackOffer": "무르기 요청 취소", - "mobileCancelDrawOffer": "무승부 요청 취소", "mobileWaitingForOpponentToJoin": "상대 참가를 기다리는 중...", + "mobileBlindfoldMode": "기물 가리기", "mobileLiveStreamers": "방송 중인 스트리머", "mobileCustomGameJoinAGame": "게임 참가", "mobileCorrespondenceClearSavedMove": "저장된 수 삭제", "mobileSomethingWentWrong": "문제가 발생했습니다.", + "mobileShowResult": "결과 표시", + "mobilePuzzleThemesSubtitle": "당신이 가장 좋아하는 오프닝으로부터의 퍼즐을 플레이하거나, 테마를 선택하십시오.", + "mobilePuzzleStormSubtitle": "3분 이내에 최대한 많은 퍼즐을 해결하십시오.", + "mobileGreeting": "안녕하세요, {param}", + "mobileGreetingWithoutName": "안녕하세요", + "mobilePrefMagnifyDraggedPiece": "드래그한 기물 확대하기", "activityActivity": "활동", "activityHostedALiveStream": "라이브 스트리밍을 함", "activityRankedInSwissTournament": "{param2}에서 {param1}등", "activitySignedUp": "Lichess에 회원가입함", - "activitySupportedNbMonths": "{count, plural, other{{count} 개월 동안 {param2} 에서 lichess.org 을 후원하였습니다.}}", - "activityPracticedNbPositions": "{count, plural, other{{param2} 에서 총 {count} 개의 포지션을 연습하였습니다.}}", - "activitySolvedNbPuzzles": "{count, plural, other{전술 문제 {count} 개를 해결하였습니다.}}", + "activitySupportedNbMonths": "{count, plural, other{{count}개월 동안 {param2}으로 lichess.org를 후원함}}", + "activityPracticedNbPositions": "{count, plural, other{{param2}에서 총 {count}개의 포지션을 연습함}}", + "activitySolvedNbPuzzles": "{count, plural, other{전술 문제 {count}개를 해결함}}", "activityPlayedNbGames": "{count, plural, other{총 {count} 회의 {param2} 게임을 하였습니다.}}", "activityPostedNbMessages": "{count, plural, other{{param2} 에 총 {count} 개의 글을 게시하였습니다.}}", - "activityPlayedNbMoves": "{count, plural, other{{count} 수를 둠}}", + "activityPlayedNbMoves": "{count, plural, other{수 {count}개를 둠}}", "activityInNbCorrespondenceGames": "{count, plural, other{{count}개의 통신전에서}}", "activityCompletedNbGames": "{count, plural, other{{count} 번의 통신전을 완료하셨습니다.}}", + "activityCompletedNbVariantGames": "{count, plural, other{{count} {param2} 긴 대국전을 완료함}}", "activityFollowedNbPlayers": "{count, plural, other{{count} 명을 팔로우 개시}}", "activityGainedNbFollowers": "{count, plural, other{{count} 명의 신규 팔로워를 얻음}}", "activityHostedNbSimuls": "{count, plural, other{{count} 번의 동시대국을 주최함}}", "activityJoinedNbSimuls": "{count, plural, other{{count} 번의 동시대국에 참가함}}", - "activityCreatedNbStudies": "{count, plural, other{{count} 건의 연구를 작성함}}", + "activityCreatedNbStudies": "{count, plural, other{공부 {count}개 작성함}}", "activityCompetedInNbTournaments": "{count, plural, other{{count} 번 토너먼트에 참가함}}", "activityRankedInTournament": "{count, plural, other{{count} 위 (상위 {param2}%) ({param4} 에서 {param3} 국)}}", "activityCompetedInNbSwissTournaments": "{count, plural, other{{count} 번 토너먼트에 참가함}}", "activityJoinedNbTeams": "{count, plural, other{{count} 팀에 참가함}}", "broadcastBroadcasts": "방송", + "broadcastMyBroadcasts": "내 방송", "broadcastLiveBroadcasts": "실시간 대회 방송", + "broadcastBroadcastCalendar": "방송 달력", + "broadcastNewBroadcast": "새 실시간 방송", + "broadcastSubscribedBroadcasts": "구독 중인 방송", + "broadcastAboutBroadcasts": "방송에 대해서", + "broadcastHowToUseLichessBroadcasts": "리체스 방송을 사용하는 방법.", + "broadcastTheNewRoundHelp": "새로운 라운드에는 이전 라운드와 동일한 구성원과 기여자가 있을 것입니다.", + "broadcastAddRound": "라운드 추가", + "broadcastOngoing": "진행중", + "broadcastUpcoming": "방영 예정", + "broadcastCompleted": "종료됨", + "broadcastCompletedHelp": "Lichess는 경기 완료를 감지하지만, 잘못될 때가 있을 수 있습니다. 수동으로 설정하기 위해 이걸 사용하세요.", + "broadcastRoundName": "라운드 이름", + "broadcastRoundNumber": "라운드 숫자", + "broadcastTournamentName": "토너먼트 이름", + "broadcastTournamentDescription": "짧은 토너먼트 설명", + "broadcastFullDescription": "전체 이벤트 설명", + "broadcastFullDescriptionHelp": "(선택) 방송에 대한 긴 설명입니다. {param1} 사용이 가능합니다. 길이는 {param2} 글자보다 짧아야 합니다.", + "broadcastSourceSingleUrl": "PGN Source URL", + "broadcastSourceUrlHelp": "Lichess가 PGN 업데이트를 받기 위해 확인할 URL입니다. 인터넷에서 공개적으로 액세스 할 수 있어야 합니다.", + "broadcastSourceGameIds": "공간으로 나눠진 64개까지의 Lichess 경기 ID.", + "broadcastStartDateTimeZone": "내 시간대의 토너먼트 시작 날짜: {param}", + "broadcastStartDateHelp": "선택 사항, 언제 이벤트가 시작되는지 알고 있는 경우", + "broadcastCurrentGameUrl": "현재 게임 URL", + "broadcastDownloadAllRounds": "모든 라운드 다운로드받기", + "broadcastResetRound": "라운드 초기화", + "broadcastDeleteRound": "라운드 삭제", + "broadcastDefinitivelyDeleteRound": "라운드와 해당 게임을 완전히 삭제합니다.", + "broadcastDeleteAllGamesOfThisRound": "이 라운드의 모든 게임을 삭제합니다. 다시 생성하려면 소스가 활성화되어 있어야 합니다.", + "broadcastEditRoundStudy": "경기 공부 편집", + "broadcastDeleteTournament": "이 토너먼트 삭제", + "broadcastDefinitivelyDeleteTournament": "토너먼트 전체의 모든 라운드와 게임을 완전히 삭제합니다.", + "broadcastShowScores": "게임 결과에 따라 플레이어 점수 표시", + "broadcastReplacePlayerTags": "선택 사항: 플레이어 이름, 레이팅 및 타이틀 바꾸기", + "broadcastFideFederations": "FIDE 연맹", + "broadcastTop10Rating": "Top 10 레이팅", + "broadcastFidePlayers": "FIDE 선수들", + "broadcastFidePlayerNotFound": "FIDE 선수 찾지 못함", + "broadcastFideProfile": "FIDE 프로필", + "broadcastFederation": "연맹", + "broadcastAgeThisYear": "올해 나이", + "broadcastUnrated": "비레이팅", + "broadcastRecentTournaments": "최근 토너먼트", + "broadcastOpenLichess": "Lichess에서 열기", + "broadcastTeams": "팀", + "broadcastBoards": "보드", + "broadcastOverview": "개요", + "broadcastSubscribeTitle": "라운드가 시작될 때 알림을 받으려면 구독하세요. 계정 설정에서 방송을 위한 벨이나 알림 푸시를 토글할 수 있습니다.", + "broadcastUploadImage": "토너먼트 사진 업로드", + "broadcastNoBoardsYet": "아직 보드가 없습니다. 게임들이 업로드되면 나타납니다.", + "broadcastBoardsCanBeLoaded": "보드들은 소스나 {param}(으)로 로드될 수 있습니다", + "broadcastStartsAfter": "{param} 후 시작", + "broadcastStartVerySoon": "방송이 곧 시작됩니다.", + "broadcastNotYetStarted": "아직 방송이 시작을 하지 않았습니다.", + "broadcastOfficialWebsite": "공식 웹사이트", + "broadcastStandings": "순위", + "broadcastIframeHelp": "{param}에서 더 많은 정보를 확인하실 수 있습니다", + "broadcastWebmastersPage": "웹마스터 페이지", + "broadcastPgnSourceHelp": "이 라운드의 공개된, 실시간 PGN 소스 입니다. 보다 더 빠르고 효율적인 동기화를 위해 {param}도 제공됩니다.", + "broadcastEmbedThisBroadcast": "이 방송을 웹사이트에 삽입하세요", + "broadcastEmbedThisRound": "{param}을(를) 웹사이트에 삼입하세요", + "broadcastRatingDiff": "레이팅 차이", + "broadcastGamesThisTournament": "이 토너먼트의 게임들", + "broadcastScore": "점수", + "broadcastNbBroadcasts": "{count, plural, other{{count} 방송}}", "challengeChallengesX": "도전: {param1}", "challengeChallengeToPlay": "도전 신청", "challengeChallengeDeclined": "도전 거절됨", @@ -91,25 +165,25 @@ "perfStatProgressOverLastXGames": "최근 {param} 게임 동안:", "perfStatRatingDeviation": "레이팅 편차: {param}.", "perfStatRatingDeviationTooltip": "낮은 값일 수록 레이팅이 안정적입니다. {param1} 이상이라면 임시 레이팅으로 간주합니다. 랭킹에 들기 위해서는 이 값이 {param2} (스탠다드 체스) 또는 {param3} (변형 체스) 보다 낮아야 합니다.", - "perfStatTotalGames": "총 게임", + "perfStatTotalGames": "총 대국", "perfStatRatedGames": "레이팅 게임", "perfStatTournamentGames": "토너먼트 게임", "perfStatBerserkedGames": "버서크 게임", "perfStatTimeSpentPlaying": "플레이한 시간", - "perfStatAverageOpponent": "상대의 평균 레이팅", - "perfStatVictories": "승리", - "perfStatDefeats": "패배", + "perfStatAverageOpponent": "상대 평균", + "perfStatVictories": "승", + "perfStatDefeats": "패", "perfStatDisconnections": "연결 끊김", "perfStatNotEnoughGames": "충분한 게임을 하지 않으셨습니다", "perfStatHighestRating": "최고 레이팅: {param}", "perfStatLowestRating": "최저 레이팅: {param}", - "perfStatFromXToY": "{param1}에서 {param2}", + "perfStatFromXToY": "{param1}에서 {param2}까지", "perfStatWinningStreak": "연승", "perfStatLosingStreak": "연패", "perfStatLongestStreak": "최고 기록: {param}", "perfStatCurrentStreak": "현재 기록: {param}", - "perfStatBestRated": "승리한 최고 레이팅", - "perfStatGamesInARow": "연속 게임 플레이", + "perfStatBestRated": "최고 레이팅 승리", + "perfStatGamesInARow": "연속 대국", "perfStatLessThanOneHour": "게임 사이가 1시간 미만인 경우", "perfStatMaxTimePlaying": "게임을 한 최대 시간", "perfStatNow": "지금", @@ -153,8 +227,9 @@ "preferencesClaimDrawOnThreefoldRepetitionAutomatically": "3회 동형반복시 자동으로 무승부 요청", "preferencesWhenTimeRemainingLessThanThirtySeconds": "남은 시간이 30초 미만일 때만", "preferencesMoveConfirmation": "피스를 움직이기 전에 물음", - "preferencesInCorrespondenceGames": "긴 대국에서만", - "preferencesCorrespondenceAndUnlimited": "긴 대국과 무제한", + "preferencesExplainCanThenBeTemporarilyDisabled": "경기 도중 보드 메뉴에서 비활성화될 수 있습니다.", + "preferencesInCorrespondenceGames": "통신전", + "preferencesCorrespondenceAndUnlimited": "통신과 무제한", "preferencesConfirmResignationAndDrawOffers": "기권 또는 무승부 제안시 물음", "preferencesCastleByMovingTheKingTwoSquaresOrOntoTheRook": "캐슬링 방법", "preferencesCastleByMovingTwoSquares": "왕을 2칸 옮기기", @@ -170,10 +245,10 @@ "preferencesNotifyInboxMsg": "새로운 받은 편지함 메시지", "preferencesNotifyForumMention": "포럼 댓글에서 당신이 언급됨", "preferencesNotifyInvitedStudy": "스터디 초대", - "preferencesNotifyGameEvent": "긴 대국 업데이트", + "preferencesNotifyGameEvent": "통신전 업데이트", "preferencesNotifyChallenge": "도전 과제", "preferencesNotifyTournamentSoon": "곧 토너먼트 시작할 때", - "preferencesNotifyTimeAlarm": "긴 대국 시간 초과", + "preferencesNotifyTimeAlarm": "통신전 시간 곧 만료됨", "preferencesNotifyBell": "리체스 내에서 벨 알림", "preferencesNotifyPush": "리체스를 사용하지 않을 때 기기 알림", "preferencesNotifyWeb": "브라우저", @@ -194,9 +269,9 @@ "puzzleVoteToLoadNextOne": "다음 퍼즐을 위해 투표해주세요!", "puzzleUpVote": "퍼즐 추천", "puzzleDownVote": "퍼즐 비추천", - "puzzleYourPuzzleRatingWillNotChange": "당신의 퍼즐 레이팅은 바뀌지 않을 것입니다. 퍼즐은 경쟁이 아니라는 걸 기억하세요. 레이팅은 당신의 현재 스킬에 맞는 퍼즐을 선택하도록 돕습니다.", + "puzzleYourPuzzleRatingWillNotChange": "당신의 퍼즐 레이팅은 바뀌지 않을 것입니다. 퍼즐은 경쟁이 아니라는 걸 기억하세요. 레이팅은 당신의 현재 수준에 맞는 퍼즐을 선택하도록 돕습니다.", "puzzleFindTheBestMoveForWhite": "백의 최고의 수를 찾아보세요.", - "puzzleFindTheBestMoveForBlack": "흑의 최고의 수를 찾아보세요.", + "puzzleFindTheBestMoveForBlack": "흑의 최선 수를 찾아보세요.", "puzzleToGetPersonalizedPuzzles": "개인화된 퍼즐을 위해선:", "puzzlePuzzleId": "퍼즐 {param}", "puzzlePuzzleOfTheDay": "오늘의 퍼즐", @@ -212,7 +287,7 @@ "puzzleOpeningsYouPlayedTheMost": "레이팅 게임에서 가장 많이 플레이한 오프닝", "puzzleUseFindInPage": "브라우저의 \"페이지에서 찾기\" 메뉴를 이용해 가장 좋아하는 오프닝을 찾으세요!", "puzzleUseCtrlF": "Ctrl+f를 사용해서 가장 좋아하는 오프닝을 찾으세요!", - "puzzleNotTheMove": "답이 아닙니다!", + "puzzleNotTheMove": "그 수가 아닙니다!", "puzzleTrySomethingElse": "다른 것 시도하기", "puzzleRatingX": "레이팅: {param}", "puzzleHidden": "숨겨짐", @@ -291,7 +366,7 @@ "puzzleThemeClearanceDescription": "이어지는 전술적 아이디어를 위해 칸, 파일 또는 대각선을 비우는 수입니다.", "puzzleThemeDefensiveMove": "방어적인 수", "puzzleThemeDefensiveMoveDescription": "기물을 잃거나 다른 손실을 피하기 위해 필요한 정확한 수입니다.", - "puzzleThemeDeflection": "유인", + "puzzleThemeDeflection": "굴절", "puzzleThemeDeflectionDescription": "중요한 칸을 수비하는 등 다른 역할을 수행하는 상대 기물의 주의를 분산시키는 수입니다. \"과부하\"라고도 불립니다.", "puzzleThemeDiscoveredAttack": "디스커버드 어택", "puzzleThemeDiscoveredAttackDescription": "장거리 기물(예: 룩)의 길을 막고 있는 기물(예: 나이트)을 이동시켜 공격합니다.", @@ -374,8 +449,8 @@ "puzzleThemeXRayAttackDescription": "기물이 상대 기물 너머의 칸을 공격 또는 방어합니다.", "puzzleThemeZugzwang": "추크추방", "puzzleThemeZugzwangDescription": "상대가 둘 수 있는 수는 제한되어 있으며, 모든 수가 포지션을 악화시킵니다.", - "puzzleThemeHealthyMix": "골고루 섞기", - "puzzleThemeHealthyMixDescription": "전부 다. 무엇이 나올지 모르기 때문에 모든 것에 준비되어 있어야 합니다. 마치 진짜 게임처럼요.", + "puzzleThemeMix": "골고루 섞기", + "puzzleThemeMixDescription": "전부 다. 무엇이 나올지 모르기 때문에 모든 것에 준비되어 있어야 합니다. 마치 진짜 게임처럼요.", "puzzleThemePlayerGames": "플레이어 게임", "puzzleThemePlayerGamesDescription": "당신의 게임이나 다른 플레이어의 게임에서 나온 퍼즐을 찾아보세요.", "puzzleThemePuzzleDownloadInformation": "이 퍼즐들은 퍼블릭 도메인이며, {param}에서 다운로드할 수 있습니다.", @@ -389,9 +464,9 @@ "settingsCloseAccountExplanation": "정말로 계정을 닫고 싶으신가요? 계정 폐쇄는 되돌릴 수 없습니다. 절대로 다시 로그인 할 수 없습니다.", "settingsThisAccountIsClosed": "계정이 폐쇄되었습니다.", "playWithAFriend": "친구와 게임하기", - "playWithTheMachine": "체스 엔진과 게임하기", + "playWithTheMachine": "체스 엔진과 붙기", "toInviteSomeoneToPlayGiveThisUrl": "이 URL로 친구를 초대하세요", - "gameOver": "게임 종료", + "gameOver": "게임 오버", "waitingForOpponent": "상대를 기다리는 중", "orLetYourOpponentScanQrCode": "또는 상대방에게 이 QR 코드를 스캔하게 하세요", "waiting": "기다리는 중", @@ -404,8 +479,8 @@ "resign": "기권", "checkmate": "체크메이트", "stalemate": "스테일메이트", - "white": "백색", - "black": "흑색", + "white": "백", + "black": "흑", "asWhite": "백일때", "asBlack": "흑일때", "randomColor": "무작위", @@ -422,59 +497,61 @@ "variantEnding": "변형 게임 엔딩", "newOpponent": "새 상대", "yourOpponentWantsToPlayANewGameWithYou": "상대가 재대결을 원합니다", - "joinTheGame": "게임 참가", - "whitePlays": "백색 차례", - "blackPlays": "흑색 차례", + "joinTheGame": "대국 참가", + "whitePlays": "백 차례", + "blackPlays": "흑 차례", "opponentLeftChoices": "당신의 상대가 게임을 나갔습니다. 상대를 기다리거나 승리 또는 무승부 처리할 수 있습니다.", - "forceResignation": "승리 처리", - "forceDraw": "무승부 처리", + "forceResignation": "승리 취하기", + "forceDraw": "무승부 선언", "talkInChat": "건전한 채팅을 해주세요!", "theFirstPersonToComeOnThisUrlWillPlayWithYou": "이 URL로 가장 먼저 들어온 사람과 체스를 두게 됩니다.", - "whiteResigned": "백색 기권", - "blackResigned": "흑색 기권", - "whiteLeftTheGame": "백색이 게임을 나갔습니다", - "blackLeftTheGame": "흑색이 게임을 나갔습니다", + "whiteResigned": "백 기권함", + "blackResigned": "흑 기권함", + "whiteLeftTheGame": "백이 게임을 나갔습니다", + "blackLeftTheGame": "흑이 게임을 나갔습니다", "whiteDidntMove": "백이 두지 않음", - "blackDidntMove": "흑이 두지 않음", - "requestAComputerAnalysis": "컴퓨터 분석 요청", + "blackDidntMove": "흑이 수를 두지 않음", + "requestAComputerAnalysis": "컴퓨터 분석 요청하기", "computerAnalysis": "컴퓨터 분석", "computerAnalysisAvailable": "컴퓨터 분석이 가능합니다.", - "computerAnalysisDisabled": "컴퓨터 분석 꺼짐", + "computerAnalysisDisabled": "컴퓨터 분석 비활성화됨", "analysis": "분석", "depthX": "{param} 수까지 탐색", "usingServerAnalysis": "서버 분석 사용하기", - "loadingEngine": "엔진 로드 중 ...", + "loadingEngine": "엔진 로드 중...", "calculatingMoves": "수 계산 중...", - "engineFailed": "엔진 로딩 에러", + "engineFailed": "엔진 불러오는 도중 오류 발생", "cloudAnalysis": "클라우드 분석", "goDeeper": "더 깊게 분석하기", - "showThreat": "위험요소 표시하기", + "showThreat": "위험요소 표시", "inLocalBrowser": "브라우저에서", - "toggleLocalEvaluation": "개인 컴퓨터에서 분석하기", - "promoteVariation": "게임 분석 후에 어떤 수에 대한 예상결과들을 확인하고 싶다면", + "toggleLocalEvaluation": "로컬 분석 토글", + "promoteVariation": "변형 승격", "makeMainLine": "주 라인으로 하기", "deleteFromHere": "여기서부터 삭제", + "collapseVariations": "바리에이션 축소하기", + "expandVariations": "바리에이션 확장하기", "forceVariation": "변화 강제하기", "copyVariationPgn": "변동 PGN 복사", "move": "수", "variantLoss": "변형 체스에서 패배", "variantWin": "변형 체스에서 승리", - "insufficientMaterial": "기물 부족으로 무승부입니다.", + "insufficientMaterial": "기물 부족", "pawnMove": "폰 이동", - "capture": "Capture", + "capture": "기물 잡기", "close": "닫기", "winning": "이기는 수", "losing": "지는 수", "drawn": "무승부", "unknown": "알 수 없음", "database": "데이터베이스", - "whiteDrawBlack": "백 : 무승부 : 흑", + "whiteDrawBlack": "백 / 무승부 / 흑", "averageRatingX": "평균 레이팅: {param}", "recentGames": "최근 게임", "topGames": "최고 레이팅 게임", "masterDbExplanation": "{param2}년과 {param3}년 사이 FIDE 레이팅이 최소 {param1}였던 선수들의 기보가 약 2백만개 있습니다.", "dtzWithRounding": "다음 포획 혹은 폰 수까지 남은 반수를 반올림후 나타낸 DTZ50\" 수치", - "noGameFound": "게임을 찾을 수 없습니다.", + "noGameFound": "게임을 찾을 수 없습니다", "maxDepthReached": "최대 깊이 도달!", "maybeIncludeMoreGamesFromThePreferencesMenu": "설정에서 더 많은 게임을 포함하세요.", "openings": "오프닝", @@ -482,32 +559,31 @@ "openingEndgameExplorer": "오프닝/엔드게임 탐색기", "xOpeningExplorer": "{param} 오프닝 탐색기", "playFirstOpeningEndgameExplorerMove": "첫 번째 오프닝/엔드게임 탐색기 수 두기", - "winPreventedBy50MoveRule": "50수 규칙에 의하여 승리가 불가능합니다.", - "lossSavedBy50MoveRule": "50수 규칙에 의하여 패배가 불가능합니다.", + "winPreventedBy50MoveRule": "50수 규칙에 의하여 승리가 불가능합니다", + "lossSavedBy50MoveRule": "50수 규칙에 의하여 패배가 불가능합니다", "winOr50MovesByPriorMistake": "승리 혹은 이전의 실수로 인한 50수 규칙 무승부", "lossOr50MovesByPriorMistake": "패배 혹은 이전의 실수로 인한 50수 규칙 무승부", "unknownDueToRounding": "DTZ 수치의 반올림 때문에 추천된 테이블베이스 라인을 따라야만 승리 및 패배가 보장됩니다.", - "allSet": "모든 설정 완료!", + "allSet": "모두 완료!", "importPgn": "PGN 가져오기", "delete": "삭제", "deleteThisImportedGame": "가져온 게임을 삭제할까요?", "replayMode": "게임 다시보기", "realtimeReplay": "실시간", - "byCPL": "평가치변화", + "byCPL": "센티폰 손실", "openStudy": "연구를 시작하기", "enable": "활성화", "bestMoveArrow": "최선의 수 화살표", "showVariationArrows": "바리에이션 화살표 표시하기", - "evaluationGauge": "평가치 게이지", - "multipleLines": "다중 분석 수", + "evaluationGauge": "평가 게이지", + "multipleLines": "다중 라인 수", "cpus": "CPU 수", "memory": "메모리", "infiniteAnalysis": "무한 분석", "removesTheDepthLimit": "탐색 깊이 제한을 없애고 컴퓨터를 따뜻하게 해줍니다", - "engineManager": "엔진 매니저", - "blunder": "심각한 실수", + "blunder": "블런더", "mistake": "실수", - "inaccuracy": "사소한 실수", + "inaccuracy": "부정확한 수", "moveTimes": "이동 시간", "flipBoard": "보드 돌리기", "threefoldRepetition": "3회 동형반복", @@ -515,54 +591,55 @@ "offerDraw": "무승부 요청", "draw": "무승부", "drawByMutualAgreement": "상호 동의에 의한 무승부", - "fiftyMovesWithoutProgress": "진전이 없이 50수 소모", - "currentGames": "진행 중인 게임", + "fiftyMovesWithoutProgress": "진전 없이 50수 소모", + "currentGames": "진행 중인 게임들", "viewInFullSize": "크게 보기", "logOut": "로그아웃", "signIn": "로그인", "rememberMe": "로그인 유지", - "youNeedAnAccountToDoThat": "회원만이 접근할 수 있습니다.", + "youNeedAnAccountToDoThat": "회원이어야 가능합니다", "signUp": "회원 가입", - "computersAreNotAllowedToPlay": "컴퓨터나 컴퓨터의 도움을 받는 플레이어는 대국이 금지되어 있습니다. 대국할 때 체스 엔진이나 관련 자료, 또는 주변 플레이어로부터 도움을 받지 마십시오. 또한, 다중 계정 사용은 권장하지 않으며 지나치게 많은 다중 계정을 사용할 시 계정이 차단될 수 있습니다.", + "computersAreNotAllowedToPlay": "컴퓨터나 컴퓨터 지원을 받는 플레이어들은 게임 참가가 금지되어 있습니다. 게임 중 체스 엔진이나, 데이터베이스나, 주변 플레이어들로부터 도움을 받지 마십시오. 이와 더불어 다중 계정 소유는 권장하지 않으며 지나치게 많은 계정들을 사용할 시 계정들이 차단될 수 있습니다.", "games": "게임", "forum": "포럼", "xPostedInForumY": "{param1}(이)가 {param2} 쓰레드에 글을 씀", "latestForumPosts": "최근 포럼 글", "players": "플레이어", "friends": "친구들", - "discussions": "토론", + "otherPlayers": "다른 플레이어들", + "discussions": "대화", "today": "오늘", "yesterday": "어제", - "minutesPerSide": "주어진 시간(분)", + "minutesPerSide": "양쪽 시간(분)", "variant": "게임 종류", "variants": "변형", - "timeControl": "시간 제한", - "realTime": "짧은 대국", + "timeControl": "제한 시간(분)", + "realTime": "차례 없음", "correspondence": "긴 대국", - "daysPerTurn": "한 수에 걸리는 일수", + "daysPerTurn": "수당 일수", "oneDay": "1일", "time": "시간", "rating": "레이팅", "ratingStats": "레이팅 통계", - "username": "아이디", + "username": "유저네임", "usernameOrEmail": "사용자 이름이나 이메일 주소", - "changeUsername": "사용자명 변경", - "changeUsernameNotSame": "글자의 대소문자 변경만 가능합니다 예: \"johndoe\" to \"JohnDoe\".", - "changeUsernameDescription": "닉네임 변경하기: 대/소문자의 변경만이 허용되며, 단 한번만 가능한 작업입니다.", + "changeUsername": "사용자 이름 변경", + "changeUsernameNotSame": "글자의 대소문자 변경만 가능합니다 예: \"chulsoo\"에서 \"ChulSoo\"로.", + "changeUsernameDescription": "사용자명 변경하기: 대/소문자만의 변경이 허용되며, 단 한번만 가능합니다.", "signupUsernameHint": "사용자 이름이 어린이를 포함해 모두에게 적절한지 확인하세요. 나중에 변경할 수 없으며 부적절한 사용자 이름을 가진 계정은 폐쇄됩니다!", "signupEmailHint": "비밀번호 초기화를 위해서만 사용됩니다.", "password": "비밀번호", "changePassword": "비밀번호 변경", - "changeEmail": "메일 주소 변경", - "email": "메일", + "changeEmail": "이메일 주소 변경", + "email": "이메일", "passwordReset": "비밀번호 초기화", "forgotPassword": "비밀번호를 잊어버리셨나요?", - "error_weakPassword": "이 비밀번호는 매우 일반적이고 추측하기 쉽습니다.", - "error_namePassword": "사용자 아이디를 비밀번호로 사용하지 마세요.", - "blankedPassword": "다른 사이트에서 동일한 비밀번호를 사용했으며 해당 사이트가 유출된 경우. 라이선스 계정의 안전을 위해 새 비밀번호를 설정해 주셔야 합니다. 양해해 주셔서 감사합니다.", - "youAreLeavingLichess": "리체스에서 나갑니다", - "neverTypeYourPassword": "다른 사이트에서는 절대로 리체스 비밀번호를 입력하지 마세요!", - "proceedToX": "{param} 진행", + "error_weakPassword": "이 비밀번호는 매우 흔하며 추측하기 쉽습니다.", + "error_namePassword": "사용자 이름을 비밀번호로 사용하지 마세요.", + "blankedPassword": "당신이 다른 사이트에서 동일한 비밀번호를 사용했으며, 해당 사이트가 유출되었습니다. Lichess 계정의 안전을 위해 새 비밀번호를 설정해 주세요. 양해해 주셔서 감사합니다.", + "youAreLeavingLichess": "Lichess에서 나갑니다", + "neverTypeYourPassword": "다른 사이트에서는 절대로 Lichess 비밀번호를 입력하지 마세요!", + "proceedToX": "{param}로 진행", "passwordSuggestion": "다른 사람이 제안한 비밀번호를 설정하지 마세요. 타인이 계정을 도용하는 데 사용할 수 있습니다.", "emailSuggestion": "다른 사람이 추천한 이메일 주소를 설정하지 마세요. 타인이 계정을 도용하는 데 사용할 수 있습니다.", "emailConfirmHelp": "이메일 확인 도움말", @@ -570,77 +647,77 @@ "whatSignupUsername": "가입할 때 어떤 사용자 이름을 사용하셨나요?", "usernameNotFound": "사용자 이름을 찾을 수 없습니다: {param}.", "usernameCanBeUsedForNewAccount": "이 사용자 이름을 사용하여 새 계정을 만들 수 있습니다", - "emailSent": "{param}로 이메일을 전송했습니다.", - "emailCanTakeSomeTime": "도착하는데 시간이 걸릴 수 있습니다.", - "refreshInboxAfterFiveMinutes": "5분 가량 기다린 후 이메일 수신함을 새로고침하세요.", - "checkSpamFolder": "또한 스펨메일함을 확인해주시고 스펨을 해제해주세요.", - "emailForSignupHelp": "모두 실패했다면 이곳으로 메일을 보내주세요:", - "copyTextToEmail": "위의 텍스트를 복사해서 {param}로 보내주세요.", + "emailSent": "{param}(으)로 이메일을 전송했습니다.", + "emailCanTakeSomeTime": "이메일이 도착하는데 시간이 좀 걸릴 수 있습니다.", + "refreshInboxAfterFiveMinutes": "5분 가량 기다린 후 이메일 수신함을 새로고침 해주세요.", + "checkSpamFolder": "또한 스팸 메일함에 들어가 있을 수 있습니다. 만약 그런 경우, 스팸이 아님으로 표시해 두세요.", + "emailForSignupHelp": "모두 실패했다면, 이곳으로 메일을 보내주세요:", + "copyTextToEmail": "위의 텍스트를 복사해서 {param}(으)로 보내주세요", "waitForSignupHelp": "가입을 완료할 수 있도록 빠르게 연락드리겠습니다.", - "accountConfirmed": "{param} 사용자가 성공적으로 확인되었습니다.", - "accountCanLogin": "이제 {param}로 로그인할 수 있습니다.", + "accountConfirmed": "유저 {param}(이)가 성공적으로 확인되었습니다.", + "accountCanLogin": "이제 {param}(으)로 로그인할 수 있습니다.", "accountConfirmationEmailNotNeeded": "이메일 확인은 필요하지 않습니다.", - "accountClosed": "{param} 계정은 폐쇄되었습니다.", + "accountClosed": "계정 {param}(은)는 폐쇄되었습니다.", "accountRegisteredWithoutEmail": "{param} 계정은 이메일 주소가 없이 등록되었습니다.", "rank": "순위", "rankX": "순위: {param}등", "gamesPlayed": "게임", "cancel": "취소", - "whiteTimeOut": "백색 시간 초과", - "blackTimeOut": "흑색 시간 초과", - "drawOfferSent": "무승부를 요청했습니다", - "drawOfferAccepted": "무승부 요청이 승낙됐습니다", - "drawOfferCanceled": "무승부 요청을 취소했습니다", - "whiteOffersDraw": "백이 무승부를 제안했습니다", - "blackOffersDraw": "흑이 무승부를 제안했습니다", - "whiteDeclinesDraw": "백이 무승부 제안을 거절했습니다", - "blackDeclinesDraw": "흑이 무승부 제안을 거절했습니다", - "yourOpponentOffersADraw": "상대가 무승부를 요청했습니다", - "accept": "승낙", + "whiteTimeOut": "백 시간 초과", + "blackTimeOut": "흑 시간 초과", + "drawOfferSent": "무승부 요청함", + "drawOfferAccepted": "무승부 요청 수락됨", + "drawOfferCanceled": "무승부 요청 취소함", + "whiteOffersDraw": "백이 무승부를 제안합니다", + "blackOffersDraw": "흑이 무승부를 제안합니다", + "whiteDeclinesDraw": "백이 무승부 제안을 거절하였습니다", + "blackDeclinesDraw": "흑이 무승부 제안을 거절하였습니다", + "yourOpponentOffersADraw": "상대가 무승부를 요청합니다", + "accept": "수락", "decline": "거절", - "playingRightNow": "대국 중", + "playingRightNow": "지금 대국 중", "eventInProgress": "지금 대국 중", - "finished": "종료", + "finished": "종료됨", "abortGame": "게임 중단", "gameAborted": "게임 중단됨", - "standard": "표준", - "customPosition": "사용자 지정 포지션", + "standard": "스탠다드", + "customPosition": "커스텀 포지션", "unlimited": "무제한", "mode": "모드", "casual": "캐주얼", "rated": "레이팅", - "casualTournament": "일반", + "casualTournament": "캐주얼", "ratedTournament": "레이팅", "thisGameIsRated": "이 게임은 레이팅 게임입니다", "rematch": "재대결", - "rematchOfferSent": "재대결 요청을 보냈습니다", - "rematchOfferAccepted": "재대결 요청이 승낙됐습니다", - "rematchOfferCanceled": "재대결 요청이 취소됐습니다", + "rematchOfferSent": "재대결 요청 전송됨", + "rematchOfferAccepted": "재대결 요청 승낙됨", + "rematchOfferCanceled": "재대결 요청 취소됨", "rematchOfferDeclined": "재대결 요청이 거절됐습니다", "cancelRematchOffer": "재대결 요청 취소", - "viewRematch": "재대결 보러 가기", + "viewRematch": "재대결 보기", "confirmMove": "수 확인", "play": "플레이", - "inbox": "받은편지함", + "inbox": "편지함", "chatRoom": "채팅", - "loginToChat": "채팅에 로그인하기", - "youHaveBeenTimedOut": "채팅에서 로그아웃 되었습니다.", + "loginToChat": "채팅하려면 로그인하세요", + "youHaveBeenTimedOut": "채팅에서 타임아웃 되었습니다.", "spectatorRoom": "관전자 채팅", "composeMessage": "메시지 작성", "subject": "제목", "send": "전송", - "incrementInSeconds": "턴 당 추가 시간(초)", + "incrementInSeconds": "수 당 추가 시간(초)", "freeOnlineChess": "무료 온라인 체스", "exportGames": "게임 내보내기", - "ratingRange": "ELO 범위", - "thisAccountViolatedTos": "이 계정은 Lichess 이용 약관을 위반했습니다.", + "ratingRange": "레이팅 범위", + "thisAccountViolatedTos": "이 계정은 Lichess 이용 약관을 위반하였습니다", "openingExplorerAndTablebase": "오프닝 탐색 & 테이블베이스", "takeback": "무르기", - "proposeATakeback": "무르기를 요청합니다", - "takebackPropositionSent": "무르기 요청을 보냈습니다", - "takebackPropositionDeclined": "무르기 요청이 거절됐습니다", - "takebackPropositionAccepted": "무르기 요청이 승낙됐습니다", - "takebackPropositionCanceled": "무르기 요청이 취소됐습니다", + "proposeATakeback": "무르기 요청", + "takebackPropositionSent": "무르기 요청 전송됨", + "takebackPropositionDeclined": "무르기 요청 거절됨", + "takebackPropositionAccepted": "무르기 요청 승낙됨", + "takebackPropositionCanceled": "무르기 요청 취소됨", "yourOpponentProposesATakeback": "상대가 무르기를 요청합니다", "bookmarkThisGame": "이 게임을 즐겨찾기에 추가하기", "tournament": "토너먼트", @@ -665,10 +742,10 @@ "startedStreaming": "스트리밍 시작", "xStartedStreaming": "{param} 님이 스트리밍을 시작했습니다", "averageElo": "평균 레이팅", - "location": "주소", - "filterGames": "필터", + "location": "위치", + "filterGames": "대국 필터", "reset": "초기화", - "apply": "적용", + "apply": "저장", "save": "저장하기", "leaderboard": "리더보드", "screenshotCurrentPosition": "스크린샷 찍기", @@ -690,7 +767,7 @@ "whiteCheckmatesInOneMove": "백이 한 수 만에 체크메이트하기", "blackCheckmatesInOneMove": "흑이 한 수 만에 체크메이트하기", "retry": "재시도", - "reconnecting": "연결 재시도 중", + "reconnecting": "연결 중", "noNetwork": "오프라인", "favoriteOpponents": "관심있는 상대", "follow": "팔로우", @@ -735,7 +812,7 @@ "resume": "재개", "youArePlaying": "참가중!", "winRate": "승률", - "berserkRate": "버서크율", + "berserkRate": "버서크 비율", "performance": "퍼포먼스 레이팅", "tournamentComplete": "대회 종료", "movesPlayed": "말 이동 횟수", @@ -757,17 +834,20 @@ "reportXToModerators": "{param} 신고", "profileCompletion": "프로필 완성도: {param}", "xRating": "레이팅: {param}", - "ifNoneLeaveEmpty": "없으면 무시하세요", + "ifNoneLeaveEmpty": "없다면 비워두세요", "profile": "프로필", "editProfile": "프로필 수정", - "realName": "본명", + "realName": "실명", + "setFlair": "아이콘을 선택하세요", + "flair": "아이콘", + "youCanHideFlair": "전체 사이트에서 모든 유저의 아이콘을 숨기는 설정이 있습니다.", "biography": "소개", - "countryRegion": "국가/지역", + "countryRegion": "국가 또는 지역", "thankYou": "감사합니다!", "socialMediaLinks": "소셜 미디어 링크", - "oneUrlPerLine": "한 줄에 1개 URL", - "inlineNotation": "기보를 가로쓰기", - "makeAStudy": "안전하게 보관하고 공유하려면 스터디를 만들어 보세요.", + "oneUrlPerLine": "한 줄에 당 URL 1개", + "inlineNotation": "기보법 가로쓰기", + "makeAStudy": "안전하게 보관하고 공유하려면 공부를 만들어 보세요.", "clearSavedMoves": "저장된 움직임 삭제", "previouslyOnLichessTV": "이전 방송", "onlinePlayers": "접속한 플레이어", @@ -777,15 +857,18 @@ "automaticallyProceedToNextGameAfterMoving": "수를 둔 다음에 자동으로 다음 게임에 이동", "autoSwitch": "자동 전환", "puzzles": "퍼즐", + "onlineBots": "온라인 봇", "name": "이름", "description": "설명", "descPrivate": "비공개 설명", "descPrivateHelp": "팀 멤버만 볼 수 있는 텍스트입니다. 설정된다면, 팀 멤버에게는 공개 설명 대신 보이게 됩니다.", "no": "아니오", "yes": "예", + "website": "웹사이트", + "mobile": "모바일", "help": "힌트:", - "createANewTopic": "새 토픽", - "topics": "토픽", + "createANewTopic": "새 주제 만들기", + "topics": "주제", "posts": "글", "lastPost": "최근 글", "views": "조회", @@ -793,7 +876,7 @@ "replyToThisTopic": "답글 달기", "reply": "전송", "message": "내용", - "createTheTopic": "새 토픽 생성", + "createTheTopic": "새 주제 생성", "reportAUser": "사용자 신고", "user": "신고할 사용자 이름", "reason": "이유", @@ -801,11 +884,13 @@ "cheat": "부정행위", "troll": "분란 조장", "other": "기타", - "reportDescriptionHelp": "게임 URL 주소를 붙여넣으시고 해당 사용자가 무엇을 잘못했는지 설명해 주세요.", + "reportCheatBoostHelp": "게임 URL 주소를 붙여넣으시고 해당 사용자가 무엇을 잘못했는지 설명해 주세요. 그냥 \"그들이 부정행위를 했어요\" 라고만 말하지 말고, 어떻게 당신이 이 결론에 도달하게 됐는지 알려주세요.", + "reportUsernameHelp": "왜 이 사용자의 이름이 불쾌한지 설명해주세요. 그저 \"불쾌해요/부적절해요\"라고만 말하지 마세요, 대신 왜 이런 결론에 도달했는지 말씀해 주세요. 단어가 난해하거나, 영어가 아니거나, 은어이거나, 문화적/역사적 배경이 있는 경우 특히 중요합니다.", + "reportProcessedFasterInEnglish": "귀하의 신고가 영어로 적혀있을 경우 빠르게 처리될 것입니다.", "error_provideOneCheatedGameLink": "부정행위가 존재하는 게임의 링크를 적어도 하나는 적어주세요.", "by": "작성: {param}", "importedByX": "{param}가 불러옴", - "thisTopicIsNowClosed": "이 토픽은 닫혔습니다.", + "thisTopicIsNowClosed": "이 주제는 닫혔습니다.", "blog": "블로그", "notes": "노트", "typePrivateNotesHere": "여기에 비공개 메모 작성하기", @@ -821,7 +906,7 @@ "newPasswordsDontMatch": "새로운 비밀번호가 일치하지 않습니다", "newPasswordStrength": "비밀번호 강도", "clockInitialTime": "기본 시간", - "clockIncrement": "한 수당 증가하는 시간", + "clockIncrement": "수 당 추가 시간", "privacy": "보안", "privacyPolicy": "개인정보취급방침", "letOtherPlayersFollowYou": "다른 사람이 팔로우할 수 있게 함", @@ -834,6 +919,7 @@ "slow": "느리게", "insideTheBoard": "보드 안쪽에", "outsideTheBoard": "보드 바깥쪽에", + "allSquaresOfTheBoard": "보드의 모든 칸", "onSlowGames": "느린 게임에서만", "always": "항상", "never": "안 함", @@ -849,20 +935,20 @@ "biographyDescription": "자신을 알려주세요. 왜 체스를 좋아하는지, 좋아하는 오프닝, 게임, 선수 등등...", "listBlockedPlayers": "이 플레이어를 차단", "human": "인간", - "computer": "인공지능", - "side": "진영", + "computer": "컴퓨터", + "side": "색", "clock": "시계", "opponent": "상대", "learnMenu": "배우기", - "studyMenu": "연구", + "studyMenu": "공부", "practice": "연습", "community": "커뮤니티", "tools": "도구", - "increment": "시간 증가", + "increment": "추가 시간", "error_unknown": "잘못된 값", "error_required": "필수 기입 사항입니다.", "error_email": "이메일 주소가 유효하지 않습니다", - "error_email_acceptable": "이 이메일 주소는 받을 수 없습니다. 다시 확인후 시도해주세요.", + "error_email_acceptable": "이 이메일 주소는 수용 불가합니다. 확인후 다시 시도해주세요.", "error_email_unique": "이메일 주소가 유효하지 않거나 이미 등록되었습니다", "error_email_different": "이미 당신의 이메일 주소입니다.", "error_minLength": "최소 {param}자여야 합니다.", @@ -892,21 +978,21 @@ "contribute": "기여하기", "termsOfService": "이용 약관", "sourceCode": "소스 코드", - "simultaneousExhibitions": "동시대국", - "host": "호스트", - "hostColorX": "호스트의 색: {param}", - "yourPendingSimuls": "대기 중인 동시대국", - "createdSimuls": "새롭게 생성된 동시대국", - "hostANewSimul": "새 동시대국을 생성하기", - "signUpToHostOrJoinASimul": "동시대국을 생성/참가하려면 로그인하세요", - "noSimulFound": "동시대국을 찾을 수 없습니다", - "noSimulExplanation": "존재하지 않는 동시대국입니다.", - "returnToSimulHomepage": "동시대국 홈으로 돌아가기", - "aboutSimul": "동시대국에서는 1인의 플레이어가 여러 플레이어와 대국을 벌입니다.", - "aboutSimulImage": "50명의 상대 중, 피셔는 47국을 승리하였고, 2국은 무승부였으며 1국만을 패배하였습니다.", - "aboutSimulRealLife": "이 동시대국의 개념은 실제 동시대국과 동일합니다. 실제로 1인 플레이어는 테이블을 넘기며 한 수씩 둡니다.", - "aboutSimulRules": "동시대국이 시작되면 모든 플레이어가 호스트와 게임을 합니다. 동시대국은 모든 플레이어와 게임이 끝나면 종료됩니다.", - "aboutSimulSettings": "동시대국은 캐주얼 전입니다. 재대결, 무르기, 시간추가를 할 수 없습니다.", + "simultaneousExhibitions": "다면기", + "host": "주최자", + "hostColorX": "주최자 색: {param}", + "yourPendingSimuls": "대기 중인 다면기", + "createdSimuls": "새롭게 생성된 다면기", + "hostANewSimul": "새 다면기 주최하기", + "signUpToHostOrJoinASimul": "다면기를 주최/참가하려면 로그인하세요", + "noSimulFound": "다면기를 찾을 수 없습니다", + "noSimulExplanation": "존재하지 않는 다면기입니다.", + "returnToSimulHomepage": "다면기 홈으로 돌아가기", + "aboutSimul": "다면기에서는 1인의 플레이어가 여러 플레이어와 대국을 벌입니다.", + "aboutSimulImage": "피셔는 50명의 상대 중, 47국을 승리하였고, 2국은 무승부였으며 1국만 패하였습니다.", + "aboutSimulRealLife": "이 컨셉은 실제 이벤트들을 본딴 것입니다. 실제 경기에서는 다면기 주최자가 테이블을 돌아다니며 한 수씩 둡니다.", + "aboutSimulRules": "다면기가 시작되면, 모든 플레이어가 주최자와 대국을 합니다. 다면기는 모든 플레이어와 게임이 끝나면 종료됩니다.", + "aboutSimulSettings": "다면기는 항상 캐주얼전입니다. 재대결, 무르기, 시간추가를 할 수 없습니다.", "create": "생성", "whenCreateSimul": "동시대국을 생성하면 한 번에 여러 명의 플레이어와 게임하게 됩니다.", "simulVariantsHint": "복수의 게임방식을 선택할 경우, 상대방 측에서 게임 방식을 선택하게 됩니다.", @@ -923,6 +1009,7 @@ "keyboardShortcuts": "키보드 단축키", "keyMoveBackwardOrForward": "뒤로/앞으로 가기", "keyGoToStartOrEnd": "처음/끝으로 가기", + "keyCycleSelectedVariation": "선택된 바리에이션 순환하기", "keyShowOrHideComments": "댓글 표시/숨기기", "keyEnterOrExitVariation": "바리에이션 들어가기/나오기", "keyRequestComputerAnalysis": "컴퓨터 분석 요청, 실수에서 배우기", @@ -930,6 +1017,12 @@ "keyNextBlunder": "다음 블런더", "keyNextMistake": "다음 실수", "keyNextInaccuracy": "다음 부정확한 수", + "keyPreviousBranch": "이전 부", + "keyNextBranch": "다음 부", + "toggleVariationArrows": "바리에이션 화살표 표시하기", + "cyclePreviousOrNextVariation": "이전/다음 바리에이션 순환하기", + "toggleGlyphAnnotations": "이동 주석 토글하기", + "togglePositionAnnotations": "위치 주석 토글하기", "variationArrowsInfo": "변형 화살표를 사용하면 이동 목록을 사용하지 않고 탐색이 가능합니다.", "playSelectedMove": "선택한 수 두기", "newTournament": "새로운 토너먼트", @@ -980,16 +1073,16 @@ "sessions": "세션", "revokeAllSessions": "모든 세션 비활성화", "playChessEverywhere": "어디에서나 체스를 즐기세요", - "asFreeAsLichess": "lichess처럼 무료입니다", + "asFreeAsLichess": "Lichess처럼 무료예요", "builtForTheLoveOfChessNotMoney": "오직 체스에 대한 열정으로 만들어졌습니다", "everybodyGetsAllFeaturesForFree": "모두가 모든 기능을 무료로 이용할 수 있습니다", "zeroAdvertisement": "광고가 없습니다", "fullFeatured": "모든 기능을 지원합니다", "phoneAndTablet": "스마트폰과 태블릿 지원", "bulletBlitzClassical": "불릿, 블리츠, 클래식 방식 지원", - "correspondenceChess": "우편 체스 지원", + "correspondenceChess": "긴 대국 체스", "onlineAndOfflinePlay": "온라인/오프라인 게임 모두 지원", - "viewTheSolution": "해답 보기", + "viewTheSolution": "정답 보기", "followAndChallengeFriends": "친구를 팔로우하고 도전하기", "gameAnalysis": "게임 분석기", "xHostsY": "{param2}가 {param1}를 시작했습니다.", @@ -1020,7 +1113,7 @@ "usernameCharsInvalid": "유저 이름에는 알파벳, 숫자, 언더스코어 ( _ ), 하이픈 ( - ) 만을 사용할 수 있습니다.", "usernameUnacceptable": "사용자 이름을 사용할 수 없습니다.", "playChessInStyle": "스타일리시하게 체스하기", - "chessBasics": "체스 기본", + "chessBasics": "체스의 기본", "coaches": "코치", "invalidPgn": "잘못된 PGN입니다.", "invalidFen": "잘못된 FEN입니다.", @@ -1051,18 +1144,18 @@ "findBetterMoveForBlack": "검은색에게 좀 더 나은 수를 찾아보세요", "resumeLearning": "학습 계속하기", "youCanDoBetter": "더 잘할 수 있어요", - "tryAnotherMoveForWhite": "흰색에게 또 다른 수를 찾아보세요", - "tryAnotherMoveForBlack": "검은색에게 또 다른 수를 찾아보세요", + "tryAnotherMoveForWhite": "백의 또 다른 수를 찾아보세요", + "tryAnotherMoveForBlack": "흑의 또 다른 수를 찾아보세요", "solution": "해답", "waitingForAnalysis": "분석을 기다리는 중", "noMistakesFoundForWhite": "백에게 악수는 없었습니다", - "noMistakesFoundForBlack": "흑에게 악수는 없었습니다", + "noMistakesFoundForBlack": "흑에게 실수는 없었습니다", "doneReviewingWhiteMistakes": "백의 악수 체크가 종료됨", - "doneReviewingBlackMistakes": "흑의 악수 체크가 종료됨", + "doneReviewingBlackMistakes": "흑의 실수 탐색이 종료됨", "doItAgain": "다시 하기", "reviewWhiteMistakes": "백의 악수를 체크", - "reviewBlackMistakes": "흑의 악수를 체크", - "advantage": "이득", + "reviewBlackMistakes": "흑의 실수 리뷰", + "advantage": "이점", "opening": "오프닝", "middlegame": "미들게임", "endgame": "엔드게임", @@ -1132,16 +1225,16 @@ "resVsX": "{param1} vs {param2}", "lostAgainstTOSViolator": "당신은 Lichess의 서비스 약관을 어긴 플레이어에게 패배했습니다.", "refundXpointsTimeControlY": "환불: {param2} 레이팅 포인트 {param1}점", - "timeAlmostUp": "시간이 거의 다 되었습니다!", + "timeAlmostUp": "시간이 거의 다 되었어요!", "clickToRevealEmailAddress": "[이메일 주소를 보려면 클릭]", "download": "다운로드", "coachManager": "코치 설정", "streamerManager": "스트리머 설정", - "cancelTournament": "토너먼트 취소", + "cancelTournament": "토너먼트 취소하기", "tournDescription": "토너먼트 설명", "tournDescriptionHelp": "참가자에게 하고 싶은 말이 있나요? 짧게 작성해주세요. 마크다운 링크가 가능합니다: [name](https://url)", "ratedFormHelp": "레이팅 게임을 합니다\n플레이어 레이팅에 영향을 줍니다", - "onlyMembersOfTeam": "팀 멤버만", + "onlyMembersOfTeam": "팀 멤버들만", "noRestriction": "제한 없음", "minimumRatedGames": "최소 레이팅 게임 참여 횟수", "minimumRating": "최소 레이팅", @@ -1161,7 +1254,7 @@ "noChat": "채팅 없음", "onlyTeamLeaders": "팀 리더만", "onlyTeamMembers": "팀 멤버만", - "navigateMoveTree": "수 탐색", + "navigateMoveTree": "수의 나무 탐색", "mouseTricks": "마우스 기능", "toggleLocalAnalysis": "로컬 컴퓨터 분석 켜기/끄기", "toggleAllAnalysis": "모든 컴퓨터 분석 켜기/끄기", @@ -1169,15 +1262,15 @@ "analysisOptions": "분석 옵션", "focusChat": "채팅에 포커스 주기", "showHelpDialog": "이 도움말 보기", - "reopenYourAccount": "계정 다시 활성화", + "reopenYourAccount": "계정 재활성화", "closedAccountChangedMind": "계정을 폐쇄한 후 마음이 바뀌었다면, 계정을 다시 활성화할 수 있는 기회가 한 번 있습니다.", "onlyWorksOnce": "단 한번만 가능합니다.", "cantDoThisTwice": "계정을 두 번째로 폐쇄했다면 복구할 방법이 없습니다.", "emailAssociatedToaccount": "계정에 등록된 이메일 주소", "sentEmailWithLink": "링크가 포함된 이메일을 보냈습니다.", "tournamentEntryCode": "토너먼트 입장 코드", - "hangOn": "잠깐!", - "gameInProgress": "{param}와 진행중인 게임이 있습니다.", + "hangOn": "잠깐만요!", + "gameInProgress": "{param}와(과) 진행중인 대국이 있습니다.", "abortTheGame": "게임 중단", "resignTheGame": "게임 기권", "youCantStartNewGame": "이 게임이 끝나기 전까지 새 게임을 시작할 수 없습니다.", @@ -1191,30 +1284,31 @@ "showMeEverything": "모두 보기", "lichessPatronInfo": "Lichess는 비영리 기구이며 완전한 무료/자유 오픈소스 소프트웨어입니다.\n모든 운영 비용, 개발, 컨텐츠 조달은 전적으로 사용자들의 기부로 이루어집니다.", "nothingToSeeHere": "지금은 여기에 볼 것이 없습니다.", - "opponentLeftCounter": "{count, plural, other{당신의 상대가 게임을 나갔습니다. {count} 초 후에 승리를 주장할 수 있습니다.}}", - "mateInXHalfMoves": "{count, plural, other{{count}반수만에 체크메이트}}", + "stats": "통계", + "opponentLeftCounter": "{count, plural, other{상대방이 게임을 나갔습니다. {count}초 후에 승리를 취할 수 있습니다.}}", + "mateInXHalfMoves": "{count, plural, other{{count}개의 반수 후 체크메이트}}", "nbBlunders": "{count, plural, other{{count} 블런더}}", "nbMistakes": "{count, plural, other{{count} 실수}}", - "nbInaccuracies": "{count, plural, other{{count} 사소한 실수}}", - "nbPlayers": "{count, plural, other{{count}명의 플레이어}}", - "nbGames": "{count, plural, other{{count}개의 게임}}", + "nbInaccuracies": "{count, plural, other{{count} 부정확한 수}}", + "nbPlayers": "{count, plural, other{플레이어 {count}명}}", + "nbGames": "{count, plural, other{게임 {count}개}}", "ratingXOverYGames": "{count, plural, other{{param2}게임간의 {count} 레이팅}}", "nbBookmarks": "{count, plural, other{{count}개의 즐겨찾기}}", "nbDays": "{count, plural, other{{count}일}}", "nbHours": "{count, plural, other{{count}시간}}", "nbMinutes": "{count, plural, other{{count}분}}", - "rankIsUpdatedEveryNbMinutes": "{count, plural, other{순위는 매 {count}분마다 갱신됩니다.}}", + "rankIsUpdatedEveryNbMinutes": "{count, plural, other{순위는 매 {count}분마다 갱신됩니다}}", "nbPuzzles": "{count, plural, other{퍼즐 {count}개}}", "nbGamesWithYou": "{count, plural, other{나와 {count}번 대국 함}}", - "nbRated": "{count, plural, other{{count}번의 레이팅 대국}}", + "nbRated": "{count, plural, other{{count}번의 레이팅 게임}}", "nbWins": "{count, plural, other{{count}번 승리}}", "nbLosses": "{count, plural, other{{count}번 패배}}", "nbDraws": "{count, plural, other{{count}번 비김}}", "nbPlaying": "{count, plural, other{플레이 중인 게임 {count}개}}", "giveNbSeconds": "{count, plural, other{{count}초 더 주기}}", - "nbTournamentPoints": "{count, plural, other{{count} 토너먼트 포인트}}", - "nbStudies": "{count, plural, other{{count} 연구}}", - "nbSimuls": "{count, plural, other{{count} 동시대국}}", + "nbTournamentPoints": "{count, plural, other{{count} 토너먼트 점수}}", + "nbStudies": "{count, plural, other{{count} 공부}}", + "nbSimuls": "{count, plural, other{{count} 다면기}}", "moreThanNbRatedGames": "{count, plural, other{레이팅전 {count} 국 이상}}", "moreThanNbPerfRatedGames": "{count, plural, other{{param2} 레이팅전 {count} 국 이상}}", "needNbMorePerfGames": "{count, plural, other{{param2} 랭크 게임을 {count}회 더 플레이해야합니다.}}", @@ -1229,7 +1323,7 @@ "blocks": "{count, plural, other{차단한 사람 {count}명}}", "nbForumPosts": "{count, plural, other{{count} 포럼 글}}", "nbPerfTypePlayersThisWeek": "{count, plural, other{이번 주의 {param2} 플레이어는 {count}명입니다.}}", - "availableInNbLanguages": "{count, plural, other{{count}개의 언어 지원!}}", + "availableInNbLanguages": "{count, plural, other{{count}개의 언어를 지원합니다!}}", "nbSecondsToPlayTheFirstMove": "{count, plural, other{{count} 초 안에 첫 수를 두십시오.}}", "nbSeconds": "{count, plural, other{{count}초}}", "andSaveNbPremoveLines": "{count, plural, other{{count} 종류의 조건 수를 설정}}", @@ -1287,6 +1381,159 @@ "stormXRuns": "{count, plural, other{{count}번 도전}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, other{{param2} 중{count}개 플레이함}}", "streamerLichessStreamers": "Lichess 스트리머", + "studyPrivate": "비공개", + "studyMyStudies": "내 공부", + "studyStudiesIContributeTo": "내가 기여한 공부", + "studyMyPublicStudies": "내 공개 공부", + "studyMyPrivateStudies": "내 개인 공부", + "studyMyFavoriteStudies": "내가 즐겨찾는 공부", + "studyWhatAreStudies": "공부가 무엇인가요?", + "studyAllStudies": "모든 공부", + "studyStudiesCreatedByX": "{param}이(가) 만든 공부", + "studyNoneYet": "아직 없음", + "studyHot": "인기있는", + "studyDateAddedNewest": "추가된 날짜(새로운 순)", + "studyDateAddedOldest": "추가된 날짜(오래된 순)", + "studyRecentlyUpdated": "최근에 업데이트된 순", + "studyMostPopular": "인기 많은 순", + "studyAlphabetical": "알파벳 순", + "studyAddNewChapter": "새 챕터 추가하기", + "studyAddMembers": "멤버 추가", + "studyInviteToTheStudy": "공부에 초대", + "studyPleaseOnlyInvitePeopleYouKnow": "당신이 아는 사람들이나 공부에 적극적으로 참여하고 싶은 사람들만 초대하세요.", + "studySearchByUsername": "사용자 이름으로 검색", + "studySpectator": "관전자", + "studyContributor": "기여자", + "studyKick": "강제 퇴장", + "studyLeaveTheStudy": "공부 나가기", + "studyYouAreNowAContributor": "당신은 이제 기여자입니다", + "studyYouAreNowASpectator": "당신은 이제 관전자입니다", + "studyPgnTags": "PGN 태그", + "studyLike": "좋아요", + "studyUnlike": "좋아요 취소", + "studyNewTag": "새 태그", + "studyCommentThisPosition": "이 포지션에 댓글 달기", + "studyCommentThisMove": "이 수에 댓글 달기", + "studyAnnotateWithGlyphs": "기호로 주석 달기", + "studyTheChapterIsTooShortToBeAnalysed": "분석되기 너무 짧은 챕터입니다.", + "studyOnlyContributorsCanRequestAnalysis": "공부 기여자들만이 컴퓨터 분석을 요청할 수 있습니다.", + "studyGetAFullComputerAnalysis": "메인라인에 대한 전체 서버 컴퓨터 분석을 가져옵니다.", + "studyMakeSureTheChapterIsComplete": "챕터가 완료되었는지 확인하세요. 분석은 한번만 요청할 수 있습니다.", + "studyAllSyncMembersRemainOnTheSamePosition": "동기화된 모든 멤버들은 같은 포지션을 공유합니다", + "studyShareChanges": "관전자와 변경 사항을 공유하고 서버에 저장", + "studyPlaying": "대국 중", + "studyShowEvalBar": "평가 막대", + "studyFirst": "처음", + "studyPrevious": "이전", + "studyNext": "다음", + "studyLast": "마지막", "studyShareAndExport": "공유 및 내보내기", - "studyStart": "시작" + "studyCloneStudy": "복제", + "studyStudyPgn": "공부 PGN", + "studyDownloadAllGames": "모든 게임 다운로드", + "studyChapterPgn": "챕터 PGN", + "studyCopyChapterPgn": "PGN 복사", + "studyDownloadGame": "게임 다운로드", + "studyStudyUrl": "공부 URL", + "studyCurrentChapterUrl": "현재 챕터 URL", + "studyYouCanPasteThisInTheForumToEmbed": "포럼에 공유하려면 이 주소를 붙여넣으세요", + "studyStartAtInitialPosition": "처음 포지션에서 시작", + "studyStartAtX": "{param}에서 시작", + "studyEmbedInYourWebsite": "웹사이트 또는 블로그에 공유하기", + "studyReadMoreAboutEmbedding": "공유에 대한 상세 정보", + "studyOnlyPublicStudiesCanBeEmbedded": "공개 공부들만 공유할 수 있습니다!", + "studyOpen": "열기", + "studyXBroughtToYouByY": "{param1}. {param2}에서 가져옴", + "studyStudyNotFound": "공부를 찾을 수 없습니다", + "studyEditChapter": "챕터 편집하기", + "studyNewChapter": "새 챕터", + "studyImportFromChapterX": "{param}에서 가져오기", + "studyOrientation": "방향", + "studyAnalysisMode": "분석 모드", + "studyPinnedChapterComment": "챕터 댓글 고정하기", + "studySaveChapter": "챕터 저장", + "studyClearAnnotations": "주석 지우기", + "studyClearVariations": "파생 초기화", + "studyDeleteChapter": "챕터 지우기", + "studyDeleteThisChapter": "이 챕터를 지울까요? 되돌릴 수 없습니다!", + "studyClearAllCommentsInThisChapter": "이 챕터의 모든 코멘트와 기호를 지울까요?", + "studyRightUnderTheBoard": "보드 우하단에", + "studyNoPinnedComment": "없음", + "studyNormalAnalysis": "일반 분석", + "studyHideNextMoves": "다음 수 숨기기", + "studyInteractiveLesson": "상호 대화형 레슨", + "studyChapterX": "챕터 {param}", + "studyEmpty": "비어있음", + "studyStartFromInitialPosition": "초기 포지션에서 시작", + "studyEditor": "편집기", + "studyStartFromCustomPosition": "커스텀 포지션에서 시작", + "studyLoadAGameByUrl": "URL로 게임 가져오기", + "studyLoadAPositionFromFen": "FEN으로 포지션 가져오기", + "studyLoadAGameFromPgn": "PGN으로 게임 가져오기", + "studyAutomatic": "자동", + "studyUrlOfTheGame": "한 줄에 하나씩, 게임의 URL", + "studyLoadAGameFromXOrY": "{param1} 또는 {param2}에서 게임 로드", + "studyCreateChapter": "챕터 만들기", + "studyCreateStudy": "공부 만들기", + "studyEditStudy": "공부 편집하기", + "studyVisibility": "공개 설정", + "studyPublic": "공개", + "studyUnlisted": "비공개", + "studyInviteOnly": "초대만", + "studyAllowCloning": "복제 허용", + "studyNobody": "아무도", + "studyOnlyMe": "나만", + "studyContributors": "기여자만", + "studyMembers": "멤버만", + "studyEveryone": "모두", + "studyEnableSync": "동기화 사용", + "studyYesKeepEveryoneOnTheSamePosition": "예: 모두가 같은 위치를 봅니다", + "studyNoLetPeopleBrowseFreely": "아니요: 사람들이 자유롭게 이동할 수 있습니다", + "studyPinnedStudyComment": "고정된 댓글", + "studyStart": "시작", + "studySave": "저장", + "studyClearChat": "채팅 기록 지우기", + "studyDeleteTheStudyChatHistory": "공부 채팅 히스토리를 지울까요? 되돌릴 수 없습니다!", + "studyDeleteStudy": "공부 삭제", + "studyConfirmDeleteStudy": "모든 공부를 삭제할까요? 복구할 수 없습니다! 확인을 위해서 공부의 이름을 입력하세요: {param}", + "studyWhereDoYouWantToStudyThat": "어디에서 공부하시겠습니까?", + "studyGoodMove": "좋은 수", + "studyMistake": "실수", + "studyBrilliantMove": "매우 좋은 수", + "studyBlunder": "블런더", + "studyInterestingMove": "흥미로운 수", + "studyDubiousMove": "애매한 수", + "studyOnlyMove": "유일한 수", + "studyZugzwang": "추크추방", + "studyEqualPosition": "동등한 포지션", + "studyUnclearPosition": "불확실한 포지션", + "studyWhiteIsSlightlyBetter": "백이 미세하게 좋음", + "studyBlackIsSlightlyBetter": "흑이 미세하게 좋음", + "studyWhiteIsBetter": "백이 유리함", + "studyBlackIsBetter": "흑이 유리함", + "studyWhiteIsWinning": "백이 이기고 있음", + "studyBlackIsWinning": "흑이 이기고 있음", + "studyNovelty": "새로운 수", + "studyDevelopment": "발전", + "studyInitiative": "주도권", + "studyAttack": "공격", + "studyCounterplay": "카운터플레이", + "studyTimeTrouble": "시간이 부족함", + "studyWithCompensation": "보상이 있음", + "studyWithTheIdea": "아이디어", + "studyNextChapter": "다음 챕터", + "studyPrevChapter": "이전 챕터", + "studyStudyActions": "공부 액션", + "studyTopics": "주제", + "studyMyTopics": "내 주제", + "studyPopularTopics": "인기 주제", + "studyManageTopics": "주제 관리", + "studyBack": "뒤로", + "studyPlayAgain": "다시 플레이", + "studyWhatWouldYouPlay": "이 포지션에서 무엇을 하시겠습니까?", + "studyYouCompletedThisLesson": "축하합니다! 이 레슨을 완료했습니다.", + "studyNbChapters": "{count, plural, other{{count} 챕터}}", + "studyNbGames": "{count, plural, other{{count} 게임}}", + "studyNbMembers": "{count, plural, other{멤버 {count}명}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, other{PGN을 여기에 붙여넣으세요. 최대 {count} 게임까지 가능합니다.}}" } \ No newline at end of file diff --git a/lib/l10n/lila_lb.arb b/lib/l10n/lila_lb.arb index b2cda00e3d..a3a90fdb0a 100644 --- a/lib/l10n/lila_lb.arb +++ b/lib/l10n/lila_lb.arb @@ -24,6 +24,7 @@ "mobilePuzzleStormSubtitle": "Léis sou vill Aufgabe wéi méiglech an 3 Minutten.", "mobileGreeting": "Moien, {param}", "mobileGreetingWithoutName": "Moien", + "mobilePrefMagnifyDraggedPiece": "Gezunne Figur vergréisseren", "activityActivity": "Verlaf", "activityHostedALiveStream": "Huet live gestreamt", "activityRankedInSwissTournament": "Huet sech als #{param1} an {param2} placéiert", @@ -36,6 +37,7 @@ "activityPlayedNbMoves": "{count, plural, =1{Huet {count} Zuch gespillt} other{Huet {count} Zich gespillt}}", "activityInNbCorrespondenceGames": "{count, plural, =1{an {count} Fernschachpartie} other{an {count} Fernschachpartien}}", "activityCompletedNbGames": "{count, plural, =1{Huet {count} Fernschachpartie gespillt} other{Huet {count} Fernschachpartien gespillt}}", + "activityCompletedNbVariantGames": "{count, plural, =1{Huet {count} {param2}-Fernschachpartie gespillt} other{Huet {count} {param2}-Fernschachpartië gespillt}}", "activityFollowedNbPlayers": "{count, plural, =1{Suivéiert {count} Spiller} other{Suivéiert {count} Spiller}}", "activityGainedNbFollowers": "{count, plural, =1{Huet {count} neien Unhänger} other{Huet {count} nei Unhänger}}", "activityHostedNbSimuls": "{count, plural, =1{Huet {count} Simultanvirstellung presentéiert} other{Huet {count} Simultanvirstellunge presentéiert}}", @@ -47,6 +49,48 @@ "activityJoinedNbTeams": "{count, plural, =1{Ass {count} Ekipp bäigetrueden} other{Ass {count} Ekippe bäigetrueden}}", "broadcastBroadcasts": "Iwwerdroungen", "broadcastLiveBroadcasts": "Live Turnéier Iwwerdroungen", + "broadcastNewBroadcast": "Nei Live Iwwerdroung", + "broadcastAddRound": "Ronn hinzufügen", + "broadcastOngoing": "Am Gaang", + "broadcastUpcoming": "Demnächst", + "broadcastCompleted": "Eriwwer", + "broadcastRoundName": "Ronnennumm", + "broadcastRoundNumber": "Ronnennummer", + "broadcastTournamentName": "Turnéiernumm", + "broadcastTournamentDescription": "Kuerz Turnéierbeschreiwung", + "broadcastFullDescription": "Komplett Turnéierbeschreiwung", + "broadcastFullDescriptionHelp": "Optional laang Beschreiwung vum Turnéier. {param1} ass disponibel. Längt muss manner wéi {param2} Buschtawen sinn.", + "broadcastSourceUrlHelp": "URL déi Lichess checkt fir PGN à jour ze halen. Muss ëffentlech iwwer Internet zougänglech sinn.", + "broadcastSourceGameIds": "Bis zu 64 Lichess-Partie-IDen, duerch Espacë getrennt.", + "broadcastStartDateTimeZone": "Startdatum vum Turnéier an der lokaler Zäitzon: {param}", + "broadcastStartDateHelp": "Optional, wann du wees wéini den Turnéier ufänkt", + "broadcastCurrentGameUrl": "URL vun der aktueller Partie", + "broadcastDownloadAllRounds": "All Ronnen eroflueden", + "broadcastResetRound": "Ronn zerécksetzen", + "broadcastDeleteRound": "Ronn läschen", + "broadcastDefinitivelyDeleteRound": "Dës Ronn an hir Partien endgülteg läschen.", + "broadcastDeleteAllGamesOfThisRound": "All Partien vun dëser Ronn läschen. D'Quell muss aktiv sinn fir se ze rekreéieren.", + "broadcastEditRoundStudy": "Ronnen-Etüd modifiéieren", + "broadcastDeleteTournament": "Dësen Turnéier läschen", + "broadcastDefinitivelyDeleteTournament": "De ganzen Turnéier definitiv läschen, all seng Ronnen an all seng Partien.", + "broadcastReplacePlayerTags": "Optional: Spillernimm, Wäertungen an Titelen ersetzen", + "broadcastFideFederations": "FIDE-Federatiounen", + "broadcastFidePlayers": "FIDE-Spiller", + "broadcastFidePlayerNotFound": "FIDE-Spiller net tfonnt", + "broadcastFideProfile": "FIDE-Profil", + "broadcastFederation": "Federatioun", + "broadcastAgeThisYear": "Alter dëst Joer", + "broadcastUnrated": "Ongewäert", + "broadcastRecentTournaments": "Rezent Turnéieren", + "broadcastTeams": "Ekippen", + "broadcastOverview": "Iwwersiicht", + "broadcastUploadImage": "Turnéierbild eroplueden", + "broadcastStartsAfter": "Fänkt no {param} un", + "broadcastOfficialWebsite": "Offiziell Websäit", + "broadcastIframeHelp": "Méi Optiounen op der {param}", + "broadcastWebmastersPage": "Webmaster-Säit", + "broadcastGamesThisTournament": "Partien an dësem Turnéier", + "broadcastNbBroadcasts": "{count, plural, =1{{count} Iwwerdroung} other{{count} Iwwerdroungen}}", "challengeChallengesX": "Erausfuerderungen: {param1}", "challengeChallengeToPlay": "Erausfuerderung zu enger Partie", "challengeChallengeDeclined": "Erausfuerderung ofgeleent", @@ -364,8 +408,8 @@ "puzzleThemeXRayAttackDescription": "Eng Figur attackéiert oder verdeedegte Feld duerch eng géigneresch Figur.", "puzzleThemeZugzwang": "Zugzwang", "puzzleThemeZugzwangDescription": "De Géigner huet eng begrenzten Unzuel un Zich an all Zuch verschlechtert seng Positioun.", - "puzzleThemeHealthyMix": "Gesonde Mix", - "puzzleThemeHealthyMixDescription": "E bësse vun allem. Du weess net wat dech erwaart, dowéinst muss op alles preparéiert sinn! Genau wéi bei echte Partien.", + "puzzleThemeMix": "Gesonde Mix", + "puzzleThemeMixDescription": "E bësse vun allem. Du weess net wat dech erwaart, dowéinst muss op alles preparéiert sinn! Genau wéi bei echte Partien.", "puzzleThemePlayerGames": "Partie vu Spiller", "puzzleThemePlayerGamesDescription": "Sich no Aufgaben, déi aus denge Partien, oder aus de Partie vun anere Spiller generéiert goufen.", "puzzleThemePuzzleDownloadInformation": "Dës Aufgaben sinn ëffentlech zougänglech an kënnen ënner {param} erofgelueden ginn.", @@ -496,7 +540,6 @@ "memory": "Aarbechtsspäicher", "infiniteAnalysis": "Endlos Analys", "removesTheDepthLimit": "Entfernt d'Déifenbegrenzung an hält däin Computer waarm", - "engineManager": "Engineverwaltung", "blunder": "Gaffe", "mistake": "Feeler", "inaccuracy": "Ongenauegkeet", @@ -795,7 +838,6 @@ "cheat": "Bedruch", "troll": "Troll", "other": "Aner", - "reportDescriptionHelp": "Post den Link vun Partie(n) and erklär wat den Problem mat dësem Benotzer sengem Verhalen ass. So net just \"Hien fuddelt\", mee so eis wéi du zu dëser Konklusioun komm bass. Däin Rapport gëtt méi schnell veraarbecht wann en op Englesch ass.", "error_provideOneCheatedGameLink": "Wannechgelift gëff eis op mannst een Link zu enger Partie mat Bedruch.", "by": "vum {param}", "importedByX": "Importéiert vun {param}", @@ -1182,6 +1224,7 @@ "showMeEverything": "Alles weisen", "lichessPatronInfo": "Lichess ass eng Wohltätegkeetsorganisatioun an eng komplett kostenfrei/open source Software.\nAll Betriebskäschten, Entwécklung an Inhalter ginn ausschließlech vun Benotzerspenden finanzéiert.", "nothingToSeeHere": "Fir de Moment gëtt et hei näischt ze gesinn.", + "stats": "Statistiken", "opponentLeftCounter": "{count, plural, =1{Däin Géigner huet d'Partie verlooss. Du kanns an {count} Sekonn d'Victoire reklaméieren.} other{Däin Géigner huet d'Partie verlooss. Du kanns an {count} Sekonnen d'Victoire reklaméieren.}}", "mateInXHalfMoves": "{count, plural, =1{Matt an {count} Hallef-Zuch} other{Matt an {count} Hallef-Zich}}", "nbBlunders": "{count, plural, =1{{count} Gaffe} other{{count} Gaffen}}", @@ -1278,6 +1321,158 @@ "stormXRuns": "{count, plural, =1{1 Duerchlaf} other{{count} Duerchleef}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Huet een Duerchlaf vun {param2} gespillt} other{Huet {count} Duerchleef vun {param2} gespillt}}", "streamerLichessStreamers": "Lichess Streamer", + "studyPrivate": "Privat", + "studyMyStudies": "Meng Etüden", + "studyStudiesIContributeTo": "Etüden, un deenen ech matwierken", + "studyMyPublicStudies": "Meng öffentlech Etüden", + "studyMyPrivateStudies": "Meng privat Etüden", + "studyMyFavoriteStudies": "Meng Lieblingsetüden", + "studyWhatAreStudies": "Wat sinn Etüden?", + "studyAllStudies": "All Etüden", + "studyStudiesCreatedByX": "Etüden kreéiert vun {param}", + "studyNoneYet": "Nach keng.", + "studyHot": "Ugesot", + "studyDateAddedNewest": "Veröffentlechungsdatum (am neisten)", + "studyDateAddedOldest": "Veröffentlechungsdatum (am aalsten)", + "studyRecentlyUpdated": "Rezent aktualiséiert", + "studyMostPopular": "Am Beléiftsten", + "studyAlphabetical": "Alphabetesch", + "studyAddNewChapter": "Neit Kapitel bäifügen", + "studyAddMembers": "Memberen hinzufügen", + "studyInviteToTheStudy": "An d'Etüd alueden", + "studyPleaseOnlyInvitePeopleYouKnow": "Wannechgelift invitéier just Leit déi du kenns an déi aktiv un der Etüd matwierken wëllen.", + "studySearchByUsername": "No Benotzernumm sichen", + "studySpectator": "Zuschauer", + "studyContributor": "Matwierkenden", + "studyKick": "Rausgehéien", + "studyLeaveTheStudy": "Etüd verloossen", + "studyYouAreNowAContributor": "Du bass elo e Contributeur", + "studyYouAreNowASpectator": "Du bass elo en Zuschauer", + "studyPgnTags": "PGN Tags", + "studyLike": "Gefällt mir", + "studyUnlike": "Gefällt mer net méi", + "studyNewTag": "Néien Tag", + "studyCommentThisPosition": "Kommentéier des Positioun", + "studyCommentThisMove": "Kommentéier dësen Zuch", + "studyAnnotateWithGlyphs": "Mat Symboler kommentéieren", + "studyTheChapterIsTooShortToBeAnalysed": "D'Kapitel ass ze kuerz fir analyséiert ze ginn.", + "studyOnlyContributorsCanRequestAnalysis": "Just Etüden Matwierkender kënnen eng Computer Analyse ufroen.", + "studyGetAFullComputerAnalysis": "Vollstänneg serversäiteg Computeranalyse vun der Haaptvariant erhalen.", + "studyMakeSureTheChapterIsComplete": "Stell sécher dass d'Kapitel vollstänneg ass. Du kanns eng Analyse just eemol ufroen.", + "studyAllSyncMembersRemainOnTheSamePosition": "All SYNC Memberen gesinn déi selwecht Positioun", + "studyShareChanges": "Deel Ännerungen mat den Zuschauer an späicher se um Server", + "studyPlaying": "Lafend Partie", + "studyFirst": "Éischt Säit", + "studyPrevious": "Zeréck", + "studyNext": "Weider", + "studyLast": "Lescht Säit", "studyShareAndExport": "Deelen & exportéieren", - "studyStart": "Lass" + "studyCloneStudy": "Klonen", + "studyStudyPgn": "Etüden PGN", + "studyDownloadAllGames": "All Partien eroflueden", + "studyChapterPgn": "Kapitel PGN", + "studyCopyChapterPgn": "PGN kopéieren", + "studyDownloadGame": "Partie eroflueden", + "studyStudyUrl": "Etüden URL", + "studyCurrentChapterUrl": "Aktuellt Kapitel URL", + "studyYouCanPasteThisInTheForumToEmbed": "Zum Anbetten an een Forum oder Blog afügen", + "studyStartAtInitialPosition": "Mat Startpositioun ufänken", + "studyStartAtX": "Bei {param} ufänken", + "studyEmbedInYourWebsite": "An Websäit anbetten", + "studyReadMoreAboutEmbedding": "Méi iwwer Anbetten liesen", + "studyOnlyPublicStudiesCanBeEmbedded": "Just ëffentlech Etüden kënnen angebett ginn!", + "studyOpen": "Opmaachen", + "studyXBroughtToYouByY": "{param1}, presentéiert vum {param2}", + "studyStudyNotFound": "Etüd net fonnt", + "studyEditChapter": "Kapitel editéieren", + "studyNewChapter": "Neit Kapitel", + "studyImportFromChapterX": "Importéieren aus {param}", + "studyOrientation": "Orientatioun", + "studyAnalysisMode": "Analysemodus", + "studyPinnedChapterComment": "Ugepinnten Kapitelkommentar", + "studySaveChapter": "Kapitel späicheren", + "studyClearAnnotations": "Annotatiounen läschen", + "studyClearVariations": "Variante läschen", + "studyDeleteChapter": "Kapitel läschen", + "studyDeleteThisChapter": "Kapitel läschen? Et gëtt keen zeréck!", + "studyClearAllCommentsInThisChapter": "All Kommentarer, Symboler an Zeechnungsformen an dësem Kapitel läschen?", + "studyRightUnderTheBoard": "Direkt ënnert dem Briet", + "studyNoPinnedComment": "Keng", + "studyNormalAnalysis": "Normal Analyse", + "studyHideNextMoves": "Nächst Zich verstoppen", + "studyInteractiveLesson": "Interaktiv Übung", + "studyChapterX": "Kapitel {param}", + "studyEmpty": "Eidel", + "studyStartFromInitialPosition": "Aus Startpositioun ufänken", + "studyEditor": "Editor", + "studyStartFromCustomPosition": "Aus benotzerdefinéierter Positioun ufänken", + "studyLoadAGameByUrl": "Partien mat URL lueden", + "studyLoadAPositionFromFen": "Positioun aus FEN lueden", + "studyLoadAGameFromPgn": "Partien aus PGN lueden", + "studyAutomatic": "Automatesch", + "studyUrlOfTheGame": "URL vun den Partien, eng pro Zeil", + "studyLoadAGameFromXOrY": "Partien vun {param1} oder {param2} lueden", + "studyCreateChapter": "Kapitel kréieren", + "studyCreateStudy": "Etüd kreéieren", + "studyEditStudy": "Etüd änneren", + "studyVisibility": "Visibilitéit", + "studyPublic": "Ëffentlech", + "studyUnlisted": "Ongelëscht", + "studyInviteOnly": "Just mat Invitatioun", + "studyAllowCloning": "Klonen erlaaben", + "studyNobody": "Keen", + "studyOnlyMe": "Just ech", + "studyContributors": "Matwierkendender", + "studyMembers": "Memberen", + "studyEveryone": "Jiddereen", + "studyEnableSync": "Synchronisatioun aktivéieren", + "studyYesKeepEveryoneOnTheSamePosition": "Jo: Jiddereen op der selwechter Positioun halen", + "studyNoLetPeopleBrowseFreely": "Nee: Leit individuell browsen loossen", + "studyPinnedStudyComment": "Ugepinnten Etüdenkommentar", + "studyStart": "Lass", + "studySave": "Späicheren", + "studyClearChat": "Chat läschen", + "studyDeleteTheStudyChatHistory": "Etüdenchat läschen? Et gëtt keen zeréck!", + "studyDeleteStudy": "Etüd läschen", + "studyConfirmDeleteStudy": "Komplett Etüd läschen? Et gëett keen zeréck! Tipp den Numm vun der Etüd an fir ze konfirméieren: {param}", + "studyWhereDoYouWantToStudyThat": "Wéieng Etüd wëlls du benotzen?", + "studyGoodMove": "Gudden Zuch", + "studyMistake": "Feeler", + "studyBrilliantMove": "Brillianten Zuch", + "studyBlunder": "Gaffe", + "studyInterestingMove": "Interessanten Zuch", + "studyDubiousMove": "Dubiosen Zuch", + "studyOnlyMove": "Eenzegen Zuch", + "studyZugzwang": "Zugzwang", + "studyEqualPosition": "Ausgeglach Positioun", + "studyUnclearPosition": "Onkloer Positioun", + "studyWhiteIsSlightlyBetter": "Wäiss steet liicht besser", + "studyBlackIsSlightlyBetter": "Schwaarz steet liicht besser", + "studyWhiteIsBetter": "Wäiss ass besser", + "studyBlackIsBetter": "Schwaarz ass besser", + "studyWhiteIsWinning": "Wéiss steet op Gewënn", + "studyBlackIsWinning": "Schwaarz steet op Gewënn", + "studyNovelty": "Neiheet", + "studyDevelopment": "Entwécklung", + "studyInitiative": "Initiativ", + "studyAttack": "Ugrëff", + "studyCounterplay": "Géigespill", + "studyTimeTrouble": "Zäitdrock", + "studyWithCompensation": "Mat Kompensatioun", + "studyWithTheIdea": "Mat der Iddi", + "studyNextChapter": "Nächst Kapitel", + "studyPrevChapter": "Kapitel virdrun", + "studyStudyActions": "Etüden-Aktiounen", + "studyTopics": "Themen", + "studyMyTopics": "Meng Themen", + "studyPopularTopics": "Beléift Themen", + "studyManageTopics": "Themen managen", + "studyBack": "Zeréck", + "studyPlayAgain": "Nach eng Kéier spillen", + "studyWhatWouldYouPlay": "Wat géifs du an dëser Positioun spillen?", + "studyYouCompletedThisLesson": "Gudd gemaach! Du hues dës Übung ofgeschloss.", + "studyNbChapters": "{count, plural, =1{{count} Kapitel} other{{count} Kapitel}}", + "studyNbGames": "{count, plural, =1{{count} Partie} other{{count} Partien}}", + "studyNbMembers": "{count, plural, =1{{count} Member} other{{count} Memberen}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{PGN Text hei asetzen, bis zu {count} Partie} other{PGN Text hei asetzen, bis zu {count} Partien}}" } \ No newline at end of file diff --git a/lib/l10n/lila_lt.arb b/lib/l10n/lila_lt.arb index 590546cb3e..23888f099d 100644 --- a/lib/l10n/lila_lt.arb +++ b/lib/l10n/lila_lt.arb @@ -21,7 +21,49 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{Dalyvavo {count} šveicariškame turnyre} few{Dalyvavo {count} šveicariškuose turnyruose} many{Dalyvavo {count} šveicariško turnyro} other{Dalyvavo {count} šveicariškų turnyrų}}", "activityJoinedNbTeams": "{count, plural, =1{Prisijungė prie {count} komandos} few{Prisijungė prie {count} komandų} many{Prisijungė prie {count} komandų} other{Prisijungė prie {count} komandų}}", "broadcastBroadcasts": "Transliacijos", + "broadcastMyBroadcasts": "Mano transliacijos", "broadcastLiveBroadcasts": "Vykstančios turnyrų transliacijos", + "broadcastNewBroadcast": "Nauja transliacija", + "broadcastSubscribedBroadcasts": "Prenumeruojamos transliacijos", + "broadcastAboutBroadcasts": "Apie transliacijas", + "broadcastHowToUseLichessBroadcasts": "Kaip naudotis Lichess transliacijomis.", + "broadcastTheNewRoundHelp": "Naujajame ture bus tie patys nariai ir bendradarbiai, kaip ir ankstesniame.", + "broadcastAddRound": "Pridėti raundą", + "broadcastOngoing": "Vykstančios", + "broadcastUpcoming": "Artėjančios", + "broadcastCompleted": "Pasibaigę", + "broadcastCompletedHelp": "Lichess aptiko turo užbaigimą, bet galimai klaidingai. Naudokite tai, norėdami nustatyti rankiniu būdu.", + "broadcastRoundName": "Raundo pavadinimas", + "broadcastRoundNumber": "Raundo numeris", + "broadcastTournamentName": "Turnyro pavadinimas", + "broadcastTournamentDescription": "Trumpas turnyro aprašymas", + "broadcastFullDescription": "Pilnas renginio aprašymas", + "broadcastFullDescriptionHelp": "Neprivalomas pilnas transliacijos aprašymas. Galima naudoti {param1}. Ilgis negali viršyti {param2} simbolių.", + "broadcastSourceSingleUrl": "PGN šaltinio URL", + "broadcastSourceUrlHelp": "URL, į kurį „Lichess“ kreipsis gauti PGN atnaujinimus. Privalo būti viešai pasiekiamas internete.", + "broadcastSourceGameIds": "Iki 64 Lichess žaidimo ID, atskirtų tarpais.", + "broadcastStartDateHelp": "Neprivaloma; tik jeigu žinote, kada prasideda renginys", + "broadcastCurrentGameUrl": "Dabartinio žaidimo adresas", + "broadcastDownloadAllRounds": "Atsisiųsti visus raundus", + "broadcastResetRound": "Atstatyti raundą", + "broadcastDeleteRound": "Ištrinti raundą", + "broadcastDefinitivelyDeleteRound": "Užtikrintai ištrinti raundą ir jo partijas.", + "broadcastDeleteAllGamesOfThisRound": "Ištrinti visas partijas šiame raunde. Norint jas perkurti reikės aktyvaus šaltinio.", + "broadcastEditRoundStudy": "Keisti raundo studiją", + "broadcastDeleteTournament": "Ištrinti šį turnyrą", + "broadcastDefinitivelyDeleteTournament": "Užtikrintai ištrinti visą turnyrą, visus raundus ir visas jų partijas.", + "broadcastShowScores": "Rodyti žaidėjų balus pagal partijų rezultatus", + "broadcastReplacePlayerTags": "Pasirenkama: pakeiskite žaidėjų vardus, reitingus ir titulus", + "broadcastFideFederations": "FIDE federacijos", + "broadcastTop10Rating": "10 aukščiausių reitingų", + "broadcastFidePlayers": "FIDE žaidėjai", + "broadcastFidePlayerNotFound": "FIDE žaidėjas nerastas", + "broadcastFideProfile": "FIDE profilis", + "broadcastFederation": "Federacija", + "broadcastAgeThisYear": "Amžius šiemet", + "broadcastUnrated": "Nereitinguota(s)", + "broadcastRecentTournaments": "Neseniai sukurti turnyrai", + "broadcastNbBroadcasts": "{count, plural, =1{{count} transliacija} few{{count} transliacijos} many{{count} transliacijos} other{{count} transliacijų}}", "challengeChallengesX": "Iššūkiai: {param1}", "challengeChallengeToPlay": "Iškelti iššūkį", "challengeChallengeDeclined": "Iššūkis atmestas", @@ -340,8 +382,8 @@ "puzzleThemeXRayAttackDescription": "Figūra puola ar gina laukelį kiaurai priešininko figūros.", "puzzleThemeZugzwang": "Priverstinis ėjimas", "puzzleThemeZugzwangDescription": "Priešininkas apribotas ėjimais, kuriuos gali padaryti, ir visi jo ėjimai tik pabloginą jo poziciją.", - "puzzleThemeHealthyMix": "Visko po truputį", - "puzzleThemeHealthyMixDescription": "Nežinote ko tikėtis, todėl būkite pasiruošę bet kam! Visai kaip tikruose žaidimuose.", + "puzzleThemeMix": "Visko po truputį", + "puzzleThemeMixDescription": "Nežinote ko tikėtis, todėl būkite pasiruošę bet kam! Visai kaip tikruose žaidimuose.", "puzzleThemePlayerGames": "Žaidėjų žaidimai", "puzzleThemePlayerGamesDescription": "Galvosūkiai sugeneruoti iš jūsų partijų ar iš kitų žaidėjų partijų.", "puzzleThemePuzzleDownloadInformation": "Šie galvosūkiai yra laisvai prieinami ir gali būti parsisiųsti iš {param}.", @@ -359,12 +401,12 @@ "toInviteSomeoneToPlayGiveThisUrl": "Norėdami pakviesti varžovą, pasidalinkite šiuo adresu", "gameOver": "Partija baigta", "waitingForOpponent": "Laukiama varžovo", - "orLetYourOpponentScanQrCode": "Arba leiskite priešininkui nuskanuoti šį QR kodą", + "orLetYourOpponentScanQrCode": "Arba leiskite priešininkui nuskenuoti šį QR kodą", "waiting": "Laukiama", "yourTurn": "Jūsų ėjimas", "aiNameLevelAiLevel": "{param1} lygis Nr. {param2}", "level": "Lygis", - "strength": "Stiprumas", + "strength": "Pasipriešinimo stiprumas", "toggleTheChat": "Įjungti / išjungti pokalbį", "chat": "Pokalbis", "resign": "Pasiduoti", @@ -372,14 +414,14 @@ "stalemate": "Patas", "white": "Baltieji", "black": "Juodieji", - "asWhite": "kaip baltieji", - "asBlack": "kaip juodieji", + "asWhite": "už baltuosius", + "asBlack": "už juoduosius", "randomColor": "Atsitiktinė spalva", "createAGame": "Kurti žaidimą", "whiteIsVictorious": "Baltieji laimėjo", "blackIsVictorious": "Juodieji laimėjo", - "youPlayTheWhitePieces": "Žaidžiate baltomis figūromis", - "youPlayTheBlackPieces": "Žaidžiate juodomis figūromis", + "youPlayTheWhitePieces": "Jūs žaidžiate baltosiomis figūromis", + "youPlayTheBlackPieces": "Jūs žaidžiate juodosiomis figūromis", "itsYourTurn": "Jūsų ėjimas!", "cheatDetected": "Aptiktas sukčiavimas", "kingInTheCenter": "Karalius centre", @@ -472,7 +514,6 @@ "memory": "Atmintinė", "infiniteAnalysis": "Neribota analizė", "removesTheDepthLimit": "Panaikina gylio limitą ir neleidžia kompiuteriui atvėsti", - "engineManager": "Variklių valdymas", "blunder": "Šiurkšti klaida", "mistake": "Klaida", "inaccuracy": "Netikslumas", @@ -498,6 +539,7 @@ "latestForumPosts": "Naujausi diskusijų pranešimai", "players": "Žaidėjai", "friends": "Draugai", + "otherPlayers": "kiti žaidėjai", "discussions": "Diskusijos", "today": "Šiandien", "yesterday": "Vakar", @@ -755,6 +797,8 @@ "descPrivateHelp": "Tekstas, kurį gali matyti tik komandos nariai. Jei nustatytas, komandos nariams pakeičia viešą aprašymą.", "no": "Ne", "yes": "Taip", + "website": "Tinklapis", + "mobile": "Mobilus", "help": "Pagalba:", "createANewTopic": "Sukurti naują temą", "topics": "Temos", @@ -773,7 +817,9 @@ "cheat": "Sukčiaviavo", "troll": "„Troll'ino“", "other": "Kita", - "reportDescriptionHelp": "Įdėkite nuorodą į partiją(-as) ir paaiškinkite, kas netinkamo yra šio vartotojo elgsenoje. Paminėkite, kaip priėjote prie tokios išvados. Jūsų pranešimas bus apdorotas greičiau, jei bus pateiktas anglų kalba.", + "reportCheatBoostHelp": "Įdėkite nuorodą į partiją(-as) ir paaiškinkite, kas netinkamo yra šio vartotojo elgsenoje. Paminėkite, kaip priėjote prie tokios išvados. Jūsų pranešimas bus apdorotas greičiau, jei bus pateiktas anglų kalba.", + "reportUsernameHelp": "Paaiškinkite, kuo šis vartotojo vardas yra įžeidžiantis. Nesakykite tiesiog „tai įžeidžia/netinkama“, bet papasakokite, kaip priėjote prie šios išvados, ypač jei įžeidimas yra užmaskuotas, ne anglų kalba, yra slengas arba yra istorinė / kultūrinė nuoroda.", + "reportProcessedFasterInEnglish": "Jūsų pranešimas bus apdorotas greičiau, jei jis bus parašytas anglų kalba.", "error_provideOneCheatedGameLink": "Pateikite bent vieną nuorodą į partiją, kurioje buvo sukčiauta.", "by": "nuo {param}", "importedByX": "Importavo {param}", @@ -1171,7 +1217,7 @@ "showMeEverything": "Rodyti viską", "lichessPatronInfo": "Lichess yra labdara ir pilnai atviro kodo/libre projektas.\nVisos veikimo išlaidos, programavimas ir turinys yra padengti išskirtinai tik vartotojų parama.", "nothingToSeeHere": "Nieko naujo.", - "opponentLeftCounter": "{count, plural, =1{Jūsų varžovas paliko partiją. Galėsite prisiimti pergalę už {count} sekundės.} few{Jūsų varžovas paliko partiją. Galėsite prisiimti pergalę už {count} sekundžių.} many{Jūsų varžovas paliko partiją. Galėsite prisiimti pergalę už {count} sekundžių.} other{Jūsų varžovas paliko partiją. Galėsite prisiimti pergalę už {count} sekundžių.}}", + "opponentLeftCounter": "{count, plural, =1{Jūsų varžovas paliko partiją. Galite reikalauti pergalės už {count} sekundės.} few{Jūsų varžovas paliko partiją. Galėsite prisiimti pergalę už {count} sekundžių.} many{Jūsų varžovas paliko partiją. Galėsite prisiimti pergalę už {count} sekundžių.} other{Jūsų varžovas paliko partiją. Galėsite prisiimti pergalę už {count} sekundžių.}}", "mateInXHalfMoves": "{count, plural, =1{Matas už {count} pus-ėjimo} few{Matas už {count} pus-ėjimų} many{Matas už {count} pus-ėjimų} other{Matas už {count} pus-ėjimų}}", "nbBlunders": "{count, plural, =1{{count} šiurkšti klaida} few{{count} šiurkščios klaidos} many{{count} šiurkščios klaidos} other{{count} šiurkščių klaidų}}", "nbMistakes": "{count, plural, =1{{count} klaida} few{{count} klaidos} many{{count} klaidos} other{{count} klaidų}}", @@ -1267,6 +1313,159 @@ "stormXRuns": "{count, plural, =1{1 eilė} few{{count} eilės} many{{count} eilės} other{{count} eilių}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Žaista viena {param2} eilė} few{Žaistos {count} {param2} eilės} many{Žaista {count} {param2} eilės} other{Žaista {count} {param2} eilių}}", "streamerLichessStreamers": "Lichess transliuotojai", + "studyPrivate": "Privati", + "studyMyStudies": "Mano studijos", + "studyStudiesIContributeTo": "Studijos, kuriose prisidedu", + "studyMyPublicStudies": "Mano viešos studijos", + "studyMyPrivateStudies": "Mano privačios studijos", + "studyMyFavoriteStudies": "Mano mėgstamiausios studijos", + "studyWhatAreStudies": "Kas yra studijos?", + "studyAllStudies": "Visos studijos", + "studyStudiesCreatedByX": "Studijos, sukurtos {param}", + "studyNoneYet": "Dar nėra.", + "studyHot": "Populiaru dabar", + "studyDateAddedNewest": "Sukūrimo data (naujausios)", + "studyDateAddedOldest": "Sukūrimo data (seniausios)", + "studyRecentlyUpdated": "Neseniai atnaujintos", + "studyMostPopular": "Populiariausios", + "studyAlphabetical": "Abėcėlės tvarka", + "studyAddNewChapter": "Pridėti naują skyrių", + "studyAddMembers": "Pridėti narių", + "studyInviteToTheStudy": "Pakviesti į studiją", + "studyPleaseOnlyInvitePeopleYouKnow": "Kvieskite tik pažįstamus žmones, ir tik norinčius dalyvauti šioje studijoje.", + "studySearchByUsername": "Ieškoti pagal naudotojo vardą", + "studySpectator": "Žiūrovas", + "studyContributor": "Talkininkas", + "studyKick": "Išmesti", + "studyLeaveTheStudy": "Palikti studiją", + "studyYouAreNowAContributor": "Dabar esate talkininkas", + "studyYouAreNowASpectator": "Dabar esate žiūrovas", + "studyPgnTags": "PGN žymos", + "studyLike": "Mėgti", + "studyUnlike": "Nebemėgti", + "studyNewTag": "Nauja žyma", + "studyCommentThisPosition": "Komentuoti šią poziciją", + "studyCommentThisMove": "Komentuoti šį ėjimą", + "studyAnnotateWithGlyphs": "Komentuoti su glifais", + "studyTheChapterIsTooShortToBeAnalysed": "Skyrius yra per trumpas analizei.", + "studyOnlyContributorsCanRequestAnalysis": "Tik studijos talkininkai gali prašyti kompiuterio analizės.", + "studyGetAFullComputerAnalysis": "Gaukite pilną pagrindinės linijos kompiuterio analizę.", + "studyMakeSureTheChapterIsComplete": "Įsitikinkite, kad skyrius užbaigtas. Analizės galite prašyti tik kartą.", + "studyAllSyncMembersRemainOnTheSamePosition": "Visi SYNC nariai lieka toje pačioje pozicijoje", + "studyShareChanges": "Dalinkitės pakeitimais su žiūrovais ir saugokite juos serveryje", + "studyPlaying": "Žaidžiama", + "studyShowEvalBar": "Vertinimo skalė", + "studyFirst": "Pirmas", + "studyPrevious": "Ankstesnis", + "studyNext": "Kitas", + "studyLast": "Paskutinis", "studyShareAndExport": "Dalintis ir eksportuoti", - "studyStart": "Pradėti" + "studyCloneStudy": "Klonuoti", + "studyStudyPgn": "Studijos PGN", + "studyDownloadAllGames": "Atsisiųsti visus žaidimus", + "studyChapterPgn": "Skyriaus PGN", + "studyCopyChapterPgn": "Kopijuoti PGN", + "studyDownloadGame": "Atsisiųsti žaidimą", + "studyStudyUrl": "Studijos URL", + "studyCurrentChapterUrl": "Dabartinio skyriaus URL", + "studyYouCanPasteThisInTheForumToEmbed": "Galite įklijuoti šį forume norėdami įterpti", + "studyStartAtInitialPosition": "Pradėti pradinėje pozicijoje", + "studyStartAtX": "Pradėti nuo {param}", + "studyEmbedInYourWebsite": "Įterpti savo svetainėje ar tinklaraštyje", + "studyReadMoreAboutEmbedding": "Skaitykite daugiau apie įterpimą", + "studyOnlyPublicStudiesCanBeEmbedded": "Gali būti įterptos tik viešos studijos!", + "studyOpen": "Atverti", + "studyXBroughtToYouByY": "{param1} iš {param2}", + "studyStudyNotFound": "Studija nerasta", + "studyEditChapter": "Redaguoti skyrių", + "studyNewChapter": "Naujas skyrius", + "studyImportFromChapterX": "Importuoti iš {param}", + "studyOrientation": "Kryptis", + "studyAnalysisMode": "Analizės režimas", + "studyPinnedChapterComment": "Prisegtas skyriaus komentaras", + "studySaveChapter": "Išsaugoti skyrių", + "studyClearAnnotations": "Pašalinti anotacijas", + "studyClearVariations": "Išvalyti variacijas", + "studyDeleteChapter": "Ištrinti skyrių", + "studyDeleteThisChapter": "Ištrinti šį skyrių? Nėra kelio atgal!", + "studyClearAllCommentsInThisChapter": "Išvalyti visus komentarus, ženklus ir figūras šiame skyriuje?", + "studyRightUnderTheBoard": "Iš karto po lenta", + "studyNoPinnedComment": "Jokio", + "studyNormalAnalysis": "Įprasta analizė", + "studyHideNextMoves": "Slėpti kitus ėjimus", + "studyInteractiveLesson": "Interaktyvi pamoka", + "studyChapterX": "Skyrius {param}", + "studyEmpty": "Tuščia", + "studyStartFromInitialPosition": "Pradėti nuo pirminės pozicijos", + "studyEditor": "Redaktorius", + "studyStartFromCustomPosition": "Pradėti nuo tinkintos pozicijos", + "studyLoadAGameByUrl": "Pakrauti partijas iš adresų", + "studyLoadAPositionFromFen": "Pakrauti poziciją iš FEN", + "studyLoadAGameFromPgn": "Pakrauti partijas iš PGN", + "studyAutomatic": "Automatinis", + "studyUrlOfTheGame": "Partijų adresai, vienas per eilutę", + "studyLoadAGameFromXOrY": "Pakrauti partijas iš {param1} arba {param2}", + "studyCreateChapter": "Sukurti skyrių", + "studyCreateStudy": "Sukurti studiją", + "studyEditStudy": "Redaguoti studiją", + "studyVisibility": "Matomumas", + "studyPublic": "Viešas", + "studyUnlisted": "Nėra sąraše", + "studyInviteOnly": "Tik su pakvietimu", + "studyAllowCloning": "Leisti kopijuoti", + "studyNobody": "Niekam", + "studyOnlyMe": "Tik man", + "studyContributors": "Dalyviams", + "studyMembers": "Nariams", + "studyEveryone": "Visiems", + "studyEnableSync": "Įgalinti sinchronizaciją", + "studyYesKeepEveryoneOnTheSamePosition": "Taip: visiems rodyti tą pačią poziciją", + "studyNoLetPeopleBrowseFreely": "Ne: leisti žmonėms naršyti laisvai", + "studyPinnedStudyComment": "Prisegtas studijos komentaras", + "studyStart": "Pradėti", + "studySave": "Išsaugoti", + "studyClearChat": "Išvalyti pokalbį", + "studyDeleteTheStudyChatHistory": "Ištrinti studijos pokalbių istoriją? Nėra kelio atgal!", + "studyDeleteStudy": "Ištrinti studiją", + "studyConfirmDeleteStudy": "Ištrinti visą studiją? Ištrynimas negrįžtamas. Norėdami tęsti įrašykite studijos pavadinimą: {param}", + "studyWhereDoYouWantToStudyThat": "Kur norite tai studijuoti?", + "studyGoodMove": "Geras ėjimas", + "studyMistake": "Klaida", + "studyBrilliantMove": "Puikus ėjimas", + "studyBlunder": "Šiurkšti klaida", + "studyInterestingMove": "Įdomus ėjimas", + "studyDubiousMove": "Abejotinas ėjimas", + "studyOnlyMove": "Vienintelis ėjimas", + "studyZugzwang": "Cugcvangas", + "studyEqualPosition": "Lygi pozicija", + "studyUnclearPosition": "Neaiški pozicija", + "studyWhiteIsSlightlyBetter": "Šiek tiek geriau baltiesiems", + "studyBlackIsSlightlyBetter": "Šiek tiek geriau juodiesiems", + "studyWhiteIsBetter": "Geriau baltiesiems", + "studyBlackIsBetter": "Geriau juodiesiems", + "studyWhiteIsWinning": "Laimi baltieji", + "studyBlackIsWinning": "Laimi juodieji", + "studyNovelty": "Naujovė", + "studyDevelopment": "Plėtojimas", + "studyInitiative": "Iniciatyva", + "studyAttack": "Ataka", + "studyCounterplay": "Kontraėjimas", + "studyTimeTrouble": "Laiko problemos", + "studyWithCompensation": "Su kompensacija", + "studyWithTheIdea": "Su mintimi", + "studyNextChapter": "Kitas skyrius", + "studyPrevChapter": "Ankstenis skyrius", + "studyStudyActions": "Studijos veiksmai", + "studyTopics": "Temos", + "studyMyTopics": "Mano temos", + "studyPopularTopics": "Populiarios temos", + "studyManageTopics": "Valdyti temas", + "studyBack": "Atgal", + "studyPlayAgain": "Žaisti dar kartą", + "studyWhatWouldYouPlay": "Ar norėtumėte žaisti nuo šios pozicijos?", + "studyYouCompletedThisLesson": "Sveikiname! Jūs pabaigėte šią pamoką.", + "studyNbChapters": "{count, plural, =1{{count} skyrius} few{{count} skyriai} many{{count} skyrių} other{{count} skyrių}}", + "studyNbGames": "{count, plural, =1{{count} partija} few{{count} partijos} many{{count} partijų} other{{count} partijų}}", + "studyNbMembers": "{count, plural, =1{{count} narys} few{{count} nariai} many{{count} narių} other{{count} narių}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Įklijuokite savo PGN tekstą čia, iki {count} žaidimo} few{Įklijuokite savo PGN tekstą čia, iki {count} žaidimų} many{Įklijuokite savo PGN tekstą čia, iki {count} žaidimo} other{Įklijuokite savo PGN tekstą čia, iki {count} žaidimų}}" } \ No newline at end of file diff --git a/lib/l10n/lila_lv.arb b/lib/l10n/lila_lv.arb index 9b83c2b18b..625fc889e6 100644 --- a/lib/l10n/lila_lv.arb +++ b/lib/l10n/lila_lv.arb @@ -22,6 +22,25 @@ "activityJoinedNbTeams": "{count, plural, =0{Pievienojās {count} komandām} =1{Pievienojās {count} komandai} other{Pievienojās {count} komandām}}", "broadcastBroadcasts": "Raidījumi", "broadcastLiveBroadcasts": "Reāllaika turnīru raidījumi", + "broadcastNewBroadcast": "Jauns reāllaika raidījums", + "broadcastAddRound": "Pievienot raundu", + "broadcastOngoing": "Notiekošie", + "broadcastUpcoming": "Gaidāmie", + "broadcastCompleted": "Notikušie", + "broadcastRoundName": "Raunda nosaukums", + "broadcastRoundNumber": "Raunda skaitlis", + "broadcastTournamentName": "Turnīra nosaukums", + "broadcastTournamentDescription": "Īss turnīra apraksts", + "broadcastFullDescription": "Pilns pasākuma apraksts", + "broadcastFullDescriptionHelp": "Neobligāts garš raidījuma apraksts. Pieejams {param1}. Garumam jābūt mazāk kā {param2} rakstzīmēm.", + "broadcastSourceUrlHelp": "URL, ko Lichess aptaujās, lai iegūtu PGN atjauninājumus. Tam jābūt publiski piekļūstamam no interneta.", + "broadcastStartDateHelp": "Neobligāts, ja zināt, kad pasākums sākas", + "broadcastCurrentGameUrl": "Pašreizējās spēles URL", + "broadcastDownloadAllRounds": "Lejupielādēt visus raundus", + "broadcastResetRound": "Atiestatīt šo raundu", + "broadcastDeleteRound": "Dzēst šo raundu", + "broadcastDefinitivelyDeleteRound": "Neatgriezeniski dzēst raundu un tā spēles.", + "broadcastDeleteAllGamesOfThisRound": "Izdzēst visas šī raunda spēles. To atjaunošanai būs nepieciešams aktīvs avots.", "challengeChallengesX": "Izaicinājumi: {param1}", "challengeChallengeToPlay": "Izaicināt uz spēli", "challengeChallengeDeclined": "Izaicinājums noraidīts", @@ -337,8 +356,8 @@ "puzzleThemeXRayAttackDescription": "Figūra uzbrūk vai apsargā lauciņu caur pretinieka figūru.", "puzzleThemeZugzwang": "Zugzwang", "puzzleThemeZugzwangDescription": "Pretiniekam ir ierobežoti iespējamie gājieni, un visi no tiem pasliktina pretinieka pozīciju.", - "puzzleThemeHealthyMix": "Veselīgs sajaukums", - "puzzleThemeHealthyMixDescription": "Mazliet no visa kā. Nezināsiet, ko sagaidīt, tāpēc paliksiet gatavs jebkam! Tieši kā īstās spēlēs.", + "puzzleThemeMix": "Veselīgs sajaukums", + "puzzleThemeMixDescription": "Mazliet no visa kā. Nezināsiet, ko sagaidīt, tāpēc paliksiet gatavs jebkam! Tieši kā īstās spēlēs.", "puzzleThemePlayerGames": "Spēlētāja spēles", "puzzleThemePlayerGamesDescription": "Meklējiet uzdevumus, kas radīti no jūsu vai cita spēlētāja spēlēm.", "puzzleThemePuzzleDownloadInformation": "Šie uzdevumi ir neaizsargājami darbi, un tos var lejupielādēt lapā {param}.", @@ -464,7 +483,6 @@ "memory": "Atmiņa", "infiniteAnalysis": "Bezgalīga analīze", "removesTheDepthLimit": "Noņem dziļuma ierobežojumu un uztur tavu datoru siltu", - "engineManager": "Dzinēja pārvaldnieks", "blunder": "Rupja kļūda", "mistake": "Kļūda", "inaccuracy": "Neprecizitāte", @@ -755,7 +773,6 @@ "cheat": "Krāpšanās", "troll": "Troļļošana", "other": "Cits", - "reportDescriptionHelp": "Ielīmējiet spēles saiti un paskaidrojiet, kas nav kārtībā ar lietotāja uzvedību. Nepietiks, ja tikai norādīsiet, ka \"lietotājs krāpjas\" — lūdzu, pastāstiet, kā nonācāt pie šī secinājuma. Ja jūsu ziņojums būs rakstīts angliski, par to varēsim parūpēties ātrāk.", "error_provideOneCheatedGameLink": "Lūdzu, norādiet vismaz vienu saiti uz spēli, kurā pretinieks ir krāpies.", "by": "no {param}", "importedByX": "Importēja {param}", @@ -1223,6 +1240,158 @@ "stormXRuns": "{count, plural, =0{{count} izspēlēšanas} =1{{count} izspēlēšana} other{{count} izspēlēšanas}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =0{Izspēlēja {param2} {count} reizes} =1{Izspēlēja {param2} {count} reizi} other{Izspēlēja {param2} {count} reizes}}", "streamerLichessStreamers": "Lichess straumētāji", + "studyPrivate": "Privāta", + "studyMyStudies": "Manas izpētes", + "studyStudiesIContributeTo": "Izpētes, kurās piedalos", + "studyMyPublicStudies": "Manas publiskās izpētes", + "studyMyPrivateStudies": "Manas privātās izpētes", + "studyMyFavoriteStudies": "Mana izpēšu izlase", + "studyWhatAreStudies": "Kas ir izpētes?", + "studyAllStudies": "Visas izpētes", + "studyStudiesCreatedByX": "Izpētes, ko izveidoja {param}", + "studyNoneYet": "Pagaidām nevienas.", + "studyHot": "Nesen populārās", + "studyDateAddedNewest": "Pievienošanas datums (jaunākās)", + "studyDateAddedOldest": "Pievienošanas datums (vecākās)", + "studyRecentlyUpdated": "Nesen atjaunotās", + "studyMostPopular": "Populārākās", + "studyAlphabetical": "Alfabētiskā secībā", + "studyAddNewChapter": "Pievienot nodaļu", + "studyAddMembers": "Pievienot dalībniekus", + "studyInviteToTheStudy": "Ielūgt uz izpēti", + "studyPleaseOnlyInvitePeopleYouKnow": "Lūdzu, ielūdziet tikai cilvēkus, kurus pazīstat un kuri vēlas pievienoties izpētei.", + "studySearchByUsername": "Meklēt pēc lietotājvārda", + "studySpectator": "Skatītājs", + "studyContributor": "Ieguldītājs", + "studyKick": "Izmest", + "studyLeaveTheStudy": "Pamest izpēti", + "studyYouAreNowAContributor": "Tagad esat ieguldītājs", + "studyYouAreNowASpectator": "Tagad esat skatītājs", + "studyPgnTags": "PGN birkas", + "studyLike": "Patīk", + "studyUnlike": "Noņemt atzīmi \"patīk\"", + "studyNewTag": "Jauna birka", + "studyCommentThisPosition": "Komentēt šo pozīciju", + "studyCommentThisMove": "Komentēt šo gājienu", + "studyAnnotateWithGlyphs": "Anotēt ar glifiem", + "studyTheChapterIsTooShortToBeAnalysed": "Šī nodaļa ir par īsu lai to analizētu.", + "studyOnlyContributorsCanRequestAnalysis": "Tikai izpētes ieguldītāji var pieprasīt datoranalīzi.", + "studyGetAFullComputerAnalysis": "Iegūstiet pilnu servera puses pamatvarianta datoranalīzi.", + "studyMakeSureTheChapterIsComplete": "Pārliecinieties, ka nodaļa ir pabeigta. Datoranalīzi var pieprasīt tikai vienreiz.", + "studyAllSyncMembersRemainOnTheSamePosition": "Visi SYNC dalībnieki paliek vienā pozīcijā", + "studyShareChanges": "Koplietot izmaiņas ar skatītājiem un saglabāt tās serverī", + "studyPlaying": "Notiek", + "studyFirst": "Pirmais", + "studyPrevious": "Iepriekšējais", + "studyNext": "Nākamais", + "studyLast": "Pēdējais", "studyShareAndExport": "Koplietot & eksportēt", - "studyStart": "Sākt" + "studyCloneStudy": "Klonēt", + "studyStudyPgn": "Izpētes PGN", + "studyDownloadAllGames": "Lejupielādēt visas spēles", + "studyChapterPgn": "Nodaļas PGN", + "studyCopyChapterPgn": "Kopēt PGN", + "studyDownloadGame": "Lejupielādēt spēli", + "studyStudyUrl": "Izpētes URL", + "studyCurrentChapterUrl": "Pašreizējās nodaļas URL", + "studyYouCanPasteThisInTheForumToEmbed": "Šo varat ielīmēt forumā, lai iegultu", + "studyStartAtInitialPosition": "Sākt no sākotnējās pozīcijas", + "studyStartAtX": "Sākt ar {param}", + "studyEmbedInYourWebsite": "Iegult savā mājaslapā vai blogā", + "studyReadMoreAboutEmbedding": "Lasīt vairāk par iegulšanu", + "studyOnlyPublicStudiesCanBeEmbedded": "Iegult var tikai publiskas izpētes!", + "studyOpen": "Atvērt", + "studyXBroughtToYouByY": "{param2} piedāvā \"{param1}\"", + "studyStudyNotFound": "Izpēte nav atrasta", + "studyEditChapter": "Rediģēt nodaļu", + "studyNewChapter": "Jauna nodaļa", + "studyImportFromChapterX": "Importēt no {param}", + "studyOrientation": "Orientācija", + "studyAnalysisMode": "Analīzes režīms", + "studyPinnedChapterComment": "Piesprausts nodaļas komentārs", + "studySaveChapter": "Saglabāt nodaļu", + "studyClearAnnotations": "Notīrīt piezīmes", + "studyClearVariations": "Notīrīt variantus", + "studyDeleteChapter": "Dzēst nodaļu", + "studyDeleteThisChapter": "Vai dzēst šo nodaļu? Atpakaļceļa nav!", + "studyClearAllCommentsInThisChapter": "Notīrīt visus komentārus un figūras šajā nodaļā?", + "studyRightUnderTheBoard": "Tieši zem galdiņa", + "studyNoPinnedComment": "Neviens", + "studyNormalAnalysis": "Parasta analīze", + "studyHideNextMoves": "Slēpt turpmākos gājienus", + "studyInteractiveLesson": "Interaktīva nodarbība", + "studyChapterX": "{param}. nodaļa", + "studyEmpty": "Tukšs", + "studyStartFromInitialPosition": "Sākt no sākotnējās pozīcijas", + "studyEditor": "Redaktors", + "studyStartFromCustomPosition": "Sākt no pielāgotas pozīcijas", + "studyLoadAGameByUrl": "Ielādēt spēli, norādot URL", + "studyLoadAPositionFromFen": "Ielādēt pozīciju no FEN", + "studyLoadAGameFromPgn": "Ielādēt spēli no PGN", + "studyAutomatic": "Automātisks", + "studyUrlOfTheGame": "Spēles URL", + "studyLoadAGameFromXOrY": "Ielādēt spēli no {param1} vai {param2}", + "studyCreateChapter": "Izveidot nodaļu", + "studyCreateStudy": "Izveidot izpēti", + "studyEditStudy": "Rediģēt izpēti", + "studyVisibility": "Redzamība", + "studyPublic": "Publiska", + "studyUnlisted": "Nerindota", + "studyInviteOnly": "Tikai ar ielūgumu", + "studyAllowCloning": "Atļaut dublēšanu", + "studyNobody": "Neviens", + "studyOnlyMe": "Tikai es", + "studyContributors": "Ieguldītāji", + "studyMembers": "Dalībnieki", + "studyEveryone": "Visi", + "studyEnableSync": "Iespējot sinhronizāciju", + "studyYesKeepEveryoneOnTheSamePosition": "Jā: paturēt visus vienā pozīcijā", + "studyNoLetPeopleBrowseFreely": "Nē: ļaut katram brīvi pārlūkot", + "studyPinnedStudyComment": "Piesprausts izpētes komentārs", + "studyStart": "Sākt", + "studySave": "Saglabāt", + "studyClearChat": "Notīrīt saraksti", + "studyDeleteTheStudyChatHistory": "Vai dzēst izpētes sarakstes vēsturi? Atpakaļceļa nav!", + "studyDeleteStudy": "Dzēst izpēti", + "studyConfirmDeleteStudy": "Dzēst visu izpēti? Atpakaļceļa nav! Ievadiet izpētes nosaukumu, lai apstiprinātu: {param}", + "studyWhereDoYouWantToStudyThat": "Kur vēlaties to izpētīt?", + "studyGoodMove": "Labs gājiens", + "studyMistake": "Kļūda", + "studyBrilliantMove": "Izcils gājiens", + "studyBlunder": "Rupja kļūda", + "studyInterestingMove": "Interesants gājiens", + "studyDubiousMove": "Apšaubāms gājiens", + "studyOnlyMove": "Vienīgais gājiens", + "studyZugzwang": "Gājiena spaids", + "studyEqualPosition": "Vienlīdzīga pozīcija", + "studyUnclearPosition": "Neskaidra pozīcija", + "studyWhiteIsSlightlyBetter": "Baltajiem nedaudz labāka pozīcija", + "studyBlackIsSlightlyBetter": "Melnajiem nedaudz labāka pozīcija", + "studyWhiteIsBetter": "Baltajiem labāka pozīcija", + "studyBlackIsBetter": "Melnajiem labāka pozīcija", + "studyWhiteIsWinning": "Baltie tuvojas uzvarai", + "studyBlackIsWinning": "Melnie tuvojas uzvarai", + "studyNovelty": "Oriģināls gājiens", + "studyDevelopment": "Attīstība", + "studyInitiative": "Iniciatīva", + "studyAttack": "Uzbrukums", + "studyCounterplay": "Pretspēle", + "studyTimeTrouble": "Laika trūkuma grūtības", + "studyWithCompensation": "Ar atlīdzinājumu", + "studyWithTheIdea": "Ar domu", + "studyNextChapter": "Nākamā nodaļa", + "studyPrevChapter": "Iepriekšējā nodaļa", + "studyStudyActions": "Izpētes darbības", + "studyTopics": "Temati", + "studyMyTopics": "Mani temati", + "studyPopularTopics": "Populāri temati", + "studyManageTopics": "Pārvaldīt tematus", + "studyBack": "Atpakaļ", + "studyPlayAgain": "Spēlēt vēlreiz", + "studyWhatWouldYouPlay": "Kā jūs spēlētu šādā pozīcijā?", + "studyYouCompletedThisLesson": "Apsveicam! Pabeidzāt šo nodarbību.", + "studyNbChapters": "{count, plural, =0{{count} Nodaļas} =1{{count} Nodaļa} other{{count} Nodaļas}}", + "studyNbGames": "{count, plural, =0{{count} Spēles} =1{{count} Spēle} other{{count} Spēles}}", + "studyNbMembers": "{count, plural, =0{{count} Dalībnieki} =1{{count} Dalībnieks} other{{count} Dalībnieki}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =0{Ielīmējiet PGN tekstu šeit, ne vairāk kā {count} spēles} =1{Ielīmējiet PGN tekstu šeit, ne vairāk kā {count} spēli} other{Ielīmējiet PGN tekstu šeit, ne vairāk kā {count} spēles}}" } \ No newline at end of file diff --git a/lib/l10n/lila_mk.arb b/lib/l10n/lila_mk.arb index 2cfffc3f16..4d2d31a540 100644 --- a/lib/l10n/lila_mk.arb +++ b/lib/l10n/lila_mk.arb @@ -1,4 +1,7 @@ { + "mobileSystemColors": "Системски бои", + "mobileFeedbackButton": "Повратна информација", + "mobileSettingsHapticFeedback": "Тактилен фидбек", "activityActivity": "Активност", "activityHostedALiveStream": "Емитуваше во живо", "activityRankedInSwissTournament": "Ранг #{param1} во {param2}", @@ -22,6 +25,15 @@ "activityJoinedNbTeams": "{count, plural, =1{Се придружи на {count} тим} other{Се придружи на {count} тимови}}", "broadcastBroadcasts": "Емитувања", "broadcastLiveBroadcasts": "Пренос на турнири во живо", + "broadcastNewBroadcast": "Ново емитување во живо", + "broadcastOngoing": "Во тек", + "broadcastUpcoming": "Претстојни", + "broadcastCompleted": "Завршени", + "broadcastRoundNumber": "Заокружен број", + "broadcastFullDescription": "Цел опис на настанот", + "broadcastFullDescriptionHelp": "Незадолжителен, долг опис на емитуваниот настан. {param1} е достапен. Должината мора да е пократка од {param2} знаци.", + "broadcastSourceUrlHelp": "URL кое Lichess ќе го користи за ажурирање на PGN датотеката. Мора да биде јавно достапно на интернет.", + "broadcastStartDateHelp": "Незадолжително, ако знаете кога почнува настанот", "challengeChallengeToPlay": "Предизвикај на Игра", "challengeChallengeDeclined": "Предизвикот е одбиен", "challengeChallengeAccepted": "Предизвикот е прифатен!", @@ -308,7 +320,6 @@ "memory": "Меморија", "infiniteAnalysis": "Бесконечна анализа", "removesTheDepthLimit": "Неограничена длабочина на анализа, го загрева вашиот компјутер", - "engineManager": "Менаџер на компјутерот", "blunder": "Глупа грешка", "mistake": "Грешка", "inaccuracy": "Непрецизност", @@ -599,7 +610,6 @@ "cheat": "Мамење", "troll": "Трол", "other": "Друго", - "reportDescriptionHelp": "Внесете линк од играта/игрите и објаснете каде е проблемот во однесувањето на овој корисник. Немојте само да обвините за мамење, туку објаснете како дојдовте до тој заклучок. Вашата пријава ќе биди разгледана побрзо ако е напишана на англиски јазик.", "error_provideOneCheatedGameLink": "Ве молиме доставете барем една врска до партија со мамење.", "by": "од {param}", "importedByX": "Внесено од {param}", diff --git a/lib/l10n/lila_nb.arb b/lib/l10n/lila_nb.arb index 0b37d7d989..eb27f36f40 100644 --- a/lib/l10n/lila_nb.arb +++ b/lib/l10n/lila_nb.arb @@ -30,7 +30,6 @@ "mobilePuzzleStormConfirmEndRun": "Vil du avslutte denne runden?", "mobilePuzzleStormFilterNothingToShow": "Ingenting her, endre filteret", "mobileCancelTakebackOffer": "Avbryt tilbud om å angre", - "mobileCancelDrawOffer": "Avbryt remistilbud", "mobileWaitingForOpponentToJoin": "Venter på motstanderen ...", "mobileBlindfoldMode": "Blindsjakk", "mobileLiveStreamers": "Direktestrømmere", @@ -39,9 +38,10 @@ "mobileSomethingWentWrong": "Noe gikk galt.", "mobileShowResult": "Vis resultat", "mobilePuzzleThemesSubtitle": "Spill sjakknøtter fra favorittåpningene dine, eller velg et tema.", - "mobilePuzzleStormSubtitle": "Løs så mange sjakknøtter som mulig i løpet av 3 minutter.", - "mobileGreeting": "Hallo, {param}", - "mobileGreetingWithoutName": "Hallo", + "mobilePuzzleStormSubtitle": "Løs så mange sjakknøtter du klarer i løpet av 3 minutter.", + "mobileGreeting": "Hei, {param}", + "mobileGreetingWithoutName": "Hei", + "mobilePrefMagnifyDraggedPiece": "Forstørr brikker når de dras", "activityActivity": "Aktivitet", "activityHostedALiveStream": "Startet en direktestrøm", "activityRankedInSwissTournament": "Ble nummer {param1} i {param2}", @@ -54,6 +54,7 @@ "activityPlayedNbMoves": "{count, plural, =1{Har spilt {count} trekk} other{Har spilt {count} trekk}}", "activityInNbCorrespondenceGames": "{count, plural, =1{i {count} fjernsjakkparti} other{i {count} fjernsjakkpartier}}", "activityCompletedNbGames": "{count, plural, =1{Har spilt ferdig {count} fjernsjakkparti} other{Har spilt ferdig {count} fjernsjakkpartier}}", + "activityCompletedNbVariantGames": "{count, plural, =1{Har spilt ferdig {count} fjernsjakkparti i {param2}} other{Har spilt ferdig {count} fjernsjakkpartier i {param2}}}", "activityFollowedNbPlayers": "{count, plural, =1{Følger {count} spiller} other{Følger {count} spillere}}", "activityGainedNbFollowers": "{count, plural, =1{Har {count} ny følger} other{Har {count} nye følgere}}", "activityHostedNbSimuls": "{count, plural, =1{Har vært vertskap for {count} simultanoppvisning} other{Har vært vertskap for {count} simultanoppvisninger}}", @@ -64,7 +65,72 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{Har deltatt i {count} sveitserturnering} other{Har deltatt i {count} sveitserturneringer}}", "activityJoinedNbTeams": "{count, plural, =1{Er medlem av {count} lag} other{Er medlem av {count} lag}}", "broadcastBroadcasts": "Overføringer", + "broadcastMyBroadcasts": "Mine overføringer", "broadcastLiveBroadcasts": "Direkteoverføringer av turneringer", + "broadcastBroadcastCalendar": "Kalender for overføringer", + "broadcastNewBroadcast": "Ny direkteoverføring", + "broadcastSubscribedBroadcasts": "Overføringer som du abonnerer på", + "broadcastAboutBroadcasts": "Om overføringer", + "broadcastHowToUseLichessBroadcasts": "Hvordan bruke overføringer hos Lichess.", + "broadcastTheNewRoundHelp": "Den nye runden vil ha de samme medlemmene og bidragsyterne som den forrige.", + "broadcastAddRound": "Legg til runde", + "broadcastOngoing": "Pågående", + "broadcastUpcoming": "Kommende", + "broadcastCompleted": "Fullført", + "broadcastCompletedHelp": "Lichess oppdager fullførte runder basert på kildepartiene. Bruk denne knappen hvis det ikke finnes noen kilde.", + "broadcastRoundName": "Rundenavn", + "broadcastRoundNumber": "Rundenummer", + "broadcastTournamentName": "Turneringsnavn", + "broadcastTournamentDescription": "Kort beskrivelse av turneringen", + "broadcastFullDescription": "Full beskrivelse av turneringen", + "broadcastFullDescriptionHelp": "Valgfri lang beskrivelse av turneringen. {param1} er tilgjengelig. Beskrivelsen må være kortere enn {param2} tegn.", + "broadcastSourceSingleUrl": "URL til PGN-kilden", + "broadcastSourceUrlHelp": "Lenke som Lichess vil hente PGN-oppdateringer fra. Den må være offentlig tilgjengelig på internett.", + "broadcastSourceGameIds": "Opptil 64 ID-er for partier hos Lichess. De må være adskilt med mellomrom.", + "broadcastStartDateTimeZone": "Startdato i turneringens lokale tidssone: {param}", + "broadcastStartDateHelp": "Valgfritt, hvis du vet når arrangementet starter", + "broadcastCurrentGameUrl": "URL for dette partiet", + "broadcastDownloadAllRounds": "Last ned alle rundene", + "broadcastResetRound": "Nullstill denne runden", + "broadcastDeleteRound": "Slett denne runden", + "broadcastDefinitivelyDeleteRound": "Slett runden og tilhørende partier ugjenkallelig.", + "broadcastDeleteAllGamesOfThisRound": "Slett alle partiene i denne runden. Kilden må være aktiv for å gjenopprette dem.", + "broadcastEditRoundStudy": "Rediger rundestudie", + "broadcastDeleteTournament": "Slett denne turneringen", + "broadcastDefinitivelyDeleteTournament": "Slett hele turneringen for godt, sammen med alle rundene og alle partiene.", + "broadcastShowScores": "Vis poeng for spillerne basert på resultater av partiene", + "broadcastReplacePlayerTags": "Valgfritt: erstatt spillernavn, ratinger og titler", + "broadcastFideFederations": "FIDE-forbund", + "broadcastTop10Rating": "Topp 10 rating", + "broadcastFidePlayers": "FIDE-spillere", + "broadcastFidePlayerNotFound": "Fant ikke FIDE-spiller", + "broadcastFideProfile": "FIDE-profil", + "broadcastFederation": "Forbund", + "broadcastAgeThisYear": "Alder i år", + "broadcastUnrated": "Uratet", + "broadcastRecentTournaments": "Nylige turneringer", + "broadcastOpenLichess": "Åpne i Lichess", + "broadcastTeams": "Lag", + "broadcastBoards": "Brett", + "broadcastOverview": "Oversikt", + "broadcastSubscribeTitle": "Abonner for å bli varslet når hver runde starter. Du kan velge varselform i kontoinnstillingene dine.", + "broadcastUploadImage": "Last opp bilde for turneringen", + "broadcastNoBoardsYet": "Ingen brett. De vises når partiene er lastet opp.", + "broadcastBoardsCanBeLoaded": "Brett kan lastes med en kilde eller via {param}", + "broadcastStartsAfter": "Starter etter {param}", + "broadcastStartVerySoon": "Overføringen starter straks.", + "broadcastNotYetStarted": "Overføringen har ikke startet.", + "broadcastOfficialWebsite": "Offisiell nettside", + "broadcastStandings": "Resultatliste", + "broadcastIframeHelp": "Flere alternativer på {param}", + "broadcastWebmastersPage": "administratorens side", + "broadcastPgnSourceHelp": "En offentlig PGN-kilde i sanntid for denne runden. Vi tilbyr også en {param} for raskere og mer effektiv synkronisering.", + "broadcastEmbedThisBroadcast": "Bygg inn denne overføringen på nettstedet ditt", + "broadcastEmbedThisRound": "Bygg inn {param} på nettstedet ditt", + "broadcastRatingDiff": "Ratingdifferanse", + "broadcastGamesThisTournament": "Partier i denne turneringen", + "broadcastScore": "Poengsum", + "broadcastNbBroadcasts": "{count, plural, =1{{count} overføring} other{{count} overføringer}}", "challengeChallengesX": "Utfordringer: {param1}", "challengeChallengeToPlay": "Utfordre til et parti", "challengeChallengeDeclined": "Utfordring avslått", @@ -383,8 +449,8 @@ "puzzleThemeXRayAttackDescription": "En brikke angriper eller dekker et felt indirekte, gjennom en av motstanderens brikker.", "puzzleThemeZugzwang": "Trekktvang", "puzzleThemeZugzwangDescription": "Motstanderen kan bare utføre trekk som forverrer egen stilling.", - "puzzleThemeHealthyMix": "Frisk blanding", - "puzzleThemeHealthyMixDescription": "Litt av alt. Du vet ikke hva du får, så du er klar for alt! Akkurat som i virkelige partier.", + "puzzleThemeMix": "Frisk blanding", + "puzzleThemeMixDescription": "Litt av alt. Du vet ikke hva du får, så du er klar for alt! Akkurat som i virkelige partier.", "puzzleThemePlayerGames": "Spillerpartier", "puzzleThemePlayerGamesDescription": "Finn sjakknøtter generert fra dine eller andres partier.", "puzzleThemePuzzleDownloadInformation": "Disse sjakknøttene er offentlig eiendom og kan lastes ned fra {param}.", @@ -515,7 +581,6 @@ "memory": "Minne", "infiniteAnalysis": "Uendelig analyse", "removesTheDepthLimit": "Fjerner dybdebegrensning, og holder maskinen din varm", - "engineManager": "Innstillinger for sjakkmotorer", "blunder": "Bukk", "mistake": "Feil", "inaccuracy": "Unøyaktighet", @@ -819,7 +884,9 @@ "cheat": "Juks", "troll": "Troll", "other": "Annet", - "reportDescriptionHelp": "Kopier lenken til partiet/partiene og forklar hva som er galt med denne brukerens oppførsel.", + "reportCheatBoostHelp": "Kopier lenken til partiet/partiene og forklar hva som er galt med denne brukerens oppførsel. Skriv en utdypende begrunnelse, ikke bare «vedkommende jukser».", + "reportUsernameHelp": "Forklar hvorfor brukernavnet er støtende. Skriv en utdypende begrunnelse, ikke bare «det er støtende/upassende». Dette gjelder særlig hvis fornærmelsen er tilslørt, ikke er på engelsk, er et slanguttrykk eller er en historisk/kulturell referanse.", + "reportProcessedFasterInEnglish": "Rapporten din blir behandlet raskere hvis den er skrevet på engelsk.", "error_provideOneCheatedGameLink": "Oppgi minst én lenke til et jukseparti.", "by": "av {param}", "importedByX": "Importert av {param}", @@ -1217,6 +1284,7 @@ "showMeEverything": "Vis meg alt", "lichessPatronInfo": "Lichess er en ideell forening, basert på fri programvare med åpen kildekode.\nAlle kostnader for drift, utvikling og innhold finansieres utelukkende av brukerbidrag.", "nothingToSeeHere": "Ingenting her for nå.", + "stats": "Statistikk", "opponentLeftCounter": "{count, plural, =1{Motspilleren din har forlatt partiet. Du kan kreve seier om {count} sekund.} other{Motspilleren din har forlatt partiet. Du kan kreve seier om {count} sekunder.}}", "mateInXHalfMoves": "{count, plural, =1{Matt om {count} halvtrekk} other{Matt om {count} halvtrekk}}", "nbBlunders": "{count, plural, =1{{count} bukk} other{{count} bukker}}", @@ -1313,6 +1381,159 @@ "stormXRuns": "{count, plural, =1{1 runde} other{{count} runder}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Har spilt én runde med {param2}} other{Har spilt {count} runder med {param2}}}", "streamerLichessStreamers": "Lichess-strømmere", + "studyPrivate": "Privat", + "studyMyStudies": "Mine studier", + "studyStudiesIContributeTo": "Studier jeg bidrar til", + "studyMyPublicStudies": "Mine offentlige studier", + "studyMyPrivateStudies": "Mine private studier", + "studyMyFavoriteStudies": "Mine favorittstudier", + "studyWhatAreStudies": "Hva er studier?", + "studyAllStudies": "Alle studier", + "studyStudiesCreatedByX": "Studier opprettet av {param}", + "studyNoneYet": "Ingen så langt.", + "studyHot": "Hett", + "studyDateAddedNewest": "Dato tilføyd (nyeste)", + "studyDateAddedOldest": "Dato tilføyd (eldste)", + "studyRecentlyUpdated": "Nylig oppdatert", + "studyMostPopular": "Mest populære", + "studyAlphabetical": "Alfabetisk", + "studyAddNewChapter": "Legg til kapittel", + "studyAddMembers": "Legg til medlemmer", + "studyInviteToTheStudy": "Inviter til studien", + "studyPleaseOnlyInvitePeopleYouKnow": "Inviter bare folk du kjenner som ønsker å delta i studien.", + "studySearchByUsername": "Søk på brukernavn", + "studySpectator": "Tilskuer", + "studyContributor": "Bidragsyter", + "studyKick": "Kast ut", + "studyLeaveTheStudy": "Forlat studien", + "studyYouAreNowAContributor": "Du er nå bidragsyter", + "studyYouAreNowASpectator": "Du er nå tilskuer", + "studyPgnTags": "PGN-merkelapper", + "studyLike": "Lik", + "studyUnlike": "Slutt å like", + "studyNewTag": "Ny merkelapp", + "studyCommentThisPosition": "Kommenter denne stillingen", + "studyCommentThisMove": "Kommenter dette trekket", + "studyAnnotateWithGlyphs": "Kommenter med symboler", + "studyTheChapterIsTooShortToBeAnalysed": "Kapittelet er for kort for analyse.", + "studyOnlyContributorsCanRequestAnalysis": "Bare bidragsyterne til studien kan be om maskinanalyse.", + "studyGetAFullComputerAnalysis": "Få full maskinanalyse av hovedvarianten fra serveren.", + "studyMakeSureTheChapterIsComplete": "Sørg for at kapittelet er fullført. Du kan bare be om analyse én gang.", + "studyAllSyncMembersRemainOnTheSamePosition": "Alle synkroniserte medlemmer ser den samme stillingen", + "studyShareChanges": "Del endringer med tilskuere og lagre dem på serveren", + "studyPlaying": "Pågår", + "studyShowEvalBar": "Evalueringssøyler", + "studyFirst": "Første", + "studyPrevious": "Forrige", + "studyNext": "Neste", + "studyLast": "Siste", "studyShareAndExport": "Del og eksporter", - "studyStart": "Start" + "studyCloneStudy": "Klon", + "studyStudyPgn": "Studie-PGN", + "studyDownloadAllGames": "Last ned alle partiene", + "studyChapterPgn": "Kapittel-PGN", + "studyCopyChapterPgn": "Kopier PGN", + "studyDownloadGame": "Last ned partiet", + "studyStudyUrl": "Studie-URL", + "studyCurrentChapterUrl": "Kapittel-URL", + "studyYouCanPasteThisInTheForumToEmbed": "Du kan lime inn dette i forumet for å bygge det inn der", + "studyStartAtInitialPosition": "Start ved innledende stilling", + "studyStartAtX": "Start ved {param}", + "studyEmbedInYourWebsite": "Bygg inn på nettstedet ditt eller bloggen din", + "studyReadMoreAboutEmbedding": "Les mer om å bygge inn", + "studyOnlyPublicStudiesCanBeEmbedded": "Bare offentlige studier kan bygges inn!", + "studyOpen": "Åpne", + "studyXBroughtToYouByY": "{param1} presentert av {param2}", + "studyStudyNotFound": "Fant ikke studien", + "studyEditChapter": "Rediger kapittel", + "studyNewChapter": "Nytt kapittel", + "studyImportFromChapterX": "Importer fra {param}", + "studyOrientation": "Retning", + "studyAnalysisMode": "Analysemodus", + "studyPinnedChapterComment": "Fastspikrede kapittelkommenter", + "studySaveChapter": "Lagre kapittelet", + "studyClearAnnotations": "Fjern notater", + "studyClearVariations": "Fjern varianter", + "studyDeleteChapter": "Slett kapittel", + "studyDeleteThisChapter": "Slette dette kapittelet? Du kan ikke angre!", + "studyClearAllCommentsInThisChapter": "Fjern alle kommentarer og figurer i dette kapittelet?", + "studyRightUnderTheBoard": "Rett under brettet", + "studyNoPinnedComment": "Ingen", + "studyNormalAnalysis": "Normal analyse", + "studyHideNextMoves": "Skjul neste trekk", + "studyInteractiveLesson": "Interaktiv leksjon", + "studyChapterX": "Kapittel {param}", + "studyEmpty": "Tom", + "studyStartFromInitialPosition": "Start ved innledende stilling", + "studyEditor": "Editor", + "studyStartFromCustomPosition": "Start fra innledende stilling", + "studyLoadAGameByUrl": "Last inn partier fra URL-er", + "studyLoadAPositionFromFen": "Last inn en stilling fra FEN", + "studyLoadAGameFromPgn": "Last inn partier fra PGN", + "studyAutomatic": "Automatisk", + "studyUrlOfTheGame": "URL for partiene, én pr. linje", + "studyLoadAGameFromXOrY": "Last inn partier fra {param1} eller {param2}", + "studyCreateChapter": "Opprett kapittel", + "studyCreateStudy": "Opprett en studie", + "studyEditStudy": "Rediger studie", + "studyVisibility": "Synlighet", + "studyPublic": "Offentlig", + "studyUnlisted": "Ikke listet", + "studyInviteOnly": "Bare etter invitasjon", + "studyAllowCloning": "Tillat kloning", + "studyNobody": "Ingen", + "studyOnlyMe": "Bare meg", + "studyContributors": "Bidragsytere", + "studyMembers": "Medlemmer", + "studyEveryone": "Alle", + "studyEnableSync": "Aktiver synkronisering", + "studyYesKeepEveryoneOnTheSamePosition": "Ja: behold alle i samme stilling", + "studyNoLetPeopleBrowseFreely": "Nei: la folk se fritt gjennom", + "studyPinnedStudyComment": "Fastspikrede studiekommentarer", + "studyStart": "Start", + "studySave": "Lagre", + "studyClearChat": "Fjern samtalen", + "studyDeleteTheStudyChatHistory": "Slette studiens samtalehistorikk? Du kan ikke angre!", + "studyDeleteStudy": "Slett studie", + "studyConfirmDeleteStudy": "Slette hele studien? Du kan ikke angre! Bekreft ved å skrive inn navnet på studien: {param}", + "studyWhereDoYouWantToStudyThat": "Hvilken studie vil du bruke?", + "studyGoodMove": "Godt trekk", + "studyMistake": "Feil", + "studyBrilliantMove": "Strålende trekk", + "studyBlunder": "Bukk", + "studyInterestingMove": "Interessant trekk", + "studyDubiousMove": "Tvilsomt trekk", + "studyOnlyMove": "Eneste trekk", + "studyZugzwang": "Trekktvang", + "studyEqualPosition": "Lik stilling", + "studyUnclearPosition": "Uavklart stilling", + "studyWhiteIsSlightlyBetter": "Hvit står litt bedre", + "studyBlackIsSlightlyBetter": "Svart står litt bedre", + "studyWhiteIsBetter": "Hvit står bedre", + "studyBlackIsBetter": "Svart står bedre", + "studyWhiteIsWinning": "Hvit står til vinst", + "studyBlackIsWinning": "Svart står til vinst", + "studyNovelty": "Nyvinning", + "studyDevelopment": "Utvikling", + "studyInitiative": "Initiativ", + "studyAttack": "Angrep", + "studyCounterplay": "Motspill", + "studyTimeTrouble": "Tidsnød", + "studyWithCompensation": "Med kompensasjon", + "studyWithTheIdea": "Med ideen", + "studyNextChapter": "Neste kapittel", + "studyPrevChapter": "Forrige kapittel", + "studyStudyActions": "Studiehandlinger", + "studyTopics": "Emner", + "studyMyTopics": "Mine emner", + "studyPopularTopics": "Populære emner", + "studyManageTopics": "Administrer emner", + "studyBack": "Tilbake", + "studyPlayAgain": "Spill igjen", + "studyWhatWouldYouPlay": "Hva vil du spille i denne stillingen?", + "studyYouCompletedThisLesson": "Gratulerer! Du har fullført denne leksjonen.", + "studyNbChapters": "{count, plural, =1{{count} kapittel} other{{count} kapitler}}", + "studyNbGames": "{count, plural, =1{{count} parti} other{{count} partier}}", + "studyNbMembers": "{count, plural, =1{{count} medlem} other{{count} medlemmer}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Sett inn PGN-teksten din her, maksimum {count} parti} other{Sett inn PGN-teksten din her, maksimum {count} partier}}" } \ No newline at end of file diff --git a/lib/l10n/lila_nl.arb b/lib/l10n/lila_nl.arb index f2e10f73dc..a9fab4dd7b 100644 --- a/lib/l10n/lila_nl.arb +++ b/lib/l10n/lila_nl.arb @@ -10,6 +10,7 @@ "mobileOkButton": "OK", "mobileSettingsHapticFeedback": "Haptische feedback", "mobileSettingsImmersiveMode": "Volledig scherm-modus", + "mobileSettingsImmersiveModeSubtitle": "Systeem-UI verbergen tijdens het spelen. Gebruik dit als je last hebt van de navigatiegebaren aan de randen van het scherm. Dit is van toepassing op spel- en Puzzle Storm schermen.", "mobileNotFollowingAnyUser": "U volgt geen gebruiker.", "mobileAllGames": "Alle partijen", "mobileRecentSearches": "Recente zoekopdrachten", @@ -26,9 +27,11 @@ "mobileShowVariations": "Toon varianten", "mobileHideVariation": "Verberg varianten", "mobileShowComments": "Opmerkingen weergeven", - "mobilePuzzleStormConfirmEndRun": "Wil je dit uitvoeren beëindigen?", - "mobileCancelDrawOffer": "Remiseaanbod intrekken", + "mobilePuzzleStormConfirmEndRun": "Wil je deze reeks beëindigen?", + "mobilePuzzleStormFilterNothingToShow": "Niets te tonen, wijzig de filters", + "mobileCancelTakebackOffer": "Terugnameaanbod annuleren", "mobileWaitingForOpponentToJoin": "Wachten op een tegenstander...", + "mobileBlindfoldMode": "Geblinddoekt", "mobileLiveStreamers": "Live streamers", "mobileCustomGameJoinAGame": "Een partij beginnen", "mobileCorrespondenceClearSavedMove": "Opgeslagen zet wissen", @@ -38,18 +41,20 @@ "mobilePuzzleStormSubtitle": "Los zoveel mogelijk puzzels op in 3 minuten.", "mobileGreeting": "Hallo, {param}", "mobileGreetingWithoutName": "Hallo", + "mobilePrefMagnifyDraggedPiece": "Versleept stuk vergroot weergeven", "activityActivity": "Activiteit", "activityHostedALiveStream": "Heeft een live stream gehost", "activityRankedInSwissTournament": "Eindigde #{param1} in {param2}", "activitySignedUp": "Geregistreerd op lichess.org", "activitySupportedNbMonths": "{count, plural, =1{Steunde lichess.org voor {count} maand als {param2}} other{Steunde lichess.org voor {count} maanden als {param2}}}", - "activityPracticedNbPositions": "{count, plural, =1{Beoefende {count} positie van {param2}} other{Beoefende {count} posities van {param2}}}", + "activityPracticedNbPositions": "{count, plural, =1{Oefende {count} positie van {param2}} other{Oefende {count} posities van {param2}}}", "activitySolvedNbPuzzles": "{count, plural, =1{{count} tactische puzzel opgelost} other{{count} tactische puzzels opgelost}}", "activityPlayedNbGames": "{count, plural, =1{Speelde {count} {param2} partij} other{Speelde {count} {param2} partijen}}", "activityPostedNbMessages": "{count, plural, =1{Plaatste {count} bericht in {param2}} other{Plaatste {count} berichten in {param2}}}", "activityPlayedNbMoves": "{count, plural, =1{Speelde {count} zet} other{Speelde {count} zet}}", "activityInNbCorrespondenceGames": "{count, plural, =1{in {count} correspondentiepartij} other{in {count} correspondentiepartijen}}", "activityCompletedNbGames": "{count, plural, =1{Voltooide {count} correspondentiepartijen} other{Voltooide {count} correspondentiepartijen}}", + "activityCompletedNbVariantGames": "{count, plural, =1{Voltooide {count} {param2} correspondentiepartijen} other{Voltooide {count} {param2} correspondentiepartijen}}", "activityFollowedNbPlayers": "{count, plural, =1{Begon {count} speler te volgen} other{Begon {count} spelers te volgen}}", "activityGainedNbFollowers": "{count, plural, =1{{count} nieuwe volger verworven} other{{count} nieuwe volgers verworven}}", "activityHostedNbSimuls": "{count, plural, =1{Gaf {count} simultaan} other{Gaf {count} simultanen}}", @@ -60,7 +65,71 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{Nam deel aan {count} Zwitsers toernooi} other{Nam deel aan {count} Zwitserse toernooien}}", "activityJoinedNbTeams": "{count, plural, =1{Sloot zich aan bij {count} team} other{Sloot zich aan bij {count} teams}}", "broadcastBroadcasts": "Uitzendingen", + "broadcastMyBroadcasts": "Mijn uitzendingen", "broadcastLiveBroadcasts": "Live toernooi uitzendingen", + "broadcastBroadcastCalendar": "Uitzendkalender", + "broadcastNewBroadcast": "Nieuwe live uitzending", + "broadcastAboutBroadcasts": "Over uitzending", + "broadcastHowToUseLichessBroadcasts": "Hoe Lichess Uitzendingen te gebruiken.", + "broadcastTheNewRoundHelp": "De nieuwe ronde zal dezelfde leden en bijdragers hebben als de vorige.", + "broadcastAddRound": "Ronde toevoegen", + "broadcastOngoing": "Lopend", + "broadcastUpcoming": "Aankomend", + "broadcastCompleted": "Voltooid", + "broadcastCompletedHelp": "Lichess detecteert voltooiing van de ronde op basis van de bronpartijen. Gebruik deze schakelaar als er geen bron is.", + "broadcastRoundName": "Naam ronde", + "broadcastRoundNumber": "Ronde", + "broadcastTournamentName": "Naam toernooi", + "broadcastTournamentDescription": "Korte toernooibeschrijving", + "broadcastFullDescription": "Volledige beschrijving evenement", + "broadcastFullDescriptionHelp": "Optionele lange beschrijving van de uitzending. {param1} is beschikbaar. Totale lengte moet minder zijn dan {param2} tekens.", + "broadcastSourceSingleUrl": "URL van PGN-bron", + "broadcastSourceUrlHelp": "Link die Lichess gebruikt om PGN updates te krijgen. Deze moet openbaar toegankelijk zijn via internet.", + "broadcastSourceGameIds": "Tot 64 Lichess partij-ID''s, gescheiden door spaties.", + "broadcastStartDateTimeZone": "Startdatum in de lokale tijdzone van het tornooi: {param}", + "broadcastStartDateHelp": "Optioneel, als je weet wanneer het evenement start", + "broadcastCurrentGameUrl": "Huidige partij-link", + "broadcastDownloadAllRounds": "Alle rondes downloaden", + "broadcastResetRound": "Deze ronde opnieuw instellen", + "broadcastDeleteRound": "Deze ronde verwijderen", + "broadcastDefinitivelyDeleteRound": "Deze ronde en bijbehorende partijen definitief verwijderen.", + "broadcastDeleteAllGamesOfThisRound": "Alle partijen van deze ronde verwijderen. De bron zal actief moeten zijn om ze opnieuw te maken.", + "broadcastEditRoundStudy": "Studieronde bewerken", + "broadcastDeleteTournament": "Verwijder dit toernooi", + "broadcastDefinitivelyDeleteTournament": "Verwijder definitief het hele toernooi, inclusief alle rondes en partijen.", + "broadcastShowScores": "Toon scores van spelers op basis van partij-uitslagen", + "broadcastReplacePlayerTags": "Optioneel: vervang spelersnamen, beoordelingen en titels", + "broadcastFideFederations": "FIDE-federaties", + "broadcastTop10Rating": "Top 10-rating", + "broadcastFidePlayers": "FIDE-spelers", + "broadcastFidePlayerNotFound": "FIDE-speler niet gevonden", + "broadcastFideProfile": "FIDE-profiel", + "broadcastFederation": "Federatie", + "broadcastAgeThisYear": "Leeftijd dit jaar", + "broadcastUnrated": "Zonder rating", + "broadcastRecentTournaments": "Recente toernooien", + "broadcastOpenLichess": "Openen in Lichess", + "broadcastTeams": "Teams", + "broadcastBoards": "Borden", + "broadcastOverview": "Overzicht", + "broadcastSubscribeTitle": "Krijg een melding wanneer elke ronde start. Je kunt bel- of pushmeldingen voor uitzendingen in je accountvoorkeuren in-/uitschakelen.", + "broadcastUploadImage": "Toernooifoto uploaden", + "broadcastNoBoardsYet": "Nog geen borden. Deze zullen verschijnen van zodra er partijen worden geüpload.", + "broadcastBoardsCanBeLoaded": "Borden kunnen geladen worden met een bron of via de {param}", + "broadcastStartsAfter": "Start na {param}", + "broadcastStartVerySoon": "De uitzending begint binnenkort.", + "broadcastNotYetStarted": "De uitzending is nog niet begonnen.", + "broadcastOfficialWebsite": "Officiële website", + "broadcastStandings": "Klassement", + "broadcastIframeHelp": "Meer opties voor de {param}", + "broadcastWebmastersPage": "pagina van de webmaster", + "broadcastPgnSourceHelp": "Een publieke real-time PGN-bron voor deze ronde. We bieden ook een {param} aan voor een snellere en efficiëntere synchronisatie.", + "broadcastEmbedThisBroadcast": "Deze uitzending insluiten in je website", + "broadcastEmbedThisRound": "{param} insluiten in je website", + "broadcastRatingDiff": "Ratingverschil", + "broadcastGamesThisTournament": "Partijen in dit toernooi", + "broadcastScore": "Score", + "broadcastNbBroadcasts": "{count, plural, =1{{count} uitzending} other{{count} uitzendingen}}", "challengeChallengesX": "Uitdagingen: {param1}", "challengeChallengeToPlay": "Uitdagen voor een partij", "challengeChallengeDeclined": "Uitdaging geweigerd", @@ -379,8 +448,8 @@ "puzzleThemeXRayAttackDescription": "Een stuk valt een veld aan of verdedigt een veld, door een vijandelijk stuk heen.", "puzzleThemeZugzwang": "Zetdwang", "puzzleThemeZugzwangDescription": "De tegenstander is beperkt in de zetten die hij kan doen, en elke zet verslechtert zijn stelling.", - "puzzleThemeHealthyMix": "Gezonde mix", - "puzzleThemeHealthyMixDescription": "Van alles wat. Je weet niet wat je te wachten staat, je moet dus op alles voorbereid zijn! Net als in echte partijen.", + "puzzleThemeMix": "Gezonde mix", + "puzzleThemeMixDescription": "Van alles wat. Je weet niet wat je te wachten staat, je moet dus op alles voorbereid zijn! Net als in echte partijen.", "puzzleThemePlayerGames": "Eigen partijen", "puzzleThemePlayerGamesDescription": "Zoek puzzels gegenereerd uit jouw partijen, of uit partijen van een andere speler.", "puzzleThemePuzzleDownloadInformation": "Deze puzzels zijn beschikbaar in het publieke domein en kunnen worden gedownload op {param}.", @@ -511,7 +580,6 @@ "memory": "Geheugen", "infiniteAnalysis": "Oneindige analyse", "removesTheDepthLimit": "Verwijdert de dieptelimiet, en houdt je computer warm", - "engineManager": "Engine-beheer", "blunder": "Blunder", "mistake": "Fout", "inaccuracy": "Onnauwkeurigheid", @@ -593,6 +661,7 @@ "rank": "Positie", "rankX": "Positie: {param}", "gamesPlayed": "Gespeelde partijen", + "ok": "Oké", "cancel": "Annuleren", "whiteTimeOut": "Tijd om voor wit", "blackTimeOut": "Tijd om voor zwart", @@ -814,7 +883,9 @@ "cheat": "Valsspelen", "troll": "Provoceren", "other": "Anders", - "reportDescriptionHelp": "Plak de link naar de partij(en) en leg uit wat er mis is met het gedrag van de gebruiker. Zeg niet alleen 'hij speelt vals', maar vertel ons hoe u bent gekomen op deze conclusie. Uw rapportage zal sneller worden verwerkt als het in het Engels is geschreven.", + "reportCheatBoostHelp": "Plak de link naar de partij(en) en leg uit wat er mis is met het gedrag van de gebruiker. Zeg niet alleen 'hij speelt vals', maar leg ook uit hoe je tot deze conclusie komt.", + "reportUsernameHelp": "Leg uit wat er aan deze gebruikersnaam beledigend is. Zeg niet gewoon \"het is aanstootgevend/ongepast\", maar vertel ons hoe je tot deze conclusie komt, vooral als de belediging verhuld wordt, niet in het Engels is, in dialect is, of een historische of culturele verwijzing is.", + "reportProcessedFasterInEnglish": "Je melding wordt sneller verwerkt als deze in het Engels is geschreven.", "error_provideOneCheatedGameLink": "Geef ten minste één link naar een partij waarin vals gespeeld is.", "by": "door {param}", "importedByX": "Geïmporteerd door {param}", @@ -1212,6 +1283,7 @@ "showMeEverything": "Alles tonen", "lichessPatronInfo": "Lichess is een organisatie zonder winstoogmerk en is volledig open en gratis (libre).\nAlle exploitatiekosten, ontwikkeling en inhoud worden enkel gefinancierd door donaties van gebruikers.", "nothingToSeeHere": "Hier is momenteel niets te zien.", + "stats": "Statistieken", "opponentLeftCounter": "{count, plural, =1{Je tegenstander heeft het spel verlaten. Je kan de overwinning opeisen over {count} seconde.} other{Je tegenstander speelt niet verder. Je kunt de overwinning opeisen over {count} seconden.}}", "mateInXHalfMoves": "{count, plural, =1{Schaakmat in {count} halfzet} other{Schaakmat in {count} halfzetten}}", "nbBlunders": "{count, plural, =1{{count} blunder} other{{count} blunders}}", @@ -1308,6 +1380,159 @@ "stormXRuns": "{count, plural, =1{1 sessie} other{{count} sessies}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Eén sessie {param2} gespeeld} other{{count} sessies {param2} gespeeld}}", "streamerLichessStreamers": "Lichess streamers", + "studyPrivate": "Privé", + "studyMyStudies": "Mijn Studies", + "studyStudiesIContributeTo": "Studies waaraan ik bijdraag", + "studyMyPublicStudies": "Mijn openbare studies", + "studyMyPrivateStudies": "Mijn privé studies", + "studyMyFavoriteStudies": "Mijn favoriete studies", + "studyWhatAreStudies": "Wat zijn studies?", + "studyAllStudies": "Alle studies", + "studyStudiesCreatedByX": "Studies gemaakt door {param}", + "studyNoneYet": "Nog geen...", + "studyHot": "Populair", + "studyDateAddedNewest": "Datum toegevoegd (nieuwste)", + "studyDateAddedOldest": "Datum toegevoegd (oudste)", + "studyRecentlyUpdated": "Recent bijgewerkt", + "studyMostPopular": "Meest populair", + "studyAlphabetical": "Alfabetisch", + "studyAddNewChapter": "Nieuw hoofdstuk toevoegen", + "studyAddMembers": "Deelnemers toevoegen", + "studyInviteToTheStudy": "Uitnodigen voor de studie", + "studyPleaseOnlyInvitePeopleYouKnow": "Nodig alleen deelnemers uit die jou kennen en actief mee willen doen aan deze studie.", + "studySearchByUsername": "Zoeken op gebruikersnaam", + "studySpectator": "Kijker", + "studyContributor": "Bijdrager", + "studyKick": "Verwijder", + "studyLeaveTheStudy": "Verlaat de studie", + "studyYouAreNowAContributor": "Je bent nu een bijdrager", + "studyYouAreNowASpectator": "Je bent nu een toeschouwer", + "studyPgnTags": "PGN labels", + "studyLike": "Vind ik leuk", + "studyUnlike": "Vind ik niet meer leuk", + "studyNewTag": "Nieuw label", + "studyCommentThisPosition": "Reageer op deze positie", + "studyCommentThisMove": "Reageer op deze zet", + "studyAnnotateWithGlyphs": "Maak aantekeningen met symbolen", + "studyTheChapterIsTooShortToBeAnalysed": "Dit hoofdstuk is te kort om geanalyseerd te worden.", + "studyOnlyContributorsCanRequestAnalysis": "Alleen de bijdragers kunnen een computer analyse aanvragen.", + "studyGetAFullComputerAnalysis": "Krijg een volledige computer analyse van de hoofdlijn.", + "studyMakeSureTheChapterIsComplete": "Zorg ervoor dat het hoofdstuk voltooid is. Je kunt slechts één keer een analyse aanvragen.", + "studyAllSyncMembersRemainOnTheSamePosition": "Alle SYNC leden blijven op dezelfde positie", + "studyShareChanges": "Deel veranderingen met toeschouwers en sla deze op op de server", + "studyPlaying": "Spelend", + "studyShowEvalBar": "Evaluatiebalk", + "studyFirst": "Eerste", + "studyPrevious": "Vorige", + "studyNext": "Volgende", + "studyLast": "Laatste", "studyShareAndExport": "Deel & exporteer", - "studyStart": "Start" + "studyCloneStudy": "Kopiëren", + "studyStudyPgn": "PGN bestuderen", + "studyDownloadAllGames": "Download alle partijen", + "studyChapterPgn": "Hoofdstuk PGN", + "studyCopyChapterPgn": "PGN kopiëren", + "studyDownloadGame": "Partij downloaden", + "studyStudyUrl": "Studie URL", + "studyCurrentChapterUrl": "Huidige hoofdstuk URL", + "studyYouCanPasteThisInTheForumToEmbed": "Je kunt deze link plakken wanneer je een bericht schrijft op het forum om de partij interactief weer te geven", + "studyStartAtInitialPosition": "Begin bij de startpositie", + "studyStartAtX": "Beginnen bij {param}", + "studyEmbedInYourWebsite": "Insluiten in blog of website", + "studyReadMoreAboutEmbedding": "Lees meer over insluiten", + "studyOnlyPublicStudiesCanBeEmbedded": "Alleen openbare studies kunnen worden ingevoegd!", + "studyOpen": "Open", + "studyXBroughtToYouByY": "{param1} aangeboden door {param2}", + "studyStudyNotFound": "Studie niet gevonden", + "studyEditChapter": "Hoofdstuk bewerken", + "studyNewChapter": "Nieuw hoofdstuk", + "studyImportFromChapterX": "Importeren van {param}", + "studyOrientation": "Oriëntatie", + "studyAnalysisMode": "Analysemodus", + "studyPinnedChapterComment": "Vastgezet commentaar van het hoofdstuk", + "studySaveChapter": "Hoofdstuk opslaan", + "studyClearAnnotations": "Wis aantekeningen", + "studyClearVariations": "Verwijder variaties", + "studyDeleteChapter": "Verwijder hoofdstuk", + "studyDeleteThisChapter": "Wil je dit hoofdstuk verwijderen? Je kan dit niet ongedaan maken!", + "studyClearAllCommentsInThisChapter": "Verwijder alle aantekeningen, tekens en getekende figuren in dit hoofdstuk?", + "studyRightUnderTheBoard": "Recht onder het bord", + "studyNoPinnedComment": "Geen", + "studyNormalAnalysis": "Normale analyse", + "studyHideNextMoves": "Verberg volgende zetten", + "studyInteractiveLesson": "Interactieve les", + "studyChapterX": "Hoofdstuk {param}", + "studyEmpty": "Leeg", + "studyStartFromInitialPosition": "Start bij de initiële positie", + "studyEditor": "Editor", + "studyStartFromCustomPosition": "Start bij een aangepaste positie", + "studyLoadAGameByUrl": "Laad partijen via een URL", + "studyLoadAPositionFromFen": "Laad een spel via een FEN", + "studyLoadAGameFromPgn": "Laad partijen via een PGN", + "studyAutomatic": "Automatisch", + "studyUrlOfTheGame": "URL van de partijen, één per regel", + "studyLoadAGameFromXOrY": "Laad partijen van {param1} of {param2}", + "studyCreateChapter": "Creëer hoofdstuk", + "studyCreateStudy": "Maak studie", + "studyEditStudy": "Bewerk studie", + "studyVisibility": "Zichtbaarheid", + "studyPublic": "Openbaar", + "studyUnlisted": "Niet openbaar", + "studyInviteOnly": "Alleen op uitnodiging", + "studyAllowCloning": "Klonen toestaan", + "studyNobody": "Niemand", + "studyOnlyMe": "Alleen ik", + "studyContributors": "Bijdragers", + "studyMembers": "Deelnemers", + "studyEveryone": "Iedereen", + "studyEnableSync": "Synchronisatie inschakelen", + "studyYesKeepEveryoneOnTheSamePosition": "Ja: houd iedereen op dezelfde stelling", + "studyNoLetPeopleBrowseFreely": "Nee: laat mensen vrij bladeren", + "studyPinnedStudyComment": "Vastgezette studie reactie", + "studyStart": "Start", + "studySave": "Opslaan", + "studyClearChat": "Maak de chat leeg", + "studyDeleteTheStudyChatHistory": "Verwijder de studiechat geschiedenis? Er is geen weg terug!", + "studyDeleteStudy": "Studie verwijderen", + "studyConfirmDeleteStudy": "De hele studie verwijderen? Er is geen weg terug! Type de naam van de studie om te bevestigen dat je de studie wilt verwijderen: {param}", + "studyWhereDoYouWantToStudyThat": "Waar wil je dat bestuderen?", + "studyGoodMove": "Goede zet", + "studyMistake": "Fout", + "studyBrilliantMove": "Briljante zet", + "studyBlunder": "Blunder", + "studyInterestingMove": "Interessante zet", + "studyDubiousMove": "Dubieuze zet", + "studyOnlyMove": "Enig mogelijke zet", + "studyZugzwang": "Zetdwang", + "studyEqualPosition": "Stelling in evenwicht", + "studyUnclearPosition": "Onduidelijke stelling", + "studyWhiteIsSlightlyBetter": "Wit staat iets beter", + "studyBlackIsSlightlyBetter": "Zwart staat iets beter", + "studyWhiteIsBetter": "Wit staat beter", + "studyBlackIsBetter": "Zwart staat beter", + "studyWhiteIsWinning": "Wit staat gewonnen", + "studyBlackIsWinning": "Zwart staat gewonnen", + "studyNovelty": "Noviteit", + "studyDevelopment": "Ontwikkeling", + "studyInitiative": "Initiatief", + "studyAttack": "Aanval", + "studyCounterplay": "Tegenspel", + "studyTimeTrouble": "Tijdnood", + "studyWithCompensation": "Met compensatie", + "studyWithTheIdea": "Met het idee", + "studyNextChapter": "Volgende hoofdstuk", + "studyPrevChapter": "Vorige hoofdstuk", + "studyStudyActions": "Studie sneltoetsen", + "studyTopics": "Onderwerpen", + "studyMyTopics": "Mijn onderwerpen", + "studyPopularTopics": "Populaire onderwerpen", + "studyManageTopics": "Onderwerpen beheren", + "studyBack": "Terug", + "studyPlayAgain": "Opnieuw spelen", + "studyWhatWouldYouPlay": "Wat zou je in deze stelling spelen?", + "studyYouCompletedThisLesson": "Gefeliciteerd! Je hebt deze les voltooid.", + "studyNbChapters": "{count, plural, =1{{count} hoofdstuk} other{{count} hoofdstukken}}", + "studyNbGames": "{count, plural, =1{{count} Partij} other{{count} Partijen}}", + "studyNbMembers": "{count, plural, =1{{count} Deelnemer} other{{count} Deelnemers}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Plak je PGN tekst hier, tot {count} spel mogelijk} other{Plak je PGN tekst hier, tot {count} spellen mogelijk}}" } \ No newline at end of file diff --git a/lib/l10n/lila_nn.arb b/lib/l10n/lila_nn.arb index b6b613a009..219c3a6d6f 100644 --- a/lib/l10n/lila_nn.arb +++ b/lib/l10n/lila_nn.arb @@ -30,7 +30,6 @@ "mobilePuzzleStormConfirmEndRun": "Vil du avslutte dette løpet?", "mobilePuzzleStormFilterNothingToShow": "Ikkje noko å syna, ver venleg å endre filtera", "mobileCancelTakebackOffer": "Avbryt tilbud om angrerett", - "mobileCancelDrawOffer": "Avbryt remistilbud", "mobileWaitingForOpponentToJoin": "Ventar på motspelar...", "mobileBlindfoldMode": "Blindsjakk", "mobileLiveStreamers": "Direkte strøymarar", @@ -42,6 +41,7 @@ "mobilePuzzleStormSubtitle": "Løys så mange oppgåver som du maktar på tre minutt.", "mobileGreeting": "Hei {param}", "mobileGreetingWithoutName": "Hei", + "mobilePrefMagnifyDraggedPiece": "Forstørr brikke som vert trekt", "activityActivity": "Aktivitet", "activityHostedALiveStream": "Starta en direktestraum", "activityRankedInSwissTournament": "Vart nr. {param1} i {param2}", @@ -54,8 +54,9 @@ "activityPlayedNbMoves": "{count, plural, =1{Spelt {count} trekk} other{Spelt {count} trekk}}", "activityInNbCorrespondenceGames": "{count, plural, =1{i {count} fjernsjakkparti} other{i {count} fjernsjakkparti}}", "activityCompletedNbGames": "{count, plural, =1{Har spela {count} fjernsjakkparti} other{Har spela {count} fjernsjakkparti}}", - "activityFollowedNbPlayers": "{count, plural, =1{Fylgjer {count} spelar} other{Fylgjer {count} spelarar}}", - "activityGainedNbFollowers": "{count, plural, =1{Har {count} nye fylgjarar} other{Har {count} nye fylgjarar}}", + "activityCompletedNbVariantGames": "{count, plural, =1{Har spelt {count} {param2}-fjernsjakkparti} other{Har spelt {count} {param2}-fjernsjakkparti}}", + "activityFollowedNbPlayers": "{count, plural, =1{Følgjer {count} spelar} other{Følgjer {count} spelarar}}", + "activityGainedNbFollowers": "{count, plural, =1{Har {count} ny følgjar} other{Har {count} nye følgjarar}}", "activityHostedNbSimuls": "{count, plural, =1{Har vore vert for {count} simultanframsyningar} other{Har vore vert for {count} simultan-matcher}}", "activityJoinedNbSimuls": "{count, plural, =1{Har vore deltakar i {count} simultanframsyningar} other{Har vore deltakar i {count} simultanmatcher}}", "activityCreatedNbStudies": "{count, plural, =1{Har laga {count} nye studiar} other{Har laga {count} nye studiar}}", @@ -64,7 +65,72 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{Har vore med i {count} sveitserturnering} other{Har vore med i {count} sveitserturneringar}}", "activityJoinedNbTeams": "{count, plural, =1{Er medlem av {count} lag} other{Er medlem av {count} lag}}", "broadcastBroadcasts": "Overføringar", + "broadcastMyBroadcasts": "Mine sendingar", "broadcastLiveBroadcasts": "Direktesende turneringar", + "broadcastBroadcastCalendar": "Kaldender for sendingar", + "broadcastNewBroadcast": "Ny direktesending", + "broadcastSubscribedBroadcasts": "Sendingar du abonnerar på", + "broadcastAboutBroadcasts": "Om sending", + "broadcastHowToUseLichessBroadcasts": "Korleis bruke Lichess-sendingar.", + "broadcastTheNewRoundHelp": "Den nye runden vil ha same medlemar og bidragsytarar som den førre.", + "broadcastAddRound": "Legg til ein runde", + "broadcastOngoing": "Pågåande", + "broadcastUpcoming": "Kommande", + "broadcastCompleted": "Fullførde", + "broadcastCompletedHelp": "Lichess detekterer ferdigspela rundar basert på kjeldeparita. Bruk denne innstillinga om det ikkje finst ei kjelde.", + "broadcastRoundName": "Rundenamn", + "broadcastRoundNumber": "Rundenummer", + "broadcastTournamentName": "Turneringsnamn", + "broadcastTournamentDescription": "Kortfatta skildring av turneringa", + "broadcastFullDescription": "Full omtale av arrangementet", + "broadcastFullDescriptionHelp": "Valfri lang omtale av overføringa. {param1} er tilgjengeleg. Omtalen må vera kortare enn {param2} teikn.", + "broadcastSourceSingleUrl": "PGN kjelde-URL", + "broadcastSourceUrlHelp": "Lenke som Lichess vil hente PGN-oppdateringar frå. Den må vera offentleg tilgjengeleg på internett.", + "broadcastSourceGameIds": "Opp til 64 Lichess spel-ID'ar, skilde med mellomrom.", + "broadcastStartDateTimeZone": "Startdato i turneringas lokale tidssone: {param}", + "broadcastStartDateHelp": "Valfritt, om du veit når arrangementet startar", + "broadcastCurrentGameUrl": "URL til pågåande parti", + "broadcastDownloadAllRounds": "Last ned alle rundene", + "broadcastResetRound": "Tilbakestill denne runden", + "broadcastDeleteRound": "Slett denne runden", + "broadcastDefinitivelyDeleteRound": "Slett runden og tilhøyrande parti ugjenkalleleg.", + "broadcastDeleteAllGamesOfThisRound": "Fjern alle parti frå denne runden. Kjelda må vera aktiv om dei skal kunne rettast opp att.", + "broadcastEditRoundStudy": "Rediger rundestudie", + "broadcastDeleteTournament": "Slett denne turneringa", + "broadcastDefinitivelyDeleteTournament": "Slett heile turneringa med alle rundene og alle partia.", + "broadcastShowScores": "Vis poengsummane til spelarar basert på spelresultatet deira", + "broadcastReplacePlayerTags": "Valfritt: bytt ut spelarnamn, rangeringar og titlar", + "broadcastFideFederations": "FIDE-forbund", + "broadcastTop10Rating": "Topp 10 rating", + "broadcastFidePlayers": "FIDE-spelarar", + "broadcastFidePlayerNotFound": "Fann ikkje FIDE-spelar", + "broadcastFideProfile": "FIDE-profil", + "broadcastFederation": "Forbund", + "broadcastAgeThisYear": "Alder i år", + "broadcastUnrated": "Urangert", + "broadcastRecentTournaments": "Nylegaste turneringar", + "broadcastOpenLichess": "Opne i Lichess", + "broadcastTeams": "Lag", + "broadcastBoards": "Brett", + "broadcastOverview": "Oversikt", + "broadcastSubscribeTitle": "Abonner for å få melding når kvarr runde startar. I konto-innstillingane dine kan du velje kva form varslane skal sendas som.", + "broadcastUploadImage": "Last opp turneringsbilete", + "broadcastNoBoardsYet": "Førebels er det ikkje brett å syne. Desse vert først vist når spel er lasta opp.", + "broadcastBoardsCanBeLoaded": "Brett kan lastas med ei kjelde eller via {param}", + "broadcastStartsAfter": "Startar etter {param}", + "broadcastStartVerySoon": "Sending vil starte om ikkje lenge.", + "broadcastNotYetStarted": "Sendinga har førebels ikkje starta.", + "broadcastOfficialWebsite": "Offisiell nettside", + "broadcastStandings": "Resultat", + "broadcastIframeHelp": "Fleire alternativ på {param}", + "broadcastWebmastersPage": "administratoren si side", + "broadcastPgnSourceHelp": "Ei offentleg PGN-kjelde i sanntid for denne runden. Vi tilbyr og ei {param} for raskare og meir effektiv synkronisering.", + "broadcastEmbedThisBroadcast": "Bygg inn denne sendinga på nettstaden din", + "broadcastEmbedThisRound": "Bygg inn {param} på nettstaden din", + "broadcastRatingDiff": "Rangeringsdiff", + "broadcastGamesThisTournament": "Spel i denne turneringa", + "broadcastScore": "Poengskår", + "broadcastNbBroadcasts": "{count, plural, =1{{count} sending} other{{count} sendingar}}", "challengeChallengesX": "Utfordringar: {param1}", "challengeChallengeToPlay": "Utfordra til eit parti", "challengeChallengeDeclined": "Utfordring avvist", @@ -383,8 +449,8 @@ "puzzleThemeXRayAttackDescription": "Ein situasjon der ei brikke går til åtak gjennom ei eller fleire andre brikker, gjerne brikker som høyrer til motspelaren.", "puzzleThemeZugzwang": "Trekktvang", "puzzleThemeZugzwangDescription": "Ei stilling der alle moglege trekk skadar stillinga.", - "puzzleThemeHealthyMix": "Blanda drops", - "puzzleThemeHealthyMixDescription": "Litt av alt. Du veit ikkje kva du blir møtt med, så du må vera førebudd på det meste. Nett som i verkelege parti.", + "puzzleThemeMix": "Blanda drops", + "puzzleThemeMixDescription": "Litt av alt. Du veit ikkje kva du blir møtt med, så du må vera førebudd på det meste. Nett som i verkelege parti.", "puzzleThemePlayerGames": "Spelar parti", "puzzleThemePlayerGamesDescription": "Finn oppgåver generert frå dine eller andre sine parti.", "puzzleThemePuzzleDownloadInformation": "Desse oppgåvene er offentleg eigedom og kan lastast ned frå {param}.", @@ -515,7 +581,6 @@ "memory": "Minne", "infiniteAnalysis": "Uendeleg analyse", "removesTheDepthLimit": "Tar bort avgrensing i søke-djupna, og varmar opp maskina", - "engineManager": "Innstillingar for sjakkprogram", "blunder": "Bukk", "mistake": "Mistak", "inaccuracy": "Småfeil", @@ -705,16 +770,16 @@ "reconnecting": "Koplar til på ny", "noNetwork": "Fråkopla", "favoriteOpponents": "Favorittmotstandarar", - "follow": "Fylgj", - "following": "Fylgjer", - "unfollow": "Slutt å fylgja", + "follow": "Følg", + "following": "Følgjer", + "unfollow": "Slutt å følgja", "followX": "Følg {param}", "unfollowX": "Slutt å følgja {param}", "block": "Blokkér", "blocked": "Blokkert", "unblock": "Fjern blokkering", - "followsYou": "Fylgjer deg", - "xStartedFollowingY": "{param1} byrja å fylgja {param2}", + "followsYou": "Følgjer deg", + "xStartedFollowingY": "{param1} byrja å følgja {param2}", "more": "Meir", "memberSince": "Medlem sidan", "lastSeenActive": "Siste innlogging {param}", @@ -819,7 +884,9 @@ "cheat": "Juks", "troll": "Troll", "other": "Anna", - "reportDescriptionHelp": "Lim inn link til partiet/partia og forklar kva som er gale med åtferda til denne brukaren.", + "reportCheatBoostHelp": "Legg ved lenke til partiet/partia og forklar kva som er gale med åtferda til denne brukaren. Å berre påstå at brukaren juksar er ikkje nok, men gje ei nærare forklaring på korleis du kom til denne konklusjonen.", + "reportUsernameHelp": "Forklår kva som gjer brukarnamnet er støytande. Det held ikkje med å påstå at \"namnet er støytande/upassande\", men fortell oss korleis du kom til denne konklusjonen, spesielt om tydinga er uklår, ikkje er på engelsk, er eit slanguttrykk, eller har ein historisk/kulturell referanse.", + "reportProcessedFasterInEnglish": "Rapporten din blir raskare behandla om du skriv på engelsk.", "error_provideOneCheatedGameLink": "Oppgje minst ei lenke til eit jukseparti.", "by": "av {param}", "importedByX": "Importert av {param}", @@ -842,7 +909,7 @@ "clockIncrement": "Inkrement", "privacy": "Privatsfære", "privacyPolicy": "Personvernpolitikk", - "letOtherPlayersFollowYou": "Lat andre spelarar fylgja deg", + "letOtherPlayersFollowYou": "Lat andre spelarar følgja deg", "letOtherPlayersChallengeYou": "Lat andre spelarar utfordra deg", "letOtherPlayersInviteYouToStudy": "Lat andre spelarar invitere til studium", "sound": "Lyd", @@ -1217,6 +1284,7 @@ "showMeEverything": "Vis alt", "lichessPatronInfo": "Lichess er ein velgjerdsorganisasjon basert på fritt tilgjengeleg open-kjeldekode-programvare.\nAlle kostnader for drift, utvikling og innhald vert finansiert eine og åleine av brukardonasjonar.", "nothingToSeeHere": "Ikkje noko å sjå nett no.", + "stats": "Statistikk", "opponentLeftCounter": "{count, plural, =1{Motspelaren din har forlate partiet. Du kan krevje vinst om {count} sekund.} other{Motspelaren din har forlate partiet. Du kan krevje siger om {count} sekund.}}", "mateInXHalfMoves": "{count, plural, =1{Matt om {count} halvtrekk} other{Matt om {count} halvtrekk}}", "nbBlunders": "{count, plural, =1{{count} bukk} other{{count} bukkar}}", @@ -1247,8 +1315,8 @@ "needNbMoreGames": "{count, plural, =1{Du må spela endå {count} rangert parti} other{Du må spela endå {count} rangerte parti}}", "nbImportedGames": "{count, plural, =1{{count} importert parti} other{{count} importerte parti}}", "nbFriendsOnline": "{count, plural, =1{{count} ven er innlogga} other{{count} vener er innlogga}}", - "nbFollowers": "{count, plural, =1{{count} fylgjar} other{{count} fylgjarar}}", - "nbFollowing": "{count, plural, =1{{count} fylgjer} other{{count} fylgjer}}", + "nbFollowers": "{count, plural, =1{{count} følgjar} other{{count} følgjarar}}", + "nbFollowing": "{count, plural, =1{{count} følgjer} other{{count} fylgjer}}", "lessThanNbMinutes": "{count, plural, =1{Mindre enn {count} minutt} other{Mindre enn {count} minutt}}", "nbGamesInPlay": "{count, plural, =1{{count} parti pågår} other{{count} parti pågår}}", "maximumNbCharacters": "{count, plural, =1{Maksimalt: {count} bokstav.} other{Maksimalt: {count} bokstavar.}}", @@ -1313,6 +1381,159 @@ "stormXRuns": "{count, plural, =1{1 runde} other{{count} rundar}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Har spela ein runde med {param2}} other{Har spela {count} rundar med {param2}}}", "streamerLichessStreamers": "Lichess-strøymarar", + "studyPrivate": "Privat", + "studyMyStudies": "Mine studiar", + "studyStudiesIContributeTo": "Studiar eg bidreg til", + "studyMyPublicStudies": "Mine offentlege studiar", + "studyMyPrivateStudies": "Mine private studiar", + "studyMyFavoriteStudies": "Mine favorittstudiar", + "studyWhatAreStudies": "Kva er studiar?", + "studyAllStudies": "Alle studiar", + "studyStudiesCreatedByX": "Studiar oppretta av {param}", + "studyNoneYet": "Ingen så langt.", + "studyHot": "Omtykt", + "studyDateAddedNewest": "Dato tilføydd (siste)", + "studyDateAddedOldest": "Dato tilføydd (første)", + "studyRecentlyUpdated": "Nyleg oppdatert", + "studyMostPopular": "Mest omtykt", + "studyAlphabetical": "Alfabetisk", + "studyAddNewChapter": "Føy til eit nytt kapittel", + "studyAddMembers": "Legg til medlemar", + "studyInviteToTheStudy": "Inviter til studien", + "studyPleaseOnlyInvitePeopleYouKnow": "Inviter berre folk du kjenner og som aktivt ynskjer å delta i studien.", + "studySearchByUsername": "Søk på brukarnamn", + "studySpectator": "Tilskodar", + "studyContributor": "Bidragsytar", + "studyKick": "Kast ut", + "studyLeaveTheStudy": "Forlat studien", + "studyYouAreNowAContributor": "Du er no bidragsytar", + "studyYouAreNowASpectator": "Du er no tilskodar", + "studyPgnTags": "PGN-merkelappar", + "studyLike": "Lik", + "studyUnlike": "Slutt å lika", + "studyNewTag": "Ny merkelapp", + "studyCommentThisPosition": "Kommenter denne stillinga", + "studyCommentThisMove": "Kommenter dette trekket", + "studyAnnotateWithGlyphs": "Kommenter med symbol", + "studyTheChapterIsTooShortToBeAnalysed": "Kapittelet er for kort for å analyserast.", + "studyOnlyContributorsCanRequestAnalysis": "Berre bidragsytarar til studien kan be om maskinanalyse.", + "studyGetAFullComputerAnalysis": "Få full maskinanalyse av hovedvarianten frå serveren.", + "studyMakeSureTheChapterIsComplete": "Sørg for at kapittelet er fullført. Du kan berre be om analyse ein gong.", + "studyAllSyncMembersRemainOnTheSamePosition": "Alle SYNC-medlemene ser den same stillingen", + "studyShareChanges": "Lagre endringar på serveren og del dei med tilskodarar", + "studyPlaying": "Spelar no", + "studyShowEvalBar": "Evalueringssøyler", + "studyFirst": "Første", + "studyPrevious": "Attende", + "studyNext": "Neste", + "studyLast": "Siste", "studyShareAndExport": "Del & eksporter", - "studyStart": "Start" + "studyCloneStudy": "Klon", + "studyStudyPgn": "Studie-PGN", + "studyDownloadAllGames": "Last ned alle spel", + "studyChapterPgn": "Kapittel-PGN", + "studyCopyChapterPgn": "Kopier PGN", + "studyDownloadGame": "Last ned spel", + "studyStudyUrl": "Studie-URL", + "studyCurrentChapterUrl": "Kapittel-URL", + "studyYouCanPasteThisInTheForumToEmbed": "Du kan lime inn dette i forumet for å syna det der", + "studyStartAtInitialPosition": "Start ved innleiande stilling", + "studyStartAtX": "Start ved {param}", + "studyEmbedInYourWebsite": "Inkorporer i websida eller bloggen din", + "studyReadMoreAboutEmbedding": "Les meir om innbygging", + "studyOnlyPublicStudiesCanBeEmbedded": "Berre offentlege studiar kan byggast inn!", + "studyOpen": "Opne", + "studyXBroughtToYouByY": "{param1} presentert av {param2}", + "studyStudyNotFound": "Fann ikkje studien", + "studyEditChapter": "Rediger kapittel", + "studyNewChapter": "Nytt kapittel", + "studyImportFromChapterX": "Importer frå {param}", + "studyOrientation": "Retning", + "studyAnalysisMode": "Analysemodus", + "studyPinnedChapterComment": "Fastspikra kapittelkommentar", + "studySaveChapter": "Lagre kapittelet", + "studyClearAnnotations": "Fjern notat", + "studyClearVariations": "Fjern variantar", + "studyDeleteChapter": "Slett kapittel", + "studyDeleteThisChapter": "Slette dette kapittelet? Avgjerda er endeleg og kan ikkje angrast!", + "studyClearAllCommentsInThisChapter": "Fjern alle kommentarar og figurar i dette kapittelet?", + "studyRightUnderTheBoard": "Rett under brettet", + "studyNoPinnedComment": "Ingen", + "studyNormalAnalysis": "Normal analyse", + "studyHideNextMoves": "Skjul neste trekk", + "studyInteractiveLesson": "Interaktiv leksjon", + "studyChapterX": "Kapittel {param}", + "studyEmpty": "Tom", + "studyStartFromInitialPosition": "Start ved innleiande stilling", + "studyEditor": "Editor", + "studyStartFromCustomPosition": "Start frå innleiande stilling", + "studyLoadAGameByUrl": "Last opp eit parti frå URL", + "studyLoadAPositionFromFen": "Last opp ein stilling frå FEN", + "studyLoadAGameFromPgn": "Last opp eit parti frå PGN", + "studyAutomatic": "Automatisk", + "studyUrlOfTheGame": "URL for partiet", + "studyLoadAGameFromXOrY": "Last opp eit parti frå {param1} eller {param2}", + "studyCreateChapter": "Opprett kapittel", + "studyCreateStudy": "Opprett ein studie", + "studyEditStudy": "Rediger studie", + "studyVisibility": "Synlegheit", + "studyPublic": "Offentleg", + "studyUnlisted": "Ikkje opplista", + "studyInviteOnly": "Berre etter invitasjon", + "studyAllowCloning": "Tillat kloning", + "studyNobody": "Ingen", + "studyOnlyMe": "Berre meg", + "studyContributors": "Bidragsytarar", + "studyMembers": "Medlemar", + "studyEveryone": "Alle", + "studyEnableSync": "Aktiver synk", + "studyYesKeepEveryoneOnTheSamePosition": "Ja: behald alle i den same stilllinga", + "studyNoLetPeopleBrowseFreely": "Nei: lat folk sjå fritt gjennom", + "studyPinnedStudyComment": "Fastspikra studiekommentar", + "studyStart": "Start", + "studySave": "Lagre", + "studyClearChat": "Fjern teksten frå kommentarfeltet", + "studyDeleteTheStudyChatHistory": "Slette studiens kommentar-historikk? Du kan ikkje angre!", + "studyDeleteStudy": "Slett studie", + "studyConfirmDeleteStudy": "Slette heile studien? Avgjerda er endeleg og kan ikke gjeras om! Skriv namnet på studien som skal stadfestast: {param}", + "studyWhereDoYouWantToStudyThat": "Kva for ein studie vil du bruke?", + "studyGoodMove": "Godt trekk", + "studyMistake": "Mistak", + "studyBrilliantMove": "Strålande trekk", + "studyBlunder": "Bukk", + "studyInterestingMove": "Interessant trekk", + "studyDubiousMove": "Tvilsamt trekk", + "studyOnlyMove": "Einaste moglege trekk", + "studyZugzwang": "Trekktvang", + "studyEqualPosition": "Lik stilling", + "studyUnclearPosition": "Uavklart stilling", + "studyWhiteIsSlightlyBetter": "Kvit står litt betre", + "studyBlackIsSlightlyBetter": "Svart står litt betre", + "studyWhiteIsBetter": "Kvit står betre", + "studyBlackIsBetter": "Svart står betre", + "studyWhiteIsWinning": "Kvit står til vinst", + "studyBlackIsWinning": "Svart står til vinst", + "studyNovelty": "Nyskapning", + "studyDevelopment": "Utvikling", + "studyInitiative": "Initiativ", + "studyAttack": "Åtak", + "studyCounterplay": "Motspel", + "studyTimeTrouble": "Tidsnaud", + "studyWithCompensation": "Med kompensasjon", + "studyWithTheIdea": "Med ideen", + "studyNextChapter": "Neste kapittel", + "studyPrevChapter": "Førre kapittel", + "studyStudyActions": "Studiehandlingar", + "studyTopics": "Tema", + "studyMyTopics": "Mine tema", + "studyPopularTopics": "Omtykte tema", + "studyManageTopics": "Administrer tema", + "studyBack": "Tilbake", + "studyPlayAgain": "Spel på ny", + "studyWhatWouldYouPlay": "Kva vil du spela i denne stillinga?", + "studyYouCompletedThisLesson": "Gratulerar! Du har fullført denne leksjonen.", + "studyNbChapters": "{count, plural, =1{{count} kapittel} other{{count} kapittel}}", + "studyNbGames": "{count, plural, =1{{count} parti} other{{count} parti}}", + "studyNbMembers": "{count, plural, =1{{count} medlem} other{{count} medlemar}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Sett inn PGN-teksten din her, maksimum {count} parti} other{Sett inn PGN-teksten din her, maksimum {count} parti}}" } \ No newline at end of file diff --git a/lib/l10n/lila_pl.arb b/lib/l10n/lila_pl.arb index 0e35a8a68e..50d5609c67 100644 --- a/lib/l10n/lila_pl.arb +++ b/lib/l10n/lila_pl.arb @@ -30,7 +30,6 @@ "mobilePuzzleStormConfirmEndRun": "Czy chcesz zakończyć tę serię?", "mobilePuzzleStormFilterNothingToShow": "Brak wyników, zmień proszę filtry", "mobileCancelTakebackOffer": "Anuluj prośbę cofnięcia ruchu", - "mobileCancelDrawOffer": "Anuluj propozycję remisu", "mobileWaitingForOpponentToJoin": "Oczekiwanie na dołączenie przeciwnika...", "mobileBlindfoldMode": "Gra na ślepo", "mobileLiveStreamers": "Aktywni streamerzy", @@ -42,6 +41,7 @@ "mobilePuzzleStormSubtitle": "Rozwiąż jak najwięcej zadań w ciągu 3 minut.", "mobileGreeting": "Witaj {param}", "mobileGreetingWithoutName": "Witaj", + "mobilePrefMagnifyDraggedPiece": "Powiększ przeciąganą bierkę", "activityActivity": "Aktywność", "activityHostedALiveStream": "Udostępnił stream na żywo", "activityRankedInSwissTournament": "{param1} miejsce w {param2}", @@ -54,6 +54,7 @@ "activityPlayedNbMoves": "{count, plural, =1{Wykonany {count} ruch} few{Wykonane {count} ruchy} many{Wykonane {count} ruchów} other{Wykonane {count} ruchów}}", "activityInNbCorrespondenceGames": "{count, plural, =1{w {count} partii korespondencyjnej} few{w {count} partiach korespondencyjnych} many{w {count} partiach korespondencyjnych} other{w {count} partiach korespondencyjnych}}", "activityCompletedNbGames": "{count, plural, =1{Zakończenie partii korespondencyjnej} few{Zakończenie {count} partii korespondencyjnych} many{Zakończenie {count} partii korespondencyjnych} other{Zakończenie {count} partii korespondencyjnych}}", + "activityCompletedNbVariantGames": "{count, plural, =1{Zakończona {count} {param2} partia korespondencyjna} few{Zakończone {count} {param2} partie korespondencyjne} many{Zakończone {count} {param2} partii korespondencyjnych} other{Zakończone {count} {param2} partii korespondencyjnych}}", "activityFollowedNbPlayers": "{count, plural, =1{Rozpoczęcie obserwowania gracza} few{Rozpoczęcie obserwowania {count} graczy} many{Rozpoczęcie obserwowania {count} graczy} other{Rozpoczęcie obserwowania {count} graczy}}", "activityGainedNbFollowers": "{count, plural, =1{Zyskano {count} nowego obserwującego/-ą} few{Zyskano {count} nowych obserwujących} many{Zyskano {count} nowych obserwujących} other{Zyskano {count} nowych obserwujących}}", "activityHostedNbSimuls": "{count, plural, =1{Rozegranie symultany} few{Rozegranie {count} symultan} many{Rozegranie {count} symultan} other{Rozegranie {count} symultan}}", @@ -64,7 +65,72 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{Uczestniczył(a) w {count} turnieju szwajcarskim} few{Uczestniczył(a) w {count} turniejach szwajcarskich} many{Uczestniczył(a) w {count} turniejach szwajcarskich} other{Uczestniczył(a) w {count} turniejach szwajcarskich}}", "activityJoinedNbTeams": "{count, plural, =1{Dołączenie do klubu} few{Dołączenie do {count} klubów} many{Dołączenie do {count} klubów} other{Dołączono do {count} zespołów}}", "broadcastBroadcasts": "Transmisje", + "broadcastMyBroadcasts": "Moje transmisje", "broadcastLiveBroadcasts": "Transmisje turniejów na żywo", + "broadcastBroadcastCalendar": "Kalendarz transmisji", + "broadcastNewBroadcast": "Nowa transmisja na żywo", + "broadcastSubscribedBroadcasts": "Subskrybowane transmisje", + "broadcastAboutBroadcasts": "O transmisji", + "broadcastHowToUseLichessBroadcasts": "Jak korzystać z transmisji na Lichess.", + "broadcastTheNewRoundHelp": "Nowa runda będzie miała tych samych uczestników co poprzednia.", + "broadcastAddRound": "Dodaj rundę", + "broadcastOngoing": "Trwające", + "broadcastUpcoming": "Nadchodzące", + "broadcastCompleted": "Zakończone", + "broadcastCompletedHelp": "Lichess wykrywa ukończenie rundy w oparciu o śledzone partie. Użyj tego przełącznika, jeśli nie ma takich partii.", + "broadcastRoundName": "Nazwa rundy", + "broadcastRoundNumber": "Numer rundy", + "broadcastTournamentName": "Nazwa turnieju", + "broadcastTournamentDescription": "Krótki opis turnieju", + "broadcastFullDescription": "Pełny opis wydarzenia", + "broadcastFullDescriptionHelp": "Opcjonalny długi opis transmisji. {param1} jest dostępny. Długość musi być mniejsza niż {param2} znaków.", + "broadcastSourceSingleUrl": "Adres URL zapisu PGN", + "broadcastSourceUrlHelp": "Adres URL, który Lichess będzie udostępniał, aby można było uzyskać aktualizacje PGN. Musi być publicznie dostępny z internetu.", + "broadcastSourceGameIds": "Do 64 identyfikatorów partii, oddzielonych spacjami.", + "broadcastStartDateTimeZone": "Data rozpoczęcia w lokalnej strefie czasowej turnieju: {param}", + "broadcastStartDateHelp": "Opcjonalne, jeśli wiesz kiedy wydarzenie się rozpocznie", + "broadcastCurrentGameUrl": "Adres URL bieżącej partii", + "broadcastDownloadAllRounds": "Pobierz wszystkie rundy", + "broadcastResetRound": "Zresetuj tę rundę", + "broadcastDeleteRound": "Usuń tę rundę", + "broadcastDefinitivelyDeleteRound": "Ostatecznie usuń rundę i jej wszystkie partie.", + "broadcastDeleteAllGamesOfThisRound": "Usuń wszystkie partie w tej rundzie. Źródło będzie musiało być aktywne, aby je odtworzyć.", + "broadcastEditRoundStudy": "Edytuj opracowanie rundy", + "broadcastDeleteTournament": "Usuń ten turniej", + "broadcastDefinitivelyDeleteTournament": "Ostatecznie usuń cały turniej, jego wszystkie rundy i partie.", + "broadcastShowScores": "Pokaż wyniki graczy na podstawie wyników gry", + "broadcastReplacePlayerTags": "Opcjonalnie: zmień nazwy, rankingi oraz tytuły gracza", + "broadcastFideFederations": "Federacje FIDE", + "broadcastTop10Rating": "10 najlepszych rankingów", + "broadcastFidePlayers": "Zawodnicy FIDE", + "broadcastFidePlayerNotFound": "Nie znaleziono zawodnika FIDE", + "broadcastFideProfile": "Profil FIDE", + "broadcastFederation": "Federacja", + "broadcastAgeThisYear": "Wiek w tym roku", + "broadcastUnrated": "Bez rankingu", + "broadcastRecentTournaments": "Najnowsze turnieje", + "broadcastOpenLichess": "Otwórz w Lichess", + "broadcastTeams": "Drużyny", + "broadcastBoards": "Szachownice", + "broadcastOverview": "Podgląd", + "broadcastSubscribeTitle": "Subskrybuj, aby dostawać powiadomienia o każdej rozpoczętej rundzie. W preferencjach konta możesz przełączać czy chcesz powiadomienia dźwiękowe czy wyskakujące notyfikacje tekstowe.", + "broadcastUploadImage": "Prześlij logo turnieju", + "broadcastNoBoardsYet": "Szachownice pojawią się jak tylko załadują się partie.", + "broadcastBoardsCanBeLoaded": "Szachownice mogą być załadowane bezpośrednio ze źródła lub przez {param}", + "broadcastStartsAfter": "Rozpoczyna się po {param}", + "broadcastStartVerySoon": "Transmisja wkrótce się rozpocznie.", + "broadcastNotYetStarted": "Transmisja jeszcze się nie rozpoczęła.", + "broadcastOfficialWebsite": "Oficjalna strona", + "broadcastStandings": "Klasyfikacja", + "broadcastIframeHelp": "Więcej opcji na {param}", + "broadcastWebmastersPage": "stronie webmasterów", + "broadcastPgnSourceHelp": "Publiczne źródło PGN w czasie rzeczywistym dla tej rundy. Oferujemy również {param} dla szybszej i skuteczniejszej synchronizacji.", + "broadcastEmbedThisBroadcast": "Umieść tę transmisję na swojej stronie internetowej", + "broadcastEmbedThisRound": "Osadź {param} na swojej stronie internetowej", + "broadcastRatingDiff": "Różnica rankingu", + "broadcastGamesThisTournament": "Partie w tym turnieju", + "broadcastScore": "Wynik", + "broadcastNbBroadcasts": "{count, plural, =1{{count} transmisja} few{{count} transmisje} many{{count} transmisji} other{{count} transmisji}}", "challengeChallengesX": "Wyzwania: {param1}", "challengeChallengeToPlay": "Zaproś do gry", "challengeChallengeDeclined": "Wyzwanie odrzucone", @@ -95,7 +161,7 @@ "perfStatPerfStats": "Statystyki dla {param}", "perfStatViewTheGames": "Zobacz partie", "perfStatProvisional": "prowizoryczny", - "perfStatNotEnoughRatedGames": "Nie zagrano wystarczająco dużo rankingowych gier, aby ustalić wiarygodną ranking.", + "perfStatNotEnoughRatedGames": "Nie zagrano wystarczająco dużo rankingowych gier, aby ustalić wiarygodny ranking.", "perfStatProgressOverLastXGames": "Postęp w ostatnich {param} partiach:", "perfStatRatingDeviation": "Odchylenie rankingu: {param}.", "perfStatRatingDeviationTooltip": "Niższa wartość oznacza, że ranking jest bardziej stabilny. Powyżej {param1}, ranking jest uważany za tymczasowy. Aby znaleźć się na listach rankingowych, wartość ta powinna być niższa niż {param2} (standardowe szachy) lub {param3} (warianty).", @@ -383,8 +449,8 @@ "puzzleThemeXRayAttackDescription": "Figura atakuje albo broni pole przez wrogą figurę.", "puzzleThemeZugzwang": "Zugzwang", "puzzleThemeZugzwangDescription": "Ograniczone ruchy przeciwnika powodują, że każde posunięcie pogarsza jego pozycję.", - "puzzleThemeHealthyMix": "Miszmasz", - "puzzleThemeHealthyMixDescription": "Bądź gotów na wszystko! Jak podczas prawdziwej partii.", + "puzzleThemeMix": "Miszmasz", + "puzzleThemeMixDescription": "Po trochu wszystkiego. Nie wiesz czego się spodziewać, więc bądź gotów na wszystko! Tak jak w prawdziwej partii.", "puzzleThemePlayerGames": "Partie gracza", "puzzleThemePlayerGamesDescription": "Wyszukaj zadania wygenerowane z Twoich partii lub z partii innego gracza.", "puzzleThemePuzzleDownloadInformation": "Te zadania dostępne są w domenie publicznej i mogą być pobrane z {param}.", @@ -515,7 +581,6 @@ "memory": "Pamięć RAM", "infiniteAnalysis": "Nieskończona analiza", "removesTheDepthLimit": "Usuwa limit głębokości analizy i rozgrzewa Twój komputer do czerwoności ;)", - "engineManager": "Ustawienia silnika", "blunder": "Błąd", "mistake": "Pomyłka", "inaccuracy": "Niedokładność", @@ -597,6 +662,7 @@ "rank": "Miejsce", "rankX": "Miejsce: {param}", "gamesPlayed": "Rozegranych partii", + "ok": "OK", "cancel": "Anuluj", "whiteTimeOut": "Upłynął czas białych", "blackTimeOut": "Upłynął czas czarnych", @@ -800,6 +866,7 @@ "no": "Nie", "yes": "Tak", "website": "Strona internetowa", + "mobile": "Aplikacja mobilna", "help": "Porada:", "createANewTopic": "Załóż nowy temat", "topics": "Liczba tematów", @@ -818,7 +885,9 @@ "cheat": "Oszust", "troll": "Natręt (troll)", "other": "Inne", - "reportDescriptionHelp": "Wklej odnośnik do partii i wyjaśnij, co złego jest w zachowaniu tego użytkownika. Nie pisz tylko, że „oszukuje”, ale wytłumacz nam, na jakiej podstawie doszedłeś/aś do takiego wniosku. Odniesiemy się do twojego zgłoszenia szybciej, jeżeli napiszesz je w języku angielskim.", + "reportCheatBoostHelp": "Wklej link do partii i wytłumacz, co złego jest w zachowaniu tego użytkownika. Nie pisz tylko \"on oszukiwał\", lecz napisz jak doszedłeś/aś do tego wniosku.", + "reportUsernameHelp": "Wytłumacz, co w nazwie użytkownika jest obraźliwe. Nie pisz tylko \"nazwa jest obraźliwa\", lecz napisz jak doszedłeś/aś do tego wniosku, zwłaszcza jeśli jest mało znany, nie po angielsku, slangowy lub odniesieniem do kultury/historii.", + "reportProcessedFasterInEnglish": "Twoje zgłoszenie będzie sprawdzone szybciej, jeśli zostanie napisane po angielsku.", "error_provideOneCheatedGameLink": "Podaj przynajmniej jeden odnośnik do gry, w której oszukiwano.", "by": "autor {param}", "importedByX": "Zaimportowane przez {param}", @@ -1216,6 +1285,7 @@ "showMeEverything": "Pokaż mi wszystko", "lichessPatronInfo": "Lichess jest organizacją niedochodową i całkowicie darmowym otwartym oprogramowaniem.\nWszystkie koszty operacyjne, rozwój i treści są finansowane wyłącznie z darowizn użytkowników.", "nothingToSeeHere": "W tej chwili nie ma nic do zobaczenia.", + "stats": "Statystyki", "opponentLeftCounter": "{count, plural, =1{Przeciwnik opuścił grę. Możesz ogłosić wygraną za {count} sekundę.} few{Przeciwnik opuścił grę. Możesz ogłosić wygraną za {count} sekundy.} many{Przeciwnik opuścił grę. Możesz ogłosić wygraną za {count} sekund.} other{Przeciwnik opuścił grę. Możesz ogłosić wygraną za {count} sekund.}}", "mateInXHalfMoves": "{count, plural, =1{Mat w {count} posunięciu} few{Mat w {count} posunięciach} many{Mat w {count} posunięciach} other{Mat w {count} posunięciach}}", "nbBlunders": "{count, plural, =1{{count} błąd} few{{count} błędy} many{{count} błędów} other{{count} błędów}}", @@ -1312,6 +1382,159 @@ "stormXRuns": "{count, plural, =1{1 próba} few{{count} próby} many{{count} prób} other{{count} prób}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Rozegrano jedną sesję {param2}} few{Rozegrano {count} sesje {param2}} many{Rozegrano {count} sesji {param2}} other{Rozegrano {count} sesji {param2}}}", "streamerLichessStreamers": "Streamerzy Lichess", + "studyPrivate": "Prywatne", + "studyMyStudies": "Moje opracowania", + "studyStudiesIContributeTo": "Opracowania, które współtworzę", + "studyMyPublicStudies": "Moje publiczne opracowania", + "studyMyPrivateStudies": "Moje prywatne opracowania", + "studyMyFavoriteStudies": "Moje ulubione opracowania", + "studyWhatAreStudies": "Czym są opracowania?", + "studyAllStudies": "Wszystkie opracowania", + "studyStudiesCreatedByX": "Opracowanie stworzone przez {param}", + "studyNoneYet": "Jeszcze brak.", + "studyHot": "Hity", + "studyDateAddedNewest": "Data dodania (od najnowszych)", + "studyDateAddedOldest": "Data dodania (od najstarszych)", + "studyRecentlyUpdated": "Ostatnio aktualizowane", + "studyMostPopular": "Najpopularniejsze", + "studyAlphabetical": "Alfabetycznie", + "studyAddNewChapter": "Dodaj nowy rozdział", + "studyAddMembers": "Dodaj uczestników", + "studyInviteToTheStudy": "Zaproś do opracowania", + "studyPleaseOnlyInvitePeopleYouKnow": "Zapraszaj do opracowania tylko znajomych, którzy chcą w nim aktywnie uczestniczyć.", + "studySearchByUsername": "Szukaj wg nazwy użytkownika", + "studySpectator": "Obserwator", + "studyContributor": "Współautor", + "studyKick": "Wyrzuć", + "studyLeaveTheStudy": "Opuść opracowanie", + "studyYouAreNowAContributor": "Jesteś teraz współautorem", + "studyYouAreNowASpectator": "Jesteś teraz obserwatorem", + "studyPgnTags": "Znaczniki PGN", + "studyLike": "Lubię to", + "studyUnlike": "Cofnij polubienie", + "studyNewTag": "Nowy znacznik", + "studyCommentThisPosition": "Skomentuj tę pozycję", + "studyCommentThisMove": "Skomentuj ten ruch", + "studyAnnotateWithGlyphs": "Dodaj adnotacje symbolami", + "studyTheChapterIsTooShortToBeAnalysed": "Rozdział jest zbyt krótki do analizy.", + "studyOnlyContributorsCanRequestAnalysis": "Tylko współautorzy opracowania mogą prosić o analizę komputerową.", + "studyGetAFullComputerAnalysis": "Uzyskaj pełną, zdalną analizę komputerową głównego wariantu.", + "studyMakeSureTheChapterIsComplete": "Upewnij się, że rozdział jest kompletny. O jego analizę możesz poprosić tylko raz.", + "studyAllSyncMembersRemainOnTheSamePosition": "Wszyscy zsynchronizowani uczestnicy pozostają na tej samej pozycji", + "studyShareChanges": "Współdzielenie zmian z obserwatorami i ich zapis na serwerze", + "studyPlaying": "W toku", + "studyShowEvalBar": "Paski ewaluacji", + "studyFirst": "Pierwszy", + "studyPrevious": "Poprzedni", + "studyNext": "Następny", + "studyLast": "Ostatni", "studyShareAndExport": "Udostępnianie i eksport", - "studyStart": "Rozpocznij" + "studyCloneStudy": "Powiel", + "studyStudyPgn": "PGN opracowania", + "studyDownloadAllGames": "Pobierz wszystkie partie", + "studyChapterPgn": "PGN rozdziału", + "studyCopyChapterPgn": "Kopiuj PGN", + "studyDownloadGame": "Pobierz partię", + "studyStudyUrl": "Link do opracowania", + "studyCurrentChapterUrl": "URL bieżącego rozdziału", + "studyYouCanPasteThisInTheForumToEmbed": "Możesz wkleić to, aby osadzić na forum", + "studyStartAtInitialPosition": "Rozpocznij z pozycji początkowej", + "studyStartAtX": "Rozpocznij od {param}", + "studyEmbedInYourWebsite": "Udostępnij na swojej stronie lub na blogu", + "studyReadMoreAboutEmbedding": "Dowiedz się więcej o osadzaniu", + "studyOnlyPublicStudiesCanBeEmbedded": "Tylko publiczne opracowania mogą być osadzane!", + "studyOpen": "Otwórz", + "studyXBroughtToYouByY": "{param1} przygotowane przez {param2}", + "studyStudyNotFound": "Nie znaleziono opracowania", + "studyEditChapter": "Edytuj rozdział", + "studyNewChapter": "Nowy rozdział", + "studyImportFromChapterX": "Zaimportuj z {param}", + "studyOrientation": "Orientacja", + "studyAnalysisMode": "Rodzaj analizy", + "studyPinnedChapterComment": "Przypięty komentarz", + "studySaveChapter": "Zapisz rozdział", + "studyClearAnnotations": "Usuń adnotacje", + "studyClearVariations": "Wyczyść warianty", + "studyDeleteChapter": "Usuń rozdział", + "studyDeleteThisChapter": "Usunąć ten rozdział? Nie będzie można tego cofnąć!", + "studyClearAllCommentsInThisChapter": "Usunąć wszystkie komentarze i oznaczenia w tym rozdziale?", + "studyRightUnderTheBoard": "Pod szachownicą, po prawej stronie", + "studyNoPinnedComment": "Brak", + "studyNormalAnalysis": "Normalna", + "studyHideNextMoves": "Ukryj następne posunięcia", + "studyInteractiveLesson": "Lekcja interaktywna", + "studyChapterX": "Rozdział {param}", + "studyEmpty": "Pusty", + "studyStartFromInitialPosition": "Rozpocznij z pozycji początkowej", + "studyEditor": "Edytor", + "studyStartFromCustomPosition": "Rozpocznij z ustawionej pozycji", + "studyLoadAGameByUrl": "Zaimportuj partię z linku", + "studyLoadAPositionFromFen": "Zaimportuj partię z FEN", + "studyLoadAGameFromPgn": "Zaimportuj partię z PGN", + "studyAutomatic": "Automatycznie", + "studyUrlOfTheGame": "Link do partii", + "studyLoadAGameFromXOrY": "Zaimportuj partię z {param1} lub {param2}", + "studyCreateChapter": "Stwórz rozdział", + "studyCreateStudy": "Stwórz opracowanie", + "studyEditStudy": "Edytuj opracowanie", + "studyVisibility": "Widoczność", + "studyPublic": "Publiczne", + "studyUnlisted": "Niepubliczne", + "studyInviteOnly": "Tylko zaproszeni", + "studyAllowCloning": "Pozwól kopiować", + "studyNobody": "Nikt", + "studyOnlyMe": "Tylko ja", + "studyContributors": "Współautorzy", + "studyMembers": "Uczestnicy", + "studyEveryone": "Każdy", + "studyEnableSync": "Włącz synchronizację", + "studyYesKeepEveryoneOnTheSamePosition": "Tak: utrzymaj wszystkich w tej samej pozycji", + "studyNoLetPeopleBrowseFreely": "Nie: pozwól oglądać wszystkim", + "studyPinnedStudyComment": "Przypięte komentarze", + "studyStart": "Rozpocznij", + "studySave": "Zapisz", + "studyClearChat": "Wyczyść czat", + "studyDeleteTheStudyChatHistory": "Usunąć historię czatu opracowania? Nie będzie można tego cofnąć!", + "studyDeleteStudy": "Usuń opracowanie", + "studyConfirmDeleteStudy": "Usunąć opracowanie? Nie będzie można go odzyskać! Wpisz nazwę opracowania, aby potwierdzić operację: {param}", + "studyWhereDoYouWantToStudyThat": "Gdzie chcesz się tego uczyć?", + "studyGoodMove": "Dobry ruch", + "studyMistake": "Pomyłka", + "studyBrilliantMove": "Świetny ruch", + "studyBlunder": "Błąd", + "studyInterestingMove": "Interesujący ruch", + "studyDubiousMove": "Wątpliwy ruch", + "studyOnlyMove": "Jedyny ruch", + "studyZugzwang": "Zugzwang", + "studyEqualPosition": "Równa pozycja", + "studyUnclearPosition": "Niejasna pozycja", + "studyWhiteIsSlightlyBetter": "Białe stoją nieznacznie lepiej", + "studyBlackIsSlightlyBetter": "Czarne stoją nieznacznie lepiej", + "studyWhiteIsBetter": "Białe stoją lepiej", + "studyBlackIsBetter": "Czarne stoją lepiej", + "studyWhiteIsWinning": "Białe wygrywają", + "studyBlackIsWinning": "Czarne wygrywają", + "studyNovelty": "Nowość", + "studyDevelopment": "Rozwój", + "studyInitiative": "Inicjatywa", + "studyAttack": "Atak", + "studyCounterplay": "Przeciwdziałanie", + "studyTimeTrouble": "Problem z czasem", + "studyWithCompensation": "Z rekompensatą", + "studyWithTheIdea": "Z pomysłem", + "studyNextChapter": "Następny rozdział", + "studyPrevChapter": "Poprzedni rozdział", + "studyStudyActions": "Opcje opracowań", + "studyTopics": "Tematy", + "studyMyTopics": "Moje tematy", + "studyPopularTopics": "Popularne tematy", + "studyManageTopics": "Zarządzaj tematami", + "studyBack": "Powrót", + "studyPlayAgain": "Odtwórz ponownie", + "studyWhatWouldYouPlay": "Co byś zagrał w tej pozycji?", + "studyYouCompletedThisLesson": "Gratulacje! Ukończono tę lekcję.", + "studyNbChapters": "{count, plural, =1{{count} rozdział} few{{count} rozdziały} many{{count} rozdziałów} other{{count} rozdziałów}}", + "studyNbGames": "{count, plural, =1{{count} partia} few{{count} partie} many{{count} partii} other{{count} partii}}", + "studyNbMembers": "{count, plural, =1{{count} uczestnik} few{{count} uczestników} many{{count} uczestników} other{{count} uczestników}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Wklej tutaj swój PGN, max {count} partię} few{Wklej tutaj swój PGN, max {count} partie} many{Wklej tutaj swój PGN, max {count} partii} other{Wklej tutaj swój PGN, max {count} partii}}" } \ No newline at end of file diff --git a/lib/l10n/lila_pt.arb b/lib/l10n/lila_pt.arb index 59f532eb24..3f4896412e 100644 --- a/lib/l10n/lila_pt.arb +++ b/lib/l10n/lila_pt.arb @@ -36,6 +36,12 @@ "mobileCustomGameJoinAGame": "Entrar num jogo", "mobileCorrespondenceClearSavedMove": "Limpar movimento salvo", "mobileSomethingWentWrong": "Algo deu errado.", + "mobileShowResult": "Mostrar resultado", + "mobilePuzzleThemesSubtitle": "Joga problemas das tuas aberturas favoritas, ou escolhe um tema.", + "mobilePuzzleStormSubtitle": "Resolve quantos problemas for possível em 3 minutos.", + "mobileGreeting": "Olá, {param}", + "mobileGreetingWithoutName": "Olá", + "mobilePrefMagnifyDraggedPiece": "Ampliar peça arrastada", "activityActivity": "Atividade", "activityHostedALiveStream": "Criou uma livestream", "activityRankedInSwissTournament": "Classificado #{param1} em {param2}", @@ -48,6 +54,7 @@ "activityPlayedNbMoves": "{count, plural, =1{Fez {count} jogada} other{Fez {count} jogadas}}", "activityInNbCorrespondenceGames": "{count, plural, =1{em {count} jogo por correspondência} other{em {count} jogos por correspondência}}", "activityCompletedNbGames": "{count, plural, =1{Completou {count} jogo por correspondência} other{Completou {count} jogos por correspondência}}", + "activityCompletedNbVariantGames": "{count, plural, =1{Completou {count} jogo {param2} por correspondência} other{Completou {count} jogos {param2} por correspondência}}", "activityFollowedNbPlayers": "{count, plural, =1{Começou a seguir {count} jogador} other{Começou a seguir {count} jogadores}}", "activityGainedNbFollowers": "{count, plural, =1{Ganhou {count} novo seguidor} other{Ganhou {count} novos seguidores}}", "activityHostedNbSimuls": "{count, plural, =1{Criou {count} exibição simultânea} other{Criou {count} exibições simultâneas}}", @@ -58,7 +65,72 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{Competiu em {count} torneio suíço} other{Competiu em {count} torneios suíços}}", "activityJoinedNbTeams": "{count, plural, =1{Entrou em {count} equipa} other{Entrou em {count} equipas}}", "broadcastBroadcasts": "Transmissões", + "broadcastMyBroadcasts": "As minhas transmissões", "broadcastLiveBroadcasts": "Transmissões do torneio em direto", + "broadcastBroadcastCalendar": "Calendário de transmissão", + "broadcastNewBroadcast": "Nova transmissão em direto", + "broadcastSubscribedBroadcasts": "Transmissões subscritas", + "broadcastAboutBroadcasts": "Sobre Transmissões", + "broadcastHowToUseLichessBroadcasts": "Como usar as Transmissões do Lichess.", + "broadcastTheNewRoundHelp": "A nova ronda terá os mesmos membros e contribuidores que a anterior.", + "broadcastAddRound": "Adicionar uma ronda", + "broadcastOngoing": "A decorrer", + "broadcastUpcoming": "Brevemente", + "broadcastCompleted": "Concluído", + "broadcastCompletedHelp": "Lichess deteta a conclusão da ronda baseada nos jogos da fonte. Use essa opção se não houver fonte.", + "broadcastRoundName": "Nome da ronda", + "broadcastRoundNumber": "Número da ronda", + "broadcastTournamentName": "Nome do torneio", + "broadcastTournamentDescription": "Breve descrição do torneio", + "broadcastFullDescription": "Descrição completa do evento", + "broadcastFullDescriptionHelp": "Descrição longa do evento opcional da transmissão. {param1} está disponível. Tem de ter menos que {param2} carácteres.", + "broadcastSourceSingleUrl": "URL da fonte PGN", + "broadcastSourceUrlHelp": "Link que o Lichess vai verificar para obter atualizações da PGN. Deve ser acessível ao público a partir da internet.", + "broadcastSourceGameIds": "Até 64 IDs de jogo Lichess, separados por espaços.", + "broadcastStartDateTimeZone": "Data de início no fuso horário local do torneio: {param}", + "broadcastStartDateHelp": "Opcional, se souberes quando começa o evento", + "broadcastCurrentGameUrl": "Link da partida atual", + "broadcastDownloadAllRounds": "Transferir todas as rondas", + "broadcastResetRound": "Reiniciar esta ronda", + "broadcastDeleteRound": "Apagar esta ronda", + "broadcastDefinitivelyDeleteRound": "Eliminar definitivamente a ronda e os seus jogos.", + "broadcastDeleteAllGamesOfThisRound": "Eliminar todos os jogos desta ronda. A fonte deverá estar ativa para poder recriá-los.", + "broadcastEditRoundStudy": "Editar estudo da ronda", + "broadcastDeleteTournament": "Eliminar este torneio", + "broadcastDefinitivelyDeleteTournament": "Excluir definitivamente todo o torneio, todas as rondas e todos os jogos.", + "broadcastShowScores": "Mostra as pontuações dos jogadores com base nos resultados dos jogos", + "broadcastReplacePlayerTags": "Opcional: substituir nomes de jogadores, avaliações e títulos", + "broadcastFideFederations": "Federações FIDE", + "broadcastTop10Rating": "10 melhores classificações", + "broadcastFidePlayers": "Jogadores FIDE", + "broadcastFidePlayerNotFound": "Jogador FIDE não encontrado", + "broadcastFideProfile": "Perfil FIDE", + "broadcastFederation": "Federação", + "broadcastAgeThisYear": "Idade neste ano", + "broadcastUnrated": "Sem classificação", + "broadcastRecentTournaments": "Torneio recentes", + "broadcastOpenLichess": "Abrir no Lichess", + "broadcastTeams": "Equipas", + "broadcastBoards": "Tabuleiros", + "broadcastOverview": "Visão geral", + "broadcastSubscribeTitle": "Subscreva para ser notificado quando cada ronda começar. Podes ativar o sino ou as notificações push para transmissões nas preferências da tua conta.", + "broadcastUploadImage": "Carregar imagem do torneio", + "broadcastNoBoardsYet": "Ainda não há tabuleiros. Estes aparecerão assim que os jogos forem carregados.", + "broadcastBoardsCanBeLoaded": "Os tabuleiros podem ser carregados com uma fonte ou através do {param}", + "broadcastStartsAfter": "Começa após {param}", + "broadcastStartVerySoon": "A transmissão terá início muito em breve.", + "broadcastNotYetStarted": "A transmissão ainda não começou.", + "broadcastOfficialWebsite": "Website oficial", + "broadcastStandings": "Classificações", + "broadcastIframeHelp": "Mais opções na {param}", + "broadcastWebmastersPage": "página webmasters", + "broadcastPgnSourceHelp": "Uma fonte PGN pública em tempo real para esta ronda. Oferecemos também a {param} para uma sincronização mais rápida e eficiente.", + "broadcastEmbedThisBroadcast": "Incorporar esta transmissão no teu website", + "broadcastEmbedThisRound": "Incorporar {param} no teu website", + "broadcastRatingDiff": "Diferença de Elo", + "broadcastGamesThisTournament": "Jogos deste torneio", + "broadcastScore": "Pontuação", + "broadcastNbBroadcasts": "{count, plural, =1{{count} transmissão} other{{count} transmissões}}", "challengeChallengesX": "Desafios: {param1}", "challengeChallengeToPlay": "Desafiar para jogar", "challengeChallengeDeclined": "Desafio recusado", @@ -377,8 +449,8 @@ "puzzleThemeXRayAttackDescription": "Uma peça ataque ou defende uma casa através de uma peça inimiga.", "puzzleThemeZugzwang": "Zugzwang", "puzzleThemeZugzwangDescription": "O adversário está limitado quanto aos seus movimentos, e todas as jogadas pioram a sua posição.", - "puzzleThemeHealthyMix": "Mistura saudável", - "puzzleThemeHealthyMixDescription": "Um pouco de tudo. Não sabes o que esperar, então ficas pronto para qualquer coisa! Exatamente como em jogos de verdade.", + "puzzleThemeMix": "Mistura saudável", + "puzzleThemeMixDescription": "Um pouco de tudo. Não sabes o que esperar, então ficas pronto para qualquer coisa! Exatamente como em jogos de verdade.", "puzzleThemePlayerGames": "Jogos de jogadores", "puzzleThemePlayerGamesDescription": "Procura problemas gerados a partir dos teus jogos ou de jogos de outro jogador.", "puzzleThemePuzzleDownloadInformation": "Esses problemas são do domínio público e podem ser obtidos em {param}.", @@ -509,7 +581,6 @@ "memory": "Memória", "infiniteAnalysis": "Análise infinita", "removesTheDepthLimit": "Remove o limite de profundidade e mantém o teu computador quente", - "engineManager": "Gestão do motor", "blunder": "Erro grave", "mistake": "Erro", "inaccuracy": "Imprecisão", @@ -535,6 +606,7 @@ "latestForumPosts": "Últimas publicações no fórum", "players": "Jogadores", "friends": "Amigos", + "otherPlayers": "outros jogadores", "discussions": "Conversas", "today": "Hoje", "yesterday": "Ontem", @@ -792,6 +864,8 @@ "descPrivateHelp": "Texto que apenas está visível para os membros da equipa. Se definido, substitui a descrição pública dos membros da equipa.", "no": "Não", "yes": "Sim", + "website": "Site", + "mobile": "Telemóvel", "help": "Ajuda:", "createANewTopic": "Criar um novo tópico", "topics": "Tópicos", @@ -810,7 +884,9 @@ "cheat": "Batota", "troll": "Troll", "other": "Outro", - "reportDescriptionHelp": "Inclui o link do(s) jogo(s) e explica o que há de errado com o comportamento deste utilizador. Não digas apenas \"ele faz batota\"; informa-nos como chegaste a essa conclusão. A tua denúncia será processada mais rapidamente se for escrita em inglês.", + "reportCheatBoostHelp": "Cola o(s) link(s) do(s) jogo(s) e explica o que está errado no comportamento deste utilizador. Não digas apenas “eles fazem batota”, mas diz-nos como chegaste a essa conclusão.", + "reportUsernameHelp": "Explica o que este nome de utilizador tem de ofensivo. Não digas apenas “é ofensivo/inapropriado”, mas diz-nos como chegaste a essa conclusão, especialmente se o insulto for ofuscado, não estiver em inglês, estiver em calão ou for uma referência histórica/cultural.", + "reportProcessedFasterInEnglish": "A tua denúncia será processado mais rapidamente se estiver escrito em inglês.", "error_provideOneCheatedGameLink": "Por favor, fornece-nos pelo menos um link para um jogo onde tenha havido batota.", "by": "por {param}", "importedByX": "Importado por {param}", @@ -1003,7 +1079,7 @@ "zeroAdvertisement": "Zero anúncios", "fullFeatured": "Cheio de recursos", "phoneAndTablet": "Telemóvel e tablet", - "bulletBlitzClassical": "Bullet, blitz, clássico", + "bulletBlitzClassical": "Bullet, rápida, clássica", "correspondenceChess": "Xadrez por correspondência", "onlineAndOfflinePlay": "Jogar online e offline", "viewTheSolution": "Ver a solução", @@ -1208,6 +1284,7 @@ "showMeEverything": "Mostra-me tudo", "lichessPatronInfo": "Lichess é uma instituição de caridade e software de código aberto totalmente livre.\nTodos os custos operacionais, de desenvolvimento e conteúdo são financiados exclusivamente por doações de usuários.", "nothingToSeeHere": "Nada para ver aqui no momento.", + "stats": "Estatísticas", "opponentLeftCounter": "{count, plural, =1{O teu adversário deixou a partida. Podes reivindicar vitória em {count} segundo.} other{O teu adversário deixou a partida. Podes reivindicar vitória em {count} segundos.}}", "mateInXHalfMoves": "{count, plural, =1{Xeque-mate em {count} meio-movimento} other{Xeque-mate em {count} meio-movimentos}}", "nbBlunders": "{count, plural, =1{{count} erro grave} other{{count} erros graves}}", @@ -1304,6 +1381,159 @@ "stormXRuns": "{count, plural, =1{1 partida} other{{count} tentativas}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Jogou uma partida de {param2}} other{Jogou {count} partidas de {param2}}}", "streamerLichessStreamers": "Streamers no Lichess", + "studyPrivate": "Privado", + "studyMyStudies": "Os meus estudos", + "studyStudiesIContributeTo": "Estudos para os quais contribui", + "studyMyPublicStudies": "Os meus estudos públicos", + "studyMyPrivateStudies": "Os meus estudos privados", + "studyMyFavoriteStudies": "Os meus estudos favoritos", + "studyWhatAreStudies": "O que são estudos?", + "studyAllStudies": "Todos os estudos", + "studyStudiesCreatedByX": "Estudos criados por {param}", + "studyNoneYet": "Nenhum ainda.", + "studyHot": "Destaques", + "studyDateAddedNewest": "Data em que foi adicionado (mais recente)", + "studyDateAddedOldest": "Data em que foi adicionado (mais antigo)", + "studyRecentlyUpdated": "Atualizado recentemente", + "studyMostPopular": "Mais popular", + "studyAlphabetical": "Ordem alfabética", + "studyAddNewChapter": "Adicionar um novo capítulo", + "studyAddMembers": "Adicionar membros", + "studyInviteToTheStudy": "Convidar para o estudo", + "studyPleaseOnlyInvitePeopleYouKnow": "Por favor, convida apenas pessoas que conheças e que querem participar ativamente neste estudo.", + "studySearchByUsername": "Pesquisar por nome de utilizador", + "studySpectator": "Espectador", + "studyContributor": "Colaborador", + "studyKick": "Expulsar", + "studyLeaveTheStudy": "Sair do estudo", + "studyYouAreNowAContributor": "Agora és um colaborador", + "studyYouAreNowASpectator": "Agora és um espectador", + "studyPgnTags": "Etiquetas PGN", + "studyLike": "Gostar", + "studyUnlike": "Remover gosto", + "studyNewTag": "Nova etiqueta", + "studyCommentThisPosition": "Comentar esta posição", + "studyCommentThisMove": "Comentar este lance", + "studyAnnotateWithGlyphs": "Anotar com símbolos", + "studyTheChapterIsTooShortToBeAnalysed": "O capítulo é demasiado curto para ser analisado.", + "studyOnlyContributorsCanRequestAnalysis": "Apenas os colaboradores de estudo podem solicitar uma análise de computador.", + "studyGetAFullComputerAnalysis": "Obtém uma análise completa da linha principal pelo servidor.", + "studyMakeSureTheChapterIsComplete": "Certifica-te que o capítulo está completo. Só podes solicitar a análise uma vez.", + "studyAllSyncMembersRemainOnTheSamePosition": "Todos os membros do SYNC permanecem na mesma posição", + "studyShareChanges": "Partilha as alterações com espectadores e guarda-as no servidor", + "studyPlaying": "A ser jogado", + "studyShowEvalBar": "Barras de avaliação", + "studyFirst": "Primeira", + "studyPrevious": "Anterior", + "studyNext": "Seguinte", + "studyLast": "Última", "studyShareAndExport": "Partilhar & exportar", - "studyStart": "Iniciar" + "studyCloneStudy": "Clonar", + "studyStudyPgn": "PGN do estudo", + "studyDownloadAllGames": "Transferir todas as partidas", + "studyChapterPgn": "PGN do capítulo", + "studyCopyChapterPgn": "Copiar PGN", + "studyDownloadGame": "Transferir partida", + "studyStudyUrl": "URL do estudo", + "studyCurrentChapterUrl": "URL do capítulo atual", + "studyYouCanPasteThisInTheForumToEmbed": "Podes colocar isto no fórum para o incorporares", + "studyStartAtInitialPosition": "Começar na posição inicial", + "studyStartAtX": "Começar em {param}", + "studyEmbedInYourWebsite": "Incorporar no teu site ou blog", + "studyReadMoreAboutEmbedding": "Ler mais sobre incorporação", + "studyOnlyPublicStudiesCanBeEmbedded": "Só estudos públicos é que podem ser incorporados!", + "studyOpen": "Abrir", + "studyXBroughtToYouByY": "{param1}, trazido a si pelo {param2}", + "studyStudyNotFound": "Estudo não encontrado", + "studyEditChapter": "Editar capítulo", + "studyNewChapter": "Novo capítulo", + "studyImportFromChapterX": "Importar de {param}", + "studyOrientation": "Orientação", + "studyAnalysisMode": "Modo de análise", + "studyPinnedChapterComment": "Comentário de capítulo afixado", + "studySaveChapter": "Guardar capítulo", + "studyClearAnnotations": "Limpar anotações", + "studyClearVariations": "Limpar variações", + "studyDeleteChapter": "Eliminar capítulo", + "studyDeleteThisChapter": "Eliminar este capítulo? Não há volta atrás!", + "studyClearAllCommentsInThisChapter": "Apagar todos os comentários e símbolos após lances neste capítulo?", + "studyRightUnderTheBoard": "Mesmo por baixo do tabuleiro", + "studyNoPinnedComment": "Nenhum", + "studyNormalAnalysis": "Análise normal", + "studyHideNextMoves": "Ocultar os próximos movimentos", + "studyInteractiveLesson": "Lição interativa", + "studyChapterX": "Capítulo {param}", + "studyEmpty": "Vazio", + "studyStartFromInitialPosition": "Começar da posição inicial", + "studyEditor": "Editor", + "studyStartFromCustomPosition": "Iniciar de uma posição personalizada", + "studyLoadAGameByUrl": "Carregar um jogo por URL", + "studyLoadAPositionFromFen": "Carregar uma posição por FEN", + "studyLoadAGameFromPgn": "Carregar um jogo por PGN", + "studyAutomatic": "Automática", + "studyUrlOfTheGame": "URL do jogo", + "studyLoadAGameFromXOrY": "Carregar um jogo do {param1} ou de {param2}", + "studyCreateChapter": "Criar capítulo", + "studyCreateStudy": "Criar estudo", + "studyEditStudy": "Editar estudo", + "studyVisibility": "Visibilidade", + "studyPublic": "Público", + "studyUnlisted": "Não listado", + "studyInviteOnly": "Apenas por convite", + "studyAllowCloning": "Permitir clonagem", + "studyNobody": "Ninguém", + "studyOnlyMe": "Apenas eu", + "studyContributors": "Contribuidores", + "studyMembers": "Membros", + "studyEveryone": "Toda a gente", + "studyEnableSync": "Ativar sincronização", + "studyYesKeepEveryoneOnTheSamePosition": "Sim: mantenha toda a gente na mesma posição", + "studyNoLetPeopleBrowseFreely": "Não: deixa as pessoas navegarem livremente", + "studyPinnedStudyComment": "Comentário de estudo fixo", + "studyStart": "Iniciar", + "studySave": "Guardar", + "studyClearChat": "Limpar o chat", + "studyDeleteTheStudyChatHistory": "Apagar o histórico do chat do estudo? Não há volta atrás!", + "studyDeleteStudy": "Eliminar estudo", + "studyConfirmDeleteStudy": "Eliminar todo o estudo? Não há volta atrás! Digite o nome do estudo para confirmar: {param}", + "studyWhereDoYouWantToStudyThat": "Onde queres estudar isso?", + "studyGoodMove": "Boa jogada", + "studyMistake": "Erro", + "studyBrilliantMove": "Jogada brilhante", + "studyBlunder": "Erro grave", + "studyInterestingMove": "Lance interessante", + "studyDubiousMove": "Lance duvidoso", + "studyOnlyMove": "Lance único", + "studyZugzwang": "Zugzwang", + "studyEqualPosition": "Posição igual", + "studyUnclearPosition": "Posição não clara", + "studyWhiteIsSlightlyBetter": "As brancas estão ligeiramente melhor", + "studyBlackIsSlightlyBetter": "As pretas estão ligeiramente melhor", + "studyWhiteIsBetter": "As brancas estão melhor", + "studyBlackIsBetter": "As pretas estão melhor", + "studyWhiteIsWinning": "Brancas estão ganhando", + "studyBlackIsWinning": "Pretas estão ganhando", + "studyNovelty": "Novidade teórica", + "studyDevelopment": "Desenvolvimento", + "studyInitiative": "Iniciativa", + "studyAttack": "Ataque", + "studyCounterplay": "Contra-jogo", + "studyTimeTrouble": "Pouco tempo", + "studyWithCompensation": "Com compensação", + "studyWithTheIdea": "Com a ideia", + "studyNextChapter": "Próximo capítulo", + "studyPrevChapter": "Capítulo anterior", + "studyStudyActions": "Opções de estudo", + "studyTopics": "Tópicos", + "studyMyTopics": "Os meus tópicos", + "studyPopularTopics": "Tópicos populares", + "studyManageTopics": "Gerir tópicos", + "studyBack": "Voltar", + "studyPlayAgain": "Jogar novamente", + "studyWhatWouldYouPlay": "O que jogaria nessa situação?", + "studyYouCompletedThisLesson": "Parabéns! Completou esta lição.", + "studyNbChapters": "{count, plural, =1{{count} capítulo} other{{count} capítulos}}", + "studyNbGames": "{count, plural, =1{{count} Jogo} other{{count} Jogos}}", + "studyNbMembers": "{count, plural, =1{{count} membro} other{{count} membros}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Cole seu texto PGN aqui, até {count} jogo} other{Cole seu texto PGN aqui, até {count} jogos}}" } \ No newline at end of file diff --git a/lib/l10n/lila_pt_BR.arb b/lib/l10n/lila_pt_BR.arb index 85cfe6863c..e099ca8d11 100644 --- a/lib/l10n/lila_pt_BR.arb +++ b/lib/l10n/lila_pt_BR.arb @@ -9,9 +9,9 @@ "mobileFeedbackButton": "Comentários", "mobileOkButton": "Ok", "mobileSettingsHapticFeedback": "Vibrar ao trocar", - "mobileSettingsImmersiveMode": "Modo imerssivo", - "mobileSettingsImmersiveModeSubtitle": "Ocultar a “interface” do sistema durante a reprodução. Use isto se você estiver incomodado com gestor de navegação do sistema nas bordas da tela. Aplica-se as telas dos jogos e desafios.", - "mobileNotFollowingAnyUser": "Você não estar seguindo nenhum usuário.", + "mobileSettingsImmersiveMode": "Modo imersivo", + "mobileSettingsImmersiveModeSubtitle": "Ocultar a “interface” do sistema durante a reprodução. Use isto se você estiver incomodado com gestor de navegação do sistema nas bordas da tela. Aplica-se às telas dos jogos e desafios.", + "mobileNotFollowingAnyUser": "Você não está seguindo nenhum usuário.", "mobileAllGames": "Todos os jogos", "mobileRecentSearches": "Pesquisas recentes", "mobileClearButton": "Limpar", @@ -20,17 +20,16 @@ "mobileAreYouSure": "Você tem certeza?", "mobilePuzzleStreakAbortWarning": "Você perderá a sua sequência atual e sua pontuação será salva.", "mobilePuzzleStormNothingToShow": "Nada para mostrar aqui. Jogue algumas rodadas da Puzzle Storm.", - "mobileSharePuzzle": "Tentar novamente este quebra-cabeça", + "mobileSharePuzzle": "Compartilhar este quebra-cabeça", "mobileShareGameURL": "Compartilhar URL do jogo", "mobileShareGamePGN": "Compartilhar PGN", "mobileSharePositionAsFEN": "Compartilhar posição como FEN", - "mobileShowVariations": "Mostrar setas da variantes", + "mobileShowVariations": "Mostrar setas de variantes", "mobileHideVariation": "Ocultar variante forçada", "mobileShowComments": "Mostrar comentários", "mobilePuzzleStormConfirmEndRun": "Você quer terminar o turno?", "mobilePuzzleStormFilterNothingToShow": "Nada para mostrar aqui, por favor, altere os filtros", "mobileCancelTakebackOffer": "Cancelar oferta de revanche", - "mobileCancelDrawOffer": "Cancelar oferta de revanche", "mobileWaitingForOpponentToJoin": "Esperando por um oponente...", "mobileBlindfoldMode": "Venda", "mobileLiveStreamers": "Streamers do Lichess", @@ -42,6 +41,7 @@ "mobilePuzzleStormSubtitle": "Resolva quantos quebra-cabeças for possível em 3 minutos.", "mobileGreeting": "Olá, {param}", "mobileGreetingWithoutName": "Olá", + "mobilePrefMagnifyDraggedPiece": "Ampliar peça segurada", "activityActivity": "Atividade", "activityHostedALiveStream": "Iniciou uma transmissão ao vivo", "activityRankedInSwissTournament": "Classificado #{param1} entre {param2}", @@ -54,6 +54,7 @@ "activityPlayedNbMoves": "{count, plural, =1{Jogou {count} movimento} other{Jogou {count} movimentos}}", "activityInNbCorrespondenceGames": "{count, plural, =1{em {count} jogo por correspondência} other{em {count} jogos por correspondência}}", "activityCompletedNbGames": "{count, plural, =1{Completou {count} jogo por correspondência} other{Completou {count} jogos por correspondência}}", + "activityCompletedNbVariantGames": "{count, plural, =1{Completou {count} {param2} partida por correspondência} other{{count} {param2} partidas por correspondência finalizadas}}", "activityFollowedNbPlayers": "{count, plural, =1{Começou a seguir {count} jogador} other{Começou a seguir {count} jogadores}}", "activityGainedNbFollowers": "{count, plural, =1{Ganhou {count} novo seguidor} other{Ganhou {count} novos seguidores}}", "activityHostedNbSimuls": "{count, plural, =1{Hospedou {count} exibição simultânea} other{Hospedou {count} exibições simultâneas}}", @@ -64,7 +65,72 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{Competiu em {count} torneio suíço} other{Competiu em {count} torneios suíços}}", "activityJoinedNbTeams": "{count, plural, =1{Entrou na {count} equipe} other{Entrou nas {count} equipes}}", "broadcastBroadcasts": "Transmissões", + "broadcastMyBroadcasts": "Minhas transmissões", "broadcastLiveBroadcasts": "Transmissões ao vivo do torneio", + "broadcastBroadcastCalendar": "Calendário das transmissões", + "broadcastNewBroadcast": "Nova transmissão ao vivo", + "broadcastSubscribedBroadcasts": "Transmissões em que você se inscreveu", + "broadcastAboutBroadcasts": "Sobre as transmissões", + "broadcastHowToUseLichessBroadcasts": "Como usar as transmissões do Lichess.", + "broadcastTheNewRoundHelp": "A nova rodada terá os mesmos membros e colaboradores que a anterior.", + "broadcastAddRound": "Adicionar uma rodada", + "broadcastOngoing": "Em andamento", + "broadcastUpcoming": "Próximos", + "broadcastCompleted": "Concluído", + "broadcastCompletedHelp": "O Lichess detecta o fim da rodada baseado nos jogos fonte. Use essa opção se não houver fonte.", + "broadcastRoundName": "Nome da rodada", + "broadcastRoundNumber": "Número da rodada", + "broadcastTournamentName": "Nome do torneio", + "broadcastTournamentDescription": "Descrição curta do torneio", + "broadcastFullDescription": "Descrição completa do evento", + "broadcastFullDescriptionHelp": "Descrição longa e opcional da transmissão. {param1} está disponível. O tamanho deve ser menor que {param2} caracteres.", + "broadcastSourceSingleUrl": "URL de origem de PGN", + "broadcastSourceUrlHelp": "URL que Lichess irá verificar para obter atualizações PGN. Deve ser acessível ao público a partir da Internet.", + "broadcastSourceGameIds": "Até 64 IDs de partidas do Lichess, separados por espaços.", + "broadcastStartDateTimeZone": "Data de início no horário local do torneio: {param}", + "broadcastStartDateHelp": "Opcional, se você sabe quando o evento começa", + "broadcastCurrentGameUrl": "URL da partida atual", + "broadcastDownloadAllRounds": "Baixar todas as rodadas", + "broadcastResetRound": "Reiniciar esta rodada", + "broadcastDeleteRound": "Excluir esta rodada", + "broadcastDefinitivelyDeleteRound": "Deletar permanentemente todas as partidas desta rodada.", + "broadcastDeleteAllGamesOfThisRound": "Deletar todas as partidas desta rodada. A fonte deverá estar ativa para criá-las novamente.", + "broadcastEditRoundStudy": "Editar estudo da rodada", + "broadcastDeleteTournament": "Excluir este torneio", + "broadcastDefinitivelyDeleteTournament": "Excluir permanentemente todo o torneio, incluindo todas as rodadas e jogos.", + "broadcastShowScores": "Mostrar pontuações dos jogadores com base nos resultados das partidas", + "broadcastReplacePlayerTags": "Opcional: substituir nomes de jogador, ratings e títulos", + "broadcastFideFederations": "Federações FIDE", + "broadcastTop10Rating": "Classificação top 10", + "broadcastFidePlayers": "Jogadores FIDE", + "broadcastFidePlayerNotFound": "Jogador não encontrando na FIDE", + "broadcastFideProfile": "Perfil FIDE", + "broadcastFederation": "Federação", + "broadcastAgeThisYear": "Idade atual", + "broadcastUnrated": "Sem rating", + "broadcastRecentTournaments": "Torneios recentes", + "broadcastOpenLichess": "Abrir no Lichess", + "broadcastTeams": "Equipes", + "broadcastBoards": "Tabuleiros", + "broadcastOverview": "Visão geral", + "broadcastSubscribeTitle": "Inscreva-se para ser notificado no início de cada rodada. Você pode configurar as notificações de transmissões nas suas preferências.", + "broadcastUploadImage": "Enviar imagem de torneio", + "broadcastNoBoardsYet": "Sem tabuleiros ainda. Eles vão aparecer quando os jogos forem enviados.", + "broadcastBoardsCanBeLoaded": "Tabuleiros são carregados com uma fonte ou pelo {param}", + "broadcastStartsAfter": "Começa após {param}", + "broadcastStartVerySoon": "A transmissão começará em breve.", + "broadcastNotYetStarted": "A transmissão ainda não começou.", + "broadcastOfficialWebsite": "Site oficial", + "broadcastStandings": "Classificação", + "broadcastIframeHelp": "Mais opções na {param}", + "broadcastWebmastersPage": "página dos webmasters", + "broadcastPgnSourceHelp": "Uma fonte PGN pública ao vivo desta rodada. Há também a {param} para uma sincronização mais rápida e eficiente.", + "broadcastEmbedThisBroadcast": "Incorporar essa transmissão em seu site", + "broadcastEmbedThisRound": "Incorporar {param} em seu site", + "broadcastRatingDiff": "Diferência de pontos", + "broadcastGamesThisTournament": "Jogos neste torneio", + "broadcastScore": "Pontuação", + "broadcastNbBroadcasts": "{count, plural, =1{{count} transmissão} other{{count} transmissões}}", "challengeChallengesX": "Desafios: {param1}", "challengeChallengeToPlay": "Desafiar para jogar", "challengeChallengeDeclined": "Desafio recusado", @@ -383,8 +449,8 @@ "puzzleThemeXRayAttackDescription": "Uma peça ataca ou defende uma casa indiretamente, através de uma peça adversária.", "puzzleThemeZugzwang": "Zugzwang", "puzzleThemeZugzwangDescription": "O adversário tem os seus movimentos limitados, e qualquer movimento que ele faça vai enfraquecer sua própria posição.", - "puzzleThemeHealthyMix": "Combinação saudável", - "puzzleThemeHealthyMixDescription": "Um pouco de tudo. Você nunca sabe o que vai encontrar, então esteja pronto para tudo! Igualzinho aos jogos em tabuleiros reais.", + "puzzleThemeMix": "Combinação saudável", + "puzzleThemeMixDescription": "Um pouco de tudo. Você nunca sabe o que vai encontrar, então esteja pronto para tudo! Igualzinho aos jogos em tabuleiros reais.", "puzzleThemePlayerGames": "Partidas de jogadores", "puzzleThemePlayerGamesDescription": "Procure quebra-cabeças gerados a partir de suas partidas ou das de outro jogador.", "puzzleThemePuzzleDownloadInformation": "Esses quebra-cabeças estão em domínio público, e você pode baixá-los em {param}.", @@ -515,7 +581,6 @@ "memory": "Memória", "infiniteAnalysis": "Análise infinita", "removesTheDepthLimit": "Remove o limite de profundidade, o que aquece seu computador", - "engineManager": "Gerenciador de engine", "blunder": "Capivarada", "mistake": "Erro", "inaccuracy": "Imprecisão", @@ -597,6 +662,7 @@ "rank": "Rank", "rankX": "Classificação: {param}", "gamesPlayed": "Partidas realizadas", + "ok": "OK", "cancel": "Cancelar", "whiteTimeOut": "Tempo das brancas esgotado", "blackTimeOut": "Tempo das pretas esgotado", @@ -819,7 +885,9 @@ "cheat": "Trapaça", "troll": "Troll", "other": "Outro", - "reportDescriptionHelp": "Cole o link do(s) jogo(s) e explique o que há de errado com o comportamento do usuário. Não diga apenas \"ele trapaceia\", informe-nos como chegou a esta conclusão. Sua denúncia será processada mais rapidamente se escrita em inglês.", + "reportCheatBoostHelp": "Cole o link do(s) jogo(s) e explique o que há de errado com o comportamento do usuário. Não diga apenas \"ele trapaceia\", informe-nos como chegou a esta conclusão.", + "reportUsernameHelp": "Explique porque este nome de usuário é ofensivo. Não diga apenas \"é ofensivo/inapropriado\", mas nos diga como chegou a essa conclusão especialmente se o insulto for ofuscado, não estiver em inglês, for uma gíria, ou for uma referência histórica/cultural.", + "reportProcessedFasterInEnglish": "A sua denúncia será processada mais rápido se for escrita em inglês.", "error_provideOneCheatedGameLink": "Por favor forneça ao menos um link para um jogo com suspeita de trapaça.", "by": "por {param}", "importedByX": "Importado por {param}", @@ -1217,6 +1285,7 @@ "showMeEverything": "Mostrar tudo", "lichessPatronInfo": "Lichess é um software de código aberto, totalmente grátis e sem fins lucrativos. Todos os custos operacionais, de desenvolvimento, e os conteúdos são financiados unicamente através de doações de usuários.", "nothingToSeeHere": "Nada para ver aqui no momento.", + "stats": "Estatísticas", "opponentLeftCounter": "{count, plural, =1{O seu adversário deixou a partida. Você pode reivindicar vitória em {count} segundo.} other{O seu adversário deixou a partida. Você pode reivindicar vitória em {count} segundos.}}", "mateInXHalfMoves": "{count, plural, =1{Mate em {count} lance} other{Mate em {count} lances}}", "nbBlunders": "{count, plural, =1{{count} capivarada} other{{count} capivaradas}}", @@ -1224,7 +1293,7 @@ "nbInaccuracies": "{count, plural, =1{{count} imprecisão} other{{count} imprecisões}}", "nbPlayers": "{count, plural, =1{{count} jogadores conectados} other{{count} jogadores conectados}}", "nbGames": "{count, plural, =1{{count} partida} other{{count} partidas}}", - "ratingXOverYGames": "{count, plural, =1{Rating {count} em {param2} jogo} other{Rating {count} após {param2} partidas}}", + "ratingXOverYGames": "{count, plural, =1{Rating {count} após {param2} partida} other{Rating {count} após {param2} partidas}}", "nbBookmarks": "{count, plural, =1{{count} Favoritos} other{{count} Favoritos}}", "nbDays": "{count, plural, =1{{count} dias} other{{count} dias}}", "nbHours": "{count, plural, =1{{count} horas} other{{count} horas}}", @@ -1313,6 +1382,159 @@ "stormXRuns": "{count, plural, =1{1 tentativa} other{{count} séries}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Jogou uma tentativa de {param2}} other{Jogou {count} tentativas de {param2}}}", "streamerLichessStreamers": "Streamers do Lichess", + "studyPrivate": "Privado", + "studyMyStudies": "Meus estudos", + "studyStudiesIContributeTo": "Estudos para os quais contribuí", + "studyMyPublicStudies": "Meus estudos públicos", + "studyMyPrivateStudies": "Meus estudos privados", + "studyMyFavoriteStudies": "Meus estudos favoritos", + "studyWhatAreStudies": "O que são estudos?", + "studyAllStudies": "Todos os estudos", + "studyStudiesCreatedByX": "Estudos criados por {param}", + "studyNoneYet": "Nenhum ainda.", + "studyHot": "Em alta", + "studyDateAddedNewest": "Data de criação (mais recente)", + "studyDateAddedOldest": "Data de criação (mais antiga)", + "studyRecentlyUpdated": "Atualizado recentemente", + "studyMostPopular": "Mais populares", + "studyAlphabetical": "Em ordem alfabética", + "studyAddNewChapter": "Adicionar um novo capítulo", + "studyAddMembers": "Adicionar membros", + "studyInviteToTheStudy": "Convidar para o estudo", + "studyPleaseOnlyInvitePeopleYouKnow": "Por favor, convide apenas pessoas que você conhece e que queiram participar efetivamente deste estudo.", + "studySearchByUsername": "Pesquisar por nome de usuário", + "studySpectator": "Espectador", + "studyContributor": "Colaborador", + "studyKick": "Expulsar", + "studyLeaveTheStudy": "Sair deste estudo", + "studyYouAreNowAContributor": "Agora você é um(a) colaborador(a)", + "studyYouAreNowASpectator": "Você agora é um(a) espectador(a)", + "studyPgnTags": "Etiquetas PGN", + "studyLike": "Gostei", + "studyUnlike": "Não gostei", + "studyNewTag": "Nova etiqueta", + "studyCommentThisPosition": "Comente sobre esta posição", + "studyCommentThisMove": "Comente sobre este lance", + "studyAnnotateWithGlyphs": "Anotar com símbolos", + "studyTheChapterIsTooShortToBeAnalysed": "O capítulo é muito curto para ser analisado.", + "studyOnlyContributorsCanRequestAnalysis": "Apenas os colaboradores de um estudo podem solicitar uma análise de computador.", + "studyGetAFullComputerAnalysis": "Obter uma análise completa dos movimentos pelo servidor.", + "studyMakeSureTheChapterIsComplete": "Certifique-se de que o capítulo está completo. Você só pode solicitar análise uma vez.", + "studyAllSyncMembersRemainOnTheSamePosition": "Todos os membros em SYNC veem a mesma posição", + "studyShareChanges": "Compartilhar as alterações com os espectadores e salvá-las no servidor", + "studyPlaying": "Jogando", + "studyShowEvalBar": "Barras de avaliação", + "studyFirst": "Primeiro", + "studyPrevious": "Anterior", + "studyNext": "Próximo", + "studyLast": "Último", "studyShareAndExport": "Compartilhar & exportar", - "studyStart": "Iniciar" + "studyCloneStudy": "Duplicar", + "studyStudyPgn": "PGN de estudo", + "studyDownloadAllGames": "Baixar todas as partidas", + "studyChapterPgn": "PGN do capítulo", + "studyCopyChapterPgn": "Copiar PGN", + "studyDownloadGame": "Baixar partida", + "studyStudyUrl": "URL de estudo", + "studyCurrentChapterUrl": "URL do capítulo atual", + "studyYouCanPasteThisInTheForumToEmbed": "Você pode colar isso no fórum para incluir o estudo na publicação", + "studyStartAtInitialPosition": "Começar na posição inicial", + "studyStartAtX": "Começar em {param}", + "studyEmbedInYourWebsite": "Incorporar em seu site ou blog", + "studyReadMoreAboutEmbedding": "Leia mais sobre como incorporar", + "studyOnlyPublicStudiesCanBeEmbedded": "Apenas os estudos públicos podem ser incorporados!", + "studyOpen": "Abertura", + "studyXBroughtToYouByY": "{param1}, disponibilizado por {param2}", + "studyStudyNotFound": "Estudo não encontrado", + "studyEditChapter": "Editar capítulo", + "studyNewChapter": "Novo capítulo", + "studyImportFromChapterX": "Importar de {param}", + "studyOrientation": "Orientação", + "studyAnalysisMode": "Modo de análise", + "studyPinnedChapterComment": "Comentário de capítulo afixado", + "studySaveChapter": "Salvar capítulo", + "studyClearAnnotations": "Remover anotações", + "studyClearVariations": "Limpar variantes", + "studyDeleteChapter": "Excluir capítulo", + "studyDeleteThisChapter": "Excluir este capítulo? Não há volta!", + "studyClearAllCommentsInThisChapter": "Remover todos os comentários e formas deste capítulo?", + "studyRightUnderTheBoard": "Logo abaixo do tabuleiro", + "studyNoPinnedComment": "Nenhum", + "studyNormalAnalysis": "Análise normal", + "studyHideNextMoves": "Ocultar próximos movimentos", + "studyInteractiveLesson": "Lição interativa", + "studyChapterX": "Capítulo {param}", + "studyEmpty": "Vazio", + "studyStartFromInitialPosition": "Reiniciar para posição inicial", + "studyEditor": "Editor", + "studyStartFromCustomPosition": "Iniciar com posição personalizada", + "studyLoadAGameByUrl": "Carregar um jogo por URL", + "studyLoadAPositionFromFen": "Carregar uma posição com FEN", + "studyLoadAGameFromPgn": "Carregar um jogo com PGN", + "studyAutomatic": "Automático", + "studyUrlOfTheGame": "URL do jogo", + "studyLoadAGameFromXOrY": "Carregar um jogo de {param1} ou {param2}", + "studyCreateChapter": "Criar capítulo", + "studyCreateStudy": "Criar estudo", + "studyEditStudy": "Editar estudo", + "studyVisibility": "Visibilidade", + "studyPublic": "Público", + "studyUnlisted": "Não listado", + "studyInviteOnly": "Apenas por convite", + "studyAllowCloning": "Permitir clonagem", + "studyNobody": "Ninguém", + "studyOnlyMe": "Apenas eu", + "studyContributors": "Colaboradores", + "studyMembers": "Membros", + "studyEveryone": "Todos", + "studyEnableSync": "Ativar sincronização", + "studyYesKeepEveryoneOnTheSamePosition": "Sim: mantenha todos na mesma posição", + "studyNoLetPeopleBrowseFreely": "Não: deixe as pessoas navegarem livremente", + "studyPinnedStudyComment": "Comentário de estudo afixado", + "studyStart": "Iniciar", + "studySave": "Salvar", + "studyClearChat": "Limpar conversação", + "studyDeleteTheStudyChatHistory": "Excluir o histórico de conversação do estudo? Não há volta!", + "studyDeleteStudy": "Excluir estudo", + "studyConfirmDeleteStudy": "Excluir todo o estudo? Não há volta! Digite o nome do estudo para confirmar: {param}", + "studyWhereDoYouWantToStudyThat": "Onde você quer estudar?", + "studyGoodMove": "Boa jogada", + "studyMistake": "Erro", + "studyBrilliantMove": "Jogada excelente", + "studyBlunder": "Erro grave", + "studyInterestingMove": "Jogada interessante", + "studyDubiousMove": "Lance questionável", + "studyOnlyMove": "Única jogada", + "studyZugzwang": "Zugzwang", + "studyEqualPosition": "Posição igual", + "studyUnclearPosition": "Posição incerta", + "studyWhiteIsSlightlyBetter": "Brancas estão um pouco melhor", + "studyBlackIsSlightlyBetter": "Pretas estão um pouco melhor", + "studyWhiteIsBetter": "Brancas estão melhor", + "studyBlackIsBetter": "Pretas estão melhor", + "studyWhiteIsWinning": "Brancas estão ganhando", + "studyBlackIsWinning": "Pretas estão ganhando", + "studyNovelty": "Novidade", + "studyDevelopment": "Desenvolvimento", + "studyInitiative": "Iniciativa", + "studyAttack": "Ataque", + "studyCounterplay": "Contra-ataque", + "studyTimeTrouble": "Problema de tempo", + "studyWithCompensation": "Com compensação", + "studyWithTheIdea": "Com a ideia", + "studyNextChapter": "Próximo capítulo", + "studyPrevChapter": "Capítulo anterior", + "studyStudyActions": "Opções de estudo", + "studyTopics": "Tópicos", + "studyMyTopics": "Meus tópicos", + "studyPopularTopics": "Tópicos populares", + "studyManageTopics": "Gerenciar tópicos", + "studyBack": "Voltar", + "studyPlayAgain": "Jogar novamente", + "studyWhatWouldYouPlay": "O que você jogaria nessa posição?", + "studyYouCompletedThisLesson": "Parabéns! Você completou essa lição.", + "studyNbChapters": "{count, plural, =1{{count} Capítulo} other{{count} Capítulos}}", + "studyNbGames": "{count, plural, =1{{count} Jogo} other{{count} Jogos}}", + "studyNbMembers": "{count, plural, =1{{count} Membro} other{{count} Membros}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Cole seu texto PGN aqui, até {count} jogo} other{Cole seu texto PGN aqui, até {count} jogos}}" } \ No newline at end of file diff --git a/lib/l10n/lila_ro.arb b/lib/l10n/lila_ro.arb index 06e6ade905..b47783dbb8 100644 --- a/lib/l10n/lila_ro.arb +++ b/lib/l10n/lila_ro.arb @@ -1,15 +1,47 @@ { "mobileHomeTab": "Acasă", + "mobilePuzzlesTab": "Puzzle-uri", + "mobileToolsTab": "Unelte", + "mobileWatchTab": "Vizionează", "mobileSettingsTab": "Setări", "mobileMustBeLoggedIn": "Trebuie să te autentifici pentru a accesa această pagină.", + "mobileSystemColors": "Culori sistem", "mobileFeedbackButton": "Feedback", "mobileOkButton": "OK", + "mobileSettingsHapticFeedback": "Control tactil", + "mobileSettingsImmersiveMode": "Mod imersiv", + "mobileSettingsImmersiveModeSubtitle": "Ascunde interfața de utilizator a sistemului în timpul jocului. Folosește această opțiune dacă ești deranjat de gesturile de navigare ale sistemului la marginile ecranului. Se aplică pentru ecranele de joc și Puzzle Storm.", + "mobileNotFollowingAnyUser": "Nu urmărești niciun utilizator.", "mobileAllGames": "Toate jocurile", + "mobileRecentSearches": "Căutări recente", + "mobileClearButton": "Resetare", + "mobilePlayersMatchingSearchTerm": "Jucători cu \"{param}\"", + "mobileNoSearchResults": "Niciun rezultat", + "mobileAreYouSure": "Ești sigur?", + "mobilePuzzleStreakAbortWarning": "Îți vei pierde streak-ul actual iar scorul va fi salvat.", + "mobilePuzzleStormNothingToShow": "Nimic de arătat. Jucați câteva partide de Puzzle Storm.", + "mobileSharePuzzle": "Distribuie acest puzzle", + "mobileShareGameURL": "Distribuie URL-ul jocului", + "mobileShareGamePGN": "Distribuie PGN", + "mobileSharePositionAsFEN": "Distribuie poziția ca FEN", + "mobileShowVariations": "Arată variațiile", + "mobileHideVariation": "Ascunde variațiile", "mobileShowComments": "Afişează сomentarii", + "mobilePuzzleStormConfirmEndRun": "Vrei să termini acest run?", + "mobilePuzzleStormFilterNothingToShow": "Nimic de afișat, vă rugăm să schimbați filtrele", + "mobileCancelTakebackOffer": "Anulați propunerea de revanșă", + "mobileWaitingForOpponentToJoin": "În așteptarea unui jucător...", + "mobileBlindfoldMode": "Legat la ochi", + "mobileLiveStreamers": "Fluxuri live", + "mobileCustomGameJoinAGame": "Alătură-te unui joc", + "mobileCorrespondenceClearSavedMove": "Șterge mutarea salvată", + "mobileSomethingWentWrong": "Ceva nu a mers bine. :(", "mobileShowResult": "Arată rezultatul", + "mobilePuzzleThemesSubtitle": "Joacă puzzle-uri din deschiderile tale preferate sau alege o temă.", "mobilePuzzleStormSubtitle": "Rezolvă cât mai multe puzzle-uri în 3 minute.", "mobileGreeting": "Salut, {param}", "mobileGreetingWithoutName": "Salut", + "mobilePrefMagnifyDraggedPiece": "Mărește piesa trasă", "activityActivity": "Activitate", "activityHostedALiveStream": "A găzduit un live stream", "activityRankedInSwissTournament": "Evaluat #{param1} în {param2}", @@ -22,6 +54,7 @@ "activityPlayedNbMoves": "{count, plural, =1{A jucat {count} mutare} few{A jucat {count} mutări} other{A jucat {count} mutări}}", "activityInNbCorrespondenceGames": "{count, plural, =1{în {count} joc de corespondență} few{în {count} jocuri de corespondență} other{în {count} jocuri de corespondență}}", "activityCompletedNbGames": "{count, plural, =1{A completat {count} meci de corespondență} few{A completat {count} meciuri de corespondență} other{A completat {count} meciuri de corespondență}}", + "activityCompletedNbVariantGames": "{count, plural, =1{Terminat {count} joc de tip {param2} de corespondență} few{Terminat {count} jocuri de tip {param2} de corespondență} other{Terminat {count} jocuri de tip {param2} de corespondență}}", "activityFollowedNbPlayers": "{count, plural, =1{A început să urmărească {count} jucător} few{A început să urmărească {count} jucători} other{A început să urmărească {count} jucători}}", "activityGainedNbFollowers": "{count, plural, =1{A obținut {count} urmăritor nou} few{A obținut {count} urmăritori noi} other{A obținut {count} urmăritori noi}}", "activityHostedNbSimuls": "{count, plural, =1{A găzduit {count} simultană de șah} few{A găzduit {count} simultane de șah} other{A găzduit {count} simultane de șah}}", @@ -32,7 +65,53 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{A concurat în {count} turneu elvețian} few{A concurat în {count} turnee elvețiene} other{A concurat în {count} turnee elvețiene}}", "activityJoinedNbTeams": "{count, plural, =1{S-a alăturat la {count} echipă} few{S-a alăturat la {count} echipe} other{S-a alăturat la {count} echipe}}", "broadcastBroadcasts": "Transmisiuni", + "broadcastMyBroadcasts": "Transmisiile mele", "broadcastLiveBroadcasts": "Difuzări de turnee în direct", + "broadcastNewBroadcast": "O nouă difuzare în direct", + "broadcastSubscribedBroadcasts": "Transmisii abonate", + "broadcastAboutBroadcasts": "Despre emisiuni", + "broadcastHowToUseLichessBroadcasts": "Cum să utilizați emisiunile Lichess.", + "broadcastTheNewRoundHelp": "Runda noua va avea aceiași membri și contribuitori ca cea anterioară.", + "broadcastAddRound": "Adaugă o rundă", + "broadcastOngoing": "În desfășurare", + "broadcastUpcoming": "Următoare", + "broadcastCompleted": "Terminate", + "broadcastCompletedHelp": "Lichess detectează finalizarea rundei pe baza jocurilor sursă. Utilizați această comutare dacă nu există nicio sursă.", + "broadcastRoundName": "Numele rundei", + "broadcastRoundNumber": "Număr rotund", + "broadcastTournamentName": "Numele turneului", + "broadcastTournamentDescription": "O descriere scurtă a turneului", + "broadcastFullDescription": "Întreaga descriere a evenimentului", + "broadcastFullDescriptionHelp": "Descriere lungă, opțională, a difuzării. {param1} este disponibil. Lungimea trebuie să fie mai mică decât {param2} caractere.", + "broadcastSourceSingleUrl": "URL sursă PGN", + "broadcastSourceUrlHelp": "URL-ul pe care Lichess îl va verifica pentru a obține actualizări al PGN-ului. Trebuie să fie public accesibil pe Internet.", + "broadcastSourceGameIds": "Până la 64 de ID-uri de joc Lichess, separate prin spații.", + "broadcastStartDateHelp": "Opțional, dacă știi când va începe evenimentul", + "broadcastCurrentGameUrl": "URL-ul partidei curente", + "broadcastDownloadAllRounds": "Descarcă toate rundele", + "broadcastResetRound": "Resetează această rundă", + "broadcastDeleteRound": "Șterge această rundă", + "broadcastDefinitivelyDeleteRound": "Șterge definitiv runda și jocurile sale.", + "broadcastDeleteAllGamesOfThisRound": "Șterge toate jocurile din această rundă. Sursa va trebui să fie activă pentru a le recrea.", + "broadcastEditRoundStudy": "Editare rundă de studiu", + "broadcastDeleteTournament": "Șterge acest turneu", + "broadcastDefinitivelyDeleteTournament": "Sigur doresc să ștergeți întregul turneu, toate rundele și toate jocurile sale.", + "broadcastShowScores": "Arată scorurile jucătorilor pe baza rezultatelor jocului", + "broadcastReplacePlayerTags": "Opțional: înlocuiește numele jucătorilor, ratingurile și titlurile", + "broadcastFideFederations": "Federații FIDE", + "broadcastTop10Rating": "Top 10 evaluări", + "broadcastFidePlayers": "Jucători FIDE", + "broadcastFidePlayerNotFound": "Jucătorul FIDE nu a fost găsit", + "broadcastFideProfile": "Profil FIDE", + "broadcastFederation": "Federație", + "broadcastAgeThisYear": "Vârsta în acest an", + "broadcastUnrated": "Fără rating", + "broadcastRecentTournaments": "Turnee recente", + "broadcastOpenLichess": "Deschide în Lichess", + "broadcastTeams": "Echipe", + "broadcastStandings": "Clasament", + "broadcastScore": "Scor", + "broadcastNbBroadcasts": "{count, plural, =1{{count} transmisiune} few{{count} transmisiuni} other{{count} de transmisiuni}}", "challengeChallengesX": "Provocări: {param1}", "challengeChallengeToPlay": "Provoacă la o partidă", "challengeChallengeDeclined": "Provocare refuzată", @@ -351,8 +430,8 @@ "puzzleThemeXRayAttackDescription": "O piesă atacă sau apară un patrat, printr-o piesă inamică.", "puzzleThemeZugzwang": "Zugzwang", "puzzleThemeZugzwangDescription": "Adversarul este limitat în mișcările pe care le poate face, iar toate mișcările îi înrăutățesc poziția.", - "puzzleThemeHealthyMix": "Amestec sănătos", - "puzzleThemeHealthyMixDescription": "Un pic din toate. Nu știi la ce să te aștepți, așa că rămâi gata pentru orice! La fel ca în jocurile reale.", + "puzzleThemeMix": "Amestec sănătos", + "puzzleThemeMixDescription": "Un pic din toate. Nu știi la ce să te aștepți, așa că rămâi gata pentru orice! La fel ca în jocurile reale.", "puzzleThemePlayerGames": "Partide jucători", "puzzleThemePlayerGamesDescription": "Caută puzzle-uri generate din partidele tale sau din partidele unui alt jucător.", "puzzleThemePuzzleDownloadInformation": "Aceste puzzle-uri sunt în domeniul public și pot fi descărcate de la {param}.", @@ -431,6 +510,8 @@ "promoteVariation": "Promovează variația", "makeMainLine": "Fă-o variația principală", "deleteFromHere": "Șterge de aici", + "collapseVariations": "Restrânge variațiile", + "expandVariations": "Extinde variațiile", "forceVariation": "Forțează variația", "copyVariationPgn": "Copiază varianta PGN", "move": "Mutare", @@ -481,7 +562,6 @@ "memory": "Memorie", "infiniteAnalysis": "Analiză infinită", "removesTheDepthLimit": "Elimină limita de adâncime (și încălzește computerul)", - "engineManager": "Manager de motor", "blunder": "Gafă", "mistake": "Greșeală", "inaccuracy": "Inexactitate", @@ -739,7 +819,9 @@ "profile": "Profil", "editProfile": "Editează profilul", "realName": "Nume real", + "setFlair": "Arată-ți stilul", "flair": "Pictograma personalizată", + "youCanHideFlair": "Există o setare pentru a ascunde flair-ul utilizatorilor pe întregului site.", "biography": "Biografie", "countryRegion": "Țara sau regiunea", "thankYou": "Mulţumesc!", @@ -756,12 +838,15 @@ "automaticallyProceedToNextGameAfterMoving": "Du-te automat la partida următoare după mutare", "autoSwitch": "Schimbă automat", "puzzles": "Probleme de șah", + "onlineBots": "Boți online", "name": "Nume", "description": "Descriere", "descPrivate": "Descriere privată", "descPrivateHelp": "Text pe care îl vor vedea doar membrii echipei. Dacă este setat, înlocuiește descrierea publică pentru membrii echipei.", "no": "Nu", "yes": "Da", + "website": "Pagină web", + "mobile": "Mobil", "help": "Ajutor:", "createANewTopic": "Creează un nou topic", "topics": "Subiecte", @@ -780,7 +865,9 @@ "cheat": "Trișează", "troll": "Troll", "other": "Altceva", - "reportDescriptionHelp": "Adaugă link-ul de la joc(uri) și arată ce este greșit cu privire la acest comportament al utilizatorului. Nu preciza doar ”trișează”, ci spune-ne cum ai ajuns la această concluzie. Raportul tău va fi procesat mai rapid dacă este scris în engleză.", + "reportCheatBoostHelp": "Adaugă link-ul de la joc(uri) și arată ce este greșit cu privire la acest comportament al utilizatorului. Nu preciza doar \"trișează\", ci spune-ne cum ai ajuns la această concluzie.", + "reportUsernameHelp": "Explică de ce acest nume de utilizator este jignitor. Nu spune doar \"e ofensiv/inadecvat\", ci spune-ne cum ai ajuns la această concluzie, mai ales în cazul în care insulta este obscură, nu este în engleză, este jargon sau este o referință istorică/culturală.", + "reportProcessedFasterInEnglish": "Raportul tău va fi procesat mai rapid dacă este scris în engleză.", "error_provideOneCheatedGameLink": "Te rugăm să furnizezi cel puțin un link către un joc în care s-a trișat.", "by": "de {param}", "importedByX": "Importat de {param}", @@ -813,6 +900,7 @@ "slow": "Lentă", "insideTheBoard": "Pe interiorul tablei", "outsideTheBoard": "În afara tablei", + "allSquaresOfTheBoard": "Toate pătratele de pe tablă", "onSlowGames": "În jocurile lente", "always": "Întotdeauna", "never": "Niciodată", @@ -913,7 +1001,9 @@ "keyPreviousBranch": "Ramura precedentă", "keyNextBranch": "Ramura următoare", "toggleVariationArrows": "Comută săgețile de variație", + "cyclePreviousOrNextVariation": "Ciclu de variație precedentă/următoare", "toggleGlyphAnnotations": "Comută adnotările gilfelor", + "togglePositionAnnotations": "Activează/Dezactivează adnotările pozițiilor", "variationArrowsInfo": "Săgețile de variație vă permit să navigați fără a utiliza lista de mutare.", "playSelectedMove": "joacă mutarea selectată", "newTournament": "Turneu nou", @@ -1174,6 +1264,8 @@ "instructions": "Instrucțiuni", "showMeEverything": "Afișează-mi tot", "lichessPatronInfo": "Lichess este o asociație non-profit și un software gratuit și open-source.\nToate costurile de operare și de dezvoltare sunt finanțate doar din donațiile utilizatorilor.", + "nothingToSeeHere": "Nimic de văzut aici momentan.", + "stats": "Statistici", "opponentLeftCounter": "{count, plural, =1{Adversarul tău a părăsit jocul. Poți revendica victoria peste {count} secundă.} few{Adversarul tău a părăsit jocul. Poți revendica victoria peste {count} secunde.} other{Adversarul tău a părăsit jocul. Poți revendica victoria peste {count} secunde.}}", "mateInXHalfMoves": "{count, plural, =1{Mat la prima mutare} few{Mat în {count} mutări} other{Mat în {count} mutări}}", "nbBlunders": "{count, plural, =1{{count} gafă} few{{count} gafe} other{{count} de gafe}}", @@ -1270,6 +1362,159 @@ "stormXRuns": "{count, plural, =1{O încercare} few{{count} încercări} other{{count} încercări}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{A jucat o încercare de {param2}} few{A jucat {count} încercări de {param2}} other{A jucat {count} încercări de {param2}}}", "streamerLichessStreamers": "Lichess streameri", + "studyPrivate": "Privat", + "studyMyStudies": "Studiile mele", + "studyStudiesIContributeTo": "Studiile la care contribui", + "studyMyPublicStudies": "Studiile mele publice", + "studyMyPrivateStudies": "Studiile mele private", + "studyMyFavoriteStudies": "Studiile mele preferate", + "studyWhatAreStudies": "Ce sunt studiile?", + "studyAllStudies": "Toate studiile", + "studyStudiesCreatedByX": "Studii create de {param}", + "studyNoneYet": "Niciunul încă.", + "studyHot": "Populare", + "studyDateAddedNewest": "Data adăugată (cele mai noi)", + "studyDateAddedOldest": "Data adăugată (cele mai vechi)", + "studyRecentlyUpdated": "Încărcate recent", + "studyMostPopular": "Cele mai populare", + "studyAlphabetical": "Alfabetic", + "studyAddNewChapter": "Adaugă un nou capitol", + "studyAddMembers": "Adaugă membri", + "studyInviteToTheStudy": "Invită la studiu", + "studyPleaseOnlyInvitePeopleYouKnow": "Vă rugăm să invitați doar persoanele pe care le cunoașteți și care vor în mod activ să se alăture studiului.", + "studySearchByUsername": "Caută după numele de utilizator", + "studySpectator": "Spectator", + "studyContributor": "Contribuitor", + "studyKick": "Înlătură", + "studyLeaveTheStudy": "Părăsește studiul", + "studyYouAreNowAContributor": "Acum ești un contribuitor", + "studyYouAreNowASpectator": "Acum ești un spectator", + "studyPgnTags": "Etichete PGN", + "studyLike": "Apreciază", + "studyUnlike": "Nu îmi mai place", + "studyNewTag": "Etichetă nouă", + "studyCommentThisPosition": "Comentează această poziție", + "studyCommentThisMove": "Comentează această mutare", + "studyAnnotateWithGlyphs": "Adnotează cu simboluri", + "studyTheChapterIsTooShortToBeAnalysed": "Capitolul este prea mic pentru a fi analizat.", + "studyOnlyContributorsCanRequestAnalysis": "Numai contribuitorii studiului pot solicita o analiză a computerului.", + "studyGetAFullComputerAnalysis": "Obțineți o întreagă analiză server-side a computerului a variației principale.", + "studyMakeSureTheChapterIsComplete": "Asigurați-vă că acest capitol este complet. Puteți solicita o analiză doar o singură dată.", + "studyAllSyncMembersRemainOnTheSamePosition": "Toți membri sincronizați rămân la aceeași poziție", + "studyShareChanges": "Împărtășește modificările cu spectatorii și salvează-le pe server", + "studyPlaying": "În desfășurare", + "studyShowEvalBar": "Bară de evaluare", + "studyFirst": "Prima", + "studyPrevious": "Precedentă", + "studyNext": "Următoarea", + "studyLast": "Ultima", "studyShareAndExport": "Împărtășește & exportă", - "studyStart": "Începe" + "studyCloneStudy": "Clonează", + "studyStudyPgn": "PGN-ul studiului", + "studyDownloadAllGames": "Descarcă toate partidele", + "studyChapterPgn": "PGN-ul capitolului", + "studyCopyChapterPgn": "Copiază PGN", + "studyDownloadGame": "Descarcă partida", + "studyStudyUrl": "URL-ul studiului", + "studyCurrentChapterUrl": "URL-ul capitolului curent", + "studyYouCanPasteThisInTheForumToEmbed": "Poți lipi acest cod în forum pentru a îngloba", + "studyStartAtInitialPosition": "Începeți de la poziția inițială", + "studyStartAtX": "Începeți la {param}", + "studyEmbedInYourWebsite": "Înglobează pe site-ul sau blog-ul tău", + "studyReadMoreAboutEmbedding": "Citește mai multe despre înglobare", + "studyOnlyPublicStudiesCanBeEmbedded": "Numai studii publice pot fi înglobate!", + "studyOpen": "Deschideți", + "studyXBroughtToYouByY": "{param1}, oferit pentru dvs. de {param2}", + "studyStudyNotFound": "Studiul nu a fost găsit", + "studyEditChapter": "Editează capitolul", + "studyNewChapter": "Capitol nou", + "studyImportFromChapterX": "Importă din {param}", + "studyOrientation": "Orientare", + "studyAnalysisMode": "Tip de analiză", + "studyPinnedChapterComment": "Comentariu fixat", + "studySaveChapter": "Salvează capitolul", + "studyClearAnnotations": "Curățați adnotările", + "studyClearVariations": "Curățați variațiile", + "studyDeleteChapter": "Ștergeți capitolul", + "studyDeleteThisChapter": "Ștergeți acest capitol? Nu există cale de întoarcere!", + "studyClearAllCommentsInThisChapter": "Ștergeți toate comentariile, simbolurile și figurile desenate din acest capitol?", + "studyRightUnderTheBoard": "Fix sub tablă", + "studyNoPinnedComment": "Niciunul", + "studyNormalAnalysis": "Analiză normală", + "studyHideNextMoves": "Ascunde următoarele mutări", + "studyInteractiveLesson": "Lecție interactivă", + "studyChapterX": "Capitolul {param}", + "studyEmpty": "Gol", + "studyStartFromInitialPosition": "Începeți de la poziția inițială", + "studyEditor": "Editor", + "studyStartFromCustomPosition": "Începeți de la o poziție personalizată", + "studyLoadAGameByUrl": "Încărcați meciul din URL", + "studyLoadAPositionFromFen": "Încărcați o poziție din FEN", + "studyLoadAGameFromPgn": "Încărcați un joc din PGN", + "studyAutomatic": "Automată", + "studyUrlOfTheGame": "URL-ul jocului", + "studyLoadAGameFromXOrY": "Încărcați un joc de pe {param1} sau {param2}", + "studyCreateChapter": "Creați capitolul", + "studyCreateStudy": "Creați studiul", + "studyEditStudy": "Editați studiul", + "studyVisibility": "Vizibilitate", + "studyPublic": "Public", + "studyUnlisted": "Nelistat", + "studyInviteOnly": "Doar invitați", + "studyAllowCloning": "Permiteți clonarea", + "studyNobody": "Nimeni", + "studyOnlyMe": "Doar eu", + "studyContributors": "Contribuitori", + "studyMembers": "Membri", + "studyEveryone": "Toată lumea", + "studyEnableSync": "Activați sincronizarea", + "studyYesKeepEveryoneOnTheSamePosition": "Da: menține-i pe toți la aceeași poziție", + "studyNoLetPeopleBrowseFreely": "Nu: permite navigarea liberă", + "studyPinnedStudyComment": "Comentariu fixat", + "studyStart": "Începe", + "studySave": "Salvează", + "studyClearChat": "Șterge conversația", + "studyDeleteTheStudyChatHistory": "Ștergeți istoricul chatului? Nu există cale de întoarcere!", + "studyDeleteStudy": "Ștergeți studiul", + "studyConfirmDeleteStudy": "Ștergeți întregul studiu? Nu există cale de întoarcere! Introduceți numele studiului pentru a confirma: {param}", + "studyWhereDoYouWantToStudyThat": "Unde vreți s-o studiați?", + "studyGoodMove": "Mutare bună", + "studyMistake": "Greșeală", + "studyBrilliantMove": "Mișcare genială", + "studyBlunder": "Gafă", + "studyInterestingMove": "Mișcare interesantă", + "studyDubiousMove": "Mutare dubioasă", + "studyOnlyMove": "Singura mișcare posibilă", + "studyZugzwang": "Zugzwang", + "studyEqualPosition": "Poziție egală", + "studyUnclearPosition": "Poziție neclară", + "studyWhiteIsSlightlyBetter": "Albul este puțin mai bun", + "studyBlackIsSlightlyBetter": "Negrul este puțin mai bun", + "studyWhiteIsBetter": "Albul este mai bun", + "studyBlackIsBetter": "Negrul este mai bun", + "studyWhiteIsWinning": "Albul câștigă", + "studyBlackIsWinning": "Negrul câștigă", + "studyNovelty": "Noutate", + "studyDevelopment": "Dezvoltare", + "studyInitiative": "Inițiativă", + "studyAttack": "Atac", + "studyCounterplay": "Contraatac", + "studyTimeTrouble": "Probleme de timp", + "studyWithCompensation": "Cu compensații", + "studyWithTheIdea": "Cu ideea", + "studyNextChapter": "Capitolul următor", + "studyPrevChapter": "Capitolul precedent", + "studyStudyActions": "Acţiuni de studiu", + "studyTopics": "Subiecte", + "studyMyTopics": "Subiectele mele", + "studyPopularTopics": "Subiecte populare", + "studyManageTopics": "Gestionează subiecte", + "studyBack": "Înapoi", + "studyPlayAgain": "Joacă din nou", + "studyWhatWouldYouPlay": "Ce ai juca în această poziție?", + "studyYouCompletedThisLesson": "Felicitări! Ai terminat această lecție.", + "studyNbChapters": "{count, plural, =1{{count} capitol} few{{count} capitole} other{{count} capitole}}", + "studyNbGames": "{count, plural, =1{{count} partidă} few{{count} partide} other{{count} partide}}", + "studyNbMembers": "{count, plural, =1{{count} membru} few{{count} membri} other{{count} membri}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Lipiți textul PGN aici, până la {count} meci} few{Lipiți textul PGN aici, până la {count} meciuri} other{Lipiți textul PGN aici, până la {count} meciuri}}" } \ No newline at end of file diff --git a/lib/l10n/lila_ru.arb b/lib/l10n/lila_ru.arb index f27d6066a3..9e5ff2160a 100644 --- a/lib/l10n/lila_ru.arb +++ b/lib/l10n/lila_ru.arb @@ -30,7 +30,6 @@ "mobilePuzzleStormConfirmEndRun": "Хотите закончить эту попытку?", "mobilePuzzleStormFilterNothingToShow": "Ничего не найдено, измените фильтры, пожалуйста", "mobileCancelTakebackOffer": "Отменить предложение о возврате хода", - "mobileCancelDrawOffer": "Отменить предложение ничьей", "mobileWaitingForOpponentToJoin": "Ожидание соперника...", "mobileBlindfoldMode": "Игра вслепую", "mobileLiveStreamers": "Стримеры в эфире", @@ -42,6 +41,7 @@ "mobilePuzzleStormSubtitle": "Решите как можно больше задач за 3 минуты.", "mobileGreeting": "Привет, {param}", "mobileGreetingWithoutName": "Привет", + "mobilePrefMagnifyDraggedPiece": "Увеличивать перетаскиваемую фигуру", "activityActivity": "Активность", "activityHostedALiveStream": "Проведён стрим", "activityRankedInSwissTournament": "Занято {param1} место в {param2}", @@ -54,6 +54,7 @@ "activityPlayedNbMoves": "{count, plural, =1{Сделан {count} ход} few{Сделано {count} хода} many{Сделано {count} ходов} other{Сделано {count} ходов}}", "activityInNbCorrespondenceGames": "{count, plural, =1{в {count} игре по переписке} few{в {count} играх по переписке} many{в {count} играх по переписке} other{в {count} играх по переписке}}", "activityCompletedNbGames": "{count, plural, =1{Завершена {count} игра по переписке} few{Завершены {count} игры по переписке} many{Завершены {count} игр по переписке} other{Завершены {count} игр по переписке}}", + "activityCompletedNbVariantGames": "{count, plural, =1{Завершена {count} {param2} игра по переписке} few{Завершены {count} {param2} игры по переписке} many{Завершены {count} {param2} игр по переписке} other{Завершены {count} {param2} игр по переписке}}", "activityFollowedNbPlayers": "{count, plural, =1{{count} игрок добавлен в подписку} few{{count} игрока добавлены в подписку} many{{count} игроков добавлены в подписку} other{{count} игроков добавлены в подписку}}", "activityGainedNbFollowers": "{count, plural, =1{Добавился {count} новый подписчик} few{Добавились {count} новых подписчика} many{Добавились {count} новых подписчиков} other{Добавились {count} новых подписчиков}}", "activityHostedNbSimuls": "{count, plural, =1{Проведён {count} сеанс одновременной игры} few{Проведены {count} сеанса одновременной игры} many{Проведены {count} сеансов одновременной игры} other{Проведены {count} сеансов одновременной игры}}", @@ -64,7 +65,72 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{Завершён {count} турнир по швейцарской системе} few{Завершено {count} турнира по швейцарской системе} many{Завершено {count} турниров по швейцарской системе} other{Завершено {count} турниров по швейцарской системе}}", "activityJoinedNbTeams": "{count, plural, =1{Принят в {count} клуб} few{Принят в {count} клуба} many{Принят в {count} клубов} other{Принят в {count} клубов}}", "broadcastBroadcasts": "Трансляции", + "broadcastMyBroadcasts": "Мои трансляции", "broadcastLiveBroadcasts": "Прямые трансляции турнира", + "broadcastBroadcastCalendar": "Календарь трансляций", + "broadcastNewBroadcast": "Новая прямая трансляция", + "broadcastSubscribedBroadcasts": "Подписанные рассылки", + "broadcastAboutBroadcasts": "О трансляции", + "broadcastHowToUseLichessBroadcasts": "Как пользоваться трансляциями Lichess.", + "broadcastTheNewRoundHelp": "В новом туре примут участие те же участники и редакторы, что и в предыдущем туре.", + "broadcastAddRound": "Добавить тур", + "broadcastOngoing": "Текущие", + "broadcastUpcoming": "Предстоящие", + "broadcastCompleted": "Завершённые", + "broadcastCompletedHelp": "Lichess определяет завершение тура на основе источника партий. Используйте этот переключатель, если нет источника.", + "broadcastRoundName": "Название тура", + "broadcastRoundNumber": "Номер тура", + "broadcastTournamentName": "Название турнира", + "broadcastTournamentDescription": "Краткое описание турнира", + "broadcastFullDescription": "Полное описание события", + "broadcastFullDescriptionHelp": "Необязательное полное описание трансляции. Доступна разметка {param1}. Длина должна быть меньше {param2} символов.", + "broadcastSourceSingleUrl": "Исходный URL PGN", + "broadcastSourceUrlHelp": "URL-адрес, с которого Lichess будет получать обновление PGN. Он должен быть доступен для получения из Интернета.", + "broadcastSourceGameIds": "До 64 идентификаторов (ID) игр Lichess, разделённых пробелами.", + "broadcastStartDateTimeZone": "Дата начала турнира в местном часовом поясе: {param}", + "broadcastStartDateHelp": "Дополнительно, если вы знаете, когда событие начнётся", + "broadcastCurrentGameUrl": "URL-адрес текущей партии", + "broadcastDownloadAllRounds": "Скачать все туры", + "broadcastResetRound": "Сбросить тур", + "broadcastDeleteRound": "Удалить этот тур", + "broadcastDefinitivelyDeleteRound": "Определенно удалить тур и его партии.", + "broadcastDeleteAllGamesOfThisRound": "Удалить все партии этого тура. Для их пересоздания потребуется активный источник.", + "broadcastEditRoundStudy": "Редактировать студию тура", + "broadcastDeleteTournament": "Удалить этот турнир", + "broadcastDefinitivelyDeleteTournament": "Окончательно удалить весь турнир, его туры и партии.", + "broadcastShowScores": "Показать очки игроков по результатам партий", + "broadcastReplacePlayerTags": "Необязательно: заменить имена игроков, рейтинги и звания", + "broadcastFideFederations": "Федерации FIDE", + "broadcastTop10Rating": "Топ-10", + "broadcastFidePlayers": "Игроки FIDE", + "broadcastFidePlayerNotFound": "Профиль FIDE не найден", + "broadcastFideProfile": "Профиль FIDE", + "broadcastFederation": "Федерация", + "broadcastAgeThisYear": "Возраст в этом году", + "broadcastUnrated": "Без рейтинга", + "broadcastRecentTournaments": "Недавние турниры", + "broadcastOpenLichess": "Открыть в Lichess", + "broadcastTeams": "Клубы", + "broadcastBoards": "Доски", + "broadcastOverview": "Обзор", + "broadcastSubscribeTitle": "Подпишитесь, чтобы получать уведомления о начале каждого раунда. Вы можете включить звуковое или пуш-уведомление для трансляций в своих настройках.", + "broadcastUploadImage": "Загрузить изображение турнира", + "broadcastNoBoardsYet": "Пока нет досок. Они появятся после загрузки партий.", + "broadcastBoardsCanBeLoaded": "Доски могут быть загружены из источника или с помощью {param}", + "broadcastStartsAfter": "Начало после {param}", + "broadcastStartVerySoon": "Трансляция начнётся совсем скоро.", + "broadcastNotYetStarted": "Трансляция ещё не началась.", + "broadcastOfficialWebsite": "Официальный веб-сайт", + "broadcastStandings": "Турнирная таблица", + "broadcastIframeHelp": "Больше опций на {param}", + "broadcastWebmastersPage": "странице веб-мастера", + "broadcastPgnSourceHelp": "Публичный PGN-источник для этого раунда в реальном времени. Мы также предлагаем {param} для более быстрой и эффективной синхронизации.", + "broadcastEmbedThisBroadcast": "Встройте эту трансляцию на ваш сайт", + "broadcastEmbedThisRound": "Встроить {param} на свой сайт", + "broadcastRatingDiff": "Разница в рейтингах", + "broadcastGamesThisTournament": "Партии этого турнира", + "broadcastScore": "Очки", + "broadcastNbBroadcasts": "{count, plural, =1{{count} трансляция} few{{count} трансляции} many{{count} трансляций} other{{count} трансляций}}", "challengeChallengesX": "Вызовов: {param1}", "challengeChallengeToPlay": "Вызвать на игру", "challengeChallengeDeclined": "Вызов отклонён", @@ -184,7 +250,7 @@ "preferencesNotifyTournamentSoon": "Турнир скоро начнётся", "preferencesNotifyTimeAlarm": "В игре по переписке скоро упадёт флажок", "preferencesNotifyBell": "Звуковое оповещение на Личесс", - "preferencesNotifyPush": "Оповещение на устройстве, когда Вы не находитесь на сайте Личесс", + "preferencesNotifyPush": "Оповещение на устройстве, когда вы не находитесь на сайте Lichess", "preferencesNotifyWeb": "Браузер", "preferencesNotifyDevice": "Устройство", "preferencesBellNotificationSound": "Звук колокольчика уведомлений", @@ -383,8 +449,8 @@ "puzzleThemeXRayAttackDescription": "Ситуация, когда на линии нападения или защиты дальнобойной фигуры стоит фигура противника.", "puzzleThemeZugzwang": "Цугцванг", "puzzleThemeZugzwangDescription": "Противник вынужден сделать один из немногих возможных ходов, но любой ход ведёт к ухудшению его положения.", - "puzzleThemeHealthyMix": "Сборная солянка", - "puzzleThemeHealthyMixDescription": "Всего понемногу. Вы не знаете, чего ожидать, так что будьте готовы ко всему! Прямо как в настоящей партии.", + "puzzleThemeMix": "Сборная солянка", + "puzzleThemeMixDescription": "Всего понемногу. Вы не знаете, чего ожидать, так что будьте готовы ко всему! Прямо как в настоящей партии.", "puzzleThemePlayerGames": "Партии игрока", "puzzleThemePlayerGamesDescription": "Найти задачи, созданные из ваших партий, или партий других игроков.", "puzzleThemePuzzleDownloadInformation": "Эти задачи находятся в общественном достоянии и вы можете скачать их: {param}.", @@ -492,12 +558,12 @@ "openingExplorer": "База дебютов", "openingEndgameExplorer": "База дебютов/окончаний", "xOpeningExplorer": "База дебютов для {param}", - "playFirstOpeningEndgameExplorerMove": "Играть первый ход изучателя дебютов/эндшпилей", + "playFirstOpeningEndgameExplorerMove": "Играть первый ход изучения дебютов/эндшпилей", "winPreventedBy50MoveRule": "Не удаётся победить из-за правила 50 ходов", "lossSavedBy50MoveRule": "Удаётся избежать поражения из-за правила 50 ходов", "winOr50MovesByPriorMistake": "Победа или правило 50 ходов", "lossOr50MovesByPriorMistake": "Поражение или 50 ходов после последней ошибки", - "unknownDueToRounding": "Победа/поражение гарантируется только если рекомендуемая последовательность ходов была выполнена с момента последнего взятия фигуры или хода пешки из-за возможного округления значений DTZ в базах Syzygy.", + "unknownDueToRounding": "Победа/поражение гарантируется, только если рекомендуемая последовательность ходов была выполнена с момента последнего взятия фигуры или хода пешки из-за возможного округления значений DTZ в базах Syzygy.", "allSet": "Готово!", "importPgn": "Импортировать в PGN", "delete": "Удалить", @@ -515,7 +581,6 @@ "memory": "Память", "infiniteAnalysis": "Бесконечный анализ", "removesTheDepthLimit": "Снимает ограничение на глубину анализа, но заставляет поработать ваш компьютер", - "engineManager": "Менеджер движка", "blunder": "Зевок", "mistake": "Ошибка", "inaccuracy": "Неточность", @@ -796,7 +861,7 @@ "name": "Имя", "description": "Описание", "descPrivate": "Описание для членов команды", - "descPrivateHelp": "Текст, который будут видеть только члены команды(добавленный текст заменит публичное описание для членов команды).", + "descPrivateHelp": "Описание, которое будут видеть только члены клуба. Если установлено, то заменяет публичное описание для всех членов клуба.", "no": "Нет", "yes": "Да", "website": "Сайт", @@ -819,7 +884,9 @@ "cheat": "Жульничество", "troll": "Троллинг", "other": "Другое", - "reportDescriptionHelp": "Поделитесь с нами ссылками на игры, где, как вам кажется, были нарушены правила, и опишите, в чём дело. Недостаточно просто написать «он мухлюет», пожалуйста, опишите, как вы пришли к такому выводу. Мы сработаем оперативнее, если вы напишете на английском языке.", + "reportCheatBoostHelp": "Вставьте ссылку на игру (или несколько игр) и объясните, что не так в поведении этого пользователя. Не надо просто писать «он жульничал», лучше распишите, как вы пришли к такому выводу.", + "reportUsernameHelp": "Объясните, что в этом имени пользователя является оскорбительным. Не надо просто писать «оно оскорбительно или неподобающе», лучше расскажите, как вы пришли к такому выводу, особенно если оскорбление завуалировано, не на английском языке, является сленгом, или же является исторической или культурной отсылкой.", + "reportProcessedFasterInEnglish": "Ваша жалоба будет рассмотрена быстрее, если она будет написана на английском языке.", "error_provideOneCheatedGameLink": "Пожалуйста, добавьте ссылку хотя бы на одну игру, где по вашему мнению были нарушены правила.", "by": "{param}", "importedByX": "Импортировано {param}", @@ -1217,6 +1284,7 @@ "showMeEverything": "Показать всё", "lichessPatronInfo": "Lichess - это благотворительное и полностью бесплатное программное обеспечение с открытым исходным кодом.\nВсе эксплуатационные расходы, разработка и контент финансируются исключительно за счет пожертвований пользователей.", "nothingToSeeHere": "Здесь ничего нет пока.", + "stats": "Статистика", "opponentLeftCounter": "{count, plural, =1{Ваш соперник покинул игру. Вы можете объявить победу через {count} секунду.} few{Ваш соперник покинул игру. Вы можете объявить победу через {count} секунды.} many{Ваш соперник покинул игру. Вы можете объявить победу через {count} секунд.} other{Ваш соперник покинул игру. Вы можете объявить победу через {count} секунд.}}", "mateInXHalfMoves": "{count, plural, =1{Мат в {count} полуход} few{Мат в {count} полухода} many{Мат в {count} полуходов} other{Мат в {count} полуходов}}", "nbBlunders": "{count, plural, =1{{count} зевок} few{{count} зевка} many{{count} зевков} other{{count} зевков}}", @@ -1228,7 +1296,7 @@ "nbBookmarks": "{count, plural, =1{{count} отмеченная} few{{count} отмеченные} many{{count} отмеченных} other{{count} отмеченных}}", "nbDays": "{count, plural, =1{{count} день} few{{count} дня} many{{count} дней} other{{count} дней}}", "nbHours": "{count, plural, =1{{count} час} few{{count} часа} many{{count} часов} other{{count} часов}}", - "nbMinutes": "{count, plural, =1{{count} минута} few{{count} минуты} many{{count} минут} other{{count} минут}}", + "nbMinutes": "{count, plural, =1{{count} одна минута} few{{count} минуты} many{{count} минут} other{{count} минут}}", "rankIsUpdatedEveryNbMinutes": "{count, plural, =1{Место обновляется ежеминутно} few{Место обновляется каждые {count} минуты} many{Место обновляется каждые {count} минут} other{Место обновляется каждые {count} минут}}", "nbPuzzles": "{count, plural, =1{{count} задача} few{{count} задачи} many{{count} задач} other{{count} задач}}", "nbGamesWithYou": "{count, plural, =1{{count} партия с вами} few{{count} партии с вами} many{{count} партий с вами} other{{count} партий с вами}}", @@ -1313,6 +1381,159 @@ "stormXRuns": "{count, plural, =1{1 попытка} few{{count} попытки} many{{count} попыток} other{{count} попыток}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Сыграна {count} серия в {param2}} few{Сыграны {count} серии в {param2}} many{Сыграны {count} серий в {param2}} other{Сыграно {count} серий в {param2}}}", "streamerLichessStreamers": "Стримеры Lichess", + "studyPrivate": "Частная", + "studyMyStudies": "Мои студии", + "studyStudiesIContributeTo": "Студии с моим участием", + "studyMyPublicStudies": "Мои публичные студии", + "studyMyPrivateStudies": "Мои частные студии", + "studyMyFavoriteStudies": "Мои отмеченные студии", + "studyWhatAreStudies": "Что такое «студии»?", + "studyAllStudies": "Все студии", + "studyStudiesCreatedByX": "Студии, созданные {param}", + "studyNoneYet": "Пока ничего.", + "studyHot": "Самые активные", + "studyDateAddedNewest": "Недавно добавленные", + "studyDateAddedOldest": "Давно добавленные", + "studyRecentlyUpdated": "Недавно обновлённые", + "studyMostPopular": "Самые популярные", + "studyAlphabetical": "По алфавиту", + "studyAddNewChapter": "Добавить новую главу", + "studyAddMembers": "Добавить участников", + "studyInviteToTheStudy": "Пригласить в студию", + "studyPleaseOnlyInvitePeopleYouKnow": "Приглашайте только тех участников, которых вы знаете, и кто активно желает участвовать в этой студии.", + "studySearchByUsername": "Поиск по имени", + "studySpectator": "Зритель", + "studyContributor": "Редактор", + "studyKick": "Выгнать", + "studyLeaveTheStudy": "Покинуть студию", + "studyYouAreNowAContributor": "Теперь вы редактор", + "studyYouAreNowASpectator": "Теперь вы зритель", + "studyPgnTags": "Теги PGN", + "studyLike": "Нравится", + "studyUnlike": "Не нравится", + "studyNewTag": "Новый тег", + "studyCommentThisPosition": "Комментировать эту позицию", + "studyCommentThisMove": "Комментировать этот ход", + "studyAnnotateWithGlyphs": "Добавить символьную аннотацию", + "studyTheChapterIsTooShortToBeAnalysed": "Глава слишком короткая для анализа.", + "studyOnlyContributorsCanRequestAnalysis": "Только редакторы студии могут запросить компьютерный анализ.", + "studyGetAFullComputerAnalysis": "Получить с сервера полный компьютерный анализ главной линии.", + "studyMakeSureTheChapterIsComplete": "Убедитесь, что глава завершена. Вы можете запросить анализ только один раз.", + "studyAllSyncMembersRemainOnTheSamePosition": "Все синхронизированные участники остаются на той же позиции", + "studyShareChanges": "Поделиться изменениями со зрителями и сохранить их на сервере", + "studyPlaying": "Активные", + "studyShowEvalBar": "Шкалы оценки", + "studyFirst": "Первая", + "studyPrevious": "Назад", + "studyNext": "Дальше", + "studyLast": "Последняя", "studyShareAndExport": "Поделиться и экспортировать", - "studyStart": "Начать" + "studyCloneStudy": "Клонировать", + "studyStudyPgn": "PGN студии", + "studyDownloadAllGames": "Скачать все партии", + "studyChapterPgn": "PGN главы", + "studyCopyChapterPgn": "Копировать PGN", + "studyDownloadGame": "Скачать партию", + "studyStudyUrl": "Ссылка на студию", + "studyCurrentChapterUrl": "Ссылка на эту главу", + "studyYouCanPasteThisInTheForumToEmbed": "Вставьте этот код на форум для вставки", + "studyStartAtInitialPosition": "Открыть в начальной позиции", + "studyStartAtX": "Начать с {param}", + "studyEmbedInYourWebsite": "Вставить в свой сайт или блог", + "studyReadMoreAboutEmbedding": "Подробнее о вставке на сайт", + "studyOnlyPublicStudiesCanBeEmbedded": "Вставлять на сайт можно только публичные студии!", + "studyOpen": "Открыть", + "studyXBroughtToYouByY": "{param1} на {param2}", + "studyStudyNotFound": "Студия не найдена", + "studyEditChapter": "Редактировать главу", + "studyNewChapter": "Новая глава", + "studyImportFromChapterX": "Импорт из {param}", + "studyOrientation": "Ориентация", + "studyAnalysisMode": "Режим анализа", + "studyPinnedChapterComment": "Закреплённый комментарий главы", + "studySaveChapter": "Сохранить главу", + "studyClearAnnotations": "Очистить аннотацию", + "studyClearVariations": "Очистить варианты", + "studyDeleteChapter": "Удалить главу", + "studyDeleteThisChapter": "Удалить эту главу? Её нельзя будет вернуть!", + "studyClearAllCommentsInThisChapter": "Очистить все комментарии и обозначения этой главы?", + "studyRightUnderTheBoard": "Прямо под доской", + "studyNoPinnedComment": "Нет", + "studyNormalAnalysis": "Обычный анализ", + "studyHideNextMoves": "Скрыть последующие ходы", + "studyInteractiveLesson": "Интерактивный урок", + "studyChapterX": "Глава {param}", + "studyEmpty": "Пусто", + "studyStartFromInitialPosition": "Начать с исходной позиции", + "studyEditor": "Редактор", + "studyStartFromCustomPosition": "Начать со своей позиции", + "studyLoadAGameByUrl": "Загрузить игру по URL", + "studyLoadAPositionFromFen": "Загрузить позицию из FEN", + "studyLoadAGameFromPgn": "Загрузить игру из PGN", + "studyAutomatic": "Автоматически", + "studyUrlOfTheGame": "URL игры", + "studyLoadAGameFromXOrY": "Загрузить игру из {param1} или {param2}", + "studyCreateChapter": "Создать главу", + "studyCreateStudy": "Создать студию", + "studyEditStudy": "Изменить студию", + "studyVisibility": "Доступно к просмотру", + "studyPublic": "Публичная", + "studyUnlisted": "Доступ по ссылке", + "studyInviteOnly": "Только по приглашению", + "studyAllowCloning": "Разрешить копирование", + "studyNobody": "Никто", + "studyOnlyMe": "Только я", + "studyContributors": "Соавторы", + "studyMembers": "Участники", + "studyEveryone": "Все", + "studyEnableSync": "Включить синхронизацию", + "studyYesKeepEveryoneOnTheSamePosition": "Да: устанавливать всем одинаковую позицию", + "studyNoLetPeopleBrowseFreely": "Нет: позволить участникам свободно изучать все позиции", + "studyPinnedStudyComment": "Закреплённый комментарий студии", + "studyStart": "Начать", + "studySave": "Сохранить", + "studyClearChat": "Очистить чат", + "studyDeleteTheStudyChatHistory": "Удалить чат студии? Восстановить будет невозможно!", + "studyDeleteStudy": "Удалить студию", + "studyConfirmDeleteStudy": "Удалить всю студию? Удаление необратимо! Введите название студии для подтверждения: {param}", + "studyWhereDoYouWantToStudyThat": "Где вы хотите создать студию?", + "studyGoodMove": "Хороший ход", + "studyMistake": "Ошибка", + "studyBrilliantMove": "Отличный ход", + "studyBlunder": "Зевок", + "studyInterestingMove": "Интересный ход", + "studyDubiousMove": "Сомнительный ход", + "studyOnlyMove": "Единственный ход", + "studyZugzwang": "Цугцванг", + "studyEqualPosition": "Равная позиция", + "studyUnclearPosition": "Неясная позиция", + "studyWhiteIsSlightlyBetter": "У белых немного лучше", + "studyBlackIsSlightlyBetter": "У чёрных немного лучше", + "studyWhiteIsBetter": "У белых лучше", + "studyBlackIsBetter": "У чёрных лучше", + "studyWhiteIsWinning": "Белые побеждают", + "studyBlackIsWinning": "Чёрные побеждают", + "studyNovelty": "Новинка", + "studyDevelopment": "Развитие", + "studyInitiative": "Инициатива", + "studyAttack": "Атака", + "studyCounterplay": "Контригра", + "studyTimeTrouble": "Цейтнот", + "studyWithCompensation": "С компенсацией", + "studyWithTheIdea": "С идеей", + "studyNextChapter": "Следующая глава", + "studyPrevChapter": "Предыдущая глава", + "studyStudyActions": "Действия в студии", + "studyTopics": "Темы", + "studyMyTopics": "Мои темы", + "studyPopularTopics": "Популярные темы", + "studyManageTopics": "Управление темами", + "studyBack": "Назад", + "studyPlayAgain": "Сыграть снова", + "studyWhatWouldYouPlay": "Как бы вы сыграли в этой позиции?", + "studyYouCompletedThisLesson": "Поздравляем! Вы прошли этот урок.", + "studyNbChapters": "{count, plural, =1{{count} глава} few{{count} главы} many{{count} глав} other{{count} глав}}", + "studyNbGames": "{count, plural, =1{{count} партия} few{{count} партии} many{{count} партий} other{{count} партий}}", + "studyNbMembers": "{count, plural, =1{{count} участник} few{{count} участника} many{{count} участников} other{{count} участников}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Вставьте текст в формате PGN, не больше {count} игры} few{Вставьте текст в формате PGN, не больше {count} игр} many{Вставьте текст в формате PGN, не больше {count} игр} other{Вставьте текст в формате PGN, не больше {count} игр}}" } \ No newline at end of file diff --git a/lib/l10n/lila_sk.arb b/lib/l10n/lila_sk.arb index a65796c05f..99b8f7ecab 100644 --- a/lib/l10n/lila_sk.arb +++ b/lib/l10n/lila_sk.arb @@ -30,7 +30,6 @@ "mobilePuzzleStormConfirmEndRun": "Chcete ukončiť tento pokus?", "mobilePuzzleStormFilterNothingToShow": "Niet čo zobraziť, prosím, zmeňte filtre", "mobileCancelTakebackOffer": "Zrušiť žiadosť o vrátenie ťahu", - "mobileCancelDrawOffer": "Zrušiť navrhnutie remízy", "mobileWaitingForOpponentToJoin": "Čaká sa na pripojenie súpera...", "mobileBlindfoldMode": "Naslepo", "mobileLiveStreamers": "Vysielajúci strímeri", @@ -42,6 +41,7 @@ "mobilePuzzleStormSubtitle": "Vyriešte čo najviac úloh za 3 minúty.", "mobileGreeting": "Ahoj, {param}", "mobileGreetingWithoutName": "Ahoj", + "mobilePrefMagnifyDraggedPiece": "Zväčšiť uchopenú figúrku", "activityActivity": "Aktivita", "activityHostedALiveStream": "Vysielal naživo", "activityRankedInSwissTournament": "Umiestnený ako #{param1} v {param2}", @@ -54,6 +54,7 @@ "activityPlayedNbMoves": "{count, plural, =1{Urobil {count} ťah} few{Zahral {count} ťahy} many{Zharal {count} ťahov} other{Zahral {count} ťahov}}", "activityInNbCorrespondenceGames": "{count, plural, =1{v {count} korešpondenčnej hre} few{v {count} korešpondenčných hrách} many{v {count} korešpondenčných hrách} other{v {count} korešpondenčných hrách}}", "activityCompletedNbGames": "{count, plural, =1{Dohraná {count} korešpondenčná partia} few{Dohrané {count} korešpondenčné partie} many{Dohraných {count} korešpondenčných partií} other{Dohraných {count} korešpondenčných partií}}", + "activityCompletedNbVariantGames": "{count, plural, =1{Odohraná {count} {param2} korešpondenčná partia} few{Odohrané {count} {param2} korešpondenčné partie} many{Odohraných {count} {param2} korešpondenčných partií} other{Odohraných {count} {param2} korešpondenčných partií}}", "activityFollowedNbPlayers": "{count, plural, =1{Začiatok sledovania {count} hráča} few{Začal sledovať {count} hráčov} many{Začal sledovať {count} hráčov} other{Začal sledovať {count} hráčov}}", "activityGainedNbFollowers": "{count, plural, =1{Získal {count} nového sledovateľa} few{Získal {count} nových sledovateľov} many{Získal {count} nových sledovateľov} other{Získal {count} nových sledovateľov}}", "activityHostedNbSimuls": "{count, plural, =1{Usporiadanie a odohranie {count} simultánky} few{Usporiadanie a odohranie {count} simultánok} many{Usporiadanie a odohranie {count} simultánok} other{Usporiadanie a odohranie {count} simultánok}}", @@ -64,7 +65,71 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{Odohraný {count} turnaj švajčiarskym systémom} few{Odohrané {count} turnaje švajčiarskym systémom} many{Odohraných {count} turnajov švajčiarskym systémom} other{Odohraných {count} turnajov švajčiarskym systémom}}", "activityJoinedNbTeams": "{count, plural, =1{Vstup do {count} družstva} few{Vstup do {count} družstiev} many{Vstup do {count} družstiev} other{Vstup do {count} družstiev}}", "broadcastBroadcasts": "Vysielanie", + "broadcastMyBroadcasts": "Moje vysielania", "broadcastLiveBroadcasts": "Živé vysielanie turnaja", + "broadcastBroadcastCalendar": "Kalendár vysielaní", + "broadcastNewBroadcast": "Nové živé vysielanie", + "broadcastSubscribedBroadcasts": "Odoberané vysielania", + "broadcastAboutBroadcasts": "O vysielaní", + "broadcastHowToUseLichessBroadcasts": "Ako používať Lichess vysielanie.", + "broadcastTheNewRoundHelp": "Nové kolo bude mať tých istých členov a prispievateľov ako to predchádzajúce.", + "broadcastAddRound": "Pridať kolo", + "broadcastOngoing": "Prebiehajúci", + "broadcastUpcoming": "Blížiace sa", + "broadcastCompleted": "Ukončené", + "broadcastCompletedHelp": "Lichess rozpozná dokončenie kola, ale môže sa pomýliť. Pomocou tejto funkcie ho môžete nastaviť ručne.", + "broadcastRoundName": "Názov kola", + "broadcastRoundNumber": "Číslo kola", + "broadcastTournamentName": "Názov turnaja", + "broadcastTournamentDescription": "Krátky popis turnaja", + "broadcastFullDescription": "Úplný popis turnaja", + "broadcastFullDescriptionHelp": "Voliteľný dlhý popis vysielania. {param1} je dostupný. Dĺžka musí byť menej ako {param2} znakov.", + "broadcastSourceSingleUrl": "Zdrojová URL pre PGN súbor", + "broadcastSourceUrlHelp": "URL, ktorú bude Lichess kontrolovať, aby získal aktualizácie PGN. Musí byť verejne prístupná z internetu.", + "broadcastSourceGameIds": "Až do 64 identifikátorov Lichess partií oddelených medzerami.", + "broadcastStartDateTimeZone": "Dátum začiatku v miestnej časovej zóne turnaja: {param}", + "broadcastStartDateHelp": "Voliteľné, ak viete kedy sa udalosť začne", + "broadcastCurrentGameUrl": "Adresa URL aktuálnej partie", + "broadcastDownloadAllRounds": "Stiahnuť všetky kolá", + "broadcastResetRound": "Resetovať toto kolo", + "broadcastDeleteRound": "Vymazať toto kolo", + "broadcastDefinitivelyDeleteRound": "Definitívne vymazať kolo a partie tohto kola.", + "broadcastDeleteAllGamesOfThisRound": "Vymazať všetky partie tohto kola. K opätovnému vytvoreniu partií bude potrebné aby bol zdroj aktívny.", + "broadcastEditRoundStudy": "Upraviť kolo štúdií", + "broadcastDeleteTournament": "Vymazať tento turnaj", + "broadcastDefinitivelyDeleteTournament": "Definitívne odstrániť celý turnaj so všetkými kolami a všetkými partiami.", + "broadcastShowScores": "Zobraziť skóre hráčov na základe výsledkov partií", + "broadcastReplacePlayerTags": "Voliteľné: nahradiť mená hráčov, hodnotenia a tituly", + "broadcastFideFederations": "FIDE federácie", + "broadcastTop10Rating": "10 najlepšie hodnotených", + "broadcastFidePlayers": "FIDE šachisti", + "broadcastFidePlayerNotFound": "FIDE šachista sa nenašiel", + "broadcastFideProfile": "FIDE profil", + "broadcastFederation": "Federácia", + "broadcastAgeThisYear": "Vek tento rok", + "broadcastUnrated": "Bez hodnotenia", + "broadcastRecentTournaments": "Posledné turnaje", + "broadcastOpenLichess": "Otvoriť na Lichess", + "broadcastTeams": "Tímy", + "broadcastBoards": "Šachovnice", + "broadcastOverview": "Prehľad", + "broadcastSubscribeTitle": "Prihláste sa, aby ste boli informovaní o začiatku každého kola. V nastaveniach účtu môžete prepnúť zvončekové alebo push upozornenia na vysielanie.", + "broadcastUploadImage": "Nahrať obrázok pre turnaj", + "broadcastNoBoardsYet": "Zatiaľ žiadne šachovnice. Objavia sa po nahratí partií.", + "broadcastBoardsCanBeLoaded": "Šachovnice možno načítať pomocou zdroja alebo pomocou {param}", + "broadcastStartVerySoon": "Vysielanie sa začne čoskoro.", + "broadcastNotYetStarted": "Vysielanie sa ešte nezačalo.", + "broadcastOfficialWebsite": "Oficiálna webstránka", + "broadcastStandings": "Poradie", + "broadcastIframeHelp": "Viac možností nájdete na {param}", + "broadcastWebmastersPage": "stránke tvorcu", + "broadcastPgnSourceHelp": "Verejný zdroj PGN v reálnom čase pre toto kolo. Ponúkame tiež {param} na rýchlejšiu a efektívnejšiu synchronizáciu.", + "broadcastEmbedThisBroadcast": "Vložiť toto vysielanie na webovú stránku", + "broadcastEmbedThisRound": "Vložiť {param} na webovú stránku", + "broadcastRatingDiff": "Ratingový rozdiel", + "broadcastGamesThisTournament": "Partie tohto turnaja", + "broadcastScore": "Skóre", + "broadcastNbBroadcasts": "{count, plural, =1{{count} vysielanie} few{{count} vysielania} many{{count} vysielaní} other{{count} vysielaní}}", "challengeChallengesX": "Výzvy: {param1}", "challengeChallengeToPlay": "Vyzvať na partiu", "challengeChallengeDeclined": "Výzva odmietnutá", @@ -383,8 +448,8 @@ "puzzleThemeXRayAttackDescription": "Figúra bráni alebo útočí na pole cez súperovu figúru.", "puzzleThemeZugzwang": "Nevýhoda ťahu", "puzzleThemeZugzwangDescription": "Súper je limitovaný vo svojich ťahoch a každý ťah zhorší jeho pozíciu.", - "puzzleThemeHealthyMix": "Zdravý mix", - "puzzleThemeHealthyMixDescription": "Zmes úloh. Neviete čo očakávať, a tak ste neustále pripravení na všetko! Presne ako v skutočných partiách.", + "puzzleThemeMix": "Zdravá zmes", + "puzzleThemeMixDescription": "Od všetkého trochu. Neviete, čo môžete očakávať, a tak ste neustále pripravení na všetko! Presne ako v skutočných partiách.", "puzzleThemePlayerGames": "Vaše partie", "puzzleThemePlayerGamesDescription": "Vyhľadajte si úlohy vygenerované z Vašich partií alebo z partií iných hráčov.", "puzzleThemePuzzleDownloadInformation": "Tieto úlohy sú voľne dostupné a môžete si ich stiahnuť z {param}.", @@ -515,7 +580,6 @@ "memory": "Pamäť", "infiniteAnalysis": "Nekonečná analýza", "removesTheDepthLimit": "Odstráni obmedzenie hĺbky analýzy a spôsobí zahrievanie Vášho počítača", - "engineManager": "Správa motorov", "blunder": "Hrubá chyba", "mistake": "Chyba", "inaccuracy": "Nepresnosť", @@ -597,6 +661,7 @@ "rank": "Poradie", "rankX": "Poradie: {param}", "gamesPlayed": "Odohraných partií", + "ok": "OK", "cancel": "Zrušiť", "whiteTimeOut": "Bielemu došiel čas", "blackTimeOut": "Čiernemu došiel čas", @@ -819,7 +884,9 @@ "cheat": "Podvod", "troll": "Troll", "other": "Iné", - "reportDescriptionHelp": "Vložte odkaz na hru/y, a vysvetlite, čo je zlé na tomto správaní používateľa.", + "reportCheatBoostHelp": "Vložte odkaz na partiu(/e) a vysvetlite, čo je na správaní tohto používateľa zlé. Nehovorte len „podvádza“, ale povedzte, ako ste k tomuto záveru dospeli.", + "reportUsernameHelp": "Vysvetlite, čo je na tomto používateľskom mene urážlivé. Nehovorte len „je to urážlivé/nevhodné“, ale povedzte nám, ako ste k tomuto záveru dospeli, najmä ak je urážka významovo zahmlená, nie je v angličtine, je v slangu alebo odkazuje na niečo z historie/kultúry.", + "reportProcessedFasterInEnglish": "Vaša správa bude spracovaná rýchlejšie, ak bude napísaná v angličtine.", "error_provideOneCheatedGameLink": "Prosím, uveďte aspoň jeden odkaz na partiu, v ktorej sa podvádzalo.", "by": "od {param}", "importedByX": "Importoval {param}", @@ -1217,6 +1284,7 @@ "showMeEverything": "Ukázať všetko", "lichessPatronInfo": "Lichess je bezplatný a úplne slobodný/nezávislý softvér s otvoreným zdrojovým kódom. Všetky prevádzkové náklady, vývoj a obsah sú financované výlučne z darov používateľov.", "nothingToSeeHere": "Momentálne tu nie je nič k zobrazeniu.", + "stats": "Štatistiky", "opponentLeftCounter": "{count, plural, =1{Váš súper odišiel od šachovnice. O {count} sekundu si môžete nárokovať výhru.} few{Váš súper odišiel od šachovnice. O {count} sekundy si môžete nárokovať výhru.} many{Váš súper odišiel od šachovnice. O {count} sekúnd si môžete nárokovať výhru.} other{Váš súper odišiel od šachovnice. O {count} sekúnd si môžete nárokovať výhru.}}", "mateInXHalfMoves": "{count, plural, =1{Mat v {count}. polťahu} few{Mat v {count}. polťahu} many{Mat v {count}. polťahu} other{Mat v {count}. polťahu}}", "nbBlunders": "{count, plural, =1{{count} hrubá chyba} few{{count} hrubé chyby} many{{count} hrubých chýb} other{{count} hrubých chýb}}", @@ -1313,6 +1381,159 @@ "stormXRuns": "{count, plural, =1{1 kolo} few{{count} kolá} many{{count} kôl} other{{count} kôl}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Odohrané jedeno kolo {param2}} few{Obohrané {count} kolá {param2}} many{Odohraných {count} kôl {param2}} other{Odohraných {count} kôl {param2}}}", "streamerLichessStreamers": "Lichess streameri", + "studyPrivate": "Súkromné", + "studyMyStudies": "Moje štúdie", + "studyStudiesIContributeTo": "Učivo, ku ktorému prispievam", + "studyMyPublicStudies": "Moje verejné štúdie", + "studyMyPrivateStudies": "Moje súkromné učivo", + "studyMyFavoriteStudies": "Moje obľúbené štúdie", + "studyWhatAreStudies": "Čo sú štúdie?", + "studyAllStudies": "Všetko učivo", + "studyStudiesCreatedByX": "Štúdie vytvorené {param}", + "studyNoneYet": "Zatiaľ žiadne.", + "studyHot": "Teraz populárne", + "studyDateAddedNewest": "Dátum pridania (najnovšie)", + "studyDateAddedOldest": "Dátum pridania (najstaršie)", + "studyRecentlyUpdated": "Nedávno aktualizované", + "studyMostPopular": "Najpopulárnejšie", + "studyAlphabetical": "Abecedne", + "studyAddNewChapter": "Pridať novú kapitolu", + "studyAddMembers": "Pridať členov", + "studyInviteToTheStudy": "Pozvať k štúdii", + "studyPleaseOnlyInvitePeopleYouKnow": "Prosím pozývajte iba ľudí ktorých poznáte a o ktorých viete, že chcú túto štúdiu vidieť.", + "studySearchByUsername": "Hľadať podľa použív. mena", + "studySpectator": "Divák", + "studyContributor": "Prispievateľ", + "studyKick": "Vyhodiť", + "studyLeaveTheStudy": "Opustiť štúdiu", + "studyYouAreNowAContributor": "Od teraz ste prispievateľom", + "studyYouAreNowASpectator": "Od teraz ste divákom", + "studyPgnTags": "PGN značka", + "studyLike": "Páči sa mi", + "studyUnlike": "Nepáči sa mi", + "studyNewTag": "Nová značka", + "studyCommentThisPosition": "Komentovať túto pozíciu", + "studyCommentThisMove": "Komentovať tento ťah", + "studyAnnotateWithGlyphs": "Anotovať pomocou glyphov", + "studyTheChapterIsTooShortToBeAnalysed": "Kapitola je príliš krátka na analýzu.", + "studyOnlyContributorsCanRequestAnalysis": "Oba prispievatelia k tejto štúdii môžu požiadať a počítačovú analýzu.", + "studyGetAFullComputerAnalysis": "Získajte úplnú počítačovú analýzu hlavného variantu na strane servera.", + "studyMakeSureTheChapterIsComplete": "Uistite sa, že kapitola je kompletná. Požiadať o analýzu môžete iba raz.", + "studyAllSyncMembersRemainOnTheSamePosition": "Všetci zosynchronizovaní členovia uvidia rovnakú pozíciu", + "studyShareChanges": "Zdieľajte zmeny s divákmi a uložte ich na server", + "studyPlaying": "Práve sa hrá", + "studyShowEvalBar": "Ukazovatele hodnotenia", + "studyFirst": "Prvá", + "studyPrevious": "Späť", + "studyNext": "Ďalej", + "studyLast": "Posledná", "studyShareAndExport": "Zdielať & export", - "studyStart": "Štart" + "studyCloneStudy": "Naklonovať", + "studyStudyPgn": "PGN štúdie", + "studyDownloadAllGames": "Stiahnuť všetky partie", + "studyChapterPgn": "PGN kapitoly", + "studyCopyChapterPgn": "Kopírovať PGN", + "studyDownloadGame": "Stiahnúť hru", + "studyStudyUrl": "URL štúdie", + "studyCurrentChapterUrl": "URL aktuálnej kapitoly", + "studyYouCanPasteThisInTheForumToEmbed": "Vložte pre zobrazenie vo fóre", + "studyStartAtInitialPosition": "Začať zo základného postavenia", + "studyStartAtX": "Začať na {param}", + "studyEmbedInYourWebsite": "Vložte na svoju webstránku alebo blog", + "studyReadMoreAboutEmbedding": "Prečítajte si viac o vkladaní", + "studyOnlyPublicStudiesCanBeEmbedded": "Vložené môžu byť iba verejné štúdie!", + "studyOpen": "Otvoriť", + "studyXBroughtToYouByY": "{param1} Vám priniesol {param2}", + "studyStudyNotFound": "Štúdia sa nenašla", + "studyEditChapter": "Upraviť kapitolu", + "studyNewChapter": "Nová kapitola", + "studyImportFromChapterX": "Importovať z {param}", + "studyOrientation": "Orientácia", + "studyAnalysisMode": "Mód analýzy", + "studyPinnedChapterComment": "Pripnutý komentár ku kapitole", + "studySaveChapter": "Uložiť kapitolu", + "studyClearAnnotations": "Vymazať anotácie", + "studyClearVariations": "Vymazať varianty", + "studyDeleteChapter": "Vymazať kapitolu", + "studyDeleteThisChapter": "Chcete vymazať túto kapitolu? Táto akcia sa nedá vratit späť!", + "studyClearAllCommentsInThisChapter": "Vymazať všetky komentáre, piktogramy a nakreslené tvary v tejto kapitole?", + "studyRightUnderTheBoard": "Pod hraciu dosku", + "studyNoPinnedComment": "Nikam", + "studyNormalAnalysis": "Normálna analýza", + "studyHideNextMoves": "Skryť nasledujuce ťahy", + "studyInteractiveLesson": "Interaktívna lekcia", + "studyChapterX": "Kapitola {param}", + "studyEmpty": "Prázdne", + "studyStartFromInitialPosition": "Začať z počiatočnej pozície", + "studyEditor": "Editor", + "studyStartFromCustomPosition": "Začať z vlastnej pozície", + "studyLoadAGameByUrl": "Načítať hru z URL", + "studyLoadAPositionFromFen": "Načítať pozíciu z FEN", + "studyLoadAGameFromPgn": "Načítať hru z PGN", + "studyAutomatic": "Automatická", + "studyUrlOfTheGame": "URL hry", + "studyLoadAGameFromXOrY": "Načítať hru z {param1} alebo z {param2}", + "studyCreateChapter": "Vytvoriť kapitolu", + "studyCreateStudy": "Vytvoriť štúdiu", + "studyEditStudy": "Upraviť učivo", + "studyVisibility": "Viditeľnosť", + "studyPublic": "Verejné", + "studyUnlisted": "Nezapísané", + "studyInviteOnly": "Iba na pozvanie", + "studyAllowCloning": "Povoliť klonovanie", + "studyNobody": "Nikto", + "studyOnlyMe": "Iba ja", + "studyContributors": "Prispievatelia", + "studyMembers": "Členovia", + "studyEveryone": "Všetci", + "studyEnableSync": "Povoliť synchronizáciu", + "studyYesKeepEveryoneOnTheSamePosition": "Ano: ponechajte každého na tej istej pozícii", + "studyNoLetPeopleBrowseFreely": "No: povoľte ľudom voľne prehľadávať", + "studyPinnedStudyComment": "Pripnutý komentár k učivu", + "studyStart": "Štart", + "studySave": "Uložiť", + "studyClearChat": "Vymazať čet", + "studyDeleteTheStudyChatHistory": "Definitívne vymazať históriu četu k tejto štúdii? Táto akcia sa nedá vrátit späť!", + "studyDeleteStudy": "Vymazať štúdiu", + "studyConfirmDeleteStudy": "Definitívne vymazať štúdiu? Táto akcia sa nedá vrátit späť! Napíšte názov štúdie pre potvrdenie: {param}", + "studyWhereDoYouWantToStudyThat": "Kde to chcete študovať?", + "studyGoodMove": "Dobrý ťah", + "studyMistake": "Chyba", + "studyBrilliantMove": "Veľmi dobrý ťah", + "studyBlunder": "Hrubá chyba", + "studyInterestingMove": "Zaujímavý ťah", + "studyDubiousMove": "Pochybný ťah", + "studyOnlyMove": "Jediný možný ťah", + "studyZugzwang": "Nevýhoda ťahu", + "studyEqualPosition": "Rovnocenná pozícia", + "studyUnclearPosition": "Nejasná pozícia", + "studyWhiteIsSlightlyBetter": "Biely stojí o trochu lepšie", + "studyBlackIsSlightlyBetter": "Čierny stojí o trochu lepšie", + "studyWhiteIsBetter": "Biely stojí lepšie", + "studyBlackIsBetter": "Čierny stojí lepšie", + "studyWhiteIsWinning": "Biely stojí na výhru", + "studyBlackIsWinning": "Čierny stojí na výhru", + "studyNovelty": "Novinka", + "studyDevelopment": "Vývin", + "studyInitiative": "Iniciatíva", + "studyAttack": "Útok", + "studyCounterplay": "Protiútok", + "studyTimeTrouble": "Časová tieseň", + "studyWithCompensation": "S výhodou", + "studyWithTheIdea": "S myšlienkou", + "studyNextChapter": "Ďalšia kapitola", + "studyPrevChapter": "Predchádzajúca kapitola", + "studyStudyActions": "Úkony pri štúdii", + "studyTopics": "Témy", + "studyMyTopics": "Moje témy", + "studyPopularTopics": "Populárne témy", + "studyManageTopics": "Spravovať témy", + "studyBack": "Späť", + "studyPlayAgain": "Hrať znova", + "studyWhatWouldYouPlay": "Čo by ste hrali v tejto pozícii?", + "studyYouCompletedThisLesson": "Gratulujeme! Túto lekciu ste ukončili.", + "studyNbChapters": "{count, plural, =1{{count} Kapitola} few{{count} Kapitoly} many{{count} Kapitol} other{{count} Kapitol}}", + "studyNbGames": "{count, plural, =1{{count} Partia} few{{count} Partie} many{{count} Partií} other{{count} Partií}}", + "studyNbMembers": "{count, plural, =1{{count} Člen} few{{count} Členovia} many{{count} Členov} other{{count} Členov}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Váš PGN text vložte sem, maximálne {count} partiu} few{Váš PGN text vložte sem, maximálne {count} partie} many{Váš PGN text vložte sem, maximálne {count} partií} other{Váš PGN text vložte sem, maximálne {count} partií}}" } \ No newline at end of file diff --git a/lib/l10n/lila_sl.arb b/lib/l10n/lila_sl.arb index 37501bdd02..15ca5cafb3 100644 --- a/lib/l10n/lila_sl.arb +++ b/lib/l10n/lila_sl.arb @@ -1,9 +1,19 @@ { + "mobileHomeTab": "Domov", + "mobilePuzzlesTab": "Problemi", + "mobileToolsTab": "Orodja", + "mobileWatchTab": "Glej", + "mobileSettingsTab": "Nastavitve", + "mobileMustBeLoggedIn": "Predenj lahko dostopaš do te strani, se je potrebno prijaviti.", + "mobileSystemColors": "Barve sistema", + "mobileFeedbackButton": "Povratne informacije", + "mobileOkButton": "OK", "mobileShowResult": "Pokaži rezultat", "mobilePuzzleThemesSubtitle": "Igrajte uganke iz svojih najljubših otvoritev ali izberite temo.", "mobilePuzzleStormSubtitle": "V 3 minutah rešite čim več ugank.", "mobileGreeting": "Pozdravljeni {param}", "mobileGreetingWithoutName": "Živjo", + "mobilePrefMagnifyDraggedPiece": "Povečaj vlečeno figuro", "activityActivity": "Aktivnost", "activityHostedALiveStream": "Gostil prenos v živo", "activityRankedInSwissTournament": "Uvrščen #{param1} v {param2}", @@ -16,6 +26,7 @@ "activityPlayedNbMoves": "{count, plural, =1{Potegnil {count} potezo} =2{Potegnil {count} potezi} few{Potegnil {count} poteze} other{Potegnil {count} potez}}", "activityInNbCorrespondenceGames": "{count, plural, =1{v {count} dopisni partiji} =2{v {count} dopisnih partijah} few{v {count} dopisnih partijah} other{v {count} dopisnih partijah}}", "activityCompletedNbGames": "{count, plural, =1{Končal {count} dopisno partijo} =2{Končal {count} dopisni partiji} few{Končal {count} dopisne partije} other{Končal {count} dopisnih partij}}", + "activityCompletedNbVariantGames": "{count, plural, =1{Dokončana {count} {param2} korespondenčna igra} =2{Dokončani {count} {param2} korespondenčni igri} few{Dokončane {count} {param2} korespondenčne igre} other{Dokončanih {count} {param2} korespondenčnih iger}}", "activityFollowedNbPlayers": "{count, plural, =1{Sledi {count} igralcu} =2{Sledi {count} igralcem} few{Sledi {count} igralcem} other{Sledi {count} igralcem}}", "activityGainedNbFollowers": "{count, plural, =1{Ima {count} novega sledilca} =2{Ima {count} nova sledilca} few{Ima {count} nove sledilce} other{Ima {count} novih sledilcev}}", "activityHostedNbSimuls": "{count, plural, =1{Gostil {count} simultanko} =2{Gostil {count} simultanki} few{Gostil {count} simultanke} other{Gostil {count} simultank}}", @@ -26,7 +37,41 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{Tekmoval na {count} švicarskem turnirju} =2{Tekmoval na {count} švicarskih turnirjih} few{Tekmoval na {count} švicarskih turnirjih} other{Tekmoval na {count} švicarskih turnirjih}}", "activityJoinedNbTeams": "{count, plural, =1{Pridružen {count} ekipi} =2{Pridružen {count} ekipam} few{Pridružen {count} ekipam} other{Pridružen {count} ekipam}}", "broadcastBroadcasts": "Prenosi", + "broadcastMyBroadcasts": "Moje oddajanja", "broadcastLiveBroadcasts": "Prenos turnirjev v živo", + "broadcastBroadcastCalendar": "Koledar oddaj", + "broadcastNewBroadcast": "Nov prenos v živo", + "broadcastSubscribedBroadcasts": "Naročene oddaje", + "broadcastAboutBroadcasts": "O oddaji", + "broadcastHowToUseLichessBroadcasts": "Kako uporabljati Lichess Broadcasts.", + "broadcastTheNewRoundHelp": "Novi krog bo imel iste člane in sodelavce kot prejšnji.", + "broadcastAddRound": "Dodajte krog", + "broadcastOngoing": "V teku", + "broadcastUpcoming": "Prihajajoči", + "broadcastCompleted": "Zaključeno", + "broadcastCompletedHelp": "Lichess zazna zaključek kroga na podlagi izvornih iger. Uporabite ta preklop, če ni vira.", + "broadcastRoundName": "Ime kroga", + "broadcastRoundNumber": "Številka kroga", + "broadcastTournamentName": "Turnirsko ime", + "broadcastTournamentDescription": "Kratek opis turnirja", + "broadcastFullDescription": "Polni opis dogodka", + "broadcastFullDescriptionHelp": "Neobvezen dolg opis prenosa. {param1} je na voljo. Dolžina mora biti manjša od {param2} znakov.", + "broadcastSourceSingleUrl": "Vir partije v PGN formatu", + "broadcastSourceUrlHelp": "URL, ki ga bo Lichess preveril, da bo prejel PGN posodobitve. Javno mora biti dostopen preko interneta.", + "broadcastStartDateTimeZone": "Začetni datum v lokalnem časovnem pasu turnirja: {param}", + "broadcastStartDateHelp": "Izbirno, če veste, kdaj se dogodek začne", + "broadcastCurrentGameUrl": "URL trenutno igrane igre", + "broadcastDownloadAllRounds": "Prenesite vse kroge", + "broadcastResetRound": "Ponastavi ta krog", + "broadcastDeleteRound": "Izbriši ta krog", + "broadcastDefinitivelyDeleteRound": "Dokončno izbrišite krog in njegove igre.", + "broadcastDeleteAllGamesOfThisRound": "Izbriši vse igre tega kroga. Vir bo moral biti aktiven, da jih lahko znova ustvarite.", + "broadcastEditRoundStudy": "Uredi krog študije", + "broadcastDeleteTournament": "Zbrišite ta turnir", + "broadcastDefinitivelyDeleteTournament": "Dokončno izbrišite celoten turnir, vse njegove kroge in vse njegove igre.", + "broadcastShowScores": "Prikaži rezultate igralcev na podlagi rezultatov igre", + "broadcastReplacePlayerTags": "Izbirno: zamenjajte imena igralcev, ratinge in nazive", + "broadcastNbBroadcasts": "{count, plural, =1{{count} oddaja} =2{{count} oddaji} few{{count} oddaje} other{{count} oddaj}}", "challengeChallengesX": "Izzivi:{param1}", "challengeChallengeToPlay": "Izzovi na partijo", "challengeChallengeDeclined": "Izziv zavrnjen", @@ -342,8 +387,8 @@ "puzzleThemeXRayAttackDescription": "Figura napada ali brani polje skozi nasprotnikovo figuro.", "puzzleThemeZugzwang": "Nujnica", "puzzleThemeZugzwangDescription": "Nasprotnik ima omejene poteze in vsaka poslabša njegovo pozicijo.", - "puzzleThemeHealthyMix": "Zdrava mešanica", - "puzzleThemeHealthyMixDescription": "Vsega po malo. Ne veste, kaj pričakovati, zato bodite pripravljeni na vse! Kot pri resničnih partijah.", + "puzzleThemeMix": "Zdrava mešanica", + "puzzleThemeMixDescription": "Vsega po malo. Ne veste, kaj pričakovati, zato bodite pripravljeni na vse! Kot pri resničnih partijah.", "puzzleThemePlayerGames": "Igralske igre", "puzzleThemePlayerGamesDescription": "Iskanje ugank, ustvarjenih iz vaših iger ali iz iger drugega igralca.", "puzzleThemePuzzleDownloadInformation": "Te uganke so v javni lasti in jih je mogoče prenesti s spletnega mesta {param}.", @@ -467,13 +512,13 @@ "openStudy": "Odpri študij", "enable": "Omogoči", "bestMoveArrow": "Puščica najboljše poteze", + "showVariationArrows": "Prikaži puščice z variacijami", "evaluationGauge": "Kazalnik ocene", "multipleLines": "Več variant", "cpus": "CPU-ji", "memory": "Spomin", "infiniteAnalysis": "Neskončna analiza", "removesTheDepthLimit": "Odstrani omejitev globine in ohrani računalnik topel", - "engineManager": "Vodja motorja", "blunder": "Spodrsljaj", "mistake": "Napaka", "inaccuracy": "Nenatančnost", @@ -499,6 +544,7 @@ "latestForumPosts": "Zadnje objave na forumu", "players": "Igralci", "friends": "Prijatelji", + "otherPlayers": "drugi igralci", "discussions": "Pogovori", "today": "Danes", "yesterday": "Včeraj", @@ -554,6 +600,7 @@ "rank": "Uvrstitev", "rankX": "Uvrstitev: {param}", "gamesPlayed": "Odigranih iger", + "ok": "V redu", "cancel": "Prekliči", "whiteTimeOut": "Belemu se je čas iztekel", "blackTimeOut": "Črnemu se je čas iztekel", @@ -729,6 +776,7 @@ "ifNoneLeaveEmpty": "Če ni, pustite prazno", "profile": "Profil", "editProfile": "Uredi profil", + "realName": "Pravo ime", "setFlair": "Določite svoj okus", "flair": "Simbol", "youCanHideFlair": "Obstaja nastavitev za skrivanje vseh uporabniških čustev na celotnem spletnem mestu.", @@ -755,6 +803,8 @@ "descPrivateHelp": "Besedilo, ki ga bodo videli samo člani ekipe. Če je nastavljen, nadomesti javni opis za člane ekipe.", "no": "Ne", "yes": "Da", + "website": "Spletna stran", + "mobile": "Mobilna aplikacija", "help": "Pomoč:", "createANewTopic": "Ustvari novo temo", "topics": "Teme", @@ -773,7 +823,7 @@ "cheat": "Goljufija", "troll": "Provokacija", "other": "Drugo", - "reportDescriptionHelp": "Prilepite povezave do igre (ali iger) in pojasnite kaj je narobe z obnašanjem uporabnika. Ne napišite samo \"uporabnik goljufa\" temveč pojasnite zakaj mislite tako. Prijava bo obdelana hitreje če bo napisana v angleščini.", + "reportCheatBoostHelp": "Prilepite povezavo do igre (ali iger) in pojasnite, kaj je narobe z nasprotnikovim načinom igranja. Ne napišite le, da \"nasprotnik goljufa\", ampak pojasnite, kako ste prišli do te ugotovitve.", "error_provideOneCheatedGameLink": "Navedite vsaj eno povezavo do igre s primerom goljufanja.", "by": "od {param}", "importedByX": "Uvozil je {param}", @@ -1255,6 +1305,158 @@ "stormXRuns": "{count, plural, =1{1 poskus} =2{{count} poskusa} few{{count} poskusi} other{{count} poskusov}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Opravljen en poskus {param2}} =2{Opravljena {count} poskusa {param2}} few{Opravljeni {count} poskusi {param2}} other{Opravljenih {count} poskusov {param2}}}", "streamerLichessStreamers": "Lichess voditelji prenosa", + "studyPrivate": "Zasebno", + "studyMyStudies": "Moje študije", + "studyStudiesIContributeTo": "Študije h katerim prispevam", + "studyMyPublicStudies": "Moje javne študije", + "studyMyPrivateStudies": "Moje zasebne študije", + "studyMyFavoriteStudies": "Moje najljubše študije", + "studyWhatAreStudies": "Kaj so študije?", + "studyAllStudies": "Vse študije", + "studyStudiesCreatedByX": "Študije, ki jih je ustvaril {param}", + "studyNoneYet": "Še nič.", + "studyHot": "Vroče", + "studyDateAddedNewest": "Dodano (novejše)", + "studyDateAddedOldest": "Dodano (starejše)", + "studyRecentlyUpdated": "Nazadnje objavljeno", + "studyMostPopular": "Najbolj popularno", + "studyAlphabetical": "Po abecednem redu", + "studyAddNewChapter": "Dodaj poglavje", + "studyAddMembers": "Dodaj člane", + "studyInviteToTheStudy": "Povabi na študijo", + "studyPleaseOnlyInvitePeopleYouKnow": "Prosimo, povabite samo tiste ljudi, ki jih poznate in ki bi se želeli pridružiti tej študiji.", + "studySearchByUsername": "Iskanje po uporabniškem imenu", + "studySpectator": "Opazovalec", + "studyContributor": "Sodelovalec", + "studyKick": "Odstrani", + "studyLeaveTheStudy": "Zapusti študijo", + "studyYouAreNowAContributor": "Ste nov sodelovalec", + "studyYouAreNowASpectator": "Sedaj ste opazovalec", + "studyPgnTags": "PGN oznake", + "studyLike": "Všečkaj", + "studyUnlike": "Ni mi všeč", + "studyNewTag": "Nova oznaka", + "studyCommentThisPosition": "Komentiraj to pozicijo", + "studyCommentThisMove": "Komentiraj to potezo", + "studyAnnotateWithGlyphs": "Označi s simbolom", + "studyTheChapterIsTooShortToBeAnalysed": "To poglavje je prekratko, da bi se analiziralo.", + "studyOnlyContributorsCanRequestAnalysis": "Samo sodelovalci v študiji lahko zahtevajo računalniško analizo.", + "studyGetAFullComputerAnalysis": "Pridobi na računalniškem strežniku izvedeno računalniško analizo glavne varjante.", + "studyMakeSureTheChapterIsComplete": "Poskrbite, da bo poglavje zaključeno. Analizo lahko zahtevate samo enkrat.", + "studyAllSyncMembersRemainOnTheSamePosition": "Vsi sinhronizirani člani so v isti poziciji", + "studyShareChanges": "Deli spremembe z gledalci in jih shrani na strežnik", + "studyPlaying": "V teku", + "studyFirst": "Prva stran", + "studyPrevious": "Prejšnja stran", + "studyNext": "Naslednja stran", + "studyLast": "Zadnja stran", "studyShareAndExport": "Deli in Izvozi podatke", - "studyStart": "Začni" + "studyCloneStudy": "Kloniraj", + "studyStudyPgn": "PGN študije", + "studyDownloadAllGames": "Prenesi vse igre", + "studyChapterPgn": "PGN poglavja", + "studyCopyChapterPgn": "Kopiraj PGN", + "studyDownloadGame": "Prenesi igro", + "studyStudyUrl": "URL študije", + "studyCurrentChapterUrl": "URL trenutnega poglavja", + "studyYouCanPasteThisInTheForumToEmbed": "To lahko prilepite na forum, da vstavite", + "studyStartAtInitialPosition": "Začni v začetni poziciji", + "studyStartAtX": "Začni z {param}", + "studyEmbedInYourWebsite": "Vstavite v vašo spletno stran ali blog", + "studyReadMoreAboutEmbedding": "Preberite več o vstavljanju", + "studyOnlyPublicStudiesCanBeEmbedded": "Vdelati je mogoče le javni študij!", + "studyOpen": "Odpri", + "studyXBroughtToYouByY": "{param1} vam ponuja {param2}", + "studyStudyNotFound": "Študije nismo našli", + "studyEditChapter": "Uredi poglavje", + "studyNewChapter": "Novo poglavje", + "studyImportFromChapterX": "Uvozi iz {param}", + "studyOrientation": "Smer", + "studyAnalysisMode": "Analizni način", + "studyPinnedChapterComment": "Pripet komentar poglavja", + "studySaveChapter": "Shrani poglavje", + "studyClearAnnotations": "Zbriši oznake", + "studyClearVariations": "Izbriši variante", + "studyDeleteChapter": "Izbriši poglavje", + "studyDeleteThisChapter": "Izbriši to poglavje? Poti nazaj ni več!", + "studyClearAllCommentsInThisChapter": "Izbriši vse komentarje in oblike v tem poglavju?", + "studyRightUnderTheBoard": "Takoj pod šahovnico", + "studyNoPinnedComment": "Brez", + "studyNormalAnalysis": "Običajna analiza", + "studyHideNextMoves": "Skrij naslednje poteze", + "studyInteractiveLesson": "Interaktivne lekcije", + "studyChapterX": "Poglavje: {param}", + "studyEmpty": "Prazno", + "studyStartFromInitialPosition": "Začni v začetni poziciji", + "studyEditor": "Urejevalnik", + "studyStartFromCustomPosition": "Začni v prilagojeni poziciji", + "studyLoadAGameByUrl": "Naloži partijo iz URL", + "studyLoadAPositionFromFen": "Naloži pozicijo iz FEN", + "studyLoadAGameFromPgn": "Naloži partijo iz PGN", + "studyAutomatic": "Samodejno", + "studyUrlOfTheGame": "URL igre", + "studyLoadAGameFromXOrY": "Naloži partijo iz {param1} ali {param2}", + "studyCreateChapter": "Ustvari poglavje", + "studyCreateStudy": "Ustvarite študijo", + "studyEditStudy": "Uredite študijo", + "studyVisibility": "Vidnost", + "studyPublic": "Javno", + "studyUnlisted": "Ni na seznamu", + "studyInviteOnly": "Samo na povabilo", + "studyAllowCloning": "Dovoli kloniranje", + "studyNobody": "Nihče", + "studyOnlyMe": "Samo jaz", + "studyContributors": "Prispevali so", + "studyMembers": "Člani", + "studyEveryone": "Kdorkoli", + "studyEnableSync": "Omogoči sinhronizacijo", + "studyYesKeepEveryoneOnTheSamePosition": "Da: vse obdrži v isti poziciji", + "studyNoLetPeopleBrowseFreely": "Ne: naj uporabniki prosto raziskujejo", + "studyPinnedStudyComment": "Označen komentar študije", + "studyStart": "Začni", + "studySave": "Shrani", + "studyClearChat": "Počisti klepet", + "studyDeleteTheStudyChatHistory": "Brisanje zgodovine klepeta? Poti nazaj več ni!", + "studyDeleteStudy": "Izbriši študijo", + "studyConfirmDeleteStudy": "Želite izbrisati celotno študijo? Ni poti nazaj! Za potrditev vnesite ime študije: {param}", + "studyWhereDoYouWantToStudyThat": "Kje želite to študirati?", + "studyGoodMove": "Dobra poteza", + "studyMistake": "Napakica", + "studyBrilliantMove": "Briljantna poteza", + "studyBlunder": "Napaka", + "studyInterestingMove": "Zanimiva poteza", + "studyDubiousMove": "Dvomljiva poteza", + "studyOnlyMove": "Edina poteza", + "studyZugzwang": "Nujnica", + "studyEqualPosition": "Enaka pozicija", + "studyUnclearPosition": "Nejasna pozicija", + "studyWhiteIsSlightlyBetter": "Beli je nekoliko boljši", + "studyBlackIsSlightlyBetter": "Črni je nekoliko boljši", + "studyWhiteIsBetter": "Beli je boljši", + "studyBlackIsBetter": "Črni je boljši", + "studyWhiteIsWinning": "Beli zmaguje", + "studyBlackIsWinning": "Črni zmaguje", + "studyNovelty": "Novost", + "studyDevelopment": "Razvoj", + "studyInitiative": "Iniciativa", + "studyAttack": "Napad", + "studyCounterplay": "Protinapad", + "studyTimeTrouble": "Časovna stiska", + "studyWithCompensation": "S kompenzacijo", + "studyWithTheIdea": "Z idejo", + "studyNextChapter": "Naslednje poglavje", + "studyPrevChapter": "Prejšnje poglavje", + "studyStudyActions": "Študijske akcije", + "studyTopics": "Teme", + "studyMyTopics": "Moje teme", + "studyPopularTopics": "Priljubljene teme", + "studyManageTopics": "Upravljaj teme", + "studyBack": "Nazaj", + "studyPlayAgain": "Igrajte ponovno", + "studyWhatWouldYouPlay": "Kaj bi igrali v tem položaju?", + "studyYouCompletedThisLesson": "Čestitke! Končali ste to lekcijo.", + "studyNbChapters": "{count, plural, =1{{count} Poglavje} =2{{count} Poglavji} few{{count} Poglavja} other{{count} poglavij}}", + "studyNbGames": "{count, plural, =1{{count} Partija} =2{{count} Partiji} few{{count} Partije} other{{count} Partij}}", + "studyNbMembers": "{count, plural, =1{{count} Član} =2{{count} Člana} few{{count} Člani} other{{count} Članov}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Prilepite PGN besedilo, z največ {count} partijo} =2{Prilepite PGN besedilo, z največ {count} partijama} few{Prilepite PGN besedilo, z največ {count} partijami} other{Prilepite PGN besedilo, z največ {count} partijami}}" } \ No newline at end of file diff --git a/lib/l10n/lila_sq.arb b/lib/l10n/lila_sq.arb index e3d0905673..aaa9671822 100644 --- a/lib/l10n/lila_sq.arb +++ b/lib/l10n/lila_sq.arb @@ -27,7 +27,6 @@ "mobilePuzzleStormConfirmEndRun": "Doni të përfundohen ku raund?", "mobilePuzzleStormFilterNothingToShow": "S’ka gjë për t’u shfaqur, ju lutemi, ndryshoni filtrat", "mobileCancelTakebackOffer": "Anulojeni ofertën për prapakthim", - "mobileCancelDrawOffer": "Anulojeni ofertën për barazim", "mobileWaitingForOpponentToJoin": "Po pritet që të vijë kundërshtari…", "mobileBlindfoldMode": "Me sytë lidhur", "mobileLiveStreamers": "Transmetues drejtpërsëdrejti", @@ -39,6 +38,7 @@ "mobilePuzzleStormSubtitle": "Zgjidhni sa më shumë puzzle-e të mundeni brenda 3 minutash.", "mobileGreeting": "Tungjatjeta, {param}", "mobileGreetingWithoutName": "Tungjatjeta", + "mobilePrefMagnifyDraggedPiece": "Zmadho gurin e tërhequr", "activityActivity": "Aktiviteti", "activityHostedALiveStream": "Priti një transmetim të drejtpërdrejtë", "activityRankedInSwissTournament": "Renditur #{param1} në {param2}", @@ -61,7 +61,69 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{Konkuroi në turneun zviceran {count}} other{Ndeshur në {count} turne zviceranë}}", "activityJoinedNbTeams": "{count, plural, =1{U bashkua me ekipin {count}} other{U bë pjesë e {count} ekipive}}", "broadcastBroadcasts": "Transmetime", + "broadcastMyBroadcasts": "Transmetimet e mia", "broadcastLiveBroadcasts": "Transmetime të drejtpërdrejta turnesh", + "broadcastBroadcastCalendar": "Kalendar transmetimesh", + "broadcastNewBroadcast": "Transmetim i ri i drejtpërdrejtë", + "broadcastSubscribedBroadcasts": "Transmetime me pajtim", + "broadcastAboutBroadcasts": "Rreth transmetimeve", + "broadcastHowToUseLichessBroadcasts": "Si të përdoren Transmetimet Lichess.", + "broadcastTheNewRoundHelp": "Raundi i ri do të ketë të njëjtën anëtarë dhe kontribues si i mëparshmi.", + "broadcastAddRound": "Shtoni një raund", + "broadcastOngoing": "Në zhvillim", + "broadcastUpcoming": "I ardhshëm", + "broadcastCompleted": "I mbaruar", + "broadcastCompletedHelp": "Lichess-i e pikas plotësimin e raundit bazuar në lojërat burim. Përdoreni këtë buton, nëse s’ka burim.", + "broadcastRoundName": "Emër raundi", + "broadcastRoundNumber": "Numër raundi", + "broadcastTournamentName": "Emër turneu", + "broadcastTournamentDescription": "Përshkrim i shkurtër i turneut", + "broadcastFullDescription": "Përshkrim i plotë i turneut", + "broadcastFullDescriptionHelp": "Përshkrim i gjatë opsional i turneut. {param1} është e disponueshme. Gjatësia duhet të jetë më pak se {param2} shenja.", + "broadcastSourceSingleUrl": "URL Burimi PGN-je", + "broadcastSourceUrlHelp": "URL-ja që do të kontrollojë Lichess-i për të marrë përditësime PGN-sh. Duhet të jetë e përdorshme publikisht që nga Interneti.", + "broadcastSourceGameIds": "Deri në 64 ID lojërash Lichess, ndarë me hapësira.", + "broadcastStartDateTimeZone": "Datë fillimi në zonën kohore vendore të turneut: {param}", + "broadcastStartDateHelp": "Opsionale, nëse e dini kur fillon veprimtaria", + "broadcastCurrentGameUrl": "URL e lojës së tanishme", + "broadcastDownloadAllRounds": "Shkarko krejt raundet", + "broadcastDeleteRound": "Fshije këtë raund", + "broadcastDefinitivelyDeleteRound": "Fshije përfundimisht raundin dhe lojërat e tij.", + "broadcastDeleteAllGamesOfThisRound": "Fshi krejt lojërat e këtij raundi. Burimi do të duhet të jetë aktiv, që të mund të rikrijohen ato.", + "broadcastEditRoundStudy": "Përpunoni analizë raundi", + "broadcastDeleteTournament": "Fshije këtë turne", + "broadcastDefinitivelyDeleteTournament": "Fshihe përfundimisht krejt turneun, krejt raundet e tij dhe krejt lojërat në të.", + "broadcastShowScores": "Shfaq pikë lojtatësh bazuar në përfundime lojërash", + "broadcastReplacePlayerTags": "Opsionale: zëvendësoni emra lojëtarësh, vlerësime dhe tituj", + "broadcastFideFederations": "Federata FIDE", + "broadcastTop10Rating": "10 vlerësimet kryesuese", + "broadcastFidePlayers": "Lojtarë FIDE", + "broadcastFidePlayerNotFound": "S’u gjet lojtar FIDE", + "broadcastFideProfile": "Profil FIDE", + "broadcastFederation": "Federim", + "broadcastAgeThisYear": "Moshë këtë vit", + "broadcastUnrated": "Pa pikë", + "broadcastRecentTournaments": "Turne së fundi", + "broadcastOpenLichess": "Hape në Lichess", + "broadcastTeams": "Ekipe", + "broadcastBoards": "Fusha", + "broadcastOverview": "Përmbledhje", + "broadcastSubscribeTitle": "Pajtohuni, që të noftoheni se kur fillon çdo raund. Mund të aktivizoni/çaktivizoni zilen, ose njoftimet “push” për transmetime, që nga parapëlqimet për llogarinë tuaj.", + "broadcastUploadImage": "Ngarkoni figurë turneu", + "broadcastNoBoardsYet": "Ende pa fusha. Këto do të shfaqen sapo të ngrkohen lojërat.", + "broadcastBoardsCanBeLoaded": "Fushat mund të ngarkohen me një burim, ose përmes {param}", + "broadcastStartsAfter": "Fillon pas {param}", + "broadcastStartVerySoon": "Transmetimi do të fillojë shumë shpejt.", + "broadcastNotYetStarted": "Transmetimi s’ka filluar ende.", + "broadcastOfficialWebsite": "Sajti zyrtar", + "broadcastIframeHelp": "Më tepër mundësi te {param}", + "broadcastWebmastersPage": "faqe webmaster-ësh", + "broadcastPgnSourceHelp": "Një burim publik,, PGN, i atypëratyshëm për këtë raund. Ofrojmë gjithashtu edhe një {param}, për njëkohësim më të shpejtë dhe më efikas.", + "broadcastEmbedThisBroadcast": "Trupëzojeni këtë transmetim në sajtin tuaj", + "broadcastEmbedThisRound": "Trupëzojeni {param} në sajtin tuaj", + "broadcastGamesThisTournament": "Lojëra në këtë turne", + "broadcastScore": "Përfundim", + "broadcastNbBroadcasts": "{count, plural, =1{{count} transmetim} other{{count} transmetime}}", "challengeChallengeToPlay": "Sfidoni në një lojë", "challengeChallengeDeclined": "Sfida u refuzua", "challengeChallengeAccepted": "Sfida u pranua!", @@ -172,6 +234,7 @@ "preferencesNotifyForumMention": "Koment forumi ku përmendeni", "preferencesNotifyInvitedStudy": "Ftesë për ushtrim", "preferencesNotifyGameEvent": "Përditësime loje me korrespondencë", + "preferencesNotifyChallenge": "Sfida", "preferencesNotifyTournamentSoon": "Turne që fillon së shpejti", "preferencesNotifyBell": "Njoftim zileje brenda Lichess-it", "preferencesNotifyPush": "Njoftim pajisjeje kur s’gjendeni në Lichess", @@ -371,8 +434,8 @@ "puzzleThemeXRayAttackDescription": "Një gur sulmon ose mbron një kuadrat, përmes një guri të kundërshtarit.", "puzzleThemeZugzwang": "Zugzwang", "puzzleThemeZugzwangDescription": "Kundërshtari është i kufizuar në lëvizjet që mund të bëjë dhe krejt lëvizjet përkeqësojnë pozicionin e tij.", - "puzzleThemeHealthyMix": "Përzierje e ushtrimeve", - "puzzleThemeHealthyMixDescription": "Pak nga të gjitha. S’dini ç’të prisni, ndaj mbeteni gati për gjithçka! Mu si në lojëra të njëmendta.", + "puzzleThemeMix": "Ushtrime të përzierë", + "puzzleThemeMixDescription": "Pak nga të gjitha. S’dini ç’të prisni, ndaj mbeteni gati për gjithçka! Mu si në lojëra të njëmendta.", "puzzleThemePlayerGames": "Lojëra të lojëtarit", "puzzleThemePuzzleDownloadInformation": "Këto ushtrime janë nën përkatësi publike dhe mund të shkarkohen nga {param}.", "searchSearch": "Kërko", @@ -583,6 +646,7 @@ "rank": "Renditje", "rankX": "Renditja: {param}", "gamesPlayed": "Lojëra të luajtura", + "ok": "OK", "cancel": "Anuloje", "whiteTimeOut": "Të bardhit i mbaroi koha", "blackTimeOut": "Të ziut i mbaroi koha", @@ -801,7 +865,9 @@ "cheat": "Hile", "troll": "Troll", "other": "Tjetër", - "reportDescriptionHelp": "Ngjitni lidhjen për te loja(ra) dhe shpjegoni çfarë nuk shkon me sjelljen e këtij përdoruesi. Mos shkruani thjesht “mashtrojnë”, por na tregoni si mbërritët në këtë përfundim. Raportimi juaj do të përpunohet më shpejt, nëse shkruhet në anglisht.", + "reportCheatBoostHelp": "Ngjitni lidhjen për te loja(rat) dhe shpjegoni se ç’nuk shkon me sjelljen e këtij përdoruesi. Mos thoni thjesht “bën me hile”, por na tregoni se si arritët në këtë konkluzion.", + "reportUsernameHelp": "Shpjegoni pse ky emër përdoruesi është fyes. Mos thoni thjesht “është fyes/i papërshtatshëm”, por na tregoni se si arritët në këtë konkluzion, veçanërisht nëse fyerja është e hollë, jo në anglisht, është në një slang, ose është një referencë historike/kulturore.", + "reportProcessedFasterInEnglish": "Raportimi juaj do të shqyrtohet më shpejt, nëse është shkruar në anglisht.", "error_provideOneCheatedGameLink": "Ju lutemi, jepni të paktën një lidhje te një lojë me hile.", "by": "nga {param}", "importedByX": "Importuar nga {param}", @@ -1190,6 +1256,7 @@ "showMeEverything": "Shfaqmë gjithçka", "lichessPatronInfo": "Lichess është një program bamirësie dhe krejtësisht falas/libre, me burim të hapët.\nKrejt kostot operative, zhvillimi dhe lënda financohen vetëm me dhurime nga përdoruesit.", "nothingToSeeHere": "S’ka ç’shihet këtu tani.", + "stats": "Statistika", "opponentLeftCounter": "{count, plural, =1{Kundërshtari juaj la lojën. Mund të kërkoni fitoren pas {count} sekondash.} other{Kundërshtari juaj la lojën. Mund të kërkoni fitoren pas {count} sekondash.}}", "mateInXHalfMoves": "{count, plural, =1{Mat në {count} gjysmë lëvizje} other{Mat në {count} gjysmë lëvizje}}", "nbBlunders": "{count, plural, =1{{count} gafë} other{{count} gafa}}", @@ -1286,6 +1353,159 @@ "stormXRuns": "{count, plural, =1{1 raund} other{{count} raunde}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Luajti një raund nga {param2}} other{Luajti {count} raunde nga {param2}}}", "streamerLichessStreamers": "Transmetues Lichess-i", + "studyPrivate": "Privat", + "studyMyStudies": "Mësimet e mia", + "studyStudiesIContributeTo": "Mësimet në të cilat kam kontribuar", + "studyMyPublicStudies": "Mësimet e mia publike", + "studyMyPrivateStudies": "Mësimet e mia private", + "studyMyFavoriteStudies": "Mësimet e mia të parapëlqyera", + "studyWhatAreStudies": "Ç’janë mësimet?", + "studyAllStudies": "Krejt mësimet", + "studyStudiesCreatedByX": "Mësime të krijuara nga {param}", + "studyNoneYet": "Ende asnjë.", + "studyHot": "Më aktualet", + "studyDateAddedNewest": "Data e krijimit (nga më e reja)", + "studyDateAddedOldest": "Data e krijimit (nga më e vjetra)", + "studyRecentlyUpdated": "E përditësuar së fundmi", + "studyMostPopular": "Më populloret", + "studyAlphabetical": "Alfabetik", + "studyAddNewChapter": "Shto një kapitull të ri", + "studyAddMembers": "Shto anëtarë", + "studyInviteToTheStudy": "Ftoni në mësim", + "studyPleaseOnlyInvitePeopleYouKnow": "Ju lutemi, ftoni vetëm njerëzit që i njihni dhe që duan vërtet të marrin pjesë në këtë mësim.", + "studySearchByUsername": "Kërkoni sipas emrit të përdoruesit", + "studySpectator": "Shikues", + "studyContributor": "Kontribues", + "studyKick": "Përjashtoje", + "studyLeaveTheStudy": "Braktisni mësimin", + "studyYouAreNowAContributor": "Tani jeni një kontribues", + "studyYouAreNowASpectator": "Tani jeni shikues", + "studyPgnTags": "Etiketa PGN", + "studyLike": "Pëlqejeni", + "studyUnlike": "Shpëlqejeni", + "studyNewTag": "Etiketë e re", + "studyCommentThisPosition": "Komentoni këtë pozicion", + "studyCommentThisMove": "Komentoni këtë lëvizje", + "studyAnnotateWithGlyphs": "Shenjoni me karaktere", + "studyTheChapterIsTooShortToBeAnalysed": "Ky kapitull është shumë i shkurtë për t’u analizuar.", + "studyOnlyContributorsCanRequestAnalysis": "Analizë kompjuterike mund të kërkohet vetëm nga kontribuesit e këtij mësimi.", + "studyGetAFullComputerAnalysis": "Merrni nga shërbyesi një analizë të plotë kompjuterike të variantit kryesor.", + "studyMakeSureTheChapterIsComplete": "Sigurohuni që kapitulli të jetë i plotë. Mund të kërkoni analizë vetëm një herë.", + "studyAllSyncMembersRemainOnTheSamePosition": "Krejt anëtarët SYNC mbesin në të njëjtin pozicion", + "studyShareChanges": "Ndani ndryshimet me shikuesit dhe ruajini ato në shërbyes", + "studyPlaying": "Po luhet", + "studyShowEvalBar": "Shtylla vlerësimi", + "studyFirst": "E para", + "studyPrevious": "E mëparshmja", + "studyNext": "Pasuesja", + "studyLast": "E fundit", "studyShareAndExport": "Ndajeni me të tjerë & eksportoni", - "studyStart": "Fillo" + "studyCloneStudy": "Klonoje", + "studyStudyPgn": "Studioni PGN", + "studyDownloadAllGames": "Shkarkoji krejt lojërat", + "studyChapterPgn": "PGN e kapitullit", + "studyCopyChapterPgn": "Kopjo PGN", + "studyDownloadGame": "Shkarko lojën", + "studyStudyUrl": "URL Mësimi", + "studyCurrentChapterUrl": "URL e Kapitullit Aktual", + "studyYouCanPasteThisInTheForumToEmbed": "Këtë mund ta ngjitni te forumi ose blogu juaj Lichess, për ta trupëzuar", + "studyStartAtInitialPosition": "Fillo në pozicionin fillestar", + "studyStartAtX": "Fillo tek {param}", + "studyEmbedInYourWebsite": "Trupëzojeni te sajti juaj", + "studyReadMoreAboutEmbedding": "Lexoni më tepër rreth trupëzimit", + "studyOnlyPublicStudiesCanBeEmbedded": "Mund të trupëzoni vetëm mësime publike!", + "studyOpen": "Hap", + "studyXBroughtToYouByY": "{param1}, sjellë për ju nga {param2}", + "studyStudyNotFound": "Mësimi s’u gjet", + "studyEditChapter": "Përpunoni kapitullin", + "studyNewChapter": "Kapitull i ri", + "studyImportFromChapterX": "Importo prej {param}", + "studyOrientation": "Drejtimi", + "studyAnalysisMode": "Mënyra Analizim", + "studyPinnedChapterComment": "Koment kapitulli i fiksuar", + "studySaveChapter": "Ruaje kapitullin", + "studyClearAnnotations": "Spastro shënimet", + "studyClearVariations": "Spastroji variantet", + "studyDeleteChapter": "Fshije kapitullin", + "studyDeleteThisChapter": "Të fshihet ky kapitull? S’ka kthim mbrapa!", + "studyClearAllCommentsInThisChapter": "Të spastrohen krejt komentet, glifet dhe format e vizatuara në këtë kapitull?", + "studyRightUnderTheBoard": "Mu nën fushë", + "studyNoPinnedComment": "Asnjë", + "studyNormalAnalysis": "Analizë normale", + "studyHideNextMoves": "Fshih lëvizjen e radhës", + "studyInteractiveLesson": "Mësim me ndërveprim", + "studyChapterX": "Kapitulli {param}", + "studyEmpty": "E zbrazët", + "studyStartFromInitialPosition": "Fillo nga pozicioni fillestar", + "studyEditor": "Përpunues", + "studyStartFromCustomPosition": "Fillo nga pozicion vetjak", + "studyLoadAGameByUrl": "Ngarko lojëra nga URL", + "studyLoadAPositionFromFen": "Ngarko pozicionin nga FEN", + "studyLoadAGameFromPgn": "Ngarko lojëra nga PGN", + "studyAutomatic": "Automatik", + "studyUrlOfTheGame": "URL e lojërave, një për rresht", + "studyLoadAGameFromXOrY": "Ngarko lojëra nga {param1} ose {param2}", + "studyCreateChapter": "Krijo kapitull", + "studyCreateStudy": "Krijoni mësim", + "studyEditStudy": "Përpunoni mësimin", + "studyVisibility": "Dukshmëri", + "studyPublic": "Publike", + "studyUnlisted": "Jo në listë", + "studyInviteOnly": "Vetëm me ftesa", + "studyAllowCloning": "Lejo klonimin", + "studyNobody": "Askush", + "studyOnlyMe": "Vetëm unë", + "studyContributors": "Kontribues", + "studyMembers": "Anëtarë", + "studyEveryone": "Cilido", + "studyEnableSync": "Lejo njëkohësim", + "studyYesKeepEveryoneOnTheSamePosition": "Po: mbaje këdo në të njëjtin pozicion", + "studyNoLetPeopleBrowseFreely": "Jo: lejoji njerëzit të shfletojnë lirisht", + "studyPinnedStudyComment": "Koment studimi i fiksuar", + "studyStart": "Fillo", + "studySave": "Ruaje", + "studyClearChat": "Spastroje bisedën", + "studyDeleteTheStudyChatHistory": "Të fshihet historiku i fjalosjeve të mësimit? S’ka kthim mbrapa!", + "studyDeleteStudy": "Fshije mësimin", + "studyConfirmDeleteStudy": "Të fshihet krejt mësimi? S’ka kthim mbrapa! Për ta ripohuar, shtypni emrin e mësimit: {param}", + "studyWhereDoYouWantToStudyThat": "Ku doni ta studioni atë?", + "studyGoodMove": "Lëvizje e mirë", + "studyMistake": "Gabim", + "studyBrilliantMove": "Lëvizje e shkëlqyer", + "studyBlunder": "Gafë", + "studyInterestingMove": "Lëvizje me interes", + "studyDubiousMove": "Lëvizje e dyshimtë", + "studyOnlyMove": "Lëvizja e vetme", + "studyZugzwang": "Zugzwang", + "studyEqualPosition": "është baras me", + "studyUnclearPosition": "Shenjë gishti e paqartë", + "studyWhiteIsSlightlyBetter": "I bardhi është pakëz më mirë", + "studyBlackIsSlightlyBetter": "I ziu është pakëz më mirë", + "studyWhiteIsBetter": "I bardhi është më mirë", + "studyBlackIsBetter": "I ziu është më mirë", + "studyWhiteIsWinning": "I bardhi po fiton", + "studyBlackIsWinning": "I ziu po fiton", + "studyNovelty": "Risi", + "studyDevelopment": "Zhvillim", + "studyInitiative": "Nismë", + "studyAttack": "Sulm", + "studyCounterplay": "Kundërsulm", + "studyTimeTrouble": "Probleme me këtë instalim?", + "studyWithCompensation": "Me kompesim", + "studyWithTheIdea": "Me idenë", + "studyNextChapter": "Kapitulli pasues", + "studyPrevChapter": "Kapitulli i mëparshëm", + "studyStudyActions": "Studioni veprimet", + "studyTopics": "Tema", + "studyMyTopics": "Temat e mia", + "studyPopularTopics": "Tema popullore", + "studyManageTopics": "Administroni tema", + "studyBack": "Mbrapsht", + "studyPlayAgain": "Riluaje", + "studyWhatWouldYouPlay": "Ç’lëvizje do të bënit në këtë pozicion?", + "studyYouCompletedThisLesson": "Përgëzime! E mbaruat këtë mësim.", + "studyNbChapters": "{count, plural, =1{{count} Kapitull} other{{count} Kapituj}}", + "studyNbGames": "{count, plural, =1{{count} Lojë} other{{count} Lojëra}}", + "studyNbMembers": "{count, plural, =1{{count} Anëtar} other{{count} Anëtarë}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Hidhni këtu tekstin e PGN-s tuaj, deri në {count} lojë} other{Hidhni këtu tekstin e PGN-s tuaj, deri në {count} lojëra}}" } \ No newline at end of file diff --git a/lib/l10n/lila_sr.arb b/lib/l10n/lila_sr.arb index 8e1ba93d63..cf5b6788f4 100644 --- a/lib/l10n/lila_sr.arb +++ b/lib/l10n/lila_sr.arb @@ -21,6 +21,12 @@ "activityJoinedNbTeams": "{count, plural, =1{Ушли у {count} тим} few{Ушли у {count} тима} other{Ушли у {count} тимова}}", "broadcastBroadcasts": "Емитовања", "broadcastLiveBroadcasts": "Уживо емитовање турнира", + "broadcastNewBroadcast": "Нова ужива емитовања", + "broadcastOngoing": "Текућа", + "broadcastUpcoming": "Предстојећа", + "broadcastCompleted": "Завршена", + "broadcastRoundNumber": "Број рунде", + "broadcastFullDescription": "Цео опис догађаја", "challengeChallengeToPlay": "Изазови на партију", "challengeChallengeDeclined": "Изазов одбијен", "challengeChallengeAccepted": "Изазов прихваћен!", @@ -277,8 +283,8 @@ "puzzleThemeXRayAttackDescription": "Фигура напада или брани поље, захваљујући противничкој фигури.", "puzzleThemeZugzwang": "Цугцванг", "puzzleThemeZugzwangDescription": "Противник има ограничен избор потеза и сваким потезом погоршава своју позицију.", - "puzzleThemeHealthyMix": "Здрава мешавина", - "puzzleThemeHealthyMixDescription": "Свега по мало. Не знаш шта да очекујеш, па остајеш спреман за све! Баш као у правим партијама.", + "puzzleThemeMix": "Здрава мешавина", + "puzzleThemeMixDescription": "Свега по мало. Не знаш шта да очекујеш, па остајеш спреман за све! Баш као у правим партијама.", "puzzleThemePlayerGames": "Играчеве партије", "puzzleThemePlayerGamesDescription": "Потражи проблеме створене на основу твојих партија или партија других грача.", "puzzleThemePuzzleDownloadInformation": "Ови проблеми су у јавном власништву и могуће их је презузети са {param}.", @@ -406,7 +412,6 @@ "memory": "Меморија", "infiniteAnalysis": "Бесконачна анализа", "removesTheDepthLimit": "Уклања ограничење дубине и греје рачунар", - "engineManager": "Менаџер машине", "blunder": "Груба грешка", "mistake": "Грешка", "inaccuracy": "Непрецизност", @@ -706,7 +711,6 @@ "cheat": "Варање", "troll": "Трол", "other": "Остало", - "reportDescriptionHelp": "Залијепите везу до игре и објасните шта није у реду са понашањем корисника. Немојте само рећи \"варао\", али реците како сте дошли до тог закључка. Ваша пријава ће бити обрађена брже ако је напишете на енглеском језику.", "error_provideOneCheatedGameLink": "Наведите барем једну везу игре у којој је играч варао.", "by": "од {param}", "importedByX": "Увезао {param}", @@ -1124,6 +1128,117 @@ "stormXRuns": "{count, plural, =1{1 рунда} few{{count} рунде} other{{count} рунди}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Одиграли једну рунду {param2}-а} few{Одиграли {count} рунде {param2}-а} other{Одиграли {count} рунди {param2}-а}}", "streamerLichessStreamers": "Личес стримери", + "studyPrivate": "Приватна", + "studyMyStudies": "Моје студије", + "studyStudiesIContributeTo": "Студије којима доприносим", + "studyMyPublicStudies": "Моје јавне студије", + "studyMyPrivateStudies": "Моје приватне студије", + "studyMyFavoriteStudies": "Моје омиљене студије", + "studyWhatAreStudies": "Шта су студије?", + "studyAllStudies": "Све студије", + "studyStudiesCreatedByX": "Студије које је {param} направио/ла", + "studyNoneYet": "Ниједна за сад.", + "studyHot": "У тренду", + "studyDateAddedNewest": "Датум додавања (најновије)", + "studyDateAddedOldest": "Датум додавања (најстарије)", + "studyRecentlyUpdated": "Недавно ажуриране", + "studyMostPopular": "Најпопуларније", + "studyAddNewChapter": "Додајте ново поглавље", + "studyAddMembers": "Додај чланове", + "studyInviteToTheStudy": "Позовите у студију", + "studyPleaseOnlyInvitePeopleYouKnow": "Молимо вас да само позивате људе које познајете и који активно желе да се придруже овој студији.", + "studySearchByUsername": "Претражујте по корисничком имену", + "studySpectator": "Посматрач", + "studyContributor": "Cарадник", + "studyKick": "Избаци", + "studyLeaveTheStudy": "Напусти студију", + "studyYouAreNowAContributor": "Сада сте сарадник", + "studyYouAreNowASpectator": "Сада сте посматрач", + "studyPgnTags": "PGN ознаке", + "studyLike": "Свиђа ми се", + "studyNewTag": "Нова ознака", + "studyCommentThisPosition": "Прокоментаришите ову позицију", + "studyCommentThisMove": "Прокоментаришите овај потез", + "studyAnnotateWithGlyphs": "Прибележите глифовима", + "studyTheChapterIsTooShortToBeAnalysed": "Поглавље је прекратко за анализу.", + "studyOnlyContributorsCanRequestAnalysis": "Само сарадници у студији могу захтевати рачунарску анализу.", + "studyGetAFullComputerAnalysis": "Добијте потпуну рачунарску анализу главне варијације од стране сервера.", + "studyMakeSureTheChapterIsComplete": "Побрините се да је поглавље завршено. Само једном можете захтевати анализу.", + "studyAllSyncMembersRemainOnTheSamePosition": "Сви SYNC чланови остају на истој позицији", + "studyShareChanges": "Делите измене са посматрачима и сачувајте их на сервер", + "studyPlaying": "У току", + "studyFirst": "Прва", + "studyPrevious": "Претходна", + "studyNext": "Следећа", + "studyLast": "Последња", "studyShareAndExport": "Подели и извези", - "studyStart": "Започни" + "studyCloneStudy": "Клонирај", + "studyStudyPgn": "PGN студије", + "studyDownloadAllGames": "Преузми све партије", + "studyChapterPgn": "PGN поглавља", + "studyDownloadGame": "Преузми партију", + "studyStudyUrl": "Линк студије", + "studyCurrentChapterUrl": "Линк тренутног поглавља", + "studyYouCanPasteThisInTheForumToEmbed": "Ово можете налепити у форум да уградите", + "studyStartAtInitialPosition": "Започни на иницијалној позицији", + "studyStartAtX": "Започни на {param}", + "studyEmbedInYourWebsite": "Угради у свој сајт или блог", + "studyReadMoreAboutEmbedding": "Прочитај више о уграђивању", + "studyOnlyPublicStudiesCanBeEmbedded": "Само јавне студије могу бити уграђене!", + "studyOpen": "Отворите", + "studyXBroughtToYouByY": "{param2} Вам доноси {param1}", + "studyStudyNotFound": "Студија није пронађена", + "studyEditChapter": "Измени поглавље", + "studyNewChapter": "Ново поглавље", + "studyOrientation": "Оријентација", + "studyAnalysisMode": "Врста анализе", + "studyPinnedChapterComment": "Закачен коментар поглавља", + "studySaveChapter": "Сачувај поглавље", + "studyClearAnnotations": "Избриши анотације", + "studyDeleteChapter": "Избриши поглавље", + "studyDeleteThisChapter": "Избриши ово поглавље? Нема повратка назад!", + "studyClearAllCommentsInThisChapter": "Избриши све коментаре, глифове и нацртане облике у овом поглављу?", + "studyRightUnderTheBoard": "Одмах испод табле", + "studyNoPinnedComment": "Ниједан", + "studyNormalAnalysis": "Нормална анализа", + "studyHideNextMoves": "Сакриј следеће потезе", + "studyInteractiveLesson": "Интерактивна лекција", + "studyChapterX": "Поглавље {param}", + "studyEmpty": "Празно", + "studyStartFromInitialPosition": "Започните од иницијалне позиције", + "studyEditor": "Уређивач", + "studyStartFromCustomPosition": "Започните од жељене позиције", + "studyLoadAGameByUrl": "Учитајте партије преко линкова", + "studyLoadAPositionFromFen": "Учитајте позицију из FEN-а", + "studyLoadAGameFromPgn": "Учитајте партију из PGN-а", + "studyAutomatic": "Аутоматски", + "studyUrlOfTheGame": "Линкови партија, једна по реду", + "studyLoadAGameFromXOrY": "Учитајте партије са {param1} или {param2}", + "studyCreateChapter": "Направи поглавље", + "studyCreateStudy": "Направи студију", + "studyEditStudy": "Измени студију", + "studyVisibility": "Видљивост", + "studyPublic": "Јавно", + "studyUnlisted": "Неприказано", + "studyInviteOnly": "Само по позиву", + "studyAllowCloning": "Дозволите клонирање", + "studyNobody": "Нико", + "studyOnlyMe": "Само ја", + "studyContributors": "Сарадници", + "studyMembers": "Чланови", + "studyEveryone": "Сви", + "studyEnableSync": "Омогући синхронизацију", + "studyYesKeepEveryoneOnTheSamePosition": "Да: задржи све на истој позицији", + "studyNoLetPeopleBrowseFreely": "Не: дозволи људима да слободно прегледају", + "studyPinnedStudyComment": "Закачен коментар студије", + "studyStart": "Започни", + "studySave": "Сачувај", + "studyClearChat": "Очисти ћаскање", + "studyDeleteTheStudyChatHistory": "Избриши историју ћаскања студије? Нема повратка назад!", + "studyDeleteStudy": "Избриши студију", + "studyWhereDoYouWantToStudyThat": "Где желите то проучити?", + "studyNbChapters": "{count, plural, =1{{count} Поглавље} few{{count} Поглављa} other{{count} Поглављa}}", + "studyNbGames": "{count, plural, =1{{count} Партија} few{{count} Партијe} other{{count} Партија}}", + "studyNbMembers": "{count, plural, =1{{count} Члан} few{{count} Чланa} other{{count} Чланова}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Налепите свој PGN текст овде, до {count} партије} few{Налепите свој PGN текст овде, до {count} партије} other{Налепите свој PGN текст овде, до {count} партија}}" } \ No newline at end of file diff --git a/lib/l10n/lila_sv.arb b/lib/l10n/lila_sv.arb index 356c119feb..de574ce557 100644 --- a/lib/l10n/lila_sv.arb +++ b/lib/l10n/lila_sv.arb @@ -1,4 +1,28 @@ { + "mobileHomeTab": "Hem", + "mobilePuzzlesTab": "Problem", + "mobileToolsTab": "Verktyg", + "mobileWatchTab": "Titta", + "mobileSystemColors": "Systemets färger", + "mobileOkButton": "OK", + "mobileAllGames": "Alla spel", + "mobileRecentSearches": "Senaste sökningar", + "mobileClearButton": "Rensa", + "mobilePlayersMatchingSearchTerm": "Spelare med \"{param}\"", + "mobileNoSearchResults": "Inga resultat", + "mobileAreYouSure": "Är du säker?", + "mobileSharePuzzle": "Dela detta schackproblem", + "mobileShareGameURL": "Dela parti-URL", + "mobileShareGamePGN": "Dela PGN", + "mobileShowVariations": "Visa variationer", + "mobileHideVariation": "Dölj variationer", + "mobileShowComments": "Visa kommentarer", + "mobileBlindfoldMode": "I blindo", + "mobileCustomGameJoinAGame": "Gå med i spel", + "mobileSomethingWentWrong": "Något gick fel.", + "mobileShowResult": "Visa resultat", + "mobileGreeting": "Hej {param}", + "mobileGreetingWithoutName": "Hej", "activityActivity": "Aktivitet", "activityHostedALiveStream": "Var värd för en direktsänd videosändning", "activityRankedInSwissTournament": "Rankad #{param1} i {param2}", @@ -21,7 +45,35 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{Tävlade i {count} swissturnering} other{Tävlade i {count} swissturneringar}}", "activityJoinedNbTeams": "{count, plural, =1{Gick med i {count} lag} other{Gick med i {count} lag}}", "broadcastBroadcasts": "Sändningar", + "broadcastMyBroadcasts": "Mina sändningar", "broadcastLiveBroadcasts": "Direktsända turneringar", + "broadcastNewBroadcast": "Ny direktsändning", + "broadcastAboutBroadcasts": "Om sändningar", + "broadcastHowToUseLichessBroadcasts": "Hur man använder Lichess-Sändningar.", + "broadcastTheNewRoundHelp": "Den nya rundan kommer att ha samma medlemmar och bidragsgivare som den föregående.", + "broadcastAddRound": "Lägg till en omgång", + "broadcastOngoing": "Pågående", + "broadcastUpcoming": "Kommande", + "broadcastCompleted": "Slutförda", + "broadcastCompletedHelp": "Lichess upptäcker slutförandet av rundor baserat på källspelen. Använd detta alternativ om det inte finns någon källa.", + "broadcastRoundName": "Omgångens namn", + "broadcastRoundNumber": "Omgångens nummer", + "broadcastTournamentName": "Turneringens namn", + "broadcastTournamentDescription": "Kort beskrivning av turneringen", + "broadcastFullDescription": "Fullständig beskrivning", + "broadcastFullDescriptionHelp": "Valfri längre beskrivning av sändningen. {param1} är tillgänglig. Längden måste vara mindre än {param2} tecken.", + "broadcastSourceUrlHelp": "URL som Lichess kan använda för att få PGN-uppdateringar. Den måste vara publikt tillgänglig från Internet.", + "broadcastStartDateHelp": "Valfritt, om du vet när händelsen startar", + "broadcastCurrentGameUrl": "Länk till aktuellt parti (URL)", + "broadcastDownloadAllRounds": "Ladda ner alla omgångar", + "broadcastResetRound": "Återställ den här omgången", + "broadcastDeleteRound": "Ta bort den här omgången", + "broadcastDefinitivelyDeleteRound": "Ta bort denna runda och dess partier definitivt.", + "broadcastDeleteAllGamesOfThisRound": "Radera alla partier i denna runda. Källan kommer behöva vara aktiv för att återskapa dem.", + "broadcastEditRoundStudy": "Redigera studie för ronden", + "broadcastDeleteTournament": "Radera turnering", + "broadcastDefinitivelyDeleteTournament": "Definitivt radera turnering.", + "broadcastNbBroadcasts": "{count, plural, =1{{count} sändning} other{{count} sändningar}}", "challengeChallengesX": "Utmaningar: {param1}", "challengeChallengeToPlay": "Utmana till ett parti", "challengeChallengeDeclined": "Utmaning avböjd", @@ -340,8 +392,8 @@ "puzzleThemeXRayAttackDescription": "En pjäs attackerar eller försvarar en ruta, genom en motståndarpjäs.", "puzzleThemeZugzwang": "Zugzwang", "puzzleThemeZugzwangDescription": "Motspelaren har begränsat antal möjliga drag, och alla möjliga drag förvärrar motspelarens position.", - "puzzleThemeHealthyMix": "Blandad kompott", - "puzzleThemeHealthyMixDescription": "Lite av varje. Du vet inte vad som kommer, så du behöver vara redo för allt! Precis som i riktiga partier.", + "puzzleThemeMix": "Blandad kompott", + "puzzleThemeMixDescription": "Lite av varje. Du vet inte vad som kommer, så du behöver vara redo för allt! Precis som i riktiga partier.", "puzzleThemePlayerGames": "Spelarspel", "puzzleThemePlayerGamesDescription": "Hitta pussel genererade från dina egna parti, eller från andra spelares parti.", "puzzleThemePuzzleDownloadInformation": "Dessa pussel tillhör den allmänna egendomen, och kan laddas ner från {param}.", @@ -470,7 +522,6 @@ "memory": "Minne", "infiniteAnalysis": "Oändlig analys", "removesTheDepthLimit": "Tar bort sökdjupsbegränsningen och håller datorn varm", - "engineManager": "Hantera analysmotor", "blunder": "Blunder", "mistake": "Misstag", "inaccuracy": "Felaktighet", @@ -770,7 +821,6 @@ "cheat": "Fusk", "troll": "Troll", "other": "Annat", - "reportDescriptionHelp": "Klistra in länken till partiet och förklara vad som är fel med den här användarens beteende. Säg inte bara \"de fuskar\", utan förklara hur du dragit denna slutsats. Din rapport kommer att behandlas fortare om den är skriven på engelska.", "error_provideOneCheatedGameLink": "Ange minst en länk till ett spel där användaren fuskade.", "by": "av {param}", "importedByX": "Importerad av {param}", @@ -1260,6 +1310,159 @@ "stormXRuns": "{count, plural, =1{1 försök} other{{count} försök}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Spelade en omgång {param2}} other{Spelade {count} omgångar {param2}}}", "streamerLichessStreamers": "Videokanaler från Lichess", + "studyPrivate": "Privat", + "studyMyStudies": "Mina studier", + "studyStudiesIContributeTo": "Studier som jag bidrar till", + "studyMyPublicStudies": "Mina offentliga studier", + "studyMyPrivateStudies": "Mina privata studier", + "studyMyFavoriteStudies": "Mina favoritstudier", + "studyWhatAreStudies": "Vad är studier?", + "studyAllStudies": "Alla studier", + "studyStudiesCreatedByX": "Studier skapade av {param}", + "studyNoneYet": "Inga ännu.", + "studyHot": "Populära", + "studyDateAddedNewest": "Datum tillagd (nyaste)", + "studyDateAddedOldest": "Datum tillagd (nyaste)", + "studyRecentlyUpdated": "Nyligen uppdaterade", + "studyMostPopular": "Mest populära", + "studyAlphabetical": "Alfabetisk", + "studyAddNewChapter": "Lägg till ett nytt kapitel", + "studyAddMembers": "Lägg till medlemmar", + "studyInviteToTheStudy": "Bjud in till studien", + "studyPleaseOnlyInvitePeopleYouKnow": "Viktigt: bjud bara in människor du känner och som aktivt vill gå med i studien.", + "studySearchByUsername": "Sök efter användarnamn", + "studySpectator": "Åskådare", + "studyContributor": "Bidragsgivare", + "studyKick": "Sparka", + "studyLeaveTheStudy": "Lämna studien", + "studyYouAreNowAContributor": "Du är nu bidragsgivare", + "studyYouAreNowASpectator": "Du är nu en åskådare", + "studyPgnTags": "PGN taggar", + "studyLike": "Gilla", + "studyUnlike": "Sluta gilla", + "studyNewTag": "Ny tag", + "studyCommentThisPosition": "Kommentera denna position", + "studyCommentThisMove": "Kommentera detta drag", + "studyAnnotateWithGlyphs": "Kommentera med glyfer", + "studyTheChapterIsTooShortToBeAnalysed": "Kapitlet är för kort för att analyseras.", + "studyOnlyContributorsCanRequestAnalysis": "Endast studiens bidragsgivare kan begära en datoranalys.", + "studyGetAFullComputerAnalysis": "Hämta en fullständig serveranalys av huvudlinjen.", + "studyMakeSureTheChapterIsComplete": "Försäkra dig om att kapitlet är färdigt. Du kan bara begära analysen en gång.", + "studyAllSyncMembersRemainOnTheSamePosition": "Alla SYNC-medlemmar är kvar på samma position", + "studyShareChanges": "Dela ändringar med åskådare och spara dem på servern", + "studyPlaying": "Spelar", + "studyShowEvalBar": "Värderingsfält", + "studyFirst": "Första", + "studyPrevious": "Föregående", + "studyNext": "Nästa", + "studyLast": "Sista", "studyShareAndExport": "Dela & exportera", - "studyStart": "Starta" + "studyCloneStudy": "Klona", + "studyStudyPgn": "Studiens PGN", + "studyDownloadAllGames": "Ladda ner alla partier", + "studyChapterPgn": "Kapitel PGN", + "studyCopyChapterPgn": "Kopiera PGN", + "studyDownloadGame": "Ladda ner parti", + "studyStudyUrl": "Studiens URL", + "studyCurrentChapterUrl": "Aktuell kapitel URL", + "studyYouCanPasteThisInTheForumToEmbed": "Du kan klistra in detta i forumet för att infoga", + "studyStartAtInitialPosition": "Start vid ursprunglig position", + "studyStartAtX": "Börja på {param}", + "studyEmbedInYourWebsite": "Infoga på din hemsida eller blogg", + "studyReadMoreAboutEmbedding": "Läs mer om att infoga", + "studyOnlyPublicStudiesCanBeEmbedded": "Endast offentliga studier kan läggas till!", + "studyOpen": "Öppna", + "studyXBroughtToYouByY": "{param1} gjord av {param2}", + "studyStudyNotFound": "Studien kan inte hittas", + "studyEditChapter": "Redigera kapitel", + "studyNewChapter": "Nytt kapitel", + "studyImportFromChapterX": "Importera från {param}", + "studyOrientation": "Orientering", + "studyAnalysisMode": "Analysläge", + "studyPinnedChapterComment": "Fastnålad kommentar till kapitlet", + "studySaveChapter": "Spara kapitlet", + "studyClearAnnotations": "Rensa kommentarer", + "studyClearVariations": "Rensa variationer", + "studyDeleteChapter": "Ta bort kapitel", + "studyDeleteThisChapter": "Ta bort detta kapitel. Det går inte att ångra!", + "studyClearAllCommentsInThisChapter": "Rensa alla kommentarer, symboler och former i detta kapitel?", + "studyRightUnderTheBoard": "Direkt under brädet", + "studyNoPinnedComment": "Ingen", + "studyNormalAnalysis": "Normal analys", + "studyHideNextMoves": "Dölj nästa drag", + "studyInteractiveLesson": "Interaktiv lektion", + "studyChapterX": "Kapitel {param}", + "studyEmpty": "Tom", + "studyStartFromInitialPosition": "Starta från ursprunglig position", + "studyEditor": "Redigeringsverktyg", + "studyStartFromCustomPosition": "Starta från anpassad position", + "studyLoadAGameByUrl": "Importera ett spel med URL", + "studyLoadAPositionFromFen": "Importera en position med FEN-kod", + "studyLoadAGameFromPgn": "Importera ett spel med PGN-kod", + "studyAutomatic": "Automatisk", + "studyUrlOfTheGame": "URL till partiet", + "studyLoadAGameFromXOrY": "Importera ett parti från {param1} eller {param2}", + "studyCreateChapter": "Skapa kapitel", + "studyCreateStudy": "Skapa en studie", + "studyEditStudy": "Redigera studie", + "studyVisibility": "Synlighet", + "studyPublic": "Offentlig", + "studyUnlisted": "Ej listad", + "studyInviteOnly": "Endast inbjudna", + "studyAllowCloning": "Tillåt kloning", + "studyNobody": "Ingen", + "studyOnlyMe": "Bara mig", + "studyContributors": "Medhjälpare", + "studyMembers": "Medlemmar", + "studyEveryone": "Alla", + "studyEnableSync": "Aktivera synkronisering", + "studyYesKeepEveryoneOnTheSamePosition": "Ja: håll alla på samma position", + "studyNoLetPeopleBrowseFreely": "Nej: låt alla bläddra fritt", + "studyPinnedStudyComment": "Ständigt synlig studiekommentar", + "studyStart": "Starta", + "studySave": "Spara", + "studyClearChat": "Rensa Chatten", + "studyDeleteTheStudyChatHistory": "Radera studiens chatthistorik? Detta går inte att ångra!", + "studyDeleteStudy": "Ta bort studie", + "studyConfirmDeleteStudy": "Radera hela studien? Detta går inte att ångra! Skriv namnet på studien för att bekräfta: {param}", + "studyWhereDoYouWantToStudyThat": "Var vill du studera detta?", + "studyGoodMove": "Bra drag", + "studyMistake": "Misstag", + "studyBrilliantMove": "Lysande drag", + "studyBlunder": "Blunder", + "studyInterestingMove": "Intressant drag", + "studyDubiousMove": "Tvivelaktigt drag", + "studyOnlyMove": "Enda draget", + "studyZugzwang": "Zugzwang", + "studyEqualPosition": "Likvärdig position", + "studyUnclearPosition": "Oklar position", + "studyWhiteIsSlightlyBetter": "Vit är något bättre", + "studyBlackIsSlightlyBetter": "Svart är något bättre", + "studyWhiteIsBetter": "Vit är bättre", + "studyBlackIsBetter": "Svart är bättre", + "studyWhiteIsWinning": "Vit vinner", + "studyBlackIsWinning": "Svart vinner", + "studyNovelty": "Ny variant", + "studyDevelopment": "Utveckling", + "studyInitiative": "Initiativ", + "studyAttack": "Attack", + "studyCounterplay": "Motspel", + "studyTimeTrouble": "Tidsproblem", + "studyWithCompensation": "Med kompensation", + "studyWithTheIdea": "Med idén", + "studyNextChapter": "Nästa kapitel", + "studyPrevChapter": "Föregående kapitel", + "studyStudyActions": "Studie-alternativ", + "studyTopics": "Ämnen", + "studyMyTopics": "Mina ämnen", + "studyPopularTopics": "Populära ämnen", + "studyManageTopics": "Hantera ämnen", + "studyBack": "Tillbaka", + "studyPlayAgain": "Spela igen", + "studyWhatWouldYouPlay": "Vad skulle du spela i denna position?", + "studyYouCompletedThisLesson": "Grattis! Du har slutfört denna lektionen.", + "studyNbChapters": "{count, plural, =1{{count} Kapitel} other{{count} Kapitel}}", + "studyNbGames": "{count, plural, =1{{count} partier} other{{count} partier}}", + "studyNbMembers": "{count, plural, =1{{count} Medlem} other{{count} Medlemmar}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Klistra in din PGN-kod här, upp till {count} parti} other{Klistra in din PGN-kod här, upp till {count} partier}}" } \ No newline at end of file diff --git a/lib/l10n/lila_tr.arb b/lib/l10n/lila_tr.arb index d59f54004e..64d6191c48 100644 --- a/lib/l10n/lila_tr.arb +++ b/lib/l10n/lila_tr.arb @@ -9,12 +9,17 @@ "mobileFeedbackButton": "Geri bildirimde bulun", "mobileOkButton": "Tamam", "mobileSettingsHapticFeedback": "Titreşimli geri bildirim", + "mobileSettingsImmersiveMode": "Sürükleyici mod", + "mobileSettingsImmersiveModeSubtitle": "Oynarken sistem arayüzünü gizle. Ekranın kenarlarındaki sistemin gezinme hareketlerinden rahatsızsan bunu kullan. Bu ayar, oyun ve Bulmaca Fırtınası ekranlarına uygulanır.", "mobileNotFollowingAnyUser": "Hiçbir kullanıcıyı takip etmiyorsunuz.", "mobileAllGames": "Tüm oyunlar", "mobileRecentSearches": "Son aramalar", "mobileClearButton": "Temizle", + "mobilePlayersMatchingSearchTerm": "\"{param}\" ile başlayan oyuncularla", "mobileNoSearchResults": "Sonuç bulunamadı", "mobileAreYouSure": "Emin misiniz?", + "mobilePuzzleStreakAbortWarning": "Mevcut serinizi kaybedeceksiniz ve puanınız kaydedilecektir.", + "mobilePuzzleStormNothingToShow": "Gösterilcek bir şey yok. Birkaç kez Bulmaca Fırtınası oyunu oynayın.", "mobileSharePuzzle": "Bulmacayı paylaş", "mobileShareGameURL": "Oyun linkini paylaş", "mobileShareGamePGN": "PGN'yi paylaş", @@ -22,12 +27,21 @@ "mobileShowVariations": "Varyasyonları göster", "mobileHideVariation": "Varyasyonu gizle", "mobileShowComments": "Yorumları göster", + "mobilePuzzleStormConfirmEndRun": "Bu oyunu bitirmek istiyor musun?", + "mobilePuzzleStormFilterNothingToShow": "Gösterilecek bir şey yok, lütfen filtreleri değiştirin", "mobileCancelTakebackOffer": "Geri alma teklifini iptal et", - "mobileCancelDrawOffer": "Berabere teklifini iptal et", "mobileWaitingForOpponentToJoin": "Rakip bekleniyor...", "mobileBlindfoldMode": "Körleme modu", + "mobileLiveStreamers": "Canlı yayıncılar", "mobileCustomGameJoinAGame": "Bir oyuna katıl", + "mobileCorrespondenceClearSavedMove": "Kayıtlı hamleyi sil", "mobileSomethingWentWrong": "Birşeyler ters gitti.", + "mobileShowResult": "Sonucu göster", + "mobilePuzzleThemesSubtitle": "En sevdiğiniz açılışlardan bulmacalar oynayın veya bir tema seçin.", + "mobilePuzzleStormSubtitle": "3 dakika içerisinde mümkün olduğunca çok bulmaca çözün.", + "mobileGreeting": "Merhaba, {param}", + "mobileGreetingWithoutName": "Merhaba", + "mobilePrefMagnifyDraggedPiece": "Sürüklenen parçayı büyüt", "activityActivity": "Son Etkinlikler", "activityHostedALiveStream": "Canlı yayın yaptı", "activityRankedInSwissTournament": "{param2} katılımcıları arasında #{param1}. oldu", @@ -40,6 +54,7 @@ "activityPlayedNbMoves": "{count, plural, =1{{count} hamle yaptı} other{{count} hamle yaptı}}", "activityInNbCorrespondenceGames": "{count, plural, =1{({count} yazışmalı oyunda)} other{({count} yazışmalı oyunda)}}", "activityCompletedNbGames": "{count, plural, =1{{count} adet yazışmalı oyun tamamladı} other{{count} adet yazışmalı oyun tamamladı}}", + "activityCompletedNbVariantGames": "{count, plural, =1{{count} {param2} yazışmalı oyunu tamamladı} other{{count} {param2} yazışmalı oyunu tamamladı}}", "activityFollowedNbPlayers": "{count, plural, =1{{count} oyuncuyu takip etmeye başladı} other{{count} oyuncuyu takip etmeye başladı}}", "activityGainedNbFollowers": "{count, plural, =1{{count} yeni takipçi kazandı} other{{count} yeni takipçi kazandı}}", "activityHostedNbSimuls": "{count, plural, =1{{count} simultaneye ev sahipliği yaptı} other{{count} eş zamanlı gösteriye ev sahipliği yaptı}}", @@ -50,7 +65,71 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{{count} İsviçre Sistemi turnuvasına katıldı} other{{count} İsviçre Sistemi turnuvasına katıldı}}", "activityJoinedNbTeams": "{count, plural, =1{{count} takıma katıldı} other{{count} takıma katıldı}}", "broadcastBroadcasts": "Canlı Turnuvalar", + "broadcastMyBroadcasts": "Canlı Turnuvalarım", "broadcastLiveBroadcasts": "Canlı Turnuva Yayınları", + "broadcastBroadcastCalendar": "Turnuva takvimi", + "broadcastNewBroadcast": "Canlı Turnuva Ekle", + "broadcastSubscribedBroadcasts": "Abone olduğunuz yayınlar", + "broadcastAboutBroadcasts": "Canlı Turnuvalar hakkında", + "broadcastHowToUseLichessBroadcasts": "Lichess Canlı Turnuvaları nasıl kullanılır.", + "broadcastTheNewRoundHelp": "Yeni tur, önceki turdaki üyeler ve katkıda bulunanlarla aynı olacak.", + "broadcastAddRound": "Bir tur ekle", + "broadcastOngoing": "Devam eden turnuvalar", + "broadcastUpcoming": "Yaklaşan turnuvalar", + "broadcastCompleted": "Tamamlanan turnuvalar", + "broadcastCompletedHelp": "Lichess, tur tamamlanmasını kaynak oyunlara dayanarak algılar. Kaynak yoksa bu anahtarı kullanın.", + "broadcastRoundName": "Tur ismi", + "broadcastRoundNumber": "Tur sayısı", + "broadcastTournamentName": "Turnuva ismi", + "broadcastTournamentDescription": "Turnuvanın kısa tanımı", + "broadcastFullDescription": "Etkinliğin detaylıca açıklaması", + "broadcastFullDescriptionHelp": "Etkinliğin isteğe bağlı detaylı açıklaması. {param1} seçeneği mevcuttur. Metnin uzunluğu azami {param2} karakter olmalıdır.", + "broadcastSourceSingleUrl": "PGN Kaynak URL'si", + "broadcastSourceUrlHelp": "Lichess, sağladığınız URL yardımıyla PGN'yi güncelleyecektir. İnternet üzerinden herkese açık bir URL yazmalısınız.", + "broadcastSourceGameIds": "Boşluklarla ayrılmış 64 adede kadar Lichess oyun ID'si.", + "broadcastStartDateTimeZone": "Turnuva yerel saati ile başlama zamanı: {param}", + "broadcastStartDateHelp": "İsteğe bağlı, etkinliğin ne zaman başladığını biliyorsanız ekleyebilirsiniz.", + "broadcastCurrentGameUrl": "Şu anki oyunun linki", + "broadcastDownloadAllRounds": "Bütün maçları indir", + "broadcastResetRound": "Bu turu sıfırla", + "broadcastDeleteRound": "Bu turu sil", + "broadcastDefinitivelyDeleteRound": "Turu ve oyunlarını tamamen sil.", + "broadcastDeleteAllGamesOfThisRound": "Bu turdaki tüm oyunları sil. Tekrardan yapılabilmesi için kaynağın aktif olması gerekir.", + "broadcastEditRoundStudy": "Tur çalışmasını düzenle", + "broadcastDeleteTournament": "Bu turnuvayı sil", + "broadcastDefinitivelyDeleteTournament": "Bütün turnuvayı, turlarını ve oyunlarını kalıcı olarak sil.", + "broadcastShowScores": "Oyuncuların puanlarını oyun sonuçlarına göre göster", + "broadcastReplacePlayerTags": "İsteğe bağlı: Oyuncu adlarını, derecelendirmelerini ve unvanlarını değiştirin", + "broadcastFideFederations": "FIDE federasyonları", + "broadcastTop10Rating": "İlk 10 rating", + "broadcastFidePlayers": "FIDE oyuncuları", + "broadcastFidePlayerNotFound": "FIDE oyuncusu bulunamadı", + "broadcastFideProfile": "FIDE profili", + "broadcastFederation": "Federasyon", + "broadcastAgeThisYear": "Bu yılki yaşı", + "broadcastUnrated": "Derecelendirilmemiş", + "broadcastRecentTournaments": "Son Turnuvalar", + "broadcastOpenLichess": "Lichess'te aç", + "broadcastTeams": "Takımlar", + "broadcastBoards": "Tahtalar", + "broadcastOverview": "Genel Bakış", + "broadcastSubscribeTitle": "Tur başladığında bildirim almak için abone olun. Hesap tercihlerinizden anlık ya da çan bildirimi tercihinizi hesap tercihlerinizden belirleyebilirsiniz.", + "broadcastUploadImage": "Turnuva görseli yükleyin", + "broadcastNoBoardsYet": "Henüz tahta bulunmamaktadır. Oyunlar yüklendikçe tahtalar ortaya çıkacaktır.", + "broadcastBoardsCanBeLoaded": "Tahtalar bir kaynaktan ya da {param}ndan yüklenebilir", + "broadcastStartsAfter": "{param}'ten sonra başlar", + "broadcastStartVerySoon": "Yayın az sonra başlayacak.", + "broadcastNotYetStarted": "Yayın henüz başlamadı.", + "broadcastOfficialWebsite": "Resmî site", + "broadcastStandings": "Sıralamalar", + "broadcastIframeHelp": "{param}nda daha fazla seçenek", + "broadcastPgnSourceHelp": "Bu turun açık, gerçek zamanlı PGN kaynağı. Daha hızlı ve verimli senkronizasyon için {param}'ımız da bulunmaktadır.", + "broadcastEmbedThisBroadcast": "İnternet sitenizde bu yayını gömülü paylaşın", + "broadcastEmbedThisRound": "{param}u İnternet sitenizde gömülü paylaşın", + "broadcastRatingDiff": "Puan farkı", + "broadcastGamesThisTournament": "Bu turnuvadaki maçlar", + "broadcastScore": "Skor", + "broadcastNbBroadcasts": "{count, plural, =1{{count} canlı turnuva} other{{count} canlı turnuva}}", "challengeChallengesX": "{param1} karşılaşmaları", "challengeChallengeToPlay": "Oyun teklif et", "challengeChallengeDeclined": "Oyun teklifi reddedildi", @@ -369,8 +448,8 @@ "puzzleThemeXRayAttackDescription": "Rakibin taşı üzerinden bir kareyi koruyan veya bir kareye saldıran taşları içeren taktikler.", "puzzleThemeZugzwang": "Zugzwang", "puzzleThemeZugzwangDescription": "Rakibin iyi bir hamlesinin kalmadığı, yapabileceği bütün hamlelerin kendisine zarar vereceği taktikler.", - "puzzleThemeHealthyMix": "Bir ondan bir bundan", - "puzzleThemeHealthyMixDescription": "Ortaya karışık bulmacalar. Karşına ne tür bir pozisyonun çıkacağı tam bir muamma. Tıpkı gerçek maçlardaki gibi her şeye hazırlıklı olmakta fayda var.", + "puzzleThemeMix": "Bir ondan bir bundan", + "puzzleThemeMixDescription": "Ortaya karışık bulmacalar. Karşına ne tür bir pozisyonun çıkacağı tam bir muamma. Tıpkı gerçek maçlardaki gibi her şeye hazırlıklı olmakta fayda var.", "puzzleThemePlayerGames": "Bireysel oyunlar", "puzzleThemePlayerGamesDescription": "Sizin veya başka bir oyuncunun maçlarından üretilen bulmacalar.", "puzzleThemePuzzleDownloadInformation": "Bütün bulmacalar kamuya açıktır ve {param} adresinden indirilebilir.", @@ -450,7 +529,7 @@ "makeMainLine": "Ana devam yolu yap", "deleteFromHere": "Bu hamleden sonrasını sil", "collapseVariations": "Varyasyonları daralt", - "expandVariations": "Varyasyonları genişler", + "expandVariations": "Varyasyonları genişlet", "forceVariation": "Varyant olarak göster", "copyVariationPgn": "Varyasyon PGN'sini kopyala", "move": "Hamle", @@ -501,7 +580,6 @@ "memory": "Hafıza", "infiniteAnalysis": "Sonsuz analiz", "removesTheDepthLimit": "Derinlik sınırını kaldırır ve bilgisayarınızı sıcacık tutar", - "engineManager": "Motor yöneticisi", "blunder": "Vahim hata", "mistake": "Hata", "inaccuracy": "Kusurlu hamle", @@ -527,6 +605,7 @@ "latestForumPosts": "En son forum gönderileri", "players": "Oyuncular", "friends": "Arkadaşlar", + "otherPlayers": "diğer oyuncular", "discussions": "Sohbetler", "today": "Bugün", "yesterday": "Dün", @@ -784,6 +863,8 @@ "descPrivateHelp": "Sadece takım üyelerinin görebileceği metindir. Kullanılması halinde, takım üyeleri genel açıklama yerine bunu görür.", "no": "Hayır", "yes": "Evet", + "website": "Web sitesi", + "mobile": "Mobil", "help": "Yardım:", "createANewTopic": "Yeni bir bildiri oluştur.", "topics": "Konular", @@ -802,7 +883,9 @@ "cheat": "Hile", "troll": "Trol", "other": "Diğer", - "reportDescriptionHelp": "Raporlamak istediğiniz oyunun linkini yapıştırın ve sorununuzu açıklayın. Lütfen sadece \"hile yapıyor\" gibisinden açıklama yazmayın, hile olduğunu nasıl anladığınızı açıklayın. Rapor edeceğiniz kişiyi ya da oyunu \"İngilizce\" açıklarsanız, daha hızlı sonuca ulaşırsınız.", + "reportCheatBoostHelp": "Oyunun (oyunların) bağlantısını yapıştırın ve kullanıcının hangi davranışı yanlış açıklayın. Sadece \"bu hile\" demeyin, bize bu sonuca nasıl vardığınızı söyleyin.", + "reportUsernameHelp": "Bu kullanıcı adı hakkında neyin saldırganca olduğunu açıklayın. Sadece \"bu saldırganca/uygunsuz\" demeyin, bu sonuca nasıl vardığınızı söyleyin bize, özellikle de hakaret gizlenmiş, ingilizce olmayan, argo, veya tarihsel/kültürel referansalar varsa.", + "reportProcessedFasterInEnglish": "Eğer İngilizce yazarsanız raporunuz daha hızlı işleme alınır.", "error_provideOneCheatedGameLink": "Lütfen hileli gördüğünüz en az 1 adet oyun linki verin.", "by": "{param} oluşturdu", "importedByX": "{param} tarafından yüklendi", @@ -835,6 +918,7 @@ "slow": "Yavaş", "insideTheBoard": "Karelerin üzerinde", "outsideTheBoard": "Karelerin dışında", + "allSquaresOfTheBoard": "Tahtadaki tüm karelerde", "onSlowGames": "Yavaş oyunlarda", "always": "Her zaman", "never": "Asla", @@ -1199,6 +1283,7 @@ "showMeEverything": "Bana her şeyi göster", "lichessPatronInfo": "Lichess bir yardım kuruluşudur ve tamamen özgür/açık kaynak kodlu bir yazılımdır. Tüm işletme maliyetleri, geliştirmeler ve içerikler yalnızca kullanıcı bağışları ile finanse edilmektedir.", "nothingToSeeHere": "Şu anda görülebilecek bir şey yok.", + "stats": "İstatistikler", "opponentLeftCounter": "{count, plural, =1{Rakibiniz oyundan ayrıldı. {count} saniye sonra galibiyet talep edebilirsiniz.} other{Rakibiniz oyundan ayrıldı. {count} saniye sonra galibiyet talep edebilirsiniz.}}", "mateInXHalfMoves": "{count, plural, =1{{count} yarım hamle sonra mat} other{{count} yarım hamlede mat}}", "nbBlunders": "{count, plural, =1{{count} vahim hata} other{{count} vahim hata}}", @@ -1295,6 +1380,159 @@ "stormXRuns": "{count, plural, =1{1 tur} other{{count} tur}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Bir kez {param2} oynadı} other{{count} kez {param2} oynadı}}", "streamerLichessStreamers": "Lichess yayıncıları", + "studyPrivate": "Gizli", + "studyMyStudies": "Çalışmalarım", + "studyStudiesIContributeTo": "Katkıda bulunduğum çalışmalar", + "studyMyPublicStudies": "Herkese açık çalışmalarım", + "studyMyPrivateStudies": "Gizli çalışmalarım", + "studyMyFavoriteStudies": "En sevdiğim çalışmalar", + "studyWhatAreStudies": "Çalışmalar nedir?", + "studyAllStudies": "Bütün çalışmalar", + "studyStudiesCreatedByX": "Çalışmalar {param} tarafından oluşturulmuştur", + "studyNoneYet": "Henüz yok.", + "studyHot": "Popüler", + "studyDateAddedNewest": "Eklenme tarihi (en yeni)", + "studyDateAddedOldest": "Eklenme tarihi (en eski)", + "studyRecentlyUpdated": "Yeni güncellenmiş", + "studyMostPopular": "En popüler", + "studyAlphabetical": "Alfabetik", + "studyAddNewChapter": "Yeni bir bölüm ekle", + "studyAddMembers": "Üye ekle", + "studyInviteToTheStudy": "Bu çalışmaya davet edin", + "studyPleaseOnlyInvitePeopleYouKnow": "Lütfen sadece tanıdığınız ve katkı sağlayacağını düşündüğünüz kişileri davet ediniz.", + "studySearchByUsername": "Kullanıcı adına göre ara", + "studySpectator": "İzleyici", + "studyContributor": "Katılımcı", + "studyKick": "Çıkar", + "studyLeaveTheStudy": "Çalışmadan ayrıl", + "studyYouAreNowAContributor": "Artık bir katılımcısınız", + "studyYouAreNowASpectator": "Artık bir izleyicisiniz", + "studyPgnTags": "PGN etiketleri", + "studyLike": "Beğen", + "studyUnlike": "Beğenmekten Vazgeç", + "studyNewTag": "Yeni etiket", + "studyCommentThisPosition": "Bu pozisyon için yorum yap", + "studyCommentThisMove": "Bu hamle için yorum yap", + "studyAnnotateWithGlyphs": "Glifler ile açıkla", + "studyTheChapterIsTooShortToBeAnalysed": "Bu bölüm analiz için çok kısa.", + "studyOnlyContributorsCanRequestAnalysis": "Sadece katılımcılar bilgisayar analizi isteyebilir.", + "studyGetAFullComputerAnalysis": "Bu varyant için ayrıntılı bir sunucu analizi yapın.", + "studyMakeSureTheChapterIsComplete": "Bu bölümü tamamladığınızdan emin olun. Sadece bir kez bilgisayar analizi talep edebilirsiniz.", + "studyAllSyncMembersRemainOnTheSamePosition": "Senkronize edilen bütün üyeler aynı pozisyonda kalır", + "studyShareChanges": "Değişiklikleri izleyiciler ile paylaşın ve sunucuya kaydedin", + "studyPlaying": "Oynanıyor", + "studyShowEvalBar": "Değerlendirme çubuğu", + "studyFirst": "İlk", + "studyPrevious": "Önceki", + "studyNext": "Sonraki", + "studyLast": "Son", "studyShareAndExport": "Paylaş ve dışa aktar", - "studyStart": "Başlat" + "studyCloneStudy": "Klon", + "studyStudyPgn": "Çalışma PGN'si", + "studyDownloadAllGames": "Bütün oyunları indir", + "studyChapterPgn": "Bölüm PGN'si", + "studyCopyChapterPgn": "PGN 'yi kopyala", + "studyDownloadGame": "Oyunu indir", + "studyStudyUrl": "Çalışma Adresi", + "studyCurrentChapterUrl": "Mevcut Bölümün Adresi", + "studyYouCanPasteThisInTheForumToEmbed": "Forumda gömülü olarak paylaşmak için yukarıdaki bağlantıyı kullanabilirsiniz", + "studyStartAtInitialPosition": "İlk pozisyondan başlasın", + "studyStartAtX": "{param} pozisyonundan başlasın", + "studyEmbedInYourWebsite": "İnternet sitenizde ya da blogunuzda gömülü olarak paylaşın", + "studyReadMoreAboutEmbedding": "Gömülü paylaşma hakkında", + "studyOnlyPublicStudiesCanBeEmbedded": "Yalnızca herkese açık çalışmalar gömülü paylaşılabilir!", + "studyOpen": "Aç", + "studyXBroughtToYouByY": "{param2} sana {param1} getirdi", + "studyStudyNotFound": "Böyle bir çalışma bulunamadı", + "studyEditChapter": "Bölümü düzenle", + "studyNewChapter": "Yeni bölüm", + "studyImportFromChapterX": "{param} çalışmasından içe aktar", + "studyOrientation": "Tahta yönü", + "studyAnalysisMode": "Analiz modu", + "studyPinnedChapterComment": "Bölüm üzerine yorumlar", + "studySaveChapter": "Bölümü kaydet", + "studyClearAnnotations": "Açıklamaları sil", + "studyClearVariations": "Varyasyonları sil", + "studyDeleteChapter": "Bölümü sil", + "studyDeleteThisChapter": "Bu bölüm silinsin mi? Bunun geri dönüşü yok!", + "studyClearAllCommentsInThisChapter": "Bu bölümdeki bütün yorumlar ve işaretler temizlensin mi?", + "studyRightUnderTheBoard": "Tahtanın hemen altında görünsün", + "studyNoPinnedComment": "Yok", + "studyNormalAnalysis": "Normal analiz", + "studyHideNextMoves": "Sonraki hamleleri gizle", + "studyInteractiveLesson": "Etkileşimli ders", + "studyChapterX": "Bölüm {param}", + "studyEmpty": "Boş", + "studyStartFromInitialPosition": "İlk pozisyondan başlasın", + "studyEditor": "Editör", + "studyStartFromCustomPosition": "Özel bir pozisyondan başlasın", + "studyLoadAGameByUrl": "URL ile oyun yükle", + "studyLoadAPositionFromFen": "FEN kullanarak pozisyon yükle", + "studyLoadAGameFromPgn": "PGN ile oyun yükle", + "studyAutomatic": "Otomatik", + "studyUrlOfTheGame": "Oyunun bağlantısı", + "studyLoadAGameFromXOrY": "{param1} veya {param2} kullanarak oyun yükle", + "studyCreateChapter": "Bölüm oluştur", + "studyCreateStudy": "Çalışma oluştur", + "studyEditStudy": "Çalışmayı düzenle", + "studyVisibility": "Görünürlük", + "studyPublic": "Herkese açık", + "studyUnlisted": "Liste dışı", + "studyInviteOnly": "Sadece davet edilenler", + "studyAllowCloning": "Klonlamaya izni olanlar", + "studyNobody": "Hiç kimse", + "studyOnlyMe": "Sadece ben", + "studyContributors": "Katkıda Bulunanlar", + "studyMembers": "Üyeler", + "studyEveryone": "Herkes", + "studyEnableSync": "Senkronizasyonu etkinleştir", + "studyYesKeepEveryoneOnTheSamePosition": "Evet: herkes aynı pozisyonda kalsın", + "studyNoLetPeopleBrowseFreely": "Hayır: herkes dilediğince gezinebilsin", + "studyPinnedStudyComment": "Çalışma üzerine yorumlar", + "studyStart": "Başlat", + "studySave": "Kaydet", + "studyClearChat": "Sohbeti temizle", + "studyDeleteTheStudyChatHistory": "Çalışmanın sohbet geçmişi silinsin mi? Bunun geri dönüşü yok!", + "studyDeleteStudy": "Çalışmayı sil", + "studyConfirmDeleteStudy": "Tüm çalışma silinsin mi? Bunun geri dönüşü yok! İşlemi onaylamak için çalışmanın adını yazın: {param}", + "studyWhereDoYouWantToStudyThat": "Bu çalışmayı nerede sürdürmek istersiniz?", + "studyGoodMove": "İyi hamle", + "studyMistake": "Hata", + "studyBrilliantMove": "Muhteşem hamle", + "studyBlunder": "Vahim hata", + "studyInterestingMove": "İlginç hamle", + "studyDubiousMove": "Şüpheli hamle", + "studyOnlyMove": "Tek hamle", + "studyZugzwang": "Zugzwang", + "studyEqualPosition": "Eşit pozisyon", + "studyUnclearPosition": "Belirsiz pozisyon", + "studyWhiteIsSlightlyBetter": "Beyaz biraz önde", + "studyBlackIsSlightlyBetter": "Siyah biraz önde", + "studyWhiteIsBetter": "Beyaz daha üstün", + "studyBlackIsBetter": "Siyah daha üstün", + "studyWhiteIsWinning": "Beyaz kazanıyor", + "studyBlackIsWinning": "Siyah kazanıyor", + "studyNovelty": "Farklı bir açılış", + "studyDevelopment": "Gelişim hamlesi", + "studyInitiative": "Girişim", + "studyAttack": "Saldırı", + "studyCounterplay": "Karşı saldırı", + "studyTimeTrouble": "Vakit sıkıntısı", + "studyWithCompensation": "Pozisyon avantajı ile", + "studyWithTheIdea": "Plan doğrultusunda", + "studyNextChapter": "Sonraki bölüm", + "studyPrevChapter": "Önceki bölüm", + "studyStudyActions": "Çalışma seçenekleri", + "studyTopics": "Konular", + "studyMyTopics": "Konularım", + "studyPopularTopics": "Popüler konular", + "studyManageTopics": "Konuları yönet", + "studyBack": "Baştan başlat", + "studyPlayAgain": "Tekrar oyna", + "studyWhatWouldYouPlay": "Burada hangi hamleyi yapardınız?", + "studyYouCompletedThisLesson": "Tebrikler! Bu dersi tamamlandınız.", + "studyNbChapters": "{count, plural, =1{{count} Bölüm} other{{count} Bölüm}}", + "studyNbGames": "{count, plural, =1{{count} oyun} other{{count} Oyun}}", + "studyNbMembers": "{count, plural, =1{{count} Üye} other{{count} Üye}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{PGN metninizi buraya yapıştırın, en fazla {count} oyuna kadar} other{PGN metninizi buraya yapıştırın, en fazla {count} oyuna kadar}}" } \ No newline at end of file diff --git a/lib/l10n/lila_uk.arb b/lib/l10n/lila_uk.arb index d027f7aa1b..122ff1be47 100644 --- a/lib/l10n/lila_uk.arb +++ b/lib/l10n/lila_uk.arb @@ -30,7 +30,6 @@ "mobilePuzzleStormConfirmEndRun": "Ви хочете закінчити цю серію?", "mobilePuzzleStormFilterNothingToShow": "Нічого не знайдено, будь ласка, змініть фільтри", "mobileCancelTakebackOffer": "Скасувати пропозицію повернення ходу", - "mobileCancelDrawOffer": "Скасувати пропозицію нічиєї", "mobileWaitingForOpponentToJoin": "Очікування на суперника...", "mobileBlindfoldMode": "Наосліп", "mobileLiveStreamers": "Стримери в прямому етері", @@ -42,6 +41,7 @@ "mobilePuzzleStormSubtitle": "Розв'яжіть якомога більше задач за 3 хвилини.", "mobileGreeting": "Привіт, {param}", "mobileGreetingWithoutName": "Привіт", + "mobilePrefMagnifyDraggedPiece": "Збільшувати розмір фігури при перетягуванні", "activityActivity": "Активність", "activityHostedALiveStream": "Проведено пряму трансляцію", "activityRankedInSwissTournament": "Зайняв #{param1} місце в {param2}", @@ -54,6 +54,7 @@ "activityPlayedNbMoves": "{count, plural, =1{Зроблено {count} хід} few{Зроблено {count} ходи} many{Зроблено {count} ходів} other{Зроблено {count} ходів}}", "activityInNbCorrespondenceGames": "{count, plural, =1{у {count} заочній грі} few{у {count} заочних іграх} many{у {count} заочних іграх} other{у {count} заочних ігор}}", "activityCompletedNbGames": "{count, plural, =1{Зіграно {count} заочну гру} few{Зіграно {count} заочні гри} many{Зіграно {count} заочних ігор} other{Зіграно {count} заочних ігор}}", + "activityCompletedNbVariantGames": "{count, plural, =1{Зіграно {count} {param2} заочну гру} few{Зіграно {count} {param2} заочні гри} many{Зіграно {count} {param2} заочних ігор} other{Зіграно {count} {param2} заочних ігор}}", "activityFollowedNbPlayers": "{count, plural, =1{Підписався на {count} гравця} few{Почав спостерігати за {count} гравцями} many{Почав спостерігати за {count} гравцями} other{Почав спостерігати за {count} гравцями}}", "activityGainedNbFollowers": "{count, plural, =1{Отримав {count} нового підписника} few{Отримав {count} нових підписників} many{Отримав {count} нових підписників} other{Отримав {count} нових підписників}}", "activityHostedNbSimuls": "{count, plural, =1{Провів {count} сеанс одночасної гри} few{Провів {count} сеанси одночасної гри} many{Провів {count} сеансів одночасної гри} other{Провів {count} сеансів одночасної гри}}", @@ -64,7 +65,51 @@ "activityCompetedInNbSwissTournaments": "{count, plural, =1{Завершив {count} турнір за швейцарською системою} few{Завершив {count} турніри за швейцарською системою} many{Завершив {count} турнірів за швейцарською системою} other{Завершив {count} турніри за швейцарською системою}}", "activityJoinedNbTeams": "{count, plural, =1{Приєднався до {count} команди} few{Приєднався до {count} команд} many{Приєднався до {count} команд} other{Приєднався до {count} команд}}", "broadcastBroadcasts": "Трансляції", + "broadcastMyBroadcasts": "Мої трансляції", "broadcastLiveBroadcasts": "Онлайн трансляції турнірів", + "broadcastBroadcastCalendar": "Календар трансляцій", + "broadcastNewBroadcast": "Нова трансляція", + "broadcastSubscribedBroadcasts": "Обрані трансляції", + "broadcastAboutBroadcasts": "Про трансляцію", + "broadcastHowToUseLichessBroadcasts": "Як користуватися Lichess трансляціями.", + "broadcastTheNewRoundHelp": "У новому раунді будуть ті самі учасники та редактори, що й у попередньому.", + "broadcastAddRound": "Додати раунд", + "broadcastOngoing": "Поточні", + "broadcastUpcoming": "Майбутні", + "broadcastCompleted": "Завершені", + "broadcastCompletedHelp": "Lichess виявляє завершення раунду на основі ігор. Використовуйте цей перемикач якщо немає джерела.", + "broadcastRoundName": "Назва раунду", + "broadcastRoundNumber": "Номер раунду", + "broadcastTournamentName": "Назва турніру", + "broadcastTournamentDescription": "Короткий опис турніру", + "broadcastFullDescription": "Повний опис події", + "broadcastFullDescriptionHelp": "Необов'язковий довгий опис трансляції. Наявна розмітка {param1}. Довжина має бути менша ніж {param2} символів.", + "broadcastSourceSingleUrl": "Адреса джерела PGN", + "broadcastSourceUrlHelp": "Посилання, яке Lichess перевірятиме, щоб отримати оновлення PGN. Воно має бути загальнодоступним в Інтернеті.", + "broadcastSourceGameIds": "До 64 ігрових ID Lichess, відокремлені пробілами.", + "broadcastStartDateHelp": "За бажанням, якщо ви знаєте, коли починається подія", + "broadcastCurrentGameUrl": "Посилання на поточну гру", + "broadcastDownloadAllRounds": "Завантажити всі тури", + "broadcastResetRound": "Скинути цей раунд", + "broadcastDeleteRound": "Видалити цей раунд", + "broadcastDefinitivelyDeleteRound": "Видалити всі ігри цього раунду.", + "broadcastDeleteAllGamesOfThisRound": "Видалити всі ігри цього раунду. Джерело має бути активним для того, щоб повторно відтворити його.", + "broadcastEditRoundStudy": "Редагувати дослідження раунду", + "broadcastDeleteTournament": "Видалити турнір", + "broadcastDefinitivelyDeleteTournament": "Остаточно видалити весь турнір, всі його раунди та всі його ігри.", + "broadcastShowScores": "Показувати результати гравців за результатами гри", + "broadcastReplacePlayerTags": "За бажанням: замінити імена, рейтинги та титули гравців", + "broadcastFideFederations": "Федерації FIDE", + "broadcastTop10Rating": "Топ 10 рейтингу", + "broadcastFidePlayers": "Гравці FIDE", + "broadcastFidePlayerNotFound": "Гравця FIDE не знайдено", + "broadcastFideProfile": "Профіль FIDE", + "broadcastFederation": "Федерація", + "broadcastAgeThisYear": "Вік цього року", + "broadcastUnrated": "Без рейтингу", + "broadcastRecentTournaments": "Нещодавні турніри", + "broadcastOfficialWebsite": "Офіційний вебсайт", + "broadcastNbBroadcasts": "{count, plural, =1{{count} трансляція} few{{count} трансляції} many{{count} трансляцій} other{{count} трансляцій}}", "challengeChallengesX": "Виклики: {param1}", "challengeChallengeToPlay": "Виклик на гру", "challengeChallengeDeclined": "Виклик відхилено", @@ -383,8 +428,8 @@ "puzzleThemeXRayAttackDescription": "Фігура, що атакує чи захищає поле, що знаходиться за фігурою суперника.", "puzzleThemeZugzwang": "Цугцванг", "puzzleThemeZugzwangDescription": "Суперник обмежений в своїх ходах, а кожен хід погіршує його позицію.", - "puzzleThemeHealthyMix": "Здорова суміш", - "puzzleThemeHealthyMixDescription": "Всього потроху. Ви не знаєте, чого очікувати, тому готуйтесь до всього! Як у справжніх партіях.", + "puzzleThemeMix": "Усього потрохи", + "puzzleThemeMixDescription": "Усього потрохи. Ви не знатимете, чого очікувати, тому готуйтеся до всього! Як у справжніх партіях.", "puzzleThemePlayerGames": "Ігри гравця", "puzzleThemePlayerGamesDescription": "Пошук задач, згенерованих з ваших ігор або з ігор інших гравців.", "puzzleThemePuzzleDownloadInformation": "Ці задачі є у публічному доступі та можуть бути завантажені з {param}.", @@ -515,7 +560,6 @@ "memory": "Пам'ять", "infiniteAnalysis": "Нескінченний аналіз", "removesTheDepthLimit": "Знімає обмеження на глибину аналізу - ваш комп’ютер стане теплішим", - "engineManager": "Менеджер рушія", "blunder": "Груба помилка", "mistake": "Помилка", "inaccuracy": "Неточність", @@ -597,6 +641,7 @@ "rank": "Місце", "rankX": "Місце: {param}", "gamesPlayed": "Ігор зіграно", + "ok": "Гаразд", "cancel": "Скасувати", "whiteTimeOut": "Час білих вийшов", "blackTimeOut": "Час чорних вийшов", @@ -819,7 +864,9 @@ "cheat": "Нечесна гра", "troll": "Тролінг", "other": "Інше", - "reportDescriptionHelp": "Вставте посилання на гру (ігри) та поясніть, що не так із поведінкою цього користувача. Не пишіть просто \"він шахраює\", а розкажіть, як ви дійшли до такого висновку. Вашу скаргу розглянуть швидше, якщо ви напишете її англійською.", + "reportCheatBoostHelp": "Вставте посилання на гру або ігри та поясніть, що не так з поведінкою цього користувача. Не кажіть «він нечесно грав», а поясніть, як ви прийшли до такого висновку.", + "reportUsernameHelp": "Поясніть, що саме в цьому імені користувача є образливе. Не кажіть «воно образливе/неприйнятне», а поясніть, чому ви так вважаєте, особливо коли образа заплутана, не англійською, на сленгу, чи є посиланням на щось історичне/культурне.", + "reportProcessedFasterInEnglish": "Ваша скарга оброблятиметься швидше, якщо буде написана англійською.", "error_provideOneCheatedGameLink": "Будь ласка, додайте посилання на хоча б одну нечесну гру.", "by": "від {param}", "importedByX": "Завантажено гравцем - {param}", @@ -1217,6 +1264,7 @@ "showMeEverything": "Показати все", "lichessPatronInfo": "Lichess — це благодійне й абсолютно безкоштовне програмне забезпечення з відкритим кодом.\nУсі витрати на обслуговування, розробку й контент фінансуються виключно пожертвуваннями користувачів.", "nothingToSeeHere": "Поки тут нічого немає.", + "stats": "Статистика", "opponentLeftCounter": "{count, plural, =1{Ваш суперник покинув гру. Ви можете оголосити перемогу за {count} секунд.} few{Ваш суперник покинув гру. Ви можете оголосити перемогу за {count} секунди.} many{Ваш суперник покинув гру. Ви можете оголосити перемогу за {count} секунд.} other{Ваш суперник покинув гру. Ви можете оголосити перемогу за {count} секунд.}}", "mateInXHalfMoves": "{count, plural, =1{Мат в {count} напівхід} few{Мат в {count} напівходи} many{Мат в {count} напівходів} other{Мат у {count} напівходів}}", "nbBlunders": "{count, plural, =1{{count} груба помилка} few{{count} грубі помилки} many{{count} грубих помилок} other{{count} грубих помилок}}", @@ -1313,6 +1361,159 @@ "stormXRuns": "{count, plural, =1{1 серія} few{{count} серії} many{{count} серій} other{{count} серій}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, =1{Зіграна одна серія в {param2}} few{Зіграно {count} серії в {param2}} many{Зіграно {count} серій в {param2}} other{Зіграно {count} серій в {param2}}}", "streamerLichessStreamers": "Стримери Lichess", + "studyPrivate": "Приватне", + "studyMyStudies": "Мої дослідження", + "studyStudiesIContributeTo": "Дослідження, яким я сприяю", + "studyMyPublicStudies": "Мої публічні дослідження", + "studyMyPrivateStudies": "Мої приватні дослідження", + "studyMyFavoriteStudies": "Мої улюблені дослідження", + "studyWhatAreStudies": "Що таке дослідження?", + "studyAllStudies": "Усі дослідження", + "studyStudiesCreatedByX": "Дослідження, створені {param}", + "studyNoneYet": "Ще немає.", + "studyHot": "Активні", + "studyDateAddedNewest": "Дата додавання (старіші)", + "studyDateAddedOldest": "Дата додавання (старіші)", + "studyRecentlyUpdated": "Нещодавно оновлені", + "studyMostPopular": "Найпопулярніші", + "studyAlphabetical": "За алфавітом", + "studyAddNewChapter": "Додати новий розділ", + "studyAddMembers": "Додати учасників", + "studyInviteToTheStudy": "Запросити до дослідження", + "studyPleaseOnlyInvitePeopleYouKnow": "Будь ласка запрошуйте лише людей, яких ви знаєте, і які хочуть активно долучитися до цього дослідження.", + "studySearchByUsername": "Пошук за іменем користувача", + "studySpectator": "Глядач", + "studyContributor": "Співавтор", + "studyKick": "Вигнати", + "studyLeaveTheStudy": "Покинути дослідження", + "studyYouAreNowAContributor": "Тепер ви співавтор", + "studyYouAreNowASpectator": "Тепер ви глядач", + "studyPgnTags": "Теги PGN", + "studyLike": "Подобається", + "studyUnlike": "Не подобається", + "studyNewTag": "Новий тег", + "studyCommentThisPosition": "Коментувати цю позицію", + "studyCommentThisMove": "Коментувати цей хід", + "studyAnnotateWithGlyphs": "Додати символьну анотацію", + "studyTheChapterIsTooShortToBeAnalysed": "Розділ занадто короткий для аналізу.", + "studyOnlyContributorsCanRequestAnalysis": "Лише співавтори дослідження можуть дати запит на комп'ютерний аналіз.", + "studyGetAFullComputerAnalysis": "Отримати повний серверний комп'ютерний аналіз головної лінії.", + "studyMakeSureTheChapterIsComplete": "Переконайтесь, що розділ завершено. Ви можете дати запит на аналіз лише один раз.", + "studyAllSyncMembersRemainOnTheSamePosition": "Усі синхронізовані учасники залишаються на тій же позиції", + "studyShareChanges": "Поділитися змінами з глядачами та зберегти їх на сервері", + "studyPlaying": "Активні", + "studyShowEvalBar": "Шкала оцінки", + "studyFirst": "Перша", + "studyPrevious": "Попередня", + "studyNext": "Наступна", + "studyLast": "Остання", "studyShareAndExport": "Надсилання та експорт", - "studyStart": "Почати" + "studyCloneStudy": "Клонувати", + "studyStudyPgn": "PGN дослідження", + "studyDownloadAllGames": "Завантажити всі партії", + "studyChapterPgn": "PGN розділу", + "studyCopyChapterPgn": "Скопіювати PGN", + "studyDownloadGame": "Завантажити гру", + "studyStudyUrl": "Посилання на дослідження", + "studyCurrentChapterUrl": "Посилання на цей розділ", + "studyYouCanPasteThisInTheForumToEmbed": "Ви можете вставити цей код на форумі для вбудування", + "studyStartAtInitialPosition": "Старт з початкової позиції", + "studyStartAtX": "Почати з {param}", + "studyEmbedInYourWebsite": "Вбудувати на своєму сайті", + "studyReadMoreAboutEmbedding": "Докладніше про вбудовування", + "studyOnlyPublicStudiesCanBeEmbedded": "Лише публічні дослідження можна вбудовувати!", + "studyOpen": "Відкрити", + "studyXBroughtToYouByY": "{param1} надано вам {param2}", + "studyStudyNotFound": "Дослідження не знайдено", + "studyEditChapter": "Редагувати розділ", + "studyNewChapter": "Новий розділ", + "studyImportFromChapterX": "Імпортувати з {param}", + "studyOrientation": "Орієнтація", + "studyAnalysisMode": "Режим аналізу", + "studyPinnedChapterComment": "Закріплений коментар розділу", + "studySaveChapter": "Зберегти розділ", + "studyClearAnnotations": "Очистити анотацію", + "studyClearVariations": "Очистити анотацію", + "studyDeleteChapter": "Видалити розділ", + "studyDeleteThisChapter": "Видалити цей розділ? Відновити буде неможливо!", + "studyClearAllCommentsInThisChapter": "Очистити всі коментарі та позначки з цього розділу?", + "studyRightUnderTheBoard": "Відразу під шахівницею", + "studyNoPinnedComment": "Немає", + "studyNormalAnalysis": "Звичайний аналіз", + "studyHideNextMoves": "Приховати наступні ходи", + "studyInteractiveLesson": "Інтерактивний урок", + "studyChapterX": "Розділ {param}", + "studyEmpty": "Порожній", + "studyStartFromInitialPosition": "Старт з початкової позиції", + "studyEditor": "Редактор", + "studyStartFromCustomPosition": "Почати з обраної позиції", + "studyLoadAGameByUrl": "Завантажте гру за посиланням", + "studyLoadAPositionFromFen": "Завантажити позицію з FEN", + "studyLoadAGameFromPgn": "Завантажити гру з PGN", + "studyAutomatic": "Автоматично", + "studyUrlOfTheGame": "Посилання на гру", + "studyLoadAGameFromXOrY": "Завантажити гру з {param1} або {param2}", + "studyCreateChapter": "Створити розділ", + "studyCreateStudy": "Створити дослідження", + "studyEditStudy": "Редагування дослідження", + "studyVisibility": "Видимість", + "studyPublic": "Публічне", + "studyUnlisted": "Поза списком", + "studyInviteOnly": "Лише за запрошенням", + "studyAllowCloning": "Дозволити копіювання", + "studyNobody": "Ніхто", + "studyOnlyMe": "Лише я", + "studyContributors": "Співавтори", + "studyMembers": "Учасники", + "studyEveryone": "Всі", + "studyEnableSync": "Увімкнути синхронізацію", + "studyYesKeepEveryoneOnTheSamePosition": "Так: однакова позиція для всіх", + "studyNoLetPeopleBrowseFreely": "Ні: дозволити вільний перегляд", + "studyPinnedStudyComment": "Закріплений коментар дослідження", + "studyStart": "Почати", + "studySave": "Зберегти", + "studyClearChat": "Очистити чат", + "studyDeleteTheStudyChatHistory": "Видалити історію чату дослідження? Відновити буде неможливо!", + "studyDeleteStudy": "Видалити дослідження", + "studyConfirmDeleteStudy": "Ви дійсно бажаєте видалити все дослідження? Назад дороги немає! Введіть назву дослідження для підтвердження: {param}", + "studyWhereDoYouWantToStudyThat": "Де ви хочете це дослідити?", + "studyGoodMove": "Хороший хід", + "studyMistake": "Помилка", + "studyBrilliantMove": "Блискучий хід", + "studyBlunder": "Груба помилка", + "studyInterestingMove": "Цікавий хід", + "studyDubiousMove": "Сумнівний хід", + "studyOnlyMove": "Єдиний хід", + "studyZugzwang": "Цугцванг", + "studyEqualPosition": "Рівна позиція", + "studyUnclearPosition": "Незрозуміла позиція", + "studyWhiteIsSlightlyBetter": "Позиція білих трохи краще", + "studyBlackIsSlightlyBetter": "Позиція чорних трохи краще", + "studyWhiteIsBetter": "Позиція білих краще", + "studyBlackIsBetter": "Позиція чорних краще", + "studyWhiteIsWinning": "Білі перемагають", + "studyBlackIsWinning": "Чорні перемагають", + "studyNovelty": "Новинка", + "studyDevelopment": "Розвиток", + "studyInitiative": "Ініціатива", + "studyAttack": "Атака", + "studyCounterplay": "Контргра", + "studyTimeTrouble": "Цейтнот", + "studyWithCompensation": "З компенсацією", + "studyWithTheIdea": "З ідеєю", + "studyNextChapter": "Наступний розділ", + "studyPrevChapter": "Попередній розділ", + "studyStudyActions": "Команди дослідження", + "studyTopics": "Теми", + "studyMyTopics": "Мої теми", + "studyPopularTopics": "Популярні теми", + "studyManageTopics": "Управління темами", + "studyBack": "Назад", + "studyPlayAgain": "Грати знову", + "studyWhatWouldYouPlay": "Що б ви грали в цій позиції?", + "studyYouCompletedThisLesson": "Вітаємо! Ви завершили цей урок.", + "studyNbChapters": "{count, plural, =1{{count} розділ} few{{count} розділи} many{{count} розділів} other{{count} розділи}}", + "studyNbGames": "{count, plural, =1{{count} Партія} few{{count} Партії} many{{count} Партій} other{{count} Партій}}", + "studyNbMembers": "{count, plural, =1{{count} учасник} few{{count} учасники} many{{count} учасників} other{{count} учасників}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Вставте ваш PGN текст тут, до {count} гри} few{Вставте ваш PGN текст тут, до {count} ігор} many{Вставте ваш PGN текст тут, до {count} ігор} other{Вставте ваш PGN текст тут, до {count} ігор}}" } \ No newline at end of file diff --git a/lib/l10n/lila_vi.arb b/lib/l10n/lila_vi.arb index a6d4b58b6a..e164ed8b4d 100644 --- a/lib/l10n/lila_vi.arb +++ b/lib/l10n/lila_vi.arb @@ -30,7 +30,6 @@ "mobilePuzzleStormConfirmEndRun": "Bạn có muốn kết thúc lượt chạy này không?", "mobilePuzzleStormFilterNothingToShow": "Không có gì để hiển thị, vui lòng thay đổi bộ lọc", "mobileCancelTakebackOffer": "Hủy đề nghị đi lại", - "mobileCancelDrawOffer": "Hủy đề nghị hòa", "mobileWaitingForOpponentToJoin": "Đang chờ đối thủ tham gia...", "mobileBlindfoldMode": "Bịt mắt", "mobileLiveStreamers": "Các Streamer phát trực tiếp", @@ -39,10 +38,14 @@ "mobileSomethingWentWrong": "Đã xảy ra lỗi.", "mobileShowResult": "Xem kết quả", "mobilePuzzleThemesSubtitle": "Giải câu đố từ những khai cuộc yêu thích của bạn hoặc chọn một chủ đề.", + "mobilePuzzleStormSubtitle": "Giải càng nhiều câu đố càng tốt trong 3 phút.", + "mobileGreeting": "Xin chào, {param}", + "mobileGreetingWithoutName": "Xin chào", + "mobilePrefMagnifyDraggedPiece": "Phóng to quân cờ được kéo", "activityActivity": "Hoạt động", "activityHostedALiveStream": "Đã phát trực tiếp", "activityRankedInSwissTournament": "Đứng hạng {param1} trong giải {param2}", - "activitySignedUp": "Đã ghi danh ở lichess.org", + "activitySignedUp": "Đã ghi danh tại lichess.org", "activitySupportedNbMonths": "{count, plural, other{Đã ủng hộ lichess.org {count} tháng với tư cách là một {param2}}}", "activityPracticedNbPositions": "{count, plural, other{Đã luyện tập {count} thế cờ trên {param2}}}", "activitySolvedNbPuzzles": "{count, plural, other{Đã giải {count} câu đố}}", @@ -51,7 +54,8 @@ "activityPlayedNbMoves": "{count, plural, other{Đã đi {count} nước}}", "activityInNbCorrespondenceGames": "{count, plural, other{trong {count} ván cờ qua thư}}", "activityCompletedNbGames": "{count, plural, other{Đã hoàn thành {count} ván cờ qua thư}}", - "activityFollowedNbPlayers": "{count, plural, other{Đã theo dõi {count} người chơi}}", + "activityCompletedNbVariantGames": "{count, plural, other{Đã hoàn thành {count} ván cờ qua thư {param2}}}", + "activityFollowedNbPlayers": "{count, plural, other{Đã theo dõi {count} kỳ thủ}}", "activityGainedNbFollowers": "{count, plural, other{Đạt được {count} người theo dõi mới}}", "activityHostedNbSimuls": "{count, plural, other{Đã chủ trì {count} sự kiện cờ đồng loạt}}", "activityJoinedNbSimuls": "{count, plural, other{Đã tham gia {count} sự kiện cờ đồng loạt}}", @@ -61,7 +65,72 @@ "activityCompetedInNbSwissTournaments": "{count, plural, other{Đã hoàn thành {count} giải đấu hệ Thụy Sĩ}}", "activityJoinedNbTeams": "{count, plural, other{Đã tham gia {count} đội}}", "broadcastBroadcasts": "Các phát sóng", + "broadcastMyBroadcasts": "Các phát sóng của tôi", "broadcastLiveBroadcasts": "Các giải đấu phát sóng trực tiếp", + "broadcastBroadcastCalendar": "Lịch phát sóng", + "broadcastNewBroadcast": "Phát sóng trực tiếp mới", + "broadcastSubscribedBroadcasts": "Các phát sóng đã đăng ký theo dõi", + "broadcastAboutBroadcasts": "Giới thiệu về phát sóng", + "broadcastHowToUseLichessBroadcasts": "Cách sử dụng Phát sóng của Lichess.", + "broadcastTheNewRoundHelp": "Vòng mới sẽ có các thành viên và cộng tác viên giống như vòng trước.", + "broadcastAddRound": "Thêm vòng", + "broadcastOngoing": "Đang diễn ra", + "broadcastUpcoming": "Sắp diễn ra", + "broadcastCompleted": "Đã hoàn thành", + "broadcastCompletedHelp": "Lichess phát hiện việc hoàn thành vòng đấu dựa trên các ván đấu nguồn. Sử dụng nút chuyển đổi này nếu không có nguồn.", + "broadcastRoundName": "Tên vòng", + "broadcastRoundNumber": "Vòng đấu số", + "broadcastTournamentName": "Tên giải đấu", + "broadcastTournamentDescription": "Mô tả ngắn giải đấu", + "broadcastFullDescription": "Mô tả đầy đủ giải đấu", + "broadcastFullDescriptionHelp": "Tùy chọn mô tả dài về giải đấu. Có thể sử dụng {param1}. Độ dài phải nhỏ hơn {param2} ký tự.", + "broadcastSourceSingleUrl": "URL Nguồn PGN", + "broadcastSourceUrlHelp": "URL mà Lichess sẽ khảo sát để nhận cập nhật PGN. Nó phải được truy cập công khai từ Internet.", + "broadcastSourceGameIds": "Tối đa 64 ID ván cờ trên Lichess, phân tách bằng dấu cách.", + "broadcastStartDateTimeZone": "Thời gian bắt đầu của giải theo múi giờ địa phương: {param}", + "broadcastStartDateHelp": "Tùy chọn, nếu bạn biết khi nào sự kiện bắt đầu", + "broadcastCurrentGameUrl": "URL ván đấu hiện tại", + "broadcastDownloadAllRounds": "Tải về tất cả ván đấu", + "broadcastResetRound": "Đặt lại vòng này", + "broadcastDeleteRound": "Xóa vòng này", + "broadcastDefinitivelyDeleteRound": "Dứt khoát xóa tất cả vòng đấu và các ván đấu trong đó.", + "broadcastDeleteAllGamesOfThisRound": "Xóa toàn bộ ván cờ trong vòng này. Để tạo lại chúng bạn cần thêm lại nguồn.", + "broadcastEditRoundStudy": "Chỉnh sửa vòng nghiên cứu", + "broadcastDeleteTournament": "Xóa giải đấu này", + "broadcastDefinitivelyDeleteTournament": "Xóa dứt khoát toàn bộ giải đấu, tất cả các vòng và tất cả ván cờ trong đó.", + "broadcastShowScores": "Hiển thị điểm số của người chơi dựa trên kết quả ván đấu", + "broadcastReplacePlayerTags": "Tùy chọn: biệt danh, hệ số Elo và danh hiệu", + "broadcastFideFederations": "Các liên đoàn FIDE", + "broadcastTop10Rating": "Hệ số Elo top 10", + "broadcastFidePlayers": "Các kỳ thủ FIDE", + "broadcastFidePlayerNotFound": "Không tìm thấy kỳ thủ FIDE", + "broadcastFideProfile": "Hồ sơ FIDE", + "broadcastFederation": "Liên đoàn", + "broadcastAgeThisYear": "Tuổi năm nay", + "broadcastUnrated": "Chưa xếp hạng", + "broadcastRecentTournaments": "Các giải đấu tham gia gần đây", + "broadcastOpenLichess": "Mở trên Lichess", + "broadcastTeams": "Các đội", + "broadcastBoards": "Các bàn đấu", + "broadcastOverview": "Tổng quan", + "broadcastSubscribeTitle": "Đăng ký để được thông báo khi mỗi vòng bắt đầu. Bạn có thể chuyển đổi chuông hoặc thông báo đẩy cho các chương trình phát sóng trong tùy chọn tài khoản của mình.", + "broadcastUploadImage": "Tải hình ảnh giải đấu lên", + "broadcastNoBoardsYet": "Chưa có bàn nào. Chúng sẽ xuất hiện khi ván đấu được tải lên.", + "broadcastBoardsCanBeLoaded": "Bàn đấu có thể được tải bằng nguồn hoặc thông qua {param}", + "broadcastStartsAfter": "Bắt đầu sau {param}", + "broadcastStartVerySoon": "Chương trình phát sóng sẽ sớm bắt đầu.", + "broadcastNotYetStarted": "Chương trình phát sóng vẫn chưa bắt đầu.", + "broadcastOfficialWebsite": "Website chính thức", + "broadcastStandings": "Bảng xếp hạng", + "broadcastIframeHelp": "Thêm tùy chọn trên {param}", + "broadcastWebmastersPage": "trang nhà phát triển web", + "broadcastPgnSourceHelp": "Nguồn PGN công khai, thời gian thực cho vòng này. Chúng tôi cũng cung cấp {param} để đồng bộ hóa nhanh hơn và hiệu quả hơn.", + "broadcastEmbedThisBroadcast": "Nhúng chương trình phát sóng này vào trang web của bạn", + "broadcastEmbedThisRound": "Nhúng {param} vào trang web của bạn", + "broadcastRatingDiff": "Độ thay đổi hệ số", + "broadcastGamesThisTournament": "Các ván đấu trong giải này", + "broadcastScore": "Điểm số", + "broadcastNbBroadcasts": "{count, plural, other{{count} phát sóng}}", "challengeChallengesX": "Số thách đấu: {param1}", "challengeChallengeToPlay": "Thách đấu một ván cờ", "challengeChallengeDeclined": "Lời thách đấu bị từ chối.", @@ -325,7 +394,7 @@ "puzzleThemeMaster": "Ván đấu cao cấp", "puzzleThemeMasterDescription": "Câu đố từ các ván đấu của người có danh hiệu.", "puzzleThemeMasterVsMaster": "Ván đấu giữa 2 kiện tướng", - "puzzleThemeMasterVsMasterDescription": "Câu đố từ các ván đấu giữa hai người chơi có danh hiệu.", + "puzzleThemeMasterVsMasterDescription": "Câu đố từ các ván đấu giữa hai kiện tướng.", "puzzleThemeMate": "Chiếu hết", "puzzleThemeMateDescription": "Chiến thắng ván cờ với phong cách.", "puzzleThemeMateIn1": "Chiếu hết trong 1 nước", @@ -380,8 +449,8 @@ "puzzleThemeXRayAttackDescription": "Một quân cờ tấn công hoặc phòng thủ một ô sau một quân cờ khác của đối phương.", "puzzleThemeZugzwang": "Cưỡng ép", "puzzleThemeZugzwangDescription": "Đối phương bị giới hạn các nước mà họ có thể đi và tất cả các nước đi ấy đều hại họ.", - "puzzleThemeHealthyMix": "Phối hợp nhịp nhàng", - "puzzleThemeHealthyMixDescription": "Mỗi thứ một chút. Bạn không biết được thứ gì đang chờ mình, vậy nên bạn cần phải sẵn sàng cho mọi thứ! Như một ván cờ thật vậy!", + "puzzleThemeMix": "Phối hợp nhịp nhàng", + "puzzleThemeMixDescription": "Mỗi thứ một chút. Bạn không biết được thứ gì đang chờ mình, vậy nên bạn cần phải sẵn sàng cho mọi thứ! Như một ván cờ thật vậy!", "puzzleThemePlayerGames": "Các ván đấu của người chơi", "puzzleThemePlayerGamesDescription": "Những câu đố từ những ván cờ của bạn hoặc từ các ván cờ của những người chơi khác.", "puzzleThemePuzzleDownloadInformation": "Những câu đố này thuộc phạm vi công khai và có thể tải về từ {param}.", @@ -397,7 +466,7 @@ "playWithAFriend": "Chơi với bạn bè", "playWithTheMachine": "Chơi với máy tính", "toInviteSomeoneToPlayGiveThisUrl": "Để mời ai đó chơi, hãy gửi URL này", - "gameOver": "Kết thúc ván cờ", + "gameOver": "Ván cờ kết thúc", "waitingForOpponent": "Đang chờ đối thủ", "orLetYourOpponentScanQrCode": "Hoặc để đối thủ của bạn quét mã QR này", "waiting": "Đang chờ", @@ -437,15 +506,15 @@ "talkInChat": "Hãy cư xử thân thiện trong cuộc trò chuyện!", "theFirstPersonToComeOnThisUrlWillPlayWithYou": "Người đầu tiên sử dụng URL này sẽ bắt đầu chơi với bạn.", "whiteResigned": "Bên trắng chịu thua", - "blackResigned": "Đen chịu thua", + "blackResigned": "Bên đen chịu thua", "whiteLeftTheGame": "Bên trắng đã rời khỏi ván cờ", "blackLeftTheGame": "Bên đen đã rời khỏi ván cờ", "whiteDidntMove": "Bên trắng không đi quân", "blackDidntMove": "Bên đen không đi quân", "requestAComputerAnalysis": "Yêu cầu máy tính phân tích", - "computerAnalysis": "Máy tính phân tích", - "computerAnalysisAvailable": "Có sẵn máy tính phân tích", - "computerAnalysisDisabled": "Phân tích máy tính bị vô hiệu hóa", + "computerAnalysis": "Phân tích từ máy tính", + "computerAnalysisAvailable": "Có sẵn phân tích từ máy tính", + "computerAnalysisDisabled": "Phân tích từ máy tính bị vô hiệu hóa", "analysis": "Bàn cờ phân tích", "depthX": "Độ sâu {param}", "usingServerAnalysis": "Sử dụng phân tích nhờ máy chủ", @@ -480,7 +549,7 @@ "averageRatingX": "Hệ số bình quân: {param}", "recentGames": "Các ván cờ gần đây", "topGames": "Các ván đấu hàng đầu", - "masterDbExplanation": "Các ván đấu OTB của các kỳ thủ có hệ số Elo FIDE {param1}+ từ năm {param2} đến {param3}", + "masterDbExplanation": "Các ván đấu OTB của các kỳ thủ có hệ số Rating FIDE {param1}+ từ năm {param2} đến {param3}", "dtzWithRounding": "DTZ50\" được làm tròn, dựa vào số nước đi quân cho tới nước ăn quân hoặc đi tiến tốt tiếp theo", "noGameFound": "Không tìm thấy ván nào", "maxDepthReached": "Đã đạt độ sâu tối đa!", @@ -512,7 +581,6 @@ "memory": "Bộ nhớ", "infiniteAnalysis": "Phân tích vô hạn", "removesTheDepthLimit": "Bỏ giới hạn độ sâu và giữ máy tính của bạn mượt hơn", - "engineManager": "Quản lý động cơ", "blunder": "Sai lầm nghiêm trọng", "mistake": "Sai lầm", "inaccuracy": "Không chính xác", @@ -556,7 +624,7 @@ "username": "Tên đăng nhập", "usernameOrEmail": "Tên đăng nhập hoặc email", "changeUsername": "Thay đổi tên đăng nhập", - "changeUsernameNotSame": "Bạn chỉ có thể thay đổi cách viết hoa/thường. Ví dụ \"johndoe\" thành \"JohnDoe\".", + "changeUsernameNotSame": "Bạn chỉ có thể thay đổi cách viết hoa/thường. Ví dụ \"dotrongkhanh04032012\" thành \"DoTrongKhanh04032012\".", "changeUsernameDescription": "Thay đổi tên người dùng của bạn. Điều này chỉ có thể thực hiện một lần và bạn chỉ được thay đổi cách viết hoa/viết thường các chữ trong tên người dùng của bạn.", "signupUsernameHint": "Hãy đảm bảo chọn tên người dùng thân thiện với mọi người. Bạn sẽ không thể thay đổi nó và bất kỳ tài khoản nào có tên người dùng không phù hợp sẽ bị đóng!", "signupEmailHint": "Chúng tôi chỉ sử dụng nó cho việc khôi phục mật khẩu.", @@ -594,6 +662,7 @@ "rank": "Hạng", "rankX": "Hạng: {param}", "gamesPlayed": "Số ván đã chơi", + "ok": "OK", "cancel": "Hủy", "whiteTimeOut": "Bên trắng hết giờ", "blackTimeOut": "Bên đen hết giờ", @@ -693,7 +762,7 @@ "importGameCaveat": "Các biến sẽ bị xóa. Để giữ chúng, hãy nhập PGN thông qua một nghiên cứu.", "importGameDataPrivacyWarning": "Ai cũng có thể truy cập PGN này. Để nhập ván cờ một cách riêng tư, hãy sử dụng nghiên cứu.", "thisIsAChessCaptcha": "Đây là mã CAPTCHA cờ vua.", - "clickOnTheBoardToMakeYourMove": "Nhấn vào bàn cờ để di chuyển và chứng minh bạn là con người.", + "clickOnTheBoardToMakeYourMove": "Nhấn vào bàn cờ để di chuyển, và chứng minh bạn là con người.", "captcha_fail": "Hãy giải mã captcha cờ vua.", "notACheckmate": "Không phải là một nước chiếu hết", "whiteCheckmatesInOneMove": "Bên trắng hãy chiếu hết trong một nước đi", @@ -816,7 +885,9 @@ "cheat": "Gian lận", "troll": "Chọc tức, chơi khăm", "other": "Khác", - "reportDescriptionHelp": "Dán đường dẫn đến (các) ván cờ và giải thích về vấn đề của kỳ thủ này. Đừng chỉ nói \"họ gian lận\" mà hãy miêu tả chi tiết nhất có thể. Vấn đề sẽ được giải quyết nhanh hơn nếu bạn viết bằng tiếng Anh.", + "reportCheatBoostHelp": "Dán đường dẫn đến (các) ván cờ và giải thích hành vi sai của kỳ thủ này. Đừng chỉ nói \"họ gian lận\", nhưng hãy cho chúng tôi biết bạn đã đi đến kết luận này như thế nào.", + "reportUsernameHelp": "Giải thích những gì về tên người dùng này là xúc phạm. Đừng chỉ nói \"nó gây khó chịu/không phù hợp\", nhưng hãy cho chúng tôi biết bạn đã đi đến kết luận này như thế nào, đặc biệt nếu sự xúc phạm bị che giấu, không phải bằng tiếng Anh, là tiếng lóng, hoặc là một tài liệu tham khảo lịch sử/văn hóa.", + "reportProcessedFasterInEnglish": "Báo cáo của bạn sẽ được xử lý nhanh hơn nếu được viết bằng tiếng Anh.", "error_provideOneCheatedGameLink": "Hãy cung cấp ít nhất một đường dẫn đến ván cờ bị gian lận.", "by": "bởi {param}", "importedByX": "Được nhập bởi {param}", @@ -869,7 +940,7 @@ "side": "Bên", "clock": "Đồng hồ", "opponent": "Đối thủ", - "learnMenu": "Học", + "learnMenu": "Học tập", "studyMenu": "Nghiên cứu", "practice": "Luyện tập", "community": "Cộng đồng", @@ -957,7 +1028,7 @@ "playSelectedMove": "chơi nước đi đã chọn", "newTournament": "Giải đấu mới", "tournamentHomeTitle": "Giải đấu cờ vua với nhiều thiết lập thời gian và biến thể phong phú", - "tournamentHomeDescription": "Chơi các giải đấu cờ vua nhịp độ nhanh! Tham gia một giải đấu chính thức hoặc tự tạo giải đấu của bạn. Cờ Đạn, cờ Chớp, cờ Nhanh, cờ Chậm, Chess960, King of the Hill, Threecheck và nhiều lựa chọn khác cho niềm vui đánh cờ vô tận.", + "tournamentHomeDescription": "Chơi các giải đấu cờ vua nhịp độ nhanh! Tham gia một giải đấu chính thức hoặc tự tạo giải đấu của bạn. Cờ đạn, Cờ chớp, Cờ nhanh, Cờ chậm, Chess960, King of the Hill, Threecheck và nhiều lựa chọn khác cho niềm vui đánh cờ vô tận.", "tournamentNotFound": "Không tìm thấy giải đấu", "tournamentDoesNotExist": "Giải đấu này không tồn tại.", "tournamentMayHaveBeenCanceled": "Giải đấu có thể đã bị huỷ, nếu tất cả người chơi rời giải trước khi giải đấu bắt đầu.", @@ -1214,6 +1285,7 @@ "showMeEverything": "Cho tôi xem mọi thứ nào", "lichessPatronInfo": "Lichess là một tổ chức phi lợi nhuận và là phần mềm hoàn toàn miễn phí/mã nguồn mở.\nMọi chi phí vận hành, phát triển, và nội dung được tài trợ bởi những đóng góp của người dùng.", "nothingToSeeHere": "Không có gì để xem ở đây vào lúc này.", + "stats": "Thống kê", "opponentLeftCounter": "{count, plural, other{Đối thủ đã rời khỏi ván cờ. Bạn có thể tuyên bố thắng cuộc trong {count} giây.}}", "mateInXHalfMoves": "{count, plural, other{Chiếu hết trong {count} nước}}", "nbBlunders": "{count, plural, other{{count} sai lầm nghiêm trọng}}", @@ -1310,6 +1382,159 @@ "stormXRuns": "{count, plural, other{{count} lần chơi}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, other{Đã chơi {count} lượt {param2}}}", "streamerLichessStreamers": "Các Streamer của Lichess", + "studyPrivate": "Riêng tư", + "studyMyStudies": "Các nghiên cứu của tôi", + "studyStudiesIContributeTo": "Các nghiên cứu tôi đóng góp", + "studyMyPublicStudies": "Nghiên cứu công khai của tôi", + "studyMyPrivateStudies": "Nghiên cứu riêng tư của tôi", + "studyMyFavoriteStudies": "Các nghiên cứu yêu thích của tôi", + "studyWhatAreStudies": "Nghiên cứu là gì?", + "studyAllStudies": "Tất cả các nghiên cứu", + "studyStudiesCreatedByX": "Các nghiên cứu được tạo bởi {param}", + "studyNoneYet": "Chưa có gì cả.", + "studyHot": "Thịnh hành", + "studyDateAddedNewest": "Ngày được thêm (mới nhất)", + "studyDateAddedOldest": "Ngày được thêm (cũ nhất)", + "studyRecentlyUpdated": "Được cập nhật gần đây", + "studyMostPopular": "Phổ biến nhất", + "studyAlphabetical": "Theo thứ tự chữ cái", + "studyAddNewChapter": "Thêm một chương mới", + "studyAddMembers": "Thêm thành viên", + "studyInviteToTheStudy": "Mời vào nghiên cứu", + "studyPleaseOnlyInvitePeopleYouKnow": "Vui lòng chỉ mời những người bạn biết và những người tích cực muốn tham gia nghiên cứu này.", + "studySearchByUsername": "Tìm kiếm theo tên người dùng", + "studySpectator": "Khán giả", + "studyContributor": "Người đóng góp", + "studyKick": "Đuổi", + "studyLeaveTheStudy": "Rời khỏi nghiên cứu", + "studyYouAreNowAContributor": "Bây giờ bạn là một người đóng góp", + "studyYouAreNowASpectator": "Bây giờ bạn là một khán giả", + "studyPgnTags": "Nhãn PGN", + "studyLike": "Thích", + "studyUnlike": "Bỏ thích", + "studyNewTag": "Nhãn mới", + "studyCommentThisPosition": "Bình luận về thế cờ này", + "studyCommentThisMove": "Bình luận về nước cờ này", + "studyAnnotateWithGlyphs": "Chú thích bằng dấu", + "studyTheChapterIsTooShortToBeAnalysed": "Chương này quá ngắn để có thể được phân tích.", + "studyOnlyContributorsCanRequestAnalysis": "Chỉ những người đóng góp nghiên cứu mới có thể yêu cầu máy tính phân tích.", + "studyGetAFullComputerAnalysis": "Nhận phân tích máy tính phía máy chủ đầy đủ về biến chính.", + "studyMakeSureTheChapterIsComplete": "Hãy chắc chắn chương đã hoàn thành. Bạn chỉ có thể yêu cầu phân tích 1 lần.", + "studyAllSyncMembersRemainOnTheSamePosition": "Đồng bộ hóa tất cả các thành viên trên cùng một thế cờ", + "studyShareChanges": "Chia sẻ các thay đổi với khán giả và lưu chúng trên máy chủ", + "studyPlaying": "Đang chơi", + "studyShowEvalBar": "Thanh lợi thế", + "studyFirst": "Trang đầu", + "studyPrevious": "Trang trước", + "studyNext": "Trang tiếp theo", + "studyLast": "Trang cuối", "studyShareAndExport": "Chia sẻ & xuất", - "studyStart": "Bắt đầu" + "studyCloneStudy": "Nhân bản", + "studyStudyPgn": "PGN nghiên cứu", + "studyDownloadAllGames": "Tải về tất cả ván đấu", + "studyChapterPgn": "PGN chương", + "studyCopyChapterPgn": "Sao chép PGN", + "studyDownloadGame": "Tải về ván cờ", + "studyStudyUrl": "URL nghiên cứu", + "studyCurrentChapterUrl": "URL chương hiện tại", + "studyYouCanPasteThisInTheForumToEmbed": "Bạn có thể dán cái này để nhúng vào diễn đàn hoặc blog Lichess cá nhân của bạn", + "studyStartAtInitialPosition": "Bắt đầu từ thế cờ ban đầu", + "studyStartAtX": "Bắt đầu tại nước {param}", + "studyEmbedInYourWebsite": "Nhúng vào trang web của bạn", + "studyReadMoreAboutEmbedding": "Đọc thêm về việc nhúng", + "studyOnlyPublicStudiesCanBeEmbedded": "Chỉ các nghiên cứu công khai mới được nhúng!", + "studyOpen": "Mở", + "studyXBroughtToYouByY": "{param1} được lấy từ {param2}", + "studyStudyNotFound": "Không tìm thấy nghiên cứu nào", + "studyEditChapter": "Sửa chương", + "studyNewChapter": "Chương mới", + "studyImportFromChapterX": "Nhập từ chương {param}", + "studyOrientation": "Nghiên cứu cho bên", + "studyAnalysisMode": "Chế độ phân tích", + "studyPinnedChapterComment": "Đã ghim bình luận chương", + "studySaveChapter": "Lưu chương", + "studyClearAnnotations": "Xóa chú thích", + "studyClearVariations": "Xóa các biến", + "studyDeleteChapter": "Xóa chương", + "studyDeleteThisChapter": "Xóa chương này. Sẽ không có cách nào để có thể khôi phục lại!", + "studyClearAllCommentsInThisChapter": "Xóa tất cả bình luận, dấu chú thích và hình vẽ trong chương này", + "studyRightUnderTheBoard": "Ngay dưới bàn cờ", + "studyNoPinnedComment": "Không có", + "studyNormalAnalysis": "Phân tích thường", + "studyHideNextMoves": "Ẩn các nước tiếp theo", + "studyInteractiveLesson": "Bài học tương tác", + "studyChapterX": "Chương {param}", + "studyEmpty": "Trống", + "studyStartFromInitialPosition": "Bắt đầu từ thế cờ ban đầu", + "studyEditor": "Chỉnh sửa bàn cờ", + "studyStartFromCustomPosition": "Bắt đầu từ thế cờ tùy chỉnh", + "studyLoadAGameByUrl": "Tải ván cờ bằng URL", + "studyLoadAPositionFromFen": "Tải thế cờ từ chuỗi FEN", + "studyLoadAGameFromPgn": "Tải ván cờ từ PGN", + "studyAutomatic": "Tự động", + "studyUrlOfTheGame": "URL của các ván, một URL mỗi dòng", + "studyLoadAGameFromXOrY": "Tải ván cờ từ {param1} hoặc {param2}", + "studyCreateChapter": "Tạo chương", + "studyCreateStudy": "Tạo nghiên cứu", + "studyEditStudy": "Chỉnh sửa nghiên cứu", + "studyVisibility": "Khả năng hiển thị", + "studyPublic": "Công khai", + "studyUnlisted": "Không công khai", + "studyInviteOnly": "Chỉ những người được mời", + "studyAllowCloning": "Cho phép tạo bản sao", + "studyNobody": "Không ai cả", + "studyOnlyMe": "Chỉ mình tôi", + "studyContributors": "Những người đóng góp", + "studyMembers": "Thành viên", + "studyEveryone": "Mọi người", + "studyEnableSync": "Kích hoạt tính năng đồng bộ hóa", + "studyYesKeepEveryoneOnTheSamePosition": "Có: giữ tất cả mọi người trên 1 thế cờ", + "studyNoLetPeopleBrowseFreely": "Không: để mọi người tự do xem xét", + "studyPinnedStudyComment": "Bình luận nghiên cứu được ghim", + "studyStart": "Bắt đầu", + "studySave": "Lưu", + "studyClearChat": "Xóa trò chuyện", + "studyDeleteTheStudyChatHistory": "Xóa lịch sử trò chuyện nghiên cứu? Không thể khôi phục lại!", + "studyDeleteStudy": "Xóa nghiên cứu", + "studyConfirmDeleteStudy": "Xóa toàn bộ nghiên cứu? Không có cách nào để khôi phục lại! Nhập tên của nghiên cứu để xác nhận: {param}", + "studyWhereDoYouWantToStudyThat": "Bạn muốn nghiên cứu ở đâu?", + "studyGoodMove": "Nước tốt", + "studyMistake": "Sai lầm", + "studyBrilliantMove": "Nước đi thiên tài", + "studyBlunder": "Sai lầm nghiêm trọng", + "studyInterestingMove": "Nước đi hay", + "studyDubiousMove": "Nước đi mơ hồ", + "studyOnlyMove": "Nước duy nhất", + "studyZugzwang": "Zugzwang", + "studyEqualPosition": "Thế trận cân bằng", + "studyUnclearPosition": "Thế cờ không rõ ràng", + "studyWhiteIsSlightlyBetter": "Bên trắng có một chút lợi thế", + "studyBlackIsSlightlyBetter": "Bên đen có một chút lợi thế", + "studyWhiteIsBetter": "Bên trắng lợi thế hơn", + "studyBlackIsBetter": "Bên đen lợi thế hơn", + "studyWhiteIsWinning": "Bên trắng đang thắng dần", + "studyBlackIsWinning": "Bên đen đang thắng dần", + "studyNovelty": "Mới lạ", + "studyDevelopment": "Phát triển", + "studyInitiative": "Chủ động", + "studyAttack": "Tấn công", + "studyCounterplay": "Phản công", + "studyTimeTrouble": "Sắp hết thời gian", + "studyWithCompensation": "Có bù đắp", + "studyWithTheIdea": "Với ý tưởng", + "studyNextChapter": "Chương tiếp theo", + "studyPrevChapter": "Chương trước", + "studyStudyActions": "Các thao tác trong nghiên cứu", + "studyTopics": "Chủ đề", + "studyMyTopics": "Chủ đề của tôi", + "studyPopularTopics": "Chủ đề phổ biến", + "studyManageTopics": "Quản lý chủ đề", + "studyBack": "Quay Lại", + "studyPlayAgain": "Chơi lại", + "studyWhatWouldYouPlay": "Bạn sẽ làm gì ở thế cờ này?", + "studyYouCompletedThisLesson": "Chúc mừng! Bạn đã hoàn thành bài học này.", + "studyNbChapters": "{count, plural, other{{count} Chương}}", + "studyNbGames": "{count, plural, other{{count} Ván cờ}}", + "studyNbMembers": "{count, plural, other{{count} Thành viên}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, other{Dán PGN ở đây, tối đa {count} ván}}" } \ No newline at end of file diff --git a/lib/l10n/lila_zh.arb b/lib/l10n/lila_zh.arb index 12b2cf1e55..f2d025b20e 100644 --- a/lib/l10n/lila_zh.arb +++ b/lib/l10n/lila_zh.arb @@ -10,38 +10,38 @@ "mobileOkButton": "好", "mobileSettingsHapticFeedback": "震动反馈", "mobileSettingsImmersiveMode": "沉浸模式", - "mobileSettingsImmersiveModeSubtitle": "播放时隐藏系统UI。 如果您对屏幕边缘的系统导航手势感到困扰,请使用此功能。 适用于游戏和益智风暴屏幕。", + "mobileSettingsImmersiveModeSubtitle": "下棋时隐藏系统界面。 如果您的操作受到屏幕边缘的系统导航手势干扰,请使用此功能。 适用于棋局和 Puzzle Storm 界面。", "mobileNotFollowingAnyUser": "你没有关注任何用户。", "mobileAllGames": "所有对局", "mobileRecentSearches": "最近搜索", "mobileClearButton": "清空", - "mobilePlayersMatchingSearchTerm": "拥有\"{param}\"的玩家", + "mobilePlayersMatchingSearchTerm": "包含\"{param}\"名称的棋手", "mobileNoSearchResults": "无结果", "mobileAreYouSure": "你确定吗?", "mobilePuzzleStreakAbortWarning": "你将失去你目前的连胜,你的分数将被保存。", - "mobilePuzzleStormNothingToShow": "没什么好表现的。 玩拼图风暴的一些运行。", + "mobilePuzzleStormNothingToShow": "没有记录。 请下几组 Puzzle Storm。", "mobileSharePuzzle": "分享这个谜题", "mobileShareGameURL": "分享棋局链接", "mobileShareGamePGN": "分享 PGN", "mobileSharePositionAsFEN": "保存局面为 FEN", - "mobileShowVariations": "显示变化", - "mobileHideVariation": "隐藏变异", + "mobileShowVariations": "显示变着", + "mobileHideVariation": "隐藏变着", "mobileShowComments": "显示评论", - "mobilePuzzleStormConfirmEndRun": "你想结束这次跑步吗?", + "mobilePuzzleStormConfirmEndRun": "你想结束这组吗?", "mobilePuzzleStormFilterNothingToShow": "没有显示,请更改过滤器", "mobileCancelTakebackOffer": "取消悔棋请求", - "mobileCancelDrawOffer": "取消和棋请求", "mobileWaitingForOpponentToJoin": "正在等待对手加入...", "mobileBlindfoldMode": "盲棋", "mobileLiveStreamers": "主播", "mobileCustomGameJoinAGame": "加入一局游戏", - "mobileCorrespondenceClearSavedMove": "清除已保存的移动", - "mobileSomethingWentWrong": "发生一些错误。", + "mobileCorrespondenceClearSavedMove": "清除已保存的着法", + "mobileSomethingWentWrong": "出了一些问题。", "mobileShowResult": "显示结果", - "mobilePuzzleThemesSubtitle": "从你最喜欢的开口玩拼图,或选择一个主题。", - "mobilePuzzleStormSubtitle": "在3分钟内尽可能多地解决谜题", + "mobilePuzzleThemesSubtitle": "从你最喜欢的开局解决谜题,或选择一个主题。", + "mobilePuzzleStormSubtitle": "在3分钟内解决尽可能多的谜题。", "mobileGreeting": "你好,{param}", "mobileGreetingWithoutName": "你好!", + "mobilePrefMagnifyDraggedPiece": "放大正在拖动的棋子", "activityActivity": "动态", "activityHostedALiveStream": "主持了直播", "activityRankedInSwissTournament": "在 {param2} 中获得第 #{param1} 名", @@ -64,7 +64,50 @@ "activityCompetedInNbSwissTournaments": "{count, plural, other{参加了 {count} 场 swiss 锦标赛}}", "activityJoinedNbTeams": "{count, plural, other{加入了 {count} 个团队}}", "broadcastBroadcasts": "转播", + "broadcastMyBroadcasts": "我的直播", "broadcastLiveBroadcasts": "赛事转播", + "broadcastBroadcastCalendar": "转播日程表", + "broadcastNewBroadcast": "新建实况转播", + "broadcastSubscribedBroadcasts": "已订阅的转播", + "broadcastAboutBroadcasts": "关于转播", + "broadcastHowToUseLichessBroadcasts": "如何使用Lichess转播", + "broadcastTheNewRoundHelp": "新一轮的成员和贡献者将与前一轮相同。", + "broadcastAddRound": "添加一轮", + "broadcastOngoing": "进行中", + "broadcastUpcoming": "即将举行", + "broadcastCompleted": "已完成", + "broadcastCompletedHelp": "Lichess基于源游戏检测游戏的完成状态。如果没有源,请使用此选项。", + "broadcastRoundName": "轮次名称", + "broadcastRoundNumber": "轮数", + "broadcastTournamentName": "锦标赛名称", + "broadcastTournamentDescription": "锦标赛简短描述", + "broadcastFullDescription": "赛事详情", + "broadcastFullDescriptionHelp": "转播内容的详细描述 (可选)。可以使用 {param1},字数少于 {param2} 个。", + "broadcastSourceSingleUrl": "PGN的URL源", + "broadcastSourceUrlHelp": "Lichess 将从该网址搜查 PGN 的更新。它必须是公开的。", + "broadcastSourceGameIds": "多达64个 Lichess 棋局Id,用空格隔开。", + "broadcastStartDateHelp": "如果你知道比赛开始时间 (可选)", + "broadcastCurrentGameUrl": "当前棋局链接", + "broadcastDownloadAllRounds": "下载所有棋局", + "broadcastResetRound": "重置此轮", + "broadcastDeleteRound": "删除此轮", + "broadcastDefinitivelyDeleteRound": "确定删除该回合及其游戏。", + "broadcastDeleteAllGamesOfThisRound": "删除此回合的所有游戏。源需要激活才能重新创建。", + "broadcastEditRoundStudy": "编辑该轮次的棋局研究", + "broadcastDeleteTournament": "删除该锦标赛", + "broadcastDefinitivelyDeleteTournament": "确定删除整个锦标赛、所有轮次和其中所有比赛。", + "broadcastShowScores": "根据比赛结果显示棋手分数", + "broadcastReplacePlayerTags": "可选项:替换选手的名字、等级分和头衔", + "broadcastFideFederations": "FIDE 成员国", + "broadcastTop10Rating": "前10名等级分", + "broadcastFidePlayers": "FIDE 棋手", + "broadcastFidePlayerNotFound": "未找到 FIDE 棋手", + "broadcastFideProfile": "FIDE个人资料", + "broadcastFederation": "棋联", + "broadcastAgeThisYear": "今年的年龄", + "broadcastUnrated": "未评级", + "broadcastRecentTournaments": "最近的比赛", + "broadcastNbBroadcasts": "{count, plural, other{{count} 直播}}", "challengeChallengesX": "挑战: {param1}", "challengeChallengeToPlay": "发起挑战", "challengeChallengeDeclined": "拒绝挑战", @@ -91,7 +134,7 @@ "contactContact": "联系", "contactContactLichess": "联系 Lichess", "patronDonate": "捐赠", - "patronLichessPatron": "赞助 Lichess", + "patronLichessPatron": "Lichess赞助者账号", "perfStatPerfStats": "{param} 战绩", "perfStatViewTheGames": "查看棋局", "perfStatProvisional": "暂定", @@ -122,7 +165,7 @@ "perfStatMaxTimePlaying": "最长连续对局时间", "perfStatNow": "现在", "preferencesPreferences": "偏好设置", - "preferencesDisplay": "显示", + "preferencesDisplay": "界面设置", "preferencesPrivacy": "隐私设置", "preferencesNotifications": "通知", "preferencesPieceAnimation": "棋子动画", @@ -383,8 +426,8 @@ "puzzleThemeXRayAttackDescription": "一个棋子穿过对方的棋子攻击或防守一个格子。", "puzzleThemeZugzwang": "楚茨文克(无等着)", "puzzleThemeZugzwangDescription": "对手可选的着法是有限的,并且所有着法都会使其局面更加恶化。", - "puzzleThemeHealthyMix": "健康搭配", - "puzzleThemeHealthyMixDescription": "每个主题中选取一些。你不知道会出现什么,因此得时刻打起精神! 就像在真实对局中一样。", + "puzzleThemeMix": "健康搭配", + "puzzleThemeMixDescription": "每个主题中选取一些。你不知道会出现什么,因此得时刻打起精神! 就像在真实对局中一样。", "puzzleThemePlayerGames": "玩家对局", "puzzleThemePlayerGamesDescription": "查找从你或其他玩家的对局中产生的谜题。", "puzzleThemePuzzleDownloadInformation": "这些谜题都是公开的,可以在 {param} 下载。", @@ -407,7 +450,7 @@ "yourTurn": "你的回合", "aiNameLevelAiLevel": "{param1}级別{param2}", "level": "级别", - "strength": "电脑的难度", + "strength": "强度", "toggleTheChat": "聊天开关", "chat": "聊天", "resign": "认输", @@ -419,25 +462,25 @@ "asBlack": "持黑", "randomColor": "随机选色", "createAGame": "创建对局", - "whiteIsVictorious": "白方胜", - "blackIsVictorious": "黑方胜", + "whiteIsVictorious": "白方胜利", + "blackIsVictorious": "黑方胜利", "youPlayTheWhitePieces": "你执白棋", "youPlayTheBlackPieces": "你执黑棋", - "itsYourTurn": "轮到你了!", + "itsYourTurn": "你的回合!", "cheatDetected": "检测到作弊", "kingInTheCenter": "王占中", - "threeChecks": "三次将军", - "raceFinished": "比赛结束", + "threeChecks": "三次将军胜", + "raceFinished": "竞王结束", "variantEnding": "变种结束", "newOpponent": "新对手", "yourOpponentWantsToPlayANewGameWithYou": "你的对手想和你再玩一局", "joinTheGame": "加入对局", "whitePlays": "白方走棋", "blackPlays": "黑方走棋", - "opponentLeftChoices": "您的对手可能已离开棋局。您可以宣布胜利,和棋,或继续等待。", + "opponentLeftChoices": "你的对手已离开棋局。你可以宣布胜利、和棋或继续等待。", "forceResignation": "宣布胜利", - "forceDraw": "和棋", - "talkInChat": "聊天请注意文明用语。", + "forceDraw": "宣布和棋", + "talkInChat": "聊天请注意文明用语!", "theFirstPersonToComeOnThisUrlWillPlayWithYou": "第一个访问此网址的人将与你下棋。", "whiteResigned": "白方认输", "blackResigned": "黑方认输", @@ -458,7 +501,7 @@ "cloudAnalysis": "云分析", "goDeeper": "深入分析", "showThreat": "显示威胁", - "inLocalBrowser": "在本地浏览器", + "inLocalBrowser": "本地浏览器", "toggleLocalEvaluation": "切换到本地分析", "promoteVariation": "提升变着", "makeMainLine": "做为主线", @@ -468,25 +511,25 @@ "forceVariation": "强制作为变着", "copyVariationPgn": "复制变着的PGN", "move": "着法", - "variantLoss": "变体输了", + "variantLoss": "变体输棋", "variantWin": "变体胜利", "insufficientMaterial": "子力不足", "pawnMove": "走兵", "capture": "吃子", "close": "关闭", - "winning": "赢棋", + "winning": "胜棋", "losing": "输棋", "drawn": "和棋", - "unknown": "结局未知", + "unknown": "未知", "database": "数据库", "whiteDrawBlack": "白胜/和棋/黑胜", "averageRatingX": "平均等级分:{param}", - "recentGames": "最近对局", + "recentGames": "最近棋局", "topGames": "名局", - "masterDbExplanation": "{param2}-{param3}年国际棋联等级分{param1}以上棋手的两百万局棋谱", + "masterDbExplanation": "{param2}-{param3}年国际棋联等级分{param1}以上棋手的棋谱", "dtzWithRounding": "经过四舍五入的DTZ50'',是基于到下次吃子或兵动的半步数目。", - "noGameFound": "没找到符合要求的棋局", - "maxDepthReached": "已达最大深度!", + "noGameFound": "未找到棋局", + "maxDepthReached": "已达到最大深度!", "maybeIncludeMoreGamesFromThePreferencesMenu": "请尝试在“选择”菜单内包括更多棋局。", "openings": "开局", "openingExplorer": "开局浏览器", @@ -515,7 +558,6 @@ "memory": "内存", "infiniteAnalysis": "开启无限分析", "removesTheDepthLimit": "取消深度限制(会提升电脑温度)", - "engineManager": "引擎管理", "blunder": "漏着", "mistake": "错着", "inaccuracy": "失准", @@ -617,7 +659,7 @@ "gameAborted": "棋局已中止", "standard": "标准国际象棋", "customPosition": "自定义位置", - "unlimited": "无限时间", + "unlimited": "无限制", "mode": "模式", "casual": "休闲", "rated": "排位", @@ -800,7 +842,7 @@ "no": "否", "yes": "是", "website": "网站", - "mobile": "流动电话", + "mobile": "移动端", "help": "帮助:", "createANewTopic": "新话题", "topics": "话题", @@ -819,7 +861,9 @@ "cheat": "作弊", "troll": "捣乱", "other": "其他", - "reportDescriptionHelp": "请附上棋局链接解释该用户的行为问题。例如如果你怀疑某用户作弊,请不要只说 “对手作弊”。请解释为什么你认为对手作弊。如果你用英语举报,我们将会更快作出答复。", + "reportCheatBoostHelp": "请附上棋局链接解释该用户的行为问题。请不要只说 “对手作弊”,而是解释为什么你认为对手作弊。", + "reportUsernameHelp": "解释这个用户名为何具有冒犯性。不要只说“它具有冒犯性/不恰当”,而是要告诉我们你是如何得出这个结论的,特别是如果侮辱性内容是隐晦的、非英语的、俚语或有历史/文化参考。", + "reportProcessedFasterInEnglish": "如果您使用英语举报,我们将会更快作出答复。", "error_provideOneCheatedGameLink": "请提供至少一局作弊的棋局的链接。", "by": "来自{param}", "importedByX": "由 {param} 导入", @@ -1313,6 +1357,159 @@ "stormXRuns": "{count, plural, other{{count}组}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, other{玩了{count}组的{param2}}}", "streamerLichessStreamers": "Lichess 主播", + "studyPrivate": "私人", + "studyMyStudies": "我的研讨", + "studyStudiesIContributeTo": "我贡献的研讨", + "studyMyPublicStudies": "我的公开研讨", + "studyMyPrivateStudies": "我的私有研讨", + "studyMyFavoriteStudies": "我收藏的研讨", + "studyWhatAreStudies": "什么是研讨?", + "studyAllStudies": "所有研讨", + "studyStudiesCreatedByX": "由 {param} 创建的研讨", + "studyNoneYet": "暂无。", + "studyHot": "热门", + "studyDateAddedNewest": "添加时间 (最新)", + "studyDateAddedOldest": "添加时间 (最早)", + "studyRecentlyUpdated": "最近更新", + "studyMostPopular": "最受欢迎", + "studyAlphabetical": "按字母顺序", + "studyAddNewChapter": "添加一个新章节", + "studyAddMembers": "添加成员", + "studyInviteToTheStudy": "邀请参加研讨", + "studyPleaseOnlyInvitePeopleYouKnow": "请仅邀请你认识的并且积极希望参与这个研讨的成员", + "studySearchByUsername": "按用户名搜索", + "studySpectator": "旁观者", + "studyContributor": "贡献者", + "studyKick": "踢出", + "studyLeaveTheStudy": "离开研讨", + "studyYouAreNowAContributor": "你现在是一位贡献者", + "studyYouAreNowASpectator": "你现在是一位旁观者", + "studyPgnTags": "PGN 标签", + "studyLike": "赞", + "studyUnlike": "取消赞", + "studyNewTag": "新建标签", + "studyCommentThisPosition": "评论当前局面", + "studyCommentThisMove": "评论这步走法", + "studyAnnotateWithGlyphs": "用符号标注", + "studyTheChapterIsTooShortToBeAnalysed": "本章节太短,无法进行分析。", + "studyOnlyContributorsCanRequestAnalysis": "只有贡献者可以请求服务器分析。", + "studyGetAFullComputerAnalysis": "请求服务器完整地分析主线走法。", + "studyMakeSureTheChapterIsComplete": "请确保章节已完成。你只能请求分析一次。", + "studyAllSyncMembersRemainOnTheSamePosition": "SYNC 中所有成员处于相同局面", + "studyShareChanges": "与旁观者共享更改并云端保存", + "studyPlaying": "正在对局", + "studyShowEvalBar": "评估条", + "studyFirst": "首页", + "studyPrevious": "上一页", + "studyNext": "下一页", + "studyLast": "末页", "studyShareAndExport": "分享并导出", - "studyStart": "开始" + "studyCloneStudy": "复制棋局", + "studyStudyPgn": "研究 PGN", + "studyDownloadAllGames": "下载所有棋局", + "studyChapterPgn": "章节PGN", + "studyCopyChapterPgn": "复制PGN", + "studyDownloadGame": "下载棋局", + "studyStudyUrl": "研究链接", + "studyCurrentChapterUrl": "当前章节链接", + "studyYouCanPasteThisInTheForumToEmbed": "你可以将此粘贴到论坛以嵌入章节", + "studyStartAtInitialPosition": "从初始局面开始", + "studyStartAtX": "从 {param} 开始", + "studyEmbedInYourWebsite": "嵌入到你的网站上", + "studyReadMoreAboutEmbedding": "阅读更多关于嵌入的信息", + "studyOnlyPublicStudiesCanBeEmbedded": "只能嵌入隐私设置为公开的研究!", + "studyOpen": "打开", + "studyXBroughtToYouByY": "{param1} 由 {param2} 提供", + "studyStudyNotFound": "找不到研究", + "studyEditChapter": "编辑章节", + "studyNewChapter": "新章节", + "studyImportFromChapterX": "从 {param} 导入", + "studyOrientation": "视角", + "studyAnalysisMode": "分析模式", + "studyPinnedChapterComment": "置顶评论", + "studySaveChapter": "保存章节", + "studyClearAnnotations": "清除注释", + "studyClearVariations": "清除变着", + "studyDeleteChapter": "删除章节", + "studyDeleteThisChapter": "删除本章节?本操作无法撤销!", + "studyClearAllCommentsInThisChapter": "清除章节中所有信息?", + "studyRightUnderTheBoard": "正下方", + "studyNoPinnedComment": "不需要", + "studyNormalAnalysis": "普通模式", + "studyHideNextMoves": "隐藏下一步", + "studyInteractiveLesson": "互动课", + "studyChapterX": "章节 {param}", + "studyEmpty": "空白", + "studyStartFromInitialPosition": "从初始局面开始", + "studyEditor": "编辑器", + "studyStartFromCustomPosition": "从自定义局面开始", + "studyLoadAGameByUrl": "通过 URL 加载游戏", + "studyLoadAPositionFromFen": "从 FEN 加载一个局面", + "studyLoadAGameFromPgn": "从 PGN 文件加载游戏", + "studyAutomatic": "自动", + "studyUrlOfTheGame": "游戏的 URL", + "studyLoadAGameFromXOrY": "从 {param1} 或 {param2} 加载游戏", + "studyCreateChapter": "创建章节", + "studyCreateStudy": "创建课程", + "studyEditStudy": "编辑课程", + "studyVisibility": "权限", + "studyPublic": "公开", + "studyUnlisted": "未列出", + "studyInviteOnly": "仅限邀请", + "studyAllowCloning": "允许复制", + "studyNobody": "没人", + "studyOnlyMe": "仅自己", + "studyContributors": "贡献者", + "studyMembers": "成员", + "studyEveryone": "所有人", + "studyEnableSync": "允许同步", + "studyYesKeepEveryoneOnTheSamePosition": "确认:每个人都处于同样的局面", + "studyNoLetPeopleBrowseFreely": "取消:让玩家自由选择", + "studyPinnedStudyComment": "置顶评论", + "studyStart": "开始", + "studySave": "保存", + "studyClearChat": "清空对话", + "studyDeleteTheStudyChatHistory": "删除课程聊天记录?本操作无法撤销!", + "studyDeleteStudy": "删除课程", + "studyConfirmDeleteStudy": "确定删除整个研讨?该操作不可恢复,输入研讨名以确认:{param}", + "studyWhereDoYouWantToStudyThat": "你想从哪里开始此项研究?", + "studyGoodMove": "好棋", + "studyMistake": "错着", + "studyBrilliantMove": "极好", + "studyBlunder": "漏着", + "studyInterestingMove": "略好", + "studyDubiousMove": "略坏", + "studyOnlyMove": "唯一着法", + "studyZugzwang": "Zugzwang", + "studyEqualPosition": "均势", + "studyUnclearPosition": "局势不明", + "studyWhiteIsSlightlyBetter": "白方略优", + "studyBlackIsSlightlyBetter": "黑方略优", + "studyWhiteIsBetter": "白方占优", + "studyBlackIsBetter": "黑方占优", + "studyWhiteIsWinning": "白方即胜", + "studyBlackIsWinning": "黑方即胜", + "studyNovelty": "新奇的", + "studyDevelopment": "发展", + "studyInitiative": "占据主动", + "studyAttack": "攻击", + "studyCounterplay": "反击", + "studyTimeTrouble": "无暇多虑", + "studyWithCompensation": "优势补偿", + "studyWithTheIdea": "教科书式的", + "studyNextChapter": "下一章节", + "studyPrevChapter": "上一章节", + "studyStudyActions": "研讨操作", + "studyTopics": "主题", + "studyMyTopics": "我的主题", + "studyPopularTopics": "热门主题", + "studyManageTopics": "管理主题", + "studyBack": "回到起始", + "studyPlayAgain": "重玩", + "studyWhatWouldYouPlay": "你会在这个位置上怎么走?", + "studyYouCompletedThisLesson": "恭喜!你完成了这个课程!", + "studyNbChapters": "{count, plural, other{共 {count} 章}}", + "studyNbGames": "{count, plural, other{共 {count} 盘棋}}", + "studyNbMembers": "{count, plural, other{{count} 位成员}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, other{在此粘贴你的 PGN 文本,最多支持 {count} 个游戏}}" } \ No newline at end of file diff --git a/lib/l10n/lila_zh_TW.arb b/lib/l10n/lila_zh_TW.arb index 4ccbf61727..6f8ad37247 100644 --- a/lib/l10n/lila_zh_TW.arb +++ b/lib/l10n/lila_zh_TW.arb @@ -1,45 +1,134 @@ { - "mobileHomeTab": "主頁", + "mobileHomeTab": "首頁", "mobilePuzzlesTab": "謎題", "mobileToolsTab": "工具", - "mobileWatchTab": "觀看", - "mobileSettingsTab": "設置", + "mobileWatchTab": "觀戰", + "mobileSettingsTab": "設定", "mobileMustBeLoggedIn": "你必須登入才能查看此頁面。", - "mobileSystemColors": "系统颜色", + "mobileSystemColors": "系統顏色", "mobileFeedbackButton": "問題反饋", "mobileOkButton": "確認", "mobileSettingsHapticFeedback": "震動回饋", "mobileSettingsImmersiveMode": "沉浸模式", - "mobileAllGames": "所有遊戲", - "mobileRecentSearches": "最近搜尋", + "mobileSettingsImmersiveModeSubtitle": "在下棋和 Puzzle Storm 時隱藏系統界面。如果您受到螢幕邊緣的系統導航手勢干擾,可以使用此功能。", + "mobileNotFollowingAnyUser": "您未被任何使用者追蹤。", + "mobileAllGames": "所有棋局", + "mobileRecentSearches": "搜尋紀錄", "mobileClearButton": "清除", - "mobileNoSearchResults": "無結果", + "mobilePlayersMatchingSearchTerm": "名稱包含「{param}」的玩家", + "mobileNoSearchResults": "沒有任何搜尋結果", "mobileAreYouSure": "您確定嗎?", + "mobilePuzzleStreakAbortWarning": "這將失去目前的連勝並且將儲存目前成績。", + "mobilePuzzleStormNothingToShow": "沒有內容可顯示。您可以進行一些 Puzzle Storm 。", + "mobileSharePuzzle": "分享這個謎題", + "mobileShareGameURL": "分享對局網址", "mobileShareGamePGN": "分享 PGN", - "mobileCustomGameJoinAGame": "加入遊戲", + "mobileSharePositionAsFEN": "以 FEN 分享棋局位置", + "mobileShowVariations": "顯示變體", + "mobileHideVariation": "隱藏變體", + "mobileShowComments": "顯示留言", + "mobilePuzzleStormConfirmEndRun": "是否中斷於此?", + "mobilePuzzleStormFilterNothingToShow": "沒有內容可顯示,請更改篩選條件", + "mobileCancelTakebackOffer": "取消悔棋請求", + "mobileWaitingForOpponentToJoin": "正在等待對手加入...", + "mobileBlindfoldMode": "盲棋", + "mobileLiveStreamers": "Lichess 實況主", + "mobileCustomGameJoinAGame": "加入棋局", + "mobileCorrespondenceClearSavedMove": "清除已儲存移動", + "mobileSomethingWentWrong": "發生了一些問題。", + "mobileShowResult": "顯示結果", + "mobilePuzzleThemesSubtitle": "從您喜歡的開局進行謎題,或選擇一個主題。", + "mobilePuzzleStormSubtitle": "在三分鐘內解開盡可能多的謎題", + "mobileGreeting": "您好, {param}", + "mobileGreetingWithoutName": "您好", + "mobilePrefMagnifyDraggedPiece": "放大被拖曳的棋子", "activityActivity": "活動", "activityHostedALiveStream": "主持一個現場直播", - "activityRankedInSwissTournament": "在{param2}中排名{param1}", - "activitySignedUp": "在lichess.org中註冊", - "activitySupportedNbMonths": "{count, plural, other{以{param2}的身分支持lichess.org{count}個月}}", - "activityPracticedNbPositions": "{count, plural, other{在{param2}練習了{count}個棋局}}", - "activitySolvedNbPuzzles": "{count, plural, other{解決了{count}個戰術題目}}", - "activityPlayedNbGames": "{count, plural, other{下了{count}場{param2}類型的棋局}}", - "activityPostedNbMessages": "{count, plural, other{在{param2}發表了{count}則訊息}}", - "activityPlayedNbMoves": "{count, plural, other{下了{count}步}}", - "activityInNbCorrespondenceGames": "{count, plural, other{在{count}場長時間棋局中}}", - "activityCompletedNbGames": "{count, plural, other{完成了{count}場長時間棋局}}", - "activityFollowedNbPlayers": "{count, plural, other{開始關注{count}個玩家}}", - "activityGainedNbFollowers": "{count, plural, other{增加了{count}個追蹤者}}", + "activityRankedInSwissTournament": "在{param2}中排名 {param1}", + "activitySignedUp": "在 lichess.org 中註冊", + "activitySupportedNbMonths": "{count, plural, other{以 {param2} 身分贊助 lichess.org {count} 個月}}", + "activityPracticedNbPositions": "{count, plural, other{在 {param2} 練習了 {count} 個棋局}}", + "activitySolvedNbPuzzles": "{count, plural, other{解決了 {count} 個戰術題目}}", + "activityPlayedNbGames": "{count, plural, other{下了 {count} 場{param2}類型的棋局}}", + "activityPostedNbMessages": "{count, plural, other{在「{param2}」發表了 {count} 則訊息}}", + "activityPlayedNbMoves": "{count, plural, other{下了 {count} 步}}", + "activityInNbCorrespondenceGames": "{count, plural, other{在 {count} 場通信棋局中}}", + "activityCompletedNbGames": "{count, plural, other{完成了 {count} 場通信棋局}}", + "activityCompletedNbVariantGames": "{count, plural, other{完成了 {count} {param2} 場通信棋局}}", + "activityFollowedNbPlayers": "{count, plural, other{開始關注 {count} 個玩家}}", + "activityGainedNbFollowers": "{count, plural, other{增加了 {count} 個追蹤者}}", "activityHostedNbSimuls": "{count, plural, other{主持了{count}場車輪戰}}", "activityJoinedNbSimuls": "{count, plural, other{加入了{count}場車輪戰}}", "activityCreatedNbStudies": "{count, plural, other{創造了{count}個新的研究}}", "activityCompetedInNbTournaments": "{count, plural, other{完成了{count}場錦標賽}}", "activityRankedInTournament": "{count, plural, other{在{param4}錦標賽中下了{param3}盤棋局,排名第{count}(前{param2}%)}}", - "activityCompetedInNbSwissTournaments": "{count, plural, other{參與過{count}'場瑞士制錦標賽}}", - "activityJoinedNbTeams": "{count, plural, other{加入{count}團隊}}", + "activityCompetedInNbSwissTournaments": "{count, plural, other{參與過 {count} 場瑞士制錦標賽}}", + "activityJoinedNbTeams": "{count, plural, other{加入 {count} 團隊}}", "broadcastBroadcasts": "比賽直播", + "broadcastMyBroadcasts": "我的直播", "broadcastLiveBroadcasts": "錦標賽直播", + "broadcastBroadcastCalendar": "直播時程表", + "broadcastNewBroadcast": "新的現場直播", + "broadcastSubscribedBroadcasts": "已訂閱的直播", + "broadcastAboutBroadcasts": "關於直播", + "broadcastHowToUseLichessBroadcasts": "如何使用 Lichess 比賽直播", + "broadcastTheNewRoundHelp": "新的一局會有跟上一局相同的成員與貢獻者", + "broadcastAddRound": "新增回合", + "broadcastOngoing": "進行中", + "broadcastUpcoming": "即將舉行", + "broadcastCompleted": "已結束", + "broadcastCompletedHelp": "Lichess 偵測棋局的結束,但有可能會偵測錯誤。請在這自行設定。", + "broadcastRoundName": "回合名稱", + "broadcastRoundNumber": "回合數", + "broadcastTournamentName": "錦標賽名稱", + "broadcastTournamentDescription": "簡短比賽說明", + "broadcastFullDescription": "完整比賽說明", + "broadcastFullDescriptionHelp": "直播內容的詳細描述 。可以利用 {param1}。字數限於{param2}個字。", + "broadcastSourceSingleUrl": "PGN 來源網址", + "broadcastSourceUrlHelp": "Lichess 將以該網址更新PGN數據,網址必須公開", + "broadcastSourceGameIds": "最多 64 個以空格分開的 Lichess 棋局序號。", + "broadcastStartDateTimeZone": "當地時區的錦標賽起始日期:{param}", + "broadcastStartDateHelp": "可選,如果知道比賽開始時間", + "broadcastCurrentGameUrl": "目前棋局連結", + "broadcastDownloadAllRounds": "下載所有棋局", + "broadcastResetRound": "重設此回合", + "broadcastDeleteRound": "刪除此回合", + "broadcastDefinitivelyDeleteRound": "刪除這局以及其所有棋局", + "broadcastDeleteAllGamesOfThisRound": "刪除所有此輪的棋局。直播來源必須是開啟的以成功重新建立棋局。", + "broadcastEditRoundStudy": "編輯此輪研究", + "broadcastDeleteTournament": "刪除此錦標賽", + "broadcastDefinitivelyDeleteTournament": "刪除錦標賽以及所有棋局", + "broadcastShowScores": "根據比賽結果顯示玩家分數", + "broadcastReplacePlayerTags": "取代玩家名字、評級、以及頭銜(選填)", + "broadcastFideFederations": "FIDE 國別", + "broadcastTop10Rating": "前 10 名平均評級", + "broadcastFidePlayers": "FIDE 玩家", + "broadcastFidePlayerNotFound": "找不到 FIDE 玩家", + "broadcastFideProfile": "FIDE 序號", + "broadcastFederation": "國籍", + "broadcastAgeThisYear": "年齡", + "broadcastUnrated": "未評級", + "broadcastRecentTournaments": "最近錦標賽", + "broadcastOpenLichess": "在 lichess 中開啟", + "broadcastTeams": "團隊", + "broadcastBoards": "棋局", + "broadcastOverview": "概覽", + "broadcastSubscribeTitle": "訂閱以在每輪開始時獲得通知。您可以在帳戶設定中切換直播的鈴聲或推播通知。", + "broadcastUploadImage": "上傳錦標賽圖片", + "broadcastNoBoardsYet": "尚無棋局。這些棋局將在對局上傳後顯示。", + "broadcastStartVerySoon": "直播即將開始。", + "broadcastNotYetStarted": "直播尚未開始。", + "broadcastOfficialWebsite": "官網", + "broadcastStandings": "排行榜", + "broadcastIframeHelp": "更多選項在{param}", + "broadcastWebmastersPage": "網頁管理員頁面", + "broadcastPgnSourceHelp": "這一輪的公開實時 PGN。我們還提供{param}以實現更快和更高效的同步。", + "broadcastEmbedThisBroadcast": "將此直播嵌入您的網站", + "broadcastEmbedThisRound": "將{param}嵌入您的網站", + "broadcastRatingDiff": "評級差異", + "broadcastGamesThisTournament": "此比賽的對局", + "broadcastScore": "分數", + "broadcastNbBroadcasts": "{count, plural, other{{count} 個直播}}", "challengeChallengesX": "挑戰: {param1}", "challengeChallengeToPlay": "邀請對弈", "challengeChallengeDeclined": "對弈邀請已拒絕", @@ -96,13 +185,13 @@ "perfStatLessThanOneHour": "兩場間距不到一小時", "perfStatMaxTimePlaying": "最高奕棋時間", "perfStatNow": "現在", - "preferencesPreferences": "偏好設置", + "preferencesPreferences": "偏好設定", "preferencesDisplay": "顯示", "preferencesPrivacy": "隱私", "preferencesNotifications": "通知", "preferencesPieceAnimation": "棋子動畫", "preferencesMaterialDifference": "子力差距", - "preferencesBoardHighlights": "棋盤高亮 (最後一步與將軍)", + "preferencesBoardHighlights": "國王紅色亮光(最後一步與將軍)", "preferencesPieceDestinations": "棋子目的地(有效走法與預先走棋)", "preferencesBoardCoordinates": "棋盤座標(A-H, 1-8)", "preferencesMoveListWhilePlaying": "遊戲進行時顯示棋譜", @@ -111,6 +200,7 @@ "preferencesPgnLetter": "字母 (K, Q, R, B, N)", "preferencesZenMode": "專注模式", "preferencesShowPlayerRatings": "顯示玩家等級分", + "preferencesShowFlairs": "顯示玩家身分", "preferencesExplainShowPlayerRatings": "這允許隱藏本網站上的所有等級分,以輔助專心下棋。每局遊戲仍可以計算及改變等級分,這個設定只會影響到你是否看得到此分數。", "preferencesDisplayBoardResizeHandle": "顯示盤面大小調整區塊", "preferencesOnlyOnInitialPosition": "只在起始局面", @@ -175,18 +265,26 @@ "puzzleSpecialMoves": "特殊移動", "puzzleDidYouLikeThisPuzzle": "您喜歡這道謎題嗎?", "puzzleVoteToLoadNextOne": "告訴我們加載下一題!", + "puzzleUpVote": "投票為好謎題", + "puzzleDownVote": "投票為壞謎題", "puzzleYourPuzzleRatingWillNotChange": "您的謎題評級不會改變。請注意,謎題不是比賽。您的評分有助於選擇最適合您當前技能的謎題。", "puzzleFindTheBestMoveForWhite": "為白方找出最佳移動", "puzzleFindTheBestMoveForBlack": "為黑方找出最佳移動", "puzzleToGetPersonalizedPuzzles": "得到個人推薦題目:", "puzzlePuzzleId": "謎題 {param}", "puzzlePuzzleOfTheDay": "每日一題", + "puzzleDailyPuzzle": "每日謎題", "puzzleClickToSolve": "點擊解題", "puzzleGoodMove": "好棋", "puzzleBestMove": "最佳走法!", "puzzleKeepGoing": "加油!", "puzzlePuzzleSuccess": "成功!", "puzzlePuzzleComplete": "解題完成!", + "puzzleByOpenings": "以開局區分", + "puzzlePuzzlesByOpenings": "以開局區分謎題", + "puzzleOpeningsYouPlayedTheMost": "您最常使用的開局", + "puzzleUseFindInPage": "在瀏覽器中使用「在頁面中尋找」以尋找你最喜歡的開局!", + "puzzleUseCtrlF": "按下 Ctrl+f 以找出您最喜歡的開局方式!", "puzzleNotTheMove": "不是這步!", "puzzleTrySomethingElse": "試試其他的移動", "puzzleRatingX": "評級:{param}", @@ -201,6 +299,7 @@ "puzzleHardest": "超困難", "puzzleExample": "範例", "puzzleAddAnotherTheme": "加入其他主題", + "puzzleNextPuzzle": "下個謎題", "puzzleJumpToNextPuzzleImmediately": "立即跳到下一個謎題", "puzzlePuzzleDashboard": "謎題能力分析", "puzzleImprovementAreas": "弱點", @@ -217,7 +316,7 @@ "puzzleLookupOfPlayer": "尋找其他棋手的棋局謎題", "puzzleFromXGames": "來自{param}棋局的謎題", "puzzleSearchPuzzles": "尋找謎題", - "puzzleFromMyGamesNone": "你在數據庫中沒有謎題,但 Lichess 仍然非常愛你。\n遊玩一些快速和經典遊戲,以增加添加拼圖的機會!", + "puzzleFromMyGamesNone": "你在資料庫中沒有謎題,但 Lichess 仍然非常愛你。\n遊玩一些快速和經典遊戲,以增加從你的棋局中生成謎題的機會!", "puzzleFromXGamesFound": "在{param2}中找到{param1}個謎題", "puzzlePuzzleDashboardDescription": "訓練、分析、改進", "puzzlePercentSolved": "{param} 已解決", @@ -231,39 +330,71 @@ "puzzleNbToReplay": "{count, plural, other{{count} 重玩}}", "puzzleThemeAdvancedPawn": "升變兵", "puzzleThemeAdvancedPawnDescription": "你的其中一個兵已經深入了對方的棋位,或許要威脅升變。", - "puzzleThemeAdvantage": "擁有優勢", + "puzzleThemeAdvantage": "取得優勢", + "puzzleThemeAdvantageDescription": "把握機會以取得決定性優勢。(200 厘兵 ≤ 評估值 ≤ 600 厘兵)", "puzzleThemeAnastasiaMate": "阿納斯塔西亞殺法", + "puzzleThemeAnastasiaMateDescription": "馬與車或后聯手在棋盤邊困住對手國王以及另一對手棋子", "puzzleThemeArabianMate": "阿拉伯殺法", "puzzleThemeArabianMateDescription": "馬和車聯手把對方的王困住在角落的位置", - "puzzleThemeAttackingF2F7": "攻擊f2或f7", + "puzzleThemeAttackingF2F7": "攻擊 f2 或 f7", + "puzzleThemeAttackingF2F7Description": "專注於 f2 或 f7 兵的攻擊,像是 fried liver 攻擊", "puzzleThemeAttraction": "吸引", + "puzzleThemeAttractionDescription": "一種換子或犧牲強迫對手旗子到某格以好進行接下來的戰術。", "puzzleThemeBackRankMate": "後排將死", - "puzzleThemeBackRankMateDescription": "在對方的王在底線被自身的棋子困住時,將殺對方的王", - "puzzleThemeBishopEndgame": "象殘局", + "puzzleThemeBackRankMateDescription": "在對方的王於底線被自身的棋子困住時,將死對方的王", + "puzzleThemeBishopEndgame": "主教殘局", "puzzleThemeBishopEndgameDescription": "只剩象和兵的殘局", "puzzleThemeBodenMate": "波登殺法", + "puzzleThemeBodenMateDescription": "以在對角線上的兩個主教將死被自身棋子困住的王。", "puzzleThemeCastling": "易位", + "puzzleThemeCastlingDescription": "讓國王回到安全,並讓車發動攻擊。", "puzzleThemeCapturingDefender": "吃子 - 防守者", + "puzzleThemeCapturingDefenderDescription": "移除防守其他棋子的防守者以攻擊未被保護的棋子", "puzzleThemeCrushing": "壓倒性優勢", "puzzleThemeCrushingDescription": "察覺對方的漏著並藉此取得巨大優勢。(大於600百分兵)", "puzzleThemeDoubleBishopMate": "雙主教將死", + "puzzleThemeDoubleBishopMateDescription": "相鄰對角線上的兩個主教將將死被自身棋子困住的王。", + "puzzleThemeDovetailMate": "柯齊奧將死", + "puzzleThemeDovetailMateDescription": "以皇后將死被自身棋子困住的國王", "puzzleThemeEquality": "均勢", + "puzzleThemeEqualityDescription": "從劣勢中反敗為和。(分析值 ≤ 200厘兵)", "puzzleThemeKingsideAttack": "王翼攻擊", + "puzzleThemeKingsideAttackDescription": "在對方於王翼易位後的攻擊。", "puzzleThemeClearance": "騰挪", + "puzzleThemeClearanceDescription": "為了施展戰術而清除我方攻擊格上的障礙物。", "puzzleThemeDefensiveMove": "加強防守", + "puzzleThemeDefensiveMoveDescription": "一種為了避免遺失棋子或優勢而採取的必要行動。", "puzzleThemeDeflection": "引離", + "puzzleThemeDeflectionDescription": "為了分散敵人專注力所採取的戰術,容易搗亂敵人原本的計畫。", "puzzleThemeDiscoveredAttack": "閃擊", + "puzzleThemeDiscoveredAttackDescription": "將一子(例如騎士)移開長程攻擊格(例如城堡)。", "puzzleThemeDoubleCheck": "雙將", + "puzzleThemeDoubleCheckDescription": "雙重將軍,讓我方能攻擊敵人的他子。", "puzzleThemeEndgame": "殘局", "puzzleThemeEndgameDescription": "棋局中最後階段的戰術", + "puzzleThemeEnPassantDescription": "一種食敵方過路兵的戰略。", + "puzzleThemeExposedKing": "未被保護的國王", + "puzzleThemeExposedKingDescription": "攻擊未被保護的國王之戰術,常常導致將死。", "puzzleThemeFork": "捉雙", + "puzzleThemeForkDescription": "一種同時攻擊敵方多個子,使敵方只能犧牲一子的戰術。", + "puzzleThemeHangingPiece": "懸子", + "puzzleThemeHangingPieceDescription": "「免費」取得他子的戰術", + "puzzleThemeHookMate": "鉤將死", + "puzzleThemeHookMateDescription": "利用車馬兵與一敵方兵以限制敵方國王的逃生路線。", + "puzzleThemeInterference": "干擾", + "puzzleThemeInterferenceDescription": "將一子擋在兩個敵方子之間以切斷防護,例如以騎士在兩車之間阻擋。", + "puzzleThemeIntermezzo": "Intermezzo", + "puzzleThemeIntermezzoDescription": "與其走正常的棋譜,不如威脅敵方子吧!這樣不但可以破壞敵方原先計畫,還可以讓敵人必須對威脅採取對應的動作。這種戰術又稱為「Zwischenzug」或「In between」。", "puzzleThemeKnightEndgame": "馬殘局", "puzzleThemeKnightEndgameDescription": "只剩馬和兵的殘局", "puzzleThemeLong": "長謎題", "puzzleThemeLongDescription": "三步獲勝", "puzzleThemeMaster": "大師棋局", + "puzzleThemeMasterDescription": "從頭銜玩家的棋局中生成的謎題。", "puzzleThemeMasterVsMaster": "大師對局", + "puzzleThemeMasterVsMasterDescription": "從兩位頭銜玩家的棋局中生成的謎題。", "puzzleThemeMate": "將軍", + "puzzleThemeMateDescription": "以你的技能贏得勝利", "puzzleThemeMateIn1": "一步殺棋", "puzzleThemeMateIn1Description": "一步將軍", "puzzleThemeMateIn2": "兩步殺棋", @@ -273,6 +404,7 @@ "puzzleThemeMateIn4": "四步殺棋", "puzzleThemeMateIn4Description": "走四步以達到將軍", "puzzleThemeMateIn5": "五步或更高 將軍", + "puzzleThemeMateIn5Description": "看出較長的將死步驟。", "puzzleThemeMiddlegame": "中局", "puzzleThemeMiddlegameDescription": "棋局中第二階段的戰術", "puzzleThemeOneMove": "一步題", @@ -282,46 +414,61 @@ "puzzleThemePawnEndgame": "兵殘局", "puzzleThemePawnEndgameDescription": "只剩兵的殘局", "puzzleThemePin": "牽制", + "puzzleThemePinDescription": "一種涉及「牽制」,讓一敵方子無法在讓其他更高價值的子不被受到攻擊下移動的戰術。", "puzzleThemePromotion": "升變", + "puzzleThemePromotionDescription": "讓兵走到後排升變為皇后或其他高價值的子。", "puzzleThemeQueenEndgame": "后殘局", "puzzleThemeQueenEndgameDescription": "只剩后和兵的殘局", "puzzleThemeQueenRookEndgame": "后與車", "puzzleThemeQueenRookEndgameDescription": "只剩后、車和兵的殘局", "puzzleThemeQueensideAttack": "后翼攻擊", + "puzzleThemeQueensideAttackDescription": "在對方於后翼易位後的攻擊。", "puzzleThemeQuietMove": "安靜的一着", + "puzzleThemeQuietMoveDescription": "隱藏在未來敵方無法避免的攻擊。", "puzzleThemeRookEndgame": "車殘局", "puzzleThemeRookEndgameDescription": "只剩車和兵的殘局", "puzzleThemeSacrifice": "棄子", + "puzzleThemeSacrificeDescription": "犧牲我方子以在一系列的移動後得到優勢。", "puzzleThemeShort": "短謎題", "puzzleThemeShortDescription": "兩步獲勝", "puzzleThemeSkewer": "串擊", + "puzzleThemeSkewerDescription": "攻擊敵方高價值的子以讓敵方移開,以攻擊背後較為低價值未受保護的他子。為一種反向的「牽制」。", "puzzleThemeSmotheredMate": "悶殺", + "puzzleThemeSmotheredMateDescription": "一種以馬將死被自身棋子所圍困的國王。", "puzzleThemeSuperGM": "超級大師賽局", "puzzleThemeSuperGMDescription": "來自世界各地優秀玩家對局的戰術題", "puzzleThemeTrappedPiece": "被困的棋子", + "puzzleThemeTrappedPieceDescription": "一子因為被限制逃生路線而無法逃離被犧牲的命運。", "puzzleThemeUnderPromotion": "升變", "puzzleThemeUnderPromotionDescription": "升變成騎士、象或車", "puzzleThemeVeryLong": "非常長的謎題", "puzzleThemeVeryLongDescription": "四步或以上獲勝", "puzzleThemeXRayAttack": "穿透攻擊", + "puzzleThemeXRayAttackDescription": "以敵方子攻擊或防守的戰術。", "puzzleThemeZugzwang": "等著", - "puzzleThemeHealthyMix": "綜合", - "puzzleThemeHealthyMixDescription": "所有類型都有!你不知道會遇到什麼題型,所以請做好準備,就像在實戰一樣。", + "puzzleThemeZugzwangDescription": "對方的棋子因為所移動的空間有限所以所到之處都會增加對方劣勢", + "puzzleThemeMix": "綜合", + "puzzleThemeMixDescription": "所有類型都有!你不知道會遇到什麼題型,所以請做好準備,就像在實戰一樣。", + "puzzleThemePlayerGames": "玩家謎題", + "puzzleThemePlayerGamesDescription": "查詢從你或其他玩家的對奕所生成的謎題。", + "puzzleThemePuzzleDownloadInformation": "這些為公開謎題,並且在 {param} 提供下載管道。", "searchSearch": "搜尋", "settingsSettings": "設定", "settingsCloseAccount": "關閉帳戶", - "settingsClosingIsDefinitive": "您確定要刪除帳號嗎?這是不能挽回的", + "settingsManagedAccountCannotBeClosed": "您的帳號已被管理並且無法關閉。", + "settingsClosingIsDefinitive": "您確定要刪除帳號嗎?這是無法挽回的。", "settingsCantOpenSimilarAccount": "即使名稱大小寫不同,您也不能使用相同的名稱開設新帳戶", "settingsChangedMindDoNotCloseAccount": "我改變主意了,不要關閉我的帳號", - "settingsCloseAccountExplanation": "您真的確定要刪除帳戶嗎? 關閉帳戶是永久性的決定, 您將「永遠無法」再次登錄。", + "settingsCloseAccountExplanation": "您真的確定要刪除帳戶嗎? 關閉帳戶是永久性的決定, 您將「永遠無法」再次登入。", "settingsThisAccountIsClosed": "此帳號已被關閉。", "playWithAFriend": "和好友下棋", "playWithTheMachine": "和電腦下棋", - "toInviteSomeoneToPlayGiveThisUrl": "邀人下棋,請分享這個網址", + "toInviteSomeoneToPlayGiveThisUrl": "請分享此網址以邀人下棋", "gameOver": "遊戲結束", "waitingForOpponent": "等待對手", - "waiting": "請稍等", - "yourTurn": "該你走", + "orLetYourOpponentScanQrCode": "或是讓對手掃描這個 QR code", + "waiting": "等待對手確認中", + "yourTurn": "該您走", "aiNameLevelAiLevel": "{param1}等級 {param2}", "level": "難度", "strength": "強度", @@ -330,24 +477,24 @@ "resign": "認輸", "checkmate": "將死", "stalemate": "逼和", - "white": "白方", - "black": "黑方", - "asWhite": "使用白棋", - "asBlack": "使用黑棋", + "white": "執白", + "black": "執黑", + "asWhite": "作為白方", + "asBlack": "作為黑方", "randomColor": "隨機選色", "createAGame": "開始對局", "whiteIsVictorious": "白方勝", "blackIsVictorious": "黑方勝", "youPlayTheWhitePieces": "您執白棋", "youPlayTheBlackPieces": "您執黑棋", - "itsYourTurn": "輪到你了!", + "itsYourTurn": "該您走!", "cheatDetected": "偵測到作弊行為", "kingInTheCenter": "王居中", "threeChecks": "三次將軍", - "raceFinished": "競王結束", - "variantEnding": "另類終局", + "raceFinished": "王至第八排", + "variantEnding": "變體終局", "newOpponent": "換個對手", - "yourOpponentWantsToPlayANewGameWithYou": "你的對手想和你複賽", + "yourOpponentWantsToPlayANewGameWithYou": "您的對手想和你複賽", "joinTheGame": "加入這盤棋", "whitePlays": "白方走棋", "blackPlays": "黑方走棋", @@ -358,19 +505,19 @@ "theFirstPersonToComeOnThisUrlWillPlayWithYou": "第一個訪問該網址的人將與您下棋。", "whiteResigned": "白方認輸", "blackResigned": "黑方認輸", - "whiteLeftTheGame": "白方棄局", - "blackLeftTheGame": "黑方棄局", + "whiteLeftTheGame": "白方棄賽", + "blackLeftTheGame": "黑方棄賽", "whiteDidntMove": "白方沒有走棋", "blackDidntMove": "黑方沒有走棋", "requestAComputerAnalysis": "請求電腦分析", "computerAnalysis": "電腦分析", "computerAnalysisAvailable": "電腦分析可用", - "computerAnalysisDisabled": "電腦分析未啟用", + "computerAnalysisDisabled": "未啟用電腦分析", "analysis": "分析棋局", "depthX": "深度 {param}", "usingServerAnalysis": "正在使用伺服器分析", "loadingEngine": "正在載入引擎 ...", - "calculatingMoves": "計算著法中。。。", + "calculatingMoves": "計算著法中...", "engineFailed": "加載引擎出錯", "cloudAnalysis": "雲端分析", "goDeeper": "深入分析", @@ -380,6 +527,8 @@ "promoteVariation": "增加變化", "makeMainLine": "將這步棋導入主要流程中", "deleteFromHere": "從這處開始刪除", + "collapseVariations": "隱藏變體", + "expandVariations": "顯示變體", "forceVariation": "移除變化", "copyVariationPgn": "複製變體 PGN", "move": "走棋", @@ -402,7 +551,7 @@ "dtzWithRounding": "經過四捨五入的DTZ50'',是基於到下次吃子或兵動的半步數目。", "noGameFound": "未找到遊戲", "maxDepthReached": "已達到最大深度!", - "maybeIncludeMoreGamesFromThePreferencesMenu": "試著從偏好設置中加入更多棋局", + "maybeIncludeMoreGamesFromThePreferencesMenu": "試著從設定中加入更多棋局", "openings": "開局", "openingExplorer": "開局瀏覽器", "openingEndgameExplorer": "開局與終局瀏覽器", @@ -419,19 +568,18 @@ "deleteThisImportedGame": "刪除此匯入的棋局?", "replayMode": "重播模式", "realtimeReplay": "實時", - "byCPL": "CPL", - "openStudy": "打開研究視窗", - "enable": "開啟", + "byCPL": "以厘兵損失", + "openStudy": "開啟研究", + "enable": "啟用", "bestMoveArrow": "最佳移動的箭頭", "showVariationArrows": "顯示變體箭頭", - "evaluationGauge": "棋力估計表", + "evaluationGauge": "評估條", "multipleLines": "路線分析線", - "cpus": "CPU", + "cpus": "CPU 數量", "memory": "記憶體", "infiniteAnalysis": "無限分析", - "removesTheDepthLimit": "取消深度限制,使您的電腦發熱。", - "engineManager": "引擎管理", - "blunder": "嚴重錯誤", + "removesTheDepthLimit": "取消深度限制,可能會使您的電腦發熱。", + "blunder": "漏著", "mistake": "錯誤", "inaccuracy": "輕微失誤", "moveTimes": "走棋時間", @@ -456,6 +604,7 @@ "latestForumPosts": "最新論壇貼文", "players": "棋手", "friends": "朋友", + "otherPlayers": "其他玩家", "discussions": "對話", "today": "今天", "yesterday": "昨天", @@ -470,47 +619,47 @@ "time": "時間", "rating": "評級", "ratingStats": "評分數據", - "username": "用戶名", - "usernameOrEmail": "用戶名或電郵地址", - "changeUsername": "更改用戶名", + "username": "使用者名稱", + "usernameOrEmail": "使用者名稱或電郵地址", + "changeUsername": "更改使用者名稱", "changeUsernameNotSame": "只能更改字母大小字。例如,將「johndoe」變成「JohnDoe」。", - "changeUsernameDescription": "更改用戶名。您最多可以更改一次字母大小寫。", - "signupUsernameHint": "請選擇一個和諧的用戶名,用戶名無法再次更改,並且不合規的用戶名會導致帳戶被封禁!", + "changeUsernameDescription": "更改使用者名稱。您最多可以更改一次字母大小寫。", + "signupUsernameHint": "請選擇一個妥當的使用者名稱。請注意使用者名稱無法再次更改,並且不妥當的名稱會導致帳號被封禁!", "signupEmailHint": "僅用於密碼重置", "password": "密碼", "changePassword": "更改密碼", - "changeEmail": "更改電郵地址", - "email": "電郵地址", + "changeEmail": "更改電子郵件", + "email": "電子郵件", "passwordReset": "重置密碼", "forgotPassword": "忘記密碼?", "error_weakPassword": "此密碼太常見,且很容易被猜到。", - "error_namePassword": "請不要把密碼設為用戶名。", + "error_namePassword": "請不要把密碼設為使用者名稱。", "blankedPassword": "你在其他站點使用過相同的密碼,並且這些站點已經失效。為確保你的 Lichess 帳戶安全,你需要設置新密碼。感謝你的理解。", - "youAreLeavingLichess": "你正在離開 Lichess", + "youAreLeavingLichess": "你正要離開 Lichess", "neverTypeYourPassword": "不要在其他網站輸入你的 Lichess 密碼!", "proceedToX": "前往 {param}", "passwordSuggestion": "不要使用他人建議的密碼,他們會用此密碼盜取你的帳戶。", - "emailSuggestion": "不要使用他人提供的郵箱地址,他們會用它盜取你的帳戶。", - "emailConfirmHelp": "協助郵件確認", + "emailSuggestion": "不要使用他人提供的電子郵件,他們會用它盜取您的帳號。", + "emailConfirmHelp": "協助電郵確認", "emailConfirmNotReceived": "註冊後沒有收到確認郵件?", - "whatSignupUsername": "你用了什麼用戶名註冊?", - "usernameNotFound": "找不到用戶 {param}。", + "whatSignupUsername": "你用了什麼使用者名稱註冊?", + "usernameNotFound": "找不到使用者名稱 {param}。", "usernameCanBeUsedForNewAccount": "你可以使用這個用戶名創建帳戶", "emailSent": "我們向 {param} 發送了電子郵件。", "emailCanTakeSomeTime": "可能需要一些時間才能收到。", "refreshInboxAfterFiveMinutes": "等待5分鐘並刷新你的收件箱。", "checkSpamFolder": "嘗試檢查你的垃圾郵件收件匣,它可能在那裡。 如果在,請將其標記為非垃圾郵件。", "emailForSignupHelp": "如果其他所有的方法都失敗了,給我們發這條短信:", - "copyTextToEmail": "複製並粘貼上面的文本然後把它發給{param}", - "waitForSignupHelp": "我們很快就會給你回復,説明你完成註冊。", - "accountConfirmed": "這個使用者 {param} 成功地確認了", + "copyTextToEmail": "複製並貼上上面的文字然後把它發給{param}", + "waitForSignupHelp": "我們很快就會給你回覆,説明你完成註冊。", + "accountConfirmed": "使用者 {param} 認證成功", "accountCanLogin": "你可以做為 {param} 登入了。", "accountConfirmationEmailNotNeeded": "你不需要確認電子郵件。", "accountClosed": "帳戶 {param} 被關閉。", "accountRegisteredWithoutEmail": "帳戶 {param} 未使用電子郵箱註冊。", "rank": "排名", "rankX": "排名:{param}", - "gamesPlayed": "盤棋已結束", + "gamesPlayed": "下過局數", "cancel": "取消", "whiteTimeOut": "白方時間到", "blackTimeOut": "黑方時間到", @@ -530,6 +679,7 @@ "abortGame": "中止本局", "gameAborted": "棋局已中止", "standard": "標準", + "customPosition": "自定義局面", "unlimited": "無限", "mode": "模式", "casual": "休閒", @@ -549,13 +699,13 @@ "inbox": "收件箱", "chatRoom": "聊天室", "loginToChat": "登入以聊天", - "youHaveBeenTimedOut": "由於時間原因您不能發言", + "youHaveBeenTimedOut": "您已被禁言", "spectatorRoom": "觀眾室", "composeMessage": "寫信息", "subject": "主題", "send": "發送", "incrementInSeconds": "增加秒數", - "freeOnlineChess": "免費線上國際象棋", + "freeOnlineChess": "免費線上西洋棋", "exportGames": "導出棋局", "ratingRange": "對方級別範圍", "thisAccountViolatedTos": "此帳號違反了Lichess的使用規定", @@ -564,7 +714,7 @@ "proposeATakeback": "請求悔棋", "takebackPropositionSent": "悔棋請求已發送", "takebackPropositionDeclined": "悔棋請求被拒絕", - "takebackPropositionAccepted": "同意悔棋", + "takebackPropositionAccepted": "悔棋請求被接受", "takebackPropositionCanceled": "悔棋請求已取消", "yourOpponentProposesATakeback": "對手請求悔棋", "bookmarkThisGame": "收藏該棋局", @@ -573,7 +723,7 @@ "tournamentPoints": "錦標賽得分", "viewTournament": "觀看錦標賽", "backToTournament": "返回錦標賽主頁", - "noDrawBeforeSwissLimit": "在瑞士錦標賽中,在下三十步棋前你不能提和.", + "noDrawBeforeSwissLimit": "在積分循環制錦標賽中,在下三十步棋前無法和局。", "thematic": "特殊開局", "yourPerfRatingIsProvisional": "您目前的評分{param}為臨時評分", "yourPerfRatingIsTooHigh": "您的 {param1} 積分 ({param2}) 過高", @@ -598,16 +748,17 @@ "leaderboard": "排行榜", "screenshotCurrentPosition": "截圖當前頁面", "gameAsGIF": "保存棋局為 GIF", - "pasteTheFenStringHere": "在此處黏貼FEN棋譜", - "pasteThePgnStringHere": "在此處黏貼PGN棋譜", + "pasteTheFenStringHere": "在此處貼上 FEN 棋譜", + "pasteThePgnStringHere": "在此處貼上 PGN 棋譜", "orUploadPgnFile": "或者上傳一個PGN文件", "fromPosition": "自定義局面", - "continueFromHere": "从此處繼續", + "continueFromHere": "從此處繼續", "toStudy": "研究", "importGame": "導入棋局", "importGameExplanation": "貼上PGN棋譜後可以重播棋局,使用電腦分析、對局聊天室及取得此棋局的分享連結。", - "importGameCaveat": "變著分支將被刪除。 若要保存這些變著,請通過導入PGN棋譜創建一個研究。", - "thisIsAChessCaptcha": "這是一個國際象棋驗證碼。", + "importGameCaveat": "變種分支將被刪除。 若要保存這些變種,請透過導入 PGN 棋譜建立一個研究。", + "importGameDataPrivacyWarning": "此為公開 PGN。若要導入私人棋局,請使用研究。", + "thisIsAChessCaptcha": "此為西洋棋驗證碼。", "clickOnTheBoardToMakeYourMove": "點擊棋盤走棋以證明您是人類。", "captcha_fail": "請完成驗證。", "notACheckmate": "沒有將死", @@ -615,6 +766,7 @@ "blackCheckmatesInOneMove": "黑方一步棋將死對手", "retry": "重試", "reconnecting": "重新連接中", + "noNetwork": "離線", "favoriteOpponents": "最喜歡的對手", "follow": "關注", "following": "已關注", @@ -635,12 +787,12 @@ "required": "必填項目。", "openTournaments": "公開錦標賽", "duration": "持續時間", - "winner": "勝利者", + "winner": "贏家", "standing": "名次", "createANewTournament": "建立新的錦標賽", "tournamentCalendar": "錦標賽日程", "conditionOfEntry": "加入限制:", - "advancedSettings": "高級設定", + "advancedSettings": "進階設定", "safeTournamentName": "幫錦標賽挑選一個適合的名字", "inappropriateNameWarning": "即便只是一點點的違規都有可能導致您的帳號被封鎖。", "emptyTournamentName": "若不填入錦標賽的名稱,將會用一位著名的棋手名字來做為錦標賽名稱。", @@ -675,7 +827,7 @@ "chess960StartPosition": "960棋局開局位置: {param}", "startPosition": "初始佈局", "clearBoard": "清空棋盤", - "loadPosition": "裝入佈局", + "loadPosition": "載入佈局", "isPrivate": "私人", "reportXToModerators": "將{param}報告給管理人員", "profileCompletion": "個人檔案完成度:{param}", @@ -683,9 +835,10 @@ "ifNoneLeaveEmpty": "如果沒有,請留空", "profile": "資料", "editProfile": "編輯資料", - "setFlair": "設置你的圖標", - "flair": "圖標", - "youCanHideFlair": "有一個設置可以隱藏整個網站上所有用户圖標。", + "realName": "真實名稱", + "setFlair": "設置你的身分", + "flair": "身分", + "youCanHideFlair": "你可以在設定中隱藏使用者身分。", "biography": "個人簡介", "countryRegion": "國家或地區", "thankYou": "謝謝!", @@ -702,41 +855,46 @@ "automaticallyProceedToNextGameAfterMoving": "移动棋子后自动进入下一盘棋", "autoSwitch": "自动更换", "puzzles": "謎題", - "name": "名", + "onlineBots": "線上機器人", + "name": "名稱", "description": "描述", "descPrivate": "內部簡介", "descPrivateHelp": "僅團隊成員可見,設置後將覆蓋公開簡介為團隊成員展示。", "no": "否", "yes": "是", + "website": "網頁版", + "mobile": "行動裝置", "help": "幫助:", - "createANewTopic": "新话题", - "topics": "话题", + "createANewTopic": "新話題", + "topics": "話題", "posts": "貼文", "lastPost": "最近貼文", - "views": "浏览", - "replies": "回复", - "replyToThisTopic": "回复此话题", - "reply": "回复", - "message": "信息", - "createTheTopic": "创建话题", - "reportAUser": "举报用户", - "user": "用户", + "views": "瀏覽", + "replies": "回覆", + "replyToThisTopic": "回覆此話題", + "reply": "回覆", + "message": "訊息", + "createTheTopic": "建立話題", + "reportAUser": "舉報使用者", + "user": "使用者", "reason": "原因", - "whatIsIheMatter": "举报原因?", + "whatIsIheMatter": "舉報原因?", "cheat": "作弊", - "troll": "钓鱼", + "troll": "搗亂", "other": "其他", - "reportDescriptionHelp": "附上游戏的网址解释该用户的行为问题", + "reportCheatBoostHelp": "請詳細說明你舉報此使用者的具體原因並貼上遊戲連結。「他作弊」等簡短說明是不被接受的。", + "reportUsernameHelp": "請詳細說明你舉報此使用者的具體原因。若必要請解釋其名詞的歷史意義、網路用語、或是此使用者名稱如何指桑罵槐。「他的使用者名稱不妥」等簡短說明是不被接受的。", + "reportProcessedFasterInEnglish": "若舉報內容為英文將會更快的被處理。", "error_provideOneCheatedGameLink": "請提供至少一局作弊棋局的連結。", - "by": "{param}作", - "importedByX": "由{param}滙入", - "thisTopicIsNowClosed": "本话题已关闭。", - "blog": "博客", - "notes": "笔记", - "typePrivateNotesHere": "在此輸入私人筆記", + "by": "作者:{param}", + "importedByX": "由{param}導入", + "thisTopicIsNowClosed": "此話題已關閉", + "blog": "部落格", + "notes": "備註", + "typePrivateNotesHere": "在此輸入私人備註", "writeAPrivateNoteAboutThisUser": "備註用戶資訊", - "noNoteYet": "尚無筆記", - "invalidUsernameOrPassword": "用户名或密碼錯誤", + "noNoteYet": "尚無備註", + "invalidUsernameOrPassword": "使用者名稱或密碼錯誤", "incorrectPassword": "舊密碼錯誤", "invalidAuthenticationCode": "驗證碼無效", "emailMeALink": "通過電郵發送連結給我", @@ -749,62 +907,63 @@ "clockIncrement": "加秒", "privacy": "隱私", "privacyPolicy": "隱私條款", - "letOtherPlayersFollowYou": "允许其他玩家关注", - "letOtherPlayersChallengeYou": "允许其他玩家挑战", + "letOtherPlayersFollowYou": "允許其他玩家關注", + "letOtherPlayersChallengeYou": "允許其他玩家發起挑戰", "letOtherPlayersInviteYouToStudy": "允許其他棋手邀請你參加研討", - "sound": "聲音", + "sound": "音效", "none": "無", "fast": "快", "normal": "普通", "slow": "慢", "insideTheBoard": "棋盤內", "outsideTheBoard": "棋盤外", + "allSquaresOfTheBoard": "包括所有棋盤內的格子", "onSlowGames": "慢棋時", "always": "總是", "never": "永不", - "xCompetesInY": "{param1}参加{param2}", - "victory": "成功!", + "xCompetesInY": "{param1}在{param2}參加", + "victory": "勝利", "defeat": "戰敗", "victoryVsYInZ": "{param1}在{param3}模式下贏了{param2}", "defeatVsYInZ": "{param1}在{param3}模式下輸給了{param2}", - "drawVsYInZ": "{param1}在{param3}模式下和{param2}平手", - "timeline": "时间线", - "starting": "开始时间:", - "allInformationIsPublicAndOptional": "所有資料是公開的,同時是可選的。", - "biographyDescription": "給我們一個您的自我介紹,像是您的興趣、您喜愛的選手等", - "listBlockedPlayers": "显示黑名单用户列表", - "human": "人类", + "drawVsYInZ": "{param1}在{param3}模式下和{param2}和棋", + "timeline": "時間軸", + "starting": "起始時間:", + "allInformationIsPublicAndOptional": "所有資料為公開並且可被隱藏。", + "biographyDescription": "給一個自我介紹,例如興趣或您喜愛的選手等", + "listBlockedPlayers": "顯示黑名單", + "human": "人類", "computer": "電腦", "side": "方", - "clock": "鐘", - "opponent": "对手", - "learnMenu": "學棋", + "clock": "棋鐘", + "opponent": "對手", + "learnMenu": "學習", "studyMenu": "研究", "practice": "練習", - "community": "社區", + "community": "社群", "tools": "工具", "increment": "加秒", "error_unknown": "無效值", - "error_required": "本项必填", - "error_email": "這個電子郵件地址無效", - "error_email_acceptable": "該電子郵件地址是不可用。請重新檢查後重試。", + "error_required": "本項必填", + "error_email": "無效電子郵件", + "error_email_acceptable": "該電子郵件地址無效。請重新檢查後重試。", "error_email_unique": "電子郵件地址無效或已被使用", "error_email_different": "這已經是您的電子郵件地址", - "error_minLength": "至少應有 {param} 個字元長", - "error_maxLength": "最多不能超過 {param} 個字元長", - "error_min": "最少 {param} 個字符", - "error_max": "最大不能超過 {param}", - "ifRatingIsPlusMinusX": "允许评级范围±{param}", - "ifRegistered": "如已註冊", + "error_minLength": "至少包含 {param} 個字元", + "error_maxLength": "最多包含 {param} 個字元", + "error_min": "最少包含 {param} 個字符", + "error_max": "最多不能超過 {param}", + "ifRatingIsPlusMinusX": "允許評級範圍±{param}", + "ifRegistered": "已登入者", "onlyExistingConversations": "僅目前對話", "onlyFriends": "只允許好友", - "menu": "菜单", - "castling": "王车易位", + "menu": "選單", + "castling": "王車易位", "whiteCastlingKingside": "白方短易位", "blackCastlingKingside": "黑方短易位", "tpTimeSpentPlaying": "花在下棋上的時間:{param}", "watchGames": "觀看對局直播", - "tpTimeSpentOnTV": "花在Lichess TV觀看直播的時間:{param}", + "tpTimeSpentOnTV": "花在Lichess TV的時間:{param}", "watch": "觀看", "videoLibrary": "影片庫", "streamersMenu": "實況主", @@ -812,112 +971,113 @@ "webmasters": "網站管理員", "about": "關於", "aboutX": "關於 {param}", - "xIsAFreeYLibreOpenSourceChessServer": "{param1}是一個免費的({param2}),開放性的,無廣告,開放資源的網站", + "xIsAFreeYLibreOpenSourceChessServer": "{param1}是一個完全免費({param2})、開放性、無廣告、並且開源的網站", "really": "真的", "contribute": "協助", "termsOfService": "服務條款", "sourceCode": "原始碼", "simultaneousExhibitions": "車輪戰", "host": "主持", - "hostColorX": "主持者使用旗子顏色:{param}", - "yourPendingSimuls": "你待處理的車輪戰", - "createdSimuls": "最近开始的同步赛", - "hostANewSimul": "主持新同步赛", + "hostColorX": "主持人所使用旗子顏色:{param}", + "yourPendingSimuls": "正在載入比賽", + "createdSimuls": "觀看最近開始的車輪戰", + "hostANewSimul": "主持車輪戰", "signUpToHostOrJoinASimul": "註冊以舉辦或參與車輪戰", - "noSimulFound": "找不到该同步赛", + "noSimulFound": "找不到該車輪戰", "noSimulExplanation": "此車輪戰不存在。", - "returnToSimulHomepage": "返回表演赛主页", + "returnToSimulHomepage": "返回車輪戰首頁", "aboutSimul": "車輪戰涉及到一個人同時和幾位棋手下棋。", - "aboutSimulImage": "在50个对手中,菲舍尔赢了47局,和了2局,输了1局。", - "aboutSimulRealLife": "这个概念来自真实的国际赛事。 在现实中,这涉及到主持在桌与桌之间来回穿梭走棋。", - "aboutSimulRules": "当表演赛开始的时候, 每个玩家都与主持开始对弈, 而主持用白方。 当所有的对局都结束时,表演赛就结束了。", - "aboutSimulSettings": "表演赛总是不定级的。 复赛、悔棋和\"加时\"功能将被禁用。", - "create": "创建", + "aboutSimulImage": "在50位對手中,費雪贏了47局、和了2局、並輸了1局。", + "aboutSimulRealLife": "這種賽制來自於真實的國際賽事。 在現實中,這涉及到主持人在棋局與棋局之間來回走棋。", + "aboutSimulRules": "當車輪賽開始時,每個玩家都會與主持人對奕,主持人持白。當所有對局結束表示車輪賽也一併結束。", + "aboutSimulSettings": "車輪賽事較為非正式的賽制。重賽、悔棋、以及加時功能皆會被禁用。", + "create": "建立", "whenCreateSimul": "當您創建車輪戰時,您要同時跟幾個棋手一起下棋。", - "simulVariantsHint": "如果您选择几个变体,每个玩家都要选择下哪一种。", - "simulClockHint": "菲舍爾時鐘設定。棋手越多,您需要的時間可能就越多。", + "simulVariantsHint": "如果您選擇多個變體,每個玩家可以選擇自己所好的變體。", + "simulClockHint": "費雪棋鐘設定。棋手越多,您所需的時間可能就越多。", "simulAddExtraTime": "您可以給您的時鍾多加點時間以幫助您應對車輪戰。", - "simulHostExtraTime": "主持人的额外时间", + "simulHostExtraTime": "主持人的額外時間", "simulAddExtraTimePerPlayer": "每有一個玩家加入車輪戰,您棋鐘的初始時間都將增加。", - "simulHostExtraTimePerPlayer": "每個玩家加入后棋鐘增加的額外時間", - "lichessTournaments": "Lichess比赛", - "tournamentFAQ": "比赛常见问题", - "timeBeforeTournamentStarts": "比赛准备时间", - "averageCentipawnLoss": "平均厘兵损失", + "simulHostExtraTimePerPlayer": "於每位玩家加入後棋鐘增加的額外時間", + "lichessTournaments": "Lichess 錦標賽", + "tournamentFAQ": "競技場錦標賽常見問題", + "timeBeforeTournamentStarts": "錦標賽準備時間", + "averageCentipawnLoss": "平均厘兵損失", "accuracy": "精準度", - "keyboardShortcuts": "快捷键", - "keyMoveBackwardOrForward": "后退/前进", - "keyGoToStartOrEnd": "跳到开始/结束", + "keyboardShortcuts": "快捷鍵", + "keyMoveBackwardOrForward": "後退/前進", + "keyGoToStartOrEnd": "跳轉到開始/結束", "keyCycleSelectedVariation": "循環已選取的變體", - "keyShowOrHideComments": "显示/隐藏评论", - "keyEnterOrExitVariation": "进入/退出变体", + "keyShowOrHideComments": "顯示/隱藏評論", + "keyEnterOrExitVariation": "進入/退出變體", "keyRequestComputerAnalysis": "請求引擎分析,從你的失誤中學習", "keyNextLearnFromYourMistakes": "下一個 (從你的失誤中學習)", "keyNextBlunder": "下一個漏著", - "keyNextMistake": "下一個錯著", - "keyNextInaccuracy": "下一個疑著", + "keyNextMistake": "下一個錯誤", + "keyNextInaccuracy": "下一個輕微失誤", "keyPreviousBranch": "上一個分支", "keyNextBranch": "下一個分支", - "toggleVariationArrows": "切換變體箭頭", + "toggleVariationArrows": "顯示變體箭頭", "cyclePreviousOrNextVariation": "循環上一個/下一個變體", - "toggleGlyphAnnotations": "切換圖形標註", + "toggleGlyphAnnotations": "顯示圖形標註", + "togglePositionAnnotations": "顯示位置標註", "variationArrowsInfo": "變體箭頭讓你不需棋步列表導航", - "playSelectedMove": "走已選的棋步", - "newTournament": "新比赛", - "tournamentHomeTitle": "国际象棋赛事均设有不同的时间控制和变体", + "playSelectedMove": "走已選取的棋步", + "newTournament": "新比賽", + "tournamentHomeTitle": "富有各種時間以及變體的西洋棋錦標賽", "tournamentHomeDescription": "加入快節奏的國際象棋比賽!加入定時賽事,或創建自己的。子彈,閃電,經典,菲舍爾任意制,王到中心,三次將軍,並提供更多的選擇為無盡的國際象棋樂趣。", - "tournamentNotFound": "找不到该比赛", - "tournamentDoesNotExist": "这个比赛不存在。", - "tournamentMayHaveBeenCanceled": "它可能已被取消,假如所有的对手在比赛开始之前离开。", - "returnToTournamentsHomepage": "返回比赛主页", - "weeklyPerfTypeRatingDistribution": "本月{param}的分数分布", - "yourPerfTypeRatingIsRating": "您的{param1}分数是{param2}分。", - "youAreBetterThanPercentOfPerfTypePlayers": "您比{param1}的{param2}棋手更强。", + "tournamentNotFound": "找不到該錦標賽", + "tournamentDoesNotExist": "這個錦標賽不存在。", + "tournamentMayHaveBeenCanceled": "錦標賽可能因為沒有其他玩家而取消。", + "returnToTournamentsHomepage": "返回錦標賽首頁", + "weeklyPerfTypeRatingDistribution": "本月{param}的分數分布", + "yourPerfTypeRatingIsRating": "您的{param1}目前{param2}分。", + "youAreBetterThanPercentOfPerfTypePlayers": "您比{param1}的{param2}棋手更強。", "userIsBetterThanPercentOfPerfTypePlayers": "{param1}比{param3}之中的{param2}棋手強。", "betterThanPercentPlayers": "您比{param1}的{param2}棋手更強。", - "youDoNotHaveAnEstablishedPerfTypeRating": "您没有准确的{param}评级。", + "youDoNotHaveAnEstablishedPerfTypeRating": "您沒有準確的{param}評級。", "yourRating": "您的評分", "cumulative": "平均累積", "glicko2Rating": "Glicko-2 積分", "checkYourEmail": "請檢查您的電子郵件", - "weHaveSentYouAnEmailClickTheLink": "我們已經發送了一封電子郵件到你的郵箱. 點擊郵件中的連結以激活您的賬號.", + "weHaveSentYouAnEmailClickTheLink": "我們已經發送了一封電子郵件到你的郵箱。點擊郵件中的連結以啟用帳號。", "ifYouDoNotSeeTheEmailCheckOtherPlaces": "若您沒收到郵件,請檢查您的其他收件箱,例如垃圾箱、促銷、社交等。", "weHaveSentYouAnEmailTo": "我們發送了一封郵件到 {param}。點擊郵件中的連結來重置您的密碼。", - "byRegisteringYouAgreeToBeBoundByOur": "您一登记,我们就假设您同意尊重我们的使用规则({param})。", + "byRegisteringYouAgreeToBeBoundByOur": "註冊帳號表示同意並且遵守 {param}", "readAboutOur": "閱讀我們的{param}", - "networkLagBetweenYouAndLichess": "您和 lichess 之間的網絡時滯", + "networkLagBetweenYouAndLichess": "您和 Lichess 之間的網路停滯", "timeToProcessAMoveOnLichessServer": "lichess 伺服器上處理走棋的時間", - "downloadAnnotated": "下载带笔记的记录", - "downloadRaw": "下载无笔记的记录", - "downloadImported": "下载已导入棋局", - "crosstable": "历史表", - "youCanAlsoScrollOverTheBoardToMoveInTheGame": "您也可以用滚动键在棋盘游戏中移动。", - "scrollOverComputerVariationsToPreviewThem": "將鼠標移到電腦分析變招上進行預覽", - "analysisShapesHowTo": "按shift点击或右键棋盘上绘制圆圈和箭头。", + "downloadAnnotated": "下載含有棋子走動方向的棋局", + "downloadRaw": "下載純文字", + "downloadImported": "下載導入的棋局", + "crosstable": "歷程表", + "youCanAlsoScrollOverTheBoardToMoveInTheGame": "您也可以捲動棋盤以移動。", + "scrollOverComputerVariationsToPreviewThem": "將鼠標移到電腦分析變種上進行預覽", + "analysisShapesHowTo": "按 shift 點及或右鍵棋盤上以繪製圓圈與箭頭。", "letOtherPlayersMessageYou": "允許其他人發送私訊給您", "receiveForumNotifications": "在論壇中被提及時接收通知", - "shareYourInsightsData": "分享您的慧眼数据", - "withNobody": "不分享", - "withFriends": "與好友分享", - "withEverybody": "與所有人分享", + "shareYourInsightsData": "顯示您的洞察數據", + "withNobody": "不顯示", + "withFriends": "好友", + "withEverybody": "所有人", "kidMode": "兒童模式", - "kidModeIsEnabled": "已啓用兒童模式", + "kidModeIsEnabled": "已啟用兒童模式", "kidModeExplanation": "考量安全,在兒童模式中,網站上全部的文字交流將會被關閉。開啟此模式來保護你的孩子及學生不被網路上的人傷害。", "inKidModeTheLichessLogoGetsIconX": "在兒童模式下,Lichess的標誌會有一個{param}圖示,讓你知道你的孩子是安全的。", "askYourChessTeacherAboutLiftingKidMode": "你的帳戶被管理,詢問你的老師解除兒童模式。", "enableKidMode": "啟用兒童模式", "disableKidMode": "停用兒童模式", "security": "資訊安全相關設定", - "sessions": "會話", + "sessions": "裝置", "revokeAllSessions": "登出所有裝置", - "playChessEverywhere": "随处下棋!", - "asFreeAsLichess": "完全又永遠的免費。", - "builtForTheLoveOfChessNotMoney": "不是為了錢,是為了國際象棋所創建。", + "playChessEverywhere": "隨處下棋!", + "asFreeAsLichess": "完全、永遠免費。", + "builtForTheLoveOfChessNotMoney": "不是為了錢,是為了西洋棋所創建。", "everybodyGetsAllFeaturesForFree": "每個人都能免費使用所有功能", "zeroAdvertisement": "沒有廣告", "fullFeatured": "功能全面", "phoneAndTablet": "手機和平板電腦", - "bulletBlitzClassical": "子彈,閃電,經典", + "bulletBlitzClassical": "快或慢都隨你!", "correspondenceChess": "通訊賽", "onlineAndOfflinePlay": "線上或離線下棋", "viewTheSolution": "看解答", @@ -936,23 +1096,29 @@ "dark": "暗", "transparent": "透明度", "deviceTheme": "設備主題", - "backgroundImageUrl": "背景圖片網址:", + "backgroundImageUrl": "背景圖片網址:", + "board": "棋盤外觀", + "size": "大小", + "opacity": "透明度", + "brightness": "亮度", + "hue": "色調", + "boardReset": "回復預設顏色設定", "pieceSet": "棋子外觀設定", "embedInYourWebsite": "嵌入您的網站", - "usernameAlreadyUsed": "此用戶名已經有人在使用,請嘗試使用別的", + "usernameAlreadyUsed": "該使用者名稱已被使用,請換一個試試!", "usernamePrefixInvalid": "使用者名稱必須以字母開頭", "usernameSuffixInvalid": "使用者名稱的結尾必須為字母或數字", "usernameCharsInvalid": "使用者名稱只能包含字母、 數字、 底線和短劃線。", - "usernameUnacceptable": "此使用者名稱不可用", + "usernameUnacceptable": "無法套用此使用者名稱", "playChessInStyle": "下棋也要穿得好看", - "chessBasics": "基本知識", + "chessBasics": "基本常識", "coaches": "教練", - "invalidPgn": "無效的PGN", - "invalidFen": "無效的FEN", + "invalidPgn": "無效的 PGN", + "invalidFen": "無效的 FEN", "custom": "自訂設定", "notifications": "通知", - "notificationsX": "通知: {param1}", - "perfRatingX": "評分:{param}", + "notificationsX": "通知:{param1}", + "perfRatingX": "評分:{param}", "practiceWithComputer": "和電腦練習", "anotherWasX": "另一個是{param}", "bestWasX": "最好的一步是{param}", @@ -974,8 +1140,8 @@ "xWasPlayed": "走了{param}", "findBetterMoveForWhite": "找出白方的最佳著法", "findBetterMoveForBlack": "找出黑方的最佳著法", - "resumeLearning": "回復學習", - "youCanDoBetter": "您還可以做得更好", + "resumeLearning": "繼續學習", + "youCanDoBetter": "還有更好的移動", "tryAnotherMoveForWhite": "嘗試白方更好其他的著法", "tryAnotherMoveForBlack": "嘗試黑方更好其他的著法", "solution": "解決方案", @@ -984,7 +1150,7 @@ "noMistakesFoundForBlack": "沒有找到黑方的失誤", "doneReviewingWhiteMistakes": "已完成觀看白方的失誤", "doneReviewingBlackMistakes": "已完成觀看黑方的失誤", - "doItAgain": "重作一次", + "doItAgain": "再試一次", "reviewWhiteMistakes": "複習白方失誤", "reviewBlackMistakes": "複習黑方失誤", "advantage": "優勢", @@ -992,25 +1158,25 @@ "middlegame": "中場", "endgame": "殘局", "conditionalPremoves": "預設棋譜", - "addCurrentVariation": "加入現有變化", - "playVariationToCreateConditionalPremoves": "著一步不同的位置以創建預估走位", + "addCurrentVariation": "加入現有變種", + "playVariationToCreateConditionalPremoves": "走一種變種以建立棋譜", "noConditionalPremoves": "無預設棋譜", "playX": "移動至{param}", - "showUnreadLichessMessage": "你收到一個來自 Lichess 的私人信息。", - "clickHereToReadIt": "點擊閱讀", - "sorry": "抱歉:(", - "weHadToTimeYouOutForAWhile": "您被封鎖了,在一陣子的時間內將不能下棋", + "showUnreadLichessMessage": "你收到一個來自 Lichess 的私訊。", + "clickHereToReadIt": "點擊以閱讀", + "sorry": "抱歉:(", + "weHadToTimeYouOutForAWhile": "我們必須將您暫時封鎖", "why": "為什麼?", - "pleasantChessExperience": "我們的目的在於為所有人提供愉快的國際象棋體驗", + "pleasantChessExperience": "我們的目的在於維持良好的下棋環境", "goodPractice": "為此,我們必須確保所有參與者都遵循良好做法", "potentialProblem": "當檢測到不良行為時,我們將顯示此消息", "howToAvoidThis": "如何避免這件事發生?", - "playEveryGame": "下好每一盤您加入的棋", + "playEveryGame": "避免在棋局中任意退出", "tryToWin": "試著在每個棋局裡獲勝(或至少平手)", - "resignLostGames": "棄權(不要讓時間耗盡)", - "temporaryInconvenience": "對於給您帶來的不便,我們深表歉意", - "wishYouGreatGames": "並祝您在lichess.org上玩得開心。", - "thankYouForReading": "感謝您的閱讀!", + "resignLostGames": "投降(不要讓時間耗盡)", + "temporaryInconvenience": "我們對於給您帶來的不便深表歉意", + "wishYouGreatGames": "並祝您在 lichess.org 上玩得開心。", + "thankYouForReading": "感謝您的閱讀!", "lifetimeScore": "帳戶總分", "currentMatchScore": "現時的對局分數", "agreementAssistance": "我同意我不會在比賽期間使用支援(從書籍、電腦運算、資料庫等等)", @@ -1019,22 +1185,23 @@ "agreementPolicy": "我同意我將會遵守Lichess的規則", "searchOrStartNewDiscussion": "尋找或開始聊天", "edit": "編輯", - "blitz": "快棋", + "bullet": "Bullet", + "blitz": "Blitz", "rapid": "快速模式", "classical": "經典", - "ultraBulletDesc": "瘋狂速度模式: 低於30秒", - "bulletDesc": "非常速度模式:低於3分鐘", - "blitzDesc": "快速模式:3到8分鐘", - "rapidDesc": "一般模式:8到25分鐘", - "classicalDesc": "經典模式:25分鐘以上", - "correspondenceDesc": "長期模式:一天或好幾天一步", + "ultraBulletDesc": "瘋狂速度模式:低於30秒", + "bulletDesc": "極快速模式:低於3分鐘", + "blitzDesc": "快速模式:3到8分鐘", + "rapidDesc": "一般模式:8到25分鐘", + "classicalDesc": "經典模式:25分鐘以上", + "correspondenceDesc": "通信模式:一天或好幾天一步", "puzzleDesc": "西洋棋戰術教練", "important": "重要", "yourQuestionMayHaveBeenAnswered": "您的問題可能已經有答案了{param1}", - "inTheFAQ": "在F.A.Q裡", - "toReportSomeoneForCheatingOrBadBehavior": "舉報一位作弊或者是不良行為的玩家,{param1}", - "useTheReportForm": "請造訪回報頁面", - "toRequestSupport": "需要請求協助,{param1}", + "inTheFAQ": "在常見問答內", + "toReportSomeoneForCheatingOrBadBehavior": "舉報一位作弊或違反善良風俗的玩家,{param1}", + "useTheReportForm": "請填寫回報表單", + "toRequestSupport": "{param1}以獲取協助", "tryTheContactPage": "請到協助頁面", "makeSureToRead": "確保你已閱讀 {param1}", "theForumEtiquette": "論壇禮儀", @@ -1044,33 +1211,33 @@ "youCannotPostYetPlaySomeGames": "您目前不能發表文章在論壇裡,先下幾盤棋吧!", "subscribe": "訂閱", "unsubscribe": "取消訂閱", - "mentionedYouInX": "在 \"{param1}\" 中提到了您。", - "xMentionedYouInY": "{param1} 在 \"{param2}\" 中提到了您。", - "invitedYouToX": "邀請您至\"{param1}\"。", - "xInvitedYouToY": "{param1} 邀請您至\"{param2}\"。", + "mentionedYouInX": "在「{param1}」中提到了您。", + "xMentionedYouInY": "{param1} 在「{param2}」中提到了您。", + "invitedYouToX": "邀請您至「{param1}」。", + "xInvitedYouToY": "{param1} 邀請您至「{param2}」。", "youAreNowPartOfTeam": "您現在是團隊的成員了。", - "youHaveJoinedTeamX": "您已加入 \"{param1}\"。", + "youHaveJoinedTeamX": "您已加入「{param1}」。", "someoneYouReportedWasBanned": "您檢舉的玩家已被封鎖帳號", "congratsYouWon": "恭喜,您贏了!", "gameVsX": "與{param1}對局", "resVsX": "{param1} vs {param2}", - "lostAgainstTOSViolator": "你輸給了違反了服務挑款的棋手", + "lostAgainstTOSViolator": "你輸給了違反了服務條款的棋手", "refundXpointsTimeControlY": "退回 {param1} {param2} 等級分。", "timeAlmostUp": "時間快到了!", - "clickToRevealEmailAddress": "[按下展示電郵位置]", + "clickToRevealEmailAddress": "[點擊以顯示電子郵件]", "download": "下載", "coachManager": "教練管理", "streamerManager": "直播管理", "cancelTournament": "取消錦標賽", "tournDescription": "錦標賽敘述", - "tournDescriptionHelp": "有甚麼特別要告訴參賽者的嗎?盡量不要太長。可以使用Markdown網址 [name](https://url)。", + "tournDescriptionHelp": "有甚麼特別要告訴參賽者的嗎?盡量不要太長。可以使用 Markdown 網址 [name](https://url)。", "ratedFormHelp": "比賽為積分賽\n會影響到棋手的積分", "onlyMembersOfTeam": "只限隊員", "noRestriction": "沒有限制", "minimumRatedGames": "評分局遊玩次數下限", "minimumRating": "評分下限", "maximumWeeklyRating": "每週最高評分", - "positionInputHelp": "將一個有效的 FEN 粘貼於此作為所有對局的起始位置。\n僅適用於標準國際象棋,對變體無效。\n你可以試用 {param} 來生成 FEN,然後將其粘貼到這裡。\n置空表示以標準位置開始比賽。", + "positionInputHelp": "將一個有效的 FEN 貼上於此作為所有對局的起始位置。\n僅適用於標準西洋棋,對變種無效。\n你可以試用 {param} 來生成 FEN,然後將其貼上到這裡。\n置空表示以預設位置開始比賽。", "cancelSimul": "取消車輪戰", "simulHostcolor": "主持所執方", "estimatedStart": "預計開始時間", @@ -1079,8 +1246,8 @@ "simulDescription": "車輪戰描述", "simulDescriptionHelp": "有甚麼要告訴參賽者的嗎?", "markdownAvailable": "{param} 可用於更高級的格式。", - "embedsAvailable": "粘貼對局URL或學習章節URL來嵌入。", - "inYourLocalTimezone": "在你的時區內", + "embedsAvailable": "貼上對局或學習章節網址來嵌入。", + "inYourLocalTimezone": "在您的時區內", "tournChat": "錦標賽聊天室", "noChat": "無聊天室", "onlyTeamLeaders": "僅限各隊隊長", @@ -1095,7 +1262,7 @@ "showHelpDialog": "顯示此說明欄", "reopenYourAccount": "重新開啟帳戶", "closedAccountChangedMind": "如果你停用了自己的帳號,但是改變了心意,你有一次的機會可以拿回帳號。", - "onlyWorksOnce": "這只能復原一次", + "onlyWorksOnce": "這只能復原一次。", "cantDoThisTwice": "如果你決定再次停用你的帳號,則不會有任何方式去復原。", "emailAssociatedToaccount": "和此帳號相關的電子信箱", "sentEmailWithLink": "我們已將網址寄送至你的信箱", @@ -1113,7 +1280,9 @@ "ourEventTips": "舉辦賽事的小建議", "instructions": "說明", "showMeEverything": "全部顯示", - "lichessPatronInfo": "Lichess是個慈善、完全免費之開源軟件。\n一切營運成本、開發和內容皆來自用戶之捐贈。", + "lichessPatronInfo": "Lichess是個慈善、完全免費且開源的軟體。\n一切營運成本、開發和內容皆來自用戶之捐贈。", + "nothingToSeeHere": "目前這裡沒有什麼好看的。", + "stats": "統計", "opponentLeftCounter": "{count, plural, other{您的對手已經離開了遊戲。您將在 {count} 秒後獲勝。}}", "mateInXHalfMoves": "{count, plural, other{在{count}步內將死對手}}", "nbBlunders": "{count, plural, other{{count} 次漏著}}", @@ -1148,14 +1317,14 @@ "nbFollowing": "{count, plural, other{關注{count}人}}", "lessThanNbMinutes": "{count, plural, other{小於{count}分鐘}}", "nbGamesInPlay": "{count, plural, other{{count}場對局正在進行中}}", - "maximumNbCharacters": "{count, plural, other{最多{count}个字符}}", - "blocks": "{count, plural, other{{count}位黑名单用户}}", + "maximumNbCharacters": "{count, plural, other{最多包含 {count} 個字符}}", + "blocks": "{count, plural, other{{count}位黑名單使用者}}", "nbForumPosts": "{count, plural, other{{count}個論壇貼文}}", "nbPerfTypePlayersThisWeek": "{count, plural, other{本周{count}位棋手下了{param2}模式的棋局}}", "availableInNbLanguages": "{count, plural, other{支援{count}種語言!}}", "nbSecondsToPlayTheFirstMove": "{count, plural, other{在{count}秒前須下出第一步}}", "nbSeconds": "{count, plural, other{{count} 秒}}", - "andSaveNbPremoveLines": "{count, plural, other{以儲存{count}列預走的棋步}}", + "andSaveNbPremoveLines": "{count, plural, other{以省略{count}個預走的棋步}}", "stormMoveToStart": "移動以開始", "stormYouPlayTheWhitePiecesInAllPuzzles": "您將在所有謎題中執白", "stormYouPlayTheBlackPiecesInAllPuzzles": "您將在所有謎題中執黑", @@ -1164,9 +1333,9 @@ "stormNewWeeklyHighscore": "新的每周紀錄!", "stormNewMonthlyHighscore": "新的每月紀錄!", "stormNewAllTimeHighscore": "新歷史紀錄!", - "stormPreviousHighscoreWasX": "之前的紀錄:{param}", + "stormPreviousHighscoreWasX": "之前的最高紀錄:{param}", "stormPlayAgain": "再玩一次", - "stormHighscoreX": "最高紀錄:{param}", + "stormHighscoreX": "最高紀錄:{param}", "stormScore": "得分", "stormMoves": "走棋", "stormAccuracy": "精準度", @@ -1175,8 +1344,8 @@ "stormTimePerMove": "平均走棋時間", "stormHighestSolved": "最難解決的題目", "stormPuzzlesPlayed": "解決過的題目", - "stormNewRun": "新的一輪 (快捷鍵:空白鍵)", - "stormEndRun": "結束此輪 (快捷鍵:Enter鍵)", + "stormNewRun": "新的一輪 (快捷鍵:空白鍵)", + "stormEndRun": "結束此輪 (快捷鍵:Enter 鍵)", "stormHighscores": "最高紀錄", "stormViewBestRuns": "顯示最佳的一輪", "stormBestRunOfDay": "今日最佳的一輪", @@ -1198,17 +1367,171 @@ "stormSkip": "跳過", "stormSkipHelp": "每場賽可略一手棋", "stormSkipExplanation": "跳過這一步來維持您的連擊紀錄!每次遊玩只能使用一次。", - "stormFailedPuzzles": "失敗了的謎題", - "stormSlowPuzzles": "慢 謎題", + "stormFailedPuzzles": "失敗的謎題", + "stormSlowPuzzles": "耗時謎題", + "stormSkippedPuzzle": "已跳過的謎題", "stormThisWeek": "本星期", "stormThisMonth": "本月", "stormAllTime": "總計", "stormClickToReload": "點擊以重新加載", - "stormThisRunHasExpired": "本次比賽已過期!", - "stormThisRunWasOpenedInAnotherTab": "本次沖刺已經在另一個標籤頁中打開!", + "stormThisRunHasExpired": "本輪已過期!", + "stormThisRunWasOpenedInAnotherTab": "本輪已經在另一個分頁中打開!", "stormXRuns": "{count, plural, other{{count}輪}}", "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, other{玩了{count}輪的{param2}}}", - "streamerLichessStreamers": "Lichess實況主", + "streamerLichessStreamers": "Lichess 實況主", + "studyPrivate": "私人的", + "studyMyStudies": "我的研究", + "studyStudiesIContributeTo": "我有貢獻的研究", + "studyMyPublicStudies": "我的公開研究", + "studyMyPrivateStudies": "我的私人研究", + "studyMyFavoriteStudies": "我最愛的研究", + "studyWhatAreStudies": "研究是什麼?", + "studyAllStudies": "所有研究", + "studyStudiesCreatedByX": "{param}創建的研究", + "studyNoneYet": "暫時沒有...", + "studyHot": "熱門的", + "studyDateAddedNewest": "新增日期(由新到舊)", + "studyDateAddedOldest": "新增日期(由舊到新)", + "studyRecentlyUpdated": "最近更新", + "studyMostPopular": "最受歡迎", + "studyAlphabetical": "按字母順序", + "studyAddNewChapter": "加入新章節", + "studyAddMembers": "新增成員", + "studyInviteToTheStudy": "邀請加入研究", + "studyPleaseOnlyInvitePeopleYouKnow": "只邀請你所認識的人,以及願意積極投入的人來共同研究", + "studySearchByUsername": "透過使用者名稱搜尋", + "studySpectator": "觀眾", + "studyContributor": "共同研究者", + "studyKick": "踢出", + "studyLeaveTheStudy": "退出研究", + "studyYouAreNowAContributor": "你現在是一位研究者了", + "studyYouAreNowASpectator": "你現在是觀眾", + "studyPgnTags": "PGN 標籤", + "studyLike": "喜歡", + "studyUnlike": "取消喜歡", + "studyNewTag": "新標籤", + "studyCommentThisPosition": "對於目前局面的評論", + "studyCommentThisMove": "對於此棋步的評論", + "studyAnnotateWithGlyphs": "以圖形標註", + "studyTheChapterIsTooShortToBeAnalysed": "因為太短,所以此章節無法被分析", + "studyOnlyContributorsCanRequestAnalysis": "只有研究專案編輯者才能要求電腦分析", + "studyGetAFullComputerAnalysis": "請求伺服器完整的分析主要走法", + "studyMakeSureTheChapterIsComplete": "確認此章節已完成,您只能要求分析一次", + "studyAllSyncMembersRemainOnTheSamePosition": "所有的SYNC成員處於相同局面", + "studyShareChanges": "向旁觀者分享這些變動並將其保留在伺服器中", + "studyPlaying": "下棋中", + "studyShowEvalBar": "評估條", + "studyFirst": "第一頁", + "studyPrevious": "上一頁", + "studyNext": "下一頁", + "studyLast": "最後一頁", "studyShareAndExport": "分享 & 導出", - "studyStart": "開始" + "studyCloneStudy": "複製", + "studyStudyPgn": "研究 PGN", + "studyDownloadAllGames": "下載所有棋局", + "studyChapterPgn": "章節PGN", + "studyCopyChapterPgn": "複製PGN", + "studyDownloadGame": "下載棋局", + "studyStudyUrl": "研究連結", + "studyCurrentChapterUrl": "目前章節連結", + "studyYouCanPasteThisInTheForumToEmbed": "您可以將此複製到論壇以嵌入", + "studyStartAtInitialPosition": "從起始局面開始", + "studyStartAtX": "從{param}開始", + "studyEmbedInYourWebsite": "嵌入到您的網站或部落格", + "studyReadMoreAboutEmbedding": "閱讀更多與嵌入有關的內容", + "studyOnlyPublicStudiesCanBeEmbedded": "只有公開的研究可以嵌入!", + "studyOpen": "打開", + "studyXBroughtToYouByY": "{param1},由{param2}提供", + "studyStudyNotFound": "找無此研究", + "studyEditChapter": "編輯章節", + "studyNewChapter": "建立新章節", + "studyImportFromChapterX": "從 {param} 導入", + "studyOrientation": "視角", + "studyAnalysisMode": "分析模式", + "studyPinnedChapterComment": "置頂留言", + "studySaveChapter": "儲存章節", + "studyClearAnnotations": "清除註記", + "studyClearVariations": "清除變化", + "studyDeleteChapter": "刪除章節", + "studyDeleteThisChapter": "刪除此章節? 此動作將無法取消!", + "studyClearAllCommentsInThisChapter": "清除此章節中的所有註釋和圖形嗎?", + "studyRightUnderTheBoard": "棋盤下方", + "studyNoPinnedComment": "無", + "studyNormalAnalysis": "一般分析", + "studyHideNextMoves": "隱藏下一步", + "studyInteractiveLesson": "互動課程", + "studyChapterX": "章節{param}", + "studyEmpty": "空的", + "studyStartFromInitialPosition": "從起始局面開始", + "studyEditor": "編輯器", + "studyStartFromCustomPosition": "從自定的局面開始", + "studyLoadAGameByUrl": "以連結導入棋局", + "studyLoadAPositionFromFen": "透過FEN讀取局面", + "studyLoadAGameFromPgn": "以PGN文件導入棋局", + "studyAutomatic": "自動", + "studyUrlOfTheGame": "棋局連結,一行一個", + "studyLoadAGameFromXOrY": "從{param1}或{param2}載入棋局", + "studyCreateChapter": "建立章節", + "studyCreateStudy": "建立研究", + "studyEditStudy": "編輯此研究", + "studyVisibility": "權限", + "studyPublic": "公開的", + "studyUnlisted": "不公開", + "studyInviteOnly": "僅限邀請", + "studyAllowCloning": "可以複製", + "studyNobody": "没有人", + "studyOnlyMe": "僅自己", + "studyContributors": "貢獻者", + "studyMembers": "成員", + "studyEveryone": "所有人", + "studyEnableSync": "允許同步", + "studyYesKeepEveryoneOnTheSamePosition": "同步:讓所有人停留在同一個局面", + "studyNoLetPeopleBrowseFreely": "不同步:允許所有人自由進行瀏覽", + "studyPinnedStudyComment": "置頂研究留言", + "studyStart": "開始", + "studySave": "存檔", + "studyClearChat": "清空對話紀錄", + "studyDeleteTheStudyChatHistory": "確定要清空課程對話紀錄嗎?此操作無法還原!", + "studyDeleteStudy": "刪除此研究", + "studyConfirmDeleteStudy": "你確定要刪除整個研究?此動作無法反悔。輸入研究名稱確認:{param}", + "studyWhereDoYouWantToStudyThat": "要從哪裡開始研究呢?", + "studyGoodMove": "好棋", + "studyMistake": "失誤", + "studyBrilliantMove": "妙着", + "studyBlunder": "嚴重失誤", + "studyInterestingMove": "有趣的一着", + "studyDubiousMove": "值得商榷的一着", + "studyOnlyMove": "唯一著法", + "studyZugzwang": "等著", + "studyEqualPosition": "勢均力敵", + "studyUnclearPosition": "局勢不明", + "studyWhiteIsSlightlyBetter": "白方稍占優勢", + "studyBlackIsSlightlyBetter": "黑方稍占優勢", + "studyWhiteIsBetter": "白方占優勢", + "studyBlackIsBetter": "黑方占優勢", + "studyWhiteIsWinning": "白方要取得勝利了", + "studyBlackIsWinning": "黑方要取得勝利了", + "studyNovelty": "新奇的", + "studyDevelopment": "發展", + "studyInitiative": "佔據主動", + "studyAttack": "攻擊", + "studyCounterplay": "反擊", + "studyTimeTrouble": "時間壓力", + "studyWithCompensation": "優勢補償", + "studyWithTheIdea": "教科書式的", + "studyNextChapter": "下一章", + "studyPrevChapter": "上一章", + "studyStudyActions": "研討操作", + "studyTopics": "主題", + "studyMyTopics": "我的主題", + "studyPopularTopics": "熱門主題", + "studyManageTopics": "管理主題", + "studyBack": "返回", + "studyPlayAgain": "再玩一次", + "studyWhatWouldYouPlay": "你會在這個位置上怎麼走?", + "studyYouCompletedThisLesson": "恭喜!您完成了這個課程。", + "studyNbChapters": "{count, plural, other{第{count}章}}", + "studyNbGames": "{count, plural, other{{count}對局}}", + "studyNbMembers": "{count, plural, other{{count}位成員}}", + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, other{在此貼上PGN文本,最多可導入{count}個棋局}}" } \ No newline at end of file diff --git a/lib/src/model/puzzle/puzzle_theme.dart b/lib/src/model/puzzle/puzzle_theme.dart index 5c7c791156..0ddf5236e3 100644 --- a/lib/src/model/puzzle/puzzle_theme.dart +++ b/lib/src/model/puzzle/puzzle_theme.dart @@ -95,8 +95,8 @@ enum PuzzleThemeKey { case PuzzleThemeKey.mix: case PuzzleThemeKey.unsupported: return PuzzleThemeL10n( - name: l10n.puzzleThemeHealthyMix, - description: l10n.puzzleThemeHealthyMixDescription, + name: l10n.puzzleThemeMix, + description: l10n.puzzleThemeMixDescription, ); case PuzzleThemeKey.advancedPawn: return PuzzleThemeL10n( From 3b4fa8b585052a0081efaf6c32bebfa059ad55a7 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 1 Nov 2024 18:15:24 +0100 Subject: [PATCH 560/979] Remove what is not working in i18n script; udpate whitelists --- scripts/update-arb-from-crowdin.mjs | 35 +++-------------------------- 1 file changed, 3 insertions(+), 32 deletions(-) diff --git a/scripts/update-arb-from-crowdin.mjs b/scripts/update-arb-from-crowdin.mjs index 07d0e37ad7..6c1ac02935 100755 --- a/scripts/update-arb-from-crowdin.mjs +++ b/scripts/update-arb-from-crowdin.mjs @@ -4,7 +4,6 @@ import { readFileSync, createWriteStream, writeFileSync, mkdirSync, existsSync } import { readdir, unlink } from 'node:fs/promises'; import { pipeline } from 'stream' import { promisify } from 'util' -import { exec } from 'child_process' import colors from 'colors/safe.js' import { parseStringPromise } from 'xml2js' import fetch from 'node-fetch' @@ -23,8 +22,6 @@ const lilaTranslationsPath = `${tmpDir}/[lichess-org.lila] master/translation/de const mobileSourcePath = `${__dirname}/../translation/source` const mobileTranslationsPath = `${tmpDir}/[lichess-org.mobile] main/translation/dest` -const unzipMaxBufferSize = 1024 * 1024 * 10 // Set maxbuffer to 10MB to avoid errors when default 1MB used - // selection of lila translation modules to include const modules = [ 'mobile', // mobile is not a module in crowdin, but another source of translations, we'll treat it as a module here for simplicity @@ -52,11 +49,8 @@ const whiteLists = { 'contact': ['contact', 'contactLichess'], 'search': ['search'], 'streamer': ['lichessStreamers'], - 'study': ['start', 'shareAndExport'], - 'broadcast': ['broadcasts', 'liveBroadcasts'], } - // Order of locales with variants matters: the fallback must always be first // eg: 'pt-PT' is before 'pt-BR' // Note that 'en-GB' is omitted here on purpose because it is the locale used in template ARB. @@ -76,10 +70,9 @@ main() // -- async function generateLilaTranslationARBs() { - // Download translations zip from crowdin - const zipFile = createWriteStream(`${tmpDir}/out.zip`) - await downloadTranslationsTo(zipFile) - await unzipTranslations(`${tmpDir}/out.zip`) + // Download zip doesn't work anymore, we need another way to get the translations + // This is tracked here: https://github.com/lichess-org/mobile/issues/945 + // for now we need to manually download the translations and put them in the tmp/translations folder // load all translations into a single object const translations = {} @@ -147,28 +140,6 @@ async function generateTemplateARB() { console.log(colors.green(' Template file successfully written.')) } -async function downloadTranslationsTo(zipFile) { - console.log(colors.blue('Downloading translations...')) - const streamPipeline = promisify(pipeline) - const response = await fetch('https://crowdin.com/backend/download/project/lichess.zip') - if (!response.ok) throw new Error(`unexpected response ${response.statusText}`) - - await streamPipeline(response.body, zipFile) - console.log(colors.green(' Download complete.')) -} - -async function unzipTranslations(zipFilePath) { - console.log(colors.blue('Unzipping translations...')) - return new Promise((resolve, reject) => { - exec(`unzip -o ${zipFilePath} -d ${tmpDir}`, {maxBuffer: unzipMaxBufferSize}, (err) => { - if (err) { - return reject('Unzip failed.') - } - resolve() - }) - }) -} - async function downloadLilaSourcesTo(dir) { console.log(colors.blue('Downloading lila source translations...')) const response = await octokitRequest('GET /repos/{owner}/{repo}/contents/{path}', { From 1721308aed9581d4b09e40bcc3d44ac36be31809 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Mon, 9 Sep 2024 23:21:42 +0200 Subject: [PATCH 561/979] feat: add study list screen --- lib/src/model/study/study_list_paginator.dart | 53 ++ lib/src/view/study/study_list_screen.dart | 467 ++++++++++++ lib/src/view/tools/tools_tab_screen.dart | 12 + test/view/study/study_list_screen_test.dart | 694 ++++++++++++++++++ 4 files changed, 1226 insertions(+) create mode 100644 lib/src/model/study/study_list_paginator.dart create mode 100644 lib/src/view/study/study_list_screen.dart create mode 100644 test/view/study/study_list_screen_test.dart diff --git a/lib/src/model/study/study_list_paginator.dart b/lib/src/model/study/study_list_paginator.dart new file mode 100644 index 0000000000..3f3d6bb068 --- /dev/null +++ b/lib/src/model/study/study_list_paginator.dart @@ -0,0 +1,53 @@ +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:lichess_mobile/src/model/study/study.dart'; +import 'package:lichess_mobile/src/model/study/study_filter.dart'; +import 'package:lichess_mobile/src/model/study/study_repository.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'study_list_paginator.g.dart'; + +typedef StudyList = ({IList studies, int? nextPage}); + +/// Gets a list of studies from the paginated API. +@riverpod +class StudyListPaginator extends _$StudyListPaginator { + @override + Future build({ + required StudyFilterState filter, + String? search, + }) async { + return _nextPage(); + } + + Future next() async { + final studyList = state.requireValue; + + final newStudyPage = await _nextPage(); + + state = AsyncData( + ( + nextPage: newStudyPage.nextPage, + studies: studyList.studies.addAll(newStudyPage.studies), + ), + ); + } + + Future _nextPage() async { + final nextPage = state.value?.nextPage ?? 1; + final studies = await ref.withClient( + (client) => search == null + ? StudyRepository(client).getStudies( + category: filter.category, + order: filter.order, + page: nextPage, + ) + : StudyRepository(client).searchStudies( + query: search!, + page: nextPage, + ), + ); + + return (studies: studies, nextPage: nextPage + 1); + } +} diff --git a/lib/src/view/study/study_list_screen.dart b/lib/src/view/study/study_list_screen.dart new file mode 100644 index 0000000000..8cb11da9e7 --- /dev/null +++ b/lib/src/view/study/study_list_screen.dart @@ -0,0 +1,467 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/auth/auth_session.dart'; +import 'package:lichess_mobile/src/model/study/study.dart'; +import 'package:lichess_mobile/src/model/study/study_filter.dart'; +import 'package:lichess_mobile/src/model/study/study_list_paginator.dart'; +import 'package:lichess_mobile/src/styles/lichess_icons.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/lichess_assets.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/filter.dart'; +import 'package:lichess_mobile/src/widgets/list.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; +import 'package:lichess_mobile/src/widgets/user_full_name.dart'; +import 'package:logging/logging.dart'; +import 'package:timeago/timeago.dart' as timeago; + +final _logger = Logger('StudyListScreen'); + +// TODO l10n +String studyCategoryL10n(StudyCategory category, BuildContext context) => + switch (category) { + StudyCategory.all => 'All', + StudyCategory.mine => 'Mine', + StudyCategory.member => 'Member', + StudyCategory.public => 'Public', + StudyCategory.private => 'Private', + StudyCategory.likes => 'Liked', + }; + +// TODO l10n +String studyListOrderL10n(StudyListOrder order, BuildContext context) => + switch (order) { + StudyListOrder.hot => 'Hot', + StudyListOrder.newest => 'Newest', + StudyListOrder.oldest => 'Oldest', + StudyListOrder.updated => 'Updated', + StudyListOrder.popular => 'Popular', + }; + +/// A screen that displays a paginated list of studies +class StudyListScreen extends ConsumerWidget { + const StudyListScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isLoggedIn = ref.read(authSessionProvider)?.user.id != null; + + final filter = ref.watch(studyFilterProvider); + final categorySection = + isLoggedIn ? ' • ${studyCategoryL10n(filter.category, context)}' : ''; + final title = Text( + '${context.l10n.studyMenu}$categorySection • ${studyListOrderL10n(filter.order, context)}', + ); + + return PlatformScaffold( + appBar: PlatformAppBar( + title: title, + actions: [ + AppBarIconButton( + icon: const Icon(Icons.tune), + semanticsLabel: 'Filter studies', + onPressed: () => showAdaptiveBottomSheet( + context: context, + builder: (_) => _StudyFilterSheet( + isLoggedIn: isLoggedIn, + ), + ), + ), + ], + ), + body: _Body( + filter: ref.watch(studyFilterProvider), + ), + ); + } +} + +class _StudyFilterSheet extends ConsumerWidget { + const _StudyFilterSheet({required this.isLoggedIn}); + + final bool isLoggedIn; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final filter = ref.watch(studyFilterProvider); + + return SafeArea( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 12.0), + // If we're not logged in, the only category available is "All" + if (isLoggedIn) ...[ + Filter( + // TODO l10n + filterName: 'Category', + filterType: FilterType.singleChoice, + choices: StudyCategory.values, + choiceSelected: (choice) => filter.category == choice, + choiceLabel: (category) => + Text(studyCategoryL10n(category, context)), + onSelected: (value, selected) => + ref.read(studyFilterProvider.notifier).setCategory(value), + ), + const PlatformDivider(thickness: 1, indent: 0), + const SizedBox(height: 10.0), + ], + Filter( + // TODO l10n + filterName: 'Sort by', + filterType: FilterType.singleChoice, + choices: StudyListOrder.values, + choiceSelected: (choice) => filter.order == choice, + choiceLabel: (order) => Text(studyListOrderL10n(order, context)), + onSelected: (value, selected) => + ref.read(studyFilterProvider.notifier).setOrder(value), + ), + ], + ), + ), + ); + } +} + +class _Body extends StatefulWidget { + const _Body({ + required this.filter, + }); + + final StudyFilterState filter; + + @override + State<_Body> createState() => _BodyState(); +} + +class _BodyState extends State<_Body> { + String? search; + + final _searchController = SearchController(); + + @override + void dispose() { + super.dispose(); + _searchController.dispose(); + } + + @override + Widget build(BuildContext context) { + return SafeArea( + child: Column( + children: [ + Padding( + padding: Styles.bodySectionPadding, + child: SearchBar( + controller: _searchController, + leading: const Icon(Icons.search), + trailing: [ + if (search != null) + IconButton( + onPressed: () => setState(() { + search = null; + _searchController.clear(); + }), + tooltip: 'Clear', + icon: const Icon( + Icons.close, + ), + ), + ], + hintText: search ?? context.l10n.searchSearch, + onSubmitted: (term) { + setState(() { + search = term; + }); + }, + ), + ), + _StudyList( + paginatorProvider: StudyListPaginatorProvider( + filter: widget.filter, + search: search, + ), + ), + ], + ), + ); + } +} + +class _StudyList extends ConsumerStatefulWidget { + const _StudyList({ + required this.paginatorProvider, + }); + + final StudyListPaginatorProvider paginatorProvider; + + @override + ConsumerState createState() => _StudyListState(); +} + +class _StudyListState extends ConsumerState<_StudyList> { + final _scrollController = ScrollController(); + + @override + void initState() { + super.initState(); + _scrollController.addListener(_scrollListener); + } + + @override + void dispose() { + _scrollController.removeListener(_scrollListener); + _scrollController.dispose(); + super.dispose(); + } + + void _scrollListener() { + if (_scrollController.position.pixels == + _scrollController.position.maxScrollExtent) { + final studiesList = ref.read(widget.paginatorProvider); + + if (!studiesList.isLoading) { + ref.read(widget.paginatorProvider.notifier).next(); + } + } + } + + @override + Widget build(BuildContext context) { + final studiesAsync = ref.watch(widget.paginatorProvider); + + return studiesAsync.when( + data: (studies) { + return Expanded( + child: ListView.separated( + controller: _scrollController, + itemCount: studies.studies.length, + separatorBuilder: (context, index) => const PlatformDivider( + height: 1, + cupertinoHasLeading: true, + ), + itemBuilder: (context, index) { + final study = studies.studies[index]; + return PlatformListTile( + padding: Styles.bodyPadding, + title: Row( + children: [ + _StudyFlair( + flair: study.flair, + size: 30, + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + study.name, + overflow: TextOverflow.ellipsis, + maxLines: 2, + ), + _StudySubtitle( + study: study, + style: const TextStyle( + fontSize: 12, + color: Colors.grey, + ), + ), + ], + ), + ), + ], + ), + subtitle: DefaultTextStyle.merge( + style: const TextStyle( + fontSize: 12, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 4, + child: _StudyChapters(study: study), + ), + Expanded( + flex: 3, + child: _StudyMembers( + study: study, + ), + ), + ], + ), + ), + onTap: () {}, + ); + }, + ), + ); + }, + loading: () { + return const Center(child: CircularProgressIndicator.adaptive()); + }, + error: (error, stack) { + _logger.severe('Error loading studies', error, stack); + return Center(child: Text(context.l10n.studyMenu)); + }, + ); + } +} + +class _StudyChapters extends StatelessWidget { + const _StudyChapters({ + required this.study, + }); + + final StudyPageData study; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...study.chapters.map( + (chapter) => Text.rich( + maxLines: 1, + overflow: TextOverflow.ellipsis, + TextSpan( + children: [ + WidgetSpan( + child: Icon( + Icons.circle_outlined, + size: DefaultTextStyle.of(context).style.fontSize, + ), + ), + TextSpan( + text: ' $chapter', + ), + ], + ), + ), + ), + ], + ); + } +} + +class _StudyMembers extends StatelessWidget { + const _StudyMembers({ + required this.study, + }); + + final StudyPageData study; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...study.members.map( + (member) => Text.rich( + maxLines: 1, + overflow: TextOverflow.ellipsis, + TextSpan( + children: [ + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Icon( + member.role == 'w' + ? LichessIcons.radio_tower_lichess + : Icons.remove_red_eye, + size: DefaultTextStyle.of(context).style.fontSize, + ), + ), + const TextSpan(text: ' '), + WidgetSpan( + alignment: PlaceholderAlignment.bottom, + child: UserFullNameWidget( + user: member.user, + showFlair: false, + ), + ), + ], + ), + ), + ), + ], + ); + } +} + +class _StudyFlair extends StatelessWidget { + const _StudyFlair({required this.flair, required this.size}); + + final String? flair; + + final double size; + + @override + Widget build(BuildContext context) { + final iconIfNoFlair = Icon( + LichessIcons.book_lichess, + size: size, + ); + + return (flair != null) + ? CachedNetworkImage( + imageUrl: lichessFlairSrc(flair!), + errorWidget: (_, __, ___) => iconIfNoFlair, + width: size, + height: size, + ) + : iconIfNoFlair; + } +} + +class _StudySubtitle extends StatelessWidget { + const _StudySubtitle({ + required this.study, + required this.style, + }); + + final StudyPageData study; + + final TextStyle style; + + @override + Widget build(BuildContext context) { + return Text.rich( + TextSpan( + children: [ + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Icon( + Icons.favorite_outline, + size: style.fontSize, + ), + ), + TextSpan(text: ' ${study.likes}', style: style), + TextSpan(text: ' • ', style: style), + if (study.owner != null) ...[ + WidgetSpan( + alignment: PlaceholderAlignment.bottom, + child: UserFullNameWidget( + user: study.owner, + style: style, + showFlair: false, + ), + ), + TextSpan(text: ' • ', style: style), + ], + TextSpan( + text: timeago.format( + study.updatedAt, + ), + style: style, + ), + ], + ), + ); + } +} diff --git a/lib/src/view/tools/tools_tab_screen.dart b/lib/src/view/tools/tools_tab_screen.dart index 62cd402965..9186e3f1b5 100644 --- a/lib/src/view/tools/tools_tab_screen.dart +++ b/lib/src/view/tools/tools_tab_screen.dart @@ -6,6 +6,7 @@ import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/navigation.dart'; import 'package:lichess_mobile/src/network/connectivity.dart'; +import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; @@ -14,6 +15,7 @@ import 'package:lichess_mobile/src/view/board_editor/board_editor_screen.dart'; import 'package:lichess_mobile/src/view/clock/clock_screen.dart'; import 'package:lichess_mobile/src/view/coordinate_training/coordinate_training_screen.dart'; import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_screen.dart'; +import 'package:lichess_mobile/src/view/study/study_list_screen.dart'; import 'package:lichess_mobile/src/view/tools/load_position_screen.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; @@ -175,6 +177,16 @@ class _Body extends ConsumerWidget { ) : null, ), + if (isOnline) + _ToolsButton( + icon: LichessIcons.book_lichess, + title: context.l10n.studyMenu, + onTap: () => pushPlatformRoute( + context, + builder: (context) => const StudyListScreen(), + rootNavigator: true, + ), + ), _ToolsButton( icon: Icons.edit_outlined, title: context.l10n.boardEditor, diff --git a/test/view/study/study_list_screen_test.dart b/test/view/study/study_list_screen_test.dart new file mode 100644 index 0000000000..1adec25903 --- /dev/null +++ b/test/view/study/study_list_screen_test.dart @@ -0,0 +1,694 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/testing.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/view/study/study_list_screen.dart'; + +import '../../test_helpers.dart'; +import '../../test_provider_scope.dart'; + +void main() { + group('Study list screen', () { + final mockClient = MockClient((request) { + if (request.url.path == '/study/all/hot') { + if (request.url.queryParameters['page'] == '1') { + return mockResponse(kStudyAllHotPage1Response, 200); + } + if (request.url.queryParameters['page'] == '2') { + return mockResponse(kStudyAllHotPage2Response, 200); + } + return mockResponse( + emptyPage(int.parse(request.url.queryParameters['page']!)), + 200, + ); + } + if (request.url.path == '/study/search') { + if (request.url.queryParameters['q'] == 'Magnus') { + return mockResponse( + ''' +{ + "paginator": { + "currentPage": 1, + "maxPerPage": 16, + "currentPageResults": [ + { + "id": "g26XbGpT", + "name": "Magnus Carlsen Games", + "liked": false, + "likes": 1, + "updatedAt": 1723817543350, + "owner": { + "name": "tom-anders", + "id": "tom-anders" + }, + "chapters": [ + "Chapter 1", + "Chapter 2" + ], + "topics": [ ], + "members": [ ] + } + ], + "previousPage": null, + "nextPage": null, + "nbResults": 1, + "nbPages": 1 + } +} + ''', + 200, + ); + } + } + return mockResponse('', 404); + }); + + testWidgets('Scrolling down loads next page', (WidgetTester tester) async { + final app = await makeTestProviderScopeApp( + tester, + home: const StudyListScreen(), + overrides: [ + lichessClientProvider.overrideWith((ref) { + return LichessClient(mockClient, ref); + }), + ], + ); + await tester.pumpWidget(app); + await tester.pumpAndSettle(); + + expect(find.text('First Study Page 1'), findsOneWidget); + expect(find.text('First Study Page 2'), findsNothing); // On page 2 + + // SearchBar also implements Scrollable, so we need to explicitly specify the ListView's Scrollable here + await tester.scrollUntilVisible( + find.text('First Study Page 2'), + 200, + scrollable: find.descendant( + of: find.byType(ListView), + matching: find.byType(Scrollable), + ), + ); + + await tester.pumpAndSettle(); + }); + + testWidgets('Searching', (WidgetTester tester) async { + final app = await makeTestProviderScopeApp( + tester, + home: const StudyListScreen(), + overrides: [ + lichessClientProvider.overrideWith((ref) { + return LichessClient(mockClient, ref); + }), + ], + ); + await tester.pumpWidget(app); + await tester.pumpAndSettle(); + + await tester.tap(find.byType(SearchBar)); + + await tester.enterText(find.byType(TextField), 'Magnus'); + // submit the search + await tester.testTextInput.receiveAction(TextInputAction.done); + + await tester.pumpAndSettle(); + + expect(find.text('Magnus Carlsen Games'), findsOneWidget); + expect(find.textContaining('Chapter 1'), findsOneWidget); + expect(find.textContaining('Chapter 2'), findsOneWidget); + expect(find.textContaining('tom-anders'), findsOneWidget); + + await tester.pumpAndSettle(); + }); + }); +} + +// Output based on the following command (with some modifications): +// curl -X GET 'https://lichess.dev/study/all/hot' -H "Accept: application/json" 2> /dev/null | jq +const kStudyAllHotPage1Response = ''' +{ + "paginator": { + "currentPage": 1, + "maxPerPage": 16, + "currentPageResults": [ + { + "id": "g26XbGpT", + "name": "First Study Page 1", + "liked": false, + "likes": 1, + "updatedAt": 1723817543350, + "owner": { + "name": "HeySerginho", + "id": "heyserginho" + }, + "chapters": [ + "Stevanic, David - Ilamparthi A R", + "Quizon, Daniel - Raahul V S" + ], + "topics": [ + "Broadcast" + ], + "members": [ + { + "user": { + "name": "HeySerginho", + "id": "heyserginho" + }, + "role": "w" + }, + { + "user": { + "name": "AAArmstark", + "flair": "activity.lichess-hogger", + "id": "aaarmstark" + }, + "role": "w" + } + ] + }, + { + "id": "6QZbnn0u", + "name": "test", + "liked": false, + "likes": 1, + "updatedAt": 1722185354120, + "owner": { + "name": "HeySerginho", + "id": "heyserginho" + }, + "chapters": [ + "Larkin, Vladyslav - Monteiro, Jose Macedo", + "Campora, Daniel H. - Pinto, Jose Joao Meireles Alves" + ], + "topics": [ + "Broadcast" + ], + "members": [ + { + "user": { + "name": "HeySerginho", + "id": "heyserginho" + }, + "role": "w" + } + ] + }, + { + "id": "Oc2oNWPH", + "name": "test", + "liked": false, + "likes": 1, + "updatedAt": 1722185242763, + "owner": { + "name": "HeySerginho", + "id": "heyserginho" + }, + "chapters": [ + "Larkin, Vladyslav - Monteiro, Jose Flavio", + "Campora, Daniel H. - Pinto, Fernando Jose Seixas" + ], + "topics": [ + "Broadcast" + ], + "members": [ + { + "user": { + "name": "HeySerginho", + "id": "heyserginho" + }, + "role": "w" + }, + { + "user": { + "name": "thibault", + "flair": "smileys.disguised-face", + "patron": true, + "id": "thibault" + }, + "role": "w" + } + ] + }, + { + "id": "WLpIyPTB", + "name": "Round 9", + "liked": false, + "likes": 1, + "updatedAt": 1722016933485, + "owner": { + "name": "AAArmstark", + "flair": "activity.lichess-hogger", + "id": "aaarmstark" + }, + "chapters": [ + "Madaminov, Mukhiddin - Macovei, Andrei", + "Gan-Erdene, Sugar - Assaubayeva, Bibisara", + "Chinguun, Sumiya - Materia, Marco", + "Sukandar, Irine Kharisma - Moiseenko, Alexander" + ], + "topics": [ + "Broadcast" + ], + "members": [ + { + "user": { + "name": "AAArmstark", + "flair": "activity.lichess-hogger", + "id": "aaarmstark" + }, + "role": "w" + } + ] + }, + { + "id": "7BaR9QW4", + "name": "Round 8", + "liked": false, + "likes": 1, + "updatedAt": 1722016933188, + "owner": { + "name": "AAArmstark", + "flair": "activity.lichess-hogger", + "id": "aaarmstark" + }, + "chapters": [ + "Assaubayeva, Bibisara - Madaminov, Mukhiddin", + "Chinguun, Sumiya - Gan-Erdene, Sugar", + "Sukandar, Irine Kharisma - Macovei, Andrei", + "Materia, Marco - Sasikiran, Krishnan" + ], + "topics": [ + "Broadcast" + ], + "members": [ + { + "user": { + "name": "AAArmstark", + "flair": "activity.lichess-hogger", + "id": "aaarmstark" + }, + "role": "w" + } + ] + }, + { + "id": "oZdBmHQG", + "name": "Round 7", + "liked": false, + "likes": 1, + "updatedAt": 1722016933000, + "owner": { + "name": "AAArmstark", + "flair": "activity.lichess-hogger", + "id": "aaarmstark" + }, + "chapters": [ + "Madaminov, Mukhiddin - Materia, Marco", + "Sasikiran, Krishnan - Sukandar, Irine Kharisma", + "Gan-Erdene, Sugar - Harsha Bharathakoti", + "Juksta, Karolis - Assaubayeva, Bibisara" + ], + "topics": [ + "Broadcast" + ], + "members": [ + { + "user": { + "name": "AAArmstark", + "flair": "activity.lichess-hogger", + "id": "aaarmstark" + }, + "role": "w" + } + ] + }, + { + "id": "wVTCciEa", + "name": "Round 6", + "liked": false, + "likes": 1, + "updatedAt": 1722016932803, + "owner": { + "name": "AAArmstark", + "flair": "activity.lichess-hogger", + "id": "aaarmstark" + }, + "chapters": [ + "Jumabayev, Rinat - Madaminov, Mukhiddin", + "Materia, Marco - Ganguly, Surya Shekhar", + "Nesterov, Arseniy - Gan-Erdene, Sugar", + "Kersten, Uwe - Sasikiran, Krishnan" + ], + "topics": [ + "Broadcast" + ], + "members": [ + { + "user": { + "name": "AAArmstark", + "flair": "activity.lichess-hogger", + "id": "aaarmstark" + }, + "role": "w" + } + ] + }, + { + "id": "9WB6pTtb", + "name": "Round 5", + "liked": false, + "likes": 1, + "updatedAt": 1722016932628, + "owner": { + "name": "AAArmstark", + "flair": "activity.lichess-hogger", + "id": "aaarmstark" + }, + "chapters": [ + "Madaminov, Mukhiddin - Nesterov, Arseniy", + "Jumabayev, Rinat - Materia, Marco", + "Munguntuul, Batkhuyag - Ganguly, Surya Shekhar", + "Sasikiran, Krishnan - Harsha Bharathakoti" + ], + "topics": [ + "Broadcast" + ], + "members": [ + { + "user": { + "name": "AAArmstark", + "flair": "activity.lichess-hogger", + "id": "aaarmstark" + }, + "role": "w" + } + ] + }, + { + "id": "vWxkJ9Dp", + "name": "Round 4", + "liked": false, + "likes": 1, + "updatedAt": 1722016932385, + "owner": { + "name": "AAArmstark", + "flair": "activity.lichess-hogger", + "id": "aaarmstark" + }, + "chapters": [ + "Gan-Erdene, Sugar - Sasikiran, Krishnan", + "Nesterov, Arseniy - Munkhzul, Turmunkh", + "Harsha Bharathakoti - Munguntuul, Batkhuyag", + "Tahay, Alexis - Jumabayev, Rinat" + ], + "topics": [ + "Broadcast" + ], + "members": [ + { + "user": { + "name": "AAArmstark", + "flair": "activity.lichess-hogger", + "id": "aaarmstark" + }, + "role": "w" + } + ] + }, + { + "id": "fJSDBhRQ", + "name": "Round 3", + "liked": false, + "likes": 1, + "updatedAt": 1722016932195, + "owner": { + "name": "AAArmstark", + "flair": "activity.lichess-hogger", + "id": "aaarmstark" + }, + "chapters": [ + "Sasikiran, Krishnan - Haimovich, Tal", + "Arcuti, Davide - Nesterov, Arseniy", + "Peycheva, Gergana - Harsha Bharathakoti", + "Jumabayev, Rinat - Kersten, Uwe" + ], + "topics": [ + "Broadcast" + ], + "members": [ + { + "user": { + "name": "AAArmstark", + "flair": "activity.lichess-hogger", + "id": "aaarmstark" + }, + "role": "w" + } + ] + }, + { + "id": "9xh4D5mM", + "name": "Round 2", + "liked": false, + "likes": 1, + "updatedAt": 1722016932070, + "owner": { + "name": "AAArmstark", + "flair": "activity.lichess-hogger", + "id": "aaarmstark" + }, + "chapters": [ + "Toktomushev, Teimur - Ganguly, Surya Shekhar", + "Moiseenko, Alexander - Vemparala, Nikash", + "Papaux, Steve - Sasikiran, Krishnan", + "Nesterov, Arseniy - Bex, Pierre-Alain" + ], + "topics": [ + "Broadcast" + ], + "members": [ + { + "user": { + "name": "AAArmstark", + "flair": "activity.lichess-hogger", + "id": "aaarmstark" + }, + "role": "w" + } + ] + }, + { + "id": "D7TAt3rj", + "name": "Round 1", + "liked": false, + "likes": 1, + "updatedAt": 1722016931939, + "owner": { + "name": "AAArmstark", + "flair": "activity.lichess-hogger", + "id": "aaarmstark" + }, + "chapters": [ + "Ganguly, Surya Shekhar - Lang, Fabian", + "Tcheau, Alain - Moiseenko, Alexander", + "Sasikiran, Krishnan - Arulanantham, Aneet", + "Ranieri, Pierpaolo - Nesterov, Arseniy" + ], + "topics": [ + "Broadcast" + ], + "members": [ + { + "user": { + "name": "AAArmstark", + "flair": "activity.lichess-hogger", + "id": "aaarmstark" + }, + "role": "w" + } + ] + }, + { + "id": "9rZ88BkF", + "name": "Round 8", + "liked": false, + "likes": 1, + "updatedAt": 1718628262858, + "owner": { + "name": "AAArmstark", + "flair": "activity.lichess-hogger", + "id": "aaarmstark" + }, + "chapters": [ + "Chapter 1" + ], + "topics": [ + "Broadcast" + ], + "members": [ + { + "user": { + "name": "AAArmstark", + "flair": "activity.lichess-hogger", + "id": "aaarmstark" + }, + "role": "w" + } + ] + }, + { + "id": "k79RXsyo", + "name": "Round 7", + "liked": false, + "likes": 1, + "updatedAt": 1718628262852, + "owner": { + "name": "AAArmstark", + "flair": "activity.lichess-hogger", + "id": "aaarmstark" + }, + "chapters": [ + "Chapter 1" + ], + "topics": [ + "Broadcast" + ], + "members": [ + { + "user": { + "name": "AAArmstark", + "flair": "activity.lichess-hogger", + "id": "aaarmstark" + }, + "role": "w" + } + ] + }, + { + "id": "yqPw9epZ", + "name": "Round 6", + "liked": false, + "likes": 1, + "updatedAt": 1718628262846, + "owner": { + "name": "AAArmstark", + "flair": "activity.lichess-hogger", + "id": "aaarmstark" + }, + "chapters": [ + "Chapter 1" + ], + "topics": [ + "Broadcast" + ], + "members": [ + { + "user": { + "name": "AAArmstark", + "flair": "activity.lichess-hogger", + "id": "aaarmstark" + }, + "role": "w" + } + ] + }, + { + "id": "wSLKGjre", + "name": "Last Study Page 1", + "liked": false, + "likes": 1, + "updatedAt": 1718628262840, + "owner": { + "name": "AAArmstark", + "flair": "activity.lichess-hogger", + "id": "aaarmstark" + }, + "chapters": [ + "Chapter 1" + ], + "topics": [ + "Broadcast" + ], + "members": [ + { + "user": { + "name": "AAArmstark", + "flair": "activity.lichess-hogger", + "id": "aaarmstark" + }, + "role": "w" + } + ] + } + ], + "previousPage": null, + "nextPage": 2, + "nbResults": 9999, + "nbPages": 625 + } +} +'''; + +const kStudyAllHotPage2Response = ''' +{ + "paginator": { + "currentPage": 1, + "maxPerPage": 16, + "currentPageResults": [ + { + "id": "g26XbGpT", + "name": "First Study Page 2", + "liked": false, + "likes": 1, + "updatedAt": 1723817543350, + "owner": { + "name": "HeySerginho", + "id": "heyserginho" + }, + "chapters": [ + "Stevanic, David - Ilamparthi A R", + "Quizon, Daniel - Raahul V S" + ], + "topics": [ + "Broadcast" + ], + "members": [ + { + "user": { + "name": "HeySerginho", + "id": "heyserginho" + }, + "role": "w" + }, + { + "user": { + "name": "AAArmstark", + "flair": "activity.lichess-hogger", + "id": "aaarmstark" + }, + "role": "w" + } + ] + } + ], + "previousPage": null, + "nextPage": 2, + "nbResults": 9999, + "nbPages": 625 + } +} +'''; + +String emptyPage(int page) => ''' +{ + "paginator": { + "currentPage": $page, + "maxPerPage": 16, + "currentPageResults": [], + "previousPage": null, + "nextPage": ${page + 1}, + "nbResults": 9999, + "nbPages": 625 + } +} +'''; From dd13d9987bd526335a2997aed5d2aef8a46540bf Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Fri, 25 Oct 2024 19:03:12 +0200 Subject: [PATCH 562/979] add empty study screen --- lib/src/view/study/study_list_screen.dart | 7 ++++++- lib/src/view/study/study_screen.dart | 16 ++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 lib/src/view/study/study_screen.dart diff --git a/lib/src/view/study/study_list_screen.dart b/lib/src/view/study/study_list_screen.dart index 8cb11da9e7..3b39513c4b 100644 --- a/lib/src/view/study/study_list_screen.dart +++ b/lib/src/view/study/study_list_screen.dart @@ -9,6 +9,8 @@ import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/lichess_assets.dart'; +import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/view/study/study_screen.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/filter.dart'; @@ -297,7 +299,10 @@ class _StudyListState extends ConsumerState<_StudyList> { ], ), ), - onTap: () {}, + onTap: () => pushPlatformRoute( + context, + builder: (context) => StudyScreen(id: study.id), + ), ); }, ), diff --git a/lib/src/view/study/study_screen.dart b/lib/src/view/study/study_screen.dart new file mode 100644 index 0000000000..5e954b702f --- /dev/null +++ b/lib/src/view/study/study_screen.dart @@ -0,0 +1,16 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; + +class StudyScreen extends ConsumerWidget { + const StudyScreen({ + required this.id, + }); + + final StudyId id; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return const SizedBox.shrink(); + } +} From 9a6da70af341a0aafbca375db950ade791edd95a Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Sun, 27 Oct 2024 20:13:25 +0100 Subject: [PATCH 563/979] use ref.watch instead of ref.read --- lib/src/view/study/study_list_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/view/study/study_list_screen.dart b/lib/src/view/study/study_list_screen.dart index 3b39513c4b..e2f41e82ea 100644 --- a/lib/src/view/study/study_list_screen.dart +++ b/lib/src/view/study/study_list_screen.dart @@ -49,7 +49,7 @@ class StudyListScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final isLoggedIn = ref.read(authSessionProvider)?.user.id != null; + final isLoggedIn = ref.watch(authSessionProvider)?.user.id != null; final filter = ref.watch(studyFilterProvider); final categorySection = From 0d95984b6d9f60eeccfc9e659a514530d36eb221 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Sun, 27 Oct 2024 21:42:39 +0100 Subject: [PATCH 564/979] add PlatformSearchBar --- lib/src/view/study/study_list_screen.dart | 21 ++---- lib/src/view/user/player_screen.dart | 29 +++------ lib/src/view/user/search_screen.dart | 66 +++++++------------ lib/src/widgets/platform_search_bar.dart | 78 +++++++++++++++++++++++ 4 files changed, 116 insertions(+), 78 deletions(-) create mode 100644 lib/src/widgets/platform_search_bar.dart diff --git a/lib/src/view/study/study_list_screen.dart b/lib/src/view/study/study_list_screen.dart index e2f41e82ea..35199a249b 100644 --- a/lib/src/view/study/study_list_screen.dart +++ b/lib/src/view/study/study_list_screen.dart @@ -16,6 +16,7 @@ import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/filter.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; +import 'package:lichess_mobile/src/widgets/platform_search_bar.dart'; import 'package:lichess_mobile/src/widgets/user_full_name.dart'; import 'package:logging/logging.dart'; import 'package:timeago/timeago.dart' as timeago; @@ -159,22 +160,12 @@ class _BodyState extends State<_Body> { children: [ Padding( padding: Styles.bodySectionPadding, - child: SearchBar( + child: PlatformSearchBar( controller: _searchController, - leading: const Icon(Icons.search), - trailing: [ - if (search != null) - IconButton( - onPressed: () => setState(() { - search = null; - _searchController.clear(); - }), - tooltip: 'Clear', - icon: const Icon( - Icons.close, - ), - ), - ], + onClear: () => setState(() { + search = null; + _searchController.clear(); + }), hintText: search ?? context.l10n.searchSearch, onSubmitted: (term) { setState(() { diff --git a/lib/src/view/user/player_screen.dart b/lib/src/view/user/player_screen.dart index 7cdc48637b..fd845720f2 100644 --- a/lib/src/view/user/player_screen.dart +++ b/lib/src/view/user/player_screen.dart @@ -1,5 +1,4 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; @@ -16,8 +15,8 @@ import 'package:lichess_mobile/src/view/user/search_screen.dart'; import 'package:lichess_mobile/src/view/user/user_screen.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; +import 'package:lichess_mobile/src/widgets/platform_search_bar.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; import 'package:lichess_mobile/src/widgets/user_full_name.dart'; @@ -80,25 +79,13 @@ class _SearchButton extends StatelessWidget { builder: (ctx) => UserScreen(user: user), ); - return PlatformWidget( - androidBuilder: (context) => SearchBar( - leading: const Icon(Icons.search), - hintText: context.l10n.searchSearch, - focusNode: AlwaysDisabledFocusNode(), - onTap: () => pushPlatformRoute( - context, - fullscreenDialog: true, - builder: (_) => SearchScreen(onUserTap: onUserTap), - ), - ), - iosBuilder: (context) => CupertinoSearchTextField( - placeholder: context.l10n.searchSearch, - focusNode: AlwaysDisabledFocusNode(), - onTap: () => pushPlatformRoute( - context, - fullscreenDialog: true, - builder: (_) => SearchScreen(onUserTap: onUserTap), - ), + return PlatformSearchBar( + hintText: context.l10n.searchSearch, + focusNode: AlwaysDisabledFocusNode(), + onTap: () => pushPlatformRoute( + context, + fullscreenDialog: true, + builder: (_) => SearchScreen(onUserTap: onUserTap), ), ); } diff --git a/lib/src/view/user/search_screen.dart b/lib/src/view/user/search_screen.dart index 30f52660db..790729686f 100644 --- a/lib/src/view/user/search_screen.dart +++ b/lib/src/view/user/search_screen.dart @@ -11,6 +11,7 @@ import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_search_bar.dart'; import 'package:lichess_mobile/src/widgets/user_list_tile.dart'; const _kSaveHistoryDebouncTimer = Duration(seconds: 2); @@ -70,55 +71,36 @@ class _SearchScreenState extends ConsumerState { @override Widget build(BuildContext context) { - return PlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, + final searchBar = PlatformSearchBar( + hintText: context.l10n.searchSearch, + controller: _searchController, + autoFocus: true, ); - } - Widget _androidBuilder(BuildContext context) { - return Scaffold( - appBar: AppBar( - toolbarHeight: 80, // Custom height to fit the search bar - title: SearchBar( - leading: const Icon(Icons.search), - trailing: [ - if (_searchController.text.isNotEmpty) - IconButton( - onPressed: () => _searchController.clear(), - tooltip: 'Clear', - icon: const Icon( - Icons.close, - ), - ), - ], - hintText: context.l10n.searchSearch, - controller: _searchController, - autoFocus: true, + final body = _Body(_term, setSearchText, widget.onUserTap); + + return PlatformWidget( + androidBuilder: (context) => Scaffold( + appBar: AppBar( + toolbarHeight: 80, // Custom height to fit the search bar + title: searchBar, ), + body: body, ), - body: _Body(_term, setSearchText, widget.onUserTap), - ); - } - - Widget _iosBuilder(BuildContext context) { - return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - automaticallyImplyLeading: false, - middle: SizedBox( - height: 36.0, - child: CupertinoSearchTextField( - placeholder: context.l10n.searchSearch, - controller: _searchController, - autofocus: true, + iosBuilder: (context) => CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + automaticallyImplyLeading: false, + middle: SizedBox( + height: 36.0, + child: searchBar, + ), + trailing: NoPaddingTextButton( + child: Text(context.l10n.close), + onPressed: () => Navigator.pop(context), ), ), - trailing: NoPaddingTextButton( - child: Text(context.l10n.close), - onPressed: () => Navigator.pop(context), - ), + child: body, ), - child: _Body(_term, setSearchText, widget.onUserTap), ); } } diff --git a/lib/src/widgets/platform_search_bar.dart b/lib/src/widgets/platform_search_bar.dart new file mode 100644 index 0000000000..a5b67b1e86 --- /dev/null +++ b/lib/src/widgets/platform_search_bar.dart @@ -0,0 +1,78 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:lichess_mobile/src/widgets/platform.dart'; + +/// Displays a [SearchBar] on Android and a [CupertinoSearchTextField] on iOS. +class PlatformSearchBar extends StatelessWidget { + const PlatformSearchBar({ + this.controller, + this.hintText, + this.autoFocus = false, + this.onClear, + this.onTap, + this.onSubmitted, + this.focusNode, + }); + + /// See [SearchBar.controller] and [CupertinoSearchTextField.controller]. + final TextEditingController? controller; + + /// Text that suggests what sort of input the field accepts. + /// + /// Displayed at the same location on the screen where text may be entered + /// when the input is empty. + final String? hintText; + + /// Whether this the search should focus itself if nothing else is already focused. + /// + /// Defaults to false. + final bool autoFocus; + + /// Called when the user taps this search bar. + final GestureTapCallback? onTap; + + /// Callback when the clear button is pressed. + /// + /// Defaults to clearing the text in the [controller]. + final VoidCallback? onClear; + + /// Callback when the search term is submitted. + final void Function(String term)? onSubmitted; + + /// {@macro flutter.widgets.Focus.focusNode} + final FocusNode? focusNode; + + @override + Widget build(BuildContext context) { + return PlatformWidget( + androidBuilder: (context) => SearchBar( + controller: controller, + leading: const Icon(Icons.search), + trailing: [ + if (controller?.text.isNotEmpty == true) + IconButton( + onPressed: onClear ?? () => controller?.clear(), + tooltip: 'Clear', + icon: const Icon( + Icons.close, + ), + ), + ], + onTap: onTap, + focusNode: focusNode, + onSubmitted: onSubmitted, + hintText: hintText, + autoFocus: autoFocus, + ), + iosBuilder: (context) => CupertinoSearchTextField( + controller: controller, + onTap: onTap, + focusNode: focusNode, + onSuffixTap: onClear, + onSubmitted: onSubmitted, + placeholder: hintText, + autofocus: autoFocus, + ), + ); + } +} From 83fa2db64b0cbba966d4d8952442e6546730065a Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Sun, 27 Oct 2024 22:11:22 +0100 Subject: [PATCH 565/979] request next page already when scrolled >= 75% --- lib/src/view/study/study_list_screen.dart | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/lib/src/view/study/study_list_screen.dart b/lib/src/view/study/study_list_screen.dart index 35199a249b..27e21c9401 100644 --- a/lib/src/view/study/study_list_screen.dart +++ b/lib/src/view/study/study_list_screen.dart @@ -198,7 +198,9 @@ class _StudyList extends ConsumerStatefulWidget { } class _StudyListState extends ConsumerState<_StudyList> { - final _scrollController = ScrollController(); + final _scrollController = ScrollController(keepScrollOffset: true); + + bool requestedNextPage = false; @override void initState() { @@ -214,11 +216,16 @@ class _StudyListState extends ConsumerState<_StudyList> { } void _scrollListener() { - if (_scrollController.position.pixels == - _scrollController.position.maxScrollExtent) { + if (!requestedNextPage && + _scrollController.position.pixels >= + 0.75 * _scrollController.position.maxScrollExtent) { final studiesList = ref.read(widget.paginatorProvider); if (!studiesList.isLoading) { + setState(() { + requestedNextPage = true; + }); + ref.read(widget.paginatorProvider.notifier).next(); } } @@ -226,6 +233,16 @@ class _StudyListState extends ConsumerState<_StudyList> { @override Widget build(BuildContext context) { + ref.listen(widget.paginatorProvider, (prev, next) { + if (prev?.value?.nextPage != next.value?.nextPage) { + WidgetsBinding.instance.addPostFrameCallback((_) { + setState(() { + requestedNextPage = false; + }); + }); + } + }); + final studiesAsync = ref.watch(widget.paginatorProvider); return studiesAsync.when( From dc5b102742ea4ce13100c7318ba7a052f3388771 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Tue, 29 Oct 2024 10:41:54 +0100 Subject: [PATCH 566/979] use LichessIcons.study instead of LichessIcons.book_lichess --- lib/src/view/study/study_list_screen.dart | 2 +- lib/src/view/tools/tools_tab_screen.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/view/study/study_list_screen.dart b/lib/src/view/study/study_list_screen.dart index 27e21c9401..6ae022cea4 100644 --- a/lib/src/view/study/study_list_screen.dart +++ b/lib/src/view/study/study_list_screen.dart @@ -417,7 +417,7 @@ class _StudyFlair extends StatelessWidget { @override Widget build(BuildContext context) { final iconIfNoFlair = Icon( - LichessIcons.book_lichess, + LichessIcons.study, size: size, ); diff --git a/lib/src/view/tools/tools_tab_screen.dart b/lib/src/view/tools/tools_tab_screen.dart index 9186e3f1b5..b071761fd4 100644 --- a/lib/src/view/tools/tools_tab_screen.dart +++ b/lib/src/view/tools/tools_tab_screen.dart @@ -179,7 +179,7 @@ class _Body extends ConsumerWidget { ), if (isOnline) _ToolsButton( - icon: LichessIcons.book_lichess, + icon: LichessIcons.study, title: context.l10n.studyMenu, onTap: () => pushPlatformRoute( context, From a6a9b006464e9ebc6652231af440bbc2458e5e4f Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Tue, 29 Oct 2024 10:42:30 +0100 Subject: [PATCH 567/979] remove redundant watch() --- lib/src/view/study/study_list_screen.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/src/view/study/study_list_screen.dart b/lib/src/view/study/study_list_screen.dart index 6ae022cea4..edf155b7ec 100644 --- a/lib/src/view/study/study_list_screen.dart +++ b/lib/src/view/study/study_list_screen.dart @@ -75,9 +75,7 @@ class StudyListScreen extends ConsumerWidget { ), ], ), - body: _Body( - filter: ref.watch(studyFilterProvider), - ), + body: _Body(filter: filter), ); } } From adc73d7646fa4d808188c9bb7f0cd421e677fb34 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Tue, 29 Oct 2024 10:43:34 +0100 Subject: [PATCH 568/979] move Expanded() upwards --- lib/src/view/study/study_list_screen.dart | 128 +++++++++++----------- 1 file changed, 64 insertions(+), 64 deletions(-) diff --git a/lib/src/view/study/study_list_screen.dart b/lib/src/view/study/study_list_screen.dart index edf155b7ec..7ef26b4d1b 100644 --- a/lib/src/view/study/study_list_screen.dart +++ b/lib/src/view/study/study_list_screen.dart @@ -172,10 +172,12 @@ class _BodyState extends State<_Body> { }, ), ), - _StudyList( - paginatorProvider: StudyListPaginatorProvider( - filter: widget.filter, - search: search, + Expanded( + child: _StudyList( + paginatorProvider: StudyListPaginatorProvider( + filter: widget.filter, + search: search, + ), ), ), ], @@ -245,73 +247,71 @@ class _StudyListState extends ConsumerState<_StudyList> { return studiesAsync.when( data: (studies) { - return Expanded( - child: ListView.separated( - controller: _scrollController, - itemCount: studies.studies.length, - separatorBuilder: (context, index) => const PlatformDivider( - height: 1, - cupertinoHasLeading: true, - ), - itemBuilder: (context, index) { - final study = studies.studies[index]; - return PlatformListTile( - padding: Styles.bodyPadding, - title: Row( + return ListView.separated( + controller: _scrollController, + itemCount: studies.studies.length, + separatorBuilder: (context, index) => const PlatformDivider( + height: 1, + cupertinoHasLeading: true, + ), + itemBuilder: (context, index) { + final study = studies.studies[index]; + return PlatformListTile( + padding: Styles.bodyPadding, + title: Row( + children: [ + _StudyFlair( + flair: study.flair, + size: 30, + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + study.name, + overflow: TextOverflow.ellipsis, + maxLines: 2, + ), + _StudySubtitle( + study: study, + style: const TextStyle( + fontSize: 12, + color: Colors.grey, + ), + ), + ], + ), + ), + ], + ), + subtitle: DefaultTextStyle.merge( + style: const TextStyle( + fontSize: 12, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - _StudyFlair( - flair: study.flair, - size: 30, + Expanded( + flex: 4, + child: _StudyChapters(study: study), ), - const SizedBox(width: 10), Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - study.name, - overflow: TextOverflow.ellipsis, - maxLines: 2, - ), - _StudySubtitle( - study: study, - style: const TextStyle( - fontSize: 12, - color: Colors.grey, - ), - ), - ], + flex: 3, + child: _StudyMembers( + study: study, ), ), ], ), - subtitle: DefaultTextStyle.merge( - style: const TextStyle( - fontSize: 12, - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - flex: 4, - child: _StudyChapters(study: study), - ), - Expanded( - flex: 3, - child: _StudyMembers( - study: study, - ), - ), - ], - ), - ), - onTap: () => pushPlatformRoute( - context, - builder: (context) => StudyScreen(id: study.id), - ), - ); - }, - ), + ), + onTap: () => pushPlatformRoute( + context, + builder: (context) => StudyScreen(id: study.id), + ), + ); + }, ); }, loading: () { From 2da7c27bc43a23f314dc5bec61da6171e3408abc Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Wed, 30 Oct 2024 21:56:00 +0100 Subject: [PATCH 569/979] make search bar non-sticky --- lib/src/view/study/study_list_screen.dart | 128 ++++++++++---------- test/view/study/study_list_screen_test.dart | 12 +- 2 files changed, 67 insertions(+), 73 deletions(-) diff --git a/lib/src/view/study/study_list_screen.dart b/lib/src/view/study/study_list_screen.dart index 7ef26b4d1b..1f67a5edff 100644 --- a/lib/src/view/study/study_list_screen.dart +++ b/lib/src/view/study/study_list_screen.dart @@ -129,7 +129,7 @@ class _StudyFilterSheet extends ConsumerWidget { } } -class _Body extends StatefulWidget { +class _Body extends ConsumerStatefulWidget { const _Body({ required this.filter, }); @@ -137,71 +137,24 @@ class _Body extends StatefulWidget { final StudyFilterState filter; @override - State<_Body> createState() => _BodyState(); + ConsumerState<_Body> createState() => _BodyState(); } -class _BodyState extends State<_Body> { +class _BodyState extends ConsumerState<_Body> { String? search; final _searchController = SearchController(); - @override - void dispose() { - super.dispose(); - _searchController.dispose(); - } - - @override - Widget build(BuildContext context) { - return SafeArea( - child: Column( - children: [ - Padding( - padding: Styles.bodySectionPadding, - child: PlatformSearchBar( - controller: _searchController, - onClear: () => setState(() { - search = null; - _searchController.clear(); - }), - hintText: search ?? context.l10n.searchSearch, - onSubmitted: (term) { - setState(() { - search = term; - }); - }, - ), - ), - Expanded( - child: _StudyList( - paginatorProvider: StudyListPaginatorProvider( - filter: widget.filter, - search: search, - ), - ), - ), - ], - ), - ); - } -} - -class _StudyList extends ConsumerStatefulWidget { - const _StudyList({ - required this.paginatorProvider, - }); - - final StudyListPaginatorProvider paginatorProvider; - - @override - ConsumerState createState() => _StudyListState(); -} - -class _StudyListState extends ConsumerState<_StudyList> { final _scrollController = ScrollController(keepScrollOffset: true); bool requestedNextPage = false; + StudyListPaginatorProvider get paginatorProvider => + StudyListPaginatorProvider( + filter: widget.filter, + search: search, + ); + @override void initState() { super.initState(); @@ -212,6 +165,7 @@ class _StudyListState extends ConsumerState<_StudyList> { void dispose() { _scrollController.removeListener(_scrollListener); _scrollController.dispose(); + _searchController.dispose(); super.dispose(); } @@ -219,37 +173,83 @@ class _StudyListState extends ConsumerState<_StudyList> { if (!requestedNextPage && _scrollController.position.pixels >= 0.75 * _scrollController.position.maxScrollExtent) { - final studiesList = ref.read(widget.paginatorProvider); + final studiesList = ref.read(paginatorProvider); if (!studiesList.isLoading) { setState(() { requestedNextPage = true; }); - ref.read(widget.paginatorProvider.notifier).next(); + ref.read(paginatorProvider.notifier).next(); } } } @override Widget build(BuildContext context) { - ref.listen(widget.paginatorProvider, (prev, next) { + ref.listen(paginatorProvider, (prev, next) { if (prev?.value?.nextPage != next.value?.nextPage) { WidgetsBinding.instance.addPostFrameCallback((_) { - setState(() { - requestedNextPage = false; - }); + if (mounted) { + setState(() { + requestedNextPage = false; + }); + } }); } }); - final studiesAsync = ref.watch(widget.paginatorProvider); + return SafeArea( + child: SingleChildScrollView( + controller: _scrollController, + child: Column( + children: [ + Padding( + padding: Styles.bodySectionPadding, + child: PlatformSearchBar( + controller: _searchController, + onClear: () => setState(() { + search = null; + _searchController.clear(); + }), + hintText: search ?? context.l10n.searchSearch, + onSubmitted: (term) { + setState(() { + search = term; + }); + }, + ), + ), + _StudyList( + paginatorProvider: StudyListPaginatorProvider( + filter: widget.filter, + search: search, + ), + ), + ], + ), + ), + ); + } +} + +class _StudyList extends ConsumerWidget { + const _StudyList({ + required this.paginatorProvider, + }); + + final StudyListPaginatorProvider paginatorProvider; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final studiesAsync = ref.watch(paginatorProvider); return studiesAsync.when( data: (studies) { return ListView.separated( - controller: _scrollController, + shrinkWrap: true, itemCount: studies.studies.length, + primary: false, separatorBuilder: (context, index) => const PlatformDivider( height: 1, cupertinoHasLeading: true, diff --git a/test/view/study/study_list_screen_test.dart b/test/view/study/study_list_screen_test.dart index 1adec25903..bc9f3ef901 100644 --- a/test/view/study/study_list_screen_test.dart +++ b/test/view/study/study_list_screen_test.dart @@ -79,17 +79,11 @@ void main() { expect(find.text('First Study Page 1'), findsOneWidget); expect(find.text('First Study Page 2'), findsNothing); // On page 2 - // SearchBar also implements Scrollable, so we need to explicitly specify the ListView's Scrollable here - await tester.scrollUntilVisible( + await tester.dragUntilVisible( find.text('First Study Page 2'), - 200, - scrollable: find.descendant( - of: find.byType(ListView), - matching: find.byType(Scrollable), - ), + find.byType(SingleChildScrollView), + const Offset(0, -250), ); - - await tester.pumpAndSettle(); }); testWidgets('Searching', (WidgetTester tester) async { From e10929c942312c11e565c717e34e1bc9a3113bfb Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Wed, 30 Oct 2024 22:47:36 +0100 Subject: [PATCH 570/979] add comment about pump reason --- test/view/study/study_list_screen_test.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/view/study/study_list_screen_test.dart b/test/view/study/study_list_screen_test.dart index bc9f3ef901..be03051356 100644 --- a/test/view/study/study_list_screen_test.dart +++ b/test/view/study/study_list_screen_test.dart @@ -74,7 +74,9 @@ void main() { ], ); await tester.pumpWidget(app); - await tester.pumpAndSettle(); + + // Wait for study list to load + await tester.pump(); expect(find.text('First Study Page 1'), findsOneWidget); expect(find.text('First Study Page 2'), findsNothing); // On page 2 From 2a38983fce2ab8a869a16ef60f2bd6a8ae477c78 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Wed, 30 Oct 2024 23:13:52 +0100 Subject: [PATCH 571/979] add stricter verification of http requests --- test/view/study/study_list_screen_test.dart | 120 ++++++++++++-------- 1 file changed, 71 insertions(+), 49 deletions(-) diff --git a/test/view/study/study_list_screen_test.dart b/test/view/study/study_list_screen_test.dart index be03051356..eb77bd0d96 100644 --- a/test/view/study/study_list_screen_test.dart +++ b/test/view/study/study_list_screen_test.dart @@ -9,23 +9,68 @@ import '../../test_provider_scope.dart'; void main() { group('Study list screen', () { - final mockClient = MockClient((request) { - if (request.url.path == '/study/all/hot') { - if (request.url.queryParameters['page'] == '1') { - return mockResponse(kStudyAllHotPage1Response, 200); - } - if (request.url.queryParameters['page'] == '2') { - return mockResponse(kStudyAllHotPage2Response, 200); - } - return mockResponse( - emptyPage(int.parse(request.url.queryParameters['page']!)), - 200, - ); - } - if (request.url.path == '/study/search') { - if (request.url.queryParameters['q'] == 'Magnus') { + testWidgets('Scrolling down loads next page', (WidgetTester tester) async { + final requestedPages = []; + final mockClient = MockClient((request) { + if (request.url.path == '/study/all/hot') { + requestedPages.add(int.parse(request.url.queryParameters['page']!)); + + if (request.url.queryParameters['page'] == '1') { + return mockResponse(kStudyAllHotPage1Response, 200); + } + if (request.url.queryParameters['page'] == '2') { + return mockResponse(kStudyAllHotPage2Response, 200); + } + + // Page 2 only contains a single item, so should also load page 3 return mockResponse( - ''' + emptyPage(int.parse(request.url.queryParameters['page']!)), + 200, + ); + } + return mockResponse('', 404); + }); + + final app = await makeTestProviderScopeApp( + tester, + home: const StudyListScreen(), + overrides: [ + lichessClientProvider.overrideWith((ref) { + return LichessClient(mockClient, ref); + }), + ], + ); + await tester.pumpWidget(app); + + // Wait for study list to load + await tester.pump(); + + expect(find.text('First Study Page 1'), findsOneWidget); + expect(find.text('First Study Page 2'), findsNothing); // On page 2 + + await tester.dragUntilVisible( + find.text('First Study Page 2'), + find.byType(SingleChildScrollView), + const Offset(0, -250), + ); + + // Wait for page 3 to load + await tester.pumpAndSettle(); + + expect(requestedPages, [1, 2, 3]); + }); + + testWidgets('Searching', (WidgetTester tester) async { + final requestedUrls = []; + final mockClient = MockClient((request) { + requestedUrls.add(request.url.toString()); + if (request.url.path == '/study/all/hot' && + request.url.queryParameters['page'] == '1') { + return mockResponse(kStudyAllHotPage1Response, 200); + } else if (request.url.path == '/study/search') { + if (request.url.queryParameters['q'] == 'Magnus') { + return mockResponse( + ''' { "paginator": { "currentPage": 1, @@ -56,14 +101,13 @@ void main() { } } ''', - 200, - ); + 200, + ); + } } - } - return mockResponse('', 404); - }); + return mockResponse('', 404); + }); - testWidgets('Scrolling down loads next page', (WidgetTester tester) async { final app = await makeTestProviderScopeApp( tester, home: const StudyListScreen(), @@ -75,38 +119,13 @@ void main() { ); await tester.pumpWidget(app); - // Wait for study list to load - await tester.pump(); - - expect(find.text('First Study Page 1'), findsOneWidget); - expect(find.text('First Study Page 2'), findsNothing); // On page 2 - - await tester.dragUntilVisible( - find.text('First Study Page 2'), - find.byType(SingleChildScrollView), - const Offset(0, -250), - ); - }); - - testWidgets('Searching', (WidgetTester tester) async { - final app = await makeTestProviderScopeApp( - tester, - home: const StudyListScreen(), - overrides: [ - lichessClientProvider.overrideWith((ref) { - return LichessClient(mockClient, ref); - }), - ], - ); - await tester.pumpWidget(app); - await tester.pumpAndSettle(); - await tester.tap(find.byType(SearchBar)); await tester.enterText(find.byType(TextField), 'Magnus'); // submit the search await tester.testTextInput.receiveAction(TextInputAction.done); + // Wait for search results to load await tester.pumpAndSettle(); expect(find.text('Magnus Carlsen Games'), findsOneWidget); @@ -114,7 +133,10 @@ void main() { expect(find.textContaining('Chapter 2'), findsOneWidget); expect(find.textContaining('tom-anders'), findsOneWidget); - await tester.pumpAndSettle(); + expect(requestedUrls, [ + 'https://lichess.dev/study/all/hot?page=1', + 'https://lichess.dev/study/search?page=1&q=Magnus', + ]); }); }); } From e997390266d645f4c53aaf2a0a200155c320aeac Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Thu, 31 Oct 2024 22:12:19 +0100 Subject: [PATCH 572/979] move SearchBar into ListView --- lib/src/view/study/study_list_screen.dart | 89 +++++++++------------ test/view/study/study_list_screen_test.dart | 2 +- 2 files changed, 39 insertions(+), 52 deletions(-) diff --git a/lib/src/view/study/study_list_screen.dart b/lib/src/view/study/study_list_screen.dart index 1f67a5edff..58c37d4191 100644 --- a/lib/src/view/study/study_list_screen.dart +++ b/lib/src/view/study/study_list_screen.dart @@ -75,7 +75,7 @@ class StudyListScreen extends ConsumerWidget { ), ], ), - body: _Body(filter: filter), + body: SafeArea(child: _Body(filter: filter)), ); } } @@ -199,63 +199,43 @@ class _BodyState extends ConsumerState<_Body> { } }); - return SafeArea( - child: SingleChildScrollView( - controller: _scrollController, - child: Column( - children: [ - Padding( - padding: Styles.bodySectionPadding, - child: PlatformSearchBar( - controller: _searchController, - onClear: () => setState(() { - search = null; - _searchController.clear(); - }), - hintText: search ?? context.l10n.searchSearch, - onSubmitted: (term) { - setState(() { - search = term; - }); - }, - ), - ), - _StudyList( - paginatorProvider: StudyListPaginatorProvider( - filter: widget.filter, - search: search, - ), - ), - ], - ), + final studiesAsync = ref.watch(paginatorProvider); + + final searchBar = Padding( + padding: Styles.bodySectionPadding, + child: PlatformSearchBar( + controller: _searchController, + onClear: () => setState(() { + search = null; + _searchController.clear(); + }), + hintText: search ?? context.l10n.searchSearch, + onSubmitted: (term) { + setState(() { + search = term; + }); + }, ), ); - } -} - -class _StudyList extends ConsumerWidget { - const _StudyList({ - required this.paginatorProvider, - }); - - final StudyListPaginatorProvider paginatorProvider; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final studiesAsync = ref.watch(paginatorProvider); return studiesAsync.when( data: (studies) { return ListView.separated( shrinkWrap: true, - itemCount: studies.studies.length, - primary: false, - separatorBuilder: (context, index) => const PlatformDivider( - height: 1, - cupertinoHasLeading: true, - ), + itemCount: studies.studies.length + 1, + controller: _scrollController, + separatorBuilder: (context, index) => index == 0 + ? const SizedBox.shrink() + : const PlatformDivider( + height: 1, + cupertinoHasLeading: true, + ), itemBuilder: (context, index) { - final study = studies.studies[index]; + if (index == 0) { + return searchBar; + } + + final study = studies.studies[index - 1]; return PlatformListTile( padding: Styles.bodyPadding, title: Row( @@ -315,7 +295,14 @@ class _StudyList extends ConsumerWidget { ); }, loading: () { - return const Center(child: CircularProgressIndicator.adaptive()); + return Column( + children: [ + searchBar, + const Expanded( + child: Center(child: CircularProgressIndicator.adaptive()), + ), + ], + ); }, error: (error, stack) { _logger.severe('Error loading studies', error, stack); diff --git a/test/view/study/study_list_screen_test.dart b/test/view/study/study_list_screen_test.dart index eb77bd0d96..b4ea96ae9b 100644 --- a/test/view/study/study_list_screen_test.dart +++ b/test/view/study/study_list_screen_test.dart @@ -50,7 +50,7 @@ void main() { await tester.dragUntilVisible( find.text('First Study Page 2'), - find.byType(SingleChildScrollView), + find.byType(ListView), const Offset(0, -250), ); From 28924b2b811ae3389696e96654ca520daad8e392 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Thu, 31 Oct 2024 22:14:57 +0100 Subject: [PATCH 573/979] factor out StudyListItem widget --- lib/src/view/study/study_list_screen.dart | 132 ++++++++++++---------- 1 file changed, 70 insertions(+), 62 deletions(-) diff --git a/lib/src/view/study/study_list_screen.dart b/lib/src/view/study/study_list_screen.dart index 58c37d4191..e1ffabca41 100644 --- a/lib/src/view/study/study_list_screen.dart +++ b/lib/src/view/study/study_list_screen.dart @@ -230,68 +230,9 @@ class _BodyState extends ConsumerState<_Body> { height: 1, cupertinoHasLeading: true, ), - itemBuilder: (context, index) { - if (index == 0) { - return searchBar; - } - - final study = studies.studies[index - 1]; - return PlatformListTile( - padding: Styles.bodyPadding, - title: Row( - children: [ - _StudyFlair( - flair: study.flair, - size: 30, - ), - const SizedBox(width: 10), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - study.name, - overflow: TextOverflow.ellipsis, - maxLines: 2, - ), - _StudySubtitle( - study: study, - style: const TextStyle( - fontSize: 12, - color: Colors.grey, - ), - ), - ], - ), - ), - ], - ), - subtitle: DefaultTextStyle.merge( - style: const TextStyle( - fontSize: 12, - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - flex: 4, - child: _StudyChapters(study: study), - ), - Expanded( - flex: 3, - child: _StudyMembers( - study: study, - ), - ), - ], - ), - ), - onTap: () => pushPlatformRoute( - context, - builder: (context) => StudyScreen(id: study.id), - ), - ); - }, + itemBuilder: (context, index) => index == 0 + ? searchBar + : _StudyListItem(study: studies.studies[index - 1]), ); }, loading: () { @@ -312,6 +253,73 @@ class _BodyState extends ConsumerState<_Body> { } } +class _StudyListItem extends StatelessWidget { + const _StudyListItem({ + required this.study, + }); + + final StudyPageData study; + + @override + Widget build(BuildContext context) { + return PlatformListTile( + padding: Styles.bodyPadding, + title: Row( + children: [ + _StudyFlair( + flair: study.flair, + size: 30, + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + study.name, + overflow: TextOverflow.ellipsis, + maxLines: 2, + ), + _StudySubtitle( + study: study, + style: const TextStyle( + fontSize: 12, + color: Colors.grey, + ), + ), + ], + ), + ), + ], + ), + subtitle: DefaultTextStyle.merge( + style: const TextStyle( + fontSize: 12, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 4, + child: _StudyChapters(study: study), + ), + Expanded( + flex: 3, + child: _StudyMembers( + study: study, + ), + ), + ], + ), + ), + onTap: () => pushPlatformRoute( + context, + builder: (context) => StudyScreen(id: study.id), + ), + ); + } +} + class _StudyChapters extends StatelessWidget { const _StudyChapters({ required this.study, From b108d1ce409d84204e100ff9dc813f1351b3b7de Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Thu, 31 Oct 2024 22:58:57 +0100 Subject: [PATCH 574/979] correctly handle last page of paginated API old code would keep requesting pages even if the API returns "nextPage": null. --- lib/src/model/study/study_list_paginator.dart | 6 ++-- lib/src/model/study/study_repository.dart | 20 ++++++----- test/view/study/study_list_screen_test.dart | 33 +++---------------- 3 files changed, 20 insertions(+), 39 deletions(-) diff --git a/lib/src/model/study/study_list_paginator.dart b/lib/src/model/study/study_list_paginator.dart index 3f3d6bb068..5d22690a38 100644 --- a/lib/src/model/study/study_list_paginator.dart +++ b/lib/src/model/study/study_list_paginator.dart @@ -22,6 +22,7 @@ class StudyListPaginator extends _$StudyListPaginator { Future next() async { final studyList = state.requireValue; + if (studyList.nextPage == null) return; final newStudyPage = await _nextPage(); @@ -35,7 +36,8 @@ class StudyListPaginator extends _$StudyListPaginator { Future _nextPage() async { final nextPage = state.value?.nextPage ?? 1; - final studies = await ref.withClient( + + return await ref.withClient( (client) => search == null ? StudyRepository(client).getStudies( category: filter.category, @@ -47,7 +49,5 @@ class StudyListPaginator extends _$StudyListPaginator { page: nextPage, ), ); - - return (studies: studies, nextPage: nextPage + 1); } } diff --git a/lib/src/model/study/study_repository.dart b/lib/src/model/study/study_repository.dart index d9032a24ba..afd5f4083f 100644 --- a/lib/src/model/study/study_repository.dart +++ b/lib/src/model/study/study_repository.dart @@ -7,6 +7,7 @@ import 'package:http/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/study/study.dart'; import 'package:lichess_mobile/src/model/study/study_filter.dart'; +import 'package:lichess_mobile/src/model/study/study_list_paginator.dart'; import 'package:lichess_mobile/src/network/http.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -22,7 +23,7 @@ class StudyRepository { final Client client; - Future> getStudies({ + Future getStudies({ required StudyCategory category, required StudyListOrder order, int page = 1, @@ -33,7 +34,7 @@ class StudyRepository { ); } - Future> searchStudies({ + Future searchStudies({ required String query, int page = 1, }) { @@ -43,7 +44,7 @@ class StudyRepository { ); } - Future> _requestStudies({ + Future _requestStudies({ required String path, required Map queryParameters, }) { @@ -57,11 +58,14 @@ class StudyRepository { final paginator = pick(json, 'paginator').asMapOrThrow(); - return pick(paginator, 'currentPageResults') - .asListOrThrow( - (pick) => StudyPageData.fromJson(pick.asMapOrThrow()), - ) - .toIList(); + return ( + studies: pick(paginator, 'currentPageResults') + .asListOrThrow( + (pick) => StudyPageData.fromJson(pick.asMapOrThrow()), + ) + .toIList(), + nextPage: pick(paginator, 'nextPage').asIntOrNull() + ); }, ); } diff --git a/test/view/study/study_list_screen_test.dart b/test/view/study/study_list_screen_test.dart index b4ea96ae9b..dbf9cec287 100644 --- a/test/view/study/study_list_screen_test.dart +++ b/test/view/study/study_list_screen_test.dart @@ -21,12 +21,6 @@ void main() { if (request.url.queryParameters['page'] == '2') { return mockResponse(kStudyAllHotPage2Response, 200); } - - // Page 2 only contains a single item, so should also load page 3 - return mockResponse( - emptyPage(int.parse(request.url.queryParameters['page']!)), - 200, - ); } return mockResponse('', 404); }); @@ -54,10 +48,7 @@ void main() { const Offset(0, -250), ); - // Wait for page 3 to load - await tester.pumpAndSettle(); - - expect(requestedPages, [1, 2, 3]); + expect(requestedPages, [1, 2]); }); testWidgets('Searching', (WidgetTester tester) async { @@ -642,7 +633,7 @@ const kStudyAllHotPage1Response = ''' "previousPage": null, "nextPage": 2, "nbResults": 9999, - "nbPages": 625 + "nbPages": 2 } } '''; @@ -650,7 +641,7 @@ const kStudyAllHotPage1Response = ''' const kStudyAllHotPage2Response = ''' { "paginator": { - "currentPage": 1, + "currentPage": 2, "maxPerPage": 16, "currentPageResults": [ { @@ -690,23 +681,9 @@ const kStudyAllHotPage2Response = ''' } ], "previousPage": null, - "nextPage": 2, - "nbResults": 9999, - "nbPages": 625 - } -} -'''; - -String emptyPage(int page) => ''' -{ - "paginator": { - "currentPage": $page, - "maxPerPage": 16, - "currentPageResults": [], - "previousPage": null, - "nextPage": ${page + 1}, + "nextPage": null, "nbResults": 9999, - "nbPages": 625 + "nbPages": 2 } } '''; From 607ae0ad7acc0d1395753597ab1e4837b6a866ba Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Thu, 31 Oct 2024 23:24:05 +0100 Subject: [PATCH 575/979] fix study search test --- test/view/study/study_list_screen_test.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/view/study/study_list_screen_test.dart b/test/view/study/study_list_screen_test.dart index dbf9cec287..09fab63d24 100644 --- a/test/view/study/study_list_screen_test.dart +++ b/test/view/study/study_list_screen_test.dart @@ -110,6 +110,9 @@ void main() { ); await tester.pumpWidget(app); + // Wait for default study list (all/hot) to load + await tester.pump(); + await tester.tap(find.byType(SearchBar)); await tester.enterText(find.byType(TextField), 'Magnus'); From 7c38df744da34783d2d999dbeaa759db7ce5f8b8 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Fri, 1 Nov 2024 22:22:49 +0100 Subject: [PATCH 576/979] translate broadcast tabs --- lib/src/view/broadcast/broadcast_screen.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_screen.dart b/lib/src/view/broadcast/broadcast_screen.dart index c89ba4ac82..d0818fc586 100644 --- a/lib/src/view/broadcast/broadcast_screen.dart +++ b/lib/src/view/broadcast/broadcast_screen.dart @@ -160,9 +160,9 @@ class _CupertinoScreenState extends State<_CupertinoScreen> { navigationBar: CupertinoNavigationBar( middle: CupertinoSlidingSegmentedControl<_ViewMode>( groupValue: _selectedSegment, - children: const { - _ViewMode.overview: Text('Overview'), - _ViewMode.boards: Text('Boards'), + children: { + _ViewMode.overview: Text(context.l10n.broadcastOverview), + _ViewMode.boards: Text(context.l10n.broadcastBoards), }, onValueChanged: (_ViewMode? view) { if (view != null) { From ae4bf2f49e53fe32e61dcd5bd8519df6197729dd Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Fri, 1 Nov 2024 23:43:47 +0100 Subject: [PATCH 577/979] refactor broadcast analysis screen --- lib/src/view/analysis/analysis_screen.dart | 1059 +------------ .../view/analysis/analysis_screen_body.dart | 948 ++++++++++++ lib/src/view/analysis/engine_depth.dart | 125 ++ .../broadcast/broadcast_analysis_screen.dart | 1319 +---------------- lib/src/view/game/game_result_dialog.dart | 1 + 5 files changed, 1143 insertions(+), 2309 deletions(-) create mode 100644 lib/src/view/analysis/analysis_screen_body.dart create mode 100644 lib/src/view/analysis/engine_depth.dart diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index a87c4155fa..200f1747f6 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -1,51 +1,19 @@ -import 'dart:math' as math; - -import 'package:collection/collection.dart'; -import 'package:dartchess/dartchess.dart'; -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; -import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; -import 'package:lichess_mobile/src/model/analysis/server_analysis_service.dart'; -import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/model/engine/engine.dart'; -import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; import 'package:lichess_mobile/src/model/game/game_repository_providers.dart'; -import 'package:lichess_mobile/src/model/game/game_share_service.dart'; -import 'package:lichess_mobile/src/network/connectivity.dart'; -import 'package:lichess_mobile/src/network/http.dart'; -import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; -import 'package:lichess_mobile/src/utils/navigation.dart'; -import 'package:lichess_mobile/src/utils/screen.dart'; -import 'package:lichess_mobile/src/utils/string.dart'; -import 'package:lichess_mobile/src/view/analysis/analysis_share_screen.dart'; -import 'package:lichess_mobile/src/view/board_editor/board_editor_screen.dart'; -import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; -import 'package:lichess_mobile/src/view/engine/engine_lines.dart'; -import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_screen.dart'; -import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; +import 'package:lichess_mobile/src/view/analysis/analysis_screen_body.dart'; +import 'package:lichess_mobile/src/view/analysis/analysis_settings.dart'; +import 'package:lichess_mobile/src/view/analysis/engine_depth.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; -import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; -import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; -import 'package:lichess_mobile/src/widgets/feedback.dart'; -import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; -import 'package:popover/popover.dart'; - -import '../../utils/share.dart'; -import 'analysis_board.dart'; -import 'analysis_settings.dart'; -import 'tree_view.dart'; class AnalysisScreen extends StatelessWidget { const AnalysisScreen({ @@ -150,7 +118,7 @@ class _LoadedAnalysisScreen extends ConsumerWidget { appBar: PlatformAppBar( title: _Title(options: options), actions: [ - _EngineDepth(ctrlProvider), + EngineDepth(ctrlProvider), AppBarIconButton( onPressed: () => showAdaptiveBottomSheet( context: context, @@ -164,7 +132,7 @@ class _LoadedAnalysisScreen extends ConsumerWidget { ), ], ), - body: _Body( + body: AnalysisScreenBody( pgn: pgn, options: options, enableDrawingShapes: enableDrawingShapes, @@ -183,7 +151,7 @@ class _LoadedAnalysisScreen extends ConsumerWidget { trailing: Row( mainAxisSize: MainAxisSize.min, children: [ - _EngineDepth(ctrlProvider), + EngineDepth(ctrlProvider), AppBarIconButton( onPressed: () => showAdaptiveBottomSheet( context: context, @@ -198,7 +166,7 @@ class _LoadedAnalysisScreen extends ConsumerWidget { ], ), ), - child: _Body( + child: AnalysisScreenBody( pgn: pgn, options: options, enableDrawingShapes: enableDrawingShapes, @@ -227,1016 +195,3 @@ class _Title extends StatelessWidget { ); } } - -class _Body extends ConsumerWidget { - const _Body({ - required this.pgn, - required this.options, - required this.enableDrawingShapes, - }); - - final String pgn; - final AnalysisOptions options; - final bool enableDrawingShapes; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final ctrlProvider = analysisControllerProvider(pgn, options); - final showEvaluationGauge = ref.watch( - analysisPreferencesProvider.select((value) => value.showEvaluationGauge), - ); - - final isEngineAvailable = ref.watch( - ctrlProvider.select( - (value) => value.isEngineAvailable, - ), - ); - - final hasEval = - ref.watch(ctrlProvider.select((value) => value.hasAvailableEval)); - - final displayMode = - ref.watch(ctrlProvider.select((value) => value.displayMode)); - - final currentNode = ref.watch( - ctrlProvider.select((value) => value.currentNode), - ); - - return Column( - children: [ - Expanded( - child: SafeArea( - bottom: false, - child: LayoutBuilder( - builder: (context, constraints) { - final aspectRatio = constraints.biggest.aspectRatio; - final defaultBoardSize = constraints.biggest.shortestSide; - final isTablet = isTabletOrLarger(context); - final remainingHeight = - constraints.maxHeight - defaultBoardSize; - final isSmallScreen = - remainingHeight < kSmallRemainingHeightLeftBoardThreshold; - final boardSize = isTablet || isSmallScreen - ? defaultBoardSize - kTabletBoardTableSidePadding * 2 - : defaultBoardSize; - - const tabletBoardRadius = - BorderRadius.all(Radius.circular(4.0)); - - final display = switch (displayMode) { - DisplayMode.summary => ServerAnalysisSummary(pgn, options), - DisplayMode.moves => AnalysisTreeView( - pgn, - options, - aspectRatio > 1 - ? Orientation.landscape - : Orientation.portrait, - ), - }; - - // If the aspect ratio is greater than 1, we are in landscape mode. - if (aspectRatio > 1) { - return Row( - mainAxisSize: MainAxisSize.max, - children: [ - Padding( - padding: const EdgeInsets.only( - left: kTabletBoardTableSidePadding, - top: kTabletBoardTableSidePadding, - bottom: kTabletBoardTableSidePadding, - ), - child: Row( - children: [ - AnalysisBoard( - pgn, - options, - boardSize, - borderRadius: isTablet ? tabletBoardRadius : null, - enableDrawingShapes: enableDrawingShapes, - ), - if (hasEval && showEvaluationGauge) ...[ - const SizedBox(width: 4.0), - _EngineGaugeVertical(ctrlProvider), - ], - ], - ), - ), - Flexible( - fit: FlexFit.loose, - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - if (isEngineAvailable) - Padding( - padding: const EdgeInsets.all( - kTabletBoardTableSidePadding, - ), - child: EngineLines( - onTapMove: ref - .read(ctrlProvider.notifier) - .onUserMove, - clientEval: currentNode.eval, - isGameOver: currentNode.position.isGameOver, - ), - ), - Expanded( - child: PlatformCard( - clipBehavior: Clip.hardEdge, - borderRadius: const BorderRadius.all( - Radius.circular(4.0), - ), - margin: const EdgeInsets.all( - kTabletBoardTableSidePadding, - ), - semanticContainer: false, - child: display, - ), - ), - ], - ), - ), - ], - ); - } - // If the aspect ratio is less than 1, we are in portrait mode. - else { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - _ColumnTopTable(ctrlProvider), - if (isTablet) - Padding( - padding: const EdgeInsets.all( - kTabletBoardTableSidePadding, - ), - child: AnalysisBoard( - pgn, - options, - boardSize, - borderRadius: isTablet ? tabletBoardRadius : null, - enableDrawingShapes: enableDrawingShapes, - ), - ) - else - AnalysisBoard( - pgn, - options, - boardSize, - borderRadius: isTablet ? tabletBoardRadius : null, - enableDrawingShapes: enableDrawingShapes, - ), - Expanded( - child: Padding( - padding: isTablet - ? const EdgeInsets.symmetric( - horizontal: kTabletBoardTableSidePadding, - ) - : EdgeInsets.zero, - child: display, - ), - ), - ], - ); - } - }, - ), - ), - ), - _BottomBar(pgn: pgn, options: options), - ], - ); - } -} - -class _EngineGaugeVertical extends ConsumerWidget { - const _EngineGaugeVertical(this.ctrlProvider); - - final AnalysisControllerProvider ctrlProvider; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final analysisState = ref.watch(ctrlProvider); - - return Container( - clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4.0), - ), - child: EngineGauge( - displayMode: EngineGaugeDisplayMode.vertical, - params: analysisState.engineGaugeParams, - ), - ); - } -} - -class _ColumnTopTable extends ConsumerWidget { - const _ColumnTopTable(this.ctrlProvider); - - final AnalysisControllerProvider ctrlProvider; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final analysisState = ref.watch(ctrlProvider); - final showEvaluationGauge = ref.watch( - analysisPreferencesProvider.select((p) => p.showEvaluationGauge), - ); - - return analysisState.hasAvailableEval - ? Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (showEvaluationGauge) - EngineGauge( - displayMode: EngineGaugeDisplayMode.horizontal, - params: analysisState.engineGaugeParams, - ), - if (analysisState.isEngineAvailable) - EngineLines( - clientEval: analysisState.currentNode.eval, - isGameOver: analysisState.currentNode.position.isGameOver, - onTapMove: ref.read(ctrlProvider.notifier).onUserMove, - ), - ], - ) - : kEmptyWidget; - } -} - -class _BottomBar extends ConsumerWidget { - const _BottomBar({ - required this.pgn, - required this.options, - }); - - final String pgn; - final AnalysisOptions options; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final ctrlProvider = analysisControllerProvider(pgn, options); - final analysisState = ref.watch(ctrlProvider); - final isOnline = - ref.watch(connectivityChangesProvider).valueOrNull?.isOnline ?? false; - - return BottomBar( - children: [ - BottomBarButton( - label: context.l10n.menu, - onTap: () { - _showAnalysisMenu(context, ref); - }, - icon: Icons.menu, - ), - if (analysisState.canShowGameSummary) - BottomBarButton( - // TODO: l10n - label: analysisState.displayMode == DisplayMode.summary - ? 'Moves' - : 'Summary', - onTap: () { - final newMode = analysisState.displayMode == DisplayMode.summary - ? DisplayMode.moves - : DisplayMode.summary; - ref.read(ctrlProvider.notifier).setDisplayMode(newMode); - }, - icon: analysisState.displayMode == DisplayMode.summary - ? LichessIcons.flow_cascade - : Icons.area_chart, - ), - BottomBarButton( - label: context.l10n.openingExplorer, - onTap: isOnline - ? () { - pushPlatformRoute( - context, - title: context.l10n.openingExplorer, - builder: (_) => OpeningExplorerScreen( - pgn: ref.read(ctrlProvider.notifier).makeCurrentNodePgn(), - options: analysisState.openingExplorerOptions, - ), - ); - } - : null, - icon: Icons.explore, - ), - RepeatButton( - onLongPress: - analysisState.canGoBack ? () => _moveBackward(ref) : null, - child: BottomBarButton( - key: const ValueKey('goto-previous'), - onTap: analysisState.canGoBack ? () => _moveBackward(ref) : null, - label: 'Previous', - icon: CupertinoIcons.chevron_back, - showTooltip: false, - ), - ), - RepeatButton( - onLongPress: analysisState.canGoNext ? () => _moveForward(ref) : null, - child: BottomBarButton( - key: const ValueKey('goto-next'), - icon: CupertinoIcons.chevron_forward, - label: context.l10n.next, - onTap: analysisState.canGoNext ? () => _moveForward(ref) : null, - showTooltip: false, - ), - ), - ], - ); - } - - void _moveForward(WidgetRef ref) => - ref.read(analysisControllerProvider(pgn, options).notifier).userNext(); - void _moveBackward(WidgetRef ref) => ref - .read(analysisControllerProvider(pgn, options).notifier) - .userPrevious(); - - Future _showAnalysisMenu(BuildContext context, WidgetRef ref) { - return showAdaptiveActionSheet( - context: context, - actions: [ - BottomSheetAction( - makeLabel: (context) => Text(context.l10n.flipBoard), - onPressed: (context) { - ref - .read(analysisControllerProvider(pgn, options).notifier) - .toggleBoard(); - }, - ), - BottomSheetAction( - makeLabel: (context) => Text(context.l10n.boardEditor), - onPressed: (context) { - final analysisState = - ref.read(analysisControllerProvider(pgn, options)); - final boardFen = analysisState.position.fen; - pushPlatformRoute( - context, - title: context.l10n.boardEditor, - builder: (_) => BoardEditorScreen( - initialFen: boardFen, - ), - ); - }, - ), - BottomSheetAction( - makeLabel: (context) => Text(context.l10n.mobileShareGamePGN), - onPressed: (_) { - pushPlatformRoute( - context, - title: context.l10n.studyShareAndExport, - builder: (_) => AnalysisShareScreen(pgn: pgn, options: options), - ); - }, - ), - BottomSheetAction( - makeLabel: (context) => Text(context.l10n.mobileSharePositionAsFEN), - onPressed: (_) { - final analysisState = - ref.read(analysisControllerProvider(pgn, options)); - launchShareDialog( - context, - text: analysisState.position.fen, - ); - }, - ), - if (options.gameAnyId != null) - BottomSheetAction( - makeLabel: (context) => - Text(context.l10n.screenshotCurrentPosition), - onPressed: (_) async { - final gameId = options.gameAnyId!.gameId; - final analysisState = - ref.read(analysisControllerProvider(pgn, options)); - try { - final image = - await ref.read(gameShareServiceProvider).screenshotPosition( - gameId, - options.orientation, - analysisState.position.fen, - analysisState.lastMove, - ); - if (context.mounted) { - launchShareDialog( - context, - files: [image], - subject: context.l10n.puzzleFromGameLink( - lichessUri('/$gameId').toString(), - ), - ); - } - } catch (e) { - if (context.mounted) { - showPlatformSnackbar( - context, - 'Failed to get GIF', - type: SnackBarType.error, - ); - } - } - }, - ), - ], - ); - } -} - -class _EngineDepth extends ConsumerWidget { - const _EngineDepth(this.ctrlProvider); - - final AnalysisControllerProvider ctrlProvider; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isEngineAvailable = ref.watch( - ctrlProvider.select( - (value) => value.isEngineAvailable, - ), - ); - final currentNode = ref.watch( - ctrlProvider.select((value) => value.currentNode), - ); - final depth = ref.watch( - engineEvaluationProvider.select((value) => value.eval?.depth), - ) ?? - currentNode.eval?.depth; - - return isEngineAvailable && depth != null - ? AppBarTextButton( - onPressed: () { - showPopover( - context: context, - bodyBuilder: (context) { - return _StockfishInfo(currentNode); - }, - direction: PopoverDirection.top, - width: 240, - backgroundColor: - Theme.of(context).platform == TargetPlatform.android - ? Theme.of(context).dialogBackgroundColor - : CupertinoDynamicColor.resolve( - CupertinoColors.tertiarySystemBackground, - context, - ), - transitionDuration: Duration.zero, - popoverTransitionBuilder: (_, child) => child, - ); - }, - child: RepaintBoundary( - child: Container( - width: 20.0, - height: 20.0, - padding: const EdgeInsets.all(2.0), - decoration: BoxDecoration( - color: Theme.of(context).platform == TargetPlatform.android - ? Theme.of(context).colorScheme.secondary - : CupertinoTheme.of(context).primaryColor, - borderRadius: BorderRadius.circular(4.0), - ), - child: FittedBox( - fit: BoxFit.contain, - child: Text( - '${math.min(99, depth)}', - style: TextStyle( - color: Theme.of(context).platform == - TargetPlatform.android - ? Theme.of(context).colorScheme.onSecondary - : CupertinoTheme.of(context).primaryContrastingColor, - fontFeatures: const [ - FontFeature.tabularFigures(), - ], - ), - ), - ), - ), - ), - ) - : const SizedBox.shrink(); - } -} - -class _StockfishInfo extends ConsumerWidget { - const _StockfishInfo(this.currentNode); - - final AnalysisCurrentNode currentNode; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final (engineName: engineName, eval: eval, state: engineState) = - ref.watch(engineEvaluationProvider); - - final currentEval = eval ?? currentNode.eval; - - final knps = engineState == EngineState.computing - ? ', ${eval?.knps.round()}kn/s' - : ''; - final depth = currentEval?.depth ?? 0; - final maxDepth = math.max(depth, kMaxEngineDepth); - - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - PlatformListTile( - leading: Image.asset( - 'assets/images/stockfish/icon.png', - width: 44, - height: 44, - ), - title: Text(engineName), - subtitle: Text( - context.l10n.depthX( - '$depth/$maxDepth$knps', - ), - ), - ), - ], - ); - } -} - -class ServerAnalysisSummary extends ConsumerWidget { - const ServerAnalysisSummary(this.pgn, this.options); - - final String pgn; - final AnalysisOptions options; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final ctrlProvider = analysisControllerProvider(pgn, options); - final playersAnalysis = - ref.watch(ctrlProvider.select((value) => value.playersAnalysis)); - final pgnHeaders = - ref.watch(ctrlProvider.select((value) => value.pgnHeaders)); - final currentGameAnalysis = ref.watch(currentAnalysisProvider); - - return playersAnalysis != null - ? ListView( - children: [ - if (currentGameAnalysis == options.gameAnyId?.gameId) - const Padding( - padding: EdgeInsets.only(top: 16.0), - child: WaitingForServerAnalysis(), - ), - AcplChart(pgn, options), - Center( - child: SizedBox( - width: math.min(MediaQuery.sizeOf(context).width, 500), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Table( - defaultVerticalAlignment: - TableCellVerticalAlignment.middle, - columnWidths: const { - 0: FlexColumnWidth(1), - 1: FlexColumnWidth(1), - 2: FlexColumnWidth(1), - }, - children: [ - TableRow( - decoration: const BoxDecoration( - border: Border( - bottom: BorderSide(color: Colors.grey), - ), - ), - children: [ - _SummaryPlayerName(Side.white, pgnHeaders), - Center( - child: Text( - pgnHeaders.get('Result') ?? '', - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - ), - ), - _SummaryPlayerName(Side.black, pgnHeaders), - ], - ), - if (playersAnalysis.white.accuracy != null && - playersAnalysis.black.accuracy != null) - TableRow( - children: [ - _SummaryNumber( - '${playersAnalysis.white.accuracy}%', - ), - Center( - heightFactor: 1.8, - child: Text( - context.l10n.accuracy, - softWrap: true, - ), - ), - _SummaryNumber( - '${playersAnalysis.black.accuracy}%', - ), - ], - ), - for (final item in [ - ( - playersAnalysis.white.inaccuracies.toString(), - context.l10n - .nbInaccuracies(2) - .replaceAll('2', '') - .trim() - .capitalize(), - playersAnalysis.black.inaccuracies.toString() - ), - ( - playersAnalysis.white.mistakes.toString(), - context.l10n - .nbMistakes(2) - .replaceAll('2', '') - .trim() - .capitalize(), - playersAnalysis.black.mistakes.toString() - ), - ( - playersAnalysis.white.blunders.toString(), - context.l10n - .nbBlunders(2) - .replaceAll('2', '') - .trim() - .capitalize(), - playersAnalysis.black.blunders.toString() - ), - ]) - TableRow( - children: [ - _SummaryNumber(item.$1), - Center( - heightFactor: 1.2, - child: Text( - item.$2, - softWrap: true, - ), - ), - _SummaryNumber(item.$3), - ], - ), - if (playersAnalysis.white.acpl != null && - playersAnalysis.black.acpl != null) - TableRow( - children: [ - _SummaryNumber( - playersAnalysis.white.acpl.toString(), - ), - Center( - heightFactor: 1.5, - child: Text( - context.l10n.averageCentipawnLoss, - softWrap: true, - textAlign: TextAlign.center, - ), - ), - _SummaryNumber( - playersAnalysis.black.acpl.toString(), - ), - ], - ), - ], - ), - ), - ), - ), - ], - ) - : Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Spacer(), - if (currentGameAnalysis == options.gameAnyId?.gameId) - const Center( - child: Padding( - padding: EdgeInsets.symmetric(vertical: 16.0), - child: WaitingForServerAnalysis(), - ), - ) - else - Center( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0), - child: Builder( - builder: (context) { - Future? pendingRequest; - return StatefulBuilder( - builder: (context, setState) { - return FutureBuilder( - future: pendingRequest, - builder: (context, snapshot) { - return SecondaryButton( - semanticsLabel: - context.l10n.requestAComputerAnalysis, - onPressed: ref.watch(authSessionProvider) == - null - ? () { - showPlatformSnackbar( - context, - context - .l10n.youNeedAnAccountToDoThat, - ); - } - : snapshot.connectionState == - ConnectionState.waiting - ? null - : () { - setState(() { - pendingRequest = ref - .read(ctrlProvider.notifier) - .requestServerAnalysis() - .catchError((Object e) { - if (context.mounted) { - showPlatformSnackbar( - context, - e.toString(), - type: SnackBarType.error, - ); - } - }); - }); - }, - child: Text( - context.l10n.requestAComputerAnalysis, - ), - ); - }, - ); - }, - ); - }, - ), - ), - ), - const Spacer(), - ], - ); - } -} - -class WaitingForServerAnalysis extends StatelessWidget { - const WaitingForServerAnalysis({super.key}); - - @override - Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.max, - children: [ - Image.asset( - 'assets/images/stockfish/icon.png', - width: 30, - height: 30, - ), - const SizedBox(width: 8.0), - Text(context.l10n.waitingForAnalysis), - const SizedBox(width: 8.0), - const CircularProgressIndicator.adaptive(), - ], - ); - } -} - -class _SummaryNumber extends StatelessWidget { - const _SummaryNumber(this.data); - final String data; - - @override - Widget build(BuildContext context) { - return Center( - child: Text( - data, - softWrap: true, - ), - ); - } -} - -class _SummaryPlayerName extends StatelessWidget { - const _SummaryPlayerName(this.side, this.pgnHeaders); - final Side side; - final IMap pgnHeaders; - - @override - Widget build(BuildContext context) { - final playerTitle = side == Side.white - ? pgnHeaders.get('WhiteTitle') - : pgnHeaders.get('BlackTitle'); - final playerName = side == Side.white - ? pgnHeaders.get('White') ?? context.l10n.white - : pgnHeaders.get('Black') ?? context.l10n.black; - - final brightness = Theme.of(context).brightness; - - return TableCell( - verticalAlignment: TableCellVerticalAlignment.top, - child: Center( - child: Padding( - padding: const EdgeInsets.only(bottom: 5), - child: Column( - children: [ - Icon( - side == Side.white - ? brightness == Brightness.light - ? CupertinoIcons.circle - : CupertinoIcons.circle_filled - : brightness == Brightness.light - ? CupertinoIcons.circle_filled - : CupertinoIcons.circle, - size: 14, - ), - Text( - '${playerTitle != null ? '$playerTitle ' : ''}$playerName', - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - textAlign: TextAlign.center, - softWrap: true, - ), - ], - ), - ), - ), - ); - } -} - -class AcplChart extends ConsumerWidget { - const AcplChart(this.pgn, this.options); - - final String pgn; - final AnalysisOptions options; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final mainLineColor = Theme.of(context).colorScheme.secondary; - // yes it looks like below/above are inverted in fl_chart - final brightness = Theme.of(context).brightness; - final white = Theme.of(context).colorScheme.surfaceContainerHighest; - final black = Theme.of(context).colorScheme.outline; - // yes it looks like below/above are inverted in fl_chart - final belowLineColor = brightness == Brightness.light ? white : black; - final aboveLineColor = brightness == Brightness.light ? black : white; - - VerticalLine phaseVerticalBar(double x, String label) => VerticalLine( - x: x, - color: const Color(0xFF707070), - strokeWidth: 0.5, - label: VerticalLineLabel( - style: TextStyle( - fontSize: 10, - color: Theme.of(context) - .textTheme - .labelMedium - ?.color - ?.withValues(alpha: 0.3), - ), - labelResolver: (line) => label, - padding: const EdgeInsets.only(right: 1), - alignment: Alignment.topRight, - direction: LabelDirection.vertical, - show: true, - ), - ); - - final data = ref.watch( - analysisControllerProvider(pgn, options) - .select((value) => value.acplChartData), - ); - - final rootPly = ref.watch( - analysisControllerProvider(pgn, options) - .select((value) => value.root.position.ply), - ); - - final currentNode = ref.watch( - analysisControllerProvider(pgn, options) - .select((value) => value.currentNode), - ); - - final isOnMainline = ref.watch( - analysisControllerProvider(pgn, options) - .select((value) => value.isOnMainline), - ); - - if (data == null) { - return const SizedBox.shrink(); - } - - final spots = data - .mapIndexed( - (i, e) => FlSpot(i.toDouble(), e.winningChances(Side.white)), - ) - .toList(growable: false); - - final divisionLines = []; - - if (options.division?.middlegame != null) { - if (options.division!.middlegame! > 0) { - divisionLines.add(phaseVerticalBar(0.0, context.l10n.opening)); - divisionLines.add( - phaseVerticalBar( - options.division!.middlegame! - 1, - context.l10n.middlegame, - ), - ); - } else { - divisionLines.add(phaseVerticalBar(0.0, context.l10n.middlegame)); - } - } - - if (options.division?.endgame != null) { - if (options.division!.endgame! > 0) { - divisionLines.add( - phaseVerticalBar( - options.division!.endgame! - 1, - context.l10n.endgame, - ), - ); - } else { - divisionLines.add( - phaseVerticalBar( - 0.0, - context.l10n.endgame, - ), - ); - } - } - return Center( - child: AspectRatio( - aspectRatio: 2.5, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: LineChart( - LineChartData( - lineTouchData: LineTouchData( - enabled: false, - touchCallback: - (FlTouchEvent event, LineTouchResponse? touchResponse) { - if (event is FlTapDownEvent || - event is FlPanUpdateEvent || - event is FlLongPressMoveUpdate) { - final touchX = event.localPosition!.dx; - final chartWidth = context.size!.width - - 32; // Insets on both sides of the chart of 16 - final minX = spots.first.x; - final maxX = spots.last.x; - final touchXDataValue = - minX + (touchX / chartWidth) * (maxX - minX); - final closestSpot = spots.reduce( - (a, b) => (a.x - touchXDataValue).abs() < - (b.x - touchXDataValue).abs() - ? a - : b, - ); - final closestNodeIndex = closestSpot.x.round(); - ref - .read(analysisControllerProvider(pgn, options).notifier) - .jumpToNthNodeOnMainline(closestNodeIndex); - } - }, - ), - minY: -1.0, - maxY: 1.0, - lineBarsData: [ - LineChartBarData( - spots: spots, - isCurved: false, - barWidth: 1, - color: mainLineColor.withValues(alpha: 0.7), - aboveBarData: BarAreaData( - show: true, - color: aboveLineColor, - applyCutOffY: true, - ), - belowBarData: BarAreaData( - show: true, - color: belowLineColor, - applyCutOffY: true, - ), - dotData: const FlDotData( - show: false, - ), - ), - ], - extraLinesData: ExtraLinesData( - verticalLines: [ - if (isOnMainline) - VerticalLine( - x: (currentNode.position.ply - 1 - rootPly).toDouble(), - color: mainLineColor, - strokeWidth: 1.0, - ), - ...divisionLines, - ], - ), - gridData: const FlGridData(show: false), - borderData: FlBorderData(show: false), - titlesData: const FlTitlesData(show: false), - ), - ), - ), - ), - ); - } -} diff --git a/lib/src/view/analysis/analysis_screen_body.dart b/lib/src/view/analysis/analysis_screen_body.dart new file mode 100644 index 0000000000..24d09051be --- /dev/null +++ b/lib/src/view/analysis/analysis_screen_body.dart @@ -0,0 +1,948 @@ +import 'dart:math' as math; + +import 'package:collection/collection.dart'; +import 'package:dartchess/dartchess.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/constants.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; +import 'package:lichess_mobile/src/model/analysis/server_analysis_service.dart'; +import 'package:lichess_mobile/src/model/auth/auth_session.dart'; +import 'package:lichess_mobile/src/model/game/game_share_service.dart'; +import 'package:lichess_mobile/src/network/connectivity.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/styles/lichess_icons.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/utils/screen.dart'; +import 'package:lichess_mobile/src/utils/share.dart'; +import 'package:lichess_mobile/src/utils/string.dart'; +import 'package:lichess_mobile/src/view/analysis/analysis_board.dart'; +import 'package:lichess_mobile/src/view/analysis/analysis_share_screen.dart'; +import 'package:lichess_mobile/src/view/analysis/tree_view.dart'; +import 'package:lichess_mobile/src/view/board_editor/board_editor_screen.dart'; +import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; +import 'package:lichess_mobile/src/view/engine/engine_lines.dart'; +import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_screen.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; +import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; +import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/feedback.dart'; +import 'package:lichess_mobile/src/widgets/platform.dart'; + +class AnalysisScreenBody extends ConsumerWidget { + const AnalysisScreenBody({ + required this.pgn, + required this.options, + required this.enableDrawingShapes, + this.broadcastWrapBoardBuilder, + }); + + final String pgn; + final AnalysisOptions options; + final bool enableDrawingShapes; + final Widget Function( + WidgetRef ref, + AnalysisBoard board, + AnalysisControllerProvider ctrlProvider, + double boardSize, + )? broadcastWrapBoardBuilder; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final ctrlProvider = analysisControllerProvider(pgn, options); + final showEvaluationGauge = ref.watch( + analysisPreferencesProvider.select((value) => value.showEvaluationGauge), + ); + + final isEngineAvailable = ref.watch( + ctrlProvider.select( + (value) => value.isEngineAvailable, + ), + ); + + final hasEval = + ref.watch(ctrlProvider.select((value) => value.hasAvailableEval)); + + final displayMode = + ref.watch(ctrlProvider.select((value) => value.displayMode)); + + final currentNode = ref.watch( + ctrlProvider.select((value) => value.currentNode), + ); + + return Column( + children: [ + Expanded( + child: SafeArea( + bottom: false, + child: LayoutBuilder( + builder: (context, constraints) { + final aspectRatio = constraints.biggest.aspectRatio; + final defaultBoardSize = constraints.biggest.shortestSide; + final isTablet = isTabletOrLarger(context); + final remainingHeight = + constraints.maxHeight - defaultBoardSize; + final isSmallScreen = + remainingHeight < kSmallRemainingHeightLeftBoardThreshold; + final boardSize = isTablet || isSmallScreen + ? defaultBoardSize - kTabletBoardTableSidePadding * 2 + : defaultBoardSize; + + const tabletBoardRadius = + BorderRadius.all(Radius.circular(4.0)); + + final display = switch (displayMode) { + DisplayMode.summary => ServerAnalysisSummary(pgn, options), + DisplayMode.moves => AnalysisTreeView( + pgn, + options, + aspectRatio > 1 + ? Orientation.landscape + : Orientation.portrait, + ), + }; + + Widget maybeWrapBoardBuilder(AnalysisBoard board) { + if (broadcastWrapBoardBuilder == null) { + return board; + } else { + return broadcastWrapBoardBuilder!( + ref, + board, + ctrlProvider, + boardSize, + ); + } + } + + final maybeWrappedBoard = maybeWrapBoardBuilder( + AnalysisBoard( + pgn, + options, + boardSize, + borderRadius: isTablet ? tabletBoardRadius : null, + enableDrawingShapes: enableDrawingShapes, + ), + ); + + // If the aspect ratio is greater than 1, we are in landscape mode. + if (aspectRatio > 1) { + return Row( + mainAxisSize: MainAxisSize.max, + children: [ + Padding( + padding: const EdgeInsets.only( + left: kTabletBoardTableSidePadding, + top: kTabletBoardTableSidePadding, + bottom: kTabletBoardTableSidePadding, + ), + child: Row( + children: [ + maybeWrappedBoard, + if (hasEval && showEvaluationGauge) ...[ + const SizedBox(width: 4.0), + _EngineGaugeVertical(ctrlProvider), + ], + ], + ), + ), + Flexible( + fit: FlexFit.loose, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + if (isEngineAvailable) + Padding( + padding: const EdgeInsets.all( + kTabletBoardTableSidePadding, + ), + child: EngineLines( + onTapMove: ref + .read(ctrlProvider.notifier) + .onUserMove, + clientEval: currentNode.eval, + isGameOver: currentNode.position.isGameOver, + ), + ), + Expanded( + child: PlatformCard( + clipBehavior: Clip.hardEdge, + borderRadius: const BorderRadius.all( + Radius.circular(4.0), + ), + margin: const EdgeInsets.all( + kTabletBoardTableSidePadding, + ), + semanticContainer: false, + child: display, + ), + ), + ], + ), + ), + ], + ); + } + // If the aspect ratio is less than 1, we are in portrait mode. + else { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + _ColumnTopTable(ctrlProvider), + if (isTablet) + Padding( + padding: const EdgeInsets.all( + kTabletBoardTableSidePadding, + ), + child: maybeWrappedBoard, + ) + else + maybeWrappedBoard, + Expanded( + child: Padding( + padding: isTablet + ? const EdgeInsets.symmetric( + horizontal: kTabletBoardTableSidePadding, + ) + : EdgeInsets.zero, + child: display, + ), + ), + ], + ); + } + }, + ), + ), + ), + _BottomBar(pgn: pgn, options: options), + ], + ); + } +} + +class _EngineGaugeVertical extends ConsumerWidget { + const _EngineGaugeVertical(this.ctrlProvider); + + final AnalysisControllerProvider ctrlProvider; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final analysisState = ref.watch(ctrlProvider); + + return Container( + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4.0), + ), + child: EngineGauge( + displayMode: EngineGaugeDisplayMode.vertical, + params: analysisState.engineGaugeParams, + ), + ); + } +} + +class _ColumnTopTable extends ConsumerWidget { + const _ColumnTopTable(this.ctrlProvider); + + final AnalysisControllerProvider ctrlProvider; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final analysisState = ref.watch(ctrlProvider); + final showEvaluationGauge = ref.watch( + analysisPreferencesProvider.select((p) => p.showEvaluationGauge), + ); + + return analysisState.hasAvailableEval + ? Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (showEvaluationGauge) + EngineGauge( + displayMode: EngineGaugeDisplayMode.horizontal, + params: analysisState.engineGaugeParams, + ), + if (analysisState.isEngineAvailable) + EngineLines( + clientEval: analysisState.currentNode.eval, + isGameOver: analysisState.currentNode.position.isGameOver, + onTapMove: ref.read(ctrlProvider.notifier).onUserMove, + ), + ], + ) + : kEmptyWidget; + } +} + +class _BottomBar extends ConsumerWidget { + const _BottomBar({ + required this.pgn, + required this.options, + }); + + final String pgn; + final AnalysisOptions options; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final ctrlProvider = analysisControllerProvider(pgn, options); + final analysisState = ref.watch(ctrlProvider); + final isOnline = + ref.watch(connectivityChangesProvider).valueOrNull?.isOnline ?? false; + + return BottomBar( + children: [ + BottomBarButton( + label: context.l10n.menu, + onTap: () { + _showAnalysisMenu(context, ref); + }, + icon: Icons.menu, + ), + if (analysisState.canShowGameSummary) + BottomBarButton( + // TODO: l10n + label: analysisState.displayMode == DisplayMode.summary + ? 'Moves' + : 'Summary', + onTap: () { + final newMode = analysisState.displayMode == DisplayMode.summary + ? DisplayMode.moves + : DisplayMode.summary; + ref.read(ctrlProvider.notifier).setDisplayMode(newMode); + }, + icon: analysisState.displayMode == DisplayMode.summary + ? LichessIcons.flow_cascade + : Icons.area_chart, + ), + BottomBarButton( + label: context.l10n.openingExplorer, + onTap: isOnline + ? () { + pushPlatformRoute( + context, + title: context.l10n.openingExplorer, + builder: (_) => OpeningExplorerScreen( + pgn: ref.read(ctrlProvider.notifier).makeCurrentNodePgn(), + options: analysisState.openingExplorerOptions, + ), + ); + } + : null, + icon: Icons.explore, + ), + RepeatButton( + onLongPress: + analysisState.canGoBack ? () => _moveBackward(ref) : null, + child: BottomBarButton( + key: const ValueKey('goto-previous'), + onTap: analysisState.canGoBack ? () => _moveBackward(ref) : null, + label: 'Previous', + icon: CupertinoIcons.chevron_back, + showTooltip: false, + ), + ), + RepeatButton( + onLongPress: analysisState.canGoNext ? () => _moveForward(ref) : null, + child: BottomBarButton( + key: const ValueKey('goto-next'), + icon: CupertinoIcons.chevron_forward, + label: context.l10n.next, + onTap: analysisState.canGoNext ? () => _moveForward(ref) : null, + showTooltip: false, + ), + ), + ], + ); + } + + void _moveForward(WidgetRef ref) => + ref.read(analysisControllerProvider(pgn, options).notifier).userNext(); + void _moveBackward(WidgetRef ref) => ref + .read(analysisControllerProvider(pgn, options).notifier) + .userPrevious(); + + Future _showAnalysisMenu(BuildContext context, WidgetRef ref) { + return showAdaptiveActionSheet( + context: context, + actions: [ + BottomSheetAction( + makeLabel: (context) => Text(context.l10n.flipBoard), + onPressed: (context) { + ref + .read(analysisControllerProvider(pgn, options).notifier) + .toggleBoard(); + }, + ), + BottomSheetAction( + makeLabel: (context) => Text(context.l10n.boardEditor), + onPressed: (context) { + final analysisState = + ref.read(analysisControllerProvider(pgn, options)); + final boardFen = analysisState.position.fen; + pushPlatformRoute( + context, + title: context.l10n.boardEditor, + builder: (_) => BoardEditorScreen( + initialFen: boardFen, + ), + ); + }, + ), + BottomSheetAction( + makeLabel: (context) => Text(context.l10n.mobileShareGamePGN), + onPressed: (_) { + pushPlatformRoute( + context, + title: context.l10n.studyShareAndExport, + builder: (_) => AnalysisShareScreen(pgn: pgn, options: options), + ); + }, + ), + BottomSheetAction( + makeLabel: (context) => Text(context.l10n.mobileSharePositionAsFEN), + onPressed: (_) { + final analysisState = + ref.read(analysisControllerProvider(pgn, options)); + launchShareDialog( + context, + text: analysisState.position.fen, + ); + }, + ), + if (options.gameAnyId != null) + BottomSheetAction( + makeLabel: (context) => + Text(context.l10n.screenshotCurrentPosition), + onPressed: (_) async { + final gameId = options.gameAnyId!.gameId; + final analysisState = + ref.read(analysisControllerProvider(pgn, options)); + try { + final image = + await ref.read(gameShareServiceProvider).screenshotPosition( + gameId, + options.orientation, + analysisState.position.fen, + analysisState.lastMove, + ); + if (context.mounted) { + launchShareDialog( + context, + files: [image], + subject: context.l10n.puzzleFromGameLink( + lichessUri('/$gameId').toString(), + ), + ); + } + } catch (e) { + if (context.mounted) { + showPlatformSnackbar( + context, + 'Failed to get GIF', + type: SnackBarType.error, + ); + } + } + }, + ), + ], + ); + } +} + +class ServerAnalysisSummary extends ConsumerWidget { + const ServerAnalysisSummary(this.pgn, this.options); + + final String pgn; + final AnalysisOptions options; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final ctrlProvider = analysisControllerProvider(pgn, options); + final playersAnalysis = + ref.watch(ctrlProvider.select((value) => value.playersAnalysis)); + final pgnHeaders = + ref.watch(ctrlProvider.select((value) => value.pgnHeaders)); + final currentGameAnalysis = ref.watch(currentAnalysisProvider); + + return playersAnalysis != null + ? ListView( + children: [ + if (currentGameAnalysis == options.gameAnyId?.gameId) + const Padding( + padding: EdgeInsets.only(top: 16.0), + child: WaitingForServerAnalysis(), + ), + AcplChart(pgn, options), + Center( + child: SizedBox( + width: math.min(MediaQuery.sizeOf(context).width, 500), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Table( + defaultVerticalAlignment: + TableCellVerticalAlignment.middle, + columnWidths: const { + 0: FlexColumnWidth(1), + 1: FlexColumnWidth(1), + 2: FlexColumnWidth(1), + }, + children: [ + TableRow( + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide(color: Colors.grey), + ), + ), + children: [ + _SummaryPlayerName(Side.white, pgnHeaders), + Center( + child: Text( + pgnHeaders.get('Result') ?? '', + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + ), + _SummaryPlayerName(Side.black, pgnHeaders), + ], + ), + if (playersAnalysis.white.accuracy != null && + playersAnalysis.black.accuracy != null) + TableRow( + children: [ + _SummaryNumber( + '${playersAnalysis.white.accuracy}%', + ), + Center( + heightFactor: 1.8, + child: Text( + context.l10n.accuracy, + softWrap: true, + ), + ), + _SummaryNumber( + '${playersAnalysis.black.accuracy}%', + ), + ], + ), + for (final item in [ + ( + playersAnalysis.white.inaccuracies.toString(), + context.l10n + .nbInaccuracies(2) + .replaceAll('2', '') + .trim() + .capitalize(), + playersAnalysis.black.inaccuracies.toString() + ), + ( + playersAnalysis.white.mistakes.toString(), + context.l10n + .nbMistakes(2) + .replaceAll('2', '') + .trim() + .capitalize(), + playersAnalysis.black.mistakes.toString() + ), + ( + playersAnalysis.white.blunders.toString(), + context.l10n + .nbBlunders(2) + .replaceAll('2', '') + .trim() + .capitalize(), + playersAnalysis.black.blunders.toString() + ), + ]) + TableRow( + children: [ + _SummaryNumber(item.$1), + Center( + heightFactor: 1.2, + child: Text( + item.$2, + softWrap: true, + ), + ), + _SummaryNumber(item.$3), + ], + ), + if (playersAnalysis.white.acpl != null && + playersAnalysis.black.acpl != null) + TableRow( + children: [ + _SummaryNumber( + playersAnalysis.white.acpl.toString(), + ), + Center( + heightFactor: 1.5, + child: Text( + context.l10n.averageCentipawnLoss, + softWrap: true, + textAlign: TextAlign.center, + ), + ), + _SummaryNumber( + playersAnalysis.black.acpl.toString(), + ), + ], + ), + ], + ), + ), + ), + ), + ], + ) + : Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Spacer(), + if (currentGameAnalysis == options.gameAnyId?.gameId) + const Center( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 16.0), + child: WaitingForServerAnalysis(), + ), + ) + else + Center( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Builder( + builder: (context) { + Future? pendingRequest; + return StatefulBuilder( + builder: (context, setState) { + return FutureBuilder( + future: pendingRequest, + builder: (context, snapshot) { + return SecondaryButton( + semanticsLabel: + context.l10n.requestAComputerAnalysis, + onPressed: ref.watch(authSessionProvider) == + null + ? () { + showPlatformSnackbar( + context, + context + .l10n.youNeedAnAccountToDoThat, + ); + } + : snapshot.connectionState == + ConnectionState.waiting + ? null + : () { + setState(() { + pendingRequest = ref + .read(ctrlProvider.notifier) + .requestServerAnalysis() + .catchError((Object e) { + if (context.mounted) { + showPlatformSnackbar( + context, + e.toString(), + type: SnackBarType.error, + ); + } + }); + }); + }, + child: Text( + context.l10n.requestAComputerAnalysis, + ), + ); + }, + ); + }, + ); + }, + ), + ), + ), + const Spacer(), + ], + ); + } +} + +class WaitingForServerAnalysis extends StatelessWidget { + const WaitingForServerAnalysis({super.key}); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ + Image.asset( + 'assets/images/stockfish/icon.png', + width: 30, + height: 30, + ), + const SizedBox(width: 8.0), + Text(context.l10n.waitingForAnalysis), + const SizedBox(width: 8.0), + const CircularProgressIndicator.adaptive(), + ], + ); + } +} + +class _SummaryNumber extends StatelessWidget { + const _SummaryNumber(this.data); + final String data; + + @override + Widget build(BuildContext context) { + return Center( + child: Text( + data, + softWrap: true, + ), + ); + } +} + +class _SummaryPlayerName extends StatelessWidget { + const _SummaryPlayerName(this.side, this.pgnHeaders); + final Side side; + final IMap pgnHeaders; + + @override + Widget build(BuildContext context) { + final playerTitle = side == Side.white + ? pgnHeaders.get('WhiteTitle') + : pgnHeaders.get('BlackTitle'); + final playerName = side == Side.white + ? pgnHeaders.get('White') ?? context.l10n.white + : pgnHeaders.get('Black') ?? context.l10n.black; + + final brightness = Theme.of(context).brightness; + + return TableCell( + verticalAlignment: TableCellVerticalAlignment.top, + child: Center( + child: Padding( + padding: const EdgeInsets.only(bottom: 5), + child: Column( + children: [ + Icon( + side == Side.white + ? brightness == Brightness.light + ? CupertinoIcons.circle + : CupertinoIcons.circle_filled + : brightness == Brightness.light + ? CupertinoIcons.circle_filled + : CupertinoIcons.circle, + size: 14, + ), + Text( + '${playerTitle != null ? '$playerTitle ' : ''}$playerName', + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + softWrap: true, + ), + ], + ), + ), + ), + ); + } +} + +class AcplChart extends ConsumerWidget { + const AcplChart(this.pgn, this.options); + + final String pgn; + final AnalysisOptions options; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final mainLineColor = Theme.of(context).colorScheme.secondary; + // yes it looks like below/above are inverted in fl_chart + final brightness = Theme.of(context).brightness; + final white = Theme.of(context).colorScheme.surfaceContainerHighest; + final black = Theme.of(context).colorScheme.outline; + // yes it looks like below/above are inverted in fl_chart + final belowLineColor = brightness == Brightness.light ? white : black; + final aboveLineColor = brightness == Brightness.light ? black : white; + + VerticalLine phaseVerticalBar(double x, String label) => VerticalLine( + x: x, + color: const Color(0xFF707070), + strokeWidth: 0.5, + label: VerticalLineLabel( + style: TextStyle( + fontSize: 10, + color: Theme.of(context) + .textTheme + .labelMedium + ?.color + ?.withValues(alpha: 0.3), + ), + labelResolver: (line) => label, + padding: const EdgeInsets.only(right: 1), + alignment: Alignment.topRight, + direction: LabelDirection.vertical, + show: true, + ), + ); + + final data = ref.watch( + analysisControllerProvider(pgn, options) + .select((value) => value.acplChartData), + ); + + final rootPly = ref.watch( + analysisControllerProvider(pgn, options) + .select((value) => value.root.position.ply), + ); + + final currentNode = ref.watch( + analysisControllerProvider(pgn, options) + .select((value) => value.currentNode), + ); + + final isOnMainline = ref.watch( + analysisControllerProvider(pgn, options) + .select((value) => value.isOnMainline), + ); + + if (data == null) { + return const SizedBox.shrink(); + } + + final spots = data + .mapIndexed( + (i, e) => FlSpot(i.toDouble(), e.winningChances(Side.white)), + ) + .toList(growable: false); + + final divisionLines = []; + + if (options.division?.middlegame != null) { + if (options.division!.middlegame! > 0) { + divisionLines.add(phaseVerticalBar(0.0, context.l10n.opening)); + divisionLines.add( + phaseVerticalBar( + options.division!.middlegame! - 1, + context.l10n.middlegame, + ), + ); + } else { + divisionLines.add(phaseVerticalBar(0.0, context.l10n.middlegame)); + } + } + + if (options.division?.endgame != null) { + if (options.division!.endgame! > 0) { + divisionLines.add( + phaseVerticalBar( + options.division!.endgame! - 1, + context.l10n.endgame, + ), + ); + } else { + divisionLines.add( + phaseVerticalBar( + 0.0, + context.l10n.endgame, + ), + ); + } + } + return Center( + child: AspectRatio( + aspectRatio: 2.5, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: LineChart( + LineChartData( + lineTouchData: LineTouchData( + enabled: false, + touchCallback: + (FlTouchEvent event, LineTouchResponse? touchResponse) { + if (event is FlTapDownEvent || + event is FlPanUpdateEvent || + event is FlLongPressMoveUpdate) { + final touchX = event.localPosition!.dx; + final chartWidth = context.size!.width - + 32; // Insets on both sides of the chart of 16 + final minX = spots.first.x; + final maxX = spots.last.x; + final touchXDataValue = + minX + (touchX / chartWidth) * (maxX - minX); + final closestSpot = spots.reduce( + (a, b) => (a.x - touchXDataValue).abs() < + (b.x - touchXDataValue).abs() + ? a + : b, + ); + final closestNodeIndex = closestSpot.x.round(); + ref + .read(analysisControllerProvider(pgn, options).notifier) + .jumpToNthNodeOnMainline(closestNodeIndex); + } + }, + ), + minY: -1.0, + maxY: 1.0, + lineBarsData: [ + LineChartBarData( + spots: spots, + isCurved: false, + barWidth: 1, + color: mainLineColor.withValues(alpha: 0.7), + aboveBarData: BarAreaData( + show: true, + color: aboveLineColor, + applyCutOffY: true, + ), + belowBarData: BarAreaData( + show: true, + color: belowLineColor, + applyCutOffY: true, + ), + dotData: const FlDotData( + show: false, + ), + ), + ], + extraLinesData: ExtraLinesData( + verticalLines: [ + if (isOnMainline) + VerticalLine( + x: (currentNode.position.ply - 1 - rootPly).toDouble(), + color: mainLineColor, + strokeWidth: 1.0, + ), + ...divisionLines, + ], + ), + gridData: const FlGridData(show: false), + borderData: FlBorderData(show: false), + titlesData: const FlTitlesData(show: false), + ), + ), + ), + ), + ); + } +} diff --git a/lib/src/view/analysis/engine_depth.dart b/lib/src/view/analysis/engine_depth.dart new file mode 100644 index 0000000000..6743a52882 --- /dev/null +++ b/lib/src/view/analysis/engine_depth.dart @@ -0,0 +1,125 @@ +import 'dart:math' as math; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; +import 'package:lichess_mobile/src/model/engine/engine.dart'; +import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/list.dart'; +import 'package:popover/popover.dart'; + +class EngineDepth extends ConsumerWidget { + const EngineDepth(this.ctrlProvider); + + final AnalysisControllerProvider ctrlProvider; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isEngineAvailable = ref.watch( + ctrlProvider.select( + (value) => value.isEngineAvailable, + ), + ); + final currentNode = ref.watch( + ctrlProvider.select((value) => value.currentNode), + ); + final depth = ref.watch( + engineEvaluationProvider.select((value) => value.eval?.depth), + ) ?? + currentNode.eval?.depth; + + return isEngineAvailable && depth != null + ? AppBarTextButton( + onPressed: () { + showPopover( + context: context, + bodyBuilder: (context) { + return _StockfishInfo(currentNode); + }, + direction: PopoverDirection.top, + width: 240, + backgroundColor: + Theme.of(context).platform == TargetPlatform.android + ? Theme.of(context).dialogBackgroundColor + : CupertinoDynamicColor.resolve( + CupertinoColors.tertiarySystemBackground, + context, + ), + transitionDuration: Duration.zero, + popoverTransitionBuilder: (_, child) => child, + ); + }, + child: RepaintBoundary( + child: Container( + width: 20.0, + height: 20.0, + padding: const EdgeInsets.all(2.0), + decoration: BoxDecoration( + color: Theme.of(context).platform == TargetPlatform.android + ? Theme.of(context).colorScheme.secondary + : CupertinoTheme.of(context).primaryColor, + borderRadius: BorderRadius.circular(4.0), + ), + child: FittedBox( + fit: BoxFit.contain, + child: Text( + '${math.min(99, depth)}', + style: TextStyle( + color: Theme.of(context).platform == + TargetPlatform.android + ? Theme.of(context).colorScheme.onSecondary + : CupertinoTheme.of(context).primaryContrastingColor, + fontFeatures: const [ + FontFeature.tabularFigures(), + ], + ), + ), + ), + ), + ), + ) + : const SizedBox.shrink(); + } +} + +class _StockfishInfo extends ConsumerWidget { + const _StockfishInfo(this.currentNode); + + final AnalysisCurrentNode currentNode; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final (engineName: engineName, eval: eval, state: engineState) = + ref.watch(engineEvaluationProvider); + + final currentEval = eval ?? currentNode.eval; + + final knps = engineState == EngineState.computing + ? ', ${eval?.knps.round()}kn/s' + : ''; + final depth = currentEval?.depth ?? 0; + final maxDepth = math.max(depth, kMaxEngineDepth); + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + PlatformListTile( + leading: Image.asset( + 'assets/images/stockfish/icon.png', + width: 44, + height: 44, + ), + title: Text(engineName), + subtitle: Text( + context.l10n.depthX( + '$depth/$maxDepth$knps', + ), + ), + ), + ], + ); + } +} diff --git a/lib/src/view/broadcast/broadcast_analysis_screen.dart b/lib/src/view/broadcast/broadcast_analysis_screen.dart index 39770b92f9..9c970a9722 100644 --- a/lib/src/view/broadcast/broadcast_analysis_screen.dart +++ b/lib/src/view/broadcast/broadcast_analysis_screen.dart @@ -1,54 +1,25 @@ -import 'dart:math' as math; - -import 'package:collection/collection.dart'; import 'package:dartchess/dartchess.dart'; -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:lichess_mobile/src/constants.dart'; -import 'package:lichess_mobile/src/model/account/account_preferences.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; -import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; -import 'package:lichess_mobile/src/model/analysis/server_analysis_service.dart'; -import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_game_controller.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_round_controller.dart'; -import 'package:lichess_mobile/src/model/common/eval.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/model/engine/engine.dart'; -import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; -import 'package:lichess_mobile/src/model/game/game_share_service.dart'; -import 'package:lichess_mobile/src/model/settings/brightness.dart'; -import 'package:lichess_mobile/src/network/connectivity.dart'; import 'package:lichess_mobile/src/network/http.dart'; -import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/duration.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/lichess_assets.dart'; -import 'package:lichess_mobile/src/utils/navigation.dart'; -import 'package:lichess_mobile/src/utils/screen.dart'; -import 'package:lichess_mobile/src/utils/share.dart'; -import 'package:lichess_mobile/src/utils/string.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_board.dart'; +import 'package:lichess_mobile/src/view/analysis/analysis_screen_body.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_settings.dart'; -import 'package:lichess_mobile/src/view/analysis/analysis_share_screen.dart'; -import 'package:lichess_mobile/src/view/analysis/tree_view.dart'; -import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; -import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_screen.dart'; -import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; +import 'package:lichess_mobile/src/view/analysis/engine_depth.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; -import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; -import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; -import 'package:lichess_mobile/src/widgets/feedback.dart'; -import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; -import 'package:popover/popover.dart'; class BroadcastAnalysisScreen extends ConsumerWidget { final BroadcastRoundId roundId; @@ -70,6 +41,56 @@ class BroadcastAnalysisScreen extends ConsumerWidget { ); } + Widget broadcastWrapBoardBuilder( + WidgetRef ref, + AnalysisBoard board, + AnalysisControllerProvider ctrlProvider, + double boardSize, + ) { + final clocks = ref.watch(ctrlProvider.select((value) => value.clocks)); + final currentPath = ref.watch( + ctrlProvider.select((value) => value.currentPath), + ); + final broadcastLivePath = ref.watch( + ctrlProvider.select((value) => value.broadcastLivePath), + ); + final playingSide = + ref.watch(ctrlProvider.select((value) => value.position.turn)); + final game = ref.watch( + broadcastRoundControllerProvider(roundId) + .select((game) => game.value?[gameId]), + ); + final pov = ref.watch(ctrlProvider.select((value) => value.pov)); + + return Column( + children: [ + if (game != null) + _PlayerWidget( + clock: (playingSide == pov.opposite) + ? clocks?.parentClock + : clocks?.clock, + width: boardSize, + game: game, + side: pov.opposite, + boardSide: _PlayerWidgetSide.top, + playingSide: playingSide, + playClock: currentPath == broadcastLivePath, + ), + board, + if (game != null) + _PlayerWidget( + clock: (playingSide == pov) ? clocks?.parentClock : clocks?.clock, + width: boardSize, + game: game, + side: pov, + boardSide: _PlayerWidgetSide.bottom, + playingSide: playingSide, + playClock: currentPath == broadcastLivePath, + ), + ], + ); + } + Widget _androidBuilder(BuildContext context, WidgetRef ref) { final pgn = ref.watch( broadcastGameControllerProvider( @@ -84,7 +105,7 @@ class BroadcastAnalysisScreen extends ConsumerWidget { title: Text(title), actions: [ if (pgn.hasValue) - _EngineDepth( + EngineDepth( analysisControllerProvider( pgn.requireValue, AnalysisState.broadcastOptions, @@ -109,10 +130,11 @@ class BroadcastAnalysisScreen extends ConsumerWidget { ], ), body: pgn.when( - data: (pgn) => _Body( - roundId: roundId, - gameId: gameId, + data: (pgn) => AnalysisScreenBody( pgn: pgn, + options: AnalysisState.broadcastOptions, + enableDrawingShapes: true, + broadcastWrapBoardBuilder: broadcastWrapBoardBuilder, ), loading: () => const Center(child: CircularProgressIndicator.adaptive()), @@ -144,7 +166,7 @@ class BroadcastAnalysisScreen extends ConsumerWidget { mainAxisSize: MainAxisSize.min, children: [ if (pgn.hasValue) - _EngineDepth( + EngineDepth( analysisControllerProvider( pgn.requireValue, AnalysisState.broadcastOptions, @@ -170,10 +192,11 @@ class BroadcastAnalysisScreen extends ConsumerWidget { ), ), child: pgn.when( - data: (pgn) => _Body( - roundId: roundId, - gameId: gameId, + data: (pgn) => AnalysisScreenBody( pgn: pgn, + options: AnalysisState.broadcastOptions, + enableDrawingShapes: true, + broadcastWrapBoardBuilder: broadcastWrapBoardBuilder, ), loading: () => const Center(child: CircularProgressIndicator.adaptive()), @@ -187,258 +210,8 @@ class BroadcastAnalysisScreen extends ConsumerWidget { } } -class _Body extends ConsumerWidget { - const _Body({ - required this.roundId, - required this.gameId, - required this.pgn, - }); - - final BroadcastRoundId roundId; - final BroadcastGameId gameId; - final String pgn; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final ctrlProvider = - analysisControllerProvider(pgn, AnalysisState.broadcastOptions); - final showEvaluationGauge = ref.watch( - analysisPreferencesProvider.select((value) => value.showEvaluationGauge), - ); - - final isEngineAvailable = ref.watch( - ctrlProvider.select( - (value) => value.isEngineAvailable, - ), - ); - - final hasEval = - ref.watch(ctrlProvider.select((value) => value.hasAvailableEval)); - - final displayMode = - ref.watch(ctrlProvider.select((value) => value.displayMode)); - - final pov = ref.watch(ctrlProvider.select((value) => value.pov)); - - return Column( - children: [ - Expanded( - child: SafeArea( - bottom: false, - child: LayoutBuilder( - builder: (context, constraints) { - final aspectRatio = constraints.biggest.aspectRatio; - final defaultBoardSize = constraints.biggest.shortestSide; - final isTablet = isTabletOrLarger(context); - final remainingHeight = - constraints.maxHeight - defaultBoardSize; - final isSmallScreen = - remainingHeight < kSmallRemainingHeightLeftBoardThreshold; - final boardSize = isTablet || isSmallScreen - ? defaultBoardSize - kTabletBoardTableSidePadding * 2 - : defaultBoardSize; - - const tabletBoardRadius = - BorderRadius.all(Radius.circular(4.0)); - - final display = switch (displayMode) { - DisplayMode.summary => - ServerAnalysisSummary(pgn, AnalysisState.broadcastOptions), - DisplayMode.moves => AnalysisTreeView( - pgn, - AnalysisState.broadcastOptions, - aspectRatio > 1 - ? Orientation.landscape - : Orientation.portrait, - ), - }; - - // If the aspect ratio is greater than 1, we are in landscape mode. - if (aspectRatio > 1) { - return Row( - mainAxisSize: MainAxisSize.max, - children: [ - Padding( - padding: const EdgeInsets.only( - left: kTabletBoardTableSidePadding, - top: kTabletBoardTableSidePadding, - bottom: kTabletBoardTableSidePadding, - ), - child: Row( - children: [ - _AnalysisBoardPlayersAndClocks( - ctrlProvider, - roundId, - gameId, - pgn, - pov, - boardSize, - borderRadius: isTablet ? tabletBoardRadius : null, - ), - if (hasEval && showEvaluationGauge) ...[ - const SizedBox(width: 4.0), - _EngineGaugeVertical(ctrlProvider), - ], - ], - ), - ), - Flexible( - fit: FlexFit.loose, - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - if (isEngineAvailable) - _EngineLines( - ctrlProvider, - isLandscape: true, - ), - Expanded( - child: PlatformCard( - clipBehavior: Clip.hardEdge, - borderRadius: const BorderRadius.all( - Radius.circular(4.0), - ), - margin: const EdgeInsets.all( - kTabletBoardTableSidePadding, - ), - semanticContainer: false, - child: display, - ), - ), - ], - ), - ), - ], - ); - } - // If the aspect ratio is less than 1, we are in portrait mode. - else { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - _ColumnTopTable(ctrlProvider), - if (isTablet) - Padding( - padding: const EdgeInsets.all( - kTabletBoardTableSidePadding, - ), - child: _AnalysisBoardPlayersAndClocks( - ctrlProvider, - roundId, - gameId, - pgn, - pov, - boardSize, - borderRadius: isTablet ? tabletBoardRadius : null, - ), - ) - else - _AnalysisBoardPlayersAndClocks( - ctrlProvider, - roundId, - gameId, - pgn, - pov, - boardSize, - borderRadius: isTablet ? tabletBoardRadius : null, - ), - Expanded( - child: Padding( - padding: isTablet - ? const EdgeInsets.symmetric( - horizontal: kTabletBoardTableSidePadding, - ) - : EdgeInsets.zero, - child: display, - ), - ), - ], - ); - } - }, - ), - ), - ), - _BottomBar(pgn: pgn, options: AnalysisState.broadcastOptions), - ], - ); - } -} - enum _PlayerWidgetSide { bottom, top } -class _AnalysisBoardPlayersAndClocks extends ConsumerWidget { - final AnalysisControllerProvider ctrlProvider; - final BroadcastRoundId roundId; - final BroadcastGameId gameId; - final String pgn; - final Side pov; - final double boardSize; - final BorderRadius? borderRadius; - - const _AnalysisBoardPlayersAndClocks( - this.ctrlProvider, - this.roundId, - this.gameId, - this.pgn, - this.pov, - this.boardSize, { - required this.borderRadius, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final clocks = ref.watch(ctrlProvider.select((value) => value.clocks)); - final currentPath = ref.watch( - ctrlProvider.select((value) => value.currentPath), - ); - final broadcastLivePath = ref.watch( - ctrlProvider.select((value) => value.broadcastLivePath), - ); - final playingSide = - ref.watch(ctrlProvider.select((value) => value.position.turn)); - final game = ref.watch( - broadcastRoundControllerProvider(roundId) - .select((game) => game.value?[gameId]), - ); - - return Column( - children: [ - if (game != null) - _PlayerWidget( - clock: (playingSide == pov.opposite) - ? clocks?.parentClock - : clocks?.clock, - width: boardSize, - game: game, - side: pov.opposite, - boardSide: _PlayerWidgetSide.top, - playingSide: playingSide, - playClock: currentPath == broadcastLivePath, - ), - AnalysisBoard( - pgn, - AnalysisState.broadcastOptions, - boardSize, - borderRadius: borderRadius, - ), - if (game != null) - _PlayerWidget( - clock: (playingSide == pov) ? clocks?.parentClock : clocks?.clock, - width: boardSize, - game: game, - side: pov, - boardSide: _PlayerWidgetSide.bottom, - playingSide: playingSide, - playClock: currentPath == broadcastLivePath, - ), - ], - ); - } -} - class _PlayerWidget extends StatelessWidget { const _PlayerWidget({ required this.width, @@ -633,971 +406,3 @@ class _PlayerWidget extends StatelessWidget { ); } } - -class _EngineGaugeVertical extends ConsumerWidget { - const _EngineGaugeVertical(this.ctrlProvider); - - final AnalysisControllerProvider ctrlProvider; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final analysisState = ref.watch(ctrlProvider); - - return Container( - clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4.0), - ), - child: EngineGauge( - displayMode: EngineGaugeDisplayMode.vertical, - params: analysisState.engineGaugeParams, - ), - ); - } -} - -class _ColumnTopTable extends ConsumerWidget { - const _ColumnTopTable(this.ctrlProvider); - - final AnalysisControllerProvider ctrlProvider; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final analysisState = ref.watch(ctrlProvider); - final showEvaluationGauge = ref.watch( - analysisPreferencesProvider.select((p) => p.showEvaluationGauge), - ); - - return analysisState.hasAvailableEval - ? Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (showEvaluationGauge) - EngineGauge( - displayMode: EngineGaugeDisplayMode.horizontal, - params: analysisState.engineGaugeParams, - ), - if (analysisState.isEngineAvailable) - _EngineLines(ctrlProvider, isLandscape: false), - ], - ) - : kEmptyWidget; - } -} - -class _EngineLines extends ConsumerWidget { - const _EngineLines(this.ctrlProvider, {required this.isLandscape}); - final AnalysisControllerProvider ctrlProvider; - final bool isLandscape; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final analysisState = ref.watch(ctrlProvider); - final numEvalLines = ref.watch( - analysisPreferencesProvider.select( - (p) => p.numEvalLines, - ), - ); - final engineEval = ref.watch(engineEvaluationProvider).eval; - final eval = engineEval ?? analysisState.currentNode.eval; - - final emptyLines = List.filled( - numEvalLines, - _Engineline.empty(ctrlProvider), - ); - - final content = !analysisState.position.isGameOver - ? (eval != null - ? eval.pvs - .take(numEvalLines) - .map( - (pv) => _Engineline(ctrlProvider, eval.position, pv), - ) - .toList() - : emptyLines) - : emptyLines; - - if (content.length < numEvalLines) { - final padding = List.filled( - numEvalLines - content.length, - _Engineline.empty(ctrlProvider), - ); - content.addAll(padding); - } - - return Padding( - padding: EdgeInsets.symmetric( - vertical: isLandscape ? kTabletBoardTableSidePadding : 0.0, - horizontal: isLandscape ? kTabletBoardTableSidePadding : 0.0, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: content, - ), - ); - } -} - -class _Engineline extends ConsumerWidget { - const _Engineline( - this.ctrlProvider, - this.fromPosition, - this.pvData, - ); - - const _Engineline.empty(this.ctrlProvider) - : pvData = const PvData(moves: IListConst([])), - fromPosition = Chess.initial; - - final AnalysisControllerProvider ctrlProvider; - final Position fromPosition; - final PvData pvData; - - @override - Widget build(BuildContext context, WidgetRef ref) { - if (pvData.moves.isEmpty) { - return const SizedBox( - height: kEvalGaugeSize, - child: SizedBox.shrink(), - ); - } - - final pieceNotation = ref.watch(pieceNotationProvider).maybeWhen( - data: (value) => value, - orElse: () => defaultAccountPreferences.pieceNotation, - ); - - final lineBuffer = StringBuffer(); - int ply = fromPosition.ply + 1; - pvData.sanMoves(fromPosition).forEachIndexed((i, s) { - lineBuffer.write( - ply.isOdd - ? '${(ply / 2).ceil()}. $s ' - : i == 0 - ? '${(ply / 2).ceil()}... $s ' - : '$s ', - ); - ply += 1; - }); - - final brightness = ref.watch(currentBrightnessProvider); - - final evalString = pvData.evalString; - return AdaptiveInkWell( - onTap: () => ref - .read(ctrlProvider.notifier) - .onUserMove(NormalMove.fromUci(pvData.moves[0])), - child: SizedBox( - height: kEvalGaugeSize, - child: Padding( - padding: const EdgeInsets.all(2.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Container( - decoration: BoxDecoration( - color: pvData.winningSide == Side.black - ? EngineGauge.backgroundColor(context, brightness) - : EngineGauge.valueColor(context, brightness), - borderRadius: BorderRadius.circular(4.0), - ), - padding: const EdgeInsets.symmetric( - horizontal: 4.0, - vertical: 2.0, - ), - child: Text( - evalString, - style: TextStyle( - color: pvData.winningSide == Side.black - ? Colors.white - : Colors.black, - fontSize: kEvalGaugeFontSize, - fontWeight: FontWeight.w600, - ), - ), - ), - const SizedBox(width: 8.0), - Expanded( - child: Text( - lineBuffer.toString(), - maxLines: 1, - softWrap: false, - style: TextStyle( - fontFamily: pieceNotation == PieceNotation.symbol - ? 'ChessFont' - : null, - ), - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ), - ), - ); - } -} - -class _BottomBar extends ConsumerWidget { - const _BottomBar({ - required this.pgn, - required this.options, - }); - - final String pgn; - final AnalysisOptions options; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final ctrlProvider = analysisControllerProvider(pgn, options); - final analysisState = ref.watch(ctrlProvider); - final isOnline = - ref.watch(connectivityChangesProvider).valueOrNull?.isOnline ?? false; - - return BottomBar( - children: [ - BottomBarButton( - label: context.l10n.menu, - onTap: () { - _showAnalysisMenu(context, ref); - }, - icon: Icons.menu, - ), - if (analysisState.canShowGameSummary) - BottomBarButton( - // TODO: l10n - label: analysisState.displayMode == DisplayMode.summary - ? 'Moves' - : 'Summary', - onTap: () { - final newMode = analysisState.displayMode == DisplayMode.summary - ? DisplayMode.moves - : DisplayMode.summary; - ref.read(ctrlProvider.notifier).setDisplayMode(newMode); - }, - icon: analysisState.displayMode == DisplayMode.summary - ? LichessIcons.flow_cascade - : Icons.area_chart, - ), - BottomBarButton( - label: context.l10n.openingExplorer, - onTap: isOnline - ? () { - pushPlatformRoute( - context, - title: context.l10n.openingExplorer, - builder: (_) => OpeningExplorerScreen( - pgn: ref.read(ctrlProvider.notifier).makeCurrentNodePgn(), - options: analysisState.openingExplorerOptions, - ), - ); - } - : null, - icon: Icons.explore, - ), - RepeatButton( - onLongPress: - analysisState.canGoBack ? () => _moveBackward(ref) : null, - child: BottomBarButton( - key: const ValueKey('goto-previous'), - onTap: analysisState.canGoBack ? () => _moveBackward(ref) : null, - label: 'Previous', - icon: CupertinoIcons.chevron_back, - showTooltip: false, - ), - ), - RepeatButton( - onLongPress: analysisState.canGoNext ? () => _moveForward(ref) : null, - child: BottomBarButton( - key: const ValueKey('goto-next'), - icon: CupertinoIcons.chevron_forward, - label: context.l10n.next, - onTap: analysisState.canGoNext ? () => _moveForward(ref) : null, - showTooltip: false, - ), - ), - ], - ); - } - - void _moveForward(WidgetRef ref) => - ref.read(analysisControllerProvider(pgn, options).notifier).userNext(); - void _moveBackward(WidgetRef ref) => ref - .read(analysisControllerProvider(pgn, options).notifier) - .userPrevious(); - - Future _showAnalysisMenu(BuildContext context, WidgetRef ref) { - return showAdaptiveActionSheet( - context: context, - actions: [ - BottomSheetAction( - makeLabel: (context) => Text(context.l10n.flipBoard), - onPressed: (context) { - ref - .read(analysisControllerProvider(pgn, options).notifier) - .toggleBoard(); - }, - ), - BottomSheetAction( - makeLabel: (context) => Text(context.l10n.mobileShareGamePGN), - onPressed: (_) { - pushPlatformRoute( - context, - title: context.l10n.studyShareAndExport, - builder: (_) => AnalysisShareScreen(pgn: pgn, options: options), - ); - }, - ), - BottomSheetAction( - makeLabel: (context) => Text(context.l10n.mobileSharePositionAsFEN), - onPressed: (_) { - launchShareDialog( - context, - text: ref - .read(analysisControllerProvider(pgn, options)) - .position - .fen, - ); - }, - ), - if (options.gameAnyId != null) - BottomSheetAction( - makeLabel: (context) => - Text(context.l10n.screenshotCurrentPosition), - onPressed: (_) async { - final gameId = options.gameAnyId!.gameId; - final state = ref.read(analysisControllerProvider(pgn, options)); - try { - final image = - await ref.read(gameShareServiceProvider).screenshotPosition( - gameId, - options.orientation, - state.position.fen, - state.lastMove, - ); - if (context.mounted) { - launchShareDialog( - context, - files: [image], - subject: context.l10n.puzzleFromGameLink( - lichessUri('/$gameId').toString(), - ), - ); - } - } catch (e) { - if (context.mounted) { - showPlatformSnackbar( - context, - 'Failed to get GIF', - type: SnackBarType.error, - ); - } - } - }, - ), - ], - ); - } -} - -class _EngineDepth extends ConsumerWidget { - const _EngineDepth(this.ctrlProvider); - - final AnalysisControllerProvider ctrlProvider; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isEngineAvailable = ref.watch( - ctrlProvider.select( - (value) => value.isEngineAvailable, - ), - ); - final currentNode = ref.watch( - ctrlProvider.select((value) => value.currentNode), - ); - final depth = ref.watch( - engineEvaluationProvider.select((value) => value.eval?.depth), - ) ?? - currentNode.eval?.depth; - - return isEngineAvailable && depth != null - ? AppBarTextButton( - onPressed: () { - showPopover( - context: context, - bodyBuilder: (context) { - return _StockfishInfo(currentNode); - }, - direction: PopoverDirection.top, - width: 240, - backgroundColor: - Theme.of(context).platform == TargetPlatform.android - ? Theme.of(context).dialogBackgroundColor - : CupertinoDynamicColor.resolve( - CupertinoColors.tertiarySystemBackground, - context, - ), - transitionDuration: Duration.zero, - popoverTransitionBuilder: (_, child) => child, - ); - }, - child: RepaintBoundary( - child: Container( - width: 20.0, - height: 20.0, - padding: const EdgeInsets.all(2.0), - decoration: BoxDecoration( - color: Theme.of(context).platform == TargetPlatform.android - ? Theme.of(context).colorScheme.secondary - : CupertinoTheme.of(context).primaryColor, - borderRadius: BorderRadius.circular(4.0), - ), - child: FittedBox( - fit: BoxFit.contain, - child: Text( - '${math.min(99, depth)}', - style: TextStyle( - color: Theme.of(context).platform == - TargetPlatform.android - ? Theme.of(context).colorScheme.onSecondary - : CupertinoTheme.of(context).primaryContrastingColor, - fontFeatures: const [ - FontFeature.tabularFigures(), - ], - ), - ), - ), - ), - ), - ) - : const SizedBox.shrink(); - } -} - -class _StockfishInfo extends ConsumerWidget { - const _StockfishInfo(this.currentNode); - - final AnalysisCurrentNode currentNode; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final (engineName: engineName, eval: eval, state: engineState) = - ref.watch(engineEvaluationProvider); - - final currentEval = eval ?? currentNode.eval; - - final knps = engineState == EngineState.computing - ? ', ${eval?.knps.round()}kn/s' - : ''; - final depth = currentEval?.depth ?? 0; - final maxDepth = math.max(depth, kMaxEngineDepth); - - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - PlatformListTile( - leading: Image.asset( - 'assets/images/stockfish/icon.png', - width: 44, - height: 44, - ), - title: Text(engineName), - subtitle: Text( - context.l10n.depthX( - '$depth/$maxDepth$knps', - ), - ), - ), - ], - ); - } -} - -class ServerAnalysisSummary extends ConsumerWidget { - const ServerAnalysisSummary(this.pgn, this.options); - - final String pgn; - final AnalysisOptions options; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final ctrlProvider = analysisControllerProvider(pgn, options); - final playersAnalysis = - ref.watch(ctrlProvider.select((value) => value.playersAnalysis)); - final pgnHeaders = - ref.watch(ctrlProvider.select((value) => value.pgnHeaders)); - final currentGameAnalysis = ref.watch(currentAnalysisProvider); - - return playersAnalysis != null - ? ListView( - children: [ - if (currentGameAnalysis == options.gameAnyId?.gameId) - const Padding( - padding: EdgeInsets.only(top: 16.0), - child: WaitingForServerAnalysis(), - ), - AcplChart(pgn, options), - Center( - child: SizedBox( - width: math.min(MediaQuery.sizeOf(context).width, 500), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Table( - defaultVerticalAlignment: - TableCellVerticalAlignment.middle, - columnWidths: const { - 0: FlexColumnWidth(1), - 1: FlexColumnWidth(1), - 2: FlexColumnWidth(1), - }, - children: [ - TableRow( - decoration: const BoxDecoration( - border: Border( - bottom: BorderSide(color: Colors.grey), - ), - ), - children: [ - _SummaryPlayerName(Side.white, pgnHeaders), - Center( - child: Text( - pgnHeaders.get('Result') ?? '', - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - ), - ), - _SummaryPlayerName(Side.black, pgnHeaders), - ], - ), - if (playersAnalysis.white.accuracy != null && - playersAnalysis.black.accuracy != null) - TableRow( - children: [ - _SummaryNumber( - '${playersAnalysis.white.accuracy}%', - ), - Center( - heightFactor: 1.8, - child: Text( - context.l10n.accuracy, - softWrap: true, - ), - ), - _SummaryNumber( - '${playersAnalysis.black.accuracy}%', - ), - ], - ), - for (final item in [ - ( - playersAnalysis.white.inaccuracies.toString(), - context.l10n - .nbInaccuracies(2) - .replaceAll('2', '') - .trim() - .capitalize(), - playersAnalysis.black.inaccuracies.toString() - ), - ( - playersAnalysis.white.mistakes.toString(), - context.l10n - .nbMistakes(2) - .replaceAll('2', '') - .trim() - .capitalize(), - playersAnalysis.black.mistakes.toString() - ), - ( - playersAnalysis.white.blunders.toString(), - context.l10n - .nbBlunders(2) - .replaceAll('2', '') - .trim() - .capitalize(), - playersAnalysis.black.blunders.toString() - ), - ]) - TableRow( - children: [ - _SummaryNumber(item.$1), - Center( - heightFactor: 1.2, - child: Text( - item.$2, - softWrap: true, - ), - ), - _SummaryNumber(item.$3), - ], - ), - if (playersAnalysis.white.acpl != null && - playersAnalysis.black.acpl != null) - TableRow( - children: [ - _SummaryNumber( - playersAnalysis.white.acpl.toString(), - ), - Center( - heightFactor: 1.5, - child: Text( - context.l10n.averageCentipawnLoss, - softWrap: true, - textAlign: TextAlign.center, - ), - ), - _SummaryNumber( - playersAnalysis.black.acpl.toString(), - ), - ], - ), - ], - ), - ), - ), - ), - ], - ) - : Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Spacer(), - if (currentGameAnalysis == options.gameAnyId?.gameId) - const Center( - child: Padding( - padding: EdgeInsets.symmetric(vertical: 16.0), - child: WaitingForServerAnalysis(), - ), - ) - else - Center( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0), - child: Builder( - builder: (context) { - Future? pendingRequest; - return StatefulBuilder( - builder: (context, setState) { - return FutureBuilder( - future: pendingRequest, - builder: (context, snapshot) { - return SecondaryButton( - semanticsLabel: - context.l10n.requestAComputerAnalysis, - onPressed: ref.watch(authSessionProvider) == - null - ? () { - showPlatformSnackbar( - context, - context - .l10n.youNeedAnAccountToDoThat, - ); - } - : snapshot.connectionState == - ConnectionState.waiting - ? null - : () { - setState(() { - pendingRequest = ref - .read(ctrlProvider.notifier) - .requestServerAnalysis() - .catchError((Object e) { - if (context.mounted) { - showPlatformSnackbar( - context, - e.toString(), - type: SnackBarType.error, - ); - } - }); - }); - }, - child: Text( - context.l10n.requestAComputerAnalysis, - ), - ); - }, - ); - }, - ); - }, - ), - ), - ), - const Spacer(), - ], - ); - } -} - -class WaitingForServerAnalysis extends StatelessWidget { - const WaitingForServerAnalysis({super.key}); - - @override - Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.max, - children: [ - Image.asset( - 'assets/images/stockfish/icon.png', - width: 30, - height: 30, - ), - const SizedBox(width: 8.0), - Text(context.l10n.waitingForAnalysis), - const SizedBox(width: 8.0), - const CircularProgressIndicator.adaptive(), - ], - ); - } -} - -class _SummaryNumber extends StatelessWidget { - const _SummaryNumber(this.data); - final String data; - - @override - Widget build(BuildContext context) { - return Center( - child: Text( - data, - softWrap: true, - ), - ); - } -} - -class _SummaryPlayerName extends StatelessWidget { - const _SummaryPlayerName(this.side, this.pgnHeaders); - final Side side; - final IMap pgnHeaders; - - @override - Widget build(BuildContext context) { - final playerTitle = side == Side.white - ? pgnHeaders.get('WhiteTitle') - : pgnHeaders.get('BlackTitle'); - final playerName = side == Side.white - ? pgnHeaders.get('White') ?? context.l10n.white - : pgnHeaders.get('Black') ?? context.l10n.black; - - final brightness = Theme.of(context).brightness; - - return TableCell( - verticalAlignment: TableCellVerticalAlignment.top, - child: Center( - child: Padding( - padding: const EdgeInsets.only(bottom: 5), - child: Column( - children: [ - Icon( - side == Side.white - ? brightness == Brightness.light - ? CupertinoIcons.circle - : CupertinoIcons.circle_filled - : brightness == Brightness.light - ? CupertinoIcons.circle_filled - : CupertinoIcons.circle, - size: 14, - ), - Text( - '${playerTitle != null ? '$playerTitle ' : ''}$playerName', - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - textAlign: TextAlign.center, - softWrap: true, - ), - ], - ), - ), - ), - ); - } -} - -class AcplChart extends ConsumerWidget { - const AcplChart(this.pgn, this.options); - - final String pgn; - final AnalysisOptions options; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final mainLineColor = Theme.of(context).colorScheme.secondary; - // yes it looks like below/above are inverted in fl_chart - final brightness = Theme.of(context).brightness; - final white = Theme.of(context).colorScheme.surfaceContainerHighest; - final black = Theme.of(context).colorScheme.outline; - // yes it looks like below/above are inverted in fl_chart - final belowLineColor = brightness == Brightness.light ? white : black; - final aboveLineColor = brightness == Brightness.light ? black : white; - - VerticalLine phaseVerticalBar(double x, String label) => VerticalLine( - x: x, - color: const Color(0xFF707070), - strokeWidth: 0.5, - label: VerticalLineLabel( - style: TextStyle( - fontSize: 10, - color: Theme.of(context) - .textTheme - .labelMedium - ?.color - ?.withValues(alpha: 0.3), - ), - labelResolver: (line) => label, - padding: const EdgeInsets.only(right: 1), - alignment: Alignment.topRight, - direction: LabelDirection.vertical, - show: true, - ), - ); - - final data = ref.watch( - analysisControllerProvider(pgn, options) - .select((value) => value.acplChartData), - ); - - final rootPly = ref.watch( - analysisControllerProvider(pgn, options) - .select((value) => value.root.position.ply), - ); - - final currentNode = ref.watch( - analysisControllerProvider(pgn, options) - .select((value) => value.currentNode), - ); - - final isOnMainline = ref.watch( - analysisControllerProvider(pgn, options) - .select((value) => value.isOnMainline), - ); - - if (data == null) { - return const SizedBox.shrink(); - } - - final spots = data - .mapIndexed( - (i, e) => FlSpot(i.toDouble(), e.winningChances(Side.white)), - ) - .toList(growable: false); - - final divisionLines = []; - - if (options.division?.middlegame != null) { - if (options.division!.middlegame! > 0) { - divisionLines.add(phaseVerticalBar(0.0, context.l10n.opening)); - divisionLines.add( - phaseVerticalBar( - options.division!.middlegame! - 1, - context.l10n.middlegame, - ), - ); - } else { - divisionLines.add(phaseVerticalBar(0.0, context.l10n.middlegame)); - } - } - - if (options.division?.endgame != null) { - if (options.division!.endgame! > 0) { - divisionLines.add( - phaseVerticalBar( - options.division!.endgame! - 1, - context.l10n.endgame, - ), - ); - } else { - divisionLines.add( - phaseVerticalBar( - 0.0, - context.l10n.endgame, - ), - ); - } - } - return Center( - child: AspectRatio( - aspectRatio: 2.5, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: LineChart( - LineChartData( - lineTouchData: LineTouchData( - enabled: false, - touchCallback: - (FlTouchEvent event, LineTouchResponse? touchResponse) { - if (event is FlTapDownEvent || - event is FlPanUpdateEvent || - event is FlLongPressMoveUpdate) { - final touchX = event.localPosition!.dx; - final chartWidth = context.size!.width - - 32; // Insets on both sides of the chart of 16 - final minX = spots.first.x; - final maxX = spots.last.x; - final touchXDataValue = - minX + (touchX / chartWidth) * (maxX - minX); - final closestSpot = spots.reduce( - (a, b) => (a.x - touchXDataValue).abs() < - (b.x - touchXDataValue).abs() - ? a - : b, - ); - final closestNodeIndex = closestSpot.x.round(); - ref - .read(analysisControllerProvider(pgn, options).notifier) - .jumpToNthNodeOnMainline(closestNodeIndex); - } - }, - ), - minY: -1.0, - maxY: 1.0, - lineBarsData: [ - LineChartBarData( - spots: spots, - isCurved: false, - barWidth: 1, - color: mainLineColor.withValues(alpha: 0.7), - aboveBarData: BarAreaData( - show: true, - color: aboveLineColor, - applyCutOffY: true, - ), - belowBarData: BarAreaData( - show: true, - color: belowLineColor, - applyCutOffY: true, - ), - dotData: const FlDotData( - show: false, - ), - ), - ], - extraLinesData: ExtraLinesData( - verticalLines: [ - if (isOnMainline) - VerticalLine( - x: (currentNode.position.ply - 1 - rootPly).toDouble(), - color: mainLineColor, - strokeWidth: 1.0, - ), - ...divisionLines, - ], - ), - gridData: const FlGridData(show: false), - borderData: FlBorderData(show: false), - titlesData: const FlTitlesData(show: false), - ), - ), - ), - ), - ); - } -} diff --git a/lib/src/view/game/game_result_dialog.dart b/lib/src/view/game/game_result_dialog.dart index 8dc98cf67f..4595595e71 100644 --- a/lib/src/view/game/game_result_dialog.dart +++ b/lib/src/view/game/game_result_dialog.dart @@ -22,6 +22,7 @@ import 'package:lichess_mobile/src/model/game/playable_game.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; +import 'package:lichess_mobile/src/view/analysis/analysis_screen_body.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/pgn.dart'; From aa931e0206e73b03ff8e52ed39482c45a3955cbf Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Sat, 2 Nov 2024 10:28:28 +0100 Subject: [PATCH 578/979] use study translation strings --- lib/src/view/study/study_list_screen.dart | 27 +++++++++++------------ 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/lib/src/view/study/study_list_screen.dart b/lib/src/view/study/study_list_screen.dart index e1ffabca41..e0527197eb 100644 --- a/lib/src/view/study/study_list_screen.dart +++ b/lib/src/view/study/study_list_screen.dart @@ -23,25 +23,24 @@ import 'package:timeago/timeago.dart' as timeago; final _logger = Logger('StudyListScreen'); -// TODO l10n String studyCategoryL10n(StudyCategory category, BuildContext context) => switch (category) { - StudyCategory.all => 'All', - StudyCategory.mine => 'Mine', - StudyCategory.member => 'Member', - StudyCategory.public => 'Public', - StudyCategory.private => 'Private', - StudyCategory.likes => 'Liked', + StudyCategory.all => context.l10n.studyAllStudies, + StudyCategory.mine => context.l10n.studyMyStudies, + StudyCategory.member => context.l10n.studyStudiesIContributeTo, + StudyCategory.public => context.l10n.studyMyPublicStudies, + StudyCategory.private => context.l10n.studyMyPrivateStudies, + StudyCategory.likes => context.l10n.studyMyFavoriteStudies, }; // TODO l10n String studyListOrderL10n(StudyListOrder order, BuildContext context) => switch (order) { - StudyListOrder.hot => 'Hot', - StudyListOrder.newest => 'Newest', - StudyListOrder.oldest => 'Oldest', - StudyListOrder.updated => 'Updated', - StudyListOrder.popular => 'Popular', + StudyListOrder.hot => context.l10n.studyHot, + StudyListOrder.newest => context.l10n.studyDateAddedNewest, + StudyListOrder.oldest => context.l10n.studyDateAddedOldest, + StudyListOrder.updated => context.l10n.studyRecentlyUpdated, + StudyListOrder.popular => context.l10n.studyMostPopular, }; /// A screen that displays a paginated list of studies @@ -99,7 +98,7 @@ class _StudyFilterSheet extends ConsumerWidget { // If we're not logged in, the only category available is "All" if (isLoggedIn) ...[ Filter( - // TODO l10n + // TODO mobile l10n filterName: 'Category', filterType: FilterType.singleChoice, choices: StudyCategory.values, @@ -113,7 +112,7 @@ class _StudyFilterSheet extends ConsumerWidget { const SizedBox(height: 10.0), ], Filter( - // TODO l10n + // TODO mobile l10n filterName: 'Sort by', filterType: FilterType.singleChoice, choices: StudyListOrder.values, From 43af6701c36396a3376b9a8a8b7fc7b1896f2e27 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Sun, 3 Nov 2024 11:28:55 +0100 Subject: [PATCH 579/979] remove unused `user.followsYou` field from API response data in general it's a good idea to only type and read the data that the app actually uses --- lib/src/model/user/user.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/src/model/user/user.dart b/lib/src/model/user/user.dart index 38fc137c5c..1f71c8bf9a 100644 --- a/lib/src/model/user/user.dart +++ b/lib/src/model/user/user.dart @@ -85,7 +85,6 @@ class User with _$User { bool? followable, bool? following, bool? blocking, - bool? followsYou, bool? canChallenge, }) = _User; @@ -128,7 +127,6 @@ class User with _$User { followable: pick('followable').asBoolOrNull(), following: pick('following').asBoolOrNull(), blocking: pick('blocking').asBoolOrNull(), - followsYou: pick('followsYou').asBoolOrNull(), canChallenge: pick('canChallenge').asBoolOrNull(), ); } From f804a4232a32a1b694a4238afbaf2a013cc8edf2 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Sat, 2 Nov 2024 14:48:12 +0100 Subject: [PATCH 580/979] Tweak home card opacity --- lib/src/view/home/home_tab_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/view/home/home_tab_screen.dart b/lib/src/view/home/home_tab_screen.dart index 6abf2eefaf..0505976032 100644 --- a/lib/src/view/home/home_tab_screen.dart +++ b/lib/src/view/home/home_tab_screen.dart @@ -733,7 +733,7 @@ class _GamePreviewCarouselItem extends StatelessWidget { @override Widget build(BuildContext context) { return Opacity( - opacity: game.speed != Speed.correspondence || game.isMyTurn ? 1.0 : 0.6, + opacity: game.speed != Speed.correspondence || game.isMyTurn ? 1.0 : 0.7, child: BoardCarouselItem( fen: game.fen, orientation: game.orientation, From 22d07270567ec54559ab7ce32b3e3e46e7b0c364 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Sun, 3 Nov 2024 21:31:06 +0100 Subject: [PATCH 581/979] Simplify study list title --- lib/src/model/study/study_filter.dart | 22 +++++++++++++++-- lib/src/view/study/study_list_screen.dart | 29 +++-------------------- 2 files changed, 23 insertions(+), 28 deletions(-) diff --git a/lib/src/model/study/study_filter.dart b/lib/src/model/study/study_filter.dart index 74edc31f94..45246f3dcf 100644 --- a/lib/src/model/study/study_filter.dart +++ b/lib/src/model/study/study_filter.dart @@ -1,4 +1,5 @@ import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:lichess_mobile/l10n/l10n.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'study_filter.freezed.dart'; @@ -10,7 +11,16 @@ enum StudyCategory { member, public, private, - likes, + likes; + + String l10n(AppLocalizations l10n) => switch (this) { + StudyCategory.all => l10n.studyAllStudies, + StudyCategory.mine => l10n.studyMyStudies, + StudyCategory.member => l10n.studyStudiesIContributeTo, + StudyCategory.public => l10n.studyMyPublicStudies, + StudyCategory.private => l10n.studyMyPrivateStudies, + StudyCategory.likes => l10n.studyMyFavoriteStudies, + }; } enum StudyListOrder { @@ -18,7 +28,15 @@ enum StudyListOrder { popular, newest, oldest, - updated, + updated; + + String l10n(AppLocalizations l10n) => switch (this) { + StudyListOrder.hot => l10n.studyHot, + StudyListOrder.newest => l10n.studyDateAddedNewest, + StudyListOrder.oldest => l10n.studyDateAddedOldest, + StudyListOrder.updated => l10n.studyRecentlyUpdated, + StudyListOrder.popular => l10n.studyMostPopular, + }; } @riverpod diff --git a/lib/src/view/study/study_list_screen.dart b/lib/src/view/study/study_list_screen.dart index e0527197eb..5edc732da5 100644 --- a/lib/src/view/study/study_list_screen.dart +++ b/lib/src/view/study/study_list_screen.dart @@ -23,26 +23,6 @@ import 'package:timeago/timeago.dart' as timeago; final _logger = Logger('StudyListScreen'); -String studyCategoryL10n(StudyCategory category, BuildContext context) => - switch (category) { - StudyCategory.all => context.l10n.studyAllStudies, - StudyCategory.mine => context.l10n.studyMyStudies, - StudyCategory.member => context.l10n.studyStudiesIContributeTo, - StudyCategory.public => context.l10n.studyMyPublicStudies, - StudyCategory.private => context.l10n.studyMyPrivateStudies, - StudyCategory.likes => context.l10n.studyMyFavoriteStudies, - }; - -// TODO l10n -String studyListOrderL10n(StudyListOrder order, BuildContext context) => - switch (order) { - StudyListOrder.hot => context.l10n.studyHot, - StudyListOrder.newest => context.l10n.studyDateAddedNewest, - StudyListOrder.oldest => context.l10n.studyDateAddedOldest, - StudyListOrder.updated => context.l10n.studyRecentlyUpdated, - StudyListOrder.popular => context.l10n.studyMostPopular, - }; - /// A screen that displays a paginated list of studies class StudyListScreen extends ConsumerWidget { const StudyListScreen({super.key}); @@ -52,10 +32,8 @@ class StudyListScreen extends ConsumerWidget { final isLoggedIn = ref.watch(authSessionProvider)?.user.id != null; final filter = ref.watch(studyFilterProvider); - final categorySection = - isLoggedIn ? ' • ${studyCategoryL10n(filter.category, context)}' : ''; final title = Text( - '${context.l10n.studyMenu}$categorySection • ${studyListOrderL10n(filter.order, context)}', + isLoggedIn ? filter.category.l10n(context.l10n) : context.l10n.studyMenu, ); return PlatformScaffold( @@ -103,8 +81,7 @@ class _StudyFilterSheet extends ConsumerWidget { filterType: FilterType.singleChoice, choices: StudyCategory.values, choiceSelected: (choice) => filter.category == choice, - choiceLabel: (category) => - Text(studyCategoryL10n(category, context)), + choiceLabel: (category) => Text(category.l10n(context.l10n)), onSelected: (value, selected) => ref.read(studyFilterProvider.notifier).setCategory(value), ), @@ -117,7 +94,7 @@ class _StudyFilterSheet extends ConsumerWidget { filterType: FilterType.singleChoice, choices: StudyListOrder.values, choiceSelected: (choice) => filter.order == choice, - choiceLabel: (order) => Text(studyListOrderL10n(order, context)), + choiceLabel: (order) => Text(order.l10n(context.l10n)), onSelected: (value, selected) => ref.read(studyFilterProvider.notifier).setOrder(value), ), From d6019c7ba4a942c1fa517401b6f57e039c6a3ce4 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Sun, 3 Nov 2024 21:50:17 +0100 Subject: [PATCH 582/979] Fix overflow in study filters bottom sheet --- lib/src/view/study/study_list_screen.dart | 63 +++++++++++------------ lib/src/widgets/filter.dart | 10 ++-- 2 files changed, 35 insertions(+), 38 deletions(-) diff --git a/lib/src/view/study/study_list_screen.dart b/lib/src/view/study/study_list_screen.dart index 5edc732da5..3c9ed65b56 100644 --- a/lib/src/view/study/study_list_screen.dart +++ b/lib/src/view/study/study_list_screen.dart @@ -42,9 +42,12 @@ class StudyListScreen extends ConsumerWidget { actions: [ AppBarIconButton( icon: const Icon(Icons.tune), + // TODO: translate semanticsLabel: 'Filter studies', onPressed: () => showAdaptiveBottomSheet( context: context, + isScrollControlled: true, + showDragHandle: true, builder: (_) => _StudyFilterSheet( isLoggedIn: isLoggedIn, ), @@ -66,41 +69,33 @@ class _StudyFilterSheet extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final filter = ref.watch(studyFilterProvider); - return SafeArea( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox(height: 12.0), - // If we're not logged in, the only category available is "All" - if (isLoggedIn) ...[ - Filter( - // TODO mobile l10n - filterName: 'Category', - filterType: FilterType.singleChoice, - choices: StudyCategory.values, - choiceSelected: (choice) => filter.category == choice, - choiceLabel: (category) => Text(category.l10n(context.l10n)), - onSelected: (value, selected) => - ref.read(studyFilterProvider.notifier).setCategory(value), - ), - const PlatformDivider(thickness: 1, indent: 0), - const SizedBox(height: 10.0), - ], - Filter( - // TODO mobile l10n - filterName: 'Sort by', - filterType: FilterType.singleChoice, - choices: StudyListOrder.values, - choiceSelected: (choice) => filter.order == choice, - choiceLabel: (order) => Text(order.l10n(context.l10n)), - onSelected: (value, selected) => - ref.read(studyFilterProvider.notifier).setOrder(value), - ), - ], + return BottomSheetScrollableContainer( + padding: const EdgeInsets.all(16.0), + children: [ + // If we're not logged in, the only category available is "All" + if (isLoggedIn) ...[ + Filter( + filterType: FilterType.singleChoice, + choices: StudyCategory.values, + choiceSelected: (choice) => filter.category == choice, + choiceLabel: (category) => Text(category.l10n(context.l10n)), + onSelected: (value, selected) => + ref.read(studyFilterProvider.notifier).setCategory(value), + ), + const PlatformDivider(thickness: 1, indent: 0), + const SizedBox(height: 10.0), + ], + Filter( + // TODO mobile l10n + filterName: 'Sort by', + filterType: FilterType.singleChoice, + choices: StudyListOrder.values, + choiceSelected: (choice) => filter.order == choice, + choiceLabel: (order) => Text(order.l10n(context.l10n)), + onSelected: (value, selected) => + ref.read(studyFilterProvider.notifier).setOrder(value), ), - ), + ], ); } } diff --git a/lib/src/widgets/filter.dart b/lib/src/widgets/filter.dart index b39084c51e..cfabd0de83 100644 --- a/lib/src/widgets/filter.dart +++ b/lib/src/widgets/filter.dart @@ -11,7 +11,7 @@ enum FilterType { /// Displays a row of choices that can be selected or deselected. class Filter extends StatelessWidget { const Filter({ - required this.filterName, + this.filterName, required this.filterType, required this.choices, this.showCheckmark = true, @@ -21,7 +21,7 @@ class Filter extends StatelessWidget { }); /// Will be displayed above the choices as a title. - final String filterName; + final String? filterName; /// Controls how choices in a [Filter] are displayed. final FilterType filterType; @@ -46,8 +46,10 @@ class Filter extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(filterName, style: const TextStyle(fontSize: 18)), - const SizedBox(height: 10), + if (filterName != null) ...[ + Text(filterName!, style: const TextStyle(fontSize: 18)), + const SizedBox(height: 10), + ], SizedBox( width: double.infinity, child: Wrap( From a562ae7f0ec5fbb06f154428f2ae75db96494fb0 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Sun, 3 Nov 2024 21:51:12 +0100 Subject: [PATCH 583/979] Remove study tools entry for now --- lib/src/view/tools/tools_tab_screen.dart | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/lib/src/view/tools/tools_tab_screen.dart b/lib/src/view/tools/tools_tab_screen.dart index b071761fd4..678a5d5f3e 100644 --- a/lib/src/view/tools/tools_tab_screen.dart +++ b/lib/src/view/tools/tools_tab_screen.dart @@ -15,7 +15,6 @@ import 'package:lichess_mobile/src/view/board_editor/board_editor_screen.dart'; import 'package:lichess_mobile/src/view/clock/clock_screen.dart'; import 'package:lichess_mobile/src/view/coordinate_training/coordinate_training_screen.dart'; import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_screen.dart'; -import 'package:lichess_mobile/src/view/study/study_list_screen.dart'; import 'package:lichess_mobile/src/view/tools/load_position_screen.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; @@ -177,16 +176,6 @@ class _Body extends ConsumerWidget { ) : null, ), - if (isOnline) - _ToolsButton( - icon: LichessIcons.study, - title: context.l10n.studyMenu, - onTap: () => pushPlatformRoute( - context, - builder: (context) => const StudyListScreen(), - rootNavigator: true, - ), - ), _ToolsButton( icon: Icons.edit_outlined, title: context.l10n.boardEditor, From a846f0422fb7942b1824cc23916414d0df1bf68f Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Sun, 3 Nov 2024 21:53:19 +0100 Subject: [PATCH 584/979] Remove unused import --- lib/src/view/tools/tools_tab_screen.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/src/view/tools/tools_tab_screen.dart b/lib/src/view/tools/tools_tab_screen.dart index 678a5d5f3e..62cd402965 100644 --- a/lib/src/view/tools/tools_tab_screen.dart +++ b/lib/src/view/tools/tools_tab_screen.dart @@ -6,7 +6,6 @@ import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/navigation.dart'; import 'package:lichess_mobile/src/network/connectivity.dart'; -import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; From 52356618e9efddd35f84434391d0c09f08e6f3f4 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Sun, 3 Nov 2024 22:54:01 +0100 Subject: [PATCH 585/979] Remove file --- test_navigator.dart | 29 ----------------------------- 1 file changed, 29 deletions(-) delete mode 100644 test_navigator.dart diff --git a/test_navigator.dart b/test_navigator.dart deleted file mode 100644 index 473ce7176c..0000000000 --- a/test_navigator.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:flutter/widgets.dart'; - -class TestNavigatorObserver extends NavigatorObserver { - void Function(Route route, Route? previousRoute)? onPushed; - void Function(Route route, Route? previousRoute)? onPopped; - void Function(Route route, Route? previousRoute)? onRemoved; - void Function(Route? route, Route? previousRoute)? - onReplaced; - - @override - void didPush(Route route, Route? previousRoute) { - onPushed?.call(route, previousRoute); - } - - @override - void didPop(Route route, Route? previousRoute) { - onPopped?.call(route, previousRoute); - } - - @override - void didRemove(Route route, Route? previousRoute) { - onRemoved?.call(route, previousRoute); - } - - @override - void didReplace({Route? oldRoute, Route? newRoute}) { - onReplaced?.call(newRoute, oldRoute); - } -} From 81ae8de5e260454b18b761d8bdf58d56d2e50ef2 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 4 Nov 2024 11:50:28 +0100 Subject: [PATCH 586/979] Prevent error when socket client is disposed Close #1132 --- lib/src/model/game/game_controller.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/src/model/game/game_controller.dart b/lib/src/model/game/game_controller.dart index aa1f73705f..d2a4ac3791 100644 --- a/lib/src/model/game/game_controller.dart +++ b/lib/src/model/game/game_controller.dart @@ -116,7 +116,9 @@ class GameController extends _$GameController { if (game.playable) { _appLifecycleListener = AppLifecycleListener( onResume: () { - if (_socketClient.isConnected) { + // socket client should never be disposed here, but in case it is + // we can safely skip the resync + if (!_socketClient.isDisposed && _socketClient.isConnected) { _resyncGameData(); } }, From 8e379eb27cb80138f4ce9f7dd9e0343e23d89362 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 4 Nov 2024 12:45:37 +0100 Subject: [PATCH 587/979] WIP on adding a border setting to the board --- lib/src/model/settings/board_preferences.dart | 13 +++++++++ lib/src/view/settings/theme_screen.dart | 28 +++++++++++-------- pubspec.lock | 9 +++--- pubspec.yaml | 3 +- 4 files changed, 36 insertions(+), 17 deletions(-) diff --git a/lib/src/model/settings/board_preferences.dart b/lib/src/model/settings/board_preferences.dart index ca5e4cb208..cc124d7bd2 100644 --- a/lib/src/model/settings/board_preferences.dart +++ b/lib/src/model/settings/board_preferences.dart @@ -2,6 +2,7 @@ import 'package:chessground/chessground.dart'; import 'package:flutter/widgets.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/color_palette.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -63,6 +64,10 @@ class BoardPreferences extends _$BoardPreferences return save(state.copyWith(coordinates: !state.coordinates)); } + Future toggleBorder() { + return save(state.copyWith(showBorder: !state.showBorder)); + } + Future togglePieceAnimation() { return save(state.copyWith(pieceAnimation: !state.pieceAnimation)); } @@ -120,6 +125,7 @@ class BoardPrefs with _$BoardPrefs implements Serializable { unknownEnumValue: ShapeColor.green, ) required ShapeColor shapeColor, + @JsonKey(defaultValue: false) required bool showBorder, }) = _BoardPrefs; static const defaults = BoardPrefs( @@ -136,12 +142,19 @@ class BoardPrefs with _$BoardPrefs implements Serializable { enableShapeDrawings: true, magnifyDraggedPiece: true, shapeColor: ShapeColor.green, + showBorder: false, ); ChessboardSettings toBoardSettings() { return ChessboardSettings( pieceAssets: pieceSet.assets, colorScheme: boardTheme.colors, + border: showBorder + ? BoardBorder( + color: darken(boardTheme.colors.darkSquare, 0.2), + width: 16.0, + ) + : null, showValidMoves: showLegalMoves, showLastMove: boardHighlights, enableCoordinates: coordinates, diff --git a/lib/src/view/settings/theme_screen.dart b/lib/src/view/settings/theme_screen.dart index f3b4b79ea3..e0db758b15 100644 --- a/lib/src/view/settings/theme_screen.dart +++ b/lib/src/view/settings/theme_screen.dart @@ -2,7 +2,6 @@ import 'dart:math' as math; import 'package:chessground/chessground.dart'; import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/constants.dart'; @@ -46,7 +45,7 @@ class _Body extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final boardPrefs = ref.watch(boardPreferencesProvider); - const horizontalPadding = 52.0; + const horizontalPadding = 16.0; return SafeArea( child: ListView( @@ -54,7 +53,7 @@ class _Body extends ConsumerWidget { LayoutBuilder( builder: (context, constraints) { final double boardSize = math.min( - 290, + 400, constraints.biggest.shortestSide - horizontalPadding * 2, ); return Padding( @@ -80,14 +79,12 @@ class _Body extends ConsumerWidget { dest: Square.fromName('c6'), ), }.lock, - settings: ChessboardSettings( - enableCoordinates: false, - borderRadius: - const BorderRadius.all(Radius.circular(4.0)), - boxShadow: boardShadows, - pieceAssets: boardPrefs.pieceSet.assets, - colorScheme: boardPrefs.boardTheme.colors, - ), + settings: boardPrefs.toBoardSettings().copyWith( + enableCoordinates: true, + borderRadius: + const BorderRadius.all(Radius.circular(4.0)), + boxShadow: boardShadows, + ), ), ), ); @@ -154,6 +151,15 @@ class _Body extends ConsumerWidget { ); }, ), + SwitchSettingTile( + // TODO translate + leading: const Icon(Icons.border_outer), + title: const Text('Show border'), + value: boardPrefs.showBorder, + onChanged: (value) { + ref.read(boardPreferencesProvider.notifier).toggleBorder(); + }, + ), ], ), ], diff --git a/pubspec.lock b/pubspec.lock index d8cefb1c94..b8965259ad 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -193,11 +193,10 @@ packages: chessground: dependency: "direct main" description: - name: chessground - sha256: "363e3c408ef360807ee2d04ee6a2caab2d63fa0f66542cda2005927300da379e" - url: "https://pub.dev" - source: hosted - version: "5.3.0" + path: "../flutter-chessground" + relative: true + source: path + version: "5.4.0" ci: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 385aff56d0..edda956ed7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,7 +13,8 @@ dependencies: app_settings: ^5.1.1 async: ^2.10.0 cached_network_image: ^3.2.2 - chessground: ^5.2.0 + chessground: + path: ../flutter-chessground collection: ^1.17.0 connectivity_plus: ^6.0.2 cronet_http: ^1.3.1 From 891b3d46c5f47694d8e247ba0ce6389d3003b2f0 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Mon, 4 Nov 2024 17:36:31 +0100 Subject: [PATCH 588/979] correct debug messages related to broadcast id --- lib/src/model/common/id.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/model/common/id.dart b/lib/src/model/common/id.dart index 15d4ea9b8e..783c6f3bb6 100644 --- a/lib/src/model/common/id.dart +++ b/lib/src/model/common/id.dart @@ -167,7 +167,7 @@ extension IDPick on Pick { return BroadcastTournamentId(value); } throw PickException( - "value $value at $debugParsingExit can't be casted to BroadcastRoundId", + "value $value at $debugParsingExit can't be casted to BroadcastTournamentId", ); } @@ -205,7 +205,7 @@ extension IDPick on Pick { return BroadcastGameId(value); } throw PickException( - "value $value at $debugParsingExit can't be casted to BroadcastRoundId", + "value $value at $debugParsingExit can't be casted to BroadcastGameId", ); } From afdfcaf4abadd39ff9d0e2b639c66f32e33a90c8 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Mon, 4 Nov 2024 17:43:26 +0100 Subject: [PATCH 589/979] correct broadcast analysis option id --- lib/src/model/analysis/analysis_controller.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index ef6b3c44fd..0d17218450 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -821,7 +821,7 @@ class AnalysisState with _$AnalysisState { ); static AnalysisOptions get broadcastOptions => const AnalysisOptions( - id: standaloneAnalysisId, + id: standaloneBroadcastId, isLocalEvaluationAllowed: true, orientation: Side.white, variant: Variant.standard, From 3903d79d57b626b9c3c482428d5314362391e59f Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Mon, 4 Nov 2024 17:46:58 +0100 Subject: [PATCH 590/979] rename BroadcastAnalysisScreen to BroadcastGameAnalysisScreen --- lib/src/view/broadcast/broadcast_boards_tab.dart | 4 ++-- ...alysis_screen.dart => broadcast_game_analysis_screen.dart} | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) rename lib/src/view/broadcast/{broadcast_analysis_screen.dart => broadcast_game_analysis_screen.dart} (99%) diff --git a/lib/src/view/broadcast/broadcast_boards_tab.dart b/lib/src/view/broadcast/broadcast_boards_tab.dart index 221baabb97..3598e8f63c 100644 --- a/lib/src/view/broadcast/broadcast_boards_tab.dart +++ b/lib/src/view/broadcast/broadcast_boards_tab.dart @@ -15,7 +15,7 @@ import 'package:lichess_mobile/src/utils/duration.dart'; import 'package:lichess_mobile/src/utils/lichess_assets.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; -import 'package:lichess_mobile/src/view/broadcast/broadcast_analysis_screen.dart'; +import 'package:lichess_mobile/src/view/broadcast/broadcast_game_analysis_screen.dart'; import 'package:lichess_mobile/src/widgets/board_thumbnail.dart'; import 'package:lichess_mobile/src/widgets/evaluation_bar.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; @@ -139,7 +139,7 @@ class BroadcastPreview extends ConsumerWidget { onTap: () { pushPlatformRoute( context, - builder: (context) => BroadcastAnalysisScreen( + builder: (context) => BroadcastGameAnalysisScreen( roundId: roundId, gameId: game.id, title: title, diff --git a/lib/src/view/broadcast/broadcast_analysis_screen.dart b/lib/src/view/broadcast/broadcast_game_analysis_screen.dart similarity index 99% rename from lib/src/view/broadcast/broadcast_analysis_screen.dart rename to lib/src/view/broadcast/broadcast_game_analysis_screen.dart index 9c970a9722..fb03f58def 100644 --- a/lib/src/view/broadcast/broadcast_analysis_screen.dart +++ b/lib/src/view/broadcast/broadcast_game_analysis_screen.dart @@ -21,12 +21,12 @@ import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; -class BroadcastAnalysisScreen extends ConsumerWidget { +class BroadcastGameAnalysisScreen extends ConsumerWidget { final BroadcastRoundId roundId; final BroadcastGameId gameId; final String title; - const BroadcastAnalysisScreen({ + const BroadcastGameAnalysisScreen({ required this.roundId, required this.gameId, required this.title, From c666bb1fc5daed539b8e5ad1be326d64063b85df Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Mon, 4 Nov 2024 19:16:46 +0100 Subject: [PATCH 591/979] use strong type for broadcast game result --- lib/src/model/broadcast/broadcast.dart | 10 +++- .../model/broadcast/broadcast_repository.dart | 48 ++++++++++++------- .../view/broadcast/broadcast_boards_tab.dart | 6 +-- .../broadcast_game_analysis_screen.dart | 14 ++---- 4 files changed, 47 insertions(+), 31 deletions(-) diff --git a/lib/src/model/broadcast/broadcast.dart b/lib/src/model/broadcast/broadcast.dart index d906906181..e257583352 100644 --- a/lib/src/model/broadcast/broadcast.dart +++ b/lib/src/model/broadcast/broadcast.dart @@ -12,6 +12,8 @@ typedef BroadcastsList = ({ int? nextPage, }); +enum BroadcastResult { whiteWins, blackWins, draw, ongoing, noResultPgnTag } + @freezed class Broadcast with _$Broadcast { const Broadcast._(); @@ -93,13 +95,17 @@ class BroadcastGame with _$BroadcastGame { required IMap players, required String fen, required Move? lastMove, - required String? status, + required BroadcastResult status, /// The amount of time that the player whose turn it is has been thinking since his last move required Duration thinkTime, }) = _BroadcastGame; - bool get isPlaying => status == '*'; + bool get isPlaying => status == BroadcastResult.ongoing; + bool get isOver => + status == BroadcastResult.draw || + status == BroadcastResult.whiteWins || + status == BroadcastResult.blackWins; Side get playingSide => Setup.parseFen(fen).turn; Duration? get timeLeft { final clock = players[playingSide]!.clock; diff --git a/lib/src/model/broadcast/broadcast_repository.dart b/lib/src/model/broadcast/broadcast_repository.dart index addc1f2245..c474795ec7 100644 --- a/lib/src/model/broadcast/broadcast_repository.dart +++ b/lib/src/model/broadcast/broadcast_repository.dart @@ -150,23 +150,37 @@ BroadcastRoundGames _gamesFromPick( MapEntry gameFromPick( RequiredPick pick, -) => - MapEntry( - pick('id').asBroadcastGameIdOrThrow(), - BroadcastGame( - id: pick('id').asBroadcastGameIdOrThrow(), - players: IMap({ - Side.white: _playerFromPick(pick('players', 0).required()), - Side.black: _playerFromPick(pick('players', 1).required()), - }), - fen: pick('fen').asStringOrNull() ?? - Variant.standard.initialPosition.fen, - lastMove: pick('lastMove').asUciMoveOrNull(), - status: pick('status').asStringOrNull(), - thinkTime: - pick('thinkTime').asDurationFromSecondsOrNull() ?? Duration.zero, - ), - ); +) { + final stringStatus = pick('status').asStringOrNull(); + + final status = (stringStatus == null) + ? BroadcastResult.noResultPgnTag + : switch (stringStatus) { + '½-½' => BroadcastResult.draw, + '1-0' => BroadcastResult.whiteWins, + '0-1' => BroadcastResult.blackWins, + '*' => BroadcastResult.ongoing, + _ => throw FormatException( + "value $stringStatus can't be interpreted as a broadcast result", + ) + }; + + return MapEntry( + pick('id').asBroadcastGameIdOrThrow(), + BroadcastGame( + id: pick('id').asBroadcastGameIdOrThrow(), + players: IMap({ + Side.white: _playerFromPick(pick('players', 0).required()), + Side.black: _playerFromPick(pick('players', 1).required()), + }), + fen: pick('fen').asStringOrNull() ?? Variant.standard.initialPosition.fen, + lastMove: pick('lastMove').asUciMoveOrNull(), + status: status, + thinkTime: + pick('thinkTime').asDurationFromSecondsOrNull() ?? Duration.zero, + ), + ); +} BroadcastPlayer _playerFromPick(RequiredPick pick) { return BroadcastPlayer( diff --git a/lib/src/view/broadcast/broadcast_boards_tab.dart b/lib/src/view/broadcast/broadcast_boards_tab.dart index 3598e8f63c..7b62cfb7bb 100644 --- a/lib/src/view/broadcast/broadcast_boards_tab.dart +++ b/lib/src/view/broadcast/broadcast_boards_tab.dart @@ -259,11 +259,11 @@ class _PlayerWidget extends StatelessWidget { ), ), const SizedBox(width: 5), - if (gameStatus != null && gameStatus != '*') + if (game.isOver) Text( - (gameStatus == '½-½') + (gameStatus == BroadcastResult.draw) ? '½' - : (gameStatus == '1-0') + : (gameStatus == BroadcastResult.whiteWins) ? side == Side.white ? '1' : '0' diff --git a/lib/src/view/broadcast/broadcast_game_analysis_screen.dart b/lib/src/view/broadcast/broadcast_game_analysis_screen.dart index fb03f58def..848bc01c6e 100644 --- a/lib/src/view/broadcast/broadcast_game_analysis_screen.dart +++ b/lib/src/view/broadcast/broadcast_game_analysis_screen.dart @@ -240,7 +240,7 @@ class _PlayerWidget extends StatelessWidget { width: width, child: Row( children: [ - if (gameStatus != null && gameStatus != '*') + if (game.isOver) Card( margin: EdgeInsets.zero, shape: RoundedRectangleBorder( @@ -259,9 +259,9 @@ class _PlayerWidget extends StatelessWidget { vertical: 4.0, ), child: Text( - (gameStatus == '½-½') + (gameStatus == BroadcastResult.draw) ? '½' - : (gameStatus == '1-0') + : (gameStatus == BroadcastResult.whiteWins) ? side == Side.white ? '1' : '0' @@ -279,17 +279,13 @@ class _PlayerWidget extends StatelessWidget { shape: RoundedRectangleBorder( borderRadius: BorderRadius.only( topLeft: Radius.circular( - boardSide == _PlayerWidgetSide.top && - (gameStatus == null || gameStatus == '*') - ? 8 - : 0, + boardSide == _PlayerWidgetSide.top && !game.isOver ? 8 : 0, ), topRight: Radius.circular( boardSide == _PlayerWidgetSide.top && clock == null ? 8 : 0, ), bottomLeft: Radius.circular( - boardSide == _PlayerWidgetSide.bottom && - (gameStatus == null || gameStatus == '*') + boardSide == _PlayerWidgetSide.bottom && !game.isOver ? 8 : 0, ), From d805ac305861072f09d4cbb7921db110c475862e Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Tue, 5 Nov 2024 01:00:29 +0100 Subject: [PATCH 592/979] make the request directly rather than through a provider --- .../broadcast/broadcast_game_controller.dart | 7 ++++--- .../model/broadcast/broadcast_providers.dart | 21 ------------------- .../broadcast/broadcast_round_controller.dart | 7 ++++--- 3 files changed, 8 insertions(+), 27 deletions(-) diff --git a/lib/src/model/broadcast/broadcast_game_controller.dart b/lib/src/model/broadcast/broadcast_game_controller.dart index c0e9c3e99f..3bd96c2c11 100644 --- a/lib/src/model/broadcast/broadcast_game_controller.dart +++ b/lib/src/model/broadcast/broadcast_game_controller.dart @@ -2,10 +2,11 @@ import 'dart:async'; import 'package:deep_pick/deep_pick.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; -import 'package:lichess_mobile/src/model/broadcast/broadcast_providers.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast_repository.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/network/socket.dart'; import 'package:lichess_mobile/src/utils/json.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -33,8 +34,8 @@ class BroadcastGameController extends _$BroadcastGameController { _subscription?.cancel(); }); - final pgn = await ref.watch( - broadcastGameProvider(roundId: roundId, gameId: gameId).future, + final pgn = await ref.withClient( + (client) => BroadcastRepository(client).getGame(roundId, gameId), ); return pgn; } diff --git a/lib/src/model/broadcast/broadcast_providers.dart b/lib/src/model/broadcast/broadcast_providers.dart index 4cadb62d0f..a4e9243a5b 100644 --- a/lib/src/model/broadcast/broadcast_providers.dart +++ b/lib/src/model/broadcast/broadcast_providers.dart @@ -53,24 +53,3 @@ Future broadcastTournament( BroadcastRepository(client).getTournament(broadcastTournamentId), ); } - -@riverpod -Future broadcastRound( - Ref ref, - BroadcastRoundId broadcastRoundId, -) { - return ref.withClient( - (client) => BroadcastRepository(client).getRound(broadcastRoundId), - ); -} - -@riverpod -Future broadcastGame( - Ref ref, { - required BroadcastRoundId roundId, - required BroadcastGameId gameId, -}) { - return ref.withClient( - (client) => BroadcastRepository(client).getGame(roundId, gameId), - ); -} diff --git a/lib/src/model/broadcast/broadcast_round_controller.dart b/lib/src/model/broadcast/broadcast_round_controller.dart index d2957ad4e7..9d7c4d0d16 100644 --- a/lib/src/model/broadcast/broadcast_round_controller.dart +++ b/lib/src/model/broadcast/broadcast_round_controller.dart @@ -4,11 +4,11 @@ import 'package:dartchess/dartchess.dart'; import 'package:deep_pick/deep_pick.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; -import 'package:lichess_mobile/src/model/broadcast/broadcast_providers.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_repository.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/network/socket.dart'; import 'package:lichess_mobile/src/utils/json.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -39,8 +39,9 @@ class BroadcastRoundController extends _$BroadcastRoundController { _timer?.cancel(); }); - final games = - await ref.watch(broadcastRoundProvider(broadcastRoundId).future); + final games = await ref.withClient( + (client) => BroadcastRepository(client).getRound(broadcastRoundId), + ); _timer = Timer.periodic( const Duration(seconds: 1), From 7053dc5185d590109453fc5ebd7d998e919cfc03 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Tue, 5 Nov 2024 01:01:49 +0100 Subject: [PATCH 593/979] fix typo --- lib/src/model/broadcast/broadcast_repository.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/model/broadcast/broadcast_repository.dart b/lib/src/model/broadcast/broadcast_repository.dart index c474795ec7..c0ec656569 100644 --- a/lib/src/model/broadcast/broadcast_repository.dart +++ b/lib/src/model/broadcast/broadcast_repository.dart @@ -38,7 +38,7 @@ class BroadcastRepository { ) { return client.readJson( Uri(path: 'api/broadcast/-/-/$broadcastRoundId'), - // The path parameters with - are the broadcast tournament and round slug + // The path parameters with - are the broadcast tournament and round slugs // They are only used for SEO, so we can safely use - for these parameters headers: {'Accept': 'application/x-ndjson'}, mapper: _makeGamesFromJson, From ad802075bc3824367e3601d91cd9f15a373fd274 Mon Sep 17 00:00:00 2001 From: James Taylor Date: Tue, 5 Nov 2024 14:54:44 +0000 Subject: [PATCH 594/979] Added toggle in settings and game settings sheet to switch the clock position to alternate side of screen --- ios/Podfile.lock | 2 +- lib/src/model/settings/board_preferences.dart | 8 ++++++++ lib/src/view/game/game_body.dart | 2 ++ lib/src/view/game/game_player.dart | 5 ++++- lib/src/view/game/game_settings.dart | 12 ++++++++++++ lib/src/view/settings/board_settings_screen.dart | 10 ++++++++++ 6 files changed, 37 insertions(+), 2 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 562392d4a9..cf6c77434d 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -273,4 +273,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 76a583f8d75b3a8c6e4bdc97ae8783ef36cc7984 -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/lib/src/model/settings/board_preferences.dart b/lib/src/model/settings/board_preferences.dart index ca5e4cb208..90bcc2539a 100644 --- a/lib/src/model/settings/board_preferences.dart +++ b/lib/src/model/settings/board_preferences.dart @@ -43,6 +43,8 @@ class BoardPreferences extends _$BoardPreferences return save(state.copyWith(hapticFeedback: !state.hapticFeedback)); } + + Future toggleImmersiveModeWhilePlaying() { return save( state.copyWith( @@ -81,6 +83,10 @@ class BoardPreferences extends _$BoardPreferences ); } + Future toggleSwitchClockPosition() { + return save(state.copyWith(switchClockPosition: !state.switchClockPosition)); + } + Future toggleEnableShapeDrawings() { return save( state.copyWith(enableShapeDrawings: !state.enableShapeDrawings), @@ -106,6 +112,7 @@ class BoardPrefs with _$BoardPrefs implements Serializable { required bool coordinates, required bool pieceAnimation, required bool showMaterialDifference, + required bool switchClockPosition, @JsonKey( defaultValue: PieceShiftMethod.either, unknownEnumValue: PieceShiftMethod.either, @@ -132,6 +139,7 @@ class BoardPrefs with _$BoardPrefs implements Serializable { coordinates: true, pieceAnimation: true, showMaterialDifference: true, + switchClockPosition: false, pieceShiftMethod: PieceShiftMethod.either, enableShapeDrawings: true, magnifyDraggedPiece: true, diff --git a/lib/src/view/game/game_body.dart b/lib/src/view/game/game_body.dart index 790d2f472a..68c6bca89c 100644 --- a/lib/src/view/game/game_body.dart +++ b/lib/src/view/game/game_body.dart @@ -136,6 +136,7 @@ class GameBody extends ConsumerWidget { : null, mePlaying: youAre == Side.black, zenMode: gameState.isZenModeActive, + alternateClockPosition: boardPreferences.switchClockPosition, confirmMoveCallbacks: youAre == Side.black && gameState.moveToConfirm != null ? ( @@ -176,6 +177,7 @@ class GameBody extends ConsumerWidget { : null, mePlaying: youAre == Side.white, zenMode: gameState.isZenModeActive, + alternateClockPosition: boardPreferences.switchClockPosition, confirmMoveCallbacks: youAre == Side.white && gameState.moveToConfirm != null ? ( diff --git a/lib/src/view/game/game_player.dart b/lib/src/view/game/game_player.dart index 619f583af3..7072558a42 100644 --- a/lib/src/view/game/game_player.dart +++ b/lib/src/view/game/game_player.dart @@ -30,6 +30,7 @@ class GamePlayer extends StatelessWidget { this.shouldLinkToUserProfile = true, this.mePlaying = false, this.zenMode = false, + this.alternateClockPosition = false, super.key, }); @@ -43,6 +44,7 @@ class GamePlayer extends StatelessWidget { final bool shouldLinkToUserProfile; final bool mePlaying; final bool zenMode; + final bool alternateClockPosition; /// Time left for the player to move at the start of the game. final Duration? timeToMove; @@ -172,6 +174,7 @@ class GamePlayer extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ + if (clock != null && alternateClockPosition) Flexible(flex: 3, child: clock!), if (mePlaying && confirmMoveCallbacks != null) Expanded( flex: 7, @@ -207,7 +210,7 @@ class GamePlayer extends StatelessWidget { : playerWidget, ), ), - if (clock != null) Flexible(flex: 3, child: clock!), + if (clock != null && !alternateClockPosition) Flexible(flex: 3, child: clock!), ], ); } diff --git a/lib/src/view/game/game_settings.dart b/lib/src/view/game/game_settings.dart index 3785e3333a..e683ebc2e2 100644 --- a/lib/src/view/game/game_settings.dart +++ b/lib/src/view/game/game_settings.dart @@ -120,6 +120,18 @@ class GameSettings extends ConsumerWidget { .toggleShowMaterialDifference(); }, ), + SwitchSettingTile( + //TODO Add i10n + title: Text( + 'Switch clock position', + ), + value: boardPrefs.switchClockPosition, + onChanged: (value) { + ref + .read(boardPreferencesProvider.notifier) + .toggleSwitchClockPosition(); + }, + ), SwitchSettingTile( title: Text( context.l10n.toggleTheChat, diff --git a/lib/src/view/settings/board_settings_screen.dart b/lib/src/view/settings/board_settings_screen.dart index eefd82f4f2..07d6616224 100644 --- a/lib/src/view/settings/board_settings_screen.dart +++ b/lib/src/view/settings/board_settings_screen.dart @@ -195,6 +195,16 @@ class _Body extends ConsumerWidget { .toggleShowMaterialDifference(); }, ), + SwitchSettingTile( + //TODO Add i10n + title: const Text('Switch clock position'), + value: boardPrefs.switchClockPosition, + onChanged: (value) { + ref + .read(boardPreferencesProvider.notifier) + .toggleSwitchClockPosition(); + }, + ), ], ), ], From 3dc341520af82c2e99fecb77697d065e3c42d7d5 Mon Sep 17 00:00:00 2001 From: James Taylor Date: Tue, 5 Nov 2024 15:27:12 +0000 Subject: [PATCH 595/979] Fix formatting --- lib/src/model/settings/board_preferences.dart | 5 ++--- lib/src/view/game/game_player.dart | 6 ++++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/src/model/settings/board_preferences.dart b/lib/src/model/settings/board_preferences.dart index 90bcc2539a..0a61b05848 100644 --- a/lib/src/model/settings/board_preferences.dart +++ b/lib/src/model/settings/board_preferences.dart @@ -43,8 +43,6 @@ class BoardPreferences extends _$BoardPreferences return save(state.copyWith(hapticFeedback: !state.hapticFeedback)); } - - Future toggleImmersiveModeWhilePlaying() { return save( state.copyWith( @@ -84,7 +82,8 @@ class BoardPreferences extends _$BoardPreferences } Future toggleSwitchClockPosition() { - return save(state.copyWith(switchClockPosition: !state.switchClockPosition)); + return save( + state.copyWith(switchClockPosition: !state.switchClockPosition)); } Future toggleEnableShapeDrawings() { diff --git a/lib/src/view/game/game_player.dart b/lib/src/view/game/game_player.dart index 7072558a42..796ee0165f 100644 --- a/lib/src/view/game/game_player.dart +++ b/lib/src/view/game/game_player.dart @@ -174,7 +174,8 @@ class GamePlayer extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ - if (clock != null && alternateClockPosition) Flexible(flex: 3, child: clock!), + if (clock != null && alternateClockPosition) + Flexible(flex: 3, child: clock!), if (mePlaying && confirmMoveCallbacks != null) Expanded( flex: 7, @@ -210,7 +211,8 @@ class GamePlayer extends StatelessWidget { : playerWidget, ), ), - if (clock != null && !alternateClockPosition) Flexible(flex: 3, child: clock!), + if (clock != null && !alternateClockPosition) + Flexible(flex: 3, child: clock!), ], ); } From 55a08eb5705fb1e566dcd08d6a165b6fc16ae712 Mon Sep 17 00:00:00 2001 From: James Taylor Date: Tue, 5 Nov 2024 15:38:42 +0000 Subject: [PATCH 596/979] linter fix --- lib/src/view/game/game_settings.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/view/game/game_settings.dart b/lib/src/view/game/game_settings.dart index e683ebc2e2..37b46d7f4f 100644 --- a/lib/src/view/game/game_settings.dart +++ b/lib/src/view/game/game_settings.dart @@ -122,7 +122,7 @@ class GameSettings extends ConsumerWidget { ), SwitchSettingTile( //TODO Add i10n - title: Text( + title: const Text( 'Switch clock position', ), value: boardPrefs.switchClockPosition, From 06de756f531c753b683b8dcfbca543917b491628 Mon Sep 17 00:00:00 2001 From: James Taylor Date: Tue, 5 Nov 2024 15:43:46 +0000 Subject: [PATCH 597/979] Trailing comma fix --- lib/src/model/settings/board_preferences.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/model/settings/board_preferences.dart b/lib/src/model/settings/board_preferences.dart index 0a61b05848..380873f00c 100644 --- a/lib/src/model/settings/board_preferences.dart +++ b/lib/src/model/settings/board_preferences.dart @@ -83,7 +83,8 @@ class BoardPreferences extends _$BoardPreferences Future toggleSwitchClockPosition() { return save( - state.copyWith(switchClockPosition: !state.switchClockPosition)); + state.copyWith(switchClockPosition: !state.switchClockPosition), + ); } Future toggleEnableShapeDrawings() { From 8fd7199b351ec9a1591d003bd39fa589e2634d95 Mon Sep 17 00:00:00 2001 From: James Taylor Date: Tue, 5 Nov 2024 15:49:05 +0000 Subject: [PATCH 598/979] Format fix --- lib/src/model/settings/board_preferences.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/model/settings/board_preferences.dart b/lib/src/model/settings/board_preferences.dart index 380873f00c..4806a4eb6f 100644 --- a/lib/src/model/settings/board_preferences.dart +++ b/lib/src/model/settings/board_preferences.dart @@ -83,7 +83,7 @@ class BoardPreferences extends _$BoardPreferences Future toggleSwitchClockPosition() { return save( - state.copyWith(switchClockPosition: !state.switchClockPosition), + state.copyWith(switchClockPosition: !state.switchClockPosition), ); } From 066fc166727aadff2f812a4a4b50d8cb8354eed5 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Tue, 5 Nov 2024 18:03:04 +0100 Subject: [PATCH 599/979] feat: add button to collapse lines in pgn tree view --- .../model/analysis/analysis_controller.dart | 1 - lib/src/widgets/pgn.dart | 131 ++++++++++++------ test/view/analysis/analysis_screen_test.dart | 68 +++++++++ 3 files changed, 158 insertions(+), 42 deletions(-) diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index f14ba20ebc..c0ab330c84 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -283,7 +283,6 @@ class AnalysisController extends _$AnalysisController @override void collapseVariations(UciPath path) { final node = _root.nodeAt(path); - for (final child in node.children) { child.isHidden = true; } diff --git a/lib/src/widgets/pgn.dart b/lib/src/widgets/pgn.dart index f480db8883..8d5144facd 100644 --- a/lib/src/widgets/pgn.dart +++ b/lib/src/widgets/pgn.dart @@ -615,6 +615,13 @@ class _SideLinePart extends ConsumerWidget { return moves; }, ).flattened, + if (nodes.last.children.any((node) => !node.isHidden)) + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: _CollapseVariationsButton( + onTap: () => params.notifier.collapseVariations(path), + ), + ), ]; return Text.rich( @@ -625,6 +632,26 @@ class _SideLinePart extends ConsumerWidget { } } +class _CollapseVariationsButton extends StatelessWidget { + const _CollapseVariationsButton({ + required this.onTap, + }); + + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return AdaptiveInkWell( + onTap: onTap, + child: Icon( + Icons.indeterminate_check_box, + color: _textColor(context, 0.6), + size: _baseTextStyle.fontSize! + 5, + ), + ); + } +} + /// A widget that renders part of the mainline. /// /// A part of the mainline is rendered on a single line. See [_mainlineParts]. @@ -650,41 +677,47 @@ class _MainLinePart extends ConsumerWidget { var path = initialPath; return Text.rich( TextSpan( - children: nodes - .takeWhile((node) => node.children.isNotEmpty) - .mapIndexed( - (i, node) { - final mainlineNode = node.children.first; - final moves = [ - _moveWithComment( - mainlineNode, - lineInfo: ( - type: _LineType.mainline, - startLine: i == 0 || (node as ViewBranch).hasTextComment, - pathToLine: initialPath, - ), - pathToNode: path, + children: [ + ...nodes.takeWhile((node) => node.children.isNotEmpty).mapIndexed( + (i, node) { + final mainlineNode = node.children.first; + final moves = [ + _moveWithComment( + mainlineNode, + lineInfo: ( + type: _LineType.mainline, + startLine: i == 0 || (node as ViewBranch).hasTextComment, + pathToLine: initialPath, + ), + pathToNode: path, + textStyle: textStyle, + params: params, + ), + if (node.children.length == 2 && + _displaySideLineAsInline(node.children[1])) ...[ + _buildInlineSideLine( + followsComment: mainlineNode.hasTextComment, + firstNode: node.children[1], + parent: node, + initialPath: path, textStyle: textStyle, params: params, ), - if (node.children.length == 2 && - _displaySideLineAsInline(node.children[1])) ...[ - _buildInlineSideLine( - followsComment: mainlineNode.hasTextComment, - firstNode: node.children[1], - parent: node, - initialPath: path, - textStyle: textStyle, - params: params, - ), - ], - ]; - path = path + mainlineNode.id; - return moves.flattened; - }, - ) - .flattened - .toList(growable: false), + ], + ]; + path = path + mainlineNode.id; + return moves.flattened; + }, + ).flattened, + if (nodes.last.children.skip(1).any((node) => !node.isHidden)) + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: _CollapseVariationsButton( + onTap: () => + params.notifier.collapseVariations(path.penultimate), + ), + ), + ], ), ); } @@ -917,16 +950,11 @@ class _IndentedSideLinesState extends State<_IndentedSideLines> { children: [ ...sideLineWidgets, if (_hasHiddenLines) - GestureDetector( - child: Icon( - Icons.add_box, - color: _textColor(context, 0.6), - key: _sideLinesStartKeys.last, - size: _baseTextStyle.fontSize! + 5, + _ExpandVariationsButton( + key: _sideLinesStartKeys.last, + onTap: () => widget.params.notifier.expandVariations( + widget.initialPath, ), - onTap: () { - widget.params.notifier.expandVariations(widget.initialPath); - }, ), ], ), @@ -935,6 +963,27 @@ class _IndentedSideLinesState extends State<_IndentedSideLines> { } } +class _ExpandVariationsButton extends StatelessWidget { + const _ExpandVariationsButton({ + super.key, + required this.onTap, + }); + + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return AdaptiveInkWell( + onTap: onTap, + child: Icon( + Icons.add_box, + color: _textColor(context, 0.6), + size: _baseTextStyle.fontSize! + 5, + ), + ); + } +} + Color? _textColor( BuildContext context, double opacity, { diff --git a/test/view/analysis/analysis_screen_test.dart b/test/view/analysis/analysis_screen_test.dart index c386d11011..1821982022 100644 --- a/test/view/analysis/analysis_screen_test.dart +++ b/test/view/analysis/analysis_screen_test.dart @@ -255,11 +255,15 @@ void main() { expect(find.text('2… Qd7'), findsNothing); // sidelines with nesting > 2 are collapsed -> expand them + expect(find.byIcon(Icons.indeterminate_check_box), findsNWidgets(2)); expect(find.byIcon(Icons.add_box), findsOneWidget); await tester.tap(find.byIcon(Icons.add_box)); await tester.pumpAndSettle(); + expect(find.byIcon(Icons.indeterminate_check_box), findsNWidgets(3)); + expect(find.byIcon(Icons.add_box), findsNothing); + expectSameLine(tester, ['2… h5']); expectSameLine(tester, ['2… Nc6', '3. d3']); expectSameLine(tester, ['2… Qd7']); @@ -275,6 +279,7 @@ void main() { // Sidelines should be collapsed again expect(find.byIcon(Icons.add_box), findsOneWidget); + expect(find.byIcon(Icons.indeterminate_check_box), findsNWidgets(2)); expect(find.text('2… h5'), findsNothing); expect(find.text('2… Nc6'), findsNothing); @@ -282,6 +287,69 @@ void main() { expect(find.text('2… Qd7'), findsNothing); }); + testWidgets('expand/collapse sidelines via icon', (tester) async { + // Will be rendered as: + // ------------------- + // 1. e4 e5 [-] // <- collapse icon + // |- 1... c5 [-] // <- collapse icon + // |- 2. Nf3 + // |- 2. Nc3 + // |- 2. h4 + // |- 1... h5 + // 2. Ke2 + await buildTree( + tester, + '1. e4 e5 (1... c5 2. Nf3 (2. Nc3) (2. h4)) (1... h5) 2. Ke2', + ); + + // Sidelines should be visible by default + expect(find.byIcon(Icons.add_box), findsNothing); + expect(find.byIcon(Icons.indeterminate_check_box), findsNWidgets(2)); + expect(find.text('1… c5'), findsOneWidget); + expect(find.text('1… h5'), findsOneWidget); + expect(find.text('2. Nc3'), findsOneWidget); + expect(find.text('2. h4'), findsOneWidget); + + await tester.tap(find.byIcon(Icons.indeterminate_check_box).first); + + // need to wait for current move change debounce delay + await tester.pumpAndSettle(); + + // After collapsing the first sideline: + // 1. e4 e5 + // |- [+] // <- expand icon + // 2. Ke2 + expect(find.text('1… c5'), findsNothing); + expect(find.text('1… h5'), findsNothing); + expect(find.text('2. Nc3'), findsNothing); + expect(find.text('2. h4'), findsNothing); + + // Expand again + expect(find.byIcon(Icons.add_box), findsOneWidget); + await tester.tap(find.byIcon(Icons.add_box)); + // need to wait for current move change debounce delay + await tester.pumpAndSettle(); + + // Collapse the inner sidelines + await tester.tap(find.byIcon(Icons.indeterminate_check_box).last); + // need to wait for current move change debounce delay + await tester.pumpAndSettle(); + + // After collapsing the inner sidelines: + // 1. e4 e5 [-] // <- collapse icon + // |- 1... c5 + // |- [+] // <- expand icon + // |- 1... h5 + // 2. Ke2 + expect(find.text('1… c5'), findsOneWidget); + expect(find.text('1… h5'), findsOneWidget); + expect(find.text('2. Nc3'), findsNothing); + expect(find.text('2. h4'), findsNothing); + + expect(find.byIcon(Icons.add_box), findsOneWidget); + expect(find.byIcon(Icons.indeterminate_check_box), findsOneWidget); + }); + testWidgets('subtrees not part of the current mainline part are cached', (tester) async { await buildTree( From a4463522a45b6dd44fda4ca4a088bc4199e7792e Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Tue, 5 Nov 2024 19:05:55 +0100 Subject: [PATCH 600/979] fix a bug related to expanding variations --- .../model/analysis/analysis_controller.dart | 6 ++- test/view/analysis/analysis_screen_test.dart | 40 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index c0ab330c84..95ed2e9fb5 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -271,7 +271,11 @@ class AnalysisController extends _$AnalysisController @override void expandVariations(UciPath path) { final node = _root.nodeAt(path); - for (final child in node.children) { + + final childrenToHide = + _root.isOnMainline(path) ? node.children.skip(1) : node.children; + + for (final child in childrenToHide) { child.isHidden = false; for (final grandChild in child.children) { grandChild.isHidden = false; diff --git a/test/view/analysis/analysis_screen_test.dart b/test/view/analysis/analysis_screen_test.dart index 1821982022..d2bb0c0a26 100644 --- a/test/view/analysis/analysis_screen_test.dart +++ b/test/view/analysis/analysis_screen_test.dart @@ -349,6 +349,46 @@ void main() { expect(find.byIcon(Icons.add_box), findsOneWidget); expect(find.byIcon(Icons.indeterminate_check_box), findsOneWidget); }); + testWidgets( + 'Expanding one line does not expand the following one (regression test)', + (tester) async { + /// Will be rendered as: + /// ------------------- + /// 1. e4 e5 + /// |- 1... d5 2. Nf3 (2.Nc3) + /// 2. Nf3 + /// |- 2. a4 d5 (2... f5) + /// ------------------- + await buildTree( + tester, + '1. e4 e5 (1... d5 2. Nf3 (2. Nc3)) 2. Nf3 (2. a4 d5 (2... f5))', + ); + + expect(find.byIcon(Icons.indeterminate_check_box), findsNWidgets(2)); + expect(find.byIcon(Icons.add_box), findsNothing); + + // Collapse both lines + await tester.tap(find.byIcon(Icons.indeterminate_check_box).first); + // need to wait for current move change debounce delay + await tester.pumpAndSettle(); + await tester.tap(find.byIcon(Icons.indeterminate_check_box).first); + // need to wait for current move change debounce delay + await tester.pumpAndSettle(); + + // In this state, there used to be a bug where expanding the first line would + // also expand the second line. + expect(find.byIcon(Icons.add_box), findsNWidgets(2)); + await tester.tap(find.byIcon(Icons.add_box).first); + + // need to wait for current move change debounce delay + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.add_box), findsOneWidget); + expect(find.byIcon(Icons.indeterminate_check_box), findsOneWidget); + + // Second sideline should still be collapsed + expect(find.text('2. a4'), findsNothing); + }); testWidgets('subtrees not part of the current mainline part are cached', (tester) async { From 4b2954d1afe33bbe9e3819d3ba1f603e46c90b86 Mon Sep 17 00:00:00 2001 From: James Taylor Date: Wed, 6 Nov 2024 16:08:32 +0000 Subject: [PATCH 601/979] Implemeted choice picker instead of toggle for choosing clock position --- lib/src/model/settings/board_preferences.dart | 10 +-- lib/src/view/game/game_body.dart | 4 +- lib/src/view/game/game_player.dart | 9 +-- lib/src/view/game/game_settings.dart | 35 +++++++--- .../settings/board_clock_position_screen.dart | 70 +++++++++++++++++++ .../view/settings/board_settings_screen.dart | 31 ++++++-- 6 files changed, 133 insertions(+), 26 deletions(-) create mode 100644 lib/src/view/settings/board_clock_position_screen.dart diff --git a/lib/src/model/settings/board_preferences.dart b/lib/src/model/settings/board_preferences.dart index 4806a4eb6f..aedadf4ebc 100644 --- a/lib/src/model/settings/board_preferences.dart +++ b/lib/src/model/settings/board_preferences.dart @@ -81,9 +81,9 @@ class BoardPreferences extends _$BoardPreferences ); } - Future toggleSwitchClockPosition() { + Future setClockPosition(ClockPosition clockPosition) { return save( - state.copyWith(switchClockPosition: !state.switchClockPosition), + state.copyWith(clockPosition: clockPosition), ); } @@ -112,7 +112,7 @@ class BoardPrefs with _$BoardPrefs implements Serializable { required bool coordinates, required bool pieceAnimation, required bool showMaterialDifference, - required bool switchClockPosition, + required ClockPosition clockPosition, @JsonKey( defaultValue: PieceShiftMethod.either, unknownEnumValue: PieceShiftMethod.either, @@ -139,7 +139,7 @@ class BoardPrefs with _$BoardPrefs implements Serializable { coordinates: true, pieceAnimation: true, showMaterialDifference: true, - switchClockPosition: false, + clockPosition: ClockPosition.left, pieceShiftMethod: PieceShiftMethod.either, enableShapeDrawings: true, magnifyDraggedPiece: true, @@ -295,3 +295,5 @@ enum BoardTheme { errorBuilder: (context, o, st) => const SizedBox.shrink(), ); } + +enum ClockPosition { left, right } diff --git a/lib/src/view/game/game_body.dart b/lib/src/view/game/game_body.dart index 68c6bca89c..6bdac40bcf 100644 --- a/lib/src/view/game/game_body.dart +++ b/lib/src/view/game/game_body.dart @@ -136,7 +136,7 @@ class GameBody extends ConsumerWidget { : null, mePlaying: youAre == Side.black, zenMode: gameState.isZenModeActive, - alternateClockPosition: boardPreferences.switchClockPosition, + clockPosition: boardPreferences.clockPosition, confirmMoveCallbacks: youAre == Side.black && gameState.moveToConfirm != null ? ( @@ -177,7 +177,7 @@ class GameBody extends ConsumerWidget { : null, mePlaying: youAre == Side.white, zenMode: gameState.isZenModeActive, - alternateClockPosition: boardPreferences.switchClockPosition, + clockPosition: boardPreferences.clockPosition, confirmMoveCallbacks: youAre == Side.white && gameState.moveToConfirm != null ? ( diff --git a/lib/src/view/game/game_player.dart b/lib/src/view/game/game_player.dart index 796ee0165f..41cafbaac8 100644 --- a/lib/src/view/game/game_player.dart +++ b/lib/src/view/game/game_player.dart @@ -7,6 +7,7 @@ import 'package:flutter/material.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/game/material_diff.dart'; import 'package:lichess_mobile/src/model/game/player.dart'; +import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/styles/lichess_colors.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; @@ -30,7 +31,7 @@ class GamePlayer extends StatelessWidget { this.shouldLinkToUserProfile = true, this.mePlaying = false, this.zenMode = false, - this.alternateClockPosition = false, + this.clockPosition = ClockPosition.right, super.key, }); @@ -44,7 +45,7 @@ class GamePlayer extends StatelessWidget { final bool shouldLinkToUserProfile; final bool mePlaying; final bool zenMode; - final bool alternateClockPosition; + final ClockPosition clockPosition; /// Time left for the player to move at the start of the game. final Duration? timeToMove; @@ -174,7 +175,7 @@ class GamePlayer extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ - if (clock != null && alternateClockPosition) + if (clock != null && clockPosition == ClockPosition.left) Flexible(flex: 3, child: clock!), if (mePlaying && confirmMoveCallbacks != null) Expanded( @@ -211,7 +212,7 @@ class GamePlayer extends StatelessWidget { : playerWidget, ), ), - if (clock != null && !alternateClockPosition) + if (clock != null && clockPosition == ClockPosition.right) Flexible(flex: 3, child: clock!), ], ); diff --git a/lib/src/view/game/game_settings.dart b/lib/src/view/game/game_settings.dart index 37b46d7f4f..94f9fce17e 100644 --- a/lib/src/view/game/game_settings.dart +++ b/lib/src/view/game/game_settings.dart @@ -11,6 +11,9 @@ import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/settings.dart'; +import '../../utils/navigation.dart'; +import '../../widgets/adaptive_choice_picker.dart'; +import '../settings/board_clock_position_screen.dart'; import 'game_screen_providers.dart'; class GameSettings extends ConsumerWidget { @@ -120,16 +123,30 @@ class GameSettings extends ConsumerWidget { .toggleShowMaterialDifference(); }, ), - SwitchSettingTile( + SettingsListTile( //TODO Add i10n - title: const Text( - 'Switch clock position', - ), - value: boardPrefs.switchClockPosition, - onChanged: (value) { - ref - .read(boardPreferencesProvider.notifier) - .toggleSwitchClockPosition(); + settingsLabel: const Text('Clock position'), //TODO: l10n + settingsValue: BoardClockPositionScreen.position( + context, boardPrefs.clockPosition), + onTap: () { + if (Theme.of(context).platform == TargetPlatform.android) { + showChoicePicker( + context, + choices: ClockPosition.values, + selectedItem: boardPrefs.clockPosition, + labelBuilder: (t) => + Text(BoardClockPositionScreen.position(context, t)), + onSelectedItemChanged: (ClockPosition? value) => ref + .read(boardPreferencesProvider.notifier) + .setClockPosition(value ?? ClockPosition.right), + ); + } else { + pushPlatformRoute( + context, + title: 'Clock position', + builder: (context) => const BoardClockPositionScreen(), + ); + } }, ), SwitchSettingTile( diff --git a/lib/src/view/settings/board_clock_position_screen.dart b/lib/src/view/settings/board_clock_position_screen.dart new file mode 100644 index 0000000000..63bfa36a9b --- /dev/null +++ b/lib/src/view/settings/board_clock_position_screen.dart @@ -0,0 +1,70 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; +import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/settings.dart'; + +class BoardClockPositionScreen extends StatelessWidget { + const BoardClockPositionScreen({super.key}); + + @override + Widget build(BuildContext context) { + return PlatformWidget( + androidBuilder: _androidBuilder, + iosBuilder: _iosBuilder, + ); + } + + Widget _androidBuilder(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Clock Position')), //TODO: l10n + body: _Body(), + ); + } + + Widget _iosBuilder(BuildContext context) { + return CupertinoPageScaffold( + navigationBar: const CupertinoNavigationBar(), + child: _Body(), + ); + } + + static String position(BuildContext context, ClockPosition position) { + switch (position) { + case ClockPosition.left: + return 'Left'; + case ClockPosition.right: + return 'Right'; + } + } +} + +class _Body extends ConsumerWidget { + @override + Widget build(BuildContext context, WidgetRef ref) { + final clockPosition = ref.watch( + boardPreferencesProvider.select((state) => state.clockPosition), + ); + + void onChanged(ClockPosition? value) => ref + .read(boardPreferencesProvider.notifier) + .setClockPosition(value ?? ClockPosition.right); + + return SafeArea( + child: ListView( + children: [ + ChoicePicker( + choices: ClockPosition.values, + selectedItem: clockPosition, + titleBuilder: (t) => + Text(BoardClockPositionScreen.position(context, t)), + onSelectedItemChanged: onChanged, + ), + ], + ), + ); + } +} diff --git a/lib/src/view/settings/board_settings_screen.dart b/lib/src/view/settings/board_settings_screen.dart index 07d6616224..82b351e6f1 100644 --- a/lib/src/view/settings/board_settings_screen.dart +++ b/lib/src/view/settings/board_settings_screen.dart @@ -7,6 +7,7 @@ import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/utils/system.dart'; +import 'package:lichess_mobile/src/view/settings/board_clock_position_screen.dart'; import 'package:lichess_mobile/src/view/settings/piece_shift_method_settings_screen.dart'; import 'package:lichess_mobile/src/widgets/adaptive_choice_picker.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; @@ -195,14 +196,30 @@ class _Body extends ConsumerWidget { .toggleShowMaterialDifference(); }, ), - SwitchSettingTile( + SettingsListTile( //TODO Add i10n - title: const Text('Switch clock position'), - value: boardPrefs.switchClockPosition, - onChanged: (value) { - ref - .read(boardPreferencesProvider.notifier) - .toggleSwitchClockPosition(); + settingsLabel: const Text('Clock position'), + settingsValue: BoardClockPositionScreen.position( + context, boardPrefs.clockPosition), + onTap: () { + if (Theme.of(context).platform == TargetPlatform.android) { + showChoicePicker( + context, + choices: ClockPosition.values, + selectedItem: boardPrefs.clockPosition, + labelBuilder: (t) => + Text(BoardClockPositionScreen.position(context, t)), + onSelectedItemChanged: (ClockPosition? value) => ref + .read(boardPreferencesProvider.notifier) + .setClockPosition(value ?? ClockPosition.right), + ); + } else { + pushPlatformRoute( + context, + title: 'Clock position', + builder: (context) => const BoardClockPositionScreen(), + ); + } }, ), ], From fd37f3b568a916ed64738a1e37e27aee82d30223 Mon Sep 17 00:00:00 2001 From: James Taylor Date: Wed, 6 Nov 2024 16:22:35 +0000 Subject: [PATCH 602/979] Fixed formatting --- lib/src/view/game/game_settings.dart | 5 ++--- lib/src/view/settings/board_clock_position_screen.dart | 2 -- lib/src/view/settings/board_settings_screen.dart | 2 +- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/lib/src/view/game/game_settings.dart b/lib/src/view/game/game_settings.dart index 94f9fce17e..5200fe0a18 100644 --- a/lib/src/view/game/game_settings.dart +++ b/lib/src/view/game/game_settings.dart @@ -1,4 +1,3 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/account/account_preferences.dart'; @@ -125,9 +124,9 @@ class GameSettings extends ConsumerWidget { ), SettingsListTile( //TODO Add i10n - settingsLabel: const Text('Clock position'), //TODO: l10n + settingsLabel: const Text('Clock position'), settingsValue: BoardClockPositionScreen.position( - context, boardPrefs.clockPosition), + context, boardPrefs.clockPosition,), onTap: () { if (Theme.of(context).platform == TargetPlatform.android) { showChoicePicker( diff --git a/lib/src/view/settings/board_clock_position_screen.dart b/lib/src/view/settings/board_clock_position_screen.dart index 63bfa36a9b..d43d1fcba9 100644 --- a/lib/src/view/settings/board_clock_position_screen.dart +++ b/lib/src/view/settings/board_clock_position_screen.dart @@ -2,8 +2,6 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; -import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; -import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/settings.dart'; diff --git a/lib/src/view/settings/board_settings_screen.dart b/lib/src/view/settings/board_settings_screen.dart index 82b351e6f1..e67b298f0c 100644 --- a/lib/src/view/settings/board_settings_screen.dart +++ b/lib/src/view/settings/board_settings_screen.dart @@ -200,7 +200,7 @@ class _Body extends ConsumerWidget { //TODO Add i10n settingsLabel: const Text('Clock position'), settingsValue: BoardClockPositionScreen.position( - context, boardPrefs.clockPosition), + context, boardPrefs.clockPosition,), onTap: () { if (Theme.of(context).platform == TargetPlatform.android) { showChoicePicker( From 292edaf28b9c1b4f3771183858b6e3dd6890fe5a Mon Sep 17 00:00:00 2001 From: James Taylor Date: Wed, 6 Nov 2024 16:29:23 +0000 Subject: [PATCH 603/979] Fixed formatting --- lib/src/view/game/game_settings.dart | 4 +++- lib/src/view/settings/board_settings_screen.dart | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/src/view/game/game_settings.dart b/lib/src/view/game/game_settings.dart index 5200fe0a18..72f4f9bf6b 100644 --- a/lib/src/view/game/game_settings.dart +++ b/lib/src/view/game/game_settings.dart @@ -126,7 +126,9 @@ class GameSettings extends ConsumerWidget { //TODO Add i10n settingsLabel: const Text('Clock position'), settingsValue: BoardClockPositionScreen.position( - context, boardPrefs.clockPosition,), + context, + boardPrefs.clockPosition, + ), onTap: () { if (Theme.of(context).platform == TargetPlatform.android) { showChoicePicker( diff --git a/lib/src/view/settings/board_settings_screen.dart b/lib/src/view/settings/board_settings_screen.dart index e67b298f0c..5deaf6ccc7 100644 --- a/lib/src/view/settings/board_settings_screen.dart +++ b/lib/src/view/settings/board_settings_screen.dart @@ -200,7 +200,9 @@ class _Body extends ConsumerWidget { //TODO Add i10n settingsLabel: const Text('Clock position'), settingsValue: BoardClockPositionScreen.position( - context, boardPrefs.clockPosition,), + context, + boardPrefs.clockPosition, + ), onTap: () { if (Theme.of(context).platform == TargetPlatform.android) { showChoicePicker( From 312ee0a2b04183883d359812d5ae0326001734eb Mon Sep 17 00:00:00 2001 From: James Taylor Date: Wed, 6 Nov 2024 16:40:06 +0000 Subject: [PATCH 604/979] Removed file --- ios/Podfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index cf6c77434d..562392d4a9 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -273,4 +273,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 76a583f8d75b3a8c6e4bdc97ae8783ef36cc7984 -COCOAPODS: 1.16.2 +COCOAPODS: 1.15.2 From 6f6449492f1b61a9fbc0ffc25749e4b6c53b1b7a Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Thu, 7 Nov 2024 23:26:46 +0100 Subject: [PATCH 605/979] create a new broadcast game controller that implements everything that is in analysis controller --- .../broadcast_game_controller_v2.dart | 1004 +++++++++++++++++ 1 file changed, 1004 insertions(+) create mode 100644 lib/src/model/broadcast/broadcast_game_controller_v2.dart diff --git a/lib/src/model/broadcast/broadcast_game_controller_v2.dart b/lib/src/model/broadcast/broadcast_game_controller_v2.dart new file mode 100644 index 0000000000..7e733dd953 --- /dev/null +++ b/lib/src/model/broadcast/broadcast_game_controller_v2.dart @@ -0,0 +1,1004 @@ +import 'dart:async'; + +import 'package:collection/collection.dart'; +import 'package:dartchess/dartchess.dart'; +import 'package:deep_pick/deep_pick.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:intl/intl.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; +import 'package:lichess_mobile/src/model/analysis/opening_service.dart'; +import 'package:lichess_mobile/src/model/analysis/server_analysis_service.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast_repository.dart'; +import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/common/eval.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/common/node.dart'; +import 'package:lichess_mobile/src/model/common/service/move_feedback.dart'; +import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; +import 'package:lichess_mobile/src/model/common/socket.dart'; +import 'package:lichess_mobile/src/model/common/uci.dart'; +import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; +import 'package:lichess_mobile/src/model/engine/work.dart'; +import 'package:lichess_mobile/src/model/game/player.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/network/socket.dart'; +import 'package:lichess_mobile/src/utils/json.dart'; +import 'package:lichess_mobile/src/utils/rate_limit.dart'; +import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; +import 'package:lichess_mobile/src/widgets/pgn.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'broadcast_game_controller_v2.freezed.dart'; +part 'broadcast_game_controller_v2.g.dart'; + +final _dateFormat = DateFormat('yyyy.MM.dd'); + +/// Whether the analysis is a standalone analysis (not a lichess game analysis). +bool _isStandaloneAnalysis(StringId id) => + id == standaloneAnalysisId || + id == standaloneOpeningExplorerId || + id == standaloneBroadcastId; + +@riverpod +class BroadcastGameController extends _$BroadcastGameController + implements PgnTreeNotifier { + static Uri broadcastSocketUri(BroadcastRoundId broadcastRoundId) => + Uri(path: 'study/$broadcastRoundId/socket/v6'); + + StreamSubscription? _subscription; + + late SocketClient _socketClient; + late Root _root; + + final _engineEvalDebounce = Debouncer(const Duration(milliseconds: 150)); + + Timer? _startEngineEvalTimer; + + @override + Future build( + AnalysisOptions options, + BroadcastRoundId roundId, + BroadcastGameId gameId, + ) async { + _socketClient = ref + .watch(socketPoolProvider) + .open(BroadcastGameController.broadcastSocketUri(roundId)); + + _subscription = _socketClient.stream.listen(_handleSocketEvent); + + ref.onDispose(() { + _subscription?.cancel(); + }); + + final evaluationService = ref.watch(evaluationServiceProvider); + final serverAnalysisService = ref.watch(serverAnalysisServiceProvider); + + final isEngineAllowed = options.isLocalEvaluationAllowed && + engineSupportedVariants.contains(options.variant); + + ref.onDispose(() { + _startEngineEvalTimer?.cancel(); + _engineEvalDebounce.dispose(); + if (isEngineAllowed) { + evaluationService.disposeEngine(); + } + serverAnalysisService.lastAnalysisEvent + .removeListener(_listenToServerAnalysisEvents); + }); + + serverAnalysisService.lastAnalysisEvent + .addListener(_listenToServerAnalysisEvents); + + UciPath path = UciPath.empty; + Move? lastMove; + + final pgn = await ref.withClient( + (client) => BroadcastRepository(client).getGame(roundId, gameId), + ); + + final game = PgnGame.parsePgn( + pgn, + initHeaders: () => options.isLichessGameAnalysis + ? {} + : { + 'Event': '?', + 'Site': '?', + 'Date': _dateFormat.format(DateTime.now()), + 'Round': '?', + 'White': '?', + 'Black': '?', + 'Result': '*', + 'WhiteElo': '?', + 'BlackElo': '?', + }, + ); + + final pgnHeaders = IMap(game.headers); + final rootComments = IList(game.comments.map((c) => PgnComment.fromPgn(c))); + + Future? openingFuture; + + _root = Root.fromPgnGame( + game, + isLichessAnalysis: options.isLichessGameAnalysis, + hideVariations: options.isLichessGameAnalysis, + onVisitNode: (root, branch, isMainline) { + if (isMainline && + options.initialMoveCursor != null && + branch.position.ply <= + root.position.ply + options.initialMoveCursor!) { + path = path + branch.id; + lastMove = branch.sanMove.move; + } + if (isMainline && options.opening == null && branch.position.ply <= 5) { + openingFuture = _fetchOpening(root, path); + } + }, + ); + + final currentPath = + options.initialMoveCursor == null ? _root.mainlinePath : path; + final currentNode = _root.nodeAt(currentPath); + + // wait for the opening to be fetched to recompute the branch opening + openingFuture?.then((_) { + _setPath(currentPath); + }); + + // don't use ref.watch here: we don't want to invalidate state when the + // analysis preferences change + final prefs = ref.read(analysisPreferencesProvider); + + final analysisState = BroadcastGameState( + variant: options.variant, + id: options.id, + currentPath: currentPath, + broadcastLivePath: options.isBroadcast && pgnHeaders['Result'] == '*' + ? currentPath + : null, + isOnMainline: _root.isOnMainline(currentPath), + root: _root.view, + currentNode: AnalysisCurrentNode.fromNode(currentNode), + pgnHeaders: pgnHeaders, + pgnRootComments: rootComments, + lastMove: lastMove, + pov: options.orientation, + contextOpening: options.opening, + isLocalEvaluationAllowed: options.isLocalEvaluationAllowed, + isLocalEvaluationEnabled: prefs.enableLocalEvaluation, + displayMode: DisplayMode.moves, + playersAnalysis: options.serverAnalysis, + acplChartData: + options.serverAnalysis != null ? _makeAcplChartData() : null, + clocks: options.isBroadcast ? _makeClocks(currentPath) : null, + ); + + if (analysisState.isEngineAvailable) { + evaluationService + .initEngine( + _evaluationContext, + options: EvaluationOptions( + multiPv: prefs.numEvalLines, + cores: prefs.numEngineCores, + ), + ) + .then((_) { + _startEngineEvalTimer = Timer(const Duration(milliseconds: 250), () { + _startEngineEval(); + }); + }); + } + + return analysisState; + } + + void _handleSocketEvent(SocketEvent event) { + if (!state.hasValue) return; + + switch (event.topic) { + // Sent when a node is recevied from the broadcast + case 'addNode': + _handleAddNodeEvent(event); + // Sent when a pgn tag changes + case 'setTags': + _handleSetTagsEvent(event); + } + } + + void _handleAddNodeEvent(SocketEvent event) { + final broadcastGameId = + pick(event.data, 'p', 'chapterId').asBroadcastGameIdOrThrow(); + + // We check if the event is for this game + if (broadcastGameId != gameId) return; + + // The path of the last and current move of the broadcasted game + // Its value is "!" if the path is identical to one of the node that was received + final currentPath = pick(event.data, 'relayPath').asUciPathOrThrow(); + + // We check that the event we received is for the last move of the game + if (currentPath.value != '!') return; + + // The path for the node that was received + final path = pick(event.data, 'p', 'path').asUciPathOrThrow(); + final uciMove = pick(event.data, 'n', 'uci').asUciMoveOrThrow(); + final clock = + pick(event.data, 'n', 'clock').asDurationFromCentiSecondsOrNull(); + + final (newPath, isNewNode) = _root.addMoveAt(path, uciMove, clock: clock); + + if (newPath != null) { + if (state.requireValue.broadcastLivePath == + state.requireValue.currentPath) { + _setPath( + newPath, + shouldRecomputeRootView: isNewNode, + shouldForceShowVariation: true, + isBroadcastMove: true, + ); + } else { + _root.promoteAt(newPath, toMainline: true); + state = AsyncData( + state.requireValue + .copyWith(broadcastLivePath: newPath, root: _root.view), + ); + } + } + } + + void _handleSetTagsEvent(SocketEvent event) { + final broadcastGameId = + pick(event.data, 'chapterId').asBroadcastGameIdOrThrow(); + + // We check if the event is for this game + if (broadcastGameId != gameId) return; + + final headers = Map.fromEntries( + pick(event.data, 'tags').asListOrThrow( + (header) => MapEntry( + header(0).asStringOrThrow(), + header(1).asStringOrThrow(), + ), + ), + ); + + for (final entry in headers.entries) { + final headers = state.requireValue.pgnHeaders.add(entry.key, entry.value); + state = AsyncData(state.requireValue.copyWith(pgnHeaders: headers)); + } + } + + EvaluationContext get _evaluationContext => EvaluationContext( + variant: options.variant, + initialPosition: _root.position, + ); + + void onUserMove(NormalMove move) { + if (!state.hasValue) return; + + if (!state.requireValue.position.isLegal(move)) return; + + if (isPromotionPawnMove(state.requireValue.position, move)) { + state = AsyncData(state.requireValue.copyWith(promotionMove: move)); + return; + } + + // For the opening explorer, last played move should always be the mainline + final shouldReplace = options.id == standaloneOpeningExplorerId; + + final (newPath, isNewNode) = _root.addMoveAt( + state.requireValue.currentPath, + move, + replace: shouldReplace, + ); + if (newPath != null) { + _setPath( + newPath, + shouldRecomputeRootView: isNewNode, + shouldForceShowVariation: true, + ); + } + } + + void onPromotionSelection(Role? role) { + if (!state.hasValue) return; + + if (role == null) { + state = AsyncData(state.requireValue.copyWith(promotionMove: null)); + return; + } + final promotionMove = state.requireValue.promotionMove; + if (promotionMove != null) { + final promotion = promotionMove.withPromotion(role); + onUserMove(promotion); + } + } + + void userNext() { + if (!state.hasValue) return; + + if (!state.requireValue.currentNode.hasChild) return; + _setPath( + state.requireValue.currentPath + + _root.nodeAt(state.requireValue.currentPath).children.first.id, + replaying: true, + ); + } + + void jumpToNthNodeOnMainline(int n) { + UciPath path = _root.mainlinePath; + while (!path.penultimate.isEmpty) { + path = path.penultimate; + } + Node? node = _root.nodeAt(path); + int count = 0; + + while (node != null && count < n) { + if (node.children.isNotEmpty) { + path = path + node.children.first.id; + node = _root.nodeAt(path); + count++; + } else { + break; + } + } + + if (node != null) { + userJump(path); + } + } + + void toggleBoard() { + if (!state.hasValue) return; + + state = AsyncData( + state.requireValue.copyWith(pov: state.requireValue.pov.opposite), + ); + } + + void userPrevious() { + _setPath(state.requireValue.currentPath.penultimate, replaying: true); + } + + @override + void userJump(UciPath path) { + _setPath(path); + } + + @override + void expandVariations(UciPath path) { + if (!state.hasValue) return; + + final node = _root.nodeAt(path); + for (final child in node.children) { + child.isHidden = false; + for (final grandChild in child.children) { + grandChild.isHidden = false; + } + } + state = AsyncData(state.requireValue.copyWith(root: _root.view)); + } + + @override + void collapseVariations(UciPath path) { + if (!state.hasValue) return; + + final node = _root.nodeAt(path); + + for (final child in node.children) { + child.isHidden = true; + } + + state = AsyncData(state.requireValue.copyWith(root: _root.view)); + } + + @override + void promoteVariation(UciPath path, bool toMainline) { + if (!state.hasValue) return; + + _root.promoteAt(path, toMainline: toMainline); + state = AsyncData( + state.requireValue.copyWith( + isOnMainline: _root.isOnMainline(state.requireValue.currentPath), + root: _root.view, + ), + ); + } + + @override + void deleteFromHere(UciPath path) { + _root.deleteAt(path); + _setPath(path.penultimate, shouldRecomputeRootView: true); + } + + Future toggleLocalEvaluation() async { + if (!state.hasValue) return; + + ref + .read(analysisPreferencesProvider.notifier) + .toggleEnableLocalEvaluation(); + + state = AsyncData( + state.requireValue.copyWith( + isLocalEvaluationEnabled: !state.requireValue.isLocalEvaluationEnabled, + ), + ); + + if (state.requireValue.isEngineAvailable) { + final prefs = ref.read(analysisPreferencesProvider); + await ref.read(evaluationServiceProvider).initEngine( + _evaluationContext, + options: EvaluationOptions( + multiPv: prefs.numEvalLines, + cores: prefs.numEngineCores, + ), + ); + _startEngineEval(); + } else { + _stopEngineEval(); + ref.read(evaluationServiceProvider).disposeEngine(); + } + } + + void setNumEvalLines(int numEvalLines) { + if (!state.hasValue) return; + + ref + .read(analysisPreferencesProvider.notifier) + .setNumEvalLines(numEvalLines); + + ref.read(evaluationServiceProvider).setOptions( + EvaluationOptions( + multiPv: numEvalLines, + cores: ref.read(analysisPreferencesProvider).numEngineCores, + ), + ); + + _root.updateAll((node) => node.eval = null); + + state = AsyncData( + state.requireValue.copyWith( + currentNode: AnalysisCurrentNode.fromNode( + _root.nodeAt(state.requireValue.currentPath), + ), + ), + ); + + _startEngineEval(); + } + + void setEngineCores(int numEngineCores) { + ref + .read(analysisPreferencesProvider.notifier) + .setEngineCores(numEngineCores); + + ref.read(evaluationServiceProvider).setOptions( + EvaluationOptions( + multiPv: ref.read(analysisPreferencesProvider).numEvalLines, + cores: numEngineCores, + ), + ); + + _startEngineEval(); + } + + void setDisplayMode(DisplayMode mode) { + if (!state.hasValue) return; + + state = AsyncData(state.requireValue.copyWith(displayMode: mode)); + } + + Future requestServerAnalysis() async { + if (!state.hasValue) return; + + if (state.requireValue.canRequestServerAnalysis) { + final service = ref.read(serverAnalysisServiceProvider); + return service.requestAnalysis( + options.id as GameAnyId, + options.orientation, + ); + } + return Future.error('Cannot request server analysis'); + } + + /// Gets the node and maybe the associated branch opening at the given path. + (Node, Opening?) _nodeOpeningAt(Node node, UciPath path, [Opening? opening]) { + if (path.isEmpty) return (node, opening); + final child = node.childById(path.head!); + if (child != null) { + return _nodeOpeningAt(child, path.tail, child.opening ?? opening); + } else { + return (node, opening); + } + } + + /// Makes a full PGN string (including headers and comments) of the current game state. + String makeExportPgn() { + if (!state.hasValue) Exception('Cannot make a PGN'); + + return _root.makePgn( + state.requireValue.pgnHeaders, + state.requireValue.pgnRootComments, + ); + } + + /// Makes a PGN string up to the current node only. + String makeCurrentNodePgn() { + if (!state.hasValue) Exception('Cannot make a PGN up to the current node'); + + final nodes = _root.branchesOn(state.requireValue.currentPath); + return nodes.map((node) => node.sanMove.san).join(' '); + } + + void _setPath( + UciPath path, { + bool shouldForceShowVariation = false, + bool shouldRecomputeRootView = false, + bool replaying = false, + bool isBroadcastMove = false, + }) { + if (!state.hasValue) return; + + final pathChange = state.requireValue.currentPath != path; + final (currentNode, opening) = _nodeOpeningAt(_root, path); + + // always show variation if the user plays a move + if (shouldForceShowVariation && + currentNode is Branch && + currentNode.isHidden) { + _root.updateAt(path, (node) { + if (node is Branch) node.isHidden = false; + }); + } + + // root view is only used to display move list, so we need to + // recompute the root view only when the nodelist length changes + // or a variation is hidden/shown + final rootView = shouldForceShowVariation || shouldRecomputeRootView + ? _root.view + : state.requireValue.root; + + final isForward = path.size > state.requireValue.currentPath.size; + if (currentNode is Branch) { + if (!replaying) { + if (isForward) { + final isCheck = currentNode.sanMove.isCheck; + if (currentNode.sanMove.isCapture) { + ref + .read(moveFeedbackServiceProvider) + .captureFeedback(check: isCheck); + } else { + ref.read(moveFeedbackServiceProvider).moveFeedback(check: isCheck); + } + } + } else if (isForward) { + final soundService = ref.read(soundServiceProvider); + if (currentNode.sanMove.isCapture) { + soundService.play(Sound.capture); + } else { + soundService.play(Sound.move); + } + } + + if (currentNode.opening == null && currentNode.position.ply <= 30) { + _fetchOpening(_root, path); + } + + state = AsyncData( + state.requireValue.copyWith( + currentPath: path, + broadcastLivePath: + isBroadcastMove ? path : state.requireValue.broadcastLivePath, + isOnMainline: _root.isOnMainline(path), + currentNode: AnalysisCurrentNode.fromNode(currentNode), + currentBranchOpening: opening, + lastMove: currentNode.sanMove.move, + promotionMove: null, + root: rootView, + clocks: options.isBroadcast ? _makeClocks(path) : null, + ), + ); + } else { + state = AsyncData( + state.requireValue.copyWith( + currentPath: path, + broadcastLivePath: + isBroadcastMove ? path : state.requireValue.broadcastLivePath, + isOnMainline: _root.isOnMainline(path), + currentNode: AnalysisCurrentNode.fromNode(currentNode), + currentBranchOpening: opening, + lastMove: null, + promotionMove: null, + root: rootView, + clocks: options.isBroadcast ? _makeClocks(path) : null, + ), + ); + } + + if (pathChange && state.requireValue.isEngineAvailable) { + _debouncedStartEngineEval(); + } + } + + Future _fetchOpening(Node fromNode, UciPath path) async { + if (!state.hasValue) return; + if (!kOpeningAllowedVariants.contains(options.variant)) return; + + final moves = fromNode.branchesOn(path).map((node) => node.sanMove.move); + if (moves.isEmpty) return; + if (moves.length > 40) return; + + final opening = + await ref.read(openingServiceProvider).fetchFromMoves(moves); + + if (opening != null) { + fromNode.updateAt(path, (node) => node.opening = opening); + + if (state.requireValue.currentPath == path) { + state = AsyncData( + state.requireValue.copyWith( + currentNode: AnalysisCurrentNode.fromNode(fromNode.nodeAt(path)), + ), + ); + } + } + } + + void _startEngineEval() { + if (!state.hasValue) return; + + if (!state.requireValue.isEngineAvailable) return; + ref + .read(evaluationServiceProvider) + .start( + state.requireValue.currentPath, + _root.branchesOn(state.requireValue.currentPath).map(Step.fromNode), + initialPositionEval: _root.eval, + shouldEmit: (work) => work.path == state.requireValue.currentPath, + ) + ?.forEach( + (t) => _root.updateAt(t.$1.path, (node) => node.eval = t.$2), + ); + } + + void _debouncedStartEngineEval() { + _engineEvalDebounce(() { + _startEngineEval(); + }); + } + + void _stopEngineEval() { + if (!state.hasValue) Exception('Cannot export PGN'); + + ref.read(evaluationServiceProvider).stop(); + // update the current node with last cached eval + state = AsyncData( + state.requireValue.copyWith( + currentNode: AnalysisCurrentNode.fromNode( + _root.nodeAt(state.requireValue.currentPath)), + ), + ); + } + + void _listenToServerAnalysisEvents() { + if (!state.hasValue) Exception('Cannot export PGN'); + + final event = + ref.read(serverAnalysisServiceProvider).lastAnalysisEvent.value; + if (event != null && event.$1 == state.requireValue.id) { + _mergeOngoingAnalysis(_root, event.$2.tree); + state = AsyncData( + state.requireValue.copyWith( + acplChartData: _makeAcplChartData(), + playersAnalysis: event.$2.analysis != null + ? ( + white: event.$2.analysis!.white, + black: event.$2.analysis!.black + ) + : null, + root: _root.view, + ), + ); + } + } + + void _mergeOngoingAnalysis(Node n1, Map n2) { + final eval = n2['eval'] as Map?; + final cp = eval?['cp'] as int?; + final mate = eval?['mate'] as int?; + final pgnEval = cp != null + ? PgnEvaluation.pawns(pawns: cpToPawns(cp)) + : mate != null + ? PgnEvaluation.mate(mate: mate) + : null; + final glyphs = n2['glyphs'] as List?; + final glyph = glyphs?.first as Map?; + final comments = n2['comments'] as List?; + final comment = + (comments?.first as Map?)?['text'] as String?; + final children = n2['children'] as List? ?? []; + final pgnComment = + pgnEval != null ? PgnComment(eval: pgnEval, text: comment) : null; + if (n1 is Branch) { + if (pgnComment != null) { + if (n1.lichessAnalysisComments == null) { + n1.lichessAnalysisComments = [pgnComment]; + } else { + n1.lichessAnalysisComments!.removeWhere((c) => c.eval != null); + n1.lichessAnalysisComments!.add(pgnComment); + } + } + if (glyph != null) { + n1.nags ??= [glyph['id'] as int]; + } + } + for (final c in children) { + final n2child = c as Map; + final id = n2child['id'] as String; + final n1child = n1.childById(UciCharPair.fromStringId(id)); + if (n1child != null) { + _mergeOngoingAnalysis(n1child, n2child); + } else { + final uci = n2child['uci'] as String; + final san = n2child['san'] as String; + final move = Move.parse(uci)!; + n1.addChild( + Branch( + position: n1.position.playUnchecked(move), + sanMove: SanMove(san, move), + isHidden: children.length > 1, + ), + ); + } + } + } + + IList? _makeAcplChartData() { + if (!_root.mainline.any((node) => node.lichessAnalysisComments != null)) { + return null; + } + final list = _root.mainline + .map( + (node) => ( + node.position.isCheckmate, + node.position.turn, + node.lichessAnalysisComments + ?.firstWhereOrNull((c) => c.eval != null) + ?.eval + ), + ) + .map( + (el) { + final (isCheckmate, side, eval) = el; + return eval != null + ? ExternalEval( + cp: eval.pawns != null ? cpFromPawns(eval.pawns!) : null, + mate: eval.mate, + depth: eval.depth, + ) + : ExternalEval( + cp: null, + // hack to display checkmate as the max eval + mate: isCheckmate + ? side == Side.white + ? -1 + : 1 + : null, + ); + }, + ).toList(growable: false); + return list.isEmpty ? null : IList(list); + } + + ({Duration? parentClock, Duration? clock}) _makeClocks(UciPath path) { + final nodeView = _root.nodeAt(path).view; + final parentView = _root.parentAt(path).view; + + return ( + parentClock: (parentView is ViewBranch) ? parentView.clock : null, + clock: (nodeView is ViewBranch) ? nodeView.clock : null, + ); + } +} + +enum DisplayMode { + moves, + summary, +} + +@freezed +class BroadcastGameState with _$BroadcastGameState { + const BroadcastGameState._(); + + const factory BroadcastGameState({ + /// Analysis ID + required StringId id, + + /// The variant of the analysis. + required Variant variant, + + /// Immutable view of the whole tree + required ViewRoot root, + + /// The current node in the analysis view. + /// + /// This is an immutable copy of the actual [Node] at the `currentPath`. + /// We don't want to use [Node.view] here because it'd copy the whole tree + /// under the current node and it's expensive. + required AnalysisCurrentNode currentNode, + + /// The path to the current node in the analysis view. + required UciPath currentPath, + + // The path to the current broadcast live move. + required UciPath? broadcastLivePath, + + /// Whether the current path is on the mainline. + required bool isOnMainline, + + /// The side to display the board from. + required Side pov, + + /// Whether local evaluation is allowed for this analysis. + required bool isLocalEvaluationAllowed, + + /// Whether the user has enabled local evaluation. + required bool isLocalEvaluationEnabled, + + /// The display mode of the analysis. + /// + /// It can be either moves, summary or opening explorer. + required DisplayMode displayMode, + + /// Clocks if avaible. Only used by the broadcast analysis screen. + ({Duration? parentClock, Duration? clock})? clocks, + + /// The last move played. + Move? lastMove, + + /// Possible promotion move to be played. + NormalMove? promotionMove, + + /// Opening of the analysis context (from lichess archived games). + Opening? contextOpening, + + /// The opening of the current branch. + Opening? currentBranchOpening, + + /// Optional server analysis to display player stats. + ({PlayerAnalysis white, PlayerAnalysis black})? playersAnalysis, + + /// Optional ACPL chart data of the game, coming from lichess server analysis. + IList? acplChartData, + + /// The PGN headers of the game. + required IMap pgnHeaders, + + /// The PGN comments of the game. + /// + /// This field is only used with user submitted PGNS. + IList? pgnRootComments, + }) = _AnalysisState; + + /// The game ID of the analysis, if it's a lichess game. + GameAnyId? get gameAnyId => + _isStandaloneAnalysis(id) ? null : GameAnyId(id.value); + + /// Whether the analysis is for a lichess game. + bool get isLichessGameAnalysis => gameAnyId != null; + + IMap> get validMoves => makeLegalMoves( + currentNode.position, + isChess960: variant == Variant.chess960, + ); + + /// Whether the user can request server analysis. + /// + /// It must be a lichess game, which is finished and not already analyzed. + bool get canRequestServerAnalysis => false; + + bool get canShowGameSummary => hasServerAnalysis || canRequestServerAnalysis; + + bool get hasServerAnalysis => playersAnalysis != null; + + /// Whether an evaluation can be available + bool get hasAvailableEval => + isEngineAvailable || + (isLocalEvaluationAllowed && + acplChartData != null && + acplChartData!.isNotEmpty); + + /// Whether the engine is allowed for this analysis and variant. + bool get isEngineAllowed => + isLocalEvaluationAllowed && engineSupportedVariants.contains(variant); + + /// Whether the engine is available for evaluation + bool get isEngineAvailable => isEngineAllowed && isLocalEvaluationEnabled; + + Position get position => currentNode.position; + bool get canGoNext => currentNode.hasChild; + bool get canGoBack => currentPath.size > UciPath.empty.size; + + EngineGaugeParams get engineGaugeParams => ( + orientation: pov, + isLocalEngineAvailable: isEngineAvailable, + position: position, + savedEval: currentNode.eval ?? currentNode.serverEval, + ); + + AnalysisOptions get openingExplorerOptions => AnalysisOptions( + id: standaloneOpeningExplorerId, + isLocalEvaluationAllowed: false, + orientation: pov, + variant: variant, + initialMoveCursor: currentPath.size, + ); + + static AnalysisOptions get broadcastOptions => const AnalysisOptions( + id: standaloneBroadcastId, + isLocalEvaluationAllowed: true, + orientation: Side.white, + variant: Variant.standard, + isBroadcast: true, + ); +} + +@freezed +class AnalysisCurrentNode with _$AnalysisCurrentNode { + const AnalysisCurrentNode._(); + + const factory AnalysisCurrentNode({ + required Position position, + required bool hasChild, + required bool isRoot, + SanMove? sanMove, + Opening? opening, + ClientEval? eval, + IList? lichessAnalysisComments, + IList? startingComments, + IList? comments, + IList? nags, + }) = _AnalysisCurrentNode; + + factory AnalysisCurrentNode.fromNode(Node node) { + if (node is Branch) { + return AnalysisCurrentNode( + sanMove: node.sanMove, + position: node.position, + isRoot: node is Root, + hasChild: node.children.isNotEmpty, + opening: node.opening, + eval: node.eval, + lichessAnalysisComments: IList(node.lichessAnalysisComments), + startingComments: IList(node.startingComments), + comments: IList(node.comments), + nags: IList(node.nags), + ); + } else { + return AnalysisCurrentNode( + position: node.position, + hasChild: node.children.isNotEmpty, + isRoot: node is Root, + opening: node.opening, + eval: node.eval, + ); + } + } + + /// The evaluation from the PGN comments. + /// + /// For now we only trust the eval coming from lichess analysis. + ExternalEval? get serverEval { + final pgnEval = + lichessAnalysisComments?.firstWhereOrNull((c) => c.eval != null)?.eval; + return pgnEval != null + ? ExternalEval( + cp: pgnEval.pawns != null ? cpFromPawns(pgnEval.pawns!) : null, + mate: pgnEval.mate, + depth: pgnEval.depth, + ) + : null; + } +} From afd2afcb3f5e79b12fd911f8243a1fd7c3ee8947 Mon Sep 17 00:00:00 2001 From: Jimima Date: Fri, 8 Nov 2024 11:51:37 +0000 Subject: [PATCH 606/979] WIP --- lib/src/model/game/material_diff.dart | 21 ++++++++++++++++++--- lib/src/view/game/game_player.dart | 24 +++++++++++++++++++++++- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/lib/src/model/game/material_diff.dart b/lib/src/model/game/material_diff.dart index 6fe688f4de..7b4373fe93 100644 --- a/lib/src/model/game/material_diff.dart +++ b/lib/src/model/game/material_diff.dart @@ -9,6 +9,8 @@ class MaterialDiffSide with _$MaterialDiffSide { const factory MaterialDiffSide({ required IMap pieces, required int score, + required IMap capturedPieces, + }) = _MaterialDiffSide; } @@ -34,6 +36,19 @@ class MaterialDiff with _$MaterialDiff { int score = 0; final IMap blackCount = board.materialCount(Side.black); final IMap whiteCount = board.materialCount(Side.white); + + final IMap blackStartingCount = Board.standard.materialCount(Side.black); + final IMap whiteStartingCount = Board.standard.materialCount(Side.white); + // TODO: parameterise starting position maybe so it can be passed in + + IMap subtractPieceCounts(IMap startingCount, IMap subtractCount){ + IMap capturedPieces = IMap(); + startingCount.forEach((role,count){capturedPieces = capturedPieces.add(role, count - (subtractCount.get(role) ?? 0) ) ;}); + return capturedPieces; + } + + final IMap blackCapturedPieces = subtractPieceCounts(whiteStartingCount, whiteCount); + final IMap whiteCapturedPieces = subtractPieceCounts(blackStartingCount, blackCount); Map count; Map black; @@ -80,9 +95,9 @@ class MaterialDiff with _$MaterialDiff { }); return MaterialDiff( - black: MaterialDiffSide(pieces: black.toIMap(), score: -score), - white: MaterialDiffSide(pieces: white.toIMap(), score: score), - ); + black: MaterialDiffSide(pieces: black.toIMap(), score: -score, capturedPieces: blackCapturedPieces), + white: MaterialDiffSide(pieces: white.toIMap(), score: score, capturedPieces: whiteCapturedPieces) + ,); } MaterialDiffSide bySide(Side side) => side == Side.black ? black : white; diff --git a/lib/src/view/game/game_player.dart b/lib/src/view/game/game_player.dart index 619f583af3..c5b41cb5eb 100644 --- a/lib/src/view/game/game_player.dart +++ b/lib/src/view/game/game_player.dart @@ -140,7 +140,29 @@ class GamePlayer extends StatelessWidget { ), if (timeToMove != null) MoveExpiration(timeToMove: timeToMove!, mePlaying: mePlaying) - else if (materialDiff != null) + else if (materialDiff != null && true) //TODO put this as a pref + Row( + children: [ + for (final role in Role.values) + for (int i = 0; i < materialDiff!.capturedPieces[role]!; i++) + Icon( + _iconByRole[role], + size: 13, + color: Colors.grey, + ), + const SizedBox(width: 3), + Text( + style: const TextStyle( + fontSize: 13, + color: Colors.grey, + ), + materialDiff != null && materialDiff!.score > 0 + ? '+${materialDiff!.score}' + : '', + ), + ], + ) + else if (materialDiff != null && false) Row( children: [ for (final role in Role.values) From ae16f7987e75e2fe5f58b41f55c8e50939202ee0 Mon Sep 17 00:00:00 2001 From: Jimima Date: Fri, 8 Nov 2024 14:58:55 +0000 Subject: [PATCH 607/979] WIP --- lib/src/model/settings/board_preferences.dart | 20 +++++++++++++++ lib/src/view/game/game_body.dart | 2 ++ lib/src/view/game/game_player.dart | 14 ++++++++++- .../view/settings/board_settings_screen.dart | 25 +++++++++++++++++++ 4 files changed, 60 insertions(+), 1 deletion(-) diff --git a/lib/src/model/settings/board_preferences.dart b/lib/src/model/settings/board_preferences.dart index ca5e4cb208..636af7e3f4 100644 --- a/lib/src/model/settings/board_preferences.dart +++ b/lib/src/model/settings/board_preferences.dart @@ -81,6 +81,12 @@ class BoardPreferences extends _$BoardPreferences ); } + Future setMaterialDifferenceFormat(MaterialDifferenceFormat materialDifferenceFormat) { + return save( + state.copyWith(materialDifferenceFormat: materialDifferenceFormat), + ); + } + Future toggleEnableShapeDrawings() { return save( state.copyWith(enableShapeDrawings: !state.enableShapeDrawings), @@ -106,6 +112,7 @@ class BoardPrefs with _$BoardPrefs implements Serializable { required bool coordinates, required bool pieceAnimation, required bool showMaterialDifference, + required MaterialDifferenceFormat materialDifferenceFormat, @JsonKey( defaultValue: PieceShiftMethod.either, unknownEnumValue: PieceShiftMethod.either, @@ -132,6 +139,7 @@ class BoardPrefs with _$BoardPrefs implements Serializable { coordinates: true, pieceAnimation: true, showMaterialDifference: true, + materialDifferenceFormat : MaterialDifferenceFormat.difference, pieceShiftMethod: PieceShiftMethod.either, enableShapeDrawings: true, magnifyDraggedPiece: true, @@ -287,3 +295,15 @@ enum BoardTheme { errorBuilder: (context, o, st) => const SizedBox.shrink(), ); } + +enum MaterialDifferenceFormat { + difference(description: 'Material difference'), + pieces(description: 'Captured pieces'); + + const MaterialDifferenceFormat({ + required this.description, + }); + + final String description; + +} diff --git a/lib/src/view/game/game_body.dart b/lib/src/view/game/game_body.dart index 790d2f472a..6bcc335cc3 100644 --- a/lib/src/view/game/game_body.dart +++ b/lib/src/view/game/game_body.dart @@ -131,6 +131,7 @@ class GameBody extends ConsumerWidget { materialDiff: boardPreferences.showMaterialDifference ? gameState.game.materialDiffAt(gameState.stepCursor, Side.black) : null, + materialDifferenceFormat: boardPreferences.materialDifferenceFormat, timeToMove: gameState.game.sideToMove == Side.black ? gameState.timeToMove : null, @@ -171,6 +172,7 @@ class GameBody extends ConsumerWidget { materialDiff: boardPreferences.showMaterialDifference ? gameState.game.materialDiffAt(gameState.stepCursor, Side.white) : null, + materialDifferenceFormat: boardPreferences.materialDifferenceFormat, timeToMove: gameState.game.sideToMove == Side.white ? gameState.timeToMove : null, diff --git a/lib/src/view/game/game_player.dart b/lib/src/view/game/game_player.dart index c5b41cb5eb..7777f8f492 100644 --- a/lib/src/view/game/game_player.dart +++ b/lib/src/view/game/game_player.dart @@ -7,6 +7,7 @@ import 'package:flutter/material.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/game/material_diff.dart'; import 'package:lichess_mobile/src/model/game/player.dart'; +import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/styles/lichess_colors.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; @@ -25,6 +26,7 @@ class GamePlayer extends StatelessWidget { required this.player, this.clock, this.materialDiff, + this.materialDifferenceFormat, this.confirmMoveCallbacks, this.timeToMove, this.shouldLinkToUserProfile = true, @@ -36,6 +38,7 @@ class GamePlayer extends StatelessWidget { final Player player; final Widget? clock; final MaterialDiffSide? materialDiff; + final MaterialDifferenceFormat? materialDifferenceFormat; /// if confirm move preference is enabled, used to display confirmation buttons final ({VoidCallback confirm, VoidCallback cancel})? confirmMoveCallbacks; @@ -140,9 +143,10 @@ class GamePlayer extends StatelessWidget { ), if (timeToMove != null) MoveExpiration(timeToMove: timeToMove!, mePlaying: mePlaying) - else if (materialDiff != null && true) //TODO put this as a pref + else if (materialDiff != null) //TODO put this as a pref Row( children: [ + if (materialDifferenceFormat == MaterialDifferenceFormat.pieces) for (final role in Role.values) for (int i = 0; i < materialDiff!.capturedPieces[role]!; i++) Icon( @@ -150,6 +154,14 @@ class GamePlayer extends StatelessWidget { size: 13, color: Colors.grey, ), + if (materialDifferenceFormat == MaterialDifferenceFormat.difference) + for (final role in Role.values) + for (int i = 0; i < materialDiff!.pieces[role]!; i++) + Icon( + _iconByRole[role], + size: 13, + color: Colors.grey, + ), const SizedBox(width: 3), Text( style: const TextStyle( diff --git a/lib/src/view/settings/board_settings_screen.dart b/lib/src/view/settings/board_settings_screen.dart index eefd82f4f2..82d4a1eb19 100644 --- a/lib/src/view/settings/board_settings_screen.dart +++ b/lib/src/view/settings/board_settings_screen.dart @@ -195,6 +195,31 @@ class _Body extends ConsumerWidget { .toggleShowMaterialDifference(); }, ), + SettingsListTile( + settingsLabel: const Text('Material difference format'), + settingsValue: boardPrefs.materialDifferenceFormat.description, + onTap: () { + // if (Theme.of(context).platform == TargetPlatform.android) { + if (true) { + showChoicePicker( + context, + choices: MaterialDifferenceFormat.values, + selectedItem: boardPrefs.materialDifferenceFormat, + labelBuilder: (t) => + Text(t.description), + onSelectedItemChanged: (MaterialDifferenceFormat? value) => ref + .read(boardPreferencesProvider.notifier) + .setMaterialDifferenceFormat(value ?? MaterialDifferenceFormat.difference), + ); + } else { + // pushPlatformRoute( + // context, + // title: 'Clock position', + // builder: (context) => const BoardClockPositionScreen(), + // ); + } + }, + ), ], ), ], From f4678228736edf06ddeae43c14037b78eec532d7 Mon Sep 17 00:00:00 2001 From: Jimima Date: Fri, 8 Nov 2024 15:12:05 +0000 Subject: [PATCH 608/979] Format fix --- lib/src/model/game/material_diff.dart | 35 ++++++++----- lib/src/model/settings/board_preferences.dart | 6 +-- lib/src/view/game/game_player.dart | 19 +++---- .../view/settings/board_settings_screen.dart | 49 ++++++++++--------- 4 files changed, 63 insertions(+), 46 deletions(-) diff --git a/lib/src/model/game/material_diff.dart b/lib/src/model/game/material_diff.dart index 7b4373fe93..122d7c7477 100644 --- a/lib/src/model/game/material_diff.dart +++ b/lib/src/model/game/material_diff.dart @@ -10,7 +10,6 @@ class MaterialDiffSide with _$MaterialDiffSide { required IMap pieces, required int score, required IMap capturedPieces, - }) = _MaterialDiffSide; } @@ -36,19 +35,27 @@ class MaterialDiff with _$MaterialDiff { int score = 0; final IMap blackCount = board.materialCount(Side.black); final IMap whiteCount = board.materialCount(Side.white); - - final IMap blackStartingCount = Board.standard.materialCount(Side.black); - final IMap whiteStartingCount = Board.standard.materialCount(Side.white); + + final IMap blackStartingCount = + Board.standard.materialCount(Side.black); + final IMap whiteStartingCount = + Board.standard.materialCount(Side.white); // TODO: parameterise starting position maybe so it can be passed in - IMap subtractPieceCounts(IMap startingCount, IMap subtractCount){ + IMap subtractPieceCounts( + IMap startingCount, IMap subtractCount) { IMap capturedPieces = IMap(); - startingCount.forEach((role,count){capturedPieces = capturedPieces.add(role, count - (subtractCount.get(role) ?? 0) ) ;}); + startingCount.forEach((role, count) { + capturedPieces = + capturedPieces.add(role, count - (subtractCount.get(role) ?? 0)); + }); return capturedPieces; } - final IMap blackCapturedPieces = subtractPieceCounts(whiteStartingCount, whiteCount); - final IMap whiteCapturedPieces = subtractPieceCounts(blackStartingCount, blackCount); + final IMap blackCapturedPieces = + subtractPieceCounts(whiteStartingCount, whiteCount); + final IMap whiteCapturedPieces = + subtractPieceCounts(blackStartingCount, blackCount); Map count; Map black; @@ -95,9 +102,15 @@ class MaterialDiff with _$MaterialDiff { }); return MaterialDiff( - black: MaterialDiffSide(pieces: black.toIMap(), score: -score, capturedPieces: blackCapturedPieces), - white: MaterialDiffSide(pieces: white.toIMap(), score: score, capturedPieces: whiteCapturedPieces) - ,); + black: MaterialDiffSide( + pieces: black.toIMap(), + score: -score, + capturedPieces: blackCapturedPieces), + white: MaterialDiffSide( + pieces: white.toIMap(), + score: score, + capturedPieces: whiteCapturedPieces), + ); } MaterialDiffSide bySide(Side side) => side == Side.black ? black : white; diff --git a/lib/src/model/settings/board_preferences.dart b/lib/src/model/settings/board_preferences.dart index 636af7e3f4..2a72fa8a7a 100644 --- a/lib/src/model/settings/board_preferences.dart +++ b/lib/src/model/settings/board_preferences.dart @@ -81,7 +81,8 @@ class BoardPreferences extends _$BoardPreferences ); } - Future setMaterialDifferenceFormat(MaterialDifferenceFormat materialDifferenceFormat) { + Future setMaterialDifferenceFormat( + MaterialDifferenceFormat materialDifferenceFormat) { return save( state.copyWith(materialDifferenceFormat: materialDifferenceFormat), ); @@ -139,7 +140,7 @@ class BoardPrefs with _$BoardPrefs implements Serializable { coordinates: true, pieceAnimation: true, showMaterialDifference: true, - materialDifferenceFormat : MaterialDifferenceFormat.difference, + materialDifferenceFormat: MaterialDifferenceFormat.difference, pieceShiftMethod: PieceShiftMethod.either, enableShapeDrawings: true, magnifyDraggedPiece: true, @@ -305,5 +306,4 @@ enum MaterialDifferenceFormat { }); final String description; - } diff --git a/lib/src/view/game/game_player.dart b/lib/src/view/game/game_player.dart index 7777f8f492..35e7c713ab 100644 --- a/lib/src/view/game/game_player.dart +++ b/lib/src/view/game/game_player.dart @@ -143,18 +143,19 @@ class GamePlayer extends StatelessWidget { ), if (timeToMove != null) MoveExpiration(timeToMove: timeToMove!, mePlaying: mePlaying) - else if (materialDiff != null) //TODO put this as a pref + else if (materialDiff != null) Row( children: [ if (materialDifferenceFormat == MaterialDifferenceFormat.pieces) - for (final role in Role.values) - for (int i = 0; i < materialDiff!.capturedPieces[role]!; i++) - Icon( - _iconByRole[role], - size: 13, - color: Colors.grey, - ), - if (materialDifferenceFormat == MaterialDifferenceFormat.difference) + for (final role in Role.values) + for (int i = 0; i < materialDiff!.capturedPieces[role]!; i++) + Icon( + _iconByRole[role], + size: 13, + color: Colors.grey, + ), + if (materialDifferenceFormat == + MaterialDifferenceFormat.difference) for (final role in Role.values) for (int i = 0; i < materialDiff!.pieces[role]!; i++) Icon( diff --git a/lib/src/view/settings/board_settings_screen.dart b/lib/src/view/settings/board_settings_screen.dart index 82d4a1eb19..f550292a4f 100644 --- a/lib/src/view/settings/board_settings_screen.dart +++ b/lib/src/view/settings/board_settings_screen.dart @@ -196,29 +196,32 @@ class _Body extends ConsumerWidget { }, ), SettingsListTile( - settingsLabel: const Text('Material difference format'), - settingsValue: boardPrefs.materialDifferenceFormat.description, - onTap: () { - // if (Theme.of(context).platform == TargetPlatform.android) { - if (true) { - showChoicePicker( - context, - choices: MaterialDifferenceFormat.values, - selectedItem: boardPrefs.materialDifferenceFormat, - labelBuilder: (t) => - Text(t.description), - onSelectedItemChanged: (MaterialDifferenceFormat? value) => ref - .read(boardPreferencesProvider.notifier) - .setMaterialDifferenceFormat(value ?? MaterialDifferenceFormat.difference), - ); - } else { - // pushPlatformRoute( - // context, - // title: 'Clock position', - // builder: (context) => const BoardClockPositionScreen(), - // ); - } - }, + settingsLabel: const Text('Material difference format'), + settingsValue: boardPrefs.materialDifferenceFormat.description, + onTap: () { + //TODO: implement different handling to android/ios + // if (Theme.of(context).platform == TargetPlatform.android) { + + if (true) { + showChoicePicker( + context, + choices: MaterialDifferenceFormat.values, + selectedItem: boardPrefs.materialDifferenceFormat, + labelBuilder: (t) => Text(t.description), + onSelectedItemChanged: + (MaterialDifferenceFormat? value) => ref + .read(boardPreferencesProvider.notifier) + .setMaterialDifferenceFormat( + value ?? MaterialDifferenceFormat.difference), + ); + } else { + // pushPlatformRoute( + // context, + // title: 'Clock position', + // builder: (context) => const BoardClockPositionScreen(), + // ); + } + }, ), ], ), From 0831c2c6d36ea6f564597d6b16204905ca3cd47d Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Sat, 9 Nov 2024 12:07:30 +0100 Subject: [PATCH 609/979] Add border to board editor and coordinate training --- .../view/board_editor/board_editor_screen.dart | 15 ++++++--------- .../coordinate_training_screen.dart | 17 ++++++++--------- pubspec.lock | 2 +- 3 files changed, 15 insertions(+), 19 deletions(-) diff --git a/lib/src/view/board_editor/board_editor_screen.dart b/lib/src/view/board_editor/board_editor_screen.dart index 351ee35d94..128e81cd06 100644 --- a/lib/src/view/board_editor/board_editor_screen.dart +++ b/lib/src/view/board_editor/board_editor_screen.dart @@ -132,15 +132,12 @@ class _BoardEditor extends ConsumerWidget { size: boardSize, pieces: pieces, orientation: orientation, - settings: ChessboardEditorSettings( - pieceAssets: boardPrefs.pieceSet.assets, - colorScheme: boardPrefs.boardTheme.colors, - enableCoordinates: boardPrefs.coordinates, - borderRadius: isTablet - ? const BorderRadius.all(Radius.circular(4.0)) - : BorderRadius.zero, - boxShadow: isTablet ? boardShadows : const [], - ), + settings: boardPrefs.toBoardSettings().copyWith( + borderRadius: isTablet + ? const BorderRadius.all(Radius.circular(4.0)) + : BorderRadius.zero, + boxShadow: isTablet ? boardShadows : const [], + ), pointerMode: editorState.editorPointerMode, onDiscardedPiece: (Square square) => ref .read(boardEditorControllerProvider(initialFen).notifier) diff --git a/lib/src/view/coordinate_training/coordinate_training_screen.dart b/lib/src/view/coordinate_training/coordinate_training_screen.dart index 0136f7ac00..02b9b3d7a7 100644 --- a/lib/src/view/coordinate_training/coordinate_training_screen.dart +++ b/lib/src/view/coordinate_training/coordinate_training_screen.dart @@ -495,15 +495,14 @@ class _TrainingBoardState extends ConsumerState<_TrainingBoard> { ), squareHighlights: widget.squareHighlights, orientation: widget.orientation, - settings: ChessboardEditorSettings( - pieceAssets: boardPrefs.pieceSet.assets, - colorScheme: boardPrefs.boardTheme.colors, - enableCoordinates: trainingPrefs.showCoordinates, - borderRadius: widget.isTablet - ? const BorderRadius.all(Radius.circular(4.0)) - : BorderRadius.zero, - boxShadow: widget.isTablet ? boardShadows : const [], - ), + settings: boardPrefs.toBoardSettings().copyWith( + enableCoordinates: trainingPrefs.showCoordinates, + borderRadius: widget.isTablet + ? const BorderRadius.all(Radius.circular(4.0)) + : BorderRadius.zero, + boxShadow: + widget.isTablet ? boardShadows : const [], + ), pointerMode: EditorPointerMode.edit, onEditedSquare: (square) { if (trainingState.trainingActive && diff --git a/pubspec.lock b/pubspec.lock index b8965259ad..f212adc2a8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -196,7 +196,7 @@ packages: path: "../flutter-chessground" relative: true source: path - version: "5.4.0" + version: "6.0.0" ci: dependency: transitive description: From b7cd9a104bb51133bd923e94f253f7712806f329 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Sat, 9 Nov 2024 12:11:22 +0100 Subject: [PATCH 610/979] Revert "Merge pull request #1137 from tom-anders/collapseButton" This reverts commit 9550b08f2d2acca9ba193abd2bdc118106fdd69e, reversing changes made to 81ae8de5e260454b18b761d8bdf58d56d2e50ef2. --- .../model/analysis/analysis_controller.dart | 7 +- lib/src/widgets/pgn.dart | 131 ++++++------------ test/view/analysis/analysis_screen_test.dart | 108 --------------- 3 files changed, 43 insertions(+), 203 deletions(-) diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index 95ed2e9fb5..f14ba20ebc 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -271,11 +271,7 @@ class AnalysisController extends _$AnalysisController @override void expandVariations(UciPath path) { final node = _root.nodeAt(path); - - final childrenToHide = - _root.isOnMainline(path) ? node.children.skip(1) : node.children; - - for (final child in childrenToHide) { + for (final child in node.children) { child.isHidden = false; for (final grandChild in child.children) { grandChild.isHidden = false; @@ -287,6 +283,7 @@ class AnalysisController extends _$AnalysisController @override void collapseVariations(UciPath path) { final node = _root.nodeAt(path); + for (final child in node.children) { child.isHidden = true; } diff --git a/lib/src/widgets/pgn.dart b/lib/src/widgets/pgn.dart index 8d5144facd..f480db8883 100644 --- a/lib/src/widgets/pgn.dart +++ b/lib/src/widgets/pgn.dart @@ -615,13 +615,6 @@ class _SideLinePart extends ConsumerWidget { return moves; }, ).flattened, - if (nodes.last.children.any((node) => !node.isHidden)) - WidgetSpan( - alignment: PlaceholderAlignment.middle, - child: _CollapseVariationsButton( - onTap: () => params.notifier.collapseVariations(path), - ), - ), ]; return Text.rich( @@ -632,26 +625,6 @@ class _SideLinePart extends ConsumerWidget { } } -class _CollapseVariationsButton extends StatelessWidget { - const _CollapseVariationsButton({ - required this.onTap, - }); - - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - return AdaptiveInkWell( - onTap: onTap, - child: Icon( - Icons.indeterminate_check_box, - color: _textColor(context, 0.6), - size: _baseTextStyle.fontSize! + 5, - ), - ); - } -} - /// A widget that renders part of the mainline. /// /// A part of the mainline is rendered on a single line. See [_mainlineParts]. @@ -677,47 +650,41 @@ class _MainLinePart extends ConsumerWidget { var path = initialPath; return Text.rich( TextSpan( - children: [ - ...nodes.takeWhile((node) => node.children.isNotEmpty).mapIndexed( - (i, node) { - final mainlineNode = node.children.first; - final moves = [ - _moveWithComment( - mainlineNode, - lineInfo: ( - type: _LineType.mainline, - startLine: i == 0 || (node as ViewBranch).hasTextComment, - pathToLine: initialPath, - ), - pathToNode: path, - textStyle: textStyle, - params: params, - ), - if (node.children.length == 2 && - _displaySideLineAsInline(node.children[1])) ...[ - _buildInlineSideLine( - followsComment: mainlineNode.hasTextComment, - firstNode: node.children[1], - parent: node, - initialPath: path, + children: nodes + .takeWhile((node) => node.children.isNotEmpty) + .mapIndexed( + (i, node) { + final mainlineNode = node.children.first; + final moves = [ + _moveWithComment( + mainlineNode, + lineInfo: ( + type: _LineType.mainline, + startLine: i == 0 || (node as ViewBranch).hasTextComment, + pathToLine: initialPath, + ), + pathToNode: path, textStyle: textStyle, params: params, ), - ], - ]; - path = path + mainlineNode.id; - return moves.flattened; - }, - ).flattened, - if (nodes.last.children.skip(1).any((node) => !node.isHidden)) - WidgetSpan( - alignment: PlaceholderAlignment.middle, - child: _CollapseVariationsButton( - onTap: () => - params.notifier.collapseVariations(path.penultimate), - ), - ), - ], + if (node.children.length == 2 && + _displaySideLineAsInline(node.children[1])) ...[ + _buildInlineSideLine( + followsComment: mainlineNode.hasTextComment, + firstNode: node.children[1], + parent: node, + initialPath: path, + textStyle: textStyle, + params: params, + ), + ], + ]; + path = path + mainlineNode.id; + return moves.flattened; + }, + ) + .flattened + .toList(growable: false), ), ); } @@ -950,11 +917,16 @@ class _IndentedSideLinesState extends State<_IndentedSideLines> { children: [ ...sideLineWidgets, if (_hasHiddenLines) - _ExpandVariationsButton( - key: _sideLinesStartKeys.last, - onTap: () => widget.params.notifier.expandVariations( - widget.initialPath, + GestureDetector( + child: Icon( + Icons.add_box, + color: _textColor(context, 0.6), + key: _sideLinesStartKeys.last, + size: _baseTextStyle.fontSize! + 5, ), + onTap: () { + widget.params.notifier.expandVariations(widget.initialPath); + }, ), ], ), @@ -963,27 +935,6 @@ class _IndentedSideLinesState extends State<_IndentedSideLines> { } } -class _ExpandVariationsButton extends StatelessWidget { - const _ExpandVariationsButton({ - super.key, - required this.onTap, - }); - - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - return AdaptiveInkWell( - onTap: onTap, - child: Icon( - Icons.add_box, - color: _textColor(context, 0.6), - size: _baseTextStyle.fontSize! + 5, - ), - ); - } -} - Color? _textColor( BuildContext context, double opacity, { diff --git a/test/view/analysis/analysis_screen_test.dart b/test/view/analysis/analysis_screen_test.dart index d2bb0c0a26..c386d11011 100644 --- a/test/view/analysis/analysis_screen_test.dart +++ b/test/view/analysis/analysis_screen_test.dart @@ -255,15 +255,11 @@ void main() { expect(find.text('2… Qd7'), findsNothing); // sidelines with nesting > 2 are collapsed -> expand them - expect(find.byIcon(Icons.indeterminate_check_box), findsNWidgets(2)); expect(find.byIcon(Icons.add_box), findsOneWidget); await tester.tap(find.byIcon(Icons.add_box)); await tester.pumpAndSettle(); - expect(find.byIcon(Icons.indeterminate_check_box), findsNWidgets(3)); - expect(find.byIcon(Icons.add_box), findsNothing); - expectSameLine(tester, ['2… h5']); expectSameLine(tester, ['2… Nc6', '3. d3']); expectSameLine(tester, ['2… Qd7']); @@ -279,7 +275,6 @@ void main() { // Sidelines should be collapsed again expect(find.byIcon(Icons.add_box), findsOneWidget); - expect(find.byIcon(Icons.indeterminate_check_box), findsNWidgets(2)); expect(find.text('2… h5'), findsNothing); expect(find.text('2… Nc6'), findsNothing); @@ -287,109 +282,6 @@ void main() { expect(find.text('2… Qd7'), findsNothing); }); - testWidgets('expand/collapse sidelines via icon', (tester) async { - // Will be rendered as: - // ------------------- - // 1. e4 e5 [-] // <- collapse icon - // |- 1... c5 [-] // <- collapse icon - // |- 2. Nf3 - // |- 2. Nc3 - // |- 2. h4 - // |- 1... h5 - // 2. Ke2 - await buildTree( - tester, - '1. e4 e5 (1... c5 2. Nf3 (2. Nc3) (2. h4)) (1... h5) 2. Ke2', - ); - - // Sidelines should be visible by default - expect(find.byIcon(Icons.add_box), findsNothing); - expect(find.byIcon(Icons.indeterminate_check_box), findsNWidgets(2)); - expect(find.text('1… c5'), findsOneWidget); - expect(find.text('1… h5'), findsOneWidget); - expect(find.text('2. Nc3'), findsOneWidget); - expect(find.text('2. h4'), findsOneWidget); - - await tester.tap(find.byIcon(Icons.indeterminate_check_box).first); - - // need to wait for current move change debounce delay - await tester.pumpAndSettle(); - - // After collapsing the first sideline: - // 1. e4 e5 - // |- [+] // <- expand icon - // 2. Ke2 - expect(find.text('1… c5'), findsNothing); - expect(find.text('1… h5'), findsNothing); - expect(find.text('2. Nc3'), findsNothing); - expect(find.text('2. h4'), findsNothing); - - // Expand again - expect(find.byIcon(Icons.add_box), findsOneWidget); - await tester.tap(find.byIcon(Icons.add_box)); - // need to wait for current move change debounce delay - await tester.pumpAndSettle(); - - // Collapse the inner sidelines - await tester.tap(find.byIcon(Icons.indeterminate_check_box).last); - // need to wait for current move change debounce delay - await tester.pumpAndSettle(); - - // After collapsing the inner sidelines: - // 1. e4 e5 [-] // <- collapse icon - // |- 1... c5 - // |- [+] // <- expand icon - // |- 1... h5 - // 2. Ke2 - expect(find.text('1… c5'), findsOneWidget); - expect(find.text('1… h5'), findsOneWidget); - expect(find.text('2. Nc3'), findsNothing); - expect(find.text('2. h4'), findsNothing); - - expect(find.byIcon(Icons.add_box), findsOneWidget); - expect(find.byIcon(Icons.indeterminate_check_box), findsOneWidget); - }); - testWidgets( - 'Expanding one line does not expand the following one (regression test)', - (tester) async { - /// Will be rendered as: - /// ------------------- - /// 1. e4 e5 - /// |- 1... d5 2. Nf3 (2.Nc3) - /// 2. Nf3 - /// |- 2. a4 d5 (2... f5) - /// ------------------- - await buildTree( - tester, - '1. e4 e5 (1... d5 2. Nf3 (2. Nc3)) 2. Nf3 (2. a4 d5 (2... f5))', - ); - - expect(find.byIcon(Icons.indeterminate_check_box), findsNWidgets(2)); - expect(find.byIcon(Icons.add_box), findsNothing); - - // Collapse both lines - await tester.tap(find.byIcon(Icons.indeterminate_check_box).first); - // need to wait for current move change debounce delay - await tester.pumpAndSettle(); - await tester.tap(find.byIcon(Icons.indeterminate_check_box).first); - // need to wait for current move change debounce delay - await tester.pumpAndSettle(); - - // In this state, there used to be a bug where expanding the first line would - // also expand the second line. - expect(find.byIcon(Icons.add_box), findsNWidgets(2)); - await tester.tap(find.byIcon(Icons.add_box).first); - - // need to wait for current move change debounce delay - await tester.pumpAndSettle(); - - expect(find.byIcon(Icons.add_box), findsOneWidget); - expect(find.byIcon(Icons.indeterminate_check_box), findsOneWidget); - - // Second sideline should still be collapsed - expect(find.text('2. a4'), findsNothing); - }); - testWidgets('subtrees not part of the current mainline part are cached', (tester) async { await buildTree( From abe5f782909e327a9ba75697b82b47a88a19c046 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Sat, 9 Nov 2024 15:36:06 +0100 Subject: [PATCH 611/979] Ensure firebase auto init is disabled; wait for APNS token on iOS --- android/app/src/main/AndroidManifest.xml | 6 ++++++ ios/Podfile.lock | 18 +++++++++--------- ios/Runner/Info.plist | 2 ++ .../notifications/notification_service.dart | 8 ++++++++ test/binding.dart | 5 +++++ 5 files changed, 30 insertions(+), 9 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index f68adfbe06..ac5dfc7a14 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -25,6 +25,12 @@ + + 11.0) - GoogleUtilities/Environment (~> 8.0) - GoogleUtilities/Logger (~> 8.0) - - FirebaseCoreExtension (11.3.0): + - FirebaseCoreExtension (11.4.1): - FirebaseCore (~> 11.0) - - FirebaseCoreInternal (11.3.0): + - FirebaseCoreInternal (11.4.2): - "GoogleUtilities/NSData+zlib (~> 8.0)" - FirebaseCrashlytics (11.2.0): - FirebaseCore (~> 11.0) @@ -51,7 +51,7 @@ PODS: - GoogleUtilities/Environment (~> 8.0) - nanopb (~> 3.30910.0) - PromisesObjC (~> 2.4) - - FirebaseInstallations (11.3.0): + - FirebaseInstallations (11.4.0): - FirebaseCore (~> 11.0) - GoogleUtilities/Environment (~> 8.0) - GoogleUtilities/UserDefaults (~> 8.0) @@ -65,7 +65,7 @@ PODS: - GoogleUtilities/Reachability (~> 8.0) - GoogleUtilities/UserDefaults (~> 8.0) - nanopb (~> 3.30910.0) - - FirebaseRemoteConfigInterop (11.3.0) + - FirebaseRemoteConfigInterop (11.4.0) - FirebaseSessions (11.3.0): - FirebaseCore (~> 11.0) - FirebaseCoreExtension (~> 11.0) @@ -243,12 +243,12 @@ SPEC CHECKSUMS: firebase_crashlytics: 37d104d457b51760b48504a93a12b3bf70995d77 firebase_messaging: 15d114e1a41fc31e4fbabcd48d765a19eec94a38 FirebaseCore: a282032ae9295c795714ded2ec9c522fc237f8da - FirebaseCoreExtension: 30bb063476ef66cd46925243d64ad8b2c8ac3264 - FirebaseCoreInternal: ac26d09a70c730e497936430af4e60fb0c68ec4e + FirebaseCoreExtension: f1bc67a4702931a7caa097d8e4ac0a1b0d16720e + FirebaseCoreInternal: 35731192cab10797b88411be84940d2beb33a238 FirebaseCrashlytics: cfc69af5b53565dc6a5e563788809b5778ac4eac - FirebaseInstallations: 58cf94dabf1e2bb2fa87725a9be5c2249171cda0 + FirebaseInstallations: 6ef4a1c7eb2a61ee1f74727d7f6ce2e72acf1414 FirebaseMessaging: c9ec7b90c399c7a6100297e9d16f8a27fc7f7152 - FirebaseRemoteConfigInterop: c3a5c31b3c22079f41ba1dc645df889d9ce38cb9 + FirebaseRemoteConfigInterop: e76f46ffa4d6a65e273d4dfebb6a79e588cec136 FirebaseSessions: 655ff17f3cc1a635cbdc2d69b953878001f9e25b Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_appauth: 408f4cda69a4ad59bdf696e04cd9e13e1449b44e @@ -273,4 +273,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 76a583f8d75b3a8c6e4bdc97ae8783ef36cc7984 -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index db6cb9b752..fb3991392e 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -133,5 +133,7 @@ UIViewControllerBasedStatusBarAppearance + FirebaseMessagingAutoInitEnabled + diff --git a/lib/src/model/notifications/notification_service.dart b/lib/src/model/notifications/notification_service.dart index 43be19c9b9..b49a82a152 100644 --- a/lib/src/model/notifications/notification_service.dart +++ b/lib/src/model/notifications/notification_service.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/l10n/l10n.dart'; @@ -294,6 +295,13 @@ class NotificationService { /// Register the device for push notifications. Future registerDevice() async { + if (defaultTargetPlatform == TargetPlatform.iOS) { + final apnsToken = await FirebaseMessaging.instance.getAPNSToken(); + if (apnsToken == null) { + _logger.warning('APNS token is null'); + return; + } + } final token = await LichessBinding.instance.firebaseMessaging.getToken(); if (token != null) { await _registerToken(token); diff --git a/test/binding.dart b/test/binding.dart index 485926ce52..229f180c2a 100644 --- a/test/binding.dart +++ b/test/binding.dart @@ -320,6 +320,11 @@ class FakeFirebaseMessaging extends Fake implements FirebaseMessaging { return _token; } + @override + Future getAPNSToken() { + return Future.value('test-apns-token'); + } + @override Stream get onTokenRefresh => _tokenController.stream; From b6ed471ebc7891114bb9ea57f786f1fcf6d2b5a8 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Sat, 9 Nov 2024 17:18:22 +0100 Subject: [PATCH 612/979] Update chessground --- pubspec.lock | 7 ++++--- pubspec.yaml | 3 +-- test/test_provider_scope.dart | 3 +-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index f212adc2a8..d08b9e90db 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -193,9 +193,10 @@ packages: chessground: dependency: "direct main" description: - path: "../flutter-chessground" - relative: true - source: path + name: chessground + sha256: e2fba2e89f69a41fc77ef034a06fd087f14fdd5909b7d98bffb6e182d56d2a3a + url: "https://pub.dev" + source: hosted version: "6.0.0" ci: dependency: transitive diff --git a/pubspec.yaml b/pubspec.yaml index edda956ed7..fbe5b83111 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,8 +13,7 @@ dependencies: app_settings: ^5.1.1 async: ^2.10.0 cached_network_image: ^3.2.2 - chessground: - path: ../flutter-chessground + chessground: ^6.0.0 collection: ^1.17.0 connectivity_plus: ^6.0.2 cronet_http: ^1.3.1 diff --git a/test/test_provider_scope.dart b/test/test_provider_scope.dart index fc55c738e9..c56d49ea5b 100644 --- a/test/test_provider_scope.dart +++ b/test/test_provider_scope.dart @@ -127,13 +127,12 @@ Future makeTestProviderScope( VisibilityDetectorController.instance.updateInterval = Duration.zero; - // disable piece animation and drawing shapes to simplify tests + // disable piece animation to simplify tests final defaultBoardPref = { 'preferences.board': jsonEncode( BoardPrefs.defaults .copyWith( pieceAnimation: false, - enableShapeDrawings: false, ) .toJson(), ), From df2e0eed116c94617eb2a58f3f6db6b6136c4961 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Tue, 22 Oct 2024 23:13:44 +0200 Subject: [PATCH 613/979] feat: add study screen --- .../model/settings/preferences_storage.dart | 1 + lib/src/model/study/study_controller.dart | 622 ++++++++++++++++++ lib/src/model/study/study_preferences.dart | 51 ++ lib/src/view/study/study_bottom_bar.dart | 70 ++ lib/src/view/study/study_screen.dart | 470 ++++++++++++- lib/src/view/study/study_settings.dart | 153 +++++ lib/src/view/study/study_tree_view.dart | 60 ++ test/view/study/study_screen_test.dart | 237 +++++++ 8 files changed, 1662 insertions(+), 2 deletions(-) create mode 100644 lib/src/model/study/study_controller.dart create mode 100644 lib/src/model/study/study_preferences.dart create mode 100644 lib/src/view/study/study_bottom_bar.dart create mode 100644 lib/src/view/study/study_settings.dart create mode 100644 lib/src/view/study/study_tree_view.dart create mode 100644 test/view/study/study_screen_test.dart diff --git a/lib/src/model/settings/preferences_storage.dart b/lib/src/model/settings/preferences_storage.dart index 6531fa752c..0575ad7072 100644 --- a/lib/src/model/settings/preferences_storage.dart +++ b/lib/src/model/settings/preferences_storage.dart @@ -18,6 +18,7 @@ enum PrefCategory { home('preferences.home'), board('preferences.board'), analysis('preferences.analysis'), + study('preferences.study'), overTheBoard('preferences.overTheBoard'), challenge('preferences.challenge'), gameSetup('preferences.gameSetup'), diff --git a/lib/src/model/study/study_controller.dart b/lib/src/model/study/study_controller.dart new file mode 100644 index 0000000000..3edbf2f818 --- /dev/null +++ b/lib/src/model/study/study_controller.dart @@ -0,0 +1,622 @@ +import 'dart:async'; + +import 'package:chessground/chessground.dart'; +import 'package:collection/collection.dart'; +import 'package:dartchess/dartchess.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; +import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/common/eval.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/common/node.dart'; +import 'package:lichess_mobile/src/model/common/service/move_feedback.dart'; +import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; +import 'package:lichess_mobile/src/model/common/uci.dart'; +import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; +import 'package:lichess_mobile/src/model/engine/work.dart'; +import 'package:lichess_mobile/src/model/study/study.dart'; +import 'package:lichess_mobile/src/model/study/study_repository.dart'; +import 'package:lichess_mobile/src/utils/rate_limit.dart'; +import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; +import 'package:lichess_mobile/src/widgets/pgn.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'study_controller.freezed.dart'; +part 'study_controller.g.dart'; + +@riverpod +class StudyController extends _$StudyController implements PgnTreeNotifier { + late Root _root; + + final _engineEvalDebounce = Debouncer(const Duration(milliseconds: 150)); + + Timer? _startEngineEvalTimer; + + Future nextChapter() async { + if (state.hasValue) { + final chapters = state.requireValue.study.chapters; + final currentChapterIndex = chapters.indexWhere( + (chapter) => chapter.id == state.requireValue.study.chapter.id, + ); + goToChapter(chapters[currentChapterIndex + 1].id); + } + } + + Future goToChapter(StudyChapterId chapterId) async { + state = const AsyncValue.loading(); + state = AsyncValue.data( + await _fetchChapter( + state.requireValue.study.id, + chapterId: chapterId, + ), + ); + } + + Future _fetchChapter( + StudyId id, { + StudyChapterId? chapterId, + }) async { + final (study, pgn) = await ref + .read(studyRepositoryProvider) + .getStudy(id: id, chapterId: chapterId); + + final game = PgnGame.parsePgn(pgn); + + final rootComments = IList(game.comments.map((c) => PgnComment.fromPgn(c))); + + final variant = study.chapter.setup.variant; + final orientation = study.chapter.setup.orientation; + + try { + _root = Root.fromPgnGame(game); + } on PositionSetupException { + return StudyState( + variant: variant, + study: study, + currentPath: UciPath.empty, + isOnMainline: true, + root: null, + currentNode: StudyCurrentNode.illegalPosition(), + pgnRootComments: rootComments, + pov: orientation, + isLocalEvaluationAllowed: false, + isLocalEvaluationEnabled: false, + pgn: pgn, + ); + } + + // don't use ref.watch here: we don't want to invalidate state when the + // analysis preferences change + final prefs = ref.read(analysisPreferencesProvider); + + const currentPath = UciPath.empty; + Move? lastMove; + + final studyState = StudyState( + variant: variant, + study: study, + currentPath: currentPath, + isOnMainline: true, + root: _root.view, + currentNode: StudyCurrentNode.fromNode(_root), + pgnRootComments: rootComments, + lastMove: lastMove, + pov: orientation, + isLocalEvaluationAllowed: + study.chapter.features.computer && !study.chapter.gamebook, + isLocalEvaluationEnabled: prefs.enableLocalEvaluation, + pgn: pgn, + ); + + final evaluationService = ref.watch(evaluationServiceProvider); + if (studyState.isEngineAvailable) { + await evaluationService.disposeEngine(); + + evaluationService + .initEngine( + _evaluationContext(studyState.variant), + options: EvaluationOptions( + multiPv: prefs.numEvalLines, + cores: prefs.numEngineCores, + ), + ) + .then((_) { + _startEngineEvalTimer = Timer(const Duration(milliseconds: 250), () { + _startEngineEval(); + }); + }); + } + + return studyState; + } + + @override + Future build(StudyId id) async { + final evaluationService = ref.watch(evaluationServiceProvider); + ref.onDispose(() { + _startEngineEvalTimer?.cancel(); + _engineEvalDebounce.dispose(); + evaluationService.disposeEngine(); + }); + return _fetchChapter(id); + } + + EvaluationContext _evaluationContext(Variant variant) => EvaluationContext( + variant: variant, + initialPosition: _root.position, + ); + + void onUserMove(NormalMove move) { + if (!state.hasValue || state.requireValue.position == null) return; + + if (!state.requireValue.position!.isLegal(move)) return; + + if (isPromotionPawnMove(state.requireValue.position!, move)) { + state = AsyncValue.data(state.requireValue.copyWith(promotionMove: move)); + return; + } + + final (newPath, isNewNode) = + _root.addMoveAt(state.requireValue.currentPath, move); + if (newPath != null) { + _setPath( + newPath, + shouldRecomputeRootView: isNewNode, + shouldForceShowVariation: true, + ); + } + } + + void onPromotionSelection(Role? role) { + final state = this.state.valueOrNull; + if (state == null) return; + + if (role == null) { + this.state = AsyncValue.data(state.copyWith(promotionMove: null)); + return; + } + final promotionMove = state.promotionMove; + if (promotionMove != null) { + final promotion = promotionMove.withPromotion(role); + onUserMove(promotion); + } + } + + void showGamebookSolution() { + onUserMove(state.requireValue.currentNode.children.first as NormalMove); + } + + void userNext() { + final state = this.state.valueOrNull; + if (state!.currentNode.children.isEmpty) return; + _setPath( + state.currentPath + _root.nodeAt(state.currentPath).children.first.id, + replaying: true, + ); + } + + void jumpToNthNodeOnMainline(int n) { + UciPath path = _root.mainlinePath; + while (!path.penultimate.isEmpty) { + path = path.penultimate; + } + Node? node = _root.nodeAt(path); + int count = 0; + + while (node != null && count < n) { + if (node.children.isNotEmpty) { + path = path + node.children.first.id; + node = _root.nodeAt(path); + count++; + } else { + break; + } + } + + if (node != null) { + userJump(path); + } + } + + void toggleBoard() { + final state = this.state.valueOrNull; + if (state != null) { + this.state = AsyncValue.data(state.copyWith(pov: state.pov.opposite)); + } + } + + void userPrevious() { + if (state.hasValue) { + _setPath(state.requireValue.currentPath.penultimate, replaying: true); + } + } + + void reset() { + if (state.hasValue) { + _setPath(UciPath.empty); + } + } + + @override + void userJump(UciPath path) { + _setPath(path); + } + + @override + void expandVariations(UciPath path) { + if (!state.hasValue) return; + + final node = _root.nodeAt(path); + for (final child in node.children) { + child.isHidden = false; + for (final grandChild in child.children) { + grandChild.isHidden = false; + } + } + state = AsyncValue.data(state.requireValue.copyWith(root: _root.view)); + } + + @override + void collapseVariations(UciPath path) { + if (!state.hasValue) return; + + final node = _root.nodeAt(path); + + for (final child in node.children) { + child.isHidden = true; + } + + state = AsyncValue.data(state.requireValue.copyWith(root: _root.view)); + } + + @override + void promoteVariation(UciPath path, bool toMainline) { + final state = this.state.valueOrNull; + if (state == null) return; + _root.promoteAt(path, toMainline: toMainline); + this.state = AsyncValue.data( + state.copyWith( + isOnMainline: _root.isOnMainline(state.currentPath), + root: _root.view, + ), + ); + } + + @override + void deleteFromHere(UciPath path) { + if (!state.hasValue) return; + + _root.deleteAt(path); + _setPath(path.penultimate, shouldRecomputeRootView: true); + } + + Future toggleLocalEvaluation() async { + if (!state.hasValue) return; + + ref + .read(analysisPreferencesProvider.notifier) + .toggleEnableLocalEvaluation(); + + state = AsyncValue.data( + state.requireValue.copyWith( + isLocalEvaluationEnabled: !state.requireValue.isLocalEvaluationEnabled, + ), + ); + + if (state.requireValue.isEngineAvailable) { + final prefs = ref.read(analysisPreferencesProvider); + await ref.read(evaluationServiceProvider).initEngine( + _evaluationContext(state.requireValue.variant), + options: EvaluationOptions( + multiPv: prefs.numEvalLines, + cores: prefs.numEngineCores, + ), + ); + _startEngineEval(); + } else { + _stopEngineEval(); + ref.read(evaluationServiceProvider).disposeEngine(); + } + } + + void setNumEvalLines(int numEvalLines) { + if (!state.hasValue) return; + + ref + .read(analysisPreferencesProvider.notifier) + .setNumEvalLines(numEvalLines); + + ref.read(evaluationServiceProvider).setOptions( + EvaluationOptions( + multiPv: numEvalLines, + cores: ref.read(analysisPreferencesProvider).numEngineCores, + ), + ); + + _root.updateAll((node) => node.eval = null); + + state = AsyncValue.data( + state.requireValue.copyWith( + currentNode: StudyCurrentNode.fromNode( + _root.nodeAt(state.requireValue.currentPath), + ), + ), + ); + + _startEngineEval(); + } + + void setEngineCores(int numEngineCores) { + ref + .read(analysisPreferencesProvider.notifier) + .setEngineCores(numEngineCores); + + ref.read(evaluationServiceProvider).setOptions( + EvaluationOptions( + multiPv: ref.read(analysisPreferencesProvider).numEvalLines, + cores: numEngineCores, + ), + ); + + _startEngineEval(); + } + + void _setPath( + UciPath path, { + bool shouldForceShowVariation = false, + bool shouldRecomputeRootView = false, + bool replaying = false, + }) { + final state = this.state.valueOrNull; + if (state == null) return; + + final pathChange = state.currentPath != path; + final currentNode = _root.nodeAt(path); + + // always show variation if the user plays a move + if (shouldForceShowVariation && + currentNode is Branch && + currentNode.isHidden) { + _root.updateAt(path, (node) { + if (node is Branch) node.isHidden = false; + }); + } + + // root view is only used to display move list, so we need to + // recompute the root view only when the nodelist length changes + // or a variation is hidden/shown + final rootView = shouldForceShowVariation || shouldRecomputeRootView + ? _root.view + : state.root; + + final isForward = path.size > state.currentPath.size; + if (currentNode is Branch) { + if (!replaying) { + if (isForward) { + final isCheck = currentNode.sanMove.isCheck; + if (currentNode.sanMove.isCapture) { + ref + .read(moveFeedbackServiceProvider) + .captureFeedback(check: isCheck); + } else { + ref.read(moveFeedbackServiceProvider).moveFeedback(check: isCheck); + } + } + } else if (isForward) { + final soundService = ref.read(soundServiceProvider); + if (currentNode.sanMove.isCapture) { + soundService.play(Sound.capture); + } else { + soundService.play(Sound.move); + } + } + + this.state = AsyncValue.data( + state.copyWith( + currentPath: path, + isOnMainline: _root.isOnMainline(path), + currentNode: StudyCurrentNode.fromNode(currentNode), + lastMove: currentNode.sanMove.move, + promotionMove: null, + root: rootView, + ), + ); + } else { + this.state = AsyncValue.data( + state.copyWith( + currentPath: path, + isOnMainline: _root.isOnMainline(path), + currentNode: StudyCurrentNode.fromNode(currentNode), + lastMove: null, + promotionMove: null, + root: rootView, + ), + ); + } + + if (pathChange) { + _debouncedStartEngineEval(); + } + } + + void _startEngineEval() { + final state = this.state.valueOrNull; + if (state == null || !state.isEngineAvailable) return; + + ref + .read(evaluationServiceProvider) + .start( + state.currentPath, + _root.branchesOn(state.currentPath).map(Step.fromNode), + // Note: AnalysisController passes _root.eval as initialPositionEval here, + // but for studies this leads to false positive cache hits when switching between chapters. + shouldEmit: (work) => work.path == state.currentPath, + ) + ?.forEach( + (t) => _root.updateAt(t.$1.path, (node) => node.eval = t.$2), + ); + } + + void _debouncedStartEngineEval() { + _engineEvalDebounce(() { + _startEngineEval(); + }); + } + + void _stopEngineEval() { + ref.read(evaluationServiceProvider).stop(); + + if (!state.hasValue) return; + + // update the current node with last cached eval + state = AsyncValue.data( + state.requireValue.copyWith( + currentNode: StudyCurrentNode.fromNode( + _root.nodeAt(state.requireValue.currentPath), + ), + ), + ); + } +} + +@freezed +class StudyState with _$StudyState { + const StudyState._(); + + const factory StudyState({ + required Study study, + required String pgn, + + /// The variant of the current chapter + required Variant variant, + + /// Immutable view of the whole tree. Null if the chapter's starting position is illegal. + required ViewRoot? root, + + /// The current node in the study tree view. + /// + /// This is an immutable copy of the actual [Node] at the `currentPath`. + /// We don't want to use [Node.view] here because it'd copy the whole tree + /// under the current node and it's expensive. + required StudyCurrentNode currentNode, + + /// The path to the current node in the analysis view. + required UciPath currentPath, + + /// Whether the current path is on the mainline. + required bool isOnMainline, + + /// The side to display the board from. + required Side pov, + + /// Whether local evaluation is allowed for this study. + required bool isLocalEvaluationAllowed, + + /// Whether the user has enabled local evaluation. + required bool isLocalEvaluationEnabled, + + /// The last move played. + Move? lastMove, + + /// Possible promotion move to be played. + NormalMove? promotionMove, + + /// The PGN root comments of the study + IList? pgnRootComments, + }) = _StudyState; + + IMap> get validMoves => currentNode.position != null + ? makeLegalMoves(currentNode.position!) + : const IMap.empty(); + + /// Whether the engine is available for evaluation + bool get isEngineAvailable => + isLocalEvaluationAllowed && + engineSupportedVariants.contains(variant) && + isLocalEvaluationEnabled; + + EngineGaugeParams? get engineGaugeParams => isEngineAvailable + ? ( + orientation: pov, + isLocalEngineAvailable: isEngineAvailable, + position: position!, + savedEval: currentNode.eval, + ) + : null; + + Position? get position => currentNode.position; + StudyChapter get currentChapter => study.chapter; + bool get canGoNext => currentNode.children.isNotEmpty; + bool get canGoBack => currentPath.size > UciPath.empty.size; + + String get currentChapterTitle => study.chapters + .firstWhere( + (chapter) => chapter.id == currentChapter.id, + ) + .name; + bool get hasNextChapter => study.chapter.id != study.chapters.last.id; + + bool get isAtEndOfChapter => isOnMainline && currentNode.children.isEmpty; + + bool get isAtStartOfChapter => currentPath.isEmpty; + + bool get isIntroductoryChapter => + currentNode.isRoot && currentNode.children.isEmpty; + + IList get pgnShapes => IList( + (currentNode.isRoot ? pgnRootComments : currentNode.comments) + ?.map((comment) => comment.shapes) + .flattened, + ); + + PlayerSide get playerSide => PlayerSide.both; +} + +@freezed +class StudyCurrentNode with _$StudyCurrentNode { + const StudyCurrentNode._(); + + const factory StudyCurrentNode({ + // Null if the chapter's starting position is illegal. + required Position? position, + required List children, + required bool isRoot, + SanMove? sanMove, + IList? startingComments, + IList? comments, + IList? nags, + ClientEval? eval, + }) = _StudyCurrentNode; + + factory StudyCurrentNode.illegalPosition() { + return const StudyCurrentNode( + position: null, + children: [], + isRoot: true, + ); + } + + factory StudyCurrentNode.fromNode(Node node) { + final children = node.children.map((n) => n.sanMove.move).toList(); + if (node is Branch) { + return StudyCurrentNode( + sanMove: node.sanMove, + position: node.position, + isRoot: false, + children: children, + eval: node.eval, + startingComments: IList(node.startingComments), + comments: IList(node.comments), + nags: IList(node.nags), + ); + } else { + return StudyCurrentNode( + position: node.position, + children: children, + eval: node.eval, + isRoot: true, + ); + } + } +} diff --git a/lib/src/model/study/study_preferences.dart b/lib/src/model/study/study_preferences.dart new file mode 100644 index 0000000000..fe3fb2f7fa --- /dev/null +++ b/lib/src/model/study/study_preferences.dart @@ -0,0 +1,51 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'study_preferences.freezed.dart'; +part 'study_preferences.g.dart'; + +@riverpod +class StudyPreferences extends _$StudyPreferences + with PreferencesStorage { + // ignore: avoid_public_notifier_properties + @override + final prefCategory = PrefCategory.study; + + // ignore: avoid_public_notifier_properties + @override + StudyPrefs get defaults => StudyPrefs.defaults; + + @override + StudyPrefs fromJson(Map json) => StudyPrefs.fromJson(json); + + @override + StudyPrefs build() { + return fetch(); + } + + Future toggleShowVariationArrows() { + return save( + state.copyWith( + showVariationArrows: !state.showVariationArrows, + ), + ); + } +} + +@Freezed(fromJson: true, toJson: true) +class StudyPrefs with _$StudyPrefs implements Serializable { + const StudyPrefs._(); + + const factory StudyPrefs({ + required bool showVariationArrows, + }) = _StudyPrefs; + + static const defaults = StudyPrefs( + showVariationArrows: false, + ); + + factory StudyPrefs.fromJson(Map json) { + return _$StudyPrefsFromJson(json); + } +} diff --git a/lib/src/view/study/study_bottom_bar.dart b/lib/src/view/study/study_bottom_bar.dart new file mode 100644 index 0000000000..2e09847174 --- /dev/null +++ b/lib/src/view/study/study_bottom_bar.dart @@ -0,0 +1,70 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/study/study_controller.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; +import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; + +class StudyBottomBar extends ConsumerWidget { + const StudyBottomBar({ + required this.id, + }); + + final StudyId id; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final state = ref.watch(studyControllerProvider(id)).valueOrNull; + if (state == null) { + return const BottomBar(children: []); + } + + final onGoForward = state.canGoNext + ? ref.read(studyControllerProvider(id).notifier).userNext + : null; + final onGoBack = state.canGoBack + ? ref.read(studyControllerProvider(id).notifier).userPrevious + : null; + + return BottomBar( + children: [ + RepeatButton( + onLongPress: onGoBack, + child: BottomBarButton( + key: const ValueKey('goto-previous'), + onTap: onGoBack, + label: 'Previous', + showLabel: true, + icon: CupertinoIcons.chevron_back, + showTooltip: false, + ), + ), + BottomBarButton( + onTap: state.hasNextChapter + ? ref.read(studyControllerProvider(id).notifier).nextChapter + : null, + icon: Icons.play_arrow, + label: 'Next chapter', + showLabel: true, + blink: !state.isIntroductoryChapter && + state.isAtEndOfChapter && + state.hasNextChapter, + ), + RepeatButton( + onLongPress: onGoForward, + child: BottomBarButton( + key: const ValueKey('goto-next'), + icon: CupertinoIcons.chevron_forward, + onTap: onGoForward, + label: context.l10n.next, + showLabel: true, + showTooltip: false, + ), + ), + ], + ); + } +} diff --git a/lib/src/view/study/study_screen.dart b/lib/src/view/study/study_screen.dart index 5e954b702f..8df9a9dd38 100644 --- a/lib/src/view/study/study_screen.dart +++ b/lib/src/view/study/study_screen.dart @@ -1,6 +1,35 @@ -import 'package:flutter/widgets.dart'; +import 'package:chessground/chessground.dart'; +import 'package:collection/collection.dart'; +import 'package:dartchess/dartchess.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/constants.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; +import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/common/eval.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; +import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; +import 'package:lichess_mobile/src/model/study/study_controller.dart'; +import 'package:lichess_mobile/src/model/study/study_preferences.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/screen.dart'; +import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; +import 'package:lichess_mobile/src/view/engine/engine_lines.dart'; +import 'package:lichess_mobile/src/view/study/study_bottom_bar.dart'; +import 'package:lichess_mobile/src/view/study/study_settings.dart'; +import 'package:lichess_mobile/src/view/study/study_tree_view.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/list.dart'; +import 'package:lichess_mobile/src/widgets/pgn.dart'; +import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; +import 'package:logging/logging.dart'; + +final _logger = Logger('StudyScreen'); class StudyScreen extends ConsumerWidget { const StudyScreen({ @@ -11,6 +40,443 @@ class StudyScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return const SizedBox.shrink(); + final state = ref.watch(studyControllerProvider(id)); + + return state.when( + data: (state) { + return PlatformScaffold( + appBar: PlatformAppBar( + title: FittedBox( + fit: BoxFit.scaleDown, + child: Text(state.currentChapterTitle), + ), + actions: [ + AppBarIconButton( + onPressed: () => showAdaptiveBottomSheet( + context: context, + isScrollControlled: true, + showDragHandle: true, + isDismissible: true, + builder: (_) => StudySettings(id), + ), + semanticsLabel: context.l10n.settingsSettings, + icon: const Icon(Icons.settings), + ), + _ChapterButton(id: id), + ], + ), + body: _Body(id: id), + ); + }, + loading: () { + return const PlatformScaffold( + appBar: PlatformAppBar( + title: Text(''), + ), + body: Center(child: CircularProgressIndicator()), + ); + }, + error: (error, st) { + _logger.severe('Cannot load study: $error', st); + Navigator.of(context).pop(); + return const SizedBox.shrink(); + }, + ); + } +} + +class _ChapterButton extends ConsumerWidget { + const _ChapterButton({required this.id}); + + final StudyId id; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final state = ref.watch(studyControllerProvider(id)).valueOrNull; + return state == null + ? const SizedBox.shrink() + : AppBarIconButton( + onPressed: () => showAdaptiveDialog( + context: context, + builder: (context) { + return SimpleDialog( + title: const Text('Chapters'), + children: [ + SizedBox( + height: MediaQuery.of(context).size.height * 0.8, + width: MediaQuery.of(context).size.width * 0.8, + child: ListView.separated( + itemBuilder: (context, index) { + final chapter = state.study.chapters[index]; + final selected = + chapter.id == state.currentChapter.id; + final checkedIcon = Theme.of(context).platform == + TargetPlatform.android + ? const Icon(Icons.check) + : Icon( + CupertinoIcons.check_mark_circled_solid, + color: + CupertinoTheme.of(context).primaryColor, + ); + return PlatformListTile( + selected: selected, + trailing: selected ? checkedIcon : null, + title: Text(chapter.name), + onTap: () { + ref + .read(studyControllerProvider(id).notifier) + .goToChapter( + chapter.id, + ); + Navigator.of(context).pop(); + }, + ); + }, + separatorBuilder: (_, __) => const PlatformDivider( + height: 1, + ), + itemCount: state.study.chapters.length, + ), + ), + ], + ); + }, + ), + semanticsLabel: 'Chapters', + icon: const Icon(Icons.menu_book), + ); + } +} + +class _Body extends ConsumerWidget { + const _Body({ + required this.id, + }); + + final StudyId id; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return SafeArea( + child: Column( + children: [ + Expanded( + child: LayoutBuilder( + builder: (context, constraints) { + final defaultBoardSize = constraints.biggest.shortestSide; + final isTablet = isTabletOrLarger(context); + final remainingHeight = + constraints.maxHeight - defaultBoardSize; + final isSmallScreen = + remainingHeight < kSmallRemainingHeightLeftBoardThreshold; + final boardSize = isTablet || isSmallScreen + ? defaultBoardSize - kTabletBoardTableSidePadding * 2 + : defaultBoardSize; + + final landscape = constraints.biggest.aspectRatio > 1; + + final engineGaugeParams = ref.watch( + studyControllerProvider(id) + .select((state) => state.valueOrNull?.engineGaugeParams), + ); + + final currentNode = ref.watch( + studyControllerProvider(id) + .select((state) => state.requireValue.currentNode), + ); + + final engineLines = EngineLines( + clientEval: currentNode.eval, + isGameOver: currentNode.position?.isGameOver ?? false, + onTapMove: ref + .read( + studyControllerProvider(id).notifier, + ) + .onUserMove, + ); + + final showEvaluationGauge = ref.watch( + analysisPreferencesProvider + .select((value) => value.showEvaluationGauge), + ); + + final engineGauge = + showEvaluationGauge && engineGaugeParams != null + ? EngineGauge( + params: engineGaugeParams, + displayMode: landscape + ? EngineGaugeDisplayMode.vertical + : EngineGaugeDisplayMode.horizontal, + ) + : null; + + return landscape + ? Row( + mainAxisSize: MainAxisSize.max, + children: [ + Padding( + padding: EdgeInsets.all( + isTablet ? kTabletBoardTableSidePadding : 0.0, + ), + child: Row( + children: [ + _StudyBoard( + id: id, + boardSize: boardSize, + isTablet: isTablet, + ), + if (engineGauge != null) ...[ + const SizedBox(width: 4.0), + engineGauge, + ], + ], + ), + ), + Flexible( + fit: FlexFit.loose, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + if (engineGaugeParams != null) engineLines, + Expanded( + child: PlatformCard( + clipBehavior: Clip.hardEdge, + borderRadius: const BorderRadius.all( + Radius.circular(4.0), + ), + margin: const EdgeInsets.all( + kTabletBoardTableSidePadding, + ), + semanticContainer: false, + child: StudyTreeView(id), + ), + ), + ], + ), + ), + ], + ) + : Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: EdgeInsets.all( + isTablet ? kTabletBoardTableSidePadding : 0.0, + ), + child: Column( + children: [ + if (engineGauge != null) ...[ + engineGauge, + engineLines, + ], + _StudyBoard( + id: id, + boardSize: boardSize, + isTablet: isTablet, + ), + ], + ), + ), + Expanded( + child: Padding( + padding: isTablet + ? const EdgeInsets.symmetric( + horizontal: kTabletBoardTableSidePadding, + ) + : EdgeInsets.zero, + child: StudyTreeView(id), + ), + ), + ], + ); + }, + ), + ), + StudyBottomBar(id: id), + ], + ), + ); + } +} + +extension on PgnCommentShape { + Shape get chessground { + final shapeColor = switch (color) { + CommentShapeColor.green => ShapeColor.green, + CommentShapeColor.red => ShapeColor.red, + CommentShapeColor.blue => ShapeColor.blue, + CommentShapeColor.yellow => ShapeColor.yellow, + }; + return from != to + ? Arrow( + color: shapeColor.color, + orig: from, + dest: to, + ) + : Circle(color: shapeColor.color, orig: from); + } +} + +class _StudyBoard extends ConsumerStatefulWidget { + const _StudyBoard({ + required this.id, + required this.boardSize, + required this.isTablet, + }); + + final StudyId id; + + final double boardSize; + + final bool isTablet; + + @override + ConsumerState<_StudyBoard> createState() => _StudyBoardState(); +} + +class _StudyBoardState extends ConsumerState<_StudyBoard> { + ISet userShapes = ISet(); + + @override + Widget build(BuildContext context) { + // Clear shapes when switching to a new chapter. + // This avoids "leftover" shapes from the previous chapter when the engine has not evaluated the new position yet. + ref.listen( + studyControllerProvider(widget.id).select((state) => state.hasValue), + (prev, next) { + if (prev != next) { + setState(() { + userShapes = ISet(); + }); + } + }); + final boardPrefs = ref.watch(boardPreferencesProvider); + + final studyState = + ref.watch(studyControllerProvider(widget.id)).requireValue; + + final currentNode = studyState.currentNode; + final position = currentNode.position; + + final showVariationArrows = ref.watch( + studyPreferencesProvider.select( + (prefs) => prefs.showVariationArrows, + ), + ) && + currentNode.children.length > 1; + + final pgnShapes = ISet( + studyState.pgnShapes.map((shape) => shape.chessground), + ); + + final variationArrows = ISet( + showVariationArrows + ? currentNode.children.mapIndexed((i, move) { + final color = Colors.white.withValues(alpha: i == 0 ? 0.9 : 0.5); + return Arrow( + color: color, + orig: (move as NormalMove).from, + dest: move.to, + ); + }).toList() + : [], + ); + + final showAnnotationsOnBoard = ref.watch( + analysisPreferencesProvider.select((value) => value.showAnnotations), + ); + + final showBestMoveArrow = ref.watch( + analysisPreferencesProvider.select( + (value) => value.showBestMoveArrow, + ), + ); + final bestMoves = ref.watch( + engineEvaluationProvider.select((s) => s.eval?.bestMoves), + ); + final ISet bestMoveShapes = + showBestMoveArrow && studyState.isEngineAvailable && bestMoves != null + ? computeBestMoveShapes( + bestMoves, + currentNode.position!.turn, + boardPrefs.pieceSet.assets, + ) + : ISet(); + + final sanMove = currentNode.sanMove; + final annotation = makeAnnotation(studyState.currentNode.nags); + + return Chessboard( + size: widget.boardSize, + settings: boardPrefs.toBoardSettings().copyWith( + borderRadius: widget.isTablet + ? const BorderRadius.all(Radius.circular(4.0)) + : BorderRadius.zero, + boxShadow: widget.isTablet ? boardShadows : const [], + drawShape: DrawShapeOptions( + enable: true, + onCompleteShape: _onCompleteShape, + onClearShapes: _onClearShapes, + newShapeColor: boardPrefs.shapeColor.color, + ), + ), + fen: studyState.position?.board.fen ?? + studyState.study.currentChapterMeta.fen ?? + kInitialFEN, + lastMove: studyState.lastMove as NormalMove?, + orientation: studyState.pov, + shapes: pgnShapes + .union(userShapes) + .union(variationArrows) + .union(bestMoveShapes), + annotations: + showAnnotationsOnBoard && sanMove != null && annotation != null + ? altCastles.containsKey(sanMove.move.uci) + ? IMap({ + Move.parse(altCastles[sanMove.move.uci]!)!.to: annotation, + }) + : IMap({sanMove.move.to: annotation}) + : null, + game: position != null + ? GameData( + playerSide: studyState.playerSide, + isCheck: position.isCheck, + sideToMove: position.turn, + validMoves: makeLegalMoves(position), + promotionMove: studyState.promotionMove, + onMove: (move, {isDrop, captured}) { + ref + .read(studyControllerProvider(widget.id).notifier) + .onUserMove(move); + }, + onPromotionSelection: (role) { + ref + .read(studyControllerProvider(widget.id).notifier) + .onPromotionSelection(role); + }, + ) + : null, + ); + } + + void _onCompleteShape(Shape shape) { + if (userShapes.any((element) => element == shape)) { + setState(() { + userShapes = userShapes.remove(shape); + }); + return; + } else { + setState(() { + userShapes = userShapes.add(shape); + }); + } + } + + void _onClearShapes() { + setState(() { + userShapes = ISet(); + }); } } diff --git a/lib/src/view/study/study_settings.dart b/lib/src/view/study/study_settings.dart new file mode 100644 index 0000000000..79349745e6 --- /dev/null +++ b/lib/src/view/study/study_settings.dart @@ -0,0 +1,153 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; +import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; +import 'package:lichess_mobile/src/model/study/study_controller.dart'; +import 'package:lichess_mobile/src/model/study/study_preferences.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; +import 'package:lichess_mobile/src/widgets/list.dart'; +import 'package:lichess_mobile/src/widgets/non_linear_slider.dart'; +import 'package:lichess_mobile/src/widgets/settings.dart'; + +class StudySettings extends ConsumerWidget { + const StudySettings(this.id); + + final StudyId id; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final studyController = studyControllerProvider(id); + + final isLocalEvaluationAllowed = ref.watch( + studyController.select((s) => s.requireValue.isLocalEvaluationAllowed), + ); + final isEngineAvailable = ref.watch( + studyController.select((s) => s.requireValue.isEngineAvailable), + ); + + final analysisPrefs = ref.watch(analysisPreferencesProvider); + final studyPrefs = ref.watch(studyPreferencesProvider); + final isSoundEnabled = ref.watch( + generalPreferencesProvider.select((pref) => pref.isSoundEnabled), + ); + + return BottomSheetScrollableContainer( + children: [ + SwitchSettingTile( + title: Text(context.l10n.toggleLocalEvaluation), + value: analysisPrefs.enableLocalEvaluation, + onChanged: isLocalEvaluationAllowed + ? (_) { + ref.read(studyController.notifier).toggleLocalEvaluation(); + } + : null, + ), + PlatformListTile( + title: Text.rich( + TextSpan( + text: '${context.l10n.multipleLines}: ', + style: const TextStyle( + fontWeight: FontWeight.normal, + ), + children: [ + TextSpan( + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + text: analysisPrefs.numEvalLines.toString(), + ), + ], + ), + ), + subtitle: NonLinearSlider( + value: analysisPrefs.numEvalLines, + values: const [1, 2, 3], + onChangeEnd: isEngineAvailable + ? (value) => ref + .read(studyController.notifier) + .setNumEvalLines(value.toInt()) + : null, + ), + ), + if (maxEngineCores > 1) + PlatformListTile( + title: Text.rich( + TextSpan( + text: '${context.l10n.cpus}: ', + style: const TextStyle( + fontWeight: FontWeight.normal, + ), + children: [ + TextSpan( + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + text: analysisPrefs.numEngineCores.toString(), + ), + ], + ), + ), + subtitle: NonLinearSlider( + value: analysisPrefs.numEngineCores, + values: List.generate(maxEngineCores, (index) => index + 1), + onChangeEnd: isEngineAvailable + ? (value) => ref + .read(studyController.notifier) + .setEngineCores(value.toInt()) + : null, + ), + ), + SwitchSettingTile( + title: Text(context.l10n.bestMoveArrow), + value: analysisPrefs.showBestMoveArrow, + onChanged: isEngineAvailable + ? (value) => ref + .read(analysisPreferencesProvider.notifier) + .toggleShowBestMoveArrow() + : null, + ), + SwitchSettingTile( + title: Text(context.l10n.showVariationArrows), + value: studyPrefs.showVariationArrows, + onChanged: (value) => ref + .read(studyPreferencesProvider.notifier) + .toggleShowVariationArrows(), + ), + SwitchSettingTile( + title: Text(context.l10n.evaluationGauge), + value: analysisPrefs.showEvaluationGauge, + onChanged: (value) => ref + .read(analysisPreferencesProvider.notifier) + .toggleShowEvaluationGauge(), + ), + SwitchSettingTile( + title: Text(context.l10n.toggleGlyphAnnotations), + value: analysisPrefs.showAnnotations, + onChanged: (_) => ref + .read(analysisPreferencesProvider.notifier) + .toggleAnnotations(), + ), + SwitchSettingTile( + title: Text(context.l10n.mobileShowComments), + value: analysisPrefs.showPgnComments, + onChanged: (_) => ref + .read(analysisPreferencesProvider.notifier) + .togglePgnComments(), + ), + SwitchSettingTile( + title: Text(context.l10n.sound), + value: isSoundEnabled, + onChanged: (value) { + ref.read(generalPreferencesProvider.notifier).toggleSoundEnabled(); + }, + ), + ], + ); + } +} diff --git a/lib/src/view/study/study_tree_view.dart b/lib/src/view/study/study_tree_view.dart new file mode 100644 index 0000000000..751b7c58af --- /dev/null +++ b/lib/src/view/study/study_tree_view.dart @@ -0,0 +1,60 @@ +import 'package:dartchess/dartchess.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/common/node.dart'; +import 'package:lichess_mobile/src/model/study/study_controller.dart'; +import 'package:lichess_mobile/src/widgets/pgn.dart'; + +const kNextChapterButtonHeight = 32.0; + +class StudyTreeView extends ConsumerWidget { + const StudyTreeView( + this.id, + ); + + final StudyId id; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final root = ref.watch( + studyControllerProvider(id) + .select((value) => value.requireValue.root), + ) ?? + // If root is null, the study chapter's position is illegal. + // We still want to display the root comments though, so create a dummy root. + const ViewRoot(position: Chess.initial, children: IList.empty()); + + final currentPath = ref.watch( + studyControllerProvider(id) + .select((value) => value.requireValue.currentPath), + ); + + final pgnRootComments = ref.watch( + studyControllerProvider(id) + .select((value) => value.requireValue.pgnRootComments), + ); + + return CustomScrollView( + slivers: [ + SliverFillRemaining( + hasScrollBody: false, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: DebouncedPgnTreeView( + root: root, + currentPath: currentPath, + pgnRootComments: pgnRootComments, + notifier: ref.read(studyControllerProvider(id).notifier), + ), + ), + ], + ), + ), + ], + ); + } +} diff --git a/test/view/study/study_screen_test.dart b/test/view/study/study_screen_test.dart new file mode 100644 index 0000000000..9c52604889 --- /dev/null +++ b/test/view/study/study_screen_test.dart @@ -0,0 +1,237 @@ +import 'package:chessground/chessground.dart'; +import 'package:dartchess/dartchess.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/study/study.dart'; +import 'package:lichess_mobile/src/model/study/study_repository.dart'; +import 'package:lichess_mobile/src/view/study/study_screen.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../../test_helpers.dart'; +import '../../test_provider_scope.dart'; + +class MockStudyRepository extends Mock implements StudyRepository {} + +const testId = StudyId('test-id'); + +StudyChapter makeChapter({ + required StudyChapterId id, + Side orientation = Side.white, + bool gamebook = false, +}) { + return StudyChapter( + id: id, + setup: StudyChapterSetup( + id: null, + orientation: orientation, + variant: Variant.standard, + fromFen: null, + ), + conceal: null, + features: (computer: false, explorer: false), + gamebook: gamebook, + practise: false, + ); +} + +Study makeStudy({ + StudyChapter? chapter, + IList? chapters, + IList hints = const IList.empty(), + IList deviationComments = const IList.empty(), +}) { + chapter = chapter ?? makeChapter(id: const StudyChapterId('1')); + return Study( + id: testId, + name: '', + liked: false, + likes: 0, + ownerId: null, + features: (cloneable: false, chat: false, sticky: false), + topics: const IList.empty(), + chapters: chapters ?? + IList([StudyChapterMeta(id: chapter.id, name: '', fen: null)]), + chapter: chapter, + hints: hints, + deviationComments: deviationComments, + ); +} + +void main() { + group('Study screen', () { + testWidgets('Displays PGN moves and comments', (WidgetTester tester) async { + final mockRepository = MockStudyRepository(); + + when(() => mockRepository.getStudy(id: testId)).thenAnswer( + (_) async => + (makeStudy(), '{root comment} 1. e4 {wow} e5 {such chess}'), + ); + + final app = await makeTestProviderScopeApp( + tester, + home: const StudyScreen(id: testId), + overrides: [ + studyRepositoryProvider.overrideWith( + (ref) => mockRepository, + ), + ], + ); + await tester.pumpWidget(app); + await tester.pumpAndSettle(); + + expect(find.text('root comment'), findsOneWidget); + expect(find.text('1. e4'), findsOneWidget); + expect(find.textContaining('wow'), findsOneWidget); + expect(find.textContaining('e5'), findsOneWidget); + expect(find.textContaining('such chess'), findsOneWidget); + }); + + testWidgets('Switch between chapters', (WidgetTester tester) async { + final mockRepository = MockStudyRepository(); + + final studyChapter1 = makeStudy( + chapter: makeChapter(id: const StudyChapterId('1')), + chapters: IList(const [ + StudyChapterMeta( + id: StudyChapterId('1'), + name: 'Chapter 1', + fen: null, + ), + StudyChapterMeta( + id: StudyChapterId('2'), + name: 'Chapter 2', + fen: null, + ), + ]), + ); + + final studyChapter2 = studyChapter1.copyWith( + chapter: makeChapter(id: const StudyChapterId('2')), + ); + + when(() => mockRepository.getStudy(id: testId)).thenAnswer( + (_) async => (studyChapter1, '{pgn 1}'), + ); + when( + () => mockRepository.getStudy( + id: testId, + chapterId: const StudyChapterId('1'), + ), + ).thenAnswer( + (_) async => (studyChapter1, '{pgn 1}'), + ); + when( + () => mockRepository.getStudy( + id: testId, + chapterId: const StudyChapterId('2'), + ), + ).thenAnswer( + (_) async => (studyChapter2, '{pgn 2}'), + ); + + final app = await makeTestProviderScopeApp( + tester, + home: const StudyScreen(id: testId), + overrides: [ + studyRepositoryProvider.overrideWith( + (ref) => mockRepository, + ), + ], + ); + await tester.pumpWidget(app); + await tester.pumpAndSettle(); + + expect(find.text('Chapter 1'), findsOneWidget); + expect(find.text('Chapter 2'), findsNothing); + + expect(find.text('pgn 1'), findsOneWidget); + expect(find.text('pgn 2'), findsNothing); + + // 2nd press should not have any effect, we're already at the last chapter + await tester.tap(find.text('Next chapter')); + await tester.pumpAndSettle(); + await tester.tap(find.text('Next chapter')); + await tester.pumpAndSettle(); + + expect(find.text('Chapter 1'), findsNothing); + expect(find.text('Chapter 2'), findsOneWidget); + + expect(find.text('pgn 1'), findsNothing); + expect(find.text('pgn 2'), findsOneWidget); + + // Open chapter selection dialog + await tester.tap(find.byTooltip('Chapters')); + await tester.pumpAndSettle(); + + expect( + find.descendant( + of: find.byType(ListView), + matching: find.text('Chapter 1'), + ), + findsOneWidget, + ); + expect( + find.descendant( + of: find.byType(ListView), + matching: find.text('Chapter 2'), + ), + findsOneWidget, + ); + + await tester.tap(find.text('Chapter 1')); + await tester.pumpAndSettle(); + + expect(find.text('Chapter 1'), findsOneWidget); + expect(find.text('Chapter 2'), findsNothing); + + expect(find.text('pgn 1'), findsOneWidget); + expect(find.text('pgn 2'), findsNothing); + }); + + testWidgets('Can play moves for both sides', (WidgetTester tester) async { + final mockRepository = MockStudyRepository(); + when(() => mockRepository.getStudy(id: testId)).thenAnswer( + (_) async => ( + makeStudy( + chapter: makeChapter( + id: const StudyChapterId('1'), + orientation: Side.black, + ), + ), + '' + ), + ); + + final app = await makeTestProviderScopeApp( + tester, + home: const StudyScreen(id: testId), + overrides: [ + studyRepositoryProvider.overrideWith( + (ref) => mockRepository, + ), + ], + ); + await tester.pumpWidget(app); + await tester.pumpAndSettle(); + + final boardRect = tester.getRect(find.byType(Chessboard)); + await playMove(tester, boardRect, 'e2', 'e4', orientation: Side.black); + await tester.pumpAndSettle(); + + expect(find.byKey(const Key('whitepawn-e2')), findsNothing); + expect(find.byKey(const Key('whitepawn-e4')), findsOneWidget); + + await playMove(tester, boardRect, 'e7', 'e5', orientation: Side.black); + await tester.pumpAndSettle(); + + expect(find.byKey(const Key('blackpawn-e5')), findsNothing); + expect(find.byKey(const Key('blackpawn-e7')), findsOneWidget); + + expect(find.text('1. e4'), findsOneWidget); + expect(find.text('e5'), findsOneWidget); + }); + }); +} From cc44c630d14a9ab51ac0d0ed758fccd60302c3d6 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Tue, 29 Oct 2024 10:56:07 +0100 Subject: [PATCH 614/979] fix test --- test/view/study/study_screen_test.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/view/study/study_screen_test.dart b/test/view/study/study_screen_test.dart index 9c52604889..505ad8aff1 100644 --- a/test/view/study/study_screen_test.dart +++ b/test/view/study/study_screen_test.dart @@ -221,14 +221,14 @@ void main() { await playMove(tester, boardRect, 'e2', 'e4', orientation: Side.black); await tester.pumpAndSettle(); - expect(find.byKey(const Key('whitepawn-e2')), findsNothing); - expect(find.byKey(const Key('whitepawn-e4')), findsOneWidget); + expect(find.byKey(const Key('e2-whitepawn')), findsNothing); + expect(find.byKey(const Key('e4-whitepawn')), findsOneWidget); await playMove(tester, boardRect, 'e7', 'e5', orientation: Side.black); await tester.pumpAndSettle(); - expect(find.byKey(const Key('blackpawn-e5')), findsNothing); - expect(find.byKey(const Key('blackpawn-e7')), findsOneWidget); + expect(find.byKey(const Key('e5-blackpawn')), findsOneWidget); + expect(find.byKey(const Key('e7-blackpawn')), findsNothing); expect(find.text('1. e4'), findsOneWidget); expect(find.text('e5'), findsOneWidget); From 4a466008ff856aa5d7d72078f2f200fb0581d08f Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Wed, 30 Oct 2024 23:19:54 +0100 Subject: [PATCH 615/979] add comments about pumpAndSettle() calls --- test/view/study/study_screen_test.dart | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/view/study/study_screen_test.dart b/test/view/study/study_screen_test.dart index 505ad8aff1..136994c437 100644 --- a/test/view/study/study_screen_test.dart +++ b/test/view/study/study_screen_test.dart @@ -80,6 +80,8 @@ void main() { ], ); await tester.pumpWidget(app); + + // Wait for study to load await tester.pumpAndSettle(); expect(find.text('root comment'), findsOneWidget); @@ -142,6 +144,7 @@ void main() { ], ); await tester.pumpWidget(app); + // Wait for study to load await tester.pumpAndSettle(); expect(find.text('Chapter 1'), findsOneWidget); @@ -152,8 +155,10 @@ void main() { // 2nd press should not have any effect, we're already at the last chapter await tester.tap(find.text('Next chapter')); + // Wait for next chapter to load await tester.pumpAndSettle(); await tester.tap(find.text('Next chapter')); + // Wait for next chapter to load (even though it shouldn't) await tester.pumpAndSettle(); expect(find.text('Chapter 1'), findsNothing); @@ -164,6 +169,7 @@ void main() { // Open chapter selection dialog await tester.tap(find.byTooltip('Chapters')); + // Wait for dialog to open await tester.pumpAndSettle(); expect( @@ -182,6 +188,7 @@ void main() { ); await tester.tap(find.text('Chapter 1')); + // Wait for chapter to load await tester.pumpAndSettle(); expect(find.text('Chapter 1'), findsOneWidget); @@ -215,17 +222,16 @@ void main() { ], ); await tester.pumpWidget(app); + // Wait for study to load await tester.pumpAndSettle(); final boardRect = tester.getRect(find.byType(Chessboard)); await playMove(tester, boardRect, 'e2', 'e4', orientation: Side.black); - await tester.pumpAndSettle(); expect(find.byKey(const Key('e2-whitepawn')), findsNothing); expect(find.byKey(const Key('e4-whitepawn')), findsOneWidget); await playMove(tester, boardRect, 'e7', 'e5', orientation: Side.black); - await tester.pumpAndSettle(); expect(find.byKey(const Key('e5-blackpawn')), findsOneWidget); expect(find.byKey(const Key('e7-blackpawn')), findsNothing); From 9f8a0ab5b1c40b976106c6ad4d1754e2c1e590c1 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Sat, 2 Nov 2024 10:40:57 +0100 Subject: [PATCH 616/979] use study crowdin translations --- lib/src/view/study/study_bottom_bar.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/view/study/study_bottom_bar.dart b/lib/src/view/study/study_bottom_bar.dart index 2e09847174..815e54a97d 100644 --- a/lib/src/view/study/study_bottom_bar.dart +++ b/lib/src/view/study/study_bottom_bar.dart @@ -36,7 +36,7 @@ class StudyBottomBar extends ConsumerWidget { child: BottomBarButton( key: const ValueKey('goto-previous'), onTap: onGoBack, - label: 'Previous', + label: context.l10n.studyBack, showLabel: true, icon: CupertinoIcons.chevron_back, showTooltip: false, @@ -47,7 +47,7 @@ class StudyBottomBar extends ConsumerWidget { ? ref.read(studyControllerProvider(id).notifier).nextChapter : null, icon: Icons.play_arrow, - label: 'Next chapter', + label: context.l10n.studyNextChapter, showLabel: true, blink: !state.isIntroductoryChapter && state.isAtEndOfChapter && @@ -59,7 +59,7 @@ class StudyBottomBar extends ConsumerWidget { key: const ValueKey('goto-next'), icon: CupertinoIcons.chevron_forward, onTap: onGoForward, - label: context.l10n.next, + label: context.l10n.studyNext, showLabel: true, showTooltip: false, ), From f291adbdd0902ebfd465869b0adfe30e3a737cae Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Tue, 5 Nov 2024 14:09:33 +0100 Subject: [PATCH 617/979] fix expanding variations when on mainline --- .../model/analysis/analysis_controller.dart | 6 ++- lib/src/model/study/study_controller.dart | 6 ++- test/view/analysis/analysis_screen_test.dart | 46 +++++++++++++++++++ 3 files changed, 56 insertions(+), 2 deletions(-) diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index f14ba20ebc..830225ab88 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -271,7 +271,11 @@ class AnalysisController extends _$AnalysisController @override void expandVariations(UciPath path) { final node = _root.nodeAt(path); - for (final child in node.children) { + + final childrenToShow = + _root.isOnMainline(path) ? node.children.skip(1) : node.children; + + for (final child in childrenToShow) { child.isHidden = false; for (final grandChild in child.children) { grandChild.isHidden = false; diff --git a/lib/src/model/study/study_controller.dart b/lib/src/model/study/study_controller.dart index 3edbf2f818..8f0a15f820 100644 --- a/lib/src/model/study/study_controller.dart +++ b/lib/src/model/study/study_controller.dart @@ -248,7 +248,11 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { if (!state.hasValue) return; final node = _root.nodeAt(path); - for (final child in node.children) { + + final childrenToShow = + _root.isOnMainline(path) ? node.children.skip(1) : node.children; + + for (final child in childrenToShow) { child.isHidden = false; for (final grandChild in child.children) { grandChild.isHidden = false; diff --git a/test/view/analysis/analysis_screen_test.dart b/test/view/analysis/analysis_screen_test.dart index c386d11011..156380c906 100644 --- a/test/view/analysis/analysis_screen_test.dart +++ b/test/view/analysis/analysis_screen_test.dart @@ -282,6 +282,52 @@ void main() { expect(find.text('2… Qd7'), findsNothing); }); + testWidgets( + 'Expanding one line does not expand the following one (regression test)', + (tester) async { + /// Will be rendered as: + /// ------------------- + /// 1. e4 e5 + /// |- 1... d5 2. Nf3 (2.Nc3) + /// 2. Nf3 + /// |- 2. a4 d5 (2... f5) + /// ------------------- + await buildTree( + tester, + '1. e4 e5 (1... d5 2. Nf3 (2. Nc3)) 2. Nf3 (2. a4 d5 (2... f5))', + ); + + expect(find.byIcon(Icons.add_box), findsNothing); + + // Collapse both lines + await tester.longPress(find.text('1… d5')); + await tester.pumpAndSettle(); // wait for context menu to appear + await tester.tap(find.text('Collapse variations')); + + // wait for dialog to close and tree to refresh + await tester.pumpAndSettle(const Duration(milliseconds: 200)); + + await tester.longPress(find.text('2. a4')); + await tester.pumpAndSettle(); // wait for context menu to appear + await tester.tap(find.text('Collapse variations')); + + // wait for dialog to close and tree to refresh + await tester.pumpAndSettle(const Duration(milliseconds: 200)); + + // In this state, there used to be a bug where expanding the first line would + // also expand the second line. + expect(find.byIcon(Icons.add_box), findsNWidgets(2)); + await tester.tap(find.byIcon(Icons.add_box).first); + + // need to wait for current move change debounce delay + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.add_box), findsOneWidget); + + // Second sideline should still be collapsed + expect(find.text('2. a4'), findsNothing); + }); + testWidgets('subtrees not part of the current mainline part are cached', (tester) async { await buildTree( From 23caa6324b1d1ecde0eb5eead4abc35c1c32d2bd Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Tue, 5 Nov 2024 19:50:24 +0100 Subject: [PATCH 618/979] move chapter selection to a separate screen --- lib/src/view/study/study_screen.dart | 107 ++++++++++++++++----------- 1 file changed, 62 insertions(+), 45 deletions(-) diff --git a/lib/src/view/study/study_screen.dart b/lib/src/view/study/study_screen.dart index 8df9a9dd38..9fcc32c133 100644 --- a/lib/src/view/study/study_screen.dart +++ b/lib/src/view/study/study_screen.dart @@ -2,7 +2,6 @@ import 'package:chessground/chessground.dart'; import 'package:collection/collection.dart'; import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/constants.dart'; @@ -15,6 +14,7 @@ import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/model/study/study_controller.dart'; import 'package:lichess_mobile/src/model/study/study_preferences.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; import 'package:lichess_mobile/src/view/engine/engine_lines.dart'; @@ -23,10 +23,10 @@ import 'package:lichess_mobile/src/view/study/study_settings.dart'; import 'package:lichess_mobile/src/view/study/study_tree_view.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; -import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/pgn.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; +import 'package:lichess_mobile/src/widgets/settings.dart'; import 'package:logging/logging.dart'; final _logger = Logger('StudyScreen'); @@ -96,50 +96,10 @@ class _ChapterButton extends ConsumerWidget { return state == null ? const SizedBox.shrink() : AppBarIconButton( - onPressed: () => showAdaptiveDialog( - context: context, + onPressed: () => pushPlatformRoute( + context, builder: (context) { - return SimpleDialog( - title: const Text('Chapters'), - children: [ - SizedBox( - height: MediaQuery.of(context).size.height * 0.8, - width: MediaQuery.of(context).size.width * 0.8, - child: ListView.separated( - itemBuilder: (context, index) { - final chapter = state.study.chapters[index]; - final selected = - chapter.id == state.currentChapter.id; - final checkedIcon = Theme.of(context).platform == - TargetPlatform.android - ? const Icon(Icons.check) - : Icon( - CupertinoIcons.check_mark_circled_solid, - color: - CupertinoTheme.of(context).primaryColor, - ); - return PlatformListTile( - selected: selected, - trailing: selected ? checkedIcon : null, - title: Text(chapter.name), - onTap: () { - ref - .read(studyControllerProvider(id).notifier) - .goToChapter( - chapter.id, - ); - Navigator.of(context).pop(); - }, - ); - }, - separatorBuilder: (_, __) => const PlatformDivider( - height: 1, - ), - itemCount: state.study.chapters.length, - ), - ), - ], - ); + return _StudyChaptersScreen(id: id); }, ), semanticsLabel: 'Chapters', @@ -148,6 +108,63 @@ class _ChapterButton extends ConsumerWidget { } } +class _StudyChaptersScreen extends ConsumerWidget { + const _StudyChaptersScreen({ + required this.id, + }); + + final StudyId id; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final state = ref.watch(studyControllerProvider(id)).requireValue; + + final currentChapterKey = GlobalKey(); + + // Scroll to the current chapter + WidgetsBinding.instance.addPostFrameCallback((_) { + if (currentChapterKey.currentContext != null) { + Scrollable.ensureVisible( + currentChapterKey.currentContext!, + alignment: 0.5, + ); + } + }); + + return PlatformScaffold( + appBar: const PlatformAppBar( + // TODO mobile l10n + title: Text('Chapters'), + ), + body: SafeArea( + child: ListView( + children: [ + ChoicePicker( + notchedTile: true, + choices: state.study.chapters.unlock, + selectedItem: state.study.chapters.firstWhere( + (chapter) => chapter.id == state.currentChapter.id, + ), + titleBuilder: (chapter) => Text( + chapter.name, + key: chapter.id == state.study.chapter.id + ? currentChapterKey + : null, + ), + onSelectedItemChanged: (chapter) { + ref.read(studyControllerProvider(id).notifier).goToChapter( + chapter.id, + ); + Navigator.of(context).pop(); + }, + ), + ], + ), + ), + ); + } +} + class _Body extends ConsumerWidget { const _Body({ required this.id, From f27b9771cf2cd2e5bcdc738b26b8170425a6713c Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Tue, 5 Nov 2024 19:52:00 +0100 Subject: [PATCH 619/979] Revert "Remove study tools entry for now" This reverts commit a562ae7f0ec5fbb06f154428f2ae75db96494fb0. --- lib/src/view/tools/tools_tab_screen.dart | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/src/view/tools/tools_tab_screen.dart b/lib/src/view/tools/tools_tab_screen.dart index 62cd402965..b071761fd4 100644 --- a/lib/src/view/tools/tools_tab_screen.dart +++ b/lib/src/view/tools/tools_tab_screen.dart @@ -6,6 +6,7 @@ import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/navigation.dart'; import 'package:lichess_mobile/src/network/connectivity.dart'; +import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; @@ -14,6 +15,7 @@ import 'package:lichess_mobile/src/view/board_editor/board_editor_screen.dart'; import 'package:lichess_mobile/src/view/clock/clock_screen.dart'; import 'package:lichess_mobile/src/view/coordinate_training/coordinate_training_screen.dart'; import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_screen.dart'; +import 'package:lichess_mobile/src/view/study/study_list_screen.dart'; import 'package:lichess_mobile/src/view/tools/load_position_screen.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; @@ -175,6 +177,16 @@ class _Body extends ConsumerWidget { ) : null, ), + if (isOnline) + _ToolsButton( + icon: LichessIcons.study, + title: context.l10n.studyMenu, + onTap: () => pushPlatformRoute( + context, + builder: (context) => const StudyListScreen(), + rootNavigator: true, + ), + ), _ToolsButton( icon: Icons.edit_outlined, title: context.l10n.boardEditor, From 83c3b85c57f5307b15a3d0262ac9c9335ce34ad2 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Wed, 6 Nov 2024 18:22:00 +0100 Subject: [PATCH 620/979] move study chapters to bottom sheet --- lib/src/view/study/study_screen.dart | 75 +++++++++++++++------------- 1 file changed, 39 insertions(+), 36 deletions(-) diff --git a/lib/src/view/study/study_screen.dart b/lib/src/view/study/study_screen.dart index 9fcc32c133..4163fdd492 100644 --- a/lib/src/view/study/study_screen.dart +++ b/lib/src/view/study/study_screen.dart @@ -14,7 +14,6 @@ import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/model/study/study_controller.dart'; import 'package:lichess_mobile/src/model/study/study_preferences.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; -import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; import 'package:lichess_mobile/src/view/engine/engine_lines.dart'; @@ -96,20 +95,21 @@ class _ChapterButton extends ConsumerWidget { return state == null ? const SizedBox.shrink() : AppBarIconButton( - onPressed: () => pushPlatformRoute( - context, - builder: (context) { - return _StudyChaptersScreen(id: id); - }, + onPressed: () => showAdaptiveBottomSheet( + context: context, + showDragHandle: true, + isDismissible: true, + builder: (_) => _StudyChaptersMenu(id: id), ), + // TODO mobile l10n semanticsLabel: 'Chapters', icon: const Icon(Icons.menu_book), ); } } -class _StudyChaptersScreen extends ConsumerWidget { - const _StudyChaptersScreen({ +class _StudyChaptersMenu extends ConsumerWidget { + const _StudyChaptersMenu({ required this.id, }); @@ -131,36 +131,39 @@ class _StudyChaptersScreen extends ConsumerWidget { } }); - return PlatformScaffold( - appBar: const PlatformAppBar( - // TODO mobile l10n - title: Text('Chapters'), - ), - body: SafeArea( - child: ListView( - children: [ - ChoicePicker( - notchedTile: true, - choices: state.study.chapters.unlock, - selectedItem: state.study.chapters.firstWhere( - (chapter) => chapter.id == state.currentChapter.id, - ), - titleBuilder: (chapter) => Text( - chapter.name, - key: chapter.id == state.study.chapter.id - ? currentChapterKey - : null, + return Column( + children: [ + Text( + context.l10n.studyNbChapters(state.study.chapters.length), + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 10), + Expanded( + child: ListView( + children: [ + ChoicePicker( + notchedTile: true, + choices: state.study.chapters.unlock, + selectedItem: state.study.chapters.firstWhere( + (chapter) => chapter.id == state.currentChapter.id, + ), + titleBuilder: (chapter) => Text( + chapter.name, + key: chapter.id == state.study.chapter.id + ? currentChapterKey + : null, + ), + onSelectedItemChanged: (chapter) { + ref.read(studyControllerProvider(id).notifier).goToChapter( + chapter.id, + ); + Navigator.of(context).pop(); + }, ), - onSelectedItemChanged: (chapter) { - ref.read(studyControllerProvider(id).notifier).goToChapter( - chapter.id, - ); - Navigator.of(context).pop(); - }, - ), - ], + ], + ), ), - ), + ], ); } } From 6b31b8b833f906abd31f4acc8f2591fc6ab0f617 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Wed, 6 Nov 2024 18:23:53 +0100 Subject: [PATCH 621/979] add comment about illegal positions --- lib/src/model/study/study_controller.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/src/model/study/study_controller.dart b/lib/src/model/study/study_controller.dart index 8f0a15f820..cf270194b7 100644 --- a/lib/src/model/study/study_controller.dart +++ b/lib/src/model/study/study_controller.dart @@ -68,6 +68,8 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { final variant = study.chapter.setup.variant; final orientation = study.chapter.setup.orientation; + // Some studies have illegal starting positions. This is usually the case for introductory chapters. + // We do not treat this as an error, but display a static board instead. try { _root = Root.fromPgnGame(game); } on PositionSetupException { From 347ad1d2454fdbe9ce3c6d2a221229bafa351ecd Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Wed, 6 Nov 2024 18:24:18 +0100 Subject: [PATCH 622/979] move build() to the start --- lib/src/model/study/study_controller.dart | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/src/model/study/study_controller.dart b/lib/src/model/study/study_controller.dart index cf270194b7..320faf2954 100644 --- a/lib/src/model/study/study_controller.dart +++ b/lib/src/model/study/study_controller.dart @@ -33,6 +33,17 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { Timer? _startEngineEvalTimer; + @override + Future build(StudyId id) async { + final evaluationService = ref.watch(evaluationServiceProvider); + ref.onDispose(() { + _startEngineEvalTimer?.cancel(); + _engineEvalDebounce.dispose(); + evaluationService.disposeEngine(); + }); + return _fetchChapter(id); + } + Future nextChapter() async { if (state.hasValue) { final chapters = state.requireValue.study.chapters; @@ -133,17 +144,6 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { return studyState; } - @override - Future build(StudyId id) async { - final evaluationService = ref.watch(evaluationServiceProvider); - ref.onDispose(() { - _startEngineEvalTimer?.cancel(); - _engineEvalDebounce.dispose(); - evaluationService.disposeEngine(); - }); - return _fetchChapter(id); - } - EvaluationContext _evaluationContext(Variant variant) => EvaluationContext( variant: variant, initialPosition: _root.position, From 5709c2615482a88e859c5339bf4c47f2013596ca Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Wed, 6 Nov 2024 18:25:28 +0100 Subject: [PATCH 623/979] display error instead of aborting when study fails to load --- lib/src/view/study/study_screen.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/src/view/study/study_screen.dart b/lib/src/view/study/study_screen.dart index 4163fdd492..ea4619710d 100644 --- a/lib/src/view/study/study_screen.dart +++ b/lib/src/view/study/study_screen.dart @@ -77,8 +77,9 @@ class StudyScreen extends ConsumerWidget { }, error: (error, st) { _logger.severe('Cannot load study: $error', st); - Navigator.of(context).pop(); - return const SizedBox.shrink(); + return Center( + child: Text('Cannot load study: $error'), + ); }, ); } From 8e79c66a12b1d4e8c52a4f67b7f2920f86a9ecd6 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Wed, 6 Nov 2024 18:28:03 +0100 Subject: [PATCH 624/979] use text id for chapter semanticsLabel --- lib/src/view/study/study_screen.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/view/study/study_screen.dart b/lib/src/view/study/study_screen.dart index ea4619710d..e79b602895 100644 --- a/lib/src/view/study/study_screen.dart +++ b/lib/src/view/study/study_screen.dart @@ -102,8 +102,8 @@ class _ChapterButton extends ConsumerWidget { isDismissible: true, builder: (_) => _StudyChaptersMenu(id: id), ), - // TODO mobile l10n - semanticsLabel: 'Chapters', + semanticsLabel: + context.l10n.studyNbChapters(state.study.chapters.length), icon: const Icon(Icons.menu_book), ); } From 00ef7197f2d6aa0a29297bd9656ca80f4cfa4e24 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Wed, 6 Nov 2024 18:56:30 +0100 Subject: [PATCH 625/979] fix test --- test/view/study/study_screen_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/view/study/study_screen_test.dart b/test/view/study/study_screen_test.dart index 136994c437..b5a4bd83c5 100644 --- a/test/view/study/study_screen_test.dart +++ b/test/view/study/study_screen_test.dart @@ -168,7 +168,7 @@ void main() { expect(find.text('pgn 2'), findsOneWidget); // Open chapter selection dialog - await tester.tap(find.byTooltip('Chapters')); + await tester.tap(find.byTooltip('2 Chapters')); // Wait for dialog to open await tester.pumpAndSettle(); From 946028574b2beeb46da8d820f03c1e9b4780ee26 Mon Sep 17 00:00:00 2001 From: Jimima Date: Mon, 11 Nov 2024 11:05:35 +0000 Subject: [PATCH 626/979] Changed player widget alignment --- lib/src/view/game/game_player.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/view/game/game_player.dart b/lib/src/view/game/game_player.dart index 41cafbaac8..c983789cf0 100644 --- a/lib/src/view/game/game_player.dart +++ b/lib/src/view/game/game_player.dart @@ -62,7 +62,7 @@ class GamePlayer extends StatelessWidget { children: [ if (!zenMode) Row( - mainAxisAlignment: MainAxisAlignment.start, + mainAxisAlignment: clockPosition == ClockPosition.right ? MainAxisAlignment.start : MainAxisAlignment.end, children: [ if (player.user != null) ...[ Icon( @@ -173,6 +173,7 @@ class GamePlayer extends StatelessWidget { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, + // mainAxisAlignment: clockPosition == ClockPosition.right ? MainAxisAlignment.start : MainAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.center, children: [ if (clock != null && clockPosition == ClockPosition.left) From 9221873b2856606a36396c9e90f2fa8aa7621623 Mon Sep 17 00:00:00 2001 From: Jimima Date: Mon, 11 Nov 2024 11:10:56 +0000 Subject: [PATCH 627/979] Actually fix format --- lib/src/view/game/game_player.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/src/view/game/game_player.dart b/lib/src/view/game/game_player.dart index c983789cf0..7d669a694f 100644 --- a/lib/src/view/game/game_player.dart +++ b/lib/src/view/game/game_player.dart @@ -62,7 +62,9 @@ class GamePlayer extends StatelessWidget { children: [ if (!zenMode) Row( - mainAxisAlignment: clockPosition == ClockPosition.right ? MainAxisAlignment.start : MainAxisAlignment.end, + mainAxisAlignment: clockPosition == ClockPosition.right + ? MainAxisAlignment.start + : MainAxisAlignment.end, children: [ if (player.user != null) ...[ Icon( From 6a254aaeec559cb7eeaebf93c8e2fbf6e787a436 Mon Sep 17 00:00:00 2001 From: Jimima Date: Mon, 11 Nov 2024 11:35:08 +0000 Subject: [PATCH 628/979] Fix material diff alignment --- lib/src/view/game/game_player.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/src/view/game/game_player.dart b/lib/src/view/game/game_player.dart index 7d669a694f..3e89446098 100644 --- a/lib/src/view/game/game_player.dart +++ b/lib/src/view/game/game_player.dart @@ -147,6 +147,9 @@ class GamePlayer extends StatelessWidget { MoveExpiration(timeToMove: timeToMove!, mePlaying: mePlaying) else if (materialDiff != null) Row( + mainAxisAlignment: clockPosition == ClockPosition.right + ? MainAxisAlignment.start + : MainAxisAlignment.end, children: [ for (final role in Role.values) for (int i = 0; i < materialDiff!.pieces[role]!; i++) @@ -175,7 +178,6 @@ class GamePlayer extends StatelessWidget { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, - // mainAxisAlignment: clockPosition == ClockPosition.right ? MainAxisAlignment.start : MainAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.center, children: [ if (clock != null && clockPosition == ClockPosition.left) From 595203bc4ade80ac8d85a1fc15d022b2fdae3570 Mon Sep 17 00:00:00 2001 From: Jimima Date: Mon, 11 Nov 2024 11:39:13 +0000 Subject: [PATCH 629/979] Fixed default clock side preference --- lib/src/model/settings/board_preferences.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/model/settings/board_preferences.dart b/lib/src/model/settings/board_preferences.dart index aedadf4ebc..90cdfc3270 100644 --- a/lib/src/model/settings/board_preferences.dart +++ b/lib/src/model/settings/board_preferences.dart @@ -139,7 +139,7 @@ class BoardPrefs with _$BoardPrefs implements Serializable { coordinates: true, pieceAnimation: true, showMaterialDifference: true, - clockPosition: ClockPosition.left, + clockPosition: ClockPosition.right, pieceShiftMethod: PieceShiftMethod.either, enableShapeDrawings: true, magnifyDraggedPiece: true, From 30eec0e4914f46838ef3617fe5fb44bbefbed416 Mon Sep 17 00:00:00 2001 From: Jimima Date: Mon, 11 Nov 2024 13:11:23 +0000 Subject: [PATCH 630/979] Initial implimentation --- lib/src/view/game/game_result_dialog.dart | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/lib/src/view/game/game_result_dialog.dart b/lib/src/view/game/game_result_dialog.dart index 8dc98cf67f..e520f1b709 100644 --- a/lib/src/view/game/game_result_dialog.dart +++ b/lib/src/view/game/game_result_dialog.dart @@ -133,6 +133,24 @@ class _GameEndDialogState extends ConsumerState { textAlign: TextAlign.center, ), ) + else if (gameState.game.opponent?.offeringRematch == true) + Row( + children: [ + Text("Rematch offered"), + SecondaryButton( + semanticsLabel: context.l10n.rematch, + child: Text("Accept"), + onPressed: () { + ref.read(ctrlProvider.notifier).proposeOrAcceptRematch(); + }), + SecondaryButton( + semanticsLabel: context.l10n.rematch, + child: Text("Decline"), + onPressed: () { + ref.read(ctrlProvider.notifier).declineRematch(); + }), + ], + ) else if (gameState.canOfferRematch) SecondaryButton( semanticsLabel: context.l10n.rematch, @@ -142,7 +160,6 @@ class _GameEndDialogState extends ConsumerState { ref.read(ctrlProvider.notifier).proposeOrAcceptRematch(); } : null, - glowing: gameState.game.opponent?.offeringRematch == true, child: Text( context.l10n.rematch, textAlign: TextAlign.center, From 3e0c64366f3e0e693165bb21d6f94c8a3a536161 Mon Sep 17 00:00:00 2001 From: Jimima Date: Mon, 11 Nov 2024 13:37:57 +0000 Subject: [PATCH 631/979] Alternate implimentation --- lib/src/view/game/game_result_dialog.dart | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/src/view/game/game_result_dialog.dart b/lib/src/view/game/game_result_dialog.dart index e520f1b709..ae6f8b9d62 100644 --- a/lib/src/view/game/game_result_dialog.dart +++ b/lib/src/view/game/game_result_dialog.dart @@ -135,20 +135,21 @@ class _GameEndDialogState extends ConsumerState { ) else if (gameState.game.opponent?.offeringRematch == true) Row( + mainAxisAlignment: MainAxisAlignment.center, children: [ - Text("Rematch offered"), - SecondaryButton( + // Text("Rematch offered"), + FatButton( semanticsLabel: context.l10n.rematch, - child: Text("Accept"), + child: const Text('Accept rematch'), onPressed: () { ref.read(ctrlProvider.notifier).proposeOrAcceptRematch(); - }), + },), SecondaryButton( semanticsLabel: context.l10n.rematch, - child: Text("Decline"), + child: const Text('Decline'), onPressed: () { ref.read(ctrlProvider.notifier).declineRematch(); - }), + },), ], ) else if (gameState.canOfferRematch) From d5b88bce1d23e4bd4980912c833850c4ca97cf5d Mon Sep 17 00:00:00 2001 From: Jimima Date: Mon, 11 Nov 2024 13:43:25 +0000 Subject: [PATCH 632/979] Fix formatting --- lib/src/view/game/game_result_dialog.dart | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/lib/src/view/game/game_result_dialog.dart b/lib/src/view/game/game_result_dialog.dart index ae6f8b9d62..30bd51ebb8 100644 --- a/lib/src/view/game/game_result_dialog.dart +++ b/lib/src/view/game/game_result_dialog.dart @@ -139,17 +139,19 @@ class _GameEndDialogState extends ConsumerState { children: [ // Text("Rematch offered"), FatButton( - semanticsLabel: context.l10n.rematch, - child: const Text('Accept rematch'), - onPressed: () { - ref.read(ctrlProvider.notifier).proposeOrAcceptRematch(); - },), + semanticsLabel: context.l10n.rematch, + child: const Text('Accept rematch'), + onPressed: () { + ref.read(ctrlProvider.notifier).proposeOrAcceptRematch(); + }, + ), SecondaryButton( - semanticsLabel: context.l10n.rematch, - child: const Text('Decline'), - onPressed: () { - ref.read(ctrlProvider.notifier).declineRematch(); - },), + semanticsLabel: context.l10n.rematch, + child: const Text('Decline'), + onPressed: () { + ref.read(ctrlProvider.notifier).declineRematch(); + }, + ), ], ) else if (gameState.canOfferRematch) From 7691710a3132c7457785b2553ec4a72ede02850c Mon Sep 17 00:00:00 2001 From: Jimima Date: Mon, 11 Nov 2024 14:03:50 +0000 Subject: [PATCH 633/979] WIP with settings option --- lib/src/model/game/material_diff.dart | 18 +++++++++++------- lib/src/model/settings/board_preferences.dart | 3 ++- .../view/settings/board_settings_screen.dart | 3 ++- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/lib/src/model/game/material_diff.dart b/lib/src/model/game/material_diff.dart index 122d7c7477..a661d1fa2d 100644 --- a/lib/src/model/game/material_diff.dart +++ b/lib/src/model/game/material_diff.dart @@ -43,7 +43,9 @@ class MaterialDiff with _$MaterialDiff { // TODO: parameterise starting position maybe so it can be passed in IMap subtractPieceCounts( - IMap startingCount, IMap subtractCount) { + IMap startingCount, + IMap subtractCount, + ) { IMap capturedPieces = IMap(); startingCount.forEach((role, count) { capturedPieces = @@ -103,13 +105,15 @@ class MaterialDiff with _$MaterialDiff { return MaterialDiff( black: MaterialDiffSide( - pieces: black.toIMap(), - score: -score, - capturedPieces: blackCapturedPieces), + pieces: black.toIMap(), + score: -score, + capturedPieces: blackCapturedPieces, + ), white: MaterialDiffSide( - pieces: white.toIMap(), - score: score, - capturedPieces: whiteCapturedPieces), + pieces: white.toIMap(), + score: score, + capturedPieces: whiteCapturedPieces, + ), ); } diff --git a/lib/src/model/settings/board_preferences.dart b/lib/src/model/settings/board_preferences.dart index 2a72fa8a7a..033e99871a 100644 --- a/lib/src/model/settings/board_preferences.dart +++ b/lib/src/model/settings/board_preferences.dart @@ -82,7 +82,8 @@ class BoardPreferences extends _$BoardPreferences } Future setMaterialDifferenceFormat( - MaterialDifferenceFormat materialDifferenceFormat) { + MaterialDifferenceFormat materialDifferenceFormat, + ) { return save( state.copyWith(materialDifferenceFormat: materialDifferenceFormat), ); diff --git a/lib/src/view/settings/board_settings_screen.dart b/lib/src/view/settings/board_settings_screen.dart index f550292a4f..3bcad626f8 100644 --- a/lib/src/view/settings/board_settings_screen.dart +++ b/lib/src/view/settings/board_settings_screen.dart @@ -212,7 +212,8 @@ class _Body extends ConsumerWidget { (MaterialDifferenceFormat? value) => ref .read(boardPreferencesProvider.notifier) .setMaterialDifferenceFormat( - value ?? MaterialDifferenceFormat.difference), + value ?? MaterialDifferenceFormat.difference, + ), ); } else { // pushPlatformRoute( From e461514520e1f34b5ad813ff8e8d72fcb95db1d5 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 12 Nov 2024 11:26:25 +0100 Subject: [PATCH 634/979] Fix study settings lines options --- lib/src/view/study/study_settings.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/view/study/study_settings.dart b/lib/src/view/study/study_settings.dart index 79349745e6..2b1527de6a 100644 --- a/lib/src/view/study/study_settings.dart +++ b/lib/src/view/study/study_settings.dart @@ -66,7 +66,7 @@ class StudySettings extends ConsumerWidget { ), subtitle: NonLinearSlider( value: analysisPrefs.numEvalLines, - values: const [1, 2, 3], + values: const [0, 1, 2, 3], onChangeEnd: isEngineAvailable ? (value) => ref .read(studyController.notifier) From bcf909ae2b212498b403fa1f5a9464a38741589a Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 12 Nov 2024 11:59:06 +0100 Subject: [PATCH 635/979] Some study style fixes --- lib/src/view/study/study_screen.dart | 64 ++++++++++++++-------------- lib/src/widgets/list.dart | 7 ++- pubspec.lock | 8 ++++ pubspec.yaml | 1 + 4 files changed, 45 insertions(+), 35 deletions(-) diff --git a/lib/src/view/study/study_screen.dart b/lib/src/view/study/study_screen.dart index e79b602895..3380854e23 100644 --- a/lib/src/view/study/study_screen.dart +++ b/lib/src/view/study/study_screen.dart @@ -1,3 +1,4 @@ +import 'package:auto_size_text/auto_size_text.dart'; import 'package:chessground/chessground.dart'; import 'package:collection/collection.dart'; import 'package:dartchess/dartchess.dart'; @@ -22,10 +23,10 @@ import 'package:lichess_mobile/src/view/study/study_settings.dart'; import 'package:lichess_mobile/src/view/study/study_tree_view.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/pgn.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; -import 'package:lichess_mobile/src/widgets/settings.dart'; import 'package:logging/logging.dart'; final _logger = Logger('StudyScreen'); @@ -45,9 +46,11 @@ class StudyScreen extends ConsumerWidget { data: (state) { return PlatformScaffold( appBar: PlatformAppBar( - title: FittedBox( - fit: BoxFit.scaleDown, - child: Text(state.currentChapterTitle), + title: AutoSizeText( + state.currentChapterTitle, + maxLines: 2, + minFontSize: 14, + overflow: TextOverflow.ellipsis, ), actions: [ AppBarIconButton( @@ -99,7 +102,11 @@ class _ChapterButton extends ConsumerWidget { onPressed: () => showAdaptiveBottomSheet( context: context, showDragHandle: true, + isScrollControlled: true, isDismissible: true, + constraints: BoxConstraints( + minHeight: MediaQuery.sizeOf(context).height * 0.5, + ), builder: (_) => _StudyChaptersMenu(id: id), ), semanticsLabel: @@ -132,38 +139,29 @@ class _StudyChaptersMenu extends ConsumerWidget { } }); - return Column( + return BottomSheetScrollableContainer( children: [ - Text( - context.l10n.studyNbChapters(state.study.chapters.length), - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - const SizedBox(height: 10), - Expanded( - child: ListView( - children: [ - ChoicePicker( - notchedTile: true, - choices: state.study.chapters.unlock, - selectedItem: state.study.chapters.firstWhere( - (chapter) => chapter.id == state.currentChapter.id, - ), - titleBuilder: (chapter) => Text( - chapter.name, - key: chapter.id == state.study.chapter.id - ? currentChapterKey - : null, - ), - onSelectedItemChanged: (chapter) { - ref.read(studyControllerProvider(id).notifier).goToChapter( - chapter.id, - ); - Navigator.of(context).pop(); - }, - ), - ], + Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + context.l10n.studyNbChapters(state.study.chapters.length), + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), ), + for (final chapter in state.study.chapters) + PlatformListTile( + key: chapter.id == state.currentChapter.id + ? currentChapterKey + : null, + title: Text(chapter.name, maxLines: 2), + onTap: () { + ref.read(studyControllerProvider(id).notifier).goToChapter( + chapter.id, + ); + Navigator.of(context).pop(); + }, + selected: chapter.id == state.currentChapter.id, + ), ], ); } diff --git a/lib/src/widgets/list.dart b/lib/src/widgets/list.dart index 2b93b4f15a..518876dfd5 100644 --- a/lib/src/widgets/list.dart +++ b/lib/src/widgets/list.dart @@ -278,6 +278,7 @@ class PlatformListTile extends StatelessWidget { this.cupertinoBackgroundColor, this.visualDensity, this.harmonizeCupertinoTitleStyle = false, + super.key, }); final Widget? leading; @@ -295,7 +296,6 @@ class PlatformListTile extends StatelessWidget { /// Useful on some screens where ListTiles with and without subtitle are mixed. final bool harmonizeCupertinoTitleStyle; - // only on android final bool selected; // only on android @@ -358,7 +358,10 @@ class PlatformListTile extends StatelessWidget { ) : title, subtitle: subtitle, - trailing: trailing, + trailing: trailing ?? + (selected == true + ? const Icon(CupertinoIcons.check_mark_circled_solid) + : null), additionalInfo: additionalInfo, padding: padding, onTap: onTap, diff --git a/pubspec.lock b/pubspec.lock index d08b9e90db..d4c1cea94d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -78,6 +78,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + auto_size_text: + dependency: "direct main" + description: + name: auto_size_text + sha256: "3f5261cd3fb5f2a9ab4e2fc3fba84fd9fcaac8821f20a1d4e71f557521b22599" + url: "https://pub.dev" + source: hosted + version: "3.0.0" boolean_selector: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index fbe5b83111..cdbd932c00 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,6 +12,7 @@ environment: dependencies: app_settings: ^5.1.1 async: ^2.10.0 + auto_size_text: ^3.0.0 cached_network_image: ^3.2.2 chessground: ^6.0.0 collection: ^1.17.0 From 78c3e5e80b0cf8242f190354890ebe33965e894d Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 12 Nov 2024 12:21:21 +0100 Subject: [PATCH 636/979] Tweak study list screen --- lib/src/view/study/study_list_screen.dart | 1 + lib/src/view/tools/tools_tab_screen.dart | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/view/study/study_list_screen.dart b/lib/src/view/study/study_list_screen.dart index 3c9ed65b56..ff78da2b3f 100644 --- a/lib/src/view/study/study_list_screen.dart +++ b/lib/src/view/study/study_list_screen.dart @@ -285,6 +285,7 @@ class _StudyListItem extends StatelessWidget { ), onTap: () => pushPlatformRoute( context, + rootNavigator: true, builder: (context) => StudyScreen(id: study.id), ), ); diff --git a/lib/src/view/tools/tools_tab_screen.dart b/lib/src/view/tools/tools_tab_screen.dart index b071761fd4..77765f9282 100644 --- a/lib/src/view/tools/tools_tab_screen.dart +++ b/lib/src/view/tools/tools_tab_screen.dart @@ -184,7 +184,6 @@ class _Body extends ConsumerWidget { onTap: () => pushPlatformRoute( context, builder: (context) => const StudyListScreen(), - rootNavigator: true, ), ), _ToolsButton( From ff98f1684b34742143aa83191570a2caf9176e10 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 12 Nov 2024 12:47:18 +0100 Subject: [PATCH 637/979] Fix study screen tests --- test/view/study/study_screen_test.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/view/study/study_screen_test.dart b/test/view/study/study_screen_test.dart index b5a4bd83c5..dd80b3a92c 100644 --- a/test/view/study/study_screen_test.dart +++ b/test/view/study/study_screen_test.dart @@ -174,14 +174,14 @@ void main() { expect( find.descendant( - of: find.byType(ListView), + of: find.byType(Scrollable), matching: find.text('Chapter 1'), ), findsOneWidget, ); expect( find.descendant( - of: find.byType(ListView), + of: find.byType(Scrollable), matching: find.text('Chapter 2'), ), findsOneWidget, From c75f3edf8bf3f12dfdecc0089d3691b1f7067376 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 12 Nov 2024 12:13:16 +0100 Subject: [PATCH 638/979] Remove eval from game result dialog Closes #1147 --- lib/src/view/game/game_result_dialog.dart | 126 ---------------------- 1 file changed, 126 deletions(-) diff --git a/lib/src/view/game/game_result_dialog.dart b/lib/src/view/game/game_result_dialog.dart index 8dc98cf67f..8883994b3f 100644 --- a/lib/src/view/game/game_result_dialog.dart +++ b/lib/src/view/game/game_result_dialog.dart @@ -1,18 +1,13 @@ import 'dart:async'; import 'dart:math'; -import 'package:collection/collection.dart'; import 'package:dartchess/dartchess.dart'; -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; -import 'package:lichess_mobile/src/model/analysis/server_analysis_service.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; -import 'package:lichess_mobile/src/model/common/eval.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/game/game.dart'; import 'package:lichess_mobile/src/model/game/game_controller.dart'; @@ -23,7 +18,6 @@ import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; -import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/pgn.dart'; import 'status_l10n.dart'; @@ -72,7 +66,6 @@ Widget _adaptiveDialog(BuildContext context, Widget content) { class _GameEndDialogState extends ConsumerState { late Timer _buttonActivationTimer; bool _activateButtons = false; - Future? _pendingAnalysisRequestFuture; @override void initState() { @@ -96,8 +89,6 @@ class _GameEndDialogState extends ConsumerState { Widget build(BuildContext context) { final ctrlProvider = gameControllerProvider(widget.id); final gameState = ref.watch(ctrlProvider).requireValue; - final session = ref.watch(authSessionProvider); - final currentGameAnalysis = ref.watch(currentAnalysisProvider); final content = Column( mainAxisSize: MainAxisSize.min, @@ -107,21 +98,6 @@ class _GameEndDialogState extends ConsumerState { padding: const EdgeInsets.only(bottom: 16.0), child: GameResult(game: gameState.game), ), - if (currentGameAnalysis == gameState.game.id) - const Padding( - padding: EdgeInsets.only(bottom: 16.0), - child: WaitingForServerAnalysis(), - ), - if (gameState.game.evals != null) - Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: _AcplChart(evals: gameState.game.evals!), - ), - if (gameState.game.white.analysis != null) - Padding( - padding: const EdgeInsets.only(bottom: 16.0), - child: PlayerSummary(game: gameState.game), - ), if (gameState.game.me?.offeringRematch == true) SecondaryButton( semanticsLabel: context.l10n.cancelRematchOffer, @@ -163,49 +139,6 @@ class _GameEndDialogState extends ConsumerState { textAlign: TextAlign.center, ), ), - if (currentGameAnalysis != gameState.game.id && - gameState.game.userAnalysable && - gameState.game.evals == null && - gameState.game.white.analysis == null) - FutureBuilder( - future: _pendingAnalysisRequestFuture, - builder: (context, snapshot) { - return SecondaryButton( - semanticsLabel: context.l10n.requestAComputerAnalysis, - onPressed: session == null - ? () { - showPlatformSnackbar( - context, - context.l10n.youNeedAnAccountToDoThat, - ); - } - : _activateButtons - ? snapshot.connectionState == ConnectionState.waiting - ? null - : () { - setState(() { - _pendingAnalysisRequestFuture = ref - .read(ctrlProvider.notifier) - .requestServerAnalysis() - .catchError((Object e) { - if (context.mounted) { - showPlatformSnackbar( - context, - e.toString(), - type: SnackBarType.error, - ); - } - }); - }); - } - : null, - child: Text( - context.l10n.requestAComputerAnalysis, - textAlign: TextAlign.center, - ), - ); - }, - ), if (gameState.game.userAnalysable) SecondaryButton( semanticsLabel: context.l10n.analysis, @@ -230,65 +163,6 @@ class _GameEndDialogState extends ConsumerState { } } -class _AcplChart extends StatelessWidget { - final IList evals; - - const _AcplChart({required this.evals}); - - @override - Widget build(BuildContext context) { - final mainLineColor = Theme.of(context).colorScheme.secondary; - final brightness = Theme.of(context).brightness; - final white = Theme.of(context).colorScheme.surfaceContainerHighest; - final black = Theme.of(context).colorScheme.outline; - // yes it looks like below/above are inverted in fl_chart - final belowLineColor = brightness == Brightness.light ? white : black; - final aboveLineColor = brightness == Brightness.light ? black : white; - final spots = evals - .mapIndexed( - (i, e) => FlSpot(i.toDouble(), e.winningChances(Side.white)), - ) - .toList(growable: false); - return AspectRatio( - aspectRatio: 2.5, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 2.0), - child: LineChart( - LineChartData( - minY: -1.0, - maxY: 1.0, - lineTouchData: const LineTouchData(enabled: false), - lineBarsData: [ - LineChartBarData( - spots: spots, - isCurved: true, - color: mainLineColor.withValues(alpha: 0.3), - barWidth: 1, - aboveBarData: BarAreaData( - show: true, - color: aboveLineColor, - applyCutOffY: true, - ), - belowBarData: BarAreaData( - show: true, - color: belowLineColor, - applyCutOffY: true, - ), - dotData: const FlDotData( - show: false, - ), - ), - ], - gridData: const FlGridData(show: false), - borderData: FlBorderData(show: false), - titlesData: const FlTitlesData(show: false), - ), - ), - ), - ); - } -} - class ArchivedGameResultDialog extends StatelessWidget { const ArchivedGameResultDialog({required this.game, super.key}); From 7ace51c313edff92d095b58454d121d4cd19ffe7 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Tue, 22 Oct 2024 21:36:52 +0200 Subject: [PATCH 639/979] feat: interactive studies --- lib/src/model/study/study_controller.dart | 80 +++++++- lib/src/view/study/study_bottom_bar.dart | 116 +++++++++++ lib/src/view/study/study_gamebook.dart | 174 ++++++++++++++++ lib/src/view/study/study_screen.dart | 14 +- test/view/study/study_screen_test.dart | 230 ++++++++++++++++++++++ 5 files changed, 611 insertions(+), 3 deletions(-) create mode 100644 lib/src/view/study/study_gamebook.dart diff --git a/lib/src/model/study/study_controller.dart b/lib/src/model/study/study_controller.dart index 320faf2954..1337dff19c 100644 --- a/lib/src/model/study/study_controller.dart +++ b/lib/src/model/study/study_controller.dart @@ -33,11 +33,14 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { Timer? _startEngineEvalTimer; + Timer? _opponentFirstMoveTimer; + @override Future build(StudyId id) async { final evaluationService = ref.watch(evaluationServiceProvider); ref.onDispose(() { _startEngineEvalTimer?.cancel(); + _opponentFirstMoveTimer?.cancel(); _engineEvalDebounce.dispose(); evaluationService.disposeEngine(); }); @@ -62,6 +65,7 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { chapterId: chapterId, ), ); + _ensureItsOurTurnIfGamebook(); } Future _fetchChapter( @@ -95,6 +99,7 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { pov: orientation, isLocalEvaluationAllowed: false, isLocalEvaluationEnabled: false, + gamebookActive: false, pgn: pgn, ); } @@ -119,6 +124,7 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { isLocalEvaluationAllowed: study.chapter.features.computer && !study.chapter.gamebook, isLocalEvaluationEnabled: prefs.enableLocalEvaluation, + gamebookActive: study.chapter.gamebook, pgn: pgn, ); @@ -144,6 +150,19 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { return studyState; } + // The PGNs of some gamebook studies start with the opponent's turn, so trigger their move after a delay + void _ensureItsOurTurnIfGamebook() { + _opponentFirstMoveTimer?.cancel(); + if (state.requireValue.isAtStartOfChapter && + state.requireValue.gamebookActive && + state.requireValue.gamebookComment == null && + state.requireValue.position!.turn != state.requireValue.pov) { + _opponentFirstMoveTimer = Timer(const Duration(milliseconds: 750), () { + userNext(); + }); + } + } + EvaluationContext _evaluationContext(Variant variant) => EvaluationContext( variant: variant, initialPosition: _root.position, @@ -168,6 +187,20 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { shouldForceShowVariation: true, ); } + + if (state.requireValue.gamebookActive) { + final comment = state.requireValue.gamebookComment; + // If there's no explicit comment why the move was good/bad, trigger next/previous move automatically + if (comment == null) { + Timer(const Duration(milliseconds: 750), () { + if (state.requireValue.isOnMainline) { + userNext(); + } else { + userPrevious(); + } + }); + } + } } void onPromotionSelection(Role? role) { @@ -237,6 +270,7 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { void reset() { if (state.hasValue) { _setPath(UciPath.empty); + _ensureItsOurTurnIfGamebook(); } } @@ -486,6 +520,14 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { } } +enum GamebookState { + startLesson, + findTheMove, + correctMove, + incorrectMove, + lessonComplete +} + @freezed class StudyState with _$StudyState { const StudyState._(); @@ -519,6 +561,9 @@ class StudyState with _$StudyState { /// Whether local evaluation is allowed for this study. required bool isLocalEvaluationAllowed, + /// Whether we're currently in gamebook mode, where the user has to find the right moves. + required bool gamebookActive, + /// Whether the user has enabled local evaluation. required bool isLocalEvaluationEnabled, @@ -567,6 +612,37 @@ class StudyState with _$StudyState { bool get isAtStartOfChapter => currentPath.isEmpty; + String? get gamebookComment { + final comment = + (currentNode.isRoot ? pgnRootComments : currentNode.comments) + ?.map((comment) => comment.text) + .nonNulls + .join('\n'); + return comment?.isNotEmpty == true + ? comment + : gamebookState == GamebookState.incorrectMove + ? gamebookDeviationComment + : null; + } + + String? get gamebookHint => study.hints.getOrNull(currentPath.size); + + String? get gamebookDeviationComment => + study.deviationComments.getOrNull(currentPath.size); + + GamebookState get gamebookState { + if (isAtEndOfChapter) return GamebookState.lessonComplete; + + final bool myTurn = currentNode.position!.turn == pov; + if (isAtStartOfChapter && !myTurn) return GamebookState.startLesson; + + return myTurn + ? GamebookState.findTheMove + : isOnMainline + ? GamebookState.correctMove + : GamebookState.incorrectMove; + } + bool get isIntroductoryChapter => currentNode.isRoot && currentNode.children.isEmpty; @@ -576,7 +652,9 @@ class StudyState with _$StudyState { .flattened, ); - PlayerSide get playerSide => PlayerSide.both; + PlayerSide get playerSide => gamebookActive + ? (pov == Side.white ? PlayerSide.white : PlayerSide.black) + : PlayerSide.both; } @freezed diff --git a/lib/src/view/study/study_bottom_bar.dart b/lib/src/view/study/study_bottom_bar.dart index 815e54a97d..ade01ed711 100644 --- a/lib/src/view/study/study_bottom_bar.dart +++ b/lib/src/view/study/study_bottom_bar.dart @@ -1,9 +1,12 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/study/study_controller.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; @@ -15,6 +18,25 @@ class StudyBottomBar extends ConsumerWidget { final StudyId id; + @override + Widget build(BuildContext context, WidgetRef ref) { + final gamebook = ref.watch( + studyControllerProvider(id).select( + (s) => s.requireValue.gamebookActive, + ), + ); + + return gamebook ? _GamebookBottomBar(id: id) : _AnalysisBottomBar(id: id); + } +} + +class _AnalysisBottomBar extends ConsumerWidget { + const _AnalysisBottomBar({ + required this.id, + }); + + final StudyId id; + @override Widget build(BuildContext context, WidgetRef ref) { final state = ref.watch(studyControllerProvider(id)).valueOrNull; @@ -68,3 +90,97 @@ class StudyBottomBar extends ConsumerWidget { ); } } + +class _GamebookBottomBar extends ConsumerWidget { + const _GamebookBottomBar({ + required this.id, + }); + + final StudyId id; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final state = ref.watch(studyControllerProvider(id)).requireValue; + + return BottomBar( + children: [ + ...switch (state.gamebookState) { + GamebookState.findTheMove => [ + if (!state.currentNode.isRoot) + BottomBarButton( + onTap: ref.read(studyControllerProvider(id).notifier).reset, + icon: Icons.skip_previous, + label: 'Back', + showLabel: true, + ), + BottomBarButton( + icon: Icons.help, + label: context.l10n.viewTheSolution, + showLabel: true, + onTap: ref + .read(studyControllerProvider(id).notifier) + .showGamebookSolution, + ), + ], + GamebookState.startLesson || GamebookState.correctMove => [ + BottomBarButton( + onTap: ref.read(studyControllerProvider(id).notifier).userNext, + icon: Icons.play_arrow, + label: context.l10n.studyNext, + showLabel: true, + blink: state.gamebookComment != null && + !state.isIntroductoryChapter, + ), + ], + GamebookState.incorrectMove => [ + BottomBarButton( + onTap: + ref.read(studyControllerProvider(id).notifier).userPrevious, + label: context.l10n.retry, + showLabel: true, + icon: Icons.refresh, + blink: state.gamebookComment != null, + ), + ], + GamebookState.lessonComplete => [ + if (!state.isIntroductoryChapter) + BottomBarButton( + onTap: ref.read(studyControllerProvider(id).notifier).reset, + icon: Icons.refresh, + label: context.l10n.studyPlayAgain, + showLabel: true, + ), + BottomBarButton( + onTap: state.hasNextChapter + ? ref.read(studyControllerProvider(id).notifier).nextChapter + : null, + icon: Icons.play_arrow, + label: context.l10n.studyNextChapter, + showLabel: true, + blink: !state.isIntroductoryChapter && state.hasNextChapter, + ), + if (!state.isIntroductoryChapter) + BottomBarButton( + onTap: () => pushPlatformRoute( + context, + rootNavigator: true, + builder: (context) => AnalysisScreen( + pgnOrId: state.pgn, + options: AnalysisOptions( + isLocalEvaluationAllowed: true, + variant: state.variant, + orientation: state.pov, + id: standaloneAnalysisId, + ), + ), + ), + icon: Icons.biotech, + label: context.l10n.analysis, + showLabel: true, + ), + ], + }, + ], + ); + } +} diff --git a/lib/src/view/study/study_gamebook.dart b/lib/src/view/study/study_gamebook.dart new file mode 100644 index 0000000000..76b45ef17c --- /dev/null +++ b/lib/src/view/study/study_gamebook.dart @@ -0,0 +1,174 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_linkify/flutter_linkify.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/study/study_controller.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class StudyGamebook extends ConsumerWidget { + const StudyGamebook( + this.id, + ); + + final StudyId id; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Padding( + padding: const EdgeInsets.all(5), + child: Column( + children: [ + Expanded( + child: Card( + child: Padding( + padding: const EdgeInsets.all(10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _Comment(id: id), + _Hint(id: id), + ], + ), + ), + ), + ), + ], + ), + ); + } +} + +class _Comment extends ConsumerWidget { + const _Comment({ + required this.id, + }); + + final StudyId id; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final state = ref.watch(studyControllerProvider(id)).requireValue; + + final comment = state.gamebookComment ?? + switch (state.gamebookState) { + GamebookState.findTheMove => context.l10n.studyWhatWouldYouPlay, + GamebookState.correctMove => context.l10n.studyGoodMove, + GamebookState.incorrectMove => context.l10n.puzzleNotTheMove, + GamebookState.lessonComplete => + context.l10n.studyYouCompletedThisLesson, + _ => '' + }; + + return Expanded( + child: Scrollbar( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.only(right: 5), + child: Linkify( + text: comment, + style: const TextStyle( + fontSize: 16, + ), + onOpen: (link) async { + launchUrl(Uri.parse(link.url)); + }, + ), + ), + ), + ), + ); + } +} + +class _Hint extends ConsumerStatefulWidget { + const _Hint({ + required this.id, + }); + + final StudyId id; + + @override + ConsumerState<_Hint> createState() => _HintState(); +} + +class _HintState extends ConsumerState<_Hint> { + bool showHint = false; + + @override + Widget build(BuildContext context) { + final hint = + ref.watch(studyControllerProvider(widget.id)).requireValue.gamebookHint; + return hint == null + ? const SizedBox.shrink() + : SizedBox( + height: 40, + child: showHint + ? Center(child: Text(hint)) + : TextButton( + onPressed: () { + setState(() { + showHint = true; + }); + }, + child: Text(context.l10n.getAHint), + ), + ); + } +} + +class GamebookButton extends StatelessWidget { + const GamebookButton({ + required this.icon, + required this.label, + required this.onTap, + this.highlighted = false, + super.key, + }); + + final IconData icon; + final String label; + final VoidCallback? onTap; + + final bool highlighted; + + bool get enabled => onTap != null; + + @override + Widget build(BuildContext context) { + final primary = Theme.of(context).colorScheme.primary; + + return Semantics( + container: true, + enabled: enabled, + button: true, + label: label, + excludeSemantics: true, + child: AdaptiveInkWell( + borderRadius: BorderRadius.zero, + onTap: onTap, + child: Opacity( + opacity: enabled ? 1.0 : 0.4, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon(icon, color: highlighted ? primary : null, size: 24), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Text( + label, + style: TextStyle( + fontSize: 16.0, + color: highlighted ? primary : null, + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/src/view/study/study_screen.dart b/lib/src/view/study/study_screen.dart index 3380854e23..7a1b246d6a 100644 --- a/lib/src/view/study/study_screen.dart +++ b/lib/src/view/study/study_screen.dart @@ -19,6 +19,7 @@ import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; import 'package:lichess_mobile/src/view/engine/engine_lines.dart'; import 'package:lichess_mobile/src/view/study/study_bottom_bar.dart'; +import 'package:lichess_mobile/src/view/study/study_gamebook.dart'; import 'package:lichess_mobile/src/view/study/study_settings.dart'; import 'package:lichess_mobile/src/view/study/study_tree_view.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; @@ -176,6 +177,11 @@ class _Body extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final gamebookActive = ref.watch( + studyControllerProvider(id) + .select((state) => state.requireValue.gamebookActive), + ); + return SafeArea( child: Column( children: [ @@ -229,6 +235,9 @@ class _Body extends ConsumerWidget { ) : null; + final bottomChild = + gamebookActive ? StudyGamebook(id) : StudyTreeView(id); + return landscape ? Row( mainAxisSize: MainAxisSize.max, @@ -267,7 +276,7 @@ class _Body extends ConsumerWidget { kTabletBoardTableSidePadding, ), semanticContainer: false, - child: StudyTreeView(id), + child: bottomChild, ), ), ], @@ -305,7 +314,7 @@ class _Body extends ConsumerWidget { horizontal: kTabletBoardTableSidePadding, ) : EdgeInsets.zero, - child: StudyTreeView(id), + child: bottomChild, ), ), ], @@ -384,6 +393,7 @@ class _StudyBoardState extends ConsumerState<_StudyBoard> { (prefs) => prefs.showVariationArrows, ), ) && + !studyState.gamebookActive && currentNode.children.length > 1; final pgnShapes = ISet( diff --git a/test/view/study/study_screen_test.dart b/test/view/study/study_screen_test.dart index dd80b3a92c..e0ea1937a9 100644 --- a/test/view/study/study_screen_test.dart +++ b/test/view/study/study_screen_test.dart @@ -239,5 +239,235 @@ void main() { expect(find.text('1. e4'), findsOneWidget); expect(find.text('e5'), findsOneWidget); }); + + testWidgets('Interactive study', (WidgetTester tester) async { + final mockRepository = MockStudyRepository(); + when(() => mockRepository.getStudy(id: testId)).thenAnswer( + (_) async => ( + makeStudy( + chapter: makeChapter( + id: const StudyChapterId('1'), + orientation: Side.white, + gamebook: true, + ), + ), + ''' +[Event "Improve Your Chess Calculation: Candidates| Ex 1: Hard"] +[Site "https://lichess.org/study/xgZOEizT/OfF4eLmN"] +[Result "*"] +[Variant "Standard"] +[ECO "?"] +[Opening "?"] +[Annotator "https://lichess.org/@/RushConnectedPawns"] +[FEN "r1b2rk1/3pbppp/p3p3/1p6/2qBPP2/P1N2R2/1PPQ2PP/R6K w - - 0 1"] +[SetUp "1"] +[UTCDate "2024.10.23"] +[UTCTime "02:04:11"] +[ChapterMode "gamebook"] + +{ We begin our lecture with an 'easy but not easy' example. White to play and win. } +1. Nd5!! { Brilliant! You noticed that the queen on c4 was kinda smothered. } (1. Ne2? { Not much to say after ...Qc7. }) 1... exd5 2. Rc3 Qa4 3. Rg3! { A fork, threatening Rg7 & b3. } { [%csl Gg7][%cal Gg3g7,Gd4g7,Gb2b3] } (3. Rxc8?? { Uh-oh! After Rc8, b3, there is the counter-sac Rxc2, which is winning for black!! } 3... Raxc8 4. b3 Rxc2!! 5. Qxc2 Qxd4 \$19) 3... g6 4. b3 \$18 { ...and the queen is trapped. GGs. If this was too hard for you, don't worry, there will be easier examples. } * + ''' + ), + ); + + final app = await makeTestProviderScopeApp( + tester, + home: const StudyScreen(id: testId), + overrides: [ + studyRepositoryProvider.overrideWith( + (ref) => mockRepository, + ), + ], + ); + await tester.pumpWidget(app); + // Wait for study to load + await tester.pumpAndSettle(); + + final boardRect = tester.getRect(find.byType(Chessboard)); + + const introText = + "We begin our lecture with an 'easy but not easy' example. White to play and win."; + + expect(find.text(introText), findsOneWidget); + + expect( + find.text( + 'Brilliant! You noticed that the queen on c4 was kinda smothered.', + ), + findsNothing, + ); + + // Play a wrong move + await playMove(tester, boardRect, 'c3', 'a2'); + expect(find.text("That's not the move!"), findsOneWidget); + expect(find.text(introText), findsNothing); + + // Wrong move will be taken back automatically after a short delay + await tester.pump(const Duration(seconds: 1)); + expect(find.text("That's not move!"), findsNothing); + expect(find.text(introText), findsOneWidget); + + // Play another wrong move, but this one has an explicit comment + await playMove(tester, boardRect, 'c3', 'e2'); + + // If there's an explicit comment, the move is not taken back automatically + // Verify this by waiting the same duration as above + await tester.pump(const Duration(seconds: 1)); + + expect(find.text('Not much to say after ...Qc7.'), findsOneWidget); + expect(find.text(introText), findsNothing); + + await tester.tap(find.byTooltip('Retry')); + await tester.pump(); // Wait for move to be taken back + + expect(find.text(introText), findsOneWidget); + + // Play the correct move + await playMove(tester, boardRect, 'c3', 'd5'); + + expect( + find.text( + 'Brilliant! You noticed that the queen on c4 was kinda smothered.', + ), + findsOneWidget, + ); + + // The move has an explicit feedback comment, so opponent move should not be played automatically + await tester.pump(const Duration(seconds: 1)); + + expect( + find.text( + 'Brilliant! You noticed that the queen on c4 was kinda smothered.', + ), + findsOneWidget, + ); + + await tester.tap(find.byTooltip('Next')); + await tester.pump(); // Wait for opponent move to be played + + expect( + find.text('What would you play in this position?'), + findsOneWidget, + ); + + await playMove(tester, boardRect, 'f3', 'c3'); + expect(find.text('Good move'), findsOneWidget); + + // No explicit feedback, so opponent move should be played automatically after delay + await tester.pump(const Duration(seconds: 1)); + + expect( + find.text('What would you play in this position?'), + findsOneWidget, + ); + + await playMove(tester, boardRect, 'c3', 'g3'); + expect(find.text('A fork, threatening Rg7 & b3.'), findsOneWidget); + + await tester.tap(find.byTooltip('Next')); + await tester.pump(); // Wait for opponent move to be played + + expect( + find.text('What would you play in this position?'), + findsOneWidget, + ); + + await playMove(tester, boardRect, 'b2', 'b3'); + + expect( + find.text( + "...and the queen is trapped. GGs. If this was too hard for you, don't worry, there will be easier examples.", + ), + findsOneWidget, + ); + + expect(find.byTooltip('Play again'), findsOneWidget); + expect(find.byTooltip('Next chapter'), findsOneWidget); + expect(find.byTooltip('Analysis board'), findsOneWidget); + }); + + testWidgets('Interactive study hints and deviation comments', + (WidgetTester tester) async { + final mockRepository = MockStudyRepository(); + when(() => mockRepository.getStudy(id: testId)).thenAnswer( + (_) async => ( + makeStudy( + chapter: makeChapter( + id: const StudyChapterId('1'), + orientation: Side.white, + gamebook: true, + ), + hints: [ + 'Hint 1', + null, + null, + null, + ].lock, + deviationComments: [ + null, + 'Shown if any move other than d4 is played', + null, + null, + ].lock, + ), + '1. e4 (1. d4 {Shown if d4 is played}) e5 2. Nf3' + ), + ); + + final app = await makeTestProviderScopeApp( + tester, + home: const StudyScreen(id: testId), + overrides: [ + studyRepositoryProvider.overrideWith( + (ref) => mockRepository, + ), + ], + ); + await tester.pumpWidget(app); + // Wait for study to load + await tester.pumpAndSettle(); + + expect(find.text('Get a hint'), findsOneWidget); + expect(find.text('Hint 1'), findsNothing); + + await tester.tap(find.text('Get a hint')); + await tester.pump(); // Wait for hint to be shown + expect(find.text('Hint 1'), findsOneWidget); + expect(find.text('Get a hint'), findsNothing); + + final boardRect = tester.getRect(find.byType(Chessboard)); + await playMove(tester, boardRect, 'e2', 'e3'); + expect( + find.text('Shown if any move other than d4 is played'), + findsOneWidget, + ); + await tester.tap(find.byTooltip('Retry')); + await tester.pump(); // Wait for move to be taken back + + await playMove(tester, boardRect, 'd2', 'd4'); + expect(find.text('Shown if d4 is played'), findsOneWidget); + await tester.tap(find.byTooltip('Retry')); + await tester.pump(); // Wait for move to be taken back + + expect(find.text('View the solution'), findsOneWidget); + await tester.tap(find.byTooltip('View the solution')); + // Wait for correct move and opponent's response to be played + await tester.pump(const Duration(seconds: 1)); + + expect(find.text('Get a hint'), findsNothing); + + // Play a wrong move again - generic feedback should be shown + await playMove(tester, boardRect, 'a2', 'a3'); + expect(find.text("That's not the move!"), findsOneWidget); + // Wait for wrong move to be taken back + await tester.pump(const Duration(seconds: 1)); + + expect( + find.text('What would you play in this position?'), + findsOneWidget, + ); + expect(find.text("That's not the move!"), findsNothing); + }); }); } From 9f528bf535b5a9a2867a1cd47813a0ea8c3afc8b Mon Sep 17 00:00:00 2001 From: Jimima Date: Tue, 12 Nov 2024 16:15:35 +0000 Subject: [PATCH 640/979] Version with single option with 3 choices --- lib/src/model/settings/board_preferences.dart | 40 ++++++++------ .../offline_correspondence_game_screen.dart | 10 ++-- lib/src/view/game/game_body.dart | 8 +-- lib/src/view/game/game_player.dart | 33 ++--------- lib/src/view/game/game_settings.dart | 22 ++++---- .../over_the_board/over_the_board_screen.dart | 3 +- .../view/settings/board_settings_screen.dart | 55 ++++++++----------- 7 files changed, 74 insertions(+), 97 deletions(-) diff --git a/lib/src/model/settings/board_preferences.dart b/lib/src/model/settings/board_preferences.dart index 033e99871a..6083e1073f 100644 --- a/lib/src/model/settings/board_preferences.dart +++ b/lib/src/model/settings/board_preferences.dart @@ -1,4 +1,5 @@ import 'package:chessground/chessground.dart'; +import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; @@ -75,17 +76,17 @@ class BoardPreferences extends _$BoardPreferences ); } - Future toggleShowMaterialDifference() { - return save( - state.copyWith(showMaterialDifference: !state.showMaterialDifference), - ); - } + // Future toggleShowMaterialDifference() { + // return save( + // state.copyWith(showMaterialDifference: !state.showMaterialDifference), + // ); + // } Future setMaterialDifferenceFormat( - MaterialDifferenceFormat materialDifferenceFormat, + MaterialDifference materialDifference, ) { return save( - state.copyWith(materialDifferenceFormat: materialDifferenceFormat), + state.copyWith(materialDifference: materialDifference), ); } @@ -113,8 +114,9 @@ class BoardPrefs with _$BoardPrefs implements Serializable { required bool boardHighlights, required bool coordinates, required bool pieceAnimation, - required bool showMaterialDifference, - required MaterialDifferenceFormat materialDifferenceFormat, + // required bool showMaterialDifference, + required MaterialDifference materialDifference, + // required MaterialDifferenceFormat materialDifferenceFormat, @JsonKey( defaultValue: PieceShiftMethod.either, unknownEnumValue: PieceShiftMethod.either, @@ -140,8 +142,9 @@ class BoardPrefs with _$BoardPrefs implements Serializable { boardHighlights: true, coordinates: true, pieceAnimation: true, - showMaterialDifference: true, - materialDifferenceFormat: MaterialDifferenceFormat.difference, + materialDifference: MaterialDifference.materialDifference, + // showMaterialDifference: true, + // materialDifferenceFormat: MaterialDifferenceFormat.difference, pieceShiftMethod: PieceShiftMethod.either, enableShapeDrawings: true, magnifyDraggedPiece: true, @@ -298,13 +301,16 @@ enum BoardTheme { ); } -enum MaterialDifferenceFormat { - difference(description: 'Material difference'), - pieces(description: 'Captured pieces'); +enum MaterialDifference { + materialDifference(label: 'Material difference', visible: false), + capturedPieces(label: 'Captured pieces', visible: true), + hidden(label: 'Hidden', visible: false); - const MaterialDifferenceFormat({ - required this.description, + const MaterialDifference({ + required this.label, + required this.visible, }); - final String description; + final String label; + final bool visible; } diff --git a/lib/src/view/correspondence/offline_correspondence_game_screen.dart b/lib/src/view/correspondence/offline_correspondence_game_screen.dart index e2f181ebd3..26cf810243 100644 --- a/lib/src/view/correspondence/offline_correspondence_game_screen.dart +++ b/lib/src/view/correspondence/offline_correspondence_game_screen.dart @@ -142,9 +142,9 @@ class _BodyState extends ConsumerState<_Body> { @override Widget build(BuildContext context) { - final shouldShowMaterialDiff = ref.watch( + final materialDifference = ref.watch( boardPreferencesProvider.select( - (prefs) => prefs.showMaterialDifference, + (prefs) => prefs.materialDifference, ), ); @@ -157,9 +157,10 @@ class _BodyState extends ConsumerState<_Body> { final black = GamePlayer( player: game.black, - materialDiff: shouldShowMaterialDiff + materialDiff: materialDifference.visible ? game.materialDiffAt(stepCursor, Side.black) : null, + materialDifference: materialDifference, shouldLinkToUserProfile: false, mePlaying: youAre == Side.black, confirmMoveCallbacks: youAre == Side.black && moveToConfirm != null @@ -179,9 +180,10 @@ class _BodyState extends ConsumerState<_Body> { ); final white = GamePlayer( player: game.white, - materialDiff: shouldShowMaterialDiff + materialDiff: materialDifference.visible ? game.materialDiffAt(stepCursor, Side.white) : null, + materialDifference: materialDifference, shouldLinkToUserProfile: false, mePlaying: youAre == Side.white, confirmMoveCallbacks: youAre == Side.white && moveToConfirm != null diff --git a/lib/src/view/game/game_body.dart b/lib/src/view/game/game_body.dart index 6bcc335cc3..928051b1ec 100644 --- a/lib/src/view/game/game_body.dart +++ b/lib/src/view/game/game_body.dart @@ -128,10 +128,10 @@ class GameBody extends ConsumerWidget { final black = GamePlayer( player: gameState.game.black, - materialDiff: boardPreferences.showMaterialDifference + materialDiff: boardPreferences.materialDifference.visible ? gameState.game.materialDiffAt(gameState.stepCursor, Side.black) : null, - materialDifferenceFormat: boardPreferences.materialDifferenceFormat, + materialDifference: boardPreferences.materialDifference, timeToMove: gameState.game.sideToMove == Side.black ? gameState.timeToMove : null, @@ -169,10 +169,10 @@ class GameBody extends ConsumerWidget { ); final white = GamePlayer( player: gameState.game.white, - materialDiff: boardPreferences.showMaterialDifference + materialDiff: boardPreferences.materialDifference.visible ? gameState.game.materialDiffAt(gameState.stepCursor, Side.white) : null, - materialDifferenceFormat: boardPreferences.materialDifferenceFormat, + materialDifference: boardPreferences.materialDifference, timeToMove: gameState.game.sideToMove == Side.white ? gameState.timeToMove : null, diff --git a/lib/src/view/game/game_player.dart b/lib/src/view/game/game_player.dart index 35e7c713ab..f6bf1b59a0 100644 --- a/lib/src/view/game/game_player.dart +++ b/lib/src/view/game/game_player.dart @@ -26,7 +26,7 @@ class GamePlayer extends StatelessWidget { required this.player, this.clock, this.materialDiff, - this.materialDifferenceFormat, + this.materialDifference, this.confirmMoveCallbacks, this.timeToMove, this.shouldLinkToUserProfile = true, @@ -38,7 +38,7 @@ class GamePlayer extends StatelessWidget { final Player player; final Widget? clock; final MaterialDiffSide? materialDiff; - final MaterialDifferenceFormat? materialDifferenceFormat; + final MaterialDifference? materialDifference; /// if confirm move preference is enabled, used to display confirmation buttons final ({VoidCallback confirm, VoidCallback cancel})? confirmMoveCallbacks; @@ -143,10 +143,10 @@ class GamePlayer extends StatelessWidget { ), if (timeToMove != null) MoveExpiration(timeToMove: timeToMove!, mePlaying: mePlaying) - else if (materialDiff != null) + else if (materialDiff != null && materialDifference?.visible == true) Row( children: [ - if (materialDifferenceFormat == MaterialDifferenceFormat.pieces) + if (materialDifference == MaterialDifference.capturedPieces) for (final role in Role.values) for (int i = 0; i < materialDiff!.capturedPieces[role]!; i++) Icon( @@ -154,8 +154,7 @@ class GamePlayer extends StatelessWidget { size: 13, color: Colors.grey, ), - if (materialDifferenceFormat == - MaterialDifferenceFormat.difference) + if (materialDifference == MaterialDifference.materialDifference) for (final role in Role.values) for (int i = 0; i < materialDiff!.pieces[role]!; i++) Icon( @@ -175,28 +174,6 @@ class GamePlayer extends StatelessWidget { ), ], ) - else if (materialDiff != null && false) - Row( - children: [ - for (final role in Role.values) - for (int i = 0; i < materialDiff!.pieces[role]!; i++) - Icon( - _iconByRole[role], - size: 13, - color: Colors.grey, - ), - const SizedBox(width: 3), - Text( - style: const TextStyle( - fontSize: 13, - color: Colors.grey, - ), - materialDiff != null && materialDiff!.score > 0 - ? '+${materialDiff!.score}' - : '', - ), - ], - ) else // to avoid shifts use an empty text widget const Text('', style: TextStyle(fontSize: 13)), diff --git a/lib/src/view/game/game_settings.dart b/lib/src/view/game/game_settings.dart index 3785e3333a..e2e1d616f4 100644 --- a/lib/src/view/game/game_settings.dart +++ b/lib/src/view/game/game_settings.dart @@ -109,17 +109,17 @@ class GameSettings extends ConsumerWidget { ref.read(boardPreferencesProvider.notifier).togglePieceAnimation(); }, ), - SwitchSettingTile( - title: Text( - context.l10n.preferencesMaterialDifference, - ), - value: boardPrefs.showMaterialDifference, - onChanged: (value) { - ref - .read(boardPreferencesProvider.notifier) - .toggleShowMaterialDifference(); - }, - ), + // SwitchSettingTile( + // title: Text( + // context.l10n.preferencesMaterialDifference, + // ), + // value: boardPrefs.showMaterialDifference, + // onChanged: (value) { + // ref + // .read(boardPreferencesProvider.notifier) + // .toggleShowMaterialDifference(); + // }, + // ), SwitchSettingTile( title: Text( context.l10n.toggleTheChat, diff --git a/lib/src/view/over_the_board/over_the_board_screen.dart b/lib/src/view/over_the_board/over_the_board_screen.dart index c1a7ecaaec..d563feb605 100644 --- a/lib/src/view/over_the_board/over_the_board_screen.dart +++ b/lib/src/view/over_the_board/over_the_board_screen.dart @@ -301,9 +301,10 @@ class _Player extends ConsumerWidget { name: side.name.capitalize(), ), ), - materialDiff: boardPreferences.showMaterialDifference + materialDiff: boardPreferences.materialDifference.visible ? gameState.currentMaterialDiff(side) : null, + materialDifference: boardPreferences.materialDifference, shouldLinkToUserProfile: false, clock: clock.timeIncrement.isInfinite ? null diff --git a/lib/src/view/settings/board_settings_screen.dart b/lib/src/view/settings/board_settings_screen.dart index 3bcad626f8..dc917a4950 100644 --- a/lib/src/view/settings/board_settings_screen.dart +++ b/lib/src/view/settings/board_settings_screen.dart @@ -184,44 +184,35 @@ class _Body extends ConsumerWidget { .togglePieceAnimation(); }, ), - SwitchSettingTile( - title: Text( - context.l10n.preferencesMaterialDifference, - ), - value: boardPrefs.showMaterialDifference, - onChanged: (value) { - ref - .read(boardPreferencesProvider.notifier) - .toggleShowMaterialDifference(); - }, - ), + // SwitchSettingTile( + // title: Text( + // context.l10n.preferencesMaterialDifference, + // ), + // value: boardPrefs.showMaterialDifference, + // onChanged: (value) { + // ref + // .read(boardPreferencesProvider.notifier) + // .toggleShowMaterialDifference(); + // }, + // ), SettingsListTile( settingsLabel: const Text('Material difference format'), - settingsValue: boardPrefs.materialDifferenceFormat.description, + settingsValue: boardPrefs.materialDifference.label, onTap: () { //TODO: implement different handling to android/ios // if (Theme.of(context).platform == TargetPlatform.android) { - if (true) { - showChoicePicker( - context, - choices: MaterialDifferenceFormat.values, - selectedItem: boardPrefs.materialDifferenceFormat, - labelBuilder: (t) => Text(t.description), - onSelectedItemChanged: - (MaterialDifferenceFormat? value) => ref - .read(boardPreferencesProvider.notifier) - .setMaterialDifferenceFormat( - value ?? MaterialDifferenceFormat.difference, - ), - ); - } else { - // pushPlatformRoute( - // context, - // title: 'Clock position', - // builder: (context) => const BoardClockPositionScreen(), - // ); - } + showChoicePicker( + context, + choices: MaterialDifference.values, + selectedItem: boardPrefs.materialDifference, + labelBuilder: (t) => Text(t.label), + onSelectedItemChanged: (MaterialDifference? value) => ref + .read(boardPreferencesProvider.notifier) + .setMaterialDifferenceFormat( + value ?? MaterialDifference.materialDifference, + ), + ); }, ), ], From 808d544034bdc3b73b5851fb81a3e389c8527c5f Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Sat, 9 Nov 2024 17:08:32 +0100 Subject: [PATCH 641/979] Fix landscape rendering on phones --- lib/src/view/game/game_player.dart | 4 +- .../view/puzzle/puzzle_feedback_widget.dart | 64 +++++++---- lib/src/widgets/board_table.dart | 17 +-- lib/src/widgets/countdown_clock.dart | 106 ++++++++++-------- 4 files changed, 117 insertions(+), 74 deletions(-) diff --git a/lib/src/view/game/game_player.dart b/lib/src/view/game/game_player.dart index 619f583af3..e43f6675e8 100644 --- a/lib/src/view/game/game_player.dart +++ b/lib/src/view/game/game_player.dart @@ -176,7 +176,7 @@ class GamePlayer extends StatelessWidget { Expanded( flex: 7, child: Padding( - padding: const EdgeInsets.only(right: 20), + padding: const EdgeInsets.only(right: 16.0), child: ConfirmMove( onConfirm: confirmMoveCallbacks!.confirm, onCancel: confirmMoveCallbacks!.cancel, @@ -187,7 +187,7 @@ class GamePlayer extends StatelessWidget { Expanded( flex: 7, child: Padding( - padding: const EdgeInsets.only(right: 20), + padding: const EdgeInsets.only(right: 16.0), child: shouldLinkToUserProfile ? GestureDetector( onTap: player.user != null diff --git a/lib/src/view/puzzle/puzzle_feedback_widget.dart b/lib/src/view/puzzle/puzzle_feedback_widget.dart index 51d24b4351..0b0cf7b568 100644 --- a/lib/src/view/puzzle/puzzle_feedback_widget.dart +++ b/lib/src/view/puzzle/puzzle_feedback_widget.dart @@ -52,17 +52,27 @@ class PuzzleFeedbackWidget extends ConsumerWidget { letterSpacing: 2.0, ), textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, ) : Text( state.result == PuzzleResult.win ? context.l10n.puzzlePuzzleSuccess : context.l10n.puzzlePuzzleComplete, + overflow: TextOverflow.ellipsis, ), subtitle: onStreak && state.result == PuzzleResult.lose ? null : RatingPrefAware( - orElse: Text('$playedXTimes.'), - child: Text('$puzzleRating. $playedXTimes.'), + orElse: Text( + '$playedXTimes.', + overflow: TextOverflow.ellipsis, + maxLines: 2, + ), + child: Text( + '$puzzleRating. $playedXTimes.', + overflow: TextOverflow.ellipsis, + maxLines: 2, + ), ), ); case PuzzleMode.load: @@ -74,8 +84,15 @@ class PuzzleFeedbackWidget extends ConsumerWidget { size: 36, color: context.lichessColors.error, ), - title: Text(context.l10n.puzzleNotTheMove), - subtitle: Text(context.l10n.puzzleTrySomethingElse), + title: Text( + context.l10n.puzzleNotTheMove, + overflow: TextOverflow.ellipsis, + ), + subtitle: Text( + context.l10n.puzzleTrySomethingElse, + overflow: TextOverflow.ellipsis, + maxLines: 2, + ), ); } else if (state.feedback == PuzzleFeedback.good) { return _FeedbackTile( @@ -104,11 +121,16 @@ class PuzzleFeedbackWidget extends ConsumerWidget { ), ), ), - title: Text(context.l10n.yourTurn), + title: Text( + context.l10n.yourTurn, + overflow: TextOverflow.ellipsis, + ), subtitle: Text( state.pov == Side.white ? context.l10n.puzzleFindTheBestMoveForWhite : context.l10n.puzzleFindTheBestMoveForBlack, + overflow: TextOverflow.ellipsis, + maxLines: 2, ), ); } @@ -135,23 +157,25 @@ class _FeedbackTile extends StatelessWidget { children: [ if (leading != null) ...[ leading!, - const SizedBox(width: 18), + const SizedBox(width: 16.0), ], - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - DefaultTextStyle.merge( - style: TextStyle( - fontSize: - defaultFontSize != null ? defaultFontSize * 1.2 : null, - fontWeight: FontWeight.bold, + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + DefaultTextStyle.merge( + style: TextStyle( + fontSize: + defaultFontSize != null ? defaultFontSize * 1.2 : null, + fontWeight: FontWeight.bold, + ), + child: title, ), - child: title, - ), - if (subtitle != null) subtitle!, - ], + if (subtitle != null) subtitle!, + ], + ), ), ], ); diff --git a/lib/src/widgets/board_table.dart b/lib/src/widgets/board_table.dart index 1de8bc6f19..c9cba27a51 100644 --- a/lib/src/widgets/board_table.dart +++ b/lib/src/widgets/board_table.dart @@ -237,11 +237,13 @@ class _BoardTableState extends ConsumerState { mainAxisSize: MainAxisSize.max, children: [ Padding( - padding: const EdgeInsets.only( - left: kTabletBoardTableSidePadding, - top: kTabletBoardTableSidePadding, - bottom: kTabletBoardTableSidePadding, - ), + padding: isTablet + ? const EdgeInsets.only( + left: kTabletBoardTableSidePadding, + top: kTabletBoardTableSidePadding, + bottom: kTabletBoardTableSidePadding, + ) + : EdgeInsets.zero, child: Row( children: [ boardWidget, @@ -258,8 +260,9 @@ class _BoardTableState extends ConsumerState { Flexible( fit: FlexFit.loose, child: Padding( - padding: - const EdgeInsets.all(kTabletBoardTableSidePadding), + padding: isTablet + ? const EdgeInsets.all(kTabletBoardTableSidePadding) + : EdgeInsets.zero, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisAlignment: MainAxisAlignment.spaceAround, diff --git a/lib/src/widgets/countdown_clock.dart b/lib/src/widgets/countdown_clock.dart index e562e0ce7a..cfae7ca1ce 100644 --- a/lib/src/widgets/countdown_clock.dart +++ b/lib/src/widgets/countdown_clock.dart @@ -154,6 +154,10 @@ class _CountdownClockState extends ConsumerState { } } +const _kClockFontSize = 26.0; +const _kClockTenthFontSize = 20.0; +const _kClockHundredsFontSize = 18.0; + /// A stateless widget that displays the time left on the clock. /// /// For a clock widget that automatically counts down, see [CountdownClock]. @@ -204,56 +208,68 @@ class Clock extends StatelessWidget { ? ClockStyle.darkThemeStyle : ClockStyle.lightThemeStyle); - return Container( - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(5.0)), - color: active - ? isEmergency - ? activeClockStyle.emergencyBackgroundColor - : activeClockStyle.activeBackgroundColor - : activeClockStyle.backgroundColor, - ), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 3.0, horizontal: 5.0), - child: MediaQuery.withClampedTextScaling( - maxScaleFactor: kMaxClockTextScaleFactor, - child: RichText( - text: TextSpan( - text: hours > 0 - ? '$hoursDisplay:${mins.toString().padLeft(2, '0')}:$secs' - : '$minsDisplay:$secs', - style: TextStyle( - color: active - ? isEmergency - ? activeClockStyle.emergencyTextColor - : activeClockStyle.activeTextColor - : activeClockStyle.textColor, - fontSize: 26, - height: - remainingHeight < kSmallRemainingHeightLeftBoardThreshold + return LayoutBuilder( + builder: (context, constraints) { + final maxWidth = constraints.maxWidth; + // TODO improve this + final fontScaleFactor = maxWidth < 90 ? 0.8 : 1.0; + return Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(5.0)), + color: active + ? isEmergency + ? activeClockStyle.emergencyBackgroundColor + : activeClockStyle.activeBackgroundColor + : activeClockStyle.backgroundColor, + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 3.0, horizontal: 5.0), + child: MediaQuery.withClampedTextScaling( + maxScaleFactor: kMaxClockTextScaleFactor, + child: RichText( + text: TextSpan( + text: hours > 0 + ? '$hoursDisplay:${mins.toString().padLeft(2, '0')}:$secs' + : '$minsDisplay:$secs', + style: TextStyle( + color: active + ? isEmergency + ? activeClockStyle.emergencyTextColor + : activeClockStyle.activeTextColor + : activeClockStyle.textColor, + fontSize: _kClockFontSize * fontScaleFactor, + height: remainingHeight < + kSmallRemainingHeightLeftBoardThreshold ? 1.0 : null, - fontFeatures: const [ - FontFeature.tabularFigures(), - ], - ), - children: [ - if (showTenths) - TextSpan( - text: '.${timeLeft.inMilliseconds.remainder(1000) ~/ 100}', - style: const TextStyle(fontSize: 20), + fontFeatures: const [ + FontFeature.tabularFigures(), + ], ), - if (!active && timeLeft < const Duration(seconds: 1)) - TextSpan( - text: - '${timeLeft.inMilliseconds.remainder(1000) ~/ 10 % 10}', - style: const TextStyle(fontSize: 18), - ), - ], + children: [ + if (showTenths) + TextSpan( + text: + '.${timeLeft.inMilliseconds.remainder(1000) ~/ 100}', + style: TextStyle( + fontSize: _kClockTenthFontSize * fontScaleFactor, + ), + ), + if (!active && timeLeft < const Duration(seconds: 1)) + TextSpan( + text: + '${timeLeft.inMilliseconds.remainder(1000) ~/ 10 % 10}', + style: TextStyle( + fontSize: _kClockHundredsFontSize * fontScaleFactor, + ), + ), + ], + ), + ), ), ), - ), - ), + ); + }, ); } } From 56a6d3a95cfca991f9374fb2c5f8025c11873a66 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 12 Nov 2024 19:01:14 +0100 Subject: [PATCH 642/979] Add board table layout tests --- test/test_helpers.dart | 26 +++++++- test/test_provider_scope.dart | 22 +++---- test/widgets/board_table_test.dart | 102 +++++++++++++++++++++++++++++ 3 files changed, 138 insertions(+), 12 deletions(-) create mode 100644 test/widgets/board_table_test.dart diff --git a/test/test_helpers.dart b/test/test_helpers.dart index 867f114d3c..448d85578c 100644 --- a/test/test_helpers.dart +++ b/test/test_helpers.dart @@ -10,7 +10,7 @@ import 'package:http/http.dart' as http; const double _kTestScreenWidth = 390.0; const double _kTestScreenHeight = 844.0; -/// iPhone 14 screen size as default test surface size +/// iPhone 14 screen size. const kTestSurfaceSize = Size(_kTestScreenWidth, _kTestScreenHeight); const kPlatformVariant = @@ -19,6 +19,30 @@ const kPlatformVariant = Matcher sameRequest(http.BaseRequest request) => _SameRequest(request); Matcher sameHeaders(Map headers) => _SameHeaders(headers); +/// Mocks a surface with a given size. +class TestSurface extends StatelessWidget { + const TestSurface({ + required this.child, + required this.size, + super.key, + }); + + final Size size; + final Widget child; + + @override + Widget build(BuildContext context) { + return MediaQuery( + data: MediaQueryData(size: size), + child: SizedBox( + width: size.width, + height: size.height, + child: child, + ), + ); + } +} + /// Mocks an http response Future mockResponse( String body, diff --git a/test/test_provider_scope.dart b/test/test_provider_scope.dart index c56d49ea5b..8410a60edc 100644 --- a/test/test_provider_scope.dart +++ b/test/test_provider_scope.dart @@ -107,7 +107,7 @@ Future makeOfflineTestProviderScope( /// /// The [child] widget is the widget we want to test. It will be wrapped in a /// [MediaQuery.new] widget, to simulate a device with a specific size, controlled -/// by [kTestSurfaceSize]. +/// by [surfaceSize] (which default to [kTestSurfaceSize]). /// /// The [overrides] parameter can be used to override any provider in the app. /// The [userSession] parameter can be used to set the initial user session state. @@ -118,12 +118,17 @@ Future makeTestProviderScope( List? overrides, AuthSessionState? userSession, Map? defaultPreferences, + Size surfaceSize = kTestSurfaceSize, + Key? key, }) async { final binding = TestLichessBinding.ensureInitialized(); addTearDown(binding.reset); - await tester.binding.setSurfaceSize(kTestSurfaceSize); + await tester.binding.setSurfaceSize(surfaceSize); + addTearDown(() { + tester.binding.setSurfaceSize(null); + }); VisibilityDetectorController.instance.updateInterval = Duration.zero; @@ -157,6 +162,7 @@ Future makeTestProviderScope( FlutterError.onError = _ignoreOverflowErrors; return ProviderScope( + key: key, overrides: [ // ignore: scoped_providers_should_specify_dependencies notificationDisplayProvider.overrideWith((ref) { @@ -220,15 +226,9 @@ Future makeTestProviderScope( }), ...overrides ?? [], ], - child: MediaQuery( - data: const MediaQueryData(size: kTestSurfaceSize), - child: Center( - child: SizedBox( - width: kTestSurfaceSize.width, - height: kTestSurfaceSize.height, - child: child, - ), - ), + child: TestSurface( + size: surfaceSize, + child: child, ), ); } diff --git a/test/widgets/board_table_test.dart b/test/widgets/board_table_test.dart new file mode 100644 index 0000000000..251d1ffd8e --- /dev/null +++ b/test/widgets/board_table_test.dart @@ -0,0 +1,102 @@ +import 'package:chessground/chessground.dart'; +import 'package:dartchess/dartchess.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:lichess_mobile/l10n/l10n.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_choice_picker.dart'; +import 'package:lichess_mobile/src/widgets/board_table.dart'; + +import '../test_helpers.dart'; +import '../test_provider_scope.dart'; + +const surfaces = [ + // https://www.browserstack.com/guide/common-screen-resolutions + // phones + Size(360, 800), + Size(390, 844), + Size(393, 873), + Size(412, 915), + Size(414, 896), + Size(360, 780), + // tablets + Size(600, 1024), + Size(810, 1080), + Size(820, 1180), + Size(1280, 800), + Size(800, 1280), + Size(601, 962), + // folded motorola + Size(564.7, 482.6), +]; + +void main() { + testWidgets( + 'board background size should match board size on all surfaces', + (WidgetTester tester) async { + for (final surface in surfaces) { + final app = await makeTestProviderScope( + key: ValueKey(surface), + tester, + child: const MaterialApp( + home: BoardTable( + orientation: Side.white, + fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR', + topTable: Padding( + padding: EdgeInsets.all(8.0), + child: Text('Top table'), + ), + bottomTable: Padding( + padding: EdgeInsets.all(8.0), + child: Text('Bottom table'), + ), + ), + ), + surfaceSize: surface, + ); + await tester.pumpWidget(app); + + final backgroundSize = tester.getSize( + find.byType(SolidColorChessboardBackground), + ); + + expect( + backgroundSize.width, + backgroundSize.height, + reason: 'Board background size is square', + ); + + final boardSize = tester.getSize(find.byType(Chessboard)); + + expect( + boardSize.width, + boardSize.height, + reason: 'Board size is square', + ); + + expect( + boardSize, + backgroundSize, + reason: 'Board size should match background size', + ); + + final isLandscape = surface.aspectRatio > 1.0; + final isTablet = surface.shortestSide > 600; + + final expectedBoardSize = isLandscape + ? isTablet + ? surface.height - 32.0 + : surface.height + : isTablet + ? surface.width - 32.0 + : surface.width; + + expect( + boardSize, + Size(expectedBoardSize, expectedBoardSize), + ); + } + }, + variant: kPlatformVariant, + ); +} From 6a1d288f0291dac4bf0f970a4a51a71d07fdc752 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 12 Nov 2024 19:14:37 +0100 Subject: [PATCH 643/979] Remove unused imports --- test/widgets/board_table_test.dart | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/widgets/board_table_test.dart b/test/widgets/board_table_test.dart index 251d1ffd8e..afbc518529 100644 --- a/test/widgets/board_table_test.dart +++ b/test/widgets/board_table_test.dart @@ -1,10 +1,7 @@ import 'package:chessground/chessground.dart'; import 'package:dartchess/dartchess.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:lichess_mobile/l10n/l10n.dart'; -import 'package:lichess_mobile/src/widgets/adaptive_choice_picker.dart'; import 'package:lichess_mobile/src/widgets/board_table.dart'; import '../test_helpers.dart'; From 7c3bbd32340b98b8af99ab1f3d35dc89c38608aa Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Wed, 13 Nov 2024 01:42:38 +0100 Subject: [PATCH 644/979] recreate the broadcast analysis screen that uses the new broadcast game controller --- .../broadcast/broadcast_game_controller.dart | 914 ++++++++++++++- .../broadcast_game_controller_v2.dart | 1004 ----------------- .../view/broadcast/broadcast_bottom_bar.dart | 122 ++ .../broadcast/broadcast_engine_depth.dart | 128 +++ .../broadcast_game_analysis_screen.dart | 483 +++++--- .../view/broadcast/broadcast_settings.dart | 159 +++ .../view/broadcast/broadcast_tree_view.dart | 99 ++ lib/src/view/game/game_result_dialog.dart | 1 - lib/src/widgets/pgn.dart | 2 +- 9 files changed, 1753 insertions(+), 1159 deletions(-) delete mode 100644 lib/src/model/broadcast/broadcast_game_controller_v2.dart create mode 100644 lib/src/view/broadcast/broadcast_bottom_bar.dart create mode 100644 lib/src/view/broadcast/broadcast_engine_depth.dart create mode 100644 lib/src/view/broadcast/broadcast_settings.dart create mode 100644 lib/src/view/broadcast/broadcast_tree_view.dart diff --git a/lib/src/model/broadcast/broadcast_game_controller.dart b/lib/src/model/broadcast/broadcast_game_controller.dart index 3bd96c2c11..95ef408058 100644 --- a/lib/src/model/broadcast/broadcast_game_controller.dart +++ b/lib/src/model/broadcast/broadcast_game_controller.dart @@ -1,29 +1,68 @@ import 'dart:async'; +import 'package:collection/collection.dart'; +import 'package:dartchess/dartchess.dart'; import 'package:deep_pick/deep_pick.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; +import 'package:lichess_mobile/src/model/analysis/opening_service.dart'; +import 'package:lichess_mobile/src/model/analysis/server_analysis_service.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_repository.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/common/eval.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/common/node.dart'; +import 'package:lichess_mobile/src/model/common/service/move_feedback.dart'; +import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; import 'package:lichess_mobile/src/model/common/socket.dart'; +import 'package:lichess_mobile/src/model/common/uci.dart'; +import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; +import 'package:lichess_mobile/src/model/engine/work.dart'; +import 'package:lichess_mobile/src/model/game/player.dart'; import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/network/socket.dart'; import 'package:lichess_mobile/src/utils/json.dart'; +import 'package:lichess_mobile/src/utils/rate_limit.dart'; +import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; +import 'package:lichess_mobile/src/widgets/pgn.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; +part 'broadcast_game_controller.freezed.dart'; part 'broadcast_game_controller.g.dart'; +final _dateFormat = DateFormat('yyyy.MM.dd'); + @riverpod -class BroadcastGameController extends _$BroadcastGameController { +class BroadcastGameController extends _$BroadcastGameController + implements PgnTreeNotifier { static Uri broadcastSocketUri(BroadcastRoundId broadcastRoundId) => Uri(path: 'study/$broadcastRoundId/socket/v6'); + static AnalysisOptions get options => const AnalysisOptions( + id: standaloneBroadcastId, + isLocalEvaluationAllowed: true, + orientation: Side.white, + variant: Variant.standard, + isBroadcast: true, + ); + StreamSubscription? _subscription; late SocketClient _socketClient; + late Root _root; + + final _engineEvalDebounce = Debouncer(const Duration(milliseconds: 150)); + + Timer? _startEngineEvalTimer; @override - Future build(BroadcastRoundId roundId, BroadcastGameId gameId) async { + Future build( + BroadcastRoundId roundId, + BroadcastGameId gameId, + ) async { _socketClient = ref .watch(socketPoolProvider) .open(BroadcastGameController.broadcastSocketUri(roundId)); @@ -34,10 +73,126 @@ class BroadcastGameController extends _$BroadcastGameController { _subscription?.cancel(); }); + final evaluationService = ref.watch(evaluationServiceProvider); + final serverAnalysisService = ref.watch(serverAnalysisServiceProvider); + + final isEngineAllowed = options.isLocalEvaluationAllowed && + engineSupportedVariants.contains(options.variant); + + ref.onDispose(() { + _startEngineEvalTimer?.cancel(); + _engineEvalDebounce.dispose(); + if (isEngineAllowed) { + evaluationService.disposeEngine(); + } + serverAnalysisService.lastAnalysisEvent + .removeListener(_listenToServerAnalysisEvents); + }); + + serverAnalysisService.lastAnalysisEvent + .addListener(_listenToServerAnalysisEvents); + + UciPath path = UciPath.empty; + Move? lastMove; + final pgn = await ref.withClient( (client) => BroadcastRepository(client).getGame(roundId, gameId), ); - return pgn; + + final game = PgnGame.parsePgn( + pgn, + initHeaders: () => options.isLichessGameAnalysis + ? {} + : { + 'Event': '?', + 'Site': '?', + 'Date': _dateFormat.format(DateTime.now()), + 'Round': '?', + 'White': '?', + 'Black': '?', + 'Result': '*', + 'WhiteElo': '?', + 'BlackElo': '?', + }, + ); + + final pgnHeaders = IMap(game.headers); + final rootComments = IList(game.comments.map((c) => PgnComment.fromPgn(c))); + + Future? openingFuture; + + _root = Root.fromPgnGame( + game, + isLichessAnalysis: options.isLichessGameAnalysis, + hideVariations: options.isLichessGameAnalysis, + onVisitNode: (root, branch, isMainline) { + if (isMainline && + options.initialMoveCursor != null && + branch.position.ply <= + root.position.ply + options.initialMoveCursor!) { + path = path + branch.id; + lastMove = branch.sanMove.move; + } + if (isMainline && options.opening == null && branch.position.ply <= 5) { + openingFuture = _fetchOpening(root, path); + } + }, + ); + + final currentPath = + options.initialMoveCursor == null ? _root.mainlinePath : path; + final currentNode = _root.nodeAt(currentPath); + + // wait for the opening to be fetched to recompute the branch opening + openingFuture?.then((_) { + _setPath(currentPath); + }); + + // don't use ref.watch here: we don't want to invalidate state when the + // analysis preferences change + final prefs = ref.read(analysisPreferencesProvider); + + final analysisState = BroadcastGameState( + variant: options.variant, + id: options.id, + currentPath: currentPath, + broadcastLivePath: options.isBroadcast && pgnHeaders['Result'] == '*' + ? currentPath + : null, + isOnMainline: _root.isOnMainline(currentPath), + root: _root.view, + currentNode: AnalysisCurrentNode.fromNode(currentNode), + pgnHeaders: pgnHeaders, + pgnRootComments: rootComments, + lastMove: lastMove, + pov: options.orientation, + contextOpening: options.opening, + isLocalEvaluationAllowed: options.isLocalEvaluationAllowed, + isLocalEvaluationEnabled: prefs.enableLocalEvaluation, + displayMode: DisplayMode.moves, + playersAnalysis: options.serverAnalysis, + acplChartData: + options.serverAnalysis != null ? _makeAcplChartData() : null, + clocks: options.isBroadcast ? _makeClocks(currentPath) : null, + ); + + if (analysisState.isEngineAvailable) { + evaluationService + .initEngine( + _evaluationContext, + options: EvaluationOptions( + multiPv: prefs.numEvalLines, + cores: prefs.numEngineCores, + ), + ) + .then((_) { + _startEngineEvalTimer = Timer(const Duration(milliseconds: 250), () { + _startEngineEval(); + }); + }); + } + + return analysisState; } void _handleSocketEvent(SocketEvent event) { @@ -73,14 +228,25 @@ class BroadcastGameController extends _$BroadcastGameController { final clock = pick(event.data, 'n', 'clock').asDurationFromCentiSecondsOrNull(); - final ctrlProviderNotifier = ref.read( - analysisControllerProvider( - state.requireValue, - AnalysisState.broadcastOptions, - ).notifier, - ); + final (newPath, isNewNode) = _root.addMoveAt(path, uciMove, clock: clock); - ctrlProviderNotifier.onBroadcastMove(path, uciMove, clock); + if (newPath != null) { + if (state.requireValue.broadcastLivePath == + state.requireValue.currentPath) { + _setPath( + newPath, + shouldRecomputeRootView: isNewNode, + shouldForceShowVariation: true, + isBroadcastMove: true, + ); + } else { + _root.promoteAt(newPath, toMainline: true); + state = AsyncData( + state.requireValue + .copyWith(broadcastLivePath: newPath, root: _root.view), + ); + } + } } void _handleSetTagsEvent(SocketEvent event) { @@ -90,13 +256,6 @@ class BroadcastGameController extends _$BroadcastGameController { // We check if the event is for this game if (broadcastGameId != gameId) return; - final ctrlProviderNotifier = ref.read( - analysisControllerProvider( - state.requireValue, - AnalysisState.broadcastOptions, - ).notifier, - ); - final headers = Map.fromEntries( pick(event.data, 'tags').asListOrThrow( (header) => MapEntry( @@ -107,7 +266,726 @@ class BroadcastGameController extends _$BroadcastGameController { ); for (final entry in headers.entries) { - ctrlProviderNotifier.updatePgnHeader(entry.key, entry.value); + final headers = state.requireValue.pgnHeaders.add(entry.key, entry.value); + state = AsyncData(state.requireValue.copyWith(pgnHeaders: headers)); + } + } + + EvaluationContext get _evaluationContext => EvaluationContext( + variant: options.variant, + initialPosition: _root.position, + ); + + void onUserMove(NormalMove move) { + if (!state.hasValue) return; + + if (!state.requireValue.position.isLegal(move)) return; + + if (isPromotionPawnMove(state.requireValue.position, move)) { + state = AsyncData(state.requireValue.copyWith(promotionMove: move)); + return; + } + + // For the opening explorer, last played move should always be the mainline + final shouldReplace = options.id == standaloneOpeningExplorerId; + + final (newPath, isNewNode) = _root.addMoveAt( + state.requireValue.currentPath, + move, + replace: shouldReplace, + ); + if (newPath != null) { + _setPath( + newPath, + shouldRecomputeRootView: isNewNode, + shouldForceShowVariation: true, + ); + } + } + + void onPromotionSelection(Role? role) { + if (!state.hasValue) return; + + if (role == null) { + state = AsyncData(state.requireValue.copyWith(promotionMove: null)); + return; + } + final promotionMove = state.requireValue.promotionMove; + if (promotionMove != null) { + final promotion = promotionMove.withPromotion(role); + onUserMove(promotion); + } + } + + void userNext() { + if (!state.hasValue) return; + + if (!state.requireValue.currentNode.hasChild) return; + _setPath( + state.requireValue.currentPath + + _root.nodeAt(state.requireValue.currentPath).children.first.id, + replaying: true, + ); + } + + void jumpToNthNodeOnMainline(int n) { + UciPath path = _root.mainlinePath; + while (!path.penultimate.isEmpty) { + path = path.penultimate; + } + Node? node = _root.nodeAt(path); + int count = 0; + + while (node != null && count < n) { + if (node.children.isNotEmpty) { + path = path + node.children.first.id; + node = _root.nodeAt(path); + count++; + } else { + break; + } + } + + if (node != null) { + userJump(path); + } + } + + void toggleBoard() { + if (!state.hasValue) return; + + state = AsyncData( + state.requireValue.copyWith(pov: state.requireValue.pov.opposite), + ); + } + + void userPrevious() { + _setPath(state.requireValue.currentPath.penultimate, replaying: true); + } + + @override + void userJump(UciPath path) { + _setPath(path); + } + + @override + void expandVariations(UciPath path) { + if (!state.hasValue) return; + + final node = _root.nodeAt(path); + for (final child in node.children) { + child.isHidden = false; + for (final grandChild in child.children) { + grandChild.isHidden = false; + } + } + state = AsyncData(state.requireValue.copyWith(root: _root.view)); + } + + @override + void collapseVariations(UciPath path) { + if (!state.hasValue) return; + + final node = _root.nodeAt(path); + + for (final child in node.children) { + child.isHidden = true; + } + + state = AsyncData(state.requireValue.copyWith(root: _root.view)); + } + + @override + void promoteVariation(UciPath path, bool toMainline) { + if (!state.hasValue) return; + + _root.promoteAt(path, toMainline: toMainline); + state = AsyncData( + state.requireValue.copyWith( + isOnMainline: _root.isOnMainline(state.requireValue.currentPath), + root: _root.view, + ), + ); + } + + @override + void deleteFromHere(UciPath path) { + _root.deleteAt(path); + _setPath(path.penultimate, shouldRecomputeRootView: true); + } + + Future toggleLocalEvaluation() async { + if (!state.hasValue) return; + + ref + .read(analysisPreferencesProvider.notifier) + .toggleEnableLocalEvaluation(); + + state = AsyncData( + state.requireValue.copyWith( + isLocalEvaluationEnabled: !state.requireValue.isLocalEvaluationEnabled, + ), + ); + + if (state.requireValue.isEngineAvailable) { + final prefs = ref.read(analysisPreferencesProvider); + await ref.read(evaluationServiceProvider).initEngine( + _evaluationContext, + options: EvaluationOptions( + multiPv: prefs.numEvalLines, + cores: prefs.numEngineCores, + ), + ); + _startEngineEval(); + } else { + _stopEngineEval(); + ref.read(evaluationServiceProvider).disposeEngine(); + } + } + + void setNumEvalLines(int numEvalLines) { + if (!state.hasValue) return; + + ref + .read(analysisPreferencesProvider.notifier) + .setNumEvalLines(numEvalLines); + + ref.read(evaluationServiceProvider).setOptions( + EvaluationOptions( + multiPv: numEvalLines, + cores: ref.read(analysisPreferencesProvider).numEngineCores, + ), + ); + + _root.updateAll((node) => node.eval = null); + + state = AsyncData( + state.requireValue.copyWith( + currentNode: AnalysisCurrentNode.fromNode( + _root.nodeAt(state.requireValue.currentPath), + ), + ), + ); + + _startEngineEval(); + } + + void setEngineCores(int numEngineCores) { + ref + .read(analysisPreferencesProvider.notifier) + .setEngineCores(numEngineCores); + + ref.read(evaluationServiceProvider).setOptions( + EvaluationOptions( + multiPv: ref.read(analysisPreferencesProvider).numEvalLines, + cores: numEngineCores, + ), + ); + + _startEngineEval(); + } + + void setDisplayMode(DisplayMode mode) { + if (!state.hasValue) return; + + state = AsyncData(state.requireValue.copyWith(displayMode: mode)); + } + + Future requestServerAnalysis() async { + if (!state.hasValue) return; + + if (state.requireValue.canRequestServerAnalysis) { + final service = ref.read(serverAnalysisServiceProvider); + return service.requestAnalysis( + options.id as GameAnyId, + options.orientation, + ); + } + return Future.error('Cannot request server analysis'); + } + + /// Gets the node and maybe the associated branch opening at the given path. + (Node, Opening?) _nodeOpeningAt(Node node, UciPath path, [Opening? opening]) { + if (path.isEmpty) return (node, opening); + final child = node.childById(path.head!); + if (child != null) { + return _nodeOpeningAt(child, path.tail, child.opening ?? opening); + } else { + return (node, opening); + } + } + + /// Makes a full PGN string (including headers and comments) of the current game state. + String makeExportPgn() { + if (!state.hasValue) Exception('Cannot make a PGN'); + + return _root.makePgn( + state.requireValue.pgnHeaders, + state.requireValue.pgnRootComments, + ); + } + + /// Makes a PGN string up to the current node only. + String makeCurrentNodePgn() { + if (!state.hasValue) Exception('Cannot make a PGN up to the current node'); + + final nodes = _root.branchesOn(state.requireValue.currentPath); + return nodes.map((node) => node.sanMove.san).join(' '); + } + + void _setPath( + UciPath path, { + bool shouldForceShowVariation = false, + bool shouldRecomputeRootView = false, + bool replaying = false, + bool isBroadcastMove = false, + }) { + if (!state.hasValue) return; + + final pathChange = state.requireValue.currentPath != path; + final (currentNode, opening) = _nodeOpeningAt(_root, path); + + // always show variation if the user plays a move + if (shouldForceShowVariation && + currentNode is Branch && + currentNode.isHidden) { + _root.updateAt(path, (node) { + if (node is Branch) node.isHidden = false; + }); + } + + // root view is only used to display move list, so we need to + // recompute the root view only when the nodelist length changes + // or a variation is hidden/shown + final rootView = shouldForceShowVariation || shouldRecomputeRootView + ? _root.view + : state.requireValue.root; + + final isForward = path.size > state.requireValue.currentPath.size; + if (currentNode is Branch) { + if (!replaying) { + if (isForward) { + final isCheck = currentNode.sanMove.isCheck; + if (currentNode.sanMove.isCapture) { + ref + .read(moveFeedbackServiceProvider) + .captureFeedback(check: isCheck); + } else { + ref.read(moveFeedbackServiceProvider).moveFeedback(check: isCheck); + } + } + } else if (isForward) { + final soundService = ref.read(soundServiceProvider); + if (currentNode.sanMove.isCapture) { + soundService.play(Sound.capture); + } else { + soundService.play(Sound.move); + } + } + + if (currentNode.opening == null && currentNode.position.ply <= 30) { + _fetchOpening(_root, path); + } + + state = AsyncData( + state.requireValue.copyWith( + currentPath: path, + broadcastLivePath: + isBroadcastMove ? path : state.requireValue.broadcastLivePath, + isOnMainline: _root.isOnMainline(path), + currentNode: AnalysisCurrentNode.fromNode(currentNode), + currentBranchOpening: opening, + lastMove: currentNode.sanMove.move, + promotionMove: null, + root: rootView, + clocks: options.isBroadcast ? _makeClocks(path) : null, + ), + ); + } else { + state = AsyncData( + state.requireValue.copyWith( + currentPath: path, + broadcastLivePath: + isBroadcastMove ? path : state.requireValue.broadcastLivePath, + isOnMainline: _root.isOnMainline(path), + currentNode: AnalysisCurrentNode.fromNode(currentNode), + currentBranchOpening: opening, + lastMove: null, + promotionMove: null, + root: rootView, + clocks: options.isBroadcast ? _makeClocks(path) : null, + ), + ); + } + + if (pathChange && state.requireValue.isEngineAvailable) { + _debouncedStartEngineEval(); + } + } + + Future _fetchOpening(Node fromNode, UciPath path) async { + if (!state.hasValue) return; + if (!kOpeningAllowedVariants.contains(options.variant)) return; + + final moves = fromNode.branchesOn(path).map((node) => node.sanMove.move); + if (moves.isEmpty) return; + if (moves.length > 40) return; + + final opening = + await ref.read(openingServiceProvider).fetchFromMoves(moves); + + if (opening != null) { + fromNode.updateAt(path, (node) => node.opening = opening); + + if (state.requireValue.currentPath == path) { + state = AsyncData( + state.requireValue.copyWith( + currentNode: AnalysisCurrentNode.fromNode(fromNode.nodeAt(path)), + ), + ); + } + } + } + + void _startEngineEval() { + if (!state.hasValue) return; + + if (!state.requireValue.isEngineAvailable) return; + ref + .read(evaluationServiceProvider) + .start( + state.requireValue.currentPath, + _root.branchesOn(state.requireValue.currentPath).map(Step.fromNode), + initialPositionEval: _root.eval, + shouldEmit: (work) => work.path == state.requireValue.currentPath, + ) + ?.forEach( + (t) => _root.updateAt(t.$1.path, (node) => node.eval = t.$2), + ); + } + + void _debouncedStartEngineEval() { + _engineEvalDebounce(() { + _startEngineEval(); + }); + } + + void _stopEngineEval() { + if (!state.hasValue) Exception('Cannot export PGN'); + + ref.read(evaluationServiceProvider).stop(); + // update the current node with last cached eval + state = AsyncData( + state.requireValue.copyWith( + currentNode: AnalysisCurrentNode.fromNode( + _root.nodeAt(state.requireValue.currentPath), + ), + ), + ); + } + + void _listenToServerAnalysisEvents() { + if (!state.hasValue) Exception('Cannot export PGN'); + + final event = + ref.read(serverAnalysisServiceProvider).lastAnalysisEvent.value; + if (event != null && event.$1 == state.requireValue.id) { + _mergeOngoingAnalysis(_root, event.$2.tree); + state = AsyncData( + state.requireValue.copyWith( + acplChartData: _makeAcplChartData(), + playersAnalysis: event.$2.analysis != null + ? ( + white: event.$2.analysis!.white, + black: event.$2.analysis!.black + ) + : null, + root: _root.view, + ), + ); + } + } + + void _mergeOngoingAnalysis(Node n1, Map n2) { + final eval = n2['eval'] as Map?; + final cp = eval?['cp'] as int?; + final mate = eval?['mate'] as int?; + final pgnEval = cp != null + ? PgnEvaluation.pawns(pawns: cpToPawns(cp)) + : mate != null + ? PgnEvaluation.mate(mate: mate) + : null; + final glyphs = n2['glyphs'] as List?; + final glyph = glyphs?.first as Map?; + final comments = n2['comments'] as List?; + final comment = + (comments?.first as Map?)?['text'] as String?; + final children = n2['children'] as List? ?? []; + final pgnComment = + pgnEval != null ? PgnComment(eval: pgnEval, text: comment) : null; + if (n1 is Branch) { + if (pgnComment != null) { + if (n1.lichessAnalysisComments == null) { + n1.lichessAnalysisComments = [pgnComment]; + } else { + n1.lichessAnalysisComments!.removeWhere((c) => c.eval != null); + n1.lichessAnalysisComments!.add(pgnComment); + } + } + if (glyph != null) { + n1.nags ??= [glyph['id'] as int]; + } + } + for (final c in children) { + final n2child = c as Map; + final id = n2child['id'] as String; + final n1child = n1.childById(UciCharPair.fromStringId(id)); + if (n1child != null) { + _mergeOngoingAnalysis(n1child, n2child); + } else { + final uci = n2child['uci'] as String; + final san = n2child['san'] as String; + final move = Move.parse(uci)!; + n1.addChild( + Branch( + position: n1.position.playUnchecked(move), + sanMove: SanMove(san, move), + isHidden: children.length > 1, + ), + ); + } + } + } + + IList? _makeAcplChartData() { + if (!_root.mainline.any((node) => node.lichessAnalysisComments != null)) { + return null; + } + final list = _root.mainline + .map( + (node) => ( + node.position.isCheckmate, + node.position.turn, + node.lichessAnalysisComments + ?.firstWhereOrNull((c) => c.eval != null) + ?.eval + ), + ) + .map( + (el) { + final (isCheckmate, side, eval) = el; + return eval != null + ? ExternalEval( + cp: eval.pawns != null ? cpFromPawns(eval.pawns!) : null, + mate: eval.mate, + depth: eval.depth, + ) + : ExternalEval( + cp: null, + // hack to display checkmate as the max eval + mate: isCheckmate + ? side == Side.white + ? -1 + : 1 + : null, + ); + }, + ).toList(growable: false); + return list.isEmpty ? null : IList(list); + } + + ({Duration? parentClock, Duration? clock}) _makeClocks(UciPath path) { + final nodeView = _root.nodeAt(path).view; + final parentView = _root.parentAt(path).view; + + return ( + parentClock: (parentView is ViewBranch) ? parentView.clock : null, + clock: (nodeView is ViewBranch) ? nodeView.clock : null, + ); + } +} + +enum DisplayMode { + moves, + summary, +} + +@freezed +class BroadcastGameState with _$BroadcastGameState { + const BroadcastGameState._(); + + const factory BroadcastGameState({ + /// Analysis ID + required StringId id, + + /// The variant of the analysis. + required Variant variant, + + /// Immutable view of the whole tree + required ViewRoot root, + + /// The current node in the analysis view. + /// + /// This is an immutable copy of the actual [Node] at the `currentPath`. + /// We don't want to use [Node.view] here because it'd copy the whole tree + /// under the current node and it's expensive. + required AnalysisCurrentNode currentNode, + + /// The path to the current node in the analysis view. + required UciPath currentPath, + + // The path to the current broadcast live move. + required UciPath? broadcastLivePath, + + /// Whether the current path is on the mainline. + required bool isOnMainline, + + /// The side to display the board from. + required Side pov, + + /// Whether local evaluation is allowed for this analysis. + required bool isLocalEvaluationAllowed, + + /// Whether the user has enabled local evaluation. + required bool isLocalEvaluationEnabled, + + /// The display mode of the analysis. + /// + /// It can be either moves, summary or opening explorer. + required DisplayMode displayMode, + + /// Clocks if avaible. Only used by the broadcast analysis screen. + ({Duration? parentClock, Duration? clock})? clocks, + + /// The last move played. + Move? lastMove, + + /// Possible promotion move to be played. + NormalMove? promotionMove, + + /// Opening of the analysis context (from lichess archived games). + Opening? contextOpening, + + /// The opening of the current branch. + Opening? currentBranchOpening, + + /// Optional server analysis to display player stats. + ({PlayerAnalysis white, PlayerAnalysis black})? playersAnalysis, + + /// Optional ACPL chart data of the game, coming from lichess server analysis. + IList? acplChartData, + + /// The PGN headers of the game. + required IMap pgnHeaders, + + /// The PGN comments of the game. + /// + /// This field is only used with user submitted PGNS. + IList? pgnRootComments, + }) = _AnalysisState; + + IMap> get validMoves => makeLegalMoves( + currentNode.position, + isChess960: variant == Variant.chess960, + ); + + /// Whether the user can request server analysis. + /// + /// It must be a lichess game, which is finished and not already analyzed. + bool get canRequestServerAnalysis => false; + + bool get canShowGameSummary => hasServerAnalysis || canRequestServerAnalysis; + + bool get hasServerAnalysis => playersAnalysis != null; + + /// Whether an evaluation can be available + bool get hasAvailableEval => + isEngineAvailable || + (isLocalEvaluationAllowed && + acplChartData != null && + acplChartData!.isNotEmpty); + + /// Whether the engine is allowed for this analysis and variant. + bool get isEngineAllowed => + isLocalEvaluationAllowed && engineSupportedVariants.contains(variant); + + /// Whether the engine is available for evaluation + bool get isEngineAvailable => isEngineAllowed && isLocalEvaluationEnabled; + + Position get position => currentNode.position; + bool get canGoNext => currentNode.hasChild; + bool get canGoBack => currentPath.size > UciPath.empty.size; + + EngineGaugeParams get engineGaugeParams => ( + orientation: pov, + isLocalEngineAvailable: isEngineAvailable, + position: position, + savedEval: currentNode.eval ?? currentNode.serverEval, + ); + + AnalysisOptions get openingExplorerOptions => AnalysisOptions( + id: standaloneOpeningExplorerId, + isLocalEvaluationAllowed: false, + orientation: pov, + variant: variant, + initialMoveCursor: currentPath.size, + ); +} + +@freezed +class AnalysisCurrentNode with _$AnalysisCurrentNode { + const AnalysisCurrentNode._(); + + const factory AnalysisCurrentNode({ + required Position position, + required bool hasChild, + required bool isRoot, + SanMove? sanMove, + Opening? opening, + ClientEval? eval, + IList? lichessAnalysisComments, + IList? startingComments, + IList? comments, + IList? nags, + }) = _AnalysisCurrentNode; + + factory AnalysisCurrentNode.fromNode(Node node) { + if (node is Branch) { + return AnalysisCurrentNode( + sanMove: node.sanMove, + position: node.position, + isRoot: node is Root, + hasChild: node.children.isNotEmpty, + opening: node.opening, + eval: node.eval, + lichessAnalysisComments: IList(node.lichessAnalysisComments), + startingComments: IList(node.startingComments), + comments: IList(node.comments), + nags: IList(node.nags), + ); + } else { + return AnalysisCurrentNode( + position: node.position, + hasChild: node.children.isNotEmpty, + isRoot: node is Root, + opening: node.opening, + eval: node.eval, + ); } } + + /// The evaluation from the PGN comments. + /// + /// For now we only trust the eval coming from lichess analysis. + ExternalEval? get serverEval { + final pgnEval = + lichessAnalysisComments?.firstWhereOrNull((c) => c.eval != null)?.eval; + return pgnEval != null + ? ExternalEval( + cp: pgnEval.pawns != null ? cpFromPawns(pgnEval.pawns!) : null, + mate: pgnEval.mate, + depth: pgnEval.depth, + ) + : null; + } } diff --git a/lib/src/model/broadcast/broadcast_game_controller_v2.dart b/lib/src/model/broadcast/broadcast_game_controller_v2.dart deleted file mode 100644 index 7e733dd953..0000000000 --- a/lib/src/model/broadcast/broadcast_game_controller_v2.dart +++ /dev/null @@ -1,1004 +0,0 @@ -import 'dart:async'; - -import 'package:collection/collection.dart'; -import 'package:dartchess/dartchess.dart'; -import 'package:deep_pick/deep_pick.dart'; -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:intl/intl.dart'; -import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; -import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; -import 'package:lichess_mobile/src/model/analysis/opening_service.dart'; -import 'package:lichess_mobile/src/model/analysis/server_analysis_service.dart'; -import 'package:lichess_mobile/src/model/broadcast/broadcast_repository.dart'; -import 'package:lichess_mobile/src/model/common/chess.dart'; -import 'package:lichess_mobile/src/model/common/eval.dart'; -import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/model/common/node.dart'; -import 'package:lichess_mobile/src/model/common/service/move_feedback.dart'; -import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; -import 'package:lichess_mobile/src/model/common/socket.dart'; -import 'package:lichess_mobile/src/model/common/uci.dart'; -import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; -import 'package:lichess_mobile/src/model/engine/work.dart'; -import 'package:lichess_mobile/src/model/game/player.dart'; -import 'package:lichess_mobile/src/network/http.dart'; -import 'package:lichess_mobile/src/network/socket.dart'; -import 'package:lichess_mobile/src/utils/json.dart'; -import 'package:lichess_mobile/src/utils/rate_limit.dart'; -import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; -import 'package:lichess_mobile/src/widgets/pgn.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'broadcast_game_controller_v2.freezed.dart'; -part 'broadcast_game_controller_v2.g.dart'; - -final _dateFormat = DateFormat('yyyy.MM.dd'); - -/// Whether the analysis is a standalone analysis (not a lichess game analysis). -bool _isStandaloneAnalysis(StringId id) => - id == standaloneAnalysisId || - id == standaloneOpeningExplorerId || - id == standaloneBroadcastId; - -@riverpod -class BroadcastGameController extends _$BroadcastGameController - implements PgnTreeNotifier { - static Uri broadcastSocketUri(BroadcastRoundId broadcastRoundId) => - Uri(path: 'study/$broadcastRoundId/socket/v6'); - - StreamSubscription? _subscription; - - late SocketClient _socketClient; - late Root _root; - - final _engineEvalDebounce = Debouncer(const Duration(milliseconds: 150)); - - Timer? _startEngineEvalTimer; - - @override - Future build( - AnalysisOptions options, - BroadcastRoundId roundId, - BroadcastGameId gameId, - ) async { - _socketClient = ref - .watch(socketPoolProvider) - .open(BroadcastGameController.broadcastSocketUri(roundId)); - - _subscription = _socketClient.stream.listen(_handleSocketEvent); - - ref.onDispose(() { - _subscription?.cancel(); - }); - - final evaluationService = ref.watch(evaluationServiceProvider); - final serverAnalysisService = ref.watch(serverAnalysisServiceProvider); - - final isEngineAllowed = options.isLocalEvaluationAllowed && - engineSupportedVariants.contains(options.variant); - - ref.onDispose(() { - _startEngineEvalTimer?.cancel(); - _engineEvalDebounce.dispose(); - if (isEngineAllowed) { - evaluationService.disposeEngine(); - } - serverAnalysisService.lastAnalysisEvent - .removeListener(_listenToServerAnalysisEvents); - }); - - serverAnalysisService.lastAnalysisEvent - .addListener(_listenToServerAnalysisEvents); - - UciPath path = UciPath.empty; - Move? lastMove; - - final pgn = await ref.withClient( - (client) => BroadcastRepository(client).getGame(roundId, gameId), - ); - - final game = PgnGame.parsePgn( - pgn, - initHeaders: () => options.isLichessGameAnalysis - ? {} - : { - 'Event': '?', - 'Site': '?', - 'Date': _dateFormat.format(DateTime.now()), - 'Round': '?', - 'White': '?', - 'Black': '?', - 'Result': '*', - 'WhiteElo': '?', - 'BlackElo': '?', - }, - ); - - final pgnHeaders = IMap(game.headers); - final rootComments = IList(game.comments.map((c) => PgnComment.fromPgn(c))); - - Future? openingFuture; - - _root = Root.fromPgnGame( - game, - isLichessAnalysis: options.isLichessGameAnalysis, - hideVariations: options.isLichessGameAnalysis, - onVisitNode: (root, branch, isMainline) { - if (isMainline && - options.initialMoveCursor != null && - branch.position.ply <= - root.position.ply + options.initialMoveCursor!) { - path = path + branch.id; - lastMove = branch.sanMove.move; - } - if (isMainline && options.opening == null && branch.position.ply <= 5) { - openingFuture = _fetchOpening(root, path); - } - }, - ); - - final currentPath = - options.initialMoveCursor == null ? _root.mainlinePath : path; - final currentNode = _root.nodeAt(currentPath); - - // wait for the opening to be fetched to recompute the branch opening - openingFuture?.then((_) { - _setPath(currentPath); - }); - - // don't use ref.watch here: we don't want to invalidate state when the - // analysis preferences change - final prefs = ref.read(analysisPreferencesProvider); - - final analysisState = BroadcastGameState( - variant: options.variant, - id: options.id, - currentPath: currentPath, - broadcastLivePath: options.isBroadcast && pgnHeaders['Result'] == '*' - ? currentPath - : null, - isOnMainline: _root.isOnMainline(currentPath), - root: _root.view, - currentNode: AnalysisCurrentNode.fromNode(currentNode), - pgnHeaders: pgnHeaders, - pgnRootComments: rootComments, - lastMove: lastMove, - pov: options.orientation, - contextOpening: options.opening, - isLocalEvaluationAllowed: options.isLocalEvaluationAllowed, - isLocalEvaluationEnabled: prefs.enableLocalEvaluation, - displayMode: DisplayMode.moves, - playersAnalysis: options.serverAnalysis, - acplChartData: - options.serverAnalysis != null ? _makeAcplChartData() : null, - clocks: options.isBroadcast ? _makeClocks(currentPath) : null, - ); - - if (analysisState.isEngineAvailable) { - evaluationService - .initEngine( - _evaluationContext, - options: EvaluationOptions( - multiPv: prefs.numEvalLines, - cores: prefs.numEngineCores, - ), - ) - .then((_) { - _startEngineEvalTimer = Timer(const Duration(milliseconds: 250), () { - _startEngineEval(); - }); - }); - } - - return analysisState; - } - - void _handleSocketEvent(SocketEvent event) { - if (!state.hasValue) return; - - switch (event.topic) { - // Sent when a node is recevied from the broadcast - case 'addNode': - _handleAddNodeEvent(event); - // Sent when a pgn tag changes - case 'setTags': - _handleSetTagsEvent(event); - } - } - - void _handleAddNodeEvent(SocketEvent event) { - final broadcastGameId = - pick(event.data, 'p', 'chapterId').asBroadcastGameIdOrThrow(); - - // We check if the event is for this game - if (broadcastGameId != gameId) return; - - // The path of the last and current move of the broadcasted game - // Its value is "!" if the path is identical to one of the node that was received - final currentPath = pick(event.data, 'relayPath').asUciPathOrThrow(); - - // We check that the event we received is for the last move of the game - if (currentPath.value != '!') return; - - // The path for the node that was received - final path = pick(event.data, 'p', 'path').asUciPathOrThrow(); - final uciMove = pick(event.data, 'n', 'uci').asUciMoveOrThrow(); - final clock = - pick(event.data, 'n', 'clock').asDurationFromCentiSecondsOrNull(); - - final (newPath, isNewNode) = _root.addMoveAt(path, uciMove, clock: clock); - - if (newPath != null) { - if (state.requireValue.broadcastLivePath == - state.requireValue.currentPath) { - _setPath( - newPath, - shouldRecomputeRootView: isNewNode, - shouldForceShowVariation: true, - isBroadcastMove: true, - ); - } else { - _root.promoteAt(newPath, toMainline: true); - state = AsyncData( - state.requireValue - .copyWith(broadcastLivePath: newPath, root: _root.view), - ); - } - } - } - - void _handleSetTagsEvent(SocketEvent event) { - final broadcastGameId = - pick(event.data, 'chapterId').asBroadcastGameIdOrThrow(); - - // We check if the event is for this game - if (broadcastGameId != gameId) return; - - final headers = Map.fromEntries( - pick(event.data, 'tags').asListOrThrow( - (header) => MapEntry( - header(0).asStringOrThrow(), - header(1).asStringOrThrow(), - ), - ), - ); - - for (final entry in headers.entries) { - final headers = state.requireValue.pgnHeaders.add(entry.key, entry.value); - state = AsyncData(state.requireValue.copyWith(pgnHeaders: headers)); - } - } - - EvaluationContext get _evaluationContext => EvaluationContext( - variant: options.variant, - initialPosition: _root.position, - ); - - void onUserMove(NormalMove move) { - if (!state.hasValue) return; - - if (!state.requireValue.position.isLegal(move)) return; - - if (isPromotionPawnMove(state.requireValue.position, move)) { - state = AsyncData(state.requireValue.copyWith(promotionMove: move)); - return; - } - - // For the opening explorer, last played move should always be the mainline - final shouldReplace = options.id == standaloneOpeningExplorerId; - - final (newPath, isNewNode) = _root.addMoveAt( - state.requireValue.currentPath, - move, - replace: shouldReplace, - ); - if (newPath != null) { - _setPath( - newPath, - shouldRecomputeRootView: isNewNode, - shouldForceShowVariation: true, - ); - } - } - - void onPromotionSelection(Role? role) { - if (!state.hasValue) return; - - if (role == null) { - state = AsyncData(state.requireValue.copyWith(promotionMove: null)); - return; - } - final promotionMove = state.requireValue.promotionMove; - if (promotionMove != null) { - final promotion = promotionMove.withPromotion(role); - onUserMove(promotion); - } - } - - void userNext() { - if (!state.hasValue) return; - - if (!state.requireValue.currentNode.hasChild) return; - _setPath( - state.requireValue.currentPath + - _root.nodeAt(state.requireValue.currentPath).children.first.id, - replaying: true, - ); - } - - void jumpToNthNodeOnMainline(int n) { - UciPath path = _root.mainlinePath; - while (!path.penultimate.isEmpty) { - path = path.penultimate; - } - Node? node = _root.nodeAt(path); - int count = 0; - - while (node != null && count < n) { - if (node.children.isNotEmpty) { - path = path + node.children.first.id; - node = _root.nodeAt(path); - count++; - } else { - break; - } - } - - if (node != null) { - userJump(path); - } - } - - void toggleBoard() { - if (!state.hasValue) return; - - state = AsyncData( - state.requireValue.copyWith(pov: state.requireValue.pov.opposite), - ); - } - - void userPrevious() { - _setPath(state.requireValue.currentPath.penultimate, replaying: true); - } - - @override - void userJump(UciPath path) { - _setPath(path); - } - - @override - void expandVariations(UciPath path) { - if (!state.hasValue) return; - - final node = _root.nodeAt(path); - for (final child in node.children) { - child.isHidden = false; - for (final grandChild in child.children) { - grandChild.isHidden = false; - } - } - state = AsyncData(state.requireValue.copyWith(root: _root.view)); - } - - @override - void collapseVariations(UciPath path) { - if (!state.hasValue) return; - - final node = _root.nodeAt(path); - - for (final child in node.children) { - child.isHidden = true; - } - - state = AsyncData(state.requireValue.copyWith(root: _root.view)); - } - - @override - void promoteVariation(UciPath path, bool toMainline) { - if (!state.hasValue) return; - - _root.promoteAt(path, toMainline: toMainline); - state = AsyncData( - state.requireValue.copyWith( - isOnMainline: _root.isOnMainline(state.requireValue.currentPath), - root: _root.view, - ), - ); - } - - @override - void deleteFromHere(UciPath path) { - _root.deleteAt(path); - _setPath(path.penultimate, shouldRecomputeRootView: true); - } - - Future toggleLocalEvaluation() async { - if (!state.hasValue) return; - - ref - .read(analysisPreferencesProvider.notifier) - .toggleEnableLocalEvaluation(); - - state = AsyncData( - state.requireValue.copyWith( - isLocalEvaluationEnabled: !state.requireValue.isLocalEvaluationEnabled, - ), - ); - - if (state.requireValue.isEngineAvailable) { - final prefs = ref.read(analysisPreferencesProvider); - await ref.read(evaluationServiceProvider).initEngine( - _evaluationContext, - options: EvaluationOptions( - multiPv: prefs.numEvalLines, - cores: prefs.numEngineCores, - ), - ); - _startEngineEval(); - } else { - _stopEngineEval(); - ref.read(evaluationServiceProvider).disposeEngine(); - } - } - - void setNumEvalLines(int numEvalLines) { - if (!state.hasValue) return; - - ref - .read(analysisPreferencesProvider.notifier) - .setNumEvalLines(numEvalLines); - - ref.read(evaluationServiceProvider).setOptions( - EvaluationOptions( - multiPv: numEvalLines, - cores: ref.read(analysisPreferencesProvider).numEngineCores, - ), - ); - - _root.updateAll((node) => node.eval = null); - - state = AsyncData( - state.requireValue.copyWith( - currentNode: AnalysisCurrentNode.fromNode( - _root.nodeAt(state.requireValue.currentPath), - ), - ), - ); - - _startEngineEval(); - } - - void setEngineCores(int numEngineCores) { - ref - .read(analysisPreferencesProvider.notifier) - .setEngineCores(numEngineCores); - - ref.read(evaluationServiceProvider).setOptions( - EvaluationOptions( - multiPv: ref.read(analysisPreferencesProvider).numEvalLines, - cores: numEngineCores, - ), - ); - - _startEngineEval(); - } - - void setDisplayMode(DisplayMode mode) { - if (!state.hasValue) return; - - state = AsyncData(state.requireValue.copyWith(displayMode: mode)); - } - - Future requestServerAnalysis() async { - if (!state.hasValue) return; - - if (state.requireValue.canRequestServerAnalysis) { - final service = ref.read(serverAnalysisServiceProvider); - return service.requestAnalysis( - options.id as GameAnyId, - options.orientation, - ); - } - return Future.error('Cannot request server analysis'); - } - - /// Gets the node and maybe the associated branch opening at the given path. - (Node, Opening?) _nodeOpeningAt(Node node, UciPath path, [Opening? opening]) { - if (path.isEmpty) return (node, opening); - final child = node.childById(path.head!); - if (child != null) { - return _nodeOpeningAt(child, path.tail, child.opening ?? opening); - } else { - return (node, opening); - } - } - - /// Makes a full PGN string (including headers and comments) of the current game state. - String makeExportPgn() { - if (!state.hasValue) Exception('Cannot make a PGN'); - - return _root.makePgn( - state.requireValue.pgnHeaders, - state.requireValue.pgnRootComments, - ); - } - - /// Makes a PGN string up to the current node only. - String makeCurrentNodePgn() { - if (!state.hasValue) Exception('Cannot make a PGN up to the current node'); - - final nodes = _root.branchesOn(state.requireValue.currentPath); - return nodes.map((node) => node.sanMove.san).join(' '); - } - - void _setPath( - UciPath path, { - bool shouldForceShowVariation = false, - bool shouldRecomputeRootView = false, - bool replaying = false, - bool isBroadcastMove = false, - }) { - if (!state.hasValue) return; - - final pathChange = state.requireValue.currentPath != path; - final (currentNode, opening) = _nodeOpeningAt(_root, path); - - // always show variation if the user plays a move - if (shouldForceShowVariation && - currentNode is Branch && - currentNode.isHidden) { - _root.updateAt(path, (node) { - if (node is Branch) node.isHidden = false; - }); - } - - // root view is only used to display move list, so we need to - // recompute the root view only when the nodelist length changes - // or a variation is hidden/shown - final rootView = shouldForceShowVariation || shouldRecomputeRootView - ? _root.view - : state.requireValue.root; - - final isForward = path.size > state.requireValue.currentPath.size; - if (currentNode is Branch) { - if (!replaying) { - if (isForward) { - final isCheck = currentNode.sanMove.isCheck; - if (currentNode.sanMove.isCapture) { - ref - .read(moveFeedbackServiceProvider) - .captureFeedback(check: isCheck); - } else { - ref.read(moveFeedbackServiceProvider).moveFeedback(check: isCheck); - } - } - } else if (isForward) { - final soundService = ref.read(soundServiceProvider); - if (currentNode.sanMove.isCapture) { - soundService.play(Sound.capture); - } else { - soundService.play(Sound.move); - } - } - - if (currentNode.opening == null && currentNode.position.ply <= 30) { - _fetchOpening(_root, path); - } - - state = AsyncData( - state.requireValue.copyWith( - currentPath: path, - broadcastLivePath: - isBroadcastMove ? path : state.requireValue.broadcastLivePath, - isOnMainline: _root.isOnMainline(path), - currentNode: AnalysisCurrentNode.fromNode(currentNode), - currentBranchOpening: opening, - lastMove: currentNode.sanMove.move, - promotionMove: null, - root: rootView, - clocks: options.isBroadcast ? _makeClocks(path) : null, - ), - ); - } else { - state = AsyncData( - state.requireValue.copyWith( - currentPath: path, - broadcastLivePath: - isBroadcastMove ? path : state.requireValue.broadcastLivePath, - isOnMainline: _root.isOnMainline(path), - currentNode: AnalysisCurrentNode.fromNode(currentNode), - currentBranchOpening: opening, - lastMove: null, - promotionMove: null, - root: rootView, - clocks: options.isBroadcast ? _makeClocks(path) : null, - ), - ); - } - - if (pathChange && state.requireValue.isEngineAvailable) { - _debouncedStartEngineEval(); - } - } - - Future _fetchOpening(Node fromNode, UciPath path) async { - if (!state.hasValue) return; - if (!kOpeningAllowedVariants.contains(options.variant)) return; - - final moves = fromNode.branchesOn(path).map((node) => node.sanMove.move); - if (moves.isEmpty) return; - if (moves.length > 40) return; - - final opening = - await ref.read(openingServiceProvider).fetchFromMoves(moves); - - if (opening != null) { - fromNode.updateAt(path, (node) => node.opening = opening); - - if (state.requireValue.currentPath == path) { - state = AsyncData( - state.requireValue.copyWith( - currentNode: AnalysisCurrentNode.fromNode(fromNode.nodeAt(path)), - ), - ); - } - } - } - - void _startEngineEval() { - if (!state.hasValue) return; - - if (!state.requireValue.isEngineAvailable) return; - ref - .read(evaluationServiceProvider) - .start( - state.requireValue.currentPath, - _root.branchesOn(state.requireValue.currentPath).map(Step.fromNode), - initialPositionEval: _root.eval, - shouldEmit: (work) => work.path == state.requireValue.currentPath, - ) - ?.forEach( - (t) => _root.updateAt(t.$1.path, (node) => node.eval = t.$2), - ); - } - - void _debouncedStartEngineEval() { - _engineEvalDebounce(() { - _startEngineEval(); - }); - } - - void _stopEngineEval() { - if (!state.hasValue) Exception('Cannot export PGN'); - - ref.read(evaluationServiceProvider).stop(); - // update the current node with last cached eval - state = AsyncData( - state.requireValue.copyWith( - currentNode: AnalysisCurrentNode.fromNode( - _root.nodeAt(state.requireValue.currentPath)), - ), - ); - } - - void _listenToServerAnalysisEvents() { - if (!state.hasValue) Exception('Cannot export PGN'); - - final event = - ref.read(serverAnalysisServiceProvider).lastAnalysisEvent.value; - if (event != null && event.$1 == state.requireValue.id) { - _mergeOngoingAnalysis(_root, event.$2.tree); - state = AsyncData( - state.requireValue.copyWith( - acplChartData: _makeAcplChartData(), - playersAnalysis: event.$2.analysis != null - ? ( - white: event.$2.analysis!.white, - black: event.$2.analysis!.black - ) - : null, - root: _root.view, - ), - ); - } - } - - void _mergeOngoingAnalysis(Node n1, Map n2) { - final eval = n2['eval'] as Map?; - final cp = eval?['cp'] as int?; - final mate = eval?['mate'] as int?; - final pgnEval = cp != null - ? PgnEvaluation.pawns(pawns: cpToPawns(cp)) - : mate != null - ? PgnEvaluation.mate(mate: mate) - : null; - final glyphs = n2['glyphs'] as List?; - final glyph = glyphs?.first as Map?; - final comments = n2['comments'] as List?; - final comment = - (comments?.first as Map?)?['text'] as String?; - final children = n2['children'] as List? ?? []; - final pgnComment = - pgnEval != null ? PgnComment(eval: pgnEval, text: comment) : null; - if (n1 is Branch) { - if (pgnComment != null) { - if (n1.lichessAnalysisComments == null) { - n1.lichessAnalysisComments = [pgnComment]; - } else { - n1.lichessAnalysisComments!.removeWhere((c) => c.eval != null); - n1.lichessAnalysisComments!.add(pgnComment); - } - } - if (glyph != null) { - n1.nags ??= [glyph['id'] as int]; - } - } - for (final c in children) { - final n2child = c as Map; - final id = n2child['id'] as String; - final n1child = n1.childById(UciCharPair.fromStringId(id)); - if (n1child != null) { - _mergeOngoingAnalysis(n1child, n2child); - } else { - final uci = n2child['uci'] as String; - final san = n2child['san'] as String; - final move = Move.parse(uci)!; - n1.addChild( - Branch( - position: n1.position.playUnchecked(move), - sanMove: SanMove(san, move), - isHidden: children.length > 1, - ), - ); - } - } - } - - IList? _makeAcplChartData() { - if (!_root.mainline.any((node) => node.lichessAnalysisComments != null)) { - return null; - } - final list = _root.mainline - .map( - (node) => ( - node.position.isCheckmate, - node.position.turn, - node.lichessAnalysisComments - ?.firstWhereOrNull((c) => c.eval != null) - ?.eval - ), - ) - .map( - (el) { - final (isCheckmate, side, eval) = el; - return eval != null - ? ExternalEval( - cp: eval.pawns != null ? cpFromPawns(eval.pawns!) : null, - mate: eval.mate, - depth: eval.depth, - ) - : ExternalEval( - cp: null, - // hack to display checkmate as the max eval - mate: isCheckmate - ? side == Side.white - ? -1 - : 1 - : null, - ); - }, - ).toList(growable: false); - return list.isEmpty ? null : IList(list); - } - - ({Duration? parentClock, Duration? clock}) _makeClocks(UciPath path) { - final nodeView = _root.nodeAt(path).view; - final parentView = _root.parentAt(path).view; - - return ( - parentClock: (parentView is ViewBranch) ? parentView.clock : null, - clock: (nodeView is ViewBranch) ? nodeView.clock : null, - ); - } -} - -enum DisplayMode { - moves, - summary, -} - -@freezed -class BroadcastGameState with _$BroadcastGameState { - const BroadcastGameState._(); - - const factory BroadcastGameState({ - /// Analysis ID - required StringId id, - - /// The variant of the analysis. - required Variant variant, - - /// Immutable view of the whole tree - required ViewRoot root, - - /// The current node in the analysis view. - /// - /// This is an immutable copy of the actual [Node] at the `currentPath`. - /// We don't want to use [Node.view] here because it'd copy the whole tree - /// under the current node and it's expensive. - required AnalysisCurrentNode currentNode, - - /// The path to the current node in the analysis view. - required UciPath currentPath, - - // The path to the current broadcast live move. - required UciPath? broadcastLivePath, - - /// Whether the current path is on the mainline. - required bool isOnMainline, - - /// The side to display the board from. - required Side pov, - - /// Whether local evaluation is allowed for this analysis. - required bool isLocalEvaluationAllowed, - - /// Whether the user has enabled local evaluation. - required bool isLocalEvaluationEnabled, - - /// The display mode of the analysis. - /// - /// It can be either moves, summary or opening explorer. - required DisplayMode displayMode, - - /// Clocks if avaible. Only used by the broadcast analysis screen. - ({Duration? parentClock, Duration? clock})? clocks, - - /// The last move played. - Move? lastMove, - - /// Possible promotion move to be played. - NormalMove? promotionMove, - - /// Opening of the analysis context (from lichess archived games). - Opening? contextOpening, - - /// The opening of the current branch. - Opening? currentBranchOpening, - - /// Optional server analysis to display player stats. - ({PlayerAnalysis white, PlayerAnalysis black})? playersAnalysis, - - /// Optional ACPL chart data of the game, coming from lichess server analysis. - IList? acplChartData, - - /// The PGN headers of the game. - required IMap pgnHeaders, - - /// The PGN comments of the game. - /// - /// This field is only used with user submitted PGNS. - IList? pgnRootComments, - }) = _AnalysisState; - - /// The game ID of the analysis, if it's a lichess game. - GameAnyId? get gameAnyId => - _isStandaloneAnalysis(id) ? null : GameAnyId(id.value); - - /// Whether the analysis is for a lichess game. - bool get isLichessGameAnalysis => gameAnyId != null; - - IMap> get validMoves => makeLegalMoves( - currentNode.position, - isChess960: variant == Variant.chess960, - ); - - /// Whether the user can request server analysis. - /// - /// It must be a lichess game, which is finished and not already analyzed. - bool get canRequestServerAnalysis => false; - - bool get canShowGameSummary => hasServerAnalysis || canRequestServerAnalysis; - - bool get hasServerAnalysis => playersAnalysis != null; - - /// Whether an evaluation can be available - bool get hasAvailableEval => - isEngineAvailable || - (isLocalEvaluationAllowed && - acplChartData != null && - acplChartData!.isNotEmpty); - - /// Whether the engine is allowed for this analysis and variant. - bool get isEngineAllowed => - isLocalEvaluationAllowed && engineSupportedVariants.contains(variant); - - /// Whether the engine is available for evaluation - bool get isEngineAvailable => isEngineAllowed && isLocalEvaluationEnabled; - - Position get position => currentNode.position; - bool get canGoNext => currentNode.hasChild; - bool get canGoBack => currentPath.size > UciPath.empty.size; - - EngineGaugeParams get engineGaugeParams => ( - orientation: pov, - isLocalEngineAvailable: isEngineAvailable, - position: position, - savedEval: currentNode.eval ?? currentNode.serverEval, - ); - - AnalysisOptions get openingExplorerOptions => AnalysisOptions( - id: standaloneOpeningExplorerId, - isLocalEvaluationAllowed: false, - orientation: pov, - variant: variant, - initialMoveCursor: currentPath.size, - ); - - static AnalysisOptions get broadcastOptions => const AnalysisOptions( - id: standaloneBroadcastId, - isLocalEvaluationAllowed: true, - orientation: Side.white, - variant: Variant.standard, - isBroadcast: true, - ); -} - -@freezed -class AnalysisCurrentNode with _$AnalysisCurrentNode { - const AnalysisCurrentNode._(); - - const factory AnalysisCurrentNode({ - required Position position, - required bool hasChild, - required bool isRoot, - SanMove? sanMove, - Opening? opening, - ClientEval? eval, - IList? lichessAnalysisComments, - IList? startingComments, - IList? comments, - IList? nags, - }) = _AnalysisCurrentNode; - - factory AnalysisCurrentNode.fromNode(Node node) { - if (node is Branch) { - return AnalysisCurrentNode( - sanMove: node.sanMove, - position: node.position, - isRoot: node is Root, - hasChild: node.children.isNotEmpty, - opening: node.opening, - eval: node.eval, - lichessAnalysisComments: IList(node.lichessAnalysisComments), - startingComments: IList(node.startingComments), - comments: IList(node.comments), - nags: IList(node.nags), - ); - } else { - return AnalysisCurrentNode( - position: node.position, - hasChild: node.children.isNotEmpty, - isRoot: node is Root, - opening: node.opening, - eval: node.eval, - ); - } - } - - /// The evaluation from the PGN comments. - /// - /// For now we only trust the eval coming from lichess analysis. - ExternalEval? get serverEval { - final pgnEval = - lichessAnalysisComments?.firstWhereOrNull((c) => c.eval != null)?.eval; - return pgnEval != null - ? ExternalEval( - cp: pgnEval.pawns != null ? cpFromPawns(pgnEval.pawns!) : null, - mate: pgnEval.mate, - depth: pgnEval.depth, - ) - : null; - } -} diff --git a/lib/src/view/broadcast/broadcast_bottom_bar.dart b/lib/src/view/broadcast/broadcast_bottom_bar.dart new file mode 100644 index 0000000000..083a90fea7 --- /dev/null +++ b/lib/src/view/broadcast/broadcast_bottom_bar.dart @@ -0,0 +1,122 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast_game_controller.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/network/connectivity.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/view/board_editor/board_editor_screen.dart'; +import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_screen.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; +import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; +import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; + +class BroadcastBottomBar extends ConsumerWidget { + const BroadcastBottomBar({ + required this.roundId, + required this.gameId, + }); + + final BroadcastRoundId roundId; + final BroadcastGameId gameId; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final ctrlProvider = broadcastGameControllerProvider(roundId, gameId); + final analysisState = ref.watch(ctrlProvider).requireValue; + final isOnline = + ref.watch(connectivityChangesProvider).valueOrNull?.isOnline ?? false; + + return BottomBar( + children: [ + BottomBarButton( + label: context.l10n.menu, + onTap: () { + _showAnalysisMenu(context, ref); + }, + icon: Icons.menu, + ), + BottomBarButton( + label: context.l10n.openingExplorer, + onTap: isOnline + ? () { + pushPlatformRoute( + context, + title: context.l10n.openingExplorer, + builder: (_) => OpeningExplorerScreen( + pgn: ref.read(ctrlProvider.notifier).makeCurrentNodePgn(), + options: analysisState.openingExplorerOptions, + ), + ); + } + : null, + icon: Icons.explore, + ), + RepeatButton( + onLongPress: + analysisState.canGoBack ? () => _moveBackward(ref) : null, + child: BottomBarButton( + key: const ValueKey('goto-previous'), + onTap: analysisState.canGoBack ? () => _moveBackward(ref) : null, + label: 'Previous', + icon: CupertinoIcons.chevron_back, + showTooltip: false, + ), + ), + RepeatButton( + onLongPress: analysisState.canGoNext ? () => _moveForward(ref) : null, + child: BottomBarButton( + key: const ValueKey('goto-next'), + icon: CupertinoIcons.chevron_forward, + label: context.l10n.next, + onTap: analysisState.canGoNext ? () => _moveForward(ref) : null, + showTooltip: false, + ), + ), + ], + ); + } + + void _moveForward(WidgetRef ref) => ref + .read(broadcastGameControllerProvider(roundId, gameId).notifier) + .userNext(); + void _moveBackward(WidgetRef ref) => ref + .read(broadcastGameControllerProvider(roundId, gameId).notifier) + .userPrevious(); + + Future _showAnalysisMenu(BuildContext context, WidgetRef ref) { + return showAdaptiveActionSheet( + context: context, + actions: [ + BottomSheetAction( + makeLabel: (context) => Text(context.l10n.flipBoard), + onPressed: (context) { + ref + .read( + broadcastGameControllerProvider(roundId, gameId).notifier, + ) + .toggleBoard(); + }, + ), + BottomSheetAction( + makeLabel: (context) => Text(context.l10n.boardEditor), + onPressed: (context) { + final analysisState = ref + .read(broadcastGameControllerProvider(roundId, gameId)) + .requireValue; + final boardFen = analysisState.position.fen; + pushPlatformRoute( + context, + title: context.l10n.boardEditor, + builder: (_) => BoardEditorScreen( + initialFen: boardFen, + ), + ); + }, + ), + ], + ); + } +} diff --git a/lib/src/view/broadcast/broadcast_engine_depth.dart b/lib/src/view/broadcast/broadcast_engine_depth.dart new file mode 100644 index 0000000000..c83bd50e37 --- /dev/null +++ b/lib/src/view/broadcast/broadcast_engine_depth.dart @@ -0,0 +1,128 @@ +import 'dart:math' as math; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast_game_controller.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/engine/engine.dart'; +import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/list.dart'; +import 'package:popover/popover.dart'; + +class BroadcastEngineDepth extends ConsumerWidget { + final BroadcastRoundId roundId; + final BroadcastGameId gameId; + + const BroadcastEngineDepth(this.roundId, this.gameId); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isEngineAvailable = ref.watch( + broadcastGameControllerProvider(roundId, gameId).select( + (value) => value.requireValue.isEngineAvailable, + ), + ); + final currentNode = ref.watch( + broadcastGameControllerProvider(roundId, gameId) + .select((value) => value.requireValue.currentNode), + ); + final depth = ref.watch( + engineEvaluationProvider.select((value) => value.eval?.depth), + ) ?? + currentNode.eval?.depth; + + return isEngineAvailable && depth != null + ? AppBarTextButton( + onPressed: () { + showPopover( + context: context, + bodyBuilder: (context) { + return _StockfishInfo(currentNode); + }, + direction: PopoverDirection.top, + width: 240, + backgroundColor: + Theme.of(context).platform == TargetPlatform.android + ? Theme.of(context).dialogBackgroundColor + : CupertinoDynamicColor.resolve( + CupertinoColors.tertiarySystemBackground, + context, + ), + transitionDuration: Duration.zero, + popoverTransitionBuilder: (_, child) => child, + ); + }, + child: RepaintBoundary( + child: Container( + width: 20.0, + height: 20.0, + padding: const EdgeInsets.all(2.0), + decoration: BoxDecoration( + color: Theme.of(context).platform == TargetPlatform.android + ? Theme.of(context).colorScheme.secondary + : CupertinoTheme.of(context).primaryColor, + borderRadius: BorderRadius.circular(4.0), + ), + child: FittedBox( + fit: BoxFit.contain, + child: Text( + '${math.min(99, depth)}', + style: TextStyle( + color: Theme.of(context).platform == + TargetPlatform.android + ? Theme.of(context).colorScheme.onSecondary + : CupertinoTheme.of(context).primaryContrastingColor, + fontFeatures: const [ + FontFeature.tabularFigures(), + ], + ), + ), + ), + ), + ), + ) + : const SizedBox.shrink(); + } +} + +class _StockfishInfo extends ConsumerWidget { + const _StockfishInfo(this.currentNode); + + final AnalysisCurrentNode currentNode; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final (engineName: engineName, eval: eval, state: engineState) = + ref.watch(engineEvaluationProvider); + + final currentEval = eval ?? currentNode.eval; + + final knps = engineState == EngineState.computing + ? ', ${eval?.knps.round()}kn/s' + : ''; + final depth = currentEval?.depth ?? 0; + final maxDepth = math.max(depth, kMaxEngineDepth); + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + PlatformListTile( + leading: Image.asset( + 'assets/images/stockfish/icon.png', + width: 44, + height: 44, + ), + title: Text(engineName), + subtitle: Text( + context.l10n.depthX( + '$depth/$maxDepth$knps', + ), + ), + ), + ], + ); + } +} diff --git a/lib/src/view/broadcast/broadcast_game_analysis_screen.dart b/lib/src/view/broadcast/broadcast_game_analysis_screen.dart index 848bc01c6e..5163429c3a 100644 --- a/lib/src/view/broadcast/broadcast_game_analysis_screen.dart +++ b/lib/src/view/broadcast/broadcast_game_analysis_screen.dart @@ -1,25 +1,39 @@ +import 'package:chessground/chessground.dart'; import 'package:dartchess/dartchess.dart'; -import 'package:flutter/cupertino.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; +import 'package:lichess_mobile/src/constants.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_game_controller.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_round_controller.dart'; +import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/common/eval.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; +import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/duration.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/lichess_assets.dart'; -import 'package:lichess_mobile/src/view/analysis/analysis_board.dart'; -import 'package:lichess_mobile/src/view/analysis/analysis_screen_body.dart'; -import 'package:lichess_mobile/src/view/analysis/analysis_settings.dart'; -import 'package:lichess_mobile/src/view/analysis/engine_depth.dart'; +import 'package:lichess_mobile/src/utils/screen.dart'; +import 'package:lichess_mobile/src/view/broadcast/broadcast_bottom_bar.dart'; +import 'package:lichess_mobile/src/view/broadcast/broadcast_engine_depth.dart'; +import 'package:lichess_mobile/src/view/broadcast/broadcast_settings.dart'; +import 'package:lichess_mobile/src/view/broadcast/broadcast_tree_view.dart'; +import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; +import 'package:lichess_mobile/src/view/engine/engine_lines.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/pgn.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; +import 'package:logging/logging.dart'; + +final _logger = Logger('BroadcastGameAnalysisScreen'); class BroadcastGameAnalysisScreen extends ConsumerWidget { final BroadcastRoundId roundId; @@ -34,33 +48,235 @@ class BroadcastGameAnalysisScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return ConsumerPlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, - ref: ref, + final state = ref.watch(broadcastGameControllerProvider(roundId, gameId)); + + return PlatformScaffold( + appBar: PlatformAppBar( + title: Text(title), + actions: [ + if (state.hasValue) BroadcastEngineDepth(roundId, gameId), + AppBarIconButton( + onPressed: () => (state.hasValue) + ? showAdaptiveBottomSheet( + context: context, + isScrollControlled: true, + showDragHandle: true, + isDismissible: true, + builder: (_) => BroadcastGameAnalysisSettings( + roundId, + gameId, + ), + ) + : null, + semanticsLabel: context.l10n.settingsSettings, + icon: const Icon(Icons.settings), + ), + ], + ), + body: state.when( + data: (state) => _Body(roundId, gameId), + loading: () => const CircularProgressIndicator.adaptive(), + error: (error, stackTrace) { + _logger.severe('Cannot load broadcast game: $error', stackTrace); + return Center( + child: Text('Cannot load broadcast game: $error'), + ); + }, + ), ); } +} - Widget broadcastWrapBoardBuilder( - WidgetRef ref, - AnalysisBoard board, - AnalysisControllerProvider ctrlProvider, - double boardSize, - ) { - final clocks = ref.watch(ctrlProvider.select((value) => value.clocks)); - final currentPath = ref.watch( - ctrlProvider.select((value) => value.currentPath), - ); - final broadcastLivePath = ref.watch( - ctrlProvider.select((value) => value.broadcastLivePath), +class _Body extends ConsumerWidget { + final BroadcastRoundId roundId; + final BroadcastGameId gameId; + + const _Body(this.roundId, this.gameId); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return SafeArea( + child: Column( + children: [ + Expanded( + child: LayoutBuilder( + builder: (context, constraints) { + final defaultBoardSize = constraints.biggest.shortestSide; + final isTablet = isTabletOrLarger(context); + final remainingHeight = + constraints.maxHeight - defaultBoardSize; + final isSmallScreen = + remainingHeight < kSmallRemainingHeightLeftBoardThreshold; + final boardSize = isTablet || isSmallScreen + ? defaultBoardSize - kTabletBoardTableSidePadding * 2 + : defaultBoardSize; + + final landscape = constraints.biggest.aspectRatio > 1; + + final engineGaugeParams = ref.watch( + broadcastGameControllerProvider(roundId, gameId) + .select((state) => state.valueOrNull?.engineGaugeParams), + ); + + final currentNode = ref.watch( + broadcastGameControllerProvider(roundId, gameId) + .select((state) => state.requireValue.currentNode), + ); + + final engineLines = EngineLines( + clientEval: currentNode.eval, + isGameOver: currentNode.position.isGameOver, + onTapMove: ref + .read( + broadcastGameControllerProvider(roundId, gameId) + .notifier, + ) + .onUserMove, + ); + + final showEvaluationGauge = ref.watch( + analysisPreferencesProvider + .select((value) => value.showEvaluationGauge), + ); + + final engineGauge = + showEvaluationGauge && engineGaugeParams != null + ? EngineGauge( + params: engineGaugeParams, + displayMode: landscape + ? EngineGaugeDisplayMode.vertical + : EngineGaugeDisplayMode.horizontal, + ) + : null; + + return landscape + ? Row( + mainAxisSize: MainAxisSize.max, + children: [ + Padding( + padding: EdgeInsets.all( + isTablet ? kTabletBoardTableSidePadding : 0.0, + ), + child: Row( + children: [ + _BroadcastBoardWithHeaders( + roundId, + gameId, + boardSize, + isTablet, + ), + if (engineGauge != null) ...[ + const SizedBox(width: 4.0), + engineGauge, + ], + ], + ), + ), + Flexible( + fit: FlexFit.loose, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + if (engineGaugeParams != null) engineLines, + Expanded( + child: PlatformCard( + clipBehavior: Clip.hardEdge, + borderRadius: const BorderRadius.all( + Radius.circular(4.0), + ), + margin: const EdgeInsets.all( + kTabletBoardTableSidePadding, + ), + semanticContainer: false, + child: BroadcastTreeView( + roundId, + gameId, + Orientation.landscape, + ), + ), + ), + ], + ), + ), + ], + ) + : Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: EdgeInsets.all( + isTablet ? kTabletBoardTableSidePadding : 0.0, + ), + child: Column( + children: [ + if (engineGauge != null) ...[ + engineGauge, + engineLines, + ], + _BroadcastBoardWithHeaders( + roundId, + gameId, + boardSize, + isTablet, + ), + ], + ), + ), + Expanded( + child: Padding( + padding: isTablet + ? const EdgeInsets.symmetric( + horizontal: kTabletBoardTableSidePadding, + ) + : EdgeInsets.zero, + child: BroadcastTreeView( + roundId, + gameId, + Orientation.portrait, + ), + ), + ), + ], + ); + }, + ), + ), + BroadcastBottomBar(roundId: roundId, gameId: gameId), + ], + ), ); - final playingSide = - ref.watch(ctrlProvider.select((value) => value.position.turn)); + } +} + +class _BroadcastBoardWithHeaders extends ConsumerWidget { + final BroadcastRoundId roundId; + final BroadcastGameId gameId; + final double size; + final bool isTablet; + + const _BroadcastBoardWithHeaders( + this.roundId, + this.gameId, + this.size, + this.isTablet, + ); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final gameState = ref + .watch(broadcastGameControllerProvider(roundId, gameId)) + .requireValue; + final clocks = gameState.clocks; + final currentPath = gameState.currentPath; + final broadcastLivePath = gameState.broadcastLivePath; + final playingSide = gameState.position.turn; + final pov = gameState.pov; final game = ref.watch( broadcastRoundControllerProvider(roundId) .select((game) => game.value?[gameId]), ); - final pov = ref.watch(ctrlProvider.select((value) => value.pov)); return Column( children: [ @@ -69,18 +285,18 @@ class BroadcastGameAnalysisScreen extends ConsumerWidget { clock: (playingSide == pov.opposite) ? clocks?.parentClock : clocks?.clock, - width: boardSize, + width: size, game: game, side: pov.opposite, boardSide: _PlayerWidgetSide.top, playingSide: playingSide, playClock: currentPath == broadcastLivePath, ), - board, + _BroadcastBoard(roundId, gameId, size), if (game != null) _PlayerWidget( clock: (playingSide == pov) ? clocks?.parentClock : clocks?.clock, - width: boardSize, + width: size, game: game, side: pov, boardSide: _PlayerWidgetSide.bottom, @@ -90,123 +306,120 @@ class BroadcastGameAnalysisScreen extends ConsumerWidget { ], ); } +} + +class _BroadcastBoard extends ConsumerStatefulWidget { + const _BroadcastBoard( + this.roundId, + this.gameId, + this.boardSize, + ); + + final BroadcastRoundId roundId; + final BroadcastGameId gameId; + final double boardSize; + + @override + ConsumerState<_BroadcastBoard> createState() => _BroadcastBoardState(); +} - Widget _androidBuilder(BuildContext context, WidgetRef ref) { - final pgn = ref.watch( - broadcastGameControllerProvider( - roundId, - gameId, +class _BroadcastBoardState extends ConsumerState<_BroadcastBoard> { + ISet userShapes = ISet(); + + @override + Widget build(BuildContext context) { + final ctrlProvider = + broadcastGameControllerProvider(widget.roundId, widget.gameId); + final broadcastAnalysisState = ref.watch(ctrlProvider).requireValue; + final boardPrefs = ref.watch(boardPreferencesProvider); + final showBestMoveArrow = ref.watch( + analysisPreferencesProvider.select( + (value) => value.showBestMoveArrow, ), ); + final showAnnotationsOnBoard = ref.watch( + analysisPreferencesProvider.select((value) => value.showAnnotations), + ); - return Scaffold( - resizeToAvoidBottomInset: false, - appBar: AppBar( - title: Text(title), - actions: [ - if (pgn.hasValue) - EngineDepth( - analysisControllerProvider( - pgn.requireValue, - AnalysisState.broadcastOptions, - ), + final evalBestMoves = ref.watch( + engineEvaluationProvider.select((s) => s.eval?.bestMoves), + ); + + final currentNode = broadcastAnalysisState.currentNode; + final annotation = makeAnnotation(currentNode.nags); + + final bestMoves = evalBestMoves ?? currentNode.eval?.bestMoves; + + final sanMove = currentNode.sanMove; + + final ISet bestMoveShapes = showBestMoveArrow && + broadcastAnalysisState.isEngineAvailable && + bestMoves != null + ? computeBestMoveShapes( + bestMoves, + currentNode.position.turn, + boardPrefs.pieceSet.assets, + ) + : ISet(); + + return Chessboard( + size: widget.boardSize, + fen: broadcastAnalysisState.position.fen, + lastMove: broadcastAnalysisState.lastMove as NormalMove?, + orientation: broadcastAnalysisState.pov, + game: GameData( + playerSide: broadcastAnalysisState.position.isGameOver + ? PlayerSide.none + : broadcastAnalysisState.position.turn == Side.white + ? PlayerSide.white + : PlayerSide.black, + isCheck: boardPrefs.boardHighlights && + broadcastAnalysisState.position.isCheck, + sideToMove: broadcastAnalysisState.position.turn, + validMoves: broadcastAnalysisState.validMoves, + promotionMove: broadcastAnalysisState.promotionMove, + onMove: (move, {isDrop, captured}) => + ref.read(ctrlProvider.notifier).onUserMove(move), + onPromotionSelection: (role) => + ref.read(ctrlProvider.notifier).onPromotionSelection(role), + ), + shapes: userShapes.union(bestMoveShapes), + annotations: + showAnnotationsOnBoard && sanMove != null && annotation != null + ? altCastles.containsKey(sanMove.move.uci) + ? IMap({ + Move.parse(altCastles[sanMove.move.uci]!)!.to: annotation, + }) + : IMap({sanMove.move.to: annotation}) + : null, + settings: boardPrefs.toBoardSettings().copyWith( + drawShape: DrawShapeOptions( + enable: boardPrefs.enableShapeDrawings, + onCompleteShape: _onCompleteShape, + onClearShapes: _onClearShapes, + newShapeColor: boardPrefs.shapeColor.color, ), - AppBarIconButton( - onPressed: () => (pgn.hasValue) - ? showAdaptiveBottomSheet( - context: context, - isScrollControlled: true, - showDragHandle: true, - isDismissible: true, - builder: (_) => AnalysisSettings( - pgn.requireValue, - AnalysisState.broadcastOptions, - ), - ) - : null, - semanticsLabel: context.l10n.settingsSettings, - icon: const Icon(Icons.settings), ), - ], - ), - body: pgn.when( - data: (pgn) => AnalysisScreenBody( - pgn: pgn, - options: AnalysisState.broadcastOptions, - enableDrawingShapes: true, - broadcastWrapBoardBuilder: broadcastWrapBoardBuilder, - ), - loading: () => - const Center(child: CircularProgressIndicator.adaptive()), - error: (error, _) { - return Center( - child: Text('Cannot load game analysis: $error'), - ); - }, - ), ); } - Widget _iosBuilder(BuildContext context, WidgetRef ref) { - final pgn = ref.watch( - broadcastGameControllerProvider( - roundId, - gameId, - ), - ); + void _onCompleteShape(Shape shape) { + if (userShapes.any((element) => element == shape)) { + setState(() { + userShapes = userShapes.remove(shape); + }); + return; + } else { + setState(() { + userShapes = userShapes.add(shape); + }); + } + } - return CupertinoPageScaffold( - resizeToAvoidBottomInset: false, - navigationBar: CupertinoNavigationBar( - backgroundColor: Styles.cupertinoScaffoldColor.resolveFrom(context), - border: null, - padding: Styles.cupertinoAppBarTrailingWidgetPadding, - middle: Text(title), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (pgn.hasValue) - EngineDepth( - analysisControllerProvider( - pgn.requireValue, - AnalysisState.broadcastOptions, - ), - ), - AppBarIconButton( - onPressed: () => (pgn.hasValue) - ? showAdaptiveBottomSheet( - context: context, - isScrollControlled: true, - showDragHandle: true, - isDismissible: true, - builder: (_) => AnalysisSettings( - pgn.requireValue, - AnalysisState.broadcastOptions, - ), - ) - : null, - semanticsLabel: context.l10n.settingsSettings, - icon: const Icon(Icons.settings), - ), - ], - ), - ), - child: pgn.when( - data: (pgn) => AnalysisScreenBody( - pgn: pgn, - options: AnalysisState.broadcastOptions, - enableDrawingShapes: true, - broadcastWrapBoardBuilder: broadcastWrapBoardBuilder, - ), - loading: () => - const Center(child: CircularProgressIndicator.adaptive()), - error: (error, _) { - return Center( - child: Text('Cannot load game analysis: $error'), - ); - }, - ), - ); + void _onClearShapes() { + setState(() { + userShapes = ISet(); + }); } } diff --git a/lib/src/view/broadcast/broadcast_settings.dart b/lib/src/view/broadcast/broadcast_settings.dart new file mode 100644 index 0000000000..ed67e5f066 --- /dev/null +++ b/lib/src/view/broadcast/broadcast_settings.dart @@ -0,0 +1,159 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast_game_controller.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; +import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; +import 'package:lichess_mobile/src/model/study/study_preferences.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; +import 'package:lichess_mobile/src/widgets/list.dart'; +import 'package:lichess_mobile/src/widgets/non_linear_slider.dart'; +import 'package:lichess_mobile/src/widgets/settings.dart'; + +class BroadcastGameAnalysisSettings extends ConsumerWidget { + const BroadcastGameAnalysisSettings(this.roundId, this.gameId); + + final BroadcastRoundId roundId; + final BroadcastGameId gameId; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final broacdcastGameAnalysisController = + broadcastGameControllerProvider(roundId, gameId); + + final isLocalEvaluationAllowed = ref.watch( + broacdcastGameAnalysisController + .select((s) => s.requireValue.isLocalEvaluationAllowed), + ); + final isEngineAvailable = ref.watch( + broacdcastGameAnalysisController + .select((s) => s.requireValue.isEngineAvailable), + ); + + final analysisPrefs = ref.watch(analysisPreferencesProvider); + final studyPrefs = ref.watch(studyPreferencesProvider); + final isSoundEnabled = ref.watch( + generalPreferencesProvider.select((pref) => pref.isSoundEnabled), + ); + + return BottomSheetScrollableContainer( + children: [ + SwitchSettingTile( + title: Text(context.l10n.toggleLocalEvaluation), + value: analysisPrefs.enableLocalEvaluation, + onChanged: isLocalEvaluationAllowed + ? (_) { + ref + .read(broacdcastGameAnalysisController.notifier) + .toggleLocalEvaluation(); + } + : null, + ), + PlatformListTile( + title: Text.rich( + TextSpan( + text: '${context.l10n.multipleLines}: ', + style: const TextStyle( + fontWeight: FontWeight.normal, + ), + children: [ + TextSpan( + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + text: analysisPrefs.numEvalLines.toString(), + ), + ], + ), + ), + subtitle: NonLinearSlider( + value: analysisPrefs.numEvalLines, + values: const [0, 1, 2, 3], + onChangeEnd: isEngineAvailable + ? (value) => ref + .read(broacdcastGameAnalysisController.notifier) + .setNumEvalLines(value.toInt()) + : null, + ), + ), + if (maxEngineCores > 1) + PlatformListTile( + title: Text.rich( + TextSpan( + text: '${context.l10n.cpus}: ', + style: const TextStyle( + fontWeight: FontWeight.normal, + ), + children: [ + TextSpan( + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + text: analysisPrefs.numEngineCores.toString(), + ), + ], + ), + ), + subtitle: NonLinearSlider( + value: analysisPrefs.numEngineCores, + values: List.generate(maxEngineCores, (index) => index + 1), + onChangeEnd: isEngineAvailable + ? (value) => ref + .read(broacdcastGameAnalysisController.notifier) + .setEngineCores(value.toInt()) + : null, + ), + ), + SwitchSettingTile( + title: Text(context.l10n.bestMoveArrow), + value: analysisPrefs.showBestMoveArrow, + onChanged: isEngineAvailable + ? (value) => ref + .read(analysisPreferencesProvider.notifier) + .toggleShowBestMoveArrow() + : null, + ), + SwitchSettingTile( + title: Text(context.l10n.showVariationArrows), + value: studyPrefs.showVariationArrows, + onChanged: (value) => ref + .read(studyPreferencesProvider.notifier) + .toggleShowVariationArrows(), + ), + SwitchSettingTile( + title: Text(context.l10n.evaluationGauge), + value: analysisPrefs.showEvaluationGauge, + onChanged: (value) => ref + .read(analysisPreferencesProvider.notifier) + .toggleShowEvaluationGauge(), + ), + SwitchSettingTile( + title: Text(context.l10n.toggleGlyphAnnotations), + value: analysisPrefs.showAnnotations, + onChanged: (_) => ref + .read(analysisPreferencesProvider.notifier) + .toggleAnnotations(), + ), + SwitchSettingTile( + title: Text(context.l10n.mobileShowComments), + value: analysisPrefs.showPgnComments, + onChanged: (_) => ref + .read(analysisPreferencesProvider.notifier) + .togglePgnComments(), + ), + SwitchSettingTile( + title: Text(context.l10n.sound), + value: isSoundEnabled, + onChanged: (value) { + ref.read(generalPreferencesProvider.notifier).toggleSoundEnabled(); + }, + ), + ], + ); + } +} diff --git a/lib/src/view/broadcast/broadcast_tree_view.dart b/lib/src/view/broadcast/broadcast_tree_view.dart new file mode 100644 index 0000000000..a92fa7bb8c --- /dev/null +++ b/lib/src/view/broadcast/broadcast_tree_view.dart @@ -0,0 +1,99 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast_game_controller.dart'; +import 'package:lichess_mobile/src/model/common/chess.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/widgets/pgn.dart'; + +const kOpeningHeaderHeight = 32.0; + +class BroadcastTreeView extends ConsumerWidget { + const BroadcastTreeView( + this.roundId, + this.gameId, + this.displayMode, + ); + + final BroadcastRoundId roundId; + final BroadcastGameId gameId; + final Orientation displayMode; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final ctrlProvider = broadcastGameControllerProvider(roundId, gameId); + + final root = + ref.watch(ctrlProvider.select((value) => value.requireValue.root)); + final currentPath = ref + .watch(ctrlProvider.select((value) => value.requireValue.currentPath)); + final broadcastLivePath = ref.watch( + ctrlProvider.select((value) => value.requireValue.broadcastLivePath), + ); + final pgnRootComments = ref.watch( + ctrlProvider.select((value) => value.requireValue.pgnRootComments), + ); + + return ListView( + padding: EdgeInsets.zero, + children: [ + _OpeningHeader( + ctrlProvider, + displayMode: displayMode, + ), + DebouncedPgnTreeView( + root: root, + currentPath: currentPath, + broadcastLivePath: broadcastLivePath, + pgnRootComments: pgnRootComments, + notifier: ref.read(ctrlProvider.notifier), + ), + ], + ); + } +} + +class _OpeningHeader extends ConsumerWidget { + const _OpeningHeader(this.ctrlProvider, {required this.displayMode}); + + final BroadcastGameControllerProvider ctrlProvider; + final Orientation displayMode; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isRootNode = ref.watch( + ctrlProvider.select((s) => s.requireValue.currentNode.isRoot), + ); + final nodeOpening = ref + .watch(ctrlProvider.select((s) => s.requireValue.currentNode.opening)); + final branchOpening = ref + .watch(ctrlProvider.select((s) => s.requireValue.currentBranchOpening)); + final contextOpening = + ref.watch(ctrlProvider.select((s) => s.requireValue.contextOpening)); + final opening = isRootNode + ? LightOpening( + eco: '', + name: context.l10n.startPosition, + ) + : nodeOpening ?? branchOpening ?? contextOpening; + + return opening != null + ? Container( + height: kOpeningHeaderHeight, + width: double.infinity, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.secondaryContainer, + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Center( + child: Text( + opening.name, + overflow: TextOverflow.ellipsis, + ), + ), + ), + ) + : const SizedBox.shrink(); + } +} diff --git a/lib/src/view/game/game_result_dialog.dart b/lib/src/view/game/game_result_dialog.dart index de94f78d6c..8883994b3f 100644 --- a/lib/src/view/game/game_result_dialog.dart +++ b/lib/src/view/game/game_result_dialog.dart @@ -17,7 +17,6 @@ import 'package:lichess_mobile/src/model/game/playable_game.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; -import 'package:lichess_mobile/src/view/analysis/analysis_screen_body.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/pgn.dart'; diff --git a/lib/src/widgets/pgn.dart b/lib/src/widgets/pgn.dart index 172ea28e10..069129a791 100644 --- a/lib/src/widgets/pgn.dart +++ b/lib/src/widgets/pgn.dart @@ -111,7 +111,7 @@ class DebouncedPgnTreeView extends ConsumerStatefulWidget { const DebouncedPgnTreeView({ required this.root, required this.currentPath, - required this.broadcastLivePath, + this.broadcastLivePath, required this.pgnRootComments, required this.notifier, }); From 05ddc9a2d71bb8d90076a0add307be2519e96b9b Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Wed, 13 Nov 2024 02:26:05 +0100 Subject: [PATCH 645/979] cleaning code --- .../model/analysis/analysis_controller.dart | 57 +- .../broadcast/broadcast_game_controller.dart | 233 +--- lib/src/view/analysis/analysis_screen.dart | 1059 ++++++++++++++++- .../view/analysis/analysis_screen_body.dart | 948 --------------- lib/src/view/analysis/engine_depth.dart | 125 -- lib/src/view/analysis/tree_view.dart | 3 - .../view/broadcast/broadcast_boards_tab.dart | 4 +- ...ar.dart => broadcast_game_bottom_bar.dart} | 4 +- ....dart => broadcast_game_engine_depth.dart} | 5 +- ...screen.dart => broadcast_game_screen.dart} | 27 +- ...ings.dart => broadcast_game_settings.dart} | 4 +- ...iew.dart => broadcast_game_tree_view.dart} | 4 +- 12 files changed, 1084 insertions(+), 1389 deletions(-) delete mode 100644 lib/src/view/analysis/analysis_screen_body.dart delete mode 100644 lib/src/view/analysis/engine_depth.dart rename lib/src/view/broadcast/{broadcast_bottom_bar.dart => broadcast_game_bottom_bar.dart} (98%) rename lib/src/view/broadcast/{broadcast_engine_depth.dart => broadcast_game_engine_depth.dart} (95%) rename lib/src/view/broadcast/{broadcast_game_analysis_screen.dart => broadcast_game_screen.dart} (96%) rename lib/src/view/broadcast/{broadcast_settings.dart => broadcast_game_settings.dart} (97%) rename lib/src/view/broadcast/{broadcast_tree_view.dart => broadcast_game_tree_view.dart} (97%) diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index ab049fa7ed..830225ab88 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -28,15 +28,12 @@ part 'analysis_controller.g.dart'; const standaloneAnalysisId = StringId('standalone_analysis'); const standaloneOpeningExplorerId = StringId('standalone_opening_explorer'); -const standaloneBroadcastId = StringId('standalone_broadcast'); final _dateFormat = DateFormat('yyyy.MM.dd'); /// Whether the analysis is a standalone analysis (not a lichess game analysis). bool _isStandaloneAnalysis(StringId id) => - id == standaloneAnalysisId || - id == standaloneOpeningExplorerId || - id == standaloneBroadcastId; + id == standaloneAnalysisId || id == standaloneOpeningExplorerId; @freezed class AnalysisOptions with _$AnalysisOptions { @@ -50,7 +47,6 @@ class AnalysisOptions with _$AnalysisOptions { int? initialMoveCursor, LightOpening? opening, Division? division, - @Default(false) bool isBroadcast, /// Optional server analysis to display player stats. ({PlayerAnalysis white, PlayerAnalysis black})? serverAnalysis, @@ -154,9 +150,6 @@ class AnalysisController extends _$AnalysisController variant: options.variant, id: options.id, currentPath: currentPath, - broadcastLivePath: options.isBroadcast && pgnHeaders['Result'] == '*' - ? currentPath - : null, isOnMainline: _root.isOnMainline(currentPath), root: _root.view, currentNode: AnalysisCurrentNode.fromNode(currentNode), @@ -171,7 +164,6 @@ class AnalysisController extends _$AnalysisController playersAnalysis: options.serverAnalysis, acplChartData: options.serverAnalysis != null ? _makeAcplChartData() : null, - clocks: options.isBroadcast ? _makeClocks(currentPath) : null, ); if (analysisState.isEngineAvailable) { @@ -220,24 +212,6 @@ class AnalysisController extends _$AnalysisController } } - void onBroadcastMove(UciPath path, Move move, Duration? clock) { - final (newPath, isNewNode) = _root.addMoveAt(path, move, clock: clock); - - if (newPath != null) { - if (state.broadcastLivePath == state.currentPath) { - _setPath( - newPath, - shouldRecomputeRootView: isNewNode, - shouldForceShowVariation: true, - isBroadcastMove: true, - ); - } else { - _root.promoteAt(newPath, toMainline: true); - state = state.copyWith(broadcastLivePath: newPath, root: _root.view); - } - } - } - void onPromotionSelection(Role? role) { if (role == null) { state = state.copyWith(promotionMove: null); @@ -445,7 +419,6 @@ class AnalysisController extends _$AnalysisController bool shouldForceShowVariation = false, bool shouldRecomputeRootView = false, bool replaying = false, - bool isBroadcastMove = false, }) { final pathChange = state.currentPath != path; final (currentNode, opening) = _nodeOpeningAt(_root, path); @@ -494,26 +467,22 @@ class AnalysisController extends _$AnalysisController state = state.copyWith( currentPath: path, - broadcastLivePath: isBroadcastMove ? path : state.broadcastLivePath, isOnMainline: _root.isOnMainline(path), currentNode: AnalysisCurrentNode.fromNode(currentNode), currentBranchOpening: opening, lastMove: currentNode.sanMove.move, promotionMove: null, root: rootView, - clocks: options.isBroadcast ? _makeClocks(path) : null, ); } else { state = state.copyWith( currentPath: path, - broadcastLivePath: isBroadcastMove ? path : state.broadcastLivePath, isOnMainline: _root.isOnMainline(path), currentNode: AnalysisCurrentNode.fromNode(currentNode), currentBranchOpening: opening, lastMove: null, promotionMove: null, root: rootView, - clocks: options.isBroadcast ? _makeClocks(path) : null, ); } @@ -675,16 +644,6 @@ class AnalysisController extends _$AnalysisController ).toList(growable: false); return list.isEmpty ? null : IList(list); } - - ({Duration? parentClock, Duration? clock}) _makeClocks(UciPath path) { - final nodeView = _root.nodeAt(path).view; - final parentView = _root.parentAt(path).view; - - return ( - parentClock: (parentView is ViewBranch) ? parentView.clock : null, - clock: (nodeView is ViewBranch) ? nodeView.clock : null, - ); - } } enum DisplayMode { @@ -716,9 +675,6 @@ class AnalysisState with _$AnalysisState { /// The path to the current node in the analysis view. required UciPath currentPath, - // The path to the current broadcast live move. - required UciPath? broadcastLivePath, - /// Whether the current path is on the mainline. required bool isOnMainline, @@ -736,9 +692,6 @@ class AnalysisState with _$AnalysisState { /// It can be either moves, summary or opening explorer. required DisplayMode displayMode, - /// Clocks if avaible. Only used by the broadcast analysis screen. - ({Duration? parentClock, Duration? clock})? clocks, - /// The last move played. Move? lastMove, @@ -823,14 +776,6 @@ class AnalysisState with _$AnalysisState { variant: variant, initialMoveCursor: currentPath.size, ); - - static AnalysisOptions get broadcastOptions => const AnalysisOptions( - id: standaloneBroadcastId, - isLocalEvaluationAllowed: true, - orientation: Side.white, - variant: Variant.standard, - isBroadcast: true, - ); } @freezed diff --git a/lib/src/model/broadcast/broadcast_game_controller.dart b/lib/src/model/broadcast/broadcast_game_controller.dart index 95ef408058..1041354d86 100644 --- a/lib/src/model/broadcast/broadcast_game_controller.dart +++ b/lib/src/model/broadcast/broadcast_game_controller.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:collection/collection.dart'; import 'package:dartchess/dartchess.dart'; import 'package:deep_pick/deep_pick.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; @@ -9,10 +8,8 @@ import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; import 'package:lichess_mobile/src/model/analysis/opening_service.dart'; -import 'package:lichess_mobile/src/model/analysis/server_analysis_service.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_repository.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; -import 'package:lichess_mobile/src/model/common/eval.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/node.dart'; import 'package:lichess_mobile/src/model/common/service/move_feedback.dart'; @@ -21,7 +18,6 @@ import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/common/uci.dart'; import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; import 'package:lichess_mobile/src/model/engine/work.dart'; -import 'package:lichess_mobile/src/model/game/player.dart'; import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/network/socket.dart'; import 'package:lichess_mobile/src/utils/json.dart'; @@ -42,11 +38,10 @@ class BroadcastGameController extends _$BroadcastGameController Uri(path: 'study/$broadcastRoundId/socket/v6'); static AnalysisOptions get options => const AnalysisOptions( - id: standaloneBroadcastId, + id: StringId(''), isLocalEvaluationAllowed: true, orientation: Side.white, variant: Variant.standard, - isBroadcast: true, ); StreamSubscription? _subscription; @@ -74,7 +69,6 @@ class BroadcastGameController extends _$BroadcastGameController }); final evaluationService = ref.watch(evaluationServiceProvider); - final serverAnalysisService = ref.watch(serverAnalysisServiceProvider); final isEngineAllowed = options.isLocalEvaluationAllowed && engineSupportedVariants.contains(options.variant); @@ -85,13 +79,8 @@ class BroadcastGameController extends _$BroadcastGameController if (isEngineAllowed) { evaluationService.disposeEngine(); } - serverAnalysisService.lastAnalysisEvent - .removeListener(_listenToServerAnalysisEvents); }); - serverAnalysisService.lastAnalysisEvent - .addListener(_listenToServerAnalysisEvents); - UciPath path = UciPath.empty; Move? lastMove; @@ -156,9 +145,7 @@ class BroadcastGameController extends _$BroadcastGameController variant: options.variant, id: options.id, currentPath: currentPath, - broadcastLivePath: options.isBroadcast && pgnHeaders['Result'] == '*' - ? currentPath - : null, + broadcastLivePath: pgnHeaders['Result'] == '*' ? currentPath : null, isOnMainline: _root.isOnMainline(currentPath), root: _root.view, currentNode: AnalysisCurrentNode.fromNode(currentNode), @@ -170,10 +157,7 @@ class BroadcastGameController extends _$BroadcastGameController isLocalEvaluationAllowed: options.isLocalEvaluationAllowed, isLocalEvaluationEnabled: prefs.enableLocalEvaluation, displayMode: DisplayMode.moves, - playersAnalysis: options.serverAnalysis, - acplChartData: - options.serverAnalysis != null ? _makeAcplChartData() : null, - clocks: options.isBroadcast ? _makeClocks(currentPath) : null, + clocks: _makeClocks(currentPath), ); if (analysisState.isEngineAvailable) { @@ -491,19 +475,6 @@ class BroadcastGameController extends _$BroadcastGameController state = AsyncData(state.requireValue.copyWith(displayMode: mode)); } - Future requestServerAnalysis() async { - if (!state.hasValue) return; - - if (state.requireValue.canRequestServerAnalysis) { - final service = ref.read(serverAnalysisServiceProvider); - return service.requestAnalysis( - options.id as GameAnyId, - options.orientation, - ); - } - return Future.error('Cannot request server analysis'); - } - /// Gets the node and maybe the associated branch opening at the given path. (Node, Opening?) _nodeOpeningAt(Node node, UciPath path, [Opening? opening]) { if (path.isEmpty) return (node, opening); @@ -598,7 +569,7 @@ class BroadcastGameController extends _$BroadcastGameController lastMove: currentNode.sanMove.move, promotionMove: null, root: rootView, - clocks: options.isBroadcast ? _makeClocks(path) : null, + clocks: _makeClocks(path), ), ); } else { @@ -613,7 +584,7 @@ class BroadcastGameController extends _$BroadcastGameController lastMove: null, promotionMove: null, root: rootView, - clocks: options.isBroadcast ? _makeClocks(path) : null, + clocks: _makeClocks(path), ), ); } @@ -684,116 +655,6 @@ class BroadcastGameController extends _$BroadcastGameController ); } - void _listenToServerAnalysisEvents() { - if (!state.hasValue) Exception('Cannot export PGN'); - - final event = - ref.read(serverAnalysisServiceProvider).lastAnalysisEvent.value; - if (event != null && event.$1 == state.requireValue.id) { - _mergeOngoingAnalysis(_root, event.$2.tree); - state = AsyncData( - state.requireValue.copyWith( - acplChartData: _makeAcplChartData(), - playersAnalysis: event.$2.analysis != null - ? ( - white: event.$2.analysis!.white, - black: event.$2.analysis!.black - ) - : null, - root: _root.view, - ), - ); - } - } - - void _mergeOngoingAnalysis(Node n1, Map n2) { - final eval = n2['eval'] as Map?; - final cp = eval?['cp'] as int?; - final mate = eval?['mate'] as int?; - final pgnEval = cp != null - ? PgnEvaluation.pawns(pawns: cpToPawns(cp)) - : mate != null - ? PgnEvaluation.mate(mate: mate) - : null; - final glyphs = n2['glyphs'] as List?; - final glyph = glyphs?.first as Map?; - final comments = n2['comments'] as List?; - final comment = - (comments?.first as Map?)?['text'] as String?; - final children = n2['children'] as List? ?? []; - final pgnComment = - pgnEval != null ? PgnComment(eval: pgnEval, text: comment) : null; - if (n1 is Branch) { - if (pgnComment != null) { - if (n1.lichessAnalysisComments == null) { - n1.lichessAnalysisComments = [pgnComment]; - } else { - n1.lichessAnalysisComments!.removeWhere((c) => c.eval != null); - n1.lichessAnalysisComments!.add(pgnComment); - } - } - if (glyph != null) { - n1.nags ??= [glyph['id'] as int]; - } - } - for (final c in children) { - final n2child = c as Map; - final id = n2child['id'] as String; - final n1child = n1.childById(UciCharPair.fromStringId(id)); - if (n1child != null) { - _mergeOngoingAnalysis(n1child, n2child); - } else { - final uci = n2child['uci'] as String; - final san = n2child['san'] as String; - final move = Move.parse(uci)!; - n1.addChild( - Branch( - position: n1.position.playUnchecked(move), - sanMove: SanMove(san, move), - isHidden: children.length > 1, - ), - ); - } - } - } - - IList? _makeAcplChartData() { - if (!_root.mainline.any((node) => node.lichessAnalysisComments != null)) { - return null; - } - final list = _root.mainline - .map( - (node) => ( - node.position.isCheckmate, - node.position.turn, - node.lichessAnalysisComments - ?.firstWhereOrNull((c) => c.eval != null) - ?.eval - ), - ) - .map( - (el) { - final (isCheckmate, side, eval) = el; - return eval != null - ? ExternalEval( - cp: eval.pawns != null ? cpFromPawns(eval.pawns!) : null, - mate: eval.mate, - depth: eval.depth, - ) - : ExternalEval( - cp: null, - // hack to display checkmate as the max eval - mate: isCheckmate - ? side == Side.white - ? -1 - : 1 - : null, - ); - }, - ).toList(growable: false); - return list.isEmpty ? null : IList(list); - } - ({Duration? parentClock, Duration? clock}) _makeClocks(UciPath path) { final nodeView = _root.nodeAt(path).view; final parentView = _root.parentAt(path).view; @@ -805,11 +666,6 @@ class BroadcastGameController extends _$BroadcastGameController } } -enum DisplayMode { - moves, - summary, -} - @freezed class BroadcastGameState with _$BroadcastGameState { const BroadcastGameState._(); @@ -869,12 +725,6 @@ class BroadcastGameState with _$BroadcastGameState { /// The opening of the current branch. Opening? currentBranchOpening, - /// Optional server analysis to display player stats. - ({PlayerAnalysis white, PlayerAnalysis black})? playersAnalysis, - - /// Optional ACPL chart data of the game, coming from lichess server analysis. - IList? acplChartData, - /// The PGN headers of the game. required IMap pgnHeaders, @@ -889,21 +739,8 @@ class BroadcastGameState with _$BroadcastGameState { isChess960: variant == Variant.chess960, ); - /// Whether the user can request server analysis. - /// - /// It must be a lichess game, which is finished and not already analyzed. - bool get canRequestServerAnalysis => false; - - bool get canShowGameSummary => hasServerAnalysis || canRequestServerAnalysis; - - bool get hasServerAnalysis => playersAnalysis != null; - /// Whether an evaluation can be available - bool get hasAvailableEval => - isEngineAvailable || - (isLocalEvaluationAllowed && - acplChartData != null && - acplChartData!.isNotEmpty); + bool get hasAvailableEval => isEngineAvailable; /// Whether the engine is allowed for this analysis and variant. bool get isEngineAllowed => @@ -931,61 +768,3 @@ class BroadcastGameState with _$BroadcastGameState { initialMoveCursor: currentPath.size, ); } - -@freezed -class AnalysisCurrentNode with _$AnalysisCurrentNode { - const AnalysisCurrentNode._(); - - const factory AnalysisCurrentNode({ - required Position position, - required bool hasChild, - required bool isRoot, - SanMove? sanMove, - Opening? opening, - ClientEval? eval, - IList? lichessAnalysisComments, - IList? startingComments, - IList? comments, - IList? nags, - }) = _AnalysisCurrentNode; - - factory AnalysisCurrentNode.fromNode(Node node) { - if (node is Branch) { - return AnalysisCurrentNode( - sanMove: node.sanMove, - position: node.position, - isRoot: node is Root, - hasChild: node.children.isNotEmpty, - opening: node.opening, - eval: node.eval, - lichessAnalysisComments: IList(node.lichessAnalysisComments), - startingComments: IList(node.startingComments), - comments: IList(node.comments), - nags: IList(node.nags), - ); - } else { - return AnalysisCurrentNode( - position: node.position, - hasChild: node.children.isNotEmpty, - isRoot: node is Root, - opening: node.opening, - eval: node.eval, - ); - } - } - - /// The evaluation from the PGN comments. - /// - /// For now we only trust the eval coming from lichess analysis. - ExternalEval? get serverEval { - final pgnEval = - lichessAnalysisComments?.firstWhereOrNull((c) => c.eval != null)?.eval; - return pgnEval != null - ? ExternalEval( - cp: pgnEval.pawns != null ? cpFromPawns(pgnEval.pawns!) : null, - mate: pgnEval.mate, - depth: pgnEval.depth, - ) - : null; - } -} diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index 200f1747f6..a87c4155fa 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -1,19 +1,51 @@ +import 'dart:math' as math; + +import 'package:collection/collection.dart'; +import 'package:dartchess/dartchess.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; +import 'package:lichess_mobile/src/model/analysis/server_analysis_service.dart'; +import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/engine/engine.dart'; +import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; import 'package:lichess_mobile/src/model/game/game_repository_providers.dart'; +import 'package:lichess_mobile/src/model/game/game_share_service.dart'; +import 'package:lichess_mobile/src/network/connectivity.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; -import 'package:lichess_mobile/src/view/analysis/analysis_screen_body.dart'; -import 'package:lichess_mobile/src/view/analysis/analysis_settings.dart'; -import 'package:lichess_mobile/src/view/analysis/engine_depth.dart'; +import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/utils/screen.dart'; +import 'package:lichess_mobile/src/utils/string.dart'; +import 'package:lichess_mobile/src/view/analysis/analysis_share_screen.dart'; +import 'package:lichess_mobile/src/view/board_editor/board_editor_screen.dart'; +import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; +import 'package:lichess_mobile/src/view/engine/engine_lines.dart'; +import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_screen.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; +import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; +import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/feedback.dart'; +import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; +import 'package:popover/popover.dart'; + +import '../../utils/share.dart'; +import 'analysis_board.dart'; +import 'analysis_settings.dart'; +import 'tree_view.dart'; class AnalysisScreen extends StatelessWidget { const AnalysisScreen({ @@ -118,7 +150,7 @@ class _LoadedAnalysisScreen extends ConsumerWidget { appBar: PlatformAppBar( title: _Title(options: options), actions: [ - EngineDepth(ctrlProvider), + _EngineDepth(ctrlProvider), AppBarIconButton( onPressed: () => showAdaptiveBottomSheet( context: context, @@ -132,7 +164,7 @@ class _LoadedAnalysisScreen extends ConsumerWidget { ), ], ), - body: AnalysisScreenBody( + body: _Body( pgn: pgn, options: options, enableDrawingShapes: enableDrawingShapes, @@ -151,7 +183,7 @@ class _LoadedAnalysisScreen extends ConsumerWidget { trailing: Row( mainAxisSize: MainAxisSize.min, children: [ - EngineDepth(ctrlProvider), + _EngineDepth(ctrlProvider), AppBarIconButton( onPressed: () => showAdaptiveBottomSheet( context: context, @@ -166,7 +198,7 @@ class _LoadedAnalysisScreen extends ConsumerWidget { ], ), ), - child: AnalysisScreenBody( + child: _Body( pgn: pgn, options: options, enableDrawingShapes: enableDrawingShapes, @@ -195,3 +227,1016 @@ class _Title extends StatelessWidget { ); } } + +class _Body extends ConsumerWidget { + const _Body({ + required this.pgn, + required this.options, + required this.enableDrawingShapes, + }); + + final String pgn; + final AnalysisOptions options; + final bool enableDrawingShapes; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final ctrlProvider = analysisControllerProvider(pgn, options); + final showEvaluationGauge = ref.watch( + analysisPreferencesProvider.select((value) => value.showEvaluationGauge), + ); + + final isEngineAvailable = ref.watch( + ctrlProvider.select( + (value) => value.isEngineAvailable, + ), + ); + + final hasEval = + ref.watch(ctrlProvider.select((value) => value.hasAvailableEval)); + + final displayMode = + ref.watch(ctrlProvider.select((value) => value.displayMode)); + + final currentNode = ref.watch( + ctrlProvider.select((value) => value.currentNode), + ); + + return Column( + children: [ + Expanded( + child: SafeArea( + bottom: false, + child: LayoutBuilder( + builder: (context, constraints) { + final aspectRatio = constraints.biggest.aspectRatio; + final defaultBoardSize = constraints.biggest.shortestSide; + final isTablet = isTabletOrLarger(context); + final remainingHeight = + constraints.maxHeight - defaultBoardSize; + final isSmallScreen = + remainingHeight < kSmallRemainingHeightLeftBoardThreshold; + final boardSize = isTablet || isSmallScreen + ? defaultBoardSize - kTabletBoardTableSidePadding * 2 + : defaultBoardSize; + + const tabletBoardRadius = + BorderRadius.all(Radius.circular(4.0)); + + final display = switch (displayMode) { + DisplayMode.summary => ServerAnalysisSummary(pgn, options), + DisplayMode.moves => AnalysisTreeView( + pgn, + options, + aspectRatio > 1 + ? Orientation.landscape + : Orientation.portrait, + ), + }; + + // If the aspect ratio is greater than 1, we are in landscape mode. + if (aspectRatio > 1) { + return Row( + mainAxisSize: MainAxisSize.max, + children: [ + Padding( + padding: const EdgeInsets.only( + left: kTabletBoardTableSidePadding, + top: kTabletBoardTableSidePadding, + bottom: kTabletBoardTableSidePadding, + ), + child: Row( + children: [ + AnalysisBoard( + pgn, + options, + boardSize, + borderRadius: isTablet ? tabletBoardRadius : null, + enableDrawingShapes: enableDrawingShapes, + ), + if (hasEval && showEvaluationGauge) ...[ + const SizedBox(width: 4.0), + _EngineGaugeVertical(ctrlProvider), + ], + ], + ), + ), + Flexible( + fit: FlexFit.loose, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + if (isEngineAvailable) + Padding( + padding: const EdgeInsets.all( + kTabletBoardTableSidePadding, + ), + child: EngineLines( + onTapMove: ref + .read(ctrlProvider.notifier) + .onUserMove, + clientEval: currentNode.eval, + isGameOver: currentNode.position.isGameOver, + ), + ), + Expanded( + child: PlatformCard( + clipBehavior: Clip.hardEdge, + borderRadius: const BorderRadius.all( + Radius.circular(4.0), + ), + margin: const EdgeInsets.all( + kTabletBoardTableSidePadding, + ), + semanticContainer: false, + child: display, + ), + ), + ], + ), + ), + ], + ); + } + // If the aspect ratio is less than 1, we are in portrait mode. + else { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + _ColumnTopTable(ctrlProvider), + if (isTablet) + Padding( + padding: const EdgeInsets.all( + kTabletBoardTableSidePadding, + ), + child: AnalysisBoard( + pgn, + options, + boardSize, + borderRadius: isTablet ? tabletBoardRadius : null, + enableDrawingShapes: enableDrawingShapes, + ), + ) + else + AnalysisBoard( + pgn, + options, + boardSize, + borderRadius: isTablet ? tabletBoardRadius : null, + enableDrawingShapes: enableDrawingShapes, + ), + Expanded( + child: Padding( + padding: isTablet + ? const EdgeInsets.symmetric( + horizontal: kTabletBoardTableSidePadding, + ) + : EdgeInsets.zero, + child: display, + ), + ), + ], + ); + } + }, + ), + ), + ), + _BottomBar(pgn: pgn, options: options), + ], + ); + } +} + +class _EngineGaugeVertical extends ConsumerWidget { + const _EngineGaugeVertical(this.ctrlProvider); + + final AnalysisControllerProvider ctrlProvider; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final analysisState = ref.watch(ctrlProvider); + + return Container( + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4.0), + ), + child: EngineGauge( + displayMode: EngineGaugeDisplayMode.vertical, + params: analysisState.engineGaugeParams, + ), + ); + } +} + +class _ColumnTopTable extends ConsumerWidget { + const _ColumnTopTable(this.ctrlProvider); + + final AnalysisControllerProvider ctrlProvider; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final analysisState = ref.watch(ctrlProvider); + final showEvaluationGauge = ref.watch( + analysisPreferencesProvider.select((p) => p.showEvaluationGauge), + ); + + return analysisState.hasAvailableEval + ? Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (showEvaluationGauge) + EngineGauge( + displayMode: EngineGaugeDisplayMode.horizontal, + params: analysisState.engineGaugeParams, + ), + if (analysisState.isEngineAvailable) + EngineLines( + clientEval: analysisState.currentNode.eval, + isGameOver: analysisState.currentNode.position.isGameOver, + onTapMove: ref.read(ctrlProvider.notifier).onUserMove, + ), + ], + ) + : kEmptyWidget; + } +} + +class _BottomBar extends ConsumerWidget { + const _BottomBar({ + required this.pgn, + required this.options, + }); + + final String pgn; + final AnalysisOptions options; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final ctrlProvider = analysisControllerProvider(pgn, options); + final analysisState = ref.watch(ctrlProvider); + final isOnline = + ref.watch(connectivityChangesProvider).valueOrNull?.isOnline ?? false; + + return BottomBar( + children: [ + BottomBarButton( + label: context.l10n.menu, + onTap: () { + _showAnalysisMenu(context, ref); + }, + icon: Icons.menu, + ), + if (analysisState.canShowGameSummary) + BottomBarButton( + // TODO: l10n + label: analysisState.displayMode == DisplayMode.summary + ? 'Moves' + : 'Summary', + onTap: () { + final newMode = analysisState.displayMode == DisplayMode.summary + ? DisplayMode.moves + : DisplayMode.summary; + ref.read(ctrlProvider.notifier).setDisplayMode(newMode); + }, + icon: analysisState.displayMode == DisplayMode.summary + ? LichessIcons.flow_cascade + : Icons.area_chart, + ), + BottomBarButton( + label: context.l10n.openingExplorer, + onTap: isOnline + ? () { + pushPlatformRoute( + context, + title: context.l10n.openingExplorer, + builder: (_) => OpeningExplorerScreen( + pgn: ref.read(ctrlProvider.notifier).makeCurrentNodePgn(), + options: analysisState.openingExplorerOptions, + ), + ); + } + : null, + icon: Icons.explore, + ), + RepeatButton( + onLongPress: + analysisState.canGoBack ? () => _moveBackward(ref) : null, + child: BottomBarButton( + key: const ValueKey('goto-previous'), + onTap: analysisState.canGoBack ? () => _moveBackward(ref) : null, + label: 'Previous', + icon: CupertinoIcons.chevron_back, + showTooltip: false, + ), + ), + RepeatButton( + onLongPress: analysisState.canGoNext ? () => _moveForward(ref) : null, + child: BottomBarButton( + key: const ValueKey('goto-next'), + icon: CupertinoIcons.chevron_forward, + label: context.l10n.next, + onTap: analysisState.canGoNext ? () => _moveForward(ref) : null, + showTooltip: false, + ), + ), + ], + ); + } + + void _moveForward(WidgetRef ref) => + ref.read(analysisControllerProvider(pgn, options).notifier).userNext(); + void _moveBackward(WidgetRef ref) => ref + .read(analysisControllerProvider(pgn, options).notifier) + .userPrevious(); + + Future _showAnalysisMenu(BuildContext context, WidgetRef ref) { + return showAdaptiveActionSheet( + context: context, + actions: [ + BottomSheetAction( + makeLabel: (context) => Text(context.l10n.flipBoard), + onPressed: (context) { + ref + .read(analysisControllerProvider(pgn, options).notifier) + .toggleBoard(); + }, + ), + BottomSheetAction( + makeLabel: (context) => Text(context.l10n.boardEditor), + onPressed: (context) { + final analysisState = + ref.read(analysisControllerProvider(pgn, options)); + final boardFen = analysisState.position.fen; + pushPlatformRoute( + context, + title: context.l10n.boardEditor, + builder: (_) => BoardEditorScreen( + initialFen: boardFen, + ), + ); + }, + ), + BottomSheetAction( + makeLabel: (context) => Text(context.l10n.mobileShareGamePGN), + onPressed: (_) { + pushPlatformRoute( + context, + title: context.l10n.studyShareAndExport, + builder: (_) => AnalysisShareScreen(pgn: pgn, options: options), + ); + }, + ), + BottomSheetAction( + makeLabel: (context) => Text(context.l10n.mobileSharePositionAsFEN), + onPressed: (_) { + final analysisState = + ref.read(analysisControllerProvider(pgn, options)); + launchShareDialog( + context, + text: analysisState.position.fen, + ); + }, + ), + if (options.gameAnyId != null) + BottomSheetAction( + makeLabel: (context) => + Text(context.l10n.screenshotCurrentPosition), + onPressed: (_) async { + final gameId = options.gameAnyId!.gameId; + final analysisState = + ref.read(analysisControllerProvider(pgn, options)); + try { + final image = + await ref.read(gameShareServiceProvider).screenshotPosition( + gameId, + options.orientation, + analysisState.position.fen, + analysisState.lastMove, + ); + if (context.mounted) { + launchShareDialog( + context, + files: [image], + subject: context.l10n.puzzleFromGameLink( + lichessUri('/$gameId').toString(), + ), + ); + } + } catch (e) { + if (context.mounted) { + showPlatformSnackbar( + context, + 'Failed to get GIF', + type: SnackBarType.error, + ); + } + } + }, + ), + ], + ); + } +} + +class _EngineDepth extends ConsumerWidget { + const _EngineDepth(this.ctrlProvider); + + final AnalysisControllerProvider ctrlProvider; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isEngineAvailable = ref.watch( + ctrlProvider.select( + (value) => value.isEngineAvailable, + ), + ); + final currentNode = ref.watch( + ctrlProvider.select((value) => value.currentNode), + ); + final depth = ref.watch( + engineEvaluationProvider.select((value) => value.eval?.depth), + ) ?? + currentNode.eval?.depth; + + return isEngineAvailable && depth != null + ? AppBarTextButton( + onPressed: () { + showPopover( + context: context, + bodyBuilder: (context) { + return _StockfishInfo(currentNode); + }, + direction: PopoverDirection.top, + width: 240, + backgroundColor: + Theme.of(context).platform == TargetPlatform.android + ? Theme.of(context).dialogBackgroundColor + : CupertinoDynamicColor.resolve( + CupertinoColors.tertiarySystemBackground, + context, + ), + transitionDuration: Duration.zero, + popoverTransitionBuilder: (_, child) => child, + ); + }, + child: RepaintBoundary( + child: Container( + width: 20.0, + height: 20.0, + padding: const EdgeInsets.all(2.0), + decoration: BoxDecoration( + color: Theme.of(context).platform == TargetPlatform.android + ? Theme.of(context).colorScheme.secondary + : CupertinoTheme.of(context).primaryColor, + borderRadius: BorderRadius.circular(4.0), + ), + child: FittedBox( + fit: BoxFit.contain, + child: Text( + '${math.min(99, depth)}', + style: TextStyle( + color: Theme.of(context).platform == + TargetPlatform.android + ? Theme.of(context).colorScheme.onSecondary + : CupertinoTheme.of(context).primaryContrastingColor, + fontFeatures: const [ + FontFeature.tabularFigures(), + ], + ), + ), + ), + ), + ), + ) + : const SizedBox.shrink(); + } +} + +class _StockfishInfo extends ConsumerWidget { + const _StockfishInfo(this.currentNode); + + final AnalysisCurrentNode currentNode; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final (engineName: engineName, eval: eval, state: engineState) = + ref.watch(engineEvaluationProvider); + + final currentEval = eval ?? currentNode.eval; + + final knps = engineState == EngineState.computing + ? ', ${eval?.knps.round()}kn/s' + : ''; + final depth = currentEval?.depth ?? 0; + final maxDepth = math.max(depth, kMaxEngineDepth); + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + PlatformListTile( + leading: Image.asset( + 'assets/images/stockfish/icon.png', + width: 44, + height: 44, + ), + title: Text(engineName), + subtitle: Text( + context.l10n.depthX( + '$depth/$maxDepth$knps', + ), + ), + ), + ], + ); + } +} + +class ServerAnalysisSummary extends ConsumerWidget { + const ServerAnalysisSummary(this.pgn, this.options); + + final String pgn; + final AnalysisOptions options; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final ctrlProvider = analysisControllerProvider(pgn, options); + final playersAnalysis = + ref.watch(ctrlProvider.select((value) => value.playersAnalysis)); + final pgnHeaders = + ref.watch(ctrlProvider.select((value) => value.pgnHeaders)); + final currentGameAnalysis = ref.watch(currentAnalysisProvider); + + return playersAnalysis != null + ? ListView( + children: [ + if (currentGameAnalysis == options.gameAnyId?.gameId) + const Padding( + padding: EdgeInsets.only(top: 16.0), + child: WaitingForServerAnalysis(), + ), + AcplChart(pgn, options), + Center( + child: SizedBox( + width: math.min(MediaQuery.sizeOf(context).width, 500), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Table( + defaultVerticalAlignment: + TableCellVerticalAlignment.middle, + columnWidths: const { + 0: FlexColumnWidth(1), + 1: FlexColumnWidth(1), + 2: FlexColumnWidth(1), + }, + children: [ + TableRow( + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide(color: Colors.grey), + ), + ), + children: [ + _SummaryPlayerName(Side.white, pgnHeaders), + Center( + child: Text( + pgnHeaders.get('Result') ?? '', + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + ), + _SummaryPlayerName(Side.black, pgnHeaders), + ], + ), + if (playersAnalysis.white.accuracy != null && + playersAnalysis.black.accuracy != null) + TableRow( + children: [ + _SummaryNumber( + '${playersAnalysis.white.accuracy}%', + ), + Center( + heightFactor: 1.8, + child: Text( + context.l10n.accuracy, + softWrap: true, + ), + ), + _SummaryNumber( + '${playersAnalysis.black.accuracy}%', + ), + ], + ), + for (final item in [ + ( + playersAnalysis.white.inaccuracies.toString(), + context.l10n + .nbInaccuracies(2) + .replaceAll('2', '') + .trim() + .capitalize(), + playersAnalysis.black.inaccuracies.toString() + ), + ( + playersAnalysis.white.mistakes.toString(), + context.l10n + .nbMistakes(2) + .replaceAll('2', '') + .trim() + .capitalize(), + playersAnalysis.black.mistakes.toString() + ), + ( + playersAnalysis.white.blunders.toString(), + context.l10n + .nbBlunders(2) + .replaceAll('2', '') + .trim() + .capitalize(), + playersAnalysis.black.blunders.toString() + ), + ]) + TableRow( + children: [ + _SummaryNumber(item.$1), + Center( + heightFactor: 1.2, + child: Text( + item.$2, + softWrap: true, + ), + ), + _SummaryNumber(item.$3), + ], + ), + if (playersAnalysis.white.acpl != null && + playersAnalysis.black.acpl != null) + TableRow( + children: [ + _SummaryNumber( + playersAnalysis.white.acpl.toString(), + ), + Center( + heightFactor: 1.5, + child: Text( + context.l10n.averageCentipawnLoss, + softWrap: true, + textAlign: TextAlign.center, + ), + ), + _SummaryNumber( + playersAnalysis.black.acpl.toString(), + ), + ], + ), + ], + ), + ), + ), + ), + ], + ) + : Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Spacer(), + if (currentGameAnalysis == options.gameAnyId?.gameId) + const Center( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 16.0), + child: WaitingForServerAnalysis(), + ), + ) + else + Center( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Builder( + builder: (context) { + Future? pendingRequest; + return StatefulBuilder( + builder: (context, setState) { + return FutureBuilder( + future: pendingRequest, + builder: (context, snapshot) { + return SecondaryButton( + semanticsLabel: + context.l10n.requestAComputerAnalysis, + onPressed: ref.watch(authSessionProvider) == + null + ? () { + showPlatformSnackbar( + context, + context + .l10n.youNeedAnAccountToDoThat, + ); + } + : snapshot.connectionState == + ConnectionState.waiting + ? null + : () { + setState(() { + pendingRequest = ref + .read(ctrlProvider.notifier) + .requestServerAnalysis() + .catchError((Object e) { + if (context.mounted) { + showPlatformSnackbar( + context, + e.toString(), + type: SnackBarType.error, + ); + } + }); + }); + }, + child: Text( + context.l10n.requestAComputerAnalysis, + ), + ); + }, + ); + }, + ); + }, + ), + ), + ), + const Spacer(), + ], + ); + } +} + +class WaitingForServerAnalysis extends StatelessWidget { + const WaitingForServerAnalysis({super.key}); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ + Image.asset( + 'assets/images/stockfish/icon.png', + width: 30, + height: 30, + ), + const SizedBox(width: 8.0), + Text(context.l10n.waitingForAnalysis), + const SizedBox(width: 8.0), + const CircularProgressIndicator.adaptive(), + ], + ); + } +} + +class _SummaryNumber extends StatelessWidget { + const _SummaryNumber(this.data); + final String data; + + @override + Widget build(BuildContext context) { + return Center( + child: Text( + data, + softWrap: true, + ), + ); + } +} + +class _SummaryPlayerName extends StatelessWidget { + const _SummaryPlayerName(this.side, this.pgnHeaders); + final Side side; + final IMap pgnHeaders; + + @override + Widget build(BuildContext context) { + final playerTitle = side == Side.white + ? pgnHeaders.get('WhiteTitle') + : pgnHeaders.get('BlackTitle'); + final playerName = side == Side.white + ? pgnHeaders.get('White') ?? context.l10n.white + : pgnHeaders.get('Black') ?? context.l10n.black; + + final brightness = Theme.of(context).brightness; + + return TableCell( + verticalAlignment: TableCellVerticalAlignment.top, + child: Center( + child: Padding( + padding: const EdgeInsets.only(bottom: 5), + child: Column( + children: [ + Icon( + side == Side.white + ? brightness == Brightness.light + ? CupertinoIcons.circle + : CupertinoIcons.circle_filled + : brightness == Brightness.light + ? CupertinoIcons.circle_filled + : CupertinoIcons.circle, + size: 14, + ), + Text( + '${playerTitle != null ? '$playerTitle ' : ''}$playerName', + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + softWrap: true, + ), + ], + ), + ), + ), + ); + } +} + +class AcplChart extends ConsumerWidget { + const AcplChart(this.pgn, this.options); + + final String pgn; + final AnalysisOptions options; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final mainLineColor = Theme.of(context).colorScheme.secondary; + // yes it looks like below/above are inverted in fl_chart + final brightness = Theme.of(context).brightness; + final white = Theme.of(context).colorScheme.surfaceContainerHighest; + final black = Theme.of(context).colorScheme.outline; + // yes it looks like below/above are inverted in fl_chart + final belowLineColor = brightness == Brightness.light ? white : black; + final aboveLineColor = brightness == Brightness.light ? black : white; + + VerticalLine phaseVerticalBar(double x, String label) => VerticalLine( + x: x, + color: const Color(0xFF707070), + strokeWidth: 0.5, + label: VerticalLineLabel( + style: TextStyle( + fontSize: 10, + color: Theme.of(context) + .textTheme + .labelMedium + ?.color + ?.withValues(alpha: 0.3), + ), + labelResolver: (line) => label, + padding: const EdgeInsets.only(right: 1), + alignment: Alignment.topRight, + direction: LabelDirection.vertical, + show: true, + ), + ); + + final data = ref.watch( + analysisControllerProvider(pgn, options) + .select((value) => value.acplChartData), + ); + + final rootPly = ref.watch( + analysisControllerProvider(pgn, options) + .select((value) => value.root.position.ply), + ); + + final currentNode = ref.watch( + analysisControllerProvider(pgn, options) + .select((value) => value.currentNode), + ); + + final isOnMainline = ref.watch( + analysisControllerProvider(pgn, options) + .select((value) => value.isOnMainline), + ); + + if (data == null) { + return const SizedBox.shrink(); + } + + final spots = data + .mapIndexed( + (i, e) => FlSpot(i.toDouble(), e.winningChances(Side.white)), + ) + .toList(growable: false); + + final divisionLines = []; + + if (options.division?.middlegame != null) { + if (options.division!.middlegame! > 0) { + divisionLines.add(phaseVerticalBar(0.0, context.l10n.opening)); + divisionLines.add( + phaseVerticalBar( + options.division!.middlegame! - 1, + context.l10n.middlegame, + ), + ); + } else { + divisionLines.add(phaseVerticalBar(0.0, context.l10n.middlegame)); + } + } + + if (options.division?.endgame != null) { + if (options.division!.endgame! > 0) { + divisionLines.add( + phaseVerticalBar( + options.division!.endgame! - 1, + context.l10n.endgame, + ), + ); + } else { + divisionLines.add( + phaseVerticalBar( + 0.0, + context.l10n.endgame, + ), + ); + } + } + return Center( + child: AspectRatio( + aspectRatio: 2.5, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: LineChart( + LineChartData( + lineTouchData: LineTouchData( + enabled: false, + touchCallback: + (FlTouchEvent event, LineTouchResponse? touchResponse) { + if (event is FlTapDownEvent || + event is FlPanUpdateEvent || + event is FlLongPressMoveUpdate) { + final touchX = event.localPosition!.dx; + final chartWidth = context.size!.width - + 32; // Insets on both sides of the chart of 16 + final minX = spots.first.x; + final maxX = spots.last.x; + final touchXDataValue = + minX + (touchX / chartWidth) * (maxX - minX); + final closestSpot = spots.reduce( + (a, b) => (a.x - touchXDataValue).abs() < + (b.x - touchXDataValue).abs() + ? a + : b, + ); + final closestNodeIndex = closestSpot.x.round(); + ref + .read(analysisControllerProvider(pgn, options).notifier) + .jumpToNthNodeOnMainline(closestNodeIndex); + } + }, + ), + minY: -1.0, + maxY: 1.0, + lineBarsData: [ + LineChartBarData( + spots: spots, + isCurved: false, + barWidth: 1, + color: mainLineColor.withValues(alpha: 0.7), + aboveBarData: BarAreaData( + show: true, + color: aboveLineColor, + applyCutOffY: true, + ), + belowBarData: BarAreaData( + show: true, + color: belowLineColor, + applyCutOffY: true, + ), + dotData: const FlDotData( + show: false, + ), + ), + ], + extraLinesData: ExtraLinesData( + verticalLines: [ + if (isOnMainline) + VerticalLine( + x: (currentNode.position.ply - 1 - rootPly).toDouble(), + color: mainLineColor, + strokeWidth: 1.0, + ), + ...divisionLines, + ], + ), + gridData: const FlGridData(show: false), + borderData: FlBorderData(show: false), + titlesData: const FlTitlesData(show: false), + ), + ), + ), + ), + ); + } +} diff --git a/lib/src/view/analysis/analysis_screen_body.dart b/lib/src/view/analysis/analysis_screen_body.dart deleted file mode 100644 index 24d09051be..0000000000 --- a/lib/src/view/analysis/analysis_screen_body.dart +++ /dev/null @@ -1,948 +0,0 @@ -import 'dart:math' as math; - -import 'package:collection/collection.dart'; -import 'package:dartchess/dartchess.dart'; -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:fl_chart/fl_chart.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lichess_mobile/src/constants.dart'; -import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; -import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; -import 'package:lichess_mobile/src/model/analysis/server_analysis_service.dart'; -import 'package:lichess_mobile/src/model/auth/auth_session.dart'; -import 'package:lichess_mobile/src/model/game/game_share_service.dart'; -import 'package:lichess_mobile/src/network/connectivity.dart'; -import 'package:lichess_mobile/src/network/http.dart'; -import 'package:lichess_mobile/src/styles/lichess_icons.dart'; -import 'package:lichess_mobile/src/utils/l10n_context.dart'; -import 'package:lichess_mobile/src/utils/navigation.dart'; -import 'package:lichess_mobile/src/utils/screen.dart'; -import 'package:lichess_mobile/src/utils/share.dart'; -import 'package:lichess_mobile/src/utils/string.dart'; -import 'package:lichess_mobile/src/view/analysis/analysis_board.dart'; -import 'package:lichess_mobile/src/view/analysis/analysis_share_screen.dart'; -import 'package:lichess_mobile/src/view/analysis/tree_view.dart'; -import 'package:lichess_mobile/src/view/board_editor/board_editor_screen.dart'; -import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; -import 'package:lichess_mobile/src/view/engine/engine_lines.dart'; -import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_screen.dart'; -import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; -import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; -import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; -import 'package:lichess_mobile/src/widgets/buttons.dart'; -import 'package:lichess_mobile/src/widgets/feedback.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; - -class AnalysisScreenBody extends ConsumerWidget { - const AnalysisScreenBody({ - required this.pgn, - required this.options, - required this.enableDrawingShapes, - this.broadcastWrapBoardBuilder, - }); - - final String pgn; - final AnalysisOptions options; - final bool enableDrawingShapes; - final Widget Function( - WidgetRef ref, - AnalysisBoard board, - AnalysisControllerProvider ctrlProvider, - double boardSize, - )? broadcastWrapBoardBuilder; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final ctrlProvider = analysisControllerProvider(pgn, options); - final showEvaluationGauge = ref.watch( - analysisPreferencesProvider.select((value) => value.showEvaluationGauge), - ); - - final isEngineAvailable = ref.watch( - ctrlProvider.select( - (value) => value.isEngineAvailable, - ), - ); - - final hasEval = - ref.watch(ctrlProvider.select((value) => value.hasAvailableEval)); - - final displayMode = - ref.watch(ctrlProvider.select((value) => value.displayMode)); - - final currentNode = ref.watch( - ctrlProvider.select((value) => value.currentNode), - ); - - return Column( - children: [ - Expanded( - child: SafeArea( - bottom: false, - child: LayoutBuilder( - builder: (context, constraints) { - final aspectRatio = constraints.biggest.aspectRatio; - final defaultBoardSize = constraints.biggest.shortestSide; - final isTablet = isTabletOrLarger(context); - final remainingHeight = - constraints.maxHeight - defaultBoardSize; - final isSmallScreen = - remainingHeight < kSmallRemainingHeightLeftBoardThreshold; - final boardSize = isTablet || isSmallScreen - ? defaultBoardSize - kTabletBoardTableSidePadding * 2 - : defaultBoardSize; - - const tabletBoardRadius = - BorderRadius.all(Radius.circular(4.0)); - - final display = switch (displayMode) { - DisplayMode.summary => ServerAnalysisSummary(pgn, options), - DisplayMode.moves => AnalysisTreeView( - pgn, - options, - aspectRatio > 1 - ? Orientation.landscape - : Orientation.portrait, - ), - }; - - Widget maybeWrapBoardBuilder(AnalysisBoard board) { - if (broadcastWrapBoardBuilder == null) { - return board; - } else { - return broadcastWrapBoardBuilder!( - ref, - board, - ctrlProvider, - boardSize, - ); - } - } - - final maybeWrappedBoard = maybeWrapBoardBuilder( - AnalysisBoard( - pgn, - options, - boardSize, - borderRadius: isTablet ? tabletBoardRadius : null, - enableDrawingShapes: enableDrawingShapes, - ), - ); - - // If the aspect ratio is greater than 1, we are in landscape mode. - if (aspectRatio > 1) { - return Row( - mainAxisSize: MainAxisSize.max, - children: [ - Padding( - padding: const EdgeInsets.only( - left: kTabletBoardTableSidePadding, - top: kTabletBoardTableSidePadding, - bottom: kTabletBoardTableSidePadding, - ), - child: Row( - children: [ - maybeWrappedBoard, - if (hasEval && showEvaluationGauge) ...[ - const SizedBox(width: 4.0), - _EngineGaugeVertical(ctrlProvider), - ], - ], - ), - ), - Flexible( - fit: FlexFit.loose, - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - if (isEngineAvailable) - Padding( - padding: const EdgeInsets.all( - kTabletBoardTableSidePadding, - ), - child: EngineLines( - onTapMove: ref - .read(ctrlProvider.notifier) - .onUserMove, - clientEval: currentNode.eval, - isGameOver: currentNode.position.isGameOver, - ), - ), - Expanded( - child: PlatformCard( - clipBehavior: Clip.hardEdge, - borderRadius: const BorderRadius.all( - Radius.circular(4.0), - ), - margin: const EdgeInsets.all( - kTabletBoardTableSidePadding, - ), - semanticContainer: false, - child: display, - ), - ), - ], - ), - ), - ], - ); - } - // If the aspect ratio is less than 1, we are in portrait mode. - else { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - _ColumnTopTable(ctrlProvider), - if (isTablet) - Padding( - padding: const EdgeInsets.all( - kTabletBoardTableSidePadding, - ), - child: maybeWrappedBoard, - ) - else - maybeWrappedBoard, - Expanded( - child: Padding( - padding: isTablet - ? const EdgeInsets.symmetric( - horizontal: kTabletBoardTableSidePadding, - ) - : EdgeInsets.zero, - child: display, - ), - ), - ], - ); - } - }, - ), - ), - ), - _BottomBar(pgn: pgn, options: options), - ], - ); - } -} - -class _EngineGaugeVertical extends ConsumerWidget { - const _EngineGaugeVertical(this.ctrlProvider); - - final AnalysisControllerProvider ctrlProvider; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final analysisState = ref.watch(ctrlProvider); - - return Container( - clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4.0), - ), - child: EngineGauge( - displayMode: EngineGaugeDisplayMode.vertical, - params: analysisState.engineGaugeParams, - ), - ); - } -} - -class _ColumnTopTable extends ConsumerWidget { - const _ColumnTopTable(this.ctrlProvider); - - final AnalysisControllerProvider ctrlProvider; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final analysisState = ref.watch(ctrlProvider); - final showEvaluationGauge = ref.watch( - analysisPreferencesProvider.select((p) => p.showEvaluationGauge), - ); - - return analysisState.hasAvailableEval - ? Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (showEvaluationGauge) - EngineGauge( - displayMode: EngineGaugeDisplayMode.horizontal, - params: analysisState.engineGaugeParams, - ), - if (analysisState.isEngineAvailable) - EngineLines( - clientEval: analysisState.currentNode.eval, - isGameOver: analysisState.currentNode.position.isGameOver, - onTapMove: ref.read(ctrlProvider.notifier).onUserMove, - ), - ], - ) - : kEmptyWidget; - } -} - -class _BottomBar extends ConsumerWidget { - const _BottomBar({ - required this.pgn, - required this.options, - }); - - final String pgn; - final AnalysisOptions options; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final ctrlProvider = analysisControllerProvider(pgn, options); - final analysisState = ref.watch(ctrlProvider); - final isOnline = - ref.watch(connectivityChangesProvider).valueOrNull?.isOnline ?? false; - - return BottomBar( - children: [ - BottomBarButton( - label: context.l10n.menu, - onTap: () { - _showAnalysisMenu(context, ref); - }, - icon: Icons.menu, - ), - if (analysisState.canShowGameSummary) - BottomBarButton( - // TODO: l10n - label: analysisState.displayMode == DisplayMode.summary - ? 'Moves' - : 'Summary', - onTap: () { - final newMode = analysisState.displayMode == DisplayMode.summary - ? DisplayMode.moves - : DisplayMode.summary; - ref.read(ctrlProvider.notifier).setDisplayMode(newMode); - }, - icon: analysisState.displayMode == DisplayMode.summary - ? LichessIcons.flow_cascade - : Icons.area_chart, - ), - BottomBarButton( - label: context.l10n.openingExplorer, - onTap: isOnline - ? () { - pushPlatformRoute( - context, - title: context.l10n.openingExplorer, - builder: (_) => OpeningExplorerScreen( - pgn: ref.read(ctrlProvider.notifier).makeCurrentNodePgn(), - options: analysisState.openingExplorerOptions, - ), - ); - } - : null, - icon: Icons.explore, - ), - RepeatButton( - onLongPress: - analysisState.canGoBack ? () => _moveBackward(ref) : null, - child: BottomBarButton( - key: const ValueKey('goto-previous'), - onTap: analysisState.canGoBack ? () => _moveBackward(ref) : null, - label: 'Previous', - icon: CupertinoIcons.chevron_back, - showTooltip: false, - ), - ), - RepeatButton( - onLongPress: analysisState.canGoNext ? () => _moveForward(ref) : null, - child: BottomBarButton( - key: const ValueKey('goto-next'), - icon: CupertinoIcons.chevron_forward, - label: context.l10n.next, - onTap: analysisState.canGoNext ? () => _moveForward(ref) : null, - showTooltip: false, - ), - ), - ], - ); - } - - void _moveForward(WidgetRef ref) => - ref.read(analysisControllerProvider(pgn, options).notifier).userNext(); - void _moveBackward(WidgetRef ref) => ref - .read(analysisControllerProvider(pgn, options).notifier) - .userPrevious(); - - Future _showAnalysisMenu(BuildContext context, WidgetRef ref) { - return showAdaptiveActionSheet( - context: context, - actions: [ - BottomSheetAction( - makeLabel: (context) => Text(context.l10n.flipBoard), - onPressed: (context) { - ref - .read(analysisControllerProvider(pgn, options).notifier) - .toggleBoard(); - }, - ), - BottomSheetAction( - makeLabel: (context) => Text(context.l10n.boardEditor), - onPressed: (context) { - final analysisState = - ref.read(analysisControllerProvider(pgn, options)); - final boardFen = analysisState.position.fen; - pushPlatformRoute( - context, - title: context.l10n.boardEditor, - builder: (_) => BoardEditorScreen( - initialFen: boardFen, - ), - ); - }, - ), - BottomSheetAction( - makeLabel: (context) => Text(context.l10n.mobileShareGamePGN), - onPressed: (_) { - pushPlatformRoute( - context, - title: context.l10n.studyShareAndExport, - builder: (_) => AnalysisShareScreen(pgn: pgn, options: options), - ); - }, - ), - BottomSheetAction( - makeLabel: (context) => Text(context.l10n.mobileSharePositionAsFEN), - onPressed: (_) { - final analysisState = - ref.read(analysisControllerProvider(pgn, options)); - launchShareDialog( - context, - text: analysisState.position.fen, - ); - }, - ), - if (options.gameAnyId != null) - BottomSheetAction( - makeLabel: (context) => - Text(context.l10n.screenshotCurrentPosition), - onPressed: (_) async { - final gameId = options.gameAnyId!.gameId; - final analysisState = - ref.read(analysisControllerProvider(pgn, options)); - try { - final image = - await ref.read(gameShareServiceProvider).screenshotPosition( - gameId, - options.orientation, - analysisState.position.fen, - analysisState.lastMove, - ); - if (context.mounted) { - launchShareDialog( - context, - files: [image], - subject: context.l10n.puzzleFromGameLink( - lichessUri('/$gameId').toString(), - ), - ); - } - } catch (e) { - if (context.mounted) { - showPlatformSnackbar( - context, - 'Failed to get GIF', - type: SnackBarType.error, - ); - } - } - }, - ), - ], - ); - } -} - -class ServerAnalysisSummary extends ConsumerWidget { - const ServerAnalysisSummary(this.pgn, this.options); - - final String pgn; - final AnalysisOptions options; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final ctrlProvider = analysisControllerProvider(pgn, options); - final playersAnalysis = - ref.watch(ctrlProvider.select((value) => value.playersAnalysis)); - final pgnHeaders = - ref.watch(ctrlProvider.select((value) => value.pgnHeaders)); - final currentGameAnalysis = ref.watch(currentAnalysisProvider); - - return playersAnalysis != null - ? ListView( - children: [ - if (currentGameAnalysis == options.gameAnyId?.gameId) - const Padding( - padding: EdgeInsets.only(top: 16.0), - child: WaitingForServerAnalysis(), - ), - AcplChart(pgn, options), - Center( - child: SizedBox( - width: math.min(MediaQuery.sizeOf(context).width, 500), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Table( - defaultVerticalAlignment: - TableCellVerticalAlignment.middle, - columnWidths: const { - 0: FlexColumnWidth(1), - 1: FlexColumnWidth(1), - 2: FlexColumnWidth(1), - }, - children: [ - TableRow( - decoration: const BoxDecoration( - border: Border( - bottom: BorderSide(color: Colors.grey), - ), - ), - children: [ - _SummaryPlayerName(Side.white, pgnHeaders), - Center( - child: Text( - pgnHeaders.get('Result') ?? '', - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - ), - ), - _SummaryPlayerName(Side.black, pgnHeaders), - ], - ), - if (playersAnalysis.white.accuracy != null && - playersAnalysis.black.accuracy != null) - TableRow( - children: [ - _SummaryNumber( - '${playersAnalysis.white.accuracy}%', - ), - Center( - heightFactor: 1.8, - child: Text( - context.l10n.accuracy, - softWrap: true, - ), - ), - _SummaryNumber( - '${playersAnalysis.black.accuracy}%', - ), - ], - ), - for (final item in [ - ( - playersAnalysis.white.inaccuracies.toString(), - context.l10n - .nbInaccuracies(2) - .replaceAll('2', '') - .trim() - .capitalize(), - playersAnalysis.black.inaccuracies.toString() - ), - ( - playersAnalysis.white.mistakes.toString(), - context.l10n - .nbMistakes(2) - .replaceAll('2', '') - .trim() - .capitalize(), - playersAnalysis.black.mistakes.toString() - ), - ( - playersAnalysis.white.blunders.toString(), - context.l10n - .nbBlunders(2) - .replaceAll('2', '') - .trim() - .capitalize(), - playersAnalysis.black.blunders.toString() - ), - ]) - TableRow( - children: [ - _SummaryNumber(item.$1), - Center( - heightFactor: 1.2, - child: Text( - item.$2, - softWrap: true, - ), - ), - _SummaryNumber(item.$3), - ], - ), - if (playersAnalysis.white.acpl != null && - playersAnalysis.black.acpl != null) - TableRow( - children: [ - _SummaryNumber( - playersAnalysis.white.acpl.toString(), - ), - Center( - heightFactor: 1.5, - child: Text( - context.l10n.averageCentipawnLoss, - softWrap: true, - textAlign: TextAlign.center, - ), - ), - _SummaryNumber( - playersAnalysis.black.acpl.toString(), - ), - ], - ), - ], - ), - ), - ), - ), - ], - ) - : Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Spacer(), - if (currentGameAnalysis == options.gameAnyId?.gameId) - const Center( - child: Padding( - padding: EdgeInsets.symmetric(vertical: 16.0), - child: WaitingForServerAnalysis(), - ), - ) - else - Center( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0), - child: Builder( - builder: (context) { - Future? pendingRequest; - return StatefulBuilder( - builder: (context, setState) { - return FutureBuilder( - future: pendingRequest, - builder: (context, snapshot) { - return SecondaryButton( - semanticsLabel: - context.l10n.requestAComputerAnalysis, - onPressed: ref.watch(authSessionProvider) == - null - ? () { - showPlatformSnackbar( - context, - context - .l10n.youNeedAnAccountToDoThat, - ); - } - : snapshot.connectionState == - ConnectionState.waiting - ? null - : () { - setState(() { - pendingRequest = ref - .read(ctrlProvider.notifier) - .requestServerAnalysis() - .catchError((Object e) { - if (context.mounted) { - showPlatformSnackbar( - context, - e.toString(), - type: SnackBarType.error, - ); - } - }); - }); - }, - child: Text( - context.l10n.requestAComputerAnalysis, - ), - ); - }, - ); - }, - ); - }, - ), - ), - ), - const Spacer(), - ], - ); - } -} - -class WaitingForServerAnalysis extends StatelessWidget { - const WaitingForServerAnalysis({super.key}); - - @override - Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.max, - children: [ - Image.asset( - 'assets/images/stockfish/icon.png', - width: 30, - height: 30, - ), - const SizedBox(width: 8.0), - Text(context.l10n.waitingForAnalysis), - const SizedBox(width: 8.0), - const CircularProgressIndicator.adaptive(), - ], - ); - } -} - -class _SummaryNumber extends StatelessWidget { - const _SummaryNumber(this.data); - final String data; - - @override - Widget build(BuildContext context) { - return Center( - child: Text( - data, - softWrap: true, - ), - ); - } -} - -class _SummaryPlayerName extends StatelessWidget { - const _SummaryPlayerName(this.side, this.pgnHeaders); - final Side side; - final IMap pgnHeaders; - - @override - Widget build(BuildContext context) { - final playerTitle = side == Side.white - ? pgnHeaders.get('WhiteTitle') - : pgnHeaders.get('BlackTitle'); - final playerName = side == Side.white - ? pgnHeaders.get('White') ?? context.l10n.white - : pgnHeaders.get('Black') ?? context.l10n.black; - - final brightness = Theme.of(context).brightness; - - return TableCell( - verticalAlignment: TableCellVerticalAlignment.top, - child: Center( - child: Padding( - padding: const EdgeInsets.only(bottom: 5), - child: Column( - children: [ - Icon( - side == Side.white - ? brightness == Brightness.light - ? CupertinoIcons.circle - : CupertinoIcons.circle_filled - : brightness == Brightness.light - ? CupertinoIcons.circle_filled - : CupertinoIcons.circle, - size: 14, - ), - Text( - '${playerTitle != null ? '$playerTitle ' : ''}$playerName', - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - textAlign: TextAlign.center, - softWrap: true, - ), - ], - ), - ), - ), - ); - } -} - -class AcplChart extends ConsumerWidget { - const AcplChart(this.pgn, this.options); - - final String pgn; - final AnalysisOptions options; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final mainLineColor = Theme.of(context).colorScheme.secondary; - // yes it looks like below/above are inverted in fl_chart - final brightness = Theme.of(context).brightness; - final white = Theme.of(context).colorScheme.surfaceContainerHighest; - final black = Theme.of(context).colorScheme.outline; - // yes it looks like below/above are inverted in fl_chart - final belowLineColor = brightness == Brightness.light ? white : black; - final aboveLineColor = brightness == Brightness.light ? black : white; - - VerticalLine phaseVerticalBar(double x, String label) => VerticalLine( - x: x, - color: const Color(0xFF707070), - strokeWidth: 0.5, - label: VerticalLineLabel( - style: TextStyle( - fontSize: 10, - color: Theme.of(context) - .textTheme - .labelMedium - ?.color - ?.withValues(alpha: 0.3), - ), - labelResolver: (line) => label, - padding: const EdgeInsets.only(right: 1), - alignment: Alignment.topRight, - direction: LabelDirection.vertical, - show: true, - ), - ); - - final data = ref.watch( - analysisControllerProvider(pgn, options) - .select((value) => value.acplChartData), - ); - - final rootPly = ref.watch( - analysisControllerProvider(pgn, options) - .select((value) => value.root.position.ply), - ); - - final currentNode = ref.watch( - analysisControllerProvider(pgn, options) - .select((value) => value.currentNode), - ); - - final isOnMainline = ref.watch( - analysisControllerProvider(pgn, options) - .select((value) => value.isOnMainline), - ); - - if (data == null) { - return const SizedBox.shrink(); - } - - final spots = data - .mapIndexed( - (i, e) => FlSpot(i.toDouble(), e.winningChances(Side.white)), - ) - .toList(growable: false); - - final divisionLines = []; - - if (options.division?.middlegame != null) { - if (options.division!.middlegame! > 0) { - divisionLines.add(phaseVerticalBar(0.0, context.l10n.opening)); - divisionLines.add( - phaseVerticalBar( - options.division!.middlegame! - 1, - context.l10n.middlegame, - ), - ); - } else { - divisionLines.add(phaseVerticalBar(0.0, context.l10n.middlegame)); - } - } - - if (options.division?.endgame != null) { - if (options.division!.endgame! > 0) { - divisionLines.add( - phaseVerticalBar( - options.division!.endgame! - 1, - context.l10n.endgame, - ), - ); - } else { - divisionLines.add( - phaseVerticalBar( - 0.0, - context.l10n.endgame, - ), - ); - } - } - return Center( - child: AspectRatio( - aspectRatio: 2.5, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: LineChart( - LineChartData( - lineTouchData: LineTouchData( - enabled: false, - touchCallback: - (FlTouchEvent event, LineTouchResponse? touchResponse) { - if (event is FlTapDownEvent || - event is FlPanUpdateEvent || - event is FlLongPressMoveUpdate) { - final touchX = event.localPosition!.dx; - final chartWidth = context.size!.width - - 32; // Insets on both sides of the chart of 16 - final minX = spots.first.x; - final maxX = spots.last.x; - final touchXDataValue = - minX + (touchX / chartWidth) * (maxX - minX); - final closestSpot = spots.reduce( - (a, b) => (a.x - touchXDataValue).abs() < - (b.x - touchXDataValue).abs() - ? a - : b, - ); - final closestNodeIndex = closestSpot.x.round(); - ref - .read(analysisControllerProvider(pgn, options).notifier) - .jumpToNthNodeOnMainline(closestNodeIndex); - } - }, - ), - minY: -1.0, - maxY: 1.0, - lineBarsData: [ - LineChartBarData( - spots: spots, - isCurved: false, - barWidth: 1, - color: mainLineColor.withValues(alpha: 0.7), - aboveBarData: BarAreaData( - show: true, - color: aboveLineColor, - applyCutOffY: true, - ), - belowBarData: BarAreaData( - show: true, - color: belowLineColor, - applyCutOffY: true, - ), - dotData: const FlDotData( - show: false, - ), - ), - ], - extraLinesData: ExtraLinesData( - verticalLines: [ - if (isOnMainline) - VerticalLine( - x: (currentNode.position.ply - 1 - rootPly).toDouble(), - color: mainLineColor, - strokeWidth: 1.0, - ), - ...divisionLines, - ], - ), - gridData: const FlGridData(show: false), - borderData: FlBorderData(show: false), - titlesData: const FlTitlesData(show: false), - ), - ), - ), - ), - ); - } -} diff --git a/lib/src/view/analysis/engine_depth.dart b/lib/src/view/analysis/engine_depth.dart deleted file mode 100644 index 6743a52882..0000000000 --- a/lib/src/view/analysis/engine_depth.dart +++ /dev/null @@ -1,125 +0,0 @@ -import 'dart:math' as math; - -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; -import 'package:lichess_mobile/src/model/engine/engine.dart'; -import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; -import 'package:lichess_mobile/src/utils/l10n_context.dart'; -import 'package:lichess_mobile/src/widgets/buttons.dart'; -import 'package:lichess_mobile/src/widgets/list.dart'; -import 'package:popover/popover.dart'; - -class EngineDepth extends ConsumerWidget { - const EngineDepth(this.ctrlProvider); - - final AnalysisControllerProvider ctrlProvider; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isEngineAvailable = ref.watch( - ctrlProvider.select( - (value) => value.isEngineAvailable, - ), - ); - final currentNode = ref.watch( - ctrlProvider.select((value) => value.currentNode), - ); - final depth = ref.watch( - engineEvaluationProvider.select((value) => value.eval?.depth), - ) ?? - currentNode.eval?.depth; - - return isEngineAvailable && depth != null - ? AppBarTextButton( - onPressed: () { - showPopover( - context: context, - bodyBuilder: (context) { - return _StockfishInfo(currentNode); - }, - direction: PopoverDirection.top, - width: 240, - backgroundColor: - Theme.of(context).platform == TargetPlatform.android - ? Theme.of(context).dialogBackgroundColor - : CupertinoDynamicColor.resolve( - CupertinoColors.tertiarySystemBackground, - context, - ), - transitionDuration: Duration.zero, - popoverTransitionBuilder: (_, child) => child, - ); - }, - child: RepaintBoundary( - child: Container( - width: 20.0, - height: 20.0, - padding: const EdgeInsets.all(2.0), - decoration: BoxDecoration( - color: Theme.of(context).platform == TargetPlatform.android - ? Theme.of(context).colorScheme.secondary - : CupertinoTheme.of(context).primaryColor, - borderRadius: BorderRadius.circular(4.0), - ), - child: FittedBox( - fit: BoxFit.contain, - child: Text( - '${math.min(99, depth)}', - style: TextStyle( - color: Theme.of(context).platform == - TargetPlatform.android - ? Theme.of(context).colorScheme.onSecondary - : CupertinoTheme.of(context).primaryContrastingColor, - fontFeatures: const [ - FontFeature.tabularFigures(), - ], - ), - ), - ), - ), - ), - ) - : const SizedBox.shrink(); - } -} - -class _StockfishInfo extends ConsumerWidget { - const _StockfishInfo(this.currentNode); - - final AnalysisCurrentNode currentNode; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final (engineName: engineName, eval: eval, state: engineState) = - ref.watch(engineEvaluationProvider); - - final currentEval = eval ?? currentNode.eval; - - final knps = engineState == EngineState.computing - ? ', ${eval?.knps.round()}kn/s' - : ''; - final depth = currentEval?.depth ?? 0; - final maxDepth = math.max(depth, kMaxEngineDepth); - - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - PlatformListTile( - leading: Image.asset( - 'assets/images/stockfish/icon.png', - width: 44, - height: 44, - ), - title: Text(engineName), - subtitle: Text( - context.l10n.depthX( - '$depth/$maxDepth$knps', - ), - ), - ), - ], - ); - } -} diff --git a/lib/src/view/analysis/tree_view.dart b/lib/src/view/analysis/tree_view.dart index 32f12d6616..63e10d8975 100644 --- a/lib/src/view/analysis/tree_view.dart +++ b/lib/src/view/analysis/tree_view.dart @@ -26,8 +26,6 @@ class AnalysisTreeView extends ConsumerWidget { final root = ref.watch(ctrlProvider.select((value) => value.root)); final currentPath = ref.watch(ctrlProvider.select((value) => value.currentPath)); - final broadcastLivePath = - ref.watch(ctrlProvider.select((value) => value.broadcastLivePath)); final pgnRootComments = ref.watch(ctrlProvider.select((value) => value.pgnRootComments)); @@ -42,7 +40,6 @@ class AnalysisTreeView extends ConsumerWidget { DebouncedPgnTreeView( root: root, currentPath: currentPath, - broadcastLivePath: broadcastLivePath, pgnRootComments: pgnRootComments, notifier: ref.read(ctrlProvider.notifier), ), diff --git a/lib/src/view/broadcast/broadcast_boards_tab.dart b/lib/src/view/broadcast/broadcast_boards_tab.dart index 7b62cfb7bb..d135170be2 100644 --- a/lib/src/view/broadcast/broadcast_boards_tab.dart +++ b/lib/src/view/broadcast/broadcast_boards_tab.dart @@ -15,7 +15,7 @@ import 'package:lichess_mobile/src/utils/duration.dart'; import 'package:lichess_mobile/src/utils/lichess_assets.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; -import 'package:lichess_mobile/src/view/broadcast/broadcast_game_analysis_screen.dart'; +import 'package:lichess_mobile/src/view/broadcast/broadcast_game_screen.dart'; import 'package:lichess_mobile/src/widgets/board_thumbnail.dart'; import 'package:lichess_mobile/src/widgets/evaluation_bar.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; @@ -139,7 +139,7 @@ class BroadcastPreview extends ConsumerWidget { onTap: () { pushPlatformRoute( context, - builder: (context) => BroadcastGameAnalysisScreen( + builder: (context) => BroadcastGameScreen( roundId: roundId, gameId: game.id, title: title, diff --git a/lib/src/view/broadcast/broadcast_bottom_bar.dart b/lib/src/view/broadcast/broadcast_game_bottom_bar.dart similarity index 98% rename from lib/src/view/broadcast/broadcast_bottom_bar.dart rename to lib/src/view/broadcast/broadcast_game_bottom_bar.dart index 083a90fea7..d89cd2a2b2 100644 --- a/lib/src/view/broadcast/broadcast_bottom_bar.dart +++ b/lib/src/view/broadcast/broadcast_game_bottom_bar.dart @@ -13,8 +13,8 @@ import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; -class BroadcastBottomBar extends ConsumerWidget { - const BroadcastBottomBar({ +class BroadcastGameBottomBar extends ConsumerWidget { + const BroadcastGameBottomBar({ required this.roundId, required this.gameId, }); diff --git a/lib/src/view/broadcast/broadcast_engine_depth.dart b/lib/src/view/broadcast/broadcast_game_engine_depth.dart similarity index 95% rename from lib/src/view/broadcast/broadcast_engine_depth.dart rename to lib/src/view/broadcast/broadcast_game_engine_depth.dart index c83bd50e37..1e10da5597 100644 --- a/lib/src/view/broadcast/broadcast_engine_depth.dart +++ b/lib/src/view/broadcast/broadcast_game_engine_depth.dart @@ -3,6 +3,7 @@ import 'dart:math' as math; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_game_controller.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/engine/engine.dart'; @@ -12,11 +13,11 @@ import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:popover/popover.dart'; -class BroadcastEngineDepth extends ConsumerWidget { +class BroadcastGameEngineDepth extends ConsumerWidget { final BroadcastRoundId roundId; final BroadcastGameId gameId; - const BroadcastEngineDepth(this.roundId, this.gameId); + const BroadcastGameEngineDepth(this.roundId, this.gameId); @override Widget build(BuildContext context, WidgetRef ref) { diff --git a/lib/src/view/broadcast/broadcast_game_analysis_screen.dart b/lib/src/view/broadcast/broadcast_game_screen.dart similarity index 96% rename from lib/src/view/broadcast/broadcast_game_analysis_screen.dart rename to lib/src/view/broadcast/broadcast_game_screen.dart index 5163429c3a..d3d5524cdb 100644 --- a/lib/src/view/broadcast/broadcast_game_analysis_screen.dart +++ b/lib/src/view/broadcast/broadcast_game_screen.dart @@ -20,10 +20,10 @@ import 'package:lichess_mobile/src/utils/duration.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/lichess_assets.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; -import 'package:lichess_mobile/src/view/broadcast/broadcast_bottom_bar.dart'; -import 'package:lichess_mobile/src/view/broadcast/broadcast_engine_depth.dart'; -import 'package:lichess_mobile/src/view/broadcast/broadcast_settings.dart'; -import 'package:lichess_mobile/src/view/broadcast/broadcast_tree_view.dart'; +import 'package:lichess_mobile/src/view/broadcast/broadcast_game_bottom_bar.dart'; +import 'package:lichess_mobile/src/view/broadcast/broadcast_game_engine_depth.dart'; +import 'package:lichess_mobile/src/view/broadcast/broadcast_game_settings.dart'; +import 'package:lichess_mobile/src/view/broadcast/broadcast_game_tree_view.dart'; import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; import 'package:lichess_mobile/src/view/engine/engine_lines.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; @@ -33,14 +33,14 @@ import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:logging/logging.dart'; -final _logger = Logger('BroadcastGameAnalysisScreen'); +final _logger = Logger('BroadcastGameScreen'); -class BroadcastGameAnalysisScreen extends ConsumerWidget { +class BroadcastGameScreen extends ConsumerWidget { final BroadcastRoundId roundId; final BroadcastGameId gameId; final String title; - const BroadcastGameAnalysisScreen({ + const BroadcastGameScreen({ required this.roundId, required this.gameId, required this.title, @@ -54,7 +54,7 @@ class BroadcastGameAnalysisScreen extends ConsumerWidget { appBar: PlatformAppBar( title: Text(title), actions: [ - if (state.hasValue) BroadcastEngineDepth(roundId, gameId), + if (state.hasValue) BroadcastGameEngineDepth(roundId, gameId), AppBarIconButton( onPressed: () => (state.hasValue) ? showAdaptiveBottomSheet( @@ -62,7 +62,7 @@ class BroadcastGameAnalysisScreen extends ConsumerWidget { isScrollControlled: true, showDragHandle: true, isDismissible: true, - builder: (_) => BroadcastGameAnalysisSettings( + builder: (_) => BroadcastGameSettings( roundId, gameId, ), @@ -75,7 +75,8 @@ class BroadcastGameAnalysisScreen extends ConsumerWidget { ), body: state.when( data: (state) => _Body(roundId, gameId), - loading: () => const CircularProgressIndicator.adaptive(), + loading: () => + const Center(child: CircularProgressIndicator.adaptive()), error: (error, stackTrace) { _logger.severe('Cannot load broadcast game: $error', stackTrace); return Center( @@ -188,7 +189,7 @@ class _Body extends ConsumerWidget { kTabletBoardTableSidePadding, ), semanticContainer: false, - child: BroadcastTreeView( + child: BroadcastGameTreeView( roundId, gameId, Orientation.landscape, @@ -231,7 +232,7 @@ class _Body extends ConsumerWidget { horizontal: kTabletBoardTableSidePadding, ) : EdgeInsets.zero, - child: BroadcastTreeView( + child: BroadcastGameTreeView( roundId, gameId, Orientation.portrait, @@ -243,7 +244,7 @@ class _Body extends ConsumerWidget { }, ), ), - BroadcastBottomBar(roundId: roundId, gameId: gameId), + BroadcastGameBottomBar(roundId: roundId, gameId: gameId), ], ), ); diff --git a/lib/src/view/broadcast/broadcast_settings.dart b/lib/src/view/broadcast/broadcast_game_settings.dart similarity index 97% rename from lib/src/view/broadcast/broadcast_settings.dart rename to lib/src/view/broadcast/broadcast_game_settings.dart index ed67e5f066..523992f332 100644 --- a/lib/src/view/broadcast/broadcast_settings.dart +++ b/lib/src/view/broadcast/broadcast_game_settings.dart @@ -13,8 +13,8 @@ import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/non_linear_slider.dart'; import 'package:lichess_mobile/src/widgets/settings.dart'; -class BroadcastGameAnalysisSettings extends ConsumerWidget { - const BroadcastGameAnalysisSettings(this.roundId, this.gameId); +class BroadcastGameSettings extends ConsumerWidget { + const BroadcastGameSettings(this.roundId, this.gameId); final BroadcastRoundId roundId; final BroadcastGameId gameId; diff --git a/lib/src/view/broadcast/broadcast_tree_view.dart b/lib/src/view/broadcast/broadcast_game_tree_view.dart similarity index 97% rename from lib/src/view/broadcast/broadcast_tree_view.dart rename to lib/src/view/broadcast/broadcast_game_tree_view.dart index a92fa7bb8c..62097c34d1 100644 --- a/lib/src/view/broadcast/broadcast_tree_view.dart +++ b/lib/src/view/broadcast/broadcast_game_tree_view.dart @@ -8,8 +8,8 @@ import 'package:lichess_mobile/src/widgets/pgn.dart'; const kOpeningHeaderHeight = 32.0; -class BroadcastTreeView extends ConsumerWidget { - const BroadcastTreeView( +class BroadcastGameTreeView extends ConsumerWidget { + const BroadcastGameTreeView( this.roundId, this.gameId, this.displayMode, From 50c2433713a14c0d63e0e2e74e90d0f09a22de2a Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Thu, 14 Nov 2024 01:19:10 +0100 Subject: [PATCH 646/979] simplify broadcast game controller --- .../broadcast/broadcast_game_controller.dart | 82 +++---------------- 1 file changed, 11 insertions(+), 71 deletions(-) diff --git a/lib/src/model/broadcast/broadcast_game_controller.dart b/lib/src/model/broadcast/broadcast_game_controller.dart index 1041354d86..aec4171e04 100644 --- a/lib/src/model/broadcast/broadcast_game_controller.dart +++ b/lib/src/model/broadcast/broadcast_game_controller.dart @@ -4,7 +4,6 @@ import 'package:dartchess/dartchess.dart'; import 'package:deep_pick/deep_pick.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; import 'package:lichess_mobile/src/model/analysis/opening_service.dart'; @@ -29,21 +28,12 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'broadcast_game_controller.freezed.dart'; part 'broadcast_game_controller.g.dart'; -final _dateFormat = DateFormat('yyyy.MM.dd'); - @riverpod class BroadcastGameController extends _$BroadcastGameController implements PgnTreeNotifier { static Uri broadcastSocketUri(BroadcastRoundId broadcastRoundId) => Uri(path: 'study/$broadcastRoundId/socket/v6'); - static AnalysisOptions get options => const AnalysisOptions( - id: StringId(''), - isLocalEvaluationAllowed: true, - orientation: Side.white, - variant: Variant.standard, - ); - StreamSubscription? _subscription; late SocketClient _socketClient; @@ -64,16 +54,12 @@ class BroadcastGameController extends _$BroadcastGameController _subscription = _socketClient.stream.listen(_handleSocketEvent); - ref.onDispose(() { - _subscription?.cancel(); - }); - final evaluationService = ref.watch(evaluationServiceProvider); - final isEngineAllowed = options.isLocalEvaluationAllowed && - engineSupportedVariants.contains(options.variant); + const isEngineAllowed = true; ref.onDispose(() { + _subscription?.cancel(); _startEngineEvalTimer?.cancel(); _engineEvalDebounce.dispose(); if (isEngineAllowed) { @@ -81,69 +67,28 @@ class BroadcastGameController extends _$BroadcastGameController } }); - UciPath path = UciPath.empty; Move? lastMove; final pgn = await ref.withClient( (client) => BroadcastRepository(client).getGame(roundId, gameId), ); - final game = PgnGame.parsePgn( - pgn, - initHeaders: () => options.isLichessGameAnalysis - ? {} - : { - 'Event': '?', - 'Site': '?', - 'Date': _dateFormat.format(DateTime.now()), - 'Round': '?', - 'White': '?', - 'Black': '?', - 'Result': '*', - 'WhiteElo': '?', - 'BlackElo': '?', - }, - ); - + final game = PgnGame.parsePgn(pgn); final pgnHeaders = IMap(game.headers); final rootComments = IList(game.comments.map((c) => PgnComment.fromPgn(c))); - Future? openingFuture; - - _root = Root.fromPgnGame( - game, - isLichessAnalysis: options.isLichessGameAnalysis, - hideVariations: options.isLichessGameAnalysis, - onVisitNode: (root, branch, isMainline) { - if (isMainline && - options.initialMoveCursor != null && - branch.position.ply <= - root.position.ply + options.initialMoveCursor!) { - path = path + branch.id; - lastMove = branch.sanMove.move; - } - if (isMainline && options.opening == null && branch.position.ply <= 5) { - openingFuture = _fetchOpening(root, path); - } - }, - ); + _root = Root.fromPgnGame(game); - final currentPath = - options.initialMoveCursor == null ? _root.mainlinePath : path; + final currentPath = _root.mainlinePath; final currentNode = _root.nodeAt(currentPath); - // wait for the opening to be fetched to recompute the branch opening - openingFuture?.then((_) { - _setPath(currentPath); - }); - // don't use ref.watch here: we don't want to invalidate state when the // analysis preferences change final prefs = ref.read(analysisPreferencesProvider); final analysisState = BroadcastGameState( - variant: options.variant, - id: options.id, + variant: Variant.standard, + id: gameId, currentPath: currentPath, broadcastLivePath: pgnHeaders['Result'] == '*' ? currentPath : null, isOnMainline: _root.isOnMainline(currentPath), @@ -152,9 +97,9 @@ class BroadcastGameController extends _$BroadcastGameController pgnHeaders: pgnHeaders, pgnRootComments: rootComments, lastMove: lastMove, - pov: options.orientation, - contextOpening: options.opening, - isLocalEvaluationAllowed: options.isLocalEvaluationAllowed, + pov: Side.white, + contextOpening: null, + isLocalEvaluationAllowed: true, isLocalEvaluationEnabled: prefs.enableLocalEvaluation, displayMode: DisplayMode.moves, clocks: _makeClocks(currentPath), @@ -256,7 +201,7 @@ class BroadcastGameController extends _$BroadcastGameController } EvaluationContext get _evaluationContext => EvaluationContext( - variant: options.variant, + variant: Variant.standard, initialPosition: _root.position, ); @@ -270,13 +215,9 @@ class BroadcastGameController extends _$BroadcastGameController return; } - // For the opening explorer, last played move should always be the mainline - final shouldReplace = options.id == standaloneOpeningExplorerId; - final (newPath, isNewNode) = _root.addMoveAt( state.requireValue.currentPath, move, - replace: shouldReplace, ); if (newPath != null) { _setPath( @@ -596,7 +537,6 @@ class BroadcastGameController extends _$BroadcastGameController Future _fetchOpening(Node fromNode, UciPath path) async { if (!state.hasValue) return; - if (!kOpeningAllowedVariants.contains(options.variant)) return; final moves = fromNode.branchesOn(path).map((node) => node.sanMove.move); if (moves.isEmpty) return; From 8ad1393574e253f9cc069a0bc106578b5b63931c Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Thu, 14 Nov 2024 01:34:02 +0100 Subject: [PATCH 647/979] remove unused methods in broadcast state --- .../model/broadcast/broadcast_game_controller.dart | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/lib/src/model/broadcast/broadcast_game_controller.dart b/lib/src/model/broadcast/broadcast_game_controller.dart index aec4171e04..683b0bfa06 100644 --- a/lib/src/model/broadcast/broadcast_game_controller.dart +++ b/lib/src/model/broadcast/broadcast_game_controller.dart @@ -611,7 +611,7 @@ class BroadcastGameState with _$BroadcastGameState { const BroadcastGameState._(); const factory BroadcastGameState({ - /// Analysis ID + /// Broadcast game ID required StringId id, /// The variant of the analysis. @@ -679,15 +679,7 @@ class BroadcastGameState with _$BroadcastGameState { isChess960: variant == Variant.chess960, ); - /// Whether an evaluation can be available - bool get hasAvailableEval => isEngineAvailable; - - /// Whether the engine is allowed for this analysis and variant. - bool get isEngineAllowed => - isLocalEvaluationAllowed && engineSupportedVariants.contains(variant); - - /// Whether the engine is available for evaluation - bool get isEngineAvailable => isEngineAllowed && isLocalEvaluationEnabled; + bool get isEngineAvailable => isLocalEvaluationEnabled; Position get position => currentNode.position; bool get canGoNext => currentNode.hasChild; From d785d98434e0f62d44bc7066040f109d3cb460e1 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Thu, 14 Nov 2024 01:53:02 +0100 Subject: [PATCH 648/979] remove useless broadcast game state parameters --- .../broadcast/broadcast_game_controller.dart | 22 ++++--------------- .../broadcast/broadcast_game_settings.dart | 16 +++++--------- .../broadcast/broadcast_game_tree_view.dart | 4 +--- 3 files changed, 10 insertions(+), 32 deletions(-) diff --git a/lib/src/model/broadcast/broadcast_game_controller.dart b/lib/src/model/broadcast/broadcast_game_controller.dart index 683b0bfa06..08ebd514e0 100644 --- a/lib/src/model/broadcast/broadcast_game_controller.dart +++ b/lib/src/model/broadcast/broadcast_game_controller.dart @@ -87,7 +87,6 @@ class BroadcastGameController extends _$BroadcastGameController final prefs = ref.read(analysisPreferencesProvider); final analysisState = BroadcastGameState( - variant: Variant.standard, id: gameId, currentPath: currentPath, broadcastLivePath: pgnHeaders['Result'] == '*' ? currentPath : null, @@ -98,8 +97,6 @@ class BroadcastGameController extends _$BroadcastGameController pgnRootComments: rootComments, lastMove: lastMove, pov: Side.white, - contextOpening: null, - isLocalEvaluationAllowed: true, isLocalEvaluationEnabled: prefs.enableLocalEvaluation, displayMode: DisplayMode.moves, clocks: _makeClocks(currentPath), @@ -614,9 +611,6 @@ class BroadcastGameState with _$BroadcastGameState { /// Broadcast game ID required StringId id, - /// The variant of the analysis. - required Variant variant, - /// Immutable view of the whole tree required ViewRoot root, @@ -639,15 +633,12 @@ class BroadcastGameState with _$BroadcastGameState { /// The side to display the board from. required Side pov, - /// Whether local evaluation is allowed for this analysis. - required bool isLocalEvaluationAllowed, - /// Whether the user has enabled local evaluation. required bool isLocalEvaluationEnabled, /// The display mode of the analysis. /// - /// It can be either moves, summary or opening explorer. + /// It can be either moves or opening explorer in this controller (summary will be added later). required DisplayMode displayMode, /// Clocks if avaible. Only used by the broadcast analysis screen. @@ -659,9 +650,6 @@ class BroadcastGameState with _$BroadcastGameState { /// Possible promotion move to be played. NormalMove? promotionMove, - /// Opening of the analysis context (from lichess archived games). - Opening? contextOpening, - /// The opening of the current branch. Opening? currentBranchOpening, @@ -674,10 +662,8 @@ class BroadcastGameState with _$BroadcastGameState { IList? pgnRootComments, }) = _AnalysisState; - IMap> get validMoves => makeLegalMoves( - currentNode.position, - isChess960: variant == Variant.chess960, - ); + IMap> get validMoves => + makeLegalMoves(currentNode.position); bool get isEngineAvailable => isLocalEvaluationEnabled; @@ -696,7 +682,7 @@ class BroadcastGameState with _$BroadcastGameState { id: standaloneOpeningExplorerId, isLocalEvaluationAllowed: false, orientation: pov, - variant: variant, + variant: Variant.standard, initialMoveCursor: currentPath.size, ); } diff --git a/lib/src/view/broadcast/broadcast_game_settings.dart b/lib/src/view/broadcast/broadcast_game_settings.dart index 523992f332..596d0bbbfd 100644 --- a/lib/src/view/broadcast/broadcast_game_settings.dart +++ b/lib/src/view/broadcast/broadcast_game_settings.dart @@ -24,10 +24,6 @@ class BroadcastGameSettings extends ConsumerWidget { final broacdcastGameAnalysisController = broadcastGameControllerProvider(roundId, gameId); - final isLocalEvaluationAllowed = ref.watch( - broacdcastGameAnalysisController - .select((s) => s.requireValue.isLocalEvaluationAllowed), - ); final isEngineAvailable = ref.watch( broacdcastGameAnalysisController .select((s) => s.requireValue.isEngineAvailable), @@ -44,13 +40,11 @@ class BroadcastGameSettings extends ConsumerWidget { SwitchSettingTile( title: Text(context.l10n.toggleLocalEvaluation), value: analysisPrefs.enableLocalEvaluation, - onChanged: isLocalEvaluationAllowed - ? (_) { - ref - .read(broacdcastGameAnalysisController.notifier) - .toggleLocalEvaluation(); - } - : null, + onChanged: (_) { + ref + .read(broacdcastGameAnalysisController.notifier) + .toggleLocalEvaluation(); + }, ), PlatformListTile( title: Text.rich( diff --git a/lib/src/view/broadcast/broadcast_game_tree_view.dart b/lib/src/view/broadcast/broadcast_game_tree_view.dart index 62097c34d1..d5b53678a5 100644 --- a/lib/src/view/broadcast/broadcast_game_tree_view.dart +++ b/lib/src/view/broadcast/broadcast_game_tree_view.dart @@ -68,14 +68,12 @@ class _OpeningHeader extends ConsumerWidget { .watch(ctrlProvider.select((s) => s.requireValue.currentNode.opening)); final branchOpening = ref .watch(ctrlProvider.select((s) => s.requireValue.currentBranchOpening)); - final contextOpening = - ref.watch(ctrlProvider.select((s) => s.requireValue.contextOpening)); final opening = isRootNode ? LightOpening( eco: '', name: context.l10n.startPosition, ) - : nodeOpening ?? branchOpening ?? contextOpening; + : nodeOpening ?? branchOpening; return opening != null ? Container( From 985c69f20490d4011890099d1068339fe4478309 Mon Sep 17 00:00:00 2001 From: Jimima Date: Thu, 14 Nov 2024 08:57:35 +0000 Subject: [PATCH 649/979] Added in-game preference option --- lib/src/model/settings/board_preferences.dart | 2 +- lib/src/view/game/game_settings.dart | 29 ++++++++++++------- .../view/settings/board_settings_screen.dart | 16 +--------- 3 files changed, 20 insertions(+), 27 deletions(-) diff --git a/lib/src/model/settings/board_preferences.dart b/lib/src/model/settings/board_preferences.dart index 6083e1073f..4ca47608c2 100644 --- a/lib/src/model/settings/board_preferences.dart +++ b/lib/src/model/settings/board_preferences.dart @@ -302,7 +302,7 @@ enum BoardTheme { } enum MaterialDifference { - materialDifference(label: 'Material difference', visible: false), + materialDifference(label: 'Material difference', visible: true), capturedPieces(label: 'Captured pieces', visible: true), hidden(label: 'Hidden', visible: false); diff --git a/lib/src/view/game/game_settings.dart b/lib/src/view/game/game_settings.dart index e2e1d616f4..d35ced93e0 100644 --- a/lib/src/view/game/game_settings.dart +++ b/lib/src/view/game/game_settings.dart @@ -11,6 +11,7 @@ import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/settings.dart'; +import '../../widgets/adaptive_choice_picker.dart'; import 'game_screen_providers.dart'; class GameSettings extends ConsumerWidget { @@ -109,17 +110,6 @@ class GameSettings extends ConsumerWidget { ref.read(boardPreferencesProvider.notifier).togglePieceAnimation(); }, ), - // SwitchSettingTile( - // title: Text( - // context.l10n.preferencesMaterialDifference, - // ), - // value: boardPrefs.showMaterialDifference, - // onChanged: (value) { - // ref - // .read(boardPreferencesProvider.notifier) - // .toggleShowMaterialDifference(); - // }, - // ), SwitchSettingTile( title: Text( context.l10n.toggleTheChat, @@ -137,6 +127,23 @@ class GameSettings extends ConsumerWidget { ref.read(gamePreferencesProvider.notifier).toggleBlindfoldMode(); }, ), + SettingsListTile( + settingsLabel: const Text('Captured pieces'), + settingsValue: boardPrefs.materialDifference.label, + onTap: () { + showChoicePicker( + context, + choices: MaterialDifference.values, + selectedItem: boardPrefs.materialDifference, + labelBuilder: (t) => Text(t.label), + onSelectedItemChanged: (MaterialDifference? value) => ref + .read(boardPreferencesProvider.notifier) + .setMaterialDifferenceFormat( + value ?? MaterialDifference.materialDifference, + ), + ); + }, + ), ], ); } diff --git a/lib/src/view/settings/board_settings_screen.dart b/lib/src/view/settings/board_settings_screen.dart index dc917a4950..1bbb760750 100644 --- a/lib/src/view/settings/board_settings_screen.dart +++ b/lib/src/view/settings/board_settings_screen.dart @@ -184,24 +184,10 @@ class _Body extends ConsumerWidget { .togglePieceAnimation(); }, ), - // SwitchSettingTile( - // title: Text( - // context.l10n.preferencesMaterialDifference, - // ), - // value: boardPrefs.showMaterialDifference, - // onChanged: (value) { - // ref - // .read(boardPreferencesProvider.notifier) - // .toggleShowMaterialDifference(); - // }, - // ), SettingsListTile( - settingsLabel: const Text('Material difference format'), + settingsLabel: const Text('Captured pieces'), settingsValue: boardPrefs.materialDifference.label, onTap: () { - //TODO: implement different handling to android/ios - // if (Theme.of(context).platform == TargetPlatform.android) { - showChoicePicker( context, choices: MaterialDifference.values, From c6ff78b3da16368ff105c8933d53e5b38800a9d7 Mon Sep 17 00:00:00 2001 From: Jimima Date: Thu, 14 Nov 2024 09:01:19 +0000 Subject: [PATCH 650/979] Fix format --- lib/src/view/game/game_settings.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/view/game/game_settings.dart b/lib/src/view/game/game_settings.dart index d35ced93e0..aebe1270da 100644 --- a/lib/src/view/game/game_settings.dart +++ b/lib/src/view/game/game_settings.dart @@ -139,8 +139,8 @@ class GameSettings extends ConsumerWidget { onSelectedItemChanged: (MaterialDifference? value) => ref .read(boardPreferencesProvider.notifier) .setMaterialDifferenceFormat( - value ?? MaterialDifference.materialDifference, - ), + value ?? MaterialDifference.materialDifference, + ), ); }, ), From 7645266a8ffb1795a25f1c0f346f02f33545072c Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 5 Nov 2024 13:44:40 +0100 Subject: [PATCH 651/979] WIP on implementing lag comp --- lib/src/model/game/game.dart | 9 ---- lib/src/model/game/game_controller.dart | 9 +++- lib/src/model/game/game_socket_events.dart | 8 +++- lib/src/model/game/playable_game.dart | 18 +++++++ lib/src/view/clock/clock_screen.dart | 2 +- lib/src/view/game/archived_game_screen.dart | 4 +- lib/src/view/game/game_body.dart | 8 +++- lib/src/view/watch/tv_screen.dart | 4 +- lib/src/widgets/countdown_clock.dart | 49 +++++++++++++++++--- test/model/game/game_socket_events_test.dart | 8 ++-- 10 files changed, 92 insertions(+), 27 deletions(-) diff --git a/lib/src/model/game/game.dart b/lib/src/model/game/game.dart index 13d11db993..5e0a3750fe 100644 --- a/lib/src/model/game/game.dart +++ b/lib/src/model/game/game.dart @@ -319,15 +319,6 @@ class GameMeta with _$GameMeta { _$GameMetaFromJson(json); } -@freezed -class PlayableClockData with _$PlayableClockData { - const factory PlayableClockData({ - required bool running, - required Duration white, - required Duration black, - }) = _PlayableClockData; -} - @Freezed(fromJson: true, toJson: true) class CorrespondenceClockData with _$CorrespondenceClockData { const factory CorrespondenceClockData({ diff --git a/lib/src/model/game/game_controller.dart b/lib/src/model/game/game_controller.dart index d2a4ac3791..eb5efe4045 100644 --- a/lib/src/model/game/game_controller.dart +++ b/lib/src/model/game/game_controller.dart @@ -602,14 +602,21 @@ class GameController extends _$GameController { } } - // TODO handle delay if (data.clock != null) { _lastMoveTime = DateTime.now(); + final lagCompensation = newState.game.playable + // server will send the lag only if it's more than 10ms + ? data.clock?.lag ?? const Duration(milliseconds: 10) + : Duration.zero; + print('Delay lag comp: $lagCompensation'); + if (newState.game.clock != null) { newState = newState.copyWith.game.clock!( white: data.clock!.white, black: data.clock!.black, + lag: lagCompensation, + at: data.clock!.at, ); } else if (newState.game.correspondenceClock != null) { newState = newState.copyWith.game.correspondenceClock!( diff --git a/lib/src/model/game/game_socket_events.dart b/lib/src/model/game/game_socket_events.dart index df44964a89..890563f925 100644 --- a/lib/src/model/game/game_socket_events.dart +++ b/lib/src/model/game/game_socket_events.dart @@ -40,7 +40,12 @@ class MoveEvent with _$MoveEvent { bool? blackOfferingDraw, GameStatus? status, Side? winner, - ({Duration white, Duration black, Duration? lag})? clock, + ({ + Duration white, + Duration black, + Duration? lag, + DateTime at, + })? clock, }) = _MoveEvent; factory MoveEvent.fromJson(Map json) => @@ -78,6 +83,7 @@ MoveEvent _socketMoveEventFromPick(RequiredPick pick) { blackOfferingDraw: pick('bDraw').asBoolOrNull(), clock: pick('clock').letOrNull( (it) => ( + at: DateTime.now(), white: it('white').asDurationFromSecondsOrThrow(), black: it('black').asDurationFromSecondsOrThrow(), lag: it('lag') diff --git a/lib/src/model/game/playable_game.dart b/lib/src/model/game/playable_game.dart index 1e23968787..26614bb500 100644 --- a/lib/src/model/game/playable_game.dart +++ b/lib/src/model/game/playable_game.dart @@ -171,6 +171,23 @@ class PlayableGame } } +@freezed +class PlayableClockData with _$PlayableClockData { + const factory PlayableClockData({ + required bool running, + required Duration white, + required Duration black, + + /// The network lag of the clock. + /// + /// Will be sent along with move events. + Duration? lag, + + /// The time when the clock event was received. + required DateTime at, + }) = _PlayableClockData; +} + PlayableGame _playableGameFromPick(RequiredPick pick) { final requiredGamePick = pick('game').required(); final meta = _playableGameMetaFromPick(pick); @@ -296,6 +313,7 @@ PlayableClockData _playableClockDataFromPick(RequiredPick pick) { running: pick('running').asBoolOrThrow(), white: pick('white').asDurationFromSecondsOrThrow(), black: pick('black').asDurationFromSecondsOrThrow(), + at: DateTime.now(), ); } diff --git a/lib/src/view/clock/clock_screen.dart b/lib/src/view/clock/clock_screen.dart index f01b4d4880..f58bc78b2d 100644 --- a/lib/src/view/clock/clock_screen.dart +++ b/lib/src/view/clock/clock_screen.dart @@ -133,7 +133,7 @@ class ClockTile extends ConsumerWidget { key: Key('${clockState.id}-$playerType'), padLeft: true, clockStyle: clockStyle, - duration: clockState.getDuration(playerType), + timeLeft: clockState.getDuration(playerType), active: clockState.isActivePlayer(playerType), onFlag: () { ref diff --git a/lib/src/view/game/archived_game_screen.dart b/lib/src/view/game/archived_game_screen.dart index a4666b2863..b8371db3d0 100644 --- a/lib/src/view/game/archived_game_screen.dart +++ b/lib/src/view/game/archived_game_screen.dart @@ -260,7 +260,7 @@ class _BoardBody extends ConsumerWidget { player: gameData.black, clock: blackClock != null ? CountdownClock( - duration: blackClock, + timeLeft: blackClock, active: false, ) : null, @@ -271,7 +271,7 @@ class _BoardBody extends ConsumerWidget { player: gameData.white, clock: whiteClock != null ? CountdownClock( - duration: whiteClock, + timeLeft: whiteClock, active: false, ) : null, diff --git a/lib/src/view/game/game_body.dart b/lib/src/view/game/game_body.dart index 790d2f472a..1b793ded99 100644 --- a/lib/src/view/game/game_body.dart +++ b/lib/src/view/game/game_body.dart @@ -150,7 +150,9 @@ class GameBody extends ConsumerWidget { clock: gameState.game.meta.clock != null ? CountdownClock( key: blackClockKey, - duration: archivedBlackClock ?? gameState.game.clock!.black, + delay: gameState.game.clock!.lag, + clockEventTime: gameState.game.clock!.at, + timeLeft: archivedBlackClock ?? gameState.game.clock!.black, active: gameState.activeClockSide == Side.black, emergencyThreshold: youAre == Side.black ? gameState.game.meta.clock?.emergency @@ -190,7 +192,9 @@ class GameBody extends ConsumerWidget { clock: gameState.game.meta.clock != null ? CountdownClock( key: whiteClockKey, - duration: archivedWhiteClock ?? gameState.game.clock!.white, + timeLeft: archivedWhiteClock ?? gameState.game.clock!.white, + delay: gameState.game.clock!.lag, + clockEventTime: gameState.game.clock!.at, active: gameState.activeClockSide == Side.white, emergencyThreshold: youAre == Side.white ? gameState.game.meta.clock?.emergency diff --git a/lib/src/view/watch/tv_screen.dart b/lib/src/view/watch/tv_screen.dart index 5d55705d7e..96b852c318 100644 --- a/lib/src/view/watch/tv_screen.dart +++ b/lib/src/view/watch/tv_screen.dart @@ -94,7 +94,7 @@ class _Body extends ConsumerWidget { clock: gameState.game.clock != null ? CountdownClock( key: blackClockKey, - duration: gameState.game.clock!.black, + timeLeft: gameState.game.clock!.black, active: gameState.activeClockSide == Side.black, ) : null, @@ -105,7 +105,7 @@ class _Body extends ConsumerWidget { clock: gameState.game.clock != null ? CountdownClock( key: whiteClockKey, - duration: gameState.game.clock!.white, + timeLeft: gameState.game.clock!.white, active: gameState.activeClockSide == Side.white, ) : null, diff --git a/lib/src/widgets/countdown_clock.dart b/lib/src/widgets/countdown_clock.dart index cfae7ca1ce..22115d9101 100644 --- a/lib/src/widgets/countdown_clock.dart +++ b/lib/src/widgets/countdown_clock.dart @@ -7,12 +7,14 @@ import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; -/// A simple countdown clock. +/// A countdown clock. /// /// The clock starts only when [active] is `true`. class CountdownClock extends ConsumerStatefulWidget { const CountdownClock({ - required this.duration, + required this.timeLeft, + this.delay, + this.clockEventTime, required this.active, this.emergencyThreshold, this.emergencySoundEnabled = true, @@ -24,7 +26,18 @@ class CountdownClock extends ConsumerStatefulWidget { }); /// The duration left on the clock. - final Duration duration; + final Duration timeLeft; + + /// The delay before the clock starts counting down. + /// + /// This can be used to implement lag compensation. + final Duration? delay; + + /// The time the time left was received at. + /// + /// Use this parameter to synchronize the clock with the time at which the clock + /// event was received from the server. + final DateTime? clockEventTime; /// If [timeLeft] is less than [emergencyThreshold], the clock will change /// its background color to [ClockStyle.emergencyBackgroundColor] activeBackgroundColor @@ -57,6 +70,7 @@ const _period = Duration(milliseconds: 100); const _emergencyDelay = Duration(seconds: 20); class _CountdownClockState extends ConsumerState { + Timer? _delayTimer; Timer? _timer; Duration timeLeft = Duration.zero; bool _shouldPlayEmergencyFeedback = true; @@ -65,6 +79,29 @@ class _CountdownClockState extends ConsumerState { final _stopwatch = Stopwatch(); void startClock() { + final now = DateTime.now(); + final delay = widget.delay ?? Duration.zero; + final clockEventTime = widget.clockEventTime ?? now; + // UI lag diff: the elapsed time between the time we received the clock event + // and the time the clock is actually started + final uiLag = now.difference(clockEventTime); + // The clock should have started at `clockEventTime`, but it started at `now`. + // so we need to adjust the delay. + final realDelay = delay - uiLag; + + if (realDelay > Duration.zero) { + _delayTimer?.cancel(); + _delayTimer = Timer(realDelay, _doStartClock); + } else if (realDelay < Duration.zero) { + // real delay is negative, so we need to adjust the timeLeft. + timeLeft = timeLeft + realDelay; + _doStartClock(); + } else { + _doStartClock(); + } + } + + void _doStartClock() { _timer?.cancel(); _stopwatch.reset(); _stopwatch.start(); @@ -116,7 +153,7 @@ class _CountdownClockState extends ConsumerState { @override void initState() { super.initState(); - timeLeft = widget.duration; + timeLeft = widget.timeLeft; if (widget.active) { startClock(); } @@ -125,8 +162,8 @@ class _CountdownClockState extends ConsumerState { @override void didUpdateWidget(CountdownClock oldClock) { super.didUpdateWidget(oldClock); - if (widget.duration != oldClock.duration) { - timeLeft = widget.duration; + if (widget.timeLeft != oldClock.timeLeft) { + timeLeft = widget.timeLeft; } if (widget.active != oldClock.active) { diff --git a/test/model/game/game_socket_events_test.dart b/test/model/game/game_socket_events_test.dart index 12d4ee32a4..9e7e1ea526 100644 --- a/test/model/game/game_socket_events_test.dart +++ b/test/model/game/game_socket_events_test.dart @@ -7,6 +7,7 @@ import 'package:lichess_mobile/src/model/common/perf.dart'; import 'package:lichess_mobile/src/model/common/speed.dart'; import 'package:lichess_mobile/src/model/game/game.dart'; import 'package:lichess_mobile/src/model/game/game_socket_events.dart'; +import 'package:lichess_mobile/src/model/game/playable_game.dart'; void main() { test('decode game full event from websocket json', () { @@ -16,10 +17,11 @@ void main() { expect(game.id, const GameId('nV3DaALy')); expect( game.clock, - const PlayableClockData( + PlayableClockData( running: true, - white: Duration(seconds: 149, milliseconds: 50), - black: Duration(seconds: 775, milliseconds: 940), + white: const Duration(seconds: 149, milliseconds: 50), + black: const Duration(seconds: 775, milliseconds: 940), + at: DateTime.now(), ), ); expect( From f59d3a26bbb096684fc364fe6809707a9d6e4a08 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 5 Nov 2024 14:01:24 +0100 Subject: [PATCH 652/979] Remove print --- lib/src/model/game/game_controller.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/src/model/game/game_controller.dart b/lib/src/model/game/game_controller.dart index eb5efe4045..c47dd9eb22 100644 --- a/lib/src/model/game/game_controller.dart +++ b/lib/src/model/game/game_controller.dart @@ -609,7 +609,6 @@ class GameController extends _$GameController { // server will send the lag only if it's more than 10ms ? data.clock?.lag ?? const Duration(milliseconds: 10) : Duration.zero; - print('Delay lag comp: $lagCompensation'); if (newState.game.clock != null) { newState = newState.copyWith.game.clock!( From 0c713e5ad4596f0389d901d8864d3408556003c5 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 5 Nov 2024 14:05:30 +0100 Subject: [PATCH 653/979] Fix tests --- test/model/game/game_socket_events_test.dart | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/test/model/game/game_socket_events_test.dart b/test/model/game/game_socket_events_test.dart index 9e7e1ea526..7bb4597c5c 100644 --- a/test/model/game/game_socket_events_test.dart +++ b/test/model/game/game_socket_events_test.dart @@ -7,7 +7,6 @@ import 'package:lichess_mobile/src/model/common/perf.dart'; import 'package:lichess_mobile/src/model/common/speed.dart'; import 'package:lichess_mobile/src/model/game/game.dart'; import 'package:lichess_mobile/src/model/game/game_socket_events.dart'; -import 'package:lichess_mobile/src/model/game/playable_game.dart'; void main() { test('decode game full event from websocket json', () { @@ -16,13 +15,17 @@ void main() { final game = fullEvent.game; expect(game.id, const GameId('nV3DaALy')); expect( - game.clock, - PlayableClockData( - running: true, - white: const Duration(seconds: 149, milliseconds: 50), - black: const Duration(seconds: 775, milliseconds: 940), - at: DateTime.now(), - ), + game.clock?.running, + true, + ); + expect( + game.clock?.white, + const Duration(seconds: 149, milliseconds: 50), + ); + + expect( + game.clock?.black, + const Duration(seconds: 775, milliseconds: 940), ); expect( game.meta, From 63412a6b14f133bf6a8aaf6b91e637adb4a47049 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 6 Nov 2024 12:42:07 +0100 Subject: [PATCH 654/979] Add tests for countdown clock --- lib/src/widgets/countdown_clock.dart | 78 ++++++----- pubspec.lock | 2 +- pubspec.yaml | 1 + test/widgets/countdown_clock_test.dart | 182 +++++++++++++++++++++++++ 4 files changed, 230 insertions(+), 33 deletions(-) create mode 100644 test/widgets/countdown_clock_test.dart diff --git a/lib/src/widgets/countdown_clock.dart b/lib/src/widgets/countdown_clock.dart index 22115d9101..d33c096973 100644 --- a/lib/src/widgets/countdown_clock.dart +++ b/lib/src/widgets/countdown_clock.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:clock/clock.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -66,20 +67,19 @@ class CountdownClock extends ConsumerStatefulWidget { ConsumerState createState() => _CountdownClockState(); } -const _period = Duration(milliseconds: 100); const _emergencyDelay = Duration(seconds: 20); +const _showTenthsThreshold = Duration(seconds: 10); class _CountdownClockState extends ConsumerState { - Timer? _delayTimer; Timer? _timer; Duration timeLeft = Duration.zero; bool _shouldPlayEmergencyFeedback = true; DateTime? _nextEmergency; - final _stopwatch = Stopwatch(); + final _stopwatch = clock.stopwatch(); void startClock() { - final now = DateTime.now(); + final now = clock.now(); final delay = widget.delay ?? Duration.zero; final clockEventTime = widget.clockEventTime ?? now; // UI lag diff: the elapsed time between the time we received the clock event @@ -89,43 +89,50 @@ class _CountdownClockState extends ConsumerState { // so we need to adjust the delay. final realDelay = delay - uiLag; - if (realDelay > Duration.zero) { - _delayTimer?.cancel(); - _delayTimer = Timer(realDelay, _doStartClock); - } else if (realDelay < Duration.zero) { - // real delay is negative, so we need to adjust the timeLeft. + // real delay is negative, so we need to adjust the timeLeft. + if (realDelay < Duration.zero) { timeLeft = timeLeft + realDelay; - _doStartClock(); - } else { - _doStartClock(); } + + _scheduleTick(realDelay); } - void _doStartClock() { + void _scheduleTick(Duration extraDelay) { _timer?.cancel(); + final delay = Duration( + milliseconds: (timeLeft < _showTenthsThreshold + ? timeLeft.inMilliseconds % 100 + : timeLeft.inMilliseconds % 500) + + 1, + ); + _timer = Timer(delay + extraDelay, _tick); _stopwatch.reset(); _stopwatch.start(); - _timer = Timer.periodic(_period, (timer) { - setState(() { - timeLeft = timeLeft - _stopwatch.elapsed; - _stopwatch.reset(); - _playEmergencyFeedback(); - if (timeLeft <= Duration.zero) { - widget.onFlag?.call(); - timeLeft = Duration.zero; - stopClock(); - } - }); - }); } - void stopClock() { + void _tick() { setState(() { timeLeft = timeLeft - _stopwatch.elapsed; - if (timeLeft < Duration.zero) { + _playEmergencyFeedback(); + if (timeLeft <= Duration.zero) { + widget.onFlag?.call(); timeLeft = Duration.zero; } }); + if (timeLeft > Duration.zero) { + _scheduleTick(Duration.zero); + } + } + + void stopClock({bool countElapsedTime = true}) { + if (countElapsedTime) { + setState(() { + timeLeft = timeLeft - _stopwatch.elapsed; + if (timeLeft < Duration.zero) { + timeLeft = Duration.zero; + } + }); + } _timer?.cancel(); _stopwatch.stop(); scheduleMicrotask(() { @@ -137,9 +144,9 @@ class _CountdownClockState extends ConsumerState { if (widget.emergencyThreshold != null && timeLeft <= widget.emergencyThreshold! && _shouldPlayEmergencyFeedback && - (_nextEmergency == null || _nextEmergency!.isBefore(DateTime.now()))) { + (_nextEmergency == null || _nextEmergency!.isBefore(clock.now()))) { _shouldPlayEmergencyFeedback = false; - _nextEmergency = DateTime.now().add(_emergencyDelay); + _nextEmergency = clock.now().add(_emergencyDelay); if (widget.emergencySoundEnabled) { ref.read(soundServiceProvider).play(Sound.lowTime); } @@ -162,12 +169,19 @@ class _CountdownClockState extends ConsumerState { @override void didUpdateWidget(CountdownClock oldClock) { super.didUpdateWidget(oldClock); - if (widget.timeLeft != oldClock.timeLeft) { + final isSameTimeConfig = widget.timeLeft == oldClock.timeLeft; + if (!isSameTimeConfig) { timeLeft = widget.timeLeft; } if (widget.active != oldClock.active) { - widget.active ? startClock() : stopClock(); + if (widget.active) { + startClock(); + } else { + // If the timeLeft was changed at the same time as the clock is stopped + // we don't want to count the elapsed time because the new time takes precedence. + stopClock(countElapsedTime: isSameTimeConfig); + } } } @@ -229,7 +243,7 @@ class Clock extends StatelessWidget { final hours = timeLeft.inHours; final mins = timeLeft.inMinutes.remainder(60); final secs = timeLeft.inSeconds.remainder(60).toString().padLeft(2, '0'); - final showTenths = timeLeft < const Duration(seconds: 10); + final showTenths = timeLeft < _showTenthsThreshold; final isEmergency = emergencyThreshold != null && timeLeft <= emergencyThreshold!; final remainingHeight = estimateRemainingHeightLeftBoard(context); diff --git a/pubspec.lock b/pubspec.lock index d4c1cea94d..f3fe6bdd8e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -223,7 +223,7 @@ packages: source: hosted version: "0.4.2" clock: - dependency: transitive + dependency: "direct main" description: name: clock sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf diff --git a/pubspec.yaml b/pubspec.yaml index cdbd932c00..050bccf49d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,6 +15,7 @@ dependencies: auto_size_text: ^3.0.0 cached_network_image: ^3.2.2 chessground: ^6.0.0 + clock: ^1.1.1 collection: ^1.17.0 connectivity_plus: ^6.0.2 cronet_http: ^1.3.1 diff --git a/test/widgets/countdown_clock_test.dart b/test/widgets/countdown_clock_test.dart new file mode 100644 index 0000000000..6ab6b0618f --- /dev/null +++ b/test/widgets/countdown_clock_test.dart @@ -0,0 +1,182 @@ +import 'package:clock/clock.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:lichess_mobile/src/widgets/countdown_clock.dart'; + +void main() { + testWidgets('does not tick when not active', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: CountdownClock( + timeLeft: Duration(seconds: 10), + active: false, + ), + ), + ); + + expect(find.text('0:10', findRichText: true), findsOneWidget); + + await tester.pump(const Duration(seconds: 2)); + expect(find.text('0:10', findRichText: true), findsOneWidget); + }); + + testWidgets('ticks when active', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: CountdownClock( + timeLeft: Duration(seconds: 10), + active: true, + ), + ), + ); + + expect(find.text('0:10', findRichText: true), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:09.9', findRichText: true), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:09.8', findRichText: true), findsOneWidget); + await tester.pump(const Duration(seconds: 10)); + expect(find.text('0:00.0', findRichText: true), findsOneWidget); + }); + + testWidgets('ticks when active', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: CountdownClock( + timeLeft: Duration(seconds: 10), + active: true, + ), + ), + ); + + expect(find.text('0:10', findRichText: true), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:09.9', findRichText: true), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:09.8', findRichText: true), findsOneWidget); + await tester.pump(const Duration(seconds: 10)); + expect(find.text('0:00.0', findRichText: true), findsOneWidget); + }); + + testWidgets('shows milliseconds when time < 1s and active is false', + (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: CountdownClock( + timeLeft: Duration(seconds: 1), + active: true, + ), + ), + ); + + expect(find.text('0:01.0', findRichText: true), findsOneWidget); + await tester.pump(const Duration(milliseconds: 10)); + expect(find.text('0:00.9', findRichText: true), findsOneWidget); + await tester.pump(const Duration(milliseconds: 10)); + expect(find.text('0:00.9', findRichText: true), findsOneWidget); + + await tester.pumpWidget( + const MaterialApp( + home: CountdownClock( + timeLeft: Duration(milliseconds: 988), + active: false, + ), + ), + duration: const Duration(milliseconds: 1000), + ); + + expect(find.text('0:00.98', findRichText: true), findsOneWidget); + }); + + testWidgets('stops when active become false', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: CountdownClock( + timeLeft: Duration(seconds: 10), + active: true, + ), + ), + ); + + expect(find.text('0:10', findRichText: true), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:09.9', findRichText: true), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:09.8', findRichText: true), findsOneWidget); + + // clock is rebuilt with same time but inactive: + // the time is kept and the clock stops counting the elapsed time + await tester.pumpWidget( + const MaterialApp( + home: CountdownClock( + timeLeft: Duration(seconds: 10), + active: false, + ), + ), + duration: const Duration(milliseconds: 100), + ); + expect(find.text('0:09.7', findRichText: true), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:09.7', findRichText: true), findsOneWidget); + }); + + testWidgets('starts with a delay if set', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: CountdownClock( + timeLeft: Duration(seconds: 10), + active: true, + delay: Duration(milliseconds: 25), + ), + ), + ); + expect(find.text('0:10', findRichText: true), findsOneWidget); + await tester.pump(const Duration(milliseconds: 25)); + expect(find.text('0:10', findRichText: true), findsOneWidget); + await tester.pump(const Duration(milliseconds: 50)); + expect(find.text('0:09.9', findRichText: true), findsOneWidget); + }); + + testWidgets('compensates for UI lag if `clockEventTime` is set', + (WidgetTester tester) async { + final now = clock.now(); + await tester.pump(const Duration(milliseconds: 10)); + + await tester.pumpWidget( + MaterialApp( + home: CountdownClock( + timeLeft: const Duration(seconds: 10), + active: true, + delay: const Duration(milliseconds: 20), + clockEventTime: now, + ), + ), + ); + expect(find.text('0:10', findRichText: true), findsOneWidget); + + await tester.pump(const Duration(milliseconds: 10)); + expect(find.text('0:10', findRichText: true), findsOneWidget); + + // delay was 20m but UI lagged 10ms so with the compensation the clock has started already + await tester.pump(const Duration(milliseconds: 10)); + expect(find.text('0:09.9', findRichText: true), findsOneWidget); + }); + + testWidgets('UI lag makes negative start delay', (WidgetTester tester) async { + final now = clock.now(); + await tester.pump(const Duration(milliseconds: 20)); + + await tester.pumpWidget( + MaterialApp( + home: CountdownClock( + timeLeft: const Duration(seconds: 10), + active: true, + delay: const Duration(milliseconds: 10), + clockEventTime: now, + ), + ), + ); + // delay was 10ms but UI lagged 20ms so the clock time is already 10ms ahead + expect(find.text('0:09.9', findRichText: true), findsOneWidget); + }); +} From cc805a1b6d43ff0632bb975d3c5ee29fe70be617 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 6 Nov 2024 15:43:17 +0100 Subject: [PATCH 655/979] Add more countdown clock tests --- lib/src/view/game/game_body.dart | 4 +- lib/src/widgets/countdown_clock.dart | 18 ++-- test/widgets/countdown_clock_test.dart | 116 +++++++++++++++++++++++-- 3 files changed, 121 insertions(+), 17 deletions(-) diff --git a/lib/src/view/game/game_body.dart b/lib/src/view/game/game_body.dart index 1b793ded99..d2200b3216 100644 --- a/lib/src/view/game/game_body.dart +++ b/lib/src/view/game/game_body.dart @@ -151,7 +151,7 @@ class GameBody extends ConsumerWidget { ? CountdownClock( key: blackClockKey, delay: gameState.game.clock!.lag, - clockEventTime: gameState.game.clock!.at, + clockStartTime: gameState.game.clock!.at, timeLeft: archivedBlackClock ?? gameState.game.clock!.black, active: gameState.activeClockSide == Side.black, emergencyThreshold: youAre == Side.black @@ -194,7 +194,7 @@ class GameBody extends ConsumerWidget { key: whiteClockKey, timeLeft: archivedWhiteClock ?? gameState.game.clock!.white, delay: gameState.game.clock!.lag, - clockEventTime: gameState.game.clock!.at, + clockStartTime: gameState.game.clock!.at, active: gameState.activeClockSide == Side.white, emergencyThreshold: youAre == Side.white ? gameState.game.meta.clock?.emergency diff --git a/lib/src/widgets/countdown_clock.dart b/lib/src/widgets/countdown_clock.dart index d33c096973..9858d31fa6 100644 --- a/lib/src/widgets/countdown_clock.dart +++ b/lib/src/widgets/countdown_clock.dart @@ -15,7 +15,7 @@ class CountdownClock extends ConsumerStatefulWidget { const CountdownClock({ required this.timeLeft, this.delay, - this.clockEventTime, + this.clockStartTime, required this.active, this.emergencyThreshold, this.emergencySoundEnabled = true, @@ -34,11 +34,11 @@ class CountdownClock extends ConsumerStatefulWidget { /// This can be used to implement lag compensation. final Duration? delay; - /// The time the time left was received at. + /// The time at which the clock should have started. /// /// Use this parameter to synchronize the clock with the time at which the clock - /// event was received from the server. - final DateTime? clockEventTime; + /// event was received from the server and to compensate for UI lag. + final DateTime? clockStartTime; /// If [timeLeft] is less than [emergencyThreshold], the clock will change /// its background color to [ClockStyle.emergencyBackgroundColor] activeBackgroundColor @@ -81,15 +81,13 @@ class _CountdownClockState extends ConsumerState { void startClock() { final now = clock.now(); final delay = widget.delay ?? Duration.zero; - final clockEventTime = widget.clockEventTime ?? now; - // UI lag diff: the elapsed time between the time we received the clock event + final clockStartTime = widget.clockStartTime ?? now; + // UI lag diff: the elapsed time between the time the clock should have started // and the time the clock is actually started - final uiLag = now.difference(clockEventTime); - // The clock should have started at `clockEventTime`, but it started at `now`. - // so we need to adjust the delay. + final uiLag = now.difference(clockStartTime); final realDelay = delay - uiLag; - // real delay is negative, so we need to adjust the timeLeft. + // real delay is negative, we need to adjust the timeLeft. if (realDelay < Duration.zero) { timeLeft = timeLeft + realDelay; } diff --git a/test/widgets/countdown_clock_test.dart b/test/widgets/countdown_clock_test.dart index 6ab6b0618f..74169dae39 100644 --- a/test/widgets/countdown_clock_test.dart +++ b/test/widgets/countdown_clock_test.dart @@ -1,9 +1,19 @@ import 'package:clock/clock.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; import 'package:lichess_mobile/src/widgets/countdown_clock.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../test_provider_scope.dart'; + +class MockSoundService extends Mock implements SoundService {} void main() { + setUpAll(() { + registerFallbackValue(MockSoundService()); + }); + testWidgets('does not tick when not active', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( @@ -137,8 +147,7 @@ void main() { expect(find.text('0:09.9', findRichText: true), findsOneWidget); }); - testWidgets('compensates for UI lag if `clockEventTime` is set', - (WidgetTester tester) async { + testWidgets('compensates for UI lag', (WidgetTester tester) async { final now = clock.now(); await tester.pump(const Duration(milliseconds: 10)); @@ -148,7 +157,7 @@ void main() { timeLeft: const Duration(seconds: 10), active: true, delay: const Duration(milliseconds: 20), - clockEventTime: now, + clockStartTime: now, ), ), ); @@ -162,7 +171,7 @@ void main() { expect(find.text('0:09.9', findRichText: true), findsOneWidget); }); - testWidgets('UI lag makes negative start delay', (WidgetTester tester) async { + testWidgets('UI lag negative start delay', (WidgetTester tester) async { final now = clock.now(); await tester.pump(const Duration(milliseconds: 20)); @@ -172,11 +181,108 @@ void main() { timeLeft: const Duration(seconds: 10), active: true, delay: const Duration(milliseconds: 10), - clockEventTime: now, + clockStartTime: now, ), ), ); // delay was 10ms but UI lagged 20ms so the clock time is already 10ms ahead expect(find.text('0:09.9', findRichText: true), findsOneWidget); }); + + testWidgets('should call onFlag', (WidgetTester tester) async { + int flagCount = 0; + await tester.pumpWidget( + MaterialApp( + home: CountdownClock( + timeLeft: const Duration(seconds: 10), + active: true, + onFlag: () { + flagCount++; + }, + ), + ), + ); + expect(find.text('0:10', findRichText: true), findsOneWidget); + await tester.pump(const Duration(seconds: 11)); + expect(flagCount, 1); + expect(find.text('0:00.0', findRichText: true), findsOneWidget); + }); + + testWidgets('calls onStop', (WidgetTester tester) async { + int onStopCount = 0; + Duration? lastOnStopTime; + await tester.pumpWidget( + MaterialApp( + home: CountdownClock( + timeLeft: const Duration(seconds: 10), + active: true, + onStop: (Duration timeLeft) { + onStopCount++; + lastOnStopTime = timeLeft; + }, + ), + ), + ); + expect(find.text('0:10', findRichText: true), findsOneWidget); + await tester.pump(const Duration(seconds: 5)); + await tester.pumpWidget( + MaterialApp( + home: CountdownClock( + timeLeft: const Duration(seconds: 10), + active: false, + onStop: (Duration timeLeft) { + onStopCount++; + lastOnStopTime = timeLeft; + }, + ), + ), + ); + + expect(onStopCount, 1); + expect(lastOnStopTime, const Duration(seconds: 5)); + expect(find.text('0:05.0', findRichText: true), findsOneWidget); + }); + + testWidgets('emergency feedback', (WidgetTester tester) async { + final mockSoundService = MockSoundService(); + when(() => mockSoundService.play(Sound.lowTime)).thenAnswer((_) async {}); + + const timeLeft = Duration(seconds: 10); + + final app = await makeTestProviderScopeApp( + tester, + home: const CountdownClock( + timeLeft: timeLeft, + active: true, + emergencyThreshold: Duration(seconds: 5), + ), + overrides: [ + soundServiceProvider.overrideWith((ref) => mockSoundService), + ], + ); + await tester.pumpWidget(app); + + expect(find.text('0:10', findRichText: true), findsOneWidget); + await tester.pump(const Duration(seconds: 5)); + expect(find.text('0:05.0', findRichText: true), findsOneWidget); + verifyNever(() => mockSoundService.play(Sound.lowTime)); + await tester.pump(const Duration(milliseconds: 1)); + verify(() => mockSoundService.play(Sound.lowTime)).called(1); + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:04.8', findRichText: true), findsOneWidget); + // emergency is only called once as long as the time is below the threshold + verifyNever(() => mockSoundService.play(Sound.lowTime)); + + // notifier.value = const CountdownClock( + // timeLeft: Duration(milliseconds: 5100), + // active: true, + // emergencyThreshold: Duration(seconds: 5), + // ); + // await tester.pump(); + // expect(find.text('0:05.1', findRichText: true), findsOneWidget); + // await tester.pump(const Duration(milliseconds: 150)); + // expect(find.text('0:04.9', findRichText: true), findsOneWidget); + // // emergency is called again after the threshold is passed + // verify(() => mockSoundService.play(Sound.lowTime)).called(1); + }); } From 0ed212703975db6e259c5124390068c339d03a28 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 6 Nov 2024 17:37:02 +0100 Subject: [PATCH 656/979] Update clock --- lib/src/model/game/game_controller.dart | 5 ++ lib/src/view/game/game_body.dart | 8 +- lib/src/widgets/countdown_clock.dart | 25 +++--- test/widgets/countdown_clock_test.dart | 104 ++++++++++++++---------- 4 files changed, 85 insertions(+), 57 deletions(-) diff --git a/lib/src/model/game/game_controller.dart b/lib/src/model/game/game_controller.dart index c47dd9eb22..7d0c9f4f07 100644 --- a/lib/src/model/game/game_controller.dart +++ b/lib/src/model/game/game_controller.dart @@ -334,6 +334,11 @@ class GameController extends _$GameController { } } + /// Play a sound when the clock is about to run out + void onClockEmergency() { + ref.read(soundServiceProvider).play(Sound.lowTime); + } + void onFlag() { _onFlagThrottler(() { if (state.hasValue) { diff --git a/lib/src/view/game/game_body.dart b/lib/src/view/game/game_body.dart index d2200b3216..02f9d648e0 100644 --- a/lib/src/view/game/game_body.dart +++ b/lib/src/view/game/game_body.dart @@ -157,7 +157,9 @@ class GameBody extends ConsumerWidget { emergencyThreshold: youAre == Side.black ? gameState.game.meta.clock?.emergency : null, - emergencySoundEnabled: emergencySoundEnabled, + onEmergency: emergencySoundEnabled + ? () => ref.read(ctrlProvider.notifier).onClockEmergency() + : null, onFlag: () => ref.read(ctrlProvider.notifier).onFlag(), ) : gameState.game.correspondenceClock != null @@ -199,7 +201,9 @@ class GameBody extends ConsumerWidget { emergencyThreshold: youAre == Side.white ? gameState.game.meta.clock?.emergency : null, - emergencySoundEnabled: emergencySoundEnabled, + onEmergency: emergencySoundEnabled + ? () => ref.read(ctrlProvider.notifier).onClockEmergency() + : null, onFlag: () => ref.read(ctrlProvider.notifier).onFlag(), ) : gameState.game.correspondenceClock != null diff --git a/lib/src/widgets/countdown_clock.dart b/lib/src/widgets/countdown_clock.dart index 9858d31fa6..ec76e7c8e2 100644 --- a/lib/src/widgets/countdown_clock.dart +++ b/lib/src/widgets/countdown_clock.dart @@ -3,22 +3,20 @@ import 'dart:async'; import 'package:clock/clock.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/constants.dart'; -import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; /// A countdown clock. /// /// The clock starts only when [active] is `true`. -class CountdownClock extends ConsumerStatefulWidget { +class CountdownClock extends StatefulWidget { const CountdownClock({ required this.timeLeft, this.delay, this.clockStartTime, required this.active, this.emergencyThreshold, - this.emergencySoundEnabled = true, + this.onEmergency, this.onFlag, this.onStop, this.clockStyle, @@ -40,13 +38,13 @@ class CountdownClock extends ConsumerStatefulWidget { /// event was received from the server and to compensate for UI lag. final DateTime? clockStartTime; - /// If [timeLeft] is less than [emergencyThreshold], the clock will change - /// its background color to [ClockStyle.emergencyBackgroundColor] activeBackgroundColor - /// If [emergencySoundEnabled] is `true`, the clock will also play a sound. + /// The duration at which the clock should change its background color to indicate an emergency. + /// + /// If [onEmergency] is provided, the clock will call it when the emergency threshold is reached. final Duration? emergencyThreshold; - /// Whether to play an emergency sound when the clock reaches the emergency - final bool emergencySoundEnabled; + /// Called when the clock reaches the emergency. + final VoidCallback? onEmergency; /// If [active] is `true`, the clock starts counting down. final bool active; @@ -64,13 +62,13 @@ class CountdownClock extends ConsumerStatefulWidget { final bool padLeft; @override - ConsumerState createState() => _CountdownClockState(); + State createState() => _CountdownClockState(); } const _emergencyDelay = Duration(seconds: 20); const _showTenthsThreshold = Duration(seconds: 10); -class _CountdownClockState extends ConsumerState { +class _CountdownClockState extends State { Timer? _timer; Duration timeLeft = Duration.zero; bool _shouldPlayEmergencyFeedback = true; @@ -145,9 +143,7 @@ class _CountdownClockState extends ConsumerState { (_nextEmergency == null || _nextEmergency!.isBefore(clock.now()))) { _shouldPlayEmergencyFeedback = false; _nextEmergency = clock.now().add(_emergencyDelay); - if (widget.emergencySoundEnabled) { - ref.read(soundServiceProvider).play(Sound.lowTime); - } + widget.onEmergency?.call(); HapticFeedback.heavyImpact(); } else if (widget.emergencyThreshold != null && timeLeft > widget.emergencyThreshold! * 1.5) { @@ -168,6 +164,7 @@ class _CountdownClockState extends ConsumerState { void didUpdateWidget(CountdownClock oldClock) { super.didUpdateWidget(oldClock); final isSameTimeConfig = widget.timeLeft == oldClock.timeLeft; + if (!isSameTimeConfig) { timeLeft = widget.timeLeft; } diff --git a/test/widgets/countdown_clock_test.dart b/test/widgets/countdown_clock_test.dart index 74169dae39..4c819887c9 100644 --- a/test/widgets/countdown_clock_test.dart +++ b/test/widgets/countdown_clock_test.dart @@ -1,19 +1,9 @@ import 'package:clock/clock.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; import 'package:lichess_mobile/src/widgets/countdown_clock.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../test_provider_scope.dart'; - -class MockSoundService extends Mock implements SoundService {} void main() { - setUpAll(() { - registerFallbackValue(MockSoundService()); - }); - testWidgets('does not tick when not active', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( @@ -49,7 +39,42 @@ void main() { expect(find.text('0:00.0', findRichText: true), findsOneWidget); }); - testWidgets('ticks when active', (WidgetTester tester) async { + testWidgets('update time by changing widget configuration', + (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: CountdownClock( + timeLeft: Duration(seconds: 10), + active: true, + ), + ), + ); + + expect(find.text('0:10', findRichText: true), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:09.9', findRichText: true), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:09.8', findRichText: true), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:09.7', findRichText: true), findsOneWidget); + + await tester.pumpWidget( + const MaterialApp( + home: CountdownClock( + timeLeft: Duration(seconds: 11), + active: true, + ), + ), + ); + expect(find.text('0:11', findRichText: true), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:10', findRichText: true), findsOneWidget); + await tester.pump(const Duration(seconds: 11)); + expect(find.text('0:00.0', findRichText: true), findsOneWidget); + }); + + testWidgets('do not update if timeLeft widget configuration is same', + (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: CountdownClock( @@ -64,6 +89,19 @@ void main() { expect(find.text('0:09.9', findRichText: true), findsOneWidget); await tester.pump(const Duration(milliseconds: 100)); expect(find.text('0:09.8', findRichText: true), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:09.7', findRichText: true), findsOneWidget); + + await tester.pumpWidget( + const MaterialApp( + home: CountdownClock( + timeLeft: Duration(seconds: 10), + active: true, + ), + ), + ); + + expect(find.text('0:09.7', findRichText: true), findsOneWidget); await tester.pump(const Duration(seconds: 10)); expect(find.text('0:00.0', findRichText: true), findsOneWidget); }); @@ -244,45 +282,29 @@ void main() { }); testWidgets('emergency feedback', (WidgetTester tester) async { - final mockSoundService = MockSoundService(); - when(() => mockSoundService.play(Sound.lowTime)).thenAnswer((_) async {}); - - const timeLeft = Duration(seconds: 10); - - final app = await makeTestProviderScopeApp( - tester, - home: const CountdownClock( - timeLeft: timeLeft, - active: true, - emergencyThreshold: Duration(seconds: 5), + int onEmergencyCount = 0; + await tester.pumpWidget( + MaterialApp( + home: CountdownClock( + timeLeft: const Duration(seconds: 10), + active: true, + emergencyThreshold: const Duration(seconds: 5), + onEmergency: () { + onEmergencyCount++; + }, + ), ), - overrides: [ - soundServiceProvider.overrideWith((ref) => mockSoundService), - ], ); - await tester.pumpWidget(app); expect(find.text('0:10', findRichText: true), findsOneWidget); await tester.pump(const Duration(seconds: 5)); expect(find.text('0:05.0', findRichText: true), findsOneWidget); - verifyNever(() => mockSoundService.play(Sound.lowTime)); + expect(onEmergencyCount, 0); await tester.pump(const Duration(milliseconds: 1)); - verify(() => mockSoundService.play(Sound.lowTime)).called(1); + expect(onEmergencyCount, 1); await tester.pump(const Duration(milliseconds: 100)); expect(find.text('0:04.8', findRichText: true), findsOneWidget); // emergency is only called once as long as the time is below the threshold - verifyNever(() => mockSoundService.play(Sound.lowTime)); - - // notifier.value = const CountdownClock( - // timeLeft: Duration(milliseconds: 5100), - // active: true, - // emergencyThreshold: Duration(seconds: 5), - // ); - // await tester.pump(); - // expect(find.text('0:05.1', findRichText: true), findsOneWidget); - // await tester.pump(const Duration(milliseconds: 150)); - // expect(find.text('0:04.9', findRichText: true), findsOneWidget); - // // emergency is called again after the threshold is passed - // verify(() => mockSoundService.play(Sound.lowTime)).called(1); + expect(onEmergencyCount, 1); }); } From 96631c2b7beeab23939d5b0619f4132fd4f39dc9 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 6 Nov 2024 19:29:22 +0100 Subject: [PATCH 657/979] Improve TV clock --- lib/src/model/game/playable_game.dart | 5 ++++- lib/src/model/tv/tv_controller.dart | 14 ++++++++++++++ lib/src/view/watch/tv_screen.dart | 2 ++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/lib/src/model/game/playable_game.dart b/lib/src/model/game/playable_game.dart index 26614bb500..9c2a6feb7f 100644 --- a/lib/src/model/game/playable_game.dart +++ b/lib/src/model/game/playable_game.dart @@ -181,7 +181,7 @@ class PlayableClockData with _$PlayableClockData { /// The network lag of the clock. /// /// Will be sent along with move events. - Duration? lag, + required Duration? lag, /// The time when the clock event was received. required DateTime at, @@ -313,6 +313,9 @@ PlayableClockData _playableClockDataFromPick(RequiredPick pick) { running: pick('running').asBoolOrThrow(), white: pick('white').asDurationFromSecondsOrThrow(), black: pick('black').asDurationFromSecondsOrThrow(), + lag: pick('lag').letOrNull( + (it) => Duration(milliseconds: it.asIntOrThrow() * 10), + ), at: DateTime.now(), ); } diff --git a/lib/src/model/tv/tv_controller.dart b/lib/src/model/tv/tv_controller.dart index f53e002667..330533c192 100644 --- a/lib/src/model/tv/tv_controller.dart +++ b/lib/src/model/tv/tv_controller.dart @@ -212,6 +212,8 @@ class TvController extends _$TvController { newState = newState.copyWith.game.clock!( white: data.clock!.white, black: data.clock!.black, + lag: data.clock!.lag, + at: data.clock!.at, ); } if (!curState.isReplaying) { @@ -228,6 +230,18 @@ class TvController extends _$TvController { state = AsyncData(newState); + case 'endData': + final endData = + GameEndEvent.fromJson(event.data as Map); + state = AsyncData( + state.requireValue.copyWith( + game: state.requireValue.game.copyWith( + status: endData.status, + winner: endData.winner, + ), + ), + ); + case 'tvSelect': final json = event.data as Map; final eventChannel = pick(json, 'channel').asTvChannelOrNull(); diff --git a/lib/src/view/watch/tv_screen.dart b/lib/src/view/watch/tv_screen.dart index 96b852c318..1cd02b3166 100644 --- a/lib/src/view/watch/tv_screen.dart +++ b/lib/src/view/watch/tv_screen.dart @@ -95,6 +95,7 @@ class _Body extends ConsumerWidget { ? CountdownClock( key: blackClockKey, timeLeft: gameState.game.clock!.black, + clockStartTime: gameState.game.clock!.at, active: gameState.activeClockSide == Side.black, ) : null, @@ -106,6 +107,7 @@ class _Body extends ConsumerWidget { ? CountdownClock( key: whiteClockKey, timeLeft: gameState.game.clock!.white, + clockStartTime: gameState.game.clock!.at, active: gameState.activeClockSide == Side.white, ) : null, From a31718e2be7ffc73c1936723e0485d91ffc28e59 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 7 Nov 2024 11:43:40 +0100 Subject: [PATCH 658/979] Reimplement standalone clock with a new ChessClock class --- lib/src/model/clock/chess_clock.dart | 187 +++++++++++++++ lib/src/model/clock/clock_controller.dart | 191 --------------- .../model/clock/clock_tool_controller.dart | 227 ++++++++++++++++++ lib/src/model/game/game_controller.dart | 11 +- lib/src/view/clock/clock_screen.dart | 74 +++--- lib/src/view/clock/clock_settings.dart | 18 +- lib/src/view/clock/custom_clock_settings.dart | 6 +- lib/src/view/game/game_body.dart | 4 +- lib/src/view/watch/tv_screen.dart | 4 +- lib/src/widgets/countdown_clock.dart | 34 +-- test/model/clock/chess_clock_test.dart | 186 ++++++++++++++ test/widgets/countdown_clock_test.dart | 63 ++--- 12 files changed, 686 insertions(+), 319 deletions(-) create mode 100644 lib/src/model/clock/chess_clock.dart delete mode 100644 lib/src/model/clock/clock_controller.dart create mode 100644 lib/src/model/clock/clock_tool_controller.dart create mode 100644 test/model/clock/chess_clock_test.dart diff --git a/lib/src/model/clock/chess_clock.dart b/lib/src/model/clock/chess_clock.dart new file mode 100644 index 0000000000..bb1b82cbf4 --- /dev/null +++ b/lib/src/model/clock/chess_clock.dart @@ -0,0 +1,187 @@ +import 'dart:async'; + +import 'package:clock/clock.dart'; +import 'package:dartchess/dartchess.dart'; +import 'package:flutter/foundation.dart'; + +const _emergencyDelay = Duration(seconds: 20); +const _tickDelay = Duration(milliseconds: 100); + +/// A chess clock. +/// +/// The clock will call the [onFlag] callback when a side's time reaches zero. +class ChessClock { + ChessClock({ + required Duration whiteTime, + required Duration blackTime, + this.emergencyThreshold, + this.onFlag, + this.onEmergency, + }) : _whiteTime = ValueNotifier(whiteTime), + _blackTime = ValueNotifier(blackTime), + _activeSide = Side.white; + + /// The duration at which the clock should change its background color to indicate an emergency. + /// + /// If [onEmergency] is provided, the clock will call it when the emergency threshold is reached. + final Duration? emergencyThreshold; + + /// Callback when the clock reaches zero. + VoidCallback? onFlag; + + /// Called when the clock reaches the emergency. + final VoidCallback? onEmergency; + + Timer? _timer; + Timer? _startDelayTimer; + DateTime? _lastStarted; + final _stopwatch = clock.stopwatch(); + bool _shouldPlayEmergencyFeedback = true; + DateTime? _nextEmergency; + + final ValueNotifier _whiteTime; + final ValueNotifier _blackTime; + Side _activeSide; + + bool get isRunning { + return _lastStarted != null; + } + + /// Returns the current white time. + ValueListenable get whiteTime => _whiteTime; + + /// Returns the current black time. + ValueListenable get blackTime => _blackTime; + + /// Returns the current active time. + ValueListenable get activeTime => _activeTime; + + /// Returns the current active side. + Side get activeSide => _activeSide; + + /// Sets the time for either side. + void setTimes({Duration? whiteTime, Duration? blackTime}) { + if (whiteTime != null) { + _whiteTime.value = whiteTime; + } + if (blackTime != null) { + _blackTime.value = blackTime; + } + } + + /// Sets the time for the given side. + void setTime(Side side, Duration time) { + if (side == Side.white) { + _whiteTime.value = time; + } else { + _blackTime.value = time; + } + } + + /// Increments the time for either side. + void incTimes({Duration? whiteInc, Duration? blackInc}) { + if (whiteInc != null) { + _whiteTime.value += whiteInc; + } + if (blackInc != null) { + _blackTime.value += blackInc; + } + } + + /// Increments the time for the given side. + void incTime(Side side, Duration increment) { + if (side == Side.white) { + _whiteTime.value += increment; + } else { + _blackTime.value += increment; + } + } + + /// Starts the clock for the given side. + /// + /// The [delay] parameter can be used to add a delay before the clock starts counting down. This is useful for lag compensation. + /// + /// Returns the think time of the active side before switching or `null` if the clock is not running. + Duration? startForSide(Side side, [Duration delay = Duration.zero]) { + _activeSide = side; + start(delay); + if (isRunning) { + return _thinkTime; + } + return null; + } + + /// Starts the clock for the active side. + /// + /// The [delay] parameter can be used to add a delay before the clock starts counting down. This is useful for lag compensation. + void start([Duration delay = Duration.zero]) { + _lastStarted = clock.now().add(delay); + _startDelayTimer?.cancel(); + _startDelayTimer = Timer(delay, _scheduleTick); + } + + /// Pauses the clock. + /// + /// Returns the current think time for the active side. + Duration stop() { + _stopwatch.stop(); + _startDelayTimer?.cancel(); + _timer?.cancel(); + final thinkTime = _thinkTime ?? Duration.zero; + _lastStarted = null; + return thinkTime; + } + + void dispose() { + _timer?.cancel(); + _startDelayTimer?.cancel(); + _whiteTime.dispose(); + _blackTime.dispose(); + } + + /// Returns the current think time for the active side. + Duration? get _thinkTime { + if (_lastStarted == null) { + return null; + } + return clock.now().difference(_lastStarted!); + } + + ValueNotifier get _activeTime { + return activeSide == Side.white ? _whiteTime : _blackTime; + } + + void _scheduleTick() { + _stopwatch.reset(); + _stopwatch.start(); + _timer?.cancel(); + _timer = Timer(_tickDelay, _tick); + } + + void _tick() { + final newTime = _activeTime.value - _stopwatch.elapsed; + _activeTime.value = newTime < Duration.zero ? Duration.zero : newTime; + _checkEmergency(); + if (_activeTime.value == Duration.zero) { + onFlag?.call(); + } + if (_activeTime.value > Duration.zero) { + _scheduleTick(); + } + } + + void _checkEmergency() { + final timeLeft = _activeTime.value; + if (emergencyThreshold != null && + timeLeft <= emergencyThreshold! && + _shouldPlayEmergencyFeedback && + (_nextEmergency == null || _nextEmergency!.isBefore(clock.now()))) { + _shouldPlayEmergencyFeedback = false; + _nextEmergency = clock.now().add(_emergencyDelay); + onEmergency?.call(); + } else if (emergencyThreshold != null && + timeLeft > emergencyThreshold! * 1.5) { + _shouldPlayEmergencyFeedback = true; + } + } +} diff --git a/lib/src/model/clock/clock_controller.dart b/lib/src/model/clock/clock_controller.dart deleted file mode 100644 index 1a70a0ff92..0000000000 --- a/lib/src/model/clock/clock_controller.dart +++ /dev/null @@ -1,191 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; -import 'package:lichess_mobile/src/model/common/time_increment.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'clock_controller.freezed.dart'; -part 'clock_controller.g.dart'; - -@riverpod -class ClockController extends _$ClockController { - @override - ClockState build() { - const time = Duration(minutes: 10); - const increment = Duration.zero; - return ClockState.fromOptions( - const ClockOptions( - timePlayerTop: time, - timePlayerBottom: time, - incrementPlayerTop: increment, - incrementPlayerBottom: increment, - ), - ); - } - - void onTap(ClockPlayerType playerType) { - final started = state.started; - if (playerType == ClockPlayerType.top) { - state = state.copyWith( - started: true, - activeSide: ClockPlayerType.bottom, - playerTopMoves: started ? state.playerTopMoves + 1 : 0, - ); - } else { - state = state.copyWith( - started: true, - activeSide: ClockPlayerType.top, - playerBottomMoves: started ? state.playerBottomMoves + 1 : 0, - ); - } - ref.read(soundServiceProvider).play(Sound.clock); - } - - void updateDuration(ClockPlayerType playerType, Duration duration) { - if (state.loser != null || state.paused) { - return; - } - - if (playerType == ClockPlayerType.top) { - state = state.copyWith( - playerTopTime: duration + state.options.incrementPlayerTop, - ); - } else { - state = state.copyWith( - playerBottomTime: duration + state.options.incrementPlayerBottom, - ); - } - } - - void updateOptions(TimeIncrement timeIncrement) => - state = ClockState.fromTimeIncrement(timeIncrement); - - void updateOptionsCustom( - TimeIncrement clock, - ClockPlayerType player, - ) => - state = ClockState.fromOptions( - ClockOptions( - timePlayerTop: player == ClockPlayerType.top - ? Duration(seconds: clock.time) - : state.options.timePlayerTop, - timePlayerBottom: player == ClockPlayerType.bottom - ? Duration(seconds: clock.time) - : state.options.timePlayerBottom, - incrementPlayerTop: player == ClockPlayerType.top - ? Duration(seconds: clock.increment) - : state.options.incrementPlayerTop, - incrementPlayerBottom: player == ClockPlayerType.bottom - ? Duration(seconds: clock.increment) - : state.options.incrementPlayerBottom, - ), - ); - - void setActiveSide(ClockPlayerType playerType) => - state = state.copyWith(activeSide: playerType); - - void setLoser(ClockPlayerType playerType) => - state = state.copyWith(loser: playerType); - - void reset() => state = ClockState.fromOptions(state.options); - - void start() => state = state.copyWith(started: true); - - void pause() => state = state.copyWith(paused: true); - - void resume() => state = state.copyWith(paused: false); -} - -enum ClockPlayerType { top, bottom } - -@freezed -class ClockOptions with _$ClockOptions { - const ClockOptions._(); - - const factory ClockOptions({ - required Duration timePlayerTop, - required Duration timePlayerBottom, - required Duration incrementPlayerTop, - required Duration incrementPlayerBottom, - }) = _ClockOptions; -} - -@freezed -class ClockState with _$ClockState { - const ClockState._(); - - const factory ClockState({ - required int id, - required ClockOptions options, - required Duration playerTopTime, - required Duration playerBottomTime, - required ClockPlayerType activeSide, - ClockPlayerType? loser, - @Default(false) bool started, - @Default(false) bool paused, - @Default(0) int playerTopMoves, - @Default(0) int playerBottomMoves, - }) = _ClockState; - - factory ClockState.fromTimeIncrement(TimeIncrement timeIncrement) { - final options = ClockOptions( - timePlayerTop: Duration(seconds: timeIncrement.time), - timePlayerBottom: Duration(seconds: timeIncrement.time), - incrementPlayerTop: Duration(seconds: timeIncrement.increment), - incrementPlayerBottom: Duration(seconds: timeIncrement.increment), - ); - - return ClockState( - id: DateTime.now().millisecondsSinceEpoch, - options: options, - activeSide: ClockPlayerType.top, - playerTopTime: options.timePlayerTop, - playerBottomTime: options.timePlayerBottom, - ); - } - - factory ClockState.fromSeparateTimeIncrements( - TimeIncrement playerTop, - TimeIncrement playerBottom, - ) { - final options = ClockOptions( - timePlayerTop: Duration(seconds: playerTop.time), - timePlayerBottom: Duration(seconds: playerBottom.time), - incrementPlayerTop: Duration(seconds: playerTop.increment), - incrementPlayerBottom: Duration(seconds: playerBottom.increment), - ); - return ClockState( - id: DateTime.now().millisecondsSinceEpoch, - activeSide: ClockPlayerType.top, - options: options, - playerTopTime: options.timePlayerTop, - playerBottomTime: options.timePlayerBottom, - ); - } - - factory ClockState.fromOptions(ClockOptions options) { - return ClockState( - id: DateTime.now().millisecondsSinceEpoch, - activeSide: ClockPlayerType.top, - options: options, - playerTopTime: options.timePlayerTop, - playerBottomTime: options.timePlayerBottom, - ); - } - - Duration getDuration(ClockPlayerType playerType) => - playerType == ClockPlayerType.top ? playerTopTime : playerBottomTime; - - int getMovesCount(ClockPlayerType playerType) => - playerType == ClockPlayerType.top ? playerTopMoves : playerBottomMoves; - - bool isPlayersTurn(ClockPlayerType playerType) => - started && activeSide == playerType && loser == null; - - bool isPlayersMoveAllowed(ClockPlayerType playerType) => - isPlayersTurn(playerType) && !paused; - - bool isActivePlayer(ClockPlayerType playerType) => - isPlayersTurn(playerType) && !paused; - - bool isLoser(ClockPlayerType playerType) => loser == playerType; -} diff --git a/lib/src/model/clock/clock_tool_controller.dart b/lib/src/model/clock/clock_tool_controller.dart new file mode 100644 index 0000000000..aa506d5966 --- /dev/null +++ b/lib/src/model/clock/clock_tool_controller.dart @@ -0,0 +1,227 @@ +import 'package:dartchess/dartchess.dart'; +import 'package:flutter/foundation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:lichess_mobile/src/model/clock/chess_clock.dart'; +import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; +import 'package:lichess_mobile/src/model/common/time_increment.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'clock_tool_controller.freezed.dart'; +part 'clock_tool_controller.g.dart'; + +@riverpod +class ClockToolController extends _$ClockToolController { + late ChessClock _clock; + + @override + ClockState build() { + const time = Duration(minutes: 10); + const increment = Duration.zero; + const options = ClockOptions( + whiteTime: time, + blackTime: time, + whiteIncrement: increment, + blackIncrement: increment, + ); + _clock = ChessClock( + whiteTime: time, + blackTime: time, + ); + + ref.onDispose(() { + _clock.dispose(); + }); + + return ClockState( + options: options, + whiteTime: _clock.whiteTime, + blackTime: _clock.blackTime, + activeSide: Side.white, + ); + } + + void onTap(Side playerType) { + final started = state.started; + if (playerType == Side.white) { + state = state.copyWith( + started: true, + activeSide: Side.black, + whiteMoves: started ? state.whiteMoves + 1 : 0, + ); + } else { + state = state.copyWith( + started: true, + activeSide: Side.white, + blackMoves: started ? state.blackMoves + 1 : 0, + ); + } + ref.read(soundServiceProvider).play(Sound.clock); + _clock.startForSide(playerType.opposite); + _clock.incTime( + playerType, + playerType == Side.white + ? state.options.whiteIncrement + : state.options.blackIncrement, + ); + } + + void updateDuration(Side playerType, Duration duration) { + if (state.loser != null || state.paused) { + return; + } + + _clock.setTimes( + whiteTime: playerType == Side.white + ? duration + state.options.whiteIncrement + : null, + blackTime: playerType == Side.black + ? duration + state.options.blackIncrement + : null, + ); + } + + void updateOptions(TimeIncrement timeIncrement) { + final options = ClockOptions.fromTimeIncrement(timeIncrement); + _clock = ChessClock( + whiteTime: options.whiteTime, + blackTime: options.blackTime, + ); + state = state.copyWith( + options: options, + whiteTime: _clock.whiteTime, + blackTime: _clock.blackTime, + ); + } + + void updateOptionsCustom( + TimeIncrement clock, + Side player, + ) { + final options = ClockOptions( + whiteTime: player == Side.white + ? Duration(seconds: clock.time) + : state.options.whiteTime, + blackTime: player == Side.black + ? Duration(seconds: clock.time) + : state.options.blackTime, + whiteIncrement: player == Side.white + ? Duration(seconds: clock.increment) + : state.options.whiteIncrement, + blackIncrement: player == Side.black + ? Duration(seconds: clock.increment) + : state.options.blackIncrement, + ); + _clock = ChessClock( + whiteTime: options.whiteTime, + blackTime: options.blackTime, + ); + state = ClockState( + options: options, + whiteTime: _clock.whiteTime, + blackTime: _clock.blackTime, + activeSide: state.activeSide, + ); + } + + void setBottomPlayer(Side playerType) => + state = state.copyWith(bottomPlayer: playerType); + + void setLoser(Side playerType) => state = state.copyWith(loser: playerType); + + void reset() { + _clock = ChessClock( + whiteTime: state.options.whiteTime, + blackTime: state.options.whiteTime, + ); + state = state.copyWith( + whiteTime: _clock.whiteTime, + blackTime: _clock.blackTime, + activeSide: Side.white, + loser: null, + started: false, + paused: false, + whiteMoves: 0, + blackMoves: 0, + ); + } + + void start() { + _clock.start(); + state = state.copyWith(started: true); + } + + void pause() { + _clock.stop(); + state = state.copyWith(paused: true); + } + + void resume() { + _clock.start(); + state = state.copyWith(paused: false); + } +} + +@freezed +class ClockOptions with _$ClockOptions { + const ClockOptions._(); + + const factory ClockOptions({ + required Duration whiteTime, + required Duration blackTime, + required Duration whiteIncrement, + required Duration blackIncrement, + }) = _ClockOptions; + + factory ClockOptions.fromTimeIncrement(TimeIncrement timeIncrement) => + ClockOptions( + whiteTime: Duration(seconds: timeIncrement.time), + blackTime: Duration(seconds: timeIncrement.time), + whiteIncrement: Duration(seconds: timeIncrement.increment), + blackIncrement: Duration(seconds: timeIncrement.increment), + ); + + factory ClockOptions.fromSeparateTimeIncrements( + TimeIncrement playerTop, + TimeIncrement playerBottom, + ) => + ClockOptions( + whiteTime: Duration(seconds: playerTop.time), + blackTime: Duration(seconds: playerBottom.time), + whiteIncrement: Duration(seconds: playerTop.increment), + blackIncrement: Duration(seconds: playerBottom.increment), + ); +} + +@freezed +class ClockState with _$ClockState { + const ClockState._(); + + const factory ClockState({ + required ClockOptions options, + required ValueListenable whiteTime, + required ValueListenable blackTime, + required Side activeSide, + @Default(Side.white) Side bottomPlayer, + Side? loser, + @Default(false) bool started, + @Default(false) bool paused, + @Default(0) int whiteMoves, + @Default(0) int blackMoves, + }) = _ClockState; + + ValueListenable getDuration(Side playerType) => + playerType == Side.white ? whiteTime : blackTime; + + int getMovesCount(Side playerType) => + playerType == Side.white ? whiteMoves : blackMoves; + + bool isPlayersTurn(Side playerType) => + started && activeSide == playerType && loser == null; + + bool isPlayersMoveAllowed(Side playerType) => + isPlayersTurn(playerType) && !paused; + + bool isActivePlayer(Side playerType) => isPlayersTurn(playerType) && !paused; + + bool isLoser(Side playerType) => loser == playerType; +} diff --git a/lib/src/model/game/game_controller.dart b/lib/src/model/game/game_controller.dart index 7d0c9f4f07..468a995f91 100644 --- a/lib/src/model/game/game_controller.dart +++ b/lib/src/model/game/game_controller.dart @@ -63,7 +63,7 @@ class GameController extends _$GameController { int? _socketEventVersion; /// Last move time - DateTime? _lastMoveTime; + DateTime? _lastClockUpdateAt; late SocketClient _socketClient; @@ -424,8 +424,8 @@ class GameController extends _$GameController { final moveTime = hasClock ? isPremove == true ? Duration.zero - : _lastMoveTime != null - ? DateTime.now().difference(_lastMoveTime!) + : _lastClockUpdateAt != null + ? DateTime.now().difference(_lastClockUpdateAt!) : null : null; _socketClient.send( @@ -547,7 +547,7 @@ class GameController extends _$GameController { return; } _socketEventVersion = fullEvent.socketEventVersion; - _lastMoveTime = null; + _lastClockUpdateAt = null; state = AsyncValue.data( GameState( @@ -608,7 +608,7 @@ class GameController extends _$GameController { } if (data.clock != null) { - _lastMoveTime = DateTime.now(); + _lastClockUpdateAt = data.clock!.at; final lagCompensation = newState.game.playable // server will send the lag only if it's more than 10ms @@ -1000,6 +1000,7 @@ class GameState with _$GameState { int? lastDrawOfferAtPly, Duration? opponentLeftCountdown, required bool stopClockWaitingForServerAck, + DateTime? clockSwitchedAt, /// Promotion waiting to be selected (only if auto queen is disabled) NormalMove? promotionMove, diff --git a/lib/src/view/clock/clock_screen.dart b/lib/src/view/clock/clock_screen.dart index f58bc78b2d..dec68243ec 100644 --- a/lib/src/view/clock/clock_screen.dart +++ b/lib/src/view/clock/clock_screen.dart @@ -1,7 +1,9 @@ +import 'package:dartchess/dartchess.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lichess_mobile/src/model/clock/clock_controller.dart'; +import 'package:lichess_mobile/src/model/clock/clock_tool_controller.dart'; import 'package:lichess_mobile/src/model/common/time_increment.dart'; +import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/immersive_mode.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; @@ -23,12 +25,14 @@ class ClockScreen extends StatelessWidget { } } +enum TilePosition { bottom, top } + class _Body extends ConsumerWidget { const _Body(); @override Widget build(BuildContext context, WidgetRef ref) { - final state = ref.watch(clockControllerProvider); + final state = ref.watch(clockToolControllerProvider); return OrientationBuilder( builder: (context, orientation) { @@ -37,16 +41,18 @@ class _Body extends ConsumerWidget { children: [ Expanded( child: ClockTile( + position: TilePosition.top, orientation: orientation, - playerType: ClockPlayerType.top, + playerType: state.bottomPlayer.opposite, clockState: state, ), ), ClockSettings(orientation: orientation), Expanded( child: ClockTile( + position: TilePosition.bottom, orientation: orientation, - playerType: ClockPlayerType.bottom, + playerType: state.bottomPlayer, clockState: state, ), ), @@ -59,13 +65,15 @@ class _Body extends ConsumerWidget { class ClockTile extends ConsumerWidget { const ClockTile({ + required this.position, required this.playerType, required this.clockState, required this.orientation, super.key, }); - final ClockPlayerType playerType; + final TilePosition position; + final Side playerType; final ClockState clockState; final Orientation orientation; @@ -92,10 +100,10 @@ class ClockTile extends ConsumerWidget { ); return RotatedBox( - quarterTurns: orientation == Orientation.portrait && - playerType == ClockPlayerType.top - ? 2 - : 0, + quarterTurns: + orientation == Orientation.portrait && position == TilePosition.top + ? 2 + : 0, child: Stack( alignment: Alignment.center, fit: StackFit.expand, @@ -107,15 +115,19 @@ class ClockTile extends ConsumerWidget { onTap: !clockState.started ? () { ref - .read(clockControllerProvider.notifier) - .setActiveSide(playerType); + .read(clockToolControllerProvider.notifier) + .setBottomPlayer( + position == TilePosition.bottom + ? Side.white + : Side.black, + ); } : null, onTapDown: clockState.started && clockState.isPlayersMoveAllowed(playerType) ? (_) { ref - .read(clockControllerProvider.notifier) + .read(clockToolControllerProvider.notifier) .onTap(playerType); } : null, @@ -129,21 +141,15 @@ class ClockTile extends ConsumerWidget { FittedBox( child: AnimatedCrossFade( duration: const Duration(milliseconds: 300), - firstChild: CountdownClock( - key: Key('${clockState.id}-$playerType'), - padLeft: true, - clockStyle: clockStyle, - timeLeft: clockState.getDuration(playerType), - active: clockState.isActivePlayer(playerType), - onFlag: () { - ref - .read(clockControllerProvider.notifier) - .setLoser(playerType); - }, - onStop: (remaining) { - ref - .read(clockControllerProvider.notifier) - .updateDuration(playerType, remaining); + firstChild: ValueListenableBuilder( + valueListenable: clockState.getDuration(playerType), + builder: (context, value, _) { + return Clock( + padLeft: true, + clockStyle: clockStyle, + timeLeft: value, + active: clockState.isActivePlayer(playerType), + ); }, ), secondChild: const Icon(Icons.flag), @@ -188,22 +194,22 @@ class ClockTile extends ConsumerWidget { builder: (BuildContext context) => CustomClockSettings( player: playerType, - clock: playerType == ClockPlayerType.top + clock: playerType == Side.white ? TimeIncrement.fromDurations( - clockState.options.timePlayerTop, - clockState.options.incrementPlayerTop, + clockState.options.whiteTime, + clockState.options.whiteIncrement, ) : TimeIncrement.fromDurations( - clockState.options.timePlayerBottom, - clockState.options.incrementPlayerBottom, + clockState.options.blackTime, + clockState.options.blackIncrement, ), onSubmit: ( - ClockPlayerType player, + Side player, TimeIncrement clock, ) { Navigator.of(context).pop(); ref - .read(clockControllerProvider.notifier) + .read(clockToolControllerProvider.notifier) .updateOptionsCustom(clock, player); }, ), diff --git a/lib/src/view/clock/clock_settings.dart b/lib/src/view/clock/clock_settings.dart index a5371b4f91..decea94991 100644 --- a/lib/src/view/clock/clock_settings.dart +++ b/lib/src/view/clock/clock_settings.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lichess_mobile/src/model/clock/clock_controller.dart'; +import 'package:lichess_mobile/src/model/clock/clock_tool_controller.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/utils/l10n_context.dart'; @@ -16,7 +16,7 @@ class ClockSettings extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final state = ref.watch(clockControllerProvider); + final state = ref.watch(clockToolControllerProvider); final buttonsEnabled = !state.started || state.paused; final isSoundEnabled = ref.watch( @@ -36,7 +36,7 @@ class ClockSettings extends ConsumerWidget { iconSize: _iconSize, onPressed: buttonsEnabled ? () { - ref.read(clockControllerProvider.notifier).reset(); + ref.read(clockToolControllerProvider.notifier).reset(); } : null, icon: const Icon(Icons.refresh), @@ -57,18 +57,18 @@ class ClockSettings extends ConsumerWidget { ), builder: (BuildContext context) { final options = ref.watch( - clockControllerProvider + clockToolControllerProvider .select((value) => value.options), ); return TimeControlModal( excludeUltraBullet: true, value: TimeIncrement( - options.timePlayerTop.inSeconds, - options.incrementPlayerTop.inSeconds, + options.whiteTime.inSeconds, + options.whiteIncrement.inSeconds, ), onSelected: (choice) { ref - .read(clockControllerProvider.notifier) + .read(clockToolControllerProvider.notifier) .updateOptions(choice); }, ); @@ -107,8 +107,8 @@ class _PlayResumeButton extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final controller = ref.read(clockControllerProvider.notifier); - final state = ref.watch(clockControllerProvider); + final controller = ref.read(clockToolControllerProvider.notifier); + final state = ref.watch(clockToolControllerProvider); if (!state.started) { return IconButton( diff --git a/lib/src/view/clock/custom_clock_settings.dart b/lib/src/view/clock/custom_clock_settings.dart index ee2bdc6d2d..3ac2172a1a 100644 --- a/lib/src/view/clock/custom_clock_settings.dart +++ b/lib/src/view/clock/custom_clock_settings.dart @@ -1,5 +1,5 @@ +import 'package:dartchess/dartchess.dart'; import 'package:flutter/material.dart'; -import 'package:lichess_mobile/src/model/clock/clock_controller.dart'; import 'package:lichess_mobile/src/model/common/time_increment.dart'; import 'package:lichess_mobile/src/model/lobby/game_setup_preferences.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; @@ -16,9 +16,9 @@ class CustomClockSettings extends StatefulWidget { required this.clock, }); - final ClockPlayerType player; + final Side player; final TimeIncrement clock; - final void Function(ClockPlayerType player, TimeIncrement clock) onSubmit; + final void Function(Side player, TimeIncrement clock) onSubmit; @override State createState() => _CustomClockSettingsState(); diff --git a/lib/src/view/game/game_body.dart b/lib/src/view/game/game_body.dart index 02f9d648e0..4e319426e4 100644 --- a/lib/src/view/game/game_body.dart +++ b/lib/src/view/game/game_body.dart @@ -151,7 +151,7 @@ class GameBody extends ConsumerWidget { ? CountdownClock( key: blackClockKey, delay: gameState.game.clock!.lag, - clockStartTime: gameState.game.clock!.at, + clockUpdatedAt: gameState.game.clock!.at, timeLeft: archivedBlackClock ?? gameState.game.clock!.black, active: gameState.activeClockSide == Side.black, emergencyThreshold: youAre == Side.black @@ -196,7 +196,7 @@ class GameBody extends ConsumerWidget { key: whiteClockKey, timeLeft: archivedWhiteClock ?? gameState.game.clock!.white, delay: gameState.game.clock!.lag, - clockStartTime: gameState.game.clock!.at, + clockUpdatedAt: gameState.game.clock!.at, active: gameState.activeClockSide == Side.white, emergencyThreshold: youAre == Side.white ? gameState.game.meta.clock?.emergency diff --git a/lib/src/view/watch/tv_screen.dart b/lib/src/view/watch/tv_screen.dart index 1cd02b3166..3bfb05846e 100644 --- a/lib/src/view/watch/tv_screen.dart +++ b/lib/src/view/watch/tv_screen.dart @@ -95,7 +95,7 @@ class _Body extends ConsumerWidget { ? CountdownClock( key: blackClockKey, timeLeft: gameState.game.clock!.black, - clockStartTime: gameState.game.clock!.at, + clockUpdatedAt: gameState.game.clock!.at, active: gameState.activeClockSide == Side.black, ) : null, @@ -107,7 +107,7 @@ class _Body extends ConsumerWidget { ? CountdownClock( key: whiteClockKey, timeLeft: gameState.game.clock!.white, - clockStartTime: gameState.game.clock!.at, + clockUpdatedAt: gameState.game.clock!.at, active: gameState.activeClockSide == Side.white, ) : null, diff --git a/lib/src/widgets/countdown_clock.dart b/lib/src/widgets/countdown_clock.dart index ec76e7c8e2..d9fdcd91fa 100644 --- a/lib/src/widgets/countdown_clock.dart +++ b/lib/src/widgets/countdown_clock.dart @@ -13,12 +13,11 @@ class CountdownClock extends StatefulWidget { const CountdownClock({ required this.timeLeft, this.delay, - this.clockStartTime, + this.clockUpdatedAt, required this.active, this.emergencyThreshold, this.onEmergency, this.onFlag, - this.onStop, this.clockStyle, this.padLeft = false, super.key, @@ -32,11 +31,11 @@ class CountdownClock extends StatefulWidget { /// This can be used to implement lag compensation. final Duration? delay; - /// The time at which the clock should have started. + /// The time at which the clock was updated. /// /// Use this parameter to synchronize the clock with the time at which the clock /// event was received from the server and to compensate for UI lag. - final DateTime? clockStartTime; + final DateTime? clockUpdatedAt; /// The duration at which the clock should change its background color to indicate an emergency. /// @@ -52,9 +51,6 @@ class CountdownClock extends StatefulWidget { /// Callback when the clock reaches zero. final VoidCallback? onFlag; - /// Callback with the remaining duration when the clock stops - final ValueSetter? onStop; - /// Custom color style final ClockStyle? clockStyle; @@ -79,10 +75,10 @@ class _CountdownClockState extends State { void startClock() { final now = clock.now(); final delay = widget.delay ?? Duration.zero; - final clockStartTime = widget.clockStartTime ?? now; + final clockUpdatedAt = widget.clockUpdatedAt ?? now; // UI lag diff: the elapsed time between the time the clock should have started // and the time the clock is actually started - final uiLag = now.difference(clockStartTime); + final uiLag = now.difference(clockUpdatedAt); final realDelay = delay - uiLag; // real delay is negative, we need to adjust the timeLeft. @@ -120,20 +116,9 @@ class _CountdownClockState extends State { } } - void stopClock({bool countElapsedTime = true}) { - if (countElapsedTime) { - setState(() { - timeLeft = timeLeft - _stopwatch.elapsed; - if (timeLeft < Duration.zero) { - timeLeft = Duration.zero; - } - }); - } + void stopClock() { _timer?.cancel(); _stopwatch.stop(); - scheduleMicrotask(() { - widget.onStop?.call(timeLeft); - }); } void _playEmergencyFeedback() { @@ -163,9 +148,8 @@ class _CountdownClockState extends State { @override void didUpdateWidget(CountdownClock oldClock) { super.didUpdateWidget(oldClock); - final isSameTimeConfig = widget.timeLeft == oldClock.timeLeft; - if (!isSameTimeConfig) { + if (widget.clockUpdatedAt != oldClock.clockUpdatedAt) { timeLeft = widget.timeLeft; } @@ -173,9 +157,7 @@ class _CountdownClockState extends State { if (widget.active) { startClock(); } else { - // If the timeLeft was changed at the same time as the clock is stopped - // we don't want to count the elapsed time because the new time takes precedence. - stopClock(countElapsedTime: isSameTimeConfig); + stopClock(); } } } diff --git a/test/model/clock/chess_clock_test.dart b/test/model/clock/chess_clock_test.dart new file mode 100644 index 0000000000..179d5137cd --- /dev/null +++ b/test/model/clock/chess_clock_test.dart @@ -0,0 +1,186 @@ +import 'package:dartchess/dartchess.dart'; +import 'package:fake_async/fake_async.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:lichess_mobile/src/model/clock/chess_clock.dart'; + +void main() { + test('make clock', () { + final clock = ChessClock( + whiteTime: const Duration(seconds: 5), + blackTime: const Duration(seconds: 5), + ); + expect(clock.isRunning, false); + expect(clock.whiteTime.value, const Duration(seconds: 5)); + expect(clock.blackTime.value, const Duration(seconds: 5)); + }); + + test('start clock', () { + fakeAsync((async) { + final clock = ChessClock( + whiteTime: const Duration(seconds: 5), + blackTime: const Duration(seconds: 5), + ); + clock.start(); + expect(clock.isRunning, true); + }); + }); + + test('clock ticking', () { + fakeAsync((async) { + final clock = ChessClock( + whiteTime: const Duration(seconds: 5), + blackTime: const Duration(seconds: 5), + ); + clock.start(); + expect(clock.whiteTime.value, const Duration(seconds: 5)); + expect(clock.blackTime.value, const Duration(seconds: 5)); + async.elapse(const Duration(seconds: 1)); + expect(clock.whiteTime.value, const Duration(seconds: 4)); + expect(clock.blackTime.value, const Duration(seconds: 5)); + async.elapse(const Duration(seconds: 1)); + expect(clock.whiteTime.value, const Duration(seconds: 3)); + expect(clock.blackTime.value, const Duration(seconds: 5)); + }); + }); + + test('stop clock', () { + fakeAsync((async) { + final clock = ChessClock( + whiteTime: const Duration(seconds: 5), + blackTime: const Duration(seconds: 5), + ); + clock.start(); + expect(clock.isRunning, true); + async.elapse(const Duration(seconds: 1)); + expect(clock.whiteTime.value, const Duration(seconds: 4)); + expect(clock.blackTime.value, const Duration(seconds: 5)); + final thinkTime = clock.stop(); + expect(clock.isRunning, false); + expect(thinkTime, const Duration(seconds: 1)); + async.elapse(const Duration(seconds: 1)); + expect(clock.whiteTime.value, const Duration(seconds: 4)); + expect(clock.blackTime.value, const Duration(seconds: 5)); + }); + }); + + test('start given side', () { + fakeAsync((async) { + final clock = ChessClock( + whiteTime: const Duration(seconds: 5), + blackTime: const Duration(seconds: 5), + ); + clock.start(); + expect(clock.isRunning, true); + expect(clock.whiteTime.value, const Duration(seconds: 5)); + expect(clock.blackTime.value, const Duration(seconds: 5)); + async.elapse(const Duration(seconds: 1)); + expect(clock.whiteTime.value, const Duration(seconds: 4)); + expect(clock.blackTime.value, const Duration(seconds: 5)); + clock.startForSide(Side.black); + async.elapse(const Duration(seconds: 1)); + expect(clock.whiteTime.value, const Duration(seconds: 4)); + expect(clock.blackTime.value, const Duration(seconds: 4)); + }); + }); + + test('start with delay', () { + fakeAsync((async) { + final clock = ChessClock( + whiteTime: const Duration(seconds: 5), + blackTime: const Duration(seconds: 5), + ); + clock.start(const Duration(milliseconds: 20)); + expect(clock.isRunning, true); + expect(clock.whiteTime.value, const Duration(seconds: 5)); + async.elapse(const Duration(milliseconds: 10)); + expect(clock.whiteTime.value, const Duration(seconds: 5)); + // the start delay is reached, but clock not updated yet since tick delay is 100ms + async.elapse(const Duration(milliseconds: 100)); + expect(clock.whiteTime.value, const Duration(seconds: 5)); + async.elapse(const Duration(milliseconds: 10)); + expect(clock.whiteTime.value, const Duration(milliseconds: 4900)); + final thinkTime = clock.stop(); + expect(thinkTime, const Duration(milliseconds: 100)); + }); + }); + + test('increment times', () { + fakeAsync((async) { + final clock = ChessClock( + whiteTime: const Duration(seconds: 5), + blackTime: const Duration(seconds: 5), + ); + clock.start(); + expect(clock.whiteTime.value, const Duration(seconds: 5)); + expect(clock.blackTime.value, const Duration(seconds: 5)); + clock.incTimes(whiteInc: const Duration(seconds: 1)); + expect(clock.whiteTime.value, const Duration(seconds: 6)); + expect(clock.blackTime.value, const Duration(seconds: 5)); + clock.incTimes(blackInc: const Duration(seconds: 1)); + expect(clock.whiteTime.value, const Duration(seconds: 6)); + expect(clock.blackTime.value, const Duration(seconds: 6)); + }); + }); + + test('increment specific side', () { + fakeAsync((async) { + final clock = ChessClock( + whiteTime: const Duration(seconds: 5), + blackTime: const Duration(seconds: 5), + ); + clock.start(); + expect(clock.whiteTime.value, const Duration(seconds: 5)); + expect(clock.blackTime.value, const Duration(seconds: 5)); + clock.incTime(Side.white, const Duration(seconds: 1)); + expect(clock.whiteTime.value, const Duration(seconds: 6)); + expect(clock.blackTime.value, const Duration(seconds: 5)); + clock.incTime(Side.black, const Duration(seconds: 1)); + expect(clock.whiteTime.value, const Duration(seconds: 6)); + expect(clock.blackTime.value, const Duration(seconds: 6)); + }); + }); + + test('flag', () { + fakeAsync((async) { + int flagCount = 0; + final clock = ChessClock( + whiteTime: const Duration(seconds: 5), + blackTime: const Duration(seconds: 5), + onFlag: () { + flagCount++; + }, + ); + clock.start(); + expect(clock.whiteTime.value, const Duration(seconds: 5)); + expect(clock.blackTime.value, const Duration(seconds: 5)); + async.elapse(const Duration(seconds: 5)); + expect(flagCount, 1); + expect(clock.whiteTime.value, Duration.zero); + expect(clock.blackTime.value, const Duration(seconds: 5)); + }); + }); + + test('onEmergency', () { + fakeAsync((async) { + int onEmergencyCount = 0; + final clock = ChessClock( + whiteTime: const Duration(seconds: 5), + blackTime: const Duration(seconds: 5), + emergencyThreshold: const Duration(seconds: 2), + onEmergency: () { + onEmergencyCount++; + }, + ); + clock.start(); + expect(clock.whiteTime.value, const Duration(seconds: 5)); + expect(clock.blackTime.value, const Duration(seconds: 5)); + async.elapse(const Duration(seconds: 2)); + expect(clock.whiteTime.value, const Duration(seconds: 3)); + expect(clock.blackTime.value, const Duration(seconds: 5)); + async.elapse(const Duration(seconds: 1)); + expect(onEmergencyCount, 1); + async.elapse(const Duration(milliseconds: 100)); + expect(onEmergencyCount, 1); + }); + }); +} diff --git a/test/widgets/countdown_clock_test.dart b/test/widgets/countdown_clock_test.dart index 4c819887c9..418050d635 100644 --- a/test/widgets/countdown_clock_test.dart +++ b/test/widgets/countdown_clock_test.dart @@ -42,9 +42,10 @@ void main() { testWidgets('update time by changing widget configuration', (WidgetTester tester) async { await tester.pumpWidget( - const MaterialApp( + MaterialApp( home: CountdownClock( - timeLeft: Duration(seconds: 10), + timeLeft: const Duration(seconds: 10), + clockUpdatedAt: clock.now(), active: true, ), ), @@ -59,9 +60,10 @@ void main() { expect(find.text('0:09.7', findRichText: true), findsOneWidget); await tester.pumpWidget( - const MaterialApp( + MaterialApp( home: CountdownClock( - timeLeft: Duration(seconds: 11), + timeLeft: const Duration(seconds: 11), + clockUpdatedAt: clock.now(), active: true, ), ), @@ -73,7 +75,7 @@ void main() { expect(find.text('0:00.0', findRichText: true), findsOneWidget); }); - testWidgets('do not update if timeLeft widget configuration is same', + testWidgets('do not update if clockUpdatedAt is same', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( @@ -95,7 +97,7 @@ void main() { await tester.pumpWidget( const MaterialApp( home: CountdownClock( - timeLeft: Duration(seconds: 10), + timeLeft: Duration(seconds: 11), active: true, ), ), @@ -109,9 +111,10 @@ void main() { testWidgets('shows milliseconds when time < 1s and active is false', (WidgetTester tester) async { await tester.pumpWidget( - const MaterialApp( + MaterialApp( home: CountdownClock( - timeLeft: Duration(seconds: 1), + timeLeft: const Duration(seconds: 1), + clockUpdatedAt: clock.now(), active: true, ), ), @@ -124,9 +127,10 @@ void main() { expect(find.text('0:00.9', findRichText: true), findsOneWidget); await tester.pumpWidget( - const MaterialApp( + MaterialApp( home: CountdownClock( - timeLeft: Duration(milliseconds: 988), + timeLeft: const Duration(milliseconds: 988), + clockUpdatedAt: clock.now(), active: false, ), ), @@ -195,7 +199,7 @@ void main() { timeLeft: const Duration(seconds: 10), active: true, delay: const Duration(milliseconds: 20), - clockStartTime: now, + clockUpdatedAt: now, ), ), ); @@ -219,7 +223,7 @@ void main() { timeLeft: const Duration(seconds: 10), active: true, delay: const Duration(milliseconds: 10), - clockStartTime: now, + clockUpdatedAt: now, ), ), ); @@ -246,41 +250,6 @@ void main() { expect(find.text('0:00.0', findRichText: true), findsOneWidget); }); - testWidgets('calls onStop', (WidgetTester tester) async { - int onStopCount = 0; - Duration? lastOnStopTime; - await tester.pumpWidget( - MaterialApp( - home: CountdownClock( - timeLeft: const Duration(seconds: 10), - active: true, - onStop: (Duration timeLeft) { - onStopCount++; - lastOnStopTime = timeLeft; - }, - ), - ), - ); - expect(find.text('0:10', findRichText: true), findsOneWidget); - await tester.pump(const Duration(seconds: 5)); - await tester.pumpWidget( - MaterialApp( - home: CountdownClock( - timeLeft: const Duration(seconds: 10), - active: false, - onStop: (Duration timeLeft) { - onStopCount++; - lastOnStopTime = timeLeft; - }, - ), - ), - ); - - expect(onStopCount, 1); - expect(lastOnStopTime, const Duration(seconds: 5)); - expect(find.text('0:05.0', findRichText: true), findsOneWidget); - }); - testWidgets('emergency feedback', (WidgetTester tester) async { int onEmergencyCount = 0; await tester.pumpWidget( From 0ae66b7a7320a8d56d03ba2bd5fe6acf38efa40f Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 7 Nov 2024 14:08:47 +0100 Subject: [PATCH 659/979] Update doc --- lib/src/model/clock/chess_clock.dart | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/src/model/clock/chess_clock.dart b/lib/src/model/clock/chess_clock.dart index bb1b82cbf4..4191df0f2d 100644 --- a/lib/src/model/clock/chess_clock.dart +++ b/lib/src/model/clock/chess_clock.dart @@ -8,8 +8,6 @@ const _emergencyDelay = Duration(seconds: 20); const _tickDelay = Duration(milliseconds: 100); /// A chess clock. -/// -/// The clock will call the [onFlag] callback when a side's time reaches zero. class ChessClock { ChessClock({ required Duration whiteTime, @@ -21,9 +19,7 @@ class ChessClock { _blackTime = ValueNotifier(blackTime), _activeSide = Side.white; - /// The duration at which the clock should change its background color to indicate an emergency. - /// - /// If [onEmergency] is provided, the clock will call it when the emergency threshold is reached. + /// The threshold at which the clock will call [onEmergency] if provided. final Duration? emergencyThreshold; /// Callback when the clock reaches zero. From 5cdfe24024b93911c4a4f361eb6705bf5a9fb801 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 8 Nov 2024 09:01:08 +0100 Subject: [PATCH 660/979] Fix chess clock thinking time --- lib/src/model/clock/chess_clock.dart | 12 +++++------- .../model/clock/clock_tool_controller.dart | 2 +- test/model/clock/chess_clock_test.dart | 19 +++++++++++++++++-- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/lib/src/model/clock/chess_clock.dart b/lib/src/model/clock/chess_clock.dart index 4191df0f2d..49b2b1e695 100644 --- a/lib/src/model/clock/chess_clock.dart +++ b/lib/src/model/clock/chess_clock.dart @@ -93,21 +93,19 @@ class ChessClock { } } - /// Starts the clock for the given side. + /// Starts the clock and switch to the given side. /// /// The [delay] parameter can be used to add a delay before the clock starts counting down. This is useful for lag compensation. /// /// Returns the think time of the active side before switching or `null` if the clock is not running. - Duration? startForSide(Side side, [Duration delay = Duration.zero]) { + Duration? startSide(Side side, [Duration delay = Duration.zero]) { _activeSide = side; + final thinkTime = _thinkTime; start(delay); - if (isRunning) { - return _thinkTime; - } - return null; + return thinkTime; } - /// Starts the clock for the active side. + /// Starts the clock. /// /// The [delay] parameter can be used to add a delay before the clock starts counting down. This is useful for lag compensation. void start([Duration delay = Duration.zero]) { diff --git a/lib/src/model/clock/clock_tool_controller.dart b/lib/src/model/clock/clock_tool_controller.dart index aa506d5966..1ceb2c9b86 100644 --- a/lib/src/model/clock/clock_tool_controller.dart +++ b/lib/src/model/clock/clock_tool_controller.dart @@ -56,7 +56,7 @@ class ClockToolController extends _$ClockToolController { ); } ref.read(soundServiceProvider).play(Sound.clock); - _clock.startForSide(playerType.opposite); + _clock.startSide(playerType.opposite); _clock.incTime( playerType, playerType == Side.white diff --git a/test/model/clock/chess_clock_test.dart b/test/model/clock/chess_clock_test.dart index 179d5137cd..07a564ab94 100644 --- a/test/model/clock/chess_clock_test.dart +++ b/test/model/clock/chess_clock_test.dart @@ -63,7 +63,21 @@ void main() { }); }); - test('start given side', () { + test('start side', () { + fakeAsync((async) { + final clock = ChessClock( + whiteTime: const Duration(seconds: 5), + blackTime: const Duration(seconds: 5), + ); + final thinkTime = clock.startSide(Side.black); + expect(thinkTime, null); + async.elapse(const Duration(seconds: 1)); + expect(clock.whiteTime.value, const Duration(seconds: 5)); + expect(clock.blackTime.value, const Duration(seconds: 4)); + }); + }); + + test('start side (running clock)', () { fakeAsync((async) { final clock = ChessClock( whiteTime: const Duration(seconds: 5), @@ -76,7 +90,8 @@ void main() { async.elapse(const Duration(seconds: 1)); expect(clock.whiteTime.value, const Duration(seconds: 4)); expect(clock.blackTime.value, const Duration(seconds: 5)); - clock.startForSide(Side.black); + final thinkTime = clock.startSide(Side.black); + expect(thinkTime, const Duration(seconds: 1)); async.elapse(const Duration(seconds: 1)); expect(clock.whiteTime.value, const Duration(seconds: 4)); expect(clock.blackTime.value, const Duration(seconds: 4)); From 00c7fa41bc4187cd12cf7b9597f50a7e5161cc43 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 8 Nov 2024 20:10:03 +0100 Subject: [PATCH 661/979] WIP on using ChessClock in GameController --- lib/src/model/clock/chess_clock.dart | 21 +- lib/src/model/game/game_controller.dart | 125 +++++--- lib/src/model/game/game_socket_events.dart | 3 +- lib/src/network/socket.dart | 3 +- lib/src/view/clock/clock_screen.dart | 1 - lib/src/view/game/game_body.dart | 96 ++++--- .../challenge/challenge_service_test.dart | 6 +- test/model/clock/chess_clock_test.dart | 25 +- test/model/game/game_socket_example_data.dart | 101 +++++++ test/network/fake_websocket_channel.dart | 38 ++- test/network/socket_test.dart | 13 +- test/test_container.dart | 2 +- test/test_helpers.dart | 8 +- test/test_provider_scope.dart | 2 +- test/view/game/game_screen_test.dart | 272 ++++++++++++++++++ .../over_the_board_screen_test.dart | 38 +-- test/view/puzzle/puzzle_screen_test.dart | 14 +- test/view/puzzle/storm_screen_test.dart | 11 +- 18 files changed, 627 insertions(+), 152 deletions(-) create mode 100644 test/model/game/game_socket_example_data.dart create mode 100644 test/view/game/game_screen_test.dart diff --git a/lib/src/model/clock/chess_clock.dart b/lib/src/model/clock/chess_clock.dart index 49b2b1e695..f902f7e2ab 100644 --- a/lib/src/model/clock/chess_clock.dart +++ b/lib/src/model/clock/chess_clock.dart @@ -25,8 +25,8 @@ class ChessClock { /// Callback when the clock reaches zero. VoidCallback? onFlag; - /// Called when the clock reaches the emergency. - final VoidCallback? onEmergency; + /// Called when one clock timers reaches the emergency threshold. + final void Function(Side activeSide)? onEmergency; Timer? _timer; Timer? _startDelayTimer; @@ -95,23 +95,28 @@ class ChessClock { /// Starts the clock and switch to the given side. /// + /// Trying to start an already running clock on the same side is a no-op. + /// /// The [delay] parameter can be used to add a delay before the clock starts counting down. This is useful for lag compensation. /// /// Returns the think time of the active side before switching or `null` if the clock is not running. - Duration? startSide(Side side, [Duration delay = Duration.zero]) { + Duration? startSide(Side side, {Duration? delay}) { + if (isRunning && _activeSide == side) { + return _thinkTime; + } _activeSide = side; final thinkTime = _thinkTime; - start(delay); + start(delay: delay); return thinkTime; } /// Starts the clock. /// /// The [delay] parameter can be used to add a delay before the clock starts counting down. This is useful for lag compensation. - void start([Duration delay = Duration.zero]) { - _lastStarted = clock.now().add(delay); + void start({Duration? delay}) { + _lastStarted = clock.now().add(delay ?? Duration.zero); _startDelayTimer?.cancel(); - _startDelayTimer = Timer(delay, _scheduleTick); + _startDelayTimer = Timer(delay ?? Duration.zero, _scheduleTick); } /// Pauses the clock. @@ -172,7 +177,7 @@ class ChessClock { (_nextEmergency == null || _nextEmergency!.isBefore(clock.now()))) { _shouldPlayEmergencyFeedback = false; _nextEmergency = clock.now().add(_emergencyDelay); - onEmergency?.call(); + onEmergency?.call(_activeSide); } else if (emergencyThreshold != null && timeLeft > emergencyThreshold! * 1.5) { _shouldPlayEmergencyFeedback = true; diff --git a/lib/src/model/game/game_controller.dart b/lib/src/model/game/game_controller.dart index 468a995f91..a8150735d8 100644 --- a/lib/src/model/game/game_controller.dart +++ b/lib/src/model/game/game_controller.dart @@ -5,6 +5,7 @@ import 'package:collection/collection.dart'; import 'package:dartchess/dartchess.dart'; import 'package:deep_pick/deep_pick.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -12,6 +13,7 @@ import 'package:lichess_mobile/src/model/account/account_preferences.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/analysis/server_analysis_service.dart'; +import 'package:lichess_mobile/src/model/clock/chess_clock.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/service/move_feedback.dart'; @@ -62,8 +64,7 @@ class GameController extends _$GameController { /// Last socket version received int? _socketEventVersion; - /// Last move time - DateTime? _lastClockUpdateAt; + ChessClock? _clock; late SocketClient _socketClient; @@ -85,6 +86,7 @@ class GameController extends _$GameController { _opponentLeftCountdownTimer?.cancel(); _transientMoveTimer?.cancel(); _appLifecycleListener?.dispose(); + _clock?.dispose(); }); return _socketClient.stream.firstWhere((e) => e.topic == 'full').then( @@ -123,13 +125,29 @@ class GameController extends _$GameController { } }, ); + + if (game.clock != null) { + _clock = ChessClock( + whiteTime: game.clock!.white, + blackTime: game.clock!.black, + emergencyThreshold: game.meta.clock?.emergency, + onEmergency: onClockEmergency, + onFlag: onFlag, + ); + if (game.clock!.running) { + final pos = game.lastPosition; + if (pos.fullmoves > 1) { + _clock!.startSide(pos.turn); + } + } + } } return GameState( gameFullId: gameFullId, game: game, stepCursor: game.steps.length - 1, - stopClockWaitingForServerAck: false, + liveClock: _liveClock, ); }, ); @@ -161,7 +179,6 @@ class GameController extends _$GameController { steps: curState.game.steps.add(newStep), ), stepCursor: curState.stepCursor + 1, - stopClockWaitingForServerAck: !shouldConfirmMove, moveToConfirm: shouldConfirmMove ? move : null, promotionMove: null, premove: null, @@ -174,7 +191,6 @@ class GameController extends _$GameController { _sendMoveToSocket( move, isPremove: isPremove ?? false, - hasClock: curState.game.clock != null, // same logic as web client // we want to send client lag only at the beginning of the game when the clock is not running yet withLag: @@ -229,14 +245,12 @@ class GameController extends _$GameController { state = AsyncValue.data( curState.copyWith( - stopClockWaitingForServerAck: true, moveToConfirm: null, ), ); _sendMoveToSocket( moveToConfirm, isPremove: false, - hasClock: curState.game.clock != null, // same logic as web client // we want to send client lag only at the beginning of the game when the clock is not running yet withLag: curState.game.clock != null && curState.activeClockSide == null, @@ -335,8 +349,12 @@ class GameController extends _$GameController { } /// Play a sound when the clock is about to run out - void onClockEmergency() { - ref.read(soundServiceProvider).play(Sound.lowTime); + Future onClockEmergency(Side activeSide) async { + if (activeSide != state.valueOrNull?.game.youAre) return; + final shouldPlay = await ref.read(clockSoundProvider.future); + if (shouldPlay) { + ref.read(soundServiceProvider).play(Sound.lowTime); + } } void onFlag() { @@ -415,18 +433,37 @@ class GameController extends _$GameController { Future.value(); } + /// Gets the live game clock if available. + LiveGameClock? get _liveClock => _clock != null + ? ( + white: _clock!.whiteTime, + black: _clock!.blackTime, + ) + : null; + + /// Update the internal clock on clock server event + void _updateClock({ + required Duration white, + required Duration black, + required Side? activeSide, + Duration? lag, + }) { + _clock?.setTimes(whiteTime: white, blackTime: black); + if (activeSide != null) { + _clock?.startSide(activeSide, delay: lag); + } + } + void _sendMoveToSocket( Move move, { required bool isPremove, - required bool hasClock, required bool withLag, }) { - final moveTime = hasClock + final thinkTime = _clock?.stop(); + final moveTime = _clock != null ? isPremove == true ? Duration.zero - : _lastClockUpdateAt != null - ? DateTime.now().difference(_lastClockUpdateAt!) - : null + : thinkTime : null; _socketClient.send( 'move', @@ -436,7 +473,7 @@ class GameController extends _$GameController { 's': (moveTime.inMilliseconds * 0.1).round().toRadixString(36), }, ackable: true, - withLag: hasClock && (moveTime == null || withLag), + withLag: _clock != null && (moveTime == null || withLag), ); _transientMoveTimer = Timer(const Duration(seconds: 10), _resyncGameData); @@ -547,20 +584,27 @@ class GameController extends _$GameController { return; } _socketEventVersion = fullEvent.socketEventVersion; - _lastClockUpdateAt = null; state = AsyncValue.data( GameState( gameFullId: gameFullId, game: fullEvent.game, stepCursor: fullEvent.game.steps.length - 1, - stopClockWaitingForServerAck: false, + liveClock: _liveClock, // cancel the premove to avoid playing wrong premove when the full // game data is reloaded premove: null, ), ); + if (fullEvent.game.clock != null) { + _updateClock( + white: fullEvent.game.clock!.white, + black: fullEvent.game.clock!.black, + activeSide: state.requireValue.activeClockSide, + ); + } + // Move event, received after sending a move or receiving a move from the // opponent case 'move': @@ -608,19 +652,26 @@ class GameController extends _$GameController { } if (data.clock != null) { - _lastClockUpdateAt = data.clock!.at; - - final lagCompensation = newState.game.playable - // server will send the lag only if it's more than 10ms - ? data.clock?.lag ?? const Duration(milliseconds: 10) - : Duration.zero; + final lagCompensation = + newState.game.playable && newState.game.isPlayerTurn + // server will send the lag only if it's more than 10ms + ? Duration.zero + : data.clock?.lag ?? const Duration(milliseconds: 10); + + _updateClock( + white: data.clock!.white, + black: data.clock!.black, + lag: lagCompensation, + activeSide: newState.activeClockSide, + ); if (newState.game.clock != null) { + // TODO remove that newState = newState.copyWith.game.clock!( white: data.clock!.white, black: data.clock!.black, - lag: lagCompensation, - at: data.clock!.at, + // lag: lagCompensation, + // at: data.clock!.at, ); } else if (newState.game.correspondenceClock != null) { newState = newState.copyWith.game.correspondenceClock!( @@ -628,10 +679,6 @@ class GameController extends _$GameController { black: data.clock!.black, ); } - - newState = newState.copyWith( - stopClockWaitingForServerAck: false, - ); } if (newState.game.expiration != null) { @@ -697,6 +744,11 @@ class GameController extends _$GameController { white: endData.clock!.white, black: endData.clock!.black, ); + _updateClock( + white: endData.clock!.white, + black: endData.clock!.black, + activeSide: newState.activeClockSide, + ); } if (curState.game.lastPosition.fullmoves > 1) { @@ -734,7 +786,11 @@ class GameController extends _$GameController { final newClock = pick(data['total']) .letOrNull((it) => Duration(milliseconds: it.asIntOrThrow() * 10)); final curState = state.requireValue; + if (side != null && newClock != null) { + _clock?.setTime(side, newClock); + + // TODO: remove final newState = side == Side.white ? curState.copyWith.game.clock!( white: newClock, @@ -989,6 +1045,11 @@ class GameController extends _$GameController { } } +typedef LiveGameClock = ({ + ValueListenable white, + ValueListenable black, +}); + @freezed class GameState with _$GameState { const GameState._(); @@ -997,10 +1058,9 @@ class GameState with _$GameState { required GameFullId gameFullId, required PlayableGame game, required int stepCursor, + required LiveGameClock? liveClock, int? lastDrawOfferAtPly, Duration? opponentLeftCountdown, - required bool stopClockWaitingForServerAck, - DateTime? clockSwitchedAt, /// Promotion waiting to be selected (only if auto queen is disabled) NormalMove? promotionMove, @@ -1102,9 +1162,6 @@ class GameState with _$GameState { if (game.clock == null && game.correspondenceClock == null) { return null; } - if (stopClockWaitingForServerAck) { - return null; - } if (game.status == GameStatus.started) { final pos = game.lastPosition; if (pos.fullmoves > 1) { diff --git a/lib/src/model/game/game_socket_events.dart b/lib/src/model/game/game_socket_events.dart index 890563f925..cfd8dd10e8 100644 --- a/lib/src/model/game/game_socket_events.dart +++ b/lib/src/model/game/game_socket_events.dart @@ -1,3 +1,4 @@ +import 'package:clock/clock.dart'; import 'package:dartchess/dartchess.dart'; import 'package:deep_pick/deep_pick.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; @@ -83,7 +84,7 @@ MoveEvent _socketMoveEventFromPick(RequiredPick pick) { blackOfferingDraw: pick('bDraw').asBoolOrNull(), clock: pick('clock').letOrNull( (it) => ( - at: DateTime.now(), + at: clock.now(), white: it('white').asDurationFromSecondsOrThrow(), black: it('black').asDurationFromSecondsOrThrow(), lag: it('lag') diff --git a/lib/src/network/socket.dart b/lib/src/network/socket.dart index 1997cf23e2..fdb9d7eed0 100644 --- a/lib/src/network/socket.dart +++ b/lib/src/network/socket.dart @@ -563,9 +563,10 @@ class SocketPool { return client; } - /// Disposes the pool and all its clients. + /// Disposes the pool and all its clients and resources. void dispose() { _averageLag.dispose(); + _disposeTimers.forEach((_, t) => t?.cancel()); _pool.forEach((_, c) => c._dispose()); } } diff --git a/lib/src/view/clock/clock_screen.dart b/lib/src/view/clock/clock_screen.dart index dec68243ec..103e51a7b6 100644 --- a/lib/src/view/clock/clock_screen.dart +++ b/lib/src/view/clock/clock_screen.dart @@ -3,7 +3,6 @@ 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/common/time_increment.dart'; -import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/immersive_mode.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; diff --git a/lib/src/view/game/game_body.dart b/lib/src/view/game/game_body.dart index 4e319426e4..557668ba52 100644 --- a/lib/src/view/game/game_body.dart +++ b/lib/src/view/game/game_body.dart @@ -6,7 +6,6 @@ import 'package:dartchess/dartchess.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lichess_mobile/src/model/account/account_preferences.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/speed.dart'; @@ -104,11 +103,6 @@ class GameBody extends ConsumerWidget { ); final boardPreferences = ref.watch(boardPreferencesProvider); - final emergencySoundEnabled = ref.watch(clockSoundProvider).maybeWhen( - data: (clockSound) => clockSound, - orElse: () => true, - ); - final blindfoldMode = ref.watch( gamePreferencesProvider.select( (prefs) => prefs.blindfoldMode, @@ -147,28 +141,33 @@ class GameBody extends ConsumerWidget { }, ) : null, - clock: gameState.game.meta.clock != null - ? CountdownClock( - key: blackClockKey, - delay: gameState.game.clock!.lag, - clockUpdatedAt: gameState.game.clock!.at, - timeLeft: archivedBlackClock ?? gameState.game.clock!.black, - active: gameState.activeClockSide == Side.black, - emergencyThreshold: youAre == Side.black - ? gameState.game.meta.clock?.emergency - : null, - onEmergency: emergencySoundEnabled - ? () => ref.read(ctrlProvider.notifier).onClockEmergency() - : null, - onFlag: () => ref.read(ctrlProvider.notifier).onFlag(), + clock: archivedBlackClock != null + ? Clock( + timeLeft: archivedBlackClock, + active: false, ) - : gameState.game.correspondenceClock != null - ? CorrespondenceClock( - duration: gameState.game.correspondenceClock!.black, - active: gameState.activeClockSide == Side.black, - onFlag: () => ref.read(ctrlProvider.notifier).onFlag(), + : gameState.liveClock != null + ? ValueListenableBuilder( + key: blackClockKey, + valueListenable: gameState.liveClock!.black, + builder: (context, value, _) { + return Clock( + timeLeft: value, + active: gameState.activeClockSide == Side.black, + emergencyThreshold: youAre == Side.black + ? gameState.game.meta.clock?.emergency + : null, + ); + }, ) - : null, + : gameState.game.correspondenceClock != null + ? CorrespondenceClock( + duration: gameState.game.correspondenceClock!.black, + active: gameState.activeClockSide == Side.black, + onFlag: () => + ref.read(ctrlProvider.notifier).onFlag(), + ) + : null, ); final white = GamePlayer( player: gameState.game.white, @@ -191,28 +190,33 @@ class GameBody extends ConsumerWidget { }, ) : null, - clock: gameState.game.meta.clock != null - ? CountdownClock( - key: whiteClockKey, - timeLeft: archivedWhiteClock ?? gameState.game.clock!.white, - delay: gameState.game.clock!.lag, - clockUpdatedAt: gameState.game.clock!.at, - active: gameState.activeClockSide == Side.white, - emergencyThreshold: youAre == Side.white - ? gameState.game.meta.clock?.emergency - : null, - onEmergency: emergencySoundEnabled - ? () => ref.read(ctrlProvider.notifier).onClockEmergency() - : null, - onFlag: () => ref.read(ctrlProvider.notifier).onFlag(), + clock: archivedWhiteClock != null + ? Clock( + timeLeft: archivedWhiteClock, + active: false, ) - : gameState.game.correspondenceClock != null - ? CorrespondenceClock( - duration: gameState.game.correspondenceClock!.white, - active: gameState.activeClockSide == Side.white, - onFlag: () => ref.read(ctrlProvider.notifier).onFlag(), + : gameState.liveClock != null + ? ValueListenableBuilder( + key: whiteClockKey, + valueListenable: gameState.liveClock!.white, + builder: (context, value, _) { + return Clock( + timeLeft: value, + active: gameState.activeClockSide == Side.white, + emergencyThreshold: youAre == Side.white + ? gameState.game.meta.clock?.emergency + : null, + ); + }, ) - : null, + : gameState.game.correspondenceClock != null + ? CorrespondenceClock( + duration: gameState.game.correspondenceClock!.white, + active: gameState.activeClockSide == Side.white, + onFlag: () => + ref.read(ctrlProvider.notifier).onFlag(), + ) + : null, ); final isBoardTurned = ref.watch(isBoardTurnedProvider); diff --git a/test/model/challenge/challenge_service_test.dart b/test/model/challenge/challenge_service_test.dart index 10872457a1..25c637853a 100644 --- a/test/model/challenge/challenge_service_test.dart +++ b/test/model/challenge/challenge_service_test.dart @@ -32,7 +32,7 @@ void main() { test('exposes a challenges stream', () async { final fakeChannel = FakeWebSocketChannel(); final socketClient = - makeTestSocketClient(FakeWebSocketChannelFactory(() => fakeChannel)); + makeTestSocketClient(FakeWebSocketChannelFactory((_) => fakeChannel)); await socketClient.connect(); await socketClient.firstConnection; @@ -118,7 +118,7 @@ void main() { fakeAsync((async) { final fakeChannel = FakeWebSocketChannel(); final socketClient = - makeTestSocketClient(FakeWebSocketChannelFactory(() => fakeChannel)); + makeTestSocketClient(FakeWebSocketChannelFactory((_) => fakeChannel)); socketClient.connect(); notificationService.start(); challengeService.start(); @@ -221,7 +221,7 @@ void main() { fakeAsync((async) { final fakeChannel = FakeWebSocketChannel(); final socketClient = - makeTestSocketClient(FakeWebSocketChannelFactory(() => fakeChannel)); + makeTestSocketClient(FakeWebSocketChannelFactory((_) => fakeChannel)); socketClient.connect(); notificationService.start(); challengeService.start(); diff --git a/test/model/clock/chess_clock_test.dart b/test/model/clock/chess_clock_test.dart index 07a564ab94..2650f35b95 100644 --- a/test/model/clock/chess_clock_test.dart +++ b/test/model/clock/chess_clock_test.dart @@ -98,13 +98,34 @@ void main() { }); }); + test('start side (running clock, same side)', () { + fakeAsync((async) { + final clock = ChessClock( + whiteTime: const Duration(seconds: 5), + blackTime: const Duration(seconds: 5), + ); + clock.start(); + expect(clock.isRunning, true); + expect(clock.whiteTime.value, const Duration(seconds: 5)); + expect(clock.blackTime.value, const Duration(seconds: 5)); + async.elapse(const Duration(seconds: 1)); + expect(clock.whiteTime.value, const Duration(seconds: 4)); + expect(clock.blackTime.value, const Duration(seconds: 5)); + final thinkTime = clock.startSide(Side.white); + expect(thinkTime, const Duration(seconds: 1)); + async.elapse(const Duration(seconds: 1)); + expect(clock.whiteTime.value, const Duration(seconds: 3)); + expect(clock.blackTime.value, const Duration(seconds: 5)); + }); + }); + test('start with delay', () { fakeAsync((async) { final clock = ChessClock( whiteTime: const Duration(seconds: 5), blackTime: const Duration(seconds: 5), ); - clock.start(const Duration(milliseconds: 20)); + clock.start(delay: const Duration(milliseconds: 20)); expect(clock.isRunning, true); expect(clock.whiteTime.value, const Duration(seconds: 5)); async.elapse(const Duration(milliseconds: 10)); @@ -182,7 +203,7 @@ void main() { whiteTime: const Duration(seconds: 5), blackTime: const Duration(seconds: 5), emergencyThreshold: const Duration(seconds: 2), - onEmergency: () { + onEmergency: (_) { onEmergencyCount++; }, ); diff --git a/test/model/game/game_socket_example_data.dart b/test/model/game/game_socket_example_data.dart new file mode 100644 index 0000000000..902668c14e --- /dev/null +++ b/test/model/game/game_socket_example_data.dart @@ -0,0 +1,101 @@ +import 'package:dartchess/dartchess.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; + +typedef FullEventTestClock = ({ + bool running, + Duration initial, + Duration increment, + Duration white, + Duration black, +}); + +String makeFullEvent( + GameId id, + String pgn, { + required String whiteUserName, + required String blackUserName, + int socketVersion = 0, + Side? youAre, + FullEventTestClock clock = const ( + running: false, + initial: Duration(minutes: 3), + increment: Duration(seconds: 2), + white: Duration(minutes: 3), + black: Duration(minutes: 3), + ), +}) { + final youAreStr = youAre != null ? '"youAre": "${youAre.name}",' : ''; + return ''' +{ + "t": "full", + "d": { + "game": { + "id": "$id", + "variant": { + "key": "standard", + "name": "Standard", + "short": "Std" + }, + "speed": "blitz", + "perf": "blitz", + "rated": false, + "source": "lobby", + "status": { + "id": 20, + "name": "started" + }, + "createdAt": 1685698678928, + "pgn": "$pgn" + }, + "white": { + "user": { + "name": "$whiteUserName", + "patron": true, + "id": "${whiteUserName.toLowerCase()}" + }, + "rating": 1806, + "provisional": true, + "onGame": true + }, + "black": { + "user": { + "name": "$blackUserName", + "patron": true, + "id": "${blackUserName.toLowerCase()}" + }, + "onGame": true + }, + $youAreStr + "socket": $socketVersion, + "clock": { + "running": ${clock.running}, + "initial": ${clock.initial.inSeconds}, + "increment": ${clock.increment.inSeconds}, + "white": ${(clock.white.inMilliseconds / 1000).toStringAsFixed(2)}, + "black": ${(clock.black.inMilliseconds / 1000).toStringAsFixed(2)}, + "emerg": 60, + "moretime": 15 + }, + "expiration": { + "idleMillis": 245, + "millisToMove": 30000 + }, + "chat": { + "lines": [ + { + "u": "Zidrox", + "t": "Good luck", + "f": "people.man-singer" + }, + { + "u": "lichess", + "t": "Takeback accepted", + "f": "activity.lichess" + } + ] + } + }, + "v": $socketVersion +} +'''; +} diff --git a/test/network/fake_websocket_channel.dart b/test/network/fake_websocket_channel.dart index 1d639eb2f2..88e3391d04 100644 --- a/test/network/fake_websocket_channel.dart +++ b/test/network/fake_websocket_channel.dart @@ -7,7 +7,7 @@ import 'package:stream_channel/stream_channel.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; class FakeWebSocketChannelFactory implements WebSocketChannelFactory { - final FutureOr Function() createFunction; + final FutureOr Function(String url) createFunction; const FakeWebSocketChannelFactory(this.createFunction); @@ -17,7 +17,7 @@ class FakeWebSocketChannelFactory implements WebSocketChannelFactory { Map? headers, Duration timeout = const Duration(seconds: 1), }) async { - return createFunction(); + return createFunction(url); } } @@ -30,11 +30,19 @@ class FakeWebSocketChannelFactory implements WebSocketChannelFactory { /// behavior can be changed by setting [shouldSendPong] to false. /// /// It also allows to increase the lag of the connection by setting the -/// [connectionLag] property. +/// [connectionLag] property. By default [connectionLag] is set to [Duration.zero] +/// to simplify testing. +/// When lag is 0, the pong response will be sent in the next microtask. /// /// The [sentMessages] and [sentMessagesExceptPing] streams can be used to /// verify that the client sends the expected messages. class FakeWebSocketChannel implements WebSocketChannel { + FakeWebSocketChannel({this.connectionLag = Duration.zero}); + + int _pongCount = 0; + + final _connectionCompleter = Completer(); + static bool isPing(dynamic data) { if (data is! String) { return false; @@ -55,13 +63,19 @@ class FakeWebSocketChannel implements WebSocketChannel { /// The controller for outgoing (to server) messages. final _outcomingController = StreamController.broadcast(); + /// The lag of the connection (duration before pong response) in milliseconds. + Duration connectionLag; + /// Whether the server should send a pong response to a ping request. /// /// Can be used to simulate a faulty connection. bool shouldSendPong = true; - /// The lag of the connection (duration before pong response) in milliseconds. - Duration connectionLag = const Duration(milliseconds: 10); + /// Number of pong response received + int get pongCount => _pongCount; + + /// A Future that resolves when the first pong message is received + Future get connectionEstablished => _connectionCompleter.future; /// The stream of all outgoing messages. Stream get sentMessages => _outcomingController.stream; @@ -157,12 +171,22 @@ class FakeWebSocketSink implements WebSocketSink { // Simulates pong response if connection is not closed if (_channel.shouldSendPong && FakeWebSocketChannel.isPing(data)) { - Future.delayed(_channel.connectionLag, () { + void sendPong() { if (_channel._incomingController.isClosed) { return; } + _channel._pongCount++; + if (_channel._pongCount == 1) { + _channel._connectionCompleter.complete(); + } _channel._incomingController.add('0'); - }); + } + + if (_channel.connectionLag > Duration.zero) { + Future.delayed(_channel.connectionLag, sendPong); + } else { + scheduleMicrotask(sendPong); + } } } diff --git a/test/network/socket_test.dart b/test/network/socket_test.dart index 86e5d34d80..c5cbe7d5d2 100644 --- a/test/network/socket_test.dart +++ b/test/network/socket_test.dart @@ -45,7 +45,7 @@ void main() { final fakeChannel = FakeWebSocketChannel(); final socketClient = - makeTestSocketClient(FakeWebSocketChannelFactory(() => fakeChannel)); + makeTestSocketClient(FakeWebSocketChannelFactory((_) => fakeChannel)); socketClient.connect(); int sentPingCount = 0; @@ -69,7 +69,7 @@ void main() { test('reconnects when connection attempt fails', () async { int numConnectionAttempts = 0; - final fakeChannelFactory = FakeWebSocketChannelFactory(() { + final fakeChannelFactory = FakeWebSocketChannelFactory((_) { numConnectionAttempts++; if (numConnectionAttempts == 1) { throw const SocketException('Connection failed'); @@ -95,7 +95,7 @@ void main() { // channels per connection attempt final Map channels = {}; - final fakeChannelFactory = FakeWebSocketChannelFactory(() { + final fakeChannelFactory = FakeWebSocketChannelFactory((_) { numConnectionAttempts++; final channel = FakeWebSocketChannel(); int sentPingCount = 0; @@ -133,10 +133,11 @@ void main() { }); test('computes average lag', () async { - final fakeChannel = FakeWebSocketChannel(); + final fakeChannel = + FakeWebSocketChannel(connectionLag: const Duration(milliseconds: 10)); final socketClient = - makeTestSocketClient(FakeWebSocketChannelFactory(() => fakeChannel)); + makeTestSocketClient(FakeWebSocketChannelFactory((_) => fakeChannel)); socketClient.connect(); // before the connection is ready the average lag is zero @@ -188,7 +189,7 @@ void main() { final fakeChannel = FakeWebSocketChannel(); final socketClient = - makeTestSocketClient(FakeWebSocketChannelFactory(() => fakeChannel)); + makeTestSocketClient(FakeWebSocketChannelFactory((_) => fakeChannel)); socketClient.connect(); await socketClient.firstConnection; diff --git a/test/test_container.dart b/test/test_container.dart index ecb340ee0a..7e1ab64506 100644 --- a/test/test_container.dart +++ b/test/test_container.dart @@ -68,7 +68,7 @@ Future makeContainer({ return db; }), webSocketChannelFactoryProvider.overrideWith((ref) { - return FakeWebSocketChannelFactory(() => FakeWebSocketChannel()); + return FakeWebSocketChannelFactory((_) => FakeWebSocketChannel()); }), socketPoolProvider.overrideWith((ref) { final pool = SocketPool(ref); diff --git a/test/test_helpers.dart b/test/test_helpers.dart index 448d85578c..dfedcc6b3c 100644 --- a/test/test_helpers.dart +++ b/test/test_helpers.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:chessground/chessground.dart'; import 'package:dartchess/dartchess.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; @@ -114,17 +115,18 @@ Offset squareOffset( /// Plays a move on the board. Future playMove( WidgetTester tester, - Rect boardRect, String from, String to, { + Rect? boardRect, Side orientation = Side.white, }) async { + final rect = boardRect ?? tester.getRect(find.byType(Chessboard)); await tester.tapAt( - squareOffset(Square.fromName(from), boardRect, orientation: orientation), + squareOffset(Square.fromName(from), rect, orientation: orientation), ); await tester.pump(); await tester.tapAt( - squareOffset(Square.fromName(to), boardRect, orientation: orientation), + squareOffset(Square.fromName(to), rect, orientation: orientation), ); await tester.pump(); } diff --git a/test/test_provider_scope.dart b/test/test_provider_scope.dart index 8410a60edc..81419fd5a0 100644 --- a/test/test_provider_scope.dart +++ b/test/test_provider_scope.dart @@ -183,7 +183,7 @@ Future makeTestProviderScope( }), // ignore: scoped_providers_should_specify_dependencies webSocketChannelFactoryProvider.overrideWith((ref) { - return FakeWebSocketChannelFactory(() => FakeWebSocketChannel()); + return FakeWebSocketChannelFactory((_) => FakeWebSocketChannel()); }), // ignore: scoped_providers_should_specify_dependencies socketPoolProvider.overrideWith((ref) { diff --git a/test/view/game/game_screen_test.dart b/test/view/game/game_screen_test.dart new file mode 100644 index 0000000000..e67ff861d8 --- /dev/null +++ b/test/view/game/game_screen_test.dart @@ -0,0 +1,272 @@ +import 'package:chessground/chessground.dart'; +import 'package:dartchess/dartchess.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/testing.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/network/socket.dart'; +import 'package:lichess_mobile/src/view/game/game_screen.dart'; +import 'package:lichess_mobile/src/widgets/countdown_clock.dart'; + +import '../../model/game/game_socket_example_data.dart'; +import '../../network/fake_websocket_channel.dart'; +import '../../test_helpers.dart'; +import '../../test_provider_scope.dart'; + +final client = MockClient((request) { + return mockResponse('', 404); +}); + +void main() { + group('Loading', () { + testWidgets('a game directly with initialGameId', + (WidgetTester tester) async { + final fakeSocket = FakeWebSocketChannel(); + + final app = await makeTestProviderScopeApp( + tester, + home: const GameScreen( + initialGameId: GameFullId('qVChCOTcHSeW'), + ), + overrides: [ + lichessClientProvider + .overrideWith((ref) => LichessClient(client, ref)), + webSocketChannelFactoryProvider.overrideWith((ref) { + return FakeWebSocketChannelFactory((_) => fakeSocket); + }), + ], + ); + await tester.pumpWidget(app); + + // while loading, displays an empty board + expect(find.byType(Chessboard), findsOneWidget); + expect(find.byType(PieceWidget), findsNothing); + + // now loads the game controller + // screen doesn't have changed yet + await tester.pump(const Duration(milliseconds: 10)); + expect(find.byType(Chessboard), findsOneWidget); + expect(find.byType(PieceWidget), findsNothing); + + await fakeSocket.connectionEstablished; + + fakeSocket.addIncomingMessages([ + makeFullEvent( + const GameId('qVChCOTc'), + '', + whiteUserName: 'Peter', + blackUserName: 'Steven', + ), + ]); + // wait for socket message + await tester.pump(const Duration(milliseconds: 10)); + + expect(find.byType(PieceWidget), findsNWidgets(32)); + expect(find.text('Peter'), findsOneWidget); + expect(find.text('Steven'), findsOneWidget); + }); + }); + + group('Clock', () { + testWidgets('loads on game start', (WidgetTester tester) async { + final fakeSocket = FakeWebSocketChannel(); + await createTestGame(fakeSocket, tester); + expect(findClockWithTime('3:00'), findsNWidgets(2)); + expect( + tester + .widgetList(findClockWithTime('3:00')) + .where((widget) => widget is Clock && widget.active == false) + .length, + 2, + reason: 'clocks are not active yet', + ); + }); + + testWidgets('ticks after the first full move', (WidgetTester tester) async { + final fakeSocket = FakeWebSocketChannel(); + await createTestGame(fakeSocket, tester); + expect(findClockWithTime('3:00'), findsNWidgets(2)); + await playMove(tester, 'e2', 'e4'); + // at that point clock is not yet started + expect( + tester + .widgetList(find.byType(Clock)) + .where((widget) => widget.active == false) + .length, + 2, + reason: 'clocks are not active yet', + ); + fakeSocket.addIncomingMessages([ + '{"t": "move", "v": 1, "d": {"ply": 1, "uci": "e2e4", "san": "e4", "clock": {"white": 180, "black": 180}}}', + '{"t": "move", "v": 2, "d": {"ply": 2, "uci": "e7e5", "san": "e5", "clock": {"white": 180, "black": 180}}}', + ]); + await tester.pump(const Duration(milliseconds: 10)); + expect( + tester.widgetList(find.byType(Clock)).last.active, + true, + reason: 'my clock is now active', + ); + await tester.pump(const Duration(seconds: 1)); + expect(findClockWithTime('2:59'), findsOneWidget); + await tester.pump(const Duration(seconds: 1)); + expect(findClockWithTime('2:58'), findsOneWidget); + }); + + testWidgets('ticks immediately when resuming game', + (WidgetTester tester) async { + final fakeSocket = FakeWebSocketChannel(); + await createTestGame( + fakeSocket, + tester, + pgn: 'e4 e5 Nf3', + clock: const ( + running: true, + initial: Duration(minutes: 3), + increment: Duration(seconds: 2), + white: Duration(minutes: 2, seconds: 58), + black: Duration(minutes: 2, seconds: 54), + ), + ); + expect( + tester.widgetList(find.byType(Clock)).first.active, + true, + reason: 'black clock is already active', + ); + expect(findClockWithTime('2:58'), findsOneWidget); + expect(findClockWithTime('2:54'), findsOneWidget); + await tester.pump(const Duration(seconds: 1)); + expect(findClockWithTime('2:53'), findsOneWidget); + await tester.pump(const Duration(seconds: 1)); + expect(findClockWithTime('2:52'), findsOneWidget); + }); + + testWidgets('switch timer side after a move', (WidgetTester tester) async { + final fakeSocket = FakeWebSocketChannel(); + await createTestGame( + fakeSocket, + tester, + pgn: 'e4 e5', + clock: const ( + running: true, + initial: Duration(minutes: 3), + increment: Duration(seconds: 2), + white: Duration(minutes: 2, seconds: 58), + black: Duration(minutes: 3), + ), + ); + expect(tester.widgetList(find.byType(Clock)).last.active, true); + // simulates think time of 3s + await tester.pump(const Duration(seconds: 3)); + await playMove(tester, 'g1', 'f3'); + expect(findClockWithTime('2:55'), findsOneWidget); + expect( + tester.widgetList(find.byType(Clock)).last.active, + false, + reason: 'white clock is stopped while waiting for server ack', + ); + expect( + tester.widgetList(find.byType(Clock)).first.active, + true, + reason: 'black clock is now active but not yet ticking', + ); + expect(findClockWithTime('3:00'), findsOneWidget); + // simulates a long lag just to show the clock is not running yet + // in such case the server would send a "lag" field but we'll ignore it in that test + await tester.pump(const Duration(milliseconds: 200)); + expect(findClockWithTime('3:00'), findsOneWidget); + // server ack having the white clock updated with the increment + fakeSocket.addIncomingMessages([ + '{"t": "move", "v": 1, "d": {"ply": 3, "uci": "g1f3", "san": "Nf3", "clock": {"white": 177, "black": 180}}}', + ]); + await tester.pump(const Duration(milliseconds: 10)); + // we see now the white clock has got its increment + expect(findClockWithTime('2:57'), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + // black clock is ticking + expect(findClockWithTime('2:59'), findsOneWidget); + await tester.pump(const Duration(seconds: 1)); + expect(findClockWithTime('2:57'), findsOneWidget); + expect(findClockWithTime('2:58'), findsOneWidget); + await tester.pump(const Duration(seconds: 1)); + expect(findClockWithTime('2:57'), findsNWidgets(2)); + await tester.pump(const Duration(seconds: 1)); + expect(findClockWithTime('2:57'), findsOneWidget); + expect(findClockWithTime('2:56'), findsOneWidget); + }); + }); +} + +Finder findClockWithTime(String text, {bool skipOffstage = true}) { + return find.ancestor( + of: find.text(text, findRichText: true, skipOffstage: skipOffstage), + matching: find.byType(Clock, skipOffstage: skipOffstage), + ); +} + +// /// Simulates playing a move and getting the ack from the server after [elapsedTime]. +// Future playMoveWithServerAck( +// FakeWebSocketChannel socket, +// WidgetTester tester, +// String from, +// String to, { +// required String san, +// required ({Duration white, Duration black}) clockAck, +// required int socketVersion, +// required int ply, +// Duration elapsedTime = const Duration(milliseconds: 10), +// Rect? rect, +// Side orientation = Side.white, +// }) async { +// await playMove(tester, from, to, boardRect: rect, orientation: orientation); +// final uci = '$from$to'; +// socket.addIncomingMessages([ +// '{"t": "move", "v": $socketVersion, "d": {"ply": $ply, "uci": "$uci", "san": "$san", "clock": {"white": 180, "black": 180}}}', +// ]); +// await tester.pump(elapsedTime); +// } + +/// Convenient function to start a new test game +Future createTestGame( + FakeWebSocketChannel socket, + WidgetTester tester, { + Side? youAre = Side.white, + String? pgn, + int socketVersion = 0, + FullEventTestClock clock = const ( + running: false, + initial: Duration(minutes: 3), + increment: Duration(seconds: 2), + white: Duration(minutes: 3), + black: Duration(minutes: 3), + ), +}) async { + final app = await makeTestProviderScopeApp( + tester, + home: const GameScreen( + initialGameId: GameFullId('qVChCOTcHSeW'), + ), + overrides: [ + lichessClientProvider.overrideWith((ref) => LichessClient(client, ref)), + webSocketChannelFactoryProvider.overrideWith((ref) { + return FakeWebSocketChannelFactory((_) => socket); + }), + ], + ); + await tester.pumpWidget(app); + await tester.pump(const Duration(milliseconds: 10)); + await socket.connectionEstablished; + + socket.addIncomingMessages([ + makeFullEvent( + const GameId('qVChCOTc'), + pgn ?? '', + whiteUserName: 'Peter', + blackUserName: 'Steven', + youAre: youAre, + socketVersion: socketVersion, + clock: clock, + ), + ]); + await tester.pump(const Duration(milliseconds: 10)); +} diff --git a/test/view/over_the_board/over_the_board_screen_test.dart b/test/view/over_the_board/over_the_board_screen_test.dart index febe1c1614..5df769e94c 100644 --- a/test/view/over_the_board/over_the_board_screen_test.dart +++ b/test/view/over_the_board/over_the_board_screen_test.dart @@ -28,11 +28,11 @@ void main() { boardRect.bottomLeft, ); - await playMove(tester, boardRect, 'e2', 'e4'); - await playMove(tester, boardRect, 'f7', 'f6'); - await playMove(tester, boardRect, 'd2', 'd4'); - await playMove(tester, boardRect, 'g7', 'g5'); - await playMove(tester, boardRect, 'd1', 'h5'); + await playMove(tester, 'e2', 'e4'); + await playMove(tester, 'f7', 'f6'); + await playMove(tester, 'd2', 'd4'); + await playMove(tester, 'g7', 'g5'); + await playMove(tester, 'd1', 'h5'); await tester.pumpAndSettle(const Duration(milliseconds: 600)); expect(find.text('Checkmate • White is victorious'), findsOneWidget); @@ -58,13 +58,13 @@ void main() { testWidgets('Game ends when out of time', (tester) async { const time = Duration(seconds: 1); - final boardRect = await initOverTheBoardGame( + await initOverTheBoardGame( tester, TimeIncrement(time.inSeconds, 0), ); - await playMove(tester, boardRect, 'e2', 'e4'); - await playMove(tester, boardRect, 'e7', 'e5'); + await playMove(tester, 'e2', 'e4'); + await playMove(tester, 'e7', 'e5'); // The clock measures system time internally, so we need to actually sleep in order // for the clock to reach 0, instead of using tester.pump() @@ -81,13 +81,13 @@ void main() { testWidgets('Pausing the clock', (tester) async { const time = Duration(seconds: 10); - final boardRect = await initOverTheBoardGame( + await initOverTheBoardGame( tester, TimeIncrement(time.inSeconds, 0), ); - await playMove(tester, boardRect, 'e2', 'e4'); - await playMove(tester, boardRect, 'e7', 'e5'); + await playMove(tester, 'e2', 'e4'); + await playMove(tester, 'e7', 'e5'); await tester.tap(find.byTooltip('Pause')); await tester.pump(); @@ -108,7 +108,7 @@ void main() { expect(activeClock(tester), null); // ... but playing a move resumes the clock - await playMove(tester, boardRect, 'd7', 'd5'); + await playMove(tester, 'd7', 'd5'); expect(activeClock(tester), Side.white); }); @@ -116,13 +116,13 @@ void main() { testWidgets('Go back and Forward', (tester) async { const time = Duration(seconds: 10); - final boardRect = await initOverTheBoardGame( + await initOverTheBoardGame( tester, TimeIncrement(time.inSeconds, 0), ); - await playMove(tester, boardRect, 'e2', 'e4'); - await playMove(tester, boardRect, 'e7', 'e5'); + await playMove(tester, 'e2', 'e4'); + await playMove(tester, 'e7', 'e5'); await tester.tap(find.byTooltip('Previous')); await tester.pumpAndSettle(); @@ -148,7 +148,7 @@ void main() { expect(activeClock(tester), Side.white); - await playMove(tester, boardRect, 'e2', 'e4'); + await playMove(tester, 'e2', 'e4'); expect(find.byKey(const ValueKey('e4-whitepawn')), findsOneWidget); expect(activeClock(tester), Side.black); @@ -166,7 +166,7 @@ void main() { testWidgets('Clock logic', (tester) async { const time = Duration(minutes: 5); - final boardRect = await initOverTheBoardGame( + await initOverTheBoardGame( tester, TimeIncrement(time.inSeconds, 3), ); @@ -176,7 +176,7 @@ void main() { expect(findWhiteClock(tester).timeLeft, time); expect(findBlackClock(tester).timeLeft, time); - await playMove(tester, boardRect, 'e2', 'e4'); + await playMove(tester, 'e2', 'e4'); const moveTime = Duration(milliseconds: 500); await tester.pumpAndSettle(moveTime); @@ -186,7 +186,7 @@ void main() { expect(findWhiteClock(tester).timeLeft, time); expect(findBlackClock(tester).timeLeft, lessThan(time)); - await playMove(tester, boardRect, 'e7', 'e5'); + await playMove(tester, 'e7', 'e5'); await tester.pumpAndSettle(); expect(activeClock(tester), Side.white); diff --git a/test/view/puzzle/puzzle_screen_test.dart b/test/view/puzzle/puzzle_screen_test.dart index 41497a07ab..8845f25da3 100644 --- a/test/view/puzzle/puzzle_screen_test.dart +++ b/test/view/puzzle/puzzle_screen_test.dart @@ -209,9 +209,7 @@ void main() { expect(find.byKey(const Key('g4-blackrook')), findsOneWidget); expect(find.byKey(const Key('h8-whitequeen')), findsOneWidget); - final boardRect = tester.getRect(find.byType(Chessboard)); - - await playMove(tester, boardRect, 'g4', 'h4', orientation: orientation); + await playMove(tester, 'g4', 'h4', orientation: orientation); expect(find.byKey(const Key('h4-blackrook')), findsOneWidget); expect(find.text('Best move!'), findsOneWidget); @@ -222,7 +220,7 @@ void main() { expect(find.byKey(const Key('h4-whitequeen')), findsOneWidget); - await playMove(tester, boardRect, 'b4', 'h4', orientation: orientation); + await playMove(tester, 'b4', 'h4', orientation: orientation); expect(find.byKey(const Key('h4-blackrook')), findsOneWidget); expect(find.text('Success!'), findsOneWidget); @@ -313,9 +311,7 @@ void main() { expect(find.byKey(const Key('g4-blackrook')), findsOneWidget); - final boardRect = tester.getRect(find.byType(Chessboard)); - - await playMove(tester, boardRect, 'g4', 'f4', orientation: orientation); + await playMove(tester, 'g4', 'f4', orientation: orientation); expect( find.text("That's not the move!"), @@ -329,7 +325,7 @@ void main() { // can still play the puzzle expect(find.byKey(const Key('g4-blackrook')), findsOneWidget); - await playMove(tester, boardRect, 'g4', 'h4', orientation: orientation); + await playMove(tester, 'g4', 'h4', orientation: orientation); expect(find.byKey(const Key('h4-blackrook')), findsOneWidget); expect(find.text('Best move!'), findsOneWidget); @@ -338,7 +334,7 @@ void main() { await tester.pump(const Duration(milliseconds: 500)); await tester.pumpAndSettle(); - await playMove(tester, boardRect, 'b4', 'h4', orientation: orientation); + await playMove(tester, 'b4', 'h4', orientation: orientation); expect(find.byKey(const Key('h4-blackrook')), findsOneWidget); expect( diff --git a/test/view/puzzle/storm_screen_test.dart b/test/view/puzzle/storm_screen_test.dart index ac581af0d4..7f6e38996a 100644 --- a/test/view/puzzle/storm_screen_test.dart +++ b/test/view/puzzle/storm_screen_test.dart @@ -96,11 +96,8 @@ void main() { expect(find.byKey(const Key('g8-blackking')), findsOneWidget); - final boardRect = tester.getRect(find.byType(Chessboard)); - await playMove( tester, - boardRect, 'h5', 'h7', orientation: Side.white, @@ -113,7 +110,6 @@ void main() { await playMove( tester, - boardRect, 'e3', 'g1', orientation: Side.white, @@ -143,11 +139,8 @@ void main() { // wait for first move to be played await tester.pump(const Duration(seconds: 1)); - final boardRect = tester.getRect(find.byType(Chessboard)); - await playMove( tester, - boardRect, 'h5', 'h7', orientation: Side.white, @@ -156,7 +149,6 @@ void main() { await tester.pump(const Duration(milliseconds: 500)); await playMove( tester, - boardRect, 'e3', 'g1', orientation: Side.white, @@ -186,9 +178,8 @@ void main() { await tester.pumpWidget(app); await tester.pump(const Duration(seconds: 1)); - final boardRect = tester.getRect(find.byType(Chessboard)); - await playMove(tester, boardRect, 'h5', 'h6'); + await playMove(tester, 'h5', 'h6'); await tester.pump(const Duration(milliseconds: 500)); expect(find.byKey(const Key('h6-blackking')), findsOneWidget); From 35bd609cc587685a418b9b3f9462a8ee28b4bb6b Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 14 Nov 2024 12:37:22 +0100 Subject: [PATCH 662/979] Checks the clock correctly flags --- .../model/account/account_preferences.dart | 16 +++-- lib/src/model/clock/chess_clock.dart | 4 +- .../model/clock/clock_tool_controller.dart | 3 + lib/src/model/game/game_controller.dart | 2 + lib/src/model/game/game_status.dart | 15 ++++ test/model/clock/chess_clock_test.dart | 9 +++ test/view/game/game_screen_test.dart | 71 ++++++++++++++++++- test/view/study/study_screen_test.dart | 6 +- 8 files changed, 110 insertions(+), 16 deletions(-) diff --git a/lib/src/model/account/account_preferences.dart b/lib/src/model/account/account_preferences.dart index ee6950f089..343843f1d7 100644 --- a/lib/src/model/account/account_preferences.dart +++ b/lib/src/model/account/account_preferences.dart @@ -1,5 +1,6 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; @@ -29,28 +30,31 @@ typedef AccountPrefState = ({ }); /// A provider that tells if the user wants to see ratings in the app. -final showRatingsPrefProvider = FutureProvider((ref) async { +@Riverpod(keepAlive: true) +Future showRatingsPref(Ref ref) async { return ref.watch( accountPreferencesProvider .selectAsync((state) => state?.showRatings.value ?? true), ); -}); +} -final clockSoundProvider = FutureProvider((ref) async { +@Riverpod(keepAlive: true) +Future clockSound(Ref ref) async { return ref.watch( accountPreferencesProvider .selectAsync((state) => state?.clockSound.value ?? true), ); -}); +} -final pieceNotationProvider = FutureProvider((ref) async { +@Riverpod(keepAlive: true) +Future pieceNotation(Ref ref) async { return ref.watch( accountPreferencesProvider.selectAsync( (state) => state?.pieceNotation ?? defaultAccountPreferences.pieceNotation, ), ); -}); +} final defaultAccountPreferences = ( zenMode: Zen.no, diff --git a/lib/src/model/clock/chess_clock.dart b/lib/src/model/clock/chess_clock.dart index f902f7e2ab..b4560932e2 100644 --- a/lib/src/model/clock/chess_clock.dart +++ b/lib/src/model/clock/chess_clock.dart @@ -164,9 +164,7 @@ class ChessClock { if (_activeTime.value == Duration.zero) { onFlag?.call(); } - if (_activeTime.value > Duration.zero) { - _scheduleTick(); - } + _scheduleTick(); } void _checkEmergency() { diff --git a/lib/src/model/clock/clock_tool_controller.dart b/lib/src/model/clock/clock_tool_controller.dart index 1ceb2c9b86..73396eede0 100644 --- a/lib/src/model/clock/clock_tool_controller.dart +++ b/lib/src/model/clock/clock_tool_controller.dart @@ -26,6 +26,9 @@ class ClockToolController extends _$ClockToolController { _clock = ChessClock( whiteTime: time, blackTime: time, + onFlag: () { + setLoser(_clock.activeSide); + }, ); ref.onDispose(() { diff --git a/lib/src/model/game/game_controller.dart b/lib/src/model/game/game_controller.dart index a8150735d8..b2b6c2098b 100644 --- a/lib/src/model/game/game_controller.dart +++ b/lib/src/model/game/game_controller.dart @@ -451,6 +451,8 @@ class GameController extends _$GameController { _clock?.setTimes(whiteTime: white, blackTime: black); if (activeSide != null) { _clock?.startSide(activeSide, delay: lag); + } else { + _clock?.stop(); } } diff --git a/lib/src/model/game/game_status.dart b/lib/src/model/game/game_status.dart index 58047c6aac..b091550940 100644 --- a/lib/src/model/game/game_status.dart +++ b/lib/src/model/game/game_status.dart @@ -2,19 +2,34 @@ import 'package:deep_pick/deep_pick.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; enum GameStatus { + /// Unknown game status (not handled by the app). unknown(-1), + + /// The game is created but not started yet. created(10), started(20), + + /// From here on, the game is finished. aborted(25), mate(30), resign(31), stalemate(32), + + /// When a player leaves the game. timeout(33), draw(34), + + /// When a player runs out of time (clock flags). outoftime(35), cheat(36), + + /// The player did not make the first move in time. noStart(37), + + /// We don't know why the game ended. unknownFinish(38), + + /// Chess variant special endings. variantEnd(60); static final nameMap = IMap(GameStatus.values.asNameMap()); diff --git a/test/model/clock/chess_clock_test.dart b/test/model/clock/chess_clock_test.dart index 2650f35b95..f0c2a45870 100644 --- a/test/model/clock/chess_clock_test.dart +++ b/test/model/clock/chess_clock_test.dart @@ -193,6 +193,15 @@ void main() { expect(flagCount, 1); expect(clock.whiteTime.value, Duration.zero); expect(clock.blackTime.value, const Duration(seconds: 5)); + + // continue ticking and calling onFlag + async.elapse(const Duration(milliseconds: 200)); + expect(flagCount, 3); + clock.stop(); + + // no more onFlag calls + async.elapse(const Duration(seconds: 5)); + expect(flagCount, 3); }); }); diff --git a/test/view/game/game_screen_test.dart b/test/view/game/game_screen_test.dart index e67ff861d8..b0b49321d1 100644 --- a/test/view/game/game_screen_test.dart +++ b/test/view/game/game_screen_test.dart @@ -1,6 +1,5 @@ import 'package:chessground/chessground.dart'; import 'package:dartchess/dartchess.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; @@ -75,8 +74,8 @@ void main() { expect(findClockWithTime('3:00'), findsNWidgets(2)); expect( tester - .widgetList(findClockWithTime('3:00')) - .where((widget) => widget is Clock && widget.active == false) + .widgetList(find.byType(Clock)) + .where((widget) => widget.active == false) .length, 2, reason: 'clocks are not active yet', @@ -194,6 +193,72 @@ void main() { expect(findClockWithTime('2:57'), findsOneWidget); expect(findClockWithTime('2:56'), findsOneWidget); }); + + testWidgets('flags', (WidgetTester tester) async { + final fakeSocket = FakeWebSocketChannel(); + await createTestGame( + fakeSocket, + tester, + pgn: 'e4 e5 Nf3', + clock: const ( + running: true, + initial: Duration(minutes: 3), + increment: Duration(seconds: 2), + white: Duration(minutes: 2, seconds: 58), + black: Duration(minutes: 2, seconds: 54), + ), + ); + expect( + tester.widgetList(find.byType(Clock)).first.active, + true, + reason: 'black clock is active', + ); + + expect(findClockWithTime('2:58'), findsOneWidget); + expect(findClockWithTime('2:54'), findsOneWidget); + await tester.pump(const Duration(seconds: 1)); + expect(findClockWithTime('2:53'), findsOneWidget); + await tester.pump(const Duration(minutes: 2, seconds: 53)); + expect(findClockWithTime('2:58'), findsOneWidget); + expect(findClockWithTime('0:00.0'), findsOneWidget); + + expect( + tester.widgetList(find.byType(Clock)).first.active, + true, + reason: + 'black clock is still active after flag (as long as we have not received server ack)', + ); + + // flag messages are throttled with 500ms delay + // we'll simulate an anormally long server response of 1s to check 2 + // flag messages are sent + expectLater( + fakeSocket.sentMessagesExceptPing, + emitsInOrder([ + '{"t":"flag","d":"black"}', + '{"t":"flag","d":"black"}', + ]), + ); + await tester.pump(const Duration(seconds: 1)); + fakeSocket.addIncomingMessages([ + '{"t":"endData","d":{"status":"outoftime","winner":"white","clock":{"wc":17800,"bc":0}}}', + ]); + await tester.pump(const Duration(milliseconds: 10)); + + expect( + tester + .widgetList(find.byType(Clock)) + .where((widget) => widget.active == false) + .length, + 2, + reason: 'both clocks are now inactive', + ); + expect(findClockWithTime('2:58'), findsOneWidget); + expect(findClockWithTime('0:00.00'), findsOneWidget); + + // wait for the dong + await tester.pump(const Duration(seconds: 500)); + }); }); } diff --git a/test/view/study/study_screen_test.dart b/test/view/study/study_screen_test.dart index dd80b3a92c..04f1c0a7f8 100644 --- a/test/view/study/study_screen_test.dart +++ b/test/view/study/study_screen_test.dart @@ -1,4 +1,3 @@ -import 'package:chessground/chessground.dart'; import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/widgets.dart'; @@ -225,13 +224,12 @@ void main() { // Wait for study to load await tester.pumpAndSettle(); - final boardRect = tester.getRect(find.byType(Chessboard)); - await playMove(tester, boardRect, 'e2', 'e4', orientation: Side.black); + await playMove(tester, 'e2', 'e4', orientation: Side.black); expect(find.byKey(const Key('e2-whitepawn')), findsNothing); expect(find.byKey(const Key('e4-whitepawn')), findsOneWidget); - await playMove(tester, boardRect, 'e7', 'e5', orientation: Side.black); + await playMove(tester, 'e7', 'e5', orientation: Side.black); expect(find.byKey(const Key('e5-blackpawn')), findsOneWidget); expect(find.byKey(const Key('e7-blackpawn')), findsNothing); From 5874914834374d8ae537ca533cd0f85bb19c059c Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 14 Nov 2024 12:53:13 +0100 Subject: [PATCH 663/979] Simplify countdown clock --- lib/src/widgets/countdown_clock.dart | 64 ++-------------- test/widgets/countdown_clock_test.dart | 102 ++----------------------- 2 files changed, 12 insertions(+), 154 deletions(-) diff --git a/lib/src/widgets/countdown_clock.dart b/lib/src/widgets/countdown_clock.dart index d9fdcd91fa..85b43e78dd 100644 --- a/lib/src/widgets/countdown_clock.dart +++ b/lib/src/widgets/countdown_clock.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:clock/clock.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; @@ -12,12 +11,8 @@ import 'package:lichess_mobile/src/utils/screen.dart'; class CountdownClock extends StatefulWidget { const CountdownClock({ required this.timeLeft, - this.delay, this.clockUpdatedAt, required this.active, - this.emergencyThreshold, - this.onEmergency, - this.onFlag, this.clockStyle, this.padLeft = false, super.key, @@ -26,31 +21,15 @@ class CountdownClock extends StatefulWidget { /// The duration left on the clock. final Duration timeLeft; - /// The delay before the clock starts counting down. - /// - /// This can be used to implement lag compensation. - final Duration? delay; - /// The time at which the clock was updated. /// /// Use this parameter to synchronize the clock with the time at which the clock /// event was received from the server and to compensate for UI lag. final DateTime? clockUpdatedAt; - /// The duration at which the clock should change its background color to indicate an emergency. - /// - /// If [onEmergency] is provided, the clock will call it when the emergency threshold is reached. - final Duration? emergencyThreshold; - - /// Called when the clock reaches the emergency. - final VoidCallback? onEmergency; - /// If [active] is `true`, the clock starts counting down. final bool active; - /// Callback when the clock reaches zero. - final VoidCallback? onFlag; - /// Custom color style final ClockStyle? clockStyle; @@ -61,43 +40,30 @@ class CountdownClock extends StatefulWidget { State createState() => _CountdownClockState(); } -const _emergencyDelay = Duration(seconds: 20); +const _tickDelay = Duration(milliseconds: 100); const _showTenthsThreshold = Duration(seconds: 10); class _CountdownClockState extends State { Timer? _timer; Duration timeLeft = Duration.zero; - bool _shouldPlayEmergencyFeedback = true; - DateTime? _nextEmergency; final _stopwatch = clock.stopwatch(); void startClock() { final now = clock.now(); - final delay = widget.delay ?? Duration.zero; final clockUpdatedAt = widget.clockUpdatedAt ?? now; // UI lag diff: the elapsed time between the time the clock should have started // and the time the clock is actually started final uiLag = now.difference(clockUpdatedAt); - final realDelay = delay - uiLag; - // real delay is negative, we need to adjust the timeLeft. - if (realDelay < Duration.zero) { - timeLeft = timeLeft + realDelay; - } + timeLeft = timeLeft - uiLag; - _scheduleTick(realDelay); + _scheduleTick(); } - void _scheduleTick(Duration extraDelay) { + void _scheduleTick() { _timer?.cancel(); - final delay = Duration( - milliseconds: (timeLeft < _showTenthsThreshold - ? timeLeft.inMilliseconds % 100 - : timeLeft.inMilliseconds % 500) + - 1, - ); - _timer = Timer(delay + extraDelay, _tick); + _timer = Timer(_tickDelay, _tick); _stopwatch.reset(); _stopwatch.start(); } @@ -105,14 +71,12 @@ class _CountdownClockState extends State { void _tick() { setState(() { timeLeft = timeLeft - _stopwatch.elapsed; - _playEmergencyFeedback(); if (timeLeft <= Duration.zero) { - widget.onFlag?.call(); timeLeft = Duration.zero; } }); if (timeLeft > Duration.zero) { - _scheduleTick(Duration.zero); + _scheduleTick(); } } @@ -121,21 +85,6 @@ class _CountdownClockState extends State { _stopwatch.stop(); } - void _playEmergencyFeedback() { - if (widget.emergencyThreshold != null && - timeLeft <= widget.emergencyThreshold! && - _shouldPlayEmergencyFeedback && - (_nextEmergency == null || _nextEmergency!.isBefore(clock.now()))) { - _shouldPlayEmergencyFeedback = false; - _nextEmergency = clock.now().add(_emergencyDelay); - widget.onEmergency?.call(); - HapticFeedback.heavyImpact(); - } else if (widget.emergencyThreshold != null && - timeLeft > widget.emergencyThreshold! * 1.5) { - _shouldPlayEmergencyFeedback = true; - } - } - @override void initState() { super.initState(); @@ -175,7 +124,6 @@ class _CountdownClockState extends State { padLeft: widget.padLeft, timeLeft: timeLeft, active: widget.active, - emergencyThreshold: widget.emergencyThreshold, clockStyle: widget.clockStyle, ), ); diff --git a/test/widgets/countdown_clock_test.dart b/test/widgets/countdown_clock_test.dart index 418050d635..4669f4ae74 100644 --- a/test/widgets/countdown_clock_test.dart +++ b/test/widgets/countdown_clock_test.dart @@ -121,10 +121,8 @@ void main() { ); expect(find.text('0:01.0', findRichText: true), findsOneWidget); - await tester.pump(const Duration(milliseconds: 10)); - expect(find.text('0:00.9', findRichText: true), findsOneWidget); - await tester.pump(const Duration(milliseconds: 10)); - expect(find.text('0:00.9', findRichText: true), findsOneWidget); + await tester.pump(const Duration(milliseconds: 900)); + expect(find.text('0:00.1', findRichText: true), findsOneWidget); await tester.pumpWidget( MaterialApp( @@ -172,108 +170,20 @@ void main() { expect(find.text('0:09.7', findRichText: true), findsOneWidget); }); - testWidgets('starts with a delay if set', (WidgetTester tester) async { - await tester.pumpWidget( - const MaterialApp( - home: CountdownClock( - timeLeft: Duration(seconds: 10), - active: true, - delay: Duration(milliseconds: 25), - ), - ), - ); - expect(find.text('0:10', findRichText: true), findsOneWidget); - await tester.pump(const Duration(milliseconds: 25)); - expect(find.text('0:10', findRichText: true), findsOneWidget); - await tester.pump(const Duration(milliseconds: 50)); - expect(find.text('0:09.9', findRichText: true), findsOneWidget); - }); - - testWidgets('compensates for UI lag', (WidgetTester tester) async { + testWidgets('UI lag compensation', (WidgetTester tester) async { final now = clock.now(); - await tester.pump(const Duration(milliseconds: 10)); + await tester.pump(const Duration(milliseconds: 200)); await tester.pumpWidget( MaterialApp( home: CountdownClock( timeLeft: const Duration(seconds: 10), active: true, - delay: const Duration(milliseconds: 20), clockUpdatedAt: now, ), ), ); - expect(find.text('0:10', findRichText: true), findsOneWidget); - - await tester.pump(const Duration(milliseconds: 10)); - expect(find.text('0:10', findRichText: true), findsOneWidget); - - // delay was 20m but UI lagged 10ms so with the compensation the clock has started already - await tester.pump(const Duration(milliseconds: 10)); - expect(find.text('0:09.9', findRichText: true), findsOneWidget); - }); - - testWidgets('UI lag negative start delay', (WidgetTester tester) async { - final now = clock.now(); - await tester.pump(const Duration(milliseconds: 20)); - - await tester.pumpWidget( - MaterialApp( - home: CountdownClock( - timeLeft: const Duration(seconds: 10), - active: true, - delay: const Duration(milliseconds: 10), - clockUpdatedAt: now, - ), - ), - ); - // delay was 10ms but UI lagged 20ms so the clock time is already 10ms ahead - expect(find.text('0:09.9', findRichText: true), findsOneWidget); - }); - - testWidgets('should call onFlag', (WidgetTester tester) async { - int flagCount = 0; - await tester.pumpWidget( - MaterialApp( - home: CountdownClock( - timeLeft: const Duration(seconds: 10), - active: true, - onFlag: () { - flagCount++; - }, - ), - ), - ); - expect(find.text('0:10', findRichText: true), findsOneWidget); - await tester.pump(const Duration(seconds: 11)); - expect(flagCount, 1); - expect(find.text('0:00.0', findRichText: true), findsOneWidget); - }); - - testWidgets('emergency feedback', (WidgetTester tester) async { - int onEmergencyCount = 0; - await tester.pumpWidget( - MaterialApp( - home: CountdownClock( - timeLeft: const Duration(seconds: 10), - active: true, - emergencyThreshold: const Duration(seconds: 5), - onEmergency: () { - onEmergencyCount++; - }, - ), - ), - ); - - expect(find.text('0:10', findRichText: true), findsOneWidget); - await tester.pump(const Duration(seconds: 5)); - expect(find.text('0:05.0', findRichText: true), findsOneWidget); - expect(onEmergencyCount, 0); - await tester.pump(const Duration(milliseconds: 1)); - expect(onEmergencyCount, 1); - await tester.pump(const Duration(milliseconds: 100)); - expect(find.text('0:04.8', findRichText: true), findsOneWidget); - // emergency is only called once as long as the time is below the threshold - expect(onEmergencyCount, 1); + // UI lagged 200ms so the clock time is already 200ms ahead + expect(find.text('0:09.8', findRichText: true), findsOneWidget); }); } From d7161d6a9c45df6c5b052be833a398e5b9ca187b Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 14 Nov 2024 13:09:28 +0100 Subject: [PATCH 664/979] Tweak --- .../correspondence/offline_correspondence_game.dart | 1 - lib/src/model/game/game_controller.dart | 9 ++++----- lib/src/model/game/playable_game.dart | 5 +++-- .../offline_correspondence_game_screen.dart | 2 +- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/lib/src/model/correspondence/offline_correspondence_game.dart b/lib/src/model/correspondence/offline_correspondence_game.dart index 1c8fc0cf5a..d7bcb1599f 100644 --- a/lib/src/model/correspondence/offline_correspondence_game.dart +++ b/lib/src/model/correspondence/offline_correspondence_game.dart @@ -72,7 +72,6 @@ class OfflineCorrespondenceGame return null; } - bool get isPlayerTurn => lastPosition.turn == youAre; bool get playable => status.value < GameStatus.aborted.value; bool get playing => status.value > GameStatus.started.value; bool get finished => status.value >= GameStatus.mate.value; diff --git a/lib/src/model/game/game_controller.dart b/lib/src/model/game/game_controller.dart index b2b6c2098b..e201c2dca5 100644 --- a/lib/src/model/game/game_controller.dart +++ b/lib/src/model/game/game_controller.dart @@ -655,7 +655,7 @@ class GameController extends _$GameController { if (data.clock != null) { final lagCompensation = - newState.game.playable && newState.game.isPlayerTurn + newState.game.playable && newState.game.isMyTurn // server will send the lag only if it's more than 10ms ? Duration.zero : data.clock?.lag ?? const Duration(milliseconds: 10); @@ -668,12 +668,11 @@ class GameController extends _$GameController { ); if (newState.game.clock != null) { - // TODO remove that + // we don't rely on these values to display the clock, but let's keep + // the game object in sync newState = newState.copyWith.game.clock!( white: data.clock!.white, black: data.clock!.black, - // lag: lagCompensation, - // at: data.clock!.at, ); } else if (newState.game.correspondenceClock != null) { newState = newState.copyWith.game.correspondenceClock!( @@ -1131,7 +1130,7 @@ class GameState with _$GameState { game.drawable && (lastDrawOfferAtPly ?? -99) < game.lastPly - 20; bool get canShowClaimWinCountdown => - !game.isPlayerTurn && + !game.isMyTurn && game.resignable && (game.meta.rules == null || !game.meta.rules!.contains(GameRule.noClaimWin)); diff --git a/lib/src/model/game/playable_game.dart b/lib/src/model/game/playable_game.dart index 9c2a6feb7f..e0ae58fc0f 100644 --- a/lib/src/model/game/playable_game.dart +++ b/lib/src/model/game/playable_game.dart @@ -95,7 +95,8 @@ class PlayableGame bool get imported => source == GameSource.import; - bool get isPlayerTurn => lastPosition.turn == youAre; + /// Whether it is the current player's turn. + bool get isMyTurn => lastPosition.turn == youAre; /// Whether the game is properly finished (not aborted). bool get finished => status.value >= GameStatus.mate.value; @@ -125,7 +126,7 @@ class PlayableGame bool get canClaimWin => opponent?.isGone == true && - !isPlayerTurn && + !isMyTurn && resignable && (meta.rules == null || !meta.rules!.contains(GameRule.noClaimWin)); diff --git a/lib/src/view/correspondence/offline_correspondence_game_screen.dart b/lib/src/view/correspondence/offline_correspondence_game_screen.dart index e2f181ebd3..0ce0cdae57 100644 --- a/lib/src/view/correspondence/offline_correspondence_game_screen.dart +++ b/lib/src/view/correspondence/offline_correspondence_game_screen.dart @@ -280,7 +280,7 @@ class _BodyState extends ConsumerState<_Body> { data: (games) { final nextTurn = games .whereNot((g) => g.$2.id == game.id) - .firstWhereOrNull((g) => g.$2.isPlayerTurn); + .firstWhereOrNull((g) => g.$2.isMyTurn); return nextTurn != null ? () { widget.onGameChanged(nextTurn); From 2cfc4bf436e38c21cee9d150d10bea7f4f489191 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 14 Nov 2024 13:35:51 +0100 Subject: [PATCH 665/979] Fix clock tool --- .../model/clock/clock_tool_controller.dart | 29 ++++++++++--------- ...ock_screen.dart => clock_tool_screen.dart} | 8 ++--- lib/src/view/tools/tools_tab_screen.dart | 4 +-- lib/src/widgets/countdown_clock.dart | 6 ++++ 4 files changed, 27 insertions(+), 20 deletions(-) rename lib/src/view/clock/{clock_screen.dart => clock_tool_screen.dart} (97%) diff --git a/lib/src/model/clock/clock_tool_controller.dart b/lib/src/model/clock/clock_tool_controller.dart index 73396eede0..524b543a9d 100644 --- a/lib/src/model/clock/clock_tool_controller.dart +++ b/lib/src/model/clock/clock_tool_controller.dart @@ -11,7 +11,7 @@ part 'clock_tool_controller.g.dart'; @riverpod class ClockToolController extends _$ClockToolController { - late ChessClock _clock; + late final ChessClock _clock; @override ClockState build() { @@ -26,9 +26,7 @@ class ClockToolController extends _$ClockToolController { _clock = ChessClock( whiteTime: time, blackTime: time, - onFlag: () { - setLoser(_clock.activeSide); - }, + onFlag: _onFlagged, ); ref.onDispose(() { @@ -43,6 +41,11 @@ class ClockToolController extends _$ClockToolController { ); } + void _onFlagged() { + _clock.stop(); + state = state.copyWith(flagged: _clock.activeSide); + } + void onTap(Side playerType) { final started = state.started; if (playerType == Side.white) { @@ -69,7 +72,7 @@ class ClockToolController extends _$ClockToolController { } void updateDuration(Side playerType, Duration duration) { - if (state.loser != null || state.paused) { + if (state.flagged != null || state.paused) { return; } @@ -85,7 +88,7 @@ class ClockToolController extends _$ClockToolController { void updateOptions(TimeIncrement timeIncrement) { final options = ClockOptions.fromTimeIncrement(timeIncrement); - _clock = ChessClock( + _clock.setTimes( whiteTime: options.whiteTime, blackTime: options.blackTime, ); @@ -114,7 +117,7 @@ class ClockToolController extends _$ClockToolController { ? Duration(seconds: clock.increment) : state.options.blackIncrement, ); - _clock = ChessClock( + _clock.setTimes( whiteTime: options.whiteTime, blackTime: options.blackTime, ); @@ -129,10 +132,8 @@ class ClockToolController extends _$ClockToolController { void setBottomPlayer(Side playerType) => state = state.copyWith(bottomPlayer: playerType); - void setLoser(Side playerType) => state = state.copyWith(loser: playerType); - void reset() { - _clock = ChessClock( + _clock.setTimes( whiteTime: state.options.whiteTime, blackTime: state.options.whiteTime, ); @@ -140,7 +141,7 @@ class ClockToolController extends _$ClockToolController { whiteTime: _clock.whiteTime, blackTime: _clock.blackTime, activeSide: Side.white, - loser: null, + flagged: null, started: false, paused: false, whiteMoves: 0, @@ -205,7 +206,7 @@ class ClockState with _$ClockState { required ValueListenable blackTime, required Side activeSide, @Default(Side.white) Side bottomPlayer, - Side? loser, + Side? flagged, @Default(false) bool started, @Default(false) bool paused, @Default(0) int whiteMoves, @@ -219,12 +220,12 @@ class ClockState with _$ClockState { playerType == Side.white ? whiteMoves : blackMoves; bool isPlayersTurn(Side playerType) => - started && activeSide == playerType && loser == null; + started && activeSide == playerType && flagged == null; bool isPlayersMoveAllowed(Side playerType) => isPlayersTurn(playerType) && !paused; bool isActivePlayer(Side playerType) => isPlayersTurn(playerType) && !paused; - bool isLoser(Side playerType) => loser == playerType; + bool isFlagged(Side playerType) => flagged == playerType; } diff --git a/lib/src/view/clock/clock_screen.dart b/lib/src/view/clock/clock_tool_screen.dart similarity index 97% rename from lib/src/view/clock/clock_screen.dart rename to lib/src/view/clock/clock_tool_screen.dart index 103e51a7b6..1f6c319adf 100644 --- a/lib/src/view/clock/clock_screen.dart +++ b/lib/src/view/clock/clock_tool_screen.dart @@ -13,8 +13,8 @@ import 'package:lichess_mobile/src/widgets/countdown_clock.dart'; import 'custom_clock_settings.dart'; -class ClockScreen extends StatelessWidget { - const ClockScreen({super.key}); +class ClockToolScreen extends StatelessWidget { + const ClockToolScreen({super.key}); @override Widget build(BuildContext context) { @@ -79,7 +79,7 @@ class ClockTile extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final colorScheme = Theme.of(context).colorScheme; - final backgroundColor = clockState.isLoser(playerType) + final backgroundColor = clockState.isFlagged(playerType) ? context.lichessColors.error : !clockState.paused && clockState.isPlayersTurn(playerType) ? colorScheme.primary @@ -152,7 +152,7 @@ class ClockTile extends ConsumerWidget { }, ), secondChild: const Icon(Icons.flag), - crossFadeState: clockState.isLoser(playerType) + crossFadeState: clockState.isFlagged(playerType) ? CrossFadeState.showSecond : CrossFadeState.showFirst, ), diff --git a/lib/src/view/tools/tools_tab_screen.dart b/lib/src/view/tools/tools_tab_screen.dart index 77765f9282..8270f84bcc 100644 --- a/lib/src/view/tools/tools_tab_screen.dart +++ b/lib/src/view/tools/tools_tab_screen.dart @@ -12,7 +12,7 @@ import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; import 'package:lichess_mobile/src/view/board_editor/board_editor_screen.dart'; -import 'package:lichess_mobile/src/view/clock/clock_screen.dart'; +import 'package:lichess_mobile/src/view/clock/clock_tool_screen.dart'; import 'package:lichess_mobile/src/view/coordinate_training/coordinate_training_screen.dart'; import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_screen.dart'; import 'package:lichess_mobile/src/view/study/study_list_screen.dart'; @@ -209,7 +209,7 @@ class _Body extends ConsumerWidget { title: context.l10n.clock, onTap: () => pushPlatformRoute( context, - builder: (context) => const ClockScreen(), + builder: (context) => const ClockToolScreen(), rootNavigator: true, ), ), diff --git a/lib/src/widgets/countdown_clock.dart b/lib/src/widgets/countdown_clock.dart index 85b43e78dd..8411ea1a8a 100644 --- a/lib/src/widgets/countdown_clock.dart +++ b/lib/src/widgets/countdown_clock.dart @@ -11,6 +11,7 @@ import 'package:lichess_mobile/src/utils/screen.dart'; class CountdownClock extends StatefulWidget { const CountdownClock({ required this.timeLeft, + this.emergencyThreshold, this.clockUpdatedAt, required this.active, this.clockStyle, @@ -21,6 +22,10 @@ class CountdownClock extends StatefulWidget { /// The duration left on the clock. final Duration timeLeft; + /// If [timeLeft] is less than [emergencyThreshold], the clock will set + /// its background color to [ClockStyle.emergencyBackgroundColor]. + final Duration? emergencyThreshold; + /// The time at which the clock was updated. /// /// Use this parameter to synchronize the clock with the time at which the clock @@ -122,6 +127,7 @@ class _CountdownClockState extends State { return RepaintBoundary( child: Clock( padLeft: widget.padLeft, + emergencyThreshold: widget.emergencyThreshold, timeLeft: timeLeft, active: widget.active, clockStyle: widget.clockStyle, From 4ac1342dc8b37176a7a77c2409aa3689bb850f62 Mon Sep 17 00:00:00 2001 From: Jimima Date: Thu, 14 Nov 2024 11:07:23 +0000 Subject: [PATCH 666/979] Back in time --- lib/src/view/game/game_result_dialog.dart | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/lib/src/view/game/game_result_dialog.dart b/lib/src/view/game/game_result_dialog.dart index 30bd51ebb8..ae6f8b9d62 100644 --- a/lib/src/view/game/game_result_dialog.dart +++ b/lib/src/view/game/game_result_dialog.dart @@ -139,19 +139,17 @@ class _GameEndDialogState extends ConsumerState { children: [ // Text("Rematch offered"), FatButton( - semanticsLabel: context.l10n.rematch, - child: const Text('Accept rematch'), - onPressed: () { - ref.read(ctrlProvider.notifier).proposeOrAcceptRematch(); - }, - ), + semanticsLabel: context.l10n.rematch, + child: const Text('Accept rematch'), + onPressed: () { + ref.read(ctrlProvider.notifier).proposeOrAcceptRematch(); + },), SecondaryButton( - semanticsLabel: context.l10n.rematch, - child: const Text('Decline'), - onPressed: () { - ref.read(ctrlProvider.notifier).declineRematch(); - }, - ), + semanticsLabel: context.l10n.rematch, + child: const Text('Decline'), + onPressed: () { + ref.read(ctrlProvider.notifier).declineRematch(); + },), ], ) else if (gameState.canOfferRematch) From b0d9fc564f670df62c0fae5b678fb9f486499cbf Mon Sep 17 00:00:00 2001 From: Jimima Date: Thu, 14 Nov 2024 13:46:39 +0000 Subject: [PATCH 667/979] Fade in buttons, no label --- lib/src/view/game/game_result_dialog.dart | 35 ++++++++++++++--------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/lib/src/view/game/game_result_dialog.dart b/lib/src/view/game/game_result_dialog.dart index ae6f8b9d62..8d4707cb31 100644 --- a/lib/src/view/game/game_result_dialog.dart +++ b/lib/src/view/game/game_result_dialog.dart @@ -132,27 +132,36 @@ class _GameEndDialogState extends ConsumerState { context.l10n.cancelRematchOffer, textAlign: TextAlign.center, ), - ) - else if (gameState.game.opponent?.offeringRematch == true) - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // Text("Rematch offered"), - FatButton( + ), + Visibility( + maintainState: true, + visible: gameState.game.opponent?.offeringRematch ?? false, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 300), + opacity: gameState.game.opponent?.offeringRematch ?? false ? 1 : 0, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FatButton( semanticsLabel: context.l10n.rematch, child: const Text('Accept rematch'), onPressed: () { ref.read(ctrlProvider.notifier).proposeOrAcceptRematch(); - },), - SecondaryButton( + }, + ), + SecondaryButton( semanticsLabel: context.l10n.rematch, child: const Text('Decline'), onPressed: () { ref.read(ctrlProvider.notifier).declineRematch(); - },), - ], - ) - else if (gameState.canOfferRematch) + }, + ), + ], + ), + ), + ), + if (gameState.canOfferRematch && + !(gameState.game.opponent?.offeringRematch ?? false)) SecondaryButton( semanticsLabel: context.l10n.rematch, onPressed: _activateButtons && From 4050090fa23af464bd488eb7c31c30638a86313f Mon Sep 17 00:00:00 2001 From: Jimima Date: Thu, 14 Nov 2024 16:21:17 +0000 Subject: [PATCH 668/979] Fixed persistand rematch button bug --- lib/src/view/game/game_result_dialog.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/view/game/game_result_dialog.dart b/lib/src/view/game/game_result_dialog.dart index 8d4707cb31..3af97d58da 100644 --- a/lib/src/view/game/game_result_dialog.dart +++ b/lib/src/view/game/game_result_dialog.dart @@ -161,7 +161,8 @@ class _GameEndDialogState extends ConsumerState { ), ), if (gameState.canOfferRematch && - !(gameState.game.opponent?.offeringRematch ?? false)) + !(gameState.game.opponent?.offeringRematch ?? false) && + !(gameState.game.me?.offeringRematch ?? false)) SecondaryButton( semanticsLabel: context.l10n.rematch, onPressed: _activateButtons && From 8edb9ef0e5dd50ca79d2906fc379ad53cd541cb5 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 14 Nov 2024 18:49:30 +0100 Subject: [PATCH 669/979] Add more game clock tests --- lib/src/model/game/game_controller.dart | 13 +- test/model/game/game_socket_example_data.dart | 4 +- test/view/game/game_screen_test.dart | 130 +++++++++++++++--- 3 files changed, 118 insertions(+), 29 deletions(-) diff --git a/lib/src/model/game/game_controller.dart b/lib/src/model/game/game_controller.dart index e201c2dca5..59c87a10f9 100644 --- a/lib/src/model/game/game_controller.dart +++ b/lib/src/model/game/game_controller.dart @@ -654,16 +654,17 @@ class GameController extends _$GameController { } if (data.clock != null) { - final lagCompensation = - newState.game.playable && newState.game.isMyTurn - // server will send the lag only if it's more than 10ms - ? Duration.zero - : data.clock?.lag ?? const Duration(milliseconds: 10); + final lag = newState.game.playable && newState.game.isMyTurn + // my own clock doesn't need to be compensated for + ? Duration.zero + // server will send the lag only if it's more than 10ms + // default lag of 10ms is also used by web client + : data.clock?.lag ?? const Duration(milliseconds: 10); _updateClock( white: data.clock!.white, black: data.clock!.black, - lag: lagCompensation, + lag: lag, activeSide: newState.activeClockSide, ); diff --git a/test/model/game/game_socket_example_data.dart b/test/model/game/game_socket_example_data.dart index 902668c14e..f4f3cc4c95 100644 --- a/test/model/game/game_socket_example_data.dart +++ b/test/model/game/game_socket_example_data.dart @@ -5,6 +5,7 @@ typedef FullEventTestClock = ({ bool running, Duration initial, Duration increment, + Duration? emerg, Duration white, Duration black, }); @@ -20,6 +21,7 @@ String makeFullEvent( running: false, initial: Duration(minutes: 3), increment: Duration(seconds: 2), + emerg: Duration(seconds: 30), white: Duration(minutes: 3), black: Duration(minutes: 3), ), @@ -73,7 +75,7 @@ String makeFullEvent( "increment": ${clock.increment.inSeconds}, "white": ${(clock.white.inMilliseconds / 1000).toStringAsFixed(2)}, "black": ${(clock.black.inMilliseconds / 1000).toStringAsFixed(2)}, - "emerg": 60, + "emerg": 30, "moretime": 15 }, "expiration": { diff --git a/test/view/game/game_screen_test.dart b/test/view/game/game_screen_test.dart index b0b49321d1..8fd416e44b 100644 --- a/test/view/game/game_screen_test.dart +++ b/test/view/game/game_screen_test.dart @@ -1,12 +1,15 @@ import 'package:chessground/chessground.dart'; import 'package:dartchess/dartchess.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/network/socket.dart'; import 'package:lichess_mobile/src/view/game/game_screen.dart'; import 'package:lichess_mobile/src/widgets/countdown_clock.dart'; +import 'package:mocktail/mocktail.dart'; import '../../model/game/game_socket_example_data.dart'; import '../../network/fake_websocket_channel.dart'; @@ -17,6 +20,8 @@ final client = MockClient((request) { return mockResponse('', 404); }); +class MockSoundService extends Mock implements SoundService {} + void main() { group('Loading', () { testWidgets('a game directly with initialGameId', @@ -125,6 +130,7 @@ void main() { increment: Duration(seconds: 2), white: Duration(minutes: 2, seconds: 58), black: Duration(minutes: 2, seconds: 54), + emerg: Duration(seconds: 30), ), ); expect( @@ -152,6 +158,7 @@ void main() { increment: Duration(seconds: 2), white: Duration(minutes: 2, seconds: 58), black: Duration(minutes: 3), + emerg: Duration(seconds: 30), ), ); expect(tester.widgetList(find.byType(Clock)).last.active, true); @@ -171,7 +178,6 @@ void main() { ); expect(findClockWithTime('3:00'), findsOneWidget); // simulates a long lag just to show the clock is not running yet - // in such case the server would send a "lag" field but we'll ignore it in that test await tester.pump(const Duration(milliseconds: 200)); expect(findClockWithTime('3:00'), findsOneWidget); // server ack having the white clock updated with the increment @@ -194,6 +200,79 @@ void main() { expect(findClockWithTime('2:56'), findsOneWidget); }); + testWidgets('compensates opponent lag', (WidgetTester tester) async { + final fakeSocket = FakeWebSocketChannel(); + int socketVersion = 0; + await createTestGame( + fakeSocket, + tester, + pgn: 'e4 e5 Nf3 Nc6', + clock: const ( + running: true, + initial: Duration(minutes: 1), + increment: Duration.zero, + white: Duration(seconds: 58), + black: Duration(seconds: 54), + emerg: Duration(seconds: 10), + ), + socketVersion: socketVersion, + ); + await tester.pump(const Duration(seconds: 3)); + await playMoveWithServerAck( + fakeSocket, + tester, + 'f1', + 'c4', + ply: 5, + san: 'Bc4', + clockAck: ( + white: const Duration(seconds: 55), + black: const Duration(seconds: 54), + lag: const Duration(milliseconds: 250), + ), + socketVersion: ++socketVersion, + ); + // black clock is active + expect(tester.widgetList(find.byType(Clock)).first.active, true); + expect(findClockWithTime('0:54'), findsOneWidget); + await tester.pump(const Duration(milliseconds: 250)); + // lag is 250ms, so clock will only start after that delay + expect(findClockWithTime('0:54'), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + expect(findClockWithTime('0:53'), findsOneWidget); + await tester.pump(const Duration(seconds: 1)); + expect(findClockWithTime('0:52'), findsOneWidget); + }); + + testWidgets('onEmergency', (WidgetTester tester) async { + final mockSoundService = MockSoundService(); + when(() => mockSoundService.play(Sound.lowTime)).thenAnswer((_) async {}); + final fakeSocket = FakeWebSocketChannel(); + await createTestGame( + fakeSocket, + tester, + pgn: 'e4 e5', + clock: const ( + running: true, + initial: Duration(minutes: 3), + increment: Duration(seconds: 2), + white: Duration(seconds: 40), + black: Duration(minutes: 3), + emerg: Duration(seconds: 30), + ), + overrides: [ + soundServiceProvider.overrideWith((_) => mockSoundService), + ], + ); + expect( + tester.widget(findClockWithTime('0:40')).emergencyThreshold, + const Duration(seconds: 30), + ); + await tester.pump(const Duration(seconds: 10)); + expect(findClockWithTime('0:30'), findsOneWidget); + verify(() => mockSoundService.play(Sound.lowTime)).called(1); + }); + testWidgets('flags', (WidgetTester tester) async { final fakeSocket = FakeWebSocketChannel(); await createTestGame( @@ -206,6 +285,7 @@ void main() { increment: Duration(seconds: 2), white: Duration(minutes: 2, seconds: 58), black: Duration(minutes: 2, seconds: 54), + emerg: Duration(seconds: 30), ), ); expect( @@ -269,27 +349,30 @@ Finder findClockWithTime(String text, {bool skipOffstage = true}) { ); } -// /// Simulates playing a move and getting the ack from the server after [elapsedTime]. -// Future playMoveWithServerAck( -// FakeWebSocketChannel socket, -// WidgetTester tester, -// String from, -// String to, { -// required String san, -// required ({Duration white, Duration black}) clockAck, -// required int socketVersion, -// required int ply, -// Duration elapsedTime = const Duration(milliseconds: 10), -// Rect? rect, -// Side orientation = Side.white, -// }) async { -// await playMove(tester, from, to, boardRect: rect, orientation: orientation); -// final uci = '$from$to'; -// socket.addIncomingMessages([ -// '{"t": "move", "v": $socketVersion, "d": {"ply": $ply, "uci": "$uci", "san": "$san", "clock": {"white": 180, "black": 180}}}', -// ]); -// await tester.pump(elapsedTime); -// } +/// Simulates playing a move and getting the ack from the server after [elapsedTime]. +Future playMoveWithServerAck( + FakeWebSocketChannel socket, + WidgetTester tester, + String from, + String to, { + required String san, + required ({Duration white, Duration black, Duration? lag}) clockAck, + required int socketVersion, + required int ply, + Duration elapsedTime = const Duration(milliseconds: 10), + Side orientation = Side.white, +}) async { + await playMove(tester, from, to, orientation: orientation); + final uci = '$from$to'; + final lagStr = clockAck.lag != null + ? ', "lag": ${(clockAck.lag!.inMilliseconds / 10).round()}' + : ''; + await tester.pump(elapsedTime - const Duration(milliseconds: 1)); + socket.addIncomingMessages([ + '{"t": "move", "v": $socketVersion, "d": {"ply": $ply, "uci": "$uci", "san": "$san", "clock": {"white": ${(clockAck.white.inMilliseconds / 1000).toStringAsFixed(2)}, "black": ${(clockAck.black.inMilliseconds / 1000).toStringAsFixed(2)}$lagStr}}}', + ]); + await tester.pump(const Duration(milliseconds: 1)); +} /// Convenient function to start a new test game Future createTestGame( @@ -304,7 +387,9 @@ Future createTestGame( increment: Duration(seconds: 2), white: Duration(minutes: 3), black: Duration(minutes: 3), + emerg: Duration(seconds: 30), ), + List? overrides, }) async { final app = await makeTestProviderScopeApp( tester, @@ -316,6 +401,7 @@ Future createTestGame( webSocketChannelFactoryProvider.overrideWith((ref) { return FakeWebSocketChannelFactory((_) => socket); }), + ...?overrides, ], ); await tester.pumpWidget(app); From 94906cdc6f5318a8393ea2419d71261735be5085 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 14 Nov 2024 19:14:45 +0100 Subject: [PATCH 670/979] Restore countdown clock delay --- lib/src/view/watch/tv_screen.dart | 4 +++ lib/src/widgets/countdown_clock.dart | 26 ++++++++++++-- test/widgets/countdown_clock_test.dart | 48 ++++++++++++++++++++++++-- 3 files changed, 72 insertions(+), 6 deletions(-) diff --git a/lib/src/view/watch/tv_screen.dart b/lib/src/view/watch/tv_screen.dart index 3bfb05846e..daaf61ba66 100644 --- a/lib/src/view/watch/tv_screen.dart +++ b/lib/src/view/watch/tv_screen.dart @@ -95,6 +95,8 @@ class _Body extends ConsumerWidget { ? CountdownClock( key: blackClockKey, timeLeft: gameState.game.clock!.black, + delay: gameState.game.clock!.lag ?? + const Duration(milliseconds: 10), clockUpdatedAt: gameState.game.clock!.at, active: gameState.activeClockSide == Side.black, ) @@ -108,6 +110,8 @@ class _Body extends ConsumerWidget { key: whiteClockKey, timeLeft: gameState.game.clock!.white, clockUpdatedAt: gameState.game.clock!.at, + delay: gameState.game.clock!.lag ?? + const Duration(milliseconds: 10), active: gameState.activeClockSide == Side.white, ) : null, diff --git a/lib/src/widgets/countdown_clock.dart b/lib/src/widgets/countdown_clock.dart index 8411ea1a8a..77ff45c2e7 100644 --- a/lib/src/widgets/countdown_clock.dart +++ b/lib/src/widgets/countdown_clock.dart @@ -11,6 +11,7 @@ import 'package:lichess_mobile/src/utils/screen.dart'; class CountdownClock extends StatefulWidget { const CountdownClock({ required this.timeLeft, + this.delay, this.emergencyThreshold, this.clockUpdatedAt, required this.active, @@ -22,6 +23,11 @@ class CountdownClock extends StatefulWidget { /// The duration left on the clock. final Duration timeLeft; + /// The delay before the clock starts counting down. + /// + /// This can be used to implement lag compensation. + final Duration? delay; + /// If [timeLeft] is less than [emergencyThreshold], the clock will set /// its background color to [ClockStyle.emergencyBackgroundColor]. final Duration? emergencyThreshold; @@ -50,20 +56,31 @@ const _showTenthsThreshold = Duration(seconds: 10); class _CountdownClockState extends State { Timer? _timer; + Timer? _delayTimer; Duration timeLeft = Duration.zero; final _stopwatch = clock.stopwatch(); void startClock() { final now = clock.now(); + final delay = widget.delay ?? Duration.zero; final clockUpdatedAt = widget.clockUpdatedAt ?? now; // UI lag diff: the elapsed time between the time the clock should have started // and the time the clock is actually started final uiLag = now.difference(clockUpdatedAt); + final realDelay = delay - uiLag; - timeLeft = timeLeft - uiLag; + // real delay is negative, we need to adjust the timeLeft. + if (realDelay < Duration.zero) { + timeLeft = timeLeft + realDelay; + } - _scheduleTick(); + if (realDelay > Duration.zero) { + _delayTimer?.cancel(); + _delayTimer = Timer(realDelay, _scheduleTick); + } else { + _scheduleTick(); + } } void _scheduleTick() { @@ -74,8 +91,9 @@ class _CountdownClockState extends State { } void _tick() { + final newTimeLeft = timeLeft - _stopwatch.elapsed; setState(() { - timeLeft = timeLeft - _stopwatch.elapsed; + timeLeft = newTimeLeft; if (timeLeft <= Duration.zero) { timeLeft = Duration.zero; } @@ -86,6 +104,7 @@ class _CountdownClockState extends State { } void stopClock() { + _delayTimer?.cancel(); _timer?.cancel(); _stopwatch.stop(); } @@ -119,6 +138,7 @@ class _CountdownClockState extends State { @override void dispose() { super.dispose(); + _delayTimer?.cancel(); _timer?.cancel(); } diff --git a/test/widgets/countdown_clock_test.dart b/test/widgets/countdown_clock_test.dart index 4669f4ae74..e3599aa2d3 100644 --- a/test/widgets/countdown_clock_test.dart +++ b/test/widgets/countdown_clock_test.dart @@ -170,7 +170,48 @@ void main() { expect(find.text('0:09.7', findRichText: true), findsOneWidget); }); - testWidgets('UI lag compensation', (WidgetTester tester) async { + testWidgets('starts with a delay if set', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: CountdownClock( + timeLeft: Duration(seconds: 10), + active: true, + delay: Duration(milliseconds: 250), + ), + ), + ); + expect(find.text('0:10', findRichText: true), findsOneWidget); + await tester.pump(const Duration(milliseconds: 250)); + expect(find.text('0:10', findRichText: true), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:09.9', findRichText: true), findsOneWidget); + }); + + testWidgets('compensates for UI lag', (WidgetTester tester) async { + final now = clock.now(); + await tester.pump(const Duration(milliseconds: 100)); + + await tester.pumpWidget( + MaterialApp( + home: CountdownClock( + timeLeft: const Duration(seconds: 10), + active: true, + delay: const Duration(milliseconds: 200), + clockUpdatedAt: now, + ), + ), + ); + expect(find.text('0:10', findRichText: true), findsOneWidget); + + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:10', findRichText: true), findsOneWidget); + + // delay was 200ms but UI lagged 100ms so with the compensation the clock has started already + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:09.9', findRichText: true), findsOneWidget); + }); + + testWidgets('UI lag negative start delay', (WidgetTester tester) async { final now = clock.now(); await tester.pump(const Duration(milliseconds: 200)); @@ -179,11 +220,12 @@ void main() { home: CountdownClock( timeLeft: const Duration(seconds: 10), active: true, + delay: const Duration(milliseconds: 100), clockUpdatedAt: now, ), ), ); - // UI lagged 200ms so the clock time is already 200ms ahead - expect(find.text('0:09.8', findRichText: true), findsOneWidget); + // delay was 100ms but UI lagged 200ms so the clock time is already 100ms ahead + expect(find.text('0:09.9', findRichText: true), findsOneWidget); }); } From 18741ad8dd62b6d4709795d0cf98f25bd8471375 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 14 Nov 2024 19:30:40 +0100 Subject: [PATCH 671/979] Make socket client final --- lib/src/model/game/game_controller.dart | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/src/model/game/game_controller.dart b/lib/src/model/game/game_controller.dart index 59c87a10f9..8dd2699f03 100644 --- a/lib/src/model/game/game_controller.dart +++ b/lib/src/model/game/game_controller.dart @@ -64,13 +64,12 @@ class GameController extends _$GameController { /// Last socket version received int? _socketEventVersion; - ChessClock? _clock; - - late SocketClient _socketClient; - static Uri gameSocketUri(GameFullId gameFullId) => Uri(path: '/play/$gameFullId/v6'); + ChessClock? _clock; + late final SocketClient _socketClient; + @override Future build(GameFullId gameFullId) { final socketPool = ref.watch(socketPoolProvider); From 3a38d17d517ca29c3b98a0090dd03cb9e1f9837d Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 14 Nov 2024 19:34:08 +0100 Subject: [PATCH 672/979] Handle tv end clock event --- lib/src/model/tv/tv_controller.dart | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/lib/src/model/tv/tv_controller.dart b/lib/src/model/tv/tv_controller.dart index 330533c192..c0c1eca164 100644 --- a/lib/src/model/tv/tv_controller.dart +++ b/lib/src/model/tv/tv_controller.dart @@ -233,14 +233,19 @@ class TvController extends _$TvController { case 'endData': final endData = GameEndEvent.fromJson(event.data as Map); - state = AsyncData( - state.requireValue.copyWith( - game: state.requireValue.game.copyWith( - status: endData.status, - winner: endData.winner, - ), + TvState newState = state.requireValue.copyWith( + game: state.requireValue.game.copyWith( + status: endData.status, + winner: endData.winner, ), ); + if (endData.clock != null) { + newState = newState.copyWith.game.clock!( + white: endData.clock!.white, + black: endData.clock!.black, + ); + } + state = AsyncData(newState); case 'tvSelect': final json = event.data as Map; From 670328fc5a8483c1ebff3290e7b8f9351611a880 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 14 Nov 2024 19:40:18 +0100 Subject: [PATCH 673/979] Remove todo --- lib/src/model/game/game_controller.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/model/game/game_controller.dart b/lib/src/model/game/game_controller.dart index 8dd2699f03..ccdde05113 100644 --- a/lib/src/model/game/game_controller.dart +++ b/lib/src/model/game/game_controller.dart @@ -791,7 +791,7 @@ class GameController extends _$GameController { if (side != null && newClock != null) { _clock?.setTime(side, newClock); - // TODO: remove + // sync game clock object even if it's not used to display the clock final newState = side == Side.white ? curState.copyWith.game.clock!( white: newClock, From 7c47cf39a64d929efafea339ba9698e6c3e5aced Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 14 Nov 2024 19:58:24 +0100 Subject: [PATCH 674/979] Fix tv endgame clock update --- lib/src/model/tv/tv_controller.dart | 2 ++ lib/src/widgets/countdown_clock.dart | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/src/model/tv/tv_controller.dart b/lib/src/model/tv/tv_controller.dart index c0c1eca164..78b6eeacc4 100644 --- a/lib/src/model/tv/tv_controller.dart +++ b/lib/src/model/tv/tv_controller.dart @@ -243,6 +243,8 @@ class TvController extends _$TvController { newState = newState.copyWith.game.clock!( white: endData.clock!.white, black: endData.clock!.black, + at: DateTime.now(), + lag: null, ); } state = AsyncData(newState); diff --git a/lib/src/widgets/countdown_clock.dart b/lib/src/widgets/countdown_clock.dart index 77ff45c2e7..6d4270c96e 100644 --- a/lib/src/widgets/countdown_clock.dart +++ b/lib/src/widgets/countdown_clock.dart @@ -72,7 +72,8 @@ class _CountdownClockState extends State { // real delay is negative, we need to adjust the timeLeft. if (realDelay < Duration.zero) { - timeLeft = timeLeft + realDelay; + final newTimeLeft = timeLeft + realDelay; + timeLeft = newTimeLeft > Duration.zero ? newTimeLeft : Duration.zero; } if (realDelay > Duration.zero) { From 56de2b5ab5096be54332d2d89a3649fddd0264b6 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 15 Nov 2024 11:44:07 +0100 Subject: [PATCH 675/979] Add a game screen test to check loading a seek --- test/view/game/game_screen_test.dart | 78 +++++++++++++++++++++++++++- 1 file changed, 76 insertions(+), 2 deletions(-) diff --git a/test/view/game/game_screen_test.dart b/test/view/game/game_screen_test.dart index 8fd416e44b..a0adade159 100644 --- a/test/view/game/game_screen_test.dart +++ b/test/view/game/game_screen_test.dart @@ -5,9 +5,11 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; +import 'package:lichess_mobile/src/model/lobby/game_seek.dart'; import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/network/socket.dart'; import 'package:lichess_mobile/src/view/game/game_screen.dart'; +import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/countdown_clock.dart'; import 'package:mocktail/mocktail.dart'; @@ -17,6 +19,9 @@ import '../../test_helpers.dart'; import '../../test_provider_scope.dart'; final client = MockClient((request) { + if (request.url.path == '/api/board/seek') { + return mockResponse('ok', 200); + } return mockResponse('', 404); }); @@ -47,8 +52,7 @@ void main() { expect(find.byType(Chessboard), findsOneWidget); expect(find.byType(PieceWidget), findsNothing); - // now loads the game controller - // screen doesn't have changed yet + // now the game controller is loading and screen doesn't have changed yet await tester.pump(const Duration(milliseconds: 10)); expect(find.byType(Chessboard), findsOneWidget); expect(find.byType(PieceWidget), findsNothing); @@ -70,6 +74,76 @@ void main() { expect(find.text('Peter'), findsOneWidget); expect(find.text('Steven'), findsOneWidget); }); + + testWidgets('a game from the pool with a seek', + (WidgetTester tester) async { + final fakeLobbySocket = FakeWebSocketChannel(); + final fakeGameSocket = FakeWebSocketChannel(); + + final app = await makeTestProviderScopeApp( + tester, + home: const GameScreen( + seek: GameSeek( + clock: (Duration(minutes: 3), Duration(seconds: 2)), + rated: true, + ), + ), + overrides: [ + lichessClientProvider + .overrideWith((ref) => LichessClient(client, ref)), + webSocketChannelFactoryProvider.overrideWith((ref) { + return FakeWebSocketChannelFactory( + (String url) => + url.contains('lobby') ? fakeLobbySocket : fakeGameSocket, + ); + }), + ], + ); + await tester.pumpWidget(app); + + expect(find.byType(Chessboard), findsOneWidget); + expect(find.byType(PieceWidget), findsNothing); + expect(find.text('Waiting for opponent to join...'), findsOneWidget); + expect(find.text('3+2'), findsOneWidget); + expect(find.widgetWithText(BottomBarButton, 'Cancel'), findsOneWidget); + + // waiting for the game + await tester.pump(const Duration(seconds: 2)); + + // when a seek is accepted, server sends a 'redirect' message with game id + fakeLobbySocket.addIncomingMessages([ + '{"t": "redirect", "d": {"id": "qVChCOTcHSeW" }, "v": 1}', + ]); + await tester.pump(const Duration(milliseconds: 1)); + + // now the game controller is loading + expect(find.byType(Chessboard), findsOneWidget); + expect(find.byType(PieceWidget), findsNothing); + expect(find.text('Waiting for opponent to join...'), findsNothing); + expect(find.text('3+2'), findsNothing); + expect(find.widgetWithText(BottomBarButton, 'Cancel'), findsNothing); + + await fakeGameSocket.connectionEstablished; + // now that game socket is open, lobby socket should be closed + expect(fakeLobbySocket.closeCode, isNotNull); + + fakeGameSocket.addIncomingMessages([ + makeFullEvent( + const GameId('qVChCOTc'), + '', + whiteUserName: 'Peter', + blackUserName: 'Steven', + ), + ]); + // wait for socket message + await tester.pump(const Duration(milliseconds: 10)); + + expect(find.byType(PieceWidget), findsNWidgets(32)); + expect(find.text('Peter'), findsOneWidget); + expect(find.text('Steven'), findsOneWidget); + expect(find.text('Waiting for opponent to join...'), findsNothing); + expect(find.text('3+2'), findsNothing); + }); }); group('Clock', () { From 64f0673b8c278d499b5963a8fc7b15b19899f017 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 15 Nov 2024 12:48:39 +0100 Subject: [PATCH 676/979] Refactor clock widgets --- lib/src/view/clock/clock_tool_screen.dart | 2 +- lib/src/view/game/archived_game_screen.dart | 16 +- .../game/correspondence_clock_widget.dart | 2 +- lib/src/view/game/game_body.dart | 54 ++-- .../over_the_board/over_the_board_screen.dart | 2 +- lib/src/view/watch/tv_screen.dart | 12 +- .../{countdown_clock.dart => clock.dart} | 297 +++++++++--------- test/view/game/game_screen_test.dart | 2 +- .../over_the_board_screen_test.dart | 2 +- test/widgets/clock_test.dart | 249 +++++++++++++++ test/widgets/countdown_clock_test.dart | 231 -------------- 11 files changed, 439 insertions(+), 430 deletions(-) rename lib/src/widgets/{countdown_clock.dart => clock.dart} (90%) create mode 100644 test/widgets/clock_test.dart delete mode 100644 test/widgets/countdown_clock_test.dart diff --git a/lib/src/view/clock/clock_tool_screen.dart b/lib/src/view/clock/clock_tool_screen.dart index 1f6c319adf..b9962a0802 100644 --- a/lib/src/view/clock/clock_tool_screen.dart +++ b/lib/src/view/clock/clock_tool_screen.dart @@ -9,7 +9,7 @@ import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/view/clock/clock_settings.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; -import 'package:lichess_mobile/src/widgets/countdown_clock.dart'; +import 'package:lichess_mobile/src/widgets/clock.dart'; import 'custom_clock_settings.dart'; diff --git a/lib/src/view/game/archived_game_screen.dart b/lib/src/view/game/archived_game_screen.dart index b8371db3d0..ab7c41cec3 100644 --- a/lib/src/view/game/archived_game_screen.dart +++ b/lib/src/view/game/archived_game_screen.dart @@ -21,7 +21,7 @@ import 'package:lichess_mobile/src/widgets/board_table.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; -import 'package:lichess_mobile/src/widgets/countdown_clock.dart'; +import 'package:lichess_mobile/src/widgets/clock.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'archived_game_screen_providers.dart'; @@ -258,23 +258,13 @@ class _BoardBody extends ConsumerWidget { final black = GamePlayer( key: const ValueKey('black-player'), player: gameData.black, - clock: blackClock != null - ? CountdownClock( - timeLeft: blackClock, - active: false, - ) - : null, + clock: blackClock != null ? Clock(timeLeft: blackClock) : null, materialDiff: game.materialDiffAt(cursor, Side.black), ); final white = GamePlayer( key: const ValueKey('white-player'), player: gameData.white, - clock: whiteClock != null - ? CountdownClock( - timeLeft: whiteClock, - active: false, - ) - : null, + clock: whiteClock != null ? Clock(timeLeft: whiteClock) : null, materialDiff: game.materialDiffAt(cursor, Side.white), ); diff --git a/lib/src/view/game/correspondence_clock_widget.dart b/lib/src/view/game/correspondence_clock_widget.dart index 418162624f..73f4e2a076 100644 --- a/lib/src/view/game/correspondence_clock_widget.dart +++ b/lib/src/view/game/correspondence_clock_widget.dart @@ -6,7 +6,7 @@ import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/settings/brightness.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; -import 'package:lichess_mobile/src/widgets/countdown_clock.dart'; +import 'package:lichess_mobile/src/widgets/clock.dart'; class CorrespondenceClock extends ConsumerStatefulWidget { /// The duration left on the clock. diff --git a/lib/src/view/game/game_body.dart b/lib/src/view/game/game_body.dart index 557668ba52..7e24ee4a4b 100644 --- a/lib/src/view/game/game_body.dart +++ b/lib/src/view/game/game_body.dart @@ -26,7 +26,7 @@ import 'package:lichess_mobile/src/widgets/board_table.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; -import 'package:lichess_mobile/src/widgets/countdown_clock.dart'; +import 'package:lichess_mobile/src/widgets/clock.dart'; import 'package:lichess_mobile/src/widgets/platform_alert_dialog.dart'; import 'package:lichess_mobile/src/widgets/user_full_name.dart'; import 'package:lichess_mobile/src/widgets/yes_no_dialog.dart'; @@ -147,18 +147,20 @@ class GameBody extends ConsumerWidget { active: false, ) : gameState.liveClock != null - ? ValueListenableBuilder( - key: blackClockKey, - valueListenable: gameState.liveClock!.black, - builder: (context, value, _) { - return Clock( - timeLeft: value, - active: gameState.activeClockSide == Side.black, - emergencyThreshold: youAre == Side.black - ? gameState.game.meta.clock?.emergency - : null, - ); - }, + ? RepaintBoundary( + child: ValueListenableBuilder( + key: blackClockKey, + valueListenable: gameState.liveClock!.black, + builder: (context, value, _) { + return Clock( + timeLeft: value, + active: gameState.activeClockSide == Side.black, + emergencyThreshold: youAre == Side.black + ? gameState.game.meta.clock?.emergency + : null, + ); + }, + ), ) : gameState.game.correspondenceClock != null ? CorrespondenceClock( @@ -196,18 +198,20 @@ class GameBody extends ConsumerWidget { active: false, ) : gameState.liveClock != null - ? ValueListenableBuilder( - key: whiteClockKey, - valueListenable: gameState.liveClock!.white, - builder: (context, value, _) { - return Clock( - timeLeft: value, - active: gameState.activeClockSide == Side.white, - emergencyThreshold: youAre == Side.white - ? gameState.game.meta.clock?.emergency - : null, - ); - }, + ? RepaintBoundary( + child: ValueListenableBuilder( + key: whiteClockKey, + valueListenable: gameState.liveClock!.white, + builder: (context, value, _) { + return Clock( + timeLeft: value, + active: gameState.activeClockSide == Side.white, + emergencyThreshold: youAre == Side.white + ? gameState.game.meta.clock?.emergency + : null, + ); + }, + ), ) : gameState.game.correspondenceClock != null ? CorrespondenceClock( diff --git a/lib/src/view/over_the_board/over_the_board_screen.dart b/lib/src/view/over_the_board/over_the_board_screen.dart index c1a7ecaaec..817d236372 100644 --- a/lib/src/view/over_the_board/over_the_board_screen.dart +++ b/lib/src/view/over_the_board/over_the_board_screen.dart @@ -25,7 +25,7 @@ import 'package:lichess_mobile/src/widgets/board_table.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; -import 'package:lichess_mobile/src/widgets/countdown_clock.dart'; +import 'package:lichess_mobile/src/widgets/clock.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; class OverTheBoardScreen extends StatelessWidget { diff --git a/lib/src/view/watch/tv_screen.dart b/lib/src/view/watch/tv_screen.dart index daaf61ba66..fe41554aae 100644 --- a/lib/src/view/watch/tv_screen.dart +++ b/lib/src/view/watch/tv_screen.dart @@ -13,7 +13,7 @@ import 'package:lichess_mobile/src/widgets/board_table.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; -import 'package:lichess_mobile/src/widgets/countdown_clock.dart'; +import 'package:lichess_mobile/src/widgets/clock.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; class TvScreen extends ConsumerStatefulWidget { @@ -92,13 +92,16 @@ class _Body extends ConsumerWidget { final blackPlayerWidget = GamePlayer( player: game.black.setOnGame(true), clock: gameState.game.clock != null - ? CountdownClock( + ? CountdownClockBuilder( key: blackClockKey, timeLeft: gameState.game.clock!.black, delay: gameState.game.clock!.lag ?? const Duration(milliseconds: 10), clockUpdatedAt: gameState.game.clock!.at, active: gameState.activeClockSide == Side.black, + builder: (context, timeLeft, active) { + return Clock(timeLeft: timeLeft, active: active); + }, ) : null, materialDiff: game.lastMaterialDiffAt(Side.black), @@ -106,13 +109,16 @@ class _Body extends ConsumerWidget { final whitePlayerWidget = GamePlayer( player: game.white.setOnGame(true), clock: gameState.game.clock != null - ? CountdownClock( + ? CountdownClockBuilder( key: whiteClockKey, timeLeft: gameState.game.clock!.white, clockUpdatedAt: gameState.game.clock!.at, delay: gameState.game.clock!.lag ?? const Duration(milliseconds: 10), active: gameState.activeClockSide == Side.white, + builder: (context, timeLeft, active) { + return Clock(timeLeft: timeLeft, active: active); + }, ) : null, materialDiff: game.lastMaterialDiffAt(Side.white), diff --git a/lib/src/widgets/countdown_clock.dart b/lib/src/widgets/clock.dart similarity index 90% rename from lib/src/widgets/countdown_clock.dart rename to lib/src/widgets/clock.dart index 6d4270c96e..e3b70f9bcb 100644 --- a/lib/src/widgets/countdown_clock.dart +++ b/lib/src/widgets/clock.dart @@ -5,158 +5,6 @@ import 'package:flutter/material.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; -/// A countdown clock. -/// -/// The clock starts only when [active] is `true`. -class CountdownClock extends StatefulWidget { - const CountdownClock({ - required this.timeLeft, - this.delay, - this.emergencyThreshold, - this.clockUpdatedAt, - required this.active, - this.clockStyle, - this.padLeft = false, - super.key, - }); - - /// The duration left on the clock. - final Duration timeLeft; - - /// The delay before the clock starts counting down. - /// - /// This can be used to implement lag compensation. - final Duration? delay; - - /// If [timeLeft] is less than [emergencyThreshold], the clock will set - /// its background color to [ClockStyle.emergencyBackgroundColor]. - final Duration? emergencyThreshold; - - /// The time at which the clock was updated. - /// - /// Use this parameter to synchronize the clock with the time at which the clock - /// event was received from the server and to compensate for UI lag. - final DateTime? clockUpdatedAt; - - /// If [active] is `true`, the clock starts counting down. - final bool active; - - /// Custom color style - final ClockStyle? clockStyle; - - /// Whether to pad with a leading zero (default is `false`). - final bool padLeft; - - @override - State createState() => _CountdownClockState(); -} - -const _tickDelay = Duration(milliseconds: 100); -const _showTenthsThreshold = Duration(seconds: 10); - -class _CountdownClockState extends State { - Timer? _timer; - Timer? _delayTimer; - Duration timeLeft = Duration.zero; - - final _stopwatch = clock.stopwatch(); - - void startClock() { - final now = clock.now(); - final delay = widget.delay ?? Duration.zero; - final clockUpdatedAt = widget.clockUpdatedAt ?? now; - // UI lag diff: the elapsed time between the time the clock should have started - // and the time the clock is actually started - final uiLag = now.difference(clockUpdatedAt); - final realDelay = delay - uiLag; - - // real delay is negative, we need to adjust the timeLeft. - if (realDelay < Duration.zero) { - final newTimeLeft = timeLeft + realDelay; - timeLeft = newTimeLeft > Duration.zero ? newTimeLeft : Duration.zero; - } - - if (realDelay > Duration.zero) { - _delayTimer?.cancel(); - _delayTimer = Timer(realDelay, _scheduleTick); - } else { - _scheduleTick(); - } - } - - void _scheduleTick() { - _timer?.cancel(); - _timer = Timer(_tickDelay, _tick); - _stopwatch.reset(); - _stopwatch.start(); - } - - void _tick() { - final newTimeLeft = timeLeft - _stopwatch.elapsed; - setState(() { - timeLeft = newTimeLeft; - if (timeLeft <= Duration.zero) { - timeLeft = Duration.zero; - } - }); - if (timeLeft > Duration.zero) { - _scheduleTick(); - } - } - - void stopClock() { - _delayTimer?.cancel(); - _timer?.cancel(); - _stopwatch.stop(); - } - - @override - void initState() { - super.initState(); - timeLeft = widget.timeLeft; - if (widget.active) { - startClock(); - } - } - - @override - void didUpdateWidget(CountdownClock oldClock) { - super.didUpdateWidget(oldClock); - - if (widget.clockUpdatedAt != oldClock.clockUpdatedAt) { - timeLeft = widget.timeLeft; - } - - if (widget.active != oldClock.active) { - if (widget.active) { - startClock(); - } else { - stopClock(); - } - } - } - - @override - void dispose() { - super.dispose(); - _delayTimer?.cancel(); - _timer?.cancel(); - } - - @override - Widget build(BuildContext context) { - return RepaintBoundary( - child: Clock( - padLeft: widget.padLeft, - emergencyThreshold: widget.emergencyThreshold, - timeLeft: timeLeft, - active: widget.active, - clockStyle: widget.clockStyle, - ), - ); - } -} - const _kClockFontSize = 26.0; const _kClockTenthFontSize = 20.0; const _kClockHundredsFontSize = 18.0; @@ -167,7 +15,7 @@ const _kClockHundredsFontSize = 18.0; class Clock extends StatelessWidget { const Clock({ required this.timeLeft, - required this.active, + this.active = false, this.clockStyle, this.emergencyThreshold, this.padLeft = false, @@ -313,3 +161,146 @@ class ClockStyle { emergencyBackgroundColor: Color(0xFFF2CCCC), ); } + +typedef ClockWidgetBuilder = Widget Function( + BuildContext context, + Duration timeLeft, + bool isActive, +); + +/// A widget that automatically starts a countdown. +/// +/// The clock starts only when [active] is `true`. +class CountdownClockBuilder extends StatefulWidget { + const CountdownClockBuilder({ + required this.timeLeft, + this.delay, + this.clockUpdatedAt, + required this.active, + required this.builder, + super.key, + }); + + /// The duration left on the clock. + final Duration timeLeft; + + /// The delay before the clock starts counting down. + /// + /// This can be used to implement lag compensation. + final Duration? delay; + + /// The time at which the clock was updated. + /// + /// Use this parameter to synchronize the clock with the time at which the clock + /// event was received from the server and to compensate for UI lag. + final DateTime? clockUpdatedAt; + + /// If [active] is `true`, the clock starts counting down. + final bool active; + + /// A [ClockWidgetBuilder] that builds the clock on each tick with the new [timeLeft] value. + final ClockWidgetBuilder builder; + + @override + State createState() => _CountdownClockState(); +} + +const _tickDelay = Duration(milliseconds: 100); +const _showTenthsThreshold = Duration(seconds: 10); + +class _CountdownClockState extends State { + Timer? _timer; + Timer? _delayTimer; + Duration timeLeft = Duration.zero; + + final _stopwatch = clock.stopwatch(); + + void startClock() { + final now = clock.now(); + final delay = widget.delay ?? Duration.zero; + final clockUpdatedAt = widget.clockUpdatedAt ?? now; + // UI lag diff: the elapsed time between the time the clock should have started + // and the time the clock is actually started + final uiLag = now.difference(clockUpdatedAt); + final realDelay = delay - uiLag; + + // real delay is negative, we need to adjust the timeLeft. + if (realDelay < Duration.zero) { + final newTimeLeft = timeLeft + realDelay; + timeLeft = newTimeLeft > Duration.zero ? newTimeLeft : Duration.zero; + } + + if (realDelay > Duration.zero) { + _delayTimer?.cancel(); + _delayTimer = Timer(realDelay, _scheduleTick); + } else { + _scheduleTick(); + } + } + + void _scheduleTick() { + _timer?.cancel(); + _timer = Timer(_tickDelay, _tick); + _stopwatch.reset(); + _stopwatch.start(); + } + + void _tick() { + final newTimeLeft = timeLeft - _stopwatch.elapsed; + setState(() { + timeLeft = newTimeLeft; + if (timeLeft <= Duration.zero) { + timeLeft = Duration.zero; + } + }); + if (timeLeft > Duration.zero) { + _scheduleTick(); + } + } + + void stopClock() { + _delayTimer?.cancel(); + _timer?.cancel(); + _stopwatch.stop(); + } + + @override + void initState() { + super.initState(); + timeLeft = widget.timeLeft; + if (widget.active) { + startClock(); + } + } + + @override + void didUpdateWidget(CountdownClockBuilder oldClock) { + super.didUpdateWidget(oldClock); + + if (widget.clockUpdatedAt != oldClock.clockUpdatedAt) { + timeLeft = widget.timeLeft; + } + + if (widget.active != oldClock.active) { + if (widget.active) { + startClock(); + } else { + stopClock(); + } + } + } + + @override + void dispose() { + super.dispose(); + _delayTimer?.cancel(); + _timer?.cancel(); + } + + @override + Widget build(BuildContext context) { + return RepaintBoundary( + child: widget.builder(context, timeLeft, widget.active), + ); + } +} diff --git a/test/view/game/game_screen_test.dart b/test/view/game/game_screen_test.dart index a0adade159..18d2c45d88 100644 --- a/test/view/game/game_screen_test.dart +++ b/test/view/game/game_screen_test.dart @@ -10,7 +10,7 @@ import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/network/socket.dart'; import 'package:lichess_mobile/src/view/game/game_screen.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; -import 'package:lichess_mobile/src/widgets/countdown_clock.dart'; +import 'package:lichess_mobile/src/widgets/clock.dart'; import 'package:mocktail/mocktail.dart'; import '../../model/game/game_socket_example_data.dart'; diff --git a/test/view/over_the_board/over_the_board_screen_test.dart b/test/view/over_the_board/over_the_board_screen_test.dart index 5df769e94c..720ccfba09 100644 --- a/test/view/over_the_board/over_the_board_screen_test.dart +++ b/test/view/over_the_board/over_the_board_screen_test.dart @@ -9,7 +9,7 @@ import 'package:lichess_mobile/src/model/common/time_increment.dart'; import 'package:lichess_mobile/src/model/over_the_board/over_the_board_clock.dart'; import 'package:lichess_mobile/src/model/over_the_board/over_the_board_game_controller.dart'; import 'package:lichess_mobile/src/view/over_the_board/over_the_board_screen.dart'; -import 'package:lichess_mobile/src/widgets/countdown_clock.dart'; +import 'package:lichess_mobile/src/widgets/clock.dart'; import '../../test_helpers.dart'; import '../../test_provider_scope.dart'; diff --git a/test/widgets/clock_test.dart b/test/widgets/clock_test.dart new file mode 100644 index 0000000000..f94a315fd5 --- /dev/null +++ b/test/widgets/clock_test.dart @@ -0,0 +1,249 @@ +import 'package:clock/clock.dart' as clock; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:lichess_mobile/src/widgets/clock.dart'; + +void main() { + group('Clock', () { + testWidgets('shows milliseconds when time < 1s and active is false', + (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Clock( + timeLeft: Duration(seconds: 1), + active: true, + ), + ), + ); + + expect(find.text('0:01.0', findRichText: true), findsOneWidget); + + await tester.pumpWidget( + const MaterialApp( + home: Clock( + timeLeft: Duration(milliseconds: 988), + active: false, + ), + ), + duration: const Duration(milliseconds: 1000), + ); + + expect(find.text('0:00.98', findRichText: true), findsOneWidget); + }); + }); + + group('CountdownClockBuilder', () { + Widget clockBuilder(BuildContext context, Duration timeLeft, bool active) { + final mins = timeLeft.inMinutes.remainder(60); + final secs = timeLeft.inSeconds.remainder(60).toString().padLeft(2, '0'); + final tenths = timeLeft.inMilliseconds.remainder(1000) ~/ 100; + return Text('$mins:$secs.$tenths'); + } + + testWidgets('does not tick when not active', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: CountdownClockBuilder( + timeLeft: const Duration(seconds: 10), + active: false, + builder: clockBuilder, + ), + ), + ); + + expect(find.text('0:10.0'), findsOneWidget); + + await tester.pump(const Duration(seconds: 2)); + expect(find.text('0:10.0'), findsOneWidget); + }); + + testWidgets('ticks when active', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: CountdownClockBuilder( + timeLeft: const Duration(seconds: 10), + active: true, + builder: clockBuilder, + ), + ), + ); + + expect(find.text('0:10.0'), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:09.9'), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:09.8'), findsOneWidget); + await tester.pump(const Duration(seconds: 10)); + expect(find.text('0:00.0'), findsOneWidget); + }); + + testWidgets('update time by changing widget configuration', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: CountdownClockBuilder( + timeLeft: const Duration(seconds: 10), + clockUpdatedAt: clock.clock.now(), + active: true, + builder: clockBuilder, + ), + ), + ); + + expect(find.text('0:10.0'), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:09.9'), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:09.8'), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:09.7'), findsOneWidget); + + await tester.pumpWidget( + MaterialApp( + home: CountdownClockBuilder( + timeLeft: const Duration(seconds: 11), + clockUpdatedAt: clock.clock.now(), + active: true, + builder: clockBuilder, + ), + ), + ); + expect(find.text('0:11.0'), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:10.9'), findsOneWidget); + await tester.pump(const Duration(seconds: 11)); + expect(find.text('0:00.0'), findsOneWidget); + }); + + testWidgets('do not update if clockUpdatedAt is same', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: CountdownClockBuilder( + timeLeft: const Duration(seconds: 10), + active: true, + builder: clockBuilder, + ), + ), + ); + + expect(find.text('0:10.0'), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:09.9'), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:09.8'), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:09.7'), findsOneWidget); + + await tester.pumpWidget( + MaterialApp( + home: CountdownClockBuilder( + timeLeft: const Duration(seconds: 11), + active: true, + builder: clockBuilder, + ), + ), + ); + + expect(find.text('0:09.7'), findsOneWidget); + await tester.pump(const Duration(seconds: 10)); + expect(find.text('0:00.0'), findsOneWidget); + }); + + testWidgets('stops when active become false', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: CountdownClockBuilder( + timeLeft: const Duration(seconds: 10), + active: true, + builder: clockBuilder, + ), + ), + ); + + expect(find.text('0:10.0', findRichText: true), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:09.9', findRichText: true), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:09.8', findRichText: true), findsOneWidget); + + // clock is rebuilt with same time but inactive: + // the time is kept and the clock stops counting the elapsed time + await tester.pumpWidget( + MaterialApp( + home: CountdownClockBuilder( + timeLeft: const Duration(seconds: 10), + active: false, + builder: clockBuilder, + ), + ), + duration: const Duration(milliseconds: 100), + ); + expect(find.text('0:09.7', findRichText: true), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:09.7', findRichText: true), findsOneWidget); + }); + + testWidgets('starts with a delay if set', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: CountdownClockBuilder( + timeLeft: const Duration(seconds: 10), + active: true, + delay: const Duration(milliseconds: 250), + builder: clockBuilder, + ), + ), + ); + expect(find.text('0:10.0', findRichText: true), findsOneWidget); + await tester.pump(const Duration(milliseconds: 250)); + expect(find.text('0:10.0', findRichText: true), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:09.9', findRichText: true), findsOneWidget); + }); + + testWidgets('compensates for UI lag', (WidgetTester tester) async { + final now = clock.clock.now(); + await tester.pump(const Duration(milliseconds: 100)); + + await tester.pumpWidget( + MaterialApp( + home: CountdownClockBuilder( + timeLeft: const Duration(seconds: 10), + active: true, + delay: const Duration(milliseconds: 200), + clockUpdatedAt: now, + builder: clockBuilder, + ), + ), + ); + expect(find.text('0:10.0', findRichText: true), findsOneWidget); + + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:10.0', findRichText: true), findsOneWidget); + + // delay was 200ms but UI lagged 100ms so with the compensation the clock has started already + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:09.9', findRichText: true), findsOneWidget); + }); + + testWidgets('UI lag negative start delay', (WidgetTester tester) async { + final now = clock.clock.now(); + await tester.pump(const Duration(milliseconds: 200)); + + await tester.pumpWidget( + MaterialApp( + home: CountdownClockBuilder( + timeLeft: const Duration(seconds: 10), + active: true, + delay: const Duration(milliseconds: 100), + clockUpdatedAt: now, + builder: clockBuilder, + ), + ), + ); + // delay was 100ms but UI lagged 200ms so the clock time is already 100ms ahead + expect(find.text('0:09.9', findRichText: true), findsOneWidget); + }); + }); +} diff --git a/test/widgets/countdown_clock_test.dart b/test/widgets/countdown_clock_test.dart deleted file mode 100644 index e3599aa2d3..0000000000 --- a/test/widgets/countdown_clock_test.dart +++ /dev/null @@ -1,231 +0,0 @@ -import 'package:clock/clock.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:lichess_mobile/src/widgets/countdown_clock.dart'; - -void main() { - testWidgets('does not tick when not active', (WidgetTester tester) async { - await tester.pumpWidget( - const MaterialApp( - home: CountdownClock( - timeLeft: Duration(seconds: 10), - active: false, - ), - ), - ); - - expect(find.text('0:10', findRichText: true), findsOneWidget); - - await tester.pump(const Duration(seconds: 2)); - expect(find.text('0:10', findRichText: true), findsOneWidget); - }); - - testWidgets('ticks when active', (WidgetTester tester) async { - await tester.pumpWidget( - const MaterialApp( - home: CountdownClock( - timeLeft: Duration(seconds: 10), - active: true, - ), - ), - ); - - expect(find.text('0:10', findRichText: true), findsOneWidget); - await tester.pump(const Duration(milliseconds: 100)); - expect(find.text('0:09.9', findRichText: true), findsOneWidget); - await tester.pump(const Duration(milliseconds: 100)); - expect(find.text('0:09.8', findRichText: true), findsOneWidget); - await tester.pump(const Duration(seconds: 10)); - expect(find.text('0:00.0', findRichText: true), findsOneWidget); - }); - - testWidgets('update time by changing widget configuration', - (WidgetTester tester) async { - await tester.pumpWidget( - MaterialApp( - home: CountdownClock( - timeLeft: const Duration(seconds: 10), - clockUpdatedAt: clock.now(), - active: true, - ), - ), - ); - - expect(find.text('0:10', findRichText: true), findsOneWidget); - await tester.pump(const Duration(milliseconds: 100)); - expect(find.text('0:09.9', findRichText: true), findsOneWidget); - await tester.pump(const Duration(milliseconds: 100)); - expect(find.text('0:09.8', findRichText: true), findsOneWidget); - await tester.pump(const Duration(milliseconds: 100)); - expect(find.text('0:09.7', findRichText: true), findsOneWidget); - - await tester.pumpWidget( - MaterialApp( - home: CountdownClock( - timeLeft: const Duration(seconds: 11), - clockUpdatedAt: clock.now(), - active: true, - ), - ), - ); - expect(find.text('0:11', findRichText: true), findsOneWidget); - await tester.pump(const Duration(milliseconds: 100)); - expect(find.text('0:10', findRichText: true), findsOneWidget); - await tester.pump(const Duration(seconds: 11)); - expect(find.text('0:00.0', findRichText: true), findsOneWidget); - }); - - testWidgets('do not update if clockUpdatedAt is same', - (WidgetTester tester) async { - await tester.pumpWidget( - const MaterialApp( - home: CountdownClock( - timeLeft: Duration(seconds: 10), - active: true, - ), - ), - ); - - expect(find.text('0:10', findRichText: true), findsOneWidget); - await tester.pump(const Duration(milliseconds: 100)); - expect(find.text('0:09.9', findRichText: true), findsOneWidget); - await tester.pump(const Duration(milliseconds: 100)); - expect(find.text('0:09.8', findRichText: true), findsOneWidget); - await tester.pump(const Duration(milliseconds: 100)); - expect(find.text('0:09.7', findRichText: true), findsOneWidget); - - await tester.pumpWidget( - const MaterialApp( - home: CountdownClock( - timeLeft: Duration(seconds: 11), - active: true, - ), - ), - ); - - expect(find.text('0:09.7', findRichText: true), findsOneWidget); - await tester.pump(const Duration(seconds: 10)); - expect(find.text('0:00.0', findRichText: true), findsOneWidget); - }); - - testWidgets('shows milliseconds when time < 1s and active is false', - (WidgetTester tester) async { - await tester.pumpWidget( - MaterialApp( - home: CountdownClock( - timeLeft: const Duration(seconds: 1), - clockUpdatedAt: clock.now(), - active: true, - ), - ), - ); - - expect(find.text('0:01.0', findRichText: true), findsOneWidget); - await tester.pump(const Duration(milliseconds: 900)); - expect(find.text('0:00.1', findRichText: true), findsOneWidget); - - await tester.pumpWidget( - MaterialApp( - home: CountdownClock( - timeLeft: const Duration(milliseconds: 988), - clockUpdatedAt: clock.now(), - active: false, - ), - ), - duration: const Duration(milliseconds: 1000), - ); - - expect(find.text('0:00.98', findRichText: true), findsOneWidget); - }); - - testWidgets('stops when active become false', (WidgetTester tester) async { - await tester.pumpWidget( - const MaterialApp( - home: CountdownClock( - timeLeft: Duration(seconds: 10), - active: true, - ), - ), - ); - - expect(find.text('0:10', findRichText: true), findsOneWidget); - await tester.pump(const Duration(milliseconds: 100)); - expect(find.text('0:09.9', findRichText: true), findsOneWidget); - await tester.pump(const Duration(milliseconds: 100)); - expect(find.text('0:09.8', findRichText: true), findsOneWidget); - - // clock is rebuilt with same time but inactive: - // the time is kept and the clock stops counting the elapsed time - await tester.pumpWidget( - const MaterialApp( - home: CountdownClock( - timeLeft: Duration(seconds: 10), - active: false, - ), - ), - duration: const Duration(milliseconds: 100), - ); - expect(find.text('0:09.7', findRichText: true), findsOneWidget); - await tester.pump(const Duration(milliseconds: 100)); - expect(find.text('0:09.7', findRichText: true), findsOneWidget); - }); - - testWidgets('starts with a delay if set', (WidgetTester tester) async { - await tester.pumpWidget( - const MaterialApp( - home: CountdownClock( - timeLeft: Duration(seconds: 10), - active: true, - delay: Duration(milliseconds: 250), - ), - ), - ); - expect(find.text('0:10', findRichText: true), findsOneWidget); - await tester.pump(const Duration(milliseconds: 250)); - expect(find.text('0:10', findRichText: true), findsOneWidget); - await tester.pump(const Duration(milliseconds: 100)); - expect(find.text('0:09.9', findRichText: true), findsOneWidget); - }); - - testWidgets('compensates for UI lag', (WidgetTester tester) async { - final now = clock.now(); - await tester.pump(const Duration(milliseconds: 100)); - - await tester.pumpWidget( - MaterialApp( - home: CountdownClock( - timeLeft: const Duration(seconds: 10), - active: true, - delay: const Duration(milliseconds: 200), - clockUpdatedAt: now, - ), - ), - ); - expect(find.text('0:10', findRichText: true), findsOneWidget); - - await tester.pump(const Duration(milliseconds: 100)); - expect(find.text('0:10', findRichText: true), findsOneWidget); - - // delay was 200ms but UI lagged 100ms so with the compensation the clock has started already - await tester.pump(const Duration(milliseconds: 100)); - expect(find.text('0:09.9', findRichText: true), findsOneWidget); - }); - - testWidgets('UI lag negative start delay', (WidgetTester tester) async { - final now = clock.now(); - await tester.pump(const Duration(milliseconds: 200)); - - await tester.pumpWidget( - MaterialApp( - home: CountdownClock( - timeLeft: const Duration(seconds: 10), - active: true, - delay: const Duration(milliseconds: 100), - clockUpdatedAt: now, - ), - ), - ); - // delay was 100ms but UI lagged 200ms so the clock time is already 100ms ahead - expect(find.text('0:09.9', findRichText: true), findsOneWidget); - }); -} From f6f7f814182d54d6a732ccc612b5bae6d2f0686b Mon Sep 17 00:00:00 2001 From: Jimima Date: Fri, 15 Nov 2024 13:23:02 +0000 Subject: [PATCH 677/979] Removed comments --- lib/src/model/settings/board_preferences.dart | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/lib/src/model/settings/board_preferences.dart b/lib/src/model/settings/board_preferences.dart index 337130c2ae..e71d0d7e44 100644 --- a/lib/src/model/settings/board_preferences.dart +++ b/lib/src/model/settings/board_preferences.dart @@ -81,12 +81,6 @@ class BoardPreferences extends _$BoardPreferences ); } - // Future toggleShowMaterialDifference() { - // return save( - // state.copyWith(showMaterialDifference: !state.showMaterialDifference), - // ); - // } - Future setMaterialDifferenceFormat( MaterialDifference materialDifference, ) { @@ -119,9 +113,7 @@ class BoardPrefs with _$BoardPrefs implements Serializable { required bool boardHighlights, required bool coordinates, required bool pieceAnimation, - // required bool showMaterialDifference, required MaterialDifference materialDifference, - // required MaterialDifferenceFormat materialDifferenceFormat, @JsonKey( defaultValue: PieceShiftMethod.either, unknownEnumValue: PieceShiftMethod.either, @@ -149,8 +141,6 @@ class BoardPrefs with _$BoardPrefs implements Serializable { coordinates: true, pieceAnimation: true, materialDifference: MaterialDifference.materialDifference, - // showMaterialDifference: true, - // materialDifferenceFormat: MaterialDifferenceFormat.difference, pieceShiftMethod: PieceShiftMethod.either, enableShapeDrawings: true, magnifyDraggedPiece: true, From 6a13b395d610f7c761a67026104bb3d17ff47d15 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Fri, 15 Nov 2024 14:30:06 +0100 Subject: [PATCH 678/979] feat: play Sound.lowTime if less than 8 seconds to make first move --- lib/src/view/game/game_player.dart | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/lib/src/view/game/game_player.dart b/lib/src/view/game/game_player.dart index 619f583af3..7456a8cc60 100644 --- a/lib/src/view/game/game_player.dart +++ b/lib/src/view/game/game_player.dart @@ -4,7 +4,9 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:dartchess/dartchess.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/constants.dart'; +import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; import 'package:lichess_mobile/src/model/game/material_diff.dart'; import 'package:lichess_mobile/src/model/game/player.dart'; import 'package:lichess_mobile/src/styles/lichess_colors.dart'; @@ -257,7 +259,7 @@ class ConfirmMove extends StatelessWidget { } } -class MoveExpiration extends StatefulWidget { +class MoveExpiration extends ConsumerStatefulWidget { const MoveExpiration({ required this.timeToMove, required this.mePlaying, @@ -268,13 +270,14 @@ class MoveExpiration extends StatefulWidget { final bool mePlaying; @override - State createState() => _MoveExpirationState(); + ConsumerState createState() => _MoveExpirationState(); } -class _MoveExpirationState extends State { +class _MoveExpirationState extends ConsumerState { static const _period = Duration(milliseconds: 1000); Timer? _timer; Duration timeLeft = Duration.zero; + bool playedEmergencySound = false; Timer startTimer() { return Timer.periodic(_period, (timer) { @@ -312,6 +315,14 @@ class _MoveExpirationState extends State { Widget build(BuildContext context) { final secs = timeLeft.inSeconds.remainder(60); final emerg = timeLeft <= const Duration(seconds: 8); + + if (emerg && widget.mePlaying && !playedEmergencySound) { + ref.read(soundServiceProvider).play(Sound.lowTime); + setState(() { + playedEmergencySound = true; + }); + } + return secs <= 20 ? Text( context.l10n.nbSecondsToPlayTheFirstMove(secs), From 3fe1a77256acaadb3ebcd013a225599fc30e5866 Mon Sep 17 00:00:00 2001 From: Jimima Date: Fri, 15 Nov 2024 13:42:39 +0000 Subject: [PATCH 679/979] Refactored enum --- lib/src/model/settings/board_preferences.dart | 20 +++++++++---------- lib/src/view/game/game_player.dart | 6 +++--- lib/src/view/game/game_settings.dart | 6 +++--- .../view/settings/board_settings_screen.dart | 6 +++--- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/lib/src/model/settings/board_preferences.dart b/lib/src/model/settings/board_preferences.dart index e71d0d7e44..482e2a0e22 100644 --- a/lib/src/model/settings/board_preferences.dart +++ b/lib/src/model/settings/board_preferences.dart @@ -82,7 +82,7 @@ class BoardPreferences extends _$BoardPreferences } Future setMaterialDifferenceFormat( - MaterialDifference materialDifference, + MaterialDifferenceFormat materialDifference, ) { return save( state.copyWith(materialDifference: materialDifference), @@ -113,7 +113,7 @@ class BoardPrefs with _$BoardPrefs implements Serializable { required bool boardHighlights, required bool coordinates, required bool pieceAnimation, - required MaterialDifference materialDifference, + required MaterialDifferenceFormat materialDifference, @JsonKey( defaultValue: PieceShiftMethod.either, unknownEnumValue: PieceShiftMethod.either, @@ -140,7 +140,7 @@ class BoardPrefs with _$BoardPrefs implements Serializable { boardHighlights: true, coordinates: true, pieceAnimation: true, - materialDifference: MaterialDifference.materialDifference, + materialDifference: MaterialDifferenceFormat.materialDifference, pieceShiftMethod: PieceShiftMethod.either, enableShapeDrawings: true, magnifyDraggedPiece: true, @@ -304,16 +304,16 @@ enum BoardTheme { ); } -enum MaterialDifference { - materialDifference(label: 'Material difference', visible: true), - capturedPieces(label: 'Captured pieces', visible: true), - hidden(label: 'Hidden', visible: false); +enum MaterialDifferenceFormat { + materialDifference(label: 'Material difference'), + capturedPieces(label: 'Captured pieces'), + hidden(label: 'Hidden'); - const MaterialDifference({ + const MaterialDifferenceFormat({ required this.label, - required this.visible, }); final String label; - final bool visible; + + bool get visible => this != MaterialDifferenceFormat.hidden; } diff --git a/lib/src/view/game/game_player.dart b/lib/src/view/game/game_player.dart index df4376bda2..639f2dd966 100644 --- a/lib/src/view/game/game_player.dart +++ b/lib/src/view/game/game_player.dart @@ -38,7 +38,7 @@ class GamePlayer extends StatelessWidget { final Player player; final Widget? clock; final MaterialDiffSide? materialDiff; - final MaterialDifference? materialDifference; + final MaterialDifferenceFormat? materialDifference; /// if confirm move preference is enabled, used to display confirmation buttons final ({VoidCallback confirm, VoidCallback cancel})? confirmMoveCallbacks; @@ -146,7 +146,7 @@ class GamePlayer extends StatelessWidget { else if (materialDiff != null && materialDifference?.visible == true) Row( children: [ - if (materialDifference == MaterialDifference.capturedPieces) + if (materialDifference == MaterialDifferenceFormat.capturedPieces) for (final role in Role.values) for (int i = 0; i < materialDiff!.capturedPieces[role]!; i++) Icon( @@ -154,7 +154,7 @@ class GamePlayer extends StatelessWidget { size: 13, color: Colors.grey, ), - if (materialDifference == MaterialDifference.materialDifference) + if (materialDifference == MaterialDifferenceFormat.materialDifference) for (final role in Role.values) for (int i = 0; i < materialDiff!.pieces[role]!; i++) Icon( diff --git a/lib/src/view/game/game_settings.dart b/lib/src/view/game/game_settings.dart index aebe1270da..2645e85973 100644 --- a/lib/src/view/game/game_settings.dart +++ b/lib/src/view/game/game_settings.dart @@ -133,13 +133,13 @@ class GameSettings extends ConsumerWidget { onTap: () { showChoicePicker( context, - choices: MaterialDifference.values, + choices: MaterialDifferenceFormat.values, selectedItem: boardPrefs.materialDifference, labelBuilder: (t) => Text(t.label), - onSelectedItemChanged: (MaterialDifference? value) => ref + onSelectedItemChanged: (MaterialDifferenceFormat? value) => ref .read(boardPreferencesProvider.notifier) .setMaterialDifferenceFormat( - value ?? MaterialDifference.materialDifference, + value ?? MaterialDifferenceFormat.materialDifference, ), ); }, diff --git a/lib/src/view/settings/board_settings_screen.dart b/lib/src/view/settings/board_settings_screen.dart index 1bbb760750..9a5d8dde24 100644 --- a/lib/src/view/settings/board_settings_screen.dart +++ b/lib/src/view/settings/board_settings_screen.dart @@ -190,13 +190,13 @@ class _Body extends ConsumerWidget { onTap: () { showChoicePicker( context, - choices: MaterialDifference.values, + choices: MaterialDifferenceFormat.values, selectedItem: boardPrefs.materialDifference, labelBuilder: (t) => Text(t.label), - onSelectedItemChanged: (MaterialDifference? value) => ref + onSelectedItemChanged: (MaterialDifferenceFormat? value) => ref .read(boardPreferencesProvider.notifier) .setMaterialDifferenceFormat( - value ?? MaterialDifference.materialDifference, + value ?? MaterialDifferenceFormat.materialDifference, ), ); }, From 21fa1e936ddc6778323a627fb3e2d11e27cbdb63 Mon Sep 17 00:00:00 2001 From: Jimima Date: Fri, 15 Nov 2024 14:26:56 +0000 Subject: [PATCH 680/979] Added test for captured pieces --- test/model/game/material_diff_test.dart | 26 +++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/test/model/game/material_diff_test.dart b/test/model/game/material_diff_test.dart index 7a1e7c6a79..a6391a6b18 100644 --- a/test/model/game/material_diff_test.dart +++ b/test/model/game/material_diff_test.dart @@ -38,6 +38,32 @@ void main() { }), ), ); + expect( + diff.bySide(Side.black).capturedPieces, + equals( + IMap(const { + Role.king: 0, + Role.queen: 0, + Role.rook: 0, + Role.bishop: 2, + Role.knight: 2, + Role.pawn: 4, + }), + ), + ); + expect( + diff.bySide(Side.white).capturedPieces, + equals( + IMap(const { + Role.king: 0, + Role.queen: 1, + Role.rook: 1, + Role.bishop: 1, + Role.knight: 2, + Role.pawn: 3, + }), + ), + ); }); }); } From c5e81eed527609b088323b77baa0c62c4c7e88c0 Mon Sep 17 00:00:00 2001 From: Jimima Date: Fri, 15 Nov 2024 14:28:48 +0000 Subject: [PATCH 681/979] Format --- lib/src/view/game/game_player.dart | 3 ++- lib/src/view/settings/board_settings_screen.dart | 12 +++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/src/view/game/game_player.dart b/lib/src/view/game/game_player.dart index 639f2dd966..134756fb9f 100644 --- a/lib/src/view/game/game_player.dart +++ b/lib/src/view/game/game_player.dart @@ -154,7 +154,8 @@ class GamePlayer extends StatelessWidget { size: 13, color: Colors.grey, ), - if (materialDifference == MaterialDifferenceFormat.materialDifference) + if (materialDifference == + MaterialDifferenceFormat.materialDifference) for (final role in Role.values) for (int i = 0; i < materialDiff!.pieces[role]!; i++) Icon( diff --git a/lib/src/view/settings/board_settings_screen.dart b/lib/src/view/settings/board_settings_screen.dart index 9a5d8dde24..0e88ed4c55 100644 --- a/lib/src/view/settings/board_settings_screen.dart +++ b/lib/src/view/settings/board_settings_screen.dart @@ -193,11 +193,13 @@ class _Body extends ConsumerWidget { choices: MaterialDifferenceFormat.values, selectedItem: boardPrefs.materialDifference, labelBuilder: (t) => Text(t.label), - onSelectedItemChanged: (MaterialDifferenceFormat? value) => ref - .read(boardPreferencesProvider.notifier) - .setMaterialDifferenceFormat( - value ?? MaterialDifferenceFormat.materialDifference, - ), + onSelectedItemChanged: (MaterialDifferenceFormat? value) => + ref + .read(boardPreferencesProvider.notifier) + .setMaterialDifferenceFormat( + value ?? + MaterialDifferenceFormat.materialDifference, + ), ); }, ), From 3dfd6b3ac1c3e5d7f7d334bdee047fa7bc1b576b Mon Sep 17 00:00:00 2001 From: Jimima Date: Fri, 15 Nov 2024 14:52:12 +0000 Subject: [PATCH 682/979] Parameterised starting position --- lib/src/model/game/material_diff.dart | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/src/model/game/material_diff.dart b/lib/src/model/game/material_diff.dart index a661d1fa2d..30cd29cf33 100644 --- a/lib/src/model/game/material_diff.dart +++ b/lib/src/model/game/material_diff.dart @@ -31,16 +31,17 @@ class MaterialDiff with _$MaterialDiff { required MaterialDiffSide white, }) = _MaterialDiff; - factory MaterialDiff.fromBoard(Board board) { + factory MaterialDiff.fromBoard(Board board, {Board? startingPosition}) { int score = 0; final IMap blackCount = board.materialCount(Side.black); final IMap whiteCount = board.materialCount(Side.white); final IMap blackStartingCount = - Board.standard.materialCount(Side.black); + startingPosition?.materialCount(Side.black) ?? + Board.standard.materialCount(Side.black); final IMap whiteStartingCount = - Board.standard.materialCount(Side.white); - // TODO: parameterise starting position maybe so it can be passed in + startingPosition?.materialCount(Side.white) ?? + Board.standard.materialCount(Side.white); IMap subtractPieceCounts( IMap startingCount, From 913d4a19518e33f0faff0b20cd20c98307a95e8e Mon Sep 17 00:00:00 2001 From: Jimima Date: Fri, 15 Nov 2024 16:36:36 +0000 Subject: [PATCH 683/979] Refactor --- lib/src/model/settings/board_preferences.dart | 8 +- .../offline_correspondence_game_screen.dart | 6 +- lib/src/view/game/game_body.dart | 8 +- lib/src/view/game/game_player.dart | 80 +++++++++++-------- lib/src/view/game/game_settings.dart | 6 +- .../over_the_board/over_the_board_screen.dart | 4 +- .../view/settings/board_settings_screen.dart | 6 +- 7 files changed, 66 insertions(+), 52 deletions(-) diff --git a/lib/src/model/settings/board_preferences.dart b/lib/src/model/settings/board_preferences.dart index 482e2a0e22..9dbc7bbeac 100644 --- a/lib/src/model/settings/board_preferences.dart +++ b/lib/src/model/settings/board_preferences.dart @@ -82,10 +82,10 @@ class BoardPreferences extends _$BoardPreferences } Future setMaterialDifferenceFormat( - MaterialDifferenceFormat materialDifference, + MaterialDifferenceFormat materialDifferenceFormat, ) { return save( - state.copyWith(materialDifference: materialDifference), + state.copyWith(materialDifferenceFormat: materialDifferenceFormat), ); } @@ -113,7 +113,7 @@ class BoardPrefs with _$BoardPrefs implements Serializable { required bool boardHighlights, required bool coordinates, required bool pieceAnimation, - required MaterialDifferenceFormat materialDifference, + required MaterialDifferenceFormat materialDifferenceFormat, @JsonKey( defaultValue: PieceShiftMethod.either, unknownEnumValue: PieceShiftMethod.either, @@ -140,7 +140,7 @@ class BoardPrefs with _$BoardPrefs implements Serializable { boardHighlights: true, coordinates: true, pieceAnimation: true, - materialDifference: MaterialDifferenceFormat.materialDifference, + materialDifferenceFormat: MaterialDifferenceFormat.materialDifference, pieceShiftMethod: PieceShiftMethod.either, enableShapeDrawings: true, magnifyDraggedPiece: true, diff --git a/lib/src/view/correspondence/offline_correspondence_game_screen.dart b/lib/src/view/correspondence/offline_correspondence_game_screen.dart index 26cf810243..8f7c6b6b08 100644 --- a/lib/src/view/correspondence/offline_correspondence_game_screen.dart +++ b/lib/src/view/correspondence/offline_correspondence_game_screen.dart @@ -144,7 +144,7 @@ class _BodyState extends ConsumerState<_Body> { Widget build(BuildContext context) { final materialDifference = ref.watch( boardPreferencesProvider.select( - (prefs) => prefs.materialDifference, + (prefs) => prefs.materialDifferenceFormat, ), ); @@ -160,7 +160,7 @@ class _BodyState extends ConsumerState<_Body> { materialDiff: materialDifference.visible ? game.materialDiffAt(stepCursor, Side.black) : null, - materialDifference: materialDifference, + materialDifferenceFormat: materialDifference, shouldLinkToUserProfile: false, mePlaying: youAre == Side.black, confirmMoveCallbacks: youAre == Side.black && moveToConfirm != null @@ -183,7 +183,7 @@ class _BodyState extends ConsumerState<_Body> { materialDiff: materialDifference.visible ? game.materialDiffAt(stepCursor, Side.white) : null, - materialDifference: materialDifference, + materialDifferenceFormat: materialDifference, shouldLinkToUserProfile: false, mePlaying: youAre == Side.white, confirmMoveCallbacks: youAre == Side.white && moveToConfirm != null diff --git a/lib/src/view/game/game_body.dart b/lib/src/view/game/game_body.dart index 928051b1ec..eea37bfced 100644 --- a/lib/src/view/game/game_body.dart +++ b/lib/src/view/game/game_body.dart @@ -128,10 +128,10 @@ class GameBody extends ConsumerWidget { final black = GamePlayer( player: gameState.game.black, - materialDiff: boardPreferences.materialDifference.visible + materialDiff: boardPreferences.materialDifferenceFormat.visible ? gameState.game.materialDiffAt(gameState.stepCursor, Side.black) : null, - materialDifference: boardPreferences.materialDifference, + materialDifferenceFormat: boardPreferences.materialDifferenceFormat, timeToMove: gameState.game.sideToMove == Side.black ? gameState.timeToMove : null, @@ -169,10 +169,10 @@ class GameBody extends ConsumerWidget { ); final white = GamePlayer( player: gameState.game.white, - materialDiff: boardPreferences.materialDifference.visible + materialDiff: boardPreferences.materialDifferenceFormat.visible ? gameState.game.materialDiffAt(gameState.stepCursor, Side.white) : null, - materialDifference: boardPreferences.materialDifference, + materialDifferenceFormat: boardPreferences.materialDifferenceFormat, timeToMove: gameState.game.sideToMove == Side.white ? gameState.timeToMove : null, diff --git a/lib/src/view/game/game_player.dart b/lib/src/view/game/game_player.dart index 134756fb9f..35c4a00f40 100644 --- a/lib/src/view/game/game_player.dart +++ b/lib/src/view/game/game_player.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:dartchess/dartchess.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:lichess_mobile/src/constants.dart'; @@ -26,7 +27,7 @@ class GamePlayer extends StatelessWidget { required this.player, this.clock, this.materialDiff, - this.materialDifference, + this.materialDifferenceFormat, this.confirmMoveCallbacks, this.timeToMove, this.shouldLinkToUserProfile = true, @@ -38,7 +39,7 @@ class GamePlayer extends StatelessWidget { final Player player; final Widget? clock; final MaterialDiffSide? materialDiff; - final MaterialDifferenceFormat? materialDifference; + final MaterialDifferenceFormat? materialDifferenceFormat; /// if confirm move preference is enabled, used to display confirmation buttons final ({VoidCallback confirm, VoidCallback cancel})? confirmMoveCallbacks; @@ -143,37 +144,12 @@ class GamePlayer extends StatelessWidget { ), if (timeToMove != null) MoveExpiration(timeToMove: timeToMove!, mePlaying: mePlaying) - else if (materialDiff != null && materialDifference?.visible == true) - Row( - children: [ - if (materialDifference == MaterialDifferenceFormat.capturedPieces) - for (final role in Role.values) - for (int i = 0; i < materialDiff!.capturedPieces[role]!; i++) - Icon( - _iconByRole[role], - size: 13, - color: Colors.grey, - ), - if (materialDifference == - MaterialDifferenceFormat.materialDifference) - for (final role in Role.values) - for (int i = 0; i < materialDiff!.pieces[role]!; i++) - Icon( - _iconByRole[role], - size: 13, - color: Colors.grey, - ), - const SizedBox(width: 3), - Text( - style: const TextStyle( - fontSize: 13, - color: Colors.grey, - ), - materialDiff != null && materialDiff!.score > 0 - ? '+${materialDiff!.score}' - : '', - ), - ], + else if (materialDiff != null && + materialDifferenceFormat?.visible == true) + //MaterialDifferenceDisplay + MaterialDifferenceDisplay( + materialDiff: materialDiff!, + materialDifferenceFormat: materialDifferenceFormat!, ) else // to avoid shifts use an empty text widget @@ -338,6 +314,44 @@ class _MoveExpirationState extends State { } } +class MaterialDifferenceDisplay extends StatelessWidget { + const MaterialDifferenceDisplay({ + required this.materialDiff, + this.materialDifferenceFormat = MaterialDifferenceFormat.materialDifference, + }); + + final MaterialDiffSide materialDiff; + final MaterialDifferenceFormat materialDifferenceFormat; + + @override + Widget build(BuildContext context) { + final IMap piecesToRender = + (materialDifferenceFormat == MaterialDifferenceFormat.capturedPieces + ? materialDiff.capturedPieces + : materialDiff.pieces); + + return Row( + children: [ + for (final role in Role.values) + for (int i = 0; i < piecesToRender[role]!; i++) + Icon( + _iconByRole[role], + size: 13, + color: Colors.grey, + ), + const SizedBox(width: 3), + Text( + style: const TextStyle( + fontSize: 13, + color: Colors.grey, + ), + materialDiff.score > 0 ? '+${materialDiff.score}' : '', + ), + ], + ); + } +} + const Map _iconByRole = { Role.king: LichessIcons.chess_king, Role.queen: LichessIcons.chess_queen, diff --git a/lib/src/view/game/game_settings.dart b/lib/src/view/game/game_settings.dart index 2645e85973..4e3095c252 100644 --- a/lib/src/view/game/game_settings.dart +++ b/lib/src/view/game/game_settings.dart @@ -128,13 +128,13 @@ class GameSettings extends ConsumerWidget { }, ), SettingsListTile( - settingsLabel: const Text('Captured pieces'), - settingsValue: boardPrefs.materialDifference.label, + settingsLabel: const Text('Material'), //TODO: l10n + settingsValue: boardPrefs.materialDifferenceFormat.label, onTap: () { showChoicePicker( context, choices: MaterialDifferenceFormat.values, - selectedItem: boardPrefs.materialDifference, + selectedItem: boardPrefs.materialDifferenceFormat, labelBuilder: (t) => Text(t.label), onSelectedItemChanged: (MaterialDifferenceFormat? value) => ref .read(boardPreferencesProvider.notifier) diff --git a/lib/src/view/over_the_board/over_the_board_screen.dart b/lib/src/view/over_the_board/over_the_board_screen.dart index d563feb605..5478945df8 100644 --- a/lib/src/view/over_the_board/over_the_board_screen.dart +++ b/lib/src/view/over_the_board/over_the_board_screen.dart @@ -301,10 +301,10 @@ class _Player extends ConsumerWidget { name: side.name.capitalize(), ), ), - materialDiff: boardPreferences.materialDifference.visible + materialDiff: boardPreferences.materialDifferenceFormat.visible ? gameState.currentMaterialDiff(side) : null, - materialDifference: boardPreferences.materialDifference, + materialDifferenceFormat: boardPreferences.materialDifferenceFormat, shouldLinkToUserProfile: false, clock: clock.timeIncrement.isInfinite ? null diff --git a/lib/src/view/settings/board_settings_screen.dart b/lib/src/view/settings/board_settings_screen.dart index 0e88ed4c55..27b468fdc2 100644 --- a/lib/src/view/settings/board_settings_screen.dart +++ b/lib/src/view/settings/board_settings_screen.dart @@ -185,13 +185,13 @@ class _Body extends ConsumerWidget { }, ), SettingsListTile( - settingsLabel: const Text('Captured pieces'), - settingsValue: boardPrefs.materialDifference.label, + settingsLabel: const Text('Material'), + settingsValue: boardPrefs.materialDifferenceFormat.label, onTap: () { showChoicePicker( context, choices: MaterialDifferenceFormat.values, - selectedItem: boardPrefs.materialDifference, + selectedItem: boardPrefs.materialDifferenceFormat, labelBuilder: (t) => Text(t.label), onSelectedItemChanged: (MaterialDifferenceFormat? value) => ref From b784f93948bc4f9a10a6cd60ab93d7117b49f4e2 Mon Sep 17 00:00:00 2001 From: Jimima Date: Fri, 15 Nov 2024 16:44:18 +0000 Subject: [PATCH 684/979] Removed comment --- lib/src/view/game/game_player.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/src/view/game/game_player.dart b/lib/src/view/game/game_player.dart index 35c4a00f40..87af26cf8d 100644 --- a/lib/src/view/game/game_player.dart +++ b/lib/src/view/game/game_player.dart @@ -146,7 +146,6 @@ class GamePlayer extends StatelessWidget { MoveExpiration(timeToMove: timeToMove!, mePlaying: mePlaying) else if (materialDiff != null && materialDifferenceFormat?.visible == true) - //MaterialDifferenceDisplay MaterialDifferenceDisplay( materialDiff: materialDiff!, materialDifferenceFormat: materialDifferenceFormat!, From 46a74804c184ca6f9d0f869368379e1a3187c99d Mon Sep 17 00:00:00 2001 From: Jimima Date: Sat, 16 Nov 2024 12:45:14 +0000 Subject: [PATCH 685/979] Placeholder l10n implementation --- lib/src/model/settings/board_preferences.dart | 8 ++++++++ lib/src/view/game/game_settings.dart | 6 ++++-- lib/src/view/settings/board_settings_screen.dart | 9 ++++++--- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/lib/src/model/settings/board_preferences.dart b/lib/src/model/settings/board_preferences.dart index 9dbc7bbeac..e07555b201 100644 --- a/lib/src/model/settings/board_preferences.dart +++ b/lib/src/model/settings/board_preferences.dart @@ -2,6 +2,7 @@ import 'package:chessground/chessground.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:lichess_mobile/l10n/l10n.dart'; import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/color_palette.dart'; @@ -316,4 +317,11 @@ enum MaterialDifferenceFormat { final String label; bool get visible => this != MaterialDifferenceFormat.hidden; + + String l10n(AppLocalizations l10n) => switch (this) { + //TODO: Add l10n + MaterialDifferenceFormat.materialDifference => materialDifference.label, + MaterialDifferenceFormat.capturedPieces => capturedPieces.label, + MaterialDifferenceFormat.hidden => hidden.label, + }; } diff --git a/lib/src/view/game/game_settings.dart b/lib/src/view/game/game_settings.dart index 4e3095c252..25e9c2b1f7 100644 --- a/lib/src/view/game/game_settings.dart +++ b/lib/src/view/game/game_settings.dart @@ -1,6 +1,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/l10n/l10n.dart'; import 'package:lichess_mobile/src/model/account/account_preferences.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/game/game_controller.dart'; @@ -129,13 +130,14 @@ class GameSettings extends ConsumerWidget { ), SettingsListTile( settingsLabel: const Text('Material'), //TODO: l10n - settingsValue: boardPrefs.materialDifferenceFormat.label, + settingsValue: boardPrefs.materialDifferenceFormat + .l10n(AppLocalizations.of(context)), onTap: () { showChoicePicker( context, choices: MaterialDifferenceFormat.values, selectedItem: boardPrefs.materialDifferenceFormat, - labelBuilder: (t) => Text(t.label), + labelBuilder: (t) => Text(t.l10n(AppLocalizations.of(context))), onSelectedItemChanged: (MaterialDifferenceFormat? value) => ref .read(boardPreferencesProvider.notifier) .setMaterialDifferenceFormat( diff --git a/lib/src/view/settings/board_settings_screen.dart b/lib/src/view/settings/board_settings_screen.dart index 27b468fdc2..e2bc8b5ccd 100644 --- a/lib/src/view/settings/board_settings_screen.dart +++ b/lib/src/view/settings/board_settings_screen.dart @@ -2,6 +2,7 @@ import 'package:chessground/chessground.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/l10n/l10n.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; @@ -185,14 +186,16 @@ class _Body extends ConsumerWidget { }, ), SettingsListTile( - settingsLabel: const Text('Material'), - settingsValue: boardPrefs.materialDifferenceFormat.label, + settingsLabel: const Text('Material'), //TODO: l10n + settingsValue: boardPrefs.materialDifferenceFormat + .l10n(AppLocalizations.of(context)), onTap: () { showChoicePicker( context, choices: MaterialDifferenceFormat.values, selectedItem: boardPrefs.materialDifferenceFormat, - labelBuilder: (t) => Text(t.label), + labelBuilder: (t) => + Text(t.l10n(AppLocalizations.of(context))), onSelectedItemChanged: (MaterialDifferenceFormat? value) => ref .read(boardPreferencesProvider.notifier) From cc00b8c807203ff2dd76211590924991c6946c18 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Nov 2024 01:52:20 +0000 Subject: [PATCH 686/979] Bump softprops/action-gh-release Bumps the ci-dependencies group with 1 update in the / directory: [softprops/action-gh-release](https://github.com/softprops/action-gh-release). Updates `softprops/action-gh-release` from 2.0.8 to 2.1.0 - [Release notes](https://github.com/softprops/action-gh-release/releases) - [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md) - [Commits](https://github.com/softprops/action-gh-release/compare/c062e08bd532815e2082a85e87e3ef29c3e6d191...01570a1f39cb168c169c802c3bceb9e93fb10974) --- updated-dependencies: - dependency-name: softprops/action-gh-release dependency-type: direct:production update-type: version-update:semver-minor dependency-group: ci-dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/draft_github_release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/draft_github_release.yml b/.github/workflows/draft_github_release.yml index 4c551738d6..6bcd8d27ea 100644 --- a/.github/workflows/draft_github_release.yml +++ b/.github/workflows/draft_github_release.yml @@ -20,7 +20,7 @@ jobs: steps: - name: Draft release with release notes id: create_release - uses: softprops/action-gh-release@c062e08bd532815e2082a85e87e3ef29c3e6d191 + uses: softprops/action-gh-release@01570a1f39cb168c169c802c3bceb9e93fb10974 with: tag_name: ${{ github.event.inputs.version }} draft: true From b863459f8107e652e792dfe41a2ea6c7b8391e3c Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 18 Nov 2024 11:20:22 +0100 Subject: [PATCH 687/979] Filter chat spam --- lib/src/model/game/chat_controller.dart | 59 +++++++++++++++++++++---- lib/src/view/game/message_screen.dart | 51 +++++++++++---------- 2 files changed, 78 insertions(+), 32 deletions(-) diff --git a/lib/src/model/game/chat_controller.dart b/lib/src/model/game/chat_controller.dart index 6ec13b78a6..8c0f294945 100644 --- a/lib/src/model/game/chat_controller.dart +++ b/lib/src/model/game/chat_controller.dart @@ -125,14 +125,8 @@ class ChatController extends _$ChatController { } } else if (event.topic == 'message') { final data = event.data as Map; - final message = data['t'] as String; - final username = data['u'] as String?; - _addMessage( - ( - message: message, - username: username, - ), - ); + final message = _messageFromPick(RequiredPick(data)); + _addMessage(message); } } } @@ -147,11 +141,58 @@ class ChatState with _$ChatState { }) = _ChatState; } -typedef Message = ({String? username, String message}); +typedef Message = ({ + String? username, + String message, + bool troll, + bool deleted, +}); Message _messageFromPick(RequiredPick pick) { return ( message: pick('t').asStringOrThrow(), username: pick('u').asStringOrNull(), + troll: pick('r').asBoolOrNull() ?? false, + deleted: pick('d').asBoolOrNull() ?? false, ); } + +bool isSpam(Message message) { + return spamRegex.hasMatch(message.message) || + followMeRegex.hasMatch(message.message); +} + +final RegExp spamRegex = RegExp( + [ + 'xcamweb.com', + '(^|[^i])chess-bot', + 'chess-cheat', + 'coolteenbitch', + 'letcafa.webcam', + 'tinyurl.com/', + 'wooga.info/', + 'bit.ly/', + 'wbt.link/', + 'eb.by/', + '001.rs/', + 'shr.name/', + 'u.to/', + '.3-a.net', + '.ssl443.org', + '.ns02.us', + '.myftp.info', + '.flinkup.com', + '.serveusers.com', + 'badoogirls.com', + 'hide.su', + 'wyon.de', + 'sexdatingcz.club', + 'qps.ru', + 'tiny.cc/', + 'trasderk.blogspot.com', + 't.ly/', + 'shorturl.at/', + ].map((url) => url.replaceAll('.', '\\.').replaceAll('/', '\\/')).join('|'), +); + +final followMeRegex = RegExp('follow me|join my team', caseSensitive: false); diff --git a/lib/src/view/game/message_screen.dart b/lib/src/view/game/message_screen.dart index d684f855c0..00187cc040 100644 --- a/lib/src/view/game/message_screen.dart +++ b/lib/src/view/game/message_screen.dart @@ -83,29 +83,34 @@ class _Body extends ConsumerWidget { child: GestureDetector( onTap: () => FocusScope.of(context).unfocus(), child: chatStateAsync.when( - data: (chatState) => ListView.builder( - // remove the automatic bottom padding of the ListView, which on iOS - // corresponds to the safe area insets - // and which is here taken care of by the _ChatBottomBar - padding: MediaQuery.of(context).padding.copyWith(bottom: 0), - reverse: true, - itemCount: chatState.messages.length, - itemBuilder: (context, index) { - final message = - chatState.messages[chatState.messages.length - index - 1]; - return (message.username == 'lichess') - ? _MessageAction(message: message.message) - : (message.username == me?.name) - ? _MessageBubble( - you: true, - message: message.message, - ) - : _MessageBubble( - you: false, - message: message.message, - ); - }, - ), + data: (chatState) { + final selectedMessages = chatState.messages + .where((m) => !m.troll && !m.deleted && !isSpam(m)) + .toList(); + final messagesCount = selectedMessages.length; + return ListView.builder( + // remove the automatic bottom padding of the ListView, which on iOS + // corresponds to the safe area insets + // and which is here taken care of by the _ChatBottomBar + padding: MediaQuery.of(context).padding.copyWith(bottom: 0), + reverse: true, + itemCount: messagesCount, + itemBuilder: (context, index) { + final message = selectedMessages[messagesCount - index - 1]; + return (message.username == 'lichess') + ? _MessageAction(message: message.message) + : (message.username == me?.name) + ? _MessageBubble( + you: true, + message: message.message, + ) + : _MessageBubble( + you: false, + message: message.message, + ); + }, + ); + }, loading: () => const Center( child: CircularProgressIndicator(), ), From c42f5c5b68c502bece9a2f2968251e6647b581a9 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 18 Nov 2024 11:35:53 +0100 Subject: [PATCH 688/979] Update countdownclock --- lib/src/view/watch/tv_screen.dart | 14 ++++++++--- lib/src/widgets/clock.dart | 42 +++++++++++++++++++------------ test/widgets/clock_test.dart | 2 +- 3 files changed, 37 insertions(+), 21 deletions(-) diff --git a/lib/src/view/watch/tv_screen.dart b/lib/src/view/watch/tv_screen.dart index fe41554aae..98df340be2 100644 --- a/lib/src/view/watch/tv_screen.dart +++ b/lib/src/view/watch/tv_screen.dart @@ -99,8 +99,11 @@ class _Body extends ConsumerWidget { const Duration(milliseconds: 10), clockUpdatedAt: gameState.game.clock!.at, active: gameState.activeClockSide == Side.black, - builder: (context, timeLeft, active) { - return Clock(timeLeft: timeLeft, active: active); + builder: (context, timeLeft) { + return Clock( + timeLeft: timeLeft, + active: gameState.activeClockSide == Side.black, + ); }, ) : null, @@ -116,8 +119,11 @@ class _Body extends ConsumerWidget { delay: gameState.game.clock!.lag ?? const Duration(milliseconds: 10), active: gameState.activeClockSide == Side.white, - builder: (context, timeLeft, active) { - return Clock(timeLeft: timeLeft, active: active); + builder: (context, timeLeft) { + return Clock( + timeLeft: timeLeft, + active: gameState.activeClockSide == Side.white, + ); }, ) : null, diff --git a/lib/src/widgets/clock.dart b/lib/src/widgets/clock.dart index e3b70f9bcb..7de23af3e0 100644 --- a/lib/src/widgets/clock.dart +++ b/lib/src/widgets/clock.dart @@ -9,9 +9,11 @@ const _kClockFontSize = 26.0; const _kClockTenthFontSize = 20.0; const _kClockHundredsFontSize = 18.0; +const _showTenthsThreshold = Duration(seconds: 10); + /// A stateless widget that displays the time left on the clock. /// -/// For a clock widget that automatically counts down, see [CountdownClock]. +/// For a clock widget that automatically counts down, see [CountdownClockBuilder]. class Clock extends StatelessWidget { const Clock({ required this.timeLeft, @@ -162,22 +164,30 @@ class ClockStyle { ); } -typedef ClockWidgetBuilder = Widget Function( - BuildContext context, - Duration timeLeft, - bool isActive, -); +typedef ClockWidgetBuilder = Widget Function(BuildContext, Duration); -/// A widget that automatically starts a countdown. +/// A widget that automatically starts a countdown from [timeLeft] when [active] is `true`. +/// +/// The clock will update the UI every [tickInterval], which defaults to 100ms, +/// and the [builder] will be called with the new [timeLeft] value. +/// +/// The clock can be synchronized with the time at which the clock event was received from the server +/// by setting the [clockUpdatedAt] parameter. +/// This widget will only update its internal clock when the [clockUpdatedAt] parameter changes. /// -/// The clock starts only when [active] is `true`. +/// The [delay] parameter can be used to delay the start of the clock. +/// +/// The clock will stop counting down when [active] is set to `false`. +/// +/// The clock will stop counting down when the time left reaches zero. class CountdownClockBuilder extends StatefulWidget { const CountdownClockBuilder({ required this.timeLeft, - this.delay, - this.clockUpdatedAt, required this.active, required this.builder, + this.delay, + this.tickInterval = const Duration(milliseconds: 100), + this.clockUpdatedAt, super.key, }); @@ -189,13 +199,16 @@ class CountdownClockBuilder extends StatefulWidget { /// This can be used to implement lag compensation. final Duration? delay; + /// The interval at which the clock updates the UI. + final Duration tickInterval; + /// The time at which the clock was updated. /// /// Use this parameter to synchronize the clock with the time at which the clock /// event was received from the server and to compensate for UI lag. final DateTime? clockUpdatedAt; - /// If [active] is `true`, the clock starts counting down. + /// If `true`, the clock starts counting down. final bool active; /// A [ClockWidgetBuilder] that builds the clock on each tick with the new [timeLeft] value. @@ -205,9 +218,6 @@ class CountdownClockBuilder extends StatefulWidget { State createState() => _CountdownClockState(); } -const _tickDelay = Duration(milliseconds: 100); -const _showTenthsThreshold = Duration(seconds: 10); - class _CountdownClockState extends State { Timer? _timer; Timer? _delayTimer; @@ -240,7 +250,7 @@ class _CountdownClockState extends State { void _scheduleTick() { _timer?.cancel(); - _timer = Timer(_tickDelay, _tick); + _timer = Timer(widget.tickInterval, _tick); _stopwatch.reset(); _stopwatch.start(); } @@ -300,7 +310,7 @@ class _CountdownClockState extends State { @override Widget build(BuildContext context) { return RepaintBoundary( - child: widget.builder(context, timeLeft, widget.active), + child: widget.builder(context, timeLeft), ); } } diff --git a/test/widgets/clock_test.dart b/test/widgets/clock_test.dart index f94a315fd5..73e20078f6 100644 --- a/test/widgets/clock_test.dart +++ b/test/widgets/clock_test.dart @@ -33,7 +33,7 @@ void main() { }); group('CountdownClockBuilder', () { - Widget clockBuilder(BuildContext context, Duration timeLeft, bool active) { + Widget clockBuilder(BuildContext context, Duration timeLeft) { final mins = timeLeft.inMinutes.remainder(60); final secs = timeLeft.inSeconds.remainder(60).toString().padLeft(2, '0'); final tenths = timeLeft.inMilliseconds.remainder(1000) ~/ 100; From c8c818b331ba1a894a546f32cdc54c7798d05a80 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Mon, 18 Nov 2024 14:01:45 +0100 Subject: [PATCH 689/979] remove menu to keep only the flip board action --- .../broadcast/broadcast_game_controller.dart | 12 +---- .../broadcast/broadcast_game_bottom_bar.dart | 46 +++---------------- 2 files changed, 8 insertions(+), 50 deletions(-) diff --git a/lib/src/model/broadcast/broadcast_game_controller.dart b/lib/src/model/broadcast/broadcast_game_controller.dart index 08ebd514e0..7832c0b7ce 100644 --- a/lib/src/model/broadcast/broadcast_game_controller.dart +++ b/lib/src/model/broadcast/broadcast_game_controller.dart @@ -424,16 +424,6 @@ class BroadcastGameController extends _$BroadcastGameController } } - /// Makes a full PGN string (including headers and comments) of the current game state. - String makeExportPgn() { - if (!state.hasValue) Exception('Cannot make a PGN'); - - return _root.makePgn( - state.requireValue.pgnHeaders, - state.requireValue.pgnRootComments, - ); - } - /// Makes a PGN string up to the current node only. String makeCurrentNodePgn() { if (!state.hasValue) Exception('Cannot make a PGN up to the current node'); @@ -641,7 +631,7 @@ class BroadcastGameState with _$BroadcastGameState { /// It can be either moves or opening explorer in this controller (summary will be added later). required DisplayMode displayMode, - /// Clocks if avaible. Only used by the broadcast analysis screen. + /// Clocks if available. ({Duration? parentClock, Duration? clock})? clocks, /// The last move played. diff --git a/lib/src/view/broadcast/broadcast_game_bottom_bar.dart b/lib/src/view/broadcast/broadcast_game_bottom_bar.dart index d89cd2a2b2..3fbf77c630 100644 --- a/lib/src/view/broadcast/broadcast_game_bottom_bar.dart +++ b/lib/src/view/broadcast/broadcast_game_bottom_bar.dart @@ -6,9 +6,7 @@ import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/network/connectivity.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; -import 'package:lichess_mobile/src/view/board_editor/board_editor_screen.dart'; import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_screen.dart'; -import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; @@ -32,11 +30,15 @@ class BroadcastGameBottomBar extends ConsumerWidget { return BottomBar( children: [ BottomBarButton( - label: context.l10n.menu, + label: context.l10n.flipBoard, onTap: () { - _showAnalysisMenu(context, ref); + ref + .read( + ctrlProvider.notifier, + ) + .toggleBoard(); }, - icon: Icons.menu, + icon: CupertinoIcons.arrow_2_squarepath, ), BottomBarButton( label: context.l10n.openingExplorer, @@ -85,38 +87,4 @@ class BroadcastGameBottomBar extends ConsumerWidget { void _moveBackward(WidgetRef ref) => ref .read(broadcastGameControllerProvider(roundId, gameId).notifier) .userPrevious(); - - Future _showAnalysisMenu(BuildContext context, WidgetRef ref) { - return showAdaptiveActionSheet( - context: context, - actions: [ - BottomSheetAction( - makeLabel: (context) => Text(context.l10n.flipBoard), - onPressed: (context) { - ref - .read( - broadcastGameControllerProvider(roundId, gameId).notifier, - ) - .toggleBoard(); - }, - ), - BottomSheetAction( - makeLabel: (context) => Text(context.l10n.boardEditor), - onPressed: (context) { - final analysisState = ref - .read(broadcastGameControllerProvider(roundId, gameId)) - .requireValue; - final boardFen = analysisState.position.fen; - pushPlatformRoute( - context, - title: context.l10n.boardEditor, - builder: (_) => BoardEditorScreen( - initialFen: boardFen, - ), - ); - }, - ), - ], - ); - } } From 063006437d4890dff7e396d6621495d0eb0a48fd Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Mon, 18 Nov 2024 14:51:06 +0100 Subject: [PATCH 690/979] remove the opening header and the opening screen in bottom bar --- .../broadcast/broadcast_game_controller.dart | 69 +------------------ .../broadcast/broadcast_game_bottom_bar.dart | 22 ------ .../broadcast/broadcast_game_tree_view.dart | 68 +++--------------- 3 files changed, 10 insertions(+), 149 deletions(-) diff --git a/lib/src/model/broadcast/broadcast_game_controller.dart b/lib/src/model/broadcast/broadcast_game_controller.dart index 7832c0b7ce..5d581eb042 100644 --- a/lib/src/model/broadcast/broadcast_game_controller.dart +++ b/lib/src/model/broadcast/broadcast_game_controller.dart @@ -6,7 +6,6 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; -import 'package:lichess_mobile/src/model/analysis/opening_service.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_repository.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; @@ -98,7 +97,6 @@ class BroadcastGameController extends _$BroadcastGameController lastMove: lastMove, pov: Side.white, isLocalEvaluationEnabled: prefs.enableLocalEvaluation, - displayMode: DisplayMode.moves, clocks: _makeClocks(currentPath), ); @@ -407,23 +405,6 @@ class BroadcastGameController extends _$BroadcastGameController _startEngineEval(); } - void setDisplayMode(DisplayMode mode) { - if (!state.hasValue) return; - - state = AsyncData(state.requireValue.copyWith(displayMode: mode)); - } - - /// Gets the node and maybe the associated branch opening at the given path. - (Node, Opening?) _nodeOpeningAt(Node node, UciPath path, [Opening? opening]) { - if (path.isEmpty) return (node, opening); - final child = node.childById(path.head!); - if (child != null) { - return _nodeOpeningAt(child, path.tail, child.opening ?? opening); - } else { - return (node, opening); - } - } - /// Makes a PGN string up to the current node only. String makeCurrentNodePgn() { if (!state.hasValue) Exception('Cannot make a PGN up to the current node'); @@ -442,7 +423,7 @@ class BroadcastGameController extends _$BroadcastGameController if (!state.hasValue) return; final pathChange = state.requireValue.currentPath != path; - final (currentNode, opening) = _nodeOpeningAt(_root, path); + final currentNode = _root.nodeAt(path); // always show variation if the user plays a move if (shouldForceShowVariation && @@ -481,11 +462,6 @@ class BroadcastGameController extends _$BroadcastGameController soundService.play(Sound.move); } } - - if (currentNode.opening == null && currentNode.position.ply <= 30) { - _fetchOpening(_root, path); - } - state = AsyncData( state.requireValue.copyWith( currentPath: path, @@ -493,7 +469,6 @@ class BroadcastGameController extends _$BroadcastGameController isBroadcastMove ? path : state.requireValue.broadcastLivePath, isOnMainline: _root.isOnMainline(path), currentNode: AnalysisCurrentNode.fromNode(currentNode), - currentBranchOpening: opening, lastMove: currentNode.sanMove.move, promotionMove: null, root: rootView, @@ -508,7 +483,6 @@ class BroadcastGameController extends _$BroadcastGameController isBroadcastMove ? path : state.requireValue.broadcastLivePath, isOnMainline: _root.isOnMainline(path), currentNode: AnalysisCurrentNode.fromNode(currentNode), - currentBranchOpening: opening, lastMove: null, promotionMove: null, root: rootView, @@ -522,29 +496,6 @@ class BroadcastGameController extends _$BroadcastGameController } } - Future _fetchOpening(Node fromNode, UciPath path) async { - if (!state.hasValue) return; - - final moves = fromNode.branchesOn(path).map((node) => node.sanMove.move); - if (moves.isEmpty) return; - if (moves.length > 40) return; - - final opening = - await ref.read(openingServiceProvider).fetchFromMoves(moves); - - if (opening != null) { - fromNode.updateAt(path, (node) => node.opening = opening); - - if (state.requireValue.currentPath == path) { - state = AsyncData( - state.requireValue.copyWith( - currentNode: AnalysisCurrentNode.fromNode(fromNode.nodeAt(path)), - ), - ); - } - } - } - void _startEngineEval() { if (!state.hasValue) return; @@ -626,11 +577,6 @@ class BroadcastGameState with _$BroadcastGameState { /// Whether the user has enabled local evaluation. required bool isLocalEvaluationEnabled, - /// The display mode of the analysis. - /// - /// It can be either moves or opening explorer in this controller (summary will be added later). - required DisplayMode displayMode, - /// Clocks if available. ({Duration? parentClock, Duration? clock})? clocks, @@ -640,9 +586,6 @@ class BroadcastGameState with _$BroadcastGameState { /// Possible promotion move to be played. NormalMove? promotionMove, - /// The opening of the current branch. - Opening? currentBranchOpening, - /// The PGN headers of the game. required IMap pgnHeaders, @@ -650,7 +593,7 @@ class BroadcastGameState with _$BroadcastGameState { /// /// This field is only used with user submitted PGNS. IList? pgnRootComments, - }) = _AnalysisState; + }) = _BroadcastGameState; IMap> get validMoves => makeLegalMoves(currentNode.position); @@ -667,12 +610,4 @@ class BroadcastGameState with _$BroadcastGameState { position: position, savedEval: currentNode.eval ?? currentNode.serverEval, ); - - AnalysisOptions get openingExplorerOptions => AnalysisOptions( - id: standaloneOpeningExplorerId, - isLocalEvaluationAllowed: false, - orientation: pov, - variant: Variant.standard, - initialMoveCursor: currentPath.size, - ); } diff --git a/lib/src/view/broadcast/broadcast_game_bottom_bar.dart b/lib/src/view/broadcast/broadcast_game_bottom_bar.dart index 3fbf77c630..ef3a9d550f 100644 --- a/lib/src/view/broadcast/broadcast_game_bottom_bar.dart +++ b/lib/src/view/broadcast/broadcast_game_bottom_bar.dart @@ -1,12 +1,8 @@ import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_game_controller.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/network/connectivity.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; -import 'package:lichess_mobile/src/utils/navigation.dart'; -import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_screen.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; @@ -24,8 +20,6 @@ class BroadcastGameBottomBar extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final ctrlProvider = broadcastGameControllerProvider(roundId, gameId); final analysisState = ref.watch(ctrlProvider).requireValue; - final isOnline = - ref.watch(connectivityChangesProvider).valueOrNull?.isOnline ?? false; return BottomBar( children: [ @@ -40,22 +34,6 @@ class BroadcastGameBottomBar extends ConsumerWidget { }, icon: CupertinoIcons.arrow_2_squarepath, ), - BottomBarButton( - label: context.l10n.openingExplorer, - onTap: isOnline - ? () { - pushPlatformRoute( - context, - title: context.l10n.openingExplorer, - builder: (_) => OpeningExplorerScreen( - pgn: ref.read(ctrlProvider.notifier).makeCurrentNodePgn(), - options: analysisState.openingExplorerOptions, - ), - ); - } - : null, - icon: Icons.explore, - ), RepeatButton( onLongPress: analysisState.canGoBack ? () => _moveBackward(ref) : null, diff --git a/lib/src/view/broadcast/broadcast_game_tree_view.dart b/lib/src/view/broadcast/broadcast_game_tree_view.dart index d5b53678a5..7f1ef46be9 100644 --- a/lib/src/view/broadcast/broadcast_game_tree_view.dart +++ b/lib/src/view/broadcast/broadcast_game_tree_view.dart @@ -1,9 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_game_controller.dart'; -import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/widgets/pgn.dart'; const kOpeningHeaderHeight = 32.0; @@ -34,64 +32,14 @@ class BroadcastGameTreeView extends ConsumerWidget { ctrlProvider.select((value) => value.requireValue.pgnRootComments), ); - return ListView( - padding: EdgeInsets.zero, - children: [ - _OpeningHeader( - ctrlProvider, - displayMode: displayMode, - ), - DebouncedPgnTreeView( - root: root, - currentPath: currentPath, - broadcastLivePath: broadcastLivePath, - pgnRootComments: pgnRootComments, - notifier: ref.read(ctrlProvider.notifier), - ), - ], + return SingleChildScrollView( + child: DebouncedPgnTreeView( + root: root, + currentPath: currentPath, + broadcastLivePath: broadcastLivePath, + pgnRootComments: pgnRootComments, + notifier: ref.read(ctrlProvider.notifier), + ), ); } } - -class _OpeningHeader extends ConsumerWidget { - const _OpeningHeader(this.ctrlProvider, {required this.displayMode}); - - final BroadcastGameControllerProvider ctrlProvider; - final Orientation displayMode; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isRootNode = ref.watch( - ctrlProvider.select((s) => s.requireValue.currentNode.isRoot), - ); - final nodeOpening = ref - .watch(ctrlProvider.select((s) => s.requireValue.currentNode.opening)); - final branchOpening = ref - .watch(ctrlProvider.select((s) => s.requireValue.currentBranchOpening)); - final opening = isRootNode - ? LightOpening( - eco: '', - name: context.l10n.startPosition, - ) - : nodeOpening ?? branchOpening; - - return opening != null - ? Container( - height: kOpeningHeaderHeight, - width: double.infinity, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.secondaryContainer, - ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Center( - child: Text( - opening.name, - overflow: TextOverflow.ellipsis, - ), - ), - ), - ) - : const SizedBox.shrink(); - } -} From dcfa4a01692b89c01f40f4e47f78ac8951460733 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Mon, 18 Nov 2024 14:54:05 +0100 Subject: [PATCH 691/979] remove engine depth on broadcast game screen --- .../broadcast_game_engine_depth.dart | 129 ------------------ .../view/broadcast/broadcast_game_screen.dart | 2 - 2 files changed, 131 deletions(-) delete mode 100644 lib/src/view/broadcast/broadcast_game_engine_depth.dart diff --git a/lib/src/view/broadcast/broadcast_game_engine_depth.dart b/lib/src/view/broadcast/broadcast_game_engine_depth.dart deleted file mode 100644 index 1e10da5597..0000000000 --- a/lib/src/view/broadcast/broadcast_game_engine_depth.dart +++ /dev/null @@ -1,129 +0,0 @@ -import 'dart:math' as math; - -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; -import 'package:lichess_mobile/src/model/broadcast/broadcast_game_controller.dart'; -import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/model/engine/engine.dart'; -import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; -import 'package:lichess_mobile/src/utils/l10n_context.dart'; -import 'package:lichess_mobile/src/widgets/buttons.dart'; -import 'package:lichess_mobile/src/widgets/list.dart'; -import 'package:popover/popover.dart'; - -class BroadcastGameEngineDepth extends ConsumerWidget { - final BroadcastRoundId roundId; - final BroadcastGameId gameId; - - const BroadcastGameEngineDepth(this.roundId, this.gameId); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isEngineAvailable = ref.watch( - broadcastGameControllerProvider(roundId, gameId).select( - (value) => value.requireValue.isEngineAvailable, - ), - ); - final currentNode = ref.watch( - broadcastGameControllerProvider(roundId, gameId) - .select((value) => value.requireValue.currentNode), - ); - final depth = ref.watch( - engineEvaluationProvider.select((value) => value.eval?.depth), - ) ?? - currentNode.eval?.depth; - - return isEngineAvailable && depth != null - ? AppBarTextButton( - onPressed: () { - showPopover( - context: context, - bodyBuilder: (context) { - return _StockfishInfo(currentNode); - }, - direction: PopoverDirection.top, - width: 240, - backgroundColor: - Theme.of(context).platform == TargetPlatform.android - ? Theme.of(context).dialogBackgroundColor - : CupertinoDynamicColor.resolve( - CupertinoColors.tertiarySystemBackground, - context, - ), - transitionDuration: Duration.zero, - popoverTransitionBuilder: (_, child) => child, - ); - }, - child: RepaintBoundary( - child: Container( - width: 20.0, - height: 20.0, - padding: const EdgeInsets.all(2.0), - decoration: BoxDecoration( - color: Theme.of(context).platform == TargetPlatform.android - ? Theme.of(context).colorScheme.secondary - : CupertinoTheme.of(context).primaryColor, - borderRadius: BorderRadius.circular(4.0), - ), - child: FittedBox( - fit: BoxFit.contain, - child: Text( - '${math.min(99, depth)}', - style: TextStyle( - color: Theme.of(context).platform == - TargetPlatform.android - ? Theme.of(context).colorScheme.onSecondary - : CupertinoTheme.of(context).primaryContrastingColor, - fontFeatures: const [ - FontFeature.tabularFigures(), - ], - ), - ), - ), - ), - ), - ) - : const SizedBox.shrink(); - } -} - -class _StockfishInfo extends ConsumerWidget { - const _StockfishInfo(this.currentNode); - - final AnalysisCurrentNode currentNode; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final (engineName: engineName, eval: eval, state: engineState) = - ref.watch(engineEvaluationProvider); - - final currentEval = eval ?? currentNode.eval; - - final knps = engineState == EngineState.computing - ? ', ${eval?.knps.round()}kn/s' - : ''; - final depth = currentEval?.depth ?? 0; - final maxDepth = math.max(depth, kMaxEngineDepth); - - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - PlatformListTile( - leading: Image.asset( - 'assets/images/stockfish/icon.png', - width: 44, - height: 44, - ), - title: Text(engineName), - subtitle: Text( - context.l10n.depthX( - '$depth/$maxDepth$knps', - ), - ), - ), - ], - ); - } -} diff --git a/lib/src/view/broadcast/broadcast_game_screen.dart b/lib/src/view/broadcast/broadcast_game_screen.dart index d3d5524cdb..990576d25f 100644 --- a/lib/src/view/broadcast/broadcast_game_screen.dart +++ b/lib/src/view/broadcast/broadcast_game_screen.dart @@ -21,7 +21,6 @@ import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/lichess_assets.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_game_bottom_bar.dart'; -import 'package:lichess_mobile/src/view/broadcast/broadcast_game_engine_depth.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_game_settings.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_game_tree_view.dart'; import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; @@ -54,7 +53,6 @@ class BroadcastGameScreen extends ConsumerWidget { appBar: PlatformAppBar( title: Text(title), actions: [ - if (state.hasValue) BroadcastGameEngineDepth(roundId, gameId), AppBarIconButton( onPressed: () => (state.hasValue) ? showAdaptiveBottomSheet( From c4c0a141f262102c866230b5f17a445ca5cb629d Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Mon, 18 Nov 2024 15:01:38 +0100 Subject: [PATCH 692/979] s/ConsumerWidget/StatelessWidget --- lib/src/view/study/study_gamebook.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/view/study/study_gamebook.dart b/lib/src/view/study/study_gamebook.dart index 76b45ef17c..67b6d17583 100644 --- a/lib/src/view/study/study_gamebook.dart +++ b/lib/src/view/study/study_gamebook.dart @@ -7,7 +7,7 @@ import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:url_launcher/url_launcher.dart'; -class StudyGamebook extends ConsumerWidget { +class StudyGamebook extends StatelessWidget { const StudyGamebook( this.id, ); @@ -15,7 +15,7 @@ class StudyGamebook extends ConsumerWidget { final StudyId id; @override - Widget build(BuildContext context, WidgetRef ref) { + Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.all(5), child: Column( From 96469e9c37123178ec0a8d409b7a07b7f8e54530 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 18 Nov 2024 15:12:48 +0100 Subject: [PATCH 693/979] Fix study tests after merging --- test/view/study/study_screen_test.dart | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/test/view/study/study_screen_test.dart b/test/view/study/study_screen_test.dart index 4bc7935aad..5073bfd240 100644 --- a/test/view/study/study_screen_test.dart +++ b/test/view/study/study_screen_test.dart @@ -282,8 +282,6 @@ void main() { // Wait for study to load await tester.pumpAndSettle(); - final boardRect = tester.getRect(find.byType(Chessboard)); - const introText = "We begin our lecture with an 'easy but not easy' example. White to play and win."; @@ -297,7 +295,7 @@ void main() { ); // Play a wrong move - await playMove(tester, boardRect, 'c3', 'a2'); + await playMove(tester, 'c3', 'a2'); expect(find.text("That's not the move!"), findsOneWidget); expect(find.text(introText), findsNothing); @@ -307,7 +305,7 @@ void main() { expect(find.text(introText), findsOneWidget); // Play another wrong move, but this one has an explicit comment - await playMove(tester, boardRect, 'c3', 'e2'); + await playMove(tester, 'c3', 'e2'); // If there's an explicit comment, the move is not taken back automatically // Verify this by waiting the same duration as above @@ -322,7 +320,7 @@ void main() { expect(find.text(introText), findsOneWidget); // Play the correct move - await playMove(tester, boardRect, 'c3', 'd5'); + await playMove(tester, 'c3', 'd5'); expect( find.text( @@ -349,7 +347,7 @@ void main() { findsOneWidget, ); - await playMove(tester, boardRect, 'f3', 'c3'); + await playMove(tester, 'f3', 'c3'); expect(find.text('Good move'), findsOneWidget); // No explicit feedback, so opponent move should be played automatically after delay @@ -360,7 +358,7 @@ void main() { findsOneWidget, ); - await playMove(tester, boardRect, 'c3', 'g3'); + await playMove(tester, 'c3', 'g3'); expect(find.text('A fork, threatening Rg7 & b3.'), findsOneWidget); await tester.tap(find.byTooltip('Next')); @@ -371,7 +369,7 @@ void main() { findsOneWidget, ); - await playMove(tester, boardRect, 'b2', 'b3'); + await playMove(tester, 'b2', 'b3'); expect( find.text( @@ -434,8 +432,7 @@ void main() { expect(find.text('Hint 1'), findsOneWidget); expect(find.text('Get a hint'), findsNothing); - final boardRect = tester.getRect(find.byType(Chessboard)); - await playMove(tester, boardRect, 'e2', 'e3'); + await playMove(tester, 'e2', 'e3'); expect( find.text('Shown if any move other than d4 is played'), findsOneWidget, @@ -443,7 +440,7 @@ void main() { await tester.tap(find.byTooltip('Retry')); await tester.pump(); // Wait for move to be taken back - await playMove(tester, boardRect, 'd2', 'd4'); + await playMove(tester, 'd2', 'd4'); expect(find.text('Shown if d4 is played'), findsOneWidget); await tester.tap(find.byTooltip('Retry')); await tester.pump(); // Wait for move to be taken back @@ -456,7 +453,7 @@ void main() { expect(find.text('Get a hint'), findsNothing); // Play a wrong move again - generic feedback should be shown - await playMove(tester, boardRect, 'a2', 'a3'); + await playMove(tester, 'a2', 'a3'); expect(find.text("That's not the move!"), findsOneWidget); // Wait for wrong move to be taken back await tester.pump(const Duration(seconds: 1)); From 99eb758fb97d48789c9a8deecfcd2e24a0a2208f Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Mon, 18 Nov 2024 15:15:28 +0100 Subject: [PATCH 694/979] add patch for dropdown menu widget --- patchs/dropdown_menu.patch | 64 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 patchs/dropdown_menu.patch diff --git a/patchs/dropdown_menu.patch b/patchs/dropdown_menu.patch new file mode 100644 index 0000000000..8c72074129 --- /dev/null +++ b/patchs/dropdown_menu.patch @@ -0,0 +1,64 @@ +diff --git a/packages/flutter/lib/src/material/dropdown_menu.dart b/packages/flutter/lib/src/material/dropdown_menu.dart +index 7ae84b4fa0330..def4ea08a24c5 100644 +--- a/packages/flutter/lib/src/material/dropdown_menu.dart ++++ b/packages/flutter/lib/src/material/dropdown_menu.dart +@@ -902,7 +902,6 @@ class _DropdownMenuState extends State> { + controller: _controller, + menuChildren: menu, + crossAxisUnconstrained: false, +- layerLink: LayerLink(), + builder: (BuildContext context, MenuController controller, Widget? child) { + assert(_initialMenu != null); + final Widget trailingButton = Padding( +diff --git a/packages/flutter/test/material/dropdown_menu_test.dart b/packages/flutter/test/material/dropdown_menu_test.dart +index 1156fe00a8635..d78f5f1488457 100644 +--- a/packages/flutter/test/material/dropdown_menu_test.dart ++++ b/packages/flutter/test/material/dropdown_menu_test.dart +@@ -3366,47 +3366,6 @@ void main() { + expect(controller.offset, 0.0); + }); + +- // Regression test for https://github.com/flutter/flutter/issues/149037. +- testWidgets('Dropdown menu follows the text field when keyboard opens', (WidgetTester tester) async { +- Widget boilerplate(double bottomInsets) { +- return MaterialApp( +- home: MediaQuery( +- data: MediaQueryData(viewInsets: EdgeInsets.only(bottom: bottomInsets)), +- child: Scaffold( +- body: Center( +- child: DropdownMenu(dropdownMenuEntries: menuChildren), +- ), +- ), +- ), +- ); +- } +- +- // Build once without bottom insets and open the menu. +- await tester.pumpWidget(boilerplate(0.0)); +- await tester.tap(find.byType(TextField).first); +- await tester.pump(); +- +- Finder findMenuPanels() { +- return find.byWidgetPredicate((Widget widget) => widget.runtimeType.toString() == '_MenuPanel'); +- } +- +- // Menu vertical position is just under the text field. +- expect( +- tester.getRect(findMenuPanels()).top, +- tester.getRect(find.byType(TextField).first).bottom, +- ); +- +- // Simulate the keyboard opening resizing the view. +- await tester.pumpWidget(boilerplate(100.0)); +- await tester.pump(); +- +- // Menu vertical position is just under the text field. +- expect( +- tester.getRect(findMenuPanels()).top, +- tester.getRect(find.byType(TextField).first).bottom, +- ); +- }); +- + testWidgets('DropdownMenu with expandedInsets can be aligned', (WidgetTester tester) async { + Widget buildMenuAnchor({ AlignmentGeometry alignment = Alignment.topCenter }) { + return MaterialApp( From 5eeafc50b63aea7e10e0eadbda312cb643bb4b8f Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 18 Nov 2024 15:25:38 +0100 Subject: [PATCH 695/979] Remove useless patch --- patchs/cupertino_transp_appbar.patch | 205 --------------------------- 1 file changed, 205 deletions(-) delete mode 100644 patchs/cupertino_transp_appbar.patch diff --git a/patchs/cupertino_transp_appbar.patch b/patchs/cupertino_transp_appbar.patch deleted file mode 100644 index 8b6ca0d244..0000000000 --- a/patchs/cupertino_transp_appbar.patch +++ /dev/null @@ -1,205 +0,0 @@ -diff --git a/packages/flutter/lib/src/cupertino/nav_bar.dart b/packages/flutter/lib/src/cupertino/nav_bar.dart -index 1495f55d2d..a3ef08bdb6 100644 ---- a/packages/flutter/lib/src/cupertino/nav_bar.dart -+++ b/packages/flutter/lib/src/cupertino/nav_bar.dart -@@ -31,6 +31,10 @@ const double _kNavBarLargeTitleHeightExtension = 52.0; - /// from the normal navigation bar to a big title below the navigation bar. - const double _kNavBarShowLargeTitleThreshold = 10.0; - -+/// Number of logical pixels scrolled during which the navigation bar's background -+/// fades in or out. -+const _kNavBarScrollUnderAnimationExtent = 10.0; -+ - const double _kNavBarEdgePadding = 16.0; - - const double _kNavBarBottomPadding = 8.0; -@@ -432,17 +436,81 @@ class CupertinoNavigationBar extends StatefulWidget implements ObstructingPrefer - class _CupertinoNavigationBarState extends State { - late _NavigationBarStaticComponentsKeys keys; - -+ ScrollNotificationObserverState? _scrollNotificationObserver; -+ double _scrollAnimationValue = 0.0; -+ -+ @override -+ void didChangeDependencies() { -+ super.didChangeDependencies(); -+ _scrollNotificationObserver?.removeListener(_handleScrollNotification); -+ _scrollNotificationObserver = ScrollNotificationObserver.maybeOf(context); -+ _scrollNotificationObserver?.addListener(_handleScrollNotification); -+ } -+ -+ @override -+ void dispose() { -+ if (_scrollNotificationObserver != null) { -+ _scrollNotificationObserver!.removeListener(_handleScrollNotification); -+ _scrollNotificationObserver = null; -+ } -+ super.dispose(); -+ } -+ - @override - void initState() { - super.initState(); - keys = _NavigationBarStaticComponentsKeys(); - } - -+ void _handleScrollNotification(ScrollNotification notification) { -+ if (notification is ScrollUpdateNotification && notification.depth == 0) { -+ final ScrollMetrics metrics = notification.metrics; -+ final oldScrollAnimationValue = _scrollAnimationValue; -+ double scrollExtent = 0.0; -+ switch (metrics.axisDirection) { -+ case AxisDirection.up: -+ // Scroll view is reversed -+ scrollExtent = metrics.extentAfter; -+ case AxisDirection.down: -+ scrollExtent = metrics.extentBefore; -+ case AxisDirection.right: -+ case AxisDirection.left: -+ // Scrolled under is only supported in the vertical axis, and should -+ // not be altered based on horizontal notifications of the same -+ // predicate since it could be a 2D scroller. -+ break; -+ } -+ -+ if (scrollExtent >= 0 && scrollExtent < _kNavBarScrollUnderAnimationExtent) { -+ setState(() { -+ _scrollAnimationValue = clampDouble(scrollExtent / _kNavBarScrollUnderAnimationExtent, 0, 1); -+ }); -+ } else if (scrollExtent > _kNavBarScrollUnderAnimationExtent && oldScrollAnimationValue != 1.0) { -+ setState(() { -+ _scrollAnimationValue = 1.0; -+ }); -+ } else if (scrollExtent <= 0 && oldScrollAnimationValue != 0.0) { -+ setState(() { -+ _scrollAnimationValue = 0.0; -+ }); -+ } -+ } -+ } -+ - @override - Widget build(BuildContext context) { - final Color backgroundColor = - CupertinoDynamicColor.maybeResolve(widget.backgroundColor, context) ?? CupertinoTheme.of(context).barBackgroundColor; - -+ final Border? effectiveBorder = Border.lerp( -+ const Border(bottom: BorderSide(width: 0.0, color: Color(0x00000000))), -+ widget.border, -+ _scrollAnimationValue, -+ ); -+ -+ final initialBackgroundColor = CupertinoTheme.of(context).scaffoldBackgroundColor; -+ final Color effectiveBackgroundColor = Color.lerp(initialBackgroundColor, backgroundColor, _scrollAnimationValue)!; -+ - final _NavigationBarStaticComponents components = _NavigationBarStaticComponents( - keys: keys, - route: ModalRoute.of(context), -@@ -458,8 +526,8 @@ class _CupertinoNavigationBarState extends State { - ); - - final Widget navBar = _wrapWithBackground( -- border: widget.border, -- backgroundColor: backgroundColor, -+ border: effectiveBorder, -+ backgroundColor: effectiveBackgroundColor, - brightness: widget.brightness, - child: DefaultTextStyle( - style: CupertinoTheme.of(context).textTheme.textStyle, -@@ -488,11 +556,11 @@ class _CupertinoNavigationBarState extends State { - transitionOnUserGestures: true, - child: _TransitionableNavigationBar( - componentsKeys: keys, -- backgroundColor: backgroundColor, -+ backgroundColor: effectiveBackgroundColor, - backButtonTextStyle: CupertinoTheme.of(context).textTheme.navActionTextStyle, - titleTextStyle: CupertinoTheme.of(context).textTheme.navTitleTextStyle, - largeTitleTextStyle: null, -- border: widget.border, -+ border: effectiveBorder, - hasUserMiddle: widget.middle != null, - largeExpanded: false, - child: navBar, -@@ -792,7 +860,13 @@ class _LargeTitleNavigationBarSliverDelegate - - @override - Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) { -- final bool showLargeTitle = shrinkOffset < maxExtent - minExtent - _kNavBarShowLargeTitleThreshold; -+ final double largeTitleThreshold = maxExtent - minExtent - _kNavBarShowLargeTitleThreshold; -+ final bool showLargeTitle = shrinkOffset < largeTitleThreshold; -+ final double shrinkAnimationValue = clampDouble( -+ (shrinkOffset - largeTitleThreshold - _kNavBarScrollUnderAnimationExtent) / _kNavBarScrollUnderAnimationExtent, -+ 0, -+ 1, -+ ); - - final _PersistentNavigationBar persistentNavigationBar = - _PersistentNavigationBar( -@@ -803,9 +877,21 @@ class _LargeTitleNavigationBarSliverDelegate - middleVisible: alwaysShowMiddle ? null : !showLargeTitle, - ); - -+ final Color effectiveBackgroundColor = Color.lerp( -+ CupertinoTheme.of(context).scaffoldBackgroundColor, -+ CupertinoDynamicColor.resolve(backgroundColor, context), -+ shrinkAnimationValue, -+ )!; -+ -+ final Border? effectiveBorder = border == null ? null : Border.lerp( -+ const Border(bottom: BorderSide(width: 0.0, color: Color(0x00000000))), -+ border, -+ shrinkAnimationValue, -+ ); -+ - final Widget navBar = _wrapWithBackground( -- border: border, -- backgroundColor: CupertinoDynamicColor.resolve(backgroundColor, context), -+ border: effectiveBorder, -+ backgroundColor: effectiveBackgroundColor, - brightness: brightness, - child: DefaultTextStyle( - style: CupertinoTheme.of(context).textTheme.textStyle, -@@ -875,11 +961,11 @@ class _LargeTitleNavigationBarSliverDelegate - // needs to wrap the top level RenderBox rather than a RenderSliver. - child: _TransitionableNavigationBar( - componentsKeys: keys, -- backgroundColor: CupertinoDynamicColor.resolve(backgroundColor, context), -+ backgroundColor: effectiveBackgroundColor, - backButtonTextStyle: CupertinoTheme.of(context).textTheme.navActionTextStyle, - titleTextStyle: CupertinoTheme.of(context).textTheme.navTitleTextStyle, - largeTitleTextStyle: CupertinoTheme.of(context).textTheme.navLargeTitleTextStyle, -- border: border, -+ border: effectiveBorder, - hasUserMiddle: userMiddle != null && (alwaysShowMiddle || !showLargeTitle), - largeExpanded: showLargeTitle, - child: navBar, -@@ -1716,6 +1802,7 @@ class _NavigationBarTransition extends StatelessWidget { - AnimatedBuilder( - animation: animation, - builder: (BuildContext context, Widget? child) { -+ - return _wrapWithBackground( - // Don't update the system status bar color mid-flight. - updateSystemUiOverlay: false, -diff --git a/packages/flutter/lib/src/cupertino/page_scaffold.dart b/packages/flutter/lib/src/cupertino/page_scaffold.dart -index 7eec24d908..4bc5fbc87a 100644 ---- a/packages/flutter/lib/src/cupertino/page_scaffold.dart -+++ b/packages/flutter/lib/src/cupertino/page_scaffold.dart -@@ -165,7 +165,7 @@ class _CupertinoPageScaffoldState extends State { - ); - } - -- return DecoratedBox( -+ final content = DecoratedBox( - decoration: BoxDecoration( - color: CupertinoDynamicColor.maybeResolve(widget.backgroundColor, context) - ?? CupertinoTheme.of(context).scaffoldBackgroundColor, -@@ -198,6 +198,8 @@ class _CupertinoPageScaffoldState extends State { - ], - ), - ); -+ -+ return ScrollNotificationObserver(child: content); - } - } - From 42256db03ff7b724d91a4b13e10db4b61d4a42c7 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Fri, 15 Nov 2024 14:20:07 +0100 Subject: [PATCH 696/979] feat: play sound when new game is started --- lib/src/model/game/game_controller.dart | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/src/model/game/game_controller.dart b/lib/src/model/game/game_controller.dart index d2a4ac3791..2b67a83e77 100644 --- a/lib/src/model/game/game_controller.dart +++ b/lib/src/model/game/game_controller.dart @@ -113,6 +113,13 @@ class GameController extends _$GameController { _socketEventVersion = fullEvent.socketEventVersion; + // Play "dong" sound when this is a new game and we're playing it (not spectating) + final isMyGame = game.youAre != null; + final noMovePlayed = game.steps.length == 1; + if (isMyGame && noMovePlayed && game.status == GameStatus.started) { + ref.read(soundServiceProvider).play(Sound.dong); + } + if (game.playable) { _appLifecycleListener = AppLifecycleListener( onResume: () { From 8f72e80036a75ae17730c5e309cf566a8f4061e8 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 19 Nov 2024 13:03:01 +0100 Subject: [PATCH 697/979] Improve board settings Add a new settings to change the drag target. Revamp in game and board setttings screens. Playing with a mouse instead of a touch device now come with better defaults. Closes #977 Closes #1034 --- lib/src/model/settings/board_preferences.dart | 14 ++ lib/src/view/game/game_common_widgets.dart | 2 + lib/src/view/game/game_settings.dart | 68 ++---- lib/src/view/puzzle/puzzle_screen.dart | 5 + .../view/puzzle/puzzle_settings_screen.dart | 48 +---- .../view/settings/board_settings_screen.dart | 199 ++++++++++++++---- .../piece_shift_method_settings_screen.dart | 79 ------- lib/src/view/settings/theme_screen.dart | 13 +- pubspec.lock | 4 +- pubspec.yaml | 2 +- 10 files changed, 222 insertions(+), 212 deletions(-) delete mode 100644 lib/src/view/settings/piece_shift_method_settings_screen.dart diff --git a/lib/src/model/settings/board_preferences.dart b/lib/src/model/settings/board_preferences.dart index cc124d7bd2..c5c1767aea 100644 --- a/lib/src/model/settings/board_preferences.dart +++ b/lib/src/model/settings/board_preferences.dart @@ -80,6 +80,10 @@ class BoardPreferences extends _$BoardPreferences ); } + Future setDragTargetKind(DragTargetKind dragTargetKind) { + return save(state.copyWith(dragTargetKind: dragTargetKind)); + } + Future toggleShowMaterialDifference() { return save( state.copyWith(showMaterialDifference: !state.showMaterialDifference), @@ -120,6 +124,8 @@ class BoardPrefs with _$BoardPrefs implements Serializable { /// Whether to enable shape drawings on the board for games and puzzles. @JsonKey(defaultValue: true) required bool enableShapeDrawings, @JsonKey(defaultValue: true) required bool magnifyDraggedPiece, + @JsonKey(defaultValue: DragTargetKind.circle) + required DragTargetKind dragTargetKind, @JsonKey( defaultValue: ShapeColor.green, unknownEnumValue: ShapeColor.green, @@ -141,6 +147,7 @@ class BoardPrefs with _$BoardPrefs implements Serializable { pieceShiftMethod: PieceShiftMethod.either, enableShapeDrawings: true, magnifyDraggedPiece: true, + dragTargetKind: DragTargetKind.circle, shapeColor: ShapeColor.green, showBorder: false, ); @@ -161,6 +168,7 @@ class BoardPrefs with _$BoardPrefs implements Serializable { animationDuration: pieceAnimationDuration, dragFeedbackScale: magnifyDraggedPiece ? 2.0 : 1.0, dragFeedbackOffset: Offset(0.0, magnifyDraggedPiece ? -1.0 : 0.0), + dragTargetKind: dragTargetKind, pieceShiftMethod: pieceShiftMethod, drawShape: DrawShapeOptions( enable: enableShapeDrawings, @@ -300,3 +308,9 @@ enum BoardTheme { errorBuilder: (context, o, st) => const SizedBox.shrink(), ); } + +String dragTargetKindLabel(DragTargetKind kind) => switch (kind) { + DragTargetKind.circle => 'Circle', + DragTargetKind.square => 'Square', + DragTargetKind.none => 'None', + }; diff --git a/lib/src/view/game/game_common_widgets.dart b/lib/src/view/game/game_common_widgets.dart index 8d3718cf57..2aed60f0dd 100644 --- a/lib/src/view/game/game_common_widgets.dart +++ b/lib/src/view/game/game_common_widgets.dart @@ -12,6 +12,7 @@ import 'package:lichess_mobile/src/model/lobby/game_seek.dart'; import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/share.dart'; +import 'package:lichess_mobile/src/view/settings/toggle_sound_button.dart'; import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; @@ -64,6 +65,7 @@ class GameAppBar extends ConsumerWidget { ? _ChallengeGameTitle(challenge: challenge!) : const SizedBox.shrink(), actions: [ + const ToggleSoundButton(), if (id != null) AppBarIconButton( onPressed: () => showAdaptiveBottomSheet( diff --git a/lib/src/view/game/game_settings.dart b/lib/src/view/game/game_settings.dart index 3785e3333a..3263b775e3 100644 --- a/lib/src/view/game/game_settings.dart +++ b/lib/src/view/game/game_settings.dart @@ -1,14 +1,14 @@ import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/account/account_preferences.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/game/game_controller.dart'; import 'package:lichess_mobile/src/model/game/game_preferences.dart'; -import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; -import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/view/settings/board_settings_screen.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; +import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/settings.dart'; import 'game_screen_providers.dart'; @@ -20,31 +20,11 @@ class GameSettings extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final isSoundEnabled = ref.watch( - generalPreferencesProvider.select( - (prefs) => prefs.isSoundEnabled, - ), - ); - final boardPrefs = ref.watch(boardPreferencesProvider); final gamePrefs = ref.watch(gamePreferencesProvider); final userPrefsAsync = ref.watch(userGamePrefsProvider(id)); return BottomSheetScrollableContainer( children: [ - SwitchSettingTile( - title: Text(context.l10n.sound), - value: isSoundEnabled, - onChanged: (value) { - ref.read(generalPreferencesProvider.notifier).toggleSoundEnabled(); - }, - ), - SwitchSettingTile( - title: Text(context.l10n.mobileSettingsHapticFeedback), - value: boardPrefs.hapticFeedback, - onChanged: (value) { - ref.read(boardPreferencesProvider.notifier).toggleHapticFeedback(); - }, - ), ...userPrefsAsync.maybeWhen( data: (data) { return [ @@ -85,39 +65,15 @@ class GameSettings extends ConsumerWidget { }, orElse: () => [], ), - SwitchSettingTile( - // TODO: Add l10n - title: const Text('Shape drawing'), - subtitle: const Text( - 'Draw shapes using two fingers.', - maxLines: 5, - textAlign: TextAlign.justify, - ), - value: boardPrefs.enableShapeDrawings, - onChanged: (value) { - ref - .read(boardPreferencesProvider.notifier) - .toggleEnableShapeDrawings(); - }, - ), - SwitchSettingTile( - title: Text( - context.l10n.preferencesPieceAnimation, - ), - value: boardPrefs.pieceAnimation, - onChanged: (value) { - ref.read(boardPreferencesProvider.notifier).togglePieceAnimation(); - }, - ), - SwitchSettingTile( - title: Text( - context.l10n.preferencesMaterialDifference, - ), - value: boardPrefs.showMaterialDifference, - onChanged: (value) { - ref - .read(boardPreferencesProvider.notifier) - .toggleShowMaterialDifference(); + PlatformListTile( + // TODO translate + title: const Text('Board settings'), + trailing: const Icon(CupertinoIcons.chevron_right), + onTap: () { + pushPlatformRoute( + context, + screen: const BoardSettingsScreen(), + ); }, ), SwitchSettingTile( diff --git a/lib/src/view/puzzle/puzzle_screen.dart b/lib/src/view/puzzle/puzzle_screen.dart index 6f51d20735..ef8681c596 100644 --- a/lib/src/view/puzzle/puzzle_screen.dart +++ b/lib/src/view/puzzle/puzzle_screen.dart @@ -31,6 +31,7 @@ import 'package:lichess_mobile/src/view/account/rating_pref_aware.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; import 'package:lichess_mobile/src/view/game/archived_game_screen.dart'; import 'package:lichess_mobile/src/view/puzzle/puzzle_settings_screen.dart'; +import 'package:lichess_mobile/src/view/settings/toggle_sound_button.dart'; import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/adaptive_choice_picker.dart'; @@ -91,6 +92,7 @@ class _PuzzleScreenState extends ConsumerState with RouteAware { child: PlatformScaffold( appBar: PlatformAppBar( actions: const [ + ToggleSoundButton(), _PuzzleSettingsButton(), ], title: _Title(angle: widget.angle), @@ -604,6 +606,9 @@ class _PuzzleSettingsButton extends StatelessWidget { isDismissible: true, isScrollControlled: true, showDragHandle: true, + constraints: BoxConstraints( + minHeight: MediaQuery.sizeOf(context).height * 0.5, + ), builder: (_) => const PuzzleSettingsScreen(), ), semanticsLabel: context.l10n.settingsSettings, diff --git a/lib/src/view/puzzle/puzzle_settings_screen.dart b/lib/src/view/puzzle/puzzle_settings_screen.dart index 7f59b61d24..3a6256aa6b 100644 --- a/lib/src/view/puzzle/puzzle_settings_screen.dart +++ b/lib/src/view/puzzle/puzzle_settings_screen.dart @@ -1,11 +1,11 @@ import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_preferences.dart'; -import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; -import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/view/settings/board_settings_screen.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; +import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/settings.dart'; class PuzzleSettingsScreen extends ConsumerWidget { @@ -13,23 +13,11 @@ class PuzzleSettingsScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final isSoundEnabled = ref.watch( - generalPreferencesProvider.select((pref) => pref.isSoundEnabled), - ); final autoNext = ref.watch( puzzlePreferencesProvider.select((value) => value.autoNext), ); - final boardPrefs = ref.watch(boardPreferencesProvider); - return BottomSheetScrollableContainer( children: [ - SwitchSettingTile( - title: Text(context.l10n.sound), - value: isSoundEnabled, - onChanged: (value) { - ref.read(generalPreferencesProvider.notifier).toggleSoundEnabled(); - }, - ), SwitchSettingTile( title: Text(context.l10n.puzzleJumpToNextPuzzleImmediately), value: autoNext, @@ -37,28 +25,14 @@ class PuzzleSettingsScreen extends ConsumerWidget { ref.read(puzzlePreferencesProvider.notifier).setAutoNext(value); }, ), - SwitchSettingTile( - // TODO: Add l10n - title: const Text('Shape drawing'), - subtitle: const Text( - 'Draw shapes using two fingers.', - maxLines: 5, - textAlign: TextAlign.justify, - ), - value: boardPrefs.enableShapeDrawings, - onChanged: (value) { - ref - .read(boardPreferencesProvider.notifier) - .toggleEnableShapeDrawings(); - }, - ), - SwitchSettingTile( - title: Text( - context.l10n.preferencesPieceAnimation, - ), - value: boardPrefs.pieceAnimation, - onChanged: (value) { - ref.read(boardPreferencesProvider.notifier).togglePieceAnimation(); + PlatformListTile( + title: const Text('Board settings'), + trailing: const Icon(CupertinoIcons.chevron_right), + onTap: () { + pushPlatformRoute( + context, + screen: const BoardSettingsScreen(), + ); }, ), ], diff --git a/lib/src/view/settings/board_settings_screen.dart b/lib/src/view/settings/board_settings_screen.dart index eefd82f4f2..17998287e4 100644 --- a/lib/src/view/settings/board_settings_screen.dart +++ b/lib/src/view/settings/board_settings_screen.dart @@ -3,11 +3,11 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/utils/system.dart'; -import 'package:lichess_mobile/src/view/settings/piece_shift_method_settings_screen.dart'; import 'package:lichess_mobile/src/widgets/adaptive_choice_picker.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; @@ -52,6 +52,7 @@ class _Body extends ConsumerWidget { child: ListView( children: [ ListSection( + header: SettingsSectionTitle(context.l10n.preferencesGameBehavior), hasLeading: false, showDivider: false, children: [ @@ -87,29 +88,80 @@ class _Body extends ConsumerWidget { }, ), SwitchSettingTile( - // TODO: Add l10n - title: const Text('Shape drawing'), + title: Text(context.l10n.mobilePrefMagnifyDraggedPiece), + value: boardPrefs.magnifyDraggedPiece, + onChanged: (value) { + ref + .read(boardPreferencesProvider.notifier) + .toggleMagnifyDraggedPiece(); + }, + ), + SettingsListTile( + // TODO translate + settingsLabel: const Text('Drag target'), + explanation: + // TODO translate + 'How the target square is highlighted when dragging a piece.', + settingsValue: dragTargetKindLabel(boardPrefs.dragTargetKind), + onTap: () { + if (Theme.of(context).platform == TargetPlatform.android) { + showChoicePicker( + context, + choices: DragTargetKind.values, + selectedItem: boardPrefs.dragTargetKind, + labelBuilder: (t) => Text(dragTargetKindLabel(t)), + onSelectedItemChanged: (DragTargetKind? value) { + ref + .read(boardPreferencesProvider.notifier) + .setDragTargetKind( + value ?? DragTargetKind.circle, + ); + }, + ); + } else { + pushPlatformRoute( + context, + title: 'Dragged piece target', + builder: (context) => + const DragTargetKindSettingsScreen(), + ); + } + }, + ), + SwitchSettingTile( + // TODO translate + title: const Text('Touch feedback'), + value: boardPrefs.hapticFeedback, subtitle: const Text( - 'Draw shapes using two fingers on game and puzzle boards.', + // TODO translate + 'Vibrate when moving pieces or capturing them.', maxLines: 5, textAlign: TextAlign.justify, ), - value: boardPrefs.enableShapeDrawings, onChanged: (value) { ref .read(boardPreferencesProvider.notifier) - .toggleEnableShapeDrawings(); + .toggleHapticFeedback(); }, ), SwitchSettingTile( - title: Text(context.l10n.mobileSettingsHapticFeedback), - value: boardPrefs.hapticFeedback, + title: Text( + context.l10n.preferencesPieceAnimation, + ), + value: boardPrefs.pieceAnimation, onChanged: (value) { ref .read(boardPreferencesProvider.notifier) - .toggleHapticFeedback(); + .togglePieceAnimation(); }, ), + ], + ), + ListSection( + header: SettingsSectionTitle(context.l10n.preferencesDisplay), + hasLeading: false, + showDivider: false, + children: [ if (Theme.of(context).platform == TargetPlatform.android && !isTabletOrLarger(context)) androidVersionAsync.maybeWhen( @@ -155,44 +207,29 @@ class _Body extends ConsumerWidget { ), SwitchSettingTile( title: Text( - context.l10n.preferencesBoardCoordinates, - ), - value: boardPrefs.coordinates, - onChanged: (value) { - ref - .read(boardPreferencesProvider.notifier) - .toggleCoordinates(); - }, - ), - SwitchSettingTile( - title: Text(context.l10n.mobilePrefMagnifyDraggedPiece), - value: boardPrefs.magnifyDraggedPiece, - onChanged: (value) { - ref - .read(boardPreferencesProvider.notifier) - .toggleMagnifyDraggedPiece(); - }, - ), - SwitchSettingTile( - title: Text( - context.l10n.preferencesPieceAnimation, + context.l10n.preferencesMaterialDifference, ), - value: boardPrefs.pieceAnimation, + value: boardPrefs.showMaterialDifference, onChanged: (value) { ref .read(boardPreferencesProvider.notifier) - .togglePieceAnimation(); + .toggleShowMaterialDifference(); }, ), SwitchSettingTile( - title: Text( - context.l10n.preferencesMaterialDifference, + // TODO: Add l10n + title: const Text('Shape drawing'), + subtitle: const Text( + // TODO: translate + 'Draw shapes using two fingers: maintain one finger on an empty square and drag another finger to draw a shape.', + maxLines: 5, + textAlign: TextAlign.justify, ), - value: boardPrefs.showMaterialDifference, + value: boardPrefs.enableShapeDrawings, onChanged: (value) { ref .read(boardPreferencesProvider.notifier) - .toggleShowMaterialDifference(); + .toggleEnableShapeDrawings(); }, ), ], @@ -202,3 +239,93 @@ class _Body extends ConsumerWidget { ); } } + +class PieceShiftMethodSettingsScreen extends ConsumerWidget { + const PieceShiftMethodSettingsScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final pieceShiftMethod = ref.watch( + boardPreferencesProvider.select( + (state) => state.pieceShiftMethod, + ), + ); + + void onChanged(PieceShiftMethod? value) { + ref + .read(boardPreferencesProvider.notifier) + .setPieceShiftMethod(value ?? PieceShiftMethod.either); + } + + return CupertinoPageScaffold( + navigationBar: const CupertinoNavigationBar(), + child: SafeArea( + child: ListView( + children: [ + ChoicePicker( + notchedTile: true, + choices: PieceShiftMethod.values, + selectedItem: pieceShiftMethod, + titleBuilder: (t) => Text(pieceShiftMethodl10n(context, t)), + onSelectedItemChanged: onChanged, + ), + ], + ), + ), + ); + } +} + +class DragTargetKindSettingsScreen extends ConsumerWidget { + const DragTargetKindSettingsScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final dragTargetKind = ref.watch( + boardPreferencesProvider.select( + (state) => state.dragTargetKind, + ), + ); + + void onChanged(DragTargetKind? value) { + ref + .read(boardPreferencesProvider.notifier) + .setDragTargetKind(value ?? DragTargetKind.circle); + } + + return CupertinoPageScaffold( + navigationBar: const CupertinoNavigationBar(), + child: SafeArea( + child: ListView( + children: [ + Padding( + padding: + Styles.horizontalBodyPadding.add(Styles.sectionTopPadding), + child: const Text( + 'How the target square is highlighted when dragging a piece.', + ), + ), + ChoicePicker( + notchedTile: true, + choices: DragTargetKind.values, + selectedItem: dragTargetKind, + titleBuilder: (t) => Text(dragTargetKindLabel(t)), + onSelectedItemChanged: onChanged, + ), + ], + ), + ), + ); + } +} + +String pieceShiftMethodl10n( + BuildContext context, + PieceShiftMethod pieceShiftMethod, +) => + switch (pieceShiftMethod) { + // TODO add this to mobile translations + PieceShiftMethod.either => 'Either tap or drag', + PieceShiftMethod.drag => context.l10n.preferencesDragPiece, + PieceShiftMethod.tapTwoSquares => 'Tap two squares', + }; diff --git a/lib/src/view/settings/piece_shift_method_settings_screen.dart b/lib/src/view/settings/piece_shift_method_settings_screen.dart deleted file mode 100644 index 4edc23ba8d..0000000000 --- a/lib/src/view/settings/piece_shift_method_settings_screen.dart +++ /dev/null @@ -1,79 +0,0 @@ -import 'package:chessground/chessground.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; -import 'package:lichess_mobile/src/utils/l10n_context.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; -import 'package:lichess_mobile/src/widgets/settings.dart'; - -class PieceShiftMethodSettingsScreen extends StatelessWidget { - const PieceShiftMethodSettingsScreen({super.key}); - - @override - Widget build(BuildContext context) { - return PlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, - ); - } - - Widget _androidBuilder(BuildContext context) { - return Scaffold( - appBar: AppBar(title: Text(context.l10n.preferencesHowDoYouMovePieces)), - body: _Body(), - ); - } - - Widget _iosBuilder(BuildContext context) { - return CupertinoPageScaffold( - navigationBar: const CupertinoNavigationBar(), - child: _Body(), - ); - } -} - -String pieceShiftMethodl10n( - BuildContext context, - PieceShiftMethod pieceShiftMethod, -) => - switch (pieceShiftMethod) { - // This is called 'Either' in the Web UI, but in the app we might display this string - // without having the other values as context, so we need to be more explicit. - // TODO add this to mobile translations - PieceShiftMethod.either => 'Either click or drag', - PieceShiftMethod.drag => context.l10n.preferencesDragPiece, - // TODO This string uses 'click', we might want to use 'tap' instead in a mobile-specific translation - PieceShiftMethod.tapTwoSquares => context.l10n.preferencesClickTwoSquares, - }; - -class _Body extends ConsumerWidget { - @override - Widget build(BuildContext context, WidgetRef ref) { - final pieceShiftMethod = ref.watch( - boardPreferencesProvider.select( - (state) => state.pieceShiftMethod, - ), - ); - - void onChanged(PieceShiftMethod? value) { - ref - .read(boardPreferencesProvider.notifier) - .setPieceShiftMethod(value ?? PieceShiftMethod.either); - } - - return SafeArea( - child: ListView( - children: [ - ChoicePicker( - notchedTile: true, - choices: PieceShiftMethod.values, - selectedItem: pieceShiftMethod, - titleBuilder: (t) => Text(pieceShiftMethodl10n(context, t)), - onSelectedItemChanged: onChanged, - ), - ], - ), - ); - } -} diff --git a/lib/src/view/settings/theme_screen.dart b/lib/src/view/settings/theme_screen.dart index e0db758b15..8f34cc8d0d 100644 --- a/lib/src/view/settings/theme_screen.dart +++ b/lib/src/view/settings/theme_screen.dart @@ -80,7 +80,6 @@ class _Body extends ConsumerWidget { ), }.lock, settings: boardPrefs.toBoardSettings().copyWith( - enableCoordinates: true, borderRadius: const BorderRadius.all(Radius.circular(4.0)), boxShadow: boardShadows, @@ -151,6 +150,18 @@ class _Body extends ConsumerWidget { ); }, ), + SwitchSettingTile( + leading: const Icon(Icons.location_on), + title: Text( + context.l10n.preferencesBoardCoordinates, + ), + value: boardPrefs.coordinates, + onChanged: (value) { + ref + .read(boardPreferencesProvider.notifier) + .toggleCoordinates(); + }, + ), SwitchSettingTile( // TODO translate leading: const Icon(Icons.border_outer), diff --git a/pubspec.lock b/pubspec.lock index f3fe6bdd8e..089cbf5f20 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -202,10 +202,10 @@ packages: dependency: "direct main" description: name: chessground - sha256: e2fba2e89f69a41fc77ef034a06fd087f14fdd5909b7d98bffb6e182d56d2a3a + sha256: "118e11871baa08022be827087bc90b82f0bda535d504278787f9717ad949132b" url: "https://pub.dev" source: hosted - version: "6.0.0" + version: "6.1.0" ci: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 050bccf49d..5c3df06d40 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,7 +14,7 @@ dependencies: async: ^2.10.0 auto_size_text: ^3.0.0 cached_network_image: ^3.2.2 - chessground: ^6.0.0 + chessground: ^6.1.0 clock: ^1.1.1 collection: ^1.17.0 connectivity_plus: ^6.0.2 From 3aef47d4a706fd592f7030d8ae73c62a36aebc1b Mon Sep 17 00:00:00 2001 From: Jimima Date: Tue, 19 Nov 2024 13:38:15 +0000 Subject: [PATCH 698/979] Animated version --- lib/src/view/game/game_result_dialog.dart | 98 ++++++++++++++++------- 1 file changed, 68 insertions(+), 30 deletions(-) diff --git a/lib/src/view/game/game_result_dialog.dart b/lib/src/view/game/game_result_dialog.dart index 79ecb4a1ad..948feed62d 100644 --- a/lib/src/view/game/game_result_dialog.dart +++ b/lib/src/view/game/game_result_dialog.dart @@ -89,6 +89,7 @@ class _GameEndDialogState extends ConsumerState { Widget build(BuildContext context) { final ctrlProvider = gameControllerProvider(widget.id); final gameState = ref.watch(ctrlProvider).requireValue; + final ValueNotifier animationFinished = ValueNotifier(false); final content = Column( mainAxisSize: MainAxisSize.min, @@ -98,6 +99,38 @@ class _GameEndDialogState extends ConsumerState { padding: const EdgeInsets.only(bottom: 16.0), child: GameResult(game: gameState.game), ), + if (gameState.game.white.analysis != null) + Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: PlayerSummary(game: gameState.game), + ), + AnimatedContainer( + duration: const Duration(milliseconds: 400), + onEnd: () { + animationFinished.value = true; + }, + height: gameState.game.opponent?.offeringRematch ?? false + ? 50 + : 0, //TODO handle nulls better? + child: ValueListenableBuilder( + valueListenable: animationFinished, + builder: (context, value, child) { + return Visibility( + maintainAnimation: true, + maintainSize: true, + maintainState: true, + visible: animationFinished.value, + child: const Padding( + padding: EdgeInsets.only(bottom: 16.0), + child: Text( + 'Your opponent has offered a rematch', + textAlign: TextAlign.center, + ), + ), + ); + }, + ), + ), if (gameState.game.me?.offeringRematch == true) SecondaryButton( semanticsLabel: context.l10n.cancelRematchOffer, @@ -108,37 +141,42 @@ class _GameEndDialogState extends ConsumerState { context.l10n.cancelRematchOffer, textAlign: TextAlign.center, ), - ), - Visibility( - maintainState: true, - visible: gameState.game.opponent?.offeringRematch ?? false, - child: AnimatedOpacity( - duration: const Duration(milliseconds: 300), - opacity: gameState.game.opponent?.offeringRematch ?? false ? 1 : 0, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - FatButton( - semanticsLabel: context.l10n.rematch, - child: const Text('Accept rematch'), - onPressed: () { - ref.read(ctrlProvider.notifier).proposeOrAcceptRematch(); - }, - ), - SecondaryButton( - semanticsLabel: context.l10n.rematch, - child: const Text('Decline'), - onPressed: () { - ref.read(ctrlProvider.notifier).declineRematch(); - }, + ) + else if (gameState.game.opponent?.offeringRematch == true) + ValueListenableBuilder( + valueListenable: animationFinished, + builder: (BuildContext context, value, Widget? child) { + return Visibility( + maintainAnimation: true, + maintainSize: true, + maintainState: true, + visible: animationFinished.value, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Text("Rematch offered"), + FatButton( + semanticsLabel: context.l10n.rematch, + child: const Text('Accept rematch'), + onPressed: () { + ref + .read(ctrlProvider.notifier) + .proposeOrAcceptRematch(); + }, + ), + SecondaryButton( + semanticsLabel: context.l10n.rematch, + child: const Text('Decline'), + onPressed: () { + ref.read(ctrlProvider.notifier).declineRematch(); + }, + ), + ], ), - ], - ), - ), - ), - if (gameState.canOfferRematch && - !(gameState.game.opponent?.offeringRematch ?? false) && - !(gameState.game.me?.offeringRematch ?? false)) + ); + }, + ) + else if (gameState.canOfferRematch) SecondaryButton( semanticsLabel: context.l10n.rematch, onPressed: _activateButtons && From bf67730fbeeb3fc769341f85d9f2d2a02423e0a7 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 19 Nov 2024 14:35:43 +0100 Subject: [PATCH 699/979] Round rating MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #1154 --- lib/src/widgets/rating.dart | 4 +--- test/view/user/perf_stats_screen_test.dart | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/src/widgets/rating.dart b/lib/src/widgets/rating.dart index 69df2388c9..fae2ea6472 100644 --- a/lib/src/widgets/rating.dart +++ b/lib/src/widgets/rating.dart @@ -19,10 +19,8 @@ class RatingWidget extends StatelessWidget { @override Widget build(BuildContext context) { - final ratingStr = - rating is double ? rating.toStringAsFixed(2) : rating.toString(); return Text( - '$ratingStr${provisional == true || deviation > kProvisionalDeviation ? '?' : ''}', + '${rating.round()}${provisional == true || deviation > kProvisionalDeviation ? '?' : ''}', style: style, ); } diff --git a/test/view/user/perf_stats_screen_test.dart b/test/view/user/perf_stats_screen_test.dart index a2a208f8e4..33013947fd 100644 --- a/test/view/user/perf_stats_screen_test.dart +++ b/test/view/user/perf_stats_screen_test.dart @@ -90,7 +90,7 @@ void main() { ]; // rating - expect(find.text('1500.42'), findsOneWidget); + expect(find.text('1500'), findsOneWidget); for (final val in requiredStatsValues) { expect( From 73662b1d0f6dc55d024ec007142ad83e0c8449b5 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 19 Nov 2024 15:03:08 +0100 Subject: [PATCH 700/979] Update dependencies --- ios/Podfile.lock | 70 +++++++++++++------------- pubspec.lock | 124 +++++++++++++++++++++++------------------------ pubspec.yaml | 2 +- 3 files changed, 98 insertions(+), 98 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 418acdefd5..b4b0ae8e9c 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -15,35 +15,35 @@ PODS: - FlutterMacOS - device_info_plus (0.0.1): - Flutter - - Firebase/CoreOnly (11.2.0): - - FirebaseCore (= 11.2.0) - - Firebase/Crashlytics (11.2.0): + - Firebase/CoreOnly (11.4.0): + - FirebaseCore (= 11.4.0) + - Firebase/Crashlytics (11.4.0): - Firebase/CoreOnly - - FirebaseCrashlytics (~> 11.2.0) - - Firebase/Messaging (11.2.0): + - FirebaseCrashlytics (~> 11.4.0) + - Firebase/Messaging (11.4.0): - Firebase/CoreOnly - - FirebaseMessaging (~> 11.2.0) - - firebase_core (3.6.0): - - Firebase/CoreOnly (= 11.2.0) + - FirebaseMessaging (~> 11.4.0) + - firebase_core (3.8.0): + - Firebase/CoreOnly (= 11.4.0) - Flutter - - firebase_crashlytics (4.1.3): - - Firebase/Crashlytics (= 11.2.0) + - firebase_crashlytics (4.1.5): + - Firebase/Crashlytics (= 11.4.0) - firebase_core - Flutter - - firebase_messaging (15.1.3): - - Firebase/Messaging (= 11.2.0) + - firebase_messaging (15.1.5): + - Firebase/Messaging (= 11.4.0) - firebase_core - Flutter - - FirebaseCore (11.2.0): + - FirebaseCore (11.4.0): - FirebaseCoreInternal (~> 11.0) - GoogleUtilities/Environment (~> 8.0) - GoogleUtilities/Logger (~> 8.0) - FirebaseCoreExtension (11.4.1): - FirebaseCore (~> 11.0) - - FirebaseCoreInternal (11.4.2): + - FirebaseCoreInternal (11.5.0): - "GoogleUtilities/NSData+zlib (~> 8.0)" - - FirebaseCrashlytics (11.2.0): - - FirebaseCore (~> 11.0) + - FirebaseCrashlytics (11.4.0): + - FirebaseCore (~> 11.4) - FirebaseInstallations (~> 11.0) - FirebaseRemoteConfigInterop (~> 11.0) - FirebaseSessions (~> 11.0) @@ -56,7 +56,7 @@ PODS: - GoogleUtilities/Environment (~> 8.0) - GoogleUtilities/UserDefaults (~> 8.0) - PromisesObjC (~> 2.4) - - FirebaseMessaging (11.2.0): + - FirebaseMessaging (11.4.0): - FirebaseCore (~> 11.0) - FirebaseInstallations (~> 11.0) - GoogleDataTransport (~> 10.0) @@ -65,10 +65,10 @@ PODS: - GoogleUtilities/Reachability (~> 8.0) - GoogleUtilities/UserDefaults (~> 8.0) - nanopb (~> 3.30910.0) - - FirebaseRemoteConfigInterop (11.4.0) - - FirebaseSessions (11.3.0): - - FirebaseCore (~> 11.0) - - FirebaseCoreExtension (~> 11.0) + - FirebaseRemoteConfigInterop (11.5.0) + - FirebaseSessions (11.4.0): + - FirebaseCore (~> 11.4) + - FirebaseCoreExtension (~> 11.4) - FirebaseInstallations (~> 11.0) - GoogleDataTransport (~> 10.0) - GoogleUtilities/Environment (~> 8.0) @@ -81,7 +81,7 @@ PODS: - Flutter - flutter_local_notifications (0.0.1): - Flutter - - flutter_native_splash (0.0.1): + - flutter_native_splash (2.4.3): - Flutter - flutter_secure_storage (6.0.0): - Flutter @@ -238,22 +238,22 @@ SPEC CHECKSUMS: connectivity_plus: 4c41c08fc6d7c91f63bc7aec70ffe3730b04f563 cupertino_http: 947a233f40cfea55167a49f2facc18434ea117ba device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342 - Firebase: 98e6bf5278170668a7983e12971a66b2cd57fc8c - firebase_core: 2bedc3136ec7c7b8561c6123ed0239387b53f2af - firebase_crashlytics: 37d104d457b51760b48504a93a12b3bf70995d77 - firebase_messaging: 15d114e1a41fc31e4fbabcd48d765a19eec94a38 - FirebaseCore: a282032ae9295c795714ded2ec9c522fc237f8da + Firebase: cf1b19f21410b029b6786a54e9764a0cacad3c99 + firebase_core: 9efc3ecf689cdbc90f13f4dc58108c83ea46b266 + firebase_crashlytics: 72a8b504422ba8bb435a7a0c0a9341320cbcbe29 + firebase_messaging: 6bf60adb4b33a848d135e16bc363fb4924f98fba + FirebaseCore: e0510f1523bc0eb21653cac00792e1e2bd6f1771 FirebaseCoreExtension: f1bc67a4702931a7caa097d8e4ac0a1b0d16720e - FirebaseCoreInternal: 35731192cab10797b88411be84940d2beb33a238 - FirebaseCrashlytics: cfc69af5b53565dc6a5e563788809b5778ac4eac + FirebaseCoreInternal: f47dd28ae7782e6a4738aad3106071a8fe0af604 + FirebaseCrashlytics: 41bbdd2b514a8523cede0c217aee6ef7ecf38401 FirebaseInstallations: 6ef4a1c7eb2a61ee1f74727d7f6ce2e72acf1414 - FirebaseMessaging: c9ec7b90c399c7a6100297e9d16f8a27fc7f7152 - FirebaseRemoteConfigInterop: e76f46ffa4d6a65e273d4dfebb6a79e588cec136 - FirebaseSessions: 655ff17f3cc1a635cbdc2d69b953878001f9e25b + FirebaseMessaging: f8a160d99c2c2e5babbbcc90c4a3e15db036aee2 + FirebaseRemoteConfigInterop: 7a7aebb9342d53913a5c890efa88e289d9e5c1bc + FirebaseSessions: 3f56f177d9e53a85021d16b31f9a111849d1dd8b Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_appauth: 408f4cda69a4ad59bdf696e04cd9e13e1449b44e - flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086 - flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778 + flutter_local_notifications: df98d66e515e1ca797af436137b4459b160ad8c9 + flutter_native_splash: e8a1e01082d97a8099d973f919f57904c925008a flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12 GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d @@ -266,7 +266,7 @@ SPEC CHECKSUMS: share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 sound_effect: 5280cfa89d4a576032186f15600dc948ca6d39ce - sqflite_darwin: a553b1fd6fe66f53bbb0fe5b4f5bab93f08d7a13 + sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d stockfish: d00cf6b95579f1d7032cbfd8e4fe874972fe2ff9 url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1 diff --git a/pubspec.lock b/pubspec.lock index 089cbf5f20..f0c96ddc96 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -13,10 +13,10 @@ packages: dependency: transitive description: name: _flutterfire_internals - sha256: "5534e701a2c505fed1f0799e652dd6ae23bd4d2c4cf797220e5ced5764a7c1c2" + sha256: "71c01c1998c40b3af1944ad0a5f374b4e6fef7f3d2df487f3970dbeadaeb25a1" url: "https://pub.dev" source: hosted - version: "1.3.44" + version: "1.3.46" _macros: dependency: transitive description: dart @@ -306,10 +306,10 @@ packages: dependency: "direct main" description: name: cupertino_http - sha256: c13f43571ba579f3d96d959e72f6c716b2a5ff379b927dda0d7639d0bcd2c49c + sha256: "3de09415040ad1def7a1b1d52cde3b07d6b29837bf0e4f7b822ca5c805e2d872" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.0.1" cupertino_icons: dependency: "direct main" description: @@ -386,10 +386,10 @@ packages: dependency: "direct main" description: name: device_info_plus - sha256: c4af09051b4f0508f6c1dc0a5c085bf014d5c9a4a0678ce1799c2b4d716387a0 + sha256: f545ffbadee826f26f2e1a0f0cbd667ae9a6011cc0f77c0f8f00a969655e6e95 url: "https://pub.dev" source: hosted - version: "11.1.0" + version: "11.1.1" device_info_plus_platform_interface: dependency: transitive description: @@ -450,10 +450,10 @@ packages: dependency: "direct main" description: name: firebase_core - sha256: "51dfe2fbf3a984787a2e7b8592f2f05c986bfedd6fdacea3f9e0a7beb334de96" + sha256: "2438a75ad803e818ad3bd5df49137ee619c46b6fc7101f4dbc23da07305ce553" url: "https://pub.dev" source: hosted - version: "3.6.0" + version: "3.8.0" firebase_core_platform_interface: dependency: transitive description: @@ -474,42 +474,42 @@ packages: dependency: "direct main" description: name: firebase_crashlytics - sha256: "6899800fff1af819955aef740f18c4c8600f8b952a2a1ea97bc0872ebb257387" + sha256: "4e80ef22428dfecf609df8049419c7446c6e1d797d7f307cad3c7ab70e72ddc5" url: "https://pub.dev" source: hosted - version: "4.1.3" + version: "4.1.5" firebase_crashlytics_platform_interface: dependency: transitive description: name: firebase_crashlytics_platform_interface - sha256: "97c47b0a1779a3d4118416a3f0c6c564cc59ad89095e899893204d4b2ad08f4c" + sha256: "1104f428ec5249fff62016985719bb232ca91c4bde0d1a033af9b7d8b7451d70" url: "https://pub.dev" source: hosted - version: "3.6.44" + version: "3.6.46" firebase_messaging: dependency: "direct main" description: name: firebase_messaging - sha256: eb6e28a3a35deda61fe8634967c84215efc19133ba58d8e0fc6c9a2af2cba05e + sha256: "4d0968ecb860d7baa15a6e2af3469ec5b0d959e51c59ce84a52b0f7632a4aa5a" url: "https://pub.dev" source: hosted - version: "15.1.3" + version: "15.1.5" firebase_messaging_platform_interface: dependency: transitive description: name: firebase_messaging_platform_interface - sha256: b316c4ee10d93d32c033644207afc282d9b2b4372f3cf9c6022f3558b3873d2d + sha256: a2cb3e7d71d40b6612e2d4e0daa0ae759f6a9d07f693f904d14d22aadf70be10 url: "https://pub.dev" source: hosted - version: "4.5.46" + version: "4.5.48" firebase_messaging_web: dependency: transitive description: name: firebase_messaging_web - sha256: d7f0147a1a9fe4313168e20154a01fd5cf332898de1527d3930ff77b8c7f5387 + sha256: "1554e190f0cd9d6fe59f61ae0275ac12006fdb78b07669f1a260d1a9e6de3a1f" url: "https://pub.dev" source: hosted - version: "3.9.2" + version: "3.9.4" fixnum: dependency: transitive description: @@ -535,10 +535,10 @@ packages: dependency: "direct main" description: name: flutter_appauth - sha256: "6ab0e7fb2cb66db472a71c00e0f0d0888f186d308beaef4bba1a6113fa861096" + sha256: d3fa52069464affa3e382a8d8ce7d003601643c39a9de727b98e7a84fab7fd7e url: "https://pub.dev" source: hosted - version: "8.0.0+1" + version: "8.0.1" flutter_appauth_platform_interface: dependency: transitive description: @@ -583,26 +583,26 @@ packages: dependency: "direct main" description: name: flutter_local_notifications - sha256: "674173fd3c9eda9d4c8528da2ce0ea69f161577495a9cc835a2a4ecd7eadeb35" + sha256: ef41ae901e7529e52934feba19ed82827b11baa67336829564aeab3129460610 url: "https://pub.dev" source: hosted - version: "17.2.4" + version: "18.0.1" flutter_local_notifications_linux: dependency: transitive description: name: flutter_local_notifications_linux - sha256: c49bd06165cad9beeb79090b18cd1eb0296f4bf4b23b84426e37dd7c027fc3af + sha256: "8f685642876742c941b29c32030f6f4f6dacd0e4eaecb3efbb187d6a3812ca01" url: "https://pub.dev" source: hosted - version: "4.0.1" + version: "5.0.0" flutter_local_notifications_platform_interface: dependency: transitive description: name: flutter_local_notifications_platform_interface - sha256: "85f8d07fe708c1bdcf45037f2c0109753b26ae077e9d9e899d55971711a4ea66" + sha256: "6c5b83c86bf819cdb177a9247a3722067dd8cc6313827ce7c77a4b238a26fd52" url: "https://pub.dev" source: hosted - version: "7.2.0" + version: "8.0.0" flutter_localizations: dependency: "direct main" description: flutter @@ -612,10 +612,10 @@ packages: dependency: "direct main" description: name: flutter_native_splash - sha256: ee5c9bd2b74ea8676442fd4ab876b5d41681df49276488854d6c81a5377c0ef1 + sha256: "1152ab0067ca5a2ebeb862fe0a762057202cceb22b7e62692dcbabf6483891bb" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.3" flutter_riverpod: dependency: "direct main" description: @@ -693,10 +693,10 @@ packages: dependency: "direct main" description: name: flutter_svg - sha256: "7b4ca6cf3304575fe9c8ec64813c8d02ee41d2afe60bcfe0678bcb5375d596a2" + sha256: "578bd8c508144fdaffd4f77b8ef2d8c523602275cd697cc3db284dbd762ef4ce" url: "https://pub.dev" source: hosted - version: "2.0.10+1" + version: "2.0.14" flutter_test: dependency: "direct dev" description: flutter @@ -999,10 +999,10 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: df3eb3e0aed5c1107bb0fdb80a8e82e778114958b1c5ac5644fb1ac9cae8a998 + sha256: da8d9ac8c4b1df253d1a328b7bf01ae77ef132833479ab40763334db13b91cce url: "https://pub.dev" source: hosted - version: "8.1.0" + version: "8.1.1" package_info_plus_platform_interface: dependency: transitive description: @@ -1023,10 +1023,10 @@ packages: dependency: transitive description: name: path_parsing - sha256: caa17e8f0b386eb190dd5b6a3b71211c76375aa8b6ffb4465b5863d019bdb334 + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.1.0" path_provider: dependency: transitive description: @@ -1159,10 +1159,10 @@ packages: dependency: transitive description: name: riverpod_analyzer_utils - sha256: dc53a659cb543b203cdc35cd4e942ed08ea893eb6ef12029301323bdf18c5d95 + sha256: c6b8222b2b483cb87ae77ad147d6408f400c64f060df7a225b127f4afef4f8c8 url: "https://pub.dev" source: hosted - version: "0.5.7" + version: "0.5.8" riverpod_annotation: dependency: "direct main" description: @@ -1175,18 +1175,18 @@ packages: dependency: "direct dev" description: name: riverpod_generator - sha256: "54458dac2fea976990dc9ed379060db6ae5c8790143f1963fedd0fb99980a326" + sha256: "63546d70952015f0981361636bf8f356d9cfd9d7f6f0815e3c07789a41233188" url: "https://pub.dev" source: hosted - version: "2.6.2" + version: "2.6.3" riverpod_lint: dependency: "direct dev" description: name: riverpod_lint - sha256: "326efc199b87f21053b9a2afbf2aea26c41b3bf6f8ba346ce69126ee17d16ebd" + sha256: "83e4caa337a9840469b7b9bd8c2351ce85abad80f570d84146911b32086fbd99" url: "https://pub.dev" source: hosted - version: "2.6.2" + version: "2.6.3" rxdart: dependency: transitive description: @@ -1199,10 +1199,10 @@ packages: dependency: "direct main" description: name: share_plus - sha256: "3af2cda1752e5c24f2fc04b6083b40f013ffe84fb90472f30c6499a9213d5442" + sha256: "9c9bafd4060728d7cdb2464c341743adbd79d327cb067ec7afb64583540b47c8" url: "https://pub.dev" source: hosted - version: "10.1.1" + version: "10.1.2" share_plus_platform_interface: dependency: transitive description: @@ -1215,10 +1215,10 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051" + sha256: "95f9997ca1fb9799d494d0cb2a780fd7be075818d59f00c43832ed112b158a82" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.3" shared_preferences_android: dependency: transitive description: @@ -1340,10 +1340,10 @@ packages: dependency: "direct main" description: name: sqflite - sha256: "79a297dc3cc137e758c6a4baf83342b039e5a6d2436fcdf3f96a00adaaf2ad62" + sha256: "2d7299468485dca85efeeadf5d38986909c5eb0cd71fd3db2c2f000e6c9454bb" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" sqflite_android: dependency: transitive description: @@ -1372,10 +1372,10 @@ packages: dependency: transitive description: name: sqflite_darwin - sha256: "769733dddf94622d5541c73e4ddc6aa7b252d865285914b6fcd54a63c4b4f027" + sha256: "96a698e2bc82bd770a4d6aab00b42396a7c63d9e33513a56945cbccb594c2474" url: "https://pub.dev" source: hosted - version: "2.4.1-1" + version: "2.4.1" sqflite_platform_interface: dependency: transitive description: @@ -1477,10 +1477,10 @@ packages: dependency: transitive description: name: timezone - sha256: "2236ec079a174ce07434e89fcd3fcda430025eb7692244139a9cf54fdcf1fc7d" + sha256: ffc9d5f4d1193534ef051f9254063fa53d588609418c84299956c3db9383587d url: "https://pub.dev" source: hosted - version: "0.9.4" + version: "0.10.0" timing: dependency: transitive description: @@ -1517,10 +1517,10 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "0dea215895a4d254401730ca0ba8204b29109a34a99fb06ae559a2b60988d2de" + sha256: "6fc2f56536ee873eeb867ad176ae15f304ccccc357848b351f6f0d8d4a40d193" url: "https://pub.dev" source: hosted - version: "6.3.13" + version: "6.3.14" url_launcher_ios: dependency: transitive description: @@ -1533,10 +1533,10 @@ packages: dependency: transitive description: name: url_launcher_linux - sha256: e2b9622b4007f97f504cd64c0128309dfb978ae66adbe944125ed9e1750f06af + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "3.2.1" url_launcher_macos: dependency: transitive description: @@ -1581,26 +1581,26 @@ packages: dependency: transitive description: name: vector_graphics - sha256: "32c3c684e02f9bc0afb0ae0aa653337a2fe022e8ab064bcd7ffda27a74e288e3" + sha256: "773c9522d66d523e1c7b25dfb95cc91c26a1e17b107039cfe147285e92de7878" url: "https://pub.dev" source: hosted - version: "1.1.11+1" + version: "1.1.14" vector_graphics_codec: dependency: transitive description: name: vector_graphics_codec - sha256: c86987475f162fadff579e7320c7ddda04cd2fdeffbe1129227a85d9ac9e03da + sha256: "2430b973a4ca3c4dbc9999b62b8c719a160100dcbae5c819bae0cacce32c9cdb" url: "https://pub.dev" source: hosted - version: "1.1.11+1" + version: "1.1.12" vector_graphics_compiler: dependency: transitive description: name: vector_graphics_compiler - sha256: "12faff3f73b1741a36ca7e31b292ddeb629af819ca9efe9953b70bd63fc8cd81" + sha256: ab9ff38fc771e9ee1139320adbe3d18a60327370c218c60752068ebee4b49ab1 url: "https://pub.dev" source: hosted - version: "1.1.11+1" + version: "1.1.15" vector_math: dependency: transitive description: @@ -1677,10 +1677,10 @@ packages: dependency: transitive description: name: win32 - sha256: "10169d3934549017f0ae278ccb07f828f9d6ea21573bab0fb77b0e1ef0fce454" + sha256: "84ba388638ed7a8cb3445a320c8273136ab2631cd5f2c57888335504ddab1bc2" url: "https://pub.dev" source: hosted - version: "5.7.2" + version: "5.8.0" win32_registry: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 5c3df06d40..f13295d426 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -37,7 +37,7 @@ dependencies: flutter_displaymode: ^0.6.0 flutter_layout_grid: ^2.0.1 flutter_linkify: ^6.0.0 - flutter_local_notifications: ^17.2.1+2 + flutter_local_notifications: ^18.0.1 flutter_localizations: sdk: flutter flutter_native_splash: ^2.3.5 From fbc105fa831899f2633f372a961d6225894d6b67 Mon Sep 17 00:00:00 2001 From: Jimima Date: Tue, 19 Nov 2024 14:16:32 +0000 Subject: [PATCH 701/979] Tweaked layout --- lib/src/view/game/game_result_dialog.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/src/view/game/game_result_dialog.dart b/lib/src/view/game/game_result_dialog.dart index 948feed62d..259136b609 100644 --- a/lib/src/view/game/game_result_dialog.dart +++ b/lib/src/view/game/game_result_dialog.dart @@ -110,7 +110,7 @@ class _GameEndDialogState extends ConsumerState { animationFinished.value = true; }, height: gameState.game.opponent?.offeringRematch ?? false - ? 50 + ? 40 : 0, //TODO handle nulls better? child: ValueListenableBuilder( valueListenable: animationFinished, @@ -154,7 +154,6 @@ class _GameEndDialogState extends ConsumerState { child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - // Text("Rematch offered"), FatButton( semanticsLabel: context.l10n.rematch, child: const Text('Accept rematch'), From 734acfdd7129d758fb74c885845cde2a86262aba Mon Sep 17 00:00:00 2001 From: Jimima Date: Tue, 19 Nov 2024 16:56:16 +0000 Subject: [PATCH 702/979] Version with persisted-but-disabled rematch button --- lib/src/view/game/game_result_dialog.dart | 117 ++++++++++++---------- 1 file changed, 63 insertions(+), 54 deletions(-) diff --git a/lib/src/view/game/game_result_dialog.dart b/lib/src/view/game/game_result_dialog.dart index 259136b609..8bca38d5d7 100644 --- a/lib/src/view/game/game_result_dialog.dart +++ b/lib/src/view/game/game_result_dialog.dart @@ -109,9 +109,7 @@ class _GameEndDialogState extends ConsumerState { onEnd: () { animationFinished.value = true; }, - height: gameState.game.opponent?.offeringRematch ?? false - ? 40 - : 0, //TODO handle nulls better? + height: gameState.game.opponent?.offeringRematch ?? false ? 105 : 0, child: ValueListenableBuilder( valueListenable: animationFinished, builder: (context, value, child) { @@ -120,66 +118,77 @@ class _GameEndDialogState extends ConsumerState { maintainSize: true, maintainState: true, visible: animationFinished.value, - child: const Padding( - padding: EdgeInsets.only(bottom: 16.0), - child: Text( - 'Your opponent has offered a rematch', - textAlign: TextAlign.center, + child: Padding( + padding: EdgeInsets.only(bottom: 15.0), + child: Column( + children: [ + const Padding( + padding: EdgeInsets.only(bottom: 15.0), + child: Text( + 'Your opponent has offered a rematch', + textAlign: TextAlign.center, + ), + ), + if (gameState.game.me?.offeringRematch == true) + SecondaryButton( + semanticsLabel: context.l10n.cancelRematchOffer, + onPressed: () { + ref.read(ctrlProvider.notifier).declineRematch(); + }, + child: Text( + context.l10n.cancelRematchOffer, + textAlign: TextAlign.center, + ), + ) + else if (gameState.game.opponent?.offeringRematch == true) + ValueListenableBuilder( + valueListenable: animationFinished, + builder: + (BuildContext context, value, Widget? child) { + return Visibility( + maintainAnimation: true, + maintainSize: true, + maintainState: true, + visible: animationFinished.value, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FatButton( + semanticsLabel: context.l10n.rematch, + child: const Text('Accept rematch'), + onPressed: () { + ref + .read(ctrlProvider.notifier) + .proposeOrAcceptRematch(); + }, + ), + SecondaryButton( + semanticsLabel: context.l10n.rematch, + child: const Text('Decline'), + onPressed: () { + ref + .read(ctrlProvider.notifier) + .declineRematch(); + }, + ), + ], + ), + ); + }, + ), + ], ), ), ); }, ), ), - if (gameState.game.me?.offeringRematch == true) - SecondaryButton( - semanticsLabel: context.l10n.cancelRematchOffer, - onPressed: () { - ref.read(ctrlProvider.notifier).declineRematch(); - }, - child: Text( - context.l10n.cancelRematchOffer, - textAlign: TextAlign.center, - ), - ) - else if (gameState.game.opponent?.offeringRematch == true) - ValueListenableBuilder( - valueListenable: animationFinished, - builder: (BuildContext context, value, Widget? child) { - return Visibility( - maintainAnimation: true, - maintainSize: true, - maintainState: true, - visible: animationFinished.value, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - FatButton( - semanticsLabel: context.l10n.rematch, - child: const Text('Accept rematch'), - onPressed: () { - ref - .read(ctrlProvider.notifier) - .proposeOrAcceptRematch(); - }, - ), - SecondaryButton( - semanticsLabel: context.l10n.rematch, - child: const Text('Decline'), - onPressed: () { - ref.read(ctrlProvider.notifier).declineRematch(); - }, - ), - ], - ), - ); - }, - ) - else if (gameState.canOfferRematch) + if (gameState.canOfferRematch) SecondaryButton( semanticsLabel: context.l10n.rematch, onPressed: _activateButtons && - gameState.game.opponent?.onGame == true + gameState.game.opponent?.onGame == true && + gameState.game.opponent?.offeringRematch != true ? () { ref.read(ctrlProvider.notifier).proposeOrAcceptRematch(); } From a0f325a27f021b44a3e72c80daafcec0b0273e0b Mon Sep 17 00:00:00 2001 From: Jimima Date: Tue, 19 Nov 2024 21:01:38 +0000 Subject: [PATCH 703/979] Improved layout and logic --- lib/src/view/game/game_result_dialog.dart | 96 ++++++++++------------- 1 file changed, 42 insertions(+), 54 deletions(-) diff --git a/lib/src/view/game/game_result_dialog.dart b/lib/src/view/game/game_result_dialog.dart index 8bca38d5d7..a745d7f06f 100644 --- a/lib/src/view/game/game_result_dialog.dart +++ b/lib/src/view/game/game_result_dialog.dart @@ -104,12 +104,11 @@ class _GameEndDialogState extends ConsumerState { padding: const EdgeInsets.only(bottom: 16.0), child: PlayerSummary(game: gameState.game), ), - AnimatedContainer( + AnimatedSize( duration: const Duration(milliseconds: 400), onEnd: () { animationFinished.value = true; }, - height: gameState.game.opponent?.offeringRematch ?? false ? 105 : 0, child: ValueListenableBuilder( valueListenable: animationFinished, builder: (context, value, child) { @@ -119,62 +118,40 @@ class _GameEndDialogState extends ConsumerState { maintainState: true, visible: animationFinished.value, child: Padding( - padding: EdgeInsets.only(bottom: 15.0), + padding: const EdgeInsets.only(bottom: 15.0), child: Column( children: [ - const Padding( - padding: EdgeInsets.only(bottom: 15.0), - child: Text( - 'Your opponent has offered a rematch', - textAlign: TextAlign.center, - ), - ), - if (gameState.game.me?.offeringRematch == true) - SecondaryButton( - semanticsLabel: context.l10n.cancelRematchOffer, - onPressed: () { - ref.read(ctrlProvider.notifier).declineRematch(); - }, + if (gameState.game.opponent?.offeringRematch == true) + const Padding( + padding: EdgeInsets.only(bottom: 15.0), child: Text( - context.l10n.cancelRematchOffer, + 'Your opponent has offered a rematch', textAlign: TextAlign.center, ), - ) - else if (gameState.game.opponent?.offeringRematch == true) - ValueListenableBuilder( - valueListenable: animationFinished, - builder: - (BuildContext context, value, Widget? child) { - return Visibility( - maintainAnimation: true, - maintainSize: true, - maintainState: true, - visible: animationFinished.value, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - FatButton( - semanticsLabel: context.l10n.rematch, - child: const Text('Accept rematch'), - onPressed: () { - ref - .read(ctrlProvider.notifier) - .proposeOrAcceptRematch(); - }, - ), - SecondaryButton( - semanticsLabel: context.l10n.rematch, - child: const Text('Decline'), - onPressed: () { - ref - .read(ctrlProvider.notifier) - .declineRematch(); - }, - ), - ], - ), - ); - }, + ), + if (gameState.game.opponent?.offeringRematch == true) + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FatButton( + semanticsLabel: context.l10n.rematch, + child: const Text('Accept rematch'), + onPressed: () { + ref + .read(ctrlProvider.notifier) + .proposeOrAcceptRematch(); + }, + ), + SecondaryButton( + semanticsLabel: context.l10n.rematch, + child: const Text('Decline'), + onPressed: () { + ref + .read(ctrlProvider.notifier) + .declineRematch(); + }, + ), + ], ), ], ), @@ -183,7 +160,18 @@ class _GameEndDialogState extends ConsumerState { }, ), ), - if (gameState.canOfferRematch) + if (gameState.game.me?.offeringRematch == true) + SecondaryButton( + semanticsLabel: context.l10n.cancelRematchOffer, + onPressed: () { + ref.read(ctrlProvider.notifier).declineRematch(); + }, + child: Text( + context.l10n.cancelRematchOffer, + textAlign: TextAlign.center, + ), + ) + else if (gameState.canOfferRematch) SecondaryButton( semanticsLabel: context.l10n.rematch, onPressed: _activateButtons && From f2c144b81ee8ba6e63489b1cef69db9c8fd8fbe3 Mon Sep 17 00:00:00 2001 From: Jimima Date: Tue, 19 Nov 2024 21:05:57 +0000 Subject: [PATCH 704/979] Removed accidental old(?) code --- lib/src/view/game/game_result_dialog.dart | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lib/src/view/game/game_result_dialog.dart b/lib/src/view/game/game_result_dialog.dart index a745d7f06f..facf90d820 100644 --- a/lib/src/view/game/game_result_dialog.dart +++ b/lib/src/view/game/game_result_dialog.dart @@ -99,11 +99,6 @@ class _GameEndDialogState extends ConsumerState { padding: const EdgeInsets.only(bottom: 16.0), child: GameResult(game: gameState.game), ), - if (gameState.game.white.analysis != null) - Padding( - padding: const EdgeInsets.only(bottom: 16.0), - child: PlayerSummary(game: gameState.game), - ), AnimatedSize( duration: const Duration(milliseconds: 400), onEnd: () { From 8159bceba349bc59ff28a186fccc6586876ed78e Mon Sep 17 00:00:00 2001 From: Jimima Date: Tue, 19 Nov 2024 22:24:30 +0000 Subject: [PATCH 705/979] Removed unnecessary padding --- lib/src/view/game/game_result_dialog.dart | 30 +++++++++++------------ 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/lib/src/view/game/game_result_dialog.dart b/lib/src/view/game/game_result_dialog.dart index facf90d820..1847c79ab7 100644 --- a/lib/src/view/game/game_result_dialog.dart +++ b/lib/src/view/game/game_result_dialog.dart @@ -112,20 +112,20 @@ class _GameEndDialogState extends ConsumerState { maintainSize: true, maintainState: true, visible: animationFinished.value, - child: Padding( - padding: const EdgeInsets.only(bottom: 15.0), - child: Column( - children: [ - if (gameState.game.opponent?.offeringRematch == true) - const Padding( - padding: EdgeInsets.only(bottom: 15.0), - child: Text( - 'Your opponent has offered a rematch', - textAlign: TextAlign.center, - ), + child: Column( + children: [ + if (gameState.game.opponent?.offeringRematch == true) + const Padding( + padding: EdgeInsets.only(bottom: 15.0), + child: Text( + 'Your opponent has offered a rematch', + textAlign: TextAlign.center, ), - if (gameState.game.opponent?.offeringRematch == true) - Row( + ), + if (gameState.game.opponent?.offeringRematch == true) + Padding( + padding: const EdgeInsets.only(bottom: 15.0), + child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ FatButton( @@ -148,8 +148,8 @@ class _GameEndDialogState extends ConsumerState { ), ], ), - ], - ), + ), + ], ), ); }, From a855430b8799f2e812eababe44acc5853cc8d7fb Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Wed, 20 Nov 2024 00:06:19 +0100 Subject: [PATCH 706/979] use the new clock wigdet --- lib/src/model/broadcast/broadcast.dart | 12 +----- .../model/broadcast/broadcast_repository.dart | 7 ++- .../broadcast/broadcast_round_controller.dart | 21 +-------- .../view/broadcast/broadcast_boards_tab.dart | 21 +++++---- .../view/broadcast/broadcast_game_screen.dart | 43 ++++++++++--------- lib/src/widgets/clock.dart | 3 +- 6 files changed, 41 insertions(+), 66 deletions(-) diff --git a/lib/src/model/broadcast/broadcast.dart b/lib/src/model/broadcast/broadcast.dart index e257583352..71d3e90cc9 100644 --- a/lib/src/model/broadcast/broadcast.dart +++ b/lib/src/model/broadcast/broadcast.dart @@ -96,9 +96,7 @@ class BroadcastGame with _$BroadcastGame { required String fen, required Move? lastMove, required BroadcastResult status, - - /// The amount of time that the player whose turn it is has been thinking since his last move - required Duration thinkTime, + required DateTime? updatedClockAt, }) = _BroadcastGame; bool get isPlaying => status == BroadcastResult.ongoing; @@ -107,14 +105,6 @@ class BroadcastGame with _$BroadcastGame { status == BroadcastResult.whiteWins || status == BroadcastResult.blackWins; Side get playingSide => Setup.parseFen(fen).turn; - Duration? get timeLeft { - final clock = players[playingSide]!.clock; - if (clock == null) return null; - final timeLeftMaybeNegative = clock - thinkTime; - return timeLeftMaybeNegative.isNegative - ? Duration.zero - : timeLeftMaybeNegative; - } } @freezed diff --git a/lib/src/model/broadcast/broadcast_repository.dart b/lib/src/model/broadcast/broadcast_repository.dart index c0ec656569..afa5f61d1c 100644 --- a/lib/src/model/broadcast/broadcast_repository.dart +++ b/lib/src/model/broadcast/broadcast_repository.dart @@ -165,6 +165,10 @@ MapEntry gameFromPick( ) }; + /// The amount of time that the player whose turn it is has been thinking since his last move + final thinkTime = + pick('thinkTime').asDurationFromSecondsOrNull() ?? Duration.zero; + return MapEntry( pick('id').asBroadcastGameIdOrThrow(), BroadcastGame( @@ -176,8 +180,7 @@ MapEntry gameFromPick( fen: pick('fen').asStringOrNull() ?? Variant.standard.initialPosition.fen, lastMove: pick('lastMove').asUciMoveOrNull(), status: status, - thinkTime: - pick('thinkTime').asDurationFromSecondsOrNull() ?? Duration.zero, + updatedClockAt: DateTime.now().subtract(thinkTime), ), ); } diff --git a/lib/src/model/broadcast/broadcast_round_controller.dart b/lib/src/model/broadcast/broadcast_round_controller.dart index 9d7c4d0d16..981d17f4e7 100644 --- a/lib/src/model/broadcast/broadcast_round_controller.dart +++ b/lib/src/model/broadcast/broadcast_round_controller.dart @@ -22,8 +22,6 @@ class BroadcastRoundController extends _$BroadcastRoundController { StreamSubscription? _subscription; - Timer? _timer; - late SocketClient _socketClient; @override @@ -36,32 +34,15 @@ class BroadcastRoundController extends _$BroadcastRoundController { ref.onDispose(() { _subscription?.cancel(); - _timer?.cancel(); }); final games = await ref.withClient( (client) => BroadcastRepository(client).getRound(broadcastRoundId), ); - _timer = Timer.periodic( - const Duration(seconds: 1), - (_) => _updateClocks(), - ); - return games; } - void _updateClocks() { - state = AsyncData( - state.requireValue.map((gameId, game) { - if (!game.isPlaying) return MapEntry(gameId, game); - final thinkTime = game.thinkTime; - final newThinkTime = thinkTime + const Duration(seconds: 1); - return MapEntry(gameId, game.copyWith(thinkTime: newThinkTime)); - }), - ); - } - void _handleSocketEvent(SocketEvent event) { if (!state.hasValue) return; @@ -109,7 +90,7 @@ class BroadcastRoundController extends _$BroadcastRoundController { ), fen: fen, lastMove: pick(event.data, 'n', 'uci').asUciMoveOrThrow(), - thinkTime: Duration.zero, + updatedClockAt: DateTime.now(), ), ), ); diff --git a/lib/src/view/broadcast/broadcast_boards_tab.dart b/lib/src/view/broadcast/broadcast_boards_tab.dart index d135170be2..d637f95c6f 100644 --- a/lib/src/view/broadcast/broadcast_boards_tab.dart +++ b/lib/src/view/broadcast/broadcast_boards_tab.dart @@ -17,6 +17,7 @@ import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_game_screen.dart'; import 'package:lichess_mobile/src/widgets/board_thumbnail.dart'; +import 'package:lichess_mobile/src/widgets/clock.dart'; import 'package:lichess_mobile/src/widgets/evaluation_bar.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; @@ -274,21 +275,19 @@ class _PlayerWidget extends StatelessWidget { const TextStyle().copyWith(fontWeight: FontWeight.bold), ) else if (player.clock != null) - if (side == playingSide) - Text( - (game.timeLeft!).toHoursMinutesSeconds(), + CountdownClockBuilder( + timeLeft: player.clock!, + active: side == playingSide, + builder: (context, timeLeft) => Text( + timeLeft.toHoursMinutesSeconds(), style: TextStyle( - color: Colors.orange[900], + color: (side == playingSide) ? Colors.orange[900] : null, fontFeatures: const [FontFeature.tabularFigures()], ), - ) - else - Text( - player.clock!.toHoursMinutesSeconds(), - style: const TextStyle( - fontFeatures: [FontFeature.tabularFigures()], - ), ), + tickInterval: const Duration(seconds: 1), + clockUpdatedAt: game.updatedClockAt, + ), ], ), ), diff --git a/lib/src/view/broadcast/broadcast_game_screen.dart b/lib/src/view/broadcast/broadcast_game_screen.dart index 990576d25f..fab76bd7b8 100644 --- a/lib/src/view/broadcast/broadcast_game_screen.dart +++ b/lib/src/view/broadcast/broadcast_game_screen.dart @@ -27,6 +27,7 @@ import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; import 'package:lichess_mobile/src/view/engine/engine_lines.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/clock.dart'; import 'package:lichess_mobile/src/widgets/pgn.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; @@ -563,8 +564,7 @@ class _PlayerWidget extends StatelessWidget { ), ), ), - if (((side == playingSide && playClock) && game.timeLeft != null) || - (!(side == playingSide && playClock) && clock != null)) + if (clock != null) Card( color: (side == playingSide) ? playClock @@ -585,28 +585,29 @@ class _PlayerWidget extends StatelessWidget { child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), - child: (side == playingSide && playClock) - ? Text( - game.timeLeft!.toHoursMinutesSeconds(), - style: TextStyle( - color: - Theme.of(context).colorScheme.onTertiaryContainer, - fontFeatures: const [ - FontFeature.tabularFigures(), - ], - ), - ) - : Text( - clock!.toHoursMinutesSeconds(), - style: TextStyle( - color: (side == playingSide) + child: CountdownClockBuilder( + timeLeft: clock!, + active: side == playingSide && playClock, + builder: (context, timeLeft) => Text( + timeLeft.toHoursMinutesSeconds(), + style: TextStyle( + color: (side == playingSide) + ? playClock ? Theme.of(context) + .colorScheme + .onTertiaryContainer + : Theme.of(context) .colorScheme .onSecondaryContainer - : null, - fontFeatures: const [FontFeature.tabularFigures()], - ), - ), + : null, + fontFeatures: const [FontFeature.tabularFigures()], + ), + ), + tickInterval: const Duration(seconds: 1), + clockUpdatedAt: (side == playingSide && playClock) + ? game.updatedClockAt + : null, + ), ), ), ], diff --git a/lib/src/widgets/clock.dart b/lib/src/widgets/clock.dart index 7de23af3e0..19f653ea1c 100644 --- a/lib/src/widgets/clock.dart +++ b/lib/src/widgets/clock.dart @@ -287,7 +287,8 @@ class _CountdownClockState extends State { void didUpdateWidget(CountdownClockBuilder oldClock) { super.didUpdateWidget(oldClock); - if (widget.clockUpdatedAt != oldClock.clockUpdatedAt) { + if (widget.timeLeft != oldClock.timeLeft || + widget.clockUpdatedAt != oldClock.clockUpdatedAt) { timeLeft = widget.timeLeft; } From 751cff14f8e91ebfadbe762f9754576482128fee Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Wed, 20 Nov 2024 00:32:07 +0100 Subject: [PATCH 707/979] fix clock test --- test/widgets/clock_test.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/widgets/clock_test.dart b/test/widgets/clock_test.dart index 73e20078f6..7bdf3759a1 100644 --- a/test/widgets/clock_test.dart +++ b/test/widgets/clock_test.dart @@ -115,7 +115,7 @@ void main() { expect(find.text('0:00.0'), findsOneWidget); }); - testWidgets('do not update if clockUpdatedAt is same', + testWidgets('do not update if timeLeft and clockUpdatedAt are same', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( @@ -138,7 +138,7 @@ void main() { await tester.pumpWidget( MaterialApp( home: CountdownClockBuilder( - timeLeft: const Duration(seconds: 11), + timeLeft: const Duration(seconds: 10), active: true, builder: clockBuilder, ), From aaa0e673c94aaacd0777da13aeed7a000b9b05c6 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Wed, 20 Nov 2024 01:34:56 +0100 Subject: [PATCH 708/979] cleaning code --- .../broadcast/broadcast_game_controller.dart | 22 +++++-------------- .../broadcast/broadcast_game_settings.dart | 9 -------- 2 files changed, 5 insertions(+), 26 deletions(-) diff --git a/lib/src/model/broadcast/broadcast_game_controller.dart b/lib/src/model/broadcast/broadcast_game_controller.dart index 5d581eb042..3693801898 100644 --- a/lib/src/model/broadcast/broadcast_game_controller.dart +++ b/lib/src/model/broadcast/broadcast_game_controller.dart @@ -55,15 +55,11 @@ class BroadcastGameController extends _$BroadcastGameController final evaluationService = ref.watch(evaluationServiceProvider); - const isEngineAllowed = true; - ref.onDispose(() { _subscription?.cancel(); _startEngineEvalTimer?.cancel(); _engineEvalDebounce.dispose(); - if (isEngineAllowed) { - evaluationService.disposeEngine(); - } + evaluationService.disposeEngine(); }); Move? lastMove; @@ -85,7 +81,7 @@ class BroadcastGameController extends _$BroadcastGameController // analysis preferences change final prefs = ref.read(analysisPreferencesProvider); - final analysisState = BroadcastGameState( + final broadcastState = BroadcastGameState( id: gameId, currentPath: currentPath, broadcastLivePath: pgnHeaders['Result'] == '*' ? currentPath : null, @@ -100,7 +96,7 @@ class BroadcastGameController extends _$BroadcastGameController clocks: _makeClocks(currentPath), ); - if (analysisState.isEngineAvailable) { + if (broadcastState.isEngineAvailable) { evaluationService .initEngine( _evaluationContext, @@ -116,7 +112,7 @@ class BroadcastGameController extends _$BroadcastGameController }); } - return analysisState; + return broadcastState; } void _handleSocketEvent(SocketEvent event) { @@ -405,14 +401,6 @@ class BroadcastGameController extends _$BroadcastGameController _startEngineEval(); } - /// Makes a PGN string up to the current node only. - String makeCurrentNodePgn() { - if (!state.hasValue) Exception('Cannot make a PGN up to the current node'); - - final nodes = _root.branchesOn(state.requireValue.currentPath); - return nodes.map((node) => node.sanMove.san).join(' '); - } - void _setPath( UciPath path, { bool shouldForceShowVariation = false, @@ -520,7 +508,7 @@ class BroadcastGameController extends _$BroadcastGameController } void _stopEngineEval() { - if (!state.hasValue) Exception('Cannot export PGN'); + if (!state.hasValue) return; ref.read(evaluationServiceProvider).stop(); // update the current node with last cached eval diff --git a/lib/src/view/broadcast/broadcast_game_settings.dart b/lib/src/view/broadcast/broadcast_game_settings.dart index 596d0bbbfd..8b85528ead 100644 --- a/lib/src/view/broadcast/broadcast_game_settings.dart +++ b/lib/src/view/broadcast/broadcast_game_settings.dart @@ -6,7 +6,6 @@ import 'package:lichess_mobile/src/model/broadcast/broadcast_game_controller.dar import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; -import 'package:lichess_mobile/src/model/study/study_preferences.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; @@ -30,7 +29,6 @@ class BroadcastGameSettings extends ConsumerWidget { ); final analysisPrefs = ref.watch(analysisPreferencesProvider); - final studyPrefs = ref.watch(studyPreferencesProvider); final isSoundEnabled = ref.watch( generalPreferencesProvider.select((pref) => pref.isSoundEnabled), ); @@ -112,13 +110,6 @@ class BroadcastGameSettings extends ConsumerWidget { .toggleShowBestMoveArrow() : null, ), - SwitchSettingTile( - title: Text(context.l10n.showVariationArrows), - value: studyPrefs.showVariationArrows, - onChanged: (value) => ref - .read(studyPreferencesProvider.notifier) - .toggleShowVariationArrows(), - ), SwitchSettingTile( title: Text(context.l10n.evaluationGauge), value: analysisPrefs.showEvaluationGauge, From 47455aea43c07e70f168d9ed6168ab1338b6708c Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Wed, 20 Nov 2024 02:11:06 +0100 Subject: [PATCH 709/979] fix local evaluation setting and tweak broadcast game controller --- .../broadcast/broadcast_game_controller.dart | 12 ++++----- .../view/broadcast/broadcast_game_screen.dart | 27 +++++++++++-------- .../broadcast/broadcast_game_settings.dart | 10 +++---- 3 files changed, 26 insertions(+), 23 deletions(-) diff --git a/lib/src/model/broadcast/broadcast_game_controller.dart b/lib/src/model/broadcast/broadcast_game_controller.dart index 3693801898..c7d3fc5ba0 100644 --- a/lib/src/model/broadcast/broadcast_game_controller.dart +++ b/lib/src/model/broadcast/broadcast_game_controller.dart @@ -96,7 +96,7 @@ class BroadcastGameController extends _$BroadcastGameController clocks: _makeClocks(currentPath), ); - if (broadcastState.isEngineAvailable) { + if (broadcastState.isLocalEvaluationEnabled) { evaluationService .initEngine( _evaluationContext, @@ -343,7 +343,7 @@ class BroadcastGameController extends _$BroadcastGameController ), ); - if (state.requireValue.isEngineAvailable) { + if (state.requireValue.isLocalEvaluationEnabled) { final prefs = ref.read(analysisPreferencesProvider); await ref.read(evaluationServiceProvider).initEngine( _evaluationContext, @@ -479,7 +479,7 @@ class BroadcastGameController extends _$BroadcastGameController ); } - if (pathChange && state.requireValue.isEngineAvailable) { + if (pathChange && state.requireValue.isLocalEvaluationEnabled) { _debouncedStartEngineEval(); } } @@ -487,7 +487,7 @@ class BroadcastGameController extends _$BroadcastGameController void _startEngineEval() { if (!state.hasValue) return; - if (!state.requireValue.isEngineAvailable) return; + if (!state.requireValue.isLocalEvaluationEnabled) return; ref .read(evaluationServiceProvider) .start( @@ -586,15 +586,13 @@ class BroadcastGameState with _$BroadcastGameState { IMap> get validMoves => makeLegalMoves(currentNode.position); - bool get isEngineAvailable => isLocalEvaluationEnabled; - Position get position => currentNode.position; bool get canGoNext => currentNode.hasChild; bool get canGoBack => currentPath.size > UciPath.empty.size; EngineGaugeParams get engineGaugeParams => ( orientation: pov, - isLocalEngineAvailable: isEngineAvailable, + isLocalEngineAvailable: isLocalEvaluationEnabled, position: position, savedEval: currentNode.eval ?? currentNode.serverEval, ); diff --git a/lib/src/view/broadcast/broadcast_game_screen.dart b/lib/src/view/broadcast/broadcast_game_screen.dart index fab76bd7b8..da1a77cbfa 100644 --- a/lib/src/view/broadcast/broadcast_game_screen.dart +++ b/lib/src/view/broadcast/broadcast_game_screen.dart @@ -110,19 +110,26 @@ class _Body extends ConsumerWidget { final boardSize = isTablet || isSmallScreen ? defaultBoardSize - kTabletBoardTableSidePadding * 2 : defaultBoardSize; - final landscape = constraints.biggest.aspectRatio > 1; + final ctrlProvider = + broadcastGameControllerProvider(roundId, gameId); final engineGaugeParams = ref.watch( - broadcastGameControllerProvider(roundId, gameId) - .select((state) => state.valueOrNull?.engineGaugeParams), + ctrlProvider + .select((state) => state.requireValue.engineGaugeParams), ); final currentNode = ref.watch( - broadcastGameControllerProvider(roundId, gameId) + ctrlProvider .select((state) => state.requireValue.currentNode), ); + final isLocalEvaluationEnabled = ref.watch( + ctrlProvider.select( + (state) => state.requireValue.isLocalEvaluationEnabled, + ), + ); + final engineLines = EngineLines( clientEval: currentNode.eval, isGameOver: currentNode.position.isGameOver, @@ -140,7 +147,7 @@ class _Body extends ConsumerWidget { ); final engineGauge = - showEvaluationGauge && engineGaugeParams != null + showEvaluationGauge && isLocalEvaluationEnabled ? EngineGauge( params: engineGaugeParams, displayMode: landscape @@ -177,7 +184,7 @@ class _Body extends ConsumerWidget { child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ - if (engineGaugeParams != null) engineLines, + if (isLocalEvaluationEnabled) engineLines, Expanded( child: PlatformCard( clipBehavior: Clip.hardEdge, @@ -211,10 +218,8 @@ class _Body extends ConsumerWidget { ), child: Column( children: [ - if (engineGauge != null) ...[ - engineGauge, - engineLines, - ], + if (engineGauge != null) engineGauge, + if (isLocalEvaluationEnabled) engineLines, _BroadcastBoardWithHeaders( roundId, gameId, @@ -353,7 +358,7 @@ class _BroadcastBoardState extends ConsumerState<_BroadcastBoard> { final sanMove = currentNode.sanMove; final ISet bestMoveShapes = showBestMoveArrow && - broadcastAnalysisState.isEngineAvailable && + broadcastAnalysisState.isLocalEvaluationEnabled && bestMoves != null ? computeBestMoveShapes( bestMoves, diff --git a/lib/src/view/broadcast/broadcast_game_settings.dart b/lib/src/view/broadcast/broadcast_game_settings.dart index 8b85528ead..4604e3c8cf 100644 --- a/lib/src/view/broadcast/broadcast_game_settings.dart +++ b/lib/src/view/broadcast/broadcast_game_settings.dart @@ -23,9 +23,9 @@ class BroadcastGameSettings extends ConsumerWidget { final broacdcastGameAnalysisController = broadcastGameControllerProvider(roundId, gameId); - final isEngineAvailable = ref.watch( + final isLocalEvaluationEnabled = ref.watch( broacdcastGameAnalysisController - .select((s) => s.requireValue.isEngineAvailable), + .select((s) => s.requireValue.isLocalEvaluationEnabled), ); final analysisPrefs = ref.watch(analysisPreferencesProvider); @@ -65,7 +65,7 @@ class BroadcastGameSettings extends ConsumerWidget { subtitle: NonLinearSlider( value: analysisPrefs.numEvalLines, values: const [0, 1, 2, 3], - onChangeEnd: isEngineAvailable + onChangeEnd: isLocalEvaluationEnabled ? (value) => ref .read(broacdcastGameAnalysisController.notifier) .setNumEvalLines(value.toInt()) @@ -94,7 +94,7 @@ class BroadcastGameSettings extends ConsumerWidget { subtitle: NonLinearSlider( value: analysisPrefs.numEngineCores, values: List.generate(maxEngineCores, (index) => index + 1), - onChangeEnd: isEngineAvailable + onChangeEnd: isLocalEvaluationEnabled ? (value) => ref .read(broacdcastGameAnalysisController.notifier) .setEngineCores(value.toInt()) @@ -104,7 +104,7 @@ class BroadcastGameSettings extends ConsumerWidget { SwitchSettingTile( title: Text(context.l10n.bestMoveArrow), value: analysisPrefs.showBestMoveArrow, - onChanged: isEngineAvailable + onChanged: isLocalEvaluationEnabled ? (value) => ref .read(analysisPreferencesProvider.notifier) .toggleShowBestMoveArrow() From 4c7200090927584b7bcc8baf6bed22246d226271 Mon Sep 17 00:00:00 2001 From: Jimima Date: Wed, 20 Nov 2024 08:44:09 +0000 Subject: [PATCH 710/979] Fixed typo --- lib/src/view/game/game_settings.dart | 2 +- lib/src/view/settings/board_settings_screen.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/view/game/game_settings.dart b/lib/src/view/game/game_settings.dart index 72f4f9bf6b..f95c8f7bee 100644 --- a/lib/src/view/game/game_settings.dart +++ b/lib/src/view/game/game_settings.dart @@ -123,7 +123,7 @@ class GameSettings extends ConsumerWidget { }, ), SettingsListTile( - //TODO Add i10n + //TODO Add l10n settingsLabel: const Text('Clock position'), settingsValue: BoardClockPositionScreen.position( context, diff --git a/lib/src/view/settings/board_settings_screen.dart b/lib/src/view/settings/board_settings_screen.dart index 5deaf6ccc7..b6217370e0 100644 --- a/lib/src/view/settings/board_settings_screen.dart +++ b/lib/src/view/settings/board_settings_screen.dart @@ -197,7 +197,7 @@ class _Body extends ConsumerWidget { }, ), SettingsListTile( - //TODO Add i10n + //TODO Add l10n settingsLabel: const Text('Clock position'), settingsValue: BoardClockPositionScreen.position( context, From 5db8348aac11b2b4907f21542253bba27d037072 Mon Sep 17 00:00:00 2001 From: Jimima Date: Wed, 20 Nov 2024 10:09:19 +0000 Subject: [PATCH 711/979] Fixed merge probs --- lib/src/model/settings/board_preferences.dart | 1 - lib/src/view/game/game_settings.dart | 4 ++-- lib/src/view/settings/board_settings_screen.dart | 1 - 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/src/model/settings/board_preferences.dart b/lib/src/model/settings/board_preferences.dart index 481d705699..5199c85427 100644 --- a/lib/src/model/settings/board_preferences.dart +++ b/lib/src/model/settings/board_preferences.dart @@ -324,4 +324,3 @@ String dragTargetKindLabel(DragTargetKind kind) => switch (kind) { DragTargetKind.square => 'Square', DragTargetKind.none => 'None', }; - diff --git a/lib/src/view/game/game_settings.dart b/lib/src/view/game/game_settings.dart index 054bbce0a5..8a3406455c 100644 --- a/lib/src/view/game/game_settings.dart +++ b/lib/src/view/game/game_settings.dart @@ -1,4 +1,3 @@ - import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -6,6 +5,7 @@ import 'package:lichess_mobile/src/model/account/account_preferences.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/game/game_controller.dart'; import 'package:lichess_mobile/src/model/game/game_preferences.dart'; +import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/settings/board_settings_screen.dart'; @@ -13,7 +13,6 @@ import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/settings.dart'; -import '../../utils/navigation.dart'; import '../../widgets/adaptive_choice_picker.dart'; import '../settings/board_clock_position_screen.dart'; import 'game_screen_providers.dart'; @@ -27,6 +26,7 @@ class GameSettings extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final gamePrefs = ref.watch(gamePreferencesProvider); final userPrefsAsync = ref.watch(userGamePrefsProvider(id)); + final boardPrefs = ref.watch(boardPreferencesProvider); return BottomSheetScrollableContainer( children: [ diff --git a/lib/src/view/settings/board_settings_screen.dart b/lib/src/view/settings/board_settings_screen.dart index ddbbca9948..8a1c4eba25 100644 --- a/lib/src/view/settings/board_settings_screen.dart +++ b/lib/src/view/settings/board_settings_screen.dart @@ -9,7 +9,6 @@ import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/utils/system.dart'; import 'package:lichess_mobile/src/view/settings/board_clock_position_screen.dart'; -import 'package:lichess_mobile/src/view/settings/piece_shift_method_settings_screen.dart'; import 'package:lichess_mobile/src/widgets/adaptive_choice_picker.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; From 98c19bdc6a547183eba0920493f3a25db256ce09 Mon Sep 17 00:00:00 2001 From: Jimima Date: Wed, 20 Nov 2024 10:14:22 +0000 Subject: [PATCH 712/979] Linter fix --- lib/src/view/game/game_settings.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/view/game/game_settings.dart b/lib/src/view/game/game_settings.dart index 8a3406455c..d358a0e6f1 100644 --- a/lib/src/view/game/game_settings.dart +++ b/lib/src/view/game/game_settings.dart @@ -1,5 +1,5 @@ -import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/account/account_preferences.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; From 277ef37f4310aaae4c03166d3e0c63ca2f392121 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 20 Nov 2024 11:47:58 +0100 Subject: [PATCH 713/979] Tweak settings --- lib/src/model/settings/board_preferences.dart | 11 +- lib/src/view/game/game_common_widgets.dart | 3 + lib/src/view/game/game_settings.dart | 34 +----- .../view/puzzle/puzzle_settings_screen.dart | 1 + .../settings/board_clock_position_screen.dart | 68 ----------- .../view/settings/board_settings_screen.dart | 114 +++++++++++------- 6 files changed, 84 insertions(+), 147 deletions(-) delete mode 100644 lib/src/view/settings/board_clock_position_screen.dart diff --git a/lib/src/model/settings/board_preferences.dart b/lib/src/model/settings/board_preferences.dart index 5199c85427..43c741ef93 100644 --- a/lib/src/model/settings/board_preferences.dart +++ b/lib/src/model/settings/board_preferences.dart @@ -317,7 +317,16 @@ enum BoardTheme { ); } -enum ClockPosition { left, right } +enum ClockPosition { + left, + right; + + // TODO: l10n + String get label => switch (this) { + ClockPosition.left => 'Left', + ClockPosition.right => 'Right', + }; +} String dragTargetKindLabel(DragTargetKind kind) => switch (kind) { DragTargetKind.circle => 'Circle', diff --git a/lib/src/view/game/game_common_widgets.dart b/lib/src/view/game/game_common_widgets.dart index 2aed60f0dd..94199f99cf 100644 --- a/lib/src/view/game/game_common_widgets.dart +++ b/lib/src/view/game/game_common_widgets.dart @@ -73,6 +73,9 @@ class GameAppBar extends ConsumerWidget { isDismissible: true, isScrollControlled: true, showDragHandle: true, + constraints: BoxConstraints( + minHeight: MediaQuery.sizeOf(context).height * 0.5, + ), builder: (_) => GameSettings(id: id!), ), semanticsLabel: context.l10n.settingsSettings, diff --git a/lib/src/view/game/game_settings.dart b/lib/src/view/game/game_settings.dart index d358a0e6f1..d91c843b37 100644 --- a/lib/src/view/game/game_settings.dart +++ b/lib/src/view/game/game_settings.dart @@ -1,11 +1,9 @@ import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/account/account_preferences.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/game/game_controller.dart'; import 'package:lichess_mobile/src/model/game/game_preferences.dart'; -import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/settings/board_settings_screen.dart'; @@ -13,8 +11,6 @@ import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/settings.dart'; -import '../../widgets/adaptive_choice_picker.dart'; -import '../settings/board_clock_position_screen.dart'; import 'game_screen_providers.dart'; class GameSettings extends ConsumerWidget { @@ -26,7 +22,6 @@ class GameSettings extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final gamePrefs = ref.watch(gamePreferencesProvider); final userPrefsAsync = ref.watch(userGamePrefsProvider(id)); - final boardPrefs = ref.watch(boardPreferencesProvider); return BottomSheetScrollableContainer( children: [ @@ -77,38 +72,11 @@ class GameSettings extends ConsumerWidget { onTap: () { pushPlatformRoute( context, + fullscreenDialog: true, screen: const BoardSettingsScreen(), ); }, ), - SettingsListTile( - //TODO Add l10n - settingsLabel: const Text('Clock position'), - settingsValue: BoardClockPositionScreen.position( - context, - boardPrefs.clockPosition, - ), - onTap: () { - if (Theme.of(context).platform == TargetPlatform.android) { - showChoicePicker( - context, - choices: ClockPosition.values, - selectedItem: boardPrefs.clockPosition, - labelBuilder: (t) => - Text(BoardClockPositionScreen.position(context, t)), - onSelectedItemChanged: (ClockPosition? value) => ref - .read(boardPreferencesProvider.notifier) - .setClockPosition(value ?? ClockPosition.right), - ); - } else { - pushPlatformRoute( - context, - title: 'Clock position', - builder: (context) => const BoardClockPositionScreen(), - ); - } - }, - ), SwitchSettingTile( title: Text( context.l10n.toggleTheChat, diff --git a/lib/src/view/puzzle/puzzle_settings_screen.dart b/lib/src/view/puzzle/puzzle_settings_screen.dart index 3a6256aa6b..33e14c9518 100644 --- a/lib/src/view/puzzle/puzzle_settings_screen.dart +++ b/lib/src/view/puzzle/puzzle_settings_screen.dart @@ -31,6 +31,7 @@ class PuzzleSettingsScreen extends ConsumerWidget { onTap: () { pushPlatformRoute( context, + fullscreenDialog: true, screen: const BoardSettingsScreen(), ); }, diff --git a/lib/src/view/settings/board_clock_position_screen.dart b/lib/src/view/settings/board_clock_position_screen.dart deleted file mode 100644 index d43d1fcba9..0000000000 --- a/lib/src/view/settings/board_clock_position_screen.dart +++ /dev/null @@ -1,68 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; -import 'package:lichess_mobile/src/widgets/settings.dart'; - -class BoardClockPositionScreen extends StatelessWidget { - const BoardClockPositionScreen({super.key}); - - @override - Widget build(BuildContext context) { - return PlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, - ); - } - - Widget _androidBuilder(BuildContext context) { - return Scaffold( - appBar: AppBar(title: const Text('Clock Position')), //TODO: l10n - body: _Body(), - ); - } - - Widget _iosBuilder(BuildContext context) { - return CupertinoPageScaffold( - navigationBar: const CupertinoNavigationBar(), - child: _Body(), - ); - } - - static String position(BuildContext context, ClockPosition position) { - switch (position) { - case ClockPosition.left: - return 'Left'; - case ClockPosition.right: - return 'Right'; - } - } -} - -class _Body extends ConsumerWidget { - @override - Widget build(BuildContext context, WidgetRef ref) { - final clockPosition = ref.watch( - boardPreferencesProvider.select((state) => state.clockPosition), - ); - - void onChanged(ClockPosition? value) => ref - .read(boardPreferencesProvider.notifier) - .setClockPosition(value ?? ClockPosition.right); - - return SafeArea( - child: ListView( - children: [ - ChoicePicker( - choices: ClockPosition.values, - selectedItem: clockPosition, - titleBuilder: (t) => - Text(BoardClockPositionScreen.position(context, t)), - onSelectedItemChanged: onChanged, - ), - ], - ), - ); - } -} diff --git a/lib/src/view/settings/board_settings_screen.dart b/lib/src/view/settings/board_settings_screen.dart index 8a1c4eba25..799b082214 100644 --- a/lib/src/view/settings/board_settings_screen.dart +++ b/lib/src/view/settings/board_settings_screen.dart @@ -8,7 +8,6 @@ import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/utils/system.dart'; -import 'package:lichess_mobile/src/view/settings/board_clock_position_screen.dart'; import 'package:lichess_mobile/src/widgets/adaptive_choice_picker.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; @@ -156,6 +155,22 @@ class _Body extends ConsumerWidget { .togglePieceAnimation(); }, ), + SwitchSettingTile( + // TODO: Add l10n + title: const Text('Shape drawing'), + subtitle: const Text( + // TODO: translate + 'Draw shapes using two fingers: maintain one finger on an empty square and drag another finger to draw a shape.', + maxLines: 5, + textAlign: TextAlign.justify, + ), + value: boardPrefs.enableShapeDrawings, + onChanged: (value) { + ref + .read(boardPreferencesProvider.notifier) + .toggleEnableShapeDrawings(); + }, + ), ], ), ListSection( @@ -184,6 +199,30 @@ class _Body extends ConsumerWidget { : const SizedBox.shrink(), orElse: () => const SizedBox.shrink(), ), + SettingsListTile( + //TODO Add l10n + settingsLabel: const Text('Clock position'), + settingsValue: boardPrefs.clockPosition.label, + onTap: () { + if (Theme.of(context).platform == TargetPlatform.android) { + showChoicePicker( + context, + choices: ClockPosition.values, + selectedItem: boardPrefs.clockPosition, + labelBuilder: (t) => Text(t.label), + onSelectedItemChanged: (ClockPosition? value) => ref + .read(boardPreferencesProvider.notifier) + .setClockPosition(value ?? ClockPosition.right), + ); + } else { + pushPlatformRoute( + context, + title: 'Clock position', + builder: (context) => const BoardClockPositionScreen(), + ); + } + }, + ), SwitchSettingTile( title: Text( context.l10n.preferencesPieceDestinations, @@ -217,50 +256,6 @@ class _Body extends ConsumerWidget { .toggleShowMaterialDifference(); }, ), - SwitchSettingTile( - // TODO: Add l10n - title: const Text('Shape drawing'), - subtitle: const Text( - // TODO: translate - 'Draw shapes using two fingers: maintain one finger on an empty square and drag another finger to draw a shape.', - maxLines: 5, - textAlign: TextAlign.justify, - ), - value: boardPrefs.enableShapeDrawings, - onChanged: (value) { - ref - .read(boardPreferencesProvider.notifier) - .toggleEnableShapeDrawings(); - }, - ), - SettingsListTile( - //TODO Add l10n - settingsLabel: const Text('Clock position'), - settingsValue: BoardClockPositionScreen.position( - context, - boardPrefs.clockPosition, - ), - onTap: () { - if (Theme.of(context).platform == TargetPlatform.android) { - showChoicePicker( - context, - choices: ClockPosition.values, - selectedItem: boardPrefs.clockPosition, - labelBuilder: (t) => - Text(BoardClockPositionScreen.position(context, t)), - onSelectedItemChanged: (ClockPosition? value) => ref - .read(boardPreferencesProvider.notifier) - .setClockPosition(value ?? ClockPosition.right), - ); - } else { - pushPlatformRoute( - context, - title: 'Clock position', - builder: (context) => const BoardClockPositionScreen(), - ); - } - }, - ), ], ), ], @@ -305,6 +300,35 @@ class PieceShiftMethodSettingsScreen extends ConsumerWidget { } } +class BoardClockPositionScreen extends ConsumerWidget { + const BoardClockPositionScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final clockPosition = ref.watch( + boardPreferencesProvider.select((state) => state.clockPosition), + ); + void onChanged(ClockPosition? value) => ref + .read(boardPreferencesProvider.notifier) + .setClockPosition(value ?? ClockPosition.right); + return CupertinoPageScaffold( + navigationBar: const CupertinoNavigationBar(), + child: SafeArea( + child: ListView( + children: [ + ChoicePicker( + choices: ClockPosition.values, + selectedItem: clockPosition, + titleBuilder: (t) => Text(t.label), + onSelectedItemChanged: onChanged, + ), + ], + ), + ), + ); + } +} + class DragTargetKindSettingsScreen extends ConsumerWidget { const DragTargetKindSettingsScreen({super.key}); From 132664d5a90b7848c59e1c764909e19324ae59cc Mon Sep 17 00:00:00 2001 From: Jimima Date: Wed, 20 Nov 2024 11:00:18 +0000 Subject: [PATCH 714/979] Improved rendering logic --- lib/src/view/game/game_result_dialog.dart | 74 +++++++++++------------ 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/lib/src/view/game/game_result_dialog.dart b/lib/src/view/game/game_result_dialog.dart index 1847c79ab7..bc10161cf0 100644 --- a/lib/src/view/game/game_result_dialog.dart +++ b/lib/src/view/game/game_result_dialog.dart @@ -112,45 +112,45 @@ class _GameEndDialogState extends ConsumerState { maintainSize: true, maintainState: true, visible: animationFinished.value, - child: Column( - children: [ - if (gameState.game.opponent?.offeringRematch == true) - const Padding( - padding: EdgeInsets.only(bottom: 15.0), - child: Text( - 'Your opponent has offered a rematch', - textAlign: TextAlign.center, - ), - ), - if (gameState.game.opponent?.offeringRematch == true) - Padding( - padding: const EdgeInsets.only(bottom: 15.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - FatButton( - semanticsLabel: context.l10n.rematch, - child: const Text('Accept rematch'), - onPressed: () { - ref - .read(ctrlProvider.notifier) - .proposeOrAcceptRematch(); - }, + child: gameState.game.opponent?.offeringRematch == true + ? Column( + children: [ + const Padding( + padding: EdgeInsets.only(bottom: 15.0), + child: Text( + 'Your opponent has offered a rematch', + textAlign: TextAlign.center, ), - SecondaryButton( - semanticsLabel: context.l10n.rematch, - child: const Text('Decline'), - onPressed: () { - ref - .read(ctrlProvider.notifier) - .declineRematch(); - }, + ), + Padding( + padding: const EdgeInsets.only(bottom: 15.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FatButton( + semanticsLabel: context.l10n.rematch, + child: const Text('Accept rematch'), + onPressed: () { + ref + .read(ctrlProvider.notifier) + .proposeOrAcceptRematch(); + }, + ), + SecondaryButton( + semanticsLabel: context.l10n.rematch, + child: const Text('Decline'), + onPressed: () { + ref + .read(ctrlProvider.notifier) + .declineRematch(); + }, + ), + ], ), - ], - ), - ), - ], - ), + ), + ], + ) + : const SizedBox.shrink(), ); }, ), From cff7251ebaca2e12e29a5750043df95a95a841a8 Mon Sep 17 00:00:00 2001 From: Jimima Date: Wed, 20 Nov 2024 11:17:30 +0000 Subject: [PATCH 715/979] Improved animation --- lib/src/view/game/game_result_dialog.dart | 96 ++++++++++------------- 1 file changed, 42 insertions(+), 54 deletions(-) diff --git a/lib/src/view/game/game_result_dialog.dart b/lib/src/view/game/game_result_dialog.dart index bc10161cf0..2611a15696 100644 --- a/lib/src/view/game/game_result_dialog.dart +++ b/lib/src/view/game/game_result_dialog.dart @@ -89,7 +89,6 @@ class _GameEndDialogState extends ConsumerState { Widget build(BuildContext context) { final ctrlProvider = gameControllerProvider(widget.id); final gameState = ref.watch(ctrlProvider).requireValue; - final ValueNotifier animationFinished = ValueNotifier(false); final content = Column( mainAxisSize: MainAxisSize.min, @@ -99,61 +98,50 @@ class _GameEndDialogState extends ConsumerState { padding: const EdgeInsets.only(bottom: 16.0), child: GameResult(game: gameState.game), ), - AnimatedSize( + AnimatedCrossFade( duration: const Duration(milliseconds: 400), - onEnd: () { - animationFinished.value = true; - }, - child: ValueListenableBuilder( - valueListenable: animationFinished, - builder: (context, value, child) { - return Visibility( - maintainAnimation: true, - maintainSize: true, - maintainState: true, - visible: animationFinished.value, - child: gameState.game.opponent?.offeringRematch == true - ? Column( - children: [ - const Padding( - padding: EdgeInsets.only(bottom: 15.0), - child: Text( - 'Your opponent has offered a rematch', - textAlign: TextAlign.center, - ), - ), - Padding( - padding: const EdgeInsets.only(bottom: 15.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - FatButton( - semanticsLabel: context.l10n.rematch, - child: const Text('Accept rematch'), - onPressed: () { - ref - .read(ctrlProvider.notifier) - .proposeOrAcceptRematch(); - }, - ), - SecondaryButton( - semanticsLabel: context.l10n.rematch, - child: const Text('Decline'), - onPressed: () { - ref - .read(ctrlProvider.notifier) - .declineRematch(); - }, - ), - ], - ), - ), - ], - ) - : const SizedBox.shrink(), - ); - }, + firstCurve: Curves.easeOutExpo, + secondCurve: Curves.easeInExpo, + sizeCurve: Curves.easeInOut, + firstChild: const SizedBox.shrink(), + secondChild: Column( + children: [ + const Padding( + padding: EdgeInsets.only(bottom: 15.0), + child: Text( + 'Your opponent has offered a rematch', + textAlign: TextAlign.center, + ), + ), + Padding( + padding: const EdgeInsets.only(bottom: 15.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FatButton( + semanticsLabel: context.l10n.rematch, + child: const Text('Accept rematch'), + onPressed: () { + ref + .read(ctrlProvider.notifier) + .proposeOrAcceptRematch(); + }, + ), + SecondaryButton( + semanticsLabel: context.l10n.rematch, + child: const Text('Decline'), + onPressed: () { + ref.read(ctrlProvider.notifier).declineRematch(); + }, + ), + ], + ), + ), + ], ), + crossFadeState: gameState.game.opponent?.offeringRematch ?? false + ? CrossFadeState.showSecond + : CrossFadeState.showFirst, ), if (gameState.game.me?.offeringRematch == true) SecondaryButton( From b8e7a04eefa1b32222d17e129b8bdab2e77affdd Mon Sep 17 00:00:00 2001 From: Jimima Date: Wed, 20 Nov 2024 15:40:40 +0000 Subject: [PATCH 716/979] Refactor --- lib/src/view/game/game_player.dart | 43 +++++++++++++++--------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/lib/src/view/game/game_player.dart b/lib/src/view/game/game_player.dart index 87af26cf8d..394ebcd672 100644 --- a/lib/src/view/game/game_player.dart +++ b/lib/src/view/game/game_player.dart @@ -144,8 +144,7 @@ class GamePlayer extends StatelessWidget { ), if (timeToMove != null) MoveExpiration(timeToMove: timeToMove!, mePlaying: mePlaying) - else if (materialDiff != null && - materialDifferenceFormat?.visible == true) + else if (materialDiff != null) MaterialDifferenceDisplay( materialDiff: materialDiff!, materialDifferenceFormat: materialDifferenceFormat!, @@ -329,25 +328,27 @@ class MaterialDifferenceDisplay extends StatelessWidget { ? materialDiff.capturedPieces : materialDiff.pieces); - return Row( - children: [ - for (final role in Role.values) - for (int i = 0; i < piecesToRender[role]!; i++) - Icon( - _iconByRole[role], - size: 13, - color: Colors.grey, - ), - const SizedBox(width: 3), - Text( - style: const TextStyle( - fontSize: 13, - color: Colors.grey, - ), - materialDiff.score > 0 ? '+${materialDiff.score}' : '', - ), - ], - ); + return !materialDifferenceFormat.visible + ? const SizedBox.shrink() + : Row( + children: [ + for (final role in Role.values) + for (int i = 0; i < piecesToRender[role]!; i++) + Icon( + _iconByRole[role], + size: 13, + color: Colors.grey, + ), + const SizedBox(width: 3), + Text( + style: const TextStyle( + fontSize: 13, + color: Colors.grey, + ), + materialDiff.score > 0 ? '+${materialDiff.score}' : '', + ), + ], + ); } } From a4716f7523a8b79dc7f27b27027844de4e2c43f7 Mon Sep 17 00:00:00 2001 From: Jimima Date: Wed, 20 Nov 2024 16:44:11 +0000 Subject: [PATCH 717/979] Syntax fix(?) --- .../model/account/account_preferences.dart | 16 +- lib/src/model/clock/chess_clock.dart | 184 +++++++ lib/src/model/clock/clock_controller.dart | 191 ------- .../model/clock/clock_tool_controller.dart | 231 ++++++++ .../offline_correspondence_game.dart | 1 - lib/src/model/game/chat_controller.dart | 59 ++- lib/src/model/game/game.dart | 9 - lib/src/model/game/game_controller.dart | 135 ++++- lib/src/model/game/game_socket_events.dart | 9 +- lib/src/model/game/game_status.dart | 15 + lib/src/model/game/playable_game.dart | 26 +- lib/src/model/settings/board_preferences.dart | 38 +- lib/src/model/study/study_controller.dart | 80 ++- lib/src/model/tv/tv_controller.dart | 21 + lib/src/network/socket.dart | 3 +- lib/src/view/clock/clock_settings.dart | 18 +- ...ock_screen.dart => clock_tool_screen.dart} | 83 +-- lib/src/view/clock/custom_clock_settings.dart | 6 +- .../offline_correspondence_game_screen.dart | 2 +- lib/src/view/game/archived_game_screen.dart | 16 +- .../game/correspondence_clock_widget.dart | 2 +- lib/src/view/game/game_body.dart | 96 ++-- lib/src/view/game/game_common_widgets.dart | 5 + lib/src/view/game/game_player.dart | 56 +- lib/src/view/game/game_settings.dart | 38 +- lib/src/view/game/message_screen.dart | 51 +- .../over_the_board/over_the_board_screen.dart | 2 +- lib/src/view/puzzle/puzzle_screen.dart | 5 + .../view/puzzle/puzzle_settings_screen.dart | 49 +- .../view/settings/board_settings_screen.dart | 229 +++++++- .../piece_shift_method_settings_screen.dart | 79 --- lib/src/view/settings/theme_screen.dart | 13 +- lib/src/view/study/study_bottom_bar.dart | 116 ++++ lib/src/view/study/study_gamebook.dart | 174 ++++++ lib/src/view/study/study_screen.dart | 14 +- lib/src/view/tools/tools_tab_screen.dart | 4 +- lib/src/view/watch/tv_screen.dart | 28 +- lib/src/widgets/board_table.dart | 12 +- .../{countdown_clock.dart => clock.dart} | 310 +++++------ lib/src/widgets/rating.dart | 4 +- .../challenge/challenge_service_test.dart | 6 +- test/model/clock/chess_clock_test.dart | 231 ++++++++ test/model/game/game_socket_events_test.dart | 17 +- test/model/game/game_socket_example_data.dart | 103 ++++ test/network/fake_websocket_channel.dart | 38 +- test/network/socket_test.dart | 13 +- test/test_container.dart | 2 +- test/test_helpers.dart | 8 +- test/test_provider_scope.dart | 2 +- test/view/game/game_screen_test.dart | 497 ++++++++++++++++++ .../over_the_board_screen_test.dart | 40 +- test/view/puzzle/puzzle_screen_test.dart | 14 +- test/view/puzzle/storm_screen_test.dart | 11 +- test/view/study/study_screen_test.dart | 233 +++++++- test/view/user/perf_stats_screen_test.dart | 2 +- test/widgets/clock_test.dart | 249 +++++++++ 56 files changed, 3113 insertions(+), 783 deletions(-) create mode 100644 lib/src/model/clock/chess_clock.dart delete mode 100644 lib/src/model/clock/clock_controller.dart create mode 100644 lib/src/model/clock/clock_tool_controller.dart rename lib/src/view/clock/{clock_screen.dart => clock_tool_screen.dart} (73%) delete mode 100644 lib/src/view/settings/piece_shift_method_settings_screen.dart create mode 100644 lib/src/view/study/study_gamebook.dart rename lib/src/widgets/{countdown_clock.dart => clock.dart} (64%) create mode 100644 test/model/clock/chess_clock_test.dart create mode 100644 test/model/game/game_socket_example_data.dart create mode 100644 test/view/game/game_screen_test.dart create mode 100644 test/widgets/clock_test.dart diff --git a/lib/src/model/account/account_preferences.dart b/lib/src/model/account/account_preferences.dart index ee6950f089..343843f1d7 100644 --- a/lib/src/model/account/account_preferences.dart +++ b/lib/src/model/account/account_preferences.dart @@ -1,5 +1,6 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; @@ -29,28 +30,31 @@ typedef AccountPrefState = ({ }); /// A provider that tells if the user wants to see ratings in the app. -final showRatingsPrefProvider = FutureProvider((ref) async { +@Riverpod(keepAlive: true) +Future showRatingsPref(Ref ref) async { return ref.watch( accountPreferencesProvider .selectAsync((state) => state?.showRatings.value ?? true), ); -}); +} -final clockSoundProvider = FutureProvider((ref) async { +@Riverpod(keepAlive: true) +Future clockSound(Ref ref) async { return ref.watch( accountPreferencesProvider .selectAsync((state) => state?.clockSound.value ?? true), ); -}); +} -final pieceNotationProvider = FutureProvider((ref) async { +@Riverpod(keepAlive: true) +Future pieceNotation(Ref ref) async { return ref.watch( accountPreferencesProvider.selectAsync( (state) => state?.pieceNotation ?? defaultAccountPreferences.pieceNotation, ), ); -}); +} final defaultAccountPreferences = ( zenMode: Zen.no, diff --git a/lib/src/model/clock/chess_clock.dart b/lib/src/model/clock/chess_clock.dart new file mode 100644 index 0000000000..b4560932e2 --- /dev/null +++ b/lib/src/model/clock/chess_clock.dart @@ -0,0 +1,184 @@ +import 'dart:async'; + +import 'package:clock/clock.dart'; +import 'package:dartchess/dartchess.dart'; +import 'package:flutter/foundation.dart'; + +const _emergencyDelay = Duration(seconds: 20); +const _tickDelay = Duration(milliseconds: 100); + +/// A chess clock. +class ChessClock { + ChessClock({ + required Duration whiteTime, + required Duration blackTime, + this.emergencyThreshold, + this.onFlag, + this.onEmergency, + }) : _whiteTime = ValueNotifier(whiteTime), + _blackTime = ValueNotifier(blackTime), + _activeSide = Side.white; + + /// The threshold at which the clock will call [onEmergency] if provided. + final Duration? emergencyThreshold; + + /// Callback when the clock reaches zero. + VoidCallback? onFlag; + + /// Called when one clock timers reaches the emergency threshold. + final void Function(Side activeSide)? onEmergency; + + Timer? _timer; + Timer? _startDelayTimer; + DateTime? _lastStarted; + final _stopwatch = clock.stopwatch(); + bool _shouldPlayEmergencyFeedback = true; + DateTime? _nextEmergency; + + final ValueNotifier _whiteTime; + final ValueNotifier _blackTime; + Side _activeSide; + + bool get isRunning { + return _lastStarted != null; + } + + /// Returns the current white time. + ValueListenable get whiteTime => _whiteTime; + + /// Returns the current black time. + ValueListenable get blackTime => _blackTime; + + /// Returns the current active time. + ValueListenable get activeTime => _activeTime; + + /// Returns the current active side. + Side get activeSide => _activeSide; + + /// Sets the time for either side. + void setTimes({Duration? whiteTime, Duration? blackTime}) { + if (whiteTime != null) { + _whiteTime.value = whiteTime; + } + if (blackTime != null) { + _blackTime.value = blackTime; + } + } + + /// Sets the time for the given side. + void setTime(Side side, Duration time) { + if (side == Side.white) { + _whiteTime.value = time; + } else { + _blackTime.value = time; + } + } + + /// Increments the time for either side. + void incTimes({Duration? whiteInc, Duration? blackInc}) { + if (whiteInc != null) { + _whiteTime.value += whiteInc; + } + if (blackInc != null) { + _blackTime.value += blackInc; + } + } + + /// Increments the time for the given side. + void incTime(Side side, Duration increment) { + if (side == Side.white) { + _whiteTime.value += increment; + } else { + _blackTime.value += increment; + } + } + + /// Starts the clock and switch to the given side. + /// + /// Trying to start an already running clock on the same side is a no-op. + /// + /// The [delay] parameter can be used to add a delay before the clock starts counting down. This is useful for lag compensation. + /// + /// Returns the think time of the active side before switching or `null` if the clock is not running. + Duration? startSide(Side side, {Duration? delay}) { + if (isRunning && _activeSide == side) { + return _thinkTime; + } + _activeSide = side; + final thinkTime = _thinkTime; + start(delay: delay); + return thinkTime; + } + + /// Starts the clock. + /// + /// The [delay] parameter can be used to add a delay before the clock starts counting down. This is useful for lag compensation. + void start({Duration? delay}) { + _lastStarted = clock.now().add(delay ?? Duration.zero); + _startDelayTimer?.cancel(); + _startDelayTimer = Timer(delay ?? Duration.zero, _scheduleTick); + } + + /// Pauses the clock. + /// + /// Returns the current think time for the active side. + Duration stop() { + _stopwatch.stop(); + _startDelayTimer?.cancel(); + _timer?.cancel(); + final thinkTime = _thinkTime ?? Duration.zero; + _lastStarted = null; + return thinkTime; + } + + void dispose() { + _timer?.cancel(); + _startDelayTimer?.cancel(); + _whiteTime.dispose(); + _blackTime.dispose(); + } + + /// Returns the current think time for the active side. + Duration? get _thinkTime { + if (_lastStarted == null) { + return null; + } + return clock.now().difference(_lastStarted!); + } + + ValueNotifier get _activeTime { + return activeSide == Side.white ? _whiteTime : _blackTime; + } + + void _scheduleTick() { + _stopwatch.reset(); + _stopwatch.start(); + _timer?.cancel(); + _timer = Timer(_tickDelay, _tick); + } + + void _tick() { + final newTime = _activeTime.value - _stopwatch.elapsed; + _activeTime.value = newTime < Duration.zero ? Duration.zero : newTime; + _checkEmergency(); + if (_activeTime.value == Duration.zero) { + onFlag?.call(); + } + _scheduleTick(); + } + + void _checkEmergency() { + final timeLeft = _activeTime.value; + if (emergencyThreshold != null && + timeLeft <= emergencyThreshold! && + _shouldPlayEmergencyFeedback && + (_nextEmergency == null || _nextEmergency!.isBefore(clock.now()))) { + _shouldPlayEmergencyFeedback = false; + _nextEmergency = clock.now().add(_emergencyDelay); + onEmergency?.call(_activeSide); + } else if (emergencyThreshold != null && + timeLeft > emergencyThreshold! * 1.5) { + _shouldPlayEmergencyFeedback = true; + } + } +} diff --git a/lib/src/model/clock/clock_controller.dart b/lib/src/model/clock/clock_controller.dart deleted file mode 100644 index 1a70a0ff92..0000000000 --- a/lib/src/model/clock/clock_controller.dart +++ /dev/null @@ -1,191 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; -import 'package:lichess_mobile/src/model/common/time_increment.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'clock_controller.freezed.dart'; -part 'clock_controller.g.dart'; - -@riverpod -class ClockController extends _$ClockController { - @override - ClockState build() { - const time = Duration(minutes: 10); - const increment = Duration.zero; - return ClockState.fromOptions( - const ClockOptions( - timePlayerTop: time, - timePlayerBottom: time, - incrementPlayerTop: increment, - incrementPlayerBottom: increment, - ), - ); - } - - void onTap(ClockPlayerType playerType) { - final started = state.started; - if (playerType == ClockPlayerType.top) { - state = state.copyWith( - started: true, - activeSide: ClockPlayerType.bottom, - playerTopMoves: started ? state.playerTopMoves + 1 : 0, - ); - } else { - state = state.copyWith( - started: true, - activeSide: ClockPlayerType.top, - playerBottomMoves: started ? state.playerBottomMoves + 1 : 0, - ); - } - ref.read(soundServiceProvider).play(Sound.clock); - } - - void updateDuration(ClockPlayerType playerType, Duration duration) { - if (state.loser != null || state.paused) { - return; - } - - if (playerType == ClockPlayerType.top) { - state = state.copyWith( - playerTopTime: duration + state.options.incrementPlayerTop, - ); - } else { - state = state.copyWith( - playerBottomTime: duration + state.options.incrementPlayerBottom, - ); - } - } - - void updateOptions(TimeIncrement timeIncrement) => - state = ClockState.fromTimeIncrement(timeIncrement); - - void updateOptionsCustom( - TimeIncrement clock, - ClockPlayerType player, - ) => - state = ClockState.fromOptions( - ClockOptions( - timePlayerTop: player == ClockPlayerType.top - ? Duration(seconds: clock.time) - : state.options.timePlayerTop, - timePlayerBottom: player == ClockPlayerType.bottom - ? Duration(seconds: clock.time) - : state.options.timePlayerBottom, - incrementPlayerTop: player == ClockPlayerType.top - ? Duration(seconds: clock.increment) - : state.options.incrementPlayerTop, - incrementPlayerBottom: player == ClockPlayerType.bottom - ? Duration(seconds: clock.increment) - : state.options.incrementPlayerBottom, - ), - ); - - void setActiveSide(ClockPlayerType playerType) => - state = state.copyWith(activeSide: playerType); - - void setLoser(ClockPlayerType playerType) => - state = state.copyWith(loser: playerType); - - void reset() => state = ClockState.fromOptions(state.options); - - void start() => state = state.copyWith(started: true); - - void pause() => state = state.copyWith(paused: true); - - void resume() => state = state.copyWith(paused: false); -} - -enum ClockPlayerType { top, bottom } - -@freezed -class ClockOptions with _$ClockOptions { - const ClockOptions._(); - - const factory ClockOptions({ - required Duration timePlayerTop, - required Duration timePlayerBottom, - required Duration incrementPlayerTop, - required Duration incrementPlayerBottom, - }) = _ClockOptions; -} - -@freezed -class ClockState with _$ClockState { - const ClockState._(); - - const factory ClockState({ - required int id, - required ClockOptions options, - required Duration playerTopTime, - required Duration playerBottomTime, - required ClockPlayerType activeSide, - ClockPlayerType? loser, - @Default(false) bool started, - @Default(false) bool paused, - @Default(0) int playerTopMoves, - @Default(0) int playerBottomMoves, - }) = _ClockState; - - factory ClockState.fromTimeIncrement(TimeIncrement timeIncrement) { - final options = ClockOptions( - timePlayerTop: Duration(seconds: timeIncrement.time), - timePlayerBottom: Duration(seconds: timeIncrement.time), - incrementPlayerTop: Duration(seconds: timeIncrement.increment), - incrementPlayerBottom: Duration(seconds: timeIncrement.increment), - ); - - return ClockState( - id: DateTime.now().millisecondsSinceEpoch, - options: options, - activeSide: ClockPlayerType.top, - playerTopTime: options.timePlayerTop, - playerBottomTime: options.timePlayerBottom, - ); - } - - factory ClockState.fromSeparateTimeIncrements( - TimeIncrement playerTop, - TimeIncrement playerBottom, - ) { - final options = ClockOptions( - timePlayerTop: Duration(seconds: playerTop.time), - timePlayerBottom: Duration(seconds: playerBottom.time), - incrementPlayerTop: Duration(seconds: playerTop.increment), - incrementPlayerBottom: Duration(seconds: playerBottom.increment), - ); - return ClockState( - id: DateTime.now().millisecondsSinceEpoch, - activeSide: ClockPlayerType.top, - options: options, - playerTopTime: options.timePlayerTop, - playerBottomTime: options.timePlayerBottom, - ); - } - - factory ClockState.fromOptions(ClockOptions options) { - return ClockState( - id: DateTime.now().millisecondsSinceEpoch, - activeSide: ClockPlayerType.top, - options: options, - playerTopTime: options.timePlayerTop, - playerBottomTime: options.timePlayerBottom, - ); - } - - Duration getDuration(ClockPlayerType playerType) => - playerType == ClockPlayerType.top ? playerTopTime : playerBottomTime; - - int getMovesCount(ClockPlayerType playerType) => - playerType == ClockPlayerType.top ? playerTopMoves : playerBottomMoves; - - bool isPlayersTurn(ClockPlayerType playerType) => - started && activeSide == playerType && loser == null; - - bool isPlayersMoveAllowed(ClockPlayerType playerType) => - isPlayersTurn(playerType) && !paused; - - bool isActivePlayer(ClockPlayerType playerType) => - isPlayersTurn(playerType) && !paused; - - bool isLoser(ClockPlayerType playerType) => loser == playerType; -} diff --git a/lib/src/model/clock/clock_tool_controller.dart b/lib/src/model/clock/clock_tool_controller.dart new file mode 100644 index 0000000000..524b543a9d --- /dev/null +++ b/lib/src/model/clock/clock_tool_controller.dart @@ -0,0 +1,231 @@ +import 'package:dartchess/dartchess.dart'; +import 'package:flutter/foundation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:lichess_mobile/src/model/clock/chess_clock.dart'; +import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; +import 'package:lichess_mobile/src/model/common/time_increment.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'clock_tool_controller.freezed.dart'; +part 'clock_tool_controller.g.dart'; + +@riverpod +class ClockToolController extends _$ClockToolController { + late final ChessClock _clock; + + @override + ClockState build() { + const time = Duration(minutes: 10); + const increment = Duration.zero; + const options = ClockOptions( + whiteTime: time, + blackTime: time, + whiteIncrement: increment, + blackIncrement: increment, + ); + _clock = ChessClock( + whiteTime: time, + blackTime: time, + onFlag: _onFlagged, + ); + + ref.onDispose(() { + _clock.dispose(); + }); + + return ClockState( + options: options, + whiteTime: _clock.whiteTime, + blackTime: _clock.blackTime, + activeSide: Side.white, + ); + } + + void _onFlagged() { + _clock.stop(); + state = state.copyWith(flagged: _clock.activeSide); + } + + void onTap(Side playerType) { + final started = state.started; + if (playerType == Side.white) { + state = state.copyWith( + started: true, + activeSide: Side.black, + whiteMoves: started ? state.whiteMoves + 1 : 0, + ); + } else { + state = state.copyWith( + started: true, + activeSide: Side.white, + blackMoves: started ? state.blackMoves + 1 : 0, + ); + } + ref.read(soundServiceProvider).play(Sound.clock); + _clock.startSide(playerType.opposite); + _clock.incTime( + playerType, + playerType == Side.white + ? state.options.whiteIncrement + : state.options.blackIncrement, + ); + } + + void updateDuration(Side playerType, Duration duration) { + if (state.flagged != null || state.paused) { + return; + } + + _clock.setTimes( + whiteTime: playerType == Side.white + ? duration + state.options.whiteIncrement + : null, + blackTime: playerType == Side.black + ? duration + state.options.blackIncrement + : null, + ); + } + + void updateOptions(TimeIncrement timeIncrement) { + final options = ClockOptions.fromTimeIncrement(timeIncrement); + _clock.setTimes( + whiteTime: options.whiteTime, + blackTime: options.blackTime, + ); + state = state.copyWith( + options: options, + whiteTime: _clock.whiteTime, + blackTime: _clock.blackTime, + ); + } + + void updateOptionsCustom( + TimeIncrement clock, + Side player, + ) { + final options = ClockOptions( + whiteTime: player == Side.white + ? Duration(seconds: clock.time) + : state.options.whiteTime, + blackTime: player == Side.black + ? Duration(seconds: clock.time) + : state.options.blackTime, + whiteIncrement: player == Side.white + ? Duration(seconds: clock.increment) + : state.options.whiteIncrement, + blackIncrement: player == Side.black + ? Duration(seconds: clock.increment) + : state.options.blackIncrement, + ); + _clock.setTimes( + whiteTime: options.whiteTime, + blackTime: options.blackTime, + ); + state = ClockState( + options: options, + whiteTime: _clock.whiteTime, + blackTime: _clock.blackTime, + activeSide: state.activeSide, + ); + } + + void setBottomPlayer(Side playerType) => + state = state.copyWith(bottomPlayer: playerType); + + void reset() { + _clock.setTimes( + whiteTime: state.options.whiteTime, + blackTime: state.options.whiteTime, + ); + state = state.copyWith( + whiteTime: _clock.whiteTime, + blackTime: _clock.blackTime, + activeSide: Side.white, + flagged: null, + started: false, + paused: false, + whiteMoves: 0, + blackMoves: 0, + ); + } + + void start() { + _clock.start(); + state = state.copyWith(started: true); + } + + void pause() { + _clock.stop(); + state = state.copyWith(paused: true); + } + + void resume() { + _clock.start(); + state = state.copyWith(paused: false); + } +} + +@freezed +class ClockOptions with _$ClockOptions { + const ClockOptions._(); + + const factory ClockOptions({ + required Duration whiteTime, + required Duration blackTime, + required Duration whiteIncrement, + required Duration blackIncrement, + }) = _ClockOptions; + + factory ClockOptions.fromTimeIncrement(TimeIncrement timeIncrement) => + ClockOptions( + whiteTime: Duration(seconds: timeIncrement.time), + blackTime: Duration(seconds: timeIncrement.time), + whiteIncrement: Duration(seconds: timeIncrement.increment), + blackIncrement: Duration(seconds: timeIncrement.increment), + ); + + factory ClockOptions.fromSeparateTimeIncrements( + TimeIncrement playerTop, + TimeIncrement playerBottom, + ) => + ClockOptions( + whiteTime: Duration(seconds: playerTop.time), + blackTime: Duration(seconds: playerBottom.time), + whiteIncrement: Duration(seconds: playerTop.increment), + blackIncrement: Duration(seconds: playerBottom.increment), + ); +} + +@freezed +class ClockState with _$ClockState { + const ClockState._(); + + const factory ClockState({ + required ClockOptions options, + required ValueListenable whiteTime, + required ValueListenable blackTime, + required Side activeSide, + @Default(Side.white) Side bottomPlayer, + Side? flagged, + @Default(false) bool started, + @Default(false) bool paused, + @Default(0) int whiteMoves, + @Default(0) int blackMoves, + }) = _ClockState; + + ValueListenable getDuration(Side playerType) => + playerType == Side.white ? whiteTime : blackTime; + + int getMovesCount(Side playerType) => + playerType == Side.white ? whiteMoves : blackMoves; + + bool isPlayersTurn(Side playerType) => + started && activeSide == playerType && flagged == null; + + bool isPlayersMoveAllowed(Side playerType) => + isPlayersTurn(playerType) && !paused; + + bool isActivePlayer(Side playerType) => isPlayersTurn(playerType) && !paused; + + bool isFlagged(Side playerType) => flagged == playerType; +} diff --git a/lib/src/model/correspondence/offline_correspondence_game.dart b/lib/src/model/correspondence/offline_correspondence_game.dart index 1c8fc0cf5a..d7bcb1599f 100644 --- a/lib/src/model/correspondence/offline_correspondence_game.dart +++ b/lib/src/model/correspondence/offline_correspondence_game.dart @@ -72,7 +72,6 @@ class OfflineCorrespondenceGame return null; } - bool get isPlayerTurn => lastPosition.turn == youAre; bool get playable => status.value < GameStatus.aborted.value; bool get playing => status.value > GameStatus.started.value; bool get finished => status.value >= GameStatus.mate.value; diff --git a/lib/src/model/game/chat_controller.dart b/lib/src/model/game/chat_controller.dart index 6ec13b78a6..8c0f294945 100644 --- a/lib/src/model/game/chat_controller.dart +++ b/lib/src/model/game/chat_controller.dart @@ -125,14 +125,8 @@ class ChatController extends _$ChatController { } } else if (event.topic == 'message') { final data = event.data as Map; - final message = data['t'] as String; - final username = data['u'] as String?; - _addMessage( - ( - message: message, - username: username, - ), - ); + final message = _messageFromPick(RequiredPick(data)); + _addMessage(message); } } } @@ -147,11 +141,58 @@ class ChatState with _$ChatState { }) = _ChatState; } -typedef Message = ({String? username, String message}); +typedef Message = ({ + String? username, + String message, + bool troll, + bool deleted, +}); Message _messageFromPick(RequiredPick pick) { return ( message: pick('t').asStringOrThrow(), username: pick('u').asStringOrNull(), + troll: pick('r').asBoolOrNull() ?? false, + deleted: pick('d').asBoolOrNull() ?? false, ); } + +bool isSpam(Message message) { + return spamRegex.hasMatch(message.message) || + followMeRegex.hasMatch(message.message); +} + +final RegExp spamRegex = RegExp( + [ + 'xcamweb.com', + '(^|[^i])chess-bot', + 'chess-cheat', + 'coolteenbitch', + 'letcafa.webcam', + 'tinyurl.com/', + 'wooga.info/', + 'bit.ly/', + 'wbt.link/', + 'eb.by/', + '001.rs/', + 'shr.name/', + 'u.to/', + '.3-a.net', + '.ssl443.org', + '.ns02.us', + '.myftp.info', + '.flinkup.com', + '.serveusers.com', + 'badoogirls.com', + 'hide.su', + 'wyon.de', + 'sexdatingcz.club', + 'qps.ru', + 'tiny.cc/', + 'trasderk.blogspot.com', + 't.ly/', + 'shorturl.at/', + ].map((url) => url.replaceAll('.', '\\.').replaceAll('/', '\\/')).join('|'), +); + +final followMeRegex = RegExp('follow me|join my team', caseSensitive: false); diff --git a/lib/src/model/game/game.dart b/lib/src/model/game/game.dart index 13d11db993..5e0a3750fe 100644 --- a/lib/src/model/game/game.dart +++ b/lib/src/model/game/game.dart @@ -319,15 +319,6 @@ class GameMeta with _$GameMeta { _$GameMetaFromJson(json); } -@freezed -class PlayableClockData with _$PlayableClockData { - const factory PlayableClockData({ - required bool running, - required Duration white, - required Duration black, - }) = _PlayableClockData; -} - @Freezed(fromJson: true, toJson: true) class CorrespondenceClockData with _$CorrespondenceClockData { const factory CorrespondenceClockData({ diff --git a/lib/src/model/game/game_controller.dart b/lib/src/model/game/game_controller.dart index d2a4ac3791..51a8cbb023 100644 --- a/lib/src/model/game/game_controller.dart +++ b/lib/src/model/game/game_controller.dart @@ -5,6 +5,7 @@ import 'package:collection/collection.dart'; import 'package:dartchess/dartchess.dart'; import 'package:deep_pick/deep_pick.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -12,6 +13,7 @@ import 'package:lichess_mobile/src/model/account/account_preferences.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/analysis/server_analysis_service.dart'; +import 'package:lichess_mobile/src/model/clock/chess_clock.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/service/move_feedback.dart'; @@ -62,14 +64,12 @@ class GameController extends _$GameController { /// Last socket version received int? _socketEventVersion; - /// Last move time - DateTime? _lastMoveTime; - - late SocketClient _socketClient; - static Uri gameSocketUri(GameFullId gameFullId) => Uri(path: '/play/$gameFullId/v6'); + ChessClock? _clock; + late final SocketClient _socketClient; + @override Future build(GameFullId gameFullId) { final socketPool = ref.watch(socketPoolProvider); @@ -85,6 +85,7 @@ class GameController extends _$GameController { _opponentLeftCountdownTimer?.cancel(); _transientMoveTimer?.cancel(); _appLifecycleListener?.dispose(); + _clock?.dispose(); }); return _socketClient.stream.firstWhere((e) => e.topic == 'full').then( @@ -113,6 +114,13 @@ class GameController extends _$GameController { _socketEventVersion = fullEvent.socketEventVersion; + // Play "dong" sound when this is a new game and we're playing it (not spectating) + final isMyGame = game.youAre != null; + final noMovePlayed = game.steps.length == 1; + if (isMyGame && noMovePlayed && game.status == GameStatus.started) { + ref.read(soundServiceProvider).play(Sound.dong); + } + if (game.playable) { _appLifecycleListener = AppLifecycleListener( onResume: () { @@ -123,13 +131,29 @@ class GameController extends _$GameController { } }, ); + + if (game.clock != null) { + _clock = ChessClock( + whiteTime: game.clock!.white, + blackTime: game.clock!.black, + emergencyThreshold: game.meta.clock?.emergency, + onEmergency: onClockEmergency, + onFlag: onFlag, + ); + if (game.clock!.running) { + final pos = game.lastPosition; + if (pos.fullmoves > 1) { + _clock!.startSide(pos.turn); + } + } + } } return GameState( gameFullId: gameFullId, game: game, stepCursor: game.steps.length - 1, - stopClockWaitingForServerAck: false, + liveClock: _liveClock, ); }, ); @@ -161,7 +185,6 @@ class GameController extends _$GameController { steps: curState.game.steps.add(newStep), ), stepCursor: curState.stepCursor + 1, - stopClockWaitingForServerAck: !shouldConfirmMove, moveToConfirm: shouldConfirmMove ? move : null, promotionMove: null, premove: null, @@ -174,7 +197,6 @@ class GameController extends _$GameController { _sendMoveToSocket( move, isPremove: isPremove ?? false, - hasClock: curState.game.clock != null, // same logic as web client // we want to send client lag only at the beginning of the game when the clock is not running yet withLag: @@ -229,14 +251,12 @@ class GameController extends _$GameController { state = AsyncValue.data( curState.copyWith( - stopClockWaitingForServerAck: true, moveToConfirm: null, ), ); _sendMoveToSocket( moveToConfirm, isPremove: false, - hasClock: curState.game.clock != null, // same logic as web client // we want to send client lag only at the beginning of the game when the clock is not running yet withLag: curState.game.clock != null && curState.activeClockSide == null, @@ -334,6 +354,15 @@ class GameController extends _$GameController { } } + /// Play a sound when the clock is about to run out + Future onClockEmergency(Side activeSide) async { + if (activeSide != state.valueOrNull?.game.youAre) return; + final shouldPlay = await ref.read(clockSoundProvider.future); + if (shouldPlay) { + ref.read(soundServiceProvider).play(Sound.lowTime); + } + } + void onFlag() { _onFlagThrottler(() { if (state.hasValue) { @@ -410,18 +439,39 @@ class GameController extends _$GameController { Future.value(); } + /// Gets the live game clock if available. + LiveGameClock? get _liveClock => _clock != null + ? ( + white: _clock!.whiteTime, + black: _clock!.blackTime, + ) + : null; + + /// Update the internal clock on clock server event + void _updateClock({ + required Duration white, + required Duration black, + required Side? activeSide, + Duration? lag, + }) { + _clock?.setTimes(whiteTime: white, blackTime: black); + if (activeSide != null) { + _clock?.startSide(activeSide, delay: lag); + } else { + _clock?.stop(); + } + } + void _sendMoveToSocket( Move move, { required bool isPremove, - required bool hasClock, required bool withLag, }) { - final moveTime = hasClock + final thinkTime = _clock?.stop(); + final moveTime = _clock != null ? isPremove == true ? Duration.zero - : _lastMoveTime != null - ? DateTime.now().difference(_lastMoveTime!) - : null + : thinkTime : null; _socketClient.send( 'move', @@ -431,7 +481,7 @@ class GameController extends _$GameController { 's': (moveTime.inMilliseconds * 0.1).round().toRadixString(36), }, ackable: true, - withLag: hasClock && (moveTime == null || withLag), + withLag: _clock != null && (moveTime == null || withLag), ); _transientMoveTimer = Timer(const Duration(seconds: 10), _resyncGameData); @@ -542,20 +592,27 @@ class GameController extends _$GameController { return; } _socketEventVersion = fullEvent.socketEventVersion; - _lastMoveTime = null; state = AsyncValue.data( GameState( gameFullId: gameFullId, game: fullEvent.game, stepCursor: fullEvent.game.steps.length - 1, - stopClockWaitingForServerAck: false, + liveClock: _liveClock, // cancel the premove to avoid playing wrong premove when the full // game data is reloaded premove: null, ), ); + if (fullEvent.game.clock != null) { + _updateClock( + white: fullEvent.game.clock!.white, + black: fullEvent.game.clock!.black, + activeSide: state.requireValue.activeClockSide, + ); + } + // Move event, received after sending a move or receiving a move from the // opponent case 'move': @@ -602,11 +659,24 @@ class GameController extends _$GameController { } } - // TODO handle delay if (data.clock != null) { - _lastMoveTime = DateTime.now(); + final lag = newState.game.playable && newState.game.isMyTurn + // my own clock doesn't need to be compensated for + ? Duration.zero + // server will send the lag only if it's more than 10ms + // default lag of 10ms is also used by web client + : data.clock?.lag ?? const Duration(milliseconds: 10); + + _updateClock( + white: data.clock!.white, + black: data.clock!.black, + lag: lag, + activeSide: newState.activeClockSide, + ); if (newState.game.clock != null) { + // we don't rely on these values to display the clock, but let's keep + // the game object in sync newState = newState.copyWith.game.clock!( white: data.clock!.white, black: data.clock!.black, @@ -617,10 +687,6 @@ class GameController extends _$GameController { black: data.clock!.black, ); } - - newState = newState.copyWith( - stopClockWaitingForServerAck: false, - ); } if (newState.game.expiration != null) { @@ -686,6 +752,11 @@ class GameController extends _$GameController { white: endData.clock!.white, black: endData.clock!.black, ); + _updateClock( + white: endData.clock!.white, + black: endData.clock!.black, + activeSide: newState.activeClockSide, + ); } if (curState.game.lastPosition.fullmoves > 1) { @@ -723,7 +794,11 @@ class GameController extends _$GameController { final newClock = pick(data['total']) .letOrNull((it) => Duration(milliseconds: it.asIntOrThrow() * 10)); final curState = state.requireValue; + if (side != null && newClock != null) { + _clock?.setTime(side, newClock); + + // sync game clock object even if it's not used to display the clock final newState = side == Side.white ? curState.copyWith.game.clock!( white: newClock, @@ -978,6 +1053,11 @@ class GameController extends _$GameController { } } +typedef LiveGameClock = ({ + ValueListenable white, + ValueListenable black, +}); + @freezed class GameState with _$GameState { const GameState._(); @@ -986,9 +1066,9 @@ class GameState with _$GameState { required GameFullId gameFullId, required PlayableGame game, required int stepCursor, + required LiveGameClock? liveClock, int? lastDrawOfferAtPly, Duration? opponentLeftCountdown, - required bool stopClockWaitingForServerAck, /// Promotion waiting to be selected (only if auto queen is disabled) NormalMove? promotionMove, @@ -1057,7 +1137,7 @@ class GameState with _$GameState { game.drawable && (lastDrawOfferAtPly ?? -99) < game.lastPly - 20; bool get canShowClaimWinCountdown => - !game.isPlayerTurn && + !game.isMyTurn && game.resignable && (game.meta.rules == null || !game.meta.rules!.contains(GameRule.noClaimWin)); @@ -1090,9 +1170,6 @@ class GameState with _$GameState { if (game.clock == null && game.correspondenceClock == null) { return null; } - if (stopClockWaitingForServerAck) { - return null; - } if (game.status == GameStatus.started) { final pos = game.lastPosition; if (pos.fullmoves > 1) { diff --git a/lib/src/model/game/game_socket_events.dart b/lib/src/model/game/game_socket_events.dart index df44964a89..cfd8dd10e8 100644 --- a/lib/src/model/game/game_socket_events.dart +++ b/lib/src/model/game/game_socket_events.dart @@ -1,3 +1,4 @@ +import 'package:clock/clock.dart'; import 'package:dartchess/dartchess.dart'; import 'package:deep_pick/deep_pick.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; @@ -40,7 +41,12 @@ class MoveEvent with _$MoveEvent { bool? blackOfferingDraw, GameStatus? status, Side? winner, - ({Duration white, Duration black, Duration? lag})? clock, + ({ + Duration white, + Duration black, + Duration? lag, + DateTime at, + })? clock, }) = _MoveEvent; factory MoveEvent.fromJson(Map json) => @@ -78,6 +84,7 @@ MoveEvent _socketMoveEventFromPick(RequiredPick pick) { blackOfferingDraw: pick('bDraw').asBoolOrNull(), clock: pick('clock').letOrNull( (it) => ( + at: clock.now(), white: it('white').asDurationFromSecondsOrThrow(), black: it('black').asDurationFromSecondsOrThrow(), lag: it('lag') diff --git a/lib/src/model/game/game_status.dart b/lib/src/model/game/game_status.dart index 58047c6aac..b091550940 100644 --- a/lib/src/model/game/game_status.dart +++ b/lib/src/model/game/game_status.dart @@ -2,19 +2,34 @@ import 'package:deep_pick/deep_pick.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; enum GameStatus { + /// Unknown game status (not handled by the app). unknown(-1), + + /// The game is created but not started yet. created(10), started(20), + + /// From here on, the game is finished. aborted(25), mate(30), resign(31), stalemate(32), + + /// When a player leaves the game. timeout(33), draw(34), + + /// When a player runs out of time (clock flags). outoftime(35), cheat(36), + + /// The player did not make the first move in time. noStart(37), + + /// We don't know why the game ended. unknownFinish(38), + + /// Chess variant special endings. variantEnd(60); static final nameMap = IMap(GameStatus.values.asNameMap()); diff --git a/lib/src/model/game/playable_game.dart b/lib/src/model/game/playable_game.dart index 1e23968787..e0ae58fc0f 100644 --- a/lib/src/model/game/playable_game.dart +++ b/lib/src/model/game/playable_game.dart @@ -95,7 +95,8 @@ class PlayableGame bool get imported => source == GameSource.import; - bool get isPlayerTurn => lastPosition.turn == youAre; + /// Whether it is the current player's turn. + bool get isMyTurn => lastPosition.turn == youAre; /// Whether the game is properly finished (not aborted). bool get finished => status.value >= GameStatus.mate.value; @@ -125,7 +126,7 @@ class PlayableGame bool get canClaimWin => opponent?.isGone == true && - !isPlayerTurn && + !isMyTurn && resignable && (meta.rules == null || !meta.rules!.contains(GameRule.noClaimWin)); @@ -171,6 +172,23 @@ class PlayableGame } } +@freezed +class PlayableClockData with _$PlayableClockData { + const factory PlayableClockData({ + required bool running, + required Duration white, + required Duration black, + + /// The network lag of the clock. + /// + /// Will be sent along with move events. + required Duration? lag, + + /// The time when the clock event was received. + required DateTime at, + }) = _PlayableClockData; +} + PlayableGame _playableGameFromPick(RequiredPick pick) { final requiredGamePick = pick('game').required(); final meta = _playableGameMetaFromPick(pick); @@ -296,6 +314,10 @@ PlayableClockData _playableClockDataFromPick(RequiredPick pick) { running: pick('running').asBoolOrThrow(), white: pick('white').asDurationFromSecondsOrThrow(), black: pick('black').asDurationFromSecondsOrThrow(), + lag: pick('lag').letOrNull( + (it) => Duration(milliseconds: it.asIntOrThrow() * 10), + ), + at: DateTime.now(), ); } diff --git a/lib/src/model/settings/board_preferences.dart b/lib/src/model/settings/board_preferences.dart index e07555b201..cfc8d8edcc 100644 --- a/lib/src/model/settings/board_preferences.dart +++ b/lib/src/model/settings/board_preferences.dart @@ -82,11 +82,18 @@ class BoardPreferences extends _$BoardPreferences ); } + Future setDragTargetKind(DragTargetKind dragTargetKind) { + return save(state.copyWith(dragTargetKind: dragTargetKind)); + } + Future setMaterialDifferenceFormat( - MaterialDifferenceFormat materialDifferenceFormat, - ) { + MaterialDifferenceFormat materialDifferenceFormat) { + return save(state.copyWith(materialDifferenceFormat: materialDifferenceFormat)); + } + + Future setClockPosition(ClockPosition clockPosition) { return save( - state.copyWith(materialDifferenceFormat: materialDifferenceFormat), + state.copyWith(clockPosition: clockPosition), ); } @@ -115,6 +122,8 @@ class BoardPrefs with _$BoardPrefs implements Serializable { required bool coordinates, required bool pieceAnimation, required MaterialDifferenceFormat materialDifferenceFormat, + required ClockPosition clockPosition, + @JsonKey( defaultValue: PieceShiftMethod.either, unknownEnumValue: PieceShiftMethod.either, @@ -124,6 +133,8 @@ class BoardPrefs with _$BoardPrefs implements Serializable { /// Whether to enable shape drawings on the board for games and puzzles. @JsonKey(defaultValue: true) required bool enableShapeDrawings, @JsonKey(defaultValue: true) required bool magnifyDraggedPiece, + @JsonKey(defaultValue: DragTargetKind.circle) + required DragTargetKind dragTargetKind, @JsonKey( defaultValue: ShapeColor.green, unknownEnumValue: ShapeColor.green, @@ -142,9 +153,11 @@ class BoardPrefs with _$BoardPrefs implements Serializable { coordinates: true, pieceAnimation: true, materialDifferenceFormat: MaterialDifferenceFormat.materialDifference, + clockPosition: ClockPosition.right, pieceShiftMethod: PieceShiftMethod.either, enableShapeDrawings: true, magnifyDraggedPiece: true, + dragTargetKind: DragTargetKind.circle, shapeColor: ShapeColor.green, showBorder: false, ); @@ -165,6 +178,7 @@ class BoardPrefs with _$BoardPrefs implements Serializable { animationDuration: pieceAnimationDuration, dragFeedbackScale: magnifyDraggedPiece ? 2.0 : 1.0, dragFeedbackOffset: Offset(0.0, magnifyDraggedPiece ? -1.0 : 0.0), + dragTargetKind: dragTargetKind, pieceShiftMethod: pieceShiftMethod, drawShape: DrawShapeOptions( enable: enableShapeDrawings, @@ -325,3 +339,21 @@ enum MaterialDifferenceFormat { MaterialDifferenceFormat.hidden => hidden.label, }; } + +enum ClockPosition { + left, + right; + + // TODO: l10n + String get label => switch (this) { + ClockPosition.left => 'Left', + ClockPosition.right => 'Right', + }; +} + +String dragTargetKindLabel(DragTargetKind kind) => switch (kind) { + DragTargetKind.circle => 'Circle', + DragTargetKind.square => 'Square', + DragTargetKind.none => 'None', + } +; diff --git a/lib/src/model/study/study_controller.dart b/lib/src/model/study/study_controller.dart index 320faf2954..1337dff19c 100644 --- a/lib/src/model/study/study_controller.dart +++ b/lib/src/model/study/study_controller.dart @@ -33,11 +33,14 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { Timer? _startEngineEvalTimer; + Timer? _opponentFirstMoveTimer; + @override Future build(StudyId id) async { final evaluationService = ref.watch(evaluationServiceProvider); ref.onDispose(() { _startEngineEvalTimer?.cancel(); + _opponentFirstMoveTimer?.cancel(); _engineEvalDebounce.dispose(); evaluationService.disposeEngine(); }); @@ -62,6 +65,7 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { chapterId: chapterId, ), ); + _ensureItsOurTurnIfGamebook(); } Future _fetchChapter( @@ -95,6 +99,7 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { pov: orientation, isLocalEvaluationAllowed: false, isLocalEvaluationEnabled: false, + gamebookActive: false, pgn: pgn, ); } @@ -119,6 +124,7 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { isLocalEvaluationAllowed: study.chapter.features.computer && !study.chapter.gamebook, isLocalEvaluationEnabled: prefs.enableLocalEvaluation, + gamebookActive: study.chapter.gamebook, pgn: pgn, ); @@ -144,6 +150,19 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { return studyState; } + // The PGNs of some gamebook studies start with the opponent's turn, so trigger their move after a delay + void _ensureItsOurTurnIfGamebook() { + _opponentFirstMoveTimer?.cancel(); + if (state.requireValue.isAtStartOfChapter && + state.requireValue.gamebookActive && + state.requireValue.gamebookComment == null && + state.requireValue.position!.turn != state.requireValue.pov) { + _opponentFirstMoveTimer = Timer(const Duration(milliseconds: 750), () { + userNext(); + }); + } + } + EvaluationContext _evaluationContext(Variant variant) => EvaluationContext( variant: variant, initialPosition: _root.position, @@ -168,6 +187,20 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { shouldForceShowVariation: true, ); } + + if (state.requireValue.gamebookActive) { + final comment = state.requireValue.gamebookComment; + // If there's no explicit comment why the move was good/bad, trigger next/previous move automatically + if (comment == null) { + Timer(const Duration(milliseconds: 750), () { + if (state.requireValue.isOnMainline) { + userNext(); + } else { + userPrevious(); + } + }); + } + } } void onPromotionSelection(Role? role) { @@ -237,6 +270,7 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { void reset() { if (state.hasValue) { _setPath(UciPath.empty); + _ensureItsOurTurnIfGamebook(); } } @@ -486,6 +520,14 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { } } +enum GamebookState { + startLesson, + findTheMove, + correctMove, + incorrectMove, + lessonComplete +} + @freezed class StudyState with _$StudyState { const StudyState._(); @@ -519,6 +561,9 @@ class StudyState with _$StudyState { /// Whether local evaluation is allowed for this study. required bool isLocalEvaluationAllowed, + /// Whether we're currently in gamebook mode, where the user has to find the right moves. + required bool gamebookActive, + /// Whether the user has enabled local evaluation. required bool isLocalEvaluationEnabled, @@ -567,6 +612,37 @@ class StudyState with _$StudyState { bool get isAtStartOfChapter => currentPath.isEmpty; + String? get gamebookComment { + final comment = + (currentNode.isRoot ? pgnRootComments : currentNode.comments) + ?.map((comment) => comment.text) + .nonNulls + .join('\n'); + return comment?.isNotEmpty == true + ? comment + : gamebookState == GamebookState.incorrectMove + ? gamebookDeviationComment + : null; + } + + String? get gamebookHint => study.hints.getOrNull(currentPath.size); + + String? get gamebookDeviationComment => + study.deviationComments.getOrNull(currentPath.size); + + GamebookState get gamebookState { + if (isAtEndOfChapter) return GamebookState.lessonComplete; + + final bool myTurn = currentNode.position!.turn == pov; + if (isAtStartOfChapter && !myTurn) return GamebookState.startLesson; + + return myTurn + ? GamebookState.findTheMove + : isOnMainline + ? GamebookState.correctMove + : GamebookState.incorrectMove; + } + bool get isIntroductoryChapter => currentNode.isRoot && currentNode.children.isEmpty; @@ -576,7 +652,9 @@ class StudyState with _$StudyState { .flattened, ); - PlayerSide get playerSide => PlayerSide.both; + PlayerSide get playerSide => gamebookActive + ? (pov == Side.white ? PlayerSide.white : PlayerSide.black) + : PlayerSide.both; } @freezed diff --git a/lib/src/model/tv/tv_controller.dart b/lib/src/model/tv/tv_controller.dart index f53e002667..78b6eeacc4 100644 --- a/lib/src/model/tv/tv_controller.dart +++ b/lib/src/model/tv/tv_controller.dart @@ -212,6 +212,8 @@ class TvController extends _$TvController { newState = newState.copyWith.game.clock!( white: data.clock!.white, black: data.clock!.black, + lag: data.clock!.lag, + at: data.clock!.at, ); } if (!curState.isReplaying) { @@ -228,6 +230,25 @@ class TvController extends _$TvController { state = AsyncData(newState); + case 'endData': + final endData = + GameEndEvent.fromJson(event.data as Map); + TvState newState = state.requireValue.copyWith( + game: state.requireValue.game.copyWith( + status: endData.status, + winner: endData.winner, + ), + ); + if (endData.clock != null) { + newState = newState.copyWith.game.clock!( + white: endData.clock!.white, + black: endData.clock!.black, + at: DateTime.now(), + lag: null, + ); + } + state = AsyncData(newState); + case 'tvSelect': final json = event.data as Map; final eventChannel = pick(json, 'channel').asTvChannelOrNull(); diff --git a/lib/src/network/socket.dart b/lib/src/network/socket.dart index 1997cf23e2..fdb9d7eed0 100644 --- a/lib/src/network/socket.dart +++ b/lib/src/network/socket.dart @@ -563,9 +563,10 @@ class SocketPool { return client; } - /// Disposes the pool and all its clients. + /// Disposes the pool and all its clients and resources. void dispose() { _averageLag.dispose(); + _disposeTimers.forEach((_, t) => t?.cancel()); _pool.forEach((_, c) => c._dispose()); } } diff --git a/lib/src/view/clock/clock_settings.dart b/lib/src/view/clock/clock_settings.dart index a5371b4f91..decea94991 100644 --- a/lib/src/view/clock/clock_settings.dart +++ b/lib/src/view/clock/clock_settings.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lichess_mobile/src/model/clock/clock_controller.dart'; +import 'package:lichess_mobile/src/model/clock/clock_tool_controller.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/utils/l10n_context.dart'; @@ -16,7 +16,7 @@ class ClockSettings extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final state = ref.watch(clockControllerProvider); + final state = ref.watch(clockToolControllerProvider); final buttonsEnabled = !state.started || state.paused; final isSoundEnabled = ref.watch( @@ -36,7 +36,7 @@ class ClockSettings extends ConsumerWidget { iconSize: _iconSize, onPressed: buttonsEnabled ? () { - ref.read(clockControllerProvider.notifier).reset(); + ref.read(clockToolControllerProvider.notifier).reset(); } : null, icon: const Icon(Icons.refresh), @@ -57,18 +57,18 @@ class ClockSettings extends ConsumerWidget { ), builder: (BuildContext context) { final options = ref.watch( - clockControllerProvider + clockToolControllerProvider .select((value) => value.options), ); return TimeControlModal( excludeUltraBullet: true, value: TimeIncrement( - options.timePlayerTop.inSeconds, - options.incrementPlayerTop.inSeconds, + options.whiteTime.inSeconds, + options.whiteIncrement.inSeconds, ), onSelected: (choice) { ref - .read(clockControllerProvider.notifier) + .read(clockToolControllerProvider.notifier) .updateOptions(choice); }, ); @@ -107,8 +107,8 @@ class _PlayResumeButton extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final controller = ref.read(clockControllerProvider.notifier); - final state = ref.watch(clockControllerProvider); + final controller = ref.read(clockToolControllerProvider.notifier); + final state = ref.watch(clockToolControllerProvider); if (!state.started) { return IconButton( diff --git a/lib/src/view/clock/clock_screen.dart b/lib/src/view/clock/clock_tool_screen.dart similarity index 73% rename from lib/src/view/clock/clock_screen.dart rename to lib/src/view/clock/clock_tool_screen.dart index f01b4d4880..b9962a0802 100644 --- a/lib/src/view/clock/clock_screen.dart +++ b/lib/src/view/clock/clock_tool_screen.dart @@ -1,6 +1,7 @@ +import 'package:dartchess/dartchess.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lichess_mobile/src/model/clock/clock_controller.dart'; +import 'package:lichess_mobile/src/model/clock/clock_tool_controller.dart'; import 'package:lichess_mobile/src/model/common/time_increment.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/immersive_mode.dart'; @@ -8,12 +9,12 @@ import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/view/clock/clock_settings.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; -import 'package:lichess_mobile/src/widgets/countdown_clock.dart'; +import 'package:lichess_mobile/src/widgets/clock.dart'; import 'custom_clock_settings.dart'; -class ClockScreen extends StatelessWidget { - const ClockScreen({super.key}); +class ClockToolScreen extends StatelessWidget { + const ClockToolScreen({super.key}); @override Widget build(BuildContext context) { @@ -23,12 +24,14 @@ class ClockScreen extends StatelessWidget { } } +enum TilePosition { bottom, top } + class _Body extends ConsumerWidget { const _Body(); @override Widget build(BuildContext context, WidgetRef ref) { - final state = ref.watch(clockControllerProvider); + final state = ref.watch(clockToolControllerProvider); return OrientationBuilder( builder: (context, orientation) { @@ -37,16 +40,18 @@ class _Body extends ConsumerWidget { children: [ Expanded( child: ClockTile( + position: TilePosition.top, orientation: orientation, - playerType: ClockPlayerType.top, + playerType: state.bottomPlayer.opposite, clockState: state, ), ), ClockSettings(orientation: orientation), Expanded( child: ClockTile( + position: TilePosition.bottom, orientation: orientation, - playerType: ClockPlayerType.bottom, + playerType: state.bottomPlayer, clockState: state, ), ), @@ -59,20 +64,22 @@ class _Body extends ConsumerWidget { class ClockTile extends ConsumerWidget { const ClockTile({ + required this.position, required this.playerType, required this.clockState, required this.orientation, super.key, }); - final ClockPlayerType playerType; + final TilePosition position; + final Side playerType; final ClockState clockState; final Orientation orientation; @override Widget build(BuildContext context, WidgetRef ref) { final colorScheme = Theme.of(context).colorScheme; - final backgroundColor = clockState.isLoser(playerType) + final backgroundColor = clockState.isFlagged(playerType) ? context.lichessColors.error : !clockState.paused && clockState.isPlayersTurn(playerType) ? colorScheme.primary @@ -92,10 +99,10 @@ class ClockTile extends ConsumerWidget { ); return RotatedBox( - quarterTurns: orientation == Orientation.portrait && - playerType == ClockPlayerType.top - ? 2 - : 0, + quarterTurns: + orientation == Orientation.portrait && position == TilePosition.top + ? 2 + : 0, child: Stack( alignment: Alignment.center, fit: StackFit.expand, @@ -107,15 +114,19 @@ class ClockTile extends ConsumerWidget { onTap: !clockState.started ? () { ref - .read(clockControllerProvider.notifier) - .setActiveSide(playerType); + .read(clockToolControllerProvider.notifier) + .setBottomPlayer( + position == TilePosition.bottom + ? Side.white + : Side.black, + ); } : null, onTapDown: clockState.started && clockState.isPlayersMoveAllowed(playerType) ? (_) { ref - .read(clockControllerProvider.notifier) + .read(clockToolControllerProvider.notifier) .onTap(playerType); } : null, @@ -129,25 +140,19 @@ class ClockTile extends ConsumerWidget { FittedBox( child: AnimatedCrossFade( duration: const Duration(milliseconds: 300), - firstChild: CountdownClock( - key: Key('${clockState.id}-$playerType'), - padLeft: true, - clockStyle: clockStyle, - duration: clockState.getDuration(playerType), - active: clockState.isActivePlayer(playerType), - onFlag: () { - ref - .read(clockControllerProvider.notifier) - .setLoser(playerType); - }, - onStop: (remaining) { - ref - .read(clockControllerProvider.notifier) - .updateDuration(playerType, remaining); + firstChild: ValueListenableBuilder( + valueListenable: clockState.getDuration(playerType), + builder: (context, value, _) { + return Clock( + padLeft: true, + clockStyle: clockStyle, + timeLeft: value, + active: clockState.isActivePlayer(playerType), + ); }, ), secondChild: const Icon(Icons.flag), - crossFadeState: clockState.isLoser(playerType) + crossFadeState: clockState.isFlagged(playerType) ? CrossFadeState.showSecond : CrossFadeState.showFirst, ), @@ -188,22 +193,22 @@ class ClockTile extends ConsumerWidget { builder: (BuildContext context) => CustomClockSettings( player: playerType, - clock: playerType == ClockPlayerType.top + clock: playerType == Side.white ? TimeIncrement.fromDurations( - clockState.options.timePlayerTop, - clockState.options.incrementPlayerTop, + clockState.options.whiteTime, + clockState.options.whiteIncrement, ) : TimeIncrement.fromDurations( - clockState.options.timePlayerBottom, - clockState.options.incrementPlayerBottom, + clockState.options.blackTime, + clockState.options.blackIncrement, ), onSubmit: ( - ClockPlayerType player, + Side player, TimeIncrement clock, ) { Navigator.of(context).pop(); ref - .read(clockControllerProvider.notifier) + .read(clockToolControllerProvider.notifier) .updateOptionsCustom(clock, player); }, ), diff --git a/lib/src/view/clock/custom_clock_settings.dart b/lib/src/view/clock/custom_clock_settings.dart index ee2bdc6d2d..3ac2172a1a 100644 --- a/lib/src/view/clock/custom_clock_settings.dart +++ b/lib/src/view/clock/custom_clock_settings.dart @@ -1,5 +1,5 @@ +import 'package:dartchess/dartchess.dart'; import 'package:flutter/material.dart'; -import 'package:lichess_mobile/src/model/clock/clock_controller.dart'; import 'package:lichess_mobile/src/model/common/time_increment.dart'; import 'package:lichess_mobile/src/model/lobby/game_setup_preferences.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; @@ -16,9 +16,9 @@ class CustomClockSettings extends StatefulWidget { required this.clock, }); - final ClockPlayerType player; + final Side player; final TimeIncrement clock; - final void Function(ClockPlayerType player, TimeIncrement clock) onSubmit; + final void Function(Side player, TimeIncrement clock) onSubmit; @override State createState() => _CustomClockSettingsState(); diff --git a/lib/src/view/correspondence/offline_correspondence_game_screen.dart b/lib/src/view/correspondence/offline_correspondence_game_screen.dart index 8f7c6b6b08..d89d631db7 100644 --- a/lib/src/view/correspondence/offline_correspondence_game_screen.dart +++ b/lib/src/view/correspondence/offline_correspondence_game_screen.dart @@ -282,7 +282,7 @@ class _BodyState extends ConsumerState<_Body> { data: (games) { final nextTurn = games .whereNot((g) => g.$2.id == game.id) - .firstWhereOrNull((g) => g.$2.isPlayerTurn); + .firstWhereOrNull((g) => g.$2.isMyTurn); return nextTurn != null ? () { widget.onGameChanged(nextTurn); diff --git a/lib/src/view/game/archived_game_screen.dart b/lib/src/view/game/archived_game_screen.dart index a4666b2863..ab7c41cec3 100644 --- a/lib/src/view/game/archived_game_screen.dart +++ b/lib/src/view/game/archived_game_screen.dart @@ -21,7 +21,7 @@ import 'package:lichess_mobile/src/widgets/board_table.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; -import 'package:lichess_mobile/src/widgets/countdown_clock.dart'; +import 'package:lichess_mobile/src/widgets/clock.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'archived_game_screen_providers.dart'; @@ -258,23 +258,13 @@ class _BoardBody extends ConsumerWidget { final black = GamePlayer( key: const ValueKey('black-player'), player: gameData.black, - clock: blackClock != null - ? CountdownClock( - duration: blackClock, - active: false, - ) - : null, + clock: blackClock != null ? Clock(timeLeft: blackClock) : null, materialDiff: game.materialDiffAt(cursor, Side.black), ); final white = GamePlayer( key: const ValueKey('white-player'), player: gameData.white, - clock: whiteClock != null - ? CountdownClock( - duration: whiteClock, - active: false, - ) - : null, + clock: whiteClock != null ? Clock(timeLeft: whiteClock) : null, materialDiff: game.materialDiffAt(cursor, Side.white), ); diff --git a/lib/src/view/game/correspondence_clock_widget.dart b/lib/src/view/game/correspondence_clock_widget.dart index 418162624f..73f4e2a076 100644 --- a/lib/src/view/game/correspondence_clock_widget.dart +++ b/lib/src/view/game/correspondence_clock_widget.dart @@ -6,7 +6,7 @@ import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/settings/brightness.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; -import 'package:lichess_mobile/src/widgets/countdown_clock.dart'; +import 'package:lichess_mobile/src/widgets/clock.dart'; class CorrespondenceClock extends ConsumerStatefulWidget { /// The duration left on the clock. diff --git a/lib/src/view/game/game_body.dart b/lib/src/view/game/game_body.dart index eea37bfced..0dcb9e9d29 100644 --- a/lib/src/view/game/game_body.dart +++ b/lib/src/view/game/game_body.dart @@ -6,7 +6,6 @@ import 'package:dartchess/dartchess.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lichess_mobile/src/model/account/account_preferences.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/speed.dart'; @@ -27,7 +26,7 @@ import 'package:lichess_mobile/src/widgets/board_table.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; -import 'package:lichess_mobile/src/widgets/countdown_clock.dart'; +import 'package:lichess_mobile/src/widgets/clock.dart'; import 'package:lichess_mobile/src/widgets/platform_alert_dialog.dart'; import 'package:lichess_mobile/src/widgets/user_full_name.dart'; import 'package:lichess_mobile/src/widgets/yes_no_dialog.dart'; @@ -104,11 +103,6 @@ class GameBody extends ConsumerWidget { ); final boardPreferences = ref.watch(boardPreferencesProvider); - final emergencySoundEnabled = ref.watch(clockSoundProvider).maybeWhen( - data: (clockSound) => clockSound, - orElse: () => true, - ); - final blindfoldMode = ref.watch( gamePreferencesProvider.select( (prefs) => prefs.blindfoldMode, @@ -137,6 +131,7 @@ class GameBody extends ConsumerWidget { : null, mePlaying: youAre == Side.black, zenMode: gameState.isZenModeActive, + clockPosition: boardPreferences.clockPosition, confirmMoveCallbacks: youAre == Side.black && gameState.moveToConfirm != null ? ( @@ -148,24 +143,35 @@ class GameBody extends ConsumerWidget { }, ) : null, - clock: gameState.game.meta.clock != null - ? CountdownClock( - key: blackClockKey, - duration: archivedBlackClock ?? gameState.game.clock!.black, - active: gameState.activeClockSide == Side.black, - emergencyThreshold: youAre == Side.black - ? gameState.game.meta.clock?.emergency - : null, - emergencySoundEnabled: emergencySoundEnabled, - onFlag: () => ref.read(ctrlProvider.notifier).onFlag(), + clock: archivedBlackClock != null + ? Clock( + timeLeft: archivedBlackClock, + active: false, ) - : gameState.game.correspondenceClock != null - ? CorrespondenceClock( - duration: gameState.game.correspondenceClock!.black, - active: gameState.activeClockSide == Side.black, - onFlag: () => ref.read(ctrlProvider.notifier).onFlag(), + : gameState.liveClock != null + ? RepaintBoundary( + child: ValueListenableBuilder( + key: blackClockKey, + valueListenable: gameState.liveClock!.black, + builder: (context, value, _) { + return Clock( + timeLeft: value, + active: gameState.activeClockSide == Side.black, + emergencyThreshold: youAre == Side.black + ? gameState.game.meta.clock?.emergency + : null, + ); + }, + ), ) - : null, + : gameState.game.correspondenceClock != null + ? CorrespondenceClock( + duration: gameState.game.correspondenceClock!.black, + active: gameState.activeClockSide == Side.black, + onFlag: () => + ref.read(ctrlProvider.notifier).onFlag(), + ) + : null, ); final white = GamePlayer( player: gameState.game.white, @@ -178,6 +184,7 @@ class GameBody extends ConsumerWidget { : null, mePlaying: youAre == Side.white, zenMode: gameState.isZenModeActive, + clockPosition: boardPreferences.clockPosition, confirmMoveCallbacks: youAre == Side.white && gameState.moveToConfirm != null ? ( @@ -189,24 +196,35 @@ class GameBody extends ConsumerWidget { }, ) : null, - clock: gameState.game.meta.clock != null - ? CountdownClock( - key: whiteClockKey, - duration: archivedWhiteClock ?? gameState.game.clock!.white, - active: gameState.activeClockSide == Side.white, - emergencyThreshold: youAre == Side.white - ? gameState.game.meta.clock?.emergency - : null, - emergencySoundEnabled: emergencySoundEnabled, - onFlag: () => ref.read(ctrlProvider.notifier).onFlag(), + clock: archivedWhiteClock != null + ? Clock( + timeLeft: archivedWhiteClock, + active: false, ) - : gameState.game.correspondenceClock != null - ? CorrespondenceClock( - duration: gameState.game.correspondenceClock!.white, - active: gameState.activeClockSide == Side.white, - onFlag: () => ref.read(ctrlProvider.notifier).onFlag(), + : gameState.liveClock != null + ? RepaintBoundary( + child: ValueListenableBuilder( + key: whiteClockKey, + valueListenable: gameState.liveClock!.white, + builder: (context, value, _) { + return Clock( + timeLeft: value, + active: gameState.activeClockSide == Side.white, + emergencyThreshold: youAre == Side.white + ? gameState.game.meta.clock?.emergency + : null, + ); + }, + ), ) - : null, + : gameState.game.correspondenceClock != null + ? CorrespondenceClock( + duration: gameState.game.correspondenceClock!.white, + active: gameState.activeClockSide == Side.white, + onFlag: () => + ref.read(ctrlProvider.notifier).onFlag(), + ) + : null, ); final isBoardTurned = ref.watch(isBoardTurnedProvider); diff --git a/lib/src/view/game/game_common_widgets.dart b/lib/src/view/game/game_common_widgets.dart index 8d3718cf57..94199f99cf 100644 --- a/lib/src/view/game/game_common_widgets.dart +++ b/lib/src/view/game/game_common_widgets.dart @@ -12,6 +12,7 @@ import 'package:lichess_mobile/src/model/lobby/game_seek.dart'; import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/share.dart'; +import 'package:lichess_mobile/src/view/settings/toggle_sound_button.dart'; import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; @@ -64,6 +65,7 @@ class GameAppBar extends ConsumerWidget { ? _ChallengeGameTitle(challenge: challenge!) : const SizedBox.shrink(), actions: [ + const ToggleSoundButton(), if (id != null) AppBarIconButton( onPressed: () => showAdaptiveBottomSheet( @@ -71,6 +73,9 @@ class GameAppBar extends ConsumerWidget { isDismissible: true, isScrollControlled: true, showDragHandle: true, + constraints: BoxConstraints( + minHeight: MediaQuery.sizeOf(context).height * 0.5, + ), builder: (_) => GameSettings(id: id!), ), semanticsLabel: context.l10n.settingsSettings, diff --git a/lib/src/view/game/game_player.dart b/lib/src/view/game/game_player.dart index 394ebcd672..9691cfae76 100644 --- a/lib/src/view/game/game_player.dart +++ b/lib/src/view/game/game_player.dart @@ -5,7 +5,9 @@ import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/constants.dart'; +import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; import 'package:lichess_mobile/src/model/game/material_diff.dart'; import 'package:lichess_mobile/src/model/game/player.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; @@ -33,6 +35,7 @@ class GamePlayer extends StatelessWidget { this.shouldLinkToUserProfile = true, this.mePlaying = false, this.zenMode = false, + this.clockPosition = ClockPosition.right, super.key, }); @@ -47,6 +50,7 @@ class GamePlayer extends StatelessWidget { final bool shouldLinkToUserProfile; final bool mePlaying; final bool zenMode; + final ClockPosition clockPosition; /// Time left for the player to move at the start of the game. final Duration? timeToMove; @@ -63,7 +67,9 @@ class GamePlayer extends StatelessWidget { children: [ if (!zenMode) Row( - mainAxisAlignment: MainAxisAlignment.start, + mainAxisAlignment: clockPosition == ClockPosition.right + ? MainAxisAlignment.start + : MainAxisAlignment.end, children: [ if (player.user != null) ...[ Icon( @@ -147,9 +153,31 @@ class GamePlayer extends StatelessWidget { else if (materialDiff != null) MaterialDifferenceDisplay( materialDiff: materialDiff!, - materialDifferenceFormat: materialDifferenceFormat!, - ) - else + materialDifferenceFormat: materialDifferenceFormat!,), + Row( + mainAxisAlignment: clockPosition == ClockPosition.right + ? MainAxisAlignment.start + : MainAxisAlignment.end, + children: [ + for (final role in Role.values) + for (int i = 0; i < materialDiff!.pieces[role]!; i++) + Icon( + _iconByRole[role], + size: 13, + color: Colors.grey, + ), + const SizedBox(width: 3), + Text( + style: const TextStyle( + fontSize: 13, + color: Colors.grey, + ), + materialDiff != null && materialDiff!.score > 0 + ? '+${materialDiff!.score}' + : '', + ), + ], + ), // to avoid shifts use an empty text widget const Text('', style: TextStyle(fontSize: 13)), ], @@ -159,6 +187,8 @@ class GamePlayer extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ + if (clock != null && clockPosition == ClockPosition.left) + Flexible(flex: 3, child: clock!), if (mePlaying && confirmMoveCallbacks != null) Expanded( flex: 7, @@ -194,7 +224,8 @@ class GamePlayer extends StatelessWidget { : playerWidget, ), ), - if (clock != null) Flexible(flex: 3, child: clock!), + if (clock != null && clockPosition == ClockPosition.right) + Flexible(flex: 3, child: clock!), ], ); } @@ -244,7 +275,7 @@ class ConfirmMove extends StatelessWidget { } } -class MoveExpiration extends StatefulWidget { +class MoveExpiration extends ConsumerStatefulWidget { const MoveExpiration({ required this.timeToMove, required this.mePlaying, @@ -255,13 +286,14 @@ class MoveExpiration extends StatefulWidget { final bool mePlaying; @override - State createState() => _MoveExpirationState(); + ConsumerState createState() => _MoveExpirationState(); } -class _MoveExpirationState extends State { +class _MoveExpirationState extends ConsumerState { static const _period = Duration(milliseconds: 1000); Timer? _timer; Duration timeLeft = Duration.zero; + bool playedEmergencySound = false; Timer startTimer() { return Timer.periodic(_period, (timer) { @@ -299,6 +331,14 @@ class _MoveExpirationState extends State { Widget build(BuildContext context) { final secs = timeLeft.inSeconds.remainder(60); final emerg = timeLeft <= const Duration(seconds: 8); + + if (emerg && widget.mePlaying && !playedEmergencySound) { + ref.read(soundServiceProvider).play(Sound.lowTime); + setState(() { + playedEmergencySound = true; + }); + } + return secs <= 20 ? Text( context.l10n.nbSecondsToPlayTheFirstMove(secs), diff --git a/lib/src/view/game/game_settings.dart b/lib/src/view/game/game_settings.dart index 25e9c2b1f7..42d6bf0290 100644 --- a/lib/src/view/game/game_settings.dart +++ b/lib/src/view/game/game_settings.dart @@ -1,5 +1,4 @@ import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/l10n/l10n.dart'; import 'package:lichess_mobile/src/model/account/account_preferences.dart'; @@ -7,9 +6,11 @@ import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/game/game_controller.dart'; import 'package:lichess_mobile/src/model/game/game_preferences.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; -import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/view/settings/board_settings_screen.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; +import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/settings.dart'; import '../../widgets/adaptive_choice_picker.dart'; @@ -22,31 +23,12 @@ class GameSettings extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final isSoundEnabled = ref.watch( - generalPreferencesProvider.select( - (prefs) => prefs.isSoundEnabled, - ), - ); - final boardPrefs = ref.watch(boardPreferencesProvider); final gamePrefs = ref.watch(gamePreferencesProvider); final userPrefsAsync = ref.watch(userGamePrefsProvider(id)); + final boardPrefs = ref.watch(boardPreferencesProvider); return BottomSheetScrollableContainer( children: [ - SwitchSettingTile( - title: Text(context.l10n.sound), - value: isSoundEnabled, - onChanged: (value) { - ref.read(generalPreferencesProvider.notifier).toggleSoundEnabled(); - }, - ), - SwitchSettingTile( - title: Text(context.l10n.mobileSettingsHapticFeedback), - value: boardPrefs.hapticFeedback, - onChanged: (value) { - ref.read(boardPreferencesProvider.notifier).toggleHapticFeedback(); - }, - ), ...userPrefsAsync.maybeWhen( data: (data) { return [ @@ -111,6 +93,18 @@ class GameSettings extends ConsumerWidget { ref.read(boardPreferencesProvider.notifier).togglePieceAnimation(); }, ), + PlatformListTile( + // TODO translate + title: const Text('Board settings'), + trailing: const Icon(CupertinoIcons.chevron_right), + onTap: () { + pushPlatformRoute( + context, + fullscreenDialog: true, + screen: const BoardSettingsScreen(), + ); + }, + ), SwitchSettingTile( title: Text( context.l10n.toggleTheChat, diff --git a/lib/src/view/game/message_screen.dart b/lib/src/view/game/message_screen.dart index d684f855c0..00187cc040 100644 --- a/lib/src/view/game/message_screen.dart +++ b/lib/src/view/game/message_screen.dart @@ -83,29 +83,34 @@ class _Body extends ConsumerWidget { child: GestureDetector( onTap: () => FocusScope.of(context).unfocus(), child: chatStateAsync.when( - data: (chatState) => ListView.builder( - // remove the automatic bottom padding of the ListView, which on iOS - // corresponds to the safe area insets - // and which is here taken care of by the _ChatBottomBar - padding: MediaQuery.of(context).padding.copyWith(bottom: 0), - reverse: true, - itemCount: chatState.messages.length, - itemBuilder: (context, index) { - final message = - chatState.messages[chatState.messages.length - index - 1]; - return (message.username == 'lichess') - ? _MessageAction(message: message.message) - : (message.username == me?.name) - ? _MessageBubble( - you: true, - message: message.message, - ) - : _MessageBubble( - you: false, - message: message.message, - ); - }, - ), + data: (chatState) { + final selectedMessages = chatState.messages + .where((m) => !m.troll && !m.deleted && !isSpam(m)) + .toList(); + final messagesCount = selectedMessages.length; + return ListView.builder( + // remove the automatic bottom padding of the ListView, which on iOS + // corresponds to the safe area insets + // and which is here taken care of by the _ChatBottomBar + padding: MediaQuery.of(context).padding.copyWith(bottom: 0), + reverse: true, + itemCount: messagesCount, + itemBuilder: (context, index) { + final message = selectedMessages[messagesCount - index - 1]; + return (message.username == 'lichess') + ? _MessageAction(message: message.message) + : (message.username == me?.name) + ? _MessageBubble( + you: true, + message: message.message, + ) + : _MessageBubble( + you: false, + message: message.message, + ); + }, + ); + }, loading: () => const Center( child: CircularProgressIndicator(), ), diff --git a/lib/src/view/over_the_board/over_the_board_screen.dart b/lib/src/view/over_the_board/over_the_board_screen.dart index 5478945df8..7470cac66d 100644 --- a/lib/src/view/over_the_board/over_the_board_screen.dart +++ b/lib/src/view/over_the_board/over_the_board_screen.dart @@ -25,7 +25,7 @@ import 'package:lichess_mobile/src/widgets/board_table.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; -import 'package:lichess_mobile/src/widgets/countdown_clock.dart'; +import 'package:lichess_mobile/src/widgets/clock.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; class OverTheBoardScreen extends StatelessWidget { diff --git a/lib/src/view/puzzle/puzzle_screen.dart b/lib/src/view/puzzle/puzzle_screen.dart index 6f51d20735..ef8681c596 100644 --- a/lib/src/view/puzzle/puzzle_screen.dart +++ b/lib/src/view/puzzle/puzzle_screen.dart @@ -31,6 +31,7 @@ import 'package:lichess_mobile/src/view/account/rating_pref_aware.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; import 'package:lichess_mobile/src/view/game/archived_game_screen.dart'; import 'package:lichess_mobile/src/view/puzzle/puzzle_settings_screen.dart'; +import 'package:lichess_mobile/src/view/settings/toggle_sound_button.dart'; import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/adaptive_choice_picker.dart'; @@ -91,6 +92,7 @@ class _PuzzleScreenState extends ConsumerState with RouteAware { child: PlatformScaffold( appBar: PlatformAppBar( actions: const [ + ToggleSoundButton(), _PuzzleSettingsButton(), ], title: _Title(angle: widget.angle), @@ -604,6 +606,9 @@ class _PuzzleSettingsButton extends StatelessWidget { isDismissible: true, isScrollControlled: true, showDragHandle: true, + constraints: BoxConstraints( + minHeight: MediaQuery.sizeOf(context).height * 0.5, + ), builder: (_) => const PuzzleSettingsScreen(), ), semanticsLabel: context.l10n.settingsSettings, diff --git a/lib/src/view/puzzle/puzzle_settings_screen.dart b/lib/src/view/puzzle/puzzle_settings_screen.dart index 7f59b61d24..33e14c9518 100644 --- a/lib/src/view/puzzle/puzzle_settings_screen.dart +++ b/lib/src/view/puzzle/puzzle_settings_screen.dart @@ -1,11 +1,11 @@ import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_preferences.dart'; -import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; -import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/view/settings/board_settings_screen.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; +import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/settings.dart'; class PuzzleSettingsScreen extends ConsumerWidget { @@ -13,23 +13,11 @@ class PuzzleSettingsScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final isSoundEnabled = ref.watch( - generalPreferencesProvider.select((pref) => pref.isSoundEnabled), - ); final autoNext = ref.watch( puzzlePreferencesProvider.select((value) => value.autoNext), ); - final boardPrefs = ref.watch(boardPreferencesProvider); - return BottomSheetScrollableContainer( children: [ - SwitchSettingTile( - title: Text(context.l10n.sound), - value: isSoundEnabled, - onChanged: (value) { - ref.read(generalPreferencesProvider.notifier).toggleSoundEnabled(); - }, - ), SwitchSettingTile( title: Text(context.l10n.puzzleJumpToNextPuzzleImmediately), value: autoNext, @@ -37,28 +25,15 @@ class PuzzleSettingsScreen extends ConsumerWidget { ref.read(puzzlePreferencesProvider.notifier).setAutoNext(value); }, ), - SwitchSettingTile( - // TODO: Add l10n - title: const Text('Shape drawing'), - subtitle: const Text( - 'Draw shapes using two fingers.', - maxLines: 5, - textAlign: TextAlign.justify, - ), - value: boardPrefs.enableShapeDrawings, - onChanged: (value) { - ref - .read(boardPreferencesProvider.notifier) - .toggleEnableShapeDrawings(); - }, - ), - SwitchSettingTile( - title: Text( - context.l10n.preferencesPieceAnimation, - ), - value: boardPrefs.pieceAnimation, - onChanged: (value) { - ref.read(boardPreferencesProvider.notifier).togglePieceAnimation(); + PlatformListTile( + title: const Text('Board settings'), + trailing: const Icon(CupertinoIcons.chevron_right), + onTap: () { + pushPlatformRoute( + context, + fullscreenDialog: true, + screen: const BoardSettingsScreen(), + ); }, ), ], diff --git a/lib/src/view/settings/board_settings_screen.dart b/lib/src/view/settings/board_settings_screen.dart index e2bc8b5ccd..d126acc24b 100644 --- a/lib/src/view/settings/board_settings_screen.dart +++ b/lib/src/view/settings/board_settings_screen.dart @@ -4,11 +4,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/l10n/l10n.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/utils/system.dart'; -import 'package:lichess_mobile/src/view/settings/piece_shift_method_settings_screen.dart'; import 'package:lichess_mobile/src/widgets/adaptive_choice_picker.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; @@ -53,6 +53,7 @@ class _Body extends ConsumerWidget { child: ListView( children: [ ListSection( + header: SettingsSectionTitle(context.l10n.preferencesGameBehavior), hasLeading: false, showDivider: false, children: [ @@ -88,29 +89,96 @@ class _Body extends ConsumerWidget { }, ), SwitchSettingTile( - // TODO: Add l10n - title: const Text('Shape drawing'), + title: Text(context.l10n.mobilePrefMagnifyDraggedPiece), + value: boardPrefs.magnifyDraggedPiece, + onChanged: (value) { + ref + .read(boardPreferencesProvider.notifier) + .toggleMagnifyDraggedPiece(); + }, + ), + SettingsListTile( + // TODO translate + settingsLabel: const Text('Drag target'), + explanation: + // TODO translate + 'How the target square is highlighted when dragging a piece.', + settingsValue: dragTargetKindLabel(boardPrefs.dragTargetKind), + onTap: () { + if (Theme.of(context).platform == TargetPlatform.android) { + showChoicePicker( + context, + choices: DragTargetKind.values, + selectedItem: boardPrefs.dragTargetKind, + labelBuilder: (t) => Text(dragTargetKindLabel(t)), + onSelectedItemChanged: (DragTargetKind? value) { + ref + .read(boardPreferencesProvider.notifier) + .setDragTargetKind( + value ?? DragTargetKind.circle, + ); + }, + ); + } else { + pushPlatformRoute( + context, + title: 'Dragged piece target', + builder: (context) => + const DragTargetKindSettingsScreen(), + ); + } + }, + ), + SwitchSettingTile( + // TODO translate + title: const Text('Touch feedback'), + value: boardPrefs.hapticFeedback, subtitle: const Text( - 'Draw shapes using two fingers on game and puzzle boards.', + // TODO translate + 'Vibrate when moving pieces or capturing them.', maxLines: 5, textAlign: TextAlign.justify, ), - value: boardPrefs.enableShapeDrawings, onChanged: (value) { ref .read(boardPreferencesProvider.notifier) - .toggleEnableShapeDrawings(); + .toggleHapticFeedback(); }, ), SwitchSettingTile( - title: Text(context.l10n.mobileSettingsHapticFeedback), - value: boardPrefs.hapticFeedback, + title: Text( + context.l10n.preferencesPieceAnimation, + ), + value: boardPrefs.pieceAnimation, onChanged: (value) { ref .read(boardPreferencesProvider.notifier) - .toggleHapticFeedback(); + .togglePieceAnimation(); + }, + ), + SwitchSettingTile( + // TODO: Add l10n + title: const Text('Shape drawing'), + subtitle: const Text( + // TODO: translate + 'Draw shapes using two fingers: maintain one finger on an empty square and drag another finger to draw a shape.', + maxLines: 5, + textAlign: TextAlign.justify, + ), + value: boardPrefs.enableShapeDrawings, + onChanged: (value) { + ref + .read(boardPreferencesProvider.notifier) + .toggleEnableShapeDrawings(); }, ), + ], + ), + ListSection( + header: SettingsSectionTitle(context.l10n.preferencesDisplay), + hasLeading: false, + showDivider: false, + children: [ if (Theme.of(context).platform == TargetPlatform.android && !isTabletOrLarger(context)) androidVersionAsync.maybeWhen( @@ -132,6 +200,30 @@ class _Body extends ConsumerWidget { : const SizedBox.shrink(), orElse: () => const SizedBox.shrink(), ), + SettingsListTile( + //TODO Add l10n + settingsLabel: const Text('Clock position'), + settingsValue: boardPrefs.clockPosition.label, + onTap: () { + if (Theme.of(context).platform == TargetPlatform.android) { + showChoicePicker( + context, + choices: ClockPosition.values, + selectedItem: boardPrefs.clockPosition, + labelBuilder: (t) => Text(t.label), + onSelectedItemChanged: (ClockPosition? value) => ref + .read(boardPreferencesProvider.notifier) + .setClockPosition(value ?? ClockPosition.right), + ); + } else { + pushPlatformRoute( + context, + title: 'Clock position', + builder: (context) => const BoardClockPositionScreen(), + ); + } + }, + ), SwitchSettingTile( title: Text( context.l10n.preferencesPieceDestinations, @@ -213,3 +305,122 @@ class _Body extends ConsumerWidget { ); } } + +class PieceShiftMethodSettingsScreen extends ConsumerWidget { + const PieceShiftMethodSettingsScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final pieceShiftMethod = ref.watch( + boardPreferencesProvider.select( + (state) => state.pieceShiftMethod, + ), + ); + + void onChanged(PieceShiftMethod? value) { + ref + .read(boardPreferencesProvider.notifier) + .setPieceShiftMethod(value ?? PieceShiftMethod.either); + } + + return CupertinoPageScaffold( + navigationBar: const CupertinoNavigationBar(), + child: SafeArea( + child: ListView( + children: [ + ChoicePicker( + notchedTile: true, + choices: PieceShiftMethod.values, + selectedItem: pieceShiftMethod, + titleBuilder: (t) => Text(pieceShiftMethodl10n(context, t)), + onSelectedItemChanged: onChanged, + ), + ], + ), + ), + ); + } +} + +class BoardClockPositionScreen extends ConsumerWidget { + const BoardClockPositionScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final clockPosition = ref.watch( + boardPreferencesProvider.select((state) => state.clockPosition), + ); + void onChanged(ClockPosition? value) => ref + .read(boardPreferencesProvider.notifier) + .setClockPosition(value ?? ClockPosition.right); + return CupertinoPageScaffold( + navigationBar: const CupertinoNavigationBar(), + child: SafeArea( + child: ListView( + children: [ + ChoicePicker( + choices: ClockPosition.values, + selectedItem: clockPosition, + titleBuilder: (t) => Text(t.label), + onSelectedItemChanged: onChanged, + ), + ], + ), + ), + ); + } +} + +class DragTargetKindSettingsScreen extends ConsumerWidget { + const DragTargetKindSettingsScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final dragTargetKind = ref.watch( + boardPreferencesProvider.select( + (state) => state.dragTargetKind, + ), + ); + + void onChanged(DragTargetKind? value) { + ref + .read(boardPreferencesProvider.notifier) + .setDragTargetKind(value ?? DragTargetKind.circle); + } + + return CupertinoPageScaffold( + navigationBar: const CupertinoNavigationBar(), + child: SafeArea( + child: ListView( + children: [ + Padding( + padding: + Styles.horizontalBodyPadding.add(Styles.sectionTopPadding), + child: const Text( + 'How the target square is highlighted when dragging a piece.', + ), + ), + ChoicePicker( + notchedTile: true, + choices: DragTargetKind.values, + selectedItem: dragTargetKind, + titleBuilder: (t) => Text(dragTargetKindLabel(t)), + onSelectedItemChanged: onChanged, + ), + ], + ), + ), + ); + } +} + +String pieceShiftMethodl10n( + BuildContext context, + PieceShiftMethod pieceShiftMethod, +) => + switch (pieceShiftMethod) { + // TODO add this to mobile translations + PieceShiftMethod.either => 'Either tap or drag', + PieceShiftMethod.drag => context.l10n.preferencesDragPiece, + PieceShiftMethod.tapTwoSquares => 'Tap two squares', + }; diff --git a/lib/src/view/settings/piece_shift_method_settings_screen.dart b/lib/src/view/settings/piece_shift_method_settings_screen.dart deleted file mode 100644 index 4edc23ba8d..0000000000 --- a/lib/src/view/settings/piece_shift_method_settings_screen.dart +++ /dev/null @@ -1,79 +0,0 @@ -import 'package:chessground/chessground.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; -import 'package:lichess_mobile/src/utils/l10n_context.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; -import 'package:lichess_mobile/src/widgets/settings.dart'; - -class PieceShiftMethodSettingsScreen extends StatelessWidget { - const PieceShiftMethodSettingsScreen({super.key}); - - @override - Widget build(BuildContext context) { - return PlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, - ); - } - - Widget _androidBuilder(BuildContext context) { - return Scaffold( - appBar: AppBar(title: Text(context.l10n.preferencesHowDoYouMovePieces)), - body: _Body(), - ); - } - - Widget _iosBuilder(BuildContext context) { - return CupertinoPageScaffold( - navigationBar: const CupertinoNavigationBar(), - child: _Body(), - ); - } -} - -String pieceShiftMethodl10n( - BuildContext context, - PieceShiftMethod pieceShiftMethod, -) => - switch (pieceShiftMethod) { - // This is called 'Either' in the Web UI, but in the app we might display this string - // without having the other values as context, so we need to be more explicit. - // TODO add this to mobile translations - PieceShiftMethod.either => 'Either click or drag', - PieceShiftMethod.drag => context.l10n.preferencesDragPiece, - // TODO This string uses 'click', we might want to use 'tap' instead in a mobile-specific translation - PieceShiftMethod.tapTwoSquares => context.l10n.preferencesClickTwoSquares, - }; - -class _Body extends ConsumerWidget { - @override - Widget build(BuildContext context, WidgetRef ref) { - final pieceShiftMethod = ref.watch( - boardPreferencesProvider.select( - (state) => state.pieceShiftMethod, - ), - ); - - void onChanged(PieceShiftMethod? value) { - ref - .read(boardPreferencesProvider.notifier) - .setPieceShiftMethod(value ?? PieceShiftMethod.either); - } - - return SafeArea( - child: ListView( - children: [ - ChoicePicker( - notchedTile: true, - choices: PieceShiftMethod.values, - selectedItem: pieceShiftMethod, - titleBuilder: (t) => Text(pieceShiftMethodl10n(context, t)), - onSelectedItemChanged: onChanged, - ), - ], - ), - ); - } -} diff --git a/lib/src/view/settings/theme_screen.dart b/lib/src/view/settings/theme_screen.dart index e0db758b15..8f34cc8d0d 100644 --- a/lib/src/view/settings/theme_screen.dart +++ b/lib/src/view/settings/theme_screen.dart @@ -80,7 +80,6 @@ class _Body extends ConsumerWidget { ), }.lock, settings: boardPrefs.toBoardSettings().copyWith( - enableCoordinates: true, borderRadius: const BorderRadius.all(Radius.circular(4.0)), boxShadow: boardShadows, @@ -151,6 +150,18 @@ class _Body extends ConsumerWidget { ); }, ), + SwitchSettingTile( + leading: const Icon(Icons.location_on), + title: Text( + context.l10n.preferencesBoardCoordinates, + ), + value: boardPrefs.coordinates, + onChanged: (value) { + ref + .read(boardPreferencesProvider.notifier) + .toggleCoordinates(); + }, + ), SwitchSettingTile( // TODO translate leading: const Icon(Icons.border_outer), diff --git a/lib/src/view/study/study_bottom_bar.dart b/lib/src/view/study/study_bottom_bar.dart index 815e54a97d..ade01ed711 100644 --- a/lib/src/view/study/study_bottom_bar.dart +++ b/lib/src/view/study/study_bottom_bar.dart @@ -1,9 +1,12 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/study/study_controller.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; @@ -15,6 +18,25 @@ class StudyBottomBar extends ConsumerWidget { final StudyId id; + @override + Widget build(BuildContext context, WidgetRef ref) { + final gamebook = ref.watch( + studyControllerProvider(id).select( + (s) => s.requireValue.gamebookActive, + ), + ); + + return gamebook ? _GamebookBottomBar(id: id) : _AnalysisBottomBar(id: id); + } +} + +class _AnalysisBottomBar extends ConsumerWidget { + const _AnalysisBottomBar({ + required this.id, + }); + + final StudyId id; + @override Widget build(BuildContext context, WidgetRef ref) { final state = ref.watch(studyControllerProvider(id)).valueOrNull; @@ -68,3 +90,97 @@ class StudyBottomBar extends ConsumerWidget { ); } } + +class _GamebookBottomBar extends ConsumerWidget { + const _GamebookBottomBar({ + required this.id, + }); + + final StudyId id; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final state = ref.watch(studyControllerProvider(id)).requireValue; + + return BottomBar( + children: [ + ...switch (state.gamebookState) { + GamebookState.findTheMove => [ + if (!state.currentNode.isRoot) + BottomBarButton( + onTap: ref.read(studyControllerProvider(id).notifier).reset, + icon: Icons.skip_previous, + label: 'Back', + showLabel: true, + ), + BottomBarButton( + icon: Icons.help, + label: context.l10n.viewTheSolution, + showLabel: true, + onTap: ref + .read(studyControllerProvider(id).notifier) + .showGamebookSolution, + ), + ], + GamebookState.startLesson || GamebookState.correctMove => [ + BottomBarButton( + onTap: ref.read(studyControllerProvider(id).notifier).userNext, + icon: Icons.play_arrow, + label: context.l10n.studyNext, + showLabel: true, + blink: state.gamebookComment != null && + !state.isIntroductoryChapter, + ), + ], + GamebookState.incorrectMove => [ + BottomBarButton( + onTap: + ref.read(studyControllerProvider(id).notifier).userPrevious, + label: context.l10n.retry, + showLabel: true, + icon: Icons.refresh, + blink: state.gamebookComment != null, + ), + ], + GamebookState.lessonComplete => [ + if (!state.isIntroductoryChapter) + BottomBarButton( + onTap: ref.read(studyControllerProvider(id).notifier).reset, + icon: Icons.refresh, + label: context.l10n.studyPlayAgain, + showLabel: true, + ), + BottomBarButton( + onTap: state.hasNextChapter + ? ref.read(studyControllerProvider(id).notifier).nextChapter + : null, + icon: Icons.play_arrow, + label: context.l10n.studyNextChapter, + showLabel: true, + blink: !state.isIntroductoryChapter && state.hasNextChapter, + ), + if (!state.isIntroductoryChapter) + BottomBarButton( + onTap: () => pushPlatformRoute( + context, + rootNavigator: true, + builder: (context) => AnalysisScreen( + pgnOrId: state.pgn, + options: AnalysisOptions( + isLocalEvaluationAllowed: true, + variant: state.variant, + orientation: state.pov, + id: standaloneAnalysisId, + ), + ), + ), + icon: Icons.biotech, + label: context.l10n.analysis, + showLabel: true, + ), + ], + }, + ], + ); + } +} diff --git a/lib/src/view/study/study_gamebook.dart b/lib/src/view/study/study_gamebook.dart new file mode 100644 index 0000000000..67b6d17583 --- /dev/null +++ b/lib/src/view/study/study_gamebook.dart @@ -0,0 +1,174 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_linkify/flutter_linkify.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/study/study_controller.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class StudyGamebook extends StatelessWidget { + const StudyGamebook( + this.id, + ); + + final StudyId id; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(5), + child: Column( + children: [ + Expanded( + child: Card( + child: Padding( + padding: const EdgeInsets.all(10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _Comment(id: id), + _Hint(id: id), + ], + ), + ), + ), + ), + ], + ), + ); + } +} + +class _Comment extends ConsumerWidget { + const _Comment({ + required this.id, + }); + + final StudyId id; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final state = ref.watch(studyControllerProvider(id)).requireValue; + + final comment = state.gamebookComment ?? + switch (state.gamebookState) { + GamebookState.findTheMove => context.l10n.studyWhatWouldYouPlay, + GamebookState.correctMove => context.l10n.studyGoodMove, + GamebookState.incorrectMove => context.l10n.puzzleNotTheMove, + GamebookState.lessonComplete => + context.l10n.studyYouCompletedThisLesson, + _ => '' + }; + + return Expanded( + child: Scrollbar( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.only(right: 5), + child: Linkify( + text: comment, + style: const TextStyle( + fontSize: 16, + ), + onOpen: (link) async { + launchUrl(Uri.parse(link.url)); + }, + ), + ), + ), + ), + ); + } +} + +class _Hint extends ConsumerStatefulWidget { + const _Hint({ + required this.id, + }); + + final StudyId id; + + @override + ConsumerState<_Hint> createState() => _HintState(); +} + +class _HintState extends ConsumerState<_Hint> { + bool showHint = false; + + @override + Widget build(BuildContext context) { + final hint = + ref.watch(studyControllerProvider(widget.id)).requireValue.gamebookHint; + return hint == null + ? const SizedBox.shrink() + : SizedBox( + height: 40, + child: showHint + ? Center(child: Text(hint)) + : TextButton( + onPressed: () { + setState(() { + showHint = true; + }); + }, + child: Text(context.l10n.getAHint), + ), + ); + } +} + +class GamebookButton extends StatelessWidget { + const GamebookButton({ + required this.icon, + required this.label, + required this.onTap, + this.highlighted = false, + super.key, + }); + + final IconData icon; + final String label; + final VoidCallback? onTap; + + final bool highlighted; + + bool get enabled => onTap != null; + + @override + Widget build(BuildContext context) { + final primary = Theme.of(context).colorScheme.primary; + + return Semantics( + container: true, + enabled: enabled, + button: true, + label: label, + excludeSemantics: true, + child: AdaptiveInkWell( + borderRadius: BorderRadius.zero, + onTap: onTap, + child: Opacity( + opacity: enabled ? 1.0 : 0.4, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon(icon, color: highlighted ? primary : null, size: 24), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Text( + label, + style: TextStyle( + fontSize: 16.0, + color: highlighted ? primary : null, + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/src/view/study/study_screen.dart b/lib/src/view/study/study_screen.dart index 3380854e23..7a1b246d6a 100644 --- a/lib/src/view/study/study_screen.dart +++ b/lib/src/view/study/study_screen.dart @@ -19,6 +19,7 @@ import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; import 'package:lichess_mobile/src/view/engine/engine_lines.dart'; import 'package:lichess_mobile/src/view/study/study_bottom_bar.dart'; +import 'package:lichess_mobile/src/view/study/study_gamebook.dart'; import 'package:lichess_mobile/src/view/study/study_settings.dart'; import 'package:lichess_mobile/src/view/study/study_tree_view.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; @@ -176,6 +177,11 @@ class _Body extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final gamebookActive = ref.watch( + studyControllerProvider(id) + .select((state) => state.requireValue.gamebookActive), + ); + return SafeArea( child: Column( children: [ @@ -229,6 +235,9 @@ class _Body extends ConsumerWidget { ) : null; + final bottomChild = + gamebookActive ? StudyGamebook(id) : StudyTreeView(id); + return landscape ? Row( mainAxisSize: MainAxisSize.max, @@ -267,7 +276,7 @@ class _Body extends ConsumerWidget { kTabletBoardTableSidePadding, ), semanticContainer: false, - child: StudyTreeView(id), + child: bottomChild, ), ), ], @@ -305,7 +314,7 @@ class _Body extends ConsumerWidget { horizontal: kTabletBoardTableSidePadding, ) : EdgeInsets.zero, - child: StudyTreeView(id), + child: bottomChild, ), ), ], @@ -384,6 +393,7 @@ class _StudyBoardState extends ConsumerState<_StudyBoard> { (prefs) => prefs.showVariationArrows, ), ) && + !studyState.gamebookActive && currentNode.children.length > 1; final pgnShapes = ISet( diff --git a/lib/src/view/tools/tools_tab_screen.dart b/lib/src/view/tools/tools_tab_screen.dart index 77765f9282..8270f84bcc 100644 --- a/lib/src/view/tools/tools_tab_screen.dart +++ b/lib/src/view/tools/tools_tab_screen.dart @@ -12,7 +12,7 @@ import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; import 'package:lichess_mobile/src/view/board_editor/board_editor_screen.dart'; -import 'package:lichess_mobile/src/view/clock/clock_screen.dart'; +import 'package:lichess_mobile/src/view/clock/clock_tool_screen.dart'; import 'package:lichess_mobile/src/view/coordinate_training/coordinate_training_screen.dart'; import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_screen.dart'; import 'package:lichess_mobile/src/view/study/study_list_screen.dart'; @@ -209,7 +209,7 @@ class _Body extends ConsumerWidget { title: context.l10n.clock, onTap: () => pushPlatformRoute( context, - builder: (context) => const ClockScreen(), + builder: (context) => const ClockToolScreen(), rootNavigator: true, ), ), diff --git a/lib/src/view/watch/tv_screen.dart b/lib/src/view/watch/tv_screen.dart index 5d55705d7e..98df340be2 100644 --- a/lib/src/view/watch/tv_screen.dart +++ b/lib/src/view/watch/tv_screen.dart @@ -13,7 +13,7 @@ import 'package:lichess_mobile/src/widgets/board_table.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; -import 'package:lichess_mobile/src/widgets/countdown_clock.dart'; +import 'package:lichess_mobile/src/widgets/clock.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; class TvScreen extends ConsumerStatefulWidget { @@ -92,10 +92,19 @@ class _Body extends ConsumerWidget { final blackPlayerWidget = GamePlayer( player: game.black.setOnGame(true), clock: gameState.game.clock != null - ? CountdownClock( + ? CountdownClockBuilder( key: blackClockKey, - duration: gameState.game.clock!.black, + timeLeft: gameState.game.clock!.black, + delay: gameState.game.clock!.lag ?? + const Duration(milliseconds: 10), + clockUpdatedAt: gameState.game.clock!.at, active: gameState.activeClockSide == Side.black, + builder: (context, timeLeft) { + return Clock( + timeLeft: timeLeft, + active: gameState.activeClockSide == Side.black, + ); + }, ) : null, materialDiff: game.lastMaterialDiffAt(Side.black), @@ -103,10 +112,19 @@ class _Body extends ConsumerWidget { final whitePlayerWidget = GamePlayer( player: game.white.setOnGame(true), clock: gameState.game.clock != null - ? CountdownClock( + ? CountdownClockBuilder( key: whiteClockKey, - duration: gameState.game.clock!.white, + timeLeft: gameState.game.clock!.white, + clockUpdatedAt: gameState.game.clock!.at, + delay: gameState.game.clock!.lag ?? + const Duration(milliseconds: 10), active: gameState.activeClockSide == Side.white, + builder: (context, timeLeft) { + return Clock( + timeLeft: timeLeft, + active: gameState.activeClockSide == Side.white, + ); + }, ) : null, materialDiff: game.lastMaterialDiffAt(Side.white), diff --git a/lib/src/widgets/board_table.dart b/lib/src/widgets/board_table.dart index c9cba27a51..90681e2455 100644 --- a/lib/src/widgets/board_table.dart +++ b/lib/src/widgets/board_table.dart @@ -267,7 +267,7 @@ class _BoardTableState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - Flexible(child: widget.topTable), + widget.topTable, if (!widget.zenMode && slicedMoves != null) Expanded( child: Padding( @@ -282,14 +282,8 @@ class _BoardTableState extends ConsumerState { ), ) else - // same height as [MoveList] - const Expanded( - child: Padding( - padding: EdgeInsets.all(16.0), - child: SizedBox(height: 40), - ), - ), - Flexible(child: widget.bottomTable), + const Spacer(), + widget.bottomTable, ], ), ), diff --git a/lib/src/widgets/countdown_clock.dart b/lib/src/widgets/clock.dart similarity index 64% rename from lib/src/widgets/countdown_clock.dart rename to lib/src/widgets/clock.dart index cfae7ca1ce..7de23af3e0 100644 --- a/lib/src/widgets/countdown_clock.dart +++ b/lib/src/widgets/clock.dart @@ -1,170 +1,23 @@ import 'dart:async'; +import 'package:clock/clock.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/constants.dart'; -import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; -/// A simple countdown clock. -/// -/// The clock starts only when [active] is `true`. -class CountdownClock extends ConsumerStatefulWidget { - const CountdownClock({ - required this.duration, - required this.active, - this.emergencyThreshold, - this.emergencySoundEnabled = true, - this.onFlag, - this.onStop, - this.clockStyle, - this.padLeft = false, - super.key, - }); - - /// The duration left on the clock. - final Duration duration; - - /// If [timeLeft] is less than [emergencyThreshold], the clock will change - /// its background color to [ClockStyle.emergencyBackgroundColor] activeBackgroundColor - /// If [emergencySoundEnabled] is `true`, the clock will also play a sound. - final Duration? emergencyThreshold; - - /// Whether to play an emergency sound when the clock reaches the emergency - final bool emergencySoundEnabled; - - /// If [active] is `true`, the clock starts counting down. - final bool active; - - /// Callback when the clock reaches zero. - final VoidCallback? onFlag; - - /// Callback with the remaining duration when the clock stops - final ValueSetter? onStop; - - /// Custom color style - final ClockStyle? clockStyle; - - /// Whether to pad with a leading zero (default is `false`). - final bool padLeft; - - @override - ConsumerState createState() => _CountdownClockState(); -} - -const _period = Duration(milliseconds: 100); -const _emergencyDelay = Duration(seconds: 20); - -class _CountdownClockState extends ConsumerState { - Timer? _timer; - Duration timeLeft = Duration.zero; - bool _shouldPlayEmergencyFeedback = true; - DateTime? _nextEmergency; - - final _stopwatch = Stopwatch(); - - void startClock() { - _timer?.cancel(); - _stopwatch.reset(); - _stopwatch.start(); - _timer = Timer.periodic(_period, (timer) { - setState(() { - timeLeft = timeLeft - _stopwatch.elapsed; - _stopwatch.reset(); - _playEmergencyFeedback(); - if (timeLeft <= Duration.zero) { - widget.onFlag?.call(); - timeLeft = Duration.zero; - stopClock(); - } - }); - }); - } - - void stopClock() { - setState(() { - timeLeft = timeLeft - _stopwatch.elapsed; - if (timeLeft < Duration.zero) { - timeLeft = Duration.zero; - } - }); - _timer?.cancel(); - _stopwatch.stop(); - scheduleMicrotask(() { - widget.onStop?.call(timeLeft); - }); - } - - void _playEmergencyFeedback() { - if (widget.emergencyThreshold != null && - timeLeft <= widget.emergencyThreshold! && - _shouldPlayEmergencyFeedback && - (_nextEmergency == null || _nextEmergency!.isBefore(DateTime.now()))) { - _shouldPlayEmergencyFeedback = false; - _nextEmergency = DateTime.now().add(_emergencyDelay); - if (widget.emergencySoundEnabled) { - ref.read(soundServiceProvider).play(Sound.lowTime); - } - HapticFeedback.heavyImpact(); - } else if (widget.emergencyThreshold != null && - timeLeft > widget.emergencyThreshold! * 1.5) { - _shouldPlayEmergencyFeedback = true; - } - } - - @override - void initState() { - super.initState(); - timeLeft = widget.duration; - if (widget.active) { - startClock(); - } - } - - @override - void didUpdateWidget(CountdownClock oldClock) { - super.didUpdateWidget(oldClock); - if (widget.duration != oldClock.duration) { - timeLeft = widget.duration; - } - - if (widget.active != oldClock.active) { - widget.active ? startClock() : stopClock(); - } - } - - @override - void dispose() { - super.dispose(); - _timer?.cancel(); - } - - @override - Widget build(BuildContext context) { - return RepaintBoundary( - child: Clock( - padLeft: widget.padLeft, - timeLeft: timeLeft, - active: widget.active, - emergencyThreshold: widget.emergencyThreshold, - clockStyle: widget.clockStyle, - ), - ); - } -} - const _kClockFontSize = 26.0; const _kClockTenthFontSize = 20.0; const _kClockHundredsFontSize = 18.0; +const _showTenthsThreshold = Duration(seconds: 10); + /// A stateless widget that displays the time left on the clock. /// -/// For a clock widget that automatically counts down, see [CountdownClock]. +/// For a clock widget that automatically counts down, see [CountdownClockBuilder]. class Clock extends StatelessWidget { const Clock({ required this.timeLeft, - required this.active, + this.active = false, this.clockStyle, this.emergencyThreshold, this.padLeft = false, @@ -192,7 +45,7 @@ class Clock extends StatelessWidget { final hours = timeLeft.inHours; final mins = timeLeft.inMinutes.remainder(60); final secs = timeLeft.inSeconds.remainder(60).toString().padLeft(2, '0'); - final showTenths = timeLeft < const Duration(seconds: 10); + final showTenths = timeLeft < _showTenthsThreshold; final isEmergency = emergencyThreshold != null && timeLeft <= emergencyThreshold!; final remainingHeight = estimateRemainingHeightLeftBoard(context); @@ -310,3 +163,154 @@ class ClockStyle { emergencyBackgroundColor: Color(0xFFF2CCCC), ); } + +typedef ClockWidgetBuilder = Widget Function(BuildContext, Duration); + +/// A widget that automatically starts a countdown from [timeLeft] when [active] is `true`. +/// +/// The clock will update the UI every [tickInterval], which defaults to 100ms, +/// and the [builder] will be called with the new [timeLeft] value. +/// +/// The clock can be synchronized with the time at which the clock event was received from the server +/// by setting the [clockUpdatedAt] parameter. +/// This widget will only update its internal clock when the [clockUpdatedAt] parameter changes. +/// +/// The [delay] parameter can be used to delay the start of the clock. +/// +/// The clock will stop counting down when [active] is set to `false`. +/// +/// The clock will stop counting down when the time left reaches zero. +class CountdownClockBuilder extends StatefulWidget { + const CountdownClockBuilder({ + required this.timeLeft, + required this.active, + required this.builder, + this.delay, + this.tickInterval = const Duration(milliseconds: 100), + this.clockUpdatedAt, + super.key, + }); + + /// The duration left on the clock. + final Duration timeLeft; + + /// The delay before the clock starts counting down. + /// + /// This can be used to implement lag compensation. + final Duration? delay; + + /// The interval at which the clock updates the UI. + final Duration tickInterval; + + /// The time at which the clock was updated. + /// + /// Use this parameter to synchronize the clock with the time at which the clock + /// event was received from the server and to compensate for UI lag. + final DateTime? clockUpdatedAt; + + /// If `true`, the clock starts counting down. + final bool active; + + /// A [ClockWidgetBuilder] that builds the clock on each tick with the new [timeLeft] value. + final ClockWidgetBuilder builder; + + @override + State createState() => _CountdownClockState(); +} + +class _CountdownClockState extends State { + Timer? _timer; + Timer? _delayTimer; + Duration timeLeft = Duration.zero; + + final _stopwatch = clock.stopwatch(); + + void startClock() { + final now = clock.now(); + final delay = widget.delay ?? Duration.zero; + final clockUpdatedAt = widget.clockUpdatedAt ?? now; + // UI lag diff: the elapsed time between the time the clock should have started + // and the time the clock is actually started + final uiLag = now.difference(clockUpdatedAt); + final realDelay = delay - uiLag; + + // real delay is negative, we need to adjust the timeLeft. + if (realDelay < Duration.zero) { + final newTimeLeft = timeLeft + realDelay; + timeLeft = newTimeLeft > Duration.zero ? newTimeLeft : Duration.zero; + } + + if (realDelay > Duration.zero) { + _delayTimer?.cancel(); + _delayTimer = Timer(realDelay, _scheduleTick); + } else { + _scheduleTick(); + } + } + + void _scheduleTick() { + _timer?.cancel(); + _timer = Timer(widget.tickInterval, _tick); + _stopwatch.reset(); + _stopwatch.start(); + } + + void _tick() { + final newTimeLeft = timeLeft - _stopwatch.elapsed; + setState(() { + timeLeft = newTimeLeft; + if (timeLeft <= Duration.zero) { + timeLeft = Duration.zero; + } + }); + if (timeLeft > Duration.zero) { + _scheduleTick(); + } + } + + void stopClock() { + _delayTimer?.cancel(); + _timer?.cancel(); + _stopwatch.stop(); + } + + @override + void initState() { + super.initState(); + timeLeft = widget.timeLeft; + if (widget.active) { + startClock(); + } + } + + @override + void didUpdateWidget(CountdownClockBuilder oldClock) { + super.didUpdateWidget(oldClock); + + if (widget.clockUpdatedAt != oldClock.clockUpdatedAt) { + timeLeft = widget.timeLeft; + } + + if (widget.active != oldClock.active) { + if (widget.active) { + startClock(); + } else { + stopClock(); + } + } + } + + @override + void dispose() { + super.dispose(); + _delayTimer?.cancel(); + _timer?.cancel(); + } + + @override + Widget build(BuildContext context) { + return RepaintBoundary( + child: widget.builder(context, timeLeft), + ); + } +} diff --git a/lib/src/widgets/rating.dart b/lib/src/widgets/rating.dart index 69df2388c9..fae2ea6472 100644 --- a/lib/src/widgets/rating.dart +++ b/lib/src/widgets/rating.dart @@ -19,10 +19,8 @@ class RatingWidget extends StatelessWidget { @override Widget build(BuildContext context) { - final ratingStr = - rating is double ? rating.toStringAsFixed(2) : rating.toString(); return Text( - '$ratingStr${provisional == true || deviation > kProvisionalDeviation ? '?' : ''}', + '${rating.round()}${provisional == true || deviation > kProvisionalDeviation ? '?' : ''}', style: style, ); } diff --git a/test/model/challenge/challenge_service_test.dart b/test/model/challenge/challenge_service_test.dart index 10872457a1..25c637853a 100644 --- a/test/model/challenge/challenge_service_test.dart +++ b/test/model/challenge/challenge_service_test.dart @@ -32,7 +32,7 @@ void main() { test('exposes a challenges stream', () async { final fakeChannel = FakeWebSocketChannel(); final socketClient = - makeTestSocketClient(FakeWebSocketChannelFactory(() => fakeChannel)); + makeTestSocketClient(FakeWebSocketChannelFactory((_) => fakeChannel)); await socketClient.connect(); await socketClient.firstConnection; @@ -118,7 +118,7 @@ void main() { fakeAsync((async) { final fakeChannel = FakeWebSocketChannel(); final socketClient = - makeTestSocketClient(FakeWebSocketChannelFactory(() => fakeChannel)); + makeTestSocketClient(FakeWebSocketChannelFactory((_) => fakeChannel)); socketClient.connect(); notificationService.start(); challengeService.start(); @@ -221,7 +221,7 @@ void main() { fakeAsync((async) { final fakeChannel = FakeWebSocketChannel(); final socketClient = - makeTestSocketClient(FakeWebSocketChannelFactory(() => fakeChannel)); + makeTestSocketClient(FakeWebSocketChannelFactory((_) => fakeChannel)); socketClient.connect(); notificationService.start(); challengeService.start(); diff --git a/test/model/clock/chess_clock_test.dart b/test/model/clock/chess_clock_test.dart new file mode 100644 index 0000000000..f0c2a45870 --- /dev/null +++ b/test/model/clock/chess_clock_test.dart @@ -0,0 +1,231 @@ +import 'package:dartchess/dartchess.dart'; +import 'package:fake_async/fake_async.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:lichess_mobile/src/model/clock/chess_clock.dart'; + +void main() { + test('make clock', () { + final clock = ChessClock( + whiteTime: const Duration(seconds: 5), + blackTime: const Duration(seconds: 5), + ); + expect(clock.isRunning, false); + expect(clock.whiteTime.value, const Duration(seconds: 5)); + expect(clock.blackTime.value, const Duration(seconds: 5)); + }); + + test('start clock', () { + fakeAsync((async) { + final clock = ChessClock( + whiteTime: const Duration(seconds: 5), + blackTime: const Duration(seconds: 5), + ); + clock.start(); + expect(clock.isRunning, true); + }); + }); + + test('clock ticking', () { + fakeAsync((async) { + final clock = ChessClock( + whiteTime: const Duration(seconds: 5), + blackTime: const Duration(seconds: 5), + ); + clock.start(); + expect(clock.whiteTime.value, const Duration(seconds: 5)); + expect(clock.blackTime.value, const Duration(seconds: 5)); + async.elapse(const Duration(seconds: 1)); + expect(clock.whiteTime.value, const Duration(seconds: 4)); + expect(clock.blackTime.value, const Duration(seconds: 5)); + async.elapse(const Duration(seconds: 1)); + expect(clock.whiteTime.value, const Duration(seconds: 3)); + expect(clock.blackTime.value, const Duration(seconds: 5)); + }); + }); + + test('stop clock', () { + fakeAsync((async) { + final clock = ChessClock( + whiteTime: const Duration(seconds: 5), + blackTime: const Duration(seconds: 5), + ); + clock.start(); + expect(clock.isRunning, true); + async.elapse(const Duration(seconds: 1)); + expect(clock.whiteTime.value, const Duration(seconds: 4)); + expect(clock.blackTime.value, const Duration(seconds: 5)); + final thinkTime = clock.stop(); + expect(clock.isRunning, false); + expect(thinkTime, const Duration(seconds: 1)); + async.elapse(const Duration(seconds: 1)); + expect(clock.whiteTime.value, const Duration(seconds: 4)); + expect(clock.blackTime.value, const Duration(seconds: 5)); + }); + }); + + test('start side', () { + fakeAsync((async) { + final clock = ChessClock( + whiteTime: const Duration(seconds: 5), + blackTime: const Duration(seconds: 5), + ); + final thinkTime = clock.startSide(Side.black); + expect(thinkTime, null); + async.elapse(const Duration(seconds: 1)); + expect(clock.whiteTime.value, const Duration(seconds: 5)); + expect(clock.blackTime.value, const Duration(seconds: 4)); + }); + }); + + test('start side (running clock)', () { + fakeAsync((async) { + final clock = ChessClock( + whiteTime: const Duration(seconds: 5), + blackTime: const Duration(seconds: 5), + ); + clock.start(); + expect(clock.isRunning, true); + expect(clock.whiteTime.value, const Duration(seconds: 5)); + expect(clock.blackTime.value, const Duration(seconds: 5)); + async.elapse(const Duration(seconds: 1)); + expect(clock.whiteTime.value, const Duration(seconds: 4)); + expect(clock.blackTime.value, const Duration(seconds: 5)); + final thinkTime = clock.startSide(Side.black); + expect(thinkTime, const Duration(seconds: 1)); + async.elapse(const Duration(seconds: 1)); + expect(clock.whiteTime.value, const Duration(seconds: 4)); + expect(clock.blackTime.value, const Duration(seconds: 4)); + }); + }); + + test('start side (running clock, same side)', () { + fakeAsync((async) { + final clock = ChessClock( + whiteTime: const Duration(seconds: 5), + blackTime: const Duration(seconds: 5), + ); + clock.start(); + expect(clock.isRunning, true); + expect(clock.whiteTime.value, const Duration(seconds: 5)); + expect(clock.blackTime.value, const Duration(seconds: 5)); + async.elapse(const Duration(seconds: 1)); + expect(clock.whiteTime.value, const Duration(seconds: 4)); + expect(clock.blackTime.value, const Duration(seconds: 5)); + final thinkTime = clock.startSide(Side.white); + expect(thinkTime, const Duration(seconds: 1)); + async.elapse(const Duration(seconds: 1)); + expect(clock.whiteTime.value, const Duration(seconds: 3)); + expect(clock.blackTime.value, const Duration(seconds: 5)); + }); + }); + + test('start with delay', () { + fakeAsync((async) { + final clock = ChessClock( + whiteTime: const Duration(seconds: 5), + blackTime: const Duration(seconds: 5), + ); + clock.start(delay: const Duration(milliseconds: 20)); + expect(clock.isRunning, true); + expect(clock.whiteTime.value, const Duration(seconds: 5)); + async.elapse(const Duration(milliseconds: 10)); + expect(clock.whiteTime.value, const Duration(seconds: 5)); + // the start delay is reached, but clock not updated yet since tick delay is 100ms + async.elapse(const Duration(milliseconds: 100)); + expect(clock.whiteTime.value, const Duration(seconds: 5)); + async.elapse(const Duration(milliseconds: 10)); + expect(clock.whiteTime.value, const Duration(milliseconds: 4900)); + final thinkTime = clock.stop(); + expect(thinkTime, const Duration(milliseconds: 100)); + }); + }); + + test('increment times', () { + fakeAsync((async) { + final clock = ChessClock( + whiteTime: const Duration(seconds: 5), + blackTime: const Duration(seconds: 5), + ); + clock.start(); + expect(clock.whiteTime.value, const Duration(seconds: 5)); + expect(clock.blackTime.value, const Duration(seconds: 5)); + clock.incTimes(whiteInc: const Duration(seconds: 1)); + expect(clock.whiteTime.value, const Duration(seconds: 6)); + expect(clock.blackTime.value, const Duration(seconds: 5)); + clock.incTimes(blackInc: const Duration(seconds: 1)); + expect(clock.whiteTime.value, const Duration(seconds: 6)); + expect(clock.blackTime.value, const Duration(seconds: 6)); + }); + }); + + test('increment specific side', () { + fakeAsync((async) { + final clock = ChessClock( + whiteTime: const Duration(seconds: 5), + blackTime: const Duration(seconds: 5), + ); + clock.start(); + expect(clock.whiteTime.value, const Duration(seconds: 5)); + expect(clock.blackTime.value, const Duration(seconds: 5)); + clock.incTime(Side.white, const Duration(seconds: 1)); + expect(clock.whiteTime.value, const Duration(seconds: 6)); + expect(clock.blackTime.value, const Duration(seconds: 5)); + clock.incTime(Side.black, const Duration(seconds: 1)); + expect(clock.whiteTime.value, const Duration(seconds: 6)); + expect(clock.blackTime.value, const Duration(seconds: 6)); + }); + }); + + test('flag', () { + fakeAsync((async) { + int flagCount = 0; + final clock = ChessClock( + whiteTime: const Duration(seconds: 5), + blackTime: const Duration(seconds: 5), + onFlag: () { + flagCount++; + }, + ); + clock.start(); + expect(clock.whiteTime.value, const Duration(seconds: 5)); + expect(clock.blackTime.value, const Duration(seconds: 5)); + async.elapse(const Duration(seconds: 5)); + expect(flagCount, 1); + expect(clock.whiteTime.value, Duration.zero); + expect(clock.blackTime.value, const Duration(seconds: 5)); + + // continue ticking and calling onFlag + async.elapse(const Duration(milliseconds: 200)); + expect(flagCount, 3); + clock.stop(); + + // no more onFlag calls + async.elapse(const Duration(seconds: 5)); + expect(flagCount, 3); + }); + }); + + test('onEmergency', () { + fakeAsync((async) { + int onEmergencyCount = 0; + final clock = ChessClock( + whiteTime: const Duration(seconds: 5), + blackTime: const Duration(seconds: 5), + emergencyThreshold: const Duration(seconds: 2), + onEmergency: (_) { + onEmergencyCount++; + }, + ); + clock.start(); + expect(clock.whiteTime.value, const Duration(seconds: 5)); + expect(clock.blackTime.value, const Duration(seconds: 5)); + async.elapse(const Duration(seconds: 2)); + expect(clock.whiteTime.value, const Duration(seconds: 3)); + expect(clock.blackTime.value, const Duration(seconds: 5)); + async.elapse(const Duration(seconds: 1)); + expect(onEmergencyCount, 1); + async.elapse(const Duration(milliseconds: 100)); + expect(onEmergencyCount, 1); + }); + }); +} diff --git a/test/model/game/game_socket_events_test.dart b/test/model/game/game_socket_events_test.dart index 12d4ee32a4..7bb4597c5c 100644 --- a/test/model/game/game_socket_events_test.dart +++ b/test/model/game/game_socket_events_test.dart @@ -15,12 +15,17 @@ void main() { final game = fullEvent.game; expect(game.id, const GameId('nV3DaALy')); expect( - game.clock, - const PlayableClockData( - running: true, - white: Duration(seconds: 149, milliseconds: 50), - black: Duration(seconds: 775, milliseconds: 940), - ), + game.clock?.running, + true, + ); + expect( + game.clock?.white, + const Duration(seconds: 149, milliseconds: 50), + ); + + expect( + game.clock?.black, + const Duration(seconds: 775, milliseconds: 940), ); expect( game.meta, diff --git a/test/model/game/game_socket_example_data.dart b/test/model/game/game_socket_example_data.dart new file mode 100644 index 0000000000..f4f3cc4c95 --- /dev/null +++ b/test/model/game/game_socket_example_data.dart @@ -0,0 +1,103 @@ +import 'package:dartchess/dartchess.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; + +typedef FullEventTestClock = ({ + bool running, + Duration initial, + Duration increment, + Duration? emerg, + Duration white, + Duration black, +}); + +String makeFullEvent( + GameId id, + String pgn, { + required String whiteUserName, + required String blackUserName, + int socketVersion = 0, + Side? youAre, + FullEventTestClock clock = const ( + running: false, + initial: Duration(minutes: 3), + increment: Duration(seconds: 2), + emerg: Duration(seconds: 30), + white: Duration(minutes: 3), + black: Duration(minutes: 3), + ), +}) { + final youAreStr = youAre != null ? '"youAre": "${youAre.name}",' : ''; + return ''' +{ + "t": "full", + "d": { + "game": { + "id": "$id", + "variant": { + "key": "standard", + "name": "Standard", + "short": "Std" + }, + "speed": "blitz", + "perf": "blitz", + "rated": false, + "source": "lobby", + "status": { + "id": 20, + "name": "started" + }, + "createdAt": 1685698678928, + "pgn": "$pgn" + }, + "white": { + "user": { + "name": "$whiteUserName", + "patron": true, + "id": "${whiteUserName.toLowerCase()}" + }, + "rating": 1806, + "provisional": true, + "onGame": true + }, + "black": { + "user": { + "name": "$blackUserName", + "patron": true, + "id": "${blackUserName.toLowerCase()}" + }, + "onGame": true + }, + $youAreStr + "socket": $socketVersion, + "clock": { + "running": ${clock.running}, + "initial": ${clock.initial.inSeconds}, + "increment": ${clock.increment.inSeconds}, + "white": ${(clock.white.inMilliseconds / 1000).toStringAsFixed(2)}, + "black": ${(clock.black.inMilliseconds / 1000).toStringAsFixed(2)}, + "emerg": 30, + "moretime": 15 + }, + "expiration": { + "idleMillis": 245, + "millisToMove": 30000 + }, + "chat": { + "lines": [ + { + "u": "Zidrox", + "t": "Good luck", + "f": "people.man-singer" + }, + { + "u": "lichess", + "t": "Takeback accepted", + "f": "activity.lichess" + } + ] + } + }, + "v": $socketVersion +} +'''; +} diff --git a/test/network/fake_websocket_channel.dart b/test/network/fake_websocket_channel.dart index 1d639eb2f2..88e3391d04 100644 --- a/test/network/fake_websocket_channel.dart +++ b/test/network/fake_websocket_channel.dart @@ -7,7 +7,7 @@ import 'package:stream_channel/stream_channel.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; class FakeWebSocketChannelFactory implements WebSocketChannelFactory { - final FutureOr Function() createFunction; + final FutureOr Function(String url) createFunction; const FakeWebSocketChannelFactory(this.createFunction); @@ -17,7 +17,7 @@ class FakeWebSocketChannelFactory implements WebSocketChannelFactory { Map? headers, Duration timeout = const Duration(seconds: 1), }) async { - return createFunction(); + return createFunction(url); } } @@ -30,11 +30,19 @@ class FakeWebSocketChannelFactory implements WebSocketChannelFactory { /// behavior can be changed by setting [shouldSendPong] to false. /// /// It also allows to increase the lag of the connection by setting the -/// [connectionLag] property. +/// [connectionLag] property. By default [connectionLag] is set to [Duration.zero] +/// to simplify testing. +/// When lag is 0, the pong response will be sent in the next microtask. /// /// The [sentMessages] and [sentMessagesExceptPing] streams can be used to /// verify that the client sends the expected messages. class FakeWebSocketChannel implements WebSocketChannel { + FakeWebSocketChannel({this.connectionLag = Duration.zero}); + + int _pongCount = 0; + + final _connectionCompleter = Completer(); + static bool isPing(dynamic data) { if (data is! String) { return false; @@ -55,13 +63,19 @@ class FakeWebSocketChannel implements WebSocketChannel { /// The controller for outgoing (to server) messages. final _outcomingController = StreamController.broadcast(); + /// The lag of the connection (duration before pong response) in milliseconds. + Duration connectionLag; + /// Whether the server should send a pong response to a ping request. /// /// Can be used to simulate a faulty connection. bool shouldSendPong = true; - /// The lag of the connection (duration before pong response) in milliseconds. - Duration connectionLag = const Duration(milliseconds: 10); + /// Number of pong response received + int get pongCount => _pongCount; + + /// A Future that resolves when the first pong message is received + Future get connectionEstablished => _connectionCompleter.future; /// The stream of all outgoing messages. Stream get sentMessages => _outcomingController.stream; @@ -157,12 +171,22 @@ class FakeWebSocketSink implements WebSocketSink { // Simulates pong response if connection is not closed if (_channel.shouldSendPong && FakeWebSocketChannel.isPing(data)) { - Future.delayed(_channel.connectionLag, () { + void sendPong() { if (_channel._incomingController.isClosed) { return; } + _channel._pongCount++; + if (_channel._pongCount == 1) { + _channel._connectionCompleter.complete(); + } _channel._incomingController.add('0'); - }); + } + + if (_channel.connectionLag > Duration.zero) { + Future.delayed(_channel.connectionLag, sendPong); + } else { + scheduleMicrotask(sendPong); + } } } diff --git a/test/network/socket_test.dart b/test/network/socket_test.dart index 86e5d34d80..c5cbe7d5d2 100644 --- a/test/network/socket_test.dart +++ b/test/network/socket_test.dart @@ -45,7 +45,7 @@ void main() { final fakeChannel = FakeWebSocketChannel(); final socketClient = - makeTestSocketClient(FakeWebSocketChannelFactory(() => fakeChannel)); + makeTestSocketClient(FakeWebSocketChannelFactory((_) => fakeChannel)); socketClient.connect(); int sentPingCount = 0; @@ -69,7 +69,7 @@ void main() { test('reconnects when connection attempt fails', () async { int numConnectionAttempts = 0; - final fakeChannelFactory = FakeWebSocketChannelFactory(() { + final fakeChannelFactory = FakeWebSocketChannelFactory((_) { numConnectionAttempts++; if (numConnectionAttempts == 1) { throw const SocketException('Connection failed'); @@ -95,7 +95,7 @@ void main() { // channels per connection attempt final Map channels = {}; - final fakeChannelFactory = FakeWebSocketChannelFactory(() { + final fakeChannelFactory = FakeWebSocketChannelFactory((_) { numConnectionAttempts++; final channel = FakeWebSocketChannel(); int sentPingCount = 0; @@ -133,10 +133,11 @@ void main() { }); test('computes average lag', () async { - final fakeChannel = FakeWebSocketChannel(); + final fakeChannel = + FakeWebSocketChannel(connectionLag: const Duration(milliseconds: 10)); final socketClient = - makeTestSocketClient(FakeWebSocketChannelFactory(() => fakeChannel)); + makeTestSocketClient(FakeWebSocketChannelFactory((_) => fakeChannel)); socketClient.connect(); // before the connection is ready the average lag is zero @@ -188,7 +189,7 @@ void main() { final fakeChannel = FakeWebSocketChannel(); final socketClient = - makeTestSocketClient(FakeWebSocketChannelFactory(() => fakeChannel)); + makeTestSocketClient(FakeWebSocketChannelFactory((_) => fakeChannel)); socketClient.connect(); await socketClient.firstConnection; diff --git a/test/test_container.dart b/test/test_container.dart index ecb340ee0a..7e1ab64506 100644 --- a/test/test_container.dart +++ b/test/test_container.dart @@ -68,7 +68,7 @@ Future makeContainer({ return db; }), webSocketChannelFactoryProvider.overrideWith((ref) { - return FakeWebSocketChannelFactory(() => FakeWebSocketChannel()); + return FakeWebSocketChannelFactory((_) => FakeWebSocketChannel()); }), socketPoolProvider.overrideWith((ref) { final pool = SocketPool(ref); diff --git a/test/test_helpers.dart b/test/test_helpers.dart index 448d85578c..dfedcc6b3c 100644 --- a/test/test_helpers.dart +++ b/test/test_helpers.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:chessground/chessground.dart'; import 'package:dartchess/dartchess.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; @@ -114,17 +115,18 @@ Offset squareOffset( /// Plays a move on the board. Future playMove( WidgetTester tester, - Rect boardRect, String from, String to, { + Rect? boardRect, Side orientation = Side.white, }) async { + final rect = boardRect ?? tester.getRect(find.byType(Chessboard)); await tester.tapAt( - squareOffset(Square.fromName(from), boardRect, orientation: orientation), + squareOffset(Square.fromName(from), rect, orientation: orientation), ); await tester.pump(); await tester.tapAt( - squareOffset(Square.fromName(to), boardRect, orientation: orientation), + squareOffset(Square.fromName(to), rect, orientation: orientation), ); await tester.pump(); } diff --git a/test/test_provider_scope.dart b/test/test_provider_scope.dart index 8410a60edc..81419fd5a0 100644 --- a/test/test_provider_scope.dart +++ b/test/test_provider_scope.dart @@ -183,7 +183,7 @@ Future makeTestProviderScope( }), // ignore: scoped_providers_should_specify_dependencies webSocketChannelFactoryProvider.overrideWith((ref) { - return FakeWebSocketChannelFactory(() => FakeWebSocketChannel()); + return FakeWebSocketChannelFactory((_) => FakeWebSocketChannel()); }), // ignore: scoped_providers_should_specify_dependencies socketPoolProvider.overrideWith((ref) { diff --git a/test/view/game/game_screen_test.dart b/test/view/game/game_screen_test.dart new file mode 100644 index 0000000000..18d2c45d88 --- /dev/null +++ b/test/view/game/game_screen_test.dart @@ -0,0 +1,497 @@ +import 'package:chessground/chessground.dart'; +import 'package:dartchess/dartchess.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/testing.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; +import 'package:lichess_mobile/src/model/lobby/game_seek.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/network/socket.dart'; +import 'package:lichess_mobile/src/view/game/game_screen.dart'; +import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; +import 'package:lichess_mobile/src/widgets/clock.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../../model/game/game_socket_example_data.dart'; +import '../../network/fake_websocket_channel.dart'; +import '../../test_helpers.dart'; +import '../../test_provider_scope.dart'; + +final client = MockClient((request) { + if (request.url.path == '/api/board/seek') { + return mockResponse('ok', 200); + } + return mockResponse('', 404); +}); + +class MockSoundService extends Mock implements SoundService {} + +void main() { + group('Loading', () { + testWidgets('a game directly with initialGameId', + (WidgetTester tester) async { + final fakeSocket = FakeWebSocketChannel(); + + final app = await makeTestProviderScopeApp( + tester, + home: const GameScreen( + initialGameId: GameFullId('qVChCOTcHSeW'), + ), + overrides: [ + lichessClientProvider + .overrideWith((ref) => LichessClient(client, ref)), + webSocketChannelFactoryProvider.overrideWith((ref) { + return FakeWebSocketChannelFactory((_) => fakeSocket); + }), + ], + ); + await tester.pumpWidget(app); + + // while loading, displays an empty board + expect(find.byType(Chessboard), findsOneWidget); + expect(find.byType(PieceWidget), findsNothing); + + // now the game controller is loading and screen doesn't have changed yet + await tester.pump(const Duration(milliseconds: 10)); + expect(find.byType(Chessboard), findsOneWidget); + expect(find.byType(PieceWidget), findsNothing); + + await fakeSocket.connectionEstablished; + + fakeSocket.addIncomingMessages([ + makeFullEvent( + const GameId('qVChCOTc'), + '', + whiteUserName: 'Peter', + blackUserName: 'Steven', + ), + ]); + // wait for socket message + await tester.pump(const Duration(milliseconds: 10)); + + expect(find.byType(PieceWidget), findsNWidgets(32)); + expect(find.text('Peter'), findsOneWidget); + expect(find.text('Steven'), findsOneWidget); + }); + + testWidgets('a game from the pool with a seek', + (WidgetTester tester) async { + final fakeLobbySocket = FakeWebSocketChannel(); + final fakeGameSocket = FakeWebSocketChannel(); + + final app = await makeTestProviderScopeApp( + tester, + home: const GameScreen( + seek: GameSeek( + clock: (Duration(minutes: 3), Duration(seconds: 2)), + rated: true, + ), + ), + overrides: [ + lichessClientProvider + .overrideWith((ref) => LichessClient(client, ref)), + webSocketChannelFactoryProvider.overrideWith((ref) { + return FakeWebSocketChannelFactory( + (String url) => + url.contains('lobby') ? fakeLobbySocket : fakeGameSocket, + ); + }), + ], + ); + await tester.pumpWidget(app); + + expect(find.byType(Chessboard), findsOneWidget); + expect(find.byType(PieceWidget), findsNothing); + expect(find.text('Waiting for opponent to join...'), findsOneWidget); + expect(find.text('3+2'), findsOneWidget); + expect(find.widgetWithText(BottomBarButton, 'Cancel'), findsOneWidget); + + // waiting for the game + await tester.pump(const Duration(seconds: 2)); + + // when a seek is accepted, server sends a 'redirect' message with game id + fakeLobbySocket.addIncomingMessages([ + '{"t": "redirect", "d": {"id": "qVChCOTcHSeW" }, "v": 1}', + ]); + await tester.pump(const Duration(milliseconds: 1)); + + // now the game controller is loading + expect(find.byType(Chessboard), findsOneWidget); + expect(find.byType(PieceWidget), findsNothing); + expect(find.text('Waiting for opponent to join...'), findsNothing); + expect(find.text('3+2'), findsNothing); + expect(find.widgetWithText(BottomBarButton, 'Cancel'), findsNothing); + + await fakeGameSocket.connectionEstablished; + // now that game socket is open, lobby socket should be closed + expect(fakeLobbySocket.closeCode, isNotNull); + + fakeGameSocket.addIncomingMessages([ + makeFullEvent( + const GameId('qVChCOTc'), + '', + whiteUserName: 'Peter', + blackUserName: 'Steven', + ), + ]); + // wait for socket message + await tester.pump(const Duration(milliseconds: 10)); + + expect(find.byType(PieceWidget), findsNWidgets(32)); + expect(find.text('Peter'), findsOneWidget); + expect(find.text('Steven'), findsOneWidget); + expect(find.text('Waiting for opponent to join...'), findsNothing); + expect(find.text('3+2'), findsNothing); + }); + }); + + group('Clock', () { + testWidgets('loads on game start', (WidgetTester tester) async { + final fakeSocket = FakeWebSocketChannel(); + await createTestGame(fakeSocket, tester); + expect(findClockWithTime('3:00'), findsNWidgets(2)); + expect( + tester + .widgetList(find.byType(Clock)) + .where((widget) => widget.active == false) + .length, + 2, + reason: 'clocks are not active yet', + ); + }); + + testWidgets('ticks after the first full move', (WidgetTester tester) async { + final fakeSocket = FakeWebSocketChannel(); + await createTestGame(fakeSocket, tester); + expect(findClockWithTime('3:00'), findsNWidgets(2)); + await playMove(tester, 'e2', 'e4'); + // at that point clock is not yet started + expect( + tester + .widgetList(find.byType(Clock)) + .where((widget) => widget.active == false) + .length, + 2, + reason: 'clocks are not active yet', + ); + fakeSocket.addIncomingMessages([ + '{"t": "move", "v": 1, "d": {"ply": 1, "uci": "e2e4", "san": "e4", "clock": {"white": 180, "black": 180}}}', + '{"t": "move", "v": 2, "d": {"ply": 2, "uci": "e7e5", "san": "e5", "clock": {"white": 180, "black": 180}}}', + ]); + await tester.pump(const Duration(milliseconds: 10)); + expect( + tester.widgetList(find.byType(Clock)).last.active, + true, + reason: 'my clock is now active', + ); + await tester.pump(const Duration(seconds: 1)); + expect(findClockWithTime('2:59'), findsOneWidget); + await tester.pump(const Duration(seconds: 1)); + expect(findClockWithTime('2:58'), findsOneWidget); + }); + + testWidgets('ticks immediately when resuming game', + (WidgetTester tester) async { + final fakeSocket = FakeWebSocketChannel(); + await createTestGame( + fakeSocket, + tester, + pgn: 'e4 e5 Nf3', + clock: const ( + running: true, + initial: Duration(minutes: 3), + increment: Duration(seconds: 2), + white: Duration(minutes: 2, seconds: 58), + black: Duration(minutes: 2, seconds: 54), + emerg: Duration(seconds: 30), + ), + ); + expect( + tester.widgetList(find.byType(Clock)).first.active, + true, + reason: 'black clock is already active', + ); + expect(findClockWithTime('2:58'), findsOneWidget); + expect(findClockWithTime('2:54'), findsOneWidget); + await tester.pump(const Duration(seconds: 1)); + expect(findClockWithTime('2:53'), findsOneWidget); + await tester.pump(const Duration(seconds: 1)); + expect(findClockWithTime('2:52'), findsOneWidget); + }); + + testWidgets('switch timer side after a move', (WidgetTester tester) async { + final fakeSocket = FakeWebSocketChannel(); + await createTestGame( + fakeSocket, + tester, + pgn: 'e4 e5', + clock: const ( + running: true, + initial: Duration(minutes: 3), + increment: Duration(seconds: 2), + white: Duration(minutes: 2, seconds: 58), + black: Duration(minutes: 3), + emerg: Duration(seconds: 30), + ), + ); + expect(tester.widgetList(find.byType(Clock)).last.active, true); + // simulates think time of 3s + await tester.pump(const Duration(seconds: 3)); + await playMove(tester, 'g1', 'f3'); + expect(findClockWithTime('2:55'), findsOneWidget); + expect( + tester.widgetList(find.byType(Clock)).last.active, + false, + reason: 'white clock is stopped while waiting for server ack', + ); + expect( + tester.widgetList(find.byType(Clock)).first.active, + true, + reason: 'black clock is now active but not yet ticking', + ); + expect(findClockWithTime('3:00'), findsOneWidget); + // simulates a long lag just to show the clock is not running yet + await tester.pump(const Duration(milliseconds: 200)); + expect(findClockWithTime('3:00'), findsOneWidget); + // server ack having the white clock updated with the increment + fakeSocket.addIncomingMessages([ + '{"t": "move", "v": 1, "d": {"ply": 3, "uci": "g1f3", "san": "Nf3", "clock": {"white": 177, "black": 180}}}', + ]); + await tester.pump(const Duration(milliseconds: 10)); + // we see now the white clock has got its increment + expect(findClockWithTime('2:57'), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + // black clock is ticking + expect(findClockWithTime('2:59'), findsOneWidget); + await tester.pump(const Duration(seconds: 1)); + expect(findClockWithTime('2:57'), findsOneWidget); + expect(findClockWithTime('2:58'), findsOneWidget); + await tester.pump(const Duration(seconds: 1)); + expect(findClockWithTime('2:57'), findsNWidgets(2)); + await tester.pump(const Duration(seconds: 1)); + expect(findClockWithTime('2:57'), findsOneWidget); + expect(findClockWithTime('2:56'), findsOneWidget); + }); + + testWidgets('compensates opponent lag', (WidgetTester tester) async { + final fakeSocket = FakeWebSocketChannel(); + int socketVersion = 0; + await createTestGame( + fakeSocket, + tester, + pgn: 'e4 e5 Nf3 Nc6', + clock: const ( + running: true, + initial: Duration(minutes: 1), + increment: Duration.zero, + white: Duration(seconds: 58), + black: Duration(seconds: 54), + emerg: Duration(seconds: 10), + ), + socketVersion: socketVersion, + ); + await tester.pump(const Duration(seconds: 3)); + await playMoveWithServerAck( + fakeSocket, + tester, + 'f1', + 'c4', + ply: 5, + san: 'Bc4', + clockAck: ( + white: const Duration(seconds: 55), + black: const Duration(seconds: 54), + lag: const Duration(milliseconds: 250), + ), + socketVersion: ++socketVersion, + ); + // black clock is active + expect(tester.widgetList(find.byType(Clock)).first.active, true); + expect(findClockWithTime('0:54'), findsOneWidget); + await tester.pump(const Duration(milliseconds: 250)); + // lag is 250ms, so clock will only start after that delay + expect(findClockWithTime('0:54'), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + expect(findClockWithTime('0:53'), findsOneWidget); + await tester.pump(const Duration(seconds: 1)); + expect(findClockWithTime('0:52'), findsOneWidget); + }); + + testWidgets('onEmergency', (WidgetTester tester) async { + final mockSoundService = MockSoundService(); + when(() => mockSoundService.play(Sound.lowTime)).thenAnswer((_) async {}); + final fakeSocket = FakeWebSocketChannel(); + await createTestGame( + fakeSocket, + tester, + pgn: 'e4 e5', + clock: const ( + running: true, + initial: Duration(minutes: 3), + increment: Duration(seconds: 2), + white: Duration(seconds: 40), + black: Duration(minutes: 3), + emerg: Duration(seconds: 30), + ), + overrides: [ + soundServiceProvider.overrideWith((_) => mockSoundService), + ], + ); + expect( + tester.widget(findClockWithTime('0:40')).emergencyThreshold, + const Duration(seconds: 30), + ); + await tester.pump(const Duration(seconds: 10)); + expect(findClockWithTime('0:30'), findsOneWidget); + verify(() => mockSoundService.play(Sound.lowTime)).called(1); + }); + + testWidgets('flags', (WidgetTester tester) async { + final fakeSocket = FakeWebSocketChannel(); + await createTestGame( + fakeSocket, + tester, + pgn: 'e4 e5 Nf3', + clock: const ( + running: true, + initial: Duration(minutes: 3), + increment: Duration(seconds: 2), + white: Duration(minutes: 2, seconds: 58), + black: Duration(minutes: 2, seconds: 54), + emerg: Duration(seconds: 30), + ), + ); + expect( + tester.widgetList(find.byType(Clock)).first.active, + true, + reason: 'black clock is active', + ); + + expect(findClockWithTime('2:58'), findsOneWidget); + expect(findClockWithTime('2:54'), findsOneWidget); + await tester.pump(const Duration(seconds: 1)); + expect(findClockWithTime('2:53'), findsOneWidget); + await tester.pump(const Duration(minutes: 2, seconds: 53)); + expect(findClockWithTime('2:58'), findsOneWidget); + expect(findClockWithTime('0:00.0'), findsOneWidget); + + expect( + tester.widgetList(find.byType(Clock)).first.active, + true, + reason: + 'black clock is still active after flag (as long as we have not received server ack)', + ); + + // flag messages are throttled with 500ms delay + // we'll simulate an anormally long server response of 1s to check 2 + // flag messages are sent + expectLater( + fakeSocket.sentMessagesExceptPing, + emitsInOrder([ + '{"t":"flag","d":"black"}', + '{"t":"flag","d":"black"}', + ]), + ); + await tester.pump(const Duration(seconds: 1)); + fakeSocket.addIncomingMessages([ + '{"t":"endData","d":{"status":"outoftime","winner":"white","clock":{"wc":17800,"bc":0}}}', + ]); + await tester.pump(const Duration(milliseconds: 10)); + + expect( + tester + .widgetList(find.byType(Clock)) + .where((widget) => widget.active == false) + .length, + 2, + reason: 'both clocks are now inactive', + ); + expect(findClockWithTime('2:58'), findsOneWidget); + expect(findClockWithTime('0:00.00'), findsOneWidget); + + // wait for the dong + await tester.pump(const Duration(seconds: 500)); + }); + }); +} + +Finder findClockWithTime(String text, {bool skipOffstage = true}) { + return find.ancestor( + of: find.text(text, findRichText: true, skipOffstage: skipOffstage), + matching: find.byType(Clock, skipOffstage: skipOffstage), + ); +} + +/// Simulates playing a move and getting the ack from the server after [elapsedTime]. +Future playMoveWithServerAck( + FakeWebSocketChannel socket, + WidgetTester tester, + String from, + String to, { + required String san, + required ({Duration white, Duration black, Duration? lag}) clockAck, + required int socketVersion, + required int ply, + Duration elapsedTime = const Duration(milliseconds: 10), + Side orientation = Side.white, +}) async { + await playMove(tester, from, to, orientation: orientation); + final uci = '$from$to'; + final lagStr = clockAck.lag != null + ? ', "lag": ${(clockAck.lag!.inMilliseconds / 10).round()}' + : ''; + await tester.pump(elapsedTime - const Duration(milliseconds: 1)); + socket.addIncomingMessages([ + '{"t": "move", "v": $socketVersion, "d": {"ply": $ply, "uci": "$uci", "san": "$san", "clock": {"white": ${(clockAck.white.inMilliseconds / 1000).toStringAsFixed(2)}, "black": ${(clockAck.black.inMilliseconds / 1000).toStringAsFixed(2)}$lagStr}}}', + ]); + await tester.pump(const Duration(milliseconds: 1)); +} + +/// Convenient function to start a new test game +Future createTestGame( + FakeWebSocketChannel socket, + WidgetTester tester, { + Side? youAre = Side.white, + String? pgn, + int socketVersion = 0, + FullEventTestClock clock = const ( + running: false, + initial: Duration(minutes: 3), + increment: Duration(seconds: 2), + white: Duration(minutes: 3), + black: Duration(minutes: 3), + emerg: Duration(seconds: 30), + ), + List? overrides, +}) async { + final app = await makeTestProviderScopeApp( + tester, + home: const GameScreen( + initialGameId: GameFullId('qVChCOTcHSeW'), + ), + overrides: [ + lichessClientProvider.overrideWith((ref) => LichessClient(client, ref)), + webSocketChannelFactoryProvider.overrideWith((ref) { + return FakeWebSocketChannelFactory((_) => socket); + }), + ...?overrides, + ], + ); + await tester.pumpWidget(app); + await tester.pump(const Duration(milliseconds: 10)); + await socket.connectionEstablished; + + socket.addIncomingMessages([ + makeFullEvent( + const GameId('qVChCOTc'), + pgn ?? '', + whiteUserName: 'Peter', + blackUserName: 'Steven', + youAre: youAre, + socketVersion: socketVersion, + clock: clock, + ), + ]); + await tester.pump(const Duration(milliseconds: 10)); +} diff --git a/test/view/over_the_board/over_the_board_screen_test.dart b/test/view/over_the_board/over_the_board_screen_test.dart index febe1c1614..720ccfba09 100644 --- a/test/view/over_the_board/over_the_board_screen_test.dart +++ b/test/view/over_the_board/over_the_board_screen_test.dart @@ -9,7 +9,7 @@ import 'package:lichess_mobile/src/model/common/time_increment.dart'; import 'package:lichess_mobile/src/model/over_the_board/over_the_board_clock.dart'; import 'package:lichess_mobile/src/model/over_the_board/over_the_board_game_controller.dart'; import 'package:lichess_mobile/src/view/over_the_board/over_the_board_screen.dart'; -import 'package:lichess_mobile/src/widgets/countdown_clock.dart'; +import 'package:lichess_mobile/src/widgets/clock.dart'; import '../../test_helpers.dart'; import '../../test_provider_scope.dart'; @@ -28,11 +28,11 @@ void main() { boardRect.bottomLeft, ); - await playMove(tester, boardRect, 'e2', 'e4'); - await playMove(tester, boardRect, 'f7', 'f6'); - await playMove(tester, boardRect, 'd2', 'd4'); - await playMove(tester, boardRect, 'g7', 'g5'); - await playMove(tester, boardRect, 'd1', 'h5'); + await playMove(tester, 'e2', 'e4'); + await playMove(tester, 'f7', 'f6'); + await playMove(tester, 'd2', 'd4'); + await playMove(tester, 'g7', 'g5'); + await playMove(tester, 'd1', 'h5'); await tester.pumpAndSettle(const Duration(milliseconds: 600)); expect(find.text('Checkmate • White is victorious'), findsOneWidget); @@ -58,13 +58,13 @@ void main() { testWidgets('Game ends when out of time', (tester) async { const time = Duration(seconds: 1); - final boardRect = await initOverTheBoardGame( + await initOverTheBoardGame( tester, TimeIncrement(time.inSeconds, 0), ); - await playMove(tester, boardRect, 'e2', 'e4'); - await playMove(tester, boardRect, 'e7', 'e5'); + await playMove(tester, 'e2', 'e4'); + await playMove(tester, 'e7', 'e5'); // The clock measures system time internally, so we need to actually sleep in order // for the clock to reach 0, instead of using tester.pump() @@ -81,13 +81,13 @@ void main() { testWidgets('Pausing the clock', (tester) async { const time = Duration(seconds: 10); - final boardRect = await initOverTheBoardGame( + await initOverTheBoardGame( tester, TimeIncrement(time.inSeconds, 0), ); - await playMove(tester, boardRect, 'e2', 'e4'); - await playMove(tester, boardRect, 'e7', 'e5'); + await playMove(tester, 'e2', 'e4'); + await playMove(tester, 'e7', 'e5'); await tester.tap(find.byTooltip('Pause')); await tester.pump(); @@ -108,7 +108,7 @@ void main() { expect(activeClock(tester), null); // ... but playing a move resumes the clock - await playMove(tester, boardRect, 'd7', 'd5'); + await playMove(tester, 'd7', 'd5'); expect(activeClock(tester), Side.white); }); @@ -116,13 +116,13 @@ void main() { testWidgets('Go back and Forward', (tester) async { const time = Duration(seconds: 10); - final boardRect = await initOverTheBoardGame( + await initOverTheBoardGame( tester, TimeIncrement(time.inSeconds, 0), ); - await playMove(tester, boardRect, 'e2', 'e4'); - await playMove(tester, boardRect, 'e7', 'e5'); + await playMove(tester, 'e2', 'e4'); + await playMove(tester, 'e7', 'e5'); await tester.tap(find.byTooltip('Previous')); await tester.pumpAndSettle(); @@ -148,7 +148,7 @@ void main() { expect(activeClock(tester), Side.white); - await playMove(tester, boardRect, 'e2', 'e4'); + await playMove(tester, 'e2', 'e4'); expect(find.byKey(const ValueKey('e4-whitepawn')), findsOneWidget); expect(activeClock(tester), Side.black); @@ -166,7 +166,7 @@ void main() { testWidgets('Clock logic', (tester) async { const time = Duration(minutes: 5); - final boardRect = await initOverTheBoardGame( + await initOverTheBoardGame( tester, TimeIncrement(time.inSeconds, 3), ); @@ -176,7 +176,7 @@ void main() { expect(findWhiteClock(tester).timeLeft, time); expect(findBlackClock(tester).timeLeft, time); - await playMove(tester, boardRect, 'e2', 'e4'); + await playMove(tester, 'e2', 'e4'); const moveTime = Duration(milliseconds: 500); await tester.pumpAndSettle(moveTime); @@ -186,7 +186,7 @@ void main() { expect(findWhiteClock(tester).timeLeft, time); expect(findBlackClock(tester).timeLeft, lessThan(time)); - await playMove(tester, boardRect, 'e7', 'e5'); + await playMove(tester, 'e7', 'e5'); await tester.pumpAndSettle(); expect(activeClock(tester), Side.white); diff --git a/test/view/puzzle/puzzle_screen_test.dart b/test/view/puzzle/puzzle_screen_test.dart index 41497a07ab..8845f25da3 100644 --- a/test/view/puzzle/puzzle_screen_test.dart +++ b/test/view/puzzle/puzzle_screen_test.dart @@ -209,9 +209,7 @@ void main() { expect(find.byKey(const Key('g4-blackrook')), findsOneWidget); expect(find.byKey(const Key('h8-whitequeen')), findsOneWidget); - final boardRect = tester.getRect(find.byType(Chessboard)); - - await playMove(tester, boardRect, 'g4', 'h4', orientation: orientation); + await playMove(tester, 'g4', 'h4', orientation: orientation); expect(find.byKey(const Key('h4-blackrook')), findsOneWidget); expect(find.text('Best move!'), findsOneWidget); @@ -222,7 +220,7 @@ void main() { expect(find.byKey(const Key('h4-whitequeen')), findsOneWidget); - await playMove(tester, boardRect, 'b4', 'h4', orientation: orientation); + await playMove(tester, 'b4', 'h4', orientation: orientation); expect(find.byKey(const Key('h4-blackrook')), findsOneWidget); expect(find.text('Success!'), findsOneWidget); @@ -313,9 +311,7 @@ void main() { expect(find.byKey(const Key('g4-blackrook')), findsOneWidget); - final boardRect = tester.getRect(find.byType(Chessboard)); - - await playMove(tester, boardRect, 'g4', 'f4', orientation: orientation); + await playMove(tester, 'g4', 'f4', orientation: orientation); expect( find.text("That's not the move!"), @@ -329,7 +325,7 @@ void main() { // can still play the puzzle expect(find.byKey(const Key('g4-blackrook')), findsOneWidget); - await playMove(tester, boardRect, 'g4', 'h4', orientation: orientation); + await playMove(tester, 'g4', 'h4', orientation: orientation); expect(find.byKey(const Key('h4-blackrook')), findsOneWidget); expect(find.text('Best move!'), findsOneWidget); @@ -338,7 +334,7 @@ void main() { await tester.pump(const Duration(milliseconds: 500)); await tester.pumpAndSettle(); - await playMove(tester, boardRect, 'b4', 'h4', orientation: orientation); + await playMove(tester, 'b4', 'h4', orientation: orientation); expect(find.byKey(const Key('h4-blackrook')), findsOneWidget); expect( diff --git a/test/view/puzzle/storm_screen_test.dart b/test/view/puzzle/storm_screen_test.dart index ac581af0d4..7f6e38996a 100644 --- a/test/view/puzzle/storm_screen_test.dart +++ b/test/view/puzzle/storm_screen_test.dart @@ -96,11 +96,8 @@ void main() { expect(find.byKey(const Key('g8-blackking')), findsOneWidget); - final boardRect = tester.getRect(find.byType(Chessboard)); - await playMove( tester, - boardRect, 'h5', 'h7', orientation: Side.white, @@ -113,7 +110,6 @@ void main() { await playMove( tester, - boardRect, 'e3', 'g1', orientation: Side.white, @@ -143,11 +139,8 @@ void main() { // wait for first move to be played await tester.pump(const Duration(seconds: 1)); - final boardRect = tester.getRect(find.byType(Chessboard)); - await playMove( tester, - boardRect, 'h5', 'h7', orientation: Side.white, @@ -156,7 +149,6 @@ void main() { await tester.pump(const Duration(milliseconds: 500)); await playMove( tester, - boardRect, 'e3', 'g1', orientation: Side.white, @@ -186,9 +178,8 @@ void main() { await tester.pumpWidget(app); await tester.pump(const Duration(seconds: 1)); - final boardRect = tester.getRect(find.byType(Chessboard)); - await playMove(tester, boardRect, 'h5', 'h6'); + await playMove(tester, 'h5', 'h6'); await tester.pump(const Duration(milliseconds: 500)); expect(find.byKey(const Key('h6-blackking')), findsOneWidget); diff --git a/test/view/study/study_screen_test.dart b/test/view/study/study_screen_test.dart index dd80b3a92c..5073bfd240 100644 --- a/test/view/study/study_screen_test.dart +++ b/test/view/study/study_screen_test.dart @@ -1,4 +1,3 @@ -import 'package:chessground/chessground.dart'; import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/widgets.dart'; @@ -225,13 +224,12 @@ void main() { // Wait for study to load await tester.pumpAndSettle(); - final boardRect = tester.getRect(find.byType(Chessboard)); - await playMove(tester, boardRect, 'e2', 'e4', orientation: Side.black); + await playMove(tester, 'e2', 'e4', orientation: Side.black); expect(find.byKey(const Key('e2-whitepawn')), findsNothing); expect(find.byKey(const Key('e4-whitepawn')), findsOneWidget); - await playMove(tester, boardRect, 'e7', 'e5', orientation: Side.black); + await playMove(tester, 'e7', 'e5', orientation: Side.black); expect(find.byKey(const Key('e5-blackpawn')), findsOneWidget); expect(find.byKey(const Key('e7-blackpawn')), findsNothing); @@ -239,5 +237,232 @@ void main() { expect(find.text('1. e4'), findsOneWidget); expect(find.text('e5'), findsOneWidget); }); + + testWidgets('Interactive study', (WidgetTester tester) async { + final mockRepository = MockStudyRepository(); + when(() => mockRepository.getStudy(id: testId)).thenAnswer( + (_) async => ( + makeStudy( + chapter: makeChapter( + id: const StudyChapterId('1'), + orientation: Side.white, + gamebook: true, + ), + ), + ''' +[Event "Improve Your Chess Calculation: Candidates| Ex 1: Hard"] +[Site "https://lichess.org/study/xgZOEizT/OfF4eLmN"] +[Result "*"] +[Variant "Standard"] +[ECO "?"] +[Opening "?"] +[Annotator "https://lichess.org/@/RushConnectedPawns"] +[FEN "r1b2rk1/3pbppp/p3p3/1p6/2qBPP2/P1N2R2/1PPQ2PP/R6K w - - 0 1"] +[SetUp "1"] +[UTCDate "2024.10.23"] +[UTCTime "02:04:11"] +[ChapterMode "gamebook"] + +{ We begin our lecture with an 'easy but not easy' example. White to play and win. } +1. Nd5!! { Brilliant! You noticed that the queen on c4 was kinda smothered. } (1. Ne2? { Not much to say after ...Qc7. }) 1... exd5 2. Rc3 Qa4 3. Rg3! { A fork, threatening Rg7 & b3. } { [%csl Gg7][%cal Gg3g7,Gd4g7,Gb2b3] } (3. Rxc8?? { Uh-oh! After Rc8, b3, there is the counter-sac Rxc2, which is winning for black!! } 3... Raxc8 4. b3 Rxc2!! 5. Qxc2 Qxd4 \$19) 3... g6 4. b3 \$18 { ...and the queen is trapped. GGs. If this was too hard for you, don't worry, there will be easier examples. } * + ''' + ), + ); + + final app = await makeTestProviderScopeApp( + tester, + home: const StudyScreen(id: testId), + overrides: [ + studyRepositoryProvider.overrideWith( + (ref) => mockRepository, + ), + ], + ); + await tester.pumpWidget(app); + // Wait for study to load + await tester.pumpAndSettle(); + + const introText = + "We begin our lecture with an 'easy but not easy' example. White to play and win."; + + expect(find.text(introText), findsOneWidget); + + expect( + find.text( + 'Brilliant! You noticed that the queen on c4 was kinda smothered.', + ), + findsNothing, + ); + + // Play a wrong move + await playMove(tester, 'c3', 'a2'); + expect(find.text("That's not the move!"), findsOneWidget); + expect(find.text(introText), findsNothing); + + // Wrong move will be taken back automatically after a short delay + await tester.pump(const Duration(seconds: 1)); + expect(find.text("That's not move!"), findsNothing); + expect(find.text(introText), findsOneWidget); + + // Play another wrong move, but this one has an explicit comment + await playMove(tester, 'c3', 'e2'); + + // If there's an explicit comment, the move is not taken back automatically + // Verify this by waiting the same duration as above + await tester.pump(const Duration(seconds: 1)); + + expect(find.text('Not much to say after ...Qc7.'), findsOneWidget); + expect(find.text(introText), findsNothing); + + await tester.tap(find.byTooltip('Retry')); + await tester.pump(); // Wait for move to be taken back + + expect(find.text(introText), findsOneWidget); + + // Play the correct move + await playMove(tester, 'c3', 'd5'); + + expect( + find.text( + 'Brilliant! You noticed that the queen on c4 was kinda smothered.', + ), + findsOneWidget, + ); + + // The move has an explicit feedback comment, so opponent move should not be played automatically + await tester.pump(const Duration(seconds: 1)); + + expect( + find.text( + 'Brilliant! You noticed that the queen on c4 was kinda smothered.', + ), + findsOneWidget, + ); + + await tester.tap(find.byTooltip('Next')); + await tester.pump(); // Wait for opponent move to be played + + expect( + find.text('What would you play in this position?'), + findsOneWidget, + ); + + await playMove(tester, 'f3', 'c3'); + expect(find.text('Good move'), findsOneWidget); + + // No explicit feedback, so opponent move should be played automatically after delay + await tester.pump(const Duration(seconds: 1)); + + expect( + find.text('What would you play in this position?'), + findsOneWidget, + ); + + await playMove(tester, 'c3', 'g3'); + expect(find.text('A fork, threatening Rg7 & b3.'), findsOneWidget); + + await tester.tap(find.byTooltip('Next')); + await tester.pump(); // Wait for opponent move to be played + + expect( + find.text('What would you play in this position?'), + findsOneWidget, + ); + + await playMove(tester, 'b2', 'b3'); + + expect( + find.text( + "...and the queen is trapped. GGs. If this was too hard for you, don't worry, there will be easier examples.", + ), + findsOneWidget, + ); + + expect(find.byTooltip('Play again'), findsOneWidget); + expect(find.byTooltip('Next chapter'), findsOneWidget); + expect(find.byTooltip('Analysis board'), findsOneWidget); + }); + + testWidgets('Interactive study hints and deviation comments', + (WidgetTester tester) async { + final mockRepository = MockStudyRepository(); + when(() => mockRepository.getStudy(id: testId)).thenAnswer( + (_) async => ( + makeStudy( + chapter: makeChapter( + id: const StudyChapterId('1'), + orientation: Side.white, + gamebook: true, + ), + hints: [ + 'Hint 1', + null, + null, + null, + ].lock, + deviationComments: [ + null, + 'Shown if any move other than d4 is played', + null, + null, + ].lock, + ), + '1. e4 (1. d4 {Shown if d4 is played}) e5 2. Nf3' + ), + ); + + final app = await makeTestProviderScopeApp( + tester, + home: const StudyScreen(id: testId), + overrides: [ + studyRepositoryProvider.overrideWith( + (ref) => mockRepository, + ), + ], + ); + await tester.pumpWidget(app); + // Wait for study to load + await tester.pumpAndSettle(); + + expect(find.text('Get a hint'), findsOneWidget); + expect(find.text('Hint 1'), findsNothing); + + await tester.tap(find.text('Get a hint')); + await tester.pump(); // Wait for hint to be shown + expect(find.text('Hint 1'), findsOneWidget); + expect(find.text('Get a hint'), findsNothing); + + await playMove(tester, 'e2', 'e3'); + expect( + find.text('Shown if any move other than d4 is played'), + findsOneWidget, + ); + await tester.tap(find.byTooltip('Retry')); + await tester.pump(); // Wait for move to be taken back + + await playMove(tester, 'd2', 'd4'); + expect(find.text('Shown if d4 is played'), findsOneWidget); + await tester.tap(find.byTooltip('Retry')); + await tester.pump(); // Wait for move to be taken back + + expect(find.text('View the solution'), findsOneWidget); + await tester.tap(find.byTooltip('View the solution')); + // Wait for correct move and opponent's response to be played + await tester.pump(const Duration(seconds: 1)); + + expect(find.text('Get a hint'), findsNothing); + + // Play a wrong move again - generic feedback should be shown + await playMove(tester, 'a2', 'a3'); + expect(find.text("That's not the move!"), findsOneWidget); + // Wait for wrong move to be taken back + await tester.pump(const Duration(seconds: 1)); + + expect( + find.text('What would you play in this position?'), + findsOneWidget, + ); + expect(find.text("That's not the move!"), findsNothing); + }); }); } diff --git a/test/view/user/perf_stats_screen_test.dart b/test/view/user/perf_stats_screen_test.dart index a2a208f8e4..33013947fd 100644 --- a/test/view/user/perf_stats_screen_test.dart +++ b/test/view/user/perf_stats_screen_test.dart @@ -90,7 +90,7 @@ void main() { ]; // rating - expect(find.text('1500.42'), findsOneWidget); + expect(find.text('1500'), findsOneWidget); for (final val in requiredStatsValues) { expect( diff --git a/test/widgets/clock_test.dart b/test/widgets/clock_test.dart new file mode 100644 index 0000000000..73e20078f6 --- /dev/null +++ b/test/widgets/clock_test.dart @@ -0,0 +1,249 @@ +import 'package:clock/clock.dart' as clock; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:lichess_mobile/src/widgets/clock.dart'; + +void main() { + group('Clock', () { + testWidgets('shows milliseconds when time < 1s and active is false', + (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Clock( + timeLeft: Duration(seconds: 1), + active: true, + ), + ), + ); + + expect(find.text('0:01.0', findRichText: true), findsOneWidget); + + await tester.pumpWidget( + const MaterialApp( + home: Clock( + timeLeft: Duration(milliseconds: 988), + active: false, + ), + ), + duration: const Duration(milliseconds: 1000), + ); + + expect(find.text('0:00.98', findRichText: true), findsOneWidget); + }); + }); + + group('CountdownClockBuilder', () { + Widget clockBuilder(BuildContext context, Duration timeLeft) { + final mins = timeLeft.inMinutes.remainder(60); + final secs = timeLeft.inSeconds.remainder(60).toString().padLeft(2, '0'); + final tenths = timeLeft.inMilliseconds.remainder(1000) ~/ 100; + return Text('$mins:$secs.$tenths'); + } + + testWidgets('does not tick when not active', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: CountdownClockBuilder( + timeLeft: const Duration(seconds: 10), + active: false, + builder: clockBuilder, + ), + ), + ); + + expect(find.text('0:10.0'), findsOneWidget); + + await tester.pump(const Duration(seconds: 2)); + expect(find.text('0:10.0'), findsOneWidget); + }); + + testWidgets('ticks when active', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: CountdownClockBuilder( + timeLeft: const Duration(seconds: 10), + active: true, + builder: clockBuilder, + ), + ), + ); + + expect(find.text('0:10.0'), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:09.9'), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:09.8'), findsOneWidget); + await tester.pump(const Duration(seconds: 10)); + expect(find.text('0:00.0'), findsOneWidget); + }); + + testWidgets('update time by changing widget configuration', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: CountdownClockBuilder( + timeLeft: const Duration(seconds: 10), + clockUpdatedAt: clock.clock.now(), + active: true, + builder: clockBuilder, + ), + ), + ); + + expect(find.text('0:10.0'), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:09.9'), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:09.8'), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:09.7'), findsOneWidget); + + await tester.pumpWidget( + MaterialApp( + home: CountdownClockBuilder( + timeLeft: const Duration(seconds: 11), + clockUpdatedAt: clock.clock.now(), + active: true, + builder: clockBuilder, + ), + ), + ); + expect(find.text('0:11.0'), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:10.9'), findsOneWidget); + await tester.pump(const Duration(seconds: 11)); + expect(find.text('0:00.0'), findsOneWidget); + }); + + testWidgets('do not update if clockUpdatedAt is same', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: CountdownClockBuilder( + timeLeft: const Duration(seconds: 10), + active: true, + builder: clockBuilder, + ), + ), + ); + + expect(find.text('0:10.0'), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:09.9'), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:09.8'), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:09.7'), findsOneWidget); + + await tester.pumpWidget( + MaterialApp( + home: CountdownClockBuilder( + timeLeft: const Duration(seconds: 11), + active: true, + builder: clockBuilder, + ), + ), + ); + + expect(find.text('0:09.7'), findsOneWidget); + await tester.pump(const Duration(seconds: 10)); + expect(find.text('0:00.0'), findsOneWidget); + }); + + testWidgets('stops when active become false', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: CountdownClockBuilder( + timeLeft: const Duration(seconds: 10), + active: true, + builder: clockBuilder, + ), + ), + ); + + expect(find.text('0:10.0', findRichText: true), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:09.9', findRichText: true), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:09.8', findRichText: true), findsOneWidget); + + // clock is rebuilt with same time but inactive: + // the time is kept and the clock stops counting the elapsed time + await tester.pumpWidget( + MaterialApp( + home: CountdownClockBuilder( + timeLeft: const Duration(seconds: 10), + active: false, + builder: clockBuilder, + ), + ), + duration: const Duration(milliseconds: 100), + ); + expect(find.text('0:09.7', findRichText: true), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:09.7', findRichText: true), findsOneWidget); + }); + + testWidgets('starts with a delay if set', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: CountdownClockBuilder( + timeLeft: const Duration(seconds: 10), + active: true, + delay: const Duration(milliseconds: 250), + builder: clockBuilder, + ), + ), + ); + expect(find.text('0:10.0', findRichText: true), findsOneWidget); + await tester.pump(const Duration(milliseconds: 250)); + expect(find.text('0:10.0', findRichText: true), findsOneWidget); + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:09.9', findRichText: true), findsOneWidget); + }); + + testWidgets('compensates for UI lag', (WidgetTester tester) async { + final now = clock.clock.now(); + await tester.pump(const Duration(milliseconds: 100)); + + await tester.pumpWidget( + MaterialApp( + home: CountdownClockBuilder( + timeLeft: const Duration(seconds: 10), + active: true, + delay: const Duration(milliseconds: 200), + clockUpdatedAt: now, + builder: clockBuilder, + ), + ), + ); + expect(find.text('0:10.0', findRichText: true), findsOneWidget); + + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:10.0', findRichText: true), findsOneWidget); + + // delay was 200ms but UI lagged 100ms so with the compensation the clock has started already + await tester.pump(const Duration(milliseconds: 100)); + expect(find.text('0:09.9', findRichText: true), findsOneWidget); + }); + + testWidgets('UI lag negative start delay', (WidgetTester tester) async { + final now = clock.clock.now(); + await tester.pump(const Duration(milliseconds: 200)); + + await tester.pumpWidget( + MaterialApp( + home: CountdownClockBuilder( + timeLeft: const Duration(seconds: 10), + active: true, + delay: const Duration(milliseconds: 100), + clockUpdatedAt: now, + builder: clockBuilder, + ), + ), + ); + // delay was 100ms but UI lagged 200ms so the clock time is already 100ms ahead + expect(find.text('0:09.9', findRichText: true), findsOneWidget); + }); + }); +} From 66d561a6fb81a8622d218db63e5a19bf04decdb2 Mon Sep 17 00:00:00 2001 From: Jimima Date: Wed, 20 Nov 2024 17:19:02 +0000 Subject: [PATCH 718/979] Merge fix --- lib/src/model/settings/board_preferences.dart | 16 ++++------ lib/src/view/game/game_player.dart | 29 ++----------------- 2 files changed, 8 insertions(+), 37 deletions(-) diff --git a/lib/src/model/settings/board_preferences.dart b/lib/src/model/settings/board_preferences.dart index 4d4d66eadd..4a3f50a646 100644 --- a/lib/src/model/settings/board_preferences.dart +++ b/lib/src/model/settings/board_preferences.dart @@ -81,19 +81,16 @@ class BoardPreferences extends _$BoardPreferences ), ); } - + Future setDragTargetKind(DragTargetKind dragTargetKind) { return save(state.copyWith(dragTargetKind: dragTargetKind)); } - - Future setMaterialDifferenceFormat( - MaterialDifferenceFormat materialDifferenceFormat) { - return save(state.copyWith(materialDifferenceFormat: materialDifferenceFormat)); - } - Future setClockPosition(ClockPosition clockPosition) { + Future setMaterialDifferenceFormat( + MaterialDifferenceFormat materialDifferenceFormat, + ) { return save( - state.copyWith(clockPosition: clockPosition), + state.copyWith(materialDifferenceFormat: materialDifferenceFormat), ); } @@ -129,9 +126,6 @@ class BoardPrefs with _$BoardPrefs implements Serializable { required bool pieceAnimation, required MaterialDifferenceFormat materialDifferenceFormat, required ClockPosition clockPosition, - required bool showMaterialDifference, - required ClockPosition clockPosition, - @JsonKey( defaultValue: PieceShiftMethod.either, unknownEnumValue: PieceShiftMethod.either, diff --git a/lib/src/view/game/game_player.dart b/lib/src/view/game/game_player.dart index 9691cfae76..00bd5bc588 100644 --- a/lib/src/view/game/game_player.dart +++ b/lib/src/view/game/game_player.dart @@ -153,33 +153,10 @@ class GamePlayer extends StatelessWidget { else if (materialDiff != null) MaterialDifferenceDisplay( materialDiff: materialDiff!, - materialDifferenceFormat: materialDifferenceFormat!,), - Row( - mainAxisAlignment: clockPosition == ClockPosition.right - ? MainAxisAlignment.start - : MainAxisAlignment.end, - children: [ - for (final role in Role.values) - for (int i = 0; i < materialDiff!.pieces[role]!; i++) - Icon( - _iconByRole[role], - size: 13, - color: Colors.grey, - ), - const SizedBox(width: 3), - Text( - style: const TextStyle( - fontSize: 13, - color: Colors.grey, - ), - materialDiff != null && materialDiff!.score > 0 - ? '+${materialDiff!.score}' - : '', - ), - ], + materialDifferenceFormat: materialDifferenceFormat!, ), - // to avoid shifts use an empty text widget - const Text('', style: TextStyle(fontSize: 13)), + // to avoid shifts use an empty text widget + const Text('', style: TextStyle(fontSize: 13)), ], ); From c3f98e2cac79affcc8fc05cd3ee56377b22642fd Mon Sep 17 00:00:00 2001 From: Jimima Date: Wed, 20 Nov 2024 17:38:05 +0000 Subject: [PATCH 719/979] Refactor --- lib/src/model/game/material_diff.dart | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/lib/src/model/game/material_diff.dart b/lib/src/model/game/material_diff.dart index 30cd29cf33..52623a11c0 100644 --- a/lib/src/model/game/material_diff.dart +++ b/lib/src/model/game/material_diff.dart @@ -47,12 +47,9 @@ class MaterialDiff with _$MaterialDiff { IMap startingCount, IMap subtractCount, ) { - IMap capturedPieces = IMap(); - startingCount.forEach((role, count) { - capturedPieces = - capturedPieces.add(role, count - (subtractCount.get(role) ?? 0)); - }); - return capturedPieces; + return startingCount.map( + (role, count) => MapEntry(role, count - (subtractCount.get(role) ?? 0)), + ); } final IMap blackCapturedPieces = From b6d4599e29313deff72da5517f70153a21fe721e Mon Sep 17 00:00:00 2001 From: Jimima Date: Wed, 20 Nov 2024 18:12:59 +0000 Subject: [PATCH 720/979] Modified null handling --- lib/src/view/game/game_player.dart | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/src/view/game/game_player.dart b/lib/src/view/game/game_player.dart index 00bd5bc588..b46198dea2 100644 --- a/lib/src/view/game/game_player.dart +++ b/lib/src/view/game/game_player.dart @@ -153,7 +153,7 @@ class GamePlayer extends StatelessWidget { else if (materialDiff != null) MaterialDifferenceDisplay( materialDiff: materialDiff!, - materialDifferenceFormat: materialDifferenceFormat!, + materialDifferenceFormat: materialDifferenceFormat, ), // to avoid shifts use an empty text widget const Text('', style: TextStyle(fontSize: 13)), @@ -336,7 +336,7 @@ class MaterialDifferenceDisplay extends StatelessWidget { }); final MaterialDiffSide materialDiff; - final MaterialDifferenceFormat materialDifferenceFormat; + final MaterialDifferenceFormat? materialDifferenceFormat; @override Widget build(BuildContext context) { @@ -345,9 +345,8 @@ class MaterialDifferenceDisplay extends StatelessWidget { ? materialDiff.capturedPieces : materialDiff.pieces); - return !materialDifferenceFormat.visible - ? const SizedBox.shrink() - : Row( + return materialDifferenceFormat?.visible ?? true + ? Row( children: [ for (final role in Role.values) for (int i = 0; i < piecesToRender[role]!; i++) @@ -365,7 +364,8 @@ class MaterialDifferenceDisplay extends StatelessWidget { materialDiff.score > 0 ? '+${materialDiff.score}' : '', ), ], - ); + ) + : const SizedBox.shrink(); } } From 18297cebc2d7e0ccceee295536037ec9a2377134 Mon Sep 17 00:00:00 2001 From: Jimima Date: Wed, 20 Nov 2024 19:57:31 +0000 Subject: [PATCH 721/979] Removed duplicates from board settings page --- .../view/settings/board_settings_screen.dart | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/lib/src/view/settings/board_settings_screen.dart b/lib/src/view/settings/board_settings_screen.dart index d126acc24b..f57d06ae24 100644 --- a/lib/src/view/settings/board_settings_screen.dart +++ b/lib/src/view/settings/board_settings_screen.dart @@ -257,26 +257,6 @@ class _Body extends ConsumerWidget { .toggleCoordinates(); }, ), - SwitchSettingTile( - title: Text(context.l10n.mobilePrefMagnifyDraggedPiece), - value: boardPrefs.magnifyDraggedPiece, - onChanged: (value) { - ref - .read(boardPreferencesProvider.notifier) - .toggleMagnifyDraggedPiece(); - }, - ), - SwitchSettingTile( - title: Text( - context.l10n.preferencesPieceAnimation, - ), - value: boardPrefs.pieceAnimation, - onChanged: (value) { - ref - .read(boardPreferencesProvider.notifier) - .togglePieceAnimation(); - }, - ), SettingsListTile( settingsLabel: const Text('Material'), //TODO: l10n settingsValue: boardPrefs.materialDifferenceFormat From 7aebb8c22f48d038030550afb9f7dcf31960e0b8 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Wed, 20 Nov 2024 20:58:17 +0100 Subject: [PATCH 722/979] rename variables and fix game clock when game ends --- .../view/broadcast/broadcast_game_screen.dart | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_game_screen.dart b/lib/src/view/broadcast/broadcast_game_screen.dart index da1a77cbfa..182147412d 100644 --- a/lib/src/view/broadcast/broadcast_game_screen.dart +++ b/lib/src/view/broadcast/broadcast_game_screen.dart @@ -294,8 +294,8 @@ class _BroadcastBoardWithHeaders extends ConsumerWidget { game: game, side: pov.opposite, boardSide: _PlayerWidgetSide.top, - playingSide: playingSide, - playClock: currentPath == broadcastLivePath, + sideToPlay: playingSide, + cursorOnLiveMove: currentPath == broadcastLivePath, ), _BroadcastBoard(roundId, gameId, size), if (game != null) @@ -305,8 +305,8 @@ class _BroadcastBoardWithHeaders extends ConsumerWidget { game: game, side: pov, boardSide: _PlayerWidgetSide.bottom, - playingSide: playingSide, - playClock: currentPath == broadcastLivePath, + sideToPlay: playingSide, + cursorOnLiveMove: currentPath == broadcastLivePath, ), ], ); @@ -437,8 +437,8 @@ class _PlayerWidget extends StatelessWidget { required this.game, required this.side, required this.boardSide, - required this.playingSide, - required this.playClock, + required this.sideToPlay, + required this.cursorOnLiveMove, }); final BroadcastGame game; @@ -446,8 +446,8 @@ class _PlayerWidget extends StatelessWidget { final Side side; final double width; final _PlayerWidgetSide boardSide; - final Side playingSide; - final bool playClock; + final Side sideToPlay; + final bool cursorOnLiveMove; @override Widget build(BuildContext context) { @@ -571,8 +571,8 @@ class _PlayerWidget extends StatelessWidget { ), if (clock != null) Card( - color: (side == playingSide) - ? playClock + color: (side == sideToPlay) + ? cursorOnLiveMove ? Theme.of(context).colorScheme.tertiaryContainer : Theme.of(context).colorScheme.secondaryContainer : null, @@ -592,12 +592,14 @@ class _PlayerWidget extends StatelessWidget { const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), child: CountdownClockBuilder( timeLeft: clock!, - active: side == playingSide && playClock, + active: side == sideToPlay && + cursorOnLiveMove && + game.status == BroadcastResult.ongoing, builder: (context, timeLeft) => Text( timeLeft.toHoursMinutesSeconds(), style: TextStyle( - color: (side == playingSide) - ? playClock + color: (side == sideToPlay) + ? cursorOnLiveMove ? Theme.of(context) .colorScheme .onTertiaryContainer @@ -609,7 +611,7 @@ class _PlayerWidget extends StatelessWidget { ), ), tickInterval: const Duration(seconds: 1), - clockUpdatedAt: (side == playingSide && playClock) + clockUpdatedAt: (side == sideToPlay && cursorOnLiveMove) ? game.updatedClockAt : null, ), From 518288165487c466ac5d79de3f7f59ce72cd6e0c Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Wed, 20 Nov 2024 21:01:49 +0100 Subject: [PATCH 723/979] rename variable --- .../view/broadcast/broadcast_game_screen.dart | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_game_screen.dart b/lib/src/view/broadcast/broadcast_game_screen.dart index 182147412d..4ade056e3d 100644 --- a/lib/src/view/broadcast/broadcast_game_screen.dart +++ b/lib/src/view/broadcast/broadcast_game_screen.dart @@ -295,7 +295,7 @@ class _BroadcastBoardWithHeaders extends ConsumerWidget { side: pov.opposite, boardSide: _PlayerWidgetSide.top, sideToPlay: playingSide, - cursorOnLiveMove: currentPath == broadcastLivePath, + isCursorOnLiveMove: currentPath == broadcastLivePath, ), _BroadcastBoard(roundId, gameId, size), if (game != null) @@ -306,7 +306,7 @@ class _BroadcastBoardWithHeaders extends ConsumerWidget { side: pov, boardSide: _PlayerWidgetSide.bottom, sideToPlay: playingSide, - cursorOnLiveMove: currentPath == broadcastLivePath, + isCursorOnLiveMove: currentPath == broadcastLivePath, ), ], ); @@ -438,7 +438,7 @@ class _PlayerWidget extends StatelessWidget { required this.side, required this.boardSide, required this.sideToPlay, - required this.cursorOnLiveMove, + required this.isCursorOnLiveMove, }); final BroadcastGame game; @@ -447,7 +447,7 @@ class _PlayerWidget extends StatelessWidget { final double width; final _PlayerWidgetSide boardSide; final Side sideToPlay; - final bool cursorOnLiveMove; + final bool isCursorOnLiveMove; @override Widget build(BuildContext context) { @@ -572,7 +572,7 @@ class _PlayerWidget extends StatelessWidget { if (clock != null) Card( color: (side == sideToPlay) - ? cursorOnLiveMove + ? isCursorOnLiveMove ? Theme.of(context).colorScheme.tertiaryContainer : Theme.of(context).colorScheme.secondaryContainer : null, @@ -593,13 +593,13 @@ class _PlayerWidget extends StatelessWidget { child: CountdownClockBuilder( timeLeft: clock!, active: side == sideToPlay && - cursorOnLiveMove && + isCursorOnLiveMove && game.status == BroadcastResult.ongoing, builder: (context, timeLeft) => Text( timeLeft.toHoursMinutesSeconds(), style: TextStyle( color: (side == sideToPlay) - ? cursorOnLiveMove + ? isCursorOnLiveMove ? Theme.of(context) .colorScheme .onTertiaryContainer @@ -611,7 +611,7 @@ class _PlayerWidget extends StatelessWidget { ), ), tickInterval: const Duration(seconds: 1), - clockUpdatedAt: (side == sideToPlay && cursorOnLiveMove) + clockUpdatedAt: (side == sideToPlay && isCursorOnLiveMove) ? game.updatedClockAt : null, ), From 9a84086183886f7ba3291f879fb7661501a66b9b Mon Sep 17 00:00:00 2001 From: Jimima Date: Wed, 20 Nov 2024 20:22:27 +0000 Subject: [PATCH 724/979] Removed from game settings (also duplicates) --- lib/src/view/game/game_settings.dart | 42 ---------------------------- 1 file changed, 42 deletions(-) diff --git a/lib/src/view/game/game_settings.dart b/lib/src/view/game/game_settings.dart index 42d6bf0290..4283166cc4 100644 --- a/lib/src/view/game/game_settings.dart +++ b/lib/src/view/game/game_settings.dart @@ -69,30 +69,6 @@ class GameSettings extends ConsumerWidget { }, orElse: () => [], ), - SwitchSettingTile( - // TODO: Add l10n - title: const Text('Shape drawing'), - subtitle: const Text( - 'Draw shapes using two fingers.', - maxLines: 5, - textAlign: TextAlign.justify, - ), - value: boardPrefs.enableShapeDrawings, - onChanged: (value) { - ref - .read(boardPreferencesProvider.notifier) - .toggleEnableShapeDrawings(); - }, - ), - SwitchSettingTile( - title: Text( - context.l10n.preferencesPieceAnimation, - ), - value: boardPrefs.pieceAnimation, - onChanged: (value) { - ref.read(boardPreferencesProvider.notifier).togglePieceAnimation(); - }, - ), PlatformListTile( // TODO translate title: const Text('Board settings'), @@ -122,24 +98,6 @@ class GameSettings extends ConsumerWidget { ref.read(gamePreferencesProvider.notifier).toggleBlindfoldMode(); }, ), - SettingsListTile( - settingsLabel: const Text('Material'), //TODO: l10n - settingsValue: boardPrefs.materialDifferenceFormat - .l10n(AppLocalizations.of(context)), - onTap: () { - showChoicePicker( - context, - choices: MaterialDifferenceFormat.values, - selectedItem: boardPrefs.materialDifferenceFormat, - labelBuilder: (t) => Text(t.l10n(AppLocalizations.of(context))), - onSelectedItemChanged: (MaterialDifferenceFormat? value) => ref - .read(boardPreferencesProvider.notifier) - .setMaterialDifferenceFormat( - value ?? MaterialDifferenceFormat.materialDifference, - ), - ); - }, - ), ], ); } From c6293bdd67dbe7efe59d17ecdde9ba78946bc548 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 20 Nov 2024 14:53:03 +0100 Subject: [PATCH 725/979] Wip on analysis tabs --- .../model/analysis/analysis_controller.dart | 3 + lib/src/view/analysis/analysis_layout.dart | 251 ++++++++++++++ lib/src/view/analysis/analysis_screen.dart | 317 +++++------------- lib/src/view/analysis/tree_view.dart | 18 +- lib/src/view/engine/engine_gauge.dart | 4 +- 5 files changed, 340 insertions(+), 253 deletions(-) create mode 100644 lib/src/view/analysis/analysis_layout.dart diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index 830225ab88..3f937ca4c7 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -52,6 +52,9 @@ class AnalysisOptions with _$AnalysisOptions { ({PlayerAnalysis white, PlayerAnalysis black})? serverAnalysis, }) = _AnalysisOptions; + bool get canShowGameSummary => + serverAnalysis != null || id != standaloneAnalysisId; + /// Whether the analysis is for a lichess game. bool get isLichessGameAnalysis => gameAnyId != null; diff --git a/lib/src/view/analysis/analysis_layout.dart b/lib/src/view/analysis/analysis_layout.dart new file mode 100644 index 0000000000..fbd27d9898 --- /dev/null +++ b/lib/src/view/analysis/analysis_layout.dart @@ -0,0 +1,251 @@ +import 'package:flutter/material.dart'; +import 'package:lichess_mobile/src/constants.dart'; +import 'package:lichess_mobile/src/utils/screen.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/platform.dart'; + +typedef BoardBuilder = Widget Function( + BuildContext context, + double boardSize, + BorderRadiusGeometry? borderRadius, +); + +typedef EngineGaugeBuilder = Widget Function( + BuildContext context, + Orientation orientation, +); + +class AnalysisTab { + const AnalysisTab({ + required this.title, + required this.icon, + }); + + final String title; + final IconData icon; +} + +/// Indicator for the analysis tab, typically shown in the app bar. +class AppBarAnalysisTabIndicator extends StatefulWidget { + const AppBarAnalysisTabIndicator({ + required this.tabs, + required this.controller, + super.key, + }); + + final TabController controller; + + /// Typically a list of two or more [AnalysisTab] widgets. + /// + /// The length of this list must match the [controller]'s [TabController.length] + /// and the length of the [AnalysisLayout.children] list. + final List tabs; + + @override + State createState() => + _AppBarAnalysisTabIndicatorState(); +} + +class _AppBarAnalysisTabIndicatorState + extends State { + @override + void didChangeDependencies() { + super.didChangeDependencies(); + widget.controller.addListener(_listener); + } + + @override + void dispose() { + widget.controller.removeListener(_listener); + super.dispose(); + } + + void _listener() { + setState(() {}); + } + + @override + Widget build(BuildContext context) { + return AppBarIconButton( + icon: Icon(widget.tabs[widget.controller.index].icon), + semanticsLabel: widget.tabs[widget.controller.index].title, + onPressed: () { + showAdaptiveActionSheet( + context: context, + actions: widget.tabs.map((tab) { + return BottomSheetAction( + leading: Icon(tab.icon), + makeLabel: (_) => Text(tab.title), + onPressed: (_) { + widget.controller.animateTo(widget.tabs.indexOf(tab)); + Navigator.of(context).pop(); + }, + ); + }).toList(), + ); + }, + ); + } +} + +/// Layout for the analysis and similar screens (study, broadcast, etc.). +class AnalysisLayout extends StatelessWidget { + const AnalysisLayout({ + required this.tabController, + required this.boardBuilder, + required this.children, + this.engineGaugeBuilder, + this.engineLines, + this.bottomBar, + super.key, + }); + + final TabController tabController; + + /// The builder for the board widget. + final BoardBuilder boardBuilder; + + /// The children of the tab bar view. + final List children; + + final EngineGaugeBuilder? engineGaugeBuilder; + final Widget? engineLines; + final Widget? bottomBar; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Expanded( + child: SafeArea( + bottom: false, + child: LayoutBuilder( + builder: (context, constraints) { + final aspectRatio = constraints.biggest.aspectRatio; + final defaultBoardSize = constraints.biggest.shortestSide; + final isTablet = isTabletOrLarger(context); + final remainingHeight = + constraints.maxHeight - defaultBoardSize; + final isSmallScreen = + remainingHeight < kSmallRemainingHeightLeftBoardThreshold; + final boardSize = isTablet || isSmallScreen + ? defaultBoardSize - kTabletBoardTableSidePadding * 2 + : defaultBoardSize; + + const tabletBoardRadius = + BorderRadius.all(Radius.circular(4.0)); + + // If the aspect ratio is greater than 1, we are in landscape mode. + if (aspectRatio > 1) { + return Row( + mainAxisSize: MainAxisSize.max, + children: [ + Padding( + padding: const EdgeInsets.only( + left: kTabletBoardTableSidePadding, + top: kTabletBoardTableSidePadding, + bottom: kTabletBoardTableSidePadding, + ), + child: Row( + children: [ + boardBuilder( + context, + boardSize, + isTablet ? tabletBoardRadius : null, + ), + if (engineGaugeBuilder != null) ...[ + const SizedBox(width: 4.0), + engineGaugeBuilder!( + context, + Orientation.landscape, + ), + ], + ], + ), + ), + Flexible( + fit: FlexFit.loose, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + if (engineLines != null) + Padding( + padding: const EdgeInsets.all( + kTabletBoardTableSidePadding, + ), + child: engineLines, + ), + Expanded( + child: PlatformCard( + clipBehavior: Clip.hardEdge, + borderRadius: const BorderRadius.all( + Radius.circular(4.0), + ), + margin: const EdgeInsets.all( + kTabletBoardTableSidePadding, + ), + semanticContainer: false, + child: TabBarView( + controller: tabController, + children: children, + ), + ), + ), + ], + ), + ), + ], + ); + } + // If the aspect ratio is less than 1, we are in portrait mode. + else { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (engineGaugeBuilder != null) + engineGaugeBuilder!( + context, + Orientation.portrait, + ), + if (engineLines != null) engineLines!, + if (isTablet) + Padding( + padding: const EdgeInsets.all( + kTabletBoardTableSidePadding, + ), + child: boardBuilder( + context, + boardSize, + tabletBoardRadius, + ), + ) + else + boardBuilder(context, boardSize, null), + Expanded( + child: Padding( + padding: isTablet + ? const EdgeInsets.symmetric( + horizontal: kTabletBoardTableSidePadding, + ) + : EdgeInsets.zero, + child: TabBarView( + controller: tabController, + children: children, + ), + ), + ), + ], + ); + } + }, + ), + ), + ), + if (bottomBar != null) bottomBar!, + ], + ); + } +} diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index a87c4155fa..d663a391b2 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -7,7 +7,6 @@ import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; import 'package:lichess_mobile/src/model/analysis/server_analysis_service.dart'; @@ -21,11 +20,10 @@ import 'package:lichess_mobile/src/model/game/game_share_service.dart'; import 'package:lichess_mobile/src/network/connectivity.dart'; import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; -import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; -import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/utils/string.dart'; +import 'package:lichess_mobile/src/view/analysis/analysis_layout.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_share_screen.dart'; import 'package:lichess_mobile/src/view/board_editor/board_editor_screen.dart'; import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; @@ -38,7 +36,6 @@ import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:popover/popover.dart'; @@ -121,7 +118,7 @@ class _LoadGame extends ConsumerWidget { } } -class _LoadedAnalysisScreen extends ConsumerWidget { +class _LoadedAnalysisScreen extends ConsumerStatefulWidget { const _LoadedAnalysisScreen({ required this.options, required this.pgn, @@ -134,30 +131,60 @@ class _LoadedAnalysisScreen extends ConsumerWidget { final bool enableDrawingShapes; @override - Widget build(BuildContext context, WidgetRef ref) { - return ConsumerPlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, - ref: ref, - ); + ConsumerState<_LoadedAnalysisScreen> createState() => + _LoadedAnalysisScreenState(); +} + +class _LoadedAnalysisScreenState extends ConsumerState<_LoadedAnalysisScreen> + with SingleTickerProviderStateMixin { + late final TabController _tabController; + late final List tabs; + + @override + void initState() { + super.initState(); + tabs = [ + const AnalysisTab( + title: 'Moves', + icon: LichessIcons.flow_cascade, + ), + if (widget.options.canShowGameSummary) + const AnalysisTab( + title: 'Summary', + icon: Icons.area_chart, + ), + ]; + + _tabController = TabController(vsync: this, length: tabs.length); } - Widget _androidBuilder(BuildContext context, WidgetRef ref) { - final ctrlProvider = analysisControllerProvider(pgn, options); + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final ctrlProvider = analysisControllerProvider(widget.pgn, widget.options); return PlatformScaffold( resizeToAvoidBottomInset: false, appBar: PlatformAppBar( - title: _Title(options: options), + title: _Title(options: widget.options), actions: [ _EngineDepth(ctrlProvider), + AppBarAnalysisTabIndicator( + tabs: tabs, + controller: _tabController, + ), AppBarIconButton( onPressed: () => showAdaptiveBottomSheet( context: context, isScrollControlled: true, showDragHandle: true, isDismissible: true, - builder: (_) => AnalysisSettings(pgn, options), + builder: (_) => AnalysisSettings(widget.pgn, widget.options), ), semanticsLabel: context.l10n.settingsSettings, icon: const Icon(Icons.settings), @@ -165,43 +192,10 @@ class _LoadedAnalysisScreen extends ConsumerWidget { ], ), body: _Body( - pgn: pgn, - options: options, - enableDrawingShapes: enableDrawingShapes, - ), - ); - } - - Widget _iosBuilder(BuildContext context, WidgetRef ref) { - final ctrlProvider = analysisControllerProvider(pgn, options); - - return CupertinoPageScaffold( - resizeToAvoidBottomInset: false, - navigationBar: CupertinoNavigationBar( - padding: Styles.cupertinoAppBarTrailingWidgetPadding, - middle: _Title(options: options), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - _EngineDepth(ctrlProvider), - AppBarIconButton( - onPressed: () => showAdaptiveBottomSheet( - context: context, - isScrollControlled: true, - showDragHandle: true, - isDismissible: true, - builder: (_) => AnalysisSettings(pgn, options), - ), - semanticsLabel: context.l10n.settingsSettings, - icon: const Icon(Icons.settings), - ), - ], - ), - ), - child: _Body( - pgn: pgn, - options: options, - enableDrawingShapes: enableDrawingShapes, + controller: _tabController, + pgn: widget.pgn, + options: widget.options, + enableDrawingShapes: widget.enableDrawingShapes, ), ); } @@ -230,11 +224,13 @@ class _Title extends StatelessWidget { class _Body extends ConsumerWidget { const _Body({ + required this.controller, required this.pgn, required this.options, required this.enableDrawingShapes, }); + final TabController controller; final String pgn; final AnalysisOptions options; final bool enableDrawingShapes; @@ -255,156 +251,37 @@ class _Body extends ConsumerWidget { final hasEval = ref.watch(ctrlProvider.select((value) => value.hasAvailableEval)); - final displayMode = - ref.watch(ctrlProvider.select((value) => value.displayMode)); - final currentNode = ref.watch( ctrlProvider.select((value) => value.currentNode), ); - return Column( + return AnalysisLayout( + tabController: controller, + boardBuilder: (context, boardSize, borderRadius) => AnalysisBoard( + pgn, + options, + boardSize, + borderRadius: borderRadius, + enableDrawingShapes: enableDrawingShapes, + ), + engineGaugeBuilder: hasEval && showEvaluationGauge + ? (context, orientation) { + return orientation == Orientation.portrait + ? _EngineGaugeHorizontal(ctrlProvider) + : _EngineGaugeVertical(ctrlProvider); + } + : null, + engineLines: isEngineAvailable + ? EngineLines( + onTapMove: ref.read(ctrlProvider.notifier).onUserMove, + clientEval: currentNode.eval, + isGameOver: currentNode.position.isGameOver, + ) + : null, + bottomBar: _BottomBar(pgn: pgn, options: options), children: [ - Expanded( - child: SafeArea( - bottom: false, - child: LayoutBuilder( - builder: (context, constraints) { - final aspectRatio = constraints.biggest.aspectRatio; - final defaultBoardSize = constraints.biggest.shortestSide; - final isTablet = isTabletOrLarger(context); - final remainingHeight = - constraints.maxHeight - defaultBoardSize; - final isSmallScreen = - remainingHeight < kSmallRemainingHeightLeftBoardThreshold; - final boardSize = isTablet || isSmallScreen - ? defaultBoardSize - kTabletBoardTableSidePadding * 2 - : defaultBoardSize; - - const tabletBoardRadius = - BorderRadius.all(Radius.circular(4.0)); - - final display = switch (displayMode) { - DisplayMode.summary => ServerAnalysisSummary(pgn, options), - DisplayMode.moves => AnalysisTreeView( - pgn, - options, - aspectRatio > 1 - ? Orientation.landscape - : Orientation.portrait, - ), - }; - - // If the aspect ratio is greater than 1, we are in landscape mode. - if (aspectRatio > 1) { - return Row( - mainAxisSize: MainAxisSize.max, - children: [ - Padding( - padding: const EdgeInsets.only( - left: kTabletBoardTableSidePadding, - top: kTabletBoardTableSidePadding, - bottom: kTabletBoardTableSidePadding, - ), - child: Row( - children: [ - AnalysisBoard( - pgn, - options, - boardSize, - borderRadius: isTablet ? tabletBoardRadius : null, - enableDrawingShapes: enableDrawingShapes, - ), - if (hasEval && showEvaluationGauge) ...[ - const SizedBox(width: 4.0), - _EngineGaugeVertical(ctrlProvider), - ], - ], - ), - ), - Flexible( - fit: FlexFit.loose, - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - if (isEngineAvailable) - Padding( - padding: const EdgeInsets.all( - kTabletBoardTableSidePadding, - ), - child: EngineLines( - onTapMove: ref - .read(ctrlProvider.notifier) - .onUserMove, - clientEval: currentNode.eval, - isGameOver: currentNode.position.isGameOver, - ), - ), - Expanded( - child: PlatformCard( - clipBehavior: Clip.hardEdge, - borderRadius: const BorderRadius.all( - Radius.circular(4.0), - ), - margin: const EdgeInsets.all( - kTabletBoardTableSidePadding, - ), - semanticContainer: false, - child: display, - ), - ), - ], - ), - ), - ], - ); - } - // If the aspect ratio is less than 1, we are in portrait mode. - else { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - _ColumnTopTable(ctrlProvider), - if (isTablet) - Padding( - padding: const EdgeInsets.all( - kTabletBoardTableSidePadding, - ), - child: AnalysisBoard( - pgn, - options, - boardSize, - borderRadius: isTablet ? tabletBoardRadius : null, - enableDrawingShapes: enableDrawingShapes, - ), - ) - else - AnalysisBoard( - pgn, - options, - boardSize, - borderRadius: isTablet ? tabletBoardRadius : null, - enableDrawingShapes: enableDrawingShapes, - ), - Expanded( - child: Padding( - padding: isTablet - ? const EdgeInsets.symmetric( - horizontal: kTabletBoardTableSidePadding, - ) - : EdgeInsets.zero, - child: display, - ), - ), - ], - ); - } - }, - ), - ), - ), - _BottomBar(pgn: pgn, options: options), + AnalysisTreeView(pgn, options), + if (options.canShowGameSummary) ServerAnalysisSummary(pgn, options), ], ); } @@ -432,37 +309,19 @@ class _EngineGaugeVertical extends ConsumerWidget { } } -class _ColumnTopTable extends ConsumerWidget { - const _ColumnTopTable(this.ctrlProvider); +class _EngineGaugeHorizontal extends ConsumerWidget { + const _EngineGaugeHorizontal(this.ctrlProvider); final AnalysisControllerProvider ctrlProvider; @override Widget build(BuildContext context, WidgetRef ref) { final analysisState = ref.watch(ctrlProvider); - final showEvaluationGauge = ref.watch( - analysisPreferencesProvider.select((p) => p.showEvaluationGauge), - ); - return analysisState.hasAvailableEval - ? Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (showEvaluationGauge) - EngineGauge( - displayMode: EngineGaugeDisplayMode.horizontal, - params: analysisState.engineGaugeParams, - ), - if (analysisState.isEngineAvailable) - EngineLines( - clientEval: analysisState.currentNode.eval, - isGameOver: analysisState.currentNode.position.isGameOver, - onTapMove: ref.read(ctrlProvider.notifier).onUserMove, - ), - ], - ) - : kEmptyWidget; + return EngineGauge( + displayMode: EngineGaugeDisplayMode.horizontal, + params: analysisState.engineGaugeParams, + ); } } @@ -491,22 +350,6 @@ class _BottomBar extends ConsumerWidget { }, icon: Icons.menu, ), - if (analysisState.canShowGameSummary) - BottomBarButton( - // TODO: l10n - label: analysisState.displayMode == DisplayMode.summary - ? 'Moves' - : 'Summary', - onTap: () { - final newMode = analysisState.displayMode == DisplayMode.summary - ? DisplayMode.moves - : DisplayMode.summary; - ref.read(ctrlProvider.notifier).setDisplayMode(newMode); - }, - icon: analysisState.displayMode == DisplayMode.summary - ? LichessIcons.flow_cascade - : Icons.area_chart, - ), BottomBarButton( label: context.l10n.openingExplorer, onTap: isOnline diff --git a/lib/src/view/analysis/tree_view.dart b/lib/src/view/analysis/tree_view.dart index 1c132b65bf..69c5b86978 100644 --- a/lib/src/view/analysis/tree_view.dart +++ b/lib/src/view/analysis/tree_view.dart @@ -12,12 +12,10 @@ class AnalysisTreeView extends ConsumerWidget { const AnalysisTreeView( this.pgn, this.options, - this.displayMode, ); final String pgn; final AnalysisOptions options; - final Orientation displayMode; @override Widget build(BuildContext context, WidgetRef ref) { @@ -33,10 +31,7 @@ class AnalysisTreeView extends ConsumerWidget { slivers: [ if (kOpeningAllowedVariants.contains(options.variant)) SliverPersistentHeader( - delegate: _OpeningHeaderDelegate( - ctrlProvider, - displayMode: displayMode, - ), + delegate: _OpeningHeaderDelegate(ctrlProvider), ), SliverFillRemaining( hasScrollBody: false, @@ -53,13 +48,9 @@ class AnalysisTreeView extends ConsumerWidget { } class _OpeningHeaderDelegate extends SliverPersistentHeaderDelegate { - const _OpeningHeaderDelegate( - this.ctrlProvider, { - required this.displayMode, - }); + const _OpeningHeaderDelegate(this.ctrlProvider); final AnalysisControllerProvider ctrlProvider; - final Orientation displayMode; @override Widget build( @@ -67,7 +58,7 @@ class _OpeningHeaderDelegate extends SliverPersistentHeaderDelegate { double shrinkOffset, bool overlapsContent, ) { - return _Opening(ctrlProvider, displayMode); + return _Opening(ctrlProvider); } @override @@ -82,10 +73,9 @@ class _OpeningHeaderDelegate extends SliverPersistentHeaderDelegate { } class _Opening extends ConsumerWidget { - const _Opening(this.ctrlProvider, this.displayMode); + const _Opening(this.ctrlProvider); final AnalysisControllerProvider ctrlProvider; - final Orientation displayMode; @override Widget build(BuildContext context, WidgetRef ref) { diff --git a/lib/src/view/engine/engine_gauge.dart b/lib/src/view/engine/engine_gauge.dart index 4d35a79bb5..d1ef7ae79b 100644 --- a/lib/src/view/engine/engine_gauge.dart +++ b/lib/src/view/engine/engine_gauge.dart @@ -8,8 +8,8 @@ import 'package:lichess_mobile/src/model/settings/brightness.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; -const double kEvalGaugeSize = 26.0; -const double kEvalGaugeFontSize = 11.0; +const double kEvalGaugeSize = 24.0; +const double kEvalGaugeFontSize = 10.0; const Color _kEvalGaugeBackgroundColor = Color(0xFF444444); const Color _kEvalGaugeValueColorDarkBg = Color(0xEEEEEEEE); const Color _kEvalGaugeValueColorLightBg = Color(0xFFFFFFFF); From dbf647de9c321d150260a019706e072283f65535 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 20 Nov 2024 16:29:12 +0100 Subject: [PATCH 726/979] More wip: analysis and explorer widget refactoring --- lib/src/view/analysis/analysis_screen.dart | 708 +----------------- lib/src/view/analysis/server_analysis.dart | 501 +++++++++++++ lib/src/view/engine/engine_depth.dart | 117 +++ .../opening_explorer_screen.dart | 514 +------------ .../opening_explorer_widgets.dart | 504 +++++++++++++ 5 files changed, 1154 insertions(+), 1190 deletions(-) create mode 100644 lib/src/view/analysis/server_analysis.dart create mode 100644 lib/src/view/engine/engine_depth.dart create mode 100644 lib/src/view/opening_explorer/opening_explorer_widgets.dart diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index d663a391b2..886fb8005d 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -1,43 +1,30 @@ -import 'dart:math' as math; - -import 'package:collection/collection.dart'; -import 'package:dartchess/dartchess.dart'; -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; -import 'package:lichess_mobile/src/model/analysis/server_analysis_service.dart'; -import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/model/engine/engine.dart'; -import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; import 'package:lichess_mobile/src/model/game/game_repository_providers.dart'; import 'package:lichess_mobile/src/model/game/game_share_service.dart'; -import 'package:lichess_mobile/src/network/connectivity.dart'; import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; -import 'package:lichess_mobile/src/utils/string.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_layout.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_share_screen.dart'; +import 'package:lichess_mobile/src/view/analysis/server_analysis.dart'; import 'package:lichess_mobile/src/view/board_editor/board_editor_screen.dart'; +import 'package:lichess_mobile/src/view/engine/engine_depth.dart'; import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; import 'package:lichess_mobile/src/view/engine/engine_lines.dart'; -import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_screen.dart'; import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; -import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; -import 'package:popover/popover.dart'; import '../../utils/share.dart'; import 'analysis_board.dart'; @@ -167,13 +154,15 @@ class _LoadedAnalysisScreenState extends ConsumerState<_LoadedAnalysisScreen> @override Widget build(BuildContext context) { final ctrlProvider = analysisControllerProvider(widget.pgn, widget.options); + final currentNodeEval = + ref.watch(ctrlProvider.select((value) => value.currentNode.eval)); return PlatformScaffold( resizeToAvoidBottomInset: false, appBar: PlatformAppBar( title: _Title(options: widget.options), actions: [ - _EngineDepth(ctrlProvider), + EngineDepth(defaultEval: currentNodeEval), AppBarAnalysisTabIndicator( tabs: tabs, controller: _tabController, @@ -237,23 +226,16 @@ class _Body extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final ctrlProvider = analysisControllerProvider(pgn, options); final showEvaluationGauge = ref.watch( analysisPreferencesProvider.select((value) => value.showEvaluationGauge), ); - final isEngineAvailable = ref.watch( - ctrlProvider.select( - (value) => value.isEngineAvailable, - ), - ); - - final hasEval = - ref.watch(ctrlProvider.select((value) => value.hasAvailableEval)); + final ctrlProvider = analysisControllerProvider(pgn, options); + final analysisState = ref.watch(ctrlProvider); - final currentNode = ref.watch( - ctrlProvider.select((value) => value.currentNode), - ); + final isEngineAvailable = analysisState.isEngineAvailable; + final hasEval = analysisState.hasAvailableEval; + final currentNode = analysisState.currentNode; return AnalysisLayout( tabController: controller, @@ -267,8 +249,20 @@ class _Body extends ConsumerWidget { engineGaugeBuilder: hasEval && showEvaluationGauge ? (context, orientation) { return orientation == Orientation.portrait - ? _EngineGaugeHorizontal(ctrlProvider) - : _EngineGaugeVertical(ctrlProvider); + ? EngineGauge( + displayMode: EngineGaugeDisplayMode.horizontal, + params: analysisState.engineGaugeParams, + ) + : Container( + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4.0), + ), + child: EngineGauge( + displayMode: EngineGaugeDisplayMode.vertical, + params: analysisState.engineGaugeParams, + ), + ); } : null, engineLines: isEngineAvailable @@ -287,44 +281,6 @@ class _Body extends ConsumerWidget { } } -class _EngineGaugeVertical extends ConsumerWidget { - const _EngineGaugeVertical(this.ctrlProvider); - - final AnalysisControllerProvider ctrlProvider; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final analysisState = ref.watch(ctrlProvider); - - return Container( - clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4.0), - ), - child: EngineGauge( - displayMode: EngineGaugeDisplayMode.vertical, - params: analysisState.engineGaugeParams, - ), - ); - } -} - -class _EngineGaugeHorizontal extends ConsumerWidget { - const _EngineGaugeHorizontal(this.ctrlProvider); - - final AnalysisControllerProvider ctrlProvider; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final analysisState = ref.watch(ctrlProvider); - - return EngineGauge( - displayMode: EngineGaugeDisplayMode.horizontal, - params: analysisState.engineGaugeParams, - ); - } -} - class _BottomBar extends ConsumerWidget { const _BottomBar({ required this.pgn, @@ -338,8 +294,6 @@ class _BottomBar extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final ctrlProvider = analysisControllerProvider(pgn, options); final analysisState = ref.watch(ctrlProvider); - final isOnline = - ref.watch(connectivityChangesProvider).valueOrNull?.isOnline ?? false; return BottomBar( children: [ @@ -350,22 +304,6 @@ class _BottomBar extends ConsumerWidget { }, icon: Icons.menu, ), - BottomBarButton( - label: context.l10n.openingExplorer, - onTap: isOnline - ? () { - pushPlatformRoute( - context, - title: context.l10n.openingExplorer, - builder: (_) => OpeningExplorerScreen( - pgn: ref.read(ctrlProvider.notifier).makeCurrentNodePgn(), - options: analysisState.openingExplorerOptions, - ), - ); - } - : null, - icon: Icons.explore, - ), RepeatButton( onLongPress: analysisState.canGoBack ? () => _moveBackward(ref) : null, @@ -485,601 +423,3 @@ class _BottomBar extends ConsumerWidget { ); } } - -class _EngineDepth extends ConsumerWidget { - const _EngineDepth(this.ctrlProvider); - - final AnalysisControllerProvider ctrlProvider; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isEngineAvailable = ref.watch( - ctrlProvider.select( - (value) => value.isEngineAvailable, - ), - ); - final currentNode = ref.watch( - ctrlProvider.select((value) => value.currentNode), - ); - final depth = ref.watch( - engineEvaluationProvider.select((value) => value.eval?.depth), - ) ?? - currentNode.eval?.depth; - - return isEngineAvailable && depth != null - ? AppBarTextButton( - onPressed: () { - showPopover( - context: context, - bodyBuilder: (context) { - return _StockfishInfo(currentNode); - }, - direction: PopoverDirection.top, - width: 240, - backgroundColor: - Theme.of(context).platform == TargetPlatform.android - ? Theme.of(context).dialogBackgroundColor - : CupertinoDynamicColor.resolve( - CupertinoColors.tertiarySystemBackground, - context, - ), - transitionDuration: Duration.zero, - popoverTransitionBuilder: (_, child) => child, - ); - }, - child: RepaintBoundary( - child: Container( - width: 20.0, - height: 20.0, - padding: const EdgeInsets.all(2.0), - decoration: BoxDecoration( - color: Theme.of(context).platform == TargetPlatform.android - ? Theme.of(context).colorScheme.secondary - : CupertinoTheme.of(context).primaryColor, - borderRadius: BorderRadius.circular(4.0), - ), - child: FittedBox( - fit: BoxFit.contain, - child: Text( - '${math.min(99, depth)}', - style: TextStyle( - color: Theme.of(context).platform == - TargetPlatform.android - ? Theme.of(context).colorScheme.onSecondary - : CupertinoTheme.of(context).primaryContrastingColor, - fontFeatures: const [ - FontFeature.tabularFigures(), - ], - ), - ), - ), - ), - ), - ) - : const SizedBox.shrink(); - } -} - -class _StockfishInfo extends ConsumerWidget { - const _StockfishInfo(this.currentNode); - - final AnalysisCurrentNode currentNode; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final (engineName: engineName, eval: eval, state: engineState) = - ref.watch(engineEvaluationProvider); - - final currentEval = eval ?? currentNode.eval; - - final knps = engineState == EngineState.computing - ? ', ${eval?.knps.round()}kn/s' - : ''; - final depth = currentEval?.depth ?? 0; - final maxDepth = math.max(depth, kMaxEngineDepth); - - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - PlatformListTile( - leading: Image.asset( - 'assets/images/stockfish/icon.png', - width: 44, - height: 44, - ), - title: Text(engineName), - subtitle: Text( - context.l10n.depthX( - '$depth/$maxDepth$knps', - ), - ), - ), - ], - ); - } -} - -class ServerAnalysisSummary extends ConsumerWidget { - const ServerAnalysisSummary(this.pgn, this.options); - - final String pgn; - final AnalysisOptions options; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final ctrlProvider = analysisControllerProvider(pgn, options); - final playersAnalysis = - ref.watch(ctrlProvider.select((value) => value.playersAnalysis)); - final pgnHeaders = - ref.watch(ctrlProvider.select((value) => value.pgnHeaders)); - final currentGameAnalysis = ref.watch(currentAnalysisProvider); - - return playersAnalysis != null - ? ListView( - children: [ - if (currentGameAnalysis == options.gameAnyId?.gameId) - const Padding( - padding: EdgeInsets.only(top: 16.0), - child: WaitingForServerAnalysis(), - ), - AcplChart(pgn, options), - Center( - child: SizedBox( - width: math.min(MediaQuery.sizeOf(context).width, 500), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Table( - defaultVerticalAlignment: - TableCellVerticalAlignment.middle, - columnWidths: const { - 0: FlexColumnWidth(1), - 1: FlexColumnWidth(1), - 2: FlexColumnWidth(1), - }, - children: [ - TableRow( - decoration: const BoxDecoration( - border: Border( - bottom: BorderSide(color: Colors.grey), - ), - ), - children: [ - _SummaryPlayerName(Side.white, pgnHeaders), - Center( - child: Text( - pgnHeaders.get('Result') ?? '', - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - ), - ), - _SummaryPlayerName(Side.black, pgnHeaders), - ], - ), - if (playersAnalysis.white.accuracy != null && - playersAnalysis.black.accuracy != null) - TableRow( - children: [ - _SummaryNumber( - '${playersAnalysis.white.accuracy}%', - ), - Center( - heightFactor: 1.8, - child: Text( - context.l10n.accuracy, - softWrap: true, - ), - ), - _SummaryNumber( - '${playersAnalysis.black.accuracy}%', - ), - ], - ), - for (final item in [ - ( - playersAnalysis.white.inaccuracies.toString(), - context.l10n - .nbInaccuracies(2) - .replaceAll('2', '') - .trim() - .capitalize(), - playersAnalysis.black.inaccuracies.toString() - ), - ( - playersAnalysis.white.mistakes.toString(), - context.l10n - .nbMistakes(2) - .replaceAll('2', '') - .trim() - .capitalize(), - playersAnalysis.black.mistakes.toString() - ), - ( - playersAnalysis.white.blunders.toString(), - context.l10n - .nbBlunders(2) - .replaceAll('2', '') - .trim() - .capitalize(), - playersAnalysis.black.blunders.toString() - ), - ]) - TableRow( - children: [ - _SummaryNumber(item.$1), - Center( - heightFactor: 1.2, - child: Text( - item.$2, - softWrap: true, - ), - ), - _SummaryNumber(item.$3), - ], - ), - if (playersAnalysis.white.acpl != null && - playersAnalysis.black.acpl != null) - TableRow( - children: [ - _SummaryNumber( - playersAnalysis.white.acpl.toString(), - ), - Center( - heightFactor: 1.5, - child: Text( - context.l10n.averageCentipawnLoss, - softWrap: true, - textAlign: TextAlign.center, - ), - ), - _SummaryNumber( - playersAnalysis.black.acpl.toString(), - ), - ], - ), - ], - ), - ), - ), - ), - ], - ) - : Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Spacer(), - if (currentGameAnalysis == options.gameAnyId?.gameId) - const Center( - child: Padding( - padding: EdgeInsets.symmetric(vertical: 16.0), - child: WaitingForServerAnalysis(), - ), - ) - else - Center( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0), - child: Builder( - builder: (context) { - Future? pendingRequest; - return StatefulBuilder( - builder: (context, setState) { - return FutureBuilder( - future: pendingRequest, - builder: (context, snapshot) { - return SecondaryButton( - semanticsLabel: - context.l10n.requestAComputerAnalysis, - onPressed: ref.watch(authSessionProvider) == - null - ? () { - showPlatformSnackbar( - context, - context - .l10n.youNeedAnAccountToDoThat, - ); - } - : snapshot.connectionState == - ConnectionState.waiting - ? null - : () { - setState(() { - pendingRequest = ref - .read(ctrlProvider.notifier) - .requestServerAnalysis() - .catchError((Object e) { - if (context.mounted) { - showPlatformSnackbar( - context, - e.toString(), - type: SnackBarType.error, - ); - } - }); - }); - }, - child: Text( - context.l10n.requestAComputerAnalysis, - ), - ); - }, - ); - }, - ); - }, - ), - ), - ), - const Spacer(), - ], - ); - } -} - -class WaitingForServerAnalysis extends StatelessWidget { - const WaitingForServerAnalysis({super.key}); - - @override - Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.max, - children: [ - Image.asset( - 'assets/images/stockfish/icon.png', - width: 30, - height: 30, - ), - const SizedBox(width: 8.0), - Text(context.l10n.waitingForAnalysis), - const SizedBox(width: 8.0), - const CircularProgressIndicator.adaptive(), - ], - ); - } -} - -class _SummaryNumber extends StatelessWidget { - const _SummaryNumber(this.data); - final String data; - - @override - Widget build(BuildContext context) { - return Center( - child: Text( - data, - softWrap: true, - ), - ); - } -} - -class _SummaryPlayerName extends StatelessWidget { - const _SummaryPlayerName(this.side, this.pgnHeaders); - final Side side; - final IMap pgnHeaders; - - @override - Widget build(BuildContext context) { - final playerTitle = side == Side.white - ? pgnHeaders.get('WhiteTitle') - : pgnHeaders.get('BlackTitle'); - final playerName = side == Side.white - ? pgnHeaders.get('White') ?? context.l10n.white - : pgnHeaders.get('Black') ?? context.l10n.black; - - final brightness = Theme.of(context).brightness; - - return TableCell( - verticalAlignment: TableCellVerticalAlignment.top, - child: Center( - child: Padding( - padding: const EdgeInsets.only(bottom: 5), - child: Column( - children: [ - Icon( - side == Side.white - ? brightness == Brightness.light - ? CupertinoIcons.circle - : CupertinoIcons.circle_filled - : brightness == Brightness.light - ? CupertinoIcons.circle_filled - : CupertinoIcons.circle, - size: 14, - ), - Text( - '${playerTitle != null ? '$playerTitle ' : ''}$playerName', - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - textAlign: TextAlign.center, - softWrap: true, - ), - ], - ), - ), - ), - ); - } -} - -class AcplChart extends ConsumerWidget { - const AcplChart(this.pgn, this.options); - - final String pgn; - final AnalysisOptions options; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final mainLineColor = Theme.of(context).colorScheme.secondary; - // yes it looks like below/above are inverted in fl_chart - final brightness = Theme.of(context).brightness; - final white = Theme.of(context).colorScheme.surfaceContainerHighest; - final black = Theme.of(context).colorScheme.outline; - // yes it looks like below/above are inverted in fl_chart - final belowLineColor = brightness == Brightness.light ? white : black; - final aboveLineColor = brightness == Brightness.light ? black : white; - - VerticalLine phaseVerticalBar(double x, String label) => VerticalLine( - x: x, - color: const Color(0xFF707070), - strokeWidth: 0.5, - label: VerticalLineLabel( - style: TextStyle( - fontSize: 10, - color: Theme.of(context) - .textTheme - .labelMedium - ?.color - ?.withValues(alpha: 0.3), - ), - labelResolver: (line) => label, - padding: const EdgeInsets.only(right: 1), - alignment: Alignment.topRight, - direction: LabelDirection.vertical, - show: true, - ), - ); - - final data = ref.watch( - analysisControllerProvider(pgn, options) - .select((value) => value.acplChartData), - ); - - final rootPly = ref.watch( - analysisControllerProvider(pgn, options) - .select((value) => value.root.position.ply), - ); - - final currentNode = ref.watch( - analysisControllerProvider(pgn, options) - .select((value) => value.currentNode), - ); - - final isOnMainline = ref.watch( - analysisControllerProvider(pgn, options) - .select((value) => value.isOnMainline), - ); - - if (data == null) { - return const SizedBox.shrink(); - } - - final spots = data - .mapIndexed( - (i, e) => FlSpot(i.toDouble(), e.winningChances(Side.white)), - ) - .toList(growable: false); - - final divisionLines = []; - - if (options.division?.middlegame != null) { - if (options.division!.middlegame! > 0) { - divisionLines.add(phaseVerticalBar(0.0, context.l10n.opening)); - divisionLines.add( - phaseVerticalBar( - options.division!.middlegame! - 1, - context.l10n.middlegame, - ), - ); - } else { - divisionLines.add(phaseVerticalBar(0.0, context.l10n.middlegame)); - } - } - - if (options.division?.endgame != null) { - if (options.division!.endgame! > 0) { - divisionLines.add( - phaseVerticalBar( - options.division!.endgame! - 1, - context.l10n.endgame, - ), - ); - } else { - divisionLines.add( - phaseVerticalBar( - 0.0, - context.l10n.endgame, - ), - ); - } - } - return Center( - child: AspectRatio( - aspectRatio: 2.5, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: LineChart( - LineChartData( - lineTouchData: LineTouchData( - enabled: false, - touchCallback: - (FlTouchEvent event, LineTouchResponse? touchResponse) { - if (event is FlTapDownEvent || - event is FlPanUpdateEvent || - event is FlLongPressMoveUpdate) { - final touchX = event.localPosition!.dx; - final chartWidth = context.size!.width - - 32; // Insets on both sides of the chart of 16 - final minX = spots.first.x; - final maxX = spots.last.x; - final touchXDataValue = - minX + (touchX / chartWidth) * (maxX - minX); - final closestSpot = spots.reduce( - (a, b) => (a.x - touchXDataValue).abs() < - (b.x - touchXDataValue).abs() - ? a - : b, - ); - final closestNodeIndex = closestSpot.x.round(); - ref - .read(analysisControllerProvider(pgn, options).notifier) - .jumpToNthNodeOnMainline(closestNodeIndex); - } - }, - ), - minY: -1.0, - maxY: 1.0, - lineBarsData: [ - LineChartBarData( - spots: spots, - isCurved: false, - barWidth: 1, - color: mainLineColor.withValues(alpha: 0.7), - aboveBarData: BarAreaData( - show: true, - color: aboveLineColor, - applyCutOffY: true, - ), - belowBarData: BarAreaData( - show: true, - color: belowLineColor, - applyCutOffY: true, - ), - dotData: const FlDotData( - show: false, - ), - ), - ], - extraLinesData: ExtraLinesData( - verticalLines: [ - if (isOnMainline) - VerticalLine( - x: (currentNode.position.ply - 1 - rootPly).toDouble(), - color: mainLineColor, - strokeWidth: 1.0, - ), - ...divisionLines, - ], - ), - gridData: const FlGridData(show: false), - borderData: FlBorderData(show: false), - titlesData: const FlTitlesData(show: false), - ), - ), - ), - ), - ); - } -} diff --git a/lib/src/view/analysis/server_analysis.dart b/lib/src/view/analysis/server_analysis.dart new file mode 100644 index 0000000000..93510a1f6b --- /dev/null +++ b/lib/src/view/analysis/server_analysis.dart @@ -0,0 +1,501 @@ +import 'dart:math' as math; + +import 'package:collection/collection.dart'; +import 'package:dartchess/dartchess.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; +import 'package:lichess_mobile/src/model/analysis/server_analysis_service.dart'; +import 'package:lichess_mobile/src/model/auth/auth_session.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/string.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/feedback.dart'; + +class ServerAnalysisSummary extends ConsumerWidget { + const ServerAnalysisSummary(this.pgn, this.options); + + final String pgn; + final AnalysisOptions options; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final ctrlProvider = analysisControllerProvider(pgn, options); + final playersAnalysis = + ref.watch(ctrlProvider.select((value) => value.playersAnalysis)); + final pgnHeaders = + ref.watch(ctrlProvider.select((value) => value.pgnHeaders)); + final currentGameAnalysis = ref.watch(currentAnalysisProvider); + + return playersAnalysis != null + ? ListView( + children: [ + if (currentGameAnalysis == options.gameAnyId?.gameId) + const Padding( + padding: EdgeInsets.only(top: 16.0), + child: WaitingForServerAnalysis(), + ), + AcplChart(pgn, options), + Center( + child: SizedBox( + width: math.min(MediaQuery.sizeOf(context).width, 500), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Table( + defaultVerticalAlignment: + TableCellVerticalAlignment.middle, + columnWidths: const { + 0: FlexColumnWidth(1), + 1: FlexColumnWidth(1), + 2: FlexColumnWidth(1), + }, + children: [ + TableRow( + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide(color: Colors.grey), + ), + ), + children: [ + _SummaryPlayerName(Side.white, pgnHeaders), + Center( + child: Text( + pgnHeaders.get('Result') ?? '', + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + ), + _SummaryPlayerName(Side.black, pgnHeaders), + ], + ), + if (playersAnalysis.white.accuracy != null && + playersAnalysis.black.accuracy != null) + TableRow( + children: [ + _SummaryNumber( + '${playersAnalysis.white.accuracy}%', + ), + Center( + heightFactor: 1.8, + child: Text( + context.l10n.accuracy, + softWrap: true, + ), + ), + _SummaryNumber( + '${playersAnalysis.black.accuracy}%', + ), + ], + ), + for (final item in [ + ( + playersAnalysis.white.inaccuracies.toString(), + context.l10n + .nbInaccuracies(2) + .replaceAll('2', '') + .trim() + .capitalize(), + playersAnalysis.black.inaccuracies.toString() + ), + ( + playersAnalysis.white.mistakes.toString(), + context.l10n + .nbMistakes(2) + .replaceAll('2', '') + .trim() + .capitalize(), + playersAnalysis.black.mistakes.toString() + ), + ( + playersAnalysis.white.blunders.toString(), + context.l10n + .nbBlunders(2) + .replaceAll('2', '') + .trim() + .capitalize(), + playersAnalysis.black.blunders.toString() + ), + ]) + TableRow( + children: [ + _SummaryNumber(item.$1), + Center( + heightFactor: 1.2, + child: Text( + item.$2, + softWrap: true, + ), + ), + _SummaryNumber(item.$3), + ], + ), + if (playersAnalysis.white.acpl != null && + playersAnalysis.black.acpl != null) + TableRow( + children: [ + _SummaryNumber( + playersAnalysis.white.acpl.toString(), + ), + Center( + heightFactor: 1.5, + child: Text( + context.l10n.averageCentipawnLoss, + softWrap: true, + textAlign: TextAlign.center, + ), + ), + _SummaryNumber( + playersAnalysis.black.acpl.toString(), + ), + ], + ), + ], + ), + ), + ), + ), + ], + ) + : Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Spacer(), + if (currentGameAnalysis == options.gameAnyId?.gameId) + const Center( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 16.0), + child: WaitingForServerAnalysis(), + ), + ) + else + Center( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Builder( + builder: (context) { + Future? pendingRequest; + return StatefulBuilder( + builder: (context, setState) { + return FutureBuilder( + future: pendingRequest, + builder: (context, snapshot) { + return SecondaryButton( + semanticsLabel: + context.l10n.requestAComputerAnalysis, + onPressed: ref.watch(authSessionProvider) == + null + ? () { + showPlatformSnackbar( + context, + context + .l10n.youNeedAnAccountToDoThat, + ); + } + : snapshot.connectionState == + ConnectionState.waiting + ? null + : () { + setState(() { + pendingRequest = ref + .read(ctrlProvider.notifier) + .requestServerAnalysis() + .catchError((Object e) { + if (context.mounted) { + showPlatformSnackbar( + context, + e.toString(), + type: SnackBarType.error, + ); + } + }); + }); + }, + child: Text( + context.l10n.requestAComputerAnalysis, + ), + ); + }, + ); + }, + ); + }, + ), + ), + ), + const Spacer(), + ], + ); + } +} + +class WaitingForServerAnalysis extends StatelessWidget { + const WaitingForServerAnalysis({super.key}); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ + Image.asset( + 'assets/images/stockfish/icon.png', + width: 30, + height: 30, + ), + const SizedBox(width: 8.0), + Text(context.l10n.waitingForAnalysis), + const SizedBox(width: 8.0), + const CircularProgressIndicator.adaptive(), + ], + ); + } +} + +class _SummaryNumber extends StatelessWidget { + const _SummaryNumber(this.data); + final String data; + + @override + Widget build(BuildContext context) { + return Center( + child: Text( + data, + softWrap: true, + ), + ); + } +} + +class _SummaryPlayerName extends StatelessWidget { + const _SummaryPlayerName(this.side, this.pgnHeaders); + final Side side; + final IMap pgnHeaders; + + @override + Widget build(BuildContext context) { + final playerTitle = side == Side.white + ? pgnHeaders.get('WhiteTitle') + : pgnHeaders.get('BlackTitle'); + final playerName = side == Side.white + ? pgnHeaders.get('White') ?? context.l10n.white + : pgnHeaders.get('Black') ?? context.l10n.black; + + final brightness = Theme.of(context).brightness; + + return TableCell( + verticalAlignment: TableCellVerticalAlignment.top, + child: Center( + child: Padding( + padding: const EdgeInsets.only(bottom: 5), + child: Column( + children: [ + Icon( + side == Side.white + ? brightness == Brightness.light + ? CupertinoIcons.circle + : CupertinoIcons.circle_filled + : brightness == Brightness.light + ? CupertinoIcons.circle_filled + : CupertinoIcons.circle, + size: 14, + ), + Text( + '${playerTitle != null ? '$playerTitle ' : ''}$playerName', + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + softWrap: true, + ), + ], + ), + ), + ), + ); + } +} + +class AcplChart extends ConsumerWidget { + const AcplChart(this.pgn, this.options); + + final String pgn; + final AnalysisOptions options; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final mainLineColor = Theme.of(context).colorScheme.secondary; + // yes it looks like below/above are inverted in fl_chart + final brightness = Theme.of(context).brightness; + final white = Theme.of(context).colorScheme.surfaceContainerHighest; + final black = Theme.of(context).colorScheme.outline; + // yes it looks like below/above are inverted in fl_chart + final belowLineColor = brightness == Brightness.light ? white : black; + final aboveLineColor = brightness == Brightness.light ? black : white; + + VerticalLine phaseVerticalBar(double x, String label) => VerticalLine( + x: x, + color: const Color(0xFF707070), + strokeWidth: 0.5, + label: VerticalLineLabel( + style: TextStyle( + fontSize: 10, + color: Theme.of(context) + .textTheme + .labelMedium + ?.color + ?.withValues(alpha: 0.3), + ), + labelResolver: (line) => label, + padding: const EdgeInsets.only(right: 1), + alignment: Alignment.topRight, + direction: LabelDirection.vertical, + show: true, + ), + ); + + final data = ref.watch( + analysisControllerProvider(pgn, options) + .select((value) => value.acplChartData), + ); + + final rootPly = ref.watch( + analysisControllerProvider(pgn, options) + .select((value) => value.root.position.ply), + ); + + final currentNode = ref.watch( + analysisControllerProvider(pgn, options) + .select((value) => value.currentNode), + ); + + final isOnMainline = ref.watch( + analysisControllerProvider(pgn, options) + .select((value) => value.isOnMainline), + ); + + if (data == null) { + return const SizedBox.shrink(); + } + + final spots = data + .mapIndexed( + (i, e) => FlSpot(i.toDouble(), e.winningChances(Side.white)), + ) + .toList(growable: false); + + final divisionLines = []; + + if (options.division?.middlegame != null) { + if (options.division!.middlegame! > 0) { + divisionLines.add(phaseVerticalBar(0.0, context.l10n.opening)); + divisionLines.add( + phaseVerticalBar( + options.division!.middlegame! - 1, + context.l10n.middlegame, + ), + ); + } else { + divisionLines.add(phaseVerticalBar(0.0, context.l10n.middlegame)); + } + } + + if (options.division?.endgame != null) { + if (options.division!.endgame! > 0) { + divisionLines.add( + phaseVerticalBar( + options.division!.endgame! - 1, + context.l10n.endgame, + ), + ); + } else { + divisionLines.add( + phaseVerticalBar( + 0.0, + context.l10n.endgame, + ), + ); + } + } + return Center( + child: AspectRatio( + aspectRatio: 2.5, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: LineChart( + LineChartData( + lineTouchData: LineTouchData( + enabled: false, + touchCallback: + (FlTouchEvent event, LineTouchResponse? touchResponse) { + if (event is FlTapDownEvent || + event is FlPanUpdateEvent || + event is FlLongPressMoveUpdate) { + final touchX = event.localPosition!.dx; + final chartWidth = context.size!.width - + 32; // Insets on both sides of the chart of 16 + final minX = spots.first.x; + final maxX = spots.last.x; + final touchXDataValue = + minX + (touchX / chartWidth) * (maxX - minX); + final closestSpot = spots.reduce( + (a, b) => (a.x - touchXDataValue).abs() < + (b.x - touchXDataValue).abs() + ? a + : b, + ); + final closestNodeIndex = closestSpot.x.round(); + ref + .read(analysisControllerProvider(pgn, options).notifier) + .jumpToNthNodeOnMainline(closestNodeIndex); + } + }, + ), + minY: -1.0, + maxY: 1.0, + lineBarsData: [ + LineChartBarData( + spots: spots, + isCurved: false, + barWidth: 1, + color: mainLineColor.withValues(alpha: 0.7), + aboveBarData: BarAreaData( + show: true, + color: aboveLineColor, + applyCutOffY: true, + ), + belowBarData: BarAreaData( + show: true, + color: belowLineColor, + applyCutOffY: true, + ), + dotData: const FlDotData( + show: false, + ), + ), + ], + extraLinesData: ExtraLinesData( + verticalLines: [ + if (isOnMainline) + VerticalLine( + x: (currentNode.position.ply - 1 - rootPly).toDouble(), + color: mainLineColor, + strokeWidth: 1.0, + ), + ...divisionLines, + ], + ), + gridData: const FlGridData(show: false), + borderData: FlBorderData(show: false), + titlesData: const FlTitlesData(show: false), + ), + ), + ), + ), + ); + } +} diff --git a/lib/src/view/engine/engine_depth.dart b/lib/src/view/engine/engine_depth.dart new file mode 100644 index 0000000000..d3352d676d --- /dev/null +++ b/lib/src/view/engine/engine_depth.dart @@ -0,0 +1,117 @@ +import 'dart:math' as math; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/common/eval.dart'; +import 'package:lichess_mobile/src/model/engine/engine.dart'; +import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/list.dart'; +import 'package:popover/popover.dart'; + +class EngineDepth extends ConsumerWidget { + const EngineDepth({this.defaultEval}); + + final ClientEval? defaultEval; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final depth = ref.watch( + engineEvaluationProvider.select((value) => value.eval?.depth), + ) ?? + defaultEval?.depth; + + return depth != null + ? AppBarTextButton( + onPressed: () { + showPopover( + context: context, + bodyBuilder: (context) { + return _StockfishInfo(defaultEval); + }, + direction: PopoverDirection.top, + width: 240, + backgroundColor: + Theme.of(context).platform == TargetPlatform.android + ? Theme.of(context).dialogBackgroundColor + : CupertinoDynamicColor.resolve( + CupertinoColors.tertiarySystemBackground, + context, + ), + transitionDuration: Duration.zero, + popoverTransitionBuilder: (_, child) => child, + ); + }, + child: RepaintBoundary( + child: Container( + width: 20.0, + height: 20.0, + padding: const EdgeInsets.all(2.0), + decoration: BoxDecoration( + color: Theme.of(context).platform == TargetPlatform.android + ? Theme.of(context).colorScheme.secondary + : CupertinoTheme.of(context).primaryColor, + borderRadius: BorderRadius.circular(4.0), + ), + child: FittedBox( + fit: BoxFit.contain, + child: Text( + '${math.min(99, depth)}', + style: TextStyle( + color: Theme.of(context).platform == + TargetPlatform.android + ? Theme.of(context).colorScheme.onSecondary + : CupertinoTheme.of(context).primaryContrastingColor, + fontFeatures: const [ + FontFeature.tabularFigures(), + ], + ), + ), + ), + ), + ), + ) + : const SizedBox.shrink(); + } +} + +class _StockfishInfo extends ConsumerWidget { + const _StockfishInfo(this.defaultEval); + + final ClientEval? defaultEval; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final (engineName: engineName, eval: eval, state: engineState) = + ref.watch(engineEvaluationProvider); + + final currentEval = eval ?? defaultEval; + + final knps = engineState == EngineState.computing + ? ', ${eval?.knps.round()}kn/s' + : ''; + final depth = currentEval?.depth ?? 0; + final maxDepth = math.max(depth, kMaxEngineDepth); + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + PlatformListTile( + leading: Image.asset( + 'assets/images/stockfish/icon.png', + width: 44, + height: 44, + ), + title: Text(engineName), + subtitle: Text( + context.l10n.depthX( + '$depth/$maxDepth$knps', + ), + ), + ), + ], + ); + } +} diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index 0e2d1dbc29..341afecb60 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -1,10 +1,7 @@ import 'package:collection/collection.dart'; -import 'package:dartchess/dartchess.dart'; -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; @@ -13,10 +10,9 @@ import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_prefe import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_repository.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; -import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_board.dart'; -import 'package:lichess_mobile/src/view/game/archived_game_screen.dart'; +import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_widgets.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; @@ -36,16 +32,6 @@ const _kTableRowPadding = EdgeInsets.symmetric( ); const _kTabletBoardRadius = BorderRadius.all(Radius.circular(4.0)); -Color _whiteBoxColor(BuildContext context) => - Theme.of(context).brightness == Brightness.dark - ? Colors.white.withValues(alpha: 0.8) - : Colors.white; - -Color _blackBoxColor(BuildContext context) => - Theme.of(context).brightness == Brightness.light - ? Colors.black.withValues(alpha: 0.7) - : Colors.black; - class OpeningExplorerScreen extends ConsumerStatefulWidget { const OpeningExplorerScreen({required this.pgn, required this.options}); @@ -122,7 +108,7 @@ class _OpeningExplorerState extends ConsumerState { isIndexing: false, children: [ openingHeader, - _OpeningExplorerMoveTable.maxDepth( + OpeningExplorerMoveTable.maxDepth( pgn: widget.pgn, options: widget.options, ), @@ -189,7 +175,7 @@ class _OpeningExplorerState extends ConsumerState { Shimmer( child: ShimmerLoading( isLoading: true, - child: _OpeningExplorerMoveTable.loading( + child: OpeningExplorerMoveTable.loading( pgn: widget.pgn, options: widget.options, ), @@ -205,7 +191,7 @@ class _OpeningExplorerState extends ConsumerState { final children = [ openingHeader, - _OpeningExplorerMoveTable( + OpeningExplorerMoveTable( moves: openingExplorer.entry.moves, whiteWins: openingExplorer.entry.white, draws: openingExplorer.entry.draws, @@ -214,7 +200,7 @@ class _OpeningExplorerState extends ConsumerState { options: widget.options, ), if (topGames != null && topGames.isNotEmpty) ...[ - _OpeningExplorerHeader( + OpeningExplorerHeaderTile( key: const Key('topGamesHeader'), child: Text(context.l10n.topGames), ), @@ -234,7 +220,7 @@ class _OpeningExplorerState extends ConsumerState { ), ], if (recentGames != null && recentGames.isNotEmpty) ...[ - _OpeningExplorerHeader( + OpeningExplorerHeaderTile( key: const Key('recentGamesHeader'), child: Text(context.l10n.recentGames), ), @@ -265,7 +251,7 @@ class _OpeningExplorerState extends ConsumerState { Shimmer( child: ShimmerLoading( isLoading: true, - child: _OpeningExplorerMoveTable.loading( + child: OpeningExplorerMoveTable.loading( pgn: widget.pgn, options: widget.options, ), @@ -338,15 +324,7 @@ class _OpeningExplorerView extends StatelessWidget { final isLandscape = aspectRatio > 1; final loadingOverlay = Positioned.fill( - child: IgnorePointer( - ignoring: !isLoading, - child: AnimatedOpacity( - duration: const Duration(milliseconds: 300), - curve: Curves.fastOutSlowIn, - opacity: isLoading ? 0.10 : 0.0, - child: const ColoredBox(color: Colors.black), - ), - ), + child: IgnorePointer(ignoring: !isLoading), ); if (isLandscape) { @@ -501,482 +479,6 @@ class _IndexingIndicatorState extends State<_IndexingIndicator> } } -/// Table of moves for the opening explorer. -class _OpeningExplorerMoveTable extends ConsumerWidget { - const _OpeningExplorerMoveTable({ - required this.moves, - required this.whiteWins, - required this.draws, - required this.blackWins, - required this.pgn, - required this.options, - }) : _isLoading = false, - _maxDepthReached = false; - - const _OpeningExplorerMoveTable.loading({ - required this.pgn, - required this.options, - }) : _isLoading = true, - moves = const IListConst([]), - whiteWins = 0, - draws = 0, - blackWins = 0, - _maxDepthReached = false; - - const _OpeningExplorerMoveTable.maxDepth({ - required this.pgn, - required this.options, - }) : _isLoading = false, - moves = const IListConst([]), - whiteWins = 0, - draws = 0, - blackWins = 0, - _maxDepthReached = true; - - final IList moves; - final int whiteWins; - final int draws; - final int blackWins; - final String pgn; - final AnalysisOptions options; - - final bool _isLoading; - final bool _maxDepthReached; - - String formatNum(int num) => NumberFormat.decimalPatternDigits().format(num); - - static const columnWidths = { - 0: FractionColumnWidth(0.15), - 1: FractionColumnWidth(0.35), - 2: FractionColumnWidth(0.50), - }; - - @override - Widget build(BuildContext context, WidgetRef ref) { - if (_isLoading) { - return loadingTable; - } - - final games = whiteWins + draws + blackWins; - final ctrlProvider = analysisControllerProvider(pgn, options); - - const topPadding = EdgeInsets.only(top: _kTableRowVerticalPadding / 2); - const headerTextStyle = TextStyle(fontSize: 12); - - return Table( - columnWidths: columnWidths, - children: [ - TableRow( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.secondaryContainer, - ), - children: [ - Padding( - padding: _kTableRowPadding.subtract(topPadding), - child: Text(context.l10n.move, style: headerTextStyle), - ), - Padding( - padding: _kTableRowPadding.subtract(topPadding), - child: Text(context.l10n.games, style: headerTextStyle), - ), - Padding( - padding: _kTableRowPadding.subtract(topPadding), - child: Text(context.l10n.whiteDrawBlack, style: headerTextStyle), - ), - ], - ), - ...List.generate( - moves.length, - (int index) { - final move = moves.get(index); - final percentGames = ((move.games / games) * 100).round(); - return TableRow( - decoration: BoxDecoration( - color: index.isEven - ? Theme.of(context).colorScheme.surfaceContainerLow - : Theme.of(context).colorScheme.surfaceContainerHigh, - ), - children: [ - TableRowInkWell( - onTap: () => ref - .read(ctrlProvider.notifier) - .onUserMove(NormalMove.fromUci(move.uci)), - child: Padding( - padding: _kTableRowPadding, - child: Text(move.san), - ), - ), - TableRowInkWell( - onTap: () => ref - .read(ctrlProvider.notifier) - .onUserMove(NormalMove.fromUci(move.uci)), - child: Padding( - padding: _kTableRowPadding, - child: Text('${formatNum(move.games)} ($percentGames%)'), - ), - ), - TableRowInkWell( - onTap: () => ref - .read(ctrlProvider.notifier) - .onUserMove(NormalMove.fromUci(move.uci)), - child: Padding( - padding: _kTableRowPadding, - child: _WinPercentageChart( - whiteWins: move.white, - draws: move.draws, - blackWins: move.black, - ), - ), - ), - ], - ); - }, - ), - if (_maxDepthReached) - TableRow( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainerLow, - ), - children: [ - Padding( - padding: _kTableRowPadding, - child: Text( - String.fromCharCode(Icons.not_interested_outlined.codePoint), - style: TextStyle( - fontFamily: Icons.not_interested_outlined.fontFamily, - ), - ), - ), - Padding( - padding: _kTableRowPadding, - child: Text(context.l10n.maxDepthReached), - ), - const Padding( - padding: _kTableRowPadding, - child: SizedBox.shrink(), - ), - ], - ) - else if (moves.isNotEmpty) - TableRow( - decoration: BoxDecoration( - color: moves.length.isEven - ? Theme.of(context).colorScheme.surfaceContainerLow - : Theme.of(context).colorScheme.surfaceContainerHigh, - ), - children: [ - Container( - padding: _kTableRowPadding, - alignment: Alignment.centerLeft, - child: const Icon(Icons.functions), - ), - Padding( - padding: _kTableRowPadding, - child: Text('${formatNum(games)} (100%)'), - ), - Padding( - padding: _kTableRowPadding, - child: _WinPercentageChart( - whiteWins: whiteWins, - draws: draws, - blackWins: blackWins, - ), - ), - ], - ) - else - TableRow( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainerLow, - ), - children: [ - Padding( - padding: _kTableRowPadding, - child: Text( - String.fromCharCode(Icons.not_interested_outlined.codePoint), - style: TextStyle( - fontFamily: Icons.not_interested_outlined.fontFamily, - ), - ), - ), - Padding( - padding: _kTableRowPadding, - child: Text(context.l10n.noGameFound), - ), - const Padding( - padding: _kTableRowPadding, - child: SizedBox.shrink(), - ), - ], - ), - ], - ); - } - - static final loadingTable = Table( - columnWidths: columnWidths, - children: List.generate( - 10, - (int index) => TableRow( - children: [ - Padding( - padding: _kTableRowPadding, - child: Container( - height: 20, - width: double.infinity, - decoration: BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(5), - ), - ), - ), - Padding( - padding: _kTableRowPadding, - child: Container( - height: 20, - width: double.infinity, - decoration: BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(5), - ), - ), - ), - Padding( - padding: _kTableRowPadding, - child: Container( - height: 20, - width: double.infinity, - decoration: BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(5), - ), - ), - ), - ], - ), - ), - ); -} - -/// A game tile for the opening explorer. -class OpeningExplorerGameTile extends ConsumerStatefulWidget { - const OpeningExplorerGameTile({ - required this.game, - required this.color, - required this.ply, - super.key, - }); - - final OpeningExplorerGame game; - final Color color; - final int ply; - - @override - ConsumerState createState() => - _OpeningExplorerGameTileState(); -} - -class _OpeningExplorerGameTileState - extends ConsumerState { - @override - Widget build(BuildContext context) { - const widthResultBox = 50.0; - const paddingResultBox = EdgeInsets.all(5); - - return Container( - padding: _kTableRowPadding, - color: widget.color, - child: AdaptiveInkWell( - onTap: () { - pushPlatformRoute( - context, - builder: (_) => ArchivedGameScreen( - gameId: widget.game.id, - orientation: Side.white, - initialCursor: widget.ply, - ), - ); - }, - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(widget.game.white.rating.toString()), - Text(widget.game.black.rating.toString()), - ], - ), - const SizedBox(width: 10), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - widget.game.white.name, - overflow: TextOverflow.ellipsis, - ), - Text( - widget.game.black.name, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - Row( - children: [ - if (widget.game.winner == 'white') - Container( - width: widthResultBox, - padding: paddingResultBox, - decoration: BoxDecoration( - color: _whiteBoxColor(context), - borderRadius: BorderRadius.circular(5), - ), - child: const Text( - '1-0', - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.black, - ), - ), - ) - else if (widget.game.winner == 'black') - Container( - width: widthResultBox, - padding: paddingResultBox, - decoration: BoxDecoration( - color: _blackBoxColor(context), - borderRadius: BorderRadius.circular(5), - ), - child: const Text( - '0-1', - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white, - ), - ), - ) - else - Container( - width: widthResultBox, - padding: paddingResultBox, - decoration: BoxDecoration( - color: Colors.grey, - borderRadius: BorderRadius.circular(5), - ), - child: const Text( - '½-½', - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white, - ), - ), - ), - if (widget.game.month != null) ...[ - const SizedBox(width: 10.0), - Text( - widget.game.month!, - style: const TextStyle( - fontFeatures: [FontFeature.tabularFigures()], - ), - ), - ], - if (widget.game.speed != null) ...[ - const SizedBox(width: 10.0), - Icon(widget.game.speed!.icon, size: 20), - ], - ], - ), - ], - ), - ), - ); - } -} - -class _OpeningExplorerHeader extends StatelessWidget { - const _OpeningExplorerHeader({required this.child, super.key}); - - final Widget child; - - @override - Widget build(BuildContext context) { - return Container( - width: double.infinity, - padding: _kTableRowPadding, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.secondaryContainer, - ), - child: child, - ); - } -} - -class _WinPercentageChart extends StatelessWidget { - const _WinPercentageChart({ - required this.whiteWins, - required this.draws, - required this.blackWins, - }); - - final int whiteWins; - final int draws; - final int blackWins; - - int percentGames(int games) => - ((games / (whiteWins + draws + blackWins)) * 100).round(); - String label(int percent) => percent < 20 ? '' : '$percent%'; - - @override - Widget build(BuildContext context) { - final percentWhite = percentGames(whiteWins); - final percentDraws = percentGames(draws); - final percentBlack = percentGames(blackWins); - - return ClipRRect( - borderRadius: BorderRadius.circular(5), - child: Row( - children: [ - Expanded( - flex: percentWhite, - child: ColoredBox( - color: _whiteBoxColor(context), - child: Text( - label(percentWhite), - textAlign: TextAlign.center, - style: const TextStyle(color: Colors.black), - ), - ), - ), - Expanded( - flex: percentDraws, - child: ColoredBox( - color: Colors.grey, - child: Text( - label(percentDraws), - textAlign: TextAlign.center, - style: const TextStyle(color: Colors.white), - ), - ), - ), - Expanded( - flex: percentBlack, - child: ColoredBox( - color: _blackBoxColor(context), - child: Text( - label(percentBlack), - textAlign: TextAlign.center, - style: const TextStyle(color: Colors.white), - ), - ), - ), - ], - ), - ); - } -} - class _MoveList extends ConsumerWidget implements PreferredSizeWidget { const _MoveList({ required this.pgn, diff --git a/lib/src/view/opening_explorer/opening_explorer_widgets.dart b/lib/src/view/opening_explorer/opening_explorer_widgets.dart new file mode 100644 index 0000000000..f0374e07ab --- /dev/null +++ b/lib/src/view/opening_explorer/opening_explorer_widgets.dart @@ -0,0 +1,504 @@ +import 'package:dartchess/dartchess.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:intl/intl.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; +import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/view/game/archived_game_screen.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; + +const _kTableRowVerticalPadding = 12.0; +const _kTableRowHorizontalPadding = 8.0; +const _kTableRowPadding = EdgeInsets.symmetric( + horizontal: _kTableRowHorizontalPadding, + vertical: _kTableRowVerticalPadding, +); + +Color _whiteBoxColor(BuildContext context) => + Theme.of(context).brightness == Brightness.dark + ? Colors.white.withValues(alpha: 0.8) + : Colors.white; + +Color _blackBoxColor(BuildContext context) => + Theme.of(context).brightness == Brightness.light + ? Colors.black.withValues(alpha: 0.7) + : Colors.black; + +/// Table of moves for the opening explorer. +class OpeningExplorerMoveTable extends ConsumerWidget { + const OpeningExplorerMoveTable({ + required this.moves, + required this.whiteWins, + required this.draws, + required this.blackWins, + required this.pgn, + required this.options, + }) : _isLoading = false, + _maxDepthReached = false; + + const OpeningExplorerMoveTable.loading({ + required this.pgn, + required this.options, + }) : _isLoading = true, + moves = const IListConst([]), + whiteWins = 0, + draws = 0, + blackWins = 0, + _maxDepthReached = false; + + const OpeningExplorerMoveTable.maxDepth({ + required this.pgn, + required this.options, + }) : _isLoading = false, + moves = const IListConst([]), + whiteWins = 0, + draws = 0, + blackWins = 0, + _maxDepthReached = true; + + final IList moves; + final int whiteWins; + final int draws; + final int blackWins; + final String pgn; + final AnalysisOptions options; + + final bool _isLoading; + final bool _maxDepthReached; + + String formatNum(int num) => NumberFormat.decimalPatternDigits().format(num); + + static const columnWidths = { + 0: FractionColumnWidth(0.15), + 1: FractionColumnWidth(0.35), + 2: FractionColumnWidth(0.50), + }; + + @override + Widget build(BuildContext context, WidgetRef ref) { + if (_isLoading) { + return loadingTable; + } + + final games = whiteWins + draws + blackWins; + final ctrlProvider = analysisControllerProvider(pgn, options); + + const topPadding = EdgeInsets.only(top: _kTableRowVerticalPadding / 2); + const headerTextStyle = TextStyle(fontSize: 12); + + return Table( + columnWidths: columnWidths, + children: [ + TableRow( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.secondaryContainer, + ), + children: [ + Padding( + padding: _kTableRowPadding.subtract(topPadding), + child: Text(context.l10n.move, style: headerTextStyle), + ), + Padding( + padding: _kTableRowPadding.subtract(topPadding), + child: Text(context.l10n.games, style: headerTextStyle), + ), + Padding( + padding: _kTableRowPadding.subtract(topPadding), + child: Text(context.l10n.whiteDrawBlack, style: headerTextStyle), + ), + ], + ), + ...List.generate( + moves.length, + (int index) { + final move = moves.get(index); + final percentGames = ((move.games / games) * 100).round(); + return TableRow( + decoration: BoxDecoration( + color: index.isEven + ? Theme.of(context).colorScheme.surfaceContainerLow + : Theme.of(context).colorScheme.surfaceContainerHigh, + ), + children: [ + TableRowInkWell( + onTap: () => ref + .read(ctrlProvider.notifier) + .onUserMove(NormalMove.fromUci(move.uci)), + child: Padding( + padding: _kTableRowPadding, + child: Text(move.san), + ), + ), + TableRowInkWell( + onTap: () => ref + .read(ctrlProvider.notifier) + .onUserMove(NormalMove.fromUci(move.uci)), + child: Padding( + padding: _kTableRowPadding, + child: Text('${formatNum(move.games)} ($percentGames%)'), + ), + ), + TableRowInkWell( + onTap: () => ref + .read(ctrlProvider.notifier) + .onUserMove(NormalMove.fromUci(move.uci)), + child: Padding( + padding: _kTableRowPadding, + child: _WinPercentageChart( + whiteWins: move.white, + draws: move.draws, + blackWins: move.black, + ), + ), + ), + ], + ); + }, + ), + if (_maxDepthReached) + TableRow( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerLow, + ), + children: [ + Padding( + padding: _kTableRowPadding, + child: Text( + String.fromCharCode(Icons.not_interested_outlined.codePoint), + style: TextStyle( + fontFamily: Icons.not_interested_outlined.fontFamily, + ), + ), + ), + Padding( + padding: _kTableRowPadding, + child: Text(context.l10n.maxDepthReached), + ), + const Padding( + padding: _kTableRowPadding, + child: SizedBox.shrink(), + ), + ], + ) + else if (moves.isNotEmpty) + TableRow( + decoration: BoxDecoration( + color: moves.length.isEven + ? Theme.of(context).colorScheme.surfaceContainerLow + : Theme.of(context).colorScheme.surfaceContainerHigh, + ), + children: [ + Container( + padding: _kTableRowPadding, + alignment: Alignment.centerLeft, + child: const Icon(Icons.functions), + ), + Padding( + padding: _kTableRowPadding, + child: Text('${formatNum(games)} (100%)'), + ), + Padding( + padding: _kTableRowPadding, + child: _WinPercentageChart( + whiteWins: whiteWins, + draws: draws, + blackWins: blackWins, + ), + ), + ], + ) + else + TableRow( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerLow, + ), + children: [ + Padding( + padding: _kTableRowPadding, + child: Text( + String.fromCharCode(Icons.not_interested_outlined.codePoint), + style: TextStyle( + fontFamily: Icons.not_interested_outlined.fontFamily, + ), + ), + ), + Padding( + padding: _kTableRowPadding, + child: Text(context.l10n.noGameFound), + ), + const Padding( + padding: _kTableRowPadding, + child: SizedBox.shrink(), + ), + ], + ), + ], + ); + } + + static final loadingTable = Table( + columnWidths: columnWidths, + children: List.generate( + 10, + (int index) => TableRow( + children: [ + Padding( + padding: _kTableRowPadding, + child: Container( + height: 20, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(5), + ), + ), + ), + Padding( + padding: _kTableRowPadding, + child: Container( + height: 20, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(5), + ), + ), + ), + Padding( + padding: _kTableRowPadding, + child: Container( + height: 20, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(5), + ), + ), + ), + ], + ), + ), + ); +} + +class OpeningExplorerHeaderTile extends StatelessWidget { + const OpeningExplorerHeaderTile({required this.child, super.key}); + + final Widget child; + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + padding: _kTableRowPadding, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.secondaryContainer, + ), + child: child, + ); + } +} + +/// A game tile for the opening explorer. +class OpeningExplorerGameTile extends ConsumerStatefulWidget { + const OpeningExplorerGameTile({ + required this.game, + required this.color, + required this.ply, + super.key, + }); + + final OpeningExplorerGame game; + final Color color; + final int ply; + + @override + ConsumerState createState() => + _OpeningExplorerGameTileState(); +} + +class _OpeningExplorerGameTileState + extends ConsumerState { + @override + Widget build(BuildContext context) { + const widthResultBox = 50.0; + const paddingResultBox = EdgeInsets.all(5); + + return Container( + padding: _kTableRowPadding, + color: widget.color, + child: AdaptiveInkWell( + onTap: () { + pushPlatformRoute( + context, + builder: (_) => ArchivedGameScreen( + gameId: widget.game.id, + orientation: Side.white, + initialCursor: widget.ply, + ), + ); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(widget.game.white.rating.toString()), + Text(widget.game.black.rating.toString()), + ], + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.game.white.name, + overflow: TextOverflow.ellipsis, + ), + Text( + widget.game.black.name, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + Row( + children: [ + if (widget.game.winner == 'white') + Container( + width: widthResultBox, + padding: paddingResultBox, + decoration: BoxDecoration( + color: _whiteBoxColor(context), + borderRadius: BorderRadius.circular(5), + ), + child: const Text( + '1-0', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.black, + ), + ), + ) + else if (widget.game.winner == 'black') + Container( + width: widthResultBox, + padding: paddingResultBox, + decoration: BoxDecoration( + color: _blackBoxColor(context), + borderRadius: BorderRadius.circular(5), + ), + child: const Text( + '0-1', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + ), + ), + ) + else + Container( + width: widthResultBox, + padding: paddingResultBox, + decoration: BoxDecoration( + color: Colors.grey, + borderRadius: BorderRadius.circular(5), + ), + child: const Text( + '½-½', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + ), + ), + ), + if (widget.game.month != null) ...[ + const SizedBox(width: 10.0), + Text( + widget.game.month!, + style: const TextStyle( + fontFeatures: [FontFeature.tabularFigures()], + ), + ), + ], + if (widget.game.speed != null) ...[ + const SizedBox(width: 10.0), + Icon(widget.game.speed!.icon, size: 20), + ], + ], + ), + ], + ), + ), + ); + } +} + +class _WinPercentageChart extends StatelessWidget { + const _WinPercentageChart({ + required this.whiteWins, + required this.draws, + required this.blackWins, + }); + + final int whiteWins; + final int draws; + final int blackWins; + + int percentGames(int games) => + ((games / (whiteWins + draws + blackWins)) * 100).round(); + String label(int percent) => percent < 20 ? '' : '$percent%'; + + @override + Widget build(BuildContext context) { + final percentWhite = percentGames(whiteWins); + final percentDraws = percentGames(draws); + final percentBlack = percentGames(blackWins); + + return ClipRRect( + borderRadius: BorderRadius.circular(5), + child: Row( + children: [ + Expanded( + flex: percentWhite, + child: ColoredBox( + color: _whiteBoxColor(context), + child: Text( + label(percentWhite), + textAlign: TextAlign.center, + style: const TextStyle(color: Colors.black), + ), + ), + ), + Expanded( + flex: percentDraws, + child: ColoredBox( + color: Colors.grey, + child: Text( + label(percentDraws), + textAlign: TextAlign.center, + style: const TextStyle(color: Colors.white), + ), + ), + ), + Expanded( + flex: percentBlack, + child: ColoredBox( + color: _blackBoxColor(context), + child: Text( + label(percentBlack), + textAlign: TextAlign.center, + style: const TextStyle(color: Colors.white), + ), + ), + ), + ], + ), + ); + } +} From ce6743e464cc814fb60ed9838a5b2a86e9ccdc69 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 20 Nov 2024 17:42:24 +0100 Subject: [PATCH 727/979] More work on analysis explorer view --- .../opening_explorer_repository.dart | 2 + lib/src/view/analysis/analysis_layout.dart | 39 ++- lib/src/view/analysis/analysis_screen.dart | 26 +- lib/src/view/analysis/analysis_settings.dart | 23 +- .../view/analysis/opening_explorer_view.dart | 236 ++++++++++++++++++ .../opening_explorer_screen.dart | 2 +- .../opening_explorer_widgets.dart | 9 +- 7 files changed, 299 insertions(+), 38 deletions(-) create mode 100644 lib/src/view/analysis/opening_explorer_view.dart diff --git a/lib/src/model/opening_explorer/opening_explorer_repository.dart b/lib/src/model/opening_explorer/opening_explorer_repository.dart index f8f6d5210b..a6b0921fc5 100644 --- a/lib/src/model/opening_explorer/opening_explorer_repository.dart +++ b/lib/src/model/opening_explorer/opening_explorer_repository.dart @@ -8,6 +8,7 @@ import 'package:lichess_mobile/src/model/common/speed.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_preferences.dart'; import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/utils/riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'opening_explorer_repository.g.dart'; @@ -20,6 +21,7 @@ class OpeningExplorer extends _$OpeningExplorer { Future<({OpeningExplorerEntry entry, bool isIndexing})?> build({ required String fen, }) async { + await ref.debounce(const Duration(milliseconds: 300)); ref.onDispose(() { _openingExplorerSubscription?.cancel(); }); diff --git a/lib/src/view/analysis/analysis_layout.dart b/lib/src/view/analysis/analysis_layout.dart index fbd27d9898..5ca457b409 100644 --- a/lib/src/view/analysis/analysis_layout.dart +++ b/lib/src/view/analysis/analysis_layout.dart @@ -1,5 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:lichess_mobile/l10n/l10n.dart'; import 'package:lichess_mobile/src/constants.dart'; +import 'package:lichess_mobile/src/styles/lichess_icons.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; @@ -16,14 +19,27 @@ typedef EngineGaugeBuilder = Widget Function( Orientation orientation, ); -class AnalysisTab { - const AnalysisTab({ - required this.title, - required this.icon, - }); +enum AnalysisTab { + opening(Icons.explore), + moves(LichessIcons.flow_cascade), + summary(Icons.area_chart); + + const AnalysisTab(this.icon); - final String title; final IconData icon; + + String l10n(AppLocalizations l10n) { + switch (this) { + case AnalysisTab.opening: + return l10n.openingExplorer; + case AnalysisTab.moves: + // TODO: Add l10n + return 'Moves'; + case AnalysisTab.summary: + // TODO: Add l10n + return 'Summary'; + } + } } /// Indicator for the analysis tab, typically shown in the app bar. @@ -69,17 +85,16 @@ class _AppBarAnalysisTabIndicatorState Widget build(BuildContext context) { return AppBarIconButton( icon: Icon(widget.tabs[widget.controller.index].icon), - semanticsLabel: widget.tabs[widget.controller.index].title, + semanticsLabel: widget.tabs[widget.controller.index].l10n(context.l10n), onPressed: () { showAdaptiveActionSheet( context: context, actions: widget.tabs.map((tab) { return BottomSheetAction( leading: Icon(tab.icon), - makeLabel: (_) => Text(tab.title), + makeLabel: (context) => Text(tab.l10n(context.l10n)), onPressed: (_) { widget.controller.animateTo(widget.tabs.indexOf(tab)); - Navigator.of(context).pop(); }, ); }).toList(), @@ -101,12 +116,16 @@ class AnalysisLayout extends StatelessWidget { super.key, }); + /// The tab controller for the tab view. final TabController tabController; /// The builder for the board widget. final BoardBuilder boardBuilder; - /// The children of the tab bar view. + /// The children of the tab view. + /// + /// The length of this list must match the [controller]'s [TabController.length] + /// and the length of the [AppBarAnalysisTabIndicator.tabs] list. final List children; final EngineGaugeBuilder? engineGaugeBuilder; diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index 886fb8005d..d9a6476420 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -8,11 +8,11 @@ import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/game/game_repository_providers.dart'; import 'package:lichess_mobile/src/model/game/game_share_service.dart'; import 'package:lichess_mobile/src/network/http.dart'; -import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_layout.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_share_screen.dart'; +import 'package:lichess_mobile/src/view/analysis/opening_explorer_view.dart'; import 'package:lichess_mobile/src/view/analysis/server_analysis.dart'; import 'package:lichess_mobile/src/view/board_editor/board_editor_screen.dart'; import 'package:lichess_mobile/src/view/engine/engine_depth.dart'; @@ -131,18 +131,16 @@ class _LoadedAnalysisScreenState extends ConsumerState<_LoadedAnalysisScreen> void initState() { super.initState(); tabs = [ - const AnalysisTab( - title: 'Moves', - icon: LichessIcons.flow_cascade, - ), - if (widget.options.canShowGameSummary) - const AnalysisTab( - title: 'Summary', - icon: Icons.area_chart, - ), + AnalysisTab.opening, + AnalysisTab.moves, + if (widget.options.canShowGameSummary) AnalysisTab.summary, ]; - _tabController = TabController(vsync: this, length: tabs.length); + _tabController = TabController( + vsync: this, + initialIndex: 1, + length: tabs.length, + ); } @override @@ -274,6 +272,7 @@ class _Body extends ConsumerWidget { : null, bottomBar: _BottomBar(pgn: pgn, options: options), children: [ + OpeningExplorerView(pgn: pgn, options: options), AnalysisTreeView(pgn, options), if (options.canShowGameSummary) ServerAnalysisSummary(pgn, options), ], @@ -304,6 +303,11 @@ class _BottomBar extends ConsumerWidget { }, icon: Icons.menu, ), + BottomBarButton( + label: context.l10n.flipBoard, + onTap: () => ref.read(ctrlProvider.notifier).toggleBoard(), + icon: CupertinoIcons.arrow_2_squarepath, + ), RepeatButton( onLongPress: analysisState.canGoBack ? () => _moveBackward(ref) : null, diff --git a/lib/src/view/analysis/analysis_settings.dart b/lib/src/view/analysis/analysis_settings.dart index f80fb44473..5f7b409bc2 100644 --- a/lib/src/view/analysis/analysis_settings.dart +++ b/lib/src/view/analysis/analysis_settings.dart @@ -4,8 +4,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; -import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_settings.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/non_linear_slider.dart'; @@ -26,12 +26,20 @@ class AnalysisSettings extends ConsumerWidget { ctrlProvider.select((s) => s.isEngineAvailable), ); final prefs = ref.watch(analysisPreferencesProvider); - final isSoundEnabled = ref.watch( - generalPreferencesProvider.select((pref) => pref.isSoundEnabled), - ); return BottomSheetScrollableContainer( children: [ + PlatformListTile( + title: Text(context.l10n.openingExplorer), + onTap: () => showAdaptiveBottomSheet( + context: context, + isScrollControlled: true, + showDragHandle: true, + isDismissible: true, + builder: (_) => OpeningExplorerSettings(pgn, options), + ), + trailing: const Icon(CupertinoIcons.chevron_right), + ), SwitchSettingTile( title: Text(context.l10n.toggleLocalEvaluation), value: prefs.enableLocalEvaluation, @@ -128,13 +136,6 @@ class AnalysisSettings extends ConsumerWidget { .read(analysisPreferencesProvider.notifier) .togglePgnComments(), ), - SwitchSettingTile( - title: Text(context.l10n.sound), - value: isSoundEnabled, - onChanged: (value) { - ref.read(generalPreferencesProvider.notifier).toggleSoundEnabled(); - }, - ), ], ); } diff --git a/lib/src/view/analysis/opening_explorer_view.dart b/lib/src/view/analysis/opening_explorer_view.dart new file mode 100644 index 0000000000..80c0b0a964 --- /dev/null +++ b/lib/src/view/analysis/opening_explorer_view.dart @@ -0,0 +1,236 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/constants.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; +import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; +import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_preferences.dart'; +import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_repository.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/screen.dart'; +import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_widgets.dart'; +import 'package:lichess_mobile/src/widgets/shimmer.dart'; + +const _kTableRowVerticalPadding = 12.0; +const _kTableRowHorizontalPadding = 8.0; +const _kTableRowPadding = EdgeInsets.symmetric( + horizontal: _kTableRowHorizontalPadding, + vertical: _kTableRowVerticalPadding, +); + +class OpeningExplorerView extends ConsumerStatefulWidget { + const OpeningExplorerView({required this.pgn, required this.options}); + + final String pgn; + final AnalysisOptions options; + + @override + ConsumerState createState() => _OpeningExplorerState(); +} + +class _OpeningExplorerState extends ConsumerState { + final Map cache = {}; + + /// Last explorer content that was successfully loaded. This is used to + /// display a loading indicator while the new content is being fetched. + List? lastExplorerWidgets; + + @override + Widget build(BuildContext context) { + final analysisState = + ref.watch(analysisControllerProvider(widget.pgn, widget.options)); + + if (analysisState.position.ply >= 50) { + return _OpeningExplorerView( + isLoading: false, + children: [ + OpeningExplorerMoveTable.maxDepth( + pgn: widget.pgn, + options: widget.options, + ), + ], + ); + } + + final prefs = ref.watch(openingExplorerPreferencesProvider); + + if (prefs.db == OpeningDatabase.player && prefs.playerDb.username == null) { + return const _OpeningExplorerView( + isLoading: false, + children: [ + Padding( + padding: _kTableRowPadding, + child: Center( + // TODO: l10n + child: Text('Select a Lichess player in the settings.'), + ), + ), + ], + ); + } + + final cacheKey = OpeningExplorerCacheKey( + fen: analysisState.position.fen, + prefs: prefs, + ); + final cacheOpeningExplorer = cache[cacheKey]; + final openingExplorerAsync = cacheOpeningExplorer != null + ? AsyncValue.data( + (entry: cacheOpeningExplorer, isIndexing: false), + ) + : ref.watch(openingExplorerProvider(fen: analysisState.position.fen)); + + if (cacheOpeningExplorer == null) { + ref.listen(openingExplorerProvider(fen: analysisState.position.fen), + (_, curAsync) { + curAsync.whenData((cur) { + if (cur != null && !cur.isIndexing) { + cache[cacheKey] = cur.entry; + } + }); + }); + } + + final isLoading = + openingExplorerAsync.isLoading || openingExplorerAsync.value == null; + + return _OpeningExplorerView( + isLoading: isLoading, + children: openingExplorerAsync.when( + data: (openingExplorer) { + if (openingExplorer == null) { + return lastExplorerWidgets ?? + [ + Shimmer( + child: ShimmerLoading( + isLoading: true, + child: OpeningExplorerMoveTable.loading( + pgn: widget.pgn, + options: widget.options, + ), + ), + ), + ]; + } + + final topGames = openingExplorer.entry.topGames; + final recentGames = openingExplorer.entry.recentGames; + + final ply = analysisState.position.ply; + + final children = [ + OpeningExplorerMoveTable( + moves: openingExplorer.entry.moves, + whiteWins: openingExplorer.entry.white, + draws: openingExplorer.entry.draws, + blackWins: openingExplorer.entry.black, + pgn: widget.pgn, + options: widget.options, + ), + if (topGames != null && topGames.isNotEmpty) ...[ + OpeningExplorerHeaderTile( + key: const Key('topGamesHeader'), + child: Text(context.l10n.topGames), + ), + ...List.generate( + topGames.length, + (int index) { + return OpeningExplorerGameTile( + key: Key('top-game-${topGames.get(index).id}'), + game: topGames.get(index), + color: index.isEven + ? Theme.of(context).colorScheme.surfaceContainerLow + : Theme.of(context).colorScheme.surfaceContainerHigh, + ply: ply, + ); + }, + growable: false, + ), + ], + if (recentGames != null && recentGames.isNotEmpty) ...[ + OpeningExplorerHeaderTile( + key: const Key('recentGamesHeader'), + child: Text(context.l10n.recentGames), + ), + ...List.generate( + recentGames.length, + (int index) { + return OpeningExplorerGameTile( + key: Key('recent-game-${recentGames.get(index).id}'), + game: recentGames.get(index), + color: index.isEven + ? Theme.of(context).colorScheme.surfaceContainerLow + : Theme.of(context).colorScheme.surfaceContainerHigh, + ply: ply, + ); + }, + growable: false, + ), + ], + ]; + + lastExplorerWidgets = children; + + return children; + }, + loading: () => + lastExplorerWidgets ?? + [ + Shimmer( + child: ShimmerLoading( + isLoading: true, + child: OpeningExplorerMoveTable.loading( + pgn: widget.pgn, + options: widget.options, + ), + ), + ), + ], + error: (e, s) { + debugPrint( + 'SEVERE: [OpeningExplorerView] could not load opening explorer data; $e\n$s', + ); + return [ + Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Text(e.toString()), + ), + ), + ]; + }, + ), + ); + } +} + +class _OpeningExplorerView extends StatelessWidget { + const _OpeningExplorerView({ + required this.children, + required this.isLoading, + }); + + final List children; + final bool isLoading; + + @override + Widget build(BuildContext context) { + final isTablet = isTabletOrLarger(context); + final loadingOverlay = Positioned.fill( + child: IgnorePointer(ignoring: !isLoading), + ); + + return Stack( + children: [ + ListView( + padding: isTablet + ? const EdgeInsets.symmetric( + horizontal: kTabletBoardTableSidePadding, + ) + : EdgeInsets.zero, + children: children, + ), + loadingOverlay, + ], + ); + } +} diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index 341afecb60..9ade504e4e 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -86,7 +86,7 @@ class _OpeningExplorerState extends ConsumerState { const SizedBox(width: 6.0), Expanded( child: Text( - '${opening.eco.isEmpty ? "" : "${opening.eco} "}${opening.name}', + opening.name, style: TextStyle( color: Theme.of(context).colorScheme.onSecondaryContainer, diff --git a/lib/src/view/opening_explorer/opening_explorer_widgets.dart b/lib/src/view/opening_explorer/opening_explorer_widgets.dart index f0374e07ab..2b19204c49 100644 --- a/lib/src/view/opening_explorer/opening_explorer_widgets.dart +++ b/lib/src/view/opening_explorer/opening_explorer_widgets.dart @@ -10,7 +10,7 @@ import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/game/archived_game_screen.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; -const _kTableRowVerticalPadding = 12.0; +const _kTableRowVerticalPadding = 10.0; const _kTableRowHorizontalPadding = 8.0; const _kTableRowPadding = EdgeInsets.symmetric( horizontal: _kTableRowHorizontalPadding, @@ -86,7 +86,6 @@ class OpeningExplorerMoveTable extends ConsumerWidget { final games = whiteWins + draws + blackWins; final ctrlProvider = analysisControllerProvider(pgn, options); - const topPadding = EdgeInsets.only(top: _kTableRowVerticalPadding / 2); const headerTextStyle = TextStyle(fontSize: 12); return Table( @@ -98,15 +97,15 @@ class OpeningExplorerMoveTable extends ConsumerWidget { ), children: [ Padding( - padding: _kTableRowPadding.subtract(topPadding), + padding: _kTableRowPadding, child: Text(context.l10n.move, style: headerTextStyle), ), Padding( - padding: _kTableRowPadding.subtract(topPadding), + padding: _kTableRowPadding, child: Text(context.l10n.games, style: headerTextStyle), ), Padding( - padding: _kTableRowPadding.subtract(topPadding), + padding: _kTableRowPadding, child: Text(context.l10n.whiteDrawBlack, style: headerTextStyle), ), ], From 417ce8ba5c4bfe4b381e0b0be5176454353dfada Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 20 Nov 2024 17:47:03 +0100 Subject: [PATCH 728/979] Remove unused code --- .../model/analysis/analysis_controller.dart | 25 ------------------- lib/src/view/analysis/analysis_settings.dart | 1 - 2 files changed, 26 deletions(-) diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index 3f937ca4c7..62cd5bafab 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -163,7 +163,6 @@ class AnalysisController extends _$AnalysisController contextOpening: options.opening, isLocalEvaluationAllowed: options.isLocalEvaluationAllowed, isLocalEvaluationEnabled: prefs.enableLocalEvaluation, - displayMode: DisplayMode.moves, playersAnalysis: options.serverAnalysis, acplChartData: options.serverAnalysis != null ? _makeAcplChartData() : null, @@ -380,10 +379,6 @@ class AnalysisController extends _$AnalysisController state = state.copyWith(pgnHeaders: headers); } - void setDisplayMode(DisplayMode mode) { - state = state.copyWith(displayMode: mode); - } - Future requestServerAnalysis() { if (state.canRequestServerAnalysis) { final service = ref.read(serverAnalysisServiceProvider); @@ -649,11 +644,6 @@ class AnalysisController extends _$AnalysisController } } -enum DisplayMode { - moves, - summary, -} - @freezed class AnalysisState with _$AnalysisState { const AnalysisState._(); @@ -690,11 +680,6 @@ class AnalysisState with _$AnalysisState { /// Whether the user has enabled local evaluation. required bool isLocalEvaluationEnabled, - /// The display mode of the analysis. - /// - /// It can be either moves, summary or opening explorer. - required DisplayMode displayMode, - /// The last move played. Move? lastMove, @@ -743,8 +728,6 @@ class AnalysisState with _$AnalysisState { !hasServerAnalysis && pgnHeaders['Result'] != '*'; - bool get canShowGameSummary => hasServerAnalysis || canRequestServerAnalysis; - bool get hasServerAnalysis => playersAnalysis != null; /// Whether an evaluation can be available @@ -771,14 +754,6 @@ class AnalysisState with _$AnalysisState { position: position, savedEval: currentNode.eval ?? currentNode.serverEval, ); - - AnalysisOptions get openingExplorerOptions => AnalysisOptions( - id: standaloneOpeningExplorerId, - isLocalEvaluationAllowed: false, - orientation: pov, - variant: variant, - initialMoveCursor: currentPath.size, - ); } @freezed diff --git a/lib/src/view/analysis/analysis_settings.dart b/lib/src/view/analysis/analysis_settings.dart index 5f7b409bc2..415548cb49 100644 --- a/lib/src/view/analysis/analysis_settings.dart +++ b/lib/src/view/analysis/analysis_settings.dart @@ -1,5 +1,4 @@ import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; From d4383bf61f8334f39b1ffabec4b95ad6bebbf0bb Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 20 Nov 2024 17:51:55 +0100 Subject: [PATCH 729/979] Fix explorer tests --- .../opening_explorer_screen_test.dart | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/test/view/opening_explorer/opening_explorer_screen_test.dart b/test/view/opening_explorer/opening_explorer_screen_test.dart index a442d6f50e..32b7e88a4f 100644 --- a/test/view/opening_explorer/opening_explorer_screen_test.dart +++ b/test/view/opening_explorer/opening_explorer_screen_test.dart @@ -74,8 +74,8 @@ void main() { ); await tester.pumpWidget(app); - // wait for opening explorer data to load - await tester.pump(const Duration(milliseconds: 50)); + // wait for opening explorer data to load (taking debounce delay into account) + await tester.pump(const Duration(milliseconds: 350)); final moves = [ 'e4', @@ -102,8 +102,6 @@ void main() { // find.byType(OpeningExplorerGameTile), // findsNWidgets(2), // ); - - await tester.pump(const Duration(milliseconds: 50)); }, variant: kPlatformVariant, ); @@ -133,8 +131,8 @@ void main() { ); await tester.pumpWidget(app); - // wait for opening explorer data to load - await tester.pump(const Duration(milliseconds: 50)); + // wait for opening explorer data to load (taking debounce delay into account) + await tester.pump(const Duration(milliseconds: 350)); final moves = [ 'd4', @@ -157,8 +155,6 @@ void main() { // find.byType(OpeningExplorerGameTile), // findsOneWidget, // ); - - await tester.pump(const Duration(milliseconds: 50)); }, variant: kPlatformVariant, ); @@ -189,8 +185,8 @@ void main() { ); await tester.pumpWidget(app); - // wait for opening explorer data to load - await tester.pump(const Duration(milliseconds: 50)); + // wait for opening explorer data to load (taking debounce delay into account) + await tester.pump(const Duration(milliseconds: 350)); final moves = [ 'c4', @@ -213,8 +209,6 @@ void main() { // find.byType(OpeningExplorerGameTile), // findsOneWidget, // ); - - await tester.pump(const Duration(milliseconds: 50)); }, variant: kPlatformVariant, ); From 38150373572f76bcf535e68d5e7797d32adde783 Mon Sep 17 00:00:00 2001 From: Jimima Date: Wed, 20 Nov 2024 20:33:44 +0000 Subject: [PATCH 730/979] Appeased linter --- lib/src/view/game/game_settings.dart | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lib/src/view/game/game_settings.dart b/lib/src/view/game/game_settings.dart index 4283166cc4..d8eed2bd0c 100644 --- a/lib/src/view/game/game_settings.dart +++ b/lib/src/view/game/game_settings.dart @@ -1,19 +1,15 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lichess_mobile/l10n/l10n.dart'; import 'package:lichess_mobile/src/model/account/account_preferences.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/game/game_controller.dart'; import 'package:lichess_mobile/src/model/game/game_preferences.dart'; -import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/settings/board_settings_screen.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/settings.dart'; - -import '../../widgets/adaptive_choice_picker.dart'; import 'game_screen_providers.dart'; class GameSettings extends ConsumerWidget { @@ -25,7 +21,6 @@ class GameSettings extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final gamePrefs = ref.watch(gamePreferencesProvider); final userPrefsAsync = ref.watch(userGamePrefsProvider(id)); - final boardPrefs = ref.watch(boardPreferencesProvider); return BottomSheetScrollableContainer( children: [ From bd40e1ac9196dd40879ea662277bc67e4262dfdc Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 20 Nov 2024 22:14:02 +0100 Subject: [PATCH 731/979] Start to simplify analysis controller interface --- .../model/analysis/analysis_controller.dart | 6 ++- lib/src/model/game/game_controller.dart | 1 + lib/src/view/analysis/analysis_board.dart | 4 +- lib/src/view/analysis/analysis_screen.dart | 54 ++++++++----------- lib/src/view/analysis/analysis_settings.dart | 7 ++- .../view/analysis/analysis_share_screen.dart | 17 +++--- .../view/analysis/opening_explorer_view.dart | 10 +--- lib/src/view/analysis/server_analysis.dart | 22 ++++---- lib/src/view/analysis/tree_view.dart | 8 +-- .../board_editor/board_editor_screen.dart | 1 + .../offline_correspondence_game_screen.dart | 1 + lib/src/view/game/archived_game_screen.dart | 1 + lib/src/view/game/game_list_tile.dart | 1 + lib/src/view/game/game_result_dialog.dart | 1 + .../opening_explorer_screen.dart | 48 +++++------------ .../opening_explorer_settings.dart | 3 +- .../opening_explorer_widgets.dart | 6 +-- lib/src/view/puzzle/puzzle_screen.dart | 1 + lib/src/view/puzzle/streak_screen.dart | 1 + lib/src/view/study/study_bottom_bar.dart | 1 + lib/src/view/tools/load_position_screen.dart | 4 +- lib/src/view/tools/tools_tab_screen.dart | 3 +- test/view/analysis/analysis_screen_test.dart | 3 ++ .../opening_explorer_screen_test.dart | 4 +- 24 files changed, 82 insertions(+), 126 deletions(-) diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index 62cd5bafab..6be0809cce 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -39,6 +39,8 @@ bool _isStandaloneAnalysis(StringId id) => class AnalysisOptions with _$AnalysisOptions { const AnalysisOptions._(); const factory AnalysisOptions({ + required String pgn, + /// The ID of the analysis. Can be a game ID or a standalone ID. required StringId id, required bool isLocalEvaluationAllowed, @@ -73,7 +75,7 @@ class AnalysisController extends _$AnalysisController Timer? _startEngineEvalTimer; @override - AnalysisState build(String pgn, AnalysisOptions options) { + AnalysisState build(AnalysisOptions options) { final evaluationService = ref.watch(evaluationServiceProvider); final serverAnalysisService = ref.watch(serverAnalysisServiceProvider); @@ -97,7 +99,7 @@ class AnalysisController extends _$AnalysisController Move? lastMove; final game = PgnGame.parsePgn( - pgn, + options.pgn, initHeaders: () => options.isLichessGameAnalysis ? {} : { diff --git a/lib/src/model/game/game_controller.dart b/lib/src/model/game/game_controller.dart index 51a8cbb023..60a7efd0b7 100644 --- a/lib/src/model/game/game_controller.dart +++ b/lib/src/model/game/game_controller.dart @@ -1183,6 +1183,7 @@ class GameState with _$GameState { String get analysisPgn => game.makePgn(); AnalysisOptions get analysisOptions => AnalysisOptions( + pgn: analysisPgn, isLocalEvaluationAllowed: true, variant: game.meta.variant, initialMoveCursor: stepCursor, diff --git a/lib/src/view/analysis/analysis_board.dart b/lib/src/view/analysis/analysis_board.dart index bfb74f672e..6e60630a7e 100644 --- a/lib/src/view/analysis/analysis_board.dart +++ b/lib/src/view/analysis/analysis_board.dart @@ -15,14 +15,12 @@ import 'package:lichess_mobile/src/widgets/pgn.dart'; class AnalysisBoard extends ConsumerStatefulWidget { const AnalysisBoard( - this.pgn, this.options, this.boardSize, { this.borderRadius, this.enableDrawingShapes = true, }); - final String pgn; final AnalysisOptions options; final double boardSize; final BorderRadiusGeometry? borderRadius; @@ -38,7 +36,7 @@ class AnalysisBoardState extends ConsumerState { @override Widget build(BuildContext context) { - final ctrlProvider = analysisControllerProvider(widget.pgn, widget.options); + final ctrlProvider = analysisControllerProvider(widget.options); final analysisState = ref.watch(ctrlProvider); final boardPrefs = ref.watch(boardPreferencesProvider); final showBestMoveArrow = ref.watch( diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index d9a6476420..2276f60d94 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -55,8 +55,9 @@ class AnalysisScreen extends StatelessWidget { enableDrawingShapes: enableDrawingShapes, ) : _LoadedAnalysisScreen( - options: options, - pgn: pgnOrId, + options: options.copyWith( + pgn: pgnOrId, + ), enableDrawingShapes: enableDrawingShapes, ); } @@ -90,8 +91,8 @@ class _LoadGame extends ConsumerWidget { opening: game.meta.opening, division: game.meta.division, serverAnalysis: serverAnalysis, + pgn: game.makePgn(), ), - pgn: game.makePgn(), enableDrawingShapes: enableDrawingShapes, ); }, @@ -108,12 +109,10 @@ class _LoadGame extends ConsumerWidget { class _LoadedAnalysisScreen extends ConsumerStatefulWidget { const _LoadedAnalysisScreen({ required this.options, - required this.pgn, required this.enableDrawingShapes, }); final AnalysisOptions options; - final String pgn; final bool enableDrawingShapes; @@ -151,7 +150,7 @@ class _LoadedAnalysisScreenState extends ConsumerState<_LoadedAnalysisScreen> @override Widget build(BuildContext context) { - final ctrlProvider = analysisControllerProvider(widget.pgn, widget.options); + final ctrlProvider = analysisControllerProvider(widget.options); final currentNodeEval = ref.watch(ctrlProvider.select((value) => value.currentNode.eval)); @@ -171,7 +170,7 @@ class _LoadedAnalysisScreenState extends ConsumerState<_LoadedAnalysisScreen> isScrollControlled: true, showDragHandle: true, isDismissible: true, - builder: (_) => AnalysisSettings(widget.pgn, widget.options), + builder: (_) => AnalysisSettings(widget.options), ), semanticsLabel: context.l10n.settingsSettings, icon: const Icon(Icons.settings), @@ -180,7 +179,6 @@ class _LoadedAnalysisScreenState extends ConsumerState<_LoadedAnalysisScreen> ), body: _Body( controller: _tabController, - pgn: widget.pgn, options: widget.options, enableDrawingShapes: widget.enableDrawingShapes, ), @@ -212,13 +210,11 @@ class _Title extends StatelessWidget { class _Body extends ConsumerWidget { const _Body({ required this.controller, - required this.pgn, required this.options, required this.enableDrawingShapes, }); final TabController controller; - final String pgn; final AnalysisOptions options; final bool enableDrawingShapes; @@ -228,7 +224,7 @@ class _Body extends ConsumerWidget { analysisPreferencesProvider.select((value) => value.showEvaluationGauge), ); - final ctrlProvider = analysisControllerProvider(pgn, options); + final ctrlProvider = analysisControllerProvider(options); final analysisState = ref.watch(ctrlProvider); final isEngineAvailable = analysisState.isEngineAvailable; @@ -238,7 +234,6 @@ class _Body extends ConsumerWidget { return AnalysisLayout( tabController: controller, boardBuilder: (context, boardSize, borderRadius) => AnalysisBoard( - pgn, options, boardSize, borderRadius: borderRadius, @@ -270,28 +265,24 @@ class _Body extends ConsumerWidget { isGameOver: currentNode.position.isGameOver, ) : null, - bottomBar: _BottomBar(pgn: pgn, options: options), + bottomBar: _BottomBar(options: options), children: [ - OpeningExplorerView(pgn: pgn, options: options), - AnalysisTreeView(pgn, options), - if (options.canShowGameSummary) ServerAnalysisSummary(pgn, options), + OpeningExplorerView(options: options), + AnalysisTreeView(options), + if (options.canShowGameSummary) ServerAnalysisSummary(options), ], ); } } class _BottomBar extends ConsumerWidget { - const _BottomBar({ - required this.pgn, - required this.options, - }); + const _BottomBar({required this.options}); - final String pgn; final AnalysisOptions options; @override Widget build(BuildContext context, WidgetRef ref) { - final ctrlProvider = analysisControllerProvider(pgn, options); + final ctrlProvider = analysisControllerProvider(options); final analysisState = ref.watch(ctrlProvider); return BottomBar( @@ -334,10 +325,9 @@ class _BottomBar extends ConsumerWidget { } void _moveForward(WidgetRef ref) => - ref.read(analysisControllerProvider(pgn, options).notifier).userNext(); - void _moveBackward(WidgetRef ref) => ref - .read(analysisControllerProvider(pgn, options).notifier) - .userPrevious(); + ref.read(analysisControllerProvider(options).notifier).userNext(); + void _moveBackward(WidgetRef ref) => + ref.read(analysisControllerProvider(options).notifier).userPrevious(); Future _showAnalysisMenu(BuildContext context, WidgetRef ref) { return showAdaptiveActionSheet( @@ -347,15 +337,14 @@ class _BottomBar extends ConsumerWidget { makeLabel: (context) => Text(context.l10n.flipBoard), onPressed: (context) { ref - .read(analysisControllerProvider(pgn, options).notifier) + .read(analysisControllerProvider(options).notifier) .toggleBoard(); }, ), BottomSheetAction( makeLabel: (context) => Text(context.l10n.boardEditor), onPressed: (context) { - final analysisState = - ref.read(analysisControllerProvider(pgn, options)); + final analysisState = ref.read(analysisControllerProvider(options)); final boardFen = analysisState.position.fen; pushPlatformRoute( context, @@ -372,15 +361,14 @@ class _BottomBar extends ConsumerWidget { pushPlatformRoute( context, title: context.l10n.studyShareAndExport, - builder: (_) => AnalysisShareScreen(pgn: pgn, options: options), + builder: (_) => AnalysisShareScreen(options: options), ); }, ), BottomSheetAction( makeLabel: (context) => Text(context.l10n.mobileSharePositionAsFEN), onPressed: (_) { - final analysisState = - ref.read(analysisControllerProvider(pgn, options)); + final analysisState = ref.read(analysisControllerProvider(options)); launchShareDialog( context, text: analysisState.position.fen, @@ -394,7 +382,7 @@ class _BottomBar extends ConsumerWidget { onPressed: (_) async { final gameId = options.gameAnyId!.gameId; final analysisState = - ref.read(analysisControllerProvider(pgn, options)); + ref.read(analysisControllerProvider(options)); try { final image = await ref.read(gameShareServiceProvider).screenshotPosition( diff --git a/lib/src/view/analysis/analysis_settings.dart b/lib/src/view/analysis/analysis_settings.dart index 415548cb49..380687363a 100644 --- a/lib/src/view/analysis/analysis_settings.dart +++ b/lib/src/view/analysis/analysis_settings.dart @@ -11,14 +11,13 @@ import 'package:lichess_mobile/src/widgets/non_linear_slider.dart'; import 'package:lichess_mobile/src/widgets/settings.dart'; class AnalysisSettings extends ConsumerWidget { - const AnalysisSettings(this.pgn, this.options); + const AnalysisSettings(this.options); - final String pgn; final AnalysisOptions options; @override Widget build(BuildContext context, WidgetRef ref) { - final ctrlProvider = analysisControllerProvider(pgn, options); + final ctrlProvider = analysisControllerProvider(options); final isLocalEvaluationAllowed = ref.watch(ctrlProvider.select((s) => s.isLocalEvaluationAllowed)); final isEngineAvailable = ref.watch( @@ -35,7 +34,7 @@ class AnalysisSettings extends ConsumerWidget { isScrollControlled: true, showDragHandle: true, isDismissible: true, - builder: (_) => OpeningExplorerSettings(pgn, options), + builder: (_) => OpeningExplorerSettings(options), ), trailing: const Icon(CupertinoIcons.chevron_right), ), diff --git a/lib/src/view/analysis/analysis_share_screen.dart b/lib/src/view/analysis/analysis_share_screen.dart index 97e626ad3f..785e8b0cf7 100644 --- a/lib/src/view/analysis/analysis_share_screen.dart +++ b/lib/src/view/analysis/analysis_share_screen.dart @@ -17,9 +17,8 @@ import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; final _dateFormatter = DateFormat('yyyy.MM.dd'); class AnalysisShareScreen extends StatelessWidget { - const AnalysisShareScreen({required this.pgn, required this.options}); + const AnalysisShareScreen({required this.options}); - final String pgn; final AnalysisOptions options; @override @@ -28,7 +27,7 @@ class AnalysisShareScreen extends StatelessWidget { appBar: PlatformAppBar( title: Text(context.l10n.studyShareAndExport), ), - body: _EditPgnTagsForm(pgn, options), + body: _EditPgnTagsForm(options), ); } } @@ -41,9 +40,8 @@ const Set _ratingHeaders = { }; class _EditPgnTagsForm extends ConsumerStatefulWidget { - const _EditPgnTagsForm(this.pgn, this.options); + const _EditPgnTagsForm(this.options); - final String pgn; final AnalysisOptions options; @override @@ -57,7 +55,7 @@ class _EditPgnTagsFormState extends ConsumerState<_EditPgnTagsForm> { @override void initState() { super.initState(); - final ctrlProvider = analysisControllerProvider(widget.pgn, widget.options); + final ctrlProvider = analysisControllerProvider(widget.options); final pgnHeaders = ref.read(ctrlProvider).pgnHeaders; for (final entry in pgnHeaders.entries) { @@ -87,7 +85,7 @@ class _EditPgnTagsFormState extends ConsumerState<_EditPgnTagsForm> { @override Widget build(BuildContext context) { - final ctrlProvider = analysisControllerProvider(widget.pgn, widget.options); + final ctrlProvider = analysisControllerProvider(widget.options); final pgnHeaders = ref.watch(ctrlProvider.select((c) => c.pgnHeaders)); final showRatingAsync = ref.watch(showRatingsPrefProvider); @@ -181,7 +179,6 @@ class _EditPgnTagsFormState extends ConsumerState<_EditPgnTagsForm> { text: ref .read( analysisControllerProvider( - widget.pgn, widget.options, ).notifier, ) @@ -207,7 +204,7 @@ class _EditPgnTagsFormState extends ConsumerState<_EditPgnTagsForm> { required BuildContext context, required void Function() onEntryChanged, }) { - final ctrlProvider = analysisControllerProvider(widget.pgn, widget.options); + final ctrlProvider = analysisControllerProvider(widget.options); if (Theme.of(context).platform == TargetPlatform.iOS) { return showCupertinoModalPopup( context: context, @@ -272,7 +269,7 @@ class _EditPgnTagsFormState extends ConsumerState<_EditPgnTagsForm> { onSelectedItemChanged: (choice) { ref .read( - analysisControllerProvider(widget.pgn, widget.options).notifier, + analysisControllerProvider(widget.options).notifier, ) .updatePgnHeader( entry.key, diff --git a/lib/src/view/analysis/opening_explorer_view.dart b/lib/src/view/analysis/opening_explorer_view.dart index 80c0b0a964..b06f1efc34 100644 --- a/lib/src/view/analysis/opening_explorer_view.dart +++ b/lib/src/view/analysis/opening_explorer_view.dart @@ -18,9 +18,8 @@ const _kTableRowPadding = EdgeInsets.symmetric( ); class OpeningExplorerView extends ConsumerStatefulWidget { - const OpeningExplorerView({required this.pgn, required this.options}); + const OpeningExplorerView({required this.options}); - final String pgn; final AnalysisOptions options; @override @@ -36,15 +35,13 @@ class _OpeningExplorerState extends ConsumerState { @override Widget build(BuildContext context) { - final analysisState = - ref.watch(analysisControllerProvider(widget.pgn, widget.options)); + final analysisState = ref.watch(analysisControllerProvider(widget.options)); if (analysisState.position.ply >= 50) { return _OpeningExplorerView( isLoading: false, children: [ OpeningExplorerMoveTable.maxDepth( - pgn: widget.pgn, options: widget.options, ), ], @@ -104,7 +101,6 @@ class _OpeningExplorerState extends ConsumerState { child: ShimmerLoading( isLoading: true, child: OpeningExplorerMoveTable.loading( - pgn: widget.pgn, options: widget.options, ), ), @@ -123,7 +119,6 @@ class _OpeningExplorerState extends ConsumerState { whiteWins: openingExplorer.entry.white, draws: openingExplorer.entry.draws, blackWins: openingExplorer.entry.black, - pgn: widget.pgn, options: widget.options, ), if (topGames != null && topGames.isNotEmpty) ...[ @@ -179,7 +174,6 @@ class _OpeningExplorerState extends ConsumerState { child: ShimmerLoading( isLoading: true, child: OpeningExplorerMoveTable.loading( - pgn: widget.pgn, options: widget.options, ), ), diff --git a/lib/src/view/analysis/server_analysis.dart b/lib/src/view/analysis/server_analysis.dart index 93510a1f6b..ba0ac8f359 100644 --- a/lib/src/view/analysis/server_analysis.dart +++ b/lib/src/view/analysis/server_analysis.dart @@ -16,14 +16,13 @@ import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; class ServerAnalysisSummary extends ConsumerWidget { - const ServerAnalysisSummary(this.pgn, this.options); + const ServerAnalysisSummary(this.options); - final String pgn; final AnalysisOptions options; @override Widget build(BuildContext context, WidgetRef ref) { - final ctrlProvider = analysisControllerProvider(pgn, options); + final ctrlProvider = analysisControllerProvider(options); final playersAnalysis = ref.watch(ctrlProvider.select((value) => value.playersAnalysis)); final pgnHeaders = @@ -38,7 +37,7 @@ class ServerAnalysisSummary extends ConsumerWidget { padding: EdgeInsets.only(top: 16.0), child: WaitingForServerAnalysis(), ), - AcplChart(pgn, options), + AcplChart(options), Center( child: SizedBox( width: math.min(MediaQuery.sizeOf(context).width, 500), @@ -321,9 +320,8 @@ class _SummaryPlayerName extends StatelessWidget { } class AcplChart extends ConsumerWidget { - const AcplChart(this.pgn, this.options); + const AcplChart(this.options); - final String pgn; final AnalysisOptions options; @override @@ -359,23 +357,21 @@ class AcplChart extends ConsumerWidget { ); final data = ref.watch( - analysisControllerProvider(pgn, options) + analysisControllerProvider(options) .select((value) => value.acplChartData), ); final rootPly = ref.watch( - analysisControllerProvider(pgn, options) + analysisControllerProvider(options) .select((value) => value.root.position.ply), ); final currentNode = ref.watch( - analysisControllerProvider(pgn, options) - .select((value) => value.currentNode), + analysisControllerProvider(options).select((value) => value.currentNode), ); final isOnMainline = ref.watch( - analysisControllerProvider(pgn, options) - .select((value) => value.isOnMainline), + analysisControllerProvider(options).select((value) => value.isOnMainline), ); if (data == null) { @@ -450,7 +446,7 @@ class AcplChart extends ConsumerWidget { ); final closestNodeIndex = closestSpot.x.round(); ref - .read(analysisControllerProvider(pgn, options).notifier) + .read(analysisControllerProvider(options).notifier) .jumpToNthNodeOnMainline(closestNodeIndex); } }, diff --git a/lib/src/view/analysis/tree_view.dart b/lib/src/view/analysis/tree_view.dart index 69c5b86978..de648427d7 100644 --- a/lib/src/view/analysis/tree_view.dart +++ b/lib/src/view/analysis/tree_view.dart @@ -9,17 +9,13 @@ import 'package:lichess_mobile/src/widgets/pgn.dart'; const kOpeningHeaderHeight = 32.0; class AnalysisTreeView extends ConsumerWidget { - const AnalysisTreeView( - this.pgn, - this.options, - ); + const AnalysisTreeView(this.options); - final String pgn; final AnalysisOptions options; @override Widget build(BuildContext context, WidgetRef ref) { - final ctrlProvider = analysisControllerProvider(pgn, options); + final ctrlProvider = analysisControllerProvider(options); final root = ref.watch(ctrlProvider.select((value) => value.root)); final currentPath = diff --git a/lib/src/view/board_editor/board_editor_screen.dart b/lib/src/view/board_editor/board_editor_screen.dart index 128e81cd06..7b4006a4ba 100644 --- a/lib/src/view/board_editor/board_editor_screen.dart +++ b/lib/src/view/board_editor/board_editor_screen.dart @@ -337,6 +337,7 @@ class _BottomBar extends ConsumerWidget { builder: (context) => AnalysisScreen( pgnOrId: editorState.pgn!, options: AnalysisOptions( + pgn: '', isLocalEvaluationAllowed: true, variant: Variant.fromPosition, orientation: editorState.orientation, diff --git a/lib/src/view/correspondence/offline_correspondence_game_screen.dart b/lib/src/view/correspondence/offline_correspondence_game_screen.dart index 0ce0cdae57..c816be5561 100644 --- a/lib/src/view/correspondence/offline_correspondence_game_screen.dart +++ b/lib/src/view/correspondence/offline_correspondence_game_screen.dart @@ -261,6 +261,7 @@ class _BodyState extends ConsumerState<_Body> { builder: (_) => AnalysisScreen( pgnOrId: game.makePgn(), options: AnalysisOptions( + pgn: '', isLocalEvaluationAllowed: false, variant: game.variant, initialMoveCursor: stepCursor, diff --git a/lib/src/view/game/archived_game_screen.dart b/lib/src/view/game/archived_game_screen.dart index ab7c41cec3..a8b0193202 100644 --- a/lib/src/view/game/archived_game_screen.dart +++ b/lib/src/view/game/archived_game_screen.dart @@ -386,6 +386,7 @@ class _BottomBar extends ConsumerWidget { builder: (context) => AnalysisScreen( pgnOrId: game.makePgn(), options: AnalysisOptions( + pgn: '', isLocalEvaluationAllowed: true, variant: gameData.variant, initialMoveCursor: cursor, diff --git a/lib/src/view/game/game_list_tile.dart b/lib/src/view/game/game_list_tile.dart index 5009b84947..c54d6c5106 100644 --- a/lib/src/view/game/game_list_tile.dart +++ b/lib/src/view/game/game_list_tile.dart @@ -228,6 +228,7 @@ class _ContextMenu extends ConsumerWidget { builder: (context) => AnalysisScreen( pgnOrId: game.id.value, options: AnalysisOptions( + pgn: '', isLocalEvaluationAllowed: true, variant: game.variant, orientation: orientation, diff --git a/lib/src/view/game/game_result_dialog.dart b/lib/src/view/game/game_result_dialog.dart index 2611a15696..20eeb61b7b 100644 --- a/lib/src/view/game/game_result_dialog.dart +++ b/lib/src/view/game/game_result_dialog.dart @@ -263,6 +263,7 @@ class OverTheBoardGameResultDialog extends StatelessWidget { builder: (_) => AnalysisScreen( pgnOrId: game.makePgn(), options: AnalysisOptions( + pgn: '', isLocalEvaluationAllowed: true, variant: game.meta.variant, orientation: Side.white, diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index 9ade504e4e..6823005fed 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -33,9 +33,8 @@ const _kTableRowPadding = EdgeInsets.symmetric( const _kTabletBoardRadius = BorderRadius.all(Radius.circular(4.0)); class OpeningExplorerScreen extends ConsumerStatefulWidget { - const OpeningExplorerScreen({required this.pgn, required this.options}); + const OpeningExplorerScreen({required this.options}); - final String pgn; final AnalysisOptions options; @override @@ -51,8 +50,7 @@ class _OpeningExplorerState extends ConsumerState { @override Widget build(BuildContext context) { - final analysisState = - ref.watch(analysisControllerProvider(widget.pgn, widget.options)); + final analysisState = ref.watch(analysisControllerProvider(widget.options)); final opening = analysisState.currentNode.isRoot ? LightOpening( @@ -102,14 +100,12 @@ class _OpeningExplorerState extends ConsumerState { if (analysisState.position.ply >= 50) { return _OpeningExplorerView( - pgn: widget.pgn, options: widget.options, isLoading: false, isIndexing: false, children: [ openingHeader, OpeningExplorerMoveTable.maxDepth( - pgn: widget.pgn, options: widget.options, ), ], @@ -120,7 +116,6 @@ class _OpeningExplorerState extends ConsumerState { if (prefs.db == OpeningDatabase.player && prefs.playerDb.username == null) { return _OpeningExplorerView( - pgn: widget.pgn, options: widget.options, isLoading: false, isIndexing: false, @@ -163,7 +158,6 @@ class _OpeningExplorerState extends ConsumerState { openingExplorerAsync.isLoading || openingExplorerAsync.value == null; return _OpeningExplorerView( - pgn: widget.pgn, options: widget.options, isLoading: isLoading, isIndexing: openingExplorerAsync.value?.isIndexing ?? false, @@ -176,7 +170,6 @@ class _OpeningExplorerState extends ConsumerState { child: ShimmerLoading( isLoading: true, child: OpeningExplorerMoveTable.loading( - pgn: widget.pgn, options: widget.options, ), ), @@ -196,7 +189,6 @@ class _OpeningExplorerState extends ConsumerState { whiteWins: openingExplorer.entry.white, draws: openingExplorer.entry.draws, blackWins: openingExplorer.entry.black, - pgn: widget.pgn, options: widget.options, ), if (topGames != null && topGames.isNotEmpty) ...[ @@ -252,7 +244,6 @@ class _OpeningExplorerState extends ConsumerState { child: ShimmerLoading( isLoading: true, child: OpeningExplorerMoveTable.loading( - pgn: widget.pgn, options: widget.options, ), ), @@ -278,14 +269,12 @@ class _OpeningExplorerState extends ConsumerState { class _OpeningExplorerView extends StatelessWidget { const _OpeningExplorerView({ - required this.pgn, required this.options, required this.children, required this.isLoading, required this.isIndexing, }); - final String pgn; final AnalysisOptions options; final List children; final bool isLoading; @@ -306,7 +295,7 @@ class _OpeningExplorerView extends StatelessWidget { horizontal: kTabletBoardTableSidePadding, ) : EdgeInsets.zero, - child: _MoveList(pgn: pgn, options: options), + child: _MoveList(options: options), ), Expanded( child: LayoutBuilder( @@ -338,7 +327,6 @@ class _OpeningExplorerView extends StatelessWidget { bottom: kTabletBoardTableSidePadding, ), child: AnalysisBoard( - pgn, options, boardSize, borderRadius: isTablet ? _kTabletBoardRadius : null, @@ -389,7 +377,6 @@ class _OpeningExplorerView extends StatelessWidget { // disable scrolling when dragging the board onVerticalDragStart: (_) {}, child: AnalysisBoard( - pgn, options, boardSize, ), @@ -404,7 +391,7 @@ class _OpeningExplorerView extends StatelessWidget { }, ), ), - _BottomBar(pgn: pgn, options: options), + _BottomBar(options: options), ], ), ); @@ -414,7 +401,7 @@ class _OpeningExplorerView extends StatelessWidget { body: body, appBar: AppBar( title: Text(context.l10n.openingExplorer), - bottom: _MoveList(pgn: pgn, options: options), + bottom: _MoveList(options: options), actions: [ if (isIndexing) const _IndexingIndicator(), ], @@ -480,12 +467,8 @@ class _IndexingIndicatorState extends State<_IndexingIndicator> } class _MoveList extends ConsumerWidget implements PreferredSizeWidget { - const _MoveList({ - required this.pgn, - required this.options, - }); + const _MoveList({required this.options}); - final String pgn; final AnalysisOptions options; @override @@ -493,7 +476,7 @@ class _MoveList extends ConsumerWidget implements PreferredSizeWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final ctrlProvider = analysisControllerProvider(pgn, options); + final ctrlProvider = analysisControllerProvider(options); final state = ref.watch(ctrlProvider); final slicedMoves = state.root.mainline .map((e) => e.sanMove.san) @@ -526,19 +509,15 @@ class _MoveList extends ConsumerWidget implements PreferredSizeWidget { } class _BottomBar extends ConsumerWidget { - const _BottomBar({ - required this.pgn, - required this.options, - }); + const _BottomBar({required this.options}); - final String pgn; final AnalysisOptions options; @override Widget build(BuildContext context, WidgetRef ref) { final db = ref .watch(openingExplorerPreferencesProvider.select((value) => value.db)); - final ctrlProvider = analysisControllerProvider(pgn, options); + final ctrlProvider = analysisControllerProvider(options); final canGoBack = ref.watch(ctrlProvider.select((value) => value.canGoBack)); final canGoNext = @@ -560,7 +539,7 @@ class _BottomBar extends ConsumerWidget { isScrollControlled: true, showDragHandle: true, isDismissible: true, - builder: (_) => OpeningExplorerSettings(pgn, options), + builder: (_) => OpeningExplorerSettings(options), ), icon: Icons.tune, ), @@ -596,8 +575,7 @@ class _BottomBar extends ConsumerWidget { } void _moveForward(WidgetRef ref) => - ref.read(analysisControllerProvider(pgn, options).notifier).userNext(); - void _moveBackward(WidgetRef ref) => ref - .read(analysisControllerProvider(pgn, options).notifier) - .userPrevious(); + ref.read(analysisControllerProvider(options).notifier).userNext(); + void _moveBackward(WidgetRef ref) => + ref.read(analysisControllerProvider(options).notifier).userPrevious(); } diff --git a/lib/src/view/opening_explorer/opening_explorer_settings.dart b/lib/src/view/opening_explorer/opening_explorer_settings.dart index 51c3c38d8f..a880d8fb33 100644 --- a/lib/src/view/opening_explorer/opening_explorer_settings.dart +++ b/lib/src/view/opening_explorer/opening_explorer_settings.dart @@ -14,9 +14,8 @@ import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; class OpeningExplorerSettings extends ConsumerWidget { - const OpeningExplorerSettings(this.pgn, this.options); + const OpeningExplorerSettings(this.options); - final String pgn; final AnalysisOptions options; @override diff --git a/lib/src/view/opening_explorer/opening_explorer_widgets.dart b/lib/src/view/opening_explorer/opening_explorer_widgets.dart index 2b19204c49..43bf9f9deb 100644 --- a/lib/src/view/opening_explorer/opening_explorer_widgets.dart +++ b/lib/src/view/opening_explorer/opening_explorer_widgets.dart @@ -34,13 +34,11 @@ class OpeningExplorerMoveTable extends ConsumerWidget { required this.whiteWins, required this.draws, required this.blackWins, - required this.pgn, required this.options, }) : _isLoading = false, _maxDepthReached = false; const OpeningExplorerMoveTable.loading({ - required this.pgn, required this.options, }) : _isLoading = true, moves = const IListConst([]), @@ -50,7 +48,6 @@ class OpeningExplorerMoveTable extends ConsumerWidget { _maxDepthReached = false; const OpeningExplorerMoveTable.maxDepth({ - required this.pgn, required this.options, }) : _isLoading = false, moves = const IListConst([]), @@ -63,7 +60,6 @@ class OpeningExplorerMoveTable extends ConsumerWidget { final int whiteWins; final int draws; final int blackWins; - final String pgn; final AnalysisOptions options; final bool _isLoading; @@ -84,7 +80,7 @@ class OpeningExplorerMoveTable extends ConsumerWidget { } final games = whiteWins + draws + blackWins; - final ctrlProvider = analysisControllerProvider(pgn, options); + final ctrlProvider = analysisControllerProvider(options); const headerTextStyle = TextStyle(fontSize: 12); diff --git a/lib/src/view/puzzle/puzzle_screen.dart b/lib/src/view/puzzle/puzzle_screen.dart index ef8681c596..b79254f87c 100644 --- a/lib/src/view/puzzle/puzzle_screen.dart +++ b/lib/src/view/puzzle/puzzle_screen.dart @@ -486,6 +486,7 @@ class _BottomBar extends ConsumerWidget { builder: (context) => AnalysisScreen( pgnOrId: ref.read(ctrlProvider.notifier).makePgn(), options: AnalysisOptions( + pgn: '', isLocalEvaluationAllowed: true, variant: Variant.standard, orientation: puzzleState.pov, diff --git a/lib/src/view/puzzle/streak_screen.dart b/lib/src/view/puzzle/streak_screen.dart index b682b49ebc..6be35275dc 100644 --- a/lib/src/view/puzzle/streak_screen.dart +++ b/lib/src/view/puzzle/streak_screen.dart @@ -293,6 +293,7 @@ class _BottomBar extends ConsumerWidget { builder: (context) => AnalysisScreen( pgnOrId: ref.read(ctrlProvider.notifier).makePgn(), options: AnalysisOptions( + pgn: '', isLocalEvaluationAllowed: true, variant: Variant.standard, orientation: puzzleState.pov, diff --git a/lib/src/view/study/study_bottom_bar.dart b/lib/src/view/study/study_bottom_bar.dart index ade01ed711..d94d6e4d57 100644 --- a/lib/src/view/study/study_bottom_bar.dart +++ b/lib/src/view/study/study_bottom_bar.dart @@ -167,6 +167,7 @@ class _GamebookBottomBar extends ConsumerWidget { builder: (context) => AnalysisScreen( pgnOrId: state.pgn, options: AnalysisOptions( + pgn: state.pgn, isLocalEvaluationAllowed: true, variant: state.variant, orientation: state.pov, diff --git a/lib/src/view/tools/load_position_screen.dart b/lib/src/view/tools/load_position_screen.dart index ac3b4d97b7..27040a0d70 100644 --- a/lib/src/view/tools/load_position_screen.dart +++ b/lib/src/view/tools/load_position_screen.dart @@ -132,7 +132,8 @@ class _BodyState extends State<_Body> { return ( pgn: '[FEN "${pos.fen}"]', fen: pos.fen, - options: const AnalysisOptions( + options: AnalysisOptions( + pgn: '[FEN "${pos.fen}"]', isLocalEvaluationAllowed: true, variant: Variant.standard, orientation: Side.white, @@ -164,6 +165,7 @@ class _BodyState extends State<_Body> { pgn: textInput!, fen: lastPosition.fen, options: AnalysisOptions( + pgn: textInput!, isLocalEvaluationAllowed: true, variant: rule != null ? Variant.fromRule(rule) : Variant.standard, initialMoveCursor: mainlineMoves.isEmpty ? 0 : 1, diff --git a/lib/src/view/tools/tools_tab_screen.dart b/lib/src/view/tools/tools_tab_screen.dart index 8270f84bcc..b2bba9d51c 100644 --- a/lib/src/view/tools/tools_tab_screen.dart +++ b/lib/src/view/tools/tools_tab_screen.dart @@ -150,6 +150,7 @@ class _Body extends ConsumerWidget { builder: (context) => const AnalysisScreen( pgnOrId: '', options: AnalysisOptions( + pgn: '', isLocalEvaluationAllowed: true, variant: Variant.standard, orientation: Side.white, @@ -166,8 +167,8 @@ class _Body extends ConsumerWidget { context, rootNavigator: true, builder: (context) => const OpeningExplorerScreen( - pgn: '', options: AnalysisOptions( + pgn: '', isLocalEvaluationAllowed: false, variant: Variant.standard, orientation: Side.white, diff --git a/test/view/analysis/analysis_screen_test.dart b/test/view/analysis/analysis_screen_test.dart index 156380c906..337fdc7785 100644 --- a/test/view/analysis/analysis_screen_test.dart +++ b/test/view/analysis/analysis_screen_test.dart @@ -36,6 +36,7 @@ void main() { home: AnalysisScreen( pgnOrId: sanMoves, options: AnalysisOptions( + pgn: '', isLocalEvaluationAllowed: false, variant: Variant.standard, opening: opening, @@ -70,6 +71,7 @@ void main() { home: AnalysisScreen( pgnOrId: sanMoves, options: AnalysisOptions( + pgn: '', isLocalEvaluationAllowed: false, variant: Variant.standard, opening: opening, @@ -135,6 +137,7 @@ void main() { home: AnalysisScreen( pgnOrId: pgn, options: const AnalysisOptions( + pgn: '', isLocalEvaluationAllowed: false, variant: Variant.standard, orientation: Side.white, diff --git a/test/view/opening_explorer/opening_explorer_screen_test.dart b/test/view/opening_explorer/opening_explorer_screen_test.dart index 32b7e88a4f..5811d32f6f 100644 --- a/test/view/opening_explorer/opening_explorer_screen_test.dart +++ b/test/view/opening_explorer/opening_explorer_screen_test.dart @@ -40,6 +40,7 @@ void main() { }); const options = AnalysisOptions( + pgn: '', id: standaloneOpeningExplorerId, isLocalEvaluationAllowed: false, orientation: Side.white, @@ -65,7 +66,6 @@ void main() { final app = await makeTestProviderScopeApp( tester, home: const OpeningExplorerScreen( - pgn: '', options: options, ), overrides: [ @@ -112,7 +112,6 @@ void main() { final app = await makeTestProviderScopeApp( tester, home: const OpeningExplorerScreen( - pgn: '', options: options, ), overrides: [ @@ -165,7 +164,6 @@ void main() { final app = await makeTestProviderScopeApp( tester, home: const OpeningExplorerScreen( - pgn: '', options: options, ), overrides: [ From 7a4dd3805070910ca6b5eb29acbd20c4848e7fbd Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Thu, 21 Nov 2024 01:12:14 +0100 Subject: [PATCH 732/979] more tweaks --- lib/src/model/broadcast/broadcast.dart | 6 +++--- lib/src/model/broadcast/broadcast_game_controller.dart | 2 +- lib/src/model/broadcast/broadcast_round_controller.dart | 1 + lib/src/view/broadcast/broadcast_boards_tab.dart | 5 ++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/src/model/broadcast/broadcast.dart b/lib/src/model/broadcast/broadcast.dart index 71d3e90cc9..8d23cdd157 100644 --- a/lib/src/model/broadcast/broadcast.dart +++ b/lib/src/model/broadcast/broadcast.dart @@ -96,15 +96,15 @@ class BroadcastGame with _$BroadcastGame { required String fen, required Move? lastMove, required BroadcastResult status, - required DateTime? updatedClockAt, + required DateTime updatedClockAt, }) = _BroadcastGame; - bool get isPlaying => status == BroadcastResult.ongoing; + bool get isOngoing => status == BroadcastResult.ongoing; bool get isOver => status == BroadcastResult.draw || status == BroadcastResult.whiteWins || status == BroadcastResult.blackWins; - Side get playingSide => Setup.parseFen(fen).turn; + Side get sideToMove => Setup.parseFen(fen).turn; } @freezed diff --git a/lib/src/model/broadcast/broadcast_game_controller.dart b/lib/src/model/broadcast/broadcast_game_controller.dart index c7d3fc5ba0..4ffe315870 100644 --- a/lib/src/model/broadcast/broadcast_game_controller.dart +++ b/lib/src/model/broadcast/broadcast_game_controller.dart @@ -151,6 +151,7 @@ class BroadcastGameController extends _$BroadcastGameController final (newPath, isNewNode) = _root.addMoveAt(path, uciMove, clock: clock); if (newPath != null) { + _root.promoteAt(newPath, toMainline: true); if (state.requireValue.broadcastLivePath == state.requireValue.currentPath) { _setPath( @@ -160,7 +161,6 @@ class BroadcastGameController extends _$BroadcastGameController isBroadcastMove: true, ); } else { - _root.promoteAt(newPath, toMainline: true); state = AsyncData( state.requireValue .copyWith(broadcastLivePath: newPath, root: _root.view), diff --git a/lib/src/model/broadcast/broadcast_round_controller.dart b/lib/src/model/broadcast/broadcast_round_controller.dart index 981d17f4e7..5dcfdfee30 100644 --- a/lib/src/model/broadcast/broadcast_round_controller.dart +++ b/lib/src/model/broadcast/broadcast_round_controller.dart @@ -112,6 +112,7 @@ class BroadcastRoundController extends _$BroadcastRoundController { state.requireValue.update( broadcastGameId, (broadcastsGame) => broadcastsGame.copyWith( + updatedClockAt: DateTime.now(), players: IMap( { Side.white: broadcastsGame.players[Side.white]!.copyWith( diff --git a/lib/src/view/broadcast/broadcast_boards_tab.dart b/lib/src/view/broadcast/broadcast_boards_tab.dart index d637f95c6f..4345472a72 100644 --- a/lib/src/view/broadcast/broadcast_boards_tab.dart +++ b/lib/src/view/broadcast/broadcast_boards_tab.dart @@ -72,20 +72,19 @@ class BroadcastBoardsTab extends ConsumerWidget { } } -class BroadcastPreview extends ConsumerWidget { +class BroadcastPreview extends StatelessWidget { final BroadcastRoundId roundId; final IList? games; final String title; const BroadcastPreview({ - super.key, required this.roundId, this.games, required this.title, }); @override - Widget build(BuildContext context, WidgetRef ref) { + Widget build(BuildContext context) { // TODO uncomment when eval bar is ready // final showEvaluationBar = ref.watch( // broadcastPreferencesProvider.select((value) => value.showEvaluationBar), From 94621178ddc3c4c0b9676d8e1b9c121009be10d1 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Thu, 21 Nov 2024 01:47:15 +0100 Subject: [PATCH 733/979] optimize rebuilds by listening to state in children widgets --- .../view/broadcast/broadcast_game_screen.dart | 120 +++++++++--------- 1 file changed, 57 insertions(+), 63 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_game_screen.dart b/lib/src/view/broadcast/broadcast_game_screen.dart index 4ade056e3d..3a4693fb7e 100644 --- a/lib/src/view/broadcast/broadcast_game_screen.dart +++ b/lib/src/view/broadcast/broadcast_game_screen.dart @@ -270,44 +270,21 @@ class _BroadcastBoardWithHeaders extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final gameState = ref - .watch(broadcastGameControllerProvider(roundId, gameId)) - .requireValue; - final clocks = gameState.clocks; - final currentPath = gameState.currentPath; - final broadcastLivePath = gameState.broadcastLivePath; - final playingSide = gameState.position.turn; - final pov = gameState.pov; - final game = ref.watch( - broadcastRoundControllerProvider(roundId) - .select((game) => game.value?[gameId]), - ); - return Column( children: [ - if (game != null) - _PlayerWidget( - clock: (playingSide == pov.opposite) - ? clocks?.parentClock - : clocks?.clock, - width: size, - game: game, - side: pov.opposite, - boardSide: _PlayerWidgetSide.top, - sideToPlay: playingSide, - isCursorOnLiveMove: currentPath == broadcastLivePath, - ), + _PlayerWidget( + roundId: roundId, + gameId: gameId, + width: size, + widgetPosition: _PlayerWidgetPosition.top, + ), _BroadcastBoard(roundId, gameId, size), - if (game != null) - _PlayerWidget( - clock: (playingSide == pov) ? clocks?.parentClock : clocks?.clock, - width: size, - game: game, - side: pov, - boardSide: _PlayerWidgetSide.bottom, - sideToPlay: playingSide, - isCursorOnLiveMove: currentPath == broadcastLivePath, - ), + _PlayerWidget( + roundId: roundId, + gameId: gameId, + width: size, + widgetPosition: _PlayerWidgetPosition.bottom, + ), ], ); } @@ -428,29 +405,40 @@ class _BroadcastBoardState extends ConsumerState<_BroadcastBoard> { } } -enum _PlayerWidgetSide { bottom, top } +enum _PlayerWidgetPosition { bottom, top } -class _PlayerWidget extends StatelessWidget { +class _PlayerWidget extends ConsumerWidget { const _PlayerWidget({ + required this.roundId, + required this.gameId, required this.width, - required this.clock, - required this.game, - required this.side, - required this.boardSide, - required this.sideToPlay, - required this.isCursorOnLiveMove, + required this.widgetPosition, }); - final BroadcastGame game; - final Duration? clock; - final Side side; + final BroadcastRoundId roundId; + final BroadcastGameId gameId; final double width; - final _PlayerWidgetSide boardSide; - final Side sideToPlay; - final bool isCursorOnLiveMove; + final _PlayerWidgetPosition widgetPosition; @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final gameState = ref + .watch(broadcastGameControllerProvider(roundId, gameId)) + .requireValue; + final clocks = gameState.clocks; + final isCursorOnLiveMove = + gameState.currentPath == gameState.broadcastLivePath; + final sideToMove = gameState.position.turn; + final side = switch (widgetPosition) { + _PlayerWidgetPosition.bottom => gameState.pov, + _PlayerWidgetPosition.top => gameState.pov.opposite, + }; + final clock = (sideToMove == side) ? clocks?.parentClock : clocks?.clock; + + final game = ref.watch( + broadcastRoundControllerProvider(roundId) + .select((game) => game.requireValue[gameId]!), + ); final player = game.players[side]!; final gameStatus = game.status; @@ -464,10 +452,10 @@ class _PlayerWidget extends StatelessWidget { shape: RoundedRectangleBorder( borderRadius: BorderRadius.only( topLeft: Radius.circular( - boardSide == _PlayerWidgetSide.top ? 8 : 0, + widgetPosition == _PlayerWidgetPosition.top ? 8 : 0, ), bottomLeft: Radius.circular( - boardSide == _PlayerWidgetSide.bottom ? 8 : 0, + widgetPosition == _PlayerWidgetPosition.bottom ? 8 : 0, ), ), ), @@ -497,18 +485,24 @@ class _PlayerWidget extends StatelessWidget { shape: RoundedRectangleBorder( borderRadius: BorderRadius.only( topLeft: Radius.circular( - boardSide == _PlayerWidgetSide.top && !game.isOver ? 8 : 0, + widgetPosition == _PlayerWidgetPosition.top && !game.isOver + ? 8 + : 0, ), topRight: Radius.circular( - boardSide == _PlayerWidgetSide.top && clock == null ? 8 : 0, + widgetPosition == _PlayerWidgetPosition.top && clock == null + ? 8 + : 0, ), bottomLeft: Radius.circular( - boardSide == _PlayerWidgetSide.bottom && !game.isOver + widgetPosition == _PlayerWidgetPosition.bottom && + !game.isOver ? 8 : 0, ), bottomRight: Radius.circular( - boardSide == _PlayerWidgetSide.bottom && clock == null + widgetPosition == _PlayerWidgetPosition.bottom && + clock == null ? 8 : 0, ), @@ -571,7 +565,7 @@ class _PlayerWidget extends StatelessWidget { ), if (clock != null) Card( - color: (side == sideToPlay) + color: (side == sideToMove) ? isCursorOnLiveMove ? Theme.of(context).colorScheme.tertiaryContainer : Theme.of(context).colorScheme.secondaryContainer @@ -580,10 +574,10 @@ class _PlayerWidget extends StatelessWidget { shape: RoundedRectangleBorder( borderRadius: BorderRadius.only( topRight: Radius.circular( - boardSide == _PlayerWidgetSide.top ? 8 : 0, + widgetPosition == _PlayerWidgetPosition.top ? 8 : 0, ), bottomRight: Radius.circular( - boardSide == _PlayerWidgetSide.bottom ? 8 : 0, + widgetPosition == _PlayerWidgetPosition.bottom ? 8 : 0, ), ), ), @@ -591,14 +585,14 @@ class _PlayerWidget extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), child: CountdownClockBuilder( - timeLeft: clock!, - active: side == sideToPlay && + timeLeft: clock, + active: side == sideToMove && isCursorOnLiveMove && game.status == BroadcastResult.ongoing, builder: (context, timeLeft) => Text( timeLeft.toHoursMinutesSeconds(), style: TextStyle( - color: (side == sideToPlay) + color: (side == sideToMove) ? isCursorOnLiveMove ? Theme.of(context) .colorScheme @@ -611,7 +605,7 @@ class _PlayerWidget extends StatelessWidget { ), ), tickInterval: const Duration(seconds: 1), - clockUpdatedAt: (side == sideToPlay && isCursorOnLiveMove) + clockUpdatedAt: (side == sideToMove && isCursorOnLiveMove) ? game.updatedClockAt : null, ), From 4a58a9cbd0b83091fa123b0988e2a158959e5ce5 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Thu, 21 Nov 2024 01:54:54 +0100 Subject: [PATCH 734/979] remove rounded borders --- .../view/broadcast/broadcast_game_screen.dart | 49 ++----------------- 1 file changed, 3 insertions(+), 46 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_game_screen.dart b/lib/src/view/broadcast/broadcast_game_screen.dart index 3a4693fb7e..06cb20e5c5 100644 --- a/lib/src/view/broadcast/broadcast_game_screen.dart +++ b/lib/src/view/broadcast/broadcast_game_screen.dart @@ -449,16 +449,7 @@ class _PlayerWidget extends ConsumerWidget { if (game.isOver) Card( margin: EdgeInsets.zero, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular( - widgetPosition == _PlayerWidgetPosition.top ? 8 : 0, - ), - bottomLeft: Radius.circular( - widgetPosition == _PlayerWidgetPosition.bottom ? 8 : 0, - ), - ), - ), + shape: const Border(), child: Padding( padding: const EdgeInsets.symmetric( horizontal: 8.0, @@ -482,32 +473,7 @@ class _PlayerWidget extends ConsumerWidget { Expanded( child: Card( margin: EdgeInsets.zero, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular( - widgetPosition == _PlayerWidgetPosition.top && !game.isOver - ? 8 - : 0, - ), - topRight: Radius.circular( - widgetPosition == _PlayerWidgetPosition.top && clock == null - ? 8 - : 0, - ), - bottomLeft: Radius.circular( - widgetPosition == _PlayerWidgetPosition.bottom && - !game.isOver - ? 8 - : 0, - ), - bottomRight: Radius.circular( - widgetPosition == _PlayerWidgetPosition.bottom && - clock == null - ? 8 - : 0, - ), - ), - ), + shape: const Border(), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), @@ -571,16 +537,7 @@ class _PlayerWidget extends ConsumerWidget { : Theme.of(context).colorScheme.secondaryContainer : null, margin: EdgeInsets.zero, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topRight: Radius.circular( - widgetPosition == _PlayerWidgetPosition.top ? 8 : 0, - ), - bottomRight: Radius.circular( - widgetPosition == _PlayerWidgetPosition.bottom ? 8 : 0, - ), - ), - ), + shape: const Border(), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), From a85a608274dd5bb12f583dea41fc0c324d82dbf7 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Thu, 21 Nov 2024 02:13:59 +0100 Subject: [PATCH 735/979] really fix this time the clock not updating correctly at the end of a game on the broadcast game screen --- .../broadcast/broadcast_game_controller.dart | 25 +++++++++++-------- .../view/broadcast/broadcast_game_screen.dart | 4 +-- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/lib/src/model/broadcast/broadcast_game_controller.dart b/lib/src/model/broadcast/broadcast_game_controller.dart index 4ffe315870..4699e2447d 100644 --- a/lib/src/model/broadcast/broadcast_game_controller.dart +++ b/lib/src/model/broadcast/broadcast_game_controller.dart @@ -176,19 +176,24 @@ class BroadcastGameController extends _$BroadcastGameController // We check if the event is for this game if (broadcastGameId != gameId) return; - final headers = Map.fromEntries( - pick(event.data, 'tags').asListOrThrow( - (header) => MapEntry( - header(0).asStringOrThrow(), - header(1).asStringOrThrow(), - ), + final pgnHeadersEntries = pick(event.data, 'tags').asListOrThrow( + (header) => MapEntry( + header(0).asStringOrThrow(), + header(1).asStringOrThrow(), ), ); - for (final entry in headers.entries) { - final headers = state.requireValue.pgnHeaders.add(entry.key, entry.value); - state = AsyncData(state.requireValue.copyWith(pgnHeaders: headers)); - } + final pgnHeaders = + state.requireValue.pgnHeaders.addEntries(pgnHeadersEntries); + state = AsyncData( + state.requireValue.copyWith( + pgnHeaders: pgnHeaders, + // If the game is not ongoing, the [broadcastLivePath] should be null + broadcastLivePath: pgnHeaders['Result'] == '*' + ? state.requireValue.broadcastLivePath + : null, + ), + ); } EvaluationContext get _evaluationContext => EvaluationContext( diff --git a/lib/src/view/broadcast/broadcast_game_screen.dart b/lib/src/view/broadcast/broadcast_game_screen.dart index 06cb20e5c5..a29a2e116b 100644 --- a/lib/src/view/broadcast/broadcast_game_screen.dart +++ b/lib/src/view/broadcast/broadcast_game_screen.dart @@ -543,9 +543,7 @@ class _PlayerWidget extends ConsumerWidget { const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), child: CountdownClockBuilder( timeLeft: clock, - active: side == sideToMove && - isCursorOnLiveMove && - game.status == BroadcastResult.ongoing, + active: side == sideToMove && isCursorOnLiveMove, builder: (context, timeLeft) => Text( timeLeft.toHoursMinutesSeconds(), style: TextStyle( From 3ab16e6f0fea1ea5b179895537fd6747c173c3d8 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 21 Nov 2024 11:31:38 +0100 Subject: [PATCH 736/979] Refactor analysis controller to be async --- .../model/analysis/analysis_controller.dart | 347 ++++++++------- .../analysis/server_analysis_service.dart | 6 +- lib/src/model/game/game_controller.dart | 25 +- lib/src/view/analysis/analysis_board.dart | 9 +- lib/src/view/analysis/analysis_screen.dart | 193 +++----- lib/src/view/analysis/analysis_settings.dart | 207 ++++----- .../view/analysis/analysis_share_screen.dart | 5 +- .../view/analysis/opening_explorer_view.dart | 3 +- lib/src/view/analysis/server_analysis.dart | 47 +- lib/src/view/analysis/tree_view.dart | 29 +- .../board_editor/board_editor_screen.dart | 12 +- .../offline_correspondence_game_screen.dart | 13 +- lib/src/view/game/archived_game_screen.dart | 19 +- lib/src/view/game/game_body.dart | 4 +- lib/src/view/game/game_list_tile.dart | 9 +- lib/src/view/game/game_result_dialog.dart | 13 +- .../opening_explorer_screen.dart | 417 ++++++++++-------- lib/src/view/puzzle/puzzle_screen.dart | 12 +- lib/src/view/puzzle/streak_screen.dart | 12 +- lib/src/view/study/study_bottom_bar.dart | 12 +- lib/src/view/tools/load_position_screen.dart | 27 +- lib/src/view/tools/tools_tab_screen.dart | 23 +- lib/src/widgets/feedback.dart | 1 - test/view/analysis/analysis_screen_test.dart | 99 ++--- .../opening_explorer_screen_test.dart | 29 +- 25 files changed, 758 insertions(+), 815 deletions(-) diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index 6be0809cce..645a922ef9 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -17,6 +17,7 @@ import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; import 'package:lichess_mobile/src/model/common/uci.dart'; import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; import 'package:lichess_mobile/src/model/engine/work.dart'; +import 'package:lichess_mobile/src/model/game/game_repository_providers.dart'; import 'package:lichess_mobile/src/model/game/player.dart'; import 'package:lichess_mobile/src/utils/rate_limit.dart'; import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; @@ -26,80 +27,73 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'analysis_controller.freezed.dart'; part 'analysis_controller.g.dart'; -const standaloneAnalysisId = StringId('standalone_analysis'); -const standaloneOpeningExplorerId = StringId('standalone_opening_explorer'); - final _dateFormat = DateFormat('yyyy.MM.dd'); -/// Whether the analysis is a standalone analysis (not a lichess game analysis). -bool _isStandaloneAnalysis(StringId id) => - id == standaloneAnalysisId || id == standaloneOpeningExplorerId; +typedef StandaloneAnalysis = ({ + String pgn, + Variant variant, + Side orientation, + bool isLocalEvaluationAllowed, +}); @freezed class AnalysisOptions with _$AnalysisOptions { const AnalysisOptions._(); - const factory AnalysisOptions({ - required String pgn, - /// The ID of the analysis. Can be a game ID or a standalone ID. - required StringId id, - required bool isLocalEvaluationAllowed, - required Side orientation, - required Variant variant, + @Assert('standalone != null || gameId != null') + const factory AnalysisOptions({ + StandaloneAnalysis? standalone, + GameId? gameId, int? initialMoveCursor, - LightOpening? opening, - Division? division, - - /// Optional server analysis to display player stats. - ({PlayerAnalysis white, PlayerAnalysis black})? serverAnalysis, }) = _AnalysisOptions; - bool get canShowGameSummary => - serverAnalysis != null || id != standaloneAnalysisId; - - /// Whether the analysis is for a lichess game. - bool get isLichessGameAnalysis => gameAnyId != null; - - /// The game ID of the analysis, if it's a lichess game. - GameAnyId? get gameAnyId => - _isStandaloneAnalysis(id) ? null : GameAnyId(id.value); + bool get isLichessGameAnalysis => gameId != null; } @riverpod class AnalysisController extends _$AnalysisController implements PgnTreeNotifier { - late Root _root; + late final Root _root; + late final Variant _variant; + late final Side _orientation; final _engineEvalDebounce = Debouncer(const Duration(milliseconds: 150)); Timer? _startEngineEvalTimer; @override - AnalysisState build(AnalysisOptions options) { + Future build(AnalysisOptions options) async { final evaluationService = ref.watch(evaluationServiceProvider); final serverAnalysisService = ref.watch(serverAnalysisServiceProvider); - final isEngineAllowed = options.isLocalEvaluationAllowed && - engineSupportedVariants.contains(options.variant); - - ref.onDispose(() { - _startEngineEvalTimer?.cancel(); - _engineEvalDebounce.dispose(); - if (isEngineAllowed) { - evaluationService.disposeEngine(); - } - serverAnalysisService.lastAnalysisEvent - .removeListener(_listenToServerAnalysisEvents); - }); - - serverAnalysisService.lastAnalysisEvent - .addListener(_listenToServerAnalysisEvents); + late final String pgn; + late final LightOpening? opening; + late final ({PlayerAnalysis white, PlayerAnalysis black})? serverAnalysis; + late final Division? division; + + if (options.gameId != null) { + final game = + await ref.watch(archivedGameProvider(id: options.gameId!).future); + _variant = game.meta.variant; + _orientation = game.youAre ?? Side.white; + pgn = game.makePgn(); + opening = game.data.opening; + serverAnalysis = game.serverAnalysis; + division = game.meta.division; + } else { + _variant = options.standalone!.variant; + _orientation = options.standalone!.orientation; + pgn = options.standalone!.pgn; + opening = null; + serverAnalysis = null; + division = null; + } UciPath path = UciPath.empty; Move? lastMove; final game = PgnGame.parsePgn( - options.pgn, + pgn, initHeaders: () => options.isLichessGameAnalysis ? {} : { @@ -118,7 +112,9 @@ class AnalysisController extends _$AnalysisController final pgnHeaders = IMap(game.headers); final rootComments = IList(game.comments.map((c) => PgnComment.fromPgn(c))); - Future? openingFuture; + final isLocalEvaluationAllowed = options.isLichessGameAnalysis + ? pgnHeaders['Result'] != '*' + : options.standalone!.isLocalEvaluationAllowed; _root = Root.fromPgnGame( game, @@ -132,17 +128,9 @@ class AnalysisController extends _$AnalysisController path = path + branch.id; lastMove = branch.sanMove.move; } - if (isMainline && options.opening == null && branch.position.ply <= 5) { - openingFuture = _fetchOpening(root, path); - } }, ); - // wait for the opening to be fetched to recompute the branch opening - openingFuture?.then((_) { - _setPath(state.currentPath); - }); - final currentPath = options.initialMoveCursor == null ? _root.mainlinePath : path; final currentNode = _root.nodeAt(currentPath); @@ -151,9 +139,24 @@ class AnalysisController extends _$AnalysisController // analysis preferences change final prefs = ref.read(analysisPreferencesProvider); + final isEngineAllowed = engineSupportedVariants.contains(_variant); + + ref.onDispose(() { + _startEngineEvalTimer?.cancel(); + _engineEvalDebounce.dispose(); + if (isEngineAllowed) { + evaluationService.disposeEngine(); + } + serverAnalysisService.lastAnalysisEvent + .removeListener(_listenToServerAnalysisEvents); + }); + + serverAnalysisService.lastAnalysisEvent + .addListener(_listenToServerAnalysisEvents); + final analysisState = AnalysisState( - variant: options.variant, - id: options.id, + variant: _variant, + gameId: options.gameId, currentPath: currentPath, isOnMainline: _root.isOnMainline(currentPath), root: _root.view, @@ -161,13 +164,13 @@ class AnalysisController extends _$AnalysisController pgnHeaders: pgnHeaders, pgnRootComments: rootComments, lastMove: lastMove, - pov: options.orientation, - contextOpening: options.opening, - isLocalEvaluationAllowed: options.isLocalEvaluationAllowed, + pov: _orientation, + contextOpening: opening, + isLocalEvaluationAllowed: isLocalEvaluationAllowed, isLocalEvaluationEnabled: prefs.enableLocalEvaluation, - playersAnalysis: options.serverAnalysis, - acplChartData: - options.serverAnalysis != null ? _makeAcplChartData() : null, + playersAnalysis: serverAnalysis, + acplChartData: serverAnalysis != null ? _makeAcplChartData() : null, + division: division, ); if (analysisState.isEngineAvailable) { @@ -190,23 +193,25 @@ class AnalysisController extends _$AnalysisController } EvaluationContext get _evaluationContext => EvaluationContext( - variant: options.variant, + variant: _variant, initialPosition: _root.position, ); - void onUserMove(NormalMove move) { - if (!state.position.isLegal(move)) return; + void onUserMove(NormalMove move, {bool shouldReplace = false}) { + if (!state.requireValue.position.isLegal(move)) return; - if (isPromotionPawnMove(state.position, move)) { - state = state.copyWith(promotionMove: move); + if (isPromotionPawnMove(state.requireValue.position, move)) { + state = AsyncValue.data( + state.requireValue.copyWith(promotionMove: move), + ); return; } - // For the opening explorer, last played move should always be the mainline - final shouldReplace = options.id == standaloneOpeningExplorerId; - - final (newPath, isNewNode) = - _root.addMoveAt(state.currentPath, move, replace: shouldReplace); + final (newPath, isNewNode) = _root.addMoveAt( + state.requireValue.currentPath, + move, + replace: shouldReplace, + ); if (newPath != null) { _setPath( newPath, @@ -218,10 +223,10 @@ class AnalysisController extends _$AnalysisController void onPromotionSelection(Role? role) { if (role == null) { - state = state.copyWith(promotionMove: null); + state = AsyncData(state.requireValue.copyWith(promotionMove: null)); return; } - final promotionMove = state.promotionMove; + final promotionMove = state.requireValue.promotionMove; if (promotionMove != null) { final promotion = promotionMove.withPromotion(role); onUserMove(promotion); @@ -229,9 +234,11 @@ class AnalysisController extends _$AnalysisController } void userNext() { - if (!state.currentNode.hasChild) return; + final curState = state.requireValue; + if (!curState.currentNode.hasChild) return; _setPath( - state.currentPath + _root.nodeAt(state.currentPath).children.first.id, + curState.currentPath + + _root.nodeAt(curState.currentPath).children.first.id, replaying: true, ); } @@ -260,11 +267,12 @@ class AnalysisController extends _$AnalysisController } void toggleBoard() { - state = state.copyWith(pov: state.pov.opposite); + final curState = state.requireValue; + state = AsyncData(curState.copyWith(pov: curState.pov.opposite)); } void userPrevious() { - _setPath(state.currentPath.penultimate, replaying: true); + _setPath(state.requireValue.currentPath.penultimate, replaying: true); } @override @@ -285,7 +293,7 @@ class AnalysisController extends _$AnalysisController grandChild.isHidden = false; } } - state = state.copyWith(root: _root.view); + state = AsyncData(state.requireValue.copyWith(root: _root.view)); } @override @@ -296,15 +304,18 @@ class AnalysisController extends _$AnalysisController child.isHidden = true; } - state = state.copyWith(root: _root.view); + state = AsyncData(state.requireValue.copyWith(root: _root.view)); } @override void promoteVariation(UciPath path, bool toMainline) { _root.promoteAt(path, toMainline: toMainline); - state = state.copyWith( - isOnMainline: _root.isOnMainline(state.currentPath), - root: _root.view, + final curState = state.requireValue; + state = AsyncData( + curState.copyWith( + isOnMainline: _root.isOnMainline(curState.currentPath), + root: _root.view, + ), ); } @@ -319,11 +330,14 @@ class AnalysisController extends _$AnalysisController .read(analysisPreferencesProvider.notifier) .toggleEnableLocalEvaluation(); - state = state.copyWith( - isLocalEvaluationEnabled: !state.isLocalEvaluationEnabled, + final curState = state.requireValue; + state = AsyncData( + curState.copyWith( + isLocalEvaluationEnabled: !curState.isLocalEvaluationEnabled, + ), ); - if (state.isEngineAvailable) { + if (state.requireValue.isEngineAvailable) { final prefs = ref.read(analysisPreferencesProvider); await ref.read(evaluationServiceProvider).initEngine( _evaluationContext, @@ -353,9 +367,12 @@ class AnalysisController extends _$AnalysisController _root.updateAll((node) => node.eval = null); - state = state.copyWith( - currentNode: - AnalysisCurrentNode.fromNode(_root.nodeAt(state.currentPath)), + final curState = state.requireValue; + state = AsyncData( + curState.copyWith( + currentNode: + AnalysisCurrentNode.fromNode(_root.nodeAt(curState.currentPath)), + ), ); _startEngineEval(); @@ -377,17 +394,14 @@ class AnalysisController extends _$AnalysisController } void updatePgnHeader(String key, String value) { - final headers = state.pgnHeaders.add(key, value); - state = state.copyWith(pgnHeaders: headers); + final headers = state.requireValue.pgnHeaders.add(key, value); + state = AsyncData(state.requireValue.copyWith(pgnHeaders: headers)); } Future requestServerAnalysis() { - if (state.canRequestServerAnalysis) { + if (state.requireValue.canRequestServerAnalysis) { final service = ref.read(serverAnalysisServiceProvider); - return service.requestAnalysis( - options.id as GameAnyId, - options.orientation, - ); + return service.requestAnalysis(options.gameId!, _orientation); } return Future.error('Cannot request server analysis'); } @@ -405,12 +419,13 @@ class AnalysisController extends _$AnalysisController /// Makes a full PGN string (including headers and comments) of the current game state. String makeExportPgn() { - return _root.makePgn(state.pgnHeaders, state.pgnRootComments); + final curState = state.requireValue; + return _root.makePgn(curState.pgnHeaders, curState.pgnRootComments); } /// Makes a PGN string up to the current node only. String makeCurrentNodePgn() { - final nodes = _root.branchesOn(state.currentPath); + final nodes = _root.branchesOn(state.requireValue.currentPath); return nodes.map((node) => node.sanMove.san).join(' '); } @@ -420,7 +435,8 @@ class AnalysisController extends _$AnalysisController bool shouldRecomputeRootView = false, bool replaying = false, }) { - final pathChange = state.currentPath != path; + final curState = state.requireValue; + final pathChange = curState.currentPath != path; final (currentNode, opening) = _nodeOpeningAt(_root, path); // always show variation if the user plays a move @@ -437,9 +453,9 @@ class AnalysisController extends _$AnalysisController // or a variation is hidden/shown final rootView = shouldForceShowVariation || shouldRecomputeRootView ? _root.view - : state.root; + : curState.root; - final isForward = path.size > state.currentPath.size; + final isForward = path.size > curState.currentPath.size; if (currentNode is Branch) { if (!replaying) { if (isForward) { @@ -462,65 +478,72 @@ class AnalysisController extends _$AnalysisController } if (currentNode.opening == null && currentNode.position.ply <= 30) { - _fetchOpening(_root, path); + _fetchOpening(_root, path).then((opening) { + if (opening != null) { + _root.updateAt(path, (node) => node.opening = opening); + + final curState = state.requireValue; + if (curState.currentPath == path) { + state = AsyncData( + curState.copyWith( + currentNode: AnalysisCurrentNode.fromNode(_root.nodeAt(path)), + ), + ); + } + } + }); } - state = state.copyWith( - currentPath: path, - isOnMainline: _root.isOnMainline(path), - currentNode: AnalysisCurrentNode.fromNode(currentNode), - currentBranchOpening: opening, - lastMove: currentNode.sanMove.move, - promotionMove: null, - root: rootView, + state = AsyncData( + curState.copyWith( + currentPath: path, + isOnMainline: _root.isOnMainline(path), + currentNode: AnalysisCurrentNode.fromNode(currentNode), + currentBranchOpening: opening, + lastMove: currentNode.sanMove.move, + promotionMove: null, + root: rootView, + ), ); } else { - state = state.copyWith( - currentPath: path, - isOnMainline: _root.isOnMainline(path), - currentNode: AnalysisCurrentNode.fromNode(currentNode), - currentBranchOpening: opening, - lastMove: null, - promotionMove: null, - root: rootView, + state = AsyncData( + curState.copyWith( + currentPath: path, + isOnMainline: _root.isOnMainline(path), + currentNode: AnalysisCurrentNode.fromNode(currentNode), + currentBranchOpening: opening, + lastMove: null, + promotionMove: null, + root: rootView, + ), ); } - if (pathChange && state.isEngineAvailable) { + if (pathChange && curState.isEngineAvailable) { _debouncedStartEngineEval(); } } - Future _fetchOpening(Node fromNode, UciPath path) async { - if (!kOpeningAllowedVariants.contains(options.variant)) return; + Future _fetchOpening(Node fromNode, UciPath path) async { + if (!kOpeningAllowedVariants.contains(_variant)) return null; final moves = fromNode.branchesOn(path).map((node) => node.sanMove.move); - if (moves.isEmpty) return; - if (moves.length > 40) return; - - final opening = - await ref.read(openingServiceProvider).fetchFromMoves(moves); + if (moves.isEmpty) return null; + if (moves.length > 40) return null; - if (opening != null) { - fromNode.updateAt(path, (node) => node.opening = opening); - - if (state.currentPath == path) { - state = state.copyWith( - currentNode: AnalysisCurrentNode.fromNode(fromNode.nodeAt(path)), - ); - } - } + return ref.read(openingServiceProvider).fetchFromMoves(moves); } void _startEngineEval() { - if (!state.isEngineAvailable) return; + final curState = state.requireValue; + if (!curState.isEngineAvailable) return; ref .read(evaluationServiceProvider) .start( - state.currentPath, - _root.branchesOn(state.currentPath).map(Step.fromNode), + curState.currentPath, + _root.branchesOn(curState.currentPath).map(Step.fromNode), initialPositionEval: _root.eval, - shouldEmit: (work) => work.path == state.currentPath, + shouldEmit: (work) => work.path == curState.currentPath, ) ?.forEach( (t) => _root.updateAt(t.$1.path, (node) => node.eval = t.$2), @@ -536,23 +559,31 @@ class AnalysisController extends _$AnalysisController void _stopEngineEval() { ref.read(evaluationServiceProvider).stop(); // update the current node with last cached eval - state = state.copyWith( - currentNode: - AnalysisCurrentNode.fromNode(_root.nodeAt(state.currentPath)), + final curState = state.requireValue; + state = AsyncData( + curState.copyWith( + currentNode: + AnalysisCurrentNode.fromNode(_root.nodeAt(curState.currentPath)), + ), ); } void _listenToServerAnalysisEvents() { final event = ref.read(serverAnalysisServiceProvider).lastAnalysisEvent.value; - if (event != null && event.$1 == state.id) { + if (event != null && event.$1 == state.requireValue.gameId) { _mergeOngoingAnalysis(_root, event.$2.tree); - state = state.copyWith( - acplChartData: _makeAcplChartData(), - playersAnalysis: event.$2.analysis != null - ? (white: event.$2.analysis!.white, black: event.$2.analysis!.black) - : null, - root: _root.view, + state = AsyncData( + state.requireValue.copyWith( + acplChartData: _makeAcplChartData(), + playersAnalysis: event.$2.analysis != null + ? ( + white: event.$2.analysis!.white, + black: event.$2.analysis!.black + ) + : null, + root: _root.view, + ), ); } } @@ -651,8 +682,8 @@ class AnalysisState with _$AnalysisState { const AnalysisState._(); const factory AnalysisState({ - /// Analysis ID - required StringId id, + /// The ID of the game if it's a lichess game. + required GameId? gameId, /// The variant of the analysis. required Variant variant, @@ -697,6 +728,9 @@ class AnalysisState with _$AnalysisState { /// Optional server analysis to display player stats. ({PlayerAnalysis white, PlayerAnalysis black})? playersAnalysis, + /// Optional game division data, given by server analysis. + Division? division, + /// Optional ACPL chart data of the game, coming from lichess server analysis. IList? acplChartData, @@ -709,12 +743,8 @@ class AnalysisState with _$AnalysisState { IList? pgnRootComments, }) = _AnalysisState; - /// The game ID of the analysis, if it's a lichess game. - GameAnyId? get gameAnyId => - _isStandaloneAnalysis(id) ? null : GameAnyId(id.value); - /// Whether the analysis is for a lichess game. - bool get isLichessGameAnalysis => gameAnyId != null; + bool get isLichessGameAnalysis => gameId != null; IMap> get validMoves => makeLegalMoves( currentNode.position, @@ -725,10 +755,7 @@ class AnalysisState with _$AnalysisState { /// /// It must be a lichess game, which is finished and not already analyzed. bool get canRequestServerAnalysis => - gameAnyId != null && - (id.length == 8 || id.length == 12) && - !hasServerAnalysis && - pgnHeaders['Result'] != '*'; + gameId != null && !hasServerAnalysis && pgnHeaders['Result'] != '*'; bool get hasServerAnalysis => playersAnalysis != null; diff --git a/lib/src/model/analysis/server_analysis_service.dart b/lib/src/model/analysis/server_analysis_service.dart index 94665dfdac..ed73593547 100644 --- a/lib/src/model/analysis/server_analysis_service.dart +++ b/lib/src/model/analysis/server_analysis_service.dart @@ -40,11 +40,9 @@ class ServerAnalysisService { /// /// This will return a future that completes when the server analysis is /// launched (but not when it is finished). - Future requestAnalysis(GameAnyId id, [Side? side]) async { + Future requestAnalysis(GameId id, [Side? side]) async { final socketPool = ref.read(socketPoolProvider); - final uri = id.isFullId - ? Uri(path: '/play/$id/v6') - : Uri(path: '/watch/$id/${side?.name ?? Side.white}/v6'); + final uri = Uri(path: '/watch/$id/${side?.name ?? Side.white}/v6'); final socketClient = socketPool.open(uri); _socketSubscription?.$2.cancel(); diff --git a/lib/src/model/game/game_controller.dart b/lib/src/model/game/game_controller.dart index 60a7efd0b7..e663d77b04 100644 --- a/lib/src/model/game/game_controller.dart +++ b/lib/src/model/game/game_controller.dart @@ -12,7 +12,6 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/account/account_preferences.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; -import 'package:lichess_mobile/src/model/analysis/server_analysis_service.dart'; import 'package:lichess_mobile/src/model/clock/chess_clock.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; @@ -424,21 +423,6 @@ class GameController extends _$GameController { _socketClient.send('rematch-no', null); } - Future requestServerAnalysis() { - return state.mapOrNull( - data: (d) { - if (!d.value.game.finished) { - return Future.error( - 'Cannot request server analysis on a non finished game', - ); - } - final service = ref.read(serverAnalysisServiceProvider); - return service.requestAnalysis(gameFullId); - }, - ) ?? - Future.value(); - } - /// Gets the live game clock if available. LiveGameClock? get _liveClock => _clock != null ? ( @@ -1183,14 +1167,7 @@ class GameState with _$GameState { String get analysisPgn => game.makePgn(); AnalysisOptions get analysisOptions => AnalysisOptions( - pgn: analysisPgn, - isLocalEvaluationAllowed: true, - variant: game.meta.variant, initialMoveCursor: stepCursor, - orientation: game.youAre ?? Side.white, - id: gameFullId, - opening: game.meta.opening, - serverAnalysis: game.serverAnalysis, - division: game.meta.division, + gameId: gameFullId.gameId, ); } diff --git a/lib/src/view/analysis/analysis_board.dart b/lib/src/view/analysis/analysis_board.dart index 6e60630a7e..c6e70e8557 100644 --- a/lib/src/view/analysis/analysis_board.dart +++ b/lib/src/view/analysis/analysis_board.dart @@ -19,6 +19,7 @@ class AnalysisBoard extends ConsumerStatefulWidget { this.boardSize, { this.borderRadius, this.enableDrawingShapes = true, + this.shouldReplaceChildOnUserMove = false, }); final AnalysisOptions options; @@ -26,6 +27,7 @@ class AnalysisBoard extends ConsumerStatefulWidget { final BorderRadiusGeometry? borderRadius; final bool enableDrawingShapes; + final bool shouldReplaceChildOnUserMove; @override ConsumerState createState() => AnalysisBoardState(); @@ -37,7 +39,7 @@ class AnalysisBoardState extends ConsumerState { @override Widget build(BuildContext context) { final ctrlProvider = analysisControllerProvider(widget.options); - final analysisState = ref.watch(ctrlProvider); + final analysisState = ref.watch(ctrlProvider).requireValue; final boardPrefs = ref.watch(boardPreferencesProvider); final showBestMoveArrow = ref.watch( analysisPreferencesProvider.select( @@ -85,7 +87,10 @@ class AnalysisBoardState extends ConsumerState { validMoves: analysisState.validMoves, promotionMove: analysisState.promotionMove, onMove: (move, {isDrop, captured}) => - ref.read(ctrlProvider.notifier).onUserMove(move), + ref.read(ctrlProvider.notifier).onUserMove( + move, + shouldReplace: widget.shouldReplaceChildOnUserMove, + ), onPromotionSelection: (role) => ref.read(ctrlProvider.notifier).onPromotionSelection(role), ), diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index 2276f60d94..cbe72e7d3a 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -4,8 +4,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; -import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/model/game/game_repository_providers.dart'; import 'package:lichess_mobile/src/model/game/game_share_service.dart'; import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; @@ -25,103 +23,30 @@ import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; +import 'package:logging/logging.dart'; import '../../utils/share.dart'; import 'analysis_board.dart'; import 'analysis_settings.dart'; import 'tree_view.dart'; -class AnalysisScreen extends StatelessWidget { +final _logger = Logger('AnalysisScreen'); + +class AnalysisScreen extends ConsumerStatefulWidget { const AnalysisScreen({ required this.options, - required this.pgnOrId, this.enableDrawingShapes = true, }); - /// The analysis options. final AnalysisOptions options; - /// The PGN or game ID to load. - final String pgnOrId; - final bool enableDrawingShapes; @override - Widget build(BuildContext context) { - return pgnOrId.length == 8 && GameId(pgnOrId).isValid - ? _LoadGame( - GameId(pgnOrId), - options, - enableDrawingShapes: enableDrawingShapes, - ) - : _LoadedAnalysisScreen( - options: options.copyWith( - pgn: pgnOrId, - ), - enableDrawingShapes: enableDrawingShapes, - ); - } + ConsumerState createState() => _AnalysisScreenState(); } -class _LoadGame extends ConsumerWidget { - const _LoadGame( - this.gameId, - this.options, { - required this.enableDrawingShapes, - }); - - final AnalysisOptions options; - final GameId gameId; - - final bool enableDrawingShapes; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final gameAsync = ref.watch(archivedGameProvider(id: gameId)); - - return gameAsync.when( - data: (game) { - final serverAnalysis = - game.white.analysis != null && game.black.analysis != null - ? (white: game.white.analysis!, black: game.black.analysis!) - : null; - return _LoadedAnalysisScreen( - options: options.copyWith( - id: game.id, - opening: game.meta.opening, - division: game.meta.division, - serverAnalysis: serverAnalysis, - pgn: game.makePgn(), - ), - enableDrawingShapes: enableDrawingShapes, - ); - }, - loading: () => const Center(child: CircularProgressIndicator.adaptive()), - error: (error, _) { - return Center( - child: Text('Cannot load game analysis: $error'), - ); - }, - ); - } -} - -class _LoadedAnalysisScreen extends ConsumerStatefulWidget { - const _LoadedAnalysisScreen({ - required this.options, - required this.enableDrawingShapes, - }); - - final AnalysisOptions options; - - final bool enableDrawingShapes; - - @override - ConsumerState<_LoadedAnalysisScreen> createState() => - _LoadedAnalysisScreenState(); -} - -class _LoadedAnalysisScreenState extends ConsumerState<_LoadedAnalysisScreen> +class _AnalysisScreenState extends ConsumerState with SingleTickerProviderStateMixin { late final TabController _tabController; late final List tabs; @@ -132,7 +57,7 @@ class _LoadedAnalysisScreenState extends ConsumerState<_LoadedAnalysisScreen> tabs = [ AnalysisTab.opening, AnalysisTab.moves, - if (widget.options.canShowGameSummary) AnalysisTab.summary, + if (widget.options.isLichessGameAnalysis) AnalysisTab.summary, ]; _tabController = TabController( @@ -151,44 +76,64 @@ class _LoadedAnalysisScreenState extends ConsumerState<_LoadedAnalysisScreen> @override Widget build(BuildContext context) { final ctrlProvider = analysisControllerProvider(widget.options); - final currentNodeEval = - ref.watch(ctrlProvider.select((value) => value.currentNode.eval)); + final asyncState = ref.watch(ctrlProvider); + + final appBarActions = [ + EngineDepth(defaultEval: asyncState.valueOrNull?.currentNode.eval), + AppBarAnalysisTabIndicator( + tabs: tabs, + controller: _tabController, + ), + AppBarIconButton( + onPressed: () => showAdaptiveBottomSheet( + context: context, + isScrollControlled: true, + showDragHandle: true, + isDismissible: true, + builder: (_) => AnalysisSettings(widget.options), + ), + semanticsLabel: context.l10n.settingsSettings, + icon: const Icon(Icons.settings), + ), + ]; - return PlatformScaffold( - resizeToAvoidBottomInset: false, - appBar: PlatformAppBar( - title: _Title(options: widget.options), - actions: [ - EngineDepth(defaultEval: currentNodeEval), - AppBarAnalysisTabIndicator( - tabs: tabs, + switch (asyncState) { + case AsyncData(:final value): + return PlatformScaffold( + resizeToAvoidBottomInset: false, + appBar: PlatformAppBar( + title: _Title(variant: value.variant), + actions: appBarActions, + ), + body: _Body( controller: _tabController, + options: widget.options, + enableDrawingShapes: widget.enableDrawingShapes, ), - AppBarIconButton( - onPressed: () => showAdaptiveBottomSheet( - context: context, - isScrollControlled: true, - showDragHandle: true, - isDismissible: true, - builder: (_) => AnalysisSettings(widget.options), - ), - semanticsLabel: context.l10n.settingsSettings, - icon: const Icon(Icons.settings), + ); + case AsyncError(:final error, :final stackTrace): + _logger.severe('Cannot load study: $error', stackTrace); + return FullScreenRetryRequest( + onRetry: () { + ref.invalidate(ctrlProvider); + }, + ); + case _: + return PlatformScaffold( + resizeToAvoidBottomInset: false, + appBar: PlatformAppBar( + title: const _Title(variant: Variant.standard), + actions: appBarActions, ), - ], - ), - body: _Body( - controller: _tabController, - options: widget.options, - enableDrawingShapes: widget.enableDrawingShapes, - ), - ); + body: const Center(child: CircularProgressIndicator()), + ); + } } } class _Title extends StatelessWidget { - const _Title({required this.options}); - final AnalysisOptions options; + const _Title({required this.variant}); + final Variant variant; static const excludedIcons = [Variant.standard, Variant.fromPosition]; @@ -197,8 +142,8 @@ class _Title extends StatelessWidget { return Row( mainAxisSize: MainAxisSize.min, children: [ - if (!excludedIcons.contains(options.variant)) ...[ - Icon(options.variant.icon), + if (!excludedIcons.contains(variant)) ...[ + Icon(variant.icon), const SizedBox(width: 5.0), ], Text(context.l10n.analysis), @@ -225,7 +170,7 @@ class _Body extends ConsumerWidget { ); final ctrlProvider = analysisControllerProvider(options); - final analysisState = ref.watch(ctrlProvider); + final analysisState = ref.watch(ctrlProvider).requireValue; final isEngineAvailable = analysisState.isEngineAvailable; final hasEval = analysisState.hasAvailableEval; @@ -269,7 +214,7 @@ class _Body extends ConsumerWidget { children: [ OpeningExplorerView(options: options), AnalysisTreeView(options), - if (options.canShowGameSummary) ServerAnalysisSummary(options), + if (options.isLichessGameAnalysis) ServerAnalysisSummary(options), ], ); } @@ -283,7 +228,7 @@ class _BottomBar extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final ctrlProvider = analysisControllerProvider(options); - final analysisState = ref.watch(ctrlProvider); + final analysisState = ref.watch(ctrlProvider).requireValue; return BottomBar( children: [ @@ -344,7 +289,8 @@ class _BottomBar extends ConsumerWidget { BottomSheetAction( makeLabel: (context) => Text(context.l10n.boardEditor), onPressed: (context) { - final analysisState = ref.read(analysisControllerProvider(options)); + final analysisState = + ref.read(analysisControllerProvider(options)).requireValue; final boardFen = analysisState.position.fen; pushPlatformRoute( context, @@ -368,26 +314,27 @@ class _BottomBar extends ConsumerWidget { BottomSheetAction( makeLabel: (context) => Text(context.l10n.mobileSharePositionAsFEN), onPressed: (_) { - final analysisState = ref.read(analysisControllerProvider(options)); + final analysisState = + ref.read(analysisControllerProvider(options)).requireValue; launchShareDialog( context, text: analysisState.position.fen, ); }, ), - if (options.gameAnyId != null) + if (options.gameId != null) BottomSheetAction( makeLabel: (context) => Text(context.l10n.screenshotCurrentPosition), onPressed: (_) async { - final gameId = options.gameAnyId!.gameId; + final gameId = options.gameId!; final analysisState = - ref.read(analysisControllerProvider(options)); + ref.read(analysisControllerProvider(options)).requireValue; try { final image = await ref.read(gameShareServiceProvider).screenshotPosition( gameId, - options.orientation, + analysisState.pov, analysisState.position.fen, analysisState.lastMove, ); diff --git a/lib/src/view/analysis/analysis_settings.dart b/lib/src/view/analysis/analysis_settings.dart index 380687363a..f7101bd8c4 100644 --- a/lib/src/view/analysis/analysis_settings.dart +++ b/lib/src/view/analysis/analysis_settings.dart @@ -6,6 +6,7 @@ import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_settings.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; +import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/non_linear_slider.dart'; import 'package:lichess_mobile/src/widgets/settings.dart'; @@ -18,123 +19,127 @@ class AnalysisSettings extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final ctrlProvider = analysisControllerProvider(options); - final isLocalEvaluationAllowed = - ref.watch(ctrlProvider.select((s) => s.isLocalEvaluationAllowed)); - final isEngineAvailable = ref.watch( - ctrlProvider.select((s) => s.isEngineAvailable), - ); final prefs = ref.watch(analysisPreferencesProvider); + final asyncState = ref.watch(ctrlProvider); - return BottomSheetScrollableContainer( - children: [ - PlatformListTile( - title: Text(context.l10n.openingExplorer), - onTap: () => showAdaptiveBottomSheet( - context: context, - isScrollControlled: true, - showDragHandle: true, - isDismissible: true, - builder: (_) => OpeningExplorerSettings(options), - ), - trailing: const Icon(CupertinoIcons.chevron_right), - ), - SwitchSettingTile( - title: Text(context.l10n.toggleLocalEvaluation), - value: prefs.enableLocalEvaluation, - onChanged: isLocalEvaluationAllowed - ? (_) { - ref.read(ctrlProvider.notifier).toggleLocalEvaluation(); - } - : null, - ), - PlatformListTile( - title: Text.rich( - TextSpan( - text: '${context.l10n.multipleLines}: ', - style: const TextStyle( - fontWeight: FontWeight.normal, + switch (asyncState) { + case AsyncData(:final value): + return BottomSheetScrollableContainer( + children: [ + PlatformListTile( + title: Text(context.l10n.openingExplorer), + onTap: () => showAdaptiveBottomSheet( + context: context, + isScrollControlled: true, + showDragHandle: true, + isDismissible: true, + builder: (_) => OpeningExplorerSettings(options), ), - children: [ + trailing: const Icon(CupertinoIcons.chevron_right), + ), + SwitchSettingTile( + title: Text(context.l10n.toggleLocalEvaluation), + value: prefs.enableLocalEvaluation, + onChanged: value.isLocalEvaluationAllowed + ? (_) { + ref.read(ctrlProvider.notifier).toggleLocalEvaluation(); + } + : null, + ), + PlatformListTile( + title: Text.rich( TextSpan( + text: '${context.l10n.multipleLines}: ', style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, + fontWeight: FontWeight.normal, ), - text: prefs.numEvalLines.toString(), + children: [ + TextSpan( + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + text: prefs.numEvalLines.toString(), + ), + ], ), - ], + ), + subtitle: NonLinearSlider( + value: prefs.numEvalLines, + values: const [0, 1, 2, 3], + onChangeEnd: value.isEngineAvailable + ? (value) => ref + .read(ctrlProvider.notifier) + .setNumEvalLines(value.toInt()) + : null, + ), ), - ), - subtitle: NonLinearSlider( - value: prefs.numEvalLines, - values: const [0, 1, 2, 3], - onChangeEnd: isEngineAvailable - ? (value) => ref - .read(ctrlProvider.notifier) - .setNumEvalLines(value.toInt()) - : null, - ), - ), - if (maxEngineCores > 1) - PlatformListTile( - title: Text.rich( - TextSpan( - text: '${context.l10n.cpus}: ', - style: const TextStyle( - fontWeight: FontWeight.normal, - ), - children: [ + if (maxEngineCores > 1) + PlatformListTile( + title: Text.rich( TextSpan( + text: '${context.l10n.cpus}: ', style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, + fontWeight: FontWeight.normal, ), - text: prefs.numEngineCores.toString(), + children: [ + TextSpan( + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + text: prefs.numEngineCores.toString(), + ), + ], ), - ], + ), + subtitle: NonLinearSlider( + value: prefs.numEngineCores, + values: List.generate(maxEngineCores, (index) => index + 1), + onChangeEnd: value.isEngineAvailable + ? (value) => ref + .read(ctrlProvider.notifier) + .setEngineCores(value.toInt()) + : null, + ), ), - ), - subtitle: NonLinearSlider( - value: prefs.numEngineCores, - values: List.generate(maxEngineCores, (index) => index + 1), - onChangeEnd: isEngineAvailable + SwitchSettingTile( + title: Text(context.l10n.bestMoveArrow), + value: prefs.showBestMoveArrow, + onChanged: value.isEngineAvailable ? (value) => ref - .read(ctrlProvider.notifier) - .setEngineCores(value.toInt()) + .read(analysisPreferencesProvider.notifier) + .toggleShowBestMoveArrow() : null, ), - ), - SwitchSettingTile( - title: Text(context.l10n.bestMoveArrow), - value: prefs.showBestMoveArrow, - onChanged: isEngineAvailable - ? (value) => ref + SwitchSettingTile( + title: Text(context.l10n.evaluationGauge), + value: prefs.showEvaluationGauge, + onChanged: (value) => ref + .read(analysisPreferencesProvider.notifier) + .toggleShowEvaluationGauge(), + ), + SwitchSettingTile( + title: Text(context.l10n.toggleGlyphAnnotations), + value: prefs.showAnnotations, + onChanged: (_) => ref .read(analysisPreferencesProvider.notifier) - .toggleShowBestMoveArrow() - : null, - ), - SwitchSettingTile( - title: Text(context.l10n.evaluationGauge), - value: prefs.showEvaluationGauge, - onChanged: (value) => ref - .read(analysisPreferencesProvider.notifier) - .toggleShowEvaluationGauge(), - ), - SwitchSettingTile( - title: Text(context.l10n.toggleGlyphAnnotations), - value: prefs.showAnnotations, - onChanged: (_) => ref - .read(analysisPreferencesProvider.notifier) - .toggleAnnotations(), - ), - SwitchSettingTile( - title: Text(context.l10n.mobileShowComments), - value: prefs.showPgnComments, - onChanged: (_) => ref - .read(analysisPreferencesProvider.notifier) - .togglePgnComments(), - ), - ], - ); + .toggleAnnotations(), + ), + SwitchSettingTile( + title: Text(context.l10n.mobileShowComments), + value: prefs.showPgnComments, + onChanged: (_) => ref + .read(analysisPreferencesProvider.notifier) + .togglePgnComments(), + ), + ], + ); + case AsyncError(:final error): + debugPrint('Error loading analysis: $error'); + return const SizedBox.shrink(); + case _: + return const CenterLoadingIndicator(); + } } } diff --git a/lib/src/view/analysis/analysis_share_screen.dart b/lib/src/view/analysis/analysis_share_screen.dart index 785e8b0cf7..63552ffe2e 100644 --- a/lib/src/view/analysis/analysis_share_screen.dart +++ b/lib/src/view/analysis/analysis_share_screen.dart @@ -56,7 +56,7 @@ class _EditPgnTagsFormState extends ConsumerState<_EditPgnTagsForm> { void initState() { super.initState(); final ctrlProvider = analysisControllerProvider(widget.options); - final pgnHeaders = ref.read(ctrlProvider).pgnHeaders; + final pgnHeaders = ref.read(ctrlProvider).requireValue.pgnHeaders; for (final entry in pgnHeaders.entries) { _controllers[entry.key] = TextEditingController(text: entry.value); @@ -86,7 +86,8 @@ class _EditPgnTagsFormState extends ConsumerState<_EditPgnTagsForm> { @override Widget build(BuildContext context) { final ctrlProvider = analysisControllerProvider(widget.options); - final pgnHeaders = ref.watch(ctrlProvider.select((c) => c.pgnHeaders)); + final pgnHeaders = + ref.watch(ctrlProvider.select((c) => c.requireValue.pgnHeaders)); final showRatingAsync = ref.watch(showRatingsPrefProvider); void focusAndSelectNextField(int index, IMap pgnHeaders) { diff --git a/lib/src/view/analysis/opening_explorer_view.dart b/lib/src/view/analysis/opening_explorer_view.dart index b06f1efc34..5f82d006dc 100644 --- a/lib/src/view/analysis/opening_explorer_view.dart +++ b/lib/src/view/analysis/opening_explorer_view.dart @@ -35,7 +35,8 @@ class _OpeningExplorerState extends ConsumerState { @override Widget build(BuildContext context) { - final analysisState = ref.watch(analysisControllerProvider(widget.options)); + final analysisState = + ref.watch(analysisControllerProvider(widget.options)).requireValue; if (analysisState.position.ply >= 50) { return _OpeningExplorerView( diff --git a/lib/src/view/analysis/server_analysis.dart b/lib/src/view/analysis/server_analysis.dart index ba0ac8f359..d943bddb25 100644 --- a/lib/src/view/analysis/server_analysis.dart +++ b/lib/src/view/analysis/server_analysis.dart @@ -23,16 +23,17 @@ class ServerAnalysisSummary extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final ctrlProvider = analysisControllerProvider(options); - final playersAnalysis = - ref.watch(ctrlProvider.select((value) => value.playersAnalysis)); - final pgnHeaders = - ref.watch(ctrlProvider.select((value) => value.pgnHeaders)); + final playersAnalysis = ref.watch( + ctrlProvider.select((value) => value.requireValue.playersAnalysis), + ); + final pgnHeaders = ref + .watch(ctrlProvider.select((value) => value.requireValue.pgnHeaders)); final currentGameAnalysis = ref.watch(currentAnalysisProvider); return playersAnalysis != null ? ListView( children: [ - if (currentGameAnalysis == options.gameAnyId?.gameId) + if (currentGameAnalysis == options.gameId) const Padding( padding: EdgeInsets.only(top: 16.0), child: WaitingForServerAnalysis(), @@ -164,7 +165,7 @@ class ServerAnalysisSummary extends ConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ const Spacer(), - if (currentGameAnalysis == options.gameAnyId?.gameId) + if (currentGameAnalysis == options.gameId) const Center( child: Padding( padding: EdgeInsets.symmetric(vertical: 16.0), @@ -356,23 +357,11 @@ class AcplChart extends ConsumerWidget { ), ); - final data = ref.watch( - analysisControllerProvider(options) - .select((value) => value.acplChartData), - ); - - final rootPly = ref.watch( - analysisControllerProvider(options) - .select((value) => value.root.position.ply), - ); - - final currentNode = ref.watch( - analysisControllerProvider(options).select((value) => value.currentNode), - ); - - final isOnMainline = ref.watch( - analysisControllerProvider(options).select((value) => value.isOnMainline), - ); + final state = ref.watch(analysisControllerProvider(options)).requireValue; + final data = state.acplChartData; + final rootPly = state.root.position.ply; + final currentNode = state.currentNode; + final isOnMainline = state.isOnMainline; if (data == null) { return const SizedBox.shrink(); @@ -386,12 +375,12 @@ class AcplChart extends ConsumerWidget { final divisionLines = []; - if (options.division?.middlegame != null) { - if (options.division!.middlegame! > 0) { + if (state.division?.middlegame != null) { + if (state.division!.middlegame! > 0) { divisionLines.add(phaseVerticalBar(0.0, context.l10n.opening)); divisionLines.add( phaseVerticalBar( - options.division!.middlegame! - 1, + state.division!.middlegame! - 1, context.l10n.middlegame, ), ); @@ -400,11 +389,11 @@ class AcplChart extends ConsumerWidget { } } - if (options.division?.endgame != null) { - if (options.division!.endgame! > 0) { + if (state.division?.endgame != null) { + if (state.division!.endgame! > 0) { divisionLines.add( phaseVerticalBar( - options.division!.endgame! - 1, + state.division!.endgame! - 1, context.l10n.endgame, ), ); diff --git a/lib/src/view/analysis/tree_view.dart b/lib/src/view/analysis/tree_view.dart index de648427d7..54c55d7bce 100644 --- a/lib/src/view/analysis/tree_view.dart +++ b/lib/src/view/analysis/tree_view.dart @@ -17,15 +17,20 @@ class AnalysisTreeView extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final ctrlProvider = analysisControllerProvider(options); - final root = ref.watch(ctrlProvider.select((value) => value.root)); - final currentPath = - ref.watch(ctrlProvider.select((value) => value.currentPath)); - final pgnRootComments = - ref.watch(ctrlProvider.select((value) => value.pgnRootComments)); + final variant = ref.watch( + ctrlProvider.select((value) => value.requireValue.variant), + ); + final root = + ref.watch(ctrlProvider.select((value) => value.requireValue.root)); + final currentPath = ref + .watch(ctrlProvider.select((value) => value.requireValue.currentPath)); + final pgnRootComments = ref.watch( + ctrlProvider.select((value) => value.requireValue.pgnRootComments), + ); return CustomScrollView( slivers: [ - if (kOpeningAllowedVariants.contains(options.variant)) + if (kOpeningAllowedVariants.contains(variant)) SliverPersistentHeader( delegate: _OpeningHeaderDelegate(ctrlProvider), ), @@ -76,14 +81,14 @@ class _Opening extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final isRootNode = ref.watch( - ctrlProvider.select((s) => s.currentNode.isRoot), + ctrlProvider.select((s) => s.requireValue.currentNode.isRoot), ); - final nodeOpening = - ref.watch(ctrlProvider.select((s) => s.currentNode.opening)); - final branchOpening = - ref.watch(ctrlProvider.select((s) => s.currentBranchOpening)); + final nodeOpening = ref + .watch(ctrlProvider.select((s) => s.requireValue.currentNode.opening)); + final branchOpening = ref + .watch(ctrlProvider.select((s) => s.requireValue.currentBranchOpening)); final contextOpening = - ref.watch(ctrlProvider.select((s) => s.contextOpening)); + ref.watch(ctrlProvider.select((s) => s.requireValue.contextOpening)); final opening = isRootNode ? LightOpening( eco: '', diff --git a/lib/src/view/board_editor/board_editor_screen.dart b/lib/src/view/board_editor/board_editor_screen.dart index 7b4006a4ba..a4269e7873 100644 --- a/lib/src/view/board_editor/board_editor_screen.dart +++ b/lib/src/view/board_editor/board_editor_screen.dart @@ -335,13 +335,13 @@ class _BottomBar extends ConsumerWidget { context, rootNavigator: true, builder: (context) => AnalysisScreen( - pgnOrId: editorState.pgn!, options: AnalysisOptions( - pgn: '', - isLocalEvaluationAllowed: true, - variant: Variant.fromPosition, - orientation: editorState.orientation, - id: standaloneAnalysisId, + standalone: ( + pgn: editorState.pgn!, + isLocalEvaluationAllowed: true, + variant: Variant.fromPosition, + orientation: editorState.orientation, + ), ), ), ); diff --git a/lib/src/view/correspondence/offline_correspondence_game_screen.dart b/lib/src/view/correspondence/offline_correspondence_game_screen.dart index c816be5561..45d3ccd843 100644 --- a/lib/src/view/correspondence/offline_correspondence_game_screen.dart +++ b/lib/src/view/correspondence/offline_correspondence_game_screen.dart @@ -259,15 +259,14 @@ class _BodyState extends ConsumerState<_Body> { pushPlatformRoute( context, builder: (_) => AnalysisScreen( - pgnOrId: game.makePgn(), options: AnalysisOptions( - pgn: '', - isLocalEvaluationAllowed: false, - variant: game.variant, + standalone: ( + pgn: game.makePgn(), + isLocalEvaluationAllowed: false, + variant: game.variant, + orientation: game.youAre, + ), initialMoveCursor: stepCursor, - orientation: game.youAre, - id: game.id, - division: game.meta.division, ), ), ); diff --git a/lib/src/view/game/archived_game_screen.dart b/lib/src/view/game/archived_game_screen.dart index a8b0193202..b6abcc72da 100644 --- a/lib/src/view/game/archived_game_screen.dart +++ b/lib/src/view/game/archived_game_screen.dart @@ -375,27 +375,10 @@ class _BottomBar extends ConsumerWidget { label: context.l10n.gameAnalysis, onTap: ref.read(gameCursorProvider(gameData.id)).hasValue ? () { - final (game, cursor) = ref - .read( - gameCursorProvider(gameData.id), - ) - .requireValue; - pushPlatformRoute( context, builder: (context) => AnalysisScreen( - pgnOrId: game.makePgn(), - options: AnalysisOptions( - pgn: '', - isLocalEvaluationAllowed: true, - variant: gameData.variant, - initialMoveCursor: cursor, - orientation: orientation, - id: gameData.id, - opening: gameData.opening, - serverAnalysis: game.serverAnalysis, - division: game.meta.division, - ), + options: AnalysisOptions(gameId: gameData.id), ), ); } diff --git a/lib/src/view/game/game_body.dart b/lib/src/view/game/game_body.dart index 77a5a209d8..e6b6a6b341 100644 --- a/lib/src/view/game/game_body.dart +++ b/lib/src/view/game/game_body.dart @@ -569,7 +569,6 @@ class _GameBottomBar extends ConsumerWidget { pushPlatformRoute( context, builder: (_) => AnalysisScreen( - pgnOrId: gameState.analysisPgn, options: gameState.analysisOptions, ), ); @@ -702,9 +701,8 @@ class _GameBottomBar extends ConsumerWidget { pushPlatformRoute( context, builder: (_) => AnalysisScreen( - pgnOrId: gameState.analysisPgn, options: gameState.analysisOptions.copyWith( - isLocalEvaluationAllowed: false, + gameId: gameState.game.id, ), ), ); diff --git a/lib/src/view/game/game_list_tile.dart b/lib/src/view/game/game_list_tile.dart index c54d6c5106..1f7417d3db 100644 --- a/lib/src/view/game/game_list_tile.dart +++ b/lib/src/view/game/game_list_tile.dart @@ -226,14 +226,7 @@ class _ContextMenu extends ConsumerWidget { pushPlatformRoute( context, builder: (context) => AnalysisScreen( - pgnOrId: game.id.value, - options: AnalysisOptions( - pgn: '', - isLocalEvaluationAllowed: true, - variant: game.variant, - orientation: orientation, - id: game.id, - ), + options: AnalysisOptions(gameId: game.id), ), ); } diff --git a/lib/src/view/game/game_result_dialog.dart b/lib/src/view/game/game_result_dialog.dart index 20eeb61b7b..4e09a5cf9d 100644 --- a/lib/src/view/game/game_result_dialog.dart +++ b/lib/src/view/game/game_result_dialog.dart @@ -191,7 +191,6 @@ class _GameEndDialogState extends ConsumerState { pushPlatformRoute( context, builder: (_) => AnalysisScreen( - pgnOrId: gameState.analysisPgn, options: gameState.analysisOptions, ), ); @@ -261,13 +260,13 @@ class OverTheBoardGameResultDialog extends StatelessWidget { pushPlatformRoute( context, builder: (_) => AnalysisScreen( - pgnOrId: game.makePgn(), options: AnalysisOptions( - pgn: '', - isLocalEvaluationAllowed: true, - variant: game.meta.variant, - orientation: Side.white, - id: standaloneAnalysisId, + standalone: ( + pgn: game.makePgn(), + isLocalEvaluationAllowed: true, + variant: game.meta.variant, + orientation: Side.white, + ), ), ), ); diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index 6823005fed..063a134d3b 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -17,6 +17,7 @@ import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/move_list.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; @@ -50,121 +51,204 @@ class _OpeningExplorerState extends ConsumerState { @override Widget build(BuildContext context) { - final analysisState = ref.watch(analysisControllerProvider(widget.options)); - - final opening = analysisState.currentNode.isRoot - ? LightOpening( - eco: '', - name: context.l10n.startPosition, - ) - : analysisState.currentNode.opening ?? - analysisState.currentBranchOpening ?? - analysisState.contextOpening; - - final Widget openingHeader = Container( - padding: _kTableRowPadding, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.secondaryContainer, - ), - child: opening != null - ? GestureDetector( - onTap: opening.name == context.l10n.startPosition - ? null - : () => launchUrl( - Uri.parse( - 'https://lichess.org/opening/${opening.name}', - ), - ), - child: Row( - children: [ - Icon( - Icons.open_in_browser_outlined, - color: Theme.of(context).colorScheme.onSecondaryContainer, - ), - const SizedBox(width: 6.0), - Expanded( - child: Text( - opening.name, - style: TextStyle( + switch (ref.watch(analysisControllerProvider(widget.options))) { + case AsyncData(value: final analysisState): + final opening = analysisState.currentNode.isRoot + ? LightOpening( + eco: '', + name: context.l10n.startPosition, + ) + : analysisState.currentNode.opening ?? + analysisState.currentBranchOpening ?? + analysisState.contextOpening; + + final Widget openingHeader = Container( + padding: _kTableRowPadding, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.secondaryContainer, + ), + child: opening != null + ? GestureDetector( + onTap: opening.name == context.l10n.startPosition + ? null + : () => launchUrl( + Uri.parse( + 'https://lichess.org/opening/${opening.name}', + ), + ), + child: Row( + children: [ + Icon( + Icons.open_in_browser_outlined, color: Theme.of(context).colorScheme.onSecondaryContainer, - fontWeight: FontWeight.bold, ), - ), + const SizedBox(width: 6.0), + Expanded( + child: Text( + opening.name, + style: TextStyle( + color: Theme.of(context) + .colorScheme + .onSecondaryContainer, + fontWeight: FontWeight.bold, + ), + ), + ), + ], ), - ], - ), - ) - : const SizedBox.shrink(), - ); + ) + : const SizedBox.shrink(), + ); - if (analysisState.position.ply >= 50) { - return _OpeningExplorerView( - options: widget.options, - isLoading: false, - isIndexing: false, - children: [ - openingHeader, - OpeningExplorerMoveTable.maxDepth( + if (analysisState.position.ply >= 50) { + return _OpeningExplorerView( options: widget.options, - ), - ], - ); - } + isLoading: false, + isIndexing: false, + children: [ + openingHeader, + OpeningExplorerMoveTable.maxDepth( + options: widget.options, + ), + ], + ); + } - final prefs = ref.watch(openingExplorerPreferencesProvider); + final prefs = ref.watch(openingExplorerPreferencesProvider); - if (prefs.db == OpeningDatabase.player && prefs.playerDb.username == null) { - return _OpeningExplorerView( - options: widget.options, - isLoading: false, - isIndexing: false, - children: [ - openingHeader, - const Padding( - padding: _kTableRowPadding, - child: Center( - // TODO: l10n - child: Text('Select a Lichess player in the settings.'), - ), - ), - ], - ); - } + if (prefs.db == OpeningDatabase.player && + prefs.playerDb.username == null) { + return _OpeningExplorerView( + options: widget.options, + isLoading: false, + isIndexing: false, + children: [ + openingHeader, + const Padding( + padding: _kTableRowPadding, + child: Center( + // TODO: l10n + child: Text('Select a Lichess player in the settings.'), + ), + ), + ], + ); + } + + final cacheKey = OpeningExplorerCacheKey( + fen: analysisState.position.fen, + prefs: prefs, + ); + final cacheOpeningExplorer = cache[cacheKey]; + final openingExplorerAsync = cacheOpeningExplorer != null + ? AsyncValue.data( + (entry: cacheOpeningExplorer, isIndexing: false), + ) + : ref.watch( + openingExplorerProvider(fen: analysisState.position.fen), + ); + + if (cacheOpeningExplorer == null) { + ref.listen(openingExplorerProvider(fen: analysisState.position.fen), + (_, curAsync) { + curAsync.whenData((cur) { + if (cur != null && !cur.isIndexing) { + cache[cacheKey] = cur.entry; + } + }); + }); + } + + final isLoading = openingExplorerAsync.isLoading || + openingExplorerAsync.value == null; + + return _OpeningExplorerView( + options: widget.options, + isLoading: isLoading, + isIndexing: openingExplorerAsync.value?.isIndexing ?? false, + children: openingExplorerAsync.when( + data: (openingExplorer) { + if (openingExplorer == null) { + return lastExplorerWidgets ?? + [ + Shimmer( + child: ShimmerLoading( + isLoading: true, + child: OpeningExplorerMoveTable.loading( + options: widget.options, + ), + ), + ), + ]; + } + + final topGames = openingExplorer.entry.topGames; + final recentGames = openingExplorer.entry.recentGames; + + final ply = analysisState.position.ply; + + final children = [ + openingHeader, + OpeningExplorerMoveTable( + moves: openingExplorer.entry.moves, + whiteWins: openingExplorer.entry.white, + draws: openingExplorer.entry.draws, + blackWins: openingExplorer.entry.black, + options: widget.options, + ), + if (topGames != null && topGames.isNotEmpty) ...[ + OpeningExplorerHeaderTile( + key: const Key('topGamesHeader'), + child: Text(context.l10n.topGames), + ), + ...List.generate( + topGames.length, + (int index) { + return OpeningExplorerGameTile( + key: Key('top-game-${topGames.get(index).id}'), + game: topGames.get(index), + color: index.isEven + ? Theme.of(context).colorScheme.surfaceContainerLow + : Theme.of(context) + .colorScheme + .surfaceContainerHigh, + ply: ply, + ); + }, + growable: false, + ), + ], + if (recentGames != null && recentGames.isNotEmpty) ...[ + OpeningExplorerHeaderTile( + key: const Key('recentGamesHeader'), + child: Text(context.l10n.recentGames), + ), + ...List.generate( + recentGames.length, + (int index) { + return OpeningExplorerGameTile( + key: Key('recent-game-${recentGames.get(index).id}'), + game: recentGames.get(index), + color: index.isEven + ? Theme.of(context).colorScheme.surfaceContainerLow + : Theme.of(context) + .colorScheme + .surfaceContainerHigh, + ply: ply, + ); + }, + growable: false, + ), + ], + ]; - final cacheKey = OpeningExplorerCacheKey( - fen: analysisState.position.fen, - prefs: prefs, - ); - final cacheOpeningExplorer = cache[cacheKey]; - final openingExplorerAsync = cacheOpeningExplorer != null - ? AsyncValue.data( - (entry: cacheOpeningExplorer, isIndexing: false), - ) - : ref.watch(openingExplorerProvider(fen: analysisState.position.fen)); - - if (cacheOpeningExplorer == null) { - ref.listen(openingExplorerProvider(fen: analysisState.position.fen), - (_, curAsync) { - curAsync.whenData((cur) { - if (cur != null && !cur.isIndexing) { - cache[cacheKey] = cur.entry; - } - }); - }); - } + lastExplorerWidgets = children; - final isLoading = - openingExplorerAsync.isLoading || openingExplorerAsync.value == null; - - return _OpeningExplorerView( - options: widget.options, - isLoading: isLoading, - isIndexing: openingExplorerAsync.value?.isIndexing ?? false, - children: openingExplorerAsync.when( - data: (openingExplorer) { - if (openingExplorer == null) { - return lastExplorerWidgets ?? + return children; + }, + loading: () => + lastExplorerWidgets ?? [ Shimmer( child: ShimmerLoading( @@ -174,96 +258,35 @@ class _OpeningExplorerState extends ConsumerState { ), ), ), - ]; - } - - final topGames = openingExplorer.entry.topGames; - final recentGames = openingExplorer.entry.recentGames; - - final ply = analysisState.position.ply; - - final children = [ - openingHeader, - OpeningExplorerMoveTable( - moves: openingExplorer.entry.moves, - whiteWins: openingExplorer.entry.white, - draws: openingExplorer.entry.draws, - blackWins: openingExplorer.entry.black, - options: widget.options, - ), - if (topGames != null && topGames.isNotEmpty) ...[ - OpeningExplorerHeaderTile( - key: const Key('topGamesHeader'), - child: Text(context.l10n.topGames), - ), - ...List.generate( - topGames.length, - (int index) { - return OpeningExplorerGameTile( - key: Key('top-game-${topGames.get(index).id}'), - game: topGames.get(index), - color: index.isEven - ? Theme.of(context).colorScheme.surfaceContainerLow - : Theme.of(context).colorScheme.surfaceContainerHigh, - ply: ply, - ); - }, - growable: false, - ), - ], - if (recentGames != null && recentGames.isNotEmpty) ...[ - OpeningExplorerHeaderTile( - key: const Key('recentGamesHeader'), - child: Text(context.l10n.recentGames), - ), - ...List.generate( - recentGames.length, - (int index) { - return OpeningExplorerGameTile( - key: Key('recent-game-${recentGames.get(index).id}'), - game: recentGames.get(index), - color: index.isEven - ? Theme.of(context).colorScheme.surfaceContainerLow - : Theme.of(context).colorScheme.surfaceContainerHigh, - ply: ply, - ); - }, - growable: false, - ), - ], - ]; - - lastExplorerWidgets = children; - - return children; - }, - loading: () => - lastExplorerWidgets ?? - [ - Shimmer( - child: ShimmerLoading( - isLoading: true, - child: OpeningExplorerMoveTable.loading( - options: widget.options, + ], + error: (e, s) { + debugPrint( + 'SEVERE: [OpeningExplorerScreen] could not load opening explorer data; $e\n$s', + ); + return [ + Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Text(e.toString()), ), ), - ), - ], - error: (e, s) { - debugPrint( - 'SEVERE: [OpeningExplorerScreen] could not load opening explorer data; $e\n$s', - ); - return [ - Center( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Text(e.toString()), - ), - ), - ]; - }, - ), - ); + ]; + }, + ), + ); + case AsyncError(:final error): + debugPrint( + 'SEVERE: [OpeningExplorerScreen] could not load analysis data; $error', + ); + return Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Text(error.toString()), + ), + ); + case _: + return const CenterLoadingIndicator(); + } } } @@ -330,6 +353,7 @@ class _OpeningExplorerView extends StatelessWidget { options, boardSize, borderRadius: isTablet ? _kTabletBoardRadius : null, + shouldReplaceChildOnUserMove: true, ), ), Flexible( @@ -379,6 +403,7 @@ class _OpeningExplorerView extends StatelessWidget { child: AnalysisBoard( options, boardSize, + shouldReplaceChildOnUserMove: true, ), ), ...children, @@ -477,7 +502,7 @@ class _MoveList extends ConsumerWidget implements PreferredSizeWidget { @override Widget build(BuildContext context, WidgetRef ref) { final ctrlProvider = analysisControllerProvider(options); - final state = ref.watch(ctrlProvider); + final state = ref.watch(ctrlProvider).requireValue; final slicedMoves = state.root.mainline .map((e) => e.sanMove.san) .toList() @@ -519,9 +544,9 @@ class _BottomBar extends ConsumerWidget { .watch(openingExplorerPreferencesProvider.select((value) => value.db)); final ctrlProvider = analysisControllerProvider(options); final canGoBack = - ref.watch(ctrlProvider.select((value) => value.canGoBack)); + ref.watch(ctrlProvider.select((value) => value.requireValue.canGoBack)); final canGoNext = - ref.watch(ctrlProvider.select((value) => value.canGoNext)); + ref.watch(ctrlProvider.select((value) => value.requireValue.canGoNext)); final dbLabel = switch (db) { OpeningDatabase.master => 'Masters', diff --git a/lib/src/view/puzzle/puzzle_screen.dart b/lib/src/view/puzzle/puzzle_screen.dart index b79254f87c..3f7aadeb45 100644 --- a/lib/src/view/puzzle/puzzle_screen.dart +++ b/lib/src/view/puzzle/puzzle_screen.dart @@ -484,13 +484,13 @@ class _BottomBar extends ConsumerWidget { pushPlatformRoute( context, builder: (context) => AnalysisScreen( - pgnOrId: ref.read(ctrlProvider.notifier).makePgn(), options: AnalysisOptions( - pgn: '', - isLocalEvaluationAllowed: true, - variant: Variant.standard, - orientation: puzzleState.pov, - id: standaloneAnalysisId, + standalone: ( + pgn: ref.read(ctrlProvider.notifier).makePgn(), + isLocalEvaluationAllowed: true, + variant: Variant.standard, + orientation: puzzleState.pov, + ), initialMoveCursor: 0, ), ), diff --git a/lib/src/view/puzzle/streak_screen.dart b/lib/src/view/puzzle/streak_screen.dart index 6be35275dc..06fc77e366 100644 --- a/lib/src/view/puzzle/streak_screen.dart +++ b/lib/src/view/puzzle/streak_screen.dart @@ -291,13 +291,13 @@ class _BottomBar extends ConsumerWidget { pushPlatformRoute( context, builder: (context) => AnalysisScreen( - pgnOrId: ref.read(ctrlProvider.notifier).makePgn(), options: AnalysisOptions( - pgn: '', - isLocalEvaluationAllowed: true, - variant: Variant.standard, - orientation: puzzleState.pov, - id: standaloneAnalysisId, + standalone: ( + pgn: ref.read(ctrlProvider.notifier).makePgn(), + isLocalEvaluationAllowed: true, + variant: Variant.standard, + orientation: puzzleState.pov, + ), initialMoveCursor: 0, ), ), diff --git a/lib/src/view/study/study_bottom_bar.dart b/lib/src/view/study/study_bottom_bar.dart index d94d6e4d57..17f19f2f3d 100644 --- a/lib/src/view/study/study_bottom_bar.dart +++ b/lib/src/view/study/study_bottom_bar.dart @@ -165,13 +165,13 @@ class _GamebookBottomBar extends ConsumerWidget { context, rootNavigator: true, builder: (context) => AnalysisScreen( - pgnOrId: state.pgn, options: AnalysisOptions( - pgn: state.pgn, - isLocalEvaluationAllowed: true, - variant: state.variant, - orientation: state.pov, - id: standaloneAnalysisId, + standalone: ( + pgn: state.pgn, + isLocalEvaluationAllowed: true, + variant: state.variant, + orientation: state.pov, + ), ), ), ), diff --git a/lib/src/view/tools/load_position_screen.dart b/lib/src/view/tools/load_position_screen.dart index 27040a0d70..6f92ea14f4 100644 --- a/lib/src/view/tools/load_position_screen.dart +++ b/lib/src/view/tools/load_position_screen.dart @@ -86,7 +86,6 @@ class _BodyState extends State<_Body> { context, rootNavigator: true, builder: (context) => AnalysisScreen( - pgnOrId: parsedInput!.pgn, options: parsedInput!.options, ), ) @@ -121,7 +120,7 @@ class _BodyState extends State<_Body> { } } - ({String pgn, String fen, AnalysisOptions options})? get parsedInput { + ({String fen, AnalysisOptions options})? get parsedInput { if (textInput == null || textInput!.trim().isEmpty) { return null; } @@ -130,14 +129,14 @@ class _BodyState extends State<_Body> { try { final pos = Chess.fromSetup(Setup.parseFen(textInput!.trim())); return ( - pgn: '[FEN "${pos.fen}"]', fen: pos.fen, options: AnalysisOptions( - pgn: '[FEN "${pos.fen}"]', - isLocalEvaluationAllowed: true, - variant: Variant.standard, - orientation: Side.white, - id: standaloneAnalysisId, + standalone: ( + pgn: '[FEN "${pos.fen}"]', + isLocalEvaluationAllowed: true, + variant: Variant.standard, + orientation: Side.white, + ), ) ); } catch (_, __) {} @@ -162,15 +161,15 @@ class _BodyState extends State<_Body> { ); return ( - pgn: textInput!, fen: lastPosition.fen, options: AnalysisOptions( - pgn: textInput!, - isLocalEvaluationAllowed: true, - variant: rule != null ? Variant.fromRule(rule) : Variant.standard, + standalone: ( + pgn: textInput!, + isLocalEvaluationAllowed: true, + variant: rule != null ? Variant.fromRule(rule) : Variant.standard, + orientation: Side.white, + ), initialMoveCursor: mainlineMoves.isEmpty ? 0 : 1, - orientation: Side.white, - id: standaloneAnalysisId, ) ); } catch (_, __) {} diff --git a/lib/src/view/tools/tools_tab_screen.dart b/lib/src/view/tools/tools_tab_screen.dart index b2bba9d51c..b602c80f26 100644 --- a/lib/src/view/tools/tools_tab_screen.dart +++ b/lib/src/view/tools/tools_tab_screen.dart @@ -148,13 +148,13 @@ class _Body extends ConsumerWidget { context, rootNavigator: true, builder: (context) => const AnalysisScreen( - pgnOrId: '', options: AnalysisOptions( - pgn: '', - isLocalEvaluationAllowed: true, - variant: Variant.standard, - orientation: Side.white, - id: standaloneAnalysisId, + standalone: ( + pgn: '', + isLocalEvaluationAllowed: true, + variant: Variant.standard, + orientation: Side.white, + ), ), ), ), @@ -168,11 +168,12 @@ class _Body extends ConsumerWidget { rootNavigator: true, builder: (context) => const OpeningExplorerScreen( options: AnalysisOptions( - pgn: '', - isLocalEvaluationAllowed: false, - variant: Variant.standard, - orientation: Side.white, - id: standaloneOpeningExplorerId, + standalone: ( + pgn: '', + isLocalEvaluationAllowed: false, + variant: Variant.standard, + orientation: Side.white, + ), ), ), ) diff --git a/lib/src/widgets/feedback.dart b/lib/src/widgets/feedback.dart index c637d2b7c8..5c64cdb703 100644 --- a/lib/src/widgets/feedback.dart +++ b/lib/src/widgets/feedback.dart @@ -169,7 +169,6 @@ class FullScreenRetryRequest extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ - // TODO translate Text( context.l10n.mobileSomethingWentWrong, style: Styles.sectionTitle, diff --git a/test/view/analysis/analysis_screen_test.dart b/test/view/analysis/analysis_screen_test.dart index 337fdc7785..fcc4f46738 100644 --- a/test/view/analysis/analysis_screen_test.dart +++ b/test/view/analysis/analysis_screen_test.dart @@ -7,14 +7,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; -import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/model/common/perf.dart'; -import 'package:lichess_mobile/src/model/common/speed.dart'; -import 'package:lichess_mobile/src/model/game/archived_game.dart'; -import 'package:lichess_mobile/src/model/game/game_status.dart'; -import 'package:lichess_mobile/src/model/game/player.dart'; import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; -import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/pgn.dart'; @@ -24,29 +17,26 @@ import '../../test_provider_scope.dart'; void main() { // ignore: avoid_dynamic_calls final sanMoves = jsonDecode(gameResponse)['moves'] as String; - const opening = LightOpening( - eco: 'C20', - name: "King's Pawn Game: Wayward Queen Attack, Kiddie Countergambit", - ); group('Analysis Screen', () { testWidgets('displays correct move and position', (tester) async { final app = await makeTestProviderScopeApp( tester, home: AnalysisScreen( - pgnOrId: sanMoves, options: AnalysisOptions( - pgn: '', - isLocalEvaluationAllowed: false, - variant: Variant.standard, - opening: opening, - orientation: Side.white, - id: gameData.id, + standalone: ( + pgn: sanMoves, + isLocalEvaluationAllowed: false, + orientation: Side.white, + variant: Variant.standard, + ), ), ), ); await tester.pumpWidget(app); + expect(find.byType(CircularProgressIndicator), findsOneWidget); + await tester.pump(const Duration(milliseconds: 1)); expect(find.byType(Chessboard), findsOneWidget); expect(find.byType(PieceWidget), findsNWidgets(25)); @@ -69,19 +59,20 @@ void main() { final app = await makeTestProviderScopeApp( tester, home: AnalysisScreen( - pgnOrId: sanMoves, options: AnalysisOptions( - pgn: '', - isLocalEvaluationAllowed: false, - variant: Variant.standard, - opening: opening, - orientation: Side.white, - id: gameData.id, + standalone: ( + pgn: sanMoves, + isLocalEvaluationAllowed: false, + variant: Variant.standard, + orientation: Side.white, + ), ), ), ); await tester.pumpWidget(app); + expect(find.byType(CircularProgressIndicator), findsOneWidget); + await tester.pump(const Duration(milliseconds: 1)); // cannot go forward expect( @@ -135,20 +126,20 @@ void main() { ), }, home: AnalysisScreen( - pgnOrId: pgn, - options: const AnalysisOptions( - pgn: '', - isLocalEvaluationAllowed: false, - variant: Variant.standard, - orientation: Side.white, - opening: opening, - id: standaloneAnalysisId, + options: AnalysisOptions( + standalone: ( + pgn: pgn, + isLocalEvaluationAllowed: false, + variant: Variant.standard, + orientation: Side.white, + ), ), enableDrawingShapes: false, ), ); await tester.pumpWidget(app); + await tester.pump(const Duration(milliseconds: 1)); } Text parentText(WidgetTester tester, String move) { @@ -464,27 +455,27 @@ void main() { }); } -final gameData = LightArchivedGame( - id: const GameId('qVChCOTc'), - rated: false, - speed: Speed.blitz, - perf: Perf.blitz, - createdAt: DateTime.parse('2023-01-11 14:30:22.389'), - lastMoveAt: DateTime.parse('2023-01-11 14:33:56.416'), - status: GameStatus.mate, - white: const Player(aiLevel: 1), - black: const Player( - user: LightUser( - id: UserId('veloce'), - name: 'veloce', - isPatron: true, - ), - rating: 1435, - ), - variant: Variant.standard, - lastFen: '1r3rk1/p1pb1ppp/3p4/8/1nBN1P2/1P6/PBPP1nPP/R1K1q3 w - - 4 1', - winner: Side.black, -); +// final gameData = LightArchivedGame( +// id: const GameId('qVChCOTc'), +// rated: false, +// speed: Speed.blitz, +// perf: Perf.blitz, +// createdAt: DateTime.parse('2023-01-11 14:30:22.389'), +// lastMoveAt: DateTime.parse('2023-01-11 14:33:56.416'), +// status: GameStatus.mate, +// white: const Player(aiLevel: 1), +// black: const Player( +// user: LightUser( +// id: UserId('veloce'), +// name: 'veloce', +// isPatron: true, +// ), +// rating: 1435, +// ), +// variant: Variant.standard, +// lastFen: '1r3rk1/p1pb1ppp/3p4/8/1nBN1P2/1P6/PBPP1nPP/R1K1q3 w - - 4 1', +// winner: Side.black, +// ); const gameResponse = ''' {"id":"qVChCOTc","rated":false,"variant":"standard","speed":"blitz","perf":"blitz","createdAt":1673443822389,"lastMoveAt":1673444036416,"status":"mate","players":{"white":{"aiLevel":1},"black":{"user":{"name":"veloce","patron":true,"id":"veloce"},"rating":1435,"provisional":true}},"winner":"black","opening":{"eco":"C20","name":"King's Pawn Game: Wayward Queen Attack, Kiddie Countergambit","ply":4},"moves":"e4 e5 Qh5 Nf6 Qxe5+ Be7 b3 d6 Qb5+ Bd7 Qxb7 Nc6 Ba3 Rb8 Qa6 Nxe4 Bb2 O-O Nc3 Nb4 Nf3 Nxa6 Nd5 Nb4 Nxe7+ Qxe7 Nd4 Qf6 f4 Qe7 Ke2 Ng3+ Kd1 Nxh1 Bc4 Nf2+ Kc1 Qe1#","clocks":[18003,18003,17915,17627,17771,16691,17667,16243,17475,15459,17355,14779,17155,13795,16915,13267,14771,11955,14451,10995,14339,10203,13899,9099,12427,8379,12003,7547,11787,6691,11355,6091,11147,5763,10851,5099,10635,4657],"clock":{"initial":180,"increment":0,"totalTime":180}} diff --git a/test/view/opening_explorer/opening_explorer_screen_test.dart b/test/view/opening_explorer/opening_explorer_screen_test.dart index 5811d32f6f..be17a9ce9d 100644 --- a/test/view/opening_explorer/opening_explorer_screen_test.dart +++ b/test/view/opening_explorer/opening_explorer_screen_test.dart @@ -40,11 +40,12 @@ void main() { }); const options = AnalysisOptions( - pgn: '', - id: standaloneOpeningExplorerId, - isLocalEvaluationAllowed: false, - orientation: Side.white, - variant: Variant.standard, + standalone: ( + pgn: '', + isLocalEvaluationAllowed: false, + orientation: Side.white, + variant: Variant.standard, + ), ); const name = 'John'; @@ -65,14 +66,14 @@ void main() { (WidgetTester tester) async { final app = await makeTestProviderScopeApp( tester, - home: const OpeningExplorerScreen( - options: options, - ), + home: const OpeningExplorerScreen(options: options), overrides: [ defaultClientProvider.overrideWithValue(mockClient), ], ); await tester.pumpWidget(app); + expect(find.byType(CircularProgressIndicator), findsOneWidget); + await tester.pump(const Duration(milliseconds: 1)); // wait for opening explorer data to load (taking debounce delay into account) await tester.pump(const Duration(milliseconds: 350)); @@ -111,9 +112,7 @@ void main() { (WidgetTester tester) async { final app = await makeTestProviderScopeApp( tester, - home: const OpeningExplorerScreen( - options: options, - ), + home: const OpeningExplorerScreen(options: options), overrides: [ defaultClientProvider.overrideWithValue(mockClient), ], @@ -129,6 +128,8 @@ void main() { }, ); await tester.pumpWidget(app); + expect(find.byType(CircularProgressIndicator), findsOneWidget); + await tester.pump(const Duration(milliseconds: 1)); // wait for opening explorer data to load (taking debounce delay into account) await tester.pump(const Duration(milliseconds: 350)); @@ -163,9 +164,7 @@ void main() { (WidgetTester tester) async { final app = await makeTestProviderScopeApp( tester, - home: const OpeningExplorerScreen( - options: options, - ), + home: const OpeningExplorerScreen(options: options), overrides: [ defaultClientProvider.overrideWithValue(mockClient), ], @@ -182,6 +181,8 @@ void main() { }, ); await tester.pumpWidget(app); + expect(find.byType(CircularProgressIndicator), findsOneWidget); + await tester.pump(const Duration(milliseconds: 1)); // wait for opening explorer data to load (taking debounce delay into account) await tester.pump(const Duration(milliseconds: 350)); From 5a6db91868ec8669d5c261ca95e28cfd11b41395 Mon Sep 17 00:00:00 2001 From: Jimima Date: Thu, 21 Nov 2024 10:35:36 +0000 Subject: [PATCH 737/979] Added alternate picker on ios --- .../view/settings/board_settings_screen.dart | 64 +++++++++++++++---- 1 file changed, 50 insertions(+), 14 deletions(-) diff --git a/lib/src/view/settings/board_settings_screen.dart b/lib/src/view/settings/board_settings_screen.dart index f57d06ae24..da3c8911aa 100644 --- a/lib/src/view/settings/board_settings_screen.dart +++ b/lib/src/view/settings/board_settings_screen.dart @@ -262,20 +262,26 @@ class _Body extends ConsumerWidget { settingsValue: boardPrefs.materialDifferenceFormat .l10n(AppLocalizations.of(context)), onTap: () { - showChoicePicker( - context, - choices: MaterialDifferenceFormat.values, - selectedItem: boardPrefs.materialDifferenceFormat, - labelBuilder: (t) => - Text(t.l10n(AppLocalizations.of(context))), - onSelectedItemChanged: (MaterialDifferenceFormat? value) => - ref - .read(boardPreferencesProvider.notifier) - .setMaterialDifferenceFormat( - value ?? - MaterialDifferenceFormat.materialDifference, - ), - ); + if (Theme.of(context).platform == TargetPlatform.android) { + showChoicePicker( + context, + choices: MaterialDifferenceFormat.values, + selectedItem: boardPrefs.materialDifferenceFormat, + labelBuilder: (t) => Text(t.label), + onSelectedItemChanged: + (MaterialDifferenceFormat? value) => ref + .read(boardPreferencesProvider.notifier) + .setMaterialDifferenceFormat(value ?? + MaterialDifferenceFormat.materialDifference), + ); + } else { + pushPlatformRoute( + context, + title: 'Clock position', + builder: (context) => + const MaterialDifferenceFormatScreen(), + ); + } }, ), ], @@ -351,6 +357,36 @@ class BoardClockPositionScreen extends ConsumerWidget { } } +class MaterialDifferenceFormatScreen extends ConsumerWidget { + const MaterialDifferenceFormatScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final materialDifferenceFormat = ref.watch( + boardPreferencesProvider + .select((state) => state.materialDifferenceFormat), + ); + void onChanged(MaterialDifferenceFormat? value) => + ref.read(boardPreferencesProvider.notifier).setMaterialDifferenceFormat( + value ?? MaterialDifferenceFormat.materialDifference); + return CupertinoPageScaffold( + navigationBar: const CupertinoNavigationBar(), + child: SafeArea( + child: ListView( + children: [ + ChoicePicker( + choices: MaterialDifferenceFormat.values, + selectedItem: materialDifferenceFormat, + titleBuilder: (t) => Text(t.label), + onSelectedItemChanged: onChanged, + ), + ], + ), + ), + ); + } +} + class DragTargetKindSettingsScreen extends ConsumerWidget { const DragTargetKindSettingsScreen({super.key}); From 442a4fd6a2d1d6a06e69ce73dd415fd7a166b8b4 Mon Sep 17 00:00:00 2001 From: Jimima Date: Thu, 21 Nov 2024 10:43:54 +0000 Subject: [PATCH 738/979] Linter fix --- lib/src/view/settings/board_settings_screen.dart | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/src/view/settings/board_settings_screen.dart b/lib/src/view/settings/board_settings_screen.dart index da3c8911aa..79a5d2f1fa 100644 --- a/lib/src/view/settings/board_settings_screen.dart +++ b/lib/src/view/settings/board_settings_screen.dart @@ -271,8 +271,10 @@ class _Body extends ConsumerWidget { onSelectedItemChanged: (MaterialDifferenceFormat? value) => ref .read(boardPreferencesProvider.notifier) - .setMaterialDifferenceFormat(value ?? - MaterialDifferenceFormat.materialDifference), + .setMaterialDifferenceFormat( + value ?? + MaterialDifferenceFormat.materialDifference, + ), ); } else { pushPlatformRoute( @@ -368,7 +370,8 @@ class MaterialDifferenceFormatScreen extends ConsumerWidget { ); void onChanged(MaterialDifferenceFormat? value) => ref.read(boardPreferencesProvider.notifier).setMaterialDifferenceFormat( - value ?? MaterialDifferenceFormat.materialDifference); + value ?? MaterialDifferenceFormat.materialDifference, + ); return CupertinoPageScaffold( navigationBar: const CupertinoNavigationBar(), child: SafeArea( From c9da3f1d4ebc9dabb47df31824c71a017b17d0e6 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 21 Nov 2024 11:56:04 +0100 Subject: [PATCH 739/979] Don't show explorer if offline --- .../view/analysis/opening_explorer_view.dart | 316 ++++++++++-------- lib/src/view/engine/engine_gauge.dart | 2 +- lib/src/view/game/archived_game_screen.dart | 6 +- 3 files changed, 175 insertions(+), 149 deletions(-) diff --git a/lib/src/view/analysis/opening_explorer_view.dart b/lib/src/view/analysis/opening_explorer_view.dart index 5f82d006dc..c631778fda 100644 --- a/lib/src/view/analysis/opening_explorer_view.dart +++ b/lib/src/view/analysis/opening_explorer_view.dart @@ -5,9 +5,11 @@ import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_preferences.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_repository.dart'; +import 'package:lichess_mobile/src/network/connectivity.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_widgets.dart'; +import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; const _kTableRowVerticalPadding = 12.0; @@ -35,68 +37,159 @@ class _OpeningExplorerState extends ConsumerState { @override Widget build(BuildContext context) { - final analysisState = - ref.watch(analysisControllerProvider(widget.options)).requireValue; - - if (analysisState.position.ply >= 50) { - return _OpeningExplorerView( - isLoading: false, - children: [ - OpeningExplorerMoveTable.maxDepth( - options: widget.options, - ), - ], - ); - } - - final prefs = ref.watch(openingExplorerPreferencesProvider); - - if (prefs.db == OpeningDatabase.player && prefs.playerDb.username == null) { - return const _OpeningExplorerView( - isLoading: false, - children: [ - Padding( - padding: _kTableRowPadding, - child: Center( - // TODO: l10n - child: Text('Select a Lichess player in the settings.'), - ), - ), - ], - ); - } + final connectivity = ref.watch(connectivityChangesProvider); + return connectivity.whenIsLoading( + loading: () => const CenterLoadingIndicator(), + offline: () => const Center( + child: Padding( + padding: EdgeInsets.all(16.0), + // TODO l10n + child: Text('Opening explorer is not available offline.'), + ), + ), + online: () { + final analysisState = + ref.watch(analysisControllerProvider(widget.options)).requireValue; + + if (analysisState.position.ply >= 50) { + return _OpeningExplorerView( + isLoading: false, + children: [ + OpeningExplorerMoveTable.maxDepth( + options: widget.options, + ), + ], + ); + } + + final prefs = ref.watch(openingExplorerPreferencesProvider); + + if (prefs.db == OpeningDatabase.player && + prefs.playerDb.username == null) { + return const _OpeningExplorerView( + isLoading: false, + children: [ + Padding( + padding: _kTableRowPadding, + child: Center( + // TODO: l10n + child: Text('Select a Lichess player in the settings.'), + ), + ), + ], + ); + } + + final cacheKey = OpeningExplorerCacheKey( + fen: analysisState.position.fen, + prefs: prefs, + ); + final cacheOpeningExplorer = cache[cacheKey]; + final openingExplorerAsync = cacheOpeningExplorer != null + ? AsyncValue.data( + (entry: cacheOpeningExplorer, isIndexing: false), + ) + : ref.watch( + openingExplorerProvider(fen: analysisState.position.fen), + ); + + if (cacheOpeningExplorer == null) { + ref.listen(openingExplorerProvider(fen: analysisState.position.fen), + (_, curAsync) { + curAsync.whenData((cur) { + if (cur != null && !cur.isIndexing) { + cache[cacheKey] = cur.entry; + } + }); + }); + } + + final isLoading = openingExplorerAsync.isLoading || + openingExplorerAsync.value == null; + + return _OpeningExplorerView( + isLoading: isLoading, + children: openingExplorerAsync.when( + data: (openingExplorer) { + if (openingExplorer == null) { + return lastExplorerWidgets ?? + [ + Shimmer( + child: ShimmerLoading( + isLoading: true, + child: OpeningExplorerMoveTable.loading( + options: widget.options, + ), + ), + ), + ]; + } - final cacheKey = OpeningExplorerCacheKey( - fen: analysisState.position.fen, - prefs: prefs, - ); - final cacheOpeningExplorer = cache[cacheKey]; - final openingExplorerAsync = cacheOpeningExplorer != null - ? AsyncValue.data( - (entry: cacheOpeningExplorer, isIndexing: false), - ) - : ref.watch(openingExplorerProvider(fen: analysisState.position.fen)); - - if (cacheOpeningExplorer == null) { - ref.listen(openingExplorerProvider(fen: analysisState.position.fen), - (_, curAsync) { - curAsync.whenData((cur) { - if (cur != null && !cur.isIndexing) { - cache[cacheKey] = cur.entry; - } - }); - }); - } - - final isLoading = - openingExplorerAsync.isLoading || openingExplorerAsync.value == null; - - return _OpeningExplorerView( - isLoading: isLoading, - children: openingExplorerAsync.when( - data: (openingExplorer) { - if (openingExplorer == null) { - return lastExplorerWidgets ?? + final topGames = openingExplorer.entry.topGames; + final recentGames = openingExplorer.entry.recentGames; + + final ply = analysisState.position.ply; + + final children = [ + OpeningExplorerMoveTable( + moves: openingExplorer.entry.moves, + whiteWins: openingExplorer.entry.white, + draws: openingExplorer.entry.draws, + blackWins: openingExplorer.entry.black, + options: widget.options, + ), + if (topGames != null && topGames.isNotEmpty) ...[ + OpeningExplorerHeaderTile( + key: const Key('topGamesHeader'), + child: Text(context.l10n.topGames), + ), + ...List.generate( + topGames.length, + (int index) { + return OpeningExplorerGameTile( + key: Key('top-game-${topGames.get(index).id}'), + game: topGames.get(index), + color: index.isEven + ? Theme.of(context).colorScheme.surfaceContainerLow + : Theme.of(context) + .colorScheme + .surfaceContainerHigh, + ply: ply, + ); + }, + growable: false, + ), + ], + if (recentGames != null && recentGames.isNotEmpty) ...[ + OpeningExplorerHeaderTile( + key: const Key('recentGamesHeader'), + child: Text(context.l10n.recentGames), + ), + ...List.generate( + recentGames.length, + (int index) { + return OpeningExplorerGameTile( + key: Key('recent-game-${recentGames.get(index).id}'), + game: recentGames.get(index), + color: index.isEven + ? Theme.of(context).colorScheme.surfaceContainerLow + : Theme.of(context) + .colorScheme + .surfaceContainerHigh, + ply: ply, + ); + }, + growable: false, + ), + ], + ]; + + lastExplorerWidgets = children; + + return children; + }, + loading: () => + lastExplorerWidgets ?? [ Shimmer( child: ShimmerLoading( @@ -106,94 +199,23 @@ class _OpeningExplorerState extends ConsumerState { ), ), ), - ]; - } - - final topGames = openingExplorer.entry.topGames; - final recentGames = openingExplorer.entry.recentGames; - - final ply = analysisState.position.ply; - - final children = [ - OpeningExplorerMoveTable( - moves: openingExplorer.entry.moves, - whiteWins: openingExplorer.entry.white, - draws: openingExplorer.entry.draws, - blackWins: openingExplorer.entry.black, - options: widget.options, - ), - if (topGames != null && topGames.isNotEmpty) ...[ - OpeningExplorerHeaderTile( - key: const Key('topGamesHeader'), - child: Text(context.l10n.topGames), - ), - ...List.generate( - topGames.length, - (int index) { - return OpeningExplorerGameTile( - key: Key('top-game-${topGames.get(index).id}'), - game: topGames.get(index), - color: index.isEven - ? Theme.of(context).colorScheme.surfaceContainerLow - : Theme.of(context).colorScheme.surfaceContainerHigh, - ply: ply, - ); - }, - growable: false, - ), - ], - if (recentGames != null && recentGames.isNotEmpty) ...[ - OpeningExplorerHeaderTile( - key: const Key('recentGamesHeader'), - child: Text(context.l10n.recentGames), - ), - ...List.generate( - recentGames.length, - (int index) { - return OpeningExplorerGameTile( - key: Key('recent-game-${recentGames.get(index).id}'), - game: recentGames.get(index), - color: index.isEven - ? Theme.of(context).colorScheme.surfaceContainerLow - : Theme.of(context).colorScheme.surfaceContainerHigh, - ply: ply, - ); - }, - growable: false, - ), - ], - ]; - - lastExplorerWidgets = children; - - return children; - }, - loading: () => - lastExplorerWidgets ?? - [ - Shimmer( - child: ShimmerLoading( - isLoading: true, - child: OpeningExplorerMoveTable.loading( - options: widget.options, + ], + error: (e, s) { + debugPrint( + 'SEVERE: [OpeningExplorerView] could not load opening explorer data; $e\n$s', + ); + return [ + Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Text(e.toString()), ), ), - ), - ], - error: (e, s) { - debugPrint( - 'SEVERE: [OpeningExplorerView] could not load opening explorer data; $e\n$s', - ); - return [ - Center( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Text(e.toString()), - ), - ), - ]; - }, - ), + ]; + }, + ), + ); + }, ); } } diff --git a/lib/src/view/engine/engine_gauge.dart b/lib/src/view/engine/engine_gauge.dart index d1ef7ae79b..0da958228c 100644 --- a/lib/src/view/engine/engine_gauge.dart +++ b/lib/src/view/engine/engine_gauge.dart @@ -9,7 +9,7 @@ import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; const double kEvalGaugeSize = 24.0; -const double kEvalGaugeFontSize = 10.0; +const double kEvalGaugeFontSize = 11.0; const Color _kEvalGaugeBackgroundColor = Color(0xFF444444); const Color _kEvalGaugeValueColorDarkBg = Color(0xEEEEEEEE); const Color _kEvalGaugeValueColorLightBg = Color(0xFFFFFFFF); diff --git a/lib/src/view/game/archived_game_screen.dart b/lib/src/view/game/archived_game_screen.dart index b6abcc72da..ec5226fe83 100644 --- a/lib/src/view/game/archived_game_screen.dart +++ b/lib/src/view/game/archived_game_screen.dart @@ -375,10 +375,14 @@ class _BottomBar extends ConsumerWidget { label: context.l10n.gameAnalysis, onTap: ref.read(gameCursorProvider(gameData.id)).hasValue ? () { + final cursor = gameCursor.requireValue.$2; pushPlatformRoute( context, builder: (context) => AnalysisScreen( - options: AnalysisOptions(gameId: gameData.id), + options: AnalysisOptions( + gameId: gameData.id, + initialMoveCursor: cursor, + ), ), ); } From cee722276ffed3f678d5e921685c9d5c1f2e0ee1 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 21 Nov 2024 14:59:49 +0100 Subject: [PATCH 740/979] Only show game summary tab when necessary --- .../model/analysis/analysis_controller.dart | 2 + lib/src/view/analysis/analysis_screen.dart | 53 ++++++++++++++----- 2 files changed, 43 insertions(+), 12 deletions(-) diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index 645a922ef9..fef06e292b 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -759,6 +759,8 @@ class AnalysisState with _$AnalysisState { bool get hasServerAnalysis => playersAnalysis != null; + bool get canShowGameSummary => hasServerAnalysis || canRequestServerAnalysis; + /// Whether an evaluation can be available bool get hasAvailableEval => isEngineAvailable || diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index cbe72e7d3a..f87067227e 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -47,9 +47,9 @@ class AnalysisScreen extends ConsumerStatefulWidget { } class _AnalysisScreenState extends ConsumerState - with SingleTickerProviderStateMixin { - late final TabController _tabController; - late final List tabs; + with TickerProviderStateMixin { + late List tabs; + late TabController _tabController; @override void initState() { @@ -57,7 +57,6 @@ class _AnalysisScreenState extends ConsumerState tabs = [ AnalysisTab.opening, AnalysisTab.moves, - if (widget.options.isLichessGameAnalysis) AnalysisTab.summary, ]; _tabController = TabController( @@ -65,6 +64,28 @@ class _AnalysisScreenState extends ConsumerState initialIndex: 1, length: tabs.length, ); + + ref.listenManual>( + analysisControllerProvider(widget.options), + (_, state) { + if (state.valueOrNull?.canShowGameSummary == true) { + setState(() { + tabs = [ + AnalysisTab.opening, + AnalysisTab.moves, + AnalysisTab.summary, + ]; + final index = _tabController.index; + _tabController.dispose(); + _tabController = TabController( + vsync: this, + initialIndex: index, + length: tabs.length, + ); + }); + } + }, + ); } @override @@ -76,8 +97,8 @@ class _AnalysisScreenState extends ConsumerState @override Widget build(BuildContext context) { final ctrlProvider = analysisControllerProvider(widget.options); - final asyncState = ref.watch(ctrlProvider); + final asyncState = ref.watch(ctrlProvider); final appBarActions = [ EngineDepth(defaultEval: asyncState.valueOrNull?.currentNode.eval), AppBarAnalysisTabIndicator( @@ -106,8 +127,9 @@ class _AnalysisScreenState extends ConsumerState actions: appBarActions, ), body: _Body( - controller: _tabController, options: widget.options, + controller: _tabController, + tabs: tabs, enableDrawingShapes: widget.enableDrawingShapes, ), ); @@ -154,12 +176,14 @@ class _Title extends StatelessWidget { class _Body extends ConsumerWidget { const _Body({ - required this.controller, required this.options, + required this.controller, + required this.tabs, required this.enableDrawingShapes, }); final TabController controller; + final List tabs; final AnalysisOptions options; final bool enableDrawingShapes; @@ -211,11 +235,16 @@ class _Body extends ConsumerWidget { ) : null, bottomBar: _BottomBar(options: options), - children: [ - OpeningExplorerView(options: options), - AnalysisTreeView(options), - if (options.isLichessGameAnalysis) ServerAnalysisSummary(options), - ], + children: tabs.map((tab) { + switch (tab) { + case AnalysisTab.opening: + return OpeningExplorerView(options: options); + case AnalysisTab.moves: + return AnalysisTreeView(options); + case AnalysisTab.summary: + return ServerAnalysisSummary(options); + } + }).toList(), ); } } From 1b593536eb2a95576d9ead309b22ba9b03b5408a Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 21 Nov 2024 15:09:58 +0100 Subject: [PATCH 741/979] Hide settings if evaluation is not allowed --- lib/src/view/analysis/analysis_screen.dart | 3 + lib/src/view/analysis/analysis_settings.dart | 144 +++++++++---------- 2 files changed, 75 insertions(+), 72 deletions(-) diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index f87067227e..476645fca7 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -111,6 +111,9 @@ class _AnalysisScreenState extends ConsumerState isScrollControlled: true, showDragHandle: true, isDismissible: true, + constraints: BoxConstraints( + minHeight: MediaQuery.sizeOf(context).height * 0.5, + ), builder: (_) => AnalysisSettings(widget.options), ), semanticsLabel: context.l10n.settingsSettings, diff --git a/lib/src/view/analysis/analysis_settings.dart b/lib/src/view/analysis/analysis_settings.dart index f7101bd8c4..0de707426f 100644 --- a/lib/src/view/analysis/analysis_settings.dart +++ b/lib/src/view/analysis/analysis_settings.dart @@ -37,48 +37,18 @@ class AnalysisSettings extends ConsumerWidget { ), trailing: const Icon(CupertinoIcons.chevron_right), ), - SwitchSettingTile( - title: Text(context.l10n.toggleLocalEvaluation), - value: prefs.enableLocalEvaluation, - onChanged: value.isLocalEvaluationAllowed - ? (_) { - ref.read(ctrlProvider.notifier).toggleLocalEvaluation(); - } - : null, - ), - PlatformListTile( - title: Text.rich( - TextSpan( - text: '${context.l10n.multipleLines}: ', - style: const TextStyle( - fontWeight: FontWeight.normal, - ), - children: [ - TextSpan( - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - ), - text: prefs.numEvalLines.toString(), - ), - ], - ), + if (value.isLocalEvaluationAllowed) ...[ + SwitchSettingTile( + title: Text(context.l10n.toggleLocalEvaluation), + value: prefs.enableLocalEvaluation, + onChanged: (_) { + ref.read(ctrlProvider.notifier).toggleLocalEvaluation(); + }, ), - subtitle: NonLinearSlider( - value: prefs.numEvalLines, - values: const [0, 1, 2, 3], - onChangeEnd: value.isEngineAvailable - ? (value) => ref - .read(ctrlProvider.notifier) - .setNumEvalLines(value.toInt()) - : null, - ), - ), - if (maxEngineCores > 1) PlatformListTile( title: Text.rich( TextSpan( - text: '${context.l10n.cpus}: ', + text: '${context.l10n.multipleLines}: ', style: const TextStyle( fontWeight: FontWeight.normal, ), @@ -88,51 +58,81 @@ class AnalysisSettings extends ConsumerWidget { fontWeight: FontWeight.bold, fontSize: 18, ), - text: prefs.numEngineCores.toString(), + text: prefs.numEvalLines.toString(), ), ], ), ), subtitle: NonLinearSlider( - value: prefs.numEngineCores, - values: List.generate(maxEngineCores, (index) => index + 1), + value: prefs.numEvalLines, + values: const [0, 1, 2, 3], onChangeEnd: value.isEngineAvailable ? (value) => ref .read(ctrlProvider.notifier) - .setEngineCores(value.toInt()) + .setNumEvalLines(value.toInt()) : null, ), ), - SwitchSettingTile( - title: Text(context.l10n.bestMoveArrow), - value: prefs.showBestMoveArrow, - onChanged: value.isEngineAvailable - ? (value) => ref - .read(analysisPreferencesProvider.notifier) - .toggleShowBestMoveArrow() - : null, - ), - SwitchSettingTile( - title: Text(context.l10n.evaluationGauge), - value: prefs.showEvaluationGauge, - onChanged: (value) => ref - .read(analysisPreferencesProvider.notifier) - .toggleShowEvaluationGauge(), - ), - SwitchSettingTile( - title: Text(context.l10n.toggleGlyphAnnotations), - value: prefs.showAnnotations, - onChanged: (_) => ref - .read(analysisPreferencesProvider.notifier) - .toggleAnnotations(), - ), - SwitchSettingTile( - title: Text(context.l10n.mobileShowComments), - value: prefs.showPgnComments, - onChanged: (_) => ref - .read(analysisPreferencesProvider.notifier) - .togglePgnComments(), - ), + if (maxEngineCores > 1) + PlatformListTile( + title: Text.rich( + TextSpan( + text: '${context.l10n.cpus}: ', + style: const TextStyle( + fontWeight: FontWeight.normal, + ), + children: [ + TextSpan( + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + text: prefs.numEngineCores.toString(), + ), + ], + ), + ), + subtitle: NonLinearSlider( + value: prefs.numEngineCores, + values: List.generate(maxEngineCores, (index) => index + 1), + onChangeEnd: value.isEngineAvailable + ? (value) => ref + .read(ctrlProvider.notifier) + .setEngineCores(value.toInt()) + : null, + ), + ), + SwitchSettingTile( + title: Text(context.l10n.bestMoveArrow), + value: prefs.showBestMoveArrow, + onChanged: value.isEngineAvailable + ? (value) => ref + .read(analysisPreferencesProvider.notifier) + .toggleShowBestMoveArrow() + : null, + ), + SwitchSettingTile( + title: Text(context.l10n.evaluationGauge), + value: prefs.showEvaluationGauge, + onChanged: (value) => ref + .read(analysisPreferencesProvider.notifier) + .toggleShowEvaluationGauge(), + ), + SwitchSettingTile( + title: Text(context.l10n.toggleGlyphAnnotations), + value: prefs.showAnnotations, + onChanged: (_) => ref + .read(analysisPreferencesProvider.notifier) + .toggleAnnotations(), + ), + SwitchSettingTile( + title: Text(context.l10n.mobileShowComments), + value: prefs.showPgnComments, + onChanged: (_) => ref + .read(analysisPreferencesProvider.notifier) + .togglePgnComments(), + ), + ], ], ); case AsyncError(:final error): From 2a6ff82f1953acbd33d20cdd4e90b60470c73fd1 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 21 Nov 2024 16:08:04 +0100 Subject: [PATCH 742/979] WIP on computer analysis toggle --- .../model/analysis/analysis_controller.dart | 52 ++++-- .../model/analysis/analysis_preferences.dart | 10 + lib/src/model/study/study_controller.dart | 8 +- lib/src/view/analysis/analysis_settings.dart | 173 ++++++++++-------- .../board_editor/board_editor_screen.dart | 2 +- .../offline_correspondence_game_screen.dart | 2 +- lib/src/view/game/game_result_dialog.dart | 2 +- lib/src/view/puzzle/puzzle_screen.dart | 2 +- lib/src/view/puzzle/streak_screen.dart | 2 +- lib/src/view/study/study_bottom_bar.dart | 2 +- lib/src/view/study/study_settings.dart | 6 +- lib/src/view/tools/load_position_screen.dart | 4 +- lib/src/view/tools/tools_tab_screen.dart | 4 +- lib/src/widgets/pgn.dart | 8 +- test/view/analysis/analysis_screen_test.dart | 6 +- .../opening_explorer_screen_test.dart | 2 +- 16 files changed, 173 insertions(+), 112 deletions(-) diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index fef06e292b..10463ea55e 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -33,7 +33,7 @@ typedef StandaloneAnalysis = ({ String pgn, Variant variant, Side orientation, - bool isLocalEvaluationAllowed, + bool isComputerAnalysisAllowed, }); @freezed @@ -112,9 +112,9 @@ class AnalysisController extends _$AnalysisController final pgnHeaders = IMap(game.headers); final rootComments = IList(game.comments.map((c) => PgnComment.fromPgn(c))); - final isLocalEvaluationAllowed = options.isLichessGameAnalysis + final isComputerAnalysisAllowed = options.isLichessGameAnalysis ? pgnHeaders['Result'] != '*' - : options.standalone!.isLocalEvaluationAllowed; + : options.standalone!.isComputerAnalysisAllowed; _root = Root.fromPgnGame( game, @@ -166,7 +166,8 @@ class AnalysisController extends _$AnalysisController lastMove: lastMove, pov: _orientation, contextOpening: opening, - isLocalEvaluationAllowed: isLocalEvaluationAllowed, + isComputerAnalysisAllowed: isComputerAnalysisAllowed, + isComputerAnalysisEnabled: prefs.enableComputerAnalysis, isLocalEvaluationEnabled: prefs.enableLocalEvaluation, playersAnalysis: serverAnalysis, acplChartData: serverAnalysis != null ? _makeAcplChartData() : null, @@ -325,15 +326,28 @@ class AnalysisController extends _$AnalysisController _setPath(path.penultimate, shouldRecomputeRootView: true); } + /// Toggles the computer analysis on/off. + /// + /// Acts both on local evaluation and server analysis. + Future toggleComputerAnalysis() async { + await ref + .read(analysisPreferencesProvider.notifier) + .toggleEnableComputerAnalysis(); + + await toggleLocalEvaluation(); + } + + /// Toggles the local evaluation on/off. Future toggleLocalEvaluation() async { - ref + await ref .read(analysisPreferencesProvider.notifier) .toggleEnableLocalEvaluation(); - final curState = state.requireValue; + final prefs = ref.read(analysisPreferencesProvider); state = AsyncData( - curState.copyWith( - isLocalEvaluationEnabled: !curState.isLocalEvaluationEnabled, + state.requireValue.copyWith( + isLocalEvaluationEnabled: prefs.enableLocalEvaluation, + isComputerAnalysisEnabled: prefs.enableComputerAnalysis, ), ); @@ -707,10 +721,19 @@ class AnalysisState with _$AnalysisState { /// The side to display the board from. required Side pov, - /// Whether local evaluation is allowed for this analysis. - required bool isLocalEvaluationAllowed, + /// Whether computer evaluation is allowed for this analysis. + /// + /// Acts on both local and server analysis. + required bool isComputerAnalysisAllowed, + + /// Whether the user has enabled computer analysis. + /// + /// This is a user preference and acts both on local and server analysis. + required bool isComputerAnalysisEnabled, /// Whether the user has enabled local evaluation. + /// + /// This is a user preference and acts only on local analysis. required bool isLocalEvaluationEnabled, /// The last move played. @@ -757,6 +780,7 @@ class AnalysisState with _$AnalysisState { bool get canRequestServerAnalysis => gameId != null && !hasServerAnalysis && pgnHeaders['Result'] != '*'; + /// Whether the server analysis is available. bool get hasServerAnalysis => playersAnalysis != null; bool get canShowGameSummary => hasServerAnalysis || canRequestServerAnalysis; @@ -764,13 +788,17 @@ class AnalysisState with _$AnalysisState { /// Whether an evaluation can be available bool get hasAvailableEval => isEngineAvailable || - (isLocalEvaluationAllowed && + (isComputerAnalysisEnabledAndAllowed && acplChartData != null && acplChartData!.isNotEmpty); + bool get isComputerAnalysisEnabledAndAllowed => + isComputerAnalysisEnabled && isComputerAnalysisAllowed; + /// Whether the engine is allowed for this analysis and variant. bool get isEngineAllowed => - isLocalEvaluationAllowed && engineSupportedVariants.contains(variant); + isComputerAnalysisEnabledAndAllowed && + engineSupportedVariants.contains(variant); /// Whether the engine is available for evaluation bool get isEngineAvailable => isEngineAllowed && isLocalEvaluationEnabled; diff --git a/lib/src/model/analysis/analysis_preferences.dart b/lib/src/model/analysis/analysis_preferences.dart index 5633e3db30..30615646d3 100644 --- a/lib/src/model/analysis/analysis_preferences.dart +++ b/lib/src/model/analysis/analysis_preferences.dart @@ -26,6 +26,14 @@ class AnalysisPreferences extends _$AnalysisPreferences return fetch(); } + Future toggleEnableComputerAnalysis() { + return save( + state.copyWith( + enableComputerAnalysis: !state.enableComputerAnalysis, + ), + ); + } + Future toggleEnableLocalEvaluation() { return save( state.copyWith( @@ -90,6 +98,7 @@ class AnalysisPrefs with _$AnalysisPrefs implements Serializable { const AnalysisPrefs._(); const factory AnalysisPrefs({ + @JsonKey(defaultValue: true) required bool enableComputerAnalysis, required bool enableLocalEvaluation, required bool showEvaluationGauge, required bool showBestMoveArrow, @@ -101,6 +110,7 @@ class AnalysisPrefs with _$AnalysisPrefs implements Serializable { }) = _AnalysisPrefs; static const defaults = AnalysisPrefs( + enableComputerAnalysis: true, enableLocalEvaluation: true, showEvaluationGauge: true, showBestMoveArrow: true, diff --git a/lib/src/model/study/study_controller.dart b/lib/src/model/study/study_controller.dart index 1337dff19c..459751d81b 100644 --- a/lib/src/model/study/study_controller.dart +++ b/lib/src/model/study/study_controller.dart @@ -97,7 +97,7 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { currentNode: StudyCurrentNode.illegalPosition(), pgnRootComments: rootComments, pov: orientation, - isLocalEvaluationAllowed: false, + isComputerAnalysisAllowed: false, isLocalEvaluationEnabled: false, gamebookActive: false, pgn: pgn, @@ -121,7 +121,7 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { pgnRootComments: rootComments, lastMove: lastMove, pov: orientation, - isLocalEvaluationAllowed: + isComputerAnalysisAllowed: study.chapter.features.computer && !study.chapter.gamebook, isLocalEvaluationEnabled: prefs.enableLocalEvaluation, gamebookActive: study.chapter.gamebook, @@ -559,7 +559,7 @@ class StudyState with _$StudyState { required Side pov, /// Whether local evaluation is allowed for this study. - required bool isLocalEvaluationAllowed, + required bool isComputerAnalysisAllowed, /// Whether we're currently in gamebook mode, where the user has to find the right moves. required bool gamebookActive, @@ -583,7 +583,7 @@ class StudyState with _$StudyState { /// Whether the engine is available for evaluation bool get isEngineAvailable => - isLocalEvaluationAllowed && + isComputerAnalysisAllowed && engineSupportedVariants.contains(variant) && isLocalEvaluationEnabled; diff --git a/lib/src/view/analysis/analysis_settings.dart b/lib/src/view/analysis/analysis_settings.dart index 0de707426f..9b03d4c943 100644 --- a/lib/src/view/analysis/analysis_settings.dart +++ b/lib/src/view/analysis/analysis_settings.dart @@ -37,102 +37,121 @@ class AnalysisSettings extends ConsumerWidget { ), trailing: const Icon(CupertinoIcons.chevron_right), ), - if (value.isLocalEvaluationAllowed) ...[ + if (value.isComputerAnalysisAllowed) SwitchSettingTile( - title: Text(context.l10n.toggleLocalEvaluation), - value: prefs.enableLocalEvaluation, + title: Text(context.l10n.computerAnalysis), + value: prefs.enableComputerAnalysis, onChanged: (_) { - ref.read(ctrlProvider.notifier).toggleLocalEvaluation(); + ref.read(ctrlProvider.notifier).toggleComputerAnalysis(); }, ), - PlatformListTile( - title: Text.rich( - TextSpan( - text: '${context.l10n.multipleLines}: ', - style: const TextStyle( - fontWeight: FontWeight.normal, - ), - children: [ + AnimatedCrossFade( + duration: const Duration(milliseconds: 300), + crossFadeState: value.isComputerAnalysisEnabledAndAllowed + ? CrossFadeState.showSecond + : CrossFadeState.showFirst, + firstChild: const SizedBox.shrink(), + secondChild: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SwitchSettingTile( + title: Text(context.l10n.toggleLocalEvaluation), + value: prefs.enableLocalEvaluation, + onChanged: (_) { + ref.read(ctrlProvider.notifier).toggleLocalEvaluation(); + }, + ), + PlatformListTile( + title: Text.rich( TextSpan( + text: '${context.l10n.multipleLines}: ', style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, + fontWeight: FontWeight.normal, ), - text: prefs.numEvalLines.toString(), + children: [ + TextSpan( + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + text: prefs.numEvalLines.toString(), + ), + ], ), - ], + ), + subtitle: NonLinearSlider( + value: prefs.numEvalLines, + values: const [0, 1, 2, 3], + onChangeEnd: value.isEngineAvailable + ? (value) => ref + .read(ctrlProvider.notifier) + .setNumEvalLines(value.toInt()) + : null, + ), ), - ), - subtitle: NonLinearSlider( - value: prefs.numEvalLines, - values: const [0, 1, 2, 3], - onChangeEnd: value.isEngineAvailable - ? (value) => ref - .read(ctrlProvider.notifier) - .setNumEvalLines(value.toInt()) - : null, - ), - ), - if (maxEngineCores > 1) - PlatformListTile( - title: Text.rich( - TextSpan( - text: '${context.l10n.cpus}: ', - style: const TextStyle( - fontWeight: FontWeight.normal, - ), - children: [ + if (maxEngineCores > 1) + PlatformListTile( + title: Text.rich( TextSpan( + text: '${context.l10n.cpus}: ', style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, + fontWeight: FontWeight.normal, ), - text: prefs.numEngineCores.toString(), + children: [ + TextSpan( + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + text: prefs.numEngineCores.toString(), + ), + ], ), - ], + ), + subtitle: NonLinearSlider( + value: prefs.numEngineCores, + values: + List.generate(maxEngineCores, (index) => index + 1), + onChangeEnd: value.isEngineAvailable + ? (value) => ref + .read(ctrlProvider.notifier) + .setEngineCores(value.toInt()) + : null, + ), ), - ), - subtitle: NonLinearSlider( - value: prefs.numEngineCores, - values: List.generate(maxEngineCores, (index) => index + 1), - onChangeEnd: value.isEngineAvailable + SwitchSettingTile( + title: Text(context.l10n.bestMoveArrow), + value: prefs.showBestMoveArrow, + onChanged: value.isEngineAvailable ? (value) => ref - .read(ctrlProvider.notifier) - .setEngineCores(value.toInt()) + .read(analysisPreferencesProvider.notifier) + .toggleShowBestMoveArrow() : null, ), - ), - SwitchSettingTile( - title: Text(context.l10n.bestMoveArrow), - value: prefs.showBestMoveArrow, - onChanged: value.isEngineAvailable - ? (value) => ref + SwitchSettingTile( + title: Text(context.l10n.evaluationGauge), + value: prefs.showEvaluationGauge, + onChanged: (value) => ref .read(analysisPreferencesProvider.notifier) - .toggleShowBestMoveArrow() - : null, - ), - SwitchSettingTile( - title: Text(context.l10n.evaluationGauge), - value: prefs.showEvaluationGauge, - onChanged: (value) => ref - .read(analysisPreferencesProvider.notifier) - .toggleShowEvaluationGauge(), - ), - SwitchSettingTile( - title: Text(context.l10n.toggleGlyphAnnotations), - value: prefs.showAnnotations, - onChanged: (_) => ref - .read(analysisPreferencesProvider.notifier) - .toggleAnnotations(), - ), - SwitchSettingTile( - title: Text(context.l10n.mobileShowComments), - value: prefs.showPgnComments, - onChanged: (_) => ref - .read(analysisPreferencesProvider.notifier) - .togglePgnComments(), + .toggleShowEvaluationGauge(), + ), + SwitchSettingTile( + title: Text(context.l10n.toggleGlyphAnnotations), + value: prefs.showAnnotations, + onChanged: (_) => ref + .read(analysisPreferencesProvider.notifier) + .toggleAnnotations(), + ), + SwitchSettingTile( + title: Text(context.l10n.mobileShowComments), + value: prefs.showPgnComments, + onChanged: (_) => ref + .read(analysisPreferencesProvider.notifier) + .togglePgnComments(), + ), + ], ), - ], + ), ], ); case AsyncError(:final error): diff --git a/lib/src/view/board_editor/board_editor_screen.dart b/lib/src/view/board_editor/board_editor_screen.dart index a4269e7873..45ee49e2b1 100644 --- a/lib/src/view/board_editor/board_editor_screen.dart +++ b/lib/src/view/board_editor/board_editor_screen.dart @@ -338,7 +338,7 @@ class _BottomBar extends ConsumerWidget { options: AnalysisOptions( standalone: ( pgn: editorState.pgn!, - isLocalEvaluationAllowed: true, + isComputerAnalysisAllowed: true, variant: Variant.fromPosition, orientation: editorState.orientation, ), diff --git a/lib/src/view/correspondence/offline_correspondence_game_screen.dart b/lib/src/view/correspondence/offline_correspondence_game_screen.dart index 45d3ccd843..931ad9e5d5 100644 --- a/lib/src/view/correspondence/offline_correspondence_game_screen.dart +++ b/lib/src/view/correspondence/offline_correspondence_game_screen.dart @@ -262,7 +262,7 @@ class _BodyState extends ConsumerState<_Body> { options: AnalysisOptions( standalone: ( pgn: game.makePgn(), - isLocalEvaluationAllowed: false, + isComputerAnalysisAllowed: false, variant: game.variant, orientation: game.youAre, ), diff --git a/lib/src/view/game/game_result_dialog.dart b/lib/src/view/game/game_result_dialog.dart index 4e09a5cf9d..e4ebf84b08 100644 --- a/lib/src/view/game/game_result_dialog.dart +++ b/lib/src/view/game/game_result_dialog.dart @@ -263,7 +263,7 @@ class OverTheBoardGameResultDialog extends StatelessWidget { options: AnalysisOptions( standalone: ( pgn: game.makePgn(), - isLocalEvaluationAllowed: true, + isComputerAnalysisAllowed: true, variant: game.meta.variant, orientation: Side.white, ), diff --git a/lib/src/view/puzzle/puzzle_screen.dart b/lib/src/view/puzzle/puzzle_screen.dart index 3f7aadeb45..0885219d38 100644 --- a/lib/src/view/puzzle/puzzle_screen.dart +++ b/lib/src/view/puzzle/puzzle_screen.dart @@ -487,7 +487,7 @@ class _BottomBar extends ConsumerWidget { options: AnalysisOptions( standalone: ( pgn: ref.read(ctrlProvider.notifier).makePgn(), - isLocalEvaluationAllowed: true, + isComputerAnalysisAllowed: true, variant: Variant.standard, orientation: puzzleState.pov, ), diff --git a/lib/src/view/puzzle/streak_screen.dart b/lib/src/view/puzzle/streak_screen.dart index 06fc77e366..cd38aa97fb 100644 --- a/lib/src/view/puzzle/streak_screen.dart +++ b/lib/src/view/puzzle/streak_screen.dart @@ -294,7 +294,7 @@ class _BottomBar extends ConsumerWidget { options: AnalysisOptions( standalone: ( pgn: ref.read(ctrlProvider.notifier).makePgn(), - isLocalEvaluationAllowed: true, + isComputerAnalysisAllowed: true, variant: Variant.standard, orientation: puzzleState.pov, ), diff --git a/lib/src/view/study/study_bottom_bar.dart b/lib/src/view/study/study_bottom_bar.dart index 17f19f2f3d..104f5c29fd 100644 --- a/lib/src/view/study/study_bottom_bar.dart +++ b/lib/src/view/study/study_bottom_bar.dart @@ -168,7 +168,7 @@ class _GamebookBottomBar extends ConsumerWidget { options: AnalysisOptions( standalone: ( pgn: state.pgn, - isLocalEvaluationAllowed: true, + isComputerAnalysisAllowed: true, variant: state.variant, orientation: state.pov, ), diff --git a/lib/src/view/study/study_settings.dart b/lib/src/view/study/study_settings.dart index 2b1527de6a..ea8393242c 100644 --- a/lib/src/view/study/study_settings.dart +++ b/lib/src/view/study/study_settings.dart @@ -22,8 +22,8 @@ class StudySettings extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final studyController = studyControllerProvider(id); - final isLocalEvaluationAllowed = ref.watch( - studyController.select((s) => s.requireValue.isLocalEvaluationAllowed), + final isComputerAnalysisAllowed = ref.watch( + studyController.select((s) => s.requireValue.isComputerAnalysisAllowed), ); final isEngineAvailable = ref.watch( studyController.select((s) => s.requireValue.isEngineAvailable), @@ -40,7 +40,7 @@ class StudySettings extends ConsumerWidget { SwitchSettingTile( title: Text(context.l10n.toggleLocalEvaluation), value: analysisPrefs.enableLocalEvaluation, - onChanged: isLocalEvaluationAllowed + onChanged: isComputerAnalysisAllowed ? (_) { ref.read(studyController.notifier).toggleLocalEvaluation(); } diff --git a/lib/src/view/tools/load_position_screen.dart b/lib/src/view/tools/load_position_screen.dart index 6f92ea14f4..e7655899c7 100644 --- a/lib/src/view/tools/load_position_screen.dart +++ b/lib/src/view/tools/load_position_screen.dart @@ -133,7 +133,7 @@ class _BodyState extends State<_Body> { options: AnalysisOptions( standalone: ( pgn: '[FEN "${pos.fen}"]', - isLocalEvaluationAllowed: true, + isComputerAnalysisAllowed: true, variant: Variant.standard, orientation: Side.white, ), @@ -165,7 +165,7 @@ class _BodyState extends State<_Body> { options: AnalysisOptions( standalone: ( pgn: textInput!, - isLocalEvaluationAllowed: true, + isComputerAnalysisAllowed: true, variant: rule != null ? Variant.fromRule(rule) : Variant.standard, orientation: Side.white, ), diff --git a/lib/src/view/tools/tools_tab_screen.dart b/lib/src/view/tools/tools_tab_screen.dart index b602c80f26..3834d28098 100644 --- a/lib/src/view/tools/tools_tab_screen.dart +++ b/lib/src/view/tools/tools_tab_screen.dart @@ -151,7 +151,7 @@ class _Body extends ConsumerWidget { options: AnalysisOptions( standalone: ( pgn: '', - isLocalEvaluationAllowed: true, + isComputerAnalysisAllowed: true, variant: Variant.standard, orientation: Side.white, ), @@ -170,7 +170,7 @@ class _Body extends ConsumerWidget { options: AnalysisOptions( standalone: ( pgn: '', - isLocalEvaluationAllowed: false, + isComputerAnalysisAllowed: false, variant: Variant.standard, orientation: Side.white, ), diff --git a/lib/src/widgets/pgn.dart b/lib/src/widgets/pgn.dart index f480db8883..fb2a2a6c85 100644 --- a/lib/src/widgets/pgn.dart +++ b/lib/src/widgets/pgn.dart @@ -196,11 +196,15 @@ class _DebouncedPgnTreeViewState extends ConsumerState { @override Widget build(BuildContext context) { final shouldShowComments = ref.watch( - analysisPreferencesProvider.select((value) => value.showPgnComments), + analysisPreferencesProvider.select( + (value) => value.enableComputerAnalysis && value.showPgnComments, + ), ); final shouldShowAnnotations = ref.watch( - analysisPreferencesProvider.select((value) => value.showAnnotations), + analysisPreferencesProvider.select( + (value) => value.enableComputerAnalysis && value.showAnnotations, + ), ); return _PgnTreeView( diff --git a/test/view/analysis/analysis_screen_test.dart b/test/view/analysis/analysis_screen_test.dart index fcc4f46738..44bf19bc87 100644 --- a/test/view/analysis/analysis_screen_test.dart +++ b/test/view/analysis/analysis_screen_test.dart @@ -26,7 +26,7 @@ void main() { options: AnalysisOptions( standalone: ( pgn: sanMoves, - isLocalEvaluationAllowed: false, + isComputerAnalysisAllowed: false, orientation: Side.white, variant: Variant.standard, ), @@ -62,7 +62,7 @@ void main() { options: AnalysisOptions( standalone: ( pgn: sanMoves, - isLocalEvaluationAllowed: false, + isComputerAnalysisAllowed: false, variant: Variant.standard, orientation: Side.white, ), @@ -129,7 +129,7 @@ void main() { options: AnalysisOptions( standalone: ( pgn: pgn, - isLocalEvaluationAllowed: false, + isComputerAnalysisAllowed: false, variant: Variant.standard, orientation: Side.white, ), diff --git a/test/view/opening_explorer/opening_explorer_screen_test.dart b/test/view/opening_explorer/opening_explorer_screen_test.dart index be17a9ce9d..10a3b08a72 100644 --- a/test/view/opening_explorer/opening_explorer_screen_test.dart +++ b/test/view/opening_explorer/opening_explorer_screen_test.dart @@ -42,7 +42,7 @@ void main() { const options = AnalysisOptions( standalone: ( pgn: '', - isLocalEvaluationAllowed: false, + isComputerAnalysisAllowed: false, orientation: Side.white, variant: Variant.standard, ), From 36958f0dd31852486ab1d32ed6add6745f44c0b3 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 21 Nov 2024 19:34:39 +0100 Subject: [PATCH 743/979] Don't show computer variations if computer disabled --- .../model/analysis/analysis_controller.dart | 30 +++-- lib/src/model/common/node.dart | 19 +++- lib/src/model/study/study_controller.dart | 10 +- lib/src/view/analysis/analysis_screen.dart | 8 +- lib/src/widgets/pgn.dart | 104 +++++++++++++----- 5 files changed, 119 insertions(+), 52 deletions(-) diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index 10463ea55e..12600de25d 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -289,9 +289,9 @@ class AnalysisController extends _$AnalysisController _root.isOnMainline(path) ? node.children.skip(1) : node.children; for (final child in childrenToShow) { - child.isHidden = false; + child.isCollapsed = false; for (final grandChild in child.children) { - grandChild.isHidden = false; + grandChild.isCollapsed = false; } } state = AsyncData(state.requireValue.copyWith(root: _root.view)); @@ -302,7 +302,7 @@ class AnalysisController extends _$AnalysisController final node = _root.nodeAt(path); for (final child in node.children) { - child.isHidden = true; + child.isCollapsed = true; } state = AsyncData(state.requireValue.copyWith(root: _root.view)); @@ -334,7 +334,19 @@ class AnalysisController extends _$AnalysisController .read(analysisPreferencesProvider.notifier) .toggleEnableComputerAnalysis(); - await toggleLocalEvaluation(); + final curState = state.requireValue; + final engineWasAvailable = curState.isEngineAvailable; + + state = AsyncData( + curState.copyWith( + isComputerAnalysisEnabled: !curState.isComputerAnalysisEnabled, + ), + ); + + final computerAllowed = state.requireValue.isComputerAnalysisEnabled; + if (!computerAllowed && engineWasAvailable) { + toggleLocalEvaluation(); + } } /// Toggles the local evaluation on/off. @@ -343,11 +355,9 @@ class AnalysisController extends _$AnalysisController .read(analysisPreferencesProvider.notifier) .toggleEnableLocalEvaluation(); - final prefs = ref.read(analysisPreferencesProvider); state = AsyncData( state.requireValue.copyWith( - isLocalEvaluationEnabled: prefs.enableLocalEvaluation, - isComputerAnalysisEnabled: prefs.enableComputerAnalysis, + isLocalEvaluationEnabled: !state.requireValue.isLocalEvaluationEnabled, ), ); @@ -456,9 +466,9 @@ class AnalysisController extends _$AnalysisController // always show variation if the user plays a move if (shouldForceShowVariation && currentNode is Branch && - currentNode.isHidden) { + currentNode.isCollapsed) { _root.updateAt(path, (node) { - if (node is Branch) node.isHidden = false; + if (node is Branch) node.isCollapsed = false; }); } @@ -646,7 +656,7 @@ class AnalysisController extends _$AnalysisController Branch( position: n1.position.playUnchecked(move), sanMove: SanMove(san, move), - isHidden: children.length > 1, + isCollapsed: children.length > 1, ), ); } diff --git a/lib/src/model/common/node.dart b/lib/src/model/common/node.dart index ea9bd6c638..fefd59fdcc 100644 --- a/lib/src/model/common/node.dart +++ b/lib/src/model/common/node.dart @@ -130,7 +130,7 @@ abstract class Node { return null; } - /// Updates all nodes. + /// Recursively applies [update] to all nodes of the tree. void updateAll(void Function(Node node) update) { update(this); for (final child in children) { @@ -360,7 +360,8 @@ class Branch extends Node { super.eval, super.opening, required this.sanMove, - this.isHidden = false, + this.isComputerVariation = false, + this.isCollapsed = false, this.lichessAnalysisComments, // below are fields from dartchess [PgnNodeData] this.startingComments, @@ -368,8 +369,11 @@ class Branch extends Node { this.nags, }); + /// Whether this branch is from a variation generated by lichess computer analysis. + final bool isComputerVariation; + /// Whether the branch should be hidden in the tree view. - bool isHidden; + bool isCollapsed; /// The id of the branch, using a concise notation of associated move. UciCharPair get id => UciCharPair.fromMove(sanMove.move); @@ -398,7 +402,8 @@ class Branch extends Node { eval: eval, opening: opening, children: IList(children.map((child) => child.view)), - isHidden: isHidden, + isComputerVariation: isComputerVariation, + isCollapsed: isCollapsed, lichessAnalysisComments: lichessAnalysisComments?.lock, startingComments: startingComments?.lock, comments: comments?.lock, @@ -487,7 +492,8 @@ class Root extends Node { final branch = Branch( sanMove: SanMove(childFrom.data.san, move), position: newPos, - isHidden: frame.nesting > 2 || hideVariations && childIdx > 0, + isCollapsed: frame.nesting > 2 || hideVariations && childIdx > 0, + isComputerVariation: isLichessAnalysis && childIdx > 0, lichessAnalysisComments: isLichessAnalysis ? comments?.toList() : null, startingComments: isLichessAnalysis @@ -587,7 +593,8 @@ class ViewBranch extends ViewNode with _$ViewBranch { required Position position, Opening? opening, required IList children, - @Default(false) bool isHidden, + @Default(false) bool isCollapsed, + required bool isComputerVariation, ClientEval? eval, IList? lichessAnalysisComments, IList? startingComments, diff --git a/lib/src/model/study/study_controller.dart b/lib/src/model/study/study_controller.dart index 459751d81b..5648833799 100644 --- a/lib/src/model/study/study_controller.dart +++ b/lib/src/model/study/study_controller.dart @@ -289,9 +289,9 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { _root.isOnMainline(path) ? node.children.skip(1) : node.children; for (final child in childrenToShow) { - child.isHidden = false; + child.isCollapsed = false; for (final grandChild in child.children) { - grandChild.isHidden = false; + grandChild.isCollapsed = false; } } state = AsyncValue.data(state.requireValue.copyWith(root: _root.view)); @@ -304,7 +304,7 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { final node = _root.nodeAt(path); for (final child in node.children) { - child.isHidden = true; + child.isCollapsed = true; } state = AsyncValue.data(state.requireValue.copyWith(root: _root.view)); @@ -417,9 +417,9 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { // always show variation if the user plays a move if (shouldForceShowVariation && currentNode is Branch && - currentNode.isHidden) { + currentNode.isCollapsed) { _root.updateAt(path, (node) { - if (node is Branch) node.isHidden = false; + if (node is Branch) node.isCollapsed = false; }); } diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index 476645fca7..2a701e57e2 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -97,10 +97,12 @@ class _AnalysisScreenState extends ConsumerState @override Widget build(BuildContext context) { final ctrlProvider = analysisControllerProvider(widget.options); - final asyncState = ref.watch(ctrlProvider); + final prefs = ref.watch(analysisPreferencesProvider); + final appBarActions = [ - EngineDepth(defaultEval: asyncState.valueOrNull?.currentNode.eval), + if (prefs.enableComputerAnalysis) + EngineDepth(defaultEval: asyncState.valueOrNull?.currentNode.eval), AppBarAnalysisTabIndicator( tabs: tabs, controller: _tabController, @@ -137,7 +139,7 @@ class _AnalysisScreenState extends ConsumerState ), ); case AsyncError(:final error, :final stackTrace): - _logger.severe('Cannot load study: $error', stackTrace); + _logger.severe('Cannot load analysis: $error', stackTrace); return FullScreenRetryRequest( onRetry: () { ref.invalidate(ctrlProvider); diff --git a/lib/src/widgets/pgn.dart b/lib/src/widgets/pgn.dart index fb2a2a6c85..8781cf9bc0 100644 --- a/lib/src/widgets/pgn.dart +++ b/lib/src/widgets/pgn.dart @@ -195,22 +195,30 @@ class _DebouncedPgnTreeViewState extends ConsumerState { // using the fast replay buttons. @override Widget build(BuildContext context) { - final shouldShowComments = ref.watch( - analysisPreferencesProvider.select( - (value) => value.enableComputerAnalysis && value.showPgnComments, - ), + final withComputerAnalysis = ref.watch( + analysisPreferencesProvider + .select((value) => value.enableComputerAnalysis), ); - final shouldShowAnnotations = ref.watch( - analysisPreferencesProvider.select( - (value) => value.enableComputerAnalysis && value.showAnnotations, - ), - ); + final shouldShowComments = withComputerAnalysis && + ref.watch( + analysisPreferencesProvider.select( + (value) => value.showPgnComments, + ), + ); + + final shouldShowAnnotations = withComputerAnalysis && + ref.watch( + analysisPreferencesProvider.select( + (value) => value.showAnnotations, + ), + ); return _PgnTreeView( root: widget.root, rootComments: widget.pgnRootComments, params: ( + withComputerAnalysis: withComputerAnalysis, shouldShowAnnotations: shouldShowAnnotations, shouldShowComments: shouldShowComments, currentMoveKey: currentMoveKey, @@ -229,6 +237,11 @@ typedef _PgnTreeViewParams = ({ /// Path to the currently selected move in the tree. UciPath pathToCurrentMove, + /// Whether to show NAG, comments, and analysis variations. + /// + /// Takes precedence over [shouldShowAnnotations], and [shouldShowComments], + bool withComputerAnalysis, + /// Whether to show NAG annotations like '!' and '??'. bool shouldShowAnnotations, @@ -243,6 +256,15 @@ typedef _PgnTreeViewParams = ({ PgnTreeNotifier notifier, }); +IList _computerPrefAwareChildren( + ViewNode node, + bool withComputerAnalysis, +) { + return node.children + .where((c) => withComputerAnalysis || !c.isComputerVariation) + .toIList(); +} + /// Whether to display the sideline inline. /// /// Sidelines are usually rendered on a new line and indented. @@ -255,16 +277,22 @@ bool _displaySideLineAsInline(ViewBranch node, [int depth = 0]) { } /// Returns whether this node has a sideline that should not be displayed inline. -bool _hasNonInlineSideLine(ViewNode node) => - node.children.length > 2 || - (node.children.length == 2 && !_displaySideLineAsInline(node.children[1])); +bool _hasNonInlineSideLine(ViewNode node, _PgnTreeViewParams params) { + final children = + _computerPrefAwareChildren(node, params.withComputerAnalysis); + return children.length > 2 || + (children.length == 2 && !_displaySideLineAsInline(children[1])); +} /// Splits the mainline into parts, where each part is a sequence of moves that are displayed on the same line. /// /// A part ends when a mainline node has a sideline that should not be displayed inline. -Iterable> _mainlineParts(ViewRoot root) => +Iterable> _mainlineParts( + ViewRoot root, + _PgnTreeViewParams params, +) => [root, ...root.mainline] - .splitAfter(_hasNonInlineSideLine) + .splitAfter((n) => _hasNonInlineSideLine(n, params)) .takeWhile((nodes) => nodes.firstOrNull?.children.isNotEmpty == true); class _PgnTreeView extends StatefulWidget { @@ -382,7 +410,8 @@ class _PgnTreeViewState extends State<_PgnTreeView> { void _updateLines({required bool fullRebuild}) { setState(() { if (fullRebuild) { - mainlineParts = _mainlineParts(widget.root).toList(growable: false); + mainlineParts = + _mainlineParts(widget.root, widget.params).toList(growable: false); } subtrees = _buildChangedSubtrees(fullRebuild: fullRebuild); @@ -400,6 +429,8 @@ class _PgnTreeViewState extends State<_PgnTreeView> { super.didUpdateWidget(oldWidget); _updateLines( fullRebuild: oldWidget.root != widget.root || + oldWidget.params.withComputerAnalysis != + widget.params.withComputerAnalysis || oldWidget.params.shouldShowComments != widget.params.shouldShowComments || oldWidget.params.shouldShowAnnotations != @@ -632,6 +663,12 @@ class _SideLinePart extends ConsumerWidget { /// A widget that renders part of the mainline. /// /// A part of the mainline is rendered on a single line. See [_mainlineParts]. +/// +/// For example: +/// 1. e4 e5 <-- mainline part +/// |- 1... d5 <-- sideline part +/// |- 1... Nc6 <-- sideline part +/// 2. Nf3 Nc6 (2... a5) 3. Bc4 <-- mainline part class _MainLinePart extends ConsumerWidget { const _MainLinePart({ required this.initialPath, @@ -655,27 +692,37 @@ class _MainLinePart extends ConsumerWidget { return Text.rich( TextSpan( children: nodes - .takeWhile((node) => node.children.isNotEmpty) + .takeWhile( + (node) => + _computerPrefAwareChildren(node, params.withComputerAnalysis) + .isNotEmpty, + ) .mapIndexed( (i, node) { - final mainlineNode = node.children.first; + final children = _computerPrefAwareChildren( + node, + params.withComputerAnalysis, + ); + final mainlineNode = children.first; final moves = [ _moveWithComment( mainlineNode, lineInfo: ( type: _LineType.mainline, - startLine: i == 0 || (node as ViewBranch).hasTextComment, + startLine: i == 0 || + (params.shouldShowComments && + (node as ViewBranch).hasTextComment), pathToLine: initialPath, ), pathToNode: path, textStyle: textStyle, params: params, ), - if (node.children.length == 2 && - _displaySideLineAsInline(node.children[1])) ...[ + if (children.length == 2 && + _displaySideLineAsInline(children[1])) ...[ _buildInlineSideLine( followsComment: mainlineNode.hasTextComment, - firstNode: node.children[1], + firstNode: children[1], parent: node, initialPath: path, textStyle: textStyle, @@ -719,7 +766,7 @@ class _SideLine extends StatelessWidget { List _getSidelinePartNodes() { final sidelineNodes = [firstNode]; while (sidelineNodes.last.children.isNotEmpty && - !_hasNonInlineSideLine(sidelineNodes.last)) { + !_hasNonInlineSideLine(sidelineNodes.last, params)) { sidelineNodes.add(sidelineNodes.last.children.first); } return sidelineNodes.toList(growable: false); @@ -848,7 +895,7 @@ class _IndentedSideLinesState extends State<_IndentedSideLines> { /// calculate the position of the indents in a post-frame callback. void _redrawIndents() { _sideLinesStartKeys = List.generate( - _visibleSideLines.length + (_hasHiddenLines ? 1 : 0), + _expandedSidelines.length + (_hasCollapsedLines ? 1 : 0), (_) => GlobalKey(), ); WidgetsBinding.instance.addPostFrameCallback((_) { @@ -871,10 +918,11 @@ class _IndentedSideLinesState extends State<_IndentedSideLines> { }); } - bool get _hasHiddenLines => widget.sideLines.any((node) => node.isHidden); + bool get _hasCollapsedLines => + widget.sideLines.any((node) => node.isCollapsed); - Iterable get _visibleSideLines => - widget.sideLines.whereNot((node) => node.isHidden); + Iterable get _expandedSidelines => + widget.sideLines.whereNot((node) => node.isCollapsed); @override void initState() { @@ -892,7 +940,7 @@ class _IndentedSideLinesState extends State<_IndentedSideLines> { @override Widget build(BuildContext context) { - final sideLineWidgets = _visibleSideLines + final sideLineWidgets = _expandedSidelines .mapIndexed( (i, firstSidelineNode) => _SideLine( firstNode: firstSidelineNode, @@ -920,7 +968,7 @@ class _IndentedSideLinesState extends State<_IndentedSideLines> { crossAxisAlignment: CrossAxisAlignment.start, children: [ ...sideLineWidgets, - if (_hasHiddenLines) + if (_hasCollapsedLines) GestureDetector( child: Icon( Icons.add_box, From 7af00eb163b02330db1bf3e0d5b78122682e985b Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Thu, 21 Nov 2024 20:47:12 +0100 Subject: [PATCH 744/979] use the switch syntax instead of when for AsyncValue --- .../view/broadcast/broadcast_boards_tab.dart | 28 ++--- .../view/broadcast/broadcast_game_screen.dart | 24 ++-- .../broadcast/broadcast_overview_tab.dart | 118 +++++++++--------- lib/src/view/broadcast/broadcast_screen.dart | 53 ++++---- 4 files changed, 110 insertions(+), 113 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_boards_tab.dart b/lib/src/view/broadcast/broadcast_boards_tab.dart index 4345472a72..24a92979cc 100644 --- a/lib/src/view/broadcast/broadcast_boards_tab.dart +++ b/lib/src/view/broadcast/broadcast_boards_tab.dart @@ -44,30 +44,30 @@ class BroadcastBoardsTab extends ConsumerWidget { return SafeArea( bottom: false, - child: games.when( - data: (games) => (games.isEmpty) + child: switch (games) { + AsyncData(:final value) => (value.isEmpty) ? const Padding( padding: Styles.bodyPadding, child: Text('No boards to show for now'), ) : BroadcastPreview( - games: games.values.toIList(), + games: value.values.toIList(), roundId: roundId, title: title, ), - loading: () => const Shimmer( - child: ShimmerLoading( - isLoading: true, - child: BroadcastPreview( - roundId: BroadcastRoundId(''), - title: '', + AsyncError(:final error) => Center( + child: Text(error.toString()), + ), + _ => const Shimmer( + child: ShimmerLoading( + isLoading: true, + child: BroadcastPreview( + roundId: BroadcastRoundId(''), + title: '', + ), ), ), - ), - error: (error, stackTrace) => Center( - child: Text(error.toString()), - ), - ), + }, ); } } diff --git a/lib/src/view/broadcast/broadcast_game_screen.dart b/lib/src/view/broadcast/broadcast_game_screen.dart index a29a2e116b..0ad08d89cc 100644 --- a/lib/src/view/broadcast/broadcast_game_screen.dart +++ b/lib/src/view/broadcast/broadcast_game_screen.dart @@ -31,9 +31,6 @@ import 'package:lichess_mobile/src/widgets/clock.dart'; import 'package:lichess_mobile/src/widgets/pgn.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; -import 'package:logging/logging.dart'; - -final _logger = Logger('BroadcastGameScreen'); class BroadcastGameScreen extends ConsumerWidget { final BroadcastRoundId roundId; @@ -48,14 +45,15 @@ class BroadcastGameScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final state = ref.watch(broadcastGameControllerProvider(roundId, gameId)); + final broadcastGameState = + ref.watch(broadcastGameControllerProvider(roundId, gameId)); return PlatformScaffold( appBar: PlatformAppBar( title: Text(title), actions: [ AppBarIconButton( - onPressed: () => (state.hasValue) + onPressed: () => (broadcastGameState.hasValue) ? showAdaptiveBottomSheet( context: context, isScrollControlled: true, @@ -72,17 +70,13 @@ class BroadcastGameScreen extends ConsumerWidget { ), ], ), - body: state.when( - data: (state) => _Body(roundId, gameId), - loading: () => - const Center(child: CircularProgressIndicator.adaptive()), - error: (error, stackTrace) { - _logger.severe('Cannot load broadcast game: $error', stackTrace); - return Center( + body: switch (broadcastGameState) { + AsyncData() => _Body(roundId, gameId), + AsyncError(:final error) => Center( child: Text('Cannot load broadcast game: $error'), - ); - }, - ), + ), + _ => const Center(child: CircularProgressIndicator.adaptive()), + }, ); } } diff --git a/lib/src/view/broadcast/broadcast_overview_tab.dart b/lib/src/view/broadcast/broadcast_overview_tab.dart index f2564e612c..f61fe79138 100644 --- a/lib/src/view/broadcast/broadcast_overview_tab.dart +++ b/lib/src/view/broadcast/broadcast_overview_tab.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_providers.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; @@ -25,69 +26,74 @@ class BroadcastOverviewTab extends ConsumerWidget { child: SingleChildScrollView( child: Padding( padding: Styles.bodyPadding, - child: tournament.when( - data: (tournament) { - final information = tournament.data.information; - final description = tournament.data.description; - - return Column( - children: [ - Wrap( - alignment: WrapAlignment.center, - children: [ - if (information.dates != null) - BroadcastOverviewCard( - CupertinoIcons.calendar, - information.dates!.endsAt == null - ? _dateFormatter - .format(information.dates!.startsAt) - : '${_dateFormatter.format(information.dates!.startsAt)} - ${_dateFormatter.format(information.dates!.endsAt!)}', - ), - if (information.format != null) - BroadcastOverviewCard( - Icons.emoji_events, - '${information.format}', - ), - if (information.timeControl != null) - BroadcastOverviewCard( - CupertinoIcons.stopwatch_fill, - '${information.timeControl}', - ), - if (information.players != null) - BroadcastOverviewCard( - Icons.person, - '${information.players}', - ), - ], - ), - if (description != null) - Padding( - padding: const EdgeInsets.all(16), - child: MarkdownBody( - data: description, - onTapLink: (text, url, title) { - if (url == null) return; - launchUrl(Uri.parse(url)); - }, - ), - ), - ], - ); - }, - loading: () => - const Center(child: CircularProgressIndicator.adaptive()), - error: (error, _) { - return Center( + child: switch (tournament) { + AsyncData(:final value) => BroadcastOverviewBody(value), + AsyncError(:final error) => Center( child: Text('Cannot load game analysis: $error'), - ); - }, - ), + ), + _ => const Center(child: CircularProgressIndicator.adaptive()), + }, ), ), ); } } +class BroadcastOverviewBody extends StatelessWidget { + final BroadcastTournament tournament; + + const BroadcastOverviewBody(this.tournament); + + @override + Widget build(BuildContext context) { + final information = tournament.data.information; + final description = tournament.data.description; + + return Column( + children: [ + Wrap( + alignment: WrapAlignment.center, + children: [ + if (information.dates != null) + BroadcastOverviewCard( + CupertinoIcons.calendar, + information.dates!.endsAt == null + ? _dateFormatter.format(information.dates!.startsAt) + : '${_dateFormatter.format(information.dates!.startsAt)} - ${_dateFormatter.format(information.dates!.endsAt!)}', + ), + if (information.format != null) + BroadcastOverviewCard( + Icons.emoji_events, + '${information.format}', + ), + if (information.timeControl != null) + BroadcastOverviewCard( + CupertinoIcons.stopwatch_fill, + '${information.timeControl}', + ), + if (information.players != null) + BroadcastOverviewCard( + Icons.person, + '${information.players}', + ), + ], + ), + if (description != null) + Padding( + padding: const EdgeInsets.all(16), + child: MarkdownBody( + data: description, + onTapLink: (text, url, title) { + if (url == null) return; + launchUrl(Uri.parse(url)); + }, + ), + ), + ], + ); + } +} + class BroadcastOverviewCard extends StatelessWidget { final IconData iconData; final String text; diff --git a/lib/src/view/broadcast/broadcast_screen.dart b/lib/src/view/broadcast/broadcast_screen.dart index d0818fc586..ee19d4f868 100644 --- a/lib/src/view/broadcast/broadcast_screen.dart +++ b/lib/src/view/broadcast/broadcast_screen.dart @@ -211,17 +211,17 @@ class _AndroidTournamentAndRoundSelector extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final tournament = ref.watch(broadcastTournamentProvider(tournamentId)); - return tournament.when( - data: (tournament) { - return Row( + return switch (tournament) { + AsyncData(:final value) => + Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - if (tournament.group != null) + if (value.group != null) Flexible( child: DropdownMenu( label: const Text('Tournament'), - initialSelection: tournament.data.id, - dropdownMenuEntries: tournament.group! + initialSelection: value.data.id, + dropdownMenuEntries: value.group! .map( (tournament) => DropdownMenuEntry( @@ -242,8 +242,8 @@ class _AndroidTournamentAndRoundSelector extends ConsumerWidget { Flexible( child: DropdownMenu( label: const Text('Round'), - initialSelection: tournament.defaultRoundId, - dropdownMenuEntries: tournament.rounds + initialSelection: value.defaultRoundId, + dropdownMenuEntries: value.rounds .map( (BroadcastRound round) => DropdownMenuEntry( @@ -258,11 +258,9 @@ class _AndroidTournamentAndRoundSelector extends ConsumerWidget { ), ), ], - ); - }, - loading: () => const SizedBox.shrink(), - error: (error, stackTrace) => Center(child: Text(error.toString())), - ); + ), + AsyncError(:final error) => Center(child: Text(error.toString())), + _ => const SizedBox.shrink(),}; } } @@ -341,12 +339,12 @@ class _IOSTournamentAndRoundSelector extends ConsumerWidget { final backgroundColor = CupertinoTheme.of(context).barBackgroundColor; final tournament = ref.watch(broadcastTournamentProvider(tournamentId)); - return tournament.when( - data: (tournament) { + return switch(tournament) { + AsyncData(:final value) => /// It should be replaced with a Flutter toolbar widget once it is implemented. /// See https://github.com/flutter/flutter/issues/134454 - return _wrapWithBackground( + _wrapWithBackground( backgroundColor: backgroundColor, border: _kDefaultToolBarBorder, child: SafeArea( @@ -357,11 +355,11 @@ class _IOSTournamentAndRoundSelector extends ConsumerWidget { spacing: 16.0, mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - if (tournament.group != null) + if (value.group != null) Flexible( child: CupertinoButton.tinted( child: Text( - tournament.group! + value.group! .firstWhere( (tournament) => tournament.id == tournamentId, ) @@ -371,11 +369,11 @@ class _IOSTournamentAndRoundSelector extends ConsumerWidget { onPressed: () { showChoicePicker( context, - choices: tournament.group! + choices: value.group! .map((tournament) => tournament.id) .toList(), labelBuilder: (tournamentId) => Text( - tournament.group! + value.group! .firstWhere( (tournament) => tournament.id == tournamentId, @@ -398,7 +396,7 @@ class _IOSTournamentAndRoundSelector extends ConsumerWidget { Flexible( child: CupertinoButton.tinted( child: Text( - tournament.rounds + value.rounds .firstWhere( (round) => round.id == roundId, ) @@ -408,13 +406,13 @@ class _IOSTournamentAndRoundSelector extends ConsumerWidget { onPressed: () { showChoicePicker( context, - choices: tournament.rounds + choices: value.rounds .map( (round) => round.id, ) .toList(), labelBuilder: (roundId) => Text( - tournament.rounds + value.rounds .firstWhere((round) => round.id == roundId) .name, ), @@ -430,11 +428,10 @@ class _IOSTournamentAndRoundSelector extends ConsumerWidget { ), ), ), - ); - }, - loading: () => const SizedBox.shrink(), - error: (error, stackTrace) => Center(child: Text(error.toString())), - ); + ), + AsyncError(:final error) => Center(child: Text(error.toString())), + _ => const SizedBox.shrink(), + }; } } From fce8378b99fc260f84b6ea08432ca7cad60e5d18 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 21 Nov 2024 21:01:44 +0100 Subject: [PATCH 745/979] More work on analysis computer toggle --- .../model/analysis/analysis_controller.dart | 4 ++- lib/src/view/analysis/analysis_board.dart | 30 ++++++++++--------- lib/src/view/analysis/analysis_screen.dart | 22 ++++++++++++-- 3 files changed, 39 insertions(+), 17 deletions(-) diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index 12600de25d..c9db2303ae 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -793,7 +793,9 @@ class AnalysisState with _$AnalysisState { /// Whether the server analysis is available. bool get hasServerAnalysis => playersAnalysis != null; - bool get canShowGameSummary => hasServerAnalysis || canRequestServerAnalysis; + bool get canShowGameSummary => + isComputerAnalysisEnabledAndAllowed && + (hasServerAnalysis || canRequestServerAnalysis); /// Whether an evaluation can be available bool get hasAvailableEval => diff --git a/lib/src/view/analysis/analysis_board.dart b/lib/src/view/analysis/analysis_board.dart index c6e70e8557..8f37f0fe54 100644 --- a/lib/src/view/analysis/analysis_board.dart +++ b/lib/src/view/analysis/analysis_board.dart @@ -41,23 +41,25 @@ class AnalysisBoardState extends ConsumerState { final ctrlProvider = analysisControllerProvider(widget.options); final analysisState = ref.watch(ctrlProvider).requireValue; final boardPrefs = ref.watch(boardPreferencesProvider); - final showBestMoveArrow = ref.watch( - analysisPreferencesProvider.select( - (value) => value.showBestMoveArrow, - ), - ); - final showAnnotationsOnBoard = ref.watch( - analysisPreferencesProvider.select((value) => value.showAnnotations), - ); - - final evalBestMoves = ref.watch( - engineEvaluationProvider.select((s) => s.eval?.bestMoves), - ); + final analysisPrefs = ref.watch(analysisPreferencesProvider); + final enableComputerAnalysis = analysisPrefs.enableComputerAnalysis; + final showBestMoveArrow = + enableComputerAnalysis && analysisPrefs.showBestMoveArrow; + final showAnnotationsOnBoard = + enableComputerAnalysis && analysisPrefs.showAnnotations; + final evalBestMoves = enableComputerAnalysis + ? ref.watch( + engineEvaluationProvider.select((s) => s.eval?.bestMoves), + ) + : null; final currentNode = analysisState.currentNode; - final annotation = makeAnnotation(currentNode.nags); + final annotation = + showAnnotationsOnBoard ? makeAnnotation(currentNode.nags) : null; - final bestMoves = evalBestMoves ?? currentNode.eval?.bestMoves; + final bestMoves = enableComputerAnalysis + ? evalBestMoves ?? currentNode.eval?.bestMoves + : null; final sanMove = currentNode.sanMove; diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index 2a701e57e2..129f27620c 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -67,8 +67,12 @@ class _AnalysisScreenState extends ConsumerState ref.listenManual>( analysisControllerProvider(widget.options), - (_, state) { - if (state.valueOrNull?.canShowGameSummary == true) { + (prev, state) { + final canPrevShowGameSummary = + prev?.valueOrNull?.canShowGameSummary == true; + final canShowGameSummary = + state.valueOrNull?.canShowGameSummary == true; + if (!canPrevShowGameSummary && canShowGameSummary) { setState(() { tabs = [ AnalysisTab.opening, @@ -83,6 +87,20 @@ class _AnalysisScreenState extends ConsumerState length: tabs.length, ); }); + } else if (canPrevShowGameSummary && !canShowGameSummary) { + setState(() { + tabs = [ + AnalysisTab.opening, + AnalysisTab.moves, + ]; + final index = _tabController.index; + _tabController.dispose(); + _tabController = TabController( + vsync: this, + initialIndex: index == 2 ? 1 : index, + length: tabs.length, + ); + }); } }, ); From ee333725f0f65d6dd4a0a1cf95ed589ef132cd45 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Thu, 21 Nov 2024 21:04:18 +0100 Subject: [PATCH 746/979] should fix some rare occurrences where clocks are not appearing on the app but are on the website --- .../broadcast/broadcast_round_controller.dart | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/lib/src/model/broadcast/broadcast_round_controller.dart b/lib/src/model/broadcast/broadcast_round_controller.dart index 5dcfdfee30..95aefe2dc8 100644 --- a/lib/src/model/broadcast/broadcast_round_controller.dart +++ b/lib/src/model/broadcast/broadcast_round_controller.dart @@ -104,22 +104,30 @@ class BroadcastRoundController extends _$BroadcastRoundController { void _handleClockEvent(SocketEvent event) { final broadcastGameId = pick(event.data, 'p', 'chapterId').asBroadcastGameIdOrThrow(); - final whiteClock = pick(event.data, 'p', 'relayClocks', 0) - .asDurationFromCentiSecondsOrNull(); - final blackClock = pick(event.data, 'p', 'relayClocks', 1) - .asDurationFromCentiSecondsOrNull(); + final relayClocks = pick(event.data, 'p', 'relayClocks'); + + // We check that the clocks for the broadcast game preview have been updated else we do nothing + if (relayClocks.value == null) return; + + final newClocks = { + Side.white: relayClocks(0).asDurationFromCentiSecondsOrNull(), + Side.black: relayClocks(1).asDurationFromCentiSecondsOrNull(), + }; + state = AsyncData( state.requireValue.update( broadcastGameId, (broadcastsGame) => broadcastsGame.copyWith( - updatedClockAt: DateTime.now(), + updatedClockAt: (newClocks[broadcastsGame.sideToMove] == null) + ? broadcastsGame.updatedClockAt + : DateTime.now(), players: IMap( { Side.white: broadcastsGame.players[Side.white]!.copyWith( - clock: whiteClock, + clock: newClocks[Side.white], ), Side.black: broadcastsGame.players[Side.black]!.copyWith( - clock: blackClock, + clock: newClocks[Side.black], ), }, ), From 28d832388f0da8e71abbb653e578a48a855f0544 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Thu, 21 Nov 2024 21:06:17 +0100 Subject: [PATCH 747/979] fix format --- lib/src/view/broadcast/broadcast_screen.dart | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_screen.dart b/lib/src/view/broadcast/broadcast_screen.dart index ee19d4f868..efcd02d318 100644 --- a/lib/src/view/broadcast/broadcast_screen.dart +++ b/lib/src/view/broadcast/broadcast_screen.dart @@ -212,8 +212,7 @@ class _AndroidTournamentAndRoundSelector extends ConsumerWidget { final tournament = ref.watch(broadcastTournamentProvider(tournamentId)); return switch (tournament) { - AsyncData(:final value) => - Row( + AsyncData(:final value) => Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ if (value.group != null) @@ -260,7 +259,8 @@ class _AndroidTournamentAndRoundSelector extends ConsumerWidget { ], ), AsyncError(:final error) => Center(child: Text(error.toString())), - _ => const SizedBox.shrink(),}; + _ => const SizedBox.shrink(), + }; } } @@ -339,12 +339,13 @@ class _IOSTournamentAndRoundSelector extends ConsumerWidget { final backgroundColor = CupertinoTheme.of(context).barBackgroundColor; final tournament = ref.watch(broadcastTournamentProvider(tournamentId)); - return switch(tournament) { - AsyncData(:final value) => + return switch (tournament) { + AsyncData(:final value) => + /// It should be replaced with a Flutter toolbar widget once it is implemented. /// See https://github.com/flutter/flutter/issues/134454 - _wrapWithBackground( + _wrapWithBackground( backgroundColor: backgroundColor, border: _kDefaultToolBarBorder, child: SafeArea( From e03e0b4a15b8dc37382b17c5cca8930b13bc74bd Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Thu, 21 Nov 2024 21:26:05 +0100 Subject: [PATCH 748/979] fix new boards not being added to the round --- lib/src/model/broadcast/broadcast_round_controller.dart | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/src/model/broadcast/broadcast_round_controller.dart b/lib/src/model/broadcast/broadcast_round_controller.dart index 95aefe2dc8..f46c866fe8 100644 --- a/lib/src/model/broadcast/broadcast_round_controller.dart +++ b/lib/src/model/broadcast/broadcast_round_controller.dart @@ -50,6 +50,9 @@ class BroadcastRoundController extends _$BroadcastRoundController { // Sent when a node is recevied from the broadcast case 'addNode': _handleAddNodeEvent(event); + // Sent when a new board is added + case 'addChapter': + _handleAddChapterEvent(event); // Sent when the state of games changes case 'chapters': _handleChaptersEvent(event); @@ -96,6 +99,10 @@ class BroadcastRoundController extends _$BroadcastRoundController { ); } + void _handleAddChapterEvent(SocketEvent event) { + ref.invalidateSelf(); + } + void _handleChaptersEvent(SocketEvent event) { final games = pick(event.data).asListOrThrow(gameFromPick); state = AsyncData(IMap.fromEntries(games)); From fd7a0c60468dbf40099a6f3de2ef7aecc7fb47de Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Thu, 21 Nov 2024 08:52:29 +0100 Subject: [PATCH 749/979] hopefully really really fix the clock not updating at the end of a game --- .../broadcast/broadcast_game_controller.dart | 31 +++++++++---------- .../view/broadcast/broadcast_game_screen.dart | 12 +++---- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/lib/src/model/broadcast/broadcast_game_controller.dart b/lib/src/model/broadcast/broadcast_game_controller.dart index 4699e2447d..e8e2db9cda 100644 --- a/lib/src/model/broadcast/broadcast_game_controller.dart +++ b/lib/src/model/broadcast/broadcast_game_controller.dart @@ -84,7 +84,7 @@ class BroadcastGameController extends _$BroadcastGameController final broadcastState = BroadcastGameState( id: gameId, currentPath: currentPath, - broadcastLivePath: pgnHeaders['Result'] == '*' ? currentPath : null, + broadcastPath: currentPath, isOnMainline: _root.isOnMainline(currentPath), root: _root.view, currentNode: AnalysisCurrentNode.fromNode(currentNode), @@ -162,8 +162,7 @@ class BroadcastGameController extends _$BroadcastGameController ); } else { state = AsyncData( - state.requireValue - .copyWith(broadcastLivePath: newPath, root: _root.view), + state.requireValue.copyWith(broadcastPath: newPath, root: _root.view), ); } } @@ -186,13 +185,7 @@ class BroadcastGameController extends _$BroadcastGameController final pgnHeaders = state.requireValue.pgnHeaders.addEntries(pgnHeadersEntries); state = AsyncData( - state.requireValue.copyWith( - pgnHeaders: pgnHeaders, - // If the game is not ongoing, the [broadcastLivePath] should be null - broadcastLivePath: pgnHeaders['Result'] == '*' - ? state.requireValue.broadcastLivePath - : null, - ), + state.requireValue.copyWith(pgnHeaders: pgnHeaders), ); } @@ -458,8 +451,8 @@ class BroadcastGameController extends _$BroadcastGameController state = AsyncData( state.requireValue.copyWith( currentPath: path, - broadcastLivePath: - isBroadcastMove ? path : state.requireValue.broadcastLivePath, + broadcastPath: + isBroadcastMove ? path : state.requireValue.broadcastPath, isOnMainline: _root.isOnMainline(path), currentNode: AnalysisCurrentNode.fromNode(currentNode), lastMove: currentNode.sanMove.move, @@ -472,8 +465,8 @@ class BroadcastGameController extends _$BroadcastGameController state = AsyncData( state.requireValue.copyWith( currentPath: path, - broadcastLivePath: - isBroadcastMove ? path : state.requireValue.broadcastLivePath, + broadcastPath: + isBroadcastMove ? path : state.requireValue.broadcastPath, isOnMainline: _root.isOnMainline(path), currentNode: AnalysisCurrentNode.fromNode(currentNode), lastMove: null, @@ -558,8 +551,8 @@ class BroadcastGameState with _$BroadcastGameState { /// The path to the current node in the analysis view. required UciPath currentPath, - // The path to the current broadcast live move. - required UciPath? broadcastLivePath, + /// The path to the last broadcast move. + required UciPath broadcastPath, /// Whether the current path is on the mainline. required bool isOnMainline, @@ -595,6 +588,12 @@ class BroadcastGameState with _$BroadcastGameState { bool get canGoNext => currentNode.hasChild; bool get canGoBack => currentPath.size > UciPath.empty.size; + /// Whether the game is still ongoing + bool get isOngoing => pgnHeaders['Result'] == '*'; + + /// The path to the current broadcast live move + UciPath? get broadcastLivePath => isOngoing ? broadcastPath : null; + EngineGaugeParams get engineGaugeParams => ( orientation: pov, isLocalEngineAvailable: isLocalEvaluationEnabled, diff --git a/lib/src/view/broadcast/broadcast_game_screen.dart b/lib/src/view/broadcast/broadcast_game_screen.dart index 0ad08d89cc..e763747b3f 100644 --- a/lib/src/view/broadcast/broadcast_game_screen.dart +++ b/lib/src/view/broadcast/broadcast_game_screen.dart @@ -416,16 +416,16 @@ class _PlayerWidget extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final gameState = ref + final broadcastGameState = ref .watch(broadcastGameControllerProvider(roundId, gameId)) .requireValue; - final clocks = gameState.clocks; + final clocks = broadcastGameState.clocks; final isCursorOnLiveMove = - gameState.currentPath == gameState.broadcastLivePath; - final sideToMove = gameState.position.turn; + broadcastGameState.currentPath == broadcastGameState.broadcastLivePath; + final sideToMove = broadcastGameState.position.turn; final side = switch (widgetPosition) { - _PlayerWidgetPosition.bottom => gameState.pov, - _PlayerWidgetPosition.top => gameState.pov.opposite, + _PlayerWidgetPosition.bottom => broadcastGameState.pov, + _PlayerWidgetPosition.top => broadcastGameState.pov.opposite, }; final clock = (sideToMove == side) ? clocks?.parentClock : clocks?.clock; From 8b43cf6b64ee5877e074664c34e40d011e726b55 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Thu, 21 Nov 2024 22:39:21 +0100 Subject: [PATCH 750/979] still play broadcast received moves even if the game ended --- .../model/broadcast/broadcast_game_controller.dart | 3 +-- .../view/broadcast/broadcast_game_bottom_bar.dart | 13 ++++++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/lib/src/model/broadcast/broadcast_game_controller.dart b/lib/src/model/broadcast/broadcast_game_controller.dart index e8e2db9cda..aa4cdd6885 100644 --- a/lib/src/model/broadcast/broadcast_game_controller.dart +++ b/lib/src/model/broadcast/broadcast_game_controller.dart @@ -152,8 +152,7 @@ class BroadcastGameController extends _$BroadcastGameController if (newPath != null) { _root.promoteAt(newPath, toMainline: true); - if (state.requireValue.broadcastLivePath == - state.requireValue.currentPath) { + if (state.requireValue.broadcastPath == state.requireValue.currentPath) { _setPath( newPath, shouldRecomputeRootView: isNewNode, diff --git a/lib/src/view/broadcast/broadcast_game_bottom_bar.dart b/lib/src/view/broadcast/broadcast_game_bottom_bar.dart index ef3a9d550f..7582fa83b5 100644 --- a/lib/src/view/broadcast/broadcast_game_bottom_bar.dart +++ b/lib/src/view/broadcast/broadcast_game_bottom_bar.dart @@ -19,7 +19,7 @@ class BroadcastGameBottomBar extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final ctrlProvider = broadcastGameControllerProvider(roundId, gameId); - final analysisState = ref.watch(ctrlProvider).requireValue; + final broadcastGameState = ref.watch(ctrlProvider).requireValue; return BottomBar( children: [ @@ -36,22 +36,25 @@ class BroadcastGameBottomBar extends ConsumerWidget { ), RepeatButton( onLongPress: - analysisState.canGoBack ? () => _moveBackward(ref) : null, + broadcastGameState.canGoBack ? () => _moveBackward(ref) : null, child: BottomBarButton( key: const ValueKey('goto-previous'), - onTap: analysisState.canGoBack ? () => _moveBackward(ref) : null, + onTap: + broadcastGameState.canGoBack ? () => _moveBackward(ref) : null, label: 'Previous', icon: CupertinoIcons.chevron_back, showTooltip: false, ), ), RepeatButton( - onLongPress: analysisState.canGoNext ? () => _moveForward(ref) : null, + onLongPress: + broadcastGameState.canGoNext ? () => _moveForward(ref) : null, child: BottomBarButton( key: const ValueKey('goto-next'), icon: CupertinoIcons.chevron_forward, label: context.l10n.next, - onTap: analysisState.canGoNext ? () => _moveForward(ref) : null, + onTap: + broadcastGameState.canGoNext ? () => _moveForward(ref) : null, showTooltip: false, ), ), From e887151ce5876847bbdabe8e4ae14ce21ad8aa44 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Thu, 21 Nov 2024 23:44:26 +0100 Subject: [PATCH 751/979] some tweaks --- .../model/broadcast/broadcast_game_controller.dart | 7 ++++++- .../model/broadcast/broadcast_round_controller.dart | 12 ++---------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/lib/src/model/broadcast/broadcast_game_controller.dart b/lib/src/model/broadcast/broadcast_game_controller.dart index aa4cdd6885..3bc5ae7f53 100644 --- a/lib/src/model/broadcast/broadcast_game_controller.dart +++ b/lib/src/model/broadcast/broadcast_game_controller.dart @@ -160,8 +160,13 @@ class BroadcastGameController extends _$BroadcastGameController isBroadcastMove: true, ); } else { + final currentNode = _root.nodeAt(path); state = AsyncData( - state.requireValue.copyWith(broadcastPath: newPath, root: _root.view), + state.requireValue.copyWith( + broadcastPath: newPath, + root: _root.view, + currentNode: AnalysisCurrentNode.fromNode(currentNode), + ), ); } } diff --git a/lib/src/model/broadcast/broadcast_round_controller.dart b/lib/src/model/broadcast/broadcast_round_controller.dart index f46c866fe8..66913e3ea6 100644 --- a/lib/src/model/broadcast/broadcast_round_controller.dart +++ b/lib/src/model/broadcast/broadcast_round_controller.dart @@ -116,25 +116,17 @@ class BroadcastRoundController extends _$BroadcastRoundController { // We check that the clocks for the broadcast game preview have been updated else we do nothing if (relayClocks.value == null) return; - final newClocks = { - Side.white: relayClocks(0).asDurationFromCentiSecondsOrNull(), - Side.black: relayClocks(1).asDurationFromCentiSecondsOrNull(), - }; - state = AsyncData( state.requireValue.update( broadcastGameId, (broadcastsGame) => broadcastsGame.copyWith( - updatedClockAt: (newClocks[broadcastsGame.sideToMove] == null) - ? broadcastsGame.updatedClockAt - : DateTime.now(), players: IMap( { Side.white: broadcastsGame.players[Side.white]!.copyWith( - clock: newClocks[Side.white], + clock: relayClocks(0).asDurationFromCentiSecondsOrNull(), ), Side.black: broadcastsGame.players[Side.black]!.copyWith( - clock: newClocks[Side.black], + clock: relayClocks(1).asDurationFromCentiSecondsOrNull(), ), }, ), From f93d164eaa3cc7212bd0decf2b0d6bf6ae1a168b Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 22 Nov 2024 09:26:03 +0100 Subject: [PATCH 752/979] Fix analysis orientation --- lib/src/model/analysis/analysis_controller.dart | 9 +++------ lib/src/model/game/game_controller.dart | 1 + lib/src/view/board_editor/board_editor_screen.dart | 2 +- .../offline_correspondence_game_screen.dart | 2 +- lib/src/view/game/archived_game_screen.dart | 1 + lib/src/view/game/game_list_tile.dart | 5 ++++- lib/src/view/game/game_result_dialog.dart | 2 +- lib/src/view/puzzle/puzzle_screen.dart | 2 +- lib/src/view/puzzle/streak_screen.dart | 2 +- lib/src/view/study/study_bottom_bar.dart | 2 +- lib/src/view/tools/load_position_screen.dart | 4 ++-- lib/src/view/tools/tools_tab_screen.dart | 4 ++-- test/view/analysis/analysis_screen_test.dart | 6 +++--- .../opening_explorer/opening_explorer_screen_test.dart | 2 +- 14 files changed, 23 insertions(+), 21 deletions(-) diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index c9db2303ae..4dd4dd4cc5 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -32,7 +32,6 @@ final _dateFormat = DateFormat('yyyy.MM.dd'); typedef StandaloneAnalysis = ({ String pgn, Variant variant, - Side orientation, bool isComputerAnalysisAllowed, }); @@ -42,6 +41,7 @@ class AnalysisOptions with _$AnalysisOptions { @Assert('standalone != null || gameId != null') const factory AnalysisOptions({ + required Side orientation, StandaloneAnalysis? standalone, GameId? gameId, int? initialMoveCursor, @@ -55,7 +55,6 @@ class AnalysisController extends _$AnalysisController implements PgnTreeNotifier { late final Root _root; late final Variant _variant; - late final Side _orientation; final _engineEvalDebounce = Debouncer(const Duration(milliseconds: 150)); @@ -75,14 +74,12 @@ class AnalysisController extends _$AnalysisController final game = await ref.watch(archivedGameProvider(id: options.gameId!).future); _variant = game.meta.variant; - _orientation = game.youAre ?? Side.white; pgn = game.makePgn(); opening = game.data.opening; serverAnalysis = game.serverAnalysis; division = game.meta.division; } else { _variant = options.standalone!.variant; - _orientation = options.standalone!.orientation; pgn = options.standalone!.pgn; opening = null; serverAnalysis = null; @@ -164,7 +161,7 @@ class AnalysisController extends _$AnalysisController pgnHeaders: pgnHeaders, pgnRootComments: rootComments, lastMove: lastMove, - pov: _orientation, + pov: options.orientation, contextOpening: opening, isComputerAnalysisAllowed: isComputerAnalysisAllowed, isComputerAnalysisEnabled: prefs.enableComputerAnalysis, @@ -425,7 +422,7 @@ class AnalysisController extends _$AnalysisController Future requestServerAnalysis() { if (state.requireValue.canRequestServerAnalysis) { final service = ref.read(serverAnalysisServiceProvider); - return service.requestAnalysis(options.gameId!, _orientation); + return service.requestAnalysis(options.gameId!, options.orientation); } return Future.error('Cannot request server analysis'); } diff --git a/lib/src/model/game/game_controller.dart b/lib/src/model/game/game_controller.dart index e663d77b04..568782422f 100644 --- a/lib/src/model/game/game_controller.dart +++ b/lib/src/model/game/game_controller.dart @@ -1167,6 +1167,7 @@ class GameState with _$GameState { String get analysisPgn => game.makePgn(); AnalysisOptions get analysisOptions => AnalysisOptions( + orientation: game.youAre ?? Side.white, initialMoveCursor: stepCursor, gameId: gameFullId.gameId, ); diff --git a/lib/src/view/board_editor/board_editor_screen.dart b/lib/src/view/board_editor/board_editor_screen.dart index 45ee49e2b1..e5a66bd187 100644 --- a/lib/src/view/board_editor/board_editor_screen.dart +++ b/lib/src/view/board_editor/board_editor_screen.dart @@ -336,11 +336,11 @@ class _BottomBar extends ConsumerWidget { rootNavigator: true, builder: (context) => AnalysisScreen( options: AnalysisOptions( + orientation: editorState.orientation, standalone: ( pgn: editorState.pgn!, isComputerAnalysisAllowed: true, variant: Variant.fromPosition, - orientation: editorState.orientation, ), ), ), diff --git a/lib/src/view/correspondence/offline_correspondence_game_screen.dart b/lib/src/view/correspondence/offline_correspondence_game_screen.dart index 931ad9e5d5..f8a6b18697 100644 --- a/lib/src/view/correspondence/offline_correspondence_game_screen.dart +++ b/lib/src/view/correspondence/offline_correspondence_game_screen.dart @@ -260,11 +260,11 @@ class _BodyState extends ConsumerState<_Body> { context, builder: (_) => AnalysisScreen( options: AnalysisOptions( + orientation: game.youAre, standalone: ( pgn: game.makePgn(), isComputerAnalysisAllowed: false, variant: game.variant, - orientation: game.youAre, ), initialMoveCursor: stepCursor, ), diff --git a/lib/src/view/game/archived_game_screen.dart b/lib/src/view/game/archived_game_screen.dart index ec5226fe83..c7ef8b358d 100644 --- a/lib/src/view/game/archived_game_screen.dart +++ b/lib/src/view/game/archived_game_screen.dart @@ -380,6 +380,7 @@ class _BottomBar extends ConsumerWidget { context, builder: (context) => AnalysisScreen( options: AnalysisOptions( + orientation: orientation, gameId: gameData.id, initialMoveCursor: cursor, ), diff --git a/lib/src/view/game/game_list_tile.dart b/lib/src/view/game/game_list_tile.dart index 1f7417d3db..3901f67c2b 100644 --- a/lib/src/view/game/game_list_tile.dart +++ b/lib/src/view/game/game_list_tile.dart @@ -226,7 +226,10 @@ class _ContextMenu extends ConsumerWidget { pushPlatformRoute( context, builder: (context) => AnalysisScreen( - options: AnalysisOptions(gameId: game.id), + options: AnalysisOptions( + orientation: orientation, + gameId: game.id, + ), ), ); } diff --git a/lib/src/view/game/game_result_dialog.dart b/lib/src/view/game/game_result_dialog.dart index e4ebf84b08..a4e6522d53 100644 --- a/lib/src/view/game/game_result_dialog.dart +++ b/lib/src/view/game/game_result_dialog.dart @@ -261,11 +261,11 @@ class OverTheBoardGameResultDialog extends StatelessWidget { context, builder: (_) => AnalysisScreen( options: AnalysisOptions( + orientation: Side.white, standalone: ( pgn: game.makePgn(), isComputerAnalysisAllowed: true, variant: game.meta.variant, - orientation: Side.white, ), ), ), diff --git a/lib/src/view/puzzle/puzzle_screen.dart b/lib/src/view/puzzle/puzzle_screen.dart index 0885219d38..63bae946cd 100644 --- a/lib/src/view/puzzle/puzzle_screen.dart +++ b/lib/src/view/puzzle/puzzle_screen.dart @@ -485,11 +485,11 @@ class _BottomBar extends ConsumerWidget { context, builder: (context) => AnalysisScreen( options: AnalysisOptions( + orientation: puzzleState.pov, standalone: ( pgn: ref.read(ctrlProvider.notifier).makePgn(), isComputerAnalysisAllowed: true, variant: Variant.standard, - orientation: puzzleState.pov, ), initialMoveCursor: 0, ), diff --git a/lib/src/view/puzzle/streak_screen.dart b/lib/src/view/puzzle/streak_screen.dart index cd38aa97fb..7382bee36c 100644 --- a/lib/src/view/puzzle/streak_screen.dart +++ b/lib/src/view/puzzle/streak_screen.dart @@ -292,11 +292,11 @@ class _BottomBar extends ConsumerWidget { context, builder: (context) => AnalysisScreen( options: AnalysisOptions( + orientation: puzzleState.pov, standalone: ( pgn: ref.read(ctrlProvider.notifier).makePgn(), isComputerAnalysisAllowed: true, variant: Variant.standard, - orientation: puzzleState.pov, ), initialMoveCursor: 0, ), diff --git a/lib/src/view/study/study_bottom_bar.dart b/lib/src/view/study/study_bottom_bar.dart index 104f5c29fd..2c8d4c7a12 100644 --- a/lib/src/view/study/study_bottom_bar.dart +++ b/lib/src/view/study/study_bottom_bar.dart @@ -166,11 +166,11 @@ class _GamebookBottomBar extends ConsumerWidget { rootNavigator: true, builder: (context) => AnalysisScreen( options: AnalysisOptions( + orientation: state.pov, standalone: ( pgn: state.pgn, isComputerAnalysisAllowed: true, variant: state.variant, - orientation: state.pov, ), ), ), diff --git a/lib/src/view/tools/load_position_screen.dart b/lib/src/view/tools/load_position_screen.dart index e7655899c7..3c48bf6ad7 100644 --- a/lib/src/view/tools/load_position_screen.dart +++ b/lib/src/view/tools/load_position_screen.dart @@ -131,11 +131,11 @@ class _BodyState extends State<_Body> { return ( fen: pos.fen, options: AnalysisOptions( + orientation: Side.white, standalone: ( pgn: '[FEN "${pos.fen}"]', isComputerAnalysisAllowed: true, variant: Variant.standard, - orientation: Side.white, ), ) ); @@ -163,11 +163,11 @@ class _BodyState extends State<_Body> { return ( fen: lastPosition.fen, options: AnalysisOptions( + orientation: Side.white, standalone: ( pgn: textInput!, isComputerAnalysisAllowed: true, variant: rule != null ? Variant.fromRule(rule) : Variant.standard, - orientation: Side.white, ), initialMoveCursor: mainlineMoves.isEmpty ? 0 : 1, ) diff --git a/lib/src/view/tools/tools_tab_screen.dart b/lib/src/view/tools/tools_tab_screen.dart index 3834d28098..f00a896a22 100644 --- a/lib/src/view/tools/tools_tab_screen.dart +++ b/lib/src/view/tools/tools_tab_screen.dart @@ -149,11 +149,11 @@ class _Body extends ConsumerWidget { rootNavigator: true, builder: (context) => const AnalysisScreen( options: AnalysisOptions( + orientation: Side.white, standalone: ( pgn: '', isComputerAnalysisAllowed: true, variant: Variant.standard, - orientation: Side.white, ), ), ), @@ -168,11 +168,11 @@ class _Body extends ConsumerWidget { rootNavigator: true, builder: (context) => const OpeningExplorerScreen( options: AnalysisOptions( + orientation: Side.white, standalone: ( pgn: '', isComputerAnalysisAllowed: false, variant: Variant.standard, - orientation: Side.white, ), ), ), diff --git a/test/view/analysis/analysis_screen_test.dart b/test/view/analysis/analysis_screen_test.dart index 44bf19bc87..bf91a6b889 100644 --- a/test/view/analysis/analysis_screen_test.dart +++ b/test/view/analysis/analysis_screen_test.dart @@ -24,10 +24,10 @@ void main() { tester, home: AnalysisScreen( options: AnalysisOptions( + orientation: Side.white, standalone: ( pgn: sanMoves, isComputerAnalysisAllowed: false, - orientation: Side.white, variant: Variant.standard, ), ), @@ -60,11 +60,11 @@ void main() { tester, home: AnalysisScreen( options: AnalysisOptions( + orientation: Side.white, standalone: ( pgn: sanMoves, isComputerAnalysisAllowed: false, variant: Variant.standard, - orientation: Side.white, ), ), ), @@ -127,11 +127,11 @@ void main() { }, home: AnalysisScreen( options: AnalysisOptions( + orientation: Side.white, standalone: ( pgn: pgn, isComputerAnalysisAllowed: false, variant: Variant.standard, - orientation: Side.white, ), ), enableDrawingShapes: false, diff --git a/test/view/opening_explorer/opening_explorer_screen_test.dart b/test/view/opening_explorer/opening_explorer_screen_test.dart index 10a3b08a72..662440da3b 100644 --- a/test/view/opening_explorer/opening_explorer_screen_test.dart +++ b/test/view/opening_explorer/opening_explorer_screen_test.dart @@ -40,10 +40,10 @@ void main() { }); const options = AnalysisOptions( + orientation: Side.white, standalone: ( pgn: '', isComputerAnalysisAllowed: false, - orientation: Side.white, variant: Variant.standard, ), ); From 52f2f6a8b1c99f14815a82cca0422a5d3c4f24d8 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 22 Nov 2024 10:03:07 +0100 Subject: [PATCH 753/979] Don't make tabs dynamic --- .../model/analysis/analysis_controller.dart | 12 ++-- lib/src/view/analysis/analysis_layout.dart | 6 +- lib/src/view/analysis/analysis_screen.dart | 66 +++---------------- lib/src/view/analysis/analysis_settings.dart | 2 +- lib/src/view/analysis/server_analysis.dart | 29 ++++++++ 5 files changed, 47 insertions(+), 68 deletions(-) diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index 4dd4dd4cc5..d12a48cae1 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -790,23 +790,21 @@ class AnalysisState with _$AnalysisState { /// Whether the server analysis is available. bool get hasServerAnalysis => playersAnalysis != null; - bool get canShowGameSummary => - isComputerAnalysisEnabledAndAllowed && - (hasServerAnalysis || canRequestServerAnalysis); + bool get canShowGameSummary => hasServerAnalysis || canRequestServerAnalysis; /// Whether an evaluation can be available bool get hasAvailableEval => isEngineAvailable || - (isComputerAnalysisEnabledAndAllowed && + (isComputerAnalysisAllowedAndEnabled && acplChartData != null && acplChartData!.isNotEmpty); - bool get isComputerAnalysisEnabledAndAllowed => - isComputerAnalysisEnabled && isComputerAnalysisAllowed; + bool get isComputerAnalysisAllowedAndEnabled => + isComputerAnalysisAllowed && isComputerAnalysisEnabled; /// Whether the engine is allowed for this analysis and variant. bool get isEngineAllowed => - isComputerAnalysisEnabledAndAllowed && + isComputerAnalysisAllowedAndEnabled && engineSupportedVariants.contains(variant); /// Whether the engine is available for evaluation diff --git a/lib/src/view/analysis/analysis_layout.dart b/lib/src/view/analysis/analysis_layout.dart index 5ca457b409..bad95d9e73 100644 --- a/lib/src/view/analysis/analysis_layout.dart +++ b/lib/src/view/analysis/analysis_layout.dart @@ -33,11 +33,9 @@ enum AnalysisTab { case AnalysisTab.opening: return l10n.openingExplorer; case AnalysisTab.moves: - // TODO: Add l10n - return 'Moves'; + return l10n.movesPlayed; case AnalysisTab.summary: - // TODO: Add l10n - return 'Summary'; + return l10n.computerAnalysis; } } } diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index 129f27620c..44852fe1a0 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -47,16 +47,18 @@ class AnalysisScreen extends ConsumerStatefulWidget { } class _AnalysisScreenState extends ConsumerState - with TickerProviderStateMixin { - late List tabs; - late TabController _tabController; + with SingleTickerProviderStateMixin { + late final List tabs; + late final TabController _tabController; @override void initState() { super.initState(); + tabs = [ AnalysisTab.opening, AnalysisTab.moves, + if (widget.options.gameId != null) AnalysisTab.summary, ]; _tabController = TabController( @@ -64,46 +66,6 @@ class _AnalysisScreenState extends ConsumerState initialIndex: 1, length: tabs.length, ); - - ref.listenManual>( - analysisControllerProvider(widget.options), - (prev, state) { - final canPrevShowGameSummary = - prev?.valueOrNull?.canShowGameSummary == true; - final canShowGameSummary = - state.valueOrNull?.canShowGameSummary == true; - if (!canPrevShowGameSummary && canShowGameSummary) { - setState(() { - tabs = [ - AnalysisTab.opening, - AnalysisTab.moves, - AnalysisTab.summary, - ]; - final index = _tabController.index; - _tabController.dispose(); - _tabController = TabController( - vsync: this, - initialIndex: index, - length: tabs.length, - ); - }); - } else if (canPrevShowGameSummary && !canShowGameSummary) { - setState(() { - tabs = [ - AnalysisTab.opening, - AnalysisTab.moves, - ]; - final index = _tabController.index; - _tabController.dispose(); - _tabController = TabController( - vsync: this, - initialIndex: index == 2 ? 1 : index, - length: tabs.length, - ); - }); - } - }, - ); } @override @@ -152,7 +114,6 @@ class _AnalysisScreenState extends ConsumerState body: _Body( options: widget.options, controller: _tabController, - tabs: tabs, enableDrawingShapes: widget.enableDrawingShapes, ), ); @@ -201,12 +162,10 @@ class _Body extends ConsumerWidget { const _Body({ required this.options, required this.controller, - required this.tabs, required this.enableDrawingShapes, }); final TabController controller; - final List tabs; final AnalysisOptions options; final bool enableDrawingShapes; @@ -258,16 +217,11 @@ class _Body extends ConsumerWidget { ) : null, bottomBar: _BottomBar(options: options), - children: tabs.map((tab) { - switch (tab) { - case AnalysisTab.opening: - return OpeningExplorerView(options: options); - case AnalysisTab.moves: - return AnalysisTreeView(options); - case AnalysisTab.summary: - return ServerAnalysisSummary(options); - } - }).toList(), + children: [ + OpeningExplorerView(options: options), + AnalysisTreeView(options), + if (options.gameId != null) ServerAnalysisSummary(options), + ], ); } } diff --git a/lib/src/view/analysis/analysis_settings.dart b/lib/src/view/analysis/analysis_settings.dart index 9b03d4c943..02ef27597c 100644 --- a/lib/src/view/analysis/analysis_settings.dart +++ b/lib/src/view/analysis/analysis_settings.dart @@ -47,7 +47,7 @@ class AnalysisSettings extends ConsumerWidget { ), AnimatedCrossFade( duration: const Duration(milliseconds: 300), - crossFadeState: value.isComputerAnalysisEnabledAndAllowed + crossFadeState: value.isComputerAnalysisAllowedAndEnabled ? CrossFadeState.showSecond : CrossFadeState.showFirst, firstChild: const SizedBox.shrink(), diff --git a/lib/src/view/analysis/server_analysis.dart b/lib/src/view/analysis/server_analysis.dart index d943bddb25..ae54ce5de7 100644 --- a/lib/src/view/analysis/server_analysis.dart +++ b/lib/src/view/analysis/server_analysis.dart @@ -8,6 +8,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; import 'package:lichess_mobile/src/model/analysis/server_analysis_service.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; @@ -22,14 +23,42 @@ class ServerAnalysisSummary extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final analysisPrefs = ref.watch(analysisPreferencesProvider); final ctrlProvider = analysisControllerProvider(options); final playersAnalysis = ref.watch( ctrlProvider.select((value) => value.requireValue.playersAnalysis), ); + final canShowGameSummary = ref.watch( + ctrlProvider.select((value) => value.requireValue.canShowGameSummary), + ); final pgnHeaders = ref .watch(ctrlProvider.select((value) => value.requireValue.pgnHeaders)); final currentGameAnalysis = ref.watch(currentAnalysisProvider); + if (analysisPrefs.enableComputerAnalysis == false) { + return Center( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Column( + mainAxisSize: MainAxisSize.max, + children: [ + const Spacer(), + Text(context.l10n.computerAnalysisDisabled), + if (canShowGameSummary) + SecondaryButton( + onPressed: () { + ref.read(ctrlProvider.notifier).toggleComputerAnalysis(); + }, + semanticsLabel: context.l10n.enable, + child: Text(context.l10n.enable), + ), + const Spacer(), + ], + ), + ), + ); + } + return playersAnalysis != null ? ListView( children: [ From aea9b72c884bdb7713771e879ab67d080cd9f6e6 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 22 Nov 2024 10:15:30 +0100 Subject: [PATCH 754/979] Tweak analysis settings --- lib/src/view/analysis/analysis_settings.dart | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/src/view/analysis/analysis_settings.dart b/lib/src/view/analysis/analysis_settings.dart index 02ef27597c..46372bbe14 100644 --- a/lib/src/view/analysis/analysis_settings.dart +++ b/lib/src/view/analysis/analysis_settings.dart @@ -122,11 +122,9 @@ class AnalysisSettings extends ConsumerWidget { SwitchSettingTile( title: Text(context.l10n.bestMoveArrow), value: prefs.showBestMoveArrow, - onChanged: value.isEngineAvailable - ? (value) => ref - .read(analysisPreferencesProvider.notifier) - .toggleShowBestMoveArrow() - : null, + onChanged: (value) => ref + .read(analysisPreferencesProvider.notifier) + .toggleShowBestMoveArrow(), ), SwitchSettingTile( title: Text(context.l10n.evaluationGauge), From 3382baf7305fb6f745280ba19ebcd9af01879e7f Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 22 Nov 2024 10:31:11 +0100 Subject: [PATCH 755/979] Tweak study settings --- lib/src/view/analysis/analysis_settings.dart | 13 +++ lib/src/view/study/study_settings.dart | 116 ++++++++++--------- 2 files changed, 72 insertions(+), 57 deletions(-) diff --git a/lib/src/view/analysis/analysis_settings.dart b/lib/src/view/analysis/analysis_settings.dart index 46372bbe14..7571566adc 100644 --- a/lib/src/view/analysis/analysis_settings.dart +++ b/lib/src/view/analysis/analysis_settings.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; +import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_settings.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; @@ -21,6 +22,9 @@ class AnalysisSettings extends ConsumerWidget { final ctrlProvider = analysisControllerProvider(options); final prefs = ref.watch(analysisPreferencesProvider); final asyncState = ref.watch(ctrlProvider); + final isSoundEnabled = ref.watch( + generalPreferencesProvider.select((pref) => pref.isSoundEnabled), + ); switch (asyncState) { case AsyncData(:final value): @@ -150,6 +154,15 @@ class AnalysisSettings extends ConsumerWidget { ], ), ), + SwitchSettingTile( + title: Text(context.l10n.sound), + value: isSoundEnabled, + onChanged: (value) { + ref + .read(generalPreferencesProvider.notifier) + .toggleSoundEnabled(); + }, + ), ], ); case AsyncError(:final error): diff --git a/lib/src/view/study/study_settings.dart b/lib/src/view/study/study_settings.dart index ea8393242c..1064c5a962 100644 --- a/lib/src/view/study/study_settings.dart +++ b/lib/src/view/study/study_settings.dart @@ -37,48 +37,20 @@ class StudySettings extends ConsumerWidget { return BottomSheetScrollableContainer( children: [ - SwitchSettingTile( - title: Text(context.l10n.toggleLocalEvaluation), - value: analysisPrefs.enableLocalEvaluation, - onChanged: isComputerAnalysisAllowed - ? (_) { - ref.read(studyController.notifier).toggleLocalEvaluation(); - } - : null, - ), - PlatformListTile( - title: Text.rich( - TextSpan( - text: '${context.l10n.multipleLines}: ', - style: const TextStyle( - fontWeight: FontWeight.normal, - ), - children: [ - TextSpan( - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - ), - text: analysisPrefs.numEvalLines.toString(), - ), - ], - ), - ), - subtitle: NonLinearSlider( - value: analysisPrefs.numEvalLines, - values: const [0, 1, 2, 3], - onChangeEnd: isEngineAvailable - ? (value) => ref - .read(studyController.notifier) - .setNumEvalLines(value.toInt()) + if (isComputerAnalysisAllowed) ...[ + SwitchSettingTile( + title: Text(context.l10n.toggleLocalEvaluation), + value: analysisPrefs.enableLocalEvaluation, + onChanged: isComputerAnalysisAllowed + ? (_) { + ref.read(studyController.notifier).toggleLocalEvaluation(); + } : null, ), - ), - if (maxEngineCores > 1) PlatformListTile( title: Text.rich( TextSpan( - text: '${context.l10n.cpus}: ', + text: '${context.l10n.multipleLines}: ', style: const TextStyle( fontWeight: FontWeight.normal, ), @@ -88,30 +60,67 @@ class StudySettings extends ConsumerWidget { fontWeight: FontWeight.bold, fontSize: 18, ), - text: analysisPrefs.numEngineCores.toString(), + text: analysisPrefs.numEvalLines.toString(), ), ], ), ), subtitle: NonLinearSlider( - value: analysisPrefs.numEngineCores, - values: List.generate(maxEngineCores, (index) => index + 1), + value: analysisPrefs.numEvalLines, + values: const [0, 1, 2, 3], onChangeEnd: isEngineAvailable ? (value) => ref .read(studyController.notifier) - .setEngineCores(value.toInt()) + .setNumEvalLines(value.toInt()) : null, ), ), - SwitchSettingTile( - title: Text(context.l10n.bestMoveArrow), - value: analysisPrefs.showBestMoveArrow, - onChanged: isEngineAvailable - ? (value) => ref - .read(analysisPreferencesProvider.notifier) - .toggleShowBestMoveArrow() - : null, - ), + if (maxEngineCores > 1) + PlatformListTile( + title: Text.rich( + TextSpan( + text: '${context.l10n.cpus}: ', + style: const TextStyle( + fontWeight: FontWeight.normal, + ), + children: [ + TextSpan( + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + text: analysisPrefs.numEngineCores.toString(), + ), + ], + ), + ), + subtitle: NonLinearSlider( + value: analysisPrefs.numEngineCores, + values: List.generate(maxEngineCores, (index) => index + 1), + onChangeEnd: isEngineAvailable + ? (value) => ref + .read(studyController.notifier) + .setEngineCores(value.toInt()) + : null, + ), + ), + SwitchSettingTile( + title: Text(context.l10n.bestMoveArrow), + value: analysisPrefs.showBestMoveArrow, + onChanged: isEngineAvailable + ? (value) => ref + .read(analysisPreferencesProvider.notifier) + .toggleShowBestMoveArrow() + : null, + ), + SwitchSettingTile( + title: Text(context.l10n.evaluationGauge), + value: analysisPrefs.showEvaluationGauge, + onChanged: (value) => ref + .read(analysisPreferencesProvider.notifier) + .toggleShowEvaluationGauge(), + ), + ], SwitchSettingTile( title: Text(context.l10n.showVariationArrows), value: studyPrefs.showVariationArrows, @@ -119,13 +128,6 @@ class StudySettings extends ConsumerWidget { .read(studyPreferencesProvider.notifier) .toggleShowVariationArrows(), ), - SwitchSettingTile( - title: Text(context.l10n.evaluationGauge), - value: analysisPrefs.showEvaluationGauge, - onChanged: (value) => ref - .read(analysisPreferencesProvider.notifier) - .toggleShowEvaluationGauge(), - ), SwitchSettingTile( title: Text(context.l10n.toggleGlyphAnnotations), value: analysisPrefs.showAnnotations, From 76e0aab250b54befe2c2a162aa3df28c51b8a8d7 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 22 Nov 2024 10:49:22 +0100 Subject: [PATCH 756/979] Use AnalysisLayout in StudyScreen --- lib/src/view/analysis/analysis_layout.dart | 7 +- lib/src/view/study/study_screen.dart | 241 ++++++++------------- 2 files changed, 94 insertions(+), 154 deletions(-) diff --git a/lib/src/view/analysis/analysis_layout.dart b/lib/src/view/analysis/analysis_layout.dart index bad95d9e73..0caf83a0ad 100644 --- a/lib/src/view/analysis/analysis_layout.dart +++ b/lib/src/view/analysis/analysis_layout.dart @@ -103,6 +103,11 @@ class _AppBarAnalysisTabIndicatorState } /// Layout for the analysis and similar screens (study, broadcast, etc.). +/// +/// The layout is responsive and adapts to the screen size and orientation. +/// +/// The length of the [children] list must match the [tabController]'s +/// [TabController.length] and the length of the [AppBarAnalysisTabIndicator.tabs] class AnalysisLayout extends StatelessWidget { const AnalysisLayout({ required this.tabController, @@ -122,7 +127,7 @@ class AnalysisLayout extends StatelessWidget { /// The children of the tab view. /// - /// The length of this list must match the [controller]'s [TabController.length] + /// The length of this list must match the [tabController]'s [TabController.length] /// and the length of the [AppBarAnalysisTabIndicator.tabs] list. final List children; diff --git a/lib/src/view/study/study_screen.dart b/lib/src/view/study/study_screen.dart index 7a1b246d6a..419c974b64 100644 --- a/lib/src/view/study/study_screen.dart +++ b/lib/src/view/study/study_screen.dart @@ -15,7 +15,7 @@ import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/model/study/study_controller.dart'; import 'package:lichess_mobile/src/model/study/study_preferences.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; -import 'package:lichess_mobile/src/utils/screen.dart'; +import 'package:lichess_mobile/src/view/analysis/analysis_layout.dart'; import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; import 'package:lichess_mobile/src/view/engine/engine_lines.dart'; import 'package:lichess_mobile/src/view/study/study_bottom_bar.dart'; @@ -26,7 +26,6 @@ import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/pgn.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:logging/logging.dart'; @@ -168,7 +167,7 @@ class _StudyChaptersMenu extends ConsumerWidget { } } -class _Body extends ConsumerWidget { +class _Body extends ConsumerStatefulWidget { const _Body({ required this.id, }); @@ -176,155 +175,91 @@ class _Body extends ConsumerWidget { final StudyId id; @override - Widget build(BuildContext context, WidgetRef ref) { + ConsumerState<_Body> createState() => _BodyState(); +} + +class _BodyState extends ConsumerState<_Body> + with SingleTickerProviderStateMixin { + late final TabController _tabController; + + @override + void initState() { + super.initState(); + + _tabController = TabController( + vsync: this, + length: 1, + ); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { final gamebookActive = ref.watch( - studyControllerProvider(id) + studyControllerProvider(widget.id) .select((state) => state.requireValue.gamebookActive), ); + final engineGaugeParams = ref.watch( + studyControllerProvider(widget.id) + .select((state) => state.valueOrNull?.engineGaugeParams), + ); - return SafeArea( - child: Column( - children: [ - Expanded( - child: LayoutBuilder( - builder: (context, constraints) { - final defaultBoardSize = constraints.biggest.shortestSide; - final isTablet = isTabletOrLarger(context); - final remainingHeight = - constraints.maxHeight - defaultBoardSize; - final isSmallScreen = - remainingHeight < kSmallRemainingHeightLeftBoardThreshold; - final boardSize = isTablet || isSmallScreen - ? defaultBoardSize - kTabletBoardTableSidePadding * 2 - : defaultBoardSize; - - final landscape = constraints.biggest.aspectRatio > 1; - - final engineGaugeParams = ref.watch( - studyControllerProvider(id) - .select((state) => state.valueOrNull?.engineGaugeParams), - ); - - final currentNode = ref.watch( - studyControllerProvider(id) - .select((state) => state.requireValue.currentNode), - ); - - final engineLines = EngineLines( - clientEval: currentNode.eval, - isGameOver: currentNode.position?.isGameOver ?? false, - onTapMove: ref - .read( - studyControllerProvider(id).notifier, - ) - .onUserMove, - ); - - final showEvaluationGauge = ref.watch( - analysisPreferencesProvider - .select((value) => value.showEvaluationGauge), - ); - - final engineGauge = - showEvaluationGauge && engineGaugeParams != null - ? EngineGauge( - params: engineGaugeParams, - displayMode: landscape - ? EngineGaugeDisplayMode.vertical - : EngineGaugeDisplayMode.horizontal, - ) - : null; - - final bottomChild = - gamebookActive ? StudyGamebook(id) : StudyTreeView(id); - - return landscape - ? Row( - mainAxisSize: MainAxisSize.max, - children: [ - Padding( - padding: EdgeInsets.all( - isTablet ? kTabletBoardTableSidePadding : 0.0, - ), - child: Row( - children: [ - _StudyBoard( - id: id, - boardSize: boardSize, - isTablet: isTablet, - ), - if (engineGauge != null) ...[ - const SizedBox(width: 4.0), - engineGauge, - ], - ], - ), - ), - Flexible( - fit: FlexFit.loose, - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - if (engineGaugeParams != null) engineLines, - Expanded( - child: PlatformCard( - clipBehavior: Clip.hardEdge, - borderRadius: const BorderRadius.all( - Radius.circular(4.0), - ), - margin: const EdgeInsets.all( - kTabletBoardTableSidePadding, - ), - semanticContainer: false, - child: bottomChild, - ), - ), - ], - ), - ), - ], - ) - : Column( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Padding( - padding: EdgeInsets.all( - isTablet ? kTabletBoardTableSidePadding : 0.0, - ), - child: Column( - children: [ - if (engineGauge != null) ...[ - engineGauge, - engineLines, - ], - _StudyBoard( - id: id, - boardSize: boardSize, - isTablet: isTablet, - ), - ], - ), - ), - Expanded( - child: Padding( - padding: isTablet - ? const EdgeInsets.symmetric( - horizontal: kTabletBoardTableSidePadding, - ) - : EdgeInsets.zero, - child: bottomChild, - ), - ), - ], - ); - }, - ), - ), - StudyBottomBar(id: id), - ], + final currentNode = ref.watch( + studyControllerProvider(widget.id) + .select((state) => state.requireValue.currentNode), + ); + + final engineLines = EngineLines( + clientEval: currentNode.eval, + isGameOver: currentNode.position?.isGameOver ?? false, + onTapMove: ref + .read( + studyControllerProvider(widget.id).notifier, + ) + .onUserMove, + ); + + final showEvaluationGauge = ref.watch( + analysisPreferencesProvider.select((value) => value.showEvaluationGauge), + ); + + final bottomChild = + gamebookActive ? StudyGamebook(widget.id) : StudyTreeView(widget.id); + + return AnalysisLayout( + tabController: _tabController, + boardBuilder: (context, boardSize, borderRadius) => _StudyBoard( + id: widget.id, + boardSize: boardSize, + borderRadius: borderRadius, ), + engineGaugeBuilder: showEvaluationGauge && engineGaugeParams != null + ? (context, orientation) { + return orientation == Orientation.portrait + ? EngineGauge( + displayMode: EngineGaugeDisplayMode.horizontal, + params: engineGaugeParams, + ) + : Container( + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4.0), + ), + child: EngineGauge( + displayMode: EngineGaugeDisplayMode.vertical, + params: engineGaugeParams, + ), + ); + } + : null, + engineLines: engineLines, + bottomBar: StudyBottomBar(id: widget.id), + children: [bottomChild], ); } } @@ -351,14 +286,14 @@ class _StudyBoard extends ConsumerStatefulWidget { const _StudyBoard({ required this.id, required this.boardSize, - required this.isTablet, + this.borderRadius, }); final StudyId id; final double boardSize; - final bool isTablet; + final BorderRadiusGeometry? borderRadius; @override ConsumerState<_StudyBoard> createState() => _StudyBoardState(); @@ -440,10 +375,10 @@ class _StudyBoardState extends ConsumerState<_StudyBoard> { return Chessboard( size: widget.boardSize, settings: boardPrefs.toBoardSettings().copyWith( - borderRadius: widget.isTablet - ? const BorderRadius.all(Radius.circular(4.0)) - : BorderRadius.zero, - boxShadow: widget.isTablet ? boardShadows : const [], + borderRadius: widget.borderRadius, + boxShadow: widget.borderRadius != null + ? boardShadows + : const [], drawShape: DrawShapeOptions( enable: true, onCompleteShape: _onCompleteShape, From 25cdd4533cad6a1a6c50a3470e62f4b3bed783ab Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 22 Nov 2024 11:05:24 +0100 Subject: [PATCH 757/979] Fix tablet analysis layout --- lib/src/view/analysis/analysis_layout.dart | 6 ++-- lib/src/view/analysis/analysis_screen.dart | 8 ++--- .../view/analysis/opening_explorer_view.dart | 10 +----- lib/src/view/engine/engine_lines.dart | 1 + lib/src/view/study/study_screen.dart | 36 +++++++++++-------- 5 files changed, 31 insertions(+), 30 deletions(-) diff --git a/lib/src/view/analysis/analysis_layout.dart b/lib/src/view/analysis/analysis_layout.dart index 0caf83a0ad..c8ead71161 100644 --- a/lib/src/view/analysis/analysis_layout.dart +++ b/lib/src/view/analysis/analysis_layout.dart @@ -193,8 +193,10 @@ class AnalysisLayout extends StatelessWidget { children: [ if (engineLines != null) Padding( - padding: const EdgeInsets.all( - kTabletBoardTableSidePadding, + padding: const EdgeInsets.only( + top: kTabletBoardTableSidePadding, + left: kTabletBoardTableSidePadding, + right: kTabletBoardTableSidePadding, ), child: engineLines, ), diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index 44852fe1a0..45d3ae1f2f 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -171,9 +171,9 @@ class _Body extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final showEvaluationGauge = ref.watch( - analysisPreferencesProvider.select((value) => value.showEvaluationGauge), - ); + final analysisPrefs = ref.watch(analysisPreferencesProvider); + final showEvaluationGauge = analysisPrefs.showEvaluationGauge; + final numEvalLines = analysisPrefs.numEvalLines; final ctrlProvider = analysisControllerProvider(options); final analysisState = ref.watch(ctrlProvider).requireValue; @@ -209,7 +209,7 @@ class _Body extends ConsumerWidget { ); } : null, - engineLines: isEngineAvailable + engineLines: isEngineAvailable && numEvalLines > 0 ? EngineLines( onTapMove: ref.read(ctrlProvider.notifier).onUserMove, clientEval: currentNode.eval, diff --git a/lib/src/view/analysis/opening_explorer_view.dart b/lib/src/view/analysis/opening_explorer_view.dart index c631778fda..9317fcbff2 100644 --- a/lib/src/view/analysis/opening_explorer_view.dart +++ b/lib/src/view/analysis/opening_explorer_view.dart @@ -231,21 +231,13 @@ class _OpeningExplorerView extends StatelessWidget { @override Widget build(BuildContext context) { - final isTablet = isTabletOrLarger(context); final loadingOverlay = Positioned.fill( child: IgnorePointer(ignoring: !isLoading), ); return Stack( children: [ - ListView( - padding: isTablet - ? const EdgeInsets.symmetric( - horizontal: kTabletBoardTableSidePadding, - ) - : EdgeInsets.zero, - children: children, - ), + ListView(children: children), loadingOverlay, ], ); diff --git a/lib/src/view/engine/engine_lines.dart b/lib/src/view/engine/engine_lines.dart index f20f21cf6b..96ffd7fcc1 100644 --- a/lib/src/view/engine/engine_lines.dart +++ b/lib/src/view/engine/engine_lines.dart @@ -55,6 +55,7 @@ class EngineLines extends ConsumerWidget { } return Column( + mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start, children: content, diff --git a/lib/src/view/study/study_screen.dart b/lib/src/view/study/study_screen.dart index 419c974b64..15dabbc644 100644 --- a/lib/src/view/study/study_screen.dart +++ b/lib/src/view/study/study_screen.dart @@ -208,25 +208,19 @@ class _BodyState extends ConsumerState<_Body> studyControllerProvider(widget.id) .select((state) => state.valueOrNull?.engineGaugeParams), ); + final isComputerAnalysisAllowed = ref.watch( + studyControllerProvider(widget.id) + .select((s) => s.requireValue.isComputerAnalysisAllowed), + ); final currentNode = ref.watch( studyControllerProvider(widget.id) .select((state) => state.requireValue.currentNode), ); - final engineLines = EngineLines( - clientEval: currentNode.eval, - isGameOver: currentNode.position?.isGameOver ?? false, - onTapMove: ref - .read( - studyControllerProvider(widget.id).notifier, - ) - .onUserMove, - ); - - final showEvaluationGauge = ref.watch( - analysisPreferencesProvider.select((value) => value.showEvaluationGauge), - ); + final analysisPrefs = ref.watch(analysisPreferencesProvider); + final showEvaluationGauge = analysisPrefs.showEvaluationGauge; + final numEvalLines = analysisPrefs.numEvalLines; final bottomChild = gamebookActive ? StudyGamebook(widget.id) : StudyTreeView(widget.id); @@ -238,7 +232,9 @@ class _BodyState extends ConsumerState<_Body> boardSize: boardSize, borderRadius: borderRadius, ), - engineGaugeBuilder: showEvaluationGauge && engineGaugeParams != null + engineGaugeBuilder: isComputerAnalysisAllowed && + showEvaluationGauge && + engineGaugeParams != null ? (context, orientation) { return orientation == Orientation.portrait ? EngineGauge( @@ -257,7 +253,17 @@ class _BodyState extends ConsumerState<_Body> ); } : null, - engineLines: engineLines, + engineLines: isComputerAnalysisAllowed && numEvalLines > 0 + ? EngineLines( + clientEval: currentNode.eval, + isGameOver: currentNode.position?.isGameOver ?? false, + onTapMove: ref + .read( + studyControllerProvider(widget.id).notifier, + ) + .onUserMove, + ) + : null, bottomBar: StudyBottomBar(id: widget.id), children: [bottomChild], ); From 920ad84af648ca98d7e2da5041f79099bb228b7b Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 22 Nov 2024 11:35:07 +0100 Subject: [PATCH 758/979] Fixes --- lib/src/view/analysis/analysis_layout.dart | 7 +- .../view/analysis/opening_explorer_view.dart | 2 - lib/src/view/study/study_screen.dart | 121 +++++++----------- 3 files changed, 54 insertions(+), 76 deletions(-) diff --git a/lib/src/view/analysis/analysis_layout.dart b/lib/src/view/analysis/analysis_layout.dart index c8ead71161..80f76e7595 100644 --- a/lib/src/view/analysis/analysis_layout.dart +++ b/lib/src/view/analysis/analysis_layout.dart @@ -106,11 +106,14 @@ class _AppBarAnalysisTabIndicatorState /// /// The layout is responsive and adapts to the screen size and orientation. /// +/// It includes a [TabBarView] with the [children] widgets. If a [TabController] +/// is not provided, then there must be a [DefaultTabController] ancestor. +/// /// The length of the [children] list must match the [tabController]'s /// [TabController.length] and the length of the [AppBarAnalysisTabIndicator.tabs] class AnalysisLayout extends StatelessWidget { const AnalysisLayout({ - required this.tabController, + this.tabController, required this.boardBuilder, required this.children, this.engineGaugeBuilder, @@ -120,7 +123,7 @@ class AnalysisLayout extends StatelessWidget { }); /// The tab controller for the tab view. - final TabController tabController; + final TabController? tabController; /// The builder for the board widget. final BoardBuilder boardBuilder; diff --git a/lib/src/view/analysis/opening_explorer_view.dart b/lib/src/view/analysis/opening_explorer_view.dart index 9317fcbff2..df320b632a 100644 --- a/lib/src/view/analysis/opening_explorer_view.dart +++ b/lib/src/view/analysis/opening_explorer_view.dart @@ -1,13 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_preferences.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_repository.dart'; import 'package:lichess_mobile/src/network/connectivity.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; -import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_widgets.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; diff --git a/lib/src/view/study/study_screen.dart b/lib/src/view/study/study_screen.dart index 15dabbc644..d7cad0abbf 100644 --- a/lib/src/view/study/study_screen.dart +++ b/lib/src/view/study/study_screen.dart @@ -167,7 +167,7 @@ class _StudyChaptersMenu extends ConsumerWidget { } } -class _Body extends ConsumerStatefulWidget { +class _Body extends ConsumerWidget { const _Body({ required this.id, }); @@ -175,46 +175,22 @@ class _Body extends ConsumerStatefulWidget { final StudyId id; @override - ConsumerState<_Body> createState() => _BodyState(); -} - -class _BodyState extends ConsumerState<_Body> - with SingleTickerProviderStateMixin { - late final TabController _tabController; - - @override - void initState() { - super.initState(); - - _tabController = TabController( - vsync: this, - length: 1, - ); - } - - @override - void dispose() { - _tabController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { final gamebookActive = ref.watch( - studyControllerProvider(widget.id) + studyControllerProvider(id) .select((state) => state.requireValue.gamebookActive), ); final engineGaugeParams = ref.watch( - studyControllerProvider(widget.id) + studyControllerProvider(id) .select((state) => state.valueOrNull?.engineGaugeParams), ); final isComputerAnalysisAllowed = ref.watch( - studyControllerProvider(widget.id) + studyControllerProvider(id) .select((s) => s.requireValue.isComputerAnalysisAllowed), ); final currentNode = ref.watch( - studyControllerProvider(widget.id) + studyControllerProvider(id) .select((state) => state.requireValue.currentNode), ); @@ -222,50 +198,51 @@ class _BodyState extends ConsumerState<_Body> final showEvaluationGauge = analysisPrefs.showEvaluationGauge; final numEvalLines = analysisPrefs.numEvalLines; - final bottomChild = - gamebookActive ? StudyGamebook(widget.id) : StudyTreeView(widget.id); + final bottomChild = gamebookActive ? StudyGamebook(id) : StudyTreeView(id); - return AnalysisLayout( - tabController: _tabController, - boardBuilder: (context, boardSize, borderRadius) => _StudyBoard( - id: widget.id, - boardSize: boardSize, - borderRadius: borderRadius, - ), - engineGaugeBuilder: isComputerAnalysisAllowed && - showEvaluationGauge && - engineGaugeParams != null - ? (context, orientation) { - return orientation == Orientation.portrait - ? EngineGauge( - displayMode: EngineGaugeDisplayMode.horizontal, - params: engineGaugeParams, - ) - : Container( - clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4.0), - ), - child: EngineGauge( - displayMode: EngineGaugeDisplayMode.vertical, + return DefaultTabController( + length: 1, + child: AnalysisLayout( + boardBuilder: (context, boardSize, borderRadius) => _StudyBoard( + id: id, + boardSize: boardSize, + borderRadius: borderRadius, + ), + engineGaugeBuilder: isComputerAnalysisAllowed && + showEvaluationGauge && + engineGaugeParams != null + ? (context, orientation) { + return orientation == Orientation.portrait + ? EngineGauge( + displayMode: EngineGaugeDisplayMode.horizontal, params: engineGaugeParams, - ), - ); - } - : null, - engineLines: isComputerAnalysisAllowed && numEvalLines > 0 - ? EngineLines( - clientEval: currentNode.eval, - isGameOver: currentNode.position?.isGameOver ?? false, - onTapMove: ref - .read( - studyControllerProvider(widget.id).notifier, - ) - .onUserMove, - ) - : null, - bottomBar: StudyBottomBar(id: widget.id), - children: [bottomChild], + ) + : Container( + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4.0), + ), + child: EngineGauge( + displayMode: EngineGaugeDisplayMode.vertical, + params: engineGaugeParams, + ), + ); + } + : null, + engineLines: isComputerAnalysisAllowed && numEvalLines > 0 + ? EngineLines( + clientEval: currentNode.eval, + isGameOver: currentNode.position?.isGameOver ?? false, + onTapMove: ref + .read( + studyControllerProvider(id).notifier, + ) + .onUserMove, + ) + : null, + bottomBar: StudyBottomBar(id: id), + children: [bottomChild], + ), ); } } From 2e5dee402aeee21259129c9120c3b1640429a723 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 22 Nov 2024 11:38:04 +0100 Subject: [PATCH 759/979] Don't make final --- lib/src/model/analysis/analysis_controller.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index d12a48cae1..91b36e52de 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -53,8 +53,8 @@ class AnalysisOptions with _$AnalysisOptions { @riverpod class AnalysisController extends _$AnalysisController implements PgnTreeNotifier { - late final Root _root; - late final Variant _variant; + late Root _root; + late Variant _variant; final _engineEvalDebounce = Debouncer(const Duration(milliseconds: 150)); From c3a96fe706f91ba08b1ae4d5d5d7bb1c384b965f Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 22 Nov 2024 11:52:25 +0100 Subject: [PATCH 760/979] Archived game bottom bar fixes --- lib/src/view/game/archived_game_screen.dart | 32 +++++++++------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/lib/src/view/game/archived_game_screen.dart b/lib/src/view/game/archived_game_screen.dart index c7ef8b358d..88b720a6f1 100644 --- a/lib/src/view/game/archived_game_screen.dart +++ b/lib/src/view/game/archived_game_screen.dart @@ -354,26 +354,22 @@ class _BottomBar extends ConsumerWidget { onTap: showGameMenu, icon: Icons.menu, ), - gameCursor.when( - data: (data) { - return BottomBarButton( - label: context.l10n.mobileShowResult, - icon: Icons.info_outline, - onTap: () { - showAdaptiveDialog( - context: context, - builder: (context) => ArchivedGameResultDialog(game: data.$1), - barrierDismissible: true, - ); - }, - ); - }, - loading: () => const SizedBox.shrink(), - error: (_, __) => const SizedBox.shrink(), - ), + if (gameCursor.hasValue) + BottomBarButton( + label: context.l10n.mobileShowResult, + icon: Icons.info_outline, + onTap: () { + showAdaptiveDialog( + context: context, + builder: (context) => + ArchivedGameResultDialog(game: gameCursor.requireValue.$1), + barrierDismissible: true, + ); + }, + ), BottomBarButton( label: context.l10n.gameAnalysis, - onTap: ref.read(gameCursorProvider(gameData.id)).hasValue + onTap: gameCursor.hasValue ? () { final cursor = gameCursor.requireValue.$2; pushPlatformRoute( From befa759c73b8fad4ed5289399ff9e59a10699173 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 22 Nov 2024 11:58:51 +0100 Subject: [PATCH 761/979] Pgn display fixes --- lib/src/widgets/pgn.dart | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/lib/src/widgets/pgn.dart b/lib/src/widgets/pgn.dart index 8781cf9bc0..0dff2207db 100644 --- a/lib/src/widgets/pgn.dart +++ b/lib/src/widgets/pgn.dart @@ -256,7 +256,8 @@ typedef _PgnTreeViewParams = ({ PgnTreeNotifier notifier, }); -IList _computerPrefAwareChildren( +/// Filter node children when computer analysis is disabled +IList _filteredChildren( ViewNode node, bool withComputerAnalysis, ) { @@ -278,8 +279,7 @@ bool _displaySideLineAsInline(ViewBranch node, [int depth = 0]) { /// Returns whether this node has a sideline that should not be displayed inline. bool _hasNonInlineSideLine(ViewNode node, _PgnTreeViewParams params) { - final children = - _computerPrefAwareChildren(node, params.withComputerAnalysis); + final children = _filteredChildren(node, params.withComputerAnalysis); return children.length > 2 || (children.length == 2 && !_displaySideLineAsInline(children[1])); } @@ -507,7 +507,9 @@ List _buildInlineSideLine({ node, lineInfo: ( type: _LineType.inlineSideline, - startLine: i == 0 || sidelineNodes[i - 1].hasTextComment, + startLine: i == 0 || + (params.shouldShowComments && + sidelineNodes[i - 1].hasTextComment), pathToLine: initialPath, ), pathToNode: pathToNode, @@ -628,7 +630,7 @@ class _SideLinePart extends ConsumerWidget { node.children.first, lineInfo: ( type: _LineType.sideline, - startLine: node.hasTextComment, + startLine: params.shouldShowComments && node.hasTextComment, pathToLine: initialPath, ), pathToNode: path, @@ -693,13 +695,12 @@ class _MainLinePart extends ConsumerWidget { TextSpan( children: nodes .takeWhile( - (node) => - _computerPrefAwareChildren(node, params.withComputerAnalysis) - .isNotEmpty, + (node) => _filteredChildren(node, params.withComputerAnalysis) + .isNotEmpty, ) .mapIndexed( (i, node) { - final children = _computerPrefAwareChildren( + final children = _filteredChildren( node, params.withComputerAnalysis, ); From b69c79fa55c852942a5085378d923c09afb5b245 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 22 Nov 2024 12:25:08 +0100 Subject: [PATCH 762/979] Bump version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index f13295d426..f1d1f5138e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: lichess_mobile description: Lichess mobile app V2 publish_to: "none" -version: 0.12.5+001205 # See README.md for details about versioning +version: 0.13.0+001300 # See README.md for details about versioning environment: sdk: ">=3.5.0 <4.0.0" From 67b324f37e35cd784adca5f78cd8b47513751328 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 22 Nov 2024 12:25:52 +0100 Subject: [PATCH 763/979] Upgrade dependencies --- pubspec.lock | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index f0c96ddc96..5dfd5490ff 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -410,10 +410,10 @@ packages: dependency: transitive description: name: equatable - sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "2.0.7" fake_async: dependency: "direct dev" description: @@ -522,10 +522,10 @@ packages: dependency: "direct main" description: name: fl_chart - sha256: "94307bef3a324a0d329d3ab77b2f0c6e5ed739185ffc029ed28c0f9b019ea7ef" + sha256: "74959b99b92b9eebeed1a4049426fd67c4abc3c5a0f4d12e2877097d6a11ae08" url: "https://pub.dev" source: hosted - version: "0.69.0" + version: "0.69.2" flutter: dependency: "direct main" description: flutter @@ -693,10 +693,10 @@ packages: dependency: "direct main" description: name: flutter_svg - sha256: "578bd8c508144fdaffd4f77b8ef2d8c523602275cd697cc3db284dbd762ef4ce" + sha256: "936d9c1c010d3e234d1672574636f3352b4941ca3decaddd3cafaeb9ad49c471" url: "https://pub.dev" source: hosted - version: "2.0.14" + version: "2.0.15" flutter_test: dependency: "direct dev" description: flutter @@ -847,10 +847,10 @@ packages: dependency: "direct dev" description: name: json_serializable - sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b + sha256: c2fcb3920cf2b6ae6845954186420fca40bc0a8abcc84903b7801f17d7050d7c url: "https://pub.dev" source: hosted - version: "6.8.0" + version: "6.9.0" leak_tracker: dependency: transitive description: @@ -1356,18 +1356,18 @@ packages: dependency: transitive description: name: sqflite_common - sha256: "4468b24876d673418a7b7147e5a08a715b4998a7ae69227acafaab762e0e5490" + sha256: "761b9740ecbd4d3e66b8916d784e581861fd3c3553eda85e167bc49fdb68f709" url: "https://pub.dev" source: hosted - version: "2.5.4+5" + version: "2.5.4+6" sqflite_common_ffi: dependency: "direct dev" description: name: sqflite_common_ffi - sha256: d316908f1537725427ff2827a5c5f3b2c1bc311caed985fe3c9b10939c9e11ca + sha256: b8ba78c1b72a9ee6c2323b06af95d43fd13e03d90c8369cb454fd7f629a72588 url: "https://pub.dev" source: hosted - version: "2.3.4" + version: "2.3.4+3" sqflite_darwin: dependency: transitive description: @@ -1581,10 +1581,10 @@ packages: dependency: transitive description: name: vector_graphics - sha256: "773c9522d66d523e1c7b25dfb95cc91c26a1e17b107039cfe147285e92de7878" + sha256: "27d5fefe86fb9aace4a9f8375b56b3c292b64d8c04510df230f849850d912cb7" url: "https://pub.dev" source: hosted - version: "1.1.14" + version: "1.1.15" vector_graphics_codec: dependency: transitive description: From 00e201bed752f78ef1a6d49f453ba4477151ae51 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 22 Nov 2024 14:06:43 +0100 Subject: [PATCH 764/979] Ensure to store post game data when accessing game --- lib/src/model/game/game_controller.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/model/game/game_controller.dart b/lib/src/model/game/game_controller.dart index 568782422f..958a42ca2c 100644 --- a/lib/src/model/game/game_controller.dart +++ b/lib/src/model/game/game_controller.dart @@ -109,6 +109,7 @@ class GameController extends _$GameController { _logger.warning('Could not get post game data: $e', e, s); return game; }); + await _storeGame(game); } _socketEventVersion = fullEvent.socketEventVersion; From 3517a445a748cccfef3faf4b9945a859d76a2106 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 22 Nov 2024 14:07:29 +0100 Subject: [PATCH 765/979] Bump version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index f1d1f5138e..ddb2f85809 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: lichess_mobile description: Lichess mobile app V2 publish_to: "none" -version: 0.13.0+001300 # See README.md for details about versioning +version: 0.13.1+001301 # See README.md for details about versioning environment: sdk: ">=3.5.0 <4.0.0" From 668c3de4634026c3311556a70cef4fb93b99fe1e Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 22 Nov 2024 15:04:58 +0100 Subject: [PATCH 766/979] Fix tree view --- lib/src/view/analysis/tree_view.dart | 9 ++++ lib/src/view/study/study_settings.dart | 7 --- lib/src/view/study/study_tree_view.dart | 4 ++ lib/src/widgets/pgn.dart | 64 +++++++++++-------------- pubspec.yaml | 2 +- 5 files changed, 42 insertions(+), 44 deletions(-) diff --git a/lib/src/view/analysis/tree_view.dart b/lib/src/view/analysis/tree_view.dart index 54c55d7bce..b28e5fac69 100644 --- a/lib/src/view/analysis/tree_view.dart +++ b/lib/src/view/analysis/tree_view.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; import 'package:lichess_mobile/src/model/analysis/opening_service.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; @@ -27,6 +28,10 @@ class AnalysisTreeView extends ConsumerWidget { final pgnRootComments = ref.watch( ctrlProvider.select((value) => value.requireValue.pgnRootComments), ); + final prefs = ref.watch(analysisPreferencesProvider); + // enable computer analysis takes effect here only if it's a lichess game + final enableComputerAnalysis = + !options.isLichessGameAnalysis || prefs.enableComputerAnalysis; return CustomScrollView( slivers: [ @@ -41,6 +46,10 @@ class AnalysisTreeView extends ConsumerWidget { currentPath: currentPath, pgnRootComments: pgnRootComments, notifier: ref.read(ctrlProvider.notifier), + shouldShowComputerVariations: enableComputerAnalysis, + shouldShowComments: enableComputerAnalysis && prefs.showPgnComments, + shouldShowAnnotations: + enableComputerAnalysis && prefs.showAnnotations, ), ), ], diff --git a/lib/src/view/study/study_settings.dart b/lib/src/view/study/study_settings.dart index 1064c5a962..d32239d040 100644 --- a/lib/src/view/study/study_settings.dart +++ b/lib/src/view/study/study_settings.dart @@ -135,13 +135,6 @@ class StudySettings extends ConsumerWidget { .read(analysisPreferencesProvider.notifier) .toggleAnnotations(), ), - SwitchSettingTile( - title: Text(context.l10n.mobileShowComments), - value: analysisPrefs.showPgnComments, - onChanged: (_) => ref - .read(analysisPreferencesProvider.notifier) - .togglePgnComments(), - ), SwitchSettingTile( title: Text(context.l10n.sound), value: isSoundEnabled, diff --git a/lib/src/view/study/study_tree_view.dart b/lib/src/view/study/study_tree_view.dart index 751b7c58af..031a2c599c 100644 --- a/lib/src/view/study/study_tree_view.dart +++ b/lib/src/view/study/study_tree_view.dart @@ -2,6 +2,7 @@ import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/node.dart'; import 'package:lichess_mobile/src/model/study/study_controller.dart'; @@ -36,6 +37,8 @@ class StudyTreeView extends ConsumerWidget { .select((value) => value.requireValue.pgnRootComments), ); + final analysisPrefs = ref.watch(analysisPreferencesProvider); + return CustomScrollView( slivers: [ SliverFillRemaining( @@ -49,6 +52,7 @@ class StudyTreeView extends ConsumerWidget { currentPath: currentPath, pgnRootComments: pgnRootComments, notifier: ref.read(studyControllerProvider(id).notifier), + shouldShowAnnotations: analysisPrefs.showAnnotations, ), ), ], diff --git a/lib/src/widgets/pgn.dart b/lib/src/widgets/pgn.dart index 0dff2207db..9ba8dafed8 100644 --- a/lib/src/widgets/pgn.dart +++ b/lib/src/widgets/pgn.dart @@ -6,7 +6,6 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/account/account_preferences.dart'; -import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; import 'package:lichess_mobile/src/model/common/node.dart'; import 'package:lichess_mobile/src/model/common/uci.dart'; import 'package:lichess_mobile/src/styles/lichess_colors.dart'; @@ -113,6 +112,9 @@ class DebouncedPgnTreeView extends ConsumerStatefulWidget { required this.currentPath, required this.pgnRootComments, required this.notifier, + this.shouldShowComputerVariations = true, + this.shouldShowAnnotations = true, + this.shouldShowComments = true, }); /// Root of the PGN tree to display @@ -127,6 +129,17 @@ class DebouncedPgnTreeView extends ConsumerStatefulWidget { /// Callbacks for when the user interacts with the tree view, e.g. selecting a different move or collapsing variations final PgnTreeNotifier notifier; + /// Whether to show analysis variations. + /// + /// Only applied to lichess game analysis. + final bool shouldShowComputerVariations; + + /// Whether to show NAG annotations like '!' and '??'. + final bool shouldShowAnnotations; + + /// Whether to show comments associated with the moves. + final bool shouldShowComments; + @override ConsumerState createState() => _DebouncedPgnTreeViewState(); @@ -195,32 +208,13 @@ class _DebouncedPgnTreeViewState extends ConsumerState { // using the fast replay buttons. @override Widget build(BuildContext context) { - final withComputerAnalysis = ref.watch( - analysisPreferencesProvider - .select((value) => value.enableComputerAnalysis), - ); - - final shouldShowComments = withComputerAnalysis && - ref.watch( - analysisPreferencesProvider.select( - (value) => value.showPgnComments, - ), - ); - - final shouldShowAnnotations = withComputerAnalysis && - ref.watch( - analysisPreferencesProvider.select( - (value) => value.showAnnotations, - ), - ); - return _PgnTreeView( root: widget.root, rootComments: widget.pgnRootComments, params: ( - withComputerAnalysis: withComputerAnalysis, - shouldShowAnnotations: shouldShowAnnotations, - shouldShowComments: shouldShowComments, + shouldShowComputerVariations: widget.shouldShowComputerVariations, + shouldShowAnnotations: widget.shouldShowAnnotations, + shouldShowComments: widget.shouldShowComments, currentMoveKey: currentMoveKey, pathToCurrentMove: pathToCurrentMove, notifier: widget.notifier, @@ -237,10 +231,8 @@ typedef _PgnTreeViewParams = ({ /// Path to the currently selected move in the tree. UciPath pathToCurrentMove, - /// Whether to show NAG, comments, and analysis variations. - /// - /// Takes precedence over [shouldShowAnnotations], and [shouldShowComments], - bool withComputerAnalysis, + /// Whether to show analysis variations. + bool shouldShowComputerVariations, /// Whether to show NAG annotations like '!' and '??'. bool shouldShowAnnotations, @@ -259,10 +251,10 @@ typedef _PgnTreeViewParams = ({ /// Filter node children when computer analysis is disabled IList _filteredChildren( ViewNode node, - bool withComputerAnalysis, + bool shouldShowComputerVariations, ) { return node.children - .where((c) => withComputerAnalysis || !c.isComputerVariation) + .where((c) => shouldShowComputerVariations || !c.isComputerVariation) .toIList(); } @@ -279,7 +271,7 @@ bool _displaySideLineAsInline(ViewBranch node, [int depth = 0]) { /// Returns whether this node has a sideline that should not be displayed inline. bool _hasNonInlineSideLine(ViewNode node, _PgnTreeViewParams params) { - final children = _filteredChildren(node, params.withComputerAnalysis); + final children = _filteredChildren(node, params.shouldShowComputerVariations); return children.length > 2 || (children.length == 2 && !_displaySideLineAsInline(children[1])); } @@ -429,8 +421,8 @@ class _PgnTreeViewState extends State<_PgnTreeView> { super.didUpdateWidget(oldWidget); _updateLines( fullRebuild: oldWidget.root != widget.root || - oldWidget.params.withComputerAnalysis != - widget.params.withComputerAnalysis || + oldWidget.params.shouldShowComputerVariations != + widget.params.shouldShowComputerVariations || oldWidget.params.shouldShowComments != widget.params.shouldShowComments || oldWidget.params.shouldShowAnnotations != @@ -695,14 +687,15 @@ class _MainLinePart extends ConsumerWidget { TextSpan( children: nodes .takeWhile( - (node) => _filteredChildren(node, params.withComputerAnalysis) - .isNotEmpty, + (node) => + _filteredChildren(node, params.shouldShowComputerVariations) + .isNotEmpty, ) .mapIndexed( (i, node) { final children = _filteredChildren( node, - params.withComputerAnalysis, + params.shouldShowComputerVariations, ); final mainlineNode = children.first; final moves = [ @@ -1248,7 +1241,6 @@ List _comments( text: comment, style: textStyle.copyWith( fontSize: textStyle.fontSize! - 2.0, - fontStyle: FontStyle.italic, ), ), ) diff --git a/pubspec.yaml b/pubspec.yaml index ddb2f85809..e0f3c21950 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: lichess_mobile description: Lichess mobile app V2 publish_to: "none" -version: 0.13.1+001301 # See README.md for details about versioning +version: 0.13.2+001302 # See README.md for details about versioning environment: sdk: ">=3.5.0 <4.0.0" From 4722085d69451d8151f48b2ea34cfd27e616f161 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 22 Nov 2024 15:16:29 +0100 Subject: [PATCH 767/979] Fix study screen engine lines --- lib/src/view/study/study_screen.dart | 29 ++++++++++------------------ 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/lib/src/view/study/study_screen.dart b/lib/src/view/study/study_screen.dart index d7cad0abbf..04ce8608d2 100644 --- a/lib/src/view/study/study_screen.dart +++ b/lib/src/view/study/study_screen.dart @@ -176,28 +176,17 @@ class _Body extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final gamebookActive = ref.watch( - studyControllerProvider(id) - .select((state) => state.requireValue.gamebookActive), - ); - final engineGaugeParams = ref.watch( - studyControllerProvider(id) - .select((state) => state.valueOrNull?.engineGaugeParams), - ); - final isComputerAnalysisAllowed = ref.watch( - studyControllerProvider(id) - .select((s) => s.requireValue.isComputerAnalysisAllowed), - ); - - final currentNode = ref.watch( - studyControllerProvider(id) - .select((state) => state.requireValue.currentNode), - ); - + final studyState = ref.watch(studyControllerProvider(id)).requireValue; final analysisPrefs = ref.watch(analysisPreferencesProvider); final showEvaluationGauge = analysisPrefs.showEvaluationGauge; final numEvalLines = analysisPrefs.numEvalLines; + final gamebookActive = studyState.gamebookActive; + final engineGaugeParams = studyState.engineGaugeParams; + final isComputerAnalysisAllowed = studyState.isComputerAnalysisAllowed; + final isLocalEvaluationEnabled = studyState.isLocalEvaluationEnabled; + final currentNode = studyState.currentNode; + final bottomChild = gamebookActive ? StudyGamebook(id) : StudyTreeView(id); return DefaultTabController( @@ -229,7 +218,9 @@ class _Body extends ConsumerWidget { ); } : null, - engineLines: isComputerAnalysisAllowed && numEvalLines > 0 + engineLines: isComputerAnalysisAllowed && + isLocalEvaluationEnabled && + numEvalLines > 0 ? EngineLines( clientEval: currentNode.eval, isGameOver: currentNode.position?.isGameOver ?? false, From 3081d45443d0f7202eae19be5ac136dd4c57a615 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Sat, 23 Nov 2024 00:47:28 +0100 Subject: [PATCH 768/979] use the new AnalysisLayout widget --- .../view/broadcast/broadcast_game_screen.dart | 213 +++++------------- .../broadcast/broadcast_game_tree_view.dart | 2 - 2 files changed, 55 insertions(+), 160 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_game_screen.dart b/lib/src/view/broadcast/broadcast_game_screen.dart index e763747b3f..94a5ba4eed 100644 --- a/lib/src/view/broadcast/broadcast_game_screen.dart +++ b/lib/src/view/broadcast/broadcast_game_screen.dart @@ -4,7 +4,6 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_game_controller.dart'; @@ -19,7 +18,7 @@ import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/duration.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/lichess_assets.dart'; -import 'package:lichess_mobile/src/utils/screen.dart'; +import 'package:lichess_mobile/src/view/analysis/analysis_layout.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_game_bottom_bar.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_game_settings.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_game_tree_view.dart'; @@ -29,7 +28,6 @@ import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/clock.dart'; import 'package:lichess_mobile/src/widgets/pgn.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; class BroadcastGameScreen extends ConsumerWidget { @@ -89,161 +87,59 @@ class _Body extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return SafeArea( - child: Column( - children: [ - Expanded( - child: LayoutBuilder( - builder: (context, constraints) { - final defaultBoardSize = constraints.biggest.shortestSide; - final isTablet = isTabletOrLarger(context); - final remainingHeight = - constraints.maxHeight - defaultBoardSize; - final isSmallScreen = - remainingHeight < kSmallRemainingHeightLeftBoardThreshold; - final boardSize = isTablet || isSmallScreen - ? defaultBoardSize - kTabletBoardTableSidePadding * 2 - : defaultBoardSize; - final landscape = constraints.biggest.aspectRatio > 1; - final ctrlProvider = - broadcastGameControllerProvider(roundId, gameId); - - final engineGaugeParams = ref.watch( - ctrlProvider - .select((state) => state.requireValue.engineGaugeParams), - ); - - final currentNode = ref.watch( - ctrlProvider - .select((state) => state.requireValue.currentNode), - ); - - final isLocalEvaluationEnabled = ref.watch( - ctrlProvider.select( - (state) => state.requireValue.isLocalEvaluationEnabled, - ), - ); - - final engineLines = EngineLines( - clientEval: currentNode.eval, - isGameOver: currentNode.position.isGameOver, - onTapMove: ref - .read( - broadcastGameControllerProvider(roundId, gameId) - .notifier, - ) - .onUserMove, - ); - - final showEvaluationGauge = ref.watch( - analysisPreferencesProvider - .select((value) => value.showEvaluationGauge), - ); - - final engineGauge = - showEvaluationGauge && isLocalEvaluationEnabled - ? EngineGauge( - params: engineGaugeParams, - displayMode: landscape - ? EngineGaugeDisplayMode.vertical - : EngineGaugeDisplayMode.horizontal, - ) - : null; - - return landscape - ? Row( - mainAxisSize: MainAxisSize.max, - children: [ - Padding( - padding: EdgeInsets.all( - isTablet ? kTabletBoardTableSidePadding : 0.0, - ), - child: Row( - children: [ - _BroadcastBoardWithHeaders( - roundId, - gameId, - boardSize, - isTablet, - ), - if (engineGauge != null) ...[ - const SizedBox(width: 4.0), - engineGauge, - ], - ], - ), - ), - Flexible( - fit: FlexFit.loose, - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - if (isLocalEvaluationEnabled) engineLines, - Expanded( - child: PlatformCard( - clipBehavior: Clip.hardEdge, - borderRadius: const BorderRadius.all( - Radius.circular(4.0), - ), - margin: const EdgeInsets.all( - kTabletBoardTableSidePadding, - ), - semanticContainer: false, - child: BroadcastGameTreeView( - roundId, - gameId, - Orientation.landscape, - ), - ), - ), - ], - ), - ), - ], + final broadcastState = ref + .watch(broadcastGameControllerProvider(roundId, gameId)) + .requireValue; + final analysisPrefs = ref.watch(analysisPreferencesProvider); + final showEvaluationGauge = analysisPrefs.showEvaluationGauge; + final numEvalLines = analysisPrefs.numEvalLines; + + final engineGaugeParams = broadcastState.engineGaugeParams; + final isLocalEvaluationEnabled = broadcastState.isLocalEvaluationEnabled; + final currentNode = broadcastState.currentNode; + + return DefaultTabController( + length: 1, + child: AnalysisLayout( + boardBuilder: (context, boardSize, borderRadius) => + _BroadcastBoardWithHeaders( + roundId, + gameId, + boardSize, + borderRadius, + ), + engineGaugeBuilder: isLocalEvaluationEnabled && showEvaluationGauge + ? (context, orientation) { + return orientation == Orientation.portrait + ? EngineGauge( + displayMode: EngineGaugeDisplayMode.horizontal, + params: engineGaugeParams, ) - : Column( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Padding( - padding: EdgeInsets.all( - isTablet ? kTabletBoardTableSidePadding : 0.0, - ), - child: Column( - children: [ - if (engineGauge != null) engineGauge, - if (isLocalEvaluationEnabled) engineLines, - _BroadcastBoardWithHeaders( - roundId, - gameId, - boardSize, - isTablet, - ), - ], - ), - ), - Expanded( - child: Padding( - padding: isTablet - ? const EdgeInsets.symmetric( - horizontal: kTabletBoardTableSidePadding, - ) - : EdgeInsets.zero, - child: BroadcastGameTreeView( - roundId, - gameId, - Orientation.portrait, - ), - ), - ), - ], + : Container( + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4.0), + ), + child: EngineGauge( + displayMode: EngineGaugeDisplayMode.vertical, + params: engineGaugeParams, + ), ); - }, - ), - ), - BroadcastGameBottomBar(roundId: roundId, gameId: gameId), - ], + } + : null, + engineLines: isLocalEvaluationEnabled && numEvalLines > 0 + ? EngineLines( + clientEval: currentNode.eval, + isGameOver: currentNode.position.isGameOver, + onTapMove: ref + .read( + broadcastGameControllerProvider(roundId, gameId).notifier, + ) + .onUserMove, + ) + : null, + bottomBar: BroadcastGameBottomBar(roundId: roundId, gameId: gameId), + children: [BroadcastGameTreeView(roundId, gameId)], ), ); } @@ -253,17 +149,18 @@ class _BroadcastBoardWithHeaders extends ConsumerWidget { final BroadcastRoundId roundId; final BroadcastGameId gameId; final double size; - final bool isTablet; + final BorderRadiusGeometry? borderRadius; const _BroadcastBoardWithHeaders( this.roundId, this.gameId, this.size, - this.isTablet, + this.borderRadius, ); @override Widget build(BuildContext context, WidgetRef ref) { + // TODO use borderRadius with players widget return Column( children: [ _PlayerWidget( diff --git a/lib/src/view/broadcast/broadcast_game_tree_view.dart b/lib/src/view/broadcast/broadcast_game_tree_view.dart index 1352a1d769..691baac605 100644 --- a/lib/src/view/broadcast/broadcast_game_tree_view.dart +++ b/lib/src/view/broadcast/broadcast_game_tree_view.dart @@ -11,12 +11,10 @@ class BroadcastGameTreeView extends ConsumerWidget { const BroadcastGameTreeView( this.roundId, this.gameId, - this.displayMode, ); final BroadcastRoundId roundId; final BroadcastGameId gameId; - final Orientation displayMode; @override Widget build(BuildContext context, WidgetRef ref) { From 825dde50c9ddee1e1c2b0abb6b86dee9e8b02195 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Sat, 23 Nov 2024 00:48:58 +0100 Subject: [PATCH 769/979] add again rounded borders --- lib/src/view/analysis/analysis_board.dart | 13 +++-- lib/src/view/analysis/analysis_layout.dart | 5 +- lib/src/view/analysis/analysis_screen.dart | 4 +- .../view/broadcast/broadcast_game_screen.dart | 57 +++++++++++++++++-- lib/src/view/study/study_screen.dart | 17 +++--- 5 files changed, 71 insertions(+), 25 deletions(-) diff --git a/lib/src/view/analysis/analysis_board.dart b/lib/src/view/analysis/analysis_board.dart index 8f37f0fe54..66794455b6 100644 --- a/lib/src/view/analysis/analysis_board.dart +++ b/lib/src/view/analysis/analysis_board.dart @@ -17,14 +17,14 @@ class AnalysisBoard extends ConsumerStatefulWidget { const AnalysisBoard( this.options, this.boardSize, { - this.borderRadius, + this.radius, this.enableDrawingShapes = true, this.shouldReplaceChildOnUserMove = false, }); final AnalysisOptions options; final double boardSize; - final BorderRadiusGeometry? borderRadius; + final Radius? radius; final bool enableDrawingShapes; final bool shouldReplaceChildOnUserMove; @@ -106,10 +106,11 @@ class AnalysisBoardState extends ConsumerState { : IMap({sanMove.move.to: annotation}) : null, settings: boardPrefs.toBoardSettings().copyWith( - borderRadius: widget.borderRadius, - boxShadow: widget.borderRadius != null - ? boardShadows - : const [], + borderRadius: (widget.radius != null) + ? BorderRadius.all(widget.radius!) + : null, + boxShadow: + widget.radius != null ? boardShadows : const [], drawShape: DrawShapeOptions( enable: widget.enableDrawingShapes, onCompleteShape: _onCompleteShape, diff --git a/lib/src/view/analysis/analysis_layout.dart b/lib/src/view/analysis/analysis_layout.dart index 80f76e7595..4364f4dcfd 100644 --- a/lib/src/view/analysis/analysis_layout.dart +++ b/lib/src/view/analysis/analysis_layout.dart @@ -11,7 +11,7 @@ import 'package:lichess_mobile/src/widgets/platform.dart'; typedef BoardBuilder = Widget Function( BuildContext context, double boardSize, - BorderRadiusGeometry? borderRadius, + Radius? boardRadius, ); typedef EngineGaugeBuilder = Widget Function( @@ -158,8 +158,7 @@ class AnalysisLayout extends StatelessWidget { ? defaultBoardSize - kTabletBoardTableSidePadding * 2 : defaultBoardSize; - const tabletBoardRadius = - BorderRadius.all(Radius.circular(4.0)); + const tabletBoardRadius = Radius.circular(4.0); // If the aspect ratio is greater than 1, we are in landscape mode. if (aspectRatio > 1) { diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index 45d3ae1f2f..bfcd121366 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -184,10 +184,10 @@ class _Body extends ConsumerWidget { return AnalysisLayout( tabController: controller, - boardBuilder: (context, boardSize, borderRadius) => AnalysisBoard( + boardBuilder: (context, boardSize, boardRadius) => AnalysisBoard( options, boardSize, - borderRadius: borderRadius, + radius: boardRadius, enableDrawingShapes: enableDrawingShapes, ), engineGaugeBuilder: hasEval && showEvaluationGauge diff --git a/lib/src/view/broadcast/broadcast_game_screen.dart b/lib/src/view/broadcast/broadcast_game_screen.dart index 94a5ba4eed..ee8c16f517 100644 --- a/lib/src/view/broadcast/broadcast_game_screen.dart +++ b/lib/src/view/broadcast/broadcast_game_screen.dart @@ -4,6 +4,7 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_game_controller.dart'; @@ -149,13 +150,13 @@ class _BroadcastBoardWithHeaders extends ConsumerWidget { final BroadcastRoundId roundId; final BroadcastGameId gameId; final double size; - final BorderRadiusGeometry? borderRadius; + final Radius? radius; const _BroadcastBoardWithHeaders( this.roundId, this.gameId, this.size, - this.borderRadius, + this.radius, ); @override @@ -168,13 +169,15 @@ class _BroadcastBoardWithHeaders extends ConsumerWidget { gameId: gameId, width: size, widgetPosition: _PlayerWidgetPosition.top, + radius: radius ?? Radius.zero, ), - _BroadcastBoard(roundId, gameId, size), + _BroadcastBoard(roundId, gameId, size, radius != null), _PlayerWidget( roundId: roundId, gameId: gameId, width: size, widgetPosition: _PlayerWidgetPosition.bottom, + radius: radius ?? Radius.zero, ), ], ); @@ -186,11 +189,13 @@ class _BroadcastBoard extends ConsumerStatefulWidget { this.roundId, this.gameId, this.boardSize, + this.hasRadius, ); final BroadcastRoundId roundId; final BroadcastGameId gameId; final double boardSize; + final bool hasRadius; @override ConsumerState<_BroadcastBoard> createState() => _BroadcastBoardState(); @@ -266,6 +271,7 @@ class _BroadcastBoardState extends ConsumerState<_BroadcastBoard> { : IMap({sanMove.move.to: annotation}) : null, settings: boardPrefs.toBoardSettings().copyWith( + boxShadow: widget.hasRadius ? boardShadows : const [], drawShape: DrawShapeOptions( enable: boardPrefs.enableShapeDrawings, onCompleteShape: _onCompleteShape, @@ -304,12 +310,14 @@ class _PlayerWidget extends ConsumerWidget { required this.gameId, required this.width, required this.widgetPosition, + required this.radius, }); final BroadcastRoundId roundId; final BroadcastGameId gameId; final double width; final _PlayerWidgetPosition widgetPosition; + final Radius radius; @override Widget build(BuildContext context, WidgetRef ref) { @@ -340,7 +348,16 @@ class _PlayerWidget extends ConsumerWidget { if (game.isOver) Card( margin: EdgeInsets.zero, - shape: const Border(), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: (widgetPosition == _PlayerWidgetPosition.top) + ? radius + : Radius.zero, + bottomLeft: (widgetPosition == _PlayerWidgetPosition.bottom) + ? radius + : Radius.zero, + ), + ), child: Padding( padding: const EdgeInsets.symmetric( horizontal: 8.0, @@ -364,7 +381,26 @@ class _PlayerWidget extends ConsumerWidget { Expanded( child: Card( margin: EdgeInsets.zero, - shape: const Border(), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: widgetPosition == _PlayerWidgetPosition.top && + !game.isOver + ? radius + : Radius.zero, + topRight: widgetPosition == _PlayerWidgetPosition.top && + clock == null + ? radius + : Radius.zero, + bottomLeft: widgetPosition == _PlayerWidgetPosition.bottom && + !game.isOver + ? radius + : Radius.zero, + bottomRight: widgetPosition == _PlayerWidgetPosition.bottom && + clock == null + ? radius + : Radius.zero, + ), + ), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), @@ -428,7 +464,16 @@ class _PlayerWidget extends ConsumerWidget { : Theme.of(context).colorScheme.secondaryContainer : null, margin: EdgeInsets.zero, - shape: const Border(), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topRight: widgetPosition == _PlayerWidgetPosition.top + ? radius + : Radius.zero, + bottomRight: widgetPosition == _PlayerWidgetPosition.bottom + ? radius + : Radius.zero, + ), + ), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), diff --git a/lib/src/view/study/study_screen.dart b/lib/src/view/study/study_screen.dart index 04ce8608d2..1fd0248ad5 100644 --- a/lib/src/view/study/study_screen.dart +++ b/lib/src/view/study/study_screen.dart @@ -192,10 +192,10 @@ class _Body extends ConsumerWidget { return DefaultTabController( length: 1, child: AnalysisLayout( - boardBuilder: (context, boardSize, borderRadius) => _StudyBoard( + boardBuilder: (context, boardSize, boardRadius) => _StudyBoard( id: id, boardSize: boardSize, - borderRadius: borderRadius, + radius: boardRadius, ), engineGaugeBuilder: isComputerAnalysisAllowed && showEvaluationGauge && @@ -260,14 +260,14 @@ class _StudyBoard extends ConsumerStatefulWidget { const _StudyBoard({ required this.id, required this.boardSize, - this.borderRadius, + this.radius, }); final StudyId id; final double boardSize; - final BorderRadiusGeometry? borderRadius; + final Radius? radius; @override ConsumerState<_StudyBoard> createState() => _StudyBoardState(); @@ -349,10 +349,11 @@ class _StudyBoardState extends ConsumerState<_StudyBoard> { return Chessboard( size: widget.boardSize, settings: boardPrefs.toBoardSettings().copyWith( - borderRadius: widget.borderRadius, - boxShadow: widget.borderRadius != null - ? boardShadows - : const [], + borderRadius: (widget.radius != null) + ? BorderRadius.all(widget.radius!) + : null, + boxShadow: + widget.radius != null ? boardShadows : const [], drawShape: DrawShapeOptions( enable: true, onCompleteShape: _onCompleteShape, From dd822ad20d7d86c9d1abcc5cced48e472274c1eb Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Sat, 23 Nov 2024 01:27:47 +0100 Subject: [PATCH 770/979] fix lint error --- lib/src/view/opening_explorer/opening_explorer_screen.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index 063a134d3b..03f71be1e8 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -31,7 +31,7 @@ const _kTableRowPadding = EdgeInsets.symmetric( horizontal: _kTableRowHorizontalPadding, vertical: _kTableRowVerticalPadding, ); -const _kTabletBoardRadius = BorderRadius.all(Radius.circular(4.0)); +const _kTabletBoardRadius = Radius.circular(4.0); class OpeningExplorerScreen extends ConsumerStatefulWidget { const OpeningExplorerScreen({required this.options}); @@ -352,7 +352,7 @@ class _OpeningExplorerView extends StatelessWidget { child: AnalysisBoard( options, boardSize, - borderRadius: isTablet ? _kTabletBoardRadius : null, + radius: isTablet ? _kTabletBoardRadius : null, shouldReplaceChildOnUserMove: true, ), ), From 80ea3ca58fb56f972d67c2941f54dcb70ab4084d Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Sat, 23 Nov 2024 01:46:04 +0100 Subject: [PATCH 771/979] fix last move not highlighted when opening a broadcast game --- lib/src/model/broadcast/broadcast_game_controller.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/model/broadcast/broadcast_game_controller.dart b/lib/src/model/broadcast/broadcast_game_controller.dart index 255f43ca71..99c33cdb79 100644 --- a/lib/src/model/broadcast/broadcast_game_controller.dart +++ b/lib/src/model/broadcast/broadcast_game_controller.dart @@ -62,8 +62,6 @@ class BroadcastGameController extends _$BroadcastGameController evaluationService.disposeEngine(); }); - Move? lastMove; - final pgn = await ref.withClient( (client) => BroadcastRepository(client).getGame(roundId, gameId), ); @@ -73,9 +71,11 @@ class BroadcastGameController extends _$BroadcastGameController final rootComments = IList(game.comments.map((c) => PgnComment.fromPgn(c))); _root = Root.fromPgnGame(game); - final currentPath = _root.mainlinePath; final currentNode = _root.nodeAt(currentPath); + final lastMove = (_root.mainlinePath.last != null) + ? Move.parse(_root.mainlinePath.last.toString()) + : null; // don't use ref.watch here: we don't want to invalidate state when the // analysis preferences change From 64595471eaa2b8500ef21308caae07182357b0ce Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Sat, 23 Nov 2024 02:01:57 +0100 Subject: [PATCH 772/979] really fix last move not highlighted --- lib/src/model/broadcast/broadcast_game_controller.dart | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/src/model/broadcast/broadcast_game_controller.dart b/lib/src/model/broadcast/broadcast_game_controller.dart index 99c33cdb79..3580efcaac 100644 --- a/lib/src/model/broadcast/broadcast_game_controller.dart +++ b/lib/src/model/broadcast/broadcast_game_controller.dart @@ -73,14 +73,11 @@ class BroadcastGameController extends _$BroadcastGameController _root = Root.fromPgnGame(game); final currentPath = _root.mainlinePath; final currentNode = _root.nodeAt(currentPath); - final lastMove = (_root.mainlinePath.last != null) - ? Move.parse(_root.mainlinePath.last.toString()) - : null; + final lastMove = _root.branchAt(_root.mainlinePath)?.sanMove.move; // don't use ref.watch here: we don't want to invalidate state when the // analysis preferences change final prefs = ref.read(analysisPreferencesProvider); - final broadcastState = BroadcastGameState( id: gameId, currentPath: currentPath, From e3ac3ea9d958d2a821f174b8c6b9415c1686b089 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Sat, 23 Nov 2024 13:11:42 +0100 Subject: [PATCH 773/979] Fix chess960 analysis Fixes #1171 --- lib/src/model/common/node.dart | 7 + lib/src/model/game/playable_game.dart | 3 + test/model/game/game_test.dart | 53 ++ test/view/analysis/analysis_screen_test.dart | 576 +++++++++---------- 4 files changed, 351 insertions(+), 288 deletions(-) diff --git a/lib/src/model/common/node.dart b/lib/src/model/common/node.dart index fefd59fdcc..0747cf4b6a 100644 --- a/lib/src/model/common/node.dart +++ b/lib/src/model/common/node.dart @@ -5,9 +5,12 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/eval.dart'; import 'package:lichess_mobile/src/model/common/uci.dart'; +import 'package:logging/logging.dart'; part 'node.freezed.dart'; +final _logger = Logger('Node'); + /// A node in a game tree. /// /// The tree is implemented with a linked list of nodes, using mutable [List] of @@ -519,6 +522,10 @@ class Root extends Node { ); onVisitNode?.call(root, branch, isMainline); + } else { + _logger.warning( + 'Invalid move: ${childFrom.data.san}, on position: ${frame.to.position}', + ); } } } diff --git a/lib/src/model/game/playable_game.dart b/lib/src/model/game/playable_game.dart index e0ae58fc0f..937ccaf03c 100644 --- a/lib/src/model/game/playable_game.dart +++ b/lib/src/model/game/playable_game.dart @@ -161,8 +161,11 @@ class PlayableGame : null, opening: meta.opening, ), + initialFen: initialFen, steps: steps, status: status, + winner: winner, + isThreefoldRepetition: isThreefoldRepetition, white: white, black: black, youAre: youAre, diff --git a/test/model/game/game_test.dart b/test/model/game/game_test.dart index 7a27de8f80..e67e44c719 100644 --- a/test/model/game/game_test.dart +++ b/test/model/game/game_test.dart @@ -29,6 +29,7 @@ void main() { ''', ); }); + test('makePgn, finished game', () { final game = PlayableGame.fromServerJson( jsonDecode(_playableGameJson) as Map, @@ -54,6 +55,54 @@ void main() { ''', ); }); + + test('toArchivedGame', () { + for (final game in [_playableGameJson, _playable960GameJson]) { + final playableGame = PlayableGame.fromServerJson( + jsonDecode(game) as Map, + ); + final now = DateTime.now(); + final archivedGame = playableGame.toArchivedGame(finishedAt: now); + + expect(archivedGame.id, playableGame.id); + expect(archivedGame.meta, playableGame.meta); + expect(archivedGame.source, playableGame.source); + expect(archivedGame.data.id, playableGame.id); + expect(archivedGame.data.lastMoveAt, now); + expect(archivedGame.data.createdAt, playableGame.meta.createdAt); + expect(archivedGame.data.lastFen, playableGame.lastPosition.fen); + expect(archivedGame.data.variant, playableGame.meta.variant); + expect(archivedGame.data.perf, playableGame.meta.perf); + expect(archivedGame.data.speed, playableGame.meta.speed); + expect(archivedGame.data.rated, playableGame.meta.rated); + expect(archivedGame.data.winner, playableGame.winner); + expect(archivedGame.data.white, playableGame.white); + expect(archivedGame.data.black, playableGame.black); + expect(archivedGame.data.opening, playableGame.meta.opening); + expect( + archivedGame.data.clock, + playableGame.meta.clock != null + ? ( + initial: playableGame.meta.clock!.initial, + increment: playableGame.meta.clock!.increment, + ) + : null, + ); + expect(archivedGame.initialFen, playableGame.initialFen); + expect( + archivedGame.isThreefoldRepetition, + playableGame.isThreefoldRepetition, + ); + expect(archivedGame.status, playableGame.status); + expect(archivedGame.winner, playableGame.winner); + expect(archivedGame.white, playableGame.white); + expect(archivedGame.black, playableGame.black); + expect(archivedGame.steps, playableGame.steps); + expect(archivedGame.clocks, playableGame.clocks); + expect(archivedGame.evals, playableGame.evals); + expect(archivedGame.youAre, playableGame.youAre); + } + }); }); group('ArchivedGame', () { @@ -121,6 +170,10 @@ const _playableGameJson = ''' {"game":{"id":"CCW6EEru","variant":{"key":"standard","name":"Standard","short":"Std"},"speed":"bullet","perf":"bullet","rated":true,"fen":"6kr/p1p2rpp/4Q3/2b1p3/8/2P5/P2N1PPP/R3R1K1 b - - 0 22","turns":43,"source":"lobby","status":{"id":31,"name":"resign"},"createdAt":1706185945680,"winner":"white","pgn":"e4 e5 Nf3 Nc6 Bc4 Bc5 b4 Bxb4 c3 Ba5 d4 Bb6 Ba3 Nf6 Qb3 d6 Bxf7+ Kf8 O-O Qe7 Nxe5 Nxe5 dxe5 Be6 Bxe6 Nxe4 Re1 Nc5 Bxc5 Bxc5 Qxb7 Re8 Bh3 dxe5 Qf3+ Kg8 Nd2 Rf8 Qd5+ Rf7 Be6 Qxe6 Qxe6"},"white":{"user":{"name":"veloce","id":"veloce"},"rating":1789,"ratingDiff":9},"black":{"user":{"name":"chabrot","id":"chabrot"},"rating":1810,"ratingDiff":-9},"socket":0,"clock":{"running":false,"initial":120,"increment":1,"white":31.2,"black":27.42,"emerg":15,"moretime":15},"takebackable":true,"youAre":"white","prefs":{"autoQueen":2,"zen":2,"confirmResign":true,"enablePremove":true},"chat":{"lines":[]}} '''; +const _playable960GameJson = ''' +{"game":{"id":"sfqnC9ZK","variant":{"key":"chess960","name":"Chess960","short":"960"},"speed":"blitz","perf":"chess960","rated":false,"fen":"1k2rb1n/pp4pp/1n3p2/2Npr3/5N2/5PP1/1PPBP2P/q1KRR1Q1 w - - 1 15","turns":28,"source":"lobby","status":{"id":30,"name":"mate"},"createdAt":1686125895867,"initialFen":"nrbkrbqn/pppppppp/8/8/8/8/PPPPPPPP/NRBKRBQN w KQkq - 0 1","winner":"black","pgn":"f3 Nb6 d3 d6 Be3 c5 d4 Bf5 O-O-O O-O-O Nf2 e5 dxe5 Rxe5 g3 Kb8 Bh3 Bxh3 Nxh3 f6 Nf4 Qxa2 Nb3 Rde8 Bd2 d5 Nxc5 Qa1#"},"white":{"user":{"name":"MinBorn","id":"minborn"},"rating":1500,"provisional":true},"black":{"user":{"name":"veloce","patron":true,"id":"veloce"},"rating":1292,"provisional":true,"onGame":true},"socket":0,"clock":{"running":false,"initial":180,"increment":2,"white":145.34,"black":118.8,"emerg":22,"moretime":15},"youAre":"black","prefs":{"autoQueen":2,"zen":0,"confirmResign":true,"enablePremove":true},"chat":{"lines":[]}} +'''; + const _archivedGameJson = ''' {"id":"CCW6EEru","rated":true,"source":"lobby","variant":"standard","speed":"bullet","perf":"bullet","createdAt":1706185945680,"lastMoveAt":1706186170504,"status":"resign","players":{"white":{"user":{"name":"veloce","id":"veloce"},"rating":1789,"ratingDiff":9,"analysis":{"inaccuracy":2,"mistake":1,"blunder":3,"acpl":90}},"black":{"user":{"name":"chabrot","id":"chabrot"},"rating":1810,"ratingDiff":-9,"analysis":{"inaccuracy":3,"mistake":0,"blunder":5,"acpl":135}}},"winner":"white","opening":{"eco":"C52","name":"Italian Game: Evans Gambit, Main Line","ply":10},"moves":"e4 e5 Nf3 Nc6 Bc4 Bc5 b4 Bxb4 c3 Ba5 d4 Bb6 Ba3 Nf6 Qb3 d6 Bxf7+ Kf8 O-O Qe7 Nxe5 Nxe5 dxe5 Be6 Bxe6 Nxe4 Re1 Nc5 Bxc5 Bxc5 Qxb7 Re8 Bh3 dxe5 Qf3+ Kg8 Nd2 Rf8 Qd5+ Rf7 Be6 Qxe6 Qxe6","clocks":[12003,12003,11883,11811,11683,11379,11307,11163,11043,11043,10899,10707,10155,10483,10019,9995,9635,9923,8963,8603,7915,8283,7763,7459,7379,6083,6587,5819,6363,5651,6075,5507,5675,4803,5059,4515,4547,3555,3971,3411,3235,3123,3120,2742],"analysis":[{"eval":32},{"eval":41},{"eval":39},{"eval":20},{"eval":17},{"eval":21},{"eval":-21},{"eval":-14},{"eval":-23},{"eval":-24},{"eval":-24},{"eval":52,"best":"d7d6","variation":"d6","judgment":{"name":"Inaccuracy","comment":"Inaccuracy. d6 was best."}},{"eval":-56,"best":"f3e5","variation":"Nxe5","judgment":{"name":"Inaccuracy","comment":"Inaccuracy. Nxe5 was best."}},{"eval":177,"best":"d7d6","variation":"d6","judgment":{"name":"Blunder","comment":"Blunder. d6 was best."}},{"eval":-19,"best":"d4e5","variation":"dxe5 Ng4 Qd5 Nh6 Nbd2 Ne7 Qd3 O-O h3 d6 g4 Kh8 exd6 cxd6","judgment":{"name":"Blunder","comment":"Blunder. dxe5 was best."}},{"eval":-16},{"eval":-20},{"eval":-12},{"eval":-145,"best":"f7d5","variation":"Bd5 Nxd5","judgment":{"name":"Mistake","comment":"Mistake. Bd5 was best."}},{"eval":72,"best":"c6a5","variation":"Na5 Qd1 Kxf7 dxe5 dxe5 Nxe5+ Ke8 Nd2 Be6 Qa4+ Bd7 Qd1 Nc6 Ndc4","judgment":{"name":"Blunder","comment":"Blunder. Na5 was best."}},{"eval":-36,"best":"f7d5","variation":"Bd5 Nxd5 exd5 Na5 Qb4 exd4 cxd4 Kg8 Re1 Qf7 Ng5 Qg6 Nc3 h6","judgment":{"name":"Inaccuracy","comment":"Inaccuracy. Bd5 was best."}},{"eval":-41},{"eval":-42},{"eval":593,"best":"e7f7","variation":"Qxf7 exf6 gxf6 c4 Rg8 Nd2 Qh5 c5 Bh3 g3 Bxc5 Bxc5 dxc5 Rfe1","judgment":{"name":"Blunder","comment":"Blunder. Qxf7 was best."}},{"eval":589},{"eval":630},{"eval":-32,"best":"e5d6","variation":"exd6 cxd6 Bd5 Nxf2 Nd2 g5 Nc4 Kg7 Nxb6 Qe3 Rxf2 Rhf8 Bf3 axb6","judgment":{"name":"Blunder","comment":"Blunder. exd6 was best."}},{"eval":602,"best":"b6f2","variation":"Bxf2+","judgment":{"name":"Blunder","comment":"Blunder. Bxf2+ was best."}},{"eval":581},{"eval":656},{"eval":662},{"mate":15,"best":"g7g6","variation":"g6 Qxa8+ Kg7 Qd5 c6 Qb3 Rf8 Re2 Qh4 Qb7+ Kh6 Qb2 dxe5 Nd2","judgment":{"name":"Blunder","comment":"Checkmate is now unavoidable. g6 was best."}},{"eval":566,"best":"b7f3","variation":"Qf3+ Qf6 exf6 g6 f7 Kg7 fxe8=Q Rxe8 Qf7+ Kh6 Qxe8 d5 g4 c6","judgment":{"name":"Blunder","comment":"Lost forced checkmate sequence. Qf3+ was best."}},{"eval":574},{"eval":566},{"eval":580},{"eval":569},{"eval":774,"best":"g7g6","variation":"g6 Ne4 Kg7 Qe2 Rd8 a4 h5 Rad1 Rxd1 Rxd1 Bb6 Rd7 Qxd7 Bxd7","judgment":{"name":"Inaccuracy","comment":"Inaccuracy. g6 was best."}},{"eval":739},{"eval":743},{"eval":615},{"eval":934,"best":"c5f2","variation":"Bxf2+ Kh1 Bxe1 Rxe1 g6 Rf1 Kg7 Rxf7+ Qxf7 Bxf7 Rf8 Be6 e4 Qxe4","judgment":{"name":"Inaccuracy","comment":"Inaccuracy. Bxf2+ was best."}},{"eval":861}],"clock":{"initial":120,"increment":1,"totalTime":160}} '''; diff --git a/test/view/analysis/analysis_screen_test.dart b/test/view/analysis/analysis_screen_test.dart index bf91a6b889..00ea3fcf06 100644 --- a/test/view/analysis/analysis_screen_test.dart +++ b/test/view/analysis/analysis_screen_test.dart @@ -108,349 +108,349 @@ void main() { isTrue, ); }); + }); - group('Analysis Tree View', () { - Future buildTree( - WidgetTester tester, - String pgn, - ) async { - final app = await makeTestProviderScopeApp( - tester, - defaultPreferences: { - PrefCategory.analysis.storageKey: jsonEncode( - AnalysisPrefs.defaults - .copyWith( - enableLocalEvaluation: false, - ) - .toJson(), - ), - }, - home: AnalysisScreen( - options: AnalysisOptions( - orientation: Side.white, - standalone: ( - pgn: pgn, - isComputerAnalysisAllowed: false, - variant: Variant.standard, - ), + group('Analysis Tree View', () { + Future buildTree( + WidgetTester tester, + String pgn, + ) async { + final app = await makeTestProviderScopeApp( + tester, + defaultPreferences: { + PrefCategory.analysis.storageKey: jsonEncode( + AnalysisPrefs.defaults + .copyWith( + enableLocalEvaluation: false, + ) + .toJson(), + ), + }, + home: AnalysisScreen( + options: AnalysisOptions( + orientation: Side.white, + standalone: ( + pgn: pgn, + isComputerAnalysisAllowed: false, + variant: Variant.standard, ), - enableDrawingShapes: false, ), - ); + enableDrawingShapes: false, + ), + ); - await tester.pumpWidget(app); - await tester.pump(const Duration(milliseconds: 1)); - } + await tester.pumpWidget(app); + await tester.pump(const Duration(milliseconds: 1)); + } - Text parentText(WidgetTester tester, String move) { - return tester.widget( - find.ancestor( - of: find.text(move), - matching: find.byType(Text), - ), - ); - } + Text parentText(WidgetTester tester, String move) { + return tester.widget( + find.ancestor( + of: find.text(move), + matching: find.byType(Text), + ), + ); + } - void expectSameLine(WidgetTester tester, Iterable moves) { - final line = parentText(tester, moves.first); + void expectSameLine(WidgetTester tester, Iterable moves) { + final line = parentText(tester, moves.first); - for (final move in moves.skip(1)) { - final moveText = find.text(move); - expect(moveText, findsOneWidget); + for (final move in moves.skip(1)) { + final moveText = find.text(move); + expect(moveText, findsOneWidget); + expect( + parentText(tester, move), + line, + ); + } + } + + void expectDifferentLines( + WidgetTester tester, + List moves, + ) { + for (int i = 0; i < moves.length; i++) { + for (int j = i + 1; j < moves.length; j++) { expect( - parentText(tester, move), - line, + parentText(tester, moves[i]), + isNot(parentText(tester, moves[j])), ); } } + } - void expectDifferentLines( - WidgetTester tester, - List moves, - ) { - for (int i = 0; i < moves.length; i++) { - for (int j = i + 1; j < moves.length; j++) { - expect( - parentText(tester, moves[i]), - isNot(parentText(tester, moves[j])), - ); - } - } - } + testWidgets('displays short sideline as inline', (tester) async { + await buildTree(tester, '1. e4 e5 (1... d5 2. exd5) 2. Nf3 *'); - testWidgets('displays short sideline as inline', (tester) async { - await buildTree(tester, '1. e4 e5 (1... d5 2. exd5) 2. Nf3 *'); + final mainline = find.ancestor( + of: find.text('1. e4'), + matching: find.byType(Text), + ); + expect(mainline, findsOneWidget); - final mainline = find.ancestor( - of: find.text('1. e4'), - matching: find.byType(Text), - ); - expect(mainline, findsOneWidget); + expectSameLine(tester, ['1. e4', 'e5', '1… d5', '2. exd5', '2. Nf3']); + }); - expectSameLine(tester, ['1. e4', 'e5', '1… d5', '2. exd5', '2. Nf3']); - }); + testWidgets('displays long sideline on its own line', (tester) async { + await buildTree( + tester, + '1. e4 e5 (1... d5 2. exd5 Qxd5 3. Nc3 Qd8 4. d4 Nf6) 2. Nc3 *', + ); - testWidgets('displays long sideline on its own line', (tester) async { - await buildTree( - tester, - '1. e4 e5 (1... d5 2. exd5 Qxd5 3. Nc3 Qd8 4. d4 Nf6) 2. Nc3 *', - ); + expectSameLine(tester, ['1. e4', 'e5']); + expectSameLine( + tester, + ['1… d5', '2. exd5', 'Qxd5', '3. Nc3', 'Qd8', '4. d4', 'Nf6'], + ); + expectSameLine(tester, ['2. Nc3']); - expectSameLine(tester, ['1. e4', 'e5']); - expectSameLine( - tester, - ['1… d5', '2. exd5', 'Qxd5', '3. Nc3', 'Qd8', '4. d4', 'Nf6'], - ); - expectSameLine(tester, ['2. Nc3']); + expectDifferentLines(tester, ['1. e4', '1… d5', '2. Nc3']); + }); - expectDifferentLines(tester, ['1. e4', '1… d5', '2. Nc3']); - }); + testWidgets('displays sideline with branching on its own line', + (tester) async { + await buildTree(tester, '1. e4 e5 (1... d5 2. exd5 (2. Nc3)) *'); - testWidgets('displays sideline with branching on its own line', - (tester) async { - await buildTree(tester, '1. e4 e5 (1... d5 2. exd5 (2. Nc3)) *'); + expectSameLine(tester, ['1. e4', 'e5']); - expectSameLine(tester, ['1. e4', 'e5']); + // 2nd branch is rendered inline again + expectSameLine(tester, ['1… d5', '2. exd5', '2. Nc3']); - // 2nd branch is rendered inline again - expectSameLine(tester, ['1… d5', '2. exd5', '2. Nc3']); + expectDifferentLines(tester, ['1. e4', '1… d5']); + }); - expectDifferentLines(tester, ['1. e4', '1… d5']); - }); + testWidgets('multiple sidelines', (tester) async { + await buildTree( + tester, + '1. e4 e5 (1... d5 2. exd5) (1... Nf6 2. e5) 2. Nf3 Nc6 (2... a5) *', + ); - testWidgets('multiple sidelines', (tester) async { - await buildTree( - tester, - '1. e4 e5 (1... d5 2. exd5) (1... Nf6 2. e5) 2. Nf3 Nc6 (2... a5) *', - ); + expectSameLine(tester, ['1. e4', 'e5']); + expectSameLine(tester, ['1… d5', '2. exd5']); + expectSameLine(tester, ['1… Nf6', '2. e5']); + expectSameLine(tester, ['2. Nf3', 'Nc6', '2… a5']); - expectSameLine(tester, ['1. e4', 'e5']); - expectSameLine(tester, ['1… d5', '2. exd5']); - expectSameLine(tester, ['1… Nf6', '2. e5']); - expectSameLine(tester, ['2. Nf3', 'Nc6', '2… a5']); + expectDifferentLines(tester, ['1. e4', '1… d5', '1… Nf6', '2. Nf3']); + }); - expectDifferentLines(tester, ['1. e4', '1… d5', '1… Nf6', '2. Nf3']); - }); + testWidgets('collapses lines with nesting > 2', (tester) async { + await buildTree( + tester, + '1. e4 e5 (1... d5 2. Nc3 (2. h4 h5 (2... Nc6 3. d3) (2... Qd7))) *', + ); - testWidgets('collapses lines with nesting > 2', (tester) async { - await buildTree( - tester, - '1. e4 e5 (1... d5 2. Nc3 (2. h4 h5 (2... Nc6 3. d3) (2... Qd7))) *', - ); + expectSameLine(tester, ['1. e4', 'e5']); + expectSameLine(tester, ['1… d5']); + expectSameLine(tester, ['2. Nc3']); + expectSameLine(tester, ['2. h4']); - expectSameLine(tester, ['1. e4', 'e5']); - expectSameLine(tester, ['1… d5']); - expectSameLine(tester, ['2. Nc3']); - expectSameLine(tester, ['2. h4']); - - expect(find.text('2… h5'), findsNothing); - expect(find.text('2… Nc6'), findsNothing); - expect(find.text('3. d3'), findsNothing); - expect(find.text('2… Qd7'), findsNothing); - - // sidelines with nesting > 2 are collapsed -> expand them - expect(find.byIcon(Icons.add_box), findsOneWidget); - - await tester.tap(find.byIcon(Icons.add_box)); - await tester.pumpAndSettle(); - - expectSameLine(tester, ['2… h5']); - expectSameLine(tester, ['2… Nc6', '3. d3']); - expectSameLine(tester, ['2… Qd7']); - - final d3 = find.text('3. d3'); - await tester.longPress(d3); - await tester.pumpAndSettle(); - - await tester.tap(find.text('Collapse variations')); - - // need to wait for current move change debounce delay - await tester.pumpAndSettle(const Duration(milliseconds: 200)); - - // Sidelines should be collapsed again - expect(find.byIcon(Icons.add_box), findsOneWidget); - - expect(find.text('2… h5'), findsNothing); - expect(find.text('2… Nc6'), findsNothing); - expect(find.text('3. d3'), findsNothing); - expect(find.text('2… Qd7'), findsNothing); - }); - - testWidgets( - 'Expanding one line does not expand the following one (regression test)', - (tester) async { - /// Will be rendered as: - /// ------------------- - /// 1. e4 e5 - /// |- 1... d5 2. Nf3 (2.Nc3) - /// 2. Nf3 - /// |- 2. a4 d5 (2... f5) - /// ------------------- - await buildTree( - tester, - '1. e4 e5 (1... d5 2. Nf3 (2. Nc3)) 2. Nf3 (2. a4 d5 (2... f5))', - ); + expect(find.text('2… h5'), findsNothing); + expect(find.text('2… Nc6'), findsNothing); + expect(find.text('3. d3'), findsNothing); + expect(find.text('2… Qd7'), findsNothing); - expect(find.byIcon(Icons.add_box), findsNothing); + // sidelines with nesting > 2 are collapsed -> expand them + expect(find.byIcon(Icons.add_box), findsOneWidget); - // Collapse both lines - await tester.longPress(find.text('1… d5')); - await tester.pumpAndSettle(); // wait for context menu to appear - await tester.tap(find.text('Collapse variations')); + await tester.tap(find.byIcon(Icons.add_box)); + await tester.pumpAndSettle(); - // wait for dialog to close and tree to refresh - await tester.pumpAndSettle(const Duration(milliseconds: 200)); + expectSameLine(tester, ['2… h5']); + expectSameLine(tester, ['2… Nc6', '3. d3']); + expectSameLine(tester, ['2… Qd7']); - await tester.longPress(find.text('2. a4')); - await tester.pumpAndSettle(); // wait for context menu to appear - await tester.tap(find.text('Collapse variations')); + final d3 = find.text('3. d3'); + await tester.longPress(d3); + await tester.pumpAndSettle(); - // wait for dialog to close and tree to refresh - await tester.pumpAndSettle(const Duration(milliseconds: 200)); + await tester.tap(find.text('Collapse variations')); - // In this state, there used to be a bug where expanding the first line would - // also expand the second line. - expect(find.byIcon(Icons.add_box), findsNWidgets(2)); - await tester.tap(find.byIcon(Icons.add_box).first); + // need to wait for current move change debounce delay + await tester.pumpAndSettle(const Duration(milliseconds: 200)); - // need to wait for current move change debounce delay - await tester.pumpAndSettle(); + // Sidelines should be collapsed again + expect(find.byIcon(Icons.add_box), findsOneWidget); - expect(find.byIcon(Icons.add_box), findsOneWidget); + expect(find.text('2… h5'), findsNothing); + expect(find.text('2… Nc6'), findsNothing); + expect(find.text('3. d3'), findsNothing); + expect(find.text('2… Qd7'), findsNothing); + }); - // Second sideline should still be collapsed - expect(find.text('2. a4'), findsNothing); - }); + testWidgets( + 'Expanding one line does not expand the following one (regression test)', + (tester) async { + /// Will be rendered as: + /// ------------------- + /// 1. e4 e5 + /// |- 1... d5 2. Nf3 (2.Nc3) + /// 2. Nf3 + /// |- 2. a4 d5 (2... f5) + /// ------------------- + await buildTree( + tester, + '1. e4 e5 (1... d5 2. Nf3 (2. Nc3)) 2. Nf3 (2. a4 d5 (2... f5))', + ); - testWidgets('subtrees not part of the current mainline part are cached', - (tester) async { - await buildTree( - tester, - '1. e4 e5 (1... d5 2. exd5) (1... Nf6 2. e5) 2. Nf3 Nc6 (2... a5) *', - ); + expect(find.byIcon(Icons.add_box), findsNothing); - // will be rendered as: - // ------------------- - // 1. e4 e5 <-- first mainline part - // |- 1... d5 2. exd5 - // |- 1... Nf6 2. e5 - // 2. Nf3 Nc6 (2... a5) <-- second mainline part - // ^ - // | - // current move + // Collapse both lines + await tester.longPress(find.text('1… d5')); + await tester.pumpAndSettle(); // wait for context menu to appear + await tester.tap(find.text('Collapse variations')); - final firstMainlinePart = parentText(tester, '1. e4'); - final secondMainlinePart = parentText(tester, '2. Nf3'); + // wait for dialog to close and tree to refresh + await tester.pumpAndSettle(const Duration(milliseconds: 200)); - expect( - tester - .widgetList( - find.ancestor( - of: find.textContaining('Nc6'), - matching: find.byType(InlineMove), - ), - ) - .last - .isCurrentMove, - isTrue, - ); + await tester.longPress(find.text('2. a4')); + await tester.pumpAndSettle(); // wait for context menu to appear + await tester.tap(find.text('Collapse variations')); - await tester.tap(find.byKey(const Key('goto-previous'))); - // need to wait for current move change debounce delay - await tester.pumpAndSettle(const Duration(milliseconds: 200)); + // wait for dialog to close and tree to refresh + await tester.pumpAndSettle(const Duration(milliseconds: 200)); - expect( - tester - .widgetList( - find.ancestor( - of: find.textContaining('Nf3'), - matching: find.byType(InlineMove), - ), - ) - .last - .isCurrentMove, - isTrue, - ); + // In this state, there used to be a bug where expanding the first line would + // also expand the second line. + expect(find.byIcon(Icons.add_box), findsNWidgets(2)); + await tester.tap(find.byIcon(Icons.add_box).first); - // first mainline part has not changed since the current move is - // not part of it - expect( - identical(firstMainlinePart, parentText(tester, '1. e4')), - isTrue, - ); + // need to wait for current move change debounce delay + await tester.pumpAndSettle(); - final secondMainlinePartOnMoveNf3 = parentText(tester, '2. Nf3'); + expect(find.byIcon(Icons.add_box), findsOneWidget); - // second mainline part has changed since the current move is part of it - expect( - secondMainlinePart, - isNot(secondMainlinePartOnMoveNf3), - ); + // Second sideline should still be collapsed + expect(find.text('2. a4'), findsNothing); + }); - await tester.tap(find.byKey(const Key('goto-previous'))); - // need to wait for current move change debounce delay - await tester.pumpAndSettle(const Duration(milliseconds: 200)); + testWidgets('subtrees not part of the current mainline part are cached', + (tester) async { + await buildTree( + tester, + '1. e4 e5 (1... d5 2. exd5) (1... Nf6 2. e5) 2. Nf3 Nc6 (2... a5) *', + ); - expect( - tester - .widgetList( - find.ancestor( - of: find.textContaining('e5'), - matching: find.byType(InlineMove), - ), - ) - .first - .isCurrentMove, - isTrue, - ); + // will be rendered as: + // ------------------- + // 1. e4 e5 <-- first mainline part + // |- 1... d5 2. exd5 + // |- 1... Nf6 2. e5 + // 2. Nf3 Nc6 (2... a5) <-- second mainline part + // ^ + // | + // current move - final firstMainlinePartOnMoveE5 = parentText(tester, '1. e4'); - final secondMainlinePartOnMoveE5 = parentText(tester, '2. Nf3'); + final firstMainlinePart = parentText(tester, '1. e4'); + final secondMainlinePart = parentText(tester, '2. Nf3'); - // first mainline part has changed since the current move is part of it - expect( - firstMainlinePart, - isNot(firstMainlinePartOnMoveE5), - ); + expect( + tester + .widgetList( + find.ancestor( + of: find.textContaining('Nc6'), + matching: find.byType(InlineMove), + ), + ) + .last + .isCurrentMove, + isTrue, + ); - // second mainline part has changed since the current move is not part of it - // anymore - expect( - secondMainlinePartOnMoveNf3, - isNot(secondMainlinePartOnMoveE5), - ); + await tester.tap(find.byKey(const Key('goto-previous'))); + // need to wait for current move change debounce delay + await tester.pumpAndSettle(const Duration(milliseconds: 200)); - await tester.tap(find.byKey(const Key('goto-previous'))); - // need to wait for current move change debounce delay - await tester.pumpAndSettle(const Duration(milliseconds: 200)); + expect( + tester + .widgetList( + find.ancestor( + of: find.textContaining('Nf3'), + matching: find.byType(InlineMove), + ), + ) + .last + .isCurrentMove, + isTrue, + ); - expect( - tester - .firstWidget( - find.ancestor( - of: find.textContaining('e4'), - matching: find.byType(InlineMove), - ), - ) - .isCurrentMove, - isTrue, - ); + // first mainline part has not changed since the current move is + // not part of it + expect( + identical(firstMainlinePart, parentText(tester, '1. e4')), + isTrue, + ); - final firstMainlinePartOnMoveE4 = parentText(tester, '1. e4'); - final secondMainlinePartOnMoveE4 = parentText(tester, '2. Nf3'); + final secondMainlinePartOnMoveNf3 = parentText(tester, '2. Nf3'); - // first mainline part has changed since the current move is part of it - expect( - firstMainlinePartOnMoveE4, - isNot(firstMainlinePartOnMoveE5), - ); + // second mainline part has changed since the current move is part of it + expect( + secondMainlinePart, + isNot(secondMainlinePartOnMoveNf3), + ); - // second mainline part has not changed since the current move is not part of it - expect( - identical(secondMainlinePartOnMoveE5, secondMainlinePartOnMoveE4), - isTrue, - ); - }); + await tester.tap(find.byKey(const Key('goto-previous'))); + // need to wait for current move change debounce delay + await tester.pumpAndSettle(const Duration(milliseconds: 200)); + + expect( + tester + .widgetList( + find.ancestor( + of: find.textContaining('e5'), + matching: find.byType(InlineMove), + ), + ) + .first + .isCurrentMove, + isTrue, + ); + + final firstMainlinePartOnMoveE5 = parentText(tester, '1. e4'); + final secondMainlinePartOnMoveE5 = parentText(tester, '2. Nf3'); + + // first mainline part has changed since the current move is part of it + expect( + firstMainlinePart, + isNot(firstMainlinePartOnMoveE5), + ); + + // second mainline part has changed since the current move is not part of it + // anymore + expect( + secondMainlinePartOnMoveNf3, + isNot(secondMainlinePartOnMoveE5), + ); + + await tester.tap(find.byKey(const Key('goto-previous'))); + // need to wait for current move change debounce delay + await tester.pumpAndSettle(const Duration(milliseconds: 200)); + + expect( + tester + .firstWidget( + find.ancestor( + of: find.textContaining('e4'), + matching: find.byType(InlineMove), + ), + ) + .isCurrentMove, + isTrue, + ); + + final firstMainlinePartOnMoveE4 = parentText(tester, '1. e4'); + final secondMainlinePartOnMoveE4 = parentText(tester, '2. Nf3'); + + // first mainline part has changed since the current move is part of it + expect( + firstMainlinePartOnMoveE4, + isNot(firstMainlinePartOnMoveE5), + ); + + // second mainline part has not changed since the current move is not part of it + expect( + identical(secondMainlinePartOnMoveE5, secondMainlinePartOnMoveE4), + isTrue, + ); }); }); } From 30b943c6bd155b5f7d9a5e46edd92df830245d8f Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Sat, 23 Nov 2024 13:20:37 +0100 Subject: [PATCH 774/979] Don't override lastMoveAt when storing local archived game --- lib/src/model/game/game_controller.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/src/model/game/game_controller.dart b/lib/src/model/game/game_controller.dart index 958a42ca2c..7cc70bf51f 100644 --- a/lib/src/model/game/game_controller.dart +++ b/lib/src/model/game/game_controller.dart @@ -979,8 +979,10 @@ class GameController extends _$GameController { Future _storeGame(PlayableGame game) async { if (game.finished) { - (await ref.read(gameStorageProvider.future)) - .save(game.toArchivedGame(finishedAt: DateTime.now())); + final gameStorage = await ref.read(gameStorageProvider.future); + final existing = await gameStorage.fetch(gameId: gameFullId.gameId); + final finishedAt = existing?.data.lastMoveAt ?? DateTime.now(); + await gameStorage.save(game.toArchivedGame(finishedAt: finishedAt)); } } From 6d8ca749137a7b4b41634865bd89c45263c06fa1 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Sat, 23 Nov 2024 13:32:19 +0100 Subject: [PATCH 775/979] Bump version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index e0f3c21950..5422a4dbcc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: lichess_mobile description: Lichess mobile app V2 publish_to: "none" -version: 0.13.2+001302 # See README.md for details about versioning +version: 0.13.3+001303 # See README.md for details about versioning environment: sdk: ">=3.5.0 <4.0.0" From 0b035401436c5f71a3bbb9b37303932b3219b15b Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Sat, 23 Nov 2024 13:34:02 +0100 Subject: [PATCH 776/979] Update fastlane --- android/Gemfile.lock | 63 ++++++++++++++++++++++++-------------------- ios/Gemfile.lock | 53 +++++++++++++++++++------------------ 2 files changed, 62 insertions(+), 54 deletions(-) diff --git a/android/Gemfile.lock b/android/Gemfile.lock index d7ff3604de..4168b7b944 100644 --- a/android/Gemfile.lock +++ b/android/Gemfile.lock @@ -5,25 +5,25 @@ GEM base64 nkf rexml - addressable (2.8.6) - public_suffix (>= 2.0.2, < 6.0) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.3.0) - aws-partitions (1.934.0) - aws-sdk-core (3.196.1) + aws-partitions (1.1013.0) + aws-sdk-core (3.213.0) aws-eventstream (~> 1, >= 1.3.0) - aws-partitions (~> 1, >= 1.651.0) - aws-sigv4 (~> 1.8) + aws-partitions (~> 1, >= 1.992.0) + aws-sigv4 (~> 1.9) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.82.0) - aws-sdk-core (~> 3, >= 3.193.0) - aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.151.0) - aws-sdk-core (~> 3, >= 3.194.0) + aws-sdk-kms (1.96.0) + aws-sdk-core (~> 3, >= 3.210.0) + aws-sigv4 (~> 1.5) + aws-sdk-s3 (1.173.0) + aws-sdk-core (~> 3, >= 3.210.0) aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.8) - aws-sigv4 (1.8.0) + aws-sigv4 (~> 1.5) + aws-sigv4 (1.10.1) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) base64 (0.2.0) @@ -38,8 +38,8 @@ GEM domain_name (0.6.20240107) dotenv (2.8.1) emoji_regex (3.2.3) - excon (0.110.0) - faraday (1.10.3) + excon (0.112.0) + faraday (1.10.4) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) faraday-excon (~> 1.1) @@ -60,15 +60,15 @@ GEM faraday-httpclient (1.0.1) faraday-multipart (1.0.4) multipart-post (~> 2) - faraday-net_http (1.0.1) + faraday-net_http (1.0.2) faraday-net_http_persistent (1.2.0) faraday-patron (1.0.0) faraday-rack (1.0.0) faraday-retry (1.0.3) - faraday_middleware (1.2.0) + faraday_middleware (1.2.1) faraday (~> 1.0) fastimage (2.3.1) - fastlane (2.221.1) + fastlane (2.225.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -84,6 +84,7 @@ GEM faraday-cookie_jar (~> 0.0.6) faraday_middleware (~> 1.0) fastimage (>= 2.1.0, < 3.0.0) + fastlane-sirp (>= 1.0.0) gh_inspector (>= 1.1.2, < 2.0.0) google-apis-androidpublisher_v3 (~> 0.3) google-apis-playcustomapp_v1 (~> 0.1) @@ -109,6 +110,8 @@ GEM xcodeproj (>= 1.13.0, < 2.0.0) xcpretty (~> 0.3.0) xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) + fastlane-sirp (1.0.0) + sysrandom (~> 1.0) gh_inspector (1.1.3) google-apis-androidpublisher_v3 (0.54.0) google-apis-core (>= 0.11.0, < 2.a) @@ -126,7 +129,7 @@ GEM google-apis-core (>= 0.11.0, < 2.a) google-apis-storage_v1 (0.31.0) google-apis-core (>= 0.11.0, < 2.a) - google-cloud-core (1.7.0) + google-cloud-core (1.7.1) google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) google-cloud-env (1.6.0) @@ -147,24 +150,24 @@ GEM os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) highline (2.0.3) - http-cookie (1.0.5) + http-cookie (1.0.7) domain_name (~> 0.5) httpclient (2.8.3) jmespath (1.6.2) - json (2.7.2) - jwt (2.8.1) + json (2.8.2) + jwt (2.9.3) base64 - mini_magick (4.12.0) + mini_magick (4.13.2) mini_mime (1.1.5) multi_json (1.15.0) multipart-post (2.4.1) - nanaimo (0.3.0) + nanaimo (0.4.0) naturally (2.2.1) nkf (0.2.0) - optparse (0.5.0) + optparse (0.6.0) os (1.1.4) plist (3.7.1) - public_suffix (5.0.5) + public_suffix (6.0.1) rake (13.2.1) representable (3.2.0) declarative (< 0.1.0) @@ -184,6 +187,7 @@ GEM simctl (1.6.10) CFPropertyList naturally + sysrandom (1.0.5) terminal-notifier (2.0.0) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) @@ -193,14 +197,15 @@ GEM tty-spinner (0.9.3) tty-cursor (~> 0.7) uber (0.1.0) - unicode-display_width (2.5.0) + unicode-display_width (2.6.0) word_wrap (1.0.0) - xcodeproj (1.19.0) + xcodeproj (1.27.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) colored2 (~> 3.1) - nanaimo (~> 0.3.0) + nanaimo (~> 0.4.0) + rexml (>= 3.3.6, < 4.0) xcpretty (0.3.0) rouge (~> 2.0.7) xcpretty-travis-formatter (1.0.1) diff --git a/ios/Gemfile.lock b/ios/Gemfile.lock index e662975a89..4168b7b944 100644 --- a/ios/Gemfile.lock +++ b/ios/Gemfile.lock @@ -10,20 +10,20 @@ GEM artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.3.0) - aws-partitions (1.959.0) - aws-sdk-core (3.201.3) + aws-partitions (1.1013.0) + aws-sdk-core (3.213.0) aws-eventstream (~> 1, >= 1.3.0) - aws-partitions (~> 1, >= 1.651.0) - aws-sigv4 (~> 1.8) + aws-partitions (~> 1, >= 1.992.0) + aws-sigv4 (~> 1.9) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.88.0) - aws-sdk-core (~> 3, >= 3.201.0) + aws-sdk-kms (1.96.0) + aws-sdk-core (~> 3, >= 3.210.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.156.0) - aws-sdk-core (~> 3, >= 3.201.0) + aws-sdk-s3 (1.173.0) + aws-sdk-core (~> 3, >= 3.210.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) - aws-sigv4 (1.9.1) + aws-sigv4 (1.10.1) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) base64 (0.2.0) @@ -38,8 +38,8 @@ GEM domain_name (0.6.20240107) dotenv (2.8.1) emoji_regex (3.2.3) - excon (0.111.0) - faraday (1.10.3) + excon (0.112.0) + faraday (1.10.4) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) faraday-excon (~> 1.1) @@ -65,10 +65,10 @@ GEM faraday-patron (1.0.0) faraday-rack (1.0.0) faraday-retry (1.0.3) - faraday_middleware (1.2.0) + faraday_middleware (1.2.1) faraday (~> 1.0) fastimage (2.3.1) - fastlane (2.222.0) + fastlane (2.225.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -84,6 +84,7 @@ GEM faraday-cookie_jar (~> 0.0.6) faraday_middleware (~> 1.0) fastimage (>= 2.1.0, < 3.0.0) + fastlane-sirp (>= 1.0.0) gh_inspector (>= 1.1.2, < 2.0.0) google-apis-androidpublisher_v3 (~> 0.3) google-apis-playcustomapp_v1 (~> 0.1) @@ -109,6 +110,8 @@ GEM xcodeproj (>= 1.13.0, < 2.0.0) xcpretty (~> 0.3.0) xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) + fastlane-sirp (1.0.0) + sysrandom (~> 1.0) gh_inspector (1.1.3) google-apis-androidpublisher_v3 (0.54.0) google-apis-core (>= 0.11.0, < 2.a) @@ -126,7 +129,7 @@ GEM google-apis-core (>= 0.11.0, < 2.a) google-apis-storage_v1 (0.31.0) google-apis-core (>= 0.11.0, < 2.a) - google-cloud-core (1.7.0) + google-cloud-core (1.7.1) google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) google-cloud-env (1.6.0) @@ -147,21 +150,21 @@ GEM os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) highline (2.0.3) - http-cookie (1.0.6) + http-cookie (1.0.7) domain_name (~> 0.5) httpclient (2.8.3) jmespath (1.6.2) - json (2.7.2) - jwt (2.8.2) + json (2.8.2) + jwt (2.9.3) base64 mini_magick (4.13.2) mini_mime (1.1.5) multi_json (1.15.0) multipart-post (2.4.1) - nanaimo (0.3.0) + nanaimo (0.4.0) naturally (2.2.1) nkf (0.2.0) - optparse (0.5.0) + optparse (0.6.0) os (1.1.4) plist (3.7.1) public_suffix (6.0.1) @@ -171,8 +174,7 @@ GEM trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (3.1.2) - rexml (3.3.3) - strscan + rexml (3.3.9) rouge (2.0.7) ruby2_keywords (0.0.5) rubyzip (2.3.2) @@ -185,7 +187,7 @@ GEM simctl (1.6.10) CFPropertyList naturally - strscan (3.1.0) + sysrandom (1.0.5) terminal-notifier (2.0.0) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) @@ -195,14 +197,15 @@ GEM tty-spinner (0.9.3) tty-cursor (~> 0.7) uber (0.1.0) - unicode-display_width (2.5.0) + unicode-display_width (2.6.0) word_wrap (1.0.0) - xcodeproj (1.19.0) + xcodeproj (1.27.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) colored2 (~> 3.1) - nanaimo (~> 0.3.0) + nanaimo (~> 0.4.0) + rexml (>= 3.3.6, < 4.0) xcpretty (0.3.0) rouge (~> 2.0.7) xcpretty-travis-formatter (1.0.1) From 21462ace9bb4b4c51b1296496480ace1ce54d8ce Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Sat, 23 Nov 2024 13:43:37 +0100 Subject: [PATCH 777/979] Add board prefs missing value fallback --- lib/src/model/settings/board_preferences.dart | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/src/model/settings/board_preferences.dart b/lib/src/model/settings/board_preferences.dart index 43c741ef93..f95bf51d93 100644 --- a/lib/src/model/settings/board_preferences.dart +++ b/lib/src/model/settings/board_preferences.dart @@ -121,6 +121,10 @@ class BoardPrefs with _$BoardPrefs implements Serializable { required bool coordinates, required bool pieceAnimation, required bool showMaterialDifference, + @JsonKey( + defaultValue: ClockPosition.right, + unknownEnumValue: ClockPosition.right, + ) required ClockPosition clockPosition, @JsonKey( defaultValue: PieceShiftMethod.either, @@ -131,7 +135,10 @@ class BoardPrefs with _$BoardPrefs implements Serializable { /// Whether to enable shape drawings on the board for games and puzzles. @JsonKey(defaultValue: true) required bool enableShapeDrawings, @JsonKey(defaultValue: true) required bool magnifyDraggedPiece, - @JsonKey(defaultValue: DragTargetKind.circle) + @JsonKey( + defaultValue: DragTargetKind.circle, + unknownEnumValue: DragTargetKind.circle, + ) required DragTargetKind dragTargetKind, @JsonKey( defaultValue: ShapeColor.green, From a3642454b178a43af3370472de3d4c5a8b1c48c2 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Sat, 23 Nov 2024 14:45:52 +0100 Subject: [PATCH 778/979] restore the PostFrameCallback for tree view removed by mistake --- lib/src/widgets/pgn.dart | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/lib/src/widgets/pgn.dart b/lib/src/widgets/pgn.dart index 49e2c59455..0cd4f75812 100644 --- a/lib/src/widgets/pgn.dart +++ b/lib/src/widgets/pgn.dart @@ -199,15 +199,17 @@ class _DebouncedPgnTreeViewState extends ConsumerState { } }); if (oldWidget.currentPath != widget.currentPath) { - if (currentMoveKey.currentContext != null) { - Scrollable.ensureVisible( - currentMoveKey.currentContext!, - duration: const Duration(milliseconds: 200), - curve: Curves.easeIn, - alignment: 0.5, - alignmentPolicy: ScrollPositionAlignmentPolicy.explicit, - ); - } + WidgetsBinding.instance.addPostFrameCallback((_) { + if (currentMoveKey.currentContext != null) { + Scrollable.ensureVisible( + currentMoveKey.currentContext!, + duration: const Duration(milliseconds: 200), + curve: Curves.easeIn, + alignment: 0.5, + alignmentPolicy: ScrollPositionAlignmentPolicy.explicit, + ); + } + }); } }); } From c72c7039804421177beda5f5edae4d37ea81a814 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Sat, 23 Nov 2024 14:52:35 +0100 Subject: [PATCH 779/979] add translations --- lib/src/view/broadcast/broadcast_screen.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_screen.dart b/lib/src/view/broadcast/broadcast_screen.dart index efcd02d318..ef41c9f70d 100644 --- a/lib/src/view/broadcast/broadcast_screen.dart +++ b/lib/src/view/broadcast/broadcast_screen.dart @@ -84,9 +84,9 @@ class _AndroidScreenState extends State<_AndroidScreen> title: Text(widget.broadcast.title), bottom: TabBar( controller: _tabController, - tabs: const [ - Tab(text: 'Overview'), - Tab(text: 'Boards'), + tabs: [ + Tab(text: context.l10n.broadcastOverview), + Tab(text: context.l10n.broadcastBoards), ], ), // TODO uncomment when eval bar is ready From 5b3dda4c800bd9ba451cf565f87bb7775448aa84 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Sat, 23 Nov 2024 15:14:22 +0100 Subject: [PATCH 780/979] add animation for boards thumbnails --- lib/src/view/broadcast/broadcast_boards_tab.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/view/broadcast/broadcast_boards_tab.dart b/lib/src/view/broadcast/broadcast_boards_tab.dart index 24a92979cc..79bb8b1807 100644 --- a/lib/src/view/broadcast/broadcast_boards_tab.dart +++ b/lib/src/view/broadcast/broadcast_boards_tab.dart @@ -136,6 +136,7 @@ class BroadcastPreview extends StatelessWidget { final playingSide = Setup.parseFen(game.fen).turn; return BoardThumbnail( + animationDuration: const Duration(milliseconds: 150), onTap: () { pushPlatformRoute( context, From ba243e1c72ffbf7e365e0ad1bfb2e4606ce6c8c6 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Sat, 23 Nov 2024 16:44:03 +0100 Subject: [PATCH 781/979] remove eval bar from this branch as it will not be part of broadcast first release --- .../broadcast/broadcast_preferences.dart | 46 ------------- .../view/broadcast/broadcast_boards_tab.dart | 36 +++------- lib/src/view/broadcast/broadcast_screen.dart | 58 ----------------- lib/src/widgets/board_thumbnail.dart | 65 ++----------------- lib/src/widgets/evaluation_bar.dart | 49 -------------- 5 files changed, 15 insertions(+), 239 deletions(-) delete mode 100644 lib/src/model/broadcast/broadcast_preferences.dart delete mode 100644 lib/src/widgets/evaluation_bar.dart diff --git a/lib/src/model/broadcast/broadcast_preferences.dart b/lib/src/model/broadcast/broadcast_preferences.dart deleted file mode 100644 index 1fe4fda5f2..0000000000 --- a/lib/src/model/broadcast/broadcast_preferences.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'broadcast_preferences.freezed.dart'; -part 'broadcast_preferences.g.dart'; - -@riverpod -class BroadcastPreferences extends _$BroadcastPreferences - with PreferencesStorage { - // ignore: avoid_public_notifier_properties - @override - PrefCategory get prefCategory => PrefCategory.broadcast; - - // ignore: avoid_public_notifier_properties - @override - BroadcastPrefs get defaults => BroadcastPrefs.defaults; - - @override - BroadcastPrefs fromJson(Map json) => - BroadcastPrefs.fromJson(json); - - @override - BroadcastPrefs build() { - return fetch(); - } - - Future toggleEvaluationBar() async { - return save(state.copyWith(showEvaluationBar: !state.showEvaluationBar)); - } -} - -@Freezed(fromJson: true, toJson: true) -class BroadcastPrefs with _$BroadcastPrefs implements Serializable { - const factory BroadcastPrefs({ - required bool showEvaluationBar, - }) = _BroadcastPrefs; - - static const defaults = BroadcastPrefs( - showEvaluationBar: true, - ); - - factory BroadcastPrefs.fromJson(Map json) => - _$BroadcastPrefsFromJson(json); -} diff --git a/lib/src/view/broadcast/broadcast_boards_tab.dart b/lib/src/view/broadcast/broadcast_boards_tab.dart index 79bb8b1807..f87280d299 100644 --- a/lib/src/view/broadcast/broadcast_boards_tab.dart +++ b/lib/src/view/broadcast/broadcast_boards_tab.dart @@ -4,9 +4,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; -// TODO remove when eval bar is ready -// ignore: unused_import -import 'package:lichess_mobile/src/model/broadcast/broadcast_preferences.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_round_controller.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/network/http.dart'; @@ -18,7 +15,6 @@ import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_game_screen.dart'; import 'package:lichess_mobile/src/widgets/board_thumbnail.dart'; import 'package:lichess_mobile/src/widgets/clock.dart'; -import 'package:lichess_mobile/src/widgets/evaluation_bar.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; // height of 1.0 is important because we need to determine the height of the text @@ -85,12 +81,6 @@ class BroadcastPreview extends StatelessWidget { @override Widget build(BuildContext context) { - // TODO uncomment when eval bar is ready - // final showEvaluationBar = ref.watch( - // broadcastPreferencesProvider.select((value) => value.showEvaluationBar), - // ); - // TODO remove when eval bar is ready - const showEvaluationBar = false; const numberLoadingBoards = 12; const boardSpacing = 10.0; // height of the text based on the font size @@ -100,7 +90,7 @@ class BroadcastPreview extends StatelessWidget { final headerAndFooterHeight = textHeight + _kPlayerWidgetPadding.vertical; final numberOfBoardsByRow = isTabletOrLarger(context) ? 4 : 2; final screenWidth = MediaQuery.sizeOf(context).width; - final boardWithMaybeEvalBarWidth = (screenWidth - + final boardWidth = (screenWidth - Styles.horizontalBodyPadding.horizontal - (numberOfBoardsByRow - 1) * boardSpacing) / numberOfBoardsByRow; @@ -112,23 +102,14 @@ class BroadcastPreview extends StatelessWidget { crossAxisCount: numberOfBoardsByRow, crossAxisSpacing: boardSpacing, mainAxisSpacing: boardSpacing, - mainAxisExtent: boardWithMaybeEvalBarWidth + 2 * headerAndFooterHeight, - childAspectRatio: 1 + evaluationBarAspectRatio, + mainAxisExtent: boardWidth + 2 * headerAndFooterHeight, ), itemBuilder: (context, index) { - final boardSize = boardWithMaybeEvalBarWidth - - (showEvaluationBar - // TODO remove when eval bar is ready - // ignore: dead_code - ? evaluationBarAspectRatio * boardWithMaybeEvalBarWidth - : 0); - if (games == null) { return BoardThumbnail.loading( - size: boardSize, - header: _PlayerWidgetLoading(width: boardWithMaybeEvalBarWidth), - footer: _PlayerWidgetLoading(width: boardWithMaybeEvalBarWidth), - showEvaluationBar: showEvaluationBar, + size: boardWidth, + header: _PlayerWidgetLoading(width: boardWidth), + footer: _PlayerWidgetLoading(width: boardWidth), ); } @@ -149,17 +130,16 @@ class BroadcastPreview extends StatelessWidget { }, orientation: Side.white, fen: game.fen, - showEvaluationBar: showEvaluationBar, lastMove: game.lastMove, - size: boardSize, + size: boardWidth, header: _PlayerWidget( - width: boardWithMaybeEvalBarWidth, + width: boardWidth, game: game, side: Side.black, playingSide: playingSide, ), footer: _PlayerWidget( - width: boardWithMaybeEvalBarWidth, + width: boardWidth, game: game, side: Side.white, playingSide: playingSide, diff --git a/lib/src/view/broadcast/broadcast_screen.dart b/lib/src/view/broadcast/broadcast_screen.dart index ef41c9f70d..caabed0d8d 100644 --- a/lib/src/view/broadcast/broadcast_screen.dart +++ b/lib/src/view/broadcast/broadcast_screen.dart @@ -5,19 +5,13 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; -import 'package:lichess_mobile/src/model/broadcast/broadcast_preferences.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_providers.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_boards_tab.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_overview_tab.dart'; -import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/adaptive_choice_picker.dart'; -import 'package:lichess_mobile/src/widgets/buttons.dart'; -import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; -import 'package:lichess_mobile/src/widgets/settings.dart'; class BroadcastScreen extends StatelessWidget { final Broadcast broadcast; @@ -89,8 +83,6 @@ class _AndroidScreenState extends State<_AndroidScreen> Tab(text: context.l10n.broadcastBoards), ], ), - // TODO uncomment when eval bar is ready - // actions: [_BroadcastSettingsButton()], ), body: TabBarView( controller: _tabController, @@ -172,7 +164,6 @@ class _CupertinoScreenState extends State<_CupertinoScreen> { } }, ), - trailing: _BroadcastSettingsButton(), ), child: Column( children: [ @@ -435,52 +426,3 @@ class _IOSTournamentAndRoundSelector extends ConsumerWidget { }; } } - -class _BroadcastSettingsButton extends StatelessWidget { - @override - Widget build(BuildContext context) => AppBarIconButton( - icon: const Icon(Icons.settings), - onPressed: () => showAdaptiveBottomSheet( - context: context, - isDismissible: true, - isScrollControlled: true, - showDragHandle: true, - builder: (_) => const _BroadcastSettingsBottomSheet(), - ), - semanticsLabel: context.l10n.settingsSettings, - ); -} - -class _BroadcastSettingsBottomSheet extends ConsumerWidget { - const _BroadcastSettingsBottomSheet(); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final broadcastPreferences = ref.watch(broadcastPreferencesProvider); - - return DraggableScrollableSheet( - initialChildSize: .6, - expand: false, - builder: (context, scrollController) => ListView( - controller: scrollController, - children: [ - PlatformListTile( - title: - Text(context.l10n.settingsSettings, style: Styles.sectionTitle), - subtitle: const SizedBox.shrink(), - ), - const SizedBox(height: 8.0), - SwitchSettingTile( - title: const Text('Evaluation bar'), - value: broadcastPreferences.showEvaluationBar, - onChanged: (value) { - ref - .read(broadcastPreferencesProvider.notifier) - .toggleEvaluationBar(); - }, - ), - ], - ), - ); - } -} diff --git a/lib/src/widgets/board_thumbnail.dart b/lib/src/widgets/board_thumbnail.dart index 041cea755f..2095e4d797 100644 --- a/lib/src/widgets/board_thumbnail.dart +++ b/lib/src/widgets/board_thumbnail.dart @@ -4,7 +4,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; -import 'package:lichess_mobile/src/widgets/evaluation_bar.dart'; /// A board thumbnail widget class BoardThumbnail extends ConsumerStatefulWidget { @@ -12,8 +11,6 @@ class BoardThumbnail extends ConsumerStatefulWidget { required this.size, required this.orientation, required this.fen, - this.showEvaluationBar = false, - this.whiteWinningChances, this.header, this.footer, this.lastMove, @@ -25,9 +22,7 @@ class BoardThumbnail extends ConsumerStatefulWidget { required this.size, this.header, this.footer, - this.showEvaluationBar = false, - }) : whiteWinningChances = null, - orientation = Side.white, + }) : orientation = Side.white, fen = kInitialFEN, lastMove = null, onTap = null, @@ -42,12 +37,6 @@ class BoardThumbnail extends ConsumerStatefulWidget { /// FEN string describing the position of the board. final String fen; - /// Whether the evaluation bar should be shown. - final bool showEvaluationBar; - - /// Winning chances from the white pov for the given fen. - final double? whiteWinningChances; - /// Last move played, used to highlight corresponding squares. final Move? lastMove; @@ -91,13 +80,8 @@ class _BoardThumbnailState extends ConsumerState { lastMove: widget.lastMove as NormalMove?, settings: ChessboardSettings( enableCoordinates: false, - borderRadius: (widget.showEvaluationBar) - ? const BorderRadius.only( - topLeft: Radius.circular(4.0), - bottomLeft: Radius.circular(4.0), - ) - : const BorderRadius.all(Radius.circular(4.0)), - boxShadow: (widget.showEvaluationBar) ? [] : boardShadows, + borderRadius: const BorderRadius.all(Radius.circular(4.0)), + boxShadow: boardShadows, animationDuration: widget.animationDuration!, pieceAssets: boardPrefs.pieceSet.assets, colorScheme: boardPrefs.boardTheme.colors, @@ -109,13 +93,8 @@ class _BoardThumbnailState extends ConsumerState { orientation: widget.orientation, lastMove: widget.lastMove as NormalMove?, enableCoordinates: false, - borderRadius: (widget.showEvaluationBar) - ? const BorderRadius.only( - topLeft: Radius.circular(4.0), - bottomLeft: Radius.circular(4.0), - ) - : const BorderRadius.all(Radius.circular(4.0)), - boxShadow: (widget.showEvaluationBar) ? [] : boardShadows, + borderRadius: const BorderRadius.all(Radius.circular(4.0)), + boxShadow: boardShadows, pieceAssets: boardPrefs.pieceSet.assets, colorScheme: boardPrefs.boardTheme.colors, ); @@ -134,45 +113,15 @@ class _BoardThumbnailState extends ConsumerState { ) : board; - final boardWithMaybeEvalBar = widget.showEvaluationBar - ? DecoratedBox( - decoration: BoxDecoration(boxShadow: boardShadows), - child: Row( - children: [ - Expanded(child: maybeTappableBoard), - ClipRRect( - borderRadius: const BorderRadius.only( - topRight: Radius.circular(4.0), - bottomRight: Radius.circular(4.0), - ), - clipBehavior: Clip.hardEdge, - child: (widget.whiteWinningChances != null) - ? EvaluationBar( - height: widget.size, - whiteWinnigChances: widget.whiteWinningChances!, - ) - : SizedBox( - height: widget.size, - width: widget.size * evaluationBarAspectRatio, - child: ColoredBox( - color: Colors.grey.withValues(alpha: 0.6), - ), - ), - ), - ], - ), - ) - : maybeTappableBoard; - return widget.header != null || widget.footer != null ? Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (widget.header != null) widget.header!, - boardWithMaybeEvalBar, + maybeTappableBoard, if (widget.footer != null) widget.footer!, ], ) - : boardWithMaybeEvalBar; + : maybeTappableBoard; } } diff --git a/lib/src/widgets/evaluation_bar.dart b/lib/src/widgets/evaluation_bar.dart deleted file mode 100644 index 00374e6c9b..0000000000 --- a/lib/src/widgets/evaluation_bar.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:flutter/material.dart'; - -const evaluationBarAspectRatio = 1 / 20; - -class EvaluationBar extends StatelessWidget { - final double height; - final double whiteWinnigChances; - - const EvaluationBar({ - super.key, - required this.height, - required this.whiteWinnigChances, - }); - - const EvaluationBar.loading({ - super.key, - required this.height, - }) : whiteWinnigChances = 0; - - @override - Widget build(BuildContext context) { - final whiteBarHeight = height * (whiteWinnigChances + 1) / 2; - - return Stack( - alignment: Alignment.center, - children: [ - Column( - children: [ - SizedBox( - height: height - whiteBarHeight, - width: height * evaluationBarAspectRatio, - child: ColoredBox(color: Colors.black.withValues(alpha: 0.6)), - ), - SizedBox( - height: whiteBarHeight, - width: height * evaluationBarAspectRatio, - child: ColoredBox(color: Colors.white.withValues(alpha: 0.6)), - ), - ], - ), - SizedBox( - height: height / 100, - width: height * evaluationBarAspectRatio, - child: const ColoredBox(color: Colors.red), - ), - ], - ); - } -} From 85be21009828659d88a7e010d3378883123c3cdf Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Sat, 23 Nov 2024 21:57:35 +0100 Subject: [PATCH 782/979] watch whole state instead of using select --- .../view/broadcast/broadcast_game_screen.dart | 13 +++-------- .../broadcast/broadcast_game_tree_view.dart | 23 ++++--------------- 2 files changed, 8 insertions(+), 28 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_game_screen.dart b/lib/src/view/broadcast/broadcast_game_screen.dart index ee8c16f517..fb3c645b3a 100644 --- a/lib/src/view/broadcast/broadcast_game_screen.dart +++ b/lib/src/view/broadcast/broadcast_game_screen.dart @@ -210,14 +210,7 @@ class _BroadcastBoardState extends ConsumerState<_BroadcastBoard> { broadcastGameControllerProvider(widget.roundId, widget.gameId); final broadcastAnalysisState = ref.watch(ctrlProvider).requireValue; final boardPrefs = ref.watch(boardPreferencesProvider); - final showBestMoveArrow = ref.watch( - analysisPreferencesProvider.select( - (value) => value.showBestMoveArrow, - ), - ); - final showAnnotationsOnBoard = ref.watch( - analysisPreferencesProvider.select((value) => value.showAnnotations), - ); + final analysisPrefs = ref.watch(analysisPreferencesProvider); final evalBestMoves = ref.watch( engineEvaluationProvider.select((s) => s.eval?.bestMoves), @@ -230,7 +223,7 @@ class _BroadcastBoardState extends ConsumerState<_BroadcastBoard> { final sanMove = currentNode.sanMove; - final ISet bestMoveShapes = showBestMoveArrow && + final ISet bestMoveShapes = analysisPrefs.showBestMoveArrow && broadcastAnalysisState.isLocalEvaluationEnabled && bestMoves != null ? computeBestMoveShapes( @@ -263,7 +256,7 @@ class _BroadcastBoardState extends ConsumerState<_BroadcastBoard> { ), shapes: userShapes.union(bestMoveShapes), annotations: - showAnnotationsOnBoard && sanMove != null && annotation != null + analysisPrefs.showAnnotations && sanMove != null && annotation != null ? altCastles.containsKey(sanMove.move.uci) ? IMap({ Move.parse(altCastles[sanMove.move.uci]!)!.to: annotation, diff --git a/lib/src/view/broadcast/broadcast_game_tree_view.dart b/lib/src/view/broadcast/broadcast_game_tree_view.dart index 691baac605..f691d03d5e 100644 --- a/lib/src/view/broadcast/broadcast_game_tree_view.dart +++ b/lib/src/view/broadcast/broadcast_game_tree_view.dart @@ -19,29 +19,16 @@ class BroadcastGameTreeView extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final ctrlProvider = broadcastGameControllerProvider(roundId, gameId); - - final root = - ref.watch(ctrlProvider.select((value) => value.requireValue.root)); - - final currentPath = ref - .watch(ctrlProvider.select((value) => value.requireValue.currentPath)); - - final broadcastLivePath = ref.watch( - ctrlProvider.select((value) => value.requireValue.broadcastLivePath), - ); - - final pgnRootComments = ref.watch( - ctrlProvider.select((value) => value.requireValue.pgnRootComments), - ); + final broadcastGameState = ref.watch(ctrlProvider).requireValue; final analysisPrefs = ref.watch(analysisPreferencesProvider); return SingleChildScrollView( child: DebouncedPgnTreeView( - root: root, - currentPath: currentPath, - broadcastLivePath: broadcastLivePath, - pgnRootComments: pgnRootComments, + root: broadcastGameState.root, + currentPath: broadcastGameState.currentPath, + broadcastLivePath: broadcastGameState.broadcastLivePath, + pgnRootComments: broadcastGameState.pgnRootComments, shouldShowAnnotations: analysisPrefs.showAnnotations, notifier: ref.read(ctrlProvider.notifier), ), From b8554119b199a9115f7f6faf9c81010ef0e936b4 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Sun, 24 Nov 2024 10:21:06 +0100 Subject: [PATCH 783/979] fix(study): improve UX when loading next chapter --- lib/src/model/study/study_controller.dart | 1 - lib/src/view/study/study_bottom_bar.dart | 71 ++++++++++++++++++----- 2 files changed, 57 insertions(+), 15 deletions(-) diff --git a/lib/src/model/study/study_controller.dart b/lib/src/model/study/study_controller.dart index 5648833799..29d7e0883c 100644 --- a/lib/src/model/study/study_controller.dart +++ b/lib/src/model/study/study_controller.dart @@ -58,7 +58,6 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { } Future goToChapter(StudyChapterId chapterId) async { - state = const AsyncValue.loading(); state = AsyncValue.data( await _fetchChapter( state.requireValue.study.id, diff --git a/lib/src/view/study/study_bottom_bar.dart b/lib/src/view/study/study_bottom_bar.dart index 2c8d4c7a12..fb38e51b29 100644 --- a/lib/src/view/study/study_bottom_bar.dart +++ b/lib/src/view/study/study_bottom_bar.dart @@ -64,13 +64,10 @@ class _AnalysisBottomBar extends ConsumerWidget { showTooltip: false, ), ), - BottomBarButton( - onTap: state.hasNextChapter - ? ref.read(studyControllerProvider(id).notifier).nextChapter - : null, - icon: Icons.play_arrow, - label: context.l10n.studyNextChapter, - showLabel: true, + _NextChapterButton( + id: id, + chapterId: state.study.chapter.id, + hasNextChapter: state.hasNextChapter, blink: !state.isIntroductoryChapter && state.isAtEndOfChapter && state.hasNextChapter, @@ -150,13 +147,10 @@ class _GamebookBottomBar extends ConsumerWidget { label: context.l10n.studyPlayAgain, showLabel: true, ), - BottomBarButton( - onTap: state.hasNextChapter - ? ref.read(studyControllerProvider(id).notifier).nextChapter - : null, - icon: Icons.play_arrow, - label: context.l10n.studyNextChapter, - showLabel: true, + _NextChapterButton( + id: id, + chapterId: state.study.chapter.id, + hasNextChapter: state.hasNextChapter, blink: !state.isIntroductoryChapter && state.hasNextChapter, ), if (!state.isIntroductoryChapter) @@ -185,3 +179,52 @@ class _GamebookBottomBar extends ConsumerWidget { ); } } + +class _NextChapterButton extends ConsumerStatefulWidget { + const _NextChapterButton({ + required this.id, + required this.chapterId, + required this.hasNextChapter, + required this.blink, + }); + + final StudyId id; + final StudyChapterId chapterId; + final bool hasNextChapter; + final bool blink; + + @override + ConsumerState<_NextChapterButton> createState() => _NextChapterButtonState(); +} + +class _NextChapterButtonState extends ConsumerState<_NextChapterButton> { + bool isLoading = false; + + @override + void didUpdateWidget(_NextChapterButton oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.chapterId != widget.chapterId) { + setState(() => isLoading = false); + } + } + + @override + Widget build(BuildContext context) { + return isLoading + ? const Center(child: CircularProgressIndicator()) + : BottomBarButton( + onTap: widget.hasNextChapter + ? () { + ref + .read(studyControllerProvider(widget.id).notifier) + .nextChapter(); + setState(() => isLoading = true); + } + : null, + icon: Icons.play_arrow, + label: context.l10n.studyNextChapter, + showLabel: true, + blink: widget.blink, + ); + } +} From a27795989fbfd8785be44b99cfca493d629eb286 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 25 Nov 2024 16:40:51 +0100 Subject: [PATCH 784/979] Fix correspondence analysis --- .../model/analysis/analysis_controller.dart | 63 +++++++-- lib/src/model/game/game_controller.dart | 20 ++- lib/src/view/analysis/server_analysis.dart | 2 +- lib/src/view/game/game_body.dart | 12 +- test/model/game/game_socket_example_data.dart | 48 +++++-- test/view/game/game_screen_test.dart | 129 +++++++++++++++++- 6 files changed, 233 insertions(+), 41 deletions(-) diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index 91b36e52de..257c7d0f58 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -113,6 +113,8 @@ class AnalysisController extends _$AnalysisController ? pgnHeaders['Result'] != '*' : options.standalone!.isComputerAnalysisAllowed; + final List> openingFutures = []; + _root = Root.fromPgnGame( game, isLichessAnalysis: options.isLichessGameAnalysis, @@ -125,9 +127,29 @@ class AnalysisController extends _$AnalysisController path = path + branch.id; lastMove = branch.sanMove.move; } + if (isMainline && opening == null && branch.position.ply <= 10) { + openingFutures.add(_fetchOpening(root, path)); + } }, ); + // wait for the opening to be fetched to recompute the branch opening + Future.wait(openingFutures).then((list) { + bool hasOpening = false; + for (final updated in list) { + if (updated != null) { + hasOpening = true; + final (path, opening) = updated; + _root.updateAt(path, (node) => node.opening = opening); + } + } + return hasOpening; + }).then((hasOpening) { + if (hasOpening) { + _setPath(state.requireValue.currentPath); + } + }); + final currentPath = options.initialMoveCursor == null ? _root.mainlinePath : path; final currentNode = _root.nodeAt(currentPath); @@ -499,18 +521,10 @@ class AnalysisController extends _$AnalysisController } if (currentNode.opening == null && currentNode.position.ply <= 30) { - _fetchOpening(_root, path).then((opening) { - if (opening != null) { - _root.updateAt(path, (node) => node.opening = opening); - - final curState = state.requireValue; - if (curState.currentPath == path) { - state = AsyncData( - curState.copyWith( - currentNode: AnalysisCurrentNode.fromNode(_root.nodeAt(path)), - ), - ); - } + _fetchOpening(_root, path).then((value) { + if (value != null) { + final (path, opening) = value; + _updateOpening(path, opening); } }); } @@ -545,14 +559,35 @@ class AnalysisController extends _$AnalysisController } } - Future _fetchOpening(Node fromNode, UciPath path) async { + Future<(UciPath, FullOpening)?> _fetchOpening( + Node fromNode, + UciPath path, + ) async { if (!kOpeningAllowedVariants.contains(_variant)) return null; final moves = fromNode.branchesOn(path).map((node) => node.sanMove.move); if (moves.isEmpty) return null; if (moves.length > 40) return null; - return ref.read(openingServiceProvider).fetchFromMoves(moves); + final opening = + await ref.read(openingServiceProvider).fetchFromMoves(moves); + if (opening != null) { + return (path, opening); + } + return null; + } + + void _updateOpening(UciPath path, FullOpening opening) { + _root.updateAt(path, (node) => node.opening = opening); + + final curState = state.requireValue; + if (curState.currentPath == path) { + state = AsyncData( + curState.copyWith( + currentNode: AnalysisCurrentNode.fromNode(_root.nodeAt(path)), + ), + ); + } } void _startEngineEval() { diff --git a/lib/src/model/game/game_controller.dart b/lib/src/model/game/game_controller.dart index 7cc70bf51f..46cbb5d52b 100644 --- a/lib/src/model/game/game_controller.dart +++ b/lib/src/model/game/game_controller.dart @@ -1169,9 +1169,19 @@ class GameState with _$GameState { String get analysisPgn => game.makePgn(); - AnalysisOptions get analysisOptions => AnalysisOptions( - orientation: game.youAre ?? Side.white, - initialMoveCursor: stepCursor, - gameId: gameFullId.gameId, - ); + AnalysisOptions get analysisOptions => game.finished + ? AnalysisOptions( + orientation: game.youAre ?? Side.white, + initialMoveCursor: stepCursor, + gameId: gameFullId.gameId, + ) + : AnalysisOptions( + orientation: game.youAre ?? Side.white, + initialMoveCursor: stepCursor, + standalone: ( + pgn: game.makePgn(), + variant: game.meta.variant, + isComputerAnalysisAllowed: false, + ), + ); } diff --git a/lib/src/view/analysis/server_analysis.dart b/lib/src/view/analysis/server_analysis.dart index ae54ce5de7..bf9ee77493 100644 --- a/lib/src/view/analysis/server_analysis.dart +++ b/lib/src/view/analysis/server_analysis.dart @@ -35,7 +35,7 @@ class ServerAnalysisSummary extends ConsumerWidget { .watch(ctrlProvider.select((value) => value.requireValue.pgnHeaders)); final currentGameAnalysis = ref.watch(currentAnalysisProvider); - if (analysisPrefs.enableComputerAnalysis == false) { + if (analysisPrefs.enableComputerAnalysis == false || !canShowGameSummary) { return Center( child: Padding( padding: const EdgeInsets.symmetric(vertical: 16.0), diff --git a/lib/src/view/game/game_body.dart b/lib/src/view/game/game_body.dart index e6b6a6b341..2e468db3d1 100644 --- a/lib/src/view/game/game_body.dart +++ b/lib/src/view/game/game_body.dart @@ -568,9 +568,8 @@ class _GameBottomBar extends ConsumerWidget { onTap: () { pushPlatformRoute( context, - builder: (_) => AnalysisScreen( - options: gameState.analysisOptions, - ), + builder: (_) => + AnalysisScreen(options: gameState.analysisOptions), ); }, ) @@ -700,11 +699,8 @@ class _GameBottomBar extends ConsumerWidget { onPressed: (context) { pushPlatformRoute( context, - builder: (_) => AnalysisScreen( - options: gameState.analysisOptions.copyWith( - gameId: gameState.game.id, - ), - ), + builder: (_) => + AnalysisScreen(options: gameState.analysisOptions), ); }, ), diff --git a/test/model/game/game_socket_example_data.dart b/test/model/game/game_socket_example_data.dart index f4f3cc4c95..f841c65cd5 100644 --- a/test/model/game/game_socket_example_data.dart +++ b/test/model/game/game_socket_example_data.dart @@ -10,6 +10,12 @@ typedef FullEventTestClock = ({ Duration black, }); +typedef FullEventTestCorrespondenceClock = ({ + Duration white, + Duration black, + int daysPerTurn, +}); + String makeFullEvent( GameId id, String pgn, { @@ -17,7 +23,7 @@ String makeFullEvent( required String blackUserName, int socketVersion = 0, Side? youAre, - FullEventTestClock clock = const ( + FullEventTestClock? clock = const ( running: false, initial: Duration(minutes: 3), increment: Duration(seconds: 2), @@ -25,8 +31,33 @@ String makeFullEvent( white: Duration(minutes: 3), black: Duration(minutes: 3), ), + FullEventTestCorrespondenceClock? correspondenceClock, }) { final youAreStr = youAre != null ? '"youAre": "${youAre.name}",' : ''; + final clockStr = clock != null + ? ''' + "clock": { + "running": ${clock.running}, + "initial": ${clock.initial.inSeconds}, + "increment": ${clock.increment.inSeconds}, + "white": ${(clock.white.inMilliseconds / 1000).toStringAsFixed(2)}, + "black": ${(clock.black.inMilliseconds / 1000).toStringAsFixed(2)}, + "emerg": 30, + "moretime": 15 + }, +''' + : ''; + + final correspondenceClockStr = correspondenceClock != null + ? ''' + "correspondence": { + "daysPerTurn": ${correspondenceClock.daysPerTurn}, + "white": ${(correspondenceClock.white.inMilliseconds / 1000).toStringAsFixed(2)}, + "black": ${(correspondenceClock.black.inMilliseconds / 1000).toStringAsFixed(2)} + }, +''' + : ''; + return ''' { "t": "full", @@ -38,8 +69,8 @@ String makeFullEvent( "name": "Standard", "short": "Std" }, - "speed": "blitz", - "perf": "blitz", + "speed": "${clock != null ? 'blitz' : 'correspondence'}", + "perf": "${clock != null ? 'blitz' : 'correspondence'}", "rated": false, "source": "lobby", "status": { @@ -67,17 +98,10 @@ String makeFullEvent( }, "onGame": true }, + $clockStr + $correspondenceClockStr $youAreStr "socket": $socketVersion, - "clock": { - "running": ${clock.running}, - "initial": ${clock.initial.inSeconds}, - "increment": ${clock.increment.inSeconds}, - "white": ${(clock.white.inMilliseconds / 1000).toStringAsFixed(2)}, - "black": ${(clock.black.inMilliseconds / 1000).toStringAsFixed(2)}, - "emerg": 30, - "moretime": 15 - }, "expiration": { "idleMillis": 245, "millisToMove": 30000 diff --git a/test/view/game/game_screen_test.dart b/test/view/game/game_screen_test.dart index 18d2c45d88..03dd851370 100644 --- a/test/view/game/game_screen_test.dart +++ b/test/view/game/game_screen_test.dart @@ -1,16 +1,22 @@ +import 'dart:convert'; + import 'package:chessground/chessground.dart'; import 'package:dartchess/dartchess.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; +import 'package:lichess_mobile/src/model/game/game_socket_events.dart'; import 'package:lichess_mobile/src/model/lobby/game_seek.dart'; import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/network/socket.dart'; +import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/view/game/game_screen.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/clock.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:mocktail/mocktail.dart'; import '../../model/game/game_socket_example_data.dart'; @@ -414,6 +420,91 @@ void main() { await tester.pump(const Duration(seconds: 500)); }); }); + + group('Opening analysis', () { + testWidgets('is not possible for an unfinished real time game', + (WidgetTester tester) async { + final fakeSocket = FakeWebSocketChannel(); + await createTestGame( + fakeSocket, + tester, + pgn: + 'e4 e5 Nf3 Nc6 Bc4 Nf6 Ng5 d5 exd5 Na5 Bb5+ c6 dxc6 bxc6 Qf3 Rb8 Bd3', + socketVersion: 0, + ); + expect(find.byType(Chessboard), findsOneWidget); + await tester.tap(find.byIcon(Icons.menu)); + await tester.pump(const Duration(milliseconds: 500)); + expect(find.byType(Dialog), findsOneWidget); + expect(find.text('Analysis board'), findsNothing); + }); + + testWidgets('for an unfinished correspondence game', + (WidgetTester tester) async { + final fakeSocket = FakeWebSocketChannel(); + await createTestGame( + fakeSocket, + tester, + pgn: + 'e4 e5 Nf3 Nc6 Bc4 Nf6 Ng5 d5 exd5 Na5 Bb5+ c6 dxc6 bxc6 Qf3 Rb8 Bd3', + clock: null, + correspondenceClock: ( + daysPerTurn: 3, + white: const Duration(days: 3), + black: const Duration(days: 2, hours: 22, minutes: 49, seconds: 59), + ), + socketVersion: 0, + ); + expect(find.byType(Chessboard), findsOneWidget); + expect(find.byKey(const Key('d3-whitebishop')), findsOneWidget); + expect(find.byKey(const Key('b5-lastMove')), findsOneWidget); + expect(find.byKey(const Key('d3-lastMove')), findsOneWidget); + await tester.tap(find.byIcon(Icons.menu)); + await tester.pump(const Duration(milliseconds: 500)); + expect(find.byType(Dialog), findsOneWidget); + await tester.tap(find.text('Analysis board')); + await tester.pumpAndSettle(); // wait for analysis screen to open + expect( + find.widgetWithText(PlatformAppBar, 'Analysis board'), + findsOneWidget, + ); // analysis screen is now open + expect(find.byKey(const Key('f3-whitequeen')), findsOneWidget); + expect(find.byKey(const Key('d3-whitebishop')), findsOneWidget); + expect(find.byKey(const Key('b5-lastMove')), findsOneWidget); + expect(find.byKey(const Key('d3-lastMove')), findsOneWidget); + await tester.tap(find.byIcon(LichessIcons.flow_cascade)); + await tester.pumpAndSettle(); // wait for the moves tab menu to open + expect(find.text('Moves played'), findsOneWidget); + // computer analysis is not available when game is not finished + expect(find.text('Computer analysis'), findsNothing); + }); + + testWidgets('for a finished game', (WidgetTester tester) async { + final fakeSocket = FakeWebSocketChannel(); + await loadFinishedTestGame(fakeSocket, tester); + expect(find.byType(Chessboard), findsOneWidget); + expect(find.byKey(const Key('e6-whitequeen')), findsOneWidget); + expect(find.byKey(const Key('d5-lastMove')), findsOneWidget); + expect(find.byKey(const Key('e6-lastMove')), findsOneWidget); + await tester.pump(const Duration(milliseconds: 500)); // wait for popup + await tester.tap(find.text('Analysis board')); + await tester.pumpAndSettle(); // wait for analysis screen to open + expect( + find.widgetWithText(PlatformAppBar, 'Analysis board'), + findsOneWidget, + ); // analysis screen is now open + expect(find.byKey(const Key('e6-whitequeen')), findsOneWidget); + expect(find.byKey(const Key('d5-lastMove')), findsOneWidget); + expect(find.byKey(const Key('e6-lastMove')), findsOneWidget); + await tester.tap(find.byIcon(LichessIcons.flow_cascade)); + await tester.pumpAndSettle(); // wait for the moves tab menu to open + expect(find.text('Moves played'), findsOneWidget); + expect( + find.text('Computer analysis'), + findsOneWidget, + ); // computer analysis is available + }); + }); } Finder findClockWithTime(String text, {bool skipOffstage = true}) { @@ -455,7 +546,7 @@ Future createTestGame( Side? youAre = Side.white, String? pgn, int socketVersion = 0, - FullEventTestClock clock = const ( + FullEventTestClock? clock = const ( running: false, initial: Duration(minutes: 3), increment: Duration(seconds: 2), @@ -463,6 +554,7 @@ Future createTestGame( black: Duration(minutes: 3), emerg: Duration(seconds: 30), ), + FullEventTestCorrespondenceClock? correspondenceClock, List? overrides, }) async { final app = await makeTestProviderScopeApp( @@ -491,7 +583,42 @@ Future createTestGame( youAre: youAre, socketVersion: socketVersion, clock: clock, + correspondenceClock: correspondenceClock, ), ]); await tester.pump(const Duration(milliseconds: 10)); } + +Future loadFinishedTestGame( + FakeWebSocketChannel socket, + WidgetTester tester, { + String serverFullEvent = _finishedGameFullEvent, + List? overrides, +}) async { + final json = jsonDecode(serverFullEvent) as Map; + final gameId = + GameFullEvent.fromJson(json['d'] as Map).game.id; + final app = await makeTestProviderScopeApp( + tester, + home: GameScreen( + initialGameId: GameFullId('${gameId.value}test'), + ), + overrides: [ + lichessClientProvider.overrideWith((ref) => LichessClient(client, ref)), + webSocketChannelFactoryProvider.overrideWith((ref) { + return FakeWebSocketChannelFactory((_) => socket); + }), + ...?overrides, + ], + ); + await tester.pumpWidget(app); + await tester.pump(const Duration(milliseconds: 10)); + await socket.connectionEstablished; + + socket.addIncomingMessages([serverFullEvent]); + await tester.pump(const Duration(milliseconds: 10)); +} + +const _finishedGameFullEvent = ''' +{"t":"full","d":{"game":{"id":"CCW6EEru","variant":{"key":"standard","name":"Standard","short":"Std"},"speed":"bullet","perf":"bullet","rated":true,"fen":"6kr/p1p2rpp/4Q3/2b1p3/8/2P5/P2N1PPP/R3R1K1 b - - 0 22","turns":43,"source":"lobby","status":{"id":31,"name":"resign"},"createdAt":1706185945680,"winner":"white","pgn":"e4 e5 Nf3 Nc6 Bc4 Bc5 b4 Bxb4 c3 Ba5 d4 Bb6 Ba3 Nf6 Qb3 d6 Bxf7+ Kf8 O-O Qe7 Nxe5 Nxe5 dxe5 Be6 Bxe6 Nxe4 Re1 Nc5 Bxc5 Bxc5 Qxb7 Re8 Bh3 dxe5 Qf3+ Kg8 Nd2 Rf8 Qd5+ Rf7 Be6 Qxe6 Qxe6"},"white":{"user":{"name":"veloce","id":"veloce"},"rating":1789,"ratingDiff":9},"black":{"user":{"name":"chabrot","id":"chabrot"},"rating":1810,"ratingDiff":-9},"socket":0,"clock":{"running":false,"initial":120,"increment":1,"white":31.2,"black":27.42,"emerg":15,"moretime":15},"takebackable":true,"youAre":"white","prefs":{"autoQueen":2,"zen":2,"confirmResign":true,"enablePremove":true},"chat":{"lines":[]}},"v": 0} +'''; From 0fcdfaaad78e68625a3c6b1e66fd1cd868f7e19a Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 25 Nov 2024 16:56:51 +0100 Subject: [PATCH 785/979] Ensure async --- lib/src/model/analysis/analysis_controller.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index 257c7d0f58..1d46fbd20d 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -146,7 +146,9 @@ class AnalysisController extends _$AnalysisController return hasOpening; }).then((hasOpening) { if (hasOpening) { - _setPath(state.requireValue.currentPath); + scheduleMicrotask(() { + _setPath(state.requireValue.currentPath); + }); } }); From ddf02bfdba2c9968fbd08f8ffdbc7065234d5eb6 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 25 Nov 2024 17:16:45 +0100 Subject: [PATCH 786/979] [Study] return an error message when variant is not supported Closes #1182 --- lib/src/view/study/study_screen.dart | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/lib/src/view/study/study_screen.dart b/lib/src/view/study/study_screen.dart index 04ce8608d2..44a9478c56 100644 --- a/lib/src/view/study/study_screen.dart +++ b/lib/src/view/study/study_screen.dart @@ -177,6 +177,24 @@ class _Body extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final studyState = ref.watch(studyControllerProvider(id)).requireValue; + final variant = studyState.variant; + if (!variant.isReadSupported) { + return DefaultTabController( + length: 1, + child: AnalysisLayout( + boardBuilder: (context, boardSize, borderRadius) => SizedBox.square( + dimension: boardSize, + child: Center( + child: Text( + '${variant.label} is not supported yet.', + ), + ), + ), + children: const [SizedBox.shrink()], + ), + ); + } + final analysisPrefs = ref.watch(analysisPreferencesProvider); final showEvaluationGauge = analysisPrefs.showEvaluationGauge; final numEvalLines = analysisPrefs.numEvalLines; From 5a08dccbec8938894bd16fdf305ab3ad4dae2705 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 26 Nov 2024 10:41:01 +0100 Subject: [PATCH 787/979] Improve explorer loading states --- .../view/analysis/opening_explorer_view.dart | 13 ++++- .../opening_explorer_screen.dart | 52 ++++++++++++------- 2 files changed, 44 insertions(+), 21 deletions(-) diff --git a/lib/src/view/analysis/opening_explorer_view.dart b/lib/src/view/analysis/opening_explorer_view.dart index df320b632a..9671292174 100644 --- a/lib/src/view/analysis/opening_explorer_view.dart +++ b/lib/src/view/analysis/opening_explorer_view.dart @@ -229,8 +229,19 @@ class _OpeningExplorerView extends StatelessWidget { @override Widget build(BuildContext context) { + final brightness = Theme.of(context).brightness; final loadingOverlay = Positioned.fill( - child: IgnorePointer(ignoring: !isLoading), + child: IgnorePointer( + ignoring: !isLoading, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 400), + curve: Curves.fastOutSlowIn, + opacity: isLoading ? 0.20 : 0.0, + child: ColoredBox( + color: brightness == Brightness.dark ? Colors.black : Colors.white, + ), + ), + ), ); return Stack( diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index 063a134d3b..6d3cb21479 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -334,9 +334,21 @@ class _OpeningExplorerView extends StatelessWidget { : defaultBoardSize; final isLandscape = aspectRatio > 1; - + final brightness = Theme.of(context).brightness; final loadingOverlay = Positioned.fill( - child: IgnorePointer(ignoring: !isLoading), + child: IgnorePointer( + ignoring: !isLoading, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 400), + curve: Curves.fastOutSlowIn, + opacity: isLoading ? 0.2 : 0.0, + child: ColoredBox( + color: brightness == Brightness.dark + ? Colors.black + : Colors.white, + ), + ), + ), ); if (isLandscape) { @@ -388,28 +400,28 @@ class _OpeningExplorerView extends StatelessWidget { ], ); } else { - return Stack( + return ListView( + padding: isTablet + ? const EdgeInsets.symmetric( + horizontal: kTabletBoardTableSidePadding, + ) + : EdgeInsets.zero, children: [ - ListView( - padding: isTablet - ? const EdgeInsets.symmetric( - horizontal: kTabletBoardTableSidePadding, - ) - : EdgeInsets.zero, + GestureDetector( + // disable scrolling when dragging the board + onVerticalDragStart: (_) {}, + child: AnalysisBoard( + options, + boardSize, + shouldReplaceChildOnUserMove: true, + ), + ), + Stack( children: [ - GestureDetector( - // disable scrolling when dragging the board - onVerticalDragStart: (_) {}, - child: AnalysisBoard( - options, - boardSize, - shouldReplaceChildOnUserMove: true, - ), - ), - ...children, + ListBody(children: children), + if (isLoading) loadingOverlay, ], ), - loadingOverlay, ], ); } From 4282f1a1adf9d3d53ebff0d56f4d3f6fb58d1d07 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 26 Nov 2024 12:19:13 +0100 Subject: [PATCH 788/979] Implement analysis search time Closes #670 --- .../model/analysis/analysis_controller.dart | 20 ++ .../model/analysis/analysis_preferences.dart | 39 +++ lib/src/model/common/eval.dart | 2 +- lib/src/model/engine/evaluation_service.dart | 9 +- lib/src/model/engine/uci_protocol.dart | 17 +- lib/src/model/engine/work.dart | 2 +- lib/src/model/study/study_controller.dart | 21 ++ lib/src/utils/json.dart | 1 + lib/src/view/analysis/analysis_screen.dart | 18 +- lib/src/view/analysis/analysis_settings.dart | 264 +++++++++++------- lib/src/view/engine/engine_depth.dart | 5 +- lib/src/widgets/list.dart | 6 +- test/model/common/node_test.dart | 4 +- 13 files changed, 267 insertions(+), 141 deletions(-) diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index 1d46fbd20d..b7206b6a1f 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -202,6 +202,7 @@ class AnalysisController extends _$AnalysisController options: EvaluationOptions( multiPv: prefs.numEvalLines, cores: prefs.numEngineCores, + searchTime: prefs.engineSearchTime, ), ) .then((_) { @@ -389,6 +390,7 @@ class AnalysisController extends _$AnalysisController options: EvaluationOptions( multiPv: prefs.numEvalLines, cores: prefs.numEngineCores, + searchTime: prefs.engineSearchTime, ), ); _startEngineEval(); @@ -407,6 +409,7 @@ class AnalysisController extends _$AnalysisController EvaluationOptions( multiPv: numEvalLines, cores: ref.read(analysisPreferencesProvider).numEngineCores, + searchTime: ref.read(analysisPreferencesProvider).engineSearchTime, ), ); @@ -432,6 +435,23 @@ class AnalysisController extends _$AnalysisController EvaluationOptions( multiPv: ref.read(analysisPreferencesProvider).numEvalLines, cores: numEngineCores, + searchTime: ref.read(analysisPreferencesProvider).engineSearchTime, + ), + ); + + _startEngineEval(); + } + + void setEngineSearchTime(Duration searchTime) { + ref + .read(analysisPreferencesProvider.notifier) + .setEngineSearchTime(searchTime); + + ref.read(evaluationServiceProvider).setOptions( + EvaluationOptions( + multiPv: ref.read(analysisPreferencesProvider).numEvalLines, + cores: ref.read(analysisPreferencesProvider).numEngineCores, + searchTime: searchTime, ), ); diff --git a/lib/src/model/analysis/analysis_preferences.dart b/lib/src/model/analysis/analysis_preferences.dart index 30615646d3..c3b682536b 100644 --- a/lib/src/model/analysis/analysis_preferences.dart +++ b/lib/src/model/analysis/analysis_preferences.dart @@ -91,6 +91,14 @@ class AnalysisPreferences extends _$AnalysisPreferences ), ); } + + Future setEngineSearchTime(Duration engineSearchTime) { + return save( + state.copyWith( + engineSearchTime: engineSearchTime, + ), + ); + } } @Freezed(fromJson: true, toJson: true) @@ -107,6 +115,12 @@ class AnalysisPrefs with _$AnalysisPrefs implements Serializable { @Assert('numEvalLines >= 0 && numEvalLines <= 3') required int numEvalLines, @Assert('numEngineCores >= 1 && numEngineCores <= maxEngineCores') required int numEngineCores, + @JsonKey( + defaultValue: _searchTimeDefault, + fromJson: _searchTimeFromJson, + toJson: _searchTimeToJson, + ) + required Duration engineSearchTime, }) = _AnalysisPrefs; static const defaults = AnalysisPrefs( @@ -118,9 +132,34 @@ class AnalysisPrefs with _$AnalysisPrefs implements Serializable { showPgnComments: true, numEvalLines: 2, numEngineCores: 1, + engineSearchTime: Duration(seconds: 10), ); factory AnalysisPrefs.fromJson(Map json) { return _$AnalysisPrefsFromJson(json); } } + +Duration _searchTimeDefault() { + return const Duration(seconds: 10); +} + +Duration _searchTimeFromJson(int seconds) { + return Duration(seconds: seconds); +} + +int _searchTimeToJson(Duration duration) { + return duration.inSeconds; +} + +const kAvailableEngineSearchTimes = [ + Duration(seconds: 4), + Duration(seconds: 6), + Duration(seconds: 8), + Duration(seconds: 10), + Duration(seconds: 12), + Duration(seconds: 15), + Duration(seconds: 20), + Duration(seconds: 30), + Duration(hours: 1), +]; diff --git a/lib/src/model/common/eval.dart b/lib/src/model/common/eval.dart index df78dd2483..902eeef306 100644 --- a/lib/src/model/common/eval.dart +++ b/lib/src/model/common/eval.dart @@ -72,7 +72,7 @@ class ClientEval with _$ClientEval implements Eval { required int nodes, required IList pvs, required int millis, - required int maxDepth, + required Duration searchTime, int? cp, int? mate, }) = _ClientEval; diff --git a/lib/src/model/engine/evaluation_service.dart b/lib/src/model/engine/evaluation_service.dart index d822ce00ce..0fc2cf0680 100644 --- a/lib/src/model/engine/evaluation_service.dart +++ b/lib/src/model/engine/evaluation_service.dart @@ -20,7 +20,6 @@ import 'work.dart'; part 'evaluation_service.g.dart'; part 'evaluation_service.freezed.dart'; -const kMaxEngineDepth = 22; final maxEngineCores = max(Platform.numberOfProcessors - 1, 1); final defaultEngineCores = min((Platform.numberOfProcessors / 2).ceil(), maxEngineCores); @@ -45,6 +44,7 @@ class EvaluationService { EvaluationOptions _options = EvaluationOptions( multiPv: 1, cores: defaultEngineCores, + searchTime: const Duration(seconds: 10), ); static const _defaultState = @@ -133,17 +133,17 @@ class EvaluationService { variant: context.variant, threads: _options.cores, hashSize: maxMemory, - maxDepth: kMaxEngineDepth, + searchTime: _options.searchTime, multiPv: _options.multiPv, path: path, initialPosition: context.initialPosition, steps: IList(steps), ); - // cancel evaluation if we already have a cached eval at max depth + // cancel evaluation if we already have a cached eval at max search time final cachedEval = work.steps.isEmpty ? initialPositionEval : work.evalCache; - if (cachedEval != null && cachedEval.depth >= kMaxEngineDepth) { + if (cachedEval != null && cachedEval.searchTime >= _options.searchTime) { _state.value = ( engineName: _state.value.engineName, state: _state.value.state, @@ -231,5 +231,6 @@ class EvaluationOptions with _$EvaluationOptions { const factory EvaluationOptions({ required int multiPv, required int cores, + required Duration searchTime, }) = _EvaluationOptions; } diff --git a/lib/src/model/engine/uci_protocol.dart b/lib/src/model/engine/uci_protocol.dart index 549867eeb4..f34a0f270e 100644 --- a/lib/src/model/engine/uci_protocol.dart +++ b/lib/src/model/engine/uci_protocol.dart @@ -151,7 +151,7 @@ class UCIProtocol { if (multiPv == 1) { _currentEval = ClientEval( position: _work!.position, - maxDepth: _work!.maxDepth, + searchTime: Duration(milliseconds: elapsedMs), depth: depth, nodes: nodes, cp: isMate ? null : ev, @@ -169,14 +169,7 @@ class UCIProtocol { if (multiPv == _expectedPvs && _currentEval != null) { _evalController.sink.add((_work!, _currentEval!)); - // Depth limits are nice in the user interface, but in clearly decided - // positions the usual depth limits are reached very quickly due to - // pruning. Therefore not using `go depth ${_work.maxDepth}` and - // manually ensuring Stockfish gets to spend a minimum amount of - // time/nodes on each position. - if (depth >= _work!.maxDepth && - elapsedMs > 8000 && - nodes > 4000 * math.exp(_work!.maxDepth * 0.3)) { + if (elapsedMs > _work!.searchTime.inMilliseconds) { _stop(); } } @@ -219,11 +212,7 @@ class UCIProtocol { ), ].join(' '), ); - _sendAndLog( - _work!.maxDepth >= 99 - ? 'go depth $maxPlies' // 'go infinite' would not finish even if entire tree search completed - : 'go movetime 60000', - ); + _sendAndLog('go movetime ${_work!.searchTime.inMilliseconds}'); _isComputing.value = true; } else { _isComputing.value = false; diff --git a/lib/src/model/engine/work.dart b/lib/src/model/engine/work.dart index 553c9e1677..7bb491ab09 100644 --- a/lib/src/model/engine/work.dart +++ b/lib/src/model/engine/work.dart @@ -20,7 +20,7 @@ class Work with _$Work { required int threads, int? hashSize, required UciPath path, - required int maxDepth, + required Duration searchTime, required int multiPv, bool? threatMode, required Position initialPosition, diff --git a/lib/src/model/study/study_controller.dart b/lib/src/model/study/study_controller.dart index 29d7e0883c..84c2031944 100644 --- a/lib/src/model/study/study_controller.dart +++ b/lib/src/model/study/study_controller.dart @@ -137,6 +137,7 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { options: EvaluationOptions( multiPv: prefs.numEvalLines, cores: prefs.numEngineCores, + searchTime: ref.read(analysisPreferencesProvider).engineSearchTime, ), ) .then((_) { @@ -350,6 +351,8 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { options: EvaluationOptions( multiPv: prefs.numEvalLines, cores: prefs.numEngineCores, + searchTime: + ref.read(analysisPreferencesProvider).engineSearchTime, ), ); _startEngineEval(); @@ -370,6 +373,7 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { EvaluationOptions( multiPv: numEvalLines, cores: ref.read(analysisPreferencesProvider).numEngineCores, + searchTime: ref.read(analysisPreferencesProvider).engineSearchTime, ), ); @@ -395,6 +399,23 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { EvaluationOptions( multiPv: ref.read(analysisPreferencesProvider).numEvalLines, cores: numEngineCores, + searchTime: ref.read(analysisPreferencesProvider).engineSearchTime, + ), + ); + + _startEngineEval(); + } + + void setEngineSearchTime(Duration searchTime) { + ref + .read(analysisPreferencesProvider.notifier) + .setEngineSearchTime(searchTime); + + ref.read(evaluationServiceProvider).setOptions( + EvaluationOptions( + multiPv: ref.read(analysisPreferencesProvider).numEvalLines, + cores: ref.read(analysisPreferencesProvider).numEngineCores, + searchTime: searchTime, ), ); diff --git a/lib/src/utils/json.dart b/lib/src/utils/json.dart index 6066eaa7b0..0d459a74ad 100644 --- a/lib/src/utils/json.dart +++ b/lib/src/utils/json.dart @@ -1,4 +1,5 @@ import 'package:deep_pick/deep_pick.dart'; +import 'package:json_annotation/json_annotation.dart'; import 'package:lichess_mobile/src/model/common/uci.dart'; extension UciExtension on Pick { diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index 45d3ae1f2f..ac319d0512 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -17,7 +17,6 @@ import 'package:lichess_mobile/src/view/engine/engine_depth.dart'; import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; import 'package:lichess_mobile/src/view/engine/engine_lines.dart'; import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; -import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; @@ -88,16 +87,13 @@ class _AnalysisScreenState extends ConsumerState controller: _tabController, ), AppBarIconButton( - onPressed: () => showAdaptiveBottomSheet( - context: context, - isScrollControlled: true, - showDragHandle: true, - isDismissible: true, - constraints: BoxConstraints( - minHeight: MediaQuery.sizeOf(context).height * 0.5, - ), - builder: (_) => AnalysisSettings(widget.options), - ), + onPressed: () { + pushPlatformRoute( + context, + title: context.l10n.settingsSettings, + builder: (_) => AnalysisSettings(widget.options), + ); + }, semanticsLabel: context.l10n.settingsSettings, icon: const Icon(Icons.settings), ), diff --git a/lib/src/view/analysis/analysis_settings.dart b/lib/src/view/analysis/analysis_settings.dart index 7571566adc..9e77f61931 100644 --- a/lib/src/view/analysis/analysis_settings.dart +++ b/lib/src/view/analysis/analysis_settings.dart @@ -10,6 +10,7 @@ import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/non_linear_slider.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:lichess_mobile/src/widgets/settings.dart'; class AnalysisSettings extends ConsumerWidget { @@ -28,76 +29,119 @@ class AnalysisSettings extends ConsumerWidget { switch (asyncState) { case AsyncData(:final value): - return BottomSheetScrollableContainer( - children: [ - PlatformListTile( - title: Text(context.l10n.openingExplorer), - onTap: () => showAdaptiveBottomSheet( - context: context, - isScrollControlled: true, - showDragHandle: true, - isDismissible: true, - builder: (_) => OpeningExplorerSettings(options), - ), - trailing: const Icon(CupertinoIcons.chevron_right), - ), - if (value.isComputerAnalysisAllowed) - SwitchSettingTile( - title: Text(context.l10n.computerAnalysis), - value: prefs.enableComputerAnalysis, - onChanged: (_) { - ref.read(ctrlProvider.notifier).toggleComputerAnalysis(); - }, - ), - AnimatedCrossFade( - duration: const Duration(milliseconds: 300), - crossFadeState: value.isComputerAnalysisAllowedAndEnabled - ? CrossFadeState.showSecond - : CrossFadeState.showFirst, - firstChild: const SizedBox.shrink(), - secondChild: Column( - mainAxisSize: MainAxisSize.min, + return PlatformScaffold( + appBar: PlatformAppBar( + title: Text(context.l10n.settingsSettings), + ), + body: ListView( + children: [ + ListSection( + header: SettingsSectionTitle(context.l10n.computerAnalysis), children: [ SwitchSettingTile( - title: Text(context.l10n.toggleLocalEvaluation), - value: prefs.enableLocalEvaluation, + title: Text(context.l10n.enable), + value: prefs.enableComputerAnalysis, onChanged: (_) { - ref.read(ctrlProvider.notifier).toggleLocalEvaluation(); + ref.read(ctrlProvider.notifier).toggleComputerAnalysis(); }, ), - PlatformListTile( - title: Text.rich( - TextSpan( - text: '${context.l10n.multipleLines}: ', - style: const TextStyle( - fontWeight: FontWeight.normal, + AnimatedCrossFade( + duration: const Duration(milliseconds: 300), + crossFadeState: value.isComputerAnalysisAllowedAndEnabled + ? CrossFadeState.showSecond + : CrossFadeState.showFirst, + firstChild: const SizedBox.shrink(), + secondChild: ListSection( + margin: EdgeInsets.zero, + cupertinoBorderRadius: BorderRadius.zero, + cupertinoClipBehavior: Clip.none, + children: [ + SwitchSettingTile( + title: Text(context.l10n.evaluationGauge), + value: prefs.showEvaluationGauge, + onChanged: (value) => ref + .read(analysisPreferencesProvider.notifier) + .toggleShowEvaluationGauge(), ), - children: [ - TextSpan( - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - ), - text: prefs.numEvalLines.toString(), + SwitchSettingTile( + title: Text(context.l10n.toggleGlyphAnnotations), + value: prefs.showAnnotations, + onChanged: (_) => ref + .read(analysisPreferencesProvider.notifier) + .toggleAnnotations(), + ), + SwitchSettingTile( + title: Text(context.l10n.mobileShowComments), + value: prefs.showPgnComments, + onChanged: (_) => ref + .read(analysisPreferencesProvider.notifier) + .togglePgnComments(), + ), + SwitchSettingTile( + title: Text(context.l10n.bestMoveArrow), + value: prefs.showBestMoveArrow, + onChanged: (value) => ref + .read(analysisPreferencesProvider.notifier) + .toggleShowBestMoveArrow(), + ), + ], + ), + ), + ], + ), + AnimatedCrossFade( + duration: const Duration(milliseconds: 300), + crossFadeState: value.isComputerAnalysisAllowedAndEnabled + ? CrossFadeState.showSecond + : CrossFadeState.showFirst, + firstChild: const SizedBox.shrink(), + secondChild: ListSection( + header: const SettingsSectionTitle('Stockfish 16'), + children: [ + SwitchSettingTile( + title: Text(context.l10n.toggleLocalEvaluation), + value: prefs.enableLocalEvaluation, + onChanged: (_) { + ref.read(ctrlProvider.notifier).toggleLocalEvaluation(); + }, + ), + PlatformListTile( + title: Text.rich( + TextSpan( + text: 'Search time: ', + style: const TextStyle( + fontWeight: FontWeight.normal, ), - ], + children: [ + TextSpan( + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + text: prefs.engineSearchTime.inSeconds == 3600 + ? '∞' + : '${prefs.engineSearchTime.inSeconds}s', + ), + ], + ), + ), + subtitle: NonLinearSlider( + labelBuilder: (value) => + value == 3600 ? '∞' : '${value}s', + value: prefs.engineSearchTime.inSeconds, + values: kAvailableEngineSearchTimes + .map((e) => e.inSeconds) + .toList(), + onChangeEnd: (value) => + ref.read(ctrlProvider.notifier).setEngineSearchTime( + Duration(seconds: value.toInt()), + ), ), ), - subtitle: NonLinearSlider( - value: prefs.numEvalLines, - values: const [0, 1, 2, 3], - onChangeEnd: value.isEngineAvailable - ? (value) => ref - .read(ctrlProvider.notifier) - .setNumEvalLines(value.toInt()) - : null, - ), - ), - if (maxEngineCores > 1) PlatformListTile( title: Text.rich( TextSpan( - text: '${context.l10n.cpus}: ', + text: '${context.l10n.multipleLines}: ', style: const TextStyle( fontWeight: FontWeight.normal, ), @@ -107,63 +151,77 @@ class AnalysisSettings extends ConsumerWidget { fontWeight: FontWeight.bold, fontSize: 18, ), - text: prefs.numEngineCores.toString(), + text: prefs.numEvalLines.toString(), ), ], ), ), subtitle: NonLinearSlider( - value: prefs.numEngineCores, - values: - List.generate(maxEngineCores, (index) => index + 1), - onChangeEnd: value.isEngineAvailable - ? (value) => ref - .read(ctrlProvider.notifier) - .setEngineCores(value.toInt()) - : null, + value: prefs.numEvalLines, + values: const [0, 1, 2, 3], + onChangeEnd: (value) => ref + .read(ctrlProvider.notifier) + .setNumEvalLines(value.toInt()), ), ), - SwitchSettingTile( - title: Text(context.l10n.bestMoveArrow), - value: prefs.showBestMoveArrow, - onChanged: (value) => ref - .read(analysisPreferencesProvider.notifier) - .toggleShowBestMoveArrow(), - ), - SwitchSettingTile( - title: Text(context.l10n.evaluationGauge), - value: prefs.showEvaluationGauge, - onChanged: (value) => ref - .read(analysisPreferencesProvider.notifier) - .toggleShowEvaluationGauge(), - ), - SwitchSettingTile( - title: Text(context.l10n.toggleGlyphAnnotations), - value: prefs.showAnnotations, - onChanged: (_) => ref - .read(analysisPreferencesProvider.notifier) - .toggleAnnotations(), + if (maxEngineCores > 1) + PlatformListTile( + title: Text.rich( + TextSpan( + text: '${context.l10n.cpus}: ', + style: const TextStyle( + fontWeight: FontWeight.normal, + ), + children: [ + TextSpan( + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + text: prefs.numEngineCores.toString(), + ), + ], + ), + ), + subtitle: NonLinearSlider( + value: prefs.numEngineCores, + values: List.generate( + maxEngineCores, + (index) => index + 1, + ), + onChangeEnd: (value) => ref + .read(ctrlProvider.notifier) + .setEngineCores(value.toInt()), + ), + ), + ], + ), + ), + ListSection( + children: [ + PlatformListTile( + title: Text(context.l10n.openingExplorer), + onTap: () => showAdaptiveBottomSheet( + context: context, + isScrollControlled: true, + showDragHandle: true, + isDismissible: true, + builder: (_) => OpeningExplorerSettings(options), + ), ), SwitchSettingTile( - title: Text(context.l10n.mobileShowComments), - value: prefs.showPgnComments, - onChanged: (_) => ref - .read(analysisPreferencesProvider.notifier) - .togglePgnComments(), + title: Text(context.l10n.sound), + value: isSoundEnabled, + onChanged: (value) { + ref + .read(generalPreferencesProvider.notifier) + .toggleSoundEnabled(); + }, ), ], ), - ), - SwitchSettingTile( - title: Text(context.l10n.sound), - value: isSoundEnabled, - onChanged: (value) { - ref - .read(generalPreferencesProvider.notifier) - .toggleSoundEnabled(); - }, - ), - ], + ], + ), ); case AsyncError(:final error): debugPrint('Error loading analysis: $error'); diff --git a/lib/src/view/engine/engine_depth.dart b/lib/src/view/engine/engine_depth.dart index d3352d676d..dc4dd5c654 100644 --- a/lib/src/view/engine/engine_depth.dart +++ b/lib/src/view/engine/engine_depth.dart @@ -93,7 +93,6 @@ class _StockfishInfo extends ConsumerWidget { ? ', ${eval?.knps.round()}kn/s' : ''; final depth = currentEval?.depth ?? 0; - final maxDepth = math.max(depth, kMaxEngineDepth); return Column( mainAxisSize: MainAxisSize.min, @@ -106,9 +105,7 @@ class _StockfishInfo extends ConsumerWidget { ), title: Text(engineName), subtitle: Text( - context.l10n.depthX( - '$depth/$maxDepth$knps', - ), + context.l10n.depthX('$depth$knps'), ), ), ], diff --git a/lib/src/widgets/list.dart b/lib/src/widgets/list.dart index 518876dfd5..d456f32c7b 100644 --- a/lib/src/widgets/list.dart +++ b/lib/src/widgets/list.dart @@ -18,6 +18,7 @@ class ListSection extends StatelessWidget { this.dense = false, this.cupertinoAdditionalDividerMargin, this.cupertinoBackgroundColor, + this.cupertinoBorderRadius, this.cupertinoClipBehavior = Clip.hardEdge, }) : _isLoading = false; @@ -36,6 +37,7 @@ class ListSection extends StatelessWidget { dense = false, cupertinoAdditionalDividerMargin = null, cupertinoBackgroundColor = null, + cupertinoBorderRadius = null, cupertinoClipBehavior = Clip.hardEdge, _isLoading = true; @@ -67,6 +69,8 @@ class ListSection extends StatelessWidget { final Color? cupertinoBackgroundColor; + final BorderRadiusGeometry? cupertinoBorderRadius; + final Clip cupertinoClipBehavior; final bool _isLoading; @@ -193,7 +197,7 @@ class ListSection extends StatelessWidget { decoration: BoxDecoration( color: cupertinoBackgroundColor ?? Styles.cupertinoCardColor.resolveFrom(context), - borderRadius: + borderRadius: cupertinoBorderRadius ?? const BorderRadius.all(Radius.circular(10.0)), ), separatorColor: diff --git a/test/model/common/node_test.dart b/test/model/common/node_test.dart index a182fd2df7..f9d82ad696 100644 --- a/test/model/common/node_test.dart +++ b/test/model/common/node_test.dart @@ -194,7 +194,7 @@ void main() { final eval = ClientEval( position: branch.position, - maxDepth: 20, + searchTime: const Duration(seconds: 10), cp: 100, depth: 10, nodes: 1000, @@ -227,7 +227,7 @@ void main() { final eval = ClientEval( position: root.position, - maxDepth: 20, + searchTime: const Duration(seconds: 10), cp: 100, depth: 10, nodes: 1000, From a7406f4adb99f79cfa4a48475966176f503e718c Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 26 Nov 2024 13:03:54 +0100 Subject: [PATCH 789/979] Refactor analysis settings --- lib/src/utils/json.dart | 1 - lib/src/view/analysis/analysis_settings.dart | 116 ++----------- lib/src/view/analysis/stockfish_settings.dart | 122 ++++++++++++++ lib/src/view/study/study_screen.dart | 14 +- lib/src/view/study/study_settings.dart | 154 ++++++------------ 5 files changed, 189 insertions(+), 218 deletions(-) create mode 100644 lib/src/view/analysis/stockfish_settings.dart diff --git a/lib/src/utils/json.dart b/lib/src/utils/json.dart index 0d459a74ad..6066eaa7b0 100644 --- a/lib/src/utils/json.dart +++ b/lib/src/utils/json.dart @@ -1,5 +1,4 @@ import 'package:deep_pick/deep_pick.dart'; -import 'package:json_annotation/json_annotation.dart'; import 'package:lichess_mobile/src/model/common/uci.dart'; extension UciExtension on Pick { diff --git a/lib/src/view/analysis/analysis_settings.dart b/lib/src/view/analysis/analysis_settings.dart index 9e77f61931..9ba31e770f 100644 --- a/lib/src/view/analysis/analysis_settings.dart +++ b/lib/src/view/analysis/analysis_settings.dart @@ -2,14 +2,13 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; -import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/view/analysis/stockfish_settings.dart'; import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_settings.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; -import 'package:lichess_mobile/src/widgets/non_linear_slider.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:lichess_mobile/src/widgets/settings.dart'; @@ -95,106 +94,19 @@ class AnalysisSettings extends ConsumerWidget { ? CrossFadeState.showSecond : CrossFadeState.showFirst, firstChild: const SizedBox.shrink(), - secondChild: ListSection( - header: const SettingsSectionTitle('Stockfish 16'), - children: [ - SwitchSettingTile( - title: Text(context.l10n.toggleLocalEvaluation), - value: prefs.enableLocalEvaluation, - onChanged: (_) { - ref.read(ctrlProvider.notifier).toggleLocalEvaluation(); - }, - ), - PlatformListTile( - title: Text.rich( - TextSpan( - text: 'Search time: ', - style: const TextStyle( - fontWeight: FontWeight.normal, - ), - children: [ - TextSpan( - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - ), - text: prefs.engineSearchTime.inSeconds == 3600 - ? '∞' - : '${prefs.engineSearchTime.inSeconds}s', - ), - ], - ), - ), - subtitle: NonLinearSlider( - labelBuilder: (value) => - value == 3600 ? '∞' : '${value}s', - value: prefs.engineSearchTime.inSeconds, - values: kAvailableEngineSearchTimes - .map((e) => e.inSeconds) - .toList(), - onChangeEnd: (value) => - ref.read(ctrlProvider.notifier).setEngineSearchTime( - Duration(seconds: value.toInt()), - ), - ), - ), - PlatformListTile( - title: Text.rich( - TextSpan( - text: '${context.l10n.multipleLines}: ', - style: const TextStyle( - fontWeight: FontWeight.normal, - ), - children: [ - TextSpan( - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - ), - text: prefs.numEvalLines.toString(), - ), - ], - ), - ), - subtitle: NonLinearSlider( - value: prefs.numEvalLines, - values: const [0, 1, 2, 3], - onChangeEnd: (value) => ref - .read(ctrlProvider.notifier) - .setNumEvalLines(value.toInt()), - ), - ), - if (maxEngineCores > 1) - PlatformListTile( - title: Text.rich( - TextSpan( - text: '${context.l10n.cpus}: ', - style: const TextStyle( - fontWeight: FontWeight.normal, - ), - children: [ - TextSpan( - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - ), - text: prefs.numEngineCores.toString(), - ), - ], - ), - ), - subtitle: NonLinearSlider( - value: prefs.numEngineCores, - values: List.generate( - maxEngineCores, - (index) => index + 1, - ), - onChangeEnd: (value) => ref - .read(ctrlProvider.notifier) - .setEngineCores(value.toInt()), - ), - ), - ], + secondChild: StockfishSettingsWidget( + onToggleLocalEvaluation: () { + ref.read(ctrlProvider.notifier).toggleLocalEvaluation(); + }, + onSetEngineSearchTime: (value) { + ref.read(ctrlProvider.notifier).setEngineSearchTime(value); + }, + onSetEngineCores: (value) { + ref.read(ctrlProvider.notifier).setEngineCores(value); + }, + onSetNumEvalLines: (value) { + ref.read(ctrlProvider.notifier).setNumEvalLines(value); + }, ), ), ListSection( diff --git a/lib/src/view/analysis/stockfish_settings.dart b/lib/src/view/analysis/stockfish_settings.dart new file mode 100644 index 0000000000..a76795dad9 --- /dev/null +++ b/lib/src/view/analysis/stockfish_settings.dart @@ -0,0 +1,122 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; +import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/widgets/list.dart'; +import 'package:lichess_mobile/src/widgets/non_linear_slider.dart'; +import 'package:lichess_mobile/src/widgets/settings.dart'; + +class StockfishSettingsWidget extends ConsumerWidget { + const StockfishSettingsWidget({ + required this.onToggleLocalEvaluation, + required this.onSetEngineSearchTime, + required this.onSetNumEvalLines, + required this.onSetEngineCores, + super.key, + }); + + final VoidCallback onToggleLocalEvaluation; + final void Function(Duration) onSetEngineSearchTime; + final void Function(int) onSetNumEvalLines; + final void Function(int) onSetEngineCores; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final prefs = ref.watch(analysisPreferencesProvider); + + return ListSection( + header: const SettingsSectionTitle('Stockfish 16'), + children: [ + SwitchSettingTile( + title: Text(context.l10n.toggleLocalEvaluation), + value: prefs.enableLocalEvaluation, + onChanged: (_) { + onToggleLocalEvaluation(); + }, + ), + PlatformListTile( + title: Text.rich( + TextSpan( + text: 'Search time: ', + style: const TextStyle( + fontWeight: FontWeight.normal, + ), + children: [ + TextSpan( + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + text: prefs.engineSearchTime.inSeconds == 3600 + ? '∞' + : '${prefs.engineSearchTime.inSeconds}s', + ), + ], + ), + ), + subtitle: NonLinearSlider( + labelBuilder: (value) => value == 3600 ? '∞' : '${value}s', + value: prefs.engineSearchTime.inSeconds, + values: + kAvailableEngineSearchTimes.map((e) => e.inSeconds).toList(), + onChangeEnd: (value) => + onSetEngineSearchTime(Duration(seconds: value.toInt())), + ), + ), + PlatformListTile( + title: Text.rich( + TextSpan( + text: '${context.l10n.multipleLines}: ', + style: const TextStyle( + fontWeight: FontWeight.normal, + ), + children: [ + TextSpan( + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + text: prefs.numEvalLines.toString(), + ), + ], + ), + ), + subtitle: NonLinearSlider( + value: prefs.numEvalLines, + values: const [0, 1, 2, 3], + onChangeEnd: (value) => onSetNumEvalLines(value.toInt()), + ), + ), + if (maxEngineCores > 1) + PlatformListTile( + title: Text.rich( + TextSpan( + text: '${context.l10n.cpus}: ', + style: const TextStyle( + fontWeight: FontWeight.normal, + ), + children: [ + TextSpan( + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + text: prefs.numEngineCores.toString(), + ), + ], + ), + ), + subtitle: NonLinearSlider( + value: prefs.numEngineCores, + values: List.generate( + maxEngineCores, + (index) => index + 1, + ), + onChangeEnd: (value) => onSetEngineCores(value.toInt()), + ), + ), + ], + ); + } +} diff --git a/lib/src/view/study/study_screen.dart b/lib/src/view/study/study_screen.dart index 44a9478c56..9b6f6fecf1 100644 --- a/lib/src/view/study/study_screen.dart +++ b/lib/src/view/study/study_screen.dart @@ -15,6 +15,7 @@ import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/model/study/study_controller.dart'; import 'package:lichess_mobile/src/model/study/study_preferences.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_layout.dart'; import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; import 'package:lichess_mobile/src/view/engine/engine_lines.dart'; @@ -54,13 +55,12 @@ class StudyScreen extends ConsumerWidget { ), actions: [ AppBarIconButton( - onPressed: () => showAdaptiveBottomSheet( - context: context, - isScrollControlled: true, - showDragHandle: true, - isDismissible: true, - builder: (_) => StudySettings(id), - ), + onPressed: () { + pushPlatformRoute( + context, + screen: StudySettings(id), + ); + }, semanticsLabel: context.l10n.settingsSettings, icon: const Icon(Icons.settings), ), diff --git a/lib/src/view/study/study_settings.dart b/lib/src/view/study/study_settings.dart index d32239d040..cbc4ed51b7 100644 --- a/lib/src/view/study/study_settings.dart +++ b/lib/src/view/study/study_settings.dart @@ -3,14 +3,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; import 'package:lichess_mobile/src/model/study/study_controller.dart'; import 'package:lichess_mobile/src/model/study/study_preferences.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; -import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; +import 'package:lichess_mobile/src/view/analysis/stockfish_settings.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; -import 'package:lichess_mobile/src/widgets/non_linear_slider.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:lichess_mobile/src/widgets/settings.dart'; class StudySettings extends ConsumerWidget { @@ -25,9 +24,6 @@ class StudySettings extends ConsumerWidget { final isComputerAnalysisAllowed = ref.watch( studyController.select((s) => s.requireValue.isComputerAnalysisAllowed), ); - final isEngineAvailable = ref.watch( - studyController.select((s) => s.requireValue.isEngineAvailable), - ); final analysisPrefs = ref.watch(analysisPreferencesProvider); final studyPrefs = ref.watch(studyPreferencesProvider); @@ -35,114 +31,56 @@ class StudySettings extends ConsumerWidget { generalPreferencesProvider.select((pref) => pref.isSoundEnabled), ); - return BottomSheetScrollableContainer( - children: [ - if (isComputerAnalysisAllowed) ...[ - SwitchSettingTile( - title: Text(context.l10n.toggleLocalEvaluation), - value: analysisPrefs.enableLocalEvaluation, - onChanged: isComputerAnalysisAllowed - ? (_) { - ref.read(studyController.notifier).toggleLocalEvaluation(); - } - : null, - ), - PlatformListTile( - title: Text.rich( - TextSpan( - text: '${context.l10n.multipleLines}: ', - style: const TextStyle( - fontWeight: FontWeight.normal, - ), - children: [ - TextSpan( - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - ), - text: analysisPrefs.numEvalLines.toString(), - ), - ], - ), + return PlatformScaffold( + appBar: PlatformAppBar( + title: Text(context.l10n.settingsSettings), + ), + body: ListView( + children: [ + if (isComputerAnalysisAllowed) + StockfishSettingsWidget( + onToggleLocalEvaluation: () => + ref.read(studyController.notifier).toggleLocalEvaluation(), + onSetEngineSearchTime: (value) => + ref.read(studyController.notifier).setEngineSearchTime(value), + onSetNumEvalLines: (value) => + ref.read(studyController.notifier).setNumEvalLines(value), + onSetEngineCores: (value) => + ref.read(studyController.notifier).setEngineCores(value), ), - subtitle: NonLinearSlider( - value: analysisPrefs.numEvalLines, - values: const [0, 1, 2, 3], - onChangeEnd: isEngineAvailable - ? (value) => ref - .read(studyController.notifier) - .setNumEvalLines(value.toInt()) - : null, - ), - ), - if (maxEngineCores > 1) - PlatformListTile( - title: Text.rich( - TextSpan( - text: '${context.l10n.cpus}: ', - style: const TextStyle( - fontWeight: FontWeight.normal, - ), - children: [ - TextSpan( - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - ), - text: analysisPrefs.numEngineCores.toString(), - ), - ], - ), + ListSection( + children: [ + SwitchSettingTile( + title: Text(context.l10n.showVariationArrows), + value: studyPrefs.showVariationArrows, + onChanged: (value) => ref + .read(studyPreferencesProvider.notifier) + .toggleShowVariationArrows(), ), - subtitle: NonLinearSlider( - value: analysisPrefs.numEngineCores, - values: List.generate(maxEngineCores, (index) => index + 1), - onChangeEnd: isEngineAvailable - ? (value) => ref - .read(studyController.notifier) - .setEngineCores(value.toInt()) - : null, - ), - ), - SwitchSettingTile( - title: Text(context.l10n.bestMoveArrow), - value: analysisPrefs.showBestMoveArrow, - onChanged: isEngineAvailable - ? (value) => ref + SwitchSettingTile( + title: Text(context.l10n.toggleGlyphAnnotations), + value: analysisPrefs.showAnnotations, + onChanged: (_) => ref .read(analysisPreferencesProvider.notifier) - .toggleShowBestMoveArrow() - : null, + .toggleAnnotations(), + ), + ], ), - SwitchSettingTile( - title: Text(context.l10n.evaluationGauge), - value: analysisPrefs.showEvaluationGauge, - onChanged: (value) => ref - .read(analysisPreferencesProvider.notifier) - .toggleShowEvaluationGauge(), + ListSection( + children: [ + SwitchSettingTile( + title: Text(context.l10n.sound), + value: isSoundEnabled, + onChanged: (value) { + ref + .read(generalPreferencesProvider.notifier) + .toggleSoundEnabled(); + }, + ), + ], ), ], - SwitchSettingTile( - title: Text(context.l10n.showVariationArrows), - value: studyPrefs.showVariationArrows, - onChanged: (value) => ref - .read(studyPreferencesProvider.notifier) - .toggleShowVariationArrows(), - ), - SwitchSettingTile( - title: Text(context.l10n.toggleGlyphAnnotations), - value: analysisPrefs.showAnnotations, - onChanged: (_) => ref - .read(analysisPreferencesProvider.notifier) - .toggleAnnotations(), - ), - SwitchSettingTile( - title: Text(context.l10n.sound), - value: isSoundEnabled, - onChanged: (value) { - ref.read(generalPreferencesProvider.notifier).toggleSoundEnabled(); - }, - ), - ], + ), ); } } From 7cd047df3cd90a1a08b4c2e05aafe962b2c50e8c Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Tue, 26 Nov 2024 19:11:22 +0100 Subject: [PATCH 790/979] add dropdown_menu fixed widget --- lib/src/view/broadcast/broadcast_screen.dart | 13 +- lib/src/view/broadcast/dropdown_menu.dart | 1375 ++++++++++++++++++ patchs/dropdown_menu.patch | 64 - 3 files changed, 1384 insertions(+), 68 deletions(-) create mode 100644 lib/src/view/broadcast/dropdown_menu.dart delete mode 100644 patchs/dropdown_menu.patch diff --git a/lib/src/view/broadcast/broadcast_screen.dart b/lib/src/view/broadcast/broadcast_screen.dart index caabed0d8d..95f069b157 100644 --- a/lib/src/view/broadcast/broadcast_screen.dart +++ b/lib/src/view/broadcast/broadcast_screen.dart @@ -10,6 +10,7 @@ import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_boards_tab.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_overview_tab.dart'; +import 'package:lichess_mobile/src/view/broadcast/dropdown_menu.dart' as fixed; import 'package:lichess_mobile/src/widgets/adaptive_choice_picker.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; @@ -208,13 +209,15 @@ class _AndroidTournamentAndRoundSelector extends ConsumerWidget { children: [ if (value.group != null) Flexible( - child: DropdownMenu( + child: // TODO replace with the Flutter framework DropdownMenu when next beta channel is released + fixed.DropdownMenu( label: const Text('Tournament'), initialSelection: value.data.id, dropdownMenuEntries: value.group! .map( (tournament) => - DropdownMenuEntry( + // TODO replace with the Flutter framework DropdownMenuEntry when next beta channel is released + fixed.DropdownMenuEntry( value: tournament.id, label: tournament.name, ), @@ -230,13 +233,15 @@ class _AndroidTournamentAndRoundSelector extends ConsumerWidget { ), ), Flexible( - child: DropdownMenu( + child: // TODO replace with the Flutter framework DropdownMenu when next beta channel is released + fixed.DropdownMenu( label: const Text('Round'), initialSelection: value.defaultRoundId, dropdownMenuEntries: value.rounds .map( (BroadcastRound round) => - DropdownMenuEntry( + // TODO replace with the Flutter framework DropdownMenuEntry when next beta channel is released + fixed.DropdownMenuEntry( value: round.id, label: round.name, ), diff --git a/lib/src/view/broadcast/dropdown_menu.dart b/lib/src/view/broadcast/dropdown_menu.dart new file mode 100644 index 0000000000..991f673df4 --- /dev/null +++ b/lib/src/view/broadcast/dropdown_menu.dart @@ -0,0 +1,1375 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math' as math; + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; + +// Examples can assume: +// late BuildContext context; +// late FocusNode myFocusNode; + +/// A callback function that returns the list of the items that matches the +/// current applied filter. +/// +/// Used by [DropdownMenu.filterCallback]. +typedef FilterCallback = List> Function( + List> entries, + String filter, +); + +/// A callback function that returns the index of the item that matches the +/// current contents of a text field. +/// +/// If a match doesn't exist then null must be returned. +/// +/// Used by [DropdownMenu.searchCallback]. +typedef SearchCallback = int? Function( + List> entries, + String query, +); + +const double _kMinimumWidth = 112.0; + +const double _kDefaultHorizontalPadding = 12.0; + +/// Defines a [DropdownMenu] menu button that represents one item view in the menu. +/// +/// See also: +/// +/// * [DropdownMenu] +class DropdownMenuEntry { + /// Creates an entry that is used with [DropdownMenu.dropdownMenuEntries]. + const DropdownMenuEntry({ + required this.value, + required this.label, + this.labelWidget, + this.leadingIcon, + this.trailingIcon, + this.enabled = true, + this.style, + }); + + /// the value used to identify the entry. + /// + /// This value must be unique across all entries in a [DropdownMenu]. + final T value; + + /// The label displayed in the center of the menu item. + final String label; + + /// Overrides the default label widget which is `Text(label)`. + /// + /// {@tool dartpad} + /// This sample shows how to override the default label [Text] + /// widget with one that forces the menu entry to appear on one line + /// by specifying [Text.maxLines] and [Text.overflow]. + /// + /// ** See code in examples/api/lib/material/dropdown_menu/dropdown_menu_entry_label_widget.0.dart ** + /// {@end-tool} + final Widget? labelWidget; + + /// An optional icon to display before the label. + final Widget? leadingIcon; + + /// An optional icon to display after the label. + final Widget? trailingIcon; + + /// Whether the menu item is enabled or disabled. + /// + /// The default value is true. If true, the [DropdownMenuEntry.label] will be filled + /// out in the text field of the [DropdownMenu] when this entry is clicked; otherwise, + /// this entry is disabled. + final bool enabled; + + /// Customizes this menu item's appearance. + /// + /// Null by default. + final ButtonStyle? style; +} + +/// A dropdown menu that can be opened from a [TextField]. The selected +/// menu item is displayed in that field. +/// +/// This widget is used to help people make a choice from a menu and put the +/// selected item into the text input field. People can also filter the list based +/// on the text input or search one item in the menu list. +/// +/// The menu is composed of a list of [DropdownMenuEntry]s. People can provide information, +/// such as: label, leading icon or trailing icon for each entry. The [TextField] +/// will be updated based on the selection from the menu entries. The text field +/// will stay empty if the selected entry is disabled. +/// +/// When the dropdown menu has focus, it can be traversed by pressing the up or down key. +/// During the process, the corresponding item will be highlighted and +/// the text field will be updated. Disabled items will be skipped during traversal. +/// +/// The menu can be scrollable if not all items in the list are displayed at once. +/// +/// {@tool dartpad} +/// This sample shows how to display outlined [DropdownMenu] and filled [DropdownMenu]. +/// +/// ** See code in examples/api/lib/material/dropdown_menu/dropdown_menu.0.dart ** +/// {@end-tool} +/// +/// See also: +/// +/// * [MenuAnchor], which is a widget used to mark the "anchor" for a set of submenus. +/// The [DropdownMenu] uses a [TextField] as the "anchor". +/// * [TextField], which is a text input widget that uses an [InputDecoration]. +/// * [DropdownMenuEntry], which is used to build the [MenuItemButton] in the [DropdownMenu] list. +class DropdownMenu extends StatefulWidget { + /// Creates a const [DropdownMenu]. + /// + /// The leading and trailing icons in the text field can be customized by using + /// [leadingIcon], [trailingIcon] and [selectedTrailingIcon] properties. They are + /// passed down to the [InputDecoration] properties, and will override values + /// in the [InputDecoration.prefixIcon] and [InputDecoration.suffixIcon]. + /// + /// Except leading and trailing icons, the text field can be configured by the + /// [InputDecorationTheme] property. The menu can be configured by the [menuStyle]. + const DropdownMenu({ + super.key, + this.enabled = true, + this.width, + this.menuHeight, + this.leadingIcon, + this.trailingIcon, + this.label, + this.hintText, + this.helperText, + this.errorText, + this.selectedTrailingIcon, + this.enableFilter = false, + this.enableSearch = true, + this.keyboardType, + this.textStyle, + this.textAlign = TextAlign.start, + this.inputDecorationTheme, + this.menuStyle, + this.controller, + this.initialSelection, + this.onSelected, + this.focusNode, + this.requestFocusOnTap, + this.expandedInsets, + this.filterCallback, + this.searchCallback, + this.alignmentOffset, + required this.dropdownMenuEntries, + this.inputFormatters, + }) : assert(filterCallback == null || enableFilter); + + /// Determine if the [DropdownMenu] is enabled. + /// + /// Defaults to true. + /// + /// {@tool dartpad} + /// This sample demonstrates how the [enabled] and [requestFocusOnTap] properties + /// affect the textfield's hover cursor. + /// + /// ** See code in examples/api/lib/material/dropdown_menu/dropdown_menu.2.dart ** + /// {@end-tool} + final bool enabled; + + /// Determine the width of the [DropdownMenu]. + /// + /// If this is null, the width of the [DropdownMenu] will be the same as the width of the widest + /// menu item plus the width of the leading/trailing icon. + final double? width; + + /// Determine the height of the menu. + /// + /// If this is null, the menu will display as many items as possible on the screen. + final double? menuHeight; + + /// An optional Icon at the front of the text input field. + /// + /// Defaults to null. If this is not null, the menu items will have extra paddings to be aligned + /// with the text in the text field. + final Widget? leadingIcon; + + /// An optional icon at the end of the text field. + /// + /// Defaults to an [Icon] with [Icons.arrow_drop_down]. + final Widget? trailingIcon; + + /// Optional widget that describes the input field. + /// + /// When the input field is empty and unfocused, the label is displayed on + /// top of the input field (i.e., at the same location on the screen where + /// text may be entered in the input field). When the input field receives + /// focus (or if the field is non-empty), the label moves above, either + /// vertically adjacent to, or to the center of the input field. + /// + /// Defaults to null. + final Widget? label; + + /// Text that suggests what sort of input the field accepts. + /// + /// Defaults to null; + final String? hintText; + + /// Text that provides context about the [DropdownMenu]'s value, such + /// as how the value will be used. + /// + /// If non-null, the text is displayed below the input field, in + /// the same location as [errorText]. If a non-null [errorText] value is + /// specified then the helper text is not shown. + /// + /// Defaults to null; + /// + /// See also: + /// + /// * [InputDecoration.helperText], which is the text that provides context about the [InputDecorator.child]'s value. + final String? helperText; + + /// Text that appears below the input field and the border to show the error message. + /// + /// If non-null, the border's color animates to red and the [helperText] is not shown. + /// + /// Defaults to null; + /// + /// See also: + /// + /// * [InputDecoration.errorText], which is the text that appears below the [InputDecorator.child] and the border. + final String? errorText; + + /// An optional icon at the end of the text field to indicate that the text + /// field is pressed. + /// + /// Defaults to an [Icon] with [Icons.arrow_drop_up]. + final Widget? selectedTrailingIcon; + + /// Determine if the menu list can be filtered by the text input. + /// + /// Defaults to false. + final bool enableFilter; + + /// Determine if the first item that matches the text input can be highlighted. + /// + /// Defaults to true as the search function could be commonly used. + final bool enableSearch; + + /// The type of keyboard to use for editing the text. + /// + /// Defaults to [TextInputType.text]. + final TextInputType? keyboardType; + + /// The text style for the [TextField] of the [DropdownMenu]; + /// + /// Defaults to the overall theme's [TextTheme.bodyLarge] + /// if the dropdown menu theme's value is null. + final TextStyle? textStyle; + + /// The text align for the [TextField] of the [DropdownMenu]. + /// + /// Defaults to [TextAlign.start]. + final TextAlign textAlign; + + /// Defines the default appearance of [InputDecoration] to show around the text field. + /// + /// By default, shows a outlined text field. + final InputDecorationTheme? inputDecorationTheme; + + /// The [MenuStyle] that defines the visual attributes of the menu. + /// + /// The default width of the menu is set to the width of the text field. + final MenuStyle? menuStyle; + + /// Controls the text being edited or selected in the menu. + /// + /// If null, this widget will create its own [TextEditingController]. + final TextEditingController? controller; + + /// The value used to for an initial selection. + /// + /// Defaults to null. + final T? initialSelection; + + /// The callback is called when a selection is made. + /// + /// Defaults to null. If null, only the text field is updated. + final ValueChanged? onSelected; + + /// Defines the keyboard focus for this widget. + /// + /// The [focusNode] is a long-lived object that's typically managed by a + /// [StatefulWidget] parent. See [FocusNode] for more information. + /// + /// To give the keyboard focus to this widget, provide a [focusNode] and then + /// use the current [FocusScope] to request the focus: + /// + /// ```dart + /// FocusScope.of(context).requestFocus(myFocusNode); + /// ``` + /// + /// This happens automatically when the widget is tapped. + /// + /// To be notified when the widget gains or loses the focus, add a listener + /// to the [focusNode]: + /// + /// ```dart + /// myFocusNode.addListener(() { print(myFocusNode.hasFocus); }); + /// ``` + /// + /// If null, this widget will create its own [FocusNode]. + /// + /// ## Keyboard + /// + /// Requesting the focus will typically cause the keyboard to be shown + /// if it's not showing already. + /// + /// On Android, the user can hide the keyboard - without changing the focus - + /// with the system back button. They can restore the keyboard's visibility + /// by tapping on a text field. The user might hide the keyboard and + /// switch to a physical keyboard, or they might just need to get it + /// out of the way for a moment, to expose something it's + /// obscuring. In this case requesting the focus again will not + /// cause the focus to change, and will not make the keyboard visible. + /// + /// If this is non-null, the behaviour of [requestFocusOnTap] is overridden + /// by the [FocusNode.canRequestFocus] property. + final FocusNode? focusNode; + + /// Determine if the dropdown button requests focus and the on-screen virtual + /// keyboard is shown in response to a touch event. + /// + /// Ignored if a [focusNode] is explicitly provided (in which case, + /// [FocusNode.canRequestFocus] controls the behavior). + /// + /// Defaults to null, which enables platform-specific behavior: + /// + /// * On mobile platforms, acts as if set to false; tapping on the text + /// field and opening the menu will not cause a focus request and the + /// virtual keyboard will not appear. + /// + /// * On desktop platforms, acts as if set to true; the dropdown takes the + /// focus when activated. + /// + /// Set this to true or false explicitly to override the default behavior. + /// + /// {@tool dartpad} + /// This sample demonstrates how the [enabled] and [requestFocusOnTap] properties + /// affect the textfield's hover cursor. + /// + /// ** See code in examples/api/lib/material/dropdown_menu/dropdown_menu.2.dart ** + /// {@end-tool} + final bool? requestFocusOnTap; + + /// Descriptions of the menu items in the [DropdownMenu]. + /// + /// This is a required parameter. It is recommended that at least one [DropdownMenuEntry] + /// is provided. If this is an empty list, the menu will be empty and only + /// contain space for padding. + final List> dropdownMenuEntries; + + /// Defines the menu text field's width to be equal to its parent's width + /// plus the horizontal width of the specified insets. + /// + /// If this property is null, the width of the text field will be determined + /// by the width of menu items or [DropdownMenu.width]. If this property is not null, + /// the text field's width will match the parent's width plus the specified insets. + /// If the value of this property is [EdgeInsets.zero], the width of the text field will be the same + /// as its parent's width. + /// + /// The [expandedInsets]' top and bottom are ignored, only its left and right + /// properties are used. + /// + /// Defaults to null. + final EdgeInsetsGeometry? expandedInsets; + + /// When [DropdownMenu.enableFilter] is true, this callback is used to + /// compute the list of filtered items. + /// + /// {@tool snippet} + /// + /// In this example the `filterCallback` returns the items that contains the + /// trimmed query. + /// + /// ```dart + /// DropdownMenu( + /// enableFilter: true, + /// filterCallback: (List> entries, String filter) { + /// final String trimmedFilter = filter.trim().toLowerCase(); + /// if (trimmedFilter.isEmpty) { + /// return entries; + /// } + /// + /// return entries + /// .where((DropdownMenuEntry entry) => + /// entry.label.toLowerCase().contains(trimmedFilter), + /// ) + /// .toList(); + /// }, + /// dropdownMenuEntries: const >[], + /// ) + /// ``` + /// {@end-tool} + /// + /// Defaults to null. If this parameter is null and the + /// [DropdownMenu.enableFilter] property is set to true, the default behavior + /// will return a filtered list. The filtered list will contain items + /// that match the text provided by the input field, with a case-insensitive + /// comparison. When this is not null, `enableFilter` must be set to true. + final FilterCallback? filterCallback; + + /// When [DropdownMenu.enableSearch] is true, this callback is used to compute + /// the index of the search result to be highlighted. + /// + /// {@tool snippet} + /// + /// In this example the `searchCallback` returns the index of the search result + /// that exactly matches the query. + /// + /// ```dart + /// DropdownMenu( + /// searchCallback: (List> entries, String query) { + /// if (query.isEmpty) { + /// return null; + /// } + /// final int index = entries.indexWhere((DropdownMenuEntry entry) => entry.label == query); + /// + /// return index != -1 ? index : null; + /// }, + /// dropdownMenuEntries: const >[], + /// ) + /// ``` + /// {@end-tool} + /// + /// Defaults to null. If this is null and [DropdownMenu.enableSearch] is true, + /// the default function will return the index of the first matching result + /// which contains the contents of the text input field. + final SearchCallback? searchCallback; + + /// Optional input validation and formatting overrides. + /// + /// Formatters are run in the provided order when the user changes the text + /// this widget contains. When this parameter changes, the new formatters will + /// not be applied until the next time the user inserts or deletes text. + /// Formatters don't run when the text is changed + /// programmatically via [controller]. + /// + /// See also: + /// + /// * [TextEditingController], which implements the [Listenable] interface + /// and notifies its listeners on [TextEditingValue] changes. + final List? inputFormatters; + + /// {@macro flutter.material.MenuAnchor.alignmentOffset} + final Offset? alignmentOffset; + + @override + State> createState() => _DropdownMenuState(); +} + +class _DropdownMenuState extends State> { + final GlobalKey _anchorKey = GlobalKey(); + final GlobalKey _leadingKey = GlobalKey(); + late List buttonItemKeys; + final MenuController _controller = MenuController(); + bool _enableFilter = false; + late bool _enableSearch; + late List> filteredEntries; + List? _initialMenu; + int? currentHighlight; + double? leadingPadding; + bool _menuHasEnabledItem = false; + TextEditingController? _localTextEditingController; + + TextEditingValue get _initialTextEditingValue { + for (final DropdownMenuEntry entry in filteredEntries) { + if (entry.value == widget.initialSelection) { + return TextEditingValue( + text: entry.label, + selection: TextSelection.collapsed(offset: entry.label.length), + ); + } + } + return TextEditingValue.empty; + } + + @override + void initState() { + super.initState(); + if (widget.controller != null) { + _localTextEditingController = widget.controller; + } else { + _localTextEditingController = TextEditingController(); + } + _enableSearch = widget.enableSearch; + filteredEntries = widget.dropdownMenuEntries; + buttonItemKeys = List.generate( + filteredEntries.length, + (int index) => GlobalKey(), + ); + _menuHasEnabledItem = + filteredEntries.any((DropdownMenuEntry entry) => entry.enabled); + _localTextEditingController?.value = _initialTextEditingValue; + + refreshLeadingPadding(); + } + + @override + void dispose() { + if (widget.controller == null) { + _localTextEditingController?.dispose(); + _localTextEditingController = null; + } + super.dispose(); + } + + @override + void didUpdateWidget(DropdownMenu oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.controller != widget.controller) { + if (widget.controller != null) { + _localTextEditingController?.dispose(); + } + _localTextEditingController = + widget.controller ?? TextEditingController(); + } + if (oldWidget.enableFilter != widget.enableFilter) { + if (!widget.enableFilter) { + _enableFilter = false; + } + } + if (oldWidget.enableSearch != widget.enableSearch) { + if (!widget.enableSearch) { + _enableSearch = widget.enableSearch; + currentHighlight = null; + } + } + if (oldWidget.dropdownMenuEntries != widget.dropdownMenuEntries) { + currentHighlight = null; + filteredEntries = widget.dropdownMenuEntries; + buttonItemKeys = List.generate( + filteredEntries.length, + (int index) => GlobalKey(), + ); + _menuHasEnabledItem = + filteredEntries.any((DropdownMenuEntry entry) => entry.enabled); + // If the text field content matches one of the new entries do not rematch the initialSelection. + final bool isCurrentSelectionValid = filteredEntries.any( + (DropdownMenuEntry entry) => + entry.label == _localTextEditingController?.text, + ); + if (!isCurrentSelectionValid) { + _localTextEditingController?.value = _initialTextEditingValue; + } + } + if (oldWidget.leadingIcon != widget.leadingIcon) { + refreshLeadingPadding(); + } + if (oldWidget.initialSelection != widget.initialSelection) { + _localTextEditingController?.value = _initialTextEditingValue; + } + } + + bool canRequestFocus() { + return widget.focusNode?.canRequestFocus ?? + widget.requestFocusOnTap ?? + switch (Theme.of(context).platform) { + TargetPlatform.iOS || + TargetPlatform.android || + TargetPlatform.fuchsia => + false, + TargetPlatform.macOS || + TargetPlatform.linux || + TargetPlatform.windows => + true, + }; + } + + void refreshLeadingPadding() { + WidgetsBinding.instance.addPostFrameCallback( + (_) { + if (!mounted) { + return; + } + setState(() { + leadingPadding = getWidth(_leadingKey); + }); + }, + debugLabel: 'DropdownMenu.refreshLeadingPadding', + ); + } + + void scrollToHighlight() { + WidgetsBinding.instance.addPostFrameCallback( + (_) { + final BuildContext? highlightContext = + buttonItemKeys[currentHighlight!].currentContext; + if (highlightContext != null) { + Scrollable.of(highlightContext) + .position + .ensureVisible(highlightContext.findRenderObject()!); + } + }, + debugLabel: 'DropdownMenu.scrollToHighlight', + ); + } + + double? getWidth(GlobalKey key) { + final BuildContext? context = key.currentContext; + if (context != null) { + final RenderBox box = context.findRenderObject()! as RenderBox; + return box.hasSize ? box.size.width : null; + } + return null; + } + + List> filter( + List> entries, + TextEditingController textEditingController, + ) { + final String filterText = textEditingController.text.toLowerCase(); + return entries + .where( + (DropdownMenuEntry entry) => + entry.label.toLowerCase().contains(filterText), + ) + .toList(); + } + + bool _shouldUpdateCurrentHighlight(List> entries) { + final String searchText = + _localTextEditingController!.value.text.toLowerCase(); + if (searchText.isEmpty) { + return true; + } + + // When `entries` are filtered by filter algorithm, currentHighlight may exceed the valid range of `entries` and should be updated. + if (currentHighlight == null || currentHighlight! >= entries.length) { + return true; + } + + if (entries[currentHighlight!].label.toLowerCase().contains(searchText)) { + return false; + } + + return true; + } + + int? search( + List> entries, + TextEditingController textEditingController, + ) { + final String searchText = textEditingController.value.text.toLowerCase(); + if (searchText.isEmpty) { + return null; + } + + final int index = entries.indexWhere( + (DropdownMenuEntry entry) => + entry.label.toLowerCase().contains(searchText), + ); + + return index != -1 ? index : null; + } + + List _buildButtons( + List> filteredEntries, + TextDirection textDirection, { + int? focusedIndex, + bool enableScrollToHighlight = true, + }) { + final List result = []; + for (int i = 0; i < filteredEntries.length; i++) { + final DropdownMenuEntry entry = filteredEntries[i]; + + // By default, when the text field has a leading icon but a menu entry doesn't + // have one, the label of the entry should have extra padding to be aligned + // with the text in the text input field. When both the text field and the + // menu entry have leading icons, the menu entry should remove the extra + // paddings so its leading icon will be aligned with the leading icon of + // the text field. + final double padding = entry.leadingIcon == null + ? (leadingPadding ?? _kDefaultHorizontalPadding) + : _kDefaultHorizontalPadding; + ButtonStyle effectiveStyle = entry.style ?? + switch (textDirection) { + TextDirection.rtl => MenuItemButton.styleFrom( + padding: EdgeInsets.only( + left: _kDefaultHorizontalPadding, + right: padding, + ), + ), + TextDirection.ltr => MenuItemButton.styleFrom( + padding: EdgeInsets.only( + left: padding, + right: _kDefaultHorizontalPadding, + ), + ), + }; + + final ButtonStyle? themeStyle = MenuButtonTheme.of(context).style; + + final WidgetStateProperty? effectiveForegroundColor = + entry.style?.foregroundColor ?? themeStyle?.foregroundColor; + final WidgetStateProperty? effectiveIconColor = + entry.style?.iconColor ?? themeStyle?.iconColor; + final WidgetStateProperty? effectiveOverlayColor = + entry.style?.overlayColor ?? themeStyle?.overlayColor; + final WidgetStateProperty? effectiveBackgroundColor = + entry.style?.backgroundColor ?? themeStyle?.backgroundColor; + + // Simulate the focused state because the text field should always be focused + // during traversal. Include potential MenuItemButton theme in the focus + // simulation for all colors in the theme. + if (entry.enabled && i == focusedIndex) { + // Query the Material 3 default style. + // TODO(bleroux): replace once a standard way for accessing defaults will be defined. + // See: https://github.com/flutter/flutter/issues/130135. + final ButtonStyle defaultStyle = + const MenuItemButton().defaultStyleOf(context); + + Color? resolveFocusedColor( + WidgetStateProperty? colorStateProperty, + ) { + return colorStateProperty + ?.resolve({WidgetState.focused}); + } + + final Color focusedForegroundColor = resolveFocusedColor( + effectiveForegroundColor ?? defaultStyle.foregroundColor!, + )!; + final Color focusedIconColor = + resolveFocusedColor(effectiveIconColor ?? defaultStyle.iconColor!)!; + final Color focusedOverlayColor = resolveFocusedColor( + effectiveOverlayColor ?? defaultStyle.overlayColor!, + )!; + // For the background color we can't rely on the default style which is transparent. + // Defaults to onSurface.withOpacity(0.12). + final Color focusedBackgroundColor = + resolveFocusedColor(effectiveBackgroundColor) ?? + Theme.of(context).colorScheme.onSurface.withOpacity(0.12); + + effectiveStyle = effectiveStyle.copyWith( + backgroundColor: + WidgetStatePropertyAll(focusedBackgroundColor), + foregroundColor: + WidgetStatePropertyAll(focusedForegroundColor), + iconColor: WidgetStatePropertyAll(focusedIconColor), + overlayColor: WidgetStatePropertyAll(focusedOverlayColor), + ); + } else { + effectiveStyle = effectiveStyle.copyWith( + backgroundColor: effectiveBackgroundColor, + foregroundColor: effectiveForegroundColor, + iconColor: effectiveIconColor, + overlayColor: effectiveOverlayColor, + ); + } + + Widget label = entry.labelWidget ?? Text(entry.label); + if (widget.width != null) { + final double horizontalPadding = padding + _kDefaultHorizontalPadding; + label = ConstrainedBox( + constraints: + BoxConstraints(maxWidth: widget.width! - horizontalPadding), + child: label, + ); + } + + final Widget menuItemButton = MenuItemButton( + key: enableScrollToHighlight ? buttonItemKeys[i] : null, + style: effectiveStyle, + leadingIcon: entry.leadingIcon, + trailingIcon: entry.trailingIcon, + onPressed: entry.enabled && widget.enabled + ? () { + _localTextEditingController?.value = TextEditingValue( + text: entry.label, + selection: + TextSelection.collapsed(offset: entry.label.length), + ); + currentHighlight = widget.enableSearch ? i : null; + widget.onSelected?.call(entry.value); + _enableFilter = false; + } + : null, + requestFocusOnHover: false, + child: label, + ); + result.add(menuItemButton); + } + + return result; + } + + void handleUpKeyInvoke(_) { + setState(() { + if (!widget.enabled || !_menuHasEnabledItem || !_controller.isOpen) { + return; + } + _enableFilter = false; + _enableSearch = false; + currentHighlight ??= 0; + currentHighlight = (currentHighlight! - 1) % filteredEntries.length; + while (!filteredEntries[currentHighlight!].enabled) { + currentHighlight = (currentHighlight! - 1) % filteredEntries.length; + } + final String currentLabel = filteredEntries[currentHighlight!].label; + _localTextEditingController?.value = TextEditingValue( + text: currentLabel, + selection: TextSelection.collapsed(offset: currentLabel.length), + ); + }); + } + + void handleDownKeyInvoke(_) { + setState(() { + if (!widget.enabled || !_menuHasEnabledItem || !_controller.isOpen) { + return; + } + _enableFilter = false; + _enableSearch = false; + currentHighlight ??= -1; + currentHighlight = (currentHighlight! + 1) % filteredEntries.length; + while (!filteredEntries[currentHighlight!].enabled) { + currentHighlight = (currentHighlight! + 1) % filteredEntries.length; + } + final String currentLabel = filteredEntries[currentHighlight!].label; + _localTextEditingController?.value = TextEditingValue( + text: currentLabel, + selection: TextSelection.collapsed(offset: currentLabel.length), + ); + }); + } + + void handlePressed(MenuController controller) { + if (controller.isOpen) { + currentHighlight = null; + controller.close(); + } else { + // close to open + if (_localTextEditingController!.text.isNotEmpty) { + _enableFilter = false; + } + controller.open(); + } + setState(() {}); + } + + @override + Widget build(BuildContext context) { + final TextDirection textDirection = Directionality.of(context); + _initialMenu ??= _buildButtons( + widget.dropdownMenuEntries, + textDirection, + enableScrollToHighlight: false, + ); + final DropdownMenuThemeData theme = DropdownMenuTheme.of(context); + final DropdownMenuThemeData defaults = _DropdownMenuDefaultsM3(context); + + if (_enableFilter) { + filteredEntries = widget.filterCallback + ?.call(filteredEntries, _localTextEditingController!.text) ?? + filter(widget.dropdownMenuEntries, _localTextEditingController!); + } else { + filteredEntries = widget.dropdownMenuEntries; + } + _menuHasEnabledItem = + filteredEntries.any((DropdownMenuEntry entry) => entry.enabled); + + if (_enableSearch) { + if (widget.searchCallback != null) { + currentHighlight = widget.searchCallback!( + filteredEntries, + _localTextEditingController!.text, + ); + } else { + final bool shouldUpdateCurrentHighlight = + _shouldUpdateCurrentHighlight(filteredEntries); + if (shouldUpdateCurrentHighlight) { + currentHighlight = + search(filteredEntries, _localTextEditingController!); + } + } + if (currentHighlight != null) { + scrollToHighlight(); + } + } + + final List menu = _buildButtons( + filteredEntries, + textDirection, + focusedIndex: currentHighlight, + ); + + final TextStyle? effectiveTextStyle = + widget.textStyle ?? theme.textStyle ?? defaults.textStyle; + + MenuStyle? effectiveMenuStyle = + widget.menuStyle ?? theme.menuStyle ?? defaults.menuStyle!; + + final double? anchorWidth = getWidth(_anchorKey); + if (widget.width != null) { + effectiveMenuStyle = effectiveMenuStyle.copyWith( + minimumSize: WidgetStatePropertyAll(Size(widget.width!, 0.0)), + ); + } else if (anchorWidth != null) { + effectiveMenuStyle = effectiveMenuStyle.copyWith( + minimumSize: WidgetStatePropertyAll(Size(anchorWidth, 0.0)), + ); + } + + if (widget.menuHeight != null) { + effectiveMenuStyle = effectiveMenuStyle.copyWith( + maximumSize: WidgetStatePropertyAll( + Size(double.infinity, widget.menuHeight!), + ), + ); + } + final InputDecorationTheme effectiveInputDecorationTheme = + widget.inputDecorationTheme ?? + theme.inputDecorationTheme ?? + defaults.inputDecorationTheme!; + + final MouseCursor? effectiveMouseCursor = switch (widget.enabled) { + true => + canRequestFocus() ? SystemMouseCursors.text : SystemMouseCursors.click, + false => null, + }; + + Widget menuAnchor = MenuAnchor( + style: effectiveMenuStyle, + alignmentOffset: widget.alignmentOffset, + controller: _controller, + menuChildren: menu, + crossAxisUnconstrained: false, + builder: + (BuildContext context, MenuController controller, Widget? child) { + assert(_initialMenu != null); + final Widget trailingButton = Padding( + padding: const EdgeInsets.all(4.0), + child: IconButton( + isSelected: controller.isOpen, + icon: widget.trailingIcon ?? const Icon(Icons.arrow_drop_down), + selectedIcon: + widget.selectedTrailingIcon ?? const Icon(Icons.arrow_drop_up), + onPressed: !widget.enabled + ? null + : () { + handlePressed(controller); + }, + ), + ); + + final Widget leadingButton = Padding( + padding: const EdgeInsets.all(8.0), + child: widget.leadingIcon ?? const SizedBox.shrink(), + ); + + final Widget textField = TextField( + key: _anchorKey, + enabled: widget.enabled, + mouseCursor: effectiveMouseCursor, + focusNode: widget.focusNode, + canRequestFocus: canRequestFocus(), + enableInteractiveSelection: canRequestFocus(), + readOnly: !canRequestFocus(), + keyboardType: widget.keyboardType, + textAlign: widget.textAlign, + textAlignVertical: TextAlignVertical.center, + style: effectiveTextStyle, + controller: _localTextEditingController, + onEditingComplete: () { + if (currentHighlight != null) { + final DropdownMenuEntry entry = + filteredEntries[currentHighlight!]; + if (entry.enabled) { + _localTextEditingController?.value = TextEditingValue( + text: entry.label, + selection: + TextSelection.collapsed(offset: entry.label.length), + ); + widget.onSelected?.call(entry.value); + } + } else { + widget.onSelected?.call(null); + } + if (!widget.enableSearch) { + currentHighlight = null; + } + controller.close(); + }, + onTap: !widget.enabled + ? null + : () { + handlePressed(controller); + }, + onChanged: (String text) { + controller.open(); + setState(() { + filteredEntries = widget.dropdownMenuEntries; + _enableFilter = widget.enableFilter; + _enableSearch = widget.enableSearch; + }); + }, + inputFormatters: widget.inputFormatters, + decoration: InputDecoration( + label: widget.label, + hintText: widget.hintText, + helperText: widget.helperText, + errorText: widget.errorText, + prefixIcon: widget.leadingIcon != null + ? SizedBox(key: _leadingKey, child: widget.leadingIcon) + : null, + suffixIcon: trailingButton, + ).applyDefaults(effectiveInputDecorationTheme), + ); + + if (widget.expandedInsets != null) { + // If [expandedInsets] is not null, the width of the text field should depend + // on its parent width. So we don't need to use `_DropdownMenuBody` to + // calculate the children's width. + return textField; + } + + return Shortcuts( + shortcuts: const { + SingleActivator(LogicalKeyboardKey.arrowLeft): + ExtendSelectionByCharacterIntent( + forward: false, + collapseSelection: true, + ), + SingleActivator(LogicalKeyboardKey.arrowRight): + ExtendSelectionByCharacterIntent( + forward: true, + collapseSelection: true, + ), + SingleActivator(LogicalKeyboardKey.arrowUp): _ArrowUpIntent(), + SingleActivator(LogicalKeyboardKey.arrowDown): _ArrowDownIntent(), + }, + child: _DropdownMenuBody( + width: widget.width, + children: [ + textField, + ..._initialMenu!.map( + (Widget item) => + ExcludeFocus(excluding: !controller.isOpen, child: item), + ), + trailingButton, + leadingButton, + ], + ), + ); + }, + ); + + if (widget.expandedInsets case final EdgeInsetsGeometry padding) { + menuAnchor = Padding( + // Clamp the top and bottom padding to 0. + padding: padding.clamp( + EdgeInsets.zero, + const EdgeInsets.only( + left: double.infinity, + right: double.infinity, + ).add( + const EdgeInsetsDirectional.only( + end: double.infinity, + start: double.infinity, + ), + ), + ), + child: menuAnchor, + ); + } + + return Actions( + actions: >{ + _ArrowUpIntent: CallbackAction<_ArrowUpIntent>( + onInvoke: handleUpKeyInvoke, + ), + _ArrowDownIntent: CallbackAction<_ArrowDownIntent>( + onInvoke: handleDownKeyInvoke, + ), + }, + child: menuAnchor, + ); + } +} + +// `DropdownMenu` dispatches these private intents on arrow up/down keys. +// They are needed instead of the typical `DirectionalFocusIntent`s because +// `DropdownMenu` does not really navigate the focus tree upon arrow up/down +// keys: the focus stays on the text field and the menu items are given fake +// highlights as if they are focused. Using `DirectionalFocusIntent`s will cause +// the action to be processed by `EditableText`. +class _ArrowUpIntent extends Intent { + const _ArrowUpIntent(); +} + +class _ArrowDownIntent extends Intent { + const _ArrowDownIntent(); +} + +class _DropdownMenuBody extends MultiChildRenderObjectWidget { + const _DropdownMenuBody({ + super.children, + this.width, + }); + + final double? width; + + @override + _RenderDropdownMenuBody createRenderObject(BuildContext context) { + return _RenderDropdownMenuBody( + width: width, + ); + } + + @override + void updateRenderObject( + BuildContext context, + _RenderDropdownMenuBody renderObject, + ) { + renderObject.width = width; + } +} + +class _DropdownMenuBodyParentData extends ContainerBoxParentData {} + +class _RenderDropdownMenuBody extends RenderBox + with + ContainerRenderObjectMixin, + RenderBoxContainerDefaultsMixin { + _RenderDropdownMenuBody({ + double? width, + }) : _width = width; + + double? get width => _width; + double? _width; + set width(double? value) { + if (_width == value) { + return; + } + _width = value; + markNeedsLayout(); + } + + @override + void setupParentData(RenderBox child) { + if (child.parentData is! _DropdownMenuBodyParentData) { + child.parentData = _DropdownMenuBodyParentData(); + } + } + + @override + void performLayout() { + final BoxConstraints constraints = this.constraints; + double maxWidth = 0.0; + double? maxHeight; + RenderBox? child = firstChild; + + final double intrinsicWidth = + width ?? getMaxIntrinsicWidth(constraints.maxHeight); + final double widthConstraint = + math.min(intrinsicWidth, constraints.maxWidth); + final BoxConstraints innerConstraints = BoxConstraints( + maxWidth: widthConstraint, + maxHeight: getMaxIntrinsicHeight(widthConstraint), + ); + while (child != null) { + if (child == firstChild) { + child.layout(innerConstraints, parentUsesSize: true); + maxHeight ??= child.size.height; + final _DropdownMenuBodyParentData childParentData = + child.parentData! as _DropdownMenuBodyParentData; + assert(child.parentData == childParentData); + child = childParentData.nextSibling; + continue; + } + child.layout(innerConstraints, parentUsesSize: true); + final _DropdownMenuBodyParentData childParentData = + child.parentData! as _DropdownMenuBodyParentData; + childParentData.offset = Offset.zero; + maxWidth = math.max(maxWidth, child.size.width); + maxHeight ??= child.size.height; + assert(child.parentData == childParentData); + child = childParentData.nextSibling; + } + + assert(maxHeight != null); + maxWidth = math.max(_kMinimumWidth, maxWidth); + size = constraints.constrain(Size(width ?? maxWidth, maxHeight!)); + } + + @override + void paint(PaintingContext context, Offset offset) { + final RenderBox? child = firstChild; + if (child != null) { + final _DropdownMenuBodyParentData childParentData = + child.parentData! as _DropdownMenuBodyParentData; + context.paintChild(child, offset + childParentData.offset); + } + } + + @override + Size computeDryLayout(BoxConstraints constraints) { + final BoxConstraints constraints = this.constraints; + double maxWidth = 0.0; + double? maxHeight; + RenderBox? child = firstChild; + final double intrinsicWidth = + width ?? getMaxIntrinsicWidth(constraints.maxHeight); + final double widthConstraint = + math.min(intrinsicWidth, constraints.maxWidth); + final BoxConstraints innerConstraints = BoxConstraints( + maxWidth: widthConstraint, + maxHeight: getMaxIntrinsicHeight(widthConstraint), + ); + + while (child != null) { + if (child == firstChild) { + final Size childSize = child.getDryLayout(innerConstraints); + maxHeight ??= childSize.height; + final _DropdownMenuBodyParentData childParentData = + child.parentData! as _DropdownMenuBodyParentData; + assert(child.parentData == childParentData); + child = childParentData.nextSibling; + continue; + } + final Size childSize = child.getDryLayout(innerConstraints); + final _DropdownMenuBodyParentData childParentData = + child.parentData! as _DropdownMenuBodyParentData; + childParentData.offset = Offset.zero; + maxWidth = math.max(maxWidth, childSize.width); + maxHeight ??= childSize.height; + assert(child.parentData == childParentData); + child = childParentData.nextSibling; + } + + assert(maxHeight != null); + maxWidth = math.max(_kMinimumWidth, maxWidth); + return constraints.constrain(Size(width ?? maxWidth, maxHeight!)); + } + + @override + double computeMinIntrinsicWidth(double height) { + RenderBox? child = firstChild; + double width = 0; + while (child != null) { + if (child == firstChild) { + final _DropdownMenuBodyParentData childParentData = + child.parentData! as _DropdownMenuBodyParentData; + child = childParentData.nextSibling; + continue; + } + final double maxIntrinsicWidth = child.getMinIntrinsicWidth(height); + if (child == lastChild) { + width += maxIntrinsicWidth; + } + if (child == childBefore(lastChild!)) { + width += maxIntrinsicWidth; + } + width = math.max(width, maxIntrinsicWidth); + final _DropdownMenuBodyParentData childParentData = + child.parentData! as _DropdownMenuBodyParentData; + child = childParentData.nextSibling; + } + + return math.max(width, _kMinimumWidth); + } + + @override + double computeMaxIntrinsicWidth(double height) { + RenderBox? child = firstChild; + double width = 0; + while (child != null) { + if (child == firstChild) { + final _DropdownMenuBodyParentData childParentData = + child.parentData! as _DropdownMenuBodyParentData; + child = childParentData.nextSibling; + continue; + } + final double maxIntrinsicWidth = child.getMaxIntrinsicWidth(height); + // Add the width of leading Icon. + if (child == lastChild) { + width += maxIntrinsicWidth; + } + // Add the width of trailing Icon. + if (child == childBefore(lastChild!)) { + width += maxIntrinsicWidth; + } + width = math.max(width, maxIntrinsicWidth); + final _DropdownMenuBodyParentData childParentData = + child.parentData! as _DropdownMenuBodyParentData; + child = childParentData.nextSibling; + } + + return math.max(width, _kMinimumWidth); + } + + @override + double computeMinIntrinsicHeight(double width) { + final RenderBox? child = firstChild; + double width = 0; + if (child != null) { + width = math.max(width, child.getMinIntrinsicHeight(width)); + } + return width; + } + + @override + double computeMaxIntrinsicHeight(double width) { + final RenderBox? child = firstChild; + double width = 0; + if (child != null) { + width = math.max(width, child.getMaxIntrinsicHeight(width)); + } + return width; + } + + @override + bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { + final RenderBox? child = firstChild; + if (child != null) { + final _DropdownMenuBodyParentData childParentData = + child.parentData! as _DropdownMenuBodyParentData; + final bool isHit = result.addWithPaintOffset( + offset: childParentData.offset, + position: position, + hitTest: (BoxHitTestResult result, Offset transformed) { + assert(transformed == position - childParentData.offset); + return child.hitTest(result, position: transformed); + }, + ); + if (isHit) { + return true; + } + } + return false; + } +} + +// Hand coded defaults. These will be updated once we have tokens/spec. +class _DropdownMenuDefaultsM3 extends DropdownMenuThemeData { + _DropdownMenuDefaultsM3(this.context); + + final BuildContext context; + late final ThemeData _theme = Theme.of(context); + + @override + TextStyle? get textStyle => _theme.textTheme.bodyLarge; + + @override + MenuStyle get menuStyle { + return const MenuStyle( + minimumSize: WidgetStatePropertyAll(Size(_kMinimumWidth, 0.0)), + maximumSize: WidgetStatePropertyAll(Size.infinite), + visualDensity: VisualDensity.standard, + ); + } + + @override + InputDecorationTheme get inputDecorationTheme { + return const InputDecorationTheme(border: OutlineInputBorder()); + } +} diff --git a/patchs/dropdown_menu.patch b/patchs/dropdown_menu.patch deleted file mode 100644 index 8c72074129..0000000000 --- a/patchs/dropdown_menu.patch +++ /dev/null @@ -1,64 +0,0 @@ -diff --git a/packages/flutter/lib/src/material/dropdown_menu.dart b/packages/flutter/lib/src/material/dropdown_menu.dart -index 7ae84b4fa0330..def4ea08a24c5 100644 ---- a/packages/flutter/lib/src/material/dropdown_menu.dart -+++ b/packages/flutter/lib/src/material/dropdown_menu.dart -@@ -902,7 +902,6 @@ class _DropdownMenuState extends State> { - controller: _controller, - menuChildren: menu, - crossAxisUnconstrained: false, -- layerLink: LayerLink(), - builder: (BuildContext context, MenuController controller, Widget? child) { - assert(_initialMenu != null); - final Widget trailingButton = Padding( -diff --git a/packages/flutter/test/material/dropdown_menu_test.dart b/packages/flutter/test/material/dropdown_menu_test.dart -index 1156fe00a8635..d78f5f1488457 100644 ---- a/packages/flutter/test/material/dropdown_menu_test.dart -+++ b/packages/flutter/test/material/dropdown_menu_test.dart -@@ -3366,47 +3366,6 @@ void main() { - expect(controller.offset, 0.0); - }); - -- // Regression test for https://github.com/flutter/flutter/issues/149037. -- testWidgets('Dropdown menu follows the text field when keyboard opens', (WidgetTester tester) async { -- Widget boilerplate(double bottomInsets) { -- return MaterialApp( -- home: MediaQuery( -- data: MediaQueryData(viewInsets: EdgeInsets.only(bottom: bottomInsets)), -- child: Scaffold( -- body: Center( -- child: DropdownMenu(dropdownMenuEntries: menuChildren), -- ), -- ), -- ), -- ); -- } -- -- // Build once without bottom insets and open the menu. -- await tester.pumpWidget(boilerplate(0.0)); -- await tester.tap(find.byType(TextField).first); -- await tester.pump(); -- -- Finder findMenuPanels() { -- return find.byWidgetPredicate((Widget widget) => widget.runtimeType.toString() == '_MenuPanel'); -- } -- -- // Menu vertical position is just under the text field. -- expect( -- tester.getRect(findMenuPanels()).top, -- tester.getRect(find.byType(TextField).first).bottom, -- ); -- -- // Simulate the keyboard opening resizing the view. -- await tester.pumpWidget(boilerplate(100.0)); -- await tester.pump(); -- -- // Menu vertical position is just under the text field. -- expect( -- tester.getRect(findMenuPanels()).top, -- tester.getRect(find.byType(TextField).first).bottom, -- ); -- }); -- - testWidgets('DropdownMenu with expandedInsets can be aligned', (WidgetTester tester) async { - Widget buildMenuAnchor({ AlignmentGeometry alignment = Alignment.topCenter }) { - return MaterialApp( From be74940790710b8f88738f859f060e0c50c3ff07 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Wed, 27 Nov 2024 01:32:25 +0100 Subject: [PATCH 791/979] fix linting --- lib/src/view/broadcast/dropdown_menu.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/src/view/broadcast/dropdown_menu.dart b/lib/src/view/broadcast/dropdown_menu.dart index 991f673df4..d0808de144 100644 --- a/lib/src/view/broadcast/dropdown_menu.dart +++ b/lib/src/view/broadcast/dropdown_menu.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// ignore_for_file: deprecated_member_use + import 'dart:math' as math; import 'package:flutter/material.dart'; From 669c7c41960a3413b2abfab373d0173bef3dadfc Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Wed, 27 Nov 2024 01:45:40 +0100 Subject: [PATCH 792/979] fix current node taking a wrong value --- .../broadcast/broadcast_game_controller.dart | 25 ++++++------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/lib/src/model/broadcast/broadcast_game_controller.dart b/lib/src/model/broadcast/broadcast_game_controller.dart index ef01a7b711..e0bd1834ed 100644 --- a/lib/src/model/broadcast/broadcast_game_controller.dart +++ b/lib/src/model/broadcast/broadcast_game_controller.dart @@ -150,23 +150,14 @@ class BroadcastGameController extends _$BroadcastGameController if (newPath != null) { _root.promoteAt(newPath, toMainline: true); - if (state.requireValue.broadcastPath == state.requireValue.currentPath) { - _setPath( - newPath, - shouldRecomputeRootView: isNewNode, - shouldForceShowVariation: true, - isBroadcastMove: true, - ); - } else { - final currentNode = _root.nodeAt(path); - state = AsyncData( - state.requireValue.copyWith( - broadcastPath: newPath, - root: _root.view, - currentNode: AnalysisCurrentNode.fromNode(currentNode), - ), - ); - } + _setPath( + (state.requireValue.broadcastPath == state.requireValue.currentPath) + ? newPath + : state.requireValue.currentPath, + shouldRecomputeRootView: isNewNode, + shouldForceShowVariation: true, + isBroadcastMove: true, + ); } } From 1b231cf9eb2b4762470f6eab0e15527ff4ceaf6e Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 27 Nov 2024 13:21:36 +0100 Subject: [PATCH 793/979] Implement study like - also make the study list UI more consistent with the rest of the app --- lib/src/model/study/study.dart | 5 +- lib/src/model/study/study_controller.dart | 51 +++++++- lib/src/view/study/study_list_screen.dart | 129 +++++++++++---------- lib/src/view/study/study_screen.dart | 81 ++++++++++--- lib/src/widgets/adaptive_bottom_sheet.dart | 3 + 5 files changed, 189 insertions(+), 80 deletions(-) diff --git a/lib/src/model/study/study.dart b/lib/src/model/study/study.dart index ea04c948e7..ae939d4761 100644 --- a/lib/src/model/study/study.dart +++ b/lib/src/model/study/study.dart @@ -38,8 +38,9 @@ class Study with _$Study { StudyChapterMeta get currentChapterMeta => chapters.firstWhere((c) => c.id == chapter.id); - factory Study.fromServerJson(Map json) => - _studyFromPick(pick(json).required()); + factory Study.fromServerJson(Map json) { + return _studyFromPick(pick(json).required()); + } } Study _studyFromPick(RequiredPick pick) { diff --git a/lib/src/model/study/study_controller.dart b/lib/src/model/study/study_controller.dart index 84c2031944..16a9c178bb 100644 --- a/lib/src/model/study/study_controller.dart +++ b/lib/src/model/study/study_controller.dart @@ -12,11 +12,13 @@ import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/common/node.dart'; import 'package:lichess_mobile/src/model/common/service/move_feedback.dart'; import 'package:lichess_mobile/src/model/common/service/sound_service.dart'; +import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/model/common/uci.dart'; import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; import 'package:lichess_mobile/src/model/engine/work.dart'; import 'package:lichess_mobile/src/model/study/study.dart'; import 'package:lichess_mobile/src/model/study/study_repository.dart'; +import 'package:lichess_mobile/src/network/socket.dart'; import 'package:lichess_mobile/src/utils/rate_limit.dart'; import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; import 'package:lichess_mobile/src/widgets/pgn.dart'; @@ -32,19 +34,31 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { final _engineEvalDebounce = Debouncer(const Duration(milliseconds: 150)); Timer? _startEngineEvalTimer; - Timer? _opponentFirstMoveTimer; + StreamSubscription? _socketSubscription; + final _likeDebouncer = Debouncer(const Duration(milliseconds: 500)); + + late final SocketClient _socketClient; @override Future build(StudyId id) async { + final socketPool = ref.watch(socketPoolProvider); final evaluationService = ref.watch(evaluationServiceProvider); + _socketClient = socketPool.open(Uri(path: '/study/$id/socket/v6')); ref.onDispose(() { _startEngineEvalTimer?.cancel(); _opponentFirstMoveTimer?.cancel(); _engineEvalDebounce.dispose(); + _socketSubscription?.cancel(); + _likeDebouncer.dispose(); evaluationService.disposeEngine(); }); - return _fetchChapter(id); + final chapter = await _fetchChapter(id); + + _socketSubscription?.cancel(); + _socketSubscription = _socketClient.stream.listen(_handleSocketEvent); + + return chapter; } Future nextChapter() async { @@ -150,6 +164,39 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { return studyState; } + void toggleLike() { + _likeDebouncer(() { + if (!state.hasValue) return; + final liked = state.requireValue.study.liked; + _socketClient.send('like', {'liked': !liked}); + state = AsyncValue.data( + state.requireValue.copyWith( + study: state.requireValue.study.copyWith(liked: !liked), + ), + ); + }); + } + + void _handleSocketEvent(SocketEvent event) { + if (!state.hasValue) { + assert(false, 'received a game SocketEvent while StudyState is null'); + return; + } + switch (event.topic) { + case 'liking': + final data = + (event.data as Map)['l'] as Map; + final likes = data['likes'] as int; + final bool meLiked = data['me'] as bool; + state = AsyncValue.data( + state.requireValue.copyWith( + study: + state.requireValue.study.copyWith(liked: meLiked, likes: likes), + ), + ); + } + } + // The PGNs of some gamebook studies start with the opponent's turn, so trigger their move after a delay void _ensureItsOurTurnIfGamebook() { _opponentFirstMoveTimer?.cancel(); diff --git a/lib/src/view/study/study_list_screen.dart b/lib/src/view/study/study_list_screen.dart index ff78da2b3f..3829dcd0aa 100644 --- a/lib/src/view/study/study_list_screen.dart +++ b/lib/src/view/study/study_list_screen.dart @@ -197,10 +197,15 @@ class _BodyState extends ConsumerState<_Body> { controller: _scrollController, separatorBuilder: (context, index) => index == 0 ? const SizedBox.shrink() - : const PlatformDivider( - height: 1, - cupertinoHasLeading: true, - ), + : Theme.of(context).platform == TargetPlatform.iOS + ? const PlatformDivider( + height: 1, + cupertinoHasLeading: true, + ) + : const PlatformDivider( + height: 1, + color: Colors.transparent, + ), itemBuilder: (context, index) => index == 0 ? searchBar : _StudyListItem(study: studies.studies[index - 1]), @@ -234,60 +239,65 @@ class _StudyListItem extends StatelessWidget { @override Widget build(BuildContext context) { return PlatformListTile( - padding: Styles.bodyPadding, - title: Row( + padding: Theme.of(context).platform == TargetPlatform.iOS + ? const EdgeInsets.symmetric( + horizontal: 14.0, + vertical: 12.0, + ) + : null, + leading: _StudyFlair( + flair: study.flair, + size: 30, + ), + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - _StudyFlair( - flair: study.flair, - size: 30, - ), - const SizedBox(width: 10), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - study.name, - overflow: TextOverflow.ellipsis, - maxLines: 2, - ), - _StudySubtitle( - study: study, - style: const TextStyle( - fontSize: 12, - color: Colors.grey, - ), - ), - ], - ), + Text( + study.name, + overflow: TextOverflow.ellipsis, + maxLines: 2, ), ], ), - subtitle: DefaultTextStyle.merge( - style: const TextStyle( - fontSize: 12, - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - flex: 4, - child: _StudyChapters(study: study), - ), - Expanded( - flex: 3, - child: _StudyMembers( - study: study, - ), - ), - ], - ), - ), + subtitle: _StudySubtitle(study: study), onTap: () => pushPlatformRoute( context, rootNavigator: true, builder: (context) => StudyScreen(id: study.id), ), + onLongPress: () { + showAdaptiveBottomSheet( + context: context, + useRootNavigator: true, + isDismissible: true, + isScrollControlled: true, + showDragHandle: true, + constraints: BoxConstraints( + minHeight: MediaQuery.sizeOf(context).height * 0.5, + ), + builder: (context) => _ContextMenu(study: study), + ); + }, + ); + } +} + +class _ContextMenu extends ConsumerWidget { + const _ContextMenu({ + required this.study, + }); + + final StudyPageData study; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return BottomSheetScrollableContainer( + padding: const EdgeInsets.all(16.0), + children: [ + _StudyChapters(study: study), + const SizedBox(height: 10.0), + _StudyMembers(study: study), + ], ); } } @@ -398,15 +408,10 @@ class _StudyFlair extends StatelessWidget { } class _StudySubtitle extends StatelessWidget { - const _StudySubtitle({ - required this.study, - required this.style, - }); + const _StudySubtitle({required this.study}); final StudyPageData study; - final TextStyle style; - @override Widget build(BuildContext context) { return Text.rich( @@ -415,28 +420,26 @@ class _StudySubtitle extends StatelessWidget { WidgetSpan( alignment: PlaceholderAlignment.middle, child: Icon( - Icons.favorite_outline, - size: style.fontSize, + study.liked ? Icons.favorite : Icons.favorite_outline, + size: 14, ), ), - TextSpan(text: ' ${study.likes}', style: style), - TextSpan(text: ' • ', style: style), + TextSpan(text: ' ${study.likes}'), + const TextSpan(text: ' • '), if (study.owner != null) ...[ WidgetSpan( - alignment: PlaceholderAlignment.bottom, + alignment: PlaceholderAlignment.middle, child: UserFullNameWidget( user: study.owner, - style: style, showFlair: false, ), ), - TextSpan(text: ' • ', style: style), + const TextSpan(text: ' • '), ], TextSpan( text: timeago.format( study.updatedAt, ), - style: style, ), ], ), diff --git a/lib/src/view/study/study_screen.dart b/lib/src/view/study/study_screen.dart index 9b6f6fecf1..b95f23d851 100644 --- a/lib/src/view/study/study_screen.dart +++ b/lib/src/view/study/study_screen.dart @@ -54,17 +54,8 @@ class StudyScreen extends ConsumerWidget { overflow: TextOverflow.ellipsis, ), actions: [ - AppBarIconButton( - onPressed: () { - pushPlatformRoute( - context, - screen: StudySettings(id), - ); - }, - semanticsLabel: context.l10n.settingsSettings, - icon: const Icon(Icons.settings), - ), _ChapterButton(id: id), + _StudyMenu(id: id), ], ), body: _Body(id: id), @@ -88,6 +79,59 @@ class StudyScreen extends ConsumerWidget { } } +class _StudyMenu extends ConsumerWidget { + const _StudyMenu({required this.id}); + + final StudyId id; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final state = ref.watch(studyControllerProvider(id)).requireValue; + return MenuAnchor( + menuChildren: [ + MenuItemButton( + leadingIcon: const Icon(Icons.settings), + semanticsLabel: context.l10n.settingsSettings, + child: Text(context.l10n.settingsSettings), + onPressed: () { + pushPlatformRoute( + context, + screen: StudySettings(id), + ); + }, + ), + MenuItemButton( + leadingIcon: + Icon(state.study.liked ? Icons.favorite : Icons.favorite_border), + semanticsLabel: context.l10n.studyLike, + child: Text( + state.study.liked + ? context.l10n.studyUnlike + : context.l10n.studyLike, + ), + onPressed: () { + ref.read(studyControllerProvider(id).notifier).toggleLike(); + }, + ), + ], + builder: + (BuildContext context, MenuController controller, Widget? child) { + return AppBarIconButton( + onPressed: () { + if (controller.isOpen) { + controller.close(); + } else { + controller.open(); + } + }, + semanticsLabel: 'Study menu', + icon: const Icon(Icons.more_horiz), + ); + }, + ); + } +} + class _ChapterButton extends ConsumerWidget { const _ChapterButton({required this.id}); @@ -104,10 +148,18 @@ class _ChapterButton extends ConsumerWidget { showDragHandle: true, isScrollControlled: true, isDismissible: true, - constraints: BoxConstraints( - minHeight: MediaQuery.sizeOf(context).height * 0.5, + builder: (_) => DraggableScrollableSheet( + initialChildSize: 0.5, + maxChildSize: 0.95, + snap: true, + expand: false, + builder: (context, scrollController) { + return _StudyChaptersMenu( + id: id, + scrollController: scrollController, + ); + }, ), - builder: (_) => _StudyChaptersMenu(id: id), ), semanticsLabel: context.l10n.studyNbChapters(state.study.chapters.length), @@ -119,9 +171,11 @@ class _ChapterButton extends ConsumerWidget { class _StudyChaptersMenu extends ConsumerWidget { const _StudyChaptersMenu({ required this.id, + required this.scrollController, }); final StudyId id; + final ScrollController scrollController; @override Widget build(BuildContext context, WidgetRef ref) { @@ -140,6 +194,7 @@ class _StudyChaptersMenu extends ConsumerWidget { }); return BottomSheetScrollableContainer( + scrollController: scrollController, children: [ Padding( padding: const EdgeInsets.all(16.0), diff --git a/lib/src/widgets/adaptive_bottom_sheet.dart b/lib/src/widgets/adaptive_bottom_sheet.dart index 996b301ec1..6404c44dd3 100644 --- a/lib/src/widgets/adaptive_bottom_sheet.dart +++ b/lib/src/widgets/adaptive_bottom_sheet.dart @@ -50,15 +50,18 @@ class BottomSheetScrollableContainer extends StatelessWidget { const BottomSheetScrollableContainer({ required this.children, this.padding = const EdgeInsets.only(bottom: 16.0), + this.scrollController, }); final List children; final EdgeInsetsGeometry? padding; + final ScrollController? scrollController; @override Widget build(BuildContext context) { return SafeArea( child: SingleChildScrollView( + controller: scrollController, padding: padding, child: ListBody( children: children, From 9b0aca0b7ea63ffba3110cee69c0609095091150 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Wed, 27 Nov 2024 13:22:42 +0100 Subject: [PATCH 794/979] fix wrong broadcast path when not on current broadcast path --- lib/src/model/broadcast/broadcast_game_controller.dart | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/src/model/broadcast/broadcast_game_controller.dart b/lib/src/model/broadcast/broadcast_game_controller.dart index e0bd1834ed..a0193cb9b4 100644 --- a/lib/src/model/broadcast/broadcast_game_controller.dart +++ b/lib/src/model/broadcast/broadcast_game_controller.dart @@ -156,7 +156,7 @@ class BroadcastGameController extends _$BroadcastGameController : state.requireValue.currentPath, shouldRecomputeRootView: isNewNode, shouldForceShowVariation: true, - isBroadcastMove: true, + broadcastPath: newPath, ); } } @@ -417,7 +417,7 @@ class BroadcastGameController extends _$BroadcastGameController bool shouldForceShowVariation = false, bool shouldRecomputeRootView = false, bool replaying = false, - bool isBroadcastMove = false, + UciPath? broadcastPath, }) { if (!state.hasValue) return; @@ -464,8 +464,7 @@ class BroadcastGameController extends _$BroadcastGameController state = AsyncData( state.requireValue.copyWith( currentPath: path, - broadcastPath: - isBroadcastMove ? path : state.requireValue.broadcastPath, + broadcastPath: broadcastPath ?? state.requireValue.broadcastPath, isOnMainline: _root.isOnMainline(path), currentNode: AnalysisCurrentNode.fromNode(currentNode), lastMove: currentNode.sanMove.move, @@ -478,8 +477,7 @@ class BroadcastGameController extends _$BroadcastGameController state = AsyncData( state.requireValue.copyWith( currentPath: path, - broadcastPath: - isBroadcastMove ? path : state.requireValue.broadcastPath, + broadcastPath: broadcastPath ?? state.requireValue.broadcastPath, isOnMainline: _root.isOnMainline(path), currentNode: AnalysisCurrentNode.fromNode(currentNode), lastMove: null, From 707b584f97353934d1af5ca93e0a7220b8258cbd Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 27 Nov 2024 13:30:49 +0100 Subject: [PATCH 795/979] Fix analysis title overflow Fixes #1173 --- lib/src/view/analysis/analysis_screen.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index ac319d0512..201ac14a49 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -148,7 +148,9 @@ class _Title extends StatelessWidget { Icon(variant.icon), const SizedBox(width: 5.0), ], - Text(context.l10n.analysis), + Flexible( + child: Text(context.l10n.analysis, overflow: TextOverflow.ellipsis), + ), ], ); } From 1db183e82c75ed53dbcbd1c38de1e03cbeb9e037 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Wed, 27 Nov 2024 13:58:57 +0100 Subject: [PATCH 796/979] remove old TODO and renaming --- lib/src/view/broadcast/broadcast_game_screen.dart | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_game_screen.dart b/lib/src/view/broadcast/broadcast_game_screen.dart index aceac5ffdc..8e38a297ff 100644 --- a/lib/src/view/broadcast/broadcast_game_screen.dart +++ b/lib/src/view/broadcast/broadcast_game_screen.dart @@ -160,7 +160,6 @@ class _BroadcastBoardWithHeaders extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - // TODO use borderRadius with players widget return Column( children: [ _PlayerWidget( @@ -188,13 +187,13 @@ class _BroadcastBoard extends ConsumerStatefulWidget { this.roundId, this.gameId, this.boardSize, - this.hasRadius, + this.hasShadow, ); final BroadcastRoundId roundId; final BroadcastGameId gameId; final double boardSize; - final bool hasRadius; + final bool hasShadow; @override ConsumerState<_BroadcastBoard> createState() => _BroadcastBoardState(); @@ -263,7 +262,7 @@ class _BroadcastBoardState extends ConsumerState<_BroadcastBoard> { : IMap({sanMove.move.to: annotation}) : null, settings: boardPrefs.toBoardSettings().copyWith( - boxShadow: widget.hasRadius ? boardShadows : const [], + boxShadow: widget.hasShadow ? boardShadows : const [], drawShape: DrawShapeOptions( enable: boardPrefs.enableShapeDrawings, onCompleteShape: _onCompleteShape, From 2fe81736ab7e5f90570174408b42fd86096759e5 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 27 Nov 2024 17:19:57 +0100 Subject: [PATCH 797/979] Wip on fixing square layout --- lib/src/view/puzzle/puzzle_screen.dart | 15 +- lib/src/widgets/board_table.dart | 243 ++++++++++++++++--------- lib/src/widgets/bottom_bar.dart | 5 + 3 files changed, 166 insertions(+), 97 deletions(-) diff --git a/lib/src/view/puzzle/puzzle_screen.dart b/lib/src/view/puzzle/puzzle_screen.dart index 63bae946cd..a76e2d36e7 100644 --- a/lib/src/view/puzzle/puzzle_screen.dart +++ b/lib/src/view/puzzle/puzzle_screen.dart @@ -145,8 +145,6 @@ class _LoadNextPuzzle extends ConsumerWidget { if (data == null) { return const Center( child: BoardTable( - topTable: kEmptyWidget, - bottomTable: kEmptyWidget, fen: kEmptyFen, orientation: Side.white, errorMessage: 'No more puzzles. Go online to get more.', @@ -165,8 +163,6 @@ class _LoadNextPuzzle extends ConsumerWidget { ); return Center( child: BoardTable( - topTable: kEmptyWidget, - bottomTable: kEmptyWidget, fen: kEmptyFen, orientation: Side.white, errorMessage: e.toString(), @@ -203,15 +199,12 @@ class _LoadPuzzleFromId extends ConsumerWidget { Expanded( child: SafeArea( bottom: false, - child: BoardTable( - fen: kEmptyFen, - orientation: Side.white, - topTable: kEmptyWidget, - bottomTable: kEmptyWidget, + child: BoardTable.empty( + showEngineGaugePlaceholder: true, ), ), ), - SizedBox(height: kBottomBarHeight), + BottomBar.empty(), ], ), error: (e, s) { @@ -226,8 +219,6 @@ class _LoadPuzzleFromId extends ConsumerWidget { child: BoardTable( fen: kEmptyFen, orientation: Side.white, - topTable: kEmptyWidget, - bottomTable: kEmptyWidget, errorMessage: e.toString(), ), ), diff --git a/lib/src/widgets/board_table.dart b/lib/src/widgets/board_table.dart index 90681e2455..0f01a91920 100644 --- a/lib/src/widgets/board_table.dart +++ b/lib/src/widgets/board_table.dart @@ -29,8 +29,8 @@ class BoardTable extends ConsumerStatefulWidget { this.gameData, this.lastMove, this.boardSettingsOverrides, - required this.topTable, - required this.bottomTable, + this.topTable = const SizedBox.shrink(), + this.bottomTable = const SizedBox.shrink(), this.shapes, this.engineGauge, this.moves, @@ -135,7 +135,8 @@ class _BoardTableState extends ConsumerState { final aspectRatio = constraints.biggest.aspectRatio; final defaultBoardSize = constraints.biggest.shortestSide; final isTablet = isTabletOrLarger(context); - final boardSize = isTablet + + final double boardSize = isTablet ? defaultBoardSize - kTabletBoardTableSidePadding * 2 : defaultBoardSize; @@ -144,6 +145,25 @@ class _BoardTableState extends ConsumerState { final verticalSpaceLeftBoardOnPortrait = constraints.biggest.height - boardSize; + final defaultSettings = boardPrefs.toBoardSettings().copyWith( + borderRadius: isTablet + ? const BorderRadius.all(Radius.circular(4.0)) + : BorderRadius.zero, + boxShadow: isTablet ? boardShadows : const [], + drawShape: DrawShapeOptions( + enable: boardPrefs.enableShapeDrawings, + onCompleteShape: _onCompleteShape, + onClearShapes: _onClearShapes, + newShapeColor: boardPrefs.shapeColor.color, + ), + ); + + final settings = widget.boardSettingsOverrides != null + ? widget.boardSettingsOverrides!.merge(defaultSettings) + : defaultSettings; + + final shapes = userShapes.union(widget.shapes ?? ISet()); + final error = widget.errorMessage != null ? SizedBox.square( dimension: boardSize, @@ -169,95 +189,59 @@ class _BoardTableState extends ConsumerState { ) : null; - final defaultSettings = boardPrefs.toBoardSettings().copyWith( - borderRadius: isTablet - ? const BorderRadius.all(Radius.circular(4.0)) - : BorderRadius.zero, - boxShadow: isTablet ? boardShadows : const [], - drawShape: DrawShapeOptions( - enable: boardPrefs.enableShapeDrawings, - onCompleteShape: _onCompleteShape, - onClearShapes: _onClearShapes, - newShapeColor: boardPrefs.shapeColor.color, - ), - ); - - final settings = widget.boardSettingsOverrides != null - ? widget.boardSettingsOverrides!.merge(defaultSettings) - : defaultSettings; - - final board = Chessboard( - key: widget.boardKey, - size: boardSize, - fen: widget.fen, - orientation: widget.orientation, - game: widget.gameData, - lastMove: widget.lastMove, - shapes: userShapes.union(widget.shapes ?? ISet()), - settings: settings, - ); - - Widget boardWidget = board; - - if (widget.boardOverlay != null) { - boardWidget = SizedBox.square( - dimension: boardSize, - child: Stack( - children: [ - board, - SizedBox.square( - dimension: boardSize, - child: Center( - child: SizedBox( - width: (boardSize / 8) * 6.6, - height: (boardSize / 8) * 4.6, - child: widget.boardOverlay, - ), - ), - ), - ], - ), - ); - } else if (error != null) { - boardWidget = SizedBox.square( - dimension: boardSize, - child: Stack( - children: [ - board, - error, - ], - ), - ); - } - final slicedMoves = widget.moves?.asMap().entries.slices(2); - return aspectRatio > 1 + return aspectRatio >= 1.0 ? Row( mainAxisSize: MainAxisSize.max, children: [ - Padding( - padding: isTablet - ? const EdgeInsets.only( - left: kTabletBoardTableSidePadding, - top: kTabletBoardTableSidePadding, - bottom: kTabletBoardTableSidePadding, - ) - : EdgeInsets.zero, - child: Row( - children: [ - boardWidget, - if (widget.engineGauge != null) - EngineGauge( - params: widget.engineGauge!, - displayMode: EngineGaugeDisplayMode.vertical, - ) - else if (widget.showEngineGaugePlaceholder) - const SizedBox(width: kEvalGaugeSize), - ], + Expanded( + flex: kFlexGoldenRatio, + child: Padding( + padding: isTablet + ? const EdgeInsets.only( + left: kTabletBoardTableSidePadding, + top: kTabletBoardTableSidePadding, + bottom: kTabletBoardTableSidePadding, + ) + : EdgeInsets.zero, + child: LayoutBuilder( + builder: (context, constraints) { + final boardSize = constraints.biggest.shortestSide - + (widget.engineGauge != null || + widget.showEngineGaugePlaceholder + ? kEvalGaugeSize + : 0); + return Row( + children: [ + _BoardWidget( + size: boardSize, + boardPrefs: boardPrefs, + fen: widget.fen, + orientation: widget.orientation, + gameData: widget.gameData, + lastMove: widget.lastMove, + shapes: shapes, + settings: settings, + boardKey: widget.boardKey ?? GlobalKey(), + boardOverlay: widget.boardOverlay, + error: error, + ), + if (widget.engineGauge != null) + EngineGauge( + params: widget.engineGauge!, + displayMode: EngineGaugeDisplayMode.vertical, + ) + else if (widget.showEngineGaugePlaceholder) + const SizedBox(width: kEvalGaugeSize), + ], + ); + }, + ), ), ), Flexible( + flex: kFlexGoldenRatioBase, fit: FlexFit.loose, child: Padding( padding: isTablet @@ -329,7 +313,19 @@ class _BoardTableState extends ConsumerState { ) else if (widget.showEngineGaugePlaceholder) const SizedBox(height: kEvalGaugeSize), - boardWidget, + _BoardWidget( + size: boardSize, + boardPrefs: boardPrefs, + fen: widget.fen, + orientation: widget.orientation, + gameData: widget.gameData, + lastMove: widget.lastMove, + shapes: shapes, + settings: settings, + boardKey: widget.boardKey ?? GlobalKey(), + boardOverlay: widget.boardOverlay, + error: error, + ), Expanded( child: Padding( padding: EdgeInsets.symmetric( @@ -365,6 +361,83 @@ class _BoardTableState extends ConsumerState { } } +class _BoardWidget extends StatelessWidget { + const _BoardWidget({ + required this.size, + required this.boardPrefs, + required this.fen, + required this.orientation, + required this.gameData, + required this.lastMove, + required this.shapes, + required this.settings, + required this.boardKey, + required this.boardOverlay, + required this.error, + }); + + final double size; + final BoardPrefs boardPrefs; + final String fen; + final Side orientation; + final GameData? gameData; + final Move? lastMove; + final ISet shapes; + final ChessboardSettings settings; + final Widget? error; + final Widget? boardOverlay; + final GlobalKey boardKey; + + @override + Widget build(BuildContext context) { + final board = Chessboard( + key: boardKey, + size: size, + fen: fen, + orientation: orientation, + game: gameData, + lastMove: lastMove, + shapes: shapes, + settings: settings, + ); + + Widget boardWidget = board; + + if (boardOverlay != null) { + boardWidget = SizedBox.square( + dimension: size, + child: Stack( + children: [ + board, + SizedBox.square( + dimension: size, + child: Center( + child: SizedBox( + width: (size / 8) * 6.6, + height: (size / 8) * 4.6, + child: boardOverlay, + ), + ), + ), + ], + ), + ); + } else if (error != null) { + boardWidget = SizedBox.square( + dimension: size, + child: Stack( + children: [ + board, + error!, + ], + ), + ); + } + + return boardWidget; + } +} + class BoardSettingsOverrides { const BoardSettingsOverrides({ this.animationDuration, diff --git a/lib/src/widgets/bottom_bar.dart b/lib/src/widgets/bottom_bar.dart index 3bf0a104d4..7267eb75ca 100644 --- a/lib/src/widgets/bottom_bar.dart +++ b/lib/src/widgets/bottom_bar.dart @@ -12,6 +12,11 @@ class BottomBar extends StatelessWidget { this.expandChildren = true, }); + const BottomBar.empty() + : children = const [], + expandChildren = true, + mainAxisAlignment = MainAxisAlignment.spaceAround; + /// Children to display in the bottom bar's [Row]. Typically instances of [BottomBarButton]. final List children; From 596d0d10eaa2a6b1dcc96d914b3afaa06dfaed7d Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 27 Nov 2024 17:57:28 +0100 Subject: [PATCH 798/979] More work on fixing square layout --- lib/src/view/analysis/analysis_layout.dart | 162 ++++++++------- lib/src/view/puzzle/puzzle_tab_screen.dart | 17 +- lib/src/widgets/board_table.dart | 226 +++++++++++---------- test/widgets/board_table_test.dart | 24 +-- 4 files changed, 223 insertions(+), 206 deletions(-) diff --git a/lib/src/view/analysis/analysis_layout.dart b/lib/src/view/analysis/analysis_layout.dart index 80f76e7595..cda6f0b664 100644 --- a/lib/src/view/analysis/analysis_layout.dart +++ b/lib/src/view/analysis/analysis_layout.dart @@ -4,6 +4,7 @@ import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; +import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; @@ -145,52 +146,53 @@ class AnalysisLayout extends StatelessWidget { Expanded( child: SafeArea( bottom: false, - child: LayoutBuilder( - builder: (context, constraints) { - final aspectRatio = constraints.biggest.aspectRatio; - final defaultBoardSize = constraints.biggest.shortestSide; + child: OrientationBuilder( + builder: (context, orientation) { final isTablet = isTabletOrLarger(context); - final remainingHeight = - constraints.maxHeight - defaultBoardSize; - final isSmallScreen = - remainingHeight < kSmallRemainingHeightLeftBoardThreshold; - final boardSize = isTablet || isSmallScreen - ? defaultBoardSize - kTabletBoardTableSidePadding * 2 - : defaultBoardSize; - const tabletBoardRadius = BorderRadius.all(Radius.circular(4.0)); - // If the aspect ratio is greater than 1, we are in landscape mode. - if (aspectRatio > 1) { + if (orientation == Orientation.landscape) { return Row( mainAxisSize: MainAxisSize.max, children: [ - Padding( - padding: const EdgeInsets.only( - left: kTabletBoardTableSidePadding, - top: kTabletBoardTableSidePadding, - bottom: kTabletBoardTableSidePadding, - ), - child: Row( - children: [ - boardBuilder( - context, - boardSize, - isTablet ? tabletBoardRadius : null, - ), - if (engineGaugeBuilder != null) ...[ - const SizedBox(width: 4.0), - engineGaugeBuilder!( - context, - Orientation.landscape, - ), - ], - ], + Expanded( + flex: kFlexGoldenRatio, + child: Padding( + padding: const EdgeInsets.only( + left: kTabletBoardTableSidePadding, + top: kTabletBoardTableSidePadding, + bottom: kTabletBoardTableSidePadding, + ), + child: LayoutBuilder( + builder: (context, constraints) { + final boardSize = + constraints.biggest.shortestSide - + (engineGaugeBuilder != null + ? kEvalGaugeSize + 4.0 + : 0); + return Row( + children: [ + boardBuilder( + context, + boardSize, + isTablet ? tabletBoardRadius : null, + ), + if (engineGaugeBuilder != null) ...[ + const SizedBox(width: 4.0), + engineGaugeBuilder!( + context, + Orientation.landscape, + ), + ], + ], + ); + }, + ), ), ), Flexible( - fit: FlexFit.loose, + flex: kFlexGoldenRatioBase, child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ @@ -224,47 +226,57 @@ class AnalysisLayout extends StatelessWidget { ), ], ); - } - // If the aspect ratio is less than 1, we are in portrait mode. - else { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - if (engineGaugeBuilder != null) - engineGaugeBuilder!( - context, - Orientation.portrait, - ), - if (engineLines != null) engineLines!, - if (isTablet) - Padding( - padding: const EdgeInsets.all( - kTabletBoardTableSidePadding, - ), - child: boardBuilder( - context, - boardSize, - tabletBoardRadius, + } else { + return LayoutBuilder( + builder: (context, constraints) { + final defaultBoardSize = constraints.biggest.shortestSide; + final remainingHeight = + constraints.maxHeight - defaultBoardSize; + final isSmallScreen = remainingHeight < + kSmallRemainingHeightLeftBoardThreshold; + final boardSize = isTablet || isSmallScreen + ? defaultBoardSize - kTabletBoardTableSidePadding * 2 + : defaultBoardSize; + + return Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (engineGaugeBuilder != null) + engineGaugeBuilder!( + context, + Orientation.portrait, + ), + if (engineLines != null) engineLines!, + Padding( + padding: isTablet + ? const EdgeInsets.all( + kTabletBoardTableSidePadding, + ) + : EdgeInsets.zero, + child: boardBuilder( + context, + boardSize, + tabletBoardRadius, + ), ), - ) - else - boardBuilder(context, boardSize, null), - Expanded( - child: Padding( - padding: isTablet - ? const EdgeInsets.symmetric( - horizontal: kTabletBoardTableSidePadding, - ) - : EdgeInsets.zero, - child: TabBarView( - controller: tabController, - children: children, + Expanded( + child: Padding( + padding: isTablet + ? const EdgeInsets.symmetric( + horizontal: kTabletBoardTableSidePadding, + ) + : EdgeInsets.zero, + child: TabBarView( + controller: tabController, + children: children, + ), + ), ), - ), - ), - ], + ], + ); + }, ); } }, diff --git a/lib/src/view/puzzle/puzzle_tab_screen.dart b/lib/src/view/puzzle/puzzle_tab_screen.dart index a8d4e61ba2..43645c36b5 100644 --- a/lib/src/view/puzzle/puzzle_tab_screen.dart +++ b/lib/src/view/puzzle/puzzle_tab_screen.dart @@ -62,7 +62,6 @@ Widget _buildMainListItem( int index, Animation animation, PuzzleAngle Function(int index) getAngle, - VoidCallback? onGoingBackFromPuzzleScreen, ) { switch (index) { case 0: @@ -91,7 +90,7 @@ Widget _buildMainListItem( builder: (context) => const PuzzleScreen( angle: PuzzleTheme(PuzzleThemeKey.mix), ), - ).then((_) => onGoingBackFromPuzzleScreen?.call()); + ); }, ); default: @@ -103,7 +102,7 @@ Widget _buildMainListItem( context, rootNavigator: true, builder: (context) => PuzzleScreen(angle: angle), - ).then((_) => onGoingBackFromPuzzleScreen?.call()); + ); }, ); } @@ -187,12 +186,6 @@ class _CupertinoTabBodyState extends ConsumerState<_CupertinoTabBody> { index, animation, (index) => _angles[index], - isTablet - ? () { - ref.read(currentBottomTabProvider.notifier).state = - BottomTab.home; - } - : null, ); if (isTablet) { @@ -352,12 +345,6 @@ class _MaterialTabBodyState extends ConsumerState<_MaterialTabBody> { index, animation, (index) => _angles[index], - isTablet - ? () { - ref.read(currentBottomTabProvider.notifier).state = - BottomTab.home; - } - : null, ); return PopScope( diff --git a/lib/src/widgets/board_table.dart b/lib/src/widgets/board_table.dart index 0f01a91920..34e34fb8d6 100644 --- a/lib/src/widgets/board_table.dart +++ b/lib/src/widgets/board_table.dart @@ -130,21 +130,10 @@ class _BoardTableState extends ConsumerState { Widget build(BuildContext context) { final boardPrefs = ref.watch(boardPreferencesProvider); - return LayoutBuilder( - builder: (context, constraints) { - final aspectRatio = constraints.biggest.aspectRatio; - final defaultBoardSize = constraints.biggest.shortestSide; + return OrientationBuilder( + builder: (context, orientation) { final isTablet = isTabletOrLarger(context); - final double boardSize = isTablet - ? defaultBoardSize - kTabletBoardTableSidePadding * 2 - : defaultBoardSize; - - // vertical space left on portrait mode to check if we can display the - // move list - final verticalSpaceLeftBoardOnPortrait = - constraints.biggest.height - boardSize; - final defaultSettings = boardPrefs.toBoardSettings().copyWith( borderRadius: isTablet ? const BorderRadius.all(Radius.circular(4.0)) @@ -163,35 +152,9 @@ class _BoardTableState extends ConsumerState { : defaultSettings; final shapes = userShapes.union(widget.shapes ?? ISet()); - - final error = widget.errorMessage != null - ? SizedBox.square( - dimension: boardSize, - child: Center( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoColors.secondarySystemBackground - .resolveFrom(context) - : Theme.of(context).colorScheme.surface, - borderRadius: - const BorderRadius.all(Radius.circular(10.0)), - ), - child: Padding( - padding: const EdgeInsets.all(10.0), - child: Text(widget.errorMessage!), - ), - ), - ), - ), - ) - : null; - final slicedMoves = widget.moves?.asMap().entries.slices(2); - return aspectRatio >= 1.0 + return orientation == Orientation.landscape ? Row( mainAxisSize: MainAxisSize.max, children: [ @@ -225,7 +188,7 @@ class _BoardTableState extends ConsumerState { settings: settings, boardKey: widget.boardKey ?? GlobalKey(), boardOverlay: widget.boardOverlay, - error: error, + error: widget.errorMessage, ), if (widget.engineGauge != null) EngineGauge( @@ -242,7 +205,6 @@ class _BoardTableState extends ConsumerState { ), Flexible( flex: kFlexGoldenRatioBase, - fit: FlexFit.loose, child: Padding( padding: isTablet ? const EdgeInsets.all(kTabletBoardTableSidePadding) @@ -274,68 +236,89 @@ class _BoardTableState extends ConsumerState { ), ], ) - : Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (!widget.zenMode && - slicedMoves != null && - verticalSpaceLeftBoardOnPortrait >= 130) - MoveList( - type: MoveListType.inline, - slicedMoves: slicedMoves, - currentMoveIndex: widget.currentMoveIndex ?? 0, - onSelectMove: widget.onSelectMove, - ) - else if (widget.showMoveListPlaceholder && - verticalSpaceLeftBoardOnPortrait >= 130) - const SizedBox(height: 40), - Expanded( - child: Padding( - padding: EdgeInsets.symmetric( - horizontal: - isTablet ? kTabletBoardTableSidePadding : 12.0, + : LayoutBuilder( + builder: (context, constraints) { + final defaultBoardSize = constraints.biggest.shortestSide; + final double boardSize = isTablet + ? defaultBoardSize - kTabletBoardTableSidePadding * 2 + : defaultBoardSize; + + // vertical space left on portrait mode to check if we can display the + // move list + final verticalSpaceLeftBoardOnPortrait = + constraints.biggest.height - boardSize; + + return Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (!widget.zenMode && + slicedMoves != null && + verticalSpaceLeftBoardOnPortrait >= 130) + MoveList( + type: MoveListType.inline, + slicedMoves: slicedMoves, + currentMoveIndex: widget.currentMoveIndex ?? 0, + onSelectMove: widget.onSelectMove, + ) + else if (widget.showMoveListPlaceholder && + verticalSpaceLeftBoardOnPortrait >= 130) + const SizedBox(height: 40), + Expanded( + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: + isTablet ? kTabletBoardTableSidePadding : 12.0, + ), + child: widget.topTable, + ), ), - child: widget.topTable, - ), - ), - if (widget.engineGauge != null) - Padding( - padding: isTablet - ? const EdgeInsets.symmetric( - horizontal: kTabletBoardTableSidePadding, - ) - : EdgeInsets.zero, - child: EngineGauge( - params: widget.engineGauge!, - displayMode: EngineGaugeDisplayMode.horizontal, + if (widget.engineGauge != null) + Padding( + padding: isTablet + ? const EdgeInsets.symmetric( + horizontal: kTabletBoardTableSidePadding, + ) + : EdgeInsets.zero, + child: EngineGauge( + params: widget.engineGauge!, + displayMode: EngineGaugeDisplayMode.horizontal, + ), + ) + else if (widget.showEngineGaugePlaceholder) + const SizedBox(height: kEvalGaugeSize), + Padding( + padding: isTablet + ? const EdgeInsets.symmetric( + horizontal: kTabletBoardTableSidePadding, + ) + : EdgeInsets.zero, + child: _BoardWidget( + size: boardSize, + boardPrefs: boardPrefs, + fen: widget.fen, + orientation: widget.orientation, + gameData: widget.gameData, + lastMove: widget.lastMove, + shapes: shapes, + settings: settings, + boardKey: widget.boardKey ?? GlobalKey(), + boardOverlay: widget.boardOverlay, + error: widget.errorMessage, + ), ), - ) - else if (widget.showEngineGaugePlaceholder) - const SizedBox(height: kEvalGaugeSize), - _BoardWidget( - size: boardSize, - boardPrefs: boardPrefs, - fen: widget.fen, - orientation: widget.orientation, - gameData: widget.gameData, - lastMove: widget.lastMove, - shapes: shapes, - settings: settings, - boardKey: widget.boardKey ?? GlobalKey(), - boardOverlay: widget.boardOverlay, - error: error, - ), - Expanded( - child: Padding( - padding: EdgeInsets.symmetric( - horizontal: - isTablet ? kTabletBoardTableSidePadding : 12.0, + Expanded( + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: + isTablet ? kTabletBoardTableSidePadding : 12.0, + ), + child: widget.bottomTable, + ), ), - child: widget.bottomTable, - ), - ), - ], + ], + ); + }, ); }, ); @@ -384,7 +367,7 @@ class _BoardWidget extends StatelessWidget { final Move? lastMove; final ISet shapes; final ChessboardSettings settings; - final Widget? error; + final String? error; final Widget? boardOverlay; final GlobalKey boardKey; @@ -428,7 +411,10 @@ class _BoardWidget extends StatelessWidget { child: Stack( children: [ board, - error!, + _ErrorWidget( + errorMessage: error!, + boardSize: size, + ), ], ), ); @@ -438,6 +424,40 @@ class _BoardWidget extends StatelessWidget { } } +class _ErrorWidget extends StatelessWidget { + const _ErrorWidget({ + required this.errorMessage, + required this.boardSize, + }); + final double boardSize; + final String errorMessage; + + @override + Widget build(BuildContext context) { + return SizedBox.square( + dimension: boardSize, + child: Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoColors.secondarySystemBackground + .resolveFrom(context) + : Theme.of(context).colorScheme.surface, + borderRadius: const BorderRadius.all(Radius.circular(10.0)), + ), + child: Padding( + padding: const EdgeInsets.all(10.0), + child: Text(errorMessage), + ), + ), + ), + ), + ); + } +} + class BoardSettingsOverrides { const BoardSettingsOverrides({ this.animationDuration, diff --git a/test/widgets/board_table_test.dart b/test/widgets/board_table_test.dart index afbc518529..d9d1b7b3ad 100644 --- a/test/widgets/board_table_test.dart +++ b/test/widgets/board_table_test.dart @@ -25,6 +25,8 @@ const surfaces = [ Size(601, 962), // folded motorola Size(564.7, 482.6), + // pixel fold unfolded + Size(701.0, 640.8), ]; void main() { @@ -77,21 +79,17 @@ void main() { reason: 'Board size should match background size', ); - final isLandscape = surface.aspectRatio > 1.0; + final isPortrait = surface.aspectRatio < 1.0; final isTablet = surface.shortestSide > 600; - final expectedBoardSize = isLandscape - ? isTablet - ? surface.height - 32.0 - : surface.height - : isTablet - ? surface.width - 32.0 - : surface.width; - - expect( - boardSize, - Size(expectedBoardSize, expectedBoardSize), - ); + if (isPortrait) { + final expectedBoardSize = + isTablet ? surface.width - 32.0 : surface.width; + expect( + boardSize, + Size(expectedBoardSize, expectedBoardSize), + ); + } } }, variant: kPlatformVariant, From ddd228c45d93fbf7d4696c04f4b29084f60c733b Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 27 Nov 2024 18:29:26 +0100 Subject: [PATCH 799/979] Fix board widget key --- lib/src/widgets/board_table.dart | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/src/widgets/board_table.dart b/lib/src/widgets/board_table.dart index 34e34fb8d6..9eff0ad3db 100644 --- a/lib/src/widgets/board_table.dart +++ b/lib/src/widgets/board_table.dart @@ -186,7 +186,7 @@ class _BoardTableState extends ConsumerState { lastMove: widget.lastMove, shapes: shapes, settings: settings, - boardKey: widget.boardKey ?? GlobalKey(), + boardKey: widget.boardKey, boardOverlay: widget.boardOverlay, error: widget.errorMessage, ), @@ -302,7 +302,7 @@ class _BoardTableState extends ConsumerState { lastMove: widget.lastMove, shapes: shapes, settings: settings, - boardKey: widget.boardKey ?? GlobalKey(), + boardKey: widget.boardKey, boardOverlay: widget.boardOverlay, error: widget.errorMessage, ), @@ -354,9 +354,9 @@ class _BoardWidget extends StatelessWidget { required this.lastMove, required this.shapes, required this.settings, - required this.boardKey, required this.boardOverlay, required this.error, + this.boardKey, }); final double size; @@ -369,7 +369,7 @@ class _BoardWidget extends StatelessWidget { final ChessboardSettings settings; final String? error; final Widget? boardOverlay; - final GlobalKey boardKey; + final GlobalKey? boardKey; @override Widget build(BuildContext context) { @@ -384,10 +384,10 @@ class _BoardWidget extends StatelessWidget { settings: settings, ); - Widget boardWidget = board; + final boardWidget = board; if (boardOverlay != null) { - boardWidget = SizedBox.square( + return SizedBox.square( dimension: size, child: Stack( children: [ @@ -406,7 +406,7 @@ class _BoardWidget extends StatelessWidget { ), ); } else if (error != null) { - boardWidget = SizedBox.square( + return SizedBox.square( dimension: size, child: Stack( children: [ From 32cc6c7a69b1fe2ea1c1d12f23f27b17ebf2f436 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 27 Nov 2024 18:37:34 +0100 Subject: [PATCH 800/979] Tweak --- lib/src/widgets/board_table.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/src/widgets/board_table.dart b/lib/src/widgets/board_table.dart index 9eff0ad3db..acbd6e99e9 100644 --- a/lib/src/widgets/board_table.dart +++ b/lib/src/widgets/board_table.dart @@ -384,8 +384,6 @@ class _BoardWidget extends StatelessWidget { settings: settings, ); - final boardWidget = board; - if (boardOverlay != null) { return SizedBox.square( dimension: size, @@ -420,7 +418,7 @@ class _BoardWidget extends StatelessWidget { ); } - return boardWidget; + return board; } } From 8d40709b1625bc5260996cf4034562e843682366 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 27 Nov 2024 19:45:39 +0100 Subject: [PATCH 801/979] Fix tests --- test/view/study/study_list_screen_test.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/view/study/study_list_screen_test.dart b/test/view/study/study_list_screen_test.dart index 09fab63d24..0cf063d7a9 100644 --- a/test/view/study/study_list_screen_test.dart +++ b/test/view/study/study_list_screen_test.dart @@ -123,6 +123,11 @@ void main() { await tester.pumpAndSettle(); expect(find.text('Magnus Carlsen Games'), findsOneWidget); + + // loads context menu + await tester.longPress(find.text('Magnus Carlsen Games')); + await tester.pumpAndSettle(); + expect(find.textContaining('Chapter 1'), findsOneWidget); expect(find.textContaining('Chapter 2'), findsOneWidget); expect(find.textContaining('tom-anders'), findsOneWidget); From 112eef3bf9a6c65745e073307ae40f2a173fa5a1 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 27 Nov 2024 20:34:20 +0100 Subject: [PATCH 802/979] Implement PlatformAppBarMenuButton --- lib/src/view/study/study_screen.dart | 42 +++----- lib/src/widgets/buttons.dart | 149 +++++++++++++++++++++++++++ 2 files changed, 162 insertions(+), 29 deletions(-) diff --git a/lib/src/view/study/study_screen.dart b/lib/src/view/study/study_screen.dart index b95f23d851..d86c1a48a7 100644 --- a/lib/src/view/study/study_screen.dart +++ b/lib/src/view/study/study_screen.dart @@ -87,12 +87,14 @@ class _StudyMenu extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final state = ref.watch(studyControllerProvider(id)).requireValue; - return MenuAnchor( - menuChildren: [ - MenuItemButton( - leadingIcon: const Icon(Icons.settings), - semanticsLabel: context.l10n.settingsSettings, - child: Text(context.l10n.settingsSettings), + + return PlatformAppBarMenuButton( + semanticsLabel: 'Study menu', + icon: const Icon(Icons.more_horiz), + actions: [ + AppBarMenuAction( + icon: Icons.settings, + label: context.l10n.settingsSettings, onPressed: () { pushPlatformRoute( context, @@ -100,34 +102,16 @@ class _StudyMenu extends ConsumerWidget { ); }, ), - MenuItemButton( - leadingIcon: - Icon(state.study.liked ? Icons.favorite : Icons.favorite_border), - semanticsLabel: context.l10n.studyLike, - child: Text( - state.study.liked - ? context.l10n.studyUnlike - : context.l10n.studyLike, - ), + AppBarMenuAction( + icon: state.study.liked ? Icons.favorite : Icons.favorite_border, + label: state.study.liked + ? context.l10n.studyUnlike + : context.l10n.studyLike, onPressed: () { ref.read(studyControllerProvider(id).notifier).toggleLike(); }, ), ], - builder: - (BuildContext context, MenuController controller, Widget? child) { - return AppBarIconButton( - onPressed: () { - if (controller.isOpen) { - controller.close(); - } else { - controller.open(); - } - }, - semanticsLabel: 'Study menu', - icon: const Icon(Icons.more_horiz), - ); - }, ); } } diff --git a/lib/src/widgets/buttons.dart b/lib/src/widgets/buttons.dart index 912db892f4..a560586939 100644 --- a/lib/src/widgets/buttons.dart +++ b/lib/src/widgets/buttons.dart @@ -4,6 +4,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:popover/popover.dart'; /// Platform agnostic button which is used for important actions. /// @@ -561,3 +562,151 @@ class PlatformIconButton extends StatelessWidget { } } } + +const _kMenuWidth = 250.0; +const Color _kBorderColor = CupertinoDynamicColor.withBrightness( + color: Color(0xFFA9A9AF), + darkColor: Color(0xFF57585A), +); + +/// A platform agnostic menu button for the app bar. +class PlatformAppBarMenuButton extends StatelessWidget { + const PlatformAppBarMenuButton({ + required this.icon, + required this.semanticsLabel, + required this.actions, + super.key, + }); + + final Widget icon; + final String semanticsLabel; + final List actions; + + @override + Widget build(BuildContext context) { + if (Theme.of(context).platform == TargetPlatform.iOS) { + final menuActions = actions.map((action) { + return CupertinoContextMenuAction( + onPressed: () { + if (action.dismissOnPress) { + Navigator.of(context).pop(); + } + action.onPressed(); + }, + trailingIcon: action.icon, + child: Text(action.label), + ); + }).toList(); + return AppBarIconButton( + onPressed: () { + showPopover( + context: context, + bodyBuilder: (context) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: SizedBox( + width: _kMenuWidth, + child: IntrinsicHeight( + child: ClipRRect( + borderRadius: + const BorderRadius.all(Radius.circular(13.0)), + child: ColoredBox( + color: CupertinoDynamicColor.resolve( + CupertinoContextMenu.kBackgroundColor, + context, + ), + child: ScrollConfiguration( + behavior: ScrollConfiguration.of(context) + .copyWith(scrollbars: false), + child: CupertinoScrollbar( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + menuActions.first, + for (final Widget action + in menuActions.skip(1)) + DecoratedBox( + decoration: BoxDecoration( + border: Border( + top: BorderSide( + color: + CupertinoDynamicColor.resolve( + _kBorderColor, + context, + ), + width: 0.4, + ), + ), + ), + position: DecorationPosition.foreground, + child: action, + ), + ], + ), + ), + ), + ), + ), + ), + ), + ), + ); + }, + arrowWidth: 0.0, + arrowHeight: 0.0, + direction: PopoverDirection.top, + width: _kMenuWidth, + backgroundColor: Colors.transparent, + ); + }, + semanticsLabel: semanticsLabel, + icon: icon, + ); + } + + return MenuAnchor( + menuChildren: actions.map((action) { + return MenuItemButton( + leadingIcon: Icon(action.icon), + semanticsLabel: action.label, + closeOnActivate: action.dismissOnPress, + onPressed: action.onPressed, + child: Text(action.label), + ); + }).toList(), + builder: + (BuildContext context, MenuController controller, Widget? child) { + return AppBarIconButton( + onPressed: () { + if (controller.isOpen) { + controller.close(); + } else { + controller.open(); + } + }, + semanticsLabel: semanticsLabel, + icon: icon, + ); + }, + ); + } +} + +class AppBarMenuAction { + const AppBarMenuAction({ + required this.icon, + required this.label, + required this.onPressed, + this.dismissOnPress = true, + }); + + final IconData icon; + final String label; + final VoidCallback onPressed; + + /// Whether the modal should be dismissed when an action is pressed. + /// + /// Default to true. + final bool dismissOnPress; +} From e482ab87652f8b6681657743e3756f206c21a725 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 27 Nov 2024 21:31:33 +0100 Subject: [PATCH 803/979] Restore clock widget --- .../view/broadcast/broadcast_game_screen.dart | 68 ++++++++++++------- lib/src/widgets/clock.dart | 3 +- test/widgets/clock_test.dart | 4 +- 3 files changed, 48 insertions(+), 27 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_game_screen.dart b/lib/src/view/broadcast/broadcast_game_screen.dart index ad57caf629..0338f8c02c 100644 --- a/lib/src/view/broadcast/broadcast_game_screen.dart +++ b/lib/src/view/broadcast/broadcast_game_screen.dart @@ -443,29 +443,24 @@ class _PlayerWidget extends ConsumerWidget { child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), - child: CountdownClockBuilder( - timeLeft: clock, - active: side == sideToMove && isCursorOnLiveMove, - builder: (context, timeLeft) => Text( - timeLeft.toHoursMinutesSeconds(), - style: TextStyle( - color: (side == sideToMove) - ? isCursorOnLiveMove - ? Theme.of(context) - .colorScheme - .onTertiaryContainer - : Theme.of(context) - .colorScheme - .onSecondaryContainer - : null, - fontFeatures: const [FontFeature.tabularFigures()], - ), - ), - tickInterval: const Duration(seconds: 1), - clockUpdatedAt: (side == sideToMove && isCursorOnLiveMove) - ? game.updatedClockAt - : null, - ), + child: isCursorOnLiveMove + ? CountdownClockBuilder( + timeLeft: clock, + active: side == sideToMove, + builder: (context, timeLeft) => _Clock( + timeLeft: timeLeft, + isSideToMove: side == sideToMove, + isLive: true, + ), + tickInterval: const Duration(seconds: 1), + clockUpdatedAt: + side == sideToMove ? game.updatedClockAt : null, + ) + : _Clock( + timeLeft: clock, + isSideToMove: side == sideToMove, + isLive: false, + ), ), ), ], @@ -473,3 +468,30 @@ class _PlayerWidget extends ConsumerWidget { ); } } + +class _Clock extends StatelessWidget { + const _Clock({ + required this.timeLeft, + required this.isSideToMove, + required this.isLive, + }); + + final Duration timeLeft; + final bool isSideToMove; + final bool isLive; + + @override + Widget build(BuildContext context) { + return Text( + timeLeft.toHoursMinutesSeconds(), + style: TextStyle( + color: isSideToMove + ? isLive + ? Theme.of(context).colorScheme.onTertiaryContainer + : Theme.of(context).colorScheme.onSecondaryContainer + : null, + fontFeatures: const [FontFeature.tabularFigures()], + ), + ); + } +} diff --git a/lib/src/widgets/clock.dart b/lib/src/widgets/clock.dart index 19f653ea1c..7de23af3e0 100644 --- a/lib/src/widgets/clock.dart +++ b/lib/src/widgets/clock.dart @@ -287,8 +287,7 @@ class _CountdownClockState extends State { void didUpdateWidget(CountdownClockBuilder oldClock) { super.didUpdateWidget(oldClock); - if (widget.timeLeft != oldClock.timeLeft || - widget.clockUpdatedAt != oldClock.clockUpdatedAt) { + if (widget.clockUpdatedAt != oldClock.clockUpdatedAt) { timeLeft = widget.timeLeft; } diff --git a/test/widgets/clock_test.dart b/test/widgets/clock_test.dart index 7bdf3759a1..73e20078f6 100644 --- a/test/widgets/clock_test.dart +++ b/test/widgets/clock_test.dart @@ -115,7 +115,7 @@ void main() { expect(find.text('0:00.0'), findsOneWidget); }); - testWidgets('do not update if timeLeft and clockUpdatedAt are same', + testWidgets('do not update if clockUpdatedAt is same', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( @@ -138,7 +138,7 @@ void main() { await tester.pumpWidget( MaterialApp( home: CountdownClockBuilder( - timeLeft: const Duration(seconds: 10), + timeLeft: const Duration(seconds: 11), active: true, builder: clockBuilder, ), From 02d6f1b56362c500fd03b6a5c86b152ff9f93c18 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 27 Nov 2024 21:35:17 +0100 Subject: [PATCH 804/979] Tweak broadcast title --- lib/src/view/broadcast/broadcast_game_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/view/broadcast/broadcast_game_screen.dart b/lib/src/view/broadcast/broadcast_game_screen.dart index 0338f8c02c..f450060954 100644 --- a/lib/src/view/broadcast/broadcast_game_screen.dart +++ b/lib/src/view/broadcast/broadcast_game_screen.dart @@ -49,7 +49,7 @@ class BroadcastGameScreen extends ConsumerWidget { return PlatformScaffold( appBar: PlatformAppBar( - title: Text(title), + title: Text(title, overflow: TextOverflow.ellipsis, maxLines: 1), actions: [ AppBarIconButton( onPressed: (broadcastGameState.hasValue) From c5cb3aac4138b8cd22935b913d6c5a31e74967bd Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 27 Nov 2024 21:43:11 +0100 Subject: [PATCH 805/979] Bump version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 79bb3041db..0194b6f27b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: lichess_mobile description: Lichess mobile app V2 publish_to: "none" -version: 0.13.3+001303 # See README.md for details about versioning +version: 0.13.4+001304 # See README.md for details about versioning environment: sdk: ">=3.5.0 <4.0.0" From bc8d16758c8e46ff1d87909bacdd8da7480bd933 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 28 Nov 2024 11:12:15 +0100 Subject: [PATCH 806/979] Reduce delay of piece sound with animation --- lib/src/model/game/game_controller.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/model/game/game_controller.dart b/lib/src/model/game/game_controller.dart index 46cbb5d52b..88e637a761 100644 --- a/lib/src/model/game/game_controller.dart +++ b/lib/src/model/game/game_controller.dart @@ -477,7 +477,7 @@ class GameController extends _$GameController { final animationDuration = ref.read(boardPreferencesProvider).pieceAnimationDuration; - final delay = animationDuration - const Duration(milliseconds: 10); + final delay = animationDuration ~/ 2; if (skipAnimationDelay || delay <= Duration.zero) { _moveFeedback(sanMove); From f94f3ad7658cbc908730a2c0f9ff45805fbeb1c7 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 28 Nov 2024 11:12:39 +0100 Subject: [PATCH 807/979] Fix analysis portrait board radius --- lib/src/view/analysis/analysis_layout.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/view/analysis/analysis_layout.dart b/lib/src/view/analysis/analysis_layout.dart index 341a2b7add..b89ee0a878 100644 --- a/lib/src/view/analysis/analysis_layout.dart +++ b/lib/src/view/analysis/analysis_layout.dart @@ -258,7 +258,7 @@ class AnalysisLayout extends StatelessWidget { child: boardBuilder( context, boardSize, - tabletBoardRadius, + isTablet ? tabletBoardRadius : null, ), ), Expanded( From b9d7baab5e1d0800db4f728f190fc2ac8f9888d1 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Thu, 28 Nov 2024 23:58:25 +0100 Subject: [PATCH 808/979] add border radius to the whole player widget and not every card widget --- .../view/broadcast/broadcast_game_screen.dart | 49 ++++++++++++++++--- 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_game_screen.dart b/lib/src/view/broadcast/broadcast_game_screen.dart index f450060954..194903e2b5 100644 --- a/lib/src/view/broadcast/broadcast_game_screen.dart +++ b/lib/src/view/broadcast/broadcast_game_screen.dart @@ -314,7 +314,7 @@ class _PlayerWidget extends ConsumerWidget { final BroadcastGameId gameId; final double width; final _PlayerWidgetPosition widgetPosition; - final BorderRadiusGeometry? borderRadius; + final BorderRadius? borderRadius; @override Widget build(BuildContext context, WidgetRef ref) { @@ -345,8 +345,10 @@ class _PlayerWidget extends ConsumerWidget { if (game.isOver) Card( margin: EdgeInsets.zero, - shape: RoundedRectangleBorder( - borderRadius: borderRadius ?? BorderRadius.zero, + shape: _makePlayerWidgetBorder( + position: widgetPosition, + borderRadius: borderRadius, + hasLeftBorderRadius: true, ), child: Padding( padding: const EdgeInsets.symmetric( @@ -371,8 +373,11 @@ class _PlayerWidget extends ConsumerWidget { Expanded( child: Card( margin: EdgeInsets.zero, - shape: RoundedRectangleBorder( - borderRadius: borderRadius ?? BorderRadius.zero, + shape: _makePlayerWidgetBorder( + position: widgetPosition, + borderRadius: borderRadius, + hasLeftBorderRadius: !game.isOver, + hasRightBorderRadius: clock == null, ), child: Padding( padding: @@ -437,8 +442,10 @@ class _PlayerWidget extends ConsumerWidget { : Theme.of(context).colorScheme.secondaryContainer : null, margin: EdgeInsets.zero, - shape: RoundedRectangleBorder( - borderRadius: borderRadius ?? BorderRadius.zero, + shape: _makePlayerWidgetBorder( + position: widgetPosition, + borderRadius: borderRadius, + hasRightBorderRadius: true, ), child: Padding( padding: @@ -495,3 +502,31 @@ class _Clock extends StatelessWidget { ); } } + +ShapeBorder _makePlayerWidgetBorder({ + required BorderRadius? borderRadius, + required _PlayerWidgetPosition position, + bool hasLeftBorderRadius = false, + bool hasRightBorderRadius = false, +}) { + if (borderRadius == null) return const Border(); + + if (!hasLeftBorderRadius && !hasRightBorderRadius) return const Border(); + + return RoundedRectangleBorder( + borderRadius: switch (position) { + _PlayerWidgetPosition.top => borderRadius.copyWith( + topLeft: hasLeftBorderRadius ? null : Radius.zero, + topRight: hasRightBorderRadius ? null : Radius.zero, + bottomLeft: Radius.zero, + bottomRight: Radius.zero, + ), + _PlayerWidgetPosition.bottom => borderRadius.copyWith( + topLeft: Radius.zero, + topRight: Radius.zero, + bottomLeft: hasLeftBorderRadius ? null : Radius.zero, + bottomRight: hasRightBorderRadius ? null : Radius.zero, + ), + }, + ); +} From e566bdfe4bf1676a1ee4d973a7053c5f26cd42da Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Fri, 29 Nov 2024 10:23:01 +0100 Subject: [PATCH 809/979] renaming --- lib/src/view/broadcast/broadcast_game_screen.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_game_screen.dart b/lib/src/view/broadcast/broadcast_game_screen.dart index 194903e2b5..fafe8fdf13 100644 --- a/lib/src/view/broadcast/broadcast_game_screen.dart +++ b/lib/src/view/broadcast/broadcast_game_screen.dart @@ -346,7 +346,7 @@ class _PlayerWidget extends ConsumerWidget { Card( margin: EdgeInsets.zero, shape: _makePlayerWidgetBorder( - position: widgetPosition, + widgetPosition: widgetPosition, borderRadius: borderRadius, hasLeftBorderRadius: true, ), @@ -374,7 +374,7 @@ class _PlayerWidget extends ConsumerWidget { child: Card( margin: EdgeInsets.zero, shape: _makePlayerWidgetBorder( - position: widgetPosition, + widgetPosition: widgetPosition, borderRadius: borderRadius, hasLeftBorderRadius: !game.isOver, hasRightBorderRadius: clock == null, @@ -443,7 +443,7 @@ class _PlayerWidget extends ConsumerWidget { : null, margin: EdgeInsets.zero, shape: _makePlayerWidgetBorder( - position: widgetPosition, + widgetPosition: widgetPosition, borderRadius: borderRadius, hasRightBorderRadius: true, ), @@ -505,7 +505,7 @@ class _Clock extends StatelessWidget { ShapeBorder _makePlayerWidgetBorder({ required BorderRadius? borderRadius, - required _PlayerWidgetPosition position, + required _PlayerWidgetPosition widgetPosition, bool hasLeftBorderRadius = false, bool hasRightBorderRadius = false, }) { @@ -514,7 +514,7 @@ ShapeBorder _makePlayerWidgetBorder({ if (!hasLeftBorderRadius && !hasRightBorderRadius) return const Border(); return RoundedRectangleBorder( - borderRadius: switch (position) { + borderRadius: switch (widgetPosition) { _PlayerWidgetPosition.top => borderRadius.copyWith( topLeft: hasLeftBorderRadius ? null : Radius.zero, topRight: hasRightBorderRadius ? null : Radius.zero, From d7d2db19847985647acb434f8bdeea2c7732b8ff Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Sun, 1 Dec 2024 15:49:01 +0100 Subject: [PATCH 810/979] fix(study): make bottom bar back/next consistent with rest of the app Fixes #1195 --- lib/src/view/study/study_bottom_bar.dart | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/src/view/study/study_bottom_bar.dart b/lib/src/view/study/study_bottom_bar.dart index fb38e51b29..f1eb375764 100644 --- a/lib/src/view/study/study_bottom_bar.dart +++ b/lib/src/view/study/study_bottom_bar.dart @@ -53,6 +53,14 @@ class _AnalysisBottomBar extends ConsumerWidget { return BottomBar( children: [ + _NextChapterButton( + id: id, + chapterId: state.study.chapter.id, + hasNextChapter: state.hasNextChapter, + blink: !state.isIntroductoryChapter && + state.isAtEndOfChapter && + state.hasNextChapter, + ), RepeatButton( onLongPress: onGoBack, child: BottomBarButton( @@ -64,14 +72,6 @@ class _AnalysisBottomBar extends ConsumerWidget { showTooltip: false, ), ), - _NextChapterButton( - id: id, - chapterId: state.study.chapter.id, - hasNextChapter: state.hasNextChapter, - blink: !state.isIntroductoryChapter && - state.isAtEndOfChapter && - state.hasNextChapter, - ), RepeatButton( onLongPress: onGoForward, child: BottomBarButton( From 7aee3bbf7528e96917bd61bfbc6dea73f9a6179a Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Sun, 1 Dec 2024 18:03:02 +0100 Subject: [PATCH 811/979] More layout fixes --- lib/src/constants.dart | 2 + lib/src/view/analysis/analysis_layout.dart | 211 +++++++------- lib/src/widgets/board_table.dart | 304 ++++++++++----------- test/widgets/board_table_test.dart | 98 ++++++- 4 files changed, 325 insertions(+), 290 deletions(-) diff --git a/lib/src/constants.dart b/lib/src/constants.dart index caad94ac7d..7d3bbf0b19 100644 --- a/lib/src/constants.dart +++ b/lib/src/constants.dart @@ -41,6 +41,8 @@ const kClueLessDeviation = 230; // UI +const kGoldenRatio = 1.61803398875; + /// Flex golden ratio base (flex has to be an int). const kFlexGoldenRatioBase = 100000000000; diff --git a/lib/src/view/analysis/analysis_layout.dart b/lib/src/view/analysis/analysis_layout.dart index b89ee0a878..948b660c36 100644 --- a/lib/src/view/analysis/analysis_layout.dart +++ b/lib/src/view/analysis/analysis_layout.dart @@ -4,7 +4,6 @@ import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; -import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; @@ -146,137 +145,113 @@ class AnalysisLayout extends StatelessWidget { Expanded( child: SafeArea( bottom: false, - child: OrientationBuilder( - builder: (context, orientation) { + child: LayoutBuilder( + builder: (context, constraints) { + final orientation = constraints.maxWidth > constraints.maxHeight + ? Orientation.landscape + : Orientation.portrait; final isTablet = isTabletOrLarger(context); const tabletBoardRadius = BorderRadius.all(Radius.circular(4.0)); if (orientation == Orientation.landscape) { - return Row( - mainAxisSize: MainAxisSize.max, - children: [ - Expanded( - flex: kFlexGoldenRatio, - child: Padding( - padding: const EdgeInsets.only( - left: kTabletBoardTableSidePadding, - top: kTabletBoardTableSidePadding, - bottom: kTabletBoardTableSidePadding, + final sideWidth = constraints.biggest.longestSide - + constraints.biggest.shortestSide; + final defaultBoardSize = constraints.biggest.shortestSide - + (kTabletBoardTableSidePadding * 2); + final boardSize = sideWidth >= 250 + ? defaultBoardSize + : constraints.biggest.longestSide / kGoldenRatio - + (kTabletBoardTableSidePadding * 2); + return Padding( + padding: const EdgeInsets.all(kTabletBoardTableSidePadding), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + boardBuilder( + context, + boardSize, + isTablet ? tabletBoardRadius : null, + ), + if (engineGaugeBuilder != null) ...[ + const SizedBox(width: 4.0), + engineGaugeBuilder!( + context, + Orientation.landscape, ), - child: LayoutBuilder( - builder: (context, constraints) { - final boardSize = - constraints.biggest.shortestSide - - (engineGaugeBuilder != null - ? kEvalGaugeSize + 4.0 - : 0); - return Row( - children: [ - boardBuilder( - context, - boardSize, - isTablet ? tabletBoardRadius : null, + ], + const SizedBox(width: 16.0), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + if (engineLines != null) engineLines!, + Expanded( + child: PlatformCard( + clipBehavior: Clip.hardEdge, + borderRadius: const BorderRadius.all( + Radius.circular(4.0), + ), + semanticContainer: false, + child: TabBarView( + controller: tabController, + children: children, ), - if (engineGaugeBuilder != null) ...[ - const SizedBox(width: 4.0), - engineGaugeBuilder!( - context, - Orientation.landscape, - ), - ], - ], - ); - }, - ), - ), - ), - Flexible( - flex: kFlexGoldenRatioBase, - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - if (engineLines != null) - Padding( - padding: const EdgeInsets.only( - top: kTabletBoardTableSidePadding, - left: kTabletBoardTableSidePadding, - right: kTabletBoardTableSidePadding, - ), - child: engineLines, - ), - Expanded( - child: PlatformCard( - clipBehavior: Clip.hardEdge, - borderRadius: const BorderRadius.all( - Radius.circular(4.0), - ), - margin: const EdgeInsets.all( - kTabletBoardTableSidePadding, - ), - semanticContainer: false, - child: TabBarView( - controller: tabController, - children: children, ), ), - ), - ], + ], + ), ), - ), - ], + ], + ), ); } else { - return LayoutBuilder( - builder: (context, constraints) { - final defaultBoardSize = constraints.biggest.shortestSide; - final remainingHeight = - constraints.maxHeight - defaultBoardSize; - final isSmallScreen = remainingHeight < - kSmallRemainingHeightLeftBoardThreshold; - final boardSize = isTablet || isSmallScreen - ? defaultBoardSize - kTabletBoardTableSidePadding * 2 - : defaultBoardSize; + final defaultBoardSize = constraints.biggest.shortestSide; + final remainingHeight = + constraints.maxHeight - defaultBoardSize; + final isSmallScreen = + remainingHeight < kSmallRemainingHeightLeftBoardThreshold; + final boardSize = isTablet || isSmallScreen + ? defaultBoardSize - kTabletBoardTableSidePadding * 2 + : defaultBoardSize; - return Column( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - if (engineGaugeBuilder != null) - engineGaugeBuilder!( - context, - Orientation.portrait, - ), - if (engineLines != null) engineLines!, - Padding( - padding: isTablet - ? const EdgeInsets.all( - kTabletBoardTableSidePadding, - ) - : EdgeInsets.zero, - child: boardBuilder( - context, - boardSize, - isTablet ? tabletBoardRadius : null, - ), - ), - Expanded( - child: Padding( - padding: isTablet - ? const EdgeInsets.symmetric( - horizontal: kTabletBoardTableSidePadding, - ) - : EdgeInsets.zero, - child: TabBarView( - controller: tabController, - children: children, - ), - ), + return Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (engineGaugeBuilder != null) + engineGaugeBuilder!( + context, + Orientation.portrait, + ), + if (engineLines != null) engineLines!, + Padding( + padding: isTablet + ? const EdgeInsets.all( + kTabletBoardTableSidePadding, + ) + : EdgeInsets.zero, + child: boardBuilder( + context, + boardSize, + isTablet ? tabletBoardRadius : null, + ), + ), + Expanded( + child: Padding( + padding: isTablet + ? const EdgeInsets.symmetric( + horizontal: kTabletBoardTableSidePadding, + ) + : EdgeInsets.zero, + child: TabBarView( + controller: tabController, + children: children, ), - ], - ); - }, + ), + ), + ], ); } }, diff --git a/lib/src/widgets/board_table.dart b/lib/src/widgets/board_table.dart index acbd6e99e9..375961ee68 100644 --- a/lib/src/widgets/board_table.dart +++ b/lib/src/widgets/board_table.dart @@ -130,8 +130,11 @@ class _BoardTableState extends ConsumerState { Widget build(BuildContext context) { final boardPrefs = ref.watch(boardPreferencesProvider); - return OrientationBuilder( - builder: (context, orientation) { + return LayoutBuilder( + builder: (context, constraints) { + final orientation = constraints.maxWidth > constraints.maxHeight + ? Orientation.landscape + : Orientation.portrait; final isTablet = isTabletOrLarger(context); final defaultSettings = boardPrefs.toBoardSettings().copyWith( @@ -154,172 +157,149 @@ class _BoardTableState extends ConsumerState { final shapes = userShapes.union(widget.shapes ?? ISet()); final slicedMoves = widget.moves?.asMap().entries.slices(2); - return orientation == Orientation.landscape - ? Row( - mainAxisSize: MainAxisSize.max, - children: [ - Expanded( - flex: kFlexGoldenRatio, - child: Padding( - padding: isTablet - ? const EdgeInsets.only( - left: kTabletBoardTableSidePadding, - top: kTabletBoardTableSidePadding, - bottom: kTabletBoardTableSidePadding, - ) - : EdgeInsets.zero, - child: LayoutBuilder( - builder: (context, constraints) { - final boardSize = constraints.biggest.shortestSide - - (widget.engineGauge != null || - widget.showEngineGaugePlaceholder - ? kEvalGaugeSize - : 0); - return Row( - children: [ - _BoardWidget( - size: boardSize, - boardPrefs: boardPrefs, - fen: widget.fen, - orientation: widget.orientation, - gameData: widget.gameData, - lastMove: widget.lastMove, - shapes: shapes, - settings: settings, - boardKey: widget.boardKey, - boardOverlay: widget.boardOverlay, - error: widget.errorMessage, - ), - if (widget.engineGauge != null) - EngineGauge( - params: widget.engineGauge!, - displayMode: EngineGaugeDisplayMode.vertical, - ) - else if (widget.showEngineGaugePlaceholder) - const SizedBox(width: kEvalGaugeSize), - ], - ); - }, - ), - ), - ), - Flexible( - flex: kFlexGoldenRatioBase, - child: Padding( - padding: isTablet - ? const EdgeInsets.all(kTabletBoardTableSidePadding) - : EdgeInsets.zero, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - widget.topTable, - if (!widget.zenMode && slicedMoves != null) - Expanded( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: MoveList( - type: MoveListType.stacked, - slicedMoves: slicedMoves, - currentMoveIndex: - widget.currentMoveIndex ?? 0, - onSelectMove: widget.onSelectMove, - ), - ), - ) - else - const Spacer(), - widget.bottomTable, - ], - ), - ), + if (orientation == Orientation.landscape) { + final defaultBoardSize = constraints.biggest.shortestSide - + (kTabletBoardTableSidePadding * 2); + final sideWidth = constraints.biggest.longestSide - defaultBoardSize; + final boardSize = sideWidth >= 250 + ? defaultBoardSize + : constraints.biggest.longestSide / kGoldenRatio - + (kTabletBoardTableSidePadding * 2); + return Padding( + padding: const EdgeInsets.all(kTabletBoardTableSidePadding), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + _BoardWidget( + size: boardSize, + boardPrefs: boardPrefs, + fen: widget.fen, + orientation: widget.orientation, + gameData: widget.gameData, + lastMove: widget.lastMove, + shapes: shapes, + settings: settings, + boardKey: widget.boardKey, + boardOverlay: widget.boardOverlay, + error: widget.errorMessage, + ), + if (widget.engineGauge != null) ...[ + const SizedBox(width: 4.0), + EngineGauge( + params: widget.engineGauge!, + displayMode: EngineGaugeDisplayMode.vertical, ), + ] else if (widget.showEngineGaugePlaceholder) ...[ + const SizedBox(width: kEvalGaugeSize + 4.0), ], - ) - : LayoutBuilder( - builder: (context, constraints) { - final defaultBoardSize = constraints.biggest.shortestSide; - final double boardSize = isTablet - ? defaultBoardSize - kTabletBoardTableSidePadding * 2 - : defaultBoardSize; - - // vertical space left on portrait mode to check if we can display the - // move list - final verticalSpaceLeftBoardOnPortrait = - constraints.biggest.height - boardSize; - - return Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.center, + const SizedBox(width: 16.0), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - if (!widget.zenMode && - slicedMoves != null && - verticalSpaceLeftBoardOnPortrait >= 130) - MoveList( - type: MoveListType.inline, - slicedMoves: slicedMoves, - currentMoveIndex: widget.currentMoveIndex ?? 0, - onSelectMove: widget.onSelectMove, - ) - else if (widget.showMoveListPlaceholder && - verticalSpaceLeftBoardOnPortrait >= 130) - const SizedBox(height: 40), - Expanded( - child: Padding( - padding: EdgeInsets.symmetric( - horizontal: - isTablet ? kTabletBoardTableSidePadding : 12.0, - ), - child: widget.topTable, - ), - ), - if (widget.engineGauge != null) - Padding( - padding: isTablet - ? const EdgeInsets.symmetric( - horizontal: kTabletBoardTableSidePadding, - ) - : EdgeInsets.zero, - child: EngineGauge( - params: widget.engineGauge!, - displayMode: EngineGaugeDisplayMode.horizontal, + widget.topTable, + if (!widget.zenMode && slicedMoves != null) + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: MoveList( + type: MoveListType.stacked, + slicedMoves: slicedMoves, + currentMoveIndex: widget.currentMoveIndex ?? 0, + onSelectMove: widget.onSelectMove, + ), ), ) - else if (widget.showEngineGaugePlaceholder) - const SizedBox(height: kEvalGaugeSize), - Padding( - padding: isTablet - ? const EdgeInsets.symmetric( - horizontal: kTabletBoardTableSidePadding, - ) - : EdgeInsets.zero, - child: _BoardWidget( - size: boardSize, - boardPrefs: boardPrefs, - fen: widget.fen, - orientation: widget.orientation, - gameData: widget.gameData, - lastMove: widget.lastMove, - shapes: shapes, - settings: settings, - boardKey: widget.boardKey, - boardOverlay: widget.boardOverlay, - error: widget.errorMessage, - ), - ), - Expanded( - child: Padding( - padding: EdgeInsets.symmetric( - horizontal: - isTablet ? kTabletBoardTableSidePadding : 12.0, - ), - child: widget.bottomTable, - ), - ), + else + const Spacer(), + widget.bottomTable, ], - ); - }, - ); + ), + ), + ], + ), + ); + } else { + final defaultBoardSize = constraints.biggest.shortestSide; + final double boardSize = isTablet + ? defaultBoardSize - kTabletBoardTableSidePadding * 2 + : defaultBoardSize; + + // vertical space left on portrait mode to check if we can display the + // move list + final verticalSpaceLeftBoardOnPortrait = + constraints.biggest.height - boardSize; + + return Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (!widget.zenMode && + slicedMoves != null && + verticalSpaceLeftBoardOnPortrait >= 130) + MoveList( + type: MoveListType.inline, + slicedMoves: slicedMoves, + currentMoveIndex: widget.currentMoveIndex ?? 0, + onSelectMove: widget.onSelectMove, + ) + else if (widget.showMoveListPlaceholder && + verticalSpaceLeftBoardOnPortrait >= 130) + const SizedBox(height: 40), + Expanded( + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: isTablet ? kTabletBoardTableSidePadding : 12.0, + ), + child: widget.topTable, + ), + ), + if (widget.engineGauge != null) + Padding( + padding: isTablet + ? const EdgeInsets.symmetric( + horizontal: kTabletBoardTableSidePadding, + ) + : EdgeInsets.zero, + child: EngineGauge( + params: widget.engineGauge!, + displayMode: EngineGaugeDisplayMode.horizontal, + ), + ) + else if (widget.showEngineGaugePlaceholder) + const SizedBox(height: kEvalGaugeSize), + Padding( + padding: isTablet + ? const EdgeInsets.symmetric( + horizontal: kTabletBoardTableSidePadding, + ) + : EdgeInsets.zero, + child: _BoardWidget( + size: boardSize, + boardPrefs: boardPrefs, + fen: widget.fen, + orientation: widget.orientation, + gameData: widget.gameData, + lastMove: widget.lastMove, + shapes: shapes, + settings: settings, + boardKey: widget.boardKey, + boardOverlay: widget.boardOverlay, + error: widget.errorMessage, + ), + ), + Expanded( + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: isTablet ? kTabletBoardTableSidePadding : 12.0, + ), + child: widget.bottomTable, + ), + ), + ], + ); + } }, ); } diff --git a/test/widgets/board_table_test.dart b/test/widgets/board_table_test.dart index d9d1b7b3ad..5701a463df 100644 --- a/test/widgets/board_table_test.dart +++ b/test/widgets/board_table_test.dart @@ -1,7 +1,10 @@ +import 'dart:math'; + import 'package:chessground/chessground.dart'; import 'package:dartchess/dartchess.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/widgets/board_table.dart'; import '../test_helpers.dart'; @@ -26,7 +29,8 @@ const surfaces = [ // folded motorola Size(564.7, 482.6), // pixel fold unfolded - Size(701.0, 640.8), + Size(701.0, 841.1), + Size(841.1, 701.0), ]; void main() { @@ -41,13 +45,19 @@ void main() { home: BoardTable( orientation: Side.white, fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR', - topTable: Padding( - padding: EdgeInsets.all(8.0), - child: Text('Top table'), + topTable: Row( + mainAxisSize: MainAxisSize.max, + key: ValueKey('top_table'), + children: [ + Text('Top table'), + ], ), - bottomTable: Padding( - padding: EdgeInsets.all(8.0), - child: Text('Bottom table'), + bottomTable: Row( + mainAxisSize: MainAxisSize.max, + key: ValueKey('bottom_table'), + children: [ + Text('Bottom table'), + ], ), ), ), @@ -62,7 +72,7 @@ void main() { expect( backgroundSize.width, backgroundSize.height, - reason: 'Board background size is square', + reason: 'Board background size is square on $surface', ); final boardSize = tester.getSize(find.byType(Chessboard)); @@ -70,17 +80,53 @@ void main() { expect( boardSize.width, boardSize.height, - reason: 'Board size is square', + reason: 'Board size is square on $surface', ); expect( boardSize, backgroundSize, - reason: 'Board size should match background size', + reason: 'Board size should match background size on $surface', + ); + } + }, + variant: kPlatformVariant, + ); + + testWidgets( + 'board size and table side size should be harmonious on all surfaces', + (WidgetTester tester) async { + for (final surface in surfaces) { + final app = await makeTestProviderScope( + key: ValueKey(surface), + tester, + child: const MaterialApp( + home: BoardTable( + orientation: Side.white, + fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR', + topTable: Row( + mainAxisSize: MainAxisSize.max, + key: ValueKey('top_table'), + children: [ + Text('Top table'), + ], + ), + bottomTable: Row( + mainAxisSize: MainAxisSize.max, + key: ValueKey('bottom_table'), + children: [ + Text('Bottom table'), + ], + ), + ), + ), + surfaceSize: surface, ); + await tester.pumpWidget(app); final isPortrait = surface.aspectRatio < 1.0; final isTablet = surface.shortestSide > 600; + final boardSize = tester.getSize(find.byType(Chessboard)); if (isPortrait) { final expectedBoardSize = @@ -88,6 +134,38 @@ void main() { expect( boardSize, Size(expectedBoardSize, expectedBoardSize), + reason: 'Board size should match surface width on $surface', + ); + } else { + final topTableSize = + tester.getSize(find.byKey(const ValueKey('top_table'))); + final bottomTableSize = + tester.getSize(find.byKey(const ValueKey('bottom_table'))); + final minBoardSize = (surface.longestSide / kGoldenRatio) - 32.0; + final maxBoardSize = surface.longestSide - 32.0; + final minSideWidth = + min(surface.longestSide - minBoardSize - 16.0 * 3, 250.0); + expect( + boardSize.width, + greaterThanOrEqualTo(minBoardSize), + reason: 'Board size should be at least $minBoardSize on $surface', + ); + expect( + boardSize.width, + lessThanOrEqualTo(maxBoardSize), + reason: 'Board size should be at most $maxBoardSize on $surface', + ); + expect( + bottomTableSize.width, + greaterThanOrEqualTo(minSideWidth), + reason: + 'Bottom table width should be at least $minSideWidth on $surface', + ); + expect( + topTableSize.width, + greaterThanOrEqualTo(minSideWidth), + reason: + 'Top table width should be at least $minSideWidth on $surface', ); } } From 109e2c390b1f879d7898e8e8335f66aa7ddcd625 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 2 Dec 2024 11:40:48 +0100 Subject: [PATCH 812/979] Add tests for analysis layout --- lib/src/view/analysis/analysis_layout.dart | 1 + test/test_helpers.dart | 23 +++ test/view/analysis/analysis_layout_test.dart | 146 +++++++++++++++++++ test/widgets/board_table_test.dart | 35 +---- 4 files changed, 177 insertions(+), 28 deletions(-) create mode 100644 test/view/analysis/analysis_layout_test.dart diff --git a/lib/src/view/analysis/analysis_layout.dart b/lib/src/view/analysis/analysis_layout.dart index 948b660c36..7b7cdf1413 100644 --- a/lib/src/view/analysis/analysis_layout.dart +++ b/lib/src/view/analysis/analysis_layout.dart @@ -184,6 +184,7 @@ class AnalysisLayout extends StatelessWidget { Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ if (engineLines != null) engineLines!, Expanded( diff --git a/test/test_helpers.dart b/test/test_helpers.dart index dfedcc6b3c..3256f9b2ab 100644 --- a/test/test_helpers.dart +++ b/test/test_helpers.dart @@ -11,6 +11,29 @@ import 'package:http/http.dart' as http; const double _kTestScreenWidth = 390.0; const double _kTestScreenHeight = 844.0; +const kTestSurfaces = [ + // https://www.browserstack.com/guide/common-screen-resolutions + // phones + Size(360, 800), + Size(390, 844), + Size(393, 873), + Size(412, 915), + Size(414, 896), + Size(360, 780), + // tablets + Size(600, 1024), + Size(810, 1080), + Size(820, 1180), + Size(1280, 800), + Size(800, 1280), + Size(601, 962), + // folded motorola + Size(564.7, 482.6), + // pixel fold unfolded + Size(701.0, 841.1), + Size(841.1, 701.0), +]; + /// iPhone 14 screen size. const kTestSurfaceSize = Size(_kTestScreenWidth, _kTestScreenHeight); diff --git a/test/view/analysis/analysis_layout_test.dart b/test/view/analysis/analysis_layout_test.dart new file mode 100644 index 0000000000..fd0c130feb --- /dev/null +++ b/test/view/analysis/analysis_layout_test.dart @@ -0,0 +1,146 @@ +import 'dart:math'; + +import 'package:chessground/chessground.dart'; +import 'package:dartchess/dartchess.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:lichess_mobile/src/constants.dart'; +import 'package:lichess_mobile/src/view/analysis/analysis_layout.dart'; + +import '../../test_helpers.dart'; +import '../../test_provider_scope.dart'; + +void main() { + testWidgets( + 'board background size should match board size on all surfaces', + (WidgetTester tester) async { + for (final surface in kTestSurfaces) { + final app = await makeTestProviderScope( + key: ValueKey(surface), + tester, + child: MaterialApp( + home: DefaultTabController( + length: 1, + child: AnalysisLayout( + boardBuilder: (context, boardSize, boardRadius) { + return Chessboard.fixed( + size: boardSize, + fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR', + orientation: Side.white, + ); + }, + bottomBar: const SizedBox(height: kBottomBarHeight), + children: const [ + Center(child: Text('Analysis tab')), + ], + ), + ), + ), + surfaceSize: surface, + ); + await tester.pumpWidget(app); + + final backgroundSize = tester.getSize( + find.byType(SolidColorChessboardBackground), + ); + + expect( + backgroundSize.width, + backgroundSize.height, + reason: 'Board background size is square on $surface', + ); + + final boardSize = tester.getSize(find.byType(Chessboard)); + + expect( + boardSize.width, + boardSize.height, + reason: 'Board size is square on $surface', + ); + + expect( + boardSize, + backgroundSize, + reason: 'Board size should match background size on $surface', + ); + } + }, + variant: kPlatformVariant, + ); + + testWidgets( + 'board size and table side size should be harmonious on all surfaces', + (WidgetTester tester) async { + for (final surface in kTestSurfaces) { + final app = await makeTestProviderScope( + key: ValueKey(surface), + tester, + child: MaterialApp( + home: DefaultTabController( + length: 1, + child: AnalysisLayout( + boardBuilder: (context, boardSize, boardRadius) { + return Chessboard.fixed( + size: boardSize, + fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR', + orientation: Side.white, + ); + }, + bottomBar: const SizedBox(height: kBottomBarHeight), + children: const [ + Center(child: Text('Analysis tab')), + ], + ), + ), + ), + surfaceSize: surface, + ); + await tester.pumpWidget(app); + + final isPortrait = surface.aspectRatio < 1.0; + final isTablet = surface.shortestSide > 600; + final boardSize = tester.getSize(find.byType(Chessboard)); + + if (isPortrait) { + final expectedBoardSize = + isTablet ? surface.width - 32.0 : surface.width; + expect( + boardSize, + Size(expectedBoardSize, expectedBoardSize), + reason: 'Board size should match surface width on $surface', + ); + } else { + final tabBarViewSize = tester.getSize(find.byType(TabBarView)); + final goldenBoardSize = (surface.longestSide / kGoldenRatio) - 32.0; + final defaultBoardSize = + surface.shortestSide - kBottomBarHeight - 32.0; + final minBoardSize = min(goldenBoardSize, defaultBoardSize); + final maxBoardSize = max(goldenBoardSize, defaultBoardSize); + // TabBarView is inside a Card so we need to account for its padding + const cardPadding = 8.0; + final minSideWidth = min( + surface.longestSide - goldenBoardSize - 16.0 * 3 - cardPadding, + 250.0, + ); + expect( + boardSize.width, + greaterThanOrEqualTo(minBoardSize), + reason: 'Board size should be at least $minBoardSize on $surface', + ); + expect( + boardSize.width, + lessThanOrEqualTo(maxBoardSize), + reason: 'Board size should be at most $maxBoardSize on $surface', + ); + expect( + tabBarViewSize.width, + greaterThanOrEqualTo(minSideWidth), + reason: + 'Tab bar view width should be at least $minSideWidth on $surface', + ); + } + } + }, + variant: kPlatformVariant, + ); +} diff --git a/test/widgets/board_table_test.dart b/test/widgets/board_table_test.dart index 5701a463df..f49bf98090 100644 --- a/test/widgets/board_table_test.dart +++ b/test/widgets/board_table_test.dart @@ -10,34 +10,11 @@ import 'package:lichess_mobile/src/widgets/board_table.dart'; import '../test_helpers.dart'; import '../test_provider_scope.dart'; -const surfaces = [ - // https://www.browserstack.com/guide/common-screen-resolutions - // phones - Size(360, 800), - Size(390, 844), - Size(393, 873), - Size(412, 915), - Size(414, 896), - Size(360, 780), - // tablets - Size(600, 1024), - Size(810, 1080), - Size(820, 1180), - Size(1280, 800), - Size(800, 1280), - Size(601, 962), - // folded motorola - Size(564.7, 482.6), - // pixel fold unfolded - Size(701.0, 841.1), - Size(841.1, 701.0), -]; - void main() { testWidgets( 'board background size should match board size on all surfaces', (WidgetTester tester) async { - for (final surface in surfaces) { + for (final surface in kTestSurfaces) { final app = await makeTestProviderScope( key: ValueKey(surface), tester, @@ -96,7 +73,7 @@ void main() { testWidgets( 'board size and table side size should be harmonious on all surfaces', (WidgetTester tester) async { - for (final surface in surfaces) { + for (final surface in kTestSurfaces) { final app = await makeTestProviderScope( key: ValueKey(surface), tester, @@ -141,10 +118,12 @@ void main() { tester.getSize(find.byKey(const ValueKey('top_table'))); final bottomTableSize = tester.getSize(find.byKey(const ValueKey('bottom_table'))); - final minBoardSize = (surface.longestSide / kGoldenRatio) - 32.0; - final maxBoardSize = surface.longestSide - 32.0; + final goldenBoardSize = (surface.longestSide / kGoldenRatio) - 32.0; + final defaultBoardSize = surface.shortestSide - 32.0; + final minBoardSize = min(goldenBoardSize, defaultBoardSize); + final maxBoardSize = max(goldenBoardSize, defaultBoardSize); final minSideWidth = - min(surface.longestSide - minBoardSize - 16.0 * 3, 250.0); + min(surface.longestSide - goldenBoardSize - 16.0 * 3, 250.0); expect( boardSize.width, greaterThanOrEqualTo(minBoardSize), From 2c706cabdb228c46884677315e53111f7117ce07 Mon Sep 17 00:00:00 2001 From: Noah <78898963+HaonRekcef@users.noreply.github.com> Date: Mon, 2 Dec 2024 12:56:20 +0100 Subject: [PATCH 813/979] add privacy setting for challenges --- .../model/account/account_preferences.dart | 51 ++++++++++ lib/src/model/account/account_repository.dart | 1 + .../settings/account_preferences_screen.dart | 99 +++++++++++++++++++ 3 files changed, 151 insertions(+) diff --git a/lib/src/model/account/account_preferences.dart b/lib/src/model/account/account_preferences.dart index 343843f1d7..27d1488df1 100644 --- a/lib/src/model/account/account_preferences.dart +++ b/lib/src/model/account/account_preferences.dart @@ -27,6 +27,7 @@ typedef AccountPrefState = ({ BooleanPref clockSound, // privacy BooleanPref follow, + Challenge challenge, }); /// A provider that tells if the user wants to see ratings in the app. @@ -71,6 +72,7 @@ final defaultAccountPreferences = ( SubmitMoveChoice.correspondence, }), follow: const BooleanPref(true), + challenge: Challenge.registered, ); /// Get the account preferences for the current user. @@ -116,6 +118,7 @@ class AccountPreferences extends _$AccountPreferences { _setPref('confirmResign', value); Future setSubmitMove(SubmitMove value) => _setPref('submitMove', value); Future setFollow(BooleanPref value) => _setPref('follow', value); + Future setChallenge(Challenge value) => _setPref('challenge', value); Future _setPref(String key, AccountPref value) async { await Future.delayed(const Duration(milliseconds: 200)); @@ -375,6 +378,54 @@ enum Moretime implements AccountPref { } } +enum Challenge implements AccountPref { + never(1), + rating(2), + friends(3), + registered(4), + always(5); + + const Challenge(this.value); + + @override + final int value; + + @override + String get toFormData => value.toString(); + + String label(BuildContext context) { + switch (this) { + case Challenge.never: + return context.l10n.never; + case Challenge.rating: + return context.l10n.ifRatingIsPlusMinusX('300'); + case Challenge.friends: + return context.l10n.onlyFriends; + case Challenge.registered: + return context.l10n.ifRegistered; + case Challenge.always: + return context.l10n.always; + } + } + + static Challenge fromInt(int value) { + switch (value) { + case 1: + return Challenge.never; + case 2: + return Challenge.rating; + case 3: + return Challenge.friends; + case 4: + return Challenge.registered; + case 5: + return Challenge.always; + default: + throw Exception('Invalid value for Challenge'); + } + } +} + class SubmitMove implements AccountPref { SubmitMove(Iterable choices) : choices = ISet(choices.toSet()); diff --git a/lib/src/model/account/account_repository.dart b/lib/src/model/account/account_repository.dart index 3a6c83c7f6..2ab4a6370a 100644 --- a/lib/src/model/account/account_repository.dart +++ b/lib/src/model/account/account_repository.dart @@ -165,6 +165,7 @@ AccountPrefState _accountPreferencesFromPick(RequiredPick pick) { pick('submitMove').asIntOrThrow(), ), follow: BooleanPref(pick('follow').asBoolOrThrow()), + challenge: Challenge.fromInt(pick('challenge').asIntOrThrow()), ); } diff --git a/lib/src/view/settings/account_preferences_screen.dart b/lib/src/view/settings/account_preferences_screen.dart index 13c79d3925..5804722ba7 100644 --- a/lib/src/view/settings/account_preferences_screen.dart +++ b/lib/src/view/settings/account_preferences_screen.dart @@ -396,6 +396,41 @@ class _AccountPreferencesScreenState ); }, ), + SettingsListTile( + settingsLabel: Text( + context.l10n.letOtherPlayersChallengeYou, + ), + settingsValue: data.challenge.label(context), + showCupertinoTrailingValue: false, + onTap: () { + if (Theme.of(context).platform == + TargetPlatform.android) { + showChoicePicker( + context, + choices: Challenge.values, + selectedItem: data.challenge, + labelBuilder: (t) => Text(t.label(context)), + onSelectedItemChanged: isLoading + ? null + : (Challenge? value) { + _setPref( + () => ref + .read( + accountPreferencesProvider.notifier, + ) + .setChallenge(value ?? data.challenge), + ); + }, + ); + } else { + pushPlatformRoute( + context, + title: context.l10n.letOtherPlayersChallengeYou, + builder: (context) => const AutoQueenSettingsScreen(), + ); + } + }, + ), ], ), ], @@ -800,3 +835,67 @@ class _MoretimeSettingsScreenState ); } } + +class ChallengeSettingsScreen extends ConsumerStatefulWidget { + const ChallengeSettingsScreen({super.key}); + + @override + ConsumerState createState() => + _ChallengeSettingsScreenState(); +} + +class _ChallengeSettingsScreenState + extends ConsumerState { + Future? _pendingSetChallenge; + + @override + Widget build(BuildContext context) { + final accountPrefs = ref.watch(accountPreferencesProvider); + return accountPrefs.when( + data: (data) { + if (data == null) { + return Center( + child: Text(context.l10n.mobileMustBeLoggedIn), + ); + } + + return FutureBuilder( + future: _pendingSetChallenge, + builder: (context, snapshot) { + return CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + trailing: snapshot.connectionState == ConnectionState.waiting + ? const CircularProgressIndicator.adaptive() + : null, + ), + child: SafeArea( + child: ListView( + children: [ + ChoicePicker( + choices: Challenge.values, + selectedItem: data.challenge, + titleBuilder: (t) => Text(t.label(context)), + onSelectedItemChanged: + snapshot.connectionState == ConnectionState.waiting + ? null + : (Challenge? v) { + final future = ref + .read(accountPreferencesProvider.notifier) + .setChallenge(v ?? data.challenge); + setState(() { + _pendingSetChallenge = future; + }); + }, + ), + ], + ), + ), + ); + }, + ); + }, + loading: () => const Center(child: CircularProgressIndicator()), + error: (err, stack) => Center(child: Text(err.toString())), + ); + } +} From 9bfe352d4865c5863ac5b50f4b9e90a8539f0f53 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 2 Dec 2024 13:18:08 +0100 Subject: [PATCH 814/979] WIP on adding opening explorer to broadcast screen --- .../broadcast/broadcast_game_controller.dart | 2 +- .../model/broadcast/broadcast_repository.dart | 11 +- lib/src/view/analysis/analysis_layout.dart | 20 +- lib/src/view/analysis/analysis_screen.dart | 20 +- .../view/analysis/opening_explorer_view.dart | 47 ++-- .../view/broadcast/broadcast_game_screen.dart | 166 +++++++++---- .../opening_explorer_screen.dart | 24 +- .../opening_explorer_view_builder.dart | 234 ++++++++++++++++++ .../opening_explorer_widgets.dart | 38 ++- 9 files changed, 437 insertions(+), 125 deletions(-) create mode 100644 lib/src/view/opening_explorer/opening_explorer_view_builder.dart diff --git a/lib/src/model/broadcast/broadcast_game_controller.dart b/lib/src/model/broadcast/broadcast_game_controller.dart index a0193cb9b4..fda93e621d 100644 --- a/lib/src/model/broadcast/broadcast_game_controller.dart +++ b/lib/src/model/broadcast/broadcast_game_controller.dart @@ -63,7 +63,7 @@ class BroadcastGameController extends _$BroadcastGameController }); final pgn = await ref.withClient( - (client) => BroadcastRepository(client).getGame(roundId, gameId), + (client) => BroadcastRepository(client).getGamePgn(roundId, gameId), ); final game = PgnGame.parsePgn(pgn); diff --git a/lib/src/model/broadcast/broadcast_repository.dart b/lib/src/model/broadcast/broadcast_repository.dart index afa5f61d1c..cc17739417 100644 --- a/lib/src/model/broadcast/broadcast_repository.dart +++ b/lib/src/model/broadcast/broadcast_repository.dart @@ -33,9 +33,7 @@ class BroadcastRepository { ); } - Future getRound( - BroadcastRoundId broadcastRoundId, - ) { + Future getRound(BroadcastRoundId broadcastRoundId) { return client.readJson( Uri(path: 'api/broadcast/-/-/$broadcastRoundId'), // The path parameters with - are the broadcast tournament and round slugs @@ -45,14 +43,11 @@ class BroadcastRepository { ); } - Future getGame( + Future getGamePgn( BroadcastRoundId roundId, BroadcastGameId gameId, ) { - return client.read( - Uri(path: 'api/study/$roundId/$gameId.pgn'), - headers: {'Accept': 'application/json'}, - ); + return client.read(Uri(path: 'api/study/$roundId/$gameId.pgn')); } } diff --git a/lib/src/view/analysis/analysis_layout.dart b/lib/src/view/analysis/analysis_layout.dart index 7b7cdf1413..ee0b052142 100644 --- a/lib/src/view/analysis/analysis_layout.dart +++ b/lib/src/view/analysis/analysis_layout.dart @@ -66,17 +66,29 @@ class _AppBarAnalysisTabIndicatorState @override void didChangeDependencies() { super.didChangeDependencies(); - widget.controller.addListener(_listener); + widget.controller.animation?.addListener(_handleTabAnimationTick); + widget.controller.addListener(_handleTabChange); } @override void dispose() { - widget.controller.removeListener(_listener); + widget.controller.animation?.removeListener(_handleTabAnimationTick); + widget.controller.removeListener(_handleTabChange); super.dispose(); } - void _listener() { - setState(() {}); + void _handleTabAnimationTick() { + if (widget.controller.indexIsChanging) { + setState(() { + // Rebuild the widget when the tab index is changing. + }); + } + } + + void _handleTabChange() { + setState(() { + // Rebuild the widget when the tab changes. + }); } @override diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index 201ac14a49..b389ea9a5b 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -216,7 +216,7 @@ class _Body extends ConsumerWidget { : null, bottomBar: _BottomBar(options: options), children: [ - OpeningExplorerView(options: options), + _OpeningExplorerTab(options: options), AnalysisTreeView(options), if (options.gameId != null) ServerAnalysisSummary(options), ], @@ -224,6 +224,24 @@ class _Body extends ConsumerWidget { } } +class _OpeningExplorerTab extends ConsumerWidget { + const _OpeningExplorerTab({required this.options}); + + final AnalysisOptions options; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final ctrlProvider = analysisControllerProvider(options); + final analysisState = ref.watch(ctrlProvider).requireValue; + + return OpeningExplorerView( + ply: analysisState.currentNode.position.ply, + fen: analysisState.currentNode.position.fen, + onMoveSelected: ref.read(ctrlProvider.notifier).onUserMove, + ); + } +} + class _BottomBar extends ConsumerWidget { const _BottomBar({required this.options}); diff --git a/lib/src/view/analysis/opening_explorer_view.dart b/lib/src/view/analysis/opening_explorer_view.dart index 9671292174..67cad3cd6d 100644 --- a/lib/src/view/analysis/opening_explorer_view.dart +++ b/lib/src/view/analysis/opening_explorer_view.dart @@ -1,6 +1,6 @@ +import 'package:dartchess/dartchess.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_preferences.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_repository.dart'; @@ -17,10 +17,17 @@ const _kTableRowPadding = EdgeInsets.symmetric( vertical: _kTableRowVerticalPadding, ); +/// An analysis tab view that displays the opening explorer for the current position. class OpeningExplorerView extends ConsumerStatefulWidget { - const OpeningExplorerView({required this.options}); + const OpeningExplorerView({ + required this.ply, + required this.fen, + this.onMoveSelected, + }); - final AnalysisOptions options; + final int ply; + final String fen; + final void Function(NormalMove)? onMoveSelected; @override ConsumerState createState() => _OpeningExplorerState(); @@ -46,16 +53,11 @@ class _OpeningExplorerState extends ConsumerState { ), ), online: () { - final analysisState = - ref.watch(analysisControllerProvider(widget.options)).requireValue; - - if (analysisState.position.ply >= 50) { - return _OpeningExplorerView( + if (widget.ply >= 50) { + return const _OpeningExplorerView( isLoading: false, children: [ - OpeningExplorerMoveTable.maxDepth( - options: widget.options, - ), + OpeningExplorerMoveTable.maxDepth(), ], ); } @@ -79,7 +81,7 @@ class _OpeningExplorerState extends ConsumerState { } final cacheKey = OpeningExplorerCacheKey( - fen: analysisState.position.fen, + fen: widget.fen, prefs: prefs, ); final cacheOpeningExplorer = cache[cacheKey]; @@ -88,12 +90,11 @@ class _OpeningExplorerState extends ConsumerState { (entry: cacheOpeningExplorer, isIndexing: false), ) : ref.watch( - openingExplorerProvider(fen: analysisState.position.fen), + openingExplorerProvider(fen: widget.fen), ); if (cacheOpeningExplorer == null) { - ref.listen(openingExplorerProvider(fen: analysisState.position.fen), - (_, curAsync) { + ref.listen(openingExplorerProvider(fen: widget.fen), (_, curAsync) { curAsync.whenData((cur) { if (cur != null && !cur.isIndexing) { cache[cacheKey] = cur.entry; @@ -112,12 +113,10 @@ class _OpeningExplorerState extends ConsumerState { if (openingExplorer == null) { return lastExplorerWidgets ?? [ - Shimmer( + const Shimmer( child: ShimmerLoading( isLoading: true, - child: OpeningExplorerMoveTable.loading( - options: widget.options, - ), + child: OpeningExplorerMoveTable.loading(), ), ), ]; @@ -126,7 +125,7 @@ class _OpeningExplorerState extends ConsumerState { final topGames = openingExplorer.entry.topGames; final recentGames = openingExplorer.entry.recentGames; - final ply = analysisState.position.ply; + final ply = widget.ply; final children = [ OpeningExplorerMoveTable( @@ -134,7 +133,7 @@ class _OpeningExplorerState extends ConsumerState { whiteWins: openingExplorer.entry.white, draws: openingExplorer.entry.draws, blackWins: openingExplorer.entry.black, - options: widget.options, + onMoveSelected: widget.onMoveSelected, ), if (topGames != null && topGames.isNotEmpty) ...[ OpeningExplorerHeaderTile( @@ -189,12 +188,10 @@ class _OpeningExplorerState extends ConsumerState { loading: () => lastExplorerWidgets ?? [ - Shimmer( + const Shimmer( child: ShimmerLoading( isLoading: true, - child: OpeningExplorerMoveTable.loading( - options: widget.options, - ), + child: OpeningExplorerMoveTable.loading(), ), ), ], diff --git a/lib/src/view/broadcast/broadcast_game_screen.dart b/lib/src/view/broadcast/broadcast_game_screen.dart index fafe8fdf13..a04458a8fd 100644 --- a/lib/src/view/broadcast/broadcast_game_screen.dart +++ b/lib/src/view/broadcast/broadcast_game_screen.dart @@ -21,6 +21,7 @@ import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/lichess_assets.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_layout.dart'; +import 'package:lichess_mobile/src/view/analysis/opening_explorer_view.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_game_bottom_bar.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_game_settings.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_game_tree_view.dart'; @@ -31,7 +32,7 @@ import 'package:lichess_mobile/src/widgets/clock.dart'; import 'package:lichess_mobile/src/widgets/pgn.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; -class BroadcastGameScreen extends ConsumerWidget { +class BroadcastGameScreen extends ConsumerStatefulWidget { final BroadcastRoundId roundId; final BroadcastGameId gameId; final String title; @@ -43,22 +44,58 @@ class BroadcastGameScreen extends ConsumerWidget { }); @override - Widget build(BuildContext context, WidgetRef ref) { - final broadcastGameState = - ref.watch(broadcastGameControllerProvider(roundId, gameId)); + ConsumerState createState() => + _BroadcastGameScreenState(); +} + +class _BroadcastGameScreenState extends ConsumerState + with SingleTickerProviderStateMixin { + late final List tabs; + late final TabController _tabController; + + @override + void initState() { + super.initState(); + + tabs = [ + AnalysisTab.opening, + AnalysisTab.moves, + ]; + + _tabController = TabController( + vsync: this, + initialIndex: 1, + length: tabs.length, + ); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final broadcastGameState = ref + .watch(broadcastGameControllerProvider(widget.roundId, widget.gameId)); return PlatformScaffold( appBar: PlatformAppBar( - title: Text(title, overflow: TextOverflow.ellipsis, maxLines: 1), + title: Text(widget.title, overflow: TextOverflow.ellipsis, maxLines: 1), actions: [ + AppBarAnalysisTabIndicator( + tabs: tabs, + controller: _tabController, + ), AppBarIconButton( onPressed: (broadcastGameState.hasValue) ? () { pushPlatformRoute( context, screen: BroadcastGameSettings( - roundId, - gameId, + widget.roundId, + widget.gameId, ), ); } @@ -69,7 +106,8 @@ class BroadcastGameScreen extends ConsumerWidget { ], ), body: switch (broadcastGameState) { - AsyncData() => _Body(roundId, gameId), + AsyncData() => + _Body(widget.roundId, widget.gameId, tabController: _tabController), AsyncError(:final error) => Center( child: Text('Cannot load broadcast game: $error'), ), @@ -80,10 +118,11 @@ class BroadcastGameScreen extends ConsumerWidget { } class _Body extends ConsumerWidget { + const _Body(this.roundId, this.gameId, {required this.tabController}); + final BroadcastRoundId roundId; final BroadcastGameId gameId; - - const _Body(this.roundId, this.gameId); + final TabController tabController; @override Widget build(BuildContext context, WidgetRef ref) { @@ -98,49 +137,72 @@ class _Body extends ConsumerWidget { final isLocalEvaluationEnabled = broadcastState.isLocalEvaluationEnabled; final currentNode = broadcastState.currentNode; - return DefaultTabController( - length: 1, - child: AnalysisLayout( - boardBuilder: (context, boardSize, borderRadius) => - _BroadcastBoardWithHeaders( - roundId, - gameId, - boardSize, - borderRadius, - ), - engineGaugeBuilder: isLocalEvaluationEnabled && showEvaluationGauge - ? (context, orientation) { - return orientation == Orientation.portrait - ? EngineGauge( - displayMode: EngineGaugeDisplayMode.horizontal, - params: engineGaugeParams, - ) - : Container( - clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4.0), - ), - child: EngineGauge( - displayMode: EngineGaugeDisplayMode.vertical, - params: engineGaugeParams, - ), - ); - } - : null, - engineLines: isLocalEvaluationEnabled && numEvalLines > 0 - ? EngineLines( - clientEval: currentNode.eval, - isGameOver: currentNode.position.isGameOver, - onTapMove: ref - .read( - broadcastGameControllerProvider(roundId, gameId).notifier, - ) - .onUserMove, - ) - : null, - bottomBar: BroadcastGameBottomBar(roundId: roundId, gameId: gameId), - children: [BroadcastGameTreeView(roundId, gameId)], + return AnalysisLayout( + tabController: tabController, + boardBuilder: (context, boardSize, borderRadius) => + _BroadcastBoardWithHeaders( + roundId, + gameId, + boardSize, + borderRadius, ), + engineGaugeBuilder: isLocalEvaluationEnabled && showEvaluationGauge + ? (context, orientation) { + return orientation == Orientation.portrait + ? EngineGauge( + displayMode: EngineGaugeDisplayMode.horizontal, + params: engineGaugeParams, + ) + : Container( + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4.0), + ), + child: EngineGauge( + displayMode: EngineGaugeDisplayMode.vertical, + params: engineGaugeParams, + ), + ); + } + : null, + engineLines: isLocalEvaluationEnabled && numEvalLines > 0 + ? EngineLines( + clientEval: currentNode.eval, + isGameOver: currentNode.position.isGameOver, + onTapMove: ref + .read( + broadcastGameControllerProvider(roundId, gameId).notifier, + ) + .onUserMove, + ) + : null, + bottomBar: BroadcastGameBottomBar(roundId: roundId, gameId: gameId), + children: [ + _OpeningExplorerTab(roundId, gameId), + BroadcastGameTreeView(roundId, gameId), + ], + ); + } +} + +class _OpeningExplorerTab extends ConsumerWidget { + const _OpeningExplorerTab( + this.roundId, + this.gameId, + ); + + final BroadcastRoundId roundId; + final BroadcastGameId gameId; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final ctrlProvider = broadcastGameControllerProvider(roundId, gameId); + final state = ref.watch(ctrlProvider).requireValue; + + return OpeningExplorerView( + ply: state.currentNode.position.ply, + fen: state.currentNode.position.fen, + onMoveSelected: ref.read(ctrlProvider.notifier).onUserMove, ); } } diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index 6d3cb21479..1feb6611ed 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -108,9 +108,7 @@ class _OpeningExplorerState extends ConsumerState { isIndexing: false, children: [ openingHeader, - OpeningExplorerMoveTable.maxDepth( - options: widget.options, - ), + const OpeningExplorerMoveTable.maxDepth(), ], ); } @@ -172,12 +170,10 @@ class _OpeningExplorerState extends ConsumerState { if (openingExplorer == null) { return lastExplorerWidgets ?? [ - Shimmer( + const Shimmer( child: ShimmerLoading( isLoading: true, - child: OpeningExplorerMoveTable.loading( - options: widget.options, - ), + child: OpeningExplorerMoveTable.loading(), ), ), ]; @@ -195,7 +191,13 @@ class _OpeningExplorerState extends ConsumerState { whiteWins: openingExplorer.entry.white, draws: openingExplorer.entry.draws, blackWins: openingExplorer.entry.black, - options: widget.options, + onMoveSelected: (move) { + ref + .read( + analysisControllerProvider(widget.options).notifier, + ) + .onUserMove(move); + }, ), if (topGames != null && topGames.isNotEmpty) ...[ OpeningExplorerHeaderTile( @@ -250,12 +252,10 @@ class _OpeningExplorerState extends ConsumerState { loading: () => lastExplorerWidgets ?? [ - Shimmer( + const Shimmer( child: ShimmerLoading( isLoading: true, - child: OpeningExplorerMoveTable.loading( - options: widget.options, - ), + child: OpeningExplorerMoveTable.loading(), ), ), ], diff --git a/lib/src/view/opening_explorer/opening_explorer_view_builder.dart b/lib/src/view/opening_explorer/opening_explorer_view_builder.dart new file mode 100644 index 0000000000..27133cf450 --- /dev/null +++ b/lib/src/view/opening_explorer/opening_explorer_view_builder.dart @@ -0,0 +1,234 @@ +import 'package:dartchess/dartchess.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; +import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_preferences.dart'; +import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_repository.dart'; +import 'package:lichess_mobile/src/network/connectivity.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_widgets.dart'; +import 'package:lichess_mobile/src/widgets/feedback.dart'; +import 'package:lichess_mobile/src/widgets/shimmer.dart'; + +const _kTableRowVerticalPadding = 12.0; +const _kTableRowHorizontalPadding = 8.0; +const _kTableRowPadding = EdgeInsets.symmetric( + horizontal: _kTableRowHorizontalPadding, + vertical: _kTableRowVerticalPadding, +); + +/// A view that displays the opening explorer for the given position. +/// +/// All the required data is fetched from the [OpeningExplorerRepository] and +/// displayed in a table view. Responses are cached to avoid unnecessary +/// network requests. +class OpeningExplorerViewBuilder extends ConsumerStatefulWidget { + const OpeningExplorerViewBuilder({ + required this.ply, + required this.fen, + required this.builder, + this.onMoveSelected, + }); + + final int ply; + final String fen; + final void Function(NormalMove)? onMoveSelected; + final Widget Function( + BuildContext context, + List children, { + required bool isLoading, + required bool isIndexing, + }) builder; + + @override + ConsumerState createState() => + _OpeningExplorerState(); +} + +class _OpeningExplorerState extends ConsumerState { + final Map cache = {}; + + /// Last explorer content that was successfully loaded. This is used to + /// display a loading indicator while the new content is being fetched. + List? lastExplorerWidgets; + + @override + Widget build(BuildContext context) { + final connectivity = ref.watch(connectivityChangesProvider); + return connectivity.whenIsLoading( + loading: () => const CenterLoadingIndicator(), + offline: () => const Center( + child: Padding( + padding: EdgeInsets.all(16.0), + // TODO l10n + child: Text('Opening explorer is not available offline.'), + ), + ), + online: () { + if (widget.ply >= 50) { + return widget.builder( + context, + [ + const OpeningExplorerMoveTable.maxDepth(), + ], + isLoading: false, + isIndexing: false, + ); + } + + final prefs = ref.watch(openingExplorerPreferencesProvider); + + if (prefs.db == OpeningDatabase.player && + prefs.playerDb.username == null) { + return widget.builder( + context, + [ + const Padding( + padding: _kTableRowPadding, + child: Center( + // TODO: l10n + child: Text('Select a Lichess player in the settings.'), + ), + ), + ], + isLoading: false, + isIndexing: false, + ); + } + + final cacheKey = OpeningExplorerCacheKey( + fen: widget.fen, + prefs: prefs, + ); + final cacheOpeningExplorer = cache[cacheKey]; + final openingExplorerAsync = cacheOpeningExplorer != null + ? AsyncValue.data( + (entry: cacheOpeningExplorer, isIndexing: false), + ) + : ref.watch( + openingExplorerProvider(fen: widget.fen), + ); + + if (cacheOpeningExplorer == null) { + ref.listen(openingExplorerProvider(fen: widget.fen), (_, curAsync) { + curAsync.whenData((cur) { + if (cur != null && !cur.isIndexing) { + cache[cacheKey] = cur.entry; + } + }); + }); + } + + final isLoading = openingExplorerAsync.isLoading || + openingExplorerAsync.value == null; + + return widget.builder( + context, + openingExplorerAsync.when( + data: (openingExplorer) { + if (openingExplorer == null) { + return lastExplorerWidgets ?? + [ + const Shimmer( + child: ShimmerLoading( + isLoading: true, + child: OpeningExplorerMoveTable.loading(), + ), + ), + ]; + } + + final topGames = openingExplorer.entry.topGames; + final recentGames = openingExplorer.entry.recentGames; + + final ply = widget.ply; + + final children = [ + OpeningExplorerMoveTable( + moves: openingExplorer.entry.moves, + whiteWins: openingExplorer.entry.white, + draws: openingExplorer.entry.draws, + blackWins: openingExplorer.entry.black, + onMoveSelected: widget.onMoveSelected, + ), + if (topGames != null && topGames.isNotEmpty) ...[ + OpeningExplorerHeaderTile( + key: const Key('topGamesHeader'), + child: Text(context.l10n.topGames), + ), + ...List.generate( + topGames.length, + (int index) { + return OpeningExplorerGameTile( + key: Key('top-game-${topGames.get(index).id}'), + game: topGames.get(index), + color: index.isEven + ? Theme.of(context).colorScheme.surfaceContainerLow + : Theme.of(context) + .colorScheme + .surfaceContainerHigh, + ply: ply, + ); + }, + growable: false, + ), + ], + if (recentGames != null && recentGames.isNotEmpty) ...[ + OpeningExplorerHeaderTile( + key: const Key('recentGamesHeader'), + child: Text(context.l10n.recentGames), + ), + ...List.generate( + recentGames.length, + (int index) { + return OpeningExplorerGameTile( + key: Key('recent-game-${recentGames.get(index).id}'), + game: recentGames.get(index), + color: index.isEven + ? Theme.of(context).colorScheme.surfaceContainerLow + : Theme.of(context) + .colorScheme + .surfaceContainerHigh, + ply: ply, + ); + }, + growable: false, + ), + ], + ]; + + lastExplorerWidgets = children; + + return children; + }, + loading: () => + lastExplorerWidgets ?? + [ + const Shimmer( + child: ShimmerLoading( + isLoading: true, + child: OpeningExplorerMoveTable.loading(), + ), + ), + ], + error: (e, s) { + debugPrint( + 'SEVERE: [OpeningExplorerView] could not load opening explorer data; $e\n$s', + ); + return [ + Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Text(e.toString()), + ), + ), + ]; + }, + ), + isLoading: isLoading, + isIndexing: openingExplorerAsync.value?.isIndexing ?? false, + ); + }, + ); + } +} diff --git a/lib/src/view/opening_explorer/opening_explorer_widgets.dart b/lib/src/view/opening_explorer/opening_explorer_widgets.dart index 43bf9f9deb..55a93a665d 100644 --- a/lib/src/view/opening_explorer/opening_explorer_widgets.dart +++ b/lib/src/view/opening_explorer/opening_explorer_widgets.dart @@ -3,7 +3,6 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; -import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; @@ -34,33 +33,33 @@ class OpeningExplorerMoveTable extends ConsumerWidget { required this.whiteWins, required this.draws, required this.blackWins, - required this.options, + this.onMoveSelected, }) : _isLoading = false, _maxDepthReached = false; - const OpeningExplorerMoveTable.loading({ - required this.options, - }) : _isLoading = true, + const OpeningExplorerMoveTable.loading() + : _isLoading = true, moves = const IListConst([]), whiteWins = 0, draws = 0, blackWins = 0, - _maxDepthReached = false; + _maxDepthReached = false, + onMoveSelected = null; - const OpeningExplorerMoveTable.maxDepth({ - required this.options, - }) : _isLoading = false, + const OpeningExplorerMoveTable.maxDepth() + : _isLoading = false, moves = const IListConst([]), whiteWins = 0, draws = 0, blackWins = 0, - _maxDepthReached = true; + _maxDepthReached = true, + onMoveSelected = null; final IList moves; final int whiteWins; final int draws; final int blackWins; - final AnalysisOptions options; + final void Function(NormalMove)? onMoveSelected; final bool _isLoading; final bool _maxDepthReached; @@ -80,8 +79,6 @@ class OpeningExplorerMoveTable extends ConsumerWidget { } final games = whiteWins + draws + blackWins; - final ctrlProvider = analysisControllerProvider(options); - const headerTextStyle = TextStyle(fontSize: 12); return Table( @@ -119,27 +116,24 @@ class OpeningExplorerMoveTable extends ConsumerWidget { ), children: [ TableRowInkWell( - onTap: () => ref - .read(ctrlProvider.notifier) - .onUserMove(NormalMove.fromUci(move.uci)), + onTap: () => + onMoveSelected?.call(NormalMove.fromUci(move.uci)), child: Padding( padding: _kTableRowPadding, child: Text(move.san), ), ), TableRowInkWell( - onTap: () => ref - .read(ctrlProvider.notifier) - .onUserMove(NormalMove.fromUci(move.uci)), + onTap: () => + onMoveSelected?.call(NormalMove.fromUci(move.uci)), child: Padding( padding: _kTableRowPadding, child: Text('${formatNum(move.games)} ($percentGames%)'), ), ), TableRowInkWell( - onTap: () => ref - .read(ctrlProvider.notifier) - .onUserMove(NormalMove.fromUci(move.uci)), + onTap: () => + onMoveSelected?.call(NormalMove.fromUci(move.uci)), child: Padding( padding: _kTableRowPadding, child: _WinPercentageChart( From d6f7899a9aedb01ef8433a0a57af249945d500c0 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 2 Dec 2024 13:44:13 +0100 Subject: [PATCH 815/979] More wip on broadcast and study explorer --- lib/src/model/study/study_controller.dart | 2 + lib/src/view/analysis/analysis_screen.dart | 28 +- .../view/analysis/opening_explorer_view.dart | 251 ---------------- .../view/broadcast/broadcast_game_screen.dart | 28 +- .../opening_explorer_screen.dart | 269 ++---------------- .../opening_explorer_view_builder.dart | 6 +- 6 files changed, 79 insertions(+), 505 deletions(-) delete mode 100644 lib/src/view/analysis/opening_explorer_view.dart diff --git a/lib/src/model/study/study_controller.dart b/lib/src/model/study/study_controller.dart index 16a9c178bb..a525107d37 100644 --- a/lib/src/model/study/study_controller.dart +++ b/lib/src/model/study/study_controller.dart @@ -654,6 +654,8 @@ class StudyState with _$StudyState { engineSupportedVariants.contains(variant) && isLocalEvaluationEnabled; + bool get isOpeningExplorerAvailable => study.chapter.features.explorer; + EngineGaugeParams? get engineGaugeParams => isEngineAvailable ? ( orientation: pov, diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index b389ea9a5b..52a18cbf9f 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -10,12 +10,12 @@ import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_layout.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_share_screen.dart'; -import 'package:lichess_mobile/src/view/analysis/opening_explorer_view.dart'; import 'package:lichess_mobile/src/view/analysis/server_analysis.dart'; import 'package:lichess_mobile/src/view/board_editor/board_editor_screen.dart'; import 'package:lichess_mobile/src/view/engine/engine_depth.dart'; import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; import 'package:lichess_mobile/src/view/engine/engine_lines.dart'; +import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_view_builder.dart'; import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; @@ -234,10 +234,34 @@ class _OpeningExplorerTab extends ConsumerWidget { final ctrlProvider = analysisControllerProvider(options); final analysisState = ref.watch(ctrlProvider).requireValue; - return OpeningExplorerView( + return OpeningExplorerViewBuilder( ply: analysisState.currentNode.position.ply, fen: analysisState.currentNode.position.fen, onMoveSelected: ref.read(ctrlProvider.notifier).onUserMove, + builder: (context, children, {required isLoading, required isIndexing}) { + final brightness = Theme.of(context).brightness; + final loadingOverlay = Positioned.fill( + child: IgnorePointer( + ignoring: !isLoading, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 400), + curve: Curves.fastOutSlowIn, + opacity: isLoading ? 0.20 : 0.0, + child: ColoredBox( + color: + brightness == Brightness.dark ? Colors.black : Colors.white, + ), + ), + ), + ); + + return Stack( + children: [ + ListView(children: children), + loadingOverlay, + ], + ); + }, ); } } diff --git a/lib/src/view/analysis/opening_explorer_view.dart b/lib/src/view/analysis/opening_explorer_view.dart deleted file mode 100644 index 67cad3cd6d..0000000000 --- a/lib/src/view/analysis/opening_explorer_view.dart +++ /dev/null @@ -1,251 +0,0 @@ -import 'package:dartchess/dartchess.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; -import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_preferences.dart'; -import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_repository.dart'; -import 'package:lichess_mobile/src/network/connectivity.dart'; -import 'package:lichess_mobile/src/utils/l10n_context.dart'; -import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_widgets.dart'; -import 'package:lichess_mobile/src/widgets/feedback.dart'; -import 'package:lichess_mobile/src/widgets/shimmer.dart'; - -const _kTableRowVerticalPadding = 12.0; -const _kTableRowHorizontalPadding = 8.0; -const _kTableRowPadding = EdgeInsets.symmetric( - horizontal: _kTableRowHorizontalPadding, - vertical: _kTableRowVerticalPadding, -); - -/// An analysis tab view that displays the opening explorer for the current position. -class OpeningExplorerView extends ConsumerStatefulWidget { - const OpeningExplorerView({ - required this.ply, - required this.fen, - this.onMoveSelected, - }); - - final int ply; - final String fen; - final void Function(NormalMove)? onMoveSelected; - - @override - ConsumerState createState() => _OpeningExplorerState(); -} - -class _OpeningExplorerState extends ConsumerState { - final Map cache = {}; - - /// Last explorer content that was successfully loaded. This is used to - /// display a loading indicator while the new content is being fetched. - List? lastExplorerWidgets; - - @override - Widget build(BuildContext context) { - final connectivity = ref.watch(connectivityChangesProvider); - return connectivity.whenIsLoading( - loading: () => const CenterLoadingIndicator(), - offline: () => const Center( - child: Padding( - padding: EdgeInsets.all(16.0), - // TODO l10n - child: Text('Opening explorer is not available offline.'), - ), - ), - online: () { - if (widget.ply >= 50) { - return const _OpeningExplorerView( - isLoading: false, - children: [ - OpeningExplorerMoveTable.maxDepth(), - ], - ); - } - - final prefs = ref.watch(openingExplorerPreferencesProvider); - - if (prefs.db == OpeningDatabase.player && - prefs.playerDb.username == null) { - return const _OpeningExplorerView( - isLoading: false, - children: [ - Padding( - padding: _kTableRowPadding, - child: Center( - // TODO: l10n - child: Text('Select a Lichess player in the settings.'), - ), - ), - ], - ); - } - - final cacheKey = OpeningExplorerCacheKey( - fen: widget.fen, - prefs: prefs, - ); - final cacheOpeningExplorer = cache[cacheKey]; - final openingExplorerAsync = cacheOpeningExplorer != null - ? AsyncValue.data( - (entry: cacheOpeningExplorer, isIndexing: false), - ) - : ref.watch( - openingExplorerProvider(fen: widget.fen), - ); - - if (cacheOpeningExplorer == null) { - ref.listen(openingExplorerProvider(fen: widget.fen), (_, curAsync) { - curAsync.whenData((cur) { - if (cur != null && !cur.isIndexing) { - cache[cacheKey] = cur.entry; - } - }); - }); - } - - final isLoading = openingExplorerAsync.isLoading || - openingExplorerAsync.value == null; - - return _OpeningExplorerView( - isLoading: isLoading, - children: openingExplorerAsync.when( - data: (openingExplorer) { - if (openingExplorer == null) { - return lastExplorerWidgets ?? - [ - const Shimmer( - child: ShimmerLoading( - isLoading: true, - child: OpeningExplorerMoveTable.loading(), - ), - ), - ]; - } - - final topGames = openingExplorer.entry.topGames; - final recentGames = openingExplorer.entry.recentGames; - - final ply = widget.ply; - - final children = [ - OpeningExplorerMoveTable( - moves: openingExplorer.entry.moves, - whiteWins: openingExplorer.entry.white, - draws: openingExplorer.entry.draws, - blackWins: openingExplorer.entry.black, - onMoveSelected: widget.onMoveSelected, - ), - if (topGames != null && topGames.isNotEmpty) ...[ - OpeningExplorerHeaderTile( - key: const Key('topGamesHeader'), - child: Text(context.l10n.topGames), - ), - ...List.generate( - topGames.length, - (int index) { - return OpeningExplorerGameTile( - key: Key('top-game-${topGames.get(index).id}'), - game: topGames.get(index), - color: index.isEven - ? Theme.of(context).colorScheme.surfaceContainerLow - : Theme.of(context) - .colorScheme - .surfaceContainerHigh, - ply: ply, - ); - }, - growable: false, - ), - ], - if (recentGames != null && recentGames.isNotEmpty) ...[ - OpeningExplorerHeaderTile( - key: const Key('recentGamesHeader'), - child: Text(context.l10n.recentGames), - ), - ...List.generate( - recentGames.length, - (int index) { - return OpeningExplorerGameTile( - key: Key('recent-game-${recentGames.get(index).id}'), - game: recentGames.get(index), - color: index.isEven - ? Theme.of(context).colorScheme.surfaceContainerLow - : Theme.of(context) - .colorScheme - .surfaceContainerHigh, - ply: ply, - ); - }, - growable: false, - ), - ], - ]; - - lastExplorerWidgets = children; - - return children; - }, - loading: () => - lastExplorerWidgets ?? - [ - const Shimmer( - child: ShimmerLoading( - isLoading: true, - child: OpeningExplorerMoveTable.loading(), - ), - ), - ], - error: (e, s) { - debugPrint( - 'SEVERE: [OpeningExplorerView] could not load opening explorer data; $e\n$s', - ); - return [ - Center( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Text(e.toString()), - ), - ), - ]; - }, - ), - ); - }, - ); - } -} - -class _OpeningExplorerView extends StatelessWidget { - const _OpeningExplorerView({ - required this.children, - required this.isLoading, - }); - - final List children; - final bool isLoading; - - @override - Widget build(BuildContext context) { - final brightness = Theme.of(context).brightness; - final loadingOverlay = Positioned.fill( - child: IgnorePointer( - ignoring: !isLoading, - child: AnimatedOpacity( - duration: const Duration(milliseconds: 400), - curve: Curves.fastOutSlowIn, - opacity: isLoading ? 0.20 : 0.0, - child: ColoredBox( - color: brightness == Brightness.dark ? Colors.black : Colors.white, - ), - ), - ), - ); - - return Stack( - children: [ - ListView(children: children), - loadingOverlay, - ], - ); - } -} diff --git a/lib/src/view/broadcast/broadcast_game_screen.dart b/lib/src/view/broadcast/broadcast_game_screen.dart index a04458a8fd..b1149b07ca 100644 --- a/lib/src/view/broadcast/broadcast_game_screen.dart +++ b/lib/src/view/broadcast/broadcast_game_screen.dart @@ -21,12 +21,12 @@ import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/lichess_assets.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_layout.dart'; -import 'package:lichess_mobile/src/view/analysis/opening_explorer_view.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_game_bottom_bar.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_game_settings.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_game_tree_view.dart'; import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; import 'package:lichess_mobile/src/view/engine/engine_lines.dart'; +import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_view_builder.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/clock.dart'; import 'package:lichess_mobile/src/widgets/pgn.dart'; @@ -199,10 +199,34 @@ class _OpeningExplorerTab extends ConsumerWidget { final ctrlProvider = broadcastGameControllerProvider(roundId, gameId); final state = ref.watch(ctrlProvider).requireValue; - return OpeningExplorerView( + return OpeningExplorerViewBuilder( ply: state.currentNode.position.ply, fen: state.currentNode.position.fen, onMoveSelected: ref.read(ctrlProvider.notifier).onUserMove, + builder: (context, children, {required isLoading, required isIndexing}) { + final brightness = Theme.of(context).brightness; + final loadingOverlay = Positioned.fill( + child: IgnorePointer( + ignoring: !isLoading, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 400), + curve: Curves.fastOutSlowIn, + opacity: isLoading ? 0.20 : 0.0, + child: ColoredBox( + color: + brightness == Brightness.dark ? Colors.black : Colors.white, + ), + ), + ), + ); + + return Stack( + children: [ + ListView(children: children), + loadingOverlay, + ], + ); + }, ); } } diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index 1feb6611ed..3c7f6a3789 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -4,15 +4,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; -import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_preferences.dart'; -import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_repository.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_board.dart'; -import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_widgets.dart'; +import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_view_builder.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; @@ -20,259 +18,38 @@ import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/move_list.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; -import 'package:lichess_mobile/src/widgets/shimmer.dart'; -import 'package:url_launcher/url_launcher.dart'; import 'opening_explorer_settings.dart'; -const _kTableRowVerticalPadding = 12.0; -const _kTableRowHorizontalPadding = 8.0; -const _kTableRowPadding = EdgeInsets.symmetric( - horizontal: _kTableRowHorizontalPadding, - vertical: _kTableRowVerticalPadding, -); const _kTabletBoardRadius = BorderRadius.all(Radius.circular(4.0)); -class OpeningExplorerScreen extends ConsumerStatefulWidget { +class OpeningExplorerScreen extends ConsumerWidget { const OpeningExplorerScreen({required this.options}); final AnalysisOptions options; @override - ConsumerState createState() => _OpeningExplorerState(); -} - -class _OpeningExplorerState extends ConsumerState { - final Map cache = {}; - - /// Last explorer content that was successfully loaded. This is used to - /// display a loading indicator while the new content is being fetched. - List? lastExplorerWidgets; - - @override - Widget build(BuildContext context) { - switch (ref.watch(analysisControllerProvider(widget.options))) { - case AsyncData(value: final analysisState): - final opening = analysisState.currentNode.isRoot - ? LightOpening( - eco: '', - name: context.l10n.startPosition, - ) - : analysisState.currentNode.opening ?? - analysisState.currentBranchOpening ?? - analysisState.contextOpening; - - final Widget openingHeader = Container( - padding: _kTableRowPadding, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.secondaryContainer, - ), - child: opening != null - ? GestureDetector( - onTap: opening.name == context.l10n.startPosition - ? null - : () => launchUrl( - Uri.parse( - 'https://lichess.org/opening/${opening.name}', - ), - ), - child: Row( - children: [ - Icon( - Icons.open_in_browser_outlined, - color: - Theme.of(context).colorScheme.onSecondaryContainer, - ), - const SizedBox(width: 6.0), - Expanded( - child: Text( - opening.name, - style: TextStyle( - color: Theme.of(context) - .colorScheme - .onSecondaryContainer, - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ), - ) - : const SizedBox.shrink(), - ); - - if (analysisState.position.ply >= 50) { - return _OpeningExplorerView( - options: widget.options, - isLoading: false, - isIndexing: false, - children: [ - openingHeader, - const OpeningExplorerMoveTable.maxDepth(), - ], - ); - } - - final prefs = ref.watch(openingExplorerPreferencesProvider); - - if (prefs.db == OpeningDatabase.player && - prefs.playerDb.username == null) { - return _OpeningExplorerView( - options: widget.options, - isLoading: false, - isIndexing: false, - children: [ - openingHeader, - const Padding( - padding: _kTableRowPadding, - child: Center( - // TODO: l10n - child: Text('Select a Lichess player in the settings.'), - ), - ), - ], - ); - } - - final cacheKey = OpeningExplorerCacheKey( - fen: analysisState.position.fen, - prefs: prefs, - ); - final cacheOpeningExplorer = cache[cacheKey]; - final openingExplorerAsync = cacheOpeningExplorer != null - ? AsyncValue.data( - (entry: cacheOpeningExplorer, isIndexing: false), - ) - : ref.watch( - openingExplorerProvider(fen: analysisState.position.fen), - ); - - if (cacheOpeningExplorer == null) { - ref.listen(openingExplorerProvider(fen: analysisState.position.fen), - (_, curAsync) { - curAsync.whenData((cur) { - if (cur != null && !cur.isIndexing) { - cache[cacheKey] = cur.entry; - } - }); - }); - } - - final isLoading = openingExplorerAsync.isLoading || - openingExplorerAsync.value == null; - - return _OpeningExplorerView( - options: widget.options, - isLoading: isLoading, - isIndexing: openingExplorerAsync.value?.isIndexing ?? false, - children: openingExplorerAsync.when( - data: (openingExplorer) { - if (openingExplorer == null) { - return lastExplorerWidgets ?? - [ - const Shimmer( - child: ShimmerLoading( - isLoading: true, - child: OpeningExplorerMoveTable.loading(), - ), - ), - ]; - } - - final topGames = openingExplorer.entry.topGames; - final recentGames = openingExplorer.entry.recentGames; - - final ply = analysisState.position.ply; - - final children = [ - openingHeader, - OpeningExplorerMoveTable( - moves: openingExplorer.entry.moves, - whiteWins: openingExplorer.entry.white, - draws: openingExplorer.entry.draws, - blackWins: openingExplorer.entry.black, - onMoveSelected: (move) { - ref - .read( - analysisControllerProvider(widget.options).notifier, - ) - .onUserMove(move); - }, - ), - if (topGames != null && topGames.isNotEmpty) ...[ - OpeningExplorerHeaderTile( - key: const Key('topGamesHeader'), - child: Text(context.l10n.topGames), - ), - ...List.generate( - topGames.length, - (int index) { - return OpeningExplorerGameTile( - key: Key('top-game-${topGames.get(index).id}'), - game: topGames.get(index), - color: index.isEven - ? Theme.of(context).colorScheme.surfaceContainerLow - : Theme.of(context) - .colorScheme - .surfaceContainerHigh, - ply: ply, - ); - }, - growable: false, - ), - ], - if (recentGames != null && recentGames.isNotEmpty) ...[ - OpeningExplorerHeaderTile( - key: const Key('recentGamesHeader'), - child: Text(context.l10n.recentGames), - ), - ...List.generate( - recentGames.length, - (int index) { - return OpeningExplorerGameTile( - key: Key('recent-game-${recentGames.get(index).id}'), - game: recentGames.get(index), - color: index.isEven - ? Theme.of(context).colorScheme.surfaceContainerLow - : Theme.of(context) - .colorScheme - .surfaceContainerHigh, - ply: ply, - ); - }, - growable: false, - ), - ], - ]; - - lastExplorerWidgets = children; - - return children; - }, - loading: () => - lastExplorerWidgets ?? - [ - const Shimmer( - child: ShimmerLoading( - isLoading: true, - child: OpeningExplorerMoveTable.loading(), - ), - ), - ], - error: (e, s) { - debugPrint( - 'SEVERE: [OpeningExplorerScreen] could not load opening explorer data; $e\n$s', - ); - return [ - Center( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Text(e.toString()), - ), - ), - ]; - }, - ), + Widget build(BuildContext context, WidgetRef ref) { + final ctrlProvider = analysisControllerProvider(options); + switch (ref.watch(ctrlProvider)) { + case AsyncData(value: final state): + return OpeningExplorerViewBuilder( + ply: state.currentNode.position.ply, + fen: state.currentNode.position.fen, + onMoveSelected: ref.read(ctrlProvider.notifier).onUserMove, + builder: ( + context, + children, { + required isLoading, + required isIndexing, + }) { + return _OpeningExplorerView( + options: options, + isLoading: isLoading, + isIndexing: isIndexing, + children: children, + ); + }, ); case AsyncError(:final error): debugPrint( diff --git a/lib/src/view/opening_explorer/opening_explorer_view_builder.dart b/lib/src/view/opening_explorer/opening_explorer_view_builder.dart index 27133cf450..a71a17e8da 100644 --- a/lib/src/view/opening_explorer/opening_explorer_view_builder.dart +++ b/lib/src/view/opening_explorer/opening_explorer_view_builder.dart @@ -17,11 +17,9 @@ const _kTableRowPadding = EdgeInsets.symmetric( vertical: _kTableRowVerticalPadding, ); -/// A view that displays the opening explorer for the given position. +/// A widget that displays the opening explorer moves and games for the given position. /// -/// All the required data is fetched from the [OpeningExplorerRepository] and -/// displayed in a table view. Responses are cached to avoid unnecessary -/// network requests. +/// Network requests are debounced and cached to avoid unnecessary requests. class OpeningExplorerViewBuilder extends ConsumerStatefulWidget { const OpeningExplorerViewBuilder({ required this.ply, From 26a5ead642d5c6d06769175799a29e542c0a6f92 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 2 Dec 2024 15:52:43 +0100 Subject: [PATCH 816/979] Add explorer tab to study screen --- lib/src/model/study/study_controller.dart | 3 +- lib/src/view/analysis/analysis_screen.dart | 49 +--- .../view/broadcast/broadcast_game_screen.dart | 29 +-- .../opening_explorer_screen.dart | 3 +- .../opening_explorer_view_builder.dart | 75 +++++- lib/src/view/study/study_screen.dart | 220 +++++++++++++----- 6 files changed, 232 insertions(+), 147 deletions(-) diff --git a/lib/src/model/study/study_controller.dart b/lib/src/model/study/study_controller.dart index a525107d37..bb67ae2c1c 100644 --- a/lib/src/model/study/study_controller.dart +++ b/lib/src/model/study/study_controller.dart @@ -654,7 +654,8 @@ class StudyState with _$StudyState { engineSupportedVariants.contains(variant) && isLocalEvaluationEnabled; - bool get isOpeningExplorerAvailable => study.chapter.features.explorer; + bool get isOpeningExplorerAvailable => + !gamebookActive && study.chapter.features.explorer; EngineGaugeParams? get engineGaugeParams => isEngineAvailable ? ( diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index 52a18cbf9f..f1ac2e5f99 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -216,7 +216,12 @@ class _Body extends ConsumerWidget { : null, bottomBar: _BottomBar(options: options), children: [ - _OpeningExplorerTab(options: options), + OpeningExplorer( + position: currentNode.position, + onMoveSelected: (move) { + ref.read(ctrlProvider.notifier).onUserMove(move); + }, + ), AnalysisTreeView(options), if (options.gameId != null) ServerAnalysisSummary(options), ], @@ -224,48 +229,6 @@ class _Body extends ConsumerWidget { } } -class _OpeningExplorerTab extends ConsumerWidget { - const _OpeningExplorerTab({required this.options}); - - final AnalysisOptions options; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final ctrlProvider = analysisControllerProvider(options); - final analysisState = ref.watch(ctrlProvider).requireValue; - - return OpeningExplorerViewBuilder( - ply: analysisState.currentNode.position.ply, - fen: analysisState.currentNode.position.fen, - onMoveSelected: ref.read(ctrlProvider.notifier).onUserMove, - builder: (context, children, {required isLoading, required isIndexing}) { - final brightness = Theme.of(context).brightness; - final loadingOverlay = Positioned.fill( - child: IgnorePointer( - ignoring: !isLoading, - child: AnimatedOpacity( - duration: const Duration(milliseconds: 400), - curve: Curves.fastOutSlowIn, - opacity: isLoading ? 0.20 : 0.0, - child: ColoredBox( - color: - brightness == Brightness.dark ? Colors.black : Colors.white, - ), - ), - ), - ); - - return Stack( - children: [ - ListView(children: children), - loadingOverlay, - ], - ); - }, - ); - } -} - class _BottomBar extends ConsumerWidget { const _BottomBar({required this.options}); diff --git a/lib/src/view/broadcast/broadcast_game_screen.dart b/lib/src/view/broadcast/broadcast_game_screen.dart index b1149b07ca..1054f590e7 100644 --- a/lib/src/view/broadcast/broadcast_game_screen.dart +++ b/lib/src/view/broadcast/broadcast_game_screen.dart @@ -199,34 +199,9 @@ class _OpeningExplorerTab extends ConsumerWidget { final ctrlProvider = broadcastGameControllerProvider(roundId, gameId); final state = ref.watch(ctrlProvider).requireValue; - return OpeningExplorerViewBuilder( - ply: state.currentNode.position.ply, - fen: state.currentNode.position.fen, + return OpeningExplorer( + position: state.currentNode.position, onMoveSelected: ref.read(ctrlProvider.notifier).onUserMove, - builder: (context, children, {required isLoading, required isIndexing}) { - final brightness = Theme.of(context).brightness; - final loadingOverlay = Positioned.fill( - child: IgnorePointer( - ignoring: !isLoading, - child: AnimatedOpacity( - duration: const Duration(milliseconds: 400), - curve: Curves.fastOutSlowIn, - opacity: isLoading ? 0.20 : 0.0, - child: ColoredBox( - color: - brightness == Brightness.dark ? Colors.black : Colors.white, - ), - ), - ), - ); - - return Stack( - children: [ - ListView(children: children), - loadingOverlay, - ], - ); - }, ); } } diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index 3c7f6a3789..9b552271cd 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -34,8 +34,7 @@ class OpeningExplorerScreen extends ConsumerWidget { switch (ref.watch(ctrlProvider)) { case AsyncData(value: final state): return OpeningExplorerViewBuilder( - ply: state.currentNode.position.ply, - fen: state.currentNode.position.fen, + position: state.currentNode.position, onMoveSelected: ref.read(ctrlProvider.notifier).onUserMove, builder: ( context, diff --git a/lib/src/view/opening_explorer/opening_explorer_view_builder.dart b/lib/src/view/opening_explorer/opening_explorer_view_builder.dart index a71a17e8da..238d2a64d0 100644 --- a/lib/src/view/opening_explorer/opening_explorer_view_builder.dart +++ b/lib/src/view/opening_explorer/opening_explorer_view_builder.dart @@ -17,20 +17,70 @@ const _kTableRowPadding = EdgeInsets.symmetric( vertical: _kTableRowVerticalPadding, ); -/// A widget that displays the opening explorer moves and games for the given position. +/// Displays an opening explorer for the given position. +/// +/// It shows the top moves, games, and recent games for the given position, in a scrollable list. +/// +/// This widget is meant to be embedded in the analysis, broadcast, and study screens. +class OpeningExplorer extends ConsumerWidget { + const OpeningExplorer({ + required this.position, + required this.onMoveSelected, + }); + + final Position position; + final void Function(NormalMove) onMoveSelected; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return OpeningExplorerViewBuilder( + position: position, + onMoveSelected: onMoveSelected, + builder: (context, children, {required isLoading, required isIndexing}) { + final brightness = Theme.of(context).brightness; + final loadingOverlay = Positioned.fill( + child: IgnorePointer( + ignoring: !isLoading, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 400), + curve: Curves.fastOutSlowIn, + opacity: isLoading ? 0.20 : 0.0, + child: ColoredBox( + color: + brightness == Brightness.dark ? Colors.black : Colors.white, + ), + ), + ), + ); + + return Stack( + children: [ + ListView(padding: EdgeInsets.zero, children: children), + loadingOverlay, + ], + ); + }, + ); + } +} + +/// A widget that builds the opening explorer moves and games for the given position. +/// +/// The [builder] function is called with the list of children to display in the +/// opening explorer view. The [isLoading] and [isIndexing] parameters are used to +/// display a loading indicator and a message when the opening explorer is +/// indexing the games. /// /// Network requests are debounced and cached to avoid unnecessary requests. class OpeningExplorerViewBuilder extends ConsumerStatefulWidget { const OpeningExplorerViewBuilder({ - required this.ply, - required this.fen, + required this.position, required this.builder, - this.onMoveSelected, + required this.onMoveSelected, }); - final int ply; - final String fen; - final void Function(NormalMove)? onMoveSelected; + final Position position; + final void Function(NormalMove) onMoveSelected; final Widget Function( BuildContext context, List children, { @@ -63,7 +113,7 @@ class _OpeningExplorerState extends ConsumerState { ), ), online: () { - if (widget.ply >= 50) { + if (widget.position.ply >= 50) { return widget.builder( context, [ @@ -95,7 +145,7 @@ class _OpeningExplorerState extends ConsumerState { } final cacheKey = OpeningExplorerCacheKey( - fen: widget.fen, + fen: widget.position.fen, prefs: prefs, ); final cacheOpeningExplorer = cache[cacheKey]; @@ -104,11 +154,12 @@ class _OpeningExplorerState extends ConsumerState { (entry: cacheOpeningExplorer, isIndexing: false), ) : ref.watch( - openingExplorerProvider(fen: widget.fen), + openingExplorerProvider(fen: widget.position.fen), ); if (cacheOpeningExplorer == null) { - ref.listen(openingExplorerProvider(fen: widget.fen), (_, curAsync) { + ref.listen(openingExplorerProvider(fen: widget.position.fen), + (_, curAsync) { curAsync.whenData((cur) { if (cur != null && !cur.isIndexing) { cache[cacheKey] = cur.entry; @@ -139,7 +190,7 @@ class _OpeningExplorerState extends ConsumerState { final topGames = openingExplorer.entry.topGames; final recentGames = openingExplorer.entry.recentGames; - final ply = widget.ply; + final ply = widget.position.ply; final children = [ OpeningExplorerMoveTable( diff --git a/lib/src/view/study/study_screen.dart b/lib/src/view/study/study_screen.dart index d86c1a48a7..eba71a60e3 100644 --- a/lib/src/view/study/study_screen.dart +++ b/lib/src/view/study/study_screen.dart @@ -19,6 +19,7 @@ import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_layout.dart'; import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; import 'package:lichess_mobile/src/view/engine/engine_lines.dart'; +import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_view_builder.dart'; import 'package:lichess_mobile/src/view/study/study_bottom_bar.dart'; import 'package:lichess_mobile/src/view/study/study_gamebook.dart'; import 'package:lichess_mobile/src/view/study/study_settings.dart'; @@ -33,32 +34,18 @@ import 'package:logging/logging.dart'; final _logger = Logger('StudyScreen'); class StudyScreen extends ConsumerWidget { - const StudyScreen({ - required this.id, - }); + const StudyScreen({required this.id}); final StudyId id; @override Widget build(BuildContext context, WidgetRef ref) { final state = ref.watch(studyControllerProvider(id)); - return state.when( data: (state) { - return PlatformScaffold( - appBar: PlatformAppBar( - title: AutoSizeText( - state.currentChapterTitle, - maxLines: 2, - minFontSize: 14, - overflow: TextOverflow.ellipsis, - ), - actions: [ - _ChapterButton(id: id), - _StudyMenu(id: id), - ], - ), - body: _Body(id: id), + return _StudyScreen( + id: id, + studyState: state, ); }, loading: () { @@ -79,6 +66,92 @@ class StudyScreen extends ConsumerWidget { } } +class _StudyScreen extends ConsumerStatefulWidget { + const _StudyScreen({ + required this.id, + required this.studyState, + }); + + final StudyId id; + final StudyState studyState; + + @override + ConsumerState<_StudyScreen> createState() => _StudyScreenState(); +} + +class _StudyScreenState extends ConsumerState<_StudyScreen> + with TickerProviderStateMixin { + late List tabs; + late TabController _tabController; + + @override + void initState() { + super.initState(); + + tabs = [ + if (widget.studyState.isOpeningExplorerAvailable) AnalysisTab.opening, + AnalysisTab.moves, + ]; + + _tabController = TabController( + vsync: this, + initialIndex: tabs.length - 1, + length: tabs.length, + ); + } + + @override + void didUpdateWidget(covariant _StudyScreen oldWidget) { + // If the study has not yet loaded the opening explorer and it is now available + // with this chapter, add the opening tab. + // We don't want to remove a tab, so if the opening explorer is not available + // anymore, we keep the tabs as they are. + // In theory, studies mixing chapters with and without opening explorer should be pretty rare. + if (tabs.length < 2 && widget.studyState.isOpeningExplorerAvailable) { + tabs = [ + AnalysisTab.opening, + AnalysisTab.moves, + ]; + _tabController = TabController( + vsync: this, + initialIndex: tabs.length - 1, + length: tabs.length, + ); + } + super.didUpdateWidget(oldWidget); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return PlatformScaffold( + appBar: PlatformAppBar( + title: AutoSizeText( + widget.studyState.currentChapterTitle, + maxLines: 2, + minFontSize: 14, + overflow: TextOverflow.ellipsis, + ), + actions: [ + if (tabs.length > 1) + AppBarAnalysisTabIndicator( + tabs: tabs, + controller: _tabController, + ), + _ChapterButton(id: widget.id), + _StudyMenu(id: widget.id), + ], + ), + body: _Body(id: widget.id, tabController: _tabController, tabs: tabs), + ); + } +} + class _StudyMenu extends ConsumerWidget { const _StudyMenu({required this.id}); @@ -209,9 +282,13 @@ class _StudyChaptersMenu extends ConsumerWidget { class _Body extends ConsumerWidget { const _Body({ required this.id, + required this.tabController, + required this.tabs, }); final StudyId id; + final TabController tabController; + final List tabs; @override Widget build(BuildContext context, WidgetRef ref) { @@ -246,51 +323,70 @@ class _Body extends ConsumerWidget { final bottomChild = gamebookActive ? StudyGamebook(id) : StudyTreeView(id); - return DefaultTabController( - length: 1, - child: AnalysisLayout( - boardBuilder: (context, boardSize, borderRadius) => _StudyBoard( - id: id, - boardSize: boardSize, - borderRadius: borderRadius, - ), - engineGaugeBuilder: isComputerAnalysisAllowed && - showEvaluationGauge && - engineGaugeParams != null - ? (context, orientation) { - return orientation == Orientation.portrait - ? EngineGauge( - displayMode: EngineGaugeDisplayMode.horizontal, - params: engineGaugeParams, - ) - : Container( - clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4.0), - ), - child: EngineGauge( - displayMode: EngineGaugeDisplayMode.vertical, - params: engineGaugeParams, - ), - ); - } - : null, - engineLines: isComputerAnalysisAllowed && - isLocalEvaluationEnabled && - numEvalLines > 0 - ? EngineLines( - clientEval: currentNode.eval, - isGameOver: currentNode.position?.isGameOver ?? false, - onTapMove: ref - .read( - studyControllerProvider(id).notifier, - ) - .onUserMove, - ) - : null, - bottomBar: StudyBottomBar(id: id), - children: [bottomChild], + return AnalysisLayout( + tabController: tabController, + boardBuilder: (context, boardSize, borderRadius) => _StudyBoard( + id: id, + boardSize: boardSize, + borderRadius: borderRadius, ), + engineGaugeBuilder: isComputerAnalysisAllowed && + showEvaluationGauge && + engineGaugeParams != null + ? (context, orientation) { + return orientation == Orientation.portrait + ? EngineGauge( + displayMode: EngineGaugeDisplayMode.horizontal, + params: engineGaugeParams, + ) + : Container( + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4.0), + ), + child: EngineGauge( + displayMode: EngineGaugeDisplayMode.vertical, + params: engineGaugeParams, + ), + ); + } + : null, + engineLines: isComputerAnalysisAllowed && + isLocalEvaluationEnabled && + numEvalLines > 0 + ? EngineLines( + clientEval: currentNode.eval, + isGameOver: currentNode.position?.isGameOver ?? false, + onTapMove: ref + .read( + studyControllerProvider(id).notifier, + ) + .onUserMove, + ) + : null, + bottomBar: StudyBottomBar(id: id), + children: tabs.map((tab) { + switch (tab) { + case AnalysisTab.opening: + if (studyState.isOpeningExplorerAvailable && + studyState.currentNode.position != null) { + return OpeningExplorer( + position: studyState.currentNode.position!, + onMoveSelected: (move) { + ref + .read(studyControllerProvider(id).notifier) + .onUserMove(move); + }, + ); + } else { + return const Center( + child: Text('Opening explorer not available.'), + ); + } + case _: + return bottomChild; + } + }).toList(), ); } } From be87a7ae01dfd602dc218dfc5d4b8b8d21b43de9 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 2 Dec 2024 16:10:48 +0100 Subject: [PATCH 817/979] Fix tests --- .../opening_explorer_screen_test.dart | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/test/view/opening_explorer/opening_explorer_screen_test.dart b/test/view/opening_explorer/opening_explorer_screen_test.dart index 662440da3b..5b224f369f 100644 --- a/test/view/opening_explorer/opening_explorer_screen_test.dart +++ b/test/view/opening_explorer/opening_explorer_screen_test.dart @@ -72,8 +72,12 @@ void main() { ], ); await tester.pumpWidget(app); + // wait for analysis controller to load expect(find.byType(CircularProgressIndicator), findsOneWidget); - await tester.pump(const Duration(milliseconds: 1)); + await tester.pump(const Duration(milliseconds: 10)); + // wait for connectivity provider to load + expect(find.byType(CircularProgressIndicator), findsOneWidget); + await tester.pump(const Duration(milliseconds: 10)); // wait for opening explorer data to load (taking debounce delay into account) await tester.pump(const Duration(milliseconds: 350)); @@ -128,8 +132,12 @@ void main() { }, ); await tester.pumpWidget(app); + // wait for analysis controller to load + expect(find.byType(CircularProgressIndicator), findsOneWidget); + await tester.pump(const Duration(milliseconds: 10)); + // wait for connectivity provider to load expect(find.byType(CircularProgressIndicator), findsOneWidget); - await tester.pump(const Duration(milliseconds: 1)); + await tester.pump(const Duration(milliseconds: 10)); // wait for opening explorer data to load (taking debounce delay into account) await tester.pump(const Duration(milliseconds: 350)); @@ -181,8 +189,12 @@ void main() { }, ); await tester.pumpWidget(app); + // wait for analysis controller to load + expect(find.byType(CircularProgressIndicator), findsOneWidget); + await tester.pump(const Duration(milliseconds: 10)); + // wait for connectivity provider to load expect(find.byType(CircularProgressIndicator), findsOneWidget); - await tester.pump(const Duration(milliseconds: 1)); + await tester.pump(const Duration(milliseconds: 10)); // wait for opening explorer data to load (taking debounce delay into account) await tester.pump(const Duration(milliseconds: 350)); From f57264001858539c29c5e8240354a1a7567ac686 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Mon, 2 Dec 2024 18:07:36 +0100 Subject: [PATCH 818/979] Get the round field from the broadcast api round endpoint to change the title on the broadcast game screen to the round name --- lib/src/model/broadcast/broadcast.dart | 5 ++ .../model/broadcast/broadcast_repository.dart | 10 +-- .../broadcast/broadcast_round_controller.dart | 72 +++++++++++-------- .../view/broadcast/broadcast_boards_tab.dart | 17 ++--- .../view/broadcast/broadcast_game_screen.dart | 2 +- lib/src/view/broadcast/broadcast_screen.dart | 10 +-- 6 files changed, 61 insertions(+), 55 deletions(-) diff --git a/lib/src/model/broadcast/broadcast.dart b/lib/src/model/broadcast/broadcast.dart index 8d23cdd157..2ad7af01fa 100644 --- a/lib/src/model/broadcast/broadcast.dart +++ b/lib/src/model/broadcast/broadcast.dart @@ -84,6 +84,11 @@ class BroadcastRound with _$BroadcastRound { }) = _BroadcastRound; } +typedef BroadcastRoundWithGames = ({ + BroadcastRound round, + BroadcastRoundGames games, +}); + typedef BroadcastRoundGames = IMap; @freezed diff --git a/lib/src/model/broadcast/broadcast_repository.dart b/lib/src/model/broadcast/broadcast_repository.dart index afa5f61d1c..6b4cf524a3 100644 --- a/lib/src/model/broadcast/broadcast_repository.dart +++ b/lib/src/model/broadcast/broadcast_repository.dart @@ -33,7 +33,7 @@ class BroadcastRepository { ); } - Future getRound( + Future getRound( BroadcastRoundId broadcastRoundId, ) { return client.readJson( @@ -41,7 +41,7 @@ class BroadcastRepository { // The path parameters with - are the broadcast tournament and round slugs // They are only used for SEO, so we can safely use - for these parameters headers: {'Accept': 'application/x-ndjson'}, - mapper: _makeGamesFromJson, + mapper: _makeRoundWithGamesFromJson, ); } @@ -140,8 +140,10 @@ BroadcastRound _roundFromPick(RequiredPick pick) { ); } -BroadcastRoundGames _makeGamesFromJson(Map json) => - _gamesFromPick(pick(json).required()); +BroadcastRoundWithGames _makeRoundWithGamesFromJson(Map json) { + final roundPick = pick(json).required(); + return (round: _roundFromPick(roundPick), games: _gamesFromPick(roundPick)); +} BroadcastRoundGames _gamesFromPick( RequiredPick pick, diff --git a/lib/src/model/broadcast/broadcast_round_controller.dart b/lib/src/model/broadcast/broadcast_round_controller.dart index 66913e3ea6..3ead272233 100644 --- a/lib/src/model/broadcast/broadcast_round_controller.dart +++ b/lib/src/model/broadcast/broadcast_round_controller.dart @@ -25,7 +25,9 @@ class BroadcastRoundController extends _$BroadcastRoundController { late SocketClient _socketClient; @override - Future build(BroadcastRoundId broadcastRoundId) async { + Future build( + BroadcastRoundId broadcastRoundId, + ) async { _socketClient = ref .watch(socketPoolProvider) .open(BroadcastRoundController.broadcastSocketUri(broadcastRoundId)); @@ -36,11 +38,11 @@ class BroadcastRoundController extends _$BroadcastRoundController { _subscription?.cancel(); }); - final games = await ref.withClient( + final round = await ref.withClient( (client) => BroadcastRepository(client).getRound(broadcastRoundId), ); - return games; + return round; } void _handleSocketEvent(SocketEvent event) { @@ -78,22 +80,25 @@ class BroadcastRoundController extends _$BroadcastRoundController { final playingSide = Setup.parseFen(fen).turn; state = AsyncData( - state.requireValue.update( - broadcastGameId, - (broadcastGame) => broadcastGame.copyWith( - players: IMap( - { - playingSide: broadcastGame.players[playingSide]!, - playingSide.opposite: - broadcastGame.players[playingSide.opposite]!.copyWith( - clock: pick(event.data, 'n', 'clock') - .asDurationFromCentiSecondsOrNull(), - ), - }, + ( + round: state.requireValue.round, + games: state.requireValue.games.update( + broadcastGameId, + (broadcastGame) => broadcastGame.copyWith( + players: IMap( + { + playingSide: broadcastGame.players[playingSide]!, + playingSide.opposite: + broadcastGame.players[playingSide.opposite]!.copyWith( + clock: pick(event.data, 'n', 'clock') + .asDurationFromCentiSecondsOrNull(), + ), + }, + ), + fen: fen, + lastMove: pick(event.data, 'n', 'uci').asUciMoveOrThrow(), + updatedClockAt: DateTime.now(), ), - fen: fen, - lastMove: pick(event.data, 'n', 'uci').asUciMoveOrThrow(), - updatedClockAt: DateTime.now(), ), ), ); @@ -105,7 +110,9 @@ class BroadcastRoundController extends _$BroadcastRoundController { void _handleChaptersEvent(SocketEvent event) { final games = pick(event.data).asListOrThrow(gameFromPick); - state = AsyncData(IMap.fromEntries(games)); + state = AsyncData( + (round: state.requireValue.round, games: IMap.fromEntries(games)), + ); } void _handleClockEvent(SocketEvent event) { @@ -117,18 +124,21 @@ class BroadcastRoundController extends _$BroadcastRoundController { if (relayClocks.value == null) return; state = AsyncData( - state.requireValue.update( - broadcastGameId, - (broadcastsGame) => broadcastsGame.copyWith( - players: IMap( - { - Side.white: broadcastsGame.players[Side.white]!.copyWith( - clock: relayClocks(0).asDurationFromCentiSecondsOrNull(), - ), - Side.black: broadcastsGame.players[Side.black]!.copyWith( - clock: relayClocks(1).asDurationFromCentiSecondsOrNull(), - ), - }, + ( + round: state.requireValue.round, + games: state.requireValue.games.update( + broadcastGameId, + (broadcastsGame) => broadcastsGame.copyWith( + players: IMap( + { + Side.white: broadcastsGame.players[Side.white]!.copyWith( + clock: relayClocks(0).asDurationFromCentiSecondsOrNull(), + ), + Side.black: broadcastsGame.players[Side.black]!.copyWith( + clock: relayClocks(1).asDurationFromCentiSecondsOrNull(), + ), + }, + ), ), ), ), diff --git a/lib/src/view/broadcast/broadcast_boards_tab.dart b/lib/src/view/broadcast/broadcast_boards_tab.dart index f87280d299..d0a092ac53 100644 --- a/lib/src/view/broadcast/broadcast_boards_tab.dart +++ b/lib/src/view/broadcast/broadcast_boards_tab.dart @@ -26,30 +26,25 @@ const _kPlayerWidgetPadding = EdgeInsets.symmetric(vertical: 5.0); /// A tab that displays the live games of a broadcast round. class BroadcastBoardsTab extends ConsumerWidget { final BroadcastRoundId roundId; - final String title; - const BroadcastBoardsTab({ - super.key, - required this.roundId, - required this.title, - }); + const BroadcastBoardsTab(this.roundId); @override Widget build(BuildContext context, WidgetRef ref) { - final games = ref.watch(broadcastRoundControllerProvider(roundId)); + final round = ref.watch(broadcastRoundControllerProvider(roundId)); return SafeArea( bottom: false, - child: switch (games) { - AsyncData(:final value) => (value.isEmpty) + child: switch (round) { + AsyncData(:final value) => (value.games.isEmpty) ? const Padding( padding: Styles.bodyPadding, child: Text('No boards to show for now'), ) : BroadcastPreview( - games: value.values.toIList(), + games: value.games.values.toIList(), roundId: roundId, - title: title, + title: value.round.name, ), AsyncError(:final error) => Center( child: Text(error.toString()), diff --git a/lib/src/view/broadcast/broadcast_game_screen.dart b/lib/src/view/broadcast/broadcast_game_screen.dart index fafe8fdf13..f95dca7e93 100644 --- a/lib/src/view/broadcast/broadcast_game_screen.dart +++ b/lib/src/view/broadcast/broadcast_game_screen.dart @@ -333,7 +333,7 @@ class _PlayerWidget extends ConsumerWidget { final game = ref.watch( broadcastRoundControllerProvider(roundId) - .select((game) => game.requireValue[gameId]!), + .select((round) => round.requireValue.games[gameId]!), ); final player = game.players[side]!; final gameStatus = game.status; diff --git a/lib/src/view/broadcast/broadcast_screen.dart b/lib/src/view/broadcast/broadcast_screen.dart index 95f069b157..8565a2cf21 100644 --- a/lib/src/view/broadcast/broadcast_screen.dart +++ b/lib/src/view/broadcast/broadcast_screen.dart @@ -89,10 +89,7 @@ class _AndroidScreenState extends State<_AndroidScreen> controller: _tabController, children: [ BroadcastOverviewTab(tournamentId: _selectedTournamentId), - BroadcastBoardsTab( - roundId: _selectedRoundId, - title: widget.broadcast.title, - ), + BroadcastBoardsTab(_selectedRoundId), ], ), bottomNavigationBar: BottomAppBar( @@ -171,10 +168,7 @@ class _CupertinoScreenState extends State<_CupertinoScreen> { Expanded( child: _selectedSegment == _ViewMode.overview ? BroadcastOverviewTab(tournamentId: _selectedTournamentId) - : BroadcastBoardsTab( - roundId: _selectedRoundId, - title: widget.broadcast.title, - ), + : BroadcastBoardsTab(_selectedRoundId), ), _IOSTournamentAndRoundSelector( tournamentId: _selectedTournamentId, From b4339a24c676126be1dc0c112737f27b8009c81c Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Mon, 2 Dec 2024 18:15:45 +0100 Subject: [PATCH 819/979] Fix pick API fields --- lib/src/model/broadcast/broadcast_repository.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/src/model/broadcast/broadcast_repository.dart b/lib/src/model/broadcast/broadcast_repository.dart index 6b4cf524a3..4c96f626ab 100644 --- a/lib/src/model/broadcast/broadcast_repository.dart +++ b/lib/src/model/broadcast/broadcast_repository.dart @@ -141,14 +141,15 @@ BroadcastRound _roundFromPick(RequiredPick pick) { } BroadcastRoundWithGames _makeRoundWithGamesFromJson(Map json) { - final roundPick = pick(json).required(); - return (round: _roundFromPick(roundPick), games: _gamesFromPick(roundPick)); + final round = pick(json, 'round').required(); + final games = pick(json, 'games').required(); + return (round: _roundFromPick(round), games: _gamesFromPick(games)); } BroadcastRoundGames _gamesFromPick( RequiredPick pick, ) => - IMap.fromEntries(pick('games').asListOrThrow(gameFromPick)); + IMap.fromEntries(pick.asListOrThrow(gameFromPick)); MapEntry gameFromPick( RequiredPick pick, From a433229b86341eeead7f84d4943e44117e749afa Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Mon, 2 Dec 2024 18:17:10 +0100 Subject: [PATCH 820/979] Fix tests --- test/model/broadcast/broadcast_repository_test.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/model/broadcast/broadcast_repository_test.dart b/test/model/broadcast/broadcast_repository_test.dart index 7d7b78f0fb..ad7d6f31d3 100644 --- a/test/model/broadcast/broadcast_repository_test.dart +++ b/test/model/broadcast/broadcast_repository_test.dart @@ -60,8 +60,8 @@ void main() { final response = await repo.getRound(const BroadcastRoundId(roundId)); - expect(response, isA()); - expect(response.length, 5); + expect(response, isA()); + expect(response.games.length, 5); }); }); } From 54386611922231d9dbc09ca432ddb2a27fec280c Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 3 Dec 2024 14:56:18 +0100 Subject: [PATCH 821/979] Refactor opening explorer widgets --- lib/src/view/analysis/analysis_screen.dart | 4 +- .../view/broadcast/broadcast_game_screen.dart | 4 +- .../opening_explorer_screen.dart | 281 +++++++----------- ...uilder.dart => opening_explorer_view.dart} | 141 ++++----- .../opening_explorer_widgets.dart | 69 ++++- lib/src/view/study/study_screen.dart | 4 +- 6 files changed, 239 insertions(+), 264 deletions(-) rename lib/src/view/opening_explorer/{opening_explorer_view_builder.dart => opening_explorer_view.dart} (76%) diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index f1ac2e5f99..4b7065c8cf 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -15,7 +15,7 @@ import 'package:lichess_mobile/src/view/board_editor/board_editor_screen.dart'; import 'package:lichess_mobile/src/view/engine/engine_depth.dart'; import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; import 'package:lichess_mobile/src/view/engine/engine_lines.dart'; -import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_view_builder.dart'; +import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_view.dart'; import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; @@ -216,7 +216,7 @@ class _Body extends ConsumerWidget { : null, bottomBar: _BottomBar(options: options), children: [ - OpeningExplorer( + OpeningExplorerView( position: currentNode.position, onMoveSelected: (move) { ref.read(ctrlProvider.notifier).onUserMove(move); diff --git a/lib/src/view/broadcast/broadcast_game_screen.dart b/lib/src/view/broadcast/broadcast_game_screen.dart index 1054f590e7..33381d917c 100644 --- a/lib/src/view/broadcast/broadcast_game_screen.dart +++ b/lib/src/view/broadcast/broadcast_game_screen.dart @@ -26,7 +26,7 @@ import 'package:lichess_mobile/src/view/broadcast/broadcast_game_settings.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_game_tree_view.dart'; import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; import 'package:lichess_mobile/src/view/engine/engine_lines.dart'; -import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_view_builder.dart'; +import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_view.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/clock.dart'; import 'package:lichess_mobile/src/widgets/pgn.dart'; @@ -199,7 +199,7 @@ class _OpeningExplorerTab extends ConsumerWidget { final ctrlProvider = broadcastGameControllerProvider(roundId, gameId); final state = ref.watch(ctrlProvider).requireValue; - return OpeningExplorer( + return OpeningExplorerView( position: state.currentNode.position, onMoveSelected: ref.read(ctrlProvider.notifier).onUserMove, ); diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index 9b552271cd..c1002bd150 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -10,7 +10,7 @@ import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_board.dart'; -import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_view_builder.dart'; +import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_view.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; @@ -31,59 +31,52 @@ class OpeningExplorerScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final ctrlProvider = analysisControllerProvider(options); - switch (ref.watch(ctrlProvider)) { - case AsyncData(value: final state): - return OpeningExplorerViewBuilder( - position: state.currentNode.position, - onMoveSelected: ref.read(ctrlProvider.notifier).onUserMove, - builder: ( - context, - children, { - required isLoading, - required isIndexing, - }) { - return _OpeningExplorerView( - options: options, - isLoading: isLoading, - isIndexing: isIndexing, - children: children, - ); - }, - ); - case AsyncError(:final error): - debugPrint( - 'SEVERE: [OpeningExplorerScreen] could not load analysis data; $error', - ); - return Center( + + final body = switch (ref.watch(ctrlProvider)) { + AsyncData(value: final state) => _Body(options: options, state: state), + AsyncError(:final error) => Center( child: Padding( padding: const EdgeInsets.all(16.0), child: Text(error.toString()), ), - ); - case _: - return const CenterLoadingIndicator(); - } + ), + _ => const CenterLoadingIndicator(), + }; + + return PlatformWidget( + androidBuilder: (_) => Scaffold( + body: body, + appBar: AppBar( + title: Text(context.l10n.openingExplorer), + bottom: _MoveList(options: options), + ), + ), + iosBuilder: (_) => CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + middle: Text(context.l10n.openingExplorer), + automaticBackgroundVisibility: false, + border: null, + ), + child: body, + ), + ); } } -class _OpeningExplorerView extends StatelessWidget { - const _OpeningExplorerView({ +class _Body extends ConsumerWidget { + const _Body({ required this.options, - required this.children, - required this.isLoading, - required this.isIndexing, + required this.state, }); final AnalysisOptions options; - final List children; - final bool isLoading; - final bool isIndexing; + final AnalysisState state; @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { final isTablet = isTabletOrLarger(context); - final body = SafeArea( + return SafeArea( bottom: false, child: Column( children: [ @@ -99,35 +92,18 @@ class _OpeningExplorerView extends StatelessWidget { Expanded( child: LayoutBuilder( builder: (context, constraints) { - final aspectRatio = constraints.biggest.aspectRatio; - final defaultBoardSize = constraints.biggest.shortestSide; - final remainingHeight = - constraints.maxHeight - defaultBoardSize; - final isSmallScreen = - remainingHeight < kSmallRemainingHeightLeftBoardThreshold; - final boardSize = isTablet || isSmallScreen - ? defaultBoardSize - kTabletBoardTableSidePadding * 2 - : defaultBoardSize; - - final isLandscape = aspectRatio > 1; - final brightness = Theme.of(context).brightness; - final loadingOverlay = Positioned.fill( - child: IgnorePointer( - ignoring: !isLoading, - child: AnimatedOpacity( - duration: const Duration(milliseconds: 400), - curve: Curves.fastOutSlowIn, - opacity: isLoading ? 0.2 : 0.0, - child: ColoredBox( - color: brightness == Brightness.dark - ? Colors.black - : Colors.white, - ), - ), - ), - ); - - if (isLandscape) { + final orientation = constraints.maxWidth > constraints.maxHeight + ? Orientation.landscape + : Orientation.portrait; + if (orientation == Orientation.landscape) { + final sideWidth = constraints.biggest.longestSide - + constraints.biggest.shortestSide; + final defaultBoardSize = constraints.biggest.shortestSide - + (kTabletBoardTableSidePadding * 2); + final boardSize = sideWidth >= 250 + ? defaultBoardSize + : constraints.biggest.longestSide / kGoldenRatio - + (kTabletBoardTableSidePadding * 2); return Row( mainAxisSize: MainAxisSize.max, children: [ @@ -159,14 +135,16 @@ class _OpeningExplorerView extends StatelessWidget { kTabletBoardTableSidePadding, ), semanticContainer: false, - child: Stack( - children: [ - ListView( - padding: EdgeInsets.zero, - children: children, - ), - loadingOverlay, - ], + child: OpeningExplorerView( + position: state.position, + onMoveSelected: (move) { + ref + .read( + analysisControllerProvider(options) + .notifier, + ) + .onUserMove(move); + }, ), ), ), @@ -176,6 +154,15 @@ class _OpeningExplorerView extends StatelessWidget { ], ); } else { + final defaultBoardSize = constraints.biggest.shortestSide; + final remainingHeight = + constraints.maxHeight - defaultBoardSize; + final isSmallScreen = + remainingHeight < kSmallRemainingHeightLeftBoardThreshold; + final boardSize = isTablet || isSmallScreen + ? defaultBoardSize - kTabletBoardTableSidePadding * 2 + : defaultBoardSize; + return ListView( padding: isTablet ? const EdgeInsets.symmetric( @@ -192,11 +179,16 @@ class _OpeningExplorerView extends StatelessWidget { shouldReplaceChildOnUserMove: true, ), ), - Stack( - children: [ - ListBody(children: children), - if (isLoading) loadingOverlay, - ], + OpeningExplorerView( + position: state.position, + onMoveSelected: (move) { + ref + .read( + analysisControllerProvider(options).notifier, + ) + .onUserMove(move); + }, + scrollable: false, ), ], ); @@ -208,74 +200,6 @@ class _OpeningExplorerView extends StatelessWidget { ], ), ); - - return PlatformWidget( - androidBuilder: (_) => Scaffold( - body: body, - appBar: AppBar( - title: Text(context.l10n.openingExplorer), - bottom: _MoveList(options: options), - actions: [ - if (isIndexing) const _IndexingIndicator(), - ], - ), - ), - iosBuilder: (_) => CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - middle: Text(context.l10n.openingExplorer), - automaticBackgroundVisibility: false, - border: null, - trailing: isIndexing ? const _IndexingIndicator() : null, - ), - child: body, - ), - ); - } -} - -class _IndexingIndicator extends StatefulWidget { - const _IndexingIndicator(); - - @override - State<_IndexingIndicator> createState() => _IndexingIndicatorState(); -} - -class _IndexingIndicatorState extends State<_IndexingIndicator> - with TickerProviderStateMixin { - late AnimationController controller; - - @override - void initState() { - controller = AnimationController( - vsync: this, - duration: const Duration(seconds: 3), - )..addListener(() { - setState(() {}); - }); - controller.repeat(); - super.initState(); - } - - @override - void dispose() { - controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(10.0), - child: SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator.adaptive( - value: controller.value, - // TODO: l10n - semanticsLabel: 'Indexing', - ), - ), - ); } } @@ -290,34 +214,39 @@ class _MoveList extends ConsumerWidget implements PreferredSizeWidget { @override Widget build(BuildContext context, WidgetRef ref) { final ctrlProvider = analysisControllerProvider(options); - final state = ref.watch(ctrlProvider).requireValue; - final slicedMoves = state.root.mainline - .map((e) => e.sanMove.san) - .toList() - .asMap() - .entries - .slices(2); - final currentMoveIndex = state.currentNode.position.ply; - return MoveList( - inlineDecoration: Theme.of(context).platform == TargetPlatform.iOS - ? BoxDecoration( - color: Styles.cupertinoAppBarColor.resolveFrom(context), - border: const Border( - bottom: BorderSide( - color: Color(0x4D000000), - width: 0.0, - ), - ), - ) - : null, - type: MoveListType.inline, - slicedMoves: slicedMoves, - currentMoveIndex: currentMoveIndex, - onSelectMove: (index) { - ref.read(ctrlProvider.notifier).jumpToNthNodeOnMainline(index - 1); - }, - ); + switch (ref.watch(ctrlProvider)) { + case AsyncData(value: final state): + final slicedMoves = state.root.mainline + .map((e) => e.sanMove.san) + .toList() + .asMap() + .entries + .slices(2); + final currentMoveIndex = state.currentNode.position.ply; + + return MoveList( + inlineDecoration: Theme.of(context).platform == TargetPlatform.iOS + ? BoxDecoration( + color: Styles.cupertinoAppBarColor.resolveFrom(context), + border: const Border( + bottom: BorderSide( + color: Color(0x4D000000), + width: 0.0, + ), + ), + ) + : null, + type: MoveListType.inline, + slicedMoves: slicedMoves, + currentMoveIndex: currentMoveIndex, + onSelectMove: (index) { + ref.read(ctrlProvider.notifier).jumpToNthNodeOnMainline(index - 1); + }, + ); + case _: + return const SizedBox.shrink(); + } } } diff --git a/lib/src/view/opening_explorer/opening_explorer_view_builder.dart b/lib/src/view/opening_explorer/opening_explorer_view.dart similarity index 76% rename from lib/src/view/opening_explorer/opening_explorer_view_builder.dart rename to lib/src/view/opening_explorer/opening_explorer_view.dart index 238d2a64d0..16a26d2238 100644 --- a/lib/src/view/opening_explorer/opening_explorer_view_builder.dart +++ b/lib/src/view/opening_explorer/opening_explorer_view.dart @@ -19,81 +19,28 @@ const _kTableRowPadding = EdgeInsets.symmetric( /// Displays an opening explorer for the given position. /// -/// It shows the top moves, games, and recent games for the given position, in a scrollable list. +/// It shows the top moves, games, and recent games for the given position in a list view. +/// By default, the view is scrollable, but it can be disabled by setting [scrollable] to false. /// /// This widget is meant to be embedded in the analysis, broadcast, and study screens. -class OpeningExplorer extends ConsumerWidget { - const OpeningExplorer({ - required this.position, - required this.onMoveSelected, - }); - - final Position position; - final void Function(NormalMove) onMoveSelected; - - @override - Widget build(BuildContext context, WidgetRef ref) { - return OpeningExplorerViewBuilder( - position: position, - onMoveSelected: onMoveSelected, - builder: (context, children, {required isLoading, required isIndexing}) { - final brightness = Theme.of(context).brightness; - final loadingOverlay = Positioned.fill( - child: IgnorePointer( - ignoring: !isLoading, - child: AnimatedOpacity( - duration: const Duration(milliseconds: 400), - curve: Curves.fastOutSlowIn, - opacity: isLoading ? 0.20 : 0.0, - child: ColoredBox( - color: - brightness == Brightness.dark ? Colors.black : Colors.white, - ), - ), - ), - ); - - return Stack( - children: [ - ListView(padding: EdgeInsets.zero, children: children), - loadingOverlay, - ], - ); - }, - ); - } -} - -/// A widget that builds the opening explorer moves and games for the given position. -/// -/// The [builder] function is called with the list of children to display in the -/// opening explorer view. The [isLoading] and [isIndexing] parameters are used to -/// display a loading indicator and a message when the opening explorer is -/// indexing the games. /// /// Network requests are debounced and cached to avoid unnecessary requests. -class OpeningExplorerViewBuilder extends ConsumerStatefulWidget { - const OpeningExplorerViewBuilder({ +class OpeningExplorerView extends ConsumerStatefulWidget { + const OpeningExplorerView({ required this.position, - required this.builder, required this.onMoveSelected, + this.scrollable = true, }); final Position position; final void Function(NormalMove) onMoveSelected; - final Widget Function( - BuildContext context, - List children, { - required bool isLoading, - required bool isIndexing, - }) builder; + final bool scrollable; @override - ConsumerState createState() => - _OpeningExplorerState(); + ConsumerState createState() => _OpeningExplorerState(); } -class _OpeningExplorerState extends ConsumerState { +class _OpeningExplorerState extends ConsumerState { final Map cache = {}; /// Last explorer content that was successfully loaded. This is used to @@ -114,13 +61,12 @@ class _OpeningExplorerState extends ConsumerState { ), online: () { if (widget.position.ply >= 50) { - return widget.builder( - context, - [ - const OpeningExplorerMoveTable.maxDepth(), - ], + return _ExplorerListView( + scrollable: widget.scrollable, isLoading: false, - isIndexing: false, + children: const [ + OpeningExplorerMoveTable.maxDepth(), + ], ); } @@ -128,10 +74,11 @@ class _OpeningExplorerState extends ConsumerState { if (prefs.db == OpeningDatabase.player && prefs.playerDb.username == null) { - return widget.builder( - context, - [ - const Padding( + return _ExplorerListView( + scrollable: widget.scrollable, + isLoading: false, + children: const [ + Padding( padding: _kTableRowPadding, child: Center( // TODO: l10n @@ -139,8 +86,6 @@ class _OpeningExplorerState extends ConsumerState { ), ), ], - isLoading: false, - isIndexing: false, ); } @@ -171,9 +116,10 @@ class _OpeningExplorerState extends ConsumerState { final isLoading = openingExplorerAsync.isLoading || openingExplorerAsync.value == null; - return widget.builder( - context, - openingExplorerAsync.when( + return _ExplorerListView( + scrollable: widget.scrollable, + isLoading: isLoading, + children: openingExplorerAsync.when( data: (openingExplorer) { if (openingExplorer == null) { return lastExplorerWidgets ?? @@ -199,6 +145,7 @@ class _OpeningExplorerState extends ConsumerState { draws: openingExplorer.entry.draws, blackWins: openingExplorer.entry.black, onMoveSelected: widget.onMoveSelected, + isIndexing: openingExplorer.isIndexing, ), if (topGames != null && topGames.isNotEmpty) ...[ OpeningExplorerHeaderTile( @@ -274,10 +221,48 @@ class _OpeningExplorerState extends ConsumerState { ]; }, ), - isLoading: isLoading, - isIndexing: openingExplorerAsync.value?.isIndexing ?? false, ); }, ); } } + +class _ExplorerListView extends StatelessWidget { + const _ExplorerListView({ + required this.children, + required this.isLoading, + required this.scrollable, + }); + + final List children; + final bool isLoading; + final bool scrollable; + + @override + Widget build(BuildContext context) { + final brightness = Theme.of(context).brightness; + final loadingOverlay = Positioned.fill( + child: IgnorePointer( + ignoring: !isLoading, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 400), + curve: Curves.fastOutSlowIn, + opacity: isLoading ? 0.20 : 0.0, + child: ColoredBox( + color: brightness == Brightness.dark ? Colors.black : Colors.white, + ), + ), + ), + ); + + return Stack( + children: [ + if (scrollable) + ListView(padding: EdgeInsets.zero, children: children) + else + ListBody(children: children), + loadingOverlay, + ], + ); + } +} diff --git a/lib/src/view/opening_explorer/opening_explorer_widgets.dart b/lib/src/view/opening_explorer/opening_explorer_widgets.dart index 55a93a665d..d8e2562cb6 100644 --- a/lib/src/view/opening_explorer/opening_explorer_widgets.dart +++ b/lib/src/view/opening_explorer/opening_explorer_widgets.dart @@ -15,6 +15,7 @@ const _kTableRowPadding = EdgeInsets.symmetric( horizontal: _kTableRowHorizontalPadding, vertical: _kTableRowVerticalPadding, ); +const _kHeaderTextStyle = TextStyle(fontSize: 12); Color _whiteBoxColor(BuildContext context) => Theme.of(context).brightness == Brightness.dark @@ -34,6 +35,7 @@ class OpeningExplorerMoveTable extends ConsumerWidget { required this.draws, required this.blackWins, this.onMoveSelected, + this.isIndexing = false, }) : _isLoading = false, _maxDepthReached = false; @@ -44,6 +46,7 @@ class OpeningExplorerMoveTable extends ConsumerWidget { draws = 0, blackWins = 0, _maxDepthReached = false, + isIndexing = false, onMoveSelected = null; const OpeningExplorerMoveTable.maxDepth() @@ -53,6 +56,7 @@ class OpeningExplorerMoveTable extends ConsumerWidget { draws = 0, blackWins = 0, _maxDepthReached = true, + isIndexing = false, onMoveSelected = null; final IList moves; @@ -60,6 +64,7 @@ class OpeningExplorerMoveTable extends ConsumerWidget { final int draws; final int blackWins; final void Function(NormalMove)? onMoveSelected; + final bool isIndexing; final bool _isLoading; final bool _maxDepthReached; @@ -79,7 +84,6 @@ class OpeningExplorerMoveTable extends ConsumerWidget { } final games = whiteWins + draws + blackWins; - const headerTextStyle = TextStyle(fontSize: 12); return Table( columnWidths: columnWidths, @@ -91,15 +95,28 @@ class OpeningExplorerMoveTable extends ConsumerWidget { children: [ Padding( padding: _kTableRowPadding, - child: Text(context.l10n.move, style: headerTextStyle), + child: Text(context.l10n.move, style: _kHeaderTextStyle), ), Padding( padding: _kTableRowPadding, - child: Text(context.l10n.games, style: headerTextStyle), + child: Text(context.l10n.games, style: _kHeaderTextStyle), ), Padding( padding: _kTableRowPadding, - child: Text(context.l10n.whiteDrawBlack, style: headerTextStyle), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + context.l10n.whiteDrawBlack, + style: _kHeaderTextStyle, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + if (isIndexing) const IndexingIndicator(), + ], + ), ), ], ), @@ -273,6 +290,50 @@ class OpeningExplorerMoveTable extends ConsumerWidget { ); } +class IndexingIndicator extends StatefulWidget { + const IndexingIndicator(); + + @override + State createState() => _IndexingIndicatorState(); +} + +class _IndexingIndicatorState extends State + with TickerProviderStateMixin { + late AnimationController controller; + + @override + void initState() { + controller = AnimationController( + vsync: this, + duration: const Duration(seconds: 3), + )..addListener(() { + setState(() {}); + }); + controller.repeat(); + super.initState(); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SizedBox( + width: 12.0, + height: 12.0, + child: CircularProgressIndicator( + strokeWidth: 1.5, + value: controller.value, + // TODO: l10n + semanticsLabel: 'Indexing', + ), + ); + } +} + class OpeningExplorerHeaderTile extends StatelessWidget { const OpeningExplorerHeaderTile({required this.child, super.key}); diff --git a/lib/src/view/study/study_screen.dart b/lib/src/view/study/study_screen.dart index eba71a60e3..e085daca9a 100644 --- a/lib/src/view/study/study_screen.dart +++ b/lib/src/view/study/study_screen.dart @@ -19,7 +19,7 @@ import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_layout.dart'; import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; import 'package:lichess_mobile/src/view/engine/engine_lines.dart'; -import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_view_builder.dart'; +import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_view.dart'; import 'package:lichess_mobile/src/view/study/study_bottom_bar.dart'; import 'package:lichess_mobile/src/view/study/study_gamebook.dart'; import 'package:lichess_mobile/src/view/study/study_settings.dart'; @@ -370,7 +370,7 @@ class _Body extends ConsumerWidget { case AnalysisTab.opening: if (studyState.isOpeningExplorerAvailable && studyState.currentNode.position != null) { - return OpeningExplorer( + return OpeningExplorerView( position: studyState.currentNode.position!, onMoveSelected: (move) { ref From 2adf9a1a15f4054a0cec266d851598a4ef21fec8 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 3 Dec 2024 15:31:39 +0100 Subject: [PATCH 822/979] More explorer improvements --- .../opening_explorer_view.dart | 301 ++++++++---------- .../opening_explorer_widgets.dart | 42 +-- .../opening_explorer_screen_test.dart | 9 - 3 files changed, 143 insertions(+), 209 deletions(-) diff --git a/lib/src/view/opening_explorer/opening_explorer_view.dart b/lib/src/view/opening_explorer/opening_explorer_view.dart index 16a26d2238..dbfe2bc374 100644 --- a/lib/src/view/opening_explorer/opening_explorer_view.dart +++ b/lib/src/view/opening_explorer/opening_explorer_view.dart @@ -7,7 +7,6 @@ import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_repos import 'package:lichess_mobile/src/network/connectivity.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_widgets.dart'; -import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; const _kTableRowVerticalPadding = 12.0; @@ -49,156 +48,58 @@ class _OpeningExplorerState extends ConsumerState { @override Widget build(BuildContext context) { - final connectivity = ref.watch(connectivityChangesProvider); - return connectivity.whenIsLoading( - loading: () => const CenterLoadingIndicator(), - offline: () => const Center( - child: Padding( - padding: EdgeInsets.all(16.0), - // TODO l10n - child: Text('Opening explorer is not available offline.'), + if (widget.position.ply >= 50) { + return Padding( + padding: _kTableRowPadding, + child: Center( + child: Text(context.l10n.maxDepthReached), ), - ), - online: () { - if (widget.position.ply >= 50) { - return _ExplorerListView( - scrollable: widget.scrollable, - isLoading: false, - children: const [ - OpeningExplorerMoveTable.maxDepth(), - ], - ); - } - - final prefs = ref.watch(openingExplorerPreferencesProvider); - - if (prefs.db == OpeningDatabase.player && - prefs.playerDb.username == null) { - return _ExplorerListView( - scrollable: widget.scrollable, - isLoading: false, - children: const [ - Padding( - padding: _kTableRowPadding, - child: Center( - // TODO: l10n - child: Text('Select a Lichess player in the settings.'), - ), - ), - ], - ); - } - - final cacheKey = OpeningExplorerCacheKey( - fen: widget.position.fen, - prefs: prefs, - ); - final cacheOpeningExplorer = cache[cacheKey]; - final openingExplorerAsync = cacheOpeningExplorer != null - ? AsyncValue.data( - (entry: cacheOpeningExplorer, isIndexing: false), - ) - : ref.watch( - openingExplorerProvider(fen: widget.position.fen), - ); - - if (cacheOpeningExplorer == null) { - ref.listen(openingExplorerProvider(fen: widget.position.fen), - (_, curAsync) { - curAsync.whenData((cur) { - if (cur != null && !cur.isIndexing) { - cache[cacheKey] = cur.entry; - } - }); - }); - } - - final isLoading = openingExplorerAsync.isLoading || - openingExplorerAsync.value == null; - - return _ExplorerListView( - scrollable: widget.scrollable, - isLoading: isLoading, - children: openingExplorerAsync.when( - data: (openingExplorer) { - if (openingExplorer == null) { - return lastExplorerWidgets ?? - [ - const Shimmer( - child: ShimmerLoading( - isLoading: true, - child: OpeningExplorerMoveTable.loading(), - ), - ), - ]; - } + ); + } - final topGames = openingExplorer.entry.topGames; - final recentGames = openingExplorer.entry.recentGames; + final prefs = ref.watch(openingExplorerPreferencesProvider); - final ply = widget.position.ply; - - final children = [ - OpeningExplorerMoveTable( - moves: openingExplorer.entry.moves, - whiteWins: openingExplorer.entry.white, - draws: openingExplorer.entry.draws, - blackWins: openingExplorer.entry.black, - onMoveSelected: widget.onMoveSelected, - isIndexing: openingExplorer.isIndexing, - ), - if (topGames != null && topGames.isNotEmpty) ...[ - OpeningExplorerHeaderTile( - key: const Key('topGamesHeader'), - child: Text(context.l10n.topGames), - ), - ...List.generate( - topGames.length, - (int index) { - return OpeningExplorerGameTile( - key: Key('top-game-${topGames.get(index).id}'), - game: topGames.get(index), - color: index.isEven - ? Theme.of(context).colorScheme.surfaceContainerLow - : Theme.of(context) - .colorScheme - .surfaceContainerHigh, - ply: ply, - ); - }, - growable: false, - ), - ], - if (recentGames != null && recentGames.isNotEmpty) ...[ - OpeningExplorerHeaderTile( - key: const Key('recentGamesHeader'), - child: Text(context.l10n.recentGames), - ), - ...List.generate( - recentGames.length, - (int index) { - return OpeningExplorerGameTile( - key: Key('recent-game-${recentGames.get(index).id}'), - game: recentGames.get(index), - color: index.isEven - ? Theme.of(context).colorScheme.surfaceContainerLow - : Theme.of(context) - .colorScheme - .surfaceContainerHigh, - ply: ply, - ); - }, - growable: false, - ), - ], - ]; + if (prefs.db == OpeningDatabase.player && prefs.playerDb.username == null) { + return const Padding( + padding: _kTableRowPadding, + child: Center( + // TODO: l10n + child: Text('Select a Lichess player in the settings.'), + ), + ); + } - lastExplorerWidgets = children; + final cacheKey = OpeningExplorerCacheKey( + fen: widget.position.fen, + prefs: prefs, + ); + final cacheOpeningExplorer = cache[cacheKey]; + final openingExplorerAsync = cacheOpeningExplorer != null + ? AsyncValue.data( + (entry: cacheOpeningExplorer, isIndexing: false), + ) + : ref.watch( + openingExplorerProvider(fen: widget.position.fen), + ); - return children; - }, - loading: () => - lastExplorerWidgets ?? + if (cacheOpeningExplorer == null) { + ref.listen(openingExplorerProvider(fen: widget.position.fen), + (_, curAsync) { + curAsync.whenData((cur) { + if (cur != null && !cur.isIndexing) { + cache[cacheKey] = cur.entry; + } + }); + }); + } + + switch (openingExplorerAsync) { + case AsyncData(:final value): + if (value == null) { + return _ExplorerListView( + scrollable: widget.scrollable, + isLoading: true, + children: lastExplorerWidgets ?? [ const Shimmer( child: ShimmerLoading( @@ -207,23 +108,103 @@ class _OpeningExplorerState extends ConsumerState { ), ), ], - error: (e, s) { - debugPrint( - 'SEVERE: [OpeningExplorerView] could not load opening explorer data; $e\n$s', - ); - return [ - Center( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Text(e.toString()), + ); + } + + final topGames = value.entry.topGames; + final recentGames = value.entry.recentGames; + + final ply = widget.position.ply; + + final children = [ + OpeningExplorerMoveTable( + moves: value.entry.moves, + whiteWins: value.entry.white, + draws: value.entry.draws, + blackWins: value.entry.black, + onMoveSelected: widget.onMoveSelected, + isIndexing: value.isIndexing, + ), + if (topGames != null && topGames.isNotEmpty) ...[ + OpeningExplorerHeaderTile( + key: const Key('topGamesHeader'), + child: Text(context.l10n.topGames), + ), + ...List.generate( + topGames.length, + (int index) { + return OpeningExplorerGameTile( + key: Key('top-game-${topGames.get(index).id}'), + game: topGames.get(index), + color: index.isEven + ? Theme.of(context).colorScheme.surfaceContainerLow + : Theme.of(context).colorScheme.surfaceContainerHigh, + ply: ply, + ); + }, + growable: false, + ), + ], + if (recentGames != null && recentGames.isNotEmpty) ...[ + OpeningExplorerHeaderTile( + key: const Key('recentGamesHeader'), + child: Text(context.l10n.recentGames), + ), + ...List.generate( + recentGames.length, + (int index) { + return OpeningExplorerGameTile( + key: Key('recent-game-${recentGames.get(index).id}'), + game: recentGames.get(index), + color: index.isEven + ? Theme.of(context).colorScheme.surfaceContainerLow + : Theme.of(context).colorScheme.surfaceContainerHigh, + ply: ply, + ); + }, + growable: false, + ), + ], + ]; + + lastExplorerWidgets = children; + + return _ExplorerListView( + scrollable: widget.scrollable, + isLoading: false, + children: children, + ); + case AsyncError(:final error): + debugPrint( + 'SEVERE: [OpeningExplorerView] could not load opening explorer data; $error', + ); + final connectivity = ref.watch(connectivityChangesProvider); + // TODO l10n + final message = connectivity.whenIs( + online: () => 'Could not load opening explorer data.', + offline: () => 'Opening Explorer is not available offline.', + ); + return Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Text(message), + ), + ); + case _: + return _ExplorerListView( + scrollable: widget.scrollable, + isLoading: true, + children: lastExplorerWidgets ?? + [ + const Shimmer( + child: ShimmerLoading( + isLoading: true, + child: OpeningExplorerMoveTable.loading(), ), ), - ]; - }, - ), + ], ); - }, - ); + } } } diff --git a/lib/src/view/opening_explorer/opening_explorer_widgets.dart b/lib/src/view/opening_explorer/opening_explorer_widgets.dart index d8e2562cb6..39f7f4e8e3 100644 --- a/lib/src/view/opening_explorer/opening_explorer_widgets.dart +++ b/lib/src/view/opening_explorer/opening_explorer_widgets.dart @@ -36,8 +36,7 @@ class OpeningExplorerMoveTable extends ConsumerWidget { required this.blackWins, this.onMoveSelected, this.isIndexing = false, - }) : _isLoading = false, - _maxDepthReached = false; + }) : _isLoading = false; const OpeningExplorerMoveTable.loading() : _isLoading = true, @@ -45,17 +44,6 @@ class OpeningExplorerMoveTable extends ConsumerWidget { whiteWins = 0, draws = 0, blackWins = 0, - _maxDepthReached = false, - isIndexing = false, - onMoveSelected = null; - - const OpeningExplorerMoveTable.maxDepth() - : _isLoading = false, - moves = const IListConst([]), - whiteWins = 0, - draws = 0, - blackWins = 0, - _maxDepthReached = true, isIndexing = false, onMoveSelected = null; @@ -67,7 +55,6 @@ class OpeningExplorerMoveTable extends ConsumerWidget { final bool isIndexing; final bool _isLoading; - final bool _maxDepthReached; String formatNum(int num) => NumberFormat.decimalPatternDigits().format(num); @@ -164,32 +151,7 @@ class OpeningExplorerMoveTable extends ConsumerWidget { ); }, ), - if (_maxDepthReached) - TableRow( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainerLow, - ), - children: [ - Padding( - padding: _kTableRowPadding, - child: Text( - String.fromCharCode(Icons.not_interested_outlined.codePoint), - style: TextStyle( - fontFamily: Icons.not_interested_outlined.fontFamily, - ), - ), - ), - Padding( - padding: _kTableRowPadding, - child: Text(context.l10n.maxDepthReached), - ), - const Padding( - padding: _kTableRowPadding, - child: SizedBox.shrink(), - ), - ], - ) - else if (moves.isNotEmpty) + if (moves.isNotEmpty) TableRow( decoration: BoxDecoration( color: moves.length.isEven diff --git a/test/view/opening_explorer/opening_explorer_screen_test.dart b/test/view/opening_explorer/opening_explorer_screen_test.dart index 5b224f369f..0ff43f2ff4 100644 --- a/test/view/opening_explorer/opening_explorer_screen_test.dart +++ b/test/view/opening_explorer/opening_explorer_screen_test.dart @@ -75,9 +75,6 @@ void main() { // wait for analysis controller to load expect(find.byType(CircularProgressIndicator), findsOneWidget); await tester.pump(const Duration(milliseconds: 10)); - // wait for connectivity provider to load - expect(find.byType(CircularProgressIndicator), findsOneWidget); - await tester.pump(const Duration(milliseconds: 10)); // wait for opening explorer data to load (taking debounce delay into account) await tester.pump(const Duration(milliseconds: 350)); @@ -135,9 +132,6 @@ void main() { // wait for analysis controller to load expect(find.byType(CircularProgressIndicator), findsOneWidget); await tester.pump(const Duration(milliseconds: 10)); - // wait for connectivity provider to load - expect(find.byType(CircularProgressIndicator), findsOneWidget); - await tester.pump(const Duration(milliseconds: 10)); // wait for opening explorer data to load (taking debounce delay into account) await tester.pump(const Duration(milliseconds: 350)); @@ -192,9 +186,6 @@ void main() { // wait for analysis controller to load expect(find.byType(CircularProgressIndicator), findsOneWidget); await tester.pump(const Duration(milliseconds: 10)); - // wait for connectivity provider to load - expect(find.byType(CircularProgressIndicator), findsOneWidget); - await tester.pump(const Duration(milliseconds: 10)); // wait for opening explorer data to load (taking debounce delay into account) await tester.pump(const Duration(milliseconds: 350)); From b6b2b9d742d02e6988ca9c710364adb47b867c09 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 3 Dec 2024 15:41:18 +0100 Subject: [PATCH 823/979] Fix wrong screen on iOS --- lib/src/view/settings/account_preferences_screen.dart | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/src/view/settings/account_preferences_screen.dart b/lib/src/view/settings/account_preferences_screen.dart index 5804722ba7..ac283ef583 100644 --- a/lib/src/view/settings/account_preferences_screen.dart +++ b/lib/src/view/settings/account_preferences_screen.dart @@ -426,7 +426,8 @@ class _AccountPreferencesScreenState pushPlatformRoute( context, title: context.l10n.letOtherPlayersChallengeYou, - builder: (context) => const AutoQueenSettingsScreen(), + builder: (context) => + const _ChallengeSettingsScreen(), ); } }, @@ -836,16 +837,16 @@ class _MoretimeSettingsScreenState } } -class ChallengeSettingsScreen extends ConsumerStatefulWidget { - const ChallengeSettingsScreen({super.key}); +class _ChallengeSettingsScreen extends ConsumerStatefulWidget { + const _ChallengeSettingsScreen(); @override - ConsumerState createState() => + ConsumerState<_ChallengeSettingsScreen> createState() => _ChallengeSettingsScreenState(); } class _ChallengeSettingsScreenState - extends ConsumerState { + extends ConsumerState<_ChallengeSettingsScreen> { Future? _pendingSetChallenge; @override From a3fe8c8a036ca45891db00ebd8dec4f2eb848786 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 3 Dec 2024 16:12:20 +0100 Subject: [PATCH 824/979] Upgrade dependencies --- ios/Podfile.lock | 42 +++++++++++++++++++++--------------------- pubspec.lock | 32 ++++++++++++++++---------------- 2 files changed, 37 insertions(+), 37 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index b4b0ae8e9c..1b78391803 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -233,15 +233,15 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/wakelock_plus/ios" SPEC CHECKSUMS: - app_settings: 017320c6a680cdc94c799949d95b84cb69389ebc + app_settings: 3507c575c2b18a462c99948f61d5de21d4420999 AppAuth: 501c04eda8a8d11f179dbe8637b7a91bb7e5d2fa - connectivity_plus: 4c41c08fc6d7c91f63bc7aec70ffe3730b04f563 - cupertino_http: 947a233f40cfea55167a49f2facc18434ea117ba - device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342 + connectivity_plus: b21496ab28d1324eb59885d888a4d83b98531f01 + cupertino_http: 94ac07f5ff090b8effa6c5e2c47871d48ab7c86c + device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe Firebase: cf1b19f21410b029b6786a54e9764a0cacad3c99 - firebase_core: 9efc3ecf689cdbc90f13f4dc58108c83ea46b266 - firebase_crashlytics: 72a8b504422ba8bb435a7a0c0a9341320cbcbe29 - firebase_messaging: 6bf60adb4b33a848d135e16bc363fb4924f98fba + firebase_core: 84a16d041be8bc166b6e00350f89849e06daf9d1 + firebase_crashlytics: 9fae6688e634a062ba57bd22d2b762b87fdcd1ec + firebase_messaging: 48ce5cf82b70ac47ed9cb9277f97bb77d1d01a38 FirebaseCore: e0510f1523bc0eb21653cac00792e1e2bd6f1771 FirebaseCoreExtension: f1bc67a4702931a7caa097d8e4ac0a1b0d16720e FirebaseCoreInternal: f47dd28ae7782e6a4738aad3106071a8fe0af604 @@ -251,25 +251,25 @@ SPEC CHECKSUMS: FirebaseRemoteConfigInterop: 7a7aebb9342d53913a5c890efa88e289d9e5c1bc FirebaseSessions: 3f56f177d9e53a85021d16b31f9a111849d1dd8b Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - flutter_appauth: 408f4cda69a4ad59bdf696e04cd9e13e1449b44e - flutter_local_notifications: df98d66e515e1ca797af436137b4459b160ad8c9 - flutter_native_splash: e8a1e01082d97a8099d973f919f57904c925008a - flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12 + flutter_appauth: a84b3dade5e072175e13b68af5b827ca76efb1f8 + flutter_local_notifications: 395056b3175ba4f08480a7c5de30cd36d69827e4 + flutter_native_splash: 576fbd69b830a63594ae678396fa17e43abbc5f8 + flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13 GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 - objective_c: 77e887b5ba1827970907e10e832eec1683f3431d - package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + objective_c: 89e720c30d716b036faf9c9684022048eee1eee2 + package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 - share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f - shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 - sound_effect: 5280cfa89d4a576032186f15600dc948ca6d39ce - sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d - stockfish: d00cf6b95579f1d7032cbfd8e4fe874972fe2ff9 - url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe - wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1 + share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + sound_effect: 7d4273d90e6c3357ca7951ea227c993723c20485 + sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 + stockfish: 3125c5e5fdd6789c398bb2d1a29a58ba3c9e5577 + url_launcher_ios: 694010445543906933d732453a59da0a173ae33d + wakelock_plus: fd58c82b1388f4afe3fe8aa2c856503a262a5b03 PODFILE CHECKSUM: 76a583f8d75b3a8c6e4bdc97ae8783ef36cc7984 diff --git a/pubspec.lock b/pubspec.lock index d9f3beebd2..2cd529f0b1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -612,10 +612,10 @@ packages: dependency: "direct main" description: name: flutter_markdown - sha256: f0e599ba89c9946c8e051780f0ec99aba4ba15895e0380a7ab68f420046fc44e + sha256: "255b00afa1a7bad19727da6a7780cf3db6c3c12e68d302d85e0ff1fdf173db9e" url: "https://pub.dev" source: hosted - version: "0.7.4+1" + version: "0.7.4+3" flutter_native_splash: dependency: "direct main" description: @@ -701,10 +701,10 @@ packages: dependency: "direct main" description: name: flutter_svg - sha256: "936d9c1c010d3e234d1672574636f3352b4941ca3decaddd3cafaeb9ad49c471" + sha256: "54900a1a1243f3c4a5506d853a2b5c2dbc38d5f27e52a52618a8054401431123" url: "https://pub.dev" source: hosted - version: "2.0.15" + version: "2.0.16" flutter_test: dependency: "direct dev" description: flutter @@ -1055,10 +1055,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: c464428172cb986b758c6d1724c603097febb8fb855aa265aeecc9280c294d4a + sha256: "8c4967f8b7cb46dc914e178daa29813d83ae502e0529d7b0478330616a691ef7" url: "https://pub.dev" source: hosted - version: "2.2.12" + version: "2.2.14" path_provider_foundation: dependency: transitive description: @@ -1239,10 +1239,10 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: "3b9febd815c9ca29c9e3520d50ec32f49157711e143b7a4ca039eb87e8ade5ab" + sha256: "7f172d1b06de5da47b6264c2692ee2ead20bbbc246690427cdb4fc301cd0c549" url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.3.4" shared_preferences_foundation: dependency: transitive description: @@ -1295,10 +1295,10 @@ packages: dependency: transitive description: name: shelf_web_socket - sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" + sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.0.1" signal_strength_indicator: dependency: "direct main" description: @@ -1404,10 +1404,10 @@ packages: dependency: transitive description: name: sqlite3 - sha256: bb174b3ec2527f9c5f680f73a89af8149dd99782fbb56ea88ad0807c5638f2ed + sha256: cb7f4e9dc1b52b1fa350f7b3d41c662e75fc3d399555fa4e5efcf267e9a4fbb5 url: "https://pub.dev" source: hosted - version: "2.4.7" + version: "2.5.0" stack_trace: dependency: transitive description: @@ -1613,10 +1613,10 @@ packages: dependency: transitive description: name: vector_graphics_compiler - sha256: ab9ff38fc771e9ee1139320adbe3d18a60327370c218c60752068ebee4b49ab1 + sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad" url: "https://pub.dev" source: hosted - version: "1.1.15" + version: "1.1.16" vector_math: dependency: transitive description: @@ -1693,10 +1693,10 @@ packages: dependency: transitive description: name: win32 - sha256: "84ba388638ed7a8cb3445a320c8273136ab2631cd5f2c57888335504ddab1bc2" + sha256: "8b338d4486ab3fbc0ba0db9f9b4f5239b6697fcee427939a40e720cbb9ee0a69" url: "https://pub.dev" source: hosted - version: "5.8.0" + version: "5.9.0" win32_registry: dependency: transitive description: From 8ed45bbbfd543a7bfa3522aeab75d7827d0341de Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 3 Dec 2024 16:45:48 +0100 Subject: [PATCH 825/979] WIP on fixing analysis layout board headers --- lib/src/view/analysis/analysis_layout.dart | 69 ++++- .../view/broadcast/broadcast_game_screen.dart | 278 ++++++------------ 2 files changed, 143 insertions(+), 204 deletions(-) diff --git a/lib/src/view/analysis/analysis_layout.dart b/lib/src/view/analysis/analysis_layout.dart index ee0b052142..28f831e526 100644 --- a/lib/src/view/analysis/analysis_layout.dart +++ b/lib/src/view/analysis/analysis_layout.dart @@ -8,6 +8,8 @@ import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; +const kAnalysisBoardHeaderHeight = 26.0; + typedef BoardBuilder = Widget Function( BuildContext context, double boardSize, @@ -128,6 +130,8 @@ class AnalysisLayout extends StatelessWidget { this.tabController, required this.boardBuilder, required this.children, + this.boardHeader, + this.boardFooter, this.engineGaugeBuilder, this.engineLines, this.bottomBar, @@ -140,14 +144,22 @@ class AnalysisLayout extends StatelessWidget { /// The builder for the board widget. final BoardBuilder boardBuilder; + final Widget? boardHeader; + final Widget? boardFooter; + /// The children of the tab view. /// /// The length of this list must match the [tabController]'s [TabController.length] /// and the length of the [AppBarAnalysisTabIndicator.tabs] list. final List children; + /// A builder for the engine gauge widget. final EngineGaugeBuilder? engineGaugeBuilder; + + /// A widget to show below the engine gauge, typically the engine lines. final Widget? engineLines; + + /// A widget to show at the bottom of the screen. final Widget? bottomBar; @override @@ -167,23 +179,42 @@ class AnalysisLayout extends StatelessWidget { BorderRadius.all(Radius.circular(4.0)); if (orientation == Orientation.landscape) { + final headerAndFooterHeight = (boardHeader != null + ? kAnalysisBoardHeaderHeight + : 0.0) + + (boardFooter != null ? kAnalysisBoardHeaderHeight : 0.0); final sideWidth = constraints.biggest.longestSide - constraints.biggest.shortestSide; final defaultBoardSize = constraints.biggest.shortestSide - (kTabletBoardTableSidePadding * 2); - final boardSize = sideWidth >= 250 - ? defaultBoardSize - : constraints.biggest.longestSide / kGoldenRatio - - (kTabletBoardTableSidePadding * 2); + final boardSize = (sideWidth >= 250 + ? defaultBoardSize + : constraints.biggest.longestSide / kGoldenRatio - + (kTabletBoardTableSidePadding * 2)) - + headerAndFooterHeight; return Padding( padding: const EdgeInsets.all(kTabletBoardTableSidePadding), child: Row( mainAxisSize: MainAxisSize.max, children: [ - boardBuilder( - context, - boardSize, - isTablet ? tabletBoardRadius : null, + Column( + children: [ + if (boardHeader != null) + SizedBox( + height: kAnalysisBoardHeaderHeight, + child: boardHeader, + ), + boardBuilder( + context, + boardSize, + isTablet ? tabletBoardRadius : null, + ), + if (boardFooter != null) + SizedBox( + height: kAnalysisBoardHeaderHeight, + child: boardFooter, + ), + ], ), if (engineGaugeBuilder != null) ...[ const SizedBox(width: 4.0), @@ -245,10 +276,24 @@ class AnalysisLayout extends StatelessWidget { kTabletBoardTableSidePadding, ) : EdgeInsets.zero, - child: boardBuilder( - context, - boardSize, - isTablet ? tabletBoardRadius : null, + child: Column( + children: [ + if (boardHeader != null) + SizedBox( + height: kAnalysisBoardHeaderHeight, + child: boardHeader, + ), + boardBuilder( + context, + boardSize, + isTablet ? tabletBoardRadius : null, + ), + if (boardFooter != null) + SizedBox( + height: kAnalysisBoardHeaderHeight, + child: boardFooter, + ), + ], ), ), Expanded( diff --git a/lib/src/view/broadcast/broadcast_game_screen.dart b/lib/src/view/broadcast/broadcast_game_screen.dart index b43b857813..19378a3d30 100644 --- a/lib/src/view/broadcast/broadcast_game_screen.dart +++ b/lib/src/view/broadcast/broadcast_game_screen.dart @@ -139,12 +139,23 @@ class _Body extends ConsumerWidget { return AnalysisLayout( tabController: tabController, - boardBuilder: (context, boardSize, borderRadius) => - _BroadcastBoardWithHeaders( + boardBuilder: (context, boardSize, borderRadius) => _BroadcastBoard( roundId, gameId, boardSize, - borderRadius, + borderRadius != null, + ), + boardHeader: _PlayerWidget( + roundId: roundId, + gameId: gameId, + widgetPosition: _PlayerWidgetPosition.top, + borderRadius: BorderRadius.zero, + ), + boardFooter: _PlayerWidget( + roundId: roundId, + gameId: gameId, + widgetPosition: _PlayerWidgetPosition.bottom, + borderRadius: BorderRadius.zero, ), engineGaugeBuilder: isLocalEvaluationEnabled && showEvaluationGauge ? (context, orientation) { @@ -206,49 +217,6 @@ class _OpeningExplorerTab extends ConsumerWidget { } } -class _BroadcastBoardWithHeaders extends ConsumerWidget { - final BroadcastRoundId roundId; - final BroadcastGameId gameId; - final double size; - final BorderRadius? borderRadius; - - const _BroadcastBoardWithHeaders( - this.roundId, - this.gameId, - this.size, - this.borderRadius, - ); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return Column( - children: [ - _PlayerWidget( - roundId: roundId, - gameId: gameId, - width: size, - widgetPosition: _PlayerWidgetPosition.top, - borderRadius: borderRadius?.copyWith( - bottomLeft: Radius.zero, - bottomRight: Radius.zero, - ), - ), - _BroadcastBoard(roundId, gameId, size, borderRadius != null), - _PlayerWidget( - roundId: roundId, - gameId: gameId, - width: size, - widgetPosition: _PlayerWidgetPosition.bottom, - borderRadius: borderRadius?.copyWith( - topLeft: Radius.zero, - topRight: Radius.zero, - ), - ), - ], - ); - } -} - class _BroadcastBoard extends ConsumerStatefulWidget { const _BroadcastBoard( this.roundId, @@ -366,14 +334,12 @@ class _PlayerWidget extends ConsumerWidget { const _PlayerWidget({ required this.roundId, required this.gameId, - required this.width, required this.widgetPosition, required this.borderRadius, }); final BroadcastRoundId roundId; final BroadcastGameId gameId; - final double width; final _PlayerWidgetPosition widgetPosition; final BorderRadius? borderRadius; @@ -399,137 +365,93 @@ class _PlayerWidget extends ConsumerWidget { final player = game.players[side]!; final gameStatus = game.status; - return SizedBox( - width: width, + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Row( children: [ - if (game.isOver) - Card( - margin: EdgeInsets.zero, - shape: _makePlayerWidgetBorder( - widgetPosition: widgetPosition, - borderRadius: borderRadius, - hasLeftBorderRadius: true, - ), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 8.0, - vertical: 4.0, - ), - child: Text( - (gameStatus == BroadcastResult.draw) - ? '½' - : (gameStatus == BroadcastResult.whiteWins) - ? side == Side.white - ? '1' - : '0' - : side == Side.black - ? '1' - : '0', - style: - const TextStyle().copyWith(fontWeight: FontWeight.bold), - ), - ), + if (game.isOver) ...[ + Text( + (gameStatus == BroadcastResult.draw) + ? '½' + : (gameStatus == BroadcastResult.whiteWins) + ? side == Side.white + ? '1' + : '0' + : side == Side.black + ? '1' + : '0', + style: const TextStyle().copyWith(fontWeight: FontWeight.bold), ), + const SizedBox(width: 5.0), + ], Expanded( - child: Card( - margin: EdgeInsets.zero, - shape: _makePlayerWidgetBorder( - widgetPosition: widgetPosition, - borderRadius: borderRadius, - hasLeftBorderRadius: !game.isOver, - hasRightBorderRadius: clock == null, - ), - child: Padding( - padding: - const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Row( - children: [ - if (player.federation != null) ...[ - Consumer( - builder: (context, widgetRef, _) { - return SvgPicture.network( - lichessFideFedSrc(player.federation!), - height: 12, - httpClient: - widgetRef.read(defaultClientProvider), - ); - }, - ), - const SizedBox(width: 5), - ], - if (player.title != null) ...[ - Text( - player.title!, - style: const TextStyle().copyWith( - color: context.lichessColors.brag, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(width: 5), - ], - Text( - player.name, - style: const TextStyle().copyWith( - fontWeight: FontWeight.bold, - ), - overflow: TextOverflow.ellipsis, - ), - if (player.rating != null) ...[ - const SizedBox(width: 5), - Text( - player.rating.toString(), - style: const TextStyle(), - overflow: TextOverflow.ellipsis, - ), - ], - ], - ), + child: Row( + children: [ + if (player.federation != null) ...[ + Consumer( + builder: (context, widgetRef, _) { + return SvgPicture.network( + lichessFideFedSrc(player.federation!), + height: 12, + httpClient: widgetRef.read(defaultClientProvider), + ); + }, + ), + const SizedBox(width: 5), + ], + if (player.title != null) ...[ + Text( + player.title!, + style: const TextStyle().copyWith( + color: context.lichessColors.brag, + fontWeight: FontWeight.bold, ), - ], + ), + const SizedBox(width: 5), + ], + Text( + player.name, + style: const TextStyle().copyWith( + fontWeight: FontWeight.bold, + ), + overflow: TextOverflow.ellipsis, ), - ), + if (player.rating != null) ...[ + const SizedBox(width: 5), + Text( + player.rating.toString(), + style: const TextStyle(), + overflow: TextOverflow.ellipsis, + ), + ], + ], ), ), if (clock != null) - Card( + ColoredBox( color: (side == sideToMove) ? isCursorOnLiveMove ? Theme.of(context).colorScheme.tertiaryContainer : Theme.of(context).colorScheme.secondaryContainer - : null, - margin: EdgeInsets.zero, - shape: _makePlayerWidgetBorder( - widgetPosition: widgetPosition, - borderRadius: borderRadius, - hasRightBorderRadius: true, - ), - child: Padding( - padding: - const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), - child: isCursorOnLiveMove - ? CountdownClockBuilder( - timeLeft: clock, - active: side == sideToMove, - builder: (context, timeLeft) => _Clock( - timeLeft: timeLeft, - isSideToMove: side == sideToMove, - isLive: true, - ), - tickInterval: const Duration(seconds: 1), - clockUpdatedAt: - side == sideToMove ? game.updatedClockAt : null, - ) - : _Clock( - timeLeft: clock, + : Colors.transparent, + child: isCursorOnLiveMove + ? CountdownClockBuilder( + timeLeft: clock, + active: side == sideToMove, + builder: (context, timeLeft) => _Clock( + timeLeft: timeLeft, isSideToMove: side == sideToMove, - isLive: false, + isLive: true, ), - ), + tickInterval: const Duration(seconds: 1), + clockUpdatedAt: + side == sideToMove ? game.updatedClockAt : null, + ) + : _Clock( + timeLeft: clock, + isSideToMove: side == sideToMove, + isLive: false, + ), ), ], ), @@ -563,31 +485,3 @@ class _Clock extends StatelessWidget { ); } } - -ShapeBorder _makePlayerWidgetBorder({ - required BorderRadius? borderRadius, - required _PlayerWidgetPosition widgetPosition, - bool hasLeftBorderRadius = false, - bool hasRightBorderRadius = false, -}) { - if (borderRadius == null) return const Border(); - - if (!hasLeftBorderRadius && !hasRightBorderRadius) return const Border(); - - return RoundedRectangleBorder( - borderRadius: switch (widgetPosition) { - _PlayerWidgetPosition.top => borderRadius.copyWith( - topLeft: hasLeftBorderRadius ? null : Radius.zero, - topRight: hasRightBorderRadius ? null : Radius.zero, - bottomLeft: Radius.zero, - bottomRight: Radius.zero, - ), - _PlayerWidgetPosition.bottom => borderRadius.copyWith( - topLeft: Radius.zero, - topRight: Radius.zero, - bottomLeft: hasLeftBorderRadius ? null : Radius.zero, - bottomRight: hasRightBorderRadius ? null : Radius.zero, - ), - }, - ); -} From 3c940f2a685d79fe47e33babdd1bc673c64528b9 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 3 Dec 2024 18:49:53 +0100 Subject: [PATCH 826/979] Fix board headers on broadcast game screen --- lib/src/view/analysis/analysis_layout.dart | 83 +++++++++-- .../view/broadcast/broadcast_game_screen.dart | 141 +++++++++--------- 2 files changed, 139 insertions(+), 85 deletions(-) diff --git a/lib/src/view/analysis/analysis_layout.dart b/lib/src/view/analysis/analysis_layout.dart index 28f831e526..8acd52fe51 100644 --- a/lib/src/view/analysis/analysis_layout.dart +++ b/lib/src/view/analysis/analysis_layout.dart @@ -8,7 +8,8 @@ import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; -const kAnalysisBoardHeaderHeight = 26.0; +/// The height of the board header or footer in the analysis layout. +const kAnalysisBoardHeaderOrFooterHeight = 26.0; typedef BoardBuilder = Widget Function( BuildContext context, @@ -180,9 +181,11 @@ class AnalysisLayout extends StatelessWidget { if (orientation == Orientation.landscape) { final headerAndFooterHeight = (boardHeader != null - ? kAnalysisBoardHeaderHeight + ? kAnalysisBoardHeaderOrFooterHeight : 0.0) + - (boardFooter != null ? kAnalysisBoardHeaderHeight : 0.0); + (boardFooter != null + ? kAnalysisBoardHeaderOrFooterHeight + : 0.0); final sideWidth = constraints.biggest.longestSide - constraints.biggest.shortestSide; final defaultBoardSize = constraints.biggest.shortestSide - @@ -200,18 +203,46 @@ class AnalysisLayout extends StatelessWidget { Column( children: [ if (boardHeader != null) - SizedBox( - height: kAnalysisBoardHeaderHeight, - child: boardHeader, + Container( + decoration: BoxDecoration( + borderRadius: isTablet + ? tabletBoardRadius.copyWith( + bottomLeft: Radius.zero, + bottomRight: Radius.zero, + ) + : null, + ), + clipBehavior: + isTablet ? Clip.hardEdge : Clip.none, + child: SizedBox( + height: kAnalysisBoardHeaderOrFooterHeight, + width: boardSize, + child: boardHeader, + ), ), boardBuilder( context, boardSize, - isTablet ? tabletBoardRadius : null, + isTablet && + boardHeader == null && + boardFooter != null + ? tabletBoardRadius + : null, ), if (boardFooter != null) - SizedBox( - height: kAnalysisBoardHeaderHeight, + Container( + decoration: BoxDecoration( + borderRadius: isTablet + ? tabletBoardRadius.copyWith( + topLeft: Radius.zero, + topRight: Radius.zero, + ) + : null, + ), + clipBehavior: + isTablet ? Clip.hardEdge : Clip.none, + height: kAnalysisBoardHeaderOrFooterHeight, + width: boardSize, child: boardFooter, ), ], @@ -279,18 +310,42 @@ class AnalysisLayout extends StatelessWidget { child: Column( children: [ if (boardHeader != null) - SizedBox( - height: kAnalysisBoardHeaderHeight, + Container( + decoration: BoxDecoration( + borderRadius: isTablet + ? tabletBoardRadius.copyWith( + bottomLeft: Radius.zero, + bottomRight: Radius.zero, + ) + : null, + ), + clipBehavior: + isTablet ? Clip.hardEdge : Clip.none, + height: kAnalysisBoardHeaderOrFooterHeight, child: boardHeader, ), boardBuilder( context, boardSize, - isTablet ? tabletBoardRadius : null, + isTablet && + boardHeader == null && + boardFooter != null + ? tabletBoardRadius + : null, ), if (boardFooter != null) - SizedBox( - height: kAnalysisBoardHeaderHeight, + Container( + decoration: BoxDecoration( + borderRadius: isTablet + ? tabletBoardRadius.copyWith( + topLeft: Radius.zero, + topRight: Radius.zero, + ) + : null, + ), + clipBehavior: + isTablet ? Clip.hardEdge : Clip.none, + height: kAnalysisBoardHeaderOrFooterHeight, child: boardFooter, ), ], diff --git a/lib/src/view/broadcast/broadcast_game_screen.dart b/lib/src/view/broadcast/broadcast_game_screen.dart index 19378a3d30..ceda09ccb9 100644 --- a/lib/src/view/broadcast/broadcast_game_screen.dart +++ b/lib/src/view/broadcast/broadcast_game_screen.dart @@ -143,19 +143,17 @@ class _Body extends ConsumerWidget { roundId, gameId, boardSize, - borderRadius != null, + borderRadius, ), boardHeader: _PlayerWidget( roundId: roundId, gameId: gameId, widgetPosition: _PlayerWidgetPosition.top, - borderRadius: BorderRadius.zero, ), boardFooter: _PlayerWidget( roundId: roundId, gameId: gameId, widgetPosition: _PlayerWidgetPosition.bottom, - borderRadius: BorderRadius.zero, ), engineGaugeBuilder: isLocalEvaluationEnabled && showEvaluationGauge ? (context, orientation) { @@ -222,13 +220,13 @@ class _BroadcastBoard extends ConsumerStatefulWidget { this.roundId, this.gameId, this.boardSize, - this.hasShadow, + this.borderRadius, ); final BroadcastRoundId roundId; final BroadcastGameId gameId; final double boardSize; - final bool hasShadow; + final BorderRadiusGeometry? borderRadius; @override ConsumerState<_BroadcastBoard> createState() => _BroadcastBoardState(); @@ -297,7 +295,10 @@ class _BroadcastBoardState extends ConsumerState<_BroadcastBoard> { : IMap({sanMove.move.to: annotation}) : null, settings: boardPrefs.toBoardSettings().copyWith( - boxShadow: widget.hasShadow ? boardShadows : const [], + borderRadius: widget.borderRadius, + boxShadow: widget.borderRadius != null + ? boardShadows + : const [], drawShape: DrawShapeOptions( enable: boardPrefs.enableShapeDrawings, onCompleteShape: _onCompleteShape, @@ -335,13 +336,11 @@ class _PlayerWidget extends ConsumerWidget { required this.roundId, required this.gameId, required this.widgetPosition, - required this.borderRadius, }); final BroadcastRoundId roundId; final BroadcastGameId gameId; final _PlayerWidgetPosition widgetPosition; - final BorderRadius? borderRadius; @override Widget build(BuildContext context, WidgetRef ref) { @@ -365,8 +364,11 @@ class _PlayerWidget extends ConsumerWidget { final player = game.players[side]!; final gameStatus = game.status; - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), + return Container( + color: Theme.of(context).platform == TargetPlatform.iOS + ? Styles.cupertinoCardColor.resolveFrom(context) + : Theme.of(context).colorScheme.surfaceContainer, + padding: const EdgeInsets.only(left: 8.0), child: Row( children: [ if (game.isOver) ...[ @@ -382,76 +384,73 @@ class _PlayerWidget extends ConsumerWidget { : '0', style: const TextStyle().copyWith(fontWeight: FontWeight.bold), ), - const SizedBox(width: 5.0), + const SizedBox(width: 16.0), ], - Expanded( - child: Row( - children: [ - if (player.federation != null) ...[ - Consumer( - builder: (context, widgetRef, _) { - return SvgPicture.network( - lichessFideFedSrc(player.federation!), - height: 12, - httpClient: widgetRef.read(defaultClientProvider), - ); - }, - ), - const SizedBox(width: 5), - ], - if (player.title != null) ...[ - Text( - player.title!, - style: const TextStyle().copyWith( - color: context.lichessColors.brag, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(width: 5), - ], - Text( - player.name, - style: const TextStyle().copyWith( - fontWeight: FontWeight.bold, - ), - overflow: TextOverflow.ellipsis, - ), - if (player.rating != null) ...[ - const SizedBox(width: 5), - Text( - player.rating.toString(), - style: const TextStyle(), - overflow: TextOverflow.ellipsis, - ), - ], - ], + if (player.federation != null) ...[ + SvgPicture.network( + lichessFideFedSrc(player.federation!), + height: 12, + httpClient: ref.read(defaultClientProvider), + ), + const SizedBox(width: 5), + ], + if (player.title != null) ...[ + Text( + player.title!, + style: const TextStyle().copyWith( + color: context.lichessColors.brag, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(width: 5), + ], + Text( + player.name, + style: const TextStyle().copyWith( + fontWeight: FontWeight.bold, ), + overflow: TextOverflow.ellipsis, ), + if (player.rating != null) ...[ + const SizedBox(width: 5), + Text( + player.rating.toString(), + style: const TextStyle(), + overflow: TextOverflow.ellipsis, + ), + ], + const Spacer(), if (clock != null) - ColoredBox( + Container( + height: kAnalysisBoardHeaderOrFooterHeight, color: (side == sideToMove) ? isCursorOnLiveMove ? Theme.of(context).colorScheme.tertiaryContainer : Theme.of(context).colorScheme.secondaryContainer : Colors.transparent, - child: isCursorOnLiveMove - ? CountdownClockBuilder( - timeLeft: clock, - active: side == sideToMove, - builder: (context, timeLeft) => _Clock( - timeLeft: timeLeft, - isSideToMove: side == sideToMove, - isLive: true, - ), - tickInterval: const Duration(seconds: 1), - clockUpdatedAt: - side == sideToMove ? game.updatedClockAt : null, - ) - : _Clock( - timeLeft: clock, - isSideToMove: side == sideToMove, - isLive: false, - ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 6.0), + child: Center( + child: isCursorOnLiveMove + ? CountdownClockBuilder( + timeLeft: clock, + active: side == sideToMove, + builder: (context, timeLeft) => _Clock( + timeLeft: timeLeft, + isSideToMove: side == sideToMove, + isLive: true, + ), + tickInterval: const Duration(seconds: 1), + clockUpdatedAt: + side == sideToMove ? game.updatedClockAt : null, + ) + : _Clock( + timeLeft: clock, + isSideToMove: side == sideToMove, + isLive: false, + ), + ), + ), ), ], ), From 087b096bf916709b581bca92ba1e28b5aa8522a9 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 3 Dec 2024 21:00:06 +0100 Subject: [PATCH 827/979] Optimize broadcast clocks --- .../broadcast/broadcast_game_controller.dart | 16 ++++++++-------- lib/src/model/common/node.dart | 7 +++++++ lib/src/utils/duration.dart | 2 +- lib/src/view/analysis/analysis_layout.dart | 9 +++++++++ 4 files changed, 25 insertions(+), 9 deletions(-) diff --git a/lib/src/model/broadcast/broadcast_game_controller.dart b/lib/src/model/broadcast/broadcast_game_controller.dart index fda93e621d..22e246c50f 100644 --- a/lib/src/model/broadcast/broadcast_game_controller.dart +++ b/lib/src/model/broadcast/broadcast_game_controller.dart @@ -90,7 +90,7 @@ class BroadcastGameController extends _$BroadcastGameController lastMove: lastMove, pov: Side.white, isLocalEvaluationEnabled: prefs.enableLocalEvaluation, - clocks: _makeClocks(currentPath), + clocks: _getClocks(currentPath), ); if (broadcastState.isLocalEvaluationEnabled) { @@ -470,7 +470,7 @@ class BroadcastGameController extends _$BroadcastGameController lastMove: currentNode.sanMove.move, promotionMove: null, root: rootView, - clocks: _makeClocks(path), + clocks: _getClocks(path), ), ); } else { @@ -483,7 +483,7 @@ class BroadcastGameController extends _$BroadcastGameController lastMove: null, promotionMove: null, root: rootView, - clocks: _makeClocks(path), + clocks: _getClocks(path), ), ); } @@ -530,13 +530,13 @@ class BroadcastGameController extends _$BroadcastGameController ); } - ({Duration? parentClock, Duration? clock}) _makeClocks(UciPath path) { - final nodeView = _root.nodeAt(path).view; - final parentView = _root.parentAt(path).view; + ({Duration? parentClock, Duration? clock}) _getClocks(UciPath path) { + final node = _root.nodeAt(path); + final parent = _root.parentAt(path); return ( - parentClock: (parentView is ViewBranch) ? parentView.clock : null, - clock: (nodeView is ViewBranch) ? nodeView.clock : null, + parentClock: (parent is Branch) ? parent.clock : null, + clock: (node is Branch) ? node.clock : null, ); } } diff --git a/lib/src/model/common/node.dart b/lib/src/model/common/node.dart index ce5dbb0fd1..b6dbb82403 100644 --- a/lib/src/model/common/node.dart +++ b/lib/src/model/common/node.dart @@ -419,6 +419,13 @@ class Branch extends Node { @override Branch branchAt(UciPath path) => nodeAt(path) as Branch; + /// Gets the clock information from the comments. + Duration? get clock { + final clockComment = (lichessAnalysisComments ?? comments) + ?.firstWhereOrNull((c) => c.clock != null); + return clockComment?.clock; + } + @override String toString() { return 'Branch(id: $id, fen: ${position.fen}, sanMove: $sanMove, eval: $eval, children: $children)'; diff --git a/lib/src/utils/duration.dart b/lib/src/utils/duration.dart index eb799f6840..0cdbf34af0 100644 --- a/lib/src/utils/duration.dart +++ b/lib/src/utils/duration.dart @@ -7,7 +7,7 @@ extension DurationExtensions on Duration { /// representation is H:MM:SS.mm, otherwise it is H:MM:SS. String toHoursMinutesSeconds({bool showTenths = false}) { if (inHours == 0) { - return '${inMinutes.remainder(60)}:${inSeconds.remainder(60).toString().padLeft(2, '0')}${showTenths ? '.${inMilliseconds.remainder(1000) ~/ 10 % 10}' : ''}'; + return '${inMinutes.remainder(60).toString().padLeft(2, '0')}:${inSeconds.remainder(60).toString().padLeft(2, '0')}${showTenths ? '.${inMilliseconds.remainder(1000) ~/ 10 % 10}' : ''}'; } else { return '$inHours:${inMinutes.remainder(60).toString().padLeft(2, '0')}:${inSeconds.remainder(60).toString().padLeft(2, '0')}'; } diff --git a/lib/src/view/analysis/analysis_layout.dart b/lib/src/view/analysis/analysis_layout.dart index 8acd52fe51..40b9edf93c 100644 --- a/lib/src/view/analysis/analysis_layout.dart +++ b/lib/src/view/analysis/analysis_layout.dart @@ -145,7 +145,16 @@ class AnalysisLayout extends StatelessWidget { /// The builder for the board widget. final BoardBuilder boardBuilder; + /// A widget to show above the board. + /// + /// The widget will included in a parent container with a height of + /// [kAnalysisBoardHeaderOrFooterHeight]. final Widget? boardHeader; + + /// A widget to show below the board. + /// + /// The widget will included in a parent container with a height of + /// [kAnalysisBoardHeaderOrFooterHeight]. final Widget? boardFooter; /// The children of the tab view. From 2ced0ee706615ea914b016666a5a256a9195c677 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 3 Dec 2024 21:10:44 +0100 Subject: [PATCH 828/979] Fix game title overflow --- lib/src/view/analysis/analysis_screen.dart | 8 ++++++- lib/src/view/game/game_common_widgets.dart | 25 +++++++++++++++++----- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index 4b7065c8cf..00ada5c5c3 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -1,3 +1,4 @@ +import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -149,7 +150,12 @@ class _Title extends StatelessWidget { const SizedBox(width: 5.0), ], Flexible( - child: Text(context.l10n.analysis, overflow: TextOverflow.ellipsis), + child: AutoSizeText( + context.l10n.analysis, + minFontSize: 14, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), ), ], ); diff --git a/lib/src/view/game/game_common_widgets.dart b/lib/src/view/game/game_common_widgets.dart index 94199f99cf..2dd3fc69a6 100644 --- a/lib/src/view/game/game_common_widgets.dart +++ b/lib/src/view/game/game_common_widgets.dart @@ -1,3 +1,4 @@ +import 'package:auto_size_text/auto_size_text.dart'; import 'package:dartchess/dartchess.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -387,15 +388,29 @@ class _StandaloneGameTitle extends ConsumerWidget { ), const SizedBox(width: 4.0), if (meta.clock != null) - Text( - '${TimeIncrement(meta.clock!.initial.inSeconds, meta.clock!.increment.inSeconds).display}$info', + Expanded( + child: AutoSizeText( + '${TimeIncrement(meta.clock!.initial.inSeconds, meta.clock!.increment.inSeconds).display}$info', + maxLines: 1, + minFontSize: 14.0, + ), ) else if (meta.daysPerTurn != null) - Text( - '${context.l10n.nbDays(meta.daysPerTurn!)}$info', + Expanded( + child: AutoSizeText( + '${context.l10n.nbDays(meta.daysPerTurn!)}$info', + maxLines: 1, + minFontSize: 14.0, + ), ) else - Text('${meta.perf.title}$info'), + Expanded( + child: AutoSizeText( + '${meta.perf.title}$info', + maxLines: 1, + minFontSize: 14.0, + ), + ), ], ); }, From 64f9a96ce4582ad5cc834901998c4ec700d28821 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 3 Dec 2024 22:42:43 +0100 Subject: [PATCH 829/979] Add study share menu & improve study menus --- lib/src/model/game/game_share_service.dart | 5 +- lib/src/model/study/study_list_paginator.dart | 24 +- lib/src/model/study/study_repository.dart | 42 +++- lib/src/view/analysis/analysis_screen.dart | 1 - lib/src/view/game/game_common_widgets.dart | 1 - lib/src/view/game/game_list_tile.dart | 1 - lib/src/view/study/study_bottom_bar.dart | 91 +++++++ lib/src/view/study/study_screen.dart | 231 +++++++++++------- lib/src/widgets/bottom_bar_button.dart | 2 + test/model/study/study_repository_test.dart | 16 +- 10 files changed, 301 insertions(+), 113 deletions(-) diff --git a/lib/src/model/game/game_share_service.dart b/lib/src/model/game/game_share_service.dart index 60f7ceb5bb..96cc557f85 100644 --- a/lib/src/model/game/game_share_service.dart +++ b/lib/src/model/game/game_share_service.dart @@ -57,9 +57,8 @@ class GameShareService { return utf8.decode(resp.bodyBytes); } - /// Fetches the GIF screenshot of a game and launches the share dialog. + /// Fetches the GIF screenshot of a position and launches the share dialog. Future screenshotPosition( - GameId id, Side orientation, String fen, Move? lastMove, @@ -80,7 +79,7 @@ class GameShareService { return XFile.fromData(resp.bodyBytes, mimeType: 'image/gif'); } - /// Fetches the GIF animation of a game and launches the share dialog. + /// Fetches the GIF animation of a game. Future gameGif(GameId id, Side orientation) async { final boardTheme = _ref.read(boardPreferencesProvider).boardTheme; final pieceTheme = _ref.read(boardPreferencesProvider).pieceSet; diff --git a/lib/src/model/study/study_list_paginator.dart b/lib/src/model/study/study_list_paginator.dart index 5d22690a38..4684a3ecd0 100644 --- a/lib/src/model/study/study_list_paginator.dart +++ b/lib/src/model/study/study_list_paginator.dart @@ -2,7 +2,6 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:lichess_mobile/src/model/study/study.dart'; import 'package:lichess_mobile/src/model/study/study_filter.dart'; import 'package:lichess_mobile/src/model/study/study_repository.dart'; -import 'package:lichess_mobile/src/network/http.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'study_list_paginator.g.dart'; @@ -37,17 +36,16 @@ class StudyListPaginator extends _$StudyListPaginator { Future _nextPage() async { final nextPage = state.value?.nextPage ?? 1; - return await ref.withClient( - (client) => search == null - ? StudyRepository(client).getStudies( - category: filter.category, - order: filter.order, - page: nextPage, - ) - : StudyRepository(client).searchStudies( - query: search!, - page: nextPage, - ), - ); + final repo = ref.read(studyRepositoryProvider); + return search == null + ? repo.getStudies( + category: filter.category, + order: filter.order, + page: nextPage, + ) + : repo.searchStudies( + query: search!, + page: nextPage, + ); } } diff --git a/lib/src/model/study/study_repository.dart b/lib/src/model/study/study_repository.dart index afd5f4083f..71d8babe8a 100644 --- a/lib/src/model/study/study_repository.dart +++ b/lib/src/model/study/study_repository.dart @@ -1,27 +1,31 @@ import 'dart:convert'; +import 'package:dartchess/dartchess.dart'; import 'package:deep_pick/deep_pick.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:http/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/model/study/study.dart'; import 'package:lichess_mobile/src/model/study/study_filter.dart'; import 'package:lichess_mobile/src/model/study/study_list_paginator.dart'; import 'package:lichess_mobile/src/network/http.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:share_plus/share_plus.dart'; part 'study_repository.g.dart'; @Riverpod(keepAlive: true) StudyRepository studyRepository(Ref ref) { - return StudyRepository(ref.read(lichessClientProvider)); + return StudyRepository(ref, ref.read(lichessClientProvider)); } class StudyRepository { - StudyRepository(this.client); + StudyRepository(this.ref, this.client); final Client client; + final Ref ref; Future getStudies({ required StudyCategory category, @@ -92,4 +96,38 @@ class StudyRepository { return (study, utf8.decode(pgnBytes)); } + + Future getStudyPgn(StudyId id) async { + final pgnBytes = await client.readBytes( + Uri(path: '/api/study/$id.pgn'), + headers: {'Accept': 'application/x-chess-pgn'}, + ); + + return utf8.decode(pgnBytes); + } + + /// Fetches the GIF animation of a study chapter. + Future chapterGif( + StudyId id, + StudyChapterId chapterId, + Side orientation, + ) async { + final boardTheme = ref.read(boardPreferencesProvider).boardTheme; + final pieceTheme = ref.read(boardPreferencesProvider).pieceSet; + final resp = await client + .get( + lichessUri( + '/study/$id/$chapterId.gif', + { + 'theme': boardTheme.name, + 'piece': pieceTheme.name, + }, + ), + ) + .timeout(const Duration(seconds: 1)); + if (resp.statusCode != 200) { + throw Exception('Failed to get GIF'); + } + return XFile.fromData(resp.bodyBytes, mimeType: 'image/gif'); + } } diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index 00ada5c5c3..ebea67cd71 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -348,7 +348,6 @@ class _BottomBar extends ConsumerWidget { try { final image = await ref.read(gameShareServiceProvider).screenshotPosition( - gameId, analysisState.pov, analysisState.position.fen, analysisState.lastMove, diff --git a/lib/src/view/game/game_common_widgets.dart b/lib/src/view/game/game_common_widgets.dart index 2dd3fc69a6..b5aff24c69 100644 --- a/lib/src/view/game/game_common_widgets.dart +++ b/lib/src/view/game/game_common_widgets.dart @@ -202,7 +202,6 @@ class GameShareBottomSheet extends ConsumerWidget { final image = await ref .read(gameShareServiceProvider) .screenshotPosition( - game.id, orientation, currentGamePosition.fen, lastMove, diff --git a/lib/src/view/game/game_list_tile.dart b/lib/src/view/game/game_list_tile.dart index 3901f67c2b..d0f9d2de20 100644 --- a/lib/src/view/game/game_list_tile.dart +++ b/lib/src/view/game/game_list_tile.dart @@ -308,7 +308,6 @@ class _ContextMenu extends ConsumerWidget { final image = await ref .read(gameShareServiceProvider) .screenshotPosition( - game.id, orientation, game.lastFen!, game.lastMove, diff --git a/lib/src/view/study/study_bottom_bar.dart b/lib/src/view/study/study_bottom_bar.dart index f1eb375764..22d89526cf 100644 --- a/lib/src/view/study/study_bottom_bar.dart +++ b/lib/src/view/study/study_bottom_bar.dart @@ -7,9 +7,11 @@ import 'package:lichess_mobile/src/model/study/study_controller.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_screen.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/list.dart'; class StudyBottomBar extends ConsumerWidget { const StudyBottomBar({ @@ -53,6 +55,7 @@ class _AnalysisBottomBar extends ConsumerWidget { return BottomBar( children: [ + _ChapterButton(state: state), _NextChapterButton( id: id, chapterId: state.study.chapter.id, @@ -101,6 +104,7 @@ class _GamebookBottomBar extends ConsumerWidget { return BottomBar( children: [ + _ChapterButton(state: state), ...switch (state.gamebookState) { GamebookState.findTheMove => [ if (!state.currentNode.isRoot) @@ -228,3 +232,90 @@ class _NextChapterButtonState extends ConsumerState<_NextChapterButton> { ); } } + +class _ChapterButton extends ConsumerWidget { + const _ChapterButton({required this.state}); + + final StudyState state; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return BottomBarButton( + onTap: () => showAdaptiveBottomSheet( + context: context, + showDragHandle: true, + isScrollControlled: true, + isDismissible: true, + builder: (_) => DraggableScrollableSheet( + initialChildSize: 0.5, + maxChildSize: 0.95, + snap: true, + expand: false, + builder: (context, scrollController) { + return _StudyChaptersMenu( + id: state.study.id, + scrollController: scrollController, + ); + }, + ), + ), + label: context.l10n.studyNbChapters(state.study.chapters.length), + showLabel: true, + icon: Icons.menu_book, + ); + } +} + +class _StudyChaptersMenu extends ConsumerWidget { + const _StudyChaptersMenu({ + required this.id, + required this.scrollController, + }); + + final StudyId id; + final ScrollController scrollController; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final state = ref.watch(studyControllerProvider(id)).requireValue; + + final currentChapterKey = GlobalKey(); + + // Scroll to the current chapter + WidgetsBinding.instance.addPostFrameCallback((_) { + if (currentChapterKey.currentContext != null) { + Scrollable.ensureVisible( + currentChapterKey.currentContext!, + alignment: 0.5, + ); + } + }); + + return BottomSheetScrollableContainer( + scrollController: scrollController, + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + context.l10n.studyNbChapters(state.study.chapters.length), + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + ), + for (final chapter in state.study.chapters) + PlatformListTile( + key: chapter.id == state.currentChapter.id + ? currentChapterKey + : null, + title: Text(chapter.name, maxLines: 2), + onTap: () { + ref.read(studyControllerProvider(id).notifier).goToChapter( + chapter.id, + ); + Navigator.of(context).pop(); + }, + selected: chapter.id == state.currentChapter.id, + ), + ], + ); + } +} diff --git a/lib/src/view/study/study_screen.dart b/lib/src/view/study/study_screen.dart index e085daca9a..b02cfbbe77 100644 --- a/lib/src/view/study/study_screen.dart +++ b/lib/src/view/study/study_screen.dart @@ -3,6 +3,7 @@ import 'package:chessground/chessground.dart'; import 'package:collection/collection.dart'; import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/constants.dart'; @@ -11,11 +12,15 @@ import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/eval.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; +import 'package:lichess_mobile/src/model/game/game_share_service.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/model/study/study_controller.dart'; import 'package:lichess_mobile/src/model/study/study_preferences.dart'; +import 'package:lichess_mobile/src/model/study/study_repository.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/utils/share.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_layout.dart'; import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; import 'package:lichess_mobile/src/view/engine/engine_lines.dart'; @@ -24,9 +29,9 @@ import 'package:lichess_mobile/src/view/study/study_bottom_bar.dart'; import 'package:lichess_mobile/src/view/study/study_gamebook.dart'; import 'package:lichess_mobile/src/view/study/study_settings.dart'; import 'package:lichess_mobile/src/view/study/study_tree_view.dart'; -import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; -import 'package:lichess_mobile/src/widgets/list.dart'; +import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/pgn.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:logging/logging.dart'; @@ -143,7 +148,6 @@ class _StudyScreenState extends ConsumerState<_StudyScreen> tabs: tabs, controller: _tabController, ), - _ChapterButton(id: widget.id), _StudyMenu(id: widget.id), ], ), @@ -184,96 +188,141 @@ class _StudyMenu extends ConsumerWidget { ref.read(studyControllerProvider(id).notifier).toggleLike(); }, ), - ], - ); - } -} - -class _ChapterButton extends ConsumerWidget { - const _ChapterButton({required this.id}); - - final StudyId id; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final state = ref.watch(studyControllerProvider(id)).valueOrNull; - return state == null - ? const SizedBox.shrink() - : AppBarIconButton( - onPressed: () => showAdaptiveBottomSheet( + AppBarMenuAction( + icon: Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoIcons.share + : Icons.share, + label: context.l10n.studyShareAndExport, + onPressed: () { + showAdaptiveActionSheet( context: context, - showDragHandle: true, - isScrollControlled: true, - isDismissible: true, - builder: (_) => DraggableScrollableSheet( - initialChildSize: 0.5, - maxChildSize: 0.95, - snap: true, - expand: false, - builder: (context, scrollController) { - return _StudyChaptersMenu( - id: id, - scrollController: scrollController, - ); - }, - ), - ), - semanticsLabel: - context.l10n.studyNbChapters(state.study.chapters.length), - icon: const Icon(Icons.menu_book), - ); - } -} - -class _StudyChaptersMenu extends ConsumerWidget { - const _StudyChaptersMenu({ - required this.id, - required this.scrollController, - }); - - final StudyId id; - final ScrollController scrollController; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final state = ref.watch(studyControllerProvider(id)).requireValue; - - final currentChapterKey = GlobalKey(); - - // Scroll to the current chapter - WidgetsBinding.instance.addPostFrameCallback((_) { - if (currentChapterKey.currentContext != null) { - Scrollable.ensureVisible( - currentChapterKey.currentContext!, - alignment: 0.5, - ); - } - }); - - return BottomSheetScrollableContainer( - scrollController: scrollController, - children: [ - Padding( - padding: const EdgeInsets.all(16.0), - child: Text( - context.l10n.studyNbChapters(state.study.chapters.length), - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), + actions: [ + BottomSheetAction( + makeLabel: (context) => Text(context.l10n.studyStudyUrl), + onPressed: (context) async { + launchShareDialog( + context, + uri: lichessUri('/study/${state.study.id}'), + ); + }, + ), + BottomSheetAction( + makeLabel: (context) => + Text(context.l10n.studyCurrentChapterUrl), + onPressed: (context) async { + launchShareDialog( + context, + uri: lichessUri( + '/study/${state.study.id}/${state.study.chapter.id}', + ), + ); + }, + ), + if (!state.gamebookActive) ...[ + BottomSheetAction( + makeLabel: (context) => Text(context.l10n.studyStudyPgn), + onPressed: (context) async { + try { + final pgn = + await ref.read(studyRepositoryProvider).getStudyPgn( + state.study.id, + ); + if (context.mounted) { + launchShareDialog( + context, + text: pgn, + ); + } + } catch (e) { + if (context.mounted) { + showPlatformSnackbar( + context, + 'Failed to get PGN', + type: SnackBarType.error, + ); + } + } + }, + ), + BottomSheetAction( + makeLabel: (context) => Text(context.l10n.studyChapterPgn), + onPressed: (context) async { + launchShareDialog( + context, + text: state.pgn, + ); + }, + ), + if (state.position != null) + BottomSheetAction( + makeLabel: (context) => + Text(context.l10n.screenshotCurrentPosition), + onPressed: (_) async { + try { + final image = await ref + .read(gameShareServiceProvider) + .screenshotPosition( + state.pov, + state.position!.fen, + state.lastMove, + ); + if (context.mounted) { + launchShareDialog( + context, + files: [image], + subject: context.l10n.puzzleFromGameLink( + lichessUri('/study/${state.study.id}') + .toString(), + ), + ); + } + } catch (e) { + if (context.mounted) { + showPlatformSnackbar( + context, + 'Failed to get GIF', + type: SnackBarType.error, + ); + } + } + }, + ), + BottomSheetAction( + makeLabel: (context) => const Text('GIF'), + onPressed: (_) async { + try { + final gif = + await ref.read(studyRepositoryProvider).chapterGif( + state.study.id, + state.study.chapter.id, + state.pov, + ); + if (context.mounted) { + launchShareDialog( + context, + files: [gif], + subject: context.l10n.studyChapterX( + state.study.currentChapterMeta.name, + ), + ); + } + } catch (e) { + debugPrint(e.toString()); + if (context.mounted) { + showPlatformSnackbar( + context, + 'Failed to get GIF', + type: SnackBarType.error, + ); + } + } + }, + ), + ], + ], + ); + }, ), - for (final chapter in state.study.chapters) - PlatformListTile( - key: chapter.id == state.currentChapter.id - ? currentChapterKey - : null, - title: Text(chapter.name, maxLines: 2), - onTap: () { - ref.read(studyControllerProvider(id).notifier).goToChapter( - chapter.id, - ); - Navigator.of(context).pop(); - }, - selected: chapter.id == state.currentChapter.id, - ), ], ); } diff --git a/lib/src/widgets/bottom_bar_button.dart b/lib/src/widgets/bottom_bar_button.dart index 42d340dc3b..7fb33f1461 100644 --- a/lib/src/widgets/bottom_bar_button.dart +++ b/lib/src/widgets/bottom_bar_button.dart @@ -89,6 +89,8 @@ class BottomBarButton extends StatelessWidget { fontSize: labelFontSize, color: highlighted ? primary : null, ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), ), ], diff --git a/test/model/study/study_repository_test.dart b/test/model/study/study_repository_test.dart index d3c269da6c..08fbde61c3 100644 --- a/test/model/study/study_repository_test.dart +++ b/test/model/study/study_repository_test.dart @@ -1,15 +1,28 @@ import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/study/study.dart'; import 'package:lichess_mobile/src/model/study/study_repository.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import '../../test_container.dart'; import '../../test_helpers.dart'; void main() { + Future makeTestContainer(MockClient mockClient) async { + return makeContainer( + overrides: [ + lichessClientProvider.overrideWith((ref) { + return LichessClient(mockClient, ref); + }), + ], + ); + } + group('StudyRepository.getStudy', () { test('correctly parse study JSON', () async { // curl -X GET https://lichess.org/study/JbWtuaeK/7OJXp679\?chapters\=1 -H "Accept: application/json" | sed "s/\\\\n/ /g" | jq 'del(.study.chat)' @@ -326,7 +339,8 @@ void main() { return mockResponse('', 404); }); - final repo = StudyRepository(mockClient); + final container = await makeTestContainer(mockClient); + final repo = container.read(studyRepositoryProvider); final (study, pgn) = await repo.getStudy( id: const StudyId('JbWtuaeK'), From ad853e33a7f069a379e16e8127691706922fd946 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 3 Dec 2024 22:49:47 +0100 Subject: [PATCH 830/979] Ensure to not recreate global key on each build --- lib/src/view/study/study_bottom_bar.dart | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/lib/src/view/study/study_bottom_bar.dart b/lib/src/view/study/study_bottom_bar.dart index 22d89526cf..005e43381f 100644 --- a/lib/src/view/study/study_bottom_bar.dart +++ b/lib/src/view/study/study_bottom_bar.dart @@ -266,7 +266,7 @@ class _ChapterButton extends ConsumerWidget { } } -class _StudyChaptersMenu extends ConsumerWidget { +class _StudyChaptersMenu extends ConsumerStatefulWidget { const _StudyChaptersMenu({ required this.id, required this.scrollController, @@ -276,10 +276,15 @@ class _StudyChaptersMenu extends ConsumerWidget { final ScrollController scrollController; @override - Widget build(BuildContext context, WidgetRef ref) { - final state = ref.watch(studyControllerProvider(id)).requireValue; + ConsumerState<_StudyChaptersMenu> createState() => _StudyChaptersMenuState(); +} - final currentChapterKey = GlobalKey(); +class _StudyChaptersMenuState extends ConsumerState<_StudyChaptersMenu> { + final currentChapterKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + final state = ref.watch(studyControllerProvider(widget.id)).requireValue; // Scroll to the current chapter WidgetsBinding.instance.addPostFrameCallback((_) { @@ -292,7 +297,7 @@ class _StudyChaptersMenu extends ConsumerWidget { }); return BottomSheetScrollableContainer( - scrollController: scrollController, + scrollController: widget.scrollController, children: [ Padding( padding: const EdgeInsets.all(16.0), @@ -308,7 +313,7 @@ class _StudyChaptersMenu extends ConsumerWidget { : null, title: Text(chapter.name, maxLines: 2), onTap: () { - ref.read(studyControllerProvider(id).notifier).goToChapter( + ref.read(studyControllerProvider(widget.id).notifier).goToChapter( chapter.id, ); Navigator.of(context).pop(); From c2ebebc6929d5da8078310b5d13df19898ceecb9 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 3 Dec 2024 23:01:49 +0100 Subject: [PATCH 831/979] Improve chapter menu --- lib/src/view/study/study_bottom_bar.dart | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/src/view/study/study_bottom_bar.dart b/lib/src/view/study/study_bottom_bar.dart index 005e43381f..43d8bf54cb 100644 --- a/lib/src/view/study/study_bottom_bar.dart +++ b/lib/src/view/study/study_bottom_bar.dart @@ -247,8 +247,9 @@ class _ChapterButton extends ConsumerWidget { isScrollControlled: true, isDismissible: true, builder: (_) => DraggableScrollableSheet( - initialChildSize: 0.5, - maxChildSize: 0.95, + initialChildSize: 0.6, + maxChildSize: 0.6, + minChildSize: 0.0, snap: true, expand: false, builder: (context, scrollController) { @@ -300,12 +301,13 @@ class _StudyChaptersMenuState extends ConsumerState<_StudyChaptersMenu> { scrollController: widget.scrollController, children: [ Padding( - padding: const EdgeInsets.all(16.0), + padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Text( context.l10n.studyNbChapters(state.study.chapters.length), style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), ), + const SizedBox(height: 16), for (final chapter in state.study.chapters) PlatformListTile( key: chapter.id == state.currentChapter.id From 272feed4e599214e13c607ba367fd0581207f767 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 3 Dec 2024 23:14:11 +0100 Subject: [PATCH 832/979] Improve study loading screen --- lib/src/view/study/study_screen.dart | 70 +++++++++++++++++++++------- 1 file changed, 54 insertions(+), 16 deletions(-) diff --git a/lib/src/view/study/study_screen.dart b/lib/src/view/study/study_screen.dart index b02cfbbe77..5bab1eec55 100644 --- a/lib/src/view/study/study_screen.dart +++ b/lib/src/view/study/study_screen.dart @@ -45,29 +45,67 @@ class StudyScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final state = ref.watch(studyControllerProvider(id)); - return state.when( - data: (state) { + final boardPrefs = ref.watch(boardPreferencesProvider); + switch (ref.watch(studyControllerProvider(id))) { + case AsyncData(:final value): return _StudyScreen( id: id, - studyState: state, + studyState: value, ); - }, - loading: () { - return const PlatformScaffold( - appBar: PlatformAppBar( + case AsyncError(:final error, :final stackTrace): + _logger.severe('Cannot load study: $error', stackTrace); + return PlatformScaffold( + appBar: const PlatformAppBar( title: Text(''), ), - body: Center(child: CircularProgressIndicator()), + body: DefaultTabController( + length: 1, + child: AnalysisLayout( + boardBuilder: (context, boardSize, borderRadius) => + Chessboard.fixed( + size: boardSize, + settings: boardPrefs.toBoardSettings().copyWith( + borderRadius: borderRadius, + boxShadow: borderRadius != null + ? boardShadows + : const [], + ), + orientation: Side.white, + fen: kEmptyFEN, + ), + children: const [ + Center( + child: Text('Failed to load study.'), + ), + ], + ), + ), ); - }, - error: (error, st) { - _logger.severe('Cannot load study: $error', st); - return Center( - child: Text('Cannot load study: $error'), + case _: + return PlatformScaffold( + appBar: const PlatformAppBar( + title: Text(''), + ), + body: DefaultTabController( + length: 1, + child: AnalysisLayout( + boardBuilder: (context, boardSize, borderRadius) => + Chessboard.fixed( + size: boardSize, + settings: boardPrefs.toBoardSettings().copyWith( + borderRadius: borderRadius, + boxShadow: borderRadius != null + ? boardShadows + : const [], + ), + orientation: Side.white, + fen: kEmptyFEN, + ), + children: const [SizedBox.shrink()], + ), + ), ); - }, - ); + } } } From c09552e51e8a667680646a5d72649f26da9fa433 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Mon, 2 Dec 2024 16:43:36 +0100 Subject: [PATCH 833/979] Rename broadcast list screen --- ...dcasts_list_screen.dart => broadcast_list_screen.dart} | 4 ++-- lib/src/view/watch/watch_tab_screen.dart | 4 ++-- test/view/broadcast/broadcasts_list_screen_test.dart | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) rename lib/src/view/broadcast/{broadcasts_list_screen.dart => broadcast_list_screen.dart} (99%) diff --git a/lib/src/view/broadcast/broadcasts_list_screen.dart b/lib/src/view/broadcast/broadcast_list_screen.dart similarity index 99% rename from lib/src/view/broadcast/broadcasts_list_screen.dart rename to lib/src/view/broadcast/broadcast_list_screen.dart index 5eb6ea697c..2e2bf6f687 100644 --- a/lib/src/view/broadcast/broadcasts_list_screen.dart +++ b/lib/src/view/broadcast/broadcast_list_screen.dart @@ -19,8 +19,8 @@ final _dateFormatter = DateFormat.MMMd().add_Hm(); final _dateFormatterWithYear = DateFormat.yMMMd().add_Hm(); /// A screen that displays a paginated list of broadcasts. -class BroadcastsListScreen extends StatelessWidget { - const BroadcastsListScreen({super.key}); +class BroadcastListScreen extends StatelessWidget { + const BroadcastListScreen({super.key}); @override Widget build(BuildContext context) { diff --git a/lib/src/view/watch/watch_tab_screen.dart b/lib/src/view/watch/watch_tab_screen.dart index d9358d6e89..dd944a56d0 100644 --- a/lib/src/view/watch/watch_tab_screen.dart +++ b/lib/src/view/watch/watch_tab_screen.dart @@ -15,8 +15,8 @@ import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/view/broadcast/broadcast_list_screen.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_tile.dart'; -import 'package:lichess_mobile/src/view/broadcast/broadcasts_list_screen.dart'; import 'package:lichess_mobile/src/view/watch/live_tv_channels_screen.dart'; import 'package:lichess_mobile/src/view/watch/streamer_screen.dart'; import 'package:lichess_mobile/src/view/watch/tv_screen.dart'; @@ -199,7 +199,7 @@ class _BroadcastWidget extends ConsumerWidget { onPressed: () { pushPlatformRoute( context, - builder: (context) => const BroadcastsListScreen(), + builder: (context) => const BroadcastListScreen(), ); }, child: Text( diff --git a/test/view/broadcast/broadcasts_list_screen_test.dart b/test/view/broadcast/broadcasts_list_screen_test.dart index f5da2b5843..166b23e880 100644 --- a/test/view/broadcast/broadcasts_list_screen_test.dart +++ b/test/view/broadcast/broadcasts_list_screen_test.dart @@ -1,7 +1,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; import 'package:lichess_mobile/src/network/http.dart'; -import 'package:lichess_mobile/src/view/broadcast/broadcasts_list_screen.dart'; +import 'package:lichess_mobile/src/view/broadcast/broadcast_list_screen.dart'; import 'package:network_image_mock/network_image_mock.dart'; import '../../test_helpers.dart'; @@ -19,14 +19,14 @@ final client = MockClient((request) { }); void main() { - group('BroadcastsListScreen', () { + group('BroadcastListScreen', () { testWidgets( 'Displays broadcast tournament screen', variant: kPlatformVariant, (tester) async { final app = await makeTestProviderScopeApp( tester, - home: const BroadcastsListScreen(), + home: const BroadcastListScreen(), overrides: [ lichessClientProvider .overrideWith((ref) => LichessClient(client, ref)), @@ -50,7 +50,7 @@ void main() { (tester) async { final app = await makeTestProviderScopeApp( tester, - home: const BroadcastsListScreen(), + home: const BroadcastListScreen(), overrides: [ lichessClientProvider .overrideWith((ref) => LichessClient(client, ref)), From aedb346afdf0c9bea6416ba2eba8a894fbf6f986 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Tue, 3 Dec 2024 01:39:42 +0100 Subject: [PATCH 834/979] Improve broadcast list screen on tablets --- .../view/broadcast/broadcast_list_screen.dart | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_list_screen.dart b/lib/src/view/broadcast/broadcast_list_screen.dart index 2e2bf6f687..665c692e3a 100644 --- a/lib/src/view/broadcast/broadcast_list_screen.dart +++ b/lib/src/view/broadcast/broadcast_list_screen.dart @@ -9,6 +9,7 @@ import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/styles/transparent_image.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_screen.dart'; import 'package:lichess_mobile/src/view/broadcast/default_broadcast_image.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; @@ -84,8 +85,11 @@ class _BodyState extends ConsumerState<_Body> { return const Center(child: Text('Could not load broadcast tournaments')); } - final itemsCount = - broadcasts.requireValue.past.length + (broadcasts.isLoading ? 10 : 0); + final isTablet = isTabletOrLarger(context); + final itemsByRow = isTablet ? 6 : 2; + final loadingItems = isTablet ? 36 : 12; + final itemsCount = broadcasts.requireValue.past.length + + (broadcasts.isLoading ? loadingItems : 0); return SafeArea( child: CustomScrollView( @@ -94,8 +98,8 @@ class _BodyState extends ConsumerState<_Body> { SliverPadding( padding: Styles.bodySectionPadding, sliver: SliverGrid.builder( - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: itemsByRow, crossAxisSpacing: 10, mainAxisSpacing: 10, ), @@ -116,8 +120,8 @@ class _BodyState extends ConsumerState<_Body> { SliverPadding( padding: Styles.bodySectionPadding, sliver: SliverGrid.builder( - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: itemsByRow, crossAxisSpacing: 10, mainAxisSpacing: 10, ), @@ -139,13 +143,13 @@ class _BodyState extends ConsumerState<_Body> { SliverPadding( padding: Styles.bodySectionPadding, sliver: SliverGrid.builder( - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: itemsByRow, crossAxisSpacing: 10, mainAxisSpacing: 10, ), itemBuilder: (context, index) => (broadcasts.isLoading && - index >= itemsCount - 10) + index >= itemsCount - loadingItems) ? Shimmer( child: ShimmerLoading( isLoading: true, @@ -156,11 +160,6 @@ class _BodyState extends ConsumerState<_Body> { itemCount: itemsCount, ), ), - const SliverToBoxAdapter( - child: SizedBox( - height: 10, - ), - ), ], ), ); From 70c0b45b47812f14fb3818948f749569be5ec2e3 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Tue, 3 Dec 2024 03:00:41 +0100 Subject: [PATCH 835/979] Use the same scroll limit to get next page when using pagination --- lib/src/view/broadcast/broadcast_list_screen.dart | 4 ++-- lib/src/view/puzzle/puzzle_history_screen.dart | 2 +- lib/src/view/study/study_list_screen.dart | 2 +- lib/src/view/user/game_history_screen.dart | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_list_screen.dart b/lib/src/view/broadcast/broadcast_list_screen.dart index 665c692e3a..6529c9e894 100644 --- a/lib/src/view/broadcast/broadcast_list_screen.dart +++ b/lib/src/view/broadcast/broadcast_list_screen.dart @@ -58,8 +58,8 @@ class _BodyState extends ConsumerState<_Body> { } void _scrollListener() { - if (_scrollController.position.pixels == - _scrollController.position.maxScrollExtent) { + if (_scrollController.position.pixels >= + _scrollController.position.maxScrollExtent - 300) { final broadcastList = ref.read(broadcastsPaginatorProvider); if (!broadcastList.isLoading) { diff --git a/lib/src/view/puzzle/puzzle_history_screen.dart b/lib/src/view/puzzle/puzzle_history_screen.dart index 6b715d2788..ba97ba3263 100644 --- a/lib/src/view/puzzle/puzzle_history_screen.dart +++ b/lib/src/view/puzzle/puzzle_history_screen.dart @@ -105,7 +105,7 @@ class _BodyState extends ConsumerState<_Body> { void _scrollListener() { if (_scrollController.position.pixels >= - _scrollController.position.maxScrollExtent * 0.7) { + _scrollController.position.maxScrollExtent - 300) { final currentState = ref.read(puzzleActivityProvider).valueOrNull; if (currentState != null && !currentState.isLoading) { ref.read(puzzleActivityProvider.notifier).getNext(); diff --git a/lib/src/view/study/study_list_screen.dart b/lib/src/view/study/study_list_screen.dart index 3829dcd0aa..d76515cf18 100644 --- a/lib/src/view/study/study_list_screen.dart +++ b/lib/src/view/study/study_list_screen.dart @@ -143,7 +143,7 @@ class _BodyState extends ConsumerState<_Body> { void _scrollListener() { if (!requestedNextPage && _scrollController.position.pixels >= - 0.75 * _scrollController.position.maxScrollExtent) { + _scrollController.position.maxScrollExtent - 300) { final studiesList = ref.read(paginatorProvider); if (!studiesList.isLoading) { diff --git a/lib/src/view/user/game_history_screen.dart b/lib/src/view/user/game_history_screen.dart index 921c60258a..0d01215b3e 100644 --- a/lib/src/view/user/game_history_screen.dart +++ b/lib/src/view/user/game_history_screen.dart @@ -110,8 +110,8 @@ class _BodyState extends ConsumerState<_Body> { } void _scrollListener() { - if (_scrollController.position.pixels == - _scrollController.position.maxScrollExtent) { + if (_scrollController.position.pixels >= + _scrollController.position.maxScrollExtent - 300) { final state = ref.read( userGameHistoryProvider( widget.user?.id, From 5765b5cb475aaabe213bcd5d79dae8bb7601c913 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 4 Dec 2024 11:13:57 +0100 Subject: [PATCH 836/979] Rename to broadcast_round_screen --- .../{broadcast_screen.dart => broadcast_round_screen.dart} | 4 ++-- lib/src/view/broadcast/broadcast_tile.dart | 4 ++-- lib/src/view/broadcast/broadcasts_list_screen.dart | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) rename lib/src/view/broadcast/{broadcast_screen.dart => broadcast_round_screen.dart} (99%) diff --git a/lib/src/view/broadcast/broadcast_screen.dart b/lib/src/view/broadcast/broadcast_round_screen.dart similarity index 99% rename from lib/src/view/broadcast/broadcast_screen.dart rename to lib/src/view/broadcast/broadcast_round_screen.dart index 8565a2cf21..895f24562f 100644 --- a/lib/src/view/broadcast/broadcast_screen.dart +++ b/lib/src/view/broadcast/broadcast_round_screen.dart @@ -14,10 +14,10 @@ import 'package:lichess_mobile/src/view/broadcast/dropdown_menu.dart' as fixed; import 'package:lichess_mobile/src/widgets/adaptive_choice_picker.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; -class BroadcastScreen extends StatelessWidget { +class BroadcastRoundScreen extends StatelessWidget { final Broadcast broadcast; - const BroadcastScreen({required this.broadcast}); + const BroadcastRoundScreen({required this.broadcast}); @override Widget build(BuildContext context) { diff --git a/lib/src/view/broadcast/broadcast_tile.dart b/lib/src/view/broadcast/broadcast_tile.dart index 818d07beed..b84c2f2d65 100644 --- a/lib/src/view/broadcast/broadcast_tile.dart +++ b/lib/src/view/broadcast/broadcast_tile.dart @@ -4,7 +4,7 @@ import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; import 'package:lichess_mobile/src/styles/transparent_image.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; -import 'package:lichess_mobile/src/view/broadcast/broadcast_screen.dart'; +import 'package:lichess_mobile/src/view/broadcast/broadcast_round_screen.dart'; import 'package:lichess_mobile/src/view/broadcast/default_broadcast_image.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; @@ -31,7 +31,7 @@ class BroadcastTile extends ConsumerWidget { context, title: context.l10n.broadcastBroadcasts, rootNavigator: true, - builder: (context) => BroadcastScreen(broadcast: broadcast), + builder: (context) => BroadcastRoundScreen(broadcast: broadcast), ); }, title: Padding( diff --git a/lib/src/view/broadcast/broadcasts_list_screen.dart b/lib/src/view/broadcast/broadcasts_list_screen.dart index 5eb6ea697c..8f88c12570 100644 --- a/lib/src/view/broadcast/broadcasts_list_screen.dart +++ b/lib/src/view/broadcast/broadcasts_list_screen.dart @@ -9,7 +9,7 @@ import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/styles/transparent_image.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; -import 'package:lichess_mobile/src/view/broadcast/broadcast_screen.dart'; +import 'package:lichess_mobile/src/view/broadcast/broadcast_round_screen.dart'; import 'package:lichess_mobile/src/view/broadcast/default_broadcast_image.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; @@ -208,7 +208,7 @@ class BroadcastGridItem extends StatelessWidget { context, title: context.l10n.broadcastBroadcasts, rootNavigator: true, - builder: (context) => BroadcastScreen(broadcast: broadcast), + builder: (context) => BroadcastRoundScreen(broadcast: broadcast), ); }, child: Container( From 4ab80ad427d75914badc1e29c8d5d985095309fc Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 4 Dec 2024 11:49:37 +0100 Subject: [PATCH 837/979] Add tournament image, location and website to overview --- lib/src/model/broadcast/broadcast.dart | 2 + .../model/broadcast/broadcast_repository.dart | 3 + .../broadcast/broadcast_overview_tab.dart | 73 ++++++++++++++----- .../broadcast/broadcasts_list_screen.dart | 17 ++--- 4 files changed, 68 insertions(+), 27 deletions(-) diff --git a/lib/src/model/broadcast/broadcast.dart b/lib/src/model/broadcast/broadcast.dart index 2ad7af01fa..33edae8d8c 100644 --- a/lib/src/model/broadcast/broadcast.dart +++ b/lib/src/model/broadcast/broadcast.dart @@ -59,7 +59,9 @@ typedef BroadcastTournamentInformation = ({ String? format, String? timeControl, String? players, + String? location, BroadcastTournamentDates? dates, + Uri? website, }); typedef BroadcastTournamentDates = ({ diff --git a/lib/src/model/broadcast/broadcast_repository.dart b/lib/src/model/broadcast/broadcast_repository.dart index 7e7e6de723..3455903a1d 100644 --- a/lib/src/model/broadcast/broadcast_repository.dart +++ b/lib/src/model/broadcast/broadcast_repository.dart @@ -91,12 +91,15 @@ BroadcastTournamentData _tournamentDataFromPick( format: pick('info', 'format').asStringOrNull(), timeControl: pick('info', 'tc').asStringOrNull(), players: pick('info', 'players').asStringOrNull(), + location: pick('info', 'location').asStringOrNull(), dates: pick('dates').letOrNull( (pick) => ( startsAt: pick(0).asDateTimeFromMillisecondsOrThrow(), endsAt: pick(1).asDateTimeFromMillisecondsOrNull(), ), ), + website: pick('info', 'website') + .letOrNull((p) => Uri.tryParse(p.asStringOrThrow())), ), ); diff --git a/lib/src/view/broadcast/broadcast_overview_tab.dart b/lib/src/view/broadcast/broadcast_overview_tab.dart index f61fe79138..061519116b 100644 --- a/lib/src/view/broadcast/broadcast_overview_tab.dart +++ b/lib/src/view/broadcast/broadcast_overview_tab.dart @@ -7,6 +7,9 @@ import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_providers.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:url_launcher/url_launcher.dart'; final _dateFormatter = DateFormat.MMMd(); @@ -29,7 +32,7 @@ class BroadcastOverviewTab extends ConsumerWidget { child: switch (tournament) { AsyncData(:final value) => BroadcastOverviewBody(value), AsyncError(:final error) => Center( - child: Text('Cannot load game analysis: $error'), + child: Text('Cannot load broadcast data: $error'), ), _ => const Center(child: CircularProgressIndicator.adaptive()), }, @@ -51,31 +54,46 @@ class BroadcastOverviewBody extends StatelessWidget { return Column( children: [ + if (tournament.data.imageUrl != null) ...[ + Image.network(tournament.data.imageUrl!), + const SizedBox(height: 16.0), + ], Wrap( alignment: WrapAlignment.center, children: [ if (information.dates != null) - BroadcastOverviewCard( + _BroadcastOverviewCard( CupertinoIcons.calendar, information.dates!.endsAt == null ? _dateFormatter.format(information.dates!.startsAt) : '${_dateFormatter.format(information.dates!.startsAt)} - ${_dateFormatter.format(information.dates!.endsAt!)}', ), if (information.format != null) - BroadcastOverviewCard( + _BroadcastOverviewCard( Icons.emoji_events, '${information.format}', ), if (information.timeControl != null) - BroadcastOverviewCard( + _BroadcastOverviewCard( CupertinoIcons.stopwatch_fill, '${information.timeControl}', ), + if (information.location != null) + _BroadcastOverviewCard( + Icons.public, + '${information.location}', + ), if (information.players != null) - BroadcastOverviewCard( + _BroadcastOverviewCard( Icons.person, '${information.players}', ), + if (information.website != null) + _BroadcastOverviewCard( + Icons.link, + context.l10n.broadcastOfficialWebsite, + information.website, + ), ], ), if (description != null) @@ -94,24 +112,43 @@ class BroadcastOverviewBody extends StatelessWidget { } } -class BroadcastOverviewCard extends StatelessWidget { +class _BroadcastOverviewCard extends StatelessWidget { + const _BroadcastOverviewCard(this.iconData, this.text, [this.website]); + final IconData iconData; final String text; - - const BroadcastOverviewCard(this.iconData, this.text); + final Uri? website; @override Widget build(BuildContext context) { - return Card( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(iconData), - const SizedBox(width: 10), - Flexible(child: Text(text)), - ], + return PlatformCard( + margin: const EdgeInsets.all(4.0), + child: AdaptiveInkWell( + onTap: website != null ? () => launchUrl(website!) : null, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + iconData, + color: website != null + ? Theme.of(context).colorScheme.primary + : null, + ), + const SizedBox(width: 10), + Flexible( + child: Text( + text, + style: TextStyle( + color: website != null + ? Theme.of(context).colorScheme.primary + : null, + ), + ), + ), + ], + ), ), ), ); diff --git a/lib/src/view/broadcast/broadcasts_list_screen.dart b/lib/src/view/broadcast/broadcasts_list_screen.dart index 8f88c12570..9ac88d0883 100644 --- a/lib/src/view/broadcast/broadcasts_list_screen.dart +++ b/lib/src/view/broadcast/broadcasts_list_screen.dart @@ -174,19 +174,18 @@ class BroadcastGridItem extends StatelessWidget { BroadcastGridItem.loading() : broadcast = Broadcast( - tour: BroadcastTournamentData( - id: const BroadcastTournamentId(''), + tour: const BroadcastTournamentData( + id: BroadcastTournamentId(''), name: '', imageUrl: null, description: '', information: ( - format: '', - timeControl: '', - players: '', - dates: ( - startsAt: DateTime.now(), - endsAt: DateTime.now(), - ), + format: null, + timeControl: null, + players: null, + website: null, + location: null, + dates: null, ), ), round: BroadcastRound( From e2861b49ef1440413564f23e08d1f59b24d661c6 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 4 Dec 2024 12:05:48 +0100 Subject: [PATCH 838/979] Improve AdaptiveInkWell --- .../broadcast/broadcast_overview_tab.dart | 2 + lib/src/view/user/perf_cards.dart | 2 +- lib/src/widgets/buttons.dart | 75 ++++--------------- 3 files changed, 19 insertions(+), 60 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_overview_tab.dart b/lib/src/view/broadcast/broadcast_overview_tab.dart index 061519116b..bd9ad5b7f8 100644 --- a/lib/src/view/broadcast/broadcast_overview_tab.dart +++ b/lib/src/view/broadcast/broadcast_overview_tab.dart @@ -122,8 +122,10 @@ class _BroadcastOverviewCard extends StatelessWidget { @override Widget build(BuildContext context) { return PlatformCard( + borderRadius: const BorderRadius.all(Radius.circular(10)), margin: const EdgeInsets.all(4.0), child: AdaptiveInkWell( + borderRadius: const BorderRadius.all(Radius.circular(10)), onTap: website != null ? () => launchUrl(website!) : null, child: Padding( padding: const EdgeInsets.all(8.0), diff --git a/lib/src/view/user/perf_cards.dart b/lib/src/view/user/perf_cards.dart index 3440bf796e..8aea390e7d 100644 --- a/lib/src/view/user/perf_cards.dart +++ b/lib/src/view/user/perf_cards.dart @@ -83,7 +83,7 @@ class PerfCards extends StatelessWidget { width: 100, child: PlatformCard( child: AdaptiveInkWell( - borderRadius: BorderRadius.circular(10), + borderRadius: const BorderRadius.all(Radius.circular(10)), onTap: isPerfWithoutStats ? null : () => _handlePerfCardTap(context, perf), diff --git a/lib/src/widgets/buttons.dart b/lib/src/widgets/buttons.dart index a560586939..38d0193698 100644 --- a/lib/src/widgets/buttons.dart +++ b/lib/src/widgets/buttons.dart @@ -328,7 +328,7 @@ class CupertinoIconButton extends StatelessWidget { /// InkWell that adapts to the iOS platform. /// /// Used to create a button that shows a ripple on Android and a highlight on iOS. -class AdaptiveInkWell extends StatefulWidget { +class AdaptiveInkWell extends StatelessWidget { const AdaptiveInkWell({ required this.child, this.onTap, @@ -350,66 +350,23 @@ class AdaptiveInkWell extends StatefulWidget { final BorderRadius? borderRadius; final Color? splashColor; - @override - State createState() => _AdaptiveInkWellState(); -} - -class _AdaptiveInkWellState extends State { - bool _isPressed = false; - @override Widget build(BuildContext context) { - switch (Theme.of(context).platform) { - case TargetPlatform.android: - return InkWell( - onTap: widget.onTap, - onTapDown: widget.onTapDown, - onTapUp: widget.onTapUp, - onTapCancel: widget.onTapCancel, - onLongPress: widget.onLongPress, - borderRadius: widget.borderRadius, - splashColor: widget.splashColor, - child: widget.child, - ); - case TargetPlatform.iOS: - return GestureDetector( - onLongPress: widget.onLongPress, - onTap: widget.onTap, - onTapDown: (details) { - widget.onTapDown?.call(details); - if (widget.onTap == null) return; - setState(() => _isPressed = true); - }, - onTapCancel: () { - widget.onTapCancel?.call(); - setState(() => _isPressed = false); - }, - onTapUp: (details) { - widget.onTapUp?.call(details); - Future.delayed(const Duration(milliseconds: 100)).then((_) { - if (mounted) { - setState(() => _isPressed = false); - } - }); - }, - child: Semantics( - button: true, - child: Container( - decoration: BoxDecoration( - borderRadius: widget.borderRadius, - color: _isPressed - ? widget.splashColor ?? - CupertinoColors.systemGrey5.resolveFrom(context) - : null, - ), - child: widget.child, - ), - ), - ); - default: - assert(false, 'Unexpected platform ${Theme.of(context).platform}'); - return const SizedBox.shrink(); - } + final platform = Theme.of(context).platform; + return InkWell( + onTap: onTap, + onTapDown: onTapDown, + onTapUp: onTapUp, + onTapCancel: onTapCancel, + onLongPress: onLongPress, + borderRadius: borderRadius, + splashColor: platform == TargetPlatform.iOS + ? splashColor ?? CupertinoColors.systemGrey5.resolveFrom(context) + : splashColor, + splashFactory: + platform == TargetPlatform.iOS ? NoSplash.splashFactory : null, + child: child, + ); } } From d22de6fd8ec0ad1469592390f77d49115ed5fa92 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 4 Dec 2024 15:15:50 +0100 Subject: [PATCH 839/979] Improve broadcast round selector by showing status and date --- lib/src/model/broadcast/broadcast.dart | 1 + .../model/broadcast/broadcast_repository.dart | 1 + .../broadcast/broadcast_round_screen.dart | 635 ++++---- lib/src/view/broadcast/dropdown_menu.dart | 1377 ----------------- lib/src/view/study/study_bottom_bar.dart | 1 - lib/src/widgets/list.dart | 4 +- 6 files changed, 301 insertions(+), 1718 deletions(-) delete mode 100644 lib/src/view/broadcast/dropdown_menu.dart diff --git a/lib/src/model/broadcast/broadcast.dart b/lib/src/model/broadcast/broadcast.dart index 33edae8d8c..deb19c88e6 100644 --- a/lib/src/model/broadcast/broadcast.dart +++ b/lib/src/model/broadcast/broadcast.dart @@ -83,6 +83,7 @@ class BroadcastRound with _$BroadcastRound { required String name, required RoundStatus status, required DateTime? startsAt, + DateTime? finishedAt, }) = _BroadcastRound; } diff --git a/lib/src/model/broadcast/broadcast_repository.dart b/lib/src/model/broadcast/broadcast_repository.dart index 3455903a1d..1b3a3eb790 100644 --- a/lib/src/model/broadcast/broadcast_repository.dart +++ b/lib/src/model/broadcast/broadcast_repository.dart @@ -137,6 +137,7 @@ BroadcastRound _roundFromPick(RequiredPick pick) { name: pick('name').asStringOrThrow(), status: status, startsAt: pick('startsAt').asDateTimeFromMillisecondsOrNull(), + finishedAt: pick('finishedAt').asDateTimeFromMillisecondsOrNull(), ); } diff --git a/lib/src/view/broadcast/broadcast_round_screen.dart b/lib/src/view/broadcast/broadcast_round_screen.dart index 895f24562f..df27adfc32 100644 --- a/lib/src/view/broadcast/broadcast_round_screen.dart +++ b/lib/src/view/broadcast/broadcast_round_screen.dart @@ -1,50 +1,38 @@ -import 'dart:ui'; - +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_providers.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_boards_tab.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_overview_tab.dart'; -import 'package:lichess_mobile/src/view/broadcast/dropdown_menu.dart' as fixed; -import 'package:lichess_mobile/src/widgets/adaptive_choice_picker.dart'; -import 'package:lichess_mobile/src/widgets/platform.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; +import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/list.dart'; -class BroadcastRoundScreen extends StatelessWidget { +class BroadcastRoundScreen extends ConsumerStatefulWidget { final Broadcast broadcast; const BroadcastRoundScreen({required this.broadcast}); @override - Widget build(BuildContext context) { - return PlatformWidget(androidBuilder: _buildAndroid, iosBuilder: _buildIos); - } - - Widget _buildAndroid(BuildContext context) => - _AndroidScreen(broadcast: broadcast); - - Widget _buildIos(BuildContext context) => - _CupertinoScreen(broadcast: broadcast); + _BroadcastRoundScreenState createState() => _BroadcastRoundScreenState(); } -class _AndroidScreen extends StatefulWidget { - final Broadcast broadcast; - - const _AndroidScreen({required this.broadcast}); - - @override - State<_AndroidScreen> createState() => _AndroidScreenState(); -} +enum _ViewMode { overview, boards } -class _AndroidScreenState extends State<_AndroidScreen> +class _BroadcastRoundScreenState extends ConsumerState with SingleTickerProviderStateMixin { + _ViewMode _selectedSegment = _ViewMode.boards; late final TabController _tabController; late BroadcastTournamentId _selectedTournamentId; - late BroadcastRoundId _selectedRoundId; + BroadcastRoundId? _selectedRoundId; @override void initState() { @@ -54,78 +42,12 @@ class _AndroidScreenState extends State<_AndroidScreen> _selectedRoundId = widget.broadcast.roundToLinkId; } - void setTournamentId(BroadcastTournamentId tournamentId) { - setState(() { - _selectedTournamentId = tournamentId; - }); - } - - void setRoundId(BroadcastRoundId roundId) { - setState(() { - _selectedRoundId = roundId; - }); - } - @override void dispose() { _tabController.dispose(); super.dispose(); } - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(widget.broadcast.title), - bottom: TabBar( - controller: _tabController, - tabs: [ - Tab(text: context.l10n.broadcastOverview), - Tab(text: context.l10n.broadcastBoards), - ], - ), - ), - body: TabBarView( - controller: _tabController, - children: [ - BroadcastOverviewTab(tournamentId: _selectedTournamentId), - BroadcastBoardsTab(_selectedRoundId), - ], - ), - bottomNavigationBar: BottomAppBar( - child: _AndroidTournamentAndRoundSelector( - tournamentId: _selectedTournamentId, - setTournamentId: setTournamentId, - setRoundId: setRoundId, - ), - ), - ); - } -} - -class _CupertinoScreen extends StatefulWidget { - final Broadcast broadcast; - - const _CupertinoScreen({required this.broadcast}); - - @override - _CupertinoScreenState createState() => _CupertinoScreenState(); -} - -enum _ViewMode { overview, boards } - -class _CupertinoScreenState extends State<_CupertinoScreen> { - _ViewMode _selectedSegment = _ViewMode.boards; - late BroadcastTournamentId _selectedTournamentId; - late BroadcastRoundId _selectedRoundId; - - @override - void initState() { - super.initState(); - _selectedTournamentId = widget.broadcast.tour.id; - _selectedRoundId = widget.broadcast.roundToLinkId; - } - void setViewMode(_ViewMode mode) { setState(() { _selectedSegment = mode; @@ -135,6 +57,7 @@ class _CupertinoScreenState extends State<_CupertinoScreen> { void setTournamentId(BroadcastTournamentId tournamentId) { setState(() { _selectedTournamentId = tournamentId; + _selectedRoundId = null; }); } @@ -146,282 +69,316 @@ class _CupertinoScreenState extends State<_CupertinoScreen> { @override Widget build(BuildContext context) { - return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - middle: CupertinoSlidingSegmentedControl<_ViewMode>( - groupValue: _selectedSegment, - children: { - _ViewMode.overview: Text(context.l10n.broadcastOverview), - _ViewMode.boards: Text(context.l10n.broadcastBoards), - }, - onValueChanged: (_ViewMode? view) { - if (view != null) { - setState(() { - _selectedSegment = view; - }); - } - }, - ), - ), - child: Column( - children: [ - Expanded( - child: _selectedSegment == _ViewMode.overview - ? BroadcastOverviewTab(tournamentId: _selectedTournamentId) - : BroadcastBoardsTab(_selectedRoundId), - ), - _IOSTournamentAndRoundSelector( - tournamentId: _selectedTournamentId, - roundId: _selectedRoundId, - setTournamentId: setTournamentId, - setRoundId: setRoundId, - ), - ], - ), - ); + final tournament = + ref.watch(broadcastTournamentProvider(_selectedTournamentId)); + + switch (tournament) { + case AsyncData(:final value): + if (Theme.of(context).platform == TargetPlatform.iOS) { + return CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + middle: CupertinoSlidingSegmentedControl<_ViewMode>( + groupValue: _selectedSegment, + children: { + _ViewMode.overview: Text(context.l10n.broadcastOverview), + _ViewMode.boards: Text(context.l10n.broadcastBoards), + }, + onValueChanged: (_ViewMode? view) { + if (view != null) { + setState(() { + _selectedSegment = view; + }); + } + }, + ), + ), + child: Column( + children: [ + Expanded( + child: _selectedSegment == _ViewMode.overview + ? BroadcastOverviewTab( + tournamentId: _selectedTournamentId, + ) + : BroadcastBoardsTab( + _selectedRoundId ?? value.defaultRoundId, + ), + ), + _BottomBar( + tournament: value, + roundId: _selectedRoundId ?? value.defaultRoundId, + setTournamentId: setTournamentId, + setRoundId: setRoundId, + ), + ], + ), + ); + } else { + return Scaffold( + appBar: AppBar( + title: AutoSizeText( + widget.broadcast.title, + minFontSize: 14.0, + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + bottom: TabBar( + controller: _tabController, + tabs: [ + Tab(text: context.l10n.broadcastOverview), + Tab(text: context.l10n.broadcastBoards), + ], + ), + ), + body: TabBarView( + controller: _tabController, + children: [ + BroadcastOverviewTab(tournamentId: _selectedTournamentId), + BroadcastBoardsTab(_selectedRoundId ?? value.defaultRoundId), + ], + ), + bottomNavigationBar: _BottomBar( + tournament: value, + roundId: _selectedRoundId ?? value.defaultRoundId, + setTournamentId: setTournamentId, + setRoundId: setRoundId, + ), + ); + } + case AsyncError(:final error): + return Center(child: Text(error.toString())); + case _: + return const Center(child: CircularProgressIndicator.adaptive()); + } } } -class _AndroidTournamentAndRoundSelector extends ConsumerWidget { - final BroadcastTournamentId tournamentId; - final void Function(BroadcastTournamentId) setTournamentId; - final void Function(BroadcastRoundId) setRoundId; - - const _AndroidTournamentAndRoundSelector({ - required this.tournamentId, +class _BottomBar extends ConsumerWidget { + const _BottomBar({ + required this.tournament, + required this.roundId, required this.setTournamentId, required this.setRoundId, }); + final BroadcastTournament tournament; + final BroadcastRoundId roundId; + final void Function(BroadcastTournamentId) setTournamentId; + final void Function(BroadcastRoundId) setRoundId; + @override Widget build(BuildContext context, WidgetRef ref) { - final tournament = ref.watch(broadcastTournamentProvider(tournamentId)); - - return switch (tournament) { - AsyncData(:final value) => Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - if (value.group != null) - Flexible( - child: // TODO replace with the Flutter framework DropdownMenu when next beta channel is released - fixed.DropdownMenu( - label: const Text('Tournament'), - initialSelection: value.data.id, - dropdownMenuEntries: value.group! - .map( - (tournament) => - // TODO replace with the Flutter framework DropdownMenuEntry when next beta channel is released - fixed.DropdownMenuEntry( - value: tournament.id, - label: tournament.name, - ), - ) - .toList(), - onSelected: (BroadcastTournamentId? value) async { - setTournamentId(value!); - final newTournament = await ref.read( - broadcastTournamentProvider(value).future, - ); - setRoundId(newTournament.defaultRoundId); - }, - ), - ), - Flexible( - child: // TODO replace with the Flutter framework DropdownMenu when next beta channel is released - fixed.DropdownMenu( - label: const Text('Round'), - initialSelection: value.defaultRoundId, - dropdownMenuEntries: value.rounds - .map( - (BroadcastRound round) => - // TODO replace with the Flutter framework DropdownMenuEntry when next beta channel is released - fixed.DropdownMenuEntry( - value: round.id, - label: round.name, - ), - ) - .toList(), - onSelected: (BroadcastRoundId? value) { - setRoundId(value!); + return BottomBar( + children: [ + if (tournament.group != null) + AdaptiveTextButton( + onPressed: () => showAdaptiveBottomSheet( + context: context, + showDragHandle: true, + isScrollControlled: true, + isDismissible: true, + builder: (_) => DraggableScrollableSheet( + initialChildSize: 0.4, + maxChildSize: 0.4, + minChildSize: 0.1, + snap: true, + expand: false, + builder: (context, scrollController) { + return _TournamentSelectorMenu( + tournament: tournament, + group: tournament.group!, + scrollController: scrollController, + setTournamentId: setTournamentId, + ); }, ), ), - ], + child: Text( + tournament.group! + .firstWhere((g) => g.id == tournament.data.id) + .name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + AdaptiveTextButton( + onPressed: () => showAdaptiveBottomSheet( + context: context, + showDragHandle: true, + isScrollControlled: true, + isDismissible: true, + builder: (_) => DraggableScrollableSheet( + initialChildSize: 0.6, + maxChildSize: 0.6, + snap: true, + expand: false, + builder: (context, scrollController) { + return _RoundSelectorMenu( + selectedRoundId: roundId, + rounds: tournament.rounds, + scrollController: scrollController, + setRoundId: setRoundId, + ); + }, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: Text( + tournament.rounds + .firstWhere((round) => round.id == roundId) + .name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 5.0), + switch (tournament.rounds + .firstWhere((round) => round.id == roundId) + .status) { + RoundStatus.finished => + Icon(Icons.check, color: context.lichessColors.good), + RoundStatus.live => + Icon(Icons.circle, color: context.lichessColors.error), + RoundStatus.upcoming => + const Icon(Icons.calendar_month, color: Colors.grey), + }, + ], + ), ), - AsyncError(:final error) => Center(child: Text(error.toString())), - _ => const SizedBox.shrink(), - }; + ], + ); } } -const Color _kDefaultToolBarBorderColor = Color(0x4D000000); +class _RoundSelectorMenu extends ConsumerStatefulWidget { + const _RoundSelectorMenu({ + required this.selectedRoundId, + required this.rounds, + required this.scrollController, + required this.setRoundId, + }); -const Border _kDefaultToolBarBorder = Border( - top: BorderSide( - color: _kDefaultToolBarBorderColor, - width: 0.0, // 0.0 means one physical pixel - ), -); + final BroadcastRoundId selectedRoundId; + final IList rounds; + final ScrollController scrollController; + final void Function(BroadcastRoundId) setRoundId; -// Code taken from the Cupertino navigation bar widget -Widget _wrapWithBackground({ - Border? border, - required Color backgroundColor, - Brightness? brightness, - required Widget child, - bool updateSystemUiOverlay = true, -}) { - Widget result = child; - if (updateSystemUiOverlay) { - final bool isDark = backgroundColor.computeLuminance() < 0.179; - final Brightness newBrightness = - brightness ?? (isDark ? Brightness.dark : Brightness.light); - final SystemUiOverlayStyle overlayStyle = switch (newBrightness) { - Brightness.dark => SystemUiOverlayStyle.light, - Brightness.light => SystemUiOverlayStyle.dark, - }; - result = AnnotatedRegion( - value: SystemUiOverlayStyle( - statusBarColor: overlayStyle.statusBarColor, - statusBarBrightness: overlayStyle.statusBarBrightness, - statusBarIconBrightness: overlayStyle.statusBarIconBrightness, - systemStatusBarContrastEnforced: - overlayStyle.systemStatusBarContrastEnforced, - ), - child: result, - ); - } - final DecoratedBox childWithBackground = DecoratedBox( - decoration: BoxDecoration( - border: border, - color: backgroundColor, - ), - child: result, - ); + @override + ConsumerState<_RoundSelectorMenu> createState() => _RoundSelectorState(); +} - if (backgroundColor.a == 0xFF) { - return childWithBackground; - } +final _dateFormat = DateFormat.yMd().add_jm(); - return ClipRect( - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0), - child: childWithBackground, - ), - ); -} +class _RoundSelectorState extends ConsumerState<_RoundSelectorMenu> { + final currentRoundKey = GlobalKey(); -class _IOSTournamentAndRoundSelector extends ConsumerWidget { - final BroadcastTournamentId tournamentId; - final BroadcastRoundId roundId; - final void Function(BroadcastTournamentId) setTournamentId; - final void Function(BroadcastRoundId) setRoundId; + @override + Widget build(BuildContext context) { + // Scroll to the current round + WidgetsBinding.instance.addPostFrameCallback((_) { + if (currentRoundKey.currentContext != null) { + Scrollable.ensureVisible( + currentRoundKey.currentContext!, + alignment: 0.5, + ); + } + }); - const _IOSTournamentAndRoundSelector({ - required this.tournamentId, - required this.roundId, + return BottomSheetScrollableContainer( + scrollController: widget.scrollController, + children: [ + for (final round in widget.rounds) + PlatformListTile( + key: round.id == widget.selectedRoundId ? currentRoundKey : null, + selected: round.id == widget.selectedRoundId, + title: Text(round.name), + trailing: switch (round.status) { + RoundStatus.finished => Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text(_dateFormat.format(round.startsAt!)), + const SizedBox(width: 5.0), + Icon(Icons.check, color: context.lichessColors.good), + ], + ), + RoundStatus.live => Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text(_dateFormat.format(round.startsAt!)), + const SizedBox(width: 5.0), + Icon(Icons.circle, color: context.lichessColors.error), + ], + ), + RoundStatus.upcoming => Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text(_dateFormat.format(round.startsAt!)), + const SizedBox(width: 5.0), + const Icon(Icons.calendar_month, color: Colors.grey), + ], + ), + }, + onTap: () { + widget.setRoundId(round.id); + Navigator.of(context).pop(); + }, + ), + ], + ); + } +} + +class _TournamentSelectorMenu extends ConsumerStatefulWidget { + const _TournamentSelectorMenu({ + required this.tournament, + required this.group, + required this.scrollController, required this.setTournamentId, - required this.setRoundId, }); + final BroadcastTournament tournament; + final IList group; + final ScrollController scrollController; + final void Function(BroadcastTournamentId) setTournamentId; + @override - Widget build(BuildContext context, WidgetRef ref) { - final backgroundColor = CupertinoTheme.of(context).barBackgroundColor; - final tournament = ref.watch(broadcastTournamentProvider(tournamentId)); + ConsumerState<_TournamentSelectorMenu> createState() => + _TournamentSelectorState(); +} - return switch (tournament) { - AsyncData(:final value) => +class _TournamentSelectorState extends ConsumerState<_TournamentSelectorMenu> { + final currentTournamentKey = GlobalKey(); - /// It should be replaced with a Flutter toolbar widget once it is implemented. - /// See https://github.com/flutter/flutter/issues/134454 + @override + Widget build(BuildContext context) { + // Scroll to the current tournament + WidgetsBinding.instance.addPostFrameCallback((_) { + if (currentTournamentKey.currentContext != null) { + Scrollable.ensureVisible( + currentTournamentKey.currentContext!, + alignment: 0.5, + ); + } + }); - _wrapWithBackground( - backgroundColor: backgroundColor, - border: _kDefaultToolBarBorder, - child: SafeArea( - top: false, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Row( - spacing: 16.0, - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - if (value.group != null) - Flexible( - child: CupertinoButton.tinted( - child: Text( - value.group! - .firstWhere( - (tournament) => tournament.id == tournamentId, - ) - .name, - overflow: TextOverflow.ellipsis, - ), - onPressed: () { - showChoicePicker( - context, - choices: value.group! - .map((tournament) => tournament.id) - .toList(), - labelBuilder: (tournamentId) => Text( - value.group! - .firstWhere( - (tournament) => - tournament.id == tournamentId, - ) - .name, - ), - selectedItem: tournamentId, - onSelectedItemChanged: (tournamentId) async { - setTournamentId(tournamentId); - final newTournament = await ref.read( - broadcastTournamentProvider(tournamentId) - .future, - ); - setRoundId(newTournament.defaultRoundId); - }, - ); - }, - ), - ), - Flexible( - child: CupertinoButton.tinted( - child: Text( - value.rounds - .firstWhere( - (round) => round.id == roundId, - ) - .name, - overflow: TextOverflow.ellipsis, - ), - onPressed: () { - showChoicePicker( - context, - choices: value.rounds - .map( - (round) => round.id, - ) - .toList(), - labelBuilder: (roundId) => Text( - value.rounds - .firstWhere((round) => round.id == roundId) - .name, - ), - selectedItem: roundId, - onSelectedItemChanged: (roundId) { - setRoundId(roundId); - }, - ); - }, - ), - ), - ], - ), - ), + return BottomSheetScrollableContainer( + scrollController: widget.scrollController, + children: [ + for (final tournament in widget.group) + PlatformListTile( + key: tournament.id == widget.tournament.data.id + ? currentTournamentKey + : null, + selected: tournament.id == widget.tournament.data.id, + title: Text(tournament.name), + onTap: () { + widget.setTournamentId(tournament.id); + Navigator.of(context).pop(); + }, ), - ), - AsyncError(:final error) => Center(child: Text(error.toString())), - _ => const SizedBox.shrink(), - }; + ], + ); } } diff --git a/lib/src/view/broadcast/dropdown_menu.dart b/lib/src/view/broadcast/dropdown_menu.dart deleted file mode 100644 index d0808de144..0000000000 --- a/lib/src/view/broadcast/dropdown_menu.dart +++ /dev/null @@ -1,1377 +0,0 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -// ignore_for_file: deprecated_member_use - -import 'dart:math' as math; - -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter/services.dart'; - -// Examples can assume: -// late BuildContext context; -// late FocusNode myFocusNode; - -/// A callback function that returns the list of the items that matches the -/// current applied filter. -/// -/// Used by [DropdownMenu.filterCallback]. -typedef FilterCallback = List> Function( - List> entries, - String filter, -); - -/// A callback function that returns the index of the item that matches the -/// current contents of a text field. -/// -/// If a match doesn't exist then null must be returned. -/// -/// Used by [DropdownMenu.searchCallback]. -typedef SearchCallback = int? Function( - List> entries, - String query, -); - -const double _kMinimumWidth = 112.0; - -const double _kDefaultHorizontalPadding = 12.0; - -/// Defines a [DropdownMenu] menu button that represents one item view in the menu. -/// -/// See also: -/// -/// * [DropdownMenu] -class DropdownMenuEntry { - /// Creates an entry that is used with [DropdownMenu.dropdownMenuEntries]. - const DropdownMenuEntry({ - required this.value, - required this.label, - this.labelWidget, - this.leadingIcon, - this.trailingIcon, - this.enabled = true, - this.style, - }); - - /// the value used to identify the entry. - /// - /// This value must be unique across all entries in a [DropdownMenu]. - final T value; - - /// The label displayed in the center of the menu item. - final String label; - - /// Overrides the default label widget which is `Text(label)`. - /// - /// {@tool dartpad} - /// This sample shows how to override the default label [Text] - /// widget with one that forces the menu entry to appear on one line - /// by specifying [Text.maxLines] and [Text.overflow]. - /// - /// ** See code in examples/api/lib/material/dropdown_menu/dropdown_menu_entry_label_widget.0.dart ** - /// {@end-tool} - final Widget? labelWidget; - - /// An optional icon to display before the label. - final Widget? leadingIcon; - - /// An optional icon to display after the label. - final Widget? trailingIcon; - - /// Whether the menu item is enabled or disabled. - /// - /// The default value is true. If true, the [DropdownMenuEntry.label] will be filled - /// out in the text field of the [DropdownMenu] when this entry is clicked; otherwise, - /// this entry is disabled. - final bool enabled; - - /// Customizes this menu item's appearance. - /// - /// Null by default. - final ButtonStyle? style; -} - -/// A dropdown menu that can be opened from a [TextField]. The selected -/// menu item is displayed in that field. -/// -/// This widget is used to help people make a choice from a menu and put the -/// selected item into the text input field. People can also filter the list based -/// on the text input or search one item in the menu list. -/// -/// The menu is composed of a list of [DropdownMenuEntry]s. People can provide information, -/// such as: label, leading icon or trailing icon for each entry. The [TextField] -/// will be updated based on the selection from the menu entries. The text field -/// will stay empty if the selected entry is disabled. -/// -/// When the dropdown menu has focus, it can be traversed by pressing the up or down key. -/// During the process, the corresponding item will be highlighted and -/// the text field will be updated. Disabled items will be skipped during traversal. -/// -/// The menu can be scrollable if not all items in the list are displayed at once. -/// -/// {@tool dartpad} -/// This sample shows how to display outlined [DropdownMenu] and filled [DropdownMenu]. -/// -/// ** See code in examples/api/lib/material/dropdown_menu/dropdown_menu.0.dart ** -/// {@end-tool} -/// -/// See also: -/// -/// * [MenuAnchor], which is a widget used to mark the "anchor" for a set of submenus. -/// The [DropdownMenu] uses a [TextField] as the "anchor". -/// * [TextField], which is a text input widget that uses an [InputDecoration]. -/// * [DropdownMenuEntry], which is used to build the [MenuItemButton] in the [DropdownMenu] list. -class DropdownMenu extends StatefulWidget { - /// Creates a const [DropdownMenu]. - /// - /// The leading and trailing icons in the text field can be customized by using - /// [leadingIcon], [trailingIcon] and [selectedTrailingIcon] properties. They are - /// passed down to the [InputDecoration] properties, and will override values - /// in the [InputDecoration.prefixIcon] and [InputDecoration.suffixIcon]. - /// - /// Except leading and trailing icons, the text field can be configured by the - /// [InputDecorationTheme] property. The menu can be configured by the [menuStyle]. - const DropdownMenu({ - super.key, - this.enabled = true, - this.width, - this.menuHeight, - this.leadingIcon, - this.trailingIcon, - this.label, - this.hintText, - this.helperText, - this.errorText, - this.selectedTrailingIcon, - this.enableFilter = false, - this.enableSearch = true, - this.keyboardType, - this.textStyle, - this.textAlign = TextAlign.start, - this.inputDecorationTheme, - this.menuStyle, - this.controller, - this.initialSelection, - this.onSelected, - this.focusNode, - this.requestFocusOnTap, - this.expandedInsets, - this.filterCallback, - this.searchCallback, - this.alignmentOffset, - required this.dropdownMenuEntries, - this.inputFormatters, - }) : assert(filterCallback == null || enableFilter); - - /// Determine if the [DropdownMenu] is enabled. - /// - /// Defaults to true. - /// - /// {@tool dartpad} - /// This sample demonstrates how the [enabled] and [requestFocusOnTap] properties - /// affect the textfield's hover cursor. - /// - /// ** See code in examples/api/lib/material/dropdown_menu/dropdown_menu.2.dart ** - /// {@end-tool} - final bool enabled; - - /// Determine the width of the [DropdownMenu]. - /// - /// If this is null, the width of the [DropdownMenu] will be the same as the width of the widest - /// menu item plus the width of the leading/trailing icon. - final double? width; - - /// Determine the height of the menu. - /// - /// If this is null, the menu will display as many items as possible on the screen. - final double? menuHeight; - - /// An optional Icon at the front of the text input field. - /// - /// Defaults to null. If this is not null, the menu items will have extra paddings to be aligned - /// with the text in the text field. - final Widget? leadingIcon; - - /// An optional icon at the end of the text field. - /// - /// Defaults to an [Icon] with [Icons.arrow_drop_down]. - final Widget? trailingIcon; - - /// Optional widget that describes the input field. - /// - /// When the input field is empty and unfocused, the label is displayed on - /// top of the input field (i.e., at the same location on the screen where - /// text may be entered in the input field). When the input field receives - /// focus (or if the field is non-empty), the label moves above, either - /// vertically adjacent to, or to the center of the input field. - /// - /// Defaults to null. - final Widget? label; - - /// Text that suggests what sort of input the field accepts. - /// - /// Defaults to null; - final String? hintText; - - /// Text that provides context about the [DropdownMenu]'s value, such - /// as how the value will be used. - /// - /// If non-null, the text is displayed below the input field, in - /// the same location as [errorText]. If a non-null [errorText] value is - /// specified then the helper text is not shown. - /// - /// Defaults to null; - /// - /// See also: - /// - /// * [InputDecoration.helperText], which is the text that provides context about the [InputDecorator.child]'s value. - final String? helperText; - - /// Text that appears below the input field and the border to show the error message. - /// - /// If non-null, the border's color animates to red and the [helperText] is not shown. - /// - /// Defaults to null; - /// - /// See also: - /// - /// * [InputDecoration.errorText], which is the text that appears below the [InputDecorator.child] and the border. - final String? errorText; - - /// An optional icon at the end of the text field to indicate that the text - /// field is pressed. - /// - /// Defaults to an [Icon] with [Icons.arrow_drop_up]. - final Widget? selectedTrailingIcon; - - /// Determine if the menu list can be filtered by the text input. - /// - /// Defaults to false. - final bool enableFilter; - - /// Determine if the first item that matches the text input can be highlighted. - /// - /// Defaults to true as the search function could be commonly used. - final bool enableSearch; - - /// The type of keyboard to use for editing the text. - /// - /// Defaults to [TextInputType.text]. - final TextInputType? keyboardType; - - /// The text style for the [TextField] of the [DropdownMenu]; - /// - /// Defaults to the overall theme's [TextTheme.bodyLarge] - /// if the dropdown menu theme's value is null. - final TextStyle? textStyle; - - /// The text align for the [TextField] of the [DropdownMenu]. - /// - /// Defaults to [TextAlign.start]. - final TextAlign textAlign; - - /// Defines the default appearance of [InputDecoration] to show around the text field. - /// - /// By default, shows a outlined text field. - final InputDecorationTheme? inputDecorationTheme; - - /// The [MenuStyle] that defines the visual attributes of the menu. - /// - /// The default width of the menu is set to the width of the text field. - final MenuStyle? menuStyle; - - /// Controls the text being edited or selected in the menu. - /// - /// If null, this widget will create its own [TextEditingController]. - final TextEditingController? controller; - - /// The value used to for an initial selection. - /// - /// Defaults to null. - final T? initialSelection; - - /// The callback is called when a selection is made. - /// - /// Defaults to null. If null, only the text field is updated. - final ValueChanged? onSelected; - - /// Defines the keyboard focus for this widget. - /// - /// The [focusNode] is a long-lived object that's typically managed by a - /// [StatefulWidget] parent. See [FocusNode] for more information. - /// - /// To give the keyboard focus to this widget, provide a [focusNode] and then - /// use the current [FocusScope] to request the focus: - /// - /// ```dart - /// FocusScope.of(context).requestFocus(myFocusNode); - /// ``` - /// - /// This happens automatically when the widget is tapped. - /// - /// To be notified when the widget gains or loses the focus, add a listener - /// to the [focusNode]: - /// - /// ```dart - /// myFocusNode.addListener(() { print(myFocusNode.hasFocus); }); - /// ``` - /// - /// If null, this widget will create its own [FocusNode]. - /// - /// ## Keyboard - /// - /// Requesting the focus will typically cause the keyboard to be shown - /// if it's not showing already. - /// - /// On Android, the user can hide the keyboard - without changing the focus - - /// with the system back button. They can restore the keyboard's visibility - /// by tapping on a text field. The user might hide the keyboard and - /// switch to a physical keyboard, or they might just need to get it - /// out of the way for a moment, to expose something it's - /// obscuring. In this case requesting the focus again will not - /// cause the focus to change, and will not make the keyboard visible. - /// - /// If this is non-null, the behaviour of [requestFocusOnTap] is overridden - /// by the [FocusNode.canRequestFocus] property. - final FocusNode? focusNode; - - /// Determine if the dropdown button requests focus and the on-screen virtual - /// keyboard is shown in response to a touch event. - /// - /// Ignored if a [focusNode] is explicitly provided (in which case, - /// [FocusNode.canRequestFocus] controls the behavior). - /// - /// Defaults to null, which enables platform-specific behavior: - /// - /// * On mobile platforms, acts as if set to false; tapping on the text - /// field and opening the menu will not cause a focus request and the - /// virtual keyboard will not appear. - /// - /// * On desktop platforms, acts as if set to true; the dropdown takes the - /// focus when activated. - /// - /// Set this to true or false explicitly to override the default behavior. - /// - /// {@tool dartpad} - /// This sample demonstrates how the [enabled] and [requestFocusOnTap] properties - /// affect the textfield's hover cursor. - /// - /// ** See code in examples/api/lib/material/dropdown_menu/dropdown_menu.2.dart ** - /// {@end-tool} - final bool? requestFocusOnTap; - - /// Descriptions of the menu items in the [DropdownMenu]. - /// - /// This is a required parameter. It is recommended that at least one [DropdownMenuEntry] - /// is provided. If this is an empty list, the menu will be empty and only - /// contain space for padding. - final List> dropdownMenuEntries; - - /// Defines the menu text field's width to be equal to its parent's width - /// plus the horizontal width of the specified insets. - /// - /// If this property is null, the width of the text field will be determined - /// by the width of menu items or [DropdownMenu.width]. If this property is not null, - /// the text field's width will match the parent's width plus the specified insets. - /// If the value of this property is [EdgeInsets.zero], the width of the text field will be the same - /// as its parent's width. - /// - /// The [expandedInsets]' top and bottom are ignored, only its left and right - /// properties are used. - /// - /// Defaults to null. - final EdgeInsetsGeometry? expandedInsets; - - /// When [DropdownMenu.enableFilter] is true, this callback is used to - /// compute the list of filtered items. - /// - /// {@tool snippet} - /// - /// In this example the `filterCallback` returns the items that contains the - /// trimmed query. - /// - /// ```dart - /// DropdownMenu( - /// enableFilter: true, - /// filterCallback: (List> entries, String filter) { - /// final String trimmedFilter = filter.trim().toLowerCase(); - /// if (trimmedFilter.isEmpty) { - /// return entries; - /// } - /// - /// return entries - /// .where((DropdownMenuEntry entry) => - /// entry.label.toLowerCase().contains(trimmedFilter), - /// ) - /// .toList(); - /// }, - /// dropdownMenuEntries: const >[], - /// ) - /// ``` - /// {@end-tool} - /// - /// Defaults to null. If this parameter is null and the - /// [DropdownMenu.enableFilter] property is set to true, the default behavior - /// will return a filtered list. The filtered list will contain items - /// that match the text provided by the input field, with a case-insensitive - /// comparison. When this is not null, `enableFilter` must be set to true. - final FilterCallback? filterCallback; - - /// When [DropdownMenu.enableSearch] is true, this callback is used to compute - /// the index of the search result to be highlighted. - /// - /// {@tool snippet} - /// - /// In this example the `searchCallback` returns the index of the search result - /// that exactly matches the query. - /// - /// ```dart - /// DropdownMenu( - /// searchCallback: (List> entries, String query) { - /// if (query.isEmpty) { - /// return null; - /// } - /// final int index = entries.indexWhere((DropdownMenuEntry entry) => entry.label == query); - /// - /// return index != -1 ? index : null; - /// }, - /// dropdownMenuEntries: const >[], - /// ) - /// ``` - /// {@end-tool} - /// - /// Defaults to null. If this is null and [DropdownMenu.enableSearch] is true, - /// the default function will return the index of the first matching result - /// which contains the contents of the text input field. - final SearchCallback? searchCallback; - - /// Optional input validation and formatting overrides. - /// - /// Formatters are run in the provided order when the user changes the text - /// this widget contains. When this parameter changes, the new formatters will - /// not be applied until the next time the user inserts or deletes text. - /// Formatters don't run when the text is changed - /// programmatically via [controller]. - /// - /// See also: - /// - /// * [TextEditingController], which implements the [Listenable] interface - /// and notifies its listeners on [TextEditingValue] changes. - final List? inputFormatters; - - /// {@macro flutter.material.MenuAnchor.alignmentOffset} - final Offset? alignmentOffset; - - @override - State> createState() => _DropdownMenuState(); -} - -class _DropdownMenuState extends State> { - final GlobalKey _anchorKey = GlobalKey(); - final GlobalKey _leadingKey = GlobalKey(); - late List buttonItemKeys; - final MenuController _controller = MenuController(); - bool _enableFilter = false; - late bool _enableSearch; - late List> filteredEntries; - List? _initialMenu; - int? currentHighlight; - double? leadingPadding; - bool _menuHasEnabledItem = false; - TextEditingController? _localTextEditingController; - - TextEditingValue get _initialTextEditingValue { - for (final DropdownMenuEntry entry in filteredEntries) { - if (entry.value == widget.initialSelection) { - return TextEditingValue( - text: entry.label, - selection: TextSelection.collapsed(offset: entry.label.length), - ); - } - } - return TextEditingValue.empty; - } - - @override - void initState() { - super.initState(); - if (widget.controller != null) { - _localTextEditingController = widget.controller; - } else { - _localTextEditingController = TextEditingController(); - } - _enableSearch = widget.enableSearch; - filteredEntries = widget.dropdownMenuEntries; - buttonItemKeys = List.generate( - filteredEntries.length, - (int index) => GlobalKey(), - ); - _menuHasEnabledItem = - filteredEntries.any((DropdownMenuEntry entry) => entry.enabled); - _localTextEditingController?.value = _initialTextEditingValue; - - refreshLeadingPadding(); - } - - @override - void dispose() { - if (widget.controller == null) { - _localTextEditingController?.dispose(); - _localTextEditingController = null; - } - super.dispose(); - } - - @override - void didUpdateWidget(DropdownMenu oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.controller != widget.controller) { - if (widget.controller != null) { - _localTextEditingController?.dispose(); - } - _localTextEditingController = - widget.controller ?? TextEditingController(); - } - if (oldWidget.enableFilter != widget.enableFilter) { - if (!widget.enableFilter) { - _enableFilter = false; - } - } - if (oldWidget.enableSearch != widget.enableSearch) { - if (!widget.enableSearch) { - _enableSearch = widget.enableSearch; - currentHighlight = null; - } - } - if (oldWidget.dropdownMenuEntries != widget.dropdownMenuEntries) { - currentHighlight = null; - filteredEntries = widget.dropdownMenuEntries; - buttonItemKeys = List.generate( - filteredEntries.length, - (int index) => GlobalKey(), - ); - _menuHasEnabledItem = - filteredEntries.any((DropdownMenuEntry entry) => entry.enabled); - // If the text field content matches one of the new entries do not rematch the initialSelection. - final bool isCurrentSelectionValid = filteredEntries.any( - (DropdownMenuEntry entry) => - entry.label == _localTextEditingController?.text, - ); - if (!isCurrentSelectionValid) { - _localTextEditingController?.value = _initialTextEditingValue; - } - } - if (oldWidget.leadingIcon != widget.leadingIcon) { - refreshLeadingPadding(); - } - if (oldWidget.initialSelection != widget.initialSelection) { - _localTextEditingController?.value = _initialTextEditingValue; - } - } - - bool canRequestFocus() { - return widget.focusNode?.canRequestFocus ?? - widget.requestFocusOnTap ?? - switch (Theme.of(context).platform) { - TargetPlatform.iOS || - TargetPlatform.android || - TargetPlatform.fuchsia => - false, - TargetPlatform.macOS || - TargetPlatform.linux || - TargetPlatform.windows => - true, - }; - } - - void refreshLeadingPadding() { - WidgetsBinding.instance.addPostFrameCallback( - (_) { - if (!mounted) { - return; - } - setState(() { - leadingPadding = getWidth(_leadingKey); - }); - }, - debugLabel: 'DropdownMenu.refreshLeadingPadding', - ); - } - - void scrollToHighlight() { - WidgetsBinding.instance.addPostFrameCallback( - (_) { - final BuildContext? highlightContext = - buttonItemKeys[currentHighlight!].currentContext; - if (highlightContext != null) { - Scrollable.of(highlightContext) - .position - .ensureVisible(highlightContext.findRenderObject()!); - } - }, - debugLabel: 'DropdownMenu.scrollToHighlight', - ); - } - - double? getWidth(GlobalKey key) { - final BuildContext? context = key.currentContext; - if (context != null) { - final RenderBox box = context.findRenderObject()! as RenderBox; - return box.hasSize ? box.size.width : null; - } - return null; - } - - List> filter( - List> entries, - TextEditingController textEditingController, - ) { - final String filterText = textEditingController.text.toLowerCase(); - return entries - .where( - (DropdownMenuEntry entry) => - entry.label.toLowerCase().contains(filterText), - ) - .toList(); - } - - bool _shouldUpdateCurrentHighlight(List> entries) { - final String searchText = - _localTextEditingController!.value.text.toLowerCase(); - if (searchText.isEmpty) { - return true; - } - - // When `entries` are filtered by filter algorithm, currentHighlight may exceed the valid range of `entries` and should be updated. - if (currentHighlight == null || currentHighlight! >= entries.length) { - return true; - } - - if (entries[currentHighlight!].label.toLowerCase().contains(searchText)) { - return false; - } - - return true; - } - - int? search( - List> entries, - TextEditingController textEditingController, - ) { - final String searchText = textEditingController.value.text.toLowerCase(); - if (searchText.isEmpty) { - return null; - } - - final int index = entries.indexWhere( - (DropdownMenuEntry entry) => - entry.label.toLowerCase().contains(searchText), - ); - - return index != -1 ? index : null; - } - - List _buildButtons( - List> filteredEntries, - TextDirection textDirection, { - int? focusedIndex, - bool enableScrollToHighlight = true, - }) { - final List result = []; - for (int i = 0; i < filteredEntries.length; i++) { - final DropdownMenuEntry entry = filteredEntries[i]; - - // By default, when the text field has a leading icon but a menu entry doesn't - // have one, the label of the entry should have extra padding to be aligned - // with the text in the text input field. When both the text field and the - // menu entry have leading icons, the menu entry should remove the extra - // paddings so its leading icon will be aligned with the leading icon of - // the text field. - final double padding = entry.leadingIcon == null - ? (leadingPadding ?? _kDefaultHorizontalPadding) - : _kDefaultHorizontalPadding; - ButtonStyle effectiveStyle = entry.style ?? - switch (textDirection) { - TextDirection.rtl => MenuItemButton.styleFrom( - padding: EdgeInsets.only( - left: _kDefaultHorizontalPadding, - right: padding, - ), - ), - TextDirection.ltr => MenuItemButton.styleFrom( - padding: EdgeInsets.only( - left: padding, - right: _kDefaultHorizontalPadding, - ), - ), - }; - - final ButtonStyle? themeStyle = MenuButtonTheme.of(context).style; - - final WidgetStateProperty? effectiveForegroundColor = - entry.style?.foregroundColor ?? themeStyle?.foregroundColor; - final WidgetStateProperty? effectiveIconColor = - entry.style?.iconColor ?? themeStyle?.iconColor; - final WidgetStateProperty? effectiveOverlayColor = - entry.style?.overlayColor ?? themeStyle?.overlayColor; - final WidgetStateProperty? effectiveBackgroundColor = - entry.style?.backgroundColor ?? themeStyle?.backgroundColor; - - // Simulate the focused state because the text field should always be focused - // during traversal. Include potential MenuItemButton theme in the focus - // simulation for all colors in the theme. - if (entry.enabled && i == focusedIndex) { - // Query the Material 3 default style. - // TODO(bleroux): replace once a standard way for accessing defaults will be defined. - // See: https://github.com/flutter/flutter/issues/130135. - final ButtonStyle defaultStyle = - const MenuItemButton().defaultStyleOf(context); - - Color? resolveFocusedColor( - WidgetStateProperty? colorStateProperty, - ) { - return colorStateProperty - ?.resolve({WidgetState.focused}); - } - - final Color focusedForegroundColor = resolveFocusedColor( - effectiveForegroundColor ?? defaultStyle.foregroundColor!, - )!; - final Color focusedIconColor = - resolveFocusedColor(effectiveIconColor ?? defaultStyle.iconColor!)!; - final Color focusedOverlayColor = resolveFocusedColor( - effectiveOverlayColor ?? defaultStyle.overlayColor!, - )!; - // For the background color we can't rely on the default style which is transparent. - // Defaults to onSurface.withOpacity(0.12). - final Color focusedBackgroundColor = - resolveFocusedColor(effectiveBackgroundColor) ?? - Theme.of(context).colorScheme.onSurface.withOpacity(0.12); - - effectiveStyle = effectiveStyle.copyWith( - backgroundColor: - WidgetStatePropertyAll(focusedBackgroundColor), - foregroundColor: - WidgetStatePropertyAll(focusedForegroundColor), - iconColor: WidgetStatePropertyAll(focusedIconColor), - overlayColor: WidgetStatePropertyAll(focusedOverlayColor), - ); - } else { - effectiveStyle = effectiveStyle.copyWith( - backgroundColor: effectiveBackgroundColor, - foregroundColor: effectiveForegroundColor, - iconColor: effectiveIconColor, - overlayColor: effectiveOverlayColor, - ); - } - - Widget label = entry.labelWidget ?? Text(entry.label); - if (widget.width != null) { - final double horizontalPadding = padding + _kDefaultHorizontalPadding; - label = ConstrainedBox( - constraints: - BoxConstraints(maxWidth: widget.width! - horizontalPadding), - child: label, - ); - } - - final Widget menuItemButton = MenuItemButton( - key: enableScrollToHighlight ? buttonItemKeys[i] : null, - style: effectiveStyle, - leadingIcon: entry.leadingIcon, - trailingIcon: entry.trailingIcon, - onPressed: entry.enabled && widget.enabled - ? () { - _localTextEditingController?.value = TextEditingValue( - text: entry.label, - selection: - TextSelection.collapsed(offset: entry.label.length), - ); - currentHighlight = widget.enableSearch ? i : null; - widget.onSelected?.call(entry.value); - _enableFilter = false; - } - : null, - requestFocusOnHover: false, - child: label, - ); - result.add(menuItemButton); - } - - return result; - } - - void handleUpKeyInvoke(_) { - setState(() { - if (!widget.enabled || !_menuHasEnabledItem || !_controller.isOpen) { - return; - } - _enableFilter = false; - _enableSearch = false; - currentHighlight ??= 0; - currentHighlight = (currentHighlight! - 1) % filteredEntries.length; - while (!filteredEntries[currentHighlight!].enabled) { - currentHighlight = (currentHighlight! - 1) % filteredEntries.length; - } - final String currentLabel = filteredEntries[currentHighlight!].label; - _localTextEditingController?.value = TextEditingValue( - text: currentLabel, - selection: TextSelection.collapsed(offset: currentLabel.length), - ); - }); - } - - void handleDownKeyInvoke(_) { - setState(() { - if (!widget.enabled || !_menuHasEnabledItem || !_controller.isOpen) { - return; - } - _enableFilter = false; - _enableSearch = false; - currentHighlight ??= -1; - currentHighlight = (currentHighlight! + 1) % filteredEntries.length; - while (!filteredEntries[currentHighlight!].enabled) { - currentHighlight = (currentHighlight! + 1) % filteredEntries.length; - } - final String currentLabel = filteredEntries[currentHighlight!].label; - _localTextEditingController?.value = TextEditingValue( - text: currentLabel, - selection: TextSelection.collapsed(offset: currentLabel.length), - ); - }); - } - - void handlePressed(MenuController controller) { - if (controller.isOpen) { - currentHighlight = null; - controller.close(); - } else { - // close to open - if (_localTextEditingController!.text.isNotEmpty) { - _enableFilter = false; - } - controller.open(); - } - setState(() {}); - } - - @override - Widget build(BuildContext context) { - final TextDirection textDirection = Directionality.of(context); - _initialMenu ??= _buildButtons( - widget.dropdownMenuEntries, - textDirection, - enableScrollToHighlight: false, - ); - final DropdownMenuThemeData theme = DropdownMenuTheme.of(context); - final DropdownMenuThemeData defaults = _DropdownMenuDefaultsM3(context); - - if (_enableFilter) { - filteredEntries = widget.filterCallback - ?.call(filteredEntries, _localTextEditingController!.text) ?? - filter(widget.dropdownMenuEntries, _localTextEditingController!); - } else { - filteredEntries = widget.dropdownMenuEntries; - } - _menuHasEnabledItem = - filteredEntries.any((DropdownMenuEntry entry) => entry.enabled); - - if (_enableSearch) { - if (widget.searchCallback != null) { - currentHighlight = widget.searchCallback!( - filteredEntries, - _localTextEditingController!.text, - ); - } else { - final bool shouldUpdateCurrentHighlight = - _shouldUpdateCurrentHighlight(filteredEntries); - if (shouldUpdateCurrentHighlight) { - currentHighlight = - search(filteredEntries, _localTextEditingController!); - } - } - if (currentHighlight != null) { - scrollToHighlight(); - } - } - - final List menu = _buildButtons( - filteredEntries, - textDirection, - focusedIndex: currentHighlight, - ); - - final TextStyle? effectiveTextStyle = - widget.textStyle ?? theme.textStyle ?? defaults.textStyle; - - MenuStyle? effectiveMenuStyle = - widget.menuStyle ?? theme.menuStyle ?? defaults.menuStyle!; - - final double? anchorWidth = getWidth(_anchorKey); - if (widget.width != null) { - effectiveMenuStyle = effectiveMenuStyle.copyWith( - minimumSize: WidgetStatePropertyAll(Size(widget.width!, 0.0)), - ); - } else if (anchorWidth != null) { - effectiveMenuStyle = effectiveMenuStyle.copyWith( - minimumSize: WidgetStatePropertyAll(Size(anchorWidth, 0.0)), - ); - } - - if (widget.menuHeight != null) { - effectiveMenuStyle = effectiveMenuStyle.copyWith( - maximumSize: WidgetStatePropertyAll( - Size(double.infinity, widget.menuHeight!), - ), - ); - } - final InputDecorationTheme effectiveInputDecorationTheme = - widget.inputDecorationTheme ?? - theme.inputDecorationTheme ?? - defaults.inputDecorationTheme!; - - final MouseCursor? effectiveMouseCursor = switch (widget.enabled) { - true => - canRequestFocus() ? SystemMouseCursors.text : SystemMouseCursors.click, - false => null, - }; - - Widget menuAnchor = MenuAnchor( - style: effectiveMenuStyle, - alignmentOffset: widget.alignmentOffset, - controller: _controller, - menuChildren: menu, - crossAxisUnconstrained: false, - builder: - (BuildContext context, MenuController controller, Widget? child) { - assert(_initialMenu != null); - final Widget trailingButton = Padding( - padding: const EdgeInsets.all(4.0), - child: IconButton( - isSelected: controller.isOpen, - icon: widget.trailingIcon ?? const Icon(Icons.arrow_drop_down), - selectedIcon: - widget.selectedTrailingIcon ?? const Icon(Icons.arrow_drop_up), - onPressed: !widget.enabled - ? null - : () { - handlePressed(controller); - }, - ), - ); - - final Widget leadingButton = Padding( - padding: const EdgeInsets.all(8.0), - child: widget.leadingIcon ?? const SizedBox.shrink(), - ); - - final Widget textField = TextField( - key: _anchorKey, - enabled: widget.enabled, - mouseCursor: effectiveMouseCursor, - focusNode: widget.focusNode, - canRequestFocus: canRequestFocus(), - enableInteractiveSelection: canRequestFocus(), - readOnly: !canRequestFocus(), - keyboardType: widget.keyboardType, - textAlign: widget.textAlign, - textAlignVertical: TextAlignVertical.center, - style: effectiveTextStyle, - controller: _localTextEditingController, - onEditingComplete: () { - if (currentHighlight != null) { - final DropdownMenuEntry entry = - filteredEntries[currentHighlight!]; - if (entry.enabled) { - _localTextEditingController?.value = TextEditingValue( - text: entry.label, - selection: - TextSelection.collapsed(offset: entry.label.length), - ); - widget.onSelected?.call(entry.value); - } - } else { - widget.onSelected?.call(null); - } - if (!widget.enableSearch) { - currentHighlight = null; - } - controller.close(); - }, - onTap: !widget.enabled - ? null - : () { - handlePressed(controller); - }, - onChanged: (String text) { - controller.open(); - setState(() { - filteredEntries = widget.dropdownMenuEntries; - _enableFilter = widget.enableFilter; - _enableSearch = widget.enableSearch; - }); - }, - inputFormatters: widget.inputFormatters, - decoration: InputDecoration( - label: widget.label, - hintText: widget.hintText, - helperText: widget.helperText, - errorText: widget.errorText, - prefixIcon: widget.leadingIcon != null - ? SizedBox(key: _leadingKey, child: widget.leadingIcon) - : null, - suffixIcon: trailingButton, - ).applyDefaults(effectiveInputDecorationTheme), - ); - - if (widget.expandedInsets != null) { - // If [expandedInsets] is not null, the width of the text field should depend - // on its parent width. So we don't need to use `_DropdownMenuBody` to - // calculate the children's width. - return textField; - } - - return Shortcuts( - shortcuts: const { - SingleActivator(LogicalKeyboardKey.arrowLeft): - ExtendSelectionByCharacterIntent( - forward: false, - collapseSelection: true, - ), - SingleActivator(LogicalKeyboardKey.arrowRight): - ExtendSelectionByCharacterIntent( - forward: true, - collapseSelection: true, - ), - SingleActivator(LogicalKeyboardKey.arrowUp): _ArrowUpIntent(), - SingleActivator(LogicalKeyboardKey.arrowDown): _ArrowDownIntent(), - }, - child: _DropdownMenuBody( - width: widget.width, - children: [ - textField, - ..._initialMenu!.map( - (Widget item) => - ExcludeFocus(excluding: !controller.isOpen, child: item), - ), - trailingButton, - leadingButton, - ], - ), - ); - }, - ); - - if (widget.expandedInsets case final EdgeInsetsGeometry padding) { - menuAnchor = Padding( - // Clamp the top and bottom padding to 0. - padding: padding.clamp( - EdgeInsets.zero, - const EdgeInsets.only( - left: double.infinity, - right: double.infinity, - ).add( - const EdgeInsetsDirectional.only( - end: double.infinity, - start: double.infinity, - ), - ), - ), - child: menuAnchor, - ); - } - - return Actions( - actions: >{ - _ArrowUpIntent: CallbackAction<_ArrowUpIntent>( - onInvoke: handleUpKeyInvoke, - ), - _ArrowDownIntent: CallbackAction<_ArrowDownIntent>( - onInvoke: handleDownKeyInvoke, - ), - }, - child: menuAnchor, - ); - } -} - -// `DropdownMenu` dispatches these private intents on arrow up/down keys. -// They are needed instead of the typical `DirectionalFocusIntent`s because -// `DropdownMenu` does not really navigate the focus tree upon arrow up/down -// keys: the focus stays on the text field and the menu items are given fake -// highlights as if they are focused. Using `DirectionalFocusIntent`s will cause -// the action to be processed by `EditableText`. -class _ArrowUpIntent extends Intent { - const _ArrowUpIntent(); -} - -class _ArrowDownIntent extends Intent { - const _ArrowDownIntent(); -} - -class _DropdownMenuBody extends MultiChildRenderObjectWidget { - const _DropdownMenuBody({ - super.children, - this.width, - }); - - final double? width; - - @override - _RenderDropdownMenuBody createRenderObject(BuildContext context) { - return _RenderDropdownMenuBody( - width: width, - ); - } - - @override - void updateRenderObject( - BuildContext context, - _RenderDropdownMenuBody renderObject, - ) { - renderObject.width = width; - } -} - -class _DropdownMenuBodyParentData extends ContainerBoxParentData {} - -class _RenderDropdownMenuBody extends RenderBox - with - ContainerRenderObjectMixin, - RenderBoxContainerDefaultsMixin { - _RenderDropdownMenuBody({ - double? width, - }) : _width = width; - - double? get width => _width; - double? _width; - set width(double? value) { - if (_width == value) { - return; - } - _width = value; - markNeedsLayout(); - } - - @override - void setupParentData(RenderBox child) { - if (child.parentData is! _DropdownMenuBodyParentData) { - child.parentData = _DropdownMenuBodyParentData(); - } - } - - @override - void performLayout() { - final BoxConstraints constraints = this.constraints; - double maxWidth = 0.0; - double? maxHeight; - RenderBox? child = firstChild; - - final double intrinsicWidth = - width ?? getMaxIntrinsicWidth(constraints.maxHeight); - final double widthConstraint = - math.min(intrinsicWidth, constraints.maxWidth); - final BoxConstraints innerConstraints = BoxConstraints( - maxWidth: widthConstraint, - maxHeight: getMaxIntrinsicHeight(widthConstraint), - ); - while (child != null) { - if (child == firstChild) { - child.layout(innerConstraints, parentUsesSize: true); - maxHeight ??= child.size.height; - final _DropdownMenuBodyParentData childParentData = - child.parentData! as _DropdownMenuBodyParentData; - assert(child.parentData == childParentData); - child = childParentData.nextSibling; - continue; - } - child.layout(innerConstraints, parentUsesSize: true); - final _DropdownMenuBodyParentData childParentData = - child.parentData! as _DropdownMenuBodyParentData; - childParentData.offset = Offset.zero; - maxWidth = math.max(maxWidth, child.size.width); - maxHeight ??= child.size.height; - assert(child.parentData == childParentData); - child = childParentData.nextSibling; - } - - assert(maxHeight != null); - maxWidth = math.max(_kMinimumWidth, maxWidth); - size = constraints.constrain(Size(width ?? maxWidth, maxHeight!)); - } - - @override - void paint(PaintingContext context, Offset offset) { - final RenderBox? child = firstChild; - if (child != null) { - final _DropdownMenuBodyParentData childParentData = - child.parentData! as _DropdownMenuBodyParentData; - context.paintChild(child, offset + childParentData.offset); - } - } - - @override - Size computeDryLayout(BoxConstraints constraints) { - final BoxConstraints constraints = this.constraints; - double maxWidth = 0.0; - double? maxHeight; - RenderBox? child = firstChild; - final double intrinsicWidth = - width ?? getMaxIntrinsicWidth(constraints.maxHeight); - final double widthConstraint = - math.min(intrinsicWidth, constraints.maxWidth); - final BoxConstraints innerConstraints = BoxConstraints( - maxWidth: widthConstraint, - maxHeight: getMaxIntrinsicHeight(widthConstraint), - ); - - while (child != null) { - if (child == firstChild) { - final Size childSize = child.getDryLayout(innerConstraints); - maxHeight ??= childSize.height; - final _DropdownMenuBodyParentData childParentData = - child.parentData! as _DropdownMenuBodyParentData; - assert(child.parentData == childParentData); - child = childParentData.nextSibling; - continue; - } - final Size childSize = child.getDryLayout(innerConstraints); - final _DropdownMenuBodyParentData childParentData = - child.parentData! as _DropdownMenuBodyParentData; - childParentData.offset = Offset.zero; - maxWidth = math.max(maxWidth, childSize.width); - maxHeight ??= childSize.height; - assert(child.parentData == childParentData); - child = childParentData.nextSibling; - } - - assert(maxHeight != null); - maxWidth = math.max(_kMinimumWidth, maxWidth); - return constraints.constrain(Size(width ?? maxWidth, maxHeight!)); - } - - @override - double computeMinIntrinsicWidth(double height) { - RenderBox? child = firstChild; - double width = 0; - while (child != null) { - if (child == firstChild) { - final _DropdownMenuBodyParentData childParentData = - child.parentData! as _DropdownMenuBodyParentData; - child = childParentData.nextSibling; - continue; - } - final double maxIntrinsicWidth = child.getMinIntrinsicWidth(height); - if (child == lastChild) { - width += maxIntrinsicWidth; - } - if (child == childBefore(lastChild!)) { - width += maxIntrinsicWidth; - } - width = math.max(width, maxIntrinsicWidth); - final _DropdownMenuBodyParentData childParentData = - child.parentData! as _DropdownMenuBodyParentData; - child = childParentData.nextSibling; - } - - return math.max(width, _kMinimumWidth); - } - - @override - double computeMaxIntrinsicWidth(double height) { - RenderBox? child = firstChild; - double width = 0; - while (child != null) { - if (child == firstChild) { - final _DropdownMenuBodyParentData childParentData = - child.parentData! as _DropdownMenuBodyParentData; - child = childParentData.nextSibling; - continue; - } - final double maxIntrinsicWidth = child.getMaxIntrinsicWidth(height); - // Add the width of leading Icon. - if (child == lastChild) { - width += maxIntrinsicWidth; - } - // Add the width of trailing Icon. - if (child == childBefore(lastChild!)) { - width += maxIntrinsicWidth; - } - width = math.max(width, maxIntrinsicWidth); - final _DropdownMenuBodyParentData childParentData = - child.parentData! as _DropdownMenuBodyParentData; - child = childParentData.nextSibling; - } - - return math.max(width, _kMinimumWidth); - } - - @override - double computeMinIntrinsicHeight(double width) { - final RenderBox? child = firstChild; - double width = 0; - if (child != null) { - width = math.max(width, child.getMinIntrinsicHeight(width)); - } - return width; - } - - @override - double computeMaxIntrinsicHeight(double width) { - final RenderBox? child = firstChild; - double width = 0; - if (child != null) { - width = math.max(width, child.getMaxIntrinsicHeight(width)); - } - return width; - } - - @override - bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { - final RenderBox? child = firstChild; - if (child != null) { - final _DropdownMenuBodyParentData childParentData = - child.parentData! as _DropdownMenuBodyParentData; - final bool isHit = result.addWithPaintOffset( - offset: childParentData.offset, - position: position, - hitTest: (BoxHitTestResult result, Offset transformed) { - assert(transformed == position - childParentData.offset); - return child.hitTest(result, position: transformed); - }, - ); - if (isHit) { - return true; - } - } - return false; - } -} - -// Hand coded defaults. These will be updated once we have tokens/spec. -class _DropdownMenuDefaultsM3 extends DropdownMenuThemeData { - _DropdownMenuDefaultsM3(this.context); - - final BuildContext context; - late final ThemeData _theme = Theme.of(context); - - @override - TextStyle? get textStyle => _theme.textTheme.bodyLarge; - - @override - MenuStyle get menuStyle { - return const MenuStyle( - minimumSize: WidgetStatePropertyAll(Size(_kMinimumWidth, 0.0)), - maximumSize: WidgetStatePropertyAll(Size.infinite), - visualDensity: VisualDensity.standard, - ); - } - - @override - InputDecorationTheme get inputDecorationTheme { - return const InputDecorationTheme(border: OutlineInputBorder()); - } -} diff --git a/lib/src/view/study/study_bottom_bar.dart b/lib/src/view/study/study_bottom_bar.dart index 43d8bf54cb..ab2de9d8c6 100644 --- a/lib/src/view/study/study_bottom_bar.dart +++ b/lib/src/view/study/study_bottom_bar.dart @@ -249,7 +249,6 @@ class _ChapterButton extends ConsumerWidget { builder: (_) => DraggableScrollableSheet( initialChildSize: 0.6, maxChildSize: 0.6, - minChildSize: 0.0, snap: true, expand: false, builder: (context, scrollController) { diff --git a/lib/src/widgets/list.dart b/lib/src/widgets/list.dart index 6afad801cf..3d16e587b2 100644 --- a/lib/src/widgets/list.dart +++ b/lib/src/widgets/list.dart @@ -353,7 +353,9 @@ class PlatformListTile extends StatelessWidget { child: GestureDetector( onLongPress: onLongPress, child: CupertinoListTile.notched( - backgroundColor: cupertinoBackgroundColor, + backgroundColor: selected == true + ? CupertinoColors.systemGrey4.resolveFrom(context) + : cupertinoBackgroundColor, leading: leading, title: harmonizeCupertinoTitleStyle ? DefaultTextStyle.merge( From f3256fcc9ee447f4d81d8c6de2566cbbe0060cbb Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 4 Dec 2024 16:08:51 +0100 Subject: [PATCH 840/979] Bump version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 0194b6f27b..3b5b3ec1f6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: lichess_mobile description: Lichess mobile app V2 publish_to: "none" -version: 0.13.4+001304 # See README.md for details about versioning +version: 0.13.5+001305 # See README.md for details about versioning environment: sdk: ">=3.5.0 <4.0.0" From 33e633d2594450c10ba2cd46c6686e6b2b78ba91 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 4 Dec 2024 19:30:21 +0100 Subject: [PATCH 841/979] Try to use colors from broadcast images --- .../view/broadcast/broadcast_list_screen.dart | 79 ++++++-- .../broadcast/broadcast_overview_tab.dart | 169 +++++++++--------- .../broadcast/broadcast_round_screen.dart | 6 +- lib/src/view/broadcast/broadcast_tile.dart | 67 ------- lib/src/view/watch/watch_tab_screen.dart | 62 ++++++- lib/src/widgets/bottom_bar.dart | 38 ++-- 6 files changed, 233 insertions(+), 188 deletions(-) delete mode 100644 lib/src/view/broadcast/broadcast_tile.dart diff --git a/lib/src/view/broadcast/broadcast_list_screen.dart b/lib/src/view/broadcast/broadcast_list_screen.dart index cd5b526ce4..d041917523 100644 --- a/lib/src/view/broadcast/broadcast_list_screen.dart +++ b/lib/src/view/broadcast/broadcast_list_screen.dart @@ -1,3 +1,4 @@ +import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; @@ -166,7 +167,7 @@ class _BodyState extends ConsumerState<_Body> { } } -class BroadcastGridItem extends StatelessWidget { +class BroadcastGridItem extends StatefulWidget { final Broadcast broadcast; const BroadcastGridItem({required this.broadcast}); @@ -197,8 +198,44 @@ class BroadcastGridItem extends StatelessWidget { roundToLinkId: const BroadcastRoundId(''), ); + @override + State createState() => _BroadcastGridItemState(); +} + +class _BroadcastGridItemState extends State { + ColorScheme? _colorScheme; + + @override + void initState() { + super.initState(); + + if (widget.broadcast.tour.imageUrl != null) { + _fetchColorScheme(widget.broadcast.tour.imageUrl!); + } + } + + Future _fetchColorScheme(String url) async { + final colorScheme = await ColorScheme.fromImageProvider( + provider: NetworkImage(url), + dynamicSchemeVariant: DynamicSchemeVariant.fidelity, + ); + if (mounted) { + setState(() { + _colorScheme = colorScheme; + }); + } + } + @override Widget build(BuildContext context) { + final titleColor = _colorScheme?.onPrimaryContainer; + final subTitleColor = + _colorScheme?.onPrimaryContainer.withValues(alpha: 0.7) ?? + textShade(context, 0.7); + final red = _colorScheme != null + ? LichessColors.red.harmonizeWith(_colorScheme!.primary) + : LichessColors.red; + return AdaptiveInkWell( borderRadius: BorderRadius.circular(20), onTap: () { @@ -206,24 +243,25 @@ class BroadcastGridItem extends StatelessWidget { context, title: context.l10n.broadcastBroadcasts, rootNavigator: true, - builder: (context) => BroadcastRoundScreen(broadcast: broadcast), + builder: (context) => + BroadcastRoundScreen(broadcast: widget.broadcast), ); }, child: Container( clipBehavior: Clip.hardEdge, decoration: BoxDecoration( borderRadius: BorderRadius.circular(20), - boxShadow: [ + color: _colorScheme?.primaryContainer, + boxShadow: const [ BoxShadow( - color: LichessColors.grey.withValues(alpha: 0.5), - blurRadius: 5, + blurRadius: 3, spreadRadius: 1, ), ], ), foregroundDecoration: BoxDecoration( - border: (broadcast.isLive) - ? Border.all(color: LichessColors.red, width: 2) + border: (widget.broadcast.isLive) + ? Border.all(color: red.withValues(alpha: 0.8), width: 3.0) : Border.all(color: LichessColors.grey), borderRadius: BorderRadius.circular(20), ), @@ -231,32 +269,34 @@ class BroadcastGridItem extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (broadcast.tour.imageUrl != null) + if (widget.broadcast.tour.imageUrl != null) AspectRatio( aspectRatio: 2.0, child: FadeInImage.memoryNetwork( placeholder: transparentImage, - image: broadcast.tour.imageUrl!, + image: widget.broadcast.tour.imageUrl!, ), ) else const DefaultBroadcastImage(aspectRatio: 2.0), const SizedBox(height: 4.0), - if (broadcast.round.startsAt != null || broadcast.isLive) + if (widget.broadcast.round.startsAt != null || + widget.broadcast.isLive) Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text( - _formatDate(broadcast.round.startsAt!), - style: Theme.of(context).textTheme.labelSmall?.copyWith( - color: textShade(context, 0.5), - ), + _formatDate(widget.broadcast.round.startsAt!), + style: TextStyle( + fontSize: 11, + color: subTitleColor, + ), overflow: TextOverflow.ellipsis, maxLines: 1, ), - if (broadcast.isLive) ...[ + if (widget.broadcast.isLive) ...[ const SizedBox(width: 4.0), const Text( 'LIVE', @@ -274,10 +314,10 @@ class BroadcastGridItem extends StatelessWidget { Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Text( - broadcast.round.name, + widget.broadcast.round.name, style: TextStyle( fontSize: 11, - color: textShade(context, 0.5), + color: subTitleColor, ), overflow: TextOverflow.ellipsis, maxLines: 1, @@ -287,10 +327,11 @@ class BroadcastGridItem extends StatelessWidget { Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Text( - broadcast.title, + widget.broadcast.title, maxLines: 2, overflow: TextOverflow.ellipsis, - style: const TextStyle( + style: TextStyle( + color: titleColor, fontSize: 13, fontWeight: FontWeight.bold, ), diff --git a/lib/src/view/broadcast/broadcast_overview_tab.dart b/lib/src/view/broadcast/broadcast_overview_tab.dart index bd9ad5b7f8..91d59344a0 100644 --- a/lib/src/view/broadcast/broadcast_overview_tab.dart +++ b/lib/src/view/broadcast/broadcast_overview_tab.dart @@ -16,99 +16,98 @@ final _dateFormatter = DateFormat.MMMd(); /// A tab that displays the overview of a broadcast. class BroadcastOverviewTab extends ConsumerWidget { - final BroadcastTournamentId tournamentId; + const BroadcastOverviewTab({ + required this.broadcast, + required this.tournamentId, + super.key, + }); - const BroadcastOverviewTab({super.key, required this.tournamentId}); + final Broadcast broadcast; + final BroadcastTournamentId tournamentId; @override Widget build(BuildContext context, WidgetRef ref) { final tournament = ref.watch(broadcastTournamentProvider(tournamentId)); - return SafeArea( - bottom: false, - child: SingleChildScrollView( - child: Padding( - padding: Styles.bodyPadding, - child: switch (tournament) { - AsyncData(:final value) => BroadcastOverviewBody(value), - AsyncError(:final error) => Center( - child: Text('Cannot load broadcast data: $error'), - ), - _ => const Center(child: CircularProgressIndicator.adaptive()), - }, - ), - ), - ); - } -} - -class BroadcastOverviewBody extends StatelessWidget { - final BroadcastTournament tournament; - - const BroadcastOverviewBody(this.tournament); - - @override - Widget build(BuildContext context) { - final information = tournament.data.information; - final description = tournament.data.description; - - return Column( - children: [ - if (tournament.data.imageUrl != null) ...[ - Image.network(tournament.data.imageUrl!), - const SizedBox(height: 16.0), - ], - Wrap( - alignment: WrapAlignment.center, - children: [ - if (information.dates != null) - _BroadcastOverviewCard( - CupertinoIcons.calendar, - information.dates!.endsAt == null - ? _dateFormatter.format(information.dates!.startsAt) - : '${_dateFormatter.format(information.dates!.startsAt)} - ${_dateFormatter.format(information.dates!.endsAt!)}', - ), - if (information.format != null) - _BroadcastOverviewCard( - Icons.emoji_events, - '${information.format}', - ), - if (information.timeControl != null) - _BroadcastOverviewCard( - CupertinoIcons.stopwatch_fill, - '${information.timeControl}', - ), - if (information.location != null) - _BroadcastOverviewCard( - Icons.public, - '${information.location}', - ), - if (information.players != null) - _BroadcastOverviewCard( - Icons.person, - '${information.players}', - ), - if (information.website != null) - _BroadcastOverviewCard( - Icons.link, - context.l10n.broadcastOfficialWebsite, - information.website, + switch (tournament) { + case AsyncData(value: final tournament): + final information = tournament.data.information; + final description = tournament.data.description; + return SafeArea( + bottom: false, + child: ListView( + padding: Styles.bodyPadding, + children: [ + if (Theme.of(context).platform == TargetPlatform.iOS) ...[ + Text( + broadcast.title, + style: Styles.title, + ), + const SizedBox(height: 16.0), + ], + if (tournament.data.imageUrl != null) ...[ + Image.network(tournament.data.imageUrl!), + const SizedBox(height: 16.0), + ], + Wrap( + alignment: WrapAlignment.center, + children: [ + if (information.dates != null) + _BroadcastOverviewCard( + CupertinoIcons.calendar, + information.dates!.endsAt == null + ? _dateFormatter.format(information.dates!.startsAt) + : '${_dateFormatter.format(information.dates!.startsAt)} - ${_dateFormatter.format(information.dates!.endsAt!)}', + ), + if (information.format != null) + _BroadcastOverviewCard( + Icons.emoji_events, + '${information.format}', + ), + if (information.timeControl != null) + _BroadcastOverviewCard( + CupertinoIcons.stopwatch_fill, + '${information.timeControl}', + ), + if (information.location != null) + _BroadcastOverviewCard( + Icons.public, + '${information.location}', + ), + if (information.players != null) + _BroadcastOverviewCard( + Icons.person, + '${information.players}', + ), + if (information.website != null) + _BroadcastOverviewCard( + Icons.link, + context.l10n.broadcastOfficialWebsite, + information.website, + ), + ], ), - ], - ), - if (description != null) - Padding( - padding: const EdgeInsets.all(16), - child: MarkdownBody( - data: description, - onTapLink: (text, url, title) { - if (url == null) return; - launchUrl(Uri.parse(url)); - }, - ), + if (description != null) + Padding( + padding: const EdgeInsets.all(16), + child: MarkdownBody( + data: description, + onTapLink: (text, url, title) { + if (url == null) return; + launchUrl(Uri.parse(url)); + }, + ), + ), + ], ), - ], - ); + ); + case AsyncError(:final error): + return Center( + child: Text('Cannot load broadcast data: $error'), + ); + case _: + return const Center(child: CircularProgressIndicator.adaptive()); + } } } diff --git a/lib/src/view/broadcast/broadcast_round_screen.dart b/lib/src/view/broadcast/broadcast_round_screen.dart index df27adfc32..d3ba9a2ed1 100644 --- a/lib/src/view/broadcast/broadcast_round_screen.dart +++ b/lib/src/view/broadcast/broadcast_round_screen.dart @@ -97,6 +97,7 @@ class _BroadcastRoundScreenState extends ConsumerState Expanded( child: _selectedSegment == _ViewMode.overview ? BroadcastOverviewTab( + broadcast: widget.broadcast, tournamentId: _selectedTournamentId, ) : BroadcastBoardsTab( @@ -132,7 +133,10 @@ class _BroadcastRoundScreenState extends ConsumerState body: TabBarView( controller: _tabController, children: [ - BroadcastOverviewTab(tournamentId: _selectedTournamentId), + BroadcastOverviewTab( + broadcast: widget.broadcast, + tournamentId: _selectedTournamentId, + ), BroadcastBoardsTab(_selectedRoundId ?? value.defaultRoundId), ], ), diff --git a/lib/src/view/broadcast/broadcast_tile.dart b/lib/src/view/broadcast/broadcast_tile.dart deleted file mode 100644 index b84c2f2d65..0000000000 --- a/lib/src/view/broadcast/broadcast_tile.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; -import 'package:lichess_mobile/src/styles/transparent_image.dart'; -import 'package:lichess_mobile/src/utils/l10n_context.dart'; -import 'package:lichess_mobile/src/utils/navigation.dart'; -import 'package:lichess_mobile/src/view/broadcast/broadcast_round_screen.dart'; -import 'package:lichess_mobile/src/view/broadcast/default_broadcast_image.dart'; -import 'package:lichess_mobile/src/widgets/list.dart'; - -class BroadcastTile extends ConsumerWidget { - const BroadcastTile({ - required this.broadcast, - }); - - final Broadcast broadcast; - - @override - Widget build(BuildContext context, WidgetRef ref) { - return PlatformListTile( - leading: (broadcast.tour.imageUrl != null) - ? FadeInImage.memoryNetwork( - placeholder: transparentImage, - image: broadcast.tour.imageUrl!, - width: 60, - height: 30, - ) - : const DefaultBroadcastImage(width: 60), - onTap: () { - pushPlatformRoute( - context, - title: context.l10n.broadcastBroadcasts, - rootNavigator: true, - builder: (context) => BroadcastRoundScreen(broadcast: broadcast), - ); - }, - title: Padding( - padding: const EdgeInsets.only(right: 5.0), - child: Row( - children: [ - Flexible( - child: Text( - broadcast.title, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ), - trailing: (broadcast.isLive) - ? const Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.circle, color: Colors.red, size: 20), - SizedBox(height: 5), - Text( - 'LIVE', - style: - TextStyle(fontWeight: FontWeight.bold, color: Colors.red), - ), - ], - ) - : null, - ); - } -} diff --git a/lib/src/view/watch/watch_tab_screen.dart b/lib/src/view/watch/watch_tab_screen.dart index dd944a56d0..7f2e6c42ba 100644 --- a/lib/src/view/watch/watch_tab_screen.dart +++ b/lib/src/view/watch/watch_tab_screen.dart @@ -4,6 +4,7 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_providers.dart'; import 'package:lichess_mobile/src/model/tv/featured_player.dart'; import 'package:lichess_mobile/src/model/tv/tv_channel.dart'; @@ -16,7 +17,7 @@ import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_list_screen.dart'; -import 'package:lichess_mobile/src/view/broadcast/broadcast_tile.dart'; +import 'package:lichess_mobile/src/view/broadcast/broadcast_round_screen.dart'; import 'package:lichess_mobile/src/view/watch/live_tv_channels_screen.dart'; import 'package:lichess_mobile/src/view/watch/streamer_screen.dart'; import 'package:lichess_mobile/src/view/watch/tv_screen.dart'; @@ -194,7 +195,6 @@ class _BroadcastWidget extends ConsumerWidget { data: (data) { return ListSection( header: Text(context.l10n.broadcastBroadcasts), - hasLeading: true, headerTrailing: NoPaddingTextButton( onPressed: () { pushPlatformRoute( @@ -209,7 +209,7 @@ class _BroadcastWidget extends ConsumerWidget { children: [ ...CombinedIterableView([data.active, data.upcoming, data.past]) .take(numberOfItems) - .map((broadcast) => BroadcastTile(broadcast: broadcast)), + .map((broadcast) => _BroadcastTile(broadcast: broadcast)), ], ); }, @@ -235,6 +235,62 @@ class _BroadcastWidget extends ConsumerWidget { } } +class _BroadcastTile extends ConsumerWidget { + const _BroadcastTile({ + required this.broadcast, + }); + + final Broadcast broadcast; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return PlatformListTile( + onTap: () { + pushPlatformRoute( + context, + title: context.l10n.broadcastBroadcasts, + rootNavigator: true, + builder: (context) => BroadcastRoundScreen(broadcast: broadcast), + ); + }, + title: Padding( + padding: const EdgeInsets.only(right: 5.0), + child: Row( + children: [ + Flexible( + child: Text( + broadcast.title, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + trailing: (broadcast.isLive) + ? Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.circle, + color: context.lichessColors.error, + size: 20, + ), + const SizedBox(height: 5), + Text( + 'LIVE', + style: TextStyle( + fontWeight: FontWeight.bold, + color: context.lichessColors.error, + ), + ), + ], + ) + : null, + ); + } +} + class _WatchTvWidget extends ConsumerWidget { const _WatchTvWidget(); diff --git a/lib/src/widgets/bottom_bar.dart b/lib/src/widgets/bottom_bar.dart index 7267eb75ca..5a96c93e4e 100644 --- a/lib/src/widgets/bottom_bar.dart +++ b/lib/src/widgets/bottom_bar.dart @@ -28,21 +28,33 @@ class BottomBar extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( - color: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoTheme.of(context).barBackgroundColor - : Theme.of(context).bottomAppBarTheme.color, - child: SafeArea( - top: false, - child: SizedBox( - height: kBottomBarHeight, - child: Row( - mainAxisAlignment: mainAxisAlignment, - children: expandChildren - ? children.map((child) => Expanded(child: child)).toList() - : children, + if (Theme.of(context).platform == TargetPlatform.iOS) { + return ColoredBox( + color: CupertinoTheme.of(context).barBackgroundColor, + child: SafeArea( + top: false, + child: SizedBox( + height: kBottomBarHeight, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: mainAxisAlignment, + children: expandChildren + ? children.map((child) => Expanded(child: child)).toList() + : children, + ), ), ), + ); + } + + return BottomAppBar( + height: kBottomBarHeight, + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6.0), + child: Row( + mainAxisAlignment: mainAxisAlignment, + children: expandChildren + ? children.map((child) => Expanded(child: child)).toList() + : children, ), ); } From e4d18e426f991fba716deb8236fc97311af761e4 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 4 Dec 2024 19:54:06 +0100 Subject: [PATCH 842/979] Remove borders, make a background gradient --- .../view/broadcast/broadcast_list_screen.dart | 59 ++++++++++--------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_list_screen.dart b/lib/src/view/broadcast/broadcast_list_screen.dart index d041917523..6c3649c625 100644 --- a/lib/src/view/broadcast/broadcast_list_screen.dart +++ b/lib/src/view/broadcast/broadcast_list_screen.dart @@ -1,11 +1,9 @@ -import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_providers.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/styles/lichess_colors.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/styles/transparent_image.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; @@ -228,13 +226,12 @@ class _BroadcastGridItemState extends State { @override Widget build(BuildContext context) { + final backgroundColor = + _colorScheme?.primaryContainer ?? Colors.transparent; final titleColor = _colorScheme?.onPrimaryContainer; final subTitleColor = _colorScheme?.onPrimaryContainer.withValues(alpha: 0.7) ?? textShade(context, 0.7); - final red = _colorScheme != null - ? LichessColors.red.harmonizeWith(_colorScheme!.primary) - : LichessColors.red; return AdaptiveInkWell( borderRadius: BorderRadius.circular(20), @@ -251,35 +248,39 @@ class _BroadcastGridItemState extends State { clipBehavior: Clip.hardEdge, decoration: BoxDecoration( borderRadius: BorderRadius.circular(20), - color: _colorScheme?.primaryContainer, - boxShadow: const [ - BoxShadow( - blurRadius: 3, - spreadRadius: 1, - ), - ], - ), - foregroundDecoration: BoxDecoration( - border: (widget.broadcast.isLive) - ? Border.all(color: red.withValues(alpha: 0.8), width: 3.0) - : Border.all(color: LichessColors.grey), - borderRadius: BorderRadius.circular(20), + color: backgroundColor, + boxShadow: Theme.of(context).platform == TargetPlatform.iOS + ? null + : kElevationToShadow[1], ), child: Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (widget.broadcast.tour.imageUrl != null) - AspectRatio( - aspectRatio: 2.0, - child: FadeInImage.memoryNetwork( - placeholder: transparentImage, - image: widget.broadcast.tour.imageUrl!, - ), - ) - else - const DefaultBroadcastImage(aspectRatio: 2.0), - const SizedBox(height: 4.0), + ShaderMask( + blendMode: BlendMode.dstOut, + shaderCallback: (bounds) { + return LinearGradient( + begin: Alignment.center, + end: Alignment.bottomCenter, + colors: [ + backgroundColor.withValues(alpha: 0.10), + backgroundColor.withValues(alpha: 1.0), + ], + stops: const [0.5, 1.00], + tileMode: TileMode.clamp, + ).createShader(bounds); + }, + child: widget.broadcast.tour.imageUrl != null + ? AspectRatio( + aspectRatio: 2.0, + child: FadeInImage.memoryNetwork( + placeholder: transparentImage, + image: widget.broadcast.tour.imageUrl!, + ), + ) + : const DefaultBroadcastImage(aspectRatio: 2.0), + ), if (widget.broadcast.round.startsAt != null || widget.broadcast.isLive) Padding( From 9f8161af2da4d42fc96899b286298a7912130398 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 4 Dec 2024 20:48:50 +0100 Subject: [PATCH 843/979] Fix tests --- .../view/broadcast/broadcast_list_screen.dart | 36 +++++++--- pubspec.lock | 2 +- pubspec.yaml | 2 +- .../broadcasts_list_screen_test.dart | 66 +++++++++++-------- 4 files changed, 66 insertions(+), 40 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_list_screen.dart b/lib/src/view/broadcast/broadcast_list_screen.dart index 6c3649c625..6415c80d83 100644 --- a/lib/src/view/broadcast/broadcast_list_screen.dart +++ b/lib/src/view/broadcast/broadcast_list_screen.dart @@ -1,4 +1,7 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; @@ -204,23 +207,34 @@ class _BroadcastGridItemState extends State { ColorScheme? _colorScheme; @override - void initState() { - super.initState(); - + void didChangeDependencies() { + super.didChangeDependencies(); if (widget.broadcast.tour.imageUrl != null) { _fetchColorScheme(widget.broadcast.tour.imageUrl!); } } Future _fetchColorScheme(String url) async { - final colorScheme = await ColorScheme.fromImageProvider( - provider: NetworkImage(url), - dynamicSchemeVariant: DynamicSchemeVariant.fidelity, - ); - if (mounted) { - setState(() { - _colorScheme = colorScheme; + if (!mounted) return; + + if (Scrollable.recommendDeferredLoadingForContext(context)) { + SchedulerBinding.instance.scheduleFrameCallback((_) { + scheduleMicrotask(() => _fetchColorScheme(url)); }); + } else { + try { + final colorScheme = await ColorScheme.fromImageProvider( + provider: NetworkImage(url), + dynamicSchemeVariant: DynamicSchemeVariant.fidelity, + ); + if (mounted) { + setState(() { + _colorScheme = colorScheme; + }); + } + } catch (_) { + // ignore + } } } @@ -277,6 +291,8 @@ class _BroadcastGridItemState extends State { child: FadeInImage.memoryNetwork( placeholder: transparentImage, image: widget.broadcast.tour.imageUrl!, + imageErrorBuilder: (context, error, stackTrace) => + const DefaultBroadcastImage(aspectRatio: 2.0), ), ) : const DefaultBroadcastImage(aspectRatio: 2.0), diff --git a/pubspec.lock b/pubspec.lock index 2cd529f0b1..a10b8cbcbb 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -972,7 +972,7 @@ packages: source: hosted version: "1.0.4" network_image_mock: - dependency: "direct main" + dependency: "direct dev" description: name: network_image_mock sha256: "855cdd01d42440e0cffee0d6c2370909fc31b3bcba308a59829f24f64be42db7" diff --git a/pubspec.yaml b/pubspec.yaml index 3b5b3ec1f6..827462f30f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -58,7 +58,6 @@ dependencies: logging: ^1.1.0 material_color_utilities: ^0.11.1 meta: ^1.8.0 - network_image_mock: ^2.1.1 package_info_plus: ^8.0.0 path: ^1.8.2 popover: ^0.3.0 @@ -91,6 +90,7 @@ dev_dependencies: json_serializable: ^6.5.4 lint: ^2.0.1 mocktail: ^1.0.0 + network_image_mock: ^2.1.1 riverpod_generator: ^2.1.0 riverpod_lint: ^2.3.3 sqflite_common_ffi: ^2.2.3 diff --git a/test/view/broadcast/broadcasts_list_screen_test.dart b/test/view/broadcast/broadcasts_list_screen_test.dart index 166b23e880..a570b58071 100644 --- a/test/view/broadcast/broadcasts_list_screen_test.dart +++ b/test/view/broadcast/broadcasts_list_screen_test.dart @@ -24,23 +24,27 @@ void main() { 'Displays broadcast tournament screen', variant: kPlatformVariant, (tester) async { - final app = await makeTestProviderScopeApp( - tester, - home: const BroadcastListScreen(), - overrides: [ - lichessClientProvider - .overrideWith((ref) => LichessClient(client, ref)), - ], - ); + mockNetworkImagesFor(() async { + final app = await makeTestProviderScopeApp( + tester, + home: const BroadcastListScreen(), + overrides: [ + lichessClientProvider + .overrideWith((ref) => LichessClient(client, ref)), + ], + ); - await tester.pumpWidget(app); + await tester.pumpWidget(app); - // let time for request to complete - await mockNetworkImagesFor( - () => tester.pump(const Duration(milliseconds: 50)), - ); + // wait for broadcast tournaments to load + await tester.pump(const Duration(milliseconds: 100)); - expect(find.byType(BroadcastGridItem), findsAtLeast(1)); + expect(find.byType(BroadcastGridItem), findsAtLeast(1)); + + // ColorScheme.fromImageProvider creates a Timer of 5s which is not automatically + // disposed + await tester.pump(const Duration(seconds: 10)); + }); }, ); @@ -48,23 +52,29 @@ void main() { 'Scroll broadcast tournament screen', variant: kPlatformVariant, (tester) async { - final app = await makeTestProviderScopeApp( - tester, - home: const BroadcastListScreen(), - overrides: [ - lichessClientProvider - .overrideWith((ref) => LichessClient(client, ref)), - ], - ); + mockNetworkImagesFor(() async { + final app = await makeTestProviderScopeApp( + tester, + home: const BroadcastListScreen(), + overrides: [ + lichessClientProvider + .overrideWith((ref) => LichessClient(client, ref)), + ], + ); + + await tester.pumpWidget(app); + + // wait for broadcast tournaments to load + await tester.pump(const Duration(milliseconds: 100)); - await tester.pumpWidget(app); + await tester.scrollUntilVisible(find.text('Past broadcasts'), 200.0); - // let time for request to complete - await mockNetworkImagesFor( - () => tester.pump(const Duration(milliseconds: 50)), - ); + await tester.pumpAndSettle(); - tester.scrollUntilVisible(find.text('Past broadcasts'), 200.0); + // ColorScheme.fromImageProvider creates a Timer of 5s which is not automatically + // disposed + await tester.pump(const Duration(seconds: 10)); + }); }, ); }); From a7a651a7028b6ab93aa19f126d30b6707270e38f Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 4 Dec 2024 22:08:15 +0100 Subject: [PATCH 844/979] Sort mobile trans, add theme string --- lib/l10n/app_en.arb | 102 +++--- lib/l10n/l10n.dart | 330 +++++++++++------- lib/l10n/l10n_af.dart | 131 ++++--- lib/l10n/l10n_ar.dart | 131 ++++--- lib/l10n/l10n_az.dart | 131 ++++--- lib/l10n/l10n_be.dart | 131 ++++--- lib/l10n/l10n_bg.dart | 131 ++++--- lib/l10n/l10n_bn.dart | 131 ++++--- lib/l10n/l10n_br.dart | 131 ++++--- lib/l10n/l10n_bs.dart | 131 ++++--- lib/l10n/l10n_ca.dart | 131 ++++--- lib/l10n/l10n_cs.dart | 131 ++++--- lib/l10n/l10n_da.dart | 131 ++++--- lib/l10n/l10n_de.dart | 131 ++++--- lib/l10n/l10n_el.dart | 131 ++++--- lib/l10n/l10n_en.dart | 225 ++++++------ lib/l10n/l10n_eo.dart | 131 ++++--- lib/l10n/l10n_es.dart | 131 ++++--- lib/l10n/l10n_et.dart | 131 ++++--- lib/l10n/l10n_eu.dart | 131 ++++--- lib/l10n/l10n_fa.dart | 131 ++++--- lib/l10n/l10n_fi.dart | 131 ++++--- lib/l10n/l10n_fo.dart | 131 ++++--- lib/l10n/l10n_fr.dart | 131 ++++--- lib/l10n/l10n_ga.dart | 131 ++++--- lib/l10n/l10n_gl.dart | 131 ++++--- lib/l10n/l10n_gsw.dart | 131 ++++--- lib/l10n/l10n_he.dart | 131 ++++--- lib/l10n/l10n_hi.dart | 131 ++++--- lib/l10n/l10n_hr.dart | 131 ++++--- lib/l10n/l10n_hu.dart | 131 ++++--- lib/l10n/l10n_hy.dart | 131 ++++--- lib/l10n/l10n_id.dart | 131 ++++--- lib/l10n/l10n_it.dart | 131 ++++--- lib/l10n/l10n_ja.dart | 131 ++++--- lib/l10n/l10n_kk.dart | 131 ++++--- lib/l10n/l10n_ko.dart | 131 ++++--- lib/l10n/l10n_lb.dart | 131 ++++--- lib/l10n/l10n_lt.dart | 131 ++++--- lib/l10n/l10n_lv.dart | 131 ++++--- lib/l10n/l10n_mk.dart | 131 ++++--- lib/l10n/l10n_nb.dart | 131 ++++--- lib/l10n/l10n_nl.dart | 131 ++++--- lib/l10n/l10n_nn.dart | 131 ++++--- lib/l10n/l10n_pl.dart | 131 ++++--- lib/l10n/l10n_pt.dart | 225 ++++++------ lib/l10n/l10n_ro.dart | 131 ++++--- lib/l10n/l10n_ru.dart | 131 ++++--- lib/l10n/l10n_sk.dart | 131 ++++--- lib/l10n/l10n_sl.dart | 131 ++++--- lib/l10n/l10n_sq.dart | 131 ++++--- lib/l10n/l10n_sr.dart | 131 ++++--- lib/l10n/l10n_sv.dart | 131 ++++--- lib/l10n/l10n_tr.dart | 131 ++++--- lib/l10n/l10n_uk.dart | 131 ++++--- lib/l10n/l10n_vi.dart | 131 ++++--- lib/l10n/l10n_zh.dart | 225 ++++++------ .../view/settings/settings_tab_screen.dart | 2 +- translation/source/mobile.xml | 67 ++-- 59 files changed, 4990 insertions(+), 2998 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 6734d91450..a05a7065c3 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1,61 +1,62 @@ { - "mobileHomeTab": "Home", - "mobilePuzzlesTab": "Puzzles", - "mobileToolsTab": "Tools", - "mobileWatchTab": "Watch", - "mobileSettingsTab": "Settings", - "mobileMustBeLoggedIn": "You must be logged in to view this page.", - "mobileSystemColors": "System colors", - "mobileFeedbackButton": "Feedback", - "mobileOkButton": "OK", - "mobileSettingsHapticFeedback": "Haptic feedback", - "mobileSettingsImmersiveMode": "Immersive mode", - "mobileSettingsImmersiveModeSubtitle": "Hide system UI while playing. Use this if you are bothered by the system's navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.", - "mobileNotFollowingAnyUser": "You are not following any user.", "mobileAllGames": "All games", - "mobileRecentSearches": "Recent searches", + "mobileAreYouSure": "Are you sure?", + "mobileBlindfoldMode": "Blindfold", + "mobileCancelTakebackOffer": "Cancel takeback offer", "mobileClearButton": "Clear", - "mobilePlayersMatchingSearchTerm": "Players with \"{param}\"", - "@mobilePlayersMatchingSearchTerm": { + "mobileCorrespondenceClearSavedMove": "Clear saved move", + "mobileCustomGameJoinAGame": "Join a game", + "mobileFeedbackButton": "Feedback", + "mobileGreeting": "Hello, {param}", + "@mobileGreeting": { "placeholders": { "param": { "type": "String" } } }, - "mobileNoSearchResults": "No results", - "mobileAreYouSure": "Are you sure?", - "mobilePuzzleStreakAbortWarning": "You will lose your current streak and your score will be saved.", - "mobilePuzzleStormNothingToShow": "Nothing to show. Play some runs of Puzzle Storm.", - "mobileSharePuzzle": "Share this puzzle", - "mobileShareGameURL": "Share game URL", - "mobileShareGamePGN": "Share PGN", - "mobileSharePositionAsFEN": "Share position as FEN", - "mobileShowVariations": "Show variations", + "mobileGreetingWithoutName": "Hello", "mobileHideVariation": "Hide variation", - "mobileShowComments": "Show comments", - "mobilePuzzleStormConfirmEndRun": "Do you want to end this run?", - "mobilePuzzleStormFilterNothingToShow": "Nothing to show, please change the filters", - "mobileCancelTakebackOffer": "Cancel takeback offer", - "mobileWaitingForOpponentToJoin": "Waiting for opponent to join...", - "mobileBlindfoldMode": "Blindfold", + "mobileHomeTab": "Home", "mobileLiveStreamers": "Live streamers", - "mobileCustomGameJoinAGame": "Join a game", - "mobileCorrespondenceClearSavedMove": "Clear saved move", - "mobileSomethingWentWrong": "Something went wrong.", - "mobileShowResult": "Show result", - "mobilePuzzleThemesSubtitle": "Play puzzles from your favorite openings, or choose a theme.", - "mobilePuzzleStormSubtitle": "Solve as many puzzles as possible in 3 minutes.", - "mobileGreeting": "Hello, {param}", - "@mobileGreeting": { + "mobileMustBeLoggedIn": "You must be logged in to view this page.", + "mobileNoSearchResults": "No results", + "mobileNotFollowingAnyUser": "You are not following any user.", + "mobileOkButton": "OK", + "mobilePlayersMatchingSearchTerm": "Players with \"{param}\"", + "@mobilePlayersMatchingSearchTerm": { "placeholders": { "param": { "type": "String" } } }, - "mobileGreetingWithoutName": "Hello", "mobilePrefMagnifyDraggedPiece": "Magnify dragged piece", + "mobilePuzzleStormConfirmEndRun": "Do you want to end this run?", + "mobilePuzzleStormFilterNothingToShow": "Nothing to show, please change the filters", + "mobilePuzzleStormNothingToShow": "Nothing to show. Play some runs of Puzzle Storm.", + "mobilePuzzleStormSubtitle": "Solve as many puzzles as possible in 3 minutes.", + "mobilePuzzleStreakAbortWarning": "You will lose your current streak and your score will be saved.", + "mobilePuzzleThemesSubtitle": "Play puzzles from your favorite openings, or choose a theme.", + "mobilePuzzlesTab": "Puzzles", + "mobileRecentSearches": "Recent searches", + "mobileSettingsHapticFeedback": "Haptic feedback", + "mobileSettingsImmersiveMode": "Immersive mode", + "mobileSettingsImmersiveModeSubtitle": "Hide system UI while playing. Use this if you are bothered by the system's navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.", + "mobileSettingsTab": "Settings", + "mobileShareGamePGN": "Share PGN", + "mobileShareGameURL": "Share game URL", + "mobileSharePositionAsFEN": "Share position as FEN", + "mobileSharePuzzle": "Share this puzzle", + "mobileShowComments": "Show comments", + "mobileShowResult": "Show result", + "mobileShowVariations": "Show variations", + "mobileSomethingWentWrong": "Something went wrong.", + "mobileSystemColors": "System colors", + "mobileTheme": "Theme", + "mobileToolsTab": "Tools", + "mobileWaitingForOpponentToJoin": "Waiting for opponent to join...", + "mobileWatchTab": "Watch", "activityActivity": "Activity", "activityHostedALiveStream": "Hosted a live stream", "activityRankedInSwissTournament": "Ranked #{param1} in {param2}", @@ -327,6 +328,7 @@ "broadcastNotYetStarted": "The broadcast has not yet started.", "broadcastOfficialWebsite": "Official website", "broadcastStandings": "Standings", + "broadcastOfficialStandings": "Official Standings", "broadcastIframeHelp": "More options on the {param}", "@broadcastIframeHelp": { "placeholders": { @@ -356,6 +358,16 @@ "broadcastRatingDiff": "Rating diff", "broadcastGamesThisTournament": "Games in this tournament", "broadcastScore": "Score", + "broadcastAllTeams": "All teams", + "broadcastTournamentFormat": "Tournament format", + "broadcastTournamentLocation": "Tournament Location", + "broadcastTopPlayers": "Top players", + "broadcastTimezone": "Time zone", + "broadcastFideRatingCategory": "FIDE rating category", + "broadcastOptionalDetails": "Optional details", + "broadcastUpcomingBroadcasts": "Upcoming broadcasts", + "broadcastPastBroadcasts": "Past broadcasts", + "broadcastAllBroadcastsByMonth": "View all broadcasts by month", "broadcastNbBroadcasts": "{count, plural, =1{{count} broadcast} other{{count} broadcasts}}", "@broadcastNbBroadcasts": { "placeholders": { @@ -1059,7 +1071,6 @@ "replayMode": "Replay mode", "realtimeReplay": "Realtime", "byCPL": "By CPL", - "openStudy": "Open study", "enable": "Enable", "bestMoveArrow": "Best move arrow", "showVariationArrows": "Show variation arrows", @@ -1452,7 +1463,6 @@ "block": "Block", "blocked": "Blocked", "unblock": "Unblock", - "followsYou": "Follows you", "xStartedFollowingY": "{param1} started following {param2}", "@xStartedFollowingY": { "placeholders": { @@ -3116,6 +3126,14 @@ "studyPlayAgain": "Play again", "studyWhatWouldYouPlay": "What would you play in this position?", "studyYouCompletedThisLesson": "Congratulations! You completed this lesson.", + "studyPerPage": "{param} per page", + "@studyPerPage": { + "placeholders": { + "param": { + "type": "String" + } + } + }, "studyNbChapters": "{count, plural, =1{{count} Chapter} other{{count} Chapters}}", "@studyNbChapters": { "placeholders": { diff --git a/lib/l10n/l10n.dart b/lib/l10n/l10n.dart index 62cb2d2d20..9191e5ece3 100644 --- a/lib/l10n/l10n.dart +++ b/lib/l10n/l10n.dart @@ -204,47 +204,47 @@ abstract class AppLocalizations { Locale('zh', 'TW') ]; - /// No description provided for @mobileHomeTab. + /// No description provided for @mobileAllGames. /// /// In en, this message translates to: - /// **'Home'** - String get mobileHomeTab; + /// **'All games'** + String get mobileAllGames; - /// No description provided for @mobilePuzzlesTab. + /// No description provided for @mobileAreYouSure. /// /// In en, this message translates to: - /// **'Puzzles'** - String get mobilePuzzlesTab; + /// **'Are you sure?'** + String get mobileAreYouSure; - /// No description provided for @mobileToolsTab. + /// No description provided for @mobileBlindfoldMode. /// /// In en, this message translates to: - /// **'Tools'** - String get mobileToolsTab; + /// **'Blindfold'** + String get mobileBlindfoldMode; - /// No description provided for @mobileWatchTab. + /// No description provided for @mobileCancelTakebackOffer. /// /// In en, this message translates to: - /// **'Watch'** - String get mobileWatchTab; + /// **'Cancel takeback offer'** + String get mobileCancelTakebackOffer; - /// No description provided for @mobileSettingsTab. + /// No description provided for @mobileClearButton. /// /// In en, this message translates to: - /// **'Settings'** - String get mobileSettingsTab; + /// **'Clear'** + String get mobileClearButton; - /// No description provided for @mobileMustBeLoggedIn. + /// No description provided for @mobileCorrespondenceClearSavedMove. /// /// In en, this message translates to: - /// **'You must be logged in to view this page.'** - String get mobileMustBeLoggedIn; + /// **'Clear saved move'** + String get mobileCorrespondenceClearSavedMove; - /// No description provided for @mobileSystemColors. + /// No description provided for @mobileCustomGameJoinAGame. /// /// In en, this message translates to: - /// **'System colors'** - String get mobileSystemColors; + /// **'Join a game'** + String get mobileCustomGameJoinAGame; /// No description provided for @mobileFeedbackButton. /// @@ -252,53 +252,59 @@ abstract class AppLocalizations { /// **'Feedback'** String get mobileFeedbackButton; - /// No description provided for @mobileOkButton. + /// No description provided for @mobileGreeting. /// /// In en, this message translates to: - /// **'OK'** - String get mobileOkButton; + /// **'Hello, {param}'** + String mobileGreeting(String param); - /// No description provided for @mobileSettingsHapticFeedback. + /// No description provided for @mobileGreetingWithoutName. /// /// In en, this message translates to: - /// **'Haptic feedback'** - String get mobileSettingsHapticFeedback; + /// **'Hello'** + String get mobileGreetingWithoutName; - /// No description provided for @mobileSettingsImmersiveMode. + /// No description provided for @mobileHideVariation. /// /// In en, this message translates to: - /// **'Immersive mode'** - String get mobileSettingsImmersiveMode; + /// **'Hide variation'** + String get mobileHideVariation; - /// No description provided for @mobileSettingsImmersiveModeSubtitle. + /// No description provided for @mobileHomeTab. /// /// In en, this message translates to: - /// **'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'** - String get mobileSettingsImmersiveModeSubtitle; + /// **'Home'** + String get mobileHomeTab; - /// No description provided for @mobileNotFollowingAnyUser. + /// No description provided for @mobileLiveStreamers. /// /// In en, this message translates to: - /// **'You are not following any user.'** - String get mobileNotFollowingAnyUser; + /// **'Live streamers'** + String get mobileLiveStreamers; - /// No description provided for @mobileAllGames. + /// No description provided for @mobileMustBeLoggedIn. /// /// In en, this message translates to: - /// **'All games'** - String get mobileAllGames; + /// **'You must be logged in to view this page.'** + String get mobileMustBeLoggedIn; - /// No description provided for @mobileRecentSearches. + /// No description provided for @mobileNoSearchResults. /// /// In en, this message translates to: - /// **'Recent searches'** - String get mobileRecentSearches; + /// **'No results'** + String get mobileNoSearchResults; - /// No description provided for @mobileClearButton. + /// No description provided for @mobileNotFollowingAnyUser. /// /// In en, this message translates to: - /// **'Clear'** - String get mobileClearButton; + /// **'You are not following any user.'** + String get mobileNotFollowingAnyUser; + + /// No description provided for @mobileOkButton. + /// + /// In en, this message translates to: + /// **'OK'** + String get mobileOkButton; /// No description provided for @mobilePlayersMatchingSearchTerm. /// @@ -306,23 +312,23 @@ abstract class AppLocalizations { /// **'Players with \"{param}\"'** String mobilePlayersMatchingSearchTerm(String param); - /// No description provided for @mobileNoSearchResults. + /// No description provided for @mobilePrefMagnifyDraggedPiece. /// /// In en, this message translates to: - /// **'No results'** - String get mobileNoSearchResults; + /// **'Magnify dragged piece'** + String get mobilePrefMagnifyDraggedPiece; - /// No description provided for @mobileAreYouSure. + /// No description provided for @mobilePuzzleStormConfirmEndRun. /// /// In en, this message translates to: - /// **'Are you sure?'** - String get mobileAreYouSure; + /// **'Do you want to end this run?'** + String get mobilePuzzleStormConfirmEndRun; - /// No description provided for @mobilePuzzleStreakAbortWarning. + /// No description provided for @mobilePuzzleStormFilterNothingToShow. /// /// In en, this message translates to: - /// **'You will lose your current streak and your score will be saved.'** - String get mobilePuzzleStreakAbortWarning; + /// **'Nothing to show, please change the filters'** + String get mobilePuzzleStormFilterNothingToShow; /// No description provided for @mobilePuzzleStormNothingToShow. /// @@ -330,137 +336,137 @@ abstract class AppLocalizations { /// **'Nothing to show. Play some runs of Puzzle Storm.'** String get mobilePuzzleStormNothingToShow; - /// No description provided for @mobileSharePuzzle. + /// No description provided for @mobilePuzzleStormSubtitle. /// /// In en, this message translates to: - /// **'Share this puzzle'** - String get mobileSharePuzzle; + /// **'Solve as many puzzles as possible in 3 minutes.'** + String get mobilePuzzleStormSubtitle; - /// No description provided for @mobileShareGameURL. + /// No description provided for @mobilePuzzleStreakAbortWarning. /// /// In en, this message translates to: - /// **'Share game URL'** - String get mobileShareGameURL; + /// **'You will lose your current streak and your score will be saved.'** + String get mobilePuzzleStreakAbortWarning; - /// No description provided for @mobileShareGamePGN. + /// No description provided for @mobilePuzzleThemesSubtitle. /// /// In en, this message translates to: - /// **'Share PGN'** - String get mobileShareGamePGN; + /// **'Play puzzles from your favorite openings, or choose a theme.'** + String get mobilePuzzleThemesSubtitle; - /// No description provided for @mobileSharePositionAsFEN. + /// No description provided for @mobilePuzzlesTab. /// /// In en, this message translates to: - /// **'Share position as FEN'** - String get mobileSharePositionAsFEN; + /// **'Puzzles'** + String get mobilePuzzlesTab; - /// No description provided for @mobileShowVariations. + /// No description provided for @mobileRecentSearches. /// /// In en, this message translates to: - /// **'Show variations'** - String get mobileShowVariations; + /// **'Recent searches'** + String get mobileRecentSearches; - /// No description provided for @mobileHideVariation. + /// No description provided for @mobileSettingsHapticFeedback. /// /// In en, this message translates to: - /// **'Hide variation'** - String get mobileHideVariation; + /// **'Haptic feedback'** + String get mobileSettingsHapticFeedback; - /// No description provided for @mobileShowComments. + /// No description provided for @mobileSettingsImmersiveMode. /// /// In en, this message translates to: - /// **'Show comments'** - String get mobileShowComments; + /// **'Immersive mode'** + String get mobileSettingsImmersiveMode; - /// No description provided for @mobilePuzzleStormConfirmEndRun. + /// No description provided for @mobileSettingsImmersiveModeSubtitle. /// /// In en, this message translates to: - /// **'Do you want to end this run?'** - String get mobilePuzzleStormConfirmEndRun; + /// **'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'** + String get mobileSettingsImmersiveModeSubtitle; - /// No description provided for @mobilePuzzleStormFilterNothingToShow. + /// No description provided for @mobileSettingsTab. /// /// In en, this message translates to: - /// **'Nothing to show, please change the filters'** - String get mobilePuzzleStormFilterNothingToShow; + /// **'Settings'** + String get mobileSettingsTab; - /// No description provided for @mobileCancelTakebackOffer. + /// No description provided for @mobileShareGamePGN. /// /// In en, this message translates to: - /// **'Cancel takeback offer'** - String get mobileCancelTakebackOffer; + /// **'Share PGN'** + String get mobileShareGamePGN; - /// No description provided for @mobileWaitingForOpponentToJoin. + /// No description provided for @mobileShareGameURL. /// /// In en, this message translates to: - /// **'Waiting for opponent to join...'** - String get mobileWaitingForOpponentToJoin; + /// **'Share game URL'** + String get mobileShareGameURL; - /// No description provided for @mobileBlindfoldMode. + /// No description provided for @mobileSharePositionAsFEN. /// /// In en, this message translates to: - /// **'Blindfold'** - String get mobileBlindfoldMode; + /// **'Share position as FEN'** + String get mobileSharePositionAsFEN; - /// No description provided for @mobileLiveStreamers. + /// No description provided for @mobileSharePuzzle. /// /// In en, this message translates to: - /// **'Live streamers'** - String get mobileLiveStreamers; + /// **'Share this puzzle'** + String get mobileSharePuzzle; - /// No description provided for @mobileCustomGameJoinAGame. + /// No description provided for @mobileShowComments. /// /// In en, this message translates to: - /// **'Join a game'** - String get mobileCustomGameJoinAGame; + /// **'Show comments'** + String get mobileShowComments; - /// No description provided for @mobileCorrespondenceClearSavedMove. + /// No description provided for @mobileShowResult. /// /// In en, this message translates to: - /// **'Clear saved move'** - String get mobileCorrespondenceClearSavedMove; + /// **'Show result'** + String get mobileShowResult; - /// No description provided for @mobileSomethingWentWrong. + /// No description provided for @mobileShowVariations. /// /// In en, this message translates to: - /// **'Something went wrong.'** - String get mobileSomethingWentWrong; + /// **'Show variations'** + String get mobileShowVariations; - /// No description provided for @mobileShowResult. + /// No description provided for @mobileSomethingWentWrong. /// /// In en, this message translates to: - /// **'Show result'** - String get mobileShowResult; + /// **'Something went wrong.'** + String get mobileSomethingWentWrong; - /// No description provided for @mobilePuzzleThemesSubtitle. + /// No description provided for @mobileSystemColors. /// /// In en, this message translates to: - /// **'Play puzzles from your favorite openings, or choose a theme.'** - String get mobilePuzzleThemesSubtitle; + /// **'System colors'** + String get mobileSystemColors; - /// No description provided for @mobilePuzzleStormSubtitle. + /// No description provided for @mobileTheme. /// /// In en, this message translates to: - /// **'Solve as many puzzles as possible in 3 minutes.'** - String get mobilePuzzleStormSubtitle; + /// **'Theme'** + String get mobileTheme; - /// No description provided for @mobileGreeting. + /// No description provided for @mobileToolsTab. /// /// In en, this message translates to: - /// **'Hello, {param}'** - String mobileGreeting(String param); + /// **'Tools'** + String get mobileToolsTab; - /// No description provided for @mobileGreetingWithoutName. + /// No description provided for @mobileWaitingForOpponentToJoin. /// /// In en, this message translates to: - /// **'Hello'** - String get mobileGreetingWithoutName; + /// **'Waiting for opponent to join...'** + String get mobileWaitingForOpponentToJoin; - /// No description provided for @mobilePrefMagnifyDraggedPiece. + /// No description provided for @mobileWatchTab. /// /// In en, this message translates to: - /// **'Magnify dragged piece'** - String get mobilePrefMagnifyDraggedPiece; + /// **'Watch'** + String get mobileWatchTab; /// No description provided for @activityActivity. /// @@ -942,6 +948,12 @@ abstract class AppLocalizations { /// **'Standings'** String get broadcastStandings; + /// No description provided for @broadcastOfficialStandings. + /// + /// In en, this message translates to: + /// **'Official Standings'** + String get broadcastOfficialStandings; + /// No description provided for @broadcastIframeHelp. /// /// In en, this message translates to: @@ -990,6 +1002,66 @@ abstract class AppLocalizations { /// **'Score'** String get broadcastScore; + /// No description provided for @broadcastAllTeams. + /// + /// In en, this message translates to: + /// **'All teams'** + String get broadcastAllTeams; + + /// No description provided for @broadcastTournamentFormat. + /// + /// In en, this message translates to: + /// **'Tournament format'** + String get broadcastTournamentFormat; + + /// No description provided for @broadcastTournamentLocation. + /// + /// In en, this message translates to: + /// **'Tournament Location'** + String get broadcastTournamentLocation; + + /// No description provided for @broadcastTopPlayers. + /// + /// In en, this message translates to: + /// **'Top players'** + String get broadcastTopPlayers; + + /// No description provided for @broadcastTimezone. + /// + /// In en, this message translates to: + /// **'Time zone'** + String get broadcastTimezone; + + /// No description provided for @broadcastFideRatingCategory. + /// + /// In en, this message translates to: + /// **'FIDE rating category'** + String get broadcastFideRatingCategory; + + /// No description provided for @broadcastOptionalDetails. + /// + /// In en, this message translates to: + /// **'Optional details'** + String get broadcastOptionalDetails; + + /// No description provided for @broadcastUpcomingBroadcasts. + /// + /// In en, this message translates to: + /// **'Upcoming broadcasts'** + String get broadcastUpcomingBroadcasts; + + /// No description provided for @broadcastPastBroadcasts. + /// + /// In en, this message translates to: + /// **'Past broadcasts'** + String get broadcastPastBroadcasts; + + /// No description provided for @broadcastAllBroadcastsByMonth. + /// + /// In en, this message translates to: + /// **'View all broadcasts by month'** + String get broadcastAllBroadcastsByMonth; + /// No description provided for @broadcastNbBroadcasts. /// /// In en, this message translates to: @@ -3636,12 +3708,6 @@ abstract class AppLocalizations { /// **'By CPL'** String get byCPL; - /// No description provided for @openStudy. - /// - /// In en, this message translates to: - /// **'Open study'** - String get openStudy; - /// No description provided for @enable. /// /// In en, this message translates to: @@ -4884,12 +4950,6 @@ abstract class AppLocalizations { /// **'Unblock'** String get unblock; - /// No description provided for @followsYou. - /// - /// In en, this message translates to: - /// **'Follows you'** - String get followsYou; - /// No description provided for @xStartedFollowingY. /// /// In en, this message translates to: @@ -9408,6 +9468,12 @@ abstract class AppLocalizations { /// **'Congratulations! You completed this lesson.'** String get studyYouCompletedThisLesson; + /// No description provided for @studyPerPage. + /// + /// In en, this message translates to: + /// **'{param} per page'** + String studyPerPage(String param); + /// No description provided for @studyNbChapters. /// /// In en, this message translates to: diff --git a/lib/l10n/l10n_af.dart b/lib/l10n/l10n_af.dart index 2a5384239d..c979ea10f9 100644 --- a/lib/l10n/l10n_af.dart +++ b/lib/l10n/l10n_af.dart @@ -9,52 +9,57 @@ class AppLocalizationsAf extends AppLocalizations { AppLocalizationsAf([String locale = 'af']) : super(locale); @override - String get mobileHomeTab => 'Tuis'; + String get mobileAllGames => 'Alle spelle'; @override - String get mobilePuzzlesTab => 'Kopkrappers'; + String get mobileAreYouSure => 'Is jy seker?'; @override - String get mobileToolsTab => 'Hulpmiddels'; + String get mobileBlindfoldMode => 'Geblinddoek'; @override - String get mobileWatchTab => 'Hou dop'; + String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @override - String get mobileSettingsTab => 'Instellings'; + String get mobileClearButton => 'Clear'; @override - String get mobileMustBeLoggedIn => 'Jy moet ingeteken wees om hierdie bladsy te kan sien.'; + String get mobileCorrespondenceClearSavedMove => 'Vee gestoorde skuif uit'; @override - String get mobileSystemColors => 'Stelselkleure'; + String get mobileCustomGameJoinAGame => 'Sluit aan by \'n spel'; @override String get mobileFeedbackButton => 'Terugvoer'; @override - String get mobileOkButton => 'Reg'; + String mobileGreeting(String param) { + return 'Hallo, $param'; + } @override - String get mobileSettingsHapticFeedback => 'Vibrasieterugvoer'; + String get mobileGreetingWithoutName => 'Hallo'; @override - String get mobileSettingsImmersiveMode => 'Volskermmodus'; + String get mobileHideVariation => 'Verberg variasie'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileHomeTab => 'Tuis'; @override - String get mobileNotFollowingAnyUser => 'Jy volg nie enige gebruikers nie.'; + String get mobileLiveStreamers => 'Live streamers'; @override - String get mobileAllGames => 'Alle spelle'; + String get mobileMustBeLoggedIn => 'Jy moet ingeteken wees om hierdie bladsy te kan sien.'; @override - String get mobileRecentSearches => 'Onlangse soektogte'; + String get mobileNoSearchResults => 'Geen resultate nie'; @override - String get mobileClearButton => 'Clear'; + String get mobileNotFollowingAnyUser => 'Jy volg nie enige gebruikers nie.'; + + @override + String get mobileOkButton => 'Reg'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,84 +67,82 @@ class AppLocalizationsAf extends AppLocalizations { } @override - String get mobileNoSearchResults => 'Geen resultate nie'; + String get mobilePrefMagnifyDraggedPiece => 'Vergroot gesleepte stuk'; @override - String get mobileAreYouSure => 'Is jy seker?'; + String get mobilePuzzleStormConfirmEndRun => 'Wil jy hierdie lopie beëindig?'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; + String get mobilePuzzleStormFilterNothingToShow => 'Niks om te wys nie; verander asb. die filters'; @override String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Deel hierdie kopkrapper'; + String get mobilePuzzleStormSubtitle => 'Los soveel kopkrappers moontlik op in 3 minute.'; @override - String get mobileShareGameURL => 'Deel spel se bronadres'; + String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; @override - String get mobileShareGamePGN => 'Deel PGN'; + String get mobilePuzzleThemesSubtitle => 'Doen kopkrappers van jou gunstelingopenings, of kies \'n tema.'; @override - String get mobileSharePositionAsFEN => 'Deel posisie as FEN'; + String get mobilePuzzlesTab => 'Kopkrappers'; @override - String get mobileShowVariations => 'Wys variasies'; + String get mobileRecentSearches => 'Onlangse soektogte'; @override - String get mobileHideVariation => 'Verberg variasie'; + String get mobileSettingsHapticFeedback => 'Vibrasieterugvoer'; @override - String get mobileShowComments => 'Wys kommentaar'; + String get mobileSettingsImmersiveMode => 'Volskermmodus'; @override - String get mobilePuzzleStormConfirmEndRun => 'Wil jy hierdie lopie beëindig?'; + String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Niks om te wys nie; verander asb. die filters'; + String get mobileSettingsTab => 'Instellings'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; + String get mobileShareGamePGN => 'Deel PGN'; @override - String get mobileWaitingForOpponentToJoin => 'Wag vir opponent om aan te sluit...'; + String get mobileShareGameURL => 'Deel spel se bronadres'; @override - String get mobileBlindfoldMode => 'Geblinddoek'; + String get mobileSharePositionAsFEN => 'Deel posisie as FEN'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileSharePuzzle => 'Deel hierdie kopkrapper'; @override - String get mobileCustomGameJoinAGame => 'Sluit aan by \'n spel'; + String get mobileShowComments => 'Wys kommentaar'; @override - String get mobileCorrespondenceClearSavedMove => 'Vee gestoorde skuif uit'; + String get mobileShowResult => 'Wys resultaat'; @override - String get mobileSomethingWentWrong => 'Iets het skeefgeloop.'; + String get mobileShowVariations => 'Wys variasies'; @override - String get mobileShowResult => 'Wys resultaat'; + String get mobileSomethingWentWrong => 'Iets het skeefgeloop.'; @override - String get mobilePuzzleThemesSubtitle => 'Doen kopkrappers van jou gunstelingopenings, of kies \'n tema.'; + String get mobileSystemColors => 'Stelselkleure'; @override - String get mobilePuzzleStormSubtitle => 'Los soveel kopkrappers moontlik op in 3 minute.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Hallo, $param'; - } + String get mobileToolsTab => 'Hulpmiddels'; @override - String get mobileGreetingWithoutName => 'Hallo'; + String get mobileWaitingForOpponentToJoin => 'Wag vir opponent om aan te sluit...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Vergroot gesleepte stuk'; + String get mobileWatchTab => 'Hou dop'; @override String get activityActivity => 'Aktiwiteite'; @@ -535,6 +538,9 @@ class AppLocalizationsAf extends AppLocalizations { @override String get broadcastStandings => 'Standings'; + @override + String get broadcastOfficialStandings => 'Official Standings'; + @override String broadcastIframeHelp(String param) { return 'More options on the $param'; @@ -565,6 +571,36 @@ class AppLocalizationsAf extends AppLocalizations { @override String get broadcastScore => 'Score'; + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + @override String broadcastNbBroadcasts(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -1992,9 +2028,6 @@ class AppLocalizationsAf extends AppLocalizations { @override String get byCPL => 'Met CPL'; - @override - String get openStudy => 'Open studie'; - @override String get enable => 'Aktief'; @@ -2662,9 +2695,6 @@ class AppLocalizationsAf extends AppLocalizations { @override String get unblock => 'Ontblok'; - @override - String get followsYou => 'Volg jou'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 het begin om $param2 te volg'; @@ -5424,6 +5454,11 @@ class AppLocalizationsAf extends AppLocalizations { @override String get studyYouCompletedThisLesson => 'Geluk! Jy het hierdie les voltooi.'; + @override + String studyPerPage(String param) { + return '$param per page'; + } + @override String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( diff --git a/lib/l10n/l10n_ar.dart b/lib/l10n/l10n_ar.dart index 54bb0da1f5..04132f59bc 100644 --- a/lib/l10n/l10n_ar.dart +++ b/lib/l10n/l10n_ar.dart @@ -9,52 +9,57 @@ class AppLocalizationsAr extends AppLocalizations { AppLocalizationsAr([String locale = 'ar']) : super(locale); @override - String get mobileHomeTab => 'الرئيسية'; + String get mobileAllGames => 'جميع الألعاب'; @override - String get mobilePuzzlesTab => 'ألغاز'; + String get mobileAreYouSure => 'هل أنت واثق؟'; @override - String get mobileToolsTab => 'أدوات'; + String get mobileBlindfoldMode => 'معصوب العينين'; @override - String get mobileWatchTab => 'شاهد'; + String get mobileCancelTakebackOffer => 'إلغاء عرض الاسترداد'; @override - String get mobileSettingsTab => 'الإعدادات'; + String get mobileClearButton => 'مسح'; @override - String get mobileMustBeLoggedIn => 'سجل الدخول لعرض هذه الصفحة.'; + String get mobileCorrespondenceClearSavedMove => 'مسح النقل المحفوظ'; @override - String get mobileSystemColors => 'ألوان النظام'; + String get mobileCustomGameJoinAGame => 'الانضمام إلى لُعْبَة'; @override String get mobileFeedbackButton => 'الملاحظات'; @override - String get mobileOkButton => 'موافق'; + String mobileGreeting(String param) { + return 'مرحبا، $param'; + } @override - String get mobileSettingsHapticFeedback => 'التعليقات اللمسية'; + String get mobileGreetingWithoutName => 'مرحبا'; @override - String get mobileSettingsImmersiveMode => 'وضع ملء الشاشة'; + String get mobileHideVariation => 'إخفاء سلسلة النقلات المرشحة'; @override - String get mobileSettingsImmersiveModeSubtitle => 'إخفاء واجهة المستخدم خلال التشغيل. استخدم هذا إذا كنت مزعجاً من إيماءات التنقل للنظام عند حواف الشاشة. ينطبق على المباريات في اللعبة والألغاز.'; + String get mobileHomeTab => 'الرئيسية'; @override - String get mobileNotFollowingAnyUser => 'أنت لا تتبع أي مستخدم.'; + String get mobileLiveStreamers => 'البث المباشر'; @override - String get mobileAllGames => 'جميع الألعاب'; + String get mobileMustBeLoggedIn => 'سجل الدخول لعرض هذه الصفحة.'; @override - String get mobileRecentSearches => 'عمليات البحث الأخيرة'; + String get mobileNoSearchResults => 'لا توجد نتائج'; @override - String get mobileClearButton => 'مسح'; + String get mobileNotFollowingAnyUser => 'أنت لا تتبع أي مستخدم.'; + + @override + String get mobileOkButton => 'موافق'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,84 +67,82 @@ class AppLocalizationsAr extends AppLocalizations { } @override - String get mobileNoSearchResults => 'لا توجد نتائج'; + String get mobilePrefMagnifyDraggedPiece => 'تكبير القطعة المسحوبة'; @override - String get mobileAreYouSure => 'هل أنت واثق؟'; + String get mobilePuzzleStormConfirmEndRun => 'هل تريد إنهاء هذا التشغيل؟'; @override - String get mobilePuzzleStreakAbortWarning => 'سوف تفقد تسلقك الحالي وسيتم حفظ نتيجتك.'; + String get mobilePuzzleStormFilterNothingToShow => 'لا شيء لإظهاره، الرجاء تغيير المرشح'; @override String get mobilePuzzleStormNothingToShow => 'لا شيء لإظهاره. العب بعض الألغاز.'; @override - String get mobileSharePuzzle => 'شارك هذا اللغز'; + String get mobilePuzzleStormSubtitle => 'حل أكبر عدد ممكن من الألغاز في 3 دقائق.'; @override - String get mobileShareGameURL => 'شارك رابط المباراة'; + String get mobilePuzzleStreakAbortWarning => 'سوف تفقد تسلقك الحالي وسيتم حفظ نتيجتك.'; @override - String get mobileShareGamePGN => 'شارك الPGN'; + String get mobilePuzzleThemesSubtitle => 'حُل الألغاز المتعلّقة بافتتاحاتك المفضّلة، أو اختر موضوعاً.'; @override - String get mobileSharePositionAsFEN => 'مشاركة الموضع كFEN'; + String get mobilePuzzlesTab => 'ألغاز'; @override - String get mobileShowVariations => 'أظهر سلسلة النقلات المرشحة'; + String get mobileRecentSearches => 'عمليات البحث الأخيرة'; @override - String get mobileHideVariation => 'إخفاء سلسلة النقلات المرشحة'; + String get mobileSettingsHapticFeedback => 'التعليقات اللمسية'; @override - String get mobileShowComments => 'عرض التعليقات'; + String get mobileSettingsImmersiveMode => 'وضع ملء الشاشة'; @override - String get mobilePuzzleStormConfirmEndRun => 'هل تريد إنهاء هذا التشغيل؟'; + String get mobileSettingsImmersiveModeSubtitle => 'إخفاء واجهة المستخدم خلال التشغيل. استخدم هذا إذا كنت مزعجاً من إيماءات التنقل للنظام عند حواف الشاشة. ينطبق على المباريات في اللعبة والألغاز.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'لا شيء لإظهاره، الرجاء تغيير المرشح'; + String get mobileSettingsTab => 'الإعدادات'; @override - String get mobileCancelTakebackOffer => 'إلغاء عرض الاسترداد'; + String get mobileShareGamePGN => 'شارك الPGN'; @override - String get mobileWaitingForOpponentToJoin => 'في انتظار انضمام الطرف الآخر...'; + String get mobileShareGameURL => 'شارك رابط المباراة'; @override - String get mobileBlindfoldMode => 'معصوب العينين'; + String get mobileSharePositionAsFEN => 'مشاركة الموضع كFEN'; @override - String get mobileLiveStreamers => 'البث المباشر'; + String get mobileSharePuzzle => 'شارك هذا اللغز'; @override - String get mobileCustomGameJoinAGame => 'الانضمام إلى لُعْبَة'; + String get mobileShowComments => 'عرض التعليقات'; @override - String get mobileCorrespondenceClearSavedMove => 'مسح النقل المحفوظ'; + String get mobileShowResult => 'إظهار النتيجة'; @override - String get mobileSomethingWentWrong => 'لقد حدث خطأ ما.'; + String get mobileShowVariations => 'أظهر سلسلة النقلات المرشحة'; @override - String get mobileShowResult => 'إظهار النتيجة'; + String get mobileSomethingWentWrong => 'لقد حدث خطأ ما.'; @override - String get mobilePuzzleThemesSubtitle => 'حُل الألغاز المتعلّقة بافتتاحاتك المفضّلة، أو اختر موضوعاً.'; + String get mobileSystemColors => 'ألوان النظام'; @override - String get mobilePuzzleStormSubtitle => 'حل أكبر عدد ممكن من الألغاز في 3 دقائق.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'مرحبا، $param'; - } + String get mobileToolsTab => 'أدوات'; @override - String get mobileGreetingWithoutName => 'مرحبا'; + String get mobileWaitingForOpponentToJoin => 'في انتظار انضمام الطرف الآخر...'; @override - String get mobilePrefMagnifyDraggedPiece => 'تكبير القطعة المسحوبة'; + String get mobileWatchTab => 'شاهد'; @override String get activityActivity => 'الأنشطة'; @@ -603,6 +606,9 @@ class AppLocalizationsAr extends AppLocalizations { @override String get broadcastStandings => 'Standings'; + @override + String get broadcastOfficialStandings => 'Official Standings'; + @override String broadcastIframeHelp(String param) { return 'More options on the $param'; @@ -633,6 +639,36 @@ class AppLocalizationsAr extends AppLocalizations { @override String get broadcastScore => 'Score'; + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + @override String broadcastNbBroadcasts(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -2084,9 +2120,6 @@ class AppLocalizationsAr extends AppLocalizations { @override String get byCPL => 'بالاثارة'; - @override - String get openStudy => 'فتح دراسة'; - @override String get enable => 'تفعيل'; @@ -2754,9 +2787,6 @@ class AppLocalizationsAr extends AppLocalizations { @override String get unblock => 'إلغاء الحظر'; - @override - String get followsYou => 'يتابعك'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 بدأ متابعة $param2'; @@ -5692,6 +5722,11 @@ class AppLocalizationsAr extends AppLocalizations { @override String get studyYouCompletedThisLesson => 'تهانينا! لقد أكملت هذا الدرس.'; + @override + String studyPerPage(String param) { + return '$param per page'; + } + @override String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( diff --git a/lib/l10n/l10n_az.dart b/lib/l10n/l10n_az.dart index 6f983e0a0b..36611cb1d5 100644 --- a/lib/l10n/l10n_az.dart +++ b/lib/l10n/l10n_az.dart @@ -9,52 +9,57 @@ class AppLocalizationsAz extends AppLocalizations { AppLocalizationsAz([String locale = 'az']) : super(locale); @override - String get mobileHomeTab => 'Home'; + String get mobileAllGames => 'All games'; @override - String get mobilePuzzlesTab => 'Puzzles'; + String get mobileAreYouSure => 'Are you sure?'; @override - String get mobileToolsTab => 'Tools'; + String get mobileBlindfoldMode => 'Blindfold'; @override - String get mobileWatchTab => 'Watch'; + String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @override - String get mobileSettingsTab => 'Settings'; + String get mobileClearButton => 'Clear'; @override - String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; + String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; @override - String get mobileSystemColors => 'System colors'; + String get mobileCustomGameJoinAGame => 'Join a game'; @override String get mobileFeedbackButton => 'Feedback'; @override - String get mobileOkButton => 'OK'; + String mobileGreeting(String param) { + return 'Hello, $param'; + } @override - String get mobileSettingsHapticFeedback => 'Haptic feedback'; + String get mobileGreetingWithoutName => 'Hello'; @override - String get mobileSettingsImmersiveMode => 'Immersive mode'; + String get mobileHideVariation => 'Hide variation'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileHomeTab => 'Home'; @override - String get mobileNotFollowingAnyUser => 'You are not following any user.'; + String get mobileLiveStreamers => 'Live streamers'; @override - String get mobileAllGames => 'All games'; + String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; @override - String get mobileRecentSearches => 'Recent searches'; + String get mobileNoSearchResults => 'No results'; @override - String get mobileClearButton => 'Clear'; + String get mobileNotFollowingAnyUser => 'You are not following any user.'; + + @override + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,84 +67,82 @@ class AppLocalizationsAz extends AppLocalizations { } @override - String get mobileNoSearchResults => 'No results'; + String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; @override - String get mobileAreYouSure => 'Are you sure?'; + String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; @override String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Share this puzzle'; + String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; @override - String get mobileShareGameURL => 'Share game URL'; + String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; @override - String get mobileShareGamePGN => 'Share PGN'; + String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; @override - String get mobileSharePositionAsFEN => 'Share position as FEN'; + String get mobilePuzzlesTab => 'Puzzles'; @override - String get mobileShowVariations => 'Show variations'; + String get mobileRecentSearches => 'Recent searches'; @override - String get mobileHideVariation => 'Hide variation'; + String get mobileSettingsHapticFeedback => 'Haptic feedback'; @override - String get mobileShowComments => 'Show comments'; + String get mobileSettingsImmersiveMode => 'Immersive mode'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobileSettingsTab => 'Settings'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; + String get mobileShareGamePGN => 'Share PGN'; @override - String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; + String get mobileShareGameURL => 'Share game URL'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get mobileSharePositionAsFEN => 'Share position as FEN'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileSharePuzzle => 'Share this puzzle'; @override - String get mobileCustomGameJoinAGame => 'Join a game'; + String get mobileShowComments => 'Show comments'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; + String get mobileShowResult => 'Show result'; @override - String get mobileSomethingWentWrong => 'Something went wrong.'; + String get mobileShowVariations => 'Show variations'; @override - String get mobileShowResult => 'Show result'; + String get mobileSomethingWentWrong => 'Something went wrong.'; @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobileSystemColors => 'System colors'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Hello, $param'; - } + String get mobileToolsTab => 'Tools'; @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Watch'; @override String get activityActivity => 'Aktivlik'; @@ -535,6 +538,9 @@ class AppLocalizationsAz extends AppLocalizations { @override String get broadcastStandings => 'Standings'; + @override + String get broadcastOfficialStandings => 'Official Standings'; + @override String broadcastIframeHelp(String param) { return 'More options on the $param'; @@ -565,6 +571,36 @@ class AppLocalizationsAz extends AppLocalizations { @override String get broadcastScore => 'Score'; + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + @override String broadcastNbBroadcasts(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -1990,9 +2026,6 @@ class AppLocalizationsAz extends AppLocalizations { @override String get byCPL => 'CPL üzrə'; - @override - String get openStudy => 'Çalışmanı aç'; - @override String get enable => 'Aktiv et'; @@ -2660,9 +2693,6 @@ class AppLocalizationsAz extends AppLocalizations { @override String get unblock => 'Blokdan çıxart'; - @override - String get followsYou => 'Səni izləyir'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 $param2 adlı oyunçunu izləməyə başladı'; @@ -5422,6 +5452,11 @@ class AppLocalizationsAz extends AppLocalizations { @override String get studyYouCompletedThisLesson => 'Congratulations! You completed this lesson.'; + @override + String studyPerPage(String param) { + return '$param per page'; + } + @override String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( diff --git a/lib/l10n/l10n_be.dart b/lib/l10n/l10n_be.dart index b52ca6007a..714b4b2b01 100644 --- a/lib/l10n/l10n_be.dart +++ b/lib/l10n/l10n_be.dart @@ -9,52 +9,57 @@ class AppLocalizationsBe extends AppLocalizations { AppLocalizationsBe([String locale = 'be']) : super(locale); @override - String get mobileHomeTab => 'Галоўная'; + String get mobileAllGames => 'All games'; @override - String get mobilePuzzlesTab => 'Задачы'; + String get mobileAreYouSure => 'Вы ўпэўнены?'; @override - String get mobileToolsTab => 'Tools'; + String get mobileBlindfoldMode => 'Blindfold'; @override - String get mobileWatchTab => 'Watch'; + String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @override - String get mobileSettingsTab => 'Налады'; + String get mobileClearButton => 'Ачысціць'; @override - String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; + String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; @override - String get mobileSystemColors => 'System colors'; + String get mobileCustomGameJoinAGame => 'Join a game'; @override String get mobileFeedbackButton => 'Feedback'; @override - String get mobileOkButton => 'Добра'; + String mobileGreeting(String param) { + return 'Hello, $param'; + } @override - String get mobileSettingsHapticFeedback => 'Haptic feedback'; + String get mobileGreetingWithoutName => 'Hello'; @override - String get mobileSettingsImmersiveMode => 'Поўнаэкранны рэжым'; + String get mobileHideVariation => 'Hide variation'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileHomeTab => 'Галоўная'; @override - String get mobileNotFollowingAnyUser => 'You are not following any user.'; + String get mobileLiveStreamers => 'Live streamers'; @override - String get mobileAllGames => 'All games'; + String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; @override - String get mobileRecentSearches => 'Нядаўнія пошукі'; + String get mobileNoSearchResults => 'Няма вынікаў'; @override - String get mobileClearButton => 'Ачысціць'; + String get mobileNotFollowingAnyUser => 'You are not following any user.'; + + @override + String get mobileOkButton => 'Добра'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,84 +67,82 @@ class AppLocalizationsBe extends AppLocalizations { } @override - String get mobileNoSearchResults => 'Няма вынікаў'; + String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; @override - String get mobileAreYouSure => 'Вы ўпэўнены?'; + String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; @override String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Share this puzzle'; + String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; @override - String get mobileShareGameURL => 'Share game URL'; + String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; @override - String get mobileShareGamePGN => 'Share PGN'; + String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; @override - String get mobileSharePositionAsFEN => 'Share position as FEN'; + String get mobilePuzzlesTab => 'Задачы'; @override - String get mobileShowVariations => 'Show variations'; + String get mobileRecentSearches => 'Нядаўнія пошукі'; @override - String get mobileHideVariation => 'Hide variation'; + String get mobileSettingsHapticFeedback => 'Haptic feedback'; @override - String get mobileShowComments => 'Show comments'; + String get mobileSettingsImmersiveMode => 'Поўнаэкранны рэжым'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobileSettingsTab => 'Налады'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; + String get mobileShareGamePGN => 'Share PGN'; @override - String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; + String get mobileShareGameURL => 'Share game URL'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get mobileSharePositionAsFEN => 'Share position as FEN'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileSharePuzzle => 'Share this puzzle'; @override - String get mobileCustomGameJoinAGame => 'Join a game'; + String get mobileShowComments => 'Show comments'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; + String get mobileShowResult => 'Show result'; @override - String get mobileSomethingWentWrong => 'Something went wrong.'; + String get mobileShowVariations => 'Show variations'; @override - String get mobileShowResult => 'Show result'; + String get mobileSomethingWentWrong => 'Something went wrong.'; @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobileSystemColors => 'System colors'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Hello, $param'; - } + String get mobileToolsTab => 'Tools'; @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Watch'; @override String get activityActivity => 'Актыўнасць'; @@ -569,6 +572,9 @@ class AppLocalizationsBe extends AppLocalizations { @override String get broadcastStandings => 'Standings'; + @override + String get broadcastOfficialStandings => 'Official Standings'; + @override String broadcastIframeHelp(String param) { return 'More options on the $param'; @@ -599,6 +605,36 @@ class AppLocalizationsBe extends AppLocalizations { @override String get broadcastScore => 'Score'; + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + @override String broadcastNbBroadcasts(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -2036,9 +2072,6 @@ class AppLocalizationsBe extends AppLocalizations { @override String get byCPL => 'Цікавае'; - @override - String get openStudy => 'Адкрыць навучанне'; - @override String get enable => 'Уключыць'; @@ -2706,9 +2739,6 @@ class AppLocalizationsBe extends AppLocalizations { @override String get unblock => 'Разблакіраваць'; - @override - String get followsYou => 'Падпісаны на вас'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 падпісаўся на $param2'; @@ -5552,6 +5582,11 @@ class AppLocalizationsBe extends AppLocalizations { @override String get studyYouCompletedThisLesson => 'Віншуем! Вы прайшлі гэты ўрок.'; + @override + String studyPerPage(String param) { + return '$param per page'; + } + @override String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( diff --git a/lib/l10n/l10n_bg.dart b/lib/l10n/l10n_bg.dart index 8ef8d6d0bb..7c82498b7b 100644 --- a/lib/l10n/l10n_bg.dart +++ b/lib/l10n/l10n_bg.dart @@ -9,52 +9,57 @@ class AppLocalizationsBg extends AppLocalizations { AppLocalizationsBg([String locale = 'bg']) : super(locale); @override - String get mobileHomeTab => 'Начало'; + String get mobileAllGames => 'Всички игри'; @override - String get mobilePuzzlesTab => 'Задачи'; + String get mobileAreYouSure => 'Сигурни ли сте?'; @override - String get mobileToolsTab => 'Анализ'; + String get mobileBlindfoldMode => 'Blindfold'; @override - String get mobileWatchTab => 'Гледай'; + String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @override - String get mobileSettingsTab => 'Настройки'; + String get mobileClearButton => 'Изчисти'; @override - String get mobileMustBeLoggedIn => 'За да видите тази страница, трябва да влезете в профила си.'; + String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; @override - String get mobileSystemColors => 'Системни цветове'; + String get mobileCustomGameJoinAGame => 'Join a game'; @override String get mobileFeedbackButton => 'Отзиви'; @override - String get mobileOkButton => 'ОК'; + String mobileGreeting(String param) { + return 'Здравейте, $param'; + } @override - String get mobileSettingsHapticFeedback => 'Вибрация при докосване'; + String get mobileGreetingWithoutName => 'Здравейте'; @override - String get mobileSettingsImmersiveMode => 'Режим \"Цял екран\"'; + String get mobileHideVariation => 'Скрий вариацията'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileHomeTab => 'Начало'; @override - String get mobileNotFollowingAnyUser => 'You are not following any user.'; + String get mobileLiveStreamers => 'Live streamers'; @override - String get mobileAllGames => 'Всички игри'; + String get mobileMustBeLoggedIn => 'За да видите тази страница, трябва да влезете в профила си.'; @override - String get mobileRecentSearches => 'Последни търсения'; + String get mobileNoSearchResults => 'Няма резултати'; @override - String get mobileClearButton => 'Изчисти'; + String get mobileNotFollowingAnyUser => 'You are not following any user.'; + + @override + String get mobileOkButton => 'ОК'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,84 +67,82 @@ class AppLocalizationsBg extends AppLocalizations { } @override - String get mobileNoSearchResults => 'Няма резултати'; + String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; @override - String get mobileAreYouSure => 'Сигурни ли сте?'; + String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; @override String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Сподели тази задача'; + String get mobilePuzzleStormSubtitle => 'Решете колкото можете повече задачи за 3 минути.'; @override - String get mobileShareGameURL => 'Сподели URL на играта'; + String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; @override - String get mobileShareGamePGN => 'Сподели PGN'; + String get mobilePuzzleThemesSubtitle => 'Решавайте задачи от любимите Ви дебюти или изберете друга тема.'; @override - String get mobileSharePositionAsFEN => 'Сподели позицията във формат FEN'; + String get mobilePuzzlesTab => 'Задачи'; @override - String get mobileShowVariations => 'Покажи вариациите'; + String get mobileRecentSearches => 'Последни търсения'; @override - String get mobileHideVariation => 'Скрий вариацията'; + String get mobileSettingsHapticFeedback => 'Вибрация при докосване'; @override - String get mobileShowComments => 'Покажи коментарите'; + String get mobileSettingsImmersiveMode => 'Режим \"Цял екран\"'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobileSettingsTab => 'Настройки'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; + String get mobileShareGamePGN => 'Сподели PGN'; @override - String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; + String get mobileShareGameURL => 'Сподели URL на играта'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get mobileSharePositionAsFEN => 'Сподели позицията във формат FEN'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileSharePuzzle => 'Сподели тази задача'; @override - String get mobileCustomGameJoinAGame => 'Join a game'; + String get mobileShowComments => 'Покажи коментарите'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; + String get mobileShowResult => 'Покажи резултат'; @override - String get mobileSomethingWentWrong => 'Възникна грешка.'; + String get mobileShowVariations => 'Покажи вариациите'; @override - String get mobileShowResult => 'Покажи резултат'; + String get mobileSomethingWentWrong => 'Възникна грешка.'; @override - String get mobilePuzzleThemesSubtitle => 'Решавайте задачи от любимите Ви дебюти или изберете друга тема.'; + String get mobileSystemColors => 'Системни цветове'; @override - String get mobilePuzzleStormSubtitle => 'Решете колкото можете повече задачи за 3 минути.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Здравейте, $param'; - } + String get mobileToolsTab => 'Анализ'; @override - String get mobileGreetingWithoutName => 'Здравейте'; + String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Гледай'; @override String get activityActivity => 'Дейност'; @@ -535,6 +538,9 @@ class AppLocalizationsBg extends AppLocalizations { @override String get broadcastStandings => 'Standings'; + @override + String get broadcastOfficialStandings => 'Official Standings'; + @override String broadcastIframeHelp(String param) { return 'More options on the $param'; @@ -565,6 +571,36 @@ class AppLocalizationsBg extends AppLocalizations { @override String get broadcastScore => 'Score'; + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + @override String broadcastNbBroadcasts(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -1992,9 +2028,6 @@ class AppLocalizationsBg extends AppLocalizations { @override String get byCPL => 'По CPL'; - @override - String get openStudy => 'Проучване'; - @override String get enable => 'Включване'; @@ -2662,9 +2695,6 @@ class AppLocalizationsBg extends AppLocalizations { @override String get unblock => 'Отблокирай'; - @override - String get followsYou => 'Следва ви'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 започна да следва $param2'; @@ -5424,6 +5454,11 @@ class AppLocalizationsBg extends AppLocalizations { @override String get studyYouCompletedThisLesson => 'Поздравления! Вие завършихте този урок.'; + @override + String studyPerPage(String param) { + return '$param per page'; + } + @override String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( diff --git a/lib/l10n/l10n_bn.dart b/lib/l10n/l10n_bn.dart index 4cb7ef5ee3..45536f57d8 100644 --- a/lib/l10n/l10n_bn.dart +++ b/lib/l10n/l10n_bn.dart @@ -9,52 +9,57 @@ class AppLocalizationsBn extends AppLocalizations { AppLocalizationsBn([String locale = 'bn']) : super(locale); @override - String get mobileHomeTab => 'Home'; + String get mobileAllGames => 'All games'; @override - String get mobilePuzzlesTab => 'Puzzles'; + String get mobileAreYouSure => 'Are you sure?'; @override - String get mobileToolsTab => 'Tools'; + String get mobileBlindfoldMode => 'Blindfold'; @override - String get mobileWatchTab => 'Watch'; + String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @override - String get mobileSettingsTab => 'Settings'; + String get mobileClearButton => 'Clear'; @override - String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; + String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; @override - String get mobileSystemColors => 'System colors'; + String get mobileCustomGameJoinAGame => 'Join a game'; @override String get mobileFeedbackButton => 'Feedback'; @override - String get mobileOkButton => 'OK'; + String mobileGreeting(String param) { + return 'Hello, $param'; + } @override - String get mobileSettingsHapticFeedback => 'Haptic feedback'; + String get mobileGreetingWithoutName => 'Hello'; @override - String get mobileSettingsImmersiveMode => 'Immersive mode'; + String get mobileHideVariation => 'Hide variation'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileHomeTab => 'Home'; @override - String get mobileNotFollowingAnyUser => 'You are not following any user.'; + String get mobileLiveStreamers => 'Live streamers'; @override - String get mobileAllGames => 'All games'; + String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; @override - String get mobileRecentSearches => 'Recent searches'; + String get mobileNoSearchResults => 'No results'; @override - String get mobileClearButton => 'Clear'; + String get mobileNotFollowingAnyUser => 'You are not following any user.'; + + @override + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,84 +67,82 @@ class AppLocalizationsBn extends AppLocalizations { } @override - String get mobileNoSearchResults => 'No results'; + String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; @override - String get mobileAreYouSure => 'Are you sure?'; + String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; @override String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Share this puzzle'; + String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; @override - String get mobileShareGameURL => 'Share game URL'; + String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; @override - String get mobileShareGamePGN => 'Share PGN'; + String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; @override - String get mobileSharePositionAsFEN => 'Share position as FEN'; + String get mobilePuzzlesTab => 'Puzzles'; @override - String get mobileShowVariations => 'Show variations'; + String get mobileRecentSearches => 'Recent searches'; @override - String get mobileHideVariation => 'Hide variation'; + String get mobileSettingsHapticFeedback => 'Haptic feedback'; @override - String get mobileShowComments => 'Show comments'; + String get mobileSettingsImmersiveMode => 'Immersive mode'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobileSettingsTab => 'Settings'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; + String get mobileShareGamePGN => 'Share PGN'; @override - String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; + String get mobileShareGameURL => 'Share game URL'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get mobileSharePositionAsFEN => 'Share position as FEN'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileSharePuzzle => 'Share this puzzle'; @override - String get mobileCustomGameJoinAGame => 'Join a game'; + String get mobileShowComments => 'Show comments'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; + String get mobileShowResult => 'Show result'; @override - String get mobileSomethingWentWrong => 'Something went wrong.'; + String get mobileShowVariations => 'Show variations'; @override - String get mobileShowResult => 'Show result'; + String get mobileSomethingWentWrong => 'Something went wrong.'; @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobileSystemColors => 'System colors'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Hello, $param'; - } + String get mobileToolsTab => 'Tools'; @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Watch'; @override String get activityActivity => 'কার্যকলাপ'; @@ -535,6 +538,9 @@ class AppLocalizationsBn extends AppLocalizations { @override String get broadcastStandings => 'Standings'; + @override + String get broadcastOfficialStandings => 'Official Standings'; + @override String broadcastIframeHelp(String param) { return 'More options on the $param'; @@ -565,6 +571,36 @@ class AppLocalizationsBn extends AppLocalizations { @override String get broadcastScore => 'Score'; + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + @override String broadcastNbBroadcasts(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -1992,9 +2028,6 @@ class AppLocalizationsBn extends AppLocalizations { @override String get byCPL => 'CPL দ্বারা'; - @override - String get openStudy => 'মুক্ত অধ্যয়ন'; - @override String get enable => 'সচল'; @@ -2662,9 +2695,6 @@ class AppLocalizationsBn extends AppLocalizations { @override String get unblock => 'বাধা উঠিয়ে নিন'; - @override - String get followsYou => 'আপনাকে অনুসরণ করছে'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 অনুসরণ করা শুরু করেছেন $param2'; @@ -5424,6 +5454,11 @@ class AppLocalizationsBn extends AppLocalizations { @override String get studyYouCompletedThisLesson => 'Congratulations! You completed this lesson.'; + @override + String studyPerPage(String param) { + return '$param per page'; + } + @override String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( diff --git a/lib/l10n/l10n_br.dart b/lib/l10n/l10n_br.dart index 7117adb327..4695b5d6bf 100644 --- a/lib/l10n/l10n_br.dart +++ b/lib/l10n/l10n_br.dart @@ -9,52 +9,57 @@ class AppLocalizationsBr extends AppLocalizations { AppLocalizationsBr([String locale = 'br']) : super(locale); @override - String get mobileHomeTab => 'Home'; + String get mobileAllGames => 'All games'; @override - String get mobilePuzzlesTab => 'Puzzles'; + String get mobileAreYouSure => 'Are you sure?'; @override - String get mobileToolsTab => 'Tools'; + String get mobileBlindfoldMode => 'Blindfold'; @override - String get mobileWatchTab => 'Watch'; + String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @override - String get mobileSettingsTab => 'Settings'; + String get mobileClearButton => 'Clear'; @override - String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; + String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; @override - String get mobileSystemColors => 'System colors'; + String get mobileCustomGameJoinAGame => 'Join a game'; @override String get mobileFeedbackButton => 'Feedback'; @override - String get mobileOkButton => 'OK'; + String mobileGreeting(String param) { + return 'Hello, $param'; + } @override - String get mobileSettingsHapticFeedback => 'Haptic feedback'; + String get mobileGreetingWithoutName => 'Hello'; @override - String get mobileSettingsImmersiveMode => 'Immersive mode'; + String get mobileHideVariation => 'Hide variation'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileHomeTab => 'Home'; @override - String get mobileNotFollowingAnyUser => 'You are not following any user.'; + String get mobileLiveStreamers => 'Live streamers'; @override - String get mobileAllGames => 'All games'; + String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; @override - String get mobileRecentSearches => 'Recent searches'; + String get mobileNoSearchResults => 'No results'; @override - String get mobileClearButton => 'Clear'; + String get mobileNotFollowingAnyUser => 'You are not following any user.'; + + @override + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,84 +67,82 @@ class AppLocalizationsBr extends AppLocalizations { } @override - String get mobileNoSearchResults => 'No results'; + String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; @override - String get mobileAreYouSure => 'Are you sure?'; + String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; @override String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Share this puzzle'; + String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; @override - String get mobileShareGameURL => 'Share game URL'; + String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; @override - String get mobileShareGamePGN => 'Share PGN'; + String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; @override - String get mobileSharePositionAsFEN => 'Share position as FEN'; + String get mobilePuzzlesTab => 'Puzzles'; @override - String get mobileShowVariations => 'Show variations'; + String get mobileRecentSearches => 'Recent searches'; @override - String get mobileHideVariation => 'Hide variation'; + String get mobileSettingsHapticFeedback => 'Haptic feedback'; @override - String get mobileShowComments => 'Show comments'; + String get mobileSettingsImmersiveMode => 'Immersive mode'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobileSettingsTab => 'Settings'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; + String get mobileShareGamePGN => 'Share PGN'; @override - String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; + String get mobileShareGameURL => 'Share game URL'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get mobileSharePositionAsFEN => 'Share position as FEN'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileSharePuzzle => 'Share this puzzle'; @override - String get mobileCustomGameJoinAGame => 'Join a game'; + String get mobileShowComments => 'Show comments'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; + String get mobileShowResult => 'Show result'; @override - String get mobileSomethingWentWrong => 'Something went wrong.'; + String get mobileShowVariations => 'Show variations'; @override - String get mobileShowResult => 'Show result'; + String get mobileSomethingWentWrong => 'Something went wrong.'; @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobileSystemColors => 'System colors'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Hello, $param'; - } + String get mobileToolsTab => 'Tools'; @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Watch'; @override String get activityActivity => 'Obererezhioù diwezhañ'; @@ -586,6 +589,9 @@ class AppLocalizationsBr extends AppLocalizations { @override String get broadcastStandings => 'Standings'; + @override + String get broadcastOfficialStandings => 'Official Standings'; + @override String broadcastIframeHelp(String param) { return 'More options on the $param'; @@ -616,6 +622,36 @@ class AppLocalizationsBr extends AppLocalizations { @override String get broadcastScore => 'Score'; + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + @override String broadcastNbBroadcasts(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -2044,9 +2080,6 @@ class AppLocalizationsBr extends AppLocalizations { @override String get byCPL => 'Dre CPL'; - @override - String get openStudy => 'Digeriñ ar studi'; - @override String get enable => 'Enaouiñ'; @@ -2714,9 +2747,6 @@ class AppLocalizationsBr extends AppLocalizations { @override String get unblock => 'Distankañ'; - @override - String get followsYou => 'Ho heuilh'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 zo krog da heuliañ $param2'; @@ -5602,6 +5632,11 @@ class AppLocalizationsBr extends AppLocalizations { @override String get studyYouCompletedThisLesson => 'Congratulations! You completed this lesson.'; + @override + String studyPerPage(String param) { + return '$param per page'; + } + @override String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( diff --git a/lib/l10n/l10n_bs.dart b/lib/l10n/l10n_bs.dart index 92b421d2a0..f9389f1eb9 100644 --- a/lib/l10n/l10n_bs.dart +++ b/lib/l10n/l10n_bs.dart @@ -9,52 +9,57 @@ class AppLocalizationsBs extends AppLocalizations { AppLocalizationsBs([String locale = 'bs']) : super(locale); @override - String get mobileHomeTab => 'Home'; + String get mobileAllGames => 'All games'; @override - String get mobilePuzzlesTab => 'Puzzles'; + String get mobileAreYouSure => 'Are you sure?'; @override - String get mobileToolsTab => 'Tools'; + String get mobileBlindfoldMode => 'Blindfold'; @override - String get mobileWatchTab => 'Watch'; + String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @override - String get mobileSettingsTab => 'Settings'; + String get mobileClearButton => 'Clear'; @override - String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; + String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; @override - String get mobileSystemColors => 'System colors'; + String get mobileCustomGameJoinAGame => 'Join a game'; @override String get mobileFeedbackButton => 'Feedback'; @override - String get mobileOkButton => 'OK'; + String mobileGreeting(String param) { + return 'Hello, $param'; + } @override - String get mobileSettingsHapticFeedback => 'Haptic feedback'; + String get mobileGreetingWithoutName => 'Hello'; @override - String get mobileSettingsImmersiveMode => 'Immersive mode'; + String get mobileHideVariation => 'Hide variation'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileHomeTab => 'Home'; @override - String get mobileNotFollowingAnyUser => 'You are not following any user.'; + String get mobileLiveStreamers => 'Live streamers'; @override - String get mobileAllGames => 'All games'; + String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; @override - String get mobileRecentSearches => 'Recent searches'; + String get mobileNoSearchResults => 'No results'; @override - String get mobileClearButton => 'Clear'; + String get mobileNotFollowingAnyUser => 'You are not following any user.'; + + @override + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,84 +67,82 @@ class AppLocalizationsBs extends AppLocalizations { } @override - String get mobileNoSearchResults => 'No results'; + String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; @override - String get mobileAreYouSure => 'Are you sure?'; + String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; @override String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Share this puzzle'; + String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; @override - String get mobileShareGameURL => 'Share game URL'; + String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; @override - String get mobileShareGamePGN => 'Share PGN'; + String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; @override - String get mobileSharePositionAsFEN => 'Share position as FEN'; + String get mobilePuzzlesTab => 'Puzzles'; @override - String get mobileShowVariations => 'Show variations'; + String get mobileRecentSearches => 'Recent searches'; @override - String get mobileHideVariation => 'Hide variation'; + String get mobileSettingsHapticFeedback => 'Haptic feedback'; @override - String get mobileShowComments => 'Show comments'; + String get mobileSettingsImmersiveMode => 'Immersive mode'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobileSettingsTab => 'Settings'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; + String get mobileShareGamePGN => 'Share PGN'; @override - String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; + String get mobileShareGameURL => 'Share game URL'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get mobileSharePositionAsFEN => 'Share position as FEN'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileSharePuzzle => 'Share this puzzle'; @override - String get mobileCustomGameJoinAGame => 'Join a game'; + String get mobileShowComments => 'Show comments'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; + String get mobileShowResult => 'Show result'; @override - String get mobileSomethingWentWrong => 'Something went wrong.'; + String get mobileShowVariations => 'Show variations'; @override - String get mobileShowResult => 'Show result'; + String get mobileSomethingWentWrong => 'Something went wrong.'; @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobileSystemColors => 'System colors'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Hello, $param'; - } + String get mobileToolsTab => 'Tools'; @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Watch'; @override String get activityActivity => 'Aktivnost'; @@ -552,6 +555,9 @@ class AppLocalizationsBs extends AppLocalizations { @override String get broadcastStandings => 'Standings'; + @override + String get broadcastOfficialStandings => 'Official Standings'; + @override String broadcastIframeHelp(String param) { return 'More options on the $param'; @@ -582,6 +588,36 @@ class AppLocalizationsBs extends AppLocalizations { @override String get broadcastScore => 'Score'; + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + @override String broadcastNbBroadcasts(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -2015,9 +2051,6 @@ class AppLocalizationsBs extends AppLocalizations { @override String get byCPL => 'Po SDP'; - @override - String get openStudy => 'Otvori studiju'; - @override String get enable => 'Omogući'; @@ -2685,9 +2718,6 @@ class AppLocalizationsBs extends AppLocalizations { @override String get unblock => 'Odblokiraj'; - @override - String get followsYou => 'Prati vas'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 je počeo pratiti $param2'; @@ -5491,6 +5521,11 @@ class AppLocalizationsBs extends AppLocalizations { @override String get studyYouCompletedThisLesson => 'Čestitamo! Kompletirali ste ovu lekciju.'; + @override + String studyPerPage(String param) { + return '$param per page'; + } + @override String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( diff --git a/lib/l10n/l10n_ca.dart b/lib/l10n/l10n_ca.dart index 9b0e1864bb..70ee7859a5 100644 --- a/lib/l10n/l10n_ca.dart +++ b/lib/l10n/l10n_ca.dart @@ -9,52 +9,57 @@ class AppLocalizationsCa extends AppLocalizations { AppLocalizationsCa([String locale = 'ca']) : super(locale); @override - String get mobileHomeTab => 'Inici'; + String get mobileAllGames => 'Totes les partides'; @override - String get mobilePuzzlesTab => 'Problemes'; + String get mobileAreYouSure => 'Estàs segur?'; @override - String get mobileToolsTab => 'Eines'; + String get mobileBlindfoldMode => 'A la cega'; @override - String get mobileWatchTab => 'Visualitza'; + String get mobileCancelTakebackOffer => 'Anul·la la petició per desfer la jugada'; @override - String get mobileSettingsTab => 'Configuració'; + String get mobileClearButton => 'Neteja'; @override - String get mobileMustBeLoggedIn => 'Has d\'estar connectat per veure aquesta pàgina.'; + String get mobileCorrespondenceClearSavedMove => 'Elimina la jugada guardada'; @override - String get mobileSystemColors => 'Colors del sistema'; + String get mobileCustomGameJoinAGame => 'Unir-se a una partida'; @override String get mobileFeedbackButton => 'Suggeriments'; @override - String get mobileOkButton => 'D\'acord'; + String mobileGreeting(String param) { + return 'Hola, $param'; + } @override - String get mobileSettingsHapticFeedback => 'Resposta hàptica'; + String get mobileGreetingWithoutName => 'Hola'; @override - String get mobileSettingsImmersiveMode => 'Mode immersiu'; + String get mobileHideVariation => 'Amaga les variacions'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileHomeTab => 'Inici'; @override - String get mobileNotFollowingAnyUser => 'No estàs seguint a cap usuari.'; + String get mobileLiveStreamers => 'Retransmissors en directe'; @override - String get mobileAllGames => 'Totes les partides'; + String get mobileMustBeLoggedIn => 'Has d\'estar connectat per veure aquesta pàgina.'; @override - String get mobileRecentSearches => 'Cerques recents'; + String get mobileNoSearchResults => 'Sense resultats'; @override - String get mobileClearButton => 'Neteja'; + String get mobileNotFollowingAnyUser => 'No estàs seguint a cap usuari.'; + + @override + String get mobileOkButton => 'D\'acord'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,84 +67,82 @@ class AppLocalizationsCa extends AppLocalizations { } @override - String get mobileNoSearchResults => 'Sense resultats'; + String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; @override - String get mobileAreYouSure => 'Estàs segur?'; + String get mobilePuzzleStormConfirmEndRun => 'Voleu acabar aquesta ronda?'; @override - String get mobilePuzzleStreakAbortWarning => 'Perdreu la vostra ratxa i la vostra puntuació es guardarà.'; + String get mobilePuzzleStormFilterNothingToShow => 'Res a mostrar, si us plau canvieu els filtres'; @override String get mobilePuzzleStormNothingToShow => 'Res a mostrar. Fes algunes rondes al Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Comparteix aquest problema'; + String get mobilePuzzleStormSubtitle => 'Resoleu el màxim nombre de problemes en 3 minuts.'; @override - String get mobileShareGameURL => 'Comparteix l\'enllaç a la partida'; + String get mobilePuzzleStreakAbortWarning => 'Perdreu la vostra ratxa i la vostra puntuació es guardarà.'; @override - String get mobileShareGamePGN => 'Comparteix PGN'; + String get mobilePuzzleThemesSubtitle => 'Resoleu problemes de les vostres obertures preferides o seleccioneu una temàtica.'; @override - String get mobileSharePositionAsFEN => 'Comparteix la posició com a FEN'; + String get mobilePuzzlesTab => 'Problemes'; @override - String get mobileShowVariations => 'Mostra les variacions'; + String get mobileRecentSearches => 'Cerques recents'; @override - String get mobileHideVariation => 'Amaga les variacions'; + String get mobileSettingsHapticFeedback => 'Resposta hàptica'; @override - String get mobileShowComments => 'Mostra comentaris'; + String get mobileSettingsImmersiveMode => 'Mode immersiu'; @override - String get mobilePuzzleStormConfirmEndRun => 'Voleu acabar aquesta ronda?'; + String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Res a mostrar, si us plau canvieu els filtres'; + String get mobileSettingsTab => 'Configuració'; @override - String get mobileCancelTakebackOffer => 'Anul·la la petició per desfer la jugada'; + String get mobileShareGamePGN => 'Comparteix PGN'; @override - String get mobileWaitingForOpponentToJoin => 'Esperant que s\'uneixi l\'adversari...'; + String get mobileShareGameURL => 'Comparteix l\'enllaç a la partida'; @override - String get mobileBlindfoldMode => 'A la cega'; + String get mobileSharePositionAsFEN => 'Comparteix la posició com a FEN'; @override - String get mobileLiveStreamers => 'Retransmissors en directe'; + String get mobileSharePuzzle => 'Comparteix aquest problema'; @override - String get mobileCustomGameJoinAGame => 'Unir-se a una partida'; + String get mobileShowComments => 'Mostra comentaris'; @override - String get mobileCorrespondenceClearSavedMove => 'Elimina la jugada guardada'; + String get mobileShowResult => 'Mostra el resultat'; @override - String get mobileSomethingWentWrong => 'Alguna cosa ha anat malament.'; + String get mobileShowVariations => 'Mostra les variacions'; @override - String get mobileShowResult => 'Mostra el resultat'; + String get mobileSomethingWentWrong => 'Alguna cosa ha anat malament.'; @override - String get mobilePuzzleThemesSubtitle => 'Resoleu problemes de les vostres obertures preferides o seleccioneu una temàtica.'; + String get mobileSystemColors => 'Colors del sistema'; @override - String get mobilePuzzleStormSubtitle => 'Resoleu el màxim nombre de problemes en 3 minuts.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Hola, $param'; - } + String get mobileToolsTab => 'Eines'; @override - String get mobileGreetingWithoutName => 'Hola'; + String get mobileWaitingForOpponentToJoin => 'Esperant que s\'uneixi l\'adversari...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Visualitza'; @override String get activityActivity => 'Activitat'; @@ -535,6 +538,9 @@ class AppLocalizationsCa extends AppLocalizations { @override String get broadcastStandings => 'Classificació'; + @override + String get broadcastOfficialStandings => 'Official Standings'; + @override String broadcastIframeHelp(String param) { return 'Més opcions a la $param'; @@ -565,6 +571,36 @@ class AppLocalizationsCa extends AppLocalizations { @override String get broadcastScore => 'Puntuació'; + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + @override String broadcastNbBroadcasts(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -1992,9 +2028,6 @@ class AppLocalizationsCa extends AppLocalizations { @override String get byCPL => 'Per CPL'; - @override - String get openStudy => 'Obrir estudi'; - @override String get enable => 'Habilitar'; @@ -2662,9 +2695,6 @@ class AppLocalizationsCa extends AppLocalizations { @override String get unblock => 'Desbloqueja'; - @override - String get followsYou => 'T\'està seguint'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 ha començat a seguir $param2'; @@ -5424,6 +5454,11 @@ class AppLocalizationsCa extends AppLocalizations { @override String get studyYouCompletedThisLesson => 'Enhorabona, heu completat aquesta lliçó.'; + @override + String studyPerPage(String param) { + return '$param per page'; + } + @override String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( diff --git a/lib/l10n/l10n_cs.dart b/lib/l10n/l10n_cs.dart index 311fce67bd..4e48b92e77 100644 --- a/lib/l10n/l10n_cs.dart +++ b/lib/l10n/l10n_cs.dart @@ -9,52 +9,57 @@ class AppLocalizationsCs extends AppLocalizations { AppLocalizationsCs([String locale = 'cs']) : super(locale); @override - String get mobileHomeTab => 'Home'; + String get mobileAllGames => 'All games'; @override - String get mobilePuzzlesTab => 'Puzzles'; + String get mobileAreYouSure => 'Jste si jistý?'; @override - String get mobileToolsTab => 'Tools'; + String get mobileBlindfoldMode => 'Páska přes oči'; @override - String get mobileWatchTab => 'Watch'; + String get mobileCancelTakebackOffer => 'Zrušit nabídnutí vrácení tahu'; @override - String get mobileSettingsTab => 'Settings'; + String get mobileClearButton => 'Vymazat'; @override - String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; + String get mobileCorrespondenceClearSavedMove => 'Vymazat uložené tahy'; @override - String get mobileSystemColors => 'System colors'; + String get mobileCustomGameJoinAGame => 'Připojit se ke hře'; @override String get mobileFeedbackButton => 'Feedback'; @override - String get mobileOkButton => 'OK'; + String mobileGreeting(String param) { + return 'Ahoj, $param'; + } @override - String get mobileSettingsHapticFeedback => 'Haptic feedback'; + String get mobileGreetingWithoutName => 'Ahoj'; @override - String get mobileSettingsImmersiveMode => 'Immersive mode'; + String get mobileHideVariation => 'Schovej variace'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileHomeTab => 'Home'; @override - String get mobileNotFollowingAnyUser => 'You are not following any user.'; + String get mobileLiveStreamers => 'Živé vysílání'; @override - String get mobileAllGames => 'All games'; + String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; @override - String get mobileRecentSearches => 'Recent searches'; + String get mobileNoSearchResults => 'Žádné výsledky'; @override - String get mobileClearButton => 'Vymazat'; + String get mobileNotFollowingAnyUser => 'You are not following any user.'; + + @override + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,84 +67,82 @@ class AppLocalizationsCs extends AppLocalizations { } @override - String get mobileNoSearchResults => 'Žádné výsledky'; + String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; @override - String get mobileAreYouSure => 'Jste si jistý?'; + String get mobilePuzzleStormConfirmEndRun => 'Chceš ukončit tento běh?'; @override - String get mobilePuzzleStreakAbortWarning => 'Ztratíte aktuální sérii a vaše skóre bude uloženo.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nic k zobrazení, prosím změn filtry'; @override String get mobilePuzzleStormNothingToShow => 'Nic k zobrazení. Zahrajte si nějaké běhy Bouřky úloh.'; @override - String get mobileSharePuzzle => 'Sdílej tuto úlohu'; + String get mobilePuzzleStormSubtitle => 'Vyřeš co nejvíce úloh co dokážeš za 3 minuty.'; @override - String get mobileShareGameURL => 'Sdílet URL hry'; + String get mobilePuzzleStreakAbortWarning => 'Ztratíte aktuální sérii a vaše skóre bude uloženo.'; @override - String get mobileShareGamePGN => 'Sdílet PGN'; + String get mobilePuzzleThemesSubtitle => 'Hrej úlohy z tvých oblíbených zahájení, nebo si vyber styl.'; @override - String get mobileSharePositionAsFEN => 'Sdílet pozici jako FEN'; + String get mobilePuzzlesTab => 'Puzzles'; @override - String get mobileShowVariations => 'Zobraz variace'; + String get mobileRecentSearches => 'Recent searches'; @override - String get mobileHideVariation => 'Schovej variace'; + String get mobileSettingsHapticFeedback => 'Haptic feedback'; @override - String get mobileShowComments => 'Zobraz komentáře'; + String get mobileSettingsImmersiveMode => 'Immersive mode'; @override - String get mobilePuzzleStormConfirmEndRun => 'Chceš ukončit tento běh?'; + String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nic k zobrazení, prosím změn filtry'; + String get mobileSettingsTab => 'Settings'; @override - String get mobileCancelTakebackOffer => 'Zrušit nabídnutí vrácení tahu'; + String get mobileShareGamePGN => 'Sdílet PGN'; @override - String get mobileWaitingForOpponentToJoin => 'Čeká se na připojení protihráče...'; + String get mobileShareGameURL => 'Sdílet URL hry'; @override - String get mobileBlindfoldMode => 'Páska přes oči'; + String get mobileSharePositionAsFEN => 'Sdílet pozici jako FEN'; @override - String get mobileLiveStreamers => 'Živé vysílání'; + String get mobileSharePuzzle => 'Sdílej tuto úlohu'; @override - String get mobileCustomGameJoinAGame => 'Připojit se ke hře'; + String get mobileShowComments => 'Zobraz komentáře'; @override - String get mobileCorrespondenceClearSavedMove => 'Vymazat uložené tahy'; + String get mobileShowResult => 'Zobrazit výsledky'; @override - String get mobileSomethingWentWrong => 'Něco se pokazilo.'; + String get mobileShowVariations => 'Zobraz variace'; @override - String get mobileShowResult => 'Zobrazit výsledky'; + String get mobileSomethingWentWrong => 'Něco se pokazilo.'; @override - String get mobilePuzzleThemesSubtitle => 'Hrej úlohy z tvých oblíbených zahájení, nebo si vyber styl.'; + String get mobileSystemColors => 'System colors'; @override - String get mobilePuzzleStormSubtitle => 'Vyřeš co nejvíce úloh co dokážeš za 3 minuty.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Ahoj, $param'; - } + String get mobileToolsTab => 'Tools'; @override - String get mobileGreetingWithoutName => 'Ahoj'; + String get mobileWaitingForOpponentToJoin => 'Čeká se na připojení protihráče...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Watch'; @override String get activityActivity => 'Aktivita'; @@ -571,6 +574,9 @@ class AppLocalizationsCs extends AppLocalizations { @override String get broadcastStandings => 'Standings'; + @override + String get broadcastOfficialStandings => 'Official Standings'; + @override String broadcastIframeHelp(String param) { return 'More options on the $param'; @@ -601,6 +607,36 @@ class AppLocalizationsCs extends AppLocalizations { @override String get broadcastScore => 'Score'; + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + @override String broadcastNbBroadcasts(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -2040,9 +2076,6 @@ class AppLocalizationsCs extends AppLocalizations { @override String get byCPL => 'Dle CPL'; - @override - String get openStudy => 'Otevřít studii'; - @override String get enable => 'Povolit analýzu'; @@ -2710,9 +2743,6 @@ class AppLocalizationsCs extends AppLocalizations { @override String get unblock => 'Odblokovat'; - @override - String get followsYou => 'Vás sleduje'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 začal sledovat $param2'; @@ -5560,6 +5590,11 @@ class AppLocalizationsCs extends AppLocalizations { @override String get studyYouCompletedThisLesson => 'Blahopřejeme! Dokončili jste tuto lekci.'; + @override + String studyPerPage(String param) { + return '$param per page'; + } + @override String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( diff --git a/lib/l10n/l10n_da.dart b/lib/l10n/l10n_da.dart index e30401ac05..5e54eb1cd8 100644 --- a/lib/l10n/l10n_da.dart +++ b/lib/l10n/l10n_da.dart @@ -9,52 +9,57 @@ class AppLocalizationsDa extends AppLocalizations { AppLocalizationsDa([String locale = 'da']) : super(locale); @override - String get mobileHomeTab => 'Hjem'; + String get mobileAllGames => 'Alle partier'; @override - String get mobilePuzzlesTab => 'Opgaver'; + String get mobileAreYouSure => 'Er du sikker?'; @override - String get mobileToolsTab => 'Værktøjer'; + String get mobileBlindfoldMode => 'Bind for øjnene'; @override - String get mobileWatchTab => 'Se'; + String get mobileCancelTakebackOffer => 'Annuller tilbud om tilbagetagelse'; @override - String get mobileSettingsTab => 'Indstillinger'; + String get mobileClearButton => 'Ryd'; @override - String get mobileMustBeLoggedIn => 'Du skal være logget ind for at se denne side.'; + String get mobileCorrespondenceClearSavedMove => 'Ryd gemt træk'; @override - String get mobileSystemColors => 'Systemfarver'; + String get mobileCustomGameJoinAGame => 'Deltag i et parti'; @override String get mobileFeedbackButton => 'Feedback'; @override - String get mobileOkButton => 'Ok'; + String mobileGreeting(String param) { + return 'Hej, $param'; + } @override - String get mobileSettingsHapticFeedback => 'Haptisk feedback'; + String get mobileGreetingWithoutName => 'Hej'; @override - String get mobileSettingsImmersiveMode => 'Fordybelsestilstand'; + String get mobileHideVariation => 'Skjul variation'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Skjul systemets brugergrænseflade, mens du spiller. Brug denne funktion, hvis du er generet af systemets navigationsbevægelser i kanterne af skærmen. Gælder for parti- og Puzzle Storm-skærme.'; + String get mobileHomeTab => 'Hjem'; @override - String get mobileNotFollowingAnyUser => 'Du følger ikke nogen brugere.'; + String get mobileLiveStreamers => 'Live-streamere'; @override - String get mobileAllGames => 'Alle partier'; + String get mobileMustBeLoggedIn => 'Du skal være logget ind for at se denne side.'; @override - String get mobileRecentSearches => 'Seneste søgninger'; + String get mobileNoSearchResults => 'Ingen resultater'; @override - String get mobileClearButton => 'Ryd'; + String get mobileNotFollowingAnyUser => 'Du følger ikke nogen brugere.'; + + @override + String get mobileOkButton => 'Ok'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,84 +67,82 @@ class AppLocalizationsDa extends AppLocalizations { } @override - String get mobileNoSearchResults => 'Ingen resultater'; + String get mobilePrefMagnifyDraggedPiece => 'Forstør brik, som trækkes'; @override - String get mobileAreYouSure => 'Er du sikker?'; + String get mobilePuzzleStormConfirmEndRun => 'Vil du afslutte dette løb?'; @override - String get mobilePuzzleStreakAbortWarning => 'Du vil miste din nuværende stime og din score vil blive gemt.'; + String get mobilePuzzleStormFilterNothingToShow => 'Intet at vise, ændr venligst filtre'; @override String get mobilePuzzleStormNothingToShow => 'Intet at vise. Spil nogle runder af Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Del denne opgave'; + String get mobilePuzzleStormSubtitle => 'Løs så mange opgaver som muligt på 3 minutter.'; @override - String get mobileShareGameURL => 'Del partiets URL'; + String get mobilePuzzleStreakAbortWarning => 'Du vil miste din nuværende stime og din score vil blive gemt.'; @override - String get mobileShareGamePGN => 'Del PGN'; + String get mobilePuzzleThemesSubtitle => 'Spil opgaver fra dine foretrukne åbninger, eller vælg et tema.'; @override - String get mobileSharePositionAsFEN => 'Del position som FEN'; + String get mobilePuzzlesTab => 'Opgaver'; @override - String get mobileShowVariations => 'Vis variationer'; + String get mobileRecentSearches => 'Seneste søgninger'; @override - String get mobileHideVariation => 'Skjul variation'; + String get mobileSettingsHapticFeedback => 'Haptisk feedback'; @override - String get mobileShowComments => 'Vis kommentarer'; + String get mobileSettingsImmersiveMode => 'Fordybelsestilstand'; @override - String get mobilePuzzleStormConfirmEndRun => 'Vil du afslutte dette løb?'; + String get mobileSettingsImmersiveModeSubtitle => 'Skjul systemets brugergrænseflade, mens du spiller. Brug denne funktion, hvis du er generet af systemets navigationsbevægelser i kanterne af skærmen. Gælder for parti- og Puzzle Storm-skærme.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Intet at vise, ændr venligst filtre'; + String get mobileSettingsTab => 'Indstillinger'; @override - String get mobileCancelTakebackOffer => 'Annuller tilbud om tilbagetagelse'; + String get mobileShareGamePGN => 'Del PGN'; @override - String get mobileWaitingForOpponentToJoin => 'Venter på at modstander slutter sig til...'; + String get mobileShareGameURL => 'Del partiets URL'; @override - String get mobileBlindfoldMode => 'Bind for øjnene'; + String get mobileSharePositionAsFEN => 'Del position som FEN'; @override - String get mobileLiveStreamers => 'Live-streamere'; + String get mobileSharePuzzle => 'Del denne opgave'; @override - String get mobileCustomGameJoinAGame => 'Deltag i et parti'; + String get mobileShowComments => 'Vis kommentarer'; @override - String get mobileCorrespondenceClearSavedMove => 'Ryd gemt træk'; + String get mobileShowResult => 'Vis resultat'; @override - String get mobileSomethingWentWrong => 'Noget gik galt.'; + String get mobileShowVariations => 'Vis variationer'; @override - String get mobileShowResult => 'Vis resultat'; + String get mobileSomethingWentWrong => 'Noget gik galt.'; @override - String get mobilePuzzleThemesSubtitle => 'Spil opgaver fra dine foretrukne åbninger, eller vælg et tema.'; + String get mobileSystemColors => 'Systemfarver'; @override - String get mobilePuzzleStormSubtitle => 'Løs så mange opgaver som muligt på 3 minutter.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Hej, $param'; - } + String get mobileToolsTab => 'Værktøjer'; @override - String get mobileGreetingWithoutName => 'Hej'; + String get mobileWaitingForOpponentToJoin => 'Venter på at modstander slutter sig til...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Forstør brik, som trækkes'; + String get mobileWatchTab => 'Se'; @override String get activityActivity => 'Aktivitet'; @@ -535,6 +538,9 @@ class AppLocalizationsDa extends AppLocalizations { @override String get broadcastStandings => 'Stillinger'; + @override + String get broadcastOfficialStandings => 'Official Standings'; + @override String broadcastIframeHelp(String param) { return 'Flere muligheder på $param'; @@ -565,6 +571,36 @@ class AppLocalizationsDa extends AppLocalizations { @override String get broadcastScore => 'Score'; + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + @override String broadcastNbBroadcasts(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -1992,9 +2028,6 @@ class AppLocalizationsDa extends AppLocalizations { @override String get byCPL => 'CBT'; - @override - String get openStudy => 'Åben studie'; - @override String get enable => 'Aktivér'; @@ -2662,9 +2695,6 @@ class AppLocalizationsDa extends AppLocalizations { @override String get unblock => 'Stop blokering'; - @override - String get followsYou => 'Følger dig'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 følger nu $param2'; @@ -5424,6 +5454,11 @@ class AppLocalizationsDa extends AppLocalizations { @override String get studyYouCompletedThisLesson => 'Tillykke! Du har fuldført denne lektion.'; + @override + String studyPerPage(String param) { + return '$param per page'; + } + @override String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( diff --git a/lib/l10n/l10n_de.dart b/lib/l10n/l10n_de.dart index f79bfa9af0..d6e7b6a1d7 100644 --- a/lib/l10n/l10n_de.dart +++ b/lib/l10n/l10n_de.dart @@ -9,52 +9,57 @@ class AppLocalizationsDe extends AppLocalizations { AppLocalizationsDe([String locale = 'de']) : super(locale); @override - String get mobileHomeTab => 'Start'; + String get mobileAllGames => 'Alle Partien'; @override - String get mobilePuzzlesTab => 'Aufgaben'; + String get mobileAreYouSure => 'Bist du sicher?'; @override - String get mobileToolsTab => 'Werkzeuge'; + String get mobileBlindfoldMode => 'Blind spielen'; @override - String get mobileWatchTab => 'Zuschauen'; + String get mobileCancelTakebackOffer => 'Zugzurücknahme-Angebot abbrechen'; @override - String get mobileSettingsTab => 'Optionen'; + String get mobileClearButton => 'Löschen'; @override - String get mobileMustBeLoggedIn => 'Du musst eingeloggt sein, um diese Seite anzuzeigen.'; + String get mobileCorrespondenceClearSavedMove => 'Gespeicherten Zug löschen'; @override - String get mobileSystemColors => 'Systemfarben'; + String get mobileCustomGameJoinAGame => 'Einer Partie beitreten'; @override String get mobileFeedbackButton => 'Feedback'; @override - String get mobileOkButton => 'OK'; + String mobileGreeting(String param) { + return 'Hallo, $param'; + } @override - String get mobileSettingsHapticFeedback => 'Haptisches Feedback'; + String get mobileGreetingWithoutName => 'Hallo'; @override - String get mobileSettingsImmersiveMode => 'Immersiver Modus'; + String get mobileHideVariation => 'Variante ausblenden'; @override - String get mobileSettingsImmersiveModeSubtitle => 'System-Benutzeroberfläche während des Spielens ausblenden. Nutze diese Option, wenn dich die Navigationsverhalten des Systems an den Bildschirmrändern stören. Gilt für Spiel- und Puzzle-Storm-Bildschirme.'; + String get mobileHomeTab => 'Start'; @override - String get mobileNotFollowingAnyUser => 'Du folgst keinem Nutzer.'; + String get mobileLiveStreamers => 'Livestreamer'; @override - String get mobileAllGames => 'Alle Partien'; + String get mobileMustBeLoggedIn => 'Du musst eingeloggt sein, um diese Seite anzuzeigen.'; @override - String get mobileRecentSearches => 'Letzte Suchen'; + String get mobileNoSearchResults => 'Keine Ergebnisse'; @override - String get mobileClearButton => 'Löschen'; + String get mobileNotFollowingAnyUser => 'Du folgst keinem Nutzer.'; + + @override + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,84 +67,82 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get mobileNoSearchResults => 'Keine Ergebnisse'; + String get mobilePrefMagnifyDraggedPiece => 'Vergrößern der gezogenen Figur'; @override - String get mobileAreYouSure => 'Bist du sicher?'; + String get mobilePuzzleStormConfirmEndRun => 'Möchtest du diesen Durchlauf beenden?'; @override - String get mobilePuzzleStreakAbortWarning => 'Du verlierst deine aktuelle Serie und dein Ergebnis wird gespeichert.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nichts anzuzeigen, bitte passe deine Filter an'; @override String get mobilePuzzleStormNothingToShow => 'Nichts anzuzeigen. Spiele ein paar Runden Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Teile diese Aufgabe'; + String get mobilePuzzleStormSubtitle => 'Löse so viele Aufgaben wie möglich in 3 Minuten.'; @override - String get mobileShareGameURL => 'Link der Partie teilen'; + String get mobilePuzzleStreakAbortWarning => 'Du verlierst deine aktuelle Serie und dein Ergebnis wird gespeichert.'; @override - String get mobileShareGamePGN => 'PGN teilen'; + String get mobilePuzzleThemesSubtitle => 'Spiele Aufgaben aus deinen Lieblings-Öffnungen oder wähle ein Theme.'; @override - String get mobileSharePositionAsFEN => 'Stellung als FEN teilen'; + String get mobilePuzzlesTab => 'Aufgaben'; @override - String get mobileShowVariations => 'Varianten anzeigen'; + String get mobileRecentSearches => 'Letzte Suchen'; @override - String get mobileHideVariation => 'Variante ausblenden'; + String get mobileSettingsHapticFeedback => 'Haptisches Feedback'; @override - String get mobileShowComments => 'Kommentare anzeigen'; + String get mobileSettingsImmersiveMode => 'Immersiver Modus'; @override - String get mobilePuzzleStormConfirmEndRun => 'Möchtest du diesen Durchlauf beenden?'; + String get mobileSettingsImmersiveModeSubtitle => 'System-Benutzeroberfläche während des Spielens ausblenden. Nutze diese Option, wenn dich die Navigationsverhalten des Systems an den Bildschirmrändern stören. Gilt für Spiel- und Puzzle-Storm-Bildschirme.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nichts anzuzeigen, bitte passe deine Filter an'; + String get mobileSettingsTab => 'Optionen'; @override - String get mobileCancelTakebackOffer => 'Zugzurücknahme-Angebot abbrechen'; + String get mobileShareGamePGN => 'PGN teilen'; @override - String get mobileWaitingForOpponentToJoin => 'Warte auf Beitritt eines Gegners...'; + String get mobileShareGameURL => 'Link der Partie teilen'; @override - String get mobileBlindfoldMode => 'Blind spielen'; + String get mobileSharePositionAsFEN => 'Stellung als FEN teilen'; @override - String get mobileLiveStreamers => 'Livestreamer'; + String get mobileSharePuzzle => 'Teile diese Aufgabe'; @override - String get mobileCustomGameJoinAGame => 'Einer Partie beitreten'; + String get mobileShowComments => 'Kommentare anzeigen'; @override - String get mobileCorrespondenceClearSavedMove => 'Gespeicherten Zug löschen'; + String get mobileShowResult => 'Ergebnis anzeigen'; @override - String get mobileSomethingWentWrong => 'Etwas ist schiefgelaufen.'; + String get mobileShowVariations => 'Varianten anzeigen'; @override - String get mobileShowResult => 'Ergebnis anzeigen'; + String get mobileSomethingWentWrong => 'Etwas ist schiefgelaufen.'; @override - String get mobilePuzzleThemesSubtitle => 'Spiele Aufgaben aus deinen Lieblings-Öffnungen oder wähle ein Theme.'; + String get mobileSystemColors => 'Systemfarben'; @override - String get mobilePuzzleStormSubtitle => 'Löse so viele Aufgaben wie möglich in 3 Minuten.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Hallo, $param'; - } + String get mobileToolsTab => 'Werkzeuge'; @override - String get mobileGreetingWithoutName => 'Hallo'; + String get mobileWaitingForOpponentToJoin => 'Warte auf Beitritt eines Gegners...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Vergrößern der gezogenen Figur'; + String get mobileWatchTab => 'Zuschauen'; @override String get activityActivity => 'Verlauf'; @@ -535,6 +538,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get broadcastStandings => 'Rangliste'; + @override + String get broadcastOfficialStandings => 'Official Standings'; + @override String broadcastIframeHelp(String param) { return 'Weitere Optionen auf der $param'; @@ -565,6 +571,36 @@ class AppLocalizationsDe extends AppLocalizations { @override String get broadcastScore => 'Punktestand'; + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + @override String broadcastNbBroadcasts(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -1992,9 +2028,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get byCPL => 'Nach CPL'; - @override - String get openStudy => 'Studie öffnen'; - @override String get enable => 'Einschalten'; @@ -2662,9 +2695,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get unblock => 'Nicht mehr blockieren'; - @override - String get followsYou => 'Folgt dir'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 folgt jetzt $param2'; @@ -5424,6 +5454,11 @@ class AppLocalizationsDe extends AppLocalizations { @override String get studyYouCompletedThisLesson => 'Gratulation! Du hast diese Lektion abgeschlossen.'; + @override + String studyPerPage(String param) { + return '$param per page'; + } + @override String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( diff --git a/lib/l10n/l10n_el.dart b/lib/l10n/l10n_el.dart index 02eda1d8ce..f78ae05357 100644 --- a/lib/l10n/l10n_el.dart +++ b/lib/l10n/l10n_el.dart @@ -9,52 +9,57 @@ class AppLocalizationsEl extends AppLocalizations { AppLocalizationsEl([String locale = 'el']) : super(locale); @override - String get mobileHomeTab => 'Αρχική'; + String get mobileAllGames => 'Όλα τα παιχνίδια'; @override - String get mobilePuzzlesTab => 'Γρίφοι'; + String get mobileAreYouSure => 'Είστε σίγουροι;'; @override - String get mobileToolsTab => 'Εργαλεία'; + String get mobileBlindfoldMode => 'Τυφλό'; @override - String get mobileWatchTab => 'Δείτε'; + String get mobileCancelTakebackOffer => 'Ακυρώστε την προσφορά αναίρεσης της κίνησης'; @override - String get mobileSettingsTab => 'Ρυθμίσεις'; + String get mobileClearButton => 'Εκκαθάριση'; @override - String get mobileMustBeLoggedIn => 'Πρέπει να συνδεθείτε για να δείτε αυτή τη σελίδα.'; + String get mobileCorrespondenceClearSavedMove => 'Εκκαθάριση αποθηκευμένης κίνησης'; @override - String get mobileSystemColors => 'Χρώματα συστήματος'; + String get mobileCustomGameJoinAGame => 'Συμμετοχή σε παιχνίδι'; @override String get mobileFeedbackButton => 'Πείτε μας τη γνώμη σας'; @override - String get mobileOkButton => 'ΟΚ'; + String mobileGreeting(String param) { + return 'Καλωσορίσατε, $param'; + } @override - String get mobileSettingsHapticFeedback => 'Απόκριση δόνησης'; + String get mobileGreetingWithoutName => 'Καλωσορίσατε'; @override - String get mobileSettingsImmersiveMode => 'Immersive mode'; + String get mobileHideVariation => 'Απόκρυψη παραλλαγής'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Αποκρύπτει τη διεπαφή του συστήματος όσο παίζεται. Ενεργοποιήστε εάν σας ενοχλούν οι χειρονομίες πλοήγησης του συστήματος στα άκρα της οθόνης. Ισχύει για την προβολή παιχνιδιού και το Puzzle Storm.'; + String get mobileHomeTab => 'Αρχική'; @override - String get mobileNotFollowingAnyUser => 'Δεν ακολουθείτε κανέναν χρήστη.'; + String get mobileLiveStreamers => 'Streamers ζωντανά αυτή τη στιγμή'; @override - String get mobileAllGames => 'Όλα τα παιχνίδια'; + String get mobileMustBeLoggedIn => 'Πρέπει να συνδεθείτε για να δείτε αυτή τη σελίδα.'; @override - String get mobileRecentSearches => 'Πρόσφατες αναζητήσεις'; + String get mobileNoSearchResults => 'Δεν βρέθηκαν αποτελέσματα'; @override - String get mobileClearButton => 'Εκκαθάριση'; + String get mobileNotFollowingAnyUser => 'Δεν ακολουθείτε κανέναν χρήστη.'; + + @override + String get mobileOkButton => 'ΟΚ'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,84 +67,82 @@ class AppLocalizationsEl extends AppLocalizations { } @override - String get mobileNoSearchResults => 'Δεν βρέθηκαν αποτελέσματα'; + String get mobilePrefMagnifyDraggedPiece => 'Μεγέθυνση του επιλεγμένου κομματιού'; @override - String get mobileAreYouSure => 'Είστε σίγουροι;'; + String get mobilePuzzleStormConfirmEndRun => 'Θέλετε να τερματίσετε αυτόν τον γύρο;'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; + String get mobilePuzzleStormFilterNothingToShow => 'Δεν υπάρχουν γρίφοι για τις συγκεκριμένες επιλογές φίλτρων, παρακαλώ δοκιμάστε κάποιες άλλες'; @override String get mobilePuzzleStormNothingToShow => 'Δεν υπάρχουν στοιχεία. Παίξτε κάποιους γύρους Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Κοινοποίηση γρίφου'; + String get mobilePuzzleStormSubtitle => 'Λύστε όσους γρίφους όσο το δυνατόν, σε 3 λεπτά.'; @override - String get mobileShareGameURL => 'Κοινοποίηση URL παιχνιδιού'; + String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; @override - String get mobileShareGamePGN => 'Κοινοποίηση PGN'; + String get mobilePuzzleThemesSubtitle => 'Παίξτε γρίφους από τα αγαπημένα σας ανοίγματα, ή επιλέξτε θέμα.'; @override - String get mobileSharePositionAsFEN => 'Κοινοποίηση θέσης ως FEN'; + String get mobilePuzzlesTab => 'Γρίφοι'; @override - String get mobileShowVariations => 'Εμφάνιση παραλλαγών'; + String get mobileRecentSearches => 'Πρόσφατες αναζητήσεις'; @override - String get mobileHideVariation => 'Απόκρυψη παραλλαγής'; + String get mobileSettingsHapticFeedback => 'Απόκριση δόνησης'; @override - String get mobileShowComments => 'Εμφάνιση σχολίων'; + String get mobileSettingsImmersiveMode => 'Immersive mode'; @override - String get mobilePuzzleStormConfirmEndRun => 'Θέλετε να τερματίσετε αυτόν τον γύρο;'; + String get mobileSettingsImmersiveModeSubtitle => 'Αποκρύπτει τη διεπαφή του συστήματος όσο παίζεται. Ενεργοποιήστε εάν σας ενοχλούν οι χειρονομίες πλοήγησης του συστήματος στα άκρα της οθόνης. Ισχύει για την προβολή παιχνιδιού και το Puzzle Storm.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Δεν υπάρχουν γρίφοι για τις συγκεκριμένες επιλογές φίλτρων, παρακαλώ δοκιμάστε κάποιες άλλες'; + String get mobileSettingsTab => 'Ρυθμίσεις'; @override - String get mobileCancelTakebackOffer => 'Ακυρώστε την προσφορά αναίρεσης της κίνησης'; + String get mobileShareGamePGN => 'Κοινοποίηση PGN'; @override - String get mobileWaitingForOpponentToJoin => 'Αναμονή για αντίπαλο...'; + String get mobileShareGameURL => 'Κοινοποίηση URL παιχνιδιού'; @override - String get mobileBlindfoldMode => 'Τυφλό'; + String get mobileSharePositionAsFEN => 'Κοινοποίηση θέσης ως FEN'; @override - String get mobileLiveStreamers => 'Streamers ζωντανά αυτή τη στιγμή'; + String get mobileSharePuzzle => 'Κοινοποίηση γρίφου'; @override - String get mobileCustomGameJoinAGame => 'Συμμετοχή σε παιχνίδι'; + String get mobileShowComments => 'Εμφάνιση σχολίων'; @override - String get mobileCorrespondenceClearSavedMove => 'Εκκαθάριση αποθηκευμένης κίνησης'; + String get mobileShowResult => 'Εμφάνιση αποτελέσματος'; @override - String get mobileSomethingWentWrong => 'Κάτι πήγε στραβά.'; + String get mobileShowVariations => 'Εμφάνιση παραλλαγών'; @override - String get mobileShowResult => 'Εμφάνιση αποτελέσματος'; + String get mobileSomethingWentWrong => 'Κάτι πήγε στραβά.'; @override - String get mobilePuzzleThemesSubtitle => 'Παίξτε γρίφους από τα αγαπημένα σας ανοίγματα, ή επιλέξτε θέμα.'; + String get mobileSystemColors => 'Χρώματα συστήματος'; @override - String get mobilePuzzleStormSubtitle => 'Λύστε όσους γρίφους όσο το δυνατόν, σε 3 λεπτά.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Καλωσορίσατε, $param'; - } + String get mobileToolsTab => 'Εργαλεία'; @override - String get mobileGreetingWithoutName => 'Καλωσορίσατε'; + String get mobileWaitingForOpponentToJoin => 'Αναμονή για αντίπαλο...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Μεγέθυνση του επιλεγμένου κομματιού'; + String get mobileWatchTab => 'Δείτε'; @override String get activityActivity => 'Δραστηριότητα'; @@ -535,6 +538,9 @@ class AppLocalizationsEl extends AppLocalizations { @override String get broadcastStandings => 'Κατάταξη'; + @override + String get broadcastOfficialStandings => 'Official Standings'; + @override String broadcastIframeHelp(String param) { return 'More options on the $param'; @@ -565,6 +571,36 @@ class AppLocalizationsEl extends AppLocalizations { @override String get broadcastScore => 'Βαθμολογία'; + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + @override String broadcastNbBroadcasts(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -1992,9 +2028,6 @@ class AppLocalizationsEl extends AppLocalizations { @override String get byCPL => 'Με CPL'; - @override - String get openStudy => 'Άνοιγμα μελέτης'; - @override String get enable => 'Ενεργοποίηση'; @@ -2662,9 +2695,6 @@ class AppLocalizationsEl extends AppLocalizations { @override String get unblock => 'Κατάργηση απόκλεισης'; - @override - String get followsYou => 'Σας ακολουθεί'; - @override String xStartedFollowingY(String param1, String param2) { return 'Ο $param1 άρχισε να ακολουθεί τον $param2'; @@ -5424,6 +5454,11 @@ class AppLocalizationsEl extends AppLocalizations { @override String get studyYouCompletedThisLesson => 'Συγχαρητήρια! Ολοκληρώσατε αυτό το μάθημα.'; + @override + String studyPerPage(String param) { + return '$param per page'; + } + @override String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( diff --git a/lib/l10n/l10n_en.dart b/lib/l10n/l10n_en.dart index d115d4734b..303980850d 100644 --- a/lib/l10n/l10n_en.dart +++ b/lib/l10n/l10n_en.dart @@ -9,52 +9,57 @@ class AppLocalizationsEn extends AppLocalizations { AppLocalizationsEn([String locale = 'en']) : super(locale); @override - String get mobileHomeTab => 'Home'; + String get mobileAllGames => 'All games'; @override - String get mobilePuzzlesTab => 'Puzzles'; + String get mobileAreYouSure => 'Are you sure?'; @override - String get mobileToolsTab => 'Tools'; + String get mobileBlindfoldMode => 'Blindfold'; @override - String get mobileWatchTab => 'Watch'; + String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @override - String get mobileSettingsTab => 'Settings'; + String get mobileClearButton => 'Clear'; @override - String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; + String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; @override - String get mobileSystemColors => 'System colors'; + String get mobileCustomGameJoinAGame => 'Join a game'; @override String get mobileFeedbackButton => 'Feedback'; @override - String get mobileOkButton => 'OK'; + String mobileGreeting(String param) { + return 'Hello, $param'; + } @override - String get mobileSettingsHapticFeedback => 'Haptic feedback'; + String get mobileGreetingWithoutName => 'Hello'; @override - String get mobileSettingsImmersiveMode => 'Immersive mode'; + String get mobileHideVariation => 'Hide variation'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileHomeTab => 'Home'; @override - String get mobileNotFollowingAnyUser => 'You are not following any user.'; + String get mobileLiveStreamers => 'Live streamers'; @override - String get mobileAllGames => 'All games'; + String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; @override - String get mobileRecentSearches => 'Recent searches'; + String get mobileNoSearchResults => 'No results'; @override - String get mobileClearButton => 'Clear'; + String get mobileNotFollowingAnyUser => 'You are not following any user.'; + + @override + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,84 +67,82 @@ class AppLocalizationsEn extends AppLocalizations { } @override - String get mobileNoSearchResults => 'No results'; + String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; @override - String get mobileAreYouSure => 'Are you sure?'; + String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; @override String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Share this puzzle'; + String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; @override - String get mobileShareGameURL => 'Share game URL'; + String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; @override - String get mobileShareGamePGN => 'Share PGN'; + String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; @override - String get mobileSharePositionAsFEN => 'Share position as FEN'; + String get mobilePuzzlesTab => 'Puzzles'; @override - String get mobileShowVariations => 'Show variations'; + String get mobileRecentSearches => 'Recent searches'; @override - String get mobileHideVariation => 'Hide variation'; + String get mobileSettingsHapticFeedback => 'Haptic feedback'; @override - String get mobileShowComments => 'Show comments'; + String get mobileSettingsImmersiveMode => 'Immersive mode'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobileSettingsTab => 'Settings'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; + String get mobileShareGamePGN => 'Share PGN'; @override - String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; + String get mobileShareGameURL => 'Share game URL'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get mobileSharePositionAsFEN => 'Share position as FEN'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileSharePuzzle => 'Share this puzzle'; @override - String get mobileCustomGameJoinAGame => 'Join a game'; + String get mobileShowComments => 'Show comments'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; + String get mobileShowResult => 'Show result'; @override - String get mobileSomethingWentWrong => 'Something went wrong.'; + String get mobileShowVariations => 'Show variations'; @override - String get mobileShowResult => 'Show result'; + String get mobileSomethingWentWrong => 'Something went wrong.'; @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobileSystemColors => 'System colors'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Hello, $param'; - } + String get mobileToolsTab => 'Tools'; @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Watch'; @override String get activityActivity => 'Activity'; @@ -535,6 +538,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get broadcastStandings => 'Standings'; + @override + String get broadcastOfficialStandings => 'Official Standings'; + @override String broadcastIframeHelp(String param) { return 'More options on the $param'; @@ -565,6 +571,36 @@ class AppLocalizationsEn extends AppLocalizations { @override String get broadcastScore => 'Score'; + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + @override String broadcastNbBroadcasts(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -1990,9 +2026,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get byCPL => 'By CPL'; - @override - String get openStudy => 'Open study'; - @override String get enable => 'Enable'; @@ -2660,9 +2693,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get unblock => 'Unblock'; - @override - String get followsYou => 'Follows you'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 started following $param2'; @@ -5422,6 +5452,11 @@ class AppLocalizationsEn extends AppLocalizations { @override String get studyYouCompletedThisLesson => 'Congratulations! You completed this lesson.'; + @override + String studyPerPage(String param) { + return '$param per page'; + } + @override String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -5472,52 +5507,57 @@ class AppLocalizationsEnUs extends AppLocalizationsEn { AppLocalizationsEnUs(): super('en_US'); @override - String get mobileHomeTab => 'Home'; + String get mobileAllGames => 'All games'; @override - String get mobilePuzzlesTab => 'Puzzles'; + String get mobileAreYouSure => 'Are you sure?'; @override - String get mobileToolsTab => 'Tools'; + String get mobileBlindfoldMode => 'Blindfold'; @override - String get mobileWatchTab => 'Watch'; + String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @override - String get mobileSettingsTab => 'Settings'; + String get mobileClearButton => 'Clear'; @override - String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; + String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; @override - String get mobileSystemColors => 'System colors'; + String get mobileCustomGameJoinAGame => 'Join a game'; @override String get mobileFeedbackButton => 'Feedback'; @override - String get mobileOkButton => 'OK'; + String mobileGreeting(String param) { + return 'Hello, $param'; + } @override - String get mobileSettingsHapticFeedback => 'Haptic feedback'; + String get mobileGreetingWithoutName => 'Hello'; @override - String get mobileSettingsImmersiveMode => 'Immersive mode'; + String get mobileHideVariation => 'Hide variation'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileHomeTab => 'Home'; @override - String get mobileNotFollowingAnyUser => 'You are not following any user.'; + String get mobileLiveStreamers => 'Live streamers'; @override - String get mobileAllGames => 'All games'; + String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; @override - String get mobileRecentSearches => 'Recent searches'; + String get mobileNoSearchResults => 'No results'; @override - String get mobileClearButton => 'Clear'; + String get mobileNotFollowingAnyUser => 'You are not following any user.'; + + @override + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -5525,84 +5565,79 @@ class AppLocalizationsEnUs extends AppLocalizationsEn { } @override - String get mobileNoSearchResults => 'No results'; + String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; @override - String get mobileAreYouSure => 'Are you sure?'; + String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak, but your score will be saved.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; @override String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Share this puzzle'; - - @override - String get mobileShareGameURL => 'Share game URL'; + String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; @override - String get mobileShareGamePGN => 'Share PGN'; + String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak, but your score will be saved.'; @override - String get mobileSharePositionAsFEN => 'Share position as FEN'; + String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; @override - String get mobileShowVariations => 'Show variations'; + String get mobilePuzzlesTab => 'Puzzles'; @override - String get mobileHideVariation => 'Hide variation'; + String get mobileRecentSearches => 'Recent searches'; @override - String get mobileShowComments => 'Show comments'; + String get mobileSettingsHapticFeedback => 'Haptic feedback'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get mobileSettingsImmersiveMode => 'Immersive mode'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; + String get mobileSettingsTab => 'Settings'; @override - String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; + String get mobileShareGamePGN => 'Share PGN'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get mobileShareGameURL => 'Share game URL'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileSharePositionAsFEN => 'Share position as FEN'; @override - String get mobileCustomGameJoinAGame => 'Join a game'; + String get mobileSharePuzzle => 'Share this puzzle'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; + String get mobileShowComments => 'Show comments'; @override - String get mobileSomethingWentWrong => 'Something went wrong.'; + String get mobileShowResult => 'Show result'; @override - String get mobileShowResult => 'Show result'; + String get mobileShowVariations => 'Show variations'; @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobileSomethingWentWrong => 'Something went wrong.'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobileSystemColors => 'System colors'; @override - String mobileGreeting(String param) { - return 'Hello, $param'; - } + String get mobileToolsTab => 'Tools'; @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Watch'; @override String get activityActivity => 'Activity'; @@ -7379,9 +7414,6 @@ class AppLocalizationsEnUs extends AppLocalizationsEn { @override String get byCPL => 'By CPL'; - @override - String get openStudy => 'Open study'; - @override String get enable => 'Enable'; @@ -8046,9 +8078,6 @@ class AppLocalizationsEnUs extends AppLocalizationsEn { @override String get unblock => 'Unblock'; - @override - String get followsYou => 'Follows you'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 started following $param2'; diff --git a/lib/l10n/l10n_eo.dart b/lib/l10n/l10n_eo.dart index 8dc62f7fc9..8015bbccc2 100644 --- a/lib/l10n/l10n_eo.dart +++ b/lib/l10n/l10n_eo.dart @@ -9,52 +9,57 @@ class AppLocalizationsEo extends AppLocalizations { AppLocalizationsEo([String locale = 'eo']) : super(locale); @override - String get mobileHomeTab => 'Hejmo'; + String get mobileAllGames => 'Ĉiuj ludoj'; @override - String get mobilePuzzlesTab => 'Puzloj'; + String get mobileAreYouSure => 'Ĉu vi certas?'; @override - String get mobileToolsTab => 'Iloj'; + String get mobileBlindfoldMode => 'Blindfold'; @override - String get mobileWatchTab => 'Spekti'; + String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @override - String get mobileSettingsTab => 'Agordoj'; + String get mobileClearButton => 'Malplenigi'; @override - String get mobileMustBeLoggedIn => 'Vi devas esti ensalutata por spekti ĉi tiun paĝon.'; + String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; @override - String get mobileSystemColors => 'Sistemaj koloroj'; + String get mobileCustomGameJoinAGame => 'Join a game'; @override String get mobileFeedbackButton => 'Prikomentado'; @override - String get mobileOkButton => 'Bone'; + String mobileGreeting(String param) { + return 'Hello, $param'; + } @override - String get mobileSettingsHapticFeedback => 'Tuŝ-retrokuplado'; + String get mobileGreetingWithoutName => 'Hello'; @override - String get mobileSettingsImmersiveMode => 'Enakviĝa reĝimo'; + String get mobileHideVariation => 'Hide variation'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileHomeTab => 'Hejmo'; @override - String get mobileNotFollowingAnyUser => 'Vi ne abonas ĉiun uzanton.'; + String get mobileLiveStreamers => 'Live streamers'; @override - String get mobileAllGames => 'Ĉiuj ludoj'; + String get mobileMustBeLoggedIn => 'Vi devas esti ensalutata por spekti ĉi tiun paĝon.'; @override - String get mobileRecentSearches => 'Lastaj serĉoj'; + String get mobileNoSearchResults => 'Neniu rezultoj'; @override - String get mobileClearButton => 'Malplenigi'; + String get mobileNotFollowingAnyUser => 'Vi ne abonas ĉiun uzanton.'; + + @override + String get mobileOkButton => 'Bone'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,84 +67,82 @@ class AppLocalizationsEo extends AppLocalizations { } @override - String get mobileNoSearchResults => 'Neniu rezultoj'; + String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; @override - String get mobileAreYouSure => 'Ĉu vi certas?'; + String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; @override String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Share this puzzle'; + String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; @override - String get mobileShareGameURL => 'Share game URL'; + String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; @override - String get mobileShareGamePGN => 'Share PGN'; + String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; @override - String get mobileSharePositionAsFEN => 'Share position as FEN'; + String get mobilePuzzlesTab => 'Puzloj'; @override - String get mobileShowVariations => 'Show variations'; + String get mobileRecentSearches => 'Lastaj serĉoj'; @override - String get mobileHideVariation => 'Hide variation'; + String get mobileSettingsHapticFeedback => 'Tuŝ-retrokuplado'; @override - String get mobileShowComments => 'Show comments'; + String get mobileSettingsImmersiveMode => 'Enakviĝa reĝimo'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobileSettingsTab => 'Agordoj'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; + String get mobileShareGamePGN => 'Share PGN'; @override - String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; + String get mobileShareGameURL => 'Share game URL'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get mobileSharePositionAsFEN => 'Share position as FEN'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileSharePuzzle => 'Share this puzzle'; @override - String get mobileCustomGameJoinAGame => 'Join a game'; + String get mobileShowComments => 'Show comments'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; + String get mobileShowResult => 'Show result'; @override - String get mobileSomethingWentWrong => 'Something went wrong.'; + String get mobileShowVariations => 'Show variations'; @override - String get mobileShowResult => 'Show result'; + String get mobileSomethingWentWrong => 'Something went wrong.'; @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobileSystemColors => 'Sistemaj koloroj'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Hello, $param'; - } + String get mobileToolsTab => 'Iloj'; @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Spekti'; @override String get activityActivity => 'Aktiveco'; @@ -535,6 +538,9 @@ class AppLocalizationsEo extends AppLocalizations { @override String get broadcastStandings => 'Standings'; + @override + String get broadcastOfficialStandings => 'Official Standings'; + @override String broadcastIframeHelp(String param) { return 'More options on the $param'; @@ -565,6 +571,36 @@ class AppLocalizationsEo extends AppLocalizations { @override String get broadcastScore => 'Score'; + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + @override String broadcastNbBroadcasts(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -1992,9 +2028,6 @@ class AppLocalizationsEo extends AppLocalizations { @override String get byCPL => 'Per eraroj'; - @override - String get openStudy => 'Malfermi analizon'; - @override String get enable => 'Ebligi'; @@ -2662,9 +2695,6 @@ class AppLocalizationsEo extends AppLocalizations { @override String get unblock => 'Malbloki'; - @override - String get followsYou => 'Sekvas vin'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 eksekvis $param2'; @@ -5424,6 +5454,11 @@ class AppLocalizationsEo extends AppLocalizations { @override String get studyYouCompletedThisLesson => 'Gratulon! Vi kompletigis la lecionon.'; + @override + String studyPerPage(String param) { + return '$param per page'; + } + @override String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( diff --git a/lib/l10n/l10n_es.dart b/lib/l10n/l10n_es.dart index a5e5bff96b..38be343310 100644 --- a/lib/l10n/l10n_es.dart +++ b/lib/l10n/l10n_es.dart @@ -9,52 +9,57 @@ class AppLocalizationsEs extends AppLocalizations { AppLocalizationsEs([String locale = 'es']) : super(locale); @override - String get mobileHomeTab => 'Inicio'; + String get mobileAllGames => 'Todas las partidas'; @override - String get mobilePuzzlesTab => 'Ejercicios'; + String get mobileAreYouSure => '¿Estás seguro?'; @override - String get mobileToolsTab => 'Herramientas'; + String get mobileBlindfoldMode => 'A ciegas'; @override - String get mobileWatchTab => 'Ver'; + String get mobileCancelTakebackOffer => 'Cancelar oferta de deshacer movimiento'; @override - String get mobileSettingsTab => 'Ajustes'; + String get mobileClearButton => 'Limpiar'; @override - String get mobileMustBeLoggedIn => 'Debes iniciar sesión para ver esta página.'; + String get mobileCorrespondenceClearSavedMove => 'Borrar movimiento guardado'; @override - String get mobileSystemColors => 'Colores del sistema'; + String get mobileCustomGameJoinAGame => 'Únete a una partida'; @override String get mobileFeedbackButton => 'Comentarios'; @override - String get mobileOkButton => 'Aceptar'; + String mobileGreeting(String param) { + return 'Hola $param'; + } @override - String get mobileSettingsHapticFeedback => 'Respuesta táctil'; + String get mobileGreetingWithoutName => 'Hola'; @override - String get mobileSettingsImmersiveMode => 'Modo inmersivo'; + String get mobileHideVariation => 'Ocultar variación'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Ocultar la interfaz del sistema durante la partida. Usa esto si te molestan los iconos de navegación del sistema en los bordes de la pantalla. Se aplica a las pantallas del juego y de Puzzle Storm.'; + String get mobileHomeTab => 'Inicio'; @override - String get mobileNotFollowingAnyUser => 'No estás siguiendo a ningún usuario.'; + String get mobileLiveStreamers => 'Presentadores en vivo'; @override - String get mobileAllGames => 'Todas las partidas'; + String get mobileMustBeLoggedIn => 'Debes iniciar sesión para ver esta página.'; @override - String get mobileRecentSearches => 'Búsquedas recientes'; + String get mobileNoSearchResults => 'No hay resultados'; @override - String get mobileClearButton => 'Limpiar'; + String get mobileNotFollowingAnyUser => 'No estás siguiendo a ningún usuario.'; + + @override + String get mobileOkButton => 'Aceptar'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,84 +67,82 @@ class AppLocalizationsEs extends AppLocalizations { } @override - String get mobileNoSearchResults => 'No hay resultados'; + String get mobilePrefMagnifyDraggedPiece => 'Aumentar la pieza arrastrada'; @override - String get mobileAreYouSure => '¿Estás seguro?'; + String get mobilePuzzleStormConfirmEndRun => '¿Quieres finalizar esta ronda?'; @override - String get mobilePuzzleStreakAbortWarning => 'Perderás tu racha actual y tu puntuación será guardada.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nada que mostrar, por favor cambia los filtros'; @override String get mobilePuzzleStormNothingToShow => 'Nada que mostrar. Juega algunas rondas de Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Compartir este ejercicio'; + String get mobilePuzzleStormSubtitle => 'Resuelve tantos ejercicios como puedas en 3 minutos.'; @override - String get mobileShareGameURL => 'Compartir enlace de la partida'; + String get mobilePuzzleStreakAbortWarning => 'Perderás tu racha actual y tu puntuación será guardada.'; @override - String get mobileShareGamePGN => 'Compartir PGN'; + String get mobilePuzzleThemesSubtitle => 'Realiza ejercicios de tus aperturas favoritas o elige un tema.'; @override - String get mobileSharePositionAsFEN => 'Compartir posición como FEN'; + String get mobilePuzzlesTab => 'Ejercicios'; @override - String get mobileShowVariations => 'Mostrar variaciones'; + String get mobileRecentSearches => 'Búsquedas recientes'; @override - String get mobileHideVariation => 'Ocultar variación'; + String get mobileSettingsHapticFeedback => 'Respuesta táctil'; @override - String get mobileShowComments => 'Mostrar comentarios'; + String get mobileSettingsImmersiveMode => 'Modo inmersivo'; @override - String get mobilePuzzleStormConfirmEndRun => '¿Quieres finalizar esta ronda?'; + String get mobileSettingsImmersiveModeSubtitle => 'Ocultar la interfaz del sistema durante la partida. Usa esto si te molestan los iconos de navegación del sistema en los bordes de la pantalla. Se aplica a las pantallas del juego y de Puzzle Storm.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nada que mostrar, por favor cambia los filtros'; + String get mobileSettingsTab => 'Ajustes'; @override - String get mobileCancelTakebackOffer => 'Cancelar oferta de deshacer movimiento'; + String get mobileShareGamePGN => 'Compartir PGN'; @override - String get mobileWaitingForOpponentToJoin => 'Esperando a que se una un oponente...'; + String get mobileShareGameURL => 'Compartir enlace de la partida'; @override - String get mobileBlindfoldMode => 'A ciegas'; + String get mobileSharePositionAsFEN => 'Compartir posición como FEN'; @override - String get mobileLiveStreamers => 'Presentadores en vivo'; + String get mobileSharePuzzle => 'Compartir este ejercicio'; @override - String get mobileCustomGameJoinAGame => 'Únete a una partida'; + String get mobileShowComments => 'Mostrar comentarios'; @override - String get mobileCorrespondenceClearSavedMove => 'Borrar movimiento guardado'; + String get mobileShowResult => 'Ver resultado'; @override - String get mobileSomethingWentWrong => 'Algo salió mal.'; + String get mobileShowVariations => 'Mostrar variaciones'; @override - String get mobileShowResult => 'Ver resultado'; + String get mobileSomethingWentWrong => 'Algo salió mal.'; @override - String get mobilePuzzleThemesSubtitle => 'Realiza ejercicios de tus aperturas favoritas o elige un tema.'; + String get mobileSystemColors => 'Colores del sistema'; @override - String get mobilePuzzleStormSubtitle => 'Resuelve tantos ejercicios como puedas en 3 minutos.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Hola $param'; - } + String get mobileToolsTab => 'Herramientas'; @override - String get mobileGreetingWithoutName => 'Hola'; + String get mobileWaitingForOpponentToJoin => 'Esperando a que se una un oponente...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Aumentar la pieza arrastrada'; + String get mobileWatchTab => 'Ver'; @override String get activityActivity => 'Actividad'; @@ -535,6 +538,9 @@ class AppLocalizationsEs extends AppLocalizations { @override String get broadcastStandings => 'Clasificación'; + @override + String get broadcastOfficialStandings => 'Official Standings'; + @override String broadcastIframeHelp(String param) { return 'Más opciones en $param'; @@ -565,6 +571,36 @@ class AppLocalizationsEs extends AppLocalizations { @override String get broadcastScore => 'Resultado'; + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + @override String broadcastNbBroadcasts(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -1992,9 +2028,6 @@ class AppLocalizationsEs extends AppLocalizations { @override String get byCPL => 'Por PCP'; - @override - String get openStudy => 'Abrir estudio'; - @override String get enable => 'Activar'; @@ -2662,9 +2695,6 @@ class AppLocalizationsEs extends AppLocalizations { @override String get unblock => 'Desbloquear'; - @override - String get followsYou => 'Te sigue'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 comenzó a seguir a $param2'; @@ -5424,6 +5454,11 @@ class AppLocalizationsEs extends AppLocalizations { @override String get studyYouCompletedThisLesson => '¡Felicidades! Has completado esta lección.'; + @override + String studyPerPage(String param) { + return '$param per page'; + } + @override String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( diff --git a/lib/l10n/l10n_et.dart b/lib/l10n/l10n_et.dart index 892e85dded..a9da5ffa6c 100644 --- a/lib/l10n/l10n_et.dart +++ b/lib/l10n/l10n_et.dart @@ -9,52 +9,57 @@ class AppLocalizationsEt extends AppLocalizations { AppLocalizationsEt([String locale = 'et']) : super(locale); @override - String get mobileHomeTab => 'Home'; + String get mobileAllGames => 'All games'; @override - String get mobilePuzzlesTab => 'Puzzles'; + String get mobileAreYouSure => 'Are you sure?'; @override - String get mobileToolsTab => 'Tools'; + String get mobileBlindfoldMode => 'Blindfold'; @override - String get mobileWatchTab => 'Watch'; + String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @override - String get mobileSettingsTab => 'Settings'; + String get mobileClearButton => 'Clear'; @override - String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; + String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; @override - String get mobileSystemColors => 'System colors'; + String get mobileCustomGameJoinAGame => 'Join a game'; @override String get mobileFeedbackButton => 'Feedback'; @override - String get mobileOkButton => 'OK'; + String mobileGreeting(String param) { + return 'Hello, $param'; + } @override - String get mobileSettingsHapticFeedback => 'Haptic feedback'; + String get mobileGreetingWithoutName => 'Hello'; @override - String get mobileSettingsImmersiveMode => 'Immersive mode'; + String get mobileHideVariation => 'Hide variation'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileHomeTab => 'Home'; @override - String get mobileNotFollowingAnyUser => 'You are not following any user.'; + String get mobileLiveStreamers => 'Live streamers'; @override - String get mobileAllGames => 'All games'; + String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; @override - String get mobileRecentSearches => 'Recent searches'; + String get mobileNoSearchResults => 'No results'; @override - String get mobileClearButton => 'Clear'; + String get mobileNotFollowingAnyUser => 'You are not following any user.'; + + @override + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,84 +67,82 @@ class AppLocalizationsEt extends AppLocalizations { } @override - String get mobileNoSearchResults => 'No results'; + String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; @override - String get mobileAreYouSure => 'Are you sure?'; + String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; @override String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Share this puzzle'; + String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; @override - String get mobileShareGameURL => 'Share game URL'; + String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; @override - String get mobileShareGamePGN => 'Share PGN'; + String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; @override - String get mobileSharePositionAsFEN => 'Share position as FEN'; + String get mobilePuzzlesTab => 'Puzzles'; @override - String get mobileShowVariations => 'Show variations'; + String get mobileRecentSearches => 'Recent searches'; @override - String get mobileHideVariation => 'Hide variation'; + String get mobileSettingsHapticFeedback => 'Haptic feedback'; @override - String get mobileShowComments => 'Show comments'; + String get mobileSettingsImmersiveMode => 'Immersive mode'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobileSettingsTab => 'Settings'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; + String get mobileShareGamePGN => 'Share PGN'; @override - String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; + String get mobileShareGameURL => 'Share game URL'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get mobileSharePositionAsFEN => 'Share position as FEN'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileSharePuzzle => 'Share this puzzle'; @override - String get mobileCustomGameJoinAGame => 'Join a game'; + String get mobileShowComments => 'Show comments'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; + String get mobileShowResult => 'Show result'; @override - String get mobileSomethingWentWrong => 'Something went wrong.'; + String get mobileShowVariations => 'Show variations'; @override - String get mobileShowResult => 'Show result'; + String get mobileSomethingWentWrong => 'Something went wrong.'; @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobileSystemColors => 'System colors'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Hello, $param'; - } + String get mobileToolsTab => 'Tools'; @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Watch'; @override String get activityActivity => 'Aktiivsus'; @@ -535,6 +538,9 @@ class AppLocalizationsEt extends AppLocalizations { @override String get broadcastStandings => 'Standings'; + @override + String get broadcastOfficialStandings => 'Official Standings'; + @override String broadcastIframeHelp(String param) { return 'More options on the $param'; @@ -565,6 +571,36 @@ class AppLocalizationsEt extends AppLocalizations { @override String get broadcastScore => 'Score'; + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + @override String broadcastNbBroadcasts(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -1992,9 +2028,6 @@ class AppLocalizationsEt extends AppLocalizations { @override String get byCPL => 'CPL järgi'; - @override - String get openStudy => 'Ava uuring'; - @override String get enable => 'Luba'; @@ -2662,9 +2695,6 @@ class AppLocalizationsEt extends AppLocalizations { @override String get unblock => 'Tühista blokeering'; - @override - String get followsYou => 'Jälgib sind'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 hakkas jälgima $param2'; @@ -5424,6 +5454,11 @@ class AppLocalizationsEt extends AppLocalizations { @override String get studyYouCompletedThisLesson => 'Palju õnne! Oled läbinud selle õppetunni.'; + @override + String studyPerPage(String param) { + return '$param per page'; + } + @override String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( diff --git a/lib/l10n/l10n_eu.dart b/lib/l10n/l10n_eu.dart index 7a405058d6..8e7aaf5144 100644 --- a/lib/l10n/l10n_eu.dart +++ b/lib/l10n/l10n_eu.dart @@ -9,52 +9,57 @@ class AppLocalizationsEu extends AppLocalizations { AppLocalizationsEu([String locale = 'eu']) : super(locale); @override - String get mobileHomeTab => 'Hasiera'; + String get mobileAllGames => 'Partida guztiak'; @override - String get mobilePuzzlesTab => 'Ariketak'; + String get mobileAreYouSure => 'Ziur zaude?'; @override - String get mobileToolsTab => 'Tresnak'; + String get mobileBlindfoldMode => 'Itsuka'; @override - String get mobileWatchTab => 'Ikusi'; + String get mobileCancelTakebackOffer => 'Bertan behera utzi atzera-egite eskaera'; @override - String get mobileSettingsTab => 'Ezarpenak'; + String get mobileClearButton => 'Garbitu'; @override - String get mobileMustBeLoggedIn => 'Sartu egin behar zara orri hau ikusteko.'; + String get mobileCorrespondenceClearSavedMove => 'Garbitu gordetako jokaldia'; @override - String get mobileSystemColors => 'Sistemaren koloreak'; + String get mobileCustomGameJoinAGame => 'Sartu partida baten'; @override String get mobileFeedbackButton => 'Iritzia'; @override - String get mobileOkButton => 'Ados'; + String mobileGreeting(String param) { + return 'Kaixo $param'; + } @override - String get mobileSettingsHapticFeedback => 'Ukipen-erantzuna'; + String get mobileGreetingWithoutName => 'Kaixo'; @override - String get mobileSettingsImmersiveMode => 'Murgiltze modua'; + String get mobileHideVariation => 'Ezkutatu aukera'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Ezkutatu sistemaren menuak jokatzen ari zaren artean. Erabili hau zure telefonoaren nabigatzeko aukerek traba egiten badizute. Partida bati eta ariketen zaparradan aplikatu daiteke.'; + String get mobileHomeTab => 'Hasiera'; @override - String get mobileNotFollowingAnyUser => 'Ez zaude erabiltzailerik jarraitzen.'; + String get mobileLiveStreamers => 'Zuzeneko streamerrak'; @override - String get mobileAllGames => 'Partida guztiak'; + String get mobileMustBeLoggedIn => 'Sartu egin behar zara orri hau ikusteko.'; @override - String get mobileRecentSearches => 'Azken bilaketak'; + String get mobileNoSearchResults => 'Emaitzarik ez'; @override - String get mobileClearButton => 'Garbitu'; + String get mobileNotFollowingAnyUser => 'Ez zaude erabiltzailerik jarraitzen.'; + + @override + String get mobileOkButton => 'Ados'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,84 +67,82 @@ class AppLocalizationsEu extends AppLocalizations { } @override - String get mobileNoSearchResults => 'Emaitzarik ez'; + String get mobilePrefMagnifyDraggedPiece => 'Handitu arrastatutako pieza'; @override - String get mobileAreYouSure => 'Ziur zaude?'; + String get mobilePuzzleStormConfirmEndRun => 'Saiakera hau amaitu nahi duzu?'; @override - String get mobilePuzzleStreakAbortWarning => 'Zure uneko bolada galduko duzu eta zure puntuazioa gorde egingo da.'; + String get mobilePuzzleStormFilterNothingToShow => 'Ez dago erakusteko ezer, aldatu filtroak'; @override String get mobilePuzzleStormNothingToShow => 'Ez dago ezer erakusteko. Jokatu Ariketa zaparrada batzuk.'; @override - String get mobileSharePuzzle => 'Partekatu ariketa hau'; + String get mobilePuzzleStormSubtitle => 'Ebatzi ahalik eta ariketa gehien 3 minututan.'; @override - String get mobileShareGameURL => 'Partekatu partidaren URLa'; + String get mobilePuzzleStreakAbortWarning => 'Zure uneko bolada galduko duzu eta zure puntuazioa gorde egingo da.'; @override - String get mobileShareGamePGN => 'Partekatu PGNa'; + String get mobilePuzzleThemesSubtitle => 'Jokatu zure irekiera gogokoenen ariketak, edo aukeratu gai bat.'; @override - String get mobileSharePositionAsFEN => 'Partekatu posizioa FEN gisa'; + String get mobilePuzzlesTab => 'Ariketak'; @override - String get mobileShowVariations => 'Erakutsi aukerak'; + String get mobileRecentSearches => 'Azken bilaketak'; @override - String get mobileHideVariation => 'Ezkutatu aukera'; + String get mobileSettingsHapticFeedback => 'Ukipen-erantzuna'; @override - String get mobileShowComments => 'Erakutsi iruzkinak'; + String get mobileSettingsImmersiveMode => 'Murgiltze modua'; @override - String get mobilePuzzleStormConfirmEndRun => 'Saiakera hau amaitu nahi duzu?'; + String get mobileSettingsImmersiveModeSubtitle => 'Ezkutatu sistemaren menuak jokatzen ari zaren artean. Erabili hau zure telefonoaren nabigatzeko aukerek traba egiten badizute. Partida bati eta ariketen zaparradan aplikatu daiteke.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Ez dago erakusteko ezer, aldatu filtroak'; + String get mobileSettingsTab => 'Ezarpenak'; @override - String get mobileCancelTakebackOffer => 'Bertan behera utzi atzera-egite eskaera'; + String get mobileShareGamePGN => 'Partekatu PGNa'; @override - String get mobileWaitingForOpponentToJoin => 'Aurkaria sartzeko zain...'; + String get mobileShareGameURL => 'Partekatu partidaren URLa'; @override - String get mobileBlindfoldMode => 'Itsuka'; + String get mobileSharePositionAsFEN => 'Partekatu posizioa FEN gisa'; @override - String get mobileLiveStreamers => 'Zuzeneko streamerrak'; + String get mobileSharePuzzle => 'Partekatu ariketa hau'; @override - String get mobileCustomGameJoinAGame => 'Sartu partida baten'; + String get mobileShowComments => 'Erakutsi iruzkinak'; @override - String get mobileCorrespondenceClearSavedMove => 'Garbitu gordetako jokaldia'; + String get mobileShowResult => 'Erakutsi emaitza'; @override - String get mobileSomethingWentWrong => 'Zerbait gaizki joan da.'; + String get mobileShowVariations => 'Erakutsi aukerak'; @override - String get mobileShowResult => 'Erakutsi emaitza'; + String get mobileSomethingWentWrong => 'Zerbait gaizki joan da.'; @override - String get mobilePuzzleThemesSubtitle => 'Jokatu zure irekiera gogokoenen ariketak, edo aukeratu gai bat.'; + String get mobileSystemColors => 'Sistemaren koloreak'; @override - String get mobilePuzzleStormSubtitle => 'Ebatzi ahalik eta ariketa gehien 3 minututan.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Kaixo $param'; - } + String get mobileToolsTab => 'Tresnak'; @override - String get mobileGreetingWithoutName => 'Kaixo'; + String get mobileWaitingForOpponentToJoin => 'Aurkaria sartzeko zain...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Handitu arrastatutako pieza'; + String get mobileWatchTab => 'Ikusi'; @override String get activityActivity => 'Jarduera'; @@ -535,6 +538,9 @@ class AppLocalizationsEu extends AppLocalizations { @override String get broadcastStandings => 'Standings'; + @override + String get broadcastOfficialStandings => 'Official Standings'; + @override String broadcastIframeHelp(String param) { return 'More options on the $param'; @@ -565,6 +571,36 @@ class AppLocalizationsEu extends AppLocalizations { @override String get broadcastScore => 'Score'; + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + @override String broadcastNbBroadcasts(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -1992,9 +2028,6 @@ class AppLocalizationsEu extends AppLocalizations { @override String get byCPL => 'CPL'; - @override - String get openStudy => 'Ikerketa ireki'; - @override String get enable => 'Aktibatu'; @@ -2662,9 +2695,6 @@ class AppLocalizationsEu extends AppLocalizations { @override String get unblock => 'Desblokeatu'; - @override - String get followsYou => 'Zu jarraitzen'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 $param2 jarraitzen hasi da'; @@ -5424,6 +5454,11 @@ class AppLocalizationsEu extends AppLocalizations { @override String get studyYouCompletedThisLesson => 'Zorionak! Ikasgai hau osatu duzu.'; + @override + String studyPerPage(String param) { + return '$param per page'; + } + @override String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( diff --git a/lib/l10n/l10n_fa.dart b/lib/l10n/l10n_fa.dart index 7567dddbc7..dae9bbe211 100644 --- a/lib/l10n/l10n_fa.dart +++ b/lib/l10n/l10n_fa.dart @@ -9,52 +9,57 @@ class AppLocalizationsFa extends AppLocalizations { AppLocalizationsFa([String locale = 'fa']) : super(locale); @override - String get mobileHomeTab => 'خانه'; + String get mobileAllGames => 'همه بازی‌ها'; @override - String get mobilePuzzlesTab => 'معماها'; + String get mobileAreYouSure => 'مطمئنید؟'; @override - String get mobileToolsTab => 'ابزارها'; + String get mobileBlindfoldMode => 'چشم‌بسته'; @override - String get mobileWatchTab => 'تماشا'; + String get mobileCancelTakebackOffer => 'رد درخواست برگرداندن'; @override - String get mobileSettingsTab => 'تنظیمات'; + String get mobileClearButton => 'پاکسازی'; @override - String get mobileMustBeLoggedIn => 'برای دیدن این برگه باید وارد شده باشید.'; + String get mobileCorrespondenceClearSavedMove => 'پاک کردن حرکت ذخیره شده'; @override - String get mobileSystemColors => 'رنگ‌های دستگاه'; + String get mobileCustomGameJoinAGame => 'به بازی بپیوندید'; @override String get mobileFeedbackButton => 'بازخورد'; @override - String get mobileOkButton => 'باشه'; + String mobileGreeting(String param) { + return 'درود، $param'; + } @override - String get mobileSettingsHapticFeedback => 'بازخورد لمسی'; + String get mobileGreetingWithoutName => 'درود'; @override - String get mobileSettingsImmersiveMode => 'حالت فراگیر'; + String get mobileHideVariation => 'بستن شاخه‌ها'; @override - String get mobileSettingsImmersiveModeSubtitle => 'رابط کاربری را هنگام بازی پنهان کنید. اگر ناوبری لمسی در لبه‌های دستگاه اذیتتان می‌کند از این استفاده کنید. کارساز برای برگه‌های بازی و معماباران.'; + String get mobileHomeTab => 'خانه'; @override - String get mobileNotFollowingAnyUser => 'شما هیچ کاربری را دنبال نمی‌کنید.'; + String get mobileLiveStreamers => 'بَرخَط-محتواسازان زنده'; @override - String get mobileAllGames => 'همه بازی‌ها'; + String get mobileMustBeLoggedIn => 'برای دیدن این برگه باید وارد شده باشید.'; @override - String get mobileRecentSearches => 'واپسین جستجوها'; + String get mobileNoSearchResults => 'بدون پیامد'; @override - String get mobileClearButton => 'پاکسازی'; + String get mobileNotFollowingAnyUser => 'شما هیچ کاربری را دنبال نمی‌کنید.'; + + @override + String get mobileOkButton => 'باشه'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,84 +67,82 @@ class AppLocalizationsFa extends AppLocalizations { } @override - String get mobileNoSearchResults => 'بدون پیامد'; + String get mobilePrefMagnifyDraggedPiece => 'بزرگ‌نمودن مهره‌ی کشیده'; @override - String get mobileAreYouSure => 'مطمئنید؟'; + String get mobilePuzzleStormConfirmEndRun => 'می‌خواهید این دور را به پایان برسانید؟'; @override - String get mobilePuzzleStreakAbortWarning => 'شما ریسه فعلی‌تان را خواهید باخت و امتیازتان ذخیره خواهد شد.'; + String get mobilePuzzleStormFilterNothingToShow => 'چیزی برای نمایش نیست، خواهشمندیم پالایه‌ها را دگرسان کنید.'; @override String get mobilePuzzleStormNothingToShow => 'چیزی برای نمایش نیست، چند دور معماباران بازی کنید.'; @override - String get mobileSharePuzzle => 'همرسانی این معما'; + String get mobilePuzzleStormSubtitle => 'هر چند تا معما را که می‌توانید در ۳ دقیقه حل کنید.'; @override - String get mobileShareGameURL => 'همرسانی وب‌نشانی بازی'; + String get mobilePuzzleStreakAbortWarning => 'شما ریسه فعلی‌تان را خواهید باخت و امتیازتان ذخیره خواهد شد.'; @override - String get mobileShareGamePGN => 'همرسانی PGN'; + String get mobilePuzzleThemesSubtitle => 'معماهایی را از گشایش دلخواه‌تان بازی کنید، یا جستاری را برگزینید.'; @override - String get mobileSharePositionAsFEN => 'همرسانی وضعیت، به شکل FEN'; + String get mobilePuzzlesTab => 'معماها'; @override - String get mobileShowVariations => 'باز کردن شاخه‌ها'; + String get mobileRecentSearches => 'واپسین جستجوها'; @override - String get mobileHideVariation => 'بستن شاخه‌ها'; + String get mobileSettingsHapticFeedback => 'بازخورد لمسی'; @override - String get mobileShowComments => 'نمایش دیدگاه‌ها'; + String get mobileSettingsImmersiveMode => 'حالت فراگیر'; @override - String get mobilePuzzleStormConfirmEndRun => 'می‌خواهید این دور را به پایان برسانید؟'; + String get mobileSettingsImmersiveModeSubtitle => 'رابط کاربری را هنگام بازی پنهان کنید. اگر ناوبری لمسی در لبه‌های دستگاه اذیتتان می‌کند از این استفاده کنید. کارساز برای برگه‌های بازی و معماباران.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'چیزی برای نمایش نیست، خواهشمندیم پالایه‌ها را دگرسان کنید.'; + String get mobileSettingsTab => 'تنظیمات'; @override - String get mobileCancelTakebackOffer => 'رد درخواست برگرداندن'; + String get mobileShareGamePGN => 'همرسانی PGN'; @override - String get mobileWaitingForOpponentToJoin => 'شکیبا برای پیوستن حریف...'; + String get mobileShareGameURL => 'همرسانی وب‌نشانی بازی'; @override - String get mobileBlindfoldMode => 'چشم‌بسته'; + String get mobileSharePositionAsFEN => 'همرسانی وضعیت، به شکل FEN'; @override - String get mobileLiveStreamers => 'بَرخَط-محتواسازان زنده'; + String get mobileSharePuzzle => 'همرسانی این معما'; @override - String get mobileCustomGameJoinAGame => 'به بازی بپیوندید'; + String get mobileShowComments => 'نمایش دیدگاه‌ها'; @override - String get mobileCorrespondenceClearSavedMove => 'پاک کردن حرکت ذخیره شده'; + String get mobileShowResult => 'نمایش پیامد'; @override - String get mobileSomethingWentWrong => 'مشکلی پیش آمد.'; + String get mobileShowVariations => 'باز کردن شاخه‌ها'; @override - String get mobileShowResult => 'نمایش پیامد'; + String get mobileSomethingWentWrong => 'مشکلی پیش آمد.'; @override - String get mobilePuzzleThemesSubtitle => 'معماهایی را از گشایش دلخواه‌تان بازی کنید، یا جستاری را برگزینید.'; + String get mobileSystemColors => 'رنگ‌های دستگاه'; @override - String get mobilePuzzleStormSubtitle => 'هر چند تا معما را که می‌توانید در ۳ دقیقه حل کنید.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'درود، $param'; - } + String get mobileToolsTab => 'ابزارها'; @override - String get mobileGreetingWithoutName => 'درود'; + String get mobileWaitingForOpponentToJoin => 'شکیبا برای پیوستن حریف...'; @override - String get mobilePrefMagnifyDraggedPiece => 'بزرگ‌نمودن مهره‌ی کشیده'; + String get mobileWatchTab => 'تماشا'; @override String get activityActivity => 'فعالیت'; @@ -535,6 +538,9 @@ class AppLocalizationsFa extends AppLocalizations { @override String get broadcastStandings => 'Standings'; + @override + String get broadcastOfficialStandings => 'Official Standings'; + @override String broadcastIframeHelp(String param) { return 'More options on the $param'; @@ -565,6 +571,36 @@ class AppLocalizationsFa extends AppLocalizations { @override String get broadcastScore => 'امتیاز'; + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + @override String broadcastNbBroadcasts(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -1992,9 +2028,6 @@ class AppLocalizationsFa extends AppLocalizations { @override String get byCPL => 'درنگ هنگام اشتباه'; - @override - String get openStudy => 'گشودن مطالعه'; - @override String get enable => 'فعال سازی'; @@ -2662,9 +2695,6 @@ class AppLocalizationsFa extends AppLocalizations { @override String get unblock => 'لغو انسداد'; - @override - String get followsYou => 'شما را می‌دنبالد'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 دنبالیدن $param2 را آغازید'; @@ -5424,6 +5454,11 @@ class AppLocalizationsFa extends AppLocalizations { @override String get studyYouCompletedThisLesson => 'تبریک! شما این درس را کامل کردید.'; + @override + String studyPerPage(String param) { + return '$param per page'; + } + @override String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( diff --git a/lib/l10n/l10n_fi.dart b/lib/l10n/l10n_fi.dart index 0228d6ce2d..9638039299 100644 --- a/lib/l10n/l10n_fi.dart +++ b/lib/l10n/l10n_fi.dart @@ -9,52 +9,57 @@ class AppLocalizationsFi extends AppLocalizations { AppLocalizationsFi([String locale = 'fi']) : super(locale); @override - String get mobileHomeTab => 'Etusivu'; + String get mobileAllGames => 'Kaikki pelit'; @override - String get mobilePuzzlesTab => 'Tehtävät'; + String get mobileAreYouSure => 'Oletko varma?'; @override - String get mobileToolsTab => 'Työkalut'; + String get mobileBlindfoldMode => 'Sokko'; @override - String get mobileWatchTab => 'Seuraa'; + String get mobileCancelTakebackOffer => 'Peruuta siirron peruutuspyyntö'; @override - String get mobileSettingsTab => 'Asetukset'; + String get mobileClearButton => 'Tyhjennä'; @override - String get mobileMustBeLoggedIn => 'Sinun täytyy olla kirjautuneena nähdäksesi tämän sivun.'; + String get mobileCorrespondenceClearSavedMove => 'Poista tallennettu siirto'; @override - String get mobileSystemColors => 'Järjestelmän värit'; + String get mobileCustomGameJoinAGame => 'Liity peliin'; @override String get mobileFeedbackButton => 'Palaute'; @override - String get mobileOkButton => 'OK'; + String mobileGreeting(String param) { + return 'Hei $param'; + } @override - String get mobileSettingsHapticFeedback => 'Kosketuspalaute'; + String get mobileGreetingWithoutName => 'Hei'; @override - String get mobileSettingsImmersiveMode => 'Kokoruututila'; + String get mobileHideVariation => 'Piilota muunnelma'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Piilota laitteen käyttöliittymä pelatessasi. Valitse tämä, jos laitteesi navigointieleet näytön laidoilla ovat sinulle häiriöksi. Asetus vaikuttaa peli- ja Puzzle Storm -näkymiin.'; + String get mobileHomeTab => 'Etusivu'; @override - String get mobileNotFollowingAnyUser => 'Et seuraa yhtäkään käyttäjää.'; + String get mobileLiveStreamers => 'Live streamers'; @override - String get mobileAllGames => 'Kaikki pelit'; + String get mobileMustBeLoggedIn => 'Sinun täytyy olla kirjautuneena nähdäksesi tämän sivun.'; @override - String get mobileRecentSearches => 'Viimeisimmät haut'; + String get mobileNoSearchResults => 'Ei hakutuloksia'; @override - String get mobileClearButton => 'Tyhjennä'; + String get mobileNotFollowingAnyUser => 'Et seuraa yhtäkään käyttäjää.'; + + @override + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,84 +67,82 @@ class AppLocalizationsFi extends AppLocalizations { } @override - String get mobileNoSearchResults => 'Ei hakutuloksia'; + String get mobilePrefMagnifyDraggedPiece => 'Suurenna vedettävä nappula'; @override - String get mobileAreYouSure => 'Oletko varma?'; + String get mobilePuzzleStormConfirmEndRun => 'Haluatko lopettaa tämän sarjan?'; @override - String get mobilePuzzleStreakAbortWarning => 'Parhaillaan menossa oleva putkesi päättyy, ja pistemääräsi tallennetaan.'; + String get mobilePuzzleStormFilterNothingToShow => 'Ei näytettävää, muuta suodatusehtoja'; @override String get mobilePuzzleStormNothingToShow => 'Ei näytettävää. Pelaa ensin muutama sarja Puzzle Stormia.'; @override - String get mobileSharePuzzle => 'Jaa tämä tehtävä'; + String get mobilePuzzleStormSubtitle => 'Ratkaise mahdollisimman monta tehtävää 3 minuutissa.'; @override - String get mobileShareGameURL => 'Jaa pelin URL'; + String get mobilePuzzleStreakAbortWarning => 'Parhaillaan menossa oleva putkesi päättyy, ja pistemääräsi tallennetaan.'; @override - String get mobileShareGamePGN => 'Jaa PGN'; + String get mobilePuzzleThemesSubtitle => 'Tee tehtäviä suosikkiavauksistasi tai valitse tehtäväteema.'; @override - String get mobileSharePositionAsFEN => 'Jaa asema FEN:nä'; + String get mobilePuzzlesTab => 'Tehtävät'; @override - String get mobileShowVariations => 'Näytä muunnelmat'; + String get mobileRecentSearches => 'Viimeisimmät haut'; @override - String get mobileHideVariation => 'Piilota muunnelma'; + String get mobileSettingsHapticFeedback => 'Kosketuspalaute'; @override - String get mobileShowComments => 'Näytä kommentit'; + String get mobileSettingsImmersiveMode => 'Kokoruututila'; @override - String get mobilePuzzleStormConfirmEndRun => 'Haluatko lopettaa tämän sarjan?'; + String get mobileSettingsImmersiveModeSubtitle => 'Piilota laitteen käyttöliittymä pelatessasi. Valitse tämä, jos laitteesi navigointieleet näytön laidoilla ovat sinulle häiriöksi. Asetus vaikuttaa peli- ja Puzzle Storm -näkymiin.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Ei näytettävää, muuta suodatusehtoja'; + String get mobileSettingsTab => 'Asetukset'; @override - String get mobileCancelTakebackOffer => 'Peruuta siirron peruutuspyyntö'; + String get mobileShareGamePGN => 'Jaa PGN'; @override - String get mobileWaitingForOpponentToJoin => 'Odotetaan vastustajan löytymistä...'; + String get mobileShareGameURL => 'Jaa pelin URL'; @override - String get mobileBlindfoldMode => 'Sokko'; + String get mobileSharePositionAsFEN => 'Jaa asema FEN:nä'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileSharePuzzle => 'Jaa tämä tehtävä'; @override - String get mobileCustomGameJoinAGame => 'Liity peliin'; + String get mobileShowComments => 'Näytä kommentit'; @override - String get mobileCorrespondenceClearSavedMove => 'Poista tallennettu siirto'; + String get mobileShowResult => 'Näytä lopputulos'; @override - String get mobileSomethingWentWrong => 'Jokin meni vikaan.'; + String get mobileShowVariations => 'Näytä muunnelmat'; @override - String get mobileShowResult => 'Näytä lopputulos'; + String get mobileSomethingWentWrong => 'Jokin meni vikaan.'; @override - String get mobilePuzzleThemesSubtitle => 'Tee tehtäviä suosikkiavauksistasi tai valitse tehtäväteema.'; + String get mobileSystemColors => 'Järjestelmän värit'; @override - String get mobilePuzzleStormSubtitle => 'Ratkaise mahdollisimman monta tehtävää 3 minuutissa.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Hei $param'; - } + String get mobileToolsTab => 'Työkalut'; @override - String get mobileGreetingWithoutName => 'Hei'; + String get mobileWaitingForOpponentToJoin => 'Odotetaan vastustajan löytymistä...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Suurenna vedettävä nappula'; + String get mobileWatchTab => 'Seuraa'; @override String get activityActivity => 'Toiminta'; @@ -535,6 +538,9 @@ class AppLocalizationsFi extends AppLocalizations { @override String get broadcastStandings => 'Tulostaulu'; + @override + String get broadcastOfficialStandings => 'Official Standings'; + @override String broadcastIframeHelp(String param) { return 'Lisäasetuksia löytyy $param'; @@ -565,6 +571,36 @@ class AppLocalizationsFi extends AppLocalizations { @override String get broadcastScore => 'Pisteet'; + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + @override String broadcastNbBroadcasts(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -1992,9 +2028,6 @@ class AppLocalizationsFi extends AppLocalizations { @override String get byCPL => 'Virheet'; - @override - String get openStudy => 'Avaa tutkielma'; - @override String get enable => 'Käytössä'; @@ -2662,9 +2695,6 @@ class AppLocalizationsFi extends AppLocalizations { @override String get unblock => 'Poista esto'; - @override - String get followsYou => 'Seuraa sinua'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 alkoi seurata $param2'; @@ -5424,6 +5454,11 @@ class AppLocalizationsFi extends AppLocalizations { @override String get studyYouCompletedThisLesson => 'Onnittelut! Olet suorittanut tämän oppiaiheen.'; + @override + String studyPerPage(String param) { + return '$param per page'; + } + @override String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( diff --git a/lib/l10n/l10n_fo.dart b/lib/l10n/l10n_fo.dart index bdf2bcc58f..9e7ec519ec 100644 --- a/lib/l10n/l10n_fo.dart +++ b/lib/l10n/l10n_fo.dart @@ -9,52 +9,57 @@ class AppLocalizationsFo extends AppLocalizations { AppLocalizationsFo([String locale = 'fo']) : super(locale); @override - String get mobileHomeTab => 'Home'; + String get mobileAllGames => 'All games'; @override - String get mobilePuzzlesTab => 'Puzzles'; + String get mobileAreYouSure => 'Are you sure?'; @override - String get mobileToolsTab => 'Tools'; + String get mobileBlindfoldMode => 'Blindfold'; @override - String get mobileWatchTab => 'Watch'; + String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @override - String get mobileSettingsTab => 'Settings'; + String get mobileClearButton => 'Clear'; @override - String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; + String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; @override - String get mobileSystemColors => 'System colors'; + String get mobileCustomGameJoinAGame => 'Join a game'; @override String get mobileFeedbackButton => 'Feedback'; @override - String get mobileOkButton => 'OK'; + String mobileGreeting(String param) { + return 'Hello, $param'; + } @override - String get mobileSettingsHapticFeedback => 'Haptic feedback'; + String get mobileGreetingWithoutName => 'Hello'; @override - String get mobileSettingsImmersiveMode => 'Immersive mode'; + String get mobileHideVariation => 'Hide variation'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileHomeTab => 'Home'; @override - String get mobileNotFollowingAnyUser => 'You are not following any user.'; + String get mobileLiveStreamers => 'Live streamers'; @override - String get mobileAllGames => 'All games'; + String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; @override - String get mobileRecentSearches => 'Recent searches'; + String get mobileNoSearchResults => 'No results'; @override - String get mobileClearButton => 'Clear'; + String get mobileNotFollowingAnyUser => 'You are not following any user.'; + + @override + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,84 +67,82 @@ class AppLocalizationsFo extends AppLocalizations { } @override - String get mobileNoSearchResults => 'No results'; + String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; @override - String get mobileAreYouSure => 'Are you sure?'; + String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; @override String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Share this puzzle'; + String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; @override - String get mobileShareGameURL => 'Share game URL'; + String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; @override - String get mobileShareGamePGN => 'Share PGN'; + String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; @override - String get mobileSharePositionAsFEN => 'Share position as FEN'; + String get mobilePuzzlesTab => 'Puzzles'; @override - String get mobileShowVariations => 'Show variations'; + String get mobileRecentSearches => 'Recent searches'; @override - String get mobileHideVariation => 'Hide variation'; + String get mobileSettingsHapticFeedback => 'Haptic feedback'; @override - String get mobileShowComments => 'Show comments'; + String get mobileSettingsImmersiveMode => 'Immersive mode'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobileSettingsTab => 'Settings'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; + String get mobileShareGamePGN => 'Share PGN'; @override - String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; + String get mobileShareGameURL => 'Share game URL'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get mobileSharePositionAsFEN => 'Share position as FEN'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileSharePuzzle => 'Share this puzzle'; @override - String get mobileCustomGameJoinAGame => 'Join a game'; + String get mobileShowComments => 'Show comments'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; + String get mobileShowResult => 'Show result'; @override - String get mobileSomethingWentWrong => 'Something went wrong.'; + String get mobileShowVariations => 'Show variations'; @override - String get mobileShowResult => 'Show result'; + String get mobileSomethingWentWrong => 'Something went wrong.'; @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobileSystemColors => 'System colors'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Hello, $param'; - } + String get mobileToolsTab => 'Tools'; @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Watch'; @override String get activityActivity => 'Virkni'; @@ -535,6 +538,9 @@ class AppLocalizationsFo extends AppLocalizations { @override String get broadcastStandings => 'Standings'; + @override + String get broadcastOfficialStandings => 'Official Standings'; + @override String broadcastIframeHelp(String param) { return 'More options on the $param'; @@ -565,6 +571,36 @@ class AppLocalizationsFo extends AppLocalizations { @override String get broadcastScore => 'Score'; + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + @override String broadcastNbBroadcasts(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -1990,9 +2026,6 @@ class AppLocalizationsFo extends AppLocalizations { @override String get byCPL => 'Við CPL'; - @override - String get openStudy => 'Lat rannsókn upp'; - @override String get enable => 'Loyv'; @@ -2660,9 +2693,6 @@ class AppLocalizationsFo extends AppLocalizations { @override String get unblock => 'Forða ikki'; - @override - String get followsYou => 'Fylgir tær'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 byrjaði at fylgja $param2'; @@ -5422,6 +5452,11 @@ class AppLocalizationsFo extends AppLocalizations { @override String get studyYouCompletedThisLesson => 'Congratulations! You completed this lesson.'; + @override + String studyPerPage(String param) { + return '$param per page'; + } + @override String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( diff --git a/lib/l10n/l10n_fr.dart b/lib/l10n/l10n_fr.dart index 9955f385e1..aceb0029ea 100644 --- a/lib/l10n/l10n_fr.dart +++ b/lib/l10n/l10n_fr.dart @@ -9,52 +9,57 @@ class AppLocalizationsFr extends AppLocalizations { AppLocalizationsFr([String locale = 'fr']) : super(locale); @override - String get mobileHomeTab => 'Accueil'; + String get mobileAllGames => 'Toutes les parties'; @override - String get mobilePuzzlesTab => 'Problèmes'; + String get mobileAreYouSure => 'Êtes-vous sûr(e) ?'; @override - String get mobileToolsTab => 'Outils'; + String get mobileBlindfoldMode => 'Partie à l\'aveugle'; @override - String get mobileWatchTab => 'Regarder'; + String get mobileCancelTakebackOffer => 'Annuler la proposition de reprise du coup'; @override - String get mobileSettingsTab => 'Paramètres'; + String get mobileClearButton => 'Effacer'; @override - String get mobileMustBeLoggedIn => 'Vous devez être connecté pour voir cette page.'; + String get mobileCorrespondenceClearSavedMove => 'Effacer les coups enregistrés'; @override - String get mobileSystemColors => 'Couleurs du système'; + String get mobileCustomGameJoinAGame => 'Joindre une partie'; @override String get mobileFeedbackButton => 'Commentaires'; @override - String get mobileOkButton => 'OK'; + String mobileGreeting(String param) { + return 'Bonjour $param'; + } @override - String get mobileSettingsHapticFeedback => 'Mode vibration'; + String get mobileGreetingWithoutName => 'Bonjour'; @override - String get mobileSettingsImmersiveMode => 'Mode plein écran'; + String get mobileHideVariation => 'Masquer les variantes'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Masquer l\'interface système durant la partie. À utiliser lorsque les gestes pour naviguer dans l\'interface système sur les bords de l\'écran vous gênent. S\'applique aux écrans de la partie et des problèmes (Puzzle Storm).'; + String get mobileHomeTab => 'Accueil'; @override - String get mobileNotFollowingAnyUser => 'Vous ne suivez aucun utilisateur.'; + String get mobileLiveStreamers => 'Diffuseurs en direct'; @override - String get mobileAllGames => 'Toutes les parties'; + String get mobileMustBeLoggedIn => 'Vous devez être connecté pour voir cette page.'; @override - String get mobileRecentSearches => 'Recherches récentes'; + String get mobileNoSearchResults => 'Aucun résultat'; @override - String get mobileClearButton => 'Effacer'; + String get mobileNotFollowingAnyUser => 'Vous ne suivez aucun utilisateur.'; + + @override + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,84 +67,82 @@ class AppLocalizationsFr extends AppLocalizations { } @override - String get mobileNoSearchResults => 'Aucun résultat'; + String get mobilePrefMagnifyDraggedPiece => 'Grossir la pièce déplacée'; @override - String get mobileAreYouSure => 'Êtes-vous sûr(e) ?'; + String get mobilePuzzleStormConfirmEndRun => 'Voulez-vous mettre fin à cette série?'; @override - String get mobilePuzzleStreakAbortWarning => 'Votre série actuelle (streak) prendra fin et votre résultat sera sauvegardé.'; + String get mobilePuzzleStormFilterNothingToShow => 'Rien à afficher. Veuillez changer les filtres.'; @override String get mobilePuzzleStormNothingToShow => 'Rien à afficher. Jouez quelques séries de problèmes (Puzzle Storm).'; @override - String get mobileSharePuzzle => 'Partager ce problème'; + String get mobilePuzzleStormSubtitle => 'Faites un maximum de problèmes en 3 minutes.'; @override - String get mobileShareGameURL => 'Partager l\'URL de la partie'; + String get mobilePuzzleStreakAbortWarning => 'Votre série actuelle (streak) prendra fin et votre résultat sera sauvegardé.'; @override - String get mobileShareGamePGN => 'Partager le PGN'; + String get mobilePuzzleThemesSubtitle => 'Faites des problèmes basés sur vos ouvertures préférées ou choisissez un thème.'; @override - String get mobileSharePositionAsFEN => 'Partager la position FEN'; + String get mobilePuzzlesTab => 'Problèmes'; @override - String get mobileShowVariations => 'Afficher les variantes'; + String get mobileRecentSearches => 'Recherches récentes'; @override - String get mobileHideVariation => 'Masquer les variantes'; + String get mobileSettingsHapticFeedback => 'Mode vibration'; @override - String get mobileShowComments => 'Afficher les commentaires'; + String get mobileSettingsImmersiveMode => 'Mode plein écran'; @override - String get mobilePuzzleStormConfirmEndRun => 'Voulez-vous mettre fin à cette série?'; + String get mobileSettingsImmersiveModeSubtitle => 'Masquer l\'interface système durant la partie. À utiliser lorsque les gestes pour naviguer dans l\'interface système sur les bords de l\'écran vous gênent. S\'applique aux écrans de la partie et des problèmes (Puzzle Storm).'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Rien à afficher. Veuillez changer les filtres.'; + String get mobileSettingsTab => 'Paramètres'; @override - String get mobileCancelTakebackOffer => 'Annuler la proposition de reprise du coup'; + String get mobileShareGamePGN => 'Partager le PGN'; @override - String get mobileWaitingForOpponentToJoin => 'En attente d\'un adversaire...'; + String get mobileShareGameURL => 'Partager l\'URL de la partie'; @override - String get mobileBlindfoldMode => 'Partie à l\'aveugle'; + String get mobileSharePositionAsFEN => 'Partager la position FEN'; @override - String get mobileLiveStreamers => 'Diffuseurs en direct'; + String get mobileSharePuzzle => 'Partager ce problème'; @override - String get mobileCustomGameJoinAGame => 'Joindre une partie'; + String get mobileShowComments => 'Afficher les commentaires'; @override - String get mobileCorrespondenceClearSavedMove => 'Effacer les coups enregistrés'; + String get mobileShowResult => 'Afficher le résultat'; @override - String get mobileSomethingWentWrong => 'Une erreur s\'est produite.'; + String get mobileShowVariations => 'Afficher les variantes'; @override - String get mobileShowResult => 'Afficher le résultat'; + String get mobileSomethingWentWrong => 'Une erreur s\'est produite.'; @override - String get mobilePuzzleThemesSubtitle => 'Faites des problèmes basés sur vos ouvertures préférées ou choisissez un thème.'; + String get mobileSystemColors => 'Couleurs du système'; @override - String get mobilePuzzleStormSubtitle => 'Faites un maximum de problèmes en 3 minutes.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Bonjour $param'; - } + String get mobileToolsTab => 'Outils'; @override - String get mobileGreetingWithoutName => 'Bonjour'; + String get mobileWaitingForOpponentToJoin => 'En attente d\'un adversaire...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Grossir la pièce déplacée'; + String get mobileWatchTab => 'Regarder'; @override String get activityActivity => 'Activité'; @@ -535,6 +538,9 @@ class AppLocalizationsFr extends AppLocalizations { @override String get broadcastStandings => 'Classement'; + @override + String get broadcastOfficialStandings => 'Official Standings'; + @override String broadcastIframeHelp(String param) { return 'Plus d\'options sur la $param'; @@ -565,6 +571,36 @@ class AppLocalizationsFr extends AppLocalizations { @override String get broadcastScore => 'Résultat'; + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + @override String broadcastNbBroadcasts(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -1992,9 +2028,6 @@ class AppLocalizationsFr extends AppLocalizations { @override String get byCPL => 'Par erreurs'; - @override - String get openStudy => 'Ouvrir l\'analyse'; - @override String get enable => 'Activée'; @@ -2662,9 +2695,6 @@ class AppLocalizationsFr extends AppLocalizations { @override String get unblock => 'Débloquer'; - @override - String get followsYou => 'Vous suit'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 a suivi $param2'; @@ -5424,6 +5454,11 @@ class AppLocalizationsFr extends AppLocalizations { @override String get studyYouCompletedThisLesson => 'Félicitations ! Vous avez terminé ce cours.'; + @override + String studyPerPage(String param) { + return '$param per page'; + } + @override String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( diff --git a/lib/l10n/l10n_ga.dart b/lib/l10n/l10n_ga.dart index 9bb52bbb8e..f6f9a3cf45 100644 --- a/lib/l10n/l10n_ga.dart +++ b/lib/l10n/l10n_ga.dart @@ -9,52 +9,57 @@ class AppLocalizationsGa extends AppLocalizations { AppLocalizationsGa([String locale = 'ga']) : super(locale); @override - String get mobileHomeTab => 'Home'; + String get mobileAllGames => 'All games'; @override - String get mobilePuzzlesTab => 'Puzzles'; + String get mobileAreYouSure => 'Are you sure?'; @override - String get mobileToolsTab => 'Tools'; + String get mobileBlindfoldMode => 'Blindfold'; @override - String get mobileWatchTab => 'Watch'; + String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @override - String get mobileSettingsTab => 'Settings'; + String get mobileClearButton => 'Clear'; @override - String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; + String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; @override - String get mobileSystemColors => 'System colors'; + String get mobileCustomGameJoinAGame => 'Join a game'; @override String get mobileFeedbackButton => 'Feedback'; @override - String get mobileOkButton => 'OK'; + String mobileGreeting(String param) { + return 'Hello, $param'; + } @override - String get mobileSettingsHapticFeedback => 'Haptic feedback'; + String get mobileGreetingWithoutName => 'Hello'; @override - String get mobileSettingsImmersiveMode => 'Immersive mode'; + String get mobileHideVariation => 'Hide variation'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileHomeTab => 'Home'; @override - String get mobileNotFollowingAnyUser => 'You are not following any user.'; + String get mobileLiveStreamers => 'Live streamers'; @override - String get mobileAllGames => 'All games'; + String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; @override - String get mobileRecentSearches => 'Recent searches'; + String get mobileNoSearchResults => 'No results'; @override - String get mobileClearButton => 'Clear'; + String get mobileNotFollowingAnyUser => 'You are not following any user.'; + + @override + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,84 +67,82 @@ class AppLocalizationsGa extends AppLocalizations { } @override - String get mobileNoSearchResults => 'No results'; + String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; @override - String get mobileAreYouSure => 'Are you sure?'; + String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; @override String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Share this puzzle'; + String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; @override - String get mobileShareGameURL => 'Share game URL'; + String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; @override - String get mobileShareGamePGN => 'Share PGN'; + String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; @override - String get mobileSharePositionAsFEN => 'Share position as FEN'; + String get mobilePuzzlesTab => 'Puzzles'; @override - String get mobileShowVariations => 'Show variations'; + String get mobileRecentSearches => 'Recent searches'; @override - String get mobileHideVariation => 'Hide variation'; + String get mobileSettingsHapticFeedback => 'Haptic feedback'; @override - String get mobileShowComments => 'Show comments'; + String get mobileSettingsImmersiveMode => 'Immersive mode'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobileSettingsTab => 'Settings'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; + String get mobileShareGamePGN => 'Share PGN'; @override - String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; + String get mobileShareGameURL => 'Share game URL'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get mobileSharePositionAsFEN => 'Share position as FEN'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileSharePuzzle => 'Share this puzzle'; @override - String get mobileCustomGameJoinAGame => 'Join a game'; + String get mobileShowComments => 'Show comments'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; + String get mobileShowResult => 'Show result'; @override - String get mobileSomethingWentWrong => 'Something went wrong.'; + String get mobileShowVariations => 'Show variations'; @override - String get mobileShowResult => 'Show result'; + String get mobileSomethingWentWrong => 'Something went wrong.'; @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobileSystemColors => 'System colors'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Hello, $param'; - } + String get mobileToolsTab => 'Tools'; @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Watch'; @override String get activityActivity => 'Gníomhaíocht'; @@ -586,6 +589,9 @@ class AppLocalizationsGa extends AppLocalizations { @override String get broadcastStandings => 'Standings'; + @override + String get broadcastOfficialStandings => 'Official Standings'; + @override String broadcastIframeHelp(String param) { return 'More options on the $param'; @@ -616,6 +622,36 @@ class AppLocalizationsGa extends AppLocalizations { @override String get broadcastScore => 'Score'; + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + @override String broadcastNbBroadcasts(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -2058,9 +2094,6 @@ class AppLocalizationsGa extends AppLocalizations { @override String get byCPL => 'De réir CPL'; - @override - String get openStudy => 'Oscail staidéar'; - @override String get enable => 'Cumasaigh'; @@ -2728,9 +2761,6 @@ class AppLocalizationsGa extends AppLocalizations { @override String get unblock => 'Bain bac de'; - @override - String get followsYou => 'Do leanúint'; - @override String xStartedFollowingY(String param1, String param2) { return 'Thosaigh $param1 ag leanúint $param2'; @@ -5616,6 +5646,11 @@ class AppLocalizationsGa extends AppLocalizations { @override String get studyYouCompletedThisLesson => 'Comhghairdeas! Chríochnaigh tú an ceacht seo.'; + @override + String studyPerPage(String param) { + return '$param per page'; + } + @override String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( diff --git a/lib/l10n/l10n_gl.dart b/lib/l10n/l10n_gl.dart index a35f0b7875..09249134de 100644 --- a/lib/l10n/l10n_gl.dart +++ b/lib/l10n/l10n_gl.dart @@ -9,52 +9,57 @@ class AppLocalizationsGl extends AppLocalizations { AppLocalizationsGl([String locale = 'gl']) : super(locale); @override - String get mobileHomeTab => 'Inicio'; + String get mobileAllGames => 'Todas as partidas'; @override - String get mobilePuzzlesTab => 'Problemas'; + String get mobileAreYouSure => 'Estás seguro?'; @override - String get mobileToolsTab => 'Ferramentas'; + String get mobileBlindfoldMode => 'Á cega'; @override - String get mobileWatchTab => 'Ver'; + String get mobileCancelTakebackOffer => 'Cancelar a proposta de cambio'; @override - String get mobileSettingsTab => 'Axustes'; + String get mobileClearButton => 'Borrar'; @override - String get mobileMustBeLoggedIn => 'Debes iniciar sesión para ver esta páxina.'; + String get mobileCorrespondenceClearSavedMove => 'Borrar a xogada gardada'; @override - String get mobileSystemColors => 'Cores do sistema'; + String get mobileCustomGameJoinAGame => 'Unirse a unha partida'; @override String get mobileFeedbackButton => 'Comentarios'; @override - String get mobileOkButton => 'OK'; + String mobileGreeting(String param) { + return 'Ola, $param'; + } @override - String get mobileSettingsHapticFeedback => 'Vibración ó mover'; + String get mobileGreetingWithoutName => 'Ola'; @override - String get mobileSettingsImmersiveMode => 'Pantalla completa'; + String get mobileHideVariation => 'Ocultar variantes'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Oculta a Interface de Usuario mentres xogas. Emprega esta opción se che molestan os xestos de navegación do sistema ós bordos da pantalla. Aplícase ás pantallas da partida e á de Puzzle Storm.'; + String get mobileHomeTab => 'Inicio'; @override - String get mobileNotFollowingAnyUser => 'Non estás a seguir a ningún usuario.'; + String get mobileLiveStreamers => 'Presentadores en directo'; @override - String get mobileAllGames => 'Todas as partidas'; + String get mobileMustBeLoggedIn => 'Debes iniciar sesión para ver esta páxina.'; @override - String get mobileRecentSearches => 'Procuras recentes'; + String get mobileNoSearchResults => 'Sen resultados'; @override - String get mobileClearButton => 'Borrar'; + String get mobileNotFollowingAnyUser => 'Non estás a seguir a ningún usuario.'; + + @override + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,84 +67,82 @@ class AppLocalizationsGl extends AppLocalizations { } @override - String get mobileNoSearchResults => 'Sen resultados'; + String get mobilePrefMagnifyDraggedPiece => 'Ampliar a peza arrastrada'; @override - String get mobileAreYouSure => 'Estás seguro?'; + String get mobilePuzzleStormConfirmEndRun => 'Queres rematar esta quenda?'; @override - String get mobilePuzzleStreakAbortWarning => 'Perderás a túa secuencia actual e o teu resultado gardarase.'; + String get mobilePuzzleStormFilterNothingToShow => 'Non aparece nada. Por favor, cambia os filtros'; @override String get mobilePuzzleStormNothingToShow => 'Non hai nada que amosar. Primeiro xoga algunha quenda de Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Compartir este crebacabezas'; + String get mobilePuzzleStormSubtitle => 'Resolve tantos crebacabezas como sexa posible en 3 minutos.'; @override - String get mobileShareGameURL => 'Compartir a URL da partida'; + String get mobilePuzzleStreakAbortWarning => 'Perderás a túa secuencia actual e o teu resultado gardarase.'; @override - String get mobileShareGamePGN => 'Compartir PGN'; + String get mobilePuzzleThemesSubtitle => 'Resolve crebacabezas das túas aperturas favoritas ou elixe un tema.'; @override - String get mobileSharePositionAsFEN => 'Compartir a posición coma FEN'; + String get mobilePuzzlesTab => 'Problemas'; @override - String get mobileShowVariations => 'Amosar variantes'; + String get mobileRecentSearches => 'Procuras recentes'; @override - String get mobileHideVariation => 'Ocultar variantes'; + String get mobileSettingsHapticFeedback => 'Vibración ó mover'; @override - String get mobileShowComments => 'Amosar comentarios'; + String get mobileSettingsImmersiveMode => 'Pantalla completa'; @override - String get mobilePuzzleStormConfirmEndRun => 'Queres rematar esta quenda?'; + String get mobileSettingsImmersiveModeSubtitle => 'Oculta a Interface de Usuario mentres xogas. Emprega esta opción se che molestan os xestos de navegación do sistema ós bordos da pantalla. Aplícase ás pantallas da partida e á de Puzzle Storm.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Non aparece nada. Por favor, cambia os filtros'; + String get mobileSettingsTab => 'Axustes'; @override - String get mobileCancelTakebackOffer => 'Cancelar a proposta de cambio'; + String get mobileShareGamePGN => 'Compartir PGN'; @override - String get mobileWaitingForOpponentToJoin => 'Agardando un rival...'; + String get mobileShareGameURL => 'Compartir a URL da partida'; @override - String get mobileBlindfoldMode => 'Á cega'; + String get mobileSharePositionAsFEN => 'Compartir a posición coma FEN'; @override - String get mobileLiveStreamers => 'Presentadores en directo'; + String get mobileSharePuzzle => 'Compartir este crebacabezas'; @override - String get mobileCustomGameJoinAGame => 'Unirse a unha partida'; + String get mobileShowComments => 'Amosar comentarios'; @override - String get mobileCorrespondenceClearSavedMove => 'Borrar a xogada gardada'; + String get mobileShowResult => 'Amosar o resultado'; @override - String get mobileSomethingWentWrong => 'Algo foi mal.'; + String get mobileShowVariations => 'Amosar variantes'; @override - String get mobileShowResult => 'Amosar o resultado'; + String get mobileSomethingWentWrong => 'Algo foi mal.'; @override - String get mobilePuzzleThemesSubtitle => 'Resolve crebacabezas das túas aperturas favoritas ou elixe un tema.'; + String get mobileSystemColors => 'Cores do sistema'; @override - String get mobilePuzzleStormSubtitle => 'Resolve tantos crebacabezas como sexa posible en 3 minutos.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Ola, $param'; - } + String get mobileToolsTab => 'Ferramentas'; @override - String get mobileGreetingWithoutName => 'Ola'; + String get mobileWaitingForOpponentToJoin => 'Agardando un rival...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Ampliar a peza arrastrada'; + String get mobileWatchTab => 'Ver'; @override String get activityActivity => 'Actividade'; @@ -535,6 +538,9 @@ class AppLocalizationsGl extends AppLocalizations { @override String get broadcastStandings => 'Clasificación'; + @override + String get broadcastOfficialStandings => 'Official Standings'; + @override String broadcastIframeHelp(String param) { return 'Máis opcións na $param'; @@ -565,6 +571,36 @@ class AppLocalizationsGl extends AppLocalizations { @override String get broadcastScore => 'Resultado'; + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + @override String broadcastNbBroadcasts(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -1992,9 +2028,6 @@ class AppLocalizationsGl extends AppLocalizations { @override String get byCPL => 'Por PCP'; - @override - String get openStudy => 'Abrir estudo'; - @override String get enable => 'Activar'; @@ -2662,9 +2695,6 @@ class AppLocalizationsGl extends AppLocalizations { @override String get unblock => 'Desbloquear'; - @override - String get followsYou => 'Séguete'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 comezou a seguir a $param2'; @@ -5424,6 +5454,11 @@ class AppLocalizationsGl extends AppLocalizations { @override String get studyYouCompletedThisLesson => 'Parabéns! Completaches esta lección.'; + @override + String studyPerPage(String param) { + return '$param per page'; + } + @override String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( diff --git a/lib/l10n/l10n_gsw.dart b/lib/l10n/l10n_gsw.dart index 76a82a010b..bb9a271163 100644 --- a/lib/l10n/l10n_gsw.dart +++ b/lib/l10n/l10n_gsw.dart @@ -9,52 +9,57 @@ class AppLocalizationsGsw extends AppLocalizations { AppLocalizationsGsw([String locale = 'gsw']) : super(locale); @override - String get mobileHomeTab => 'Afangssite'; + String get mobileAllGames => 'All Schpiel'; @override - String get mobilePuzzlesTab => 'Ufgabe'; + String get mobileAreYouSure => 'Bisch sicher?'; @override - String get mobileToolsTab => 'Werchzüg'; + String get mobileBlindfoldMode => 'Blind schpille'; @override - String get mobileWatchTab => 'Luege'; + String get mobileCancelTakebackOffer => 'Zugsrücknam-Offerte zruggzieh'; @override - String get mobileSettingsTab => 'Ischtelle'; + String get mobileClearButton => 'Leere'; @override - String get mobileMustBeLoggedIn => 'Muesch iglogt si, zum die Site z\'gseh.'; + String get mobileCorrespondenceClearSavedMove => 'Lösch die gschpeicherete Züg'; @override - String get mobileSystemColors => 'Syschtem-Farbe'; + String get mobileCustomGameJoinAGame => 'Es Schpiel mitschpille'; @override String get mobileFeedbackButton => 'Rückmäldig'; @override - String get mobileOkButton => 'OK'; + String mobileGreeting(String param) { + return 'Hoi, $param'; + } @override - String get mobileSettingsHapticFeedback => 'Rückmäldig mit Vibration'; + String get mobileGreetingWithoutName => 'Hoi'; @override - String get mobileSettingsImmersiveMode => 'Ibettete Modus'; + String get mobileHideVariation => 'Variante verberge'; @override - String get mobileSettingsImmersiveModeSubtitle => 'UI-Syschtem während em schpille usblände. Benutz die Option, wänn dich d\'Navigationsgeschte, vum Sysychtem, am Bildschirmrand störed. Das gilt für Schpiel- und Puzzle Storm-Bildschirm.'; + String get mobileHomeTab => 'Afangssite'; @override - String get mobileNotFollowingAnyUser => 'Du folgsch keim Schpiller.'; + String get mobileLiveStreamers => 'Live Streamer'; @override - String get mobileAllGames => 'All Schpiel'; + String get mobileMustBeLoggedIn => 'Muesch iglogt si, zum die Site z\'gseh.'; @override - String get mobileRecentSearches => 'Kürzlich Gsuechts'; + String get mobileNoSearchResults => 'Nüt g\'funde'; @override - String get mobileClearButton => 'Leere'; + String get mobileNotFollowingAnyUser => 'Du folgsch keim Schpiller.'; + + @override + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,84 +67,82 @@ class AppLocalizationsGsw extends AppLocalizations { } @override - String get mobileNoSearchResults => 'Nüt g\'funde'; + String get mobilePrefMagnifyDraggedPiece => 'Vegrösserig vu de zogene Figur'; @override - String get mobileAreYouSure => 'Bisch sicher?'; + String get mobilePuzzleStormConfirmEndRun => 'Wottsch de Lauf beände?'; @override - String get mobilePuzzleStreakAbortWarning => 'Du verlürsch din aktuelle Lauf und din Rekord wird g\'schpeicheret.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nüt zum Zeige, bitte d\'Filter ändere'; @override String get mobilePuzzleStormNothingToShow => 'Es git nüt zum Zeige. Schpill zerscht ochli Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Teil die Ufgab'; + String get mobilePuzzleStormSubtitle => 'Lös i 3 Minute so vill Ufgabe wie möglich.'; @override - String get mobileShareGameURL => 'Teil d\'Schpiel-URL'; + String get mobilePuzzleStreakAbortWarning => 'Du verlürsch din aktuelle Lauf und din Rekord wird g\'schpeicheret.'; @override - String get mobileShareGamePGN => 'Teil s\'PGN'; + String get mobilePuzzleThemesSubtitle => 'Schpill Ufgabe mit dine Lieblings-Eröffnige oder wähl es Thema.'; @override - String get mobileSharePositionAsFEN => 'Teil d\'Position als FEN'; + String get mobilePuzzlesTab => 'Ufgabe'; @override - String get mobileShowVariations => 'Zeig Variante'; + String get mobileRecentSearches => 'Kürzlich Gsuechts'; @override - String get mobileHideVariation => 'Variante verberge'; + String get mobileSettingsHapticFeedback => 'Rückmäldig mit Vibration'; @override - String get mobileShowComments => 'Zeig Kommentär'; + String get mobileSettingsImmersiveMode => 'Ibettete Modus'; @override - String get mobilePuzzleStormConfirmEndRun => 'Wottsch de Lauf beände?'; + String get mobileSettingsImmersiveModeSubtitle => 'UI-Syschtem während em schpille usblände. Benutz die Option, wänn dich d\'Navigationsgeschte, vum Sysychtem, am Bildschirmrand störed. Das gilt für Schpiel- und Puzzle Storm-Bildschirm.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nüt zum Zeige, bitte d\'Filter ändere'; + String get mobileSettingsTab => 'Ischtelle'; @override - String get mobileCancelTakebackOffer => 'Zugsrücknam-Offerte zruggzieh'; + String get mobileShareGamePGN => 'Teil s\'PGN'; @override - String get mobileWaitingForOpponentToJoin => 'Warte bis en Gegner erschint...'; + String get mobileShareGameURL => 'Teil d\'Schpiel-URL'; @override - String get mobileBlindfoldMode => 'Blind schpille'; + String get mobileSharePositionAsFEN => 'Teil d\'Position als FEN'; @override - String get mobileLiveStreamers => 'Live Streamer'; + String get mobileSharePuzzle => 'Teil die Ufgab'; @override - String get mobileCustomGameJoinAGame => 'Es Schpiel mitschpille'; + String get mobileShowComments => 'Zeig Kommentär'; @override - String get mobileCorrespondenceClearSavedMove => 'Lösch die gschpeicherete Züg'; + String get mobileShowResult => 'Resultat zeige'; @override - String get mobileSomethingWentWrong => 'Es isch öppis schief gange.'; + String get mobileShowVariations => 'Zeig Variante'; @override - String get mobileShowResult => 'Resultat zeige'; + String get mobileSomethingWentWrong => 'Es isch öppis schief gange.'; @override - String get mobilePuzzleThemesSubtitle => 'Schpill Ufgabe mit dine Lieblings-Eröffnige oder wähl es Thema.'; + String get mobileSystemColors => 'Syschtem-Farbe'; @override - String get mobilePuzzleStormSubtitle => 'Lös i 3 Minute so vill Ufgabe wie möglich.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Hoi, $param'; - } + String get mobileToolsTab => 'Werchzüg'; @override - String get mobileGreetingWithoutName => 'Hoi'; + String get mobileWaitingForOpponentToJoin => 'Warte bis en Gegner erschint...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Vegrösserig vu de zogene Figur'; + String get mobileWatchTab => 'Luege'; @override String get activityActivity => 'Aktivitäte'; @@ -535,6 +538,9 @@ class AppLocalizationsGsw extends AppLocalizations { @override String get broadcastStandings => 'Tabälle'; + @override + String get broadcastOfficialStandings => 'Official Standings'; + @override String broadcastIframeHelp(String param) { return 'Meh Optionen uf $param'; @@ -565,6 +571,36 @@ class AppLocalizationsGsw extends AppLocalizations { @override String get broadcastScore => 'Resultat'; + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + @override String broadcastNbBroadcasts(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -1992,9 +2028,6 @@ class AppLocalizationsGsw extends AppLocalizations { @override String get byCPL => 'Nach CPL'; - @override - String get openStudy => 'Schtudie eröffne'; - @override String get enable => 'Ischalte'; @@ -2662,9 +2695,6 @@ class AppLocalizationsGsw extends AppLocalizations { @override String get unblock => 'Blockierig ufhebe'; - @override - String get followsYou => 'Folgt dir'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 folgt jetzt $param2'; @@ -5424,6 +5454,11 @@ class AppLocalizationsGsw extends AppLocalizations { @override String get studyYouCompletedThisLesson => 'Gratulation! Du häsch die Lektion abgschlosse.'; + @override + String studyPerPage(String param) { + return '$param per page'; + } + @override String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( diff --git a/lib/l10n/l10n_he.dart b/lib/l10n/l10n_he.dart index 86118c87c4..1b849eca5c 100644 --- a/lib/l10n/l10n_he.dart +++ b/lib/l10n/l10n_he.dart @@ -9,52 +9,57 @@ class AppLocalizationsHe extends AppLocalizations { AppLocalizationsHe([String locale = 'he']) : super(locale); @override - String get mobileHomeTab => 'בית'; + String get mobileAllGames => 'כל המשחקים'; @override - String get mobilePuzzlesTab => 'חידות'; + String get mobileAreYouSure => 'בטוח?'; @override - String get mobileToolsTab => 'כלים'; + String get mobileBlindfoldMode => 'משחק עיוור'; @override - String get mobileWatchTab => 'צפייה'; + String get mobileCancelTakebackOffer => 'ביטול ההצעה להחזיר את המהלך האחרון'; @override - String get mobileSettingsTab => 'הגדרות'; + String get mobileClearButton => 'ניקוי'; @override - String get mobileMustBeLoggedIn => 'יש להתחבר כדי לצפות בדף זה.'; + String get mobileCorrespondenceClearSavedMove => 'ניקוי המהלך השמור'; @override - String get mobileSystemColors => 'צבעי מערכת ההפעלה'; + String get mobileCustomGameJoinAGame => 'הצטרפות למשחק'; @override String get mobileFeedbackButton => 'משוב'; @override - String get mobileOkButton => 'בסדר'; + String mobileGreeting(String param) { + return 'שלום, $param'; + } @override - String get mobileSettingsHapticFeedback => 'רטט בכל מהלך'; + String get mobileGreetingWithoutName => 'שלום'; @override - String get mobileSettingsImmersiveMode => 'מצב ריכוז'; + String get mobileHideVariation => 'הסתרת וריאציות'; @override - String get mobileSettingsImmersiveModeSubtitle => 'הסתירו את שאר הממשק במהלך המשחק. מומלץ להפעיל הגדרה זו אם אפשרויות הניווט בקצות הלוח מפריעות לכם לשחק. רלוונטי למשחקים ול־Puzzle Storm.'; + String get mobileHomeTab => 'בית'; @override - String get mobileNotFollowingAnyUser => 'אינכם עוקבים אחר אף אחד.'; + String get mobileLiveStreamers => 'שדרנים בשידור חי'; @override - String get mobileAllGames => 'כל המשחקים'; + String get mobileMustBeLoggedIn => 'יש להתחבר כדי לצפות בדף זה.'; @override - String get mobileRecentSearches => 'חיפושים אחרונים'; + String get mobileNoSearchResults => 'אין תוצאות'; @override - String get mobileClearButton => 'ניקוי'; + String get mobileNotFollowingAnyUser => 'אינכם עוקבים אחר אף אחד.'; + + @override + String get mobileOkButton => 'בסדר'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,84 +67,82 @@ class AppLocalizationsHe extends AppLocalizations { } @override - String get mobileNoSearchResults => 'אין תוצאות'; + String get mobilePrefMagnifyDraggedPiece => 'הגדלת הכלי הנגרר'; @override - String get mobileAreYouSure => 'בטוח?'; + String get mobilePuzzleStormConfirmEndRun => 'האם לסיים את הסבב?'; @override - String get mobilePuzzleStreakAbortWarning => 'הרצף הנוכחי שלך ייאבד אך הניקוד יישמר.'; + String get mobilePuzzleStormFilterNothingToShow => 'אין מה להראות. ניתן לשנות את חתכי הסינון'; @override String get mobilePuzzleStormNothingToShow => 'אין מה להראות. שחקו כמה סיבובים של Puzzle Storm קודם.'; @override - String get mobileSharePuzzle => 'שיתוף החידה'; + String get mobilePuzzleStormSubtitle => 'פתרו כמה שיותר חידות ב־3 דקות.'; @override - String get mobileShareGameURL => 'שיתוף הקישור למשחק'; + String get mobilePuzzleStreakAbortWarning => 'הרצף הנוכחי שלך ייאבד אך הניקוד יישמר.'; @override - String get mobileShareGamePGN => 'שיתוף ה־PGN'; + String get mobilePuzzleThemesSubtitle => 'פתרו חידות עם הפתיחות האהובות עליכם או בחרו ממגוון נושאים.'; @override - String get mobileSharePositionAsFEN => 'שיתוף העמדה כ־FEN'; + String get mobilePuzzlesTab => 'חידות'; @override - String get mobileShowVariations => 'הצגת וריאציות'; + String get mobileRecentSearches => 'חיפושים אחרונים'; @override - String get mobileHideVariation => 'הסתרת וריאציות'; + String get mobileSettingsHapticFeedback => 'רטט בכל מהלך'; @override - String get mobileShowComments => 'הצגת הערות'; + String get mobileSettingsImmersiveMode => 'מצב ריכוז'; @override - String get mobilePuzzleStormConfirmEndRun => 'האם לסיים את הסבב?'; + String get mobileSettingsImmersiveModeSubtitle => 'הסתירו את שאר הממשק במהלך המשחק. מומלץ להפעיל הגדרה זו אם אפשרויות הניווט בקצות הלוח מפריעות לכם לשחק. רלוונטי למשחקים ול־Puzzle Storm.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'אין מה להראות. ניתן לשנות את חתכי הסינון'; + String get mobileSettingsTab => 'הגדרות'; @override - String get mobileCancelTakebackOffer => 'ביטול ההצעה להחזיר את המהלך האחרון'; + String get mobileShareGamePGN => 'שיתוף ה־PGN'; @override - String get mobileWaitingForOpponentToJoin => 'ממתין שיריב יצטרף...'; + String get mobileShareGameURL => 'שיתוף הקישור למשחק'; @override - String get mobileBlindfoldMode => 'משחק עיוור'; + String get mobileSharePositionAsFEN => 'שיתוף העמדה כ־FEN'; @override - String get mobileLiveStreamers => 'שדרנים בשידור חי'; + String get mobileSharePuzzle => 'שיתוף החידה'; @override - String get mobileCustomGameJoinAGame => 'הצטרפות למשחק'; + String get mobileShowComments => 'הצגת הערות'; @override - String get mobileCorrespondenceClearSavedMove => 'ניקוי המהלך השמור'; + String get mobileShowResult => 'הצגת תוצאת המשחק'; @override - String get mobileSomethingWentWrong => 'משהו השתבש.'; + String get mobileShowVariations => 'הצגת וריאציות'; @override - String get mobileShowResult => 'הצגת תוצאת המשחק'; + String get mobileSomethingWentWrong => 'משהו השתבש.'; @override - String get mobilePuzzleThemesSubtitle => 'פתרו חידות עם הפתיחות האהובות עליכם או בחרו ממגוון נושאים.'; + String get mobileSystemColors => 'צבעי מערכת ההפעלה'; @override - String get mobilePuzzleStormSubtitle => 'פתרו כמה שיותר חידות ב־3 דקות.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'שלום, $param'; - } + String get mobileToolsTab => 'כלים'; @override - String get mobileGreetingWithoutName => 'שלום'; + String get mobileWaitingForOpponentToJoin => 'ממתין שיריב יצטרף...'; @override - String get mobilePrefMagnifyDraggedPiece => 'הגדלת הכלי הנגרר'; + String get mobileWatchTab => 'צפייה'; @override String get activityActivity => 'פעילות'; @@ -571,6 +574,9 @@ class AppLocalizationsHe extends AppLocalizations { @override String get broadcastStandings => 'תוצאות'; + @override + String get broadcastOfficialStandings => 'Official Standings'; + @override String broadcastIframeHelp(String param) { return 'ישנן אפשרויות נוספות ב$param'; @@ -601,6 +607,36 @@ class AppLocalizationsHe extends AppLocalizations { @override String get broadcastScore => 'ניקוד'; + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + @override String broadcastNbBroadcasts(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -2040,9 +2076,6 @@ class AppLocalizationsHe extends AppLocalizations { @override String get byCPL => 'עפ\"י CPL'; - @override - String get openStudy => 'פתח לוח למידה'; - @override String get enable => 'הפעלה'; @@ -2710,9 +2743,6 @@ class AppLocalizationsHe extends AppLocalizations { @override String get unblock => 'בטל חסימה'; - @override - String get followsYou => 'עוקב/ת אחריך'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 התחיל/ה לעקוב אחרי $param2'; @@ -5560,6 +5590,11 @@ class AppLocalizationsHe extends AppLocalizations { @override String get studyYouCompletedThisLesson => 'מזל טוב! סיימתם את השיעור.'; + @override + String studyPerPage(String param) { + return '$param per page'; + } + @override String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( diff --git a/lib/l10n/l10n_hi.dart b/lib/l10n/l10n_hi.dart index f058ddd5e7..376fd6813d 100644 --- a/lib/l10n/l10n_hi.dart +++ b/lib/l10n/l10n_hi.dart @@ -9,52 +9,57 @@ class AppLocalizationsHi extends AppLocalizations { AppLocalizationsHi([String locale = 'hi']) : super(locale); @override - String get mobileHomeTab => 'होम'; + String get mobileAllGames => 'सारे गेम्स'; @override - String get mobilePuzzlesTab => 'पज़ल'; + String get mobileAreYouSure => 'क्या आप सुनिश्चित हैं?'; @override - String get mobileToolsTab => 'टूल्स'; + String get mobileBlindfoldMode => 'Blindfold'; @override - String get mobileWatchTab => 'देखें'; + String get mobileCancelTakebackOffer => 'Takeback प्रस्ताव रद्द करें'; @override - String get mobileSettingsTab => 'सेटिंग'; + String get mobileClearButton => 'Clear'; @override - String get mobileMustBeLoggedIn => 'इस पेज को देखने के लिए आपको login करना होगा'; + String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; @override - String get mobileSystemColors => 'System colors'; + String get mobileCustomGameJoinAGame => 'Join a game'; @override String get mobileFeedbackButton => 'फीडबैक'; @override - String get mobileOkButton => 'ओके'; + String mobileGreeting(String param) { + return 'Hello, $param'; + } @override - String get mobileSettingsHapticFeedback => 'कंपन फीडबैक'; + String get mobileGreetingWithoutName => 'Hello'; @override - String get mobileSettingsImmersiveMode => 'इमर्सिव मोड'; + String get mobileHideVariation => 'वेरिएशन छुपाए'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileHomeTab => 'होम'; @override - String get mobileNotFollowingAnyUser => 'You are not following any user.'; + String get mobileLiveStreamers => 'लाइव स्ट्रीमर्स'; @override - String get mobileAllGames => 'सारे गेम्स'; + String get mobileMustBeLoggedIn => 'इस पेज को देखने के लिए आपको login करना होगा'; @override - String get mobileRecentSearches => 'Recent searches'; + String get mobileNoSearchResults => 'कोई परिणाम नहीं'; @override - String get mobileClearButton => 'Clear'; + String get mobileNotFollowingAnyUser => 'You are not following any user.'; + + @override + String get mobileOkButton => 'ओके'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,84 +67,82 @@ class AppLocalizationsHi extends AppLocalizations { } @override - String get mobileNoSearchResults => 'कोई परिणाम नहीं'; + String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; @override - String get mobileAreYouSure => 'क्या आप सुनिश्चित हैं?'; + String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; @override String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; @override - String get mobileSharePuzzle => 'पज़ल शरीर करें'; + String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; @override - String get mobileShareGameURL => 'गेम URL शेयर करें'; + String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; @override - String get mobileShareGamePGN => 'PGN शेयर करें'; + String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; @override - String get mobileSharePositionAsFEN => 'पोजीशन FEN के रूप में शेयर करें'; + String get mobilePuzzlesTab => 'पज़ल'; @override - String get mobileShowVariations => 'वेरिएशन देखें'; + String get mobileRecentSearches => 'Recent searches'; @override - String get mobileHideVariation => 'वेरिएशन छुपाए'; + String get mobileSettingsHapticFeedback => 'कंपन फीडबैक'; @override - String get mobileShowComments => 'कमेंट्स देखें'; + String get mobileSettingsImmersiveMode => 'इमर्सिव मोड'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobileSettingsTab => 'सेटिंग'; @override - String get mobileCancelTakebackOffer => 'Takeback प्रस्ताव रद्द करें'; + String get mobileShareGamePGN => 'PGN शेयर करें'; @override - String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; + String get mobileShareGameURL => 'गेम URL शेयर करें'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get mobileSharePositionAsFEN => 'पोजीशन FEN के रूप में शेयर करें'; @override - String get mobileLiveStreamers => 'लाइव स्ट्रीमर्स'; + String get mobileSharePuzzle => 'पज़ल शरीर करें'; @override - String get mobileCustomGameJoinAGame => 'Join a game'; + String get mobileShowComments => 'कमेंट्स देखें'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; + String get mobileShowResult => 'Show result'; @override - String get mobileSomethingWentWrong => 'Something went wrong.'; + String get mobileShowVariations => 'वेरिएशन देखें'; @override - String get mobileShowResult => 'Show result'; + String get mobileSomethingWentWrong => 'Something went wrong.'; @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobileSystemColors => 'System colors'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Hello, $param'; - } + String get mobileToolsTab => 'टूल्स'; @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'देखें'; @override String get activityActivity => 'कार्यकलाप'; @@ -535,6 +538,9 @@ class AppLocalizationsHi extends AppLocalizations { @override String get broadcastStandings => 'Standings'; + @override + String get broadcastOfficialStandings => 'Official Standings'; + @override String broadcastIframeHelp(String param) { return 'More options on the $param'; @@ -565,6 +571,36 @@ class AppLocalizationsHi extends AppLocalizations { @override String get broadcastScore => 'Score'; + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + @override String broadcastNbBroadcasts(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -1990,9 +2026,6 @@ class AppLocalizationsHi extends AppLocalizations { @override String get byCPL => 'CPL द्वारा'; - @override - String get openStudy => 'अध्ययन खोलो'; - @override String get enable => 'सक्षम करें'; @@ -2660,9 +2693,6 @@ class AppLocalizationsHi extends AppLocalizations { @override String get unblock => 'अवस्र्द्ध (ब्लॉक) न करें'; - @override - String get followsYou => 'आपका अनुसरण कर रहे हैं'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 ने $param2 का अनुसरण करना शुरू किया'; @@ -5422,6 +5452,11 @@ class AppLocalizationsHi extends AppLocalizations { @override String get studyYouCompletedThisLesson => 'बधाई हो! आपने यह सबक पूरा कर लिया है।'; + @override + String studyPerPage(String param) { + return '$param per page'; + } + @override String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( diff --git a/lib/l10n/l10n_hr.dart b/lib/l10n/l10n_hr.dart index a87cd54149..202c5f6e53 100644 --- a/lib/l10n/l10n_hr.dart +++ b/lib/l10n/l10n_hr.dart @@ -9,52 +9,57 @@ class AppLocalizationsHr extends AppLocalizations { AppLocalizationsHr([String locale = 'hr']) : super(locale); @override - String get mobileHomeTab => 'Home'; + String get mobileAllGames => 'All games'; @override - String get mobilePuzzlesTab => 'Puzzles'; + String get mobileAreYouSure => 'Are you sure?'; @override - String get mobileToolsTab => 'Tools'; + String get mobileBlindfoldMode => 'Blindfold'; @override - String get mobileWatchTab => 'Watch'; + String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @override - String get mobileSettingsTab => 'Settings'; + String get mobileClearButton => 'Clear'; @override - String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; + String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; @override - String get mobileSystemColors => 'System colors'; + String get mobileCustomGameJoinAGame => 'Join a game'; @override String get mobileFeedbackButton => 'Feedback'; @override - String get mobileOkButton => 'OK'; + String mobileGreeting(String param) { + return 'Hello, $param'; + } @override - String get mobileSettingsHapticFeedback => 'Haptic feedback'; + String get mobileGreetingWithoutName => 'Hello'; @override - String get mobileSettingsImmersiveMode => 'Immersive mode'; + String get mobileHideVariation => 'Hide variation'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileHomeTab => 'Home'; @override - String get mobileNotFollowingAnyUser => 'You are not following any user.'; + String get mobileLiveStreamers => 'Live streamers'; @override - String get mobileAllGames => 'All games'; + String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; @override - String get mobileRecentSearches => 'Recent searches'; + String get mobileNoSearchResults => 'No results'; @override - String get mobileClearButton => 'Clear'; + String get mobileNotFollowingAnyUser => 'You are not following any user.'; + + @override + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,84 +67,82 @@ class AppLocalizationsHr extends AppLocalizations { } @override - String get mobileNoSearchResults => 'No results'; + String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; @override - String get mobileAreYouSure => 'Are you sure?'; + String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; @override String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Share this puzzle'; + String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; @override - String get mobileShareGameURL => 'Share game URL'; + String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; @override - String get mobileShareGamePGN => 'Share PGN'; + String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; @override - String get mobileSharePositionAsFEN => 'Share position as FEN'; + String get mobilePuzzlesTab => 'Puzzles'; @override - String get mobileShowVariations => 'Show variations'; + String get mobileRecentSearches => 'Recent searches'; @override - String get mobileHideVariation => 'Hide variation'; + String get mobileSettingsHapticFeedback => 'Haptic feedback'; @override - String get mobileShowComments => 'Show comments'; + String get mobileSettingsImmersiveMode => 'Immersive mode'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobileSettingsTab => 'Settings'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; + String get mobileShareGamePGN => 'Share PGN'; @override - String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; + String get mobileShareGameURL => 'Share game URL'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get mobileSharePositionAsFEN => 'Share position as FEN'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileSharePuzzle => 'Share this puzzle'; @override - String get mobileCustomGameJoinAGame => 'Join a game'; + String get mobileShowComments => 'Show comments'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; + String get mobileShowResult => 'Show result'; @override - String get mobileSomethingWentWrong => 'Something went wrong.'; + String get mobileShowVariations => 'Show variations'; @override - String get mobileShowResult => 'Show result'; + String get mobileSomethingWentWrong => 'Something went wrong.'; @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobileSystemColors => 'System colors'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Hello, $param'; - } + String get mobileToolsTab => 'Tools'; @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Watch'; @override String get activityActivity => 'Aktivnost'; @@ -552,6 +555,9 @@ class AppLocalizationsHr extends AppLocalizations { @override String get broadcastStandings => 'Standings'; + @override + String get broadcastOfficialStandings => 'Official Standings'; + @override String broadcastIframeHelp(String param) { return 'More options on the $param'; @@ -582,6 +588,36 @@ class AppLocalizationsHr extends AppLocalizations { @override String get broadcastScore => 'Score'; + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + @override String broadcastNbBroadcasts(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -2015,9 +2051,6 @@ class AppLocalizationsHr extends AppLocalizations { @override String get byCPL => 'Po SDP'; - @override - String get openStudy => 'Otvori studiju'; - @override String get enable => 'Omogući'; @@ -2685,9 +2718,6 @@ class AppLocalizationsHr extends AppLocalizations { @override String get unblock => 'Odblokiraj'; - @override - String get followsYou => 'Prati te'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 je počeo pratiti $param2'; @@ -5489,6 +5519,11 @@ class AppLocalizationsHr extends AppLocalizations { @override String get studyYouCompletedThisLesson => 'Čestitamo! Završili ste lekciju.'; + @override + String studyPerPage(String param) { + return '$param per page'; + } + @override String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( diff --git a/lib/l10n/l10n_hu.dart b/lib/l10n/l10n_hu.dart index aa70650fe0..ad4dd2d3af 100644 --- a/lib/l10n/l10n_hu.dart +++ b/lib/l10n/l10n_hu.dart @@ -9,52 +9,57 @@ class AppLocalizationsHu extends AppLocalizations { AppLocalizationsHu([String locale = 'hu']) : super(locale); @override - String get mobileHomeTab => 'Kezdőlap'; + String get mobileAllGames => 'Összes játszma'; @override - String get mobilePuzzlesTab => 'Feladvány'; + String get mobileAreYouSure => 'Biztos vagy benne?'; @override - String get mobileToolsTab => 'Eszközök'; + String get mobileBlindfoldMode => 'Vakjátszma mód'; @override - String get mobileWatchTab => 'Néznivaló'; + String get mobileCancelTakebackOffer => 'Visszalépés kérésének visszavonása'; @override - String get mobileSettingsTab => 'Beállítás'; + String get mobileClearButton => 'Törlés'; @override - String get mobileMustBeLoggedIn => 'Az oldal megtekintéséhez be kell jelentkezned.'; + String get mobileCorrespondenceClearSavedMove => 'Mentett lépés törlése'; @override - String get mobileSystemColors => 'Rendszerszínek'; + String get mobileCustomGameJoinAGame => 'Csatlakozás játszmához'; @override String get mobileFeedbackButton => 'Visszajelzés'; @override - String get mobileOkButton => 'OK'; + String mobileGreeting(String param) { + return 'Üdv $param!'; + } @override - String get mobileSettingsHapticFeedback => 'Érintésalapú visszajelzés'; + String get mobileGreetingWithoutName => 'Üdv'; @override - String get mobileSettingsImmersiveMode => 'Teljes képernyős mód'; + String get mobileHideVariation => 'Változatok elrejtése'; @override - String get mobileSettingsImmersiveModeSubtitle => 'A rendszer gombjainak elrejtése játék közben. Kapcsold be, ha zavarnak a rendszer navigációs mozdulatai a képernyő sarkainál. A játszmaképernyőn és a Puzzle Storm képernyőjén működik.'; + String get mobileHomeTab => 'Kezdőlap'; @override - String get mobileNotFollowingAnyUser => 'Jelenleg nem követsz senkit.'; + String get mobileLiveStreamers => 'Lichess streamerek'; @override - String get mobileAllGames => 'Összes játszma'; + String get mobileMustBeLoggedIn => 'Az oldal megtekintéséhez be kell jelentkezned.'; @override - String get mobileRecentSearches => 'Keresési előzmények'; + String get mobileNoSearchResults => 'Nincs találat'; @override - String get mobileClearButton => 'Törlés'; + String get mobileNotFollowingAnyUser => 'Jelenleg nem követsz senkit.'; + + @override + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,84 +67,82 @@ class AppLocalizationsHu extends AppLocalizations { } @override - String get mobileNoSearchResults => 'Nincs találat'; + String get mobilePrefMagnifyDraggedPiece => 'Mozdított bábu nagyítása'; @override - String get mobileAreYouSure => 'Biztos vagy benne?'; + String get mobilePuzzleStormConfirmEndRun => 'Befejezed a futamot?'; @override - String get mobilePuzzleStreakAbortWarning => 'A jelenlegi sorozatod elveszik és az eredményedet rögzítjük.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nincs megjeleníthető elem, változtasd meg a szűrőket'; @override String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Feladvány megosztása'; + String get mobilePuzzleStormSubtitle => 'Oldd meg a lehető legtöbb feladványt 3 perc alatt.'; @override - String get mobileShareGameURL => 'Játszma URL megosztása'; + String get mobilePuzzleStreakAbortWarning => 'A jelenlegi sorozatod elveszik és az eredményedet rögzítjük.'; @override - String get mobileShareGamePGN => 'PGN megosztása'; + String get mobilePuzzleThemesSubtitle => 'Oldj feladványokat kedvenc megnyitásaid kapcsán vagy válassz egy tematikát.'; @override - String get mobileSharePositionAsFEN => 'Állás megosztása FEN-ként'; + String get mobilePuzzlesTab => 'Feladvány'; @override - String get mobileShowVariations => 'Változatok megjelenítése'; + String get mobileRecentSearches => 'Keresési előzmények'; @override - String get mobileHideVariation => 'Változatok elrejtése'; + String get mobileSettingsHapticFeedback => 'Érintésalapú visszajelzés'; @override - String get mobileShowComments => 'Megjegyzések megjelenítése'; + String get mobileSettingsImmersiveMode => 'Teljes képernyős mód'; @override - String get mobilePuzzleStormConfirmEndRun => 'Befejezed a futamot?'; + String get mobileSettingsImmersiveModeSubtitle => 'A rendszer gombjainak elrejtése játék közben. Kapcsold be, ha zavarnak a rendszer navigációs mozdulatai a képernyő sarkainál. A játszmaképernyőn és a Puzzle Storm képernyőjén működik.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nincs megjeleníthető elem, változtasd meg a szűrőket'; + String get mobileSettingsTab => 'Beállítás'; @override - String get mobileCancelTakebackOffer => 'Visszalépés kérésének visszavonása'; + String get mobileShareGamePGN => 'PGN megosztása'; @override - String get mobileWaitingForOpponentToJoin => 'Várakozás az ellenfél csatlakozására...'; + String get mobileShareGameURL => 'Játszma URL megosztása'; @override - String get mobileBlindfoldMode => 'Vakjátszma mód'; + String get mobileSharePositionAsFEN => 'Állás megosztása FEN-ként'; @override - String get mobileLiveStreamers => 'Lichess streamerek'; + String get mobileSharePuzzle => 'Feladvány megosztása'; @override - String get mobileCustomGameJoinAGame => 'Csatlakozás játszmához'; + String get mobileShowComments => 'Megjegyzések megjelenítése'; @override - String get mobileCorrespondenceClearSavedMove => 'Mentett lépés törlése'; + String get mobileShowResult => 'Eredmény mutatása'; @override - String get mobileSomethingWentWrong => 'Hiba történt.'; + String get mobileShowVariations => 'Változatok megjelenítése'; @override - String get mobileShowResult => 'Eredmény mutatása'; + String get mobileSomethingWentWrong => 'Hiba történt.'; @override - String get mobilePuzzleThemesSubtitle => 'Oldj feladványokat kedvenc megnyitásaid kapcsán vagy válassz egy tematikát.'; + String get mobileSystemColors => 'Rendszerszínek'; @override - String get mobilePuzzleStormSubtitle => 'Oldd meg a lehető legtöbb feladványt 3 perc alatt.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Üdv $param!'; - } + String get mobileToolsTab => 'Eszközök'; @override - String get mobileGreetingWithoutName => 'Üdv'; + String get mobileWaitingForOpponentToJoin => 'Várakozás az ellenfél csatlakozására...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Mozdított bábu nagyítása'; + String get mobileWatchTab => 'Néznivaló'; @override String get activityActivity => 'Aktivitás'; @@ -535,6 +538,9 @@ class AppLocalizationsHu extends AppLocalizations { @override String get broadcastStandings => 'Standings'; + @override + String get broadcastOfficialStandings => 'Official Standings'; + @override String broadcastIframeHelp(String param) { return 'More options on the $param'; @@ -565,6 +571,36 @@ class AppLocalizationsHu extends AppLocalizations { @override String get broadcastScore => 'Score'; + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + @override String broadcastNbBroadcasts(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -1992,9 +2028,6 @@ class AppLocalizationsHu extends AppLocalizations { @override String get byCPL => 'CPL'; - @override - String get openStudy => 'Tanulmány megnyitása'; - @override String get enable => 'Engedélyezve'; @@ -2662,9 +2695,6 @@ class AppLocalizationsHu extends AppLocalizations { @override String get unblock => 'Letiltás feloldása'; - @override - String get followsYou => 'Követ téged'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 $param2 követője lett'; @@ -5424,6 +5454,11 @@ class AppLocalizationsHu extends AppLocalizations { @override String get studyYouCompletedThisLesson => 'Gratulálok! A fejezet végére értél.'; + @override + String studyPerPage(String param) { + return '$param per page'; + } + @override String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( diff --git a/lib/l10n/l10n_hy.dart b/lib/l10n/l10n_hy.dart index 6075423a15..098206cd4d 100644 --- a/lib/l10n/l10n_hy.dart +++ b/lib/l10n/l10n_hy.dart @@ -9,52 +9,57 @@ class AppLocalizationsHy extends AppLocalizations { AppLocalizationsHy([String locale = 'hy']) : super(locale); @override - String get mobileHomeTab => 'Home'; + String get mobileAllGames => 'All games'; @override - String get mobilePuzzlesTab => 'Puzzles'; + String get mobileAreYouSure => 'Are you sure?'; @override - String get mobileToolsTab => 'Tools'; + String get mobileBlindfoldMode => 'Blindfold'; @override - String get mobileWatchTab => 'Watch'; + String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @override - String get mobileSettingsTab => 'Settings'; + String get mobileClearButton => 'Clear'; @override - String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; + String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; @override - String get mobileSystemColors => 'System colors'; + String get mobileCustomGameJoinAGame => 'Join a game'; @override String get mobileFeedbackButton => 'Feedback'; @override - String get mobileOkButton => 'OK'; + String mobileGreeting(String param) { + return 'Hello, $param'; + } @override - String get mobileSettingsHapticFeedback => 'Haptic feedback'; + String get mobileGreetingWithoutName => 'Hello'; @override - String get mobileSettingsImmersiveMode => 'Immersive mode'; + String get mobileHideVariation => 'Hide variation'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileHomeTab => 'Home'; @override - String get mobileNotFollowingAnyUser => 'You are not following any user.'; + String get mobileLiveStreamers => 'Live streamers'; @override - String get mobileAllGames => 'All games'; + String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; @override - String get mobileRecentSearches => 'Recent searches'; + String get mobileNoSearchResults => 'No results'; @override - String get mobileClearButton => 'Clear'; + String get mobileNotFollowingAnyUser => 'You are not following any user.'; + + @override + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,84 +67,82 @@ class AppLocalizationsHy extends AppLocalizations { } @override - String get mobileNoSearchResults => 'No results'; + String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; @override - String get mobileAreYouSure => 'Are you sure?'; + String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; @override String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Share this puzzle'; + String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; @override - String get mobileShareGameURL => 'Share game URL'; + String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; @override - String get mobileShareGamePGN => 'Share PGN'; + String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; @override - String get mobileSharePositionAsFEN => 'Share position as FEN'; + String get mobilePuzzlesTab => 'Puzzles'; @override - String get mobileShowVariations => 'Show variations'; + String get mobileRecentSearches => 'Recent searches'; @override - String get mobileHideVariation => 'Hide variation'; + String get mobileSettingsHapticFeedback => 'Haptic feedback'; @override - String get mobileShowComments => 'Show comments'; + String get mobileSettingsImmersiveMode => 'Immersive mode'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobileSettingsTab => 'Settings'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; + String get mobileShareGamePGN => 'Share PGN'; @override - String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; + String get mobileShareGameURL => 'Share game URL'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get mobileSharePositionAsFEN => 'Share position as FEN'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileSharePuzzle => 'Share this puzzle'; @override - String get mobileCustomGameJoinAGame => 'Join a game'; + String get mobileShowComments => 'Show comments'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; + String get mobileShowResult => 'Show result'; @override - String get mobileSomethingWentWrong => 'Something went wrong.'; + String get mobileShowVariations => 'Show variations'; @override - String get mobileShowResult => 'Show result'; + String get mobileSomethingWentWrong => 'Something went wrong.'; @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobileSystemColors => 'System colors'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Hello, $param'; - } + String get mobileToolsTab => 'Tools'; @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Watch'; @override String get activityActivity => 'Գործունեություն'; @@ -535,6 +538,9 @@ class AppLocalizationsHy extends AppLocalizations { @override String get broadcastStandings => 'Standings'; + @override + String get broadcastOfficialStandings => 'Official Standings'; + @override String broadcastIframeHelp(String param) { return 'More options on the $param'; @@ -565,6 +571,36 @@ class AppLocalizationsHy extends AppLocalizations { @override String get broadcastScore => 'Score'; + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + @override String broadcastNbBroadcasts(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -1992,9 +2028,6 @@ class AppLocalizationsHy extends AppLocalizations { @override String get byCPL => 'Ըստ սխալների'; - @override - String get openStudy => 'Բացել ուսուցումը'; - @override String get enable => 'Միացնել'; @@ -2662,9 +2695,6 @@ class AppLocalizationsHy extends AppLocalizations { @override String get unblock => 'Հանել արգելափակումը'; - @override - String get followsYou => 'Հետևում են ձեզ'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1-ը այժմ հետևում է $param2-ին'; @@ -5424,6 +5454,11 @@ class AppLocalizationsHy extends AppLocalizations { @override String get studyYouCompletedThisLesson => 'Շնորհավորո՜ւմ ենք։ Դուք ավարեցիք այս դասը։'; + @override + String studyPerPage(String param) { + return '$param per page'; + } + @override String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( diff --git a/lib/l10n/l10n_id.dart b/lib/l10n/l10n_id.dart index 700c873747..8d38b867c2 100644 --- a/lib/l10n/l10n_id.dart +++ b/lib/l10n/l10n_id.dart @@ -9,52 +9,57 @@ class AppLocalizationsId extends AppLocalizations { AppLocalizationsId([String locale = 'id']) : super(locale); @override - String get mobileHomeTab => 'Home'; + String get mobileAllGames => 'All games'; @override - String get mobilePuzzlesTab => 'Puzzles'; + String get mobileAreYouSure => 'Are you sure?'; @override - String get mobileToolsTab => 'Tools'; + String get mobileBlindfoldMode => 'Blindfold'; @override - String get mobileWatchTab => 'Watch'; + String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @override - String get mobileSettingsTab => 'Settings'; + String get mobileClearButton => 'Clear'; @override - String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; + String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; @override - String get mobileSystemColors => 'System colors'; + String get mobileCustomGameJoinAGame => 'Join a game'; @override String get mobileFeedbackButton => 'Feedback'; @override - String get mobileOkButton => 'OK'; + String mobileGreeting(String param) { + return 'Hello, $param'; + } @override - String get mobileSettingsHapticFeedback => 'Haptic feedback'; + String get mobileGreetingWithoutName => 'Hello'; @override - String get mobileSettingsImmersiveMode => 'Immersive mode'; + String get mobileHideVariation => 'Hide variation'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileHomeTab => 'Home'; @override - String get mobileNotFollowingAnyUser => 'You are not following any user.'; + String get mobileLiveStreamers => 'Live streamers'; @override - String get mobileAllGames => 'All games'; + String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; @override - String get mobileRecentSearches => 'Recent searches'; + String get mobileNoSearchResults => 'No results'; @override - String get mobileClearButton => 'Clear'; + String get mobileNotFollowingAnyUser => 'You are not following any user.'; + + @override + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,84 +67,82 @@ class AppLocalizationsId extends AppLocalizations { } @override - String get mobileNoSearchResults => 'No results'; + String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; @override - String get mobileAreYouSure => 'Are you sure?'; + String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; @override String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Share this puzzle'; + String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; @override - String get mobileShareGameURL => 'Share game URL'; + String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; @override - String get mobileShareGamePGN => 'Share PGN'; + String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; @override - String get mobileSharePositionAsFEN => 'Share position as FEN'; + String get mobilePuzzlesTab => 'Puzzles'; @override - String get mobileShowVariations => 'Show variations'; + String get mobileRecentSearches => 'Recent searches'; @override - String get mobileHideVariation => 'Hide variation'; + String get mobileSettingsHapticFeedback => 'Haptic feedback'; @override - String get mobileShowComments => 'Show comments'; + String get mobileSettingsImmersiveMode => 'Immersive mode'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobileSettingsTab => 'Settings'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; + String get mobileShareGamePGN => 'Share PGN'; @override - String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; + String get mobileShareGameURL => 'Share game URL'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get mobileSharePositionAsFEN => 'Share position as FEN'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileSharePuzzle => 'Share this puzzle'; @override - String get mobileCustomGameJoinAGame => 'Join a game'; + String get mobileShowComments => 'Show comments'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; + String get mobileShowResult => 'Show result'; @override - String get mobileSomethingWentWrong => 'Something went wrong.'; + String get mobileShowVariations => 'Show variations'; @override - String get mobileShowResult => 'Show result'; + String get mobileSomethingWentWrong => 'Something went wrong.'; @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobileSystemColors => 'System colors'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Hello, $param'; - } + String get mobileToolsTab => 'Tools'; @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Watch'; @override String get activityActivity => 'Aktivitas'; @@ -518,6 +521,9 @@ class AppLocalizationsId extends AppLocalizations { @override String get broadcastStandings => 'Standings'; + @override + String get broadcastOfficialStandings => 'Official Standings'; + @override String broadcastIframeHelp(String param) { return 'More options on the $param'; @@ -548,6 +554,36 @@ class AppLocalizationsId extends AppLocalizations { @override String get broadcastScore => 'Score'; + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + @override String broadcastNbBroadcasts(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -1970,9 +2006,6 @@ class AppLocalizationsId extends AppLocalizations { @override String get byCPL => 'Secara CPL'; - @override - String get openStudy => 'Buka studi'; - @override String get enable => 'Aktifkan'; @@ -2640,9 +2673,6 @@ class AppLocalizationsId extends AppLocalizations { @override String get unblock => 'Buka blokir'; - @override - String get followsYou => 'Mengikuti anda'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 mulai mengikuti $param2'; @@ -5358,6 +5388,11 @@ class AppLocalizationsId extends AppLocalizations { @override String get studyYouCompletedThisLesson => 'Selamat. Anda telah menyelesaikan pelajaran ini.'; + @override + String studyPerPage(String param) { + return '$param per page'; + } + @override String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( diff --git a/lib/l10n/l10n_it.dart b/lib/l10n/l10n_it.dart index 997afd9ccb..fed9f6fb6c 100644 --- a/lib/l10n/l10n_it.dart +++ b/lib/l10n/l10n_it.dart @@ -9,52 +9,57 @@ class AppLocalizationsIt extends AppLocalizations { AppLocalizationsIt([String locale = 'it']) : super(locale); @override - String get mobileHomeTab => 'Home'; + String get mobileAllGames => 'Tutte le partite'; @override - String get mobilePuzzlesTab => 'Tattiche'; + String get mobileAreYouSure => 'Sei sicuro?'; @override - String get mobileToolsTab => 'Strumenti'; + String get mobileBlindfoldMode => 'Alla cieca'; @override - String get mobileWatchTab => 'Guarda'; + String get mobileCancelTakebackOffer => 'Annulla richiesta di ritiro mossa'; @override - String get mobileSettingsTab => 'Settaggi'; + String get mobileClearButton => 'Elimina'; @override - String get mobileMustBeLoggedIn => 'Devi aver effettuato l\'accesso per visualizzare questa pagina.'; + String get mobileCorrespondenceClearSavedMove => 'Cancella mossa salvata'; @override - String get mobileSystemColors => 'Tema app'; + String get mobileCustomGameJoinAGame => 'Unisciti a una partita'; @override String get mobileFeedbackButton => 'Suggerimenti'; @override - String get mobileOkButton => 'Ok'; + String mobileGreeting(String param) { + return 'Ciao, $param'; + } @override - String get mobileSettingsHapticFeedback => 'Feedback tattile'; + String get mobileGreetingWithoutName => 'Ciao'; @override - String get mobileSettingsImmersiveMode => 'Modalità immersiva'; + String get mobileHideVariation => 'Nascondi variante'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Nascondi la UI di sistema mentre giochi. Attiva se i gesti di navigazione ai bordi dello schermo ti danno fastidio. Si applica alla schermata di gioco e Puzzle Storm.'; + String get mobileHomeTab => 'Home'; @override - String get mobileNotFollowingAnyUser => 'Non stai seguendo nessun utente.'; + String get mobileLiveStreamers => 'Streamer in diretta'; @override - String get mobileAllGames => 'Tutte le partite'; + String get mobileMustBeLoggedIn => 'Devi aver effettuato l\'accesso per visualizzare questa pagina.'; @override - String get mobileRecentSearches => 'Ricerche recenti'; + String get mobileNoSearchResults => 'Nessun risultato'; @override - String get mobileClearButton => 'Elimina'; + String get mobileNotFollowingAnyUser => 'Non stai seguendo nessun utente.'; + + @override + String get mobileOkButton => 'Ok'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,84 +67,82 @@ class AppLocalizationsIt extends AppLocalizations { } @override - String get mobileNoSearchResults => 'Nessun risultato'; + String get mobilePrefMagnifyDraggedPiece => 'Ingrandisci il pezzo trascinato'; @override - String get mobileAreYouSure => 'Sei sicuro?'; + String get mobilePuzzleStormConfirmEndRun => 'Vuoi terminare questa serie?'; @override - String get mobilePuzzleStreakAbortWarning => 'Perderai la tua serie corrente e il tuo punteggio verrà salvato.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nessun risultato, per favore modifica i filtri'; @override String get mobilePuzzleStormNothingToShow => 'Niente da mostrare. Gioca ad alcune partite di Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Condividi questa tattica'; + String get mobilePuzzleStormSubtitle => 'Risolvi il maggior numero di puzzle in tre minuti.'; @override - String get mobileShareGameURL => 'Condividi URL della partita'; + String get mobilePuzzleStreakAbortWarning => 'Perderai la tua serie corrente e il tuo punteggio verrà salvato.'; @override - String get mobileShareGamePGN => 'Condividi PGN'; + String get mobilePuzzleThemesSubtitle => '.'; @override - String get mobileSharePositionAsFEN => 'Condividi posizione come FEN'; + String get mobilePuzzlesTab => 'Tattiche'; @override - String get mobileShowVariations => 'Mostra varianti'; + String get mobileRecentSearches => 'Ricerche recenti'; @override - String get mobileHideVariation => 'Nascondi variante'; + String get mobileSettingsHapticFeedback => 'Feedback tattile'; @override - String get mobileShowComments => 'Mostra commenti'; + String get mobileSettingsImmersiveMode => 'Modalità immersiva'; @override - String get mobilePuzzleStormConfirmEndRun => 'Vuoi terminare questa serie?'; + String get mobileSettingsImmersiveModeSubtitle => 'Nascondi la UI di sistema mentre giochi. Attiva se i gesti di navigazione ai bordi dello schermo ti danno fastidio. Si applica alla schermata di gioco e Puzzle Storm.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nessun risultato, per favore modifica i filtri'; + String get mobileSettingsTab => 'Settaggi'; @override - String get mobileCancelTakebackOffer => 'Annulla richiesta di ritiro mossa'; + String get mobileShareGamePGN => 'Condividi PGN'; @override - String get mobileWaitingForOpponentToJoin => 'In attesa dell\'avversario...'; + String get mobileShareGameURL => 'Condividi URL della partita'; @override - String get mobileBlindfoldMode => 'Alla cieca'; + String get mobileSharePositionAsFEN => 'Condividi posizione come FEN'; @override - String get mobileLiveStreamers => 'Streamer in diretta'; + String get mobileSharePuzzle => 'Condividi questa tattica'; @override - String get mobileCustomGameJoinAGame => 'Unisciti a una partita'; + String get mobileShowComments => 'Mostra commenti'; @override - String get mobileCorrespondenceClearSavedMove => 'Cancella mossa salvata'; + String get mobileShowResult => 'Mostra il risultato'; @override - String get mobileSomethingWentWrong => 'Si è verificato un errore.'; + String get mobileShowVariations => 'Mostra varianti'; @override - String get mobileShowResult => 'Mostra il risultato'; + String get mobileSomethingWentWrong => 'Si è verificato un errore.'; @override - String get mobilePuzzleThemesSubtitle => '.'; + String get mobileSystemColors => 'Tema app'; @override - String get mobilePuzzleStormSubtitle => 'Risolvi il maggior numero di puzzle in tre minuti.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Ciao, $param'; - } + String get mobileToolsTab => 'Strumenti'; @override - String get mobileGreetingWithoutName => 'Ciao'; + String get mobileWaitingForOpponentToJoin => 'In attesa dell\'avversario...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Ingrandisci il pezzo trascinato'; + String get mobileWatchTab => 'Guarda'; @override String get activityActivity => 'Attività'; @@ -535,6 +538,9 @@ class AppLocalizationsIt extends AppLocalizations { @override String get broadcastStandings => 'Standings'; + @override + String get broadcastOfficialStandings => 'Official Standings'; + @override String broadcastIframeHelp(String param) { return 'More options on the $param'; @@ -565,6 +571,36 @@ class AppLocalizationsIt extends AppLocalizations { @override String get broadcastScore => 'Score'; + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + @override String broadcastNbBroadcasts(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -1992,9 +2028,6 @@ class AppLocalizationsIt extends AppLocalizations { @override String get byCPL => 'Per CPL'; - @override - String get openStudy => 'Apri studio'; - @override String get enable => 'Abilita'; @@ -2662,9 +2695,6 @@ class AppLocalizationsIt extends AppLocalizations { @override String get unblock => 'Sblocca'; - @override - String get followsYou => 'Ti segue'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 ha iniziato a seguire $param2'; @@ -5424,6 +5454,11 @@ class AppLocalizationsIt extends AppLocalizations { @override String get studyYouCompletedThisLesson => 'Congratulazioni! Hai completato questa lezione.'; + @override + String studyPerPage(String param) { + return '$param per page'; + } + @override String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( diff --git a/lib/l10n/l10n_ja.dart b/lib/l10n/l10n_ja.dart index 65c193783a..a78b58e2b7 100644 --- a/lib/l10n/l10n_ja.dart +++ b/lib/l10n/l10n_ja.dart @@ -9,52 +9,57 @@ class AppLocalizationsJa extends AppLocalizations { AppLocalizationsJa([String locale = 'ja']) : super(locale); @override - String get mobileHomeTab => 'ホーム'; + String get mobileAllGames => 'すべて'; @override - String get mobilePuzzlesTab => '問題'; + String get mobileAreYouSure => '本当にいいですか?'; @override - String get mobileToolsTab => 'ツール'; + String get mobileBlindfoldMode => 'めかくしモード'; @override - String get mobileWatchTab => '見る'; + String get mobileCancelTakebackOffer => '待ったをキャンセル'; @override - String get mobileSettingsTab => '設定'; + String get mobileClearButton => 'クリア'; @override - String get mobileMustBeLoggedIn => 'このページを見るにはログインが必要です。'; + String get mobileCorrespondenceClearSavedMove => '保存した手を削除'; @override - String get mobileSystemColors => 'OS と同じ色設定'; + String get mobileCustomGameJoinAGame => 'ゲームに参加'; @override String get mobileFeedbackButton => 'フィードバック'; @override - String get mobileOkButton => 'OK'; + String mobileGreeting(String param) { + return 'こんにちは $param さん'; + } @override - String get mobileSettingsHapticFeedback => '振動フィードバック'; + String get mobileGreetingWithoutName => 'こんにちは'; @override - String get mobileSettingsImmersiveMode => '没入モード'; + String get mobileHideVariation => '変化手順を隠す'; @override - String get mobileSettingsImmersiveModeSubtitle => '対局中にシステム用の UI を隠します。画面端のナビゲーションなどがじゃまな人はこれを使ってください。対局と問題ストームの画面に適用されます。'; + String get mobileHomeTab => 'ホーム'; @override - String get mobileNotFollowingAnyUser => '誰もフォローしていません。'; + String get mobileLiveStreamers => 'ライブ配信者'; @override - String get mobileAllGames => 'すべて'; + String get mobileMustBeLoggedIn => 'このページを見るにはログインが必要です。'; @override - String get mobileRecentSearches => '最近の検索'; + String get mobileNoSearchResults => '検索結果なし'; @override - String get mobileClearButton => 'クリア'; + String get mobileNotFollowingAnyUser => '誰もフォローしていません。'; + + @override + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,84 +67,82 @@ class AppLocalizationsJa extends AppLocalizations { } @override - String get mobileNoSearchResults => '検索結果なし'; + String get mobilePrefMagnifyDraggedPiece => 'ドラッグ中の駒を拡大'; @override - String get mobileAreYouSure => '本当にいいですか?'; + String get mobilePuzzleStormConfirmEndRun => 'このストームを終了しますか?'; @override - String get mobilePuzzleStreakAbortWarning => '現在の連続正解が終わり、スコアが保存されます。'; + String get mobilePuzzleStormFilterNothingToShow => '条件に合う問題がありません。フィルターを変更してください'; @override String get mobilePuzzleStormNothingToShow => 'データがありません。まず問題ストームをプレイして。'; @override - String get mobileSharePuzzle => 'この問題を共有する'; + String get mobilePuzzleStormSubtitle => '3 分間でできるだけ多くの問題を解いてください。'; @override - String get mobileShareGameURL => 'ゲーム URLを共有'; + String get mobilePuzzleStreakAbortWarning => '現在の連続正解が終わり、スコアが保存されます。'; @override - String get mobileShareGamePGN => 'PGN を共有'; + String get mobilePuzzleThemesSubtitle => 'お気に入りのオープニングやテーマの問題が選べます。'; @override - String get mobileSharePositionAsFEN => '局面を FEN で共有'; + String get mobilePuzzlesTab => '問題'; @override - String get mobileShowVariations => '変化手順を表示'; + String get mobileRecentSearches => '最近の検索'; @override - String get mobileHideVariation => '変化手順を隠す'; + String get mobileSettingsHapticFeedback => '振動フィードバック'; @override - String get mobileShowComments => 'コメントを表示'; + String get mobileSettingsImmersiveMode => '没入モード'; @override - String get mobilePuzzleStormConfirmEndRun => 'このストームを終了しますか?'; + String get mobileSettingsImmersiveModeSubtitle => '対局中にシステム用の UI を隠します。画面端のナビゲーションなどがじゃまな人はこれを使ってください。対局と問題ストームの画面に適用されます。'; @override - String get mobilePuzzleStormFilterNothingToShow => '条件に合う問題がありません。フィルターを変更してください'; + String get mobileSettingsTab => '設定'; @override - String get mobileCancelTakebackOffer => '待ったをキャンセル'; + String get mobileShareGamePGN => 'PGN を共有'; @override - String get mobileWaitingForOpponentToJoin => '対戦相手の参加を待っています…'; + String get mobileShareGameURL => 'ゲーム URLを共有'; @override - String get mobileBlindfoldMode => 'めかくしモード'; + String get mobileSharePositionAsFEN => '局面を FEN で共有'; @override - String get mobileLiveStreamers => 'ライブ配信者'; + String get mobileSharePuzzle => 'この問題を共有する'; @override - String get mobileCustomGameJoinAGame => 'ゲームに参加'; + String get mobileShowComments => 'コメントを表示'; @override - String get mobileCorrespondenceClearSavedMove => '保存した手を削除'; + String get mobileShowResult => '結果を表示'; @override - String get mobileSomethingWentWrong => '問題が発生しました。'; + String get mobileShowVariations => '変化手順を表示'; @override - String get mobileShowResult => '結果を表示'; + String get mobileSomethingWentWrong => '問題が発生しました。'; @override - String get mobilePuzzleThemesSubtitle => 'お気に入りのオープニングやテーマの問題が選べます。'; + String get mobileSystemColors => 'OS と同じ色設定'; @override - String get mobilePuzzleStormSubtitle => '3 分間でできるだけ多くの問題を解いてください。'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'こんにちは $param さん'; - } + String get mobileToolsTab => 'ツール'; @override - String get mobileGreetingWithoutName => 'こんにちは'; + String get mobileWaitingForOpponentToJoin => '対戦相手の参加を待っています…'; @override - String get mobilePrefMagnifyDraggedPiece => 'ドラッグ中の駒を拡大'; + String get mobileWatchTab => '見る'; @override String get activityActivity => '活動'; @@ -517,6 +520,9 @@ class AppLocalizationsJa extends AppLocalizations { @override String get broadcastStandings => '順位'; + @override + String get broadcastOfficialStandings => 'Official Standings'; + @override String broadcastIframeHelp(String param) { return '他のオプションは $param にあります'; @@ -547,6 +553,36 @@ class AppLocalizationsJa extends AppLocalizations { @override String get broadcastScore => 'スコア'; + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + @override String broadcastNbBroadcasts(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -1968,9 +2004,6 @@ class AppLocalizationsJa extends AppLocalizations { @override String get byCPL => '評価値で'; - @override - String get openStudy => '研究を開く'; - @override String get enable => '解析する'; @@ -2638,9 +2671,6 @@ class AppLocalizationsJa extends AppLocalizations { @override String get unblock => 'ブロックを外す'; - @override - String get followsYou => 'あなたをフォローしています'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 が $param2 のフォローを開始'; @@ -5356,6 +5386,11 @@ class AppLocalizationsJa extends AppLocalizations { @override String get studyYouCompletedThisLesson => 'おめでとう ! このレッスンを修了しました。'; + @override + String studyPerPage(String param) { + return '$param per page'; + } + @override String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( diff --git a/lib/l10n/l10n_kk.dart b/lib/l10n/l10n_kk.dart index 8794975fa0..edb76bae45 100644 --- a/lib/l10n/l10n_kk.dart +++ b/lib/l10n/l10n_kk.dart @@ -9,52 +9,57 @@ class AppLocalizationsKk extends AppLocalizations { AppLocalizationsKk([String locale = 'kk']) : super(locale); @override - String get mobileHomeTab => 'Үйге'; + String get mobileAllGames => 'Барлық ойындар'; @override - String get mobilePuzzlesTab => 'Жұмбақ'; + String get mobileAreYouSure => 'Растайсыз ба?'; @override - String get mobileToolsTab => 'Құрал'; + String get mobileBlindfoldMode => 'Blindfold'; @override - String get mobileWatchTab => 'Бақылау'; + String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @override - String get mobileSettingsTab => 'Баптау'; + String get mobileClearButton => 'Өшіру'; @override - String get mobileMustBeLoggedIn => 'Бұл бетті көру үшін тіркелгіге кіріңіз.'; + String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; @override - String get mobileSystemColors => 'Жүйе түстері'; + String get mobileCustomGameJoinAGame => 'Join a game'; @override String get mobileFeedbackButton => 'Пікір айту'; @override - String get mobileOkButton => 'Иә'; + String mobileGreeting(String param) { + return 'Ассаламу ғалейкүм, $param'; + } @override - String get mobileSettingsHapticFeedback => 'Дірілмен білдіру'; + String get mobileGreetingWithoutName => 'Ассаламу ғалейкүм'; @override - String get mobileSettingsImmersiveMode => 'Оқшау көрініс'; + String get mobileHideVariation => 'Hide variation'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Ойын кезінде жүйенің элементтерін жасыру. Экран жиегіндегі жүйенің навигация қимыл белгілері сізге кедергі келтірсе - қолданарлық жағдай. Ойын мен Жұмбақ Дауылы кезінде жұмыс істейді.'; + String get mobileHomeTab => 'Үйге'; @override - String get mobileNotFollowingAnyUser => 'Сіз әзір ешкіге серік емессіз.'; + String get mobileLiveStreamers => 'Live streamers'; @override - String get mobileAllGames => 'Барлық ойындар'; + String get mobileMustBeLoggedIn => 'Бұл бетті көру үшін тіркелгіге кіріңіз.'; @override - String get mobileRecentSearches => 'Кейінгі іздеулер'; + String get mobileNoSearchResults => 'Нәтиже жоқ'; @override - String get mobileClearButton => 'Өшіру'; + String get mobileNotFollowingAnyUser => 'Сіз әзір ешкіге серік емессіз.'; + + @override + String get mobileOkButton => 'Иә'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,84 +67,82 @@ class AppLocalizationsKk extends AppLocalizations { } @override - String get mobileNoSearchResults => 'Нәтиже жоқ'; + String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; @override - String get mobileAreYouSure => 'Растайсыз ба?'; + String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; @override - String get mobilePuzzleStreakAbortWarning => 'Қазіргі тізбектен айрыласыз, нәтиже сақталады.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; @override String get mobilePuzzleStormNothingToShow => 'Нәтиже әзір жоқ. Жұмбақ Дауылын ойнап көріңіз.'; @override - String get mobileSharePuzzle => 'Бұл жұмбақты тарату'; + String get mobilePuzzleStormSubtitle => '3 минутта барынша көп жұмбақ шешіп көр.'; @override - String get mobileShareGameURL => 'Ойын сілтемесін тарату'; + String get mobilePuzzleStreakAbortWarning => 'Қазіргі тізбектен айрыласыз, нәтиже сақталады.'; @override - String get mobileShareGamePGN => 'PGN тарату'; + String get mobilePuzzleThemesSubtitle => 'Өз бастауларыңызға негізделген жұмбақтар, не кез-келген тақырып.'; @override - String get mobileSharePositionAsFEN => 'Share position as FEN'; + String get mobilePuzzlesTab => 'Жұмбақ'; @override - String get mobileShowVariations => 'Show variations'; + String get mobileRecentSearches => 'Кейінгі іздеулер'; @override - String get mobileHideVariation => 'Hide variation'; + String get mobileSettingsHapticFeedback => 'Дірілмен білдіру'; @override - String get mobileShowComments => 'Show comments'; + String get mobileSettingsImmersiveMode => 'Оқшау көрініс'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get mobileSettingsImmersiveModeSubtitle => 'Ойын кезінде жүйенің элементтерін жасыру. Экран жиегіндегі жүйенің навигация қимыл белгілері сізге кедергі келтірсе - қолданарлық жағдай. Ойын мен Жұмбақ Дауылы кезінде жұмыс істейді.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobileSettingsTab => 'Баптау'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; + String get mobileShareGamePGN => 'PGN тарату'; @override - String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; + String get mobileShareGameURL => 'Ойын сілтемесін тарату'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get mobileSharePositionAsFEN => 'Share position as FEN'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileSharePuzzle => 'Бұл жұмбақты тарату'; @override - String get mobileCustomGameJoinAGame => 'Join a game'; + String get mobileShowComments => 'Show comments'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; + String get mobileShowResult => 'Нәтижесін көру'; @override - String get mobileSomethingWentWrong => 'Something went wrong.'; + String get mobileShowVariations => 'Show variations'; @override - String get mobileShowResult => 'Нәтижесін көру'; + String get mobileSomethingWentWrong => 'Something went wrong.'; @override - String get mobilePuzzleThemesSubtitle => 'Өз бастауларыңызға негізделген жұмбақтар, не кез-келген тақырып.'; + String get mobileSystemColors => 'Жүйе түстері'; @override - String get mobilePuzzleStormSubtitle => '3 минутта барынша көп жұмбақ шешіп көр.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Ассаламу ғалейкүм, $param'; - } + String get mobileToolsTab => 'Құрал'; @override - String get mobileGreetingWithoutName => 'Ассаламу ғалейкүм'; + String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Бақылау'; @override String get activityActivity => 'Белсенділігі'; @@ -535,6 +538,9 @@ class AppLocalizationsKk extends AppLocalizations { @override String get broadcastStandings => 'Standings'; + @override + String get broadcastOfficialStandings => 'Official Standings'; + @override String broadcastIframeHelp(String param) { return 'More options on the $param'; @@ -565,6 +571,36 @@ class AppLocalizationsKk extends AppLocalizations { @override String get broadcastScore => 'Score'; + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + @override String broadcastNbBroadcasts(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -1992,9 +2028,6 @@ class AppLocalizationsKk extends AppLocalizations { @override String get byCPL => 'CPL сәйкес'; - @override - String get openStudy => 'Зерттеуді ашу'; - @override String get enable => 'Қосу'; @@ -2662,9 +2695,6 @@ class AppLocalizationsKk extends AppLocalizations { @override String get unblock => 'Бұғаттан шығару'; - @override - String get followsYou => 'Сізге серік'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 $param2 серігі болды'; @@ -5424,6 +5454,11 @@ class AppLocalizationsKk extends AppLocalizations { @override String get studyYouCompletedThisLesson => 'Құтты болсын! Сіз бұл сабақты бітірдіңіз.'; + @override + String studyPerPage(String param) { + return '$param per page'; + } + @override String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( diff --git a/lib/l10n/l10n_ko.dart b/lib/l10n/l10n_ko.dart index d2613ec56a..a10e11e820 100644 --- a/lib/l10n/l10n_ko.dart +++ b/lib/l10n/l10n_ko.dart @@ -9,52 +9,57 @@ class AppLocalizationsKo extends AppLocalizations { AppLocalizationsKo([String locale = 'ko']) : super(locale); @override - String get mobileHomeTab => '홈'; + String get mobileAllGames => '모든 대국'; @override - String get mobilePuzzlesTab => '퍼즐'; + String get mobileAreYouSure => '확실하십니까?'; @override - String get mobileToolsTab => '도구'; + String get mobileBlindfoldMode => '기물 가리기'; @override - String get mobileWatchTab => '관람'; + String get mobileCancelTakebackOffer => '무르기 요청 취소'; @override - String get mobileSettingsTab => '설정'; + String get mobileClearButton => '지우기'; @override - String get mobileMustBeLoggedIn => '이 페이지를 보려면 로그인해야 합니다.'; + String get mobileCorrespondenceClearSavedMove => '저장된 수 삭제'; @override - String get mobileSystemColors => '시스템 색상'; + String get mobileCustomGameJoinAGame => '게임 참가'; @override String get mobileFeedbackButton => '피드백'; @override - String get mobileOkButton => '확인'; + String mobileGreeting(String param) { + return '안녕하세요, $param'; + } @override - String get mobileSettingsHapticFeedback => '터치 시 진동'; + String get mobileGreetingWithoutName => '안녕하세요'; @override - String get mobileSettingsImmersiveMode => '전체 화면 모드'; + String get mobileHideVariation => '바리에이션 숨기기'; @override - String get mobileSettingsImmersiveModeSubtitle => '플레이 중 시스템 UI를 숨깁니다. 화면 가장자리의 시스템 내비게이션 제스처가 불편하다면 사용하세요. 대국과 퍼즐 스톰 화면에서 적용됩니다.'; + String get mobileHomeTab => '홈'; @override - String get mobileNotFollowingAnyUser => '팔로우한 사용자가 없습니다.'; + String get mobileLiveStreamers => '방송 중인 스트리머'; @override - String get mobileAllGames => '모든 대국'; + String get mobileMustBeLoggedIn => '이 페이지를 보려면 로그인해야 합니다.'; @override - String get mobileRecentSearches => '최근 검색어'; + String get mobileNoSearchResults => '결과 없음'; @override - String get mobileClearButton => '지우기'; + String get mobileNotFollowingAnyUser => '팔로우한 사용자가 없습니다.'; + + @override + String get mobileOkButton => '확인'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,84 +67,82 @@ class AppLocalizationsKo extends AppLocalizations { } @override - String get mobileNoSearchResults => '결과 없음'; + String get mobilePrefMagnifyDraggedPiece => '드래그한 기물 확대하기'; @override - String get mobileAreYouSure => '확실하십니까?'; + String get mobilePuzzleStormConfirmEndRun => '이 도전을 종료하시겠습니까?'; @override - String get mobilePuzzleStreakAbortWarning => '현재 연속 해결 기록을 잃고 점수는 저장됩니다.'; + String get mobilePuzzleStormFilterNothingToShow => '표시할 것이 없습니다. 필터를 변경해 주세요'; @override String get mobilePuzzleStormNothingToShow => '표시할 것이 없습니다. 먼저 퍼즐 스톰을 플레이하세요.'; @override - String get mobileSharePuzzle => '이 퍼즐 공유'; + String get mobilePuzzleStormSubtitle => '3분 이내에 최대한 많은 퍼즐을 해결하십시오.'; @override - String get mobileShareGameURL => '게임 URL 공유'; + String get mobilePuzzleStreakAbortWarning => '현재 연속 해결 기록을 잃고 점수는 저장됩니다.'; @override - String get mobileShareGamePGN => 'PGN 공유'; + String get mobilePuzzleThemesSubtitle => '당신이 가장 좋아하는 오프닝으로부터의 퍼즐을 플레이하거나, 테마를 선택하십시오.'; @override - String get mobileSharePositionAsFEN => 'FEN으로 공유'; + String get mobilePuzzlesTab => '퍼즐'; @override - String get mobileShowVariations => '바리에이션 보이기'; + String get mobileRecentSearches => '최근 검색어'; @override - String get mobileHideVariation => '바리에이션 숨기기'; + String get mobileSettingsHapticFeedback => '터치 시 진동'; @override - String get mobileShowComments => '댓글 보기'; + String get mobileSettingsImmersiveMode => '전체 화면 모드'; @override - String get mobilePuzzleStormConfirmEndRun => '이 도전을 종료하시겠습니까?'; + String get mobileSettingsImmersiveModeSubtitle => '플레이 중 시스템 UI를 숨깁니다. 화면 가장자리의 시스템 내비게이션 제스처가 불편하다면 사용하세요. 대국과 퍼즐 스톰 화면에서 적용됩니다.'; @override - String get mobilePuzzleStormFilterNothingToShow => '표시할 것이 없습니다. 필터를 변경해 주세요'; + String get mobileSettingsTab => '설정'; @override - String get mobileCancelTakebackOffer => '무르기 요청 취소'; + String get mobileShareGamePGN => 'PGN 공유'; @override - String get mobileWaitingForOpponentToJoin => '상대 참가를 기다리는 중...'; + String get mobileShareGameURL => '게임 URL 공유'; @override - String get mobileBlindfoldMode => '기물 가리기'; + String get mobileSharePositionAsFEN => 'FEN으로 공유'; @override - String get mobileLiveStreamers => '방송 중인 스트리머'; + String get mobileSharePuzzle => '이 퍼즐 공유'; @override - String get mobileCustomGameJoinAGame => '게임 참가'; + String get mobileShowComments => '댓글 보기'; @override - String get mobileCorrespondenceClearSavedMove => '저장된 수 삭제'; + String get mobileShowResult => '결과 표시'; @override - String get mobileSomethingWentWrong => '문제가 발생했습니다.'; + String get mobileShowVariations => '바리에이션 보이기'; @override - String get mobileShowResult => '결과 표시'; + String get mobileSomethingWentWrong => '문제가 발생했습니다.'; @override - String get mobilePuzzleThemesSubtitle => '당신이 가장 좋아하는 오프닝으로부터의 퍼즐을 플레이하거나, 테마를 선택하십시오.'; + String get mobileSystemColors => '시스템 색상'; @override - String get mobilePuzzleStormSubtitle => '3분 이내에 최대한 많은 퍼즐을 해결하십시오.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return '안녕하세요, $param'; - } + String get mobileToolsTab => '도구'; @override - String get mobileGreetingWithoutName => '안녕하세요'; + String get mobileWaitingForOpponentToJoin => '상대 참가를 기다리는 중...'; @override - String get mobilePrefMagnifyDraggedPiece => '드래그한 기물 확대하기'; + String get mobileWatchTab => '관람'; @override String get activityActivity => '활동'; @@ -517,6 +520,9 @@ class AppLocalizationsKo extends AppLocalizations { @override String get broadcastStandings => '순위'; + @override + String get broadcastOfficialStandings => 'Official Standings'; + @override String broadcastIframeHelp(String param) { return '$param에서 더 많은 정보를 확인하실 수 있습니다'; @@ -547,6 +553,36 @@ class AppLocalizationsKo extends AppLocalizations { @override String get broadcastScore => '점수'; + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + @override String broadcastNbBroadcasts(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -1968,9 +2004,6 @@ class AppLocalizationsKo extends AppLocalizations { @override String get byCPL => '센티폰 손실'; - @override - String get openStudy => '연구를 시작하기'; - @override String get enable => '활성화'; @@ -2638,9 +2671,6 @@ class AppLocalizationsKo extends AppLocalizations { @override String get unblock => '차단 해제'; - @override - String get followsYou => '팔로워'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1(이)가 $param2(을)를 팔로우했습니다'; @@ -5356,6 +5386,11 @@ class AppLocalizationsKo extends AppLocalizations { @override String get studyYouCompletedThisLesson => '축하합니다! 이 레슨을 완료했습니다.'; + @override + String studyPerPage(String param) { + return '$param per page'; + } + @override String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( diff --git a/lib/l10n/l10n_lb.dart b/lib/l10n/l10n_lb.dart index e7c01e58c0..a916e11a70 100644 --- a/lib/l10n/l10n_lb.dart +++ b/lib/l10n/l10n_lb.dart @@ -9,52 +9,57 @@ class AppLocalizationsLb extends AppLocalizations { AppLocalizationsLb([String locale = 'lb']) : super(locale); @override - String get mobileHomeTab => 'Home'; + String get mobileAllGames => 'All Partien'; @override - String get mobilePuzzlesTab => 'Aufgaben'; + String get mobileAreYouSure => 'Bass de sécher?'; @override - String get mobileToolsTab => 'Tools'; + String get mobileBlindfoldMode => 'Blann'; @override - String get mobileWatchTab => 'Watch'; + String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @override - String get mobileSettingsTab => 'Settings'; + String get mobileClearButton => 'Clear'; @override - String get mobileMustBeLoggedIn => 'Du muss ageloggt si fir dës Säit ze gesinn.'; + String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; @override - String get mobileSystemColors => 'Systemsfaarwen'; + String get mobileCustomGameJoinAGame => 'Join a game'; @override String get mobileFeedbackButton => 'Feedback'; @override - String get mobileOkButton => 'OK'; + String mobileGreeting(String param) { + return 'Moien, $param'; + } @override - String get mobileSettingsHapticFeedback => 'Haptesche Feedback'; + String get mobileGreetingWithoutName => 'Moien'; @override - String get mobileSettingsImmersiveMode => 'Immersive Modus'; + String get mobileHideVariation => 'Variante verstoppen'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileHomeTab => 'Home'; @override - String get mobileNotFollowingAnyUser => 'You are not following any user.'; + String get mobileLiveStreamers => 'Live streamers'; @override - String get mobileAllGames => 'All Partien'; + String get mobileMustBeLoggedIn => 'Du muss ageloggt si fir dës Säit ze gesinn.'; @override - String get mobileRecentSearches => 'Recent searches'; + String get mobileNoSearchResults => 'Keng Resultater'; @override - String get mobileClearButton => 'Clear'; + String get mobileNotFollowingAnyUser => 'You are not following any user.'; + + @override + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,84 +67,82 @@ class AppLocalizationsLb extends AppLocalizations { } @override - String get mobileNoSearchResults => 'Keng Resultater'; + String get mobilePrefMagnifyDraggedPiece => 'Gezunne Figur vergréisseren'; @override - String get mobileAreYouSure => 'Bass de sécher?'; + String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; @override String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Dës Aufgab deelen'; + String get mobilePuzzleStormSubtitle => 'Léis sou vill Aufgabe wéi méiglech an 3 Minutten.'; @override - String get mobileShareGameURL => 'URL vun der Partie deelen'; + String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; @override - String get mobileShareGamePGN => 'PGN deelen'; + String get mobilePuzzleThemesSubtitle => 'Maach Aufgaben aus denge Liiblingserëffnungen oder sich dir een Theema eraus.'; @override - String get mobileSharePositionAsFEN => 'Stellung als FEN deelen'; + String get mobilePuzzlesTab => 'Aufgaben'; @override - String get mobileShowVariations => 'Variante weisen'; + String get mobileRecentSearches => 'Recent searches'; @override - String get mobileHideVariation => 'Variante verstoppen'; + String get mobileSettingsHapticFeedback => 'Haptesche Feedback'; @override - String get mobileShowComments => 'Kommentarer weisen'; + String get mobileSettingsImmersiveMode => 'Immersive Modus'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobileSettingsTab => 'Settings'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; + String get mobileShareGamePGN => 'PGN deelen'; @override - String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; + String get mobileShareGameURL => 'URL vun der Partie deelen'; @override - String get mobileBlindfoldMode => 'Blann'; + String get mobileSharePositionAsFEN => 'Stellung als FEN deelen'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileSharePuzzle => 'Dës Aufgab deelen'; @override - String get mobileCustomGameJoinAGame => 'Join a game'; + String get mobileShowComments => 'Kommentarer weisen'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; + String get mobileShowResult => 'Resultat weisen'; @override - String get mobileSomethingWentWrong => 'Et ass eppes schifgaang.'; + String get mobileShowVariations => 'Variante weisen'; @override - String get mobileShowResult => 'Resultat weisen'; + String get mobileSomethingWentWrong => 'Et ass eppes schifgaang.'; @override - String get mobilePuzzleThemesSubtitle => 'Maach Aufgaben aus denge Liiblingserëffnungen oder sich dir een Theema eraus.'; + String get mobileSystemColors => 'Systemsfaarwen'; @override - String get mobilePuzzleStormSubtitle => 'Léis sou vill Aufgabe wéi méiglech an 3 Minutten.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Moien, $param'; - } + String get mobileToolsTab => 'Tools'; @override - String get mobileGreetingWithoutName => 'Moien'; + String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Gezunne Figur vergréisseren'; + String get mobileWatchTab => 'Watch'; @override String get activityActivity => 'Verlaf'; @@ -535,6 +538,9 @@ class AppLocalizationsLb extends AppLocalizations { @override String get broadcastStandings => 'Standings'; + @override + String get broadcastOfficialStandings => 'Official Standings'; + @override String broadcastIframeHelp(String param) { return 'Méi Optiounen op der $param'; @@ -565,6 +571,36 @@ class AppLocalizationsLb extends AppLocalizations { @override String get broadcastScore => 'Score'; + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + @override String broadcastNbBroadcasts(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -1992,9 +2028,6 @@ class AppLocalizationsLb extends AppLocalizations { @override String get byCPL => 'No CPL'; - @override - String get openStudy => 'Studie opmaachen'; - @override String get enable => 'Aktivéieren'; @@ -2662,9 +2695,6 @@ class AppLocalizationsLb extends AppLocalizations { @override String get unblock => 'Spär ophiewen'; - @override - String get followsYou => 'Followt dir'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 followt elo $param2'; @@ -5424,6 +5454,11 @@ class AppLocalizationsLb extends AppLocalizations { @override String get studyYouCompletedThisLesson => 'Gudd gemaach! Du hues dës Übung ofgeschloss.'; + @override + String studyPerPage(String param) { + return '$param per page'; + } + @override String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( diff --git a/lib/l10n/l10n_lt.dart b/lib/l10n/l10n_lt.dart index df24d46e02..447f0a1a4d 100644 --- a/lib/l10n/l10n_lt.dart +++ b/lib/l10n/l10n_lt.dart @@ -9,52 +9,57 @@ class AppLocalizationsLt extends AppLocalizations { AppLocalizationsLt([String locale = 'lt']) : super(locale); @override - String get mobileHomeTab => 'Home'; + String get mobileAllGames => 'All games'; @override - String get mobilePuzzlesTab => 'Puzzles'; + String get mobileAreYouSure => 'Are you sure?'; @override - String get mobileToolsTab => 'Tools'; + String get mobileBlindfoldMode => 'Blindfold'; @override - String get mobileWatchTab => 'Watch'; + String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @override - String get mobileSettingsTab => 'Settings'; + String get mobileClearButton => 'Clear'; @override - String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; + String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; @override - String get mobileSystemColors => 'System colors'; + String get mobileCustomGameJoinAGame => 'Join a game'; @override String get mobileFeedbackButton => 'Feedback'; @override - String get mobileOkButton => 'OK'; + String mobileGreeting(String param) { + return 'Hello, $param'; + } @override - String get mobileSettingsHapticFeedback => 'Haptic feedback'; + String get mobileGreetingWithoutName => 'Hello'; @override - String get mobileSettingsImmersiveMode => 'Immersive mode'; + String get mobileHideVariation => 'Hide variation'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileHomeTab => 'Home'; @override - String get mobileNotFollowingAnyUser => 'You are not following any user.'; + String get mobileLiveStreamers => 'Live streamers'; @override - String get mobileAllGames => 'All games'; + String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; @override - String get mobileRecentSearches => 'Recent searches'; + String get mobileNoSearchResults => 'No results'; @override - String get mobileClearButton => 'Clear'; + String get mobileNotFollowingAnyUser => 'You are not following any user.'; + + @override + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,84 +67,82 @@ class AppLocalizationsLt extends AppLocalizations { } @override - String get mobileNoSearchResults => 'No results'; + String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; @override - String get mobileAreYouSure => 'Are you sure?'; + String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; @override String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Share this puzzle'; + String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; @override - String get mobileShareGameURL => 'Share game URL'; + String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; @override - String get mobileShareGamePGN => 'Share PGN'; + String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; @override - String get mobileSharePositionAsFEN => 'Share position as FEN'; + String get mobilePuzzlesTab => 'Puzzles'; @override - String get mobileShowVariations => 'Show variations'; + String get mobileRecentSearches => 'Recent searches'; @override - String get mobileHideVariation => 'Hide variation'; + String get mobileSettingsHapticFeedback => 'Haptic feedback'; @override - String get mobileShowComments => 'Show comments'; + String get mobileSettingsImmersiveMode => 'Immersive mode'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobileSettingsTab => 'Settings'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; + String get mobileShareGamePGN => 'Share PGN'; @override - String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; + String get mobileShareGameURL => 'Share game URL'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get mobileSharePositionAsFEN => 'Share position as FEN'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileSharePuzzle => 'Share this puzzle'; @override - String get mobileCustomGameJoinAGame => 'Join a game'; + String get mobileShowComments => 'Show comments'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; + String get mobileShowResult => 'Show result'; @override - String get mobileSomethingWentWrong => 'Something went wrong.'; + String get mobileShowVariations => 'Show variations'; @override - String get mobileShowResult => 'Show result'; + String get mobileSomethingWentWrong => 'Something went wrong.'; @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobileSystemColors => 'System colors'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Hello, $param'; - } + String get mobileToolsTab => 'Tools'; @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Watch'; @override String get activityActivity => 'Veikla'; @@ -569,6 +572,9 @@ class AppLocalizationsLt extends AppLocalizations { @override String get broadcastStandings => 'Standings'; + @override + String get broadcastOfficialStandings => 'Official Standings'; + @override String broadcastIframeHelp(String param) { return 'More options on the $param'; @@ -599,6 +605,36 @@ class AppLocalizationsLt extends AppLocalizations { @override String get broadcastScore => 'Score'; + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + @override String broadcastNbBroadcasts(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -2038,9 +2074,6 @@ class AppLocalizationsLt extends AppLocalizations { @override String get byCPL => 'Pagal įvertį'; - @override - String get openStudy => 'Atverti studiją'; - @override String get enable => 'Įjungti'; @@ -2708,9 +2741,6 @@ class AppLocalizationsLt extends AppLocalizations { @override String get unblock => 'Atblokuoti'; - @override - String get followsYou => 'Seka jus'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 pradėjo sekti $param2'; @@ -5558,6 +5588,11 @@ class AppLocalizationsLt extends AppLocalizations { @override String get studyYouCompletedThisLesson => 'Sveikiname! Jūs pabaigėte šią pamoką.'; + @override + String studyPerPage(String param) { + return '$param per page'; + } + @override String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( diff --git a/lib/l10n/l10n_lv.dart b/lib/l10n/l10n_lv.dart index cc7a53e80c..ecca060c73 100644 --- a/lib/l10n/l10n_lv.dart +++ b/lib/l10n/l10n_lv.dart @@ -9,52 +9,57 @@ class AppLocalizationsLv extends AppLocalizations { AppLocalizationsLv([String locale = 'lv']) : super(locale); @override - String get mobileHomeTab => 'Home'; + String get mobileAllGames => 'All games'; @override - String get mobilePuzzlesTab => 'Puzzles'; + String get mobileAreYouSure => 'Are you sure?'; @override - String get mobileToolsTab => 'Tools'; + String get mobileBlindfoldMode => 'Blindfold'; @override - String get mobileWatchTab => 'Watch'; + String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @override - String get mobileSettingsTab => 'Settings'; + String get mobileClearButton => 'Clear'; @override - String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; + String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; @override - String get mobileSystemColors => 'System colors'; + String get mobileCustomGameJoinAGame => 'Join a game'; @override String get mobileFeedbackButton => 'Feedback'; @override - String get mobileOkButton => 'OK'; + String mobileGreeting(String param) { + return 'Hello, $param'; + } @override - String get mobileSettingsHapticFeedback => 'Haptic feedback'; + String get mobileGreetingWithoutName => 'Hello'; @override - String get mobileSettingsImmersiveMode => 'Immersive mode'; + String get mobileHideVariation => 'Hide variation'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileHomeTab => 'Home'; @override - String get mobileNotFollowingAnyUser => 'You are not following any user.'; + String get mobileLiveStreamers => 'Live streamers'; @override - String get mobileAllGames => 'All games'; + String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; @override - String get mobileRecentSearches => 'Recent searches'; + String get mobileNoSearchResults => 'No results'; @override - String get mobileClearButton => 'Clear'; + String get mobileNotFollowingAnyUser => 'You are not following any user.'; + + @override + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,84 +67,82 @@ class AppLocalizationsLv extends AppLocalizations { } @override - String get mobileNoSearchResults => 'No results'; + String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; @override - String get mobileAreYouSure => 'Are you sure?'; + String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; @override String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Share this puzzle'; + String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; @override - String get mobileShareGameURL => 'Share game URL'; + String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; @override - String get mobileShareGamePGN => 'Share PGN'; + String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; @override - String get mobileSharePositionAsFEN => 'Share position as FEN'; + String get mobilePuzzlesTab => 'Puzzles'; @override - String get mobileShowVariations => 'Show variations'; + String get mobileRecentSearches => 'Recent searches'; @override - String get mobileHideVariation => 'Hide variation'; + String get mobileSettingsHapticFeedback => 'Haptic feedback'; @override - String get mobileShowComments => 'Show comments'; + String get mobileSettingsImmersiveMode => 'Immersive mode'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobileSettingsTab => 'Settings'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; + String get mobileShareGamePGN => 'Share PGN'; @override - String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; + String get mobileShareGameURL => 'Share game URL'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get mobileSharePositionAsFEN => 'Share position as FEN'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileSharePuzzle => 'Share this puzzle'; @override - String get mobileCustomGameJoinAGame => 'Join a game'; + String get mobileShowComments => 'Show comments'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; + String get mobileShowResult => 'Show result'; @override - String get mobileSomethingWentWrong => 'Something went wrong.'; + String get mobileShowVariations => 'Show variations'; @override - String get mobileShowResult => 'Show result'; + String get mobileSomethingWentWrong => 'Something went wrong.'; @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobileSystemColors => 'System colors'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Hello, $param'; - } + String get mobileToolsTab => 'Tools'; @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Watch'; @override String get activityActivity => 'Aktivitāte'; @@ -552,6 +555,9 @@ class AppLocalizationsLv extends AppLocalizations { @override String get broadcastStandings => 'Standings'; + @override + String get broadcastOfficialStandings => 'Official Standings'; + @override String broadcastIframeHelp(String param) { return 'More options on the $param'; @@ -582,6 +588,36 @@ class AppLocalizationsLv extends AppLocalizations { @override String get broadcastScore => 'Score'; + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + @override String broadcastNbBroadcasts(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -2014,9 +2050,6 @@ class AppLocalizationsLv extends AppLocalizations { @override String get byCPL => 'Pēc CPL'; - @override - String get openStudy => 'Atvērt izpēti'; - @override String get enable => 'Iespējot'; @@ -2684,9 +2717,6 @@ class AppLocalizationsLv extends AppLocalizations { @override String get unblock => 'Atbloķēt'; - @override - String get followsYou => 'Sekotāji'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 sāka sekot $param2'; @@ -5490,6 +5520,11 @@ class AppLocalizationsLv extends AppLocalizations { @override String get studyYouCompletedThisLesson => 'Apsveicam! Pabeidzāt šo nodarbību.'; + @override + String studyPerPage(String param) { + return '$param per page'; + } + @override String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( diff --git a/lib/l10n/l10n_mk.dart b/lib/l10n/l10n_mk.dart index 2473af76e7..b6e23ab136 100644 --- a/lib/l10n/l10n_mk.dart +++ b/lib/l10n/l10n_mk.dart @@ -9,52 +9,57 @@ class AppLocalizationsMk extends AppLocalizations { AppLocalizationsMk([String locale = 'mk']) : super(locale); @override - String get mobileHomeTab => 'Home'; + String get mobileAllGames => 'All games'; @override - String get mobilePuzzlesTab => 'Puzzles'; + String get mobileAreYouSure => 'Are you sure?'; @override - String get mobileToolsTab => 'Tools'; + String get mobileBlindfoldMode => 'Blindfold'; @override - String get mobileWatchTab => 'Watch'; + String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @override - String get mobileSettingsTab => 'Settings'; + String get mobileClearButton => 'Clear'; @override - String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; + String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; @override - String get mobileSystemColors => 'Системски бои'; + String get mobileCustomGameJoinAGame => 'Join a game'; @override String get mobileFeedbackButton => 'Повратна информација'; @override - String get mobileOkButton => 'OK'; + String mobileGreeting(String param) { + return 'Hello, $param'; + } @override - String get mobileSettingsHapticFeedback => 'Тактилен фидбек'; + String get mobileGreetingWithoutName => 'Hello'; @override - String get mobileSettingsImmersiveMode => 'Immersive mode'; + String get mobileHideVariation => 'Hide variation'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileHomeTab => 'Home'; @override - String get mobileNotFollowingAnyUser => 'You are not following any user.'; + String get mobileLiveStreamers => 'Live streamers'; @override - String get mobileAllGames => 'All games'; + String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; @override - String get mobileRecentSearches => 'Recent searches'; + String get mobileNoSearchResults => 'No results'; @override - String get mobileClearButton => 'Clear'; + String get mobileNotFollowingAnyUser => 'You are not following any user.'; + + @override + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,84 +67,82 @@ class AppLocalizationsMk extends AppLocalizations { } @override - String get mobileNoSearchResults => 'No results'; + String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; @override - String get mobileAreYouSure => 'Are you sure?'; + String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; @override String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Share this puzzle'; + String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; @override - String get mobileShareGameURL => 'Share game URL'; + String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; @override - String get mobileShareGamePGN => 'Share PGN'; + String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; @override - String get mobileSharePositionAsFEN => 'Share position as FEN'; + String get mobilePuzzlesTab => 'Puzzles'; @override - String get mobileShowVariations => 'Show variations'; + String get mobileRecentSearches => 'Recent searches'; @override - String get mobileHideVariation => 'Hide variation'; + String get mobileSettingsHapticFeedback => 'Тактилен фидбек'; @override - String get mobileShowComments => 'Show comments'; + String get mobileSettingsImmersiveMode => 'Immersive mode'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobileSettingsTab => 'Settings'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; + String get mobileShareGamePGN => 'Share PGN'; @override - String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; + String get mobileShareGameURL => 'Share game URL'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get mobileSharePositionAsFEN => 'Share position as FEN'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileSharePuzzle => 'Share this puzzle'; @override - String get mobileCustomGameJoinAGame => 'Join a game'; + String get mobileShowComments => 'Show comments'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; + String get mobileShowResult => 'Show result'; @override - String get mobileSomethingWentWrong => 'Something went wrong.'; + String get mobileShowVariations => 'Show variations'; @override - String get mobileShowResult => 'Show result'; + String get mobileSomethingWentWrong => 'Something went wrong.'; @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobileSystemColors => 'Системски бои'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Hello, $param'; - } + String get mobileToolsTab => 'Tools'; @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Watch'; @override String get activityActivity => 'Активност'; @@ -535,6 +538,9 @@ class AppLocalizationsMk extends AppLocalizations { @override String get broadcastStandings => 'Standings'; + @override + String get broadcastOfficialStandings => 'Official Standings'; + @override String broadcastIframeHelp(String param) { return 'More options on the $param'; @@ -565,6 +571,36 @@ class AppLocalizationsMk extends AppLocalizations { @override String get broadcastScore => 'Score'; + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + @override String broadcastNbBroadcasts(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -1992,9 +2028,6 @@ class AppLocalizationsMk extends AppLocalizations { @override String get byCPL => 'По CPL'; - @override - String get openStudy => 'Отвори студија'; - @override String get enable => 'Овозможи'; @@ -2662,9 +2695,6 @@ class AppLocalizationsMk extends AppLocalizations { @override String get unblock => 'Одблокирај'; - @override - String get followsYou => 'Те следи'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 почна да го следи $param2'; @@ -5424,6 +5454,11 @@ class AppLocalizationsMk extends AppLocalizations { @override String get studyYouCompletedThisLesson => 'Congratulations! You completed this lesson.'; + @override + String studyPerPage(String param) { + return '$param per page'; + } + @override String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( diff --git a/lib/l10n/l10n_nb.dart b/lib/l10n/l10n_nb.dart index 4afa372d7a..60b8513a24 100644 --- a/lib/l10n/l10n_nb.dart +++ b/lib/l10n/l10n_nb.dart @@ -9,52 +9,57 @@ class AppLocalizationsNb extends AppLocalizations { AppLocalizationsNb([String locale = 'nb']) : super(locale); @override - String get mobileHomeTab => 'Hjem'; + String get mobileAllGames => 'Alle partier'; @override - String get mobilePuzzlesTab => 'Nøtter'; + String get mobileAreYouSure => 'Er du sikker?'; @override - String get mobileToolsTab => 'Verktøy'; + String get mobileBlindfoldMode => 'Blindsjakk'; @override - String get mobileWatchTab => 'Se'; + String get mobileCancelTakebackOffer => 'Avbryt tilbud om å angre'; @override - String get mobileSettingsTab => 'Valg'; + String get mobileClearButton => 'Tøm'; @override - String get mobileMustBeLoggedIn => 'Du må være logget inn for å vise denne siden.'; + String get mobileCorrespondenceClearSavedMove => 'Fjern lagret trekk'; @override - String get mobileSystemColors => 'Systemfarger'; + String get mobileCustomGameJoinAGame => 'Bli med på et parti'; @override String get mobileFeedbackButton => 'Tilbakemeldinger'; @override - String get mobileOkButton => 'Ok'; + String mobileGreeting(String param) { + return 'Hei, $param'; + } @override - String get mobileSettingsHapticFeedback => 'Haptiske tilbakemeldinger'; + String get mobileGreetingWithoutName => 'Hei'; @override - String get mobileSettingsImmersiveMode => 'Fordypelsesmodus'; + String get mobileHideVariation => 'Skjul variant'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Skjul systemgrensesnittet mens du spiller. Bruk dette hvis du blir forstyrret av systemets navigasjonsgester på skjermkanten. Gjelder for partier og Puzzle Storm.'; + String get mobileHomeTab => 'Hjem'; @override - String get mobileNotFollowingAnyUser => 'Du følger ingen brukere.'; + String get mobileLiveStreamers => 'Direktestrømmere'; @override - String get mobileAllGames => 'Alle partier'; + String get mobileMustBeLoggedIn => 'Du må være logget inn for å vise denne siden.'; @override - String get mobileRecentSearches => 'Nylige søk'; + String get mobileNoSearchResults => 'Ingen treff'; @override - String get mobileClearButton => 'Tøm'; + String get mobileNotFollowingAnyUser => 'Du følger ingen brukere.'; + + @override + String get mobileOkButton => 'Ok'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,84 +67,82 @@ class AppLocalizationsNb extends AppLocalizations { } @override - String get mobileNoSearchResults => 'Ingen treff'; + String get mobilePrefMagnifyDraggedPiece => 'Forstørr brikker når de dras'; @override - String get mobileAreYouSure => 'Er du sikker?'; + String get mobilePuzzleStormConfirmEndRun => 'Vil du avslutte denne runden?'; @override - String get mobilePuzzleStreakAbortWarning => 'Du mister rekken og poengsummen din blir lagret.'; + String get mobilePuzzleStormFilterNothingToShow => 'Ingenting her, endre filteret'; @override String get mobilePuzzleStormNothingToShow => 'Ingenting her. Spill noen runder med Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Del denne sjakknøtten'; + String get mobilePuzzleStormSubtitle => 'Løs så mange sjakknøtter du klarer i løpet av 3 minutter.'; @override - String get mobileShareGameURL => 'Del URL til partiet'; + String get mobilePuzzleStreakAbortWarning => 'Du mister rekken og poengsummen din blir lagret.'; @override - String get mobileShareGamePGN => 'Del PGN'; + String get mobilePuzzleThemesSubtitle => 'Spill sjakknøtter fra favorittåpningene dine, eller velg et tema.'; @override - String get mobileSharePositionAsFEN => 'Del stillingen som FEN'; + String get mobilePuzzlesTab => 'Nøtter'; @override - String get mobileShowVariations => 'Vis varianter'; + String get mobileRecentSearches => 'Nylige søk'; @override - String get mobileHideVariation => 'Skjul variant'; + String get mobileSettingsHapticFeedback => 'Haptiske tilbakemeldinger'; @override - String get mobileShowComments => 'Vis kommentarer'; + String get mobileSettingsImmersiveMode => 'Fordypelsesmodus'; @override - String get mobilePuzzleStormConfirmEndRun => 'Vil du avslutte denne runden?'; + String get mobileSettingsImmersiveModeSubtitle => 'Skjul systemgrensesnittet mens du spiller. Bruk dette hvis du blir forstyrret av systemets navigasjonsgester på skjermkanten. Gjelder for partier og Puzzle Storm.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Ingenting her, endre filteret'; + String get mobileSettingsTab => 'Valg'; @override - String get mobileCancelTakebackOffer => 'Avbryt tilbud om å angre'; + String get mobileShareGamePGN => 'Del PGN'; @override - String get mobileWaitingForOpponentToJoin => 'Venter på motstanderen ...'; + String get mobileShareGameURL => 'Del URL til partiet'; @override - String get mobileBlindfoldMode => 'Blindsjakk'; + String get mobileSharePositionAsFEN => 'Del stillingen som FEN'; @override - String get mobileLiveStreamers => 'Direktestrømmere'; + String get mobileSharePuzzle => 'Del denne sjakknøtten'; @override - String get mobileCustomGameJoinAGame => 'Bli med på et parti'; + String get mobileShowComments => 'Vis kommentarer'; @override - String get mobileCorrespondenceClearSavedMove => 'Fjern lagret trekk'; + String get mobileShowResult => 'Vis resultat'; @override - String get mobileSomethingWentWrong => 'Noe gikk galt.'; + String get mobileShowVariations => 'Vis varianter'; @override - String get mobileShowResult => 'Vis resultat'; + String get mobileSomethingWentWrong => 'Noe gikk galt.'; @override - String get mobilePuzzleThemesSubtitle => 'Spill sjakknøtter fra favorittåpningene dine, eller velg et tema.'; + String get mobileSystemColors => 'Systemfarger'; @override - String get mobilePuzzleStormSubtitle => 'Løs så mange sjakknøtter du klarer i løpet av 3 minutter.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Hei, $param'; - } + String get mobileToolsTab => 'Verktøy'; @override - String get mobileGreetingWithoutName => 'Hei'; + String get mobileWaitingForOpponentToJoin => 'Venter på motstanderen ...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Forstørr brikker når de dras'; + String get mobileWatchTab => 'Se'; @override String get activityActivity => 'Aktivitet'; @@ -535,6 +538,9 @@ class AppLocalizationsNb extends AppLocalizations { @override String get broadcastStandings => 'Resultatliste'; + @override + String get broadcastOfficialStandings => 'Official Standings'; + @override String broadcastIframeHelp(String param) { return 'Flere alternativer på $param'; @@ -565,6 +571,36 @@ class AppLocalizationsNb extends AppLocalizations { @override String get broadcastScore => 'Poengsum'; + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + @override String broadcastNbBroadcasts(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -1992,9 +2028,6 @@ class AppLocalizationsNb extends AppLocalizations { @override String get byCPL => 'Etter CBT'; - @override - String get openStudy => 'Åpne studie'; - @override String get enable => 'Aktiver'; @@ -2662,9 +2695,6 @@ class AppLocalizationsNb extends AppLocalizations { @override String get unblock => 'Fjern blokkering'; - @override - String get followsYou => 'Følger deg'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 begynte å følge $param2'; @@ -5424,6 +5454,11 @@ class AppLocalizationsNb extends AppLocalizations { @override String get studyYouCompletedThisLesson => 'Gratulerer! Du har fullført denne leksjonen.'; + @override + String studyPerPage(String param) { + return '$param per page'; + } + @override String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( diff --git a/lib/l10n/l10n_nl.dart b/lib/l10n/l10n_nl.dart index ff2469fcf7..5b5b6a0579 100644 --- a/lib/l10n/l10n_nl.dart +++ b/lib/l10n/l10n_nl.dart @@ -9,52 +9,57 @@ class AppLocalizationsNl extends AppLocalizations { AppLocalizationsNl([String locale = 'nl']) : super(locale); @override - String get mobileHomeTab => 'Startscherm'; + String get mobileAllGames => 'Alle partijen'; @override - String get mobilePuzzlesTab => 'Puzzels'; + String get mobileAreYouSure => 'Weet je het zeker?'; @override - String get mobileToolsTab => 'Gereedschap'; + String get mobileBlindfoldMode => 'Geblinddoekt'; @override - String get mobileWatchTab => 'Kijken'; + String get mobileCancelTakebackOffer => 'Terugnameaanbod annuleren'; @override - String get mobileSettingsTab => 'Instellingen'; + String get mobileClearButton => 'Wissen'; @override - String get mobileMustBeLoggedIn => 'Je moet ingelogd zijn om deze pagina te bekijken.'; + String get mobileCorrespondenceClearSavedMove => 'Opgeslagen zet wissen'; @override - String get mobileSystemColors => 'Systeemkleuren'; + String get mobileCustomGameJoinAGame => 'Een partij beginnen'; @override String get mobileFeedbackButton => 'Feedback'; @override - String get mobileOkButton => 'OK'; + String mobileGreeting(String param) { + return 'Hallo, $param'; + } @override - String get mobileSettingsHapticFeedback => 'Haptische feedback'; + String get mobileGreetingWithoutName => 'Hallo'; @override - String get mobileSettingsImmersiveMode => 'Volledig scherm-modus'; + String get mobileHideVariation => 'Verberg varianten'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Systeem-UI verbergen tijdens het spelen. Gebruik dit als je last hebt van de navigatiegebaren aan de randen van het scherm. Dit is van toepassing op spel- en Puzzle Storm schermen.'; + String get mobileHomeTab => 'Startscherm'; @override - String get mobileNotFollowingAnyUser => 'U volgt geen gebruiker.'; + String get mobileLiveStreamers => 'Live streamers'; @override - String get mobileAllGames => 'Alle partijen'; + String get mobileMustBeLoggedIn => 'Je moet ingelogd zijn om deze pagina te bekijken.'; @override - String get mobileRecentSearches => 'Recente zoekopdrachten'; + String get mobileNoSearchResults => 'Geen resultaten'; @override - String get mobileClearButton => 'Wissen'; + String get mobileNotFollowingAnyUser => 'U volgt geen gebruiker.'; + + @override + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,84 +67,82 @@ class AppLocalizationsNl extends AppLocalizations { } @override - String get mobileNoSearchResults => 'Geen resultaten'; + String get mobilePrefMagnifyDraggedPiece => 'Versleept stuk vergroot weergeven'; @override - String get mobileAreYouSure => 'Weet je het zeker?'; + String get mobilePuzzleStormConfirmEndRun => 'Wil je deze reeks beëindigen?'; @override - String get mobilePuzzleStreakAbortWarning => 'Je verliest je huidige reeks en de score wordt opgeslagen.'; + String get mobilePuzzleStormFilterNothingToShow => 'Niets te tonen, wijzig de filters'; @override String get mobilePuzzleStormNothingToShow => 'Niets om te tonen. Speel een aantal reeksen Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Deze puzzel delen'; + String get mobilePuzzleStormSubtitle => 'Los zoveel mogelijk puzzels op in 3 minuten.'; @override - String get mobileShareGameURL => 'Partij URL delen'; + String get mobilePuzzleStreakAbortWarning => 'Je verliest je huidige reeks en de score wordt opgeslagen.'; @override - String get mobileShareGamePGN => 'PGN delen'; + String get mobilePuzzleThemesSubtitle => 'Speel puzzels uit je favorieten openingen, of kies een thema.'; @override - String get mobileSharePositionAsFEN => 'Stelling delen als FEN'; + String get mobilePuzzlesTab => 'Puzzels'; @override - String get mobileShowVariations => 'Toon varianten'; + String get mobileRecentSearches => 'Recente zoekopdrachten'; @override - String get mobileHideVariation => 'Verberg varianten'; + String get mobileSettingsHapticFeedback => 'Haptische feedback'; @override - String get mobileShowComments => 'Opmerkingen weergeven'; + String get mobileSettingsImmersiveMode => 'Volledig scherm-modus'; @override - String get mobilePuzzleStormConfirmEndRun => 'Wil je deze reeks beëindigen?'; + String get mobileSettingsImmersiveModeSubtitle => 'Systeem-UI verbergen tijdens het spelen. Gebruik dit als je last hebt van de navigatiegebaren aan de randen van het scherm. Dit is van toepassing op spel- en Puzzle Storm schermen.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Niets te tonen, wijzig de filters'; + String get mobileSettingsTab => 'Instellingen'; @override - String get mobileCancelTakebackOffer => 'Terugnameaanbod annuleren'; + String get mobileShareGamePGN => 'PGN delen'; @override - String get mobileWaitingForOpponentToJoin => 'Wachten op een tegenstander...'; + String get mobileShareGameURL => 'Partij URL delen'; @override - String get mobileBlindfoldMode => 'Geblinddoekt'; + String get mobileSharePositionAsFEN => 'Stelling delen als FEN'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileSharePuzzle => 'Deze puzzel delen'; @override - String get mobileCustomGameJoinAGame => 'Een partij beginnen'; + String get mobileShowComments => 'Opmerkingen weergeven'; @override - String get mobileCorrespondenceClearSavedMove => 'Opgeslagen zet wissen'; + String get mobileShowResult => 'Toon resultaat'; @override - String get mobileSomethingWentWrong => 'Er is iets fout gegaan.'; + String get mobileShowVariations => 'Toon varianten'; @override - String get mobileShowResult => 'Toon resultaat'; + String get mobileSomethingWentWrong => 'Er is iets fout gegaan.'; @override - String get mobilePuzzleThemesSubtitle => 'Speel puzzels uit je favorieten openingen, of kies een thema.'; + String get mobileSystemColors => 'Systeemkleuren'; @override - String get mobilePuzzleStormSubtitle => 'Los zoveel mogelijk puzzels op in 3 minuten.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Hallo, $param'; - } + String get mobileToolsTab => 'Gereedschap'; @override - String get mobileGreetingWithoutName => 'Hallo'; + String get mobileWaitingForOpponentToJoin => 'Wachten op een tegenstander...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Versleept stuk vergroot weergeven'; + String get mobileWatchTab => 'Kijken'; @override String get activityActivity => 'Activiteit'; @@ -535,6 +538,9 @@ class AppLocalizationsNl extends AppLocalizations { @override String get broadcastStandings => 'Klassement'; + @override + String get broadcastOfficialStandings => 'Official Standings'; + @override String broadcastIframeHelp(String param) { return 'Meer opties voor de $param'; @@ -565,6 +571,36 @@ class AppLocalizationsNl extends AppLocalizations { @override String get broadcastScore => 'Score'; + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + @override String broadcastNbBroadcasts(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -1992,9 +2028,6 @@ class AppLocalizationsNl extends AppLocalizations { @override String get byCPL => 'Door CPL'; - @override - String get openStudy => 'Open Study'; - @override String get enable => 'Aanzetten'; @@ -2662,9 +2695,6 @@ class AppLocalizationsNl extends AppLocalizations { @override String get unblock => 'Deblokkeren'; - @override - String get followsYou => 'Volgt u'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 volgt nu $param2'; @@ -5424,6 +5454,11 @@ class AppLocalizationsNl extends AppLocalizations { @override String get studyYouCompletedThisLesson => 'Gefeliciteerd! Je hebt deze les voltooid.'; + @override + String studyPerPage(String param) { + return '$param per page'; + } + @override String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( diff --git a/lib/l10n/l10n_nn.dart b/lib/l10n/l10n_nn.dart index 003c052e73..b13793d594 100644 --- a/lib/l10n/l10n_nn.dart +++ b/lib/l10n/l10n_nn.dart @@ -9,52 +9,57 @@ class AppLocalizationsNn extends AppLocalizations { AppLocalizationsNn([String locale = 'nn']) : super(locale); @override - String get mobileHomeTab => 'Startside'; + String get mobileAllGames => 'Alle spel'; @override - String get mobilePuzzlesTab => 'Oppgåver'; + String get mobileAreYouSure => 'Er du sikker?'; @override - String get mobileToolsTab => 'Verktøy'; + String get mobileBlindfoldMode => 'Blindsjakk'; @override - String get mobileWatchTab => 'Sjå'; + String get mobileCancelTakebackOffer => 'Avbryt tilbud om angrerett'; @override - String get mobileSettingsTab => 'Innstillingar'; + String get mobileClearButton => 'Tøm'; @override - String get mobileMustBeLoggedIn => 'Du må vera innlogga for å sjå denne sida.'; + String get mobileCorrespondenceClearSavedMove => 'Fjern lagra trekk'; @override - String get mobileSystemColors => 'Systemfargar'; + String get mobileCustomGameJoinAGame => 'Bli med på eit parti'; @override String get mobileFeedbackButton => 'Tilbakemelding'; @override - String get mobileOkButton => 'Ok'; + String mobileGreeting(String param) { + return 'Hei $param'; + } @override - String get mobileSettingsHapticFeedback => 'Haptisk tilbakemelding'; + String get mobileGreetingWithoutName => 'Hei'; @override - String get mobileSettingsImmersiveMode => 'Immersiv modus'; + String get mobileHideVariation => 'Skjul variant'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Skjul system-UI mens du spelar. Bruk dette dersom systemet sine navigasjonsrørsler ved skjermkanten forstyrrar deg. Gjelder skjermbileta for spel og oppgåvestorm.'; + String get mobileHomeTab => 'Startside'; @override - String get mobileNotFollowingAnyUser => 'Du følgjer ingen brukarar.'; + String get mobileLiveStreamers => 'Direkte strøymarar'; @override - String get mobileAllGames => 'Alle spel'; + String get mobileMustBeLoggedIn => 'Du må vera innlogga for å sjå denne sida.'; @override - String get mobileRecentSearches => 'Nylege søk'; + String get mobileNoSearchResults => 'Ingen resultat'; @override - String get mobileClearButton => 'Tøm'; + String get mobileNotFollowingAnyUser => 'Du følgjer ingen brukarar.'; + + @override + String get mobileOkButton => 'Ok'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,84 +67,82 @@ class AppLocalizationsNn extends AppLocalizations { } @override - String get mobileNoSearchResults => 'Ingen resultat'; + String get mobilePrefMagnifyDraggedPiece => 'Forstørr brikke som vert trekt'; @override - String get mobileAreYouSure => 'Er du sikker?'; + String get mobilePuzzleStormConfirmEndRun => 'Vil du avslutte dette løpet?'; @override - String get mobilePuzzleStreakAbortWarning => 'Du vil mista din noverande vinstrekke og poengsummen din vert lagra.'; + String get mobilePuzzleStormFilterNothingToShow => 'Ikkje noko å syna, ver venleg å endre filtera'; @override String get mobilePuzzleStormNothingToShow => 'Ikkje noko å visa. Spel nokre omgangar Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Del denne oppgåva'; + String get mobilePuzzleStormSubtitle => 'Løys så mange oppgåver som du maktar på tre minutt.'; @override - String get mobileShareGameURL => 'Del URLen til partiet'; + String get mobilePuzzleStreakAbortWarning => 'Du vil mista din noverande vinstrekke og poengsummen din vert lagra.'; @override - String get mobileShareGamePGN => 'Del PGN'; + String get mobilePuzzleThemesSubtitle => 'Spel oppgåver frå favorittopningane dine, eller velg eit tema.'; @override - String get mobileSharePositionAsFEN => 'Del stilling som FEN'; + String get mobilePuzzlesTab => 'Oppgåver'; @override - String get mobileShowVariations => 'Vis variantpilar'; + String get mobileRecentSearches => 'Nylege søk'; @override - String get mobileHideVariation => 'Skjul variant'; + String get mobileSettingsHapticFeedback => 'Haptisk tilbakemelding'; @override - String get mobileShowComments => 'Vis kommentarar'; + String get mobileSettingsImmersiveMode => 'Immersiv modus'; @override - String get mobilePuzzleStormConfirmEndRun => 'Vil du avslutte dette løpet?'; + String get mobileSettingsImmersiveModeSubtitle => 'Skjul system-UI mens du spelar. Bruk dette dersom systemet sine navigasjonsrørsler ved skjermkanten forstyrrar deg. Gjelder skjermbileta for spel og oppgåvestorm.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Ikkje noko å syna, ver venleg å endre filtera'; + String get mobileSettingsTab => 'Innstillingar'; @override - String get mobileCancelTakebackOffer => 'Avbryt tilbud om angrerett'; + String get mobileShareGamePGN => 'Del PGN'; @override - String get mobileWaitingForOpponentToJoin => 'Ventar på motspelar...'; + String get mobileShareGameURL => 'Del URLen til partiet'; @override - String get mobileBlindfoldMode => 'Blindsjakk'; + String get mobileSharePositionAsFEN => 'Del stilling som FEN'; @override - String get mobileLiveStreamers => 'Direkte strøymarar'; + String get mobileSharePuzzle => 'Del denne oppgåva'; @override - String get mobileCustomGameJoinAGame => 'Bli med på eit parti'; + String get mobileShowComments => 'Vis kommentarar'; @override - String get mobileCorrespondenceClearSavedMove => 'Fjern lagra trekk'; + String get mobileShowResult => 'Vis resultat'; @override - String get mobileSomethingWentWrong => 'Det oppsto ein feil.'; + String get mobileShowVariations => 'Vis variantpilar'; @override - String get mobileShowResult => 'Vis resultat'; + String get mobileSomethingWentWrong => 'Det oppsto ein feil.'; @override - String get mobilePuzzleThemesSubtitle => 'Spel oppgåver frå favorittopningane dine, eller velg eit tema.'; + String get mobileSystemColors => 'Systemfargar'; @override - String get mobilePuzzleStormSubtitle => 'Løys så mange oppgåver som du maktar på tre minutt.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Hei $param'; - } + String get mobileToolsTab => 'Verktøy'; @override - String get mobileGreetingWithoutName => 'Hei'; + String get mobileWaitingForOpponentToJoin => 'Ventar på motspelar...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Forstørr brikke som vert trekt'; + String get mobileWatchTab => 'Sjå'; @override String get activityActivity => 'Aktivitet'; @@ -535,6 +538,9 @@ class AppLocalizationsNn extends AppLocalizations { @override String get broadcastStandings => 'Resultat'; + @override + String get broadcastOfficialStandings => 'Official Standings'; + @override String broadcastIframeHelp(String param) { return 'Fleire alternativ på $param'; @@ -565,6 +571,36 @@ class AppLocalizationsNn extends AppLocalizations { @override String get broadcastScore => 'Poengskår'; + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + @override String broadcastNbBroadcasts(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -1992,9 +2028,6 @@ class AppLocalizationsNn extends AppLocalizations { @override String get byCPL => 'CPL'; - @override - String get openStudy => 'Opne studie'; - @override String get enable => 'Aktiver'; @@ -2662,9 +2695,6 @@ class AppLocalizationsNn extends AppLocalizations { @override String get unblock => 'Fjern blokkering'; - @override - String get followsYou => 'Følgjer deg'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 byrja å følgja $param2'; @@ -5424,6 +5454,11 @@ class AppLocalizationsNn extends AppLocalizations { @override String get studyYouCompletedThisLesson => 'Gratulerar! Du har fullført denne leksjonen.'; + @override + String studyPerPage(String param) { + return '$param per page'; + } + @override String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( diff --git a/lib/l10n/l10n_pl.dart b/lib/l10n/l10n_pl.dart index d1148a26d7..c7d286ebb8 100644 --- a/lib/l10n/l10n_pl.dart +++ b/lib/l10n/l10n_pl.dart @@ -9,52 +9,57 @@ class AppLocalizationsPl extends AppLocalizations { AppLocalizationsPl([String locale = 'pl']) : super(locale); @override - String get mobileHomeTab => 'Start'; + String get mobileAllGames => 'Wszystkie partie'; @override - String get mobilePuzzlesTab => 'Zadania'; + String get mobileAreYouSure => 'Jesteś pewien?'; @override - String get mobileToolsTab => 'Narzędzia'; + String get mobileBlindfoldMode => 'Gra na ślepo'; @override - String get mobileWatchTab => 'Oglądaj'; + String get mobileCancelTakebackOffer => 'Anuluj prośbę cofnięcia ruchu'; @override - String get mobileSettingsTab => 'Ustawienia'; + String get mobileClearButton => 'Wyczyść'; @override - String get mobileMustBeLoggedIn => 'Musisz być zalogowany, aby wyświetlić tę stronę.'; + String get mobileCorrespondenceClearSavedMove => 'Usuń zapisany ruch'; @override - String get mobileSystemColors => 'Kolory systemowe'; + String get mobileCustomGameJoinAGame => 'Dołącz do partii'; @override String get mobileFeedbackButton => 'Opinie'; @override - String get mobileOkButton => 'OK'; + String mobileGreeting(String param) { + return 'Witaj $param'; + } @override - String get mobileSettingsHapticFeedback => 'Wibracja przy dotknięciu'; + String get mobileGreetingWithoutName => 'Witaj'; @override - String get mobileSettingsImmersiveMode => 'Tryb pełnoekranowy'; + String get mobileHideVariation => 'Ukryj wariant'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Ukryj interfejs użytkownika podczas gry. Użyj tego, jeśli rozpraszają Cię elementy nawigacji systemu na krawędziach ekranu. Dotyczy ekranów gry i rozwiązywania zadań.'; + String get mobileHomeTab => 'Start'; @override - String get mobileNotFollowingAnyUser => 'Nie obserwujesz żadnego gracza.'; + String get mobileLiveStreamers => 'Aktywni streamerzy'; @override - String get mobileAllGames => 'Wszystkie partie'; + String get mobileMustBeLoggedIn => 'Musisz być zalogowany, aby wyświetlić tę stronę.'; @override - String get mobileRecentSearches => 'Ostatnio wyszukiwane'; + String get mobileNoSearchResults => 'Brak wyników'; @override - String get mobileClearButton => 'Wyczyść'; + String get mobileNotFollowingAnyUser => 'Nie obserwujesz żadnego gracza.'; + + @override + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,84 +67,82 @@ class AppLocalizationsPl extends AppLocalizations { } @override - String get mobileNoSearchResults => 'Brak wyników'; + String get mobilePrefMagnifyDraggedPiece => 'Powiększ przeciąganą bierkę'; @override - String get mobileAreYouSure => 'Jesteś pewien?'; + String get mobilePuzzleStormConfirmEndRun => 'Czy chcesz zakończyć tę serię?'; @override - String get mobilePuzzleStreakAbortWarning => 'Przerwiesz swoją dobrą passę, a Twój wynik zostanie zapisany.'; + String get mobilePuzzleStormFilterNothingToShow => 'Brak wyników, zmień proszę filtry'; @override String get mobilePuzzleStormNothingToShow => 'Nic do wyświetlenia. Rozegraj kilka serii.'; @override - String get mobileSharePuzzle => 'Udostępnij to zadanie'; + String get mobilePuzzleStormSubtitle => 'Rozwiąż jak najwięcej zadań w ciągu 3 minut.'; @override - String get mobileShareGameURL => 'Udostępnij adres URL partii'; + String get mobilePuzzleStreakAbortWarning => 'Przerwiesz swoją dobrą passę, a Twój wynik zostanie zapisany.'; @override - String get mobileShareGamePGN => 'Udostępnij PGN'; + String get mobilePuzzleThemesSubtitle => 'Rozwiąż zadania z ulubionego debiutu lub wybierz motyw.'; @override - String get mobileSharePositionAsFEN => 'Udostępnij pozycję jako FEN'; + String get mobilePuzzlesTab => 'Zadania'; @override - String get mobileShowVariations => 'Pokaż warianty'; + String get mobileRecentSearches => 'Ostatnio wyszukiwane'; @override - String get mobileHideVariation => 'Ukryj wariant'; + String get mobileSettingsHapticFeedback => 'Wibracja przy dotknięciu'; @override - String get mobileShowComments => 'Pokaż komentarze'; + String get mobileSettingsImmersiveMode => 'Tryb pełnoekranowy'; @override - String get mobilePuzzleStormConfirmEndRun => 'Czy chcesz zakończyć tę serię?'; + String get mobileSettingsImmersiveModeSubtitle => 'Ukryj interfejs użytkownika podczas gry. Użyj tego, jeśli rozpraszają Cię elementy nawigacji systemu na krawędziach ekranu. Dotyczy ekranów gry i rozwiązywania zadań.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Brak wyników, zmień proszę filtry'; + String get mobileSettingsTab => 'Ustawienia'; @override - String get mobileCancelTakebackOffer => 'Anuluj prośbę cofnięcia ruchu'; + String get mobileShareGamePGN => 'Udostępnij PGN'; @override - String get mobileWaitingForOpponentToJoin => 'Oczekiwanie na dołączenie przeciwnika...'; + String get mobileShareGameURL => 'Udostępnij adres URL partii'; @override - String get mobileBlindfoldMode => 'Gra na ślepo'; + String get mobileSharePositionAsFEN => 'Udostępnij pozycję jako FEN'; @override - String get mobileLiveStreamers => 'Aktywni streamerzy'; + String get mobileSharePuzzle => 'Udostępnij to zadanie'; @override - String get mobileCustomGameJoinAGame => 'Dołącz do partii'; + String get mobileShowComments => 'Pokaż komentarze'; @override - String get mobileCorrespondenceClearSavedMove => 'Usuń zapisany ruch'; + String get mobileShowResult => 'Pokaż wynik'; @override - String get mobileSomethingWentWrong => 'Coś poszło nie tak.'; + String get mobileShowVariations => 'Pokaż warianty'; @override - String get mobileShowResult => 'Pokaż wynik'; + String get mobileSomethingWentWrong => 'Coś poszło nie tak.'; @override - String get mobilePuzzleThemesSubtitle => 'Rozwiąż zadania z ulubionego debiutu lub wybierz motyw.'; + String get mobileSystemColors => 'Kolory systemowe'; @override - String get mobilePuzzleStormSubtitle => 'Rozwiąż jak najwięcej zadań w ciągu 3 minut.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Witaj $param'; - } + String get mobileToolsTab => 'Narzędzia'; @override - String get mobileGreetingWithoutName => 'Witaj'; + String get mobileWaitingForOpponentToJoin => 'Oczekiwanie na dołączenie przeciwnika...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Powiększ przeciąganą bierkę'; + String get mobileWatchTab => 'Oglądaj'; @override String get activityActivity => 'Aktywność'; @@ -571,6 +574,9 @@ class AppLocalizationsPl extends AppLocalizations { @override String get broadcastStandings => 'Klasyfikacja'; + @override + String get broadcastOfficialStandings => 'Official Standings'; + @override String broadcastIframeHelp(String param) { return 'Więcej opcji na $param'; @@ -601,6 +607,36 @@ class AppLocalizationsPl extends AppLocalizations { @override String get broadcastScore => 'Wynik'; + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + @override String broadcastNbBroadcasts(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -2040,9 +2076,6 @@ class AppLocalizationsPl extends AppLocalizations { @override String get byCPL => 'Wg SCP'; - @override - String get openStudy => 'Otwórz opracowanie'; - @override String get enable => 'Włącz'; @@ -2710,9 +2743,6 @@ class AppLocalizationsPl extends AppLocalizations { @override String get unblock => 'Odblokuj'; - @override - String get followsYou => 'Obserwuje Cię'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 obserwuje $param2'; @@ -5560,6 +5590,11 @@ class AppLocalizationsPl extends AppLocalizations { @override String get studyYouCompletedThisLesson => 'Gratulacje! Ukończono tę lekcję.'; + @override + String studyPerPage(String param) { + return '$param per page'; + } + @override String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( diff --git a/lib/l10n/l10n_pt.dart b/lib/l10n/l10n_pt.dart index e933087200..a61f8f9bc8 100644 --- a/lib/l10n/l10n_pt.dart +++ b/lib/l10n/l10n_pt.dart @@ -9,52 +9,57 @@ class AppLocalizationsPt extends AppLocalizations { AppLocalizationsPt([String locale = 'pt']) : super(locale); @override - String get mobileHomeTab => 'Início'; + String get mobileAllGames => 'Todos os jogos'; @override - String get mobilePuzzlesTab => 'Problemas'; + String get mobileAreYouSure => 'Tens a certeza?'; @override - String get mobileToolsTab => 'Tools'; + String get mobileBlindfoldMode => 'De olhos vendados'; @override - String get mobileWatchTab => 'Assistir'; + String get mobileCancelTakebackOffer => 'Cancelar pedido de voltar'; @override - String get mobileSettingsTab => 'Definições'; + String get mobileClearButton => 'Limpar'; @override - String get mobileMustBeLoggedIn => 'Tem de iniciar sessão para visualizar esta página.'; + String get mobileCorrespondenceClearSavedMove => 'Limpar movimento salvo'; @override - String get mobileSystemColors => 'Cores do sistema'; + String get mobileCustomGameJoinAGame => 'Entrar num jogo'; @override String get mobileFeedbackButton => 'Feedback'; @override - String get mobileOkButton => 'OK'; + String mobileGreeting(String param) { + return 'Olá, $param'; + } @override - String get mobileSettingsHapticFeedback => 'Feedback tátil'; + String get mobileGreetingWithoutName => 'Olá'; @override - String get mobileSettingsImmersiveMode => 'Modo imersivo'; + String get mobileHideVariation => 'Ocultar variação'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Ocultar a interface do sistema durante o jogo. Utiliza esta opção se sentires incomodado com os gestos de navegação do sistema nas extremidades do ecrã. Aplica-se aos ecrãs de jogo e do Puzzle Storm.'; + String get mobileHomeTab => 'Início'; @override - String get mobileNotFollowingAnyUser => 'Não segues nenhum utilizador.'; + String get mobileLiveStreamers => 'Streamers em direto'; @override - String get mobileAllGames => 'Todos os jogos'; + String get mobileMustBeLoggedIn => 'Tem de iniciar sessão para visualizar esta página.'; @override - String get mobileRecentSearches => 'Pesquisas recentes'; + String get mobileNoSearchResults => 'Sem resultados'; @override - String get mobileClearButton => 'Limpar'; + String get mobileNotFollowingAnyUser => 'Não segues nenhum utilizador.'; + + @override + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,84 +67,82 @@ class AppLocalizationsPt extends AppLocalizations { } @override - String get mobileNoSearchResults => 'Sem resultados'; + String get mobilePrefMagnifyDraggedPiece => 'Ampliar peça arrastada'; @override - String get mobileAreYouSure => 'Tens a certeza?'; + String get mobilePuzzleStormConfirmEndRun => 'Queres terminar esta corrida?'; @override - String get mobilePuzzleStreakAbortWarning => 'Perderas a tua sequência atual e a pontuação será salva.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nada para mostrar, por favor, altera os filtros'; @override String get mobilePuzzleStormNothingToShow => 'Nada para mostrar. Joga alguns Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Partilhar este problema'; + String get mobilePuzzleStormSubtitle => 'Resolve quantos problemas for possível em 3 minutos.'; @override - String get mobileShareGameURL => 'Partilhar o URL do jogo'; + String get mobilePuzzleStreakAbortWarning => 'Perderas a tua sequência atual e a pontuação será salva.'; @override - String get mobileShareGamePGN => 'Partilhar PGN'; + String get mobilePuzzleThemesSubtitle => 'Joga problemas das tuas aberturas favoritas, ou escolhe um tema.'; @override - String get mobileSharePositionAsFEN => 'Partilhar posição como FEN'; + String get mobilePuzzlesTab => 'Problemas'; @override - String get mobileShowVariations => 'Mostrar variações'; + String get mobileRecentSearches => 'Pesquisas recentes'; @override - String get mobileHideVariation => 'Ocultar variação'; + String get mobileSettingsHapticFeedback => 'Feedback tátil'; @override - String get mobileShowComments => 'Mostrar comentários'; + String get mobileSettingsImmersiveMode => 'Modo imersivo'; @override - String get mobilePuzzleStormConfirmEndRun => 'Queres terminar esta corrida?'; + String get mobileSettingsImmersiveModeSubtitle => 'Ocultar a interface do sistema durante o jogo. Utiliza esta opção se sentires incomodado com os gestos de navegação do sistema nas extremidades do ecrã. Aplica-se aos ecrãs de jogo e do Puzzle Storm.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nada para mostrar, por favor, altera os filtros'; + String get mobileSettingsTab => 'Definições'; @override - String get mobileCancelTakebackOffer => 'Cancelar pedido de voltar'; + String get mobileShareGamePGN => 'Partilhar PGN'; @override - String get mobileWaitingForOpponentToJoin => 'À espera do adversário entrar...'; + String get mobileShareGameURL => 'Partilhar o URL do jogo'; @override - String get mobileBlindfoldMode => 'De olhos vendados'; + String get mobileSharePositionAsFEN => 'Partilhar posição como FEN'; @override - String get mobileLiveStreamers => 'Streamers em direto'; + String get mobileSharePuzzle => 'Partilhar este problema'; @override - String get mobileCustomGameJoinAGame => 'Entrar num jogo'; + String get mobileShowComments => 'Mostrar comentários'; @override - String get mobileCorrespondenceClearSavedMove => 'Limpar movimento salvo'; + String get mobileShowResult => 'Mostrar resultado'; @override - String get mobileSomethingWentWrong => 'Algo deu errado.'; + String get mobileShowVariations => 'Mostrar variações'; @override - String get mobileShowResult => 'Mostrar resultado'; + String get mobileSomethingWentWrong => 'Algo deu errado.'; @override - String get mobilePuzzleThemesSubtitle => 'Joga problemas das tuas aberturas favoritas, ou escolhe um tema.'; + String get mobileSystemColors => 'Cores do sistema'; @override - String get mobilePuzzleStormSubtitle => 'Resolve quantos problemas for possível em 3 minutos.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Olá, $param'; - } + String get mobileToolsTab => 'Tools'; @override - String get mobileGreetingWithoutName => 'Olá'; + String get mobileWaitingForOpponentToJoin => 'À espera do adversário entrar...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Ampliar peça arrastada'; + String get mobileWatchTab => 'Assistir'; @override String get activityActivity => 'Atividade'; @@ -535,6 +538,9 @@ class AppLocalizationsPt extends AppLocalizations { @override String get broadcastStandings => 'Classificações'; + @override + String get broadcastOfficialStandings => 'Official Standings'; + @override String broadcastIframeHelp(String param) { return 'Mais opções na $param'; @@ -565,6 +571,36 @@ class AppLocalizationsPt extends AppLocalizations { @override String get broadcastScore => 'Pontuação'; + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + @override String broadcastNbBroadcasts(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -1992,9 +2028,6 @@ class AppLocalizationsPt extends AppLocalizations { @override String get byCPL => 'Por CPL'; - @override - String get openStudy => 'Abrir estudo'; - @override String get enable => 'Ativar'; @@ -2662,9 +2695,6 @@ class AppLocalizationsPt extends AppLocalizations { @override String get unblock => 'Desbloquear'; - @override - String get followsYou => 'Segue-te'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 começou a seguir $param2'; @@ -5424,6 +5454,11 @@ class AppLocalizationsPt extends AppLocalizations { @override String get studyYouCompletedThisLesson => 'Parabéns! Completou esta lição.'; + @override + String studyPerPage(String param) { + return '$param per page'; + } + @override String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -5474,52 +5509,57 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { AppLocalizationsPtBr(): super('pt_BR'); @override - String get mobileHomeTab => 'Início'; + String get mobileAllGames => 'Todos os jogos'; @override - String get mobilePuzzlesTab => 'Problemas'; + String get mobileAreYouSure => 'Você tem certeza?'; @override - String get mobileToolsTab => 'Ferramentas'; + String get mobileBlindfoldMode => 'Venda'; @override - String get mobileWatchTab => 'Assistir'; + String get mobileCancelTakebackOffer => 'Cancelar oferta de revanche'; @override - String get mobileSettingsTab => 'Ajustes'; + String get mobileClearButton => 'Limpar'; @override - String get mobileMustBeLoggedIn => 'Você precisa estar logado para ver essa pagina.'; + String get mobileCorrespondenceClearSavedMove => 'Limpar movimento salvos'; @override - String get mobileSystemColors => 'Cores do sistema'; + String get mobileCustomGameJoinAGame => 'Entrar em um jogo'; @override String get mobileFeedbackButton => 'Comentários'; @override - String get mobileOkButton => 'Ok'; + String mobileGreeting(String param) { + return 'Olá, $param'; + } @override - String get mobileSettingsHapticFeedback => 'Vibrar ao trocar'; + String get mobileGreetingWithoutName => 'Olá'; @override - String get mobileSettingsImmersiveMode => 'Modo imersivo'; + String get mobileHideVariation => 'Ocultar variante forçada'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Ocultar a “interface” do sistema durante a reprodução. Use isto se você estiver incomodado com gestor de navegação do sistema nas bordas da tela. Aplica-se às telas dos jogos e desafios.'; + String get mobileHomeTab => 'Início'; @override - String get mobileNotFollowingAnyUser => 'Você não está seguindo nenhum usuário.'; + String get mobileLiveStreamers => 'Streamers do Lichess'; @override - String get mobileAllGames => 'Todos os jogos'; + String get mobileMustBeLoggedIn => 'Você precisa estar logado para ver essa pagina.'; @override - String get mobileRecentSearches => 'Pesquisas recentes'; + String get mobileNoSearchResults => 'Sem Resultados'; @override - String get mobileClearButton => 'Limpar'; + String get mobileNotFollowingAnyUser => 'Você não está seguindo nenhum usuário.'; + + @override + String get mobileOkButton => 'Ok'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -5527,84 +5567,79 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { } @override - String get mobileNoSearchResults => 'Sem Resultados'; + String get mobilePrefMagnifyDraggedPiece => 'Ampliar peça segurada'; @override - String get mobileAreYouSure => 'Você tem certeza?'; + String get mobilePuzzleStormConfirmEndRun => 'Você quer terminar o turno?'; @override - String get mobilePuzzleStreakAbortWarning => 'Você perderá a sua sequência atual e sua pontuação será salva.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nada para mostrar aqui, por favor, altere os filtros'; @override String get mobilePuzzleStormNothingToShow => 'Nada para mostrar aqui. Jogue algumas rodadas da Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Compartilhar este quebra-cabeça'; - - @override - String get mobileShareGameURL => 'Compartilhar URL do jogo'; + String get mobilePuzzleStormSubtitle => 'Resolva quantos quebra-cabeças for possível em 3 minutos.'; @override - String get mobileShareGamePGN => 'Compartilhar PGN'; + String get mobilePuzzleStreakAbortWarning => 'Você perderá a sua sequência atual e sua pontuação será salva.'; @override - String get mobileSharePositionAsFEN => 'Compartilhar posição como FEN'; + String get mobilePuzzleThemesSubtitle => 'Jogue quebra-cabeças de suas aberturas favoritas, ou escolha um tema.'; @override - String get mobileShowVariations => 'Mostrar setas de variantes'; + String get mobilePuzzlesTab => 'Problemas'; @override - String get mobileHideVariation => 'Ocultar variante forçada'; + String get mobileRecentSearches => 'Pesquisas recentes'; @override - String get mobileShowComments => 'Mostrar comentários'; + String get mobileSettingsHapticFeedback => 'Vibrar ao trocar'; @override - String get mobilePuzzleStormConfirmEndRun => 'Você quer terminar o turno?'; + String get mobileSettingsImmersiveMode => 'Modo imersivo'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nada para mostrar aqui, por favor, altere os filtros'; + String get mobileSettingsImmersiveModeSubtitle => 'Ocultar a “interface” do sistema durante a reprodução. Use isto se você estiver incomodado com gestor de navegação do sistema nas bordas da tela. Aplica-se às telas dos jogos e desafios.'; @override - String get mobileCancelTakebackOffer => 'Cancelar oferta de revanche'; + String get mobileSettingsTab => 'Ajustes'; @override - String get mobileWaitingForOpponentToJoin => 'Esperando por um oponente...'; + String get mobileShareGamePGN => 'Compartilhar PGN'; @override - String get mobileBlindfoldMode => 'Venda'; + String get mobileShareGameURL => 'Compartilhar URL do jogo'; @override - String get mobileLiveStreamers => 'Streamers do Lichess'; + String get mobileSharePositionAsFEN => 'Compartilhar posição como FEN'; @override - String get mobileCustomGameJoinAGame => 'Entrar em um jogo'; + String get mobileSharePuzzle => 'Compartilhar este quebra-cabeça'; @override - String get mobileCorrespondenceClearSavedMove => 'Limpar movimento salvos'; + String get mobileShowComments => 'Mostrar comentários'; @override - String get mobileSomethingWentWrong => 'Houve algum problema.'; + String get mobileShowResult => 'Mostrar resultado'; @override - String get mobileShowResult => 'Mostrar resultado'; + String get mobileShowVariations => 'Mostrar setas de variantes'; @override - String get mobilePuzzleThemesSubtitle => 'Jogue quebra-cabeças de suas aberturas favoritas, ou escolha um tema.'; + String get mobileSomethingWentWrong => 'Houve algum problema.'; @override - String get mobilePuzzleStormSubtitle => 'Resolva quantos quebra-cabeças for possível em 3 minutos.'; + String get mobileSystemColors => 'Cores do sistema'; @override - String mobileGreeting(String param) { - return 'Olá, $param'; - } + String get mobileToolsTab => 'Ferramentas'; @override - String get mobileGreetingWithoutName => 'Olá'; + String get mobileWaitingForOpponentToJoin => 'Esperando por um oponente...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Ampliar peça segurada'; + String get mobileWatchTab => 'Assistir'; @override String get activityActivity => 'Atividade'; @@ -7457,9 +7492,6 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { @override String get byCPL => 'Por erros'; - @override - String get openStudy => 'Abrir estudo'; - @override String get enable => 'Ativar'; @@ -8127,9 +8159,6 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { @override String get unblock => 'Desbloquear'; - @override - String get followsYou => 'Segue você'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 começou a seguir $param2'; diff --git a/lib/l10n/l10n_ro.dart b/lib/l10n/l10n_ro.dart index cf82a7509e..6bea31a671 100644 --- a/lib/l10n/l10n_ro.dart +++ b/lib/l10n/l10n_ro.dart @@ -9,52 +9,57 @@ class AppLocalizationsRo extends AppLocalizations { AppLocalizationsRo([String locale = 'ro']) : super(locale); @override - String get mobileHomeTab => 'Acasă'; + String get mobileAllGames => 'Toate jocurile'; @override - String get mobilePuzzlesTab => 'Puzzle-uri'; + String get mobileAreYouSure => 'Ești sigur?'; @override - String get mobileToolsTab => 'Unelte'; + String get mobileBlindfoldMode => 'Legat la ochi'; @override - String get mobileWatchTab => 'Vizionează'; + String get mobileCancelTakebackOffer => 'Anulați propunerea de revanșă'; @override - String get mobileSettingsTab => 'Setări'; + String get mobileClearButton => 'Resetare'; @override - String get mobileMustBeLoggedIn => 'Trebuie să te autentifici pentru a accesa această pagină.'; + String get mobileCorrespondenceClearSavedMove => 'Șterge mutarea salvată'; @override - String get mobileSystemColors => 'Culori sistem'; + String get mobileCustomGameJoinAGame => 'Alătură-te unui joc'; @override String get mobileFeedbackButton => 'Feedback'; @override - String get mobileOkButton => 'OK'; + String mobileGreeting(String param) { + return 'Salut, $param'; + } @override - String get mobileSettingsHapticFeedback => 'Control tactil'; + String get mobileGreetingWithoutName => 'Salut'; @override - String get mobileSettingsImmersiveMode => 'Mod imersiv'; + String get mobileHideVariation => 'Ascunde variațiile'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Ascunde interfața de utilizator a sistemului în timpul jocului. Folosește această opțiune dacă ești deranjat de gesturile de navigare ale sistemului la marginile ecranului. Se aplică pentru ecranele de joc și Puzzle Storm.'; + String get mobileHomeTab => 'Acasă'; @override - String get mobileNotFollowingAnyUser => 'Nu urmărești niciun utilizator.'; + String get mobileLiveStreamers => 'Fluxuri live'; @override - String get mobileAllGames => 'Toate jocurile'; + String get mobileMustBeLoggedIn => 'Trebuie să te autentifici pentru a accesa această pagină.'; @override - String get mobileRecentSearches => 'Căutări recente'; + String get mobileNoSearchResults => 'Niciun rezultat'; @override - String get mobileClearButton => 'Resetare'; + String get mobileNotFollowingAnyUser => 'Nu urmărești niciun utilizator.'; + + @override + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,84 +67,82 @@ class AppLocalizationsRo extends AppLocalizations { } @override - String get mobileNoSearchResults => 'Niciun rezultat'; + String get mobilePrefMagnifyDraggedPiece => 'Mărește piesa trasă'; @override - String get mobileAreYouSure => 'Ești sigur?'; + String get mobilePuzzleStormConfirmEndRun => 'Vrei să termini acest run?'; @override - String get mobilePuzzleStreakAbortWarning => 'Îți vei pierde streak-ul actual iar scorul va fi salvat.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nimic de afișat, vă rugăm să schimbați filtrele'; @override String get mobilePuzzleStormNothingToShow => 'Nimic de arătat. Jucați câteva partide de Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Distribuie acest puzzle'; + String get mobilePuzzleStormSubtitle => 'Rezolvă cât mai multe puzzle-uri în 3 minute.'; @override - String get mobileShareGameURL => 'Distribuie URL-ul jocului'; + String get mobilePuzzleStreakAbortWarning => 'Îți vei pierde streak-ul actual iar scorul va fi salvat.'; @override - String get mobileShareGamePGN => 'Distribuie PGN'; + String get mobilePuzzleThemesSubtitle => 'Joacă puzzle-uri din deschiderile tale preferate sau alege o temă.'; @override - String get mobileSharePositionAsFEN => 'Distribuie poziția ca FEN'; + String get mobilePuzzlesTab => 'Puzzle-uri'; @override - String get mobileShowVariations => 'Arată variațiile'; + String get mobileRecentSearches => 'Căutări recente'; @override - String get mobileHideVariation => 'Ascunde variațiile'; + String get mobileSettingsHapticFeedback => 'Control tactil'; @override - String get mobileShowComments => 'Afişează сomentarii'; + String get mobileSettingsImmersiveMode => 'Mod imersiv'; @override - String get mobilePuzzleStormConfirmEndRun => 'Vrei să termini acest run?'; + String get mobileSettingsImmersiveModeSubtitle => 'Ascunde interfața de utilizator a sistemului în timpul jocului. Folosește această opțiune dacă ești deranjat de gesturile de navigare ale sistemului la marginile ecranului. Se aplică pentru ecranele de joc și Puzzle Storm.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nimic de afișat, vă rugăm să schimbați filtrele'; + String get mobileSettingsTab => 'Setări'; @override - String get mobileCancelTakebackOffer => 'Anulați propunerea de revanșă'; + String get mobileShareGamePGN => 'Distribuie PGN'; @override - String get mobileWaitingForOpponentToJoin => 'În așteptarea unui jucător...'; + String get mobileShareGameURL => 'Distribuie URL-ul jocului'; @override - String get mobileBlindfoldMode => 'Legat la ochi'; + String get mobileSharePositionAsFEN => 'Distribuie poziția ca FEN'; @override - String get mobileLiveStreamers => 'Fluxuri live'; + String get mobileSharePuzzle => 'Distribuie acest puzzle'; @override - String get mobileCustomGameJoinAGame => 'Alătură-te unui joc'; + String get mobileShowComments => 'Afişează сomentarii'; @override - String get mobileCorrespondenceClearSavedMove => 'Șterge mutarea salvată'; + String get mobileShowResult => 'Arată rezultatul'; @override - String get mobileSomethingWentWrong => 'Ceva nu a mers bine. :('; + String get mobileShowVariations => 'Arată variațiile'; @override - String get mobileShowResult => 'Arată rezultatul'; + String get mobileSomethingWentWrong => 'Ceva nu a mers bine. :('; @override - String get mobilePuzzleThemesSubtitle => 'Joacă puzzle-uri din deschiderile tale preferate sau alege o temă.'; + String get mobileSystemColors => 'Culori sistem'; @override - String get mobilePuzzleStormSubtitle => 'Rezolvă cât mai multe puzzle-uri în 3 minute.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Salut, $param'; - } + String get mobileToolsTab => 'Unelte'; @override - String get mobileGreetingWithoutName => 'Salut'; + String get mobileWaitingForOpponentToJoin => 'În așteptarea unui jucător...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Mărește piesa trasă'; + String get mobileWatchTab => 'Vizionează'; @override String get activityActivity => 'Activitate'; @@ -553,6 +556,9 @@ class AppLocalizationsRo extends AppLocalizations { @override String get broadcastStandings => 'Clasament'; + @override + String get broadcastOfficialStandings => 'Official Standings'; + @override String broadcastIframeHelp(String param) { return 'More options on the $param'; @@ -583,6 +589,36 @@ class AppLocalizationsRo extends AppLocalizations { @override String get broadcastScore => 'Scor'; + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + @override String broadcastNbBroadcasts(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -2016,9 +2052,6 @@ class AppLocalizationsRo extends AppLocalizations { @override String get byCPL => 'După CPL'; - @override - String get openStudy => 'Studiu deschis'; - @override String get enable => 'Activează'; @@ -2686,9 +2719,6 @@ class AppLocalizationsRo extends AppLocalizations { @override String get unblock => 'Deblocare'; - @override - String get followsYou => 'Vă urmărește'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 a început să vă urmărească $param2'; @@ -5492,6 +5522,11 @@ class AppLocalizationsRo extends AppLocalizations { @override String get studyYouCompletedThisLesson => 'Felicitări! Ai terminat această lecție.'; + @override + String studyPerPage(String param) { + return '$param per page'; + } + @override String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( diff --git a/lib/l10n/l10n_ru.dart b/lib/l10n/l10n_ru.dart index 9908cf5b0a..99d35c95ce 100644 --- a/lib/l10n/l10n_ru.dart +++ b/lib/l10n/l10n_ru.dart @@ -9,52 +9,57 @@ class AppLocalizationsRu extends AppLocalizations { AppLocalizationsRu([String locale = 'ru']) : super(locale); @override - String get mobileHomeTab => 'Главная'; + String get mobileAllGames => 'Все игры'; @override - String get mobilePuzzlesTab => 'Задачи'; + String get mobileAreYouSure => 'Вы уверены?'; @override - String get mobileToolsTab => 'Анализ'; + String get mobileBlindfoldMode => 'Игра вслепую'; @override - String get mobileWatchTab => 'Просмотр'; + String get mobileCancelTakebackOffer => 'Отменить предложение о возврате хода'; @override - String get mobileSettingsTab => 'Настройки'; + String get mobileClearButton => 'Очистить'; @override - String get mobileMustBeLoggedIn => 'Вы должны войти для просмотра этой страницы.'; + String get mobileCorrespondenceClearSavedMove => 'Очистить сохранённый ход'; @override - String get mobileSystemColors => 'Цвет интерфейса'; + String get mobileCustomGameJoinAGame => 'Присоединиться к игре'; @override String get mobileFeedbackButton => 'Отзыв'; @override - String get mobileOkButton => 'ОК'; + String mobileGreeting(String param) { + return 'Привет, $param'; + } @override - String get mobileSettingsHapticFeedback => 'Виброотклик'; + String get mobileGreetingWithoutName => 'Привет'; @override - String get mobileSettingsImmersiveMode => 'Полноэкранный режим'; + String get mobileHideVariation => 'Скрыть варианты'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Скрывать интерфейс во время игры. Воспользуйтесь, если вам мешает навигация по краям экрана. Применяется в режиме партий и задач.'; + String get mobileHomeTab => 'Главная'; @override - String get mobileNotFollowingAnyUser => 'Вы не подписаны на других пользователей.'; + String get mobileLiveStreamers => 'Стримеры в эфире'; @override - String get mobileAllGames => 'Все игры'; + String get mobileMustBeLoggedIn => 'Вы должны войти для просмотра этой страницы.'; @override - String get mobileRecentSearches => 'Последние запросы'; + String get mobileNoSearchResults => 'Ничего не найденo'; @override - String get mobileClearButton => 'Очистить'; + String get mobileNotFollowingAnyUser => 'Вы не подписаны на других пользователей.'; + + @override + String get mobileOkButton => 'ОК'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,84 +67,82 @@ class AppLocalizationsRu extends AppLocalizations { } @override - String get mobileNoSearchResults => 'Ничего не найденo'; + String get mobilePrefMagnifyDraggedPiece => 'Увеличивать перетаскиваемую фигуру'; @override - String get mobileAreYouSure => 'Вы уверены?'; + String get mobilePuzzleStormConfirmEndRun => 'Хотите закончить эту попытку?'; @override - String get mobilePuzzleStreakAbortWarning => 'Вы потеряете свою текущую серию, и результаты будут сохранены.'; + String get mobilePuzzleStormFilterNothingToShow => 'Ничего не найдено, измените фильтры, пожалуйста'; @override String get mobilePuzzleStormNothingToShow => 'Ничего нет. Сыграйте несколько попыток.'; @override - String get mobileSharePuzzle => 'Поделиться задачей'; + String get mobilePuzzleStormSubtitle => 'Решите как можно больше задач за 3 минуты.'; @override - String get mobileShareGameURL => 'Поделиться ссылкой на игру'; + String get mobilePuzzleStreakAbortWarning => 'Вы потеряете свою текущую серию, и результаты будут сохранены.'; @override - String get mobileShareGamePGN => 'Поделиться PGN'; + String get mobilePuzzleThemesSubtitle => 'Решайте задачи по вашим любимым дебютам или выберите тему.'; @override - String get mobileSharePositionAsFEN => 'Поделиться FEN'; + String get mobilePuzzlesTab => 'Задачи'; @override - String get mobileShowVariations => 'Показывать варианты'; + String get mobileRecentSearches => 'Последние запросы'; @override - String get mobileHideVariation => 'Скрыть варианты'; + String get mobileSettingsHapticFeedback => 'Виброотклик'; @override - String get mobileShowComments => 'Показать комментарии'; + String get mobileSettingsImmersiveMode => 'Полноэкранный режим'; @override - String get mobilePuzzleStormConfirmEndRun => 'Хотите закончить эту попытку?'; + String get mobileSettingsImmersiveModeSubtitle => 'Скрывать интерфейс во время игры. Воспользуйтесь, если вам мешает навигация по краям экрана. Применяется в режиме партий и задач.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Ничего не найдено, измените фильтры, пожалуйста'; + String get mobileSettingsTab => 'Настройки'; @override - String get mobileCancelTakebackOffer => 'Отменить предложение о возврате хода'; + String get mobileShareGamePGN => 'Поделиться PGN'; @override - String get mobileWaitingForOpponentToJoin => 'Ожидание соперника...'; + String get mobileShareGameURL => 'Поделиться ссылкой на игру'; @override - String get mobileBlindfoldMode => 'Игра вслепую'; + String get mobileSharePositionAsFEN => 'Поделиться FEN'; @override - String get mobileLiveStreamers => 'Стримеры в эфире'; + String get mobileSharePuzzle => 'Поделиться задачей'; @override - String get mobileCustomGameJoinAGame => 'Присоединиться к игре'; + String get mobileShowComments => 'Показать комментарии'; @override - String get mobileCorrespondenceClearSavedMove => 'Очистить сохранённый ход'; + String get mobileShowResult => 'Показать результат'; @override - String get mobileSomethingWentWrong => 'Что-то пошло не так.'; + String get mobileShowVariations => 'Показывать варианты'; @override - String get mobileShowResult => 'Показать результат'; + String get mobileSomethingWentWrong => 'Что-то пошло не так.'; @override - String get mobilePuzzleThemesSubtitle => 'Решайте задачи по вашим любимым дебютам или выберите тему.'; + String get mobileSystemColors => 'Цвет интерфейса'; @override - String get mobilePuzzleStormSubtitle => 'Решите как можно больше задач за 3 минуты.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Привет, $param'; - } + String get mobileToolsTab => 'Анализ'; @override - String get mobileGreetingWithoutName => 'Привет'; + String get mobileWaitingForOpponentToJoin => 'Ожидание соперника...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Увеличивать перетаскиваемую фигуру'; + String get mobileWatchTab => 'Просмотр'; @override String get activityActivity => 'Активность'; @@ -571,6 +574,9 @@ class AppLocalizationsRu extends AppLocalizations { @override String get broadcastStandings => 'Турнирная таблица'; + @override + String get broadcastOfficialStandings => 'Official Standings'; + @override String broadcastIframeHelp(String param) { return 'Больше опций на $param'; @@ -601,6 +607,36 @@ class AppLocalizationsRu extends AppLocalizations { @override String get broadcastScore => 'Очки'; + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + @override String broadcastNbBroadcasts(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -2040,9 +2076,6 @@ class AppLocalizationsRu extends AppLocalizations { @override String get byCPL => 'По ошибкам'; - @override - String get openStudy => 'Открыть в студии'; - @override String get enable => 'Включить'; @@ -2710,9 +2743,6 @@ class AppLocalizationsRu extends AppLocalizations { @override String get unblock => 'Разблокировать'; - @override - String get followsYou => 'Подписан на вас'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 подписался на $param2'; @@ -5560,6 +5590,11 @@ class AppLocalizationsRu extends AppLocalizations { @override String get studyYouCompletedThisLesson => 'Поздравляем! Вы прошли этот урок.'; + @override + String studyPerPage(String param) { + return '$param per page'; + } + @override String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( diff --git a/lib/l10n/l10n_sk.dart b/lib/l10n/l10n_sk.dart index 51330c0e2a..f72ede6176 100644 --- a/lib/l10n/l10n_sk.dart +++ b/lib/l10n/l10n_sk.dart @@ -9,52 +9,57 @@ class AppLocalizationsSk extends AppLocalizations { AppLocalizationsSk([String locale = 'sk']) : super(locale); @override - String get mobileHomeTab => 'Domov'; + String get mobileAllGames => 'Všetky partie'; @override - String get mobilePuzzlesTab => 'Úlohy'; + String get mobileAreYouSure => 'Ste si istý?'; @override - String get mobileToolsTab => 'Nástroje'; + String get mobileBlindfoldMode => 'Naslepo'; @override - String get mobileWatchTab => 'Sledovať'; + String get mobileCancelTakebackOffer => 'Zrušiť žiadosť o vrátenie ťahu'; @override - String get mobileSettingsTab => 'Nastavenia'; + String get mobileClearButton => 'Odstrániť'; @override - String get mobileMustBeLoggedIn => 'Na zobrazenie tejto stránky musíte byť prihlásený.'; + String get mobileCorrespondenceClearSavedMove => 'Vymazať uložený ťah'; @override - String get mobileSystemColors => 'Farby operačného systému'; + String get mobileCustomGameJoinAGame => 'Pripojiť sa k partii'; @override String get mobileFeedbackButton => 'Spätná väzba'; @override - String get mobileOkButton => 'OK'; + String mobileGreeting(String param) { + return 'Ahoj, $param'; + } @override - String get mobileSettingsHapticFeedback => 'Vibrovanie zariadenia'; + String get mobileGreetingWithoutName => 'Ahoj'; @override - String get mobileSettingsImmersiveMode => 'Režim celej obrazovky'; + String get mobileHideVariation => 'Skryť varianty'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Skrytie používateľského rozhrania systému počas hrania. Túto funkciu použite, ak vám prekážajú navigačné gestá systému na okrajoch obrazovky. Vzťahuje sa na obrazovku počas partie a Puzzle Storm.'; + String get mobileHomeTab => 'Domov'; @override - String get mobileNotFollowingAnyUser => 'Nesledujete žiadneho používateľa.'; + String get mobileLiveStreamers => 'Vysielajúci strímeri'; @override - String get mobileAllGames => 'Všetky partie'; + String get mobileMustBeLoggedIn => 'Na zobrazenie tejto stránky musíte byť prihlásený.'; @override - String get mobileRecentSearches => 'Posledné vyhľadávania'; + String get mobileNoSearchResults => 'Nič sa nenašlo'; @override - String get mobileClearButton => 'Odstrániť'; + String get mobileNotFollowingAnyUser => 'Nesledujete žiadneho používateľa.'; + + @override + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,84 +67,82 @@ class AppLocalizationsSk extends AppLocalizations { } @override - String get mobileNoSearchResults => 'Nič sa nenašlo'; + String get mobilePrefMagnifyDraggedPiece => 'Zväčšiť uchopenú figúrku'; @override - String get mobileAreYouSure => 'Ste si istý?'; + String get mobilePuzzleStormConfirmEndRun => 'Chcete ukončiť tento pokus?'; @override - String get mobilePuzzleStreakAbortWarning => 'Stratíte svoju aktuálnu sériu a vaše skóre sa uloží.'; + String get mobilePuzzleStormFilterNothingToShow => 'Niet čo zobraziť, prosím, zmeňte filtre'; @override String get mobilePuzzleStormNothingToShow => 'Niet čo zobraziť. Zahrajte si niekoľko kôl Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Zdieľať túto úlohu'; + String get mobilePuzzleStormSubtitle => 'Vyriešte čo najviac úloh za 3 minúty.'; @override - String get mobileShareGameURL => 'Zdieľať URL partie'; + String get mobilePuzzleStreakAbortWarning => 'Stratíte svoju aktuálnu sériu a vaše skóre sa uloží.'; @override - String get mobileShareGamePGN => 'Zdieľať PGN'; + String get mobilePuzzleThemesSubtitle => 'Riešte úlohy zo svojich obľúbených otvorení alebo si vyberte tému.'; @override - String get mobileSharePositionAsFEN => 'Zdieľať pozíciu vo formáte FEN'; + String get mobilePuzzlesTab => 'Úlohy'; @override - String get mobileShowVariations => 'Zobraziť varianty'; + String get mobileRecentSearches => 'Posledné vyhľadávania'; @override - String get mobileHideVariation => 'Skryť varianty'; + String get mobileSettingsHapticFeedback => 'Vibrovanie zariadenia'; @override - String get mobileShowComments => 'Zobraziť komentáre'; + String get mobileSettingsImmersiveMode => 'Režim celej obrazovky'; @override - String get mobilePuzzleStormConfirmEndRun => 'Chcete ukončiť tento pokus?'; + String get mobileSettingsImmersiveModeSubtitle => 'Skrytie používateľského rozhrania systému počas hrania. Túto funkciu použite, ak vám prekážajú navigačné gestá systému na okrajoch obrazovky. Vzťahuje sa na obrazovku počas partie a Puzzle Storm.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Niet čo zobraziť, prosím, zmeňte filtre'; + String get mobileSettingsTab => 'Nastavenia'; @override - String get mobileCancelTakebackOffer => 'Zrušiť žiadosť o vrátenie ťahu'; + String get mobileShareGamePGN => 'Zdieľať PGN'; @override - String get mobileWaitingForOpponentToJoin => 'Čaká sa na pripojenie súpera...'; + String get mobileShareGameURL => 'Zdieľať URL partie'; @override - String get mobileBlindfoldMode => 'Naslepo'; + String get mobileSharePositionAsFEN => 'Zdieľať pozíciu vo formáte FEN'; @override - String get mobileLiveStreamers => 'Vysielajúci strímeri'; + String get mobileSharePuzzle => 'Zdieľať túto úlohu'; @override - String get mobileCustomGameJoinAGame => 'Pripojiť sa k partii'; + String get mobileShowComments => 'Zobraziť komentáre'; @override - String get mobileCorrespondenceClearSavedMove => 'Vymazať uložený ťah'; + String get mobileShowResult => 'Zobraziť výsledok'; @override - String get mobileSomethingWentWrong => 'Došlo k chybe.'; + String get mobileShowVariations => 'Zobraziť varianty'; @override - String get mobileShowResult => 'Zobraziť výsledok'; + String get mobileSomethingWentWrong => 'Došlo k chybe.'; @override - String get mobilePuzzleThemesSubtitle => 'Riešte úlohy zo svojich obľúbených otvorení alebo si vyberte tému.'; + String get mobileSystemColors => 'Farby operačného systému'; @override - String get mobilePuzzleStormSubtitle => 'Vyriešte čo najviac úloh za 3 minúty.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Ahoj, $param'; - } + String get mobileToolsTab => 'Nástroje'; @override - String get mobileGreetingWithoutName => 'Ahoj'; + String get mobileWaitingForOpponentToJoin => 'Čaká sa na pripojenie súpera...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Zväčšiť uchopenú figúrku'; + String get mobileWatchTab => 'Sledovať'; @override String get activityActivity => 'Aktivita'; @@ -571,6 +574,9 @@ class AppLocalizationsSk extends AppLocalizations { @override String get broadcastStandings => 'Poradie'; + @override + String get broadcastOfficialStandings => 'Official Standings'; + @override String broadcastIframeHelp(String param) { return 'Viac možností nájdete na $param'; @@ -601,6 +607,36 @@ class AppLocalizationsSk extends AppLocalizations { @override String get broadcastScore => 'Skóre'; + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + @override String broadcastNbBroadcasts(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -2040,9 +2076,6 @@ class AppLocalizationsSk extends AppLocalizations { @override String get byCPL => 'CHYBY'; - @override - String get openStudy => 'Otvoriť štúdie'; - @override String get enable => 'Povoliť analýzu'; @@ -2710,9 +2743,6 @@ class AppLocalizationsSk extends AppLocalizations { @override String get unblock => 'Odblokovať'; - @override - String get followsYou => 'Sleduje Vás'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 začal sledovať $param2'; @@ -5560,6 +5590,11 @@ class AppLocalizationsSk extends AppLocalizations { @override String get studyYouCompletedThisLesson => 'Gratulujeme! Túto lekciu ste ukončili.'; + @override + String studyPerPage(String param) { + return '$param per page'; + } + @override String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( diff --git a/lib/l10n/l10n_sl.dart b/lib/l10n/l10n_sl.dart index bcc5e32413..ba033cc7f3 100644 --- a/lib/l10n/l10n_sl.dart +++ b/lib/l10n/l10n_sl.dart @@ -9,52 +9,57 @@ class AppLocalizationsSl extends AppLocalizations { AppLocalizationsSl([String locale = 'sl']) : super(locale); @override - String get mobileHomeTab => 'Domov'; + String get mobileAllGames => 'All games'; @override - String get mobilePuzzlesTab => 'Problemi'; + String get mobileAreYouSure => 'Are you sure?'; @override - String get mobileToolsTab => 'Orodja'; + String get mobileBlindfoldMode => 'Blindfold'; @override - String get mobileWatchTab => 'Glej'; + String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @override - String get mobileSettingsTab => 'Nastavitve'; + String get mobileClearButton => 'Clear'; @override - String get mobileMustBeLoggedIn => 'Predenj lahko dostopaš do te strani, se je potrebno prijaviti.'; + String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; @override - String get mobileSystemColors => 'Barve sistema'; + String get mobileCustomGameJoinAGame => 'Join a game'; @override String get mobileFeedbackButton => 'Povratne informacije'; @override - String get mobileOkButton => 'OK'; + String mobileGreeting(String param) { + return 'Pozdravljeni $param'; + } @override - String get mobileSettingsHapticFeedback => 'Haptic feedback'; + String get mobileGreetingWithoutName => 'Živjo'; @override - String get mobileSettingsImmersiveMode => 'Immersive mode'; + String get mobileHideVariation => 'Hide variation'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileHomeTab => 'Domov'; @override - String get mobileNotFollowingAnyUser => 'You are not following any user.'; + String get mobileLiveStreamers => 'Live streamers'; @override - String get mobileAllGames => 'All games'; + String get mobileMustBeLoggedIn => 'Predenj lahko dostopaš do te strani, se je potrebno prijaviti.'; @override - String get mobileRecentSearches => 'Recent searches'; + String get mobileNoSearchResults => 'No results'; @override - String get mobileClearButton => 'Clear'; + String get mobileNotFollowingAnyUser => 'You are not following any user.'; + + @override + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,84 +67,82 @@ class AppLocalizationsSl extends AppLocalizations { } @override - String get mobileNoSearchResults => 'No results'; + String get mobilePrefMagnifyDraggedPiece => 'Povečaj vlečeno figuro'; @override - String get mobileAreYouSure => 'Are you sure?'; + String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; @override String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Share this puzzle'; + String get mobilePuzzleStormSubtitle => 'V 3 minutah rešite čim več ugank.'; @override - String get mobileShareGameURL => 'Share game URL'; + String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; @override - String get mobileShareGamePGN => 'Share PGN'; + String get mobilePuzzleThemesSubtitle => 'Igrajte uganke iz svojih najljubših otvoritev ali izberite temo.'; @override - String get mobileSharePositionAsFEN => 'Share position as FEN'; + String get mobilePuzzlesTab => 'Problemi'; @override - String get mobileShowVariations => 'Show variations'; + String get mobileRecentSearches => 'Recent searches'; @override - String get mobileHideVariation => 'Hide variation'; + String get mobileSettingsHapticFeedback => 'Haptic feedback'; @override - String get mobileShowComments => 'Show comments'; + String get mobileSettingsImmersiveMode => 'Immersive mode'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobileSettingsTab => 'Nastavitve'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; + String get mobileShareGamePGN => 'Share PGN'; @override - String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; + String get mobileShareGameURL => 'Share game URL'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get mobileSharePositionAsFEN => 'Share position as FEN'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileSharePuzzle => 'Share this puzzle'; @override - String get mobileCustomGameJoinAGame => 'Join a game'; + String get mobileShowComments => 'Show comments'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; + String get mobileShowResult => 'Pokaži rezultat'; @override - String get mobileSomethingWentWrong => 'Something went wrong.'; + String get mobileShowVariations => 'Show variations'; @override - String get mobileShowResult => 'Pokaži rezultat'; + String get mobileSomethingWentWrong => 'Something went wrong.'; @override - String get mobilePuzzleThemesSubtitle => 'Igrajte uganke iz svojih najljubših otvoritev ali izberite temo.'; + String get mobileSystemColors => 'Barve sistema'; @override - String get mobilePuzzleStormSubtitle => 'V 3 minutah rešite čim več ugank.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Pozdravljeni $param'; - } + String get mobileToolsTab => 'Orodja'; @override - String get mobileGreetingWithoutName => 'Živjo'; + String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Povečaj vlečeno figuro'; + String get mobileWatchTab => 'Glej'; @override String get activityActivity => 'Aktivnost'; @@ -571,6 +574,9 @@ class AppLocalizationsSl extends AppLocalizations { @override String get broadcastStandings => 'Standings'; + @override + String get broadcastOfficialStandings => 'Official Standings'; + @override String broadcastIframeHelp(String param) { return 'More options on the $param'; @@ -601,6 +607,36 @@ class AppLocalizationsSl extends AppLocalizations { @override String get broadcastScore => 'Score'; + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + @override String broadcastNbBroadcasts(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -2040,9 +2076,6 @@ class AppLocalizationsSl extends AppLocalizations { @override String get byCPL => 'Za stotinko kmeta'; - @override - String get openStudy => 'Odpri študij'; - @override String get enable => 'Omogoči'; @@ -2710,9 +2743,6 @@ class AppLocalizationsSl extends AppLocalizations { @override String get unblock => 'Odblokiraj'; - @override - String get followsYou => 'Sledi vam'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 je začel slediti $param2'; @@ -5560,6 +5590,11 @@ class AppLocalizationsSl extends AppLocalizations { @override String get studyYouCompletedThisLesson => 'Čestitke! Končali ste to lekcijo.'; + @override + String studyPerPage(String param) { + return '$param per page'; + } + @override String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( diff --git a/lib/l10n/l10n_sq.dart b/lib/l10n/l10n_sq.dart index 28bd224dab..bb4b31b081 100644 --- a/lib/l10n/l10n_sq.dart +++ b/lib/l10n/l10n_sq.dart @@ -9,52 +9,57 @@ class AppLocalizationsSq extends AppLocalizations { AppLocalizationsSq([String locale = 'sq']) : super(locale); @override - String get mobileHomeTab => 'Kreu'; + String get mobileAllGames => 'Krejt lojërat'; @override - String get mobilePuzzlesTab => 'Puzzles'; + String get mobileAreYouSure => 'Jeni i sigurt?'; @override - String get mobileToolsTab => 'Mjete'; + String get mobileBlindfoldMode => 'Me sytë lidhur'; @override - String get mobileWatchTab => 'Shiheni'; + String get mobileCancelTakebackOffer => 'Anulojeni ofertën për prapakthim'; @override - String get mobileSettingsTab => 'Rregullime'; + String get mobileClearButton => 'Spastroje'; @override - String get mobileMustBeLoggedIn => 'Që të shihni këtë faqe, duhet të keni bërë hyrjen në llogari.'; + String get mobileCorrespondenceClearSavedMove => 'Spastroje lëvizjen e ruajtur'; @override - String get mobileSystemColors => 'Ngjyra sistemi'; + String get mobileCustomGameJoinAGame => 'Merrni pjesë në një lojë'; @override String get mobileFeedbackButton => 'Përshtypje'; @override - String get mobileOkButton => 'OK'; + String mobileGreeting(String param) { + return 'Tungjatjeta, $param'; + } @override - String get mobileSettingsHapticFeedback => 'Dridhje gjatë lëvizjesh'; + String get mobileGreetingWithoutName => 'Tungjatjeta'; @override - String get mobileSettingsImmersiveMode => 'Immersive mode'; + String get mobileHideVariation => 'Fshihe variantin'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Fshihni ndërfaqen e sistemit teksa luani. Përdoreni këtë nëse ju bezdisin gjeste sistemi për lëvizjet në skaje të ekranit. Ka vend për lojëra dhe skena Puzzle Storm.'; + String get mobileHomeTab => 'Kreu'; @override - String get mobileNotFollowingAnyUser => 'S’ndiqni ndonjë përdorues.'; + String get mobileLiveStreamers => 'Transmetues drejtpërsëdrejti'; @override - String get mobileAllGames => 'Krejt lojërat'; + String get mobileMustBeLoggedIn => 'Që të shihni këtë faqe, duhet të keni bërë hyrjen në llogari.'; @override - String get mobileRecentSearches => 'Kërkime së fundi'; + String get mobileNoSearchResults => 'S’ka përfundime'; @override - String get mobileClearButton => 'Spastroje'; + String get mobileNotFollowingAnyUser => 'S’ndiqni ndonjë përdorues.'; + + @override + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,84 +67,82 @@ class AppLocalizationsSq extends AppLocalizations { } @override - String get mobileNoSearchResults => 'S’ka përfundime'; + String get mobilePrefMagnifyDraggedPiece => 'Zmadho gurin e tërhequr'; @override - String get mobileAreYouSure => 'Jeni i sigurt?'; + String get mobilePuzzleStormConfirmEndRun => 'Doni të përfundohen ku raund?'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; + String get mobilePuzzleStormFilterNothingToShow => 'S’ka gjë për t’u shfaqur, ju lutemi, ndryshoni filtrat'; @override String get mobilePuzzleStormNothingToShow => 'S’ka gjë për shfaqje. Luani ndonjë raund Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Ndajeni këtë ushtrim me të tjerët'; + String get mobilePuzzleStormSubtitle => 'Zgjidhni sa më shumë puzzle-e të mundeni brenda 3 minutash.'; @override - String get mobileShareGameURL => 'Ndani URL loje me të tjerë'; + String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; @override - String get mobileShareGamePGN => 'Ndani PGN me të tjerë'; + String get mobilePuzzleThemesSubtitle => 'Luani puzzle-e nga hapjet tuaja të parapëlqyera, ose zgjidhni një temë.'; @override - String get mobileSharePositionAsFEN => 'Tregojuni të tjerëve pozicionin si FEN'; + String get mobilePuzzlesTab => 'Puzzles'; @override - String get mobileShowVariations => 'Shfaq variante'; + String get mobileRecentSearches => 'Kërkime së fundi'; @override - String get mobileHideVariation => 'Fshihe variantin'; + String get mobileSettingsHapticFeedback => 'Dridhje gjatë lëvizjesh'; @override - String get mobileShowComments => 'Shfaq komente'; + String get mobileSettingsImmersiveMode => 'Immersive mode'; @override - String get mobilePuzzleStormConfirmEndRun => 'Doni të përfundohen ku raund?'; + String get mobileSettingsImmersiveModeSubtitle => 'Fshihni ndërfaqen e sistemit teksa luani. Përdoreni këtë nëse ju bezdisin gjeste sistemi për lëvizjet në skaje të ekranit. Ka vend për lojëra dhe skena Puzzle Storm.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'S’ka gjë për t’u shfaqur, ju lutemi, ndryshoni filtrat'; + String get mobileSettingsTab => 'Rregullime'; @override - String get mobileCancelTakebackOffer => 'Anulojeni ofertën për prapakthim'; + String get mobileShareGamePGN => 'Ndani PGN me të tjerë'; @override - String get mobileWaitingForOpponentToJoin => 'Po pritet që të vijë kundërshtari…'; + String get mobileShareGameURL => 'Ndani URL loje me të tjerë'; @override - String get mobileBlindfoldMode => 'Me sytë lidhur'; + String get mobileSharePositionAsFEN => 'Tregojuni të tjerëve pozicionin si FEN'; @override - String get mobileLiveStreamers => 'Transmetues drejtpërsëdrejti'; + String get mobileSharePuzzle => 'Ndajeni këtë ushtrim me të tjerët'; @override - String get mobileCustomGameJoinAGame => 'Merrni pjesë në një lojë'; + String get mobileShowComments => 'Shfaq komente'; @override - String get mobileCorrespondenceClearSavedMove => 'Spastroje lëvizjen e ruajtur'; + String get mobileShowResult => 'Shfaq përfundimin'; @override - String get mobileSomethingWentWrong => 'Diç shkoi ters.'; + String get mobileShowVariations => 'Shfaq variante'; @override - String get mobileShowResult => 'Shfaq përfundimin'; + String get mobileSomethingWentWrong => 'Diç shkoi ters.'; @override - String get mobilePuzzleThemesSubtitle => 'Luani puzzle-e nga hapjet tuaja të parapëlqyera, ose zgjidhni një temë.'; + String get mobileSystemColors => 'Ngjyra sistemi'; @override - String get mobilePuzzleStormSubtitle => 'Zgjidhni sa më shumë puzzle-e të mundeni brenda 3 minutash.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Tungjatjeta, $param'; - } + String get mobileToolsTab => 'Mjete'; @override - String get mobileGreetingWithoutName => 'Tungjatjeta'; + String get mobileWaitingForOpponentToJoin => 'Po pritet që të vijë kundërshtari…'; @override - String get mobilePrefMagnifyDraggedPiece => 'Zmadho gurin e tërhequr'; + String get mobileWatchTab => 'Shiheni'; @override String get activityActivity => 'Aktiviteti'; @@ -535,6 +538,9 @@ class AppLocalizationsSq extends AppLocalizations { @override String get broadcastStandings => 'Standings'; + @override + String get broadcastOfficialStandings => 'Official Standings'; + @override String broadcastIframeHelp(String param) { return 'Më tepër mundësi te $param'; @@ -565,6 +571,36 @@ class AppLocalizationsSq extends AppLocalizations { @override String get broadcastScore => 'Përfundim'; + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + @override String broadcastNbBroadcasts(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -1992,9 +2028,6 @@ class AppLocalizationsSq extends AppLocalizations { @override String get byCPL => 'nga CPL'; - @override - String get openStudy => 'Studim i hapur'; - @override String get enable => 'Aktivizoje'; @@ -2662,9 +2695,6 @@ class AppLocalizationsSq extends AppLocalizations { @override String get unblock => 'Zhbllokoje'; - @override - String get followsYou => 'Ju ndjek juve'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 nisi të ndjekë $param2'; @@ -5424,6 +5454,11 @@ class AppLocalizationsSq extends AppLocalizations { @override String get studyYouCompletedThisLesson => 'Përgëzime! E mbaruat këtë mësim.'; + @override + String studyPerPage(String param) { + return '$param per page'; + } + @override String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( diff --git a/lib/l10n/l10n_sr.dart b/lib/l10n/l10n_sr.dart index 8977a23734..13335d3d99 100644 --- a/lib/l10n/l10n_sr.dart +++ b/lib/l10n/l10n_sr.dart @@ -9,52 +9,57 @@ class AppLocalizationsSr extends AppLocalizations { AppLocalizationsSr([String locale = 'sr']) : super(locale); @override - String get mobileHomeTab => 'Home'; + String get mobileAllGames => 'All games'; @override - String get mobilePuzzlesTab => 'Puzzles'; + String get mobileAreYouSure => 'Are you sure?'; @override - String get mobileToolsTab => 'Tools'; + String get mobileBlindfoldMode => 'Blindfold'; @override - String get mobileWatchTab => 'Watch'; + String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @override - String get mobileSettingsTab => 'Settings'; + String get mobileClearButton => 'Clear'; @override - String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; + String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; @override - String get mobileSystemColors => 'System colors'; + String get mobileCustomGameJoinAGame => 'Join a game'; @override String get mobileFeedbackButton => 'Feedback'; @override - String get mobileOkButton => 'OK'; + String mobileGreeting(String param) { + return 'Hello, $param'; + } @override - String get mobileSettingsHapticFeedback => 'Haptic feedback'; + String get mobileGreetingWithoutName => 'Hello'; @override - String get mobileSettingsImmersiveMode => 'Immersive mode'; + String get mobileHideVariation => 'Hide variation'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileHomeTab => 'Home'; @override - String get mobileNotFollowingAnyUser => 'You are not following any user.'; + String get mobileLiveStreamers => 'Live streamers'; @override - String get mobileAllGames => 'All games'; + String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; @override - String get mobileRecentSearches => 'Recent searches'; + String get mobileNoSearchResults => 'No results'; @override - String get mobileClearButton => 'Clear'; + String get mobileNotFollowingAnyUser => 'You are not following any user.'; + + @override + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,84 +67,82 @@ class AppLocalizationsSr extends AppLocalizations { } @override - String get mobileNoSearchResults => 'No results'; + String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; @override - String get mobileAreYouSure => 'Are you sure?'; + String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; @override String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Share this puzzle'; + String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; @override - String get mobileShareGameURL => 'Share game URL'; + String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; @override - String get mobileShareGamePGN => 'Share PGN'; + String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; @override - String get mobileSharePositionAsFEN => 'Share position as FEN'; + String get mobilePuzzlesTab => 'Puzzles'; @override - String get mobileShowVariations => 'Show variations'; + String get mobileRecentSearches => 'Recent searches'; @override - String get mobileHideVariation => 'Hide variation'; + String get mobileSettingsHapticFeedback => 'Haptic feedback'; @override - String get mobileShowComments => 'Show comments'; + String get mobileSettingsImmersiveMode => 'Immersive mode'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobileSettingsTab => 'Settings'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; + String get mobileShareGamePGN => 'Share PGN'; @override - String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; + String get mobileShareGameURL => 'Share game URL'; @override - String get mobileBlindfoldMode => 'Blindfold'; + String get mobileSharePositionAsFEN => 'Share position as FEN'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileSharePuzzle => 'Share this puzzle'; @override - String get mobileCustomGameJoinAGame => 'Join a game'; + String get mobileShowComments => 'Show comments'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; + String get mobileShowResult => 'Show result'; @override - String get mobileSomethingWentWrong => 'Something went wrong.'; + String get mobileShowVariations => 'Show variations'; @override - String get mobileShowResult => 'Show result'; + String get mobileSomethingWentWrong => 'Something went wrong.'; @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobileSystemColors => 'System colors'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Hello, $param'; - } + String get mobileToolsTab => 'Tools'; @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Watch'; @override String get activityActivity => 'Активност'; @@ -551,6 +554,9 @@ class AppLocalizationsSr extends AppLocalizations { @override String get broadcastStandings => 'Standings'; + @override + String get broadcastOfficialStandings => 'Official Standings'; + @override String broadcastIframeHelp(String param) { return 'More options on the $param'; @@ -581,6 +587,36 @@ class AppLocalizationsSr extends AppLocalizations { @override String get broadcastScore => 'Score'; + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + @override String broadcastNbBroadcasts(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -2007,9 +2043,6 @@ class AppLocalizationsSr extends AppLocalizations { @override String get byCPL => 'По рачунару'; - @override - String get openStudy => 'Отвори проуку'; - @override String get enable => 'Укључи'; @@ -2677,9 +2710,6 @@ class AppLocalizationsSr extends AppLocalizations { @override String get unblock => 'Одблокирај'; - @override - String get followsYou => 'Прате тебе'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 је почео/ла пратити $param2'; @@ -5478,6 +5508,11 @@ class AppLocalizationsSr extends AppLocalizations { @override String get studyYouCompletedThisLesson => 'Congratulations! You completed this lesson.'; + @override + String studyPerPage(String param) { + return '$param per page'; + } + @override String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( diff --git a/lib/l10n/l10n_sv.dart b/lib/l10n/l10n_sv.dart index 0012a34527..cd623853c4 100644 --- a/lib/l10n/l10n_sv.dart +++ b/lib/l10n/l10n_sv.dart @@ -9,52 +9,57 @@ class AppLocalizationsSv extends AppLocalizations { AppLocalizationsSv([String locale = 'sv']) : super(locale); @override - String get mobileHomeTab => 'Hem'; + String get mobileAllGames => 'Alla spel'; @override - String get mobilePuzzlesTab => 'Problem'; + String get mobileAreYouSure => 'Är du säker?'; @override - String get mobileToolsTab => 'Verktyg'; + String get mobileBlindfoldMode => 'I blindo'; @override - String get mobileWatchTab => 'Titta'; + String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @override - String get mobileSettingsTab => 'Settings'; + String get mobileClearButton => 'Rensa'; @override - String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; + String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; @override - String get mobileSystemColors => 'Systemets färger'; + String get mobileCustomGameJoinAGame => 'Gå med i spel'; @override String get mobileFeedbackButton => 'Feedback'; @override - String get mobileOkButton => 'OK'; + String mobileGreeting(String param) { + return 'Hej $param'; + } @override - String get mobileSettingsHapticFeedback => 'Haptic feedback'; + String get mobileGreetingWithoutName => 'Hej'; @override - String get mobileSettingsImmersiveMode => 'Immersive mode'; + String get mobileHideVariation => 'Dölj variationer'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; + String get mobileHomeTab => 'Hem'; @override - String get mobileNotFollowingAnyUser => 'You are not following any user.'; + String get mobileLiveStreamers => 'Live streamers'; @override - String get mobileAllGames => 'Alla spel'; + String get mobileMustBeLoggedIn => 'You must be logged in to view this page.'; @override - String get mobileRecentSearches => 'Senaste sökningar'; + String get mobileNoSearchResults => 'Inga resultat'; @override - String get mobileClearButton => 'Rensa'; + String get mobileNotFollowingAnyUser => 'You are not following any user.'; + + @override + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,84 +67,82 @@ class AppLocalizationsSv extends AppLocalizations { } @override - String get mobileNoSearchResults => 'Inga resultat'; + String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; @override - String get mobileAreYouSure => 'Är du säker?'; + String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; @override - String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; + String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; @override String get mobilePuzzleStormNothingToShow => 'Nothing to show. Play some runs of Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Dela detta schackproblem'; + String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; @override - String get mobileShareGameURL => 'Dela parti-URL'; + String get mobilePuzzleStreakAbortWarning => 'You will lose your current streak and your score will be saved.'; @override - String get mobileShareGamePGN => 'Dela PGN'; + String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; @override - String get mobileSharePositionAsFEN => 'Share position as FEN'; + String get mobilePuzzlesTab => 'Problem'; @override - String get mobileShowVariations => 'Visa variationer'; + String get mobileRecentSearches => 'Senaste sökningar'; @override - String get mobileHideVariation => 'Dölj variationer'; + String get mobileSettingsHapticFeedback => 'Haptic feedback'; @override - String get mobileShowComments => 'Visa kommentarer'; + String get mobileSettingsImmersiveMode => 'Immersive mode'; @override - String get mobilePuzzleStormConfirmEndRun => 'Do you want to end this run?'; + String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Nothing to show, please change the filters'; + String get mobileSettingsTab => 'Settings'; @override - String get mobileCancelTakebackOffer => 'Cancel takeback offer'; + String get mobileShareGamePGN => 'Dela PGN'; @override - String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; + String get mobileShareGameURL => 'Dela parti-URL'; @override - String get mobileBlindfoldMode => 'I blindo'; + String get mobileSharePositionAsFEN => 'Share position as FEN'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileSharePuzzle => 'Dela detta schackproblem'; @override - String get mobileCustomGameJoinAGame => 'Gå med i spel'; + String get mobileShowComments => 'Visa kommentarer'; @override - String get mobileCorrespondenceClearSavedMove => 'Clear saved move'; + String get mobileShowResult => 'Visa resultat'; @override - String get mobileSomethingWentWrong => 'Något gick fel.'; + String get mobileShowVariations => 'Visa variationer'; @override - String get mobileShowResult => 'Visa resultat'; + String get mobileSomethingWentWrong => 'Något gick fel.'; @override - String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; + String get mobileSystemColors => 'Systemets färger'; @override - String get mobilePuzzleStormSubtitle => 'Solve as many puzzles as possible in 3 minutes.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Hej $param'; - } + String get mobileToolsTab => 'Verktyg'; @override - String get mobileGreetingWithoutName => 'Hej'; + String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Magnify dragged piece'; + String get mobileWatchTab => 'Titta'; @override String get activityActivity => 'Aktivitet'; @@ -535,6 +538,9 @@ class AppLocalizationsSv extends AppLocalizations { @override String get broadcastStandings => 'Standings'; + @override + String get broadcastOfficialStandings => 'Official Standings'; + @override String broadcastIframeHelp(String param) { return 'More options on the $param'; @@ -565,6 +571,36 @@ class AppLocalizationsSv extends AppLocalizations { @override String get broadcastScore => 'Score'; + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + @override String broadcastNbBroadcasts(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -1992,9 +2028,6 @@ class AppLocalizationsSv extends AppLocalizations { @override String get byCPL => 'CPL'; - @override - String get openStudy => 'Öppna studie'; - @override String get enable => 'Aktivera'; @@ -2662,9 +2695,6 @@ class AppLocalizationsSv extends AppLocalizations { @override String get unblock => 'Avblockera'; - @override - String get followsYou => 'Följer dig'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 började följa $param2'; @@ -5424,6 +5454,11 @@ class AppLocalizationsSv extends AppLocalizations { @override String get studyYouCompletedThisLesson => 'Grattis! Du har slutfört denna lektionen.'; + @override + String studyPerPage(String param) { + return '$param per page'; + } + @override String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( diff --git a/lib/l10n/l10n_tr.dart b/lib/l10n/l10n_tr.dart index 6756739357..c169a231ec 100644 --- a/lib/l10n/l10n_tr.dart +++ b/lib/l10n/l10n_tr.dart @@ -9,52 +9,57 @@ class AppLocalizationsTr extends AppLocalizations { AppLocalizationsTr([String locale = 'tr']) : super(locale); @override - String get mobileHomeTab => 'Ana sayfa'; + String get mobileAllGames => 'Tüm oyunlar'; @override - String get mobilePuzzlesTab => 'Bulmacalar'; + String get mobileAreYouSure => 'Emin misiniz?'; @override - String get mobileToolsTab => 'Araçlar'; + String get mobileBlindfoldMode => 'Körleme modu'; @override - String get mobileWatchTab => 'İzle'; + String get mobileCancelTakebackOffer => 'Geri alma teklifini iptal et'; @override - String get mobileSettingsTab => 'Ayarlar'; + String get mobileClearButton => 'Temizle'; @override - String get mobileMustBeLoggedIn => 'Bu sayfayı görüntülemek için giriş yapmalısınız.'; + String get mobileCorrespondenceClearSavedMove => 'Kayıtlı hamleyi sil'; @override - String get mobileSystemColors => 'Sistem renkleri'; + String get mobileCustomGameJoinAGame => 'Bir oyuna katıl'; @override String get mobileFeedbackButton => 'Geri bildirimde bulun'; @override - String get mobileOkButton => 'Tamam'; + String mobileGreeting(String param) { + return 'Merhaba, $param'; + } @override - String get mobileSettingsHapticFeedback => 'Titreşimli geri bildirim'; + String get mobileGreetingWithoutName => 'Merhaba'; @override - String get mobileSettingsImmersiveMode => 'Sürükleyici mod'; + String get mobileHideVariation => 'Varyasyonu gizle'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Oynarken sistem arayüzünü gizle. Ekranın kenarlarındaki sistemin gezinme hareketlerinden rahatsızsan bunu kullan. Bu ayar, oyun ve Bulmaca Fırtınası ekranlarına uygulanır.'; + String get mobileHomeTab => 'Ana sayfa'; @override - String get mobileNotFollowingAnyUser => 'Hiçbir kullanıcıyı takip etmiyorsunuz.'; + String get mobileLiveStreamers => 'Canlı yayıncılar'; @override - String get mobileAllGames => 'Tüm oyunlar'; + String get mobileMustBeLoggedIn => 'Bu sayfayı görüntülemek için giriş yapmalısınız.'; @override - String get mobileRecentSearches => 'Son aramalar'; + String get mobileNoSearchResults => 'Sonuç bulunamadı'; @override - String get mobileClearButton => 'Temizle'; + String get mobileNotFollowingAnyUser => 'Hiçbir kullanıcıyı takip etmiyorsunuz.'; + + @override + String get mobileOkButton => 'Tamam'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,84 +67,82 @@ class AppLocalizationsTr extends AppLocalizations { } @override - String get mobileNoSearchResults => 'Sonuç bulunamadı'; + String get mobilePrefMagnifyDraggedPiece => 'Sürüklenen parçayı büyüt'; @override - String get mobileAreYouSure => 'Emin misiniz?'; + String get mobilePuzzleStormConfirmEndRun => 'Bu oyunu bitirmek istiyor musun?'; @override - String get mobilePuzzleStreakAbortWarning => 'Mevcut serinizi kaybedeceksiniz ve puanınız kaydedilecektir.'; + String get mobilePuzzleStormFilterNothingToShow => 'Gösterilecek bir şey yok, lütfen filtreleri değiştirin'; @override String get mobilePuzzleStormNothingToShow => 'Gösterilcek bir şey yok. Birkaç kez Bulmaca Fırtınası oyunu oynayın.'; @override - String get mobileSharePuzzle => 'Bulmacayı paylaş'; + String get mobilePuzzleStormSubtitle => '3 dakika içerisinde mümkün olduğunca çok bulmaca çözün.'; @override - String get mobileShareGameURL => 'Oyun linkini paylaş'; + String get mobilePuzzleStreakAbortWarning => 'Mevcut serinizi kaybedeceksiniz ve puanınız kaydedilecektir.'; @override - String get mobileShareGamePGN => 'PGN\'yi paylaş'; + String get mobilePuzzleThemesSubtitle => 'En sevdiğiniz açılışlardan bulmacalar oynayın veya bir tema seçin.'; @override - String get mobileSharePositionAsFEN => 'Konumu FEN olarak paylaş'; + String get mobilePuzzlesTab => 'Bulmacalar'; @override - String get mobileShowVariations => 'Varyasyonları göster'; + String get mobileRecentSearches => 'Son aramalar'; @override - String get mobileHideVariation => 'Varyasyonu gizle'; + String get mobileSettingsHapticFeedback => 'Titreşimli geri bildirim'; @override - String get mobileShowComments => 'Yorumları göster'; + String get mobileSettingsImmersiveMode => 'Sürükleyici mod'; @override - String get mobilePuzzleStormConfirmEndRun => 'Bu oyunu bitirmek istiyor musun?'; + String get mobileSettingsImmersiveModeSubtitle => 'Oynarken sistem arayüzünü gizle. Ekranın kenarlarındaki sistemin gezinme hareketlerinden rahatsızsan bunu kullan. Bu ayar, oyun ve Bulmaca Fırtınası ekranlarına uygulanır.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Gösterilecek bir şey yok, lütfen filtreleri değiştirin'; + String get mobileSettingsTab => 'Ayarlar'; @override - String get mobileCancelTakebackOffer => 'Geri alma teklifini iptal et'; + String get mobileShareGamePGN => 'PGN\'yi paylaş'; @override - String get mobileWaitingForOpponentToJoin => 'Rakip bekleniyor...'; + String get mobileShareGameURL => 'Oyun linkini paylaş'; @override - String get mobileBlindfoldMode => 'Körleme modu'; + String get mobileSharePositionAsFEN => 'Konumu FEN olarak paylaş'; @override - String get mobileLiveStreamers => 'Canlı yayıncılar'; + String get mobileSharePuzzle => 'Bulmacayı paylaş'; @override - String get mobileCustomGameJoinAGame => 'Bir oyuna katıl'; + String get mobileShowComments => 'Yorumları göster'; @override - String get mobileCorrespondenceClearSavedMove => 'Kayıtlı hamleyi sil'; + String get mobileShowResult => 'Sonucu göster'; @override - String get mobileSomethingWentWrong => 'Birşeyler ters gitti.'; + String get mobileShowVariations => 'Varyasyonları göster'; @override - String get mobileShowResult => 'Sonucu göster'; + String get mobileSomethingWentWrong => 'Birşeyler ters gitti.'; @override - String get mobilePuzzleThemesSubtitle => 'En sevdiğiniz açılışlardan bulmacalar oynayın veya bir tema seçin.'; + String get mobileSystemColors => 'Sistem renkleri'; @override - String get mobilePuzzleStormSubtitle => '3 dakika içerisinde mümkün olduğunca çok bulmaca çözün.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Merhaba, $param'; - } + String get mobileToolsTab => 'Araçlar'; @override - String get mobileGreetingWithoutName => 'Merhaba'; + String get mobileWaitingForOpponentToJoin => 'Rakip bekleniyor...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Sürüklenen parçayı büyüt'; + String get mobileWatchTab => 'İzle'; @override String get activityActivity => 'Son Etkinlikler'; @@ -535,6 +538,9 @@ class AppLocalizationsTr extends AppLocalizations { @override String get broadcastStandings => 'Sıralamalar'; + @override + String get broadcastOfficialStandings => 'Official Standings'; + @override String broadcastIframeHelp(String param) { return '${param}nda daha fazla seçenek'; @@ -565,6 +571,36 @@ class AppLocalizationsTr extends AppLocalizations { @override String get broadcastScore => 'Skor'; + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + @override String broadcastNbBroadcasts(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -1992,9 +2028,6 @@ class AppLocalizationsTr extends AppLocalizations { @override String get byCPL => 'CPL ile'; - @override - String get openStudy => 'Çalışma oluştur'; - @override String get enable => 'Etkinleştir'; @@ -2662,9 +2695,6 @@ class AppLocalizationsTr extends AppLocalizations { @override String get unblock => 'Engeli kaldır'; - @override - String get followsYou => 'Sizi takip ediyor'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1, $param2 isimli oyuncuyu takip etmeye başladı'; @@ -5424,6 +5454,11 @@ class AppLocalizationsTr extends AppLocalizations { @override String get studyYouCompletedThisLesson => 'Tebrikler! Bu dersi tamamlandınız.'; + @override + String studyPerPage(String param) { + return '$param per page'; + } + @override String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( diff --git a/lib/l10n/l10n_uk.dart b/lib/l10n/l10n_uk.dart index c6ebba1edf..8bb7295b30 100644 --- a/lib/l10n/l10n_uk.dart +++ b/lib/l10n/l10n_uk.dart @@ -9,52 +9,57 @@ class AppLocalizationsUk extends AppLocalizations { AppLocalizationsUk([String locale = 'uk']) : super(locale); @override - String get mobileHomeTab => 'Головна'; + String get mobileAllGames => 'Усі ігри'; @override - String get mobilePuzzlesTab => 'Задачі'; + String get mobileAreYouSure => 'Ви впевнені?'; @override - String get mobileToolsTab => 'Інструм.'; + String get mobileBlindfoldMode => 'Наосліп'; @override - String get mobileWatchTab => 'Дивитися'; + String get mobileCancelTakebackOffer => 'Скасувати пропозицію повернення ходу'; @override - String get mobileSettingsTab => 'Налашт.'; + String get mobileClearButton => 'Очистити'; @override - String get mobileMustBeLoggedIn => 'Ви повинні ввійти, аби переглянути цю сторінку.'; + String get mobileCorrespondenceClearSavedMove => 'Очистити збережений хід'; @override - String get mobileSystemColors => 'Системні кольори'; + String get mobileCustomGameJoinAGame => 'Приєднатися до гри'; @override String get mobileFeedbackButton => 'Відгук'; @override - String get mobileOkButton => 'Гаразд'; + String mobileGreeting(String param) { + return 'Привіт, $param'; + } @override - String get mobileSettingsHapticFeedback => 'Вібрація при ході'; + String get mobileGreetingWithoutName => 'Привіт'; @override - String get mobileSettingsImmersiveMode => 'Повноекранний режим'; + String get mobileHideVariation => 'Сховати варіанти'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Приховати інтерфейс системи під час гри. Використовуйте, якщо вас турбують навігаційні жести системи по краях екрану. Застосовується до екранів гри та задач.'; + String get mobileHomeTab => 'Головна'; @override - String get mobileNotFollowingAnyUser => 'Ви ні на кого не підписані.'; + String get mobileLiveStreamers => 'Стримери в прямому етері'; @override - String get mobileAllGames => 'Усі ігри'; + String get mobileMustBeLoggedIn => 'Ви повинні ввійти, аби переглянути цю сторінку.'; @override - String get mobileRecentSearches => 'Недавні пошуки'; + String get mobileNoSearchResults => 'Немає результатів '; @override - String get mobileClearButton => 'Очистити'; + String get mobileNotFollowingAnyUser => 'Ви ні на кого не підписані.'; + + @override + String get mobileOkButton => 'Гаразд'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,84 +67,82 @@ class AppLocalizationsUk extends AppLocalizations { } @override - String get mobileNoSearchResults => 'Немає результатів '; + String get mobilePrefMagnifyDraggedPiece => 'Збільшувати розмір фігури при перетягуванні'; @override - String get mobileAreYouSure => 'Ви впевнені?'; + String get mobilePuzzleStormConfirmEndRun => 'Ви хочете закінчити цю серію?'; @override - String get mobilePuzzleStreakAbortWarning => 'Ви втратите поточну серію, і ваш рахунок буде збережено.'; + String get mobilePuzzleStormFilterNothingToShow => 'Нічого не знайдено, будь ласка, змініть фільтри'; @override String get mobilePuzzleStormNothingToShow => 'Нічого показати. Зіграйте в гру Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Поділитися задачею'; + String get mobilePuzzleStormSubtitle => 'Розв\'яжіть якомога більше задач за 3 хвилини.'; @override - String get mobileShareGameURL => 'Поділитися посиланням на гру'; + String get mobilePuzzleStreakAbortWarning => 'Ви втратите поточну серію, і ваш рахунок буде збережено.'; @override - String get mobileShareGamePGN => 'Поділитися PGN'; + String get mobilePuzzleThemesSubtitle => 'Розв\'язуйте задачі з улюбленими дебютами або обирайте тему.'; @override - String get mobileSharePositionAsFEN => 'Поділитися FEN'; + String get mobilePuzzlesTab => 'Задачі'; @override - String get mobileShowVariations => 'Показати варіанти'; + String get mobileRecentSearches => 'Недавні пошуки'; @override - String get mobileHideVariation => 'Сховати варіанти'; + String get mobileSettingsHapticFeedback => 'Вібрація при ході'; @override - String get mobileShowComments => 'Показати коментарі'; + String get mobileSettingsImmersiveMode => 'Повноекранний режим'; @override - String get mobilePuzzleStormConfirmEndRun => 'Ви хочете закінчити цю серію?'; + String get mobileSettingsImmersiveModeSubtitle => 'Приховати інтерфейс системи під час гри. Використовуйте, якщо вас турбують навігаційні жести системи по краях екрану. Застосовується до екранів гри та задач.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Нічого не знайдено, будь ласка, змініть фільтри'; + String get mobileSettingsTab => 'Налашт.'; @override - String get mobileCancelTakebackOffer => 'Скасувати пропозицію повернення ходу'; + String get mobileShareGamePGN => 'Поділитися PGN'; @override - String get mobileWaitingForOpponentToJoin => 'Очікування на суперника...'; + String get mobileShareGameURL => 'Поділитися посиланням на гру'; @override - String get mobileBlindfoldMode => 'Наосліп'; + String get mobileSharePositionAsFEN => 'Поділитися FEN'; @override - String get mobileLiveStreamers => 'Стримери в прямому етері'; + String get mobileSharePuzzle => 'Поділитися задачею'; @override - String get mobileCustomGameJoinAGame => 'Приєднатися до гри'; + String get mobileShowComments => 'Показати коментарі'; @override - String get mobileCorrespondenceClearSavedMove => 'Очистити збережений хід'; + String get mobileShowResult => 'Показати результат'; @override - String get mobileSomethingWentWrong => 'Щось пішло не так.'; + String get mobileShowVariations => 'Показати варіанти'; @override - String get mobileShowResult => 'Показати результат'; + String get mobileSomethingWentWrong => 'Щось пішло не так.'; @override - String get mobilePuzzleThemesSubtitle => 'Розв\'язуйте задачі з улюбленими дебютами або обирайте тему.'; + String get mobileSystemColors => 'Системні кольори'; @override - String get mobilePuzzleStormSubtitle => 'Розв\'яжіть якомога більше задач за 3 хвилини.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Привіт, $param'; - } + String get mobileToolsTab => 'Інструм.'; @override - String get mobileGreetingWithoutName => 'Привіт'; + String get mobileWaitingForOpponentToJoin => 'Очікування на суперника...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Збільшувати розмір фігури при перетягуванні'; + String get mobileWatchTab => 'Дивитися'; @override String get activityActivity => 'Активність'; @@ -571,6 +574,9 @@ class AppLocalizationsUk extends AppLocalizations { @override String get broadcastStandings => 'Standings'; + @override + String get broadcastOfficialStandings => 'Official Standings'; + @override String broadcastIframeHelp(String param) { return 'More options on the $param'; @@ -601,6 +607,36 @@ class AppLocalizationsUk extends AppLocalizations { @override String get broadcastScore => 'Score'; + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + @override String broadcastNbBroadcasts(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -2040,9 +2076,6 @@ class AppLocalizationsUk extends AppLocalizations { @override String get byCPL => 'Цікаве'; - @override - String get openStudy => 'Почати дослідження'; - @override String get enable => 'Увімкнути'; @@ -2710,9 +2743,6 @@ class AppLocalizationsUk extends AppLocalizations { @override String get unblock => 'Розблокувати'; - @override - String get followsYou => 'Спостерігає за вами'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 починає спостерігати за $param2'; @@ -5560,6 +5590,11 @@ class AppLocalizationsUk extends AppLocalizations { @override String get studyYouCompletedThisLesson => 'Вітаємо! Ви завершили цей урок.'; + @override + String studyPerPage(String param) { + return '$param per page'; + } + @override String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( diff --git a/lib/l10n/l10n_vi.dart b/lib/l10n/l10n_vi.dart index 585607d0ff..6edb7f57d0 100644 --- a/lib/l10n/l10n_vi.dart +++ b/lib/l10n/l10n_vi.dart @@ -9,52 +9,57 @@ class AppLocalizationsVi extends AppLocalizations { AppLocalizationsVi([String locale = 'vi']) : super(locale); @override - String get mobileHomeTab => 'Trang chủ'; + String get mobileAllGames => 'Tất cả ván đấu'; @override - String get mobilePuzzlesTab => 'Câu đố'; + String get mobileAreYouSure => 'Bạn chắc chứ?'; @override - String get mobileToolsTab => 'Công cụ'; + String get mobileBlindfoldMode => 'Bịt mắt'; @override - String get mobileWatchTab => 'Xem'; + String get mobileCancelTakebackOffer => 'Hủy đề nghị đi lại'; @override - String get mobileSettingsTab => 'Cài đặt'; + String get mobileClearButton => 'Xóa'; @override - String get mobileMustBeLoggedIn => 'Bạn phải đăng nhập để xem trang này.'; + String get mobileCorrespondenceClearSavedMove => 'Xóa nước cờ đã lưu'; @override - String get mobileSystemColors => 'Màu hệ thống'; + String get mobileCustomGameJoinAGame => 'Tham gia một ván cờ'; @override String get mobileFeedbackButton => 'Phản hồi'; @override - String get mobileOkButton => 'OK'; + String mobileGreeting(String param) { + return 'Xin chào, $param'; + } @override - String get mobileSettingsHapticFeedback => 'Rung phản hồi'; + String get mobileGreetingWithoutName => 'Xin chào'; @override - String get mobileSettingsImmersiveMode => 'Chế độ toàn màn hình'; + String get mobileHideVariation => 'Ẩn các biến'; @override - String get mobileSettingsImmersiveModeSubtitle => 'Ẩn UI hệ thống trong khi chơi. Sử dụng điều này nếu bạn bị làm phiền bởi các cử chỉ điều hướng của hệ thống ở các cạnh của màn hình. Áp dụng cho màn hình ván đấu và Puzzle Strom.'; + String get mobileHomeTab => 'Trang chủ'; @override - String get mobileNotFollowingAnyUser => 'Bạn chưa theo dõi người dùng nào.'; + String get mobileLiveStreamers => 'Các Streamer phát trực tiếp'; @override - String get mobileAllGames => 'Tất cả ván đấu'; + String get mobileMustBeLoggedIn => 'Bạn phải đăng nhập để xem trang này.'; @override - String get mobileRecentSearches => 'Tìm kiếm gần đây'; + String get mobileNoSearchResults => 'Không có kết quả'; @override - String get mobileClearButton => 'Xóa'; + String get mobileNotFollowingAnyUser => 'Bạn chưa theo dõi người dùng nào.'; + + @override + String get mobileOkButton => 'OK'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,84 +67,82 @@ class AppLocalizationsVi extends AppLocalizations { } @override - String get mobileNoSearchResults => 'Không có kết quả'; + String get mobilePrefMagnifyDraggedPiece => 'Phóng to quân cờ được kéo'; @override - String get mobileAreYouSure => 'Bạn chắc chứ?'; + String get mobilePuzzleStormConfirmEndRun => 'Bạn có muốn kết thúc lượt chạy này không?'; @override - String get mobilePuzzleStreakAbortWarning => 'Bạn sẽ mất chuỗi hiện tại và điểm của bạn sẽ được lưu.'; + String get mobilePuzzleStormFilterNothingToShow => 'Không có gì để hiển thị, vui lòng thay đổi bộ lọc'; @override String get mobilePuzzleStormNothingToShow => 'Không có gì để xem. Chơi một vài ván Puzzle Storm.'; @override - String get mobileSharePuzzle => 'Chia sẻ câu đố này'; + String get mobilePuzzleStormSubtitle => 'Giải càng nhiều câu đố càng tốt trong 3 phút.'; @override - String get mobileShareGameURL => 'Chia sẻ URL ván cờ'; + String get mobilePuzzleStreakAbortWarning => 'Bạn sẽ mất chuỗi hiện tại và điểm của bạn sẽ được lưu.'; @override - String get mobileShareGamePGN => 'Chia sẻ tập tin PGN'; + String get mobilePuzzleThemesSubtitle => 'Giải câu đố từ những khai cuộc yêu thích của bạn hoặc chọn một chủ đề.'; @override - String get mobileSharePositionAsFEN => 'Chia sẻ thế cờ dạng FEN'; + String get mobilePuzzlesTab => 'Câu đố'; @override - String get mobileShowVariations => 'Hiện các biến'; + String get mobileRecentSearches => 'Tìm kiếm gần đây'; @override - String get mobileHideVariation => 'Ẩn các biến'; + String get mobileSettingsHapticFeedback => 'Rung phản hồi'; @override - String get mobileShowComments => 'Hiển thị bình luận'; + String get mobileSettingsImmersiveMode => 'Chế độ toàn màn hình'; @override - String get mobilePuzzleStormConfirmEndRun => 'Bạn có muốn kết thúc lượt chạy này không?'; + String get mobileSettingsImmersiveModeSubtitle => 'Ẩn UI hệ thống trong khi chơi. Sử dụng điều này nếu bạn bị làm phiền bởi các cử chỉ điều hướng của hệ thống ở các cạnh của màn hình. Áp dụng cho màn hình ván đấu và Puzzle Strom.'; @override - String get mobilePuzzleStormFilterNothingToShow => 'Không có gì để hiển thị, vui lòng thay đổi bộ lọc'; + String get mobileSettingsTab => 'Cài đặt'; @override - String get mobileCancelTakebackOffer => 'Hủy đề nghị đi lại'; + String get mobileShareGamePGN => 'Chia sẻ tập tin PGN'; @override - String get mobileWaitingForOpponentToJoin => 'Đang chờ đối thủ tham gia...'; + String get mobileShareGameURL => 'Chia sẻ URL ván cờ'; @override - String get mobileBlindfoldMode => 'Bịt mắt'; + String get mobileSharePositionAsFEN => 'Chia sẻ thế cờ dạng FEN'; @override - String get mobileLiveStreamers => 'Các Streamer phát trực tiếp'; + String get mobileSharePuzzle => 'Chia sẻ câu đố này'; @override - String get mobileCustomGameJoinAGame => 'Tham gia một ván cờ'; + String get mobileShowComments => 'Hiển thị bình luận'; @override - String get mobileCorrespondenceClearSavedMove => 'Xóa nước cờ đã lưu'; + String get mobileShowResult => 'Xem kết quả'; @override - String get mobileSomethingWentWrong => 'Đã xảy ra lỗi.'; + String get mobileShowVariations => 'Hiện các biến'; @override - String get mobileShowResult => 'Xem kết quả'; + String get mobileSomethingWentWrong => 'Đã xảy ra lỗi.'; @override - String get mobilePuzzleThemesSubtitle => 'Giải câu đố từ những khai cuộc yêu thích của bạn hoặc chọn một chủ đề.'; + String get mobileSystemColors => 'Màu hệ thống'; @override - String get mobilePuzzleStormSubtitle => 'Giải càng nhiều câu đố càng tốt trong 3 phút.'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return 'Xin chào, $param'; - } + String get mobileToolsTab => 'Công cụ'; @override - String get mobileGreetingWithoutName => 'Xin chào'; + String get mobileWaitingForOpponentToJoin => 'Đang chờ đối thủ tham gia...'; @override - String get mobilePrefMagnifyDraggedPiece => 'Phóng to quân cờ được kéo'; + String get mobileWatchTab => 'Xem'; @override String get activityActivity => 'Hoạt động'; @@ -517,6 +520,9 @@ class AppLocalizationsVi extends AppLocalizations { @override String get broadcastStandings => 'Bảng xếp hạng'; + @override + String get broadcastOfficialStandings => 'Official Standings'; + @override String broadcastIframeHelp(String param) { return 'Thêm tùy chọn trên $param'; @@ -547,6 +553,36 @@ class AppLocalizationsVi extends AppLocalizations { @override String get broadcastScore => 'Điểm số'; + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + @override String broadcastNbBroadcasts(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -1968,9 +2004,6 @@ class AppLocalizationsVi extends AppLocalizations { @override String get byCPL => 'Theo phần trăm mất tốt (CPL)'; - @override - String get openStudy => 'Mở nghiên cứu'; - @override String get enable => 'Bật'; @@ -2638,9 +2671,6 @@ class AppLocalizationsVi extends AppLocalizations { @override String get unblock => 'Bỏ chặn'; - @override - String get followsYou => 'Theo dõi bạn'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1 đã bắt đầu theo dõi $param2'; @@ -5356,6 +5386,11 @@ class AppLocalizationsVi extends AppLocalizations { @override String get studyYouCompletedThisLesson => 'Chúc mừng! Bạn đã hoàn thành bài học này.'; + @override + String studyPerPage(String param) { + return '$param per page'; + } + @override String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( diff --git a/lib/l10n/l10n_zh.dart b/lib/l10n/l10n_zh.dart index 74aaa3f0ea..18127594d7 100644 --- a/lib/l10n/l10n_zh.dart +++ b/lib/l10n/l10n_zh.dart @@ -9,52 +9,57 @@ class AppLocalizationsZh extends AppLocalizations { AppLocalizationsZh([String locale = 'zh']) : super(locale); @override - String get mobileHomeTab => '主页'; + String get mobileAllGames => '所有对局'; @override - String get mobilePuzzlesTab => '谜题'; + String get mobileAreYouSure => '你确定吗?'; @override - String get mobileToolsTab => '工具'; + String get mobileBlindfoldMode => '盲棋'; @override - String get mobileWatchTab => '观看'; + String get mobileCancelTakebackOffer => '取消悔棋请求'; @override - String get mobileSettingsTab => '设置'; + String get mobileClearButton => '清空'; @override - String get mobileMustBeLoggedIn => '您必须登录才能浏览此页面。'; + String get mobileCorrespondenceClearSavedMove => '清除已保存的着法'; @override - String get mobileSystemColors => '系统颜色'; + String get mobileCustomGameJoinAGame => '加入一局游戏'; @override String get mobileFeedbackButton => '问题反馈'; @override - String get mobileOkButton => '好'; + String mobileGreeting(String param) { + return '你好,$param'; + } @override - String get mobileSettingsHapticFeedback => '震动反馈'; + String get mobileGreetingWithoutName => '你好!'; @override - String get mobileSettingsImmersiveMode => '沉浸模式'; + String get mobileHideVariation => '隐藏变着'; @override - String get mobileSettingsImmersiveModeSubtitle => '下棋时隐藏系统界面。 如果您的操作受到屏幕边缘的系统导航手势干扰,请使用此功能。 适用于棋局和 Puzzle Storm 界面。'; + String get mobileHomeTab => '主页'; @override - String get mobileNotFollowingAnyUser => '你没有关注任何用户。'; + String get mobileLiveStreamers => '主播'; @override - String get mobileAllGames => '所有对局'; + String get mobileMustBeLoggedIn => '您必须登录才能浏览此页面。'; @override - String get mobileRecentSearches => '最近搜索'; + String get mobileNoSearchResults => '无结果'; @override - String get mobileClearButton => '清空'; + String get mobileNotFollowingAnyUser => '你没有关注任何用户。'; + + @override + String get mobileOkButton => '好'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -62,84 +67,82 @@ class AppLocalizationsZh extends AppLocalizations { } @override - String get mobileNoSearchResults => '无结果'; + String get mobilePrefMagnifyDraggedPiece => '放大正在拖动的棋子'; @override - String get mobileAreYouSure => '你确定吗?'; + String get mobilePuzzleStormConfirmEndRun => '你想结束这组吗?'; @override - String get mobilePuzzleStreakAbortWarning => '你将失去你目前的连胜,你的分数将被保存。'; + String get mobilePuzzleStormFilterNothingToShow => '没有显示,请更改过滤器'; @override String get mobilePuzzleStormNothingToShow => '没有记录。 请下几组 Puzzle Storm。'; @override - String get mobileSharePuzzle => '分享这个谜题'; + String get mobilePuzzleStormSubtitle => '在3分钟内解决尽可能多的谜题。'; @override - String get mobileShareGameURL => '分享棋局链接'; + String get mobilePuzzleStreakAbortWarning => '你将失去你目前的连胜,你的分数将被保存。'; @override - String get mobileShareGamePGN => '分享 PGN'; + String get mobilePuzzleThemesSubtitle => '从你最喜欢的开局解决谜题,或选择一个主题。'; @override - String get mobileSharePositionAsFEN => '保存局面为 FEN'; + String get mobilePuzzlesTab => '谜题'; @override - String get mobileShowVariations => '显示变着'; + String get mobileRecentSearches => '最近搜索'; @override - String get mobileHideVariation => '隐藏变着'; + String get mobileSettingsHapticFeedback => '震动反馈'; @override - String get mobileShowComments => '显示评论'; + String get mobileSettingsImmersiveMode => '沉浸模式'; @override - String get mobilePuzzleStormConfirmEndRun => '你想结束这组吗?'; + String get mobileSettingsImmersiveModeSubtitle => '下棋时隐藏系统界面。 如果您的操作受到屏幕边缘的系统导航手势干扰,请使用此功能。 适用于棋局和 Puzzle Storm 界面。'; @override - String get mobilePuzzleStormFilterNothingToShow => '没有显示,请更改过滤器'; + String get mobileSettingsTab => '设置'; @override - String get mobileCancelTakebackOffer => '取消悔棋请求'; + String get mobileShareGamePGN => '分享 PGN'; @override - String get mobileWaitingForOpponentToJoin => '正在等待对手加入...'; + String get mobileShareGameURL => '分享棋局链接'; @override - String get mobileBlindfoldMode => '盲棋'; + String get mobileSharePositionAsFEN => '保存局面为 FEN'; @override - String get mobileLiveStreamers => '主播'; + String get mobileSharePuzzle => '分享这个谜题'; @override - String get mobileCustomGameJoinAGame => '加入一局游戏'; + String get mobileShowComments => '显示评论'; @override - String get mobileCorrespondenceClearSavedMove => '清除已保存的着法'; + String get mobileShowResult => '显示结果'; @override - String get mobileSomethingWentWrong => '出了一些问题。'; + String get mobileShowVariations => '显示变着'; @override - String get mobileShowResult => '显示结果'; + String get mobileSomethingWentWrong => '出了一些问题。'; @override - String get mobilePuzzleThemesSubtitle => '从你最喜欢的开局解决谜题,或选择一个主题。'; + String get mobileSystemColors => '系统颜色'; @override - String get mobilePuzzleStormSubtitle => '在3分钟内解决尽可能多的谜题。'; + String get mobileTheme => 'Theme'; @override - String mobileGreeting(String param) { - return '你好,$param'; - } + String get mobileToolsTab => '工具'; @override - String get mobileGreetingWithoutName => '你好!'; + String get mobileWaitingForOpponentToJoin => '正在等待对手加入...'; @override - String get mobilePrefMagnifyDraggedPiece => '放大正在拖动的棋子'; + String get mobileWatchTab => '观看'; @override String get activityActivity => '动态'; @@ -518,6 +521,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get broadcastStandings => 'Standings'; + @override + String get broadcastOfficialStandings => 'Official Standings'; + @override String broadcastIframeHelp(String param) { return 'More options on the $param'; @@ -548,6 +554,36 @@ class AppLocalizationsZh extends AppLocalizations { @override String get broadcastScore => 'Score'; + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + @override String broadcastNbBroadcasts(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -1969,9 +2005,6 @@ class AppLocalizationsZh extends AppLocalizations { @override String get byCPL => '按厘兵损失'; - @override - String get openStudy => '进入研讨室'; - @override String get enable => '启用'; @@ -2639,9 +2672,6 @@ class AppLocalizationsZh extends AppLocalizations { @override String get unblock => '移出黑名单'; - @override - String get followsYou => '关注了你'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1开始关注$param2'; @@ -5357,6 +5387,11 @@ class AppLocalizationsZh extends AppLocalizations { @override String get studyYouCompletedThisLesson => '恭喜!你完成了这个课程!'; + @override + String studyPerPage(String param) { + return '$param per page'; + } + @override String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -5403,52 +5438,57 @@ class AppLocalizationsZhTw extends AppLocalizationsZh { AppLocalizationsZhTw(): super('zh_TW'); @override - String get mobileHomeTab => '首頁'; + String get mobileAllGames => '所有棋局'; @override - String get mobilePuzzlesTab => '謎題'; + String get mobileAreYouSure => '您確定嗎?'; @override - String get mobileToolsTab => '工具'; + String get mobileBlindfoldMode => '盲棋'; @override - String get mobileWatchTab => '觀戰'; + String get mobileCancelTakebackOffer => '取消悔棋請求'; @override - String get mobileSettingsTab => '設定'; + String get mobileClearButton => '清除'; @override - String get mobileMustBeLoggedIn => '你必須登入才能查看此頁面。'; + String get mobileCorrespondenceClearSavedMove => '清除已儲存移動'; @override - String get mobileSystemColors => '系統顏色'; + String get mobileCustomGameJoinAGame => '加入棋局'; @override String get mobileFeedbackButton => '問題反饋'; @override - String get mobileOkButton => '確認'; + String mobileGreeting(String param) { + return '您好, $param'; + } @override - String get mobileSettingsHapticFeedback => '震動回饋'; + String get mobileGreetingWithoutName => '您好'; @override - String get mobileSettingsImmersiveMode => '沉浸模式'; + String get mobileHideVariation => '隱藏變體'; @override - String get mobileSettingsImmersiveModeSubtitle => '在下棋和 Puzzle Storm 時隱藏系統界面。如果您受到螢幕邊緣的系統導航手勢干擾,可以使用此功能。'; + String get mobileHomeTab => '首頁'; @override - String get mobileNotFollowingAnyUser => '您未被任何使用者追蹤。'; + String get mobileLiveStreamers => 'Lichess 實況主'; @override - String get mobileAllGames => '所有棋局'; + String get mobileMustBeLoggedIn => '你必須登入才能查看此頁面。'; @override - String get mobileRecentSearches => '搜尋紀錄'; + String get mobileNoSearchResults => '沒有任何搜尋結果'; @override - String get mobileClearButton => '清除'; + String get mobileNotFollowingAnyUser => '您未被任何使用者追蹤。'; + + @override + String get mobileOkButton => '確認'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -5456,84 +5496,79 @@ class AppLocalizationsZhTw extends AppLocalizationsZh { } @override - String get mobileNoSearchResults => '沒有任何搜尋結果'; + String get mobilePrefMagnifyDraggedPiece => '放大被拖曳的棋子'; @override - String get mobileAreYouSure => '您確定嗎?'; + String get mobilePuzzleStormConfirmEndRun => '是否中斷於此?'; @override - String get mobilePuzzleStreakAbortWarning => '這將失去目前的連勝並且將儲存目前成績。'; + String get mobilePuzzleStormFilterNothingToShow => '沒有內容可顯示,請更改篩選條件'; @override String get mobilePuzzleStormNothingToShow => '沒有內容可顯示。您可以進行一些 Puzzle Storm 。'; @override - String get mobileSharePuzzle => '分享這個謎題'; - - @override - String get mobileShareGameURL => '分享對局網址'; + String get mobilePuzzleStormSubtitle => '在三分鐘內解開盡可能多的謎題'; @override - String get mobileShareGamePGN => '分享 PGN'; + String get mobilePuzzleStreakAbortWarning => '這將失去目前的連勝並且將儲存目前成績。'; @override - String get mobileSharePositionAsFEN => '以 FEN 分享棋局位置'; + String get mobilePuzzleThemesSubtitle => '從您喜歡的開局進行謎題,或選擇一個主題。'; @override - String get mobileShowVariations => '顯示變體'; + String get mobilePuzzlesTab => '謎題'; @override - String get mobileHideVariation => '隱藏變體'; + String get mobileRecentSearches => '搜尋紀錄'; @override - String get mobileShowComments => '顯示留言'; + String get mobileSettingsHapticFeedback => '震動回饋'; @override - String get mobilePuzzleStormConfirmEndRun => '是否中斷於此?'; + String get mobileSettingsImmersiveMode => '沉浸模式'; @override - String get mobilePuzzleStormFilterNothingToShow => '沒有內容可顯示,請更改篩選條件'; + String get mobileSettingsImmersiveModeSubtitle => '在下棋和 Puzzle Storm 時隱藏系統界面。如果您受到螢幕邊緣的系統導航手勢干擾,可以使用此功能。'; @override - String get mobileCancelTakebackOffer => '取消悔棋請求'; + String get mobileSettingsTab => '設定'; @override - String get mobileWaitingForOpponentToJoin => '正在等待對手加入...'; + String get mobileShareGamePGN => '分享 PGN'; @override - String get mobileBlindfoldMode => '盲棋'; + String get mobileShareGameURL => '分享對局網址'; @override - String get mobileLiveStreamers => 'Lichess 實況主'; + String get mobileSharePositionAsFEN => '以 FEN 分享棋局位置'; @override - String get mobileCustomGameJoinAGame => '加入棋局'; + String get mobileSharePuzzle => '分享這個謎題'; @override - String get mobileCorrespondenceClearSavedMove => '清除已儲存移動'; + String get mobileShowComments => '顯示留言'; @override - String get mobileSomethingWentWrong => '發生了一些問題。'; + String get mobileShowResult => '顯示結果'; @override - String get mobileShowResult => '顯示結果'; + String get mobileShowVariations => '顯示變體'; @override - String get mobilePuzzleThemesSubtitle => '從您喜歡的開局進行謎題,或選擇一個主題。'; + String get mobileSomethingWentWrong => '發生了一些問題。'; @override - String get mobilePuzzleStormSubtitle => '在三分鐘內解開盡可能多的謎題'; + String get mobileSystemColors => '系統顏色'; @override - String mobileGreeting(String param) { - return '您好, $param'; - } + String get mobileToolsTab => '工具'; @override - String get mobileGreetingWithoutName => '您好'; + String get mobileWaitingForOpponentToJoin => '正在等待對手加入...'; @override - String get mobilePrefMagnifyDraggedPiece => '放大被拖曳的棋子'; + String get mobileWatchTab => '觀戰'; @override String get activityActivity => '活動'; @@ -7352,9 +7387,6 @@ class AppLocalizationsZhTw extends AppLocalizationsZh { @override String get byCPL => '以厘兵損失'; - @override - String get openStudy => '開啟研究'; - @override String get enable => '啟用'; @@ -8019,9 +8051,6 @@ class AppLocalizationsZhTw extends AppLocalizationsZh { @override String get unblock => '移除出黑名單'; - @override - String get followsYou => '關注您'; - @override String xStartedFollowingY(String param1, String param2) { return '$param1開始關注$param2'; diff --git a/lib/src/view/settings/settings_tab_screen.dart b/lib/src/view/settings/settings_tab_screen.dart index 98b62dd96d..bfc3c6a9c1 100644 --- a/lib/src/view/settings/settings_tab_screen.dart +++ b/lib/src/view/settings/settings_tab_screen.dart @@ -254,7 +254,7 @@ class _Body extends ConsumerWidget { ), SettingsListTile( icon: const Icon(Icons.palette_outlined), - settingsLabel: const Text('Theme'), + settingsLabel: Text(context.l10n.mobileTheme), settingsValue: '${boardPrefs.boardTheme.label} / ${boardPrefs.pieceSet.label}', onTap: () { diff --git a/translation/source/mobile.xml b/translation/source/mobile.xml index 2822b3ef7f..afc8d841bf 100644 --- a/translation/source/mobile.xml +++ b/translation/source/mobile.xml @@ -1,46 +1,47 @@ + All games + Are you sure? + Blindfold + Cancel takeback offer + Clear + Clear saved move + Join a game + Feedback + Hello, %s + Hello + Hide variation Home - Puzzles - Tools - Watch - Settings + Live streamers You must be logged in to view this page. - System colors - Feedback + No results + You are not following any user. OK + Players with "%s" + Magnify dragged piece + Do you want to end this run? + Nothing to show, please change the filters + Nothing to show. Play some runs of Puzzle Storm. + Solve as many puzzles as possible in 3 minutes. + You will lose your current streak and your score will be saved. + Play puzzles from your favorite openings, or choose a theme. + Puzzles + Recent searches Haptic feedback Immersive mode Hide system UI while playing. Use this if you are bothered by the system's navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens. - You are not following any user. - All games - Recent searches - Clear - Players with "%s" - No results - Are you sure? - You will lose your current streak and your score will be saved. - Nothing to show. Play some runs of Puzzle Storm. - Share this puzzle - Share game URL + Settings Share PGN + Share game URL Share position as FEN - Show variations - Hide variation + Share this puzzle Show comments - Do you want to end this run? - Nothing to show, please change the filters - Cancel takeback offer - Waiting for opponent to join... - Blindfold - Live streamers - Join a game - Clear saved move - Something went wrong. Show result - Play puzzles from your favorite openings, or choose a theme. - Solve as many puzzles as possible in 3 minutes. - Hello, %s - Hello - Magnify dragged piece + Show variations + Something went wrong. + System colors + Theme + Tools + Waiting for opponent to join... + Watch From 4e316000fe0e5107f1ef7b226211224ba095b0c9 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 4 Dec 2024 22:16:10 +0100 Subject: [PATCH 845/979] Move android system colors to theme section --- .../view/settings/settings_tab_screen.dart | 19 ------------------ lib/src/view/settings/theme_screen.dart | 20 +++++++++++++++++++ 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/lib/src/view/settings/settings_tab_screen.dart b/lib/src/view/settings/settings_tab_screen.dart index bfc3c6a9c1..af1101463b 100644 --- a/lib/src/view/settings/settings_tab_screen.dart +++ b/lib/src/view/settings/settings_tab_screen.dart @@ -16,7 +16,6 @@ import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; -import 'package:lichess_mobile/src/utils/system.dart'; import 'package:lichess_mobile/src/view/account/profile_screen.dart'; import 'package:lichess_mobile/src/view/settings/app_background_mode_screen.dart'; import 'package:lichess_mobile/src/view/settings/theme_screen.dart'; @@ -97,8 +96,6 @@ class _Body extends ConsumerWidget { ref.read(preloadedDataProvider).requireValue.packageInfo; final dbSize = ref.watch(getDbSizeInBytesProvider); - final androidVersionAsync = ref.watch(androidVersionProvider); - final Widget? donateButton = userSession == null || userSession.user.isPatron != true ? PlatformListTile( @@ -208,22 +205,6 @@ class _Body extends ConsumerWidget { ); }, ), - if (Theme.of(context).platform == TargetPlatform.android) - androidVersionAsync.maybeWhen( - data: (version) => version != null && version.sdkInt >= 31 - ? SwitchSettingTile( - leading: const Icon(Icons.colorize_outlined), - title: Text(context.l10n.mobileSystemColors), - value: generalPrefs.systemColors, - onChanged: (value) { - ref - .read(generalPreferencesProvider.notifier) - .toggleSystemColors(); - }, - ) - : const SizedBox.shrink(), - orElse: () => const SizedBox.shrink(), - ), SettingsListTile( icon: const Icon(Icons.brightness_medium_outlined), settingsLabel: Text(context.l10n.background), diff --git a/lib/src/view/settings/theme_screen.dart b/lib/src/view/settings/theme_screen.dart index 8f34cc8d0d..a4de7ca25d 100644 --- a/lib/src/view/settings/theme_screen.dart +++ b/lib/src/view/settings/theme_screen.dart @@ -6,9 +6,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; +import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/utils/system.dart'; import 'package:lichess_mobile/src/view/settings/board_theme_screen.dart'; import 'package:lichess_mobile/src/view/settings/piece_set_screen.dart'; import 'package:lichess_mobile/src/widgets/adaptive_choice_picker.dart'; @@ -43,7 +45,9 @@ String shapeColorL10n( class _Body extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final generalPrefs = ref.watch(generalPreferencesProvider); final boardPrefs = ref.watch(boardPreferencesProvider); + final androidVersionAsync = ref.watch(androidVersionProvider); const horizontalPadding = 16.0; @@ -92,6 +96,22 @@ class _Body extends ConsumerWidget { ListSection( hasLeading: true, children: [ + if (Theme.of(context).platform == TargetPlatform.android) + androidVersionAsync.maybeWhen( + data: (version) => version != null && version.sdkInt >= 31 + ? SwitchSettingTile( + leading: const Icon(Icons.colorize_outlined), + title: Text(context.l10n.mobileSystemColors), + value: generalPrefs.systemColors, + onChanged: (value) { + ref + .read(generalPreferencesProvider.notifier) + .toggleSystemColors(); + }, + ) + : const SizedBox.shrink(), + orElse: () => const SizedBox.shrink(), + ), SettingsListTile( icon: const Icon(LichessIcons.chess_board), settingsLabel: Text(context.l10n.board), From 798849ebb7fc28b37ea3147dcae48066c812fe0c Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 5 Dec 2024 13:54:26 +0100 Subject: [PATCH 846/979] Spawn an isolate to avoid UI freezes when getting image color --- assets/images/broadcast_image.png | Bin 0 -> 224372 bytes .../model/broadcast/broadcast_providers.dart | 14 ++ lib/src/utils/image.dart | 137 +++++++++++++++++ .../view/broadcast/broadcast_list_screen.dart | 143 ++++++++++++------ pubspec.lock | 2 +- pubspec.yaml | 1 + .../broadcasts_list_screen_test.dart | 41 ++++- 7 files changed, 284 insertions(+), 54 deletions(-) create mode 100644 assets/images/broadcast_image.png create mode 100644 lib/src/utils/image.dart diff --git a/assets/images/broadcast_image.png b/assets/images/broadcast_image.png new file mode 100644 index 0000000000000000000000000000000000000000..fed57f32936b16c9f71f117f915e5866321aca18 GIT binary patch literal 224372 zcmV(|K+(U6P)90004mX+uL$Nkc;* zaB^>EX>4Tx04R}tkv&MmKpe$iQ%hA^9PA+CkfA!+MT}?mh0_0Ya# zo%23%gq3BL_?&pspbHW|a$RxxjdRIifoDdHY-XM~LM)bgSm|L_HZ^gBRM1zJxlVHoDJ)_M5=1Cypn@ta#Aww?F_EGDq=$dh@k``V$W;L& z#{z25AUl5WKlt6PS)877lR`-#@M7B^<3Mm1Xg6*9``EVICxHJMxYE1+S__!{B)!?y zqDR2cHgIv>)s#Kpat9cBs>_D#NPe0^u?W1M(KqFR;agyE&F!tTkJASrOI@XJfP+I| zqD0y29`Ek&?d{()o&J6Rfh2OQ)ogB?00006VoOIv0KWi%03GNhIn)3E010qNS#tmY z4#WTe4#WYKD-Ig~03ZNKL_t(|+U&hqvK&FOE9NNUHnczOPy2XFD(Hii5g0t#SI_h# zQ}d$-@|+qnGa}ppIDr4-|NLM7*W@3}4CTM&?+6ZvA!fjtf&38tHORleHu(F0o|k-n z6#9R*Kg;LP_a*szy}y4+>aSrCFMQwsz92D!--qSzukY`lpE{pg-}mol`TJAb<(l|< z&ns@P&ELO2|N30#vEO*!X?qX5vhr`d?||RmGj8j-ru_ZebCqcKk&qilF5eP%mGy&y z`c0@;)b91)_xZgB=jDpV=YD=lZoqjR!tTrZ`Tj?L`1{`f`1k+G>u%5T+nW1-`Um$y z=VUz<{t^SnOdbjJCtxSthI)A9Q~%C*J4hkyT` zR+>pCX6426I#2Tm=rwx2tH1vIUZ208e@YyH_xH*3d-Hr&xt@?`?r(WSDf4K>^L<|_ zbr7$opAYwb=yUt`XXGAp?7rRmahp5X^QHGeAM541^Y?rFtw;Oba;tH#kv|B}O}h63 zvVK1^_TQ-g{@%4{&((dO&;6jbj_R4z0@|+c`1Q0k!@r+l^s9ZV;6!%y^R>_MTwnd} z!1wpO?XPv8h3CHiW51Yv52CJZ@UD5im(-G(r`X5P_B~W-PWz5Lz{$2(=j-VA%KdC! zN_$32b-x?v*F~Sv``rHBha3cXuJirikmne&t|polPMl9a;0WhuSoSj4}Va!eOCOoU9u21oPLYWJn+8g^IkYr2kvFahU8aKDBp43L=e7n zP*biq5isG`aZ7%MF3TqN{iVrHNDar}VteS00dTB;hRFxqi~ zq@4_wWMWk%`={6KSAspPtQLCoTbgLOzPesBy&enwW=IG^XLDc}=LBN&t#pcqU6Ho6{fLyq%0_nFdXmC}sM73KJvvqF~lb@=SH zzK38f{JTJYu9Y`)Z{IDJ*s_lAJ?}C2h&tBZWmx~q#+%*e0OY`}z*O>p&YrJ%Dwp`Aw?V$-LSPlOi|H`a2{n>NH^|B!TEe9eWPAi8e=xX{G47*ix(^MtkicIye}FMbU7Z`@4^?g|&XC^SmG!NtIsi zAS5{06@LhKfJ%8Ob4uOX*R4`qFt|+iE?QK z6-eC{9}c*C0p~outGSg1lCy2t3BVyhA^#rlJr~~vykN+8U;DQIjX(Uy>++u*>Hj|& zklz9F$b1m{^jYAPLrPf)JX(7J424Pg%Pz>FRvkI+CIQ$IyR#5lSVqw%1@avm!0mUi zA8iWYor4qo@GFW*E%0y|7TrWX&mk61GbTWprzrwWp5e^LKTrzDYitGAK|T}7lz;Q&hk5`ey+|NOM_9qwsLX{6tXrCW0Esdpvg#4 z1ut`hC{^9oNZ|Lp{r#-pamf`7QNNp(tc|Sy3^d3FZmMXs0uWbf_dI~md%`}2Aq)X8 z=X9lYc!o+{FBSOJ(s{Rp@sGk*4C@O6_+pEJ243*=YWK%aTS%QRd!7YdjxC35cb zdcUJd%%VTnkpczSSe0rjeJ$N>lV@K=tlfDlnHBU|&Mictr_C9c2{0D03IUMEfENPi zIZ2MZ?|HrlZEtt>E@#$iGPgc8>-OfS@`*ydC;gwZAqXo+n4g8ljG_QPpYQum#M!?f z<|#eG`=@TcGn?N(_Vw<67-9=4P_W8~U^KH8p`artx{1bNt_US$gCGJy)`BmQHYkGc z2bC3j6NA;yg-;2EQm^n6k8&^<qUNi72_gGO{2qEVMSf&^$=UQ{eoXvCmV$@XESmC;dHR^v3pS(AeX{D3A#L`LeLJ z^(#%J9&RW_l=QCO9A$8#@srIl(v4Y zaDHT6EY*egTCk?{n8q9_a8X7HFJ z0N8nyL&k+gYJSd5jxZ7W+4Xf&WR_=@OAl`omq1OZE#i4Rtcad7<8o?nQAl88%XbMV zLKdIT5MJbGTdBOwKc$S*0X5c9gTmXay(k$OY@3qQ+<-1|+ zZ@i^(_A1+k{=OUUyOsi{5&OHK{y>rBEGWj&T>4)BD=y(OiFKotB9RO7DpDb3HtYr} zyhRxDj)~trD0X+UI=^xTwB@y39)?12lGT`CEwH#5fm*PK;|!ddPbrBl0)%1>@^HA= z*lq`ES1BA3GZMD>BR=Q26v`1Tzo%M^RsVox=7RB^kYbc>PAH5Ov1^VC z+YrZmES}0?+Gz@6RP*EM2hl3~=Ez)^0MP0R- zyXgHv`$0FomIB8QRl4T*Vd{(|^trXNzs}c@_fG~8I+KTJvM?qsd)SoEDefH|&3Om} zs8JO0G74}jnx|oVUPt9eu6!@OS{O_?1EQNO4OPDP^?TPIbFY|a{&ojiYh^aU`4ws2!ii;R#mHkmWi|0B~kyC~Q z5Jw_v1!K;EsG&_^H6n{)uoqNj(q-JUVy`|mWpfsEsfqztR&2eE+K+*jj!c8s@NWR3PSSkCVSbH07RSAboWAbq95n*}_& z(p5twNmn^g81#^Wdb|PcnwY9Uo630b+lV7rBLR0fJqI(Bq;Bjzr!HVvG{kj$Sgw#? zy4dT2I6K>UXZ(0vN}sc`JpwPLPJ3^aq>cccccRjr9jOP5HD`Z6_aEE*6+K5Fi<@r+ z+=a*G4?5IKcZsQ|T$qRK`k2xa$byeU34VYp%9OfoJ9Kow{R{^`zx(y~(C=7Ua4l{e z4${F}m0UUd5r|4ORxB&xZ?i60I9X%Km3mdl+Ed6dS6dXuP{qV@z*zRdxVzA%y*A_o z-Xz^-OSxaPMWiw)IzIqb!8am5`CF`tUecsmcVqe-&;v4mR788Deo%JIXEX?=Ca;k6 zsyUhNUHnN^cq9szr!uT(7%|Lf_mABGgA-apY3h!F#G(zyOM?|(XmiWMl0#%O22gZ7 znoPRP#HBQ>z38$Xc&F%1_(Y}G7CQ0{iL1bMbiL#fzPJ-8AG5r_M64M9wfXbfKT==U2YB`L5$)V{xA5(~X|AA-u=ix7oqp z939n^fp?uSSyDAe+Dw_w_5H4Z5oVE&Y$mD_DTD z8Q*mT73Xy#X_k?L5oCYq_+0Lzd3XVRVj%UGF5-t7}y}i__bm zp?`MP#RrV31)oY=^iV_;#OV1VolRoSWWbraWYt<8kC6I73T8AKT7&nrfSZpM|FHeT z)_7_37hCVi`w5pzPD_n|bof)L=tPmKjNjr2I8rns3e0qGP}2f-H3a&s4ju_i%noGk zhkq{xEJ9ARIHpkm5B?BSZIG}y9{&CLkZIW04FCr8B8p?@ikb?*=8Rz4^OTdFT9oNn z7gEzO92i|MD#4*!UR$wFR$*qDNbrzdZ``oJ-6-d|frn!rQ;m@4l@i00DzJ0lvWm_c z-$C`D1n8j3Wlj<15AK%f6Y0TnpYH@$I{+El<9i0+QmvG?ElQWeuGuWiHizJAuri6F z?pO^cP)pkj&jL@=Vw)wGzxilhA3%)|@GD@=EJ0LUj|2D4B7%E|0p*i8hEAO!RZ|xgJ<%|GX9exu}_Zn6s?v zg(dUqN(~~|_-g0@&Nr6#)%y7XOaHiI&w|*$4e07%0Cwk`De&_c8aZm;|_k5#|lKv_4P`ziaHMA}ZTx4wv`!|bJX+%~OliG4Pzzzh{!{%Ir4y=Z4 zD{B5LaxtK2oo=+z5UX4bFBYKo3+&%F7LkGAMsd50nArRSWVR^R$4or4xsRdLybX#t z`GtU@(>AmkHNJy5HX9&KNO`H$;jfCTWb-!3>QwWEY1b!8aNsjz1@s899)O3T{H>Xf zDY0Ndd}Pq4!tz;u1=8i`B8?w7=;pJA=)rB>iDDGSXCLD)&2SMdT8N72=28&+|&qgJK(ON$NWZRV!}x3#six# zgUaZY7(tAN)sgMqEq{mQ!CXTh-m z-KlJ0e3AN{UwQTikt@I#3Z)`^z@1U37$TQtyqR}lp+Hc_NR4Etev zZte$grX@9y(<#!WrCdg^^Z>J%qpBe{OSm|oxA(v{P(WMchbA=LwVsIG&v)_AT6mSJ zp(^h9&e%i7e9-k;8J9F>xER+(7z|v053uUBl(%p;N0p$Oi)vJrcNFwuTYdLH9awLF zd~GrbmTXQJvN00%4B?=`#ujRmvUt(oViRT(P#(zIlmwsedp?(W5Is@pGiN$Kkx4nH zPRk}OhdikHYaa6!de)Jmv6f+C42U~GO&l)Sww zzp;sZrz3KjhxBb&pYHE!`-nhEHt~kbt%5};dD&*U#+5DI|Ay=P9{JSjBkkX%9inqk zk?Z?Ecjx0{Nlj2`Lthe^S`aJI%3Eyw%p-crI4Oxq{E(90Pf2_&6+5o*(=FU`~) zcAv$VFcAE4d&QDdlpl9yMX(DIaZ7tPNJIDn1;gyg!_5FLjpw*7`h zIX4@dkFa#f0$A+WuV{b`UQwhiV}&7-?k5hS_&LpVTgqCKOW#N9a2ikjCIUqInN(ah z4J$~LwqRI>5ugX&G7P0wz6a^YE?K>{rI95P&6SATkODV-kA*q z)u*yxw~C)K1E7Kh^6<>HPfQ=om7xcRdJAEdIl4jwIV!&WwSOOd1kYQ z#Fmc?N?*H*%JimrRLD;9UuxEfb;hsR*_)?@Zm2SMs z*OuoTeU;Pb!VGrEHKcJxGF6^6J*aO66-a*-*_RG_Nz2&DEGWkO!MS^_cS# zuYK+^0Ke)p7TX}`s&lgZCnt&gBf7D~ClmLi$)e2O%<%VGVL*@(>6Hca6xWzie4>Lx z{l;E9)a%f;tnUmN3_B%Y8{eSO5-?dAtO-f`+>x^i9}`T00^5wyE1yBo$b_1sQpq3O z1jd{wRZbh>3Z{546~s@?0+!3F0ZvvwB4kre?REZJ7TQ zYMWdK<#(4M{S3vK3_F7dMulMQe&($Rd=JCH+9b`)%QtzO)0kjXw&ux3Sek^?hIcON)Kjt-;+b=rEnGLsTqJg0WOG(u5Oj-~tIA)w2ZJF#-HamNuj;og zw-QPyPO!V>y+BX1Y&8hXKB*kJFR8d z7~K(kpz{|nm=r5piW!%`oRF$TG@v3((N?i0K6KY=JW{+cG<{v2gD|AkF#A-3Em=a0`=* zdUz=Eo{og?a@?6HfB|wM=ORFq8mOPyI_hjxAPk2^20Uv+%pi~s4`&EH&ol>EkW3ns z<(zO48@qSUy=6Fznre40d%>q*)>%J2LnCBYi}_T|Jl5J=3`I)uqM@IGGiWnmW)=c0 zAw!;%Q54X{PLIuPOgbKh^JW7CY8o*&Bj{nh!2h7cSwNBnKqLbYjqBpiI(r6WKR~@W zbxk))uU+4whRKDuFn?w&gOx%1abqDk+q4QY3~6R5@L%pSb0QM~3~kb##{N9)2&QRf zU5~GX%FUvgT-YRL+1B}w?yBO2Y&sRB)6d^wz3=tLhiLbSPGYV(m?>L7d$4t}G6v=u zDfdyZd}bOwjq77pw~R)SOJNEbT3?F9EHyt%gPEV_Q%!F2LR*$EJDlzmoOex3xJOF> z03ZNKL_t)1teB7DYn4-SVHBt5X{wP-_A;_HmJ+OgH+}KNK5`jwn`*>FTWhbQ%^lah zYv(w+xMmFFPnDIq<})9OW_vwV#D8bE?4T!=>*vdvD-F=Yza*?7k;!Iqre=szYR1yr zhgEK9^0_#!aXjDWKgnBH>C|}9zAFz&_1XQYvZ|_}R2_(s}~35jY{SLpG7}G(n4fpAmD(HC|K_VlJcZXxnq8 zP(u{06emf74(=_@lLvA9BX?tNq0V5;lPvX^8)-(7Qeq!rGiStwC1kO>v`i(w=8?72 zXU!N+B#MKPbncAEHv8l*qRGCx3IK;$gULg42*zZuB@Mo-xXxgaV_GCZ1WVffiWhLo znkIBekIf#*|2EM8W8HJyVB&6=FjnHOf1qm&JwL{r1%s8a{1bmp5P-%^0_|cTgs2f8B`QyYWehUg|oZ5RCrSy#2<3SII-+B z)+BIV)3>b%+N?ay+D~qw}LTf{b$Z(xsdm zz>RpQTzj4+91;KyKWcA$)VVOGVzfuy7*J8L(KF+=#HbCa;Rc|t@^ z27|H7#Wo)8T2Ed_3u`gN&WTy@*gf6~(YJY}-CmMQv5JKtLo`T+g6H62L9dr_teG56 zIpCR_+Uwd!u`_yVer7?Y*XHQ0=|ss)=Vf@@*GkU!%*))77MSeowOXBbi-c9+ILo@= zyR;VD>=AqjG+wbKwcUc?6-@cIBy4Fq@sGA6&#bA>$MwGVUW1ng_Zw|dNLF5$cLtNV zJ;m2~FrP^l1eTv2FZR?n$5Rryh0MALt0j3VOE_wh{EKvyF}{Gj@w1(liB*#bAhdRN%jQ3=~D-RyeK0P-(C93 z$05KHH?<>jOWYeqCX7LebjJOvu>f}G^HD;+7-t?sbNMVhNt|CanG*|V+sHiNk3Xbe z7B*+)%di%k)9*hM#iZv_*!4`0x^de%taJZ?P9$y29TngH9&TvwKKo0FvgeA9WO>M7 z9Vlx8CSWimWus5}9n;AI=`}2NfZQ9&%Gn3e}n@f_p%TNCW5VZ8f z8~&WO{MK~Xe*zDDrijy#01S;q^7@?~Z!@fz1r((O*={ULgAzGk)?iftB^6;Wvp=su zS2up2QLEjII9Ds>a2^y@fZ3?P84Sb%#3EzP^M0NAo6Qnd_FVLkcMpAU0oJnuE1Ox5 zLpeDm%h!A}FUMI^lBHI`4uojd8A=jGi>4@W>-rj-MYiU|agxao8wHuasVW9tYnbaM zIDfW+?@ z#Nk5mo0f1nta5Ha9V?WcLxkDr5K`GJJUYL9IGy^v^DHu6tdlWWVQ6y1uu^p5M{(>m zPdJSp;#eFVI2Dq{C2XohfYRvk?XY4t%=z;v7h@DAnZkalPHXzkJ-SjjX9SnSSCiI9 z{mN+wVg)sw|M^Ym#pGCgVHt|mOd7m;6Po)J%K*Y z%goYb;8=tZuGXd-qA%Zp%RJIVz}Wq@XfGiW7XvZGxk#T;vlZ25Kcregt!XxeUIPy8 zIaA=lt$zJ$+i>WN4J_+tC#P&=djd)YX=$ht<&WkTl}(Z#4Ja-#7|LZ{?KH=XRAx3< z!EH$2WSh51={lCF*n-7on|ONt>DMii?(J(bg67(K3zf-qG;OB3JR4)K~{2D3%Rmdw}* zA;m=^nB~?b%*v|P&m(1uT;*f)m=@R?E}>@0MQ@YxEm07*{Q_YMX!6i+hbf~CU+^M+ z+>BU00hDl-;^M`keYNB-s1Qq}0QWf}78wGKfdK7U1Fdq00{8=?PGBmlLok~!j+&!6Rhno7i(V`8uLNN=mF{G6l(2iDXsSG=%DJ? zT0=KViM>Jg1Knyfso0Y1t*>R}qHT(PV_?}%bVdSfnSIrqr!M{L)HC@)ork-wzrR^6j7G$CZ{oT9k%=>o<&&l zd^eQupbb}t#*fLomdu)%KWHd}Gb@OaP0eP*off&=d9G!CMVq>o4OlEZ;`$Ii{-pGd|?F(Bzc(jS>>Cv*tsb1w>vTNMV7nGO*UVSbtX?f z5%>>gkpGl%Rvj7U85ZJc@=UJgN!#a0`KQUQRtA}?^mt}|49MD~qoq*lh=Hcz)6)J7 zhEWrWFY-U!DL^0ixu~e3w3p)XL{XoQ5Z1=%78=6B48)@#=In%G9gNRpYH(seiEmjo z_FXQE0{`DLEnhhsG)_Sa=@JY91+o$LhDgyIHsZ*Wa6UHoaval6h|Tw&x>YER&DePH zB4Ifgli;Z3LALCvI4Iv}so7S5VOGg8PN z8h#0Jno9I;w!>Uha$ZU954QQAlB;<-Y*z3vDtD{^D}*x&RE*d;otZ6E7J$cN#I<<+ z>>f5|`->IcFnQC3&F}E0Gw@u8D-(f-X}X&*_kvPf%xpMSn6zYwG*C*`Gs7F-WtYF; zz$j2~au6w>0l%fla_lMT*^-=lC3dKF`4@2b+WKOgf7^^0rxE>R-s&ugqx&Nob1cKP zA1hKBU)tF>E*|;bOt}O(tu`(AE1Ne>0%y!%41LX5lL{NgZ26}HMyM_v1r}J5m01(7 zw({eA!(*Xp4pYHSldsa6nNT7S>s(pQ1%xyZ3tpa5RG9oV#a5BM6ix1#MU?s?N)wmd z@g*fprA?WAcxE2Ad8aF}KI?0fQBT;x;O+q_VMU2a7i;$yhiQM`4U}6o25m(@vEMCC zS4Gw+tyHpg zRGhLp^BS&&uZFyx+q)bfIXW}s3Vef$f{#JZ6U~_8YjT6EX^>eLF)x+PPZD~%c>>ci znUIBIHaX`_RkvhfqVfq;g9Nl}aijQ6zE&64%SAG02>dxkS*_DS5;jt8L-)$eab7I7 zC9En7!n6a5fGq`OQVO`?uSimoIgIS(4#Jw0#^@oW#mP{F)Qqemv(QWGoZc)+JTcU7 zU`*&*r7;}t(qLBBlR8EHoue|*#oPUpXQO(7i5Vx!b-3ppOd`WgHZpHxP*MQG4Q#e& ztjukg4u$dgI!1nZ&%Cy1S?R{&+P)J`PcKsT1Y(eJQtGFRYSBZ^#Y%u|~`DVWu$ z;VOg*N%BQusOXc>eKvt_4{~ay8uoEHi-Pp)$08j8%kJ3k7(3iN{N8P6W1xIk*ga+M zO6u2PZZqsKZ7nc#20(3_rHOO*lbA)a_5SGnVorccWee6%EVCsdyPH?Dr14*q%!XtT zt>i+aR&!GQrfZcYA<4vUVe?c{Gr>A)kR9QS1a33U^r9RZ%lR~(Jedv~B8T83-eknm z*g@^NH(8vb`}B|~E{VeEa7U+=RL;7vYEfv}z7P^H)8jAlHI+zS;K{$g+U#o4=u!&4 zN=0dF%0<;;4q z)5Q{mh_pJ+-`TO2C0JV7fbl#qE;JOeOXF<$q}T{H2jL7usMYoF(-m|!_7N-7ir556 zO)rOSJ>Hb&EY#wvM|cgPb$TX}NYnX%mevo*JS$^g|1;JcEI}~JX#5uANA&5(Y2YkW zF4kZ^Uva4Lqg#&9;Wy`!bc_n`?N2f#W&_5uY6520Sc1#@an*8~+DqkA?4}4M+j5D{ z1KH_&Yb{n5zcYK_(1X4${+ERWA+K#34@?xczhc*l6GIhBUMhIJG zbGhtfVG^|QOX(-(W$s4}C4?PSZ4rH4y5SvNiYSaB@)Y{92psepX4--W1Dz;YFz zDn0+VIr9U3ezv0bti#qr7IV!E^u-b~cI2*fN6BDk^SruwSAW;*=Sl635hn!VKXf($ z3oD)lBNQl0R#{|a%qEq4UQ?-~MK%DNO+*7ZZ(jTCy&MGqq1eV`!jfqprpYor2QZB> za?ao<0Yx>+;qQezXJ^lEWMkpTHDLuRGmwrcw1UZVXtPeRIUHjLmSA7jp5e*bX1bw* zuXt`$C6ILs#qIDpOHH1YE|ud_NalOxj0_rJ9F?)#^UkuG$0D<9G-sN{NSSRmT)2@E zP4xK`SQ*f0U5AnTDi-*4pQj9*&;qysaAY$}Fo(-$k}vvaBt(bSNcfHUOBXvbOteEs zh4Rdib^2%$^ux3#wRsbtLmVC;WP^ZW13YSh>!hAyNEkiz!(}QHeIpNp*sK#h&)|7r zJ{tI^?ov=qtB2)iA&bHvzAZ-@FIvcOm15FkH--N1n*{QzzR}cxBlwWH!7pDcj!{80 zKAb)4|LL-(`Ix*I;JnQ7zkJ7WC&&{RuN5Lnt3EsGQKXg=&Wv@lY=jjKk%4N z#P@xbsV>-Q6d^Vfw#YoSSpF;jg*0c6#(fN^vnDpcAsE9T5*rogFd8`cJcYY;dCHxE zH+&MPF_s0kG?}qHDu-yD)lpmrQigC>3j$umnp>)c_;N)zu-Mh7g((E!AY5JJLCW_Y zACN=?bjQ(aP3%6Ocah1sWE#P+)7?2kZdtedXB3f6ksqog=b3?WLzW3=wxo+-vLi1% zFOnedBo?l^4gDq|k(QdQ=5Bqh0|t|DLN~kEANmpFU7=QKKA5+FwyO8@WW!gT^91UTMf-x(SjNG$P57HW z7t4U4COS@QNmq;FGI&7DlH{NhOxBVKxzvs^gCEyUerkr~X_hQ8*9tplKkgw8XCr1x z^QXExni~!rq-TU&E>T@+n)nQpGZ2z{gT7-a(J`b<>jJ91kE=YB;sy|l#AHgcl<2nV z9f^fdz|chta7 zW7Q;B5)0E-4Q=`%T@+Otc0~^DP(d(=#h9R2F2i_b9nN&#jTW)Y91E(<#cf2*vF$0I zLvHA!Q@o73$ezq)4b8^={r&IVw^Mo=2)f4!g#kqb>iP^qNKaEPV{0YWNtr%$A(=s8 z2ce~>e``Z_oSt$IiL=|b2Bm_<=@4xrO=S8$#^pJh^H zk>C~r&+ytm7neTQ3tkt^BT=KU?)U6vQJl$~-C0DI1eY>D9G7yvR=H}bQehB-|7y4Vi)-#1`GO+5gh#kqiI^CX=mJS$S4cOF(zB zeSZv?1 zxxsti(2XEcC!Op0SgY}!mt0M|ek%&E)VLLbdNAC3)pmhcZayp6X_d(2?8b(E#O8qT z)3!d&P^QHNDcC14Q%|R@+6)0@`HQ;+-9F!u`4TL)$LF5XB#z@11cs(J*0p7;K(7%m zW3~Eqb1HpyZ8mEQxGrWsm+p-m;;>zy?8Uz#8efhzx+(|cQtS~vqk|>V=@ypvb9@kz z2iGV11`T@p@H}Y$J@~wv`ikj+&li0jCKJLyA+@s-etPz}ZyylagyjJRUQIuC)^ai! zv6?9dX)yHS6s>9YA}t-*W}UUsLK3-#A69SZ2d6pnh*7iIw{HMZ45Wf~>o2P3>@vdj z#&zlUywg+bxZxq>8;?vy>}1dLPMu%!oq`F<9V`1r6A^UCK9d-78(m{)6Ng4h-P&D| zTSmwjC@zRP!;rJcXX&bj(5+#5dp&J`W`Rb z?p|~E2yCIIWrYuC-5K&yx~#Vf)MP8BXYy)Jso2IbVKaE@O1TS-2bC;&3`)Op`NTR3}wo{?As6p!Q;OF$N=C(*|zvVfDT^j-7{E z_KKUN{z$fA{dC`wFAROmmM3vzgUd^C`i; z22}mQ?6VDT1vH|_-4ZrYg&lXKy)<~KtK{+>JjBhiM%F(`T;W(!7P_;{gH$LH_B2sl zxyZSu$S-mA-mF;*o-&W~aAH9^X?LT$SH?oM{uNkseuxmTWEB>qT89L!0MurYV%``$ zh!vAu*w+{(21U;dqpK%Ax5A$3)&P~q@p}tPD^?#TB$Vd=Fd0fWhn&RZTv;$~jz%># z83*FL^|g_MqYch@h+*Hq)W{}-5c*dLqkHF$oIDyhV z2OB(w%d9C?VuZHyOsUmhBZnn5)>Bpl$qOkY(mrus%^6dlTUOdO$iK<6!oNOH{n|%k zu%9Nk%Z=&lvZu}VcCi4c>=dGb0d#Qf=QnquASFT@ufO|=edVL>idfD&Cc>Fb7v+27 z=blI#@;%QXBEO+#5}8i-?O`7>HJg#+V)T&~*;ouFGV6;D7&)V|~EV6%h|yn8xGTmjWLfTN&#{U{*wbQLoBJ1?Fe-&_LjgF z2DI%+c+x+~yueY1n6WT7kq5QHt_VdHXLp0nImNA*+q{i?5p58{lojZeKz*ZeLllRN zY-V^+=1i2=c_6`^+tQMeO@;?CAn-Yc?MlSKQj7{L_&A5ZY6fRAu-=PnEVCnAL{>hq z$BiJmW(w#e=P`#hx&%?$C>jCQb|!9*H?-=$ zHi68EoRIS0#@6A^rReMpm)Q_~(#kkurmSaKg%;atmvP16g1LoQPM6tN-be(#h*uEZ3w0Y)uQLP zvQ~l21VJ7Qla9^q=i<#kHbcUeU=shgVPw`!$W*bf%vp`45;jatopHpXE1$5W*R@h$ z0bYSI$PjQ!Pt%R-N@f0nF%xr+mWqx{-RLvt)(En8LLzmGnsA@`$wLWcx^M8W12^B! zBFUli1YNMH0;cu>L^B8kV>4mwg0$?Hy+@ci1c*0ShIcVb=%QpdNYHHo8A$tIiG}0z z9Hh<9%3MSm#3E`pQH69qeB1h-*%(=$b~c>#Hj)jpxOdVSjs=guYg$zebE75nyi+jx zC~xSfzJb4JgO;2nA|6#vNX&j$>I1j1y-nTM)^y~PN{!*NNzJXhGPZB(MGa&Db3{Q* z8*uZZPM|a7jGGMv38XaSr3eSYKNB`#oR@IGg(XM}+&l`BQslgcC*>iwCFXM6n>ej* z-~8`qS)%@LZY6qYWydCI3wbu)!Ui|Z|49bGa&Qk3l0>xsQ47%il@pDScwq@EYDA(8 z1u)etRa5aI*K1aVqBovOJ$?0>j|LaOKiH}V{X{*-5Y)uo&roUfYtUYq@vt`RH$|H# z<#wUSwaMh(BtoSFG>124W9>V;2&T#homrkdWV>TE}jQ?DH<%O*m5O6T2QZ_)pyv>;10w$e40to|%xH zHEtHjC+Pg}8f2*9gYn*sxvCD@scGb6pixDB?2BVd>&2=cF2Mo=1PVYJk*ScmPUoWi zZQMQuJ%3#;H9ix0z+9QD#;el8YaIkV?lW@1HaRYHW!}GHnJSlB=0EE*7?K_!29tWb zEP0zmxIznTin{o6oK001BWNkl8@Sa{O)@+c`-Mwkf+Pt>C zp*e?5x{I(V+YFU!8wo}uw=rQevy8dU>-Z~+^Ke`-2aY8q30-)m#l*a}ue8;Qy#!wx z%r>%~5`(?5S31Zz$w7Ei9gzE*#Boc{?)xE9lo$Ll*-Z5G=u+s-%4lxa;1-YHwJ`Pd z%m{y+oo9thb}%{2`og|j4lHfZ6UF{;m(+$Y0W$cufT@45t?0tIZ5w2z({08lWmxun z>g*6Fp(JS2%t(+&S}H?KIv`93`WfG(CgUa_G)aZqMi>{$1-p!6L-S)O=MAHIvK)?1 zu`7s48`RS4=Cm-bOziUu2>pn`O`71CwDmkt-t>?7BDo?9{SxXf-ikRG<(*SG^EqV|CREOa?n+Nax78Zz@^Lp<6fio~(b z4zUm)|L1j{5gvXznsX-ofa%&GHoT}c?KcUbS6_=tTbv^{u)7l~7ID_{%*#U*0bz_} zRFgrCSwnkv4#O#gt>2p^10rk>Z-Tcsv1xC{I52JKl^sHb@sPNa$z|+{YcqB5O#RHg zV1*%`W1(G=L{DFY5L?m>wOor&w=xP%W)}SFRo$iHrTa1vFQ`5J{OS<96z2K#Vz_Vt zosf!ZQn5A_o)S7SaLJwR6L*+H=dT6+rnSY%kf_EevZD;0Uh*Y6XlcwRywZdX+yBOD zX0U(rB8Qw;DVvu^YSTIOy1JQnX22{iG_yCIS$uN)S1t_4{$2R`-5BB)uIAdU6UOEW zzci?r)rD#id~uZqqKm8CAw>s{0%8(&;V##F(U9vi{{a>jD4D)l+URj~I-k#Y&yN{Z z;H6bor-aS%vd?Hu1!K```n@T%|0A9Lo#|!A=vBF zD2~q!bN1lrV&ATO+|FH{<=bW$E7K?>rNi~vZss)1Y-(LZkb5y9FRq5^oSJn*%-u!b zAFM4Sc$&_{$1@Hp1G30a40YC1JrcdWzzRQmW`N79TKRl|4^uxKsmF?lm@5dXV6XyR z>FtnqsoPP>Pq#!~^La)zXY|1#(Y)D0VqYV;2IZpZ>Gf_kl^k;B{he*yf@G$n!R8>r z1wBol`XvDBqDSAWnH-wsU=J@XGP9(>l_e$j;@WoqZqz)YU@IhHO=dD7X052@Nakqr z4%hO9gfbit?)srzGSz;{@vNbKPG-v`WFy)W*1lvxHg|Q}gqn8Bfr8w<^ z(;Ybwg)VG{O?1!kS%#hrehzT4H*{AN#0Z%(OI}QdXx7(Oe0+Z5ZknpNT-bR0Q*=8EYX^b4W@%}? zD7`kFEu^c0@D&aUiPX~ct$X!+qd>d`XBz1A%YK)}ahdPi91ADr0{#J}*mD6iLjx{S zDwe+9q_64EW6Tt*q1~B`&CWedNLrR#b4^uH>wFiMhBEdGNFhDhUKj-MTJbS zSQIIg{XXdHX^{2X%Jh!UnL3v!SzTT881R!$mx20vYwXp0%# z58Os<5=|@(X@nHV0eJO@R(pHW<#;d=V&Ra=`eds4n0{{|ajX&2PI6))p5qf<%fKwA z7}o>t0%GtmtfPL=o1TThbu^jFWMGjzF{Nd(96tIWYm=uby-#pc<|3?3r0z5=ojVqT z;~Nt&Z3BAV>+2y{Y%Mk;bkRN9zm1qLQrm2TG`8B)5WE2IHQGhq|NnR#0qb?b5=uaW4GJ|#>4NSU8Wx_C}VYwzujHVSa z6U)d^$Ryizf{YXg+|+3-MqBq8<%DtBUG+NOF+MEGwSelD87G%{LK5nyZ?-IOiP65IF zxLx+!YjNgPcE%S5YIeh{xybr9(BACsCfTE_0%l304jeKfA{J?2cm^8lsZvbt(uc`{ zYPtS3`;J9DfNDgiL~FL%7ul%J0b;pd_uGFWktOUgT!I711uA10{zC)c8DiNuf0m*b zo8)+%Q`{ySS?C^LaVA~6TrN54TDX`Q;RU>aFgA?5B`k7gU(?ON zVViwjW(2_nCCYkmfk|ldK6y&86-ipe0Az<_^Yv z&WpywkalP3M)mvDA8c+5p1kW$3E_I7!-K`$$N|Avm~=_Z%$`K5!&i2^nyhDBhZ*t? zqve+D3{8#d#7Ivqz5}4wB?vNng~k1ALL+_kigFQ|a|Q5WrmU=Ex83cE> z$nhkD>aAhJDGG0+009A>`%UpLzB~5&M;LC%^UY6Aa$^oiZ|N#;XJujV3t zv&|zSI~Zd|NpBKe6adjdPlvaeHe!)*Vdg_GHJ^s(O;T6WOtL+E#BnjtG%_D{m|WTn zTtYj9ENz}Jvk6J^S|q}o!+1VhhSCgFKF>>j?n?tXFOs`iq_BSfIW0ANGl6K9j&?>c za$Qzn@`dS1c`shF%UC&ITxTI?`DUA6C|oM=(%{0tUjc`en|1EMG}&Y!D13w2guOY;1nykK)By6B)B<5e z00fmULv5dfE{i?fKpbww3QiJ!J?(daFxT;|Xa;kxDU7ut14_ztq_hWI*cyA*xM-|d zflHt>A|(js8#SwG<$>3nufUQYnM`Y31FeX`yYwU^GZ}n$=<0jjXi4*lnom`JKJ3Su z1VFJ3)5uHG^MnQH`R_V&;7AdzA>2mEr({awwD)t$XO#=qcg}!En%)_w;Jm+jpGQVh z)9Bn~Up~=z)8wLVm3-xPQ532y;b~$m6b%cCm@>Lqe@2VY8e(0fn#f`r(a7cdo$vCs zhV||-n+XE{F&`L%n`wxoMd-8Mf7YljE6vQzkasa{LOV<7`F(`5R#&}@p;%?)H7Jh3 zY;lpQ&~u=eb#u@PDHD4N>kK)xVR^Jlfga|`g)LyPB~}!Mv^3ASw<)DrO=M=LaO~M@ z$)^Nsc;=Y_G};sE-WnR#E=O-D;$abV0C;K0QqmDKc339F6eU61zL|36BOvCgnP0f3 zZtA{k8*qWQa9LC{n8zSO>#1L3{hu<(_B*^wC?Q!FZUEz;&I%x|f4?_sE;(}6yo{5U zwS|qj34XDg^EVdJ8=_kS*7C3Bv&B@e_DgFWzFQli8f9o8rzFWMNAd?)ia12@4pX}! zJ1DqrxGICuxdD{+F=Rq9pOKbeh=d`ISo8vm=JYAbiLP;N|jt3Ei7Ixxu zf8;N2wdMgzf;yhC8d_M?v$z@l-nulPvX#;InS=am@E=reX0kIcJg>{#*JV^O=ab$? z?s`F^=9r1xV1$99tQbpmex8NsyMskz75EozxK?nvX@_L%g}OJ5e$1_yL<>`k?C)VP z&e$QGFBmq$(iGi~)`>))fm~B8sLGV}H70Ea=?z*W^uo|vbu9`-@Ol1k5v}tTtA$_8 zcBQv2HjjD5y3onUK2#WDoCjGBRhwB|YUt%?!7OMdzS6v>w185qmm;K+aSebOH>0qO zh+rbz61M5|21J~h&<})gCfxwRRmYyTE6e-kV4~YVvUJq0MGS*HxDBWUan=+rqu}*% zvk#}5KbJy?_~kf(NCRXcCYQ-G=Q}*A0UY-o-D+TI>3%)^D`1tU_+p_0ImQHzvw+Kc z>7jxE@uFjs$XMz>B*^qJdqLV0+VNbAPt6p=4&%^H5ptbkUOafD-1hpL_21 z`ggsi$;1tv9CqWo5&~;O=S@EnC}kScY`}S&)a2h`S>~EpuiNsUWD$%7gX`2-C`9y1 zY+G(F_OyxwBJ|>OJ_p@1UG4DB7eY~{Kx&Qg`T?Qoq-Z=BAszYT=MEd%!!Yn_zVGWs zUP3sJ7#c_)XFF0Z&<-k3JN1Av3m-D?ZgG&9o@;SBEg2WM8tsgPVuvp|CB=exw#*}pEBhmAjq#FL0Lz?EMq{yhF?^z2?4%lncMjz~* zpo&Ksz_4#rr>Kk6H`5PUV+OI41tQ%0u|;@#(VPSD>4OfU2m5M>LW}&+yK10OYWDCB zm;fv}U0FXf4K37t$*sOMAzD9|A2@;tVaH63fs8AQv(`eitRM@Wxzqqa=w{70G^DBA z6c$=+N6^4sQ%0xrA9GBwX2Epoj3y}EeiH2$87W0Jm`yE}*_u6o-Z2RF2*HGdjRv!& z&9*!`EK`@e76D06&Y(QDTbltnf+SW5XR-Gh=D8=JGuJCG5IEYXs|;u{yD*2u$7)kP zqq^--x(7&g^0)EuO%if;9CHtCn8X_tZ1TQ)}6tHvLzz+7rIhH2j!Q}_J3ad8il5@s(2nU$ zsA^=e_=Il&7kucb=&|K@E8ikrB1Tis1))J8G{(%dWV7tvY%`-T-X4W9y7V(HJ2E_m zUg&)$%6h%QI<-uB%gfS#*iY6hQpX6YWLj}<=Kq{F> zCZls5T2$CJ%f;351>CQ6LLfAX?(L35bx zF>cdBa|Rq|v9pZ|>+@=K-))&h+YCqk)O((Rem(!fJ41&G(n46w2O7H!!DEvHnGwJi z-iKI=e-U=5TZ~-SnOX8)Xoe3dJ$%k$&hAR-`^ws{*9?u%b4igamYlA{b*49SU6!~T zV`jW5+GU%*&$WEb_8XfWi@Zbu%#>9322cSH`W)JHv6(D|Lo*Rhse6optz*U_tDyjc z{h8TKGLGy}&SudInu{P1!GAtjeZvZ!$vi)kvy~OdjCQ=7cIedS?Kq$+Q@70j_(%#I zp%)wbR1UZc>2#f1b2u3a9SiJf(luys7_5QHPrH$IU?JGcDCl?Bz^D26b~?!UeP!ss z@+rUfJ}m**7hI;WY&j$43{0}1l!h;*aDOcKVRAF~*6pK+tqi1V!HO)T^TCw%(g%H? z16c=UbHZ32{ljX2@pVTxLMg;Kt|4xq?Nd=jfnkc0_GF zCxITR<(#s}JxJ#)A!v3T{BX4{~b3( zNLJ)0h=E(kauzAOqM6DJj%U7Tv8sp6Y0?ZKVg?ZlDk#rcnll(WqgtVT6PsjR3{B#Z zn-J`iT#DCG)=nh_z>Ct-mCuI_DBtl7)X49rMNU{xPH6bGh))fCR4I z{0=iU?_e^p@vuCV*P$N**EPoMlp!#{fmsOQwb~0zlbj)s@tZRGo6NaFlr?FuKlOj-80dJ^M zQT9HSXUGjp)1h?PuYwf21OCZAj#FrqDmwba)0T`Z}1o<`aa8onGb)m;z|F$#7=r!Z&8c?@^VeZvF>X&&K?q|IlBgK5rK zT0WaFK525{#J6UyxWvDdLN-^p%^gXeQ_9`oAS&n1^c}}aPVE7on!x(I7d->@$B~f1 z1X|Hj zD^j+%Caa3`(Gu)qsl_EJRU}OhdRW8fp-E|@*u%wss>Pg7f>20hlORg+ch~(3lSAP5 z*?iX29r(L!?N`_{yrB0nWS*f-r@`Dk&q>xZT0Zqybf64YE*zfBDm&LG7lC`c*1-1| zV`Qd`3<^~unUVR6%U{+K+rInX`z9)WIYMUlG*g~{O&&(rq9RiCx z9gMZsfoddl&tTkbz#I|Us`*1$@TrUZUfX!MwYjpFzxf~P^$Y|A;|Ol=4>VNNkaPhM zObWI+C5|`oe+wkpt8I=WU3l)SAGgq8ovkvt%pyNY_cLjB?0l<7Y!y1_!o;DOHG7)J2u62#^69N#YW z^hDCkruZ37TRS^YlcTJ{oK~iq+dS+1#E?;Cv$LqG(4SqRxB;${apx_y1orWYRHFG9 zHa$f5_nH4}h&&QuDv#y}-U?4?=8l`P7;|yLbSh)}GUF}0Fh;G~jY#JEW%?0OG(Sy` zdmnd*QsqCdMR9ZCZB6E{#)zxu7F$KvfKxLYRR0V_v!xH(vy*?9Jm9cyEjKAxv3UpO z@!wQf_nOsiTSBz~l=+e;2fk zW-ZJ&vl0aIqPkMqKt9vnEX<96VNPIT^9kGQYzVQW6^k^DF2Tx_;q(^oxerg$ZlVru z8ZlGrnA~PF?s_%fjHOw2MoX+vpZ7&8Y&{J)+JOoQ3)@}vft#!6zX`c2^S}t zr|$_FnYPZEjBP;MbD!MdXBKN!sDxE!!qlneBiA4W2zG{r>liX~D4G&$+%^CxH2ujs zAd`^8yBslxSMwm#IZbX8kFu`z0A9K7)PPiaU7`#+XrRb7tNC76FTMY232cYa^Y1aF zPnDZpr?8?YxjS|Bu=lmrcN_Gvd5<)U(HVe}Gw1B=v~mFHBb0MP3Y0Fup9X$14{>vFm6H;0BpMxY5Oja_m|IFq{EVBpZe_Nixhe?=FDfa<*aU5j3Sp}bw0y# z52=BO2qxvCwt7$jiDu_BaQDhjF%!vNg>AFEMoM21_Pj_*SI#SO%>7T91DX_aDot7C zV(#qC1tk=V<21uq>^qB%i-J5!KKn7i&w-hm^P52ZuQUQ?#Y=U)SrU9T2oxa^y}EZA zLyYlxfK89D=byoF!uD^@a89B;?8H~r@htONtXzT4II4O73id2;!ps6Wp9;WX&$XH~ zmwp#>UWo5W+XEf8?a)okUB_u|gK9!VDy5)Bh3DM$|H*l&e9n^v`k~@JDE;C{SV|5? zwvV2dJ-cCNSbX-&wYk6W4O5OT=-QOyjZrZDgoWf*fp zbj4NFq&)(#7$ByIWf;By65eQXr#(x_2f0}$73qUCv(Esl)N{hlTi1jtEJmB7xCRq7 zc7l5>$*i;KHoui%4#w7wTYUT6*2JD}l~c)tbsp*Ozx|mye6S!`n|0TCw>6A_K34|9 zgi%}1Dv8EUqHAQ0n;g}nGGy4CCbl1 zy4j=!cwvY#*I5spKvT2Tdr)Rsmt>%*mU6r01&8+md{yhPzsgDgb zQnAm`cB^!OtE{ljD!9DwGiq@R8?x4GdG*b4?%9xTr_6J1v7_#Jhn3`njB9LWAn7N> zy+GNi!kqO~i=FdTnP;R07jr$AeUMibf8}Z4I7OSy*XKnWNk^i+p~&QJVoQm4n|-c( zxOdcu!c+GORSV=AypO=w=UGbP2y@;ezZNP=&_7eo9KAN5bl1g=4Y8s#*6_<&#nPX$ zW*uF==8wi{6hw;lgbr6Aa-MVMY^XJbf}a^)5y6M*OqhlDgiCx{&fC6#wp11d!>rR) zxsn~qBTr3sh0Coj4Vu!eB4%_bB(b-I-7!0R=JA688QdYDR%u_ zqwSu%`Mq_9va(2G;NG7)+$!Jrl#prGblQ0f3LGwF{t|VM zPT3b_O`Wh0wB9R8ubyz&fZA9^lfD`B=9!}Y#(t9m75 z%F3|Vxp{vUz<)d~T-Zt1beEsI21LDFs|*_I=N60JJA$@q-Q;Z7;t;f@3RlA>uaN~Y zP=$)-{509+1wi5J8{M77;yCIEE{&q0NA28FQGprMxy?{5z)TgxyaSJng3Lm>!ly?K zCq`Yrfb}^crlFb47wRc^m&=?%Zb5K=@=gLJh1B&yn+^O*LCtd=Z-kD{)qER{$^t~5 zL9_lGF3wMAd6O20V$Epkkj+gi8ua1=i&P}f9WHuMCfgdab;$a}vW=td z?Nt^-cvx=E$C&(Uk$W@d9&!eUHva+-Vw71tl@nTzIw7m8OwzAsP#IfDY5W7kO6M-o zv=Okm*fgooIVsC}jWBHw#&dbs-}k>xb!QLQ&dOk=xT-_K)UeoYXA*P`JV4e*t$+pm zxj@$*g6#wL83A#Dj}OBEmwnbl8v74j{;CS{hnZ{^T%~@lF8DKxRxi^dQ~k4GPez-g z(0dd*TbT`H7ln*T2w5Buw-A~|WH<C2i{s|I$i9h%qCHgm&R*e zX#80V!C4#8WT#R`&1hcsf_;+$>HF<6uXO9*OR)YV@3cO4@B3(~-W{Ja_taU2lo$a)@Vk{F%+sOP*^d4C3$A;7&NIk=v{i;mJSWPr=Rf|+Bu zsLawlm4ek7V(*fb<#$DW=lLLclq-~(THFN3cbV3 zx(=rx2xeH?&>WE%$p0DyW}pMCAiXj$>`Y@V2>75Ua_0#KLvo>54e$nkpiaLVvO6VP zv!^&f%_E~W^p;lSSOdAX*587aEaJ2RbO6BnL1AZxoXwgDrPij=@}97;$cXEe^7bh; zLdF1art-uTEKe(WY7p{CyH^R{*BIA~%evP1zrBNB+2h7p5YVLY%8>ub;qgS>w*ZxG zGq=T3p2~b@8`Owpac43lkA3jLQfQ~rZ>Kgq?z0?dv;bU+SJKKq8(>O}oO}*Ts;(qV z&niJ!zdgubdU`#`56Ki78pU}FqWMP|J<|CHQcABd3}s#qRn-?_k@saetpP#etIhAq zRvgJ0ze@{3qb?Z(Q7GqXc4I|21up?Q`kMW-kIA!N2BVUs*-AZ1S}+OH6#?vv7q``hBudZc~i^ zG(X0b>EtRf?ZU96c4I{XGkL|G#&&c01_kw9$nCr5R&OkO}A+`oFB}CnP zU8&D5QAJ+EVDS*1&`?=fT0b4+9RHBQBAUFg(6&&+ZI~!zz`E)egP*$w!e(Edb(^X~ zTa+AV1_{w{*qo=yGtEiSC!s^)S|Lg{$jHp2D zCm3wh^SDX=+6(xSO+)@2LV=?c%KJQ?7uX+q5E*3r&g#l=>F*mm@6Hq~TrvlA&cpuGT}FnM-z4O` zs5E6c5VXq0!r`Q58_1<0%DIHAWyMnJXrYRboqV+Lg=YkKb*In5f=p6!FRE~W$=nK= z6mt!1zem2H|8lS{TE;WO^U55_A;}H|U=(?|)S=;0@oyHe6vtAI-rB|mtX7g!4e-SJ zd=raZtHUd?WTB%TsE4vhyhS6@+4)u8=9a!34;f}8>t6N}>Ty2eM8qjx0a1vxrGOVz znh#V5Oi=A*cNkKLY%2BtXZfGt$D8bLV|x0~-Wff-NVw*e5m__EJ zi#5KQ+SKGO8^nrPJ}fgg41j@P+2LF8tqif}Xvw_T$E#$ubTp@?zvB)nKRgn%nWeEy zRy6@3b^jZah|TnKHo`z5Zb2zJq@g>Zuwg~dp*2GD52ciTuID!Y(FKuVQvaI2Fu6%g z#v5}G#SyY-MCdiuLXG3j2D*5Sb_`T7B{Z3jv|!&eDQ9|qUPEd2a4o9$`7eoACwnq= zdP_IfM#qEl=w<{&Z){o^J&qL;4yf)elmuP1_q_cCc1aT5s@I&BYD2!m!ygMcilJhL z`G5jdyu}6IW?$0Ir6gQa)|fM?o6KKlAsn-yfKC%r0>PHTF=ml=shSSj#rk8+O@xlx z7d)53VJjNt5OAxW)~7z#ZZVrs@vhHXVU^sCw-d!?sEdqCC&w}(4h<~o?Ok8GG?Wf+oI!Rzz0 zB{pL*GkLoXFsUdOW^$7$-u*P&pBHIaT%$L(ZkPFa^9l5f zg;*%GryaGpforJn1hF8XJ1A*$e%Ii5SYB@AoW%!gj)qjXv=`?#kRM?tCj;4oL8;sl^)6vTXCugi=Do@2?BS)J4-127ZqzcmBuwKuS4qUK>2j%7xp<$Zay7C6y^^0P=JUx1Be zk7x;?f`!R~t(GP=JC&lcz1iL7B=c`l^ogC`8W!|%WLx8L1>Owgee zQE}en;@;Xo(Pt5*y*1kmwl)XZseAVFS|2q4Q6)w`SH<=%fcTqqdOc96nUYDpFJ?lw zKK%Sjr$07d5rwBMkjSy16hk@C*wgD-2nEV`5{7$Y4d(V^_A>DAitJi{q!%7`axxr^T z{10I#jA@3-9N0*lGVqi3eR1pyS;h{YZ*tb<iV^?FaE zP$t(ru+cdlzA%P(13hy8^!4+1ulX*aevJNuQF?8`&6zL8c{67i&a1Re)g62>zv%|$ z-wgdOZQbna-vX-NXA~zSu)DF%;*_XORJxk6txPr-&vmwOnI!S4r*}gcyv=k5yMPxV zX9_eg@2hfn?6PT;ZO7tLhiy!yVGB7g|C}j!csYPHR}PpN4|0?2ai*Ha@PNCa$Yr-p z_Ujj1yVkVKhYvg53l=fkmHNu~{g7kWb1Jtf2L!oT370)x+w=J~TBL1SzebmTwq!P= zkel~DXMzr&DV&?>T6n+968^?hsV+&lyi5SFAr&&9%IL>Wx~SO25oKGa$RR0KpU0P8 zQK$P+H$O0!m$d$}B{8&ymSCyDVDh!*B|8vZn8;}Z0`;L(ZMI4zPBTmV2Ks{*CeN@P zJyw1?G5vcE{j#s*S6DVI^8ta7E=8CRigE!DgQ1j}n6;o}N}}V?(SgBev^ZhqX=%?> zNT?=!Z(&rjF6`4S;VJn;nNAtJ<83Kx!{U-S$^wCThgzWfQ(9#B;clqoMKcvZH?;f7E$CuU1 z0d@mgDt_YMcJ4DN{baB>vzG&TK3P$=1yTx28K7M|SqG?*`uQAKUF4W3_Irel3%9e7 zeIsOqv+S=75U6SBP2$!7pR$D8e9KAnUJ5{+^Q0K3;6(<=kVQL!Hmyn!~2yP&?$FOfA+r7hAz?s zEDD!@unLoD8%527=$a!tra&tia8rwu_dDeH(XEtkv`B$j(oB7M1J7DYsg?|$l z9fCDLOwq9Mg-(oz4`nR8ekDcAEGcrVn5;BsrdIQ^77v?XxeDk3no|Zar5JH2pZN*V zE_3#j>~a10IdfN?^tX^Tb-{YvnA8GK51LP+8_79Vs*>l3+(zG@X%P%KSLtct7i$ z5T(KQ90Osv1SuokFkl!PF@pzH8I}h6F)83UkgTY4{hjGL&m0MEV`F;haVZtFF%=gw zX=Ev$_*i%O+??U&5Slj;(j48>P!!a5YJM#wO65sfh$(UW_Yl0ZO=Y;Dl2&_Zio7d~ z5oB%!a)@3emMe>x2PPJ?4wH`JXWC(M#$C3$Y4VMU$oHrL|16GG?k3X8(C?4-OWPh4 zXCmeE^mCZs)900HPf6FbWHgiMpO|43Tn#nTMgwG6MMji}NU169vGK?I7P`!Y_=z=u zml+D88pWGiaiy}I%ja)5g&HX2tZGJN4hxDuMduVRjw_Ulyfda{_8x= z*je#e#7JlT>kQG7W%m61Cs=2MRq)^UQ52dzh7n+Eu4!c#SAd7pG9x-C^)_&?b6c}Y zy?!wY+_iM~>%f3Wa06nsp=$|`?g)C}JjzHTQ^xu6`wqf%m`1_UW-EM${jU;$r>5P#+gO6N9|My9%MXCz5By zJx{?5u%ugw93U1kA~~@JxHHg&jp}R()|yaU`5B1p5i?EpgOLl$@a|pD7ikSkVm0X^ zu;}$vlg;fKVCiwoW^oQ8nyJV%2Z%+7sq+R83lV7aop12}pIQh%czrmYlk$(d<>+vp z=DnO^5VxTFnEXvps<4avJl9iP+%iS<6n0#bdVUQdqnMbGc2U+2mVtRmx#pHx5Ym*x z2#q_Rq(m$x<5S88kx`6Vonjf-M@vIabCQl5|2Jb-w%y2bBN06M|4(N4p;ei&pmgnr z8N0hBlEqvA#HN!jwc9TDHGiy#5F)!&_1T19{^Y*J4ckgO<-9<}e)eo>K+z0l4#~jN z*11iPm8v$2`rx6ODeMyPw>Ef074v6*YTqIbHN?C$X;C;T(iXAJ408&z6E%;0c47?V znv9*(4WaLS+&FwZ1>7AXU2>?XA)|2&>)vQzd(*qsUeF4d-px2a+b7^l<4ZJ5i=fFUxr=!4ibq&nAbP_y^O3`QmW zoKx@Xm;jdFYs`?Yi&Ce7q+182GpVWJU1^j|l%Q1BAl(%e)0D(RMV9907zODq|GT#~ z=eVt59sG*WPdel%p>!%q_goC(MP9zvZ~@_18l$*8fd!T!F9LChQrr$?okR9hRFb@SAxNXt=D3&Ih<)-V&}*6$Tn7U|Gbcu*|-N$0r62I;f)QKe8e8qVt{+|Ni`QVyG@U^iks$ zumX#7?V+eYUr%S>ioh*N9lP%D-FuuZ#N~Uog;G9k;5g1)FilPAYqoqgy<=eXbDP^B zkXLx#MwToZHhN8DatI)UnHVabK}_7Q#DdW31sm##Y}Yg{B5Mumd>?Lb7* zSUq@2H_O4sPj-ex1~NUO#_u9{5l0l8sEgmSUOuurY_dd$jHfauxUhz(H04`B3|mwN z23==t;=!kIn+d`2z7cDvua}Gp0qp{b;-1>@j0y0q;DY<7KNPb<70w0U)^_eiozsbg zl9m1GrZf-Z#)%X7NX zH~ADzv&hNh^qK}xh3H=>1CrM{i=2dCoASFg--M1LbxR#IVvXL!9!I#KbwY?7qPL03 z^{mBwh0`gGQ=S8t)KuO7<&C1Km&k0dwK*(^D&B@n=O?mAE~Vu-CQ)rPcmJY7Fl{Re zJRZrj!=~@e&^2TFMdkE~aGv!!6LAx95i-)4PTu^D@42YBEF85gwke`&eV!-6ujk2( z4iIpK#3o3))B8`LA1*$QF)zaq!otH+!;#yPPCv|IL8t$T6i~DWQE-}sZqW|v{|1^) zhtTwR(<6ULMt(uN@mznEbZ+FK6f4EUt=VVaM>7TO`b1ul&!;dHQ97oO17mKgt(YSDZn9I;ptTw2yCh>lzwvYt_bu`*d+PQwMARb&g z`9}vaO0fe>w`X>;WT9>v4xNpN8d_XbFa|i``JnV-tVTw(vmoy{a7qWfQbO55+N~WD zePFC_57O29**BO?D$`;wX)4kXVsgo+KSXgF88yhze{~MtY!sU0@8? zMVOCmzzSZ3U40aSUvdWqG9Av#adSL645#mzf*qPCvS|BGrc}XS;H>+CHxX&r5P)xQ0?pO89 zkEyoOXCRi@nBZ5lNCFzDmkw%A74GyiwZ_&7ESwdftsUG=s=X|YY@St(1jJ%7^ZbefBj?C#yyhtoO+DxqJ{13qdSF=)F&s z!Gd7Zg#A3N$%iveAQ%Q4+koVtnI;9PRn!I}l3t)^#ux>u?(zq1OyUl9cKsp4mDI=uwaIcqyzHj#8wtqm4Y7uiFz-^cLd zBq-VgPnajYi>HdGESTxe#&BBA8=zz%aOeDk2mc|bC-ReURoaF6+QT}@3}MO}n6WI* zQ=Cra$1;Lx#wJR5^?a6@#^h+Y0X_-7!s%Fevowp=NF^emlZbXhQ{BYpG!^Zo ze<@*T3d*@(zd<|DHN&OAP3;52?)iJ&wQPNTr`^@0usPpV(2|srrRW^=XZ(F6Vql;N z-kYmzj*-Q7(Q>%%yG+1kQvD`VmdB`S={80w0&`+I6CUJ|BIu1J3?ZVNbmp?Oy#Ap^ z>a-qQy~=a$l^KYY(0xlMIf_?|#ubLSEOk2Mfg-8j2g+HfT6B8P*k% zw|PVdACG7vn(sV)ul@eq$0A{bUWg8kt>I$oDm1%DH5||Z0r$ZuYt-%<1Z|L%28Nz^ zyDo__D3Rh8`C2-I>VZZX%pl=VmX$w&B38h(U(Gr|AOk5zf;MJ?`WFS(v@DCG=a!E( z+5ihxF9ti=m_EZxs+LT2d3>=DRz&fdK!gkf=(Rg))h-s*OQVK)lF(;&gmzA|w+rb> z{L$&qY!krk=R4#E(_QOcY|d<{s;JmSTBe0>_;5qWtda5-bpK>jxX|~Vmk4_5=r##m zn!WRX?a{u8-qT-O>^Jk=>OADo3`EhalTs<1Z}6Q?Vs{VliK+i8o}Nr_on?K-^OYtw z;rF6FO(m8@a1U~t>p8{s zSWt(j89TgdeJ!|-3*i`inm&P(`QpQVQMj+`iK2-jBLGl9ufMA$ib=+0@881}F9Q2c zVmt?>W|q0wDB>H%u~EicV?la)FS5(ztlFg-9vCW!%5BjhjXvk%s{`x!1)DYF660_N zFzR}g=Oi!XRfX?8h;i4IGws)TqzGvN@y0PEAJx3r1+Cc_Cod{4ZGoOH6=AX1!tT1S z5atp8S4avS#X(GJ@D*B2GXVfUPt0x1yvMyM#o`Fz!!5j6?Z2SoN4KH>mU}Tdt}|^{ z#Bk^w_WmBHT}?K!p?^>`0}(}Wruf?gj~3(^N4#;_>ysOD=X2FPS+_2hMdXu2Mn+LT^{oo?sI z0@%%yX)(Tm0$kIi)0|8OXEQV|Ow&GDt6vWTRy%$(A!kHB=qR|_e^OI?3vZ;*5>|;}4r`B~|M>?kT>4|x} zoMXQsHC(VF5fxk!HrN|VfaYYkCP|CWXAgKWQ=Mht$>!jKiIn1?#aN&9^&FUK6nzcf zTqr6Wu4lpooyI9vzK=(=X}d7D@4Iqm+_II_Tm%dKGIbY@Jr!!%8jdlyFk-p)VhWvA zve86U)}qfOm9~V@d-#Oel4dcUUv5%b-1V)SP%kva;6Z4Tr-)t925bR?Mg{E}$3@~w zopC5o2PQK?Um{+pR4A9DSBf)&=9zmF?CJSRmDb0rMe_GNA#R1E5g?dM)N7;u7DW-E z)EP~Hq<`DcETa?UObitEhH5_4Gofii5&I0%fZOOlx+Y}a)8f{q#ypffGfX9|GZ+An z`sjR}^d@8C&K^B)l=}KKYD&w~T}0Stsk$xZ7uWbg^^PvHAQKg*=&7ZljUnqZVE%FJ zp~FeATIKD$W`kFltpdZld6t=WUm95|VcLQXcHFz`O;JO*UrewGD^`8O4v+1g*j#BpTl#y8+lcx&B4aLwyuKk?zNoBJa(`Rc&S+DCvu9!^3LBAE{AJw@ae~w9BnVEDt z;Yw`4OCE2l$+lAeev2P0`k$AE`J~5J0xe$FTKk44Lby_<0x`taAlkp{=?4DD6&IiH6 zw!w<-PZ2ezEi|mBKwr#j#hBjR3dZt^%rKJ^{l0Nh?O5{2SU6@K? zZeivm)uBe1tk{PWSne9PY`$NGr7SMf`^-}bcQ~Rg@!<%nPPu?+ppEY80&@zgkhJUM zCqV{4MrD^T@RQBrq0$9?4&%d7UIBEXL;q$--+vVELtRp{01uqD5Kd_RQmkj>RT3Ir z)d0?>-RZgl58Z9qv2_jvRSwN$Dm20N5khnHaxc3f088U`8;kuc>JV*3^`kBbO0AhW z=`dQD3qR$6b9nzk09%S74R2Zox%|EauR#SRi+us^Ex`WtCE5mERls$QHp~r_N=!JTg zO_Q1!4LK=?pLs8K?azNt9dD!<2EYp!xUbN#T1ir=t@{;kHl1K?q6p(kCS;^&-eWO~4CqTKb^Ua

  • G;*D&@gzbw zF~pg$m$o6~f<|FxmZ6HRjVwmcL9^;`goS>S?7~NTK%{1|Q57-~8@N3g7WBw2S=l$D z<7l(Q?flsAd=Z)U36kU+KRXc9`=o10)Dz*3I=Zf%Qqkd{u6LYh3gFIQE2S(b*I^Ja zG-ht+3992c-gEDcM-GA`oq*o+*6O zO@K0#NW{boYm%&&S)qBK+(w|eD@1F-SxvIH6L z<|#*oSQ#G}FgySC``fx_c90%XJaC7=7dHy|Jb0NkZ*)5S{K#}u6NYvc%9K&T7No-k z$QMcnV``b+;Fg4}u3g_qKHiIYZjES6AM66-MY*ns?wPi+6i2<+7}_BO(id4f`7O!i z=KkoZ8+Qa^rBX^L(z=&k2&A>wFs}m$7sM-N2Uq8?6bpO{hr))gmUTtp&gqy(BziS} zqkV<8X|BT?Qnx?5ZD53Y5?$=3?&Q-a?KHgOd-~rQd-uk^Uwg+qx;(s)cWpnxjSrq_ z<_5eli&yCDd@ZdV6XVy{(AsZ@QR00ca5T?iN7=Vwimvgpc&>A}@5@7=!NgK8q>M`+Ao_kOXWH z%P57KN0B*YvotS@SEwSDf!28<`O7`1V)lu7lu)`ZCH;9FcRq78u&X)KH&%w&))a1L zD|!}9xz44DrH(fNy-g@*-wt0^2%+wS7~D)YNuzW}wZ`}C71#XI>6h6A+Q^V*h@|1o znRK8ix4HWr=?=|w=$;{5y%nfKf4HVdeXf5%%uc?ILXp86jZV+I=$=53LIH@K;VufI zVuSP6E+;xdY96Peu;)xmPQ5N6sW*ti{Nw^idbW0AO4qy=ag?4^z3U}eRHij*9ZAlj zshL;|+vGF^kBoWzrZF;&BP|ix;m}0R+u6vcfRshIAz54}@>$?oSW||jO&79U4qq-F z-b~24w(FvU0v8RSBQ~M+tmfFl%`*6jDw<`_H|-xQYS1*Z%RiMVsHXQ$K&r)w-ip^} z#|dARY0J$OO|1L_jO?{>OTr+Wd(#xfKG77AgYZSLT7(kp%&~7_YZ@_B*vHsp{=4-NQG}jo?)TDHl{kaSa z3T0BqvW@EbGJsmn6>Q$qMw^Q>#L%>_weXG*U>Tgak&Mjb12by$H3zFzlw!;y>FDJHCE;=XXixzXA>soWQ+R~_cKLM%zHo`eifZgaujxaFk=)LoRLiz8B! z!KV}DOSnkx3DpSi{AHLnu^!_*8q4^IpYI?xkGJ5&bD~~=SZH73BE*G*6L~2r9(A`7 zMswL_nzY)>tOptT#L@6rM1uG zj&!lY3o~$&HC^T`cFVycn1#IQx})vBbqo6E1wJ#&r}fN`q;<}^K9WjFqd{Uhd2HVU z#cO1dN#MIdkn3nse8xonBk)uPJbQz-{QPppEx=RL zu(4CwTpxD@(}yyPHzdDVDO~i26ZZ2qDEX}~%8k*4?R--btyuyi_6k z*c}>e#a|nPCGxn7kf&IF8Xc%DsoI|=y~-XDi?UO^d8u5jh9J#{*}gOi47?yklx{Jd zxHzcB%ea)>E^Z0kiyGe(F}r{?E@PyDVx5d~Z6>)MG8byJ!(sb*^Z7<4d17(SAbToq z#V+3p!Kq?Q>9DFrcpNT>9)VYpd`_P_uxKvhR(zAb&TMpYO_jwG*n=1P z$wdR8K~OqlLb_=PRgR0z@Mfp_Y)5)ZIq2dyhwpEL>Mo%99l?`Cjm9(PA8rVmTET|h zy4&rl2>5$xb|DT{Dqf3zb9y~ulZR-^9F4gSENZiGINa)Ru)cQ01~cK~L{HL6ow9ASeZMX_E=o&X&K0GAm}``sPv6+*F}W67Qm0b-S_EV4rVX%s7Nxjl zvGt}q+h}pjAVDUjFN#OLPwwy1qNfz6$Z~1uX86+RbrB8IOCbUR{okd$&pGSdn=on< zy4Y$!_xYGkvI>u1E3ISs%boMPAwG^1BJ8GEwGS3zVk>_HxzEANEcr{58bK}OTpxk7 zZ<|?kg1aKiQ`x%}S`qZ1h?*WM=u?n7#9hQ%&*+25O$WA7F=={Hyx_^Zilph9_bGp; zJ>K>J>^JmpYkZ7)$0@7rnZFQ{w^o^P)N`5e&q^ypSm^Yq=FM1Q`CHJ&LRipRb>hk= z*JBY*k5k|Nc3yw)O1WHdKHu0s$06x)p)j7=&1t|kjTFkuv+%D=0V-@I-**ZwGqg`@ z4C-O=i_&o6;ogGPlRgV2LWtrBZ$@PDTY@4wUB~!`7)$1)x`OB`L9v%a9006%K5Qxw>hWIaLMl%~oX zJvyhDvs{Q_*%@#*1AR&>!~@|+!JKrOq{CgOCn-FF{%XbBB9W=7PBd@lwV8ZuJwUaH zbW!Lho=X*|MoCN3SzKa5x3u#BjGUEd9=Ka#CMtM;T^m+Z6B)xZP|rs}Rw`y2FOAe! zF2z=>I8`;Zkyz1YJ%x#;e$9+G^U-^Pwxhh~`=8M-%Aky;oJM zM?<}t*@HGb+0hLeb>-wa1k$noCmNcYj;(`(#};WB2{OBVuj< zljU$uwPA_h5Ayv9M76cNtWw&NP@gOfE;_=s@!w4;<}XB1a!lOCbVnPp;6K?Q0<$!% zZ>S`%Aw`Zlf&$w{5?pEs>_NS=necz(HNLaipZGxiyI`{XBYb(ISMA0ZTYQDS&QAu} zAg%gh^8T^Td*`xYJT-GAh)T%K@Qr)SJ7qt7Hc#xd!XW*}7i+g$*ditYEZ=2n=-J|4 z0{G{kp(T!R47|yx6(sIHNppH=Dmj`25hxTY{LnfEV+i&z2Bg;^E(+$P)@g^*s$)s25yRCu!8#h;E=;6l7DS2GQ;jd zuMt@_nO5q7bM%R5bj4;b8<*lW74V6_qpQN9&b>_ps==Bb`kl-%aoR)yHId!GNK|Kr zU(Re@Nl{Ul^&Y%=GQ5~>wh$smYtqx&a9~CPgFxpjDPBUH5QweP&rZU&K6{$*A2F^s zgi{P9xFH6Ccs<4-|B3+`eeQ~pY1G5>On<%kZ;_os^f$*o?xRLSDdRIH`p2N>JdGGK zP_Xx(=N>G@^@8ekg7Cld_%k#b?A1F3B8hO8kp>R-u;{=R-LCVY1>W`Gd*QQ~yx=r~ z5IDg|8;%H6P9XsopFUlyGFmGqh5KKrn61?8T8ci{^mD7DSnoVO{~7h9KJ9I zOSA+$XjX#`XZbf;kQ7nV12U|R$;EWuQFC&7YLDLkKG@+4jv?m=Q8dn^hG9eBb?>i~ z+bxnn;vsZkAC2j1udnyB(vn}C7R~OLtU*UbUMQPop|6$E1B2XWE~WxRwnoqmP`+oDht57v1OY~4jq_w~J~$y`fP+lHC+jGXejogXtPRs*EWoq~(u zQlvI(VGthMjHWBk)dNvmlh5K`9wdN4cjNs#<0-;73KTc5fSmiRK4`nqBJFA5mMkYI zMM|c}2x`wnGsulnrs#%zrfW;s3{V)ojk1Zu>TW?4jOrzYs8JuV<&LjkSBh zKMpu3YSs;g5_hjPy?I(+J@L`a+EUI0DNgMkfQQ+AX55oq!qR5KQ-0FTCN)iRxl}7F z1z&PvQ=_Lt9WG7^DG*h==cRFT@nsB_w&u#-527()XgmU)c8*~+0rq(qjpmtY$W$`i z8S(PFoMEe)qfES4WD&kiqQL5c!)@q~ej;^3(g;zES9g$XW2pPTBpRgxJV&%6 ziMtaq2O)f+RL4E9`;usL5T%DAO<-o$+@{IKDvCThYFK&(8w?==@~~tj&D|^O01x3p zD$E%BLc^7@rf`VpgolRzbsg`tiy|Ux!!()aXTCv^ZRVe&bu{GSQpsbmTJMNOsdPh; z=Z|ik3)xF)oZ{NtnaYley3x&@;yhg#Du249DdpRaXTkf5HW?ItZt5S-kBe75VbUzW zEH%^@^|6)(jYn?1l*Ys_3VXz1>uHQSfx-eMO4wD!t6J6W2m~@|)R|F`)Zk6O?D#-IW11s>uqx3VvIUG|ux5zU5&7NZ;& z)sru(X>`MZiw>rK(}rrPu>#{|6mk5D`9yqWSSI@;dsYSRnxCK~oS@m5XV~I;2+MQ2 zjpZl}eW6QZp~au1dg>69#fwe)j3EPFs5A{m>O2Nx!3B^^wvgwMt*0uB68#ka7qM{H z%=`3dN70k1V!Ga>jOl$B^hqk}Vp)8C_I;!9p=vw9Y&ReMpU?J?n1_LfX{fp{5=hVyv4V4?t<5Q&V-*G34v{ z9+On1>+objPs%cbQ;{5&y9PF z3YoV;Md3W45urFwrlSet1=+ike6awQ1ICMlCuC1P$5TB17J&yf3}jD#*G0{EH5Kul z@R0rW_Jq^DY}A=pv;k-MKjjXG9_H|e5Kx103qLgx_ zvvFp(^0l8?1|2ftcWY@kRLSYP9x~TnIh$YHdu5vXi9`eJ;QETz=zG?zS>@7$2A@6e z!`Up_gTjf~5jL`Y%a|0eMlP6jg=V%1C7Uwg)LE{XJ4_FnLRPw<57EeBF^rhl@55d7 zpLa@{{1{6+w3b_P12^}wY4!@aP%ItcefFZxVSgs|4H^}%Wg#}}If-tEPneW1X+0OX z=JaB_4bmf`3Vk2lX_(Mci~_X7$S7BO=3o|yQj45CLwXl66(cnQp{O4iON`r6^c}&{ zL*40;(55iC0aCEf5<_xLY=ucko+&SP(@iOc-Ce14+{Swb{CjBr=u|k4QpA*AXYN1n zVImgs>j$0wYZr*p-!|x627@a0M@_gFq_Io};k2#uQQ`D}Lc35LS}PtR`Sx-|Rb{WzaPcgGns6aHX67?H+Zy)WZSM0r{VlUdc6bwiVR#t5>Jbf&Et(Bc zJ3IVwwaMc2NdPv3g^G&vZ2;FI=vSt;z-eY+XT*bM>PaR>8v2>n&_s@%&j&h((<<~_ z&+MX!>11afEGV#{e3_NR`EUuKGeeSvp7&@Xz!0WrbCwz}n4wD1TqmfVjI*c!f*i;! zwxi47%aXB!KX!t22}6xs!^AXxtHGL5Xg#TcmW*sk$x9*3S(<6k5|^I>)9(L+ns}l5 z3^42+r3uYb*R!&BKgN6yS0H=^_-NqGklMgGN8H4Mla3TPHhStxt zm~k)JP(Yk53H&yGiqJ9~V-vbc zmWl;O(e)}2q#$i}rP6q)T(?7K=|b*>Lx<+!R5-JD6M7o4grPw;FktyHh5$K9%qFXW zsme2h7e>FIpNNJ|p!``moH>pdPSYAR)564n8mRdDK0aUz*b&ZE<9U>s ztx$+xB(XiU2vsI;0^rbIC^zZ8Fvjs(4QS041Z$0r21B9 z6r&l2i7PXgIU zo(^MZAUX_tFMKGn-^pxVlCeQ->A!bcl0ja(MWX)r*Szx^b=d{ZZ(%~S6p8PF zfD>YKDP)7PmGgqJ6aWAq07*naRP?S-PBdephb<4ry^RgMNb1+rlMMS*aG|=zXC>P( zAmGDtXi7>;Y9ey7lEO9Qq)aXa=maAGZ=)}Yr&&C+!R+Bg$)!IA5)njaPIHh(GE+>G z`jYNXA%Z+mP)y``H6O{Tp2;-vv}4rf4?*4VqCtzO+bD;4JP8kyg3C9$ic&_EON=d- zF6a{w(~f6T`|I09%8UC)^{F*Wu7O{c71CANcFKmWDR%LMzsnCX#hNBZoHP zws+mZq!`=wG4L{&S!}3PdzeCY%wnj)J$otl#H0jw|2(gw4X|oYG@#d+dOYVJqxd}c{YHKDNwa9aB z%91>FM>Ay^nC_k02nv#2!&WU)(@e&ObuRigO|R8BF#Bs9A?uhWxG`Q7DFD{On#MRl z40q`WT*o+(bEMG~snd~x(2pV#+4VS=mkuex1w_vVO}N8 z25o5}Q6b(}4TFd2fV=PmgS(A(-Zj)hsbf06MLYrch2?q_G<jlnjZC_Qt+5lY!#}&PD!4jC4NI zI-em2E0ZBYpZ$Fo+XmC?S#=#Gbk*B(a1hC1DJpR z`6>6_u#R&ZU9oO8tF&1na>Sa~e%tQ^f&1J)Q!V|wf5LDYUsxTg6P}vCsZX4jX}bJ_ z&tzN;_ogWi+_PxDzMz)ZE?0&6yZ6^zY--kq5f%D_rO14y0CM=kFX{a?V#EDW_}&9I zC+sxq)eO3OYb-m}6GE>_^Z~mF&>WYVK1V5xxD=v*bE+AHU}eHkE?X_;7!7MDlQRB} zrIm21LtLon1Yy-Cp;+frDKkA?{JMXX+2;nGa)|<)^fZrvQEn-(_%6ieC@$kwEY}5H zl;5=_znhC~wl(cBQP)F!IhM}Ewc5x)9KojPY~=}dN~}I$e}R z*iQKlj6RDl)MrCLWBoYl!AFc?TuyPL-@{E+x7-FM)h#jrw!(ic^-vBLYVAnL$hhLcm^wZe z($b7Y{E;;2be>`@kxpLickT_vN_CK*aH`i=G)jTch!qUp(6F35t_@c*~3zDn^sJ zp?|DKXJUF-r6L-G=~+C@Yy)9LQ9N~YqdDt7*iMqKINazIbsU)xolE1Q@&2I~y$xJWPA$36HZCfwqV3XiLbGOKC8A5_RyDh zW#YJz!5&Hp$#|hr$Q=FmJ>v~>zb=C6m3l+eZRdN!g9;-eBf>o5W^lfsQWm93BrSH7V~2_)jj*rPG&XEeYW0t)odBtusK z-<#y#SWj5|mQ6l9pfPjJ3O!7k-PX66!{4~0+Ng#un!|A#e#S`HX()B{-McWW;j;(i zK~W<%fF-}`7W;&_Tft2NIt@iFi}ilEHR_LXU&*?(KUU1f)}e58PY30pZ3t?H7rx4$ z7U!6tgP9ei^72G1Prhuj>@+MerBRc6F-K9p6u8BZO;?iXW(zA);q(Oy&#AQ7O_a2t z5`3easkQ^+CmxB@OWJfEfu&zF=cJ7y7ndn}zMt1Pf)bbxA=Z7bDg&-Cc9*#mw3IoH zUn~QWEWb0cIS`Q9Ma2Fuu+S16ckm2~bZ>%W((I@X7iuGj*GVFZ4EU|r%tBF7I;TlB zO%wLa`O+-yD+4bRPfr(oOQ-FY4fD27yJ^MRF!GPjG5=Si$mxk|Igc&P(-QLV-iRjx z4=(SBJ^^1)#~!A<5u2}S}VCbhzTod*VTHqhtj7s5IK;4 zTdOk~GO3gp)u^d<5lgO)T5UUma!n$LWV3V#qBlD{dXr;`9VQmZ<8w>!g))U{f%ODB zulk#rut|zD4pH!f-Ms2xV9C8Rrh$ZxYCz#w=9`2rJ0CcW4&m_T z6t{2$6^*Zj^FFk+r}?)2ToDlE=pi_7B80Jrt(|b7l1E@dowm~t)Cg}Hn^2};B;bDv zSMqmpGSV}_gBd72ckyEzn&y#@A&tD1F!_@%SCZ5!4}_%!lRNzWs8i{GXA zUZ`UD#`~Q%7w0UEprQ|`$qJg_;)1*vy#wbBg=8HrO383+OIHEoqp+xsLqw>Rmq;vY zh?r8n*}2Y(WIe1_cM7G3*7;o1Rjhbb?tZa5pv8i?I~c zp?J$1^rtQp>54MP^<}0xmX(a6T+L_6gO(0Vp*_)3q>BaN^C0njpM^T(Jy-vROeix- z9>u@@Xq#BDzfCl0Lom13lcrV280tD@p=D{QNGTRiPOphRllTmV_ zUjr5|RLnm?Eawl6)XS8febG2|Gt!$^x>j5^(<4kWvL`l_axEN}KU*8ZYMHM8rdo5F{OWl#EW}uHmF#=khclQBJ%4Ndw$uI%qiaReY}W30 zLlgHqHR-GO444;HonzWF?r6$zjdJRe@6zoY=Xf!HQ7OD2hA~HG{BHxLMlc@~ zjZPsq?T!TRmRp_d%->7kr)dJqbV4qFnn>>^>sl^E=x1h`h+7a6BFIn$vGgwdY8?Xh zjA<4lJmsVOHKC6&zF?&R&?t;J2*+c!cKDUVGU*-{)AAPao!JnG1XZXCKj>wYjEK90 zxdnVL|8CGhdNNERD4Lmm9u#kS zf{r&Y6J(w>En>tdplqKU{n5OfE5S{mALbemcJ|~*!&7|jvCivx?L+6Rc4MR0;k7W- zDIydR3TUW8C>fucFSNr4iKKr?Hi$`&_e8Q!ZVH+f3Wj5Ctf|r}?N*+<(ZoUr4LJ%H z9hSSkWD_+utAykP?M1v?eG9o9lY=6>QslaZNLA40aLE^sFVz0})SHa0(xuL%?#&wV&qiQSC7m02FpWNFKRF?L2_HS^~BSO1V99%TSM#C~^8#ZplFdgDtY zGK*PsH@r$Sk7o{UPjsQ1w<$*F&}IBXG6t|r`ehGHI>*I+Lld>}r?Oawu;1z5ETQ3( zx=<{q-PiU$ipw!CO^zwTEQs`{5|DGE0S`7Za&(yW9w4a2-n5)!gFX*U;sV$gK( z)C?&AqEirf6Q!`>gWhQ%p`s|8V;8ePY&sKaa@j?q?H@@+2mNr;nsB$arlWhy+|Goo zOudc@xWBz`(HzwLd{izmoz~PSotq4uqAFZ@#t=2Gm;Tu>W*IEz5awdD@b0ZyaxU^m!^oh-f9vb}{I{0>dk^X58g{y!YC$ zp6TeKC`GyCK~O$7r8pLv8uWFB0N+ucZ6GP^K2-RWMJkt2u|G^Ff@-`nMCYAhfLDY| z_9T1gfq7|i*`G7-C%zT+k?gz`qg%=59Qp|jCwqD=IU;C|jOpD{w0%<8mrr5)dNxKw zQ0sIm$ah1PO|_M@9~+#)!&2LF<4r$=4GWaI$2A*oj+d%kowc#OfB#vU={sb*pZ-%t z>9_^4IE8ka3|=}H3c~@*ddsA3&TP7hmHLQRMDT^lRg99o`F*E-dcfd@SZ#ZVEAn3Z zEm=B67sYDO(nE+VQK;JE#h}Nkw9vj`Lt60HbN=^Pe!R+uO<5JSIE5&iqfOAQV<`bW zb1ylH_|_Ne7IXj4s+UkVB$!hQl$!GV_q5kDdX;=->VK_sspI9?kQd;bmc!#tSd9_H zn6w;*a3)b4&eBBSip|k~Zg3gh)~&eQi}MmStOmEJPPA$^u|GUBV0UqnLy4X>q>d9E z2-d%gHdP<9jnnX(^cAlu5khZ|T&GHt(MhW|=bh(r#B-R)Ie{1FdQdPLYWQV%atXA+ zumCL0hiC+l&)%V;L69kTXrzJ`W+FfJ+Eb?u#)-ql^9p(Rv5Y4$OZxVVBnRK0^$@+W zkhggmE_mo$@Sv*!)8>4!%{022Tmsv;5LaZ3@Tmp%BF_Rf>DrsjbC`ve`ESpD9m!&3 zVhG~8qb#@4!3z>F3);XXegSz-sC7-!c%`T&7u^gb7lDB;Snhy)WHCyiU0>o!?orah+VBBu9BZ|T%gpjBqo?qsB-=qci)LF zf|_g=Ou;`a&76wD>6E>v;m$kF>8F9>U>V%Z-)B;#txa3W`|xwI6yb|82IswDL(@&M zIw;0L!WQkneNX5BAge>%X%suYmz2 z#9OA%ccriT8va(%^?afq#1t`8%F9011V8RtmAv<(=3n@`dg)#(+4!cxWLFJK$lXF- zRq0jrJ2E&hyTgVEv26Af!t@i&6Jb-OA^4q=VrlMB$dDY0ns6RI>UvvX z5r7$bU`2&|{%Y=)rtz_Ylgol`M^E)H=Tpy?YG( zYRyEV1da0l5vs|cz{$_TV+NM8%&DP_0+?qFz}7iJi!<9W$#|8%hK!0Oh+$Fmw1BI# zHE7mJ@*=B@D}tRRLSH5Vxy{1as7xuAa5b^d8A3tDc`ftvY~GX5?hxjoBC{q+VsJy& zKGnfjPNan?y=#7>4nuxsFpA*s)m@&S`7YG3moAA6t!t}RiYmi*SHrfx9k<)%Zb9#K z=pEt6EGZ-C#u;VJo%cbenZ*JcupC#pY7cdwNq7@i5ysV!ERLno4BlW-UL&W_qrNrz zA-@-7atzq`nURp95{cWIcU62$F-)niAKEQ!bClhTxZFi?$di|<1; zE54DD)vABB&SBt6%+j8O z+Gd%?0v#%*2ix_;cDexthW=>#@HBc*$FtzDBQDq+E-)q#79f}bkT{>~qY2r|4dm3V z8y@wcUc1Sy4~k1naMZZq8X0^IAyB?Ha`M#aq*D$gyg`}A0$V3I!9{NLf{O*D2duBi z#FuM73yWr4dZ^T>(;t(4cni+ZA30EakvD{kxC!aG zu$j?!j%w?)8e|N7Y&s8HY1fH`Ft^66yfZtr{z=lAZLylh8)N-MBqgNi5Ch>6u~(} zN-c$^VskO4!SXpyLn-+j3@0`QazE;Bc|VW0Wjh*(T#S~{qBOBPoODMc8Y0i2$-8-6 z1di&xJm}vU0}zhxlfXVWujyeEMl!b7HZuXJtCH$`wdo35jod>rken^t8aJu@7WM7u zrf4(dE;)Kjc|@+wh3}Kap8F%$+J}onn-1r!iIX$(9Kx=tpn5qK{c?IR8x(H@f6x#R z-iF-=AsfQ@Zsf9Q5}$?iV;3U=Sx6zRp(x&=Gk%AQ%)6Llq>g&RH0*F8!bPYo_8jhX z^!xtH&olLh7G2^Zv+$PJ25Q^}c+iv{g~k1g4KlI@^hAoKiXk8g&>6~uWh!8zQ3NF{ z1GFd>!b_-`(MmMoblkET6~G2TqNK1tQ`0~~IBf|nlqSuhAl#KpeW{V+NQ2Q4{_s*9 zL#CdKEumF&!jDD(7j9Y3Wcj%k!JH(Psgk${<(-bOU8MNkBFDSw#5GH!rfJJI!B!m@ zy>MQ78Z(W6?1+aR#IberMW~D-j)FsdZ~GFVG83ZpqtZj6EiKL)AYDd-)L;t2tr0Q$n>EM zlQF|!(SV*6qBX&xqIc~>qdaf)w#CVAdzP_|@2@~>JC;Kf!a^`jc*gBQVcG{`Tl5T` z?v$l!uHmtwYZEiYgm($jnKD3%O(-r`>No_WU9u(nSb zjTL^Uqp|N?jIG6rBw|B@qNa2@QyO?_V3BLREd$Lj+){Rv6RY}@D28QHb=M%B<1AsN z54wT6@h14sbVMQ2hMg_NwqlHQVZLSq_#_l`qkxp`9*U9Br8d8kLRa$jBOSN);P}|j zMn33$*a2oy$jXpH&(DuJ;mKOpIs;s0HxV$#L=eU%voI7+X%XT+^SdZMqV2N%$=ol2 zSXdK|Wx!9PGNpV_MdX%&qpo(KQ5gq|=r#h%@9*#e*UzpnVpe)}1o__R=>zUh{$SyM8@xs?T+^}Q$bLo-Cxl8XiiYYM zaXBa*FP6McgjnvmI411M-hW8G{C`Ap?jah^0!-CUrZhX!}K6Rf^P>%7U z*T!4evM9!4Xv+?fh#4MjA|)B|4ZW|Fby!#orezNA{XILJoiFht@uWw$|>lyHVW z6BeG5GznPf&oJS-(_oOS0|ZLX)K;|fSP%WwA<>xzZDWfbVyM0$2QUG1-$e4*=WIO` z)e^k+6`kIRH^F^q?q+u-JbDn-eoc}s*d`6^q4m9U?s-(PSxA95i|}^)M1+aV%iz#- zvNVTJTBjnKC^M5_(S<=DG8^%t%obqi{n-nM^CgEH@_9ocIA$HZxO-BXB8&^>C-%|v zt)AzN#fm<%ShUeCQe2>uKW^WZu7kgd`V3z>5A!SCcBftzIuV6G@t*n`gJYWnI;%Ko z2KW7LQ5&16+y%1=|BB9{gkt?(wIKE;l9imAMgQ(OAZP|=We)1^!Um1ZHr-Y3slOj} zak<0NKs*2dAOJ~3K~&uX<6FqBy77E@K+2)ew{he6WsMQ{q zr>cnHHFrp{y4!YBr*@ItON0wlI}T3;7s4A9(2>B47Dh__*ubs{AyA6U9?Zqy(Pr`P zjxM34%YjUUl^HOZE<k>Zc;Ij8l#s782+&CS?gOsJ%lm-FGP4{4> zLJTKDS10a-L*0NIjkx}fCN~2%hbC6Co;F^T#;)NhRa*o;F?dk_(j#0;8mbMZFA?v-XXoV>f%zG;}$~DbBVA6mn-4 z>AdJCA<5579a%+9m=G~W3&O>JvT`&LX^Rn+mc?cNHdbO~i($gWDB{SGi!n45D(b>} zWPG&_nVL$p;K~0js*gu3zBMc1k8IN0ebZ#?5tQpU)HJLl9{2&X|9f@P|L1#0y ziz4|s2p`G_DQ->qOkxis*L~!vz+G<4I6;2x2cBK=4_Qz*J6y{@%!3ADRcv_&7$aEN{`Zwa$ zSmku4(JBn+Zp6<;_>BzP7kx?;X!+jL-*{%0%;N2x2Ba1Z}UmO4u{L3O(U#W|L z-52~FWa2tGoQ;kIxv1>=-y)c@F!Tc32xa3VYN9))kz|iPmofFPVV;Z8oA$>vH6~6zlf3K%db1~Oo46bhg+MQIpy{~B^ z;50Vh*LQfr-|uowGoap=WAw8eoSD#jvsUd4^8Q2Q10`Y{Yy+dK zVW*oJ9nlR9)UMz6v3F#q(b}$D1}Y)6HHL11lDXs&Siir&q2#828&ximN!Kx|F(1P@ zisNpjm!6w}zcOreMRVN9(4>6wB)fRlh0qdY5j2r}?_?vmeEd1@Y*&KI~ zM>MABGjjbsL0c_764ct4g+AEXB=n?-X>GC-Oy`q6_*L92daPtS1P#chinA14VGDa3 zPk2trq~-_J2YA zYpcVk>pFkGFTewYF-@_HXXOFBTW{@q@hMdQq5W>C1B4L{d{5W=l3MIgz1KU!3sj3P zi2NaDpr0WUYF{t3vGJGyN5XKj$+Y;l;jd}Ru3MaGVP7=D7`FDn2GW5Z7Mq}_bhlRZ zz+^vP*?DTS3~{1Nx~{h3plTxdrnF@y*^j}!qrpU2CjKx_dY)VKc7Y1xaTX!X%wk{m zj!3*`YQ_P_Sfn4RKFKrIXij}iZuDPI* z8&bkLZ$l@syoYWqC`G4ckaU(p4lSD7jz;VLGKwfntoc%&-y4nQidrrM3?wyl6Sh-~ zCE_#NL<^JUA6^iQ-VaTirF#g~nL(za%T!W3xZhg>w9s2r!6 z3?OBexE5l`qzpgA+m0@5cdUw*Ar74| zqXr8Mu9oQeT@n2E*L^;2Tw{4Y84y3;5Y_kYufg7;&$T{T=$J^uo}t|7O{crra`TeA zG8QnX<_?#onYPqo8~-p4p7$numWf{K-i^Rvcj)3mAD31MdGB0MP_ zIVq{Mbq2f~e8Y`ig2l>mR3IpnI`yT`pPdXZ+KWt|^xVtyL%*y)aR(er&Y=ZIUaMS| zm~l3$T3B{w>8~h+2a?T-sNIlARx6fsB((#5Yras;*?Y5~8YXgcR^r|S#Veqi6fU_j zSarf!P(5vy%vN-L?dG^@GPqlQcha)Yx4({ViZ}T!GQVi$dzfyNFcE89$|r-Lbntum4^ zYxh9^GJtkfE3$ZVspWv)$w-&Og{i=x`dFpuen_Q~HM{tvN&5T!Oc-|5T78D2THK?W zJ>YHF;!O=Aie2Lr>gV{!>uKY!v@Z|iF>Tw%ok2iN@AA)~ zVe~Od+muk=q~Vo*8L-?TEHo>aYs)=PI31KrZCs3+D77uvL7($0LN7;#q{(n8j1VDT z6WwbaQeisro3h*?DZy=I|9lpUh~&~PJjbR{o*apqfLWUx4gv*l$-jGwc-Whr-i&Mq zCd@{ij4yzmbZ$Z_S=`#jCbHoUR5E!n=L(^InMXW!5qt>roVLP}_nub{M5CxJ+_}vb zR-6f|ZtuQMZxy{n1^Pt6su1QhESe4~rfm>NJwpL?>EGm>Is-!azk8t8;^E9^#8EK`N4k6{I}1X0w{H?xZ_;t`QyjuB+z$wEh0#lZ_^!fJB1b`NX8*$6q8 zf6K+TePZZycUB@bU(=Q_3|E5f6tuH9Gn82=r#l)-!_K@B+Sbs-T6R63xyUV8DP9mo zE95XQLy?&l{l5JSj1Ko1x~O<1OXkXKkH+&r%D*#^XYBd*jqK>e%pVtvDBaq(5 z)hhQRJcPBHee}4&8sx}hDkFl@yWRT05J3bH9LfaIrmcr*Sq!JsA&1jGcB~!GRks^^ zZ$WiZLNCj(zNTm4VL|qPG62oZm$4>ADxj*tYvv}A4JVyq zaJ4Fa3A3P(0t!Kpu|vtkki>A{JULmeIuDjX`5oNwn&w3V}4?iqETrW;Jm;s>1bD zaU$weGxPrQ9CpgU%nQ253J%`U>u!Ydz3_EE{iPS=qW^0Or@MmjmHDycGL<+z#n-Cq zYFAGP6JT$gyCI!*I=-02q%H?<-*X2B=)fSPOghR|_b2GzPI6dKA1;r&+59 zwiYIyAWRiYGbj@pwu2^|^o6ZsLiR-`3BzvP=>bZV%mHJW7f7{GITFF+QV;MeC&@P& zZKWEfm{)w!NoyX~{Zj!@vxY6Xu)ofNQQOvX0peSzp+o3UVm8?!p{A&&a5|m;oDCne z(Kl*WxYn8T5BuU%7@IWo>$-!&4^v6|z3XVqKD&z97;-5UjI&r5+CU65We&m+cehhM zE97mXZT3w6_7dW9Ws}r^+moP$kmSdZg(14!6dly>Rwi;hdmw2M=%Ubcfhn{cpWyL3 zv-=Uf+f61i7#DF$xB$A_8J)7@Q4uWSWhRG@SZOb$X;{JCsWbpItrY|G# z4lH;JsyM{bd`u4?UPP*8A~CMV^Y!UA`sx+26eCow8?JT*F&fARE;F%BsFP4#Tws7VPWF4aTv)$f%_q5~qbaj= zTyHBP*MSmSm5D?$!exWnKqwVX8Ws(p2`Pk$`pq|8bvcd5-1XBF6Zq)bFAyvP;%N%t zb!mVVqKbM@Qe&?Z4#WXX-^LAPq%?-KBEg?E;i4=s`I>B-V=l)K-DOB$ljX-ADAzMY zkRVX)&&Qd1>-3FTgpoCOIzgsJw@Z2p0-#znP;=MK4JDf%9xNW?A3ej;_~OmLK?4k7 znms`ZXC*p$Qzew85K*qw%JW?Z%6Pu7EpQWM_4vHcPll_ZXSqsgd-ezD2cMff!O)i% ztf{7%>Hwwp#MK>na2Yr1pq+WBsLQr@{h8&=S&)9jpPReiS_HE4 zXEuz{>7JY3sg*|f!wqz!pYH1d)?BYtx>rP&^&a?o1`AymNyD5MI;%IfTpK#}v#jUU zzcZ<7$TT!;3hE@b?bi6%oXF7{KFp^4+|Del=1S!3(FXCmegF+Fae5F6eqtB515aMw z^dwQtxc7S``f7IvrG1GAS|^Qw|C#zBYgi2LtnPkh74rSQ<>Mut(_Tbqb&qJZk)(ym zWO(CPs_WU@k=SmIWDw|13VH?-;fy)q3`&>Aq;#EP1?7?ey}XFuRMzAR!!8_X28E=D zQibzc#2XKd_BC>clhIey{>IIlUnG(s%%sNfTkUiD++OG2;qYON8ifzK4t1;h$wOE4bA;n}d4bK`s|xpjEf3 zM?BBPiZH_g#%&7g`Kg8lCoSmM+uhbAzFY(hQf}jK25e>?kg_o(E_YFdK~LjI;d?EI#a0 z85XdnC8v;Mf=4^Qn9fRxt^8=IJLreka0e)Ex@wDe~wgw0R;22tz?JxE0=W5dq^@D>w^Eu*#MRAjK&o zq>91x!~(9Wi^XpN%yDZnYGUv@!T35|>EX9frV_;ao~=O?gKGJMDO=Ei|bJZBDe+#Ir4L^Ss6Y+bx5Zfa~=@$!qZ74 zmQ;CecD+jN!wLmb5QD5S$Mhf=WAgDP$Pbt4_wVI^#RbgBAnEp^rU($hu-F?Wy0uLn zC!>k*jW<7`JE5o4i&o0Ek&5ocu;BSWpP<0K`8|G8RA@{h7UE>V|72aIX!YBn%L&B#9U=DHuNl{vI*hZ?E3CvECMYv0JU#qri zo{gPzY2RZ%e-ZJ`7Zl@0OUf)!>sW>{jDZLfz%guZKoltp8Ysz`mJ^jEl=oelk zdJ;NQg@y05P{42Q2i$9+b&8m9_joZpRaeQPtI^a_2+ww*ipxR_Fm1XLP%uY1+cZ;e z1lo}kMd|u3ohcg?aUY^OG^N!hP!rR`Y{@ADWzhqbz?#XwMtjJMf`0KGfv=)4c859i zcwDZYZg$B1YQDGVRIrI1)#g(=pNQia z7=-?5zVhA36fpM27;P*hX{DiJ6`a6Q*oSyGFJC(Ym?^Izs!wESqsZluqr$0{!6=*_ zbbk#R#pHM+lr9$z6%}Hsv>ZRHFq;_nrbyqZI-**k4rPz)x* znz7I^iiU~soFqLDl+Fq@uW&kMYw2!7_Mnv7kaIOZfx9G5hx2LWOOEGQWW!caLloZ+ zrB&?85gcjNJhd&(LN5u#o+>ufys3qcpOaDqA_sW#b(_0P+XExnsfvDM(n=Ij1W)PT z^E09wNEN*6Fs2#QF#s&MkF~9ivG%DSJ_Y^*nJ?m}8vr2u0635%l5r zjyum(n^r|ibBaDJ*T$m8Vr*Ex(%?*Q$Q@DJDB5gvJy|(e+!l@TLHaXG1EE0f5PPK% zngjs+py-Uh7pwiEm)Ucx;IR0DFskFL8aU{Ec&eP)@ieDnkd&&4+y^ zaiJU<3Yyx;p@n8W@V}m6pND!X!CxmIAt)0pr?_Y~GrY};;BU}9Lr*lWf2zqAvOsGm z4z+27T?&y;f(>Czi$+HP`}g`NB%2S{!o=W7&)?~ZI>ZnC4pSnTeap_#VmGLl)XAox z5BmHG*HL&w*^e*Zt66R-nZ-UAvlvvc`S4o7NB|!~j zn6Knuaq|(;;gd6mboR$?QkdL;=PvwKSa;z8Y9MoRK)|3^&0@0`MIH8o2H2GMIe}z3 zvheM9htTTz;vvNa!qw?1s;q#GOWP<65;cz~KpYzP6#{7pa)R1{S^j*D3u|JGWeki+6QHW6lnHonKpFiQpcoGhFIHIoasZ5=zh>bdPdG6g&Zp2buArmfc6 zQV}xBcF#=;I?a?N8v9Cf{rv1R{rLtB4Oj3zSG&>HcvC{vYw<^+>is8=4*mcDAOJ~3 zK~$sD?erZiMdC(L@X7j{gHBdF)0ojl3|;iS>6W&g+I^eGAWvGDQzyV&Z3XUncu)ld;wRNFBDi(Li47(u zhi`0zXg=sF=OVK7;Dw`2KYNb0PDYSaelf$Y4o(*^$0M>5o%Qmkxapl@SPR#YJxMQ` zKk6kD?6y0+_?`j9vK)-lY{o6q6w;!CK31)=Et@DK#x@6w+#K4?MmUxUY?< z898tOgUT8j#Ah(GKIHWLgE4H;aTFC-$57_=^E+R$~Fg)XQr zaVE01x<0h`#(hYuL4dV?iE5rjH0?DWX?Lo!PAJIMF+c)iKlUDF@r>oX1AddxC-nL)dmulmf$w%hl|Neak z#;klfwEy@T8np%T!1EFChj%zleBU8U#b=(Te%K^+89FQFDyd$eQ*T^|-~2%2^Z{e8 z-w;Q$sV4Fk5S^lo<)Vd-_@$|eJdQ{by#ahNQB8^jGHF#ZWl&V)YSwM4gNXE!OUL=* zaMVQ(cur4k)2E!p@yv1S73w=;^RGDjh;=GelpK1mu_ym1`JPV{iYbue6!p-hn2MD_6d&txDQq(Ml>Ec?hQq~BrPEAf@4AuKO5q1nHNyRNB-h?y#RYV<_`5Xe4b{0%JH~Sr zt~OI0i6ZDUg!PzZNb-c~T0^FV)^~B^Q)(xR28JlgZsAEIQEW}4*>lZwXl-(r5Pz;Z z%PK%=h7Y$a@WG0nc|cIdjDPP&Eq8CVyP+dHm{en5)(B?{*}RTtaUNs99=h9dYpHdKMSISNuml!KhvZ}QUU?oi~~ikC~D48w@o1rlcO z88~;|Imy!dC1R@#5v8sIa+d!7?9ZEp;(7_L z6Vw)S>*3Tukf&nLO8Q#Tjw9pLlPFp5_1#0fr@{owDVu2{8q;D|$Ea8;*^`+j>x}Ws zgLbl9^Ef?h$Xz32Fjn6`SxejS!xf>*%)(m~+b#b`T7uNi) z)@@1vr0O`TxD5|&w3T8{{T4rX<5GTonwJ4vrEBOk(^+&#t|j9tvb91oip0BtAnqU= z?Pd4x1{XvrVf%85@~JBdOw(M1m{zT5=4J-rl#a7zp+(LFEsQhqXWp9%%x-#%JBi64 zFb3UnN|)5GE$4kdA*ZGnKn032J`2kEjGcWEfuLG2Q-dLm^N*rodUz!w*b_E1uqMKM z-eb7J`OP00GrSmeg^9+;0~&Csa;Xujq~0|MZ_4+6YXrA@tI$J9vsO{*iauIuX4rGj zbWRD_VZY`Ohq^x}%?4}$A;?*q(izqDA4L;x{^6Vx3s7rr$L3W4H<@_WBw!#WgC0lK zAVH?nxX<7Ws!Y#}o{~e0S<&=y$yAbkM7+?BsHOnXP@8>rG#5IB2h8@|DXzC^RTm_s zJX$EPqI8ocHup2%kj-n3aSxcUEnLr^qD{B)IFSkxq9`>$a|$zRcoo2KZIT9u>R^vN z>po#Qq%WZcr^Z4=Ki;g{*aK_Y-U_W+4AEg>K>aZO?Ffa!;uX`0q zR2pnme(FeOZy%22nQ0KBaS)q#D)Ymsou+fq^)8TK8cnEW_fH}SyXUeela?!YSTi?1%s}mx1>`=jQ$mlW6ff5Lsl)u z_t53IpkGKzs=;;G_soXG6-tY@&@YCi_C{q}(sxBK6q%Qv`o3s4AVUZ} z3y44)hjrn4LCtg$of3>1oiYSyGj7TVr-9_o#p{?UmbG%BMYMR$G2j|R#V5;#up z;a6z`+oztMUkR7VFnsN19#=ad(U~+T6>=+Ud9%x zBoN*2d+)jDp67Y?-fJSd+g&ufNeNdvC{nk#6^%k|q&Qk38iXoGQ^<<^u1vemeH}rW zr%}?Dli5U>gNgV_ZytB8k67zdI>2j0QHCEVt8LAEM~69Z0e^Fo6tjzyS^1`hrZ8`$J8(sE>VI7Y4*7OHFTh z%Ko%F<#TM5GRKrrf^UC+G9pv8_wGrS8Fa(eU7!3LZnU_`Qf-#T=QUBRR_g$js6zzA zo3T2i_)Jr~PMcvANj-1D=iHpLV;VB%16QTh@e$9dZT!{kTiqg>hjbv6I}TM?Ku%4i z($7qLFsw^7^-Q+aPW#H=GdjXR|4f@=dgb9OZH9ynn*Q=>%s5oYaRWzd)118|m8!-> z;6SV44I_$;QpK&P#C+dMg!QIxz|@GX!Wlyo=_384Sm|`{-1_oPJ)=_sF^J2Dp^XXn zD+^qXsFopei0@78Ez_+{iwDHvZRyn0@GF~BePST|Y0(Ih+I;ln<~5;Y*v3~-xsv`F zwB?Zlssed97?m_@R70~(M}ZzirnDJr7NO|AyqXDp5yls}Uss)>n%U%Fi-Jb`z2@Lb9hO9JZo9wEKxp-!Sz5yfZf5zfj%sW8(&(W^Q|-Fo^~HF&vAU62 z=%^TgTwy3hP8VV9vP%rd9rZ`vp=k;)lLt6RlwLNr&7xrt#jEg?11+Vp`kQH3GuknP zx)h^v3r%@m z^6%8)V!}%hO#rhALG@s^FrvhW|8&~kc3mgoSYyykqKOtNBIkq*a zyB3BA-|1&wXr6jaBxuW>}f#6dGRqhJ$ih%rB5E=*IXTnEVGyyg&VeQZn~K`-qI zLxc6S0Z5FZ%o|j+Cp(7Dh^hX7L1Os!I+pw>^fFSgG0;4Biips);OO_#3dm3d?kWSY z;czs%cq&Tskni->95Dj9;;|Qprxx&qG0VY<7&4rl)KH;2d_jv)ZQ^hsYOb_tkDr|; z@;d#*Brv1rm$acBZO_6;rRL$Yj|rXRxm(P~5GV`Nq)%F*Z3UUEnGx0Xrl<`&LLiJs z?m<4h;6Rd4DvnZU7=PC%%rS_BTj7r1xoj|;YK*X4r%{G8?+EgeX#ye{Pt$o?VR4V1 zTZU#Tv{3qq+UrI@VcRQ6$7!Her7DN(cQ?zJr8KEQP}HZvD9zW=tw7r zXWdO}Xg+=-I2Q~mn1A=_Kh*Nnn6;Fii}ap7pT#_H!R*e%cnXbKu_3t#1ahkbuc`)^ zNK#VUcF*W!+H>oE>lqpkS%eh0B#f~P-KgLKA}psoGe=UdRn%Lq2bd1`)H>{G#{!q{ zoX1*R%-vxleUT%4O$C+iO65UGzN4fNERhm=cRT4FOuV`k2Eq+Zc1MT=m2|ONC=Six z+^8v(cQ%d?QiR@TDhCB_MaWkOz|-ok=_FE|U5yf_8!Yo@6%`a{au=bA0B%AMoY36S zg_cYnG?y$K*eGUM;2$G}by&jJuxk8{s;iZ9`q!o7fJR3b16GC;I+}Lct*NIb6@eyJ zv&%qDQv6O!i?-Mvic>>3C+$)7VlUR}5IAz7tYL5&I--UhubU%GNRdMXJ!m?VYBUz` zRfz$etbqcDsc|0*+eqZaaf8Z7G{E_+`%W;gc`uL=Pif7;rJU%9gBH&6al$O5a0OC~M2z6aac)aW|a z!MhD{Lr9eWolC+dALmAn8ujpzy@SBkoPxla51L;D2LlVsn5ZF1b+Y;7kb%^vIYz}9 z5g5&Z)jjH{wTfbtaS$iOKUz*JQ*<_-(BrK)YDfB^NB9~vvgeWSS;t{DXW;~xTT3FX zr)H{whP)VVig{3kf!NwDUS~37%Z7-d%Q3ZcrhZ~*4%41{YF8#5)A$V9#yVMIoE;%T z>gT#ypy@;l_TY}j-Poa1r9XmJ|3DUW!>Jtt&ryOREHKOV`9C2 zesc+e6_^-EqP>v|EIl^A)CL{x2Ff*>!g?F6Sq9@BDI6$~Ro&uvM1g7R{Z#HSx7afJ ziO$o+(;Tv3s;>S%Hgr9pMi*vOS>qNO1BP^nL?`-)e4APhsgUT0P8&N$$>&Akj*Al| z+cUcS=|f|v41*rChey!083;ayTsh4v$pU{hFXkGTCN8@UGKTbxM^Ns9#eL#0-By)` zj5bTXQ>izwAQ#Qw7&c{V)}{>CmKH8o17wK?2QmU?mK|F9pQG_6;r?ina*mD+G%CoD z5N~ck-f?<|ECsb%Dps}7Bm+!XZHmq+qd6(^kOMo5FyRtpLTqL+P`Wrx7D40p4#S{c zpu*{=W(`U|Xo$hNDH2g@pu*V^u{C34*Tz7u?T{uW<*sDNEb%Pj^Q(hNUpp&}+da1T zxSZYj$x%n^a;Qih4@d%s6`%1%XK6|qpWpjtceb}rx(5k zr$Mqt(1>1h^VsMr+r4KWM0Fb4YGIAU_$4jj3=Q5RPv8a&Fv*8mV5>^K%|g zc=V~$hzR5Gd7PTXb2KPy!=E~&avn8&!@vp0C|zorSVzV&?9_40$fwb{A3f`h z1Im+%jb@@Q9Q2ST<;yN1D)Z*|FJTyR`a#;F97qYM9H#nYf4q0t*QfGr zv!AR)V^DxZcHp!Ow_nspl>$_zrD~**A5h*@6n9;Jb4_lgal)Sb9Z2J*tJwvaRTTo) zQc9iXS7OMZIg-}z>v%bs9P?QBnS&lS;L$3Fai7;;#yVdAoLO}7`&n@1p<*={7#=qEfv<#q?)??}nK5 zi7s0>C!;{m!t?T7G2DCPAtnbyiOf?9qZV@!(q=KwGix#{24Q80Zc;-zpq)gbPu&Bt z5l`5Q*$Fe63|VSuwmB5u!}zf2o|49CHl%}Heil&apf=y9_=HIB1%-Q&<4c2qrSEtuMwoiQ-0&yJ963`ikHl2;7_9xY#Z=x7$J^AY-| zT>Hlw9FF4;)i3dIz0J+?+;C6pm!hNgVQ0924a1BwF@9KqA6A}^8YtzsiX9JVlxdo7 z2$)FIYfWAiRJTT%;yU#Q!Npt$Pz}K|`fz8F78I@RG!B(J7)~K>=@ZGhD(>pVlT*q# z4B2||n*lPg=EDP(oF54fEmF>})gjw`Irj-DdM!PZ8 z0ez^lW!mI?z9r{27KPI<%`u7`1?fiXOv+Anz|>{0pgqkuHL@SDXhBh&huZ62!Il_iuRn%0)tu7kRLbp6>@HZe82jxck;h&R1BPXu z-YIbLDK!Dnolh!KCA~TEq{~baT6s2iTExMK6mGeq5IO3Ghg}`cg%&+*6^nFBIE9eI5^;ZRb!iFy zscj4Fw3;pK5d-Yb4BX+_l8=1?yP80{K~)#!Dt%@sHaUpSwRWQFJ1CR^v#sXku?zGt znlMEZEu2^dW93$Q{`EpMLTo%zTH349hh`c9Vj@{6gP%8B(RH2*mo{j4$~B$`4T`qK z<4Z?1_iu(ACfa3uRw|Wkr-^mD$N0Jo)%5+r5#OB-c6r^?<$FG1eIM82bwmIgY+pqT z4lLf+B4X466K+;f@ts14D#De%G;xSLmt8NW)}tDrMhpR4A!r9=*dW)I3GNjQTxLrM zU6$IwYN_QG{_*=9>5lsSD)+n*i^c-f^=kXt4$x;`gQ{_i5W>k>&~gEwdxj$Wf;bv7 z$A-qE&HAe~#--OLpT`4^cf%-FsGyy><<{UfEUKkzt4SxXb80mujR60XaA4t znvW=LjoLz^_S3XAHl^2vgMKYFB%+pR5%y*a^g%R&6G{(Zvf_BKeAI9q1C&5dJr54= zsoY~)Im^vTHE4f+O{)o916ZFt*&HR4oTcE$aKa&HJ{f%0ED*~!|23720x%M*o10k% zbyZn1bs;v4U3}lh=i@}Tah_!zPrmRBx*bEX?FI(wx}k+BXHGWClecXzI~&E1F?1k3 zG|v)~TVXml!%&bxI3(Xh_+S*I&KowWngOwy^})doHV&%+%|PZeQIV(;pLS0=cdqm_ zJrz11)0G7+K+Eh@g~H_8DsPsz8+lUtD;LUlqU^?Eys-J}yB5h4#9h-DTEu2=j({8C zQ*tw=K;r_ccCNjYO=tV05QD*$TVBRM0Np9e);Il(WFT7lomRLU+P;J;(2G4sm(H@` zhS`NamyqCQ}J1Q1BW3zn(_>@HRe9u*`JOUk0@7z zo$hWx!<|Me3tz}bI(VR)7R7MSVe;w}GWDXKm6%H`K;EoSLN-k6&T%3N4nv2Mqpf+ucX$)=XSPYjlXOmRVTwqU5 z*AOln#U`47_IH+$3ZXlcI;3Ue)w)I9*YQ#yDv)1IHSX_HI-fp=h=+ zEhf@81ZN`c_>?N92ayO1Qw%>fPD$a}o7Z#Lbh-!TSo96Vh;B<>L4WlIxe5Ka4r4Oh zTXaGVG-_rgP*r6GZa!C~XmAV<1VCw=Z{{9n2OqpRg- z1OHnc3)kADQPC8o z4_XxTw!%?q&{%0lh464SLzE~y!C3z>c`l(lBQ#&8O>E`?+aN=l6QwASGjT}vL4>YV z2P@ZK;aOqhr8i`{$El8Gs}s(0U9e{VdU>7&6^F0on(Vg+2U8?{nx!Q`7R&6ggoCv$ zErERop2KjmS=#$hgC0s~nHsFFTwx%WDWCQfB$v_kv$n!0zR^C>_rx-r;_lR{VZwXM zphIP81k?y5TH4{hH5$_8d1HJ~aT_bC3j*OhRiZ<=F3VXt6$UW_5@?Jcd7!TLhi^|9 zT?b>j7?@-eg9v9wP0m8;iQz@s3m4tLczbVXhw4pGzQ3BUmK2nvx(IQnhm8S*svE!t zNuvEu6ZnwgLtRb$NmR|@YO$G?2bocead|*}r3+0h5=FhR9q0(-bqS4UOTG*tQ4M1I ztszo+$|!iNv}dfQ<~FI zk2q?DpfwJ-wG~O*XE=*)My7W$IO1gs#R)=~BpkGIAYb0`i9IXvxLZ(xs2i2(qiW+RR z-DA)?Sc6w?T7g6`A;&9M>bf^=v`kmT^fc*|Yu|y>V7Fpc?R|4^pdSnvv!$wk7S%PB zI~F!>0B}H$zkYld8sMvtmxYK`(Dy*wBWUQJX9k7EmFu01QWul*a36Ea+_Sk%npThO zVsAQXj5JS(;to-Ov0E6fp%8NrP~E~k!cduOqmGScQbWaTYV|V<^=~%~$9&Vko*4G-w@Z79DM6I~1O|Q$ScsBT;BQ{h!QY4KXJ=9SVJkC6S)3&iIQM73S zqW~^H>kOc3CNt+iR_K{v**wZMqYaKc<{cMH{WQSQTqvb32kF5d$u{)s8FgfL_q20U zo_+t>A+ZZH(!rqSQJyifI>MsX`-8EC%dR7u^}sH8RuZw%*lB|vZ5$;xMTX=`tGU{y zBqyC9UG8kB%{W$!nC|aZff@e88sI;5?x71T5pE1}Q_S;Xquv()03ZNKL_t(KlM};# z+l1e77;CejX`Vp$PoGoXgSL|t66MQkD=gDX1g}XCuna8Pzs$$nUjS1U2~Qk(P?p7*cgvK0}7S;A&r481h7$Stl&ZfjcuPvrjdFeFZUur1?XHx zpxcs`G#uo5rjd~Lq#dXl{PshfJ4{so=eIXc1#vUGw%22`s?`Y=p`m8gCbv#0%;``{ z?V_m?+RR?u8&jn>Yid%_eeY$BYEhR13q=aq?G~L^9|=z~nKWDHpo7}vA0GbXfmjR* zs_!dhSrQUtrgqmMN9bv}OUuz1KigiJY#eA)5Qs`l=5;NMe(@RYg!39Q4d@QTYo7EZ z^|FReh1NlniJ;r&k8h2xe<$|nBF4C&s|%v=*Zu57*P!{JqMI)qWk}_8{uF0ovj?^T zkIP+mc7Pzy7zE*nE(0=D^nJJ&Zu|MfL3F^4)|%)kD(%I?n#i;w&5}Hb9Ebr9IO#u@ zC29-!Gc%jD$PldlV>+f*l|zaS({8xW==MSn-JufU6qdslYJr+V5LU}dQ=|`5xZq@- z0lC|p-O8H1?{;Qth@n!P1%2q~at=C0OFWLV0Q8N~5pb9KX&NX~Am}L!%PT?b;7P;l{w2=`Uq01G>vleh7AX=~yEAR)=HFM|U z!v(+a$%4V2N(b4XISe{L;Yex$Z$xD>zUz%LgARMO3BOxHS~w4f5t(iPSkwp&JhS3e z3up+7Rx}FB<`43ge#jUqV?y8F^e+952*44OotTAKp*TcY<6t7$3n>_E1Lt!kizw!w}(ulsXJqn!{* z(G+XiVPCXnTi1X!6JdD_rO#ln>C`Yk=kjj{=Lum z1Nli>ryNfJ2VVE!mQrP*VJSl28^N%bMK-e9XqKaPI(=cw}D7 zZqq+H+6qgexU@u#HyA};iuDoKk)6!mv<-Hx_QmqZj?Eh~D-ZjpSF-|4C;{6v*Er6q zQ*ADj(w(Tm1N0m=;DJu{GOa@G=Bv8km5^KHYRh-A5UQ z5)LeJCT2>;UZI?ZIOLEW(ojQ-d6k863T!a=B)5lwq4k*joU@8djfb4>+qA!DVoy_! zq^WTRUPjBZ>VLK)1Bk@Xa`zHc7lc(+x*=dRW1D$78IQxJm0_fxZU{?^J*3#VMwhhM zjtvU#2eV88A^}+}b(;`Apg>rrL+St!j*91tK842xogy0}C5lF2@uoI)4;dmS5N|Fz zOBs@m1~|{_4GLBtQW9!=%d0NxfpVSkjPqfPBuap1wV99=#80$er^-! zMiRfEWGhf^qvY(xAv`>^ZCWvjF>Oz2fYY#rZvN=3u!xpJ2~sd1>}^G$2*i4B=N|i{ zzWP*hE$j1$E^;$eV9(fqOvZ?u&w4gS?#4T14%0T|LlRz)@Jbp?(n#Z8ITKQa#*YXS z4Q2@jU7y)UV?a*Xn=LEo27N;A^ZFO3A<4Tzl*tyyI<<_No68XqsD*BAZ9J*NtcMCM zA^IM==>U*4Q0MCE^TbTsR{Cp9`S-yKdjO=HCM=27H)jo2skHBM}bq-L0+PFx6@=|WfJ{&Q$b(cin%!YR&{CHCAEMrw*mpPkw# zXin4RntY)`Q*?cTKqp%_9@Syflz0kH zDz=H(WC95E_fD06-YIiMDMae~!L)V=^6^3YKw$~h769WM`fH>Q_EflL2k#!#$mxGo z8^1taj{!q!M#9cv-QCrO?=-YSrB;sY@cTkOLLXD%#s+UrGN5aP0>QMqdL4`Jh%Q7| zx)}~69-F-^x`T!mR(rZ=MB_AjeX~DS(Jcz-1!Nkf8084n3P2v|9Wa?T$R;*e^Fi;i zO{pM7uR6;Jm8B|Z8)+pS!E@+(tdQf_&zxGr*J7i>9y3iOFiiU+fj0F@(GWLV*?m&E z4&c=Mm<4zs14O)6tp+bc+7<$uK#e1MzI~j7atNpLrURRy_kzBNmdNyRaD9wXN#PBv zey?owP>gxFyQQo?-pyG`KMFf-lF^=_)5vJZlr~TUavsiNru|_!-yRXvx90=Zx#)_f zXP_JshOE|f?vcxy(Nt1Q7Kh(wOgh$eoYeZeO04}|kFxLMqDQPO+V)iF&|n}MIIVO{ zxJ|R!bsA6Eo`XobM(&G5dC&wL#YHa~QG@Q%syWdIKb+R(iu0MV3X?Kc1MRRJ zQDrvK0sEoVB!M(28YvM2h-Ti=hfK(voX3>%9@cf79Xp55tjXWmHwAP7eI;*PIp5=?->C}K5C%RX zJV!DkV&YJ(nVZ6?re}}Cw65eZMxz!~)tEpFWOIwz1fV?iwoTwF7W`hw!O=-fjq#-I zmO$;xG7tb;2bzOshp0iOcYyx?dI38=vt)0)(KthD4a3qCV zfVv?8CyEKRxIbH)We1XvfOZN`=Va&J5Xi&i(@zdw>;3R$QP8qmEWLKKGDb z3dX?R$ZXc&XM1Q}Ga0&Hx+Hj+ywD{JwHk`02Z3v!%%qZgt>8ug6x7bcf+>6CwNc1< zzfQK?EXn$T8(|pYpDdD~nrY_VRaFnQ8KL#6xwCBf9KFzFr5|=3*xhw!2>jS0vPLvx zqPX-w<4B{z!4-9Dgd{FHD}qwX1t|ZkJHp> z6t~};?CuuYvV~jr0`Zvs=E$s1lALQyY2>C<<{H-z$3OD{eIfvnE{z^6h66f|p@z*e z+wU@J)=;cNwd;Xk_Z+q-7+BGosytg!J?P6DGY*H~*aS|fVoo$6ENSZI3@?^4AXMG_ z;L}E`5e6g`xUIDFevna|1(jd!RKuQ)Q|0br*$8nQ5y;-PQ$*>mI&Bnl4t?16 zMYRG@P&w!dZYb%A&lKlGf}Rd3j!_Z;QMmZ>|rG_?^>vf zvxtWBPD3SXV-?y#lG{}syiT)#o63#VlE`H7)DQA)J01@S+jp{CL$|%_o-77oBFN+o)&FauK%QkhR zrO3m}Q0^=X$|zt5{u|ni@y7(Igv_|_4*-fUvC$ZAMK*r|CzDAyX?w9}M~8C>nD zaY*m$c&22bh8OiC5Q9l8=T^Cl4WO)W4Kix=1~&d|NZUTer#V7l7Ti3kd9vEh!%89I z&YfOvV>m!evT=0+q4Hb3crCZO4(zE8+LVTxv<3iy$#rybZ)nlxtM&>U?nSO0L>V|_ zMi11?StHO<(vD!U1tK2M#Oymc*P%~)9T-3i+<)kV)hKrpDu}Vg$ArpNV@V!1dGpke zdypoNn!)tT18p`36|IQLLQmh*4!1s$Rg}?1pd6^Pk)R#>KSXP!sqd_$ zJJGmL8tZ0%zXS+oUeeaLak9&~;^qFnpcKYpykdcS+CsO{@-}JT5QRSIu=f+HC&+PJ zml?>JP~X-}=!}#S!WN9nI1ewZ0qAbwL(4|Sf)WsePJCG<|nf?ri~8T96J5bUW?bQYYHoPHERV&dlWgsrHFnPNbD#xxoeP z0}kvIZH#9}gj9zSgSt9)dia3Ql?&rJ?jd3flIt(72UvTw8ZTWzy{p*|#LV(APiUFE z89G3O<3Vtyn{t!RB0yYAOZ|=F9pRQTh*OZ*E9N5F${us~aQnYbqfuMSRPk(n;gjYA zp6*O0>BUN`fu$3LnjCh7w`V%}ArDG+=hyBC#>H8MmCf6XUFh<^MJ)B zkGm5_l(ai9JKP?UHyK<}j0GvfRxqs=1AZHsyw zaQ-!9vx%2`X^p}G%EyBZ5r@VV)5Ni(L>PR&==OMspdF9}eGR#OY;=1W-*3@4Pd)0YHS|hh zJvpPwcSx|I^{vQ6N9X6l+^_vqZ1>txSspi&(7>)523|S5L3dWnET4KbMKy<17zZ32 z*K=y-?8O)cF(GI_)H*X+3yDc*>w*V}fuvN8Au-&h3gJchFQbhbN#I8$79- zWHnQh!9qT3T|vTZhtcK|)1DjjRCGtY)D?O-w2W#8O`C={UwKk2?7^Q(Lz|w+6pCcf zPT-HPe5gv29iR@CNsWGNJloHaWjVe?ejCYOp zsNc%D&AX?f5+=fBl;6?7n5)?kK4k}16i@8BqpAHo4aIxQ`QU=BstXe@LGHr+@XA|7tO5*_z>c1ex;3QA;1Cc0+t` zhXw4=RxdLOjAMtiws1qS_*+57{p{|IK*e6RpCL!X3`|3fPLpF*{g40Tv;Xkv`*&Zw z>38^hfA)jl`~Cm-t?yT(G0wEcYzn80W(+CPK2hXBTYl91orm>*+%WyP_dg9Z;6&QT zrgQi%cDE{U4sDIK*t8@#9>{c7=lq#XIEyqcakCW|R;4v#Xb*_4*+)+0b=oQj8`GR6 zpI#;P1|19)Xh=u#5h%zuq}(Z4JW~QUhFEKsSF!PtT z)+7VSo?~fLO>2j&SI<5iG9+ox9%E*;w8m(Ae;vrGL6B+-P4AJm!DW#R3&E)tWt9l4 zQ;sux%|QhYE;tCvUDZXaCzQ_(?PBt-A_jS!wT=xRjS>xxM{qfInwdPy%?>&5N>4)_`^yhti9`bueA8e#f=lU5qU%t1q+eYI^Pj|MW>9k+Rc|9NC zQ2cQr5s5Y;6L>d02S=n1jhaJ4@0y5Nin82u5NjR9dgsAMZ~r9U;jjG6^S|`+Q&)Ep z=a$)g${J$32DI;Kv#LgGu4&XbUB}aBQZ%%$&)5eH3FSbJnbBguo_@AqxF@R+;iW;9 zXTNFd7#hS0y_`^8HacX#o*#!K^8mHU=V|--v+Yx;^QxyXuq5rfJ;xfI*7AA}>^dDk z$C1|W2WXhnl;~MSpnren$x1UT!Zb{kdvdz`&Z#Og#1QvRJ_r4LLeiQJzDJav&ugsH zT?ysvro0C;_JK|#r8}|OGfGF1{48_YCs3{SEb1CyLrE|(FYL^s)wNK9A{px_>Orja zn7T8)CdB!UxR=0i_TU`NTi-8E*fzWfkU6!HsT7D5`#Gqi%Xuw0xFF3EB`x&u`pZAb z6a`gQ?Ck|T&Q<`xxUX~`GkVb?0|(h0Vlu)F&FLH`FcyAH>@m`2s99%AbQv7&{UFZ= z3>7To^@Z;b2Bg5$!qhz0moUn0Hem}v(H@`NvmFQubF*#Ug2e1uB-M&wuv(*Qo2Ae# z42AKa-~E}>=b|5gj7EpHh>>=5iBQq6XKLgSo1vo;+g`DJ)Axms{(fcyNqOcVp4^pu z`0e{X`-!KuKgyfqqNKnk|2Q@mp7(o*0bce=AMG`@K?5gy>^0qnGPg1;o<6Ce173K4Fk#K4#a*eEK zjZwT$#pteK6Z8*isZ0#Tdb#V#Ky{!#3x@?Nyhu9u?p5dE`;eR_C`0gVo9Rv7SE1_S3qgi4>4 zIw-`gaAX$N+EnWLs9`AJO>9Nh($pN*k2%j<<6pXFhXil?{l2dwietYXFD=!C!VB`; zyE&g}(e$x~>mD!z#2)0<6BpPk?)kOv;Q1eZj&;hPi$1Y zY6a+gty=WwIo|4oy?L9?5e`GXwXXa?>I3Qgj@2%Evl-7}+G(U$avYra@6Xqd*suH^ z#2_ZvEvFc&A5*2(&RzxWaZRkDdFKvFRBH+8GqbHDM=|7G*CSKB_d#Y7wi z6#KKmf^8*0*Pi_A%KB~{X@bq{95*o@((V=qUt)=uwod@iV`uk&pP=t;q~rG5tKm{# zaS`%EtmhD1YAj<_AUwsJ2e~&^vyh)9FVv}t`@!w^9tnY?JEKj_5U?6mKn`AYF%g<*M>j}kg3hVSr-Tz^@tF{76iK7 ziy)c^wGv$u@h z;q&*d>e;6rd*PumG@&p4XCJHX#Hn=`*hXnSimmaASuG?U6Y%GXNGB^tt@oFk=}jSZQ-=Z~weI-8*| z1sKq-p}9F8?$GPCPYUb6rnMC0A!sr!DxOB+079(Jxr5rj*bR1^XaHaoh5^|rWXO03 zbb5aISt=DvDFiH;?pE^m@BwC#UCk>AdyOmN2fIaEjMBCb}tNtVfZ>ruKCQKEO>i!v_-L}Jn%Vtz~ zl#D3^6pis>tsz5lp_BO7);`7hmz4y6Sp8V;br1^OF!NWKH*FX5%Kibxw3lmj@$R04cUGK0bZhK8B z0&VDa6pD&ON|=?0bFod@sun`L?}?QVeM}C?L3SW8yzRa06Y`w0?QTftn7--cFeZyk znuYHd|N4)<|L5QN!8_mlNxsvsy!!0VKXLnX)x@W53V040AbqGbd=C)R%?cr^=)&{#pqz%FWMekVd7K?-?&-!SyoK2I%8LWHU58Ln2-XjxZ$gq6M^ zI(se*hp8{a5vfGLOfx~KnvfluDGoxHiSj%Ts|!TWs6(AJ3&n(fpsyoCOGQfxoEwF; zjM|k9lJkzp*%}{bz8!s-vbutebTD8{DtH~zP)`*Q4u`Xo)zDFGR!*3xUUTE83>q9Z zpmNUh04-#vS^@bzazHS>p2J8{o&JtN!<;^=G9Yk3dt29_=#@89PU}l#=mb0T1+Ilp znqU-Z#-xS2X2B+R!ZS3whx*}j#mj?e)EIwo*(o@raAtEu_rm;S#SdJNA$)rKHjR?owdNGoYKc(YTTqN;c0TeYO{`6pJb) zYe}cDYf*;i4-;K%-8b6|w(oJU?J@e&RxmQPCbWYeLD$mjFKIZT8#tX}*O(uOXK!fi zW=cl+ddopXL-gn=+RW+xtc@vi@^i|WTb-Y6zxc<$@bWMGB-E?-KltdKfA#y{{=N4e ze*E#en6nj`vp(f)&Q4&b{xymbh3AYI)*J})ZK^fy4~fv^H%4;=@}k)ro|5Tb&lOnI z78=r3`+D8}`-wZJTX$~Zv`@?F0}|cuoU-G^A;|h){@`!_+XYREox zJ69tqX!FE#dnOusCe51eEvJDiZ#7BKtZbqy0d}Y1`9}Frb>9=US<}KAlh$&u_>b>j z2azYbKFBHCHPkL_%}=8iOMC#CnPTNFLk&uxExOJF zX@Gzn=9f8~LFjinEJ-S>vkE#epk@1#Sa_pzRnvrcXaw59FDar8KBrG}s?@1Ur|&(? za6~@#o1h0>3k>xlWL)^o9yz#zkZdbNnzcA~NJF-?4X*B2cQNcPMvx4lq=NL^k##Gu zqc=MZjq7{jZXGCAiKN>UQh1vXKaI-u+_yXHe5%_-=VxQT*d zoLKle(6z%3*`xh=_qQ)8UdG%8%#${4G%ZVF7^3V!N^;G#XT27TY%x99i)fZVSFkB% znX_G5!?9U~3YCXTLu5{F7srx)JzSoFuYT^S=bw4eEl$eFiAJ`{3c*|LHq# z{mq|z`>oeCTZ5L$E9vf7&(gLh?RigzW~vK}+*O8+bfDp<%^h)j&&v;!vetzux)OlJ zelVZea(x|8_4A*(|J*PA^b22k^4^{2SjE@heDJ;h>l;7(({H`@;5|+MR)?H66beht zGmHQ8?|tXr{fpoF{BPcS?Do?)`F=n9()}-d_Srjs{hil8crTfL?!7ReBasi*8x=1Q z*V&_%jat|tY`YQf&6dkke(c-Yw0%c&12jEdLo7K&#Zu0xDsxV>jIG_>j4}B~b7ZOz z81H*QFtRDazRi&n5r{>*pL)L}+>;oFULKA}vCwXosYE;bDM&7vXA+kMIFv9UXML#w zN34IT3)Ffwvr=H;eC}}QStjBD$!b>2Ye1OnuCoP(Dxy50Cl+-89sYUKoB3yzebj_r z2-{_%)HmC1_UFvv9Js7gxBCaFMfP<@4T!8@izHCz;oa?i44+TWKz%0M!-*EbV79}| z477OGBLh!$O{CdcvoPGJR|GD zNH~=gU}%7#P0M7%nn;i}csjEO^BBL6WX&jSPB@pCa2*rjLNFklv>=U>Y|fD}&^g}N8=hPnJIcZ+WcOesw#*5H zo@v#7aLZQ2frI532HP`q((0K&r+9vLZHPVBFhom}yp1=WoOIZOX7-s#&beto24to3 zx~~84cfR(@S0B53^~6nzQ&#cc{>gWL|1ZD)-dm-gdEyPjeFOCG{_9_S>D7Pq^DqC} z)A#Q_clXY%$M4*}_4xgJcb1Q6h{TF}x_7AmyS8Ye5^u>!e{ap)@EkW=q zv_;i)M~pou@8jB|V9%Co1?lL=J;3_rLh*r#|=a z{fDpp;H?iHtTnF_Ksk?O{|6U8`@=K&=G!0q=!>6v>hrh($^`&V-@A=(eCO@&`!mFN zuF!Q1a!xMe5FK!!ajVUOTlXsTd;$oys?$dh1MzKN8466z?~;KCtibQ+UpA zTAJK)(i3qem5;FnlSbi4%Xvg^LSIur2a$Br?&sFA1_FD)WHmjUx-J==7A;!gQZNph zqRR)i!3+BO+|MD=Dp+LN{%o!SnpsF-)<9W!Cnp`QJo7Ce8M~uld#x#}L#fA&8ss+K zdoXD>mAWsqRIH&`q3eIOkxflsseYM*J_lt#GjYEVmd1In+H)TW$Tb5eYL|gBT9H9D zxwBa?BKn@Me|#EHpHl@-%)AIr};IFhD^fN-%(Xd_XSqua%@x`iJG zJKr%XFGc0)u(U_mPd1etC?kE!q#Bs!B!b{jBEEPAycxE-rSK;zAk2XeIKcXqyilP- zDJ8E%hi45-3C9ICpF}@^8M=<3a1ZV$&Y3>%P+{XLinVB^^V$ZrntrEt%T~2s_UHTT z#VkY5fpWWAb-%$mGKOfH7@Y<&_^GNJvPVKgo^)C%)V~eAahJGi&j$uUFQv{ze=pSU91nW0;kb3eR@o9nyLDtawjqdZRm0z ze){Dno_glV$6mTA`tjF)`{tkj@weap5hDE5X$<;V%k4N(^;=(g{&PS3`Ded+R1|;u z#rt3Q=fC>tujJ&|o$!3En|4I^DlUCRbM_-!sx;8Mc^{Uv^M6h{*cuAHDbPH@@}SpWO6& ze(AY;pL^l{)m_~eG>2;KFOG7%)w`nzng(uWCvrftO@RITRT>>_J%>h;GUFtTx~Ezp z)MOsS$n~Oq22h_xn#K}vijh&Z={Ckjf40G91^u|1OQq2X8Rw)qmIW1y2f6~;Ur$x8 zW0h&PI{SISEgBTAJ@=%PSte2?ZcA{QtA76LKR=tbt)v|nHtT$L`i~*yO&P8fZU(E5 z-PI9UGi5NSp3|yvbBYz(aGNb-#hMpNh-(D@f$6U4uABxVARQ5fvPIPp!)W3#imB){ zEposq$t0T-9EJ=k)g;@!LRo}DnXZVokC3Zj0lVX>ov-lqZbK)!#z}Q!u6dE{GNIR> ztjK{|pP|7{?4IKYPMS`9cy1o<#`#AUoSGBj@VGUdWA;f@zxD8`IZQ7 zAXt$Np`5p~Ob{p`Ze4tZ!EQ}ONa~&rvI&bQjbr11XR{I9Mo3p~g((>tpu9eHLPFEJ zxVUd<2#FAbGQ97*D6k2b;D_YCXl#NA`z(fRl}=b?11E#SIZfJ}*KMDjvT1FiwroC` zR?OG`cS4^)SRu@9da)WoXtA&b0_}fCr);!dUPtZc0Tqg5P-L+jhd5lY1l3Z(o1oS3 zorm{gZU^S6@2-EcsSa)>x*ssCrdMz4!b)w{^-TZz7oY#qO^ecZA3S{HfB&QJ{<#e# zLK~nIH82k_*4ID()C)iVg%`f|u8lz#$Juo_#NXyA~yR>!=zBD2jqTckk-XSHJYa*Xt2M?kCjMKmO_~zy9j;kKIRk zp=$a)83dh_+kg4)|IMHN;9IZ1{r(Sb@?D>RTfhFZFZ|SLZ(`X)i>YY3o^fF;VkQBX zGv#7|t?LxLE?6P@I80Q}s?qAZnCp>FqoJI2p(8=i!c;rz+PU){HdsiJZKsnoRGvPl z@PVEi$f%xXA25kdl;|AM<>;k$-fTmVRfu{9lV?S+=Ni%1#(rIb*r#5@23l3t(9uF! zCFI#A2payJgT_w7Gg=hmY))!qYGA^-P>phW?pLe@YaV^iXg0D~fwfj|QqjA}(!kOk z>}L(asy^t&z0UKx!6{#7Nvl8G*|85h*k9iUVtV%KpahAco?|$7Py>h5%K`$)rt8~5 z0*0vzQyZuQ95%)iDuxb~aTlv`+n$q?KKz<6DS`K+Xa+5m22lWW2)~Ih+gzQn5_aJt zsEV7HzQZvpj}2|gWN=UazM00zgF|?nJ?N1qfpYlm@-wHZ2!CTo1>*U&u)CP_AUBYz z)->4xvj4}qhjSUy;G45e{OOGxEpfC{$R^Km58@Yu4z>saK0%}d9R3KU&Qh0PtRJq2Xk?%l_I?8U%lx-A|PPUJ-n&b ztP!^hb=ymSeyfWK)jflY2o5!^*k}QPgsjHJ4JRr>lPr)?pJhEvkpqHW>@{SOGRY~{ z(~n)zQvUwQdgaqMVlFa5KhefrZ# zgf#6$H&H#*VWuQJ>fDDCh#nJ;GU9^xVcdJ2`sHp~kwIj`EFX&e@|T|b%&ilye%yVy zx^=qqJHPVTUw`VctK0jOj)8(v(Kgj!t@r-DKl|RFkT=T*`Sc6-U%hi{c~67vlU6M% z5AqYPe^<^zz_jAFuL;^Yg-`A#FS%w>%}KJKog5~iUg$ciUmi}XPI~kSt)VX|`mUg# zA_pIW`lcxfYHV_SSblc*A2f}zZ{K@1U7p1gbD?&GLD5Y84iI`OH0Q&?fClMIDvcSs zBUfXghg%@VY_Kqj(5ZH@Wnt~FMISJ)gzO|FftHZn)jv^ZH_UsjvX?pXylN6O}Gk8!9WQ8mM*9_{nI&DqWod=s0={+j2;8^gusO&b_igvucU zuBuIlbGnjtf4cGX#AXIEp@-+OB^!t7E1E;Xinu>YdbX*9Bs!yNu-lF4d!jJ=&eofv zPQyum`XOv)NGaZukxxDkmJ1%CIJKl97CZk=v_()WDwiMvl0=T!BgBDrel07o~8Me`ut zX)Gq^Mc3jXg)~VGQe%d6;0b55T#wJ(zx&K5y$^S9-@5ltf8{e@Kk3AtLs%ZzIM3V9 z{q>I?yz{p|cY9*C`*Mv?gM3@&@4rMu)=QRSy?qFy9+1W9c?iiVEY|1<$H2@ z;Jul5pG8bdZQ}YEq<(PK|4pyb;K_GzRGgiEuY$A3$+Hz{oaGG8x=exjJYAi;5AxP7yHHcBmk)4?^45^&)7Kn?`t-*MX7_`(b zH}3f_@Uzls(5ls%==jxK*epnw+JtFM=g&KmChI)%{k|KmE8Hj{&lM3`T4_qInlsNf zok|GQC2(TGA%b9LAK9OQ#??@7~{|f>odb9K6V>(=S?!NZR~{P5w& zAN=Ue_uu}r@4WTeM`yRV+2yKSnky=bCIy$85iPpP?$LkiCimjGryqaix4!b?PyL%e z`~EkXU%t6}`5P}cv8sRjjqm@(|M23IuinIV`PEmR`pm!k=Ih_zDYCMUY0dRtK{R;~8mC$l=g5r%UubQq>4GDOul~k`taMWeOlWcCR+#?3^MF>{UY2IYgr#oY>p&&;vl26UrH3`S@>kjbMO@WrNZei{)r(Kv zx%=e(n>7MIc>UdPfA6gi9yE03vhi-8`LtVrN=r)COHbdu`^C>Z`!hGBeAR>ZAHD~f zJT<2iovwyW3FP3QSl~T<@AmCq|CtxQ@bdHbKlS+Ct0#}%`^%rJ`n8{X<>BjZfAIZ3 z`l}!O3cr^%)OWI-MMw^!G|Az44YazJ!n}Y^U&(r zZFgydgGA<8)o9CEAeJ;{)fTkLKwD7#WLWybYK+iE9=1Zb9e4e z2T?5?t4>SAOk z>jACmUy%gC+R?C58=5XVX(z^t}aTSl->qp;k9Xx5={G786GEKefs+`1uO_!O`}5rN*#hpE>Aw|$ ze4pq&BTcuQ?lYxEBhubyXSRt)8UL4t5XA$+a_2qIp>@?D^XWT8hI5tL>2Y*7OVb0i zlRbR?(_}ADr0IF*{8_be`tqlre(@2#==jec{pAn-N*zt?z}WA@ zStxlfw*QTvd-*G$MDLLQeDm+#`a240*iv-O+PbeNoqArKfZzSf5b8BL*RMv)2=2zfrs+yKwb3Fj2W%rrDedQx+xD-X8w^($xnPIXyS5`>^1 zVZ(0wA|>-`KrcWxW}fazE!V+pYPm<^rST0hQAbyVp6Lnc)Zon+fKXR6M_N3L84;U6 zOV-Xaeb3{83+YoA#me_*OvlAu+Uy&rP)*-&ATEch*Q$onRhcC$!%ZxM6lG1g9u9tv zm1J4a^%~o2pTg{*2JOXa1k@o@=yVY}Wq!03dp2{g!?OcUZ?o2~@ zK`a)%jt4y*L)+&e#VprrqG_}?ofLbqmKQrtXe!0KT3no+f2h=zaLq`Ovbj#=15z~> zFxA5AjyBsE3#D#>$!RWIY|G<&v)+pbsksA%t3eICy-k}S!l_fajpZqtyOtgPUdU_2jijrmSl_wKUB-NGmO~EzP8BzY%n?e`XYLYWO%n;x+0mx~4~`^6^^3 z^WL|)k^nnK;U=7Eo)0xg9Tu;H;(quuvyGjvxw)q$PrEoETHO_GX#>vtzq8<92VU@f z2huAr{Mtg*TJwi_a?NxYi{7Prv=1ZWLnyGeACc}bSwjty`E?UAvLz!sJZ%bDY-^R-9}X6!<>H3iMlQuCPMeA|1b7@+PNjmS$gS3_{D zv+a5~F<+bpd+P30yD|cW)L7c)9|DQ1_Yt`w3xqvkUtjYaZuM;A- z^zTl;@NvQ)!fVO~Pj)vmhWN9(#jW=*>1{nBCa5g@PUQ`!A#J%pp#gt7MSyc-OH)!S zuD#qPZFnwF-O()zW}hIU(xSPVz_6p3)@%%oYgnaOdVOwmJqM@;G^GY~@$7~UYV~)3 ztnDy#Y@QsNGlhWl+2JPsJ)l(Z+rTQJ2z@TzWbwhzBAqjl%(KNN=HYye(6gu zdKheJ~f%+6SoL*Xk=-@W(W{pJ^c>#6&Xy-@!@|G9m2>#=|G z_0Rw2FMr{=m#6YzYD7-x1BdN@{OdpX>wo|L2M^!AQ744LFMavNpYG@@4hz<@f}Ztv zKYHhHZ}Of`-@p4@4zg-0h~m5Ig7h?8nNZ3whlo8LHS9ZEp(<+`c+n0U_8$%TuN`pk%+^-P)Sd#69ncQ{*yV z2*Z9|HuzyXYtuE!hg8)vG*H3yFQHon>Y~l2+bOf5sg=X9XrOjfa2tFwt&-Uq-|Y*5 zn#~M@Pj{lMmXn6!P@Bzl&Z26m6V-H14lEJ4vt4DHdriTutY$> zu<>MycVVFGbjPJm0jdZBgV|H=h4v6chy&VGOKTm93os7DM}$S0GjLLN7(zkw^*=WR zfg5zMw_a@olkzIIb!%cu&vv;KxRAg!O5+GRw_gub_mj`>BMuOXzyEeOm~J!b@-Y|l zPywVKe9Q!5+I^^?b*um;cPg7FaZV$sICxHo=ddA#y+QE3prT3iwd2K-4?<1@(&27t z*MI)p{o8jQyLn~K7oWNJ=_l^ox@7@jljScu%3f9VAHV&^ zjp$$Z@7;O&@!Po7=_(zqoRDPye<7#sIa3|Lf8~v=E&*2a`vK|g(0@H8}nm5KCFq_43tBcGz zj)BDSknPY0ku_(-lnyn~T}WqiI<5g6_X#&O-~qs}#w?&SQk<y27TzUF-ZUXTsjPB8fARCrzI^L+x{>zd55Mui>WoewwG37sZWJPesYzELM zH#n zO+Ew5JMR@%hR!j>G6yhv4-Uk=5Z42iaX(A47KjRtv5RD_t8GOc+q8GPa57TJr9pJK z*MpqaCUoxUx%N^q;h{H42o#mk_%vu#oML=>9Vm}L#9-%zE|{@u(gDFcM_It^Yrq1y zbgI__qpF-X**1K51GRCykx+9Syn7RrDfb*UB{}TeJD!&HvWnoKf@*kXwiW0+W2b=7 z0@hkHr2AWP@zt`HRZrc!`&90W001BWNkl z?C|@qz4Prq`POScT>I-e!uHPooi`5hnr8jVm!ALBvrj$#a{U+j=g#e0kN?Kcf9hxB z6p-M{D6q*^fNr1R5B}nZ-+kx7hp*k_{Xg^Klb>>W!~qHxHt5^>f9stOZuEXn-@kKj z(r_s)ASb6MrRZ`#xV?Z<4Pn~Ma44W+l370Cgl30qHku8Y*ZFBWWzEG=PCqjf3L~l$ zISn#20AnDPHnp*DE~*)gu8DZgss=i1>_aPaByy6cCJ|k)cu2oftzDyN)mXGo&lYNO z4NYoWf1Qb{he`|iE>3?m+A>i>WAKA+xWnou8s5C3QdfueJE%$5heEsWo&`1|jegHE zS|L%eNxN0Cc6dO`JP7tH9^Tivf#o3@II&q8+KQ>Mq9LbfRLsekCe5DKf1k+ih9tg9 zDDGdVR)RJ1*iiLoG1mTFq8?rKopOrEyJXVXX^!|^Ia z{S0TZl@6?7bBJO@3))>&#WGERPBnmDJQvvt)oKu}%udLW8nq}~()e;fX9j1ry^%zD z5-txy00)$DH8^F+5ir2gjXtSx_jv$~HcRYC0GFgzR9WCzA{^<6V@S=G3@?ugm3Ei# zzrYiF)54@xy8pTEO_oX@_KYz7(4v)8G()h&VAlP6ckbP!=zRUH2d`Hmin&fM*Ki8- z<8-RJfA7vs?0)(0|NMJ@o(P%KU*U&knh5ZEJzkv(U-_vQf42S$|MTLr_doaQ^Y`u} zCTJBj0CkW72O0-h|Ic51?;AIH@2BoR_S~%#oU(^ZXD-t{D%JS4w;sHHqxXB_?h`eH z=9Zk9+a$k`{QFMDVjuyKbZ&}8E`mCgg_aeF=;5MX=+~HbY+A~um+RBowO>G%I^0bM z94yAGGIZ4_s_`?uiAmVC#^F!8V@gU(#T1Or=PluxLRU;4`$*P+5fYY-_%qB0rp~is9 z`R|+{zS5^QNv6?6jH;zyR;1XD6H+aDHErEEcY>raH(S{>D2E1^qy4z)SX2+=WYbc6 z-RU3-R?_qepcZWv>5xjB2`F3(B@9!W!9Fp?U0L*1aQ;CR7`f+rI@SQlj}iJQ(|{mptiKuvu(CpQm{eAd*5x&Sju1W*`@aN#cxrI^x;rgL@9f+D;z7dowM>>u`lrxJyRdp) z>t~*M{QjNWS9d?@d%gYc2d{nm_4nSb*dKCgZNr#0IJy40U;5$;uiU+T>wf(g{s$<0 z{mU<_xQwuS?ATT0*f&Ks}2`{2VjKk0j4-8$X*{PXvol#MCeUO1$%V{aH< zf9t_JH~FlO-@Uq51FCouT3OSmU!Kzws?sF9A*)j)kxbU6_ub5?{}|BHQZAk>`>J=xw5+Sr5p;}kNAtD0yA zDSywZDSiW)W?o^NDmulYQ#~u)#VSSAZZ4C>Brtt%O5aXYzoxJ#|Gt~kQ`7}bcX*+O z+Vng^cXz}N(a=o^>oesm-Njn?G$uOSM5W|l$bj;|noiZ4Swb}3o}xKvn?E}^c9a4( zBnTU9WaeJH7ufWQoocp>n`{7YbnvsGAj<`gXFs@5!42~%vx#hJr0TIKshEVEbTMdA z0x3A@_JlT-KWNpiEm4&^M7M#Bjjwg!%sjKC2T5GLPP5>y4X|-DeX2b{Ry;s#5^|T~ z_oTsH%5x%{on3P~3PJrYbf!FT=puMs;6QvI6lmNgnR>6CW|nlYlEU9@`Qo*(g{9HM z^frFGG-hduhj4T-C>vAbu)XhjTyRExQff;e8`q!xJZga;+LE-~g}RifFxT{0gR>TW zqi>pwNaS?~I!1sITj+A(d$TnYAauN$XD{MaX!u^-)@9CdspM314-N~PHtLUJ-0)zeMhT4(!qc`6B-i;8LXC8kN zbR-e2IS)O+<$HYN-4EWoSp#zWo^hfjgttvqCAoKNTD<@n&SKRp%DD+9rKF6PT|rqi z4sfsqDhkF*G@~4)RHGHuyq#pgcrwI2vT7IwwM|=5?$5=}P$0+2zM+58ZM4bkkaOXh zNBL~hG`X9hJ0MojPZCLTYOG&-3;Gjnm$D(&U==ln)CSGRshF}SpgN)@OcijjjE6gY z=1|dGG|7%`qni^L`J)xT%eV+)s|y5;yQ*uQ-~xJ$nXqi6OPxs3MFWHVJS14#m;v1B z-W`2BaD{8sG!B%&8fa7@!~d#I8iIb;YOBLqjOMmgq1{(eg)DVd&Pu+H`n>7I1N*BHJQ}5S>Pi5;{+YD|tf|wtvbQ5(F)IcL1Cc zH1Hq2k=6^1fFYsDH10E2h>;FP8+Vi8RE3_x3MmvcNtlQ%8#M%=A24gpL8efSLTaXZ zQly%V@Fda=xZWF`>t~74K^QQs^%C;UU}b0f6nufO-^_Yp-H(qF#P*S=d^pQ`ZeQ}f1kj`v*69?U9g9N(Nugeu>IF zKp!mSpfm=(eR!tDD6IImaK|i$=>yKm{jBMM*aHl!719yzXF5V>Fc>O`p}=Q20`B)7 z@jNFg5iW{k5By{{0dCmhv^cb8W3oe^!<#FH<2}AY>0F(%y4Ai6*FnZPG=+ViEmOrQ zk3*iDnm*-#H8i)UeQ{2m&!tm5HSX5gIeVh3*r`MC1K;%p@$A@!kfBV~rx?hLhX^}i zwf`PPGfKtKq95q+Vpyhno=F6zk&W`L#r4{F;G!zk!`KuMjYo{&m?NOo;^2iHsic^= ze9ZJ+Klm-Y2W^iI+8V8kzn<)gWKSqLltsREp~}g0*QPmiaJ2nvocJ>Cf<&TYLcOcekqz#p;^;uP;i>TYxky4T5x`R=11Gr z81zh`Vwf06n$D`a_t>2qp)KF}(Yx;$1>~X@CBp%cnw=x&6_X2PUvf^|NVAD{W~lfKvK zgj>)|DLNADjl1scqC?T10#3Qi%%qjG5Y)ujLo5RUY{-W?kePN6!U# zls5{yq&nG}cKf9Pk(cmMZuj*pv9_qKguNs|IwNaqa*8?%P{r>DIPp*ZkMv0ti z#Z~W|jdlnQW+L#jZf_3R*)6Fbv>MUq#_%i*%(-4TFu6d~5zzMw(~06i&R&I97h;V| zcDv%B!6S0Ga*$u5@!6a9eOTm8q62PG8>#*`TyVF~cC{biSb3^SM^RxLgF->VZr--| zRlW}SZ|W4eq9|_xRgk;|zbUlos&wMAfVQ9@CM5-8O1L>?A+MS3XI5mSOq#f0+@0hQ ze%iF`4u(ce1?J~~Nn&>&bCLC= z6R8FPC$g^Hygpw+1sbV%=nGZ9Z_@eQ8!RXbxu}9Td9GWx@4QqG#jDX3WwD%c#JElH zJV}*Gdy)^Y9AoL?<4kBU=UVQk6FxBlaz39w+N{65kq?giFu}TL z6h85LeSAJYeE8AF=Mn^V`*^3wepEH^BcS zeE2b09|84=?%fG>%hC4Bb7LWBm;HRVabMSPKJ(-Hge~c<6Rwcqa2NY+5{pV!OPsB5 zMx7?=-Xv$uJ_rET*~3dl5tbVyx1r#^Mqow8zQ!~e8Hi%B_UqPl*|uGEedIC`p_)u= z=k(>eC^veipbh88qSLeO6|{3+_6Iq4Pcqz6*ahNJsrwi%=O*DrD=)*(ZJ*Ixrp30% z@X?aS0wO+cyl2{__m zj+*<;$fny5`0d#(YE9;Jq3sKUR>Sff>SJxX;>&we7(o=qkG=<`HrOuZ`Z=bbQ^fvf zn}0O6`jln0#xje>{!H>xpO7AVu*X#Z_WiBc2?SGOJzpg3+Sg-nE&@Vl_wSkrnxj4g z^f$XsnnzINrLMZ%!>fgWRGsv3&!R<|6S8bFF}W|)fzP{p&SpvD1#db=RmHTo7Jp9} zk5fR5Ul(LEqbMR$Y#+@4h6~?{T6v`REswe|;nS#iZ9>`@Y5ve2gdy6V?vQ{WW^H`E z{coi3MfJjS?FSep8Cei-t_p}-)6|e5aza)W@%J+Ck_Vqs%zBnFEn9?-xvgmdNxU8vRrFe zCwy`Q`sn=O90CR(*4SYu|GU;X{={Rq@7CY* zKX-3m-Mw~ZyIwD+VsFe@7Pbk>kVgc=A3pr(gS&UGKGD5fP2QRmo}_qPhxXZY4%_~5 zKA%6TPuPK6BOus|)nx%5`Bm8|8U-94&?!T^lgR84=R*r3J?EM@KHlqv-U}w1A z-jf}>Zq;i;uV_-W-gFulq8{kiJJo?pmy5`wk!K9)a+@`9S*W26X|{T+I%5XqP4_xd zGg5+erGn&&W`i2hjy5icGST`RIaqyx`G?IZd?F~6iLO3y>I~u?@TsaVs7nWLH)xm+ zoNOJOa~mG#!R}^NjtYnXmEn87Z$7s0eL}m;+v1Z2Jl>(@=>6*(pIVZm17G_w!wb z(`W_id0k1M7Pe1&86btV-m|jvfapV^PAcfq=^Yt>P^%DHrW)#| z!4Xo2yQZ~)6vZJZ57q)~Hpw<1jxMYflGJOzG|QBaSM*VDiFQPrkoD|vHPcwAIre87 zab~XO1DB1BE%ycDpy!RZo?YAE4}#415b1vA5`Cb$SFQW`um)fasN1v?kVa3a)6{|2 z)~uoKm4%mxwQqpXo z74vLU{lk_^iq&*yW|Z{NEC!n6c*yFe_l4O$CDVk?u5Lp{VArQZpR;WWa2&yUY_w;h zUxgQO*oD0;w~kzs{mBIcgZqe+a5i}RB=n&ZG_VSRo=hP{^E2GhrnxDcn!L8Y!nmNa zXB!$I9E2WLIYp>gO_!iroN81^j+0W#E{uT+*J(8@_25vmfa@X%&7n^Z01b^?yInrt z7^gJ0YYAeuZIWm5qg%l3PdM?uvONm$wG+UX(?L-me|-MvlNPpjZr{4=vmiw{YcC#a zpC(eK{Mcz?K-GKiKm6eDyZ3nigNGlu_?Z6eH!gVRXYZ)S)#(!>IA`)B9RO*8RW*3w z({+QV8<-ND&-@sTTRQA^kRq^pL1tp=9nCgg&`uRv1wSGOBu+Tx!h@z~s3@|Pf&$}0 z1g&-JnZBqADGfHUe8TP4{nJsZ(7C=5*Ad8zWZlhh2Zro&hl)DPy9- zRXVt*QIIiAk(81RJv;PV(lZR$2a<*o*%{=6iuKqb!i$wk2n#DkpG*(#%ZT?wl;+NT z+djut=X5!2L8K7<6hC|uiz|=TTxD~Bp$OIoztw&t6$}mMO61=2X_p~D ze-(Wt(Qk}MOx~Gne9&Oyit@V`$yt7LXpS*VF*?l2XgAm9Q`P*)xwIBJ&&{KBUEg_F z4YZ~-Az20j=+FiRG;M93`(&$fvL4T>a$(1YY@?r@*Jnu7Mf_LZxOCZ{L%pw~t})3$80U|2>*3DM z{=xV^v&tu`T^FdJd|nd?_yI#zbd&hFlh1iz(QrE`9b?h(Tfd1CltXxN0d zunSqS+3hBj){J|$PC-U&&+tNMa&Us>HiS8Gsav1rj06Y;MagPmBMA*1)2ysHQz11h zRO*H7t+}~@ueCzGSK+k*4=fTEW*pVVIyB=d{T#H3enmS*eZP%c;t=Lu*&PT8x&9Gd zk=a@Po3R;N1UHuNd{%3*tIZ}iW*JQYA8I|s;irONZjc%e;uWVP)Fx6~+a|=XL_K)s zSYu}^Yk}{%rQNfxGhtd&SiTlY1_--5sztS>Z4xzKETPF+Hp-r0=V8`dl9XcKm|EJ&XE;S>*~3&wzZo!^xv>d$c8MUO=c!nBDI2D9vCgKHN`ul`fqcfnpS0O zHOL#>Sh61E-#10-e9$7=KpE zMu4(e@bg)NIO}_ObJE#5#@hOlYd^Nl$$HXneI3b;@z=2cx@el`YAM(uU^M|*Lp~`9 zHI{=?pS!U|MY_-|P25HFdL~%l6sqfsUzhZTx2_EYHWTwNib_Pjdg;u$@YGuKRu4jn z*&XZAlbt8WDW>0g?b2(GH9B&aD+31d-QkH>Bx` zTC|qrojaje9cN#F52aB@)4Vs<=RTRtY7?K${u*`;_%(qwXKj(}0}#3%u+YSIzu+u0 z8qB6OXq{#)SOKL z8yp7cw|ZU!V?8@&v}=?gSP^;`4e6*gBbvQKa$@Sa>KQXLI=ITz@#f5H1k0YAKieoZ z8lM3JW$d_!d^)#>hOSlr!`p&_3c;Eg*!nU1YhL?$|Dnxc)-zfg;oLMndTqm!>IbEs zR5;14Ba`Akmu`+dqfr7?9>NH(a_#MI)O!wgFSj%JFOHrbi2W~*mB)1R{Fb($b1 zFX+>AYCzk}($Jw4X}R!@GpOMUnTE*vTbZaV5HV)An?*3km@7oQs=Vd3&>jgPe zR5T+h?-aFS%Q&QxWY_E?jws%IN*Ikm^zd`+KHoo9`t{XI=gu8wC-sdE@3Dg&#62<>}em&TJdMh|DrK5W`1!YF%hre?9 z!m%2VTlcrOlMOr!HEL1K&Hui9;mo;X(afIj@1$8#)(IwLAi{`$8dbQWdQlL+Z0@Zz z#5RSj*-2lD{SmAKoo>1>dl1xP0Nv zOG%BR3@uZUNm%js>7B>h$Du-EwX>2Gd*_Iz*u@xONS z0R(>X{*7OIF*-PM1~iAZ7P8l`UO4~K`7>9(=zH(&?eE^W_vCTcaCF_SPxh`{Ja^$( zbgyU6pRQ{H`^k5V8KLCH-#qR8qiM|0TETn~XGu>?cNCPZH_-gu8tSe=j!U6u*8tHV z&1?5NtkA^DAg!nIPM^W`cpU;fG=nccK89{6>j+``_k}PR21^O=?~e{A100Om>^b)*B;M82IP9w}Z!GxyUFLjS14T5GC1e}{EH|PyRLG8P8k06TgDB-?oXpbT- zpt^~ZoqfAa`#gD^Co0f`l(>NcQ#xu8nwq4I8Sq$z9`bf z6FsB_6|$vt;Z_=bY@VPvd=8mxNUA-uacQ%HAYJ8#mg=R+r@Wz2^M_irCx8Pj0ASEvQv2P=2BFAlm6~G>r$RxITUM{8$Lc%a_hy>2yNUG-osX zNW=)l_LH4^$9ccsf9JKY&qyv{2&xy;M#Z4lIT%O$^25)5`LEr8+`j+lcQ@`neyo#7 zA9Oi=EF+>K1mF7oYhV97W)uH<^yJyyHQIo_zFI^~_wn*eXJ0}Zq&~!Vl$ntPKWQ@AwdsghomQ79AI2Da)6i%dTuC^H3K{$wrOaWrtMrkKs?+* z2J9?GS~h>vz)Vg!sKT-`tA%JZiHj*D>udObBGy@t_Nb9~R&UTd?&0@=RyLq2^{Lt!M?)--zp)pa!YYYd|^2 zv|epGqPB+)R|AZKDP=4(nu%p7^xYyON+Dom0%e-BXo12f{8RJ9{^Ev?@_NS<3YsKT z1wE4tlM8(-D=Zu-Hn{UaOx)}0q6I-wra{QV$H&uUymao&l@qgTmp7TJlj7%e+H-H* ze)#Ee-tX+0Qy0Je2iM*yVty#UKQoFB%N@l2<%64_K6v!xUyX{jx3|CZ{h$BtmpphX z9O3L}m|kEa{@|Ufue@^k!du6A|4(l}{Im>~)+kQNL#e~}zj)#7mE*kM_S4;`Iw*=F zYHcY?%>iEbzePaL88BHiXjzImr71(3{9p~EpW(XBRC9(^qX?$QXi=_fmbHa^o%AQ1 zvI;AF)KsI0^ie12hHPN+UE1;;}q3V>4*#X>c>b zkkOu;$ZqtSZF= z(@av)i!up?PTrtY94Qu|lS$cRg+{d^RznAQDOxepil9^|rIqQ%ywv04aue9WgHkOa zh2vDbjY}b!+?<6q1fw)d3pY`KBhfTyVQOwez-lV}T!f*1*FY6Zfg1z~kOgT6_;SO zPf1!w(;=FcmBEySB<)$UUWxwqZ*M+$2;d9H(49CjoOtOnE6pL7X#GHqX=u#DD$gU;S`r_xZ83C|~5) zIO5>DKmE=3?>*kxoso0UepnDog}#}N_r}W?&i&ap-u^eodH;iP+<*6Xw{On0Mn*7m zWeSZ&=lJ^N^Jh<;+&Y#9hQo@#6`6$wU5+8{fbK!f=rtprn90a^};!;`JMJAg;@%X#wo6qPN`l!A)dAb!7My z)djWFS_wkV&m>~iEX0G}`(f$YHV$DzM1x-QkhaXMv!1cj88gIj=xk^O@X_rNN3dIR zg%O>Gj%SO!Xv5dXIrMeq8q(6Cy$b@ujZ?xLE66U7Hml4YMomuMW!JQH3&qrp3dWC5 zv|!x~)g@2dvu-+bvL|LWzAq9FO_6{d3SkK@ym{K1u!bEXEYiQs1s_DE=J3tn4SQgo zext1!NOLnV>S5F{o>rwaW0s&y{H}oyD=XB8P>PxsfBHlU?Sutht~W{L4!Wmyq>19w zsd!xI5)B8nnJ(E8#kzT1ilbBtqG*pu?~{qemJTqG2Zff9#WBn5kf)HRNqkNlDAEqO zM1xqN@WdG&1>9!WCYFF`w~{95QwSOojmD&|3qg>?Sw|0LrAah8nbMuAMXMJV5sbiK z))XZTB7$j{p{Pql#PhxVgQw4)KR8a|`Q^8+T$?mmL$K+_VQTWg&7uuuo;}|?xcgxH zljFSKka^-S{`kv(e(vPf)^hL1!;FdOrsF*|AR}UDcYpuye(?T(+kL)wEad0&nvb8n z_q!kb_U6NfY1GsvvS4Y7p%$x=5$8^AZT;1se&yeuI5C_$&S$uF_tD3@&-X_K8F(-9 zqDiOgU%P(w&Eve^v)#Sz?Vaa)TueDd6WLEeJN~trJS;+2MY=hZRi~M0)ZT=-45iXo zmA;sZa}yg~boQU551g+cqBsMk6>^I7E<#zg#Wyz|MyV3fIT?n~bwlctXrBLKL(7Ak zh+;c>RKpma7XhP!L2X>;GXiL+JYjBRofgh4lrsvN=0l%w)0!WO@K$@&hc zjcB@ObsBucg|S#a)&o_T*?3FVA~D9lzSKRmG+Acrs9Y&2gSEr!QC8;jNXaAdw9~RS@ppv5AZ`qoamNo1K*67e>ku?ltNL#4}5`kB|8!F z`wT9aH+ck$gj8Q_r8g6UQ zkYV;tN#ih+77a$h;s&u~yma=v&Gd}#(WA#tkEP3a?aIZs4j*;QMPkrrMU!m5>_-{@ z^8P391A*hb=b2L{FZ`!(|Nft5AZi{VBc5qA*RZSt#GQvvp8lWz__M!%^!VwW|8CmV z{=u{F{P_KU{Mm;$Z+gQelSA}wIy3cl^Q`^NpMUL7&YV7Vtn3c}_}Q;n`0g|{Yv56- zw69&gcSaVM`V6ChkR}b2YhP*|&BIY7)GWm5rSOc96xy(y%TQETvM0D9NNZFak!E8P zD8(~d1B~gGryv@^15;}-!w{B2Lk8LjzM#GQ`Qh0oT;Q6X1*UDwW_TC6ruq6wi~*Fo zE=@3BzuAG*P$$YPi*9XYujiRR&%Eyv8jemI6B|K$|NXPIkRMxw#pbOv7#`($i5#h6 zY>IxTt}_#)LoBe4Tx+$QqYjSTUWz_AoRexwL$evgnX1gopVT3%4+JhJ^foYiFKfyo6a%!vsi4C2FX_G`CEXwV^B;ADQytP~lU$jsA%(WVTHrg@X1)U_$y zlfBrdrVc!*O7eS$cl!F;NZh>lX(HA}E>bAQA1>Cm5!yFVl9jp(b8>{})*xqDhyy3c0W`qoH`(YOz0Nb{M%w)=_Yme}32AJ@5yxyo zBCRe?7c`7i6^XcSZ)n?UcIWx^=LN#2ZYgT+B`qe^J zi$CRV7fzuS)Ms8%MT6p=T3vV z#ez5*hYO+&d9+odUUqsh+}AI4Jj<)WeBDU0a=7v{2T0}H&&0amgfAWD`C7w7y; zl#Lcj{eVwoD7QpdT7>Ab{ct+hHI3@A`O@#-xN~P5zjz{gM8wzLe(kH(b<3vj*;y(d zezx!b;=^AYjK?qh-+ukd8`J9#V8e_wmnqFA8i*z|JwFEr2l(#KKK%86`G=qU*WZ46 z=e=jUd!MTfIXD>i?mc|`$>0C*SAX|^fA8HN-P?Y;E9-^RjA@+=ymnxg!J8ZU&g)m+ zIPUd07{~qZ|NO&urvN<-*yG{AsPi(?O8nvPz44V}bzTwp@YB0@LZ*tFK*ChJ(!+*i zEX9vdRZL{8h{_nkM%w5N8MT$c1YlV?m;u_!-({vuv;B6OFu#b`t>AS_e?rvLsV=rZ z$Hp)sBATI%p&Uky$W%9^!ab)9GN#ao3n8GiMJ&Tn>FpW;bQXJ9I-Wy}2Gd2$mZk&JuGPTUL#PptGnUp^}G| ztk)T(iIeGlE={SfT1P4;_qEZUHGc;9Y~K9)Y|Lj8rMH|-QjBSX(ile+chu=Mc@P1W z1I$AlD4RnDz%W{!P*l?#TsH&bWM@yynqh6J(blDQOvIKiPDGO$-lUZ;)TvzZFVY^e z?4&Ttbqg+jlN~WOd!`56QTSQ#^md{b5U;BQ#$=jhAbk!*;lD0_@A?h;)PP{a*;veZ zWt!B5%ET&`?iXeU$)DMEF(FrN`FzMx9hQ>O1jx^X!F$3COrqL&@Y!_D9RxH8UC|&~ zjOjX_(CGq}Lxndn59I@Grt^=whG_bx*7?KX#3s3zvt+;%q_iJf3-t^-;K3^al>0tC z^B_84%yc~FpUxZ2<|2bXHy0BUFhxHv&-WsmTAn1(pDmu>qWRgNFhfLu#3e)K>|7V+ zIhwfZITx3h*3N8soXg^8u`F@QmwP=SEKX-jYX1EMB#lVndrwx((`%0L{5cNpKYD!g z>gAWdub2X}2 z3izaAGPQ2a<#m$Ad?^l=$}>$Yn|c~eCunai`vnCiryL4gw-`_|BcjbA+uEeka|tcp z0gw!?F*+ccTBRDhPDY)V=AWI9V6D|)l;x0&mC(8f)on4-_Ch*qmLO;yUd`vWnFT}Qn^Xh32&bx64Zn&Xj6{V?gHjD82I?>L8R*kr%SxsQ|wExDcr zVwkSP3?C;AcGa9DXWss_{-dFBK^Ae+7bup-Q%%803)SKQt(~^?LDy5VOBe+m)Q6XO zoNeluIZGH zD!>J*E5lT5FTD~L8VwakL)IWepAdLKP356{|M_8pqe!;tQKL7Ppf4D?Q|u z;=%A7EedVnsE9<7L`5<;*4x=6v9ZGtkj8d8sIyVME)e179wTj|>ipK}5YzFFmAIh? z*z~L;r64<`2&AURv)fohGqMw$=os0GDG>&1gLFp%lUo`B(RbieN9gsIfdTA+gL2%L z;b(Quo1aOb8Y+Z(>T@9luJp5}2_y7iwTeO;5o#7bzIpGH<1`=<5#RjETYvE3r}w@y z%498NJ|{q09rE?;={<%<`-@Z=vO4m9`3PWNieLuZ@|3=2bTKfexyc;LP*{U2l zYYA*bNM~nK`t&sX%Gp!|l~gY=0+<`gLp>ljfb-cl(NeMOHUl?k+-+`h$B=%;li(APuexX0}CM?6KLAEu^Acf|W zY&ODEKFrLSk?yg65AeBby-PL9(cxamkk5vU+#uSS!@4pvqtl^g5PHwUiexP>56q1A zAKE-2>+oJ1t)(7awq_l8vMw1k0E9&RP0u#v0K)2GqRb5*vXa?b<8=ws)sC)myxBi| z_7~1*1yybVtErKFN^JV1z=uUMM1@+*6Qqb0B%t_(m)Bt6dZP(@8*4=Rd2xJ!CH6L19a=|XC2dSCldFy< zEhw)`Rt@1q%c0VUKqZGI`zAeT6K4?}H_OA!EAFnbJKE^;G?WY&o*pNkX7=}H8JRVW ze3(|L3)hNJd^voAC|#`GLe_+4MD-vq`7uq(E$^+##umjAyIOS%ogIs2&1r2JOu<&G z;a`1x`}Y39!Smx3q8HAez51=Mym?JT^B6_5Ht)q26gC|WfB%E`e*AoI{|kp9K773M zKojfP+02(6+FTMajYh1iL+BC9v`hC}KBPU6t0dN^Lidrb`&$a($4{SqVSC_%gYo%4 z{^(af9O&>+N>eK3t58lduD^2e!s}Nrefe1QD`5QPN1uJRG*%&*O<3e~SuYRJ`0h*SQ zjJA6=Rhqqdy6$!JdBv^JG!P)F7St3#gGEEn_-yRY{hF0fL=7BH-l)Z?h;8zz`N1@R zK#>C^HFiVVGi$oo4AUMpc{IT~NTPkE5A0xBJQ1bQR;y~$)#NC9M~0c#phgx7_B zGZfzB(E1%$M+wouL`~ZC&1|qHD{Y#R9bGL|H!|^&k4`{!XDz5Uq@;(|2CRnHXi9zE zq@7>r38)#>H?w(2Lv5A4iH&UTNt!KZlW^TFM!yzeDWfP&s)s*SAMrVaDH&XFx09OK z8c_rGJc!YeLgXuk4o$n3gka0?!eGcaaoBcDV^(HlC z$xtxV7D#Ln?L19$y)#VTLm7wg4%$TT!br%94yV>EL6*kJnnQ7i zTsZJ-d%tE6J3nJgK@778C$aR9I!Kd@FVk!Z%_!9DB|Z2yZTO3$^4T`JKX`0{BLPH1 zO0D%08dRHefOM$NPq*Qj#x~WJi_6)lvnxVX0V<@S(Gl~Mo}J86n-U+a&&?>gj>3qTA!un zGi6~JPy;>>eRQ~_14P77ok5`XpA7dxFOmnt2%{Q}O(5^Zb8t==dB`4~HOsIzQPT@N ztfrfW*{7ATyDr3NdRVhk($YTnQ~;+fctn>18!P{f0{>+j!bTIKP0?M^j&)nQt`Q9L ztu9HyYYCmjkKLzm)wZv=(saRt-kk9vwJBMb|3kR{b}K3;s+#^?mgY#7dv+(4j$+z*m@Q`7J$rZ{DD{UfaK3RpwXt(AqsQ4-bBG;%gf08dD=K6l&##O>DT`_9Tv|TpqRclZJx8w z?mv3`&p-R%d%%%Gc)a`TPk!|1>F#q!1s7DJ<}bNng+FhD8AdQyxE~IX}W%P;Tr*|Jc{`A(p4?gdjzxUydpS<_GTX#a542VmhhjN7pF~9wXUwY@_g>$bS zx8Z#EH#dH*VoZ0`=?o|g5rB0j9w0ei^-=1IL$p90emYyuj6PT_3yL15Q<|TMnhBN$=cE;$F{9%!>Y&r$bLD(-XHf`Ewhe<0 zw>+l3%+fP8J1Eo{6o!x#`{7tS6%w?uv#JH>Pzb%q)tLs|E(%4G)_M4Ci-hBL9jYm@z17Fg|vw) z0ZGcvHZ&mBcb%rlB5}$Po4EP+89f@4Sq?APoP^ok!H|;Wy^MU;g&D!c$MCjMe8a?OMHY<)sUM z`Uk)F?c-`c9&SIm^@Ddm`aqAJG*&=LvD2>qth+<9eGak|e0+1#r>}gTtsQj{y>3g4 zO5DAkMenI0zIxy+NN4KW<@ZxyjL;3&le?pshPe>LFHLHG#J<2_nLA| zOdK*KpHF7Mf7E$m#ugnh@L=X~=#XGXi;sunfW%qbTmsQEf=*3v-x*7TAm{`}wn z&EInIswvA!kaU=HifuODQD&6VKrM3>a*Y+LObDH$D_Si1nEA<;teP1;ZHTxwG6~9P z60K8h@gnOYP7T8oNG#S4>!z8@m8R~R+ku?)-bdg7XI>S zhhr)Qj7#DyMQ9dkSZ(5J;pq)!=gh{>P*^i3?<8=vdw~1K*A>rh`4H6|E2C-ZymSXS zO?ja>LA@z|u7`jY#)vG`ywtkF)_jWK9wkI6#l)f%wFPO*)#fbZzIkG>Z^n>o)NRz6 zpa0B9L_FQuef;HXuYUQs)SRoAUwZYy!|j{fPoF(sTV6wJ{IOiM``b@;?mpPQap|RV zXV08Ibv`5V$>eR}8i&d#&jr%rA~Ajao=dpma@Y=8RCKmXvzzxv(H+bMeotflS{ zfaPDEKDBk?KmPe2{^hBYG6bk!K|r$eo$qT7g;CK$>gj z%I`Dk^DECRIc)i#&qJG)O8Kball{d&D>bb^Os;RTo?&h&kk)(*?3g4FcS_-#(alR6$$Rj5pI8N z-)r;RzxuOp{Nc6Nu735n1?t}3{?7mV-+uNF_qU%u6MIp+d1#s^x8*)DoET1Soycd; z_YVBYR+KVrzjFMtTlGFG*9WYi7g&Gh`&U@oPuBMP{?X#A#e3mVN6=AE&XAe^%m4hX zZ(n-p{OiZPw+|jY{_Owy&cA&B^NzweVs?aMzNfD7JmNrJ+m((wz;k_9L)Jl8`L|!> z8?Ei6`8iLF?fa{*vG%##<~iCkT2DHo;o2G5egfEjlzgTv1v#2Q$>)C;KS|jjZJCy% zY3tHhQVWDQpS5Tj)MYKcKq;9=epY3SxHkjS|I3~PwV})-o({cL zo)<%IZXSTy<=^z5Mu#}!5SfNn=gMk!F>VtSd2_%*eGtK5;y882FMPo@uE#m+&Xo5=h<*g{D=< z0}f?rgxg$<0~Q3sJW7R0htSZQS>$FguX3<8opKOfwNj>Z`bga8H2$ayQj0AoCrWqD zMX2z+2*{y)YuAXNx*&=3PD7mo^M={JPCkV3i95{@PYGlVpu&tKh%FY*%`=hCm5ok7 zNEUr_ntZP<$<${u%;7@Pc?(qEppe9UuhX15*;$4cqaNesenykqIDP+dfoGu`n>QFdmH0o+BU zUNc=&o%THNwJe4Q%!(N`G%z$2JIPTGjf-SPR|jeMtm|+)N88psh-%!yG^ASVEYHVa z`^2OR8)k=;=n5N6;beO<5qM>_;^Tq|SiG7SaT9STVf;Io>y8VjFnJVrSq>!F5!6D9 zb3|C8coS_On(mH(xuw)iQo<`U<$9@Sc#r@|3%R6xgeb2&vVKVy-sBk_L5Z)L_No~H zYMVOSx3R-k8n@AkU=bC;5|%+$uk$TcYfXjcQE37o`@p#o3~^=PWNWcMCXrrdo$C(V zyoh6mZKhCsYAB8p>I5M-$dp=XZZz&dB=o4pqLx#nHPtlQLz&+LoNN!}J}>_HZVMWP zhUUK)rwiN26fO@Db#_J)+~hKpy@Ye}^vFBW^GF0MK8nImgu_se1HGf zzrFGEc!6JMPM^B;SAY7AKjEnr5JpX-A`h^{__6L|O(D8QT}$azT%>*YZ{B+K(#4pa zG`Z`j|71wg{H<15v1^lFGeeM=tBTb#T-@GU)8d^oC~9 z(MdrDE4qQE7=~%Gy5S7gO&7wfPf;6_Tp9kkW~mxxG|fzD3eYsfV;!b+gVBWPAgddt zre#+X3~xyK7=(J?)13;V7NWU2B!JoR+F2ISI39*jwbs^E256|^GLqE_s@$G;AAO2Q+(vc=EhmkpzJLmZvDYPba`f;9Lqy?{S z{(C3_85CuFcKl_fV5;^;1Z9;H)KN;)e}-Ns-99lLv1!sDdz$CM%WJrw!jp;0B^8ck zHhjad91s%PW+P}{rWVTibAr+=Xf@&L^u1>~7D1Bf{@@0IiZ?$|$d+rJQk-*$SrW%} zwv}UDLqc973WqccG#&EV(G=s%m2RfS+$^v#7;8M=uYc*y>le?T$(?7=>j5E+k|5J zF}p@}=eNIRgq)eZVGHjs8LrRO;#|*mGz7L>rE%5I=g*rCyR&GYxsj&^Vep64k0n(=ymSjeTaHiex7Mf*w02YHi)8kEz!;L`@u{azQ}jZh}hzAkvWJe zLeekOl9;EzPiT<@Ec{STu?|XeLkfCarsk4Ud^2EEgQVw34_@%Y@7Ekg z1GSXkBEzA*it(2*#`ve7{PKtY=`a7}KW?2kaq75*>$TUfUVH!Jn|C!$Iv4QFlA{s& zJi{lrm_pN1fB~WQpxGOs#m_gsbnV(OG| zjayt+OafOplFQV>`FV-9RPlsi8HT|Y9MkhQ7vPV+_RiN|r1=l9HNLYX1Nq^)2?>P`%@_Ce5)iDcYF>WI+uBo%i+m5ydpG=7)MJ)jy9*l=lHHFPe* zjAgRXcI!HUsim?}8P2<4a|Jt1ZM`}X5r+X;80SeZC^d4JzYN09^ob?`&=*UHAZ3nv z=Pn9XqfG~q7s{4SEtG-K=0!w5;m;H*8p6rs^WqSYMw+fP8@}3Pg}4I*xg#-0Q&xL2 z2U1g;Wnu}GYc&^$d`Q{P@&}uXNGk5;1}c_~HM}{HMl3dkB5VOgt)@2*2{)W`(^!P_ zxg0vajbyAo+|%nEzU~-Bn3vj2gEua+a{)C%^H*mfbh4)t#8MKJZv(P4&>1$(!C_uT%5#=n)w3r#Z63i$og)|(HPA?qld3oA$hMZd^r67-?%}EGM2t8K^7XB?)oFF!Yr1m;rGD^}UQwU5DM!e<_SbdrnKyzyRX9-_J z`rn~&ZL3hW;k0`~;u`Crq>zi=`H<@{)Y?6S6UH z=wqeDz>y0G)n>?Oec!VP%0ur<5v4-_9}m)TK^BKwF0d+;b6lE2h<$XB%gAzg(S*e* zOX&Eu%al zX`xfY0}t2MdmrAo_1W#azj=`Yb{68HpM1*y=Fh|02`E)A)9%0N`O1`!C?2n0xp?mU z*)vzZ$Y(z>3@1;X+&X*eoj?Qx5-OL8(@_4Fe}GA==G|O zfsencA8LbC4fT*P&#)}L@Vx|^wQM1>7JQkxIoC}OS*+V>A5@wu7M4hQA={nf8qQam z(^+GgEj|43pYW8P^;;Brh(&LetT#*uoL0o_(|QF>_4!$|TE$IXICrAt%w!k;K&?vV zsx7dl(4*313$|}VX)!%fJ#j(THX?l0gQ200ao}~W#V2znM(K*#f&j;A)={*NqJgd2 zKKC5yf@qa;sL$$-mE=A0WDs-MYV1Qkdmd(95Oi#oLcN`vRx>z>9G8C&&qj&jjH=HLp_D$n4anKr2d#E)fZD`mTJk%* z$hWmC(>|=|Lb7R2FsI#T31_7}LTaj|0WA(+Bpu*&<@-@}@2kv-VS&!j4E~$WXD-YY z>rHddVV#7G7#l~1oTis<1(|4-{W4*gn~^bwNyJk^ z6qffW0z#VFvVb_tCNSD(tK39)%wa1&aggdBM0Po1d)JK8@v_TAM3|MQp-3``PiZ6t zD5hyb^3*mV8$`&1-5?ooq$}pljGe&r(C0?m8o~?-BuXm!8A$=0a$uVqmV!VMddaOO zp#u9zZe}-yF^HE<_X>d48LZpkiM~+umWH~lQ$QvjB4AFV*~&9*&m+6!F|=z`C`J@` zvmB_n7>xF*ZgJMorn$n<5q?{_dZD`p-Mh zcE4~Td*kNa8}ofnI~0>0^vx8 ztrJn>(%tq@TrrZj-l z8Guq-D@{3K8Q7|oUZZ*AVVa}=c6LVRerSk#w2=YPM3?CM27w#{fFf|s7Pd1t0p0)s zYXThR*Bp_w=?y{!N{ikCW<^WW|1!b0LNPIOiPb~87Ddc~%djrm9#X(FPSQr}L99ld z_ISunh34rYb_hODG7+?3bbSo;9yJz!by~w{540&P8?8>^N+sHg9h#f~5y<-esEC?c z1Y2Whhgq?cl|@?{?74A-;<4%QBtJt~c%L3xmprvj+aG4mW;EPo$h1ldOE?V5O&N;i zUpDe4J1CQOrbqb!HtD(y{hH87EXbX#=|n>%;i6}0U@>v_gVS~q9sL>m+&Y${7dF3++k|}O?Kj_E zCW+&wFf}@z_?eHA#5*KYVwCzJ&wN(On{QTmNTa*?$!E7ew+Y$X+u!-l4}SK&r#riQ zX>1|nSe!Dz{;MaAM11EYuXkVDVcN(os8F_52L zkOfDrJZC2Z)sj0q!9LjwvxS@3y+^r@O50>=PIuNawUL??p{It?sazALwO@8r8>dSr zN(f!&C|V}$#oistsum`T4w8mcHp$E!fSxBM76I$&)-1vwN>SQa{3q|>R+vZ4zgb*9 z2`%6Op+x}1%oMPqU058JHwV+J(14>9_SsOVp?PU+WxUXv-DzyiHaRGHY0@7#%Bsk# zHWQ2>ao(3@j%2N~0HQ(1qH|3izVb7K23s7g>^i+rxJ^3S@0Gp9U_kvPX=;R2)_;>#up5dreWtsdaX$V>R< z6b01~2LjY?Rt^U&BJvawmJ0sGWh69gDhg-6vx9U$`o+F0yG4{8hnYeK?fzz{C~^5- ze0`-VA#2+#lMJOdrB3}zr|%-Q_#BUVR4#t`{&j-uc_fl~Q?xI!0`o1!nTPuXcuH)Z z`Y~<_IW$Oy>T|lyizlJNtEWacnc5-dE9*S#7F}b6VTQm_bQqD1c8ZH7{tgp8Elf)h ze1IuZWdkmPt9`V6`0pQWKi&P_kAME%=X-lkSDTMV+n@f!4}SWiMFbjkLe1AYi58fo zb%LX#wz*U83sdB#*Ml8sZ@>B4+sADrp6)z*@MP!N{p0@JD_1VRHDqLB(piCQch%Eh z_EVtdBc$$$=|vf=Az-}>-RJNAH$s&!0d2ryu{~yZ5&r?+6ORpN;iV zSk`+IYA=&c&)8*nvgdu=BFiM%0T6l1GD3#_RjB*^Ni?pn%X)QO@ z5!SvG$|z7QnuWpzXqa2c)d~t3Mx_@Q#3Bq;5>bupAM(7xf(Fy5!=5YQ+MtPAidcpl zVCpp^)sto-iJGg_vP2j3OPUOxX(U8Pm^8f6eUcj+FYIBIUt^^pw5cU#@jNU5icVuW zhZSYRCRln2vsQgm(OhPfAm3S&A)$U@O%n}A;Xmv9wAZ+m#WL3mtY@VTx7JNC+N?IS zLwGPaL{FGPPt`t3LKUfw9c0~NVfk2)c6j}3V1Te(QU^r*Z%+Khzx(UI^#klEUu?GC z%=mnda1r@f5TCwhN>L2^J9qic#@#!jbAgIRm8Mv5lIzN>XMl@uJs^$?-=xB~w{`sA0pJpiKY;hv6u!qcshUeCGQIgcNNY{9plR7<# z-gCR47%Qgo)pIdy%|jF}5nvezr+ zt;l+xo#|2&=qc1jv}Ea9Llb(#XqCC#EQ5uVhVseI?%s`?w?Dr4()q2kr%zv)#M#ew zcemgF=;NP#|EKT0w|}q?O9a$uLDu|}Oj^fA=SZ6vWLka(-&D~n9RB|^r%!Hu>+4_n zpT3BYoPXWAbMJRM&vqYQdg;O|$N9Mv!?3l#w|D>EqwS~jLCIzc&BBA52vjgb91J}x z#@&_V?>4vo#;rT|4)*u&zjWc;#gkiGrz0W`#&Q4l-TNPX=SM&P;e$typLr}rw-3yZ z-TX+=U?ox}hmxnjpPkkqvd_Mm*H)&y9_V#-4O0F}s!#25RuncJ@BjcH07*naR73Wd zh-FPK`ZvvYkiDNWnS4DjT8otI7R?o^?utq`Orc&aO@QTYWl}MmbCE9@S^R9k{B&sn zCYV`Kv&WLp+l)HELw9)S{={2cdN8l=ZFAwbPVFkC&&q)|pg&`WYz7=FIc3>ot7S*U zLBv{0s=vM>A!)jVn62A`Eabz`u7cPkttqZ#rA|oOOS71rz_P2-9oUjTLsE!t3O#zM zGnZ#P>7HXo<@Jp5oGJqa?H_22Ku)jCnPBM*k7b>J;{^O383TxaE`waOT$8ut3>@0< zrjJ&zvdE%IN32%HLWoGABe~ZybCO08*;Ms^MPSrRdQG>oHg#>12su%;aXCPQyDiTk zF^k;ScyV25TN=S0evLD1Hm}#%?~6tjHU8?Y-!*K1z+%OhYMCm?O!kh?GY7r-tjOA+ zz>7&bL&B~rj6mV<5s~$9@%Cu`?EdL{ibhx(jSy1HGm@KWyRL$;te5Pby(9;4Kq{QH z@8z1HzG-X(8ln0S-rxeJ`5-+LxUk1qlRVQCr~SczG}pz#H$1s|t-sR9oAcGTK7y303Pj;T|{r&fU^3$yoCw_kA(o5%_J%7IUX#43dip!1BGRt$3 zG+oTu>XrT6FuD|FViiLfSWrEPr#q?RWLNzEm$|tsI0jZ_% zW6GNCV=%WV7YdWTvzo@#+8}AdTU-^1atPm^{CY4_$XcK9qSN^Gp>#^I@y24!KAS*r z9#-l;gw*ZDo2!|0G*QkCd-Zx@pD4l_9cFF5w>&H4_(&If&KS#O zomn7&oq*Fd$(fd;`6^m?^NyO9O;M~6LmsW=`G(heNjATxLE$sjm>{s`Au!fv2l_sK zKNHdBSR_yjb7BU}`^$_rg_!WbZO@3hJtRA4>c*W%GcMOV%6FJifBL*zDT)Nt1aqK7#)u~~!jklwDX;i9qZG{Q_|lKKvi*}NCyDw? zXWwU(f{}}moDaLX((QYjsD+`gM4I@bG+NnAJa7b3kP)ly3{h=5 zErB)5imhqGA;sAmlGb_1XZ!p_u@7A~Q zX>PcP!b%I(e_N?BQrmwi>}Ya?j_O50gpgKGlegToYI3lF?*QJD0sj>aYPMAchS~kdiR0#fpr( zy|`K?#>@8{Z3nW@E<;3N?r)f>KS`wus)@=Jls(6rJTn$P)!e==D`rxlv(pGxQ@(jh%`4BHLpzv z5}3o4-Si>L$dX1M%IuT9eEyI_>Hru8o0yDnQWz$ajvLb&vOs6q%xHZMZU?G|0ZO#};Y}SC zQ8eH3<@SzJLS#v{j^cIgx(s4!l+~!7mzZlpI51Oe(*ALKwInK8x#O;q!T_8G5#pY!WSwO(HPvkVc> z^~hnA>lrN_4rb@cXd#N`L9s6{($IRYcYN8Fe$en&lRgW>&h7pXVMZM&&r90Bta&<7&@Niy2M!(MZM;Xfg$= z?ohN3^XDFGQOI=>Ar~^qj40pTpG}ytiU^b=#mx{jBN~onCcFg-h9BUeNA-i`>5D~i{1mp%qJODlA0FvH8RYQCq$Q_s94oy zD)otxV>87FtBaaE72Ov{nuxOio3u(BOWm|HY?$f!V^gq_@s3*oORPc6XLXj4*C@s~ zCI6#1vB3!j+HkJcp%AA*x#x08EmMnG2%*m%=vi3m<*X(!K(OJFydk>Z0l7RE404QDeSG(1yLJ+6p4#)$Z3OP zHxmgLWG-;1CGe^~h#DXG;fvY?xZ_3HxNTb~g8R_Iu{OS`g+Nv_fCw}+z8Vr%C}*Vs zLle3a#Zk^7k^~BQtWM3UFL>GpduC$h_*P+)oY=7JCWX)p>HgcT*1Kz0wHZ5NbmSNYUlhN zQF470$>e4aLJ{IjGReq7lygfR%24gft-$qKr`X$*we@TP&YFc#?}Y{>0Z!34fKs9$5b8`gcNLoG*CJL4*eP$`myj z{ex7fTmT4S!T!mt@Xv07tR0qyuGj{^p@GtULm(1&FE0F(GK6PF|E^lv`yz! zf>MIAubJeb4a(3b4aw{fTmEyj=&mA;Q3fh{Iu*pCao<^FyXvHfBcEk7=yI;2+*vI4 zLxO<)d2sq}XWi~;;nAQf6^T;nNy5UAyZ7;Pr%5R9ip60bxGV#cbjw)uj(1?rD!da*(gM}Yf(d?)&n}qUp)C;dxo5@z930YDL&oqb|;oj&r^@sY&bNYFm zN@8)YX++1Fy*0BaXtJ3qQBV@QG%+0m4MiYhCCdgY#s>jQRfW0lK%nrTwt1z8n8gZe648mkW%DgXn!@D~o-q&& zy|Y7@8fcsYB%QJpQB!&`7TS#1QCjFGTc7g&*an9cv}|0V!F7lrSP$H6=8H`y8sttW zHQ7@JkP&ac`P!A!r%oL!`y(P^Z-4*Uo%;_SPy4{`?(Y86ot=9x(#U-I`n9)U7O$DT zSx&d!S0{FIa7O+6v>cDsqB0vAQnd@Mh{ECFIv{FMNC)-MZThb3X%M@03hz1^GL&mN z)+V7r+j##%pnZ>SNRzQbdu9n#}LXr)Qx?>{zwXW33s(f#W1WY1TkSg-paDLeGm zZFg;7cWOAS9>CnV=Hix=u5)T=l@V=hfwBu=A;J7_Q*gC3dQ*0j{bt~jD?@@}Gh0&a zvuBiKKV)%cj?-a>vhJWYT-HV~LzyB&pfyl!DO^!&Hf?xnsh45rN~r}qriEF?w}F6- zJ$^P>A{IQoe2G!7HQxg$J0jN3`s@)!rh;uT6a;D(V2&@|#rw7?{4t}BzzX}t2tkpC zbfF-W%Ls7s80ubWA!3Wba4N)d&hA*;V?td8Lb-y&TzG|)mN^(J!HN&0_bT3V^r~IC>1lZ zSpz)<(*HUqM>m_ysw1Ee3W#Hk2sLD&^D64J7bHBb_cBPMK%c{1aOk<^eO*e(^w(N8 zAVE4e&&Bfl2)DM##xGC=v&}lQRac5OzS|in785e)XGYWI%ol|-ivyE zNa+#&9erc&3ud++}dz)X%`aK^==HXGg(|7CFMI>tGgy0kYS6Em)0XdW1@e z!suNb8*~Yd7^c3zH)w9fvJ8UVA}NI}P(Np9iINK~E3KjAEH<+pj)>w~8fAQf>hKwh zSwNN{u6e_<`ATs+rFy2Eb7`xe?sW1ow$#>Hc7ll3JSLs`nKguK32LcOb9m<}JiX2{ znjH_+Zq@}lfT0y(!myRR5lQPz6`L;5zIj&La4=DW-30x~r87*o$?LjZvnFoE)4|-( zQG|)YgC(Q|D?D%Ke}?G=B(*ira>t!25GGR71Z+TcbeC4kL|LYq9?{0|fN?1kH$`&nlL>O}JJw5mQLqv;hb!ED@(q zZJl`e@}+Ap(tzB)ckgyu2;`?XZ{2#4*E+eib>{c3zj;;p#!}ku z*(gUAOeG*@cs?to;rtL_};Y)_BzQ^8^PLBH~JUrfQgad)^=@L`%9cn13}>ZkYc( z|9fhP90dsvzp-fn*|^E~j3vlvvH%?nW@V3la7YZjhv+4w(PJCFY&Mc*Bh9p_R+2xs zWH@A~XIjg8>4=JynhIZQ{p(dbOEAc5xJF0rh%-^_Naoh>4D zeD$5TUnu({BI3r)TX*M4=jL;KcKh!3-roMs$&*`WkNbLWz4`j}-~8^AJAzodTX~xJ zQR)r|T@>gM_i1tYxds?LS5QZ`UZrP#S;)nN6XLaL&tM9PqCpN91aoPay9qyA?5F0+ zFiL_t7Zuzub$!|PAp2)9GDp{bki9pJt|8;n2ECpT7~N-vqCbstjbh4N3O^s|`ezeb z%=s(K7-;FwDf?Z;f$E>HP5Gw>YH1vENq}G7k5`2CW^Y^jB*^o~`y8mq1sa4zSQDU$ zH>%5L(lAeSN74K~#Kn&XV_Y=Ab*7oi&?kDUd)HAEA7wb&mf<%3ASy2k+VgpdVcnqK z(aw~5a?%!eSj-gp;Q4NR+6lWxPb7nT_4S9<$OBJ*i3TYd%g(lHxUGmG=?qiuo8IaR(z-e=S3%S`0v>jLB24F+p}b6q%@@> zuGbjdMlenjcDj}W;QWZ<(2#lf(6Zx2vZDj1rEs8BL=o$;o<-cHpcZ;_ZUhJTB-X3}Gu@tBv%T$Oh;>=-%Pu@f9M(5W6#1um`MSwgDheeq4fk)MDWLPS)T3~0|z0AfU z7aBq&9Ag+FYCiHvbE4s5KSP{Rg81kHZ?ormF6a^8&)LiZsHPV*=#VBL7Z(sk8lnUz z7allmR&30%SbBzT(r5|}x1RN6Wtgskk(YVDPT4=-c!BJXh=^y;o;`g0(JioS?EyxlK??Yl^wwVcg^_NH?T;PD!DMG`Nz?%_7}w zTnEL5+J-H1uheqh5py+1(a(#?SR-99tcdljXPgjx6N|#EZ*4nRG+J!JlBgUd-ZNvN zO9JZ1B`%myH=00mVVs7s9;GwUP1E+u=$I6o+?`eP8CLrwq1|Ej zxpJm7#z{#w*ApVqIe0RffR+XsEy{pw3{Fg(N5+blOZP@VHFG?) zOkOKl8(D5bnZ6bg5*}KE$3c3VSkXUa(Z(sOfu1jhlG7h5-q>L-3iLr1iUKOe8!JV+ z3t1A?t%^Obn?)CJuWO1WHA^10l&d&19qh2iBxv()l3gRsGn^M{vNHmtAYa5m3NPvu zpNh4n0P83NNq*XCVFBP~ip0wZb@-sVLd06R@%d(vE?~tMn5)X}O6tm`I+7^%Y;qW- z3hdEr(#gfV2&I^|W$+-H$(k1oB;4Xjw@;ZW)Sb(HOV{fN8TzpCif$)4{Q03c!i7!C zHWv&#t-VfT*a4-+46q>rpKgxggFc*+AZ)2(2<}mO{iRpKE>=lNwTf zoD$MJ@|1Jhb6&o5;mk`HE?j+)X5qo3?K@FuWQUF5oqP9Qh=w%`dE%>I{?ePJxPTfu zNW+un3mM`(Qd_i|E}c<1IEx){wK<=j5#-2@6H1xkOIB=dT|$d;UP+?8 z(bX#D3PJ(T=q+sBjBG5f@cJCiMx@hNTZ})zVusRaRWw z654LXj(5=lgHe3hu*rwlVyeW?3Zmxr_;XyciPorzHmKp; znc0{~YgWkt0qZ2(eTHM>qYrM0#&N(8_r;>u=nq=cmxWLR_&N;+%I2yQ59k2`I&3n` zlFh{TQiM$fmkJZ-i)>cB+~f>Cy)??$wAhC{0_8Agh{0|8(oq)*(@U5WLMUnw+GsJ) z)f>pAxDMrRbAS!ZYBrnmm?!J*oRzAh5Pq*Uz`@00xZPbCm!7AWkb&Mr&EiIjh28GF zB+hDJG2mwNg-=5ut{wB|ppYgF?9qlTEo>{hLc2`m$2kvXxu1MbN8!ZAgkmzpVJhzd&gg(xuQR<% zhpvQL&e?3H4@yrw*UzA+P}QKiyP)>J5PId|Tz9m3#s^${!A5!pLIx&n6KUEk@DMZ? z7g6N6yf&KWFJ0e7gIGb}9HHkD4P{C%&mQYCeg73}Ue&Zt|J&xj#X+^?7L}n`AAsm? zwOJ6UtcVxfnJo&!l*ZIiRIdgK3yXtjd;b=UZ;RpeJ zhNT!9HT;Kfw$c4{ux@rRoWo$8P-K*A!G&CuMn>UB-XM-jvzB|BAyYsVTUI`1nOn6)T2y0)vWo;~UOc$Agn=_5cDVE2}J~LAD*i5DfnG|}U6rP%JoEh=T)vGV$eg$B> zar4&wqRrUs1$9 ze&$0dq;8L$?gZ2c-C@)bXaogoThJi*K%uZKeLFS1Sjk)EpSb;0ODvd#N0bZm6lF4^R|WF7Pg^0}dvp;)z!r6v%jWhl)~8d+#wSh3J| zv7Gl|$%8hOA?tz~Ep-9>!&$=$AD~V{vPA#6ib;S^`m@*pXOpqQft6@8KG!p*x3kWx$#x}21-AuFS2GN9(?EFCRs zdfrm!pqxWGXtiNgMq`u9jhfMU4FyK`_gK!RU@2DiZfOi;Agi!{aD9Ll1fw}MM8i{~ zE*4BpFa!WKYeBp9rHB}4!kKVw!;0T9tl3HQ2LwH^gL8n>3*M-xg7a1jq*fE1875UO zJE|i~N&e-+xTg%U&Oy1#U7(k40=eO)&-S#b+)LJJq`}L@Rc1}E(}a9Q$%yfWZdw55 z1jN*nT!E0GL4I0Tn2S=`BbP;nZo=&Phtx)G`FEp*8cps{sI-DfyM$;trkw4O&aF+; zikKUmOc@2iGwP*fz)0im4Lqtpf3SO-TVRQZDyn7)Mqz`CEeRd-!H6_?gldv01Jg_g zUK_~`5lXXM=s#02OaY53S~>jl!38l0N$=%?Qx59jgEGx@mp&hqULpcm5(_641Cw^` z?Kj_i`Sht%7ha@Ec=F`w-RHZ{57Ic#BKe%u^ZJ^Q)RVp=G3S|k`HDp1m97SuUL67bqAonAu zxUe<0Tm7TaW>u=_&`?853U`?zTNa793Brd~)Bpe=07*naRBAEUI#n^c1${m>0576O z2iWvKqc-3(GB+;nZBu=pgsR)KXsl|>zZ&KGiCGJ1|6|(m>^^EWX7cMafr5#mtrd21 zMm6f1kZQ zx?7%8DFXnBKz6^eI*e8(5u^dyXo+gdw74Nflr%%WP(}Dk5P6a?H7`R2dvQ8v}s5e zC1SC@?Hf*1iUD*pb-TT68o|tE>aN$`pnDT!F`UVTffu}q^;Ber*%^AD53cO3=N1Q| z&PUz(=*?n8uFwi|Ha1AJTrM8)=KkZ92Nq&0;AfO2x0ww-v^+SoX;v8J;`{(;ldaRE zn%3;0hXomk%5|=~Nu@6?jr+JvLdh{Jisz2eM2Z2&XcI<*@8@eXN;fO{4VJzhMnM#c zSvrNosE5%7z1j59-842ocl`ykKO!RTKYVb9?WIMOyZP@=KKty}i@yG=uUx%;xCCLO z>ju@Ei>8c{Ar)w?sl`C06o=lpEX}l^5#^jrYkr(E2|nAFXRL^wxe+jGgkA?>^@yvZ z6oMJO&98))8fBR@eq!@(W-%WA*9LIf@MtN_M;lmyh!!YyQdytU2tm+QMTcus=0$m7 zelQ)%tmT*M<8D>38-ZruHT0UN=V-!?U0YyOHlZe36F0~}aLEK1?2L7b{VB(y)`SH2 zZU!uagp2XRrop(}JmpjbP6$kfnatQwQP8$jiA;hYdRvEK11k;2^XEb8%z|e6pvx@q zdCw-MOTsOuy9PVdx;=@0<+={H<;~@}QHnbZ0N~npta}{mjmvo!lrcojW*XZdA%hsP z=w>ngeE*gvbXW@D(GpgzFpFqYOXUL5hBZXZlF0TD_98!JQV~(z3!+I~RL*T@m`0YG z(R;7+XI}H#XJPnJ9@diJJ~>8FQfQxq+~;{@N&3x#pr{u)%qn+@w8?@anSCG?Kht%9 z-9MD(r}HrHl7d~_HcH~Xgw_dt3YU>(LP6zJ}8|QmYpqMrF0?;oui`d z-9P;MI2Xx*fG)K+Z3*6XD+M#6al2PvdfA-#OSF`iF z5}RYOc#uen6iM+^Rn)=V?GDmO?t2_RzEwZ@xfBX(}&T+zS#|sXddUcw_asHL( zm4N|Fum7e2w@8||n$L*wiaVWk+P|5GT>PI?TSN1kwE`6xb>4lNljML9RpTAkG@3T{ z3TILY^)ssPP&ZYh*V2#{NBOjYLSyTwZh7q=tVZS>hTx>PHLY6KC>&_1hOXgkcs4e1 z_lzvWY@!v-Y=JhMFaD&Rv?gPhG|k=EqD;C?^+1~Qu_tZ8WX(3Y4-6>FweY8!rT}SM zHgwjwC+)NDZT6CeR#g{)Gzk?a%y%`FPFQD)=V+7pq{Nk{r_U27v?*r|Yp!u<%h9QP zKZX8TCCzSWD*Saol9eHvhEtrD07}Mga#&>RgPP`B0h}#-bT0&e_ z{k7-Dj57(n2@>eJtyvoa#92IPYOLTSyF;HkG8O9^@4ff-Gu!s;(=-R4e){Q?AO7e^ zKR+(3tpXj8_wch1K6q*r`^4)~^< zYlTyWQ^vkw>epcA9$PS3AgG=e-Yw)(r|3HHI<V!;4ObQQ5Tdm;#9|WlPK7gvqBth6D4Ei; zX(4*5YK_fJPM@mf7W9InO*W#?p%kSpjnXXg22DaFQdYV5dJPGYH8gYCFt7&BW2Gje z+&!oLycapF&dtTxV9n5e8H!Mmj@zwAXB(cu-#eOz`DZ#(QB~vm^y%S#p?~6tGF46d z<-WEdaCoOLmNWuWQf(Kc)-VC8Vskp__|}BvVN57(O{$_{4ZiQ9H!YoXLPtX&V^T$o z3BVPakZFw=+6J14KJx*_j1KQ(Oqa1j==`$fQ0!W>77U9%ca zG#cmmcJ{sX=9`b7wlVnm&wu`-BT>O}QdPHbd!F}y^5ds&Kwf?2m3NTlOpqz16t>gTUH6( z1+HlqYO;MU3ofe&e_O)>3=~uLJ3gpcWC&=}txPr$wRs7hSa;FNqE!XBtMF&iXfDvJ zbm!9Vo^dWxhb(5SQ!R?X9X>;s!0H+hq&_FVw`zG)Xf(CzgHJ}7sc9qjaR6XCp6Yp8 zb|*Acf3UblUAS}wjVSDl)49;7c8VxYXui3X9-2VOV}fWUl`VtA!Bkm3413%`jvy#Q zvLI|2!cL8Dmf@(5eCk@&JiT+Fx#kI?6Y;w~+*2{tG=h9ju4W9~O>@%?w=e7~I$*lg zE2uxLf$CG?>@ukHlX}q#KJE>E?!L$q4J4+Z!xVBnj;1&HoLv8CKA`f zX9(j%q6@fa<|31#CQ8R>&&F^l8j8|5`7*0xYBIBdL-i>kMg%63wgXy)@lEsflqJZU z#pHm5OpT%++$$B7KtqZA5Jj6TL*GMdjy>3`?rm-`qBx9(8Ex&=J#;sY&T(S{Htv}W zed_xS#qKC~_0N#KGU$ZXfxKV($`=-DGoya&&1yp|Cj>L#Yt-^~uQ_1LO*xiUN}bwH zYwQsX#zpgru~GE|>+%M$hP{d?Ja0ey%F8dk@THeue&cBygFF344TcsH?s$8y?|uLK zPi@D$ZQC>7c<;TpC*2uF@+iY_VWEu~DAo|POFBnc@ymuz$q*?m#;Y#FV%>>t4L_VZ zS)=ibz7lh9$Dk8B#J-pxXBE@?fV$mtK7b8*OaGLO1FC`9c6ZWjs*9AK|7%^t?zfqK zQ}ggqib%DIJwkAC5%{QCEo&l-QzyF(*1@(W6)g-TB}0F6jU|o6>Xs6t38O-R7) zJ%&x}6@5fEuc&DHA@rTbu!uYb*BHv4#~k=JGAp=`^vL5)uqI7IHMcrozH{t3Kr;iG z&VIjB;y!R~_Su+h!vtEU&o@`Zp^;UJ&BSUsXk=JSnJLT59WNPE_JxFNb9d+w79v*z zYQ>As#L1$WK@mIZm z8XGXOqm@i&&x{Q)tLwW+h%-^WHKKc61x;?6?5y^ruuh3KxoMOzsNiw19IN2zUU4{W zmG2RNbT?JpFgryV(M+{P57l?w*|ZC2HdgI(;6fQt2> z858`te3{g{6Rr;=g&xq@c=s5b%Gisv!kk6Y&TZM0-m%oFr6Y=6`-Z{LVooc5}9Xh?N z?MpZU0uGdLUg%!g&g`=|g%4M>P@Qy)jZ%49MYFFn_+XNnMRMe*9?u*M0${E;jyi#j zbx{K)e(%k0E$SLZ8sMURu`(o)hJXV-HUo7X4RJjjoY*TDbmqfeyKkk2_~bpgS(E5! zqUE4m)Ot`FcZQ}1Ra4z5)BYA32G{Dl`AkF%2U@O-Xb(3*wzi7q64lIpPVJG3ycm$4 zJM18Yd1c%?aF`e&3gEB_Ysryj>5AoYA%ZN@)76!Ojt~thFS|K?v@oVRH#92))+!QB z%Wd}t#~^Kh7BnZ;x2?HjLo@~mpi-pl(V9VOuM2GiLr@q2R~rWEl7{Qxnu}fIgmmxC zUJYWboWzkm(8{b^!}^kQafgKX0}gFPfw&{_ilzkB@kQdVHg0gyqKPxHraXsR8Z_)l zJ{t~+*0_0j9|@xby5mZf`M?9SIe~wEywtdGY;a|A#~P5usel^3tF+uKpP1QeXeegi zEnTL zeg669Uw+z#;DZl-_Wn2o8L2RLKlfLE{q%OcRaIYo{NBUvWQPlf!Z5Zlx*#ql$)3vV z`X7uxhY4xU92VO=bl*HHM?J=0xn z1RDb)P}O!uhkbn9ADuGUi#yidGmPqj?>;t*`(72FW@!^4F7knntdBD^T{`T6Gs)m6QMZF|gh7i<=Zmu_B0hKyxSmZIGGa-c- z_P!6U=t|dCz)FihoJBVe?{H#?^RT9~q$)08?{A$QXngnMb=*OU>ph(R%=OQ-K&tgz zXIPm~;7D0Rh4s6!j*hpo`Kwz0pEU**a>$Oepey+#Sn6!EU}$1NzKi0pOOD9;3_b95 zcMFH}`#?uvq)5^x$`Q&a#?XT!(TeZgJj71>NApF8JGDq^x$Xz(Ug4ast{3g`xgj0X zj*nkxZ9LPZCkkR;r)M)tMdOV?+(NjD>l^pW7nNB{x&9%S*ZTYrHj%QX01xQJT66C` zdi?0~Gy^~W$&Y`euaEKy5&QS}Z$JL=`@i`0uYdi^XPFumSh5 z^X^D@ys8ZgWi@8V`Okax!w>`ArCLEp)o%^WCo;&8i}te>mbIyI09_P}!Pu*Ye3x|Q z+ZtVpjsK)V`tx4ZwDs!QJQ{9lcgA^FG=f#2-zw;Z+V>63Kt%wko>Y+6kiUoXbtgTG z1=|x9d(HQ$zka~?X^lSzuZ1eZ8*_Caz8j1tJY}!V{14?h)fo{|E7107f5rRo306Fp zqD3$#(&Nro1c}OCkk5;zcZB_IKN+1i%%N6H-CVQpH`i9Q8k05AsGJte7!=iDj}4>Z z&SwFJkr>(@nPBkVfKN>`2B@@KZqK?@AIkb~WN|va*4z&W$Km#fJ}B8tpl%1( zL&UpTBI4v-8XQ1fxU?eQps3_yQn*db;?+}-rs6#Cwyl&QICnPu^FXcL$O1B~=Rf8rnTH!mp=Tc~BkA^Uv zpRpu**K&5C7Orr(nAKBVt9gZA2#(=X>SY~>=;ARrkin-$>`rTko(L0ipc5Kd?{k8E z8D-%_G|0A5kAzZl#9-I1@Mw6)38`+QRVcOZ08u0~D@oY2#~m%I^Wc%f_xn_v(D0->v&CQ44kl52bu*=7Q5c_eSyBiX-3mxFp2S2uC5|DUOR^h7rHzh)?u6w&GeIM z2uKaI*O@kBTA4h%IHRbQ>GRw#sB!y13q0H(nbzJQUivEHlYQ;hSIhi;()vih<4tskn zDC%7`XufJ3isb9)i0RZk_nI5AS0{HyMd2>dy=D|JKhEYwxAKkm<_KgiE2Je$QVX$k zDo|dc7MbYc%{v+gx)>YYyjY5IhNu|@)bkLW!!5c5(_!3R10}&-1AQh34-ums(*cAS z5R5zsu}nON!4Mi;1kWAC7#VOU9A!~1s27JceM_a)pB0GI!P8o+gO5FkzBZy;gYzyR$;SQ%00qjAm z3uhs9(L+oiEB(#$LhNbW8a8^`@NuB>NOM_r6^JxBAyuP994H3i!v_M(;iOH`MNqEk z@P$+KpT#H`T-am&)eH#y%vjXRMWw+>OrX$Gy4wnfC>R#h(i&&ALj+DPw1$a_))Vw+?#!-6kR{M1s6W; zpLgGV_tAaY3@!LJy5)5>YgoGAR;a&-Y=VkQ)|=U!?xncW=`kh3V@%<)oUPHT(W&a% zs3hQxY)DohDcYVz=AvMB%sU1{a2NG7b5a-nXLC*a~Q%RcDVBLG=s0K_GO3r zLz{6DFWM*GGaaWF+gmm!%o?ul(E!Hlz1`2|3RZK2+E@S_&WoS`PwTohiDTy6s)i83 zgytZZNKVwHu0zf+-auQYFve_t#Pzc`bdwQ~Ivl18-|U=A%MFw^4t-d&xtDV9SgY== z^^lH9NH|w-sGiWMHfY=6?qeM+x?nS~_bB$*gVErL!&~e6JOO!=%YM`JXx^;*=z|ep zhQs^C)I*+p5$a;uUDrWB$jtVQF_9*v>7a?;83nA>(g@o?i2WVe{qN=AMuayl*9Hkk z6V3{_iaVXyy3-jYFh?CK=+_#S*nJ&DFt-QRD$;nqFSr;^+`w9J8ER2mw1Ly9W~8=4 z{!RmT)UL41Lz{8fRoDn#Irc<>lQ!7w6XMg|4a4*!3v->qgsy2r^>c#5SzxG{q7gTp z5vnDrT6(P__K2ZJxV)Z24unrSb?pju3grzsMVmEE;^3&B9*C%EsK6l$L=6CF_Jqrl z5|KQwR*hQF9)lpcP@wdly9a#d?RP#G{px2Q{Oo}r`9TDyQAz{YCmcyoU;q<&G6%=jV(d|30pcTcv>#1ZeSG}bipx(i;|fRjH>W+*Cv$k!iyY){uE^GKrId^K1>3d zVZzaP3d(8ewn~B2@1B}mJ7fZ2p=L#zDaA<-qfEN#)1ai%oJfZTT@5A=ed25Pil33; zgob7*D3fk<2fmDr@ANb;e4MJXxl;bMV_6_JQtEHCD6X`M-M2dQ zr6-yD;DJ44m={B-$qfzr@L3)2J!y3NKug-)v57)iqZi!`sYy%aA%HOHZEnk4_n=>C zy9{fhEjShtoe-!b(G7!qZ^C0I5;alNA$q;qoVHFBJuPctCKi5fxMNvEKHxOaq}Q1W zm#&o-Dbh6pTNlv>WH5}%-5?SB2<8&Ti)81xAREDUrn)R4x^y^$++j?aZSDomY2Ku; zfr{C0QxaIJdQ7!gZTe!x0~nN=POrhBuQ*YQ@IG!~EW$;d8a4a$bHOSVOpDwKezWi% ziE2m>*h7S%M($8iD~)ck6!Gi@-k?!9R<-d(1ywq}D-Ism(3}l8&Fh>LDMd}j+vaW| z1uPg7Xqg!5WLqaY=OqdMzDlR;^st>P2tFfSmC~1%zPQ+iiZX(OOUlvs*#zC!J|cL} zh7a1rRz=eCwM}S^Q1Nw6i#A|&PJbMMJ>Mf;BQzmLV?!MJLx{-zJUh@iv%p0nJ0diU z12rKUVpQfh(hA<5TOgDf(A|pY)G9^dT9(3X4}A5jUw;VTsk1-!egEV~Kl-b`&7ntL z@;c?UVjir&{o9{@E;p<T34wW=;IMzbns4Mk-Pk7^KHYmCDcnl03o ze8R2!)2%RzchlH}HmnUja_A%)_C^;?MC&M@$g)6$>eOb)ka3-Hu*c$1e7TO0Laz># zR46YTGGG>Jnv9bOX*PGMpv4oyqNuKyi2J!tJKq>O>5xg;0r1)i(V!U07mkk^QaPdC zp1V?ZR^(oTxN>%S7F(LT=F-Ld)Pn~Z2a-+xlQv(&Yuy7u+(k1~15J)|l6+Nn+l)qS zvw>nsT6+#=yY|(S)7hLitI<1?MIb1v@8X=n!M4CqJ`|2Qw#bQ?%3$RWt5s_}KbfIUlx zyGsbe!jELtIysb~pNP!_D=Ph1hjRb`AOJ~3K~yM@5<>q(~5T`j`Vw7d?Jvqobmw z7h4QXw9)E^Yn_G{ttcw*)%z5*XTrEf%R^NG)`nfszTMBv+>1Brnhi9x$X3$>@^fW> zR8@WW;V<6*7_Tm^rct6uqc;{_Muc8ip@0! zdP)VSy69O**651fu;OfB^T3Cn50%bT6`^=C$yb8|ROx#>^@5zzu-Ss{lp;#3yor;l zOR?D0y9P_Mcs5PvFlH6eq#A|9YvM$cas8(wOHm{fLzYF?1swZ!11AAS(72O=av>UK z>TC|3;qG~8K~tadVIUOrmiwcWkTl?dH@;hn$SGM)n6!GCH5Nz$x=p#c5~{&MBzGq@ z34zuuq%8EITnz(5v~H#v<*XX7bNuzgEC_{y%A~1LTqRYna5QDG8rL+9zec zGb%SAOwcA zQ>E!8Qpi2VvFozmay`SFp!6=UmU6p7oj3{qcyak02vJhUDh|O!Bv9?Mtm3$Tq4KNh zQ(i4ltT0Yp59S2=yBdI@yIB0|9E|u!C;A_O;k7v>C4HHTB5Lq?ML(@NfHMUcMy)%> z(7cIhnUKCX5VES?eDm#BUU=aPU-~@7|4)DVllK=?pT$q0IJRwZ(t>{SlOKKf!3cdHCZl=4V#{?4NEX!b)Nf9QF9DBqh71(nK9c?x3;E)IY9COPu2xpr={dRS!Zi!KXVe4z5`Bl&`j}Om%^im zU;SL@M^)8(?|toGy!YPM{zd(N{O64~-h8}m_!I7;N5VSG$sVd?!zCno=Tt%J6j|b| zPDBFL5>P5~0x0LeNl^pVK<1#yNMS=x^xHN)ur;pv4r*g=QU)m&OjpV3I>a+e$#=t+ z!K0_PMN&QNv${>3B?|-cd9cx);A?u|p|A1+=`-*2W~&+2!3mk2Rilxu^VTj-Ch9IG z2lQA04=6>ZBYRd<%UW<`=R87U{Z-~Pz)`1)A!ao?Xi!F>15|k>m}rbHJm-Z=+GqrD zI(A}_EzZvzJxc?U)s;-ZL$iCH%ksT2R&+Eo%T|{|4GO12mi%hdW8HPjlslPM5gt|5 zqOMNV1*UUqZ&owVa#QPld8{w+++UC~f#wJcPhL5!$n~CM&pW%LWy;^5<}Yw$@%FrD z#!(JmkM9tY7(;Wxs*yZoZhf~pLku=%Z4(da ztI5+tCgCRGcnnZfSPeEU?3QR~pFvNM?(n;Xm>EaZ-N&@Zaf%2+H=+|e4C%(9$>Ft4 zWAY}~D2)wKaP3e`xNC4QsSbZ`MhCn|ksX|^5Z>*!PB~2ygXe5q;9aIK*EW=B?_a0$ zdoLu*U2a&_Y?l4--vx=;#fv{_Soh@x9Ml==vtIbfm)U5a2RMe;AG#7VUl{01+1#Tp zv{G?qtj1`{b)i`h7Co<_LOc#QebclJA^2X7bhlN1XqTW(QF!O0BbP0hv_hZ6;8DjE z^mzFXo_Pje{_<-N>mSIU=bn4+#kb#j=gXKVZW%b+s5OJGgQBxZO^y}5U3{7DRYlk6 zsCmb^4wXuh1{@<>k7m+By<50YNIl~YB!M!KB-yD)NR1Mk9urwy3F^r_V=0qZ)xU@( zGm(fuopeogbR3a-8@dQ>C+TjCjFQUxtE^yxjDC=G&2(KG%sxt;(CE|R^Zn`mR*^Ob z%VnYXY^g_5>P%8f1r;fw+qJcx)TW3mz{P-NlV&IFrMI9bDIHAnSGIC(h|yo^>F$%e zlGaz7i0M!9ydol@&6p$^UI$EJo333oie+k0YOrP=SgLKG$#BzPcErBajK^ksX?{mR;=8K#33fRPz_X+3Y7?=`0kAlq&P_m&~V{V-+`;7d5SRz%Fm>rjJe$DEXZU+d)ozml`wiBWkH4jnl8u73y5{3YXw!6S zYz3L(kRZxNsq$%nwNt2;y_(3v`AstI>>LfTeL$LzzNM_nw6#*F(YRu;T-*|mdxGhC zYsvKO^R?YY@0&0D)9)x?y3Wc*N2ttq-S5tmT2o<1I{W%L*Zq*U0FAtz!W7$>aSnGI zPKY_6bPaTG&Bbo1t}cGxz2Jp6oD~yO1UGx3K^!NgnyX?LWuQ2Z9)0a|WqkvrJ(Tl~%<5NM^=zi{_4 zh4Er9A{YS&lE8rH7rKKm5`q`rcBJ4|6PFnV9mWT?(=eO?X_LuLEp^Cqu@z49p92A* z8waiLS=c%9<$}|D^kF}2U((G5MA!4c4^Omd5eJyfQ5IRiJz>W$ar^Cr&a$JDQqA2k z6@52`lr{pqEV<&6&_vFWij`gTI+4h!fDu^R-urqVTTGXk3(UNy;ge`&i3dk&qQHV| z5d92fzT4<-ecFxIJoHwt8g&hG3ESy!Sf`u?^Ppcay-0-oPOek+O~-J(Cn|xV@zB>p zyD_`J(B9rFVp^LN91ajVhvo_KH%^YC8?~q#O%x6JZ@QPLlX_gxCsD;1I4}yibqJwP z0d0oL)&}TfYjH$v@-d%Hw0+0+Gq!5&bJ1eeu1dLqkLix@7z}5lQbQQutD-~pt;(go zS&tPBq%^?DGW>QSy#4mW&rQGjPwmgEUw-Z3bI(2ZttTHp`Be9-32%`Bg`o>OzwSke zTjEUBob9ccPTC#m)L|_?uhW&?(TVZ%9FQ@!x1=_$5aBE4JMSD+F;Evf3!J)2>QHM@ zNhaE2#l4{e8XM~%>0o~2nqiRMQF{IhH-^R_fDNa%u=6jar|RoN3RG-_mDB2U zfUwTqiI}~p)2z7cD=BN)98{yKK@)Ccpkp++A{Hpt0-H`X0P`+i@Kp6^K zPjajiR8X`7;D`S_gu12Fz4MMbhc-cRE7=S5;mV?~`+&ko&gVcyOl`_(*;GVTn#yAd zZ%aTD9kW?f=CrRTXiXQvNXr5)Z4_&w&{Wcjf@dgDd0MYkP@|evtijUmc!2>9_OU}c zgYBKNmc_aXDQob;9t73XYwgfbsvz96NGNbmz47*>CrJHgm|=Fx6_` zak$+z+rF~rz<@%vh7NV+MGP~sd#HDc%!753@ry4$_tHx*f3ED0{}lgh+xFnmqp!c^NT439 zZc?D3Zxv4C2=4bn<7NG;LyJ&tj)&hybZb6uF(GWokV1E3+D@A3L}`p-u-NEfo3UXg zt6>hWi;sB0p{mpd zI+f38O2b)}?we>7?&#C*ghLTbEgNpi)tk*%5K{`F1OPy_KqJ_vmbkP z>%!3h0LO)}2mf~O8>8x?;5#m#{k&kBC1z_db`QI21+0f_ne8&CFi!T1+@SPD9b?Pe z*e};E(^k|9cShQ>=UQ9^I-wQN9GgC8udo#%xVJE81c^E7n%b01{q5G+HN3ulo@BEP z^nOtbv#mG`5zw_|t*P%#`egHqLVU(-!+;vrYDe$pee~7$e(NECt^NW2dGpP;A5A(h z49&0>W2+!_-OrU)fR!P7OUp1f?okY+>rf8RGMKB;L_5WHx-&8Ci{22liB?b-_5aY; zWeWp_gdZBEIj9%Iz*22gR|XY3RdE<{+8~B*BR{)0p$*Ymv7YwEb%C4}TZ2~8B&L3% z*8)ewx(|B5_+8N0CC4Bq@Ar20K@A!2!fwx`dv-kqYrhqUgU${f8m+He?{=!51w9~< zKBIOTmE8?{8@OU>w41A;>gxJ_W;C{{jZnRy3gkVTiB|Ku&8-J?o~~%)5O<=%yTvxr zrdUI!;|{b$`k@iuv|pr$7^arZ(WB6AWB@c6F$se-Ix_*;>&f-cxT9d9U7{4_d zJJL`N3epQqrJ$&GLmd*B5L;4f41(7>ozeRVm5f0yX^q+X9V$gDtZb=t8smyVF68Q* z!$a?Les{EWP~wtg;zyLES>s&%>!TSshb*I{nZWA`@v>h}-NAxnm%ap&K^NT7Vo(p| zvL<9|;lwjs(Y_nnRc+#{uF_+4yiWe7G9ug*zCM9y$~$pcjVi@h5dw z?0dZ@F%+#~jh}SP{&%6((ces(oD}mMKZ|Sn=jo6KHm$7Z#;kj%!c4z6t1wqcRBE2O zLtK7V7`|0tMvH;vXr#0+x6Fk*IA&9LXosG%eP1C5(wNvO{++32L*42|ozTRdL1)m6v>^m#Tdbx{8r)m^V6 zY^ho4_vL(FZ`P?~aM#pzYR9RW1Q{|Efkm6P$7FT;BKEXQh&ZOYbt)=Tu${+=MmZ@< zY_&Xy!ibPKokyP@PQzk)w8OGW9L{za4-v9ACNdAMJ_R;JL{Wnl;#4%7!-}-j<($x| zP7o-&YA@c>=Su^v%(-PjZAUQHQT!E;W@@k01mr}KBQ2>MWk@?-Mzb04x_fxj{KC%( zU${S<+X;~f+X{N$Cl&yR9^%)tN;nI$GYE8=svxE=4B@5j$BQtrcK+7nxsIVp;y`=! zY0ec*sX1clX^RLhOGmj36hhhLC}HP`%7o^;=Q%h|k?ZuY=CBg{*KUcD7YJ>_Ug&hw z_Iz-6B-CE7z5dqAFTC)DSLz?`A3T2Vw;uU-h>_N5G#;{{N)vj_0sYFf-`RpRDH#)z zFipqZ%}*zU+eF=*3V;iZ9-{fc;4;|n63#T=;-Hr+V+){I*I8g3ka`XU^0NQk8mc+b zH-|t#aM&}fYby2yMNX+(jiNTwvRfMUM8m^Aeit3hpJ?oRd(W9yRu^b=3r%c=W>7k- zo1)Y}sml9za?u34d#Re|vdc2^GQMK<1zQrnxv{P_`mwnVVPfS@H}T&1T5%M6#`WjFZwuliVVA2$wWe9yuVM z;CS|-y!qG@L9yv>QIc}s^{eOiu}cDF$a~M!SKO#D3&CLIE~vghr~hl^?x%afDMsT$ zWh$l&%S&0HE&chJd=o=AD$`~a-L|1PQ>40zaSb(f)WM&VsBj9)5T7zZ(IG-wVqsn? z`Q1l5j0RY>K_-M+uu51BBSqCh3@5}uzeu`Beu>f|7r__LF!_|V%osVyhrNa@m<8Nn zCLoK@6S_G}-ytcjhu~N|w=D!sKOe;FF%%%CvWQ}rWpg}6k+wX*;ZwiU*W4XAj<-f; zv%a6`RTx!2i7Q##@i7_@}x7z)pd?JA>dF zekOiM%d`NqpcjW6x*37mVj~pVFj@>ohat8srXiCzQ8G>q0&c?~G{nvAV(YZ%K3`${ zw?=_!v-aI~9EYNrB5YluoxRYtdAr`d%H=lL zOvD`AJ`+D6#LcO$ChCcVu=yxXcv=n35yyrd_FjDGm1`7rWiEqefD;)SHJG}z&vZf_ z2lSz?Xt4PhRg-S82HTLiNZwaQSDKQv=bzXBWylo4hNO1S();#bx}+)cw}L-=_SVce zsG9euG9WmZz)V}EX=ut#HU|H^Dxe;$l+T!gqWd%l&qxY3ocvA|Qj^}2=p?!6Alk0Y zLRG8r3_6NN(q5BXRLAlN3K)WVwwyP*z!=sIbeI+rmNP+vRcb{RMF#yVG+SKZE#e5@ z(j$Zn=u;ME8!RvC9GmSSmt|@=8aO90LyE-OO=mK<=JN=_%RDEeSRL~av4%GKmRQ}b zlxnx1RJP!To}b~Cf@Q@Y+Zmpzu}*VS3+r?0!f-yTGIuizr>Y1(oNvG$9Ch?jlt4sn z(sJ({4dm_n-k`x{(o~((45eYFdzzv`IMG3Jpj(`4N;nHdyNC=5HcK3g1yMJX{qFzy&5u9+_!D(VYCP}mNZjD~hY!E;#y|V*|LVW}O=we|d;a;C z-+b%QD}VDh-}_m+pJcMjTj#uG?{~{DpuJ`W^;Zf8qEqOSL&QZJL31DCg^@ffjbEDQ z8{7(d!wR`XqvMs?z319l--pnMTNrQXJ(%Rqvil2G6L{eVUS(Z%2PECgX_Y(O{jQ~3 z!D>YLuq{i(_SU+|iVspblCy9*?5H5W(ebG~BN?uL4b=)K&~y zP8wmJVYOyZp`mfRCuf&)&rVToqrc=~!{!qVt>{FM)hI^#dJxu#NZJmAmU82xvC|@y z6^0De6Zpx4W->Qqfb0>X@yra=zf;RNA|Nr2|6Zq%NGT??z@yX2LZ^I5RhmK_1Nkt{ zDDPgQ1!^7N0aut0Qm84=E$H9x(#eSZ?d?VZve0*6>8da@=iU5gAK9H^L_L%QJ`o^0No_+TD-(2(YufF-@<6nJ(XnZ__#SJuC z<9S>6Klt8v-~Y~k`1k*oUQ8ozE0Nrd16FCFaC55SJ2Ch;vr5)SujFejts3i_nMqnLCsiBjoxU$pgx3i zTTD(cQ8s<-mU&)BcVSdnz4Pc4U_*aO&xg1RtJ5_$TN9`nT*{m>It8raw07R)`L_#4 zQ3&lfT@QP>orcCne0KyY=fJQVuSDHcg9}sSptQ-n8k&HEvB|o@TjR#{U<Bd!tOA zdf0FWv^x$S>^sqi2wqV0q^2A5)3TY`I}#pZ>_(B~?0$$rON5{v=el4+cV>!dQno!r zvC|f9z!ydic|T#Kmd@0+ExxPGc+}}$bQE|-b#hL&ab+o%c^cG1+&xxnEIdinxCRF67sr#w$8O`d^H5aSEJ)NOYcMcRK_Il@r*bUV{Xg(HCw9I)7 zp*;XIFJ!%EWLmiem&HaQ2k-GLBz8{_N9X6BPGSdbD7lA}rb@pMdH zqs^uxZi>)BlZ$m7MCz|nKtOh9GrBgXN*y@n&`N!sj<)LAz?K}0(AZpAW<&H-$Dl+L zuK{sLlLpl&o3igY411vKghRdz;=&hqHr=~=UfB1q1C?-CIezl3-%$3)fqXpq_@hs9 z>S`Ql3<#f|je16a^@H#I$&bGKC;!jyk>uxnjjz1=`ojm$JlK*?yfuEhTgPsur@N(2 z+pwc-pSf;{&7H=EX_P6XihCvHyCfyt_H#M#urya?u7WJIv~Q?hNgcRyxRZBy`T9+F zA03i)N2)}FwF%!BA|wtnDeR7cEIAKt>VogDYy|WMDm4&DohE#WKG#1n$ZXW4uEBAuT`lb6 zb7v1AKI_Qx&s6QAH`L%rcOlA#)*3nV4NG9P&jze!*_vD||S-GUjH>`^+ z>im><|AOFFUu){V&lAj=E;skIMkWqp=$H{X8wwM15WHz4Wm8(UejO$d2s%;}+8&gX z+J;0C=o=$z-1O)$)Ip}XI@uMAJjCW4a~}|0FKC$@&9d|cFKSc7C^pV!Q-xC+;uE1= zR@@0HM#HDkAL{*fA+_py9s&bnP;AT{N6u# zv*>8$-MZP^aLR%J11(v+>|O3YwE=fdxUvCGgw;^U%#KJTov#0qfs*BIRG>>yq$kzO zhcR8RU`TnGOC;jDsZ}GR801CfgofqaF5^vgGm*cL*4?vDL!6f<9gd^*I>K;ei}1yD z&I=vnhE@ZdGuHqBAOJ~3K~yv&mv)|&+x8E1mStZGOZiqouc0>5&^r3+{v?pb(6Uj| zL`0o1;TFP3YSl@D+AmE_&b`7w5{^5IVMCm?MD1Y~<8_iv^}$vTw6jo|CYnkgwby9x z&vcCPbH5p*1VUvsTUgq6OWP2cPv$eWUN0z9tcHB7#x2!jVa*Ljy)h#a7^l)p_>OF1 zWj$~57)@uB_QgkC7};yvN}+eC^6*EtFBWb^tByh%5?ayK!#-s836&o7@QY2))yya0 zi8wHvVJPfG2FpGy|Dv_QM^t;f$)ZN%f>+RRM6G|+r%Uf1ii>N4G7aifR0RH%7QT1 zkOMC}=y+T-o>kCrSrAh`chxG{y2NUx0vWK+K^s0hoZ+rZM}exJqcqx<@1pd!Qu?9O z&@?cNGC}O?d~xZ|!2+s9@bnPSDDI=7#CX}UQvn&dBi8);`B6quk%=WD0k}~lSMlEp z5R?o~!Vb|IF)8Q-3H3%R&PFLfH1G~gT9eZgbx|F^_xP*->~m*-eDu*TfAa5s_jkVe z>yJPB1R>bp5H{@`hB8P*G(z2O-|t_1_gjDUmw*19Kd5_6s6YG9SKfU0F_da*gCY?@ z8nwY`jWqcJ=cH!l8{cx)<6M zpM8pNVQWgQVtj|)Xl6bBzCKfU6BSPv4w;7t-RWhX-0lV3%C-oK>Vb05E%JGwFsfyw zBD?+^HuQia&=u3}5dB?Ht+O~Qqmk7u^b)LaxzR>1ia|tOrj%c)K{i=e+l>-t1Jlf_=A_&A&x#zwZesza0v-YP)7?@h7>7ekH2 zXbakd8)0eKk$gr~?q|R1S$YFm=4;^1vzP{)|0)QIe6%)U%nN-~0H08LC2Lo>;vF5- z;u=Ier@!Zo5q~l?r`7gz85O0bj0||U+!EG}s~h+;!?lI$dmVFCQX@k-+~5t6ImR*0 zzQYg!VhoTD8J3rTnPzZ))hx5ZZ@v)g@m_!9tw*1?`S|_c{he?A`pK_8DRfDS>|AT+ z8D9vRxN|ktr%Ov!{rR8%`|oYfYH(wi^6{Oa=`{`{vOpJ!LO z=^k#*i)NgV?qiJ070HP4pVNl@EI5vQBUTKLj)GWYsR+8?+!)Rii zK?OIxprP@@171ES?Wj2J2nM9>dmDCcYJNkaqc}t~cHg5jGmJyO%q_5-RNQDHOdS9; zr^s_Zs#(qLGa^&7194BJWn)ny0bCOY180}pXPFtyu}-I;yE^Yaz3*cUi=joe&_=h4 zxif?j!vearyE!3pnWUKE=76>_5cr6)>}Q?os?>OHa5r3M(kwXF91NEod9de7VxZRr zam;64Te=#qJqxEAYrS$bxMtwQdn%*&HaHH$?V4)oE0&7p8^!$Ji!)<$^Ig+Z7 z{nTE?5ZPQpq2>^1Y?<@FNj!#Li2Cb#ZIG%ee*Pql>jMOf>Ej_J$g0@*hwM*x}57Y0xdz3S{h-Q7d=xdw7leB3q zGQ%N?)~4AAybo|lxR5-@9)xA0-rz$5)u?a=Qz8;Z&jaX&1f{3z385;~WQ)PD=qclqdy?l{yH4vcVdY$yxJ+kL4LC z4Gx!^W=1&5bvjj-snj}sYN!`hpj1^NaKaHU9}K$AiAJR};x}f>rk!#6)(_V9F33q=NsnucFQfj0z(V@lMBFA5&n3jo7R!m-X zKZu3!oVo zG%dZ+#N$qk{HhXm%wim<@x{@Y?yhsS+S7-waBw$jz49b@F8yz!k;V8df}UF#vX2-B z6Stpva2?nsT`lHtl$yA9RmP!*z5q-b5=~=tE6p;Vy@ogVDM1xh>2xuY#WlE)Vt?zC z{Q!gV`6H7e!}8b;|8PK;2?{NR64v#r_cROP`yFUPt%?6SIAyvqa14o_o^bvYc%cB% zpWJI48^na)oMq5mZchkZuVnPvT1@?0P3N`@OL+@NR_(1 zhVF=SI_crYRFaKhMf?;j*4;E{YmRMiw&#VeJBGC_TqtOJ8Fp~4&zF6Bt7!qs$}G@} zaSYS)mIy0rmVt`a6r?0yNCRt&Qsnt)mvr>ueC3A3U`;j;8mJ&KYYk%v%stLaK{b`H zhqd=e{~W2dtH-2X_O0m_Wb*mW!>>R3jWi#$MYpI85%zGRl_#3uma%sipyU;pvpH=t z_$Pn-dw&jWc<13)|I4R&{cn8b?Z@BwV}5s{Pj5nPisap}+g7_;y8CNg9)B`O8A3S` zjxk8bs+gxK{SdS6sTvwv&6%>(j?3;Y`*$)A)PN5$A_VC4fT57qgf3kq^~n~g{7IN_WQ_((dRzXhDO#X$u^uh-mjW} zFxI=rce&^H#>i8;iOLzldr8Y}p2TrLA*ni<6mNxhIB@2`onbf~rX7B{vERcb=syrK z42fiFEC9UmFGx1marUnIm}F$Wgr&CH<{wcCjj@21cXh^5$Uh@~hgG*A!sfH=R38`t z!$ZtEipvPQ!78Gv(%sr8^JjqIKH#qV0ADfCi;0{(Q8)Go)#(DIz81Z3WGmZ;%R=Gn z_8PJo<3JyqIHXB4>*&(RS;t)9&J>i-3O5o@gBvuA%!k38g2`#R)Lt{2nW#JzA(xZKN+~TP_=Yzm?)J0HpOrOu z^Byng8rS=4?~71hiCDs}{VfzWI1FvCUCjiD_Fe1at;0M2>>b86txCM^CXi1SnqN3$ zE==RK(2M1PTU!&KzN)#$wFZ&HSp;beC8L76POLeQRfEE-Uw-4MvOj+H(T8XAfx7ar zmGc(qK32o`cwAC`LASgJt_$cp-~Qb{!?rzm`<=(X{r7zj&prRb%Wu5#@Jm1b(f2>- zulH!=l=i8f`5s6*6?IN^^o$s{(e|W+6~L!=n%{xUeIWN5!}6LX??rW(!h zZ*CY<6*lb}lWI&R@M*!dx_34?gOPoXL!6SwR3lQ8mc{2#XIQcjq1sjxJ(@AmWmLTt zIYar`q2J3Iw0d?zy2cO|`)eRl&1i-9q&NhHRL?gu8at<9S7>>BVz+FAD>AxtKeb_v zjzWbZrgR%b_~JpAqZG(RMO1pDxwh6`JOW^oL452pr2tms$679m?{x-sh!^(}t1MJd zhZja`)8upvOxmso# zk|m>q-@D&=aFrUV!B|wj33)T?Z zBonNjDGLj1%Yes)CJxEySPdtGj>xg8yHRV(5myvqGP$1(tMZf)ytMPwx*==tJo;x} zdGO%b7oMg8dEpB$zV_e!_y5!XSpS*-dHdb3J^u0g-~XdZ8_A?UW3bnS?_~@(+jKe? z&`^vfO0K%t+j^#ILgsXKu1V@$vbfXljy-&w=Trf}&Cw06AUG zC`G9L56&Ps)6w%c6RAi9MKwHuQfonX_jzbh=VvwH4qh}<96yj0IddITEn|W2eM zq&YiPRlC*Lb2jLA6=bTvc>d17U|wnG>*!jMKA^imEh_2TT+Dr-FG+BP(OoXib1~J(EVa!YI$1toJtaFOq zY~rfg2xlOr^@&D&VtKJ}LSP0uL2FWh_Bl?C?A>PHv$Tkx8=bs|_L7;q8qJdm2svU1 zC>l-Vc&tt7fy97N<)x#)zYp(&l2I~KIMWaXHD8#cC;PO%fe{Vtd+R>x%0QLD1*$o$ zqH61lEy%0?Uee|+5Fklff9svczme>Z|D6B4`q~>0A8gNj>(l)cGKhEU0*oRuAYhhf z%I5~EY4CF_4rA* zj`y(8NH+*zMFFgl4uhrkGKmm`MM90Wp=KaXiPUZ9u#T-dC~>pmFHu zRu?Ih{qJf_4Rs!&h_6WtL&8AmS`eoO31t!2(@wX%DD)yEFJ5Og1YsNhdp9YVJ~t09 z%M66D_qzxMtO#bxg24st9=6wQ>ewcTJ$#6RnAnl1-UQN|Kh!g=d65sV7cEdT(?@-> z$Ki9BilwF%LAAXzJs%r#+4W>Ke(qY_(`-YhW~6_2S8IEZ=h&-iPV2@!!-DC2!DXW| zr8@7iHs`h9_kQ<$$Iyg5|NIvpy!0E${`k-R&oj?Fcmh}D1+INO3h7B8WO5!FO-VCMT9%&1kA%Z42^h0 zcGWGDHQvFOdo1oy`ViL7g$P^35R|HCKuy7eVp*SADLo&97dc&J_w;FCqvweJk zGSle$xEQ?Us$^0%BEyJrPQuMr`Z<-T?R@8eQ=lE2bDvvdbBXQ49vJQ3iT#sdkb)W? znmMB1tr~i(W;hI;5z0fWVXvXQ-VmL#2N*l!08|V7$mA%_L4+6>6^AaCUACfD-*%i$ z(E>@gv%Dht+1!&*8|!NI>eMiM*mgDTOn4QXpZ5byT7lf?&8BY`3%7E=wLC~UH3{r$ zD1vk02D+EzQ2orz5v&4IRd*N{ZB05T3&j&FJ(?&>MRTx((Y7g>2T~1$KRt|K z)Us{FUaqu6%KPoKx+x5FL>y{D(=Z~SxH7v?xLRTD;b$4zAr4JQ-Gf+mqUMYOc4|m9 zGulbM zj3599YT|lQ`L5}DBQ)yiFhU8(t;^qwwY-pu;}hF!lP{Vlo$7f1VsU@5c()3>`k3-( z>;j@`J`sD#NMdUa(Wziyzoo#|2i8zO=H6&1T|3?IwdJ!NUft@ud^)>m@wa$zcl4Cq$&=v;ZQMq(1whya3ftY1 z+yI0)u&#Y{>$N=Kkk)CfJJyq`ypS0gVDYY`D1;MZS7_xjS(Ek zdnVB(yw^#AbMcv&EsiwYCQ{Qz52wv$D-!fIqv^Zs+ceJf`S`e`%qpAuM)e zLyka2j=c`qV?(*ixuLZKIT~uJlNezWPhzEtPvjMY@y!?g_P7wPaP31Uh>ERAF0tz3UWWIHzg|a2)uQ6q7VWBzIrMx!;o!ytziO;@x-y?3uzrneY{T0-$&m}6YSikLH2 zaaEQTFumZS8FuY2)CEeQw55SWql;HtmAEj_nFyFL8q+d((PkB03=Tx59L_8oMnbt3 z6!r=Y4`7VGHsC$a;v1A1l)QG(r(`P6FtDw zW;=h|s8%gkK)to;cDVv0v)mi~s|uF4*)aCGbn`$hpaBOqxT%kX<2U|>dqZAmhx zhSA}=D6`CuxB=AOeF@R_GyQd%YTAUw4%WSI5YmD-Err-T(K*tP)Vtk2drM~Dm)H5p z+h)0#)swRi=z(#xDUyGu)8yA*|9rvIT}DtNUCW_)Z$9m{%Nl;=)i+=K!WUoq@;|m7 z$3DRJ=&Qf|t~Z=sGeHo7D+=ER)>hMSqTfkL$H}~kD{F0Ei#5>eIt{b`b1&RjN%;s` zaZrg^{rs{#E-dQHygoZ^l|tb$QyC~_GqRtQVs=3V^Lut11>~}Lz zLElnNmLr8N;k>1z(`t#jMHKK7sb6)P=cuq5CAIT{Qg0tk7w2r4O^bGIB6LE&61t~| z`Q$Ll;?;VB^r=3bv#E=%kW&VFcuF=nmQRp6k-8KxfQ=sHGTKjCYTEoIsOj;n@`Tazp7@Co@kV!7C7Oq>02&WYF4{QI*!jgr(}*!XRcKcT{Q_e>fKD$Y z!(E~`22N?DaX{U?fzC6nILM;NM(BM~2L(%db_gHVNCt{hKg?Gvq&hdxH|WTd9I z&1w@nMw5K?XZLW?szEDqJpX>(C?kTnRP<>2x*ni-a(-hFk{pFaM_w!{Ct z_WE0oOA2NCteJ&K1z-Q5(XwemY)~~6-ZeC(O(PM7L>53b4D50ej+!R>s{q|<2pLmE zcN#E`+7H)mv;kG^K_Lklq?6T@eHLD6YQY180jZcA@4GC(ok z^VCdci{l{sEa__{T5ucpfC)DdN91E3eSPeG4R zId`&>$FQ_uMn}mgGIEm$5;BRL$??o|>%}EBRFPj9ViJ6DI0qP>7IcDjXznPQBwmr4 zE{Fk!3#2uHevof+v0RBaGnMx4d3G=O5NJ+v9Hqlw3C4RnNLF&~TpG?hg&J_8G8~t#jAw`EU?F$Rvw&kOS3!X+R$?>-f+YV?-lL#*oY@&F2z-!J4*YrWS z;D|;5b^#4Yp?v2EVVzQ>pb56w@SqnRXax~HP*Q&{O$m_5i#IA5=wzLi)(bG$_XoexrKu^XPHZWk})z{v9SpWFP{yhJ}7hie(E04ZZLhH-v z-P8oz`bP|%^o|K9TcVpkw2K|X`Bixz_}OYK0C*6tC(DP<>a>t%FA(Ww54wL6EmYE) z+WV#f1XTMdCK6stD2;j9q-H^?!sO&lpXb`xtPSGjx(L22AD*Ph+=a=)i$~s{<@HPD z;2?`L_Pc)OH^+L~=d2|3Y5kOiwq2kMmqWSF)E9LWaT(&|z_KolH!hI|cCr8c=#URe zInw8x@Z1ed=pL7FKh0$MAdKZ7yfi{subNOvZ`xRxNP!~5rS{={DQ=-#;s$#yx= zY&@$Ku>FfS`(B~goCtxu7j>3cKgU+s9r^?qpFfH#;ZnBG0hA(pP287NWKPDVm_|&s z?b1Ssp9DQ=^rG(ivVJy-VJ-^OLPi#$*U+k*8pdngu-2<3rE5dTWwf{{ZH_V3=RilG zOhn0!G7bAxHx&|!$8FkwH21t}T(o(n6k9z`8d8Y0f?k?DEyxfJD9oW|HLN8CJPl@E z>#nY*aVSrfHh=xyC>6HP7|12m5m3$w=KFwXYBb?DQBJ*-)@y$A;Xi%j!GmYN@Q*D= z|MTWMUwfES%I5Ib;n)~VP>{!7z*gNQF++sR8-#+Q0}HYT>B-<>6WXj&lQ%`9jY_+( zyFr30EB2zDVdwAF8VFGzpV61?&GD|%5tA|ep`IlRO|r&CCaA}fXG~+2_5pL~$~%si zw)vVzk%-1QG=em0&f^VKU6}(h;V0z3q0!5#q26_h;fe0T*U=mJ8FQQ^gV7K5ev!Kh z11*n(VTLxT+Q9avY0l1SbjG|($KsP04xnswRZLU%pwn)nE8@_oHBFPE_7f^Kql(@W z?uInfh%VG%fU?{Du{Gr68nNv3JDx+;D@4_rWL{~W_PTbhgBu;e0$x5a;^4MbW$!7Pp4- zbR|1ne>Blj!weX-quZIl2zOh7NxHA;OHI&NBQ#(Qp4C5Hm+*RvSWP(9aZaa@n2dv= z?nYn$03ZNKL_t)_9W&rXrywAwv&^aMffWH^9WZ@v4q&n-TG^6^JM`-5-(@Bil4 zPd@sj#0a6ANXW*~%qg-fYpLGvh&@zu%#G}o|Kh*-AO6LcUw`XspXYtO`r4b1o_Xe( zZ||Re>V7Ej`P-CYPhHa$sm1L13`dcz|LcYmJd14H!VPeS*rBrrLBG~X3*WZA&IlT% z>qvpCtk9F`;!x=B&`OVUYCI zD22=%M_oN*;AsJUdCS#NM&AL^k zb1?X;5jKUsD+kzGe9_g<9yQQJWC8}Ch;5FiHy$?HXie>0DbKs~MX)bJ5HXoxs&%tF z3DHmhqsrXY)dm;86>_SWhnGyl+ax<_OB|;#Hi?DWPnS%`Sv3?xCU_?8^D~+UDPG*Q zY_*kcp!q%G_NAWk6mNL5r=*@CG?=~_QiKko;_w39bfR(E_41}4yo!dZ@Ly3irnyU{ z@4Zvx);jZjWmFKQjaIBjd7*lqKy+m%3e7IsfA+aAJb3BlSKt0TMfxYde)7w2{r5){=?t-qhJ2wgTMYf@9mjq9z6fn z!{7dj8Fz?<^v1F^sb}UU1)QBdY-U00{UMQ158pGh5hy)nm!T57JET%T^URUv#?34^ zPe1kb8k)N@Os3hQo+E}Dvn;o#$bJuqb-kso=Tsa?e|A$1NI{pBPmDB}Bjhen7cK9S z?lnp=ovJZsDl<^@oNghS z!+nD4)In6!*Cb^y^X4W*Xua3nkvC!bI9fB)+|Ct>pl*>D`bfIb3>S;1*^8IfGfTd3 zXJMY6r0KWJ@E&H3Ui#Vf19|DviyW{wIBJ${^*>_W-1V7UEPVuc9?Mb&ew7(l%XL6x0aK2jx60aV4apuW0A_bV;HjD+nC&iP-kCQ8n1_ zzzXVVqsK+^wGRsE2*(tZW*QlF4U9a6&~dE^@$U519P*`B)*y!6z8ep>t4bXz($$bY z&?1u}!#OdV!_d;G=H)c$bn;ZHm#ty{u4(!?_Im@c!5tY|9zYs=M!+KEaU``p13FP* zC?FKTV{l1Fld6KIjulK=hC>7@Dima!K&M+MeT{B;e-8SV!Z0kSuj$1Qp@9OIbVa?( zqJ-_&HnSiWV#{vpdt-I#Y7K*w+nKVhC*5sQJ~;@Smbl!_k`*Q-_sIon5a@zYj4IN< zHYg{N40@B)^A>2$Qg=sS({<>S#q5Els_~1nF1c=yfTC{07Kh7N1cXxrWlEa*61Yw3 zn$~u%=2kt)b2S%(Fj6O*IH*pD314t7G5EOKl$k^EtXv4mWf3igPu8PgW_cQ?ZTkJ* zBx^&R;_&HWRSvfPY%Uz0f*Zw0sq5J8`Wbeiw(1`Ib@%s`6IoPueOc>nU(+=Dcj*X# z7wWx`?yQ;;`kLyrCbTn!wdNwGqQ)Do3+O!DuD!WUWJD0Ynso-fu;Q|iX*C(&o3A3h zh*Z;LcH6AMnU7&Kz+uh#VZ9aW^*0|qHT{a@{%7C${onoD_y6P1&n7>uu<1zIxaU%s zdpOzM*4}ZUoEH(&j=Nec)Nvkt^2x70`Su_F>wo*{r=NWMwD0q!mtTGBg)hGRoEII= z0dKB1i0@Bml}-DHJ-{1pUY!Clak-V^Ngr^&dV>IA6g3&QYYoj)7cH3WeUn^6uu0|kQ~LsQIicx-yz0bQ_ZB!f9%!DUO^JSA zh&&h$HolPHN!{Dp*e4|}t$&XUA_kkn@6;98sYuS`U}pP;K@B`Whhs1qb=_IR-PX^_ zXj|`jlN;D4)j=LhBx6n$W)z~y8e>1?xP&@YgF{9Qe%Z*5m@&1`Lt>u1+Lu{>7Z3Mc zHb)m4O&;Z4q$T#l%)A1h*p+Z9i8W1S+LCe!kn~V7SCVFhs&8OF^$R$IWCUTtL8{G_H!9Chq3knxqr9?rmBad_SKj>M z3txEgwWlr2zyBBC{=*-A|Bv6-i#?N+#v~SQ7U48)x|19_bdEtFZAE!9ciBT}-C1m}QE<#Kq418h{ku(!5W|k?lf=!!88z%1EM%MBS@(lI!=LmiyXc-x1R}ZCli~TKV#bh6dqC4^mUweRnss4Y4f8){;f?1m;F z9*ZvY%RwF$$tia;99$dgN~;n`MrMBGP%#JM)+3n)YVS-Sqm;3mI^<(x4_acJ9hDg< z!&!ftU4$XgV90X*%x#|&dc3eVoH3N&Qrp3*fUfch%j{dxtS@V@q!YnbURYRg=9OP_ znePj_#SDlJJgttI7qWoR493&fVzYne3tJL-V4b{l5D8JCEBW&xSSCZP86&JfP;-o0W3X?_k;``g0Fx=%FKIT0+Bl z@H&j~#}_elOvNYraidpRP3l^vllEGR?tB`*AS~cf5^n4NU5i>f(-;=-9P(34_#XN- zO}Z9+mbs3xLgYo!3gmmPh%HD1kkswLqXHmJh74HkJVc@gWj67K>NK~)nc7|XyGA_N z9Xa7eV|h@kFLXv3X+VUdDVJ;7IoOHt<5eH*f?`j@A$mP-niZP43?jsMb`?`itv3`T zhb$PSBKm7W*wLQHJK`=n_9=DX3D*!M#aO=2wMBb$#Dv&17J-;ZsctrhaT={Lf(Ck4 zVzAw1Aec;zY)8&vL)v3Mh6UY3acF@sU<(yW`^fa1#y#n|-0)l2c zu}oUE%UE4NN)t)7P~Q8LI1(^91~Uywo$Ym|O>c)8tcoMMu~Q?Xx(G)R^XI`nXNj zO}{kwn`3CUIqX55gu*Ffy#UYmR-u#XNMeb$g<-|m0>a>rhhYMT$(Ay0G&#{D{V+cp zH>KMSXr|V_>Kvr>(oC_o^w^2U0A5g0`x&HG%ROhMTTs?;o*5!~eD)jamDk>Sss`j| zKmD7({O-5@^`BUSAK%NENs)n5S`3+`Dm>>Uf^0i2LbV`5*NSG~`+xQaKlt(AeE&O7 z^PXS$!i%rI^5usw8FGNGD^Om&QO$D7WqC|-E}Ch8BJXoh7mSuB)9N7rxV!KLttk27 zM&V@R&U@f(P(Yd^Qw02it*Kh~i&$QlbMnosqQMVNBU3%c-Fx4S>Z{`aEUq1eVW6Z+ zA7|Kkuk(z@U`ftxqFO_k_TvWyt$r}*>j!7 zDG;ij-`Oi9FpW9r;luPWWfy7p^TuuW8Q*bO5vLQS4a%x%f{?uaL^_%IOoD7iECI}A zX)3CFJXIg0?!$mpY0XL7ZVMiEVc##@5?g!eg&Rz)O#5piwua{FFC5UIMW6l_h z-D4+?$cbV^DJsQ?KqF>_5I;HZ+6h@WAa|o2N#Ng+Ct@ z#y@|48#w-{AyAuOM`F;r)tq`_qG??sHw*!HLUohZb_$KWuwb$3>i0qrbsow!LB!GF zqo)Hi3vX$-%IGsgaTiUU6;Ba3meclp4?{3UySPu+*fYI8@@5XW9nrwSX$+@TXJotB zxoNr)cduw}ZIeK@!Nl>MqAmvjdgy8pkvo8i8`8MP8LSDEn>s|1qy;CYHRq_}@wsjW z`TBGXB~54QT^h<60`ywbQQJ@K0}*xH$v{HW2r(i44%N4CN2aUM-Os-9_HVuZ?6c2( z@$XxR|MC|<`ca!E#B=F(=M?EzojKFBs|XpJ6V7F z@Bh^w|NLh^{{GW6A#XnXtw+aeAcvIZ)Xvoupv#K^1)X-g<+?2$2GR@#v>u?lPi z{iwSXCHi0C$Yvkaefl>e8HnkliF8sFS}}Rx@PZp!u{WM@q8_wDw&~ma zcUy3qYbB}%@`J{`(&Quj|C24L-!=8)~q$D_IO>3bM^PlG+?qKp4<=4 zrG(Cf%!}QZo;7$q9kqLUS4|hp6f#69NNe~EI3RkwK)^I56>Yj`qBu)D=0GW#Q(;G| zy(#^;I%07DU|^;_BE--f8%y6i>)H0Papp*S3~2iZCzFs%Go3qm(w&yUm=;xt`KRJ5 z)6gMYZro#Wm^3__%2u^Wu{(nth)kz$Azj2OIl!F`*{vHrmG>*}eB<%ow*Y_g@kc-V z_V548-`n?3SW!%r1g%cguSyPkG=6C47+c{s9%6%AS{6jAHDvAeAO7%n{_s~HefZPA zHx28xw;ll-Do`8`le^OJB{ei1q@*AUhpeuLNJ`S6gGxTP7EytEw{^`$U*_OCW9yhR z``|Fc@TOZegu9>6=z9ml{$dO(Dc0zBcWNMifmpd=$FlctNIBdBy9TqCEX?gQV!7pR z<}IoA=D2$_uvitrI$V@)1?2%Vr#`|E(?A=u$wM}V!UPz^;~UcSVG3I;cly&iPU{_Z z>V37MTn9JWG-Yiu8TZK9tJe5|R=QJWjd~d~;u*{%8^vwVVOG$kbb8Mc-2-~B-0kE2 zShW847D~-JGU|L#m4SdA)l{ujSpU--DMnO8XQ@liU=4LZJc@m$CC zMhWjD@Bdv%KRYQ}yi7l;Ax`SzZ9^b6*kyz)&oflW%IaG{l>|9XPwk;l+9aYfG6+M= zP5HcEpE+KmUSQ~!#nqpG4%VsmzuERo|7~u??p+oA+eZd9U9YTRh|t!F1}${Y})mEVXWr-dq-J$u@afV zc=35%J!*e95mtG*!h6t`{{0I0JgjM9ub&b#R>M2+Ffa|w!{s-1o37BtN8vtCc#vs1@|h>+YJwDWEuj~e2+b}Cf|$4rR4*o(a4eDqJY zodX&~OTYWZ$05@%yDTjh^K;v6qk*B*C(kJvQ@JVVc{OZ_etiYU<9Z}xGnH&#umvQa zp}H8&W7^WNmwguiYiQ3Il82Ja!dMSJ1ks?mqn1MkaaPN(JlO|Ig}v{zt$R-L_7zlJ zoPH91Ti5ZF%wWWxoqBKmiMaW$KeM{HgH-bg(T~KIIhQ=dNFWO50SdFMyk0={FHbvA z{V;4lUR6c4Eh4Vw)?yP z8SgS_NoPN>Ve@e-OOEH=d1W9?|0#y`IuKMl~6OBjvh0g1@-=E=oF>j4`7jKy?uW8yB zg|j3x`+fga8L8jA`b>7pG*99zQ$#JftXPXmS1s8QcX*zOO7?!*khiV8_Y_-NajerF zhHKa`>^4U;9w5(Xoc2gcRaL*J;1Dm#~*wSW1blS;$KtOPN1|ptEi{$|F=y z*=uXV80-8=1O^K+_MMh%zzbI{jaZ3>KkoL2tO;h0LSZK69lV+mv{Igvx5y ze(DqnK+IxwD8(_m$OZSil)roH+`C-X2n=PO<@1ib&N_XS3G1cp_`O&=k|PwBUb9vZ zLyUPrGt6KW!acC8duNiqhGG$DADFrc8z~$l1;tJ-#Yuy&%t%vWp$r^N+`nP)Y?l?+bYpVKXsomN2z0aj%1z7`S%oWz~c|?`>a0wu$IpD$-v*g^R3eF}gQ$5FRNo|R;e<)KZF1#qtQGAMJ-T|E;8TY;Kh{QQ! z&S45M$AIB&XHq4ntu?RIRCU~DbXe)2&%yM3*&LVquJKatrI#j9E>I!rwCLsddAen4gI~#bPrYBMj@St={9qJ z8YaDZw$J+9^PljNn07{iieAaF3|Ze+lySLD0mmy2Gg}(p)3sKm$LfW)-(?lDpWEp8jL`zYL*Vk`k&!2VQcTCD6 zVy60w_S;wve;KZ9Ek0&9T+u3>g*ME(_qd+=Ta%V)-i+Uir5r=xU5M%KPvL>r)O$$5 z&0*^=>*sr?Vp$RWUz;9I2W^dZGLReuE=tjq_DoodXh!wT6?fm(sS+Akf0ttu#`loe z#SguXQE?)VPi2Q8vLm8NeZ$ZZH~fzJIf9z|>-n#bR1}|T3h(*%QG$&a6xesKddvky z-O;fdh%l?+W9D-+%3Nc-qzW2^zbP|zmc8x2`WZA3CRA9QUZ;IsryLFMN+9(ye7W%5g4y zJoF@9LJ(K3&2Ljj?{kzQl5amPcCt)@+SIdDN>lOp$pzQ>j`?P8h=Zg$>mB0FC>Nwo z>9iY)$8N~xyI@RR#O9vaZISoFIH#x>zK{z450WR`q~eVRr_)&mvOSh0pMwUxqJUQX z$)A6gCKfME9k3};`G3$y=Y{gNHHE0N0)PD5ZCy{1IlU^(ze84xD;YHW;e?rh*QPj% z(sEaXxzV9MN9@ky=!Ioa?S(vJi0EsTJK_p$?`SMQ#ya5Z5QIfV;@V`ypmBskNvT7( zZyWJ>sCNGM=RPt1@iX*?#bYa7UiVXoZR)$c^7A`IkH1*I;QZ6joV@fMcH=t1s6DNP ztxXTJlVhI=zXdAQMMQX`S{RE>(J4+61ij)VnRD4FUrIs5G?@x#Li-Xy1LVq(f6sZN z*UBVbxKlsq$oD*NMzNgy!~>r{w1Caw@VQdW9pSDr^`gH$0i$>cT8b^e?}Z%&`O>K= zvT=h?EPNQtCmGX^#^V(mru zg(dHrqZQ?|WP3ag9H>bt*$5)GViTEyi;LuO1W6l1!>kZHDb5Sum@+9PgF>h46t|!p z@4+l_m5>622FDfMQr;&5nYeoe=0*OTkl1GEhH`#md2ITKDQXO}Xs-$Ty$E=o!FMU6 z>XZ%QNaFWNlHc(EZ#adjeCB|lE@`k%%I}lTIr!>-u zA_A9pf11xKa?`X(b_(B1=~;$|HQ?Yr3r;F$Hd~a64a-b0sYk!P8Z4> zf)Ms8By(#9Su~wp2iXJO4Q+cv3iD4t`Dee{Mxa|{yimwdi01gdnt45oDhfi@)MumW z%6d)mXHuh@^p)e(7&aE7iCP19Ko8G)!_2NkU21K_*0qfh4hBB(S8(*6qOgOg57Wsy1szIgNut>0Dq_ zhfM$Gm&Pd!8OncciXiY}YJnh)n@k?=B>gsl*HTVOKL{^V8%)BqPgWf8qAgT}BskdA z4M#!cG{ovLL{LO)ZsE%V9eT>~;c}0_^~@XvrGe4V65|T5kM{X)ahf@WVIighXK(7~ zkAcA~wt-a6Y)owGvZ3#pqE1o#{iAX8%sO_{7wC&NT6almVBTc8Rb+alp=6k z^2`ijei~B8(B;g^&C98|B9_bbe3qd7FkFPZ+1si`hLU#by+HMbRcz|}|DGh~y3}QG zz-o?yLXwEtZzK7U0nHt_sXl{6>+`{g4sP6rbZ@?t+WW3WwBH%%??)TB8B=ik}9iv>& zrtvn1MHUQX`&~EJRw&P&-E25kYN*tM{d%l5&Sl+qbHW!2XrqzqP(+3SjwIaL?DP8> zKP%o8dLg3T%cqEsjo>|c$IpVGV!=c3LL1dP=HC@}X{UB4gj$?F{Ny|3S6|>U%LApN zoN1>~T*6-Kxgd2cfy*^Bip=x1$N_PwVWG6w!WA*!LCXn|DAg%U<+!P2U-b^nP=gC1y3!i8IHM&qT1D8Hw1+#8=St2ZMs_``C+$dN!(Gsf3qrY1A z{$JdBrIfnnJuVLZ!4FQPE@?(w#BA0VaN!FuXjEQ6Z#2SFZ@WLv(0@QbVD0Ftk{gMDiwdhl)WOuMh7T%SlbZc9!?r zbbZ=*B0J=sj3X3udO03e2XAH>|MYTX=`BAG63tot3^6`32j2^Nr&Q^I*%ZWCi&!ia zvNZe%G&}~{J?(!pok)})MuWWU9)>Z~WfoW!X|#=5n&TJ@zS71LdFjTIa)&lH@(rX> zFg`txIrW|0cXG{k{-#JWBoXqzbqG1%U>dNz@EA>4;%fh#V&CFM=%xhg!i<}zxIieK zK!k;@)n|srY-kmi*IE3|(WuBpdw!>R@Tu;YfA=(O`OrV439)CSUtUdA>!biz zChy6kFBZe0Y~3GVUQE9`jZrwjz>qc#g&ykRvY^@v{oIrr_-r={Z2>aXH_O3YYUP@2 z8r8%Yg%ud(C=BV1ggVtc=iY6`oQibNKxkn+OEU(^pOHw@aB73BTNWZG3DI-i)$X6s z5Chi*(I_2>A{GkxhdZtTmyA6`FDw$Cb&gV5+Sl(DMp-z>4?SsIha?NgAiHBK?LM?(1EP6pD7x# z@W3C`L(T_698F?&5Kb&+*~_MaTGrL{0Ls`%D@`L1*=duEgR+|W#MP-lj5~~ymd;E< z1+9Ru`J;Q|ti=--%5i>nkJEagt{`b}NlVG*k1;FB^~*v z02~x`VP70UIjb?$2Jz?4a(TU>JNENQ&iV%JHW9jZG6FC?Z~O*1NnM39Mu*MrWN58}Nlnq% z&=cH@W8iE6ZG7~<(`-fP*ApmbJ*VF%`OiQxgvfQ8+owWDLm>1;qEINenXtW5P{e>y zJKUdz{=J7sO#P<%Hkm4d#s4R~SY{zIVtL9dY(@%NH9*M<^}Uh8EE7=;LQ`SYW?vBe z03e4UixO9xo@GLb_#QOy@D#?7DF#D`#|d||d%2YTT`qP#Af+L@DV^i!m~J5DqYlHA z{EqApniG4Eabkes#x!1xo*yLRSGpN0q|oE@nZkWU?SH9L7Mmmr{7OW@gma@%2$!nu zQWOzbT$R;>eqgx`+FYFpp-ksSX%mK?3Mzxp+Ds1((Xo{qsWuKWv4juN6WYFN^0sy6`c!lKo!f>R1Y zh}?fuG`(~H09;USwAq7_(M%mmKZ3B!eVQRnx73D2)6RK4_R}fM5zX<2*v;fb1DP&t zlC>cuYHPaSrPtBz$lYi|13ecHEiXi-*9~hA$P||Cn1^xNs|9=}`GQFSz4yZ>PpL9;+2PW(1~+H5Y?e1_ zs`;`$>({B%2>Z;YwM31R(00(ZFEY5;@8*C>wNI(Y=0)Cr)~FZdBzAk{aAvgoW5N+t z(E*E;c|+qzD43yuV-H)Kg6BwZMq6mop=%GNww&t_b()UDD2lu3K>}4n2gF22N$6iO zsd1Vz3(DX0Ks$PcKV1eB*tHS-%V1_D8dn^~vJ$~o; z0u%`dq(P>G!=Aoc9Q_L_c|_(&_aC9bSc8C??j0n8@a{h@kgM<3&jgv)k5Ftj_$^ol z%VstAPZQ3{L|>aebgIG$%A#C)f}A9Mu1&bU(SYJKK-s_{U57v^Rs(ttx08=%&?u$t zWunnmh0ZTkSX(ELIFp)i5nA10=tB6Hz$z?3SXR6(mz($hEH1lECna8i>QGdtEmNBQaIV8spaQTj{qdwo9%!x8G5N%KkD2erU3doX^w3ODxaGTGFdP7HOOS~VDX6c zA!sD~@+32ON{}{1Av2`W@^+TIHQWZwX_Mh<74$r+4uW@zdBf;LOQm$tk`kt+rRojD zt?FrhfyM1lE?B>T;!dH{6n|5E4;5x4Sy`Li(I77w@(iL+STuLm6!!#w(wQ;QzhdME z&2-ya3l+6enAI#LHmPyZ@zMw{ZjgE|KnGP7Bo_gK#)NRIX_(Pquosx$LD<+I*FZT3 z4~-6@PhwN*m}fKF_#*D3SvdBgE;^Xw_xa-9#PogV1h*Ggc_Zj3yl>%~dCj{9%_Wp*x?{`5SA*}aXobkCR19{PwsL_~nukZzcK z6&h#4$nYp;UHs0~e0G>P1EdQQmWgH2F=oZ}GQArwyloSqzeIL#jUG0^@8$}|y=cD$ zty~Cf>YLInelC?IYgu&$4Y?Uagk_DJL?Ha8k!XZ{K_aqeTOKE})uF~tH=VcrHBEt3XE1;<#ortP{erS! z$HW7qiV`qEV z;u0nd%|h>n%4PzFP8t30h@i#=l~6x0-LCEEk4pNK&s;L^6A02+l1@CvR6bFbvML4zC)=VFrw=fhv%qW_0G+f`F(uXDpPR#e>HVb1xJ0HzP z(R1v-5y%8%@WQp}%w`Y)rK0#>K?BwOWQbVMX46^i=yQTLl{Vhx(#RTqdi?;SbQhI- zq=6IaskyrS4o+c4H;5vi(G;}g9KfP08AIDrXQYN1JqGy3M~!MdH48xKgJ^7q)fG+j zz&8Kr3E&BLJRe_#Wk7&ZrkM3J12G9=Uh!-AFtaoRSoJK(w|gHhweyVa3S7 zng$Vwh87k6JU>b3#!u6StrP;|-?FB)X)wu3M@3kV&KeJM?@68Jk5B9L&k^ORrjOce z!CT0OhY2bEm_;G)wAQWW;>YyzrE*_4WYztwySb3!R$)dHvW0Ew0>SEp1hu-54v=yA zCnL(K4@iV+T!*06uZ-WFK~NLctho}fO&Qj3vqUMr3x&sS1#d?GO6v&?letdCMR0R& zXs=13*s2v%4H)m2lN*@K%OdT2Bfrj-lB4Rc-7nncg4 z%`b-)K5X{cp#|PV`>w6jW=&8;BG7{<1WoGai1?m<^1W)-mw14QIwK}Rb{C`A4ht=H zvt@HQDa=}YpB2KEHOivnu-|V@ZiG`J9o*oB{p$dRIn()7X%3hVv7^gU_41bWb* zhC>n)ftf0Z7^ohM&449!T^&j*YSX{JpcJ^))%A2(Evt8o?|IJuCi#fKHg2D?+rqBo z&ndq_ZB-yML|JV3NDJD(c%&9y;s@iJSPFkAYl1BPgFeHFq6mw)ekst^)-jp~RT21m z7~^^?P>Xm51$V^)s3Lrv5_1=-1Whi5`$i*sa}BXsUR-i4A8u*1u!lS8MW@B?o&3TN zX)CVViQRK>Gb7i(^3U8Us$LRiX@}M!{Y+g)U_%5)D>I@;cGsPCE76_UJ9R z=#yPUM#_ADKReDu`dp%6H*FKfY{-+J9hciwegn%>Jb$kvn=(nlMQc`MeM@Pb1a@_A zG^%;s>;zRXPBBcnz=fK_>0IYFx^Xm^g}~%AexnPx!;#(XN(0MKKF07V(Mr2Q-GF0a zhojOIHu@+1yi-a*3=0@s^&>P#Sm-{q*fX4E$M{Vyd%Ltok0AugNsVqg`8EG=%bU0-ih(+GxLX}M+O}~iP|9&q%*2IVA9N7hR zCdd%+7Wtw-K$xx80q9Yj>58n{gzK;sbZ>p_G<#63QKIT&w?QTkr9qZyJ;SnAUJX{gdwYmCi5*J8$S#4qmZXX3suYt!5S6jTL%fM`DH= z>f|7{^V5)VPH+qNgJ>U1{GT8Er<1q#>&W~<6!W>I7u%)8l)*!OTsUl9>auB^*fqO+XewTSyN|_0e^j* z$9bk{^Z8OJA(BNqDHhcTo#|@wpeGXCi((axU$oaO*u8TfQsX|7XTl{p*NS1Gn&D-T zGmB2=K+~251Dhfvrr5VAk{pP9#t3ywp^peiHX?vO6?kfuov7hlan1PRc4v$$IoJQ17tbkxZw1fTzqV0 zeeE>YfW?gRnRF(@tZ3I@7MC(E1tkj&of6kh=Oc;?~HDR%WAULR)B{8LjB1IRvgm&s);cjV+Ufz|Ft-m}JewE=jg z^+VkwY<*kRSQ%~TRQfRG@gTria-dM!4?U+^3L1k-P7P=%uR-;{;vFj^(=WMs+Fg{* zEcb&_g81KC7^a&VAecKLO_(|QJvF)E*3_0HO&hyFffign9c7N=!2a=C3cqKbzTi{6zA69MKion)bPF@>%WVC)BD()z~<_h zUVF)5*)n208)eGaLZ>PDM3n|=lUIbvgA;Ipe2#0 zDSG$DhHw@@WKKN$0gaO<`e^qWG6@}BLp|9>@jMv3YhY=R7^qqm)0}K<%j_m;km~8q+qCB%DP{_jlIB!VH@%6B6QTK^+U3xv6G~~Lkr74> z3b|^);UCIxA5PUXi?NxGF?r9A(6nKa>%`P24k-U@d%UkBAD;kcxB7_!)sxq;JANtu zl!!uiv)I96c^0Dm^m(oW`#Y5wxkS=nQFg-;ui2C{o2-s&7#4^hMVpw&@N9RajV{0_ z=(beRd7;=%`iIx=*qX++RM;~?-F3toP&mF+1#97PTa!pD=<+ki{8gHfc*dBOsD;^b zUuVH0QEB1%8}4UAQE%{q55k&VEti#(jA&Ko)!%)cvNgfC%mHc&UU)1cATmG6b5UUl zBz1cM&r5-( zpUX3I9leyC8z8VdIF;iTehg~6BhXG!iTj6Qqg?-YAMhv7Pono}P;1Xxz&G}%UO|-L zH%M;K+puZQK#aL(0+DNhdgr?IK*N!U4LSk14?DRXUpYKkkiu}9!-CM){e$T$|M8&zx1?jW_QTWGbW?9t@dKVe0 z8SZ}4)kEoW*haW}YYsE&xwwdV*%(zY%sdbhpYnz zouf{6lILA`_+3}`^L(<*wLl@a*1eZQWT&jHRhRH(qgjctGh5KjtVMdl(WnXC5YY(6 zbP97)xmVbHO*iFDFE@uXS&ZR!y}b!#Lvwm4J%HgC5*8Jgf|Z)4+(&u>}SqZ=8ECf<7J1 z1Z4m0cfHZymLD)kJt%ySWsoQI&jmKqTo~+Qqbo2+1%-~%OZ3FlVv3%lc89`ZybvZH zVbpD6;~zxvTb#;kkTJVVSHm9%-niM2;tw)3Y=?;WXlP8i0p{F66 zTM-4Fb<1fxa>`IKG+6D-8Tl(|tBtsOP0NLdNu%s5&u&rYu76(WZAy=6O-E*OG#kkf zi|sNyfo4`vSt}>6y>S0BRL@rA{<=;hbmx+Dh??+yJayBIw2}kvZZZ%z8K@Ii5BO_s zYx3#k5k|p{&q?h)`?5|Y>tK0R0v+Ulo%yY!|@u| za6gfl6$w;o?|_TDQXZs4W=4B~2v}Ip)$rg7(%E*6_Zgy!;2yEaXZ*Fl{KPkt1@`Ap z6m5(H$~W|gyQMLz4CF#6xRjcD?iTh)OLbkvZ-qp8P~I#JPdBQ&;TghvKbuspjZ-%+ zIUJZ5T530X%fAmxLsqdKDF@ivLHdA0Xg$6cIj9-)_ImxkxjxrrD6~-9?s2v~#aY*5 zH=xUTaxgsvy$wk~ZN%Q5Ct`0<=t@&<8{+&zSZiOa+#FL}#N6)QEc%fRqdU;ma3IIv z_|`~DzhkAD@R`IKOW+%t(Ps~TdjQkMS8GAIB5D)*!7ZEV#WI_wZ|cFwO4VMPNunJ> z&CWK5adMv!uE+S1%WUXjXUkA%Ye{Xf(+o}_Q}!X3fyO-pAi=nqB6w zUXNndVR%m_zfKP|P1Mpn8Uh}{nKCr;j1|9|y|JR8V`D}Mg{Ve$*%?~Q{00#!&un(< zw;4}F42^B%kHMTcwiLw2F0B02HAyv828pH|NH735(D|k+jH}|wQchV=MpfJB@e@SlS>Y#RQMF(o)2EhcCGFWs$ zG$+zgLl0adcB&+D3i2d8CtRBrd-$F(fX6$0UPwVB38P>tT@;;BaUla#o!u8(8)rKA z6A6$pk4ByN& zy(pdcaK2@7xuW3nUKHXFnjY5X&8;E-LjJOWsv%JnaXENFH2o{N7&_cq-V0cR7e?+) zUd|?VK7%4`L*BR3XBLQKp=Rn(O$zgm(tu{qFf$|&Sod!u zhiT~^PtV*`cO8+VHiE1o(U3kP_=+<2XR0xoy7Fb`8h;kGeQ!#0 z?qHfnTG@is~X4Nxc8B1g#aR*2;39RC~k$5<7^3W5SlA1#H zH*H^j8{bB&?6M9QqC)!K5V>qpzhzav(NW*gc`leNUq1f^1oH(I4Rs$j~bl%(L+NtCb%`*EaVT%^T3j(5O<#FMQlfbwJ&NkaGct(wx)P}pG~lOhOD(?QSUfd zFf$ujMkXWcKGf`NtZqy{d#ZXTX{ct>-k87yA3msL9<)1$&Cf5M)u-5ffVHMY8NYSg#j;=?r33 z*ry)b&n#Bykh!JK#~#CCPi?h}Q37V@XdKnTfmRG#yK3g)B5LaRzPz(xU^Wg36JPtindv`@xR>b~DQ zG4#R1eHDtE6UWyVV|rkT&S^>#mY{Fc(itB%WZc>vpxk|!*EZQj$(sra^%K3uH{?^S zn_@WE}Fp%R}32R@uzZL5T001BWNklKxB zeZ!mhO5tTp@|(N`(_Rq^oh}qlrMJVpj(2svn|aO2Is0uSv6ug|4Mr*_ZVSoS(P%6f zrRA?ZLBr=g=ZQkAssAbJp-v_DGTl&%KEaQp)Kj>Hm-?N8EYN>lP~UaZUDG_~75gpv zQ)%&x>)~);1^=9a`77=mQ{7$BN{WiIB(ZNWB3$3VOy0#dp}EdOw#4Y^$)IZv&hu6- zAh=E?KTGkxIw^orjQHa8=53_Wnm;Cl{(=Y|Iq4A6N1um^D5xlNXwFqTQD>!># z52YwkTAF&DTT(QB);M8JSNe|M>9KdxFvLu%uxB>@i27u1JzL)p;gKDwG4@ z4tW`t$e1Y69o8f=-e^_Ye5H>)qu8ZUHgo995;$7!NYdl!YoACvVQE}-Z_z1(?}UFmWh?ZmI*Its#B>-&e};uRPxoYjs$Dl4SsF=psX( zq2Dngjd|!vJRqiA)zFYanUD%9$SM(Hp zhFmSv38y&Xm7um6z6cdsn@?(?S$vz>l+#$!Uk*dTLhGD~aDsZ5;mM_k&qRT9Q1Q)| z5PwiDJ9lh~j0c;W3w4KB>5M*wNUQu8md$W?7_$J**yDeoeJgDhvktTq^_(74NuD@r zu!yBb?LEL~_;w`nJZbQ?p+s0TVWBv;Uhn<0ZF@E)8*c-HS?c~@1cWlF!LL?95r~ex zAE?*$E%;RDaCvF_`yM1ks}Ihe)g zDg4XIP9B;Ra5Y7yQ)rY^j^LQyfAh78G;PCrAv&78X@$s~f?XQ~7nN&~C&5~E5@=u` zEs0n;pYEFBQF~y|a2U6t5{_Nd4WL z9ty!Pq56Rn&HW=H#A~cM)=LMZO9`|g(tY>)Z$G@kJ`Y=p^~?prt|5auSp^h-{OTe0 zn@J<{(Kq7Ys8GAf;?u>5XVO}C$|3;d3q48v%{#=ajkE8Nwt4m6TNiPhJDT85TE zN1)4vDKftIt)OLOTFp(Ie-YK+GM-E2w^M6~4mmWW%e1?34~1SgTe0#4Spuk4Lq#0_BRr=L z!ipZ)ptPD3x6r@trCh06-DGY*o2n~e}2i$=!^bqZz;!~+q$z9HgszBic-EL1Wa41!J@gL0u<^c1J=f98vRLpEm z#WYPO8W_9JNM8pbDm&a?PK6CR)b8s(H4T*1S?kPBk?=hlAiW%wc6QR3$(Tbh^oPnf~$5MW`O@nIs}-e$o;o%d{95L3Dt=Sr*%lDh8>~02jfJYO{X0Riz3cwlBM?Do%&b} zIn$LBNjnY1KGPmH#AbaDuxG)_CW+>DXj)AZ+|q5#$s*Ry#AkUv_dlBo82r6>?5Zjs zTaZ}@91ML}MAgz!oC41Hw^JM6r|$@>KfkaHAjH~JPok7I)i(qvvPy93R^7&mjTQ)eLreA*%RJHxM=6k*A!$)WqYjN-dYN4ra&KC~7lxoM|E@1~@dD4Qlk&Z$l-N1)&X<4R_ z6iYMVey2qkE0<`y@HLVCo<)qt&?%wB_Iqze2!mcoJ$GD}dIuh(>EwvP(D(pE-$xEj zcugCuAXT&=?6UP-y)H@P>npV@Snf)Cp}^2q*c%_G4!z{9Mz#^7&~o1VRy1-t9a_T- zDxc%c8A{KHQ!oD%b~o6XmHFg=Pba`%iY<03GlMcdaX%=!u^BCJw#{s7Nl`#9lpR{~ zdK9sm>lsEkS@3y7%chj8*~W-QwDb!3{zY3xy0tG`N#JY=-NNaYnbc8f?CgvY>YOET zk6ZClLID#^IoBBEY)8ZjIK{b!SH-bK4EOX-;ZmBxWC%74vUGD9%<~tS&@2N!j&br` z6HT!$DkN6@(5XxLmu<9DEGsmSJ@`P{WuJO-3yNl_zPyS0r{2c1^eu<047Gin;$tEd z^%~|w1eGS9#wHea_&q$8(*uB7aZZJgqhWzFSHsuNz+MGB2*A^Gfrq!TnI+J z`G#n}3(ZR_f@yEsz7K9Eta+piQGXcK8!c676)0H4%^l8Peea}4F2j3pJuB}UVt=*a z%hcn5*ukXOj#(ozOdN7jHp|7WsEHsURu3(+gO5=+wc+$Brg_yb8ZD=wT#bpQP}3+h zaqy3z2LV3ADw43meI|zVJ^WGpa#<%%2MOq3i!;dN*0vI3@(x4cEGHt0TX2ENJ(fC} zK>eI?2}Ko+JXfaoBSpr2L+ub$F1S{SqM%4Sb&9nBZV{z{st0XObSVrXv?iZy;G!XO z#;8m;pT;99nr#^6#$N2=`eDJrUDOyFs7&6@WMgISDSa*rzY!Z+rT!d!4zJ77DT0Qp zF_{KAvS?wVi-_CR87ZOt;=_N{E$tF3)SI(FpU73ta{12#G@QNrk1+ z=G}#l&Mh?>d}Vaet9!R1$7%(>A?P1v83dp2Up^do_2@;ZZ6@~zuT6Bv8|S~Vguys< zz^MtxSq9%`SpS~1=ZgbEoD7<55})g+P%IQAUUnp3~KqhZ3-q)Xp#!s-vA z7cwB{X+!OGQ+WIJ$-;3;lRYPNEcay(YURSy_9n))2u`7D*EDO=rXAsyF{m}rEWd7= z%b2A-Gc{SxUH(D4;AmkO3z#+_k*CxWQ8fnHw7PLcA@~z{1PWQrHC9Mr5t5TVPD%5k zGX)tK?^^EuGt-IKCuabYlh#9Z-E@GrN${hL9=fLbMt*ODW+=wztG zxm##S&8FQ{s>wlO#`b_8y*gl2S~Hs)&$08)Ql2N>E2pwn{SI#p;6W$3X#a#!!JyNf z?}A~eFX*slHJtM9(WH0(EHtVZHCSXk<^G|BU5B+iWAuJhCqtoX z>o7*|3A0-dG@H{desJU)X=RTw&E80kTHUL3nz4MzZQaaoWS3p~HZm1n&1iBRs~e-D z9-^Vad@DqE57eaBFl8R$punkzp9W|N-tHZ!h37Z?ws6=muw?k}Vs3xSlz*9D_j6&; zCgp@r@<~5A^IT|r0QJMX55JUfd8PGbX4+gW) zL?@;qiXCTR2p=?23PZ@)MQHFvV6-!z=7URs5fuTUfB%oMfzp6DZOe7^LF48I(90A~ z6O}#qnGowH@4|{WnfhDWEzE3SEkqZj?>ICWzg{lcLNr4-t*@=b~YTj$LPtTQo7pyiWo59Zgj;I$5|EA}J;a zN?W3KfkrDg+u}I5P@%%%&6fl_a-&4-Q81nz>jAUcxT^kukwG)#l(7dLwV*%VbwwQr zlI!pVai?+cON)yMDJ_U49`@!I=FNto-e-t3Zey>X&l*{GEQ8)7jJ2oxQrBz_gT&IB zi$>7QC;+pSSJWIC>XxbAP`Yc^E%qF>*&NpMW8QbBR}(6>QZHw`;YkLFWseCXAVm)5 zA@dc3ouKaNfOVnvt5yipjT(dIeF}f0-806>8*ih(x@N{4=s_tstJVoh6Pp-%Dm$2TQyZ`;2wi)|M0qztSEhr6LIM4KJ0zUV658>b}C{#E`WzY&nl zxaB3T2qE<6A~i3YQk~=K(xHfF?^?eI1Hmw%! z-LIy6oyWo+&WKZ0hGFW^>{*zEGyxM01R{lcyg*Fj0*GG1tmQQvD+Ui*aA~H|BpCMW zgQ8_z`i>#pr}6z7-S%cfjRlAnQchpzMiZOPYob~IYjf;^`Q0v*F_Z``!??+V>5xdz zDa(PDnlT`)NMo=v>WCIYJGidXhFx>lG1)pSb=^Vgaw4zh`J5kPU7=l%-~-RhXBt7P ze9G1Cfv7kOcMY9+F;SQ>zzN!%lGDABV^P3~E$bpRR$u#^_@zGIWC6$t$18mn zo06ZDXQ9n`2x6d~8KxOn+pI;<6|hWpXffPRYe6jB{-SD4d!^6fRgCfYvYEIXrjv{L z`kXpw>HX*Euy9y#CF@IbvBm$4C8}OG+Ztm>7jukmYHFKxA>%&W)Pkb0HaSXhxxG` z%_7b-d{Bt$f`WziNH6duYqe=Km}O8iC9)p)Uew^jOdwZk3T%(j!X=;S9O*PeE?j1P zONwHKV{AkJGcU1QTanC#>GPffpEOeYj@1$zv*s|C7$D=g!<3@!(RBgj|3=*oa7 zUOE0~W*KRkZ;FrQK!^#_#wB<6-No4jOTd^-*d$wfqarQHpreIY)0)ra7mc%0u5|ix znn1AAxv}7d3;iTfwI3si@rzW+S`j`t*nXc_crwtvC22JUa`yuIi{I+j+&UR!AzOkv z=DO2M7~8tP;%_X&EAQ)AnkFnyriM1y6Mik@h|9>bA|d%+C0OTiJYQ@lg3jzEhfg`uhHjYj9L6`pinv*_W20$_WO&Y^ ztV!5yM=SCZX%=jM58t~?7lEI{(%!gEU4nb_HZP5hAD6p-_+&waeqaCfnuUUZYntfj zI>O#Kx4Q@<{OWBVE|kLhdw4it(&iS#&x1O%wL+mm{ghg8-_7e((Q|2@?!E*p7ht^_ z+WHX9fnv}n6UVEDi}fYNd!N}2gngXv0cYxQ2rJae1#bDob_$3c_WhnDaaSo}OVg-B zrP5EtS_j_F8EpXW6w>U_;f>+Uu8-f&7ozP@F`in_(CA?%IP8dK6w4q85&Ak&{3fmO z4WZYJEEM(j$|03^Ej9QN@%BN-To|(_}dBPo3YUP{yEb zq3L0XYSewG%}==5`JSek&usK%b@((CB#-$psz4SGhRVfI=*7TBhpXVS&O&^F-?dM3 z+}Q`R#?D|jU>kSQjhV)%*GQ_FHe9E4^Z+iT9oXDGt>Su=6}DSaZg%_(dZ@nEZJcy? zM;IASO$Oi@CF_A*izy8fK^Y9d8b!tHqFeJU(U_0>yX6z7+tEUQw=&^h` zq_D2JQc9i6%-0zEy>Oiet{dS^E*C9yl0>c-XVYOCNlFBxQcTBT+nP=iYvKLc2%&Um zRU+4BnfBdg;X=NVv2aWd_zyEl>^+9hqK z>>U}g>sY3XtaR21moWcHz9M5<*wfVW*6^wbcEK6XZx+%pUQowb8q0H8^U}b!&)pO> zw(H0^7k;-2-i&Md{Ljp!ErkrSYjgY5xnO=)bGK}$|w zo(tCt8cPZ^>n6}3v?GW$`e_DnAwC%k%ibxnJ+DXDoifG8hCqZsH7w7s&Tg9ikpqov z`XVIMEVlzKnQ}FZwD{zA{r>Jx6v93#4k{>`I-H@km<4%bPc1E{DUr0)aFjqcoPk~} zRkyv`ui-;Rrx``{8Whu8bmfFSau7@;9|kfyBvV~o>DN$v{*%|WCcdW_O;#47J;ThF zmW2G?n7K_tk4BG%ERzWECWxpIUToI;(sWqBLmIxs`Oc-0LZW@y2q$v(6x0vJI+`SR zAM}YlW8h_Bfss9SLr}+cNvazfzUm82O^3D&u4hS>DUQ+0Ez2t@Un7)l(R`xo%cu7H zh$w$v8Yo>K&C_uYcgQ`KhD!g`dpPgea-88d%^b0#yuRO6Zm0j`+{9mN9>^fmH21Y* z?8^~6=C!<+o}_*z(!rSYG5p6bgio=o8%q(rR*K?yqPC@@BJ(uqX>CR6m4>gBpyyAo zdx?mr;%?Dtk{(p7)Ni-`M0tM1fouii5@MFFG*jWmcDQ{MnPE0(yT`b}iF`-zG_<09 zz=c!qG2`oHnE^#BjLNO?LQ$6{_~G4A(dyAeObwP?u-npdQ*n-imHiF02fpQ`O~ zlp;c4BiIi`BUskfO09hH&5rZ@)B%a}MjwK0VR_h4b^FsF?I(8#H_fJO0j zmHT2QgZi1*SXXNCDyM_!x%_E;rO?fBl)6BM7R>BKzgCgz&)dBhYlzz!m9##g^r-Fr z&(D0#k?B+C(MC6)LUc0tRxij;J481k4zCpO1UCxGxTSxmhNrcFTS7XT5*qZ-2?R9} z^vXY)&K^^na66<%sF?82Jmv)&V_md>0A>E72;4{z?4q9v(c_zIqV2)QEe&eX#K^SE zT1m`0Y;dz!7{?GsMeI0M-j`Nwci3Z1EcQ{!b0GMWg#J+kggr~&szkFGg-CZ@&H~{WwPBV91$=Sv8dqIWxzc~_ zg_njSi=PaIT89ZXacJ=$)&5ub$gLRQi|ZQxppmVT@O?TB%z7NnMxkf?x0AizJP`fV z|1Ry{hU!ff@711y_1)(@`7=(ZWI7z?p!*elb5wF~?vhHw(om^BG0jC>z`fbm&r8Gn zSLPB{n(3eS*b6L*K*`=Tp6ls|EnQh?9+<-Fur{>&`^kO6X;dihg?*j27NQ)os`#0O z6_J@=w^m_HoU?uOq-(6ri{j@4MqB@_hM;3FL*Iu${i9JsaSZ4~P8-Uk4uzJP=KFoe zPG_8^mL&BIF2uO*>UC|?W5GZ37A=u~9(umb9nr+#87_>a@tiVzn2iOQXY6LKG2NtE zP-fO>IZ(4^R1l9^+dHcL?}|&{3k9(?z!l|uia?sjdCoGWxcqh50o!_VK)om{6!NC`tEA)@!qZIV&+ z!S)LmM00xYT8Q6oAPx<3EHpT9FAPR|U^Ty_^s_ zuCKZ5b*@w&Zz)6*Qcq1IA_wt+0R zidqor@7V_p?0}rZ<8z)M!=@1Q&?OiQ5`+*le~IxR7trDbj${8gl3~ttjnfv0X-M6mOE2jvw!ipA2d}akU8&aXucw6Y~$$w7OEFl%aJ$*%M z_JAi{=VwYu6)A?n#B99VaHW}9efP@#mD1R3ESt&CB{UU1zc`WdlT9G35oj7Ly%CJp zOs&_=R$#dFii1uZ)gG3astQIQ7xW$IXMjlnSU{)0N{~ge9rY0BG>Vf7o9MJPA37{K z_QP4+Jya?bb&S!(g?ORR4-|3gkN>j))_2M_M}|bupgP$*&QpIYpdvVPrW1;_*;?Lg|Kn6|4;t7v#_r-*-A@V{V*&P9ufi zW>*fOj{0W`f7rm`)Z&^i#pt~k%Oi9_2=`$pL;II#;(bryp!oW`yJ5+<&K75M_SJ0d>NzVP#uEOVvkc*p5O^ zizY-mc!t}?6MVQi)5SoxH}diPzAN~1y1aW>e(1dRFu$)aKAld^UBU*k*T{sKg;K?J z!gPgX`d-?qP-v;qI5J!sWTA6ng3s5DXww4*oG6Da58B1{Q=g=>^<{Mnr9{M%E-B5G zy#7I*{&dY{A zrpufV;s3nm=b%e8`^JL5wTOv0@eifR(j%EIEOy)VcU$b)>A=w(yiE1^LV2Z988U;# z!!{jdWH{zM++ly7mF-HWCKhTO!EUw0#_3Ve_7)OywY;_)L1}X;ZL>`7ZYU+iGCs&n ziZC*Z0WP>H0u}cH+8aIZ5ZEPOz^%Pe%vL>>}1w+jvK_S+zM=Bw$1CO0}k0 zGFu}i&5<^>oj&c)7k^NLi_`_J6M{zl3<)!=a5>*Pf>ypANy&aQsli~SRyt}a*%J>U zmH~l_Ax>jGzUEgtn`ua=LaWobW~V}OGdPw3TE<_9h7gx6vk_mtrq~cMS}5)-B3F}h zT4RTWthbE?7B$w71|*#;%;is~rs>=g2fgv-5!_ccwP3qRS^nv)K%IyGZ;LC^kxMtA zK}`rM-VwUZG7-6j3~Nf*86k6oIr+151iE*k3h&5Cr?gF=8>ugu>~fqUbc)SUnw?N= zM2LqxRFc-B7bCaBD<)N#<hGY%kc5^7Jm913pn8p)5EFG zg6EPNT_KiEb7?KbSo37Gv;{=~H+bH4Nf3Hd9Sq=N2}TPBwVb}`o4!kMR#p~y&% z^22;0h4uX2xU|h7k7KOf=g?ITaC0T6oeVY0u~D?Q?DgCFVHy=~LrQ1Xl$S>G&S%R^r&*Dawt#S&;|~Q(=qku5(Qjc2 zhM%$q0G+scr6AnP%l*Uy!pjL2@N=nbKAiGt9+#)|lAck=t*@oHVw_VAnW){(ityHs z0J9|zAU)7am{S|Zi^KjMOBy_1;AiZcQzv^qeY%qdLJJ&rs`RP5a0UA^Sun?q6+RRs zq$m!tp%EDa))4yl$7H}oF?ww%vF0j_B3-<+gY<46@rIuzeciHPV(JF9A}uGY-vb{U z%aTF_5>@ViYby@@%{a0!aB(L)-|yU0j5RxgPJb~SQ%v&^O&lfyJ%z{1hG}&Y>1oNj z#*^n9x+S94dGgv-tFlsIlnPq820l=+X=Lb|$_)vNE(_`C#&^o ze+3~%hE>*37>a-hJ>_lE3gt&-HQa0nr7`Mh{ylfn=Rr@nTLrL?b}eu&HK*wO@?P9D z=;ti>kQOYa_l^m1A1a%>pNwVcOs{(%+%Ztoq^5B>i~KIcE0vT^kqnE`$>vOLTqFF_ zHHHha5(<`epqdnu8mvJQzhMX&C95o*qijgYUMU*vn3ndH{-wi0%Fsa1kfgcJf)J^P zA3SbhgsC!>k~W4!T?eXfQ}iNI*DW%rE#?F4~?br*5cjy0N*ME$knYSv++le;W|=WT-gmVDbO5XfsLM32jHy<8+Si^nFPyQYxKc4hYu}UklFLg+qHlEE+vBjenjhj#*A?a|} zIA7P-rmh(Zh&1TtrCr^x=Ib`kZ(Pd@jrreWT4$uGrE#m3CP5EmFy8x4=L3T>vZQ~T z)#YP0s=7bYhadP}>MG-BrNMd-jGw*!^~h+- z_Yw3ncS6fOrsy#P84O_)bpd{*!1$hGD9>|mlD<=>08=otmpWXEFig|U@IntW3Mf0& zOPihFGXsoK!V&CuAfzI|n_Hd>#Tgn0(aq#A=$EFdXCw!<7tWc^%>8lKLK|GrDai~f zALUH2y*#oH&Rw8P2FdG^##c=zjF6#-A7h$E71OJ6O&f(!*K7R#!&;!Hf*0M}iqG#E zl%D6B4a*s3d~KSYj!|p>Q28ls)%tn!Z;E<#qUf8AqAWv)YlRz@s&fA#kb74Iu}vek zb7Ho0AdAdyxEMD58SvMJ7Yqt}4&_+u%4_UvX-&zLdr-=2rjt4*MIY&d2C#KZazD*R zK9YKMbj{+vKcg)#`WFJ8PEOB38nO1C(A{058K<)OxgtBP1}+v&5@9>} zV(b3hjj2c4JsH921b~01w{uK{Hmy;p?q<_AWK%_ltMBaTdeU&^mK? z0cmp$-f%H%ed6Gx^+KXq&{W;90k%LNOyyH#5p|Piy*4q8Q7Q(NllS|BA9x6oLsB_8 z6oD`zDA6tgxG2$(m!Hq{Ik`Mtq+X;#4}W)-Lh`35(`O(i-gk{|87xyDJ!R4JLUUY* z-&S1GDR>M2c;!mh<(~c++);Z3Ehn^B2qe;emXISZ(`;D1w%v8dXx~+-!OTMSTx_?pkiA{-T zJh1I?931zl=kIj?V=c#1#C#<WR8$COM5cCigmZhCBC!3zkD95$zvc;ThQlQ;dq<>L+nI8 z3SNrn>U(}WfnzH_kfCg=2k!kRce^6!&Qpv`kc~Rp@28tN8L-SmJt2j(H7g?1Dsg=h zXK3scMJsGHOAZ&i?`)oH+D=E+xYcP-BWikxq(91lQ_hX&y?*i@F^VExa*&Od>SDD4 z(D;cIA8W!rd!TU!MwtR1$N`iFe15z`bnz1HWW(aP|FEzDbnv6=8H`L8Igj;oDhQCo zjv3u|_cQ*>V(M}{@nrqGdB%de>%MuaT$wcQ|R-qOl5 zHK4+b;i~5uhBipLjIM%^$R!T-Y#^>ji zTO5mrI4?41Q&vcW13Fz!CmEpG<}q?&9v3qjlGzS9)*KRt|3B&$g~jSm`L4qAE*};u zQjr`iKloHkZ-WcO?<%BoBq&r0tMD8|(e{3Rp9R4!DY-T&mM$GqbaG7zq1hl?Ix_SO z{Ju;l>2z}ZLDgjH^Tgf~K7q5webW>b|M^l^5W|sjA*9d<1;*>MFEf#9F{pTi!xyo?#qTH1(da#_N9Cp~dIn2_ z(j8tNeQJsn34Kt+Z&6P=*i!e2yC%9Fy(9Zum~;e=8!Bx&3_*r2%Fea?*&4*_0P6QC z;f)={K;$q0vUMR{&p0>y*$~{KO`@PY$a<*gfS_z^W*URye@ufV9exyPLr^i9nK5JR z3m{BrbfiH_G=S_|Yew8+zJ&1UV5iXw<2h&!GP5kW51lsZk`&VSaP2ytDvgSKi8g=^ zMU=Vl7`KSth%^68@HMkSj5a+1y+f0POg!+tmM@7+%$Oq!M|pGkqKdG6%Tla4vyF(< zA)P~SrLhG1U_%R1>_s~XbU99}8QVP9itDaddPeJ6mE8?+N*n6!qL-+-SY^ zd&^{BF1*&%n|s0gAW>LTT2YN>b0RO5*aQy4?0kT z-*xqgvI<%SVe9@_2iC*I#zHT_=FJ`rA%lNxwxICul0n_EM0NyxIzGk68wDn&pWdMO zCBojuU$NU;PYr;(Ge!nz&Kx) zhJ-x&hw()1Ls@+qC7(7~5q_qhu!aalIla`q3Q=1^PtG1sTjzs*EHsxKB6y|U9HNTb zUTDKFCcJ)CyY7+cXz12x^8LBk8==RlY5*_89rpc+2mwxd^Gx4;iLx0@F*Kc73?VL(r=}}M z+jJT$L>2GSI{t#(9Te_unTfd7FrPhLKI>G*n-W zBTmyHx(3siOb_5HfxD!?LZcNrEmQ>mbqe?b5;+pB`}6yBh<_~~b#r7hcr^@KcnI`g*Pg?PdZ-X@zS#Y_&Cx6` zr*YWdM>{$tMEEdt>{U88Vu3lUq2d)vFl;C5(#L`p3SX0ZkjE0wl>uk^zOzwm?zpF_ z=fn&p3{(5qvUI07jr zoy}0TbHRV@07LrzuF0@+a^{7mtJU1h71xgxSq618G(>V=c|31%K3@PaQ9!pxm6>nhCbg-FkxxdLpT#|??ER?IWI@F9q#Jpg} zPXv!&f=Or512KLF9gWdFaNHmlH>ETH{V{n_dd?Wf-GZ4I%@IOhH2k%*{xzOB4gSq` z0q$_omxDVG-i@ve#9Y zPR(fG3xW=D4mJ0qA&3(#-9bNo8Ol zOm>_Va2oQeO~jN4p7B-YKj*!F?@;!b3@k%Z?g82V<~7zH(-H3~CjNTKmmx9R8% zfWYZkHev!^Zdkv`z*xAUd$F7-fwwPr4}6#AhO16sb#sD=KB~miGB*wOmxMv>#fZ^ z$Pm4`SKCHheU|f7*lBb6e%V!(((48r|49>{7Y})(7la5F3suq(@_W%Hz0E>hp()2b ztTcsU}+!(K3t33t1quo390ipPI>Yb#d*2kiF-s7@ZaW+01Pn zPINk%iA+t=p=%}IRa^M_C*e{=X5-BgX!AAk^xcpCSll+NG^T8q9%CHL+7UZZ*o2SR z-7YLBOau1f_o76WL6tFW7mBYBqnBj^%Rp+}X{V@@oA`CgKS2KNisy!e`ZlPX4eijH zWpjjD_|yvfv$YK1J{{lB*i1flI*kdykp{L*If@JCMGs>1&vY$2%~>Yr`qbEHmtteB ziMb5>warKV{M+AY=qEm-PU+7S)NG!!?t;1aIZA&#;oCSSYBr|N!(!O+I3J$RqJ}9JR@4~< zTm-X*6h^b2cGsqh%HfVtr|1EzR>_6!b=??gx!ZXjIoJUcDZP!Ou^o=1(-iI1SOxOs z@JT0Ma?(ywD7wE{;`Wf1xZ`eY8n+Osx0m8o-RjjdBw}ThyBr#+NtlXIusLo zJR8iAMjW%KbujB^y$$0Z8{XuKaVZXSdNaMAOr&&OQy8Av31`k4)1I)thQj5g)$v}L z0PRf+D!b8Bxm)z;4iSin&L@tb{1p38bzb2N~b&8mm+bow65n>Lz_vub+0$>at7+KQIQH6Qc$!aNKUe5^oblm*#Y=9 z7)YIRTf3?|9AsF8IN##Sv~0jUYwybYK=T5BzMFsPB9oa4V2gUYDDCvjJx_67=HV{B zMTT@S5s7(8VBB0knS_fM*p4fPaR6gWeE)?4a}s`FG&&j}VNjNo&ekQkcytdS*`fjhuu6J{)> zKov@x!)VTg<_5erj#>a#Zqd>h4!9Jmtm4I=G?%kTQHPg6Pw<7!%tF`l*~UoY<=pIc znYGTiS}iNxF7<4DS?iWbT}_%u;dNUYaW<>x9+4WK>YytEuyXIthjbYsOKyqLI#O zSMSIK17+~ZzU8Nl{US%fXGA1l3t!f^(m+ESykuUK9y6c_bsh!P8Eq>_u5%V-FvBO< zGhuTsRCCpynLz^F6agqStoA0F3Vp@ui?}{$hFbvk(FbqzO{wpG5L~q^pUzNP8FTEI zhv{J86)6%knnK&-b^n>`O-(mwxf*T}&Jd^Y{kE$_QvbZ41-U?EEs-5U^MTu%eZ-9; zIh*e3yqSN${~7ee2vp#c)`eNL69$Es&@IG$mJIzeZ$o;uJI2!C7xvs8U#wJFN%<{u z5xL-A?aRI)2Ut=;XEx#1c*t3u^Y+{|wV9?N-Qpt4}mHH7}5yl*d*q+0@zQP-Q zxlvK4C(Zgkcf>ZM41h*Q>}*-q@FeZE?PeySJJfJyUPd%oD?+C4v0HYieF>M2y@E6* zX{}*MuTL+G^E{tK7@B2dToar@jj$4A z{n`4#z7K6Buh#sEYP(0_%oI8HGKG_F8d9)-kb0%99(8n(v!ok8!PN3BJ~k+m_q&8{ zEP`m&7WrQWz?jdn@L3b0w*rr`F`kAYIgAsjOa@18HNY+Y9Zh)t+J(HH8F8JUU|#1F z1=CE0FUi8+O6~qPZ__~cN0z=W99#Cw%#z?a;Y>5Twu9~|&&9gw2j2Cdug^Rl%|1!B z<}!JNY&5_qYBDKeSaX`CT+K-DA*5EeU@EqJz=#a3BI>gjwWORu#w_S&q3(GB8SXRZ z%ZGC?4dz4)R+r{I?DMNpd|+SZzWY|gdz^9*)CxLnM?P`XgFy5Crn$Q1HE&LdHL0G;fv`=hI8Ya;ju`he{e zBG(3>Q3yv;({N(g4Px%8V~ZhT-H(;0tMAMo=pGkpCl^|2>x=roqqtzBgV5f z;mdQ`EU;fy6yN$gygU~-LwG6TmT79?uXuUhMWJS)y=hIrKN>~6AZ>pk_gX*r9riv1 z%-p5Fj5gjV-;4%_(+KJ_k^lf807*naRQHKDz|1tHb?C!s{RBou^G$Vg`cD4dw5y@~ zRwwa&V!$v2x49w#jENfB3&DzyhPae(k3d`M;zh~rM#yf>)YHIHG4E+ zP&u>ep%asRbs-oqkc_wh6r!FE*zux8i6(fVQc;TF1rZB<&*+4Lm%~Xec5w*Z8=GgE z#XpU2#_DJC0-6o)5#+#lw9yo9`X_2-%B7h)-!Du*bUf+hdv!~6dNS(-kDjZCUY8A$ zHvw`%AUZkRY`E9(b6D})&)xGCrck{9Mil53=sBZu;@ZXS_w4bsGgY?^Ym(Ul+d|2i z2#hzIWU>M~{1xfOGnuK%wFJ}l1?Z^lkyUz*2-s!7g@#`0yp-^gJW zN)w@5GIW`3(8PMSWSTDIc6>t{1EQZ<&=SNrW%YA=XQGbgQp_^^`1xO-7wX&_Le*+FB_I>28S?z%~_$h>bHhXlojdUr7mo)nM zQz>ZAz_e0{45Z0Tp%9(LGN~Ed7!wbQhMiepLmh0sSC4Hyw-dwN&C0-sFHWZLI(?;p zZm8Ts<>YT<#c?Zh1B<-t{vE4P6%W#I81GrG*TI2RAYiK6$&`9l4%D`7Xls z@O2hnk5f*{2-^`yQX$80r>aN8R#TYDXVbL-0wkZI(vi57cgK zsu_r3*&?*Xb2i*a+aB8=)e&TiY_v5>QuR$X#|upqa_Iy;VdjtbeKm5&Q+{%N#NOrZ zKA24)xrLD4*R<1@6chBVFpPaYT(xVn3h)<7$(w)3%$kqofc7@^eb@f%in3YJ$1GOw z^|9&bxG)Bme_wRAM)PK!j(676$Y9DH=HB#AThoZ5?(B_Ow>^4SVlnv8f8X}|v;D?u zd|I!ynY08wiu^N%b1fp{`%IcxmY`VHKqp1b^7-b4p50dWk}19*V-hC%I8FTr64JNd zn~i_x(m7r2{(HXAPT$51SjnG`YH)eYK9@OL!q`j??dRn^WJ`E0;2U?_9cZ@^in}T% zEd@bNn|YGZ++Fo=_lR?`tW*yvXf^$Gwa9~J z7sOPBIr~!1>UX(ZHN?u(TVXB=5IE|(9+7%UNrpgBxh|LTUiKL(v|U{HKfWFoMF}pX zTcs;B%E`o5Z`L%(=3ZPh1OxIJ+vLiT7l+N^sDf0ppBbl>%})2S0$7UJrq8J!;YRyb zJO?y*P6|v$&c=c?Ot_RFZE1B+iW8K%H8Gq%OgsjsJ1zlQ2q;aPV%Iwbr+GT?jjN?~ z1VNRKK@yQUhFmZrF^q=DdC_FVlqWcmmRs~VmGEoLFf(jU3p*@9s|*^#X2Hs!RH}=~ zTx3$&bCJ;%>xzav)n&-3$)3XT6T)wjlfb4r(qy&)LLa40Jv@6(6Q*V8v}gmLz5Y40 zQ4vuyX##%MY(@-DtMh54o=EW*K^&|Ni}FyTI9L&AV+pL(`VB zWI^ZWuW4pGJ?duwvm-*#8iiK+WaLOCrGrLqYVTer+7;KQ)Liq#p1WhEy8$sI)7NAb zyCcI$)5a&IS9J`W6`AXjEsDX?-Nl+~iB0X1?VG>*8h>j49Fg2~N{#1yCfyo01dle@ zXuM-$-D5@8xjK8k=yvH%el~L-W5p~Q+C`saaTH+Z2-N`G_f{ew9}CugZ&UVW2&S2v z7n7vFMhckLLcR^w-t>F@tlm(?b{sY9_@J=loc{tMD6 zK_<|Uj;94}PxEty+y=YlwC*J#GASJvmc^c*SvVHr&&B=WQeBkpCZcnp%PQiZOPH&(gMtW&ce8aCG7xCGBip&Up^zVF|#xMKFdWcAcCRfWu7N2=~jq=h_PGQOB zaXPy5n~gz7^ENFQlIv_5IEJVxnrYtmkO5Q>YMY_6N%a#=#s@y(HVb4#Yyx(a1hd%d zrXz?pe2Thoy_alVre{tEdM%V7Eo>EywNS3nFn9T^tu28Tew?Ma^&)GI*#Jt9avu>4 zCAaIysH5T6Wd*6ceoz-6XeQCsjWW@W+^_`IGmJZ3_3EM;4pds$H1o|U z$%r4I?44CNg+||v78*sTI}7IeZ95)a`#=xO?;s+npUXU0_BuK`*=*;x!W@iBa2aBG z7_H|-!8ws)F!M%TdNYuidCMJJjVN$+iC8AqlV~UxF}*SZWUnBLy)CuXf<~uIWhgf2 zuZ3<7g9?$HHS$1?8ga7H28mX118M{#zXRSFmxp4pJ$C#54Y9wClxUcZtifu)H<}fe zR%aG{+-g@J5e|HbHef}lHw72twe?80Qj|7cXHm7F_01JdV(=2xHG{$#KtF{d~ghBLbLuLrYNIG$#A;v>f z3Q>qMt#-qWqwmEb4PK>$KNCXj94{SVL`aX|Sz2+UVtW-hk;ZFR`?TWG=X8E4YJJ%M zi$ZBm3!cddSKBvWbN68*W=qjL6TbZE7zVxu;vgpIIYB`AV$Xv7ZL!;Yhq4(BoB4NZ z03YX?J_Kz=_)e~K;%WRK!AoSiy(v`z@eI>R$rFb?wYx7Nn;_HoD3!V?7k+wfxsUv` z?O=eZQ-&KC&2#_r??q;o#sz{toQ7EGlxi;-`l91fgoR%$mWTSG&K^1I_8YG$skK#; zQFKS!Yl<_|K7#F}Y;9(LhdLj%2tVInQ`DTEDU*F|s*#jk=X8Grd2yPia-FgAx)C}m z8w63Cz2Mezo3L((gHl{Q0^JHP(JZC*wkULIr5a`wifkG5MNisxbPmfI8!6wp*Y(Vx z(@5RGVuaJpn4ZhNJ$C|~l1$XSGvTiE)Du3e|K59)D?8|rU_h+HMJp1yxT`_ut~)g? zDcB4omv$~-GJH7;c|-Lz#Fef-N&2j#AGa?0P#e_oy{Gyn{T+x-V3JbYsM^i47c+Gv)2CNDrnA3MMDeZUywXULR^zaaZtnm2>X)kNOELJ z6@R?{kzH~TG(ZM5W-YpZWkm)_rx~CsBR$^{ONFsMgzk~P66cm;a|Kn%c})`>d_9bX z38bd{ZCUo$#ZCRf$+TgSakkrME(7AU_lIvmg^nT}`9)@Bm!{th-P<4wj3*U{L`Q!Y z=WLqbu_E8 zbA-XG3+TvPWONhx6A|o$4Uj3Li%t~?DhmC5YbN^$YAsTbNYuD@^^#06OhuJN_Syg{ zmp;NQM!d;)g*6Ej*M|fzEOP_;3tCvMzo|YY@DIIx8DMEv%d}rh)6{s7@PWW)sTuv zp7{!+aW6nBRij3CFb0H9_R2s-KffG!VSc~kg=dJz*dwU}MFl%tAeTzK>7(y^sFFA& zsm4Xt6AFaX=ppb92S4_1g1S2`RW=!i1h+7zoJhH-3}7ZTi#r?epa|6Py4e`m{ReEv z1~AnkZB5R$MBeD*jujtIx6{Ft2~ciDtM?g)6bs%nL|CSf6xnO|e4@3)Fam4V2d|m= zK+Y+>7n}U8{777Ue&!MLBKhGLgY&M?mdKwL{>XcLuRXi67dYBFAl@@X%XV1iT8e!em`Ij1t77zIhH>S zX+bun`~A8npW)Yp9z`OUL?f4c2TdwK==%-rmBCQSAw9ct6Ejme?!YC(u9e-4M*VuR zVaf=oe@AHMnT@w0<5F!h<`xI3WMw38|4@nJ4*~^2DPt|1LOwd|=r>po8yO;#Lc^lw zN-aCHuxOU=Z9?ia&UV=4f;|gAC0~eDJ_nGaorX%u7ENYq%XJ~(nBmwAx!E>v5~6!4 z(kiRNP?!>|8v6a0Ksc3f-VC6UYR+}Qk-sPX1TxEUSs1zKi$|iYTV z$YeBO@bzKgxvbywb@8<#R7)&dqD+}cJlkdqk*~P24pu!+Bb2CDt@i7xNKWFlS|O5*v#OL6W!Ti{?Y``|%VA%qJYg!xAb#4!Mh z=iUMW5ZTiDmgF`^@B_Fq^UW0aqYF2q{8nd9lMQ3y-MH!&WL~xU*3^3|4{iQ$oGr8k zveb>2w;n&z!|HElJblna=GdQhtOS?w*8Ka+jA)Y1*X>*F z*xWV1g46>p&qZf6hl!Pus-#hzSpNO{4xYnA6W-tlrrwygq{$%eP0`LgWD_tUl=b+6 zGKLuXqLXot(2I%ThbSPyQGVK)p>hb3Wm-wu{iV1$J7isTl)_q+CpE)>OLFE8tVs0_ z<1&L}&J$j^1)7gVe{XKW6;#pC$me{z!4t$1-3>W2(nBX~jDC$$hCk;G#4n;}rpQS! z(a@b2VO_?GN6^@i3D$8|bG+-;i8omeOZJJ0;DLp~w&FXwr!a#MfG#%G^SDb|9k8hT zbEbMzJhTGx4hQpM@Q7$X(M-v?6>L)Qm6r-0I&h{0eQ0j zsLbk!D+~cBi9FMo_z7F+s)F$(QGLJf*l|JFkFu`nC0ZHOK>0VbvYoSVKOWO7;5W*%AWnFIy785O-A>RT%6;gg3DPAY+i&Nuud}%~EhESTr}3L>W9+ z!`x}<$Fnm;pZPRMKSd(X4~?ZqaBWTb-UDMiRxQUz=JGMV;ihbhW}KEEIHUldI!xfw zn)5)zhE`vERNfHCWBLqiaQA{VEdaNom>LKds%+CI@ABgPlCuGwRRX2_I|7 zZx`0YWe8_kkclpNfsHd@jcmdVb0x*;eqdiCdBMc|BrN`sK5KxDNg;H`@HlWjGSEeZ%hgNjEK7kd$|;(>UB72v z$cZF`Y3GBN{C+QM8dx$-J&^La>`Y=Q4TzAW(TdIES#8;qeM{oewTAd{$s+RIl?Ujj zQv#lcC0oza;PS>C%(NY>|0xz7b{Cjsb7GkIs)R-Y*|4P!&>N~v%EgYfgQG0k?C451 znxS2F(N|v5m$&&!`ZwUMlZH}aF`;LqpVHy}cRhdqM^~ihWwjIJyJWB{)9`hV!-H0; zY;0K$)JLIaSk~`+>&?hdYnG%b|5^KF<&uQLSJthJZ?hM1d=e2AX~zedzTuJ+EcurC zULh|AY^+MqPFTwS9>ci)j96RMrVHz~;>5TBms0_~6B=6L17-}SY9xn$zsUGe^A%(^ zy&#RaLnbXP2+l5qWJ()f4=};0(yU}y4Ua;;tB&?w^YQPQ0`Wh{K$vI5L3GE>^Ms;^ zQHVIf;GFMGJtDKD%UmXMW-1utxkNt#>ZlRf23`9elpS-~0hZ-h4Y`N#GfKR~@Q?9lDdh$s`Q#>Xz_Ju$D zNKm-{C@gV#kbqRr&%9*u($1DhD3r60!UBTHh{C~*@b+KB?>Ekj@bfQ><%|>zNA$kS zTq7Vup;u;WJr{sS?ng$wk4zVgc>4P}CE%~` zpG(H!MiyhJT{7$1I(Q0+Gb#+pFlFAO`yKBUBFnKPoI0!fJ%8zs)5#0~mDSg|sNZ|= zT^0$fl{Co~Y)=2BXzR^4P(>`8|6ntvv)WwmpfN?S?HS6?FO=(pU*CTXkBqUCg+-{J zX6wkTpBJGQjn7jV5d0w%;tMjuSqg`MC6bP&@-Z|Ux=13B*kCbKhbgGP8-wMg4Yk-t zG$Rmr%EF@(!tOKoH8@d6PkPA(veRqc{LFJYVidXjE^y~EumrlM`t0oG8T(%J6aE@Y z=|d~SPePNr0fF+PTfTd_YbLKeNV5!-xgcVZN=jN-ndNbf_vPIQF70$4gpbE0Cx~+) zzE7)rI>8gOnC4;3F_v@7B$mr!j%tn90@KV`(b84_J_sL#SmmMljIhrxN{9z(ve3|4legA zHb&p683ag!I4kJH{UTorra_=+?bVrxN`*>2O+tjY2%OE;P#Qf*5yd5*6zyI`>bnkQ z_(hrtIO8up8Y-vK#I`(7)MqCt>AuJR3f5tT4TwD=7-~!-E7+U z;zCmHLv;SE*}UuWovjkd(T9q9JO3V6`{P!D+uDAtLua^B0x?hEvIkNIV*m7Ae3lgO zw>D8MauqY_X2Q_Y;8SHcYhn?@htNnr!cc@o!q#6ilaRLLBE}FnA&=P@h^>8^`yz{o zUxYG@SboRu0rC(CH{XZb@c!n3HI?F)aV`z3*=I3ZL8yFs>bKA#3f=JjT#k7rFx&1q z6%Foef)m-JK@q>ka|Up72PpSM73e|-4X~V&m?zcE_4IZRA=yPOBc1sqf+^*n)^v~Q z0Ev)|r5_6x?(uwquc6(=C{~a{$P1PQHvjKj^s2EvdV&n0OKA&oQHsc1(emfgH?N@= zE&aLahf6E+R@D2-Mq*Marfif-=ZLY?!$C{-a?qWHxMX&HM5qd5883Orf(TT7AENpf zvy{l8Dk#(!W}Yz9g505X$~ToTrN_v-t&tVvF$~8ZVkaS zbuNR2f|}i6j_?UbyM}-v1}@Fw9pRKsLFdK0guv_q=Uf_3QFSlSW4n?l@?fEm=?SwG zaLcS=0WT(7R!|HU-jJXvF-tlc!do5nR{FDnNn(n0i}Ul)cJ?4LQ9?gj)F|ueSzKB? zGreXRBIhgTI+kLUg?YeIniLl$$p0Ur!7>zK>4=!+aP1Ix)h{QC!r~sbm82IqE`gy8 zmLc6WSl0;jSh<>*&{-o}=ETZ`c%J_j=-iE)fAVU$W<_6gTez0b%%+j_pRJ!AJ+x4t zbCeSKE4g;;ewO8oTP$L*a5bdr&j8N6Fl8zq80IR%3Kegbqq%7e60&}`(s&#Wq=j7=Gs&BwTq3$zi~roxTgdmLWtoVmYPwvIcqw61RB?+Swe$FE z?8evt5pwM6w=NutohU zg9WJCd{05wYnP7t_zb{Wz$gO?5FIiz7Ulpk%uYiOcK2!IDEJ~HH<_?#N*FXwQrf{u08Haw%%jUdsDb zfg|^`1y6omIvWglg^gL$s`g*yloqmHwoLW`tK5&x1#EE^1{yCcK0-ds;7b`|d3z9)XcXkc2rkDAJ%`C>Y>#)P{S4K+ls%q;Tm>@0=LKw$ zQ_IA@Ht;vn=5mTpl30e6d?Vs=E0al@KQxhsWUyLcx53o?8BPrPch@A2Sw8ZW_y(c&v#{w<{IC|^*^)+Pw6zs~01jEIWR9_VCbHGFt}M(d zck6c~mo}Hwt(+>q))}DRKWb%pVH{TmarV@;0$KiA);3UPWI+ti*b*QyDs+UV13Ht2 zl(9{u>iltzv_zGEN>-hrmj#fUToAil71_+ZMW%E4W5+A8RT9R>bo5{yKxy41uwf$xf*G{7q_%{mj_9Hb5;& zruS7zdRa$nbB~T6wnQ6gE`I(_H0#;9E&I;{E}#=kvT0?RHBG%SMR;@lf!ckNmGd!Hq~8W~Wh zX@7dSm<(EhG6+C2CS8RAI0^cEuN?9TV6by3zz2KJ#L5v%Xx2OYjJBaBoo<>>zM$C2 zAbh*1$I=s&hI%)sbC$%9N1w}guSdyqr1`cY)>OyFL`s(nEwq7_9KbORum+JI<2YlP z0yR)-;tojY&Y*r^&>*p!rB40krDjO|OTB{C%` z>%eN5Th+Ne6KWURQEY^JSJrlm!DksCq{fOZjV>rpa`_j%v=sZ2yW%kr}B z8mEQGxwoZiYTdgz?pyPAE;PY3IJ!`4(mUrj^H&`iYRJNdFWVdI3unq*H)$;YSp51~ zcu#Q2?~BXm!!-b`~5W6Ysp zAh~#13@@$E3ZiE5uB}X+Zaz+z3$-K(~ zL$G--(G|AoEprK6-UV^0A2tJJ^d=(>HtG~yme&h}`@K9Od(zZ^?j$O>>$eGoo_xTF&3Xw<2cAh-~F2ua~7%D+xOlg%v>4!iBX80jLfpnL_WUz zrCc)dCPnL0E4j$_P0q4{a#{4S08v1$zXZK>de28=sTV#k<)4OpE+R9?-O2Ft@kOGE z(1JPpl&nU8C1HxL?x9tB$@k$e7j`s?k7OB&d735hb6Z5CD9%1l_Gj}aOX>j;SmLqk z#HfIc0M;ZnU+>iIoJsGK4bfnU5Yc$xvZOnIcb7dHlJ5>c?W!naio>*6(pXihtf-g>a1A&*u{inuY=BB7GTIu6mpZeVZhK ztaP>(G`@7y%^DJ+>5chyf2{X`FYVgnk)l>mV~jxDKJ-EV4m^8e%oE|IvJ|wRcTeP6 zA~C%!c8aEDY<+yO;380dd$`Q46O=gBh^J^ZZG*k#X!^VO>k6JL851Yn?U?YQr(160 z*W|}`dmbuNv(B+K(-NMRhMVs4$(Hg7c1;_MxfC}tQnBp!_MM2W@ubx-i788jZKy+l zf#{?cZAm_hH{UNIyfSfbgQ5!jq(dU2QW>bJl{aM+rxsG>P;{0PHh`&8N!17gLFsSr zWizQbz=f2ul{5|IfueyIiza%qBPC)9^$JsDh}n7%X^{@Gm{H&dbaX@2 z`TG5SVo2pnS|}n~{DbylCaaDFPZ#IdU-HoRg{l(IU;e|@2Y_|(oEJIh!fY%{>-M|E z`nfM(Ph;h=EVeed<4*}_^WwA!VU53j59)FAysC%lW5cG<*kba%*c71^utIV4V$Jm& zcK+IYPcmbxH}6Mtmdhks2<)AX5Yn9wHPc;>l&@_LRxlNr{fLy3WDGRwYl2SnGeDcn zmG&|*-5-PG^Ucy|FBaz3v1oSrttkfMS@_BnSXtrbchIhYyg&sZeTr|O)V5Q8sk6@S z&(vzn&5tiKPaV*;iFVoWE0~S07p&jK>?v(gRwc@}<@=0sGkIpCOnb~o&6gHn#1%>} zsd@^N?n1M0Y9Z6R(#7xxX))MK+S>W9$$!hGL6odxIuZIwq5Q z;MDv18d!!-WfHiYzqC#OW#`7qOZlgibCyN&A1jJ=Fwj`xy3ELh33IdJ_W7NsmM+P& zzi07o*|Ry}(;IZ;=d^s#L>Iq*VR?gam&D78vM~RYliETzVag@LxG7&-qd=vnLL29Y zkp2W18{~3Q`GkqJ)%<=x*<~S&p)4u1c&NNs9kSI1ZFTb|>u~yjcs)+@>Vsp19{9kD zMOxPX?CXGz0YP751fr@VO7l_{nUYEtb7HAXCB2JewqT7B@371M{$>~7q<(LngSD`C z(x8@mbhMv-5t=pVJZ;#=wo((U} zqUKFUtR$JVw{#xF!*LNGjWCrM&@}W!izx(A)wo)ihLq7VIybLf2E->Hd1R|?sR!2g z>_gT2;(X0YAWMk_5vdA)AUO}Z%(2Y+G+5rjF^+S%G-s!;qhY(wKCWJ0OP>%;a;9J) z5LsHBD~|-uiiOk{f~B!n|4(|7nlCaD1VERLDFSrME&Uf*DIbRH?#;Azgi9HhMoVvq zs5Y@5&(ADAqbibvnPA73G1%d1%qeaxuWjPv=gS!gpJA0)4o~?S^wgU3=j9!N{0w^d zd}dxy{uyx>$$zhBZ^Hdq7K#Mon|AjSg@rLBYf5yXz@-0(7xTESEc zy7RnIRQW-2DUuz_eMg`f`u!q^t~g{^7XT`MTIB_w!`$3~7>cgOXP6Dox&ZnRaSxl% z=+GUD%4`BY9pk7-%n7TG6Z4YpW4ZNVbtG8vr= zry2=8B6sYr#P5x3uB`kRR+AkGf0;_0G=0(0V>*jZ*QZ-v1MlZR8^Wb^8LS3LXEkDQ zE{UMXT|^#s-)R>3{feq;Ts7!Yv7L*vP-6ps5ZZDt56o}!>5;DSCOrF302b#}3 zE?GJnu?J}->(FFGbt!#yKioxQNQL#h<$HoHxxaA4Kh3lkYz- zrGmPQwFdRF1MG!Ye|+8qjWoL57{93!CI9~%38l3F&x=0Sk?4$ObYzBYj1`oTOda|N z>K_*k)lhzocWUUjC_PRTacb$9Hn?{@Gp&ryhbwP4zu^+#75Q!Uv$<2NnD|}-tyHfIZKou_9;opll(QT8o020SN86F=}Mpz->`Q1r7 z`g6I{T6py$R;K9=uGx*-nZpcR25|ldM2FC6YJ_B@a=hxqV%Eq*O4>j`_0ML?L ze_p?fg8#*rkOtKgDa?L+ zlqEwq!AhK4vv^C)??fiNeQsR-S$c9JsRha2^+k5S-x#p9?8sy0(qKZ;4ndK(oy>~D;;ydcEibKE5J}wgH!m!`6lp1Yt;1Eb9yjE4 z_M{vR+zS;0DYw>PWqJ-n8)rx0gLdAL4)E9@ve=OiGKy+ozdPdZrJ>E{)=GQNYen~; zOXj1xYh`ZoCg&}Ynm{-qEaKpoQ4l}RBT@Yn3&BeNLx51~SDKPp`672d8yO-$Qk2zD z{o)ZI8ld4z5|SdA$OV)Q#N5=)^}W~<2KV9fz8gTu@N$>avo;r2I0LAxI#R8e&DF&s z(uYMv7!4LCNByIbd^hS7`dtUlv#vl>rt;}9sbHbzOhkp(OvXgKbvCJD0e)VHP|6}v zOJk2%cznmwbDxuK?ufoKSrC5j&<`0bpe&*SEPki}>P&ZH%w%Y6v3emhev~brGmul{ z`SH6wL-!`}Ot5l!UyP08RMP0usp6d`Q=U9Uihdli{FojmN=&LJQwoI zhpJ7F%F0^cOtB+&;7o@Hn0LXzHd?Ps=r-=ZV-iA$Zkka!0+V4-27!M~sGfn996Q+h zPL&_rDBY9;EglK+1OX|va6cErV_~0T_k%_$SeV9?O;K4|8rwqJVrPcFXZ)I`5&4Yf z(rEAe*JPe<`2R&b)zfm`kM9O!iPu8nPYFB7uSJDRTd^DRO+vOV)*?Mxlx48bAcy2d z#&bd~=t=iW3oon}$hqmt+=nBz*-b|Jq6U+2`ZJ$x5zt*I!WF8SdUSyrF{q2MlZ%EU zmz}|q0w~fJ9ZrWa>>`p5C2)mKMe9Oz%KYT-`i!eEOR|CR+u6N67JYh3#v;8+3*7); z08o*(u*5t=P!+7eg36meYpj8JLSh5!n@Yx8h8lXG5|)%hzou#N`BtcG@>0EPeb7ZL zbU7lGlYm@6Dqs9IZ$~nY6H>G_JoE99f#Tm~Kl0U4G{y!zi;&8$U>52OEy^}<7Yt=Y zv|gpKA4D0mdmgyyTtX+JcoGuC0!*%a&6a5E{K$qyHBT5;7`FgAQB&!(DqP-suf}p}WT0N7V+MjCZNa>1uWuUw-9*>?dlW7}B|PfrFpb-ff<@(sX^34 zDNG{^kC7g!V~aZ>XnIa$e%t%PH^A6`Eq3w3Q6$q_i(cJ26Lo-7{6^5L$0GspcS(FT zby!5C!cb*oV|noh95LO>v}6j>I169S>`E0c1aVWwj!|9vYa|T~@bm?gio%nG9!cKw zd-%+lOs`UMkkW(DQ~)hs00YFijLC^(Mhsjjk+aJ%f(Y2C=wYzV-}mjLLX)d0%l>^a zxK9k!;c38EWKD+X)LfBzZI7xJIpTvbV|NiCO!^E7=g@nPJcTMyd^E#o>~2I0>xHgT zHcB-2xjXf9$pLHJux~Ng5M@E^SA&II4~B^7K~ZDm0UGPzeF%OALJDi`ZU)SMq0GMZ zgs69F@UO=~qwkYB5*32`K9uosAOpbSZ-IkgaQ_v$J3v0$Z_FPsFAySx<}O@c+w7qx zw)4F`-}`g-rNk`5VJ4RB>Dc`gRB@3A(}6=QfiPGK-OkKM0Yk^a7T+b)lPrZOGsfkw zjz@K9MV}wEa!V+RO;V!={E(e(G*c% zXSHFye7nQ>0XqHY`P&bzR!*N;WFK!SGRZ|js_x|>0V>6SVaB=h1}q8<=LMSA(jY8! zKEsG@`nWj0A)6tnF|Izu8oHKWzand4RNLf5m9mgoqydw0pe(&4H|Au`Af(f!_K%~3 zVwOQxfBs)IDw3AXTVV{=BgwK*1j_i5-O(nc&#eo=1|XVc@{=>*i>%Dv2QI%a zPm9uLq9Os|$kD&g`s?T|REIwm%#evsWJ_#3d6=FqRZ`Ia1VUDFhB)*9TXDF-ijaxIBWQl1Ix3+dR$cg1l&~iXibTF1rAl1+lk!!B*&>}z`hAha&(=@b? z!is=XK6iYBmt|B%40?4IKhZ?VbAUCQ>_LhfDT)wP)O?B!7uHOCy(SZaBt3B%oQ#^| zSb|m?B*Piqd^Ut-aE7N)CHv{K0hf@BUd$(&)U6xoS#Ewo z9z8>|;^D`sJin}nl-fA!v+DF_*5O?vYb(nbMO2=l&JY|Mo-(D$iz!}Y?PqzhVwhF; z=$Gv*!)(ePe4N~e$BjS7bVKW;kEBo1SZ;y~uCkgi=&>`nki50ZU)T|Fs8E52G5A`0 zD-*HXN_XI$A^TzA|BfOh&i2!l0fyv%1%XE$NNy~14e`CZzt@5JZz3gZ>RhOW zZ(sHnW*~mXw=|+eAn!Nn5zx-oMO7f>-7N+U)^*{Wda^vyU6FwT%jIt!B%8G4`Fjw< zG0~|1Fq)3LAK_!B;P&#AmVc>tuigbk|5sx}yysAckEOdYV23`qB8Db?Fc7RX%#FVp zlesK=+F61?b;G+k6+Le~2ELFb;wnfYK23^o50st72Sq;TV zUmCKAtD4F4UhR~o3=XMi;L(4srDY9%sF(3s7%YqcSGv<))DFIY7N}0YKW!p#zytx_ zd}b+Fv`(*yBE^iXeY{SZkfz|+uN>1gdyz5_cUyFyV8rZi;EkjQiZ@1mkh8J&`T1f8 zO*h2J07s2th2ErJ@6i$O7$8!#iV45|GO~*|A>kFqlEJ-AEXgnH?8SzrE$SDSvI?UR zEJcM5Oe&Tn0TEYrZ-#P)KM39XK|eI)nNGmPp95-nHNx-7_9Pm(y+k~#2G7(#xFA1E zzIEpkzt`3Oq>PQ6Fz-e3u63{M@^H1+&GXZTSQ;Y@&yN zZhQ^H`*>PcBQO2utBDs*<@sm&f_5(Hgy*+USUAD>I6T?HxdfP?S5tCS@441SWg-}T-HX&OXnKVm`WM~N59j!G7``B+UNz-;US|a?Iq~km zl!MR3z}g6|(lpp$2s7n!9Xfj%FQCI{{r)o(eg+c8Ia4Ul+!@Dw&LBNx$s`yF`A(*Y!_ZDdu^Pcfr zzPK5qrfeD)X&RgP425l?O^4;%Yv(eeQ<;#jy$isE=gH&enM-K-oIFIp7=5O^Y#hju z1U4Auz_?R`?FiP*u{ByxsoziC{q%sHdB<0vkf!u0de6;o5{!u?)}`57Wj7YYf>0%} z&ms$k7zc%7m#}V@sFUz>J`Bd(f@4Z0tvyzVyX#j%`TM_Tzz&h!XvzBCpEHw2&&l`rRU8Q2YTjd0vTiWP|yU|$H??Q={sL>$ol(p`OWjZ|Mb?v zy@TeZqz0y?jz)%il=~=Vn=KYyg~&PozZyc0Xv!ziRjw zs*%xR|F9rtCfml?xPVrG9xQTUOaesLXl6l6Vxrc;f_V06tcl5V7V{U&eM;pFhO#^mnG7d9T;dZsOC>T!+kHrG{G?;p2v8{X;^RX@mjXKDHRU(r zB9CLFpN2HIz=tspku8-{ep?>7SuVmC8EKaB^&yx{Hl&bi3b0_HrYKhieCQ4@oOK2>SVrdjnE`pK_LMx!V{t&Ssp!@*BnL-{)y^xoCQ(9{a1HGJz~X zV`s;l=DsFZ*0P4>sj(#tEcVlr&^({4lgzoFA+xst_wPsJx@oNQ_U9)4Ob%7ETtaTn zZ^Vf&5=e&r6~$Vz8gWfJGhBwJF=nLCDwv09Yg2_L46cl0B=Nm4a`yYJ@D5C6G$V^^ z@$&9ZNMLFD=L)94p2UO&wWWATure&oB(`^|pCAp-lvJ$bmJv3iDW%ii!}VTbBskh! zpK?nF;MH#k%>0~Ob}QTS$Sq`%UuK>!@t>@ASaz0jr9|LFOlGR^TYF{s66i>L4yRZL z$zGE5;U&YQv^1k~Md}ePkaU@m_JVAYX9A~Pvmk<@{z_K(EI&8@;5xQNv9-izIFrN= zE9{GyFN=I+E=087VTNsH3u}GqTT?UIsLvP1D?fv+rBxflx4{<62tBfh;$@xHl$VcQ zd9}dJ3v%e+V;P%Ympi-;^!viNv3PI8+bqi?US*s4eWJWbSm^Csj`8xlK((=^aepk9 zI%1fY0E$HIv3$1pgCpcSmN6P4yDw&Z5x0&>c3_?d7E3qlyH8sHuSxMi7okiLkS|w7Z$p6 z?*>Ni6H{t7?v=#${zbT@4wI|zN7)6S7TuPhPQ>prB=oYZOV9-~F)~(96PWz|*$=!Q z$Igype!tI%CE|&k)Md@__QaV5X7CU0StG1uRSG~E1ec^@DYDMT-dr^R>1o)^5ZT8&a{Lb|}~$vtPh z#BU*?o2A1*5q{NzlJy2;~_0k#AB+KjZ9F4}mYwNBncF97`JrOA1yDQ2&m%swFe~#Uh82TyE0K zttbyG(|ehA7!CjVli8dYS~r%i0V33i&lpl&Hi#|1SuXNzoz3TNU%B}|Pzq~&;0pTO zVF&iNzWkiU1iid3;3A66yNiqQJiqI&4WKoeV@fV*pU5KdoIe-fnH^f|a|aOt{|uC$ z>8>SblISu!Y%s|#XPxlGjKzV{QSN6zL!8K{?&ts{M%aPu71O~gWm&WiiDV=pQyHpH zW~<1CFP=J>f&pBPF@=2`KbKP377Ux$V{CP!RgiIIhWN2oNAsDo0rwI#^iVrF<8pU+ecD{?zbiv0Z`ZZCTZmx$cO;TZUBl7=Ua8 z))$BD@j#Jz?$prHw8}+EI!3A5I|N>(eGs>-4kv1moap1$evn85p+hMTQ(=WV3@!WI zI6{m^6DF$W9Q0Bs^^0aw3n=Cec}9{WGPAdV^(-(wAP%(9hRa2Eqbxsf!)-{{Oww2b z4PDas`?-EHce^w+8%e+l7lXBI(~8?}gY-6eQ&J;>A>_U=LMC~WJTHiKqW}8bPc3q~ z#JXR}m%o0dn`szF`C`v)hmjZoYj}_GO(iEpjF50S!D!K6LvAN76`tI&qyG>jYjqih zgf<2&m8)h`a2izYck{i&F~;7Td6jMP&ykzYW(^rTjkkAGcng4#ut)~~eBhSwnMcCxepkG+tq1^h z0Y5xp(YtyYnD~ZYaw=>f!Om28C}XEdisxs5b_d_y366xEnQ|Zp=ten@S}@@ zEXaPY06L+PqOX!&%%ATL(U^OVaVg0G^9^3fwM?%upBFv z#>0&%)V_gz7`8N$N=J$Tk`W}J4+=9l1A*{5BHCj$OQy+`t64O5$SnbHO@BZkPI1Pd zo+-|1Di>OdNgO|!;L_~Tk{EN|=@y|Cc=51^in+nPm)C>m4e2W{qG#nE%IDtA*#Qe0 z+s74@R8Mb{*Zx|gnC$h zQ7kEucv+m#jg~e1&*DRD$?lsssb$uS;H}uxkySal%uV+7#>#X3yXQrUs$gh%3z6c` zmhJA}&o|ZsI%u*inn<~Xu`pLEHIbyoI3JR&e$zbEQ-&SznPNtE&z?!9BVJs~D!KBk zvoP2)%jWryguM}p40;D>egYCYkxa58p9+wC5FRR9*kp2w`21(q!mxVj^1T5yp1MJw z8?f_8uz-Eb58!y<nkYgY&p8yYYS^PYo!Dh;j{O@iwMzx5dq8B+CT2dY)O}nAC5ifGL$>q}Hh_{k2 z7~?l)xU*LDjqw|KhN7Fs`itfkFy6!Fkm=EKpE9kRC7w;#q{Jah14FPgAt9l~1A{|wTM z+fE0~kNoZmRm3dbrTbi4lG`JT4VG?Xz}4Lj%*3jY14*(Fxuo?W;w1B2vRiTqxc~x` zQQbXXWnVDq=6EkkGV+CY%RsbvtNQyH`#nzC3^1X8aLFoP3VrFRUYbLp!5-w43=`3k zT?}JoncV~3^z#ZXsE3e_D!AuvxB;@L|N6BgO3 z%ShiGWs46c!?I#M`)rKpi%c?wUw&V5Un1q5?~-pXy30cY z$^tiWY0i=D{AH|98H^qJ&#*=K;3j?zh_;s5EUO4y#B@UA2pOzM4u|rCeZshJ&CHsq zEeH9w@HwKoTpX@>j9Lxud17P$5@V=(F?!QcsFzjSIz+?+mP#V+6Jh_>UE?1Ml|Hk? zEb12@f~)(*3${ucZ5i$^$t+#<+;=Gh2$;VKn*M z8lS`VW9l83wb^XXossafoOX_yWR;ue>84Kk^}%l0PTjIDu6kkmZr}?dU*xCE?wVxu zM4FSVe@KP3Mcd>&<=vPtv4(**{%$N-^+CTWC@WiDi86vTOW0zBPA%M$)KCp!-lTkg zr!ZDTgZ9^WZFxfMPLd{UX9*LuoEjtZoPd3ed+^#gx2Z9v1N*yQOTbKdE_Kej3IbZ% zFEo;^YwcT9{p|ATS`HWQRCx!N^s$t@aT6onJ2Q*``)+} z)fW^jBNF(ciXH*S=m)T2PtNQQHmde(C5DG4R}HQ#5+A(iq2nC*WWz9Xuy54ienc|( z>W-W#HPpdDQO`seZORGujjMgP4_#k8S#8oTT4%(*c^A*kez(j0up2Q9g{;$}@-{gd zoDRK+LU%FvN!+I;5(PDh2wW~wvN4s}+^=!oojB;J?dS6~3C6t`uy$*3Yh<~FgZq_~>V3iDZ}id7o}}<>8Tc5ooaJnFHl4m82GBGA zBdPXb&hgM0Wu7OPKzcNG0xYAfJ87DuTM)h&2+@)JxD2Ovh%xq&cjWDcjnjSub2V9?q*SDGwxYhQHu7R zxo3hA{)BvomQE!6{&`<8BA`Q<&Ga~K3+>LM?QEVVk={ol>n6B(b~lcU^HW7#oIQ`8 z{mo%^)&ff;svqyXgBL~5smz8ALMr)+m$2yl$Xymh%k2)@PA%=ilo(n)aRG18wi2OP zm;gDv;VB}F$ovO^v|Rh~=2hfuVfpqfy%Zv=`5ZM2;~3~e+Q5sJ5Gdfl{5ti= zMog^@_4mL#{Q*(VG?NIMwoEh9$(pS!`FEscrLWHprffA|Jh9m(ZH78VS&|vpu35td zXP~-tEFx3WIL6aEhA55r9S}3IX4Y~q2t4kncPzQ`i}xM`^-#$;%gBe~H)0CMN&|h^ z`Fenkyp&8vQK5ZEboY5)8Kf(%SsDBudFk_9PLvFuR2aPs({Ybv2_`^;_h+=^T`39C zb+hW?~`Y?6Gr**guEu z?>-VjD?vX8I8O&cdxXqmmj>37*T@2xfl%4@&StB$ql6_Jji~J7OZ0Rq4H8jV9kpPT zKImONyt0A6!yU5}@Gi+_ffvu7NuUXrtI5KB7n|?GP9%!Y6J*gb*y=NLQ}$?d?eSCw z2^O9@hTfkMJ--2`e(v#=_1%>Wl+lp!o(7|~oFSiSH&3=tl}IS=HgM`4wFQdo5V|U&6Hmo1|Sh^dfA_YM5Dycto{`QV9Lsreg+?s#?f$1 z+}TjSg^Bo6N@03fu0@kJ7>VqSJ?8}d=A~J*!E~a?E)kLY?`r`Un0iU}&!edRlHrJu z4p@(ZpxMWmJ&$oo=Z)Je4~8U6v#_UivXB8x(G9fJTy&G!Dmv!z$yQRDCVf1fnjQX4 zNrXD`FFk0GW;5FI)AQYKV5G{+@;p@q0x}pZqz_~%GCmr$H}DukhOJ2r!_Q@*G29eA zE4w=&S(Zl<_tD?o?=yZ1_MAowDZ@EmkbQ)yDb)kPDVRiq)*iwUGykA^4 zytshGr=ouj4K3dxOFcx{lf=;d(%C{XO#Vj0D|bOPwC6&k^k^6J^53f2v$F_LOFbw4AA3#6>&EI4VG>| zk)*M1C7vN1L@l8N`-OV`o`_nk+B%i@1mjU-2ZC9UrQ$}V#&d>w)iib=fHD|iYKQB- zDvHvcahNebmq5NSfv-AUPI#^7 zp*(;Yzr7sGW4MfjM5IXnJXv&^rqk|s(Ny`|7?3g*8<`EY#>UuLa-~d1uBy!4VDPYM z8GvO&!1FD9BRPd-Lv=aUQoJWi>=j9)SUB&4Xl;LjLIrGuRs!+vJBSni0%Q2of<{g; z;>vnGED<6eOOd^Euh9o7ur*s|d^WF054&FU?#z7#EvU5O-|w2yD=>@x)*e0?4s}TP zdrT#nmP9y_40w`#V2sJ>>wuIsLJ@m?^;pstMw{P>$Dc;)F%c#s9#EC6kzs5{O04tb zfShd_Z<`x%AX^cpuqAzWap}K%M`dE4?A$oBg{Pfi16^mr7ZdTtkr=udbWQT*{@Jc{ zhEy(;Z}M4I8J!k+4i>|)^rK`;`c_bC-4XP!-hk1Ext0+I z%g^rD#s%`O>^i0K0@56SYAYnAY2H$y*DR;YIcHn;YVqPPA%*Dwh@G6ApZkN~u8I3Q z_kl4H{G*QtYe-jp@Luc{LzWDc=tz!Tk2s0s`0$}G+P%0JiNM&LRPvOHbK9s7=`v6T zDP!BVuk9DFYMlcHL(Ns^AaXfWwup|gB+Sndt|zBiY2fYXm}MDD*usb zw=j=JEG>*-*iA(Ha}BB zO{z(g#MmM{?mIL|t3H!QW<+PXL#mwc_oeI#*4=WRn&e+0{!zB~Vg(q54FH3Om>w@#{Ep0+Z9M~Mb{%C?xv6K8f)FQ9|6%Z0+G$+ z1;V?~4Wk^@NPV_*=RpM1? zl#{Gn_1igHkcEi<(CL_MhKzdeXky}5QVTK0z&q;^V=8Fi zJ{0&fPa*7lWTw z-l(*dewQH;%R_7BF+%ZY8|2DXFC7)wLSYt)oPr$1cT+zji{a@s0gJNL*C6t#mFY1A zW!kr7v@c{fKvu@Ii*}f2WxM{lUM(%S1ViNdJ`17PGNsiIZOR zIFH`=TL!&VtDdoF7()&=1xab&*({wJI9*!6U>kEV^mh=sWRU$c+2xi)#kj^C*h@R> zV)RRwwV38byLc8Jt^RW%!MifD{T$;GEhdGIS;XP+K8BZKD!rU(MmyN=g~5Ja>7+Am z&-1O`fh)&>0ZG@)E_C-v@wqa%Y-=7gZ;_|VdDnyfsv4f56=TwiYb@rp&lHpa+5g>u zwxDgm{{}>8$5gUNT0RPw$b4iX%k2BRz#-G)y^Ks3f~QXj(_y6GE82(=Y6(R8cN2<}5-$8fu;nUT)e%d-vAcr)@|po#Q@0S7$r4m2IOQTS?0Z)N^>_P(m)OkrR*qL zL<%3(@5S)%B7G?IK*vyT=)J&buu-<+DkNEjdsfbyixxdK>Hy{i0*#$@DV~7H*FMkL zGg7d;1x*BSgQZ%c_e!?1A`?QL2zM4DJxMR>0K2D6C7|auT`D@wVwqr>TM;fS*-I5H zH{2l#ZiAe7zNMekDYE)6V+boZ{9Z}Yi3U8`%%w`h{`KAlIaW6DEFaxPE)lNsDsGCk z;+pF&dqOOYpjls^sKJuMT$$J*c19MW4c;}5U?pU1sScjR{w*4s!!GDqhS{dj-kM|6 zXdeq-YOibX+U0sz#u1E39E9l6b!pD+d}}vFa#}&D`R{I=07de({LUEiAWDLnw%!x5 z(5j!HjL>ScW@xp?rD;OLGQ${CTn~>&Q&E9!gS0c z*MJSz`*W(rY2M{S(-c!aC$u29QTlV}$08U%ud`#y_JqVW%zDYFyeoWN2)bUQqMnIotB-pXD{BJ%|PT7wkaen}(Dcz$fz5 zB>9l%f=D-1=9wNgCQ}b#8zzES5Q8VZBwjLGOPSy5iu<;{9CY z>w)Mjair*_tYk!%`G_9=u|55o7PkDpMi(_i+~SwY4>~A?3$cP5{#{($c8>%#&0r}Dwl1hd@X-3xGaMKHX;|5j-;c6 z*p@i@fTsaEOqN;?g+&rud!i2++R9n0yEk0yLK)sRY&l^B7@Lzf8mDFcXjj zL{=j;@U=VC!%#ya7N|Ys{jN?Q=zX8d&DWyuC!hI9QtOT-mq{Z&CvC|^Jk!A2i1Gg3 zRe@$Lf#NwaAdNNrg8R>eZDy#{RSlnTPvT^72wO^rR3og!E*<}wrFh!n?Q=)aunL%b zQe}capP8Me)h~Ql*wEs#lD_?L$)@flCJ9N^FqfAcC+|gNgKVYnm+puq9eHb%&4Z#|rh1`SpB>|+xL@~MD@#(dH-+Rymst{{T%h=Wc}$Ud z&7~1EnK|ssRSvhc&~1V@!%k&PeUp%lU8?uOQC*{ExbIo$>N!a^qLt@U4yReyij5WQ zLEJy9#QJc}oRec2%M;JH7W;G&M?q>E%@IZ(@#N4X$O#FLV@&;lFLp{|DTbJ64# z@Ce^=zrwv=7+}ei1e3tb!j+@`^cWq9w67w}u$tcw)c2LyD9``^AOJ~3K~$P4HY%Ij z-eF=^S@OR!O+h3CvwooIrVf1XQ^kiExP z5K1o0!}l`1X!fqnx3d5<)159OJ`_nAS|jl1{e15z5G`s>W@G8k(aM2fPy9%2pfrzM zDoBfv0ozr8o2rKUo>9!nhn0gTLe~7E44#JG&_NR|qaI2AgQkE+nGZs$TRc8zb8=Q8 zAV*V1H8C(16=)e!n7;lky4R!pFajS*wlm*@FeZ8SQos~Nv?@_h*lwMBOU@X9Nm~zc zdD%}QC|VL$GB-MyVhbRbrHU5-{QdlXG~Gje-Wtp0&p+z}vm-sW$uwvoMq`S>CfiRv z0**m_Mcm=jejItzh>Tcmw?>O8&!u6x&f9V}UMh1_Kg9e_U$RKNY?pjR=8@_3gg+=| zmSBvE+9C`K`r$I>KT(~oDfO0PMr^~Cj;OZtW_W)VJhU3R>@W8l>DBX&p%7}E!xGh z2CKZWS)H!8=k)*eAH1>G_}Yd2^gawCF>P5?F$|{U?dREy<=@NfEpJGl5xNll^}P0Z zp3h~z37Y^(K(@aO&G#u89MH?@U2r5(s~TzV-=Q)06_Xvl2U^}f>OEsfwKyF)fw%sr zU_e-~&CY8Peukw?+6U4%uqe{vBpqdAw8w@-dhm@l*8d^p-Uh=OI73>j@>-Qd;gl}p z&-d&uj`0rRz8Cf+)6wP;Nuejc<;C_bR&-OkKoE9+o?DK3n7?REt^pLWOfKOwU;?Zz zVYYrHI$Lg%y%|p#dr=>lrx1(60IY&b{d4M3xx;!yN6#Ar5iHLE7LvtyQ37dbn})g= zp@v#Sym-`^Nm}!J-cPyD?W&tDWQ0kt?JnTV50(RbqFx^5wB8v5q`;kuV!$YJms4p2 z__DpK)^mKgJ308-Sa|@SuWeMWE&t;(+(F#dVakh0jV`qjgV7E0>Nw&D6d9{JQA3 z1dYCsFY6PR7T+B5TzdD-yh78R#tLwes?|>=2vrFk8P6^YfT>=jehUQgd`jbR4tvl8{OU&qtFDnLV zl=Ci_qTuiBcZcn4P+8iy-kL|6mR`7}EVaI8iFb(StqJi=oa#QqiwKZ{gs zlUTk<(H7#hBd^o5lrjAJeqiRu9vA~O10Nu(J=ux@^Tp>2wP{6Bj0nAv=q`D&>hDLP zX}TewQo%>F%_i+_!^2D#2q6w3H~*+YLGg)m=1ZV4E;-rKhShAT`}5l>$GPMn~Z`?GifR-6IuP7bR)rhUNg~{9;r5z zRGJlQCxTw?WR_1Xzi*?)DP`im5Us^fq4lm+H1KtGe7MV~iMku}SY! ze(uc}x{L<(XPY5_$u2i}>33;4)pJZIh<|OoH++DyAnejmzDUS6Heo_e!^*SI;;jBV z_>!jRDTTetrQ~sE{^m%f9={@%VUw#kG9d&mu_(PTsP2DHtjYYd${(8DMaMtj1;m=W zG&MI)8s8J+O!2!IuZ*_8`&a;}U2>ZL`F3ly;<80ADe(C{FYo941eua}8H&#;9sNrt z%9kgrS=ee7*JTykbGCRLY9ZN^&}KSCER)Szv4Gd5$y9?G+!3DyR)r0w6Byroir&k$ zFLyMfcSg-Bd%~1|A!PrRRJxo%x;&%b6)|~QON%%yUisx^S zo<0_j?k6ic5E0&armj))cai(i0-h|(L23Z76ib5<4|Zu#l{4;#3yX4mhR*XKRH@MN zsvQ{(ygWq@JB=~ZA~q^AEA%#X3#BhonT03I50?XuQF(cX$wj*#LkkU+4Tl03ITyn} zB6-v&rcf3bO2PyfoBi`=X!;?Mf!$LD6wT&?0+7`kVkt*UUE+iAd?Giy0nJFAsOn^I z{DdUTcupaT@w*XyF!mx1@u~-hmJAYQTzfw652c*RM60-Y^e+WMwsdlN9}fyI3#UbE z-fVn-N5+uNJ1}sOrv>v!ZF%m2fqAA_w*c*#&j>|td^WwwA~(A%peCI%2U;ua>xkcy ziD7Qaizc-(+Pr7+-E=sPJzsm_?z3DuU9K`8M6)H6sS7rge)X7%^uil%Uy9hbAJNBnzMF7i>l2wVU9&aEW$JaPM7s2s+pR^cTv5GjX4g?lg; zz*k1mT`n2Nj|A4xD@uadI!}FM{z9Z~U2{8v$aX*G+xMJhu?V#dI29}1p)`G_{2!ax z_pHyT2=Km3d)3~6!^)u}M=>BJiV#EW97W0eoduq$hxVh3O)Y^`HoO2-TEs#WAq2iH+}) zCcg4DMzr$qeDb<1;g?()y<%cxM(E~)D)O*Ly29VQ?O9lX-#^1B@*y|$f!=mrGQHd# z*i4%}Cq4jHfwj=-Ie;AYi|s>zWxfXsCZ$cbgjn95Ui2)~hZCtA;q$l&E&3%DUF>HV zqJIXjp8iQO%~)b)=DD`PTQjbfjv`pk$E&bpNfE}vXtLAuvwQ;iti>4~BWu?-lfDiE zpIsgy&kVC+rRwk^_LfDDIOpm&`xP1Ke!lNW`CU#!84=)%$EYF(JU1C4{8m$?dNYph zK_i}j&w#B6)qDrJ(A(X>KrNej=$=E`&o?jfpqZ@)X&WRLc>yO3yj=R$GSXB=VLjhw zTPo2W5l9&sTO@}+M@|}Rh!rq_eVE6jt11Fcu%~Z=R+%ShD4csHwfNUS48+|PKPOP{ z@5j*BZRwf@K392b%TK!~Z(#sxtbXwgEM}t3Fd;TcL+--CC&m+tVO~in|8)5Su}W$#6)CnK*6U+Qf4>~U79xCqn3JsJI5sQ zFY+)t_uYDjs0Ms+QrB0K0iP_Ah&4+E1{%|WCjP`Dou6Qo0pq~t;&vU@D zsOliwr~BbX5e9R5*Aj-mfV+gvXzD6tx^ao-70QH_e3Y};a8WB<;NaWd1STWMlI3wW zACvS3P(Q-B2>ITVa{?tMqO(gbI^O+Y=LM<=)P_c=#1qSYEvSZQN?r45G-s&pl&Kzt)(#s#peNGmf`LI zT1MtzQa(G8oapb>y({zn;##@e!@9RTJIhZm9z!h9oM!MA-(O05^WHPr$Iv9Bw@V>H zHP$=mkzp`Yjr1sYJqyb>YY7Z`(INkj9yz}gV_Y*FSPel7D@X-p3rK$W?p0~+9Ge-9IyR^#7oMrJq@&*(kAyLE&&BOIVS)KuO_j!brO)v&1Q) zgztPFP1d6^bk4Lmf;82z-o-6b8QU&1xs1h4xRNE!S~kBn^)tK%9(cm*pqQ&#^Lv(o=dyw6>^WFb$;(vGl~Wd#yi4_H30)`k1x47@*HIHNjaivWvN*%2 zmEDDWu5_lGVQwMm>C21lYv*e~F0F{UGg)J|w5Z1k)#7{OsFR+(EFvQ_;1;x<{P<{@ z|NJsAw-9ZrMDe^|Dw~;l>1|NSE6Eu}?-W37I6+m-=k!t~y2K~_ zM?7U2gp@}U=#>ozAWj*p&RG@A0b@}w z5IJ^Fu);L5lZC(UDJt&`>0L;Ft@{8r{`xQ(r}GRvY8mJ`QFfpVb4rB;Pl_N36u4sFo$ zPAk6;^p}mkoDqA##J`ho#DhlM(#7Zf@MHx~>G1TgY$evH#7smvgCKU33^av=7@)4#3-ClL%KETpZUjln)!7{1Fw!v&WC0(;!T2U==QzpzA z71-|x{?E?y?A{0-M9+uao6hmIHt{TJz{29e(&D}$19IaAh+@qCEetGkFA{w!(2Fn~xsft^6+bNe0m&O5_}g-`XLBDnG#GgQ}^rgMz89%gAQ zq&6r|u=a=HTayJd5}Y1KzbL9c;#ItIIsM%<>ztpr@pT&(S|NE_G$qBp4nZj z9#vnIoj{GK>ogpypY}BsbW2`zfZ5m~=+X|K#})ismL{HMinj@Uw7bGee&ho78{K3s zW@^q0DNrXEx++7X45o&V0mjPQMZ)hx^@o%;{`~)6qtp?dA7*0Ds51!7c?JLsxA-&O zrKSd086NI0{P}`k=?(FNL8A*TdwMaNGNLK((D;?6dE8Evh-zaZS3#H0^wMWcLpIv# zz$A^Ym*Qmf9_Ry2Z#mzFgm#)tnt>sPB}&(VZPYWr2KXu3Tyy%dNi0qG?(TfImTcyv7Ric9A$WOMtA&tGcaa4qTYsC54oY}$C0v?lKAe@pX}et$Y8!ODVx-} z&u)*-r^YU7(t>2zzQ?c>JpcJYS&BMF&bG#g-#ig; zTSRI$<|4*qzMPp38LsGm9jJ&j0z`)el1j>9HXE9CtJ{~Qq+1mYMMnD~HD>xt%=n&( zO*@T7vsi>gqb-{CE74(~o2gTc{U}M^V}Q)146ai~=Ua^(QXvPyi1{(}_{fd|T^jYk z(j`r(W4JN@-@EdaXyQkfq!g|!NfaX}@)@^j-H->)w+i*9-m?+oeumLA%y`23Orn#4 zz$7FzQ@V3wHZ)eRm84`{CGHP8?=8gGhcUT}zY8;- zQJhk*v|v(sH5mTZY|sY~`;lF|riGLc9GlWK2am@PU~y_V0iDWK6C7-CX7~MkvJn z{#AMK5y0-qfz|jvE`tJmUclhB#H$CZB@Ge!3_lA~#Aa`vG6QXz4RCKj#O&wq_4r&6 zOO4Y_TWq)%-liEJ8$&PYZZ1Wn`Sfgc4+&a!9N`6f!`kS}0CQhT)y7lSwwVxeFYA%5 zj{~tW7dbQ1!H){7NY4@d(0rD$NEpp!a-9LV9P(Mb$c^@gOWw~t?(*wmI4DVH++orJbc|59hz z>eM)uv{CKEJ`{6ZHX4~CH?;G40lsJX@VL%9sGeWJf(*L3FoMXUgl8g`+Oyx|MVTzb z3Y1I!j?YBuRGdOU&Pk$SjN$A^9O5N{W{X1yvOEZV`z=#uWi}%a!mB&)GOjzETto_v zz@CLCl-O{M(@lZVQlK?p)`x3s-766E;QF7;5Q#wb1CdS{kVy`W{#~SUdwW4-MnGnL zF@ipy?DHG|JCH=D#5wN0KsA5fWUmacG+G_mK27&3r~w$;&lf16wzQ+EbV_!w_L|r; zX0MN2l%PL{Bl3jsCtV)|?XpwT zU$1CQmG%%k@8XrxL*$0;gT?WQs&UwoIHT-&qJIprz!U04)R|!e++69hW|)I5%cU2| zZc7egX=9n{V^NhQ)1%|ON0nJ*YvC14X)fdo@(XGL@=ov-_i4J;&MT8Draua14M zv@}tZ%vs$O%QjF_yBs-lhPe!g=rZS}IaOZXfOcl%K-GLZf=0uVAU3JUh8*`6_{rin zJ6I##0&QOyc&2?xZ9-3-r8nA^4WL^lml77;TFqV*vXZIcUn1uKJFL(436^DR*QmSj z6S6eefe)4D`S+FvR#cl1yQ=Cr{19y0L$yn;H3wnP_JvPN)tmvK$Na_bje*?Vp+Ku4#m_$A2StVW1dm4NvqW?#vDl|#i zcw99qD$sP}hHu5t8nzSZci@8d(IY&;V*O~cuejj37#k+PUm2{(MvWQCF!`>Cj0Ey~ zIjJ(l0Sinj^ZPqJJxbaiqzwHOJZS^ik1*-FWB{k5e$mO}sQ;7sIeldFO`-l7e5|#UYm90Q&FF(kC0RMt z!;oRl8l9DY*Fs(HJ75ohoe8_-wxjE%^kT$B8d%V+SdAAfelER3df_W2{~$Al60b|! z9coK@W$caoTyj}?wVS5bj3KEvB04%K;*fMbyho9ezJ#-w$*K$W%jH-?7-s6PJe1!c z8yHQ3@w*5qnYN*tuZAM0Ltdy~_tR)fh#KEDX(wfQAuq+^FF<*ZTGnD}jgv0sl3Pe} z6I=_`+vT~)eZW2EguV~Twq=*?QE@U7jc5rUP-ZF{)Kl7`DS(@XTJ&%T#saC0P_PHF z;M>3%a!y{a91Q8K!YCVJ@w(V+4^5-)-nNWh#vVhaG5Kg#T_wO8>=BpB1qu8D2fpbH zc2p8vyBcmA@}nw|cv-e#1(xU7vX>q!p~wh(o?$8X~qL{Z>L zx(sAYlmL$>VHv11&|={DVd6bD-B5uXmw;Qqi-0mEHM8e%>nLGKIqy;`GYinmwt{~Y zDX+%f>sQXEbcPNW5#ugkpMG8S^Hg!D$=KCU{d}!&EQ=39fBlKhn(L@@pIu|Ig`H6L z>%42TB#dT6{@l<7WV#`9(euTNbdW(qR_@=g9ZeC*8}k{XGc5~7Sth{=38AY2tbfSP zoGU{&<39b)P#+@H^P5H|dW8PZrt9?D`mIehhG(VgYAW4|kz56f7voQww_(F=cjPWX zt8KEuHqg$C>@2e`ynmt-B3}8e>Cn;`n+an=OyV?H=azdhy8a({t6kc)yZ^q**Okd- zYGwSKbTnY846^U6nnhWkvR$w-Xh%uC+bk>;+Hv9Cfms_D<+luBFa3L$L>=k5j7VXvee_w{+t;Wb0%lPxAL$iy=)er5C%R`-}yGFUG<#*`?DZ|NixFSL0O* zori)jd{Yz&Pa1t;0Mqr{43~KB@GRY#H1oZe2v9_hyvR9CwgFa0lqVl$`|`pv>HiDX zKLyg*y_nerQA9C~zcNcxUde!oU{>^9Nmiv;`jk%Er8X63KZ_JeB{jBG`hrq~9^B`I zlyRNHL_uO1%%R@cm}ZFe&v(&~gAK0pinwKE#bIY%hFK^$q|w|*9va^Z>DOFVt6-+m@vbG*(&woZs{HCpB=a_G(J(o@PTc z3Vz0hSRWhTi)ubgU3>P$?u9L)ska03>*&>H@>%}#UwhxO>^71t8BWvxe=>X)B9Q(F zuR+LnuaHjq?2fKSKnd~wm}18oo~6X=_KX!M=mBp ztU#j=bNwvI(g=E*P*S|$ofTo#yZMP{%?BQizjfXK03ZNKL_t(o)?Y%|vj%OI76LI1 zdRwRoR*pK?_@VZSRwlO@Q!aVWfCRmE5m`*;o@e<)J<~vB&D;x3wQsp^7QGOyb$jwN zzLU$+JP5VW3WxFHHNR6Y-U5rfn?OdUfDBEZNuW^GVsI>W^6r9{?rt8K8T@Z^6&y#R zbnlQ)=WB^!M8u_V)1s7*ko=W`I#a=lWy$SP?=`!y{iPX{lE0N}=I_n;eE&Zo6tSeF zM81mAEX%HyPFo4hfotM&i0YQ7ia4ZIy`qk7htH;1yCt80hNRgnd!?NsH&qR=+DWZn zpI1_Im|)8!l$D_5*qP}CJgM|zZ(=RCDxIHHjE|T{KtnVaCp4v@F82t5JRCif46G1H zIh>ZCT0UcuB7%AmiS+d5--b4HwA{ji(Ufdh0Aq@N6|r!u-_N9YzfFP+?#TBpEts1y zeZK$q&nm);Lh^C%Ecb`*S^eg)C;m8#isSAP14D&LY8hA^Idi&f^$h zrK<*Lae84-PV@E4_QtLgGq?Y@-G|PNE_X8J6*49cm1x;ejU8Cy+KWbH^e!t3b3Mh} zO1r5ru06$97s#hdzb+s@lhUnzj&m{Y&Cz&D^z?KDFHIy&?3$<9An@`}B#)rjCaPY7PdGRpwt{;5yuj%H(ZaY_mU4^>1V6rrVv!#529a!BOww*(5# zSm@m?6d9tyWyNpCwjRRfp^-N-7b*u0H6G`;t3k%2-&38` zjG<8sn{uN`+2mM;PZ?}!XQvd%)*NZpR8WNO^I2WV$f7jUT=wQCbrYJ~6b&9;*bcQ1 z2BN8S&`Ly4$0;P@xYntBLQ@PVTm3oQWCe}C2pFZ=o1NXQL6mO;Bx=^<`Vg(PKp8T%kioh!yP|Di;W?#Ys?XVJ zqxzGdEb48N-;Li#dEM*Om#>BO+uCYmYF>0r??@b$wkjuyNR$!(RH)*WPP%>8mwDmZ z?Cp(U(nTk*x6~70y{?t(9$6?@5WHSPTy1RkHG#J91f!Jp;$oxHG3Y)@naMkL=wOQe zb)edY?@gePEZS!j1aybO%u3o&*mWoqRJ=DlA33gBCZwn64mKmQklh;4F-m5l3Q5sv z^M{zxBit72uv^@-?3w1vGn>P~9c5W_INq|Aj4$7GGjlkDPnI_)w9x?}P}xICGe!jO zQr@dV^~h*+QAzVa(nQa>_-2|zq0bRvHiI5ZjRZ!H;q2FWtsKU#O=UWuUg!Qjh0670 zN4J7f;U45 z5;>sO`H#KyU6UYKKZYPsnOQ4qCegVyFnV^^)N~zx8Sem<}1yK?qw+VkC4a$kOW(YM6VDO&H#C4ErsG1j~o(=OBF3S)ePv?o**OxZscxeXdIbSPd zAlB1D)!8Bm@KW0VuCoKSJxhax*{SiNKQgQNJcY3I?5cIzsG{hhlMZt3dwQ(bBkBC9 z7ZG%Er)RlCqPw4}J}BvRs=uTKxx}z#kq3BQk%mvA2LXuMGR!BV6kkb!+B+N`A>NO+@nve?@^Fmxn+Kj3#7L@~NsjLd;7t`iNl#g1A z62OJD6$e+swY$I!VX&nxY zm9ezZMdv3PhL>T^dq!7N4e?_f)HVJ4@Mt%cVJY@DGZ!q~F@*Gp7wdg=dl6%oerwCrfY%HqUiI_Yn1Rt}A+hfgHkS2G_s_tErXr!3(4rV;M-@tLaaD zy;*=$5?8GWlW?@0)v#i<_XK5WE+F=X!%sYCZpSut=u} z-MO-u&89T3O{rYsl}@9cd59mYlW-KmlT0T}1Z3){ws^B7*y=(0{W-S)L|B}{LivW? zcA2io(3a!x@5qQlpd3H-&Y=P;Zl5(v)P&rK5;r`5hk@!^Eh+FptSgxG>{FjWGnDS87K%tl%F7`0JWInn;I?)QMGiCNnbd8wHnC7 zKJ}pGy=o0LW(`U3I7ZNOsM1_wM$S4~DtR(sK@>%^o)JvgYQ|LjrP1*_?}hJXLcy2U znCb8T9mNyz?)=gk@)Zd>3GK8&)Mj{*A;WHVF+y3Ma?7*e&)EkuTPXY6kY<+GacuBy zu(LHyNm<;@aJJX7)d9p6FwAV+Ui*EQe~VF2vx>wC`Lh8&u4iHP!^rwrnqML#=;i^Pnk>%LwPGxPj5zU{`U9ZTxY7g4OB5JB>a32<`@*8C|3Fphe%+?3j zIEcvs8Md{!F$Fk()ZUY?krCSmVq_`Bq*Xb8;;K*y%9=D;9AjFie3~|RXMPvN>XwmQ zP@US{=R-_ddAlaug)6eCJs#2%t4cMCf*dglG33guS)AfEiku_@roQ)c5=0TjsPZ~B1=)&pT(=r|cL!4kln6Pi zu)v&ZeE<5%tQ`@LTEmK&&)qm%E(N!|9tk`OzVzzi786D;YnFcDNyAX@b*gW@a?Gb? z=Y{ALEh+wfPeF{w9O?6hOu&%R7rwdhKXHJAJuNKjZZ=9<=s1`Chsqa^v(1v2 zqZy4(z|01iNzk!(nls*1bIZ|0@DTixt5rVM#I9K zEAi8an;95r;fU80{>I9pOd2?5ij2lJXn-}h;`;T@sz-^U+Xld!lW5-Cq^NRjV6Y4h z65+u9?Z*4^1zFpWw)dC^lrD)&bBg+W8|?~9GoMtnGL_g62W*t{U-~E~I9BTbe~$1H z1&@V3iFHk;F%LVX3s^K%%}0N0i5h?0!$(=ya9-n2C0%F%9=^kju^l3b(Z&g1YrN-R z7dnro*4|S_Gn{%vP2)i@$s^?AUP-$620?qV*SYMC-FiG9PrTxf7rOcunACv(+Pi6zelLyLm(TFV5Zf&d?}pHn&fM_qd$vmjE0dO z%?Q*RGt-Ac1zMMz!;BgUMR*t@@Gp|oz%k?uV#Rq+&lnkfQ}kT6Xn1k&{9D_;9F%Zg z_6)p<8foyK?|7PrEzKx{%x=zWQbdjBO&CkyG*m?LxH0Agswuwn!$(eNsv3-y8aGtL zabSUhy1Qi3N9+2GG1jP_-5MtQ(9Q1m=HBF=&v9Sd7596pm^^1Qo`&wEansOr&&^p9 zqo@|c58Bd7JjW|M-Th#1#2P3UbEz8ROhP>M!pv|Wz3N%6jIrN&Kl0=ISl=I zDKKR1j~z4C^ZY>ubPxJ3rG=H^u8s?)wsXz=bix)JGSwQR|Akv1TA^B5MIT_!z6 z%44)PszJg&7>L*J6DM_OYU8c6ac|~7W~Lz_oE69&Mm`Iour`P~AdGqliCF6(+~Vu{AJ)u%mucjVl8ADUg$L?z*FGbOaMP74h+~5 zQcNNmn7bHG!MubSX?WY;_Z6|RWJ*AieC?qs&+mzp(SU|iIL_oDP)sfU?dBD}c|-fA=k|E~PQ~+$qX{ z^NekxD)dZ29u8g1&xQ{#0obI7L>_4ny3`rmPwr z+s+tz;Nq|{m<)ATXq@)HbZyaT?6^}AGBKJj1u)qYM@i5In^ zoO+I*Q7uxR(jYuYFb@?QD@Ad0$E44+;<@_pY66Tj$$ZhPx#m`zDA%+w8Zy;sHlp%| zLhxlT$Ti6r&c4xPH#>SgP+NN>42<15x+wJ~zBUa5vGUk;5JAO=o*u;5Wl1rvHdB7g$Lnq%K4K4 zRf{U{6ru7(e<TGcbGoH8!_f%zt`)q@@5D#H9MT*siW-2aiSBk{?&+g+ zzS_o(TimCBwh#rK9`US#<#4osJ9^lme)&kpmQ0Nub~2}kutLLaT?&bCI8nEU02_>& zcYYsW4iuYmG#=h%fke8r5FREFt1sE?7zm+-)sgdoaK0s6)0`&8t>(zCW}q}ZgeY3R zJBrHBCKG0JHxcG_guHjNj&zKnBT<$M6JP3Q0%^(w;uPPfUw8}*AEBHSXZlbye>$Ff zM8lUAggm>Uo%4bX$8#T%1ngrp*N{Dyq+m=Gxir6i@LCulq~YL#gs8T{ocWZR=0bFB zW{jTs6!9zxy$)E^g>B&TzvnrHyYNgHBU*7NU%b;(Ai9g=_Hr66pYo>80X4Y|?%6`5 zJDbU6w&YF$qw&h`bESNz61{q+p_<4Yb>yfP$xnPRkXVXqU})=B^AqqnYsZ2Rd76Kt zaOj%2grdsI+OqRSEc4%rL7#93jTJ%nlfjsyX|ZqUbe64YLDD{Aj-Z1FGk1})E%i(W zWX8S5n(M0QJ3SlXJ8&gq7{H`QwPx!eof3OMNLC4og`vnijkbH-zbufiT3^40Ev-tM z<}D?R%p|G6Q!Eg6VqnxLmT3$a@0cl;iRxf0)&t#q&SFO3r#orcAd%ZMFF7>q{4xu7 zRCN7?np|d1cw@yJ-P7=Q6KPpT%=7m}Y;I@gM58te@{k7q}r)ut; zC0~{-Zl?yV=AY(1weqd#?d~C4 z^gd^EDse^g)jF3nM!@)xXm{cC;U|K=iptyL+O(L2r(wEUQ-L4P*FNyXRiP#tN8gFEj3nz>-CmxD5)G{)DJ!>*A<30bF&@g( zOnX#5vciuN+``HH4^JSzi~eUy&SRfRxJNgUN!;gkF^F_S;dNuwrbgKB@&wd#f?;L_-!Hd;cQm3UcsV;z$@oXeg;2~)}C8AjF?_ysAMAEZH zM6R~1=u=&tm@=DFbT4ldP53mQkMo|RF^(X>?U={cP=@s#R1BWvD}|L0Txpa^o36=5 z&_QuW6VXwD6quWyfE^5h=Y6ukH1yP&&)PgbLzWaG)uB7{0bCnqdvOj|V9sVqchL~e zyLi<@PMeG@C`TsZL3xj9|0ReAqFCgL3hX5HJR|+ND7wvNwVmrU|82F}W!u9~+BDM= zjP*P-D#oNpJ18~FAZ+Fbvw*74ag9+T_&|r*C`B^mWLO}(=bTxx6fm=SCA(Uoj=M-~ zPY7D*RAy8l2-;(3{S0lmlB8PLku=gVYMr{@sbP+1rMbQC-!u3Qh!#^1MbVz!?vg&7|sw{OawRF}2i0R!D zMO2hT&f*iB!2D>&PU=P~10Qu=<#o;CGzcGbB{kHdH8dmdTr;O>$m%@D`HIlp1cIHI zt5lT~uf+7&EzsPt%BL$_h<&hP>h9J)c7I}$E7Sxspa5j0xN!NA-s#}GY;hY!Jvx)HNhOT!J9C0rG*aj&7z zeN^^fkV*|g6myJ% zAt`Uq+%I!+#>LIX>cW(jVp?Xa``(oC#uCJ*sM2gU*<7eMg03Tq39a~4+b7MVQdyct zr(Z8DmnQRjR;yFdRz0~A5UR-*D2ov!ueB{y@nq&=J=8I{W{VLtVaUM9X67`TbkQ+o zM}*>i!m|3fz46d8xDj&N=w#J|$iZySap>|st6m{GN!v#}MAh^%$VB_a3RgRNMOuQg z3e&vNDd}tnw(c9eOR}>EO&hYaV2=I_D>XLWa}(Gv4ti$WEw;vo&RI3C3^#osaMtwS z=k@R47O^)4-758g@WHPQ7U{7Gk8W4>xfEj$Xt9o`Nvj6NAUn}V1d5{+)u9jRwFx~g z+^{o*f?R5__G4&+2(+YYr8MuwsMUHdi2hT*>rc@8rkXKCeW^^Q%hoP)PadbSPq5!@ zXs057!YZUc$s2PA2H|4^HPO+j<=i$ao51?=M_MoJAcU&~&w3yI}Y;gp8E*&=Q^V^CH?@($`ifCC4w<8yfBN;K9ha|LDc9$PpQLL3ixtlvU^91 zZBy#gQrpTj$qN&2dKb!>6}@~p;F4Y^$Vj@#Zv;pD;y5S>hrg{PN8ouv}ChY4iw zYyH^!&pBYzIu&km>-12X*6Z^WJ6aU%rlJx05hGOw=}oPOHAkQ)yGHBGElh;%h*V}9 z)WkYOoDCbT%wJ=v2bl&2V|3wnO%&Jd*z%&*3QwQkDXK=TAqK^CHfk@0`XtXbQT%ms zYG6X8dCnLm^L%Y0fYq~-S&?|7XwN7`&~9h~4ikZBz+s{uhtoMXuB5xSu?7zhr6$5_ zl`_mgg-KdIT6d6$>Inv?s@>km?9mz`h140aGRfqNhpQ>q()Q z-m6GUg^23$f`Xf!uAwUsx>nF~V#NG0Erp|KO6o>?#u}9bNkN<4y=Z&HRcqvHOzlKj z>Dhq`h9EdOxta?~7Y;A}^q98AWxAu;aD>6{AJ@rAO+~b|;YpSb1p9&X#{#)Vii9l- zLZ^0RVi4LBMF(t8f}3}6yone=Bz?bkq{@0xI5m%^mx+A=j+~Y@MTa-cjsoTbRTLxB zHLBQI_cBcB(rhmoqf6e!d}l(uPu4F+}eeV}M(h9D4~XInx(_}Wl+yVdgsR#`MZ&FD40_x0+F0Ff85!x?fU71Y9v$WH zLtU& zf;QoZL53q*J_i=6NKTIs0ew(Cl@ECDXWj(cH?%&xFL{1eJ+Ms}I%;b?kHN$iy;ORt zVH~taB$y4(lQvJaq(1cH*eQVk03ZNKL_t)RPw-rm26xZ{X)IIVS*T{a*(XJEv_LPq zoFE)ArPPYxQ(5%6H?;WPCYjhZ0=4e3aDdmqfrYwOZh`16;3eC87)aZajy~X`Er|S~ z8Xj^srfn7Y$aYjG{6Q~z3VWT(lyn-U=^&S3*J6oI?%}4O7q_ZqFgtq003?R> z2(yt83Yx#nDX>FYA2$9KLaW0e1aVyqEA%x#5OKQQdz7`jIfE_szz=mh&zNUFxMJKj zvjH5(0qxce3PoA>{4Ft^XE=GRy7So{v*Ax_l{$U-UddvgXT}J&c#qU|C>?ElST)=l zOE-4N9JRU+s>|8(RE*lng*8B@=UEn6XwB%_2%{|1&tV#P9qx{bF2U<8%3}<|m@bc9 zGSqM>sdKQi&(Ap-Y}$9KniK_{~zvet+-J z-W;Iq`liE}upCrPtx9W^bhB9?>^1h}?O&%jva{eDtm5*By1VF2f0D)1dc1q(&t%u_ zntq`ZJ#EUIQ!G>B$&~6Gr)>W0vx%2}#nW(U#@LIX%Dkn~1`7?+KcBVS@_@G44*I$h z7tebnQa|?;B!e5Z%06vO1ANuQ9h=$>h^yI*2uecncq7PBc-JwY_274^c#;W(WMH^OctZ z`mKrG@X>9TcS@FNH0E|UT1Fxzau)l)B`G#{6SetvyJ@bYm!hM5|Mtl{3>Rt63sQIv zocK3nVK`L`pbQn-4h6E#3!B7J)MCPYP>b%=RZNMUyJ&LDV8R8Pjcd|PPvB|rgET~; zjSUbM;qOza5bTe$12s$&+qzWE9|EgO7jb01m5&|WC?%RLaaJ;$_srAELd-NIo#tAi zdmm)>bEvfnGHgwov!+!tk1RnlXb9l)8tX#jwKA zNd4}%!Hps$44UeMcpI&&tWym8FxY5vVdC;;JC-)e5WYW=0%q=eIT8gyWmG9bl6|rv z9L&98VX08FJ!VDF+*C$&dK=q7VjhCwX*I7=!YFzdHz63A>Z>c2lY5>1eHv(Qn%s3% z%qS418e(=*)W?);i8KdqyH(m;Accoz^T230RT6$*$){yRZJ<&8PrA8Af)t??`+5kpb z8_qW-9t?{#zJ&6SL1!aZIsw7?2p`}H(W~M~1M%2&I)tc=ei#r_R-|0MABGObo}1Nj zH5&qkTMBKHI1VeR^FiBO1|--CrrY!%E45=C-xlFaz3P*khj&Wage_*;gpuo&pb9`P zSJVw&^0UdUm5~WjI=oEMTfUhY>A`O4(DOpw>?RB^RL6Jv%@q`=zyHQ10r)Kuct

    RT73_x9wI5s>xqi+~JtjrU$8k8J$ZSb%I8((y z!uG~`N%)-(v~>h=%JSU98kZuoG=J#zj=tW~jy)6DtM>tmlv9X{ zW8Q;QPc>|ZFoLjqj>SwDZr{szzL?H@W?0l{fjWwwp%f}rJ~OE9-}}r7*WCl2vCbJG z6K^Uq7+F$AYntwK&vm(L<~cVI1C_Ced6)$~7iuTg=rb^eJZi`j^ipU;vcU2iGwAG3 zG>w?APgXNC%b>%=^Go`qDDY%mXCfWtp36_@y2`RS>cZtSVF=F6=c=pb)o0SR3079mz$NJjXa^$n}8yM5ze% zb&Tg;H4H4yGS}wGUsusby^m5LJ{+R2rS?Y7geJPbk@0RA`<^zsAopEwm1~VFtxY#) zETuNoD59cz;f>E|#H4>nTftfT#M7lb=xR&=4rf<;huTk+JYe0jB z#b+ygUYc+$rB`s1Q+o&Q4eF+U;5PC6ffT0QJ6%U~|&SuQ9oC&iy z5hC@1cwr9A=Dfywjt~eUBf;7c#C#63X}y*fcS`p&H7WODWum=$v3h;pXa?hg@1`=C z#n=bYR$>hxcRgQzHgQAq#&lzCs2gDpVd-vQ*lGZn;mXeULNHqxG}wjtvt&y>*NHv# zeGFmun)QRw?cofO$Bp|#yWft+T>IghEF3nO_|fbbEBRumZHfmh^Dlt zO2)HK^Q8e~GZEKFdEdVX-_;dGkV>u z>&|sU8#1%3bx{rd^x8(-;zt(hnBek-%+{6)8Y@ecUxS(!UEXDvNCea z^kt~+qgnliZ01FjjB)Wvgt>8UGBbZn)my&x!Apsz@1ts7WkIW!Yw_BGBz ztMa+Rg2ZDvBj1r=F4WV$FIzLWx6QK-jNthVhKJ_ob{eUM6i!)mNbUrD&`{qUybuNw zS>Z~NO#~$Asq>0rUOmS#y5?qNb2n|#jF!u&`ycpPUJA`QL5NA!!%A_bPy8p*v}i~( zJaxRDZR>e1*m=}P%6eKHPZ^m{N`>B^VSfe;%_L5S8BoutH^a2_a$CrPr8WOIGd_5lN0MsTAW#gI&7%Y^9(rYp?&l3+}qzh`VArAQb;2TSE0~2 zQZSB$m}Oh&QSQa11vP#4XP(bF!g`9TtB9O;2oBU4BAt1%P*2B9HHpBnd!yxRw6-KI z{vt5#h?1v0CGhqQ)SZOSpiCG^9F8l>fyAf{dmZjO+pf_ zcyT)(AHFO<$8kb4#qp+0D7JF&Z1?TJMsDB#LOBVI z0^`v4@WMtZl;1yLg_(yCx9M^2B|5A@i@!I@+bj+2Z(15(5EKSIZJ`Y!7@jqGKWhy^ zsqwWE1_xB#8G>FEXP{1nbBg&Rg}5x|A6+ZG{Nxg_yXudoXv}Rs!(btt$@H#QWDj5p z)!1l=k^8i^BAbh5e#Q;cnx&E2)GRyU60d|zKd;C)W10=APIHqCjNKdM$}W4+;Ju-q zMB#-gQ23s6`HG1B@Q)oj zQoQlDJH4&E(lea00Bw_xNzIo&fDPz2epybS#sb*Lsq|5#WoR`T)96J~$gtBE)ce^Q zCZtz?*qAXEHI@gQUKkRbwbD(omu49-f5#2;p~XCoZ5otcsO}P+KqFuppFRj6lohXn z)&j2tiRX)}C2ge`fYrG*E@ZZG?E zkMrO|k5W|Y?|Wz<*J8E#GD&=zGrl1cmuOd--bf3irlsaa(1d(Vte4s>mCFtjvkJTU zN;{iMmp#1h(29xR52D{n^CbpCXU*#@&x)9woPw22@e(@b1C=w5Msi<2RWSOYFw#5| zi~M{^Ypa2w2pLcDiAse2d#}F}TXd(B%?`IC{<)1;%)L6r-WwYlN^07iFRHF6o6+c) zC>H|J3tf(;GA|7+xzKwT8VM|DSoDf?4c6P>V55j>==_w8s1G_yCxi*hPcE*JfxL^E zOVW~3AE*ughr6--*^`?ZJq!5|D`jwr_yreh+*xTxp8UQc8nc>fJ3{Gu^{xY_*|4UM z^X{s->6TvhP05RjVrwDpB?+<%S1!n$n?LZiq3U)fubqwBRz$+QPU6AN;&E55DIBvi zW?r;@;mVXqMd~&9GVJ4mkaGRGike8y| zZ3}NaCX(~mXu^LtVQqwYi8Wpkprs)+2`b&m+!o+9mXTmy^76Tqt7uSHgzg+3|8+hH zJ+I+e*wy>6=_fcMQ#+h;U(IA;yRP%^pQMdxaimn7r1)gGVQgUJtUl=sybugaLB7y6 z@&nS51-v0zG8wIO_Pgkhg3u0SNxV(u!gWHz!!|mbeq0YUqH<(wJ3WVzY8pI&Rj8RX zOAG(bA`ELRB{kO&4JBnDE_v$d8dXZ$GgW*V%iN^{XvqSo7nU^; zgPYSJvWlhD9buV*I>%b9OoQI}nGOq1(+l%;7oPah9Ksq@GQ_RQ`Gz!h+*}zAsfLMCpEYd=B$f_4CaSX5 zA=axEa3;km5(+C6fJJiCvk0bzylQHk@val4Qu6N)g(zmC^s;7i3slwuBjnR)DHCfN zxh$_L*&d-glZdr=`EhgSoRy+|Laq5aY?^top|3$^7xm9t*RVnU$~|lJBloH2oFWS+ zbo7iiOtG>QQ~Mw`w0WU3uIL20P&Ok_c4@qnjnDKZ7g!dIuzq`qRI7;iMt7XuYct(VzM7J@Rlk#_&2$vKR+;6Y-FOxVOT zf@sWP2fj%J8s}3~^J8XEo7g)+De_^|0z?poX@drq)PYEsO_CFQpMJA;pdvcjJe zyd6yNgOjr&F6Oc}PVdC(VV}?L#9;>u=%UhY+_EH$G7zC+F~E^#l}{m!HQ1ARFM()I zB7cH$EIs>-8%#Le_5^K$UNG}UP9!mS;)puzEkWLSaa*7!Q8(1h(8 z;Dn>&Co~AFGmQ-XKI2xeK7>?@>!gb=4pVGI#wljI$i9LbDGeBVj_h=mJZA&#v`BB7 zrK#Qr9fuSFs()f7z4WlkarY~xG+EW1gCO;g9tTHOGi}4%5BUhOw$e@ zaad3mR)Za?C9O=%TxoN7YWEizq;Y$Fbnb(#+2;+eAJh&WVa{lB>2Xp@*s7Af+0-fX z%~!E4Vj>vM05kr6D9S#om{vMeg`Z)Z5&#;VuIP2jsen(h?4YT1-SK2^^D2imj&L{S z2<8bVjA~jugz5;=bHi3+0o?|J>kQqjuuq39emo{Ep|i-Kx_ng4{?q;qi>6L&9)n_` zqIdHoM@;%7R4i&Ac`gL*lf&ZguU^v+xKBf=9tvsNP_s?GClL*> zs2pd)520x3be4;T&Ldkzi=ozSQ#g3SEA|3O6NNAH%`lAi~?W^P`&*7W-10+{e@>Vs;S^ zs|jnxap&wDtV&roNDvEZ5%iQybp8I0O_~)($KoYTS$y{Js&2yY*hs-$rds+~`S~i% zpSn6M2VeY=qYbQSFJHJkMEn-}iC~Lz8*Q>&dl5?j{$cQ99kRh&6C>!B%jTrAR!>-vbMg6qOK_ zA%z?fP;A2tI&G{C?#~%#Ty-vu!~N`Pqnt2A#53Sh$?07c%EGi3!X}#xSUfhiL!;3EBoyV%EI;uw z^+0nMpAA+8=~g`@GSco}I@U@aaR&vBThl5m7X)6L(%pq7u{4bdu^q8Gr0G*y3Dr-$ zJl@Zyl>$bKQ&mB|A`amgqat+Y>~Eej`W&M->Zj=c@AhCIZHqM}0WH;>Y-mIjWu9*YqDebvL1w)=v+ei6TFrb7|$Y5{aGkRDFXV@ zjGFPaF4IV=;JBO$!IMWz;8yTio8ALgFnLy|sn&Wm8#?GB7aO8e(xCz81N;slb3n6riU&@`Hg@=<8(O4s~O`r)?TccD+A-92bJ^E>SM zU!uB)R>~I>6xLo&Mc8=704qRuOuLF1IG-r?iyn5mWJ6v%x`|NTQz7MH9lUCdjm47L zQ21*(U+#a$C?$KbKR!8)gEXVqd3ZNzt7-u*#shO7WMGQBNlx%Ez40J2d?<_7aJBVD zvK^V{NACWjg*3J(SrPIFu?Jkioz)ZtS}<$9;pZ5GY96VuHHWxUT&(@whrLIj){ z3?}qB;TTMZ#ofi^H51<2>R<*%3&NjZ4LHjKx4y_D7*Fo!5mc)~R;}h!>@J9%Hi8b> zE&YZ)uMzk}cEh6lV^#VmPZu%yQ7V?|D^gX7374{PGn+FtGW%h|gMd0M9IvhpN zldG6M!eFzYbB1e(p>puo867V>(~1C;o`op~^wOY{_)Y-DRP=xYK|)X6Is zYuWJ>U873TIsVuV!balF6+-SMlEWKd>Sk*Xi3+5 z)mdg{7vmXTK^^Y&=P?%~AT)YP2ZQXjaNUwsEYyL;cqWVv8g;GA2x0-xk7#MV(QL{4Ag~})OL5H0Q-IK2==9*`GR-&XBC=*() zfuo8VzPTJCMajy7*lh5BP*O8%LwUT4Dc@{>4?RC>27d$p*vKYqh=%22q6l{{p=wBL z`VZzuG1$iftRF)c9;6MvHioIMSz=ag@G|9!rVcv?6?Qa{l-E&)3)8Av88%WyZq9|u zVi5)cy@(2});9@#wIH9gITK3C_+&?a&X3}7oLWPDbP93Fc;-7roohbvTZKmEIf(8( zY&w?{1SIcms+iq4nz>O|RDFzL_5iLaq`<}S9u$EndeXjF>1v!Mjy85cz^8i$ad5wG zyol|OU;ap1=+~-N?I95k;L^fFd2%xCBc_m&`i>(sX_IrQWoL!sS&HeFTJMF>OyrTs zJ-2%Yy7i>+@KXSz@cv`ifS_*79J^7?hspF<__Xd58<-~6`b4JDw5Rbb8znB&GC!a* zghrnQY_zkS+B2Se2GI3=e?Q-SI->{eCbB!lz6U~;8;DNxM>8PywCPHBFY8n87VTOb z)472VG>th06=Vb8DljhlvF`(*C?J2kX&slF*`FrgUWKiqm$0mMPSo5geED`=jYj>7mX&65r-y2yYl>_4Imwn50CNL z*5iF`TXq*>e>zlh1|@b<7l5(z4uMFv5pf2uj!}y9yi?@lnFm)=`1snP?={T5xYoWP zk>k#H3{WBIo<+}{B~2i*BOVd_pi}`j+D>BzXdtod*Mas0$XL#0r>>BhAJ`dQ8Lu^T zIw;3%?`OWqH)F^dhBXQjei)SWT32Z%koM6m`m5ERmIKZq&l)beQ-92N?Mn_Ink{x81jWs7&>!qRa%-H*-T#u^l?9@QDQLQ89u1vxp@ zLo{M-)mGP~R>IR8v|AW5;QY%zF9aSuoBGveYRt(NgC6BsCWH3CpJ>)nh}aF}Wkq%! zh;uAL%L^1KN}`LO=0#UjxF}oG5eupdM1~F`z3D|;VqFg#sdy$&KAlA!yl{WWydQGS zWhV?+k}&fi(;OFweyTKGNu7yAZNRzGAsnpMi9Hpi<;b{t2U7G>+@5v=37I&<544Di zJkwxHD z5k22qk!wRQK}%W~8|3lOB4XO5_!a1Y)P#U+hm?SuP<}C#l zNxKZc3x+-XWBaND4!nmw&!T^rGJ#MH)NBM*0UK7Q zKRp$t!_xQ0i`UnC z5&o4$)Z6D@p#8z}nFAiLLvGdvVU`S!3@Ecrc(GygmS}72lmb&#bi1T$8Y@siPY(yY z&@x8dgQNQD zyPsy|xC=fP-4n~R3||Xa1oEj7s5M#$VLZQhS!)scSUMu?Lk@5EmDaLogUEKG+sp^v zl8Di?731UH7Q&`qt1+N(r6cTdvQEQ-jIwetW;&PTpo#fRpXsdWui)-U125ie_=#u~ zQ!GzdF`{|6$6fF~+%72wZMZ!wG+3IvKs^twD>I>lHdkVRYsw1TT;gaG>qpC!dtTCE zY>&9gHnW86OGTjk>(6+048lciqBuQ`vIKy|YZR>_=wOZ1$n%;Zo#3R{0y!um*QT_o zwr2zePg}LM<1B!)a2SrcE`ISz``<>y(KzNk0497yIeQ#LNAFmYjUTV_~ z$$SBWhD~pz;`=h&dqFg92!xjmrAKebm$cV?0C>Re%LiLui)}Ze>A}JgEGOE1?6{S6 zz|d>V_*{X@`Sl4jA)QMtjUKx(giigt^O>SSev~mpzm1AgbG4q9GZT)`Vl_X?Gc(R$ ztY|{hIjy0GHIfJ`Jydw;!qy3Bi+Qep-}uk`Fa3k{FnAB@`M>*z|M2&J*Vz60W-#D* z{BuKye{Afw;F>N#-x}Dh;d%>!@}M9tbh25}j{BMuYr;wxZMzYp3#7)W5JG3kx_5ly z`SgQ+I<-0Duu{9RO4-%)01JKXDHMaU5X2Wx`S#`#uQ}yrvTgXG`qVSVSlV13-ZPem zUmGk2Upbm&&D5A5(`T#W<9E^`6JeZDC@W`~c^&=Kv@GS-=4NKeFnC$xnUhqLxn;i# z9gIE8Z^dsMb7*y_dt!6)G+I>m;ZR$Ad1g=DD36X7t?9AT!r_ z7EY{ZlJ&ePq9=wl8Fq{*3ijuD@5?l$|7#nN|Ni_x+!X%9zx4$B-ym=Q4#k^)K;-^i z6UiS<);+kdSJ*EYalkeal)no2W>t#`e7)Xy1s9c=76wZy4Fk zq^~>NiAEuRzBc%1;dFc&Z!_PpKrin3Ivns62*3X+EcO>MyXnkUMkepV{p9QW{Z22J zf_T&P5#0>a9E-Xj{b>1a)j}o(%f#=S4b`Q8WL1^6?HCL{DYmG2TC!LzwBQ$;oYnSO;h{lthl$xj2X$KXghx$23WHx z!u!+I1+CTgp}=cmN}sqsR3DmC^l$P0$tG3ySOvAJ`B-dy&l#o~*CT#DHu+=U!Tmy? zK!&jlDvhRulc{7t^JbeLPN|AkRwJA1qMS7Y;Tt!9fYHs^8l*YKCN<=F1_8If1{%$` zfK{wIa0}3VOeEqAH%&oU$5%L^An;!Y&;J^b|Ni{<=f6My*!=vDy2gKO*4~@2H+|X6 z?zHv$wfNqkV!wR+LlcXCdChyX`%m}uEq~>O#-eV3WKl3NE@jYp3ES^TzKT>po`k#w)r^T>VHVNtXm;FvQ=2hMlPTyOr2xYzfV`<{N(_-r2tsw$ ziO+y|{9(t!P!vC|?d)GokB9x7S6BN_)N4E<+dvs75M;FkZa6Oxn?e{HFHWTGevWV9 zJ>YIj_WIzC22N8p~Bx z*D^?^j2dldqr1x$-tRapa1^+_vioHCh@Hi#Q(5Q{GXza??3aK#)>#tQ98c2 zCTwYrt1;nPP~!Doy;Dqq9SLbvG&OvvAkTd5W+d^oI8F3#Lk{m(hZKd}(6G3EE|kuh z%)yz*3g4!iVh633w(Ws=rlD>OKV_+%I46^PUJXJA<>vy@qkfjsoi)&mw>?g>kE zutIaUflCdIDm&r5p{jw5#2>&GE!V~lspA0VT~~}kdRjj+?VPqX-VB%35h|I`rHD)_ zAG1-{r`;Xpb1evdZgCU7wt-3`aUGt4hGK?M_cnM7>O6)cZnumQabEpDWCPXw9qH`~ P00000NkvXXu0mjfDN6yb literal 0 HcmV?d00001 diff --git a/lib/src/model/broadcast/broadcast_providers.dart b/lib/src/model/broadcast/broadcast_providers.dart index a4e9243a5b..2f33523288 100644 --- a/lib/src/model/broadcast/broadcast_providers.dart +++ b/lib/src/model/broadcast/broadcast_providers.dart @@ -3,6 +3,7 @@ import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_repository.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/utils/image.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'broadcast_providers.g.dart'; @@ -53,3 +54,16 @@ Future broadcastTournament( BroadcastRepository(client).getTournament(broadcastTournamentId), ); } + +@Riverpod(keepAlive: true) +BroadcastImageWorkerFactory broadcastImageWorkerFactory(Ref ref) { + return const BroadcastImageWorkerFactory(); +} + +class BroadcastImageWorkerFactory { + const BroadcastImageWorkerFactory(); + + Future spawn() async { + return ImageColorWorker.spawn(); + } +} diff --git a/lib/src/utils/image.dart b/lib/src/utils/image.dart new file mode 100644 index 0000000000..24b2cdf77d --- /dev/null +++ b/lib/src/utils/image.dart @@ -0,0 +1,137 @@ +import 'dart:async'; +import 'dart:isolate'; +import 'package:http/http.dart' as http; +import 'package:image/image.dart' as img; + +import 'package:material_color_utilities/material_color_utilities.dart'; + +/// A worker that calculates the `primaryContainer` color of a remote image. +/// +/// The worker is created by calling [ImageColorWorker.spawn], and the computation +/// is run in a separate isolate. +class ImageColorWorker { + final SendPort _commands; + final ReceivePort _responses; + final Map> _activeRequests = {}; + int _idCounter = 0; + bool _closed = false; + + bool get closed => _closed; + + Future<(int, int)?> getImageColors(String url) async { + if (_closed) throw StateError('Closed'); + final completer = Completer<(int, int)?>.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + _commands.send((id, url)); + return await completer.future; + } + + static Future spawn() async { + final initPort = RawReceivePort(); + final connection = Completer<(ReceivePort, SendPort)>.sync(); + initPort.handler = (dynamic initialMessage) { + final commandPort = initialMessage as SendPort; + connection.complete( + ( + ReceivePort.fromRawReceivePort(initPort), + commandPort, + ), + ); + }; + + try { + await Isolate.spawn(_startRemoteIsolate, initPort.sendPort); + } on Object { + initPort.close(); + rethrow; + } + + final (ReceivePort receivePort, SendPort sendPort) = + await connection.future; + + return ImageColorWorker._(receivePort, sendPort); + } + + ImageColorWorker._(this._responses, this._commands) { + _responses.listen(_handleResponsesFromIsolate); + } + + void _handleResponsesFromIsolate(dynamic message) { + final (int id, Object response) = message as (int, Object); + final completer = _activeRequests.remove(id)!; + + if (response is RemoteError) { + completer.completeError(response); + } else { + completer.complete(response as (int, int)); + } + + if (_closed && _activeRequests.isEmpty) _responses.close(); + } + + static void _handleCommandsToIsolate( + ReceivePort receivePort, + SendPort sendPort, + ) { + receivePort.listen((message) async { + if (message == 'shutdown') { + receivePort.close(); + return; + } + final (int id, String url) = message as (int, String); + try { + final bytes = await http.readBytes(Uri.parse(url)); + final image = img.decodeImage(bytes); + final resized = img.copyResize(image!, width: 112); + final QuantizerResult quantizerResult = + await QuantizerCelebi().quantize( + resized.buffer.asUint32List(), + 128, + returnInputPixelToClusterPixel: true, + ); + final Map colorToCount = quantizerResult.colorToCount.map( + (int key, int value) => + MapEntry(_getArgbFromAbgr(key), value), + ); + // Score colors for color scheme suitability. + final List scoredResults = Score.score(colorToCount, desired: 1); + final Hct sourceColor = Hct.fromInt(scoredResults.first); + final scheme = SchemeFidelity( + sourceColorHct: sourceColor, + isDark: false, + contrastLevel: 0.0, + ); + final colors = (scheme.primaryContainer, scheme.onPrimaryContainer); + sendPort.send((id, colors)); + } catch (e) { + sendPort.send((id, RemoteError(e.toString(), ''))); + } + }); + } + + static void _startRemoteIsolate(SendPort sendPort) { + final receivePort = ReceivePort(); + sendPort.send(receivePort.sendPort); + _handleCommandsToIsolate(receivePort, sendPort); + } + + void close() { + if (!_closed) { + _closed = true; + _commands.send('shutdown'); + if (_activeRequests.isEmpty) _responses.close(); + } + } +} + +// Converts AABBGGRR color int to AARRGGBB format. +int _getArgbFromAbgr(int abgr) { + const int exceptRMask = 0xFF00FFFF; + const int onlyRMask = ~exceptRMask; + const int exceptBMask = 0xFFFFFF00; + const int onlyBMask = ~exceptBMask; + final int r = (abgr & onlyRMask) >> 16; + final int b = abgr & onlyBMask; + return (abgr & exceptRMask & exceptBMask) | (b << 16) | r; +} diff --git a/lib/src/view/broadcast/broadcast_list_screen.dart b/lib/src/view/broadcast/broadcast_list_screen.dart index 6415c80d83..8a912847f9 100644 --- a/lib/src/view/broadcast/broadcast_list_screen.dart +++ b/lib/src/view/broadcast/broadcast_list_screen.dart @@ -7,13 +7,13 @@ import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_providers.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/styles/lichess_colors.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/styles/transparent_image.dart'; +import 'package:lichess_mobile/src/utils/image.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_round_screen.dart'; -import 'package:lichess_mobile/src/view/broadcast/default_broadcast_image.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; @@ -45,20 +45,32 @@ class _Body extends ConsumerStatefulWidget { class _BodyState extends ConsumerState<_Body> { final ScrollController _scrollController = ScrollController(); + ImageColorWorker? _worker; @override void initState() { super.initState(); _scrollController.addListener(_scrollListener); + _initWorker(); } @override void dispose() { _scrollController.removeListener(_scrollListener); _scrollController.dispose(); + _worker?.close(); super.dispose(); } + Future _initWorker() async { + final worker = await ref.read(broadcastImageWorkerFactoryProvider).spawn(); + if (mounted) { + setState(() { + _worker = worker; + }); + } + } + void _scrollListener() { if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 300) { @@ -74,7 +86,7 @@ class _BodyState extends ConsumerState<_Body> { Widget build(BuildContext context) { final broadcasts = ref.watch(broadcastsPaginatorProvider); - if (!broadcasts.hasValue && broadcasts.isLoading) { + if (_worker == null || (!broadcasts.hasValue && broadcasts.isLoading)) { return const Center( child: CircularProgressIndicator.adaptive(), ); @@ -105,8 +117,10 @@ class _BodyState extends ConsumerState<_Body> { crossAxisSpacing: 10, mainAxisSpacing: 10, ), - itemBuilder: (context, index) => - BroadcastGridItem(broadcast: broadcasts.value!.active[index]), + itemBuilder: (context, index) => BroadcastGridItem( + broadcast: broadcasts.value!.active[index], + worker: _worker!, + ), itemCount: broadcasts.value!.active.length, ), ), @@ -128,6 +142,7 @@ class _BodyState extends ConsumerState<_Body> { mainAxisSpacing: 10, ), itemBuilder: (context, index) => BroadcastGridItem( + worker: _worker!, broadcast: broadcasts.value!.upcoming[index], ), itemCount: broadcasts.value!.upcoming.length, @@ -150,15 +165,18 @@ class _BodyState extends ConsumerState<_Body> { crossAxisSpacing: 10, mainAxisSpacing: 10, ), - itemBuilder: (context, index) => (broadcasts.isLoading && - index >= itemsCount - loadingItems) - ? Shimmer( - child: ShimmerLoading( - isLoading: true, - child: BroadcastGridItem.loading(), - ), - ) - : BroadcastGridItem(broadcast: broadcasts.value!.past[index]), + itemBuilder: (context, index) => + (broadcasts.isLoading && index >= itemsCount - loadingItems) + ? Shimmer( + child: ShimmerLoading( + isLoading: true, + child: BroadcastGridItem.loading(_worker!), + ), + ) + : BroadcastGridItem( + worker: _worker!, + broadcast: broadcasts.value!.past[index], + ), itemCount: itemsCount, ), ), @@ -169,11 +187,16 @@ class _BodyState extends ConsumerState<_Body> { } class BroadcastGridItem extends StatefulWidget { - final Broadcast broadcast; + const BroadcastGridItem({ + required this.broadcast, + required this.worker, + super.key, + }); - const BroadcastGridItem({required this.broadcast}); + final Broadcast broadcast; + final ImageColorWorker worker; - BroadcastGridItem.loading() + BroadcastGridItem.loading(this.worker) : broadcast = Broadcast( tour: const BroadcastTournamentData( id: BroadcastTournamentId(''), @@ -203,48 +226,71 @@ class BroadcastGridItem extends StatefulWidget { State createState() => _BroadcastGridItemState(); } +typedef _CardColors = ({ + Color primaryContainer, + Color onPrimaryContainer, +}); +final Map _colorsCache = { + kDefaultBroadcastImage: ( + primaryContainer: LichessColors.brag, + onPrimaryContainer: const Color(0xFF000000), + ), +}; + +const kDefaultBroadcastImage = AssetImage('assets/images/broadcast_image.png'); + class _BroadcastGridItemState extends State { - ColorScheme? _colorScheme; + _CardColors? _cardColors; + + String? get imageUrl => widget.broadcast.tour.imageUrl; + + ImageProvider get image => + imageUrl != null ? NetworkImage(imageUrl!) : kDefaultBroadcastImage; @override void didChangeDependencies() { super.didChangeDependencies(); - if (widget.broadcast.tour.imageUrl != null) { - _fetchColorScheme(widget.broadcast.tour.imageUrl!); + final cachedColors = _colorsCache[image]; + if (cachedColors != null) { + _cardColors = cachedColors; + } else { + if (imageUrl != null) { + _fetchImageAndColors(image as NetworkImage); + } } } - Future _fetchColorScheme(String url) async { + Future _fetchImageAndColors(NetworkImage provider) async { if (!mounted) return; if (Scrollable.recommendDeferredLoadingForContext(context)) { SchedulerBinding.instance.scheduleFrameCallback((_) { - scheduleMicrotask(() => _fetchColorScheme(url)); + scheduleMicrotask(() => _fetchImageAndColors(provider)); }); - } else { - try { - final colorScheme = await ColorScheme.fromImageProvider( - provider: NetworkImage(url), - dynamicSchemeVariant: DynamicSchemeVariant.fidelity, + } else if (widget.worker.closed == false) { + final response = await widget.worker.getImageColors(provider.url); + if (response != null) { + final (primaryContainer, onPrimaryContainer) = response; + final cardColors = ( + primaryContainer: Color(primaryContainer), + onPrimaryContainer: Color(onPrimaryContainer), ); + _colorsCache[provider] = cardColors; if (mounted) { setState(() { - _colorScheme = colorScheme; + _cardColors = cardColors; }); } - } catch (_) { - // ignore } } } @override Widget build(BuildContext context) { - final backgroundColor = - _colorScheme?.primaryContainer ?? Colors.transparent; - final titleColor = _colorScheme?.onPrimaryContainer; + final backgroundColor = _cardColors?.primaryContainer ?? Colors.transparent; + final titleColor = _cardColors?.onPrimaryContainer; final subTitleColor = - _colorScheme?.onPrimaryContainer.withValues(alpha: 0.7) ?? + _cardColors?.onPrimaryContainer.withValues(alpha: 0.7) ?? textShade(context, 0.7); return AdaptiveInkWell( @@ -267,6 +313,15 @@ class _BroadcastGridItemState extends State { ? null : kElevationToShadow[1], ), + foregroundDecoration: BoxDecoration( + border: (widget.broadcast.isLive) + ? Border.all( + color: LichessColors.red.withValues(alpha: 0.7), + width: 3, + ) + : null, + borderRadius: BorderRadius.circular(20), + ), child: Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, @@ -285,17 +340,15 @@ class _BroadcastGridItemState extends State { tileMode: TileMode.clamp, ).createShader(bounds); }, - child: widget.broadcast.tour.imageUrl != null - ? AspectRatio( - aspectRatio: 2.0, - child: FadeInImage.memoryNetwork( - placeholder: transparentImage, - image: widget.broadcast.tour.imageUrl!, - imageErrorBuilder: (context, error, stackTrace) => - const DefaultBroadcastImage(aspectRatio: 2.0), - ), - ) - : const DefaultBroadcastImage(aspectRatio: 2.0), + child: AspectRatio( + aspectRatio: 2.0, + child: FadeInImage( + placeholder: kDefaultBroadcastImage, + image: image, + imageErrorBuilder: (context, error, stackTrace) => + const Image(image: kDefaultBroadcastImage), + ), + ), ), if (widget.broadcast.round.startsAt != null || widget.broadcast.isLive) diff --git a/pubspec.lock b/pubspec.lock index a10b8cbcbb..2de7ca885d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -804,7 +804,7 @@ packages: source: hosted version: "0.1.0" image: - dependency: transitive + dependency: "direct main" description: name: image sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d diff --git a/pubspec.yaml b/pubspec.yaml index 827462f30f..90f4e62475 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -52,6 +52,7 @@ dependencies: flutter_svg: ^2.0.10+1 freezed_annotation: ^2.2.0 http: ^1.1.0 + image: ^4.3.0 intl: ^0.19.0 json_annotation: ^4.7.0 linkify: ^5.0.0 diff --git a/test/view/broadcast/broadcasts_list_screen_test.dart b/test/view/broadcast/broadcasts_list_screen_test.dart index a570b58071..b68ed3f660 100644 --- a/test/view/broadcast/broadcasts_list_screen_test.dart +++ b/test/view/broadcast/broadcasts_list_screen_test.dart @@ -1,12 +1,35 @@ +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast_providers.dart'; import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/utils/image.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_list_screen.dart'; import 'package:network_image_mock/network_image_mock.dart'; import '../../test_helpers.dart'; import '../../test_provider_scope.dart'; +class FakeImageColorWorker implements ImageColorWorker { + @override + void close() {} + + @override + bool get closed => false; + + @override + Future<(int, int)?> getImageColors(String url) { + return Future.value(null); + } +} + +class FakeBroadcastImageWorkerFactory implements BroadcastImageWorkerFactory { + @override + Future spawn() { + return Future.value(FakeImageColorWorker()); + } +} + final client = MockClient((request) { if (request.url.path == '/api/broadcast/top') { return mockResponse( @@ -31,19 +54,20 @@ void main() { overrides: [ lichessClientProvider .overrideWith((ref) => LichessClient(client, ref)), + broadcastImageWorkerFactoryProvider.overrideWith( + (ref) => FakeBroadcastImageWorkerFactory(), + ), ], ); await tester.pumpWidget(app); + expect(find.byType(CircularProgressIndicator), findsOneWidget); + // wait for broadcast tournaments to load await tester.pump(const Duration(milliseconds: 100)); expect(find.byType(BroadcastGridItem), findsAtLeast(1)); - - // ColorScheme.fromImageProvider creates a Timer of 5s which is not automatically - // disposed - await tester.pump(const Duration(seconds: 10)); }); }, ); @@ -59,21 +83,22 @@ void main() { overrides: [ lichessClientProvider .overrideWith((ref) => LichessClient(client, ref)), + broadcastImageWorkerFactoryProvider.overrideWith( + (ref) => FakeBroadcastImageWorkerFactory(), + ), ], ); await tester.pumpWidget(app); + expect(find.byType(CircularProgressIndicator), findsOneWidget); + // wait for broadcast tournaments to load await tester.pump(const Duration(milliseconds: 100)); await tester.scrollUntilVisible(find.text('Past broadcasts'), 200.0); await tester.pumpAndSettle(); - - // ColorScheme.fromImageProvider creates a Timer of 5s which is not automatically - // disposed - await tester.pump(const Duration(seconds: 10)); }); }, ); From e4939310c82c5326de9f8d018e5212eaa11e0684 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 5 Dec 2024 17:37:01 +0100 Subject: [PATCH 847/979] Remove unused files --- lib/src/styles/transparent_image.dart | 71 ------------------- .../broadcast/default_broadcast_image.dart | 43 ----------- 2 files changed, 114 deletions(-) delete mode 100644 lib/src/styles/transparent_image.dart delete mode 100644 lib/src/view/broadcast/default_broadcast_image.dart diff --git a/lib/src/styles/transparent_image.dart b/lib/src/styles/transparent_image.dart deleted file mode 100644 index 774d642d8b..0000000000 --- a/lib/src/styles/transparent_image.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'dart:typed_data'; - -final Uint8List transparentImage = Uint8List.fromList([ - 0x89, - 0x50, - 0x4E, - 0x47, - 0x0D, - 0x0A, - 0x1A, - 0x0A, - 0x00, - 0x00, - 0x00, - 0x0D, - 0x49, - 0x48, - 0x44, - 0x52, - 0x00, - 0x00, - 0x00, - 0x01, - 0x00, - 0x00, - 0x00, - 0x01, - 0x08, - 0x06, - 0x00, - 0x00, - 0x00, - 0x1F, - 0x15, - 0xC4, - 0x89, - 0x00, - 0x00, - 0x00, - 0x0A, - 0x49, - 0x44, - 0x41, - 0x54, - 0x78, - 0x9C, - 0x63, - 0x00, - 0x01, - 0x00, - 0x00, - 0x05, - 0x00, - 0x01, - 0x0D, - 0x0A, - 0x2D, - 0xB4, - 0x00, - 0x00, - 0x00, - 0x00, - 0x49, - 0x45, - 0x4E, - 0x44, - 0xAE, - 0x42, - 0x60, - 0x82, -]); diff --git a/lib/src/view/broadcast/default_broadcast_image.dart b/lib/src/view/broadcast/default_broadcast_image.dart deleted file mode 100644 index e4c944db0b..0000000000 --- a/lib/src/view/broadcast/default_broadcast_image.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lichess_mobile/src/styles/lichess_colors.dart'; -import 'package:lichess_mobile/src/styles/lichess_icons.dart'; - -class DefaultBroadcastImage extends StatelessWidget { - final double? width; - final double aspectRatio; - - const DefaultBroadcastImage({ - super.key, - this.width, - this.aspectRatio = 2.0, - }); - - @override - Widget build(BuildContext context) { - return SizedBox( - width: width, - child: AspectRatio( - aspectRatio: aspectRatio, - child: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - LichessColors.primary.withValues(alpha: 0.7), - LichessColors.brag.withValues(alpha: 0.7), - ], - ), - ), - child: LayoutBuilder( - builder: (context, constraints) => Icon( - LichessIcons.radio_tower_lichess, - color: Theme.of(context).colorScheme.onSurfaceVariant, - size: constraints.maxWidth / 4, - ), - ), - ), - ), - ); - } -} From cf3ee300a8d1e7f7b6546f42a06b0f74f1d27c71 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 6 Dec 2024 00:24:51 +0100 Subject: [PATCH 848/979] More broadcasts improvements - better list layout: larger cards - avoid unnecessary requests when switching tabs - better nav bar on iOS --- .../view/broadcast/broadcast_list_screen.dart | 158 +++++++++--------- .../broadcast/broadcast_overview_tab.dart | 7 - .../broadcast/broadcast_round_screen.dart | 101 +++++++---- 3 files changed, 153 insertions(+), 113 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_list_screen.dart b/lib/src/view/broadcast/broadcast_list_screen.dart index 8a912847f9..73b9f87cae 100644 --- a/lib/src/view/broadcast/broadcast_list_screen.dart +++ b/lib/src/view/broadcast/broadcast_list_screen.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -21,6 +22,10 @@ import 'package:lichess_mobile/src/widgets/shimmer.dart'; final _dateFormatter = DateFormat.MMMd().add_Hm(); final _dateFormatterWithYear = DateFormat.yMMMd().add_Hm(); +const kDefaultBroadcastImage = AssetImage('assets/images/broadcast_image.png'); +const kBroadcastGridItemBorderRadius = BorderRadius.all(Radius.circular(16.0)); +const kBroadcastGridItemContentPadding = EdgeInsets.symmetric(horizontal: 16.0); + /// A screen that displays a paginated list of broadcasts. class BroadcastListScreen extends StatelessWidget { const BroadcastListScreen({super.key}); @@ -29,7 +34,12 @@ class BroadcastListScreen extends StatelessWidget { Widget build(BuildContext context) { return PlatformScaffold( appBar: PlatformAppBar( - title: Text(context.l10n.broadcastLiveBroadcasts), + title: AutoSizeText( + context.l10n.broadcastLiveBroadcasts, + minFontSize: 14.0, + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), ), body: const _Body(), ); @@ -100,11 +110,18 @@ class _BodyState extends ConsumerState<_Body> { } final isTablet = isTabletOrLarger(context); - final itemsByRow = isTablet ? 6 : 2; - final loadingItems = isTablet ? 36 : 12; + final itemsByRow = isTablet ? 2 : 1; + const loadingItems = 12; final itemsCount = broadcasts.requireValue.past.length + (broadcasts.isLoading ? loadingItems : 0); + final gridDelegate = SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: itemsByRow, + crossAxisSpacing: 10, + mainAxisSpacing: 10, + childAspectRatio: 1.3, + ); + return SafeArea( child: CustomScrollView( controller: _scrollController, @@ -112,11 +129,7 @@ class _BodyState extends ConsumerState<_Body> { SliverPadding( padding: Styles.bodySectionPadding, sliver: SliverGrid.builder( - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: itemsByRow, - crossAxisSpacing: 10, - mainAxisSpacing: 10, - ), + gridDelegate: gridDelegate, itemBuilder: (context, index) => BroadcastGridItem( broadcast: broadcasts.value!.active[index], worker: _worker!, @@ -136,11 +149,7 @@ class _BodyState extends ConsumerState<_Body> { SliverPadding( padding: Styles.bodySectionPadding, sliver: SliverGrid.builder( - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: itemsByRow, - crossAxisSpacing: 10, - mainAxisSpacing: 10, - ), + gridDelegate: gridDelegate, itemBuilder: (context, index) => BroadcastGridItem( worker: _worker!, broadcast: broadcasts.value!.upcoming[index], @@ -160,11 +169,7 @@ class _BodyState extends ConsumerState<_Body> { SliverPadding( padding: Styles.bodySectionPadding, sliver: SliverGrid.builder( - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: itemsByRow, - crossAxisSpacing: 10, - mainAxisSpacing: 10, - ), + gridDelegate: gridDelegate, itemBuilder: (context, index) => (broadcasts.isLoading && index >= itemsCount - loadingItems) ? Shimmer( @@ -237,8 +242,6 @@ final Map _colorsCache = { ), }; -const kDefaultBroadcastImage = AssetImage('assets/images/broadcast_image.png'); - class _BroadcastGridItemState extends State { _CardColors? _cardColors; @@ -294,7 +297,7 @@ class _BroadcastGridItemState extends State { textShade(context, 0.7); return AdaptiveInkWell( - borderRadius: BorderRadius.circular(20), + borderRadius: kBroadcastGridItemBorderRadius, onTap: () { pushPlatformRoute( context, @@ -307,7 +310,7 @@ class _BroadcastGridItemState extends State { child: Container( clipBehavior: Clip.hardEdge, decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), + borderRadius: kBroadcastGridItemBorderRadius, color: backgroundColor, boxShadow: Theme.of(context).platform == TargetPlatform.iOS ? null @@ -320,7 +323,7 @@ class _BroadcastGridItemState extends State { width: 3, ) : null, - borderRadius: BorderRadius.circular(20), + borderRadius: kBroadcastGridItemBorderRadius, ), child: Column( mainAxisAlignment: MainAxisAlignment.start, @@ -350,61 +353,66 @@ class _BroadcastGridItemState extends State { ), ), ), - if (widget.broadcast.round.startsAt != null || - widget.broadcast.isLive) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - _formatDate(widget.broadcast.round.startsAt!), - style: TextStyle( - fontSize: 11, - color: subTitleColor, + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (widget.broadcast.round.startsAt != null || + widget.broadcast.isLive) + Padding( + padding: kBroadcastGridItemContentPadding, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + widget.broadcast.round.name, + style: TextStyle( + fontSize: 12, + color: subTitleColor, + ), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + const SizedBox(width: 4.0), + if (widget.broadcast.isLive) + const Text( + 'LIVE', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.bold, + color: Colors.red, + ), + overflow: TextOverflow.ellipsis, + ) + else + Text( + _formatDate(widget.broadcast.round.startsAt!), + style: TextStyle( + fontSize: 12, + color: subTitleColor, + ), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ], ), - overflow: TextOverflow.ellipsis, - maxLines: 1, ), - if (widget.broadcast.isLive) ...[ - const SizedBox(width: 4.0), - const Text( - 'LIVE', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: Colors.red, - ), - overflow: TextOverflow.ellipsis, + const SizedBox(height: 4.0), + Padding( + padding: kBroadcastGridItemContentPadding, + child: Text( + widget.broadcast.title, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: titleColor, + fontWeight: FontWeight.bold, + fontSize: 16, ), - ], - ], - ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Text( - widget.broadcast.round.name, - style: TextStyle( - fontSize: 11, - color: subTitleColor, - ), - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), - ), - const SizedBox(height: 4.0), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Text( - widget.broadcast.title, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: titleColor, - fontSize: 13, - fontWeight: FontWeight.bold, - ), + ), + ), + ], ), ), ], diff --git a/lib/src/view/broadcast/broadcast_overview_tab.dart b/lib/src/view/broadcast/broadcast_overview_tab.dart index 91d59344a0..94ae982e4f 100644 --- a/lib/src/view/broadcast/broadcast_overview_tab.dart +++ b/lib/src/view/broadcast/broadcast_overview_tab.dart @@ -38,13 +38,6 @@ class BroadcastOverviewTab extends ConsumerWidget { child: ListView( padding: Styles.bodyPadding, children: [ - if (Theme.of(context).platform == TargetPlatform.iOS) ...[ - Text( - broadcast.title, - style: Styles.title, - ), - const SizedBox(height: 16.0), - ], if (tournament.data.imageUrl != null) ...[ Image.network(tournament.data.imageUrl!), const SizedBox(height: 16.0), diff --git a/lib/src/view/broadcast/broadcast_round_screen.dart b/lib/src/view/broadcast/broadcast_round_screen.dart index d3ba9a2ed1..0bd6fa2edf 100644 --- a/lib/src/view/broadcast/broadcast_round_screen.dart +++ b/lib/src/view/broadcast/broadcast_round_screen.dart @@ -6,6 +6,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_providers.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast_round_controller.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; @@ -74,43 +75,81 @@ class _BroadcastRoundScreenState extends ConsumerState switch (tournament) { case AsyncData(:final value): + // Eagerly initalize the round controller so it stays alive when switching tabs + ref.watch( + broadcastRoundControllerProvider( + _selectedRoundId ?? value.defaultRoundId, + ), + ); if (Theme.of(context).platform == TargetPlatform.iOS) { return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( - middle: CupertinoSlidingSegmentedControl<_ViewMode>( - groupValue: _selectedSegment, - children: { - _ViewMode.overview: Text(context.l10n.broadcastOverview), - _ViewMode.boards: Text(context.l10n.broadcastBoards), - }, - onValueChanged: (_ViewMode? view) { - if (view != null) { - setState(() { - _selectedSegment = view; - }); - } - }, + middle: AutoSizeText( + widget.broadcast.title, + minFontSize: 14.0, + overflow: TextOverflow.ellipsis, + maxLines: 1, ), + automaticBackgroundVisibility: false, + border: null, ), - child: Column( - children: [ - Expanded( - child: _selectedSegment == _ViewMode.overview - ? BroadcastOverviewTab( - broadcast: widget.broadcast, - tournamentId: _selectedTournamentId, - ) - : BroadcastBoardsTab( - _selectedRoundId ?? value.defaultRoundId, + child: SafeArea( + bottom: false, + child: Column( + children: [ + Container( + height: kMinInteractiveDimensionCupertino, + width: double.infinity, + decoration: BoxDecoration( + color: Styles.cupertinoAppBarColor.resolveFrom(context), + border: const Border( + bottom: BorderSide( + color: Color(0x4D000000), + width: 0.0, ), - ), - _BottomBar( - tournament: value, - roundId: _selectedRoundId ?? value.defaultRoundId, - setTournamentId: setTournamentId, - setRoundId: setRoundId, - ), - ], + ), + ), + child: Padding( + padding: const EdgeInsets.only( + left: 16.0, + right: 16.0, + bottom: 8.0, + ), + child: CupertinoSlidingSegmentedControl<_ViewMode>( + groupValue: _selectedSegment, + children: { + _ViewMode.overview: + Text(context.l10n.broadcastOverview), + _ViewMode.boards: Text(context.l10n.broadcastBoards), + }, + onValueChanged: (_ViewMode? view) { + if (view != null) { + setState(() { + _selectedSegment = view; + }); + } + }, + ), + ), + ), + Expanded( + child: _selectedSegment == _ViewMode.overview + ? BroadcastOverviewTab( + broadcast: widget.broadcast, + tournamentId: _selectedTournamentId, + ) + : BroadcastBoardsTab( + _selectedRoundId ?? value.defaultRoundId, + ), + ), + _BottomBar( + tournament: value, + roundId: _selectedRoundId ?? value.defaultRoundId, + setTournamentId: setTournamentId, + setRoundId: setRoundId, + ), + ], + ), ), ); } else { From 79c718f7c7106569743d7b5b4ce5390cc9718907 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 6 Dec 2024 11:47:14 +0100 Subject: [PATCH 849/979] More broadcast fixes --- .../model/broadcast/broadcast_providers.dart | 4 +- .../view/broadcast/broadcast_list_screen.dart | 45 +++++++++++-------- 2 files changed, 29 insertions(+), 20 deletions(-) diff --git a/lib/src/model/broadcast/broadcast_providers.dart b/lib/src/model/broadcast/broadcast_providers.dart index 2f33523288..53340f3dc9 100644 --- a/lib/src/model/broadcast/broadcast_providers.dart +++ b/lib/src/model/broadcast/broadcast_providers.dart @@ -25,12 +25,12 @@ class BroadcastsPaginator extends _$BroadcastsPaginator { final broadcastList = state.requireValue; final nextPage = broadcastList.nextPage; - if (nextPage != null && nextPage > 20) return; + if (nextPage == null || nextPage > 20) return; state = const AsyncLoading(); final broadcastListNewPage = await ref.withClient( - (client) => BroadcastRepository(client).getBroadcasts(page: nextPage!), + (client) => BroadcastRepository(client).getBroadcasts(page: nextPage), ); state = AsyncData( diff --git a/lib/src/view/broadcast/broadcast_list_screen.dart b/lib/src/view/broadcast/broadcast_list_screen.dart index 73b9f87cae..d3e2462155 100644 --- a/lib/src/view/broadcast/broadcast_list_screen.dart +++ b/lib/src/view/broadcast/broadcast_list_screen.dart @@ -15,7 +15,6 @@ import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_round_screen.dart'; -import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; @@ -35,7 +34,7 @@ class BroadcastListScreen extends StatelessWidget { return PlatformScaffold( appBar: PlatformAppBar( title: AutoSizeText( - context.l10n.broadcastLiveBroadcasts, + context.l10n.broadcastBroadcasts, minFontSize: 14.0, overflow: TextOverflow.ellipsis, maxLines: 1, @@ -235,12 +234,7 @@ typedef _CardColors = ({ Color primaryContainer, Color onPrimaryContainer, }); -final Map _colorsCache = { - kDefaultBroadcastImage: ( - primaryContainer: LichessColors.brag, - onPrimaryContainer: const Color(0xFF000000), - ), -}; +final Map _colorsCache = {}; class _BroadcastGridItemState extends State { _CardColors? _cardColors; @@ -290,14 +284,18 @@ class _BroadcastGridItemState extends State { @override Widget build(BuildContext context) { - final backgroundColor = _cardColors?.primaryContainer ?? Colors.transparent; + final defaultBackgroundColor = + Theme.of(context).platform == TargetPlatform.iOS + ? Styles.cupertinoCardColor.resolveFrom(context) + : Theme.of(context).colorScheme.surfaceContainer; + final backgroundColor = + _cardColors?.primaryContainer ?? defaultBackgroundColor; final titleColor = _cardColors?.onPrimaryContainer; final subTitleColor = _cardColors?.onPrimaryContainer.withValues(alpha: 0.7) ?? textShade(context, 0.7); - return AdaptiveInkWell( - borderRadius: kBroadcastGridItemBorderRadius, + return GestureDetector( onTap: () { pushPlatformRoute( context, @@ -307,7 +305,8 @@ class _BroadcastGridItemState extends State { BroadcastRoundScreen(broadcast: widget.broadcast), ); }, - child: Container( + child: AnimatedContainer( + duration: const Duration(milliseconds: 500), clipBehavior: Clip.hardEdge, decoration: BoxDecoration( borderRadius: kBroadcastGridItemBorderRadius, @@ -345,10 +344,20 @@ class _BroadcastGridItemState extends State { }, child: AspectRatio( aspectRatio: 2.0, - child: FadeInImage( - placeholder: kDefaultBroadcastImage, + child: Image( image: image, - imageErrorBuilder: (context, error, stackTrace) => + frameBuilder: + (context, child, frame, wasSynchronouslyLoaded) { + if (wasSynchronouslyLoaded) { + return child; + } + return AnimatedOpacity( + duration: const Duration(milliseconds: 500), + opacity: frame == null ? 0 : 1, + child: child, + ); + }, + errorBuilder: (context, error, stackTrace) => const Image(image: kDefaultBroadcastImage), ), ), @@ -379,7 +388,7 @@ class _BroadcastGridItemState extends State { const Text( 'LIVE', style: TextStyle( - fontSize: 13, + fontSize: 15, fontWeight: FontWeight.bold, color: Colors.red, ), @@ -427,8 +436,8 @@ String _formatDate(DateTime date) { return (!diff.isNegative && diff.inDays == 0) ? diff.inHours == 0 - ? 'In ${diff.inMinutes} minutes' // TODO translate with https://github.com/lichess-org/lila/blob/65b28ea8e43e0133df6c7ed40e03c2954f247d1e/translation/source/timeago.xml#L8 - : 'In ${diff.inHours} hours' // TODO translate with https://github.com/lichess-org/lila/blob/65b28ea8e43e0133df6c7ed40e03c2954f247d1e/translation/source/timeago.xml#L12 + ? 'in ${diff.inMinutes} minutes' // TODO translate with https://github.com/lichess-org/lila/blob/65b28ea8e43e0133df6c7ed40e03c2954f247d1e/translation/source/timeago.xml#L8 + : 'in ${diff.inHours} hours' // TODO translate with https://github.com/lichess-org/lila/blob/65b28ea8e43e0133df6c7ed40e03c2954f247d1e/translation/source/timeago.xml#L12 : diff.inDays < 365 ? _dateFormatter.format(date) : _dateFormatterWithYear.format(date); From b66ebbd0f8069c6b10d430cfe26672d96cf1b520 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Fri, 6 Dec 2024 17:28:27 +0100 Subject: [PATCH 850/979] Fix broadcast list that was reloading when scrolling on the watch_tab_screen --- lib/src/model/broadcast/broadcast.dart | 2 +- .../model/broadcast/broadcast_providers.dart | 2 +- .../model/broadcast/broadcast_repository.dart | 4 +- lib/src/view/watch/watch_tab_screen.dart | 45 +++++++++++-------- .../broadcast/broadcast_repository_test.dart | 2 +- 5 files changed, 31 insertions(+), 24 deletions(-) diff --git a/lib/src/model/broadcast/broadcast.dart b/lib/src/model/broadcast/broadcast.dart index deb19c88e6..4a69054d9c 100644 --- a/lib/src/model/broadcast/broadcast.dart +++ b/lib/src/model/broadcast/broadcast.dart @@ -5,7 +5,7 @@ import 'package:lichess_mobile/src/model/common/id.dart'; part 'broadcast.freezed.dart'; -typedef BroadcastsList = ({ +typedef BroadcastList = ({ IList active, IList upcoming, IList past, diff --git a/lib/src/model/broadcast/broadcast_providers.dart b/lib/src/model/broadcast/broadcast_providers.dart index 2f33523288..dcba05c1e9 100644 --- a/lib/src/model/broadcast/broadcast_providers.dart +++ b/lib/src/model/broadcast/broadcast_providers.dart @@ -13,7 +13,7 @@ part 'broadcast_providers.g.dart'; @riverpod class BroadcastsPaginator extends _$BroadcastsPaginator { @override - Future build() async { + Future build() async { final broadcastList = await ref.withClient( (client) => BroadcastRepository(client).getBroadcasts(), ); diff --git a/lib/src/model/broadcast/broadcast_repository.dart b/lib/src/model/broadcast/broadcast_repository.dart index 1b3a3eb790..9063efff0d 100644 --- a/lib/src/model/broadcast/broadcast_repository.dart +++ b/lib/src/model/broadcast/broadcast_repository.dart @@ -12,7 +12,7 @@ class BroadcastRepository { final LichessClient client; - Future getBroadcasts({int page = 1}) { + Future getBroadcasts({int page = 1}) { return client.readJson( Uri( path: '/api/broadcast/top', @@ -53,7 +53,7 @@ class BroadcastRepository { } } -BroadcastsList _makeBroadcastResponseFromJson( +BroadcastList _makeBroadcastResponseFromJson( Map json, ) { return ( diff --git a/lib/src/view/watch/watch_tab_screen.dart b/lib/src/view/watch/watch_tab_screen.dart index 7f2e6c42ba..5c3d9b9635 100644 --- a/lib/src/view/watch/watch_tab_screen.dart +++ b/lib/src/view/watch/watch_tab_screen.dart @@ -10,6 +10,7 @@ import 'package:lichess_mobile/src/model/tv/featured_player.dart'; import 'package:lichess_mobile/src/model/tv/tv_channel.dart'; import 'package:lichess_mobile/src/model/tv/tv_game.dart'; import 'package:lichess_mobile/src/model/tv/tv_repository.dart'; +import 'package:lichess_mobile/src/model/user/streamer.dart'; import 'package:lichess_mobile/src/model/user/user_repository_providers.dart'; import 'package:lichess_mobile/src/navigation.dart'; import 'package:lichess_mobile/src/network/http.dart'; @@ -85,11 +86,17 @@ class _WatchScreenState extends ConsumerState { ); } - List get watchTabWidgets => const [ - _BroadcastWidget(), - _WatchTvWidget(), - _StreamerWidget(), - ]; + List watchTabWidgets(WidgetRef ref) { + final broadcastList = ref.watch(broadcastsPaginatorProvider); + final featuredChannels = ref.watch(featuredChannelsProvider); + final streamers = ref.watch(liveStreamersProvider); + + return [ + _BroadcastWidget(broadcastList), + _WatchTvWidget(featuredChannels), + _StreamerWidget(streamers), + ]; + } Widget _buildAndroid(BuildContext context, WidgetRef ref) { return PopScope( @@ -112,7 +119,7 @@ class _WatchScreenState extends ConsumerState { return orientation == Orientation.portrait ? ListView( controller: watchScrollController, - children: watchTabWidgets, + children: watchTabWidgets(ref), ) : GridView( controller: watchScrollController, @@ -121,7 +128,7 @@ class _WatchScreenState extends ConsumerState { crossAxisCount: 2, childAspectRatio: 0.92, ), - children: watchTabWidgets, + children: watchTabWidgets(ref), ); }, ), @@ -152,7 +159,7 @@ class _WatchScreenState extends ConsumerState { sliver: orientation == Orientation.portrait ? SliverList( delegate: SliverChildListDelegate( - watchTabWidgets, + watchTabWidgets(ref), ), ) : SliverGrid( @@ -161,7 +168,7 @@ class _WatchScreenState extends ConsumerState { crossAxisCount: 2, childAspectRatio: 0.92, ), - delegate: SliverChildListDelegate(watchTabWidgets), + delegate: SliverChildListDelegate(watchTabWidgets(ref)), ), ), ], @@ -183,14 +190,14 @@ Future _refreshData(WidgetRef ref) { } class _BroadcastWidget extends ConsumerWidget { - const _BroadcastWidget(); + final AsyncValue broadcastList; + + const _BroadcastWidget(this.broadcastList); static const int numberOfItems = 5; @override Widget build(BuildContext context, WidgetRef ref) { - final broadcastList = ref.watch(broadcastsPaginatorProvider); - return broadcastList.when( data: (data) { return ListSection( @@ -292,12 +299,12 @@ class _BroadcastTile extends ConsumerWidget { } class _WatchTvWidget extends ConsumerWidget { - const _WatchTvWidget(); + final AsyncValue> featuredChannels; + + const _WatchTvWidget(this.featuredChannels); @override Widget build(BuildContext context, WidgetRef ref) { - final featuredChannels = ref.watch(featuredChannelsProvider); - return featuredChannels.when( data: (data) { if (data.isEmpty) { @@ -356,15 +363,15 @@ class _WatchTvWidget extends ConsumerWidget { } class _StreamerWidget extends ConsumerWidget { - const _StreamerWidget(); + final AsyncValue> streamers; + + const _StreamerWidget(this.streamers); static const int numberOfItems = 10; @override Widget build(BuildContext context, WidgetRef ref) { - final streamerState = ref.watch(liveStreamersProvider); - - return streamerState.when( + return streamers.when( data: (data) { if (data.isEmpty) { return const SizedBox.shrink(); diff --git a/test/model/broadcast/broadcast_repository_test.dart b/test/model/broadcast/broadcast_repository_test.dart index ad7d6f31d3..d56e080a9a 100644 --- a/test/model/broadcast/broadcast_repository_test.dart +++ b/test/model/broadcast/broadcast_repository_test.dart @@ -33,7 +33,7 @@ void main() { final response = await repo.getBroadcasts(); - expect(response, isA()); + expect(response, isA()); expect(response.active.isNotEmpty, true); expect(response.upcoming.isNotEmpty, true); expect(response.past.isNotEmpty, true); From 93be9608e1a80d3a304fabf5cc530d6ce5ed6ab0 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 6 Dec 2024 18:02:08 +0100 Subject: [PATCH 851/979] Broadcast list improvements - better broadcast preview in watch tab - sticky headers in broadcast list - pull to refresh --- .../view/broadcast/broadcast_list_screen.dart | 150 ++++++++++-------- lib/src/view/watch/watch_tab_screen.dart | 44 ++--- lib/src/widgets/platform_scaffold.dart | 17 +- .../broadcasts_list_screen_test.dart | 2 +- 4 files changed, 118 insertions(+), 95 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_list_screen.dart b/lib/src/view/broadcast/broadcast_list_screen.dart index d3e2462155..50f73e7fe4 100644 --- a/lib/src/view/broadcast/broadcast_list_screen.dart +++ b/lib/src/view/broadcast/broadcast_list_screen.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:auto_size_text/auto_size_text.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -15,7 +16,7 @@ import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_round_screen.dart'; -import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; +import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; final _dateFormatter = DateFormat.MMMd().add_Hm(); @@ -31,16 +32,26 @@ class BroadcastListScreen extends StatelessWidget { @override Widget build(BuildContext context) { - return PlatformScaffold( - appBar: PlatformAppBar( - title: AutoSizeText( - context.l10n.broadcastBroadcasts, - minFontSize: 14.0, - overflow: TextOverflow.ellipsis, - maxLines: 1, + final title = AutoSizeText( + context.l10n.broadcastBroadcasts, + minFontSize: 14.0, + overflow: TextOverflow.ellipsis, + maxLines: 1, + ); + return PlatformWidget( + androidBuilder: (_) => Scaffold( + body: const _Body(), + appBar: AppBar(title: title), + ), + iosBuilder: (_) => CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + middle: title, + automaticBackgroundVisibility: false, + backgroundColor: Colors.transparent, + border: null, ), + child: const _Body(), ), - body: const _Body(), ); } } @@ -56,6 +67,9 @@ class _BodyState extends ConsumerState<_Body> { final ScrollController _scrollController = ScrollController(); ImageColorWorker? _worker; + final GlobalKey _refreshIndicatorKey = + GlobalKey(); + @override void initState() { super.initState(); @@ -121,69 +135,71 @@ class _BodyState extends ConsumerState<_Body> { childAspectRatio: 1.3, ); - return SafeArea( + final sections = [ + (context.l10n.broadcastOngoing, broadcasts.value!.active), + (context.l10n.broadcastUpcoming, broadcasts.value!.upcoming), + (context.l10n.broadcastCompleted, broadcasts.value!.past), + ]; + + return RefreshIndicator.adaptive( + edgeOffset: Theme.of(context).platform == TargetPlatform.iOS + ? MediaQuery.paddingOf(context).top + 16.0 + : 0, + key: _refreshIndicatorKey, + onRefresh: () async => ref.refresh(broadcastsPaginatorProvider), child: CustomScrollView( controller: _scrollController, slivers: [ - SliverPadding( - padding: Styles.bodySectionPadding, - sliver: SliverGrid.builder( - gridDelegate: gridDelegate, - itemBuilder: (context, index) => BroadcastGridItem( - broadcast: broadcasts.value!.active[index], - worker: _worker!, - ), - itemCount: broadcasts.value!.active.length, - ), - ), - SliverPadding( - padding: Styles.horizontalBodyPadding.add(Styles.sectionTopPadding), - sliver: SliverToBoxAdapter( - child: DefaultTextStyle.merge( - style: Styles.sectionTitle, - child: const Text('Upcoming broadcasts'), - ), - ), - ), - SliverPadding( - padding: Styles.bodySectionPadding, - sliver: SliverGrid.builder( - gridDelegate: gridDelegate, - itemBuilder: (context, index) => BroadcastGridItem( - worker: _worker!, - broadcast: broadcasts.value!.upcoming[index], - ), - itemCount: broadcasts.value!.upcoming.length, - ), - ), - SliverPadding( - padding: Styles.horizontalBodyPadding.add(Styles.sectionTopPadding), - sliver: SliverToBoxAdapter( - child: DefaultTextStyle.merge( - style: Styles.sectionTitle, - child: const Text('Past broadcasts'), - ), - ), - ), - SliverPadding( - padding: Styles.bodySectionPadding, - sliver: SliverGrid.builder( - gridDelegate: gridDelegate, - itemBuilder: (context, index) => - (broadcasts.isLoading && index >= itemsCount - loadingItems) - ? Shimmer( - child: ShimmerLoading( - isLoading: true, - child: BroadcastGridItem.loading(_worker!), + for (final section in sections) + SliverMainAxisGroup( + key: ValueKey(section), + slivers: [ + if (Theme.of(context).platform == TargetPlatform.iOS) + CupertinoSliverNavigationBar( + automaticallyImplyLeading: false, + leading: null, + largeTitle: AutoSizeText( + section.$1, + maxLines: 1, + minFontSize: 14, + overflow: TextOverflow.ellipsis, + ), + transitionBetweenRoutes: false, + ) + else + SliverAppBar( + automaticallyImplyLeading: false, + title: AutoSizeText( + section.$1, + maxLines: 1, + minFontSize: 14, + overflow: TextOverflow.ellipsis, + ), + pinned: true, + ), + SliverPadding( + padding: Theme.of(context).platform == TargetPlatform.iOS + ? Styles.horizontalBodyPadding + : Styles.bodySectionPadding, + sliver: SliverGrid.builder( + gridDelegate: gridDelegate, + itemBuilder: (context, index) => (broadcasts.isLoading && + index >= itemsCount - loadingItems) + ? Shimmer( + child: ShimmerLoading( + isLoading: true, + child: BroadcastGridItem.loading(_worker!), + ), + ) + : BroadcastGridItem( + worker: _worker!, + broadcast: section.$2[index], ), - ) - : BroadcastGridItem( - worker: _worker!, - broadcast: broadcasts.value!.past[index], - ), - itemCount: itemsCount, + itemCount: section.$2.length, + ), + ), + ], ), - ), ], ), ); diff --git a/lib/src/view/watch/watch_tab_screen.dart b/lib/src/view/watch/watch_tab_screen.dart index 7f2e6c42ba..aece6e45c4 100644 --- a/lib/src/view/watch/watch_tab_screen.dart +++ b/lib/src/view/watch/watch_tab_screen.dart @@ -4,6 +4,7 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_providers.dart'; import 'package:lichess_mobile/src/model/tv/featured_player.dart'; @@ -13,6 +14,7 @@ import 'package:lichess_mobile/src/model/tv/tv_repository.dart'; import 'package:lichess_mobile/src/model/user/user_repository_providers.dart'; import 'package:lichess_mobile/src/navigation.dart'; import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; @@ -242,6 +244,8 @@ class _BroadcastTile extends ConsumerWidget { final Broadcast broadcast; + static final _dateFormat = DateFormat.E().add_jm(); + @override Widget build(BuildContext context, WidgetRef ref) { return PlatformListTile( @@ -253,6 +257,7 @@ class _BroadcastTile extends ConsumerWidget { builder: (context) => BroadcastRoundScreen(broadcast: broadcast), ); }, + leading: const Icon(LichessIcons.radio_tower_lichess), title: Padding( padding: const EdgeInsets.only(right: 5.0), child: Row( @@ -267,26 +272,25 @@ class _BroadcastTile extends ConsumerWidget { ], ), ), - trailing: (broadcast.isLive) - ? Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.circle, - color: context.lichessColors.error, - size: 20, - ), - const SizedBox(height: 5), - Text( - 'LIVE', - style: TextStyle( - fontWeight: FontWeight.bold, - color: context.lichessColors.error, - ), - ), - ], - ) - : null, + trailing: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (broadcast.round.startsAt != null) + Text( + _dateFormat.format(broadcast.round.startsAt!), + style: const TextStyle(fontSize: 10.0), + ), + if (broadcast.isLive) + Text( + 'LIVE', + style: TextStyle( + fontSize: 12.0, + color: context.lichessColors.error, + fontWeight: FontWeight.bold, + ), + ), + ], + ), ); } } diff --git a/lib/src/widgets/platform_scaffold.dart b/lib/src/widgets/platform_scaffold.dart index d8c3020249..e8a0913e9c 100644 --- a/lib/src/widgets/platform_scaffold.dart +++ b/lib/src/widgets/platform_scaffold.dart @@ -126,7 +126,7 @@ class _CupertinoNavBarWrapper extends StatelessWidget class PlatformScaffold extends StatelessWidget { const PlatformScaffold({ super.key, - required this.appBar, + this.appBar, required this.body, this.resizeToAvoidBottomInset = true, }); @@ -134,7 +134,7 @@ class PlatformScaffold extends StatelessWidget { /// Acts as the [AppBar] for Android and as the [CupertinoNavigationBar] for iOS. /// /// Usually an instance of [PlatformAppBar]. - final Widget appBar; + final Widget? appBar; /// The main content of the screen, displayed below the navigation bar. final Widget body; @@ -145,10 +145,12 @@ class PlatformScaffold extends StatelessWidget { Widget _androidBuilder(BuildContext context) { return Scaffold( resizeToAvoidBottomInset: resizeToAvoidBottomInset, - appBar: PreferredSize( - preferredSize: const Size.fromHeight(kToolbarHeight), - child: appBar, - ), + appBar: appBar != null + ? PreferredSize( + preferredSize: const Size.fromHeight(kToolbarHeight), + child: appBar!, + ) + : null, body: body, ); } @@ -156,7 +158,8 @@ class PlatformScaffold extends StatelessWidget { Widget _iosBuilder(BuildContext context) { return CupertinoPageScaffold( resizeToAvoidBottomInset: resizeToAvoidBottomInset, - navigationBar: _CupertinoNavBarWrapper(child: appBar), + navigationBar: + appBar != null ? _CupertinoNavBarWrapper(child: appBar!) : null, child: body, ); } diff --git a/test/view/broadcast/broadcasts_list_screen_test.dart b/test/view/broadcast/broadcasts_list_screen_test.dart index b68ed3f660..7f31b21346 100644 --- a/test/view/broadcast/broadcasts_list_screen_test.dart +++ b/test/view/broadcast/broadcasts_list_screen_test.dart @@ -96,7 +96,7 @@ void main() { // wait for broadcast tournaments to load await tester.pump(const Duration(milliseconds: 100)); - await tester.scrollUntilVisible(find.text('Past broadcasts'), 200.0); + await tester.scrollUntilVisible(find.text('Completed'), 200.0); await tester.pumpAndSettle(); }); From 1516e7be02e5bc4b137fccd6b923dc8c35921063 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 6 Dec 2024 19:19:45 +0100 Subject: [PATCH 852/979] Fix tablet landscape layout --- lib/src/view/watch/watch_tab_screen.dart | 97 ++++++++++++------------ 1 file changed, 48 insertions(+), 49 deletions(-) diff --git a/lib/src/view/watch/watch_tab_screen.dart b/lib/src/view/watch/watch_tab_screen.dart index 044c732915..6597a908bd 100644 --- a/lib/src/view/watch/watch_tab_screen.dart +++ b/lib/src/view/watch/watch_tab_screen.dart @@ -88,18 +88,6 @@ class _WatchScreenState extends ConsumerState { ); } - List watchTabWidgets(WidgetRef ref) { - final broadcastList = ref.watch(broadcastsPaginatorProvider); - final featuredChannels = ref.watch(featuredChannelsProvider); - final streamers = ref.watch(liveStreamersProvider); - - return [ - _BroadcastWidget(broadcastList), - _WatchTvWidget(featuredChannels), - _StreamerWidget(streamers), - ]; - } - Widget _buildAndroid(BuildContext context, WidgetRef ref) { return PopScope( canPop: false, @@ -112,29 +100,14 @@ class _WatchScreenState extends ConsumerState { appBar: AppBar( title: Text(context.l10n.watch), ), - body: RefreshIndicator( - key: _androidRefreshKey, - onRefresh: refreshData, - child: SafeArea( - child: OrientationBuilder( - builder: (context, orientation) { - return orientation == Orientation.portrait - ? ListView( - controller: watchScrollController, - children: watchTabWidgets(ref), - ) - : GridView( - controller: watchScrollController, - gridDelegate: - const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - childAspectRatio: 0.92, - ), - children: watchTabWidgets(ref), - ); - }, - ), - ), + body: OrientationBuilder( + builder: (context, orientation) { + return RefreshIndicator( + key: _androidRefreshKey, + onRefresh: refreshData, + child: _Body(orientation), + ); + }, ), ), ); @@ -158,20 +131,7 @@ class _WatchScreenState extends ConsumerState { ), SliverSafeArea( top: false, - sliver: orientation == Orientation.portrait - ? SliverList( - delegate: SliverChildListDelegate( - watchTabWidgets(ref), - ), - ) - : SliverGrid( - gridDelegate: - const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - childAspectRatio: 0.92, - ), - delegate: SliverChildListDelegate(watchTabWidgets(ref)), - ), + sliver: _Body(orientation), ), ], ); @@ -183,6 +143,45 @@ class _WatchScreenState extends ConsumerState { Future refreshData() => _refreshData(ref); } +class _Body extends ConsumerWidget { + const _Body(this.orientation); + + final Orientation orientation; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final broadcastList = ref.watch(broadcastsPaginatorProvider); + final featuredChannels = ref.watch(featuredChannelsProvider); + final streamers = ref.watch(liveStreamersProvider); + + final content = orientation == Orientation.portrait + ? [ + _BroadcastWidget(broadcastList), + _WatchTvWidget(featuredChannels), + _StreamerWidget(streamers), + ] + : [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(child: _BroadcastWidget(broadcastList)), + Expanded(child: _WatchTvWidget(featuredChannels)), + ], + ), + _StreamerWidget(streamers), + ]; + + return Theme.of(context).platform == TargetPlatform.iOS + ? SliverList( + delegate: SliverChildListDelegate(content), + ) + : ListView( + controller: watchScrollController, + children: content, + ); + } +} + Future _refreshData(WidgetRef ref) { return Future.wait([ ref.refresh(broadcastsPaginatorProvider.future), From 7fb0301ab6f662238008840ffefc6ae0c98a263a Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 6 Dec 2024 19:42:17 +0100 Subject: [PATCH 853/979] Remove useless SafeArea to fix cupertino headers --- lib/src/view/puzzle/dashboard_screen.dart | 11 +- lib/src/view/puzzle/opening_screen.dart | 69 +- lib/src/view/puzzle/puzzle_themes_screen.dart | 81 +- .../settings/account_preferences_screen.dart | 731 +++++++++--------- .../view/settings/board_settings_screen.dart | 404 +++++----- .../view/settings/sound_settings_screen.dart | 52 +- lib/src/view/settings/theme_screen.dart | 270 ++++--- lib/src/view/study/study_list_screen.dart | 2 +- lib/src/view/user/game_history_screen.dart | 104 ++- lib/src/view/user/perf_stats_screen.dart | 456 ++++++----- lib/src/view/user/player_screen.dart | 20 +- 11 files changed, 1084 insertions(+), 1116 deletions(-) diff --git a/lib/src/view/puzzle/dashboard_screen.dart b/lib/src/view/puzzle/dashboard_screen.dart index 705c13a762..7cb172be93 100644 --- a/lib/src/view/puzzle/dashboard_screen.dart +++ b/lib/src/view/puzzle/dashboard_screen.dart @@ -39,13 +39,10 @@ class _Body extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return SafeArea( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - PuzzleDashboardWidget(), - ], - ), + return ListView( + children: [ + PuzzleDashboardWidget(), + ], ); } } diff --git a/lib/src/view/puzzle/opening_screen.dart b/lib/src/view/puzzle/opening_screen.dart index 62166de177..b2386aab32 100644 --- a/lib/src/view/puzzle/opening_screen.dart +++ b/lib/src/view/puzzle/opening_screen.dart @@ -53,40 +53,41 @@ class _Body extends ConsumerWidget { : null; final openings = ref.watch(_openingsProvider); - return SafeArea( - child: openings.when( - data: (data) { - final (isOnline, savedOpenings, onlineOpenings) = data; - if (isOnline && onlineOpenings != null) { - return ListView( - children: [ - for (final openingFamily in onlineOpenings) - _OpeningFamily( - openingFamily: openingFamily, - titleStyle: titleStyle, - ), - ], - ); - } else { - return ListSection( - children: [ - for (final openingKey in savedOpenings.keys) - _OpeningTile( - name: openingKey.replaceAll('_', ' '), - openingKey: openingKey, - count: savedOpenings[openingKey]!, - titleStyle: titleStyle, - ), - ], - ); - } - }, - error: (error, stack) { - return const Center(child: Text('Could not load openings.')); - }, - loading: () => - const Center(child: CircularProgressIndicator.adaptive()), - ), + return openings.when( + data: (data) { + final (isOnline, savedOpenings, onlineOpenings) = data; + if (isOnline && onlineOpenings != null) { + return ListView( + children: [ + for (final openingFamily in onlineOpenings) + _OpeningFamily( + openingFamily: openingFamily, + titleStyle: titleStyle, + ), + ], + ); + } else { + return ListView( + children: [ + ListSection( + children: [ + for (final openingKey in savedOpenings.keys) + _OpeningTile( + name: openingKey.replaceAll('_', ' '), + openingKey: openingKey, + count: savedOpenings[openingKey]!, + titleStyle: titleStyle, + ), + ], + ), + ], + ); + } + }, + error: (error, stack) { + return const Center(child: Text('Could not load openings.')); + }, + loading: () => const Center(child: CircularProgressIndicator.adaptive()), ); } } diff --git a/lib/src/view/puzzle/puzzle_themes_screen.dart b/lib/src/view/puzzle/puzzle_themes_screen.dart index e29e793f05..5f080eae17 100644 --- a/lib/src/view/puzzle/puzzle_themes_screen.dart +++ b/lib/src/view/puzzle/puzzle_themes_screen.dart @@ -67,51 +67,48 @@ class _Body extends ConsumerWidget { ? CupertinoColors.secondaryLabel.resolveFrom(context) : null; - return SafeArea( - child: themes.when( - data: (data) { - final (hasConnectivity, savedThemes, onlineThemes, hasSavedOpenings) = - data; + return themes.when( + data: (data) { + final (hasConnectivity, savedThemes, onlineThemes, hasSavedOpenings) = + data; - final openingsAvailable = hasConnectivity || hasSavedOpenings; - return ListView( - children: [ - Theme( - data: Theme.of(context) - .copyWith(dividerColor: Colors.transparent), - child: Opacity( - opacity: openingsAvailable ? 1 : 0.5, - child: ExpansionTile( - iconColor: expansionTileColor, - collapsedIconColor: expansionTileColor, - title: Text(context.l10n.puzzleByOpenings), - trailing: const Icon(Icons.keyboard_arrow_right), - onExpansionChanged: openingsAvailable - ? (expanded) { - pushPlatformRoute( - context, - builder: (ctx) => const OpeningThemeScreen(), - ); - } - : null, - ), + final openingsAvailable = hasConnectivity || hasSavedOpenings; + return ListView( + children: [ + Theme( + data: + Theme.of(context).copyWith(dividerColor: Colors.transparent), + child: Opacity( + opacity: openingsAvailable ? 1 : 0.5, + child: ExpansionTile( + iconColor: expansionTileColor, + collapsedIconColor: expansionTileColor, + title: Text(context.l10n.puzzleByOpenings), + trailing: const Icon(Icons.keyboard_arrow_right), + onExpansionChanged: openingsAvailable + ? (expanded) { + pushPlatformRoute( + context, + builder: (ctx) => const OpeningThemeScreen(), + ); + } + : null, ), ), - for (final category in list) - _Category( - hasConnectivity: hasConnectivity, - category: category, - onlineThemes: onlineThemes, - savedThemes: savedThemes, - ), - ], - ); - }, - loading: () => - const Center(child: CircularProgressIndicator.adaptive()), - error: (error, stack) => - const Center(child: Text('Could not load themes.')), - ), + ), + for (final category in list) + _Category( + hasConnectivity: hasConnectivity, + category: category, + onlineThemes: onlineThemes, + savedThemes: savedThemes, + ), + ], + ); + }, + loading: () => const Center(child: CircularProgressIndicator.adaptive()), + error: (error, stack) => + const Center(child: Text('Could not load themes.')), ); } } diff --git a/lib/src/view/settings/account_preferences_screen.dart b/lib/src/view/settings/account_preferences_screen.dart index ac283ef583..f6797646d3 100644 --- a/lib/src/view/settings/account_preferences_screen.dart +++ b/lib/src/view/settings/account_preferences_screen.dart @@ -47,395 +47,384 @@ class _AccountPreferencesScreenState ); } - return SafeArea( - child: ListView( - children: [ - ListSection( - header: SettingsSectionTitle(context.l10n.preferencesDisplay), - hasLeading: false, - children: [ - SettingsListTile( - settingsLabel: Text( - context.l10n.preferencesZenMode, - ), - settingsValue: data.zenMode.label(context), - showCupertinoTrailingValue: false, - onTap: () { - if (Theme.of(context).platform == - TargetPlatform.android) { - showChoicePicker( - context, - choices: Zen.values, - selectedItem: data.zenMode, - labelBuilder: (t) => Text(t.label(context)), - onSelectedItemChanged: isLoading - ? null - : (Zen? value) { - _setPref( - () => ref - .read( - accountPreferencesProvider.notifier, - ) - .setZen(value ?? data.zenMode), - ); - }, - ); - } else { - pushPlatformRoute( - context, - title: context.l10n.preferencesZenMode, - builder: (context) => const ZenSettingsScreen(), - ); - } - }, + return ListView( + children: [ + ListSection( + header: SettingsSectionTitle(context.l10n.preferencesDisplay), + hasLeading: false, + children: [ + SettingsListTile( + settingsLabel: Text( + context.l10n.preferencesZenMode, ), - SettingsListTile( - settingsLabel: Text( - context.l10n.preferencesPgnPieceNotation, - ), - settingsValue: data.pieceNotation.label(context), - showCupertinoTrailingValue: false, - onTap: () { - if (Theme.of(context).platform == - TargetPlatform.android) { - showChoicePicker( - context, - choices: PieceNotation.values, - selectedItem: data.pieceNotation, - labelBuilder: (t) => Text(t.label(context)), - onSelectedItemChanged: isLoading - ? null - : (PieceNotation? value) { - _setPref( - () => ref - .read( - accountPreferencesProvider.notifier, - ) - .setPieceNotation( - value ?? data.pieceNotation, - ), - ); - }, - ); - } else { - pushPlatformRoute( - context, - title: context.l10n.preferencesPgnPieceNotation, - builder: (context) => - const PieceNotationSettingsScreen(), - ); - } - }, + settingsValue: data.zenMode.label(context), + showCupertinoTrailingValue: false, + onTap: () { + if (Theme.of(context).platform == TargetPlatform.android) { + showChoicePicker( + context, + choices: Zen.values, + selectedItem: data.zenMode, + labelBuilder: (t) => Text(t.label(context)), + onSelectedItemChanged: isLoading + ? null + : (Zen? value) { + _setPref( + () => ref + .read( + accountPreferencesProvider.notifier, + ) + .setZen(value ?? data.zenMode), + ); + }, + ); + } else { + pushPlatformRoute( + context, + title: context.l10n.preferencesZenMode, + builder: (context) => const ZenSettingsScreen(), + ); + } + }, + ), + SettingsListTile( + settingsLabel: Text( + context.l10n.preferencesPgnPieceNotation, ), - SwitchSettingTile( - title: Text(context.l10n.preferencesShowPlayerRatings), - subtitle: Text( - context.l10n.preferencesExplainShowPlayerRatings, - maxLines: 5, - textAlign: TextAlign.justify, - ), - value: data.showRatings.value, - onChanged: isLoading - ? null - : (value) { - _setPref( - () => ref - .read(accountPreferencesProvider.notifier) - .setShowRatings(BooleanPref(value)), - ); - }, + settingsValue: data.pieceNotation.label(context), + showCupertinoTrailingValue: false, + onTap: () { + if (Theme.of(context).platform == TargetPlatform.android) { + showChoicePicker( + context, + choices: PieceNotation.values, + selectedItem: data.pieceNotation, + labelBuilder: (t) => Text(t.label(context)), + onSelectedItemChanged: isLoading + ? null + : (PieceNotation? value) { + _setPref( + () => ref + .read( + accountPreferencesProvider.notifier, + ) + .setPieceNotation( + value ?? data.pieceNotation, + ), + ); + }, + ); + } else { + pushPlatformRoute( + context, + title: context.l10n.preferencesPgnPieceNotation, + builder: (context) => + const PieceNotationSettingsScreen(), + ); + } + }, + ), + SwitchSettingTile( + title: Text(context.l10n.preferencesShowPlayerRatings), + subtitle: Text( + context.l10n.preferencesExplainShowPlayerRatings, + maxLines: 5, + textAlign: TextAlign.justify, ), - ], - ), - ListSection( - header: - SettingsSectionTitle(context.l10n.preferencesGameBehavior), - hasLeading: false, - children: [ - SwitchSettingTile( - title: Text( - context.l10n.preferencesPremovesPlayingDuringOpponentTurn, - ), - value: data.premove.value, - onChanged: isLoading - ? null - : (value) { - _setPref( - () => ref - .read(accountPreferencesProvider.notifier) - .setPremove(BooleanPref(value)), - ); - }, + value: data.showRatings.value, + onChanged: isLoading + ? null + : (value) { + _setPref( + () => ref + .read(accountPreferencesProvider.notifier) + .setShowRatings(BooleanPref(value)), + ); + }, + ), + ], + ), + ListSection( + header: + SettingsSectionTitle(context.l10n.preferencesGameBehavior), + hasLeading: false, + children: [ + SwitchSettingTile( + title: Text( + context.l10n.preferencesPremovesPlayingDuringOpponentTurn, ), - SwitchSettingTile( - title: Text( - context.l10n.preferencesConfirmResignationAndDrawOffers, - ), - value: data.confirmResign.value, - onChanged: isLoading - ? null - : (value) { - _setPref( - () => ref - .read(accountPreferencesProvider.notifier) - .setConfirmResign(BooleanPref(value)), - ); - }, + value: data.premove.value, + onChanged: isLoading + ? null + : (value) { + _setPref( + () => ref + .read(accountPreferencesProvider.notifier) + .setPremove(BooleanPref(value)), + ); + }, + ), + SwitchSettingTile( + title: Text( + context.l10n.preferencesConfirmResignationAndDrawOffers, ), - SettingsListTile( - settingsLabel: Text( - context.l10n.preferencesTakebacksWithOpponentApproval, - ), - settingsValue: data.takeback.label(context), - showCupertinoTrailingValue: false, - onTap: () { - if (Theme.of(context).platform == - TargetPlatform.android) { - showChoicePicker( - context, - choices: Takeback.values, - selectedItem: data.takeback, - labelBuilder: (t) => Text(t.label(context)), - onSelectedItemChanged: isLoading - ? null - : (Takeback? value) { - _setPref( - () => ref - .read( - accountPreferencesProvider.notifier, - ) - .setTakeback(value ?? data.takeback), - ); - }, - ); - } else { - pushPlatformRoute( - context, - title: context - .l10n.preferencesTakebacksWithOpponentApproval, - builder: (context) => const TakebackSettingsScreen(), - ); - } - }, + value: data.confirmResign.value, + onChanged: isLoading + ? null + : (value) { + _setPref( + () => ref + .read(accountPreferencesProvider.notifier) + .setConfirmResign(BooleanPref(value)), + ); + }, + ), + SettingsListTile( + settingsLabel: Text( + context.l10n.preferencesTakebacksWithOpponentApproval, ), - SettingsListTile( - settingsLabel: Text( - context.l10n.preferencesPromoteToQueenAutomatically, - ), - settingsValue: data.autoQueen.label(context), - showCupertinoTrailingValue: false, - onTap: () { - if (Theme.of(context).platform == - TargetPlatform.android) { - showChoicePicker( - context, - choices: AutoQueen.values, - selectedItem: data.autoQueen, - labelBuilder: (t) => Text(t.label(context)), - onSelectedItemChanged: isLoading - ? null - : (AutoQueen? value) { - _setPref( - () => ref - .read( - accountPreferencesProvider.notifier, - ) - .setAutoQueen(value ?? data.autoQueen), - ); - }, - ); - } else { - pushPlatformRoute( - context, - title: context - .l10n.preferencesPromoteToQueenAutomatically, - builder: (context) => const AutoQueenSettingsScreen(), - ); - } - }, + settingsValue: data.takeback.label(context), + showCupertinoTrailingValue: false, + onTap: () { + if (Theme.of(context).platform == TargetPlatform.android) { + showChoicePicker( + context, + choices: Takeback.values, + selectedItem: data.takeback, + labelBuilder: (t) => Text(t.label(context)), + onSelectedItemChanged: isLoading + ? null + : (Takeback? value) { + _setPref( + () => ref + .read( + accountPreferencesProvider.notifier, + ) + .setTakeback(value ?? data.takeback), + ); + }, + ); + } else { + pushPlatformRoute( + context, + title: context + .l10n.preferencesTakebacksWithOpponentApproval, + builder: (context) => const TakebackSettingsScreen(), + ); + } + }, + ), + SettingsListTile( + settingsLabel: Text( + context.l10n.preferencesPromoteToQueenAutomatically, ), - SettingsListTile( - settingsLabel: Text( - context.l10n - .preferencesClaimDrawOnThreefoldRepetitionAutomatically, - ), - settingsValue: data.autoThreefold.label(context), - showCupertinoTrailingValue: false, - onTap: () { - if (Theme.of(context).platform == - TargetPlatform.android) { - showChoicePicker( - context, - choices: AutoThreefold.values, - selectedItem: data.autoThreefold, - labelBuilder: (t) => Text(t.label(context)), - onSelectedItemChanged: isLoading - ? null - : (AutoThreefold? value) { - _setPref( - () => ref - .read( - accountPreferencesProvider.notifier, - ) - .setAutoThreefold( - value ?? data.autoThreefold, - ), - ); - }, - ); - } else { - pushPlatformRoute( - context, - title: context.l10n - .preferencesClaimDrawOnThreefoldRepetitionAutomatically, - builder: (context) => - const AutoThreefoldSettingsScreen(), + settingsValue: data.autoQueen.label(context), + showCupertinoTrailingValue: false, + onTap: () { + if (Theme.of(context).platform == TargetPlatform.android) { + showChoicePicker( + context, + choices: AutoQueen.values, + selectedItem: data.autoQueen, + labelBuilder: (t) => Text(t.label(context)), + onSelectedItemChanged: isLoading + ? null + : (AutoQueen? value) { + _setPref( + () => ref + .read( + accountPreferencesProvider.notifier, + ) + .setAutoQueen(value ?? data.autoQueen), + ); + }, + ); + } else { + pushPlatformRoute( + context, + title: + context.l10n.preferencesPromoteToQueenAutomatically, + builder: (context) => const AutoQueenSettingsScreen(), + ); + } + }, + ), + SettingsListTile( + settingsLabel: Text( + context.l10n + .preferencesClaimDrawOnThreefoldRepetitionAutomatically, + ), + settingsValue: data.autoThreefold.label(context), + showCupertinoTrailingValue: false, + onTap: () { + if (Theme.of(context).platform == TargetPlatform.android) { + showChoicePicker( + context, + choices: AutoThreefold.values, + selectedItem: data.autoThreefold, + labelBuilder: (t) => Text(t.label(context)), + onSelectedItemChanged: isLoading + ? null + : (AutoThreefold? value) { + _setPref( + () => ref + .read( + accountPreferencesProvider.notifier, + ) + .setAutoThreefold( + value ?? data.autoThreefold, + ), + ); + }, + ); + } else { + pushPlatformRoute( + context, + title: context.l10n + .preferencesClaimDrawOnThreefoldRepetitionAutomatically, + builder: (context) => + const AutoThreefoldSettingsScreen(), + ); + } + }, + ), + SettingsListTile( + settingsLabel: Text( + context.l10n.preferencesMoveConfirmation, + ), + settingsValue: data.submitMove.label(context), + showCupertinoTrailingValue: false, + onTap: () { + showMultipleChoicesPicker( + context, + choices: SubmitMoveChoice.values, + selectedItems: data.submitMove.choices, + labelBuilder: (t) => Text(t.label(context)), + ).then((value) { + if (value != null) { + _setPref( + () => ref + .read(accountPreferencesProvider.notifier) + .setSubmitMove(SubmitMove(value)), ); } - }, + }); + }, + explanation: context + .l10n.preferencesExplainCanThenBeTemporarilyDisabled, + ), + ], + ), + ListSection( + header: SettingsSectionTitle(context.l10n.preferencesChessClock), + hasLeading: false, + children: [ + SettingsListTile( + settingsLabel: Text( + context.l10n.preferencesGiveMoreTime, ), - SettingsListTile( - settingsLabel: Text( - context.l10n.preferencesMoveConfirmation, - ), - settingsValue: data.submitMove.label(context), - showCupertinoTrailingValue: false, - onTap: () { - showMultipleChoicesPicker( + settingsValue: data.moretime.label(context), + showCupertinoTrailingValue: false, + onTap: () { + if (Theme.of(context).platform == TargetPlatform.android) { + showChoicePicker( context, - choices: SubmitMoveChoice.values, - selectedItems: data.submitMove.choices, + choices: Moretime.values, + selectedItem: data.moretime, labelBuilder: (t) => Text(t.label(context)), - ).then((value) { - if (value != null) { + onSelectedItemChanged: isLoading + ? null + : (Moretime? value) { + _setPref( + () => ref + .read( + accountPreferencesProvider.notifier, + ) + .setMoretime(value ?? data.moretime), + ); + }, + ); + } else { + pushPlatformRoute( + context, + title: context.l10n.preferencesGiveMoreTime, + builder: (context) => const MoretimeSettingsScreen(), + ); + } + }, + ), + SwitchSettingTile( + title: + Text(context.l10n.preferencesSoundWhenTimeGetsCritical), + value: data.clockSound.value, + onChanged: isLoading + ? null + : (value) { _setPref( () => ref .read(accountPreferencesProvider.notifier) - .setSubmitMove(SubmitMove(value)), + .setClockSound(BooleanPref(value)), ); - } - }); - }, - explanation: context - .l10n.preferencesExplainCanThenBeTemporarilyDisabled, - ), - ], - ), - ListSection( - header: - SettingsSectionTitle(context.l10n.preferencesChessClock), - hasLeading: false, - children: [ - SettingsListTile( - settingsLabel: Text( - context.l10n.preferencesGiveMoreTime, - ), - settingsValue: data.moretime.label(context), - showCupertinoTrailingValue: false, - onTap: () { - if (Theme.of(context).platform == - TargetPlatform.android) { - showChoicePicker( - context, - choices: Moretime.values, - selectedItem: data.moretime, - labelBuilder: (t) => Text(t.label(context)), - onSelectedItemChanged: isLoading - ? null - : (Moretime? value) { - _setPref( - () => ref - .read( - accountPreferencesProvider.notifier, - ) - .setMoretime(value ?? data.moretime), - ); - }, - ); - } else { - pushPlatformRoute( - context, - title: context.l10n.preferencesGiveMoreTime, - builder: (context) => const MoretimeSettingsScreen(), - ); - } - }, - ), - SwitchSettingTile( - title: - Text(context.l10n.preferencesSoundWhenTimeGetsCritical), - value: data.clockSound.value, - onChanged: isLoading - ? null - : (value) { - _setPref( - () => ref - .read(accountPreferencesProvider.notifier) - .setClockSound(BooleanPref(value)), - ); - }, - ), - ], - ), - ListSection( - header: SettingsSectionTitle(context.l10n.preferencesPrivacy), - hasLeading: false, - children: [ - SwitchSettingTile( - title: Text( - context.l10n.letOtherPlayersFollowYou, - ), - value: data.follow.value, - onChanged: isLoading - ? null - : (value) { - _setPref( - () => ref - .read(accountPreferencesProvider.notifier) - .setFollow(BooleanPref(value)), - ); - }, + }, + ), + ], + ), + ListSection( + header: SettingsSectionTitle(context.l10n.preferencesPrivacy), + hasLeading: false, + children: [ + SwitchSettingTile( + title: Text( + context.l10n.letOtherPlayersFollowYou, ), - SettingsListTile( - settingsLabel: Text( - context.l10n.letOtherPlayersChallengeYou, - ), - settingsValue: data.challenge.label(context), - showCupertinoTrailingValue: false, - onTap: () { - if (Theme.of(context).platform == - TargetPlatform.android) { - showChoicePicker( - context, - choices: Challenge.values, - selectedItem: data.challenge, - labelBuilder: (t) => Text(t.label(context)), - onSelectedItemChanged: isLoading - ? null - : (Challenge? value) { - _setPref( - () => ref - .read( - accountPreferencesProvider.notifier, - ) - .setChallenge(value ?? data.challenge), - ); - }, - ); - } else { - pushPlatformRoute( - context, - title: context.l10n.letOtherPlayersChallengeYou, - builder: (context) => - const _ChallengeSettingsScreen(), - ); - } - }, + value: data.follow.value, + onChanged: isLoading + ? null + : (value) { + _setPref( + () => ref + .read(accountPreferencesProvider.notifier) + .setFollow(BooleanPref(value)), + ); + }, + ), + SettingsListTile( + settingsLabel: Text( + context.l10n.letOtherPlayersChallengeYou, ), - ], - ), - ], - ), + settingsValue: data.challenge.label(context), + showCupertinoTrailingValue: false, + onTap: () { + if (Theme.of(context).platform == TargetPlatform.android) { + showChoicePicker( + context, + choices: Challenge.values, + selectedItem: data.challenge, + labelBuilder: (t) => Text(t.label(context)), + onSelectedItemChanged: isLoading + ? null + : (Challenge? value) { + _setPref( + () => ref + .read( + accountPreferencesProvider.notifier, + ) + .setChallenge(value ?? data.challenge), + ); + }, + ); + } else { + pushPlatformRoute( + context, + title: context.l10n.letOtherPlayersChallengeYou, + builder: (context) => const _ChallengeSettingsScreen(), + ); + } + }, + ), + ], + ), + ], ); }, loading: () => const Center(child: CircularProgressIndicator()), diff --git a/lib/src/view/settings/board_settings_screen.dart b/lib/src/view/settings/board_settings_screen.dart index 799b082214..d031528993 100644 --- a/lib/src/view/settings/board_settings_screen.dart +++ b/lib/src/view/settings/board_settings_screen.dart @@ -48,218 +48,214 @@ class _Body extends ConsumerWidget { final androidVersionAsync = ref.watch(androidVersionProvider); - return SafeArea( - child: ListView( - children: [ - ListSection( - header: SettingsSectionTitle(context.l10n.preferencesGameBehavior), - hasLeading: false, - showDivider: false, - children: [ - SettingsListTile( - settingsLabel: Text(context.l10n.preferencesHowDoYouMovePieces), - settingsValue: - pieceShiftMethodl10n(context, boardPrefs.pieceShiftMethod), - showCupertinoTrailingValue: false, - onTap: () { - if (Theme.of(context).platform == TargetPlatform.android) { - showChoicePicker( - context, - choices: PieceShiftMethod.values, - selectedItem: boardPrefs.pieceShiftMethod, - labelBuilder: (t) => - Text(pieceShiftMethodl10n(context, t)), - onSelectedItemChanged: (PieceShiftMethod? value) { - ref - .read(boardPreferencesProvider.notifier) - .setPieceShiftMethod( - value ?? PieceShiftMethod.either, - ); - }, - ); - } else { - pushPlatformRoute( - context, - title: context.l10n.preferencesHowDoYouMovePieces, - builder: (context) => - const PieceShiftMethodSettingsScreen(), - ); - } - }, - ), - SwitchSettingTile( - title: Text(context.l10n.mobilePrefMagnifyDraggedPiece), - value: boardPrefs.magnifyDraggedPiece, - onChanged: (value) { - ref - .read(boardPreferencesProvider.notifier) - .toggleMagnifyDraggedPiece(); - }, - ), - SettingsListTile( - // TODO translate - settingsLabel: const Text('Drag target'), - explanation: - // TODO translate - 'How the target square is highlighted when dragging a piece.', - settingsValue: dragTargetKindLabel(boardPrefs.dragTargetKind), - onTap: () { - if (Theme.of(context).platform == TargetPlatform.android) { - showChoicePicker( - context, - choices: DragTargetKind.values, - selectedItem: boardPrefs.dragTargetKind, - labelBuilder: (t) => Text(dragTargetKindLabel(t)), - onSelectedItemChanged: (DragTargetKind? value) { - ref - .read(boardPreferencesProvider.notifier) - .setDragTargetKind( - value ?? DragTargetKind.circle, - ); - }, - ); - } else { - pushPlatformRoute( - context, - title: 'Dragged piece target', - builder: (context) => - const DragTargetKindSettingsScreen(), - ); - } - }, - ), - SwitchSettingTile( - // TODO translate - title: const Text('Touch feedback'), - value: boardPrefs.hapticFeedback, - subtitle: const Text( + return ListView( + children: [ + ListSection( + header: SettingsSectionTitle(context.l10n.preferencesGameBehavior), + hasLeading: false, + showDivider: false, + children: [ + SettingsListTile( + settingsLabel: Text(context.l10n.preferencesHowDoYouMovePieces), + settingsValue: + pieceShiftMethodl10n(context, boardPrefs.pieceShiftMethod), + showCupertinoTrailingValue: false, + onTap: () { + if (Theme.of(context).platform == TargetPlatform.android) { + showChoicePicker( + context, + choices: PieceShiftMethod.values, + selectedItem: boardPrefs.pieceShiftMethod, + labelBuilder: (t) => Text(pieceShiftMethodl10n(context, t)), + onSelectedItemChanged: (PieceShiftMethod? value) { + ref + .read(boardPreferencesProvider.notifier) + .setPieceShiftMethod( + value ?? PieceShiftMethod.either, + ); + }, + ); + } else { + pushPlatformRoute( + context, + title: context.l10n.preferencesHowDoYouMovePieces, + builder: (context) => + const PieceShiftMethodSettingsScreen(), + ); + } + }, + ), + SwitchSettingTile( + title: Text(context.l10n.mobilePrefMagnifyDraggedPiece), + value: boardPrefs.magnifyDraggedPiece, + onChanged: (value) { + ref + .read(boardPreferencesProvider.notifier) + .toggleMagnifyDraggedPiece(); + }, + ), + SettingsListTile( + // TODO translate + settingsLabel: const Text('Drag target'), + explanation: // TODO translate - 'Vibrate when moving pieces or capturing them.', - maxLines: 5, - textAlign: TextAlign.justify, - ), - onChanged: (value) { - ref - .read(boardPreferencesProvider.notifier) - .toggleHapticFeedback(); - }, + 'How the target square is highlighted when dragging a piece.', + settingsValue: dragTargetKindLabel(boardPrefs.dragTargetKind), + onTap: () { + if (Theme.of(context).platform == TargetPlatform.android) { + showChoicePicker( + context, + choices: DragTargetKind.values, + selectedItem: boardPrefs.dragTargetKind, + labelBuilder: (t) => Text(dragTargetKindLabel(t)), + onSelectedItemChanged: (DragTargetKind? value) { + ref + .read(boardPreferencesProvider.notifier) + .setDragTargetKind( + value ?? DragTargetKind.circle, + ); + }, + ); + } else { + pushPlatformRoute( + context, + title: 'Dragged piece target', + builder: (context) => const DragTargetKindSettingsScreen(), + ); + } + }, + ), + SwitchSettingTile( + // TODO translate + title: const Text('Touch feedback'), + value: boardPrefs.hapticFeedback, + subtitle: const Text( + // TODO translate + 'Vibrate when moving pieces or capturing them.', + maxLines: 5, + textAlign: TextAlign.justify, ), - SwitchSettingTile( - title: Text( - context.l10n.preferencesPieceAnimation, - ), - value: boardPrefs.pieceAnimation, - onChanged: (value) { - ref - .read(boardPreferencesProvider.notifier) - .togglePieceAnimation(); - }, + onChanged: (value) { + ref + .read(boardPreferencesProvider.notifier) + .toggleHapticFeedback(); + }, + ), + SwitchSettingTile( + title: Text( + context.l10n.preferencesPieceAnimation, ), - SwitchSettingTile( - // TODO: Add l10n - title: const Text('Shape drawing'), - subtitle: const Text( - // TODO: translate - 'Draw shapes using two fingers: maintain one finger on an empty square and drag another finger to draw a shape.', - maxLines: 5, - textAlign: TextAlign.justify, - ), - value: boardPrefs.enableShapeDrawings, - onChanged: (value) { - ref - .read(boardPreferencesProvider.notifier) - .toggleEnableShapeDrawings(); - }, + value: boardPrefs.pieceAnimation, + onChanged: (value) { + ref + .read(boardPreferencesProvider.notifier) + .togglePieceAnimation(); + }, + ), + SwitchSettingTile( + // TODO: Add l10n + title: const Text('Shape drawing'), + subtitle: const Text( + // TODO: translate + 'Draw shapes using two fingers: maintain one finger on an empty square and drag another finger to draw a shape.', + maxLines: 5, + textAlign: TextAlign.justify, ), - ], - ), - ListSection( - header: SettingsSectionTitle(context.l10n.preferencesDisplay), - hasLeading: false, - showDivider: false, - children: [ - if (Theme.of(context).platform == TargetPlatform.android && - !isTabletOrLarger(context)) - androidVersionAsync.maybeWhen( - data: (version) => version != null && version.sdkInt >= 29 - ? SwitchSettingTile( - title: Text(context.l10n.mobileSettingsImmersiveMode), - subtitle: Text( - context.l10n.mobileSettingsImmersiveModeSubtitle, - textAlign: TextAlign.justify, - maxLines: 5, - ), - value: boardPrefs.immersiveModeWhilePlaying ?? false, - onChanged: (value) { - ref - .read(boardPreferencesProvider.notifier) - .toggleImmersiveModeWhilePlaying(); - }, - ) - : const SizedBox.shrink(), - orElse: () => const SizedBox.shrink(), - ), - SettingsListTile( - //TODO Add l10n - settingsLabel: const Text('Clock position'), - settingsValue: boardPrefs.clockPosition.label, - onTap: () { - if (Theme.of(context).platform == TargetPlatform.android) { - showChoicePicker( - context, - choices: ClockPosition.values, - selectedItem: boardPrefs.clockPosition, - labelBuilder: (t) => Text(t.label), - onSelectedItemChanged: (ClockPosition? value) => ref - .read(boardPreferencesProvider.notifier) - .setClockPosition(value ?? ClockPosition.right), - ); - } else { - pushPlatformRoute( - context, - title: 'Clock position', - builder: (context) => const BoardClockPositionScreen(), - ); - } - }, + value: boardPrefs.enableShapeDrawings, + onChanged: (value) { + ref + .read(boardPreferencesProvider.notifier) + .toggleEnableShapeDrawings(); + }, + ), + ], + ), + ListSection( + header: SettingsSectionTitle(context.l10n.preferencesDisplay), + hasLeading: false, + showDivider: false, + children: [ + if (Theme.of(context).platform == TargetPlatform.android && + !isTabletOrLarger(context)) + androidVersionAsync.maybeWhen( + data: (version) => version != null && version.sdkInt >= 29 + ? SwitchSettingTile( + title: Text(context.l10n.mobileSettingsImmersiveMode), + subtitle: Text( + context.l10n.mobileSettingsImmersiveModeSubtitle, + textAlign: TextAlign.justify, + maxLines: 5, + ), + value: boardPrefs.immersiveModeWhilePlaying ?? false, + onChanged: (value) { + ref + .read(boardPreferencesProvider.notifier) + .toggleImmersiveModeWhilePlaying(); + }, + ) + : const SizedBox.shrink(), + orElse: () => const SizedBox.shrink(), ), - SwitchSettingTile( - title: Text( - context.l10n.preferencesPieceDestinations, - ), - value: boardPrefs.showLegalMoves, - onChanged: (value) { - ref - .read(boardPreferencesProvider.notifier) - .toggleShowLegalMoves(); - }, + SettingsListTile( + //TODO Add l10n + settingsLabel: const Text('Clock position'), + settingsValue: boardPrefs.clockPosition.label, + onTap: () { + if (Theme.of(context).platform == TargetPlatform.android) { + showChoicePicker( + context, + choices: ClockPosition.values, + selectedItem: boardPrefs.clockPosition, + labelBuilder: (t) => Text(t.label), + onSelectedItemChanged: (ClockPosition? value) => ref + .read(boardPreferencesProvider.notifier) + .setClockPosition(value ?? ClockPosition.right), + ); + } else { + pushPlatformRoute( + context, + title: 'Clock position', + builder: (context) => const BoardClockPositionScreen(), + ); + } + }, + ), + SwitchSettingTile( + title: Text( + context.l10n.preferencesPieceDestinations, ), - SwitchSettingTile( - title: Text( - context.l10n.preferencesBoardHighlights, - ), - value: boardPrefs.boardHighlights, - onChanged: (value) { - ref - .read(boardPreferencesProvider.notifier) - .toggleBoardHighlights(); - }, + value: boardPrefs.showLegalMoves, + onChanged: (value) { + ref + .read(boardPreferencesProvider.notifier) + .toggleShowLegalMoves(); + }, + ), + SwitchSettingTile( + title: Text( + context.l10n.preferencesBoardHighlights, ), - SwitchSettingTile( - title: Text( - context.l10n.preferencesMaterialDifference, - ), - value: boardPrefs.showMaterialDifference, - onChanged: (value) { - ref - .read(boardPreferencesProvider.notifier) - .toggleShowMaterialDifference(); - }, + value: boardPrefs.boardHighlights, + onChanged: (value) { + ref + .read(boardPreferencesProvider.notifier) + .toggleBoardHighlights(); + }, + ), + SwitchSettingTile( + title: Text( + context.l10n.preferencesMaterialDifference, ), - ], - ), - ], - ), + value: boardPrefs.showMaterialDifference, + onChanged: (value) { + ref + .read(boardPreferencesProvider.notifier) + .toggleShowMaterialDifference(); + }, + ), + ], + ), + ], ); } } diff --git a/lib/src/view/settings/sound_settings_screen.dart b/lib/src/view/settings/sound_settings_screen.dart index e4d3fdee62..75010c06c4 100644 --- a/lib/src/view/settings/sound_settings_screen.dart +++ b/lib/src/view/settings/sound_settings_screen.dart @@ -70,33 +70,31 @@ class _Body extends ConsumerWidget { ); } - return SafeArea( - child: ListView( - children: [ - ListSection( - children: [ - SliderSettingsTile( - icon: const Icon(Icons.volume_up), - value: generalPrefs.masterVolume, - values: kMasterVolumeValues, - onChangeEnd: (value) { - ref - .read(generalPreferencesProvider.notifier) - .setMasterVolume(value); - }, - labelBuilder: volumeLabel, - ), - ], - ), - ChoicePicker( - notchedTile: true, - choices: SoundTheme.values, - selectedItem: generalPrefs.soundTheme, - titleBuilder: (t) => Text(soundThemeL10n(context, t)), - onSelectedItemChanged: onChanged, - ), - ], - ), + return ListView( + children: [ + ListSection( + children: [ + SliderSettingsTile( + icon: const Icon(Icons.volume_up), + value: generalPrefs.masterVolume, + values: kMasterVolumeValues, + onChangeEnd: (value) { + ref + .read(generalPreferencesProvider.notifier) + .setMasterVolume(value); + }, + labelBuilder: volumeLabel, + ), + ], + ), + ChoicePicker( + notchedTile: true, + choices: SoundTheme.values, + selectedItem: generalPrefs.soundTheme, + titleBuilder: (t) => Text(soundThemeL10n(context, t)), + onSelectedItemChanged: onChanged, + ), + ], ); } } diff --git a/lib/src/view/settings/theme_screen.dart b/lib/src/view/settings/theme_screen.dart index a4de7ca25d..2e16832dc7 100644 --- a/lib/src/view/settings/theme_screen.dart +++ b/lib/src/view/settings/theme_screen.dart @@ -51,150 +51,146 @@ class _Body extends ConsumerWidget { const horizontalPadding = 16.0; - return SafeArea( - child: ListView( - children: [ - LayoutBuilder( - builder: (context, constraints) { - final double boardSize = math.min( - 400, - constraints.biggest.shortestSide - horizontalPadding * 2, - ); - return Padding( - padding: const EdgeInsets.symmetric( - horizontal: horizontalPadding, - vertical: 16, - ), - child: Center( - child: Chessboard.fixed( - size: boardSize, - orientation: Side.white, - lastMove: const NormalMove(from: Square.e2, to: Square.e4), - fen: - 'rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1', - shapes: { - Circle( - color: boardPrefs.shapeColor.color, - orig: Square.fromName('b8'), - ), - Arrow( - color: boardPrefs.shapeColor.color, - orig: Square.fromName('b8'), - dest: Square.fromName('c6'), + return ListView( + children: [ + LayoutBuilder( + builder: (context, constraints) { + final double boardSize = math.min( + 400, + constraints.biggest.shortestSide - horizontalPadding * 2, + ); + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: horizontalPadding, + vertical: 16, + ), + child: Center( + child: Chessboard.fixed( + size: boardSize, + orientation: Side.white, + lastMove: const NormalMove(from: Square.e2, to: Square.e4), + fen: + 'rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1', + shapes: { + Circle( + color: boardPrefs.shapeColor.color, + orig: Square.fromName('b8'), + ), + Arrow( + color: boardPrefs.shapeColor.color, + orig: Square.fromName('b8'), + dest: Square.fromName('c6'), + ), + }.lock, + settings: boardPrefs.toBoardSettings().copyWith( + borderRadius: + const BorderRadius.all(Radius.circular(4.0)), + boxShadow: boardShadows, ), - }.lock, - settings: boardPrefs.toBoardSettings().copyWith( - borderRadius: - const BorderRadius.all(Radius.circular(4.0)), - boxShadow: boardShadows, - ), - ), - ), - ); - }, - ), - ListSection( - hasLeading: true, - children: [ - if (Theme.of(context).platform == TargetPlatform.android) - androidVersionAsync.maybeWhen( - data: (version) => version != null && version.sdkInt >= 31 - ? SwitchSettingTile( - leading: const Icon(Icons.colorize_outlined), - title: Text(context.l10n.mobileSystemColors), - value: generalPrefs.systemColors, - onChanged: (value) { - ref - .read(generalPreferencesProvider.notifier) - .toggleSystemColors(); - }, - ) - : const SizedBox.shrink(), - orElse: () => const SizedBox.shrink(), ), - SettingsListTile( - icon: const Icon(LichessIcons.chess_board), - settingsLabel: Text(context.l10n.board), - settingsValue: boardPrefs.boardTheme.label, - onTap: () { - pushPlatformRoute( - context, - title: context.l10n.board, - builder: (context) => const BoardThemeScreen(), - ); - }, ), - SettingsListTile( - icon: const Icon(LichessIcons.chess_pawn), - settingsLabel: Text(context.l10n.pieceSet), - settingsValue: boardPrefs.pieceSet.label, - onTap: () { - pushPlatformRoute( - context, - title: context.l10n.pieceSet, - builder: (context) => const PieceSetScreen(), - ); - }, + ); + }, + ), + ListSection( + hasLeading: true, + children: [ + if (Theme.of(context).platform == TargetPlatform.android) + androidVersionAsync.maybeWhen( + data: (version) => version != null && version.sdkInt >= 31 + ? SwitchSettingTile( + leading: const Icon(Icons.colorize_outlined), + title: Text(context.l10n.mobileSystemColors), + value: generalPrefs.systemColors, + onChanged: (value) { + ref + .read(generalPreferencesProvider.notifier) + .toggleSystemColors(); + }, + ) + : const SizedBox.shrink(), + orElse: () => const SizedBox.shrink(), ), - SettingsListTile( - icon: const Icon(LichessIcons.arrow_full_upperright), - settingsLabel: const Text('Shape color'), - settingsValue: shapeColorL10n(context, boardPrefs.shapeColor), - onTap: () { - showChoicePicker( - context, - choices: ShapeColor.values, - selectedItem: boardPrefs.shapeColor, - labelBuilder: (t) => Text.rich( - TextSpan( - children: [ - TextSpan( - text: shapeColorL10n(context, t), - ), - const TextSpan(text: ' '), - WidgetSpan( - child: Container( - width: 15, - height: 15, - color: t.color, - ), + SettingsListTile( + icon: const Icon(LichessIcons.chess_board), + settingsLabel: Text(context.l10n.board), + settingsValue: boardPrefs.boardTheme.label, + onTap: () { + pushPlatformRoute( + context, + title: context.l10n.board, + builder: (context) => const BoardThemeScreen(), + ); + }, + ), + SettingsListTile( + icon: const Icon(LichessIcons.chess_pawn), + settingsLabel: Text(context.l10n.pieceSet), + settingsValue: boardPrefs.pieceSet.label, + onTap: () { + pushPlatformRoute( + context, + title: context.l10n.pieceSet, + builder: (context) => const PieceSetScreen(), + ); + }, + ), + SettingsListTile( + icon: const Icon(LichessIcons.arrow_full_upperright), + settingsLabel: const Text('Shape color'), + settingsValue: shapeColorL10n(context, boardPrefs.shapeColor), + onTap: () { + showChoicePicker( + context, + choices: ShapeColor.values, + selectedItem: boardPrefs.shapeColor, + labelBuilder: (t) => Text.rich( + TextSpan( + children: [ + TextSpan( + text: shapeColorL10n(context, t), + ), + const TextSpan(text: ' '), + WidgetSpan( + child: Container( + width: 15, + height: 15, + color: t.color, ), - ], - ), + ), + ], ), - onSelectedItemChanged: (ShapeColor? value) { - ref - .read(boardPreferencesProvider.notifier) - .setShapeColor(value ?? ShapeColor.green); - }, - ); - }, - ), - SwitchSettingTile( - leading: const Icon(Icons.location_on), - title: Text( - context.l10n.preferencesBoardCoordinates, - ), - value: boardPrefs.coordinates, - onChanged: (value) { - ref - .read(boardPreferencesProvider.notifier) - .toggleCoordinates(); - }, - ), - SwitchSettingTile( - // TODO translate - leading: const Icon(Icons.border_outer), - title: const Text('Show border'), - value: boardPrefs.showBorder, - onChanged: (value) { - ref.read(boardPreferencesProvider.notifier).toggleBorder(); - }, + ), + onSelectedItemChanged: (ShapeColor? value) { + ref + .read(boardPreferencesProvider.notifier) + .setShapeColor(value ?? ShapeColor.green); + }, + ); + }, + ), + SwitchSettingTile( + leading: const Icon(Icons.location_on), + title: Text( + context.l10n.preferencesBoardCoordinates, ), - ], - ), - ], - ), + value: boardPrefs.coordinates, + onChanged: (value) { + ref.read(boardPreferencesProvider.notifier).toggleCoordinates(); + }, + ), + SwitchSettingTile( + // TODO translate + leading: const Icon(Icons.border_outer), + title: const Text('Show border'), + value: boardPrefs.showBorder, + onChanged: (value) { + ref.read(boardPreferencesProvider.notifier).toggleBorder(); + }, + ), + ], + ), + ], ); } } diff --git a/lib/src/view/study/study_list_screen.dart b/lib/src/view/study/study_list_screen.dart index d76515cf18..f209737c6d 100644 --- a/lib/src/view/study/study_list_screen.dart +++ b/lib/src/view/study/study_list_screen.dart @@ -55,7 +55,7 @@ class StudyListScreen extends ConsumerWidget { ), ], ), - body: SafeArea(child: _Body(filter: filter)), + body: SafeArea(top: false, child: _Body(filter: filter)), ); } } diff --git a/lib/src/view/user/game_history_screen.dart b/lib/src/view/user/game_history_screen.dart index 0d01215b3e..93cd532e69 100644 --- a/lib/src/view/user/game_history_screen.dart +++ b/lib/src/view/user/game_history_screen.dart @@ -157,63 +157,61 @@ class _BodyState extends ConsumerState<_Body> { data: (state) { final list = state.gameList; - return SafeArea( - child: list.isEmpty - ? const Padding( - padding: EdgeInsets.symmetric(vertical: 32.0), - child: Center( - child: Text( - 'No games found', - ), + return list.isEmpty + ? const Padding( + padding: EdgeInsets.symmetric(vertical: 32.0), + child: Center( + child: Text( + 'No games found', ), - ) - : ListView.separated( - controller: _scrollController, - separatorBuilder: (context, index) => - Theme.of(context).platform == TargetPlatform.iOS - ? const PlatformDivider( - height: 1, - cupertinoHasLeading: true, - ) - : const PlatformDivider( - height: 1, - color: Colors.transparent, - ), - itemCount: list.length + (state.isLoading ? 1 : 0), - itemBuilder: (context, index) { - if (state.isLoading && index == list.length) { - return const Padding( - padding: EdgeInsets.symmetric(vertical: 32.0), - child: CenterLoadingIndicator(), - ); - } else if (state.hasError && - state.hasMore && - index == list.length) { - // TODO: add a retry button - return const Padding( - padding: EdgeInsets.symmetric(vertical: 32.0), - child: Center( - child: Text( - 'Could not load more games', + ), + ) + : ListView.separated( + controller: _scrollController, + separatorBuilder: (context, index) => + Theme.of(context).platform == TargetPlatform.iOS + ? const PlatformDivider( + height: 1, + cupertinoHasLeading: true, + ) + : const PlatformDivider( + height: 1, + color: Colors.transparent, ), + itemCount: list.length + (state.isLoading ? 1 : 0), + itemBuilder: (context, index) { + if (state.isLoading && index == list.length) { + return const Padding( + padding: EdgeInsets.symmetric(vertical: 32.0), + child: CenterLoadingIndicator(), + ); + } else if (state.hasError && + state.hasMore && + index == list.length) { + // TODO: add a retry button + return const Padding( + padding: EdgeInsets.symmetric(vertical: 32.0), + child: Center( + child: Text( + 'Could not load more games', ), - ); - } - - return ExtendedGameListTile( - item: list[index], - userId: widget.user?.id, - // see: https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/cupertino/list_tile.dart#L30 for horizontal padding value - padding: Theme.of(context).platform == TargetPlatform.iOS - ? const EdgeInsets.symmetric( - horizontal: 14.0, - vertical: 12.0, - ) - : null, + ), ); - }, - ), - ); + } + + return ExtendedGameListTile( + item: list[index], + userId: widget.user?.id, + // see: https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/cupertino/list_tile.dart#L30 for horizontal padding value + padding: Theme.of(context).platform == TargetPlatform.iOS + ? const EdgeInsets.symmetric( + horizontal: 14.0, + vertical: 12.0, + ) + : null, + ); + }, + ); }, error: (e, s) { debugPrint( diff --git a/lib/src/view/user/perf_stats_screen.dart b/lib/src/view/user/perf_stats_screen.dart index bbb6a612d9..9db550fd7d 100644 --- a/lib/src/view/user/perf_stats_screen.dart +++ b/lib/src/view/user/perf_stats_screen.dart @@ -151,268 +151,266 @@ class _Body extends ConsumerWidget { return perfStats.when( data: (data) { - return SafeArea( - child: ListView( - padding: Styles.bodyPadding, - scrollDirection: Axis.vertical, - children: [ - ratingHistory.when( - data: (ratingHistoryData) { - final ratingHistoryPerfData = ratingHistoryData - .firstWhereOrNull((element) => element.perf == perf); - - if (ratingHistoryPerfData == null || - ratingHistoryPerfData.points.isEmpty) { - return const SizedBox.shrink(); - } - return _EloChart(ratingHistoryPerfData); - }, - error: (error, stackTrace) { - debugPrint( - 'SEVERE: [PerfStatsScreen] could not load rating history data; $error\n$stackTrace', - ); - return const Text('Could not show chart elo chart'); - }, - loading: () { + return ListView( + padding: Styles.bodyPadding.add(MediaQuery.paddingOf(context)), + scrollDirection: Axis.vertical, + children: [ + ratingHistory.when( + data: (ratingHistoryData) { + final ratingHistoryPerfData = ratingHistoryData + .firstWhereOrNull((element) => element.perf == perf); + + if (ratingHistoryPerfData == null || + ratingHistoryPerfData.points.isEmpty) { return const SizedBox.shrink(); - }, - ), - Row( - crossAxisAlignment: CrossAxisAlignment.baseline, - textBaseline: TextBaseline.alphabetic, - children: [ - Text( - '${context.l10n.rating} ', - style: Styles.sectionTitle, - ), - RatingWidget( - rating: data.rating, - deviation: data.deviation, - provisional: data.provisional, - style: _mainValueStyle, - ), - ], + } + return _EloChart(ratingHistoryPerfData); + }, + error: (error, stackTrace) { + debugPrint( + 'SEVERE: [PerfStatsScreen] could not load rating history data; $error\n$stackTrace', + ); + return const Text('Could not show chart elo chart'); + }, + loading: () { + return const SizedBox.shrink(); + }, + ), + Row( + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + Text( + '${context.l10n.rating} ', + style: Styles.sectionTitle, + ), + RatingWidget( + rating: data.rating, + deviation: data.deviation, + provisional: data.provisional, + style: _mainValueStyle, + ), + ], + ), + if (perf != Perf.puzzle) ...[ + if (data.percentile != null && data.percentile! > 0.0) + Text( + (loggedInUser != null && loggedInUser.user.id == user.id) + ? context.l10n.youAreBetterThanPercentOfPerfTypePlayers( + '${data.percentile!.toStringAsFixed(2)}%', + perf.title, + ) + : context.l10n.userIsBetterThanPercentOfPerfTypePlayers( + user.username, + '${data.percentile!.toStringAsFixed(2)}%', + perf.title, + ), + style: TextStyle(color: textShade(context, 0.7)), + ), + subStatSpace, + // The number '12' here is not arbitrary, since the API returns the progression for the last 12 games (as far as I know). + StatCard( + context.l10n + .perfStatProgressOverLastXGames('12') + .replaceAll(':', ''), + child: _ProgressionWidget(data.progress), ), - if (perf != Perf.puzzle) ...[ - if (data.percentile != null && data.percentile! > 0.0) - Text( - (loggedInUser != null && loggedInUser.user.id == user.id) - ? context.l10n.youAreBetterThanPercentOfPerfTypePlayers( - '${data.percentile!.toStringAsFixed(2)}%', - perf.title, - ) - : context.l10n.userIsBetterThanPercentOfPerfTypePlayers( - user.username, - '${data.percentile!.toStringAsFixed(2)}%', - perf.title, - ), - style: TextStyle(color: textShade(context, 0.7)), + StatCardRow([ + if (data.rank != null) + StatCard( + context.l10n.rank, + value: data.rank == null + ? '?' + : NumberFormat.decimalPattern( + Intl.getCurrentLocale(), + ).format(data.rank), ), - subStatSpace, - // The number '12' here is not arbitrary, since the API returns the progression for the last 12 games (as far as I know). StatCard( context.l10n - .perfStatProgressOverLastXGames('12') - .replaceAll(':', ''), - child: _ProgressionWidget(data.progress), + .perfStatRatingDeviation('') + .replaceAll(': .', ''), + value: data.deviation.toStringAsFixed(2), ), - StatCardRow([ - if (data.rank != null) - StatCard( - context.l10n.rank, - value: data.rank == null - ? '?' - : NumberFormat.decimalPattern( - Intl.getCurrentLocale(), - ).format(data.rank), - ), - StatCard( - context.l10n - .perfStatRatingDeviation('') - .replaceAll(': .', ''), - value: data.deviation.toStringAsFixed(2), - ), - ]), - StatCardRow([ - StatCard( - context.l10n.perfStatHighestRating('').replaceAll(':', ''), - child: _RatingWidget( - data.highestRating, - data.highestRatingGame, - context.lichessColors.good, - ), + ]), + StatCardRow([ + StatCard( + context.l10n.perfStatHighestRating('').replaceAll(':', ''), + child: _RatingWidget( + data.highestRating, + data.highestRatingGame, + context.lichessColors.good, ), - StatCard( - context.l10n.perfStatLowestRating('').replaceAll(':', ''), - child: _RatingWidget( - data.lowestRating, - data.lowestRatingGame, - context.lichessColors.error, - ), + ), + StatCard( + context.l10n.perfStatLowestRating('').replaceAll(':', ''), + child: _RatingWidget( + data.lowestRating, + data.lowestRatingGame, + context.lichessColors.error, ), - ]), - statGroupSpace, - Semantics( - container: true, - enabled: true, - button: true, - label: context.l10n.perfStatViewTheGames, - child: Tooltip( - excludeFromSemantics: true, - message: context.l10n.perfStatViewTheGames, - child: AdaptiveInkWell( - onTap: () { - pushPlatformRoute( - context, - builder: (context) => GameHistoryScreen( - user: user.lightUser, - isOnline: true, - gameFilter: GameFilterState(perfs: ISet({perf})), + ), + ]), + statGroupSpace, + Semantics( + container: true, + enabled: true, + button: true, + label: context.l10n.perfStatViewTheGames, + child: Tooltip( + excludeFromSemantics: true, + message: context.l10n.perfStatViewTheGames, + child: AdaptiveInkWell( + onTap: () { + pushPlatformRoute( + context, + builder: (context) => GameHistoryScreen( + user: user.lightUser, + isOnline: true, + gameFilter: GameFilterState(perfs: ISet({perf})), + ), + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 3.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + Text( + '${context.l10n.perfStatTotalGames} ' + .localizeNumbers(), + style: Styles.sectionTitle, ), - ); - }, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 3.0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.baseline, - textBaseline: TextBaseline.alphabetic, - children: [ - Text( - '${context.l10n.perfStatTotalGames} ' - .localizeNumbers(), - style: Styles.sectionTitle, - ), - Text( - data.totalGames.toString().localizeNumbers(), - style: _mainValueStyle, + Text( + data.totalGames.toString().localizeNumbers(), + style: _mainValueStyle, + ), + Text( + String.fromCharCode( + Icons.arrow_forward_ios.codePoint, ), - Text( - String.fromCharCode( - Icons.arrow_forward_ios.codePoint, - ), - style: Styles.sectionTitle.copyWith( - fontFamily: 'MaterialIcons', - ), + style: Styles.sectionTitle.copyWith( + fontFamily: 'MaterialIcons', ), - ], - ), + ), + ], ), ), ), ), - subStatSpace, - StatCardRow([ - StatCard( - context.l10n.wins, - child: _PercentageValueWidget( - data.wonGames, - data.totalGames, - color: context.lichessColors.good, - ), - ), - StatCard( - context.l10n.draws, - child: _PercentageValueWidget( - data.drawnGames, - data.totalGames, - color: textShade(context, _customOpacity), - isShaded: true, - ), - ), - StatCard( - context.l10n.losses, - child: _PercentageValueWidget( - data.lostGames, - data.totalGames, - color: context.lichessColors.error, - ), - ), - ]), - StatCardRow([ - StatCard( - context.l10n.rated, - child: _PercentageValueWidget( - data.ratedGames, - data.totalGames, - ), - ), - StatCard( - context.l10n.tournament, - child: _PercentageValueWidget( - data.tournamentGames, - data.totalGames, - ), + ), + subStatSpace, + StatCardRow([ + StatCard( + context.l10n.wins, + child: _PercentageValueWidget( + data.wonGames, + data.totalGames, + color: context.lichessColors.good, ), - StatCard( - context.l10n.perfStatBerserkedGames.replaceAll( - ' ${context.l10n.games.toLowerCase()}', - '', - ), - child: _PercentageValueWidget( - data.berserkGames, - data.totalGames, - ), + ), + StatCard( + context.l10n.draws, + child: _PercentageValueWidget( + data.drawnGames, + data.totalGames, + color: textShade(context, _customOpacity), + isShaded: true, ), - StatCard( - context.l10n.perfStatDisconnections, - child: _PercentageValueWidget( - data.disconnections, - data.totalGames, - ), + ), + StatCard( + context.l10n.losses, + child: _PercentageValueWidget( + data.lostGames, + data.totalGames, + color: context.lichessColors.error, ), - ]), - StatCardRow([ - StatCard( - context.l10n.averageOpponent, - value: data.avgOpponent == null - ? '?' - : data.avgOpponent.toString(), + ), + ]), + StatCardRow([ + StatCard( + context.l10n.rated, + child: _PercentageValueWidget( + data.ratedGames, + data.totalGames, ), - StatCard( - context.l10n.perfStatTimeSpentPlaying, - value: data.timePlayed - .toDaysHoursMinutes(AppLocalizations.of(context)), + ), + StatCard( + context.l10n.tournament, + child: _PercentageValueWidget( + data.tournamentGames, + data.totalGames, ), - ]), + ), StatCard( - context.l10n.perfStatWinningStreak, - child: _StreakWidget( - data.maxWinStreak, - data.curWinStreak, - color: context.lichessColors.good, + context.l10n.perfStatBerserkedGames.replaceAll( + ' ${context.l10n.games.toLowerCase()}', + '', + ), + child: _PercentageValueWidget( + data.berserkGames, + data.totalGames, ), ), StatCard( - context.l10n.perfStatLosingStreak, - child: _StreakWidget( - data.maxLossStreak, - data.curLossStreak, - color: context.lichessColors.error, + context.l10n.perfStatDisconnections, + child: _PercentageValueWidget( + data.disconnections, + data.totalGames, ), ), + ]), + StatCardRow([ StatCard( - context.l10n.perfStatGamesInARow, - child: _StreakWidget(data.maxPlayStreak, data.curPlayStreak), + context.l10n.averageOpponent, + value: data.avgOpponent == null + ? '?' + : data.avgOpponent.toString(), ), StatCard( - context.l10n.perfStatMaxTimePlaying, - child: _StreakWidget(data.maxTimeStreak, data.curTimeStreak), + context.l10n.perfStatTimeSpentPlaying, + value: data.timePlayed + .toDaysHoursMinutes(AppLocalizations.of(context)), ), - if (data.bestWins != null && data.bestWins!.isNotEmpty) ...[ - statGroupSpace, - _GameListWidget( - games: data.bestWins!, - perf: perf, - user: user, - header: Text( - context.l10n.perfStatBestRated, - style: Styles.sectionTitle, - ), + ]), + StatCard( + context.l10n.perfStatWinningStreak, + child: _StreakWidget( + data.maxWinStreak, + data.curWinStreak, + color: context.lichessColors.good, + ), + ), + StatCard( + context.l10n.perfStatLosingStreak, + child: _StreakWidget( + data.maxLossStreak, + data.curLossStreak, + color: context.lichessColors.error, + ), + ), + StatCard( + context.l10n.perfStatGamesInARow, + child: _StreakWidget(data.maxPlayStreak, data.curPlayStreak), + ), + StatCard( + context.l10n.perfStatMaxTimePlaying, + child: _StreakWidget(data.maxTimeStreak, data.curTimeStreak), + ), + if (data.bestWins != null && data.bestWins!.isNotEmpty) ...[ + statGroupSpace, + _GameListWidget( + games: data.bestWins!, + perf: perf, + user: user, + header: Text( + context.l10n.perfStatBestRated, + style: Styles.sectionTitle, ), - ], + ), ], ], - ), + ], ); }, error: (error, stackTrace) { diff --git a/lib/src/view/user/player_screen.dart b/lib/src/view/user/player_screen.dart index fd845720f2..a947bacaf4 100644 --- a/lib/src/view/user/player_screen.dart +++ b/lib/src/view/user/player_screen.dart @@ -49,17 +49,15 @@ class _Body extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final session = ref.watch(authSessionProvider); - return SafeArea( - child: ListView( - children: [ - const Padding( - padding: Styles.bodySectionPadding, - child: _SearchButton(), - ), - if (session != null) _OnlineFriendsWidget(), - RatingPrefAware(child: LeaderboardWidget()), - ], - ), + return ListView( + children: [ + const Padding( + padding: Styles.bodySectionPadding, + child: _SearchButton(), + ), + if (session != null) _OnlineFriendsWidget(), + RatingPrefAware(child: LeaderboardWidget()), + ], ); } } From 55e0e2632ec8045a0cdcac1de7cd1b7316669f57 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Sat, 7 Dec 2024 10:29:12 +0100 Subject: [PATCH 854/979] Fix current eval not in sync with position Fixes #1217 --- lib/src/model/analysis/analysis_controller.dart | 2 +- lib/src/model/broadcast/broadcast_game_controller.dart | 2 +- lib/src/model/study/study_controller.dart | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index b7206b6a1f..4ea9640325 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -621,7 +621,7 @@ class AnalysisController extends _$AnalysisController curState.currentPath, _root.branchesOn(curState.currentPath).map(Step.fromNode), initialPositionEval: _root.eval, - shouldEmit: (work) => work.path == curState.currentPath, + shouldEmit: (work) => work.path == state.valueOrNull?.currentPath, ) ?.forEach( (t) => _root.updateAt(t.$1.path, (node) => node.eval = t.$2), diff --git a/lib/src/model/broadcast/broadcast_game_controller.dart b/lib/src/model/broadcast/broadcast_game_controller.dart index 22e246c50f..5a9834613f 100644 --- a/lib/src/model/broadcast/broadcast_game_controller.dart +++ b/lib/src/model/broadcast/broadcast_game_controller.dart @@ -503,7 +503,7 @@ class BroadcastGameController extends _$BroadcastGameController state.requireValue.currentPath, _root.branchesOn(state.requireValue.currentPath).map(Step.fromNode), initialPositionEval: _root.eval, - shouldEmit: (work) => work.path == state.requireValue.currentPath, + shouldEmit: (work) => work.path == state.valueOrNull?.currentPath, ) ?.forEach( (t) => _root.updateAt(t.$1.path, (node) => node.eval = t.$2), diff --git a/lib/src/model/study/study_controller.dart b/lib/src/model/study/study_controller.dart index bb67ae2c1c..c77ccf64f4 100644 --- a/lib/src/model/study/study_controller.dart +++ b/lib/src/model/study/study_controller.dart @@ -558,7 +558,8 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { _root.branchesOn(state.currentPath).map(Step.fromNode), // Note: AnalysisController passes _root.eval as initialPositionEval here, // but for studies this leads to false positive cache hits when switching between chapters. - shouldEmit: (work) => work.path == state.currentPath, + shouldEmit: (work) => + work.path == this.state.valueOrNull?.currentPath, ) ?.forEach( (t) => _root.updateAt(t.$1.path, (node) => node.eval = t.$2), From d8ab907cfb5db7a920144b9c5369a884aed1aa77 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Sat, 7 Dec 2024 11:04:39 +0100 Subject: [PATCH 855/979] Resync broadcast state on app resume Fixes #1222 --- lib/src/model/broadcast/broadcast_game_controller.dart | 9 +++++++++ lib/src/model/broadcast/broadcast_round_controller.dart | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/lib/src/model/broadcast/broadcast_game_controller.dart b/lib/src/model/broadcast/broadcast_game_controller.dart index 5a9834613f..2addb7111a 100644 --- a/lib/src/model/broadcast/broadcast_game_controller.dart +++ b/lib/src/model/broadcast/broadcast_game_controller.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:dartchess/dartchess.dart'; import 'package:deep_pick/deep_pick.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/widgets.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; @@ -33,6 +34,7 @@ class BroadcastGameController extends _$BroadcastGameController static Uri broadcastSocketUri(BroadcastRoundId broadcastRoundId) => Uri(path: 'study/$broadcastRoundId/socket/v6'); + AppLifecycleListener? _appLifecycleListener; StreamSubscription? _subscription; late SocketClient _socketClient; @@ -55,11 +57,18 @@ class BroadcastGameController extends _$BroadcastGameController final evaluationService = ref.watch(evaluationServiceProvider); + _appLifecycleListener = AppLifecycleListener( + onResume: () { + ref.invalidateSelf(); + }, + ); + ref.onDispose(() { _subscription?.cancel(); _startEngineEvalTimer?.cancel(); _engineEvalDebounce.dispose(); evaluationService.disposeEngine(); + _appLifecycleListener?.dispose(); }); final pgn = await ref.withClient( diff --git a/lib/src/model/broadcast/broadcast_round_controller.dart b/lib/src/model/broadcast/broadcast_round_controller.dart index 3ead272233..7f68bc5eb1 100644 --- a/lib/src/model/broadcast/broadcast_round_controller.dart +++ b/lib/src/model/broadcast/broadcast_round_controller.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:dartchess/dartchess.dart'; import 'package:deep_pick/deep_pick.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/widgets.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_repository.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; @@ -21,6 +22,7 @@ class BroadcastRoundController extends _$BroadcastRoundController { Uri(path: 'study/$broadcastRoundId/socket/v6'); StreamSubscription? _subscription; + AppLifecycleListener? _appLifecycleListener; late SocketClient _socketClient; @@ -34,8 +36,15 @@ class BroadcastRoundController extends _$BroadcastRoundController { _subscription = _socketClient.stream.listen(_handleSocketEvent); + _appLifecycleListener = AppLifecycleListener( + onResume: () { + ref.invalidateSelf(); + }, + ); + ref.onDispose(() { _subscription?.cancel(); + _appLifecycleListener?.dispose(); }); final round = await ref.withClient( From c33629fe6319df119a50e37928a54fef32592843 Mon Sep 17 00:00:00 2001 From: tom-anders <13141438+tom-anders@users.noreply.github.com> Date: Tue, 3 Dec 2024 05:58:57 +0100 Subject: [PATCH 856/979] feat: save active streak in local storage when exiting streak screen Closes #324 --- lib/src/model/puzzle/puzzle_controller.dart | 13 +- lib/src/model/puzzle/puzzle_providers.dart | 37 ++- lib/src/model/puzzle/puzzle_streak.dart | 6 +- lib/src/model/puzzle/streak_storage.dart | 49 +++ lib/src/view/puzzle/streak_screen.dart | 13 +- test/view/puzzle/streak_screen_test.dart | 334 ++++++++++++++++++++ 6 files changed, 438 insertions(+), 14 deletions(-) create mode 100644 lib/src/model/puzzle/streak_storage.dart create mode 100644 test/view/puzzle/streak_screen_test.dart diff --git a/lib/src/model/puzzle/puzzle_controller.dart b/lib/src/model/puzzle/puzzle_controller.dart index 71c92b7343..b590cea4ee 100644 --- a/lib/src/model/puzzle/puzzle_controller.dart +++ b/lib/src/model/puzzle/puzzle_controller.dart @@ -21,6 +21,7 @@ import 'package:lichess_mobile/src/model/puzzle/puzzle_service.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_session.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_streak.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_theme.dart'; +import 'package:lichess_mobile/src/model/puzzle/streak_storage.dart'; import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/utils/rate_limit.dart'; import 'package:result_extensions/result_extensions.dart'; @@ -273,7 +274,15 @@ class PuzzleController extends _$PuzzleController { state = _loadNewContext(nextContext, nextStreak ?? state.streak); } - void sendStreakResult() { + void saveStreakResultLocally() { + ref.read(streakStorageProvider(initialContext.userId)).saveActiveStreak( + state.streak!, + ); + } + + void _sendStreakResult() { + ref.read(streakStorageProvider(initialContext.userId)).clearActiveStreak(); + if (initialContext.userId != null) { final streak = state.streak?.index; if (streak != null && streak > 0) { @@ -420,7 +429,7 @@ class PuzzleController extends _$PuzzleController { finished: true, ), ); - sendStreakResult(); + _sendStreakResult(); } else { if (_nextPuzzleFuture == null) { assert(false, 'next puzzle future cannot be null with streak'); diff --git a/lib/src/model/puzzle/puzzle_providers.dart b/lib/src/model/puzzle/puzzle_providers.dart index 1873c9ffba..b742ffdf2a 100644 --- a/lib/src/model/puzzle/puzzle_providers.dart +++ b/lib/src/model/puzzle/puzzle_providers.dart @@ -11,8 +11,10 @@ import 'package:lichess_mobile/src/model/puzzle/puzzle_opening.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_repository.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_service.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_storage.dart'; +import 'package:lichess_mobile/src/model/puzzle/puzzle_streak.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_theme.dart'; import 'package:lichess_mobile/src/model/puzzle/storm.dart'; +import 'package:lichess_mobile/src/model/puzzle/streak_storage.dart'; import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/utils/riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -35,9 +37,40 @@ Future nextPuzzle(Ref ref, PuzzleAngle angle) async { ); } +typedef InitialStreak = ({ + PuzzleStreak streak, + Puzzle puzzle, +}); + +/// Fetches the active streak from the local storage if available, otherwise fetches it from the server. @riverpod -Future streak(Ref ref) { - return ref.withClient((client) => PuzzleRepository(client).streak()); +Future streak(Ref ref) async { + final session = ref.watch(authSessionProvider); + final streakStorage = ref.watch(streakStorageProvider(session?.user.id)); + final activeStreak = await streakStorage.loadActiveStreak(); + if (activeStreak != null) { + final puzzle = await ref + .read(puzzleProvider(activeStreak.streak[activeStreak.index]).future); + + return ( + streak: activeStreak, + puzzle: puzzle, + ); + } + + final rsp = + await ref.withClient((client) => PuzzleRepository(client).streak()); + + return ( + streak: PuzzleStreak( + streak: rsp.streak, + index: 0, + hasSkipped: false, + finished: false, + timestamp: rsp.timestamp, + ), + puzzle: rsp.puzzle, + ); } @riverpod diff --git a/lib/src/model/puzzle/puzzle_streak.dart b/lib/src/model/puzzle/puzzle_streak.dart index 02b7122be0..c950bc9681 100644 --- a/lib/src/model/puzzle/puzzle_streak.dart +++ b/lib/src/model/puzzle/puzzle_streak.dart @@ -2,11 +2,12 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; +part 'puzzle_streak.g.dart'; part 'puzzle_streak.freezed.dart'; typedef Streak = IList; -@freezed +@Freezed(fromJson: true, toJson: true) class PuzzleStreak with _$PuzzleStreak { const PuzzleStreak._(); @@ -19,4 +20,7 @@ class PuzzleStreak with _$PuzzleStreak { }) = _PuzzleStreak; PuzzleId? get nextId => streak.getOrNull(index + 1); + + factory PuzzleStreak.fromJson(Map json) => + _$PuzzleStreakFromJson(json); } diff --git a/lib/src/model/puzzle/streak_storage.dart b/lib/src/model/puzzle/streak_storage.dart new file mode 100644 index 0000000000..dbe0101a90 --- /dev/null +++ b/lib/src/model/puzzle/streak_storage.dart @@ -0,0 +1,49 @@ +import 'dart:convert'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:lichess_mobile/src/binding.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/puzzle/puzzle_streak.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +part 'streak_storage.g.dart'; + +@Riverpod(keepAlive: true) +StreakStorage streakStorage(Ref ref, UserId? userId) { + return StreakStorage(userId); +} + +/// Local storage for the current puzzle streak. +class StreakStorage { + const StreakStorage(this.userId); + final UserId? userId; + + Future loadActiveStreak() async { + final stored = _store.getString(_storageKey); + if (stored == null) { + return null; + } + + return PuzzleStreak.fromJson( + jsonDecode(stored) as Map, + ); + } + + Future saveActiveStreak(PuzzleStreak streak) async { + await _store.setString( + _storageKey, + jsonEncode(streak), + ); + } + + Future clearActiveStreak() async { + await _store.remove(_storageKey); + } + + SharedPreferencesWithCache get _store => + LichessBinding.instance.sharedPreferences; + + String get _storageKey => 'puzzle_streak.${userId ?? '**anon**'}'; +} diff --git a/lib/src/view/puzzle/streak_screen.dart b/lib/src/view/puzzle/streak_screen.dart index 7382bee36c..db086e705c 100644 --- a/lib/src/view/puzzle/streak_screen.dart +++ b/lib/src/view/puzzle/streak_screen.dart @@ -65,13 +65,7 @@ class _Load extends ConsumerWidget { angle: const PuzzleTheme(PuzzleThemeKey.mix), userId: session?.user.id, ), - streak: PuzzleStreak( - streak: data.streak, - index: 0, - hasSkipped: false, - finished: false, - timestamp: data.timestamp, - ), + streak: data.streak, ); }, loading: () => const Center(child: CircularProgressIndicator.adaptive()), @@ -219,9 +213,10 @@ class _Body extends ConsumerWidget { context: context, builder: (context) => YesNoDialog( title: Text(context.l10n.mobileAreYouSure), - content: Text(context.l10n.mobilePuzzleStreakAbortWarning), + content: + const Text('No worries, your score will be saved locally.'), onYes: () { - ref.read(ctrlProvider.notifier).sendStreakResult(); + ref.read(ctrlProvider.notifier).saveStreakResultLocally(); return Navigator.of(context).pop(true); }, onNo: () => Navigator.of(context).pop(false), diff --git a/test/view/puzzle/streak_screen_test.dart b/test/view/puzzle/streak_screen_test.dart new file mode 100644 index 0000000000..efdd87166e --- /dev/null +++ b/test/view/puzzle/streak_screen_test.dart @@ -0,0 +1,334 @@ +import 'package:dartchess/dartchess.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/testing.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/view/puzzle/streak_screen.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; + +import '../../test_helpers.dart'; +import '../../test_provider_scope.dart'; + +final client = MockClient((request) { + if (request.url.path == '/api/streak') { + return mockResponse( + ''' + { + "game": { + "id": "Xndtxsoa", + "perf": { + "key": "blitz", + "name": "Blitz" + }, + "rated": true, + "players": [ + { + "name": "VincV", + "id": "vincv", + "color": "white", + "rating": 1889 + }, + { + "name": "Rex9646", + "id": "rex9646", + "color": "black", + "rating": 1881 + } + ], + "pgn": "e4 c5 Nf3 e6 d4 cxd4 Nxd4 a6 Be2 Nf6 Nc3 Bb4 e5 Ne4 Bd2 Bxc3 Bxc3 Nxc3 bxc3 Qa5 O-O Qxe5 Re1 O-O Bxa6 Qc5 Bd3 Nc6 Re3 f5 Rh3 h6 Nxc6 bxc6 c4 Ra3 Qh5 Qd4 Rf1 Rxa2 Rg3 Kh8 Rg6 Rf6 Rxf6 Qxf6 Qe8+ Kh7 Qxc8 Qe7 h3 Qf7 Re1 Qe7 Bxf5+ g6 Bd3 Kg7 Qb7 Ra5 Rd1 Re5 Bxg6 Kxg6 Rxd7 Qf6 Qc8", + "clock": "3+2" + }, + "puzzle": { + "id": "MptxK", + "rating": 1012, + "plays": 5557, + "solution": [ + "e5e1", + "g1h2", + "f6f4", + "g2g3", + "f4f2" + ], + "themes": [ + "endgame", + "long", + "mateIn3" + ], + "initialPly": 66 + }, + "angle": { + "key": "mix", + "name": "Puzzle Themes", + "desc": "A bit of everything. You don't know what to expect, so you remain ready for anything! Just like in real games." + }, + "streak": "MptxK 4CZxz kcN3a 1I9Ly kOx90 eTrkO G0tpf iwTxQ tg2IU TovLC 0miTI Jpmkf 8VqjS XftoM 70UGG lm8O8 R4Y49 76Llk XZOyq QUgzo dACnQ qFjLp ytKo4 6JIj1 SYz3x kEkib dkvMp Dk0Ln Ok3qk zbRCc fQSVb vmDLx VJw06 3up01 X9aHm EicvD 5lhwD fTJE0 08LZy XAsVO TVB8s VCLTk KH6zc CaByR E2dUi JOxJg Agtzu KwbY9 Rmcf7 k9jGo 0zTgd 5YCx8 BtqDp DQdRO ytwPd sHqWB 1WunB Fovke mmMDN UNcwu isI02 3sIJB mnuzi 4aaRt Jvkvj UsXO2 kLfmz gsC1H TADGH a0Jz6 oUPR2 1IOBO 9PUdj haSH3 wn5by 22fL0 CR3Wu FaBtd DorJu unTls qeu0r xo40H DssQ9 D6s6S hkWx4 GF7s5 rzREu vhsbo s1haw j9ckI ekJnL TvcVB a7T4o 1olwh pydoy rGs3G k5ljZ gowEl UNXOV XkaUw 10lYO 6Ufqg Q45go KxGe3 vgwIt lqoaX nBtOq uAo3e jsbpu JLtdz TGUcX PobG5 ScDAL YPEfv o52sU FV0lM evQzq qAny0 dkDJi 0AUNz uzI6q kh13r Rubxa ecY6Q T9EL2 TmBka DPT5t qmzEf dyo0g MsGbE hPkmk 3wZBI 7kpeT 6EKGn kozHL Vnaiz 6DzDP HQ5RQ 7Ilyn 9n7Pz PwtXo kgMG2 J7gat gXcxs 4YVfC e8jGb m71Kb 9OrKY z530i" + } + ''', + 200, + ); + } else if (request.url.path == '/api/puzzle/4CZxz') { + return mockResponse( + ''' + { + "game": { + "id": "MQOxq7Jl", + "perf": { + "key": "blitz", + "name": "Blitz" + }, + "rated": true, + "players": [ + { + "name": "mikeroh", + "flair": "nature.t-rex", + "id": "mikeroh", + "color": "white", + "rating": 1600 + }, + { + "name": "huthayfa78", + "id": "huthayfa78", + "color": "black", + "rating": 1577 + } + ], + "pgn": "d4 d5 Bf4 h6 e3 a6 Nf3 Nf6 c3 Nh5 Be5 f6 Bg3 Nxg3 hxg3 Qd6 Bd3 Nc6 Qc2 Be6 Nbd2 O-O-O e4 dxe4 Nxe4 Qd5 O-O-O Qxa2 Nc5 Bf7 Bf5+ e6 Be4 Bxc5 dxc5 Qa1+ Qb1 Rxd1+ Rxd1 Qa4 Nd4 Nxd4 cxd4 Re8 f4 Qc4+ Bc2 Rd8 b3 Qc3 Qb2 Qe3+ Rd2 Qxg3 f5 exf5 Bxf5+ Kb8 Qa3 Qe1+ Rd1 Qe3+ Kb1 Rxd4 Rxd4 Qxd4 c6 Qd1+ Kb2 Qe2+ Bc2 bxc6 Qf8+ Be8 Qb4+ Kc8 Kc3 Qxg2 Bf5+ Kd8 Qb8+ Ke7 Qxc7+ Kf8 Qd6+ Kg8 Qe6+ Bf7", + "clock": "3+2" + }, + "puzzle": { + "id": "4CZxz", + "rating": 1058, + "plays": 2060, + "solution": [ + "e6c8", + "f7e8", + "c8e8" + ], + "themes": [ + "short", + "endgame", + "mateIn2" + ], + "initialPly": 87 + } + } + ''', + 200, + ); + } else if (request.url.path == '/api/puzzle/kcN3a') { + return mockResponse( + ''' + { + "game": { + "id": "bEuHKQSa", + "perf": { + "key": "rapid", + "name": "Rapid" + }, + "rated": true, + "players": [ + { + "name": "franpite", + "id": "franpite", + "color": "white", + "rating": 1773 + }, + { + "name": "cleomarzin777", + "id": "cleomarzin777", + "color": "black", + "rating": 1752 + } + ], + "pgn": "e4 c5 Nf3 d6 d4 cxd4 Nxd4 Nf6 Nc3 a6 Be3 e5 Nf3 b5 Be2 Bb7 O-O Nxe4 Nd5 Nf6 Nxf6+ Qxf6 Bg5 Qe6 Re1 Be7 Bxe7 Qxe7 Qd3 O-O Rad1 Rd8 Nd2 Qg5 Ne4 Qg6 Nxd6", + "clock": "10+0" + }, + "puzzle": { + "id": "kcN3a", + "rating": 1069, + "plays": 294, + "solution": [ + "g6g2" + ], + "themes": [ + "middlegame", + "oneMove", + "mateIn1", + "kingsideAttack" + ], + "initialPly": 36 + } + } + ''', + 200, + ); + } else if (request.url.path == '/api/puzzle/1I9Ly') { + return mockResponse( + ''' + { + "game": { + "id": "DTmg6BsX", + "perf": { + "key": "rapid", + "name": "Rapid" + }, + "rated": true, + "players": [ + { + "name": "Eskozhanov_1_Semey1", + "id": "eskozhanov_1_semey1", + "color": "white", + "rating": 1928 + }, + { + "name": "sirlancelots", + "id": "sirlancelots", + "color": "black", + "rating": 2124 + } + ], + "pgn": "e4 c5 Nc3 e6 Nf3 a6 d4 cxd4 Nxd4 Qc7 g3 Nf6 Bg2 Nc6 a3 d6 O-O Be7 f4 Nxd4 Qxd4 d5 exd5", + "clock": "10+0" + }, + "puzzle": { + "id": "1I9Ly", + "rating": 1087, + "plays": 3873, + "solution": [ + "e7c5", + "c1e3", + "c5d4" + ], + "themes": [ + "opening", + "crushing", + "short", + "pin" + ], + "initialPly": 22 + } + } + ''', + 200, + ); + } + return mockResponse('', 404); +}); + +void main() { + group('StreakScreen', () { + testWidgets( + 'meets accessibility guidelines', + (tester) async { + final SemanticsHandle handle = tester.ensureSemantics(); + + final app = await makeTestProviderScopeApp( + tester, + home: const StreakScreen(), + overrides: [ + lichessClientProvider + .overrideWith((ref) => LichessClient(client, ref)), + ], + ); + + await tester.pumpWidget(app); + + await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); + handle.dispose(); + }, + variant: kPlatformVariant, + ); + + testWidgets('Score is saved when exiting screen', (tester) async { + final app = await makeTestProviderScopeApp( + tester, + home: Builder( + builder: (context) => PlatformScaffold( + appBar: const PlatformAppBar(title: Text('Test Streak Screen')), + body: FatButton( + semanticsLabel: 'Start Streak', + child: const Text('Start Streak'), + onPressed: () => pushPlatformRoute( + context, + rootNavigator: true, + builder: (context) => const StreakScreen(), + ), + ), + ), + ), + overrides: [ + lichessClientProvider + .overrideWith((ref) => LichessClient(client, ref)), + ], + ); + await tester.pumpWidget(app); + + await tester.tap(find.text('Start Streak')); + + // wait for puzzle to load from api and opponent move to be played + await tester.pumpAndSettle(const Duration(seconds: 2)); + + expect(find.textContaining(RegExp('0\$')), findsOneWidget); + + await playMove(tester, 'e5', 'e1', orientation: Side.black); + // Wait for opponent move to be played + await tester.pumpAndSettle(const Duration(milliseconds: 500)); + + await playMove(tester, 'f6', 'f4', orientation: Side.black); + // Wait for opponent move to be played + await tester.pumpAndSettle(const Duration(milliseconds: 500)); + + await playMove(tester, 'f4', 'f2', orientation: Side.black); + + // Wait for next puzzle to load and score to be updated + await tester.pumpAndSettle(const Duration(seconds: 1)); + + expect(find.textContaining(RegExp('1\$')), findsOneWidget); + + // Exit screen -> score should be saved + await tester.pageBack(); + await tester.pump(); + await tester.tap(find.text('Yes')); + await tester.pumpAndSettle(); + + // Enter streak screen again -> previous score should be loaded + await tester.tap(find.text('Start Streak')); + // Wait for puzzle to load from api and opponent move to be played + await tester.pumpAndSettle(const Duration(seconds: 2)); + + expect(find.textContaining(RegExp('1\$')), findsOneWidget); + + await playMove(tester, 'e6', 'c8', orientation: Side.white); + // Wait for opponent move to be played + await tester.pumpAndSettle(const Duration(milliseconds: 500)); + + await playMove(tester, 'f7', 'e8', orientation: Side.white); + // Wait for opponent move to be played + await tester.pumpAndSettle(const Duration(milliseconds: 500)); + + await playMove(tester, 'c8', 'e8', orientation: Side.white); + + // Wait for next puzzle to load and score to be updated + await tester.pumpAndSettle(const Duration(seconds: 2)); + + expect(find.textContaining(RegExp('2\$')), findsOneWidget); + + // Play a wrong move + await playMove(tester, 'd8', 'd7', orientation: Side.black); + await tester.pumpAndSettle(const Duration(seconds: 2)); + expect(find.text('GAME OVER'), findsOneWidget); + + // Exit screen and enter screen again + // -> local score should be cleared, so score should be 0 again + await tester.pageBack(); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Start Streak')); + await tester.pumpAndSettle(const Duration(seconds: 2)); + + expect(find.textContaining(RegExp('0\$')), findsOneWidget); + }); + }); +} From 64daba1249d45a4bd654edbfe568ee2153b60eb9 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Sat, 7 Dec 2024 11:23:09 +0100 Subject: [PATCH 857/979] Fix pgn view comment jitter Fixes #1206 --- lib/src/widgets/pgn.dart | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/src/widgets/pgn.dart b/lib/src/widgets/pgn.dart index 0cd4f75812..bee99a263d 100644 --- a/lib/src/widgets/pgn.dart +++ b/lib/src/widgets/pgn.dart @@ -1079,11 +1079,9 @@ class InlineMove extends ConsumerWidget { final moveTextStyle = textStyle.copyWith( fontFamily: moveFontFamily, - fontWeight: isCurrentMove - ? FontWeight.bold - : lineInfo.type == _LineType.inlineSideline - ? FontWeight.normal - : FontWeight.w600, + fontWeight: lineInfo.type == _LineType.inlineSideline + ? FontWeight.normal + : FontWeight.w600, ); final indexTextStyle = textStyle.copyWith( From 290e8d97970abd7978f34bcff97f410a273f7b01 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Sun, 8 Dec 2024 10:25:53 +0100 Subject: [PATCH 858/979] Fix null value error on broadcast round start date --- lib/src/model/broadcast/broadcast.dart | 3 +- .../model/broadcast/broadcast_repository.dart | 1 + .../view/broadcast/broadcast_list_screen.dart | 14 +++-- .../broadcast/broadcast_round_screen.dart | 57 ++++++++++--------- 4 files changed, 41 insertions(+), 34 deletions(-) diff --git a/lib/src/model/broadcast/broadcast.dart b/lib/src/model/broadcast/broadcast.dart index 4a69054d9c..7487c2f0d5 100644 --- a/lib/src/model/broadcast/broadcast.dart +++ b/lib/src/model/broadcast/broadcast.dart @@ -83,7 +83,8 @@ class BroadcastRound with _$BroadcastRound { required String name, required RoundStatus status, required DateTime? startsAt, - DateTime? finishedAt, + required DateTime? finishedAt, + required bool startsAfterPrevious, }) = _BroadcastRound; } diff --git a/lib/src/model/broadcast/broadcast_repository.dart b/lib/src/model/broadcast/broadcast_repository.dart index 9063efff0d..af15531f99 100644 --- a/lib/src/model/broadcast/broadcast_repository.dart +++ b/lib/src/model/broadcast/broadcast_repository.dart @@ -138,6 +138,7 @@ BroadcastRound _roundFromPick(RequiredPick pick) { status: status, startsAt: pick('startsAt').asDateTimeFromMillisecondsOrNull(), finishedAt: pick('finishedAt').asDateTimeFromMillisecondsOrNull(), + startsAfterPrevious: pick('startsAfterPrevious').asBoolOrFalse(), ); } diff --git a/lib/src/view/broadcast/broadcast_list_screen.dart b/lib/src/view/broadcast/broadcast_list_screen.dart index 50f73e7fe4..b56de99dd3 100644 --- a/lib/src/view/broadcast/broadcast_list_screen.dart +++ b/lib/src/view/broadcast/broadcast_list_screen.dart @@ -216,9 +216,9 @@ class BroadcastGridItem extends StatefulWidget { final Broadcast broadcast; final ImageColorWorker worker; - BroadcastGridItem.loading(this.worker) - : broadcast = Broadcast( - tour: const BroadcastTournamentData( + const BroadcastGridItem.loading(this.worker) + : broadcast = const Broadcast( + tour: BroadcastTournamentData( id: BroadcastTournamentId(''), name: '', imageUrl: null, @@ -233,13 +233,15 @@ class BroadcastGridItem extends StatefulWidget { ), ), round: BroadcastRound( - id: const BroadcastRoundId(''), + id: BroadcastRoundId(''), name: '', status: RoundStatus.finished, - startsAt: DateTime.now(), + startsAt: null, + finishedAt: null, + startsAfterPrevious: false, ), group: null, - roundToLinkId: const BroadcastRoundId(''), + roundToLinkId: BroadcastRoundId(''), ); @override diff --git a/lib/src/view/broadcast/broadcast_round_screen.dart b/lib/src/view/broadcast/broadcast_round_screen.dart index 0bd6fa2edf..926232a679 100644 --- a/lib/src/view/broadcast/broadcast_round_screen.dart +++ b/lib/src/view/broadcast/broadcast_round_screen.dart @@ -332,37 +332,40 @@ class _RoundSelectorState extends ConsumerState<_RoundSelectorMenu> { return BottomSheetScrollableContainer( scrollController: widget.scrollController, children: [ - for (final round in widget.rounds) + for (final (index, round) in widget.rounds.indexed) PlatformListTile( key: round.id == widget.selectedRoundId ? currentRoundKey : null, selected: round.id == widget.selectedRoundId, title: Text(round.name), - trailing: switch (round.status) { - RoundStatus.finished => Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text(_dateFormat.format(round.startsAt!)), - const SizedBox(width: 5.0), - Icon(Icons.check, color: context.lichessColors.good), - ], - ), - RoundStatus.live => Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text(_dateFormat.format(round.startsAt!)), - const SizedBox(width: 5.0), - Icon(Icons.circle, color: context.lichessColors.error), - ], - ), - RoundStatus.upcoming => Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text(_dateFormat.format(round.startsAt!)), - const SizedBox(width: 5.0), - const Icon(Icons.calendar_month, color: Colors.grey), - ], - ), - }, + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (round.startsAt != null || round.startsAfterPrevious) ...[ + Text( + round.startsAt != null + ? _dateFormat.format(round.startsAt!) + : context.l10n.broadcastStartsAfter( + widget.rounds[index - 1].name, + ), + ), + const SizedBox(width: 5.0), + ], + switch (round.status) { + RoundStatus.finished => Icon( + Icons.check, + color: context.lichessColors.good, + ), + RoundStatus.live => Icon( + Icons.circle, + color: context.lichessColors.error, + ), + RoundStatus.upcoming => const Icon( + Icons.calendar_month, + color: Colors.grey, + ), + }, + ], + ), onTap: () { widget.setRoundId(round.id); Navigator.of(context).pop(); From c6b649002d940e22b7d2128e4f7b4986d4550568 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 9 Dec 2024 09:34:50 +0100 Subject: [PATCH 859/979] Fix cupertino tab header scroll view --- .../view/broadcast/broadcast_boards_tab.dart | 152 +++++++++--------- .../view/broadcast/broadcast_list_screen.dart | 2 +- .../broadcast/broadcast_overview_tab.dart | 17 +- .../broadcast/broadcast_round_screen.dart | 148 +++++++++-------- lib/src/view/watch/watch_tab_screen.dart | 2 +- lib/src/widgets/shimmer.dart | 11 +- 6 files changed, 180 insertions(+), 152 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_boards_tab.dart b/lib/src/view/broadcast/broadcast_boards_tab.dart index d0a092ac53..364e8416ba 100644 --- a/lib/src/view/broadcast/broadcast_boards_tab.dart +++ b/lib/src/view/broadcast/broadcast_boards_tab.dart @@ -25,55 +25,54 @@ const _kPlayerWidgetPadding = EdgeInsets.symmetric(vertical: 5.0); /// A tab that displays the live games of a broadcast round. class BroadcastBoardsTab extends ConsumerWidget { - final BroadcastRoundId roundId; + const BroadcastBoardsTab({ + required this.roundId, + }); - const BroadcastBoardsTab(this.roundId); + final BroadcastRoundId roundId; @override Widget build(BuildContext context, WidgetRef ref) { final round = ref.watch(broadcastRoundControllerProvider(roundId)); - return SafeArea( - bottom: false, - child: switch (round) { - AsyncData(:final value) => (value.games.isEmpty) - ? const Padding( - padding: Styles.bodyPadding, - child: Text('No boards to show for now'), - ) - : BroadcastPreview( - games: value.games.values.toIList(), - roundId: roundId, - title: value.round.name, - ), - AsyncError(:final error) => Center( - child: Text(error.toString()), - ), - _ => const Shimmer( - child: ShimmerLoading( - isLoading: true, - child: BroadcastPreview( - roundId: BroadcastRoundId(''), - title: '', + return switch (round) { + AsyncData(:final value) => value.games.isEmpty + ? const SliverFillRemaining( + child: Center( + child: Text('No games found.'), ), + ) + : BroadcastPreview( + games: value.games.values.toIList(), + roundId: roundId, + title: value.round.name, ), + AsyncError(:final error) => SliverFillRemaining( + child: Center( + child: Text('Could not load broadcast: $error'), ), - }, - ); + ), + _ => BroadcastPreview.loading(roundId: roundId), + }; } } class BroadcastPreview extends StatelessWidget { - final BroadcastRoundId roundId; - final IList? games; - final String title; - const BroadcastPreview({ required this.roundId, - this.games, + required this.games, required this.title, }); + const BroadcastPreview.loading({ + required this.roundId, + }) : games = null, + title = null; + + final BroadcastRoundId roundId; + final IList? games; + final String? title; + @override Widget build(BuildContext context) { const numberLoadingBoards = 12; @@ -83,64 +82,69 @@ class BroadcastPreview extends StatelessWidget { // see: https://api.flutter.dev/flutter/painting/TextStyle/height.html final textHeight = _kPlayerWidgetTextStyle.fontSize!; final headerAndFooterHeight = textHeight + _kPlayerWidgetPadding.vertical; - final numberOfBoardsByRow = isTabletOrLarger(context) ? 4 : 2; + final numberOfBoardsByRow = isTabletOrLarger(context) ? 3 : 2; final screenWidth = MediaQuery.sizeOf(context).width; final boardWidth = (screenWidth - Styles.horizontalBodyPadding.horizontal - (numberOfBoardsByRow - 1) * boardSpacing) / numberOfBoardsByRow; - return GridView.builder( - padding: Styles.bodyPadding, - itemCount: games == null ? numberLoadingBoards : games!.length, + return SliverGrid( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: numberOfBoardsByRow, crossAxisSpacing: boardSpacing, mainAxisSpacing: boardSpacing, mainAxisExtent: boardWidth + 2 * headerAndFooterHeight, ), - itemBuilder: (context, index) { - if (games == null) { - return BoardThumbnail.loading( - size: boardWidth, - header: _PlayerWidgetLoading(width: boardWidth), - footer: _PlayerWidgetLoading(width: boardWidth), - ); - } + delegate: SliverChildBuilderDelegate( + childCount: games == null ? numberLoadingBoards : games!.length, + (context, index) { + if (games == null || title == null) { + return ShimmerLoading( + isLoading: true, + child: BoardThumbnail.loading( + size: boardWidth, + header: _PlayerWidgetLoading(width: boardWidth), + footer: _PlayerWidgetLoading(width: boardWidth), + ), + ); + } - final game = games![index]; - final playingSide = Setup.parseFen(game.fen).turn; + final game = games![index]; + final playingSide = Setup.parseFen(game.fen).turn; - return BoardThumbnail( - animationDuration: const Duration(milliseconds: 150), - onTap: () { - pushPlatformRoute( - context, - builder: (context) => BroadcastGameScreen( - roundId: roundId, - gameId: game.id, + return BoardThumbnail( + animationDuration: const Duration(milliseconds: 150), + onTap: () { + pushPlatformRoute( + context, title: title, - ), - ); - }, - orientation: Side.white, - fen: game.fen, - lastMove: game.lastMove, - size: boardWidth, - header: _PlayerWidget( - width: boardWidth, - game: game, - side: Side.black, - playingSide: playingSide, - ), - footer: _PlayerWidget( - width: boardWidth, - game: game, - side: Side.white, - playingSide: playingSide, - ), - ); - }, + builder: (context) => BroadcastGameScreen( + roundId: roundId, + gameId: game.id, + title: title!, + ), + ); + }, + orientation: Side.white, + fen: game.fen, + lastMove: game.lastMove, + size: boardWidth, + header: _PlayerWidget( + width: boardWidth, + game: game, + side: Side.black, + playingSide: playingSide, + ), + footer: _PlayerWidget( + width: boardWidth, + game: game, + side: Side.white, + playingSide: playingSide, + ), + ); + }, + ), ); } } diff --git a/lib/src/view/broadcast/broadcast_list_screen.dart b/lib/src/view/broadcast/broadcast_list_screen.dart index 50f73e7fe4..c66e0c663e 100644 --- a/lib/src/view/broadcast/broadcast_list_screen.dart +++ b/lib/src/view/broadcast/broadcast_list_screen.dart @@ -315,7 +315,7 @@ class _BroadcastGridItemState extends State { onTap: () { pushPlatformRoute( context, - title: context.l10n.broadcastBroadcasts, + title: widget.broadcast.title, rootNavigator: true, builder: (context) => BroadcastRoundScreen(broadcast: widget.broadcast), diff --git a/lib/src/view/broadcast/broadcast_overview_tab.dart b/lib/src/view/broadcast/broadcast_overview_tab.dart index 94ae982e4f..67314e9be9 100644 --- a/lib/src/view/broadcast/broadcast_overview_tab.dart +++ b/lib/src/view/broadcast/broadcast_overview_tab.dart @@ -6,7 +6,6 @@ import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_providers.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; @@ -33,11 +32,9 @@ class BroadcastOverviewTab extends ConsumerWidget { case AsyncData(value: final tournament): final information = tournament.data.information; final description = tournament.data.description; - return SafeArea( - bottom: false, - child: ListView( - padding: Styles.bodyPadding, - children: [ + return SliverList( + delegate: SliverChildListDelegate( + [ if (tournament.data.imageUrl != null) ...[ Image.network(tournament.data.imageUrl!), const SizedBox(height: 16.0), @@ -95,11 +92,13 @@ class BroadcastOverviewTab extends ConsumerWidget { ), ); case AsyncError(:final error): - return Center( - child: Text('Cannot load broadcast data: $error'), + return SliverFillRemaining( + child: Center(child: Text('Cannot load broadcast data: $error')), ); case _: - return const Center(child: CircularProgressIndicator.adaptive()); + return const SliverFillRemaining( + child: Center(child: CircularProgressIndicator.adaptive()), + ); } } } diff --git a/lib/src/view/broadcast/broadcast_round_screen.dart b/lib/src/view/broadcast/broadcast_round_screen.dart index 0bd6fa2edf..a77c21ecef 100644 --- a/lib/src/view/broadcast/broadcast_round_screen.dart +++ b/lib/src/view/broadcast/broadcast_round_screen.dart @@ -16,6 +16,7 @@ import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; +import 'package:lichess_mobile/src/widgets/shimmer.dart'; class BroadcastRoundScreen extends ConsumerStatefulWidget { final Broadcast broadcast; @@ -26,11 +27,11 @@ class BroadcastRoundScreen extends ConsumerStatefulWidget { _BroadcastRoundScreenState createState() => _BroadcastRoundScreenState(); } -enum _ViewMode { overview, boards } +enum _CupertinoView { overview, boards } class _BroadcastRoundScreenState extends ConsumerState with SingleTickerProviderStateMixin { - _ViewMode _selectedSegment = _ViewMode.boards; + _CupertinoView _selectedSegment = _CupertinoView.boards; late final TabController _tabController; late BroadcastTournamentId _selectedTournamentId; BroadcastRoundId? _selectedRoundId; @@ -49,7 +50,7 @@ class _BroadcastRoundScreenState extends ConsumerState super.dispose(); } - void setViewMode(_ViewMode mode) { + void setViewMode(_CupertinoView mode) { setState(() { _selectedSegment = mode; }); @@ -82,6 +83,20 @@ class _BroadcastRoundScreenState extends ConsumerState ), ); if (Theme.of(context).platform == TargetPlatform.iOS) { + final tabSwitcher = CupertinoSlidingSegmentedControl<_CupertinoView>( + groupValue: _selectedSegment, + children: { + _CupertinoView.overview: Text(context.l10n.broadcastOverview), + _CupertinoView.boards: Text(context.l10n.broadcastBoards), + }, + onValueChanged: (_CupertinoView? view) { + if (view != null) { + setState(() { + _selectedSegment = view; + }); + } + }, + ); return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( middle: AutoSizeText( @@ -90,66 +105,32 @@ class _BroadcastRoundScreenState extends ConsumerState overflow: TextOverflow.ellipsis, maxLines: 1, ), - automaticBackgroundVisibility: false, - border: null, ), - child: SafeArea( - bottom: false, - child: Column( - children: [ - Container( - height: kMinInteractiveDimensionCupertino, - width: double.infinity, - decoration: BoxDecoration( - color: Styles.cupertinoAppBarColor.resolveFrom(context), - border: const Border( - bottom: BorderSide( - color: Color(0x4D000000), - width: 0.0, - ), - ), - ), - child: Padding( - padding: const EdgeInsets.only( - left: 16.0, - right: 16.0, - bottom: 8.0, - ), - child: CupertinoSlidingSegmentedControl<_ViewMode>( - groupValue: _selectedSegment, - children: { - _ViewMode.overview: - Text(context.l10n.broadcastOverview), - _ViewMode.boards: Text(context.l10n.broadcastBoards), - }, - onValueChanged: (_ViewMode? view) { - if (view != null) { - setState(() { - _selectedSegment = view; - }); - } - }, - ), - ), - ), - Expanded( - child: _selectedSegment == _ViewMode.overview - ? BroadcastOverviewTab( + child: Column( + children: [ + Expanded( + child: _selectedSegment == _CupertinoView.overview + ? _TabView( + cupertinoTabSwitcher: tabSwitcher, + sliver: BroadcastOverviewTab( broadcast: widget.broadcast, tournamentId: _selectedTournamentId, - ) - : BroadcastBoardsTab( - _selectedRoundId ?? value.defaultRoundId, ), - ), - _BottomBar( - tournament: value, - roundId: _selectedRoundId ?? value.defaultRoundId, - setTournamentId: setTournamentId, - setRoundId: setRoundId, - ), - ], - ), + ) + : _TabView( + cupertinoTabSwitcher: tabSwitcher, + sliver: BroadcastBoardsTab( + roundId: _selectedRoundId ?? value.defaultRoundId, + ), + ), + ), + _BottomBar( + tournament: value, + roundId: _selectedRoundId ?? value.defaultRoundId, + setTournamentId: setTournamentId, + setRoundId: setRoundId, + ), + ], ), ); } else { @@ -172,11 +153,17 @@ class _BroadcastRoundScreenState extends ConsumerState body: TabBarView( controller: _tabController, children: [ - BroadcastOverviewTab( - broadcast: widget.broadcast, - tournamentId: _selectedTournamentId, + _TabView( + sliver: BroadcastOverviewTab( + broadcast: widget.broadcast, + tournamentId: _selectedTournamentId, + ), + ), + _TabView( + sliver: BroadcastBoardsTab( + roundId: _selectedRoundId ?? value.defaultRoundId, + ), ), - BroadcastBoardsTab(_selectedRoundId ?? value.defaultRoundId), ], ), bottomNavigationBar: _BottomBar( @@ -195,6 +182,41 @@ class _BroadcastRoundScreenState extends ConsumerState } } +class _TabView extends StatelessWidget { + const _TabView({ + required this.sliver, + this.cupertinoTabSwitcher, + }); + + final Widget sliver; + final Widget? cupertinoTabSwitcher; + + @override + Widget build(BuildContext context) { + final edgeInsets = MediaQuery.paddingOf(context) - + (cupertinoTabSwitcher != null + ? EdgeInsets.only(top: MediaQuery.paddingOf(context).top) + : EdgeInsets.zero) + + Styles.bodyPadding; + return Shimmer( + child: CustomScrollView( + slivers: [ + if (cupertinoTabSwitcher != null) + SliverPadding( + padding: Styles.bodyPadding + + EdgeInsets.only(top: MediaQuery.paddingOf(context).top), + sliver: SliverToBoxAdapter(child: cupertinoTabSwitcher), + ), + SliverPadding( + padding: edgeInsets, + sliver: sliver, + ), + ], + ), + ); + } +} + class _BottomBar extends ConsumerWidget { const _BottomBar({ required this.tournament, diff --git a/lib/src/view/watch/watch_tab_screen.dart b/lib/src/view/watch/watch_tab_screen.dart index 6597a908bd..634837807c 100644 --- a/lib/src/view/watch/watch_tab_screen.dart +++ b/lib/src/view/watch/watch_tab_screen.dart @@ -258,7 +258,7 @@ class _BroadcastTile extends ConsumerWidget { onTap: () { pushPlatformRoute( context, - title: context.l10n.broadcastBroadcasts, + title: broadcast.title, rootNavigator: true, builder: (context) => BroadcastRoundScreen(broadcast: broadcast), ); diff --git a/lib/src/widgets/shimmer.dart b/lib/src/widgets/shimmer.dart index 825c17d88e..a7d3e566d7 100644 --- a/lib/src/widgets/shimmer.dart +++ b/lib/src/widgets/shimmer.dart @@ -130,10 +130,13 @@ class _ShimmerLoadingState extends State { } final shimmerSize = shimmer.size; final gradient = shimmer.gradient; - final offsetWithinShimmer = shimmer.getDescendantOffset( - // ignore: cast_nullable_to_non_nullable - descendant: context.findRenderObject() as RenderBox, - ); + final renderObject = context.findRenderObject(); + final offsetWithinShimmer = renderObject != null + ? shimmer.getDescendantOffset( + // ignore: cast_nullable_to_non_nullable + descendant: renderObject as RenderBox, + ) + : Offset.zero; return ShaderMask( blendMode: BlendMode.srcATop, From 49de467456e9ea331a5e7b4f3c10e9c94e210f46 Mon Sep 17 00:00:00 2001 From: Shachar Zidon Date: Mon, 9 Dec 2024 10:46:10 +0200 Subject: [PATCH 860/979] Added Bluesky to social links --- lib/src/model/user/profile.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/model/user/profile.dart b/lib/src/model/user/profile.dart index a6d708a938..20ae4ace7f 100644 --- a/lib/src/model/user/profile.dart +++ b/lib/src/model/user/profile.dart @@ -101,6 +101,7 @@ enum LinkSite { ]), ), twitter('Twitter', IListConst(['twitter.com'])), + bluesky('Bluesky', IListConst(['bsky.app'])), facebook('Facebook', IListConst(['facebook.com'])), instagram('Instagram', IListConst(['instagram.com'])), youtube('YouTube', IListConst(['youtube.com'])), From 5d27912bf7b2c6f91a754d8cd1d63bcf25873880 Mon Sep 17 00:00:00 2001 From: rizqicy Date: Mon, 9 Dec 2024 16:24:51 +0700 Subject: [PATCH 861/979] fix inconsistent font size in puzzle's ratingDiff --- lib/src/view/puzzle/puzzle_session_widget.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/view/puzzle/puzzle_session_widget.dart b/lib/src/view/puzzle/puzzle_session_widget.dart index 763ba870ce..37d8511b92 100644 --- a/lib/src/view/puzzle/puzzle_session_widget.dart +++ b/lib/src/view/puzzle/puzzle_session_widget.dart @@ -221,7 +221,7 @@ class _SessionItem extends StatelessWidget { child: Padding( padding: const EdgeInsets.all(2.0), child: FittedBox( - fit: BoxFit.cover, + fit: BoxFit.fitHeight, child: Text( attempt!.ratingDiffString!, maxLines: 1, From 0465ff69684e5ee16b1a0871281cdedcea7cd0f9 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 9 Dec 2024 10:56:39 +0100 Subject: [PATCH 862/979] Show broadcast boards only if available --- .../view/broadcast/broadcast_boards_tab.dart | 14 +- .../broadcast/broadcast_round_screen.dart | 287 ++++++++++++------ 2 files changed, 200 insertions(+), 101 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_boards_tab.dart b/lib/src/view/broadcast/broadcast_boards_tab.dart index 364e8416ba..89cfc2f49a 100644 --- a/lib/src/view/broadcast/broadcast_boards_tab.dart +++ b/lib/src/view/broadcast/broadcast_boards_tab.dart @@ -9,6 +9,7 @@ import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/duration.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/lichess_assets.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; @@ -37,9 +38,16 @@ class BroadcastBoardsTab extends ConsumerWidget { return switch (round) { AsyncData(:final value) => value.games.isEmpty - ? const SliverFillRemaining( - child: Center( - child: Text('No games found.'), + ? SliverPadding( + padding: const EdgeInsets.only(top: 16.0), + sliver: SliverToBoxAdapter( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.info, size: 30), + Text(context.l10n.broadcastNoBoardsYet), + ], + ), ), ) : BroadcastPreview( diff --git a/lib/src/view/broadcast/broadcast_round_screen.dart b/lib/src/view/broadcast/broadcast_round_screen.dart index a77c21ecef..5696508069 100644 --- a/lib/src/view/broadcast/broadcast_round_screen.dart +++ b/lib/src/view/broadcast/broadcast_round_screen.dart @@ -1,3 +1,5 @@ +import 'dart:ui'; + import 'package:auto_size_text/auto_size_text.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/cupertino.dart'; @@ -31,15 +33,18 @@ enum _CupertinoView { overview, boards } class _BroadcastRoundScreenState extends ConsumerState with SingleTickerProviderStateMixin { - _CupertinoView _selectedSegment = _CupertinoView.boards; + _CupertinoView selectedTab = _CupertinoView.overview; late final TabController _tabController; late BroadcastTournamentId _selectedTournamentId; BroadcastRoundId? _selectedRoundId; + bool roundLoaded = false; + double headerOpacity = 0; + @override void initState() { super.initState(); - _tabController = TabController(initialIndex: 1, length: 2, vsync: this); + _tabController = TabController(initialIndex: 0, length: 2, vsync: this); _selectedTournamentId = widget.broadcast.tour.id; _selectedRoundId = widget.broadcast.roundToLinkId; } @@ -50,9 +55,10 @@ class _BroadcastRoundScreenState extends ConsumerState super.dispose(); } - void setViewMode(_CupertinoView mode) { + void setCupertinoTab(_CupertinoView mode) { setState(() { - _selectedSegment = mode; + selectedTab = mode; + headerOpacity = 0.0; }); } @@ -69,113 +75,173 @@ class _BroadcastRoundScreenState extends ConsumerState }); } + bool handleScrollNotification(ScrollNotification notification) { + if (notification is ScrollUpdateNotification && notification.depth == 0) { + final ScrollMetrics metrics = notification.metrics; + double scrollExtent = 0.0; + switch (metrics.axisDirection) { + case AxisDirection.up: + scrollExtent = metrics.extentAfter; + case AxisDirection.down: + scrollExtent = metrics.extentBefore; + case AxisDirection.right: + case AxisDirection.left: + break; + } + + final opacity = scrollExtent > 0.0 ? 1.0 : 0.0; + + if (opacity != headerOpacity) { + setState(() { + headerOpacity = opacity; + }); + } + } + return false; + } + @override Widget build(BuildContext context) { final tournament = ref.watch(broadcastTournamentProvider(_selectedTournamentId)); switch (tournament) { - case AsyncData(:final value): + case AsyncData(value: final tournament): // Eagerly initalize the round controller so it stays alive when switching tabs - ref.watch( + // and to know if the round has games to show + final round = ref.watch( broadcastRoundControllerProvider( - _selectedRoundId ?? value.defaultRoundId, + _selectedRoundId ?? tournament.defaultRoundId, ), ); - if (Theme.of(context).platform == TargetPlatform.iOS) { - final tabSwitcher = CupertinoSlidingSegmentedControl<_CupertinoView>( - groupValue: _selectedSegment, - children: { - _CupertinoView.overview: Text(context.l10n.broadcastOverview), - _CupertinoView.boards: Text(context.l10n.broadcastBoards), - }, - onValueChanged: (_CupertinoView? view) { - if (view != null) { - setState(() { - _selectedSegment = view; - }); + + ref.listen( + broadcastRoundControllerProvider( + _selectedRoundId ?? tournament.defaultRoundId, + ), + (_, round) { + if (!roundLoaded && round.hasValue) { + roundLoaded = true; + if (round.value!.games.isNotEmpty) { + _tabController.animateTo(1, duration: Duration.zero); + + if (Theme.of(context).platform == TargetPlatform.iOS) { + setCupertinoTab(_CupertinoView.boards); + } } - }, - ); - return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - middle: AutoSizeText( - widget.broadcast.title, - minFontSize: 14.0, - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), - ), - child: Column( - children: [ - Expanded( - child: _selectedSegment == _CupertinoView.overview - ? _TabView( - cupertinoTabSwitcher: tabSwitcher, - sliver: BroadcastOverviewTab( - broadcast: widget.broadcast, - tournamentId: _selectedTournamentId, - ), - ) - : _TabView( - cupertinoTabSwitcher: tabSwitcher, - sliver: BroadcastBoardsTab( - roundId: _selectedRoundId ?? value.defaultRoundId, - ), - ), - ), - _BottomBar( - tournament: value, - roundId: _selectedRoundId ?? value.defaultRoundId, - setTournamentId: setTournamentId, - setRoundId: setRoundId, - ), - ], - ), - ); - } else { - return Scaffold( - appBar: AppBar( - title: AutoSizeText( - widget.broadcast.title, - minFontSize: 14.0, - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), - bottom: TabBar( - controller: _tabController, - tabs: [ - Tab(text: context.l10n.broadcastOverview), - Tab(text: context.l10n.broadcastBoards), - ], - ), - ), - body: TabBarView( - controller: _tabController, - children: [ - _TabView( - sliver: BroadcastOverviewTab( - broadcast: widget.broadcast, - tournamentId: _selectedTournamentId, + } + }, + ); + + switch (round) { + case AsyncData(value: final _): + if (Theme.of(context).platform == TargetPlatform.iOS) { + final tabSwitcher = + CupertinoSlidingSegmentedControl<_CupertinoView>( + groupValue: selectedTab, + children: { + _CupertinoView.overview: Text(context.l10n.broadcastOverview), + _CupertinoView.boards: Text(context.l10n.broadcastBoards), + }, + onValueChanged: (_CupertinoView? view) { + if (view != null) { + setCupertinoTab(view); + } + }, + ); + return NotificationListener( + onNotification: handleScrollNotification, + child: CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + automaticBackgroundVisibility: false, + backgroundColor: Colors.transparent, + border: null, + middle: AutoSizeText( + widget.broadcast.title, + minFontSize: 14.0, + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + child: Column( + children: [ + Expanded( + child: selectedTab == _CupertinoView.overview + ? _TabView( + cupertinoHeaderOpacity: headerOpacity, + cupertinoTabSwitcher: tabSwitcher, + sliver: BroadcastOverviewTab( + broadcast: widget.broadcast, + tournamentId: _selectedTournamentId, + ), + ) + : _TabView( + cupertinoHeaderOpacity: headerOpacity, + cupertinoTabSwitcher: tabSwitcher, + sliver: BroadcastBoardsTab( + roundId: _selectedRoundId ?? + tournament.defaultRoundId, + ), + ), + ), + _BottomBar( + tournament: tournament, + roundId: _selectedRoundId ?? tournament.defaultRoundId, + setTournamentId: setTournamentId, + setRoundId: setRoundId, + ), + ], ), ), - _TabView( - sliver: BroadcastBoardsTab( - roundId: _selectedRoundId ?? value.defaultRoundId, + ); + } else { + return Scaffold( + appBar: AppBar( + title: AutoSizeText( + widget.broadcast.title, + minFontSize: 14.0, + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + bottom: TabBar( + controller: _tabController, + tabs: [ + Tab(text: context.l10n.broadcastOverview), + Tab(text: context.l10n.broadcastBoards), + ], ), ), - ], - ), - bottomNavigationBar: _BottomBar( - tournament: value, - roundId: _selectedRoundId ?? value.defaultRoundId, - setTournamentId: setTournamentId, - setRoundId: setRoundId, - ), - ); + body: TabBarView( + controller: _tabController, + children: [ + _TabView( + sliver: BroadcastOverviewTab( + broadcast: widget.broadcast, + tournamentId: _selectedTournamentId, + ), + ), + _TabView( + sliver: BroadcastBoardsTab( + roundId: _selectedRoundId ?? tournament.defaultRoundId, + ), + ), + ], + ), + bottomNavigationBar: _BottomBar( + tournament: tournament, + roundId: _selectedRoundId ?? tournament.defaultRoundId, + setTournamentId: setTournamentId, + setRoundId: setRoundId, + ), + ); + } + case AsyncError(:final error): + return Center(child: Text('Could not load broadcast: $error')); + case _: + return const Center(child: CircularProgressIndicator.adaptive()); } case AsyncError(:final error): - return Center(child: Text(error.toString())); + return Center(child: Text('Could not load broadcast: $error')); case _: return const Center(child: CircularProgressIndicator.adaptive()); } @@ -186,10 +252,12 @@ class _TabView extends StatelessWidget { const _TabView({ required this.sliver, this.cupertinoTabSwitcher, + this.cupertinoHeaderOpacity = 0.0, }); final Widget sliver; final Widget? cupertinoTabSwitcher; + final double cupertinoHeaderOpacity; @override Widget build(BuildContext context) { @@ -198,14 +266,37 @@ class _TabView extends StatelessWidget { ? EdgeInsets.only(top: MediaQuery.paddingOf(context).top) : EdgeInsets.zero) + Styles.bodyPadding; + final backgroundColor = Styles.cupertinoAppBarColor.resolveFrom(context); return Shimmer( child: CustomScrollView( slivers: [ if (cupertinoTabSwitcher != null) - SliverPadding( - padding: Styles.bodyPadding + - EdgeInsets.only(top: MediaQuery.paddingOf(context).top), - sliver: SliverToBoxAdapter(child: cupertinoTabSwitcher), + PinnedHeaderSliver( + child: ClipRect( + child: BackdropFilter( + enabled: backgroundColor.alpha != 0xFF, + filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0), + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + decoration: ShapeDecoration( + color: cupertinoHeaderOpacity == 1.0 + ? backgroundColor + : Colors.transparent, + shape: LinearBorder.bottom( + side: BorderSide( + color: cupertinoHeaderOpacity == 1.0 + ? const Color(0x4D000000) + : Colors.transparent, + width: 0.0, + ), + ), + ), + padding: Styles.bodyPadding + + EdgeInsets.only(top: MediaQuery.paddingOf(context).top), + child: cupertinoTabSwitcher, + ), + ), + ), ), SliverPadding( padding: edgeInsets, From 1ca8661d99695b511fe46c7ed4b3e8f59b353a77 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Mon, 9 Dec 2024 11:07:37 +0100 Subject: [PATCH 863/979] Tweak markdown padding on broadcast overview tab --- .../broadcast/broadcast_overview_tab.dart | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_overview_tab.dart b/lib/src/view/broadcast/broadcast_overview_tab.dart index 94ae982e4f..1f5d60941e 100644 --- a/lib/src/view/broadcast/broadcast_overview_tab.dart +++ b/lib/src/view/broadcast/broadcast_overview_tab.dart @@ -80,17 +80,16 @@ class BroadcastOverviewTab extends ConsumerWidget { ), ], ), - if (description != null) - Padding( - padding: const EdgeInsets.all(16), - child: MarkdownBody( - data: description, - onTapLink: (text, url, title) { - if (url == null) return; - launchUrl(Uri.parse(url)); - }, - ), + if (description != null) ...[ + const SizedBox(height: 16), + MarkdownBody( + data: description, + onTapLink: (text, url, title) { + if (url == null) return; + launchUrl(Uri.parse(url)); + }, ), + ], ], ), ); From ad4a210408350aec028840f67258db22464ad4b0 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 9 Dec 2024 11:22:24 +0100 Subject: [PATCH 864/979] Improve cupertino custom game screen creation --- .../view/play/create_custom_game_screen.dart | 411 +++++++++++------- 1 file changed, 253 insertions(+), 158 deletions(-) diff --git a/lib/src/view/play/create_custom_game_screen.dart b/lib/src/view/play/create_custom_game_screen.dart index 7840de19b6..57fa7b7e72 100644 --- a/lib/src/view/play/create_custom_game_screen.dart +++ b/lib/src/view/play/create_custom_game_screen.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:ui'; import 'package:deep_pick/deep_pick.dart'; import 'package:flutter/cupertino.dart'; @@ -46,7 +47,11 @@ class CreateCustomGameScreen extends StatelessWidget { Widget _buildIos(BuildContext context) { return const CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar(), + navigationBar: CupertinoNavigationBar( + automaticBackgroundVisibility: false, + backgroundColor: Colors.transparent, + border: null, + ), child: _CupertinoBody(), ); } @@ -103,8 +108,8 @@ class _AndroidBodyState extends State<_AndroidBody> body: TabBarView( controller: _tabController, children: [ - _CreateGameBody(setViewMode: setViewMode), - _ChallengesBody(setViewMode: setViewMode), + _TabView(sliver: _CreateGameBody(setViewMode: setViewMode)), + _TabView(sliver: _ChallengesBody(setViewMode: setViewMode)), ], ), ); @@ -120,45 +125,127 @@ class _CupertinoBody extends StatefulWidget { class _CupertinoBodyState extends State<_CupertinoBody> { _ViewMode _selectedSegment = _ViewMode.create; + double headerOpacity = 0; void setViewMode(_ViewMode mode) { setState(() { _selectedSegment = mode; + headerOpacity = 0.0; }); } + bool handleScrollNotification(ScrollNotification notification) { + if (notification is ScrollUpdateNotification && notification.depth == 0) { + final ScrollMetrics metrics = notification.metrics; + double scrollExtent = 0.0; + switch (metrics.axisDirection) { + case AxisDirection.up: + scrollExtent = metrics.extentAfter; + case AxisDirection.down: + scrollExtent = metrics.extentBefore; + case AxisDirection.right: + case AxisDirection.left: + break; + } + + final opacity = scrollExtent > 0.0 ? 1.0 : 0.0; + + if (opacity != headerOpacity) { + setState(() { + headerOpacity = opacity; + }); + } + } + return false; + } + @override Widget build(BuildContext context) { - return SafeArea( - child: Column( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: Styles.bodyPadding, - child: CupertinoSlidingSegmentedControl<_ViewMode>( - groupValue: _selectedSegment, - children: { - _ViewMode.create: Text(context.l10n.createAGame), - _ViewMode.challenges: - Text(context.l10n.mobileCustomGameJoinAGame), - }, - onValueChanged: (_ViewMode? view) { - if (view != null) { - setState(() { - _selectedSegment = view; - }); - } - }, + final tabSwitcher = CupertinoSlidingSegmentedControl<_ViewMode>( + groupValue: _selectedSegment, + children: { + _ViewMode.create: Text(context.l10n.createAGame), + _ViewMode.challenges: Text(context.l10n.mobileCustomGameJoinAGame), + }, + onValueChanged: (_ViewMode? view) { + if (view != null) { + setState(() { + _selectedSegment = view; + }); + } + }, + ); + return NotificationListener( + onNotification: handleScrollNotification, + child: _selectedSegment == _ViewMode.create + ? _TabView( + cupertinoTabSwitcher: tabSwitcher, + cupertinoHeaderOpacity: headerOpacity, + sliver: _CreateGameBody(setViewMode: setViewMode), + ) + : _TabView( + cupertinoTabSwitcher: tabSwitcher, + cupertinoHeaderOpacity: headerOpacity, + sliver: _ChallengesBody(setViewMode: setViewMode), + ), + ); + } +} + +class _TabView extends StatelessWidget { + const _TabView({ + required this.sliver, + this.cupertinoTabSwitcher, + this.cupertinoHeaderOpacity = 0.0, + }); + + final Widget sliver; + final Widget? cupertinoTabSwitcher; + final double cupertinoHeaderOpacity; + + @override + Widget build(BuildContext context) { + final edgeInsets = MediaQuery.paddingOf(context) - + (cupertinoTabSwitcher != null + ? EdgeInsets.only(top: MediaQuery.paddingOf(context).top) + : EdgeInsets.zero) + + Styles.verticalBodyPadding; + final backgroundColor = Styles.cupertinoAppBarColor.resolveFrom(context); + return CustomScrollView( + slivers: [ + if (cupertinoTabSwitcher != null) + PinnedHeaderSliver( + child: ClipRect( + child: BackdropFilter( + enabled: backgroundColor.alpha != 0xFF, + filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0), + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + decoration: ShapeDecoration( + color: cupertinoHeaderOpacity == 1.0 + ? backgroundColor + : Colors.transparent, + shape: LinearBorder.bottom( + side: BorderSide( + color: cupertinoHeaderOpacity == 1.0 + ? const Color(0x4D000000) + : Colors.transparent, + width: 0.0, + ), + ), + ), + padding: Styles.bodyPadding + + EdgeInsets.only(top: MediaQuery.paddingOf(context).top), + child: cupertinoTabSwitcher, + ), + ), ), ), - Expanded( - child: _selectedSegment == _ViewMode.create - ? _CreateGameBody(setViewMode: setViewMode) - : _ChallengesBody(setViewMode: setViewMode), - ), - ], - ), + SliverPadding( + padding: edgeInsets, + sliver: sliver, + ), + ], ); } } @@ -225,7 +312,7 @@ class _ChallengesBodyState extends ConsumerState<_ChallengesBody> { final supportedChallenges = challenges .where((challenge) => challenge.variant.isPlaySupported) .toList(); - return ListView.separated( + return SliverList.separated( itemCount: supportedChallenges.length, separatorBuilder: (context, index) => const PlatformDivider(height: 1, cupertinoHasLeading: true), @@ -276,10 +363,13 @@ class _ChallengesBodyState extends ConsumerState<_ChallengesBody> { ); }, loading: () { - return const Center(child: CircularProgressIndicator.adaptive()); + return const SliverFillRemaining( + child: Center(child: CircularProgressIndicator.adaptive()), + ); }, - error: (error, stack) => - Center(child: Text(context.l10n.mobileCustomGameJoinAGame)), + error: (error, stack) => SliverFillRemaining( + child: Center(child: Text(context.l10n.mobileCustomGameJoinAGame)), + ), ); } } @@ -455,156 +545,161 @@ class _CreateGameBodyState extends ConsumerState<_CreateGameBody> { final userPerf = account?.perfs[timeControl == TimeControl.realTime ? preferences.perfFromCustom : Perf.correspondence]; - return Center( - child: ListView( - shrinkWrap: true, - padding: Theme.of(context).platform == TargetPlatform.iOS - ? Styles.sectionBottomPadding - : Styles.verticalBodyPadding, - children: [ - if (account != null) + return SliverPadding( + padding: Styles.sectionBottomPadding, + sliver: SliverList( + delegate: SliverChildListDelegate( + [ + if (account != null) + PlatformListTile( + harmonizeCupertinoTitleStyle: true, + title: Text(context.l10n.timeControl), + trailing: AdaptiveTextButton( + onPressed: () { + showChoicePicker( + context, + choices: [ + TimeControl.realTime, + TimeControl.correspondence, + ], + selectedItem: preferences.customTimeControl, + labelBuilder: (TimeControl timeControl) => Text( + timeControl == TimeControl.realTime + ? context.l10n.realTime + : context.l10n.correspondence, + ), + onSelectedItemChanged: (TimeControl value) { + ref + .read(gameSetupPreferencesProvider.notifier) + .setCustomTimeControl(value); + }, + ); + }, + child: Text( + preferences.customTimeControl == TimeControl.realTime + ? context.l10n.realTime + : context.l10n.correspondence, + ), + ), + ), + if (timeControl == TimeControl.realTime) + ...realTimeSelector + else + ...correspondenceSelector, PlatformListTile( harmonizeCupertinoTitleStyle: true, - title: Text(context.l10n.timeControl), + title: Text(context.l10n.variant), trailing: AdaptiveTextButton( onPressed: () { showChoicePicker( context, - choices: [ - TimeControl.realTime, - TimeControl.correspondence, - ], - selectedItem: preferences.customTimeControl, - labelBuilder: (TimeControl timeControl) => Text( - timeControl == TimeControl.realTime - ? context.l10n.realTime - : context.l10n.correspondence, - ), - onSelectedItemChanged: (TimeControl value) { + choices: [Variant.standard, Variant.chess960], + selectedItem: preferences.customVariant, + labelBuilder: (Variant variant) => Text(variant.label), + onSelectedItemChanged: (Variant variant) { ref .read(gameSetupPreferencesProvider.notifier) - .setCustomTimeControl(value); + .setCustomVariant(variant); }, ); }, - child: Text( - preferences.customTimeControl == TimeControl.realTime - ? context.l10n.realTime - : context.l10n.correspondence, + child: Text(preferences.customVariant.label), + ), + ), + ExpandedSection( + expand: preferences.customRated == false, + child: PlatformListTile( + harmonizeCupertinoTitleStyle: true, + title: Text(context.l10n.side), + trailing: AdaptiveTextButton( + onPressed: null, + child: Text(SideChoice.random.label(context.l10n)), ), ), ), - if (timeControl == TimeControl.realTime) - ...realTimeSelector - else - ...correspondenceSelector, - PlatformListTile( - harmonizeCupertinoTitleStyle: true, - title: Text(context.l10n.variant), - trailing: AdaptiveTextButton( - onPressed: () { - showChoicePicker( - context, - choices: [Variant.standard, Variant.chess960], - selectedItem: preferences.customVariant, - labelBuilder: (Variant variant) => Text(variant.label), - onSelectedItemChanged: (Variant variant) { + if (account != null) + PlatformListTile( + harmonizeCupertinoTitleStyle: true, + title: Text(context.l10n.rated), + trailing: Switch.adaptive( + applyCupertinoTheme: true, + value: preferences.customRated, + onChanged: (bool value) { ref .read(gameSetupPreferencesProvider.notifier) - .setCustomVariant(variant); + .setCustomRated(value); }, - ); - }, - child: Text(preferences.customVariant.label), - ), - ), - ExpandedSection( - expand: preferences.customRated == false, - child: PlatformListTile( - harmonizeCupertinoTitleStyle: true, - title: Text(context.l10n.side), - trailing: AdaptiveTextButton( - onPressed: null, - child: Text(SideChoice.random.label(context.l10n)), + ), ), - ), - ), - if (account != null) - PlatformListTile( - harmonizeCupertinoTitleStyle: true, - title: Text(context.l10n.rated), - trailing: Switch.adaptive( - applyCupertinoTheme: true, - value: preferences.customRated, - onChanged: (bool value) { + if (userPerf != null) + PlayRatingRange( + perf: userPerf, + ratingDelta: preferences.customRatingDelta, + onRatingDeltaChange: (int subtract, int add) { ref .read(gameSetupPreferencesProvider.notifier) - .setCustomRated(value); + .setCustomRatingRange(subtract, add); }, ), - ), - if (userPerf != null) - PlayRatingRange( - perf: userPerf, - ratingDelta: preferences.customRatingDelta, - onRatingDeltaChange: (int subtract, int add) { - ref - .read(gameSetupPreferencesProvider.notifier) - .setCustomRatingRange(subtract, add); + const SizedBox(height: 20), + FutureBuilder( + future: _pendingCreateGame, + builder: (context, snapshot) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: FatButton( + semanticsLabel: context.l10n.createAGame, + onPressed: timeControl == TimeControl.realTime + ? isValidTimeControl + ? () { + pushPlatformRoute( + context, + rootNavigator: true, + builder: (BuildContext context) { + return GameScreen( + seek: GameSeek.custom( + preferences, + account, + ), + ); + }, + ); + } + : null + : snapshot.connectionState == + ConnectionState.waiting + ? null + : () async { + _pendingCreateGame = ref + .read(createGameServiceProvider) + .newCorrespondenceGame( + GameSeek.correspondence( + preferences, + account, + ), + ); + + await _pendingCreateGame; + widget.setViewMode(_ViewMode.challenges); + }, + child: + Text(context.l10n.createAGame, style: Styles.bold), + ), + ); }, ), - const SizedBox(height: 20), - FutureBuilder( - future: _pendingCreateGame, - builder: (context, snapshot) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 20.0), - child: FatButton( - semanticsLabel: context.l10n.createAGame, - onPressed: timeControl == TimeControl.realTime - ? isValidTimeControl - ? () { - pushPlatformRoute( - context, - rootNavigator: true, - builder: (BuildContext context) { - return GameScreen( - seek: GameSeek.custom( - preferences, - account, - ), - ); - }, - ); - } - : null - : snapshot.connectionState == ConnectionState.waiting - ? null - : () async { - _pendingCreateGame = ref - .read(createGameServiceProvider) - .newCorrespondenceGame( - GameSeek.correspondence( - preferences, - account, - ), - ); - - await _pendingCreateGame; - widget.setViewMode(_ViewMode.challenges); - }, - child: Text(context.l10n.createAGame, style: Styles.bold), - ), - ); - }, - ), - ], + ], + ), ), ); }, - loading: () => const Center(child: CircularProgressIndicator.adaptive()), - error: (error, stackTrace) => const Center( - child: Text('Could not load account data'), + loading: () => const SliverFillRemaining( + child: Center(child: CircularProgressIndicator.adaptive()), + ), + error: (error, stackTrace) => const SliverFillRemaining( + child: Center( + child: Text('Could not load account data'), + ), ), ); } From e51e989cad0673b60d392155b387be9a92bc09f8 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 9 Dec 2024 11:26:31 +0100 Subject: [PATCH 865/979] Don't use a pinner header on broadcast screen --- .../broadcast/broadcast_round_screen.dart | 136 +++++------------- 1 file changed, 37 insertions(+), 99 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_round_screen.dart b/lib/src/view/broadcast/broadcast_round_screen.dart index 5696508069..d85ee37ad1 100644 --- a/lib/src/view/broadcast/broadcast_round_screen.dart +++ b/lib/src/view/broadcast/broadcast_round_screen.dart @@ -1,5 +1,3 @@ -import 'dart:ui'; - import 'package:auto_size_text/auto_size_text.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/cupertino.dart'; @@ -39,7 +37,6 @@ class _BroadcastRoundScreenState extends ConsumerState BroadcastRoundId? _selectedRoundId; bool roundLoaded = false; - double headerOpacity = 0; @override void initState() { @@ -58,7 +55,6 @@ class _BroadcastRoundScreenState extends ConsumerState void setCupertinoTab(_CupertinoView mode) { setState(() { selectedTab = mode; - headerOpacity = 0.0; }); } @@ -75,31 +71,6 @@ class _BroadcastRoundScreenState extends ConsumerState }); } - bool handleScrollNotification(ScrollNotification notification) { - if (notification is ScrollUpdateNotification && notification.depth == 0) { - final ScrollMetrics metrics = notification.metrics; - double scrollExtent = 0.0; - switch (metrics.axisDirection) { - case AxisDirection.up: - scrollExtent = metrics.extentAfter; - case AxisDirection.down: - scrollExtent = metrics.extentBefore; - case AxisDirection.right: - case AxisDirection.left: - break; - } - - final opacity = scrollExtent > 0.0 ? 1.0 : 0.0; - - if (opacity != headerOpacity) { - setState(() { - headerOpacity = opacity; - }); - } - } - return false; - } - @override Widget build(BuildContext context) { final tournament = @@ -149,49 +120,41 @@ class _BroadcastRoundScreenState extends ConsumerState } }, ); - return NotificationListener( - onNotification: handleScrollNotification, - child: CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - automaticBackgroundVisibility: false, - backgroundColor: Colors.transparent, - border: null, - middle: AutoSizeText( - widget.broadcast.title, - minFontSize: 14.0, - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), + return CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + middle: AutoSizeText( + widget.broadcast.title, + minFontSize: 14.0, + overflow: TextOverflow.ellipsis, + maxLines: 1, ), - child: Column( - children: [ - Expanded( - child: selectedTab == _CupertinoView.overview - ? _TabView( - cupertinoHeaderOpacity: headerOpacity, - cupertinoTabSwitcher: tabSwitcher, - sliver: BroadcastOverviewTab( - broadcast: widget.broadcast, - tournamentId: _selectedTournamentId, - ), - ) - : _TabView( - cupertinoHeaderOpacity: headerOpacity, - cupertinoTabSwitcher: tabSwitcher, - sliver: BroadcastBoardsTab( - roundId: _selectedRoundId ?? - tournament.defaultRoundId, - ), + ), + child: Column( + children: [ + Expanded( + child: selectedTab == _CupertinoView.overview + ? _TabView( + cupertinoTabSwitcher: tabSwitcher, + sliver: BroadcastOverviewTab( + broadcast: widget.broadcast, + tournamentId: _selectedTournamentId, ), - ), - _BottomBar( - tournament: tournament, - roundId: _selectedRoundId ?? tournament.defaultRoundId, - setTournamentId: setTournamentId, - setRoundId: setRoundId, - ), - ], - ), + ) + : _TabView( + cupertinoTabSwitcher: tabSwitcher, + sliver: BroadcastBoardsTab( + roundId: _selectedRoundId ?? + tournament.defaultRoundId, + ), + ), + ), + _BottomBar( + tournament: tournament, + roundId: _selectedRoundId ?? tournament.defaultRoundId, + setTournamentId: setTournamentId, + setRoundId: setRoundId, + ), + ], ), ); } else { @@ -252,12 +215,10 @@ class _TabView extends StatelessWidget { const _TabView({ required this.sliver, this.cupertinoTabSwitcher, - this.cupertinoHeaderOpacity = 0.0, }); final Widget sliver; final Widget? cupertinoTabSwitcher; - final double cupertinoHeaderOpacity; @override Widget build(BuildContext context) { @@ -266,37 +227,14 @@ class _TabView extends StatelessWidget { ? EdgeInsets.only(top: MediaQuery.paddingOf(context).top) : EdgeInsets.zero) + Styles.bodyPadding; - final backgroundColor = Styles.cupertinoAppBarColor.resolveFrom(context); return Shimmer( child: CustomScrollView( slivers: [ if (cupertinoTabSwitcher != null) - PinnedHeaderSliver( - child: ClipRect( - child: BackdropFilter( - enabled: backgroundColor.alpha != 0xFF, - filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0), - child: AnimatedContainer( - duration: const Duration(milliseconds: 200), - decoration: ShapeDecoration( - color: cupertinoHeaderOpacity == 1.0 - ? backgroundColor - : Colors.transparent, - shape: LinearBorder.bottom( - side: BorderSide( - color: cupertinoHeaderOpacity == 1.0 - ? const Color(0x4D000000) - : Colors.transparent, - width: 0.0, - ), - ), - ), - padding: Styles.bodyPadding + - EdgeInsets.only(top: MediaQuery.paddingOf(context).top), - child: cupertinoTabSwitcher, - ), - ), - ), + SliverPadding( + padding: Styles.bodyPadding + + EdgeInsets.only(top: MediaQuery.paddingOf(context).top), + sliver: SliverToBoxAdapter(child: cupertinoTabSwitcher), ), SliverPadding( padding: edgeInsets, From b1f808e8d2943750f20466f0d35c8e1bb6197ac3 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 9 Dec 2024 11:32:03 +0100 Subject: [PATCH 866/979] Fix cupertino header background color --- lib/src/view/play/create_custom_game_screen.dart | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/src/view/play/create_custom_game_screen.dart b/lib/src/view/play/create_custom_game_screen.dart index 57fa7b7e72..a1fa193a8f 100644 --- a/lib/src/view/play/create_custom_game_screen.dart +++ b/lib/src/view/play/create_custom_game_screen.dart @@ -46,13 +46,15 @@ class CreateCustomGameScreen extends StatelessWidget { } Widget _buildIos(BuildContext context) { - return const CupertinoPageScaffold( + return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( automaticBackgroundVisibility: false, - backgroundColor: Colors.transparent, + backgroundColor: Styles.cupertinoAppBarColor + .resolveFrom(context) + .withValues(alpha: 0.0), border: null, ), - child: _CupertinoBody(), + child: const _CupertinoBody(), ); } @@ -224,7 +226,7 @@ class _TabView extends StatelessWidget { decoration: ShapeDecoration( color: cupertinoHeaderOpacity == 1.0 ? backgroundColor - : Colors.transparent, + : backgroundColor.withAlpha(0), shape: LinearBorder.bottom( side: BorderSide( color: cupertinoHeaderOpacity == 1.0 From 37b9913177ba422e5852d748f654284b36567614 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 9 Dec 2024 12:37:30 +0100 Subject: [PATCH 867/979] Remove upcoming broadcast section & tweaks --- lib/src/model/broadcast/broadcast.dart | 1 - .../model/broadcast/broadcast_providers.dart | 1 - .../model/broadcast/broadcast_repository.dart | 2 -- lib/src/utils/image.dart | 20 ++++++++++++++----- .../view/broadcast/broadcast_list_screen.dart | 18 ++++++++++------- lib/src/view/watch/watch_tab_screen.dart | 2 +- .../broadcast/broadcast_repository_test.dart | 3 +-- .../broadcasts_list_screen_test.dart | 4 ++-- 8 files changed, 30 insertions(+), 21 deletions(-) diff --git a/lib/src/model/broadcast/broadcast.dart b/lib/src/model/broadcast/broadcast.dart index 4a69054d9c..035aab4e68 100644 --- a/lib/src/model/broadcast/broadcast.dart +++ b/lib/src/model/broadcast/broadcast.dart @@ -7,7 +7,6 @@ part 'broadcast.freezed.dart'; typedef BroadcastList = ({ IList active, - IList upcoming, IList past, int? nextPage, }); diff --git a/lib/src/model/broadcast/broadcast_providers.dart b/lib/src/model/broadcast/broadcast_providers.dart index 6287d8686a..0c9f685d32 100644 --- a/lib/src/model/broadcast/broadcast_providers.dart +++ b/lib/src/model/broadcast/broadcast_providers.dart @@ -36,7 +36,6 @@ class BroadcastsPaginator extends _$BroadcastsPaginator { state = AsyncData( ( active: broadcastList.active, - upcoming: broadcastList.upcoming, past: broadcastList.past.addAll(broadcastListNewPage.past), nextPage: broadcastListNewPage.nextPage, ), diff --git a/lib/src/model/broadcast/broadcast_repository.dart b/lib/src/model/broadcast/broadcast_repository.dart index 9063efff0d..04bdbc2ea7 100644 --- a/lib/src/model/broadcast/broadcast_repository.dart +++ b/lib/src/model/broadcast/broadcast_repository.dart @@ -58,8 +58,6 @@ BroadcastList _makeBroadcastResponseFromJson( ) { return ( active: pick(json, 'active').asListOrThrow(_broadcastFromPick).toIList(), - upcoming: - pick(json, 'upcoming').asListOrThrow(_broadcastFromPick).toIList(), past: pick(json, 'past', 'currentPageResults') .asListOrThrow(_broadcastFromPick) .toIList(), diff --git a/lib/src/utils/image.dart b/lib/src/utils/image.dart index 24b2cdf77d..41b5a205ba 100644 --- a/lib/src/utils/image.dart +++ b/lib/src/utils/image.dart @@ -5,6 +5,12 @@ import 'package:image/image.dart' as img; import 'package:material_color_utilities/material_color_utilities.dart'; +typedef ImageColors = ({ + int primaryContainer, + int onPrimaryContainer, + int error, +}); + /// A worker that calculates the `primaryContainer` color of a remote image. /// /// The worker is created by calling [ImageColorWorker.spawn], and the computation @@ -12,15 +18,15 @@ import 'package:material_color_utilities/material_color_utilities.dart'; class ImageColorWorker { final SendPort _commands; final ReceivePort _responses; - final Map> _activeRequests = {}; + final Map> _activeRequests = {}; int _idCounter = 0; bool _closed = false; bool get closed => _closed; - Future<(int, int)?> getImageColors(String url) async { + Future getImageColors(String url) async { if (_closed) throw StateError('Closed'); - final completer = Completer<(int, int)?>.sync(); + final completer = Completer.sync(); final id = _idCounter++; _activeRequests[id] = completer; _commands.send((id, url)); @@ -64,7 +70,7 @@ class ImageColorWorker { if (response is RemoteError) { completer.completeError(response); } else { - completer.complete(response as (int, int)); + completer.complete(response as ImageColors); } if (_closed && _activeRequests.isEmpty) _responses.close(); @@ -102,7 +108,11 @@ class ImageColorWorker { isDark: false, contrastLevel: 0.0, ); - final colors = (scheme.primaryContainer, scheme.onPrimaryContainer); + final colors = ( + primaryContainer: scheme.primaryContainer, + onPrimaryContainer: scheme.onPrimaryContainer, + error: scheme.error, + ); sendPort.send((id, colors)); } catch (e) { sendPort.send((id, RemoteError(e.toString(), ''))); diff --git a/lib/src/view/broadcast/broadcast_list_screen.dart b/lib/src/view/broadcast/broadcast_list_screen.dart index c66e0c663e..6d55990abe 100644 --- a/lib/src/view/broadcast/broadcast_list_screen.dart +++ b/lib/src/view/broadcast/broadcast_list_screen.dart @@ -47,7 +47,9 @@ class BroadcastListScreen extends StatelessWidget { navigationBar: CupertinoNavigationBar( middle: title, automaticBackgroundVisibility: false, - backgroundColor: Colors.transparent, + backgroundColor: Styles.cupertinoAppBarColor + .resolveFrom(context) + .withValues(alpha: 0.0), border: null, ), child: const _Body(), @@ -137,7 +139,6 @@ class _BodyState extends ConsumerState<_Body> { final sections = [ (context.l10n.broadcastOngoing, broadcasts.value!.active), - (context.l10n.broadcastUpcoming, broadcasts.value!.upcoming), (context.l10n.broadcastCompleted, broadcasts.value!.past), ]; @@ -249,6 +250,7 @@ class BroadcastGridItem extends StatefulWidget { typedef _CardColors = ({ Color primaryContainer, Color onPrimaryContainer, + Color error, }); final Map _colorsCache = {}; @@ -283,10 +285,11 @@ class _BroadcastGridItemState extends State { } else if (widget.worker.closed == false) { final response = await widget.worker.getImageColors(provider.url); if (response != null) { - final (primaryContainer, onPrimaryContainer) = response; + final (:primaryContainer, :onPrimaryContainer, :error) = response; final cardColors = ( primaryContainer: Color(primaryContainer), onPrimaryContainer: Color(onPrimaryContainer), + error: Color(error), ); _colorsCache[provider] = cardColors; if (mounted) { @@ -310,6 +313,7 @@ class _BroadcastGridItemState extends State { final subTitleColor = _cardColors?.onPrimaryContainer.withValues(alpha: 0.7) ?? textShade(context, 0.7); + final liveColor = _cardColors?.error ?? LichessColors.red; return GestureDetector( onTap: () { @@ -351,10 +355,10 @@ class _BroadcastGridItemState extends State { begin: Alignment.center, end: Alignment.bottomCenter, colors: [ - backgroundColor.withValues(alpha: 0.10), + backgroundColor.withValues(alpha: 0.0), backgroundColor.withValues(alpha: 1.0), ], - stops: const [0.5, 1.00], + stops: const [0.7, 1.10], tileMode: TileMode.clamp, ).createShader(bounds); }, @@ -401,12 +405,12 @@ class _BroadcastGridItemState extends State { ), const SizedBox(width: 4.0), if (widget.broadcast.isLive) - const Text( + Text( 'LIVE', style: TextStyle( fontSize: 15, fontWeight: FontWeight.bold, - color: Colors.red, + color: liveColor, ), overflow: TextOverflow.ellipsis, ) diff --git a/lib/src/view/watch/watch_tab_screen.dart b/lib/src/view/watch/watch_tab_screen.dart index 634837807c..f7b93fe9c0 100644 --- a/lib/src/view/watch/watch_tab_screen.dart +++ b/lib/src/view/watch/watch_tab_screen.dart @@ -215,7 +215,7 @@ class _BroadcastWidget extends ConsumerWidget { ), ), children: [ - ...CombinedIterableView([data.active, data.upcoming, data.past]) + ...CombinedIterableView([data.active, data.past]) .take(numberOfItems) .map((broadcast) => _BroadcastTile(broadcast: broadcast)), ], diff --git a/test/model/broadcast/broadcast_repository_test.dart b/test/model/broadcast/broadcast_repository_test.dart index d56e080a9a..e07216c35e 100644 --- a/test/model/broadcast/broadcast_repository_test.dart +++ b/test/model/broadcast/broadcast_repository_test.dart @@ -18,7 +18,7 @@ void main() { if (request.url.path == '/api/broadcast/top') { return mockResponse( r''' -{"active":[{"tour":{"id":"ioLNPN8j","name":"2nd Rustam Kasimdzhanov Cup 2024","slug":"2nd-rustam-kasimdzhanov-cup-2024","info":{"format":"10-player round-robin","tc":"Rapid","players":"Abdusattorov, Rapport, Mamedyarov, Grischuk"},"createdAt":1720352380417,"url":"https://lichess.org/broadcast/2nd-rustam-kasimdzhanov-cup-2024/ioLNPN8j","tier":5,"dates":[1720431600000,1720519800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=uzchess23:relay:ioLNPN8j:ya35G192.jpg&w=800&sig=b6543625b806cf43f4ee652d0a23d80fad236b35","markup":"

    The 2nd International Rustam Kasimdzhanov Cup 2024 is a 10-player round-robin tournament, held from the 8th to the 9th of July in Tashkent, Uzbekistan.

    \n

    Time control is 15 minutes for the entire game with a 10-second increment from move 1.

    \n

    Standings

    \n","leaderboard":true},"round":{"id":"A4J7qTO6","name":"Round 8","slug":"round-8","createdAt":1720438046707,"ongoing":true,"startsAt":1720516500000,"url":"https://lichess.org/broadcast/2nd-rustam-kasimdzhanov-cup-2024/round-8/A4J7qTO6"}},{"tour":{"id":"a4gBsu31","name":"Dutch Championship 2024 | Open","slug":"dutch-championship-2024--open","info":{"format":"10-player knockout","tc":"Classical","players":"Warmerdam, l’Ami, Bok, Sokolov"},"createdAt":1720037021926,"url":"https://lichess.org/broadcast/dutch-championship-2024--open/a4gBsu31","tier":4,"dates":[1720263600000,1720882800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:a4gBsu31:OgaRY7Pw.jpg&w=800&sig=cb141524b135d0cdc45deafcb9b4bfffc805ecee","markup":"

    The Dutch Championship 2024 | Open is a 10-player single-elimination knockout tournament, held from the 6th to the 13th of July in Utrecht, the Netherlands.

    \n

    Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

    \n

    Official Website | Results

    \n
    \n

    If a round ends in a tie after 2 classical games, a tiebreak match of 2 blitz games is played. Time control is 4+2.

    \n

    If the first tiebreak match ends in another tie, a second tiebreak match of 2 blitz games with reversed colours is played. Time control is 4+2.

    \n

    If the second tiebreak match ends in another tie, colours are drawn and a sudden death is played. Time control is 4+2 for White and 5+2 for Black. The first player to win a game, wins the round. After every 2 games, the colour order is changed.

    \n"},"round":{"id":"Xfe00Awr","name":"Quarter-Finals | Game 2","slug":"quarter-finals--game-2","createdAt":1720037148839,"ongoing":true,"startsAt":1720522800000,"url":"https://lichess.org/broadcast/dutch-championship-2024--open/quarter-finals--game-2/Xfe00Awr"},"group":"Dutch Championship 2024"},{"tour":{"id":"aPC3ATVG","name":"FIDE World Senior Team Chess Championships 2024 | 50+","slug":"fide-world-senior-team-chess-championships-2024--50","info":{"format":"9-round Swiss for teams","tc":"Classical","players":"Adams, Ehlvest, David, Novikov"},"createdAt":1719921457211,"url":"https://lichess.org/broadcast/fide-world-senior-team-chess-championships-2024--50/aPC3ATVG","tier":4,"dates":[1719926100000,1720685700000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=mansuba64:relay:aPC3ATVG:xuiZWY67.jpg&w=800&sig=bfc2aa87dce4ed7bdfb5ce5b9f16285e23479f05","markup":"

    The FIDE World Senior Team Chess Championships 2024 | 50+ is a 9-round Swiss for teams, held from the 2nd to the 11th of July in Kraków, Poland.

    \n

    Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

    \n

    Official Website | Standings

    \n
    \n

    There shall be two categories; Open age 50+ and Open age 65+ with separate events for women.
    The player must have reached or reach the required age during the year of competition.
    There shall be separate Women’s Championship(s) if there are at least ten teams from at least two continents. Otherwise women’s teams play in Open competition
    The Championships are open tournaments for teams registered by their federation. FIDE member federations shall have the right to send as many teams as they wish.

    \n

    The winning team obtains the title “World Team Champion “age 50+ (or age 65+)”.
    The best placed women team obtains the title “World Women Team Champion” age 50+ (or age 65+).

    \n

    Prize Fund: 10,000 EUR

    \n","teamTable":true},"round":{"id":"YIw910wS","name":"Round 7","slug":"round-7","createdAt":1719928673349,"startsAt":1720530900000,"url":"https://lichess.org/broadcast/fide-world-senior-team-chess-championships-2024--50/round-6/Gue2qJfw"},"group":"FIDE World Senior Team Chess Championships 2024"},{"tour":{"id":"tCMfpIJI","name":"43rd Villa de Benasque Open 2024","slug":"43rd-villa-de-benasque-open-2024","info":{"format":"10-round Swiss","tc":"Classical","players":"Alekseenko, Bartel, Pichot"},"createdAt":1719422556116,"url":"https://lichess.org/broadcast/43rd-villa-de-benasque-open-2024/tCMfpIJI","tier":4,"dates":[1720189800000,1720941300000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=kike0:relay:tCMfpIJI:P6c1Rrxn.jpg&w=800&sig=69d4e6158f133578bbb35519346e4395d891ca2c","markup":"

    The 43rd Villa de Benasque Open 2024 is a 10-round Swiss, held from the 5th to the 14th of July in Benasque, Spain.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move one.

    \n

    GM Kirill Alekseenko is the tournament's top seed - with nearly 100 titled players, 500 players in total, and over €50,000 in prizes.

    \n

    Official Website | Standings

    \n
    \n

    El XLIII Open Internacional Villa de Benasque se disputará por el Sistema Suizo a 10 rondas, del 5 al 14 de Julio de 2024. El GM Alekseenko lidera un ranking con cerca de 100 titulados, 500 jugadores y más de 50.000 euros de premios en metálico. El local de juego será el Pabellón Polideportivo de Benasque (España).

    \n

    El ritmo de juego será de 90 minutos + 30 segundos de incremento acumulativo por jugada empezando desde la primera.

    \n

    Web Oficial | Chess-Results

    \n"},"round":{"id":"SXAjWw0G","name":"Round 5","slug":"round-5","createdAt":1719422658882,"startsAt":1720534500000,"url":"https://lichess.org/broadcast/43rd-villa-de-benasque-open-2024/round-4/she3bD2w"}},{"tour":{"id":"yOuW4siY","name":"Spanish U12 Championships 2024 | Classical","slug":"spanish-u12-championships-2024--classical","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1720081884293,"url":"https://lichess.org/broadcast/spanish-u12-championships-2024--classical/yOuW4siY","tier":3,"dates":[1720425600000,1720857600000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=josefeda:relay:yOuW4siY:FCsABLhH.jpg&w=800&sig=ca8faadb2725de80ab6a316e98b8505f1f620f71","markup":"

    The Spanish U12 Championship 2024 is a 9-round Swiss, held from the 6th to the 7th of July in Salobreña, Granada, Spain.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website | Standings

    \n
    \n

    Campeonato de España Sub 12 2024

    \n"},"round":{"id":"eCa2CbqM","name":"Ronda 3","slug":"ronda-3","createdAt":1720082094252,"ongoing":true,"startsAt":1720512000000,"url":"https://lichess.org/broadcast/spanish-u12-championships-2024--classical/ronda-3/eCa2CbqM"},"group":"Spanish U12 Championships 2024"},{"tour":{"id":"JQGYmn68","name":"Scottish Championship International Open 2024","slug":"scottish-championship-international-open-2024","info":{"format":"9-round Swiss","tc":"90+30"},"createdAt":1720440336101,"url":"https://lichess.org/broadcast/scottish-championship-international-open-2024/JQGYmn68","tier":3,"dates":[1720447200000,1720965600000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=prospect_d:relay:JQGYmn68:I58xFyHC.jpg&w=800&sig=ccd5889235ee538ce022dcc5ff8ed1568e5f4377","markup":"

    The Scottish Championship International Open 2024 is a 9-round Swiss, held from the 8th to the 14th of July in Dunfermline, Scotland.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Standings

    \n"},"round":{"id":"Nw190iGM","name":"Round 2","slug":"round-2","createdAt":1720451119128,"ongoing":true,"startsAt":1720515600000,"url":"https://lichess.org/broadcast/scottish-championship-international-open-2024/round-2/Nw190iGM"}},{"tour":{"id":"YBTYQbxm","name":"South Wales International Open 2024","slug":"south-wales-international-open-2024","info":{"format":"9-round Swiss","tc":"Classical","players":"Chatalbashev, Cuenca, Grieve, Han"},"createdAt":1720127613709,"url":"https://lichess.org/broadcast/south-wales-international-open-2024/YBTYQbxm","tier":3,"dates":[1720170000000,1720602000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:YBTYQbxm:BDfr290h.jpg&w=800&sig=2e914768c4d3781264493309d8e37c86221c8ac7","markup":"

    The South Wales International Open 2024 is a 9-round Swiss title norm tournament taking place in Bridgend, Wales from the 5th to the 10th of July.

    \n

    Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

    \n

    Official Website | Standings

    \n","leaderboard":true},"round":{"id":"Svyiq7jS","name":"Round 7","slug":"round-7","createdAt":1720127909656,"ongoing":true,"startsAt":1720515600000,"url":"https://lichess.org/broadcast/south-wales-international-open-2024/round-7/Svyiq7jS"}},{"tour":{"id":"BgVqV6b0","name":"Koege Open 2024","slug":"koege-open-2024","info":{"format":"10-player round-robin","tc":"Classical","players":"Petrov, Smith, Hector"},"createdAt":1720361492349,"url":"https://lichess.org/broadcast/koege-open-2024/BgVqV6b0","tier":3,"dates":[1720512300000,1720944300000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=fishdefend:relay:BgVqV6b0:kr4GaRvW.jpg&w=800&sig=b2f59c1ae2cb70a88c8e7872b1327b93c64cdad3","markup":"

    The Koege Open 2024 is a 10-player round-robin tournament, held from the 9th to the 14th of July in Køge, Denmark.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Standings

    \n
    \n

    Group 1: Boards 1-5
    Group 2: Boards 6-10

    \n"},"round":{"id":"y0ksveWZ","name":"Round 1","slug":"round-1","createdAt":1720460080111,"ongoing":true,"startsAt":1720512300000,"url":"https://lichess.org/broadcast/koege-open-2024/round-1/y0ksveWZ"}},{"tour":{"id":"XV3jpD1b","name":"Belgian Championship 2024 | Expert","slug":"belgian-championship-2024--expert","info":{"format":"10-player round-robin","tc":"Classical"},"createdAt":1720276555430,"url":"https://lichess.org/broadcast/belgian-championship-2024--expert/XV3jpD1b","tier":3,"dates":[1720267200000,1720944000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=sergioglorias:relay:XV3jpD1b:BkW8q64n.jpg&w=800&sig=017b9b9cf354268158da36eba185e961d2cfc5e3","markup":"

    The Belgian Championship 2024 | Expert is a 10-player round-robin tournament, held from the 6th to the 14th of July in Lier, Belgium.

    \n

    The winner will be crowned Belgian Chess Champion 2024.

    \n

    Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

    \n

    Official Website

    \n","leaderboard":true},"round":{"id":"iSD0HAuQ","name":"Round 4","slug":"round-4","createdAt":1720276601311,"ongoing":true,"startsAt":1720526400000,"url":"https://lichess.org/broadcast/belgian-championship-2024--expert/round-4/iSD0HAuQ"},"group":"Belgian Championship 2024"},{"tour":{"id":"oo69aO3w","name":"SAIF Powertec Bangladesh Championship 2024","slug":"saif-powertec-bangladesh-championship-2024","info":{"format":"14-player round-robin","tc":"Classical"},"createdAt":1719050225145,"url":"https://lichess.org/broadcast/saif-powertec-bangladesh-championship-2024/oo69aO3w","tier":3,"dates":[1719133200000,1720602000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=fathirahman:relay:oo69aO3w:o2ATDvXh.jpg&w=800&sig=f93c0ea42a7223efb8efdc4e245acca39962b9f3","markup":"

    The SAIF Powertec Bangladesh Championship 2024 is a 14-player round-robin tournament, held from the 23rd of June to the 6th of July in Dhaka, Bangladesh.

    \n

    Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

    \n

    Official Website | Results

    \n
    \n

    SAIF Powertec 48th Bangladesh National Chess Championship 2024 is Bangladesh's national chess championship. Top 5 players are:

    \n
      \n
    1. FM Manon, Reja Neer 2445
    2. \n
    3. IM Mohammad Fahad, Rahman 2437
    4. \n
    5. GM Rahman, Ziaur 2423
    6. \n
    7. GM Hossain, Enamul 2365
    8. \n
    9. GM Murshed, Niaz 2317
    10. \n
    \n

    The previous series (2022) champion is GM Enamul Hossain. This is a 14-player round-robin tournament, where 3 GMs have been invited to play directly, and 11 players are from the top 11 of the qualifying round, known as the National B Championship.

    \n

    Five GMs were invited, but only three accepted the invitation. Therefore, instead of taking 9 players from National B, 11 players qualified to fulfill the round requirements.

    \n

    The top 5 players qualify for the Olympiad team.

    \n

    Here are useful links:

    \n\n"},"round":{"id":"LLqfCDm6","name":"Round 12 (Postponed)","slug":"round-12-postponed","createdAt":1719050487028,"ongoing":true,"startsAt":1720515600000,"url":"https://lichess.org/broadcast/saif-powertec-bangladesh-championship-2024/round-12-postponed/LLqfCDm6"}},{"tour":{"id":"Qag4N0cA","name":"4th La Plagne Festival 2024","slug":"4th-la-plagne-festival-2024","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1720274920410,"url":"https://lichess.org/broadcast/4th-la-plagne-festival-2024/Qag4N0cA","tier":3,"dates":[1720278000000,1720771200000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:Qag4N0cA:v3g6RfVf.jpg&w=800&sig=78b9c7d33a747e3c20fc26fd5360d07c9546debc","markup":"

    The 4th La Plagne International Chess Festival is a 9-round Swiss, held from the 6th to the 12th of July at La Plagne in Savoie, France.

    \n

    Time control is is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

    \n

    Official Website | Standings

    \n"},"round":{"id":"0qufdZnF","name":"Round 5","slug":"round-5","createdAt":1720286940762,"startsAt":1720531800000,"url":"https://lichess.org/broadcast/4th-la-plagne-festival-2024/round-4/gQt8ubbC"}},{"tour":{"id":"95l4pho3","name":"Peruvian Championship Finals 2024 | Open","slug":"peruvian-championship-finals-2024--open","info":{"format":"12-player round-robin","tc":"Classical","players":"Terry, Flores Quillas, Leiva"},"createdAt":1720272022000,"url":"https://lichess.org/broadcast/peruvian-championship-finals-2024--open/95l4pho3","tier":3,"dates":[1720278000000,1720796400000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:95l4pho3:mDrHsa8C.jpg&w=800&sig=0d81273752b2bcdd47180ae23201f15074a91be9","markup":"

    The Peruvian Championship Finals 2024 | Open is a 12-player round-robin tournament, held from the 6th to the 12th of July in Lima, Peru.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Standings

    \n","leaderboard":true},"round":{"id":"Pi0HtFDs","name":"Round 6","slug":"round-6","createdAt":1720272103102,"startsAt":1720537200000,"url":"https://lichess.org/broadcast/peruvian-championship-finals-2024--open/round-5/JuIghW2d"},"group":"Peruvian Championship Finals 2024"},{"tour":{"id":"85buXS8z","name":"2024 Sydney Championships | Open","slug":"2024-sydney-championships--open","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1713001604469,"url":"https://lichess.org/broadcast/2024-sydney-championships--open/85buXS8z","tier":3,"dates":[1720226700000,1720570500000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=rootyhillcc:relay:85buXS8z:GSmVFAej.jpg&w=800&sig=5319f37a9eb1bdd5f399316b507522048594d7ed","markup":"

    The 2024 Sydney Championships | Open is a 9-round Swiss, held from the 6th to the 10th in Sydney, Australia.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website | Results

    \n"},"round":{"id":"FxnR92Ll","name":"Round 9","slug":"round-9","createdAt":1720514826452,"startsAt":1720570500000,"url":"https://lichess.org/broadcast/2024-sydney-championships--open/round-8/GPbAETkc"},"group":"2024 Sydney Championships"},{"tour":{"id":"s7YVTwll","name":"United Arab Emirates Championship 2024 | Open","slug":"united-arab-emirates-championship-2024--open","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1720095141515,"url":"https://lichess.org/broadcast/united-arab-emirates-championship-2024--open/s7YVTwll","tier":3,"dates":[1720011600000,1720614600000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:s7YVTwll:t67JoGYK.jpg&w=800&sig=026a374d1ceff3c19522d12949c8ae28cd9e5ac6","markup":"

    The United Arab Emirates Championship 2024 | Open is a 9-round Swiss, held from the 3rd to the 10th of July in Dubai, United Arab Emirates.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Standings

    \n"},"round":{"id":"12JAmxw6","name":"Round 8","slug":"round-8","createdAt":1720095220069,"startsAt":1720530000000,"url":"https://lichess.org/broadcast/united-arab-emirates-championship-2024--open/round-8/12JAmxw6"},"group":"United Arab Emirates Championship 2024"},{"tour":{"id":"6s43vSQx","name":"Satranc Arena IM Chess Tournament Series - 5","slug":"satranc-arena-im-chess-tournament-series-5","info":{"format":"6-player double round-robin","tc":"Classical"},"createdAt":1720442634682,"url":"https://lichess.org/broadcast/satranc-arena-im-chess-tournament-series-5/6s43vSQx","tier":3,"dates":[1720425600000,1720792800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=arbiter_ubh:relay:6s43vSQx:1g42zUbN.jpg&w=800&sig=2031b1d31739c7d4cfe505cbd396b0d3bb44dce0","markup":"

    The Satranc Arena IM Chess Tournament Series - 5 is a 6-player double round-robin, held from the 8th to the 12th of July in Güzelbahçe, İzmir, Türkiye.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Standings

    \n","leaderboard":true},"round":{"id":"NOVf9rXm","name":"Round 4","slug":"round-4","createdAt":1720442689924,"startsAt":1720533600000,"url":"https://lichess.org/broadcast/satranc-arena-im-chess-tournament-series-5/round-3/WoVzBwaJ"}},{"tour":{"id":"veT0PjZv","name":"Paraćin Open 2024","slug":"paracin-open-2024","info":{"format":"9-round Swiss","tc":"Classical","players":"Safarli, Fier, Sasikiran, Prohászka"},"createdAt":1719958223829,"url":"https://lichess.org/broadcast/paracin-open-2024/veT0PjZv","tier":3,"dates":[1720015200000,1720683000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:veT0PjZv:hPF40XDY.jpg&w=800&sig=22712721714425152122d47a0017eb9cc9f8a8cb","markup":"

    The Paraćin Open 2024 is a 9-round Swiss, held from the 3rd to the 11th of July in Paraćin, Serbia.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website | Standings

    \n"},"round":{"id":"A81Fjh6K","name":"Round 7","slug":"round-7","createdAt":1719958344863,"startsAt":1720533600000,"url":"https://lichess.org/broadcast/paracin-open-2024/round-6/2m0ylraL"}},{"tour":{"id":"wv9ahJeR","name":"Greek Team Championship 2024 | Boards 1-40","slug":"greek-team-championship-2024--boards-1-40","info":{"format":"7-round Swiss for teams","tc":"Classical"},"createdAt":1720136757006,"url":"https://lichess.org/broadcast/greek-team-championship-2024--boards-1-40/wv9ahJeR","tier":3,"dates":[1720102500000,1720595700000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:wv9ahJeR:VlUevU6S.jpg&w=800&sig=7d53f78c2ae858286587919e0719844d343a8eb8","markup":"

    The Greek Team Championship 2024 is a 7-round Swiss for teams, held from the 4th to the 10th of July in Trikala, Greece.

    \n

    Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

    \n

    Official Website | Standings

    \n
    \n

    Photo by Nestoras Argiris on Unsplash

    \n","teamTable":true},"round":{"id":"myEffF4b","name":"Round 6","slug":"round-6","createdAt":1720137200753,"startsAt":1720534500000,"url":"https://lichess.org/broadcast/greek-team-championship-2024--boards-1-40/round-5/TEXHbMwG"},"group":"Greek Team Championship 2024"},{"tour":{"id":"F443vhNo","name":"46th Barberà del Vallès Open 2024","slug":"46th-barbera-del-valles-open-2024","info":{"format":"9-round Swiss","tc":"Classical","players":"Cuartas, Berdayes Ason, Alsina Leal"},"createdAt":1720274091992,"url":"https://lichess.org/broadcast/46th-barbera-del-valles-open-2024/F443vhNo","tier":3,"dates":[1720105200000,1720796400000],"markup":"

    The 46th Barberà del Vallès Open 2024 is a 9-round Swiss, held from the 4th to the 12th of July in Barberà del Vallès, Barcelona, Spain.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website | Standings

    \n"},"round":{"id":"CKW9YIsw","name":"Round 6","slug":"round-6","createdAt":1720274140173,"startsAt":1720537200000,"url":"https://lichess.org/broadcast/46th-barbera-del-valles-open-2024/round-5/XsCOWnCp"}},{"tour":{"id":"r4302nsd","name":"1000GM Independence Day GM Norm II","slug":"1000gm-independence-day-gm-norm-ii","info":{"format":"10-player round-robin","tc":"Classical"},"createdAt":1720337947267,"url":"https://lichess.org/broadcast/1000gm-independence-day-gm-norm-ii/r4302nsd","tier":3,"dates":[1720484100000,1720829700000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=linuxbrickie:relay:r4302nsd:fUxEnBVj.jpg&w=800&sig=ea04e951970b23cbca0242ffc8f687b7ab114c3e","markup":"

    The 1000GM Independence Day GM Norm II is a 10-player round-robin, held from the 8th to the 12th of July in San Jose, California, USA.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment starting from move one.

    \n

    Official Website

    \n"},"round":{"id":"o8NDvvjs","name":"Round 2","slug":"round-2","createdAt":1720338512373,"startsAt":1720548900000,"url":"https://lichess.org/broadcast/1000gm-independence-day-gm-norm-ii/round-1/oPQRIyNj"}},{"tour":{"id":"MRV2q3Yq","name":"ACC Monday Nights 2024 | Winter Cup","slug":"acc-monday-nights-2024--winter-cup","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1718105814905,"url":"https://lichess.org/broadcast/acc-monday-nights-2024--winter-cup/MRV2q3Yq","tier":3,"dates":[1718607600000,1723446000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=iacaster:relay:MRV2q3Yq:Vk5eiu90.jpg&w=800&sig=694fd9118495924e183277e487b619a62b0af671","markup":"

    The ACC Monday Nights 2024 | Winter Cup is a 9-round Swiss, held from the 17th of June to the 12th of August in Auckland, New Zealand.

    \n

    Time control is 75 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website | Results

    \n"},"round":{"id":"mzKINdP8","name":"Round 5","slug":"round-5","createdAt":1718106020711,"startsAt":1721026800000,"url":"https://lichess.org/broadcast/acc-monday-nights-2024--winter-cup/round-4/KGgLx2jQ"},"group":"ACC Monday Nights 2024"},{"tour":{"id":"yuUxbxbH","name":"II IRT do GM Milos","slug":"ii-irt-do-gm-milos","info":{"format":"5-round Swiss","tc":"Classical"},"createdAt":1718658553809,"url":"https://lichess.org/broadcast/ii-irt-do-gm-milos/yuUxbxbH","tier":3,"dates":[1718667000000,1721086200000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=sergioglorias:relay:yuUxbxbH:OkKBHwai.jpg&w=800&sig=cd93d77987db9ee650a714ac01d0e0980b68bccb","markup":"

    The II IRT do GM Milos is a 5-round Swiss tournament, held from the 17th of June to the 15th of July in São Paulo, Brazil.

    \n

    Time control is 60 minutes for the entire game, with a 30-second increment from move 1.

    \n

    Official Website | Results

    \n"},"round":{"id":"uKz9Ifu8","name":"Round 5","slug":"round-5","createdAt":1718660015687,"startsAt":1721086200000,"url":"https://lichess.org/broadcast/ii-irt-do-gm-milos/round-4/3YxAk0fs"}},{"tour":{"id":"vs7L5OPC","name":"Switzerland Team Championships SMM 2024","slug":"switzerland-team-championships-smm-2024","info":{"format":"10-team round-robin","tc":"Classical"},"createdAt":1713748519128,"url":"https://lichess.org/broadcast/switzerland-team-championships-smm-2024/vs7L5OPC","tier":3,"dates":[1710070200000,1728810000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:vs7L5OPC:GKhiEYf1.jpg&w=800&sig=f3c322ce93f23b997cec3c06e54493a48bdb561f","markup":"

    The Switzerland Team Championships SMM 2024 | NLA is a 10-team round-robin competition, held from the 10th of March to the 13th of October in Zurich, Switzerland.

    \n

    Time control is 100 minutes for 40 moves, followed by 50 minutes for the next 20 moves, followed by 15 minutes for the rest of the game, with a 30-second increment from move 1.

    \n

    Official Website | Standings (NLA) | Standings (NLB Ost A) | Standings (NLB Ost B)

    \n","teamTable":true},"round":{"id":"aHBXoEjV","name":"Round 6","slug":"round-6","createdAt":1713748519369,"startsAt":1724495400000,"url":"https://lichess.org/broadcast/switzerland-team-championships-smm-2024/round-5/Ted0iPnO"}}],"upcoming":[{"tour":{"id":"wXto4wTE","name":"Warsaw Chess Festival 2024 | Najdorf Memorial","slug":"warsaw-chess-festival-2024--najdorf-memorial","info":{"format":"9-round Swiss","tc":"Classical","players":"Kazakouski, Krasenkiw, Markowski, Rozentalis"},"createdAt":1720377119509,"url":"https://lichess.org/broadcast/warsaw-chess-festival-2024--najdorf-memorial/wXto4wTE","tier":3,"dates":[1720538100000,1721204100000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=alefzero:relay:wXto4wTE:rdHCIp4Q.jpg&w=800&sig=8e8dad9d2c0ec3c494b4226173d1c14ca1fd0040","markup":"

    The Najdorf Memorial 2024 is a 9-round Swiss, held from the 9th to the 17th of July in Warsaw, Poland.

    \n

    Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

    \n

    Official Website | Standings

    \n
    \n

    23rd international open tournament to commemorate GM Miguel (Mieczysław) Najdorf

    \n"},"round":{"id":"ENq91IQo","name":"Round 1","slug":"round-1","createdAt":1720377162688,"startsAt":1720538100000,"url":"https://lichess.org/broadcast/warsaw-chess-festival-2024--najdorf-memorial/round-1/ENq91IQo"},"group":"Warsaw Chess Festival 2024"},{"tour":{"id":"lb23JlWD","name":"2024 Australian University Chess League","slug":"2024-australian-university-chess-league","info":{"format":"6-team round-robin","tc":"Classical"},"createdAt":1718194400018,"url":"https://lichess.org/broadcast/2024-australian-university-chess-league/lb23JlWD","tier":3,"dates":[1720775700000,1720934100000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=rootyhillcc:relay:lb23JlWD:vBwmZg4h.jpg&w=800&sig=f6ed72005b1790b9bfc351908acad119b5c369ba","markup":"

    The 2024 Australian University Chess League is a 6-team round-robin, held from the 12th to the 14th of July in Sydney, Australia. There are four players per team.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment starting from move one.

    \n","teamTable":true},"round":{"id":"yAulT9F2","name":"Round 1","slug":"round-1","createdAt":1718194461669,"startsAt":1720775700000,"url":"https://lichess.org/broadcast/2024-australian-university-chess-league/round-1/yAulT9F2"}},{"tour":{"id":"GboZ8j0F","name":"IV Open Internacional Vila del Vendrell","slug":"iv-open-internacional-vila-del-vendrell","info":{"format":"9-round Swiss","tc":"Rapid"},"createdAt":1719974387311,"url":"https://lichess.org/broadcast/iv-open-internacional-vila-del-vendrell/GboZ8j0F","tier":3,"dates":[1720855800000,1720889100000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=iacaster:relay:GboZ8j0F:BSyQN0Ff.jpg&w=800&sig=a39542ad349425ce3be423a1b5a1d15ef326e1f9","markup":"

    The IV Open Internacional Vila del Vendrell is a 9-round Swiss, held on 13th of July in El Vendrell, Tarragona, Catalonia, Spain.

    \n

    Time control is 15 minutes for the entire game with a 5-second increment from move 1.

    \n

    Official Website | Results

    \n"},"round":{"id":"LBvFee4s","name":"Round 1","slug":"round-1","createdAt":1719974485751,"startsAt":1720855800000,"url":"https://lichess.org/broadcast/iv-open-internacional-vila-del-vendrell/round-1/LBvFee4s"}},{"tour":{"id":"Es02AbFN","name":"Swiss Individual Championships 2024 | Open","slug":"swiss-individual-championships-2024--open","info":{"format":"10-player round-robin","tc":"Classical"},"createdAt":1717543242414,"url":"https://lichess.org/broadcast/swiss-individual-championships-2024--open/Es02AbFN","tier":3,"dates":[1720871100000,1721546100000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=iacaster:relay:Es02AbFN:Yz0Im12b.jpg&w=800&sig=8e0f71606055775d5ea2006c6f09f2851872823a","markup":"

    The Swiss Individual Championships 2024 | Open is a 10-player round-robin, held from the 13th to the 21st of July in Flims, Switzerland.

    \n

    Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

    \n

    Official Website | Results

    \n"},"round":{"id":"vhvpniLr","name":"Round 1","slug":"round-1","createdAt":1717543458521,"startsAt":1720871100000,"url":"https://lichess.org/broadcast/swiss-individual-championships-2024--open/round-1/vhvpniLr"},"group":"Swiss Individual Championships 2024"},{"tour":{"id":"op7Dy2aB","name":"Zadar Chess Festival 2024","slug":"zadar-chess-festival-2024","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1720373669999,"url":"https://lichess.org/broadcast/zadar-chess-festival-2024/op7Dy2aB","tier":3,"dates":[1721142000000,1721721600000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=coach_goran:relay:op7Dy2aB:SVKZikJd.jpg&w=800&sig=70198997a1e6ce8a8f9ecdce566d2c86cf1bcc2b","markup":"

    The Zadar Chess Festival 2024 Tournament-A is a 9-round Swiss, held from the 16th to the 23rd of July in Zadar, Croatia.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Standings

    \n
    \n

    The Zadar Chess Festival brings chess players from all continents to Zadar. The tournament is divided into two categories, Tournament A and Tournament B. Tournament A is a grandmaster-level event where players compete for master norms, while Tournament B is for amateurs. Attendance is expected to be on par with last year when around 300 players participated. As part of the festival, a blitz championship will also be held, which this year coincides with World Chess Day.

    \n

    Zadar is a beautiful and extremely popular seaside destination visited by numerous tourists from all over the world. Besides its unique beauty, Zadar is a place of culture and heritage, fantastic gastronomy, a well-known sports city, and a city where everyone can find everything for a perfect summer vacation.

    \n

    Zadar Tourist Board

    \n"},"round":{"id":"Lx0mdzh1","name":"Round 1","slug":"round-1","createdAt":1720437555322,"startsAt":1721142000000,"url":"https://lichess.org/broadcast/zadar-chess-festival-2024/round-1/Lx0mdzh1"}},{"tour":{"id":"ilou0biG","name":"3rd Father Cup | Masters","slug":"3rd-father-cup--masters","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1717535598646,"url":"https://lichess.org/broadcast/3rd-father-cup--masters/ilou0biG","tier":3,"dates":[1722169800000,1722666600000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=ali9fazeli:relay:ilou0biG:ni99wVnt.jpg&w=800&sig=795312938ba49ed18f08120aee2efa1eed924764","markup":"

    The 3rd Father Cup | Masters is a 9-round Swiss, held from 28th of July to 3th of August in Rasht, Iran.

    \n

    Time control is 90 minutes for 40 moves, followed by 15 minutes for the rest of the game, with a 30-second increment from move 1.

    \n

    Results

    \n"},"round":{"id":"jcUBMfjn","name":"Round 1","slug":"round-1","createdAt":1717535725866,"startsAt":1722169800000,"url":"https://lichess.org/broadcast/3rd-father-cup--masters/round-1/jcUBMfjn"},"group":"3rd Father Cup"},{"tour":{"id":"nMDYY8rH","name":"28th Créon International 2024 | Main","slug":"28th-creon-international-2024--main","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1720108038852,"url":"https://lichess.org/broadcast/28th-creon-international-2024--main/nMDYY8rH","tier":3,"dates":[1722254400000,1722754800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:nMDYY8rH:kljC7xWa.jpg&w=800&sig=b06f8d53b209c0e4eadd232c24700ced2535b02f","markup":"

    The 28th Créon International 2024 | Main is a 9-round Swiss, held from the 29th of July to the 4th of August in Créon, France.

    \n

    Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

    \n

    Official Website | Standings

    \n"},"round":{"id":"4ONAFGnx","name":"Round 1","slug":"round-1","createdAt":1720108110532,"startsAt":1722254400000,"url":"https://lichess.org/broadcast/28th-creon-international-2024--main/round-1/4ONAFGnx"},"group":"28th Créon International 2024"}],"past":{"currentPage":1,"maxPerPage":20,"currentPageResults":[{"tour":{"id":"7GLYGExC","name":"7th Başkent University Open 2024 | Category A","slug":"7th-baskent-university-open-2024--category-a","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1719997546699,"url":"https://lichess.org/broadcast/7th-baskent-university-open-2024--category-a/7GLYGExC","tier":3,"dates":[1720011600000,1720422000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=okanbilir7:relay:7GLYGExC:XYAyM4VC.jpg&w=800&sig=31c9372a36c05be914612c115c355584617bef62","markup":"

    The 7th Başkent University Open 2024 is a 9-round Swiss, held from the 3rd to the 8th of July in Ankara, Türkiye.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website | Standings

    \n"},"round":{"id":"KYyH44IQ","name":"Round 9","slug":"round-9","createdAt":1720012037962,"finished":true,"startsAt":1720422000000,"url":"https://lichess.org/broadcast/7th-baskent-university-open-2024--category-a/round-9/KYyH44IQ"},"group":"7th Başkent University Open 2024"},{"tour":{"id":"Zpm2BkR3","name":"1000GM Independence Day GM Norm 2024","slug":"1000gm-independence-day-gm-norm-2024","info":{"format":"10-player round-robin","tc":"Classical"},"createdAt":1719922866329,"url":"https://lichess.org/broadcast/1000gm-independence-day-gm-norm-2024/Zpm2BkR3","tier":3,"dates":[1720052100000,1720397700000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:Zpm2BkR3:qTsJhBme.jpg&w=800&sig=d291f09ca0d17c2d5412ccf5f99351451dd433d9","markup":"

    The 1000GM Independence Day GM Norm 2024 is a 10-player round-robin tournament, held from the 3rd to the 7th of July in San Jose, California, USA.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website

    \n","leaderboard":true},"round":{"id":"SlAoLwYT","name":"Round 9","slug":"round-9","createdAt":1719923007464,"finished":true,"startsAt":1720397700000,"url":"https://lichess.org/broadcast/1000gm-independence-day-gm-norm-2024/round-9/SlAoLwYT"}},{"tour":{"id":"eRDPod9B","name":"Marshall Monthly FIDE Premier 2024 | July","slug":"marshall-monthly-fide-premier-2024--july","info":{"format":"5-round Swiss","tc":"Classical"},"createdAt":1720121901223,"url":"https://lichess.org/broadcast/marshall-monthly-fide-premier-2024--july/eRDPod9B","tier":3,"dates":[1720221300000,1720388700000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=sergioglorias:relay:eRDPod9B:ABp4RLul.jpg&w=800&sig=976ad750500d05b752225cb73df7ff01775b90ac","markup":"

    The Marshall Chess Club FIDE Premier July 2024 is a 5-round Swiss, held from the 5th to the 7th of July in New York City, USA.

    \n

    Time control is 90 minutes for the entire game, with a 30-second increment from move 1.

    \n

    Official Website

    \n"},"round":{"id":"v2n1zP96","name":"Round 5","slug":"round-5","createdAt":1720122277391,"finished":true,"startsAt":1720388700000,"url":"https://lichess.org/broadcast/marshall-monthly-fide-premier-2024--july/round-5/v2n1zP96"},"group":"Marshall Monthly FIDE Premier 2024"},{"tour":{"id":"fLqpKaC4","name":"CCA World Open 2024","slug":"cca-world-open-2024","info":{"format":"9-round Swiss","tc":"Classical","players":"Liang, Durarbayli, McShane, Yoo"},"createdAt":1719970723789,"url":"https://lichess.org/broadcast/cca-world-open-2024/fLqpKaC4","tier":4,"dates":[1720048020000,1720386420000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=iacaster:relay:fLqpKaC4:4FYyYcEx.jpg&w=800&sig=883c18f09e16639e631b07c69af344e5a83f1ba1","markup":"

    The CCA World Open 2024 is a 9-round Swiss, held from the 3rd to the 7th of July in Philadelphia, Pennsylvania, USA.

    \n

    Time control is 40 moves in 90 minutes, then 30 minutes, with a 30 second delay after every move.

    \n

    Official Website | Results

    \n
    \n

    Title image photo by Paul Frendach

    \n"},"round":{"id":"uYzumLEp","name":"Round 9","slug":"round-9","createdAt":1719971455782,"finished":true,"startsAt":1720386420000,"url":"https://lichess.org/broadcast/cca-world-open-2024/round-9/uYzumLEp"}},{"tour":{"id":"7t6naO2X","name":"2nd Annual Independence Day Open","slug":"2nd-annual-independence-day-open","info":{"format":"5-round Swiss","tc":"Classical"},"createdAt":1720201395792,"url":"https://lichess.org/broadcast/2nd-annual-independence-day-open/7t6naO2X","tier":3,"dates":[1720221300000,1720379700000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:7t6naO2X:4N8EH5wl.jpg&w=800&sig=d51590b24b9fb8ac5fe204457b54284bc43bcb0d","markup":"

    The 2nd Annual Independence Day Open is a 5-round Swiss, held from the 5th to the 7th of July in Dulles, Virginia, USA.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website

    \n"},"round":{"id":"UNMyETL4","name":"Round 5","slug":"round-5","createdAt":1720201460857,"finished":true,"startsAt":1720379700000,"url":"https://lichess.org/broadcast/2nd-annual-independence-day-open/round-5/UNMyETL4"}},{"tour":{"id":"9Uablwir","name":"1000GM Summer Dual Scheveningen 2024 #3 | Group A","slug":"1000gm-summer-dual-scheveningen-2024-3--group-a","info":{"format":"10-player Semi-Scheveningen","tc":"Classical"},"createdAt":1720124060927,"url":"https://lichess.org/broadcast/1000gm-summer-dual-scheveningen-2024-3--group-a/9Uablwir","tier":3,"dates":[1720199700000,1720372500000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=sergioglorias:relay:9Uablwir:xdjJ2iwL.jpg&w=800&sig=f08151608f83e9a7738e07bd23f7fe05f1db06e4","markup":"

    The 1000GM Summer Dual Scheveningen 2024 #3 | Group A is a 10-player Semi-Scheveningen, held from the 5th to the 7th of July in San Jose, California, USA.

    \n

    Time control is 90 minutes for the entire game, with a 30-second increment from move 1.

    \n

    Official Website

    \n"},"round":{"id":"JxDiKnHY","name":"Round 5","slug":"round-5","createdAt":1720124654816,"finished":true,"startsAt":1720372500000,"url":"https://lichess.org/broadcast/1000gm-summer-dual-scheveningen-2024-3--group-a/round-5/JxDiKnHY"},"group":"1000GM Summer Dual Scheveningen 2024 #3"},{"tour":{"id":"aec1RGgy","name":"Schack-SM 2024 | Sverigemästarklassen","slug":"schack-sm-2024--sverigemastarklassen","info":{"format":"10-player round-robin","tc":"Classical"},"createdAt":1719330577301,"url":"https://lichess.org/broadcast/schack-sm-2024--sverigemastarklassen/aec1RGgy","tier":4,"dates":[1719666000000,1720342800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=claes1981:relay:aec1RGgy:2VfDqH1O.jpg&w=800&sig=dd784c694cd31ea8df2dd4deab2a331379f03e2d","markup":"

    The Swedish Championship week takes place from June 28th to July 7th in Fortnox Arena, Växjö, Sweden. The event includes several sections, which are linked at the bottom.

    \n

    SE

    \n

    Officiell webbplats | Video-kommentering | Resultat och lottning | Livechess PGN

    \n

    Betänketid Sverigemästarklassen: 90 minuter för 40 drag, plus 30 minuter för resten av partiet, plus 30 sekunder per drag från drag ett.

    \n

    Sverigemästarklassen | Mästarklassen-elit | Junior-SM | Mästarklassen | Veteran-SM 50+ | Veteran-SM 65+ | Weekendturneringen I | Klass I-IV | SM-blixten | SM 2023

    \n

    EN

    \n

    Official Website | Video commentary | Results and Pairings | Livechess PGN

    \n

    Time control Swedish Champion Class: 90 minutes for 40 moves, plus 30 minutes for the rest of the game, plus 30 seconds per move from move one.

    \n

    Swedish Champion Class | Elite Masterclass | Swedish Junior Championship | Masterclass | Swedish Senior Championship 50+ | Swedish Senior Championship 65+ | The Weekend Tournament I | Class I-IV | The SM Blitz | 2023

    \n"},"round":{"id":"sJ5sZRMs","name":"Rond 9","slug":"rond-9","createdAt":1719331504689,"finished":true,"startsAt":1720342800000,"url":"https://lichess.org/broadcast/schack-sm-2024--sverigemastarklassen/rond-9/sJ5sZRMs"},"group":"Schack-SM 2024"},{"tour":{"id":"2XEWNHQG","name":"Baku Open 2024 | Group A","slug":"baku-open-2024--group-a","info":{"format":"9-round Swiss","tc":"Classical","players":"Narayanan, Mamedov, Pranav"},"createdAt":1719363025661,"url":"https://lichess.org/broadcast/baku-open-2024--group-a/2XEWNHQG","tier":4,"dates":[1719659700000,1720347300000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:2XEWNHQG:dVkQzLbt.jpg&w=800&sig=d6c75dc5ce69c7d0641e9a5634c658683696b2a5","markup":"

    The Baku Open 2024 is a 9-round Swiss, held from the 29th of June to the 7th of July in Baku, Azerbaijan.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website | Results

    \n
    \n

    Title image photo by Dario Daniel Silva on Unsplash

    \n"},"round":{"id":"TOAPN9Bi","name":"Round 9","slug":"round-9","createdAt":1719363202831,"finished":true,"startsAt":1720347300000,"url":"https://lichess.org/broadcast/baku-open-2024--group-a/round-9/TOAPN9Bi"},"group":"Baku Open 2024"},{"tour":{"id":"Kont9lyt","name":"Spanish U10 Rapid Championship 2024","slug":"spanish-u10-rapid-championship-2024","info":{"format":"9-round Swiss","tc":"Rapid"},"createdAt":1719478161692,"url":"https://lichess.org/broadcast/spanish-u10-rapid-championship-2024/Kont9lyt","tier":3,"dates":[1720278000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=josefeda:relay:Kont9lyt:e7kU6oM9.jpg&w=800&sig=eafa6855e06b5bb948e815534d4212d20f7aa010","markup":"

    The Spanish U10 Rapid Championship 2024 is a 9-round Swiss, held from the 6th to the 7th of July in Salobreña, Granada, Spain.

    \n

    Time control is 10 minutes for the entire game with a 5-second increment from move 1.

    \n

    Official Website | Standings

    \n
    \n

    Campeonato de España Rapido Sub 10 2024

    \n"},"round":{"id":"tIeqLJf0","name":"Ronda 9","slug":"ronda-9","createdAt":1719478541343,"finished":true,"startsAt":1720351800000,"url":"https://lichess.org/broadcast/spanish-u10-rapid-championship-2024/ronda-9/tIeqLJf0"}},{"tour":{"id":"lunItMBB","name":"Saxony-Anhalt Seniors Championships 2024 | 50+","slug":"saxony-anhalt-seniors-championships-2024--50","info":{"format":"7-round Swiss","tc":"Classical"},"createdAt":1719827879431,"url":"https://lichess.org/broadcast/saxony-anhalt-seniors-championships-2024--50/lunItMBB","tier":3,"dates":[1719839700000,1720340100000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:lunItMBB:M2ARPeSq.jpg&w=800&sig=6230cf839867f551e78c70673e839a03162a541b","markup":"

    The Saxony-Anhalt Seniors Championships 2024 | 50+ is a 7-round Swiss, held from the 1st to the 7th of July in Magdeburg, Germany.

    \n

    Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

    \n

    Official Website

    \n"},"round":{"id":"Hh4EwihK","name":"Round 7","slug":"round-7","createdAt":1719827954310,"finished":true,"startsAt":1720340100000,"url":"https://lichess.org/broadcast/saxony-anhalt-seniors-championships-2024--50/round-7/Hh4EwihK"},"group":"Saxony-Anhalt Seniors Championships 2024"},{"tour":{"id":"47N9XRWe","name":"České Budějovice Chess Festival 2024 | GM A","slug":"ceske-budejovice-chess-festival-2024--gm-a","info":{"format":"10-player round-robin","tc":"Classical"},"createdAt":1719327405409,"url":"https://lichess.org/broadcast/ceske-budejovice-chess-festival-2024--gm-a/47N9XRWe","tier":3,"dates":[1719669600000,1720339200000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:47N9XRWe:aOKaYfNt.jpg&w=800&sig=85338600beb8ded4a6165adec640570c8112dd42","markup":"

    The České Budějovice Chess Festival 2024 | GM A is a 10-player round-robin tournament, held from the 29th of June to the 7th of July in České Budějovice, Czech Republic.

    \n

    Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

    \n

    Offiical Website | Standings

    \n
    \n

    Title image photo by Hans Lemuet (Spone), CC BY-SA 3.0, via Wikimedia Commons

    \n","leaderboard":true},"round":{"id":"CjE6k07C","name":"Round 9","slug":"round-9","createdAt":1719327648068,"finished":true,"startsAt":1720339200000,"url":"https://lichess.org/broadcast/ceske-budejovice-chess-festival-2024--gm-a/round-9/CjE6k07C"},"group":"České Budějovice Chess Festival 2024"},{"tour":{"id":"5143V4eE","name":"XXIV Open Internacional d'Escacs de Torredembarra","slug":"xxiv-open-internacional-descacs-de-torredembarra","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1719588450645,"url":"https://lichess.org/broadcast/xxiv-open-internacional-descacs-de-torredembarra/5143V4eE","tier":3,"dates":[1719671400000,1720335600000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=ukkina:relay:5143V4eE:R0iNiocy.jpg&w=800&sig=163d30e58a300f59c5255f3e765e709fe5ccf8c7","markup":"

    The XXIV Open Internacional d'Escacs de Torredembarra is a 9-round Swiss, held from the 29th of June to the 7th of July in
    Torredembarra, Spain.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website | Standings

    \n
    \n

    Del 29 de juny al 7 de juliol de 2024
    ORGANITZA: CLUB D’ESCACS TORREDEMBARRA
    (Integrat al XX Circuit Català d’Oberts Internacionals d’Escacs, classificat amb categoria B, b. (http://www.escacs.cat).

    \n"},"round":{"id":"1x9bhyjf","name":"Round 9","slug":"round-9","createdAt":1719761787694,"finished":true,"startsAt":1720335600000,"url":"https://lichess.org/broadcast/xxiv-open-internacional-descacs-de-torredembarra/round-9/1x9bhyjf"}},{"tour":{"id":"HeOoTDru","name":"All-Ukrainian Festival Morshyn 2024","slug":"all-ukrainian-festival-morshyn-2024","info":{"format":"9-round Swiss","tc":"Rapid"},"createdAt":1720267972743,"url":"https://lichess.org/broadcast/all-ukrainian-festival-morshyn-2024/HeOoTDru","tier":3,"dates":[1720252800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:HeOoTDru:TAcEY8XI.jpg&w=800&sig=729d010e7afad48d86fa086f6176102734f901f5","markup":"

    The All-Ukrainian Festival Morshyn 2024 is a 9-round Swiss, held on the 6th of July in Morshyn, Ukraine.

    \n

    Time control is 10 minutes for the entire game with a 5-second increment from move 1.

    \n

    Standings

    \n
    \n

    Title image photo by ЯдвигаВереск - Own work, CC BY-SA 4.0

    \n"},"round":{"id":"6YRSXzDZ","name":"Round 9","slug":"round-9","createdAt":1720268064062,"finished":true,"startsAt":1720276200000,"url":"https://lichess.org/broadcast/all-ukrainian-festival-morshyn-2024/round-9/6YRSXzDZ"}},{"tour":{"id":"Db0i9sGV","name":"Spanish U10 Championship 2024","slug":"spanish-u10-championship-2024","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1719401439623,"url":"https://lichess.org/broadcast/spanish-u10-championship-2024/Db0i9sGV","tier":3,"dates":[1719820800000,1720252800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=josefeda:relay:Db0i9sGV:TyPuOKoC.jpg&w=800&sig=67728c0d69503809c4f6a137ff382f56ed8d8af7","markup":"

    The Spanish U10 Championship 2024 is a 9-round Swiss, held from the 1st to the 6th of July in Salobreña, Granada, Spain.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website | Standings

    \n
    \n

    Campeonato de España Sub 10 2024

    \n"},"round":{"id":"KYK9G7kE","name":"Ronda 9","slug":"ronda-9","createdAt":1719401772691,"finished":true,"startsAt":1720252800000,"url":"https://lichess.org/broadcast/spanish-u10-championship-2024/ronda-9/KYK9G7kE"}},{"tour":{"id":"BjKO6Jrs","name":"Italian U18 Youth Championships 2024 | U18","slug":"italian-u18-youth-championships-2024--u18","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1719575583240,"url":"https://lichess.org/broadcast/italian-u18-youth-championships-2024--u18/BjKO6Jrs","tier":3,"dates":[1719666900000,1720251900000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:BjKO6Jrs:ztfSSOiP.jpg&w=800&sig=f640d262f4c1545eb47cf716766179385ae51b6e","markup":"

    The Italian U18 Youth Championships 2024 | U18 is a 9-round Swiss, held from the 29th of June to the 6th of July in Salsomaggiore Terme, Italy.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website | Results

    \n"},"round":{"id":"OCMHlRDH","name":"Round 9","slug":"round-9","createdAt":1719575679992,"finished":true,"startsAt":1720251900000,"url":"https://lichess.org/broadcast/italian-u18-youth-championships-2024--u18/round-9/OCMHlRDH"},"group":"Italian U18 Youth Championships 2024"},{"tour":{"id":"hdQQ1Waq","name":"Norwegian Championships 2024 | Elite and Seniors 65+","slug":"norwegian-championships-2024--elite-and-seniors-65","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1719174080786,"url":"https://lichess.org/broadcast/norwegian-championships-2024--elite-and-seniors-65/hdQQ1Waq","tier":4,"dates":[1719591300000,1720253700000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:hdQQ1Waq:K1YsAziL.jpg&w=800&sig=1921f0a71b78530c0a6e8b0e564e19af8b91c499","markup":"

    The Norwegian Championships 2024 | Elite and Seniors 65+ is a 9-round Swiss, held from the 28th of June to the 6th of July in Storefjell, Norway.

    \n

    Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

    \n

    Board 1-9 Elite
    Board 10 - 28 Senior 65+

    \n

    Official Website | Results

    \n
    \n

    Landsturneringen 2024

    \n

    Eliteklassen og Senior 65+

    \n

    Spilles på Storefjell resort hotell 28.06.2024 - 06.07.2024

    \n

    Turneringen spilles over 9 runder, med betenkningstid 90 min på 40 trekk, 30 min på resten av partiet og 30 sek tillegg fra trekk 1

    \n

    Bord 1-9 Eliteklassen
    Bord 10 - 28 Senior 65+

    \n

    Clono partier:
    Mikroputt
    \nhttps://lichess.org/broadcast/nm-i-sjakk-2024-mikroputt/round-1/020oDPUm#boards
    Miniputt
    https://lichess.org/broadcast/nm-i-sjakk-2024-miniputt/round-1/pCvV4G8i#boards
    Lilleputt
    https://lichess.org/broadcast/nm-i-sjakk-2024-lilleputt/round-1/k8GS6LrP
    Junior B
    https://lichess.org/broadcast/nm-i-sjakk-junior-b/round-1/AZhM1hMm
    Klasse 1
    https://lichess.org/broadcast/nm-i-sjakk-2024-klasse-1/round-1/aWw2RwQ1
    Klasse 2
    https://lichess.org/broadcast/nm-i-sjakk-2024-klasse-2/round-1/Mnxw76OR
    Klasse 3
    https://lichess.org/broadcast/nmi-sjakk-2024-klasse-3/round-1/ZheSrANG
    Klasse 4
    https://lichess.org/broadcast/nm-i-sjakk-klasse-4/round-1/X673vUlD
    Klasse 5
    https://lichess.org/broadcast/nm-i-sjakk-2024-klasse-5/round-1/C6m3qitn
    Klasse Mester
    https://lichess.org/broadcast/nm-i-sjakk-2024-mesterklassen/round-2/lZu3t3A7#boards

    \n"},"round":{"id":"LQn45rIa","name":"Round 9","slug":"round-9","createdAt":1719175255813,"finished":true,"startsAt":1720253700000,"url":"https://lichess.org/broadcast/norwegian-championships-2024--elite-and-seniors-65/round-9/LQn45rIa"},"group":"Norwegian Championships 2024"},{"tour":{"id":"K1NfeoWE","name":"Superbet Romania Chess Classic 2024","slug":"superbet-romania-chess-classic-2024","info":{"format":"10-player Round Robin","tc":"Classical","players":"Caruana, Nepomniachtchi, Gukesh, Giri"},"createdAt":1719187354944,"url":"https://lichess.org/broadcast/superbet-romania-chess-classic-2024/K1NfeoWE","tier":5,"dates":[1719405000000,1720198800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=iacaster:relay:K1NfeoWE:6kBI06CJ.jpg&w=800&sig=c6b93e8db217a6bb504dcb5e0695337a472655b7","markup":"

    The Superbet Romania Chess Classic 2024 is a 10-player Round Robin, held from the 26th of June to the 5th of July in Bucharest, Romania.

    \n

    Time control is 120 minutes for the entire game, plus a 30-second increment per move.

    \n

    Superbet Chess Classic Romania is the first of two classical events, this tournament will feature a 10-player round robin with nine tour regulars, Caruana, Nepomniachtchi, Abdusattorov, Gukesh, So, Praggnanandhaa, Giri, Firouzja, Vachier-Lagrave, and one wildcard, local Romanian favorite Bogdan-Daniel Deac.

    \n

    Official Website | Results

    \n
    \n

    In the event of a tie for 1st place, a double round-robin will be played with 2 players, or a single round-robin will be played with 3 or more players. Time control is 10 minutes for the entire game with a 5-second increment from move 1.

    \n

    In the event of another tie, knockout armageddon games will be played. Time control is 5 minutes for White, 4 minutes for Black, with a 2-second increment from move 61.

    \n
    \n

    Grand Chess Tour | Tour Standings
    2024 Superbet Poland Rapid & Blitz
    2024 Superbet Romania Chess Classic
    2024 Superunited Croatia Rapid & Blitz

    \n
    \n

    Title image photo by Arvid Olson from Pixabay

    \n","leaderboard":true},"round":{"id":"QC9QC8Lr","name":"Tiebreaks","slug":"tiebreaks","createdAt":1720197015416,"finished":true,"startsAt":1720198800000,"url":"https://lichess.org/broadcast/superbet-romania-chess-classic-2024/tiebreaks/QC9QC8Lr"}},{"tour":{"id":"ZmFLmrss","name":"III Magistral Internacional Ciudad de Sant Joan de Alacant","slug":"iii-magistral-internacional-ciudad-de-sant-joan-de-alacant","info":{"format":"10-player round-robin","tc":"Classical"},"createdAt":1719791889764,"url":"https://lichess.org/broadcast/iii-magistral-internacional-ciudad-de-sant-joan-de-alacant/ZmFLmrss","tier":3,"dates":[1719820800000,1720162800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:ZmFLmrss:2RKhpn3T.jpg&w=800&sig=8be9210de5ef07975dbfb9d300fffc3ad1e38ecc","markup":"

    The III Magistral Internacional Ciudad de Sant Joan de Alacant is a 10-player round-robin tournament, held from the 1st to the 5th of July in Sant Joan d'Alacant, Spain.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website | Standings

    \n","leaderboard":true},"round":{"id":"MIy50UWQ","name":"Round 9","slug":"round-9","createdAt":1719791991550,"finished":true,"startsAt":1720162800000,"url":"https://lichess.org/broadcast/iii-magistral-internacional-ciudad-de-sant-joan-de-alacant/round-9/MIy50UWQ"}},{"tour":{"id":"fQu6hjlI","name":"1000GM Summer Dual Scheveningen 2024 #2 | Group A","slug":"1000gm-summer-dual-scheveningen-2024-2--group-a","info":{"format":"10-player Semi-Scheveningen","tc":"Classical"},"createdAt":1719922442023,"url":"https://lichess.org/broadcast/1000gm-summer-dual-scheveningen-2024-2--group-a/fQu6hjlI","tier":3,"dates":[1719940500000,1720113300000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:fQu6hjlI:mVZ0X3CV.jpg&w=800&sig=1d2b55c6d0cac0ceda72a4fbc83e837a45006b9e","markup":"

    The 1000GM Summer Dual Scheveningen 2024 #2 | Group A is a 10-player Semi-Scheveningen, held from the 2nd to the 4th of July in San Jose, California, USA.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website

    \n","leaderboard":true},"round":{"id":"D5IvvZGj","name":"Round 5","slug":"round-5","createdAt":1719922517399,"finished":true,"startsAt":1720113300000,"url":"https://lichess.org/broadcast/1000gm-summer-dual-scheveningen-2024-2--group-a/round-5/D5IvvZGj"},"group":"1000GM Summer Dual Scheveningen 2024 #2"},{"tour":{"id":"4ERHDodE","name":"Atlantic Chess Independence Day GM Norm Invitational","slug":"atlantic-chess-independence-day-gm-norm-invitational","info":{"format":"10-player round-robin","tc":"Classical","players":"Erenburg, Plát, Barbosa, Gauri"},"createdAt":1719693938025,"url":"https://lichess.org/broadcast/atlantic-chess-independence-day-gm-norm-invitational/4ERHDodE","tier":3,"dates":[1719695700000,1720098900000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:4ERHDodE:l3iVV7Ym.jpg&w=800&sig=d57277f927849426413b3b26fccacbad1027ae51","markup":"

    The Atlantic Chess Independence Day GM Norm Invitational is a 10-player round-robin tournament, held from the 29th of June to the 4th of July in Dulles, Virginia, USA.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website | Standings

    \n
    \n

    The Atlantic Chess Association is organizing the Independence Day Norm Tournament. It is a 6 day, 9 rounds, 10 player Round Robin tournament.

    \n

    Chief Arbiter: IA Gregory Vaserstein

    \n

    Venue: Hampton Inn & Suites Washington-Dulles International Airport (4050 Westfax Dr., Chantilly, VA 20151)

    \n","leaderboard":true},"round":{"id":"PVG8wijk","name":"Round 9","slug":"round-9","createdAt":1719698677225,"finished":true,"startsAt":1720098900000,"url":"https://lichess.org/broadcast/atlantic-chess-independence-day-gm-norm-invitational/round-9/PVG8wijk"}}],"previousPage":null,"nextPage":2}} +{"active":[{"tour":{"id":"ioLNPN8j","name":"2nd Rustam Kasimdzhanov Cup 2024","slug":"2nd-rustam-kasimdzhanov-cup-2024","info":{"format":"10-player round-robin","tc":"Rapid","players":"Abdusattorov, Rapport, Mamedyarov, Grischuk"},"createdAt":1720352380417,"url":"https://lichess.org/broadcast/2nd-rustam-kasimdzhanov-cup-2024/ioLNPN8j","tier":5,"dates":[1720431600000,1720519800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=uzchess23:relay:ioLNPN8j:ya35G192.jpg&w=800&sig=b6543625b806cf43f4ee652d0a23d80fad236b35","markup":"

    The 2nd International Rustam Kasimdzhanov Cup 2024 is a 10-player round-robin tournament, held from the 8th to the 9th of July in Tashkent, Uzbekistan.

    \n

    Time control is 15 minutes for the entire game with a 10-second increment from move 1.

    \n

    Standings

    \n","leaderboard":true},"round":{"id":"A4J7qTO6","name":"Round 8","slug":"round-8","createdAt":1720438046707,"ongoing":true,"startsAt":1720516500000,"url":"https://lichess.org/broadcast/2nd-rustam-kasimdzhanov-cup-2024/round-8/A4J7qTO6"}},{"tour":{"id":"a4gBsu31","name":"Dutch Championship 2024 | Open","slug":"dutch-championship-2024--open","info":{"format":"10-player knockout","tc":"Classical","players":"Warmerdam, l’Ami, Bok, Sokolov"},"createdAt":1720037021926,"url":"https://lichess.org/broadcast/dutch-championship-2024--open/a4gBsu31","tier":4,"dates":[1720263600000,1720882800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:a4gBsu31:OgaRY7Pw.jpg&w=800&sig=cb141524b135d0cdc45deafcb9b4bfffc805ecee","markup":"

    The Dutch Championship 2024 | Open is a 10-player single-elimination knockout tournament, held from the 6th to the 13th of July in Utrecht, the Netherlands.

    \n

    Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

    \n

    Official Website | Results

    \n
    \n

    If a round ends in a tie after 2 classical games, a tiebreak match of 2 blitz games is played. Time control is 4+2.

    \n

    If the first tiebreak match ends in another tie, a second tiebreak match of 2 blitz games with reversed colours is played. Time control is 4+2.

    \n

    If the second tiebreak match ends in another tie, colours are drawn and a sudden death is played. Time control is 4+2 for White and 5+2 for Black. The first player to win a game, wins the round. After every 2 games, the colour order is changed.

    \n"},"round":{"id":"Xfe00Awr","name":"Quarter-Finals | Game 2","slug":"quarter-finals--game-2","createdAt":1720037148839,"ongoing":true,"startsAt":1720522800000,"url":"https://lichess.org/broadcast/dutch-championship-2024--open/quarter-finals--game-2/Xfe00Awr"},"group":"Dutch Championship 2024"},{"tour":{"id":"aPC3ATVG","name":"FIDE World Senior Team Chess Championships 2024 | 50+","slug":"fide-world-senior-team-chess-championships-2024--50","info":{"format":"9-round Swiss for teams","tc":"Classical","players":"Adams, Ehlvest, David, Novikov"},"createdAt":1719921457211,"url":"https://lichess.org/broadcast/fide-world-senior-team-chess-championships-2024--50/aPC3ATVG","tier":4,"dates":[1719926100000,1720685700000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=mansuba64:relay:aPC3ATVG:xuiZWY67.jpg&w=800&sig=bfc2aa87dce4ed7bdfb5ce5b9f16285e23479f05","markup":"

    The FIDE World Senior Team Chess Championships 2024 | 50+ is a 9-round Swiss for teams, held from the 2nd to the 11th of July in Kraków, Poland.

    \n

    Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

    \n

    Official Website | Standings

    \n
    \n

    There shall be two categories; Open age 50+ and Open age 65+ with separate events for women.
    The player must have reached or reach the required age during the year of competition.
    There shall be separate Women’s Championship(s) if there are at least ten teams from at least two continents. Otherwise women’s teams play in Open competition
    The Championships are open tournaments for teams registered by their federation. FIDE member federations shall have the right to send as many teams as they wish.

    \n

    The winning team obtains the title “World Team Champion “age 50+ (or age 65+)”.
    The best placed women team obtains the title “World Women Team Champion” age 50+ (or age 65+).

    \n

    Prize Fund: 10,000 EUR

    \n","teamTable":true},"round":{"id":"YIw910wS","name":"Round 7","slug":"round-7","createdAt":1719928673349,"startsAt":1720530900000,"url":"https://lichess.org/broadcast/fide-world-senior-team-chess-championships-2024--50/round-6/Gue2qJfw"},"group":"FIDE World Senior Team Chess Championships 2024"},{"tour":{"id":"tCMfpIJI","name":"43rd Villa de Benasque Open 2024","slug":"43rd-villa-de-benasque-open-2024","info":{"format":"10-round Swiss","tc":"Classical","players":"Alekseenko, Bartel, Pichot"},"createdAt":1719422556116,"url":"https://lichess.org/broadcast/43rd-villa-de-benasque-open-2024/tCMfpIJI","tier":4,"dates":[1720189800000,1720941300000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=kike0:relay:tCMfpIJI:P6c1Rrxn.jpg&w=800&sig=69d4e6158f133578bbb35519346e4395d891ca2c","markup":"

    The 43rd Villa de Benasque Open 2024 is a 10-round Swiss, held from the 5th to the 14th of July in Benasque, Spain.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move one.

    \n

    GM Kirill Alekseenko is the tournament's top seed - with nearly 100 titled players, 500 players in total, and over €50,000 in prizes.

    \n

    Official Website | Standings

    \n
    \n

    El XLIII Open Internacional Villa de Benasque se disputará por el Sistema Suizo a 10 rondas, del 5 al 14 de Julio de 2024. El GM Alekseenko lidera un ranking con cerca de 100 titulados, 500 jugadores y más de 50.000 euros de premios en metálico. El local de juego será el Pabellón Polideportivo de Benasque (España).

    \n

    El ritmo de juego será de 90 minutos + 30 segundos de incremento acumulativo por jugada empezando desde la primera.

    \n

    Web Oficial | Chess-Results

    \n"},"round":{"id":"SXAjWw0G","name":"Round 5","slug":"round-5","createdAt":1719422658882,"startsAt":1720534500000,"url":"https://lichess.org/broadcast/43rd-villa-de-benasque-open-2024/round-4/she3bD2w"}},{"tour":{"id":"yOuW4siY","name":"Spanish U12 Championships 2024 | Classical","slug":"spanish-u12-championships-2024--classical","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1720081884293,"url":"https://lichess.org/broadcast/spanish-u12-championships-2024--classical/yOuW4siY","tier":3,"dates":[1720425600000,1720857600000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=josefeda:relay:yOuW4siY:FCsABLhH.jpg&w=800&sig=ca8faadb2725de80ab6a316e98b8505f1f620f71","markup":"

    The Spanish U12 Championship 2024 is a 9-round Swiss, held from the 6th to the 7th of July in Salobreña, Granada, Spain.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website | Standings

    \n
    \n

    Campeonato de España Sub 12 2024

    \n"},"round":{"id":"eCa2CbqM","name":"Ronda 3","slug":"ronda-3","createdAt":1720082094252,"ongoing":true,"startsAt":1720512000000,"url":"https://lichess.org/broadcast/spanish-u12-championships-2024--classical/ronda-3/eCa2CbqM"},"group":"Spanish U12 Championships 2024"},{"tour":{"id":"JQGYmn68","name":"Scottish Championship International Open 2024","slug":"scottish-championship-international-open-2024","info":{"format":"9-round Swiss","tc":"90+30"},"createdAt":1720440336101,"url":"https://lichess.org/broadcast/scottish-championship-international-open-2024/JQGYmn68","tier":3,"dates":[1720447200000,1720965600000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=prospect_d:relay:JQGYmn68:I58xFyHC.jpg&w=800&sig=ccd5889235ee538ce022dcc5ff8ed1568e5f4377","markup":"

    The Scottish Championship International Open 2024 is a 9-round Swiss, held from the 8th to the 14th of July in Dunfermline, Scotland.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Standings

    \n"},"round":{"id":"Nw190iGM","name":"Round 2","slug":"round-2","createdAt":1720451119128,"ongoing":true,"startsAt":1720515600000,"url":"https://lichess.org/broadcast/scottish-championship-international-open-2024/round-2/Nw190iGM"}},{"tour":{"id":"YBTYQbxm","name":"South Wales International Open 2024","slug":"south-wales-international-open-2024","info":{"format":"9-round Swiss","tc":"Classical","players":"Chatalbashev, Cuenca, Grieve, Han"},"createdAt":1720127613709,"url":"https://lichess.org/broadcast/south-wales-international-open-2024/YBTYQbxm","tier":3,"dates":[1720170000000,1720602000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:YBTYQbxm:BDfr290h.jpg&w=800&sig=2e914768c4d3781264493309d8e37c86221c8ac7","markup":"

    The South Wales International Open 2024 is a 9-round Swiss title norm tournament taking place in Bridgend, Wales from the 5th to the 10th of July.

    \n

    Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

    \n

    Official Website | Standings

    \n","leaderboard":true},"round":{"id":"Svyiq7jS","name":"Round 7","slug":"round-7","createdAt":1720127909656,"ongoing":true,"startsAt":1720515600000,"url":"https://lichess.org/broadcast/south-wales-international-open-2024/round-7/Svyiq7jS"}},{"tour":{"id":"BgVqV6b0","name":"Koege Open 2024","slug":"koege-open-2024","info":{"format":"10-player round-robin","tc":"Classical","players":"Petrov, Smith, Hector"},"createdAt":1720361492349,"url":"https://lichess.org/broadcast/koege-open-2024/BgVqV6b0","tier":3,"dates":[1720512300000,1720944300000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=fishdefend:relay:BgVqV6b0:kr4GaRvW.jpg&w=800&sig=b2f59c1ae2cb70a88c8e7872b1327b93c64cdad3","markup":"

    The Koege Open 2024 is a 10-player round-robin tournament, held from the 9th to the 14th of July in Køge, Denmark.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Standings

    \n
    \n

    Group 1: Boards 1-5
    Group 2: Boards 6-10

    \n"},"round":{"id":"y0ksveWZ","name":"Round 1","slug":"round-1","createdAt":1720460080111,"ongoing":true,"startsAt":1720512300000,"url":"https://lichess.org/broadcast/koege-open-2024/round-1/y0ksveWZ"}},{"tour":{"id":"XV3jpD1b","name":"Belgian Championship 2024 | Expert","slug":"belgian-championship-2024--expert","info":{"format":"10-player round-robin","tc":"Classical"},"createdAt":1720276555430,"url":"https://lichess.org/broadcast/belgian-championship-2024--expert/XV3jpD1b","tier":3,"dates":[1720267200000,1720944000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=sergioglorias:relay:XV3jpD1b:BkW8q64n.jpg&w=800&sig=017b9b9cf354268158da36eba185e961d2cfc5e3","markup":"

    The Belgian Championship 2024 | Expert is a 10-player round-robin tournament, held from the 6th to the 14th of July in Lier, Belgium.

    \n

    The winner will be crowned Belgian Chess Champion 2024.

    \n

    Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

    \n

    Official Website

    \n","leaderboard":true},"round":{"id":"iSD0HAuQ","name":"Round 4","slug":"round-4","createdAt":1720276601311,"ongoing":true,"startsAt":1720526400000,"url":"https://lichess.org/broadcast/belgian-championship-2024--expert/round-4/iSD0HAuQ"},"group":"Belgian Championship 2024"},{"tour":{"id":"oo69aO3w","name":"SAIF Powertec Bangladesh Championship 2024","slug":"saif-powertec-bangladesh-championship-2024","info":{"format":"14-player round-robin","tc":"Classical"},"createdAt":1719050225145,"url":"https://lichess.org/broadcast/saif-powertec-bangladesh-championship-2024/oo69aO3w","tier":3,"dates":[1719133200000,1720602000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=fathirahman:relay:oo69aO3w:o2ATDvXh.jpg&w=800&sig=f93c0ea42a7223efb8efdc4e245acca39962b9f3","markup":"

    The SAIF Powertec Bangladesh Championship 2024 is a 14-player round-robin tournament, held from the 23rd of June to the 6th of July in Dhaka, Bangladesh.

    \n

    Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

    \n

    Official Website | Results

    \n
    \n

    SAIF Powertec 48th Bangladesh National Chess Championship 2024 is Bangladesh's national chess championship. Top 5 players are:

    \n
      \n
    1. FM Manon, Reja Neer 2445
    2. \n
    3. IM Mohammad Fahad, Rahman 2437
    4. \n
    5. GM Rahman, Ziaur 2423
    6. \n
    7. GM Hossain, Enamul 2365
    8. \n
    9. GM Murshed, Niaz 2317
    10. \n
    \n

    The previous series (2022) champion is GM Enamul Hossain. This is a 14-player round-robin tournament, where 3 GMs have been invited to play directly, and 11 players are from the top 11 of the qualifying round, known as the National B Championship.

    \n

    Five GMs were invited, but only three accepted the invitation. Therefore, instead of taking 9 players from National B, 11 players qualified to fulfill the round requirements.

    \n

    The top 5 players qualify for the Olympiad team.

    \n

    Here are useful links:

    \n\n"},"round":{"id":"LLqfCDm6","name":"Round 12 (Postponed)","slug":"round-12-postponed","createdAt":1719050487028,"ongoing":true,"startsAt":1720515600000,"url":"https://lichess.org/broadcast/saif-powertec-bangladesh-championship-2024/round-12-postponed/LLqfCDm6"}},{"tour":{"id":"Qag4N0cA","name":"4th La Plagne Festival 2024","slug":"4th-la-plagne-festival-2024","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1720274920410,"url":"https://lichess.org/broadcast/4th-la-plagne-festival-2024/Qag4N0cA","tier":3,"dates":[1720278000000,1720771200000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:Qag4N0cA:v3g6RfVf.jpg&w=800&sig=78b9c7d33a747e3c20fc26fd5360d07c9546debc","markup":"

    The 4th La Plagne International Chess Festival is a 9-round Swiss, held from the 6th to the 12th of July at La Plagne in Savoie, France.

    \n

    Time control is is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

    \n

    Official Website | Standings

    \n"},"round":{"id":"0qufdZnF","name":"Round 5","slug":"round-5","createdAt":1720286940762,"startsAt":1720531800000,"url":"https://lichess.org/broadcast/4th-la-plagne-festival-2024/round-4/gQt8ubbC"}},{"tour":{"id":"95l4pho3","name":"Peruvian Championship Finals 2024 | Open","slug":"peruvian-championship-finals-2024--open","info":{"format":"12-player round-robin","tc":"Classical","players":"Terry, Flores Quillas, Leiva"},"createdAt":1720272022000,"url":"https://lichess.org/broadcast/peruvian-championship-finals-2024--open/95l4pho3","tier":3,"dates":[1720278000000,1720796400000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:95l4pho3:mDrHsa8C.jpg&w=800&sig=0d81273752b2bcdd47180ae23201f15074a91be9","markup":"

    The Peruvian Championship Finals 2024 | Open is a 12-player round-robin tournament, held from the 6th to the 12th of July in Lima, Peru.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Standings

    \n","leaderboard":true},"round":{"id":"Pi0HtFDs","name":"Round 6","slug":"round-6","createdAt":1720272103102,"startsAt":1720537200000,"url":"https://lichess.org/broadcast/peruvian-championship-finals-2024--open/round-5/JuIghW2d"},"group":"Peruvian Championship Finals 2024"},{"tour":{"id":"85buXS8z","name":"2024 Sydney Championships | Open","slug":"2024-sydney-championships--open","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1713001604469,"url":"https://lichess.org/broadcast/2024-sydney-championships--open/85buXS8z","tier":3,"dates":[1720226700000,1720570500000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=rootyhillcc:relay:85buXS8z:GSmVFAej.jpg&w=800&sig=5319f37a9eb1bdd5f399316b507522048594d7ed","markup":"

    The 2024 Sydney Championships | Open is a 9-round Swiss, held from the 6th to the 10th in Sydney, Australia.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website | Results

    \n"},"round":{"id":"FxnR92Ll","name":"Round 9","slug":"round-9","createdAt":1720514826452,"startsAt":1720570500000,"url":"https://lichess.org/broadcast/2024-sydney-championships--open/round-8/GPbAETkc"},"group":"2024 Sydney Championships"},{"tour":{"id":"s7YVTwll","name":"United Arab Emirates Championship 2024 | Open","slug":"united-arab-emirates-championship-2024--open","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1720095141515,"url":"https://lichess.org/broadcast/united-arab-emirates-championship-2024--open/s7YVTwll","tier":3,"dates":[1720011600000,1720614600000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:s7YVTwll:t67JoGYK.jpg&w=800&sig=026a374d1ceff3c19522d12949c8ae28cd9e5ac6","markup":"

    The United Arab Emirates Championship 2024 | Open is a 9-round Swiss, held from the 3rd to the 10th of July in Dubai, United Arab Emirates.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Standings

    \n"},"round":{"id":"12JAmxw6","name":"Round 8","slug":"round-8","createdAt":1720095220069,"startsAt":1720530000000,"url":"https://lichess.org/broadcast/united-arab-emirates-championship-2024--open/round-8/12JAmxw6"},"group":"United Arab Emirates Championship 2024"},{"tour":{"id":"6s43vSQx","name":"Satranc Arena IM Chess Tournament Series - 5","slug":"satranc-arena-im-chess-tournament-series-5","info":{"format":"6-player double round-robin","tc":"Classical"},"createdAt":1720442634682,"url":"https://lichess.org/broadcast/satranc-arena-im-chess-tournament-series-5/6s43vSQx","tier":3,"dates":[1720425600000,1720792800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=arbiter_ubh:relay:6s43vSQx:1g42zUbN.jpg&w=800&sig=2031b1d31739c7d4cfe505cbd396b0d3bb44dce0","markup":"

    The Satranc Arena IM Chess Tournament Series - 5 is a 6-player double round-robin, held from the 8th to the 12th of July in Güzelbahçe, İzmir, Türkiye.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Standings

    \n","leaderboard":true},"round":{"id":"NOVf9rXm","name":"Round 4","slug":"round-4","createdAt":1720442689924,"startsAt":1720533600000,"url":"https://lichess.org/broadcast/satranc-arena-im-chess-tournament-series-5/round-3/WoVzBwaJ"}},{"tour":{"id":"veT0PjZv","name":"Paraćin Open 2024","slug":"paracin-open-2024","info":{"format":"9-round Swiss","tc":"Classical","players":"Safarli, Fier, Sasikiran, Prohászka"},"createdAt":1719958223829,"url":"https://lichess.org/broadcast/paracin-open-2024/veT0PjZv","tier":3,"dates":[1720015200000,1720683000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:veT0PjZv:hPF40XDY.jpg&w=800&sig=22712721714425152122d47a0017eb9cc9f8a8cb","markup":"

    The Paraćin Open 2024 is a 9-round Swiss, held from the 3rd to the 11th of July in Paraćin, Serbia.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website | Standings

    \n"},"round":{"id":"A81Fjh6K","name":"Round 7","slug":"round-7","createdAt":1719958344863,"startsAt":1720533600000,"url":"https://lichess.org/broadcast/paracin-open-2024/round-6/2m0ylraL"}},{"tour":{"id":"wv9ahJeR","name":"Greek Team Championship 2024 | Boards 1-40","slug":"greek-team-championship-2024--boards-1-40","info":{"format":"7-round Swiss for teams","tc":"Classical"},"createdAt":1720136757006,"url":"https://lichess.org/broadcast/greek-team-championship-2024--boards-1-40/wv9ahJeR","tier":3,"dates":[1720102500000,1720595700000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:wv9ahJeR:VlUevU6S.jpg&w=800&sig=7d53f78c2ae858286587919e0719844d343a8eb8","markup":"

    The Greek Team Championship 2024 is a 7-round Swiss for teams, held from the 4th to the 10th of July in Trikala, Greece.

    \n

    Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

    \n

    Official Website | Standings

    \n
    \n

    Photo by Nestoras Argiris on Unsplash

    \n","teamTable":true},"round":{"id":"myEffF4b","name":"Round 6","slug":"round-6","createdAt":1720137200753,"startsAt":1720534500000,"url":"https://lichess.org/broadcast/greek-team-championship-2024--boards-1-40/round-5/TEXHbMwG"},"group":"Greek Team Championship 2024"},{"tour":{"id":"F443vhNo","name":"46th Barberà del Vallès Open 2024","slug":"46th-barbera-del-valles-open-2024","info":{"format":"9-round Swiss","tc":"Classical","players":"Cuartas, Berdayes Ason, Alsina Leal"},"createdAt":1720274091992,"url":"https://lichess.org/broadcast/46th-barbera-del-valles-open-2024/F443vhNo","tier":3,"dates":[1720105200000,1720796400000],"markup":"

    The 46th Barberà del Vallès Open 2024 is a 9-round Swiss, held from the 4th to the 12th of July in Barberà del Vallès, Barcelona, Spain.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website | Standings

    \n"},"round":{"id":"CKW9YIsw","name":"Round 6","slug":"round-6","createdAt":1720274140173,"startsAt":1720537200000,"url":"https://lichess.org/broadcast/46th-barbera-del-valles-open-2024/round-5/XsCOWnCp"}},{"tour":{"id":"r4302nsd","name":"1000GM Independence Day GM Norm II","slug":"1000gm-independence-day-gm-norm-ii","info":{"format":"10-player round-robin","tc":"Classical"},"createdAt":1720337947267,"url":"https://lichess.org/broadcast/1000gm-independence-day-gm-norm-ii/r4302nsd","tier":3,"dates":[1720484100000,1720829700000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=linuxbrickie:relay:r4302nsd:fUxEnBVj.jpg&w=800&sig=ea04e951970b23cbca0242ffc8f687b7ab114c3e","markup":"

    The 1000GM Independence Day GM Norm II is a 10-player round-robin, held from the 8th to the 12th of July in San Jose, California, USA.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment starting from move one.

    \n

    Official Website

    \n"},"round":{"id":"o8NDvvjs","name":"Round 2","slug":"round-2","createdAt":1720338512373,"startsAt":1720548900000,"url":"https://lichess.org/broadcast/1000gm-independence-day-gm-norm-ii/round-1/oPQRIyNj"}},{"tour":{"id":"MRV2q3Yq","name":"ACC Monday Nights 2024 | Winter Cup","slug":"acc-monday-nights-2024--winter-cup","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1718105814905,"url":"https://lichess.org/broadcast/acc-monday-nights-2024--winter-cup/MRV2q3Yq","tier":3,"dates":[1718607600000,1723446000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=iacaster:relay:MRV2q3Yq:Vk5eiu90.jpg&w=800&sig=694fd9118495924e183277e487b619a62b0af671","markup":"

    The ACC Monday Nights 2024 | Winter Cup is a 9-round Swiss, held from the 17th of June to the 12th of August in Auckland, New Zealand.

    \n

    Time control is 75 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website | Results

    \n"},"round":{"id":"mzKINdP8","name":"Round 5","slug":"round-5","createdAt":1718106020711,"startsAt":1721026800000,"url":"https://lichess.org/broadcast/acc-monday-nights-2024--winter-cup/round-4/KGgLx2jQ"},"group":"ACC Monday Nights 2024"},{"tour":{"id":"yuUxbxbH","name":"II IRT do GM Milos","slug":"ii-irt-do-gm-milos","info":{"format":"5-round Swiss","tc":"Classical"},"createdAt":1718658553809,"url":"https://lichess.org/broadcast/ii-irt-do-gm-milos/yuUxbxbH","tier":3,"dates":[1718667000000,1721086200000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=sergioglorias:relay:yuUxbxbH:OkKBHwai.jpg&w=800&sig=cd93d77987db9ee650a714ac01d0e0980b68bccb","markup":"

    The II IRT do GM Milos is a 5-round Swiss tournament, held from the 17th of June to the 15th of July in São Paulo, Brazil.

    \n

    Time control is 60 minutes for the entire game, with a 30-second increment from move 1.

    \n

    Official Website | Results

    \n"},"round":{"id":"uKz9Ifu8","name":"Round 5","slug":"round-5","createdAt":1718660015687,"startsAt":1721086200000,"url":"https://lichess.org/broadcast/ii-irt-do-gm-milos/round-4/3YxAk0fs"}},{"tour":{"id":"vs7L5OPC","name":"Switzerland Team Championships SMM 2024","slug":"switzerland-team-championships-smm-2024","info":{"format":"10-team round-robin","tc":"Classical"},"createdAt":1713748519128,"url":"https://lichess.org/broadcast/switzerland-team-championships-smm-2024/vs7L5OPC","tier":3,"dates":[1710070200000,1728810000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:vs7L5OPC:GKhiEYf1.jpg&w=800&sig=f3c322ce93f23b997cec3c06e54493a48bdb561f","markup":"

    The Switzerland Team Championships SMM 2024 | NLA is a 10-team round-robin competition, held from the 10th of March to the 13th of October in Zurich, Switzerland.

    \n

    Time control is 100 minutes for 40 moves, followed by 50 minutes for the next 20 moves, followed by 15 minutes for the rest of the game, with a 30-second increment from move 1.

    \n

    Official Website | Standings (NLA) | Standings (NLB Ost A) | Standings (NLB Ost B)

    \n","teamTable":true},"round":{"id":"aHBXoEjV","name":"Round 6","slug":"round-6","createdAt":1713748519369,"startsAt":1724495400000,"url":"https://lichess.org/broadcast/switzerland-team-championships-smm-2024/round-5/Ted0iPnO"}}],"past":{"currentPage":1,"maxPerPage":20,"currentPageResults":[{"tour":{"id":"7GLYGExC","name":"7th Başkent University Open 2024 | Category A","slug":"7th-baskent-university-open-2024--category-a","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1719997546699,"url":"https://lichess.org/broadcast/7th-baskent-university-open-2024--category-a/7GLYGExC","tier":3,"dates":[1720011600000,1720422000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=okanbilir7:relay:7GLYGExC:XYAyM4VC.jpg&w=800&sig=31c9372a36c05be914612c115c355584617bef62","markup":"

    The 7th Başkent University Open 2024 is a 9-round Swiss, held from the 3rd to the 8th of July in Ankara, Türkiye.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website | Standings

    \n"},"round":{"id":"KYyH44IQ","name":"Round 9","slug":"round-9","createdAt":1720012037962,"finished":true,"startsAt":1720422000000,"url":"https://lichess.org/broadcast/7th-baskent-university-open-2024--category-a/round-9/KYyH44IQ"},"group":"7th Başkent University Open 2024"},{"tour":{"id":"Zpm2BkR3","name":"1000GM Independence Day GM Norm 2024","slug":"1000gm-independence-day-gm-norm-2024","info":{"format":"10-player round-robin","tc":"Classical"},"createdAt":1719922866329,"url":"https://lichess.org/broadcast/1000gm-independence-day-gm-norm-2024/Zpm2BkR3","tier":3,"dates":[1720052100000,1720397700000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:Zpm2BkR3:qTsJhBme.jpg&w=800&sig=d291f09ca0d17c2d5412ccf5f99351451dd433d9","markup":"

    The 1000GM Independence Day GM Norm 2024 is a 10-player round-robin tournament, held from the 3rd to the 7th of July in San Jose, California, USA.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website

    \n","leaderboard":true},"round":{"id":"SlAoLwYT","name":"Round 9","slug":"round-9","createdAt":1719923007464,"finished":true,"startsAt":1720397700000,"url":"https://lichess.org/broadcast/1000gm-independence-day-gm-norm-2024/round-9/SlAoLwYT"}},{"tour":{"id":"eRDPod9B","name":"Marshall Monthly FIDE Premier 2024 | July","slug":"marshall-monthly-fide-premier-2024--july","info":{"format":"5-round Swiss","tc":"Classical"},"createdAt":1720121901223,"url":"https://lichess.org/broadcast/marshall-monthly-fide-premier-2024--july/eRDPod9B","tier":3,"dates":[1720221300000,1720388700000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=sergioglorias:relay:eRDPod9B:ABp4RLul.jpg&w=800&sig=976ad750500d05b752225cb73df7ff01775b90ac","markup":"

    The Marshall Chess Club FIDE Premier July 2024 is a 5-round Swiss, held from the 5th to the 7th of July in New York City, USA.

    \n

    Time control is 90 minutes for the entire game, with a 30-second increment from move 1.

    \n

    Official Website

    \n"},"round":{"id":"v2n1zP96","name":"Round 5","slug":"round-5","createdAt":1720122277391,"finished":true,"startsAt":1720388700000,"url":"https://lichess.org/broadcast/marshall-monthly-fide-premier-2024--july/round-5/v2n1zP96"},"group":"Marshall Monthly FIDE Premier 2024"},{"tour":{"id":"fLqpKaC4","name":"CCA World Open 2024","slug":"cca-world-open-2024","info":{"format":"9-round Swiss","tc":"Classical","players":"Liang, Durarbayli, McShane, Yoo"},"createdAt":1719970723789,"url":"https://lichess.org/broadcast/cca-world-open-2024/fLqpKaC4","tier":4,"dates":[1720048020000,1720386420000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=iacaster:relay:fLqpKaC4:4FYyYcEx.jpg&w=800&sig=883c18f09e16639e631b07c69af344e5a83f1ba1","markup":"

    The CCA World Open 2024 is a 9-round Swiss, held from the 3rd to the 7th of July in Philadelphia, Pennsylvania, USA.

    \n

    Time control is 40 moves in 90 minutes, then 30 minutes, with a 30 second delay after every move.

    \n

    Official Website | Results

    \n
    \n

    Title image photo by Paul Frendach

    \n"},"round":{"id":"uYzumLEp","name":"Round 9","slug":"round-9","createdAt":1719971455782,"finished":true,"startsAt":1720386420000,"url":"https://lichess.org/broadcast/cca-world-open-2024/round-9/uYzumLEp"}},{"tour":{"id":"7t6naO2X","name":"2nd Annual Independence Day Open","slug":"2nd-annual-independence-day-open","info":{"format":"5-round Swiss","tc":"Classical"},"createdAt":1720201395792,"url":"https://lichess.org/broadcast/2nd-annual-independence-day-open/7t6naO2X","tier":3,"dates":[1720221300000,1720379700000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:7t6naO2X:4N8EH5wl.jpg&w=800&sig=d51590b24b9fb8ac5fe204457b54284bc43bcb0d","markup":"

    The 2nd Annual Independence Day Open is a 5-round Swiss, held from the 5th to the 7th of July in Dulles, Virginia, USA.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website

    \n"},"round":{"id":"UNMyETL4","name":"Round 5","slug":"round-5","createdAt":1720201460857,"finished":true,"startsAt":1720379700000,"url":"https://lichess.org/broadcast/2nd-annual-independence-day-open/round-5/UNMyETL4"}},{"tour":{"id":"9Uablwir","name":"1000GM Summer Dual Scheveningen 2024 #3 | Group A","slug":"1000gm-summer-dual-scheveningen-2024-3--group-a","info":{"format":"10-player Semi-Scheveningen","tc":"Classical"},"createdAt":1720124060927,"url":"https://lichess.org/broadcast/1000gm-summer-dual-scheveningen-2024-3--group-a/9Uablwir","tier":3,"dates":[1720199700000,1720372500000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=sergioglorias:relay:9Uablwir:xdjJ2iwL.jpg&w=800&sig=f08151608f83e9a7738e07bd23f7fe05f1db06e4","markup":"

    The 1000GM Summer Dual Scheveningen 2024 #3 | Group A is a 10-player Semi-Scheveningen, held from the 5th to the 7th of July in San Jose, California, USA.

    \n

    Time control is 90 minutes for the entire game, with a 30-second increment from move 1.

    \n

    Official Website

    \n"},"round":{"id":"JxDiKnHY","name":"Round 5","slug":"round-5","createdAt":1720124654816,"finished":true,"startsAt":1720372500000,"url":"https://lichess.org/broadcast/1000gm-summer-dual-scheveningen-2024-3--group-a/round-5/JxDiKnHY"},"group":"1000GM Summer Dual Scheveningen 2024 #3"},{"tour":{"id":"aec1RGgy","name":"Schack-SM 2024 | Sverigemästarklassen","slug":"schack-sm-2024--sverigemastarklassen","info":{"format":"10-player round-robin","tc":"Classical"},"createdAt":1719330577301,"url":"https://lichess.org/broadcast/schack-sm-2024--sverigemastarklassen/aec1RGgy","tier":4,"dates":[1719666000000,1720342800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=claes1981:relay:aec1RGgy:2VfDqH1O.jpg&w=800&sig=dd784c694cd31ea8df2dd4deab2a331379f03e2d","markup":"

    The Swedish Championship week takes place from June 28th to July 7th in Fortnox Arena, Växjö, Sweden. The event includes several sections, which are linked at the bottom.

    \n

    SE

    \n

    Officiell webbplats | Video-kommentering | Resultat och lottning | Livechess PGN

    \n

    Betänketid Sverigemästarklassen: 90 minuter för 40 drag, plus 30 minuter för resten av partiet, plus 30 sekunder per drag från drag ett.

    \n

    Sverigemästarklassen | Mästarklassen-elit | Junior-SM | Mästarklassen | Veteran-SM 50+ | Veteran-SM 65+ | Weekendturneringen I | Klass I-IV | SM-blixten | SM 2023

    \n

    EN

    \n

    Official Website | Video commentary | Results and Pairings | Livechess PGN

    \n

    Time control Swedish Champion Class: 90 minutes for 40 moves, plus 30 minutes for the rest of the game, plus 30 seconds per move from move one.

    \n

    Swedish Champion Class | Elite Masterclass | Swedish Junior Championship | Masterclass | Swedish Senior Championship 50+ | Swedish Senior Championship 65+ | The Weekend Tournament I | Class I-IV | The SM Blitz | 2023

    \n"},"round":{"id":"sJ5sZRMs","name":"Rond 9","slug":"rond-9","createdAt":1719331504689,"finished":true,"startsAt":1720342800000,"url":"https://lichess.org/broadcast/schack-sm-2024--sverigemastarklassen/rond-9/sJ5sZRMs"},"group":"Schack-SM 2024"},{"tour":{"id":"2XEWNHQG","name":"Baku Open 2024 | Group A","slug":"baku-open-2024--group-a","info":{"format":"9-round Swiss","tc":"Classical","players":"Narayanan, Mamedov, Pranav"},"createdAt":1719363025661,"url":"https://lichess.org/broadcast/baku-open-2024--group-a/2XEWNHQG","tier":4,"dates":[1719659700000,1720347300000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:2XEWNHQG:dVkQzLbt.jpg&w=800&sig=d6c75dc5ce69c7d0641e9a5634c658683696b2a5","markup":"

    The Baku Open 2024 is a 9-round Swiss, held from the 29th of June to the 7th of July in Baku, Azerbaijan.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website | Results

    \n
    \n

    Title image photo by Dario Daniel Silva on Unsplash

    \n"},"round":{"id":"TOAPN9Bi","name":"Round 9","slug":"round-9","createdAt":1719363202831,"finished":true,"startsAt":1720347300000,"url":"https://lichess.org/broadcast/baku-open-2024--group-a/round-9/TOAPN9Bi"},"group":"Baku Open 2024"},{"tour":{"id":"Kont9lyt","name":"Spanish U10 Rapid Championship 2024","slug":"spanish-u10-rapid-championship-2024","info":{"format":"9-round Swiss","tc":"Rapid"},"createdAt":1719478161692,"url":"https://lichess.org/broadcast/spanish-u10-rapid-championship-2024/Kont9lyt","tier":3,"dates":[1720278000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=josefeda:relay:Kont9lyt:e7kU6oM9.jpg&w=800&sig=eafa6855e06b5bb948e815534d4212d20f7aa010","markup":"

    The Spanish U10 Rapid Championship 2024 is a 9-round Swiss, held from the 6th to the 7th of July in Salobreña, Granada, Spain.

    \n

    Time control is 10 minutes for the entire game with a 5-second increment from move 1.

    \n

    Official Website | Standings

    \n
    \n

    Campeonato de España Rapido Sub 10 2024

    \n"},"round":{"id":"tIeqLJf0","name":"Ronda 9","slug":"ronda-9","createdAt":1719478541343,"finished":true,"startsAt":1720351800000,"url":"https://lichess.org/broadcast/spanish-u10-rapid-championship-2024/ronda-9/tIeqLJf0"}},{"tour":{"id":"lunItMBB","name":"Saxony-Anhalt Seniors Championships 2024 | 50+","slug":"saxony-anhalt-seniors-championships-2024--50","info":{"format":"7-round Swiss","tc":"Classical"},"createdAt":1719827879431,"url":"https://lichess.org/broadcast/saxony-anhalt-seniors-championships-2024--50/lunItMBB","tier":3,"dates":[1719839700000,1720340100000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:lunItMBB:M2ARPeSq.jpg&w=800&sig=6230cf839867f551e78c70673e839a03162a541b","markup":"

    The Saxony-Anhalt Seniors Championships 2024 | 50+ is a 7-round Swiss, held from the 1st to the 7th of July in Magdeburg, Germany.

    \n

    Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

    \n

    Official Website

    \n"},"round":{"id":"Hh4EwihK","name":"Round 7","slug":"round-7","createdAt":1719827954310,"finished":true,"startsAt":1720340100000,"url":"https://lichess.org/broadcast/saxony-anhalt-seniors-championships-2024--50/round-7/Hh4EwihK"},"group":"Saxony-Anhalt Seniors Championships 2024"},{"tour":{"id":"47N9XRWe","name":"České Budějovice Chess Festival 2024 | GM A","slug":"ceske-budejovice-chess-festival-2024--gm-a","info":{"format":"10-player round-robin","tc":"Classical"},"createdAt":1719327405409,"url":"https://lichess.org/broadcast/ceske-budejovice-chess-festival-2024--gm-a/47N9XRWe","tier":3,"dates":[1719669600000,1720339200000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:47N9XRWe:aOKaYfNt.jpg&w=800&sig=85338600beb8ded4a6165adec640570c8112dd42","markup":"

    The České Budějovice Chess Festival 2024 | GM A is a 10-player round-robin tournament, held from the 29th of June to the 7th of July in České Budějovice, Czech Republic.

    \n

    Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

    \n

    Offiical Website | Standings

    \n
    \n

    Title image photo by Hans Lemuet (Spone), CC BY-SA 3.0, via Wikimedia Commons

    \n","leaderboard":true},"round":{"id":"CjE6k07C","name":"Round 9","slug":"round-9","createdAt":1719327648068,"finished":true,"startsAt":1720339200000,"url":"https://lichess.org/broadcast/ceske-budejovice-chess-festival-2024--gm-a/round-9/CjE6k07C"},"group":"České Budějovice Chess Festival 2024"},{"tour":{"id":"5143V4eE","name":"XXIV Open Internacional d'Escacs de Torredembarra","slug":"xxiv-open-internacional-descacs-de-torredembarra","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1719588450645,"url":"https://lichess.org/broadcast/xxiv-open-internacional-descacs-de-torredembarra/5143V4eE","tier":3,"dates":[1719671400000,1720335600000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=ukkina:relay:5143V4eE:R0iNiocy.jpg&w=800&sig=163d30e58a300f59c5255f3e765e709fe5ccf8c7","markup":"

    The XXIV Open Internacional d'Escacs de Torredembarra is a 9-round Swiss, held from the 29th of June to the 7th of July in
    Torredembarra, Spain.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website | Standings

    \n
    \n

    Del 29 de juny al 7 de juliol de 2024
    ORGANITZA: CLUB D’ESCACS TORREDEMBARRA
    (Integrat al XX Circuit Català d’Oberts Internacionals d’Escacs, classificat amb categoria B, b. (http://www.escacs.cat).

    \n"},"round":{"id":"1x9bhyjf","name":"Round 9","slug":"round-9","createdAt":1719761787694,"finished":true,"startsAt":1720335600000,"url":"https://lichess.org/broadcast/xxiv-open-internacional-descacs-de-torredembarra/round-9/1x9bhyjf"}},{"tour":{"id":"HeOoTDru","name":"All-Ukrainian Festival Morshyn 2024","slug":"all-ukrainian-festival-morshyn-2024","info":{"format":"9-round Swiss","tc":"Rapid"},"createdAt":1720267972743,"url":"https://lichess.org/broadcast/all-ukrainian-festival-morshyn-2024/HeOoTDru","tier":3,"dates":[1720252800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:HeOoTDru:TAcEY8XI.jpg&w=800&sig=729d010e7afad48d86fa086f6176102734f901f5","markup":"

    The All-Ukrainian Festival Morshyn 2024 is a 9-round Swiss, held on the 6th of July in Morshyn, Ukraine.

    \n

    Time control is 10 minutes for the entire game with a 5-second increment from move 1.

    \n

    Standings

    \n
    \n

    Title image photo by ЯдвигаВереск - Own work, CC BY-SA 4.0

    \n"},"round":{"id":"6YRSXzDZ","name":"Round 9","slug":"round-9","createdAt":1720268064062,"finished":true,"startsAt":1720276200000,"url":"https://lichess.org/broadcast/all-ukrainian-festival-morshyn-2024/round-9/6YRSXzDZ"}},{"tour":{"id":"Db0i9sGV","name":"Spanish U10 Championship 2024","slug":"spanish-u10-championship-2024","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1719401439623,"url":"https://lichess.org/broadcast/spanish-u10-championship-2024/Db0i9sGV","tier":3,"dates":[1719820800000,1720252800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=josefeda:relay:Db0i9sGV:TyPuOKoC.jpg&w=800&sig=67728c0d69503809c4f6a137ff382f56ed8d8af7","markup":"

    The Spanish U10 Championship 2024 is a 9-round Swiss, held from the 1st to the 6th of July in Salobreña, Granada, Spain.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website | Standings

    \n
    \n

    Campeonato de España Sub 10 2024

    \n"},"round":{"id":"KYK9G7kE","name":"Ronda 9","slug":"ronda-9","createdAt":1719401772691,"finished":true,"startsAt":1720252800000,"url":"https://lichess.org/broadcast/spanish-u10-championship-2024/ronda-9/KYK9G7kE"}},{"tour":{"id":"BjKO6Jrs","name":"Italian U18 Youth Championships 2024 | U18","slug":"italian-u18-youth-championships-2024--u18","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1719575583240,"url":"https://lichess.org/broadcast/italian-u18-youth-championships-2024--u18/BjKO6Jrs","tier":3,"dates":[1719666900000,1720251900000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:BjKO6Jrs:ztfSSOiP.jpg&w=800&sig=f640d262f4c1545eb47cf716766179385ae51b6e","markup":"

    The Italian U18 Youth Championships 2024 | U18 is a 9-round Swiss, held from the 29th of June to the 6th of July in Salsomaggiore Terme, Italy.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website | Results

    \n"},"round":{"id":"OCMHlRDH","name":"Round 9","slug":"round-9","createdAt":1719575679992,"finished":true,"startsAt":1720251900000,"url":"https://lichess.org/broadcast/italian-u18-youth-championships-2024--u18/round-9/OCMHlRDH"},"group":"Italian U18 Youth Championships 2024"},{"tour":{"id":"hdQQ1Waq","name":"Norwegian Championships 2024 | Elite and Seniors 65+","slug":"norwegian-championships-2024--elite-and-seniors-65","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1719174080786,"url":"https://lichess.org/broadcast/norwegian-championships-2024--elite-and-seniors-65/hdQQ1Waq","tier":4,"dates":[1719591300000,1720253700000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:hdQQ1Waq:K1YsAziL.jpg&w=800&sig=1921f0a71b78530c0a6e8b0e564e19af8b91c499","markup":"

    The Norwegian Championships 2024 | Elite and Seniors 65+ is a 9-round Swiss, held from the 28th of June to the 6th of July in Storefjell, Norway.

    \n

    Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

    \n

    Board 1-9 Elite
    Board 10 - 28 Senior 65+

    \n

    Official Website | Results

    \n
    \n

    Landsturneringen 2024

    \n

    Eliteklassen og Senior 65+

    \n

    Spilles på Storefjell resort hotell 28.06.2024 - 06.07.2024

    \n

    Turneringen spilles over 9 runder, med betenkningstid 90 min på 40 trekk, 30 min på resten av partiet og 30 sek tillegg fra trekk 1

    \n

    Bord 1-9 Eliteklassen
    Bord 10 - 28 Senior 65+

    \n

    Clono partier:
    Mikroputt
    \nhttps://lichess.org/broadcast/nm-i-sjakk-2024-mikroputt/round-1/020oDPUm#boards
    Miniputt
    https://lichess.org/broadcast/nm-i-sjakk-2024-miniputt/round-1/pCvV4G8i#boards
    Lilleputt
    https://lichess.org/broadcast/nm-i-sjakk-2024-lilleputt/round-1/k8GS6LrP
    Junior B
    https://lichess.org/broadcast/nm-i-sjakk-junior-b/round-1/AZhM1hMm
    Klasse 1
    https://lichess.org/broadcast/nm-i-sjakk-2024-klasse-1/round-1/aWw2RwQ1
    Klasse 2
    https://lichess.org/broadcast/nm-i-sjakk-2024-klasse-2/round-1/Mnxw76OR
    Klasse 3
    https://lichess.org/broadcast/nmi-sjakk-2024-klasse-3/round-1/ZheSrANG
    Klasse 4
    https://lichess.org/broadcast/nm-i-sjakk-klasse-4/round-1/X673vUlD
    Klasse 5
    https://lichess.org/broadcast/nm-i-sjakk-2024-klasse-5/round-1/C6m3qitn
    Klasse Mester
    https://lichess.org/broadcast/nm-i-sjakk-2024-mesterklassen/round-2/lZu3t3A7#boards

    \n"},"round":{"id":"LQn45rIa","name":"Round 9","slug":"round-9","createdAt":1719175255813,"finished":true,"startsAt":1720253700000,"url":"https://lichess.org/broadcast/norwegian-championships-2024--elite-and-seniors-65/round-9/LQn45rIa"},"group":"Norwegian Championships 2024"},{"tour":{"id":"K1NfeoWE","name":"Superbet Romania Chess Classic 2024","slug":"superbet-romania-chess-classic-2024","info":{"format":"10-player Round Robin","tc":"Classical","players":"Caruana, Nepomniachtchi, Gukesh, Giri"},"createdAt":1719187354944,"url":"https://lichess.org/broadcast/superbet-romania-chess-classic-2024/K1NfeoWE","tier":5,"dates":[1719405000000,1720198800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=iacaster:relay:K1NfeoWE:6kBI06CJ.jpg&w=800&sig=c6b93e8db217a6bb504dcb5e0695337a472655b7","markup":"

    The Superbet Romania Chess Classic 2024 is a 10-player Round Robin, held from the 26th of June to the 5th of July in Bucharest, Romania.

    \n

    Time control is 120 minutes for the entire game, plus a 30-second increment per move.

    \n

    Superbet Chess Classic Romania is the first of two classical events, this tournament will feature a 10-player round robin with nine tour regulars, Caruana, Nepomniachtchi, Abdusattorov, Gukesh, So, Praggnanandhaa, Giri, Firouzja, Vachier-Lagrave, and one wildcard, local Romanian favorite Bogdan-Daniel Deac.

    \n

    Official Website | Results

    \n
    \n

    In the event of a tie for 1st place, a double round-robin will be played with 2 players, or a single round-robin will be played with 3 or more players. Time control is 10 minutes for the entire game with a 5-second increment from move 1.

    \n

    In the event of another tie, knockout armageddon games will be played. Time control is 5 minutes for White, 4 minutes for Black, with a 2-second increment from move 61.

    \n
    \n

    Grand Chess Tour | Tour Standings
    2024 Superbet Poland Rapid & Blitz
    2024 Superbet Romania Chess Classic
    2024 Superunited Croatia Rapid & Blitz

    \n
    \n

    Title image photo by Arvid Olson from Pixabay

    \n","leaderboard":true},"round":{"id":"QC9QC8Lr","name":"Tiebreaks","slug":"tiebreaks","createdAt":1720197015416,"finished":true,"startsAt":1720198800000,"url":"https://lichess.org/broadcast/superbet-romania-chess-classic-2024/tiebreaks/QC9QC8Lr"}},{"tour":{"id":"ZmFLmrss","name":"III Magistral Internacional Ciudad de Sant Joan de Alacant","slug":"iii-magistral-internacional-ciudad-de-sant-joan-de-alacant","info":{"format":"10-player round-robin","tc":"Classical"},"createdAt":1719791889764,"url":"https://lichess.org/broadcast/iii-magistral-internacional-ciudad-de-sant-joan-de-alacant/ZmFLmrss","tier":3,"dates":[1719820800000,1720162800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:ZmFLmrss:2RKhpn3T.jpg&w=800&sig=8be9210de5ef07975dbfb9d300fffc3ad1e38ecc","markup":"

    The III Magistral Internacional Ciudad de Sant Joan de Alacant is a 10-player round-robin tournament, held from the 1st to the 5th of July in Sant Joan d'Alacant, Spain.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website | Standings

    \n","leaderboard":true},"round":{"id":"MIy50UWQ","name":"Round 9","slug":"round-9","createdAt":1719791991550,"finished":true,"startsAt":1720162800000,"url":"https://lichess.org/broadcast/iii-magistral-internacional-ciudad-de-sant-joan-de-alacant/round-9/MIy50UWQ"}},{"tour":{"id":"fQu6hjlI","name":"1000GM Summer Dual Scheveningen 2024 #2 | Group A","slug":"1000gm-summer-dual-scheveningen-2024-2--group-a","info":{"format":"10-player Semi-Scheveningen","tc":"Classical"},"createdAt":1719922442023,"url":"https://lichess.org/broadcast/1000gm-summer-dual-scheveningen-2024-2--group-a/fQu6hjlI","tier":3,"dates":[1719940500000,1720113300000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:fQu6hjlI:mVZ0X3CV.jpg&w=800&sig=1d2b55c6d0cac0ceda72a4fbc83e837a45006b9e","markup":"

    The 1000GM Summer Dual Scheveningen 2024 #2 | Group A is a 10-player Semi-Scheveningen, held from the 2nd to the 4th of July in San Jose, California, USA.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website

    \n","leaderboard":true},"round":{"id":"D5IvvZGj","name":"Round 5","slug":"round-5","createdAt":1719922517399,"finished":true,"startsAt":1720113300000,"url":"https://lichess.org/broadcast/1000gm-summer-dual-scheveningen-2024-2--group-a/round-5/D5IvvZGj"},"group":"1000GM Summer Dual Scheveningen 2024 #2"},{"tour":{"id":"4ERHDodE","name":"Atlantic Chess Independence Day GM Norm Invitational","slug":"atlantic-chess-independence-day-gm-norm-invitational","info":{"format":"10-player round-robin","tc":"Classical","players":"Erenburg, Plát, Barbosa, Gauri"},"createdAt":1719693938025,"url":"https://lichess.org/broadcast/atlantic-chess-independence-day-gm-norm-invitational/4ERHDodE","tier":3,"dates":[1719695700000,1720098900000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:4ERHDodE:l3iVV7Ym.jpg&w=800&sig=d57277f927849426413b3b26fccacbad1027ae51","markup":"

    The Atlantic Chess Independence Day GM Norm Invitational is a 10-player round-robin tournament, held from the 29th of June to the 4th of July in Dulles, Virginia, USA.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website | Standings

    \n
    \n

    The Atlantic Chess Association is organizing the Independence Day Norm Tournament. It is a 6 day, 9 rounds, 10 player Round Robin tournament.

    \n

    Chief Arbiter: IA Gregory Vaserstein

    \n

    Venue: Hampton Inn & Suites Washington-Dulles International Airport (4050 Westfax Dr., Chantilly, VA 20151)

    \n","leaderboard":true},"round":{"id":"PVG8wijk","name":"Round 9","slug":"round-9","createdAt":1719698677225,"finished":true,"startsAt":1720098900000,"url":"https://lichess.org/broadcast/atlantic-chess-independence-day-gm-norm-invitational/round-9/PVG8wijk"}}],"previousPage":null,"nextPage":2}} ''', 200, headers: {'content-type': 'application/json; charset=utf-8'}, @@ -35,7 +35,6 @@ void main() { expect(response, isA()); expect(response.active.isNotEmpty, true); - expect(response.upcoming.isNotEmpty, true); expect(response.past.isNotEmpty, true); }); diff --git a/test/view/broadcast/broadcasts_list_screen_test.dart b/test/view/broadcast/broadcasts_list_screen_test.dart index 7f31b21346..a60c308f80 100644 --- a/test/view/broadcast/broadcasts_list_screen_test.dart +++ b/test/view/broadcast/broadcasts_list_screen_test.dart @@ -18,7 +18,7 @@ class FakeImageColorWorker implements ImageColorWorker { bool get closed => false; @override - Future<(int, int)?> getImageColors(String url) { + Future getImageColors(String url) { return Future.value(null); } } @@ -106,5 +106,5 @@ void main() { } const broadcastsResponse = r''' -{"active":[{"tour":{"id":"ioLNPN8j","name":"2nd Rustam Kasimdzhanov Cup 2024","slug":"2nd-rustam-kasimdzhanov-cup-2024","info":{"format":"10-player round-robin","tc":"Rapid","players":"Abdusattorov, Rapport, Mamedyarov, Grischuk"},"createdAt":1720352380417,"url":"https://lichess.org/broadcast/2nd-rustam-kasimdzhanov-cup-2024/ioLNPN8j","tier":5,"dates":[1720431600000,1720519800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=uzchess23:relay:ioLNPN8j:ya35G192.jpg&w=800&sig=b6543625b806cf43f4ee652d0a23d80fad236b35","markup":"

    The 2nd International Rustam Kasimdzhanov Cup 2024 is a 10-player round-robin tournament, held from the 8th to the 9th of July in Tashkent, Uzbekistan.

    \n

    Time control is 15 minutes for the entire game with a 10-second increment from move 1.

    \n

    Standings

    \n","leaderboard":true},"round":{"id":"A4J7qTO6","name":"Round 8","slug":"round-8","createdAt":1720438046707,"ongoing":true,"startsAt":1720516500000,"url":"https://lichess.org/broadcast/2nd-rustam-kasimdzhanov-cup-2024/round-8/A4J7qTO6"}},{"tour":{"id":"a4gBsu31","name":"Dutch Championship 2024 | Open","slug":"dutch-championship-2024--open","info":{"format":"10-player knockout","tc":"Classical","players":"Warmerdam, l’Ami, Bok, Sokolov"},"createdAt":1720037021926,"url":"https://lichess.org/broadcast/dutch-championship-2024--open/a4gBsu31","tier":4,"dates":[1720263600000,1720882800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:a4gBsu31:OgaRY7Pw.jpg&w=800&sig=cb141524b135d0cdc45deafcb9b4bfffc805ecee","markup":"

    The Dutch Championship 2024 | Open is a 10-player single-elimination knockout tournament, held from the 6th to the 13th of July in Utrecht, the Netherlands.

    \n

    Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

    \n

    Official Website | Results

    \n
    \n

    If a round ends in a tie after 2 classical games, a tiebreak match of 2 blitz games is played. Time control is 4+2.

    \n

    If the first tiebreak match ends in another tie, a second tiebreak match of 2 blitz games with reversed colours is played. Time control is 4+2.

    \n

    If the second tiebreak match ends in another tie, colours are drawn and a sudden death is played. Time control is 4+2 for White and 5+2 for Black. The first player to win a game, wins the round. After every 2 games, the colour order is changed.

    \n"},"round":{"id":"Xfe00Awr","name":"Quarter-Finals | Game 2","slug":"quarter-finals--game-2","createdAt":1720037148839,"ongoing":true,"startsAt":1720522800000,"url":"https://lichess.org/broadcast/dutch-championship-2024--open/quarter-finals--game-2/Xfe00Awr"},"group":"Dutch Championship 2024"},{"tour":{"id":"aPC3ATVG","name":"FIDE World Senior Team Chess Championships 2024 | 50+","slug":"fide-world-senior-team-chess-championships-2024--50","info":{"format":"9-round Swiss for teams","tc":"Classical","players":"Adams, Ehlvest, David, Novikov"},"createdAt":1719921457211,"url":"https://lichess.org/broadcast/fide-world-senior-team-chess-championships-2024--50/aPC3ATVG","tier":4,"dates":[1719926100000,1720685700000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=mansuba64:relay:aPC3ATVG:xuiZWY67.jpg&w=800&sig=bfc2aa87dce4ed7bdfb5ce5b9f16285e23479f05","markup":"

    The FIDE World Senior Team Chess Championships 2024 | 50+ is a 9-round Swiss for teams, held from the 2nd to the 11th of July in Kraków, Poland.

    \n

    Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

    \n

    Official Website | Standings

    \n
    \n

    There shall be two categories; Open age 50+ and Open age 65+ with separate events for women.
    The player must have reached or reach the required age during the year of competition.
    There shall be separate Women’s Championship(s) if there are at least ten teams from at least two continents. Otherwise women’s teams play in Open competition
    The Championships are open tournaments for teams registered by their federation. FIDE member federations shall have the right to send as many teams as they wish.

    \n

    The winning team obtains the title “World Team Champion “age 50+ (or age 65+)”.
    The best placed women team obtains the title “World Women Team Champion” age 50+ (or age 65+).

    \n

    Prize Fund: 10,000 EUR

    \n","teamTable":true},"round":{"id":"YIw910wS","name":"Round 7","slug":"round-7","createdAt":1719928673349,"startsAt":1720530900000,"url":"https://lichess.org/broadcast/fide-world-senior-team-chess-championships-2024--50/round-6/Gue2qJfw"},"group":"FIDE World Senior Team Chess Championships 2024"},{"tour":{"id":"tCMfpIJI","name":"43rd Villa de Benasque Open 2024","slug":"43rd-villa-de-benasque-open-2024","info":{"format":"10-round Swiss","tc":"Classical","players":"Alekseenko, Bartel, Pichot"},"createdAt":1719422556116,"url":"https://lichess.org/broadcast/43rd-villa-de-benasque-open-2024/tCMfpIJI","tier":4,"dates":[1720189800000,1720941300000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=kike0:relay:tCMfpIJI:P6c1Rrxn.jpg&w=800&sig=69d4e6158f133578bbb35519346e4395d891ca2c","markup":"

    The 43rd Villa de Benasque Open 2024 is a 10-round Swiss, held from the 5th to the 14th of July in Benasque, Spain.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move one.

    \n

    GM Kirill Alekseenko is the tournament's top seed - with nearly 100 titled players, 500 players in total, and over €50,000 in prizes.

    \n

    Official Website | Standings

    \n
    \n

    El XLIII Open Internacional Villa de Benasque se disputará por el Sistema Suizo a 10 rondas, del 5 al 14 de Julio de 2024. El GM Alekseenko lidera un ranking con cerca de 100 titulados, 500 jugadores y más de 50.000 euros de premios en metálico. El local de juego será el Pabellón Polideportivo de Benasque (España).

    \n

    El ritmo de juego será de 90 minutos + 30 segundos de incremento acumulativo por jugada empezando desde la primera.

    \n

    Web Oficial | Chess-Results

    \n"},"round":{"id":"SXAjWw0G","name":"Round 5","slug":"round-5","createdAt":1719422658882,"startsAt":1720534500000,"url":"https://lichess.org/broadcast/43rd-villa-de-benasque-open-2024/round-4/she3bD2w"}},{"tour":{"id":"yOuW4siY","name":"Spanish U12 Championships 2024 | Classical","slug":"spanish-u12-championships-2024--classical","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1720081884293,"url":"https://lichess.org/broadcast/spanish-u12-championships-2024--classical/yOuW4siY","tier":3,"dates":[1720425600000,1720857600000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=josefeda:relay:yOuW4siY:FCsABLhH.jpg&w=800&sig=ca8faadb2725de80ab6a316e98b8505f1f620f71","markup":"

    The Spanish U12 Championship 2024 is a 9-round Swiss, held from the 6th to the 7th of July in Salobreña, Granada, Spain.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website | Standings

    \n
    \n

    Campeonato de España Sub 12 2024

    \n"},"round":{"id":"eCa2CbqM","name":"Ronda 3","slug":"ronda-3","createdAt":1720082094252,"ongoing":true,"startsAt":1720512000000,"url":"https://lichess.org/broadcast/spanish-u12-championships-2024--classical/ronda-3/eCa2CbqM"},"group":"Spanish U12 Championships 2024"},{"tour":{"id":"JQGYmn68","name":"Scottish Championship International Open 2024","slug":"scottish-championship-international-open-2024","info":{"format":"9-round Swiss","tc":"90+30"},"createdAt":1720440336101,"url":"https://lichess.org/broadcast/scottish-championship-international-open-2024/JQGYmn68","tier":3,"dates":[1720447200000,1720965600000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=prospect_d:relay:JQGYmn68:I58xFyHC.jpg&w=800&sig=ccd5889235ee538ce022dcc5ff8ed1568e5f4377","markup":"

    The Scottish Championship International Open 2024 is a 9-round Swiss, held from the 8th to the 14th of July in Dunfermline, Scotland.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Standings

    \n"},"round":{"id":"Nw190iGM","name":"Round 2","slug":"round-2","createdAt":1720451119128,"ongoing":true,"startsAt":1720515600000,"url":"https://lichess.org/broadcast/scottish-championship-international-open-2024/round-2/Nw190iGM"}},{"tour":{"id":"YBTYQbxm","name":"South Wales International Open 2024","slug":"south-wales-international-open-2024","info":{"format":"9-round Swiss","tc":"Classical","players":"Chatalbashev, Cuenca, Grieve, Han"},"createdAt":1720127613709,"url":"https://lichess.org/broadcast/south-wales-international-open-2024/YBTYQbxm","tier":3,"dates":[1720170000000,1720602000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:YBTYQbxm:BDfr290h.jpg&w=800&sig=2e914768c4d3781264493309d8e37c86221c8ac7","markup":"

    The South Wales International Open 2024 is a 9-round Swiss title norm tournament taking place in Bridgend, Wales from the 5th to the 10th of July.

    \n

    Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

    \n

    Official Website | Standings

    \n","leaderboard":true},"round":{"id":"Svyiq7jS","name":"Round 7","slug":"round-7","createdAt":1720127909656,"ongoing":true,"startsAt":1720515600000,"url":"https://lichess.org/broadcast/south-wales-international-open-2024/round-7/Svyiq7jS"}},{"tour":{"id":"BgVqV6b0","name":"Koege Open 2024","slug":"koege-open-2024","info":{"format":"10-player round-robin","tc":"Classical","players":"Petrov, Smith, Hector"},"createdAt":1720361492349,"url":"https://lichess.org/broadcast/koege-open-2024/BgVqV6b0","tier":3,"dates":[1720512300000,1720944300000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=fishdefend:relay:BgVqV6b0:kr4GaRvW.jpg&w=800&sig=b2f59c1ae2cb70a88c8e7872b1327b93c64cdad3","markup":"

    The Koege Open 2024 is a 10-player round-robin tournament, held from the 9th to the 14th of July in Køge, Denmark.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Standings

    \n
    \n

    Group 1: Boards 1-5
    Group 2: Boards 6-10

    \n"},"round":{"id":"y0ksveWZ","name":"Round 1","slug":"round-1","createdAt":1720460080111,"ongoing":true,"startsAt":1720512300000,"url":"https://lichess.org/broadcast/koege-open-2024/round-1/y0ksveWZ"}},{"tour":{"id":"XV3jpD1b","name":"Belgian Championship 2024 | Expert","slug":"belgian-championship-2024--expert","info":{"format":"10-player round-robin","tc":"Classical"},"createdAt":1720276555430,"url":"https://lichess.org/broadcast/belgian-championship-2024--expert/XV3jpD1b","tier":3,"dates":[1720267200000,1720944000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=sergioglorias:relay:XV3jpD1b:BkW8q64n.jpg&w=800&sig=017b9b9cf354268158da36eba185e961d2cfc5e3","markup":"

    The Belgian Championship 2024 | Expert is a 10-player round-robin tournament, held from the 6th to the 14th of July in Lier, Belgium.

    \n

    The winner will be crowned Belgian Chess Champion 2024.

    \n

    Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

    \n

    Official Website

    \n","leaderboard":true},"round":{"id":"iSD0HAuQ","name":"Round 4","slug":"round-4","createdAt":1720276601311,"ongoing":true,"startsAt":1720526400000,"url":"https://lichess.org/broadcast/belgian-championship-2024--expert/round-4/iSD0HAuQ"},"group":"Belgian Championship 2024"},{"tour":{"id":"oo69aO3w","name":"SAIF Powertec Bangladesh Championship 2024","slug":"saif-powertec-bangladesh-championship-2024","info":{"format":"14-player round-robin","tc":"Classical"},"createdAt":1719050225145,"url":"https://lichess.org/broadcast/saif-powertec-bangladesh-championship-2024/oo69aO3w","tier":3,"dates":[1719133200000,1720602000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=fathirahman:relay:oo69aO3w:o2ATDvXh.jpg&w=800&sig=f93c0ea42a7223efb8efdc4e245acca39962b9f3","markup":"

    The SAIF Powertec Bangladesh Championship 2024 is a 14-player round-robin tournament, held from the 23rd of June to the 6th of July in Dhaka, Bangladesh.

    \n

    Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

    \n

    Official Website | Results

    \n
    \n

    SAIF Powertec 48th Bangladesh National Chess Championship 2024 is Bangladesh's national chess championship. Top 5 players are:

    \n
      \n
    1. FM Manon, Reja Neer 2445
    2. \n
    3. IM Mohammad Fahad, Rahman 2437
    4. \n
    5. GM Rahman, Ziaur 2423
    6. \n
    7. GM Hossain, Enamul 2365
    8. \n
    9. GM Murshed, Niaz 2317
    10. \n
    \n

    The previous series (2022) champion is GM Enamul Hossain. This is a 14-player round-robin tournament, where 3 GMs have been invited to play directly, and 11 players are from the top 11 of the qualifying round, known as the National B Championship.

    \n

    Five GMs were invited, but only three accepted the invitation. Therefore, instead of taking 9 players from National B, 11 players qualified to fulfill the round requirements.

    \n

    The top 5 players qualify for the Olympiad team.

    \n

    Here are useful links:

    \n\n"},"round":{"id":"LLqfCDm6","name":"Round 12 (Postponed)","slug":"round-12-postponed","createdAt":1719050487028,"ongoing":true,"startsAt":1720515600000,"url":"https://lichess.org/broadcast/saif-powertec-bangladesh-championship-2024/round-12-postponed/LLqfCDm6"}},{"tour":{"id":"Qag4N0cA","name":"4th La Plagne Festival 2024","slug":"4th-la-plagne-festival-2024","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1720274920410,"url":"https://lichess.org/broadcast/4th-la-plagne-festival-2024/Qag4N0cA","tier":3,"dates":[1720278000000,1720771200000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:Qag4N0cA:v3g6RfVf.jpg&w=800&sig=78b9c7d33a747e3c20fc26fd5360d07c9546debc","markup":"

    The 4th La Plagne International Chess Festival is a 9-round Swiss, held from the 6th to the 12th of July at La Plagne in Savoie, France.

    \n

    Time control is is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

    \n

    Official Website | Standings

    \n"},"round":{"id":"0qufdZnF","name":"Round 5","slug":"round-5","createdAt":1720286940762,"startsAt":1720531800000,"url":"https://lichess.org/broadcast/4th-la-plagne-festival-2024/round-4/gQt8ubbC"}},{"tour":{"id":"95l4pho3","name":"Peruvian Championship Finals 2024 | Open","slug":"peruvian-championship-finals-2024--open","info":{"format":"12-player round-robin","tc":"Classical","players":"Terry, Flores Quillas, Leiva"},"createdAt":1720272022000,"url":"https://lichess.org/broadcast/peruvian-championship-finals-2024--open/95l4pho3","tier":3,"dates":[1720278000000,1720796400000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:95l4pho3:mDrHsa8C.jpg&w=800&sig=0d81273752b2bcdd47180ae23201f15074a91be9","markup":"

    The Peruvian Championship Finals 2024 | Open is a 12-player round-robin tournament, held from the 6th to the 12th of July in Lima, Peru.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Standings

    \n","leaderboard":true},"round":{"id":"Pi0HtFDs","name":"Round 6","slug":"round-6","createdAt":1720272103102,"startsAt":1720537200000,"url":"https://lichess.org/broadcast/peruvian-championship-finals-2024--open/round-5/JuIghW2d"},"group":"Peruvian Championship Finals 2024"},{"tour":{"id":"85buXS8z","name":"2024 Sydney Championships | Open","slug":"2024-sydney-championships--open","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1713001604469,"url":"https://lichess.org/broadcast/2024-sydney-championships--open/85buXS8z","tier":3,"dates":[1720226700000,1720570500000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=rootyhillcc:relay:85buXS8z:GSmVFAej.jpg&w=800&sig=5319f37a9eb1bdd5f399316b507522048594d7ed","markup":"

    The 2024 Sydney Championships | Open is a 9-round Swiss, held from the 6th to the 10th in Sydney, Australia.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website | Results

    \n"},"round":{"id":"FxnR92Ll","name":"Round 9","slug":"round-9","createdAt":1720514826452,"startsAt":1720570500000,"url":"https://lichess.org/broadcast/2024-sydney-championships--open/round-8/GPbAETkc"},"group":"2024 Sydney Championships"},{"tour":{"id":"s7YVTwll","name":"United Arab Emirates Championship 2024 | Open","slug":"united-arab-emirates-championship-2024--open","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1720095141515,"url":"https://lichess.org/broadcast/united-arab-emirates-championship-2024--open/s7YVTwll","tier":3,"dates":[1720011600000,1720614600000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:s7YVTwll:t67JoGYK.jpg&w=800&sig=026a374d1ceff3c19522d12949c8ae28cd9e5ac6","markup":"

    The United Arab Emirates Championship 2024 | Open is a 9-round Swiss, held from the 3rd to the 10th of July in Dubai, United Arab Emirates.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Standings

    \n"},"round":{"id":"12JAmxw6","name":"Round 8","slug":"round-8","createdAt":1720095220069,"startsAt":1720530000000,"url":"https://lichess.org/broadcast/united-arab-emirates-championship-2024--open/round-8/12JAmxw6"},"group":"United Arab Emirates Championship 2024"},{"tour":{"id":"6s43vSQx","name":"Satranc Arena IM Chess Tournament Series - 5","slug":"satranc-arena-im-chess-tournament-series-5","info":{"format":"6-player double round-robin","tc":"Classical"},"createdAt":1720442634682,"url":"https://lichess.org/broadcast/satranc-arena-im-chess-tournament-series-5/6s43vSQx","tier":3,"dates":[1720425600000,1720792800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=arbiter_ubh:relay:6s43vSQx:1g42zUbN.jpg&w=800&sig=2031b1d31739c7d4cfe505cbd396b0d3bb44dce0","markup":"

    The Satranc Arena IM Chess Tournament Series - 5 is a 6-player double round-robin, held from the 8th to the 12th of July in Güzelbahçe, İzmir, Türkiye.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Standings

    \n","leaderboard":true},"round":{"id":"NOVf9rXm","name":"Round 4","slug":"round-4","createdAt":1720442689924,"startsAt":1720533600000,"url":"https://lichess.org/broadcast/satranc-arena-im-chess-tournament-series-5/round-3/WoVzBwaJ"}},{"tour":{"id":"veT0PjZv","name":"Paraćin Open 2024","slug":"paracin-open-2024","info":{"format":"9-round Swiss","tc":"Classical","players":"Safarli, Fier, Sasikiran, Prohászka"},"createdAt":1719958223829,"url":"https://lichess.org/broadcast/paracin-open-2024/veT0PjZv","tier":3,"dates":[1720015200000,1720683000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:veT0PjZv:hPF40XDY.jpg&w=800&sig=22712721714425152122d47a0017eb9cc9f8a8cb","markup":"

    The Paraćin Open 2024 is a 9-round Swiss, held from the 3rd to the 11th of July in Paraćin, Serbia.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website | Standings

    \n"},"round":{"id":"A81Fjh6K","name":"Round 7","slug":"round-7","createdAt":1719958344863,"startsAt":1720533600000,"url":"https://lichess.org/broadcast/paracin-open-2024/round-6/2m0ylraL"}},{"tour":{"id":"wv9ahJeR","name":"Greek Team Championship 2024 | Boards 1-40","slug":"greek-team-championship-2024--boards-1-40","info":{"format":"7-round Swiss for teams","tc":"Classical"},"createdAt":1720136757006,"url":"https://lichess.org/broadcast/greek-team-championship-2024--boards-1-40/wv9ahJeR","tier":3,"dates":[1720102500000,1720595700000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:wv9ahJeR:VlUevU6S.jpg&w=800&sig=7d53f78c2ae858286587919e0719844d343a8eb8","markup":"

    The Greek Team Championship 2024 is a 7-round Swiss for teams, held from the 4th to the 10th of July in Trikala, Greece.

    \n

    Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

    \n

    Official Website | Standings

    \n
    \n

    Photo by Nestoras Argiris on Unsplash

    \n","teamTable":true},"round":{"id":"myEffF4b","name":"Round 6","slug":"round-6","createdAt":1720137200753,"startsAt":1720534500000,"url":"https://lichess.org/broadcast/greek-team-championship-2024--boards-1-40/round-5/TEXHbMwG"},"group":"Greek Team Championship 2024"},{"tour":{"id":"F443vhNo","name":"46th Barberà del Vallès Open 2024","slug":"46th-barbera-del-valles-open-2024","info":{"format":"9-round Swiss","tc":"Classical","players":"Cuartas, Berdayes Ason, Alsina Leal"},"createdAt":1720274091992,"url":"https://lichess.org/broadcast/46th-barbera-del-valles-open-2024/F443vhNo","tier":3,"dates":[1720105200000,1720796400000],"markup":"

    The 46th Barberà del Vallès Open 2024 is a 9-round Swiss, held from the 4th to the 12th of July in Barberà del Vallès, Barcelona, Spain.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website | Standings

    \n"},"round":{"id":"CKW9YIsw","name":"Round 6","slug":"round-6","createdAt":1720274140173,"startsAt":1720537200000,"url":"https://lichess.org/broadcast/46th-barbera-del-valles-open-2024/round-5/XsCOWnCp"}},{"tour":{"id":"r4302nsd","name":"1000GM Independence Day GM Norm II","slug":"1000gm-independence-day-gm-norm-ii","info":{"format":"10-player round-robin","tc":"Classical"},"createdAt":1720337947267,"url":"https://lichess.org/broadcast/1000gm-independence-day-gm-norm-ii/r4302nsd","tier":3,"dates":[1720484100000,1720829700000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=linuxbrickie:relay:r4302nsd:fUxEnBVj.jpg&w=800&sig=ea04e951970b23cbca0242ffc8f687b7ab114c3e","markup":"

    The 1000GM Independence Day GM Norm II is a 10-player round-robin, held from the 8th to the 12th of July in San Jose, California, USA.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment starting from move one.

    \n

    Official Website

    \n"},"round":{"id":"o8NDvvjs","name":"Round 2","slug":"round-2","createdAt":1720338512373,"startsAt":1720548900000,"url":"https://lichess.org/broadcast/1000gm-independence-day-gm-norm-ii/round-1/oPQRIyNj"}},{"tour":{"id":"MRV2q3Yq","name":"ACC Monday Nights 2024 | Winter Cup","slug":"acc-monday-nights-2024--winter-cup","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1718105814905,"url":"https://lichess.org/broadcast/acc-monday-nights-2024--winter-cup/MRV2q3Yq","tier":3,"dates":[1718607600000,1723446000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=iacaster:relay:MRV2q3Yq:Vk5eiu90.jpg&w=800&sig=694fd9118495924e183277e487b619a62b0af671","markup":"

    The ACC Monday Nights 2024 | Winter Cup is a 9-round Swiss, held from the 17th of June to the 12th of August in Auckland, New Zealand.

    \n

    Time control is 75 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website | Results

    \n"},"round":{"id":"mzKINdP8","name":"Round 5","slug":"round-5","createdAt":1718106020711,"startsAt":1721026800000,"url":"https://lichess.org/broadcast/acc-monday-nights-2024--winter-cup/round-4/KGgLx2jQ"},"group":"ACC Monday Nights 2024"},{"tour":{"id":"yuUxbxbH","name":"II IRT do GM Milos","slug":"ii-irt-do-gm-milos","info":{"format":"5-round Swiss","tc":"Classical"},"createdAt":1718658553809,"url":"https://lichess.org/broadcast/ii-irt-do-gm-milos/yuUxbxbH","tier":3,"dates":[1718667000000,1721086200000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=sergioglorias:relay:yuUxbxbH:OkKBHwai.jpg&w=800&sig=cd93d77987db9ee650a714ac01d0e0980b68bccb","markup":"

    The II IRT do GM Milos is a 5-round Swiss tournament, held from the 17th of June to the 15th of July in São Paulo, Brazil.

    \n

    Time control is 60 minutes for the entire game, with a 30-second increment from move 1.

    \n

    Official Website | Results

    \n"},"round":{"id":"uKz9Ifu8","name":"Round 5","slug":"round-5","createdAt":1718660015687,"startsAt":1721086200000,"url":"https://lichess.org/broadcast/ii-irt-do-gm-milos/round-4/3YxAk0fs"}},{"tour":{"id":"vs7L5OPC","name":"Switzerland Team Championships SMM 2024","slug":"switzerland-team-championships-smm-2024","info":{"format":"10-team round-robin","tc":"Classical"},"createdAt":1713748519128,"url":"https://lichess.org/broadcast/switzerland-team-championships-smm-2024/vs7L5OPC","tier":3,"dates":[1710070200000,1728810000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:vs7L5OPC:GKhiEYf1.jpg&w=800&sig=f3c322ce93f23b997cec3c06e54493a48bdb561f","markup":"

    The Switzerland Team Championships SMM 2024 | NLA is a 10-team round-robin competition, held from the 10th of March to the 13th of October in Zurich, Switzerland.

    \n

    Time control is 100 minutes for 40 moves, followed by 50 minutes for the next 20 moves, followed by 15 minutes for the rest of the game, with a 30-second increment from move 1.

    \n

    Official Website | Standings (NLA) | Standings (NLB Ost A) | Standings (NLB Ost B)

    \n","teamTable":true},"round":{"id":"aHBXoEjV","name":"Round 6","slug":"round-6","createdAt":1713748519369,"startsAt":1724495400000,"url":"https://lichess.org/broadcast/switzerland-team-championships-smm-2024/round-5/Ted0iPnO"}}],"upcoming":[{"tour":{"id":"wXto4wTE","name":"Warsaw Chess Festival 2024 | Najdorf Memorial","slug":"warsaw-chess-festival-2024--najdorf-memorial","info":{"format":"9-round Swiss","tc":"Classical","players":"Kazakouski, Krasenkiw, Markowski, Rozentalis"},"createdAt":1720377119509,"url":"https://lichess.org/broadcast/warsaw-chess-festival-2024--najdorf-memorial/wXto4wTE","tier":3,"dates":[1720538100000,1721204100000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=alefzero:relay:wXto4wTE:rdHCIp4Q.jpg&w=800&sig=8e8dad9d2c0ec3c494b4226173d1c14ca1fd0040","markup":"

    The Najdorf Memorial 2024 is a 9-round Swiss, held from the 9th to the 17th of July in Warsaw, Poland.

    \n

    Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

    \n

    Official Website | Standings

    \n
    \n

    23rd international open tournament to commemorate GM Miguel (Mieczysław) Najdorf

    \n"},"round":{"id":"ENq91IQo","name":"Round 1","slug":"round-1","createdAt":1720377162688,"startsAt":1720538100000,"url":"https://lichess.org/broadcast/warsaw-chess-festival-2024--najdorf-memorial/round-1/ENq91IQo"},"group":"Warsaw Chess Festival 2024"},{"tour":{"id":"lb23JlWD","name":"2024 Australian University Chess League","slug":"2024-australian-university-chess-league","info":{"format":"6-team round-robin","tc":"Classical"},"createdAt":1718194400018,"url":"https://lichess.org/broadcast/2024-australian-university-chess-league/lb23JlWD","tier":3,"dates":[1720775700000,1720934100000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=rootyhillcc:relay:lb23JlWD:vBwmZg4h.jpg&w=800&sig=f6ed72005b1790b9bfc351908acad119b5c369ba","markup":"

    The 2024 Australian University Chess League is a 6-team round-robin, held from the 12th to the 14th of July in Sydney, Australia. There are four players per team.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment starting from move one.

    \n","teamTable":true},"round":{"id":"yAulT9F2","name":"Round 1","slug":"round-1","createdAt":1718194461669,"startsAt":1720775700000,"url":"https://lichess.org/broadcast/2024-australian-university-chess-league/round-1/yAulT9F2"}},{"tour":{"id":"GboZ8j0F","name":"IV Open Internacional Vila del Vendrell","slug":"iv-open-internacional-vila-del-vendrell","info":{"format":"9-round Swiss","tc":"Rapid"},"createdAt":1719974387311,"url":"https://lichess.org/broadcast/iv-open-internacional-vila-del-vendrell/GboZ8j0F","tier":3,"dates":[1720855800000,1720889100000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=iacaster:relay:GboZ8j0F:BSyQN0Ff.jpg&w=800&sig=a39542ad349425ce3be423a1b5a1d15ef326e1f9","markup":"

    The IV Open Internacional Vila del Vendrell is a 9-round Swiss, held on 13th of July in El Vendrell, Tarragona, Catalonia, Spain.

    \n

    Time control is 15 minutes for the entire game with a 5-second increment from move 1.

    \n

    Official Website | Results

    \n"},"round":{"id":"LBvFee4s","name":"Round 1","slug":"round-1","createdAt":1719974485751,"startsAt":1720855800000,"url":"https://lichess.org/broadcast/iv-open-internacional-vila-del-vendrell/round-1/LBvFee4s"}},{"tour":{"id":"Es02AbFN","name":"Swiss Individual Championships 2024 | Open","slug":"swiss-individual-championships-2024--open","info":{"format":"10-player round-robin","tc":"Classical"},"createdAt":1717543242414,"url":"https://lichess.org/broadcast/swiss-individual-championships-2024--open/Es02AbFN","tier":3,"dates":[1720871100000,1721546100000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=iacaster:relay:Es02AbFN:Yz0Im12b.jpg&w=800&sig=8e0f71606055775d5ea2006c6f09f2851872823a","markup":"

    The Swiss Individual Championships 2024 | Open is a 10-player round-robin, held from the 13th to the 21st of July in Flims, Switzerland.

    \n

    Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

    \n

    Official Website | Results

    \n"},"round":{"id":"vhvpniLr","name":"Round 1","slug":"round-1","createdAt":1717543458521,"startsAt":1720871100000,"url":"https://lichess.org/broadcast/swiss-individual-championships-2024--open/round-1/vhvpniLr"},"group":"Swiss Individual Championships 2024"},{"tour":{"id":"op7Dy2aB","name":"Zadar Chess Festival 2024","slug":"zadar-chess-festival-2024","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1720373669999,"url":"https://lichess.org/broadcast/zadar-chess-festival-2024/op7Dy2aB","tier":3,"dates":[1721142000000,1721721600000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=coach_goran:relay:op7Dy2aB:SVKZikJd.jpg&w=800&sig=70198997a1e6ce8a8f9ecdce566d2c86cf1bcc2b","markup":"

    The Zadar Chess Festival 2024 Tournament-A is a 9-round Swiss, held from the 16th to the 23rd of July in Zadar, Croatia.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Standings

    \n
    \n

    The Zadar Chess Festival brings chess players from all continents to Zadar. The tournament is divided into two categories, Tournament A and Tournament B. Tournament A is a grandmaster-level event where players compete for master norms, while Tournament B is for amateurs. Attendance is expected to be on par with last year when around 300 players participated. As part of the festival, a blitz championship will also be held, which this year coincides with World Chess Day.

    \n

    Zadar is a beautiful and extremely popular seaside destination visited by numerous tourists from all over the world. Besides its unique beauty, Zadar is a place of culture and heritage, fantastic gastronomy, a well-known sports city, and a city where everyone can find everything for a perfect summer vacation.

    \n

    Zadar Tourist Board

    \n"},"round":{"id":"Lx0mdzh1","name":"Round 1","slug":"round-1","createdAt":1720437555322,"startsAt":1721142000000,"url":"https://lichess.org/broadcast/zadar-chess-festival-2024/round-1/Lx0mdzh1"}},{"tour":{"id":"ilou0biG","name":"3rd Father Cup | Masters","slug":"3rd-father-cup--masters","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1717535598646,"url":"https://lichess.org/broadcast/3rd-father-cup--masters/ilou0biG","tier":3,"dates":[1722169800000,1722666600000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=ali9fazeli:relay:ilou0biG:ni99wVnt.jpg&w=800&sig=795312938ba49ed18f08120aee2efa1eed924764","markup":"

    The 3rd Father Cup | Masters is a 9-round Swiss, held from 28th of July to 3th of August in Rasht, Iran.

    \n

    Time control is 90 minutes for 40 moves, followed by 15 minutes for the rest of the game, with a 30-second increment from move 1.

    \n

    Results

    \n"},"round":{"id":"jcUBMfjn","name":"Round 1","slug":"round-1","createdAt":1717535725866,"startsAt":1722169800000,"url":"https://lichess.org/broadcast/3rd-father-cup--masters/round-1/jcUBMfjn"},"group":"3rd Father Cup"},{"tour":{"id":"nMDYY8rH","name":"28th Créon International 2024 | Main","slug":"28th-creon-international-2024--main","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1720108038852,"url":"https://lichess.org/broadcast/28th-creon-international-2024--main/nMDYY8rH","tier":3,"dates":[1722254400000,1722754800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:nMDYY8rH:kljC7xWa.jpg&w=800&sig=b06f8d53b209c0e4eadd232c24700ced2535b02f","markup":"

    The 28th Créon International 2024 | Main is a 9-round Swiss, held from the 29th of July to the 4th of August in Créon, France.

    \n

    Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

    \n

    Official Website | Standings

    \n"},"round":{"id":"4ONAFGnx","name":"Round 1","slug":"round-1","createdAt":1720108110532,"startsAt":1722254400000,"url":"https://lichess.org/broadcast/28th-creon-international-2024--main/round-1/4ONAFGnx"},"group":"28th Créon International 2024"}],"past":{"currentPage":1,"maxPerPage":20,"currentPageResults":[{"tour":{"id":"7GLYGExC","name":"7th Başkent University Open 2024 | Category A","slug":"7th-baskent-university-open-2024--category-a","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1719997546699,"url":"https://lichess.org/broadcast/7th-baskent-university-open-2024--category-a/7GLYGExC","tier":3,"dates":[1720011600000,1720422000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=okanbilir7:relay:7GLYGExC:XYAyM4VC.jpg&w=800&sig=31c9372a36c05be914612c115c355584617bef62","markup":"

    The 7th Başkent University Open 2024 is a 9-round Swiss, held from the 3rd to the 8th of July in Ankara, Türkiye.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website | Standings

    \n"},"round":{"id":"KYyH44IQ","name":"Round 9","slug":"round-9","createdAt":1720012037962,"finished":true,"startsAt":1720422000000,"url":"https://lichess.org/broadcast/7th-baskent-university-open-2024--category-a/round-9/KYyH44IQ"},"group":"7th Başkent University Open 2024"},{"tour":{"id":"Zpm2BkR3","name":"1000GM Independence Day GM Norm 2024","slug":"1000gm-independence-day-gm-norm-2024","info":{"format":"10-player round-robin","tc":"Classical"},"createdAt":1719922866329,"url":"https://lichess.org/broadcast/1000gm-independence-day-gm-norm-2024/Zpm2BkR3","tier":3,"dates":[1720052100000,1720397700000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:Zpm2BkR3:qTsJhBme.jpg&w=800&sig=d291f09ca0d17c2d5412ccf5f99351451dd433d9","markup":"

    The 1000GM Independence Day GM Norm 2024 is a 10-player round-robin tournament, held from the 3rd to the 7th of July in San Jose, California, USA.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website

    \n","leaderboard":true},"round":{"id":"SlAoLwYT","name":"Round 9","slug":"round-9","createdAt":1719923007464,"finished":true,"startsAt":1720397700000,"url":"https://lichess.org/broadcast/1000gm-independence-day-gm-norm-2024/round-9/SlAoLwYT"}},{"tour":{"id":"eRDPod9B","name":"Marshall Monthly FIDE Premier 2024 | July","slug":"marshall-monthly-fide-premier-2024--july","info":{"format":"5-round Swiss","tc":"Classical"},"createdAt":1720121901223,"url":"https://lichess.org/broadcast/marshall-monthly-fide-premier-2024--july/eRDPod9B","tier":3,"dates":[1720221300000,1720388700000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=sergioglorias:relay:eRDPod9B:ABp4RLul.jpg&w=800&sig=976ad750500d05b752225cb73df7ff01775b90ac","markup":"

    The Marshall Chess Club FIDE Premier July 2024 is a 5-round Swiss, held from the 5th to the 7th of July in New York City, USA.

    \n

    Time control is 90 minutes for the entire game, with a 30-second increment from move 1.

    \n

    Official Website

    \n"},"round":{"id":"v2n1zP96","name":"Round 5","slug":"round-5","createdAt":1720122277391,"finished":true,"startsAt":1720388700000,"url":"https://lichess.org/broadcast/marshall-monthly-fide-premier-2024--july/round-5/v2n1zP96"},"group":"Marshall Monthly FIDE Premier 2024"},{"tour":{"id":"fLqpKaC4","name":"CCA World Open 2024","slug":"cca-world-open-2024","info":{"format":"9-round Swiss","tc":"Classical","players":"Liang, Durarbayli, McShane, Yoo"},"createdAt":1719970723789,"url":"https://lichess.org/broadcast/cca-world-open-2024/fLqpKaC4","tier":4,"dates":[1720048020000,1720386420000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=iacaster:relay:fLqpKaC4:4FYyYcEx.jpg&w=800&sig=883c18f09e16639e631b07c69af344e5a83f1ba1","markup":"

    The CCA World Open 2024 is a 9-round Swiss, held from the 3rd to the 7th of July in Philadelphia, Pennsylvania, USA.

    \n

    Time control is 40 moves in 90 minutes, then 30 minutes, with a 30 second delay after every move.

    \n

    Official Website | Results

    \n
    \n

    Title image photo by Paul Frendach

    \n"},"round":{"id":"uYzumLEp","name":"Round 9","slug":"round-9","createdAt":1719971455782,"finished":true,"startsAt":1720386420000,"url":"https://lichess.org/broadcast/cca-world-open-2024/round-9/uYzumLEp"}},{"tour":{"id":"7t6naO2X","name":"2nd Annual Independence Day Open","slug":"2nd-annual-independence-day-open","info":{"format":"5-round Swiss","tc":"Classical"},"createdAt":1720201395792,"url":"https://lichess.org/broadcast/2nd-annual-independence-day-open/7t6naO2X","tier":3,"dates":[1720221300000,1720379700000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:7t6naO2X:4N8EH5wl.jpg&w=800&sig=d51590b24b9fb8ac5fe204457b54284bc43bcb0d","markup":"

    The 2nd Annual Independence Day Open is a 5-round Swiss, held from the 5th to the 7th of July in Dulles, Virginia, USA.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website

    \n"},"round":{"id":"UNMyETL4","name":"Round 5","slug":"round-5","createdAt":1720201460857,"finished":true,"startsAt":1720379700000,"url":"https://lichess.org/broadcast/2nd-annual-independence-day-open/round-5/UNMyETL4"}},{"tour":{"id":"9Uablwir","name":"1000GM Summer Dual Scheveningen 2024 #3 | Group A","slug":"1000gm-summer-dual-scheveningen-2024-3--group-a","info":{"format":"10-player Semi-Scheveningen","tc":"Classical"},"createdAt":1720124060927,"url":"https://lichess.org/broadcast/1000gm-summer-dual-scheveningen-2024-3--group-a/9Uablwir","tier":3,"dates":[1720199700000,1720372500000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=sergioglorias:relay:9Uablwir:xdjJ2iwL.jpg&w=800&sig=f08151608f83e9a7738e07bd23f7fe05f1db06e4","markup":"

    The 1000GM Summer Dual Scheveningen 2024 #3 | Group A is a 10-player Semi-Scheveningen, held from the 5th to the 7th of July in San Jose, California, USA.

    \n

    Time control is 90 minutes for the entire game, with a 30-second increment from move 1.

    \n

    Official Website

    \n"},"round":{"id":"JxDiKnHY","name":"Round 5","slug":"round-5","createdAt":1720124654816,"finished":true,"startsAt":1720372500000,"url":"https://lichess.org/broadcast/1000gm-summer-dual-scheveningen-2024-3--group-a/round-5/JxDiKnHY"},"group":"1000GM Summer Dual Scheveningen 2024 #3"},{"tour":{"id":"aec1RGgy","name":"Schack-SM 2024 | Sverigemästarklassen","slug":"schack-sm-2024--sverigemastarklassen","info":{"format":"10-player round-robin","tc":"Classical"},"createdAt":1719330577301,"url":"https://lichess.org/broadcast/schack-sm-2024--sverigemastarklassen/aec1RGgy","tier":4,"dates":[1719666000000,1720342800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=claes1981:relay:aec1RGgy:2VfDqH1O.jpg&w=800&sig=dd784c694cd31ea8df2dd4deab2a331379f03e2d","markup":"

    The Swedish Championship week takes place from June 28th to July 7th in Fortnox Arena, Växjö, Sweden. The event includes several sections, which are linked at the bottom.

    \n

    SE

    \n

    Officiell webbplats | Video-kommentering | Resultat och lottning | Livechess PGN

    \n

    Betänketid Sverigemästarklassen: 90 minuter för 40 drag, plus 30 minuter för resten av partiet, plus 30 sekunder per drag från drag ett.

    \n

    Sverigemästarklassen | Mästarklassen-elit | Junior-SM | Mästarklassen | Veteran-SM 50+ | Veteran-SM 65+ | Weekendturneringen I | Klass I-IV | SM-blixten | SM 2023

    \n

    EN

    \n

    Official Website | Video commentary | Results and Pairings | Livechess PGN

    \n

    Time control Swedish Champion Class: 90 minutes for 40 moves, plus 30 minutes for the rest of the game, plus 30 seconds per move from move one.

    \n

    Swedish Champion Class | Elite Masterclass | Swedish Junior Championship | Masterclass | Swedish Senior Championship 50+ | Swedish Senior Championship 65+ | The Weekend Tournament I | Class I-IV | The SM Blitz | 2023

    \n"},"round":{"id":"sJ5sZRMs","name":"Rond 9","slug":"rond-9","createdAt":1719331504689,"finished":true,"startsAt":1720342800000,"url":"https://lichess.org/broadcast/schack-sm-2024--sverigemastarklassen/rond-9/sJ5sZRMs"},"group":"Schack-SM 2024"},{"tour":{"id":"2XEWNHQG","name":"Baku Open 2024 | Group A","slug":"baku-open-2024--group-a","info":{"format":"9-round Swiss","tc":"Classical","players":"Narayanan, Mamedov, Pranav"},"createdAt":1719363025661,"url":"https://lichess.org/broadcast/baku-open-2024--group-a/2XEWNHQG","tier":4,"dates":[1719659700000,1720347300000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:2XEWNHQG:dVkQzLbt.jpg&w=800&sig=d6c75dc5ce69c7d0641e9a5634c658683696b2a5","markup":"

    The Baku Open 2024 is a 9-round Swiss, held from the 29th of June to the 7th of July in Baku, Azerbaijan.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website | Results

    \n
    \n

    Title image photo by Dario Daniel Silva on Unsplash

    \n"},"round":{"id":"TOAPN9Bi","name":"Round 9","slug":"round-9","createdAt":1719363202831,"finished":true,"startsAt":1720347300000,"url":"https://lichess.org/broadcast/baku-open-2024--group-a/round-9/TOAPN9Bi"},"group":"Baku Open 2024"},{"tour":{"id":"Kont9lyt","name":"Spanish U10 Rapid Championship 2024","slug":"spanish-u10-rapid-championship-2024","info":{"format":"9-round Swiss","tc":"Rapid"},"createdAt":1719478161692,"url":"https://lichess.org/broadcast/spanish-u10-rapid-championship-2024/Kont9lyt","tier":3,"dates":[1720278000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=josefeda:relay:Kont9lyt:e7kU6oM9.jpg&w=800&sig=eafa6855e06b5bb948e815534d4212d20f7aa010","markup":"

    The Spanish U10 Rapid Championship 2024 is a 9-round Swiss, held from the 6th to the 7th of July in Salobreña, Granada, Spain.

    \n

    Time control is 10 minutes for the entire game with a 5-second increment from move 1.

    \n

    Official Website | Standings

    \n
    \n

    Campeonato de España Rapido Sub 10 2024

    \n"},"round":{"id":"tIeqLJf0","name":"Ronda 9","slug":"ronda-9","createdAt":1719478541343,"finished":true,"startsAt":1720351800000,"url":"https://lichess.org/broadcast/spanish-u10-rapid-championship-2024/ronda-9/tIeqLJf0"}},{"tour":{"id":"lunItMBB","name":"Saxony-Anhalt Seniors Championships 2024 | 50+","slug":"saxony-anhalt-seniors-championships-2024--50","info":{"format":"7-round Swiss","tc":"Classical"},"createdAt":1719827879431,"url":"https://lichess.org/broadcast/saxony-anhalt-seniors-championships-2024--50/lunItMBB","tier":3,"dates":[1719839700000,1720340100000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:lunItMBB:M2ARPeSq.jpg&w=800&sig=6230cf839867f551e78c70673e839a03162a541b","markup":"

    The Saxony-Anhalt Seniors Championships 2024 | 50+ is a 7-round Swiss, held from the 1st to the 7th of July in Magdeburg, Germany.

    \n

    Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

    \n

    Official Website

    \n"},"round":{"id":"Hh4EwihK","name":"Round 7","slug":"round-7","createdAt":1719827954310,"finished":true,"startsAt":1720340100000,"url":"https://lichess.org/broadcast/saxony-anhalt-seniors-championships-2024--50/round-7/Hh4EwihK"},"group":"Saxony-Anhalt Seniors Championships 2024"},{"tour":{"id":"47N9XRWe","name":"České Budějovice Chess Festival 2024 | GM A","slug":"ceske-budejovice-chess-festival-2024--gm-a","info":{"format":"10-player round-robin","tc":"Classical"},"createdAt":1719327405409,"url":"https://lichess.org/broadcast/ceske-budejovice-chess-festival-2024--gm-a/47N9XRWe","tier":3,"dates":[1719669600000,1720339200000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:47N9XRWe:aOKaYfNt.jpg&w=800&sig=85338600beb8ded4a6165adec640570c8112dd42","markup":"

    The České Budějovice Chess Festival 2024 | GM A is a 10-player round-robin tournament, held from the 29th of June to the 7th of July in České Budějovice, Czech Republic.

    \n

    Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

    \n

    Offiical Website | Standings

    \n
    \n

    Title image photo by Hans Lemuet (Spone), CC BY-SA 3.0, via Wikimedia Commons

    \n","leaderboard":true},"round":{"id":"CjE6k07C","name":"Round 9","slug":"round-9","createdAt":1719327648068,"finished":true,"startsAt":1720339200000,"url":"https://lichess.org/broadcast/ceske-budejovice-chess-festival-2024--gm-a/round-9/CjE6k07C"},"group":"České Budějovice Chess Festival 2024"},{"tour":{"id":"5143V4eE","name":"XXIV Open Internacional d'Escacs de Torredembarra","slug":"xxiv-open-internacional-descacs-de-torredembarra","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1719588450645,"url":"https://lichess.org/broadcast/xxiv-open-internacional-descacs-de-torredembarra/5143V4eE","tier":3,"dates":[1719671400000,1720335600000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=ukkina:relay:5143V4eE:R0iNiocy.jpg&w=800&sig=163d30e58a300f59c5255f3e765e709fe5ccf8c7","markup":"

    The XXIV Open Internacional d'Escacs de Torredembarra is a 9-round Swiss, held from the 29th of June to the 7th of July in
    Torredembarra, Spain.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website | Standings

    \n
    \n

    Del 29 de juny al 7 de juliol de 2024
    ORGANITZA: CLUB D’ESCACS TORREDEMBARRA
    (Integrat al XX Circuit Català d’Oberts Internacionals d’Escacs, classificat amb categoria B, b. (http://www.escacs.cat).

    \n"},"round":{"id":"1x9bhyjf","name":"Round 9","slug":"round-9","createdAt":1719761787694,"finished":true,"startsAt":1720335600000,"url":"https://lichess.org/broadcast/xxiv-open-internacional-descacs-de-torredembarra/round-9/1x9bhyjf"}},{"tour":{"id":"HeOoTDru","name":"All-Ukrainian Festival Morshyn 2024","slug":"all-ukrainian-festival-morshyn-2024","info":{"format":"9-round Swiss","tc":"Rapid"},"createdAt":1720267972743,"url":"https://lichess.org/broadcast/all-ukrainian-festival-morshyn-2024/HeOoTDru","tier":3,"dates":[1720252800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:HeOoTDru:TAcEY8XI.jpg&w=800&sig=729d010e7afad48d86fa086f6176102734f901f5","markup":"

    The All-Ukrainian Festival Morshyn 2024 is a 9-round Swiss, held on the 6th of July in Morshyn, Ukraine.

    \n

    Time control is 10 minutes for the entire game with a 5-second increment from move 1.

    \n

    Standings

    \n
    \n

    Title image photo by ЯдвигаВереск - Own work, CC BY-SA 4.0

    \n"},"round":{"id":"6YRSXzDZ","name":"Round 9","slug":"round-9","createdAt":1720268064062,"finished":true,"startsAt":1720276200000,"url":"https://lichess.org/broadcast/all-ukrainian-festival-morshyn-2024/round-9/6YRSXzDZ"}},{"tour":{"id":"Db0i9sGV","name":"Spanish U10 Championship 2024","slug":"spanish-u10-championship-2024","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1719401439623,"url":"https://lichess.org/broadcast/spanish-u10-championship-2024/Db0i9sGV","tier":3,"dates":[1719820800000,1720252800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=josefeda:relay:Db0i9sGV:TyPuOKoC.jpg&w=800&sig=67728c0d69503809c4f6a137ff382f56ed8d8af7","markup":"

    The Spanish U10 Championship 2024 is a 9-round Swiss, held from the 1st to the 6th of July in Salobreña, Granada, Spain.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website | Standings

    \n
    \n

    Campeonato de España Sub 10 2024

    \n"},"round":{"id":"KYK9G7kE","name":"Ronda 9","slug":"ronda-9","createdAt":1719401772691,"finished":true,"startsAt":1720252800000,"url":"https://lichess.org/broadcast/spanish-u10-championship-2024/ronda-9/KYK9G7kE"}},{"tour":{"id":"BjKO6Jrs","name":"Italian U18 Youth Championships 2024 | U18","slug":"italian-u18-youth-championships-2024--u18","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1719575583240,"url":"https://lichess.org/broadcast/italian-u18-youth-championships-2024--u18/BjKO6Jrs","tier":3,"dates":[1719666900000,1720251900000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:BjKO6Jrs:ztfSSOiP.jpg&w=800&sig=f640d262f4c1545eb47cf716766179385ae51b6e","markup":"

    The Italian U18 Youth Championships 2024 | U18 is a 9-round Swiss, held from the 29th of June to the 6th of July in Salsomaggiore Terme, Italy.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website | Results

    \n"},"round":{"id":"OCMHlRDH","name":"Round 9","slug":"round-9","createdAt":1719575679992,"finished":true,"startsAt":1720251900000,"url":"https://lichess.org/broadcast/italian-u18-youth-championships-2024--u18/round-9/OCMHlRDH"},"group":"Italian U18 Youth Championships 2024"},{"tour":{"id":"hdQQ1Waq","name":"Norwegian Championships 2024 | Elite and Seniors 65+","slug":"norwegian-championships-2024--elite-and-seniors-65","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1719174080786,"url":"https://lichess.org/broadcast/norwegian-championships-2024--elite-and-seniors-65/hdQQ1Waq","tier":4,"dates":[1719591300000,1720253700000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:hdQQ1Waq:K1YsAziL.jpg&w=800&sig=1921f0a71b78530c0a6e8b0e564e19af8b91c499","markup":"

    The Norwegian Championships 2024 | Elite and Seniors 65+ is a 9-round Swiss, held from the 28th of June to the 6th of July in Storefjell, Norway.

    \n

    Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

    \n

    Board 1-9 Elite
    Board 10 - 28 Senior 65+

    \n

    Official Website | Results

    \n
    \n

    Landsturneringen 2024

    \n

    Eliteklassen og Senior 65+

    \n

    Spilles på Storefjell resort hotell 28.06.2024 - 06.07.2024

    \n

    Turneringen spilles over 9 runder, med betenkningstid 90 min på 40 trekk, 30 min på resten av partiet og 30 sek tillegg fra trekk 1

    \n

    Bord 1-9 Eliteklassen
    Bord 10 - 28 Senior 65+

    \n

    Clono partier:
    Mikroputt
    \nhttps://lichess.org/broadcast/nm-i-sjakk-2024-mikroputt/round-1/020oDPUm#boards
    Miniputt
    https://lichess.org/broadcast/nm-i-sjakk-2024-miniputt/round-1/pCvV4G8i#boards
    Lilleputt
    https://lichess.org/broadcast/nm-i-sjakk-2024-lilleputt/round-1/k8GS6LrP
    Junior B
    https://lichess.org/broadcast/nm-i-sjakk-junior-b/round-1/AZhM1hMm
    Klasse 1
    https://lichess.org/broadcast/nm-i-sjakk-2024-klasse-1/round-1/aWw2RwQ1
    Klasse 2
    https://lichess.org/broadcast/nm-i-sjakk-2024-klasse-2/round-1/Mnxw76OR
    Klasse 3
    https://lichess.org/broadcast/nmi-sjakk-2024-klasse-3/round-1/ZheSrANG
    Klasse 4
    https://lichess.org/broadcast/nm-i-sjakk-klasse-4/round-1/X673vUlD
    Klasse 5
    https://lichess.org/broadcast/nm-i-sjakk-2024-klasse-5/round-1/C6m3qitn
    Klasse Mester
    https://lichess.org/broadcast/nm-i-sjakk-2024-mesterklassen/round-2/lZu3t3A7#boards

    \n"},"round":{"id":"LQn45rIa","name":"Round 9","slug":"round-9","createdAt":1719175255813,"finished":true,"startsAt":1720253700000,"url":"https://lichess.org/broadcast/norwegian-championships-2024--elite-and-seniors-65/round-9/LQn45rIa"},"group":"Norwegian Championships 2024"},{"tour":{"id":"K1NfeoWE","name":"Superbet Romania Chess Classic 2024","slug":"superbet-romania-chess-classic-2024","info":{"format":"10-player Round Robin","tc":"Classical","players":"Caruana, Nepomniachtchi, Gukesh, Giri"},"createdAt":1719187354944,"url":"https://lichess.org/broadcast/superbet-romania-chess-classic-2024/K1NfeoWE","tier":5,"dates":[1719405000000,1720198800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=iacaster:relay:K1NfeoWE:6kBI06CJ.jpg&w=800&sig=c6b93e8db217a6bb504dcb5e0695337a472655b7","markup":"

    The Superbet Romania Chess Classic 2024 is a 10-player Round Robin, held from the 26th of June to the 5th of July in Bucharest, Romania.

    \n

    Time control is 120 minutes for the entire game, plus a 30-second increment per move.

    \n

    Superbet Chess Classic Romania is the first of two classical events, this tournament will feature a 10-player round robin with nine tour regulars, Caruana, Nepomniachtchi, Abdusattorov, Gukesh, So, Praggnanandhaa, Giri, Firouzja, Vachier-Lagrave, and one wildcard, local Romanian favorite Bogdan-Daniel Deac.

    \n

    Official Website | Results

    \n
    \n

    In the event of a tie for 1st place, a double round-robin will be played with 2 players, or a single round-robin will be played with 3 or more players. Time control is 10 minutes for the entire game with a 5-second increment from move 1.

    \n

    In the event of another tie, knockout armageddon games will be played. Time control is 5 minutes for White, 4 minutes for Black, with a 2-second increment from move 61.

    \n
    \n

    Grand Chess Tour | Tour Standings
    2024 Superbet Poland Rapid & Blitz
    2024 Superbet Romania Chess Classic
    2024 Superunited Croatia Rapid & Blitz

    \n
    \n

    Title image photo by Arvid Olson from Pixabay

    \n","leaderboard":true},"round":{"id":"QC9QC8Lr","name":"Tiebreaks","slug":"tiebreaks","createdAt":1720197015416,"finished":true,"startsAt":1720198800000,"url":"https://lichess.org/broadcast/superbet-romania-chess-classic-2024/tiebreaks/QC9QC8Lr"}},{"tour":{"id":"ZmFLmrss","name":"III Magistral Internacional Ciudad de Sant Joan de Alacant","slug":"iii-magistral-internacional-ciudad-de-sant-joan-de-alacant","info":{"format":"10-player round-robin","tc":"Classical"},"createdAt":1719791889764,"url":"https://lichess.org/broadcast/iii-magistral-internacional-ciudad-de-sant-joan-de-alacant/ZmFLmrss","tier":3,"dates":[1719820800000,1720162800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:ZmFLmrss:2RKhpn3T.jpg&w=800&sig=8be9210de5ef07975dbfb9d300fffc3ad1e38ecc","markup":"

    The III Magistral Internacional Ciudad de Sant Joan de Alacant is a 10-player round-robin tournament, held from the 1st to the 5th of July in Sant Joan d'Alacant, Spain.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website | Standings

    \n","leaderboard":true},"round":{"id":"MIy50UWQ","name":"Round 9","slug":"round-9","createdAt":1719791991550,"finished":true,"startsAt":1720162800000,"url":"https://lichess.org/broadcast/iii-magistral-internacional-ciudad-de-sant-joan-de-alacant/round-9/MIy50UWQ"}},{"tour":{"id":"fQu6hjlI","name":"1000GM Summer Dual Scheveningen 2024 #2 | Group A","slug":"1000gm-summer-dual-scheveningen-2024-2--group-a","info":{"format":"10-player Semi-Scheveningen","tc":"Classical"},"createdAt":1719922442023,"url":"https://lichess.org/broadcast/1000gm-summer-dual-scheveningen-2024-2--group-a/fQu6hjlI","tier":3,"dates":[1719940500000,1720113300000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:fQu6hjlI:mVZ0X3CV.jpg&w=800&sig=1d2b55c6d0cac0ceda72a4fbc83e837a45006b9e","markup":"

    The 1000GM Summer Dual Scheveningen 2024 #2 | Group A is a 10-player Semi-Scheveningen, held from the 2nd to the 4th of July in San Jose, California, USA.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website

    \n","leaderboard":true},"round":{"id":"D5IvvZGj","name":"Round 5","slug":"round-5","createdAt":1719922517399,"finished":true,"startsAt":1720113300000,"url":"https://lichess.org/broadcast/1000gm-summer-dual-scheveningen-2024-2--group-a/round-5/D5IvvZGj"},"group":"1000GM Summer Dual Scheveningen 2024 #2"},{"tour":{"id":"4ERHDodE","name":"Atlantic Chess Independence Day GM Norm Invitational","slug":"atlantic-chess-independence-day-gm-norm-invitational","info":{"format":"10-player round-robin","tc":"Classical","players":"Erenburg, Plát, Barbosa, Gauri"},"createdAt":1719693938025,"url":"https://lichess.org/broadcast/atlantic-chess-independence-day-gm-norm-invitational/4ERHDodE","tier":3,"dates":[1719695700000,1720098900000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:4ERHDodE:l3iVV7Ym.jpg&w=800&sig=d57277f927849426413b3b26fccacbad1027ae51","markup":"

    The Atlantic Chess Independence Day GM Norm Invitational is a 10-player round-robin tournament, held from the 29th of June to the 4th of July in Dulles, Virginia, USA.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website | Standings

    \n
    \n

    The Atlantic Chess Association is organizing the Independence Day Norm Tournament. It is a 6 day, 9 rounds, 10 player Round Robin tournament.

    \n

    Chief Arbiter: IA Gregory Vaserstein

    \n

    Venue: Hampton Inn & Suites Washington-Dulles International Airport (4050 Westfax Dr., Chantilly, VA 20151)

    \n","leaderboard":true},"round":{"id":"PVG8wijk","name":"Round 9","slug":"round-9","createdAt":1719698677225,"finished":true,"startsAt":1720098900000,"url":"https://lichess.org/broadcast/atlantic-chess-independence-day-gm-norm-invitational/round-9/PVG8wijk"}}],"previousPage":null,"nextPage":2}} +{"active":[{"tour":{"id":"ioLNPN8j","name":"2nd Rustam Kasimdzhanov Cup 2024","slug":"2nd-rustam-kasimdzhanov-cup-2024","info":{"format":"10-player round-robin","tc":"Rapid","players":"Abdusattorov, Rapport, Mamedyarov, Grischuk"},"createdAt":1720352380417,"url":"https://lichess.org/broadcast/2nd-rustam-kasimdzhanov-cup-2024/ioLNPN8j","tier":5,"dates":[1720431600000,1720519800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=uzchess23:relay:ioLNPN8j:ya35G192.jpg&w=800&sig=b6543625b806cf43f4ee652d0a23d80fad236b35","markup":"

    The 2nd International Rustam Kasimdzhanov Cup 2024 is a 10-player round-robin tournament, held from the 8th to the 9th of July in Tashkent, Uzbekistan.

    \n

    Time control is 15 minutes for the entire game with a 10-second increment from move 1.

    \n

    Standings

    \n","leaderboard":true},"round":{"id":"A4J7qTO6","name":"Round 8","slug":"round-8","createdAt":1720438046707,"ongoing":true,"startsAt":1720516500000,"url":"https://lichess.org/broadcast/2nd-rustam-kasimdzhanov-cup-2024/round-8/A4J7qTO6"}},{"tour":{"id":"a4gBsu31","name":"Dutch Championship 2024 | Open","slug":"dutch-championship-2024--open","info":{"format":"10-player knockout","tc":"Classical","players":"Warmerdam, l’Ami, Bok, Sokolov"},"createdAt":1720037021926,"url":"https://lichess.org/broadcast/dutch-championship-2024--open/a4gBsu31","tier":4,"dates":[1720263600000,1720882800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:a4gBsu31:OgaRY7Pw.jpg&w=800&sig=cb141524b135d0cdc45deafcb9b4bfffc805ecee","markup":"

    The Dutch Championship 2024 | Open is a 10-player single-elimination knockout tournament, held from the 6th to the 13th of July in Utrecht, the Netherlands.

    \n

    Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

    \n

    Official Website | Results

    \n
    \n

    If a round ends in a tie after 2 classical games, a tiebreak match of 2 blitz games is played. Time control is 4+2.

    \n

    If the first tiebreak match ends in another tie, a second tiebreak match of 2 blitz games with reversed colours is played. Time control is 4+2.

    \n

    If the second tiebreak match ends in another tie, colours are drawn and a sudden death is played. Time control is 4+2 for White and 5+2 for Black. The first player to win a game, wins the round. After every 2 games, the colour order is changed.

    \n"},"round":{"id":"Xfe00Awr","name":"Quarter-Finals | Game 2","slug":"quarter-finals--game-2","createdAt":1720037148839,"ongoing":true,"startsAt":1720522800000,"url":"https://lichess.org/broadcast/dutch-championship-2024--open/quarter-finals--game-2/Xfe00Awr"},"group":"Dutch Championship 2024"},{"tour":{"id":"aPC3ATVG","name":"FIDE World Senior Team Chess Championships 2024 | 50+","slug":"fide-world-senior-team-chess-championships-2024--50","info":{"format":"9-round Swiss for teams","tc":"Classical","players":"Adams, Ehlvest, David, Novikov"},"createdAt":1719921457211,"url":"https://lichess.org/broadcast/fide-world-senior-team-chess-championships-2024--50/aPC3ATVG","tier":4,"dates":[1719926100000,1720685700000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=mansuba64:relay:aPC3ATVG:xuiZWY67.jpg&w=800&sig=bfc2aa87dce4ed7bdfb5ce5b9f16285e23479f05","markup":"

    The FIDE World Senior Team Chess Championships 2024 | 50+ is a 9-round Swiss for teams, held from the 2nd to the 11th of July in Kraków, Poland.

    \n

    Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

    \n

    Official Website | Standings

    \n
    \n

    There shall be two categories; Open age 50+ and Open age 65+ with separate events for women.
    The player must have reached or reach the required age during the year of competition.
    There shall be separate Women’s Championship(s) if there are at least ten teams from at least two continents. Otherwise women’s teams play in Open competition
    The Championships are open tournaments for teams registered by their federation. FIDE member federations shall have the right to send as many teams as they wish.

    \n

    The winning team obtains the title “World Team Champion “age 50+ (or age 65+)”.
    The best placed women team obtains the title “World Women Team Champion” age 50+ (or age 65+).

    \n

    Prize Fund: 10,000 EUR

    \n","teamTable":true},"round":{"id":"YIw910wS","name":"Round 7","slug":"round-7","createdAt":1719928673349,"startsAt":1720530900000,"url":"https://lichess.org/broadcast/fide-world-senior-team-chess-championships-2024--50/round-6/Gue2qJfw"},"group":"FIDE World Senior Team Chess Championships 2024"},{"tour":{"id":"tCMfpIJI","name":"43rd Villa de Benasque Open 2024","slug":"43rd-villa-de-benasque-open-2024","info":{"format":"10-round Swiss","tc":"Classical","players":"Alekseenko, Bartel, Pichot"},"createdAt":1719422556116,"url":"https://lichess.org/broadcast/43rd-villa-de-benasque-open-2024/tCMfpIJI","tier":4,"dates":[1720189800000,1720941300000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=kike0:relay:tCMfpIJI:P6c1Rrxn.jpg&w=800&sig=69d4e6158f133578bbb35519346e4395d891ca2c","markup":"

    The 43rd Villa de Benasque Open 2024 is a 10-round Swiss, held from the 5th to the 14th of July in Benasque, Spain.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move one.

    \n

    GM Kirill Alekseenko is the tournament's top seed - with nearly 100 titled players, 500 players in total, and over €50,000 in prizes.

    \n

    Official Website | Standings

    \n
    \n

    El XLIII Open Internacional Villa de Benasque se disputará por el Sistema Suizo a 10 rondas, del 5 al 14 de Julio de 2024. El GM Alekseenko lidera un ranking con cerca de 100 titulados, 500 jugadores y más de 50.000 euros de premios en metálico. El local de juego será el Pabellón Polideportivo de Benasque (España).

    \n

    El ritmo de juego será de 90 minutos + 30 segundos de incremento acumulativo por jugada empezando desde la primera.

    \n

    Web Oficial | Chess-Results

    \n"},"round":{"id":"SXAjWw0G","name":"Round 5","slug":"round-5","createdAt":1719422658882,"startsAt":1720534500000,"url":"https://lichess.org/broadcast/43rd-villa-de-benasque-open-2024/round-4/she3bD2w"}},{"tour":{"id":"yOuW4siY","name":"Spanish U12 Championships 2024 | Classical","slug":"spanish-u12-championships-2024--classical","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1720081884293,"url":"https://lichess.org/broadcast/spanish-u12-championships-2024--classical/yOuW4siY","tier":3,"dates":[1720425600000,1720857600000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=josefeda:relay:yOuW4siY:FCsABLhH.jpg&w=800&sig=ca8faadb2725de80ab6a316e98b8505f1f620f71","markup":"

    The Spanish U12 Championship 2024 is a 9-round Swiss, held from the 6th to the 7th of July in Salobreña, Granada, Spain.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website | Standings

    \n
    \n

    Campeonato de España Sub 12 2024

    \n"},"round":{"id":"eCa2CbqM","name":"Ronda 3","slug":"ronda-3","createdAt":1720082094252,"ongoing":true,"startsAt":1720512000000,"url":"https://lichess.org/broadcast/spanish-u12-championships-2024--classical/ronda-3/eCa2CbqM"},"group":"Spanish U12 Championships 2024"},{"tour":{"id":"JQGYmn68","name":"Scottish Championship International Open 2024","slug":"scottish-championship-international-open-2024","info":{"format":"9-round Swiss","tc":"90+30"},"createdAt":1720440336101,"url":"https://lichess.org/broadcast/scottish-championship-international-open-2024/JQGYmn68","tier":3,"dates":[1720447200000,1720965600000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=prospect_d:relay:JQGYmn68:I58xFyHC.jpg&w=800&sig=ccd5889235ee538ce022dcc5ff8ed1568e5f4377","markup":"

    The Scottish Championship International Open 2024 is a 9-round Swiss, held from the 8th to the 14th of July in Dunfermline, Scotland.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Standings

    \n"},"round":{"id":"Nw190iGM","name":"Round 2","slug":"round-2","createdAt":1720451119128,"ongoing":true,"startsAt":1720515600000,"url":"https://lichess.org/broadcast/scottish-championship-international-open-2024/round-2/Nw190iGM"}},{"tour":{"id":"YBTYQbxm","name":"South Wales International Open 2024","slug":"south-wales-international-open-2024","info":{"format":"9-round Swiss","tc":"Classical","players":"Chatalbashev, Cuenca, Grieve, Han"},"createdAt":1720127613709,"url":"https://lichess.org/broadcast/south-wales-international-open-2024/YBTYQbxm","tier":3,"dates":[1720170000000,1720602000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:YBTYQbxm:BDfr290h.jpg&w=800&sig=2e914768c4d3781264493309d8e37c86221c8ac7","markup":"

    The South Wales International Open 2024 is a 9-round Swiss title norm tournament taking place in Bridgend, Wales from the 5th to the 10th of July.

    \n

    Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

    \n

    Official Website | Standings

    \n","leaderboard":true},"round":{"id":"Svyiq7jS","name":"Round 7","slug":"round-7","createdAt":1720127909656,"ongoing":true,"startsAt":1720515600000,"url":"https://lichess.org/broadcast/south-wales-international-open-2024/round-7/Svyiq7jS"}},{"tour":{"id":"BgVqV6b0","name":"Koege Open 2024","slug":"koege-open-2024","info":{"format":"10-player round-robin","tc":"Classical","players":"Petrov, Smith, Hector"},"createdAt":1720361492349,"url":"https://lichess.org/broadcast/koege-open-2024/BgVqV6b0","tier":3,"dates":[1720512300000,1720944300000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=fishdefend:relay:BgVqV6b0:kr4GaRvW.jpg&w=800&sig=b2f59c1ae2cb70a88c8e7872b1327b93c64cdad3","markup":"

    The Koege Open 2024 is a 10-player round-robin tournament, held from the 9th to the 14th of July in Køge, Denmark.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Standings

    \n
    \n

    Group 1: Boards 1-5
    Group 2: Boards 6-10

    \n"},"round":{"id":"y0ksveWZ","name":"Round 1","slug":"round-1","createdAt":1720460080111,"ongoing":true,"startsAt":1720512300000,"url":"https://lichess.org/broadcast/koege-open-2024/round-1/y0ksveWZ"}},{"tour":{"id":"XV3jpD1b","name":"Belgian Championship 2024 | Expert","slug":"belgian-championship-2024--expert","info":{"format":"10-player round-robin","tc":"Classical"},"createdAt":1720276555430,"url":"https://lichess.org/broadcast/belgian-championship-2024--expert/XV3jpD1b","tier":3,"dates":[1720267200000,1720944000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=sergioglorias:relay:XV3jpD1b:BkW8q64n.jpg&w=800&sig=017b9b9cf354268158da36eba185e961d2cfc5e3","markup":"

    The Belgian Championship 2024 | Expert is a 10-player round-robin tournament, held from the 6th to the 14th of July in Lier, Belgium.

    \n

    The winner will be crowned Belgian Chess Champion 2024.

    \n

    Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

    \n

    Official Website

    \n","leaderboard":true},"round":{"id":"iSD0HAuQ","name":"Round 4","slug":"round-4","createdAt":1720276601311,"ongoing":true,"startsAt":1720526400000,"url":"https://lichess.org/broadcast/belgian-championship-2024--expert/round-4/iSD0HAuQ"},"group":"Belgian Championship 2024"},{"tour":{"id":"oo69aO3w","name":"SAIF Powertec Bangladesh Championship 2024","slug":"saif-powertec-bangladesh-championship-2024","info":{"format":"14-player round-robin","tc":"Classical"},"createdAt":1719050225145,"url":"https://lichess.org/broadcast/saif-powertec-bangladesh-championship-2024/oo69aO3w","tier":3,"dates":[1719133200000,1720602000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=fathirahman:relay:oo69aO3w:o2ATDvXh.jpg&w=800&sig=f93c0ea42a7223efb8efdc4e245acca39962b9f3","markup":"

    The SAIF Powertec Bangladesh Championship 2024 is a 14-player round-robin tournament, held from the 23rd of June to the 6th of July in Dhaka, Bangladesh.

    \n

    Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

    \n

    Official Website | Results

    \n
    \n

    SAIF Powertec 48th Bangladesh National Chess Championship 2024 is Bangladesh's national chess championship. Top 5 players are:

    \n
      \n
    1. FM Manon, Reja Neer 2445
    2. \n
    3. IM Mohammad Fahad, Rahman 2437
    4. \n
    5. GM Rahman, Ziaur 2423
    6. \n
    7. GM Hossain, Enamul 2365
    8. \n
    9. GM Murshed, Niaz 2317
    10. \n
    \n

    The previous series (2022) champion is GM Enamul Hossain. This is a 14-player round-robin tournament, where 3 GMs have been invited to play directly, and 11 players are from the top 11 of the qualifying round, known as the National B Championship.

    \n

    Five GMs were invited, but only three accepted the invitation. Therefore, instead of taking 9 players from National B, 11 players qualified to fulfill the round requirements.

    \n

    The top 5 players qualify for the Olympiad team.

    \n

    Here are useful links:

    \n\n"},"round":{"id":"LLqfCDm6","name":"Round 12 (Postponed)","slug":"round-12-postponed","createdAt":1719050487028,"ongoing":true,"startsAt":1720515600000,"url":"https://lichess.org/broadcast/saif-powertec-bangladesh-championship-2024/round-12-postponed/LLqfCDm6"}},{"tour":{"id":"Qag4N0cA","name":"4th La Plagne Festival 2024","slug":"4th-la-plagne-festival-2024","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1720274920410,"url":"https://lichess.org/broadcast/4th-la-plagne-festival-2024/Qag4N0cA","tier":3,"dates":[1720278000000,1720771200000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:Qag4N0cA:v3g6RfVf.jpg&w=800&sig=78b9c7d33a747e3c20fc26fd5360d07c9546debc","markup":"

    The 4th La Plagne International Chess Festival is a 9-round Swiss, held from the 6th to the 12th of July at La Plagne in Savoie, France.

    \n

    Time control is is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

    \n

    Official Website | Standings

    \n"},"round":{"id":"0qufdZnF","name":"Round 5","slug":"round-5","createdAt":1720286940762,"startsAt":1720531800000,"url":"https://lichess.org/broadcast/4th-la-plagne-festival-2024/round-4/gQt8ubbC"}},{"tour":{"id":"95l4pho3","name":"Peruvian Championship Finals 2024 | Open","slug":"peruvian-championship-finals-2024--open","info":{"format":"12-player round-robin","tc":"Classical","players":"Terry, Flores Quillas, Leiva"},"createdAt":1720272022000,"url":"https://lichess.org/broadcast/peruvian-championship-finals-2024--open/95l4pho3","tier":3,"dates":[1720278000000,1720796400000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:95l4pho3:mDrHsa8C.jpg&w=800&sig=0d81273752b2bcdd47180ae23201f15074a91be9","markup":"

    The Peruvian Championship Finals 2024 | Open is a 12-player round-robin tournament, held from the 6th to the 12th of July in Lima, Peru.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Standings

    \n","leaderboard":true},"round":{"id":"Pi0HtFDs","name":"Round 6","slug":"round-6","createdAt":1720272103102,"startsAt":1720537200000,"url":"https://lichess.org/broadcast/peruvian-championship-finals-2024--open/round-5/JuIghW2d"},"group":"Peruvian Championship Finals 2024"},{"tour":{"id":"85buXS8z","name":"2024 Sydney Championships | Open","slug":"2024-sydney-championships--open","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1713001604469,"url":"https://lichess.org/broadcast/2024-sydney-championships--open/85buXS8z","tier":3,"dates":[1720226700000,1720570500000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=rootyhillcc:relay:85buXS8z:GSmVFAej.jpg&w=800&sig=5319f37a9eb1bdd5f399316b507522048594d7ed","markup":"

    The 2024 Sydney Championships | Open is a 9-round Swiss, held from the 6th to the 10th in Sydney, Australia.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website | Results

    \n"},"round":{"id":"FxnR92Ll","name":"Round 9","slug":"round-9","createdAt":1720514826452,"startsAt":1720570500000,"url":"https://lichess.org/broadcast/2024-sydney-championships--open/round-8/GPbAETkc"},"group":"2024 Sydney Championships"},{"tour":{"id":"s7YVTwll","name":"United Arab Emirates Championship 2024 | Open","slug":"united-arab-emirates-championship-2024--open","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1720095141515,"url":"https://lichess.org/broadcast/united-arab-emirates-championship-2024--open/s7YVTwll","tier":3,"dates":[1720011600000,1720614600000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:s7YVTwll:t67JoGYK.jpg&w=800&sig=026a374d1ceff3c19522d12949c8ae28cd9e5ac6","markup":"

    The United Arab Emirates Championship 2024 | Open is a 9-round Swiss, held from the 3rd to the 10th of July in Dubai, United Arab Emirates.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Standings

    \n"},"round":{"id":"12JAmxw6","name":"Round 8","slug":"round-8","createdAt":1720095220069,"startsAt":1720530000000,"url":"https://lichess.org/broadcast/united-arab-emirates-championship-2024--open/round-8/12JAmxw6"},"group":"United Arab Emirates Championship 2024"},{"tour":{"id":"6s43vSQx","name":"Satranc Arena IM Chess Tournament Series - 5","slug":"satranc-arena-im-chess-tournament-series-5","info":{"format":"6-player double round-robin","tc":"Classical"},"createdAt":1720442634682,"url":"https://lichess.org/broadcast/satranc-arena-im-chess-tournament-series-5/6s43vSQx","tier":3,"dates":[1720425600000,1720792800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=arbiter_ubh:relay:6s43vSQx:1g42zUbN.jpg&w=800&sig=2031b1d31739c7d4cfe505cbd396b0d3bb44dce0","markup":"

    The Satranc Arena IM Chess Tournament Series - 5 is a 6-player double round-robin, held from the 8th to the 12th of July in Güzelbahçe, İzmir, Türkiye.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Standings

    \n","leaderboard":true},"round":{"id":"NOVf9rXm","name":"Round 4","slug":"round-4","createdAt":1720442689924,"startsAt":1720533600000,"url":"https://lichess.org/broadcast/satranc-arena-im-chess-tournament-series-5/round-3/WoVzBwaJ"}},{"tour":{"id":"veT0PjZv","name":"Paraćin Open 2024","slug":"paracin-open-2024","info":{"format":"9-round Swiss","tc":"Classical","players":"Safarli, Fier, Sasikiran, Prohászka"},"createdAt":1719958223829,"url":"https://lichess.org/broadcast/paracin-open-2024/veT0PjZv","tier":3,"dates":[1720015200000,1720683000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:veT0PjZv:hPF40XDY.jpg&w=800&sig=22712721714425152122d47a0017eb9cc9f8a8cb","markup":"

    The Paraćin Open 2024 is a 9-round Swiss, held from the 3rd to the 11th of July in Paraćin, Serbia.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website | Standings

    \n"},"round":{"id":"A81Fjh6K","name":"Round 7","slug":"round-7","createdAt":1719958344863,"startsAt":1720533600000,"url":"https://lichess.org/broadcast/paracin-open-2024/round-6/2m0ylraL"}},{"tour":{"id":"wv9ahJeR","name":"Greek Team Championship 2024 | Boards 1-40","slug":"greek-team-championship-2024--boards-1-40","info":{"format":"7-round Swiss for teams","tc":"Classical"},"createdAt":1720136757006,"url":"https://lichess.org/broadcast/greek-team-championship-2024--boards-1-40/wv9ahJeR","tier":3,"dates":[1720102500000,1720595700000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:wv9ahJeR:VlUevU6S.jpg&w=800&sig=7d53f78c2ae858286587919e0719844d343a8eb8","markup":"

    The Greek Team Championship 2024 is a 7-round Swiss for teams, held from the 4th to the 10th of July in Trikala, Greece.

    \n

    Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

    \n

    Official Website | Standings

    \n
    \n

    Photo by Nestoras Argiris on Unsplash

    \n","teamTable":true},"round":{"id":"myEffF4b","name":"Round 6","slug":"round-6","createdAt":1720137200753,"startsAt":1720534500000,"url":"https://lichess.org/broadcast/greek-team-championship-2024--boards-1-40/round-5/TEXHbMwG"},"group":"Greek Team Championship 2024"},{"tour":{"id":"F443vhNo","name":"46th Barberà del Vallès Open 2024","slug":"46th-barbera-del-valles-open-2024","info":{"format":"9-round Swiss","tc":"Classical","players":"Cuartas, Berdayes Ason, Alsina Leal"},"createdAt":1720274091992,"url":"https://lichess.org/broadcast/46th-barbera-del-valles-open-2024/F443vhNo","tier":3,"dates":[1720105200000,1720796400000],"markup":"

    The 46th Barberà del Vallès Open 2024 is a 9-round Swiss, held from the 4th to the 12th of July in Barberà del Vallès, Barcelona, Spain.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website | Standings

    \n"},"round":{"id":"CKW9YIsw","name":"Round 6","slug":"round-6","createdAt":1720274140173,"startsAt":1720537200000,"url":"https://lichess.org/broadcast/46th-barbera-del-valles-open-2024/round-5/XsCOWnCp"}},{"tour":{"id":"r4302nsd","name":"1000GM Independence Day GM Norm II","slug":"1000gm-independence-day-gm-norm-ii","info":{"format":"10-player round-robin","tc":"Classical"},"createdAt":1720337947267,"url":"https://lichess.org/broadcast/1000gm-independence-day-gm-norm-ii/r4302nsd","tier":3,"dates":[1720484100000,1720829700000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=linuxbrickie:relay:r4302nsd:fUxEnBVj.jpg&w=800&sig=ea04e951970b23cbca0242ffc8f687b7ab114c3e","markup":"

    The 1000GM Independence Day GM Norm II is a 10-player round-robin, held from the 8th to the 12th of July in San Jose, California, USA.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment starting from move one.

    \n

    Official Website

    \n"},"round":{"id":"o8NDvvjs","name":"Round 2","slug":"round-2","createdAt":1720338512373,"startsAt":1720548900000,"url":"https://lichess.org/broadcast/1000gm-independence-day-gm-norm-ii/round-1/oPQRIyNj"}},{"tour":{"id":"MRV2q3Yq","name":"ACC Monday Nights 2024 | Winter Cup","slug":"acc-monday-nights-2024--winter-cup","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1718105814905,"url":"https://lichess.org/broadcast/acc-monday-nights-2024--winter-cup/MRV2q3Yq","tier":3,"dates":[1718607600000,1723446000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=iacaster:relay:MRV2q3Yq:Vk5eiu90.jpg&w=800&sig=694fd9118495924e183277e487b619a62b0af671","markup":"

    The ACC Monday Nights 2024 | Winter Cup is a 9-round Swiss, held from the 17th of June to the 12th of August in Auckland, New Zealand.

    \n

    Time control is 75 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website | Results

    \n"},"round":{"id":"mzKINdP8","name":"Round 5","slug":"round-5","createdAt":1718106020711,"startsAt":1721026800000,"url":"https://lichess.org/broadcast/acc-monday-nights-2024--winter-cup/round-4/KGgLx2jQ"},"group":"ACC Monday Nights 2024"},{"tour":{"id":"yuUxbxbH","name":"II IRT do GM Milos","slug":"ii-irt-do-gm-milos","info":{"format":"5-round Swiss","tc":"Classical"},"createdAt":1718658553809,"url":"https://lichess.org/broadcast/ii-irt-do-gm-milos/yuUxbxbH","tier":3,"dates":[1718667000000,1721086200000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=sergioglorias:relay:yuUxbxbH:OkKBHwai.jpg&w=800&sig=cd93d77987db9ee650a714ac01d0e0980b68bccb","markup":"

    The II IRT do GM Milos is a 5-round Swiss tournament, held from the 17th of June to the 15th of July in São Paulo, Brazil.

    \n

    Time control is 60 minutes for the entire game, with a 30-second increment from move 1.

    \n

    Official Website | Results

    \n"},"round":{"id":"uKz9Ifu8","name":"Round 5","slug":"round-5","createdAt":1718660015687,"startsAt":1721086200000,"url":"https://lichess.org/broadcast/ii-irt-do-gm-milos/round-4/3YxAk0fs"}},{"tour":{"id":"vs7L5OPC","name":"Switzerland Team Championships SMM 2024","slug":"switzerland-team-championships-smm-2024","info":{"format":"10-team round-robin","tc":"Classical"},"createdAt":1713748519128,"url":"https://lichess.org/broadcast/switzerland-team-championships-smm-2024/vs7L5OPC","tier":3,"dates":[1710070200000,1728810000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:vs7L5OPC:GKhiEYf1.jpg&w=800&sig=f3c322ce93f23b997cec3c06e54493a48bdb561f","markup":"

    The Switzerland Team Championships SMM 2024 | NLA is a 10-team round-robin competition, held from the 10th of March to the 13th of October in Zurich, Switzerland.

    \n

    Time control is 100 minutes for 40 moves, followed by 50 minutes for the next 20 moves, followed by 15 minutes for the rest of the game, with a 30-second increment from move 1.

    \n

    Official Website | Standings (NLA) | Standings (NLB Ost A) | Standings (NLB Ost B)

    \n","teamTable":true},"round":{"id":"aHBXoEjV","name":"Round 6","slug":"round-6","createdAt":1713748519369,"startsAt":1724495400000,"url":"https://lichess.org/broadcast/switzerland-team-championships-smm-2024/round-5/Ted0iPnO"}}],"past":{"currentPage":1,"maxPerPage":20,"currentPageResults":[{"tour":{"id":"7GLYGExC","name":"7th Başkent University Open 2024 | Category A","slug":"7th-baskent-university-open-2024--category-a","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1719997546699,"url":"https://lichess.org/broadcast/7th-baskent-university-open-2024--category-a/7GLYGExC","tier":3,"dates":[1720011600000,1720422000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=okanbilir7:relay:7GLYGExC:XYAyM4VC.jpg&w=800&sig=31c9372a36c05be914612c115c355584617bef62","markup":"

    The 7th Başkent University Open 2024 is a 9-round Swiss, held from the 3rd to the 8th of July in Ankara, Türkiye.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website | Standings

    \n"},"round":{"id":"KYyH44IQ","name":"Round 9","slug":"round-9","createdAt":1720012037962,"finished":true,"startsAt":1720422000000,"url":"https://lichess.org/broadcast/7th-baskent-university-open-2024--category-a/round-9/KYyH44IQ"},"group":"7th Başkent University Open 2024"},{"tour":{"id":"Zpm2BkR3","name":"1000GM Independence Day GM Norm 2024","slug":"1000gm-independence-day-gm-norm-2024","info":{"format":"10-player round-robin","tc":"Classical"},"createdAt":1719922866329,"url":"https://lichess.org/broadcast/1000gm-independence-day-gm-norm-2024/Zpm2BkR3","tier":3,"dates":[1720052100000,1720397700000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:Zpm2BkR3:qTsJhBme.jpg&w=800&sig=d291f09ca0d17c2d5412ccf5f99351451dd433d9","markup":"

    The 1000GM Independence Day GM Norm 2024 is a 10-player round-robin tournament, held from the 3rd to the 7th of July in San Jose, California, USA.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website

    \n","leaderboard":true},"round":{"id":"SlAoLwYT","name":"Round 9","slug":"round-9","createdAt":1719923007464,"finished":true,"startsAt":1720397700000,"url":"https://lichess.org/broadcast/1000gm-independence-day-gm-norm-2024/round-9/SlAoLwYT"}},{"tour":{"id":"eRDPod9B","name":"Marshall Monthly FIDE Premier 2024 | July","slug":"marshall-monthly-fide-premier-2024--july","info":{"format":"5-round Swiss","tc":"Classical"},"createdAt":1720121901223,"url":"https://lichess.org/broadcast/marshall-monthly-fide-premier-2024--july/eRDPod9B","tier":3,"dates":[1720221300000,1720388700000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=sergioglorias:relay:eRDPod9B:ABp4RLul.jpg&w=800&sig=976ad750500d05b752225cb73df7ff01775b90ac","markup":"

    The Marshall Chess Club FIDE Premier July 2024 is a 5-round Swiss, held from the 5th to the 7th of July in New York City, USA.

    \n

    Time control is 90 minutes for the entire game, with a 30-second increment from move 1.

    \n

    Official Website

    \n"},"round":{"id":"v2n1zP96","name":"Round 5","slug":"round-5","createdAt":1720122277391,"finished":true,"startsAt":1720388700000,"url":"https://lichess.org/broadcast/marshall-monthly-fide-premier-2024--july/round-5/v2n1zP96"},"group":"Marshall Monthly FIDE Premier 2024"},{"tour":{"id":"fLqpKaC4","name":"CCA World Open 2024","slug":"cca-world-open-2024","info":{"format":"9-round Swiss","tc":"Classical","players":"Liang, Durarbayli, McShane, Yoo"},"createdAt":1719970723789,"url":"https://lichess.org/broadcast/cca-world-open-2024/fLqpKaC4","tier":4,"dates":[1720048020000,1720386420000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=iacaster:relay:fLqpKaC4:4FYyYcEx.jpg&w=800&sig=883c18f09e16639e631b07c69af344e5a83f1ba1","markup":"

    The CCA World Open 2024 is a 9-round Swiss, held from the 3rd to the 7th of July in Philadelphia, Pennsylvania, USA.

    \n

    Time control is 40 moves in 90 minutes, then 30 minutes, with a 30 second delay after every move.

    \n

    Official Website | Results

    \n
    \n

    Title image photo by Paul Frendach

    \n"},"round":{"id":"uYzumLEp","name":"Round 9","slug":"round-9","createdAt":1719971455782,"finished":true,"startsAt":1720386420000,"url":"https://lichess.org/broadcast/cca-world-open-2024/round-9/uYzumLEp"}},{"tour":{"id":"7t6naO2X","name":"2nd Annual Independence Day Open","slug":"2nd-annual-independence-day-open","info":{"format":"5-round Swiss","tc":"Classical"},"createdAt":1720201395792,"url":"https://lichess.org/broadcast/2nd-annual-independence-day-open/7t6naO2X","tier":3,"dates":[1720221300000,1720379700000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:7t6naO2X:4N8EH5wl.jpg&w=800&sig=d51590b24b9fb8ac5fe204457b54284bc43bcb0d","markup":"

    The 2nd Annual Independence Day Open is a 5-round Swiss, held from the 5th to the 7th of July in Dulles, Virginia, USA.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website

    \n"},"round":{"id":"UNMyETL4","name":"Round 5","slug":"round-5","createdAt":1720201460857,"finished":true,"startsAt":1720379700000,"url":"https://lichess.org/broadcast/2nd-annual-independence-day-open/round-5/UNMyETL4"}},{"tour":{"id":"9Uablwir","name":"1000GM Summer Dual Scheveningen 2024 #3 | Group A","slug":"1000gm-summer-dual-scheveningen-2024-3--group-a","info":{"format":"10-player Semi-Scheveningen","tc":"Classical"},"createdAt":1720124060927,"url":"https://lichess.org/broadcast/1000gm-summer-dual-scheveningen-2024-3--group-a/9Uablwir","tier":3,"dates":[1720199700000,1720372500000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=sergioglorias:relay:9Uablwir:xdjJ2iwL.jpg&w=800&sig=f08151608f83e9a7738e07bd23f7fe05f1db06e4","markup":"

    The 1000GM Summer Dual Scheveningen 2024 #3 | Group A is a 10-player Semi-Scheveningen, held from the 5th to the 7th of July in San Jose, California, USA.

    \n

    Time control is 90 minutes for the entire game, with a 30-second increment from move 1.

    \n

    Official Website

    \n"},"round":{"id":"JxDiKnHY","name":"Round 5","slug":"round-5","createdAt":1720124654816,"finished":true,"startsAt":1720372500000,"url":"https://lichess.org/broadcast/1000gm-summer-dual-scheveningen-2024-3--group-a/round-5/JxDiKnHY"},"group":"1000GM Summer Dual Scheveningen 2024 #3"},{"tour":{"id":"aec1RGgy","name":"Schack-SM 2024 | Sverigemästarklassen","slug":"schack-sm-2024--sverigemastarklassen","info":{"format":"10-player round-robin","tc":"Classical"},"createdAt":1719330577301,"url":"https://lichess.org/broadcast/schack-sm-2024--sverigemastarklassen/aec1RGgy","tier":4,"dates":[1719666000000,1720342800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=claes1981:relay:aec1RGgy:2VfDqH1O.jpg&w=800&sig=dd784c694cd31ea8df2dd4deab2a331379f03e2d","markup":"

    The Swedish Championship week takes place from June 28th to July 7th in Fortnox Arena, Växjö, Sweden. The event includes several sections, which are linked at the bottom.

    \n

    SE

    \n

    Officiell webbplats | Video-kommentering | Resultat och lottning | Livechess PGN

    \n

    Betänketid Sverigemästarklassen: 90 minuter för 40 drag, plus 30 minuter för resten av partiet, plus 30 sekunder per drag från drag ett.

    \n

    Sverigemästarklassen | Mästarklassen-elit | Junior-SM | Mästarklassen | Veteran-SM 50+ | Veteran-SM 65+ | Weekendturneringen I | Klass I-IV | SM-blixten | SM 2023

    \n

    EN

    \n

    Official Website | Video commentary | Results and Pairings | Livechess PGN

    \n

    Time control Swedish Champion Class: 90 minutes for 40 moves, plus 30 minutes for the rest of the game, plus 30 seconds per move from move one.

    \n

    Swedish Champion Class | Elite Masterclass | Swedish Junior Championship | Masterclass | Swedish Senior Championship 50+ | Swedish Senior Championship 65+ | The Weekend Tournament I | Class I-IV | The SM Blitz | 2023

    \n"},"round":{"id":"sJ5sZRMs","name":"Rond 9","slug":"rond-9","createdAt":1719331504689,"finished":true,"startsAt":1720342800000,"url":"https://lichess.org/broadcast/schack-sm-2024--sverigemastarklassen/rond-9/sJ5sZRMs"},"group":"Schack-SM 2024"},{"tour":{"id":"2XEWNHQG","name":"Baku Open 2024 | Group A","slug":"baku-open-2024--group-a","info":{"format":"9-round Swiss","tc":"Classical","players":"Narayanan, Mamedov, Pranav"},"createdAt":1719363025661,"url":"https://lichess.org/broadcast/baku-open-2024--group-a/2XEWNHQG","tier":4,"dates":[1719659700000,1720347300000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:2XEWNHQG:dVkQzLbt.jpg&w=800&sig=d6c75dc5ce69c7d0641e9a5634c658683696b2a5","markup":"

    The Baku Open 2024 is a 9-round Swiss, held from the 29th of June to the 7th of July in Baku, Azerbaijan.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website | Results

    \n
    \n

    Title image photo by Dario Daniel Silva on Unsplash

    \n"},"round":{"id":"TOAPN9Bi","name":"Round 9","slug":"round-9","createdAt":1719363202831,"finished":true,"startsAt":1720347300000,"url":"https://lichess.org/broadcast/baku-open-2024--group-a/round-9/TOAPN9Bi"},"group":"Baku Open 2024"},{"tour":{"id":"Kont9lyt","name":"Spanish U10 Rapid Championship 2024","slug":"spanish-u10-rapid-championship-2024","info":{"format":"9-round Swiss","tc":"Rapid"},"createdAt":1719478161692,"url":"https://lichess.org/broadcast/spanish-u10-rapid-championship-2024/Kont9lyt","tier":3,"dates":[1720278000000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=josefeda:relay:Kont9lyt:e7kU6oM9.jpg&w=800&sig=eafa6855e06b5bb948e815534d4212d20f7aa010","markup":"

    The Spanish U10 Rapid Championship 2024 is a 9-round Swiss, held from the 6th to the 7th of July in Salobreña, Granada, Spain.

    \n

    Time control is 10 minutes for the entire game with a 5-second increment from move 1.

    \n

    Official Website | Standings

    \n
    \n

    Campeonato de España Rapido Sub 10 2024

    \n"},"round":{"id":"tIeqLJf0","name":"Ronda 9","slug":"ronda-9","createdAt":1719478541343,"finished":true,"startsAt":1720351800000,"url":"https://lichess.org/broadcast/spanish-u10-rapid-championship-2024/ronda-9/tIeqLJf0"}},{"tour":{"id":"lunItMBB","name":"Saxony-Anhalt Seniors Championships 2024 | 50+","slug":"saxony-anhalt-seniors-championships-2024--50","info":{"format":"7-round Swiss","tc":"Classical"},"createdAt":1719827879431,"url":"https://lichess.org/broadcast/saxony-anhalt-seniors-championships-2024--50/lunItMBB","tier":3,"dates":[1719839700000,1720340100000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:lunItMBB:M2ARPeSq.jpg&w=800&sig=6230cf839867f551e78c70673e839a03162a541b","markup":"

    The Saxony-Anhalt Seniors Championships 2024 | 50+ is a 7-round Swiss, held from the 1st to the 7th of July in Magdeburg, Germany.

    \n

    Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

    \n

    Official Website

    \n"},"round":{"id":"Hh4EwihK","name":"Round 7","slug":"round-7","createdAt":1719827954310,"finished":true,"startsAt":1720340100000,"url":"https://lichess.org/broadcast/saxony-anhalt-seniors-championships-2024--50/round-7/Hh4EwihK"},"group":"Saxony-Anhalt Seniors Championships 2024"},{"tour":{"id":"47N9XRWe","name":"České Budějovice Chess Festival 2024 | GM A","slug":"ceske-budejovice-chess-festival-2024--gm-a","info":{"format":"10-player round-robin","tc":"Classical"},"createdAt":1719327405409,"url":"https://lichess.org/broadcast/ceske-budejovice-chess-festival-2024--gm-a/47N9XRWe","tier":3,"dates":[1719669600000,1720339200000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:47N9XRWe:aOKaYfNt.jpg&w=800&sig=85338600beb8ded4a6165adec640570c8112dd42","markup":"

    The České Budějovice Chess Festival 2024 | GM A is a 10-player round-robin tournament, held from the 29th of June to the 7th of July in České Budějovice, Czech Republic.

    \n

    Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

    \n

    Offiical Website | Standings

    \n
    \n

    Title image photo by Hans Lemuet (Spone), CC BY-SA 3.0, via Wikimedia Commons

    \n","leaderboard":true},"round":{"id":"CjE6k07C","name":"Round 9","slug":"round-9","createdAt":1719327648068,"finished":true,"startsAt":1720339200000,"url":"https://lichess.org/broadcast/ceske-budejovice-chess-festival-2024--gm-a/round-9/CjE6k07C"},"group":"České Budějovice Chess Festival 2024"},{"tour":{"id":"5143V4eE","name":"XXIV Open Internacional d'Escacs de Torredembarra","slug":"xxiv-open-internacional-descacs-de-torredembarra","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1719588450645,"url":"https://lichess.org/broadcast/xxiv-open-internacional-descacs-de-torredembarra/5143V4eE","tier":3,"dates":[1719671400000,1720335600000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=ukkina:relay:5143V4eE:R0iNiocy.jpg&w=800&sig=163d30e58a300f59c5255f3e765e709fe5ccf8c7","markup":"

    The XXIV Open Internacional d'Escacs de Torredembarra is a 9-round Swiss, held from the 29th of June to the 7th of July in
    Torredembarra, Spain.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website | Standings

    \n
    \n

    Del 29 de juny al 7 de juliol de 2024
    ORGANITZA: CLUB D’ESCACS TORREDEMBARRA
    (Integrat al XX Circuit Català d’Oberts Internacionals d’Escacs, classificat amb categoria B, b. (http://www.escacs.cat).

    \n"},"round":{"id":"1x9bhyjf","name":"Round 9","slug":"round-9","createdAt":1719761787694,"finished":true,"startsAt":1720335600000,"url":"https://lichess.org/broadcast/xxiv-open-internacional-descacs-de-torredembarra/round-9/1x9bhyjf"}},{"tour":{"id":"HeOoTDru","name":"All-Ukrainian Festival Morshyn 2024","slug":"all-ukrainian-festival-morshyn-2024","info":{"format":"9-round Swiss","tc":"Rapid"},"createdAt":1720267972743,"url":"https://lichess.org/broadcast/all-ukrainian-festival-morshyn-2024/HeOoTDru","tier":3,"dates":[1720252800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:HeOoTDru:TAcEY8XI.jpg&w=800&sig=729d010e7afad48d86fa086f6176102734f901f5","markup":"

    The All-Ukrainian Festival Morshyn 2024 is a 9-round Swiss, held on the 6th of July in Morshyn, Ukraine.

    \n

    Time control is 10 minutes for the entire game with a 5-second increment from move 1.

    \n

    Standings

    \n
    \n

    Title image photo by ЯдвигаВереск - Own work, CC BY-SA 4.0

    \n"},"round":{"id":"6YRSXzDZ","name":"Round 9","slug":"round-9","createdAt":1720268064062,"finished":true,"startsAt":1720276200000,"url":"https://lichess.org/broadcast/all-ukrainian-festival-morshyn-2024/round-9/6YRSXzDZ"}},{"tour":{"id":"Db0i9sGV","name":"Spanish U10 Championship 2024","slug":"spanish-u10-championship-2024","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1719401439623,"url":"https://lichess.org/broadcast/spanish-u10-championship-2024/Db0i9sGV","tier":3,"dates":[1719820800000,1720252800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=josefeda:relay:Db0i9sGV:TyPuOKoC.jpg&w=800&sig=67728c0d69503809c4f6a137ff382f56ed8d8af7","markup":"

    The Spanish U10 Championship 2024 is a 9-round Swiss, held from the 1st to the 6th of July in Salobreña, Granada, Spain.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website | Standings

    \n
    \n

    Campeonato de España Sub 10 2024

    \n"},"round":{"id":"KYK9G7kE","name":"Ronda 9","slug":"ronda-9","createdAt":1719401772691,"finished":true,"startsAt":1720252800000,"url":"https://lichess.org/broadcast/spanish-u10-championship-2024/ronda-9/KYK9G7kE"}},{"tour":{"id":"BjKO6Jrs","name":"Italian U18 Youth Championships 2024 | U18","slug":"italian-u18-youth-championships-2024--u18","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1719575583240,"url":"https://lichess.org/broadcast/italian-u18-youth-championships-2024--u18/BjKO6Jrs","tier":3,"dates":[1719666900000,1720251900000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:BjKO6Jrs:ztfSSOiP.jpg&w=800&sig=f640d262f4c1545eb47cf716766179385ae51b6e","markup":"

    The Italian U18 Youth Championships 2024 | U18 is a 9-round Swiss, held from the 29th of June to the 6th of July in Salsomaggiore Terme, Italy.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website | Results

    \n"},"round":{"id":"OCMHlRDH","name":"Round 9","slug":"round-9","createdAt":1719575679992,"finished":true,"startsAt":1720251900000,"url":"https://lichess.org/broadcast/italian-u18-youth-championships-2024--u18/round-9/OCMHlRDH"},"group":"Italian U18 Youth Championships 2024"},{"tour":{"id":"hdQQ1Waq","name":"Norwegian Championships 2024 | Elite and Seniors 65+","slug":"norwegian-championships-2024--elite-and-seniors-65","info":{"format":"9-round Swiss","tc":"Classical"},"createdAt":1719174080786,"url":"https://lichess.org/broadcast/norwegian-championships-2024--elite-and-seniors-65/hdQQ1Waq","tier":4,"dates":[1719591300000,1720253700000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:hdQQ1Waq:K1YsAziL.jpg&w=800&sig=1921f0a71b78530c0a6e8b0e564e19af8b91c499","markup":"

    The Norwegian Championships 2024 | Elite and Seniors 65+ is a 9-round Swiss, held from the 28th of June to the 6th of July in Storefjell, Norway.

    \n

    Time control is 90 minutes for 40 moves, followed by 30 minutes for the rest of the game, with a 30-second increment from move 1.

    \n

    Board 1-9 Elite
    Board 10 - 28 Senior 65+

    \n

    Official Website | Results

    \n
    \n

    Landsturneringen 2024

    \n

    Eliteklassen og Senior 65+

    \n

    Spilles på Storefjell resort hotell 28.06.2024 - 06.07.2024

    \n

    Turneringen spilles over 9 runder, med betenkningstid 90 min på 40 trekk, 30 min på resten av partiet og 30 sek tillegg fra trekk 1

    \n

    Bord 1-9 Eliteklassen
    Bord 10 - 28 Senior 65+

    \n

    Clono partier:
    Mikroputt
    \nhttps://lichess.org/broadcast/nm-i-sjakk-2024-mikroputt/round-1/020oDPUm#boards
    Miniputt
    https://lichess.org/broadcast/nm-i-sjakk-2024-miniputt/round-1/pCvV4G8i#boards
    Lilleputt
    https://lichess.org/broadcast/nm-i-sjakk-2024-lilleputt/round-1/k8GS6LrP
    Junior B
    https://lichess.org/broadcast/nm-i-sjakk-junior-b/round-1/AZhM1hMm
    Klasse 1
    https://lichess.org/broadcast/nm-i-sjakk-2024-klasse-1/round-1/aWw2RwQ1
    Klasse 2
    https://lichess.org/broadcast/nm-i-sjakk-2024-klasse-2/round-1/Mnxw76OR
    Klasse 3
    https://lichess.org/broadcast/nmi-sjakk-2024-klasse-3/round-1/ZheSrANG
    Klasse 4
    https://lichess.org/broadcast/nm-i-sjakk-klasse-4/round-1/X673vUlD
    Klasse 5
    https://lichess.org/broadcast/nm-i-sjakk-2024-klasse-5/round-1/C6m3qitn
    Klasse Mester
    https://lichess.org/broadcast/nm-i-sjakk-2024-mesterklassen/round-2/lZu3t3A7#boards

    \n"},"round":{"id":"LQn45rIa","name":"Round 9","slug":"round-9","createdAt":1719175255813,"finished":true,"startsAt":1720253700000,"url":"https://lichess.org/broadcast/norwegian-championships-2024--elite-and-seniors-65/round-9/LQn45rIa"},"group":"Norwegian Championships 2024"},{"tour":{"id":"K1NfeoWE","name":"Superbet Romania Chess Classic 2024","slug":"superbet-romania-chess-classic-2024","info":{"format":"10-player Round Robin","tc":"Classical","players":"Caruana, Nepomniachtchi, Gukesh, Giri"},"createdAt":1719187354944,"url":"https://lichess.org/broadcast/superbet-romania-chess-classic-2024/K1NfeoWE","tier":5,"dates":[1719405000000,1720198800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=iacaster:relay:K1NfeoWE:6kBI06CJ.jpg&w=800&sig=c6b93e8db217a6bb504dcb5e0695337a472655b7","markup":"

    The Superbet Romania Chess Classic 2024 is a 10-player Round Robin, held from the 26th of June to the 5th of July in Bucharest, Romania.

    \n

    Time control is 120 minutes for the entire game, plus a 30-second increment per move.

    \n

    Superbet Chess Classic Romania is the first of two classical events, this tournament will feature a 10-player round robin with nine tour regulars, Caruana, Nepomniachtchi, Abdusattorov, Gukesh, So, Praggnanandhaa, Giri, Firouzja, Vachier-Lagrave, and one wildcard, local Romanian favorite Bogdan-Daniel Deac.

    \n

    Official Website | Results

    \n
    \n

    In the event of a tie for 1st place, a double round-robin will be played with 2 players, or a single round-robin will be played with 3 or more players. Time control is 10 minutes for the entire game with a 5-second increment from move 1.

    \n

    In the event of another tie, knockout armageddon games will be played. Time control is 5 minutes for White, 4 minutes for Black, with a 2-second increment from move 61.

    \n
    \n

    Grand Chess Tour | Tour Standings
    2024 Superbet Poland Rapid & Blitz
    2024 Superbet Romania Chess Classic
    2024 Superunited Croatia Rapid & Blitz

    \n
    \n

    Title image photo by Arvid Olson from Pixabay

    \n","leaderboard":true},"round":{"id":"QC9QC8Lr","name":"Tiebreaks","slug":"tiebreaks","createdAt":1720197015416,"finished":true,"startsAt":1720198800000,"url":"https://lichess.org/broadcast/superbet-romania-chess-classic-2024/tiebreaks/QC9QC8Lr"}},{"tour":{"id":"ZmFLmrss","name":"III Magistral Internacional Ciudad de Sant Joan de Alacant","slug":"iii-magistral-internacional-ciudad-de-sant-joan-de-alacant","info":{"format":"10-player round-robin","tc":"Classical"},"createdAt":1719791889764,"url":"https://lichess.org/broadcast/iii-magistral-internacional-ciudad-de-sant-joan-de-alacant/ZmFLmrss","tier":3,"dates":[1719820800000,1720162800000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:ZmFLmrss:2RKhpn3T.jpg&w=800&sig=8be9210de5ef07975dbfb9d300fffc3ad1e38ecc","markup":"

    The III Magistral Internacional Ciudad de Sant Joan de Alacant is a 10-player round-robin tournament, held from the 1st to the 5th of July in Sant Joan d'Alacant, Spain.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website | Standings

    \n","leaderboard":true},"round":{"id":"MIy50UWQ","name":"Round 9","slug":"round-9","createdAt":1719791991550,"finished":true,"startsAt":1720162800000,"url":"https://lichess.org/broadcast/iii-magistral-internacional-ciudad-de-sant-joan-de-alacant/round-9/MIy50UWQ"}},{"tour":{"id":"fQu6hjlI","name":"1000GM Summer Dual Scheveningen 2024 #2 | Group A","slug":"1000gm-summer-dual-scheveningen-2024-2--group-a","info":{"format":"10-player Semi-Scheveningen","tc":"Classical"},"createdAt":1719922442023,"url":"https://lichess.org/broadcast/1000gm-summer-dual-scheveningen-2024-2--group-a/fQu6hjlI","tier":3,"dates":[1719940500000,1720113300000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:fQu6hjlI:mVZ0X3CV.jpg&w=800&sig=1d2b55c6d0cac0ceda72a4fbc83e837a45006b9e","markup":"

    The 1000GM Summer Dual Scheveningen 2024 #2 | Group A is a 10-player Semi-Scheveningen, held from the 2nd to the 4th of July in San Jose, California, USA.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website

    \n","leaderboard":true},"round":{"id":"D5IvvZGj","name":"Round 5","slug":"round-5","createdAt":1719922517399,"finished":true,"startsAt":1720113300000,"url":"https://lichess.org/broadcast/1000gm-summer-dual-scheveningen-2024-2--group-a/round-5/D5IvvZGj"},"group":"1000GM Summer Dual Scheveningen 2024 #2"},{"tour":{"id":"4ERHDodE","name":"Atlantic Chess Independence Day GM Norm Invitational","slug":"atlantic-chess-independence-day-gm-norm-invitational","info":{"format":"10-player round-robin","tc":"Classical","players":"Erenburg, Plát, Barbosa, Gauri"},"createdAt":1719693938025,"url":"https://lichess.org/broadcast/atlantic-chess-independence-day-gm-norm-invitational/4ERHDodE","tier":3,"dates":[1719695700000,1720098900000],"image":"https://image.lichess1.org/display?h=400&op=thumbnail&path=aaarmstark:relay:4ERHDodE:l3iVV7Ym.jpg&w=800&sig=d57277f927849426413b3b26fccacbad1027ae51","markup":"

    The Atlantic Chess Independence Day GM Norm Invitational is a 10-player round-robin tournament, held from the 29th of June to the 4th of July in Dulles, Virginia, USA.

    \n

    Time control is 90 minutes for the entire game with a 30-second increment from move 1.

    \n

    Official Website | Standings

    \n
    \n

    The Atlantic Chess Association is organizing the Independence Day Norm Tournament. It is a 6 day, 9 rounds, 10 player Round Robin tournament.

    \n

    Chief Arbiter: IA Gregory Vaserstein

    \n

    Venue: Hampton Inn & Suites Washington-Dulles International Airport (4050 Westfax Dr., Chantilly, VA 20151)

    \n","leaderboard":true},"round":{"id":"PVG8wijk","name":"Round 9","slug":"round-9","createdAt":1719698677225,"finished":true,"startsAt":1720098900000,"url":"https://lichess.org/broadcast/atlantic-chess-independence-day-gm-norm-invitational/round-9/PVG8wijk"}}],"previousPage":null,"nextPage":2}} '''; From e12bd8daa9d30879d2a126ace07d331993bef486 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 9 Dec 2024 12:50:26 +0100 Subject: [PATCH 868/979] Add opening explorer settings to study and broadcast settings --- lib/src/view/analysis/analysis_settings.dart | 2 +- lib/src/view/broadcast/broadcast_game_settings.dart | 12 ++++++++++++ .../opening_explorer/opening_explorer_screen.dart | 2 +- .../opening_explorer/opening_explorer_settings.dart | 5 +---- lib/src/view/study/study_settings.dart | 12 ++++++++++++ 5 files changed, 27 insertions(+), 6 deletions(-) diff --git a/lib/src/view/analysis/analysis_settings.dart b/lib/src/view/analysis/analysis_settings.dart index 9ba31e770f..6c893a3539 100644 --- a/lib/src/view/analysis/analysis_settings.dart +++ b/lib/src/view/analysis/analysis_settings.dart @@ -118,7 +118,7 @@ class AnalysisSettings extends ConsumerWidget { isScrollControlled: true, showDragHandle: true, isDismissible: true, - builder: (_) => OpeningExplorerSettings(options), + builder: (_) => const OpeningExplorerSettings(), ), ), SwitchSettingTile( diff --git a/lib/src/view/broadcast/broadcast_game_settings.dart b/lib/src/view/broadcast/broadcast_game_settings.dart index 8fbac642e0..4fd699e371 100644 --- a/lib/src/view/broadcast/broadcast_game_settings.dart +++ b/lib/src/view/broadcast/broadcast_game_settings.dart @@ -7,6 +7,8 @@ import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/view/analysis/stockfish_settings.dart'; +import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_settings.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:lichess_mobile/src/widgets/settings.dart'; @@ -65,6 +67,16 @@ class BroadcastGameSettings extends ConsumerWidget { ), ListSection( children: [ + PlatformListTile( + title: Text(context.l10n.openingExplorer), + onTap: () => showAdaptiveBottomSheet( + context: context, + isScrollControlled: true, + showDragHandle: true, + isDismissible: true, + builder: (_) => const OpeningExplorerSettings(), + ), + ), SwitchSettingTile( title: Text(context.l10n.sound), value: isSoundEnabled, diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index c1002bd150..a94f195493 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -281,7 +281,7 @@ class _BottomBar extends ConsumerWidget { isScrollControlled: true, showDragHandle: true, isDismissible: true, - builder: (_) => OpeningExplorerSettings(options), + builder: (_) => const OpeningExplorerSettings(), ), icon: Icons.tune, ), diff --git a/lib/src/view/opening_explorer/opening_explorer_settings.dart b/lib/src/view/opening_explorer/opening_explorer_settings.dart index a880d8fb33..15c11c63da 100644 --- a/lib/src/view/opening_explorer/opening_explorer_settings.dart +++ b/lib/src/view/opening_explorer/opening_explorer_settings.dart @@ -2,7 +2,6 @@ import 'package:dartchess/dartchess.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/perf.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; @@ -14,9 +13,7 @@ import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; class OpeningExplorerSettings extends ConsumerWidget { - const OpeningExplorerSettings(this.options); - - final AnalysisOptions options; + const OpeningExplorerSettings(); @override Widget build(BuildContext context, WidgetRef ref) { diff --git a/lib/src/view/study/study_settings.dart b/lib/src/view/study/study_settings.dart index cbc4ed51b7..74bc0eef8a 100644 --- a/lib/src/view/study/study_settings.dart +++ b/lib/src/view/study/study_settings.dart @@ -8,6 +8,8 @@ import 'package:lichess_mobile/src/model/study/study_controller.dart'; import 'package:lichess_mobile/src/model/study/study_preferences.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/view/analysis/stockfish_settings.dart'; +import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_settings.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:lichess_mobile/src/widgets/settings.dart'; @@ -68,6 +70,16 @@ class StudySettings extends ConsumerWidget { ), ListSection( children: [ + PlatformListTile( + title: Text(context.l10n.openingExplorer), + onTap: () => showAdaptiveBottomSheet( + context: context, + isScrollControlled: true, + showDragHandle: true, + isDismissible: true, + builder: (_) => const OpeningExplorerSettings(), + ), + ), SwitchSettingTile( title: Text(context.l10n.sound), value: isSoundEnabled, From 891ab98390d440d9b014f9944c72ea78adf08fd5 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 9 Dec 2024 12:54:16 +0100 Subject: [PATCH 869/979] Upgrade dependencies --- ios/Podfile.lock | 12 +++++------ pubspec.lock | 52 ++++++++++++++++++++++++------------------------ 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 1b78391803..8d23aefe58 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -23,14 +23,14 @@ PODS: - Firebase/Messaging (11.4.0): - Firebase/CoreOnly - FirebaseMessaging (~> 11.4.0) - - firebase_core (3.8.0): + - firebase_core (3.8.1): - Firebase/CoreOnly (= 11.4.0) - Flutter - - firebase_crashlytics (4.1.5): + - firebase_crashlytics (4.2.0): - Firebase/Crashlytics (= 11.4.0) - firebase_core - Flutter - - firebase_messaging (15.1.5): + - firebase_messaging (15.1.6): - Firebase/Messaging (= 11.4.0) - firebase_core - Flutter @@ -239,9 +239,9 @@ SPEC CHECKSUMS: cupertino_http: 94ac07f5ff090b8effa6c5e2c47871d48ab7c86c device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe Firebase: cf1b19f21410b029b6786a54e9764a0cacad3c99 - firebase_core: 84a16d041be8bc166b6e00350f89849e06daf9d1 - firebase_crashlytics: 9fae6688e634a062ba57bd22d2b762b87fdcd1ec - firebase_messaging: 48ce5cf82b70ac47ed9cb9277f97bb77d1d01a38 + firebase_core: 222ad1787f0b9c145a06af2fdaa265a366576c0d + firebase_crashlytics: 1c2e2091a0b06bf6e3d6535010469a98c44c4d67 + firebase_messaging: a538130cb2bca3ea0ff0892b8c948bd7d20ecaed FirebaseCore: e0510f1523bc0eb21653cac00792e1e2bd6f1771 FirebaseCoreExtension: f1bc67a4702931a7caa097d8e4ac0a1b0d16720e FirebaseCoreInternal: f47dd28ae7782e6a4738aad3106071a8fe0af604 diff --git a/pubspec.lock b/pubspec.lock index 2de7ca885d..0f2776c019 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -13,10 +13,10 @@ packages: dependency: transitive description: name: _flutterfire_internals - sha256: "71c01c1998c40b3af1944ad0a5f374b4e6fef7f3d2df487f3970dbeadaeb25a1" + sha256: eae3133cbb06de9205899b822e3897fc6a8bc278ad4c944b4ce612689369694b url: "https://pub.dev" source: hosted - version: "1.3.46" + version: "1.3.47" _macros: dependency: transitive description: dart @@ -450,66 +450,66 @@ packages: dependency: "direct main" description: name: firebase_core - sha256: "2438a75ad803e818ad3bd5df49137ee619c46b6fc7101f4dbc23da07305ce553" + sha256: fef81a53ba1ca618def1f8bef4361df07968434e62cb204c1fb90bb880a03da2 url: "https://pub.dev" source: hosted - version: "3.8.0" + version: "3.8.1" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface - sha256: e30da58198a6d4b49d5bce4e852f985c32cb10db329ebef9473db2b9f09ce810 + sha256: b94b217e3ad745e784960603d33d99471621ecca151c99c670869b76e50ad2a6 url: "https://pub.dev" source: hosted - version: "5.3.0" + version: "5.3.1" firebase_core_web: dependency: transitive description: name: firebase_core_web - sha256: f967a7138f5d2ffb1ce15950e2a382924239eaa521150a8f144af34e68b3b3e5 + sha256: "9e69806bb3d905aeec3c1242e0e1475de6ea6d48f456af29d598fb229a2b4e5e" url: "https://pub.dev" source: hosted - version: "2.18.1" + version: "2.18.2" firebase_crashlytics: dependency: "direct main" description: name: firebase_crashlytics - sha256: "4e80ef22428dfecf609df8049419c7446c6e1d797d7f307cad3c7ab70e72ddc5" + sha256: e235c8452d5622fc271404592388fde179e4b62c50e777ad3c8c3369296104ed url: "https://pub.dev" source: hosted - version: "4.1.5" + version: "4.2.0" firebase_crashlytics_platform_interface: dependency: transitive description: name: firebase_crashlytics_platform_interface - sha256: "1104f428ec5249fff62016985719bb232ca91c4bde0d1a033af9b7d8b7451d70" + sha256: "4ddadf44ed0a202f3acad053f12c083877940fa8cc1a9f747ae09e1ef4372160" url: "https://pub.dev" source: hosted - version: "3.6.46" + version: "3.7.0" firebase_messaging: dependency: "direct main" description: name: firebase_messaging - sha256: "4d0968ecb860d7baa15a6e2af3469ec5b0d959e51c59ce84a52b0f7632a4aa5a" + sha256: "151a3ee68736abf293aab66d1317ade53c88abe1db09c75a0460aebf7767bbdf" url: "https://pub.dev" source: hosted - version: "15.1.5" + version: "15.1.6" firebase_messaging_platform_interface: dependency: transitive description: name: firebase_messaging_platform_interface - sha256: a2cb3e7d71d40b6612e2d4e0daa0ae759f6a9d07f693f904d14d22aadf70be10 + sha256: f331ee51e40c243f90cc7bc059222dfec4e5df53125b08d31fb28961b00d2a9d url: "https://pub.dev" source: hosted - version: "4.5.48" + version: "4.5.49" firebase_messaging_web: dependency: transitive description: name: firebase_messaging_web - sha256: "1554e190f0cd9d6fe59f61ae0275ac12006fdb78b07669f1a260d1a9e6de3a1f" + sha256: efaf3fdc54cd77e0eedb8e75f7f01c808828c64d052ddbf94d3009974e47d30f url: "https://pub.dev" source: hosted - version: "3.9.4" + version: "3.9.5" fixnum: dependency: transitive description: @@ -1063,10 +1063,10 @@ packages: dependency: transitive description: name: path_provider_foundation - sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" path_provider_linux: dependency: transitive description: @@ -1380,10 +1380,10 @@ packages: dependency: "direct dev" description: name: sqflite_common_ffi - sha256: b8ba78c1b72a9ee6c2323b06af95d43fd13e03d90c8369cb454fd7f629a72588 + sha256: "883dd810b2b49e6e8c3b980df1829ef550a94e3f87deab5d864917d27ca6bf36" url: "https://pub.dev" source: hosted - version: "2.3.4+3" + version: "2.3.4+4" sqflite_darwin: dependency: transitive description: @@ -1541,10 +1541,10 @@ packages: dependency: transitive description: name: url_launcher_ios - sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e + sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626" url: "https://pub.dev" source: hosted - version: "6.3.1" + version: "6.3.2" url_launcher_linux: dependency: transitive description: @@ -1557,10 +1557,10 @@ packages: dependency: transitive description: name: url_launcher_macos - sha256: "769549c999acdb42b8bcfa7c43d72bf79a382ca7441ab18a808e101149daf672" + sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.2" url_launcher_platform_interface: dependency: transitive description: From 51497016a3a2c16552fae099e03d63089a50686d Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 9 Dec 2024 12:54:34 +0100 Subject: [PATCH 870/979] Bump version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 90f4e62475..5eef9bfcba 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: lichess_mobile description: Lichess mobile app V2 publish_to: "none" -version: 0.13.5+001305 # See README.md for details about versioning +version: 0.13.6+001306 # See README.md for details about versioning environment: sdk: ">=3.5.0 <4.0.0" From 6773a7404a376405102d353479b4cf6f80f28250 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 9 Dec 2024 16:00:47 +0100 Subject: [PATCH 871/979] Improve image color ranking --- lib/src/utils/image.dart | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/src/utils/image.dart b/lib/src/utils/image.dart index 41b5a205ba..761d230f08 100644 --- a/lib/src/utils/image.dart +++ b/lib/src/utils/image.dart @@ -93,15 +93,19 @@ class ImageColorWorker { final QuantizerResult quantizerResult = await QuantizerCelebi().quantize( resized.buffer.asUint32List(), - 128, - returnInputPixelToClusterPixel: true, + 32, ); final Map colorToCount = quantizerResult.colorToCount.map( (int key, int value) => MapEntry(_getArgbFromAbgr(key), value), ); // Score colors for color scheme suitability. - final List scoredResults = Score.score(colorToCount, desired: 1); + final List scoredResults = Score.score( + colorToCount, + desired: 1, + fallbackColorARGB: 0xFFEEEEEE, + filter: false, + ); final Hct sourceColor = Hct.fromInt(scoredResults.first); final scheme = SchemeFidelity( sourceColorHct: sourceColor, From 1d3c02a64b165f1abf0baeb753f4964eaddb86c2 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 10 Dec 2024 11:51:17 +0100 Subject: [PATCH 872/979] More improvements to broadcast list screen --- lib/src/utils/image.dart | 39 ++- .../view/broadcast/broadcast_list_screen.dart | 252 ++++++++++-------- .../broadcasts_list_screen_test.dart | 2 +- 3 files changed, 174 insertions(+), 119 deletions(-) diff --git a/lib/src/utils/image.dart b/lib/src/utils/image.dart index 761d230f08..c79603ab7b 100644 --- a/lib/src/utils/image.dart +++ b/lib/src/utils/image.dart @@ -1,17 +1,20 @@ import 'dart:async'; import 'dart:isolate'; +import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import 'package:image/image.dart' as img; import 'package:material_color_utilities/material_color_utilities.dart'; typedef ImageColors = ({ + Uint8List image, int primaryContainer, int onPrimaryContainer, int error, }); -/// A worker that calculates the `primaryContainer` color of a remote image. +/// A worker that quantizes an image and returns a minimal color scheme associated +/// with the image. /// /// The worker is created by calling [ImageColorWorker.spawn], and the computation /// is run in a separate isolate. @@ -24,12 +27,21 @@ class ImageColorWorker { bool get closed => _closed; - Future getImageColors(String url) async { + /// Returns a minimal color scheme associated with the image at the given [url]. + /// + /// The [fileExtension] parameter is optional and is used to specify the file + /// extension of the image at the given [url] if it is known. It will speed up + /// the decoding process, as otherwise the worker will check the image data + /// against all supported decoders. + Future getImageColors( + String url, { + String? fileExtension, + }) async { if (_closed) throw StateError('Closed'); final completer = Completer.sync(); final id = _idCounter++; _activeRequests[id] = completer; - _commands.send((id, url)); + _commands.send((id, url, fileExtension)); return await completer.future; } @@ -85,21 +97,28 @@ class ImageColorWorker { receivePort.close(); return; } - final (int id, String url) = message as (int, String); + final (int id, String url, String? extension) = + message as (int, String, String?); try { final bytes = await http.readBytes(Uri.parse(url)); - final image = img.decodeImage(bytes); + // final stopwatch0 = Stopwatch()..start(); + final decoder = extension != null + ? img.findDecoderForNamedImage('.$extension') + : img.findDecoderForData(bytes); + final image = decoder!.decode(bytes); final resized = img.copyResize(image!, width: 112); final QuantizerResult quantizerResult = await QuantizerCelebi().quantize( resized.buffer.asUint32List(), 32, ); + // debugPrint( + // 'Decoding and quantization took: ${stopwatch0.elapsedMilliseconds}ms', + // ); final Map colorToCount = quantizerResult.colorToCount.map( (int key, int value) => MapEntry(_getArgbFromAbgr(key), value), ); - // Score colors for color scheme suitability. final List scoredResults = Score.score( colorToCount, desired: 1, @@ -107,17 +126,21 @@ class ImageColorWorker { filter: false, ); final Hct sourceColor = Hct.fromInt(scoredResults.first); + if (sourceColor.tone > 90.0) { + sourceColor.tone = 90.0; + } final scheme = SchemeFidelity( sourceColorHct: sourceColor, isDark: false, contrastLevel: 0.0, ); - final colors = ( + final result = ( + image: bytes, primaryContainer: scheme.primaryContainer, onPrimaryContainer: scheme.onPrimaryContainer, error: scheme.error, ); - sendPort.send((id, colors)); + sendPort.send((id, result)); } catch (e) { sendPort.send((id, RemoteError(e.toString(), ''))); } diff --git a/lib/src/view/broadcast/broadcast_list_screen.dart b/lib/src/view/broadcast/broadcast_list_screen.dart index a55720a012..5137929664 100644 --- a/lib/src/view/broadcast/broadcast_list_screen.dart +++ b/lib/src/view/broadcast/broadcast_list_screen.dart @@ -134,7 +134,7 @@ class _BodyState extends ConsumerState<_Body> { crossAxisCount: itemsByRow, crossAxisSpacing: 10, mainAxisSpacing: 10, - childAspectRatio: 1.3, + childAspectRatio: 1.45, ); final sections = [ @@ -258,6 +258,8 @@ final Map _colorsCache = {}; class _BroadcastGridItemState extends State { _CardColors? _cardColors; + ImageProvider? _imageProvider; + bool _tapDown = false; String? get imageUrl => widget.broadcast.tour.imageUrl; @@ -270,9 +272,12 @@ class _BroadcastGridItemState extends State { final cachedColors = _colorsCache[image]; if (cachedColors != null) { _cardColors = cachedColors; + _imageProvider = image; } else { if (imageUrl != null) { - _fetchImageAndColors(image as NetworkImage); + _fetchImageAndColors(NetworkImage(imageUrl!)); + } else { + _imageProvider = kDefaultBroadcastImage; } } } @@ -285,9 +290,11 @@ class _BroadcastGridItemState extends State { scheduleMicrotask(() => _fetchImageAndColors(provider)); }); } else if (widget.worker.closed == false) { - final response = await widget.worker.getImageColors(provider.url); + final response = await widget.worker + .getImageColors(provider.url, fileExtension: 'webp'); if (response != null) { - final (:primaryContainer, :onPrimaryContainer, :error) = response; + final (:image, :primaryContainer, :onPrimaryContainer, :error) = + response; final cardColors = ( primaryContainer: Color(primaryContainer), onPrimaryContainer: Color(onPrimaryContainer), @@ -296,6 +303,7 @@ class _BroadcastGridItemState extends State { _colorsCache[provider] = cardColors; if (mounted) { setState(() { + _imageProvider = MemoryImage(image); _cardColors = cardColors; }); } @@ -303,6 +311,14 @@ class _BroadcastGridItemState extends State { } } + void _onTapDown() { + setState(() => _tapDown = true); + } + + void _onTapCancel() { + setState(() => _tapDown = false); + } + @override Widget build(BuildContext context) { final defaultBackgroundColor = @@ -327,98 +343,76 @@ class _BroadcastGridItemState extends State { BroadcastRoundScreen(broadcast: widget.broadcast), ); }, - child: AnimatedContainer( - duration: const Duration(milliseconds: 500), - clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - borderRadius: kBroadcastGridItemBorderRadius, - color: backgroundColor, - boxShadow: Theme.of(context).platform == TargetPlatform.iOS - ? null - : kElevationToShadow[1], - ), - foregroundDecoration: BoxDecoration( - border: (widget.broadcast.isLive) - ? Border.all( - color: LichessColors.red.withValues(alpha: 0.7), - width: 3, - ) - : null, - borderRadius: kBroadcastGridItemBorderRadius, - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ShaderMask( - blendMode: BlendMode.dstOut, - shaderCallback: (bounds) { - return LinearGradient( - begin: Alignment.center, - end: Alignment.bottomCenter, - colors: [ - backgroundColor.withValues(alpha: 0.0), - backgroundColor.withValues(alpha: 1.0), - ], - stops: const [0.7, 1.10], - tileMode: TileMode.clamp, - ).createShader(bounds); - }, - child: AspectRatio( - aspectRatio: 2.0, - child: Image( - image: image, - frameBuilder: - (context, child, frame, wasSynchronouslyLoaded) { - if (wasSynchronouslyLoaded) { - return child; - } - return AnimatedOpacity( - duration: const Duration(milliseconds: 500), - opacity: frame == null ? 0 : 1, - child: child, - ); - }, - errorBuilder: (context, error, stackTrace) => - const Image(image: kDefaultBroadcastImage), + onTapDown: (_) => _onTapDown(), + onTapCancel: _onTapCancel, + onTapUp: (_) => _onTapCancel(), + child: AnimatedOpacity( + opacity: _tapDown ? 1.0 : 0.85, + duration: const Duration(milliseconds: 100), + child: AnimatedContainer( + duration: const Duration(milliseconds: 500), + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + borderRadius: kBroadcastGridItemBorderRadius, + color: backgroundColor, + boxShadow: Theme.of(context).platform == TargetPlatform.iOS + ? null + : kElevationToShadow[1], + ), + child: Stack( + children: [ + ShaderMask( + blendMode: BlendMode.dstOut, + shaderCallback: (bounds) { + return LinearGradient( + begin: Alignment.center, + end: Alignment.bottomCenter, + colors: [ + backgroundColor.withValues(alpha: 0.0), + backgroundColor.withValues(alpha: 1.0), + ], + stops: const [0.5, 1.10], + tileMode: TileMode.clamp, + ).createShader(bounds); + }, + child: AspectRatio( + aspectRatio: 2.0, + child: _imageProvider != null + ? Image( + image: _imageProvider!, + frameBuilder: + (context, child, frame, wasSynchronouslyLoaded) { + if (wasSynchronouslyLoaded) { + return child; + } + return AnimatedOpacity( + duration: const Duration(milliseconds: 500), + opacity: frame == null ? 0 : 1, + child: child, + ); + }, + errorBuilder: (context, error, stackTrace) => + const Image(image: kDefaultBroadcastImage), + ) + : const SizedBox.shrink(), ), ), - ), - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (widget.broadcast.round.startsAt != null || - widget.broadcast.isLive) - Padding( - padding: kBroadcastGridItemContentPadding, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - widget.broadcast.round.name, - style: TextStyle( - fontSize: 12, - color: subTitleColor, - ), - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), - const SizedBox(width: 4.0), - if (widget.broadcast.isLive) - Text( - 'LIVE', - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.bold, - color: liveColor, - ), - overflow: TextOverflow.ellipsis, - ) - else + Positioned( + left: 0, + right: 0, + bottom: 12.0, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (widget.broadcast.round.startsAt != null || + widget.broadcast.isLive) + Padding( + padding: kBroadcastGridItemContentPadding, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ Text( - _formatDate(widget.broadcast.round.startsAt!), + widget.broadcast.round.name, style: TextStyle( fontSize: 12, color: subTitleColor, @@ -426,27 +420,65 @@ class _BroadcastGridItemState extends State { overflow: TextOverflow.ellipsis, maxLines: 1, ), - ], + const SizedBox(width: 4.0), + if (widget.broadcast.isLive) + Text( + 'LIVE', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.bold, + color: liveColor, + ), + overflow: TextOverflow.ellipsis, + ) + else + Text( + _formatDate(widget.broadcast.round.startsAt!), + style: TextStyle( + fontSize: 12, + color: subTitleColor, + ), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ], + ), ), - ), - const SizedBox(height: 4.0), - Padding( - padding: kBroadcastGridItemContentPadding, - child: Text( - widget.broadcast.title, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: titleColor, - fontWeight: FontWeight.bold, - fontSize: 16, + Padding( + padding: kBroadcastGridItemContentPadding.add( + const EdgeInsets.symmetric(vertical: 3.0), + ), + child: Text( + widget.broadcast.title, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: titleColor, + fontWeight: FontWeight.bold, + height: 1.0, + fontSize: 16, + ), ), ), - ), - ], + if (widget.broadcast.tour.information.players != null) + Padding( + padding: kBroadcastGridItemContentPadding, + child: Text( + widget.broadcast.tour.information.players!, + style: TextStyle( + fontSize: 12, + color: subTitleColor, + letterSpacing: -0.2, + ), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + ], + ), ), - ), - ], + ], + ), ), ), ); diff --git a/test/view/broadcast/broadcasts_list_screen_test.dart b/test/view/broadcast/broadcasts_list_screen_test.dart index a60c308f80..59b9c2e87a 100644 --- a/test/view/broadcast/broadcasts_list_screen_test.dart +++ b/test/view/broadcast/broadcasts_list_screen_test.dart @@ -18,7 +18,7 @@ class FakeImageColorWorker implements ImageColorWorker { bool get closed => false; @override - Future getImageColors(String url) { + Future getImageColors(String url, {String? fileExtension}) { return Future.value(null); } } From e00ba005a7e3c0d4e95b28eae37b423792c68543 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 10 Dec 2024 12:42:27 +0100 Subject: [PATCH 873/979] Tweak --- lib/src/view/broadcast/broadcast_list_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/view/broadcast/broadcast_list_screen.dart b/lib/src/view/broadcast/broadcast_list_screen.dart index 5137929664..3dafbabb5f 100644 --- a/lib/src/view/broadcast/broadcast_list_screen.dart +++ b/lib/src/view/broadcast/broadcast_list_screen.dart @@ -347,7 +347,7 @@ class _BroadcastGridItemState extends State { onTapCancel: _onTapCancel, onTapUp: (_) => _onTapCancel(), child: AnimatedOpacity( - opacity: _tapDown ? 1.0 : 0.85, + opacity: _tapDown ? 1.0 : 0.80, duration: const Duration(milliseconds: 100), child: AnimatedContainer( duration: const Duration(milliseconds: 500), From c78f5dcb83734d27f49a2b2af5dba021073ea57d Mon Sep 17 00:00:00 2001 From: Noah <78898963+HaonRekcef@users.noreply.github.com> Date: Tue, 10 Dec 2024 11:51:32 +0100 Subject: [PATCH 874/979] fix-gif-creation --- lib/src/model/game/game_share_service.dart | 9 ++-- lib/src/model/settings/board_preferences.dart | 51 ++++++++++--------- lib/src/model/study/study_repository.dart | 12 +++-- lib/src/view/study/study_screen.dart | 1 - 4 files changed, 39 insertions(+), 34 deletions(-) diff --git a/lib/src/model/game/game_share_service.dart b/lib/src/model/game/game_share_service.dart index 96cc557f85..09df89a3a4 100644 --- a/lib/src/model/game/game_share_service.dart +++ b/lib/src/model/game/game_share_service.dart @@ -81,13 +81,16 @@ class GameShareService { /// Fetches the GIF animation of a game. Future gameGif(GameId id, Side orientation) async { - final boardTheme = _ref.read(boardPreferencesProvider).boardTheme; - final pieceTheme = _ref.read(boardPreferencesProvider).pieceSet; + final boardPreferences = _ref.read(boardPreferencesProvider); + final boardTheme = boardPreferences.boardTheme == BoardTheme.system + ? BoardTheme.brown + : boardPreferences.boardTheme; + final pieceTheme = boardPreferences.pieceSet; final resp = await _ref .read(defaultClientProvider) .get( Uri.parse( - '$kLichessCDNHost/game/export/gif/${orientation.name}/$id.gif?theme=${boardTheme.name}&piece=${pieceTheme.name}', + '$kLichessCDNHost/game/export/gif/${orientation.name}/$id.gif?theme=${boardTheme.gifApiName}&piece=${pieceTheme.name}', ), ) .timeout(const Duration(seconds: 1)); diff --git a/lib/src/model/settings/board_preferences.dart b/lib/src/model/settings/board_preferences.dart index a0974d0f9c..1c4f87319e 100644 --- a/lib/src/model/settings/board_preferences.dart +++ b/lib/src/model/settings/board_preferences.dart @@ -223,34 +223,35 @@ enum ShapeColor { /// The chessboard theme. enum BoardTheme { - system('System'), - blue('Blue'), - blue2('Blue2'), - blue3('Blue3'), - blueMarble('Blue Marble'), - canvas('Canvas'), - wood('Wood'), - wood2('Wood2'), - wood3('Wood3'), - wood4('Wood4'), - maple('Maple'), - maple2('Maple 2'), - brown('Brown'), - leather('Leather'), - green('Green'), - marble('Marble'), - greenPlastic('Green Plastic'), - grey('Grey'), - metal('Metal'), - olive('Olive'), - newspaper('Newspaper'), - purpleDiag('Purple-Diag'), - pinkPyramid('Pink'), - horsey('Horsey'); + system('System', 'system'), + blue('Blue', 'blue'), + blue2('Blue 2', 'blue2'), + blue3('Blue 3', 'blue3'), + blueMarble('Blue Marble', 'blue-marble'), + canvas('Canvas', 'canvas'), + wood('Wood', 'wood'), + wood2('Wood 2', 'wood2'), + wood3('Wood 3', 'wood3'), + wood4('Wood 4', 'wood4'), + maple('Maple', 'maple'), + maple2('Maple 2', 'maple2'), + brown('Brown', 'brown'), + leather('Leather', 'leather'), + green('Green', 'green'), + marble('Marble', 'marble'), + greenPlastic('Green Plastic', 'green-plastic'), + grey('Grey', 'grey'), + metal('Metal', 'metal'), + olive('Olive', 'olive'), + newspaper('Newspaper', 'newspaper'), + purpleDiag('Purple-Diag', 'purple-diag'), + pinkPyramid('Pink', 'pink'), + horsey('Horsey', 'horsey'); final String label; + final String gifApiName; - const BoardTheme(this.label); + const BoardTheme(this.label, this.gifApiName); ChessboardColorScheme get colors { switch (this) { diff --git a/lib/src/model/study/study_repository.dart b/lib/src/model/study/study_repository.dart index 71d8babe8a..89d745be37 100644 --- a/lib/src/model/study/study_repository.dart +++ b/lib/src/model/study/study_repository.dart @@ -1,6 +1,5 @@ import 'dart:convert'; -import 'package:dartchess/dartchess.dart'; import 'package:deep_pick/deep_pick.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -110,16 +109,19 @@ class StudyRepository { Future chapterGif( StudyId id, StudyChapterId chapterId, - Side orientation, ) async { - final boardTheme = ref.read(boardPreferencesProvider).boardTheme; - final pieceTheme = ref.read(boardPreferencesProvider).pieceSet; + final boardPreferences = ref.read(boardPreferencesProvider); + final boardTheme = boardPreferences.boardTheme == BoardTheme.system + ? BoardTheme.brown + : boardPreferences.boardTheme; + final pieceTheme = boardPreferences.pieceSet; + final resp = await client .get( lichessUri( '/study/$id/$chapterId.gif', { - 'theme': boardTheme.name, + 'theme': boardTheme.gifApiName, 'piece': pieceTheme.name, }, ), diff --git a/lib/src/view/study/study_screen.dart b/lib/src/view/study/study_screen.dart index 5bab1eec55..a91783c806 100644 --- a/lib/src/view/study/study_screen.dart +++ b/lib/src/view/study/study_screen.dart @@ -333,7 +333,6 @@ class _StudyMenu extends ConsumerWidget { await ref.read(studyRepositoryProvider).chapterGif( state.study.id, state.study.chapter.id, - state.pov, ); if (context.mounted) { launchShareDialog( From ac9f5355a11e0a6c86a72c52bffec83f31fe9496 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Mon, 9 Dec 2024 12:53:05 +0100 Subject: [PATCH 875/979] Add broadcast players tab --- lib/src/model/broadcast/broadcast.dart | 19 +- .../model/broadcast/broadcast_providers.dart | 45 ++++ .../model/broadcast/broadcast_repository.dart | 30 ++- lib/src/model/common/id.dart | 21 ++ .../view/broadcast/broadcast_boards_tab.dart | 95 +++---- .../view/broadcast/broadcast_game_screen.dart | 44 +--- .../broadcast/broadcast_overview_tab.dart | 129 +++++---- .../broadcast/broadcast_player_widget.dart | 63 +++++ .../view/broadcast/broadcast_players_tab.dart | 248 ++++++++++++++++++ .../broadcast/broadcast_round_screen.dart | 57 ++-- lib/src/view/user/perf_stats_screen.dart | 47 +--- lib/src/widgets/progression_widget.dart | 48 ++++ 12 files changed, 618 insertions(+), 228 deletions(-) create mode 100644 lib/src/view/broadcast/broadcast_player_widget.dart create mode 100644 lib/src/view/broadcast/broadcast_players_tab.dart create mode 100644 lib/src/widgets/progression_widget.dart diff --git a/lib/src/model/broadcast/broadcast.dart b/lib/src/model/broadcast/broadcast.dart index e662e90090..fff18f2d78 100644 --- a/lib/src/model/broadcast/broadcast.dart +++ b/lib/src/model/broadcast/broadcast.dart @@ -75,8 +75,6 @@ typedef BroadcastTournamentGroup = ({ @freezed class BroadcastRound with _$BroadcastRound { - const BroadcastRound._(); - const factory BroadcastRound({ required BroadcastRoundId id, required String name, @@ -117,17 +115,30 @@ class BroadcastGame with _$BroadcastGame { @freezed class BroadcastPlayer with _$BroadcastPlayer { - const BroadcastPlayer._(); - const factory BroadcastPlayer({ required String name, required String? title, required int? rating, required Duration? clock, required String? federation, + required FideId? fideId, }) = _BroadcastPlayer; } +@freezed +class BroadcastPlayerExtended with _$BroadcastPlayerExtended { + const factory BroadcastPlayerExtended({ + required String name, + required String? title, + required int? rating, + required String? federation, + required FideId? fideId, + required int played, + required double? score, + required int? ratingDiff, + }) = _BroadcastPlayerExtended; +} + enum RoundStatus { live, finished, diff --git a/lib/src/model/broadcast/broadcast_providers.dart b/lib/src/model/broadcast/broadcast_providers.dart index 0c9f685d32..b782ae7081 100644 --- a/lib/src/model/broadcast/broadcast_providers.dart +++ b/lib/src/model/broadcast/broadcast_providers.dart @@ -1,3 +1,4 @@ +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_repository.dart'; @@ -54,6 +55,50 @@ Future broadcastTournament( ); } +enum BroadcastPlayersSortingTypes { player, elo, score } + +@riverpod +class BroadcastPlayers extends _$BroadcastPlayers { + @override + Future> build( + BroadcastTournamentId tournamentId, + ) async { + final players = ref.withClient( + (client) => BroadcastRepository(client).getPlayers(tournamentId), + ); + + return players; + } + + void sort(BroadcastPlayersSortingTypes sortingType, [bool reverse = false]) { + if (!state.hasValue) return; + + final compare = switch (sortingType) { + BroadcastPlayersSortingTypes.player => + (BroadcastPlayerExtended a, BroadcastPlayerExtended b) => + a.name.compareTo(b.name), + BroadcastPlayersSortingTypes.elo => + (BroadcastPlayerExtended a, BroadcastPlayerExtended b) { + if (a.rating == null) return -1; + if (b.rating == null) return 1; + return b.rating!.compareTo(a.rating!); + }, + BroadcastPlayersSortingTypes.score => + (BroadcastPlayerExtended a, BroadcastPlayerExtended b) { + if (a.score == null) return -1; + if (b.score == null) return 1; + return b.score!.compareTo(a.score!); + } + }; + + state = AsyncData( + reverse + ? state.requireValue.sortReversed(compare) + : state.requireValue.sort(compare), + ); + } +} + @Riverpod(keepAlive: true) BroadcastImageWorkerFactory broadcastImageWorkerFactory(Ref ref) { return const BroadcastImageWorkerFactory(); diff --git a/lib/src/model/broadcast/broadcast_repository.dart b/lib/src/model/broadcast/broadcast_repository.dart index 05ddfe2b49..67dec06f47 100644 --- a/lib/src/model/broadcast/broadcast_repository.dart +++ b/lib/src/model/broadcast/broadcast_repository.dart @@ -18,7 +18,6 @@ class BroadcastRepository { path: '/api/broadcast/top', queryParameters: {'page': page.toString()}, ), - headers: {'Accept': 'application/json'}, mapper: _makeBroadcastResponseFromJson, ); } @@ -28,7 +27,6 @@ class BroadcastRepository { ) { return client.readJson( Uri(path: 'api/broadcast/$broadcastTournamentId'), - headers: {'Accept': 'application/json'}, mapper: _makeTournamentFromJson, ); } @@ -40,7 +38,6 @@ class BroadcastRepository { Uri(path: 'api/broadcast/-/-/$broadcastRoundId'), // The path parameters with - are the broadcast tournament and round slugs // They are only used for SEO, so we can safely use - for these parameters - headers: {'Accept': 'application/x-ndjson'}, mapper: _makeRoundWithGamesFromJson, ); } @@ -51,6 +48,15 @@ class BroadcastRepository { ) { return client.read(Uri(path: 'api/study/$roundId/$gameId.pgn')); } + + Future> getPlayers( + BroadcastTournamentId tournamentId, + ) { + return client.readJsonList( + Uri(path: '/broadcast/$tournamentId/players'), + mapper: _makePlayerFromJson, + ); + } } BroadcastList _makeBroadcastResponseFromJson( @@ -195,5 +201,23 @@ BroadcastPlayer _playerFromPick(RequiredPick pick) { rating: pick('rating').asIntOrNull(), clock: pick('clock').asDurationFromCentiSecondsOrNull(), federation: pick('fed').asStringOrNull(), + fideId: pick('fideId').asFideIdOrNull(), + ); +} + +BroadcastPlayerExtended _makePlayerFromJson(Map json) { + return _playerExtendedFromPick(pick(json).required()); +} + +BroadcastPlayerExtended _playerExtendedFromPick(RequiredPick pick) { + return BroadcastPlayerExtended( + name: pick('name').asStringOrThrow(), + title: pick('title').asStringOrNull(), + rating: pick('rating').asIntOrNull(), + federation: pick('fed').asStringOrNull(), + fideId: pick('fideId').asFideIdOrNull(), + played: pick('played').asIntOrThrow(), + score: pick('score').asDoubleOrNull(), + ratingDiff: pick('ratingDiff').asIntOrNull(), ); } diff --git a/lib/src/model/common/id.dart b/lib/src/model/common/id.dart index 783c6f3bb6..b41a4e82a6 100644 --- a/lib/src/model/common/id.dart +++ b/lib/src/model/common/id.dart @@ -65,6 +65,8 @@ extension type const StudyChapterId(String value) implements StringId { StudyChapterId.fromJson(dynamic json) : this(json as String); } +extension type const FideId(String value) implements StringId {} + extension IDPick on Pick { UserId asUserIdOrThrow() { final value = required().value; @@ -227,4 +229,23 @@ extension IDPick on Pick { "value $value at $debugParsingExit can't be casted to StudyId", ); } + + FideId asFideIdOrThrow() { + final value = required().value; + if (value is String) { + return FideId(value); + } + throw PickException( + "value $value at $debugParsingExit can't be casted to FideId", + ); + } + + FideId? asFideIdOrNull() { + if (value == null) return null; + try { + return asFideIdOrThrow(); + } catch (_) { + return null; + } + } } diff --git a/lib/src/view/broadcast/broadcast_boards_tab.dart b/lib/src/view/broadcast/broadcast_boards_tab.dart index 89cfc2f49a..f04cbe4d07 100644 --- a/lib/src/view/broadcast/broadcast_boards_tab.dart +++ b/lib/src/view/broadcast/broadcast_boards_tab.dart @@ -2,18 +2,16 @@ import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_svg/flutter_svg.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_round_controller.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/duration.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; -import 'package:lichess_mobile/src/utils/lichess_assets.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_game_screen.dart'; +import 'package:lichess_mobile/src/view/broadcast/broadcast_player_widget.dart'; import 'package:lichess_mobile/src/widgets/board_thumbnail.dart'; import 'package:lichess_mobile/src/widgets/clock.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; @@ -34,34 +32,42 @@ class BroadcastBoardsTab extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final edgeInsets = MediaQuery.paddingOf(context) - + (Theme.of(context).platform == TargetPlatform.iOS + ? EdgeInsets.only(top: MediaQuery.paddingOf(context).top) + : EdgeInsets.zero) + + Styles.bodyPadding; final round = ref.watch(broadcastRoundControllerProvider(roundId)); - return switch (round) { - AsyncData(:final value) => value.games.isEmpty - ? SliverPadding( - padding: const EdgeInsets.only(top: 16.0), - sliver: SliverToBoxAdapter( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.info, size: 30), - Text(context.l10n.broadcastNoBoardsYet), - ], + return SliverPadding( + padding: edgeInsets, + sliver: switch (round) { + AsyncData(:final value) => value.games.isEmpty + ? SliverPadding( + padding: const EdgeInsets.only(top: 16.0), + sliver: SliverToBoxAdapter( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.info, size: 30), + Text(context.l10n.broadcastNoBoardsYet), + ], + ), ), + ) + : BroadcastPreview( + games: value.games.values.toIList(), + roundId: roundId, + title: value.round.name, ), - ) - : BroadcastPreview( - games: value.games.values.toIList(), - roundId: roundId, - title: value.round.name, + AsyncError(:final error) => SliverFillRemaining( + child: Center( + child: Text('Could not load broadcast: $error'), ), - AsyncError(:final error) => SliverFillRemaining( - child: Center( - child: Text('Could not load broadcast: $error'), ), - ), - _ => BroadcastPreview.loading(roundId: roundId), - }; + _ => BroadcastPreview.loading(roundId: roundId), + }, + ); } } @@ -210,40 +216,11 @@ class _PlayerWidget extends StatelessWidget { mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Flexible( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (player.federation != null) ...[ - Consumer( - builder: (context, widgetRef, _) { - return SvgPicture.network( - lichessFideFedSrc(player.federation!), - height: 12, - httpClient: widgetRef.read(defaultClientProvider), - ); - }, - ), - ], - const SizedBox(width: 5), - if (player.title != null) ...[ - Text( - player.title!, - style: const TextStyle().copyWith( - color: context.lichessColors.brag, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(width: 5), - ], - Flexible( - child: Text( - player.name, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - ], + Expanded( + child: BroadcastPlayerWidget( + federation: player.federation, + title: player.title, + name: player.name, ), ), const SizedBox(width: 5), diff --git a/lib/src/view/broadcast/broadcast_game_screen.dart b/lib/src/view/broadcast/broadcast_game_screen.dart index ceda09ccb9..75e656fc22 100644 --- a/lib/src/view/broadcast/broadcast_game_screen.dart +++ b/lib/src/view/broadcast/broadcast_game_screen.dart @@ -3,7 +3,6 @@ import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_svg/svg.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; @@ -14,16 +13,15 @@ import 'package:lichess_mobile/src/model/common/eval.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/engine/evaluation_service.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; -import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/duration.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; -import 'package:lichess_mobile/src/utils/lichess_assets.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_layout.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_game_bottom_bar.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_game_settings.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_game_tree_view.dart'; +import 'package:lichess_mobile/src/view/broadcast/broadcast_player_widget.dart'; import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; import 'package:lichess_mobile/src/view/engine/engine_lines.dart'; import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_view.dart'; @@ -386,40 +384,16 @@ class _PlayerWidget extends ConsumerWidget { ), const SizedBox(width: 16.0), ], - if (player.federation != null) ...[ - SvgPicture.network( - lichessFideFedSrc(player.federation!), - height: 12, - httpClient: ref.read(defaultClientProvider), + Expanded( + child: BroadcastPlayerWidget( + federation: player.federation, + title: player.title, + name: player.name, + rating: player.rating, + textStyle: + const TextStyle().copyWith(fontWeight: FontWeight.bold), ), - const SizedBox(width: 5), - ], - if (player.title != null) ...[ - Text( - player.title!, - style: const TextStyle().copyWith( - color: context.lichessColors.brag, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(width: 5), - ], - Text( - player.name, - style: const TextStyle().copyWith( - fontWeight: FontWeight.bold, - ), - overflow: TextOverflow.ellipsis, ), - if (player.rating != null) ...[ - const SizedBox(width: 5), - Text( - player.rating.toString(), - style: const TextStyle(), - overflow: TextOverflow.ellipsis, - ), - ], - const Spacer(), if (clock != null) Container( height: kAnalysisBoardHeaderOrFooterHeight, diff --git a/lib/src/view/broadcast/broadcast_overview_tab.dart b/lib/src/view/broadcast/broadcast_overview_tab.dart index 20cc020cc9..0208fed424 100644 --- a/lib/src/view/broadcast/broadcast_overview_tab.dart +++ b/lib/src/view/broadcast/broadcast_overview_tab.dart @@ -6,6 +6,7 @@ import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_providers.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; @@ -26,77 +27,91 @@ class BroadcastOverviewTab extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final edgeInsets = MediaQuery.paddingOf(context) - + (Theme.of(context).platform == TargetPlatform.iOS + ? EdgeInsets.only(top: MediaQuery.paddingOf(context).top) + : EdgeInsets.zero) + + Styles.bodyPadding; final tournament = ref.watch(broadcastTournamentProvider(tournamentId)); switch (tournament) { case AsyncData(value: final tournament): final information = tournament.data.information; final description = tournament.data.description; - return SliverList( - delegate: SliverChildListDelegate( - [ - if (tournament.data.imageUrl != null) ...[ - Image.network(tournament.data.imageUrl!), - const SizedBox(height: 16.0), - ], - Wrap( - alignment: WrapAlignment.center, - children: [ - if (information.dates != null) - _BroadcastOverviewCard( - CupertinoIcons.calendar, - information.dates!.endsAt == null - ? _dateFormatter.format(information.dates!.startsAt) - : '${_dateFormatter.format(information.dates!.startsAt)} - ${_dateFormatter.format(information.dates!.endsAt!)}', - ), - if (information.format != null) - _BroadcastOverviewCard( - Icons.emoji_events, - '${information.format}', - ), - if (information.timeControl != null) - _BroadcastOverviewCard( - CupertinoIcons.stopwatch_fill, - '${information.timeControl}', - ), - if (information.location != null) - _BroadcastOverviewCard( - Icons.public, - '${information.location}', - ), - if (information.players != null) - _BroadcastOverviewCard( - Icons.person, - '${information.players}', - ), - if (information.website != null) - _BroadcastOverviewCard( - Icons.link, - context.l10n.broadcastOfficialWebsite, - information.website, - ), + return SliverPadding( + padding: edgeInsets, + sliver: SliverList( + delegate: SliverChildListDelegate( + [ + if (tournament.data.imageUrl != null) ...[ + Image.network(tournament.data.imageUrl!), + const SizedBox(height: 16.0), ], - ), - if (description != null) ...[ - const SizedBox(height: 16), - MarkdownBody( - data: description, - onTapLink: (text, url, title) { - if (url == null) return; - launchUrl(Uri.parse(url)); - }, + Wrap( + alignment: WrapAlignment.center, + children: [ + if (information.dates != null) + _BroadcastOverviewCard( + CupertinoIcons.calendar, + information.dates!.endsAt == null + ? _dateFormatter.format(information.dates!.startsAt) + : '${_dateFormatter.format(information.dates!.startsAt)} - ${_dateFormatter.format(information.dates!.endsAt!)}', + ), + if (information.format != null) + _BroadcastOverviewCard( + Icons.emoji_events, + '${information.format}', + ), + if (information.timeControl != null) + _BroadcastOverviewCard( + CupertinoIcons.stopwatch_fill, + '${information.timeControl}', + ), + if (information.location != null) + _BroadcastOverviewCard( + Icons.public, + '${information.location}', + ), + if (information.players != null) + _BroadcastOverviewCard( + Icons.person, + '${information.players}', + ), + if (information.website != null) + _BroadcastOverviewCard( + Icons.link, + context.l10n.broadcastOfficialWebsite, + information.website, + ), + ], ), + if (description != null) ...[ + const SizedBox(height: 16), + MarkdownBody( + data: description, + onTapLink: (text, url, title) { + if (url == null) return; + launchUrl(Uri.parse(url)); + }, + ), + ], ], - ], + ), ), ); case AsyncError(:final error): - return SliverFillRemaining( - child: Center(child: Text('Cannot load broadcast data: $error')), + return SliverPadding( + padding: edgeInsets, + sliver: SliverFillRemaining( + child: Center(child: Text('Cannot load broadcast data: $error')), + ), ); case _: - return const SliverFillRemaining( - child: Center(child: CircularProgressIndicator.adaptive()), + return SliverPadding( + padding: edgeInsets, + sliver: const SliverFillRemaining( + child: Center(child: CircularProgressIndicator.adaptive()), + ), ); } } diff --git a/lib/src/view/broadcast/broadcast_player_widget.dart b/lib/src/view/broadcast/broadcast_player_widget.dart new file mode 100644 index 0000000000..598cc62f46 --- /dev/null +++ b/lib/src/view/broadcast/broadcast_player_widget.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/utils/lichess_assets.dart'; + +class BroadcastPlayerWidget extends ConsumerWidget { + const BroadcastPlayerWidget({ + required this.federation, + required this.title, + required this.name, + this.rating, + this.textStyle, + }); + + final String? federation; + final String? title; + final int? rating; + final String name; + final TextStyle? textStyle; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Row( + children: [ + if (federation != null) ...[ + SvgPicture.network( + lichessFideFedSrc(federation!), + height: 12, + httpClient: ref.read(defaultClientProvider), + ), + const SizedBox(width: 5), + ], + if (title != null) ...[ + Text( + title!, + style: const TextStyle().copyWith( + color: context.lichessColors.brag, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(width: 5), + ], + Flexible( + child: Text( + name, + style: textStyle, + overflow: TextOverflow.ellipsis, + ), + ), + if (rating != null) ...[ + const SizedBox(width: 5), + Text( + rating.toString(), + style: const TextStyle(), + overflow: TextOverflow.ellipsis, + ), + ], + ], + ); + } +} diff --git a/lib/src/view/broadcast/broadcast_players_tab.dart b/lib/src/view/broadcast/broadcast_players_tab.dart new file mode 100644 index 0000000000..f47c7d3b1d --- /dev/null +++ b/lib/src/view/broadcast/broadcast_players_tab.dart @@ -0,0 +1,248 @@ +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast_providers.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/view/broadcast/broadcast_player_widget.dart'; +import 'package:lichess_mobile/src/widgets/progression_widget.dart'; +import 'package:lichess_mobile/src/widgets/shimmer.dart'; + +/// A tab that displays the players participating in a broadcast tournament. +class BroadcastPlayersTab extends ConsumerWidget { + const BroadcastPlayersTab({required this.tournamentId}); + + final BroadcastTournamentId tournamentId; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final edgeInsets = MediaQuery.paddingOf(context) - + (Theme.of(context).platform == TargetPlatform.iOS + ? EdgeInsets.only(top: MediaQuery.paddingOf(context).top) + : EdgeInsets.zero) + + Styles.bodyPadding; + final players = ref.watch(broadcastPlayersProvider(tournamentId)); + + return switch (players) { + AsyncData(value: final players) => + SliverFillRemaining(child: PlayersList(players)), + AsyncError(:final error) => SliverPadding( + padding: edgeInsets, + sliver: SliverFillRemaining( + child: Center(child: Text('Cannot load players data: $error')), + ), + ), + _ => SliverFillRemaining( + child: Shimmer( + child: ShimmerLoading( + isLoading: true, + child: PlayersList.loading(), + ), + ), + ), + }; + } +} + +enum _SortingTypes { player, elo, score } + +const _kTableRowVerticalPadding = 10.0; +const _kTableRowHorizontalPadding = 12.0; +const _kTableRowPadding = EdgeInsets.symmetric( + horizontal: _kTableRowHorizontalPadding, + vertical: _kTableRowVerticalPadding, +); +const _kHeaderTextStyle = TextStyle(fontWeight: FontWeight.bold); + +class PlayersList extends ConsumerStatefulWidget { + const PlayersList(this.players); + + PlayersList.loading() + : players = List.generate( + 10, + (_) => const BroadcastPlayerExtended( + name: '', + title: null, + rating: null, + federation: null, + fideId: null, + played: 0, + score: null, + ratingDiff: null, + ), + ).toIList(); + + final IList players; + + @override + ConsumerState createState() => _PlayersListState(); +} + +class _PlayersListState extends ConsumerState { + late IList players; + _SortingTypes? currentSort; + bool reverse = false; + + void sort(_SortingTypes newSort) { + final compare = switch (newSort) { + _SortingTypes.player => + (BroadcastPlayerExtended a, BroadcastPlayerExtended b) => + a.name.compareTo(b.name), + _SortingTypes.elo => + (BroadcastPlayerExtended a, BroadcastPlayerExtended b) { + if (a.rating == null) return 1; + if (b.rating == null) return -1; + return b.rating!.compareTo(a.rating!); + }, + _SortingTypes.score => + (BroadcastPlayerExtended a, BroadcastPlayerExtended b) { + if (a.score == null) return 1; + if (b.score == null) return -1; + return b.score!.compareTo(a.score!); + } + }; + + setState(() { + if (currentSort == newSort) { + reverse = !reverse; + } else { + reverse = false; + currentSort = newSort; + } + players = reverse ? players.sortReversed(compare) : players.sort(compare); + }); + } + + @override + void initState() { + super.initState(); + players = widget.players; + sort(_SortingTypes.score); + } + + @override + Widget build(BuildContext context) { + return Table( + columnWidths: const { + 1: MaxColumnWidth(FlexColumnWidth(0.3), FixedColumnWidth(100)), + 2: MaxColumnWidth(FlexColumnWidth(0.3), FixedColumnWidth(100)), + }, + children: [ + TableRow( + children: [ + _TableTitleCell( + text: context.l10n.player, + onTap: () { + sort(_SortingTypes.player); + }, + icon: (currentSort == _SortingTypes.player) + ? reverse + ? Icons.keyboard_arrow_up + : Icons.keyboard_arrow_down + : null, + ), + _TableTitleCell( + text: 'Elo', + onTap: () { + sort(_SortingTypes.elo); + }, + icon: (currentSort == _SortingTypes.elo) + ? reverse + ? Icons.keyboard_arrow_up + : Icons.keyboard_arrow_down + : null, + ), + _TableTitleCell( + text: context.l10n.broadcastScore, + onTap: () { + sort(_SortingTypes.score); + }, + icon: (currentSort == _SortingTypes.score) + ? reverse + ? Icons.keyboard_arrow_up + : Icons.keyboard_arrow_down + : null, + ), + ], + ), + ...players.indexed.map( + (player) => TableRow( + decoration: BoxDecoration( + color: player.$1.isEven + ? Theme.of(context).colorScheme.surfaceContainerLow + : Theme.of(context).colorScheme.surfaceContainerHigh, + ), + children: [ + Padding( + padding: _kTableRowPadding, + child: BroadcastPlayerWidget( + federation: player.$2.federation, + title: player.$2.title, + name: player.$2.name, + ), + ), + Padding( + padding: _kTableRowPadding, + child: Row( + children: [ + if (player.$2.rating != null) ...[ + Text(player.$2.rating.toString()), + const SizedBox(width: 5), + if (player.$2.ratingDiff != null) + ProgressionWidget(player.$2.ratingDiff!, fontSize: 16), + ], + ], + ), + ), + Padding( + padding: _kTableRowPadding, + child: Align( + alignment: Alignment.centerRight, + child: (player.$2.score != null) + ? Text( + '${player.$2.score!.toStringAsFixed((player.$2.score! == player.$2.score!.roundToDouble()) ? 0 : 1)}/${player.$2.played}', + ) + : const SizedBox.shrink(), + ), + ), + ], + ), + ), + ], + ); + } +} + +class _TableTitleCell extends StatelessWidget { + const _TableTitleCell({required this.text, required this.onTap, this.icon}); + + final String text; + final void Function() onTap; + final IconData? icon; + + @override + Widget build(BuildContext context) { + return TableCell( + verticalAlignment: + (icon == null) ? TableCellVerticalAlignment.fill : null, + child: GestureDetector( + onTap: onTap, + child: ColoredBox( + color: Theme.of(context).colorScheme.secondaryContainer, + child: Padding( + padding: _kTableRowPadding, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(text, style: _kHeaderTextStyle), + if (icon != null) Icon(icon), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/src/view/broadcast/broadcast_round_screen.dart b/lib/src/view/broadcast/broadcast_round_screen.dart index a0723ad863..7481875061 100644 --- a/lib/src/view/broadcast/broadcast_round_screen.dart +++ b/lib/src/view/broadcast/broadcast_round_screen.dart @@ -12,6 +12,7 @@ import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_boards_tab.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_overview_tab.dart'; +import 'package:lichess_mobile/src/view/broadcast/broadcast_players_tab.dart'; import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; @@ -27,7 +28,7 @@ class BroadcastRoundScreen extends ConsumerStatefulWidget { _BroadcastRoundScreenState createState() => _BroadcastRoundScreenState(); } -enum _CupertinoView { overview, boards } +enum _CupertinoView { overview, boards, players } class _BroadcastRoundScreenState extends ConsumerState with SingleTickerProviderStateMixin { @@ -41,7 +42,7 @@ class _BroadcastRoundScreenState extends ConsumerState @override void initState() { super.initState(); - _tabController = TabController(initialIndex: 0, length: 2, vsync: this); + _tabController = TabController(initialIndex: 0, length: 3, vsync: this); _selectedTournamentId = widget.broadcast.tour.id; _selectedRoundId = widget.broadcast.roundToLinkId; } @@ -113,6 +114,7 @@ class _BroadcastRoundScreenState extends ConsumerState children: { _CupertinoView.overview: Text(context.l10n.broadcastOverview), _CupertinoView.boards: Text(context.l10n.broadcastBoards), + _CupertinoView.players: Text(context.l10n.players), }, onValueChanged: (_CupertinoView? view) { if (view != null) { @@ -132,21 +134,28 @@ class _BroadcastRoundScreenState extends ConsumerState child: Column( children: [ Expanded( - child: selectedTab == _CupertinoView.overview - ? _TabView( - cupertinoTabSwitcher: tabSwitcher, - sliver: BroadcastOverviewTab( - broadcast: widget.broadcast, - tournamentId: _selectedTournamentId, - ), - ) - : _TabView( - cupertinoTabSwitcher: tabSwitcher, - sliver: BroadcastBoardsTab( - roundId: _selectedRoundId ?? - tournament.defaultRoundId, - ), + child: switch (selectedTab) { + _CupertinoView.overview => _TabView( + cupertinoTabSwitcher: tabSwitcher, + sliver: BroadcastOverviewTab( + broadcast: widget.broadcast, + tournamentId: _selectedTournamentId, ), + ), + _CupertinoView.boards => _TabView( + cupertinoTabSwitcher: tabSwitcher, + sliver: BroadcastBoardsTab( + roundId: + _selectedRoundId ?? tournament.defaultRoundId, + ), + ), + _CupertinoView.players => _TabView( + cupertinoTabSwitcher: tabSwitcher, + sliver: BroadcastPlayersTab( + tournamentId: _selectedTournamentId, + ), + ), + }, ), _BottomBar( tournament: tournament, @@ -171,6 +180,7 @@ class _BroadcastRoundScreenState extends ConsumerState tabs: [ Tab(text: context.l10n.broadcastOverview), Tab(text: context.l10n.broadcastBoards), + Tab(text: context.l10n.players), ], ), ), @@ -188,6 +198,11 @@ class _BroadcastRoundScreenState extends ConsumerState roundId: _selectedRoundId ?? tournament.defaultRoundId, ), ), + _TabView( + sliver: BroadcastPlayersTab( + tournamentId: _selectedTournamentId, + ), + ), ], ), bottomNavigationBar: _BottomBar( @@ -222,11 +237,6 @@ class _TabView extends StatelessWidget { @override Widget build(BuildContext context) { - final edgeInsets = MediaQuery.paddingOf(context) - - (cupertinoTabSwitcher != null - ? EdgeInsets.only(top: MediaQuery.paddingOf(context).top) - : EdgeInsets.zero) + - Styles.bodyPadding; return Shimmer( child: CustomScrollView( slivers: [ @@ -236,10 +246,7 @@ class _TabView extends StatelessWidget { EdgeInsets.only(top: MediaQuery.paddingOf(context).top), sliver: SliverToBoxAdapter(child: cupertinoTabSwitcher), ), - SliverPadding( - padding: edgeInsets, - sliver: sliver, - ), + sliver, ], ), ); diff --git a/lib/src/view/user/perf_stats_screen.dart b/lib/src/view/user/perf_stats_screen.dart index 9db550fd7d..b26f90f383 100644 --- a/lib/src/view/user/perf_stats_screen.dart +++ b/lib/src/view/user/perf_stats_screen.dart @@ -17,7 +17,6 @@ import 'package:lichess_mobile/src/model/game/game_repository.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/model/user/user_repository_providers.dart'; import 'package:lichess_mobile/src/network/http.dart'; -import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/duration.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; @@ -31,6 +30,7 @@ import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; +import 'package:lichess_mobile/src/widgets/progression_widget.dart'; import 'package:lichess_mobile/src/widgets/rating.dart'; import 'package:lichess_mobile/src/widgets/stat_card.dart'; import 'package:lichess_mobile/src/widgets/user_full_name.dart'; @@ -213,7 +213,7 @@ class _Body extends ConsumerWidget { context.l10n .perfStatProgressOverLastXGames('12') .replaceAll(':', ''), - child: _ProgressionWidget(data.progress), + child: ProgressionWidget(data.progress), ), StatCardRow([ if (data.rank != null) @@ -424,49 +424,6 @@ class _Body extends ConsumerWidget { } } -class _ProgressionWidget extends StatelessWidget { - final int progress; - - const _ProgressionWidget(this.progress); - - @override - Widget build(BuildContext context) { - const progressionFontSize = 20.0; - - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (progress != 0) ...[ - Icon( - progress > 0 - ? LichessIcons.arrow_full_upperright - : LichessIcons.arrow_full_lowerright, - color: progress > 0 - ? context.lichessColors.good - : context.lichessColors.error, - ), - Text( - progress.abs().toString(), - style: TextStyle( - color: progress > 0 - ? context.lichessColors.good - : context.lichessColors.error, - fontSize: progressionFontSize, - ), - ), - ] else - Text( - '0', - style: TextStyle( - color: textShade(context, _customOpacity), - fontSize: progressionFontSize, - ), - ), - ], - ); - } -} - class _UserGameWidget extends StatelessWidget { final UserPerfGame? game; diff --git a/lib/src/widgets/progression_widget.dart b/lib/src/widgets/progression_widget.dart new file mode 100644 index 0000000000..544c48ecb1 --- /dev/null +++ b/lib/src/widgets/progression_widget.dart @@ -0,0 +1,48 @@ +import 'package:flutter/widgets.dart'; +import 'package:lichess_mobile/src/styles/lichess_icons.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; + +const _customOpacity = 0.6; + +class ProgressionWidget extends StatelessWidget { + final int progress; + final double fontSize; + + const ProgressionWidget(this.progress, {this.fontSize = 20}); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (progress != 0) ...[ + Icon( + progress > 0 + ? LichessIcons.arrow_full_upperright + : LichessIcons.arrow_full_lowerright, + size: fontSize, + color: progress > 0 + ? context.lichessColors.good + : context.lichessColors.error, + ), + Text( + progress.abs().toString(), + style: TextStyle( + color: progress > 0 + ? context.lichessColors.good + : context.lichessColors.error, + fontSize: fontSize, + ), + ), + ] else + Text( + '0', + style: TextStyle( + color: textShade(context, _customOpacity), + fontSize: fontSize, + ), + ), + ], + ); + } +} From 262dc97872a6f06e52f1907dbf27c6dd028043a4 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 10 Dec 2024 15:45:43 +0100 Subject: [PATCH 876/979] Fix create game section not shown when offline on tablets Closes #1248 --- lib/src/view/home/home_tab_screen.dart | 9 ++-- lib/src/view/play/quick_game_matrix.dart | 68 ++++++++++++++---------- 2 files changed, 43 insertions(+), 34 deletions(-) diff --git a/lib/src/view/home/home_tab_screen.dart b/lib/src/view/home/home_tab_screen.dart index 0505976032..2754be744d 100644 --- a/lib/src/view/home/home_tab_screen.dart +++ b/lib/src/view/home/home_tab_screen.dart @@ -309,10 +309,9 @@ class _HomeScreenState extends ConsumerState with RouteAware { if (isTablet) Row( children: [ - if (status.isOnline) - const Flexible( - child: _TabletCreateAGameSection(), - ), + const Flexible( + child: _TabletCreateAGameSection(), + ), Flexible( child: Column( children: welcomeWidgets, @@ -361,7 +360,7 @@ class _HomeScreenState extends ConsumerState with RouteAware { child: Column( children: [ const SizedBox(height: 8.0), - if (status.isOnline) const _TabletCreateAGameSection(), + const _TabletCreateAGameSection(), if (status.isOnline) _OngoingGamesPreview( ongoingGames, diff --git a/lib/src/view/play/quick_game_matrix.dart b/lib/src/view/play/quick_game_matrix.dart index 0a49662d6d..f8753b27d0 100644 --- a/lib/src/view/play/quick_game_matrix.dart +++ b/lib/src/view/play/quick_game_matrix.dart @@ -5,6 +5,7 @@ import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/common/speed.dart'; import 'package:lichess_mobile/src/model/common/time_increment.dart'; import 'package:lichess_mobile/src/model/lobby/game_seek.dart'; +import 'package:lichess_mobile/src/network/connectivity.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; @@ -85,6 +86,8 @@ class _SectionChoices extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final session = ref.watch(authSessionProvider); + final isOnline = + ref.watch(connectivityChangesProvider).valueOrNull?.isOnline ?? false; final choiceWidgets = choices .mapIndexed((index, choice) { return [ @@ -99,15 +102,17 @@ class _SectionChoices extends ConsumerWidget { ), ), speed: choice.speed, - onSelected: (bool selected) { - pushPlatformRoute( - context, - rootNavigator: true, - builder: (_) => GameScreen( - seek: GameSeek.fastPairing(choice, session), - ), - ); - }, + onTap: isOnline + ? () { + pushPlatformRoute( + context, + rootNavigator: true, + builder: (_) => GameScreen( + seek: GameSeek.fastPairing(choice, session), + ), + ); + } + : null, ), ), if (index < choices.length - 1) @@ -127,12 +132,14 @@ class _SectionChoices extends ConsumerWidget { Expanded( child: _ChoiceChip( label: Text(context.l10n.custom), - onSelected: (bool selected) { - pushPlatformRoute( - context, - builder: (_) => const CreateCustomGameScreen(), - ); - }, + onTap: isOnline + ? () { + pushPlatformRoute( + context, + builder: (_) => const CreateCustomGameScreen(), + ); + } + : null, ), ), ], @@ -146,13 +153,13 @@ class _ChoiceChip extends StatefulWidget { const _ChoiceChip({ required this.label, this.speed, - required this.onSelected, + required this.onTap, super.key, }); final Widget label; final Speed? speed; - final void Function(bool selected) onSelected; + final void Function()? onTap; @override State<_ChoiceChip> createState() => _ChoiceChipState(); @@ -165,18 +172,21 @@ class _ChoiceChipState extends State<_ChoiceChip> { ? Styles.cupertinoCardColor.resolveFrom(context).withValues(alpha: 0.7) : Theme.of(context).colorScheme.surfaceContainer.withValues(alpha: 0.7); - return Container( - decoration: BoxDecoration( - color: cardColor, - borderRadius: const BorderRadius.all(Radius.circular(6.0)), - ), - child: AdaptiveInkWell( - borderRadius: const BorderRadius.all(Radius.circular(5.0)), - onTap: () => widget.onSelected(true), - splashColor: Theme.of(context).primaryColor.withValues(alpha: 0.2), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0), - child: Center(child: widget.label), + return Opacity( + opacity: widget.onTap != null ? 1.0 : 0.5, + child: Container( + decoration: BoxDecoration( + color: cardColor, + borderRadius: const BorderRadius.all(Radius.circular(6.0)), + ), + child: AdaptiveInkWell( + borderRadius: const BorderRadius.all(Radius.circular(5.0)), + onTap: widget.onTap, + splashColor: Theme.of(context).primaryColor.withValues(alpha: 0.2), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Center(child: widget.label), + ), ), ), ); From 524c0168ee916fef3a4108520c74fab2d4ac03a8 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Tue, 10 Dec 2024 17:01:42 +0100 Subject: [PATCH 877/979] Remove forgotten sorting logic that was left in the provider --- .../model/broadcast/broadcast_providers.dart | 48 +++---------------- 1 file changed, 7 insertions(+), 41 deletions(-) diff --git a/lib/src/model/broadcast/broadcast_providers.dart b/lib/src/model/broadcast/broadcast_providers.dart index b782ae7081..a7e02edc8d 100644 --- a/lib/src/model/broadcast/broadcast_providers.dart +++ b/lib/src/model/broadcast/broadcast_providers.dart @@ -55,48 +55,14 @@ Future broadcastTournament( ); } -enum BroadcastPlayersSortingTypes { player, elo, score } - @riverpod -class BroadcastPlayers extends _$BroadcastPlayers { - @override - Future> build( - BroadcastTournamentId tournamentId, - ) async { - final players = ref.withClient( - (client) => BroadcastRepository(client).getPlayers(tournamentId), - ); - - return players; - } - - void sort(BroadcastPlayersSortingTypes sortingType, [bool reverse = false]) { - if (!state.hasValue) return; - - final compare = switch (sortingType) { - BroadcastPlayersSortingTypes.player => - (BroadcastPlayerExtended a, BroadcastPlayerExtended b) => - a.name.compareTo(b.name), - BroadcastPlayersSortingTypes.elo => - (BroadcastPlayerExtended a, BroadcastPlayerExtended b) { - if (a.rating == null) return -1; - if (b.rating == null) return 1; - return b.rating!.compareTo(a.rating!); - }, - BroadcastPlayersSortingTypes.score => - (BroadcastPlayerExtended a, BroadcastPlayerExtended b) { - if (a.score == null) return -1; - if (b.score == null) return 1; - return b.score!.compareTo(a.score!); - } - }; - - state = AsyncData( - reverse - ? state.requireValue.sortReversed(compare) - : state.requireValue.sort(compare), - ); - } +Future> broadcastPlayers( + Ref ref, + BroadcastTournamentId tournamentId, +) { + return ref.withClient( + (client) => BroadcastRepository(client).getPlayers(tournamentId), + ); } @Riverpod(keepAlive: true) From 1088f746234335ef3cee00db3660ac5a711f9bb7 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 10 Dec 2024 18:08:36 +0100 Subject: [PATCH 878/979] Add default value --- lib/src/model/settings/general_preferences.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/model/settings/general_preferences.dart b/lib/src/model/settings/general_preferences.dart index ef458d8bd6..3e87d9c6cf 100644 --- a/lib/src/model/settings/general_preferences.dart +++ b/lib/src/model/settings/general_preferences.dart @@ -100,7 +100,7 @@ class GeneralPrefs with _$GeneralPrefs implements Serializable { @JsonKey(defaultValue: 0.8) required double masterVolume, /// Should enable system color palette (android 12+ only) - required bool systemColors, + @JsonKey(defaultValue: true) required bool systemColors, /// Locale to use in the app, use system locale if null @JsonKey(toJson: _localeToJson, fromJson: _localeFromJson) Locale? locale, From 5816664434208f9e22839293daeed325eadf7ecd Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 10 Dec 2024 18:08:47 +0100 Subject: [PATCH 879/979] Fix overflows and list --- .../view/broadcast/broadcast_players_tab.dart | 39 ++++++++++++------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_players_tab.dart b/lib/src/view/broadcast/broadcast_players_tab.dart index f47c7d3b1d..16979e9226 100644 --- a/lib/src/view/broadcast/broadcast_players_tab.dart +++ b/lib/src/view/broadcast/broadcast_players_tab.dart @@ -26,21 +26,26 @@ class BroadcastPlayersTab extends ConsumerWidget { final players = ref.watch(broadcastPlayersProvider(tournamentId)); return switch (players) { - AsyncData(value: final players) => - SliverFillRemaining(child: PlayersList(players)), + AsyncData(value: final players) => SliverList( + delegate: SliverChildListDelegate.fixed([ + PlayersList(players), + ]), + ), AsyncError(:final error) => SliverPadding( padding: edgeInsets, sliver: SliverFillRemaining( child: Center(child: Text('Cannot load players data: $error')), ), ), - _ => SliverFillRemaining( - child: Shimmer( - child: ShimmerLoading( - isLoading: true, - child: PlayersList.loading(), + _ => SliverList( + delegate: SliverChildListDelegate.fixed([ + Shimmer( + child: ShimmerLoading( + isLoading: true, + child: PlayersList.loading(), + ), ), - ), + ]), ), }; } @@ -48,8 +53,8 @@ class BroadcastPlayersTab extends ConsumerWidget { enum _SortingTypes { player, elo, score } -const _kTableRowVerticalPadding = 10.0; -const _kTableRowHorizontalPadding = 12.0; +const _kTableRowVerticalPadding = 12.0; +const _kTableRowHorizontalPadding = 8.0; const _kTableRowPadding = EdgeInsets.symmetric( horizontal: _kTableRowHorizontalPadding, vertical: _kTableRowVerticalPadding, @@ -126,8 +131,8 @@ class _PlayersListState extends ConsumerState { Widget build(BuildContext context) { return Table( columnWidths: const { - 1: MaxColumnWidth(FlexColumnWidth(0.3), FixedColumnWidth(100)), - 2: MaxColumnWidth(FlexColumnWidth(0.3), FixedColumnWidth(100)), + 1: MaxColumnWidth(FlexColumnWidth(0.2), FixedColumnWidth(100)), + 2: MaxColumnWidth(FlexColumnWidth(0.2), FixedColumnWidth(100)), }, children: [ TableRow( @@ -191,7 +196,7 @@ class _PlayersListState extends ConsumerState { Text(player.$2.rating.toString()), const SizedBox(width: 5), if (player.$2.ratingDiff != null) - ProgressionWidget(player.$2.ratingDiff!, fontSize: 16), + ProgressionWidget(player.$2.ratingDiff!, fontSize: 14), ], ], ), @@ -236,7 +241,13 @@ class _TableTitleCell extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text(text, style: _kHeaderTextStyle), + Expanded( + child: Text( + text, + style: _kHeaderTextStyle, + overflow: TextOverflow.ellipsis, + ), + ), if (icon != null) Icon(icon), ], ), From 5f1635bbfe83e85d1df63951507b00f89427f96c Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 11 Dec 2024 11:05:21 +0100 Subject: [PATCH 880/979] More broadcast tweaks --- .../model/settings/general_preferences.dart | 2 +- lib/src/utils/image.dart | 5 +- .../view/broadcast/broadcast_list_screen.dart | 196 +++++++++--------- .../broadcast/broadcast_round_screen.dart | 11 +- 4 files changed, 110 insertions(+), 104 deletions(-) diff --git a/lib/src/model/settings/general_preferences.dart b/lib/src/model/settings/general_preferences.dart index ef458d8bd6..3e87d9c6cf 100644 --- a/lib/src/model/settings/general_preferences.dart +++ b/lib/src/model/settings/general_preferences.dart @@ -100,7 +100,7 @@ class GeneralPrefs with _$GeneralPrefs implements Serializable { @JsonKey(defaultValue: 0.8) required double masterVolume, /// Should enable system color palette (android 12+ only) - required bool systemColors, + @JsonKey(defaultValue: true) required bool systemColors, /// Locale to use in the app, use system locale if null @JsonKey(toJson: _localeToJson, fromJson: _localeFromJson) Locale? locale, diff --git a/lib/src/utils/image.dart b/lib/src/utils/image.dart index c79603ab7b..1067e67e3a 100644 --- a/lib/src/utils/image.dart +++ b/lib/src/utils/image.dart @@ -122,13 +122,10 @@ class ImageColorWorker { final List scoredResults = Score.score( colorToCount, desired: 1, - fallbackColorARGB: 0xFFEEEEEE, + fallbackColorARGB: 0xFF000000, filter: false, ); final Hct sourceColor = Hct.fromInt(scoredResults.first); - if (sourceColor.tone > 90.0) { - sourceColor.tone = 90.0; - } final scheme = SchemeFidelity( sourceColorHct: sourceColor, isDark: false, diff --git a/lib/src/view/broadcast/broadcast_list_screen.dart b/lib/src/view/broadcast/broadcast_list_screen.dart index 3dafbabb5f..6ed0dab15d 100644 --- a/lib/src/view/broadcast/broadcast_list_screen.dart +++ b/lib/src/view/broadcast/broadcast_list_screen.dart @@ -290,8 +290,10 @@ class _BroadcastGridItemState extends State { scheduleMicrotask(() => _fetchImageAndColors(provider)); }); } else if (widget.worker.closed == false) { - final response = await widget.worker - .getImageColors(provider.url, fileExtension: 'webp'); + final response = await widget.worker.getImageColors( + provider.url, + fileExtension: 'webp', + ); if (response != null) { final (:image, :primaryContainer, :onPrimaryContainer, :error) = response; @@ -346,35 +348,35 @@ class _BroadcastGridItemState extends State { onTapDown: (_) => _onTapDown(), onTapCancel: _onTapCancel, onTapUp: (_) => _onTapCancel(), - child: AnimatedOpacity( - opacity: _tapDown ? 1.0 : 0.80, - duration: const Duration(milliseconds: 100), - child: AnimatedContainer( - duration: const Duration(milliseconds: 500), - clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - borderRadius: kBroadcastGridItemBorderRadius, - color: backgroundColor, - boxShadow: Theme.of(context).platform == TargetPlatform.iOS - ? null - : kElevationToShadow[1], - ), - child: Stack( - children: [ - ShaderMask( - blendMode: BlendMode.dstOut, - shaderCallback: (bounds) { - return LinearGradient( - begin: Alignment.center, - end: Alignment.bottomCenter, - colors: [ - backgroundColor.withValues(alpha: 0.0), - backgroundColor.withValues(alpha: 1.0), - ], - stops: const [0.5, 1.10], - tileMode: TileMode.clamp, - ).createShader(bounds); - }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 500), + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + borderRadius: kBroadcastGridItemBorderRadius, + color: backgroundColor, + boxShadow: Theme.of(context).platform == TargetPlatform.iOS + ? null + : kElevationToShadow[1], + ), + child: Stack( + children: [ + ShaderMask( + blendMode: BlendMode.dstOut, + shaderCallback: (bounds) { + return LinearGradient( + begin: const Alignment(0.0, 0.5), + end: Alignment.bottomCenter, + colors: [ + backgroundColor.withValues(alpha: 0.0), + backgroundColor.withValues(alpha: 1.0), + ], + stops: const [0.5, 1.10], + tileMode: TileMode.clamp, + ).createShader(bounds); + }, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 100), + opacity: _tapDown ? 1.0 : 0.7, child: AspectRatio( aspectRatio: 2.0, child: _imageProvider != null @@ -397,22 +399,44 @@ class _BroadcastGridItemState extends State { : const SizedBox.shrink(), ), ), - Positioned( - left: 0, - right: 0, - bottom: 12.0, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (widget.broadcast.round.startsAt != null || - widget.broadcast.isLive) - Padding( - padding: kBroadcastGridItemContentPadding, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ + ), + Positioned( + left: 0, + right: 0, + bottom: 12.0, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (widget.broadcast.round.startsAt != null || + widget.broadcast.isLive) + Padding( + padding: kBroadcastGridItemContentPadding, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + widget.broadcast.round.name, + style: TextStyle( + fontSize: 12, + color: subTitleColor, + ), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + const SizedBox(width: 4.0), + if (widget.broadcast.isLive) Text( - widget.broadcast.round.name, + 'LIVE', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.bold, + color: liveColor, + ), + overflow: TextOverflow.ellipsis, + ) + else + Text( + _formatDate(widget.broadcast.round.startsAt!), style: TextStyle( fontSize: 12, color: subTitleColor, @@ -420,65 +444,43 @@ class _BroadcastGridItemState extends State { overflow: TextOverflow.ellipsis, maxLines: 1, ), - const SizedBox(width: 4.0), - if (widget.broadcast.isLive) - Text( - 'LIVE', - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.bold, - color: liveColor, - ), - overflow: TextOverflow.ellipsis, - ) - else - Text( - _formatDate(widget.broadcast.round.startsAt!), - style: TextStyle( - fontSize: 12, - color: subTitleColor, - ), - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), - ], - ), + ], ), - Padding( - padding: kBroadcastGridItemContentPadding.add( - const EdgeInsets.symmetric(vertical: 3.0), + ), + Padding( + padding: kBroadcastGridItemContentPadding.add( + const EdgeInsets.symmetric(vertical: 3.0), + ), + child: Text( + widget.broadcast.title, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: titleColor, + fontWeight: FontWeight.bold, + height: 1.0, + fontSize: 16, ), + ), + ), + if (widget.broadcast.tour.information.players != null) + Padding( + padding: kBroadcastGridItemContentPadding, child: Text( - widget.broadcast.title, - maxLines: 2, - overflow: TextOverflow.ellipsis, + widget.broadcast.tour.information.players!, style: TextStyle( - color: titleColor, - fontWeight: FontWeight.bold, - height: 1.0, - fontSize: 16, + fontSize: 12, + color: subTitleColor, + letterSpacing: -0.2, ), + overflow: TextOverflow.ellipsis, + maxLines: 1, ), ), - if (widget.broadcast.tour.information.players != null) - Padding( - padding: kBroadcastGridItemContentPadding, - child: Text( - widget.broadcast.tour.information.players!, - style: TextStyle( - fontSize: 12, - color: subTitleColor, - letterSpacing: -0.2, - ), - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), - ), - ], - ), + ], ), - ], - ), + ), + ], ), ), ); diff --git a/lib/src/view/broadcast/broadcast_round_screen.dart b/lib/src/view/broadcast/broadcast_round_screen.dart index a0723ad863..48445e1465 100644 --- a/lib/src/view/broadcast/broadcast_round_screen.dart +++ b/lib/src/view/broadcast/broadcast_round_screen.dart @@ -363,7 +363,8 @@ class _RoundSelectorMenu extends ConsumerStatefulWidget { ConsumerState<_RoundSelectorMenu> createState() => _RoundSelectorState(); } -final _dateFormat = DateFormat.yMd().add_jm(); +final _dateFormatMonth = DateFormat.MMMd().add_jm(); +final _dateFormatYearMonth = DateFormat.yMMMd().add_jm(); class _RoundSelectorState extends ConsumerState<_RoundSelectorMenu> { final currentRoundKey = GlobalKey(); @@ -394,7 +395,13 @@ class _RoundSelectorState extends ConsumerState<_RoundSelectorMenu> { if (round.startsAt != null || round.startsAfterPrevious) ...[ Text( round.startsAt != null - ? _dateFormat.format(round.startsAt!) + ? round.startsAt! + .difference(DateTime.now()) + .inDays + .abs() < + 30 + ? _dateFormatMonth.format(round.startsAt!) + : _dateFormatYearMonth.format(round.startsAt!) : context.l10n.broadcastStartsAfter( widget.rounds[index - 1].name, ), From f537fe281b3570eed3d4d64b896a045b222c09e6 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 11 Dec 2024 11:16:04 +0100 Subject: [PATCH 881/979] Fix locale provider Fixes #1252 --- lib/src/localizations.dart | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/src/localizations.dart b/lib/src/localizations.dart index 830328c270..d89448ccb3 100644 --- a/lib/src/localizations.dart +++ b/lib/src/localizations.dart @@ -1,5 +1,6 @@ import 'package:flutter/widgets.dart'; import 'package:lichess_mobile/l10n/l10n.dart'; +import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'localizations.g.dart'; @@ -13,6 +14,7 @@ typedef ActiveLocalizations = ({ class Localizations extends _$Localizations { @override ActiveLocalizations build() { + final generalPrefs = ref.watch(generalPreferencesProvider); final observer = _LocaleObserver((locales) { _update(); }); @@ -20,14 +22,21 @@ class Localizations extends _$Localizations { binding.addObserver(observer); ref.onDispose(() => binding.removeObserver(observer)); - return _getLocale(); + return _getLocale(generalPrefs); } void _update() { - state = _getLocale(); + final generalPrefs = ref.read(generalPreferencesProvider); + state = _getLocale(generalPrefs); } - ActiveLocalizations _getLocale() { + ActiveLocalizations _getLocale(GeneralPrefs prefs) { + if (prefs.locale != null) { + return ( + locale: prefs.locale!, + strings: lookupAppLocalizations(prefs.locale!), + ); + } final locale = WidgetsBinding.instance.platformDispatcher.locale; return ( locale: locale, From ef3d92952a0907fababa9df791dff25d20b20eb5 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 11 Dec 2024 12:39:09 +0100 Subject: [PATCH 882/979] Convert players tab to a list for perf --- .../view/broadcast/broadcast_players_tab.dart | 290 ++++++++++-------- 1 file changed, 167 insertions(+), 123 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_players_tab.dart b/lib/src/view/broadcast/broadcast_players_tab.dart index 16979e9226..1105463580 100644 --- a/lib/src/view/broadcast/broadcast_players_tab.dart +++ b/lib/src/view/broadcast/broadcast_players_tab.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -26,27 +28,14 @@ class BroadcastPlayersTab extends ConsumerWidget { final players = ref.watch(broadcastPlayersProvider(tournamentId)); return switch (players) { - AsyncData(value: final players) => SliverList( - delegate: SliverChildListDelegate.fixed([ - PlayersList(players), - ]), - ), + AsyncData(value: final players) => PlayersList(players), AsyncError(:final error) => SliverPadding( padding: edgeInsets, sliver: SliverFillRemaining( child: Center(child: Text('Cannot load players data: $error')), ), ), - _ => SliverList( - delegate: SliverChildListDelegate.fixed([ - Shimmer( - child: ShimmerLoading( - isLoading: true, - child: PlayersList.loading(), - ), - ), - ]), - ), + _ => PlayersList.loading(), }; } } @@ -59,10 +48,11 @@ const _kTableRowPadding = EdgeInsets.symmetric( horizontal: _kTableRowHorizontalPadding, vertical: _kTableRowVerticalPadding, ); -const _kHeaderTextStyle = TextStyle(fontWeight: FontWeight.bold); +const _kHeaderTextStyle = + TextStyle(fontWeight: FontWeight.bold, overflow: TextOverflow.ellipsis); class PlayersList extends ConsumerStatefulWidget { - const PlayersList(this.players); + const PlayersList(this.players) : _isLoading = false; PlayersList.loading() : players = List.generate( @@ -77,9 +67,11 @@ class PlayersList extends ConsumerStatefulWidget { score: null, ratingDiff: null, ), - ).toIList(); + ).toIList(), + _isLoading = true; final IList players; + final bool _isLoading; @override ConsumerState createState() => _PlayersListState(); @@ -87,10 +79,10 @@ class PlayersList extends ConsumerStatefulWidget { class _PlayersListState extends ConsumerState { late IList players; - _SortingTypes? currentSort; + _SortingTypes currentSort = _SortingTypes.score; bool reverse = false; - void sort(_SortingTypes newSort) { + void sort(_SortingTypes newSort, {bool toggleReverse = false}) { final compare = switch (newSort) { _SortingTypes.player => (BroadcastPlayerExtended a, BroadcastPlayerExtended b) => @@ -110,12 +102,12 @@ class _PlayersListState extends ConsumerState { }; setState(() { - if (currentSort == newSort) { - reverse = !reverse; - } else { + if (currentSort != newSort) { reverse = false; - currentSort = newSort; + } else { + reverse = toggleReverse ? !reverse : reverse; } + currentSort = newSort; players = reverse ? players.sortReversed(compare) : players.sort(compare); }); } @@ -127,130 +119,182 @@ class _PlayersListState extends ConsumerState { sort(_SortingTypes.score); } + @override + void didUpdateWidget(PlayersList oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.players != widget.players) { + players = widget.players; + sort(_SortingTypes.score); + } + } + @override Widget build(BuildContext context) { - return Table( - columnWidths: const { - 1: MaxColumnWidth(FlexColumnWidth(0.2), FixedColumnWidth(100)), - 2: MaxColumnWidth(FlexColumnWidth(0.2), FixedColumnWidth(100)), - }, - children: [ - TableRow( - children: [ - _TableTitleCell( - text: context.l10n.player, - onTap: () { - sort(_SortingTypes.player); - }, - icon: (currentSort == _SortingTypes.player) - ? reverse - ? Icons.keyboard_arrow_up - : Icons.keyboard_arrow_down - : null, - ), - _TableTitleCell( - text: 'Elo', - onTap: () { - sort(_SortingTypes.elo); - }, - icon: (currentSort == _SortingTypes.elo) - ? reverse - ? Icons.keyboard_arrow_up - : Icons.keyboard_arrow_down - : null, - ), - _TableTitleCell( - text: context.l10n.broadcastScore, - onTap: () { - sort(_SortingTypes.score); - }, - icon: (currentSort == _SortingTypes.score) - ? reverse - ? Icons.keyboard_arrow_up - : Icons.keyboard_arrow_down - : null, - ), - ], - ), - ...players.indexed.map( - (player) => TableRow( - decoration: BoxDecoration( - color: player.$1.isEven - ? Theme.of(context).colorScheme.surfaceContainerLow - : Theme.of(context).colorScheme.surfaceContainerHigh, + final double eloWidth = max(MediaQuery.sizeOf(context).width * 0.2, 100); + final double scoreWidth = max(MediaQuery.sizeOf(context).width * 0.15, 70); + + return SliverList.builder( + itemCount: players.length + 1, + itemBuilder: (context, index) { + if (widget._isLoading) { + return ShimmerLoading( + isLoading: true, + child: Container( + height: 50, + decoration: BoxDecoration( + color: index.isEven + ? Theme.of(context).colorScheme.surfaceContainerLow + : Theme.of(context).colorScheme.surfaceContainerHigh, + ), ), + ); + } + + if (index == 0) { + return Row( + crossAxisAlignment: CrossAxisAlignment.center, children: [ - Padding( - padding: _kTableRowPadding, - child: BroadcastPlayerWidget( - federation: player.$2.federation, - title: player.$2.title, - name: player.$2.name, + Expanded( + child: _TableTitleCell( + title: Text(context.l10n.player, style: _kHeaderTextStyle), + onTap: () => sort( + _SortingTypes.player, + toggleReverse: currentSort == _SortingTypes.player, + ), + sortIcon: (currentSort == _SortingTypes.player) + ? (reverse + ? Icons.keyboard_arrow_up + : Icons.keyboard_arrow_down) + : null, ), ), - Padding( - padding: _kTableRowPadding, - child: Row( - children: [ - if (player.$2.rating != null) ...[ - Text(player.$2.rating.toString()), - const SizedBox(width: 5), - if (player.$2.ratingDiff != null) - ProgressionWidget(player.$2.ratingDiff!, fontSize: 14), - ], - ], + SizedBox( + width: eloWidth, + child: _TableTitleCell( + title: const Text('Elo', style: _kHeaderTextStyle), + onTap: () => sort( + _SortingTypes.elo, + toggleReverse: currentSort == _SortingTypes.elo, + ), + sortIcon: (currentSort == _SortingTypes.elo) + ? (reverse + ? Icons.keyboard_arrow_up + : Icons.keyboard_arrow_down) + : null, ), ), - Padding( - padding: _kTableRowPadding, - child: Align( - alignment: Alignment.centerRight, - child: (player.$2.score != null) - ? Text( - '${player.$2.score!.toStringAsFixed((player.$2.score! == player.$2.score!.roundToDouble()) ? 0 : 1)}/${player.$2.played}', - ) - : const SizedBox.shrink(), + SizedBox( + width: scoreWidth, + child: _TableTitleCell( + title: Text( + context.l10n.broadcastScore, + style: _kHeaderTextStyle, + ), + onTap: () => sort( + _SortingTypes.score, + toggleReverse: currentSort == _SortingTypes.score, + ), + sortIcon: (currentSort == _SortingTypes.score) + ? (reverse + ? Icons.keyboard_arrow_up + : Icons.keyboard_arrow_down) + : null, ), ), ], - ), - ), - ], + ); + } else { + final player = players[index - 1]; + return Container( + decoration: BoxDecoration( + color: index.isEven + ? Theme.of(context).colorScheme.surfaceContainerLow + : Theme.of(context).colorScheme.surfaceContainerHigh, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Padding( + padding: _kTableRowPadding, + child: BroadcastPlayerWidget( + federation: player.federation, + title: player.title, + name: player.name, + ), + ), + ), + SizedBox( + width: eloWidth, + child: Padding( + padding: _kTableRowPadding, + child: Row( + children: [ + if (player.rating != null) ...[ + Text(player.rating.toString()), + const SizedBox(width: 5), + if (player.ratingDiff != null) + ProgressionWidget(player.ratingDiff!, fontSize: 14), + ], + ], + ), + ), + ), + SizedBox( + width: scoreWidth, + child: Padding( + padding: _kTableRowPadding, + child: (player.score != null) + ? Text( + '${player.score!.toStringAsFixed((player.score! == player.score!.roundToDouble()) ? 0 : 1)}/${player.played}', + ) + : const SizedBox.shrink(), + ), + ), + ], + ), + ); + } + }, ); } } class _TableTitleCell extends StatelessWidget { - const _TableTitleCell({required this.text, required this.onTap, this.icon}); + const _TableTitleCell({ + required this.title, + required this.onTap, + this.sortIcon, + }); - final String text; + final Widget title; final void Function() onTap; - final IconData? icon; + final IconData? sortIcon; @override Widget build(BuildContext context) { - return TableCell( - verticalAlignment: - (icon == null) ? TableCellVerticalAlignment.fill : null, + return SizedBox( + height: 44, child: GestureDetector( onTap: onTap, - child: ColoredBox( - color: Theme.of(context).colorScheme.secondaryContainer, - child: Padding( - padding: _kTableRowPadding, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text( - text, - style: _kHeaderTextStyle, - overflow: TextOverflow.ellipsis, + child: Padding( + padding: _kTableRowPadding, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: title, + ), + if (sortIcon != null) + Text( + String.fromCharCode(sortIcon!.codePoint), + style: _kHeaderTextStyle.copyWith( + fontSize: 16, + fontFamily: sortIcon!.fontFamily, ), ), - if (icon != null) Icon(icon), - ], - ), + ], ), ), ), From a82aecb31669b5b2537be39482e06db4d7213541 Mon Sep 17 00:00:00 2001 From: Noah <78898963+HaonRekcef@users.noreply.github.com> Date: Wed, 11 Dec 2024 12:41:41 +0100 Subject: [PATCH 883/979] refacter chapter GIF and use gifAPInames for Screenshots --- lib/src/model/game/game_share_service.dart | 31 +++++++++++++++++++++- lib/src/view/study/study_screen.dart | 2 +- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/lib/src/model/game/game_share_service.dart b/lib/src/model/game/game_share_service.dart index 09df89a3a4..b182564e67 100644 --- a/lib/src/model/game/game_share_service.dart +++ b/lib/src/model/game/game_share_service.dart @@ -69,7 +69,7 @@ class GameShareService { .read(defaultClientProvider) .get( Uri.parse( - '$kLichessCDNHost/export/fen.gif?fen=${Uri.encodeComponent(fen)}&color=${orientation.name}${lastMove != null ? '&lastMove=${lastMove.uci}' : ''}&theme=${boardTheme.name}&piece=${pieceTheme.name}', + '$kLichessCDNHost/export/fen.gif?fen=${Uri.encodeComponent(fen)}&color=${orientation.name}${lastMove != null ? '&lastMove=${lastMove.uci}' : ''}&theme=${boardTheme.gifApiName}&piece=${pieceTheme.name}', ), ) .timeout(const Duration(seconds: 1)); @@ -99,4 +99,33 @@ class GameShareService { } return XFile.fromData(resp.bodyBytes, mimeType: 'image/gif'); } + + /// Fetches the GIF animation of a study chapter. + Future chapterGif( + StringId id, + StringId chapterId, + ) async { + final boardPreferences = _ref.read(boardPreferencesProvider); + final boardTheme = boardPreferences.boardTheme == BoardTheme.system + ? BoardTheme.brown + : boardPreferences.boardTheme; + final pieceTheme = boardPreferences.pieceSet; + + final resp = await _ref + .read(lichessClientProvider) + .get( + lichessUri( + '/study/$id/$chapterId.gif', + { + 'theme': boardTheme.gifApiName, + 'piece': pieceTheme.name, + }, + ), + ) + .timeout(const Duration(seconds: 1)); + if (resp.statusCode != 200) { + throw Exception('Failed to get GIF'); + } + return XFile.fromData(resp.bodyBytes, mimeType: 'image/gif'); + } } diff --git a/lib/src/view/study/study_screen.dart b/lib/src/view/study/study_screen.dart index a91783c806..ed66346eb7 100644 --- a/lib/src/view/study/study_screen.dart +++ b/lib/src/view/study/study_screen.dart @@ -330,7 +330,7 @@ class _StudyMenu extends ConsumerWidget { onPressed: (_) async { try { final gif = - await ref.read(studyRepositoryProvider).chapterGif( + await ref.read(gameShareServiceProvider).chapterGif( state.study.id, state.study.chapter.id, ); From f18288524a111cd706d68067bcf71fc332c527f1 Mon Sep 17 00:00:00 2001 From: Noah <78898963+HaonRekcef@users.noreply.github.com> Date: Wed, 11 Dec 2024 12:42:31 +0100 Subject: [PATCH 884/979] remove duplicate code --- lib/src/model/study/study_repository.dart | 30 ----------------------- 1 file changed, 30 deletions(-) diff --git a/lib/src/model/study/study_repository.dart b/lib/src/model/study/study_repository.dart index 89d745be37..c0c2cc4886 100644 --- a/lib/src/model/study/study_repository.dart +++ b/lib/src/model/study/study_repository.dart @@ -5,13 +5,11 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:http/http.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/model/study/study.dart'; import 'package:lichess_mobile/src/model/study/study_filter.dart'; import 'package:lichess_mobile/src/model/study/study_list_paginator.dart'; import 'package:lichess_mobile/src/network/http.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:share_plus/share_plus.dart'; part 'study_repository.g.dart'; @@ -104,32 +102,4 @@ class StudyRepository { return utf8.decode(pgnBytes); } - - /// Fetches the GIF animation of a study chapter. - Future chapterGif( - StudyId id, - StudyChapterId chapterId, - ) async { - final boardPreferences = ref.read(boardPreferencesProvider); - final boardTheme = boardPreferences.boardTheme == BoardTheme.system - ? BoardTheme.brown - : boardPreferences.boardTheme; - final pieceTheme = boardPreferences.pieceSet; - - final resp = await client - .get( - lichessUri( - '/study/$id/$chapterId.gif', - { - 'theme': boardTheme.gifApiName, - 'piece': pieceTheme.name, - }, - ), - ) - .timeout(const Duration(seconds: 1)); - if (resp.statusCode != 200) { - throw Exception('Failed to get GIF'); - } - return XFile.fromData(resp.bodyBytes, mimeType: 'image/gif'); - } } From a4a1acf6da016e6016237f561142a8af8eff6a74 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 11 Dec 2024 12:46:43 +0100 Subject: [PATCH 885/979] Avoid flash of loading content --- .../view/broadcast/broadcast_players_tab.dart | 39 +++---------------- 1 file changed, 6 insertions(+), 33 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_players_tab.dart b/lib/src/view/broadcast/broadcast_players_tab.dart index 1105463580..9c2d1ba180 100644 --- a/lib/src/view/broadcast/broadcast_players_tab.dart +++ b/lib/src/view/broadcast/broadcast_players_tab.dart @@ -35,7 +35,11 @@ class BroadcastPlayersTab extends ConsumerWidget { child: Center(child: Text('Cannot load players data: $error')), ), ), - _ => PlayersList.loading(), + _ => const SliverFillRemaining( + child: Center( + child: CircularProgressIndicator.adaptive(), + ), + ), }; } } @@ -52,26 +56,9 @@ const _kHeaderTextStyle = TextStyle(fontWeight: FontWeight.bold, overflow: TextOverflow.ellipsis); class PlayersList extends ConsumerStatefulWidget { - const PlayersList(this.players) : _isLoading = false; - - PlayersList.loading() - : players = List.generate( - 10, - (_) => const BroadcastPlayerExtended( - name: '', - title: null, - rating: null, - federation: null, - fideId: null, - played: 0, - score: null, - ratingDiff: null, - ), - ).toIList(), - _isLoading = true; + const PlayersList(this.players); final IList players; - final bool _isLoading; @override ConsumerState createState() => _PlayersListState(); @@ -136,20 +123,6 @@ class _PlayersListState extends ConsumerState { return SliverList.builder( itemCount: players.length + 1, itemBuilder: (context, index) { - if (widget._isLoading) { - return ShimmerLoading( - isLoading: true, - child: Container( - height: 50, - decoration: BoxDecoration( - color: index.isEven - ? Theme.of(context).colorScheme.surfaceContainerLow - : Theme.of(context).colorScheme.surfaceContainerHigh, - ), - ), - ); - } - if (index == 0) { return Row( crossAxisAlignment: CrossAxisAlignment.center, From 10c247e45b93534e56e9ed584e94a362796d8837 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 11 Dec 2024 12:51:46 +0100 Subject: [PATCH 886/979] Upgrade dependencies --- ios/Podfile.lock | 24 ++++++++++++------------ pubspec.lock | 20 ++++++++++---------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 8d23aefe58..43c1fe8af7 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,11 +1,11 @@ PODS: - app_settings (5.1.1): - Flutter - - AppAuth (1.7.5): - - AppAuth/Core (= 1.7.5) - - AppAuth/ExternalUserAgent (= 1.7.5) - - AppAuth/Core (1.7.5) - - AppAuth/ExternalUserAgent (1.7.5): + - AppAuth (1.7.6): + - AppAuth/Core (= 1.7.6) + - AppAuth/ExternalUserAgent (= 1.7.6) + - AppAuth/Core (1.7.6) + - AppAuth/ExternalUserAgent (1.7.6): - AppAuth/Core - connectivity_plus (0.0.1): - Flutter @@ -40,7 +40,7 @@ PODS: - GoogleUtilities/Logger (~> 8.0) - FirebaseCoreExtension (11.4.1): - FirebaseCore (~> 11.0) - - FirebaseCoreInternal (11.5.0): + - FirebaseCoreInternal (11.6.0): - "GoogleUtilities/NSData+zlib (~> 8.0)" - FirebaseCrashlytics (11.4.0): - FirebaseCore (~> 11.4) @@ -65,7 +65,7 @@ PODS: - GoogleUtilities/Reachability (~> 8.0) - GoogleUtilities/UserDefaults (~> 8.0) - nanopb (~> 3.30910.0) - - FirebaseRemoteConfigInterop (11.5.0) + - FirebaseRemoteConfigInterop (11.6.0) - FirebaseSessions (11.4.0): - FirebaseCore (~> 11.4) - FirebaseCoreExtension (~> 11.4) @@ -77,7 +77,7 @@ PODS: - PromisesSwift (~> 2.1) - Flutter (1.0.0) - flutter_appauth (0.0.1): - - AppAuth (= 1.7.5) + - AppAuth (= 1.7.6) - Flutter - flutter_local_notifications (0.0.1): - Flutter @@ -234,7 +234,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: app_settings: 3507c575c2b18a462c99948f61d5de21d4420999 - AppAuth: 501c04eda8a8d11f179dbe8637b7a91bb7e5d2fa + AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73 connectivity_plus: b21496ab28d1324eb59885d888a4d83b98531f01 cupertino_http: 94ac07f5ff090b8effa6c5e2c47871d48ab7c86c device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe @@ -244,14 +244,14 @@ SPEC CHECKSUMS: firebase_messaging: a538130cb2bca3ea0ff0892b8c948bd7d20ecaed FirebaseCore: e0510f1523bc0eb21653cac00792e1e2bd6f1771 FirebaseCoreExtension: f1bc67a4702931a7caa097d8e4ac0a1b0d16720e - FirebaseCoreInternal: f47dd28ae7782e6a4738aad3106071a8fe0af604 + FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2 FirebaseCrashlytics: 41bbdd2b514a8523cede0c217aee6ef7ecf38401 FirebaseInstallations: 6ef4a1c7eb2a61ee1f74727d7f6ce2e72acf1414 FirebaseMessaging: f8a160d99c2c2e5babbbcc90c4a3e15db036aee2 - FirebaseRemoteConfigInterop: 7a7aebb9342d53913a5c890efa88e289d9e5c1bc + FirebaseRemoteConfigInterop: e75e348953352a000331eb77caf01e424248e176 FirebaseSessions: 3f56f177d9e53a85021d16b31f9a111849d1dd8b Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - flutter_appauth: a84b3dade5e072175e13b68af5b827ca76efb1f8 + flutter_appauth: 240be31017bab05dc0ad10a863886d49eba477aa flutter_local_notifications: 395056b3175ba4f08480a7c5de30cd36d69827e4 flutter_native_splash: 576fbd69b830a63594ae678396fa17e43abbc5f8 flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13 diff --git a/pubspec.lock b/pubspec.lock index 0f2776c019..1844c28a42 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -535,10 +535,10 @@ packages: dependency: "direct main" description: name: flutter_appauth - sha256: d3fa52069464affa3e382a8d8ce7d003601643c39a9de727b98e7a84fab7fd7e + sha256: "354a9df0254ccb64b5c4031a8b2fe376b1214fb3f957f768248f4437c289308f" url: "https://pub.dev" source: hosted - version: "8.0.1" + version: "8.0.2" flutter_appauth_platform_interface: dependency: transitive description: @@ -823,10 +823,10 @@ packages: dependency: transitive description: name: io - sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.5" jni: dependency: transitive description: @@ -1007,10 +1007,10 @@ packages: dependency: transitive description: name: package_config - sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" package_info_plus: dependency: "direct main" description: @@ -1055,10 +1055,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "8c4967f8b7cb46dc914e178daa29813d83ae502e0529d7b0478330616a691ef7" + sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" url: "https://pub.dev" source: hosted - version: "2.2.14" + version: "2.2.15" path_provider_foundation: dependency: transitive description: @@ -1135,10 +1135,10 @@ packages: dependency: "direct main" description: name: pub_semver - sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.5" pubspec_parse: dependency: transitive description: From 0480d6a582bbcd8bc18c7ab54868fbd067076ad1 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 11 Dec 2024 12:52:15 +0100 Subject: [PATCH 887/979] Bump version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 5eef9bfcba..41986d6ae3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: lichess_mobile description: Lichess mobile app V2 publish_to: "none" -version: 0.13.6+001306 # See README.md for details about versioning +version: 0.13.7+001307 # See README.md for details about versioning environment: sdk: ">=3.5.0 <4.0.0" From c2c931fd51ed077c91d1d805c2383dcd43ca8d4e Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 11 Dec 2024 12:55:47 +0100 Subject: [PATCH 888/979] Fix lint --- lib/src/view/broadcast/broadcast_players_tab.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/src/view/broadcast/broadcast_players_tab.dart b/lib/src/view/broadcast/broadcast_players_tab.dart index 9c2d1ba180..9088af4e2d 100644 --- a/lib/src/view/broadcast/broadcast_players_tab.dart +++ b/lib/src/view/broadcast/broadcast_players_tab.dart @@ -10,7 +10,6 @@ import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_player_widget.dart'; import 'package:lichess_mobile/src/widgets/progression_widget.dart'; -import 'package:lichess_mobile/src/widgets/shimmer.dart'; /// A tab that displays the players participating in a broadcast tournament. class BroadcastPlayersTab extends ConsumerWidget { From 561c83514939f5153d6bfced012b68a812bd7c03 Mon Sep 17 00:00:00 2001 From: Noah <78898963+HaonRekcef@users.noreply.github.com> Date: Wed, 11 Dec 2024 13:04:26 +0100 Subject: [PATCH 889/979] add share to broadcast game screen --- .../view/broadcast/broadcast_boards_tab.dart | 26 ++++-- .../broadcast/broadcast_game_bottom_bar.dart | 87 +++++++++++++++++++ .../view/broadcast/broadcast_game_screen.dart | 38 ++++++-- .../broadcast/broadcast_round_screen.dart | 2 + 4 files changed, 138 insertions(+), 15 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_boards_tab.dart b/lib/src/view/broadcast/broadcast_boards_tab.dart index f04cbe4d07..49ee131501 100644 --- a/lib/src/view/broadcast/broadcast_boards_tab.dart +++ b/lib/src/view/broadcast/broadcast_boards_tab.dart @@ -26,9 +26,11 @@ const _kPlayerWidgetPadding = EdgeInsets.symmetric(vertical: 5.0); class BroadcastBoardsTab extends ConsumerWidget { const BroadcastBoardsTab({ required this.roundId, + required this.broadcastTitle, }); final BroadcastRoundId roundId; + final String broadcastTitle; @override Widget build(BuildContext context, WidgetRef ref) { @@ -58,14 +60,18 @@ class BroadcastBoardsTab extends ConsumerWidget { : BroadcastPreview( games: value.games.values.toIList(), roundId: roundId, - title: value.round.name, + broadcastTitle: broadcastTitle, + roundTitle: value.round.name, ), AsyncError(:final error) => SliverFillRemaining( child: Center( child: Text('Could not load broadcast: $error'), ), ), - _ => BroadcastPreview.loading(roundId: roundId), + _ => BroadcastPreview.loading( + roundId: roundId, + broadcastTitle: broadcastTitle, + ), }, ); } @@ -75,17 +81,20 @@ class BroadcastPreview extends StatelessWidget { const BroadcastPreview({ required this.roundId, required this.games, - required this.title, + required this.broadcastTitle, + required this.roundTitle, }); const BroadcastPreview.loading({ required this.roundId, + required this.broadcastTitle, }) : games = null, - title = null; + roundTitle = null; final BroadcastRoundId roundId; final IList? games; - final String? title; + final String broadcastTitle; + final String? roundTitle; @override Widget build(BuildContext context) { @@ -113,7 +122,7 @@ class BroadcastPreview extends StatelessWidget { delegate: SliverChildBuilderDelegate( childCount: games == null ? numberLoadingBoards : games!.length, (context, index) { - if (games == null || title == null) { + if (games == null || roundTitle == null) { return ShimmerLoading( isLoading: true, child: BoardThumbnail.loading( @@ -132,11 +141,12 @@ class BroadcastPreview extends StatelessWidget { onTap: () { pushPlatformRoute( context, - title: title, + title: roundTitle, builder: (context) => BroadcastGameScreen( roundId: roundId, gameId: game.id, - title: title!, + broadcastTitle: broadcastTitle, + roundTitle: roundTitle!, ), ); }, diff --git a/lib/src/view/broadcast/broadcast_game_bottom_bar.dart b/lib/src/view/broadcast/broadcast_game_bottom_bar.dart index 7582fa83b5..a2c7a5b804 100644 --- a/lib/src/view/broadcast/broadcast_game_bottom_bar.dart +++ b/lib/src/view/broadcast/broadcast_game_bottom_bar.dart @@ -1,20 +1,31 @@ import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_game_controller.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast_repository.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/model/game/game_share_service.dart'; +import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/share.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar_button.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/feedback.dart'; class BroadcastGameBottomBar extends ConsumerWidget { const BroadcastGameBottomBar({ required this.roundId, required this.gameId, + required this.broadcastTitle, + required this.roundTitle, }); final BroadcastRoundId roundId; final BroadcastGameId gameId; + final String broadcastTitle; + final String roundTitle; @override Widget build(BuildContext context, WidgetRef ref) { @@ -23,6 +34,82 @@ class BroadcastGameBottomBar extends ConsumerWidget { return BottomBar( children: [ + BottomBarButton( + label: context.l10n.menu, + onTap: () { + showAdaptiveActionSheet( + context: context, + actions: [ + BottomSheetAction( + makeLabel: (context) => Text(context.l10n.mobileShareGameURL), + onPressed: (context) async { + launchShareDialog( + context, + uri: lichessUri( + '/broadcast/${broadcastTitle.toLowerCase().replaceAll(' ', '-')}/${roundTitle.toLowerCase().replaceAll(' ', '-')}/$roundId/$gameId', + ), + ); + }, + ), + BottomSheetAction( + makeLabel: (context) => Text(context.l10n.mobileShareGamePGN), + onPressed: (context) async { + try { + final pgn = await ref.withClient( + (client) => BroadcastRepository(client) + .getGamePgn(roundId, gameId), + ); + if (context.mounted) { + launchShareDialog( + context, + text: pgn, + ); + } + } catch (e) { + if (context.mounted) { + showPlatformSnackbar( + context, + 'Failed to get PGN', + type: SnackBarType.error, + ); + } + } + }, + ), + BottomSheetAction( + makeLabel: (context) => const Text('GIF'), + onPressed: (_) async { + try { + final gif = + await ref.read(gameShareServiceProvider).chapterGif( + roundId, + gameId, + ); + if (context.mounted) { + launchShareDialog( + context, + files: [gif], + ); + } + } catch (e) { + debugPrint(e.toString()); + if (context.mounted) { + showPlatformSnackbar( + context, + 'Failed to get GIF', + type: SnackBarType.error, + ); + } + } + }, + ), + ], + ); + }, + icon: Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoIcons.share + : Icons.share, + ), BottomBarButton( label: context.l10n.flipBoard, onTap: () { diff --git a/lib/src/view/broadcast/broadcast_game_screen.dart b/lib/src/view/broadcast/broadcast_game_screen.dart index 75e656fc22..55a01f0352 100644 --- a/lib/src/view/broadcast/broadcast_game_screen.dart +++ b/lib/src/view/broadcast/broadcast_game_screen.dart @@ -33,12 +33,14 @@ import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; class BroadcastGameScreen extends ConsumerStatefulWidget { final BroadcastRoundId roundId; final BroadcastGameId gameId; - final String title; + final String broadcastTitle; + final String roundTitle; const BroadcastGameScreen({ required this.roundId, required this.gameId, - required this.title, + required this.broadcastTitle, + required this.roundTitle, }); @override @@ -80,7 +82,11 @@ class _BroadcastGameScreenState extends ConsumerState return PlatformScaffold( appBar: PlatformAppBar( - title: Text(widget.title, overflow: TextOverflow.ellipsis, maxLines: 1), + title: Text( + widget.roundTitle, + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), actions: [ AppBarAnalysisTabIndicator( tabs: tabs, @@ -104,8 +110,13 @@ class _BroadcastGameScreenState extends ConsumerState ], ), body: switch (broadcastGameState) { - AsyncData() => - _Body(widget.roundId, widget.gameId, tabController: _tabController), + AsyncData() => _Body( + widget.roundId, + widget.gameId, + widget.broadcastTitle, + widget.roundTitle, + tabController: _tabController, + ), AsyncError(:final error) => Center( child: Text('Cannot load broadcast game: $error'), ), @@ -116,10 +127,18 @@ class _BroadcastGameScreenState extends ConsumerState } class _Body extends ConsumerWidget { - const _Body(this.roundId, this.gameId, {required this.tabController}); + const _Body( + this.roundId, + this.gameId, + this.broadcastTitle, + this.roundTitle, { + required this.tabController, + }); final BroadcastRoundId roundId; final BroadcastGameId gameId; + final String broadcastTitle; + final String roundTitle; final TabController tabController; @override @@ -183,7 +202,12 @@ class _Body extends ConsumerWidget { .onUserMove, ) : null, - bottomBar: BroadcastGameBottomBar(roundId: roundId, gameId: gameId), + bottomBar: BroadcastGameBottomBar( + roundId: roundId, + gameId: gameId, + broadcastTitle: broadcastTitle, + roundTitle: roundTitle, + ), children: [ _OpeningExplorerTab(roundId, gameId), BroadcastGameTreeView(roundId, gameId), diff --git a/lib/src/view/broadcast/broadcast_round_screen.dart b/lib/src/view/broadcast/broadcast_round_screen.dart index 476f16f566..0677389f7d 100644 --- a/lib/src/view/broadcast/broadcast_round_screen.dart +++ b/lib/src/view/broadcast/broadcast_round_screen.dart @@ -147,6 +147,7 @@ class _BroadcastRoundScreenState extends ConsumerState sliver: BroadcastBoardsTab( roundId: _selectedRoundId ?? tournament.defaultRoundId, + broadcastTitle: widget.broadcast.title, ), ), _CupertinoView.players => _TabView( @@ -196,6 +197,7 @@ class _BroadcastRoundScreenState extends ConsumerState _TabView( sliver: BroadcastBoardsTab( roundId: _selectedRoundId ?? tournament.defaultRoundId, + broadcastTitle: widget.broadcast.title, ), ), _TabView( From 5b25bd003d9aabaae7b4c44a607396efa237041d Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 11 Dec 2024 13:32:30 +0100 Subject: [PATCH 890/979] Restore opening name in explorer Closes #1247 --- lib/src/view/analysis/analysis_screen.dart | 10 ++++ lib/src/view/analysis/tree_view.dart | 52 ------------------- .../opening_explorer_screen.dart | 8 +++ .../opening_explorer_view.dart | 28 ++++------ .../opening_explorer_widgets.dart | 48 +++++++++++++++++ 5 files changed, 76 insertions(+), 70 deletions(-) diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index ebea67cd71..edd2ce3d01 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; +import 'package:lichess_mobile/src/model/analysis/opening_service.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/game/game_share_service.dart'; import 'package:lichess_mobile/src/network/http.dart'; @@ -224,6 +225,15 @@ class _Body extends ConsumerWidget { children: [ OpeningExplorerView( position: currentNode.position, + opening: kOpeningAllowedVariants.contains(analysisState.variant) + ? analysisState.currentNode.isRoot + ? LightOpening( + eco: '', + name: context.l10n.startPosition, + ) + : analysisState.currentNode.opening ?? + analysisState.currentBranchOpening + : null, onMoveSelected: (move) { ref.read(ctrlProvider.notifier).onUserMove(move); }, diff --git a/lib/src/view/analysis/tree_view.dart b/lib/src/view/analysis/tree_view.dart index 477231ca9c..7087ecba0d 100644 --- a/lib/src/view/analysis/tree_view.dart +++ b/lib/src/view/analysis/tree_view.dart @@ -2,9 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; -import 'package:lichess_mobile/src/model/analysis/opening_service.dart'; -import 'package:lichess_mobile/src/model/common/chess.dart'; -import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/widgets/pgn.dart'; const kOpeningHeaderHeight = 32.0; @@ -18,9 +15,6 @@ class AnalysisTreeView extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final ctrlProvider = analysisControllerProvider(options); - final variant = ref.watch( - ctrlProvider.select((value) => value.requireValue.variant), - ); final root = ref.watch(ctrlProvider.select((value) => value.requireValue.root)); final currentPath = ref @@ -36,8 +30,6 @@ class AnalysisTreeView extends ConsumerWidget { return ListView( padding: EdgeInsets.zero, children: [ - if (kOpeningAllowedVariants.contains(variant)) - _OpeningHeader(ctrlProvider), DebouncedPgnTreeView( root: root, currentPath: currentPath, @@ -52,47 +44,3 @@ class AnalysisTreeView extends ConsumerWidget { ); } } - -class _OpeningHeader extends ConsumerWidget { - const _OpeningHeader(this.ctrlProvider); - - final AnalysisControllerProvider ctrlProvider; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isRootNode = ref.watch( - ctrlProvider.select((s) => s.requireValue.currentNode.isRoot), - ); - final nodeOpening = ref - .watch(ctrlProvider.select((s) => s.requireValue.currentNode.opening)); - final branchOpening = ref - .watch(ctrlProvider.select((s) => s.requireValue.currentBranchOpening)); - final contextOpening = - ref.watch(ctrlProvider.select((s) => s.requireValue.contextOpening)); - final opening = isRootNode - ? LightOpening( - eco: '', - name: context.l10n.startPosition, - ) - : nodeOpening ?? branchOpening ?? contextOpening; - - return opening != null - ? Container( - height: kOpeningHeaderHeight, - width: double.infinity, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.secondaryContainer, - ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Center( - child: Text( - opening.name, - overflow: TextOverflow.ellipsis, - ), - ), - ), - ) - : const SizedBox.shrink(); - } -} diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index a94f195493..2c9722ba5a 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_controller.dart'; +import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_preferences.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; @@ -181,6 +182,13 @@ class _Body extends ConsumerWidget { ), OpeningExplorerView( position: state.position, + opening: state.currentNode.isRoot + ? LightOpening( + eco: '', + name: context.l10n.startPosition, + ) + : state.currentNode.opening ?? + state.currentBranchOpening, onMoveSelected: (move) { ref .read( diff --git a/lib/src/view/opening_explorer/opening_explorer_view.dart b/lib/src/view/opening_explorer/opening_explorer_view.dart index dbfe2bc374..00c358ff46 100644 --- a/lib/src/view/opening_explorer/opening_explorer_view.dart +++ b/lib/src/view/opening_explorer/opening_explorer_view.dart @@ -1,6 +1,7 @@ import 'package:dartchess/dartchess.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_preferences.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_repository.dart'; @@ -9,13 +10,6 @@ import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/view/opening_explorer/opening_explorer_widgets.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; -const _kTableRowVerticalPadding = 12.0; -const _kTableRowHorizontalPadding = 8.0; -const _kTableRowPadding = EdgeInsets.symmetric( - horizontal: _kTableRowHorizontalPadding, - vertical: _kTableRowVerticalPadding, -); - /// Displays an opening explorer for the given position. /// /// It shows the top moves, games, and recent games for the given position in a list view. @@ -28,10 +22,12 @@ class OpeningExplorerView extends ConsumerStatefulWidget { const OpeningExplorerView({ required this.position, required this.onMoveSelected, + this.opening, this.scrollable = true, }); final Position position; + final Opening? opening; final void Function(NormalMove) onMoveSelected; final bool scrollable; @@ -49,23 +45,17 @@ class _OpeningExplorerState extends ConsumerState { @override Widget build(BuildContext context) { if (widget.position.ply >= 50) { - return Padding( - padding: _kTableRowPadding, - child: Center( - child: Text(context.l10n.maxDepthReached), - ), + return Center( + child: Text(context.l10n.maxDepthReached), ); } final prefs = ref.watch(openingExplorerPreferencesProvider); if (prefs.db == OpeningDatabase.player && prefs.playerDb.username == null) { - return const Padding( - padding: _kTableRowPadding, - child: Center( - // TODO: l10n - child: Text('Select a Lichess player in the settings.'), - ), + return const Center( + // TODO: l10n + child: Text('Select a Lichess player in the settings.'), ); } @@ -117,6 +107,8 @@ class _OpeningExplorerState extends ConsumerState { final ply = widget.position.ply; final children = [ + if (widget.opening != null) + OpeningNameHeader(opening: widget.opening!), OpeningExplorerMoveTable( moves: value.entry.moves, whiteWins: value.entry.white, diff --git a/lib/src/view/opening_explorer/opening_explorer_widgets.dart b/lib/src/view/opening_explorer/opening_explorer_widgets.dart index 39f7f4e8e3..cad14f93fb 100644 --- a/lib/src/view/opening_explorer/opening_explorer_widgets.dart +++ b/lib/src/view/opening_explorer/opening_explorer_widgets.dart @@ -3,11 +3,13 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; +import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/game/archived_game_screen.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:url_launcher/url_launcher.dart'; const _kTableRowVerticalPadding = 10.0; const _kTableRowHorizontalPadding = 8.0; @@ -27,6 +29,52 @@ Color _blackBoxColor(BuildContext context) => ? Colors.black.withValues(alpha: 0.7) : Colors.black; +class OpeningNameHeader extends StatelessWidget { + const OpeningNameHeader({required this.opening, super.key}); + + final Opening opening; + + @override + Widget build(BuildContext context) { + return Container( + padding: _kTableRowPadding, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.secondaryContainer, + ), + child: GestureDetector( + onTap: opening.name == context.l10n.startPosition + ? null + : () => launchUrl( + Uri.parse( + 'https://lichess.org/opening/${opening.name}', + ), + ), + child: Row( + children: [ + if (opening.name != context.l10n.startPosition) ...[ + Icon( + Icons.open_in_browser_outlined, + color: Theme.of(context).colorScheme.onSecondaryContainer, + ), + const SizedBox(width: 6.0), + ], + Expanded( + child: Text( + opening.name, + style: TextStyle( + color: Theme.of(context).colorScheme.onSecondaryContainer, + fontWeight: FontWeight.bold, + ), + maxLines: 1, + ), + ), + ], + ), + ), + ); + } +} + /// Table of moves for the opening explorer. class OpeningExplorerMoveTable extends ConsumerWidget { const OpeningExplorerMoveTable({ From a76d95f9a729e85f26663a521fbb6efac56fb9cd Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 11 Dec 2024 15:10:06 +0100 Subject: [PATCH 891/979] Only refresh a live broadcast paused more than 5m Closes #1256 --- .../model/broadcast/broadcast_game_controller.dart | 13 ++++++++++++- .../broadcast/broadcast_round_controller.dart | 14 +++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/lib/src/model/broadcast/broadcast_game_controller.dart b/lib/src/model/broadcast/broadcast_game_controller.dart index 2addb7111a..33c2adc907 100644 --- a/lib/src/model/broadcast/broadcast_game_controller.dart +++ b/lib/src/model/broadcast/broadcast_game_controller.dart @@ -42,6 +42,7 @@ class BroadcastGameController extends _$BroadcastGameController final _engineEvalDebounce = Debouncer(const Duration(milliseconds: 150)); + DateTime? _onPauseAt; Timer? _startEngineEvalTimer; @override @@ -58,8 +59,18 @@ class BroadcastGameController extends _$BroadcastGameController final evaluationService = ref.watch(evaluationServiceProvider); _appLifecycleListener = AppLifecycleListener( + onPause: () { + _onPauseAt = DateTime.now(); + }, onResume: () { - ref.invalidateSelf(); + if (state.valueOrNull?.isOngoing == true) { + if (_onPauseAt != null) { + final diff = DateTime.now().difference(_onPauseAt!); + if (diff >= const Duration(minutes: 5)) { + ref.invalidateSelf(); + } + } + } }, ); diff --git a/lib/src/model/broadcast/broadcast_round_controller.dart b/lib/src/model/broadcast/broadcast_round_controller.dart index 7f68bc5eb1..a2b8f5354e 100644 --- a/lib/src/model/broadcast/broadcast_round_controller.dart +++ b/lib/src/model/broadcast/broadcast_round_controller.dart @@ -24,6 +24,8 @@ class BroadcastRoundController extends _$BroadcastRoundController { StreamSubscription? _subscription; AppLifecycleListener? _appLifecycleListener; + DateTime? _onPauseAt; + late SocketClient _socketClient; @override @@ -37,8 +39,18 @@ class BroadcastRoundController extends _$BroadcastRoundController { _subscription = _socketClient.stream.listen(_handleSocketEvent); _appLifecycleListener = AppLifecycleListener( + onPause: () { + _onPauseAt = DateTime.now(); + }, onResume: () { - ref.invalidateSelf(); + if (state.valueOrNull?.round.status == RoundStatus.live) { + if (_onPauseAt != null) { + final diff = DateTime.now().difference(_onPauseAt!); + if (diff >= const Duration(minutes: 5)) { + ref.invalidateSelf(); + } + } + } }, ); From 59e1a91700383104c590501352f140b50d7a7c47 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 11 Dec 2024 16:21:16 +0100 Subject: [PATCH 892/979] Precache broadcast images & improve preview widget --- lib/src/utils/image.dart | 20 +++-- lib/src/utils/l10n.dart | 19 ++++ .../view/broadcast/broadcast_list_screen.dart | 90 ++++++++++++------- lib/src/view/watch/watch_tab_screen.dart | 85 ++++++++++++------ .../broadcasts_list_screen_test.dart | 8 +- 5 files changed, 152 insertions(+), 70 deletions(-) diff --git a/lib/src/utils/image.dart b/lib/src/utils/image.dart index 1067e67e3a..15c3447e1b 100644 --- a/lib/src/utils/image.dart +++ b/lib/src/utils/image.dart @@ -7,7 +7,7 @@ import 'package:image/image.dart' as img; import 'package:material_color_utilities/material_color_utilities.dart'; typedef ImageColors = ({ - Uint8List image, + Uint8List? image, int primaryContainer, int onPrimaryContainer, int error, @@ -27,7 +27,8 @@ class ImageColorWorker { bool get closed => _closed; - /// Returns a minimal color scheme associated with the image at the given [url]. + /// Returns a minimal color scheme associated with the image at the given [url], or + /// the given [image] if provided. /// /// The [fileExtension] parameter is optional and is used to specify the file /// extension of the image at the given [url] if it is known. It will speed up @@ -35,13 +36,14 @@ class ImageColorWorker { /// against all supported decoders. Future getImageColors( String url, { + Uint8List? image, String? fileExtension, }) async { if (_closed) throw StateError('Closed'); final completer = Completer.sync(); final id = _idCounter++; _activeRequests[id] = completer; - _commands.send((id, url, fileExtension)); + _commands.send((id, url, image, fileExtension)); return await completer.future; } @@ -97,16 +99,16 @@ class ImageColorWorker { receivePort.close(); return; } - final (int id, String url, String? extension) = - message as (int, String, String?); + final (int id, String url, Uint8List? image, String? extension) = + message as (int, String, Uint8List?, String?); try { - final bytes = await http.readBytes(Uri.parse(url)); + final bytes = image ?? await http.readBytes(Uri.parse(url)); // final stopwatch0 = Stopwatch()..start(); final decoder = extension != null ? img.findDecoderForNamedImage('.$extension') : img.findDecoderForData(bytes); - final image = decoder!.decode(bytes); - final resized = img.copyResize(image!, width: 112); + final decoded = decoder!.decode(bytes); + final resized = img.copyResize(decoded!, width: 112); final QuantizerResult quantizerResult = await QuantizerCelebi().quantize( resized.buffer.asUint32List(), @@ -132,7 +134,7 @@ class ImageColorWorker { contrastLevel: 0.0, ); final result = ( - image: bytes, + image: image == null ? bytes : null, primaryContainer: scheme.primaryContainer, onPrimaryContainer: scheme.onPrimaryContainer, error: scheme.error, diff --git a/lib/src/utils/l10n.dart b/lib/src/utils/l10n.dart index fcfe83aee0..bf7b6adc33 100644 --- a/lib/src/utils/l10n.dart +++ b/lib/src/utils/l10n.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; /// Returns a localized string with a single placeholder replaced by a widget. /// @@ -43,6 +44,24 @@ Text l10nWithWidget( ); } +final _dayFormatter = DateFormat.E().add_jm(); +final _monthFormatter = DateFormat.MMMd().add_Hm(); +final _dateFormatterWithYear = DateFormat.yMMMd().add_Hm(); + +String relativeDate(DateTime date) { + final diff = date.difference(DateTime.now()); + + return (!diff.isNegative && diff.inDays == 0) + ? diff.inHours == 0 + ? 'in ${diff.inMinutes} minute${diff.inMinutes > 1 ? 's' : ''}' // TODO translate with https://github.com/lichess-org/lila/blob/65b28ea8e43e0133df6c7ed40e03c2954f247d1e/translation/source/timeago.xml#L8 + : 'in ${diff.inHours} hour${diff.inHours > 1 ? 's' : ''}' // TODO translate with https://github.com/lichess-org/lila/blob/65b28ea8e43e0133df6c7ed40e03c2954f247d1e/translation/source/timeago.xml#L12 + : diff.inDays <= 7 + ? _dayFormatter.format(date) + : diff.inDays < 365 + ? _monthFormatter.format(date) + : _dateFormatterWithYear.format(date); +} + /// Returns a localized locale name. /// /// Names taken from https://github.com/lichess-org/lila/blob/master/modules/i18n/src/main/LangList.scala. diff --git a/lib/src/view/broadcast/broadcast_list_screen.dart b/lib/src/view/broadcast/broadcast_list_screen.dart index 6ed0dab15d..dd589f6dc4 100644 --- a/lib/src/view/broadcast/broadcast_list_screen.dart +++ b/lib/src/view/broadcast/broadcast_list_screen.dart @@ -1,17 +1,18 @@ import 'dart:async'; +import 'dart:typed_data'; import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_providers.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/styles/lichess_colors.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/image.dart'; +import 'package:lichess_mobile/src/utils/l10n.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; @@ -19,9 +20,6 @@ import 'package:lichess_mobile/src/view/broadcast/broadcast_round_screen.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; -final _dateFormatter = DateFormat.MMMd().add_Hm(); -final _dateFormatterWithYear = DateFormat.yMMMd().add_Hm(); - const kDefaultBroadcastImage = AssetImage('assets/images/broadcast_image.png'); const kBroadcastGridItemBorderRadius = BorderRadius.all(Radius.circular(16.0)); const kBroadcastGridItemContentPadding = EdgeInsets.symmetric(horizontal: 16.0); @@ -256,6 +254,28 @@ typedef _CardColors = ({ }); final Map _colorsCache = {}; +Future<(_CardColors, Uint8List?)?> _computeImageColors( + ImageColorWorker worker, + String imageUrl, [ + Uint8List? image, +]) async { + final response = await worker.getImageColors( + imageUrl, + fileExtension: 'webp', + ); + if (response != null) { + final (:image, :primaryContainer, :onPrimaryContainer, :error) = response; + final cardColors = ( + primaryContainer: Color(primaryContainer), + onPrimaryContainer: Color(onPrimaryContainer), + error: Color(error), + ); + _colorsCache[NetworkImage(imageUrl)] = cardColors; + return (cardColors, image); + } + return null; +} + class _BroadcastGridItemState extends State { _CardColors? _cardColors; ImageProvider? _imageProvider; @@ -263,16 +283,16 @@ class _BroadcastGridItemState extends State { String? get imageUrl => widget.broadcast.tour.imageUrl; - ImageProvider get image => + ImageProvider get imageProvider => imageUrl != null ? NetworkImage(imageUrl!) : kDefaultBroadcastImage; @override void didChangeDependencies() { super.didChangeDependencies(); - final cachedColors = _colorsCache[image]; + final cachedColors = _colorsCache[imageProvider]; if (cachedColors != null) { _cardColors = cachedColors; - _imageProvider = image; + _imageProvider = imageProvider; } else { if (imageUrl != null) { _fetchImageAndColors(NetworkImage(imageUrl!)); @@ -290,22 +310,12 @@ class _BroadcastGridItemState extends State { scheduleMicrotask(() => _fetchImageAndColors(provider)); }); } else if (widget.worker.closed == false) { - final response = await widget.worker.getImageColors( - provider.url, - fileExtension: 'webp', - ); + final response = await _computeImageColors(widget.worker, provider.url); if (response != null) { - final (:image, :primaryContainer, :onPrimaryContainer, :error) = - response; - final cardColors = ( - primaryContainer: Color(primaryContainer), - onPrimaryContainer: Color(onPrimaryContainer), - error: Color(error), - ); - _colorsCache[provider] = cardColors; + final (cardColors, image) = response; if (mounted) { setState(() { - _imageProvider = MemoryImage(image); + _imageProvider = image != null ? MemoryImage(image) : imageProvider; _cardColors = cardColors; }); } @@ -436,7 +446,7 @@ class _BroadcastGridItemState extends State { ) else Text( - _formatDate(widget.broadcast.round.startsAt!), + relativeDate(widget.broadcast.round.startsAt!), style: TextStyle( fontSize: 12, color: subTitleColor, @@ -487,14 +497,32 @@ class _BroadcastGridItemState extends State { } } -String _formatDate(DateTime date) { - final diff = date.difference(DateTime.now()); - - return (!diff.isNegative && diff.inDays == 0) - ? diff.inHours == 0 - ? 'in ${diff.inMinutes} minutes' // TODO translate with https://github.com/lichess-org/lila/blob/65b28ea8e43e0133df6c7ed40e03c2954f247d1e/translation/source/timeago.xml#L8 - : 'in ${diff.inHours} hours' // TODO translate with https://github.com/lichess-org/lila/blob/65b28ea8e43e0133df6c7ed40e03c2954f247d1e/translation/source/timeago.xml#L12 - : diff.inDays < 365 - ? _dateFormatter.format(date) - : _dateFormatterWithYear.format(date); +Future preCacheBroadcastImages( + BuildContext context, { + required Iterable broadcasts, + required ImageColorWorker worker, +}) async { + for (final broadcast in broadcasts) { + final imageUrl = broadcast.tour.imageUrl; + if (imageUrl != null) { + final provider = NetworkImage(imageUrl); + await precacheImage(provider, context); + final imageStream = provider.resolve(ImageConfiguration.empty); + final Completer completer = Completer(); + final ImageStreamListener listener = ImageStreamListener( + (imageInfo, synchronousCall) async { + final bytes = await imageInfo.image.toByteData(); + if (!completer.isCompleted) { + completer.complete(bytes?.buffer.asUint8List()); + } + }, + ); + imageStream.addListener(listener); + final imageBytes = await completer.future; + imageStream.removeListener(listener); + if (imageBytes != null) { + await _computeImageColors(worker, imageUrl, imageBytes); + } + } + } } diff --git a/lib/src/view/watch/watch_tab_screen.dart b/lib/src/view/watch/watch_tab_screen.dart index f7b93fe9c0..acf4804e53 100644 --- a/lib/src/view/watch/watch_tab_screen.dart +++ b/lib/src/view/watch/watch_tab_screen.dart @@ -4,7 +4,6 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_providers.dart'; import 'package:lichess_mobile/src/model/tv/featured_player.dart'; @@ -17,6 +16,8 @@ import 'package:lichess_mobile/src/navigation.dart'; import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/utils/image.dart'; +import 'package:lichess_mobile/src/utils/l10n.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_list_screen.dart'; @@ -143,18 +144,53 @@ class _WatchScreenState extends ConsumerState { Future refreshData() => _refreshData(ref); } -class _Body extends ConsumerWidget { +class _Body extends ConsumerStatefulWidget { const _Body(this.orientation); final Orientation orientation; @override - Widget build(BuildContext context, WidgetRef ref) { + ConsumerState<_Body> createState() => _BodyState(); +} + +class _BodyState extends ConsumerState<_Body> { + ImageColorWorker? _worker; + bool _imageAreCached = false; + + @override + void initState() { + super.initState(); + _precacheImages(); + } + + @override + void dispose() { + _worker?.close(); + super.dispose(); + } + + Future _precacheImages() async { + _worker = await ref.read(broadcastImageWorkerFactoryProvider).spawn(); + ref.listenManual(broadcastsPaginatorProvider, (_, current) async { + if (current.hasValue && !_imageAreCached) { + _imageAreCached = true; + await preCacheBroadcastImages( + context, + broadcasts: current.value!.active.take(10), + worker: _worker!, + ); + } + _worker?.close(); + }); + } + + @override + Widget build(BuildContext context) { final broadcastList = ref.watch(broadcastsPaginatorProvider); final featuredChannels = ref.watch(featuredChannelsProvider); final streamers = ref.watch(liveStreamersProvider); - final content = orientation == Orientation.portrait + final content = widget.orientation == Orientation.portrait ? [ _BroadcastWidget(broadcastList), _WatchTvWidget(featuredChannels), @@ -250,8 +286,6 @@ class _BroadcastTile extends ConsumerWidget { final Broadcast broadcast; - static final _dateFormat = DateFormat.E().add_jm(); - @override Widget build(BuildContext context, WidgetRef ref) { return PlatformListTile( @@ -264,39 +298,32 @@ class _BroadcastTile extends ConsumerWidget { ); }, leading: const Icon(LichessIcons.radio_tower_lichess), - title: Padding( - padding: const EdgeInsets.only(right: 5.0), - child: Row( - children: [ - Flexible( - child: Text( - broadcast.title, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ), - trailing: Column( - mainAxisAlignment: MainAxisAlignment.center, + subtitle: Row( children: [ - if (broadcast.round.startsAt != null) - Text( - _dateFormat.format(broadcast.round.startsAt!), - style: const TextStyle(fontSize: 10.0), - ), - if (broadcast.isLive) + Text(broadcast.round.name), + if (broadcast.isLive) ...[ + const SizedBox(width: 5.0), Text( 'LIVE', style: TextStyle( - fontSize: 12.0, color: context.lichessColors.error, fontWeight: FontWeight.bold, ), ), + ] else if (broadcast.round.startsAt != null) ...[ + const SizedBox(width: 5.0), + Text(relativeDate(broadcast.round.startsAt!)), + ], ], ), + title: Padding( + padding: const EdgeInsets.only(right: 5.0), + child: Text( + broadcast.title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), ); } } diff --git a/test/view/broadcast/broadcasts_list_screen_test.dart b/test/view/broadcast/broadcasts_list_screen_test.dart index 59b9c2e87a..1c3e093c6f 100644 --- a/test/view/broadcast/broadcasts_list_screen_test.dart +++ b/test/view/broadcast/broadcasts_list_screen_test.dart @@ -1,3 +1,5 @@ +import 'dart:typed_data'; + import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/testing.dart'; @@ -18,7 +20,11 @@ class FakeImageColorWorker implements ImageColorWorker { bool get closed => false; @override - Future getImageColors(String url, {String? fileExtension}) { + Future getImageColors( + String url, { + Uint8List? image, + String? fileExtension, + }) { return Future.value(null); } } From 7fece0211b2678ec226e93d3aa620276c932d4f9 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 11 Dec 2024 16:58:00 +0100 Subject: [PATCH 893/979] Fix screen flickering while changing rounds Fixes #1255 --- .../view/broadcast/broadcast_boards_tab.dart | 7 +- .../broadcast/broadcast_round_screen.dart | 268 ++++++++++-------- 2 files changed, 150 insertions(+), 125 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_boards_tab.dart b/lib/src/view/broadcast/broadcast_boards_tab.dart index 49ee131501..269f03d455 100644 --- a/lib/src/view/broadcast/broadcast_boards_tab.dart +++ b/lib/src/view/broadcast/broadcast_boards_tab.dart @@ -68,9 +68,10 @@ class BroadcastBoardsTab extends ConsumerWidget { child: Text('Could not load broadcast: $error'), ), ), - _ => BroadcastPreview.loading( - roundId: roundId, - broadcastTitle: broadcastTitle, + _ => const SliverFillRemaining( + child: Center( + child: CircularProgressIndicator.adaptive(), + ), ), }, ); diff --git a/lib/src/view/broadcast/broadcast_round_screen.dart b/lib/src/view/broadcast/broadcast_round_screen.dart index 0677389f7d..aa7ccd245e 100644 --- a/lib/src/view/broadcast/broadcast_round_screen.dart +++ b/lib/src/view/broadcast/broadcast_round_screen.dart @@ -17,6 +17,7 @@ import 'package:lichess_mobile/src/widgets/adaptive_bottom_sheet.dart'; import 'package:lichess_mobile/src/widgets/bottom_bar.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; +import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; class BroadcastRoundScreen extends ConsumerStatefulWidget { @@ -68,20 +69,152 @@ class _BroadcastRoundScreenState extends ConsumerState void setRoundId(BroadcastRoundId roundId) { setState(() { + roundLoaded = false; _selectedRoundId = roundId; }); } + Widget _iosBuilder( + BuildContext context, + AsyncValue asyncTournament, + ) { + final tabSwitcher = CupertinoSlidingSegmentedControl<_CupertinoView>( + groupValue: selectedTab, + children: { + _CupertinoView.overview: Text(context.l10n.broadcastOverview), + _CupertinoView.boards: Text(context.l10n.broadcastBoards), + _CupertinoView.players: Text(context.l10n.players), + }, + onValueChanged: (_CupertinoView? view) { + if (view != null) { + setCupertinoTab(view); + } + }, + ); + return CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + middle: AutoSizeText( + widget.broadcast.title, + minFontSize: 14.0, + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + child: Column( + children: [ + Expanded( + child: switch (selectedTab) { + _CupertinoView.overview => _TabView( + cupertinoTabSwitcher: tabSwitcher, + sliver: BroadcastOverviewTab( + broadcast: widget.broadcast, + tournamentId: _selectedTournamentId, + ), + ), + _CupertinoView.boards => _TabView( + cupertinoTabSwitcher: tabSwitcher, + sliver: switch (asyncTournament) { + AsyncData(:final value) => BroadcastBoardsTab( + roundId: _selectedRoundId ?? value.defaultRoundId, + broadcastTitle: widget.broadcast.title, + ), + _ => const SliverFillRemaining( + child: SizedBox.shrink(), + ), + }, + ), + _CupertinoView.players => _TabView( + cupertinoTabSwitcher: tabSwitcher, + sliver: BroadcastPlayersTab( + tournamentId: _selectedTournamentId, + ), + ), + }, + ), + switch (asyncTournament) { + AsyncData(:final value) => _BottomBar( + tournament: value, + roundId: _selectedRoundId ?? value.defaultRoundId, + setTournamentId: setTournamentId, + setRoundId: setRoundId, + ), + _ => const BottomBar.empty(), + }, + ], + ), + ); + } + + Widget _androidBuilder( + BuildContext context, + AsyncValue asyncTournament, + ) { + return Scaffold( + appBar: AppBar( + title: AutoSizeText( + widget.broadcast.title, + minFontSize: 14.0, + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + bottom: TabBar( + controller: _tabController, + tabs: [ + Tab(text: context.l10n.broadcastOverview), + Tab(text: context.l10n.broadcastBoards), + Tab(text: context.l10n.players), + ], + ), + ), + body: TabBarView( + controller: _tabController, + children: [ + _TabView( + sliver: BroadcastOverviewTab( + broadcast: widget.broadcast, + tournamentId: _selectedTournamentId, + ), + ), + _TabView( + sliver: switch (asyncTournament) { + AsyncData(:final value) => BroadcastBoardsTab( + roundId: _selectedRoundId ?? value.defaultRoundId, + broadcastTitle: widget.broadcast.title, + ), + _ => const SliverFillRemaining( + child: SizedBox.shrink(), + ), + }, + ), + _TabView( + sliver: BroadcastPlayersTab( + tournamentId: _selectedTournamentId, + ), + ), + ], + ), + bottomNavigationBar: switch (asyncTournament) { + AsyncData(:final value) => _BottomBar( + tournament: value, + roundId: _selectedRoundId ?? value.defaultRoundId, + setTournamentId: setTournamentId, + setRoundId: setRoundId, + ), + _ => const BottomBar.empty(), + }, + ); + } + @override Widget build(BuildContext context) { - final tournament = + final asyncTour = ref.watch(broadcastTournamentProvider(_selectedTournamentId)); - switch (tournament) { + switch (asyncTour) { case AsyncData(value: final tournament): // Eagerly initalize the round controller so it stays alive when switching tabs // and to know if the round has games to show - final round = ref.watch( + ref.watch( broadcastRoundControllerProvider( _selectedRoundId ?? tournament.defaultRoundId, ), @@ -92,7 +225,7 @@ class _BroadcastRoundScreenState extends ConsumerState _selectedRoundId ?? tournament.defaultRoundId, ), (_, round) { - if (!roundLoaded && round.hasValue) { + if (round.hasValue && !roundLoaded) { roundLoaded = true; if (round.value!.games.isNotEmpty) { _tabController.animateTo(1, duration: Duration.zero); @@ -105,125 +238,16 @@ class _BroadcastRoundScreenState extends ConsumerState }, ); - switch (round) { - case AsyncData(value: final _): - if (Theme.of(context).platform == TargetPlatform.iOS) { - final tabSwitcher = - CupertinoSlidingSegmentedControl<_CupertinoView>( - groupValue: selectedTab, - children: { - _CupertinoView.overview: Text(context.l10n.broadcastOverview), - _CupertinoView.boards: Text(context.l10n.broadcastBoards), - _CupertinoView.players: Text(context.l10n.players), - }, - onValueChanged: (_CupertinoView? view) { - if (view != null) { - setCupertinoTab(view); - } - }, - ); - return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - middle: AutoSizeText( - widget.broadcast.title, - minFontSize: 14.0, - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), - ), - child: Column( - children: [ - Expanded( - child: switch (selectedTab) { - _CupertinoView.overview => _TabView( - cupertinoTabSwitcher: tabSwitcher, - sliver: BroadcastOverviewTab( - broadcast: widget.broadcast, - tournamentId: _selectedTournamentId, - ), - ), - _CupertinoView.boards => _TabView( - cupertinoTabSwitcher: tabSwitcher, - sliver: BroadcastBoardsTab( - roundId: - _selectedRoundId ?? tournament.defaultRoundId, - broadcastTitle: widget.broadcast.title, - ), - ), - _CupertinoView.players => _TabView( - cupertinoTabSwitcher: tabSwitcher, - sliver: BroadcastPlayersTab( - tournamentId: _selectedTournamentId, - ), - ), - }, - ), - _BottomBar( - tournament: tournament, - roundId: _selectedRoundId ?? tournament.defaultRoundId, - setTournamentId: setTournamentId, - setRoundId: setRoundId, - ), - ], - ), - ); - } else { - return Scaffold( - appBar: AppBar( - title: AutoSizeText( - widget.broadcast.title, - minFontSize: 14.0, - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), - bottom: TabBar( - controller: _tabController, - tabs: [ - Tab(text: context.l10n.broadcastOverview), - Tab(text: context.l10n.broadcastBoards), - Tab(text: context.l10n.players), - ], - ), - ), - body: TabBarView( - controller: _tabController, - children: [ - _TabView( - sliver: BroadcastOverviewTab( - broadcast: widget.broadcast, - tournamentId: _selectedTournamentId, - ), - ), - _TabView( - sliver: BroadcastBoardsTab( - roundId: _selectedRoundId ?? tournament.defaultRoundId, - broadcastTitle: widget.broadcast.title, - ), - ), - _TabView( - sliver: BroadcastPlayersTab( - tournamentId: _selectedTournamentId, - ), - ), - ], - ), - bottomNavigationBar: _BottomBar( - tournament: tournament, - roundId: _selectedRoundId ?? tournament.defaultRoundId, - setTournamentId: setTournamentId, - setRoundId: setRoundId, - ), - ); - } - case AsyncError(:final error): - return Center(child: Text('Could not load broadcast: $error')); - case _: - return const Center(child: CircularProgressIndicator.adaptive()); - } - case AsyncError(:final error): - return Center(child: Text('Could not load broadcast: $error')); + return PlatformWidget( + androidBuilder: (context) => _androidBuilder(context, asyncTour), + iosBuilder: (context) => _iosBuilder(context, asyncTour), + ); + case _: - return const Center(child: CircularProgressIndicator.adaptive()); + return PlatformWidget( + androidBuilder: (context) => _androidBuilder(context, asyncTour), + iosBuilder: (context) => _iosBuilder(context, asyncTour), + ); } } } From 85c35226cbd84bc976ca4f45943bf4a4f08c014c Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 11 Dec 2024 17:17:58 +0100 Subject: [PATCH 894/979] Update clock if node already exists in broadcast Fixes #1257 --- .../model/broadcast/broadcast_game_controller.dart | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/src/model/broadcast/broadcast_game_controller.dart b/lib/src/model/broadcast/broadcast_game_controller.dart index 33c2adc907..c5b13db99a 100644 --- a/lib/src/model/broadcast/broadcast_game_controller.dart +++ b/lib/src/model/broadcast/broadcast_game_controller.dart @@ -168,6 +168,17 @@ class BroadcastGameController extends _$BroadcastGameController final (newPath, isNewNode) = _root.addMoveAt(path, uciMove, clock: clock); + if (newPath != null && isNewNode == false) { + _root.updateAt(newPath, (node) { + if (node is Branch) { + node.comments = [ + ...node.comments ?? [], + PgnComment(clock: clock), + ]; + } + }); + } + if (newPath != null) { _root.promoteAt(newPath, toMainline: true); _setPath( From 8bdda2f2d5b8c98617fbc6e1b536d12697a16e36 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 11 Dec 2024 17:29:43 +0100 Subject: [PATCH 895/979] Improve loading of android boards --- .../broadcast/broadcast_round_screen.dart | 72 +++++++++++-------- 1 file changed, 42 insertions(+), 30 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_round_screen.dart b/lib/src/view/broadcast/broadcast_round_screen.dart index aa7ccd245e..814d1e0237 100644 --- a/lib/src/view/broadcast/broadcast_round_screen.dart +++ b/lib/src/view/broadcast/broadcast_round_screen.dart @@ -77,6 +77,7 @@ class _BroadcastRoundScreenState extends ConsumerState Widget _iosBuilder( BuildContext context, AsyncValue asyncTournament, + AsyncValue asyncRound, ) { final tabSwitcher = CupertinoSlidingSegmentedControl<_CupertinoView>( groupValue: selectedTab, @@ -148,6 +149,7 @@ class _BroadcastRoundScreenState extends ConsumerState Widget _androidBuilder( BuildContext context, AsyncValue asyncTournament, + AsyncValue asyncRound, ) { return Scaffold( appBar: AppBar( @@ -166,33 +168,38 @@ class _BroadcastRoundScreenState extends ConsumerState ], ), ), - body: TabBarView( - controller: _tabController, - children: [ - _TabView( - sliver: BroadcastOverviewTab( - broadcast: widget.broadcast, - tournamentId: _selectedTournamentId, - ), - ), - _TabView( - sliver: switch (asyncTournament) { - AsyncData(:final value) => BroadcastBoardsTab( - roundId: _selectedRoundId ?? value.defaultRoundId, - broadcastTitle: widget.broadcast.title, + body: switch (asyncRound) { + AsyncData(value: final _) => TabBarView( + controller: _tabController, + children: [ + _TabView( + sliver: BroadcastOverviewTab( + broadcast: widget.broadcast, + tournamentId: _selectedTournamentId, ), - _ => const SliverFillRemaining( - child: SizedBox.shrink(), + ), + _TabView( + sliver: switch (asyncTournament) { + AsyncData(:final value) => BroadcastBoardsTab( + roundId: _selectedRoundId ?? value.defaultRoundId, + broadcastTitle: widget.broadcast.title, + ), + _ => const SliverFillRemaining( + child: SizedBox.shrink(), + ), + }, + ), + _TabView( + sliver: BroadcastPlayersTab( + tournamentId: _selectedTournamentId, ), - }, - ), - _TabView( - sliver: BroadcastPlayersTab( - tournamentId: _selectedTournamentId, - ), + ), + ], ), - ], - ), + _ => const Center( + child: CircularProgressIndicator(), + ) + }, bottomNavigationBar: switch (asyncTournament) { AsyncData(:final value) => _BottomBar( tournament: value, @@ -210,11 +217,13 @@ class _BroadcastRoundScreenState extends ConsumerState final asyncTour = ref.watch(broadcastTournamentProvider(_selectedTournamentId)); + const loadingRound = AsyncValue.loading(); + switch (asyncTour) { case AsyncData(value: final tournament): // Eagerly initalize the round controller so it stays alive when switching tabs // and to know if the round has games to show - ref.watch( + final round = ref.watch( broadcastRoundControllerProvider( _selectedRoundId ?? tournament.defaultRoundId, ), @@ -228,7 +237,7 @@ class _BroadcastRoundScreenState extends ConsumerState if (round.hasValue && !roundLoaded) { roundLoaded = true; if (round.value!.games.isNotEmpty) { - _tabController.animateTo(1, duration: Duration.zero); + _tabController.index = 1; if (Theme.of(context).platform == TargetPlatform.iOS) { setCupertinoTab(_CupertinoView.boards); @@ -239,14 +248,17 @@ class _BroadcastRoundScreenState extends ConsumerState ); return PlatformWidget( - androidBuilder: (context) => _androidBuilder(context, asyncTour), - iosBuilder: (context) => _iosBuilder(context, asyncTour), + androidBuilder: (context) => + _androidBuilder(context, asyncTour, round), + iosBuilder: (context) => _iosBuilder(context, asyncTour, round), ); case _: return PlatformWidget( - androidBuilder: (context) => _androidBuilder(context, asyncTour), - iosBuilder: (context) => _iosBuilder(context, asyncTour), + androidBuilder: (context) => + _androidBuilder(context, asyncTour, loadingRound), + iosBuilder: (context) => + _iosBuilder(context, asyncTour, loadingRound), ); } } From 5fdc8660d7db2901391f789a0baae69a11af221e Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 11 Dec 2024 17:37:39 +0100 Subject: [PATCH 896/979] Tweak spacing --- lib/src/view/broadcast/broadcast_list_screen.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_list_screen.dart b/lib/src/view/broadcast/broadcast_list_screen.dart index dd589f6dc4..c6602c0775 100644 --- a/lib/src/view/broadcast/broadcast_list_screen.dart +++ b/lib/src/view/broadcast/broadcast_list_screen.dart @@ -130,8 +130,8 @@ class _BodyState extends ConsumerState<_Body> { final gridDelegate = SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: itemsByRow, - crossAxisSpacing: 10, - mainAxisSpacing: 10, + crossAxisSpacing: 16.0, + mainAxisSpacing: 16.0, childAspectRatio: 1.45, ); From c96f7dffd5819169e3f3aeb62c485cc09635e2f1 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 11 Dec 2024 17:40:24 +0100 Subject: [PATCH 897/979] Fix close worker logic --- lib/src/view/watch/watch_tab_screen.dart | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/src/view/watch/watch_tab_screen.dart b/lib/src/view/watch/watch_tab_screen.dart index acf4804e53..6dccdb2aa0 100644 --- a/lib/src/view/watch/watch_tab_screen.dart +++ b/lib/src/view/watch/watch_tab_screen.dart @@ -174,13 +174,16 @@ class _BodyState extends ConsumerState<_Body> { ref.listenManual(broadcastsPaginatorProvider, (_, current) async { if (current.hasValue && !_imageAreCached) { _imageAreCached = true; - await preCacheBroadcastImages( - context, - broadcasts: current.value!.active.take(10), - worker: _worker!, - ); + try { + await preCacheBroadcastImages( + context, + broadcasts: current.value!.active.take(10), + worker: _worker!, + ); + } finally { + _worker?.close(); + } } - _worker?.close(); }); } From 1e8253b90fda428f96efe43e1ddfe00f07b00c45 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Mon, 9 Dec 2024 10:55:14 +0100 Subject: [PATCH 898/979] Add player results screen --- lib/src/model/broadcast/broadcast.dart | 31 ++ .../model/broadcast/broadcast_federation.dart | 205 ++++++++++ .../model/broadcast/broadcast_providers.dart | 12 + .../model/broadcast/broadcast_repository.dart | 57 ++- lib/src/model/common/id.dart | 10 +- .../view/broadcast/broadcast_boards_tab.dart | 38 +- .../broadcast/broadcast_game_bottom_bar.dart | 33 +- .../view/broadcast/broadcast_game_screen.dart | 26 +- .../view/broadcast/broadcast_list_screen.dart | 1 + .../broadcast_player_results_screen.dart | 351 ++++++++++++++++++ .../broadcast/broadcast_player_widget.dart | 9 +- .../view/broadcast/broadcast_players_tab.dart | 106 +++--- .../broadcast/broadcast_round_screen.dart | 2 - 13 files changed, 778 insertions(+), 103 deletions(-) create mode 100644 lib/src/model/broadcast/broadcast_federation.dart create mode 100644 lib/src/view/broadcast/broadcast_player_results_screen.dart diff --git a/lib/src/model/broadcast/broadcast.dart b/lib/src/model/broadcast/broadcast.dart index fff18f2d78..a1ad6be868 100644 --- a/lib/src/model/broadcast/broadcast.dart +++ b/lib/src/model/broadcast/broadcast.dart @@ -82,6 +82,7 @@ class BroadcastRound with _$BroadcastRound { required DateTime? startsAt, required DateTime? finishedAt, required bool startsAfterPrevious, + required String? url, }) = _BroadcastRound; } @@ -136,9 +137,39 @@ class BroadcastPlayerExtended with _$BroadcastPlayerExtended { required int played, required double? score, required int? ratingDiff, + required int? performance, }) = _BroadcastPlayerExtended; } +typedef BroadcastFideData = ({ + ({ + int? standard, + int? rapid, + int? blitz, + }) ratings, + int? birthYear, +}); + +typedef BroadcastPlayerResults = ({ + BroadcastPlayerExtended player, + BroadcastFideData fideData, + IList games, +}); + +enum BroadcastPoints { one, half, zero } + +@freezed +class BroadcastPlayerResultData with _$BroadcastPlayerResultData { + const factory BroadcastPlayerResultData({ + required BroadcastRoundId roundId, + required BroadcastGameId gameId, + required Side color, + required BroadcastPoints? points, + required int? ratingDiff, + required BroadcastPlayer opponent, + }) = _BroadcastPlayerResult; +} + enum RoundStatus { live, finished, diff --git a/lib/src/model/broadcast/broadcast_federation.dart b/lib/src/model/broadcast/broadcast_federation.dart new file mode 100644 index 0000000000..1deb73bb2e --- /dev/null +++ b/lib/src/model/broadcast/broadcast_federation.dart @@ -0,0 +1,205 @@ +const fedIdToName = { + 'FID': 'FIDE', + 'USA': 'United States of America', + 'IND': 'India', + 'CHN': 'China', + 'RUS': 'Russia', + 'AZE': 'Azerbaijan', + 'FRA': 'France', + 'UKR': 'Ukraine', + 'ARM': 'Armenia', + 'GER': 'Germany', + 'ESP': 'Spain', + 'NED': 'Netherlands', + 'HUN': 'Hungary', + 'POL': 'Poland', + 'ENG': 'England', + 'ROU': 'Romania', + 'NOR': 'Norway', + 'UZB': 'Uzbekistan', + 'ISR': 'Israel', + 'CZE': 'Czech Republic', + 'SRB': 'Serbia', + 'CRO': 'Croatia', + 'GRE': 'Greece', + 'IRI': 'Iran', + 'TUR': 'Turkiye', + 'SLO': 'Slovenia', + 'ARG': 'Argentina', + 'SWE': 'Sweden', + 'GEO': 'Georgia', + 'ITA': 'Italy', + 'CUB': 'Cuba', + 'AUT': 'Austria', + 'PER': 'Peru', + 'BUL': 'Bulgaria', + 'BRA': 'Brazil', + 'DEN': 'Denmark', + 'SUI': 'Switzerland', + 'CAN': 'Canada', + 'SVK': 'Slovakia', + 'LTU': 'Lithuania', + 'VIE': 'Vietnam', + 'AUS': 'Australia', + 'BEL': 'Belgium', + 'MNE': 'Montenegro', + 'MDA': 'Moldova', + 'KAZ': 'Kazakhstan', + 'ISL': 'Iceland', + 'COL': 'Colombia', + 'BIH': 'Bosnia & Herzegovina', + 'EGY': 'Egypt', + 'FIN': 'Finland', + 'MGL': 'Mongolia', + 'PHI': 'Philippines', + 'BLR': 'Belarus', + 'LAT': 'Latvia', + 'POR': 'Portugal', + 'CHI': 'Chile', + 'MEX': 'Mexico', + 'MKD': 'North Macedonia', + 'INA': 'Indonesia', + 'PAR': 'Paraguay', + 'EST': 'Estonia', + 'SGP': 'Singapore', + 'SCO': 'Scotland', + 'VEN': 'Venezuela', + 'IRL': 'Ireland', + 'URU': 'Uruguay', + 'TKM': 'Turkmenistan', + 'MAR': 'Morocco', + 'MAS': 'Malaysia', + 'BAN': 'Bangladesh', + 'ALG': 'Algeria', + 'RSA': 'South Africa', + 'AND': 'Andorra', + 'ALB': 'Albania', + 'KGZ': 'Kyrgyzstan', + 'KOS': 'Kosovo *', + 'FAI': 'Faroe Islands', + 'ZAM': 'Zambia', + 'MYA': 'Myanmar', + 'NZL': 'New Zealand', + 'ECU': 'Ecuador', + 'CRC': 'Costa Rica', + 'NGR': 'Nigeria', + 'JPN': 'Japan', + 'SYR': 'Syria', + 'DOM': 'Dominican Republic', + 'LUX': 'Luxembourg', + 'WLS': 'Wales', + 'BOL': 'Bolivia', + 'TUN': 'Tunisia', + 'UAE': 'United Arab Emirates', + 'MNC': 'Monaco', + 'TJK': 'Tajikistan', + 'PAN': 'Panama', + 'LBN': 'Lebanon', + 'NCA': 'Nicaragua', + 'ESA': 'El Salvador', + 'ANG': 'Angola', + 'TTO': 'Trinidad & Tobago', + 'SRI': 'Sri Lanka', + 'IRQ': 'Iraq', + 'JOR': 'Jordan', + 'UGA': 'Uganda', + 'MAD': 'Madagascar', + 'ZIM': 'Zimbabwe', + 'MLT': 'Malta', + 'SUD': 'Sudan', + 'KOR': 'South Korea', + 'PUR': 'Puerto Rico', + 'HON': 'Honduras', + 'GUA': 'Guatemala', + 'PAK': 'Pakistan', + 'JAM': 'Jamaica', + 'THA': 'Thailand', + 'YEM': 'Yemen', + 'LBA': 'Libya', + 'CYP': 'Cyprus', + 'NEP': 'Nepal', + 'HKG': 'Hong Kong, China', + 'SSD': 'South Sudan', + 'BOT': 'Botswana', + 'PLE': 'Palestine', + 'KEN': 'Kenya', + 'AHO': 'Netherlands Antilles', + 'MAW': 'Malawi', + 'LIE': 'Liechtenstein', + 'TPE': 'Chinese Taipei', + 'AFG': 'Afghanistan', + 'MOZ': 'Mozambique', + 'KSA': 'Saudi Arabia', + 'BAR': 'Barbados', + 'NAM': 'Namibia', + 'HAI': 'Haiti', + 'ARU': 'Aruba', + 'CIV': 'Cote d’Ivoire', + 'CPV': 'Cape Verde', + 'SUR': 'Suriname', + 'LBR': 'Liberia', + 'IOM': 'Isle of Man', + 'MTN': 'Mauritania', + 'BRN': 'Bahrain', + 'GHA': 'Ghana', + 'OMA': 'Oman', + 'BRU': 'Brunei Darussalam', + 'GCI': 'Guernsey', + 'GUM': 'Guam', + 'KUW': 'Kuwait', + 'JCI': 'Jersey', + 'MRI': 'Mauritius', + 'SEN': 'Senegal', + 'BAH': 'Bahamas', + 'MDV': 'Maldives', + 'NRU': 'Nauru', + 'TOG': 'Togo', + 'FIJ': 'Fiji', + 'PLW': 'Palau', + 'GUY': 'Guyana', + 'LES': 'Lesotho', + 'CAY': 'Cayman Islands', + 'SOM': 'Somalia', + 'SWZ': 'Eswatini', + 'TAN': 'Tanzania', + 'LCA': 'Saint Lucia', + 'ISV': 'US Virgin Islands', + 'SLE': 'Sierra Leone', + 'BER': 'Bermuda', + 'SMR': 'San Marino', + 'BDI': 'Burundi', + 'QAT': 'Qatar', + 'ETH': 'Ethiopia', + 'DJI': 'Djibouti', + 'SEY': 'Seychelles', + 'PNG': 'Papua New Guinea', + 'DMA': 'Dominica', + 'STP': 'Sao Tome and Principe', + 'MAC': 'Macau', + 'CAM': 'Cambodia', + 'VIN': 'Saint Vincent and the Grenadines', + 'BUR': 'Burkina Faso', + 'COM': 'Comoros Islands', + 'GAB': 'Gabon', + 'RWA': 'Rwanda', + 'CMR': 'Cameroon', + 'MLI': 'Mali', + 'ANT': 'Antigua and Barbuda', + 'CHA': 'Chad', + 'GAM': 'Gambia', + 'COD': 'Democratic Republic of the Congo', + 'SKN': 'Saint Kitts and Nevis', + 'BHU': 'Bhutan', + 'NIG': 'Niger', + 'GRN': 'Grenada', + 'BIZ': 'Belize', + 'CAF': 'Central African Republic', + 'ERI': 'Eritrea', + 'GEQ': 'Equatorial Guinea', + 'IVB': 'British Virgin Islands', + 'LAO': 'Laos', + 'SOL': 'Solomon Islands', + 'TGA': 'Tonga', + 'TLS': 'Timor-Leste', + 'VAN': 'Vanuatu', +}; diff --git a/lib/src/model/broadcast/broadcast_providers.dart b/lib/src/model/broadcast/broadcast_providers.dart index a7e02edc8d..f06c422cc8 100644 --- a/lib/src/model/broadcast/broadcast_providers.dart +++ b/lib/src/model/broadcast/broadcast_providers.dart @@ -65,6 +65,18 @@ Future> broadcastPlayers( ); } +@riverpod +Future broadcastPlayerResult( + Ref ref, + BroadcastTournamentId broadcastTournamentId, + String playerId, +) { + return ref.withClient( + (client) => BroadcastRepository(client) + .getPlayerResults(broadcastTournamentId, playerId), + ); +} + @Riverpod(keepAlive: true) BroadcastImageWorkerFactory broadcastImageWorkerFactory(Ref ref) { return const BroadcastImageWorkerFactory(); diff --git a/lib/src/model/broadcast/broadcast_repository.dart b/lib/src/model/broadcast/broadcast_repository.dart index 67dec06f47..c5f943d719 100644 --- a/lib/src/model/broadcast/broadcast_repository.dart +++ b/lib/src/model/broadcast/broadcast_repository.dart @@ -57,6 +57,16 @@ class BroadcastRepository { mapper: _makePlayerFromJson, ); } + + Future getPlayerResults( + BroadcastTournamentId tournamentId, + String playerId, + ) { + return client.readJson( + Uri(path: 'broadcast/$tournamentId/players/$playerId'), + mapper: _makePlayerResultsFromJson, + ); + } } BroadcastList _makeBroadcastResponseFromJson( @@ -79,7 +89,7 @@ Broadcast _broadcastFromPick(RequiredPick pick) { round: _roundFromPick(pick('round').required()), group: pick('group').asStringOrNull(), roundToLinkId: - pick('roundToLink', 'id').asBroadcastRoundIddOrNull() ?? roundId, + pick('roundToLink', 'id').asBroadcastRoundIdOrNull() ?? roundId, ); } @@ -143,6 +153,7 @@ BroadcastRound _roundFromPick(RequiredPick pick) { startsAt: pick('startsAt').asDateTimeFromMillisecondsOrNull(), finishedAt: pick('finishedAt').asDateTimeFromMillisecondsOrNull(), startsAfterPrevious: pick('startsAfterPrevious').asBoolOrFalse(), + url: pick('url').asStringOrNull(), ); } @@ -219,5 +230,49 @@ BroadcastPlayerExtended _playerExtendedFromPick(RequiredPick pick) { played: pick('played').asIntOrThrow(), score: pick('score').asDoubleOrNull(), ratingDiff: pick('ratingDiff').asIntOrNull(), + performance: pick('performance').asIntOrNull(), + ); +} + +BroadcastPlayerResults _makePlayerResultsFromJson( + Map json, +) { + return ( + player: _playerExtendedFromPick(pick(json).required()), + fideData: _fideDataFromPick(pick(json, 'fide')), + games: + pick(json, 'games').asListOrThrow(_makePlayerResultFromPick).toIList() + ); +} + +BroadcastFideData _fideDataFromPick(Pick pick) { + return ( + ratings: ( + standard: pick('ratings', 'standard').asIntOrNull(), + rapid: pick('ratings', 'rapid').asIntOrNull(), + blitz: pick('ratings', 'blitz').asIntOrNull() + ), + birthYear: pick('year').asIntOrNull(), + ); +} + +BroadcastPlayerResultData _makePlayerResultFromPick(RequiredPick pick) { + final pointsString = pick('points').asStringOrNull(); + BroadcastPoints? points; + if (pointsString == '1') { + points = BroadcastPoints.one; + } else if (pointsString == '1/2') { + points = BroadcastPoints.half; + } else if (pointsString == '0') { + points = BroadcastPoints.zero; + } + + return BroadcastPlayerResultData( + roundId: pick('round').asBroadcastRoundIdOrThrow(), + gameId: pick('id').asBroadcastGameIdOrThrow(), + color: pick('color').asSideOrThrow(), + ratingDiff: pick('ratingDiff').asIntOrNull(), + points: points, + opponent: _playerFromPick(pick('opponent').required()), ); } diff --git a/lib/src/model/common/id.dart b/lib/src/model/common/id.dart index b41a4e82a6..6f0a3729d6 100644 --- a/lib/src/model/common/id.dart +++ b/lib/src/model/common/id.dart @@ -8,6 +8,8 @@ extension type const StringId(String value) { bool startsWith(String prefix) => value.startsWith(prefix); } +extension type const IntId(int value) {} + extension type const GameAnyId._(String value) implements StringId { GameAnyId(this.value) : assert(value.length == 8 || value.length == 12); GameId get gameId => GameId(value.substring(0, 8)); @@ -65,7 +67,7 @@ extension type const StudyChapterId(String value) implements StringId { StudyChapterId.fromJson(dynamic json) : this(json as String); } -extension type const FideId(String value) implements StringId {} +extension type const FideId(int value) implements IntId {} extension IDPick on Pick { UserId asUserIdOrThrow() { @@ -192,7 +194,7 @@ extension IDPick on Pick { ); } - BroadcastRoundId? asBroadcastRoundIddOrNull() { + BroadcastRoundId? asBroadcastRoundIdOrNull() { if (value == null) return null; try { return asBroadcastRoundIdOrThrow(); @@ -211,7 +213,7 @@ extension IDPick on Pick { ); } - BroadcastGameId? asBroadcastGameIddOrNull() { + BroadcastGameId? asBroadcastGameIdOrNull() { if (value == null) return null; try { return asBroadcastGameIdOrThrow(); @@ -232,7 +234,7 @@ extension IDPick on Pick { FideId asFideIdOrThrow() { final value = required().value; - if (value is String) { + if (value is int && value != 0) { return FideId(value); } throw PickException( diff --git a/lib/src/view/broadcast/broadcast_boards_tab.dart b/lib/src/view/broadcast/broadcast_boards_tab.dart index 269f03d455..fa1b3dde91 100644 --- a/lib/src/view/broadcast/broadcast_boards_tab.dart +++ b/lib/src/view/broadcast/broadcast_boards_tab.dart @@ -26,11 +26,9 @@ const _kPlayerWidgetPadding = EdgeInsets.symmetric(vertical: 5.0); class BroadcastBoardsTab extends ConsumerWidget { const BroadcastBoardsTab({ required this.roundId, - required this.broadcastTitle, }); final BroadcastRoundId roundId; - final String broadcastTitle; @override Widget build(BuildContext context, WidgetRef ref) { @@ -60,19 +58,15 @@ class BroadcastBoardsTab extends ConsumerWidget { : BroadcastPreview( games: value.games.values.toIList(), roundId: roundId, - broadcastTitle: broadcastTitle, - roundTitle: value.round.name, + title: value.round.name, + roundUrl: value.round.url, ), AsyncError(:final error) => SliverFillRemaining( child: Center( child: Text('Could not load broadcast: $error'), ), ), - _ => const SliverFillRemaining( - child: Center( - child: CircularProgressIndicator.adaptive(), - ), - ), + _ => const BroadcastPreview.loading() }, ); } @@ -82,20 +76,20 @@ class BroadcastPreview extends StatelessWidget { const BroadcastPreview({ required this.roundId, required this.games, - required this.broadcastTitle, - required this.roundTitle, + required this.title, + required this.roundUrl, }); - const BroadcastPreview.loading({ - required this.roundId, - required this.broadcastTitle, - }) : games = null, - roundTitle = null; + const BroadcastPreview.loading() + : roundId = const BroadcastRoundId(''), + games = null, + title = '', + roundUrl = null; final BroadcastRoundId roundId; final IList? games; - final String broadcastTitle; - final String? roundTitle; + final String title; + final String? roundUrl; @override Widget build(BuildContext context) { @@ -123,7 +117,7 @@ class BroadcastPreview extends StatelessWidget { delegate: SliverChildBuilderDelegate( childCount: games == null ? numberLoadingBoards : games!.length, (context, index) { - if (games == null || roundTitle == null) { + if (games == null) { return ShimmerLoading( isLoading: true, child: BoardThumbnail.loading( @@ -142,12 +136,12 @@ class BroadcastPreview extends StatelessWidget { onTap: () { pushPlatformRoute( context, - title: roundTitle, + title: title, builder: (context) => BroadcastGameScreen( roundId: roundId, gameId: game.id, - broadcastTitle: broadcastTitle, - roundTitle: roundTitle!, + roundUrl: roundUrl, + title: title, ), ); }, diff --git a/lib/src/view/broadcast/broadcast_game_bottom_bar.dart b/lib/src/view/broadcast/broadcast_game_bottom_bar.dart index a2c7a5b804..d2973ec3d9 100644 --- a/lib/src/view/broadcast/broadcast_game_bottom_bar.dart +++ b/lib/src/view/broadcast/broadcast_game_bottom_bar.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_game_controller.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_repository.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast_round_controller.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/game/game_share_service.dart'; import 'package:lichess_mobile/src/network/http.dart'; @@ -18,19 +19,19 @@ class BroadcastGameBottomBar extends ConsumerWidget { const BroadcastGameBottomBar({ required this.roundId, required this.gameId, - required this.broadcastTitle, - required this.roundTitle, + this.roundUrl, }); final BroadcastRoundId roundId; final BroadcastGameId gameId; - final String broadcastTitle; - final String roundTitle; + final String? roundUrl; @override Widget build(BuildContext context, WidgetRef ref) { final ctrlProvider = broadcastGameControllerProvider(roundId, gameId); final broadcastGameState = ref.watch(ctrlProvider).requireValue; + final broadcastRoundState = + ref.watch(broadcastRoundControllerProvider(roundId)); return BottomBar( children: [ @@ -40,17 +41,19 @@ class BroadcastGameBottomBar extends ConsumerWidget { showAdaptiveActionSheet( context: context, actions: [ - BottomSheetAction( - makeLabel: (context) => Text(context.l10n.mobileShareGameURL), - onPressed: (context) async { - launchShareDialog( - context, - uri: lichessUri( - '/broadcast/${broadcastTitle.toLowerCase().replaceAll(' ', '-')}/${roundTitle.toLowerCase().replaceAll(' ', '-')}/$roundId/$gameId', - ), - ); - }, - ), + if (roundUrl != null || broadcastRoundState.hasValue) + BottomSheetAction( + makeLabel: (context) => + Text(context.l10n.mobileShareGameURL), + onPressed: (context) async { + launchShareDialog( + context, + uri: Uri.parse( + '${roundUrl ?? broadcastRoundState.requireValue.round.url}/$gameId', + ), + ); + }, + ), BottomSheetAction( makeLabel: (context) => Text(context.l10n.mobileShareGamePGN), onPressed: (context) async { diff --git a/lib/src/view/broadcast/broadcast_game_screen.dart b/lib/src/view/broadcast/broadcast_game_screen.dart index 55a01f0352..68f860f08e 100644 --- a/lib/src/view/broadcast/broadcast_game_screen.dart +++ b/lib/src/view/broadcast/broadcast_game_screen.dart @@ -33,14 +33,14 @@ import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; class BroadcastGameScreen extends ConsumerStatefulWidget { final BroadcastRoundId roundId; final BroadcastGameId gameId; - final String broadcastTitle; - final String roundTitle; + final String? roundUrl; + final String? title; const BroadcastGameScreen({ required this.roundId, required this.gameId, - required this.broadcastTitle, - required this.roundTitle, + this.roundUrl, + this.title, }); @override @@ -79,11 +79,15 @@ class _BroadcastGameScreenState extends ConsumerState Widget build(BuildContext context) { final broadcastGameState = ref .watch(broadcastGameControllerProvider(widget.roundId, widget.gameId)); + final broadcastRoundState = + ref.watch(broadcastRoundControllerProvider(widget.roundId)); return PlatformScaffold( appBar: PlatformAppBar( title: Text( - widget.roundTitle, + widget.title ?? + broadcastRoundState.value?.round.name ?? + 'BroadcastGame', overflow: TextOverflow.ellipsis, maxLines: 1, ), @@ -113,8 +117,7 @@ class _BroadcastGameScreenState extends ConsumerState AsyncData() => _Body( widget.roundId, widget.gameId, - widget.broadcastTitle, - widget.roundTitle, + widget.roundUrl, tabController: _tabController, ), AsyncError(:final error) => Center( @@ -130,15 +133,13 @@ class _Body extends ConsumerWidget { const _Body( this.roundId, this.gameId, - this.broadcastTitle, - this.roundTitle, { + this.roundUrl, { required this.tabController, }); final BroadcastRoundId roundId; final BroadcastGameId gameId; - final String broadcastTitle; - final String roundTitle; + final String? roundUrl; final TabController tabController; @override @@ -205,8 +206,7 @@ class _Body extends ConsumerWidget { bottomBar: BroadcastGameBottomBar( roundId: roundId, gameId: gameId, - broadcastTitle: broadcastTitle, - roundTitle: roundTitle, + roundUrl: roundUrl, ), children: [ _OpeningExplorerTab(roundId, gameId), diff --git a/lib/src/view/broadcast/broadcast_list_screen.dart b/lib/src/view/broadcast/broadcast_list_screen.dart index dd589f6dc4..0274768015 100644 --- a/lib/src/view/broadcast/broadcast_list_screen.dart +++ b/lib/src/view/broadcast/broadcast_list_screen.dart @@ -238,6 +238,7 @@ class BroadcastGridItem extends StatefulWidget { startsAt: null, finishedAt: null, startsAfterPrevious: false, + url: null, ), group: null, roundToLinkId: BroadcastRoundId(''), diff --git a/lib/src/view/broadcast/broadcast_player_results_screen.dart b/lib/src/view/broadcast/broadcast_player_results_screen.dart new file mode 100644 index 0000000000..cb53bc66ef --- /dev/null +++ b/lib/src/view/broadcast/broadcast_player_results_screen.dart @@ -0,0 +1,351 @@ +import 'dart:math'; + +import 'package:dartchess/dartchess.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast_federation.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast_providers.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:lichess_mobile/src/network/http.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/lichess_assets.dart'; +import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/view/broadcast/broadcast_game_screen.dart'; +import 'package:lichess_mobile/src/view/broadcast/broadcast_player_widget.dart'; +import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; +import 'package:lichess_mobile/src/widgets/progression_widget.dart'; +import 'package:lichess_mobile/src/widgets/stat_card.dart'; + +class BroadcastPlayerResultsScreen extends StatelessWidget { + final BroadcastTournamentId tournamentId; + final String playerId; + final String? playerTitle; + final String playerName; + + const BroadcastPlayerResultsScreen( + this.tournamentId, + this.playerId, + this.playerTitle, + this.playerName, + ); + + @override + Widget build(BuildContext context) { + return PlatformScaffold( + appBar: PlatformAppBar( + title: BroadcastPlayerWidget(title: playerTitle, name: playerName), + ), + body: _Body(tournamentId, playerId), + ); + } +} + +const _kTableRowPadding = EdgeInsets.symmetric( + vertical: 12.0, +); + +class _Body extends ConsumerWidget { + final BroadcastTournamentId tournamentId; + final String playerId; + + const _Body(this.tournamentId, this.playerId); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final playersResults = + ref.watch(broadcastPlayerResultProvider(tournamentId, playerId)); + + switch (playersResults) { + case AsyncData(value: final playerResults): + final player = playerResults.player; + final fideData = playerResults.fideData; + final showRatingDiff = + playerResults.games.any((result) => result.ratingDiff != null); + final statWidth = (MediaQuery.sizeOf(context).width - + Styles.bodyPadding.horizontal - + 10 * 2) / + 3; + const cardSpacing = 10.0; + final indexWidth = max( + 8.0 + playerResults.games.length.toString().length * 10.0, + 28.0, + ); + + return Column( + children: [ + Expanded( + child: ListView.builder( + itemCount: playerResults.games.length + 1, + itemBuilder: (context, index) { + if (index == 0) { + return Padding( + padding: Styles.bodyPadding, + child: Column( + spacing: cardSpacing, + children: [ + if (fideData.ratings.standard != null && + fideData.ratings.rapid != null && + fideData.ratings.blitz != null) + Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: cardSpacing, + children: [ + if (fideData.ratings.standard != null) + SizedBox( + width: statWidth, + child: StatCard( + context.l10n.classical, + value: + fideData.ratings.standard.toString(), + ), + ), + if (fideData.ratings.rapid != null) + SizedBox( + width: statWidth, + child: StatCard( + context.l10n.rapid, + value: fideData.ratings.rapid.toString(), + ), + ), + if (fideData.ratings.blitz != null) + SizedBox( + width: statWidth, + child: StatCard( + context.l10n.blitz, + value: fideData.ratings.blitz.toString(), + ), + ), + ], + ), + if (fideData.birthYear != null && + player.federation != null && + player.fideId != null) + Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: cardSpacing, + children: [ + if (fideData.birthYear != null) + SizedBox( + width: statWidth, + child: StatCard( + context.l10n.broadcastAgeThisYear, + value: (DateTime.now().year - + fideData.birthYear!) + .toString(), + ), + ), + if (player.federation != null) + SizedBox( + width: statWidth, + child: StatCard( + context.l10n.broadcastFederation, + child: Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + SvgPicture.network( + lichessFideFedSrc( + player.federation!, + ), + height: 12, + httpClient: + ref.read(defaultClientProvider), + ), + const SizedBox(width: 5), + Flexible( + child: Text( + fedIdToName[player.federation!]!, + style: const TextStyle( + fontSize: 18.0, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ), + if (player.fideId != null) + SizedBox( + width: statWidth, + child: StatCard( + 'FIDE ID', + value: player.fideId!.toString(), + ), + ), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: cardSpacing, + children: [ + SizedBox( + width: statWidth, + child: StatCard( + context.l10n.broadcastScore, + value: + '${player.score!.toStringAsFixed((player.score! == player.score!.roundToDouble()) ? 0 : 1)} / ${player.played}', + ), + ), + if (player.performance != null) + SizedBox( + width: statWidth, + child: StatCard( + context.l10n.performance, + value: player.performance.toString(), + ), + ), + if (player.ratingDiff != null) + SizedBox( + width: statWidth, + child: StatCard( + context.l10n.broadcastRatingDiff, + child: ProgressionWidget( + player.ratingDiff!, + fontSize: 18.0, + ), + ), + ), + ], + ), + ], + ), + ); + } + + final playerResult = playerResults.games[index - 1]; + + return GestureDetector( + onTap: () { + pushPlatformRoute( + context, + builder: (context) => BroadcastGameScreen( + roundId: playerResult.roundId, + gameId: playerResult.gameId, + ), + ); + }, + child: ColoredBox( + color: (index - 1).isEven + ? Theme.of(context).colorScheme.surfaceContainerLow + : Theme.of(context).colorScheme.surfaceContainerHigh, + child: Padding( + padding: _kTableRowPadding, + child: Row( + children: [ + SizedBox( + width: indexWidth, + child: Center( + child: Text( + index.toString(), + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + ), + ), + Expanded( + flex: 5, + child: BroadcastPlayerWidget( + federation: playerResult.opponent.federation, + title: playerResult.opponent.title, + name: playerResult.opponent.name, + ), + ), + Expanded( + flex: 3, + child: (playerResult.opponent.rating != null) + ? Center( + child: Text( + playerResult.opponent.rating.toString(), + ), + ) + : const SizedBox.shrink(), + ), + SizedBox( + width: 30, + child: Center( + child: Container( + width: 15, + height: 15, + decoration: BoxDecoration( + border: (Theme.of(context).brightness == + Brightness.light && + playerResult.color == + Side.white || + Theme.of(context).brightness == + Brightness.dark && + playerResult.color == + Side.black) + ? Border.all( + width: 2.0, + color: Theme.of(context) + .colorScheme + .outline, + ) + : null, + shape: BoxShape.circle, + color: switch (playerResult.color) { + Side.white => + Colors.white.withValues(alpha: 0.9), + Side.black => + Colors.black.withValues(alpha: 0.9), + }, + ), + ), + ), + ), + SizedBox( + width: 30, + child: Center( + child: Text( + switch (playerResult.points) { + BroadcastPoints.one => '1', + BroadcastPoints.half => '½', + BroadcastPoints.zero => '0', + _ => '*' + }, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: switch (playerResult.points) { + BroadcastPoints.one => + context.lichessColors.good, + BroadcastPoints.zero => + context.lichessColors.error, + _ => null + }, + ), + ), + ), + ), + if (showRatingDiff) + SizedBox( + width: 38, + child: (playerResult.ratingDiff != null) + ? ProgressionWidget( + playerResult.ratingDiff!, + fontSize: 14, + ) + : null, + ), + ], + ), + ), + ), + ); + }, + ), + ), + ], + ); + case AsyncError(:final error): + return Center(child: Text('Cannot load player data: $error')); + case _: + return const Center(child: CircularProgressIndicator.adaptive()); + } + } +} diff --git a/lib/src/view/broadcast/broadcast_player_widget.dart b/lib/src/view/broadcast/broadcast_player_widget.dart index 598cc62f46..dd25b72236 100644 --- a/lib/src/view/broadcast/broadcast_player_widget.dart +++ b/lib/src/view/broadcast/broadcast_player_widget.dart @@ -7,7 +7,7 @@ import 'package:lichess_mobile/src/utils/lichess_assets.dart'; class BroadcastPlayerWidget extends ConsumerWidget { const BroadcastPlayerWidget({ - required this.federation, + this.federation, required this.title, required this.name, this.rating, @@ -35,8 +35,10 @@ class BroadcastPlayerWidget extends ConsumerWidget { if (title != null) ...[ Text( title!, - style: const TextStyle().copyWith( - color: context.lichessColors.brag, + style: TextStyle( + color: (title == 'BOT') + ? context.lichessColors.purple + : context.lichessColors.brag, fontWeight: FontWeight.bold, ), ), @@ -53,7 +55,6 @@ class BroadcastPlayerWidget extends ConsumerWidget { const SizedBox(width: 5), Text( rating.toString(), - style: const TextStyle(), overflow: TextOverflow.ellipsis, ), ], diff --git a/lib/src/view/broadcast/broadcast_players_tab.dart b/lib/src/view/broadcast/broadcast_players_tab.dart index 9088af4e2d..c3a46bf5f9 100644 --- a/lib/src/view/broadcast/broadcast_players_tab.dart +++ b/lib/src/view/broadcast/broadcast_players_tab.dart @@ -8,6 +8,8 @@ import 'package:lichess_mobile/src/model/broadcast/broadcast_providers.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; +import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/view/broadcast/broadcast_player_results_screen.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_player_widget.dart'; import 'package:lichess_mobile/src/widgets/progression_widget.dart'; @@ -27,7 +29,7 @@ class BroadcastPlayersTab extends ConsumerWidget { final players = ref.watch(broadcastPlayersProvider(tournamentId)); return switch (players) { - AsyncData(value: final players) => PlayersList(players), + AsyncData(value: final players) => PlayersList(players, tournamentId), AsyncError(:final error) => SliverPadding( padding: edgeInsets, sliver: SliverFillRemaining( @@ -55,9 +57,10 @@ const _kHeaderTextStyle = TextStyle(fontWeight: FontWeight.bold, overflow: TextOverflow.ellipsis); class PlayersList extends ConsumerStatefulWidget { - const PlayersList(this.players); + const PlayersList(this.players, this.tournamentId); final IList players; + final BroadcastTournamentId tournamentId; @override ConsumerState createState() => _PlayersListState(); @@ -117,7 +120,7 @@ class _PlayersListState extends ConsumerState { @override Widget build(BuildContext context) { final double eloWidth = max(MediaQuery.sizeOf(context).width * 0.2, 100); - final double scoreWidth = max(MediaQuery.sizeOf(context).width * 0.15, 70); + final double scoreWidth = max(MediaQuery.sizeOf(context).width * 0.15, 90); return SliverList.builder( itemCount: players.length + 1, @@ -177,53 +180,72 @@ class _PlayersListState extends ConsumerState { ); } else { final player = players[index - 1]; - return Container( - decoration: BoxDecoration( - color: index.isEven + return GestureDetector( + onTap: () { + pushPlatformRoute( + context, + builder: (context) => BroadcastPlayerResultsScreen( + widget.tournamentId, + player.fideId != null + ? player.fideId.toString() + : player.name, + player.title, + player.name, + ), + ); + }, + child: ColoredBox( + color: (index - 1).isEven ? Theme.of(context).colorScheme.surfaceContainerLow : Theme.of(context).colorScheme.surfaceContainerHigh, - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: Padding( - padding: _kTableRowPadding, - child: BroadcastPlayerWidget( - federation: player.federation, - title: player.title, - name: player.name, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Padding( + padding: _kTableRowPadding, + child: BroadcastPlayerWidget( + federation: player.federation, + title: player.title, + name: player.name, + ), ), ), - ), - SizedBox( - width: eloWidth, - child: Padding( - padding: _kTableRowPadding, - child: Row( - children: [ - if (player.rating != null) ...[ - Text(player.rating.toString()), - const SizedBox(width: 5), - if (player.ratingDiff != null) - ProgressionWidget(player.ratingDiff!, fontSize: 14), + SizedBox( + width: eloWidth, + child: Padding( + padding: _kTableRowPadding, + child: Row( + children: [ + if (player.rating != null) ...[ + Text(player.rating.toString()), + const SizedBox(width: 5), + if (player.ratingDiff != null) + ProgressionWidget( + player.ratingDiff!, + fontSize: 14, + ), + ], ], - ], + ), ), ), - ), - SizedBox( - width: scoreWidth, - child: Padding( - padding: _kTableRowPadding, - child: (player.score != null) - ? Text( - '${player.score!.toStringAsFixed((player.score! == player.score!.roundToDouble()) ? 0 : 1)}/${player.played}', - ) - : const SizedBox.shrink(), + SizedBox( + width: scoreWidth, + child: Padding( + padding: _kTableRowPadding, + child: (player.score != null) + ? Align( + alignment: Alignment.centerRight, + child: Text( + '${player.score!.toStringAsFixed((player.score! == player.score!.roundToDouble()) ? 0 : 1)} / ${player.played}', + ), + ) + : const SizedBox.shrink(), + ), ), - ), - ], + ], + ), ), ); } diff --git a/lib/src/view/broadcast/broadcast_round_screen.dart b/lib/src/view/broadcast/broadcast_round_screen.dart index 814d1e0237..085e8e4e16 100644 --- a/lib/src/view/broadcast/broadcast_round_screen.dart +++ b/lib/src/view/broadcast/broadcast_round_screen.dart @@ -117,7 +117,6 @@ class _BroadcastRoundScreenState extends ConsumerState sliver: switch (asyncTournament) { AsyncData(:final value) => BroadcastBoardsTab( roundId: _selectedRoundId ?? value.defaultRoundId, - broadcastTitle: widget.broadcast.title, ), _ => const SliverFillRemaining( child: SizedBox.shrink(), @@ -182,7 +181,6 @@ class _BroadcastRoundScreenState extends ConsumerState sliver: switch (asyncTournament) { AsyncData(:final value) => BroadcastBoardsTab( roundId: _selectedRoundId ?? value.defaultRoundId, - broadcastTitle: widget.broadcast.title, ), _ => const SliverFillRemaining( child: SizedBox.shrink(), From 413b8c0397ee997059463f600ac529bd489a32f3 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 12 Dec 2024 11:19:21 +0100 Subject: [PATCH 899/979] Ensure live broadcasts stay in sync if ws reconnects --- .../broadcast/broadcast_game_controller.dart | 67 ++++++-- .../broadcast/broadcast_round_controller.dart | 47 ++++-- lib/src/model/common/eval.dart | 8 + lib/src/model/common/node.dart | 75 ++++++++- test/model/common/node_test.dart | 148 +++++++++++++++--- 5 files changed, 298 insertions(+), 47 deletions(-) diff --git a/lib/src/model/broadcast/broadcast_game_controller.dart b/lib/src/model/broadcast/broadcast_game_controller.dart index c5b13db99a..3ac22f1e48 100644 --- a/lib/src/model/broadcast/broadcast_game_controller.dart +++ b/lib/src/model/broadcast/broadcast_game_controller.dart @@ -36,15 +36,18 @@ class BroadcastGameController extends _$BroadcastGameController AppLifecycleListener? _appLifecycleListener; StreamSubscription? _subscription; + StreamSubscription? _socketOpenSubscription; late SocketClient _socketClient; late Root _root; final _engineEvalDebounce = Debouncer(const Duration(milliseconds: 150)); + final _syncDebouncer = Debouncer(const Duration(milliseconds: 150)); - DateTime? _onPauseAt; Timer? _startEngineEvalTimer; + Object? _key = Object(); + @override Future build( BroadcastRoundId roundId, @@ -56,30 +59,37 @@ class BroadcastGameController extends _$BroadcastGameController _subscription = _socketClient.stream.listen(_handleSocketEvent); + await _socketClient.firstConnection; + + _socketOpenSubscription = _socketClient.connectedStream.listen((_) { + if (state.valueOrNull?.isOngoing == true) { + _syncDebouncer(() { + _reloadPgn(); + }); + } + }); + final evaluationService = ref.watch(evaluationServiceProvider); _appLifecycleListener = AppLifecycleListener( - onPause: () { - _onPauseAt = DateTime.now(); - }, onResume: () { if (state.valueOrNull?.isOngoing == true) { - if (_onPauseAt != null) { - final diff = DateTime.now().difference(_onPauseAt!); - if (diff >= const Duration(minutes: 5)) { - ref.invalidateSelf(); - } - } + _syncDebouncer(() { + _reloadPgn(); + }); } }, ); ref.onDispose(() { + _key = null; _subscription?.cancel(); + _socketOpenSubscription?.cancel(); _startEngineEvalTimer?.cancel(); _engineEvalDebounce.dispose(); evaluationService.disposeEngine(); _appLifecycleListener?.dispose(); + _syncDebouncer.dispose(); }); final pgn = await ref.withClient( @@ -133,6 +143,43 @@ class BroadcastGameController extends _$BroadcastGameController return broadcastState; } + Future _reloadPgn() async { + if (!state.hasValue) return; + final key = _key; + + final pgn = await ref.withClient( + (client) => BroadcastRepository(client).getGamePgn(roundId, gameId), + ); + + // check provider is still mounted + if (key == _key) { + final game = PgnGame.parsePgn(pgn); + final pgnHeaders = IMap(game.headers); + final rootComments = + IList(game.comments.map((c) => PgnComment.fromPgn(c))); + + final newRoot = Root.fromPgnGame(game); + + final broadcastPath = newRoot.mainlinePath; + final lastMove = newRoot.branchAt(newRoot.mainlinePath)?.sanMove.move; + + newRoot.merge(_root); + + _root = newRoot; + + state = AsyncData( + state.requireValue.copyWith( + pgnHeaders: pgnHeaders, + pgnRootComments: rootComments, + broadcastPath: broadcastPath, + root: _root.view, + lastMove: lastMove, + clocks: _getClocks(state.requireValue.currentPath), + ), + ); + } + } + void _handleSocketEvent(SocketEvent event) { if (!state.hasValue) return; diff --git a/lib/src/model/broadcast/broadcast_round_controller.dart b/lib/src/model/broadcast/broadcast_round_controller.dart index a2b8f5354e..680e26ddd8 100644 --- a/lib/src/model/broadcast/broadcast_round_controller.dart +++ b/lib/src/model/broadcast/broadcast_round_controller.dart @@ -12,6 +12,7 @@ import 'package:lichess_mobile/src/model/common/socket.dart'; import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/network/socket.dart'; import 'package:lichess_mobile/src/utils/json.dart'; +import 'package:lichess_mobile/src/utils/rate_limit.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'broadcast_round_controller.g.dart'; @@ -22,12 +23,15 @@ class BroadcastRoundController extends _$BroadcastRoundController { Uri(path: 'study/$broadcastRoundId/socket/v6'); StreamSubscription? _subscription; + StreamSubscription? _socketOpenSubscription; AppLifecycleListener? _appLifecycleListener; - DateTime? _onPauseAt; - late SocketClient _socketClient; + final _debouncer = Debouncer(const Duration(milliseconds: 150)); + + Object? _key = Object(); + @override Future build( BroadcastRoundId broadcastRoundId, @@ -38,32 +42,49 @@ class BroadcastRoundController extends _$BroadcastRoundController { _subscription = _socketClient.stream.listen(_handleSocketEvent); + await _socketClient.firstConnection; + + _socketOpenSubscription = _socketClient.connectedStream.listen((_) { + if (state.valueOrNull?.round.status == RoundStatus.live) { + _debouncer(() { + _syncRound(); + }); + } + }); + _appLifecycleListener = AppLifecycleListener( - onPause: () { - _onPauseAt = DateTime.now(); - }, onResume: () { if (state.valueOrNull?.round.status == RoundStatus.live) { - if (_onPauseAt != null) { - final diff = DateTime.now().difference(_onPauseAt!); - if (diff >= const Duration(minutes: 5)) { - ref.invalidateSelf(); - } - } + _debouncer(() { + _syncRound(); + }); } }, ); ref.onDispose(() { + _key = null; _subscription?.cancel(); + _socketOpenSubscription?.cancel(); _appLifecycleListener?.dispose(); + _debouncer.dispose(); }); - final round = await ref.withClient( + return ref.withClient( (client) => BroadcastRepository(client).getRound(broadcastRoundId), ); + } - return round; + Future _syncRound() async { + if (state.hasValue == false) return; + final key = _key; + final round = await ref.withClient( + (client) => BroadcastRepository(client).getRound(broadcastRoundId), + ); + // check provider is still mounted + if (key == _key) { + state = AsyncData(round); + } } void _handleSocketEvent(SocketEvent event) { diff --git a/lib/src/model/common/eval.dart b/lib/src/model/common/eval.dart index 902eeef306..46976bc307 100644 --- a/lib/src/model/common/eval.dart +++ b/lib/src/model/common/eval.dart @@ -31,6 +31,14 @@ class ExternalEval with _$ExternalEval implements Eval { ({String name, String comment})? judgment, }) = _ExternalEval; + factory ExternalEval.fromPgnEval(PgnEvaluation eval) { + return ExternalEval( + cp: eval.pawns != null ? cpFromPawns(eval.pawns!) : null, + mate: eval.mate, + depth: eval.depth, + ); + } + factory ExternalEval.fromJson(Map json) => _$ExternalEvalFromJson(json); diff --git a/lib/src/model/common/node.dart b/lib/src/model/common/node.dart index b6dbb82403..fd971cc615 100644 --- a/lib/src/model/common/node.dart +++ b/lib/src/model/common/node.dart @@ -141,6 +141,23 @@ abstract class Node { } } + void merge(Node other) { + if (other.eval != null) { + eval = other.eval; + } + if (other.opening != null) { + opening = other.opening; + } + for (final otherChild in other.children) { + final child = childById(otherChild.id); + if (child != null) { + child.merge(otherChild); + } else { + addChild(otherChild); + } + } + } + /// Adds a new node at the given path and returns the new path. /// /// Returns a tuple of the new path and whether the node was added. @@ -419,13 +436,69 @@ class Branch extends Node { @override Branch branchAt(UciPath path) => nodeAt(path) as Branch; - /// Gets the clock information from the comments. + @override + void merge(Node other) { + if (other is Branch) { + other.lichessAnalysisComments?.forEach((c) { + if (lichessAnalysisComments == null) { + lichessAnalysisComments = [c]; + } else { + final existing = lichessAnalysisComments?.firstWhereOrNull( + (e) => e.text == c.text, + ); + if (existing == null) { + lichessAnalysisComments?.add(c); + } + } + }); + other.startingComments?.forEach((c) { + if (startingComments == null) { + startingComments = [c]; + } else { + final existing = startingComments?.firstWhereOrNull( + (e) => e.text == c.text, + ); + if (existing == null) { + startingComments?.add(c); + } + } + }); + other.comments?.forEach((c) { + if (comments == null) { + comments = [c]; + } else { + final existing = comments?.firstWhereOrNull( + (e) => e.text == c.text, + ); + if (existing == null) { + comments?.add(c); + } + } + }); + if (other.nags != null) { + nags = other.nags; + } + } + super.merge(other); + } + + /// Gets the first available clock from the comments. Duration? get clock { final clockComment = (lichessAnalysisComments ?? comments) ?.firstWhereOrNull((c) => c.clock != null); return clockComment?.clock; } + /// Gets the first available external eval from the comments. + ExternalEval? get externalEval { + final comment = (lichessAnalysisComments ?? comments)?.firstWhereOrNull( + (c) => c.eval != null, + ); + return comment?.eval != null + ? ExternalEval.fromPgnEval(comment!.eval!) + : null; + } + @override String toString() { return 'Branch(id: $id, fen: ${position.fen}, sanMove: $sanMove, eval: $eval, children: $children)'; diff --git a/test/model/common/node_test.dart b/test/model/common/node_test.dart index f9d82ad696..8459d79ff1 100644 --- a/test/model/common/node_test.dart +++ b/test/model/common/node_test.dart @@ -1,3 +1,4 @@ +import 'package:collection/collection.dart'; import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -453,6 +454,106 @@ void main() { ), ); }); + + group('merge', () { + test('add moves', () { + const pgn = ''' +1. d4 { [%clk 1:00:00] } Nf6 { [%clk 1:00:00] } 2. c4 { [%clk 1:00:00] } g6 { [%clk 1:00:00] } 3. Nc3 { [%clk 1:00:00] } Bg7 { [%clk 1:00:00] } 4. e4 { [%clk 1:00:00] } d6 { [%clk 1:00:00] } 5. f3 { [%clk 1:00:00] } O-O { [%clk 1:00:00] } 6. Be3 { [%clk 1:00:00] } e5 { [%clk 1:00:00] } 7. d5 { [%clk 1:00:00] } Nh5 { [%clk 1:00:00] } 8. Qd2 { [%clk 1:00:00] } Qh4+ { [%clk 1:00:00] } 9. g3 { [%clk 1:00:00] } Qe7 { [%clk 1:00:00] } 10. Nh3 { [%clk 1:00:00] } f5 { [%clk 0:56:44] } 11. exf5 { [%clk 0:58:18] } gxf5 { [%clk 0:55:20] } 12. O-O-O { [%clk 0:57:22] } Na6 { [%clk 0:52:30] } 13. Re1 { [%clk 0:52:22] } Nf6 { [%clk 0:48:20] } 14. Ng5 { [%clk 0:50:43] } c6 { [%clk 0:47:38] } 15. h4 { [%clk 0:50:01] } h6 { [%clk 0:46:10] } 16. Nh3 { [%clk 0:49:18] } cxd5 { [%clk 0:45:06] } 17. Bxh6 { [%clk 0:47:13] } Bxh6 { [%clk 0:44:17] } 18. Qxh6 { [%clk 0:45:59] } Bd7 { [%clk 0:43:34] } 19. cxd5 { [%clk 0:45:15] } Nc5 { [%clk 0:42:50] } 20. Kb1 { [%clk 0:44:14] } Qg7 { [%clk 0:41:29] } 21. Qd2 { [%clk 0:42:39] } e4 { [%clk 0:40:55] } 22. b4 { [%clk 0:40:31] } Na4 { [%clk 0:39:58] } 23. Nxa4 { [%clk 0:39:13] } Bxa4 { [%clk 0:38:39] } 24. Ng5 { [%clk 0:37:47] } Rfc8 { [%clk 0:37:14] } * +'''; + + const pgn2 = ''' +1. d4 { [%clk 1:00:00] } Nf6 { [%clk 1:00:00] } 2. c4 { [%clk 1:00:00] } g6 { [%clk 1:00:00] } 3. Nc3 { [%clk 1:00:00] } Bg7 { [%clk 1:00:00] } 4. e4 { [%clk 1:00:00] } d6 { [%clk 1:00:00] } 5. f3 { [%clk 1:00:00] } O-O { [%clk 1:00:00] } 6. Be3 { [%clk 1:00:00] } e5 { [%clk 1:00:00] } 7. d5 { [%clk 1:00:00] } Nh5 { [%clk 1:00:00] } 8. Qd2 { [%clk 1:00:00] } Qh4+ { [%clk 1:00:00] } 9. g3 { [%clk 1:00:00] } Qe7 { [%clk 1:00:00] } 10. Nh3 { [%clk 1:00:00] } f5 { [%clk 0:56:44] } 11. exf5 { [%clk 0:58:18] } gxf5 { [%clk 0:55:20] } 12. O-O-O { [%clk 0:57:22] } Na6 { [%clk 0:52:30] } 13. Re1 { [%clk 0:52:22] } Nf6 { [%clk 0:48:20] } 14. Ng5 { [%clk 0:50:43] } c6 { [%clk 0:47:38] } 15. h4 { [%clk 0:50:01] } h6 { [%clk 0:46:10] } 16. Nh3 { [%clk 0:49:18] } cxd5 { [%clk 0:45:06] } 17. Bxh6 { [%clk 0:47:13] } Bxh6 { [%clk 0:44:17] } 18. Qxh6 { [%clk 0:45:59] } Bd7 { [%clk 0:43:34] } 19. cxd5 { [%clk 0:45:15] } Nc5 { [%clk 0:42:50] } 20. Kb1 { [%clk 0:44:14] } Qg7 { [%clk 0:41:29] } 21. Qd2 { [%clk 0:42:39] } e4 { [%clk 0:40:55] } 22. b4 { [%clk 0:40:31] } Na4 { [%clk 0:39:58] } 23. Nxa4 { [%clk 0:39:13] } Bxa4 { [%clk 0:38:39] } 24. Ng5 { [%clk 0:37:47] } Rfc8 { [%clk 0:37:14] } 25. Ne6 { [%clk 0:36:01] } Rc2 { [%clk 0:36:38] } * +'''; + + final root = Root.fromPgnGame(PgnGame.parsePgn(pgn)); + expect(root.mainline.length, equals(48)); + + final root2 = Root.fromPgnGame(PgnGame.parsePgn(pgn2)); + expect(root2.mainline.length, equals(50)); + + root2.merge(root); + expect(root2.mainline.length, equals(50)); + expect(root2.mainline.last.sanMove.san, equals('Rc2')); + + for (final nodes in IterableZip([root.mainline, root2.mainline])) { + final [node1, node2] = nodes; + expect(node1.sanMove, equals(node2.sanMove)); + expect(node1.position.fen, equals(node2.position.fen)); + expect(node1.clock, equals(node2.clock)); + } + }); + + test('preserve variations', () { + const pgn = ''' +1. d4 { [%clk 1:00:00] } Nf6 { [%clk 1:00:00] } 2. c4 { [%clk 1:00:00] } g6 { [%clk 1:00:00] } 3. Nc3 { [%clk 1:00:00] } Bg7 { [%clk 1:00:00] } 4. e4 { [%clk 1:00:00] } d6 { [%clk 1:00:00] } 5. f3 { [%clk 1:00:00] } O-O { [%clk 1:00:00] } 6. Be3 { [%clk 1:00:00] } e5 { [%clk 1:00:00] } 7. d5 { [%clk 1:00:00] } Nh5 { [%clk 1:00:00] } 8. Qd2 { [%clk 1:00:00] } Qh4+ { [%clk 1:00:00] } 9. g3 { [%clk 1:00:00] } Qe7 { [%clk 1:00:00] } 10. Nh3 { [%clk 1:00:00] } f5 { [%clk 0:56:44] } 11. exf5 { [%clk 0:58:18] } gxf5 { [%clk 0:55:20] } 12. O-O-O { [%clk 0:57:22] } Na6 { [%clk 0:52:30] } 13. Re1 { [%clk 0:52:22] } Nf6 { [%clk 0:48:20] } 14. Ng5 { [%clk 0:50:43] } c6 { [%clk 0:47:38] } 15. h4 { [%clk 0:50:01] } h6 { [%clk 0:46:10] } 16. Nh3 { [%clk 0:49:18] } cxd5 { [%clk 0:45:06] } 17. Bxh6 { [%clk 0:47:13] } Bxh6 { [%clk 0:44:17] } 18. Qxh6 { [%clk 0:45:59] } Bd7 { [%clk 0:43:34] } 19. cxd5 { [%clk 0:45:15] } Nc5 { [%clk 0:42:50] } 20. Kb1 { [%clk 0:44:14] } Qg7 { [%clk 0:41:29] } 21. Qd2 { [%clk 0:42:39] } e4 { [%clk 0:40:55] } 22. b4 { [%clk 0:40:31] } Na4 { [%clk 0:39:58] } 23. Nxa4 { [%clk 0:39:13] } Bxa4 { [%clk 0:38:39] } 24. Ng5 { [%clk 0:37:47] } Rfc8 { [%clk 0:37:14] } 25. Ne6 { [%clk 0:36:01] } Rc2 { [%clk 0:36:38] } 26. Qe3 { [%clk 0:34:49] } Nxd5 { [%clk 0:34:34] } ( 26... Qe7 27. Rc1 ) * +'''; + final root = Root.fromPgnGame(PgnGame.parsePgn(pgn)); + expect(root.mainline.length, equals(52)); + + const pgn2 = ''' + 1. d4 { [%clk 1:00:00] } Nf6 { [%clk 1:00:00] } 2. c4 { [%clk 1:00:00] } g6 { [%clk 1:00:00] } 3. Nc3 { [%clk 1:00:00] } Bg7 { [%clk 1:00:00] } 4. e4 { [%clk 1:00:00] } d6 { [%clk 1:00:00] } 5. f3 { [%clk 1:00:00] } O-O { [%clk 1:00:00] } 6. Be3 { [%clk 1:00:00] } e5 { [%clk 1:00:00] } 7. d5 { [%clk 1:00:00] } Nh5 { [%clk 1:00:00] } 8. Qd2 { [%clk 1:00:00] } Qh4+ { [%clk 1:00:00] } 9. g3 { [%clk 1:00:00] } Qe7 { [%clk 1:00:00] } 10. Nh3 { [%clk 1:00:00] } f5 { [%clk 0:56:44] } 11. exf5 { [%clk 0:58:18] } gxf5 { [%clk 0:55:20] } 12. O-O-O { [%clk 0:57:22] } Na6 { [%clk 0:52:30] } 13. Re1 { [%clk 0:52:22] } Nf6 { [%clk 0:48:20] } 14. Ng5 { [%clk 0:50:43] } c6 { [%clk 0:47:38] } 15. h4 { [%clk 0:50:01] } h6 { [%clk 0:46:10] } 16. Nh3 { [%clk 0:49:18] } cxd5 { [%clk 0:45:06] } 17. Bxh6 { [%clk 0:47:13] } Bxh6 { [%clk 0:44:17] } 18. Qxh6 { [%clk 0:45:59] } Bd7 { [%clk 0:43:34] } 19. cxd5 { [%clk 0:45:15] } Nc5 { [%clk 0:42:50] } 20. Kb1 { [%clk 0:44:14] } Qg7 { [%clk 0:41:29] } 21. Qd2 { [%clk 0:42:39] } e4 { [%clk 0:40:55] } 22. b4 { [%clk 0:40:31] } Na4 { [%clk 0:39:58] } 23. Nxa4 { [%clk 0:39:13] } Bxa4 { [%clk 0:38:39] } 24. Ng5 { [%clk 0:37:47] } Rfc8 { [%clk 0:37:14] } 25. Ne6 { [%clk 0:36:01] } Rc2 { [%clk 0:36:38] } 26. Qe3 { [%clk 0:34:49] } Nxd5 { [%clk 0:34:34] } 27. Nxg7 { [%clk 0:34:17] } Nxe3 { [%clk 0:34:04] } 28. Rxe3 { [%clk 0:33:12] } Kxg7 { [%clk 0:33:33] } 29. Ra3 { [%clk 0:31:18] } Rac8 { [%clk 0:32:46] } 30. Bh3 { [%clk 0:30:15] } * + '''; + final root2 = Root.fromPgnGame(PgnGame.parsePgn(pgn2)); + expect(root2.mainline.length, equals(59)); + + root2.merge(root); + expect(root2.mainline.length, equals(59)); + expect(root2.makePgn(), ''' +1. d4 { [%clk 1:00:00] } Nf6 { [%clk 1:00:00] } 2. c4 { [%clk 1:00:00] } g6 { [%clk 1:00:00] } 3. Nc3 { [%clk 1:00:00] } Bg7 { [%clk 1:00:00] } 4. e4 { [%clk 1:00:00] } d6 { [%clk 1:00:00] } 5. f3 { [%clk 1:00:00] } O-O { [%clk 1:00:00] } 6. Be3 { [%clk 1:00:00] } e5 { [%clk 1:00:00] } 7. d5 { [%clk 1:00:00] } Nh5 { [%clk 1:00:00] } 8. Qd2 { [%clk 1:00:00] } Qh4+ { [%clk 1:00:00] } 9. g3 { [%clk 1:00:00] } Qe7 { [%clk 1:00:00] } 10. Nh3 { [%clk 1:00:00] } f5 { [%clk 0:56:44] } 11. exf5 { [%clk 0:58:18] } gxf5 { [%clk 0:55:20] } 12. O-O-O { [%clk 0:57:22] } Na6 { [%clk 0:52:30] } 13. Re1 { [%clk 0:52:22] } Nf6 { [%clk 0:48:20] } 14. Ng5 { [%clk 0:50:43] } c6 { [%clk 0:47:38] } 15. h4 { [%clk 0:50:01] } h6 { [%clk 0:46:10] } 16. Nh3 { [%clk 0:49:18] } cxd5 { [%clk 0:45:06] } 17. Bxh6 { [%clk 0:47:13] } Bxh6 { [%clk 0:44:17] } 18. Qxh6 { [%clk 0:45:59] } Bd7 { [%clk 0:43:34] } 19. cxd5 { [%clk 0:45:15] } Nc5 { [%clk 0:42:50] } 20. Kb1 { [%clk 0:44:14] } Qg7 { [%clk 0:41:29] } 21. Qd2 { [%clk 0:42:39] } e4 { [%clk 0:40:55] } 22. b4 { [%clk 0:40:31] } Na4 { [%clk 0:39:58] } 23. Nxa4 { [%clk 0:39:13] } Bxa4 { [%clk 0:38:39] } 24. Ng5 { [%clk 0:37:47] } Rfc8 { [%clk 0:37:14] } 25. Ne6 { [%clk 0:36:01] } Rc2 { [%clk 0:36:38] } 26. Qe3 { [%clk 0:34:49] } Nxd5 { [%clk 0:34:34] } ( 26... Qe7 27. Rc1 ) 27. Nxg7 { [%clk 0:34:17] } Nxe3 { [%clk 0:34:04] } 28. Rxe3 { [%clk 0:33:12] } Kxg7 { [%clk 0:33:33] } 29. Ra3 { [%clk 0:31:18] } Rac8 { [%clk 0:32:46] } 30. Bh3 { [%clk 0:30:15] } * +'''); + }); + + test('preserve evals', () { + const pgn = ''' +1. d4 { [%clk 1:00:00] } Nf6 { [%clk 1:00:00] } 2. c4 { [%clk 1:00:00] } g6 { [%clk 1:00:00] } 3. Nc3 { [%clk 1:00:00] } Bg7 { [%clk 1:00:00] } 4. e4 { [%clk 1:00:00] } d6 { [%clk 1:00:00] } 5. f3 { [%clk 1:00:00] } O-O { [%clk 1:00:00] } 6. Be3 { [%clk 1:00:00] } e5 { [%clk 1:00:00] } 7. d5 { [%clk 1:00:00] } Nh5 { [%clk 1:00:00] } 8. Qd2 { [%clk 1:00:00] } Qh4+ { [%clk 1:00:00] } 9. g3 { [%clk 1:00:00] } Qe7 { [%clk 1:00:00] } 10. Nh3 { [%clk 1:00:00] } f5 { [%clk 0:56:44] } 11. exf5 { [%clk 0:58:18] } gxf5 { [%clk 0:55:20] } 12. O-O-O { [%clk 0:57:22] } Na6 { [%clk 0:52:30] } 13. Re1 { [%clk 0:52:22] } Nf6 { [%clk 0:48:20] } 14. Ng5 { [%clk 0:50:43] } c6 { [%clk 0:47:38] } 15. h4 { [%clk 0:50:01] } h6 { [%clk 0:46:10] } 16. Nh3 { [%clk 0:49:18] } cxd5 { [%clk 0:45:06] } 17. Bxh6 { [%clk 0:47:13] } Bxh6 { [%clk 0:44:17] } 18. Qxh6 { [%clk 0:45:59] } Bd7 { [%clk 0:43:34] } 19. cxd5 { [%clk 0:45:15] } Nc5 { [%clk 0:42:50] } 20. Kb1 { [%clk 0:44:14] } Qg7 { [%clk 0:41:29] } 21. Qd2 { [%clk 0:42:39] } e4 { [%clk 0:40:55] } 22. b4 { [%clk 0:40:31] } Na4 { [%clk 0:39:58] } 23. Nxa4 { [%clk 0:39:13] } Bxa4 { [%clk 0:38:39] } 24. Ng5 { [%clk 0:37:47] } Rfc8 { [%clk 0:37:14] } 25. Ne6 { [%clk 0:36:01] } Rc2 { [%clk 0:36:38] } 26. Qe3 { [%clk 0:34:49] } Nxd5 { [%clk 0:34:34] } 27. Nxg7 { [%clk 0:34:17] } Nxe3 { [%clk 0:34:04] } 28. Rxe3 { [%clk 0:33:12] } Kxg7 { [%clk 0:33:33] } 29. Ra3 { [%clk 0:31:18] } Rac8 { [%clk 0:32:46] } 30. Bh3 { [%clk 0:30:15] } * + '''; + + final root = Root.fromPgnGame(PgnGame.parsePgn(pgn)); + expect(root.mainline.length, equals(59)); + final clientEval = ClientEval( + position: Chess.initial, + depth: 22, + nodes: 100000, + pvs: IList(), + millis: 1230900, + searchTime: const Duration(milliseconds: 1230900), + cp: 23, + ); + root.mainline.last.eval = clientEval; + + const pgn2 = ''' +1. d4 { [%clk 1:00:00] } Nf6 { [%clk 1:00:00] } 2. c4 { [%clk 1:00:00] } g6 { [%clk 1:00:00] } 3. Nc3 { [%clk 1:00:00] } Bg7 { [%clk 1:00:00] } 4. e4 { [%clk 1:00:00] } d6 { [%clk 1:00:00] } 5. f3 { [%clk 1:00:00] } O-O { [%clk 1:00:00] } 6. Be3 { [%clk 1:00:00] } e5 { [%clk 1:00:00] } 7. d5 { [%clk 1:00:00] } Nh5 { [%clk 1:00:00] } 8. Qd2 { [%clk 1:00:00] } Qh4+ { [%clk 1:00:00] } 9. g3 { [%clk 1:00:00] } Qe7 { [%clk 1:00:00] } 10. Nh3 { [%clk 1:00:00] } f5 { [%clk 0:56:44] } 11. exf5 { [%clk 0:58:18] } gxf5 { [%clk 0:55:20] } 12. O-O-O { [%clk 0:57:22] } Na6 { [%clk 0:52:30] } 13. Re1 { [%clk 0:52:22] } Nf6 { [%clk 0:48:20] } 14. Ng5 { [%clk 0:50:43] } c6 { [%clk 0:47:38] } 15. h4 { [%clk 0:50:01] } h6 { [%clk 0:46:10] } 16. Nh3 { [%clk 0:49:18] } cxd5 { [%clk 0:45:06] } 17. Bxh6 { [%clk 0:47:13] } Bxh6 { [%clk 0:44:17] } 18. Qxh6 { [%clk 0:45:59] } Bd7 { [%clk 0:43:34] } 19. cxd5 { [%clk 0:45:15] } Nc5 { [%clk 0:42:50] } 20. Kb1 { [%clk 0:44:14] } Qg7 { [%clk 0:41:29] } 21. Qd2 { [%clk 0:42:39] } e4 { [%clk 0:40:55] } 22. b4 { [%clk 0:40:31] } Na4 { [%clk 0:39:58] } 23. Nxa4 { [%clk 0:39:13] } Bxa4 { [%clk 0:38:39] } 24. Ng5 { [%clk 0:37:47] } Rfc8 { [%clk 0:37:14] } 25. Ne6 { [%clk 0:36:01] } Rc2 { [%clk 0:36:38] } 26. Qe3 { [%clk 0:34:49] } Nxd5 { [%clk 0:34:34] } 27. Nxg7 { [%clk 0:34:17] } Nxe3 { [%clk 0:34:04] } 28. Rxe3 { [%clk 0:33:12] } Kxg7 { [%clk 0:33:33] } 29. Ra3 { [%clk 0:31:18] } Rac8 { [%clk 0:32:46] } 30. Bh3 { [%clk 0:30:15] } Bd7 { [%clk 0:32:05] } 31. fxe4 { [%clk 0:29:38] } R8c4 { [%clk 0:31:11] } 32. Rxa7 { [%clk 0:27:46] } Bc6 { [%clk 0:30:37] } 33. Bxf5 { [%clk 0:27:20] } Re2 { [%clk 0:29:32] } 34. b5 { [%clk 0:26:56] } Rb4+ { [%clk 0:29:02] } 35. Ka1 { [%clk 0:25:59] } Rxb5 { [%clk 0:28:13] } 36. Rb1 { [%clk 0:25:17] } Rc5 { [%clk 0:27:47] } 37. h5 { [%clk 0:23:42] } Rh2 { [%clk 0:27:22] } 38. g4 { [%clk 0:22:55] } Kf6 { [%clk 0:26:59] } 39. Ra3 { [%clk 0:22:10] } Rc4 { [%clk 0:26:36] } 40. Re1 { [%eval 1.17,33] [%clk 0:19:30] } * +'''; + + final root2 = Root.fromPgnGame(PgnGame.parsePgn(pgn2)); + expect(root2.mainline.length, equals(79)); + + root2.merge(root); + expect(root2.mainline.length, equals(79)); + + for (final nodes in IterableZip([root.mainline, root2.mainline])) { + final [node1, node2] = nodes; + expect(node1.sanMove, equals(node2.sanMove)); + expect(node1.position.fen, equals(node2.position.fen)); + expect(node1.clock, equals(node2.clock)); + } + // one new external eval + expect( + root2.mainline.where((n) => n.externalEval != null).length, + equals(1), + ); + // one old client eval preseved + expect( + root2.mainline.where((node) => node.eval != null).length, + equals(1), + ); + expect( + root2.mainline.firstWhereOrNull((node) => node.eval != null)?.eval, + equals(clientEval), + ); + }); + }); + group('convert alternative castling move', () { void makeTestAltCastlingMove(String pgn, String alt1, String alt2) { final root = Root.fromPgnGame(PgnGame.parsePgn(pgn)); @@ -505,29 +606,30 @@ void main() { 'e8a8', ); }); - }); - test('only convert king moves in altCastlingMove', () { - const pgn = - '1. e4 e5 2. Bc4 Qh4 3. Nf3 Qxh2 4. Ke2 Qxh1 5. Qe1 Qh5 6. Qh1'; - final root = Root.fromPgnGame(PgnGame.parsePgn(pgn)); - final initialPng = root.makePgn(); - final previousUciPath = root.mainlinePath.penultimate; - final move = Move.parse('e1g1'); - root.addMoveAt(previousUciPath, move!); - expect(root.makePgn(), isNot(initialPng)); - }); - test( - 'do not convert castling move if rook is on the alternative castling square', - () { - const pgn = - '[FEN "rnbqkbnr/pppppppp/8/8/8/2NBQ3/PPPPPPPP/2R1KBNR w KQkq - 0 1"]'; - final root = Root.fromPgnGame(PgnGame.parsePgn(pgn)); - final initialPng = root.makePgn(); - final previousUciPath = root.mainlinePath.penultimate; - final move = Move.parse('e1c1'); - root.addMoveAt(previousUciPath, move!); - expect(root.makePgn(), isNot(initialPng)); - expect(root.mainline.last.sanMove.move, move); + test('only convert king moves in altCastlingMove', () { + const pgn = + '1. e4 e5 2. Bc4 Qh4 3. Nf3 Qxh2 4. Ke2 Qxh1 5. Qe1 Qh5 6. Qh1'; + final root = Root.fromPgnGame(PgnGame.parsePgn(pgn)); + final initialPng = root.makePgn(); + final previousUciPath = root.mainlinePath.penultimate; + final move = Move.parse('e1g1'); + root.addMoveAt(previousUciPath, move!); + expect(root.makePgn(), isNot(initialPng)); + }); + + test( + 'do not convert castling move if rook is on the alternative castling square', + () { + const pgn = + '[FEN "rnbqkbnr/pppppppp/8/8/8/2NBQ3/PPPPPPPP/2R1KBNR w KQkq - 0 1"]'; + final root = Root.fromPgnGame(PgnGame.parsePgn(pgn)); + final initialPng = root.makePgn(); + final previousUciPath = root.mainlinePath.penultimate; + final move = Move.parse('e1c1'); + root.addMoveAt(previousUciPath, move!); + expect(root.makePgn(), isNot(initialPng)); + expect(root.mainline.last.sanMove.move, move); + }); }); }); From 9c9c20f9506ba7ec649cce1cb0b6a14499b94a1a Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 12 Dec 2024 12:10:21 +0100 Subject: [PATCH 900/979] Wait before loading ios round tab --- .../broadcast/broadcast_round_screen.dart | 51 ++++++++++--------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_round_screen.dart b/lib/src/view/broadcast/broadcast_round_screen.dart index 814d1e0237..d3fae35352 100644 --- a/lib/src/view/broadcast/broadcast_round_screen.dart +++ b/lib/src/view/broadcast/broadcast_round_screen.dart @@ -104,31 +104,36 @@ class _BroadcastRoundScreenState extends ConsumerState child: Column( children: [ Expanded( - child: switch (selectedTab) { - _CupertinoView.overview => _TabView( - cupertinoTabSwitcher: tabSwitcher, - sliver: BroadcastOverviewTab( - broadcast: widget.broadcast, - tournamentId: _selectedTournamentId, - ), - ), - _CupertinoView.boards => _TabView( - cupertinoTabSwitcher: tabSwitcher, - sliver: switch (asyncTournament) { - AsyncData(:final value) => BroadcastBoardsTab( - roundId: _selectedRoundId ?? value.defaultRoundId, - broadcastTitle: widget.broadcast.title, + child: switch (asyncRound) { + AsyncData(value: final _) => switch (selectedTab) { + _CupertinoView.overview => _TabView( + cupertinoTabSwitcher: tabSwitcher, + sliver: BroadcastOverviewTab( + broadcast: widget.broadcast, + tournamentId: _selectedTournamentId, ), - _ => const SliverFillRemaining( - child: SizedBox.shrink(), + ), + _CupertinoView.boards => _TabView( + cupertinoTabSwitcher: tabSwitcher, + sliver: switch (asyncTournament) { + AsyncData(:final value) => BroadcastBoardsTab( + roundId: _selectedRoundId ?? value.defaultRoundId, + broadcastTitle: widget.broadcast.title, + ), + _ => const SliverFillRemaining( + child: SizedBox.shrink(), + ), + }, + ), + _CupertinoView.players => _TabView( + cupertinoTabSwitcher: tabSwitcher, + sliver: BroadcastPlayersTab( + tournamentId: _selectedTournamentId, ), - }, - ), - _CupertinoView.players => _TabView( - cupertinoTabSwitcher: tabSwitcher, - sliver: BroadcastPlayersTab( - tournamentId: _selectedTournamentId, - ), + ), + }, + _ => const Center( + child: CircularProgressIndicator.adaptive(), ), }, ), From ea4289ad97305410743a6cf886e9202c49b2ecae Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 12 Dec 2024 12:14:47 +0100 Subject: [PATCH 901/979] Remove mobile blindfold and update translations Fixes #1259 --- lib/l10n/app_en.arb | 3 +- lib/l10n/l10n.dart | 18 +- lib/l10n/l10n_af.dart | 9 +- lib/l10n/l10n_ar.dart | 9 +- lib/l10n/l10n_az.dart | 9 +- lib/l10n/l10n_be.dart | 11 +- lib/l10n/l10n_bg.dart | 27 +- lib/l10n/l10n_bn.dart | 9 +- lib/l10n/l10n_br.dart | 15 +- lib/l10n/l10n_bs.dart | 9 +- lib/l10n/l10n_ca.dart | 33 +-- lib/l10n/l10n_cs.dart | 43 ++- lib/l10n/l10n_da.dart | 35 ++- lib/l10n/l10n_de.dart | 35 ++- lib/l10n/l10n_el.dart | 49 ++-- lib/l10n/l10n_en.dart | 135 ++++++++- lib/l10n/l10n_eo.dart | 9 +- lib/l10n/l10n_es.dart | 33 +-- lib/l10n/l10n_et.dart | 9 +- lib/l10n/l10n_eu.dart | 77 +++-- lib/l10n/l10n_fa.dart | 399 +++++++++++++------------- lib/l10n/l10n_fi.dart | 39 ++- lib/l10n/l10n_fo.dart | 9 +- lib/l10n/l10n_fr.dart | 35 ++- lib/l10n/l10n_ga.dart | 9 +- lib/l10n/l10n_gl.dart | 39 ++- lib/l10n/l10n_gsw.dart | 33 +-- lib/l10n/l10n_he.dart | 35 ++- lib/l10n/l10n_hi.dart | 9 +- lib/l10n/l10n_hr.dart | 39 ++- lib/l10n/l10n_hu.dart | 9 +- lib/l10n/l10n_hy.dart | 9 +- lib/l10n/l10n_id.dart | 43 ++- lib/l10n/l10n_it.dart | 77 +++-- lib/l10n/l10n_ja.dart | 31 +- lib/l10n/l10n_kk.dart | 9 +- lib/l10n/l10n_ko.dart | 199 +++++++------ lib/l10n/l10n_lb.dart | 27 +- lib/l10n/l10n_lt.dart | 79 +++-- lib/l10n/l10n_lv.dart | 9 +- lib/l10n/l10n_mk.dart | 21 +- lib/l10n/l10n_nb.dart | 33 +-- lib/l10n/l10n_nl.dart | 35 ++- lib/l10n/l10n_nn.dart | 37 ++- lib/l10n/l10n_pl.dart | 37 ++- lib/l10n/l10n_pt.dart | 97 +++++-- lib/l10n/l10n_ro.dart | 25 +- lib/l10n/l10n_ru.dart | 41 ++- lib/l10n/l10n_sk.dart | 35 ++- lib/l10n/l10n_sl.dart | 33 +-- lib/l10n/l10n_sq.dart | 37 ++- lib/l10n/l10n_sr.dart | 15 +- lib/l10n/l10n_sv.dart | 13 +- lib/l10n/l10n_tr.dart | 35 ++- lib/l10n/l10n_uk.dart | 63 ++-- lib/l10n/l10n_vi.dart | 33 +-- lib/l10n/l10n_zh.dart | 60 +++- lib/l10n/lila_af.arb | 59 ++-- lib/l10n/lila_ar.arb | 69 +++-- lib/l10n/lila_az.arb | 2 - lib/l10n/lila_be.arb | 18 +- lib/l10n/lila_bg.arb | 51 ++-- lib/l10n/lila_bn.arb | 2 - lib/l10n/lila_br.arb | 5 +- lib/l10n/lila_bs.arb | 2 - lib/l10n/lila_ca.arb | 79 ++--- lib/l10n/lila_cs.arb | 60 ++-- lib/l10n/lila_da.arb | 83 +++--- lib/l10n/lila_de.arb | 83 +++--- lib/l10n/lila_el.arb | 87 +++--- lib/l10n/lila_en_US.arb | 105 ++++--- lib/l10n/lila_eo.arb | 28 +- lib/l10n/lila_es.arb | 81 +++--- lib/l10n/lila_et.arb | 2 - lib/l10n/lila_eu.arb | 104 ++++--- lib/l10n/lila_fa.arb | 411 ++++++++++++++------------- lib/l10n/lila_fi.arb | 83 +++--- lib/l10n/lila_fo.arb | 2 - lib/l10n/lila_fr.arb | 83 +++--- lib/l10n/lila_ga.arb | 2 - lib/l10n/lila_gl.arb | 85 +++--- lib/l10n/lila_gsw.arb | 81 +++--- lib/l10n/lila_he.arb | 83 +++--- lib/l10n/lila_hi.arb | 30 +- lib/l10n/lila_hr.arb | 21 +- lib/l10n/lila_hu.arb | 67 +++-- lib/l10n/lila_hy.arb | 2 - lib/l10n/lila_id.arb | 19 +- lib/l10n/lila_it.arb | 104 ++++--- lib/l10n/lila_ja.arb | 80 +++--- lib/l10n/lila_kk.arb | 44 ++- lib/l10n/lila_ko.arb | 246 ++++++++-------- lib/l10n/lila_lb.arb | 48 ++-- lib/l10n/lila_lt.arb | 38 ++- lib/l10n/lila_lv.arb | 2 - lib/l10n/lila_mk.arb | 12 +- lib/l10n/lila_nb.arb | 82 +++--- lib/l10n/lila_nl.arb | 82 +++--- lib/l10n/lila_nn.arb | 84 +++--- lib/l10n/lila_pl.arb | 85 +++--- lib/l10n/lila_pt.arb | 83 +++--- lib/l10n/lila_pt_BR.arb | 101 ++++--- lib/l10n/lila_ro.arb | 79 ++--- lib/l10n/lila_ru.arb | 86 +++--- lib/l10n/lila_sk.arb | 82 +++--- lib/l10n/lila_sl.arb | 38 ++- lib/l10n/lila_sq.arb | 80 +++--- lib/l10n/lila_sr.arb | 5 +- lib/l10n/lila_sv.arb | 41 +-- lib/l10n/lila_tr.arb | 82 +++--- lib/l10n/lila_uk.arb | 97 ++++--- lib/l10n/lila_vi.arb | 81 +++--- lib/l10n/lila_zh.arb | 72 ++--- lib/l10n/lila_zh_TW.arb | 80 +++--- lib/src/view/game/game_settings.dart | 2 +- translation/source/mobile.xml | 1 - 116 files changed, 3358 insertions(+), 2813 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index a05a7065c3..5e36f84c81 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1,7 +1,6 @@ { "mobileAllGames": "All games", "mobileAreYouSure": "Are you sure?", - "mobileBlindfoldMode": "Blindfold", "mobileCancelTakebackOffer": "Cancel takeback offer", "mobileClearButton": "Clear", "mobileCorrespondenceClearSavedMove": "Clear saved move", @@ -365,7 +364,6 @@ "broadcastTimezone": "Time zone", "broadcastFideRatingCategory": "FIDE rating category", "broadcastOptionalDetails": "Optional details", - "broadcastUpcomingBroadcasts": "Upcoming broadcasts", "broadcastPastBroadcasts": "Past broadcasts", "broadcastAllBroadcastsByMonth": "View all broadcasts by month", "broadcastNbBroadcasts": "{count, plural, =1{{count} broadcast} other{{count} broadcasts}}", @@ -616,6 +614,7 @@ "preferencesNotifyWeb": "Browser", "preferencesNotifyDevice": "Device", "preferencesBellNotificationSound": "Bell notification sound", + "preferencesBlindfold": "Blindfold", "puzzlePuzzles": "Puzzles", "puzzlePuzzleThemes": "Puzzle Themes", "puzzleRecommended": "Recommended", diff --git a/lib/l10n/l10n.dart b/lib/l10n/l10n.dart index 9191e5ece3..1fd742dfc6 100644 --- a/lib/l10n/l10n.dart +++ b/lib/l10n/l10n.dart @@ -216,12 +216,6 @@ abstract class AppLocalizations { /// **'Are you sure?'** String get mobileAreYouSure; - /// No description provided for @mobileBlindfoldMode. - /// - /// In en, this message translates to: - /// **'Blindfold'** - String get mobileBlindfoldMode; - /// No description provided for @mobileCancelTakebackOffer. /// /// In en, this message translates to: @@ -1044,12 +1038,6 @@ abstract class AppLocalizations { /// **'Optional details'** String get broadcastOptionalDetails; - /// No description provided for @broadcastUpcomingBroadcasts. - /// - /// In en, this message translates to: - /// **'Upcoming broadcasts'** - String get broadcastUpcomingBroadcasts; - /// No description provided for @broadcastPastBroadcasts. /// /// In en, this message translates to: @@ -1806,6 +1794,12 @@ abstract class AppLocalizations { /// **'Bell notification sound'** String get preferencesBellNotificationSound; + /// No description provided for @preferencesBlindfold. + /// + /// In en, this message translates to: + /// **'Blindfold'** + String get preferencesBlindfold; + /// No description provided for @puzzlePuzzles. /// /// In en, this message translates to: diff --git a/lib/l10n/l10n_af.dart b/lib/l10n/l10n_af.dart index c979ea10f9..cf973a2915 100644 --- a/lib/l10n/l10n_af.dart +++ b/lib/l10n/l10n_af.dart @@ -14,9 +14,6 @@ class AppLocalizationsAf extends AppLocalizations { @override String get mobileAreYouSure => 'Is jy seker?'; - @override - String get mobileBlindfoldMode => 'Geblinddoek'; - @override String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @@ -592,9 +589,6 @@ class AppLocalizationsAf extends AppLocalizations { @override String get broadcastOptionalDetails => 'Optional details'; - @override - String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; - @override String get broadcastPastBroadcasts => 'Past broadcasts'; @@ -1011,6 +1005,9 @@ class AppLocalizationsAf extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Klokkie kennisgewing klank'; + @override + String get preferencesBlindfold => 'Blinddoek'; + @override String get puzzlePuzzles => 'Raaisels'; diff --git a/lib/l10n/l10n_ar.dart b/lib/l10n/l10n_ar.dart index 04132f59bc..282165317c 100644 --- a/lib/l10n/l10n_ar.dart +++ b/lib/l10n/l10n_ar.dart @@ -14,9 +14,6 @@ class AppLocalizationsAr extends AppLocalizations { @override String get mobileAreYouSure => 'هل أنت واثق؟'; - @override - String get mobileBlindfoldMode => 'معصوب العينين'; - @override String get mobileCancelTakebackOffer => 'إلغاء عرض الاسترداد'; @@ -660,9 +657,6 @@ class AppLocalizationsAr extends AppLocalizations { @override String get broadcastOptionalDetails => 'Optional details'; - @override - String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; - @override String get broadcastPastBroadcasts => 'Past broadcasts'; @@ -1083,6 +1077,9 @@ class AppLocalizationsAr extends AppLocalizations { @override String get preferencesBellNotificationSound => 'صوت التنبيه'; + @override + String get preferencesBlindfold => 'معصوب العينين'; + @override String get puzzlePuzzles => 'الألغاز'; diff --git a/lib/l10n/l10n_az.dart b/lib/l10n/l10n_az.dart index 36611cb1d5..c3ae3a2155 100644 --- a/lib/l10n/l10n_az.dart +++ b/lib/l10n/l10n_az.dart @@ -14,9 +14,6 @@ class AppLocalizationsAz extends AppLocalizations { @override String get mobileAreYouSure => 'Are you sure?'; - @override - String get mobileBlindfoldMode => 'Blindfold'; - @override String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @@ -592,9 +589,6 @@ class AppLocalizationsAz extends AppLocalizations { @override String get broadcastOptionalDetails => 'Optional details'; - @override - String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; - @override String get broadcastPastBroadcasts => 'Past broadcasts'; @@ -1011,6 +1005,9 @@ class AppLocalizationsAz extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Bell notification sound'; + @override + String get preferencesBlindfold => 'Blindfold'; + @override String get puzzlePuzzles => 'Tapmacalar'; diff --git a/lib/l10n/l10n_be.dart b/lib/l10n/l10n_be.dart index 714b4b2b01..17898397ce 100644 --- a/lib/l10n/l10n_be.dart +++ b/lib/l10n/l10n_be.dart @@ -14,9 +14,6 @@ class AppLocalizationsBe extends AppLocalizations { @override String get mobileAreYouSure => 'Вы ўпэўнены?'; - @override - String get mobileBlindfoldMode => 'Blindfold'; - @override String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @@ -626,9 +623,6 @@ class AppLocalizationsBe extends AppLocalizations { @override String get broadcastOptionalDetails => 'Optional details'; - @override - String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; - @override String get broadcastPastBroadcasts => 'Past broadcasts'; @@ -1045,6 +1039,9 @@ class AppLocalizationsBe extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Гукавое паведамленне'; + @override + String get preferencesBlindfold => 'Blindfold'; + @override String get puzzlePuzzles => 'Задачы'; @@ -1908,7 +1905,7 @@ class AppLocalizationsBe extends AppLocalizations { String get usingServerAnalysis => 'Выкарыстоўваецца серверны аналіз'; @override - String get loadingEngine => 'Загружаем шахматную праграму...'; + String get loadingEngine => 'Загружаем рухавічок...'; @override String get calculatingMoves => 'Пралічваем хады...'; diff --git a/lib/l10n/l10n_bg.dart b/lib/l10n/l10n_bg.dart index 7c82498b7b..3b5edf1dcc 100644 --- a/lib/l10n/l10n_bg.dart +++ b/lib/l10n/l10n_bg.dart @@ -14,9 +14,6 @@ class AppLocalizationsBg extends AppLocalizations { @override String get mobileAreYouSure => 'Сигурни ли сте?'; - @override - String get mobileBlindfoldMode => 'Blindfold'; - @override String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @@ -496,16 +493,16 @@ class AppLocalizationsBg extends AppLocalizations { String get broadcastRecentTournaments => 'Recent tournaments'; @override - String get broadcastOpenLichess => 'Open in Lichess'; + String get broadcastOpenLichess => 'Отвори в Lichess'; @override - String get broadcastTeams => 'Teams'; + String get broadcastTeams => 'Отбори'; @override - String get broadcastBoards => 'Boards'; + String get broadcastBoards => 'Дъски'; @override - String get broadcastOverview => 'Overview'; + String get broadcastOverview => 'Общ преглед'; @override String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; @@ -533,10 +530,10 @@ class AppLocalizationsBg extends AppLocalizations { String get broadcastNotYetStarted => 'The broadcast has not yet started.'; @override - String get broadcastOfficialWebsite => 'Official website'; + String get broadcastOfficialWebsite => 'Официален уебсайт'; @override - String get broadcastStandings => 'Standings'; + String get broadcastStandings => 'Класиране'; @override String get broadcastOfficialStandings => 'Official Standings'; @@ -566,10 +563,10 @@ class AppLocalizationsBg extends AppLocalizations { String get broadcastRatingDiff => 'Rating diff'; @override - String get broadcastGamesThisTournament => 'Games in this tournament'; + String get broadcastGamesThisTournament => 'Игри в този турнир'; @override - String get broadcastScore => 'Score'; + String get broadcastScore => 'Резултат'; @override String get broadcastAllTeams => 'All teams'; @@ -592,9 +589,6 @@ class AppLocalizationsBg extends AppLocalizations { @override String get broadcastOptionalDetails => 'Optional details'; - @override - String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; - @override String get broadcastPastBroadcasts => 'Past broadcasts'; @@ -1011,6 +1005,9 @@ class AppLocalizationsBg extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Мелодия за известия'; + @override + String get preferencesBlindfold => 'Blindfold'; + @override String get puzzlePuzzles => 'Задачи'; @@ -4339,7 +4336,7 @@ class AppLocalizationsBg extends AppLocalizations { String get nothingToSeeHere => 'Nothing to see here at the moment.'; @override - String get stats => 'Stats'; + String get stats => 'Статистика'; @override String opponentLeftCounter(int count) { diff --git a/lib/l10n/l10n_bn.dart b/lib/l10n/l10n_bn.dart index 45536f57d8..f86d89cfb1 100644 --- a/lib/l10n/l10n_bn.dart +++ b/lib/l10n/l10n_bn.dart @@ -14,9 +14,6 @@ class AppLocalizationsBn extends AppLocalizations { @override String get mobileAreYouSure => 'Are you sure?'; - @override - String get mobileBlindfoldMode => 'Blindfold'; - @override String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @@ -592,9 +589,6 @@ class AppLocalizationsBn extends AppLocalizations { @override String get broadcastOptionalDetails => 'Optional details'; - @override - String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; - @override String get broadcastPastBroadcasts => 'Past broadcasts'; @@ -1011,6 +1005,9 @@ class AppLocalizationsBn extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Bell notification sound'; + @override + String get preferencesBlindfold => 'Blindfold'; + @override String get puzzlePuzzles => 'পাজল'; diff --git a/lib/l10n/l10n_br.dart b/lib/l10n/l10n_br.dart index 4695b5d6bf..6ea61db6fd 100644 --- a/lib/l10n/l10n_br.dart +++ b/lib/l10n/l10n_br.dart @@ -14,9 +14,6 @@ class AppLocalizationsBr extends AppLocalizations { @override String get mobileAreYouSure => 'Are you sure?'; - @override - String get mobileBlindfoldMode => 'Blindfold'; - @override String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @@ -643,9 +640,6 @@ class AppLocalizationsBr extends AppLocalizations { @override String get broadcastOptionalDetails => 'Optional details'; - @override - String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; - @override String get broadcastPastBroadcasts => 'Past broadcasts'; @@ -1062,6 +1056,9 @@ class AppLocalizationsBr extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Klevet ar c\'hloc\'h'; + @override + String get preferencesBlindfold => 'Blindfold'; + @override String get puzzlePuzzles => 'Poelladennoù'; @@ -5528,16 +5525,16 @@ class AppLocalizationsBr extends AppLocalizations { String get studyWhereDoYouWantToStudyThat => 'Pelec\'h ho peus c\'hoant da studiañ se?'; @override - String get studyGoodMove => 'Good move'; + String get studyGoodMove => 'Fiñvadenn vat'; @override - String get studyMistake => 'Mistake'; + String get studyMistake => 'Fazi'; @override String get studyBrilliantMove => 'Brilliant move'; @override - String get studyBlunder => 'Blunder'; + String get studyBlunder => 'Bourd'; @override String get studyInterestingMove => 'Interesting move'; diff --git a/lib/l10n/l10n_bs.dart b/lib/l10n/l10n_bs.dart index f9389f1eb9..8eb0a68c0a 100644 --- a/lib/l10n/l10n_bs.dart +++ b/lib/l10n/l10n_bs.dart @@ -14,9 +14,6 @@ class AppLocalizationsBs extends AppLocalizations { @override String get mobileAreYouSure => 'Are you sure?'; - @override - String get mobileBlindfoldMode => 'Blindfold'; - @override String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @@ -609,9 +606,6 @@ class AppLocalizationsBs extends AppLocalizations { @override String get broadcastOptionalDetails => 'Optional details'; - @override - String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; - @override String get broadcastPastBroadcasts => 'Past broadcasts'; @@ -1029,6 +1023,9 @@ class AppLocalizationsBs extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Zvuk obavještenja'; + @override + String get preferencesBlindfold => 'Blindfold'; + @override String get puzzlePuzzles => 'Problemi'; diff --git a/lib/l10n/l10n_ca.dart b/lib/l10n/l10n_ca.dart index 70ee7859a5..4959fce700 100644 --- a/lib/l10n/l10n_ca.dart +++ b/lib/l10n/l10n_ca.dart @@ -14,9 +14,6 @@ class AppLocalizationsCa extends AppLocalizations { @override String get mobileAreYouSure => 'Estàs segur?'; - @override - String get mobileBlindfoldMode => 'A la cega'; - @override String get mobileCancelTakebackOffer => 'Anul·la la petició per desfer la jugada'; @@ -133,7 +130,7 @@ class AppLocalizationsCa extends AppLocalizations { String get mobileSystemColors => 'Colors del sistema'; @override - String get mobileTheme => 'Theme'; + String get mobileTheme => 'Tema'; @override String get mobileToolsTab => 'Eines'; @@ -539,7 +536,7 @@ class AppLocalizationsCa extends AppLocalizations { String get broadcastStandings => 'Classificació'; @override - String get broadcastOfficialStandings => 'Official Standings'; + String get broadcastOfficialStandings => 'Classificació oficial'; @override String broadcastIframeHelp(String param) { @@ -572,34 +569,31 @@ class AppLocalizationsCa extends AppLocalizations { String get broadcastScore => 'Puntuació'; @override - String get broadcastAllTeams => 'All teams'; - - @override - String get broadcastTournamentFormat => 'Tournament format'; + String get broadcastAllTeams => 'Tots els equips'; @override - String get broadcastTournamentLocation => 'Tournament Location'; + String get broadcastTournamentFormat => 'Format del torneig'; @override - String get broadcastTopPlayers => 'Top players'; + String get broadcastTournamentLocation => 'Ubicació del torneig'; @override - String get broadcastTimezone => 'Time zone'; + String get broadcastTopPlayers => 'Millors jugadors'; @override - String get broadcastFideRatingCategory => 'FIDE rating category'; + String get broadcastTimezone => 'Zona horària'; @override - String get broadcastOptionalDetails => 'Optional details'; + String get broadcastFideRatingCategory => 'Categoria puntuació FIDE'; @override - String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + String get broadcastOptionalDetails => 'Detalls opcionals'; @override - String get broadcastPastBroadcasts => 'Past broadcasts'; + String get broadcastPastBroadcasts => 'Retransmissions finalitzades'; @override - String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + String get broadcastAllBroadcastsByMonth => 'Veure totes les retransmissions per més'; @override String broadcastNbBroadcasts(int count) { @@ -1011,6 +1005,9 @@ class AppLocalizationsCa extends AppLocalizations { @override String get preferencesBellNotificationSound => 'So de notificació'; + @override + String get preferencesBlindfold => 'A la cega'; + @override String get puzzlePuzzles => 'Problemes'; @@ -5456,7 +5453,7 @@ class AppLocalizationsCa extends AppLocalizations { @override String studyPerPage(String param) { - return '$param per page'; + return '$param per pàgina'; } @override diff --git a/lib/l10n/l10n_cs.dart b/lib/l10n/l10n_cs.dart index 4e48b92e77..953aa04b93 100644 --- a/lib/l10n/l10n_cs.dart +++ b/lib/l10n/l10n_cs.dart @@ -14,9 +14,6 @@ class AppLocalizationsCs extends AppLocalizations { @override String get mobileAreYouSure => 'Jste si jistý?'; - @override - String get mobileBlindfoldMode => 'Páska přes oči'; - @override String get mobileCancelTakebackOffer => 'Zrušit nabídnutí vrácení tahu'; @@ -85,7 +82,7 @@ class AppLocalizationsCs extends AppLocalizations { String get mobilePuzzleStreakAbortWarning => 'Ztratíte aktuální sérii a vaše skóre bude uloženo.'; @override - String get mobilePuzzleThemesSubtitle => 'Hrej úlohy z tvých oblíbených zahájení, nebo si vyber styl.'; + String get mobilePuzzleThemesSubtitle => 'Hraj úlohy z tvých oblíbených zahájení, nebo si vyber styl.'; @override String get mobilePuzzlesTab => 'Puzzles'; @@ -532,25 +529,25 @@ class AppLocalizationsCs extends AppLocalizations { String get broadcastRecentTournaments => 'Nedávné tournamenty'; @override - String get broadcastOpenLichess => 'Open in Lichess'; + String get broadcastOpenLichess => 'Otevřít v Lichess'; @override - String get broadcastTeams => 'Teams'; + String get broadcastTeams => 'Týmy'; @override - String get broadcastBoards => 'Boards'; + String get broadcastBoards => 'Šachovnice'; @override - String get broadcastOverview => 'Overview'; + String get broadcastOverview => 'Přehled'; @override String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; @override - String get broadcastUploadImage => 'Upload tournament image'; + String get broadcastUploadImage => 'Nahrát obrázek turnaje'; @override - String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + String get broadcastNoBoardsYet => 'Zatím žádné šachovnice. Ty se zobrazí se po nahrání partií.'; @override String broadcastBoardsCanBeLoaded(String param) { @@ -559,27 +556,27 @@ class AppLocalizationsCs extends AppLocalizations { @override String broadcastStartsAfter(String param) { - return 'Starts after $param'; + return 'Začíná po $param'; } @override - String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + String get broadcastStartVerySoon => 'Vysílání začne velmi brzy.'; @override - String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + String get broadcastNotYetStarted => 'Vysílání ještě nezačalo.'; @override - String get broadcastOfficialWebsite => 'Official website'; + String get broadcastOfficialWebsite => 'Oficiální stránka'; @override - String get broadcastStandings => 'Standings'; + String get broadcastStandings => 'Pořadí'; @override - String get broadcastOfficialStandings => 'Official Standings'; + String get broadcastOfficialStandings => 'Oficiální pořadí'; @override String broadcastIframeHelp(String param) { - return 'More options on the $param'; + return 'Více možností na $param'; } @override @@ -605,7 +602,7 @@ class AppLocalizationsCs extends AppLocalizations { String get broadcastGamesThisTournament => 'Games in this tournament'; @override - String get broadcastScore => 'Score'; + String get broadcastScore => 'Skóre'; @override String get broadcastAllTeams => 'All teams'; @@ -628,9 +625,6 @@ class AppLocalizationsCs extends AppLocalizations { @override String get broadcastOptionalDetails => 'Optional details'; - @override - String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; - @override String get broadcastPastBroadcasts => 'Past broadcasts'; @@ -1049,6 +1043,9 @@ class AppLocalizationsCs extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Typ zvukového upozornění'; + @override + String get preferencesBlindfold => 'Páska přes oči'; + @override String get puzzlePuzzles => 'Úlohy'; @@ -4387,7 +4384,7 @@ class AppLocalizationsCs extends AppLocalizations { String get nothingToSeeHere => 'Momentálně zde není nic k vidění.'; @override - String get stats => 'Stats'; + String get stats => 'Statistiky'; @override String opponentLeftCounter(int count) { @@ -5592,7 +5589,7 @@ class AppLocalizationsCs extends AppLocalizations { @override String studyPerPage(String param) { - return '$param per page'; + return '$param na stránku'; } @override diff --git a/lib/l10n/l10n_da.dart b/lib/l10n/l10n_da.dart index 5e54eb1cd8..4c1a01e34f 100644 --- a/lib/l10n/l10n_da.dart +++ b/lib/l10n/l10n_da.dart @@ -14,9 +14,6 @@ class AppLocalizationsDa extends AppLocalizations { @override String get mobileAreYouSure => 'Er du sikker?'; - @override - String get mobileBlindfoldMode => 'Bind for øjnene'; - @override String get mobileCancelTakebackOffer => 'Annuller tilbud om tilbagetagelse'; @@ -133,7 +130,7 @@ class AppLocalizationsDa extends AppLocalizations { String get mobileSystemColors => 'Systemfarver'; @override - String get mobileTheme => 'Theme'; + String get mobileTheme => 'Tema'; @override String get mobileToolsTab => 'Værktøjer'; @@ -539,7 +536,7 @@ class AppLocalizationsDa extends AppLocalizations { String get broadcastStandings => 'Stillinger'; @override - String get broadcastOfficialStandings => 'Official Standings'; + String get broadcastOfficialStandings => 'Officiel stilling'; @override String broadcastIframeHelp(String param) { @@ -572,34 +569,31 @@ class AppLocalizationsDa extends AppLocalizations { String get broadcastScore => 'Score'; @override - String get broadcastAllTeams => 'All teams'; - - @override - String get broadcastTournamentFormat => 'Tournament format'; + String get broadcastAllTeams => 'Alle hold'; @override - String get broadcastTournamentLocation => 'Tournament Location'; + String get broadcastTournamentFormat => 'Turneringsformat'; @override - String get broadcastTopPlayers => 'Top players'; + String get broadcastTournamentLocation => 'Turneringssted'; @override - String get broadcastTimezone => 'Time zone'; + String get broadcastTopPlayers => 'Topspillere'; @override - String get broadcastFideRatingCategory => 'FIDE rating category'; + String get broadcastTimezone => 'Tidszone'; @override - String get broadcastOptionalDetails => 'Optional details'; + String get broadcastFideRatingCategory => 'FIDE-ratingkategori'; @override - String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + String get broadcastOptionalDetails => 'Valgfri detaljer'; @override - String get broadcastPastBroadcasts => 'Past broadcasts'; + String get broadcastPastBroadcasts => 'Tidligere udsendelser'; @override - String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + String get broadcastAllBroadcastsByMonth => 'Vis alle udsendelser efter måned'; @override String broadcastNbBroadcasts(int count) { @@ -1011,6 +1005,9 @@ class AppLocalizationsDa extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Notifikationslyd'; + @override + String get preferencesBlindfold => 'Blindskak'; + @override String get puzzlePuzzles => 'Taktikopgaver'; @@ -1847,7 +1844,7 @@ class AppLocalizationsDa extends AppLocalizations { String get computerAnalysis => 'Computeranalyse'; @override - String get computerAnalysisAvailable => 'Computeranalyse klar'; + String get computerAnalysisAvailable => 'Computeranalyse tilgængelig'; @override String get computerAnalysisDisabled => 'Computeranalyse deaktiveret'; @@ -5456,7 +5453,7 @@ class AppLocalizationsDa extends AppLocalizations { @override String studyPerPage(String param) { - return '$param per page'; + return '$param pr. side'; } @override diff --git a/lib/l10n/l10n_de.dart b/lib/l10n/l10n_de.dart index d6e7b6a1d7..dc6551fb6d 100644 --- a/lib/l10n/l10n_de.dart +++ b/lib/l10n/l10n_de.dart @@ -14,9 +14,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get mobileAreYouSure => 'Bist du sicher?'; - @override - String get mobileBlindfoldMode => 'Blind spielen'; - @override String get mobileCancelTakebackOffer => 'Zugzurücknahme-Angebot abbrechen'; @@ -133,7 +130,7 @@ class AppLocalizationsDe extends AppLocalizations { String get mobileSystemColors => 'Systemfarben'; @override - String get mobileTheme => 'Theme'; + String get mobileTheme => 'Erscheinungsbild'; @override String get mobileToolsTab => 'Werkzeuge'; @@ -539,7 +536,7 @@ class AppLocalizationsDe extends AppLocalizations { String get broadcastStandings => 'Rangliste'; @override - String get broadcastOfficialStandings => 'Official Standings'; + String get broadcastOfficialStandings => 'Offizielle Rangliste'; @override String broadcastIframeHelp(String param) { @@ -572,34 +569,31 @@ class AppLocalizationsDe extends AppLocalizations { String get broadcastScore => 'Punktestand'; @override - String get broadcastAllTeams => 'All teams'; - - @override - String get broadcastTournamentFormat => 'Tournament format'; + String get broadcastAllTeams => 'Alle Teams'; @override - String get broadcastTournamentLocation => 'Tournament Location'; + String get broadcastTournamentFormat => 'Turnierformat'; @override - String get broadcastTopPlayers => 'Top players'; + String get broadcastTournamentLocation => 'Turnierort'; @override - String get broadcastTimezone => 'Time zone'; + String get broadcastTopPlayers => 'Spitzenspieler'; @override - String get broadcastFideRatingCategory => 'FIDE rating category'; + String get broadcastTimezone => 'Zeitzone'; @override - String get broadcastOptionalDetails => 'Optional details'; + String get broadcastFideRatingCategory => 'FIDE-Wertungskategorie'; @override - String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + String get broadcastOptionalDetails => 'Optionale Details'; @override - String get broadcastPastBroadcasts => 'Past broadcasts'; + String get broadcastPastBroadcasts => 'Vergangene Übertragungen'; @override - String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + String get broadcastAllBroadcastsByMonth => 'Alle Übertragungen nach Monat anzeigen'; @override String broadcastNbBroadcasts(int count) { @@ -1011,6 +1005,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Glocken-Benachrichtigungston'; + @override + String get preferencesBlindfold => 'Blindschach'; + @override String get puzzlePuzzles => 'Taktikaufgaben'; @@ -1654,7 +1651,7 @@ class AppLocalizationsDe extends AppLocalizations { String get puzzleThemeMix => 'Gesunder Mix'; @override - String get puzzleThemeMixDescription => 'Ein bisschen von Allem. Du weißt nicht, was dich erwartet, deshalb bleibst du auf alles vorbereitet! Genau wie in echten Partien.'; + String get puzzleThemeMixDescription => 'Ein bisschen von allem. Du weißt nicht, was dich erwartet, deshalb bleibst du bereit für alles! Genau wie in echten Partien.'; @override String get puzzleThemePlayerGames => 'Partien von Spielern'; @@ -5456,7 +5453,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String studyPerPage(String param) { - return '$param per page'; + return '$param pro Seite'; } @override diff --git a/lib/l10n/l10n_el.dart b/lib/l10n/l10n_el.dart index f78ae05357..b95720f1dc 100644 --- a/lib/l10n/l10n_el.dart +++ b/lib/l10n/l10n_el.dart @@ -14,9 +14,6 @@ class AppLocalizationsEl extends AppLocalizations { @override String get mobileAreYouSure => 'Είστε σίγουροι;'; - @override - String get mobileBlindfoldMode => 'Τυφλό'; - @override String get mobileCancelTakebackOffer => 'Ακυρώστε την προσφορά αναίρεσης της κίνησης'; @@ -97,7 +94,7 @@ class AppLocalizationsEl extends AppLocalizations { String get mobileSettingsHapticFeedback => 'Απόκριση δόνησης'; @override - String get mobileSettingsImmersiveMode => 'Immersive mode'; + String get mobileSettingsImmersiveMode => 'Λειτουργία εστίασης'; @override String get mobileSettingsImmersiveModeSubtitle => 'Αποκρύπτει τη διεπαφή του συστήματος όσο παίζεται. Ενεργοποιήστε εάν σας ενοχλούν οι χειρονομίες πλοήγησης του συστήματος στα άκρα της οθόνης. Ισχύει για την προβολή παιχνιδιού και το Puzzle Storm.'; @@ -133,7 +130,7 @@ class AppLocalizationsEl extends AppLocalizations { String get mobileSystemColors => 'Χρώματα συστήματος'; @override - String get mobileTheme => 'Theme'; + String get mobileTheme => 'Εμφάνιση'; @override String get mobileToolsTab => 'Εργαλεία'; @@ -366,7 +363,7 @@ class AppLocalizationsEl extends AppLocalizations { String get broadcastLiveBroadcasts => 'Αναμεταδόσεις ζωντανών τουρνούα'; @override - String get broadcastBroadcastCalendar => 'Broadcast calendar'; + String get broadcastBroadcastCalendar => 'Ημερολόγιο αναμεταδόσεων'; @override String get broadcastNewBroadcast => 'Νέα ζωντανή αναμετάδοση'; @@ -419,7 +416,7 @@ class AppLocalizationsEl extends AppLocalizations { } @override - String get broadcastSourceSingleUrl => 'PGN Source URL'; + String get broadcastSourceSingleUrl => 'Πηγαίο URL για PGN'; @override String get broadcastSourceUrlHelp => 'URL για λήψη PGN ενημερώσεων. Πρέπει να είναι δημόσια προσβάσιμο μέσω διαδικτύου.'; @@ -490,7 +487,7 @@ class AppLocalizationsEl extends AppLocalizations { String get broadcastAgeThisYear => 'Φετινή ηλικία'; @override - String get broadcastUnrated => 'Unrated'; + String get broadcastUnrated => 'Μη βαθμολογημένο'; @override String get broadcastRecentTournaments => 'Πρόσφατα τουρνουά'; @@ -518,7 +515,7 @@ class AppLocalizationsEl extends AppLocalizations { @override String broadcastBoardsCanBeLoaded(String param) { - return 'Boards can be loaded with a source or via the $param'; + return 'Οι σκακιέρες μπορούν να φορτωθούν απο μια πηγή ή μέσω του $param'; } @override @@ -527,10 +524,10 @@ class AppLocalizationsEl extends AppLocalizations { } @override - String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + String get broadcastStartVerySoon => 'Η αναμετάδοση θα ξεκινήσει πολύ σύντομα.'; @override - String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + String get broadcastNotYetStarted => 'Η αναμετάδοση δεν έχει ξεκινήσει ακόμα.'; @override String get broadcastOfficialWebsite => 'Επίσημη ιστοσελίδα'; @@ -539,27 +536,27 @@ class AppLocalizationsEl extends AppLocalizations { String get broadcastStandings => 'Κατάταξη'; @override - String get broadcastOfficialStandings => 'Official Standings'; + String get broadcastOfficialStandings => 'Επίσημη Κατάταξη'; @override String broadcastIframeHelp(String param) { - return 'More options on the $param'; + return 'Περισσότερες επιλογές στη $param'; } @override - String get broadcastWebmastersPage => 'webmasters page'; + String get broadcastWebmastersPage => 'σελίδα για webmasters'; @override String broadcastPgnSourceHelp(String param) { - return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + return 'Μια δημόσια πηγή PGN πολύ λειτουργεί σε πραγματικό χρόνο για αυτόν τον γύρο. Προσφέρουμε επίσης το $param για γρηγορότερο και αποτελεσματικότερο συγχρονισμό.'; } @override - String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + String get broadcastEmbedThisBroadcast => 'Ενσωμάτωση αυτήν την αναμετάδοση στην ιστοσελίδα σας'; @override String broadcastEmbedThisRound(String param) { - return 'Embed $param in your website'; + return 'Ενσωματώστε τον $param στην ιστοσελίδα σας'; } @override @@ -572,34 +569,31 @@ class AppLocalizationsEl extends AppLocalizations { String get broadcastScore => 'Βαθμολογία'; @override - String get broadcastAllTeams => 'All teams'; + String get broadcastAllTeams => 'Όλες οι ομάδες'; @override String get broadcastTournamentFormat => 'Tournament format'; @override - String get broadcastTournamentLocation => 'Tournament Location'; + String get broadcastTournamentLocation => 'Τοποθεσία Τουρνουά'; @override String get broadcastTopPlayers => 'Top players'; @override - String get broadcastTimezone => 'Time zone'; + String get broadcastTimezone => 'Ζώνη ώρας'; @override String get broadcastFideRatingCategory => 'FIDE rating category'; @override - String get broadcastOptionalDetails => 'Optional details'; - - @override - String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + String get broadcastOptionalDetails => 'Προαιρετικές λεπτομέρειες'; @override - String get broadcastPastBroadcasts => 'Past broadcasts'; + String get broadcastPastBroadcasts => 'Προηγούμενες αναμετάδοσεις'; @override - String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + String get broadcastAllBroadcastsByMonth => 'Προβολή όλων των αναμεταδόσεων ανά μήνα'; @override String broadcastNbBroadcasts(int count) { @@ -1011,6 +1005,9 @@ class AppLocalizationsEl extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Ειδοποίηση με ήχο από καμπανάκι'; + @override + String get preferencesBlindfold => 'Τυφλό'; + @override String get puzzlePuzzles => 'Γρίφοι'; diff --git a/lib/l10n/l10n_en.dart b/lib/l10n/l10n_en.dart index 303980850d..fe34e1ef9d 100644 --- a/lib/l10n/l10n_en.dart +++ b/lib/l10n/l10n_en.dart @@ -14,9 +14,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get mobileAreYouSure => 'Are you sure?'; - @override - String get mobileBlindfoldMode => 'Blindfold'; - @override String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @@ -592,9 +589,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get broadcastOptionalDetails => 'Optional details'; - @override - String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; - @override String get broadcastPastBroadcasts => 'Past broadcasts'; @@ -1011,6 +1005,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Bell notification sound'; + @override + String get preferencesBlindfold => 'Blindfold'; + @override String get puzzlePuzzles => 'Puzzles'; @@ -5512,9 +5509,6 @@ class AppLocalizationsEnUs extends AppLocalizationsEn { @override String get mobileAreYouSure => 'Are you sure?'; - @override - String get mobileBlindfoldMode => 'Blindfold'; - @override String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @@ -5630,6 +5624,9 @@ class AppLocalizationsEnUs extends AppLocalizationsEn { @override String get mobileSystemColors => 'System colors'; + @override + String get mobileTheme => 'Theme'; + @override String get mobileToolsTab => 'Tools'; @@ -5990,6 +5987,109 @@ class AppLocalizationsEnUs extends AppLocalizationsEn { @override String get broadcastRecentTournaments => 'Recent tournaments'; + @override + String get broadcastOpenLichess => 'Open in Lichess'; + + @override + String get broadcastTeams => 'Teams'; + + @override + String get broadcastBoards => 'Boards'; + + @override + String get broadcastOverview => 'Overview'; + + @override + String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + + @override + String get broadcastUploadImage => 'Upload tournament image'; + + @override + String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + + @override + String broadcastBoardsCanBeLoaded(String param) { + return 'Boards can be loaded with a source or via the $param'; + } + + @override + String broadcastStartsAfter(String param) { + return 'Starts after $param'; + } + + @override + String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + + @override + String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + + @override + String get broadcastOfficialWebsite => 'Official website'; + + @override + String get broadcastStandings => 'Standings'; + + @override + String get broadcastOfficialStandings => 'Official Standings'; + + @override + String broadcastIframeHelp(String param) { + return 'More options on the $param'; + } + + @override + String get broadcastWebmastersPage => 'webmasters page'; + + @override + String broadcastPgnSourceHelp(String param) { + return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronization.'; + } + + @override + String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + + @override + String broadcastEmbedThisRound(String param) { + return 'Embed $param in your website'; + } + + @override + String get broadcastRatingDiff => 'Rating diff'; + + @override + String get broadcastGamesThisTournament => 'Games in this tournament'; + + @override + String get broadcastScore => 'Score'; + + @override + String get broadcastAllTeams => 'All teams'; + + @override + String get broadcastTournamentFormat => 'Tournament format'; + + @override + String get broadcastTournamentLocation => 'Tournament Location'; + + @override + String get broadcastTopPlayers => 'Top players'; + + @override + String get broadcastTimezone => 'Time zone'; + + @override + String get broadcastFideRatingCategory => 'FIDE rating category'; + + @override + String get broadcastOptionalDetails => 'Optional details'; + + @override + String get broadcastPastBroadcasts => 'Past broadcasts'; + + @override + String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + @override String broadcastNbBroadcasts(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -6247,6 +6347,9 @@ class AppLocalizationsEnUs extends AppLocalizationsEn { @override String get preferencesShowFlairs => 'Show player flairs'; + @override + String get preferencesExplainShowPlayerRatings => 'This hides all ratings from Lichess, to help focus on the chess. Rated games still impact your rating, this is only about what you get to see.'; + @override String get preferencesDisplayBoardResizeHandle => 'Show board resize handle'; @@ -6397,6 +6500,9 @@ class AppLocalizationsEnUs extends AppLocalizationsEn { @override String get preferencesBellNotificationSound => 'Bell notification sound'; + @override + String get preferencesBlindfold => 'Blindfold'; + @override String get puzzlePuzzles => 'Puzzles'; @@ -7704,6 +7810,9 @@ class AppLocalizationsEnUs extends AppLocalizationsEn { @override String get gamesPlayed => 'Games played'; + @override + String get ok => 'OK'; + @override String get cancel => 'Cancel'; @@ -9721,6 +9830,9 @@ class AppLocalizationsEnUs extends AppLocalizationsEn { @override String get nothingToSeeHere => 'Nothing to see here at the moment.'; + @override + String get stats => 'Stats'; + @override String opponentLeftCounter(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -10834,6 +10946,11 @@ class AppLocalizationsEnUs extends AppLocalizationsEn { @override String get studyYouCompletedThisLesson => 'Congratulations! You completed this lesson.'; + @override + String studyPerPage(String param) { + return '$param per page'; + } + @override String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( diff --git a/lib/l10n/l10n_eo.dart b/lib/l10n/l10n_eo.dart index 8015bbccc2..d59d32157b 100644 --- a/lib/l10n/l10n_eo.dart +++ b/lib/l10n/l10n_eo.dart @@ -14,9 +14,6 @@ class AppLocalizationsEo extends AppLocalizations { @override String get mobileAreYouSure => 'Ĉu vi certas?'; - @override - String get mobileBlindfoldMode => 'Blindfold'; - @override String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @@ -592,9 +589,6 @@ class AppLocalizationsEo extends AppLocalizations { @override String get broadcastOptionalDetails => 'Optional details'; - @override - String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; - @override String get broadcastPastBroadcasts => 'Past broadcasts'; @@ -1011,6 +1005,9 @@ class AppLocalizationsEo extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Sonorile sciiga sono'; + @override + String get preferencesBlindfold => 'Blindfold'; + @override String get puzzlePuzzles => 'Puzloj'; diff --git a/lib/l10n/l10n_es.dart b/lib/l10n/l10n_es.dart index 38be343310..7f16383c64 100644 --- a/lib/l10n/l10n_es.dart +++ b/lib/l10n/l10n_es.dart @@ -14,9 +14,6 @@ class AppLocalizationsEs extends AppLocalizations { @override String get mobileAreYouSure => '¿Estás seguro?'; - @override - String get mobileBlindfoldMode => 'A ciegas'; - @override String get mobileCancelTakebackOffer => 'Cancelar oferta de deshacer movimiento'; @@ -133,7 +130,7 @@ class AppLocalizationsEs extends AppLocalizations { String get mobileSystemColors => 'Colores del sistema'; @override - String get mobileTheme => 'Theme'; + String get mobileTheme => 'Tema'; @override String get mobileToolsTab => 'Herramientas'; @@ -539,7 +536,7 @@ class AppLocalizationsEs extends AppLocalizations { String get broadcastStandings => 'Clasificación'; @override - String get broadcastOfficialStandings => 'Official Standings'; + String get broadcastOfficialStandings => 'Clasificación oficial'; @override String broadcastIframeHelp(String param) { @@ -572,34 +569,31 @@ class AppLocalizationsEs extends AppLocalizations { String get broadcastScore => 'Resultado'; @override - String get broadcastAllTeams => 'All teams'; - - @override - String get broadcastTournamentFormat => 'Tournament format'; + String get broadcastAllTeams => 'Todos los equipos'; @override - String get broadcastTournamentLocation => 'Tournament Location'; + String get broadcastTournamentFormat => 'Formato del torneo'; @override - String get broadcastTopPlayers => 'Top players'; + String get broadcastTournamentLocation => 'Ubicación del torneo'; @override - String get broadcastTimezone => 'Time zone'; + String get broadcastTopPlayers => 'Mejores jugadores'; @override - String get broadcastFideRatingCategory => 'FIDE rating category'; + String get broadcastTimezone => 'Zona horaria'; @override - String get broadcastOptionalDetails => 'Optional details'; + String get broadcastFideRatingCategory => 'Categoría de calificación de FIDE'; @override - String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + String get broadcastOptionalDetails => 'Detalles opcionales'; @override - String get broadcastPastBroadcasts => 'Past broadcasts'; + String get broadcastPastBroadcasts => 'Transmisiones pasadas'; @override - String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + String get broadcastAllBroadcastsByMonth => 'Ver todas las transmisiones por mes'; @override String broadcastNbBroadcasts(int count) { @@ -1011,6 +1005,9 @@ class AppLocalizationsEs extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Campana de notificación'; + @override + String get preferencesBlindfold => 'A ciegas'; + @override String get puzzlePuzzles => 'Ejercicios'; @@ -5456,7 +5453,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String studyPerPage(String param) { - return '$param per page'; + return '$param por página'; } @override diff --git a/lib/l10n/l10n_et.dart b/lib/l10n/l10n_et.dart index a9da5ffa6c..188bb10f2e 100644 --- a/lib/l10n/l10n_et.dart +++ b/lib/l10n/l10n_et.dart @@ -14,9 +14,6 @@ class AppLocalizationsEt extends AppLocalizations { @override String get mobileAreYouSure => 'Are you sure?'; - @override - String get mobileBlindfoldMode => 'Blindfold'; - @override String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @@ -592,9 +589,6 @@ class AppLocalizationsEt extends AppLocalizations { @override String get broadcastOptionalDetails => 'Optional details'; - @override - String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; - @override String get broadcastPastBroadcasts => 'Past broadcasts'; @@ -1011,6 +1005,9 @@ class AppLocalizationsEt extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Teavituste heli'; + @override + String get preferencesBlindfold => 'Blindfold'; + @override String get puzzlePuzzles => 'Pusled'; diff --git a/lib/l10n/l10n_eu.dart b/lib/l10n/l10n_eu.dart index 8e7aaf5144..baf1b5453b 100644 --- a/lib/l10n/l10n_eu.dart +++ b/lib/l10n/l10n_eu.dart @@ -14,9 +14,6 @@ class AppLocalizationsEu extends AppLocalizations { @override String get mobileAreYouSure => 'Ziur zaude?'; - @override - String get mobileBlindfoldMode => 'Itsuka'; - @override String get mobileCancelTakebackOffer => 'Bertan behera utzi atzera-egite eskaera'; @@ -133,7 +130,7 @@ class AppLocalizationsEu extends AppLocalizations { String get mobileSystemColors => 'Sistemaren koloreak'; @override - String get mobileTheme => 'Theme'; + String get mobileTheme => 'Itxura'; @override String get mobileToolsTab => 'Tresnak'; @@ -496,110 +493,107 @@ class AppLocalizationsEu extends AppLocalizations { String get broadcastRecentTournaments => 'Azken txapelketak'; @override - String get broadcastOpenLichess => 'Open in Lichess'; + String get broadcastOpenLichess => 'Ireki Lichessen'; @override - String get broadcastTeams => 'Teams'; + String get broadcastTeams => 'Taldeak'; @override - String get broadcastBoards => 'Boards'; + String get broadcastBoards => 'Taulak'; @override - String get broadcastOverview => 'Overview'; + String get broadcastOverview => 'Laburpena'; @override - String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + String get broadcastSubscribeTitle => 'Harpidetu txanda bakoitza hastean jakinarazpena jasotzeko. Kanpaia edo push erako notifikazioak zure kontuaren hobespenetan aktibatu ditzakezu.'; @override - String get broadcastUploadImage => 'Upload tournament image'; + String get broadcastUploadImage => 'Kargatu txapelketaren irudia'; @override - String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + String get broadcastNoBoardsYet => 'Taularik ez oraindik. Partidak igotzean agertuko dira.'; @override String broadcastBoardsCanBeLoaded(String param) { - return 'Boards can be loaded with a source or via the $param'; + return 'Taulak iturburu batekin edo ${param}ren bidez kargatu daitezke'; } @override String broadcastStartsAfter(String param) { - return 'Starts after $param'; + return '${param}ren ondoren hasiko da'; } @override - String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + String get broadcastStartVerySoon => 'Zuzenekoa berehala hasiko da.'; @override - String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + String get broadcastNotYetStarted => 'Zuzenekoa ez da oraindik hasi.'; @override - String get broadcastOfficialWebsite => 'Official website'; + String get broadcastOfficialWebsite => 'Webgune ofiziala'; @override - String get broadcastStandings => 'Standings'; + String get broadcastStandings => 'Sailkapena'; @override - String get broadcastOfficialStandings => 'Official Standings'; + String get broadcastOfficialStandings => 'Sailkapen ofiziala'; @override String broadcastIframeHelp(String param) { - return 'More options on the $param'; + return 'Aukera gehiago ${param}ean'; } @override - String get broadcastWebmastersPage => 'webmasters page'; + String get broadcastWebmastersPage => 'webmasterraren webgune'; @override String broadcastPgnSourceHelp(String param) { - return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + return 'Txanda honen zuzeneko PGN iturburua. $param ere eskaintzen dugu sinkronizazio zehatzagoa nahi baduzu.'; } @override - String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + String get broadcastEmbedThisBroadcast => 'Txertatu zuzeneko hau zure webgunean'; @override String broadcastEmbedThisRound(String param) { - return 'Embed $param in your website'; + return 'Txertatu $param zure webgunean'; } @override - String get broadcastRatingDiff => 'Rating diff'; - - @override - String get broadcastGamesThisTournament => 'Games in this tournament'; + String get broadcastRatingDiff => 'Elo diferentzia'; @override - String get broadcastScore => 'Score'; + String get broadcastGamesThisTournament => 'Txapelketa honetako partidak'; @override - String get broadcastAllTeams => 'All teams'; + String get broadcastScore => 'Emaitza'; @override - String get broadcastTournamentFormat => 'Tournament format'; + String get broadcastAllTeams => 'Talde guztiak'; @override - String get broadcastTournamentLocation => 'Tournament Location'; + String get broadcastTournamentFormat => 'Txapelketaren formatua'; @override - String get broadcastTopPlayers => 'Top players'; + String get broadcastTournamentLocation => 'Txapelketaren kokalekua'; @override - String get broadcastTimezone => 'Time zone'; + String get broadcastTopPlayers => 'Jokalari onenak'; @override - String get broadcastFideRatingCategory => 'FIDE rating category'; + String get broadcastTimezone => 'Ordu-zona'; @override - String get broadcastOptionalDetails => 'Optional details'; + String get broadcastFideRatingCategory => 'FIDE rating kategoria'; @override - String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + String get broadcastOptionalDetails => 'Hautazko xehetasunak'; @override - String get broadcastPastBroadcasts => 'Past broadcasts'; + String get broadcastPastBroadcasts => 'Pasatutako zuzenekoak'; @override - String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + String get broadcastAllBroadcastsByMonth => 'Ikusi zuzeneko guztiak hilabeteka'; @override String broadcastNbBroadcasts(int count) { @@ -1011,6 +1005,9 @@ class AppLocalizationsEu extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Kanpaiaren jakinarazpen soinua'; + @override + String get preferencesBlindfold => 'Itsuka'; + @override String get puzzlePuzzles => 'Ariketak'; @@ -4339,7 +4336,7 @@ class AppLocalizationsEu extends AppLocalizations { String get nothingToSeeHere => 'Hemen ez dago ezer zuretzat.'; @override - String get stats => 'Stats'; + String get stats => 'Estatistikak'; @override String opponentLeftCounter(int count) { @@ -5456,7 +5453,7 @@ class AppLocalizationsEu extends AppLocalizations { @override String studyPerPage(String param) { - return '$param per page'; + return '$param orrialde bakoitzean'; } @override diff --git a/lib/l10n/l10n_fa.dart b/lib/l10n/l10n_fa.dart index dae9bbe211..b627c91041 100644 --- a/lib/l10n/l10n_fa.dart +++ b/lib/l10n/l10n_fa.dart @@ -14,9 +14,6 @@ class AppLocalizationsFa extends AppLocalizations { @override String get mobileAreYouSure => 'مطمئنید؟'; - @override - String get mobileBlindfoldMode => 'چشم‌بسته'; - @override String get mobileCancelTakebackOffer => 'رد درخواست برگرداندن'; @@ -41,7 +38,7 @@ class AppLocalizationsFa extends AppLocalizations { String get mobileGreetingWithoutName => 'درود'; @override - String get mobileHideVariation => 'بستن شاخه‌ها'; + String get mobileHideVariation => 'پنهانیدن وَرتِش'; @override String get mobileHomeTab => 'خانه'; @@ -56,7 +53,7 @@ class AppLocalizationsFa extends AppLocalizations { String get mobileNoSearchResults => 'بدون پیامد'; @override - String get mobileNotFollowingAnyUser => 'شما هیچ کاربری را دنبال نمی‌کنید.'; + String get mobileNotFollowingAnyUser => 'شما هیچ کاربری را نمی‌دنبالید.'; @override String get mobileOkButton => 'باشه'; @@ -124,7 +121,7 @@ class AppLocalizationsFa extends AppLocalizations { String get mobileShowResult => 'نمایش پیامد'; @override - String get mobileShowVariations => 'باز کردن شاخه‌ها'; + String get mobileShowVariations => 'نمایش وَرتِش'; @override String get mobileSomethingWentWrong => 'مشکلی پیش آمد.'; @@ -133,13 +130,13 @@ class AppLocalizationsFa extends AppLocalizations { String get mobileSystemColors => 'رنگ‌های دستگاه'; @override - String get mobileTheme => 'Theme'; + String get mobileTheme => 'پوسته'; @override String get mobileToolsTab => 'ابزارها'; @override - String get mobileWaitingForOpponentToJoin => 'شکیبا برای پیوستن حریف...'; + String get mobileWaitingForOpponentToJoin => 'در انتظار آمدن حریف...'; @override String get mobileWatchTab => 'تماشا'; @@ -156,7 +153,7 @@ class AppLocalizationsFa extends AppLocalizations { } @override - String get activitySignedUp => 'در لیچس ثبت نام کرد'; + String get activitySignedUp => 'در lichess.org نام‌نوشت'; @override String activitySupportedNbMonths(int count, String param2) { @@ -196,8 +193,8 @@ class AppLocalizationsFa extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count بازی $param2 را انجام داد', - one: '$count بازی $param2 را انجام داد', + other: '$count بازی $param2 کرد', + one: '$count بازی $param2 کرد', ); return '$_temp0'; } @@ -207,8 +204,8 @@ class AppLocalizationsFa extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count پیام را در $param2 فرستاد', - one: '$count پیام را در $param2 فرستاد', + other: '$count پیام در $param2 فرستاد', + one: '$count پیام در $param2 فرستاد', ); return '$_temp0'; } @@ -263,7 +260,7 @@ class AppLocalizationsFa extends AppLocalizations { count, locale: localeName, other: 'شروع به دنبالیدن $count بازیکن کرد', - one: '$count بازیکن را دنبال کرد', + one: 'شروع به دنبالیدن $count بازیکن کرد', ); return '$_temp0'; } @@ -317,8 +314,8 @@ class AppLocalizationsFa extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'در $count مسابقه آرنا رقابت کرد', - one: 'در $count مسابقه آرنا رقابت کرد', + other: 'در $count مسابقهٔ راوان رقابت کرد', + one: 'در $count مسابقهٔ راوان رقابت کرد', ); return '$_temp0'; } @@ -372,7 +369,7 @@ class AppLocalizationsFa extends AppLocalizations { String get broadcastNewBroadcast => 'پخش زنده جدید'; @override - String get broadcastSubscribedBroadcasts => 'پخش‌های دنبال‌شده'; + String get broadcastSubscribedBroadcasts => 'پخش‌های دنبالیده'; @override String get broadcastAboutBroadcasts => 'درباره پخش‌های همگانی'; @@ -396,7 +393,7 @@ class AppLocalizationsFa extends AppLocalizations { String get broadcastCompleted => 'کامل‌شده'; @override - String get broadcastCompletedHelp => 'Lichess تکمیل دور را بر اساس بازی‌های منبع تشخیص می‌دهد. اگر منبعی وجود ندارد، از این کلید استفاده کنید.'; + String get broadcastCompletedHelp => 'Lichess تکمیل دور را شناسایی می‌کند، اما می‌تواند آن را اشتباه بگیرد. از این کلید برای تنظیم دستی بهرایید.'; @override String get broadcastRoundName => 'نام دور'; @@ -425,7 +422,7 @@ class AppLocalizationsFa extends AppLocalizations { String get broadcastSourceUrlHelp => 'وب‌نشانی‌ای که Lichess برای دریافت به‌روزرسانی‌های PGN می‌بررسد. آن باید از راه اینترنت در دسترس همگان باشد.'; @override - String get broadcastSourceGameIds => 'تا ۶۴ شناسه بازی لیچس٬ جداشده با فاصله.'; + String get broadcastSourceGameIds => 'تا ۶۴ شناسهٔ بازی Lichess، جداشده با فاصله.'; @override String broadcastStartDateTimeZone(String param) { @@ -496,29 +493,29 @@ class AppLocalizationsFa extends AppLocalizations { String get broadcastRecentTournaments => 'مسابقاتِ اخیر'; @override - String get broadcastOpenLichess => 'Open in Lichess'; + String get broadcastOpenLichess => 'آزاد در Lichess'; @override - String get broadcastTeams => 'تیم‌ها'; + String get broadcastTeams => 'یَران‌ها'; @override - String get broadcastBoards => 'Boards'; + String get broadcastBoards => 'میز‌ها'; @override - String get broadcastOverview => 'Overview'; + String get broadcastOverview => 'نمای کلی'; @override - String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + String get broadcastSubscribeTitle => 'مشترک شوید تا از آغاز هر دور باخبر شوید. می‌توانید اعلان‌های زنگی یا رانشی برای پخش‌های زنده را در تنظیمات حساب‌تان تغییر دهید.'; @override - String get broadcastUploadImage => 'Upload tournament image'; + String get broadcastUploadImage => 'بارگذاری تصویر مسابقات'; @override - String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + String get broadcastNoBoardsYet => 'تاکنون هیچی. وقتی بازی‌ها بارگذاری شدند، میزها پدیدار خواهند شد.'; @override String broadcastBoardsCanBeLoaded(String param) { - return 'Boards can be loaded with a source or via the $param'; + return 'میزها را می‌توان از یک منبع یا از راه $param بارگذاری کرد'; } @override @@ -530,76 +527,73 @@ class AppLocalizationsFa extends AppLocalizations { String get broadcastStartVerySoon => 'پخش زنده به زودی آغاز خواهد شد.'; @override - String get broadcastNotYetStarted => 'پخش زنده هنوز آغاز نشده است.'; + String get broadcastNotYetStarted => 'پخش زنده هنوز نیاغازیده است.'; @override - String get broadcastOfficialWebsite => 'تارنمای رسمی'; + String get broadcastOfficialWebsite => 'وبگاه رسمی'; @override - String get broadcastStandings => 'Standings'; + String get broadcastStandings => 'رده‌بندی'; @override - String get broadcastOfficialStandings => 'Official Standings'; + String get broadcastOfficialStandings => 'رده‌بندی رسمی'; @override String broadcastIframeHelp(String param) { - return 'More options on the $param'; + return 'گزینه‌های بیشتر در $param'; } @override - String get broadcastWebmastersPage => 'webmasters page'; + String get broadcastWebmastersPage => 'صفحهٔ وبداران'; @override String broadcastPgnSourceHelp(String param) { - return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + return 'یک منبع عمومی و بی‌درنگ PGN برای این دور. ما همچنین $param را برای همگامِش تندتر و کارآمدتر پیشنهاد می‌دهیم.'; } @override - String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + String get broadcastEmbedThisBroadcast => 'جاسازی این پخش زنده در وبگاه‌تان'; @override String broadcastEmbedThisRound(String param) { - return 'Embed $param in your website'; + return 'جاسازی $param در وبگاه‌تان'; } @override - String get broadcastRatingDiff => 'ناسانی امتیازات'; + String get broadcastRatingDiff => 'اختلاف درجه‌بندی'; @override - String get broadcastGamesThisTournament => 'Games in this tournament'; + String get broadcastGamesThisTournament => 'بازی‌های این مسابقات'; @override String get broadcastScore => 'امتیاز'; @override - String get broadcastAllTeams => 'All teams'; - - @override - String get broadcastTournamentFormat => 'Tournament format'; + String get broadcastAllTeams => 'همهٔ یَران‌ها'; @override - String get broadcastTournamentLocation => 'Tournament Location'; + String get broadcastTournamentFormat => 'ساختار مسابقات'; @override - String get broadcastTopPlayers => 'Top players'; + String get broadcastTournamentLocation => 'مکان مسابقات'; @override - String get broadcastTimezone => 'Time zone'; + String get broadcastTopPlayers => 'بازیکنان برتر'; @override - String get broadcastFideRatingCategory => 'FIDE rating category'; + String get broadcastTimezone => 'زمان-یانه'; @override - String get broadcastOptionalDetails => 'Optional details'; + String get broadcastFideRatingCategory => 'رسته‌بندی درجه‌بندی فیده'; @override - String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + String get broadcastOptionalDetails => 'جزئیات اختیاری'; @override - String get broadcastPastBroadcasts => 'Past broadcasts'; + String get broadcastPastBroadcasts => 'پخش‌های گذشته'; @override - String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + String get broadcastAllBroadcastsByMonth => 'دیدن پخش‌های هر ماه'; @override String broadcastNbBroadcasts(int count) { @@ -649,7 +643,7 @@ class AppLocalizationsFa extends AppLocalizations { @override String challengeCannotChallengeDueToProvisionalXRating(String param) { - return 'به‌خاطر داشتن درجه‌بندی $param موقت، نمی‌توانید پیشنهاد بازی دهید.'; + return 'به‌خاطر درجه‌بندی $param موقت، نمی‌توانید پیشنهاد بازی دهید.'; } @override @@ -679,10 +673,10 @@ class AppLocalizationsFa extends AppLocalizations { String get challengeDeclineCasual => 'لطفا به جایش، پیشنهاد بازی نارسمی بده.'; @override - String get challengeDeclineStandard => 'الان پیشنهاد بازی‌های شطرنج‌گونه را نمی‌پذیرم.'; + String get challengeDeclineStandard => 'اکنون پیشنهاد بازی‌های وَرتا را نمی‌پذیرم.'; @override - String get challengeDeclineVariant => 'الان مایل نیستم این شطرنج‌گونه را بازی کنم.'; + String get challengeDeclineVariant => 'اکنون مایل نیستم این وَرتا را بازی کنم.'; @override String get challengeDeclineNoBot => 'من پیشنهاد بازی از ربات‌ها را نمی‌پذیرم.'; @@ -707,7 +701,7 @@ class AppLocalizationsFa extends AppLocalizations { @override String perfStatPerfStats(String param) { - return 'وضعیت $param'; + return 'آمار $param'; } @override @@ -731,7 +725,7 @@ class AppLocalizationsFa extends AppLocalizations { @override String perfStatRatingDeviationTooltip(String param1, String param2, String param3) { - return 'مقدار کمتر به این معنی است که درجه‌بندی پایدارتر است. بالاتر از $param1، درجه‌بندی موقت در نظر گرفته می‌شود. برای قرار گرفتن در درجه‌بندی‌ها، این مقدار باید کم‌تر از $param2 (در شطرنج استاندارد) یا $param3 (در شطرنج‌گونه‌ها) باشد.'; + return 'مقدار کمتر به معنای درجه‌بندی پایدارتر است. بالاتر از $param1، درجه‌بندی موقت در نظر گرفته می‌شود. برای قرارگیری در درجه‌بندی‌ها، این مقدار باید کم‌تر از $param2 (در شطرنج استاندارد) یا $param3 (در وَرتاها) باشد.'; } @override @@ -820,7 +814,7 @@ class AppLocalizationsFa extends AppLocalizations { String get preferencesPrivacy => 'امنیت و حریم شخصی'; @override - String get preferencesNotifications => 'اعلانات'; + String get preferencesNotifications => 'اعلان'; @override String get preferencesPieceAnimation => 'حرکت مهره ها'; @@ -838,7 +832,7 @@ class AppLocalizationsFa extends AppLocalizations { String get preferencesBoardCoordinates => 'مختصات صفحه(A-H، 1-8)'; @override - String get preferencesMoveListWhilePlaying => 'لیست حرکات هنگام بازی کردن'; + String get preferencesMoveListWhilePlaying => 'فهرست حرکت هنگام بازی کردن'; @override String get preferencesPgnPieceNotation => 'نشانه‌گذاری حرکات'; @@ -904,10 +898,10 @@ class AppLocalizationsFa extends AppLocalizations { String get preferencesBothClicksAndDrag => 'هر دو'; @override - String get preferencesPremovesPlayingDuringOpponentTurn => 'پیش حرکت (بازی در نوبت حریف)'; + String get preferencesPremovesPlayingDuringOpponentTurn => 'پیش‌حرکت (بازی در نوبت حریف)'; @override - String get preferencesTakebacksWithOpponentApproval => 'پس گرفتن حرکت (با تایید حریف)'; + String get preferencesTakebacksWithOpponentApproval => 'برگردان (با تایید حریف)'; @override String get preferencesInCasualGamesOnly => 'فقط در بازی‌های نارسمی'; @@ -970,10 +964,10 @@ class AppLocalizationsFa extends AppLocalizations { String get preferencesScrollOnTheBoardToReplayMoves => 'برای بازپخش حرکت‌ها، روی صفحه بازی بِنَوَردید'; @override - String get preferencesCorrespondenceEmailNotification => 'ایمیل های روزانه که بازی های شبیه شما را به صورت لیست درمی‌آورند'; + String get preferencesCorrespondenceEmailNotification => 'فهرست رایانامهٔ روزانه از بازی‌های مکاتبه‌ای‌تان'; @override - String get preferencesNotifyStreamStart => 'استریمر شروع به فعالیت کرد'; + String get preferencesNotifyStreamStart => 'بَرخَط-محتواساز روی پخش است'; @override String get preferencesNotifyInboxMsg => 'پیام جدید'; @@ -985,31 +979,34 @@ class AppLocalizationsFa extends AppLocalizations { String get preferencesNotifyInvitedStudy => 'دعوت به مطالعه'; @override - String get preferencesNotifyGameEvent => 'اعلان به روزرسانی بازی'; + String get preferencesNotifyGameEvent => 'به‌روزرسانی‌های بازی مکاتبه‌ای'; @override String get preferencesNotifyChallenge => 'پیشنهاد بازی'; @override - String get preferencesNotifyTournamentSoon => 'تورنمت به زودی آغاز می شود'; + String get preferencesNotifyTournamentSoon => 'مسابقات به‌زودی می‌آغازد'; @override String get preferencesNotifyTimeAlarm => 'هشدار تنگی زمان'; @override - String get preferencesNotifyBell => 'زنگوله اعلانات لیچس'; + String get preferencesNotifyBell => 'اعلان زنگی در Lichess'; @override - String get preferencesNotifyPush => 'اعلانات برای زمانی که شما در لیچس نیستید'; + String get preferencesNotifyPush => 'اعلان اَفزاره، هنگامی که در Lichess نیستید'; @override String get preferencesNotifyWeb => 'مرورگر'; @override - String get preferencesNotifyDevice => 'دستگاه'; + String get preferencesNotifyDevice => 'اَفزاره'; + + @override + String get preferencesBellNotificationSound => 'صدای اعلان زنگی'; @override - String get preferencesBellNotificationSound => 'زنگ اعلان'; + String get preferencesBlindfold => 'چشم‌بسته'; @override String get puzzlePuzzles => 'معماها'; @@ -1214,7 +1211,7 @@ class AppLocalizationsFa extends AppLocalizations { String get puzzleSearchPuzzles => 'جستجوی معما'; @override - String get puzzleFromMyGamesNone => 'شما هیچ معمایی در دادگان ندارید، اما Lichess همچنان شما را بسیار دوست دارد.\n\nبازی‌های سریع و مرسوم را انجام دهید تا بخت‌تان را برای افزودن معمایی از خودتان بیفزایید!'; + String get puzzleFromMyGamesNone => 'شما هیچ معمایی در دادگان ندارید، اما Lichess همچنان شما را بسیار دوست دارد.\n\nبازی‌های سریع و فکری را انجام دهید تا بخت‌تان را برای افزودن معمایی از خودتان بیفزایید!'; @override String puzzleFromXGamesFound(String param1, String param2) { @@ -1222,7 +1219,7 @@ class AppLocalizationsFa extends AppLocalizations { } @override - String get puzzlePuzzleDashboardDescription => 'تمرین کن، تحلیل کن، پیشرفت کن'; + String get puzzlePuzzleDashboardDescription => 'آموزش، واکاوی، بهبود'; @override String puzzlePercentSolved(String param) { @@ -1233,7 +1230,7 @@ class AppLocalizationsFa extends AppLocalizations { String get puzzleNoPuzzlesToShow => 'چیزی برای نمایش نیست، نخست بروید و چند معما حل کنید!'; @override - String get puzzleImprovementAreasDescription => 'این‌ها را تمرین کنید تا روند پیشرفت خود را بهبود ببخشید!'; + String get puzzleImprovementAreasDescription => 'برای بهینیدن پیشرفت‌تان، این‌ها را بیاموزید!'; @override String get puzzleStrengthDescription => 'شما در این زمینه‌ها بهترین عملکرد را دارید'; @@ -1627,7 +1624,7 @@ class AppLocalizationsFa extends AppLocalizations { String get puzzleThemeTrappedPieceDescription => 'یک مهره قادر به فرار کردن از زده شدن نیست چون حرکات محدودی دارد.'; @override - String get puzzleThemeUnderPromotion => 'فرو-ارتقا'; + String get puzzleThemeUnderPromotion => 'کم‌ارتقا'; @override String get puzzleThemeUnderPromotionDescription => 'ارتقا به اسب، فیل یا رخ.'; @@ -1654,7 +1651,7 @@ class AppLocalizationsFa extends AppLocalizations { String get puzzleThemeMix => 'آمیزهٔ همگن'; @override - String get puzzleThemeMixDescription => 'ذره‌ای از هر چیزی. شما نمی‌دانید چه چیزی پیش روی شماست، بنابراین برای هر چیزی آماده می‌مانید! درست مانند بازی‌های واقعی.'; + String get puzzleThemeMixDescription => 'کمی از هر چیزی. شما نمی‌دانید چه چیزی پیش روی شماست، بنابراین برای هر چیزی آماده می‌مانید! درست مانند بازی‌های واقعی.'; @override String get puzzleThemePlayerGames => 'بازی‌های بازیکن'; @@ -1701,7 +1698,7 @@ class AppLocalizationsFa extends AppLocalizations { String get playWithTheMachine => 'بازی با رایانه'; @override - String get toInviteSomeoneToPlayGiveThisUrl => 'برای دعوت کسی به بازی، این وب‌نشانی را دهید'; + String get toInviteSomeoneToPlayGiveThisUrl => 'برای دعوت کردن حریف این لینک را برای او بفرستید'; @override String get gameOver => 'پایان بازی'; @@ -1710,7 +1707,7 @@ class AppLocalizationsFa extends AppLocalizations { String get waitingForOpponent => 'انتطار برای حریف'; @override - String get orLetYourOpponentScanQrCode => 'یا اجازه دهید حریف شما این QR کد را پویش کند'; + String get orLetYourOpponentScanQrCode => 'یا اجازه دهید حریف‌تان این کد QR را بِروبینَد'; @override String get waiting => 'در حال انتظار'; @@ -1784,13 +1781,13 @@ class AppLocalizationsFa extends AppLocalizations { String get kingInTheCenter => 'شاه روی تپه'; @override - String get threeChecks => 'سه کیش'; + String get threeChecks => 'سه‌کیش'; @override String get raceFinished => 'مسابقه تمام شد'; @override - String get variantEnding => 'پایان شطرنج‌گونه'; + String get variantEnding => 'پایان وَرتا'; @override String get newOpponent => 'حریف جدید'; @@ -1864,13 +1861,13 @@ class AppLocalizationsFa extends AppLocalizations { String get usingServerAnalysis => 'با استفاده از کارسازِ تحلیل'; @override - String get loadingEngine => 'پردازشگر بارمی‌گذارد...'; + String get loadingEngine => 'موتور بارمی‌گذارد...'; @override - String get calculatingMoves => 'در حال محاسبه حرکات...'; + String get calculatingMoves => 'محاسبهٔ حرکت‌ها...'; @override - String get engineFailed => 'خطا در بارگذاری پردازشگر'; + String get engineFailed => 'خطا در بارگذاری موتور'; @override String get cloudAnalysis => 'تحلیل ابری'; @@ -1888,7 +1885,7 @@ class AppLocalizationsFa extends AppLocalizations { String get toggleLocalEvaluation => 'کلید ارزیابی محلی'; @override - String get promoteVariation => 'افزایش عمق شاخه اصلی'; + String get promoteVariation => 'ارتقای وَرتِش'; @override String get makeMainLine => 'خط کنونی را به خط اصلی تبدیل کنید'; @@ -1897,25 +1894,25 @@ class AppLocalizationsFa extends AppLocalizations { String get deleteFromHere => 'از اینجا به بعد را پاک کنید'; @override - String get collapseVariations => 'بستن شاخه‌ها'; + String get collapseVariations => 'بستن وَرتِش‌ها'; @override - String get expandVariations => 'باز کردن شاخه‌ها'; + String get expandVariations => 'گستردنِ وَرتِش‌ها'; @override - String get forceVariation => 'نتیجه تحلیل را به عنوان یکی از تنوعهای بازی انتخاب نمایید'; + String get forceVariation => 'وَرتِشِ زوری'; @override - String get copyVariationPgn => 'کپی PGN این شاخه'; + String get copyVariationPgn => 'رونوشت‌گیری PGN ِ وَرتِش'; @override String get move => 'حرکت'; @override - String get variantLoss => 'حرکت بازنده'; + String get variantLoss => 'باختِ وَرتا'; @override - String get variantWin => 'بُردِ شطرنج‌گونه'; + String get variantWin => 'بُردِ وَرتا'; @override String get insufficientMaterial => 'مُهره ناکافی برای مات'; @@ -1986,7 +1983,7 @@ class AppLocalizationsFa extends AppLocalizations { @override String xOpeningExplorer(String param) { - return 'جستجوگر گشایش $param'; + return 'پویشگر گشایش $param'; } @override @@ -2032,10 +2029,10 @@ class AppLocalizationsFa extends AppLocalizations { String get enable => 'فعال سازی'; @override - String get bestMoveArrow => 'فلش نشان دهنده بهترین حرکت'; + String get bestMoveArrow => 'پیکانِ بهترین حرکت'; @override - String get showVariationArrows => 'نمایش پیکان‌های شاخه اصلی'; + String get showVariationArrows => 'نمایش پیکان‌های وَرتِش'; @override String get evaluationGauge => 'میله ارزیابی'; @@ -2050,7 +2047,7 @@ class AppLocalizationsFa extends AppLocalizations { String get memory => 'حافظه'; @override - String get infiniteAnalysis => 'آنالیز بی پایان'; + String get infiniteAnalysis => 'تحلیل بی‌کران'; @override String get removesTheDepthLimit => 'محدودیت عمق را برمی‌دارد و رایانه‌تان داغ می‌ماند'; @@ -2071,7 +2068,7 @@ class AppLocalizationsFa extends AppLocalizations { String get flipBoard => 'چرخاندن صفحه'; @override - String get threefoldRepetition => 'تکرار سه گانه'; + String get threefoldRepetition => 'تکرار سه‌گانه'; @override String get claimADraw => 'ادعای تساوی'; @@ -2089,7 +2086,7 @@ class AppLocalizationsFa extends AppLocalizations { String get fiftyMovesWithoutProgress => 'قانون ۵۰ حرکت'; @override - String get currentGames => 'بازی های در جریان'; + String get currentGames => 'بازی‌های جاری'; @override String get viewInFullSize => 'نمایش در اندازه کامل'; @@ -2145,13 +2142,13 @@ class AppLocalizationsFa extends AppLocalizations { String get yesterday => 'دیروز'; @override - String get minutesPerSide => 'زمان برای هر بازیکن(به دقیقه)'; + String get minutesPerSide => 'هر بازیکن چند دقیقه'; @override - String get variant => 'گونه'; + String get variant => 'وَرتا'; @override - String get variants => 'گونه‌ها'; + String get variants => 'وَرتا'; @override String get timeControl => 'زمان'; @@ -2181,7 +2178,7 @@ class AppLocalizationsFa extends AppLocalizations { String get username => 'نام کاربری'; @override - String get usernameOrEmail => 'نام کاربری یا ایمیل'; + String get usernameOrEmail => 'نام کاربری یا رایانامه'; @override String get changeUsername => 'تغییر نام کاربری'; @@ -2199,10 +2196,10 @@ class AppLocalizationsFa extends AppLocalizations { String get signupEmailHint => 'ما فقط برای بازنشاندن گذرواژه، از آن استفاده خواهیم کرد.'; @override - String get password => 'رمز عبور'; + String get password => 'گذرواژه'; @override - String get changePassword => 'تغییر کلمه عبور'; + String get changePassword => 'تغییر گذرواژه'; @override String get changeEmail => 'تغییر ایمیل'; @@ -2211,25 +2208,25 @@ class AppLocalizationsFa extends AppLocalizations { String get email => 'ایمیل'; @override - String get passwordReset => 'بازیابی کلمه عبور'; + String get passwordReset => 'بازنشانی گذرواژه'; @override - String get forgotPassword => 'آیا کلمه عبور را فراموش کرده اید؟'; + String get forgotPassword => 'گذرواژه را فراموش کرده‌اید؟'; @override - String get error_weakPassword => 'این رمز به شدت معمول و قابل حدس است.'; + String get error_weakPassword => 'این گذرواژه بسیار رایج و آسان‌حدس است.'; @override - String get error_namePassword => 'لطفا رمز خود را متفاوت از نام کاربری خود انتخاب کنید.'; + String get error_namePassword => 'خواهشانه از نام کاربری‌تان برای گذرواژه‌تان استفاده نکنید.'; @override - String get blankedPassword => 'شما از همین رمز عبور در سایت دیگری استفاده کرده اید و آن سایت در معرض خطر قرار گرفته است. برای اطمینان از ایمنی حساب لیچس خود، لازم است که شما یک رمز عبور جدید ایجاد کنید. ممنون از همکاری شما.'; + String get blankedPassword => 'شما از گذرواژهٔ یکسانی در وبگاه دیگری بهراییده‌اید و آن وبگاه به خطر افتاده است. برای اطمینان از ایمنی حساب Lichessتان، به شما نیاز داریم تا گذرواژهٔ نویی را تعیین کنید. از درک‌تان سپاسگزاریم.'; @override String get youAreLeavingLichess => 'در حال ترک lichess هستید'; @override - String get neverTypeYourPassword => 'هرگز رمز خود را در سایت دیگر وارد نکنید!'; + String get neverTypeYourPassword => 'هرگز گذرواژهٔ Lichessتان را در وبگاه دیگری ننویسید!'; @override String proceedToX(String param) { @@ -2361,10 +2358,10 @@ class AppLocalizationsFa extends AppLocalizations { String get decline => 'رد کردن'; @override - String get playingRightNow => 'بازی در حال انجام'; + String get playingRightNow => 'هم‌اکنون بازی می‌کنند'; @override - String get eventInProgress => 'بازی در حال انجام'; + String get eventInProgress => 'اکنون بازی می‌کنند'; @override String get finished => 'تمام شده'; @@ -2472,25 +2469,25 @@ class AppLocalizationsFa extends AppLocalizations { String get openingExplorerAndTablebase => 'پویشگر گشایش و آخربازی'; @override - String get takeback => 'پس گرفتن حرکت'; + String get takeback => 'برگردان'; @override - String get proposeATakeback => 'پیشنهاد پس گرفتن حرکت'; + String get proposeATakeback => 'پیشنهاد برگردان'; @override - String get takebackPropositionSent => 'پیشنهاد پس گرفتن حرکت فرستاده شد'; + String get takebackPropositionSent => 'برگردان فرستاده شد'; @override - String get takebackPropositionDeclined => 'پیشنهاد پس گرفتن حرکت رد شد'; + String get takebackPropositionDeclined => 'برگردان رد شد'; @override - String get takebackPropositionAccepted => 'پیشنهاد پس گرفتن حرکت پذیرفته شد'; + String get takebackPropositionAccepted => 'برگردان پذیرفته شد'; @override - String get takebackPropositionCanceled => 'پیشنهاد پس گرفتن حرکت لغو شد'; + String get takebackPropositionCanceled => 'برگردان لغو شد'; @override - String get yourOpponentProposesATakeback => 'حریف پیشنهاد پس گرفتن حرکت می دهد'; + String get yourOpponentProposesATakeback => 'حریف‌تان پیشنهاد «برگرداندن» می‌دهد'; @override String get bookmarkThisGame => 'نشانک‌گذاری'; @@ -2508,7 +2505,7 @@ class AppLocalizationsFa extends AppLocalizations { String get viewTournament => 'دیدن مسابقات'; @override - String get backToTournament => 'برگشت به مسابقه'; + String get backToTournament => 'بازگشت به مسابقات'; @override String get noDrawBeforeSwissLimit => 'شما نمی‌توانید در مسابقات سوییس تا قبل از حرکت ۳۰ام بازی را مساوی کنید.'; @@ -2573,11 +2570,11 @@ class AppLocalizationsFa extends AppLocalizations { } @override - String get startedStreaming => 'پخش را آغازید'; + String get startedStreaming => 'جریان‌سازی را آغازید'; @override String xStartedStreaming(String param) { - return '$param پخش را آغازید'; + return '$param جریان‌سازی را آغازید'; } @override @@ -2608,7 +2605,7 @@ class AppLocalizationsFa extends AppLocalizations { String get gameAsGIF => 'بارگیری GIF بازی'; @override - String get pasteTheFenStringHere => 'پوزیشن دلخواه(FEN) را در این قسمت وارد کنید'; + String get pasteTheFenStringHere => 'رشته FEN را در این قسمت قرار دهید'; @override String get pasteThePgnStringHere => 'متن PGN را در این قسمت وارد کنید'; @@ -2632,7 +2629,7 @@ class AppLocalizationsFa extends AppLocalizations { String get importGameExplanation => 'برای دریافت بازپخش مرورپذیر، واکاوی رایانه‌ای، گپ‌های بازی، و وب‌نشانی همگانی همرسانی‌پذیر، PGN یک بازی را جای‌گذاری کنید.'; @override - String get importGameCaveat => 'تغییرات پاک خواهند شد. برای حفظ آنها، PGN را از طریق مطالعه وارد کنید.'; + String get importGameCaveat => 'ورتش‌ها پاک خواهند شد. برای حفظشان، PGN را از طریق مطالعه درون‌بَرید.'; @override String get importGameDataPrivacyWarning => 'این PGN برای عموم در دسترس است، برای وارد کردن یک بازی خصوصی، از *مطالعه* استفاده کنید.'; @@ -2659,7 +2656,7 @@ class AppLocalizationsFa extends AppLocalizations { String get retry => 'تلاش دوباره'; @override - String get reconnecting => 'در حال بازاتصال'; + String get reconnecting => 'بازاتصال...'; @override String get noNetwork => 'بُرون‌خط'; @@ -2671,7 +2668,7 @@ class AppLocalizationsFa extends AppLocalizations { String get follow => 'دنبالیدن'; @override - String get following => 'دنبالنده'; + String get following => 'دنبال‌شدگان'; @override String get unfollow => 'وادنبالیدن'; @@ -2733,13 +2730,13 @@ class AppLocalizationsFa extends AppLocalizations { String get winner => 'برنده'; @override - String get standing => 'رتبه بندی'; + String get standing => 'رده‌بندی'; @override - String get createANewTournament => 'درست کردن یک مسابقه ی جدید'; + String get createANewTournament => 'ایجاد یک مسابقهٔ نو'; @override - String get tournamentCalendar => 'برنامه ی مسابقات'; + String get tournamentCalendar => 'گاهشمار مسابقات'; @override String get conditionOfEntry => 'شرایط ورود:'; @@ -2754,7 +2751,7 @@ class AppLocalizationsFa extends AppLocalizations { String get inappropriateNameWarning => 'هرچیز حتی کمی نامناسب ممکن است باعث بسته شدن حساب کاربری شما بشود.'; @override - String get emptyTournamentName => 'این مکان را خالی بگذارید تا به صورت تصادفی اسم یک استاد بزرگ برای مسابقات انتخاب شود.'; + String get emptyTournamentName => 'برای نامیدن مسابقات به نام یک شطرنج‌باز برجسته، خالی بگذارید.'; @override String get makePrivateTournament => 'تورنومنت را به حالت خصوصی در بیاورید و دسترسی را محدود به داشتن پسورد کنید'; @@ -2795,13 +2792,13 @@ class AppLocalizationsFa extends AppLocalizations { String get resume => 'ادامه دادن'; @override - String get youArePlaying => 'شما بازی میکنید!'; + String get youArePlaying => 'شما بازی می‌کنید!'; @override String get winRate => 'درصد برد'; @override - String get berserkRate => 'میزان جنون'; + String get berserkRate => 'میزان دیوانگی'; @override String get performance => 'عملکرد'; @@ -2836,7 +2833,7 @@ class AppLocalizationsFa extends AppLocalizations { String get boardEditor => 'مُهره‌چینی'; @override - String get setTheBoard => 'مهره‌ها را بچینید'; + String get setTheBoard => 'میز را بچینید'; @override String get popularOpenings => 'گشایش‌های محبوب'; @@ -3067,31 +3064,31 @@ class AppLocalizationsFa extends AppLocalizations { String get noNoteYet => 'تاکنون، بدون یادداشت'; @override - String get invalidUsernameOrPassword => 'نام کاربری یا رمز عبور نادرست است'; + String get invalidUsernameOrPassword => 'نام کاربری یا گذرواژهٔ نامعتبر'; @override - String get incorrectPassword => 'گذرواژه‌ی نادرست'; + String get incorrectPassword => 'گذرواژهٔ نادرست'; @override - String get invalidAuthenticationCode => 'کد اصالت سنجی نامعتبر'; + String get invalidAuthenticationCode => 'کد راستین‌آزمایی نامعتبر'; @override String get emailMeALink => 'یک لینک به من ایمیل کنید'; @override - String get currentPassword => 'رمز جاری'; + String get currentPassword => 'گذرواژهٔ جاری'; @override - String get newPassword => 'رمز جدید'; + String get newPassword => 'گذرواژهٔ نو'; @override - String get newPasswordAgain => '(رمز جدید(برای دومین بار'; + String get newPasswordAgain => 'گذرواژهٔ نو (دوباره)'; @override - String get newPasswordsDontMatch => 'کلمه‌های عبور وارد شده مطابقت ندارند'; + String get newPasswordsDontMatch => 'گذرواژه‌های نو هم‌جور نیستند'; @override - String get newPasswordStrength => 'استحکام کلمه عبور'; + String get newPasswordStrength => 'نیرومندی گذرواژه'; @override String get clockInitialTime => 'مقدار زمان اولیه'; @@ -3207,10 +3204,10 @@ class AppLocalizationsFa extends AppLocalizations { String get learnMenu => 'یادگیری'; @override - String get studyMenu => 'مطالعه‌ها'; + String get studyMenu => 'مطالعه'; @override - String get practice => 'تمرین کردن'; + String get practice => 'تمرین'; @override String get community => 'همدارگان'; @@ -3246,7 +3243,7 @@ class AppLocalizationsFa extends AppLocalizations { @override String error_maxLength(String param) { - return 'باید حداقل دارای $param حرف باشد'; + return 'باید حداکثر $param نویسه داشته باشد'; } @override @@ -3295,17 +3292,17 @@ class AppLocalizationsFa extends AppLocalizations { @override String tpTimeSpentOnTV(String param) { - return 'مدت گذرانده در تلویزیون: $param'; + return 'مدت آرنگیده در تلویزیون: $param'; } @override String get watch => 'تماشا'; @override - String get videoLibrary => 'فیلم ها'; + String get videoLibrary => 'فیلم‌ها'; @override - String get streamersMenu => 'بَرخَط-محتواسازها'; + String get streamersMenu => 'بَرخَط-محتواسازان'; @override String get mobileApp => 'گوشی‌افزار'; @@ -3383,7 +3380,7 @@ class AppLocalizationsFa extends AppLocalizations { String get aboutSimulRules => 'وقتی نمایش همزمان شروع شود، هر بازیکن یک بازی را با میزبان که با مهره سفید بازی میکند آغاز میکند. نمایش وقتی تمام می شود که تمام بازی ها تمام شده باشند.'; @override - String get aboutSimulSettings => 'نمایش های همزمان همیشه غیر رسمی هستند. بازی دوباره، پس گرفتن حرکت و اضافه کردن زمان غیرفعال شده اند.'; + String get aboutSimulSettings => 'نمایشگاه همزمان همیشه نارسمی است. بازرویارویی، برگرداندن و زمان افزاینده نافعال شده‌اند.'; @override String get create => 'ساختن'; @@ -3392,7 +3389,7 @@ class AppLocalizationsFa extends AppLocalizations { String get whenCreateSimul => 'وقتی یک نمایش همزمان ایجاد میکنید باید با چند نفر همزمان بازی کنید.'; @override - String get simulVariantsHint => 'اگر چندین گونه را انتخاب کنید، هر بازیکن می‌تواند انتخاب کند که کدام یک را بازی کند.'; + String get simulVariantsHint => 'اگر چندین وَرتا را برگزینید، هر بازیکن می‌تواند انتخاب کند که کدام‌یک را بازی کند.'; @override String get simulClockHint => 'تنظیم ساعت فیشر. هرچه از بازیکنان بیشتری برنده شوید، زمان بیشتری نیاز دارید'; @@ -3410,10 +3407,10 @@ class AppLocalizationsFa extends AppLocalizations { String get simulHostExtraTimePerPlayer => 'زمان اضافه میزبان به ازای بازیکن'; @override - String get lichessTournaments => 'مسابقات لی چس'; + String get lichessTournaments => 'مسابقات Lichess'; @override - String get tournamentFAQ => 'سوالات متداول مسابقات'; + String get tournamentFAQ => 'پرسش‌های پربسامد مسابقات راوان'; @override String get timeBeforeTournamentStarts => 'زمان باقی مانده به شروع مسابقه'; @@ -3425,7 +3422,7 @@ class AppLocalizationsFa extends AppLocalizations { String get accuracy => 'دقت'; @override - String get keyboardShortcuts => 'میانبر های صفحه کلید'; + String get keyboardShortcuts => 'میانبرهای صفحه‌کلید'; @override String get keyMoveBackwardOrForward => 'حرکت به عقب/جلو'; @@ -3434,13 +3431,13 @@ class AppLocalizationsFa extends AppLocalizations { String get keyGoToStartOrEnd => 'رفتن به آغاز/پایان'; @override - String get keyCycleSelectedVariation => 'چرخه شاخه اصلی انتخاب‌شده'; + String get keyCycleSelectedVariation => 'چرخاندن وَرتِش گزیده'; @override String get keyShowOrHideComments => 'نمایش/پنهان کردن نظرها'; @override - String get keyEnterOrExitVariation => 'ورود / خروج به شاخه'; + String get keyEnterOrExitVariation => 'ورود/خروج به وَرتِش'; @override String get keyRequestComputerAnalysis => 'درخواست تحلیل رایانه‌ای، از اشتباه‌های‌تان بیاموزید'; @@ -3464,10 +3461,10 @@ class AppLocalizationsFa extends AppLocalizations { String get keyNextBranch => 'شاخه بعدی'; @override - String get toggleVariationArrows => 'کلید پیکان‌های شاخه اصلی'; + String get toggleVariationArrows => 'کلید پیکان‌های وَرتِش'; @override - String get cyclePreviousOrNextVariation => 'چرخه پیشین/پسین شاخه اصلی'; + String get cyclePreviousOrNextVariation => 'چرخاندن پیشین/پسین وَرتِش'; @override String get toggleGlyphAnnotations => 'کلید علائم حرکت‌نویسی'; @@ -3476,7 +3473,7 @@ class AppLocalizationsFa extends AppLocalizations { String get togglePositionAnnotations => 'تغییر حرکت‌نویسی وضعیت'; @override - String get variationArrowsInfo => 'پیکان های شاخه اصلی به شما امکان می‌دهد بدون استفاده از فهرست حرکت، پیمایش کنید.'; + String get variationArrowsInfo => 'پیکان های وَرتِش به شما امکان ناوِش بدون استفاده از فهرستِ حرکت را می‌دهد.'; @override String get playSelectedMove => 'حرکت انتخاب شده را بازی کن'; @@ -3485,10 +3482,10 @@ class AppLocalizationsFa extends AppLocalizations { String get newTournament => 'مسابقه جدید'; @override - String get tournamentHomeTitle => 'مسابقات شطرنج با گونه‌ها و زمان‌بندی‌های مختلف'; + String get tournamentHomeTitle => 'مسابقات شطرنج با وَرتاها و زمان‌بندی‌های گوناگون'; @override - String get tournamentHomeDescription => 'هرچه زودتر شطرنج بازی کنید! به یک مسابقه رسمی برنامه‌ریزی‌شده بپیوندید یا مسابقات خودتان را بسازید. شطرنج گلوله‌ای، برق‌آسا، مرسوم، ۹۶۰، پادشاه تپه‌ها، سه‌کیش و دیگر گزینه‌ها، برای لذت بی‌پایان از شطرنج در دسترسند.'; + String get tournamentHomeDescription => 'هرچه زودتر شطرنج بازی کنید! به یک مسابقه رسمی برنامه‌ریزی‌شده بپیوندید یا مسابقات خودتان را بسازید. شطرنج گلوله‌ای، برق‌آسا، فکری، ۹۶۰، پادشاه تپه‌ها، سه‌کیش و دیگر گزینه‌ها، برای لذت بی‌پایان از شطرنج در دسترسند.'; @override String get tournamentNotFound => 'مسابقات یافت نشد'; @@ -3524,7 +3521,7 @@ class AppLocalizationsFa extends AppLocalizations { @override String betterThanPercentPlayers(String param1, String param2) { - return 'بهتر از $param1 بازیکنان در $param2'; + return 'بهتر از $param1 بازیکنان $param2'; } @override @@ -3552,7 +3549,7 @@ class AppLocalizationsFa extends AppLocalizations { @override String weHaveSentYouAnEmailTo(String param) { - return 'ایمیل ارسال شد.بر روی لینک داخل ایمیل کلیک کنید تا پسورد شما ریست شود $param به آدرس'; + return 'ما یک رایانامه به $param فرستاده‌ایم. برای بازنشانی گذرواژه‌تان، روی پیوند موجود در رایانامه بزنید.'; } @override @@ -3566,7 +3563,7 @@ class AppLocalizationsFa extends AppLocalizations { } @override - String get networkLagBetweenYouAndLichess => 'تاخیر شبکه بین شما و Lichess'; + String get networkLagBetweenYouAndLichess => 'تاخیر شبکه میان شما و Lichess'; @override String get timeToProcessAMoveOnLichessServer => 'زمان سپری شده برای پردازش یک حرکت'; @@ -3587,16 +3584,16 @@ class AppLocalizationsFa extends AppLocalizations { String get youCanAlsoScrollOverTheBoardToMoveInTheGame => 'برای حرکت، روی صفحه بازی بِنَوَردید.'; @override - String get scrollOverComputerVariationsToPreviewThem => 'برای پیش‌نمایش آن‌ها، روی شاخه‌های رایانه‌ای بِنَوَردید.'; + String get scrollOverComputerVariationsToPreviewThem => 'برای پیش‌نمایش آن‌ها، روی وَرتِش‌های رایانه‌ای بِغَرالید.'; @override - String get analysisShapesHowTo => 'و کلیک کنید یا راست کلیک کنید تا دایره یا فلش در صفحه بکشید shift'; + String get analysisShapesHowTo => 'برای رسم دایره و پیکان روی تخته، shift+click یا راست-تِلیک را بفشارید.'; @override String get letOtherPlayersMessageYou => 'ارسال پیام توسط بقیه به شما'; @override - String get receiveForumNotifications => 'دریافت اعلان در هنگام ذکر شدن در انجمن'; + String get receiveForumNotifications => 'دریافت اعلان هنگام نام‌بَری در انجمن'; @override String get shareYourInsightsData => 'اشتراک گذاشتن داده های شما'; @@ -3611,33 +3608,33 @@ class AppLocalizationsFa extends AppLocalizations { String get withEverybody => 'با همه'; @override - String get kidMode => 'حالت کودکان'; + String get kidMode => 'حالت کودک'; @override String get kidModeIsEnabled => 'حالت کودک فعال است.'; @override - String get kidModeExplanation => 'این گزینه،امنیتی است.با فعال کردن حالت ((کودکانه))،همه ی ارتباطات(چت کردن و...)غیر فعال می شوند.با فعال کردن این گزینه،کودکان خود را محافطت کنید.'; + String get kidModeExplanation => 'این دربارهٔ ایمنی است. در حالت کودک، همهٔ ارتباط‌های وبگاه نافعال است. این را برای فرزندان و شطرنج‌آموزان مدرسه خود فعال کنید تا از آنها در برابر دیگر کاربران اینترنت حفاظت کنید.'; @override String inKidModeTheLichessLogoGetsIconX(String param) { - return 'در حالت کودکانه،به نماد لیچس،یک $param اضافه می شود تا شما از فعال بودن آن مطلع شوید.'; + return 'در حالت کودک، نماد Lichess نقشک $param را می‌گیرد، بنابراین می‌دانید کودکان‌تان در امانند.'; } @override String get askYourChessTeacherAboutLiftingKidMode => 'حسابتان مدیریت می‌شود. از آموزگار شطرنج‌تان درباره برداشتن حالت کودک بپرسید.'; @override - String get enableKidMode => 'فعال کردن حالت کودکانه'; + String get enableKidMode => 'فعال‌سازی حالت کودک'; @override - String get disableKidMode => 'غیر فعال کردن حالت کودکانه'; + String get disableKidMode => 'ازکاراندازی حالت کودک'; @override String get security => 'امنیت'; @override - String get sessions => 'جلسات'; + String get sessions => 'جلسه'; @override String get revokeAllSessions => 'باطل کردن تمامی موارد'; @@ -3664,7 +3661,7 @@ class AppLocalizationsFa extends AppLocalizations { String get phoneAndTablet => 'گوشی و رایانک'; @override - String get bulletBlitzClassical => 'گلوله‌ای، برق‌آسا، مرسوم'; + String get bulletBlitzClassical => 'گلوله‌ای، برق‌آسا، فکری'; @override String get correspondenceChess => 'شطرنج مکاتبه ای'; @@ -3726,7 +3723,7 @@ class AppLocalizationsFa extends AppLocalizations { String get transparent => 'شفاف'; @override - String get deviceTheme => 'طرح زمینه دستگاه'; + String get deviceTheme => 'پوستهٔ اَفزاره'; @override String get backgroundImageUrl => 'وب‌نشانی تصویر پس‌زمینه:'; @@ -3768,7 +3765,7 @@ class AppLocalizationsFa extends AppLocalizations { String get usernameCharsInvalid => 'نام کاربری فقط می تواند شامل حروف،اعداد،خط فاصله یا زیر خط(under line) باشد.'; @override - String get usernameUnacceptable => 'این نام کاربری قابل قبول نیست.'; + String get usernameUnacceptable => 'این نام کاربری پذیرفتنی نیست.'; @override String get playChessInStyle => 'شطرنج‌بازیِ نوگارانه'; @@ -3777,10 +3774,10 @@ class AppLocalizationsFa extends AppLocalizations { String get chessBasics => 'پایه‌های شطرنج'; @override - String get coaches => 'مربی ها'; + String get coaches => 'مربیان'; @override - String get invalidPgn => 'فایل PGN نامعتبر است'; + String get invalidPgn => 'PGN ِ نامعتبر'; @override String get invalidFen => 'وضعیت نامعتبر'; @@ -3789,11 +3786,11 @@ class AppLocalizationsFa extends AppLocalizations { String get custom => 'دلخواه'; @override - String get notifications => 'گزارش'; + String get notifications => 'اعلان'; @override String notificationsX(String param1) { - return 'هشدار: $param1'; + return 'اعلان: $param1'; } @override @@ -3815,10 +3812,10 @@ class AppLocalizationsFa extends AppLocalizations { } @override - String get youBrowsedAway => 'پوزیشن را به هم زدید!'; + String get youBrowsedAway => 'دور شُدید'; @override - String get resumePractice => 'ادامه تمرین'; + String get resumePractice => 'از سرگیری تمرین'; @override String get drawByFiftyMoves => 'بازی با قانون پنجاه حرکت مساوی شده است.'; @@ -3827,7 +3824,7 @@ class AppLocalizationsFa extends AppLocalizations { String get theGameIsADraw => 'بازی مساوی است.'; @override - String get computerThinking => 'محاسبه رایانه‌ای ...'; + String get computerThinking => 'محاسبهٔ رایانه‌ای...'; @override String get seeBestMove => 'دیدن بهترین حرکت'; @@ -3839,7 +3836,7 @@ class AppLocalizationsFa extends AppLocalizations { String get getAHint => 'راهنمایی'; @override - String get evaluatingYourMove => 'در حال بررسی حرکت شما...'; + String get evaluatingYourMove => 'حرکت‌تان را می‌ارزیابد...'; @override String get whiteWinsGame => 'سفید می‌برد'; @@ -3925,10 +3922,10 @@ class AppLocalizationsFa extends AppLocalizations { String get conditionalPremoves => 'پیش‌حرکت‌های شرطی'; @override - String get addCurrentVariation => 'اضافه کردن این نوع حرکات'; + String get addCurrentVariation => 'افزودن وَرتِش جاری'; @override - String get playVariationToCreateConditionalPremoves => 'یک نوع حرکات را بازی کنید تا پیش حرکت های شرطی را بسازید'; + String get playVariationToCreateConditionalPremoves => 'بازی کردن یک وَرتِش، برای ایجاد پیش‌حرکت‌های شرطی'; @override String get noConditionalPremoves => 'بدون پیش‌حرکت‌های شرطی'; @@ -4019,7 +4016,7 @@ class AppLocalizationsFa extends AppLocalizations { String get rapid => 'سریع'; @override - String get classical => 'کلاسیک'; + String get classical => 'فکری'; @override String get ultraBulletDesc => 'بازی‌های سرعتی دیوانه‌وار: کمتر از ۳۰ ثانیه'; @@ -4034,7 +4031,7 @@ class AppLocalizationsFa extends AppLocalizations { String get rapidDesc => 'بازی های سریع: ۸ تا ۲۵ دقیقه'; @override - String get classicalDesc => 'بازی های کلاسیک : 25 دقیقه یا بیشتر'; + String get classicalDesc => 'بازی های فکری: ۲۵ دقیقه یا بیشتر'; @override String get correspondenceDesc => 'بازی های مکاتبه ای : یک یا چند روز برای هر حرکت'; @@ -4051,7 +4048,7 @@ class AppLocalizationsFa extends AppLocalizations { } @override - String get inTheFAQ => 'در سوالات متداول باشد.'; + String get inTheFAQ => 'در پرسش‌های پُربسامد'; @override String toReportSomeoneForCheatingOrBadBehavior(String param1) { @@ -4101,7 +4098,7 @@ class AppLocalizationsFa extends AppLocalizations { @override String mentionedYouInX(String param1) { - return 'از شما در $param1 نام برده شد.'; + return 'در «$param1» از شما نام‌برده شد.'; } @override @@ -4124,7 +4121,7 @@ class AppLocalizationsFa extends AppLocalizations { @override String youHaveJoinedTeamX(String param1) { - return 'شما به \"$param1\" پیوستید.'; + return 'شما به «$param1» پیوسته‌اید.'; } @override @@ -4152,10 +4149,10 @@ class AppLocalizationsFa extends AppLocalizations { } @override - String get timeAlmostUp => 'زمان تقریباً تمام شده است!'; + String get timeAlmostUp => 'زمان نزدیک به پایان است!'; @override - String get clickToRevealEmailAddress => '[برای آشکارسازی نشانی رایانامه بتلیکید]'; + String get clickToRevealEmailAddress => '[برای آشکارسازی نشانیِ رایانامه بتِلیکید]'; @override String get download => 'بارگیری'; @@ -4164,7 +4161,7 @@ class AppLocalizationsFa extends AppLocalizations { String get coachManager => 'تنظیمات مربی'; @override - String get streamerManager => 'مدیریت پخش'; + String get streamerManager => 'مدیریت جریان‌سازی'; @override String get cancelTournament => 'لغو مسابقه'; @@ -4195,7 +4192,7 @@ class AppLocalizationsFa extends AppLocalizations { @override String positionInputHelp(String param) { - return 'برای آغاز هر بازی از یک وضعیت مشخص، یک FEN معتبر جای‌گذارید.\nتنها برای شطرنج معیار کار می‌کند، نه با شطرنج‌گونه‌ها.\nمی‌توانید از $param برای آزانیدن وضعیت FEN استفاده کنید، سپس آن را اینجا جای‌گذارید.\nبرای آغاز بازی از وضعیت نخستین معمولی، خالی بگذارید.'; + return 'برای آغاز هر بازی از یک وضعیت مشخص، یک FEN معتبر جای‌گذارید.\nتنها برای شطرنج معیار کار می‌کند، نه با وَرتاها.\nمی‌توانید از $param برای آزانیدن وضعیت FEN بهرایید، سپس آن را اینجا جای‌گذارید.\nبرای آغاز بازی از وضعیت نخستین معمولی، خالی بگذارید.'; } @override @@ -4209,7 +4206,7 @@ class AppLocalizationsFa extends AppLocalizations { @override String simulFeatured(String param) { - return 'نمایش در $param'; + return 'آرنگیدن در $param'; } @override @@ -4238,7 +4235,7 @@ class AppLocalizationsFa extends AppLocalizations { String get tournChat => 'چت مسابقه'; @override - String get noChat => 'بدون چت'; + String get noChat => 'بدون گپ'; @override String get onlyTeamLeaders => 'تنها مسئولان تیم'; @@ -4423,7 +4420,7 @@ class AppLocalizationsFa extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count ریتینگ در $param2 بازی', + other: 'درجه‌بندی $count در $param2 بازی', one: 'درجه‌بندی $count در $param2 بازی', ); return '$_temp0'; @@ -4555,8 +4552,8 @@ class AppLocalizationsFa extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count بازی در حال انجام', - one: '$count بازی در حال انجام', + other: '$count بازیِ اکنونی', + one: '$count بازیِ اکنونی', ); return '$_temp0'; } @@ -4632,8 +4629,8 @@ class AppLocalizationsFa extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'شما باید$count بازی رسمی$param2 انجام دهید.', - one: 'شما باید$count بازی رسمی$param2 انجام دهید.', + other: 'شما باید $count بازی رسمی $param2 دیگر کنید', + one: 'شما باید $count بازی رسمی $param2 دیگر کنید', ); return '$_temp0'; } @@ -4687,8 +4684,8 @@ class AppLocalizationsFa extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count دنبالنده', - one: '$count دنبالنده', + other: '$count دنبال‌شده', + one: '$count دنبالیده', ); return '$_temp0'; } @@ -4753,8 +4750,8 @@ class AppLocalizationsFa extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count بازیکن $param2 این هفته فعالیت داشته‌اند.', - one: '$count بازیکن $param2 این هفته فعالیت داشته‌ است.', + other: 'این هفته، $count بازیکن $param2.', + one: 'این هفته، $count بازیکن $param2.', ); return '$_temp0'; } @@ -4886,7 +4883,7 @@ class AppLocalizationsFa extends AppLocalizations { String get stormGetReady => 'آماده شوید!'; @override - String get stormWaitingForMorePlayers => 'در حال انتظار برای پیوستن بازیکنان بیشتر...'; + String get stormWaitingForMorePlayers => 'در انتظارِ پیوستن بازیکنان بیشتر...'; @override String get stormRaceComplete => 'مسابقه تمام شد!'; @@ -5212,7 +5209,7 @@ class AppLocalizationsFa extends AppLocalizations { String get studyClearAnnotations => 'پاک کردن حرکت‌نویسی'; @override - String get studyClearVariations => 'پاک کردن تغییرات'; + String get studyClearVariations => 'پاکیدن وَرتِش‌ها'; @override String get studyDeleteChapter => 'حذف بخش'; @@ -5456,7 +5453,7 @@ class AppLocalizationsFa extends AppLocalizations { @override String studyPerPage(String param) { - return '$param per page'; + return '$param میز'; } @override diff --git a/lib/l10n/l10n_fi.dart b/lib/l10n/l10n_fi.dart index 9638039299..faa1f1e95c 100644 --- a/lib/l10n/l10n_fi.dart +++ b/lib/l10n/l10n_fi.dart @@ -14,9 +14,6 @@ class AppLocalizationsFi extends AppLocalizations { @override String get mobileAreYouSure => 'Oletko varma?'; - @override - String get mobileBlindfoldMode => 'Sokko'; - @override String get mobileCancelTakebackOffer => 'Peruuta siirron peruutuspyyntö'; @@ -47,7 +44,7 @@ class AppLocalizationsFi extends AppLocalizations { String get mobileHomeTab => 'Etusivu'; @override - String get mobileLiveStreamers => 'Live streamers'; + String get mobileLiveStreamers => 'Live-striimaajat'; @override String get mobileMustBeLoggedIn => 'Sinun täytyy olla kirjautuneena nähdäksesi tämän sivun.'; @@ -133,7 +130,7 @@ class AppLocalizationsFi extends AppLocalizations { String get mobileSystemColors => 'Järjestelmän värit'; @override - String get mobileTheme => 'Theme'; + String get mobileTheme => 'Teema'; @override String get mobileToolsTab => 'Työkalut'; @@ -518,12 +515,12 @@ class AppLocalizationsFi extends AppLocalizations { @override String broadcastBoardsCanBeLoaded(String param) { - return 'Boards can be loaded with a source or via the $param'; + return 'Laudat voidaan ladata lähteen kautta tai $param kautta'; } @override String broadcastStartsAfter(String param) { - return 'Alkuun on aikaa $param'; + return 'Alkaa $param:n jälkeen'; } @override @@ -539,7 +536,7 @@ class AppLocalizationsFi extends AppLocalizations { String get broadcastStandings => 'Tulostaulu'; @override - String get broadcastOfficialStandings => 'Official Standings'; + String get broadcastOfficialStandings => 'Virallinen tulostaulu'; @override String broadcastIframeHelp(String param) { @@ -572,34 +569,31 @@ class AppLocalizationsFi extends AppLocalizations { String get broadcastScore => 'Pisteet'; @override - String get broadcastAllTeams => 'All teams'; - - @override - String get broadcastTournamentFormat => 'Tournament format'; + String get broadcastAllTeams => 'Kaikki joukkueet'; @override - String get broadcastTournamentLocation => 'Tournament Location'; + String get broadcastTournamentFormat => 'Turnauksen laji'; @override - String get broadcastTopPlayers => 'Top players'; + String get broadcastTournamentLocation => 'Turnauksen sijainti'; @override - String get broadcastTimezone => 'Time zone'; + String get broadcastTopPlayers => 'Parhaat pelaajat'; @override - String get broadcastFideRatingCategory => 'FIDE rating category'; + String get broadcastTimezone => 'Aikavyöhyke'; @override - String get broadcastOptionalDetails => 'Optional details'; + String get broadcastFideRatingCategory => 'Kategoria (FIDE-vahvuuslukujen mukaan)'; @override - String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + String get broadcastOptionalDetails => 'Mahdolliset lisätiedot'; @override - String get broadcastPastBroadcasts => 'Past broadcasts'; + String get broadcastPastBroadcasts => 'Menneet lähetykset'; @override - String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + String get broadcastAllBroadcastsByMonth => 'Näytä kaikki lähetykset kuukausikohtaisesti'; @override String broadcastNbBroadcasts(int count) { @@ -1011,6 +1005,9 @@ class AppLocalizationsFi extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Ilmoitusten kilahdusääni'; + @override + String get preferencesBlindfold => 'Sokko'; + @override String get puzzlePuzzles => 'Tehtävät'; @@ -5456,7 +5453,7 @@ class AppLocalizationsFi extends AppLocalizations { @override String studyPerPage(String param) { - return '$param per page'; + return '$param per sivu'; } @override diff --git a/lib/l10n/l10n_fo.dart b/lib/l10n/l10n_fo.dart index 9e7ec519ec..91354d0b75 100644 --- a/lib/l10n/l10n_fo.dart +++ b/lib/l10n/l10n_fo.dart @@ -14,9 +14,6 @@ class AppLocalizationsFo extends AppLocalizations { @override String get mobileAreYouSure => 'Are you sure?'; - @override - String get mobileBlindfoldMode => 'Blindfold'; - @override String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @@ -592,9 +589,6 @@ class AppLocalizationsFo extends AppLocalizations { @override String get broadcastOptionalDetails => 'Optional details'; - @override - String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; - @override String get broadcastPastBroadcasts => 'Past broadcasts'; @@ -1011,6 +1005,9 @@ class AppLocalizationsFo extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Bell notification sound'; + @override + String get preferencesBlindfold => 'Blindfold'; + @override String get puzzlePuzzles => 'Uppgávur'; diff --git a/lib/l10n/l10n_fr.dart b/lib/l10n/l10n_fr.dart index aceb0029ea..228c302998 100644 --- a/lib/l10n/l10n_fr.dart +++ b/lib/l10n/l10n_fr.dart @@ -14,9 +14,6 @@ class AppLocalizationsFr extends AppLocalizations { @override String get mobileAreYouSure => 'Êtes-vous sûr(e) ?'; - @override - String get mobileBlindfoldMode => 'Partie à l\'aveugle'; - @override String get mobileCancelTakebackOffer => 'Annuler la proposition de reprise du coup'; @@ -133,7 +130,7 @@ class AppLocalizationsFr extends AppLocalizations { String get mobileSystemColors => 'Couleurs du système'; @override - String get mobileTheme => 'Theme'; + String get mobileTheme => 'Thème'; @override String get mobileToolsTab => 'Outils'; @@ -523,7 +520,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String broadcastStartsAfter(String param) { - return 'Commence après $param'; + return 'Commence après la $param'; } @override @@ -539,7 +536,7 @@ class AppLocalizationsFr extends AppLocalizations { String get broadcastStandings => 'Classement'; @override - String get broadcastOfficialStandings => 'Official Standings'; + String get broadcastOfficialStandings => 'Résultats officiels'; @override String broadcastIframeHelp(String param) { @@ -572,34 +569,31 @@ class AppLocalizationsFr extends AppLocalizations { String get broadcastScore => 'Résultat'; @override - String get broadcastAllTeams => 'All teams'; - - @override - String get broadcastTournamentFormat => 'Tournament format'; + String get broadcastAllTeams => 'Toutes les équipes'; @override - String get broadcastTournamentLocation => 'Tournament Location'; + String get broadcastTournamentFormat => 'Format du tournoi'; @override - String get broadcastTopPlayers => 'Top players'; + String get broadcastTournamentLocation => 'Lieu du tournoi'; @override - String get broadcastTimezone => 'Time zone'; + String get broadcastTopPlayers => 'Meilleurs joueurs'; @override - String get broadcastFideRatingCategory => 'FIDE rating category'; + String get broadcastTimezone => 'Fuseau horaire'; @override - String get broadcastOptionalDetails => 'Optional details'; + String get broadcastFideRatingCategory => 'Catégorie FIDE'; @override - String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + String get broadcastOptionalDetails => 'Informations facultatives'; @override - String get broadcastPastBroadcasts => 'Past broadcasts'; + String get broadcastPastBroadcasts => 'Diffusions passées'; @override - String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + String get broadcastAllBroadcastsByMonth => 'Voir les diffusions par mois'; @override String broadcastNbBroadcasts(int count) { @@ -1011,6 +1005,9 @@ class AppLocalizationsFr extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Son de notification'; + @override + String get preferencesBlindfold => 'Partie à l\'aveugle'; + @override String get puzzlePuzzles => 'Problèmes'; @@ -5456,7 +5453,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String studyPerPage(String param) { - return '$param per page'; + return '$param par page'; } @override diff --git a/lib/l10n/l10n_ga.dart b/lib/l10n/l10n_ga.dart index f6f9a3cf45..a0e67ed4bf 100644 --- a/lib/l10n/l10n_ga.dart +++ b/lib/l10n/l10n_ga.dart @@ -14,9 +14,6 @@ class AppLocalizationsGa extends AppLocalizations { @override String get mobileAreYouSure => 'Are you sure?'; - @override - String get mobileBlindfoldMode => 'Blindfold'; - @override String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @@ -643,9 +640,6 @@ class AppLocalizationsGa extends AppLocalizations { @override String get broadcastOptionalDetails => 'Optional details'; - @override - String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; - @override String get broadcastPastBroadcasts => 'Past broadcasts'; @@ -1062,6 +1056,9 @@ class AppLocalizationsGa extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Bell notification sound'; + @override + String get preferencesBlindfold => 'Blindfold'; + @override String get puzzlePuzzles => 'Fadhbanna'; diff --git a/lib/l10n/l10n_gl.dart b/lib/l10n/l10n_gl.dart index 09249134de..4617d10b8a 100644 --- a/lib/l10n/l10n_gl.dart +++ b/lib/l10n/l10n_gl.dart @@ -14,9 +14,6 @@ class AppLocalizationsGl extends AppLocalizations { @override String get mobileAreYouSure => 'Estás seguro?'; - @override - String get mobileBlindfoldMode => 'Á cega'; - @override String get mobileCancelTakebackOffer => 'Cancelar a proposta de cambio'; @@ -133,10 +130,10 @@ class AppLocalizationsGl extends AppLocalizations { String get mobileSystemColors => 'Cores do sistema'; @override - String get mobileTheme => 'Theme'; + String get mobileTheme => 'Tema'; @override - String get mobileToolsTab => 'Ferramentas'; + String get mobileToolsTab => 'Ferrament.'; @override String get mobileWaitingForOpponentToJoin => 'Agardando un rival...'; @@ -523,7 +520,7 @@ class AppLocalizationsGl extends AppLocalizations { @override String broadcastStartsAfter(String param) { - return 'Comeza en $param'; + return 'Comeza tras a $param'; } @override @@ -539,7 +536,7 @@ class AppLocalizationsGl extends AppLocalizations { String get broadcastStandings => 'Clasificación'; @override - String get broadcastOfficialStandings => 'Official Standings'; + String get broadcastOfficialStandings => 'Clasificación oficial'; @override String broadcastIframeHelp(String param) { @@ -572,34 +569,31 @@ class AppLocalizationsGl extends AppLocalizations { String get broadcastScore => 'Resultado'; @override - String get broadcastAllTeams => 'All teams'; - - @override - String get broadcastTournamentFormat => 'Tournament format'; + String get broadcastAllTeams => 'Todos os equipos'; @override - String get broadcastTournamentLocation => 'Tournament Location'; + String get broadcastTournamentFormat => 'Formato do torneo'; @override - String get broadcastTopPlayers => 'Top players'; + String get broadcastTournamentLocation => 'Lugar do torneo'; @override - String get broadcastTimezone => 'Time zone'; + String get broadcastTopPlayers => 'Mellores xogadores'; @override - String get broadcastFideRatingCategory => 'FIDE rating category'; + String get broadcastTimezone => 'Zona horaria'; @override - String get broadcastOptionalDetails => 'Optional details'; + String get broadcastFideRatingCategory => 'Categoría de puntuación FIDE'; @override - String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + String get broadcastOptionalDetails => 'Detalles opcionais'; @override - String get broadcastPastBroadcasts => 'Past broadcasts'; + String get broadcastPastBroadcasts => 'Emisións finalizadas'; @override - String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + String get broadcastAllBroadcastsByMonth => 'Ver todas as emisións por mes'; @override String broadcastNbBroadcasts(int count) { @@ -1011,6 +1005,9 @@ class AppLocalizationsGl extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Son da notificación'; + @override + String get preferencesBlindfold => 'Ás cegas'; + @override String get puzzlePuzzles => 'Crebacabezas'; @@ -2584,7 +2581,7 @@ class AppLocalizationsGl extends AppLocalizations { String get averageElo => 'Puntuación media'; @override - String get location => 'Ubicación'; + String get location => 'Lugar'; @override String get filterGames => 'Filtrar partidas'; @@ -5456,7 +5453,7 @@ class AppLocalizationsGl extends AppLocalizations { @override String studyPerPage(String param) { - return '$param per page'; + return '$param por páxina'; } @override diff --git a/lib/l10n/l10n_gsw.dart b/lib/l10n/l10n_gsw.dart index bb9a271163..a1aeb7c6d8 100644 --- a/lib/l10n/l10n_gsw.dart +++ b/lib/l10n/l10n_gsw.dart @@ -14,9 +14,6 @@ class AppLocalizationsGsw extends AppLocalizations { @override String get mobileAreYouSure => 'Bisch sicher?'; - @override - String get mobileBlindfoldMode => 'Blind schpille'; - @override String get mobileCancelTakebackOffer => 'Zugsrücknam-Offerte zruggzieh'; @@ -133,7 +130,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get mobileSystemColors => 'Syschtem-Farbe'; @override - String get mobileTheme => 'Theme'; + String get mobileTheme => 'Farbschema'; @override String get mobileToolsTab => 'Werchzüg'; @@ -539,7 +536,7 @@ class AppLocalizationsGsw extends AppLocalizations { String get broadcastStandings => 'Tabälle'; @override - String get broadcastOfficialStandings => 'Official Standings'; + String get broadcastOfficialStandings => 'Offizielli Ranglischte'; @override String broadcastIframeHelp(String param) { @@ -572,34 +569,31 @@ class AppLocalizationsGsw extends AppLocalizations { String get broadcastScore => 'Resultat'; @override - String get broadcastAllTeams => 'All teams'; - - @override - String get broadcastTournamentFormat => 'Tournament format'; + String get broadcastAllTeams => 'Alli Teams'; @override - String get broadcastTournamentLocation => 'Tournament Location'; + String get broadcastTournamentFormat => 'Turnier-Format'; @override - String get broadcastTopPlayers => 'Top players'; + String get broadcastTournamentLocation => 'Turnier-Lokal'; @override - String get broadcastTimezone => 'Time zone'; + String get broadcastTopPlayers => 'Top-Schpiller'; @override - String get broadcastFideRatingCategory => 'FIDE rating category'; + String get broadcastTimezone => 'Zitzone'; @override - String get broadcastOptionalDetails => 'Optional details'; + String get broadcastFideRatingCategory => 'FIDE-Wertigskategorie'; @override - String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + String get broadcastOptionalDetails => 'Optionali Details'; @override - String get broadcastPastBroadcasts => 'Past broadcasts'; + String get broadcastPastBroadcasts => 'G\'machti Überträgige'; @override - String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + String get broadcastAllBroadcastsByMonth => 'Zeig alli Überträgige im Monet'; @override String broadcastNbBroadcasts(int count) { @@ -1011,6 +1005,9 @@ class AppLocalizationsGsw extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Ton für Benachrichtige'; + @override + String get preferencesBlindfold => 'Blind schpille'; + @override String get puzzlePuzzles => 'Ufgabe'; @@ -5456,7 +5453,7 @@ class AppLocalizationsGsw extends AppLocalizations { @override String studyPerPage(String param) { - return '$param per page'; + return '$param pro Site'; } @override diff --git a/lib/l10n/l10n_he.dart b/lib/l10n/l10n_he.dart index 1b849eca5c..fb6d7d98f9 100644 --- a/lib/l10n/l10n_he.dart +++ b/lib/l10n/l10n_he.dart @@ -14,9 +14,6 @@ class AppLocalizationsHe extends AppLocalizations { @override String get mobileAreYouSure => 'בטוח?'; - @override - String get mobileBlindfoldMode => 'משחק עיוור'; - @override String get mobileCancelTakebackOffer => 'ביטול ההצעה להחזיר את המהלך האחרון'; @@ -133,7 +130,7 @@ class AppLocalizationsHe extends AppLocalizations { String get mobileSystemColors => 'צבעי מערכת ההפעלה'; @override - String get mobileTheme => 'Theme'; + String get mobileTheme => 'עיצוב'; @override String get mobileToolsTab => 'כלים'; @@ -575,7 +572,7 @@ class AppLocalizationsHe extends AppLocalizations { String get broadcastStandings => 'תוצאות'; @override - String get broadcastOfficialStandings => 'Official Standings'; + String get broadcastOfficialStandings => 'טבלת מובילים רשמית'; @override String broadcastIframeHelp(String param) { @@ -608,34 +605,31 @@ class AppLocalizationsHe extends AppLocalizations { String get broadcastScore => 'ניקוד'; @override - String get broadcastAllTeams => 'All teams'; - - @override - String get broadcastTournamentFormat => 'Tournament format'; + String get broadcastAllTeams => 'כל הקבוצות'; @override - String get broadcastTournamentLocation => 'Tournament Location'; + String get broadcastTournamentFormat => 'שיטת הטורניר'; @override - String get broadcastTopPlayers => 'Top players'; + String get broadcastTournamentLocation => 'מיקום הטורניר'; @override - String get broadcastTimezone => 'Time zone'; + String get broadcastTopPlayers => 'שחקני צמרת'; @override - String get broadcastFideRatingCategory => 'FIDE rating category'; + String get broadcastTimezone => 'אזור זמן'; @override - String get broadcastOptionalDetails => 'Optional details'; + String get broadcastFideRatingCategory => 'קטגוריית דירוג FIDE'; @override - String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + String get broadcastOptionalDetails => 'פרטים אופציונאליים'; @override - String get broadcastPastBroadcasts => 'Past broadcasts'; + String get broadcastPastBroadcasts => 'הקרנות עבר'; @override - String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + String get broadcastAllBroadcastsByMonth => 'צפו בכל ההקרנות לפי חודש'; @override String broadcastNbBroadcasts(int count) { @@ -1049,6 +1043,9 @@ class AppLocalizationsHe extends AppLocalizations { @override String get preferencesBellNotificationSound => 'השמע צליל עבור התראות פעמון'; + @override + String get preferencesBlindfold => 'משחק עיוור'; + @override String get puzzlePuzzles => 'פאזלים'; @@ -1731,7 +1728,7 @@ class AppLocalizationsHe extends AppLocalizations { String get settingsClosingIsDefinitive => 'הסגירה היא סופית. אין דרך חזרה. האם את/ה בטוח/ה?'; @override - String get settingsCantOpenSimilarAccount => 'לא תוכל/י לפתוח חשבון חדש עם אותו השם, אפילו בשינוי אותיות קטנות לגדולות והפוך. '; + String get settingsCantOpenSimilarAccount => 'לא תוכל/י לפתוח חשבון חדש עם אותו השם, אפילו בשינוי אותיות קטנות לגדולות והפוך.'; @override String get settingsChangedMindDoNotCloseAccount => 'שיניתי את דעתי, אל תסגרו את החשבון שלי'; @@ -5592,7 +5589,7 @@ class AppLocalizationsHe extends AppLocalizations { @override String studyPerPage(String param) { - return '$param per page'; + return '$param לכל עמוד'; } @override diff --git a/lib/l10n/l10n_hi.dart b/lib/l10n/l10n_hi.dart index 376fd6813d..509e3007bb 100644 --- a/lib/l10n/l10n_hi.dart +++ b/lib/l10n/l10n_hi.dart @@ -14,9 +14,6 @@ class AppLocalizationsHi extends AppLocalizations { @override String get mobileAreYouSure => 'क्या आप सुनिश्चित हैं?'; - @override - String get mobileBlindfoldMode => 'Blindfold'; - @override String get mobileCancelTakebackOffer => 'Takeback प्रस्ताव रद्द करें'; @@ -592,9 +589,6 @@ class AppLocalizationsHi extends AppLocalizations { @override String get broadcastOptionalDetails => 'Optional details'; - @override - String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; - @override String get broadcastPastBroadcasts => 'Past broadcasts'; @@ -1011,6 +1005,9 @@ class AppLocalizationsHi extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Bell notification sound'; + @override + String get preferencesBlindfold => 'Blindfold'; + @override String get puzzlePuzzles => 'पहेलियाँ'; diff --git a/lib/l10n/l10n_hr.dart b/lib/l10n/l10n_hr.dart index 202c5f6e53..306ac9b54d 100644 --- a/lib/l10n/l10n_hr.dart +++ b/lib/l10n/l10n_hr.dart @@ -14,9 +14,6 @@ class AppLocalizationsHr extends AppLocalizations { @override String get mobileAreYouSure => 'Are you sure?'; - @override - String get mobileBlindfoldMode => 'Blindfold'; - @override String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @@ -609,9 +606,6 @@ class AppLocalizationsHr extends AppLocalizations { @override String get broadcastOptionalDetails => 'Optional details'; - @override - String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; - @override String get broadcastPastBroadcasts => 'Past broadcasts'; @@ -632,7 +626,7 @@ class AppLocalizationsHr extends AppLocalizations { @override String challengeChallengesX(String param1) { - return 'Challenges: $param1'; + return 'Izazova: $param1'; } @override @@ -1018,7 +1012,7 @@ class AppLocalizationsHr extends AppLocalizations { String get preferencesNotifyBell => 'Obavijest zvonom unutar Lichessa'; @override - String get preferencesNotifyPush => 'Obavijest uređaja kada niste na Lichessu'; + String get preferencesNotifyPush => 'Obavijest uređaja kada niste na Lichess-u'; @override String get preferencesNotifyWeb => 'Preglednik'; @@ -1029,6 +1023,9 @@ class AppLocalizationsHr extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Obavijest kao zvuk'; + @override + String get preferencesBlindfold => 'Blindfold'; + @override String get puzzlePuzzles => 'Zadaci'; @@ -2219,7 +2216,7 @@ class AppLocalizationsHr extends AppLocalizations { String get signupUsernameHint => 'Obavezno odaberi obiteljsko korisničko ime. Ne možeš ga kasnije promijeniti i svi računi s neprikladnim korisničkim imenima bit će zatvoreni!'; @override - String get signupEmailHint => 'Koristit ćemo ga samo za ponovno postavljanje lozinke.'; + String get signupEmailHint => 'Koristiti ćemo ga samo za ponovno postavljanje lozinke.'; @override String get password => 'Lozinka'; @@ -2243,7 +2240,7 @@ class AppLocalizationsHr extends AppLocalizations { String get error_weakPassword => 'Ova je lozinka iznimno česta i previše je lako pogoditi.'; @override - String get error_namePassword => 'Molimo da ne koristitiš svoje korisničko ime kao lozinku.'; + String get error_namePassword => 'Molimo da ne koristiš svoje korisničko ime kao lozinku.'; @override String get blankedPassword => 'Koristio si istu lozinku na drugom mjestu, a to je mjesto ugroženo. Kako bismo osigurali sigurnost tvoga Lichess računa, potrebno je da postaviš novu lozinku. Hvala na razumijevanju.'; @@ -2909,7 +2906,7 @@ class AppLocalizationsHr extends AppLocalizations { String get editProfile => 'Uredi profil'; @override - String get realName => 'Real name'; + String get realName => 'Puno ime'; @override String get setFlair => 'Set your flair'; @@ -2924,7 +2921,7 @@ class AppLocalizationsHr extends AppLocalizations { String get biography => 'Životopis'; @override - String get countryRegion => 'Country or region'; + String get countryRegion => 'Država ili regija'; @override String get thankYou => 'Hvala!'; @@ -3466,10 +3463,10 @@ class AppLocalizationsHr extends AppLocalizations { String get keyEnterOrExitVariation => 'otvori/zatvori varijantu'; @override - String get keyRequestComputerAnalysis => 'Request computer analysis, Learn from your mistakes'; + String get keyRequestComputerAnalysis => 'Zatraži računalnu analizu, Uči na svojim greškama'; @override - String get keyNextLearnFromYourMistakes => 'Next (Learn from your mistakes)'; + String get keyNextLearnFromYourMistakes => 'Sljedeće (Uči na svojim greškama)'; @override String get keyNextBlunder => 'Next blunder'; @@ -3755,16 +3752,16 @@ class AppLocalizationsHr extends AppLocalizations { String get backgroundImageUrl => 'URL pozadinske slike:'; @override - String get board => 'Board'; + String get board => 'Ploča'; @override - String get size => 'Size'; + String get size => 'Veličina'; @override String get opacity => 'Opacity'; @override - String get brightness => 'Brightness'; + String get brightness => 'Svjetlina'; @override String get hue => 'Hue'; @@ -3816,7 +3813,7 @@ class AppLocalizationsHr extends AppLocalizations { @override String notificationsX(String param1) { - return 'Notifications: $param1'; + return 'Obavijesti: $param1'; } @override @@ -3965,7 +3962,7 @@ class AppLocalizationsHr extends AppLocalizations { String get showUnreadLichessMessage => 'You have received a private message from Lichess.'; @override - String get clickHereToReadIt => 'Click here to read it'; + String get clickHereToReadIt => 'Klikni ovdje da pročitaš'; @override String get sorry => 'Oprosti :('; @@ -4218,7 +4215,7 @@ class AppLocalizationsHr extends AppLocalizations { @override String positionInputHelp(String param) { - return 'Zalijepite važeći FEN da biste započeli svaku igru s određene pozicije.\nRadi samo za standardne igre, ne i za varijante.\nMožete koristiti $param za generiranje FEN pozicije, a zatim ga zalijepite ovdje.\nOstavite prazno za početak igre s normalne početne pozicije.'; + return 'Zalijepite važeći FEN da biste započeli svaku igru s određene pozicije.\nRadi samo za standardne igre, ne i za varijante.\nMožete koristiti $param za generiranje FEN pozicije, zatim ga zalijepite ovdje.\nOstavite prazno za početak igre s normalne početne pozicije.'; } @override @@ -4362,7 +4359,7 @@ class AppLocalizationsHr extends AppLocalizations { String get nothingToSeeHere => 'Nothing to see here at the moment.'; @override - String get stats => 'Stats'; + String get stats => 'Statistika'; @override String opponentLeftCounter(int count) { diff --git a/lib/l10n/l10n_hu.dart b/lib/l10n/l10n_hu.dart index ad4dd2d3af..f18daabe27 100644 --- a/lib/l10n/l10n_hu.dart +++ b/lib/l10n/l10n_hu.dart @@ -14,9 +14,6 @@ class AppLocalizationsHu extends AppLocalizations { @override String get mobileAreYouSure => 'Biztos vagy benne?'; - @override - String get mobileBlindfoldMode => 'Vakjátszma mód'; - @override String get mobileCancelTakebackOffer => 'Visszalépés kérésének visszavonása'; @@ -592,9 +589,6 @@ class AppLocalizationsHu extends AppLocalizations { @override String get broadcastOptionalDetails => 'Optional details'; - @override - String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; - @override String get broadcastPastBroadcasts => 'Past broadcasts'; @@ -1011,6 +1005,9 @@ class AppLocalizationsHu extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Hangjelzés'; + @override + String get preferencesBlindfold => 'Vakjátszma mód'; + @override String get puzzlePuzzles => 'Feladványok'; diff --git a/lib/l10n/l10n_hy.dart b/lib/l10n/l10n_hy.dart index 098206cd4d..828d8a92fd 100644 --- a/lib/l10n/l10n_hy.dart +++ b/lib/l10n/l10n_hy.dart @@ -14,9 +14,6 @@ class AppLocalizationsHy extends AppLocalizations { @override String get mobileAreYouSure => 'Are you sure?'; - @override - String get mobileBlindfoldMode => 'Blindfold'; - @override String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @@ -592,9 +589,6 @@ class AppLocalizationsHy extends AppLocalizations { @override String get broadcastOptionalDetails => 'Optional details'; - @override - String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; - @override String get broadcastPastBroadcasts => 'Past broadcasts'; @@ -1011,6 +1005,9 @@ class AppLocalizationsHy extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Ծանուցումների զանգակի ձայնը'; + @override + String get preferencesBlindfold => 'Blindfold'; + @override String get puzzlePuzzles => 'Խնդիրներ'; diff --git a/lib/l10n/l10n_id.dart b/lib/l10n/l10n_id.dart index 8d38b867c2..2a90f49d5f 100644 --- a/lib/l10n/l10n_id.dart +++ b/lib/l10n/l10n_id.dart @@ -9,13 +9,10 @@ class AppLocalizationsId extends AppLocalizations { AppLocalizationsId([String locale = 'id']) : super(locale); @override - String get mobileAllGames => 'All games'; + String get mobileAllGames => 'Semua permainan'; @override - String get mobileAreYouSure => 'Are you sure?'; - - @override - String get mobileBlindfoldMode => 'Blindfold'; + String get mobileAreYouSure => 'Apa kamu yakin?'; @override String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @@ -30,21 +27,21 @@ class AppLocalizationsId extends AppLocalizations { String get mobileCustomGameJoinAGame => 'Join a game'; @override - String get mobileFeedbackButton => 'Feedback'; + String get mobileFeedbackButton => 'Ulas balik'; @override String mobileGreeting(String param) { - return 'Hello, $param'; + return 'Halo, $param'; } @override - String get mobileGreetingWithoutName => 'Hello'; + String get mobileGreetingWithoutName => 'Halo'; @override - String get mobileHideVariation => 'Hide variation'; + String get mobileHideVariation => 'Sembunyikan variasi'; @override - String get mobileHomeTab => 'Home'; + String get mobileHomeTab => 'Beranda'; @override String get mobileLiveStreamers => 'Live streamers'; @@ -59,7 +56,7 @@ class AppLocalizationsId extends AppLocalizations { String get mobileNotFollowingAnyUser => 'You are not following any user.'; @override - String get mobileOkButton => 'OK'; + String get mobileOkButton => 'Oke'; @override String mobilePlayersMatchingSearchTerm(String param) { @@ -88,7 +85,7 @@ class AppLocalizationsId extends AppLocalizations { String get mobilePuzzleThemesSubtitle => 'Play puzzles from your favorite openings, or choose a theme.'; @override - String get mobilePuzzlesTab => 'Puzzles'; + String get mobilePuzzlesTab => 'Teka-teki'; @override String get mobileRecentSearches => 'Recent searches'; @@ -103,28 +100,28 @@ class AppLocalizationsId extends AppLocalizations { String get mobileSettingsImmersiveModeSubtitle => 'Hide system UI while playing. Use this if you are bothered by the system\'s navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.'; @override - String get mobileSettingsTab => 'Settings'; + String get mobileSettingsTab => 'Pengaturan'; @override - String get mobileShareGamePGN => 'Share PGN'; + String get mobileShareGamePGN => 'Bagikan GPN'; @override - String get mobileShareGameURL => 'Share game URL'; + String get mobileShareGameURL => 'Bagikan URL permainan'; @override String get mobileSharePositionAsFEN => 'Share position as FEN'; @override - String get mobileSharePuzzle => 'Share this puzzle'; + String get mobileSharePuzzle => 'Bagikan teka-teki ini'; @override - String get mobileShowComments => 'Show comments'; + String get mobileShowComments => 'Tampilkan komentar'; @override - String get mobileShowResult => 'Show result'; + String get mobileShowResult => 'Tampilkan hasil'; @override - String get mobileShowVariations => 'Show variations'; + String get mobileShowVariations => 'Tampilkan variasi'; @override String get mobileSomethingWentWrong => 'Something went wrong.'; @@ -142,7 +139,7 @@ class AppLocalizationsId extends AppLocalizations { String get mobileWaitingForOpponentToJoin => 'Waiting for opponent to join...'; @override - String get mobileWatchTab => 'Watch'; + String get mobileWatchTab => 'Tontonan'; @override String get activityActivity => 'Aktivitas'; @@ -575,9 +572,6 @@ class AppLocalizationsId extends AppLocalizations { @override String get broadcastOptionalDetails => 'Optional details'; - @override - String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; - @override String get broadcastPastBroadcasts => 'Past broadcasts'; @@ -994,6 +988,9 @@ class AppLocalizationsId extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Suara pemberitahuan'; + @override + String get preferencesBlindfold => 'Blindfold'; + @override String get puzzlePuzzles => 'Teka-teki'; diff --git a/lib/l10n/l10n_it.dart b/lib/l10n/l10n_it.dart index fed9f6fb6c..cec456b4ac 100644 --- a/lib/l10n/l10n_it.dart +++ b/lib/l10n/l10n_it.dart @@ -14,9 +14,6 @@ class AppLocalizationsIt extends AppLocalizations { @override String get mobileAreYouSure => 'Sei sicuro?'; - @override - String get mobileBlindfoldMode => 'Alla cieca'; - @override String get mobileCancelTakebackOffer => 'Annulla richiesta di ritiro mossa'; @@ -133,7 +130,7 @@ class AppLocalizationsIt extends AppLocalizations { String get mobileSystemColors => 'Tema app'; @override - String get mobileTheme => 'Theme'; + String get mobileTheme => 'Tema'; @override String get mobileToolsTab => 'Strumenti'; @@ -496,110 +493,107 @@ class AppLocalizationsIt extends AppLocalizations { String get broadcastRecentTournaments => 'Tornei recenti'; @override - String get broadcastOpenLichess => 'Open in Lichess'; + String get broadcastOpenLichess => 'Apri con Lichess'; @override - String get broadcastTeams => 'Teams'; + String get broadcastTeams => 'Squadre'; @override - String get broadcastBoards => 'Boards'; + String get broadcastBoards => 'Scacchiere'; @override - String get broadcastOverview => 'Overview'; + String get broadcastOverview => 'Panoramica'; @override - String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + String get broadcastSubscribeTitle => 'Iscriviti per ricevere notifiche sull\'inizio di ogni round. Puoi attivare o disattivare la campanella o le notifiche push per le dirette nelle preferenze del tuo account.'; @override - String get broadcastUploadImage => 'Upload tournament image'; + String get broadcastUploadImage => 'Carica immagine del torneo'; @override - String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + String get broadcastNoBoardsYet => 'Non sono ancora presenti scacchiere. Esse compariranno non appena i giochi saranno stati caricati.'; @override String broadcastBoardsCanBeLoaded(String param) { - return 'Boards can be loaded with a source or via the $param'; + return 'Le scacchiere possono essere caricate con una sorgente o tramite $param'; } @override String broadcastStartsAfter(String param) { - return 'Starts after $param'; + return 'Inizia tra $param'; } @override - String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + String get broadcastStartVerySoon => 'Questa trasmissione inizierà a breve.'; @override - String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + String get broadcastNotYetStarted => 'Questa trasmissione non è ancora cominciata.'; @override - String get broadcastOfficialWebsite => 'Official website'; + String get broadcastOfficialWebsite => 'Sito web ufficiale'; @override - String get broadcastStandings => 'Standings'; + String get broadcastStandings => 'Classifica'; @override - String get broadcastOfficialStandings => 'Official Standings'; + String get broadcastOfficialStandings => 'Classifica Ufficiale'; @override String broadcastIframeHelp(String param) { - return 'More options on the $param'; + return 'Altre opzioni si trovano nella $param'; } @override - String get broadcastWebmastersPage => 'webmasters page'; + String get broadcastWebmastersPage => 'pagina dei gestori web'; @override String broadcastPgnSourceHelp(String param) { - return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + return 'Una sorgente PGN pubblica per questo round. Viene offerta anche un\'$param per una sincronizzazione più rapida ed efficiente.'; } @override - String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + String get broadcastEmbedThisBroadcast => 'Incorpora questa trasmissione nel tuo sito web'; @override String broadcastEmbedThisRound(String param) { - return 'Embed $param in your website'; + return 'Incorpora $param nel tuo sito web'; } @override - String get broadcastRatingDiff => 'Rating diff'; - - @override - String get broadcastGamesThisTournament => 'Games in this tournament'; + String get broadcastRatingDiff => 'Differenza di punteggio'; @override - String get broadcastScore => 'Score'; + String get broadcastGamesThisTournament => 'Partite in questo torneo'; @override - String get broadcastAllTeams => 'All teams'; + String get broadcastScore => 'Punteggio'; @override - String get broadcastTournamentFormat => 'Tournament format'; + String get broadcastAllTeams => 'Tutte le squadre'; @override - String get broadcastTournamentLocation => 'Tournament Location'; + String get broadcastTournamentFormat => 'Formato del torneo'; @override - String get broadcastTopPlayers => 'Top players'; + String get broadcastTournamentLocation => 'Luogo del Torneo'; @override - String get broadcastTimezone => 'Time zone'; + String get broadcastTopPlayers => 'Giocatori migliori'; @override - String get broadcastFideRatingCategory => 'FIDE rating category'; + String get broadcastTimezone => 'Fuso orario'; @override - String get broadcastOptionalDetails => 'Optional details'; + String get broadcastFideRatingCategory => 'Categoria di punteggio FIDE'; @override - String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + String get broadcastOptionalDetails => 'Dettagli facoltativi'; @override - String get broadcastPastBroadcasts => 'Past broadcasts'; + String get broadcastPastBroadcasts => 'Trasmissioni precedenti'; @override - String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + String get broadcastAllBroadcastsByMonth => 'Visualizza tutte le trasmissioni per mese'; @override String broadcastNbBroadcasts(int count) { @@ -1011,6 +1005,9 @@ class AppLocalizationsIt extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Tono notifica'; + @override + String get preferencesBlindfold => 'Alla cieca'; + @override String get puzzlePuzzles => 'Problemi'; @@ -4339,7 +4336,7 @@ class AppLocalizationsIt extends AppLocalizations { String get nothingToSeeHere => 'Niente da vedere qui al momento.'; @override - String get stats => 'Stats'; + String get stats => 'Statistiche'; @override String opponentLeftCounter(int count) { @@ -5456,7 +5453,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String studyPerPage(String param) { - return '$param per page'; + return '$param per pagina'; } @override diff --git a/lib/l10n/l10n_ja.dart b/lib/l10n/l10n_ja.dart index a78b58e2b7..e7d9800353 100644 --- a/lib/l10n/l10n_ja.dart +++ b/lib/l10n/l10n_ja.dart @@ -14,9 +14,6 @@ class AppLocalizationsJa extends AppLocalizations { @override String get mobileAreYouSure => '本当にいいですか?'; - @override - String get mobileBlindfoldMode => 'めかくしモード'; - @override String get mobileCancelTakebackOffer => '待ったをキャンセル'; @@ -521,7 +518,7 @@ class AppLocalizationsJa extends AppLocalizations { String get broadcastStandings => '順位'; @override - String get broadcastOfficialStandings => 'Official Standings'; + String get broadcastOfficialStandings => '公式順位'; @override String broadcastIframeHelp(String param) { @@ -554,34 +551,31 @@ class AppLocalizationsJa extends AppLocalizations { String get broadcastScore => 'スコア'; @override - String get broadcastAllTeams => 'All teams'; - - @override - String get broadcastTournamentFormat => 'Tournament format'; + String get broadcastAllTeams => 'すべてのチーム'; @override - String get broadcastTournamentLocation => 'Tournament Location'; + String get broadcastTournamentFormat => 'トーナメント形式'; @override - String get broadcastTopPlayers => 'Top players'; + String get broadcastTournamentLocation => '開催地'; @override - String get broadcastTimezone => 'Time zone'; + String get broadcastTopPlayers => 'トッププレイヤー'; @override - String get broadcastFideRatingCategory => 'FIDE rating category'; + String get broadcastTimezone => 'タイムゾーン'; @override - String get broadcastOptionalDetails => 'Optional details'; + String get broadcastFideRatingCategory => 'FIDE レーティング カテゴリー'; @override - String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + String get broadcastOptionalDetails => 'その他詳細(オプション)'; @override - String get broadcastPastBroadcasts => 'Past broadcasts'; + String get broadcastPastBroadcasts => '過去の中継'; @override - String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + String get broadcastAllBroadcastsByMonth => 'すべての中継を月別に表示'; @override String broadcastNbBroadcasts(int count) { @@ -992,6 +986,9 @@ class AppLocalizationsJa extends AppLocalizations { @override String get preferencesBellNotificationSound => 'ベル通知の音'; + @override + String get preferencesBlindfold => 'めかくしモード'; + @override String get puzzlePuzzles => 'タクティクス問題'; @@ -5388,7 +5385,7 @@ class AppLocalizationsJa extends AppLocalizations { @override String studyPerPage(String param) { - return '$param per page'; + return '$param 件/ページ'; } @override diff --git a/lib/l10n/l10n_kk.dart b/lib/l10n/l10n_kk.dart index edb76bae45..dbc3125f02 100644 --- a/lib/l10n/l10n_kk.dart +++ b/lib/l10n/l10n_kk.dart @@ -14,9 +14,6 @@ class AppLocalizationsKk extends AppLocalizations { @override String get mobileAreYouSure => 'Растайсыз ба?'; - @override - String get mobileBlindfoldMode => 'Blindfold'; - @override String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @@ -592,9 +589,6 @@ class AppLocalizationsKk extends AppLocalizations { @override String get broadcastOptionalDetails => 'Optional details'; - @override - String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; - @override String get broadcastPastBroadcasts => 'Past broadcasts'; @@ -1011,6 +1005,9 @@ class AppLocalizationsKk extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Қоңыраулы ескерту'; + @override + String get preferencesBlindfold => 'Blindfold'; + @override String get puzzlePuzzles => 'Жұмбақтар'; diff --git a/lib/l10n/l10n_ko.dart b/lib/l10n/l10n_ko.dart index a10e11e820..edcaacb157 100644 --- a/lib/l10n/l10n_ko.dart +++ b/lib/l10n/l10n_ko.dart @@ -14,9 +14,6 @@ class AppLocalizationsKo extends AppLocalizations { @override String get mobileAreYouSure => '확실하십니까?'; - @override - String get mobileBlindfoldMode => '기물 가리기'; - @override String get mobileCancelTakebackOffer => '무르기 요청 취소'; @@ -133,7 +130,7 @@ class AppLocalizationsKo extends AppLocalizations { String get mobileSystemColors => '시스템 색상'; @override - String get mobileTheme => 'Theme'; + String get mobileTheme => '테마'; @override String get mobileToolsTab => '도구'; @@ -223,7 +220,7 @@ class AppLocalizationsKo extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count개의 통신전에서', + other: '$count개의 통신 대국에서', ); return '$_temp0'; } @@ -233,7 +230,7 @@ class AppLocalizationsKo extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count 번의 통신전을 완료하셨습니다.', + other: '$count번의 통신 대국을 완료함', ); return '$_temp0'; } @@ -243,7 +240,7 @@ class AppLocalizationsKo extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count $param2 긴 대국전을 완료함', + other: '$count번의 $param2 통신 대국을 완료함', ); return '$_temp0'; } @@ -293,7 +290,7 @@ class AppLocalizationsKo extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '공부 $count개 작성함', + other: '새 연구 $count개 작성함', ); return '$_temp0'; } @@ -360,7 +357,7 @@ class AppLocalizationsKo extends AppLocalizations { String get broadcastAboutBroadcasts => '방송에 대해서'; @override - String get broadcastHowToUseLichessBroadcasts => '리체스 방송을 사용하는 방법.'; + String get broadcastHowToUseLichessBroadcasts => 'Lichess 방송을 사용하는 방법.'; @override String get broadcastTheNewRoundHelp => '새로운 라운드에는 이전 라운드와 동일한 구성원과 기여자가 있을 것입니다.'; @@ -436,7 +433,7 @@ class AppLocalizationsKo extends AppLocalizations { String get broadcastDeleteAllGamesOfThisRound => '이 라운드의 모든 게임을 삭제합니다. 다시 생성하려면 소스가 활성화되어 있어야 합니다.'; @override - String get broadcastEditRoundStudy => '경기 공부 편집'; + String get broadcastEditRoundStudy => '경기 연구 편집'; @override String get broadcastDeleteTournament => '이 토너먼트 삭제'; @@ -521,7 +518,7 @@ class AppLocalizationsKo extends AppLocalizations { String get broadcastStandings => '순위'; @override - String get broadcastOfficialStandings => 'Official Standings'; + String get broadcastOfficialStandings => '공식 순위'; @override String broadcastIframeHelp(String param) { @@ -541,7 +538,7 @@ class AppLocalizationsKo extends AppLocalizations { @override String broadcastEmbedThisRound(String param) { - return '$param을(를) 웹사이트에 삼입하세요'; + return '$param을(를) 웹사이트에 삽입하세요'; } @override @@ -554,34 +551,31 @@ class AppLocalizationsKo extends AppLocalizations { String get broadcastScore => '점수'; @override - String get broadcastAllTeams => 'All teams'; - - @override - String get broadcastTournamentFormat => 'Tournament format'; + String get broadcastAllTeams => '모든 팀'; @override - String get broadcastTournamentLocation => 'Tournament Location'; + String get broadcastTournamentFormat => '토너먼트 형식'; @override - String get broadcastTopPlayers => 'Top players'; + String get broadcastTournamentLocation => '토너먼트 장소'; @override - String get broadcastTimezone => 'Time zone'; + String get broadcastTopPlayers => '상위 플레이어들'; @override - String get broadcastFideRatingCategory => 'FIDE rating category'; + String get broadcastTimezone => '시간대'; @override - String get broadcastOptionalDetails => 'Optional details'; + String get broadcastFideRatingCategory => 'FIDE 레이팅 범주'; @override - String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + String get broadcastOptionalDetails => '선택적 세부 정보'; @override - String get broadcastPastBroadcasts => 'Past broadcasts'; + String get broadcastPastBroadcasts => '과거 방송들'; @override - String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + String get broadcastAllBroadcastsByMonth => '월별 방송들 모두 보기'; @override String broadcastNbBroadcasts(int count) { @@ -645,13 +639,13 @@ class AppLocalizationsKo extends AppLocalizations { String get challengeDeclineLater => '시간이 맞지 않습니다. 나중에 다시 요청해주세요.'; @override - String get challengeDeclineTooFast => '시간이 너무 짧습니다. 더 긴 게임으로 신청해주세요.'; + String get challengeDeclineTooFast => '시간이 너무 짧습니다. 더 느린 게임으로 신청해주세요.'; @override - String get challengeDeclineTooSlow => '시간이 너무 깁니다. 더 빠른 게임으로 신청해주세요.'; + String get challengeDeclineTooSlow => '시간이 너무 깁니다. 더 빠른 게임으로 다시 신청해주세요.'; @override - String get challengeDeclineTimeControl => '이 시간으로는 도전을 받지 않습니다.'; + String get challengeDeclineTimeControl => '이 시간 제한으로는 도전을 받지 않겠습니다.'; @override String get challengeDeclineRated => '대신 레이팅 대전을 신청해주세요.'; @@ -798,10 +792,10 @@ class AppLocalizationsKo extends AppLocalizations { String get preferencesDisplay => '화면'; @override - String get preferencesPrivacy => '프라이버시'; + String get preferencesPrivacy => '보안'; @override - String get preferencesNotifications => '공지 사항'; + String get preferencesNotifications => '알림'; @override String get preferencesPieceAnimation => '기물 움직임 애니메이션'; @@ -819,13 +813,13 @@ class AppLocalizationsKo extends AppLocalizations { String get preferencesBoardCoordinates => '보드 좌표 (A-H, 1-8)'; @override - String get preferencesMoveListWhilePlaying => '피스 움직임 기록'; + String get preferencesMoveListWhilePlaying => '기물 움직임 기록'; @override String get preferencesPgnPieceNotation => 'PGN 기물표기방식'; @override - String get preferencesChessPieceSymbol => '체스 말 기호'; + String get preferencesChessPieceSymbol => '체스 기물 기호'; @override String get preferencesPgnLetter => '알파벳 (K, Q, R, B, N)'; @@ -837,7 +831,7 @@ class AppLocalizationsKo extends AppLocalizations { String get preferencesShowPlayerRatings => '플레이어 레이팅 보기'; @override - String get preferencesShowFlairs => '플레이어 레이팅 보기'; + String get preferencesShowFlairs => '플레이어 아이콘 보기'; @override String get preferencesExplainShowPlayerRatings => '체스에 집중할 수 있도록 웹사이트에서 레이팅을 모두 숨깁니다. 경기는 여전히 레이팅에 반영될 것이며, 눈으로 보이는 정보에만 영향을 줍니다.'; @@ -885,7 +879,7 @@ class AppLocalizationsKo extends AppLocalizations { String get preferencesBothClicksAndDrag => '아무 방법으로'; @override - String get preferencesPremovesPlayingDuringOpponentTurn => '미리두기 (상대 턴일 때 수를 두기)'; + String get preferencesPremovesPlayingDuringOpponentTurn => '미리두기 (상대 차례일 때 수를 두기)'; @override String get preferencesTakebacksWithOpponentApproval => '무르기 (상대 승인과 함께)'; @@ -900,7 +894,7 @@ class AppLocalizationsKo extends AppLocalizations { String get preferencesExplainPromoteToQueenAutomatically => '일시적으로 자동 승진을 끄기 위해 승진하는 동안 를 누르세요'; @override - String get preferencesWhenPremoving => '미리둘 때만'; + String get preferencesWhenPremoving => '미리두기 때만'; @override String get preferencesClaimDrawOnThreefoldRepetitionAutomatically => '3회 동형반복시 자동으로 무승부 요청'; @@ -909,16 +903,16 @@ class AppLocalizationsKo extends AppLocalizations { String get preferencesWhenTimeRemainingLessThanThirtySeconds => '남은 시간이 30초 미만일 때만'; @override - String get preferencesMoveConfirmation => '피스를 움직이기 전에 물음'; + String get preferencesMoveConfirmation => '수 확인'; @override String get preferencesExplainCanThenBeTemporarilyDisabled => '경기 도중 보드 메뉴에서 비활성화될 수 있습니다.'; @override - String get preferencesInCorrespondenceGames => '통신전'; + String get preferencesInCorrespondenceGames => '통신 대국'; @override - String get preferencesCorrespondenceAndUnlimited => '통신과 무제한'; + String get preferencesCorrespondenceAndUnlimited => '통신 대국과 무제한'; @override String get preferencesConfirmResignationAndDrawOffers => '기권 또는 무승부 제안시 물음'; @@ -927,7 +921,7 @@ class AppLocalizationsKo extends AppLocalizations { String get preferencesCastleByMovingTheKingTwoSquaresOrOntoTheRook => '캐슬링 방법'; @override - String get preferencesCastleByMovingTwoSquares => '왕을 2칸 옮기기'; + String get preferencesCastleByMovingTwoSquares => '킹을 2칸 옮기기'; @override String get preferencesCastleByMovingOntoTheRook => '킹을 룩한테 이동'; @@ -963,10 +957,10 @@ class AppLocalizationsKo extends AppLocalizations { String get preferencesNotifyForumMention => '포럼 댓글에서 당신이 언급됨'; @override - String get preferencesNotifyInvitedStudy => '스터디 초대'; + String get preferencesNotifyInvitedStudy => '연구 초대'; @override - String get preferencesNotifyGameEvent => '통신전 업데이트'; + String get preferencesNotifyGameEvent => '통신 대국 업데이트'; @override String get preferencesNotifyChallenge => '도전 과제'; @@ -975,13 +969,13 @@ class AppLocalizationsKo extends AppLocalizations { String get preferencesNotifyTournamentSoon => '곧 토너먼트 시작할 때'; @override - String get preferencesNotifyTimeAlarm => '통신전 시간 곧 만료됨'; + String get preferencesNotifyTimeAlarm => '통신 대국 시간 곧 만료됨'; @override - String get preferencesNotifyBell => '리체스 내에서 벨 알림'; + String get preferencesNotifyBell => 'Lichess 내에서 벨 알림'; @override - String get preferencesNotifyPush => '리체스를 사용하지 않을 때 기기 알림'; + String get preferencesNotifyPush => 'Lichess를 사용하지 않을 때 기기 알림'; @override String get preferencesNotifyWeb => '브라우저'; @@ -992,6 +986,9 @@ class AppLocalizationsKo extends AppLocalizations { @override String get preferencesBellNotificationSound => '벨 알림 음'; + @override + String get preferencesBlindfold => '기물 가리기'; + @override String get puzzlePuzzles => '퍼즐'; @@ -1041,7 +1038,7 @@ class AppLocalizationsKo extends AppLocalizations { String get puzzleYourPuzzleRatingWillNotChange => '당신의 퍼즐 레이팅은 바뀌지 않을 것입니다. 퍼즐은 경쟁이 아니라는 걸 기억하세요. 레이팅은 당신의 현재 수준에 맞는 퍼즐을 선택하도록 돕습니다.'; @override - String get puzzleFindTheBestMoveForWhite => '백의 최고의 수를 찾아보세요.'; + String get puzzleFindTheBestMoveForWhite => '백의 최선 수를 찾아보세요.'; @override String get puzzleFindTheBestMoveForBlack => '흑의 최선 수를 찾아보세요.'; @@ -1799,10 +1796,10 @@ class AppLocalizationsKo extends AppLocalizations { String get theFirstPersonToComeOnThisUrlWillPlayWithYou => '이 URL로 가장 먼저 들어온 사람과 체스를 두게 됩니다.'; @override - String get whiteResigned => '백 기권함'; + String get whiteResigned => '백이 기권하였습니다'; @override - String get blackResigned => '흑 기권함'; + String get blackResigned => '흑이 기권하였습니다'; @override String get whiteLeftTheGame => '백이 게임을 나갔습니다'; @@ -1811,7 +1808,7 @@ class AppLocalizationsKo extends AppLocalizations { String get blackLeftTheGame => '흑이 게임을 나갔습니다'; @override - String get whiteDidntMove => '백이 두지 않음'; + String get whiteDidntMove => '백이 수를 두지 않음'; @override String get blackDidntMove => '흑이 수를 두지 않음'; @@ -1823,7 +1820,7 @@ class AppLocalizationsKo extends AppLocalizations { String get computerAnalysis => '컴퓨터 분석'; @override - String get computerAnalysisAvailable => '컴퓨터 분석이 가능합니다.'; + String get computerAnalysisAvailable => '컴퓨터 분석 가능'; @override String get computerAnalysisDisabled => '컴퓨터 분석 비활성화됨'; @@ -1861,10 +1858,10 @@ class AppLocalizationsKo extends AppLocalizations { String get inLocalBrowser => '브라우저에서'; @override - String get toggleLocalEvaluation => '로컬 분석 토글'; + String get toggleLocalEvaluation => '로컬 분석 전환'; @override - String get promoteVariation => '변형 승격'; + String get promoteVariation => '바리에이션 승격하기'; @override String get makeMainLine => '주 라인으로 하기'; @@ -1879,10 +1876,10 @@ class AppLocalizationsKo extends AppLocalizations { String get expandVariations => '바리에이션 확장하기'; @override - String get forceVariation => '변화 강제하기'; + String get forceVariation => '바리에이션 강제하기'; @override - String get copyVariationPgn => '변동 PGN 복사'; + String get copyVariationPgn => '바리에이션 PGN 복사'; @override String get move => '수'; @@ -2096,7 +2093,7 @@ class AppLocalizationsKo extends AppLocalizations { @override String xPostedInForumY(String param1, String param2) { - return '$param1(이)가 $param2 쓰레드에 글을 씀'; + return '$param1(이)가 $param2 주제에 글을 씀'; } @override @@ -2121,7 +2118,7 @@ class AppLocalizationsKo extends AppLocalizations { String get yesterday => '어제'; @override - String get minutesPerSide => '양쪽 시간(분)'; + String get minutesPerSide => '제한 시간(분)'; @override String get variant => '게임 종류'; @@ -2130,13 +2127,13 @@ class AppLocalizationsKo extends AppLocalizations { String get variants => '변형'; @override - String get timeControl => '제한 시간(분)'; + String get timeControl => '시간 제한'; @override - String get realTime => '차례 없음'; + String get realTime => '실시간'; @override - String get correspondence => '긴 대국'; + String get correspondence => '통신 대국'; @override String get daysPerTurn => '수당 일수'; @@ -2154,7 +2151,7 @@ class AppLocalizationsKo extends AppLocalizations { String get ratingStats => '레이팅 통계'; @override - String get username => '유저네임'; + String get username => '사용자 이름'; @override String get usernameOrEmail => '사용자 이름이나 이메일 주소'; @@ -2295,7 +2292,7 @@ class AppLocalizationsKo extends AppLocalizations { String get gamesPlayed => '게임'; @override - String get ok => 'OK'; + String get ok => '확인'; @override String get cancel => '취소'; @@ -2608,7 +2605,7 @@ class AppLocalizationsKo extends AppLocalizations { String get importGameExplanation => '게임의 PGN 을 붙여넣으면, 브라우저에서의 리플레이, 컴퓨터 해석, 게임챗, 공유가능 URL을 얻습니다.'; @override - String get importGameCaveat => '변형은 지워집니다. 변형을 유지하려면 스터디를 통해 PGN을 가져오세요.'; + String get importGameCaveat => '변형은 지워집니다. 변형을 유지하려면 연구를 통해 PGN을 가져오세요.'; @override String get importGameDataPrivacyWarning => '이 PGN은 모두가 볼 수 있게 됩니다. 비공개로 게임을 불러오려면, 연구 기능을 이용하세요.'; @@ -2892,7 +2889,7 @@ class AppLocalizationsKo extends AppLocalizations { String get inlineNotation => '기보법 가로쓰기'; @override - String get makeAStudy => '안전하게 보관하고 공유하려면 공부를 만들어 보세요.'; + String get makeAStudy => '안전하게 보관하고 공유하려면 연구를 만들어 보세요.'; @override String get clearSavedMoves => '저장된 움직임 삭제'; @@ -3088,7 +3085,7 @@ class AppLocalizationsKo extends AppLocalizations { String get letOtherPlayersChallengeYou => '다른 사람이 나에게 도전할 수 있게 함'; @override - String get letOtherPlayersInviteYouToStudy => '다른 플레이어들이 나를 학습에 초대할 수 있음'; + String get letOtherPlayersInviteYouToStudy => '다른 플레이어들이 나를 연구에 초대할 수 있음'; @override String get sound => '소리'; @@ -3183,7 +3180,7 @@ class AppLocalizationsKo extends AppLocalizations { String get learnMenu => '배우기'; @override - String get studyMenu => '공부'; + String get studyMenu => '연구'; @override String get practice => '연습'; @@ -3461,7 +3458,7 @@ class AppLocalizationsKo extends AppLocalizations { String get newTournament => '새로운 토너먼트'; @override - String get tournamentHomeTitle => '다양한 제한시간과 게임방식을 지원하는 체스 토너먼트'; + String get tournamentHomeTitle => '다양한 시간 제한과 변형을 지원하는 체스 토너먼트'; @override String get tournamentHomeDescription => '빠른 체스 토너먼트를 즐겨 보세요! 공식 일정이 잡힌 토너먼트에 참가할 수도, 당신만의 토너먼트를 만들 수도 있습니다. 불릿, 블리츠, 클래식, 체스960, 언덕의 왕, 3체크를 비롯하여 다양한 게임방식을 즐길 수 있습니다.'; @@ -3643,7 +3640,7 @@ class AppLocalizationsKo extends AppLocalizations { String get bulletBlitzClassical => '불릿, 블리츠, 클래식 방식 지원'; @override - String get correspondenceChess => '긴 대국 체스'; + String get correspondenceChess => '통신 체스'; @override String get onlineAndOfflinePlay => '온라인/오프라인 게임 모두 지원'; @@ -3865,13 +3862,13 @@ class AppLocalizationsKo extends AppLocalizations { String get waitingForAnalysis => '분석을 기다리는 중'; @override - String get noMistakesFoundForWhite => '백에게 악수는 없었습니다'; + String get noMistakesFoundForWhite => '백에게 실수는 없었습니다'; @override String get noMistakesFoundForBlack => '흑에게 실수는 없었습니다'; @override - String get doneReviewingWhiteMistakes => '백의 악수 체크가 종료됨'; + String get doneReviewingWhiteMistakes => '백의 실수 탐색이 종료됨'; @override String get doneReviewingBlackMistakes => '흑의 실수 탐색이 종료됨'; @@ -3880,10 +3877,10 @@ class AppLocalizationsKo extends AppLocalizations { String get doItAgain => '다시 하기'; @override - String get reviewWhiteMistakes => '백의 악수를 체크'; + String get reviewWhiteMistakes => '백의 실수 탐색하기'; @override - String get reviewBlackMistakes => '흑의 실수 리뷰'; + String get reviewBlackMistakes => '흑의 실수 탐색하기'; @override String get advantage => '이점'; @@ -3915,7 +3912,7 @@ class AppLocalizationsKo extends AppLocalizations { } @override - String get showUnreadLichessMessage => '리체스로부터 비공개 메시지를 받았습니다.'; + String get showUnreadLichessMessage => 'Lichess로부터 비공개 메시지를 받았습니다.'; @override String get clickHereToReadIt => '클릭하여 읽기'; @@ -4013,7 +4010,7 @@ class AppLocalizationsKo extends AppLocalizations { String get classicalDesc => '클래시컬 게임: 25분 이상'; @override - String get correspondenceDesc => '통신전: 한 수당 하루 또는 수 일'; + String get correspondenceDesc => '통신 대국: 한 수당 하루 또는 며칠'; @override String get puzzleDesc => '체스 전술 트레이너'; @@ -4039,7 +4036,7 @@ class AppLocalizationsKo extends AppLocalizations { @override String toRequestSupport(String param1) { - return '$param1에서 리체스에 문의하실 수 있습니다.'; + return '$param1에서 문의하실 수 있습니다.'; } @override @@ -4205,7 +4202,7 @@ class AppLocalizationsKo extends AppLocalizations { } @override - String get embedsAvailable => '포함할 게임 URL 또는 스터디 챕터 URL을 붙여넣으세요.'; + String get embedsAvailable => '포함할 게임 URL 또는 연구 챕터 URL을 붙여넣으세요.'; @override String get inYourLocalTimezone => '본인의 현지 시간대 기준'; @@ -4291,7 +4288,7 @@ class AppLocalizationsKo extends AppLocalizations { String get until => '까지'; @override - String get lichessDbExplanation => '모든 리체스 플레이어의 레이팅 게임 샘플'; + String get lichessDbExplanation => '모든 Lichess 플레이어의 레이팅 게임 샘플'; @override String get switchSides => '색 바꾸기'; @@ -4542,7 +4539,7 @@ class AppLocalizationsKo extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '$count 공부', + other: '$count 연구', ); return '$_temp0'; } @@ -4923,29 +4920,29 @@ class AppLocalizationsKo extends AppLocalizations { String get studyPrivate => '비공개'; @override - String get studyMyStudies => '내 공부'; + String get studyMyStudies => '내 연구'; @override - String get studyStudiesIContributeTo => '내가 기여한 공부'; + String get studyStudiesIContributeTo => '내가 기여한 연구'; @override - String get studyMyPublicStudies => '내 공개 공부'; + String get studyMyPublicStudies => '내 공개 연구'; @override - String get studyMyPrivateStudies => '내 개인 공부'; + String get studyMyPrivateStudies => '내 비공개 연구'; @override - String get studyMyFavoriteStudies => '내가 즐겨찾는 공부'; + String get studyMyFavoriteStudies => '내가 즐겨찾는 연구'; @override - String get studyWhatAreStudies => '공부가 무엇인가요?'; + String get studyWhatAreStudies => '연구란 무엇인가요?'; @override - String get studyAllStudies => '모든 공부'; + String get studyAllStudies => '모든 연구'; @override String studyStudiesCreatedByX(String param) { - return '$param이(가) 만든 공부'; + return '$param이(가) 만든 연구'; } @override @@ -4976,10 +4973,10 @@ class AppLocalizationsKo extends AppLocalizations { String get studyAddMembers => '멤버 추가'; @override - String get studyInviteToTheStudy => '공부에 초대'; + String get studyInviteToTheStudy => '연구에 초대'; @override - String get studyPleaseOnlyInvitePeopleYouKnow => '당신이 아는 사람들이나 공부에 적극적으로 참여하고 싶은 사람들만 초대하세요.'; + String get studyPleaseOnlyInvitePeopleYouKnow => '당신이 아는 사람들이나 연구에 적극적으로 참여하고 싶은 사람들만 초대하세요.'; @override String get studySearchByUsername => '사용자 이름으로 검색'; @@ -4994,7 +4991,7 @@ class AppLocalizationsKo extends AppLocalizations { String get studyKick => '강제 퇴장'; @override - String get studyLeaveTheStudy => '공부 나가기'; + String get studyLeaveTheStudy => '연구 나가기'; @override String get studyYouAreNowAContributor => '당신은 이제 기여자입니다'; @@ -5027,7 +5024,7 @@ class AppLocalizationsKo extends AppLocalizations { String get studyTheChapterIsTooShortToBeAnalysed => '분석되기 너무 짧은 챕터입니다.'; @override - String get studyOnlyContributorsCanRequestAnalysis => '공부 기여자들만이 컴퓨터 분석을 요청할 수 있습니다.'; + String get studyOnlyContributorsCanRequestAnalysis => '연구 기여자만이 컴퓨터 분석을 요청할 수 있습니다.'; @override String get studyGetAFullComputerAnalysis => '메인라인에 대한 전체 서버 컴퓨터 분석을 가져옵니다.'; @@ -5066,7 +5063,7 @@ class AppLocalizationsKo extends AppLocalizations { String get studyCloneStudy => '복제'; @override - String get studyStudyPgn => '공부 PGN'; + String get studyStudyPgn => '연구 PGN'; @override String get studyDownloadAllGames => '모든 게임 다운로드'; @@ -5081,7 +5078,7 @@ class AppLocalizationsKo extends AppLocalizations { String get studyDownloadGame => '게임 다운로드'; @override - String get studyStudyUrl => '공부 URL'; + String get studyStudyUrl => '연구 URL'; @override String get studyCurrentChapterUrl => '현재 챕터 URL'; @@ -5104,7 +5101,7 @@ class AppLocalizationsKo extends AppLocalizations { String get studyReadMoreAboutEmbedding => '공유에 대한 상세 정보'; @override - String get studyOnlyPublicStudiesCanBeEmbedded => '공개 공부들만 공유할 수 있습니다!'; + String get studyOnlyPublicStudiesCanBeEmbedded => '공개 연구만 공유할 수 있습니다!'; @override String get studyOpen => '열기'; @@ -5115,7 +5112,7 @@ class AppLocalizationsKo extends AppLocalizations { } @override - String get studyStudyNotFound => '공부를 찾을 수 없습니다'; + String get studyStudyNotFound => '연구를 찾을 수 없음'; @override String get studyEditChapter => '챕터 편집하기'; @@ -5144,7 +5141,7 @@ class AppLocalizationsKo extends AppLocalizations { String get studyClearAnnotations => '주석 지우기'; @override - String get studyClearVariations => '파생 초기화'; + String get studyClearVariations => '바리에이션 초기화'; @override String get studyDeleteChapter => '챕터 지우기'; @@ -5211,10 +5208,10 @@ class AppLocalizationsKo extends AppLocalizations { String get studyCreateChapter => '챕터 만들기'; @override - String get studyCreateStudy => '공부 만들기'; + String get studyCreateStudy => '연구 만들기'; @override - String get studyEditStudy => '공부 편집하기'; + String get studyEditStudy => '연구 편집하기'; @override String get studyVisibility => '공개 설정'; @@ -5268,18 +5265,18 @@ class AppLocalizationsKo extends AppLocalizations { String get studyClearChat => '채팅 기록 지우기'; @override - String get studyDeleteTheStudyChatHistory => '공부 채팅 히스토리를 지울까요? 되돌릴 수 없습니다!'; + String get studyDeleteTheStudyChatHistory => '연구 채팅 기록을 삭제할까요? 되돌릴 수 없습니다!'; @override - String get studyDeleteStudy => '공부 삭제'; + String get studyDeleteStudy => '연구 삭제'; @override String studyConfirmDeleteStudy(String param) { - return '모든 공부를 삭제할까요? 복구할 수 없습니다! 확인을 위해서 공부의 이름을 입력하세요: $param'; + return '모든 연구를 삭제할까요? 복구할 수 없습니다! 확인을 위해서 연구의 이름을 입력하세요: $param'; } @override - String get studyWhereDoYouWantToStudyThat => '어디에서 공부하시겠습니까?'; + String get studyWhereDoYouWantToStudyThat => '어디에서 연구를 시작하시겠습니까?'; @override String get studyGoodMove => '좋은 수'; @@ -5342,7 +5339,7 @@ class AppLocalizationsKo extends AppLocalizations { String get studyAttack => '공격'; @override - String get studyCounterplay => '카운터플레이'; + String get studyCounterplay => '반격'; @override String get studyTimeTrouble => '시간이 부족함'; @@ -5360,7 +5357,7 @@ class AppLocalizationsKo extends AppLocalizations { String get studyPrevChapter => '이전 챕터'; @override - String get studyStudyActions => '공부 액션'; + String get studyStudyActions => '연구 작업'; @override String get studyTopics => '주제'; @@ -5388,7 +5385,7 @@ class AppLocalizationsKo extends AppLocalizations { @override String studyPerPage(String param) { - return '$param per page'; + return '페이지 당 $param개'; } @override diff --git a/lib/l10n/l10n_lb.dart b/lib/l10n/l10n_lb.dart index a916e11a70..a226306021 100644 --- a/lib/l10n/l10n_lb.dart +++ b/lib/l10n/l10n_lb.dart @@ -14,9 +14,6 @@ class AppLocalizationsLb extends AppLocalizations { @override String get mobileAreYouSure => 'Bass de sécher?'; - @override - String get mobileBlindfoldMode => 'Blann'; - @override String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @@ -539,7 +536,7 @@ class AppLocalizationsLb extends AppLocalizations { String get broadcastStandings => 'Standings'; @override - String get broadcastOfficialStandings => 'Official Standings'; + String get broadcastOfficialStandings => 'Offizielle Stand'; @override String broadcastIframeHelp(String param) { @@ -572,28 +569,25 @@ class AppLocalizationsLb extends AppLocalizations { String get broadcastScore => 'Score'; @override - String get broadcastAllTeams => 'All teams'; - - @override - String get broadcastTournamentFormat => 'Tournament format'; + String get broadcastAllTeams => 'All Ekippen'; @override - String get broadcastTournamentLocation => 'Tournament Location'; + String get broadcastTournamentFormat => 'Turnéierformat'; @override - String get broadcastTopPlayers => 'Top players'; + String get broadcastTournamentLocation => 'Turnéierplaz'; @override - String get broadcastTimezone => 'Time zone'; + String get broadcastTopPlayers => 'Topspiller'; @override - String get broadcastFideRatingCategory => 'FIDE rating category'; + String get broadcastTimezone => 'Zäitzon'; @override - String get broadcastOptionalDetails => 'Optional details'; + String get broadcastFideRatingCategory => 'FIDE-Wäertungskategorie'; @override - String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + String get broadcastOptionalDetails => 'Fakultativ Detailler'; @override String get broadcastPastBroadcasts => 'Past broadcasts'; @@ -1011,6 +1005,9 @@ class AppLocalizationsLb extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Glacken-Notifikatiounstoun'; + @override + String get preferencesBlindfold => 'Blann'; + @override String get puzzlePuzzles => 'Aufgaben'; @@ -5456,7 +5453,7 @@ class AppLocalizationsLb extends AppLocalizations { @override String studyPerPage(String param) { - return '$param per page'; + return '$param pro Säit'; } @override diff --git a/lib/l10n/l10n_lt.dart b/lib/l10n/l10n_lt.dart index 447f0a1a4d..f0e557895f 100644 --- a/lib/l10n/l10n_lt.dart +++ b/lib/l10n/l10n_lt.dart @@ -14,9 +14,6 @@ class AppLocalizationsLt extends AppLocalizations { @override String get mobileAreYouSure => 'Are you sure?'; - @override - String get mobileBlindfoldMode => 'Blindfold'; - @override String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @@ -400,7 +397,7 @@ class AppLocalizationsLt extends AppLocalizations { String get broadcastLiveBroadcasts => 'Vykstančios turnyrų transliacijos'; @override - String get broadcastBroadcastCalendar => 'Broadcast calendar'; + String get broadcastBroadcastCalendar => 'Transliavimo kalendorius'; @override String get broadcastNewBroadcast => 'Nauja transliacija'; @@ -463,7 +460,7 @@ class AppLocalizationsLt extends AppLocalizations { @override String broadcastStartDateTimeZone(String param) { - return 'Start date in the tournament local timezone: $param'; + return 'Turnyro pradžia vietos laiku: $param'; } @override @@ -530,110 +527,107 @@ class AppLocalizationsLt extends AppLocalizations { String get broadcastRecentTournaments => 'Neseniai sukurti turnyrai'; @override - String get broadcastOpenLichess => 'Open in Lichess'; + String get broadcastOpenLichess => 'Atverti Lichess-e'; @override - String get broadcastTeams => 'Teams'; + String get broadcastTeams => 'Komandos'; @override - String get broadcastBoards => 'Boards'; + String get broadcastBoards => 'Lentos'; @override - String get broadcastOverview => 'Overview'; + String get broadcastOverview => 'Apžvalga'; @override - String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; + String get broadcastSubscribeTitle => 'Užsakykite pranešimą apie kiekvieno turo pradžią. Paskyros nustatymuose galite perjungti transliacijų skambėjimo signalą arba tiesioginius pranešimus.'; @override - String get broadcastUploadImage => 'Upload tournament image'; + String get broadcastUploadImage => 'Įkelkite turnyro paveikslėlį'; @override - String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + String get broadcastNoBoardsYet => 'Dar nėra lentų. Jos bus rodomos, kai bus įkeltos partijos.'; @override String broadcastBoardsCanBeLoaded(String param) { - return 'Boards can be loaded with a source or via the $param'; + return 'Lentas galima įkelti iš šaltinio arba per $param'; } @override String broadcastStartsAfter(String param) { - return 'Starts after $param'; + return 'Pradedama po $param'; } @override - String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + String get broadcastStartVerySoon => 'Transliacija prasidės visai netrukus.'; @override - String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + String get broadcastNotYetStarted => 'Transliacija dar neprasidėjo.'; @override - String get broadcastOfficialWebsite => 'Official website'; + String get broadcastOfficialWebsite => 'Oficialus tinklapis'; @override - String get broadcastStandings => 'Standings'; + String get broadcastStandings => 'Rezultatai'; @override - String get broadcastOfficialStandings => 'Official Standings'; + String get broadcastOfficialStandings => 'Oficialūs rezultatai'; @override String broadcastIframeHelp(String param) { - return 'More options on the $param'; + return 'Daugiau parinkčių $param'; } @override - String get broadcastWebmastersPage => 'webmasters page'; + String get broadcastWebmastersPage => 'žiniatinklio valdytojų puslapis'; @override String broadcastPgnSourceHelp(String param) { - return 'A public, real-time PGN source for this round. We also offer a $param for faster and more efficient synchronisation.'; + return 'Viešas realaus laiko PGN šaltinis šiam turui. Taip pat siūlome $param greitesniam ir efektyvesniam sinchronizavimui.'; } @override - String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + String get broadcastEmbedThisBroadcast => 'Įterpkite šią transliaciją į savo svetainę'; @override String broadcastEmbedThisRound(String param) { - return 'Embed $param in your website'; + return 'Įterpkite $param į savo svetainę'; } @override - String get broadcastRatingDiff => 'Rating diff'; - - @override - String get broadcastGamesThisTournament => 'Games in this tournament'; + String get broadcastRatingDiff => 'Reitingo skirtumas'; @override - String get broadcastScore => 'Score'; + String get broadcastGamesThisTournament => 'Partijos šiame turnyre'; @override - String get broadcastAllTeams => 'All teams'; + String get broadcastScore => 'Taškų skaičius'; @override - String get broadcastTournamentFormat => 'Tournament format'; + String get broadcastAllTeams => 'Visos komandos'; @override - String get broadcastTournamentLocation => 'Tournament Location'; + String get broadcastTournamentFormat => 'Turnyro formatas'; @override - String get broadcastTopPlayers => 'Top players'; + String get broadcastTournamentLocation => 'Turnyro vieta'; @override - String get broadcastTimezone => 'Time zone'; + String get broadcastTopPlayers => 'Geriausi žaidėjai'; @override - String get broadcastFideRatingCategory => 'FIDE rating category'; + String get broadcastTimezone => 'Laiko juosta'; @override - String get broadcastOptionalDetails => 'Optional details'; + String get broadcastFideRatingCategory => 'FIDE reitingo kategorija'; @override - String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + String get broadcastOptionalDetails => 'Papildoma informacija'; @override - String get broadcastPastBroadcasts => 'Past broadcasts'; + String get broadcastPastBroadcasts => 'Ankstesnės transliacijos'; @override - String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + String get broadcastAllBroadcastsByMonth => 'Rodyti visas transliacijas pagal mėnesį'; @override String broadcastNbBroadcasts(int count) { @@ -1047,6 +1041,9 @@ class AppLocalizationsLt extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Pranešimų varpelio garsas'; + @override + String get preferencesBlindfold => 'Blindfold'; + @override String get puzzlePuzzles => 'Užduotys'; @@ -4385,7 +4382,7 @@ class AppLocalizationsLt extends AppLocalizations { String get nothingToSeeHere => 'Nieko naujo.'; @override - String get stats => 'Stats'; + String get stats => 'Statistika'; @override String opponentLeftCounter(int count) { @@ -5590,7 +5587,7 @@ class AppLocalizationsLt extends AppLocalizations { @override String studyPerPage(String param) { - return '$param per page'; + return '$param puslapyje'; } @override diff --git a/lib/l10n/l10n_lv.dart b/lib/l10n/l10n_lv.dart index ecca060c73..e2a9300ac3 100644 --- a/lib/l10n/l10n_lv.dart +++ b/lib/l10n/l10n_lv.dart @@ -14,9 +14,6 @@ class AppLocalizationsLv extends AppLocalizations { @override String get mobileAreYouSure => 'Are you sure?'; - @override - String get mobileBlindfoldMode => 'Blindfold'; - @override String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @@ -609,9 +606,6 @@ class AppLocalizationsLv extends AppLocalizations { @override String get broadcastOptionalDetails => 'Optional details'; - @override - String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; - @override String get broadcastPastBroadcasts => 'Past broadcasts'; @@ -1028,6 +1022,9 @@ class AppLocalizationsLv extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Paziņojumu skaņa'; + @override + String get preferencesBlindfold => 'Blindfold'; + @override String get puzzlePuzzles => 'Uzdevumi'; diff --git a/lib/l10n/l10n_mk.dart b/lib/l10n/l10n_mk.dart index b6e23ab136..04034cc1dc 100644 --- a/lib/l10n/l10n_mk.dart +++ b/lib/l10n/l10n_mk.dart @@ -14,9 +14,6 @@ class AppLocalizationsMk extends AppLocalizations { @override String get mobileAreYouSure => 'Are you sure?'; - @override - String get mobileBlindfoldMode => 'Blindfold'; - @override String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @@ -592,9 +589,6 @@ class AppLocalizationsMk extends AppLocalizations { @override String get broadcastOptionalDetails => 'Optional details'; - @override - String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; - @override String get broadcastPastBroadcasts => 'Past broadcasts'; @@ -1011,6 +1005,9 @@ class AppLocalizationsMk extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Bell notification sound'; + @override + String get preferencesBlindfold => 'Blindfold'; + @override String get puzzlePuzzles => 'Загатки'; @@ -5122,7 +5119,7 @@ class AppLocalizationsMk extends AppLocalizations { String get studyPrevious => 'Previous'; @override - String get studyNext => 'Next'; + String get studyNext => 'Следно'; @override String get studyLast => 'Last'; @@ -5166,7 +5163,7 @@ class AppLocalizationsMk extends AppLocalizations { } @override - String get studyEmbedInYourWebsite => 'Embed in your website'; + String get studyEmbedInYourWebsite => 'Вгради во твојот сајт'; @override String get studyReadMoreAboutEmbedding => 'Read more about embedding'; @@ -5330,7 +5327,7 @@ class AppLocalizationsMk extends AppLocalizations { String get studyStart => 'Start'; @override - String get studySave => 'Save'; + String get studySave => 'Зачувај'; @override String get studyClearChat => 'Clear chat'; @@ -5350,16 +5347,16 @@ class AppLocalizationsMk extends AppLocalizations { String get studyWhereDoYouWantToStudyThat => 'Where do you want to study that?'; @override - String get studyGoodMove => 'Good move'; + String get studyGoodMove => 'Добар потег'; @override - String get studyMistake => 'Mistake'; + String get studyMistake => 'Грешка'; @override String get studyBrilliantMove => 'Brilliant move'; @override - String get studyBlunder => 'Blunder'; + String get studyBlunder => 'Глупа грешка'; @override String get studyInterestingMove => 'Interesting move'; diff --git a/lib/l10n/l10n_nb.dart b/lib/l10n/l10n_nb.dart index 60b8513a24..4f4f77f6ea 100644 --- a/lib/l10n/l10n_nb.dart +++ b/lib/l10n/l10n_nb.dart @@ -14,9 +14,6 @@ class AppLocalizationsNb extends AppLocalizations { @override String get mobileAreYouSure => 'Er du sikker?'; - @override - String get mobileBlindfoldMode => 'Blindsjakk'; - @override String get mobileCancelTakebackOffer => 'Avbryt tilbud om å angre'; @@ -133,7 +130,7 @@ class AppLocalizationsNb extends AppLocalizations { String get mobileSystemColors => 'Systemfarger'; @override - String get mobileTheme => 'Theme'; + String get mobileTheme => 'Tema'; @override String get mobileToolsTab => 'Verktøy'; @@ -539,7 +536,7 @@ class AppLocalizationsNb extends AppLocalizations { String get broadcastStandings => 'Resultatliste'; @override - String get broadcastOfficialStandings => 'Official Standings'; + String get broadcastOfficialStandings => 'Offisiell tabell'; @override String broadcastIframeHelp(String param) { @@ -572,34 +569,31 @@ class AppLocalizationsNb extends AppLocalizations { String get broadcastScore => 'Poengsum'; @override - String get broadcastAllTeams => 'All teams'; - - @override - String get broadcastTournamentFormat => 'Tournament format'; + String get broadcastAllTeams => 'Alle lag'; @override - String get broadcastTournamentLocation => 'Tournament Location'; + String get broadcastTournamentFormat => 'Turneringsformat'; @override - String get broadcastTopPlayers => 'Top players'; + String get broadcastTournamentLocation => 'Turneringssted'; @override - String get broadcastTimezone => 'Time zone'; + String get broadcastTopPlayers => 'Toppspillere'; @override - String get broadcastFideRatingCategory => 'FIDE rating category'; + String get broadcastTimezone => 'Tidssone'; @override - String get broadcastOptionalDetails => 'Optional details'; + String get broadcastFideRatingCategory => 'FIDE-ratingkategori'; @override - String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + String get broadcastOptionalDetails => 'Valgfrie detaljer'; @override - String get broadcastPastBroadcasts => 'Past broadcasts'; + String get broadcastPastBroadcasts => 'Tidligere overføringer'; @override - String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + String get broadcastAllBroadcastsByMonth => 'Vis alle overføringer etter måned'; @override String broadcastNbBroadcasts(int count) { @@ -1011,6 +1005,9 @@ class AppLocalizationsNb extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Bjellevarsel med lyd'; + @override + String get preferencesBlindfold => 'Blindsjakk'; + @override String get puzzlePuzzles => 'Sjakknøtter'; @@ -5456,7 +5453,7 @@ class AppLocalizationsNb extends AppLocalizations { @override String studyPerPage(String param) { - return '$param per page'; + return '$param per side'; } @override diff --git a/lib/l10n/l10n_nl.dart b/lib/l10n/l10n_nl.dart index 5b5b6a0579..a2945123fd 100644 --- a/lib/l10n/l10n_nl.dart +++ b/lib/l10n/l10n_nl.dart @@ -14,9 +14,6 @@ class AppLocalizationsNl extends AppLocalizations { @override String get mobileAreYouSure => 'Weet je het zeker?'; - @override - String get mobileBlindfoldMode => 'Geblinddoekt'; - @override String get mobileCancelTakebackOffer => 'Terugnameaanbod annuleren'; @@ -133,7 +130,7 @@ class AppLocalizationsNl extends AppLocalizations { String get mobileSystemColors => 'Systeemkleuren'; @override - String get mobileTheme => 'Theme'; + String get mobileTheme => 'Thema'; @override String get mobileToolsTab => 'Gereedschap'; @@ -539,7 +536,7 @@ class AppLocalizationsNl extends AppLocalizations { String get broadcastStandings => 'Klassement'; @override - String get broadcastOfficialStandings => 'Official Standings'; + String get broadcastOfficialStandings => 'Officiële standen'; @override String broadcastIframeHelp(String param) { @@ -572,34 +569,31 @@ class AppLocalizationsNl extends AppLocalizations { String get broadcastScore => 'Score'; @override - String get broadcastAllTeams => 'All teams'; - - @override - String get broadcastTournamentFormat => 'Tournament format'; + String get broadcastAllTeams => 'Alle teams'; @override - String get broadcastTournamentLocation => 'Tournament Location'; + String get broadcastTournamentFormat => 'Toernooivorm'; @override - String get broadcastTopPlayers => 'Top players'; + String get broadcastTournamentLocation => 'Toernooilocatie'; @override - String get broadcastTimezone => 'Time zone'; + String get broadcastTopPlayers => 'Topspelers'; @override - String get broadcastFideRatingCategory => 'FIDE rating category'; + String get broadcastTimezone => 'Tijdzone'; @override - String get broadcastOptionalDetails => 'Optional details'; + String get broadcastFideRatingCategory => 'FIDE-rating categorie'; @override - String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + String get broadcastOptionalDetails => 'Optionele info'; @override - String get broadcastPastBroadcasts => 'Past broadcasts'; + String get broadcastPastBroadcasts => 'Afgelopen uitzendingen'; @override - String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + String get broadcastAllBroadcastsByMonth => 'Alle uitzendingen per maand weergeven'; @override String broadcastNbBroadcasts(int count) { @@ -1011,6 +1005,9 @@ class AppLocalizationsNl extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Meldingsgeluid'; + @override + String get preferencesBlindfold => 'Geblinddoekt'; + @override String get puzzlePuzzles => 'Puzzels'; @@ -2379,7 +2376,7 @@ class AppLocalizationsNl extends AppLocalizations { String get standard => 'Standaard'; @override - String get customPosition => 'Custom position'; + String get customPosition => 'Aangepaste positie'; @override String get unlimited => 'Onbeperkt'; @@ -5456,7 +5453,7 @@ class AppLocalizationsNl extends AppLocalizations { @override String studyPerPage(String param) { - return '$param per page'; + return '$param per pagina'; } @override diff --git a/lib/l10n/l10n_nn.dart b/lib/l10n/l10n_nn.dart index b13793d594..ab60607f82 100644 --- a/lib/l10n/l10n_nn.dart +++ b/lib/l10n/l10n_nn.dart @@ -14,9 +14,6 @@ class AppLocalizationsNn extends AppLocalizations { @override String get mobileAreYouSure => 'Er du sikker?'; - @override - String get mobileBlindfoldMode => 'Blindsjakk'; - @override String get mobileCancelTakebackOffer => 'Avbryt tilbud om angrerett'; @@ -133,7 +130,7 @@ class AppLocalizationsNn extends AppLocalizations { String get mobileSystemColors => 'Systemfargar'; @override - String get mobileTheme => 'Theme'; + String get mobileTheme => 'Tema'; @override String get mobileToolsTab => 'Verktøy'; @@ -415,7 +412,7 @@ class AppLocalizationsNn extends AppLocalizations { @override String broadcastFullDescriptionHelp(String param1, String param2) { - return 'Valfri lang omtale av overføringa. $param1 er tilgjengeleg. Omtalen må vera kortare enn $param2 teikn.'; + return 'Valfri lang omtale av turneringa. $param1 er tilgjengeleg. Omtalen må vera kortare enn $param2 teikn.'; } @override @@ -539,7 +536,7 @@ class AppLocalizationsNn extends AppLocalizations { String get broadcastStandings => 'Resultat'; @override - String get broadcastOfficialStandings => 'Official Standings'; + String get broadcastOfficialStandings => 'Offisiell tabell'; @override String broadcastIframeHelp(String param) { @@ -572,34 +569,31 @@ class AppLocalizationsNn extends AppLocalizations { String get broadcastScore => 'Poengskår'; @override - String get broadcastAllTeams => 'All teams'; - - @override - String get broadcastTournamentFormat => 'Tournament format'; + String get broadcastAllTeams => 'Alle lag'; @override - String get broadcastTournamentLocation => 'Tournament Location'; + String get broadcastTournamentFormat => 'Turneringsformat'; @override - String get broadcastTopPlayers => 'Top players'; + String get broadcastTournamentLocation => 'Turneringsstad'; @override - String get broadcastTimezone => 'Time zone'; + String get broadcastTopPlayers => 'Toppspelarar'; @override - String get broadcastFideRatingCategory => 'FIDE rating category'; + String get broadcastTimezone => 'Tidssone'; @override - String get broadcastOptionalDetails => 'Optional details'; + String get broadcastFideRatingCategory => 'FIDE-ratingkategori'; @override - String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + String get broadcastOptionalDetails => 'Valfrie detaljar'; @override - String get broadcastPastBroadcasts => 'Past broadcasts'; + String get broadcastPastBroadcasts => 'Tidlegare overføringar'; @override - String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + String get broadcastAllBroadcastsByMonth => 'Vis alle overføringar etter månad'; @override String broadcastNbBroadcasts(int count) { @@ -1011,6 +1005,9 @@ class AppLocalizationsNn extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Varsellyd'; + @override + String get preferencesBlindfold => 'Blindsjakk'; + @override String get puzzlePuzzles => 'Taktikkoppgåver'; @@ -2319,7 +2316,7 @@ class AppLocalizationsNn extends AppLocalizations { String get gamesPlayed => 'Spelte parti'; @override - String get ok => 'OK'; + String get ok => 'Ok'; @override String get cancel => 'Avbryt'; @@ -5456,7 +5453,7 @@ class AppLocalizationsNn extends AppLocalizations { @override String studyPerPage(String param) { - return '$param per page'; + return '$param per side'; } @override diff --git a/lib/l10n/l10n_pl.dart b/lib/l10n/l10n_pl.dart index c7d286ebb8..8a925c1515 100644 --- a/lib/l10n/l10n_pl.dart +++ b/lib/l10n/l10n_pl.dart @@ -14,9 +14,6 @@ class AppLocalizationsPl extends AppLocalizations { @override String get mobileAreYouSure => 'Jesteś pewien?'; - @override - String get mobileBlindfoldMode => 'Gra na ślepo'; - @override String get mobileCancelTakebackOffer => 'Anuluj prośbę cofnięcia ruchu'; @@ -133,7 +130,7 @@ class AppLocalizationsPl extends AppLocalizations { String get mobileSystemColors => 'Kolory systemowe'; @override - String get mobileTheme => 'Theme'; + String get mobileTheme => 'Motyw'; @override String get mobileToolsTab => 'Narzędzia'; @@ -575,7 +572,7 @@ class AppLocalizationsPl extends AppLocalizations { String get broadcastStandings => 'Klasyfikacja'; @override - String get broadcastOfficialStandings => 'Official Standings'; + String get broadcastOfficialStandings => 'Oficjalna klasyfikacja'; @override String broadcastIframeHelp(String param) { @@ -608,34 +605,31 @@ class AppLocalizationsPl extends AppLocalizations { String get broadcastScore => 'Wynik'; @override - String get broadcastAllTeams => 'All teams'; - - @override - String get broadcastTournamentFormat => 'Tournament format'; + String get broadcastAllTeams => 'Wszystkie kluby'; @override - String get broadcastTournamentLocation => 'Tournament Location'; + String get broadcastTournamentFormat => 'Format turnieju'; @override - String get broadcastTopPlayers => 'Top players'; + String get broadcastTournamentLocation => 'Lokalizacja turnieju'; @override - String get broadcastTimezone => 'Time zone'; + String get broadcastTopPlayers => 'Najlepsi gracze'; @override - String get broadcastFideRatingCategory => 'FIDE rating category'; + String get broadcastTimezone => 'Strefa czasowa'; @override - String get broadcastOptionalDetails => 'Optional details'; + String get broadcastFideRatingCategory => 'Kategoria rankingu FIDE'; @override - String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + String get broadcastOptionalDetails => 'Opcjonalne szczegóły'; @override - String get broadcastPastBroadcasts => 'Past broadcasts'; + String get broadcastPastBroadcasts => 'Poprzednie transmisje'; @override - String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + String get broadcastAllBroadcastsByMonth => 'Zobacz wszystkie transmisje w danym miesiącu'; @override String broadcastNbBroadcasts(int count) { @@ -1049,6 +1043,9 @@ class AppLocalizationsPl extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Dźwięk powiadomień'; + @override + String get preferencesBlindfold => 'Gra na ślepo'; + @override String get puzzlePuzzles => 'Zadania szachowe'; @@ -1951,7 +1948,7 @@ class AppLocalizationsPl extends AppLocalizations { String get expandVariations => 'Rozwiń warianty'; @override - String get forceVariation => 'Przedstaw jako wariant'; + String get forceVariation => 'Zamień w wariant'; @override String get copyVariationPgn => 'Skopiuj wariant PGN'; @@ -2038,7 +2035,7 @@ class AppLocalizationsPl extends AppLocalizations { } @override - String get playFirstOpeningEndgameExplorerMove => 'Zagraj pierwsze posunięcie z przeglądarki otwarć/końcówek'; + String get playFirstOpeningEndgameExplorerMove => 'Zagraj pierwsze posunięcie z biblioteki otwarć'; @override String get winPreventedBy50MoveRule => 'Bez wygranej ze względu na regułę 50 ruchów'; @@ -5592,7 +5589,7 @@ class AppLocalizationsPl extends AppLocalizations { @override String studyPerPage(String param) { - return '$param per page'; + return '$param na stronie'; } @override diff --git a/lib/l10n/l10n_pt.dart b/lib/l10n/l10n_pt.dart index a61f8f9bc8..457c60f3f5 100644 --- a/lib/l10n/l10n_pt.dart +++ b/lib/l10n/l10n_pt.dart @@ -14,9 +14,6 @@ class AppLocalizationsPt extends AppLocalizations { @override String get mobileAreYouSure => 'Tens a certeza?'; - @override - String get mobileBlindfoldMode => 'De olhos vendados'; - @override String get mobileCancelTakebackOffer => 'Cancelar pedido de voltar'; @@ -539,7 +536,7 @@ class AppLocalizationsPt extends AppLocalizations { String get broadcastStandings => 'Classificações'; @override - String get broadcastOfficialStandings => 'Official Standings'; + String get broadcastOfficialStandings => 'Classificações oficiais'; @override String broadcastIframeHelp(String param) { @@ -572,34 +569,31 @@ class AppLocalizationsPt extends AppLocalizations { String get broadcastScore => 'Pontuação'; @override - String get broadcastAllTeams => 'All teams'; - - @override - String get broadcastTournamentFormat => 'Tournament format'; + String get broadcastAllTeams => 'Todas as equipas'; @override - String get broadcastTournamentLocation => 'Tournament Location'; + String get broadcastTournamentFormat => 'Formato do torneio'; @override - String get broadcastTopPlayers => 'Top players'; + String get broadcastTournamentLocation => 'Localização do Torneio'; @override - String get broadcastTimezone => 'Time zone'; + String get broadcastTopPlayers => 'Melhores jogadores'; @override - String get broadcastFideRatingCategory => 'FIDE rating category'; + String get broadcastTimezone => 'Fuso horário'; @override - String get broadcastOptionalDetails => 'Optional details'; + String get broadcastFideRatingCategory => 'Categoria do Elo FIDE'; @override - String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + String get broadcastOptionalDetails => 'Detalhes opcionais'; @override - String get broadcastPastBroadcasts => 'Past broadcasts'; + String get broadcastPastBroadcasts => 'Transmissões anteriores'; @override - String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + String get broadcastAllBroadcastsByMonth => 'Ver todas as transmissões por mês'; @override String broadcastNbBroadcasts(int count) { @@ -1011,6 +1005,9 @@ class AppLocalizationsPt extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Som da notificação'; + @override + String get preferencesBlindfold => 'De olhos vendados'; + @override String get puzzlePuzzles => 'Problemas'; @@ -2659,7 +2656,7 @@ class AppLocalizationsPt extends AppLocalizations { String get retry => 'Tentar novamente'; @override - String get reconnecting => 'Reconectando'; + String get reconnecting => 'A reconectar'; @override String get noNetwork => 'Desligado'; @@ -5456,7 +5453,7 @@ class AppLocalizationsPt extends AppLocalizations { @override String studyPerPage(String param) { - return '$param per page'; + return '$param por página'; } @override @@ -5514,9 +5511,6 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { @override String get mobileAreYouSure => 'Você tem certeza?'; - @override - String get mobileBlindfoldMode => 'Venda'; - @override String get mobileCancelTakebackOffer => 'Cancelar oferta de revanche'; @@ -5632,6 +5626,9 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { @override String get mobileSystemColors => 'Cores do sistema'; + @override + String get mobileTheme => 'Tema'; + @override String get mobileToolsTab => 'Ferramentas'; @@ -6035,6 +6032,9 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { @override String get broadcastStandings => 'Classificação'; + @override + String get broadcastOfficialStandings => 'Classificação oficial'; + @override String broadcastIframeHelp(String param) { return 'Mais opções na $param'; @@ -6065,6 +6065,33 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { @override String get broadcastScore => 'Pontuação'; + @override + String get broadcastAllTeams => 'Todas as equipes'; + + @override + String get broadcastTournamentFormat => 'Formato do torneio'; + + @override + String get broadcastTournamentLocation => 'Local do torneio'; + + @override + String get broadcastTopPlayers => 'Melhores jogadores'; + + @override + String get broadcastTimezone => 'Fuso horário'; + + @override + String get broadcastFideRatingCategory => 'Categoria de rating FIDE'; + + @override + String get broadcastOptionalDetails => 'Detalhes opcionais'; + + @override + String get broadcastPastBroadcasts => 'Transmissões passadas'; + + @override + String get broadcastAllBroadcastsByMonth => 'Ver todas as transmissões por mês'; + @override String broadcastNbBroadcasts(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -6475,6 +6502,9 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { @override String get preferencesBellNotificationSound => 'Som da notificação'; + @override + String get preferencesBlindfold => 'Às cegas'; + @override String get puzzlePuzzles => 'Quebra-cabeças'; @@ -7174,7 +7204,7 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get waitingForOpponent => 'Aguardando oponente'; @override - String get orLetYourOpponentScanQrCode => 'Ou deixe seu oponente ler este QR Code'; + String get orLetYourOpponentScanQrCode => 'Ou deixe seu oponente ler este código QR'; @override String get waiting => 'Aguardando'; @@ -7299,10 +7329,10 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get blackLeftTheGame => 'Pretas deixaram a partida'; @override - String get whiteDidntMove => 'As brancas não se moveram'; + String get whiteDidntMove => 'Brancas não moveram'; @override - String get blackDidntMove => 'As pretas não se moveram'; + String get blackDidntMove => 'Pretas não moveram'; @override String get requestAComputerAnalysis => 'Solicitar uma análise do computador'; @@ -7361,10 +7391,10 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get deleteFromHere => 'Excluir a partir daqui'; @override - String get collapseVariations => 'Esconder variantes'; + String get collapseVariations => 'Recolher variações'; @override - String get expandVariations => 'Mostrar variantes'; + String get expandVariations => 'Expandir variações'; @override String get forceVariation => 'Variante forçada'; @@ -7428,7 +7458,7 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { } @override - String get dtzWithRounding => 'DTZ50\" com arredondamento, baseado no número de meias-jogadas até a próxima captura ou jogada de peão'; + String get dtzWithRounding => 'DTZ50\" com arredondamento, baseado no número de lances até a próxima captura ou movimento de peão'; @override String get noGameFound => 'Nenhuma partida encontrada'; @@ -8087,7 +8117,7 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get continueFromHere => 'Continuar daqui'; @override - String get toStudy => 'Estudo'; + String get toStudy => 'Estudar'; @override String get importGame => 'Importar partida'; @@ -8290,7 +8320,7 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { @override String nextXTournament(String param) { - return 'Próximo torneio $param:'; + return 'Próximo torneio de $param:'; } @override @@ -8558,7 +8588,7 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get newPasswordStrength => 'Senha forte'; @override - String get clockInitialTime => 'Tempo de relógio'; + String get clockInitialTime => 'Tempo inicial no relógio'; @override String get clockIncrement => 'Incremento do relógio'; @@ -9943,7 +9973,7 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { count, locale: localeName, other: 'O ranking é atualizado a cada $count minutos', - one: 'O ranking é atualizado a cada $count minutos', + one: 'O ranking é atualizado a cada $count minuto', ); return '$_temp0'; } @@ -10918,6 +10948,11 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { @override String get studyYouCompletedThisLesson => 'Parabéns! Você completou essa lição.'; + @override + String studyPerPage(String param) { + return '$param por página'; + } + @override String studyNbChapters(int count) { String _temp0 = intl.Intl.pluralLogic( diff --git a/lib/l10n/l10n_ro.dart b/lib/l10n/l10n_ro.dart index 6bea31a671..89d5b08c53 100644 --- a/lib/l10n/l10n_ro.dart +++ b/lib/l10n/l10n_ro.dart @@ -14,9 +14,6 @@ class AppLocalizationsRo extends AppLocalizations { @override String get mobileAreYouSure => 'Ești sigur?'; - @override - String get mobileBlindfoldMode => 'Legat la ochi'; - @override String get mobileCancelTakebackOffer => 'Anulați propunerea de revanșă'; @@ -133,7 +130,7 @@ class AppLocalizationsRo extends AppLocalizations { String get mobileSystemColors => 'Culori sistem'; @override - String get mobileTheme => 'Theme'; + String get mobileTheme => 'Tema'; @override String get mobileToolsTab => 'Unelte'; @@ -557,7 +554,7 @@ class AppLocalizationsRo extends AppLocalizations { String get broadcastStandings => 'Clasament'; @override - String get broadcastOfficialStandings => 'Official Standings'; + String get broadcastOfficialStandings => 'Clasament oficial'; @override String broadcastIframeHelp(String param) { @@ -590,19 +587,19 @@ class AppLocalizationsRo extends AppLocalizations { String get broadcastScore => 'Scor'; @override - String get broadcastAllTeams => 'All teams'; + String get broadcastAllTeams => 'Toate echipele'; @override - String get broadcastTournamentFormat => 'Tournament format'; + String get broadcastTournamentFormat => 'Format turneu'; @override - String get broadcastTournamentLocation => 'Tournament Location'; + String get broadcastTournamentLocation => 'Locație turneu'; @override String get broadcastTopPlayers => 'Top players'; @override - String get broadcastTimezone => 'Time zone'; + String get broadcastTimezone => 'Fus orar'; @override String get broadcastFideRatingCategory => 'FIDE rating category'; @@ -610,9 +607,6 @@ class AppLocalizationsRo extends AppLocalizations { @override String get broadcastOptionalDetails => 'Optional details'; - @override - String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; - @override String get broadcastPastBroadcasts => 'Past broadcasts'; @@ -1030,6 +1024,9 @@ class AppLocalizationsRo extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Sunet de notificare'; + @override + String get preferencesBlindfold => 'Legat la ochi'; + @override String get puzzlePuzzles => 'Probleme de șah'; @@ -1675,7 +1672,7 @@ class AppLocalizationsRo extends AppLocalizations { String get puzzleThemeZugzwangDescription => 'Adversarul este limitat în mișcările pe care le poate face, iar toate mișcările îi înrăutățesc poziția.'; @override - String get puzzleThemeMix => 'Amestec sănătos'; + String get puzzleThemeMix => 'Mixt'; @override String get puzzleThemeMixDescription => 'Un pic din toate. Nu știi la ce să te aștepți, așa că rămâi gata pentru orice! La fel ca în jocurile reale.'; @@ -5524,7 +5521,7 @@ class AppLocalizationsRo extends AppLocalizations { @override String studyPerPage(String param) { - return '$param per page'; + return '$param pe pagină'; } @override diff --git a/lib/l10n/l10n_ru.dart b/lib/l10n/l10n_ru.dart index 99d35c95ce..1aa28e3191 100644 --- a/lib/l10n/l10n_ru.dart +++ b/lib/l10n/l10n_ru.dart @@ -14,9 +14,6 @@ class AppLocalizationsRu extends AppLocalizations { @override String get mobileAreYouSure => 'Вы уверены?'; - @override - String get mobileBlindfoldMode => 'Игра вслепую'; - @override String get mobileCancelTakebackOffer => 'Отменить предложение о возврате хода'; @@ -133,7 +130,7 @@ class AppLocalizationsRu extends AppLocalizations { String get mobileSystemColors => 'Цвет интерфейса'; @override - String get mobileTheme => 'Theme'; + String get mobileTheme => 'Оформление'; @override String get mobileToolsTab => 'Анализ'; @@ -575,7 +572,7 @@ class AppLocalizationsRu extends AppLocalizations { String get broadcastStandings => 'Турнирная таблица'; @override - String get broadcastOfficialStandings => 'Official Standings'; + String get broadcastOfficialStandings => 'Официальная турнирная таблица'; @override String broadcastIframeHelp(String param) { @@ -608,34 +605,31 @@ class AppLocalizationsRu extends AppLocalizations { String get broadcastScore => 'Очки'; @override - String get broadcastAllTeams => 'All teams'; - - @override - String get broadcastTournamentFormat => 'Tournament format'; + String get broadcastAllTeams => 'Все клубы'; @override - String get broadcastTournamentLocation => 'Tournament Location'; + String get broadcastTournamentFormat => 'Формат турнира'; @override - String get broadcastTopPlayers => 'Top players'; + String get broadcastTournamentLocation => 'Местоположение турнира'; @override - String get broadcastTimezone => 'Time zone'; + String get broadcastTopPlayers => 'Лучшие игроки'; @override - String get broadcastFideRatingCategory => 'FIDE rating category'; + String get broadcastTimezone => 'Часовой пояс'; @override - String get broadcastOptionalDetails => 'Optional details'; + String get broadcastFideRatingCategory => 'Категория рейтинга FIDE'; @override - String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + String get broadcastOptionalDetails => 'Необязательные данные'; @override - String get broadcastPastBroadcasts => 'Past broadcasts'; + String get broadcastPastBroadcasts => 'Завершённые трансляции'; @override - String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + String get broadcastAllBroadcastsByMonth => 'Просмотр всех трансляций за месяц'; @override String broadcastNbBroadcasts(int count) { @@ -1049,6 +1043,9 @@ class AppLocalizationsRu extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Звук колокольчика уведомлений'; + @override + String get preferencesBlindfold => 'Игра вслепую'; + @override String get puzzlePuzzles => 'Задачи'; @@ -1310,7 +1307,7 @@ class AppLocalizationsRu extends AppLocalizations { other: '$count баллов выше вашего рейтинга в задачах', many: '$count баллов выше вашего рейтинга в задачах', few: '$count баллов выше вашего рейтинга в задачах', - one: 'Один балл выше вашего рейтинга в пазлах', + one: 'Один балл выше вашего рейтинга в задачах', ); return '$_temp0'; } @@ -2367,7 +2364,7 @@ class AppLocalizationsRu extends AppLocalizations { String get gamesPlayed => 'Сыграно партий'; @override - String get ok => 'OK'; + String get ok => 'ОК'; @override String get cancel => 'Отменить'; @@ -4539,8 +4536,8 @@ class AppLocalizationsRu extends AppLocalizations { locale: localeName, other: '$count минут', many: '$count минут', - few: '$count минуты', - one: '$count одна минута', + few: '$count Минуты', + one: '$count Одна минута', ); return '$_temp0'; } @@ -5592,7 +5589,7 @@ class AppLocalizationsRu extends AppLocalizations { @override String studyPerPage(String param) { - return '$param per page'; + return '$param на страницу'; } @override diff --git a/lib/l10n/l10n_sk.dart b/lib/l10n/l10n_sk.dart index f72ede6176..e2c919cdb0 100644 --- a/lib/l10n/l10n_sk.dart +++ b/lib/l10n/l10n_sk.dart @@ -14,9 +14,6 @@ class AppLocalizationsSk extends AppLocalizations { @override String get mobileAreYouSure => 'Ste si istý?'; - @override - String get mobileBlindfoldMode => 'Naslepo'; - @override String get mobileCancelTakebackOffer => 'Zrušiť žiadosť o vrátenie ťahu'; @@ -133,7 +130,7 @@ class AppLocalizationsSk extends AppLocalizations { String get mobileSystemColors => 'Farby operačného systému'; @override - String get mobileTheme => 'Theme'; + String get mobileTheme => 'Vzhľad'; @override String get mobileToolsTab => 'Nástroje'; @@ -559,7 +556,7 @@ class AppLocalizationsSk extends AppLocalizations { @override String broadcastStartsAfter(String param) { - return 'Starts after $param'; + return 'Začína po $param'; } @override @@ -575,7 +572,7 @@ class AppLocalizationsSk extends AppLocalizations { String get broadcastStandings => 'Poradie'; @override - String get broadcastOfficialStandings => 'Official Standings'; + String get broadcastOfficialStandings => 'Oficiálne poradie'; @override String broadcastIframeHelp(String param) { @@ -608,34 +605,31 @@ class AppLocalizationsSk extends AppLocalizations { String get broadcastScore => 'Skóre'; @override - String get broadcastAllTeams => 'All teams'; - - @override - String get broadcastTournamentFormat => 'Tournament format'; + String get broadcastAllTeams => 'Všetky tímy'; @override - String get broadcastTournamentLocation => 'Tournament Location'; + String get broadcastTournamentFormat => 'Formát turnaja'; @override - String get broadcastTopPlayers => 'Top players'; + String get broadcastTournamentLocation => 'Miesto konania turnaja'; @override - String get broadcastTimezone => 'Time zone'; + String get broadcastTopPlayers => 'Najlepší hráči'; @override - String get broadcastFideRatingCategory => 'FIDE rating category'; + String get broadcastTimezone => 'Časové pásmo'; @override - String get broadcastOptionalDetails => 'Optional details'; + String get broadcastFideRatingCategory => 'Kategória FIDE ratingu'; @override - String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + String get broadcastOptionalDetails => 'Nepovinné údaje'; @override - String get broadcastPastBroadcasts => 'Past broadcasts'; + String get broadcastPastBroadcasts => 'Predchádzajúce vysielania'; @override - String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + String get broadcastAllBroadcastsByMonth => 'Zobraziť všetky vysielania podľa mesiacov'; @override String broadcastNbBroadcasts(int count) { @@ -1049,6 +1043,9 @@ class AppLocalizationsSk extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Zvuk upozornenia'; + @override + String get preferencesBlindfold => 'Naslepo'; + @override String get puzzlePuzzles => 'Šachové úlohy'; @@ -5592,7 +5589,7 @@ class AppLocalizationsSk extends AppLocalizations { @override String studyPerPage(String param) { - return '$param per page'; + return '$param na stránku'; } @override diff --git a/lib/l10n/l10n_sl.dart b/lib/l10n/l10n_sl.dart index ba033cc7f3..1b1df04618 100644 --- a/lib/l10n/l10n_sl.dart +++ b/lib/l10n/l10n_sl.dart @@ -14,9 +14,6 @@ class AppLocalizationsSl extends AppLocalizations { @override String get mobileAreYouSure => 'Are you sure?'; - @override - String get mobileBlindfoldMode => 'Blindfold'; - @override String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @@ -133,7 +130,7 @@ class AppLocalizationsSl extends AppLocalizations { String get mobileSystemColors => 'Barve sistema'; @override - String get mobileTheme => 'Theme'; + String get mobileTheme => 'Tema'; @override String get mobileToolsTab => 'Orodja'; @@ -575,7 +572,7 @@ class AppLocalizationsSl extends AppLocalizations { String get broadcastStandings => 'Standings'; @override - String get broadcastOfficialStandings => 'Official Standings'; + String get broadcastOfficialStandings => 'Uradna lestvica'; @override String broadcastIframeHelp(String param) { @@ -608,34 +605,31 @@ class AppLocalizationsSl extends AppLocalizations { String get broadcastScore => 'Score'; @override - String get broadcastAllTeams => 'All teams'; - - @override - String get broadcastTournamentFormat => 'Tournament format'; + String get broadcastAllTeams => 'Vse ekipe'; @override - String get broadcastTournamentLocation => 'Tournament Location'; + String get broadcastTournamentFormat => 'Oblika turnirja'; @override - String get broadcastTopPlayers => 'Top players'; + String get broadcastTournamentLocation => 'Lokacija turnirja'; @override - String get broadcastTimezone => 'Time zone'; + String get broadcastTopPlayers => 'Najboljši igralci'; @override - String get broadcastFideRatingCategory => 'FIDE rating category'; + String get broadcastTimezone => 'Časovni pas'; @override - String get broadcastOptionalDetails => 'Optional details'; + String get broadcastFideRatingCategory => 'FIDE rating kategorija'; @override - String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + String get broadcastOptionalDetails => 'Neobvezne podrobnosti'; @override - String get broadcastPastBroadcasts => 'Past broadcasts'; + String get broadcastPastBroadcasts => 'Pretekle oddaje'; @override - String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + String get broadcastAllBroadcastsByMonth => 'Oglejte si vse oddaje po mesecih'; @override String broadcastNbBroadcasts(int count) { @@ -1049,6 +1043,9 @@ class AppLocalizationsSl extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Zvok obvestila zvonca'; + @override + String get preferencesBlindfold => 'Šah z zavezanimi očmi'; + @override String get puzzlePuzzles => 'Šahovski problemi'; @@ -5592,7 +5589,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String studyPerPage(String param) { - return '$param per page'; + return '$param na stran'; } @override diff --git a/lib/l10n/l10n_sq.dart b/lib/l10n/l10n_sq.dart index bb4b31b081..dc19c47545 100644 --- a/lib/l10n/l10n_sq.dart +++ b/lib/l10n/l10n_sq.dart @@ -14,9 +14,6 @@ class AppLocalizationsSq extends AppLocalizations { @override String get mobileAreYouSure => 'Jeni i sigurt?'; - @override - String get mobileBlindfoldMode => 'Me sytë lidhur'; - @override String get mobileCancelTakebackOffer => 'Anulojeni ofertën për prapakthim'; @@ -88,7 +85,7 @@ class AppLocalizationsSq extends AppLocalizations { String get mobilePuzzleThemesSubtitle => 'Luani puzzle-e nga hapjet tuaja të parapëlqyera, ose zgjidhni një temë.'; @override - String get mobilePuzzlesTab => 'Puzzles'; + String get mobilePuzzlesTab => 'Ushtrime'; @override String get mobileRecentSearches => 'Kërkime së fundi'; @@ -133,7 +130,7 @@ class AppLocalizationsSq extends AppLocalizations { String get mobileSystemColors => 'Ngjyra sistemi'; @override - String get mobileTheme => 'Theme'; + String get mobileTheme => 'Temë'; @override String get mobileToolsTab => 'Mjete'; @@ -572,34 +569,31 @@ class AppLocalizationsSq extends AppLocalizations { String get broadcastScore => 'Përfundim'; @override - String get broadcastAllTeams => 'All teams'; - - @override - String get broadcastTournamentFormat => 'Tournament format'; + String get broadcastAllTeams => 'Krejt ekipet'; @override - String get broadcastTournamentLocation => 'Tournament Location'; + String get broadcastTournamentFormat => 'Format turneu'; @override - String get broadcastTopPlayers => 'Top players'; + String get broadcastTournamentLocation => 'Vendndodhje Turney'; @override - String get broadcastTimezone => 'Time zone'; + String get broadcastTopPlayers => 'Lojtarët kryesues'; @override - String get broadcastFideRatingCategory => 'FIDE rating category'; + String get broadcastTimezone => 'Zonë kohore'; @override - String get broadcastOptionalDetails => 'Optional details'; + String get broadcastFideRatingCategory => 'Kategori vlerësimi FIDE'; @override - String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + String get broadcastOptionalDetails => 'Hollësi opsionale'; @override - String get broadcastPastBroadcasts => 'Past broadcasts'; + String get broadcastPastBroadcasts => 'Transmetime të kaluara'; @override - String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + String get broadcastAllBroadcastsByMonth => 'Shihni krejt transmetimet sipas muajsh'; @override String broadcastNbBroadcasts(int count) { @@ -614,11 +608,11 @@ class AppLocalizationsSq extends AppLocalizations { @override String challengeChallengesX(String param1) { - return 'Challenges: $param1'; + return 'Sfida: $param1'; } @override - String get challengeChallengeToPlay => 'Sfidoni në një lojë'; + String get challengeChallengeToPlay => 'Sfidoni me një lojë'; @override String get challengeChallengeDeclined => 'Sfida u refuzua'; @@ -1011,6 +1005,9 @@ class AppLocalizationsSq extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Tingull zileje njoftimesh'; + @override + String get preferencesBlindfold => 'Me sytë lidhur'; + @override String get puzzlePuzzles => 'Ushtrime'; @@ -5456,7 +5453,7 @@ class AppLocalizationsSq extends AppLocalizations { @override String studyPerPage(String param) { - return '$param per page'; + return '$param për faqe'; } @override diff --git a/lib/l10n/l10n_sr.dart b/lib/l10n/l10n_sr.dart index 13335d3d99..a758a58517 100644 --- a/lib/l10n/l10n_sr.dart +++ b/lib/l10n/l10n_sr.dart @@ -14,9 +14,6 @@ class AppLocalizationsSr extends AppLocalizations { @override String get mobileAreYouSure => 'Are you sure?'; - @override - String get mobileBlindfoldMode => 'Blindfold'; - @override String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @@ -608,9 +605,6 @@ class AppLocalizationsSr extends AppLocalizations { @override String get broadcastOptionalDetails => 'Optional details'; - @override - String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; - @override String get broadcastPastBroadcasts => 'Past broadcasts'; @@ -1027,6 +1021,9 @@ class AppLocalizationsSr extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Bell notification sound'; + @override + String get preferencesBlindfold => 'Blindfold'; + @override String get puzzlePuzzles => 'Проблеми'; @@ -5404,16 +5401,16 @@ class AppLocalizationsSr extends AppLocalizations { String get studyWhereDoYouWantToStudyThat => 'Где желите то проучити?'; @override - String get studyGoodMove => 'Good move'; + String get studyGoodMove => 'Добар потез'; @override - String get studyMistake => 'Mistake'; + String get studyMistake => 'Грешка'; @override String get studyBrilliantMove => 'Brilliant move'; @override - String get studyBlunder => 'Blunder'; + String get studyBlunder => 'Груба грешка'; @override String get studyInterestingMove => 'Interesting move'; diff --git a/lib/l10n/l10n_sv.dart b/lib/l10n/l10n_sv.dart index cd623853c4..d9b08e8ad6 100644 --- a/lib/l10n/l10n_sv.dart +++ b/lib/l10n/l10n_sv.dart @@ -14,9 +14,6 @@ class AppLocalizationsSv extends AppLocalizations { @override String get mobileAreYouSure => 'Är du säker?'; - @override - String get mobileBlindfoldMode => 'I blindo'; - @override String get mobileCancelTakebackOffer => 'Cancel takeback offer'; @@ -56,7 +53,7 @@ class AppLocalizationsSv extends AppLocalizations { String get mobileNoSearchResults => 'Inga resultat'; @override - String get mobileNotFollowingAnyUser => 'You are not following any user.'; + String get mobileNotFollowingAnyUser => 'Du följer inte någon användare.'; @override String get mobileOkButton => 'OK'; @@ -112,7 +109,7 @@ class AppLocalizationsSv extends AppLocalizations { String get mobileShareGameURL => 'Dela parti-URL'; @override - String get mobileSharePositionAsFEN => 'Share position as FEN'; + String get mobileSharePositionAsFEN => 'Dela position som FEN'; @override String get mobileSharePuzzle => 'Dela detta schackproblem'; @@ -592,9 +589,6 @@ class AppLocalizationsSv extends AppLocalizations { @override String get broadcastOptionalDetails => 'Optional details'; - @override - String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; - @override String get broadcastPastBroadcasts => 'Past broadcasts'; @@ -1011,6 +1005,9 @@ class AppLocalizationsSv extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Klock-notisljud'; + @override + String get preferencesBlindfold => 'I blindo'; + @override String get puzzlePuzzles => 'Problem'; diff --git a/lib/l10n/l10n_tr.dart b/lib/l10n/l10n_tr.dart index c169a231ec..c9cfdab80b 100644 --- a/lib/l10n/l10n_tr.dart +++ b/lib/l10n/l10n_tr.dart @@ -14,9 +14,6 @@ class AppLocalizationsTr extends AppLocalizations { @override String get mobileAreYouSure => 'Emin misiniz?'; - @override - String get mobileBlindfoldMode => 'Körleme modu'; - @override String get mobileCancelTakebackOffer => 'Geri alma teklifini iptal et'; @@ -539,7 +536,7 @@ class AppLocalizationsTr extends AppLocalizations { String get broadcastStandings => 'Sıralamalar'; @override - String get broadcastOfficialStandings => 'Official Standings'; + String get broadcastOfficialStandings => 'Resmi Sıralamalar'; @override String broadcastIframeHelp(String param) { @@ -547,7 +544,7 @@ class AppLocalizationsTr extends AppLocalizations { } @override - String get broadcastWebmastersPage => 'webmasters page'; + String get broadcastWebmastersPage => 'ağ yöneticileri sayfası'; @override String broadcastPgnSourceHelp(String param) { @@ -572,34 +569,31 @@ class AppLocalizationsTr extends AppLocalizations { String get broadcastScore => 'Skor'; @override - String get broadcastAllTeams => 'All teams'; - - @override - String get broadcastTournamentFormat => 'Tournament format'; + String get broadcastAllTeams => 'Tüm takımlar'; @override - String get broadcastTournamentLocation => 'Tournament Location'; + String get broadcastTournamentFormat => 'Turnuva biçimi'; @override - String get broadcastTopPlayers => 'Top players'; + String get broadcastTournamentLocation => 'Turnuva Konumu'; @override - String get broadcastTimezone => 'Time zone'; + String get broadcastTopPlayers => 'En iyi oyuncular'; @override - String get broadcastFideRatingCategory => 'FIDE rating category'; + String get broadcastTimezone => 'Zaman dilimi'; @override - String get broadcastOptionalDetails => 'Optional details'; + String get broadcastFideRatingCategory => 'FIDE derecelendirme kategorisi'; @override - String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + String get broadcastOptionalDetails => 'İsteğe bağlı ayrıntılar'; @override - String get broadcastPastBroadcasts => 'Past broadcasts'; + String get broadcastPastBroadcasts => 'Geçmiş yayınlar'; @override - String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + String get broadcastAllBroadcastsByMonth => 'Tüm yayınları aylara göre görüntüleyin'; @override String broadcastNbBroadcasts(int count) { @@ -1011,6 +1005,9 @@ class AppLocalizationsTr extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Çan bildirimi sesi'; + @override + String get preferencesBlindfold => 'Körleme modu'; + @override String get puzzlePuzzles => 'Bulmacalar'; @@ -2319,7 +2316,7 @@ class AppLocalizationsTr extends AppLocalizations { String get gamesPlayed => 'Oynanmış oyunlar'; @override - String get ok => 'OK'; + String get ok => 'Tamam'; @override String get cancel => 'İptal et'; @@ -5456,7 +5453,7 @@ class AppLocalizationsTr extends AppLocalizations { @override String studyPerPage(String param) { - return '$param per page'; + return 'Sayfa başına $param'; } @override diff --git a/lib/l10n/l10n_uk.dart b/lib/l10n/l10n_uk.dart index 8bb7295b30..6a1e4fa4a9 100644 --- a/lib/l10n/l10n_uk.dart +++ b/lib/l10n/l10n_uk.dart @@ -14,9 +14,6 @@ class AppLocalizationsUk extends AppLocalizations { @override String get mobileAreYouSure => 'Ви впевнені?'; - @override - String get mobileBlindfoldMode => 'Наосліп'; - @override String get mobileCancelTakebackOffer => 'Скасувати пропозицію повернення ходу'; @@ -133,7 +130,7 @@ class AppLocalizationsUk extends AppLocalizations { String get mobileSystemColors => 'Системні кольори'; @override - String get mobileTheme => 'Theme'; + String get mobileTheme => 'Тема'; @override String get mobileToolsTab => 'Інструм.'; @@ -532,25 +529,25 @@ class AppLocalizationsUk extends AppLocalizations { String get broadcastRecentTournaments => 'Нещодавні турніри'; @override - String get broadcastOpenLichess => 'Open in Lichess'; + String get broadcastOpenLichess => 'Відкрити в Lichess'; @override - String get broadcastTeams => 'Teams'; + String get broadcastTeams => 'Команди'; @override - String get broadcastBoards => 'Boards'; + String get broadcastBoards => 'Дошки'; @override - String get broadcastOverview => 'Overview'; + String get broadcastOverview => 'Огляд'; @override String get broadcastSubscribeTitle => 'Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.'; @override - String get broadcastUploadImage => 'Upload tournament image'; + String get broadcastUploadImage => 'Завантажити зображення турніру'; @override - String get broadcastNoBoardsYet => 'No boards yet. These will appear once games are uploaded.'; + String get broadcastNoBoardsYet => 'Ще немає дощок. Вони з\'являться, коли ігри будуть завантажені.'; @override String broadcastBoardsCanBeLoaded(String param) { @@ -563,23 +560,23 @@ class AppLocalizationsUk extends AppLocalizations { } @override - String get broadcastStartVerySoon => 'The broadcast will start very soon.'; + String get broadcastStartVerySoon => 'Трансляція розпочнеться дуже скоро.'; @override - String get broadcastNotYetStarted => 'The broadcast has not yet started.'; + String get broadcastNotYetStarted => 'Трансляція ще не розпочалася.'; @override String get broadcastOfficialWebsite => 'Офіційний вебсайт'; @override - String get broadcastStandings => 'Standings'; + String get broadcastStandings => 'Турнірна таблиця'; @override - String get broadcastOfficialStandings => 'Official Standings'; + String get broadcastOfficialStandings => 'Офіційна турнірна таблиця'; @override String broadcastIframeHelp(String param) { - return 'More options on the $param'; + return 'Більше опцій на $param'; } @override @@ -591,48 +588,45 @@ class AppLocalizationsUk extends AppLocalizations { } @override - String get broadcastEmbedThisBroadcast => 'Embed this broadcast in your website'; + String get broadcastEmbedThisBroadcast => 'Вбудувати цю трансляцію на своєму сайті'; @override String broadcastEmbedThisRound(String param) { - return 'Embed $param in your website'; + return 'Вбудувати $param на своєму сайті'; } @override - String get broadcastRatingDiff => 'Rating diff'; - - @override - String get broadcastGamesThisTournament => 'Games in this tournament'; + String get broadcastRatingDiff => 'Різниця у рейтингу'; @override - String get broadcastScore => 'Score'; + String get broadcastGamesThisTournament => 'Ігри в цьому турнірі'; @override - String get broadcastAllTeams => 'All teams'; + String get broadcastScore => 'Очки'; @override - String get broadcastTournamentFormat => 'Tournament format'; + String get broadcastAllTeams => 'Усі команди'; @override - String get broadcastTournamentLocation => 'Tournament Location'; + String get broadcastTournamentFormat => 'Формат турніру'; @override - String get broadcastTopPlayers => 'Top players'; + String get broadcastTournamentLocation => 'Місце турніру'; @override - String get broadcastTimezone => 'Time zone'; + String get broadcastTopPlayers => 'Найкращі гравці'; @override - String get broadcastFideRatingCategory => 'FIDE rating category'; + String get broadcastTimezone => 'Часовий пояс'; @override - String get broadcastOptionalDetails => 'Optional details'; + String get broadcastFideRatingCategory => 'Категорія рейтингу FIDE'; @override - String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + String get broadcastOptionalDetails => 'Додаткові деталі'; @override - String get broadcastPastBroadcasts => 'Past broadcasts'; + String get broadcastPastBroadcasts => 'Минулі трансляції'; @override String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; @@ -975,7 +969,7 @@ class AppLocalizationsUk extends AppLocalizations { String get preferencesInCorrespondenceGames => 'У заочних партіях'; @override - String get preferencesCorrespondenceAndUnlimited => 'За листуванням та необмежені'; + String get preferencesCorrespondenceAndUnlimited => 'Заочні та необмежені'; @override String get preferencesConfirmResignationAndDrawOffers => 'Підтверджувати повернення ходу та пропозиції нічий'; @@ -1049,6 +1043,9 @@ class AppLocalizationsUk extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Звук сповіщення'; + @override + String get preferencesBlindfold => 'Наосліп'; + @override String get puzzlePuzzles => 'Задачі'; @@ -5592,7 +5589,7 @@ class AppLocalizationsUk extends AppLocalizations { @override String studyPerPage(String param) { - return '$param per page'; + return '$param на сторінку'; } @override diff --git a/lib/l10n/l10n_vi.dart b/lib/l10n/l10n_vi.dart index 6edb7f57d0..deb1db3bc2 100644 --- a/lib/l10n/l10n_vi.dart +++ b/lib/l10n/l10n_vi.dart @@ -14,9 +14,6 @@ class AppLocalizationsVi extends AppLocalizations { @override String get mobileAreYouSure => 'Bạn chắc chứ?'; - @override - String get mobileBlindfoldMode => 'Bịt mắt'; - @override String get mobileCancelTakebackOffer => 'Hủy đề nghị đi lại'; @@ -133,7 +130,7 @@ class AppLocalizationsVi extends AppLocalizations { String get mobileSystemColors => 'Màu hệ thống'; @override - String get mobileTheme => 'Theme'; + String get mobileTheme => 'Giao diện'; @override String get mobileToolsTab => 'Công cụ'; @@ -521,7 +518,7 @@ class AppLocalizationsVi extends AppLocalizations { String get broadcastStandings => 'Bảng xếp hạng'; @override - String get broadcastOfficialStandings => 'Official Standings'; + String get broadcastOfficialStandings => 'Bảng xếp hạng Chính thức'; @override String broadcastIframeHelp(String param) { @@ -554,34 +551,31 @@ class AppLocalizationsVi extends AppLocalizations { String get broadcastScore => 'Điểm số'; @override - String get broadcastAllTeams => 'All teams'; - - @override - String get broadcastTournamentFormat => 'Tournament format'; + String get broadcastAllTeams => 'Tất cả đội'; @override - String get broadcastTournamentLocation => 'Tournament Location'; + String get broadcastTournamentFormat => 'Điều lệ giải đấu'; @override - String get broadcastTopPlayers => 'Top players'; + String get broadcastTournamentLocation => 'Địa điểm tổ chức giải đấu'; @override - String get broadcastTimezone => 'Time zone'; + String get broadcastTopPlayers => 'Những kỳ thủ hàng đầu'; @override - String get broadcastFideRatingCategory => 'FIDE rating category'; + String get broadcastTimezone => 'Múi giờ'; @override - String get broadcastOptionalDetails => 'Optional details'; + String get broadcastFideRatingCategory => 'Thể loại xếp hạng FIDE'; @override - String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + String get broadcastOptionalDetails => 'Tùy chọn chi tiết'; @override - String get broadcastPastBroadcasts => 'Past broadcasts'; + String get broadcastPastBroadcasts => 'Các phát sóng đã qua'; @override - String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + String get broadcastAllBroadcastsByMonth => 'Xem tất cả phát sóng theo tháng'; @override String broadcastNbBroadcasts(int count) { @@ -992,6 +986,9 @@ class AppLocalizationsVi extends AppLocalizations { @override String get preferencesBellNotificationSound => 'Âm thanh chuông báo'; + @override + String get preferencesBlindfold => 'Bịt mắt'; + @override String get puzzlePuzzles => 'Câu đố'; @@ -5388,7 +5385,7 @@ class AppLocalizationsVi extends AppLocalizations { @override String studyPerPage(String param) { - return '$param per page'; + return '$param mỗi trang'; } @override diff --git a/lib/l10n/l10n_zh.dart b/lib/l10n/l10n_zh.dart index 18127594d7..8dc61905f6 100644 --- a/lib/l10n/l10n_zh.dart +++ b/lib/l10n/l10n_zh.dart @@ -14,9 +14,6 @@ class AppLocalizationsZh extends AppLocalizations { @override String get mobileAreYouSure => '你确定吗?'; - @override - String get mobileBlindfoldMode => '盲棋'; - @override String get mobileCancelTakebackOffer => '取消悔棋请求'; @@ -73,7 +70,7 @@ class AppLocalizationsZh extends AppLocalizations { String get mobilePuzzleStormConfirmEndRun => '你想结束这组吗?'; @override - String get mobilePuzzleStormFilterNothingToShow => '没有显示,请更改过滤器'; + String get mobilePuzzleStormFilterNothingToShow => '没有结果,请更改筛选条件'; @override String get mobilePuzzleStormNothingToShow => '没有记录。 请下几组 Puzzle Storm。'; @@ -133,7 +130,7 @@ class AppLocalizationsZh extends AppLocalizations { String get mobileSystemColors => '系统颜色'; @override - String get mobileTheme => 'Theme'; + String get mobileTheme => '主题'; @override String get mobileToolsTab => '工具'; @@ -576,13 +573,10 @@ class AppLocalizationsZh extends AppLocalizations { String get broadcastOptionalDetails => 'Optional details'; @override - String get broadcastUpcomingBroadcasts => 'Upcoming broadcasts'; + String get broadcastPastBroadcasts => '结束的转播'; @override - String get broadcastPastBroadcasts => 'Past broadcasts'; - - @override - String get broadcastAllBroadcastsByMonth => 'View all broadcasts by month'; + String get broadcastAllBroadcastsByMonth => '按月查看所有转播'; @override String broadcastNbBroadcasts(int count) { @@ -993,6 +987,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get preferencesBellNotificationSound => '通知铃声'; + @override + String get preferencesBlindfold => '盲棋'; + @override String get puzzlePuzzles => '谜题'; @@ -5443,9 +5440,6 @@ class AppLocalizationsZhTw extends AppLocalizationsZh { @override String get mobileAreYouSure => '您確定嗎?'; - @override - String get mobileBlindfoldMode => '盲棋'; - @override String get mobileCancelTakebackOffer => '取消悔棋請求'; @@ -5924,6 +5918,16 @@ class AppLocalizationsZhTw extends AppLocalizationsZh { @override String get broadcastNoBoardsYet => '尚無棋局。這些棋局將在對局上傳後顯示。'; + @override + String broadcastBoardsCanBeLoaded(String param) { + return '棋盤能夠以輸入源投放或是利用$param'; + } + + @override + String broadcastStartsAfter(String param) { + return '於$param開始'; + } + @override String get broadcastStartVerySoon => '直播即將開始。'; @@ -5966,6 +5970,33 @@ class AppLocalizationsZhTw extends AppLocalizationsZh { @override String get broadcastScore => '分數'; + @override + String get broadcastAllTeams => '所有團隊'; + + @override + String get broadcastTournamentFormat => '錦標賽格式'; + + @override + String get broadcastTournamentLocation => '錦標賽地點'; + + @override + String get broadcastTopPlayers => '排行榜'; + + @override + String get broadcastTimezone => '時區'; + + @override + String get broadcastFideRatingCategory => 'FIDE 評級類別'; + + @override + String get broadcastOptionalDetails => '其他細節'; + + @override + String get broadcastPastBroadcasts => '直播紀錄'; + + @override + String get broadcastAllBroadcastsByMonth => '以月份顯示所有直播'; + @override String broadcastNbBroadcasts(int count) { String _temp0 = intl.Intl.pluralLogic( @@ -6375,6 +6406,9 @@ class AppLocalizationsZhTw extends AppLocalizationsZh { @override String get preferencesBellNotificationSound => '通知鈴聲'; + @override + String get preferencesBlindfold => '盲棋'; + @override String get puzzlePuzzles => '謎題'; diff --git a/lib/l10n/lila_af.arb b/lib/l10n/lila_af.arb index 1f85ff390f..ef72132e95 100644 --- a/lib/l10n/lila_af.arb +++ b/lib/l10n/lila_af.arb @@ -1,41 +1,41 @@ { + "mobileAllGames": "Alle spelle", + "mobileAreYouSure": "Is jy seker?", + "mobileBlindfoldMode": "Geblinddoek", + "mobileCorrespondenceClearSavedMove": "Vee gestoorde skuif uit", + "mobileCustomGameJoinAGame": "Sluit aan by 'n spel", + "mobileFeedbackButton": "Terugvoer", + "mobileGreeting": "Hallo, {param}", + "mobileGreetingWithoutName": "Hallo", + "mobileHideVariation": "Verberg variasie", "mobileHomeTab": "Tuis", - "mobilePuzzlesTab": "Kopkrappers", - "mobileToolsTab": "Hulpmiddels", - "mobileWatchTab": "Hou dop", - "mobileSettingsTab": "Instellings", "mobileMustBeLoggedIn": "Jy moet ingeteken wees om hierdie bladsy te kan sien.", - "mobileSystemColors": "Stelselkleure", - "mobileFeedbackButton": "Terugvoer", + "mobileNoSearchResults": "Geen resultate nie", + "mobileNotFollowingAnyUser": "Jy volg nie enige gebruikers nie.", "mobileOkButton": "Reg", + "mobilePlayersMatchingSearchTerm": "Spelers met \"{param}\"", + "mobilePrefMagnifyDraggedPiece": "Vergroot gesleepte stuk", + "mobilePuzzleStormConfirmEndRun": "Wil jy hierdie lopie beëindig?", + "mobilePuzzleStormFilterNothingToShow": "Niks om te wys nie; verander asb. die filters", + "mobilePuzzleStormSubtitle": "Los soveel kopkrappers moontlik op in 3 minute.", + "mobilePuzzleThemesSubtitle": "Doen kopkrappers van jou gunstelingopenings, of kies 'n tema.", + "mobilePuzzlesTab": "Kopkrappers", + "mobileRecentSearches": "Onlangse soektogte", "mobileSettingsHapticFeedback": "Vibrasieterugvoer", "mobileSettingsImmersiveMode": "Volskermmodus", - "mobileNotFollowingAnyUser": "Jy volg nie enige gebruikers nie.", - "mobileAllGames": "Alle spelle", - "mobileRecentSearches": "Onlangse soektogte", - "mobilePlayersMatchingSearchTerm": "Spelers met \"{param}\"", - "mobileNoSearchResults": "Geen resultate nie", - "mobileAreYouSure": "Is jy seker?", - "mobileSharePuzzle": "Deel hierdie kopkrapper", - "mobileShareGameURL": "Deel spel se bronadres", + "mobileSettingsTab": "Instellings", "mobileShareGamePGN": "Deel PGN", + "mobileShareGameURL": "Deel spel se bronadres", "mobileSharePositionAsFEN": "Deel posisie as FEN", - "mobileShowVariations": "Wys variasies", - "mobileHideVariation": "Verberg variasie", + "mobileSharePuzzle": "Deel hierdie kopkrapper", "mobileShowComments": "Wys kommentaar", - "mobilePuzzleStormConfirmEndRun": "Wil jy hierdie lopie beëindig?", - "mobilePuzzleStormFilterNothingToShow": "Niks om te wys nie; verander asb. die filters", - "mobileWaitingForOpponentToJoin": "Wag vir opponent om aan te sluit...", - "mobileBlindfoldMode": "Geblinddoek", - "mobileCustomGameJoinAGame": "Sluit aan by 'n spel", - "mobileCorrespondenceClearSavedMove": "Vee gestoorde skuif uit", - "mobileSomethingWentWrong": "Iets het skeefgeloop.", "mobileShowResult": "Wys resultaat", - "mobilePuzzleThemesSubtitle": "Doen kopkrappers van jou gunstelingopenings, of kies 'n tema.", - "mobilePuzzleStormSubtitle": "Los soveel kopkrappers moontlik op in 3 minute.", - "mobileGreeting": "Hallo, {param}", - "mobileGreetingWithoutName": "Hallo", - "mobilePrefMagnifyDraggedPiece": "Vergroot gesleepte stuk", + "mobileShowVariations": "Wys variasies", + "mobileSomethingWentWrong": "Iets het skeefgeloop.", + "mobileSystemColors": "Stelselkleure", + "mobileToolsTab": "Hulpmiddels", + "mobileWaitingForOpponentToJoin": "Wag vir opponent om aan te sluit...", + "mobileWatchTab": "Hou dop", "activityActivity": "Aktiwiteite", "activityHostedALiveStream": "Het 'n lewendige uitsending aangebied", "activityRankedInSwissTournament": "Rang van #{param1} uit {param2}", @@ -212,6 +212,7 @@ "preferencesNotifyWeb": "Blaaier", "preferencesNotifyDevice": "Toestel", "preferencesBellNotificationSound": "Klokkie kennisgewing klank", + "preferencesBlindfold": "Blinddoek", "puzzlePuzzles": "Raaisels", "puzzlePuzzleThemes": "Raaisel temas", "puzzleRecommended": "Aanbeveeldede", @@ -529,7 +530,6 @@ "replayMode": "Oorspeel modus", "realtimeReplay": "Ware Tyd", "byCPL": "Met CPL", - "openStudy": "Open studie", "enable": "Aktief", "bestMoveArrow": "Beste skuif pyl", "showVariationArrows": "Wys variasiepyle", @@ -736,7 +736,6 @@ "block": "Blokeer", "blocked": "Geblok", "unblock": "Ontblok", - "followsYou": "Volg jou", "xStartedFollowingY": "{param1} het begin om {param2} te volg", "more": "Meer", "memberSince": "Lid sedert", diff --git a/lib/l10n/lila_ar.arb b/lib/l10n/lila_ar.arb index 6c5d274d87..80958d1ef6 100644 --- a/lib/l10n/lila_ar.arb +++ b/lib/l10n/lila_ar.arb @@ -1,47 +1,47 @@ { + "mobileAllGames": "جميع الألعاب", + "mobileAreYouSure": "هل أنت واثق؟", + "mobileBlindfoldMode": "معصوب العينين", + "mobileCancelTakebackOffer": "إلغاء عرض الاسترداد", + "mobileClearButton": "مسح", + "mobileCorrespondenceClearSavedMove": "مسح النقل المحفوظ", + "mobileCustomGameJoinAGame": "الانضمام إلى لُعْبَة", + "mobileFeedbackButton": "الملاحظات", + "mobileGreeting": "مرحبا، {param}", + "mobileGreetingWithoutName": "مرحبا", + "mobileHideVariation": "إخفاء سلسلة النقلات المرشحة", "mobileHomeTab": "الرئيسية", - "mobilePuzzlesTab": "ألغاز", - "mobileToolsTab": "أدوات", - "mobileWatchTab": "شاهد", - "mobileSettingsTab": "الإعدادات", + "mobileLiveStreamers": "البث المباشر", "mobileMustBeLoggedIn": "سجل الدخول لعرض هذه الصفحة.", - "mobileSystemColors": "ألوان النظام", - "mobileFeedbackButton": "الملاحظات", + "mobileNoSearchResults": "لا توجد نتائج", + "mobileNotFollowingAnyUser": "أنت لا تتبع أي مستخدم.", "mobileOkButton": "موافق", + "mobilePlayersMatchingSearchTerm": "لاعبين مع \"{param}\"", + "mobilePrefMagnifyDraggedPiece": "تكبير القطعة المسحوبة", + "mobilePuzzleStormConfirmEndRun": "هل تريد إنهاء هذا التشغيل؟", + "mobilePuzzleStormFilterNothingToShow": "لا شيء لإظهاره، الرجاء تغيير المرشح", + "mobilePuzzleStormNothingToShow": "لا شيء لإظهاره. العب بعض الألغاز.", + "mobilePuzzleStormSubtitle": "حل أكبر عدد ممكن من الألغاز في 3 دقائق.", + "mobilePuzzleStreakAbortWarning": "سوف تفقد تسلقك الحالي وسيتم حفظ نتيجتك.", + "mobilePuzzleThemesSubtitle": "حُل الألغاز المتعلّقة بافتتاحاتك المفضّلة، أو اختر موضوعاً.", + "mobilePuzzlesTab": "ألغاز", + "mobileRecentSearches": "عمليات البحث الأخيرة", "mobileSettingsHapticFeedback": "التعليقات اللمسية", "mobileSettingsImmersiveMode": "وضع ملء الشاشة", "mobileSettingsImmersiveModeSubtitle": "إخفاء واجهة المستخدم خلال التشغيل. استخدم هذا إذا كنت مزعجاً من إيماءات التنقل للنظام عند حواف الشاشة. ينطبق على المباريات في اللعبة والألغاز.", - "mobileNotFollowingAnyUser": "أنت لا تتبع أي مستخدم.", - "mobileAllGames": "جميع الألعاب", - "mobileRecentSearches": "عمليات البحث الأخيرة", - "mobileClearButton": "مسح", - "mobilePlayersMatchingSearchTerm": "لاعبين مع \"{param}\"", - "mobileNoSearchResults": "لا توجد نتائج", - "mobileAreYouSure": "هل أنت واثق؟", - "mobilePuzzleStreakAbortWarning": "سوف تفقد تسلقك الحالي وسيتم حفظ نتيجتك.", - "mobilePuzzleStormNothingToShow": "لا شيء لإظهاره. العب بعض الألغاز.", - "mobileSharePuzzle": "شارك هذا اللغز", - "mobileShareGameURL": "شارك رابط المباراة", + "mobileSettingsTab": "الإعدادات", "mobileShareGamePGN": "شارك الPGN", + "mobileShareGameURL": "شارك رابط المباراة", "mobileSharePositionAsFEN": "مشاركة الموضع كFEN", - "mobileShowVariations": "أظهر سلسلة النقلات المرشحة", - "mobileHideVariation": "إخفاء سلسلة النقلات المرشحة", + "mobileSharePuzzle": "شارك هذا اللغز", "mobileShowComments": "عرض التعليقات", - "mobilePuzzleStormConfirmEndRun": "هل تريد إنهاء هذا التشغيل؟", - "mobilePuzzleStormFilterNothingToShow": "لا شيء لإظهاره، الرجاء تغيير المرشح", - "mobileCancelTakebackOffer": "إلغاء عرض الاسترداد", - "mobileWaitingForOpponentToJoin": "في انتظار انضمام الطرف الآخر...", - "mobileBlindfoldMode": "معصوب العينين", - "mobileLiveStreamers": "البث المباشر", - "mobileCustomGameJoinAGame": "الانضمام إلى لُعْبَة", - "mobileCorrespondenceClearSavedMove": "مسح النقل المحفوظ", - "mobileSomethingWentWrong": "لقد حدث خطأ ما.", "mobileShowResult": "إظهار النتيجة", - "mobilePuzzleThemesSubtitle": "حُل الألغاز المتعلّقة بافتتاحاتك المفضّلة، أو اختر موضوعاً.", - "mobilePuzzleStormSubtitle": "حل أكبر عدد ممكن من الألغاز في 3 دقائق.", - "mobileGreeting": "مرحبا، {param}", - "mobileGreetingWithoutName": "مرحبا", - "mobilePrefMagnifyDraggedPiece": "تكبير القطعة المسحوبة", + "mobileShowVariations": "أظهر سلسلة النقلات المرشحة", + "mobileSomethingWentWrong": "لقد حدث خطأ ما.", + "mobileSystemColors": "ألوان النظام", + "mobileToolsTab": "أدوات", + "mobileWaitingForOpponentToJoin": "في انتظار انضمام الطرف الآخر...", + "mobileWatchTab": "شاهد", "activityActivity": "الأنشطة", "activityHostedALiveStream": "بدأ بث مباشر", "activityRankedInSwissTournament": "حائز على تصنيف #{param1} في {param2}", @@ -232,6 +232,7 @@ "preferencesNotifyWeb": "المتصفح", "preferencesNotifyDevice": "الجهاز", "preferencesBellNotificationSound": "صوت التنبيه", + "preferencesBlindfold": "معصوب العينين", "puzzlePuzzles": "الألغاز", "puzzlePuzzleThemes": "خصائص الألغاز", "puzzleRecommended": "مقترح", @@ -549,7 +550,6 @@ "replayMode": "نمط إعادة العرض", "realtimeReplay": "ذات الوقت", "byCPL": "بالاثارة", - "openStudy": "فتح دراسة", "enable": "تفعيل", "bestMoveArrow": "سهم أفضل نقلة", "showVariationArrows": "أظهر سلسلة النقلات المرشحة", @@ -756,7 +756,6 @@ "block": "حظر", "blocked": "محظور", "unblock": "إلغاء الحظر", - "followsYou": "يتابعك", "xStartedFollowingY": "{param1} بدأ متابعة {param2}", "more": "المزيد", "memberSince": "مسجل منذ", diff --git a/lib/l10n/lila_az.arb b/lib/l10n/lila_az.arb index 5b3bb5686c..b2853ea2a5 100644 --- a/lib/l10n/lila_az.arb +++ b/lib/l10n/lila_az.arb @@ -432,7 +432,6 @@ "replayMode": "Təkrar rejimi", "realtimeReplay": "Gerçək zamanlı", "byCPL": "CPL üzrə", - "openStudy": "Çalışmanı aç", "enable": "Aktiv et", "bestMoveArrow": "Ən yaxşı gedişi göstər", "showVariationArrows": "Variasiya oxlarını göstərin", @@ -635,7 +634,6 @@ "block": "Blok", "blocked": "Bloklanıb", "unblock": "Blokdan çıxart", - "followsYou": "Səni izləyir", "xStartedFollowingY": "{param1} {param2} adlı oyunçunu izləməyə başladı", "more": "Daha çox", "memberSince": "Üzvlük tarixi", diff --git a/lib/l10n/lila_be.arb b/lib/l10n/lila_be.arb index 245123ff66..4d3cd2c043 100644 --- a/lib/l10n/lila_be.arb +++ b/lib/l10n/lila_be.arb @@ -1,14 +1,14 @@ { + "mobileAreYouSure": "Вы ўпэўнены?", + "mobileClearButton": "Ачысціць", "mobileHomeTab": "Галоўная", - "mobilePuzzlesTab": "Задачы", - "mobileSettingsTab": "Налады", + "mobileNoSearchResults": "Няма вынікаў", "mobileOkButton": "Добра", - "mobileSettingsImmersiveMode": "Поўнаэкранны рэжым", - "mobileRecentSearches": "Нядаўнія пошукі", - "mobileClearButton": "Ачысціць", "mobilePlayersMatchingSearchTerm": "Гульцы з «{param}»", - "mobileNoSearchResults": "Няма вынікаў", - "mobileAreYouSure": "Вы ўпэўнены?", + "mobilePuzzlesTab": "Задачы", + "mobileRecentSearches": "Нядаўнія пошукі", + "mobileSettingsImmersiveMode": "Поўнаэкранны рэжым", + "mobileSettingsTab": "Налады", "activityActivity": "Актыўнасць", "activityHostedALiveStream": "Правялі прамую трансляцыю", "activityRankedInSwissTournament": "Скончыў на {param1} месцы ў {param2}", @@ -441,7 +441,7 @@ "analysis": "Дошка для аналіза", "depthX": "Глыбіня {param}", "usingServerAnalysis": "Выкарыстоўваецца серверны аналіз", - "loadingEngine": "Загружаем шахматную праграму...", + "loadingEngine": "Загружаем рухавічок...", "calculatingMoves": "Пралічваем хады...", "engineFailed": "Памылка пры загрузцы шахматнай праграмы", "cloudAnalysis": "Воблачны аналіз", @@ -492,7 +492,6 @@ "replayMode": "Рэжым паўтору", "realtimeReplay": "У рэальным часе", "byCPL": "Цікавае", - "openStudy": "Адкрыць навучанне", "enable": "Уключыць", "bestMoveArrow": "Паказваць стрэлкай найлепшы ход", "evaluationGauge": "Шкала ацэнкі", @@ -694,7 +693,6 @@ "block": "Заблакаваць", "blocked": "Заблакаваны", "unblock": "Разблакіраваць", - "followsYou": "Падпісаны на вас", "xStartedFollowingY": "{param1} падпісаўся на {param2}", "more": "Яшчэ", "memberSince": "Далучыўся", diff --git a/lib/l10n/lila_bg.arb b/lib/l10n/lila_bg.arb index 903fbdb4bf..8d6ebe5654 100644 --- a/lib/l10n/lila_bg.arb +++ b/lib/l10n/lila_bg.arb @@ -1,33 +1,33 @@ { + "mobileAllGames": "Всички игри", + "mobileAreYouSure": "Сигурни ли сте?", + "mobileClearButton": "Изчисти", + "mobileFeedbackButton": "Отзиви", + "mobileGreeting": "Здравейте, {param}", + "mobileGreetingWithoutName": "Здравейте", + "mobileHideVariation": "Скрий вариацията", "mobileHomeTab": "Начало", - "mobilePuzzlesTab": "Задачи", - "mobileToolsTab": "Анализ", - "mobileWatchTab": "Гледай", - "mobileSettingsTab": "Настройки", "mobileMustBeLoggedIn": "За да видите тази страница, трябва да влезете в профила си.", - "mobileSystemColors": "Системни цветове", - "mobileFeedbackButton": "Отзиви", + "mobileNoSearchResults": "Няма резултати", "mobileOkButton": "ОК", + "mobilePuzzleStormSubtitle": "Решете колкото можете повече задачи за 3 минути.", + "mobilePuzzleThemesSubtitle": "Решавайте задачи от любимите Ви дебюти или изберете друга тема.", + "mobilePuzzlesTab": "Задачи", + "mobileRecentSearches": "Последни търсения", "mobileSettingsHapticFeedback": "Вибрация при докосване", "mobileSettingsImmersiveMode": "Режим \"Цял екран\"", - "mobileAllGames": "Всички игри", - "mobileRecentSearches": "Последни търсения", - "mobileClearButton": "Изчисти", - "mobileNoSearchResults": "Няма резултати", - "mobileAreYouSure": "Сигурни ли сте?", - "mobileSharePuzzle": "Сподели тази задача", - "mobileShareGameURL": "Сподели URL на играта", + "mobileSettingsTab": "Настройки", "mobileShareGamePGN": "Сподели PGN", + "mobileShareGameURL": "Сподели URL на играта", "mobileSharePositionAsFEN": "Сподели позицията във формат FEN", - "mobileShowVariations": "Покажи вариациите", - "mobileHideVariation": "Скрий вариацията", + "mobileSharePuzzle": "Сподели тази задача", "mobileShowComments": "Покажи коментарите", - "mobileSomethingWentWrong": "Възникна грешка.", "mobileShowResult": "Покажи резултат", - "mobilePuzzleThemesSubtitle": "Решавайте задачи от любимите Ви дебюти или изберете друга тема.", - "mobilePuzzleStormSubtitle": "Решете колкото можете повече задачи за 3 минути.", - "mobileGreeting": "Здравейте, {param}", - "mobileGreetingWithoutName": "Здравейте", + "mobileShowVariations": "Покажи вариациите", + "mobileSomethingWentWrong": "Възникна грешка.", + "mobileSystemColors": "Системни цветове", + "mobileToolsTab": "Анализ", + "mobileWatchTab": "Гледай", "activityActivity": "Дейност", "activityHostedALiveStream": "Стартира предаване на живо", "activityRankedInSwissTournament": "Рейтинг #{param1} от {param2}", @@ -80,6 +80,14 @@ "broadcastFideFederations": "ФИДЕ федерации", "broadcastFideProfile": "ФИДЕ профил", "broadcastFederation": "Федерация", + "broadcastOpenLichess": "Отвори в Lichess", + "broadcastTeams": "Отбори", + "broadcastBoards": "Дъски", + "broadcastOverview": "Общ преглед", + "broadcastOfficialWebsite": "Официален уебсайт", + "broadcastStandings": "Класиране", + "broadcastGamesThisTournament": "Игри в този турнир", + "broadcastScore": "Резултат", "broadcastNbBroadcasts": "{count, plural, =1{{count} излъчване} other{{count} излъчвания}}", "challengeChallengesX": "Предизвикателства: {param1}", "challengeChallengeToPlay": "Предизвикайте на партия", @@ -521,7 +529,6 @@ "replayMode": "Режим на повторение", "realtimeReplay": "В реално време", "byCPL": "По CPL", - "openStudy": "Проучване", "enable": "Включване", "bestMoveArrow": "Показване на най-добър ход", "showVariationArrows": "Показване на стрелки за вариациите", @@ -727,7 +734,6 @@ "block": "Блокирай", "blocked": "Блокирани", "unblock": "Отблокирай", - "followsYou": "Следва ви", "xStartedFollowingY": "{param1} започна да следва {param2}", "more": "Още", "memberSince": "Член от", @@ -1218,6 +1224,7 @@ "instructions": "Инструкции", "showMeEverything": "Покажи ми всичко", "lichessPatronInfo": "Lichess е благотворителна организация и работи с напълно безплатен софтуер и отворен код. Всички разходи за опериране, разработка и съдържание са финансирани единствено от дарения от потребителите ни.", + "stats": "Статистика", "opponentLeftCounter": "{count, plural, =1{Опонентът напусна играта. Можете да заявите победа след {count} секунди.} other{Опонентът напусна играта. Можете да заявите победа след {count} секунди.}}", "mateInXHalfMoves": "{count, plural, =1{Мат в {count} полуход} other{Мат в {count} полухода}}", "nbBlunders": "{count, plural, =1{{count} груба грешка} other{{count} груби грешки}}", diff --git a/lib/l10n/lila_bn.arb b/lib/l10n/lila_bn.arb index 34b1ccc246..c1683f7564 100644 --- a/lib/l10n/lila_bn.arb +++ b/lib/l10n/lila_bn.arb @@ -410,7 +410,6 @@ "replayMode": "উত্তর ধরন", "realtimeReplay": "সঠিকসময়", "byCPL": "CPL দ্বারা", - "openStudy": "মুক্ত অধ্যয়ন", "enable": "সচল", "bestMoveArrow": "সরানোর উত্তম চিহ্ন", "showVariationArrows": "বৈচিত্র্য তীর দেখান", @@ -616,7 +615,6 @@ "block": "বাধা দিন", "blocked": "বাধাগ্রস্ত", "unblock": "বাধা উঠিয়ে নিন", - "followsYou": "আপনাকে অনুসরণ করছে", "xStartedFollowingY": "{param1} অনুসরণ করা শুরু করেছেন {param2}", "more": "আরও", "memberSince": "সদস্য রয়েছেন", diff --git a/lib/l10n/lila_br.arb b/lib/l10n/lila_br.arb index 4ef366adb4..2fc6fead20 100644 --- a/lib/l10n/lila_br.arb +++ b/lib/l10n/lila_br.arb @@ -294,7 +294,6 @@ "replayMode": "Mod adwelet", "realtimeReplay": "Amzer wirion", "byCPL": "Dre CPL", - "openStudy": "Digeriñ ar studi", "enable": "Enaouiñ", "bestMoveArrow": "Bir ar gwellañ fiñvadenn", "evaluationGauge": "Jaoj priziañ", @@ -490,7 +489,6 @@ "block": "Stankañ", "blocked": "Stanket", "unblock": "Distankañ", - "followsYou": "Ho heuilh", "xStartedFollowingY": "{param1} zo krog da heuliañ {param2}", "more": "Muioc'h", "memberSince": "Ezel abaoe an/ar", @@ -1128,6 +1126,9 @@ "studyDeleteTheStudyChatHistory": "Dilemel an istor-flapañ? Hep distro e vo!", "studyDeleteStudy": "Dilemel ar studiadenn", "studyWhereDoYouWantToStudyThat": "Pelec'h ho peus c'hoant da studiañ se?", + "studyGoodMove": "Fiñvadenn vat", + "studyMistake": "Fazi", + "studyBlunder": "Bourd", "studyNbChapters": "{count, plural, =1{{count} pennad} =2{{count} pennad} few{{count} pennad} many{{count} pennad} other{{count} pennad}}", "studyNbGames": "{count, plural, =1{{count} C'hoariadenn} =2{{count} C'hoariadenn} few{{count} C'hoariadenn} many{{count} C'hoariadenn} other{{count} C'hoariadenn}}", "studyNbMembers": "{count, plural, =1{{count} Ezel} =2{{count} Ezel} few{{count} Ezel} many{{count} Ezel} other{{count} Ezel}}", diff --git a/lib/l10n/lila_bs.arb b/lib/l10n/lila_bs.arb index ad2c1392df..924910ac29 100644 --- a/lib/l10n/lila_bs.arb +++ b/lib/l10n/lila_bs.arb @@ -481,7 +481,6 @@ "replayMode": "Repriza partije", "realtimeReplay": "U stvarnom vremenu", "byCPL": "Po SDP", - "openStudy": "Otvori studiju", "enable": "Omogući", "bestMoveArrow": "Strelica za najbolji potez", "showVariationArrows": "Prikaži strelice za varijante", @@ -684,7 +683,6 @@ "block": "Blokirajte", "blocked": "Blokiran", "unblock": "Odblokiraj", - "followsYou": "Prati vas", "xStartedFollowingY": "{param1} je počeo pratiti {param2}", "more": "Više", "memberSince": "Član od", diff --git a/lib/l10n/lila_ca.arb b/lib/l10n/lila_ca.arb index b9f9d21d81..4d891e003b 100644 --- a/lib/l10n/lila_ca.arb +++ b/lib/l10n/lila_ca.arb @@ -1,45 +1,46 @@ { + "mobileAllGames": "Totes les partides", + "mobileAreYouSure": "Estàs segur?", + "mobileBlindfoldMode": "A la cega", + "mobileCancelTakebackOffer": "Anul·la la petició per desfer la jugada", + "mobileClearButton": "Neteja", + "mobileCorrespondenceClearSavedMove": "Elimina la jugada guardada", + "mobileCustomGameJoinAGame": "Unir-se a una partida", + "mobileFeedbackButton": "Suggeriments", + "mobileGreeting": "Hola, {param}", + "mobileGreetingWithoutName": "Hola", + "mobileHideVariation": "Amaga les variacions", "mobileHomeTab": "Inici", - "mobilePuzzlesTab": "Problemes", - "mobileToolsTab": "Eines", - "mobileWatchTab": "Visualitza", - "mobileSettingsTab": "Configuració", + "mobileLiveStreamers": "Retransmissors en directe", "mobileMustBeLoggedIn": "Has d'estar connectat per veure aquesta pàgina.", - "mobileSystemColors": "Colors del sistema", - "mobileFeedbackButton": "Suggeriments", - "mobileOkButton": "D'acord", - "mobileSettingsHapticFeedback": "Resposta hàptica", - "mobileSettingsImmersiveMode": "Mode immersiu", + "mobileNoSearchResults": "Sense resultats", "mobileNotFollowingAnyUser": "No estàs seguint a cap usuari.", - "mobileAllGames": "Totes les partides", - "mobileRecentSearches": "Cerques recents", - "mobileClearButton": "Neteja", + "mobileOkButton": "D'acord", "mobilePlayersMatchingSearchTerm": "Jugadors amb \"{param}\"", - "mobileNoSearchResults": "Sense resultats", - "mobileAreYouSure": "Estàs segur?", - "mobilePuzzleStreakAbortWarning": "Perdreu la vostra ratxa i la vostra puntuació es guardarà.", + "mobilePuzzleStormConfirmEndRun": "Voleu acabar aquesta ronda?", + "mobilePuzzleStormFilterNothingToShow": "Res a mostrar, si us plau canvieu els filtres", "mobilePuzzleStormNothingToShow": "Res a mostrar. Fes algunes rondes al Puzzle Storm.", - "mobileSharePuzzle": "Comparteix aquest problema", - "mobileShareGameURL": "Comparteix l'enllaç a la partida", + "mobilePuzzleStormSubtitle": "Resoleu el màxim nombre de problemes en 3 minuts.", + "mobilePuzzleStreakAbortWarning": "Perdreu la vostra ratxa i la vostra puntuació es guardarà.", + "mobilePuzzleThemesSubtitle": "Resoleu problemes de les vostres obertures preferides o seleccioneu una temàtica.", + "mobilePuzzlesTab": "Problemes", + "mobileRecentSearches": "Cerques recents", + "mobileSettingsHapticFeedback": "Resposta hàptica", + "mobileSettingsImmersiveMode": "Mode immersiu", + "mobileSettingsTab": "Configuració", "mobileShareGamePGN": "Comparteix PGN", + "mobileShareGameURL": "Comparteix l'enllaç a la partida", "mobileSharePositionAsFEN": "Comparteix la posició com a FEN", - "mobileShowVariations": "Mostra les variacions", - "mobileHideVariation": "Amaga les variacions", + "mobileSharePuzzle": "Comparteix aquest problema", "mobileShowComments": "Mostra comentaris", - "mobilePuzzleStormConfirmEndRun": "Voleu acabar aquesta ronda?", - "mobilePuzzleStormFilterNothingToShow": "Res a mostrar, si us plau canvieu els filtres", - "mobileCancelTakebackOffer": "Anul·la la petició per desfer la jugada", - "mobileWaitingForOpponentToJoin": "Esperant que s'uneixi l'adversari...", - "mobileBlindfoldMode": "A la cega", - "mobileLiveStreamers": "Retransmissors en directe", - "mobileCustomGameJoinAGame": "Unir-se a una partida", - "mobileCorrespondenceClearSavedMove": "Elimina la jugada guardada", - "mobileSomethingWentWrong": "Alguna cosa ha anat malament.", "mobileShowResult": "Mostra el resultat", - "mobilePuzzleThemesSubtitle": "Resoleu problemes de les vostres obertures preferides o seleccioneu una temàtica.", - "mobilePuzzleStormSubtitle": "Resoleu el màxim nombre de problemes en 3 minuts.", - "mobileGreeting": "Hola, {param}", - "mobileGreetingWithoutName": "Hola", + "mobileShowVariations": "Mostra les variacions", + "mobileSomethingWentWrong": "Alguna cosa ha anat malament.", + "mobileSystemColors": "Colors del sistema", + "mobileTheme": "Tema", + "mobileToolsTab": "Eines", + "mobileWaitingForOpponentToJoin": "Esperant que s'uneixi l'adversari...", + "mobileWatchTab": "Visualitza", "activityActivity": "Activitat", "activityHostedALiveStream": "Has fet una retransmissió en directe", "activityRankedInSwissTournament": "Classificat #{param1} en {param2}", @@ -119,6 +120,7 @@ "broadcastNotYetStarted": "La retransmissió encara no ha començat.", "broadcastOfficialWebsite": "Lloc web oficial", "broadcastStandings": "Classificació", + "broadcastOfficialStandings": "Classificació oficial", "broadcastIframeHelp": "Més opcions a la {param}", "broadcastWebmastersPage": "pàgina d'administració", "broadcastPgnSourceHelp": "Un origen públic en PGN públic en temps real d'aquesta ronda. També oferim un {param} per una sincronització més ràpida i eficient.", @@ -127,6 +129,15 @@ "broadcastRatingDiff": "Diferència puntuació", "broadcastGamesThisTournament": "Partides en aquest torneig", "broadcastScore": "Puntuació", + "broadcastAllTeams": "Tots els equips", + "broadcastTournamentFormat": "Format del torneig", + "broadcastTournamentLocation": "Ubicació del torneig", + "broadcastTopPlayers": "Millors jugadors", + "broadcastTimezone": "Zona horària", + "broadcastFideRatingCategory": "Categoria puntuació FIDE", + "broadcastOptionalDetails": "Detalls opcionals", + "broadcastPastBroadcasts": "Retransmissions finalitzades", + "broadcastAllBroadcastsByMonth": "Veure totes les retransmissions per més", "broadcastNbBroadcasts": "{count, plural, =1{{count} retransmissió} other{{count} retransmissions}}", "challengeChallengesX": "Desafiaments: {param1}", "challengeChallengeToPlay": "Desafia a una partida", @@ -251,6 +262,7 @@ "preferencesNotifyWeb": "Navegador", "preferencesNotifyDevice": "Dispositiu", "preferencesBellNotificationSound": "So de notificació", + "preferencesBlindfold": "A la cega", "puzzlePuzzles": "Problemes", "puzzlePuzzleThemes": "Temàtiques de problemes", "puzzleRecommended": "Recomanat", @@ -568,7 +580,6 @@ "replayMode": "Mode de reproducció", "realtimeReplay": "En temps real", "byCPL": "Per CPL", - "openStudy": "Obrir estudi", "enable": "Habilitar", "bestMoveArrow": "Fletxa de la millor jugada", "showVariationArrows": "Mostrar fletxes de les variants", @@ -775,7 +786,6 @@ "block": "Bloqueja", "blocked": "Bloquejat", "unblock": "Desbloqueja", - "followsYou": "T'està seguint", "xStartedFollowingY": "{param1} ha començat a seguir {param2}", "more": "Més", "memberSince": "Membre des del", @@ -1529,6 +1539,7 @@ "studyPlayAgain": "Torna a jugar", "studyWhatWouldYouPlay": "Que jugaríeu en aquesta posició?", "studyYouCompletedThisLesson": "Enhorabona, heu completat aquesta lliçó.", + "studyPerPage": "{param} per pàgina", "studyNbChapters": "{count, plural, =1{{count} Capítol} other{{count} Capítols}}", "studyNbGames": "{count, plural, =1{{count} Joc} other{{count} Jocs}}", "studyNbMembers": "{count, plural, =1{{count} Membre} other{{count} Membres}}", diff --git a/lib/l10n/lila_cs.arb b/lib/l10n/lila_cs.arb index 6743885eca..f5e4043905 100644 --- a/lib/l10n/lila_cs.arb +++ b/lib/l10n/lila_cs.arb @@ -1,31 +1,31 @@ { + "mobileAreYouSure": "Jste si jistý?", + "mobileBlindfoldMode": "Páska přes oči", + "mobileCancelTakebackOffer": "Zrušit nabídnutí vrácení tahu", "mobileClearButton": "Vymazat", - "mobilePlayersMatchingSearchTerm": "Hráči s \"{param}\"", + "mobileCorrespondenceClearSavedMove": "Vymazat uložené tahy", + "mobileCustomGameJoinAGame": "Připojit se ke hře", + "mobileGreeting": "Ahoj, {param}", + "mobileGreetingWithoutName": "Ahoj", + "mobileHideVariation": "Schovej variace", + "mobileLiveStreamers": "Živé vysílání", "mobileNoSearchResults": "Žádné výsledky", - "mobileAreYouSure": "Jste si jistý?", - "mobilePuzzleStreakAbortWarning": "Ztratíte aktuální sérii a vaše skóre bude uloženo.", + "mobilePlayersMatchingSearchTerm": "Hráči s \"{param}\"", + "mobilePuzzleStormConfirmEndRun": "Chceš ukončit tento běh?", + "mobilePuzzleStormFilterNothingToShow": "Nic k zobrazení, prosím změn filtry", "mobilePuzzleStormNothingToShow": "Nic k zobrazení. Zahrajte si nějaké běhy Bouřky úloh.", - "mobileSharePuzzle": "Sdílej tuto úlohu", - "mobileShareGameURL": "Sdílet URL hry", + "mobilePuzzleStormSubtitle": "Vyřeš co nejvíce úloh co dokážeš za 3 minuty.", + "mobilePuzzleStreakAbortWarning": "Ztratíte aktuální sérii a vaše skóre bude uloženo.", + "mobilePuzzleThemesSubtitle": "Hraj úlohy z tvých oblíbených zahájení, nebo si vyber styl.", "mobileShareGamePGN": "Sdílet PGN", + "mobileShareGameURL": "Sdílet URL hry", "mobileSharePositionAsFEN": "Sdílet pozici jako FEN", - "mobileShowVariations": "Zobraz variace", - "mobileHideVariation": "Schovej variace", + "mobileSharePuzzle": "Sdílej tuto úlohu", "mobileShowComments": "Zobraz komentáře", - "mobilePuzzleStormConfirmEndRun": "Chceš ukončit tento běh?", - "mobilePuzzleStormFilterNothingToShow": "Nic k zobrazení, prosím změn filtry", - "mobileCancelTakebackOffer": "Zrušit nabídnutí vrácení tahu", - "mobileWaitingForOpponentToJoin": "Čeká se na připojení protihráče...", - "mobileBlindfoldMode": "Páska přes oči", - "mobileLiveStreamers": "Živé vysílání", - "mobileCustomGameJoinAGame": "Připojit se ke hře", - "mobileCorrespondenceClearSavedMove": "Vymazat uložené tahy", - "mobileSomethingWentWrong": "Něco se pokazilo.", "mobileShowResult": "Zobrazit výsledky", - "mobilePuzzleThemesSubtitle": "Hrej úlohy z tvých oblíbených zahájení, nebo si vyber styl.", - "mobilePuzzleStormSubtitle": "Vyřeš co nejvíce úloh co dokážeš za 3 minuty.", - "mobileGreeting": "Ahoj, {param}", - "mobileGreetingWithoutName": "Ahoj", + "mobileShowVariations": "Zobraz variace", + "mobileSomethingWentWrong": "Něco se pokazilo.", + "mobileWaitingForOpponentToJoin": "Čeká se na připojení protihráče...", "activityActivity": "Aktivita", "activityHostedALiveStream": "Hostoval živý stream", "activityRankedInSwissTournament": "{param1}. místo v turnaji {param2}", @@ -93,6 +93,20 @@ "broadcastAgeThisYear": "Věk tento rok", "broadcastUnrated": "Nehodnocen", "broadcastRecentTournaments": "Nedávné tournamenty", + "broadcastOpenLichess": "Otevřít v Lichess", + "broadcastTeams": "Týmy", + "broadcastBoards": "Šachovnice", + "broadcastOverview": "Přehled", + "broadcastUploadImage": "Nahrát obrázek turnaje", + "broadcastNoBoardsYet": "Zatím žádné šachovnice. Ty se zobrazí se po nahrání partií.", + "broadcastStartsAfter": "Začíná po {param}", + "broadcastStartVerySoon": "Vysílání začne velmi brzy.", + "broadcastNotYetStarted": "Vysílání ještě nezačalo.", + "broadcastOfficialWebsite": "Oficiální stránka", + "broadcastStandings": "Pořadí", + "broadcastOfficialStandings": "Oficiální pořadí", + "broadcastIframeHelp": "Více možností na {param}", + "broadcastScore": "Skóre", "broadcastNbBroadcasts": "{count, plural, =1{{count} vysílání} few{{count} vysílání} many{{count} vysílání} other{{count} vysílání}}", "challengeChallengesX": "Výzvy: {param1}", "challengeChallengeToPlay": "Vyzvat k partii", @@ -217,6 +231,7 @@ "preferencesNotifyWeb": "Prohlížeč", "preferencesNotifyDevice": "Zařízení", "preferencesBellNotificationSound": "Typ zvukového upozornění", + "preferencesBlindfold": "Páska přes oči", "puzzlePuzzles": "Úlohy", "puzzlePuzzleThemes": "Motivy úloh", "puzzleRecommended": "Doporučené", @@ -534,7 +549,6 @@ "replayMode": "Mód přehrávání", "realtimeReplay": "Jako při hře", "byCPL": "Dle CPL", - "openStudy": "Otevřít studii", "enable": "Povolit analýzu", "bestMoveArrow": "Šipka pro nejlepší tah", "showVariationArrows": "Zobrazit šipky variant", @@ -625,6 +639,7 @@ "rank": "Pořadí", "rankX": "Pořadí: {param}", "gamesPlayed": "Odehraných partií", + "ok": "OK", "cancel": "Zrušit", "whiteTimeOut": "Bílému došel čas", "blackTimeOut": "Černému došel čas", @@ -741,7 +756,6 @@ "block": "Blokovat", "blocked": "Blokován", "unblock": "Odblokovat", - "followsYou": "Vás sleduje", "xStartedFollowingY": "{param1} začal sledovat {param2}", "more": "Více", "memberSince": "Členem od", @@ -1247,6 +1261,7 @@ "showMeEverything": "Ukaž mi všechno", "lichessPatronInfo": "Lichess je bezplatný a zcela svobodný/nezávislý softvér s otevřeným zdrojovým kódem.\nVeškeré provozní náklady, vývoj a obsah jsou financovány výhradně z příspěvků uživatelů.", "nothingToSeeHere": "Momentálně zde není nic k vidění.", + "stats": "Statistiky", "opponentLeftCounter": "{count, plural, =1{Tvůj soupeř opustil hru. Můžeš si vyžádat vítězství za {count} sekundu.} few{Tvůj soupeř opustil hru. Můžeš si vyžádat vítězství za {count} sekundy.} many{Tvůj soupeř opustil hru. Můžeš si vyžádat vítězství za {count} sekund.} other{Tvůj soupeř opustil hru. Můžeš si vyžádat vítězství za {count} sekund.}}", "mateInXHalfMoves": "{count, plural, =1{Mat v {count} půltahu} few{Mat v {count} půltazích} many{Mat v {count} půltazích} other{Mat v {count} půltazích}}", "nbBlunders": "{count, plural, =1{{count} hrubá chyba} few{{count} hrubé chyby} many{{count} hrubých chyb} other{{count} hrubých chyb}}", @@ -1494,6 +1509,7 @@ "studyPlayAgain": "Hrát znovu", "studyWhatWouldYouPlay": "Co byste v této pozici hráli?", "studyYouCompletedThisLesson": "Blahopřejeme! Dokončili jste tuto lekci.", + "studyPerPage": "{param} na stránku", "studyNbChapters": "{count, plural, =1{{count} kapitola} few{{count} kapitoly} many{{count} kapitol} other{{count} kapitol}}", "studyNbGames": "{count, plural, =1{{count} hra} few{{count} hry} many{{count} her} other{{count} her}}", "studyNbMembers": "{count, plural, =1{{count} člen} few{{count} členi} many{{count} členů} other{{count} členů}}", diff --git a/lib/l10n/lila_da.arb b/lib/l10n/lila_da.arb index 7233f2c3af..53ce39ecb3 100644 --- a/lib/l10n/lila_da.arb +++ b/lib/l10n/lila_da.arb @@ -1,47 +1,48 @@ { + "mobileAllGames": "Alle partier", + "mobileAreYouSure": "Er du sikker?", + "mobileBlindfoldMode": "Bind for øjnene", + "mobileCancelTakebackOffer": "Annuller tilbud om tilbagetagelse", + "mobileClearButton": "Ryd", + "mobileCorrespondenceClearSavedMove": "Ryd gemt træk", + "mobileCustomGameJoinAGame": "Deltag i et parti", + "mobileFeedbackButton": "Feedback", + "mobileGreeting": "Hej, {param}", + "mobileGreetingWithoutName": "Hej", + "mobileHideVariation": "Skjul variation", "mobileHomeTab": "Hjem", - "mobilePuzzlesTab": "Opgaver", - "mobileToolsTab": "Værktøjer", - "mobileWatchTab": "Se", - "mobileSettingsTab": "Indstillinger", + "mobileLiveStreamers": "Live-streamere", "mobileMustBeLoggedIn": "Du skal være logget ind for at se denne side.", - "mobileSystemColors": "Systemfarver", - "mobileFeedbackButton": "Feedback", + "mobileNoSearchResults": "Ingen resultater", + "mobileNotFollowingAnyUser": "Du følger ikke nogen brugere.", "mobileOkButton": "Ok", + "mobilePlayersMatchingSearchTerm": "Spillere med \"{param}\"", + "mobilePrefMagnifyDraggedPiece": "Forstør brik, som trækkes", + "mobilePuzzleStormConfirmEndRun": "Vil du afslutte dette løb?", + "mobilePuzzleStormFilterNothingToShow": "Intet at vise, ændr venligst filtre", + "mobilePuzzleStormNothingToShow": "Intet at vise. Spil nogle runder af Puzzle Storm.", + "mobilePuzzleStormSubtitle": "Løs så mange opgaver som muligt på 3 minutter.", + "mobilePuzzleStreakAbortWarning": "Du vil miste din nuværende stime og din score vil blive gemt.", + "mobilePuzzleThemesSubtitle": "Spil opgaver fra dine foretrukne åbninger, eller vælg et tema.", + "mobilePuzzlesTab": "Opgaver", + "mobileRecentSearches": "Seneste søgninger", "mobileSettingsHapticFeedback": "Haptisk feedback", "mobileSettingsImmersiveMode": "Fordybelsestilstand", "mobileSettingsImmersiveModeSubtitle": "Skjul systemets brugergrænseflade, mens du spiller. Brug denne funktion, hvis du er generet af systemets navigationsbevægelser i kanterne af skærmen. Gælder for parti- og Puzzle Storm-skærme.", - "mobileNotFollowingAnyUser": "Du følger ikke nogen brugere.", - "mobileAllGames": "Alle partier", - "mobileRecentSearches": "Seneste søgninger", - "mobileClearButton": "Ryd", - "mobilePlayersMatchingSearchTerm": "Spillere med \"{param}\"", - "mobileNoSearchResults": "Ingen resultater", - "mobileAreYouSure": "Er du sikker?", - "mobilePuzzleStreakAbortWarning": "Du vil miste din nuværende stime og din score vil blive gemt.", - "mobilePuzzleStormNothingToShow": "Intet at vise. Spil nogle runder af Puzzle Storm.", - "mobileSharePuzzle": "Del denne opgave", - "mobileShareGameURL": "Del partiets URL", + "mobileSettingsTab": "Indstillinger", "mobileShareGamePGN": "Del PGN", + "mobileShareGameURL": "Del partiets URL", "mobileSharePositionAsFEN": "Del position som FEN", - "mobileShowVariations": "Vis variationer", - "mobileHideVariation": "Skjul variation", + "mobileSharePuzzle": "Del denne opgave", "mobileShowComments": "Vis kommentarer", - "mobilePuzzleStormConfirmEndRun": "Vil du afslutte dette løb?", - "mobilePuzzleStormFilterNothingToShow": "Intet at vise, ændr venligst filtre", - "mobileCancelTakebackOffer": "Annuller tilbud om tilbagetagelse", - "mobileWaitingForOpponentToJoin": "Venter på at modstander slutter sig til...", - "mobileBlindfoldMode": "Bind for øjnene", - "mobileLiveStreamers": "Live-streamere", - "mobileCustomGameJoinAGame": "Deltag i et parti", - "mobileCorrespondenceClearSavedMove": "Ryd gemt træk", - "mobileSomethingWentWrong": "Noget gik galt.", "mobileShowResult": "Vis resultat", - "mobilePuzzleThemesSubtitle": "Spil opgaver fra dine foretrukne åbninger, eller vælg et tema.", - "mobilePuzzleStormSubtitle": "Løs så mange opgaver som muligt på 3 minutter.", - "mobileGreeting": "Hej, {param}", - "mobileGreetingWithoutName": "Hej", - "mobilePrefMagnifyDraggedPiece": "Forstør brik, som trækkes", + "mobileShowVariations": "Vis variationer", + "mobileSomethingWentWrong": "Noget gik galt.", + "mobileSystemColors": "Systemfarver", + "mobileTheme": "Tema", + "mobileToolsTab": "Værktøjer", + "mobileWaitingForOpponentToJoin": "Venter på at modstander slutter sig til...", + "mobileWatchTab": "Se", "activityActivity": "Aktivitet", "activityHostedALiveStream": "Hostede en livestream", "activityRankedInSwissTournament": "Rangeret #{param1} i {param2}", @@ -122,6 +123,7 @@ "broadcastNotYetStarted": "Udsendelsen er endnu ikke startet.", "broadcastOfficialWebsite": "Officielt websted", "broadcastStandings": "Stillinger", + "broadcastOfficialStandings": "Officiel stilling", "broadcastIframeHelp": "Flere muligheder på {param}", "broadcastWebmastersPage": "webmasters side", "broadcastPgnSourceHelp": "En offentlig, realtids PGN-kilde til denne runde. Vi tilbyder også en {param} for hurtigere og mere effektiv synkronisering.", @@ -130,6 +132,15 @@ "broadcastRatingDiff": "Rating-forskel", "broadcastGamesThisTournament": "Partier i denne turnering", "broadcastScore": "Score", + "broadcastAllTeams": "Alle hold", + "broadcastTournamentFormat": "Turneringsformat", + "broadcastTournamentLocation": "Turneringssted", + "broadcastTopPlayers": "Topspillere", + "broadcastTimezone": "Tidszone", + "broadcastFideRatingCategory": "FIDE-ratingkategori", + "broadcastOptionalDetails": "Valgfri detaljer", + "broadcastPastBroadcasts": "Tidligere udsendelser", + "broadcastAllBroadcastsByMonth": "Vis alle udsendelser efter måned", "broadcastNbBroadcasts": "{count, plural, =1{{count} udsendelse} other{{count} udsendelser}}", "challengeChallengesX": "Udfordringer: {param1}", "challengeChallengeToPlay": "Udfordr til et spil", @@ -254,6 +265,7 @@ "preferencesNotifyWeb": "Browser", "preferencesNotifyDevice": "Enhed", "preferencesBellNotificationSound": "Notifikationslyd", + "preferencesBlindfold": "Blindskak", "puzzlePuzzles": "Taktikopgaver", "puzzlePuzzleThemes": "Opgavetemaer", "puzzleRecommended": "Anbefalet", @@ -513,7 +525,7 @@ "blackDidntMove": "Sort flyttede ikke", "requestAComputerAnalysis": "Anmod om en computeranalyse", "computerAnalysis": "Computeranalyse", - "computerAnalysisAvailable": "Computeranalyse klar", + "computerAnalysisAvailable": "Computeranalyse tilgængelig", "computerAnalysisDisabled": "Computeranalyse deaktiveret", "analysis": "Analysebræt", "depthX": "Dybde {param}", @@ -571,7 +583,6 @@ "replayMode": "Genafspilning", "realtimeReplay": "Realtid", "byCPL": "CBT", - "openStudy": "Åben studie", "enable": "Aktivér", "bestMoveArrow": "Bedste træk pil", "showVariationArrows": "Vis variantpile", @@ -779,7 +790,6 @@ "block": "Blokér", "blocked": "Blokeret", "unblock": "Stop blokering", - "followsYou": "Følger dig", "xStartedFollowingY": "{param1} følger nu {param2}", "more": "Mere", "memberSince": "Medlem siden", @@ -1533,6 +1543,7 @@ "studyPlayAgain": "Spil igen", "studyWhatWouldYouPlay": "Hvad ville du spille i denne position?", "studyYouCompletedThisLesson": "Tillykke! Du har fuldført denne lektion.", + "studyPerPage": "{param} pr. side", "studyNbChapters": "{count, plural, =1{{count} kapitel} other{{count} kapitler}}", "studyNbGames": "{count, plural, =1{{count} parti} other{{count} partier}}", "studyNbMembers": "{count, plural, =1{{count} Medlem} other{{count} Medlemmer}}", diff --git a/lib/l10n/lila_de.arb b/lib/l10n/lila_de.arb index 076be42dde..e3d2a3b0d4 100644 --- a/lib/l10n/lila_de.arb +++ b/lib/l10n/lila_de.arb @@ -1,47 +1,48 @@ { + "mobileAllGames": "Alle Partien", + "mobileAreYouSure": "Bist du sicher?", + "mobileBlindfoldMode": "Blind spielen", + "mobileCancelTakebackOffer": "Zugzurücknahme-Angebot abbrechen", + "mobileClearButton": "Löschen", + "mobileCorrespondenceClearSavedMove": "Gespeicherten Zug löschen", + "mobileCustomGameJoinAGame": "Einer Partie beitreten", + "mobileFeedbackButton": "Feedback", + "mobileGreeting": "Hallo, {param}", + "mobileGreetingWithoutName": "Hallo", + "mobileHideVariation": "Variante ausblenden", "mobileHomeTab": "Start", - "mobilePuzzlesTab": "Aufgaben", - "mobileToolsTab": "Werkzeuge", - "mobileWatchTab": "Zuschauen", - "mobileSettingsTab": "Optionen", + "mobileLiveStreamers": "Livestreamer", "mobileMustBeLoggedIn": "Du musst eingeloggt sein, um diese Seite anzuzeigen.", - "mobileSystemColors": "Systemfarben", - "mobileFeedbackButton": "Feedback", + "mobileNoSearchResults": "Keine Ergebnisse", + "mobileNotFollowingAnyUser": "Du folgst keinem Nutzer.", "mobileOkButton": "OK", + "mobilePlayersMatchingSearchTerm": "Spieler mit \"{param}\"", + "mobilePrefMagnifyDraggedPiece": "Vergrößern der gezogenen Figur", + "mobilePuzzleStormConfirmEndRun": "Möchtest du diesen Durchlauf beenden?", + "mobilePuzzleStormFilterNothingToShow": "Nichts anzuzeigen, bitte passe deine Filter an", + "mobilePuzzleStormNothingToShow": "Nichts anzuzeigen. Spiele ein paar Runden Puzzle Storm.", + "mobilePuzzleStormSubtitle": "Löse so viele Aufgaben wie möglich in 3 Minuten.", + "mobilePuzzleStreakAbortWarning": "Du verlierst deine aktuelle Serie und dein Ergebnis wird gespeichert.", + "mobilePuzzleThemesSubtitle": "Spiele Aufgaben aus deinen Lieblings-Öffnungen oder wähle ein Theme.", + "mobilePuzzlesTab": "Aufgaben", + "mobileRecentSearches": "Letzte Suchen", "mobileSettingsHapticFeedback": "Haptisches Feedback", "mobileSettingsImmersiveMode": "Immersiver Modus", "mobileSettingsImmersiveModeSubtitle": "System-Benutzeroberfläche während des Spielens ausblenden. Nutze diese Option, wenn dich die Navigationsverhalten des Systems an den Bildschirmrändern stören. Gilt für Spiel- und Puzzle-Storm-Bildschirme.", - "mobileNotFollowingAnyUser": "Du folgst keinem Nutzer.", - "mobileAllGames": "Alle Partien", - "mobileRecentSearches": "Letzte Suchen", - "mobileClearButton": "Löschen", - "mobilePlayersMatchingSearchTerm": "Spieler mit \"{param}\"", - "mobileNoSearchResults": "Keine Ergebnisse", - "mobileAreYouSure": "Bist du sicher?", - "mobilePuzzleStreakAbortWarning": "Du verlierst deine aktuelle Serie und dein Ergebnis wird gespeichert.", - "mobilePuzzleStormNothingToShow": "Nichts anzuzeigen. Spiele ein paar Runden Puzzle Storm.", - "mobileSharePuzzle": "Teile diese Aufgabe", - "mobileShareGameURL": "Link der Partie teilen", + "mobileSettingsTab": "Optionen", "mobileShareGamePGN": "PGN teilen", + "mobileShareGameURL": "Link der Partie teilen", "mobileSharePositionAsFEN": "Stellung als FEN teilen", - "mobileShowVariations": "Varianten anzeigen", - "mobileHideVariation": "Variante ausblenden", + "mobileSharePuzzle": "Teile diese Aufgabe", "mobileShowComments": "Kommentare anzeigen", - "mobilePuzzleStormConfirmEndRun": "Möchtest du diesen Durchlauf beenden?", - "mobilePuzzleStormFilterNothingToShow": "Nichts anzuzeigen, bitte passe deine Filter an", - "mobileCancelTakebackOffer": "Zugzurücknahme-Angebot abbrechen", - "mobileWaitingForOpponentToJoin": "Warte auf Beitritt eines Gegners...", - "mobileBlindfoldMode": "Blind spielen", - "mobileLiveStreamers": "Livestreamer", - "mobileCustomGameJoinAGame": "Einer Partie beitreten", - "mobileCorrespondenceClearSavedMove": "Gespeicherten Zug löschen", - "mobileSomethingWentWrong": "Etwas ist schiefgelaufen.", "mobileShowResult": "Ergebnis anzeigen", - "mobilePuzzleThemesSubtitle": "Spiele Aufgaben aus deinen Lieblings-Öffnungen oder wähle ein Theme.", - "mobilePuzzleStormSubtitle": "Löse so viele Aufgaben wie möglich in 3 Minuten.", - "mobileGreeting": "Hallo, {param}", - "mobileGreetingWithoutName": "Hallo", - "mobilePrefMagnifyDraggedPiece": "Vergrößern der gezogenen Figur", + "mobileShowVariations": "Varianten anzeigen", + "mobileSomethingWentWrong": "Etwas ist schiefgelaufen.", + "mobileSystemColors": "Systemfarben", + "mobileTheme": "Erscheinungsbild", + "mobileToolsTab": "Werkzeuge", + "mobileWaitingForOpponentToJoin": "Warte auf Beitritt eines Gegners...", + "mobileWatchTab": "Zuschauen", "activityActivity": "Verlauf", "activityHostedALiveStream": "Hat live gestreamt", "activityRankedInSwissTournament": "Hat Platz #{param1} im Turnier {param2} belegt", @@ -122,6 +123,7 @@ "broadcastNotYetStarted": "Die Übertragung hat noch nicht begonnen.", "broadcastOfficialWebsite": "Offizielle Webseite", "broadcastStandings": "Rangliste", + "broadcastOfficialStandings": "Offizielle Rangliste", "broadcastIframeHelp": "Weitere Optionen auf der {param}", "broadcastWebmastersPage": "Webmaster-Seite", "broadcastPgnSourceHelp": "Eine öffentliche Echtzeit-PGN-Quelle für diese Runde. Wir bieten auch eine {param} für eine schnellere und effizientere Synchronisation.", @@ -130,6 +132,15 @@ "broadcastRatingDiff": "Wertungsdifferenz", "broadcastGamesThisTournament": "Partien in diesem Turnier", "broadcastScore": "Punktestand", + "broadcastAllTeams": "Alle Teams", + "broadcastTournamentFormat": "Turnierformat", + "broadcastTournamentLocation": "Turnierort", + "broadcastTopPlayers": "Spitzenspieler", + "broadcastTimezone": "Zeitzone", + "broadcastFideRatingCategory": "FIDE-Wertungskategorie", + "broadcastOptionalDetails": "Optionale Details", + "broadcastPastBroadcasts": "Vergangene Übertragungen", + "broadcastAllBroadcastsByMonth": "Alle Übertragungen nach Monat anzeigen", "broadcastNbBroadcasts": "{count, plural, =1{{count} Übertragung} other{{count} Übertragungen}}", "challengeChallengesX": "Herausforderungen: {param1}", "challengeChallengeToPlay": "Zu einer Partie herausfordern", @@ -254,6 +265,7 @@ "preferencesNotifyWeb": "Browser", "preferencesNotifyDevice": "Gerät", "preferencesBellNotificationSound": "Glocken-Benachrichtigungston", + "preferencesBlindfold": "Blindschach", "puzzlePuzzles": "Taktikaufgaben", "puzzlePuzzleThemes": "Aufgabenthemen", "puzzleRecommended": "Empfohlen", @@ -450,7 +462,7 @@ "puzzleThemeZugzwang": "Zugzwang", "puzzleThemeZugzwangDescription": "Der Gegner ist in der Anzahl seiner Züge limitiert und jeder seiner Züge verschlechtert seine Stellung.", "puzzleThemeMix": "Gesunder Mix", - "puzzleThemeMixDescription": "Ein bisschen von Allem. Du weißt nicht, was dich erwartet, deshalb bleibst du auf alles vorbereitet! Genau wie in echten Partien.", + "puzzleThemeMixDescription": "Ein bisschen von allem. Du weißt nicht, was dich erwartet, deshalb bleibst du bereit für alles! Genau wie in echten Partien.", "puzzleThemePlayerGames": "Partien von Spielern", "puzzleThemePlayerGamesDescription": "Suche Aufgaben, die aus deinen Partien, oder den Partien eines anderen Spielers generiert wurden.", "puzzleThemePuzzleDownloadInformation": "Diese Aufgaben sind öffentlich zugänglich und können unter {param} heruntergeladen werden.", @@ -571,7 +583,6 @@ "replayMode": "Wiedergabemodus", "realtimeReplay": "Echtzeit", "byCPL": "Nach CPL", - "openStudy": "Studie öffnen", "enable": "Einschalten", "bestMoveArrow": "Pfeil für besten Zug", "showVariationArrows": "Varianten-Pfeile anzeigen", @@ -779,7 +790,6 @@ "block": "Blockieren", "blocked": "Blockiert", "unblock": "Nicht mehr blockieren", - "followsYou": "Folgt dir", "xStartedFollowingY": "{param1} folgt jetzt {param2}", "more": "Mehr", "memberSince": "Mitglied seit", @@ -1533,6 +1543,7 @@ "studyPlayAgain": "Erneut spielen", "studyWhatWouldYouPlay": "Was würdest du in dieser Stellung spielen?", "studyYouCompletedThisLesson": "Gratulation! Du hast diese Lektion abgeschlossen.", + "studyPerPage": "{param} pro Seite", "studyNbChapters": "{count, plural, =1{{count} Kapitel} other{{count} Kapitel}}", "studyNbGames": "{count, plural, =1{{count} Partie} other{{count} Partien}}", "studyNbMembers": "{count, plural, =1{{count} Mitglied} other{{count} Mitglieder}}", diff --git a/lib/l10n/lila_el.arb b/lib/l10n/lila_el.arb index 1645d6ff90..880c8bc1f6 100644 --- a/lib/l10n/lila_el.arb +++ b/lib/l10n/lila_el.arb @@ -1,45 +1,47 @@ { + "mobileAllGames": "Όλα τα παιχνίδια", + "mobileAreYouSure": "Είστε σίγουροι;", + "mobileBlindfoldMode": "Τυφλό", + "mobileCancelTakebackOffer": "Ακυρώστε την προσφορά αναίρεσης της κίνησης", + "mobileClearButton": "Εκκαθάριση", + "mobileCorrespondenceClearSavedMove": "Εκκαθάριση αποθηκευμένης κίνησης", + "mobileCustomGameJoinAGame": "Συμμετοχή σε παιχνίδι", + "mobileFeedbackButton": "Πείτε μας τη γνώμη σας", + "mobileGreeting": "Καλωσορίσατε, {param}", + "mobileGreetingWithoutName": "Καλωσορίσατε", + "mobileHideVariation": "Απόκρυψη παραλλαγής", "mobileHomeTab": "Αρχική", - "mobilePuzzlesTab": "Γρίφοι", - "mobileToolsTab": "Εργαλεία", - "mobileWatchTab": "Δείτε", - "mobileSettingsTab": "Ρυθμίσεις", + "mobileLiveStreamers": "Streamers ζωντανά αυτή τη στιγμή", "mobileMustBeLoggedIn": "Πρέπει να συνδεθείτε για να δείτε αυτή τη σελίδα.", - "mobileSystemColors": "Χρώματα συστήματος", - "mobileFeedbackButton": "Πείτε μας τη γνώμη σας", - "mobileOkButton": "ΟΚ", - "mobileSettingsHapticFeedback": "Απόκριση δόνησης", - "mobileSettingsImmersiveModeSubtitle": "Αποκρύπτει τη διεπαφή του συστήματος όσο παίζεται. Ενεργοποιήστε εάν σας ενοχλούν οι χειρονομίες πλοήγησης του συστήματος στα άκρα της οθόνης. Ισχύει για την προβολή παιχνιδιού και το Puzzle Storm.", + "mobileNoSearchResults": "Δεν βρέθηκαν αποτελέσματα", "mobileNotFollowingAnyUser": "Δεν ακολουθείτε κανέναν χρήστη.", - "mobileAllGames": "Όλα τα παιχνίδια", - "mobileRecentSearches": "Πρόσφατες αναζητήσεις", - "mobileClearButton": "Εκκαθάριση", + "mobileOkButton": "ΟΚ", "mobilePlayersMatchingSearchTerm": "Παίκτες με \"{param}\"", - "mobileNoSearchResults": "Δεν βρέθηκαν αποτελέσματα", - "mobileAreYouSure": "Είστε σίγουροι;", + "mobilePrefMagnifyDraggedPiece": "Μεγέθυνση του επιλεγμένου κομματιού", + "mobilePuzzleStormConfirmEndRun": "Θέλετε να τερματίσετε αυτόν τον γύρο;", + "mobilePuzzleStormFilterNothingToShow": "Δεν υπάρχουν γρίφοι για τις συγκεκριμένες επιλογές φίλτρων, παρακαλώ δοκιμάστε κάποιες άλλες", "mobilePuzzleStormNothingToShow": "Δεν υπάρχουν στοιχεία. Παίξτε κάποιους γύρους Puzzle Storm.", - "mobileSharePuzzle": "Κοινοποίηση γρίφου", - "mobileShareGameURL": "Κοινοποίηση URL παιχνιδιού", + "mobilePuzzleStormSubtitle": "Λύστε όσους γρίφους όσο το δυνατόν, σε 3 λεπτά.", + "mobilePuzzleThemesSubtitle": "Παίξτε γρίφους από τα αγαπημένα σας ανοίγματα, ή επιλέξτε θέμα.", + "mobilePuzzlesTab": "Γρίφοι", + "mobileRecentSearches": "Πρόσφατες αναζητήσεις", + "mobileSettingsHapticFeedback": "Απόκριση δόνησης", + "mobileSettingsImmersiveMode": "Λειτουργία εστίασης", + "mobileSettingsImmersiveModeSubtitle": "Αποκρύπτει τη διεπαφή του συστήματος όσο παίζεται. Ενεργοποιήστε εάν σας ενοχλούν οι χειρονομίες πλοήγησης του συστήματος στα άκρα της οθόνης. Ισχύει για την προβολή παιχνιδιού και το Puzzle Storm.", + "mobileSettingsTab": "Ρυθμίσεις", "mobileShareGamePGN": "Κοινοποίηση PGN", + "mobileShareGameURL": "Κοινοποίηση URL παιχνιδιού", "mobileSharePositionAsFEN": "Κοινοποίηση θέσης ως FEN", - "mobileShowVariations": "Εμφάνιση παραλλαγών", - "mobileHideVariation": "Απόκρυψη παραλλαγής", + "mobileSharePuzzle": "Κοινοποίηση γρίφου", "mobileShowComments": "Εμφάνιση σχολίων", - "mobilePuzzleStormConfirmEndRun": "Θέλετε να τερματίσετε αυτόν τον γύρο;", - "mobilePuzzleStormFilterNothingToShow": "Δεν υπάρχουν γρίφοι για τις συγκεκριμένες επιλογές φίλτρων, παρακαλώ δοκιμάστε κάποιες άλλες", - "mobileCancelTakebackOffer": "Ακυρώστε την προσφορά αναίρεσης της κίνησης", - "mobileWaitingForOpponentToJoin": "Αναμονή για αντίπαλο...", - "mobileBlindfoldMode": "Τυφλό", - "mobileLiveStreamers": "Streamers ζωντανά αυτή τη στιγμή", - "mobileCustomGameJoinAGame": "Συμμετοχή σε παιχνίδι", - "mobileCorrespondenceClearSavedMove": "Εκκαθάριση αποθηκευμένης κίνησης", - "mobileSomethingWentWrong": "Κάτι πήγε στραβά.", "mobileShowResult": "Εμφάνιση αποτελέσματος", - "mobilePuzzleThemesSubtitle": "Παίξτε γρίφους από τα αγαπημένα σας ανοίγματα, ή επιλέξτε θέμα.", - "mobilePuzzleStormSubtitle": "Λύστε όσους γρίφους όσο το δυνατόν, σε 3 λεπτά.", - "mobileGreeting": "Καλωσορίσατε, {param}", - "mobileGreetingWithoutName": "Καλωσορίσατε", - "mobilePrefMagnifyDraggedPiece": "Μεγέθυνση του επιλεγμένου κομματιού", + "mobileShowVariations": "Εμφάνιση παραλλαγών", + "mobileSomethingWentWrong": "Κάτι πήγε στραβά.", + "mobileSystemColors": "Χρώματα συστήματος", + "mobileTheme": "Εμφάνιση", + "mobileToolsTab": "Εργαλεία", + "mobileWaitingForOpponentToJoin": "Αναμονή για αντίπαλο...", + "mobileWatchTab": "Δείτε", "activityActivity": "Δραστηριότητα", "activityHostedALiveStream": "Μεταδίδει ζωντανά", "activityRankedInSwissTournament": "Κατατάχθηκε #{param1} στο {param2}", @@ -65,6 +67,7 @@ "broadcastBroadcasts": "Αναμεταδόσεις", "broadcastMyBroadcasts": "Οι αναμεταδόσεις μου", "broadcastLiveBroadcasts": "Αναμεταδόσεις ζωντανών τουρνούα", + "broadcastBroadcastCalendar": "Ημερολόγιο αναμεταδόσεων", "broadcastNewBroadcast": "Νέα ζωντανή αναμετάδοση", "broadcastSubscribedBroadcasts": "Εγγεγραμμένες μεταδώσεις", "broadcastAboutBroadcasts": "Σχετικά με εκπομπές", @@ -81,6 +84,7 @@ "broadcastTournamentDescription": "Σύντομη περιγραφή τουρνουά", "broadcastFullDescription": "Πλήρης περιγραφή γεγονότος", "broadcastFullDescriptionHelp": "Προαιρετική αναλυτική περιγραφή της αναμετάδοσης. Η μορφή {param1} είναι διαθέσιμη. Το μήκος πρέπει μικρότερο από {param2} χαρακτήρες.", + "broadcastSourceSingleUrl": "Πηγαίο URL για PGN", "broadcastSourceUrlHelp": "URL για λήψη PGN ενημερώσεων. Πρέπει να είναι δημόσια προσβάσιμο μέσω διαδικτύου.", "broadcastStartDateHelp": "Προαιρετικό, εάν γνωρίζετε πότε αρχίζει η εκδήλωση", "broadcastCurrentGameUrl": "Διεύθυνση URL αυτού του παιχνιδιού", @@ -98,18 +102,34 @@ "broadcastFideProfile": "Προφίλ FIDE", "broadcastFederation": "Ομοσπονδία", "broadcastAgeThisYear": "Φετινή ηλικία", + "broadcastUnrated": "Μη βαθμολογημένο", "broadcastRecentTournaments": "Πρόσφατα τουρνουά", "broadcastOpenLichess": "Άνοιγμα στο Lichess", "broadcastTeams": "Ομάδες", "broadcastBoards": "Σκακιέρες", "broadcastOverview": "Επισκόπηση", "broadcastUploadImage": "Ανεβάστε εικόνα τουρνουά", + "broadcastBoardsCanBeLoaded": "Οι σκακιέρες μπορούν να φορτωθούν απο μια πηγή ή μέσω του {param}", "broadcastStartsAfter": "Ξεκινάει μετά από {param}", + "broadcastStartVerySoon": "Η αναμετάδοση θα ξεκινήσει πολύ σύντομα.", + "broadcastNotYetStarted": "Η αναμετάδοση δεν έχει ξεκινήσει ακόμα.", "broadcastOfficialWebsite": "Επίσημη ιστοσελίδα", "broadcastStandings": "Κατάταξη", + "broadcastOfficialStandings": "Επίσημη Κατάταξη", + "broadcastIframeHelp": "Περισσότερες επιλογές στη {param}", + "broadcastWebmastersPage": "σελίδα για webmasters", + "broadcastPgnSourceHelp": "Μια δημόσια πηγή PGN πολύ λειτουργεί σε πραγματικό χρόνο για αυτόν τον γύρο. Προσφέρουμε επίσης το {param} για γρηγορότερο και αποτελεσματικότερο συγχρονισμό.", + "broadcastEmbedThisBroadcast": "Ενσωμάτωση αυτήν την αναμετάδοση στην ιστοσελίδα σας", + "broadcastEmbedThisRound": "Ενσωματώστε τον {param} στην ιστοσελίδα σας", "broadcastRatingDiff": "Διαφορά βαθμολογίας", "broadcastGamesThisTournament": "Παρτίδες σε αυτό το τουρνουά", "broadcastScore": "Βαθμολογία", + "broadcastAllTeams": "Όλες οι ομάδες", + "broadcastTournamentLocation": "Τοποθεσία Τουρνουά", + "broadcastTimezone": "Ζώνη ώρας", + "broadcastOptionalDetails": "Προαιρετικές λεπτομέρειες", + "broadcastPastBroadcasts": "Προηγούμενες αναμετάδοσεις", + "broadcastAllBroadcastsByMonth": "Προβολή όλων των αναμεταδόσεων ανά μήνα", "broadcastNbBroadcasts": "{count, plural, =1{{count} αναμετάδοση} other{{count} αναμεταδόσεις}}", "challengeChallengesX": "Προκλήσεις: {param1}", "challengeChallengeToPlay": "Προκαλέστε σε παιχνίδι", @@ -233,6 +253,7 @@ "preferencesNotifyWeb": "Browser", "preferencesNotifyDevice": "Συσκευή", "preferencesBellNotificationSound": "Ειδοποίηση με ήχο από καμπανάκι", + "preferencesBlindfold": "Τυφλό", "puzzlePuzzles": "Γρίφοι", "puzzlePuzzleThemes": "Θέματα γρίφων", "puzzleRecommended": "Προτεινόμενα", @@ -550,7 +571,6 @@ "replayMode": "Επανάληψη", "realtimeReplay": "Σε πραγματικό χρόνο", "byCPL": "Με CPL", - "openStudy": "Άνοιγμα μελέτης", "enable": "Ενεργοποίηση", "bestMoveArrow": "Βέλτιστη κίνηση βέλους", "showVariationArrows": "Εμφάνισε βελάκια παραλλαγών", @@ -758,7 +778,6 @@ "block": "Αποκλείστε", "blocked": "Αποκλεισμένος", "unblock": "Κατάργηση απόκλεισης", - "followsYou": "Σας ακολουθεί", "xStartedFollowingY": "Ο {param1} άρχισε να ακολουθεί τον {param2}", "more": "Περισσότερα", "memberSince": "Μέλος από τις", diff --git a/lib/l10n/lila_en_US.arb b/lib/l10n/lila_en_US.arb index 119151ae45..ce5bd7af84 100644 --- a/lib/l10n/lila_en_US.arb +++ b/lib/l10n/lila_en_US.arb @@ -1,47 +1,48 @@ { + "mobileAllGames": "All games", + "mobileAreYouSure": "Are you sure?", + "mobileBlindfoldMode": "Blindfold", + "mobileCancelTakebackOffer": "Cancel takeback offer", + "mobileClearButton": "Clear", + "mobileCorrespondenceClearSavedMove": "Clear saved move", + "mobileCustomGameJoinAGame": "Join a game", + "mobileFeedbackButton": "Feedback", + "mobileGreeting": "Hello, {param}", + "mobileGreetingWithoutName": "Hello", + "mobileHideVariation": "Hide variation", "mobileHomeTab": "Home", - "mobilePuzzlesTab": "Puzzles", - "mobileToolsTab": "Tools", - "mobileWatchTab": "Watch", - "mobileSettingsTab": "Settings", + "mobileLiveStreamers": "Live streamers", "mobileMustBeLoggedIn": "You must be logged in to view this page.", - "mobileSystemColors": "System colors", - "mobileFeedbackButton": "Feedback", + "mobileNoSearchResults": "No results", + "mobileNotFollowingAnyUser": "You are not following any user.", "mobileOkButton": "OK", + "mobilePlayersMatchingSearchTerm": "Players with \"{param}\"", + "mobilePrefMagnifyDraggedPiece": "Magnify dragged piece", + "mobilePuzzleStormConfirmEndRun": "Do you want to end this run?", + "mobilePuzzleStormFilterNothingToShow": "Nothing to show, please change the filters", + "mobilePuzzleStormNothingToShow": "Nothing to show. Play some runs of Puzzle Storm.", + "mobilePuzzleStormSubtitle": "Solve as many puzzles as possible in 3 minutes.", + "mobilePuzzleStreakAbortWarning": "You will lose your current streak, but your score will be saved.", + "mobilePuzzleThemesSubtitle": "Play puzzles from your favorite openings, or choose a theme.", + "mobilePuzzlesTab": "Puzzles", + "mobileRecentSearches": "Recent searches", "mobileSettingsHapticFeedback": "Haptic feedback", "mobileSettingsImmersiveMode": "Immersive mode", "mobileSettingsImmersiveModeSubtitle": "Hide system UI while playing. Use this if you are bothered by the system's navigation gestures at the edges of the screen. Applies to game and Puzzle Storm screens.", - "mobileNotFollowingAnyUser": "You are not following any user.", - "mobileAllGames": "All games", - "mobileRecentSearches": "Recent searches", - "mobileClearButton": "Clear", - "mobilePlayersMatchingSearchTerm": "Players with \"{param}\"", - "mobileNoSearchResults": "No results", - "mobileAreYouSure": "Are you sure?", - "mobilePuzzleStreakAbortWarning": "You will lose your current streak, but your score will be saved.", - "mobilePuzzleStormNothingToShow": "Nothing to show. Play some runs of Puzzle Storm.", - "mobileSharePuzzle": "Share this puzzle", - "mobileShareGameURL": "Share game URL", + "mobileSettingsTab": "Settings", "mobileShareGamePGN": "Share PGN", + "mobileShareGameURL": "Share game URL", "mobileSharePositionAsFEN": "Share position as FEN", - "mobileShowVariations": "Show variations", - "mobileHideVariation": "Hide variation", + "mobileSharePuzzle": "Share this puzzle", "mobileShowComments": "Show comments", - "mobilePuzzleStormConfirmEndRun": "Do you want to end this run?", - "mobilePuzzleStormFilterNothingToShow": "Nothing to show, please change the filters", - "mobileCancelTakebackOffer": "Cancel takeback offer", - "mobileWaitingForOpponentToJoin": "Waiting for opponent to join...", - "mobileBlindfoldMode": "Blindfold", - "mobileLiveStreamers": "Live streamers", - "mobileCustomGameJoinAGame": "Join a game", - "mobileCorrespondenceClearSavedMove": "Clear saved move", - "mobileSomethingWentWrong": "Something went wrong.", "mobileShowResult": "Show result", - "mobilePuzzleThemesSubtitle": "Play puzzles from your favorite openings, or choose a theme.", - "mobilePuzzleStormSubtitle": "Solve as many puzzles as possible in 3 minutes.", - "mobileGreeting": "Hello, {param}", - "mobileGreetingWithoutName": "Hello", - "mobilePrefMagnifyDraggedPiece": "Magnify dragged piece", + "mobileShowVariations": "Show variations", + "mobileSomethingWentWrong": "Something went wrong.", + "mobileSystemColors": "System colors", + "mobileTheme": "Theme", + "mobileToolsTab": "Tools", + "mobileWaitingForOpponentToJoin": "Waiting for opponent to join...", + "mobileWatchTab": "Watch", "activityActivity": "Activity", "activityHostedALiveStream": "Hosted a live stream", "activityRankedInSwissTournament": "Ranked #{param1} in {param2}", @@ -109,6 +110,37 @@ "broadcastAgeThisYear": "Age this year", "broadcastUnrated": "Unrated", "broadcastRecentTournaments": "Recent tournaments", + "broadcastOpenLichess": "Open in Lichess", + "broadcastTeams": "Teams", + "broadcastBoards": "Boards", + "broadcastOverview": "Overview", + "broadcastSubscribeTitle": "Subscribe to be notified when each round starts. You can toggle bell or push notifications for broadcasts in your account preferences.", + "broadcastUploadImage": "Upload tournament image", + "broadcastNoBoardsYet": "No boards yet. These will appear once games are uploaded.", + "broadcastBoardsCanBeLoaded": "Boards can be loaded with a source or via the {param}", + "broadcastStartsAfter": "Starts after {param}", + "broadcastStartVerySoon": "The broadcast will start very soon.", + "broadcastNotYetStarted": "The broadcast has not yet started.", + "broadcastOfficialWebsite": "Official website", + "broadcastStandings": "Standings", + "broadcastOfficialStandings": "Official Standings", + "broadcastIframeHelp": "More options on the {param}", + "broadcastWebmastersPage": "webmasters page", + "broadcastPgnSourceHelp": "A public, real-time PGN source for this round. We also offer a {param} for faster and more efficient synchronization.", + "broadcastEmbedThisBroadcast": "Embed this broadcast in your website", + "broadcastEmbedThisRound": "Embed {param} in your website", + "broadcastRatingDiff": "Rating diff", + "broadcastGamesThisTournament": "Games in this tournament", + "broadcastScore": "Score", + "broadcastAllTeams": "All teams", + "broadcastTournamentFormat": "Tournament format", + "broadcastTournamentLocation": "Tournament Location", + "broadcastTopPlayers": "Top players", + "broadcastTimezone": "Time zone", + "broadcastFideRatingCategory": "FIDE rating category", + "broadcastOptionalDetails": "Optional details", + "broadcastPastBroadcasts": "Past broadcasts", + "broadcastAllBroadcastsByMonth": "View all broadcasts by month", "broadcastNbBroadcasts": "{count, plural, =1{{count} broadcast} other{{count} broadcasts}}", "challengeChallengesX": "Challenges: {param1}", "challengeChallengeToPlay": "Challenge to a game", @@ -182,6 +214,7 @@ "preferencesZenMode": "Zen mode", "preferencesShowPlayerRatings": "Show player ratings", "preferencesShowFlairs": "Show player flairs", + "preferencesExplainShowPlayerRatings": "This hides all ratings from Lichess, to help focus on the chess. Rated games still impact your rating, this is only about what you get to see.", "preferencesDisplayBoardResizeHandle": "Show board resize handle", "preferencesOnlyOnInitialPosition": "Only on initial position", "preferencesInGameOnly": "In-game only", @@ -232,6 +265,7 @@ "preferencesNotifyWeb": "Browser", "preferencesNotifyDevice": "Device", "preferencesBellNotificationSound": "Bell notification sound", + "preferencesBlindfold": "Blindfold", "puzzlePuzzles": "Puzzles", "puzzlePuzzleThemes": "Puzzle Themes", "puzzleRecommended": "Recommended", @@ -549,7 +583,6 @@ "replayMode": "Replay mode", "realtimeReplay": "Realtime", "byCPL": "By CPL", - "openStudy": "Open study", "enable": "Enable", "bestMoveArrow": "Best move arrow", "showVariationArrows": "Show variation arrows", @@ -640,6 +673,7 @@ "rank": "Rank", "rankX": "Rank: {param}", "gamesPlayed": "Games played", + "ok": "OK", "cancel": "Cancel", "whiteTimeOut": "White time out", "blackTimeOut": "Black time out", @@ -756,7 +790,6 @@ "block": "Block", "blocked": "Blocked", "unblock": "Unblock", - "followsYou": "Follows you", "xStartedFollowingY": "{param1} started following {param2}", "more": "More", "memberSince": "Member since", @@ -1262,6 +1295,7 @@ "showMeEverything": "Show me everything", "lichessPatronInfo": "Lichess is a charity and entirely free/libre open source software.\nAll operating costs, development, and content are funded solely by user donations.", "nothingToSeeHere": "Nothing to see here at the moment.", + "stats": "Stats", "opponentLeftCounter": "{count, plural, =1{Your opponent left the game. You can claim victory in {count} second.} other{Your opponent left the game. You can claim victory in {count} seconds.}}", "mateInXHalfMoves": "{count, plural, =1{Mate in {count} half-move} other{Mate in {count} half-moves}}", "nbBlunders": "{count, plural, =1{{count} blunder} other{{count} blunders}}", @@ -1509,6 +1543,7 @@ "studyPlayAgain": "Play again", "studyWhatWouldYouPlay": "What would you play in this position?", "studyYouCompletedThisLesson": "Congratulations! You completed this lesson.", + "studyPerPage": "{param} per page", "studyNbChapters": "{count, plural, =1{{count} Chapter} other{{count} Chapters}}", "studyNbGames": "{count, plural, =1{{count} Game} other{{count} Games}}", "studyNbMembers": "{count, plural, =1{{count} Member} other{{count} Members}}", diff --git a/lib/l10n/lila_eo.arb b/lib/l10n/lila_eo.arb index 9cc737237d..ebd0a91db6 100644 --- a/lib/l10n/lila_eo.arb +++ b/lib/l10n/lila_eo.arb @@ -1,22 +1,22 @@ { + "mobileAllGames": "Ĉiuj ludoj", + "mobileAreYouSure": "Ĉu vi certas?", + "mobileClearButton": "Malplenigi", + "mobileFeedbackButton": "Prikomentado", "mobileHomeTab": "Hejmo", - "mobilePuzzlesTab": "Puzloj", - "mobileToolsTab": "Iloj", - "mobileWatchTab": "Spekti", - "mobileSettingsTab": "Agordoj", "mobileMustBeLoggedIn": "Vi devas esti ensalutata por spekti ĉi tiun paĝon.", - "mobileSystemColors": "Sistemaj koloroj", - "mobileFeedbackButton": "Prikomentado", + "mobileNoSearchResults": "Neniu rezultoj", + "mobileNotFollowingAnyUser": "Vi ne abonas ĉiun uzanton.", "mobileOkButton": "Bone", + "mobilePlayersMatchingSearchTerm": "Ludantanto kun \"{param}\"", + "mobilePuzzlesTab": "Puzloj", + "mobileRecentSearches": "Lastaj serĉoj", "mobileSettingsHapticFeedback": "Tuŝ-retrokuplado", "mobileSettingsImmersiveMode": "Enakviĝa reĝimo", - "mobileNotFollowingAnyUser": "Vi ne abonas ĉiun uzanton.", - "mobileAllGames": "Ĉiuj ludoj", - "mobileRecentSearches": "Lastaj serĉoj", - "mobileClearButton": "Malplenigi", - "mobilePlayersMatchingSearchTerm": "Ludantanto kun \"{param}\"", - "mobileNoSearchResults": "Neniu rezultoj", - "mobileAreYouSure": "Ĉu vi certas?", + "mobileSettingsTab": "Agordoj", + "mobileSystemColors": "Sistemaj koloroj", + "mobileToolsTab": "Iloj", + "mobileWatchTab": "Spekti", "activityActivity": "Aktiveco", "activityHostedALiveStream": "Gastigis vivan rivereton", "activityRankedInSwissTournament": "Rangita #{param1} en {param2}", @@ -509,7 +509,6 @@ "replayMode": "Reluda reĝimo", "realtimeReplay": "Reala tempo", "byCPL": "Per eraroj", - "openStudy": "Malfermi analizon", "enable": "Ebligi", "bestMoveArrow": "Sago por optimuma movo", "showVariationArrows": "Montri variaĵojn sagojn", @@ -715,7 +714,6 @@ "block": "Bloki", "blocked": "Blokita", "unblock": "Malbloki", - "followsYou": "Sekvas vin", "xStartedFollowingY": "{param1} eksekvis {param2}", "more": "Pli", "memberSince": "Membro ekde", diff --git a/lib/l10n/lila_es.arb b/lib/l10n/lila_es.arb index 4c83c59b27..a6405e015d 100644 --- a/lib/l10n/lila_es.arb +++ b/lib/l10n/lila_es.arb @@ -1,47 +1,48 @@ { + "mobileAllGames": "Todas las partidas", + "mobileAreYouSure": "¿Estás seguro?", + "mobileBlindfoldMode": "A ciegas", + "mobileCancelTakebackOffer": "Cancelar oferta de deshacer movimiento", + "mobileClearButton": "Limpiar", + "mobileCorrespondenceClearSavedMove": "Borrar movimiento guardado", + "mobileCustomGameJoinAGame": "Únete a una partida", + "mobileFeedbackButton": "Comentarios", + "mobileGreeting": "Hola {param}", + "mobileGreetingWithoutName": "Hola", + "mobileHideVariation": "Ocultar variación", "mobileHomeTab": "Inicio", - "mobilePuzzlesTab": "Ejercicios", - "mobileToolsTab": "Herramientas", - "mobileWatchTab": "Ver", - "mobileSettingsTab": "Ajustes", + "mobileLiveStreamers": "Presentadores en vivo", "mobileMustBeLoggedIn": "Debes iniciar sesión para ver esta página.", - "mobileSystemColors": "Colores del sistema", - "mobileFeedbackButton": "Comentarios", + "mobileNoSearchResults": "No hay resultados", + "mobileNotFollowingAnyUser": "No estás siguiendo a ningún usuario.", "mobileOkButton": "Aceptar", + "mobilePlayersMatchingSearchTerm": "Jugadores con \"{param}\"", + "mobilePrefMagnifyDraggedPiece": "Aumentar la pieza arrastrada", + "mobilePuzzleStormConfirmEndRun": "¿Quieres finalizar esta ronda?", + "mobilePuzzleStormFilterNothingToShow": "Nada que mostrar, por favor cambia los filtros", + "mobilePuzzleStormNothingToShow": "Nada que mostrar. Juega algunas rondas de Puzzle Storm.", + "mobilePuzzleStormSubtitle": "Resuelve tantos ejercicios como puedas en 3 minutos.", + "mobilePuzzleStreakAbortWarning": "Perderás tu racha actual y tu puntuación será guardada.", + "mobilePuzzleThemesSubtitle": "Realiza ejercicios de tus aperturas favoritas o elige un tema.", + "mobilePuzzlesTab": "Ejercicios", + "mobileRecentSearches": "Búsquedas recientes", "mobileSettingsHapticFeedback": "Respuesta táctil", "mobileSettingsImmersiveMode": "Modo inmersivo", "mobileSettingsImmersiveModeSubtitle": "Ocultar la interfaz del sistema durante la partida. Usa esto si te molestan los iconos de navegación del sistema en los bordes de la pantalla. Se aplica a las pantallas del juego y de Puzzle Storm.", - "mobileNotFollowingAnyUser": "No estás siguiendo a ningún usuario.", - "mobileAllGames": "Todas las partidas", - "mobileRecentSearches": "Búsquedas recientes", - "mobileClearButton": "Limpiar", - "mobilePlayersMatchingSearchTerm": "Jugadores con \"{param}\"", - "mobileNoSearchResults": "No hay resultados", - "mobileAreYouSure": "¿Estás seguro?", - "mobilePuzzleStreakAbortWarning": "Perderás tu racha actual y tu puntuación será guardada.", - "mobilePuzzleStormNothingToShow": "Nada que mostrar. Juega algunas rondas de Puzzle Storm.", - "mobileSharePuzzle": "Compartir este ejercicio", - "mobileShareGameURL": "Compartir enlace de la partida", + "mobileSettingsTab": "Ajustes", "mobileShareGamePGN": "Compartir PGN", + "mobileShareGameURL": "Compartir enlace de la partida", "mobileSharePositionAsFEN": "Compartir posición como FEN", - "mobileShowVariations": "Mostrar variaciones", - "mobileHideVariation": "Ocultar variación", + "mobileSharePuzzle": "Compartir este ejercicio", "mobileShowComments": "Mostrar comentarios", - "mobilePuzzleStormConfirmEndRun": "¿Quieres finalizar esta ronda?", - "mobilePuzzleStormFilterNothingToShow": "Nada que mostrar, por favor cambia los filtros", - "mobileCancelTakebackOffer": "Cancelar oferta de deshacer movimiento", - "mobileWaitingForOpponentToJoin": "Esperando a que se una un oponente...", - "mobileBlindfoldMode": "A ciegas", - "mobileLiveStreamers": "Presentadores en vivo", - "mobileCustomGameJoinAGame": "Únete a una partida", - "mobileCorrespondenceClearSavedMove": "Borrar movimiento guardado", - "mobileSomethingWentWrong": "Algo salió mal.", "mobileShowResult": "Ver resultado", - "mobilePuzzleThemesSubtitle": "Realiza ejercicios de tus aperturas favoritas o elige un tema.", - "mobilePuzzleStormSubtitle": "Resuelve tantos ejercicios como puedas en 3 minutos.", - "mobileGreeting": "Hola {param}", - "mobileGreetingWithoutName": "Hola", - "mobilePrefMagnifyDraggedPiece": "Aumentar la pieza arrastrada", + "mobileShowVariations": "Mostrar variaciones", + "mobileSomethingWentWrong": "Algo salió mal.", + "mobileSystemColors": "Colores del sistema", + "mobileTheme": "Tema", + "mobileToolsTab": "Herramientas", + "mobileWaitingForOpponentToJoin": "Esperando a que se una un oponente...", + "mobileWatchTab": "Ver", "activityActivity": "Actividad", "activityHostedALiveStream": "Emitió en directo", "activityRankedInSwissTournament": "#{param1} En la Clasificatoria de {param2}", @@ -122,6 +123,7 @@ "broadcastNotYetStarted": "La transmisión aún no ha comenzado.", "broadcastOfficialWebsite": "Sitio oficial", "broadcastStandings": "Clasificación", + "broadcastOfficialStandings": "Clasificación oficial", "broadcastIframeHelp": "Más opciones en {param}", "broadcastWebmastersPage": "la página del webmaster", "broadcastPgnSourceHelp": "Una fuente PGN pública en tiempo real para esta ronda. También ofrecemos {param} para una sincronización más rápida y eficiente.", @@ -130,6 +132,15 @@ "broadcastRatingDiff": "Diferencia de valoración", "broadcastGamesThisTournament": "Partidas en este torneo", "broadcastScore": "Resultado", + "broadcastAllTeams": "Todos los equipos", + "broadcastTournamentFormat": "Formato del torneo", + "broadcastTournamentLocation": "Ubicación del torneo", + "broadcastTopPlayers": "Mejores jugadores", + "broadcastTimezone": "Zona horaria", + "broadcastFideRatingCategory": "Categoría de calificación de FIDE", + "broadcastOptionalDetails": "Detalles opcionales", + "broadcastPastBroadcasts": "Transmisiones pasadas", + "broadcastAllBroadcastsByMonth": "Ver todas las transmisiones por mes", "broadcastNbBroadcasts": "{count, plural, =1{{count} retransmisión} other{{count} retransmisiones}}", "challengeChallengesX": "Desafíos: {param1}", "challengeChallengeToPlay": "Desafiar a una partida", @@ -254,6 +265,7 @@ "preferencesNotifyWeb": "Navegador", "preferencesNotifyDevice": "Dispositivo", "preferencesBellNotificationSound": "Campana de notificación", + "preferencesBlindfold": "A ciegas", "puzzlePuzzles": "Ejercicios", "puzzlePuzzleThemes": "Ejercicios por temas", "puzzleRecommended": "Recomendado", @@ -571,7 +583,6 @@ "replayMode": "Modo de repetición", "realtimeReplay": "Tiempo real", "byCPL": "Por PCP", - "openStudy": "Abrir estudio", "enable": "Activar", "bestMoveArrow": "Indicar la mejor jugada", "showVariationArrows": "Mostrar flechas de variantes", @@ -779,7 +790,6 @@ "block": "Bloquear", "blocked": "Bloqueado", "unblock": "Desbloquear", - "followsYou": "Te sigue", "xStartedFollowingY": "{param1} comenzó a seguir a {param2}", "more": "Más", "memberSince": "Miembro desde", @@ -1533,6 +1543,7 @@ "studyPlayAgain": "Jugar de nuevo", "studyWhatWouldYouPlay": "¿Qué jugarías en esta posición?", "studyYouCompletedThisLesson": "¡Felicidades! Has completado esta lección.", + "studyPerPage": "{param} por página", "studyNbChapters": "{count, plural, =1{{count} Capítulo} other{{count} Capítulos}}", "studyNbGames": "{count, plural, =1{{count} Partida} other{{count} Partidas}}", "studyNbMembers": "{count, plural, =1{{count} Miembro} other{{count} Miembros}}", diff --git a/lib/l10n/lila_et.arb b/lib/l10n/lila_et.arb index 4da5e3acf2..c301ed2780 100644 --- a/lib/l10n/lila_et.arb +++ b/lib/l10n/lila_et.arb @@ -429,7 +429,6 @@ "replayMode": "Kordusrežiim", "realtimeReplay": "Reaalajas", "byCPL": "CPL järgi", - "openStudy": "Ava uuring", "enable": "Luba", "bestMoveArrow": "Parima käigu nool", "evaluationGauge": "Hinnangunäidik", @@ -628,7 +627,6 @@ "block": "Blokeeri", "blocked": "Blokeeritud", "unblock": "Tühista blokeering", - "followsYou": "Jälgib sind", "xStartedFollowingY": "{param1} hakkas jälgima {param2}", "more": "Rohkem", "memberSince": "Liitunud", diff --git a/lib/l10n/lila_eu.arb b/lib/l10n/lila_eu.arb index 2fc816570f..4f706e77ed 100644 --- a/lib/l10n/lila_eu.arb +++ b/lib/l10n/lila_eu.arb @@ -1,47 +1,48 @@ { + "mobileAllGames": "Partida guztiak", + "mobileAreYouSure": "Ziur zaude?", + "mobileBlindfoldMode": "Itsuka", + "mobileCancelTakebackOffer": "Bertan behera utzi atzera-egite eskaera", + "mobileClearButton": "Garbitu", + "mobileCorrespondenceClearSavedMove": "Garbitu gordetako jokaldia", + "mobileCustomGameJoinAGame": "Sartu partida baten", + "mobileFeedbackButton": "Iritzia", + "mobileGreeting": "Kaixo {param}", + "mobileGreetingWithoutName": "Kaixo", + "mobileHideVariation": "Ezkutatu aukera", "mobileHomeTab": "Hasiera", - "mobilePuzzlesTab": "Ariketak", - "mobileToolsTab": "Tresnak", - "mobileWatchTab": "Ikusi", - "mobileSettingsTab": "Ezarpenak", + "mobileLiveStreamers": "Zuzeneko streamerrak", "mobileMustBeLoggedIn": "Sartu egin behar zara orri hau ikusteko.", - "mobileSystemColors": "Sistemaren koloreak", - "mobileFeedbackButton": "Iritzia", + "mobileNoSearchResults": "Emaitzarik ez", + "mobileNotFollowingAnyUser": "Ez zaude erabiltzailerik jarraitzen.", "mobileOkButton": "Ados", + "mobilePlayersMatchingSearchTerm": "\"{param}\" duten jokalariak", + "mobilePrefMagnifyDraggedPiece": "Handitu arrastatutako pieza", + "mobilePuzzleStormConfirmEndRun": "Saiakera hau amaitu nahi duzu?", + "mobilePuzzleStormFilterNothingToShow": "Ez dago erakusteko ezer, aldatu filtroak", + "mobilePuzzleStormNothingToShow": "Ez dago ezer erakusteko. Jokatu Ariketa zaparrada batzuk.", + "mobilePuzzleStormSubtitle": "Ebatzi ahalik eta ariketa gehien 3 minututan.", + "mobilePuzzleStreakAbortWarning": "Zure uneko bolada galduko duzu eta zure puntuazioa gorde egingo da.", + "mobilePuzzleThemesSubtitle": "Jokatu zure irekiera gogokoenen ariketak, edo aukeratu gai bat.", + "mobilePuzzlesTab": "Ariketak", + "mobileRecentSearches": "Azken bilaketak", "mobileSettingsHapticFeedback": "Ukipen-erantzuna", "mobileSettingsImmersiveMode": "Murgiltze modua", "mobileSettingsImmersiveModeSubtitle": "Ezkutatu sistemaren menuak jokatzen ari zaren artean. Erabili hau zure telefonoaren nabigatzeko aukerek traba egiten badizute. Partida bati eta ariketen zaparradan aplikatu daiteke.", - "mobileNotFollowingAnyUser": "Ez zaude erabiltzailerik jarraitzen.", - "mobileAllGames": "Partida guztiak", - "mobileRecentSearches": "Azken bilaketak", - "mobileClearButton": "Garbitu", - "mobilePlayersMatchingSearchTerm": "\"{param}\" duten jokalariak", - "mobileNoSearchResults": "Emaitzarik ez", - "mobileAreYouSure": "Ziur zaude?", - "mobilePuzzleStreakAbortWarning": "Zure uneko bolada galduko duzu eta zure puntuazioa gorde egingo da.", - "mobilePuzzleStormNothingToShow": "Ez dago ezer erakusteko. Jokatu Ariketa zaparrada batzuk.", - "mobileSharePuzzle": "Partekatu ariketa hau", - "mobileShareGameURL": "Partekatu partidaren URLa", + "mobileSettingsTab": "Ezarpenak", "mobileShareGamePGN": "Partekatu PGNa", + "mobileShareGameURL": "Partekatu partidaren URLa", "mobileSharePositionAsFEN": "Partekatu posizioa FEN gisa", - "mobileShowVariations": "Erakutsi aukerak", - "mobileHideVariation": "Ezkutatu aukera", + "mobileSharePuzzle": "Partekatu ariketa hau", "mobileShowComments": "Erakutsi iruzkinak", - "mobilePuzzleStormConfirmEndRun": "Saiakera hau amaitu nahi duzu?", - "mobilePuzzleStormFilterNothingToShow": "Ez dago erakusteko ezer, aldatu filtroak", - "mobileCancelTakebackOffer": "Bertan behera utzi atzera-egite eskaera", - "mobileWaitingForOpponentToJoin": "Aurkaria sartzeko zain...", - "mobileBlindfoldMode": "Itsuka", - "mobileLiveStreamers": "Zuzeneko streamerrak", - "mobileCustomGameJoinAGame": "Sartu partida baten", - "mobileCorrespondenceClearSavedMove": "Garbitu gordetako jokaldia", - "mobileSomethingWentWrong": "Zerbait gaizki joan da.", "mobileShowResult": "Erakutsi emaitza", - "mobilePuzzleThemesSubtitle": "Jokatu zure irekiera gogokoenen ariketak, edo aukeratu gai bat.", - "mobilePuzzleStormSubtitle": "Ebatzi ahalik eta ariketa gehien 3 minututan.", - "mobileGreeting": "Kaixo {param}", - "mobileGreetingWithoutName": "Kaixo", - "mobilePrefMagnifyDraggedPiece": "Handitu arrastatutako pieza", + "mobileShowVariations": "Erakutsi aukerak", + "mobileSomethingWentWrong": "Zerbait gaizki joan da.", + "mobileSystemColors": "Sistemaren koloreak", + "mobileTheme": "Itxura", + "mobileToolsTab": "Tresnak", + "mobileWaitingForOpponentToJoin": "Aurkaria sartzeko zain...", + "mobileWatchTab": "Ikusi", "activityActivity": "Jarduera", "activityHostedALiveStream": "Zuzeneko emanaldi bat egin du", "activityRankedInSwissTournament": "Sailkapena {param1}/{param2}", @@ -109,6 +110,37 @@ "broadcastAgeThisYear": "Adina", "broadcastUnrated": "Ez du sailkapenik", "broadcastRecentTournaments": "Azken txapelketak", + "broadcastOpenLichess": "Ireki Lichessen", + "broadcastTeams": "Taldeak", + "broadcastBoards": "Taulak", + "broadcastOverview": "Laburpena", + "broadcastSubscribeTitle": "Harpidetu txanda bakoitza hastean jakinarazpena jasotzeko. Kanpaia edo push erako notifikazioak zure kontuaren hobespenetan aktibatu ditzakezu.", + "broadcastUploadImage": "Kargatu txapelketaren irudia", + "broadcastNoBoardsYet": "Taularik ez oraindik. Partidak igotzean agertuko dira.", + "broadcastBoardsCanBeLoaded": "Taulak iturburu batekin edo {param}ren bidez kargatu daitezke", + "broadcastStartsAfter": "{param}ren ondoren hasiko da", + "broadcastStartVerySoon": "Zuzenekoa berehala hasiko da.", + "broadcastNotYetStarted": "Zuzenekoa ez da oraindik hasi.", + "broadcastOfficialWebsite": "Webgune ofiziala", + "broadcastStandings": "Sailkapena", + "broadcastOfficialStandings": "Sailkapen ofiziala", + "broadcastIframeHelp": "Aukera gehiago {param}ean", + "broadcastWebmastersPage": "webmasterraren webgune", + "broadcastPgnSourceHelp": "Txanda honen zuzeneko PGN iturburua. {param} ere eskaintzen dugu sinkronizazio zehatzagoa nahi baduzu.", + "broadcastEmbedThisBroadcast": "Txertatu zuzeneko hau zure webgunean", + "broadcastEmbedThisRound": "Txertatu {param} zure webgunean", + "broadcastRatingDiff": "Elo diferentzia", + "broadcastGamesThisTournament": "Txapelketa honetako partidak", + "broadcastScore": "Emaitza", + "broadcastAllTeams": "Talde guztiak", + "broadcastTournamentFormat": "Txapelketaren formatua", + "broadcastTournamentLocation": "Txapelketaren kokalekua", + "broadcastTopPlayers": "Jokalari onenak", + "broadcastTimezone": "Ordu-zona", + "broadcastFideRatingCategory": "FIDE rating kategoria", + "broadcastOptionalDetails": "Hautazko xehetasunak", + "broadcastPastBroadcasts": "Pasatutako zuzenekoak", + "broadcastAllBroadcastsByMonth": "Ikusi zuzeneko guztiak hilabeteka", "broadcastNbBroadcasts": "{count, plural, =1{Zuzeneko {count}} other{{count} zuzeneko}}", "challengeChallengesX": "Erronkak: {param1}", "challengeChallengeToPlay": "Partida baterako erronka egin", @@ -233,6 +265,7 @@ "preferencesNotifyWeb": "Nabigatzailea", "preferencesNotifyDevice": "Gailua", "preferencesBellNotificationSound": "Kanpaiaren jakinarazpen soinua", + "preferencesBlindfold": "Itsuka", "puzzlePuzzles": "Ariketak", "puzzlePuzzleThemes": "Ariketen gaiak", "puzzleRecommended": "Gomendatutakoak", @@ -550,7 +583,6 @@ "replayMode": "Partida berriz ikusteko modua", "realtimeReplay": "Denbora errealean", "byCPL": "CPL", - "openStudy": "Ikerketa ireki", "enable": "Aktibatu", "bestMoveArrow": "Jokaldi onenaren gezia", "showVariationArrows": "Erakutsi aldaeren geziak", @@ -641,6 +673,7 @@ "rank": "Maila", "rankX": "Sailkapena: {param}", "gamesPlayed": "Partida jokaturik", + "ok": "OK", "cancel": "Ezeztatu", "whiteTimeOut": "Zuriaren denbora agortu egin da", "blackTimeOut": "Beltzaren denbora agortu egin da", @@ -757,7 +790,6 @@ "block": "Blokeatu", "blocked": "Blokeatuta", "unblock": "Desblokeatu", - "followsYou": "Zu jarraitzen", "xStartedFollowingY": "{param1} {param2} jarraitzen hasi da", "more": "Gehiago", "memberSince": "Noiztik kidea:", @@ -1263,6 +1295,7 @@ "showMeEverything": "Erakutsi guztia", "lichessPatronInfo": "Lichess software librea da.\nGarapen eta mantentze-kostu guztiak erabiltzaileen dohaintzekin ordaintzen dira.", "nothingToSeeHere": "Hemen ez dago ezer zuretzat.", + "stats": "Estatistikak", "opponentLeftCounter": "{count, plural, =1{Zure aurkariak partida utzi egin du. Partida irabaztea eskatu dezakezu segundo {count}en.} other{Zure aurkariak partida utzi egin du. Partida irabaztea eskatu dezakezu {count} segundotan.}}", "mateInXHalfMoves": "{count, plural, =1{Mate jokaldi erdi {count}n} other{Mate {count} jokaldi erditan}}", "nbBlunders": "{count, plural, =1{Hanka-sartze {count}} other{{count} hanka-sartze}}", @@ -1510,6 +1543,7 @@ "studyPlayAgain": "Jokatu berriz", "studyWhatWouldYouPlay": "Zer jokatuko zenuke posizio honetan?", "studyYouCompletedThisLesson": "Zorionak! Ikasgai hau osatu duzu.", + "studyPerPage": "{param} orrialde bakoitzean", "studyNbChapters": "{count, plural, =1{Kapitulu {count}} other{{count} kapitulu}}", "studyNbGames": "{count, plural, =1{Partida {count}} other{{count} partida}}", "studyNbMembers": "{count, plural, =1{Kide {count}} other{{count} kide}}", diff --git a/lib/l10n/lila_fa.arb b/lib/l10n/lila_fa.arb index eca15d7121..14c9eb2aa4 100644 --- a/lib/l10n/lila_fa.arb +++ b/lib/l10n/lila_fa.arb @@ -1,66 +1,67 @@ { + "mobileAllGames": "همه بازی‌ها", + "mobileAreYouSure": "مطمئنید؟", + "mobileBlindfoldMode": "چشم‌بسته", + "mobileCancelTakebackOffer": "رد درخواست برگرداندن", + "mobileClearButton": "پاکسازی", + "mobileCorrespondenceClearSavedMove": "پاک کردن حرکت ذخیره شده", + "mobileCustomGameJoinAGame": "به بازی بپیوندید", + "mobileFeedbackButton": "بازخورد", + "mobileGreeting": "درود، {param}", + "mobileGreetingWithoutName": "درود", + "mobileHideVariation": "پنهانیدن وَرتِش", "mobileHomeTab": "خانه", - "mobilePuzzlesTab": "معماها", - "mobileToolsTab": "ابزارها", - "mobileWatchTab": "تماشا", - "mobileSettingsTab": "تنظیمات", + "mobileLiveStreamers": "بَرخَط-محتواسازان زنده", "mobileMustBeLoggedIn": "برای دیدن این برگه باید وارد شده باشید.", - "mobileSystemColors": "رنگ‌های دستگاه", - "mobileFeedbackButton": "بازخورد", + "mobileNoSearchResults": "بدون پیامد", + "mobileNotFollowingAnyUser": "شما هیچ کاربری را نمی‌دنبالید.", "mobileOkButton": "باشه", + "mobilePlayersMatchingSearchTerm": "کاربران با پیوند «{param}»", + "mobilePrefMagnifyDraggedPiece": "بزرگ‌نمودن مهره‌ی کشیده", + "mobilePuzzleStormConfirmEndRun": "می‌خواهید این دور را به پایان برسانید؟", + "mobilePuzzleStormFilterNothingToShow": "چیزی برای نمایش نیست، خواهشمندیم پالایه‌ها را دگرسان کنید.", + "mobilePuzzleStormNothingToShow": "چیزی برای نمایش نیست، چند دور معماباران بازی کنید.", + "mobilePuzzleStormSubtitle": "هر چند تا معما را که می‌توانید در ۳ دقیقه حل کنید.", + "mobilePuzzleStreakAbortWarning": "شما ریسه فعلی‌تان را خواهید باخت و امتیازتان ذخیره خواهد شد.", + "mobilePuzzleThemesSubtitle": "معماهایی را از گشایش دلخواه‌تان بازی کنید، یا جستاری را برگزینید.", + "mobilePuzzlesTab": "معماها", + "mobileRecentSearches": "واپسین جستجوها", "mobileSettingsHapticFeedback": "بازخورد لمسی", "mobileSettingsImmersiveMode": "حالت فراگیر", "mobileSettingsImmersiveModeSubtitle": "رابط کاربری را هنگام بازی پنهان کنید. اگر ناوبری لمسی در لبه‌های دستگاه اذیتتان می‌کند از این استفاده کنید. کارساز برای برگه‌های بازی و معماباران.", - "mobileNotFollowingAnyUser": "شما هیچ کاربری را دنبال نمی‌کنید.", - "mobileAllGames": "همه بازی‌ها", - "mobileRecentSearches": "واپسین جستجوها", - "mobileClearButton": "پاکسازی", - "mobilePlayersMatchingSearchTerm": "کاربران با پیوند «{param}»", - "mobileNoSearchResults": "بدون پیامد", - "mobileAreYouSure": "مطمئنید؟", - "mobilePuzzleStreakAbortWarning": "شما ریسه فعلی‌تان را خواهید باخت و امتیازتان ذخیره خواهد شد.", - "mobilePuzzleStormNothingToShow": "چیزی برای نمایش نیست، چند دور معماباران بازی کنید.", - "mobileSharePuzzle": "همرسانی این معما", - "mobileShareGameURL": "همرسانی وب‌نشانی بازی", + "mobileSettingsTab": "تنظیمات", "mobileShareGamePGN": "همرسانی PGN", + "mobileShareGameURL": "همرسانی وب‌نشانی بازی", "mobileSharePositionAsFEN": "همرسانی وضعیت، به شکل FEN", - "mobileShowVariations": "باز کردن شاخه‌ها", - "mobileHideVariation": "بستن شاخه‌ها", + "mobileSharePuzzle": "همرسانی این معما", "mobileShowComments": "نمایش دیدگاه‌ها", - "mobilePuzzleStormConfirmEndRun": "می‌خواهید این دور را به پایان برسانید؟", - "mobilePuzzleStormFilterNothingToShow": "چیزی برای نمایش نیست، خواهشمندیم پالایه‌ها را دگرسان کنید.", - "mobileCancelTakebackOffer": "رد درخواست برگرداندن", - "mobileWaitingForOpponentToJoin": "شکیبا برای پیوستن حریف...", - "mobileBlindfoldMode": "چشم‌بسته", - "mobileLiveStreamers": "بَرخَط-محتواسازان زنده", - "mobileCustomGameJoinAGame": "به بازی بپیوندید", - "mobileCorrespondenceClearSavedMove": "پاک کردن حرکت ذخیره شده", - "mobileSomethingWentWrong": "مشکلی پیش آمد.", "mobileShowResult": "نمایش پیامد", - "mobilePuzzleThemesSubtitle": "معماهایی را از گشایش دلخواه‌تان بازی کنید، یا جستاری را برگزینید.", - "mobilePuzzleStormSubtitle": "هر چند تا معما را که می‌توانید در ۳ دقیقه حل کنید.", - "mobileGreeting": "درود، {param}", - "mobileGreetingWithoutName": "درود", - "mobilePrefMagnifyDraggedPiece": "بزرگ‌نمودن مهره‌ی کشیده", + "mobileShowVariations": "نمایش وَرتِش", + "mobileSomethingWentWrong": "مشکلی پیش آمد.", + "mobileSystemColors": "رنگ‌های دستگاه", + "mobileTheme": "پوسته", + "mobileToolsTab": "ابزارها", + "mobileWaitingForOpponentToJoin": "در انتظار آمدن حریف...", + "mobileWatchTab": "تماشا", "activityActivity": "فعالیت", "activityHostedALiveStream": "میزبان پخش زنده بود", "activityRankedInSwissTournament": "رتبه #{param1} را در {param2} به دست آورد", - "activitySignedUp": "در لیچس ثبت نام کرد", + "activitySignedUp": "در lichess.org نام‌نوشت", "activitySupportedNbMonths": "{count, plural, =1{به عنوان {param2} برای {count} ماه از lichess.org حمایت کرد} other{به عنوان {param2} برای {count} ماه از lichess.org حمایت کرد}}", "activityPracticedNbPositions": "{count, plural, =1{{count} وضعیت تمرین‌شده در {param2}} other{{count} وضعیت تمرین‌شده در {param2}}}", "activitySolvedNbPuzzles": "{count, plural, =1{{count} معمای آموزشی را حل کرد} other{{count} مساله تاکتیکی را حل کرد}}", - "activityPlayedNbGames": "{count, plural, =1{{count} بازی {param2} را انجام داد} other{{count} بازی {param2} را انجام داد}}", - "activityPostedNbMessages": "{count, plural, =1{{count} پیام را در {param2} فرستاد} other{{count} پیام را در {param2} فرستاد}}", + "activityPlayedNbGames": "{count, plural, =1{{count} بازی {param2} کرد} other{{count} بازی {param2} کرد}}", + "activityPostedNbMessages": "{count, plural, =1{{count} پیام در {param2} فرستاد} other{{count} پیام در {param2} فرستاد}}", "activityPlayedNbMoves": "{count, plural, =1{{count} حرکت انجام داد} other{{count} حرکت انجام داد}}", "activityInNbCorrespondenceGames": "{count, plural, =1{در {count} بازی مکاتبه‌ای} other{در {count} بازی مکاتبه‌ای}}", "activityCompletedNbGames": "{count, plural, =1{{count} بازی مکاتبه‌ای را به پایان رساند} other{{count} بازی مکاتبه‌ای را به پایان رساند}}", "activityCompletedNbVariantGames": "{count, plural, =1{تکمیل {count} بازی مکاتبه‌ای {param2}} other{تکمیل {count} بازی مکاتبه‌ای {param2}}}", - "activityFollowedNbPlayers": "{count, plural, =1{{count} بازیکن را دنبال کرد} other{شروع به دنبالیدن {count} بازیکن کرد}}", + "activityFollowedNbPlayers": "{count, plural, =1{شروع به دنبالیدن {count} بازیکن کرد} other{شروع به دنبالیدن {count} بازیکن کرد}}", "activityGainedNbFollowers": "{count, plural, =1{{count} دنبال‌گر جدید به‌دست آورد} other{{count} دنبال‌گر جدید به‌دست آورد}}", "activityHostedNbSimuls": "{count, plural, =1{{count} مسابقه هم‌زمان برگزار کرد} other{{count} مسابقه هم‌زمان برگزار کرد}}", "activityJoinedNbSimuls": "{count, plural, =1{در {count} مسابقه هم‌زمان شرکت کرد} other{در {count} مسابقه هم‌زمان شرکت کرد}}", "activityCreatedNbStudies": "{count, plural, =1{{count} درس جدید ساخت} other{{count} درس جدید ساخت}}", - "activityCompetedInNbTournaments": "{count, plural, =1{در {count} مسابقه آرنا رقابت کرد} other{در {count} مسابقه آرنا رقابت کرد}}", + "activityCompetedInNbTournaments": "{count, plural, =1{در {count} مسابقهٔ راوان رقابت کرد} other{در {count} مسابقهٔ راوان رقابت کرد}}", "activityRankedInTournament": "{count, plural, =1{رتبه #{count} ({param2}% برتر) با {param3} بازی در {param4}} other{رتبه #{count} ({param2}% برتر) با {param3} بازی در {param4}}}", "activityCompetedInNbSwissTournaments": "{count, plural, =1{در {count} مسابقه سوئیسی رقابت کرد} other{در {count} مسابقه سوئیسی رقابت کرد}}", "activityJoinedNbTeams": "{count, plural, =1{به {count} تیم پیوست} other{به {count} تیم پیوست}}", @@ -69,7 +70,7 @@ "broadcastLiveBroadcasts": "پخش زنده مسابقات", "broadcastBroadcastCalendar": "تقویم پخش", "broadcastNewBroadcast": "پخش زنده جدید", - "broadcastSubscribedBroadcasts": "پخش‌های دنبال‌شده", + "broadcastSubscribedBroadcasts": "پخش‌های دنبالیده", "broadcastAboutBroadcasts": "درباره پخش‌های همگانی", "broadcastHowToUseLichessBroadcasts": "نحوه استفاده از پخش همگانی Lichess.", "broadcastTheNewRoundHelp": "دور جدید، همان اعضا و مشارکت‌کنندگان دور قبلی را خواهد داشت.", @@ -77,7 +78,7 @@ "broadcastOngoing": "ادامه‌دار", "broadcastUpcoming": "آینده", "broadcastCompleted": "کامل‌شده", - "broadcastCompletedHelp": "Lichess تکمیل دور را بر اساس بازی‌های منبع تشخیص می‌دهد. اگر منبعی وجود ندارد، از این کلید استفاده کنید.", + "broadcastCompletedHelp": "Lichess تکمیل دور را شناسایی می‌کند، اما می‌تواند آن را اشتباه بگیرد. از این کلید برای تنظیم دستی بهرایید.", "broadcastRoundName": "نام دور", "broadcastRoundNumber": "شماره دور", "broadcastTournamentName": "نام مسابقات", @@ -86,7 +87,7 @@ "broadcastFullDescriptionHelp": "توضیحات بلند و اختیاری پخش همگانی. {param1} قابل‌استفاده است. طول متن باید کمتر از {param2} نویسه باشد.", "broadcastSourceSingleUrl": "وب‌نشانیِ PGN", "broadcastSourceUrlHelp": "وب‌نشانی‌ای که Lichess برای دریافت به‌روزرسانی‌های PGN می‌بررسد. آن باید از راه اینترنت در دسترس همگان باشد.", - "broadcastSourceGameIds": "تا ۶۴ شناسه بازی لیچس٬ جداشده با فاصله.", + "broadcastSourceGameIds": "تا ۶۴ شناسهٔ بازی Lichess، جداشده با فاصله.", "broadcastStartDateTimeZone": "تاریخ آغاز در زمان-یانه محلی مسابقات: {param}", "broadcastStartDateHelp": "اختیاری است، اگر می‌دانید چه زمانی رویداد شروع می‌شود", "broadcastCurrentGameUrl": "نشانی بازی کنونی", @@ -109,12 +110,36 @@ "broadcastAgeThisYear": "سنِ امسال", "broadcastUnrated": "بی‌درجه‌بندی", "broadcastRecentTournaments": "مسابقاتِ اخیر", - "broadcastTeams": "تیم‌ها", + "broadcastOpenLichess": "آزاد در Lichess", + "broadcastTeams": "یَران‌ها", + "broadcastBoards": "میز‌ها", + "broadcastOverview": "نمای کلی", + "broadcastSubscribeTitle": "مشترک شوید تا از آغاز هر دور باخبر شوید. می‌توانید اعلان‌های زنگی یا رانشی برای پخش‌های زنده را در تنظیمات حساب‌تان تغییر دهید.", + "broadcastUploadImage": "بارگذاری تصویر مسابقات", + "broadcastNoBoardsYet": "تاکنون هیچی. وقتی بازی‌ها بارگذاری شدند، میزها پدیدار خواهند شد.", + "broadcastBoardsCanBeLoaded": "میزها را می‌توان از یک منبع یا از راه {param} بارگذاری کرد", "broadcastStartVerySoon": "پخش زنده به زودی آغاز خواهد شد.", - "broadcastNotYetStarted": "پخش زنده هنوز آغاز نشده است.", - "broadcastOfficialWebsite": "تارنمای رسمی", - "broadcastRatingDiff": "ناسانی امتیازات", + "broadcastNotYetStarted": "پخش زنده هنوز نیاغازیده است.", + "broadcastOfficialWebsite": "وبگاه رسمی", + "broadcastStandings": "رده‌بندی", + "broadcastOfficialStandings": "رده‌بندی رسمی", + "broadcastIframeHelp": "گزینه‌های بیشتر در {param}", + "broadcastWebmastersPage": "صفحهٔ وبداران", + "broadcastPgnSourceHelp": "یک منبع عمومی و بی‌درنگ PGN برای این دور. ما همچنین {param} را برای همگامِش تندتر و کارآمدتر پیشنهاد می‌دهیم.", + "broadcastEmbedThisBroadcast": "جاسازی این پخش زنده در وبگاه‌تان", + "broadcastEmbedThisRound": "جاسازی {param} در وبگاه‌تان", + "broadcastRatingDiff": "اختلاف درجه‌بندی", + "broadcastGamesThisTournament": "بازی‌های این مسابقات", "broadcastScore": "امتیاز", + "broadcastAllTeams": "همهٔ یَران‌ها", + "broadcastTournamentFormat": "ساختار مسابقات", + "broadcastTournamentLocation": "مکان مسابقات", + "broadcastTopPlayers": "بازیکنان برتر", + "broadcastTimezone": "زمان-یانه", + "broadcastFideRatingCategory": "رسته‌بندی درجه‌بندی فیده", + "broadcastOptionalDetails": "جزئیات اختیاری", + "broadcastPastBroadcasts": "پخش‌های گذشته", + "broadcastAllBroadcastsByMonth": "دیدن پخش‌های هر ماه", "broadcastNbBroadcasts": "{count, plural, =1{{count} پخش همگانی} other{{count} پخش همگانی}}", "challengeChallengesX": "پیشنهاد بازی: {param1}", "challengeChallengeToPlay": "پیشنهاد بازی دادن", @@ -125,7 +150,7 @@ "challengeYouCannotChallengeX": "شما نمی‌توانید به {param} پیشنهاد بازی دهید.", "challengeXDoesNotAcceptChallenges": "{param} پیشنهاد بازی را نپذیرفت.", "challengeYourXRatingIsTooFarFromY": "درجه‌بندی {param1} شما با {param2} اختلاف زیادی دارد.", - "challengeCannotChallengeDueToProvisionalXRating": "به‌خاطر داشتن درجه‌بندی {param} موقت، نمی‌توانید پیشنهاد بازی دهید.", + "challengeCannotChallengeDueToProvisionalXRating": "به‌خاطر درجه‌بندی {param} موقت، نمی‌توانید پیشنهاد بازی دهید.", "challengeXOnlyAcceptsChallengesFromFriends": "{param} فقط پیشنهاد بازی از دوستانش را می‌پذیرد.", "challengeDeclineGeneric": "من فعلا پیشنهاد بازی نمی‌پذیرم.", "challengeDeclineLater": "الان زمان مناسبی برای من نیست، لطفا بعدا دوباره درخواست دهید.", @@ -134,8 +159,8 @@ "challengeDeclineTimeControl": "من با این زمان‌بندی، پیشنهاد بازی را نمی‌پذیرم.", "challengeDeclineRated": "لطفا به جایش، پیشنهاد بازی رسمی بده.", "challengeDeclineCasual": "لطفا به جایش، پیشنهاد بازی نارسمی بده.", - "challengeDeclineStandard": "الان پیشنهاد بازی‌های شطرنج‌گونه را نمی‌پذیرم.", - "challengeDeclineVariant": "الان مایل نیستم این شطرنج‌گونه را بازی کنم.", + "challengeDeclineStandard": "اکنون پیشنهاد بازی‌های وَرتا را نمی‌پذیرم.", + "challengeDeclineVariant": "اکنون مایل نیستم این وَرتا را بازی کنم.", "challengeDeclineNoBot": "من پیشنهاد بازی از ربات‌ها را نمی‌پذیرم.", "challengeDeclineOnlyBot": "من فقط پیشنهاد بازی از ربات‌ها را می‌پذیرم.", "challengeInviteLichessUser": "یا یک کاربر Lichess را دعوت کنید:", @@ -143,13 +168,13 @@ "contactContactLichess": "ارتباط با Lichess", "patronDonate": "کمک مالی", "patronLichessPatron": "یاورِ Lichess", - "perfStatPerfStats": "وضعیت {param}", + "perfStatPerfStats": "آمار {param}", "perfStatViewTheGames": "دیدن بازی‌ها", "perfStatProvisional": "موقت", "perfStatNotEnoughRatedGames": "بازی های رسمی کافی برای تعیین کردن یک درجه‌بندی قابل‌اتکا انجام نشده است.", "perfStatProgressOverLastXGames": "پیشرفت در آخرین {param} بازی ها:", "perfStatRatingDeviation": "انحراف درجه‌بندی: {param}.", - "perfStatRatingDeviationTooltip": "مقدار کمتر به این معنی است که درجه‌بندی پایدارتر است. بالاتر از {param1}، درجه‌بندی موقت در نظر گرفته می‌شود. برای قرار گرفتن در درجه‌بندی‌ها، این مقدار باید کم‌تر از {param2} (در شطرنج استاندارد) یا {param3} (در شطرنج‌گونه‌ها) باشد.", + "perfStatRatingDeviationTooltip": "مقدار کمتر به معنای درجه‌بندی پایدارتر است. بالاتر از {param1}، درجه‌بندی موقت در نظر گرفته می‌شود. برای قرارگیری در درجه‌بندی‌ها، این مقدار باید کم‌تر از {param2} (در شطرنج استاندارد) یا {param3} (در وَرتاها) باشد.", "perfStatTotalGames": "تمام بازی ها", "perfStatRatedGames": "بازی های رسمی", "perfStatTournamentGames": "بازی های مسابقه ای", @@ -175,13 +200,13 @@ "preferencesPreferences": "تنظیمات", "preferencesDisplay": "صفحه نمایش", "preferencesPrivacy": "امنیت و حریم شخصی", - "preferencesNotifications": "اعلانات", + "preferencesNotifications": "اعلان", "preferencesPieceAnimation": "حرکت مهره ها", "preferencesMaterialDifference": "تفاوت مُهره‌ها", "preferencesBoardHighlights": "رنگ‌نمایی صفحه (آخرین حرکت و کیش)", "preferencesPieceDestinations": "مقصد مهره(حرکت معتبر و پیش حرکت )", "preferencesBoardCoordinates": "مختصات صفحه(A-H، 1-8)", - "preferencesMoveListWhilePlaying": "لیست حرکات هنگام بازی کردن", + "preferencesMoveListWhilePlaying": "فهرست حرکت هنگام بازی کردن", "preferencesPgnPieceNotation": "نشانه‌گذاری حرکات", "preferencesChessPieceSymbol": "نماد مهره", "preferencesPgnLetter": "حرف (K, Q, R, B, N)", @@ -203,8 +228,8 @@ "preferencesClickTwoSquares": "انتخاب دو مربع مبدا و مقصد", "preferencesDragPiece": "کشیدن یک مهره", "preferencesBothClicksAndDrag": "هر دو", - "preferencesPremovesPlayingDuringOpponentTurn": "پیش حرکت (بازی در نوبت حریف)", - "preferencesTakebacksWithOpponentApproval": "پس گرفتن حرکت (با تایید حریف)", + "preferencesPremovesPlayingDuringOpponentTurn": "پیش‌حرکت (بازی در نوبت حریف)", + "preferencesTakebacksWithOpponentApproval": "برگردان (با تایید حریف)", "preferencesInCasualGamesOnly": "فقط در بازی‌های نارسمی", "preferencesPromoteToQueenAutomatically": "ارتقا خودکار به وزیر", "preferencesExplainPromoteToQueenAutomatically": " را در هنگام تبلیغ بزنید تا به طور موقت تبلیغات خودکار را غیرفعال کنید", @@ -225,20 +250,21 @@ "preferencesSayGgWpAfterLosingOrDrawing": "گفتن \"بازی خوبی بود، خوب بازی کردی\" در هنگام باخت یا تساوی", "preferencesYourPreferencesHaveBeenSaved": "تغییرات شما ذخیره شده است", "preferencesScrollOnTheBoardToReplayMoves": "برای بازپخش حرکت‌ها، روی صفحه بازی بِنَوَردید", - "preferencesCorrespondenceEmailNotification": "ایمیل های روزانه که بازی های شبیه شما را به صورت لیست درمی‌آورند", - "preferencesNotifyStreamStart": "استریمر شروع به فعالیت کرد", + "preferencesCorrespondenceEmailNotification": "فهرست رایانامهٔ روزانه از بازی‌های مکاتبه‌ای‌تان", + "preferencesNotifyStreamStart": "بَرخَط-محتواساز روی پخش است", "preferencesNotifyInboxMsg": "پیام جدید", "preferencesNotifyForumMention": "در انجمن از شما نام‌بُرده‌اند", "preferencesNotifyInvitedStudy": "دعوت به مطالعه", - "preferencesNotifyGameEvent": "اعلان به روزرسانی بازی", + "preferencesNotifyGameEvent": "به‌روزرسانی‌های بازی مکاتبه‌ای", "preferencesNotifyChallenge": "پیشنهاد بازی", - "preferencesNotifyTournamentSoon": "تورنمت به زودی آغاز می شود", + "preferencesNotifyTournamentSoon": "مسابقات به‌زودی می‌آغازد", "preferencesNotifyTimeAlarm": "هشدار تنگی زمان", - "preferencesNotifyBell": "زنگوله اعلانات لیچس", - "preferencesNotifyPush": "اعلانات برای زمانی که شما در لیچس نیستید", + "preferencesNotifyBell": "اعلان زنگی در Lichess", + "preferencesNotifyPush": "اعلان اَفزاره، هنگامی که در Lichess نیستید", "preferencesNotifyWeb": "مرورگر", - "preferencesNotifyDevice": "دستگاه", - "preferencesBellNotificationSound": "زنگ اعلان", + "preferencesNotifyDevice": "اَفزاره", + "preferencesBellNotificationSound": "صدای اعلان زنگی", + "preferencesBlindfold": "چشم‌بسته", "puzzlePuzzles": "معماها", "puzzlePuzzleThemes": "موضوع معما", "puzzleRecommended": "توصیه شده", @@ -303,12 +329,12 @@ "puzzleLookupOfPlayer": "به دنبال معماهای برگرفته از بازی‌های یک بازیکن مشخص، بگردید", "puzzleFromXGames": "معماهای برگرفته از بازی‌های {param}", "puzzleSearchPuzzles": "جستجوی معما", - "puzzleFromMyGamesNone": "شما هیچ معمایی در دادگان ندارید، اما Lichess همچنان شما را بسیار دوست دارد.\n\nبازی‌های سریع و مرسوم را انجام دهید تا بخت‌تان را برای افزودن معمایی از خودتان بیفزایید!", + "puzzleFromMyGamesNone": "شما هیچ معمایی در دادگان ندارید، اما Lichess همچنان شما را بسیار دوست دارد.\n\nبازی‌های سریع و فکری را انجام دهید تا بخت‌تان را برای افزودن معمایی از خودتان بیفزایید!", "puzzleFromXGamesFound": "{param1} معما در بازی‌های {param2} یافت شد", - "puzzlePuzzleDashboardDescription": "تمرین کن، تحلیل کن، پیشرفت کن", + "puzzlePuzzleDashboardDescription": "آموزش، واکاوی، بهبود", "puzzlePercentSolved": "{param} حل‌شده", "puzzleNoPuzzlesToShow": "چیزی برای نمایش نیست، نخست بروید و چند معما حل کنید!", - "puzzleImprovementAreasDescription": "این‌ها را تمرین کنید تا روند پیشرفت خود را بهبود ببخشید!", + "puzzleImprovementAreasDescription": "برای بهینیدن پیشرفت‌تان، این‌ها را بیاموزید!", "puzzleStrengthDescription": "شما در این زمینه‌ها بهترین عملکرد را دارید", "puzzlePlayedXTimes": "{count, plural, =1{{count} بار بازی شده است} other{{count} بار بازی شده}}", "puzzleNbPointsBelowYourPuzzleRating": "{count, plural, =1{یک امتیاز زیر درجه‌بندی معمایی‌تان} other{{count} امتیاز زیر درجه‌بندی معمایی‌تان}}", @@ -426,7 +452,7 @@ "puzzleThemeSuperGMDescription": "معماهای برگرفته از بازی‌های بهترین بازیکنان جهان.", "puzzleThemeTrappedPiece": "مهره به‌دام‌افتاده", "puzzleThemeTrappedPieceDescription": "یک مهره قادر به فرار کردن از زده شدن نیست چون حرکات محدودی دارد.", - "puzzleThemeUnderPromotion": "فرو-ارتقا", + "puzzleThemeUnderPromotion": "کم‌ارتقا", "puzzleThemeUnderPromotionDescription": "ارتقا به اسب، فیل یا رخ.", "puzzleThemeVeryLong": "معمای خیلی طولانی", "puzzleThemeVeryLongDescription": "بُردن با چهار حرکت یا بیشتر.", @@ -435,7 +461,7 @@ "puzzleThemeZugzwang": "زوگزوانگ", "puzzleThemeZugzwangDescription": "حریف در حرکت‌هایش محدود است و همه‌شان وضعیتش را بدتر می‌کند.", "puzzleThemeMix": "آمیزهٔ همگن", - "puzzleThemeMixDescription": "ذره‌ای از هر چیزی. شما نمی‌دانید چه چیزی پیش روی شماست، بنابراین برای هر چیزی آماده می‌مانید! درست مانند بازی‌های واقعی.", + "puzzleThemeMixDescription": "کمی از هر چیزی. شما نمی‌دانید چه چیزی پیش روی شماست، بنابراین برای هر چیزی آماده می‌مانید! درست مانند بازی‌های واقعی.", "puzzleThemePlayerGames": "بازی‌های بازیکن", "puzzleThemePlayerGamesDescription": "دنبال معماهای ایجادشده از بازی‌های خودتان یا بازی‌های سایر بازیکنان، بگردید.", "puzzleThemePuzzleDownloadInformation": "این معماها به صورت همگانی هستند و می‌توانید از {param} بارگیریدشان.", @@ -450,10 +476,10 @@ "settingsThisAccountIsClosed": "این حساب بسته شده است", "playWithAFriend": "بازی با دوستان", "playWithTheMachine": "بازی با رایانه", - "toInviteSomeoneToPlayGiveThisUrl": "برای دعوت کسی به بازی، این وب‌نشانی را دهید", + "toInviteSomeoneToPlayGiveThisUrl": "برای دعوت کردن حریف این لینک را برای او بفرستید", "gameOver": "پایان بازی", "waitingForOpponent": "انتطار برای حریف", - "orLetYourOpponentScanQrCode": "یا اجازه دهید حریف شما این QR کد را پویش کند", + "orLetYourOpponentScanQrCode": "یا اجازه دهید حریف‌تان این کد QR را بِروبینَد", "waiting": "در حال انتظار", "yourTurn": "نوبت شماست", "aiNameLevelAiLevel": "{param1} سطح {param2}", @@ -477,9 +503,9 @@ "itsYourTurn": "نوبت شماست!", "cheatDetected": "تقلب تشخیص داده شد", "kingInTheCenter": "شاه روی تپه", - "threeChecks": "سه کیش", + "threeChecks": "سه‌کیش", "raceFinished": "مسابقه تمام شد", - "variantEnding": "پایان شطرنج‌گونه", + "variantEnding": "پایان وَرتا", "newOpponent": "حریف جدید", "yourOpponentWantsToPlayANewGameWithYou": "حریف شما می خواهد که دوباره با شما بازی کند", "joinTheGame": "به بازی بپیوندید", @@ -503,24 +529,24 @@ "analysis": "تحلیل بازی", "depthX": "عمق {param}", "usingServerAnalysis": "با استفاده از کارسازِ تحلیل", - "loadingEngine": "پردازشگر بارمی‌گذارد...", - "calculatingMoves": "در حال محاسبه حرکات...", - "engineFailed": "خطا در بارگذاری پردازشگر", + "loadingEngine": "موتور بارمی‌گذارد...", + "calculatingMoves": "محاسبهٔ حرکت‌ها...", + "engineFailed": "خطا در بارگذاری موتور", "cloudAnalysis": "تحلیل ابری", "goDeeper": "بررسی عمیق‌تر", "showThreat": "نمایش تهدید", "inLocalBrowser": "در مرورگر محلی", "toggleLocalEvaluation": "کلید ارزیابی محلی", - "promoteVariation": "افزایش عمق شاخه اصلی", + "promoteVariation": "ارتقای وَرتِش", "makeMainLine": "خط کنونی را به خط اصلی تبدیل کنید", "deleteFromHere": "از اینجا به بعد را پاک کنید", - "collapseVariations": "بستن شاخه‌ها", - "expandVariations": "باز کردن شاخه‌ها", - "forceVariation": "نتیجه تحلیل را به عنوان یکی از تنوعهای بازی انتخاب نمایید", - "copyVariationPgn": "کپی PGN این شاخه", + "collapseVariations": "بستن وَرتِش‌ها", + "expandVariations": "گستردنِ وَرتِش‌ها", + "forceVariation": "وَرتِشِ زوری", + "copyVariationPgn": "رونوشت‌گیری PGN ِ وَرتِش", "move": "حرکت", - "variantLoss": "حرکت بازنده", - "variantWin": "بُردِ شطرنج‌گونه", + "variantLoss": "باختِ وَرتا", + "variantWin": "بُردِ وَرتا", "insufficientMaterial": "مُهره ناکافی برای مات", "pawnMove": "حرکت پیاده", "capture": "گرفتن مهره", @@ -542,7 +568,7 @@ "openings": "گشایش‌ها", "openingExplorer": "پویشگر گشایش‌", "openingEndgameExplorer": "پویشگر گشایش/آخربازی", - "xOpeningExplorer": "جستجوگر گشایش {param}", + "xOpeningExplorer": "پویشگر گشایش {param}", "playFirstOpeningEndgameExplorerMove": "نخستین حرکت گشایش/آخربازی پویشگر را برو", "winPreventedBy50MoveRule": "قانون پنجاه حرکت جلوی پیروزی را گرفت", "lossSavedBy50MoveRule": "قانون ۵۰ حرکت از شکست جلوگیری کرد", @@ -556,28 +582,27 @@ "replayMode": "حالت بازپخش", "realtimeReplay": "مشابه بازی", "byCPL": "درنگ هنگام اشتباه", - "openStudy": "گشودن مطالعه", "enable": "فعال سازی", - "bestMoveArrow": "فلش نشان دهنده بهترین حرکت", - "showVariationArrows": "نمایش پیکان‌های شاخه اصلی", + "bestMoveArrow": "پیکانِ بهترین حرکت", + "showVariationArrows": "نمایش پیکان‌های وَرتِش", "evaluationGauge": "میله ارزیابی", "multipleLines": "شاخه های متعدد", "cpus": "پردازنده(ها)", "memory": "حافظه", - "infiniteAnalysis": "آنالیز بی پایان", + "infiniteAnalysis": "تحلیل بی‌کران", "removesTheDepthLimit": "محدودیت عمق را برمی‌دارد و رایانه‌تان داغ می‌ماند", "blunder": "اشتباه فاحش", "mistake": "اشتباه", "inaccuracy": "بی دقتی", "moveTimes": "مدت حركت‌ها", "flipBoard": "چرخاندن صفحه", - "threefoldRepetition": "تکرار سه گانه", + "threefoldRepetition": "تکرار سه‌گانه", "claimADraw": "ادعای تساوی", "offerDraw": "پیشنهاد مساوی", "draw": "مساوی", "drawByMutualAgreement": "تساوی با توافق طرفین", "fiftyMovesWithoutProgress": "قانون ۵۰ حرکت", - "currentGames": "بازی های در جریان", + "currentGames": "بازی‌های جاری", "viewInFullSize": "نمایش در اندازه کامل", "logOut": "خروج", "signIn": "ورود", @@ -595,9 +620,9 @@ "discussions": "مکالمات", "today": "امروز", "yesterday": "دیروز", - "minutesPerSide": "زمان برای هر بازیکن(به دقیقه)", - "variant": "گونه", - "variants": "گونه‌ها", + "minutesPerSide": "هر بازیکن چند دقیقه", + "variant": "وَرتا", + "variants": "وَرتا", "timeControl": "زمان", "realTime": "زمان محدود", "correspondence": "مکاتبه ای", @@ -607,23 +632,23 @@ "rating": "درجه‌بندی", "ratingStats": "آماره‌های درجه‌بندی", "username": "نام کاربری", - "usernameOrEmail": "نام کاربری یا ایمیل", + "usernameOrEmail": "نام کاربری یا رایانامه", "changeUsername": "تغییر نام کاربری", "changeUsernameNotSame": "تنها اندازه حروف میتوانند تغییر کنند. برای مثال \"johndoe\" به \"JohnDoe\".", "changeUsernameDescription": "نام کاربری خود را تغییر دهید. این تنها یک بار انجام پذیر است و شما تنها مجازید اندازه حروف نام کاربری‌تان را تغییر دهید.", "signupUsernameHint": "مطمئن شوید که یک نام کاربری مناسب انتخاب میکنید. بعداً نمی توانید آن را تغییر دهید و هر حسابی با نام کاربری نامناسب بسته می شود!", "signupEmailHint": "ما فقط برای بازنشاندن گذرواژه، از آن استفاده خواهیم کرد.", - "password": "رمز عبور", - "changePassword": "تغییر کلمه عبور", + "password": "گذرواژه", + "changePassword": "تغییر گذرواژه", "changeEmail": "تغییر ایمیل", "email": "ایمیل", - "passwordReset": "بازیابی کلمه عبور", - "forgotPassword": "آیا کلمه عبور را فراموش کرده اید؟", - "error_weakPassword": "این رمز به شدت معمول و قابل حدس است.", - "error_namePassword": "لطفا رمز خود را متفاوت از نام کاربری خود انتخاب کنید.", - "blankedPassword": "شما از همین رمز عبور در سایت دیگری استفاده کرده اید و آن سایت در معرض خطر قرار گرفته است. برای اطمینان از ایمنی حساب لیچس خود، لازم است که شما یک رمز عبور جدید ایجاد کنید. ممنون از همکاری شما.", + "passwordReset": "بازنشانی گذرواژه", + "forgotPassword": "گذرواژه را فراموش کرده‌اید؟", + "error_weakPassword": "این گذرواژه بسیار رایج و آسان‌حدس است.", + "error_namePassword": "خواهشانه از نام کاربری‌تان برای گذرواژه‌تان استفاده نکنید.", + "blankedPassword": "شما از گذرواژهٔ یکسانی در وبگاه دیگری بهراییده‌اید و آن وبگاه به خطر افتاده است. برای اطمینان از ایمنی حساب Lichessتان، به شما نیاز داریم تا گذرواژهٔ نویی را تعیین کنید. از درک‌تان سپاسگزاریم.", "youAreLeavingLichess": "در حال ترک lichess هستید", - "neverTypeYourPassword": "هرگز رمز خود را در سایت دیگر وارد نکنید!", + "neverTypeYourPassword": "هرگز گذرواژهٔ Lichessتان را در وبگاه دیگری ننویسید!", "proceedToX": "بروید به {param}", "passwordSuggestion": "از رمز عبور پیشنهاد شده از شخص دیگر استفاده نکنید. در این صورت احتمال سرقت حساب شما وجود دارد.", "emailSuggestion": "از ایمیلی که از شخص دیگر به شما پیشنهاد داده است استفاده نکنید. در این صورت احتمال سرقت حساب شما وجود دارد.", @@ -661,8 +686,8 @@ "yourOpponentOffersADraw": "حریف شما پیشنهاد تساوی می دهد", "accept": "پذیرفتن", "decline": "رد کردن", - "playingRightNow": "بازی در حال انجام", - "eventInProgress": "بازی در حال انجام", + "playingRightNow": "هم‌اکنون بازی می‌کنند", + "eventInProgress": "اکنون بازی می‌کنند", "finished": "تمام شده", "abortGame": "انصراف از بازی", "gameAborted": "بازی لغو شد", @@ -698,19 +723,19 @@ "ratingRange": "محدوده درجه‌بندی", "thisAccountViolatedTos": "این حساب قوانین را نقض کرده است", "openingExplorerAndTablebase": "پویشگر گشایش و آخربازی", - "takeback": "پس گرفتن حرکت", - "proposeATakeback": "پیشنهاد پس گرفتن حرکت", - "takebackPropositionSent": "پیشنهاد پس گرفتن حرکت فرستاده شد", - "takebackPropositionDeclined": "پیشنهاد پس گرفتن حرکت رد شد", - "takebackPropositionAccepted": "پیشنهاد پس گرفتن حرکت پذیرفته شد", - "takebackPropositionCanceled": "پیشنهاد پس گرفتن حرکت لغو شد", - "yourOpponentProposesATakeback": "حریف پیشنهاد پس گرفتن حرکت می دهد", + "takeback": "برگردان", + "proposeATakeback": "پیشنهاد برگردان", + "takebackPropositionSent": "برگردان فرستاده شد", + "takebackPropositionDeclined": "برگردان رد شد", + "takebackPropositionAccepted": "برگردان پذیرفته شد", + "takebackPropositionCanceled": "برگردان لغو شد", + "yourOpponentProposesATakeback": "حریف‌تان پیشنهاد «برگرداندن» می‌دهد", "bookmarkThisGame": "نشانک‌گذاری", "tournament": "مسابقه", "tournaments": "مسابقات", "tournamentPoints": "مجموع امتیازات مسابقات", "viewTournament": "دیدن مسابقات", - "backToTournament": "برگشت به مسابقه", + "backToTournament": "بازگشت به مسابقات", "noDrawBeforeSwissLimit": "شما نمی‌توانید در مسابقات سوییس تا قبل از حرکت ۳۰ام بازی را مساوی کنید.", "thematic": "موضوعی", "yourPerfRatingIsProvisional": "درجه‌بندی {param} شما موقتی است", @@ -725,8 +750,8 @@ "siteDescription": "کارساز برخط و رایگان شطرنج. با میانایی روان، شطرنج بازی کنید. بدون نام‌نویسی، بدون تبلیغ، بدون نیاز به افزونه. با رایانه، دوستان یا حریفان تصادفی شطرنج بازی کنید.", "xJoinedTeamY": "{param1} به تیم {param2} پیوست", "xCreatedTeamY": "{param1} تیم {param2} را ایجاد کرد", - "startedStreaming": "پخش را آغازید", - "xStartedStreaming": "{param} پخش را آغازید", + "startedStreaming": "جریان‌سازی را آغازید", + "xStartedStreaming": "{param} جریان‌سازی را آغازید", "averageElo": "میانگین درجه‌بندی", "location": "محل", "filterGames": "پالابش بازی‌ها", @@ -736,7 +761,7 @@ "leaderboard": "جدول رده‌بندی", "screenshotCurrentPosition": "نماگرفت از وضعیت فعلی", "gameAsGIF": "بارگیری GIF بازی", - "pasteTheFenStringHere": "پوزیشن دلخواه(FEN) را در این قسمت وارد کنید", + "pasteTheFenStringHere": "رشته FEN را در این قسمت قرار دهید", "pasteThePgnStringHere": "متن PGN را در این قسمت وارد کنید", "orUploadPgnFile": "یا یک فایل PGN بارگذاری کنید", "fromPosition": "از وضعیت", @@ -744,7 +769,7 @@ "toStudy": "مطالعه", "importGame": "بارگذاری بازی", "importGameExplanation": "برای دریافت بازپخش مرورپذیر، واکاوی رایانه‌ای، گپ‌های بازی، و وب‌نشانی همگانی همرسانی‌پذیر، PGN یک بازی را جای‌گذاری کنید.", - "importGameCaveat": "تغییرات پاک خواهند شد. برای حفظ آنها، PGN را از طریق مطالعه وارد کنید.", + "importGameCaveat": "ورتش‌ها پاک خواهند شد. برای حفظشان، PGN را از طریق مطالعه درون‌بَرید.", "importGameDataPrivacyWarning": "این PGN برای عموم در دسترس است، برای وارد کردن یک بازی خصوصی، از *مطالعه* استفاده کنید.", "thisIsAChessCaptcha": "این یک کپچا [کد امنیتی] شطرنجی است", "clickOnTheBoardToMakeYourMove": "روی صفحه بزنید تا حرکت‌تان را بروید و اثبات کنید که انسانید.", @@ -753,18 +778,17 @@ "whiteCheckmatesInOneMove": "سفید در یک حرکت مات می‌کند", "blackCheckmatesInOneMove": "سیاه در یک حرکت مات می‌کند", "retry": "تلاش دوباره", - "reconnecting": "در حال بازاتصال", + "reconnecting": "بازاتصال...", "noNetwork": "بُرون‌خط", "favoriteOpponents": "رقبای مورد علاقه", "follow": "دنبالیدن", - "following": "دنبالنده", + "following": "دنبال‌شدگان", "unfollow": "وادنبالیدن", "followX": "دنبالیدن {param}", "unfollowX": "وادنبالیدن {param}", "block": "مسدود کن", "blocked": "مسدود شده", "unblock": "لغو انسداد", - "followsYou": "شما را می‌دنبالد", "xStartedFollowingY": "{param1} دنبالیدن {param2} را آغازید", "more": "بیشتر", "memberSince": "عضویت از تاریخ", @@ -776,14 +800,14 @@ "openTournaments": "باز کردن مسابقه", "duration": "مدت", "winner": "برنده", - "standing": "رتبه بندی", - "createANewTournament": "درست کردن یک مسابقه ی جدید", - "tournamentCalendar": "برنامه ی مسابقات", + "standing": "رده‌بندی", + "createANewTournament": "ایجاد یک مسابقهٔ نو", + "tournamentCalendar": "گاهشمار مسابقات", "conditionOfEntry": "شرایط ورود:", "advancedSettings": "تنظیمات پیشرفته", "safeTournamentName": "یک نام بسیار امن برای مسابقات انتخاب کنید.", "inappropriateNameWarning": "هرچیز حتی کمی نامناسب ممکن است باعث بسته شدن حساب کاربری شما بشود.", - "emptyTournamentName": "این مکان را خالی بگذارید تا به صورت تصادفی اسم یک استاد بزرگ برای مسابقات انتخاب شود.", + "emptyTournamentName": "برای نامیدن مسابقات به نام یک شطرنج‌باز برجسته، خالی بگذارید.", "makePrivateTournament": "تورنومنت را به حالت خصوصی در بیاورید و دسترسی را محدود به داشتن پسورد کنید", "join": "ملحق شدن", "withdraw": "منصرف شدن", @@ -796,9 +820,9 @@ "standByX": "حریف {param} است،آماده باشید!", "pause": "توقف", "resume": "ادامه دادن", - "youArePlaying": "شما بازی میکنید!", + "youArePlaying": "شما بازی می‌کنید!", "winRate": "درصد برد", - "berserkRate": "میزان جنون", + "berserkRate": "میزان دیوانگی", "performance": "عملکرد", "tournamentComplete": "مسابقات به پایان رسید", "movesPlayed": "حرکات انجام شده", @@ -809,7 +833,7 @@ "nextXTournament": "مسابقه ی {param} بعدی:", "averageOpponent": "میانگین امتیاز حریف ها", "boardEditor": "مُهره‌چینی", - "setTheBoard": "مهره‌ها را بچینید", + "setTheBoard": "میز را بچینید", "popularOpenings": "گشایش‌های محبوب", "endgamePositions": "وضعیت‌های آخربازی", "chess960StartPosition": "وضعیت آغازین شطرنج۹۶۰: {param}", @@ -882,15 +906,15 @@ "typePrivateNotesHere": "یادداشت‌های خصوصی را اینجا بنویسید", "writeAPrivateNoteAboutThisUser": "یک یادداشت خصوصی درباره این کاربر بنویسید", "noNoteYet": "تاکنون، بدون یادداشت", - "invalidUsernameOrPassword": "نام کاربری یا رمز عبور نادرست است", - "incorrectPassword": "گذرواژه‌ی نادرست", - "invalidAuthenticationCode": "کد اصالت سنجی نامعتبر", + "invalidUsernameOrPassword": "نام کاربری یا گذرواژهٔ نامعتبر", + "incorrectPassword": "گذرواژهٔ نادرست", + "invalidAuthenticationCode": "کد راستین‌آزمایی نامعتبر", "emailMeALink": "یک لینک به من ایمیل کنید", - "currentPassword": "رمز جاری", - "newPassword": "رمز جدید", - "newPasswordAgain": "(رمز جدید(برای دومین بار", - "newPasswordsDontMatch": "کلمه‌های عبور وارد شده مطابقت ندارند", - "newPasswordStrength": "استحکام کلمه عبور", + "currentPassword": "گذرواژهٔ جاری", + "newPassword": "گذرواژهٔ نو", + "newPasswordAgain": "گذرواژهٔ نو (دوباره)", + "newPasswordsDontMatch": "گذرواژه‌های نو هم‌جور نیستند", + "newPasswordStrength": "نیرومندی گذرواژه", "clockInitialTime": "مقدار زمان اولیه", "clockIncrement": "مقدار زمان اضافی به ازای هر حرکت", "privacy": "حریم شخصی", @@ -926,8 +950,8 @@ "clock": "ساعت", "opponent": "حریف", "learnMenu": "یادگیری", - "studyMenu": "مطالعه‌ها", - "practice": "تمرین کردن", + "studyMenu": "مطالعه", + "practice": "تمرین", "community": "همدارگان", "tools": "ابزارها", "increment": "افزایش زمان", @@ -938,7 +962,7 @@ "error_email_unique": "آدرس ایمیل نامعتبر است یا قبلا در سیستم ثبت شده است", "error_email_different": "اکنون، این نشانی رایانامه‌تان شما است", "error_minLength": "باید حداقل {param} حرف داشته باشد", - "error_maxLength": "باید حداقل دارای {param} حرف باشد", + "error_maxLength": "باید حداکثر {param} نویسه داشته باشد", "error_min": "باید حداقل {param} باشد", "error_max": "باید حداکثر {param} باشد", "ifRatingIsPlusMinusX": "اگر درجه‌بندی‌شان {param}± است", @@ -951,10 +975,10 @@ "blackCastlingKingside": "O-O سیاه", "tpTimeSpentPlaying": "زمان بازی کردن: {param}", "watchGames": "تماشای بازی‌ها", - "tpTimeSpentOnTV": "مدت گذرانده در تلویزیون: {param}", + "tpTimeSpentOnTV": "مدت آرنگیده در تلویزیون: {param}", "watch": "تماشا", - "videoLibrary": "فیلم ها", - "streamersMenu": "بَرخَط-محتواسازها", + "videoLibrary": "فیلم‌ها", + "streamersMenu": "بَرخَط-محتواسازان", "mobileApp": "گوشی‌افزار", "webmasters": "وبداران", "about": "درباره ما", @@ -978,26 +1002,26 @@ "aboutSimulImage": "از ۵۰ بازی فیشر موفق به کسب ۴۷ برد و ۲ تساوی و یک باخت شد.", "aboutSimulRealLife": "این مفهوم از رویدادهای واقعی الهام گرفته شده است. در آن جا میزبان میز به میز برای انجام حرکت خود، حرکت می کند.", "aboutSimulRules": "وقتی نمایش همزمان شروع شود، هر بازیکن یک بازی را با میزبان که با مهره سفید بازی میکند آغاز میکند. نمایش وقتی تمام می شود که تمام بازی ها تمام شده باشند.", - "aboutSimulSettings": "نمایش های همزمان همیشه غیر رسمی هستند. بازی دوباره، پس گرفتن حرکت و اضافه کردن زمان غیرفعال شده اند.", + "aboutSimulSettings": "نمایشگاه همزمان همیشه نارسمی است. بازرویارویی، برگرداندن و زمان افزاینده نافعال شده‌اند.", "create": "ساختن", "whenCreateSimul": "وقتی یک نمایش همزمان ایجاد میکنید باید با چند نفر همزمان بازی کنید.", - "simulVariantsHint": "اگر چندین گونه را انتخاب کنید، هر بازیکن می‌تواند انتخاب کند که کدام یک را بازی کند.", + "simulVariantsHint": "اگر چندین وَرتا را برگزینید، هر بازیکن می‌تواند انتخاب کند که کدام‌یک را بازی کند.", "simulClockHint": "تنظیم ساعت فیشر. هرچه از بازیکنان بیشتری برنده شوید، زمان بیشتری نیاز دارید", "simulAddExtraTime": "برای کمک به شما میتوانید برای خود زمان اضافی در نظر بگیرید.", "simulHostExtraTime": "زمان اضافی میزبان", "simulAddExtraTimePerPlayer": "به ازای پیوستن هر بازیکن، به زمان اولیه خود اضافه کنید.", "simulHostExtraTimePerPlayer": "زمان اضافه میزبان به ازای بازیکن", - "lichessTournaments": "مسابقات لی چس", - "tournamentFAQ": "سوالات متداول مسابقات", + "lichessTournaments": "مسابقات Lichess", + "tournamentFAQ": "پرسش‌های پربسامد مسابقات راوان", "timeBeforeTournamentStarts": "زمان باقی مانده به شروع مسابقه", "averageCentipawnLoss": "میانگین سرباز از دست داده", "accuracy": "دقت", - "keyboardShortcuts": "میانبر های صفحه کلید", + "keyboardShortcuts": "میانبرهای صفحه‌کلید", "keyMoveBackwardOrForward": "حرکت به عقب/جلو", "keyGoToStartOrEnd": "رفتن به آغاز/پایان", - "keyCycleSelectedVariation": "چرخه شاخه اصلی انتخاب‌شده", + "keyCycleSelectedVariation": "چرخاندن وَرتِش گزیده", "keyShowOrHideComments": "نمایش/پنهان کردن نظرها", - "keyEnterOrExitVariation": "ورود / خروج به شاخه", + "keyEnterOrExitVariation": "ورود/خروج به وَرتِش", "keyRequestComputerAnalysis": "درخواست تحلیل رایانه‌ای، از اشتباه‌های‌تان بیاموزید", "keyNextLearnFromYourMistakes": "بعدی (از اشتباه‌های‌تان بیاموزید)", "keyNextBlunder": "اشتباه فاحش بعدی", @@ -1005,15 +1029,15 @@ "keyNextInaccuracy": "بی‌دقتی بعدی", "keyPreviousBranch": "شاخه پیشین", "keyNextBranch": "شاخه بعدی", - "toggleVariationArrows": "کلید پیکان‌های شاخه اصلی", - "cyclePreviousOrNextVariation": "چرخه پیشین/پسین شاخه اصلی", + "toggleVariationArrows": "کلید پیکان‌های وَرتِش", + "cyclePreviousOrNextVariation": "چرخاندن پیشین/پسین وَرتِش", "toggleGlyphAnnotations": "کلید علائم حرکت‌نویسی", "togglePositionAnnotations": "تغییر حرکت‌نویسی وضعیت", - "variationArrowsInfo": "پیکان های شاخه اصلی به شما امکان می‌دهد بدون استفاده از فهرست حرکت، پیمایش کنید.", + "variationArrowsInfo": "پیکان های وَرتِش به شما امکان ناوِش بدون استفاده از فهرستِ حرکت را می‌دهد.", "playSelectedMove": "حرکت انتخاب شده را بازی کن", "newTournament": "مسابقه جدید", - "tournamentHomeTitle": "مسابقات شطرنج با گونه‌ها و زمان‌بندی‌های مختلف", - "tournamentHomeDescription": "هرچه زودتر شطرنج بازی کنید! به یک مسابقه رسمی برنامه‌ریزی‌شده بپیوندید یا مسابقات خودتان را بسازید. شطرنج گلوله‌ای، برق‌آسا، مرسوم، ۹۶۰، پادشاه تپه‌ها، سه‌کیش و دیگر گزینه‌ها، برای لذت بی‌پایان از شطرنج در دسترسند.", + "tournamentHomeTitle": "مسابقات شطرنج با وَرتاها و زمان‌بندی‌های گوناگون", + "tournamentHomeDescription": "هرچه زودتر شطرنج بازی کنید! به یک مسابقه رسمی برنامه‌ریزی‌شده بپیوندید یا مسابقات خودتان را بسازید. شطرنج گلوله‌ای، برق‌آسا، فکری، ۹۶۰، پادشاه تپه‌ها، سه‌کیش و دیگر گزینه‌ها، برای لذت بی‌پایان از شطرنج در دسترسند.", "tournamentNotFound": "مسابقات یافت نشد", "tournamentDoesNotExist": "این مسابقات وجود ندارد", "tournamentMayHaveBeenCanceled": "ممکن است مسابقه لغو شده باشد,شاید همه ی بازیکنان مسابقه را قبل از شروع ترک کرده باشند", @@ -1022,7 +1046,7 @@ "yourPerfTypeRatingIsRating": "درجه‌بندی {param1} شما {param2} است.", "youAreBetterThanPercentOfPerfTypePlayers": "شما بهتر از {param1} بازیکن ها در {param2} هستید.", "userIsBetterThanPercentOfPerfTypePlayers": "{param1} بهتر از {param2} بازیکنان {param3} است.", - "betterThanPercentPlayers": "بهتر از {param1} بازیکنان در {param2}", + "betterThanPercentPlayers": "بهتر از {param1} بازیکنان {param2}", "youDoNotHaveAnEstablishedPerfTypeRating": "شما درجه‌بندی {param} تثبیت‌شده‌ای ندارید.", "yourRating": "درجه‌بندی شما", "cumulative": "تجمعی", @@ -1030,33 +1054,33 @@ "checkYourEmail": "به رایانامه‌تان سر زنید", "weHaveSentYouAnEmailClickTheLink": "ما به شما ایمیل فرستادیم. روی لینکی که در ایمیل است کلیک کنید", "ifYouDoNotSeeTheEmailCheckOtherPlaces": "اگر رایانامه را نمی‌بینید، مکان‌های دیگری مانند پوشه‌های ناخواسته، هرزنامه، اجتماعی یا سایر موردها را بررسی کنید.", - "weHaveSentYouAnEmailTo": "ایمیل ارسال شد.بر روی لینک داخل ایمیل کلیک کنید تا پسورد شما ریست شود {param} به آدرس", + "weHaveSentYouAnEmailTo": "ما یک رایانامه به {param} فرستاده‌ایم. برای بازنشانی گذرواژه‌تان، روی پیوند موجود در رایانامه بزنید.", "byRegisteringYouAgreeToBeBoundByOur": "با ثبت‌نام، با {param} موافقت می‌کنید.", "readAboutOur": "درباره {param} ما بخوانید.", - "networkLagBetweenYouAndLichess": "تاخیر شبکه بین شما و Lichess", + "networkLagBetweenYouAndLichess": "تاخیر شبکه میان شما و Lichess", "timeToProcessAMoveOnLichessServer": "زمان سپری شده برای پردازش یک حرکت", "downloadAnnotated": "بارگیری حرکت‌نویسی", "downloadRaw": "بارگیری خام", "downloadImported": "بارگیری درونبُرد", "crosstable": "رودررو", "youCanAlsoScrollOverTheBoardToMoveInTheGame": "برای حرکت، روی صفحه بازی بِنَوَردید.", - "scrollOverComputerVariationsToPreviewThem": "برای پیش‌نمایش آن‌ها، روی شاخه‌های رایانه‌ای بِنَوَردید.", - "analysisShapesHowTo": "و کلیک کنید یا راست کلیک کنید تا دایره یا فلش در صفحه بکشید shift", + "scrollOverComputerVariationsToPreviewThem": "برای پیش‌نمایش آن‌ها، روی وَرتِش‌های رایانه‌ای بِغَرالید.", + "analysisShapesHowTo": "برای رسم دایره و پیکان روی تخته، shift+click یا راست-تِلیک را بفشارید.", "letOtherPlayersMessageYou": "ارسال پیام توسط بقیه به شما", - "receiveForumNotifications": "دریافت اعلان در هنگام ذکر شدن در انجمن", + "receiveForumNotifications": "دریافت اعلان هنگام نام‌بَری در انجمن", "shareYourInsightsData": "اشتراک گذاشتن داده های شما", "withNobody": "هیچکس", "withFriends": "با دوستان", "withEverybody": "با همه", - "kidMode": "حالت کودکان", + "kidMode": "حالت کودک", "kidModeIsEnabled": "حالت کودک فعال است.", - "kidModeExplanation": "این گزینه،امنیتی است.با فعال کردن حالت ((کودکانه))،همه ی ارتباطات(چت کردن و...)غیر فعال می شوند.با فعال کردن این گزینه،کودکان خود را محافطت کنید.", - "inKidModeTheLichessLogoGetsIconX": "در حالت کودکانه،به نماد لیچس،یک {param} اضافه می شود تا شما از فعال بودن آن مطلع شوید.", + "kidModeExplanation": "این دربارهٔ ایمنی است. در حالت کودک، همهٔ ارتباط‌های وبگاه نافعال است. این را برای فرزندان و شطرنج‌آموزان مدرسه خود فعال کنید تا از آنها در برابر دیگر کاربران اینترنت حفاظت کنید.", + "inKidModeTheLichessLogoGetsIconX": "در حالت کودک، نماد Lichess نقشک {param} را می‌گیرد، بنابراین می‌دانید کودکان‌تان در امانند.", "askYourChessTeacherAboutLiftingKidMode": "حسابتان مدیریت می‌شود. از آموزگار شطرنج‌تان درباره برداشتن حالت کودک بپرسید.", - "enableKidMode": "فعال کردن حالت کودکانه", - "disableKidMode": "غیر فعال کردن حالت کودکانه", + "enableKidMode": "فعال‌سازی حالت کودک", + "disableKidMode": "ازکاراندازی حالت کودک", "security": "امنیت", - "sessions": "جلسات", + "sessions": "جلسه", "revokeAllSessions": "باطل کردن تمامی موارد", "playChessEverywhere": "همه جا شطرنج بازی کنید", "asFreeAsLichess": "کاملا رایگان", @@ -1065,7 +1089,7 @@ "zeroAdvertisement": "بدون تبلیغات", "fullFeatured": "با تمامی امکانات", "phoneAndTablet": "گوشی و رایانک", - "bulletBlitzClassical": "گلوله‌ای، برق‌آسا، مرسوم", + "bulletBlitzClassical": "گلوله‌ای، برق‌آسا، فکری", "correspondenceChess": "شطرنج مکاتبه ای", "onlineAndOfflinePlay": "بازی بَرخط و بُرون‌خط", "viewTheSolution": "دیدن راه‌حل", @@ -1083,7 +1107,7 @@ "light": "روشن", "dark": "تیره", "transparent": "شفاف", - "deviceTheme": "طرح زمینه دستگاه", + "deviceTheme": "پوستهٔ اَفزاره", "backgroundImageUrl": "وب‌نشانی تصویر پس‌زمینه:", "board": "صفحه شطرنج", "size": "اندازه", @@ -1097,28 +1121,28 @@ "usernamePrefixInvalid": "نام کاربری باید با حرف شروع شود.", "usernameSuffixInvalid": "نام کاربری باید با حرف یا شماره خاتمه یابد.", "usernameCharsInvalid": "نام کاربری فقط می تواند شامل حروف،اعداد،خط فاصله یا زیر خط(under line) باشد.", - "usernameUnacceptable": "این نام کاربری قابل قبول نیست.", + "usernameUnacceptable": "این نام کاربری پذیرفتنی نیست.", "playChessInStyle": "شطرنج‌بازیِ نوگارانه", "chessBasics": "پایه‌های شطرنج", - "coaches": "مربی ها", - "invalidPgn": "فایل PGN نامعتبر است", + "coaches": "مربیان", + "invalidPgn": "PGN ِ نامعتبر", "invalidFen": "وضعیت نامعتبر", "custom": "دلخواه", - "notifications": "گزارش", - "notificationsX": "هشدار: {param1}", + "notifications": "اعلان", + "notificationsX": "اعلان: {param1}", "perfRatingX": "درجه‌بندی: {param}", "practiceWithComputer": "تمرین با رایانه", "anotherWasX": "حرکت مناسب دیگر {param} بود", "bestWasX": "بهترین حرکت {param} بود", - "youBrowsedAway": "پوزیشن را به هم زدید!", - "resumePractice": "ادامه تمرین", + "youBrowsedAway": "دور شُدید", + "resumePractice": "از سرگیری تمرین", "drawByFiftyMoves": "بازی با قانون پنجاه حرکت مساوی شده است.", "theGameIsADraw": "بازی مساوی است.", - "computerThinking": "محاسبه رایانه‌ای ...", + "computerThinking": "محاسبهٔ رایانه‌ای...", "seeBestMove": "دیدن بهترین حرکت", "hideBestMove": "پنهان کردن بهترین حرکت", "getAHint": "راهنمایی", - "evaluatingYourMove": "در حال بررسی حرکت شما...", + "evaluatingYourMove": "حرکت‌تان را می‌ارزیابد...", "whiteWinsGame": "سفید می‌برد", "blackWinsGame": "سیاه می‌برد", "learnFromYourMistakes": "از اشتباه‌های‌تان بیاموزید", @@ -1146,8 +1170,8 @@ "middlegame": "وسط بازی", "endgame": "آخربازی", "conditionalPremoves": "پیش‌حرکت‌های شرطی", - "addCurrentVariation": "اضافه کردن این نوع حرکات", - "playVariationToCreateConditionalPremoves": "یک نوع حرکات را بازی کنید تا پیش حرکت های شرطی را بسازید", + "addCurrentVariation": "افزودن وَرتِش جاری", + "playVariationToCreateConditionalPremoves": "بازی کردن یک وَرتِش، برای ایجاد پیش‌حرکت‌های شرطی", "noConditionalPremoves": "بدون پیش‌حرکت‌های شرطی", "playX": "{param} را انجام دهید", "showUnreadLichessMessage": "شما یک پیام خصوصی از Lichess دریافت کرده‌اید.", @@ -1176,17 +1200,17 @@ "bullet": "گلوله‌ای", "blitz": "برق‌آسا", "rapid": "سریع", - "classical": "کلاسیک", + "classical": "فکری", "ultraBulletDesc": "بازی‌های سرعتی دیوانه‌وار: کمتر از ۳۰ ثانیه", "bulletDesc": "بازی‌های خیلی سرعتی: کمتر از ۳ دقیقه", "blitzDesc": "بازی های سرعتی: ۳ تا ۸ دقیقه", "rapidDesc": "بازی های سریع: ۸ تا ۲۵ دقیقه", - "classicalDesc": "بازی های کلاسیک : 25 دقیقه یا بیشتر", + "classicalDesc": "بازی های فکری: ۲۵ دقیقه یا بیشتر", "correspondenceDesc": "بازی های مکاتبه ای : یک یا چند روز برای هر حرکت", "puzzleDesc": "تمرین تاکتیک های شطرنج", "important": "مهم!", "yourQuestionMayHaveBeenAnswered": "سوال شما ممکن است که از قبل پاسخی داشته باشد {param1}", - "inTheFAQ": "در سوالات متداول باشد.", + "inTheFAQ": "در پرسش‌های پُربسامد", "toReportSomeoneForCheatingOrBadBehavior": "برای گزارش دادن یک کاربر به علت تقلب یا بدرفتاری، {param1}", "useTheReportForm": "از فرم گزارش استفاده کنید.", "toRequestSupport": "جهت درخواست پشتیبانی، {param1}", @@ -1199,23 +1223,23 @@ "youCannotPostYetPlaySomeGames": "هنوز نمی‌توانید در انجمن‌ها فرسته گذارید. چند بازی کنید!", "subscribe": "مشترک شدن", "unsubscribe": "لغو اشتراک", - "mentionedYouInX": "از شما در {param1} نام برده شد.", + "mentionedYouInX": "در «{param1}» از شما نام‌برده شد.", "xMentionedYouInY": "{param1} از شما در \"{param2}\" نام برد.", "invitedYouToX": "به «{param1}» دعوت شده‌اید.", "xInvitedYouToY": "{param1} شما را به «{param2}» دعوت کرده است.", "youAreNowPartOfTeam": "شما در حال حاضر عضوی از تیم هستید.", - "youHaveJoinedTeamX": "شما به \"{param1}\" پیوستید.", + "youHaveJoinedTeamX": "شما به «{param1}» پیوسته‌اید.", "someoneYouReportedWasBanned": "شخصی که گزارش کردید مسدود شد", "congratsYouWon": "شادباش، شما بُردید!", "gameVsX": "بازی در برابر {param1}", "resVsX": "{param1} در برابر {param2}", "lostAgainstTOSViolator": "شما برابر کسی که قانون‌های Lichess را نقض کرده، امتیاز درجه‌بندی از دست دادید", "refundXpointsTimeControlY": "پس‌دادن: {param1} امتیاز به درجه‌بندی {param2}.", - "timeAlmostUp": "زمان تقریباً تمام شده است!", - "clickToRevealEmailAddress": "[برای آشکارسازی نشانی رایانامه بتلیکید]", + "timeAlmostUp": "زمان نزدیک به پایان است!", + "clickToRevealEmailAddress": "[برای آشکارسازی نشانیِ رایانامه بتِلیکید]", "download": "بارگیری", "coachManager": "تنظیمات مربی", - "streamerManager": "مدیریت پخش", + "streamerManager": "مدیریت جریان‌سازی", "cancelTournament": "لغو مسابقه", "tournDescription": "توضیحات مسابقه", "tournDescriptionHelp": "نکته خاصی را می‌خواهید به شرکت‌کنندگان گویید؟ بکوشید کوتاه باشد. پیوندهای فرونشان موجودند:\n[name](https://url)", @@ -1225,11 +1249,11 @@ "minimumRatedGames": "حداقل بازی های ریتد", "minimumRating": "حداقل درجه‌بندی", "maximumWeeklyRating": "حداکثر درجه‌بندی هفتگی", - "positionInputHelp": "برای آغاز هر بازی از یک وضعیت مشخص، یک FEN معتبر جای‌گذارید.\nتنها برای شطرنج معیار کار می‌کند، نه با شطرنج‌گونه‌ها.\nمی‌توانید از {param} برای آزانیدن وضعیت FEN استفاده کنید، سپس آن را اینجا جای‌گذارید.\nبرای آغاز بازی از وضعیت نخستین معمولی، خالی بگذارید.", + "positionInputHelp": "برای آغاز هر بازی از یک وضعیت مشخص، یک FEN معتبر جای‌گذارید.\nتنها برای شطرنج معیار کار می‌کند، نه با وَرتاها.\nمی‌توانید از {param} برای آزانیدن وضعیت FEN بهرایید، سپس آن را اینجا جای‌گذارید.\nبرای آغاز بازی از وضعیت نخستین معمولی، خالی بگذارید.", "cancelSimul": "بازی هم‌زمان (سیمولتانه) را لغو نمایید", "simulHostcolor": "رنگ مربوط به نمایش‌دهنده یا میزبان برای هر بازی", "estimatedStart": "زمان تقریبی شروع بازی", - "simulFeatured": "نمایش در {param}", + "simulFeatured": "آرنگیدن در {param}", "simulFeaturedHelp": "بازی هم‌زمان خود را برای همه بر روی لینک {param} نشان بدهید. برای دسترسی خصوصی آن را غیرفعال نمایید.", "simulDescription": "توصیف بازی هم‌زمان", "simulDescriptionHelp": "آیا می‌خواهید مطلبی را به شرکت‌کنندگان بگویید؟", @@ -1237,7 +1261,7 @@ "embedsAvailable": "وب‌نشانی بازی یا وب‌نشانی بخشی از مطالعه را، برای جاسازی آن، جایگذاری کنید.", "inYourLocalTimezone": "ذر منطقه زمانی شما", "tournChat": "چت مسابقه", - "noChat": "بدون چت", + "noChat": "بدون گپ", "onlyTeamLeaders": "تنها مسئولان تیم", "onlyTeamMembers": "تنها اعضای تیم", "navigateMoveTree": "ناویدن فهرست حرکت‌ها", @@ -1278,7 +1302,7 @@ "nbInaccuracies": "{count, plural, =1{{count} نادقیق} other{{count} نادقیق}}", "nbPlayers": "{count, plural, =1{{count} بازیکن} other{{count} بازیکن}}", "nbGames": "{count, plural, =1{{count} بازی} other{{count} بازی}}", - "ratingXOverYGames": "{count, plural, =1{درجه‌بندی {count} در {param2} بازی} other{{count} ریتینگ در {param2} بازی}}", + "ratingXOverYGames": "{count, plural, =1{درجه‌بندی {count} در {param2} بازی} other{درجه‌بندی {count} در {param2} بازی}}", "nbBookmarks": "{count, plural, =1{{count} نشانک} other{{count} نشانک}}", "nbDays": "{count, plural, =1{{count} روز} other{{count} روز}}", "nbHours": "{count, plural, =1{{count} ساعت} other{{count} ساعت}}", @@ -1290,25 +1314,25 @@ "nbWins": "{count, plural, =1{{count} برد} other{{count} برد}}", "nbLosses": "{count, plural, =1{{count} باخت} other{{count} باخت}}", "nbDraws": "{count, plural, =1{{count} مساوی} other{{count} مساوی}}", - "nbPlaying": "{count, plural, =1{{count} بازی در حال انجام} other{{count} بازی در حال انجام}}", + "nbPlaying": "{count, plural, =1{{count} بازیِ اکنونی} other{{count} بازیِ اکنونی}}", "giveNbSeconds": "{count, plural, =1{{count} ثانیه اضافه کن} other{{count} ثانیه اضافه کن}}", "nbTournamentPoints": "{count, plural, =1{مجموع امتیازات مسابقات:{count}} other{مجموع امتیازات مسابقات:{count}}}", "nbStudies": "{count, plural, =1{{count} مطالعه} other{{count} مطالعه}}", "nbSimuls": "{count, plural, =1{{count} سیمولتانه} other{{count} سیمولتانه}}", "moreThanNbRatedGames": "{count, plural, =1{بیشتر از {count} بازی رسمی} other{بیشتر از {count} بازی رسمی}}", "moreThanNbPerfRatedGames": "{count, plural, =1{بیشتر از {count} بازی رسمی {param2}} other{بیشتر از {count} بازی رسمی {param2}}}", - "needNbMorePerfGames": "{count, plural, =1{شما باید{count} بازی رسمی{param2} انجام دهید.} other{شما باید{count} بازی رسمی{param2} انجام دهید.}}", + "needNbMorePerfGames": "{count, plural, =1{شما باید {count} بازی رسمی {param2} دیگر کنید} other{شما باید {count} بازی رسمی {param2} دیگر کنید}}", "needNbMoreGames": "{count, plural, =1{شما باید{count} بازی رسمی دیگر انجام دهید.} other{شما باید{count} بازی رسمی دیگر انجام دهید.}}", "nbImportedGames": "{count, plural, =1{{count} بارگذاری شده} other{{count} بارگذاری شده}}", "nbFriendsOnline": "{count, plural, =1{{count} دوست بَرخط} other{{count} دوست بَرخط}}", "nbFollowers": "{count, plural, =1{{count} دنبال‌گر} other{{count} دنبال‌گر}}", - "nbFollowing": "{count, plural, =1{{count} دنبالنده} other{{count} دنبالنده}}", + "nbFollowing": "{count, plural, =1{{count} دنبالیده} other{{count} دنبال‌شده}}", "lessThanNbMinutes": "{count, plural, =1{کمتر از {count} دقیقه} other{کمتر از {count} دقیقه}}", "nbGamesInPlay": "{count, plural, =1{{count} بازی در حال انجام است} other{{count} بازی در حال انجام است}}", "maximumNbCharacters": "{count, plural, =1{حداکثر: {count} حرف} other{حداکثر: {count} حرف}}", "blocks": "{count, plural, =1{{count} مسدود شده} other{{count} مسدود شده}}", "nbForumPosts": "{count, plural, =1{{count} فرسته در انجمن} other{{count} فرسته در انجمن}}", - "nbPerfTypePlayersThisWeek": "{count, plural, =1{{count} بازیکن {param2} این هفته فعالیت داشته‌ است.} other{{count} بازیکن {param2} این هفته فعالیت داشته‌اند.}}", + "nbPerfTypePlayersThisWeek": "{count, plural, =1{این هفته، {count} بازیکن {param2}.} other{این هفته، {count} بازیکن {param2}.}}", "availableInNbLanguages": "{count, plural, =1{در {count} زبان موجود است!} other{در {count} زبان موجود است!}}", "nbSecondsToPlayTheFirstMove": "{count, plural, =1{{count} ثانیه برای شروع اولین حرکت} other{{count} ثانیه برای شروع اولین حرکت}}", "nbSeconds": "{count, plural, =1{{count} ثانیه} other{{count} ثانیه}}", @@ -1339,7 +1363,7 @@ "stormBestRunOfDay": "بهترین دور روز", "stormRuns": "دورها", "stormGetReady": "آماده شوید!", - "stormWaitingForMorePlayers": "در حال انتظار برای پیوستن بازیکنان بیشتر...", + "stormWaitingForMorePlayers": "در انتظارِ پیوستن بازیکنان بیشتر...", "stormRaceComplete": "مسابقه تمام شد!", "stormSpectating": "در حال تماشا", "stormJoinTheRace": "به مسابقه بپیوندید!", @@ -1439,7 +1463,7 @@ "studyPinnedChapterComment": "یادداشت سنجاقیده‌ٔ بخش", "studySaveChapter": "ذخیره بخش", "studyClearAnnotations": "پاک کردن حرکت‌نویسی", - "studyClearVariations": "پاک کردن تغییرات", + "studyClearVariations": "پاکیدن وَرتِش‌ها", "studyDeleteChapter": "حذف بخش", "studyDeleteThisChapter": "حذف این بخش. بازگشت وجود ندارد!", "studyClearAllCommentsInThisChapter": "همه دیدگاه‌ها، نمادها و شکل‌های ترسیم شده در این بخش، پاک شوند", @@ -1518,6 +1542,7 @@ "studyPlayAgain": "دوباره بازی کنید", "studyWhatWouldYouPlay": "در این وضعیت چطور بازی می‌کنید؟", "studyYouCompletedThisLesson": "تبریک! شما این درس را کامل کردید.", + "studyPerPage": "{param} میز", "studyNbChapters": "{count, plural, =1{{count} بخش} other{{count} بخش}}", "studyNbGames": "{count, plural, =1{{count} بازی} other{{count} بازی}}", "studyNbMembers": "{count, plural, =1{{count} عضو} other{{count} عضو}}", diff --git a/lib/l10n/lila_fi.arb b/lib/l10n/lila_fi.arb index ad423bdeab..4faba1b965 100644 --- a/lib/l10n/lila_fi.arb +++ b/lib/l10n/lila_fi.arb @@ -1,46 +1,48 @@ { + "mobileAllGames": "Kaikki pelit", + "mobileAreYouSure": "Oletko varma?", + "mobileBlindfoldMode": "Sokko", + "mobileCancelTakebackOffer": "Peruuta siirron peruutuspyyntö", + "mobileClearButton": "Tyhjennä", + "mobileCorrespondenceClearSavedMove": "Poista tallennettu siirto", + "mobileCustomGameJoinAGame": "Liity peliin", + "mobileFeedbackButton": "Palaute", + "mobileGreeting": "Hei {param}", + "mobileGreetingWithoutName": "Hei", + "mobileHideVariation": "Piilota muunnelma", "mobileHomeTab": "Etusivu", - "mobilePuzzlesTab": "Tehtävät", - "mobileToolsTab": "Työkalut", - "mobileWatchTab": "Seuraa", - "mobileSettingsTab": "Asetukset", + "mobileLiveStreamers": "Live-striimaajat", "mobileMustBeLoggedIn": "Sinun täytyy olla kirjautuneena nähdäksesi tämän sivun.", - "mobileSystemColors": "Järjestelmän värit", - "mobileFeedbackButton": "Palaute", + "mobileNoSearchResults": "Ei hakutuloksia", + "mobileNotFollowingAnyUser": "Et seuraa yhtäkään käyttäjää.", "mobileOkButton": "OK", + "mobilePlayersMatchingSearchTerm": "Pelaajat, joiden tunnuksesta löytyy \"{param}\"", + "mobilePrefMagnifyDraggedPiece": "Suurenna vedettävä nappula", + "mobilePuzzleStormConfirmEndRun": "Haluatko lopettaa tämän sarjan?", + "mobilePuzzleStormFilterNothingToShow": "Ei näytettävää, muuta suodatusehtoja", + "mobilePuzzleStormNothingToShow": "Ei näytettävää. Pelaa ensin muutama sarja Puzzle Stormia.", + "mobilePuzzleStormSubtitle": "Ratkaise mahdollisimman monta tehtävää 3 minuutissa.", + "mobilePuzzleStreakAbortWarning": "Parhaillaan menossa oleva putkesi päättyy, ja pistemääräsi tallennetaan.", + "mobilePuzzleThemesSubtitle": "Tee tehtäviä suosikkiavauksistasi tai valitse tehtäväteema.", + "mobilePuzzlesTab": "Tehtävät", + "mobileRecentSearches": "Viimeisimmät haut", "mobileSettingsHapticFeedback": "Kosketuspalaute", "mobileSettingsImmersiveMode": "Kokoruututila", "mobileSettingsImmersiveModeSubtitle": "Piilota laitteen käyttöliittymä pelatessasi. Valitse tämä, jos laitteesi navigointieleet näytön laidoilla ovat sinulle häiriöksi. Asetus vaikuttaa peli- ja Puzzle Storm -näkymiin.", - "mobileNotFollowingAnyUser": "Et seuraa yhtäkään käyttäjää.", - "mobileAllGames": "Kaikki pelit", - "mobileRecentSearches": "Viimeisimmät haut", - "mobileClearButton": "Tyhjennä", - "mobilePlayersMatchingSearchTerm": "Pelaajat, joiden tunnuksesta löytyy \"{param}\"", - "mobileNoSearchResults": "Ei hakutuloksia", - "mobileAreYouSure": "Oletko varma?", - "mobilePuzzleStreakAbortWarning": "Parhaillaan menossa oleva putkesi päättyy, ja pistemääräsi tallennetaan.", - "mobilePuzzleStormNothingToShow": "Ei näytettävää. Pelaa ensin muutama sarja Puzzle Stormia.", - "mobileSharePuzzle": "Jaa tämä tehtävä", - "mobileShareGameURL": "Jaa pelin URL", + "mobileSettingsTab": "Asetukset", "mobileShareGamePGN": "Jaa PGN", + "mobileShareGameURL": "Jaa pelin URL", "mobileSharePositionAsFEN": "Jaa asema FEN:nä", - "mobileShowVariations": "Näytä muunnelmat", - "mobileHideVariation": "Piilota muunnelma", + "mobileSharePuzzle": "Jaa tämä tehtävä", "mobileShowComments": "Näytä kommentit", - "mobilePuzzleStormConfirmEndRun": "Haluatko lopettaa tämän sarjan?", - "mobilePuzzleStormFilterNothingToShow": "Ei näytettävää, muuta suodatusehtoja", - "mobileCancelTakebackOffer": "Peruuta siirron peruutuspyyntö", - "mobileWaitingForOpponentToJoin": "Odotetaan vastustajan löytymistä...", - "mobileBlindfoldMode": "Sokko", - "mobileCustomGameJoinAGame": "Liity peliin", - "mobileCorrespondenceClearSavedMove": "Poista tallennettu siirto", - "mobileSomethingWentWrong": "Jokin meni vikaan.", "mobileShowResult": "Näytä lopputulos", - "mobilePuzzleThemesSubtitle": "Tee tehtäviä suosikkiavauksistasi tai valitse tehtäväteema.", - "mobilePuzzleStormSubtitle": "Ratkaise mahdollisimman monta tehtävää 3 minuutissa.", - "mobileGreeting": "Hei {param}", - "mobileGreetingWithoutName": "Hei", - "mobilePrefMagnifyDraggedPiece": "Suurenna vedettävä nappula", + "mobileShowVariations": "Näytä muunnelmat", + "mobileSomethingWentWrong": "Jokin meni vikaan.", + "mobileSystemColors": "Järjestelmän värit", + "mobileTheme": "Teema", + "mobileToolsTab": "Työkalut", + "mobileWaitingForOpponentToJoin": "Odotetaan vastustajan löytymistä...", + "mobileWatchTab": "Seuraa", "activityActivity": "Toiminta", "activityHostedALiveStream": "Piti livestreamin", "activityRankedInSwissTournament": "Tuli {param1}. sijalle turnauksessa {param2}", @@ -115,11 +117,13 @@ "broadcastSubscribeTitle": "Tilaa ilmoitukset kunkin kierroksen alkamisesta. Käyttäjätunnuksesi asetuksista voit kytkeä ääni- ja puskuilmoitukset päälle tai pois.", "broadcastUploadImage": "Lisää turnauksen kuva", "broadcastNoBoardsYet": "Pelilautoja ei vielä ole. Ne tulevat näkyviin sitä mukaa, kun pelit ladataan tänne.", - "broadcastStartsAfter": "Alkuun on aikaa {param}", + "broadcastBoardsCanBeLoaded": "Laudat voidaan ladata lähteen kautta tai {param} kautta", + "broadcastStartsAfter": "Alkaa {param}:n jälkeen", "broadcastStartVerySoon": "Lähetys alkaa aivan pian.", "broadcastNotYetStarted": "Lähetys ei ole vielä alkanut.", "broadcastOfficialWebsite": "Virallinen verkkosivu", "broadcastStandings": "Tulostaulu", + "broadcastOfficialStandings": "Virallinen tulostaulu", "broadcastIframeHelp": "Lisäasetuksia löytyy {param}", "broadcastWebmastersPage": "webmasterin sivulta", "broadcastPgnSourceHelp": "Tämän kierroksen julkinen ja reaaliaikainen PGN-tiedosto. Nopeampaan ja tehokkaampaan synkronisointiin on tarjolla myös {param}.", @@ -128,6 +132,15 @@ "broadcastRatingDiff": "Vahvuuslukujen erotus", "broadcastGamesThisTournament": "Pelit tässä turnauksessa", "broadcastScore": "Pisteet", + "broadcastAllTeams": "Kaikki joukkueet", + "broadcastTournamentFormat": "Turnauksen laji", + "broadcastTournamentLocation": "Turnauksen sijainti", + "broadcastTopPlayers": "Parhaat pelaajat", + "broadcastTimezone": "Aikavyöhyke", + "broadcastFideRatingCategory": "Kategoria (FIDE-vahvuuslukujen mukaan)", + "broadcastOptionalDetails": "Mahdolliset lisätiedot", + "broadcastPastBroadcasts": "Menneet lähetykset", + "broadcastAllBroadcastsByMonth": "Näytä kaikki lähetykset kuukausikohtaisesti", "broadcastNbBroadcasts": "{count, plural, =1{{count} lähetys} other{{count} lähetystä}}", "challengeChallengesX": "Haasteet: {param1}", "challengeChallengeToPlay": "Haasta peliin", @@ -252,6 +265,7 @@ "preferencesNotifyWeb": "Selain", "preferencesNotifyDevice": "Laite", "preferencesBellNotificationSound": "Ilmoitusten kilahdusääni", + "preferencesBlindfold": "Sokko", "puzzlePuzzles": "Tehtävät", "puzzlePuzzleThemes": "Tehtävien aiheet", "puzzleRecommended": "Suosittelemme", @@ -569,7 +583,6 @@ "replayMode": "Toistotapa", "realtimeReplay": "Reaaliaik.", "byCPL": "Virheet", - "openStudy": "Avaa tutkielma", "enable": "Käytössä", "bestMoveArrow": "Parhaan siirron nuoli", "showVariationArrows": "Näytä muunnelman nuolet", @@ -777,7 +790,6 @@ "block": "Estä", "blocked": "Estetty", "unblock": "Poista esto", - "followsYou": "Seuraa sinua", "xStartedFollowingY": "{param1} alkoi seurata {param2}", "more": "Lisää", "memberSince": "Liittynyt", @@ -1531,6 +1543,7 @@ "studyPlayAgain": "Pelaa uudelleen", "studyWhatWouldYouPlay": "Mitä pelaisit tässä asemassa?", "studyYouCompletedThisLesson": "Onnittelut! Olet suorittanut tämän oppiaiheen.", + "studyPerPage": "{param} per sivu", "studyNbChapters": "{count, plural, =1{{count} luku} other{{count} lukua}}", "studyNbGames": "{count, plural, =1{{count} peli} other{{count} peliä}}", "studyNbMembers": "{count, plural, =1{{count} jäsen} other{{count} jäsentä}}", diff --git a/lib/l10n/lila_fo.arb b/lib/l10n/lila_fo.arb index f8a1ad671e..28cd5b0f21 100644 --- a/lib/l10n/lila_fo.arb +++ b/lib/l10n/lila_fo.arb @@ -370,7 +370,6 @@ "replayMode": "Endurspælsháttur", "realtimeReplay": "Verulig tíð", "byCPL": "Við CPL", - "openStudy": "Lat rannsókn upp", "enable": "Loyv", "bestMoveArrow": "Pílur fyri besta leik", "evaluationGauge": "Eftirmetingarmát", @@ -540,7 +539,6 @@ "block": "Forða", "blocked": "Forðaður", "unblock": "Forða ikki", - "followsYou": "Fylgir tær", "xStartedFollowingY": "{param1} byrjaði at fylgja {param2}", "more": "Meira", "memberSince": "Limur síðani", diff --git a/lib/l10n/lila_fr.arb b/lib/l10n/lila_fr.arb index be964e37d9..5095de4a54 100644 --- a/lib/l10n/lila_fr.arb +++ b/lib/l10n/lila_fr.arb @@ -1,47 +1,48 @@ { + "mobileAllGames": "Toutes les parties", + "mobileAreYouSure": "Êtes-vous sûr(e) ?", + "mobileBlindfoldMode": "Partie à l'aveugle", + "mobileCancelTakebackOffer": "Annuler la proposition de reprise du coup", + "mobileClearButton": "Effacer", + "mobileCorrespondenceClearSavedMove": "Effacer les coups enregistrés", + "mobileCustomGameJoinAGame": "Joindre une partie", + "mobileFeedbackButton": "Commentaires", + "mobileGreeting": "Bonjour {param}", + "mobileGreetingWithoutName": "Bonjour", + "mobileHideVariation": "Masquer les variantes", "mobileHomeTab": "Accueil", - "mobilePuzzlesTab": "Problèmes", - "mobileToolsTab": "Outils", - "mobileWatchTab": "Regarder", - "mobileSettingsTab": "Paramètres", + "mobileLiveStreamers": "Diffuseurs en direct", "mobileMustBeLoggedIn": "Vous devez être connecté pour voir cette page.", - "mobileSystemColors": "Couleurs du système", - "mobileFeedbackButton": "Commentaires", + "mobileNoSearchResults": "Aucun résultat", + "mobileNotFollowingAnyUser": "Vous ne suivez aucun utilisateur.", "mobileOkButton": "OK", + "mobilePlayersMatchingSearchTerm": "Joueurs – \"{param}\"", + "mobilePrefMagnifyDraggedPiece": "Grossir la pièce déplacée", + "mobilePuzzleStormConfirmEndRun": "Voulez-vous mettre fin à cette série?", + "mobilePuzzleStormFilterNothingToShow": "Rien à afficher. Veuillez changer les filtres.", + "mobilePuzzleStormNothingToShow": "Rien à afficher. Jouez quelques séries de problèmes (Puzzle Storm).", + "mobilePuzzleStormSubtitle": "Faites un maximum de problèmes en 3 minutes.", + "mobilePuzzleStreakAbortWarning": "Votre série actuelle (streak) prendra fin et votre résultat sera sauvegardé.", + "mobilePuzzleThemesSubtitle": "Faites des problèmes basés sur vos ouvertures préférées ou choisissez un thème.", + "mobilePuzzlesTab": "Problèmes", + "mobileRecentSearches": "Recherches récentes", "mobileSettingsHapticFeedback": "Mode vibration", "mobileSettingsImmersiveMode": "Mode plein écran", "mobileSettingsImmersiveModeSubtitle": "Masquer l'interface système durant la partie. À utiliser lorsque les gestes pour naviguer dans l'interface système sur les bords de l'écran vous gênent. S'applique aux écrans de la partie et des problèmes (Puzzle Storm).", - "mobileNotFollowingAnyUser": "Vous ne suivez aucun utilisateur.", - "mobileAllGames": "Toutes les parties", - "mobileRecentSearches": "Recherches récentes", - "mobileClearButton": "Effacer", - "mobilePlayersMatchingSearchTerm": "Joueurs – \"{param}\"", - "mobileNoSearchResults": "Aucun résultat", - "mobileAreYouSure": "Êtes-vous sûr(e) ?", - "mobilePuzzleStreakAbortWarning": "Votre série actuelle (streak) prendra fin et votre résultat sera sauvegardé.", - "mobilePuzzleStormNothingToShow": "Rien à afficher. Jouez quelques séries de problèmes (Puzzle Storm).", - "mobileSharePuzzle": "Partager ce problème", - "mobileShareGameURL": "Partager l'URL de la partie", + "mobileSettingsTab": "Paramètres", "mobileShareGamePGN": "Partager le PGN", + "mobileShareGameURL": "Partager l'URL de la partie", "mobileSharePositionAsFEN": "Partager la position FEN", - "mobileShowVariations": "Afficher les variantes", - "mobileHideVariation": "Masquer les variantes", + "mobileSharePuzzle": "Partager ce problème", "mobileShowComments": "Afficher les commentaires", - "mobilePuzzleStormConfirmEndRun": "Voulez-vous mettre fin à cette série?", - "mobilePuzzleStormFilterNothingToShow": "Rien à afficher. Veuillez changer les filtres.", - "mobileCancelTakebackOffer": "Annuler la proposition de reprise du coup", - "mobileWaitingForOpponentToJoin": "En attente d'un adversaire...", - "mobileBlindfoldMode": "Partie à l'aveugle", - "mobileLiveStreamers": "Diffuseurs en direct", - "mobileCustomGameJoinAGame": "Joindre une partie", - "mobileCorrespondenceClearSavedMove": "Effacer les coups enregistrés", - "mobileSomethingWentWrong": "Une erreur s'est produite.", "mobileShowResult": "Afficher le résultat", - "mobilePuzzleThemesSubtitle": "Faites des problèmes basés sur vos ouvertures préférées ou choisissez un thème.", - "mobilePuzzleStormSubtitle": "Faites un maximum de problèmes en 3 minutes.", - "mobileGreeting": "Bonjour {param}", - "mobileGreetingWithoutName": "Bonjour", - "mobilePrefMagnifyDraggedPiece": "Grossir la pièce déplacée", + "mobileShowVariations": "Afficher les variantes", + "mobileSomethingWentWrong": "Une erreur s'est produite.", + "mobileSystemColors": "Couleurs du système", + "mobileTheme": "Thème", + "mobileToolsTab": "Outils", + "mobileWaitingForOpponentToJoin": "En attente d'un adversaire...", + "mobileWatchTab": "Regarder", "activityActivity": "Activité", "activityHostedALiveStream": "A hébergé une diffusion en direct", "activityRankedInSwissTournament": "Classé {param1} dans le tournoi {param2}", @@ -117,11 +118,12 @@ "broadcastUploadImage": "Téléverser une image pour le tournoi", "broadcastNoBoardsYet": "Pas d'échiquiers pour le moment. Ils s'afficheront lorsque les parties seront téléversées.", "broadcastBoardsCanBeLoaded": "Les échiquiers sont chargés à partir d'une source ou de l'{param}.", - "broadcastStartsAfter": "Commence après {param}", + "broadcastStartsAfter": "Commence après la {param}", "broadcastStartVerySoon": "La diffusion commencera très bientôt.", "broadcastNotYetStarted": "La diffusion n'a pas encore commencé.", "broadcastOfficialWebsite": "Site Web officiel", "broadcastStandings": "Classement", + "broadcastOfficialStandings": "Résultats officiels", "broadcastIframeHelp": "Plus d'options sur la {param}", "broadcastWebmastersPage": "page des webmestres", "broadcastPgnSourceHelp": "Source PGN publique en temps réel pour cette ronde. Nous offrons également un {param} pour permettre une synchronisation rapide et efficace.", @@ -130,6 +132,15 @@ "broadcastRatingDiff": "Différence de cote", "broadcastGamesThisTournament": "Partie de ce tournoi", "broadcastScore": "Résultat", + "broadcastAllTeams": "Toutes les équipes", + "broadcastTournamentFormat": "Format du tournoi", + "broadcastTournamentLocation": "Lieu du tournoi", + "broadcastTopPlayers": "Meilleurs joueurs", + "broadcastTimezone": "Fuseau horaire", + "broadcastFideRatingCategory": "Catégorie FIDE", + "broadcastOptionalDetails": "Informations facultatives", + "broadcastPastBroadcasts": "Diffusions passées", + "broadcastAllBroadcastsByMonth": "Voir les diffusions par mois", "broadcastNbBroadcasts": "{count, plural, =1{{count} diffusion} other{{count} diffusions}}", "challengeChallengesX": "Défis : {param1}", "challengeChallengeToPlay": "Défier ce joueur", @@ -254,6 +265,7 @@ "preferencesNotifyWeb": "Navigateur", "preferencesNotifyDevice": "Appareil", "preferencesBellNotificationSound": "Son de notification", + "preferencesBlindfold": "Partie à l'aveugle", "puzzlePuzzles": "Problèmes", "puzzlePuzzleThemes": "Thèmes des problèmes", "puzzleRecommended": "Recommandé", @@ -571,7 +583,6 @@ "replayMode": "Rejouer la partie", "realtimeReplay": "Temps réel", "byCPL": "Par erreurs", - "openStudy": "Ouvrir l'analyse", "enable": "Activée", "bestMoveArrow": "Flèche du meilleur coup", "showVariationArrows": "Afficher les flèches de variantes", @@ -779,7 +790,6 @@ "block": "Bloquer", "blocked": "Bloqué", "unblock": "Débloquer", - "followsYou": "Vous suit", "xStartedFollowingY": "{param1} a suivi {param2}", "more": "Plus", "memberSince": "Membre depuis", @@ -1533,6 +1543,7 @@ "studyPlayAgain": "Jouer à nouveau", "studyWhatWouldYouPlay": "Que joueriez-vous dans cette position ?", "studyYouCompletedThisLesson": "Félicitations ! Vous avez terminé ce cours.", + "studyPerPage": "{param} par page", "studyNbChapters": "{count, plural, =1{{count} chapitre} other{{count} chapitres}}", "studyNbGames": "{count, plural, =1{{count} partie} other{{count} parties}}", "studyNbMembers": "{count, plural, =1{{count} membre} other{{count} membres}}", diff --git a/lib/l10n/lila_ga.arb b/lib/l10n/lila_ga.arb index d765fa7f66..432fa0743d 100644 --- a/lib/l10n/lila_ga.arb +++ b/lib/l10n/lila_ga.arb @@ -464,7 +464,6 @@ "replayMode": "Modh athimeartha", "realtimeReplay": "Fíor-am", "byCPL": "De réir CPL", - "openStudy": "Oscail staidéar", "enable": "Cumasaigh", "bestMoveArrow": "Saighead don bheart is fearr", "evaluationGauge": "Tomhsaire measúnachta", @@ -666,7 +665,6 @@ "block": "Blocáil", "blocked": "Blocáilte", "unblock": "Bain bac de", - "followsYou": "Do leanúint", "xStartedFollowingY": "Thosaigh {param1} ag leanúint {param2}", "more": "Tuilleadh", "memberSince": "Ball ó", diff --git a/lib/l10n/lila_gl.arb b/lib/l10n/lila_gl.arb index 62c07d440e..8f233c76ed 100644 --- a/lib/l10n/lila_gl.arb +++ b/lib/l10n/lila_gl.arb @@ -1,47 +1,48 @@ { + "mobileAllGames": "Todas as partidas", + "mobileAreYouSure": "Estás seguro?", + "mobileBlindfoldMode": "Ás cegas", + "mobileCancelTakebackOffer": "Cancelar a proposta de cambio", + "mobileClearButton": "Borrar", + "mobileCorrespondenceClearSavedMove": "Borrar a xogada gardada", + "mobileCustomGameJoinAGame": "Unirse a unha partida", + "mobileFeedbackButton": "Comentarios", + "mobileGreeting": "Ola, {param}", + "mobileGreetingWithoutName": "Ola", + "mobileHideVariation": "Ocultar variantes", "mobileHomeTab": "Inicio", - "mobilePuzzlesTab": "Problemas", - "mobileToolsTab": "Ferramentas", - "mobileWatchTab": "Ver", - "mobileSettingsTab": "Axustes", + "mobileLiveStreamers": "Presentadores en directo", "mobileMustBeLoggedIn": "Debes iniciar sesión para ver esta páxina.", - "mobileSystemColors": "Cores do sistema", - "mobileFeedbackButton": "Comentarios", + "mobileNoSearchResults": "Sen resultados", + "mobileNotFollowingAnyUser": "Non estás a seguir a ningún usuario.", "mobileOkButton": "OK", + "mobilePlayersMatchingSearchTerm": "O nome de usuario contén \"{param}\"", + "mobilePrefMagnifyDraggedPiece": "Ampliar a peza arrastrada", + "mobilePuzzleStormConfirmEndRun": "Queres rematar esta quenda?", + "mobilePuzzleStormFilterNothingToShow": "Non aparece nada. Por favor, cambia os filtros", + "mobilePuzzleStormNothingToShow": "Non hai nada que amosar. Primeiro xoga algunha quenda de Puzzle Storm.", + "mobilePuzzleStormSubtitle": "Resolve tantos crebacabezas como sexa posible en 3 minutos.", + "mobilePuzzleStreakAbortWarning": "Perderás a túa secuencia actual e o teu resultado gardarase.", + "mobilePuzzleThemesSubtitle": "Resolve crebacabezas das túas aperturas favoritas ou elixe un tema.", + "mobilePuzzlesTab": "Problemas", + "mobileRecentSearches": "Procuras recentes", "mobileSettingsHapticFeedback": "Vibración ó mover", "mobileSettingsImmersiveMode": "Pantalla completa", "mobileSettingsImmersiveModeSubtitle": "Oculta a Interface de Usuario mentres xogas. Emprega esta opción se che molestan os xestos de navegación do sistema ós bordos da pantalla. Aplícase ás pantallas da partida e á de Puzzle Storm.", - "mobileNotFollowingAnyUser": "Non estás a seguir a ningún usuario.", - "mobileAllGames": "Todas as partidas", - "mobileRecentSearches": "Procuras recentes", - "mobileClearButton": "Borrar", - "mobilePlayersMatchingSearchTerm": "O nome de usuario contén \"{param}\"", - "mobileNoSearchResults": "Sen resultados", - "mobileAreYouSure": "Estás seguro?", - "mobilePuzzleStreakAbortWarning": "Perderás a túa secuencia actual e o teu resultado gardarase.", - "mobilePuzzleStormNothingToShow": "Non hai nada que amosar. Primeiro xoga algunha quenda de Puzzle Storm.", - "mobileSharePuzzle": "Compartir este crebacabezas", - "mobileShareGameURL": "Compartir a URL da partida", + "mobileSettingsTab": "Axustes", "mobileShareGamePGN": "Compartir PGN", + "mobileShareGameURL": "Compartir a URL da partida", "mobileSharePositionAsFEN": "Compartir a posición coma FEN", - "mobileShowVariations": "Amosar variantes", - "mobileHideVariation": "Ocultar variantes", + "mobileSharePuzzle": "Compartir este crebacabezas", "mobileShowComments": "Amosar comentarios", - "mobilePuzzleStormConfirmEndRun": "Queres rematar esta quenda?", - "mobilePuzzleStormFilterNothingToShow": "Non aparece nada. Por favor, cambia os filtros", - "mobileCancelTakebackOffer": "Cancelar a proposta de cambio", - "mobileWaitingForOpponentToJoin": "Agardando un rival...", - "mobileBlindfoldMode": "Á cega", - "mobileLiveStreamers": "Presentadores en directo", - "mobileCustomGameJoinAGame": "Unirse a unha partida", - "mobileCorrespondenceClearSavedMove": "Borrar a xogada gardada", - "mobileSomethingWentWrong": "Algo foi mal.", "mobileShowResult": "Amosar o resultado", - "mobilePuzzleThemesSubtitle": "Resolve crebacabezas das túas aperturas favoritas ou elixe un tema.", - "mobilePuzzleStormSubtitle": "Resolve tantos crebacabezas como sexa posible en 3 minutos.", - "mobileGreeting": "Ola, {param}", - "mobileGreetingWithoutName": "Ola", - "mobilePrefMagnifyDraggedPiece": "Ampliar a peza arrastrada", + "mobileShowVariations": "Amosar variantes", + "mobileSomethingWentWrong": "Algo foi mal.", + "mobileSystemColors": "Cores do sistema", + "mobileTheme": "Tema", + "mobileToolsTab": "Ferrament.", + "mobileWaitingForOpponentToJoin": "Agardando un rival...", + "mobileWatchTab": "Ver", "activityActivity": "Actividade", "activityHostedALiveStream": "Emitiu en directo", "activityRankedInSwissTournament": "{param1}º na clasificación de {param2}", @@ -117,11 +118,12 @@ "broadcastUploadImage": "Subir a imaxe do torneo", "broadcastNoBoardsYet": "Aínda non hai taboleiros. Aparecerán cando se suban as partidas.", "broadcastBoardsCanBeLoaded": "Os taboleiros pódense cargar dende a fonte ou a través da {param}", - "broadcastStartsAfter": "Comeza en {param}", + "broadcastStartsAfter": "Comeza tras a {param}", "broadcastStartVerySoon": "A emisión comeza decontado.", "broadcastNotYetStarted": "A emisión aínda non comezou.", "broadcastOfficialWebsite": "Páxina web oficial", "broadcastStandings": "Clasificación", + "broadcastOfficialStandings": "Clasificación oficial", "broadcastIframeHelp": "Máis opcións na {param}", "broadcastWebmastersPage": "páxina do administrador web", "broadcastPgnSourceHelp": "Unha fonte dos PGN pública e en tempo real para esta rolda. Tamén ofrecemos unha {param} para unha sincronización máis rápida e eficiente.", @@ -130,6 +132,15 @@ "broadcastRatingDiff": "Diferenza de puntuación", "broadcastGamesThisTournament": "Partidas neste torneo", "broadcastScore": "Resultado", + "broadcastAllTeams": "Todos os equipos", + "broadcastTournamentFormat": "Formato do torneo", + "broadcastTournamentLocation": "Lugar do torneo", + "broadcastTopPlayers": "Mellores xogadores", + "broadcastTimezone": "Zona horaria", + "broadcastFideRatingCategory": "Categoría de puntuación FIDE", + "broadcastOptionalDetails": "Detalles opcionais", + "broadcastPastBroadcasts": "Emisións finalizadas", + "broadcastAllBroadcastsByMonth": "Ver todas as emisións por mes", "broadcastNbBroadcasts": "{count, plural, =1{{count} emisión} other{{count} emisións}}", "challengeChallengesX": "Desafíos: {param1}", "challengeChallengeToPlay": "Desafía a unha partida", @@ -254,6 +265,7 @@ "preferencesNotifyWeb": "Navegador", "preferencesNotifyDevice": "Dispositivo", "preferencesBellNotificationSound": "Son da notificación", + "preferencesBlindfold": "Ás cegas", "puzzlePuzzles": "Crebacabezas", "puzzlePuzzleThemes": "Temas de quebracabezas", "puzzleRecommended": "Recomendado", @@ -571,7 +583,6 @@ "replayMode": "Modo de repetición", "realtimeReplay": "Tempo real", "byCPL": "Por PCP", - "openStudy": "Abrir estudo", "enable": "Activar", "bestMoveArrow": "Frecha coa mellor xogada", "showVariationArrows": "Amosar as frechas das variantes", @@ -743,7 +754,7 @@ "startedStreaming": "comezou unha retransmisión", "xStartedStreaming": "{param} comezou unha retransmisión", "averageElo": "Puntuación media", - "location": "Ubicación", + "location": "Lugar", "filterGames": "Filtrar partidas", "reset": "Restablecer", "apply": "Aplicar", @@ -779,7 +790,6 @@ "block": "Bloquear", "blocked": "Bloqueado", "unblock": "Desbloquear", - "followsYou": "Séguete", "xStartedFollowingY": "{param1} comezou a seguir a {param2}", "more": "Máis", "memberSince": "Membro dende", @@ -1533,6 +1543,7 @@ "studyPlayAgain": "Xogar de novo", "studyWhatWouldYouPlay": "Que xogarías nesta posición?", "studyYouCompletedThisLesson": "Parabéns! Completaches esta lección.", + "studyPerPage": "{param} por páxina", "studyNbChapters": "{count, plural, =1{{count} Capítulo} other{{count} Capítulos}}", "studyNbGames": "{count, plural, =1{{count} Partida} other{{count} Partidas}}", "studyNbMembers": "{count, plural, =1{{count} Membro} other{{count} Membros}}", diff --git a/lib/l10n/lila_gsw.arb b/lib/l10n/lila_gsw.arb index 2bac00b7c3..8958162c02 100644 --- a/lib/l10n/lila_gsw.arb +++ b/lib/l10n/lila_gsw.arb @@ -1,47 +1,48 @@ { + "mobileAllGames": "All Schpiel", + "mobileAreYouSure": "Bisch sicher?", + "mobileBlindfoldMode": "Blind schpille", + "mobileCancelTakebackOffer": "Zugsrücknam-Offerte zruggzieh", + "mobileClearButton": "Leere", + "mobileCorrespondenceClearSavedMove": "Lösch die gschpeicherete Züg", + "mobileCustomGameJoinAGame": "Es Schpiel mitschpille", + "mobileFeedbackButton": "Rückmäldig", + "mobileGreeting": "Hoi, {param}", + "mobileGreetingWithoutName": "Hoi", + "mobileHideVariation": "Variante verberge", "mobileHomeTab": "Afangssite", - "mobilePuzzlesTab": "Ufgabe", - "mobileToolsTab": "Werchzüg", - "mobileWatchTab": "Luege", - "mobileSettingsTab": "Ischtelle", + "mobileLiveStreamers": "Live Streamer", "mobileMustBeLoggedIn": "Muesch iglogt si, zum die Site z'gseh.", - "mobileSystemColors": "Syschtem-Farbe", - "mobileFeedbackButton": "Rückmäldig", + "mobileNoSearchResults": "Nüt g'funde", + "mobileNotFollowingAnyUser": "Du folgsch keim Schpiller.", "mobileOkButton": "OK", + "mobilePlayersMatchingSearchTerm": "Schpiller mit \"{param}%", + "mobilePrefMagnifyDraggedPiece": "Vegrösserig vu de zogene Figur", + "mobilePuzzleStormConfirmEndRun": "Wottsch de Lauf beände?", + "mobilePuzzleStormFilterNothingToShow": "Nüt zum Zeige, bitte d'Filter ändere", + "mobilePuzzleStormNothingToShow": "Es git nüt zum Zeige. Schpill zerscht ochli Puzzle Storm.", + "mobilePuzzleStormSubtitle": "Lös i 3 Minute so vill Ufgabe wie möglich.", + "mobilePuzzleStreakAbortWarning": "Du verlürsch din aktuelle Lauf und din Rekord wird g'schpeicheret.", + "mobilePuzzleThemesSubtitle": "Schpill Ufgabe mit dine Lieblings-Eröffnige oder wähl es Thema.", + "mobilePuzzlesTab": "Ufgabe", + "mobileRecentSearches": "Kürzlich Gsuechts", "mobileSettingsHapticFeedback": "Rückmäldig mit Vibration", "mobileSettingsImmersiveMode": "Ibettete Modus", "mobileSettingsImmersiveModeSubtitle": "UI-Syschtem während em schpille usblände. Benutz die Option, wänn dich d'Navigationsgeschte, vum Sysychtem, am Bildschirmrand störed. Das gilt für Schpiel- und Puzzle Storm-Bildschirm.", - "mobileNotFollowingAnyUser": "Du folgsch keim Schpiller.", - "mobileAllGames": "All Schpiel", - "mobileRecentSearches": "Kürzlich Gsuechts", - "mobileClearButton": "Leere", - "mobilePlayersMatchingSearchTerm": "Schpiller mit \"{param}%", - "mobileNoSearchResults": "Nüt g'funde", - "mobileAreYouSure": "Bisch sicher?", - "mobilePuzzleStreakAbortWarning": "Du verlürsch din aktuelle Lauf und din Rekord wird g'schpeicheret.", - "mobilePuzzleStormNothingToShow": "Es git nüt zum Zeige. Schpill zerscht ochli Puzzle Storm.", - "mobileSharePuzzle": "Teil die Ufgab", - "mobileShareGameURL": "Teil d'Schpiel-URL", + "mobileSettingsTab": "Ischtelle", "mobileShareGamePGN": "Teil s'PGN", + "mobileShareGameURL": "Teil d'Schpiel-URL", "mobileSharePositionAsFEN": "Teil d'Position als FEN", - "mobileShowVariations": "Zeig Variante", - "mobileHideVariation": "Variante verberge", + "mobileSharePuzzle": "Teil die Ufgab", "mobileShowComments": "Zeig Kommentär", - "mobilePuzzleStormConfirmEndRun": "Wottsch de Lauf beände?", - "mobilePuzzleStormFilterNothingToShow": "Nüt zum Zeige, bitte d'Filter ändere", - "mobileCancelTakebackOffer": "Zugsrücknam-Offerte zruggzieh", - "mobileWaitingForOpponentToJoin": "Warte bis en Gegner erschint...", - "mobileBlindfoldMode": "Blind schpille", - "mobileLiveStreamers": "Live Streamer", - "mobileCustomGameJoinAGame": "Es Schpiel mitschpille", - "mobileCorrespondenceClearSavedMove": "Lösch die gschpeicherete Züg", - "mobileSomethingWentWrong": "Es isch öppis schief gange.", "mobileShowResult": "Resultat zeige", - "mobilePuzzleThemesSubtitle": "Schpill Ufgabe mit dine Lieblings-Eröffnige oder wähl es Thema.", - "mobilePuzzleStormSubtitle": "Lös i 3 Minute so vill Ufgabe wie möglich.", - "mobileGreeting": "Hoi, {param}", - "mobileGreetingWithoutName": "Hoi", - "mobilePrefMagnifyDraggedPiece": "Vegrösserig vu de zogene Figur", + "mobileShowVariations": "Zeig Variante", + "mobileSomethingWentWrong": "Es isch öppis schief gange.", + "mobileSystemColors": "Syschtem-Farbe", + "mobileTheme": "Farbschema", + "mobileToolsTab": "Werchzüg", + "mobileWaitingForOpponentToJoin": "Warte bis en Gegner erschint...", + "mobileWatchTab": "Luege", "activityActivity": "Aktivitäte", "activityHostedALiveStream": "Hät en Live Stream gmacht", "activityRankedInSwissTournament": "Hät Rang #{param1} im Turnier {param2} erreicht", @@ -122,6 +123,7 @@ "broadcastNotYetStarted": "Die Überträgig hät nonig agfange.", "broadcastOfficialWebsite": "Offizielli Website", "broadcastStandings": "Tabälle", + "broadcastOfficialStandings": "Offizielli Ranglischte", "broadcastIframeHelp": "Meh Optionen uf {param}", "broadcastWebmastersPage": "Webmaster Site", "broadcastPgnSourceHelp": "Öffentlichi, real-time PGN Quälle, für die Rundi. Mir offeriered au {param} für e schnälleri und effiziänteri Synchronisation.", @@ -130,6 +132,15 @@ "broadcastRatingDiff": "Wertigs Differänz", "broadcastGamesThisTournament": "Schpiel i dem Turnier", "broadcastScore": "Resultat", + "broadcastAllTeams": "Alli Teams", + "broadcastTournamentFormat": "Turnier-Format", + "broadcastTournamentLocation": "Turnier-Lokal", + "broadcastTopPlayers": "Top-Schpiller", + "broadcastTimezone": "Zitzone", + "broadcastFideRatingCategory": "FIDE-Wertigskategorie", + "broadcastOptionalDetails": "Optionali Details", + "broadcastPastBroadcasts": "G'machti Überträgige", + "broadcastAllBroadcastsByMonth": "Zeig alli Überträgige im Monet", "broadcastNbBroadcasts": "{count, plural, =1{{count} Überträgige} other{{count} Überträgige}}", "challengeChallengesX": "Useforderige: {param1}", "challengeChallengeToPlay": "Zum Schpiel fordere", @@ -254,6 +265,7 @@ "preferencesNotifyWeb": "Browser", "preferencesNotifyDevice": "Grät", "preferencesBellNotificationSound": "Ton für Benachrichtige", + "preferencesBlindfold": "Blind schpille", "puzzlePuzzles": "Ufgabe", "puzzlePuzzleThemes": "Ufgabe Theme", "puzzleRecommended": "Empfohle", @@ -571,7 +583,6 @@ "replayMode": "Widergabemodus", "realtimeReplay": "Ächtzit", "byCPL": "Nach CPL", - "openStudy": "Schtudie eröffne", "enable": "Ischalte", "bestMoveArrow": "Pfil für de bescht Zug", "showVariationArrows": "Pfil für Variante azeige", @@ -779,7 +790,6 @@ "block": "Blockiärä", "blocked": "Blockiärt", "unblock": "Blockierig ufhebe", - "followsYou": "Folgt dir", "xStartedFollowingY": "{param1} folgt jetzt {param2}", "more": "Meh", "memberSince": "Mitglid sit", @@ -1533,6 +1543,7 @@ "studyPlayAgain": "Vo vornä", "studyWhatWouldYouPlay": "Was würdisch du ih derä Stellig spiele?", "studyYouCompletedThisLesson": "Gratulation! Du häsch die Lektion abgschlosse.", + "studyPerPage": "{param} pro Site", "studyNbChapters": "{count, plural, =1{{count} Kapitel} other{{count} Kapitäl}}", "studyNbGames": "{count, plural, =1{{count} Schpiel} other{{count} Schpiel}}", "studyNbMembers": "{count, plural, =1{{count} Mitglid} other{{count} Mitglider}}", diff --git a/lib/l10n/lila_he.arb b/lib/l10n/lila_he.arb index 7569832308..e817e5ae6d 100644 --- a/lib/l10n/lila_he.arb +++ b/lib/l10n/lila_he.arb @@ -1,47 +1,48 @@ { + "mobileAllGames": "כל המשחקים", + "mobileAreYouSure": "בטוח?", + "mobileBlindfoldMode": "משחק עיוור", + "mobileCancelTakebackOffer": "ביטול ההצעה להחזיר את המהלך האחרון", + "mobileClearButton": "ניקוי", + "mobileCorrespondenceClearSavedMove": "ניקוי המהלך השמור", + "mobileCustomGameJoinAGame": "הצטרפות למשחק", + "mobileFeedbackButton": "משוב", + "mobileGreeting": "שלום, {param}", + "mobileGreetingWithoutName": "שלום", + "mobileHideVariation": "הסתרת וריאציות", "mobileHomeTab": "בית", - "mobilePuzzlesTab": "חידות", - "mobileToolsTab": "כלים", - "mobileWatchTab": "צפייה", - "mobileSettingsTab": "הגדרות", + "mobileLiveStreamers": "שדרנים בשידור חי", "mobileMustBeLoggedIn": "יש להתחבר כדי לצפות בדף זה.", - "mobileSystemColors": "צבעי מערכת ההפעלה", - "mobileFeedbackButton": "משוב", + "mobileNoSearchResults": "אין תוצאות", + "mobileNotFollowingAnyUser": "אינכם עוקבים אחר אף אחד.", "mobileOkButton": "בסדר", + "mobilePlayersMatchingSearchTerm": "שחקנים עם ״{param}״", + "mobilePrefMagnifyDraggedPiece": "הגדלת הכלי הנגרר", + "mobilePuzzleStormConfirmEndRun": "האם לסיים את הסבב?", + "mobilePuzzleStormFilterNothingToShow": "אין מה להראות. ניתן לשנות את חתכי הסינון", + "mobilePuzzleStormNothingToShow": "אין מה להראות. שחקו כמה סיבובים של Puzzle Storm קודם.", + "mobilePuzzleStormSubtitle": "פתרו כמה שיותר חידות ב־3 דקות.", + "mobilePuzzleStreakAbortWarning": "הרצף הנוכחי שלך ייאבד אך הניקוד יישמר.", + "mobilePuzzleThemesSubtitle": "פתרו חידות עם הפתיחות האהובות עליכם או בחרו ממגוון נושאים.", + "mobilePuzzlesTab": "חידות", + "mobileRecentSearches": "חיפושים אחרונים", "mobileSettingsHapticFeedback": "רטט בכל מהלך", "mobileSettingsImmersiveMode": "מצב ריכוז", "mobileSettingsImmersiveModeSubtitle": "הסתירו את שאר הממשק במהלך המשחק. מומלץ להפעיל הגדרה זו אם אפשרויות הניווט בקצות הלוח מפריעות לכם לשחק. רלוונטי למשחקים ול־Puzzle Storm.", - "mobileNotFollowingAnyUser": "אינכם עוקבים אחר אף אחד.", - "mobileAllGames": "כל המשחקים", - "mobileRecentSearches": "חיפושים אחרונים", - "mobileClearButton": "ניקוי", - "mobilePlayersMatchingSearchTerm": "שחקנים עם ״{param}״", - "mobileNoSearchResults": "אין תוצאות", - "mobileAreYouSure": "בטוח?", - "mobilePuzzleStreakAbortWarning": "הרצף הנוכחי שלך ייאבד אך הניקוד יישמר.", - "mobilePuzzleStormNothingToShow": "אין מה להראות. שחקו כמה סיבובים של Puzzle Storm קודם.", - "mobileSharePuzzle": "שיתוף החידה", - "mobileShareGameURL": "שיתוף הקישור למשחק", + "mobileSettingsTab": "הגדרות", "mobileShareGamePGN": "שיתוף ה־PGN", + "mobileShareGameURL": "שיתוף הקישור למשחק", "mobileSharePositionAsFEN": "שיתוף העמדה כ־FEN", - "mobileShowVariations": "הצגת וריאציות", - "mobileHideVariation": "הסתרת וריאציות", + "mobileSharePuzzle": "שיתוף החידה", "mobileShowComments": "הצגת הערות", - "mobilePuzzleStormConfirmEndRun": "האם לסיים את הסבב?", - "mobilePuzzleStormFilterNothingToShow": "אין מה להראות. ניתן לשנות את חתכי הסינון", - "mobileCancelTakebackOffer": "ביטול ההצעה להחזיר את המהלך האחרון", - "mobileWaitingForOpponentToJoin": "ממתין שיריב יצטרף...", - "mobileBlindfoldMode": "משחק עיוור", - "mobileLiveStreamers": "שדרנים בשידור חי", - "mobileCustomGameJoinAGame": "הצטרפות למשחק", - "mobileCorrespondenceClearSavedMove": "ניקוי המהלך השמור", - "mobileSomethingWentWrong": "משהו השתבש.", "mobileShowResult": "הצגת תוצאת המשחק", - "mobilePuzzleThemesSubtitle": "פתרו חידות עם הפתיחות האהובות עליכם או בחרו ממגוון נושאים.", - "mobilePuzzleStormSubtitle": "פתרו כמה שיותר חידות ב־3 דקות.", - "mobileGreeting": "שלום, {param}", - "mobileGreetingWithoutName": "שלום", - "mobilePrefMagnifyDraggedPiece": "הגדלת הכלי הנגרר", + "mobileShowVariations": "הצגת וריאציות", + "mobileSomethingWentWrong": "משהו השתבש.", + "mobileSystemColors": "צבעי מערכת ההפעלה", + "mobileTheme": "עיצוב", + "mobileToolsTab": "כלים", + "mobileWaitingForOpponentToJoin": "ממתין שיריב יצטרף...", + "mobileWatchTab": "צפייה", "activityActivity": "פעילות", "activityHostedALiveStream": "על\\תה לשידור חי", "activityRankedInSwissTournament": "סיים/ה במקום {param1} ב־{param2}", @@ -122,6 +123,7 @@ "broadcastNotYetStarted": "ההקרנה טרם החלה.", "broadcastOfficialWebsite": "האתר הרשמי", "broadcastStandings": "תוצאות", + "broadcastOfficialStandings": "טבלת מובילים רשמית", "broadcastIframeHelp": "ישנן אפשרויות נוספות ב{param}", "broadcastWebmastersPage": "עמוד המתכנתים", "broadcastPgnSourceHelp": "קישור ל־PGN פומבי המתעדכן בשידור חי. אנו מציעים גם {param} לסנכרון מיטבי ומהיר.", @@ -130,6 +132,15 @@ "broadcastRatingDiff": "הפרש הדירוג", "broadcastGamesThisTournament": "משחקים בטורניר זה", "broadcastScore": "ניקוד", + "broadcastAllTeams": "כל הקבוצות", + "broadcastTournamentFormat": "שיטת הטורניר", + "broadcastTournamentLocation": "מיקום הטורניר", + "broadcastTopPlayers": "שחקני צמרת", + "broadcastTimezone": "אזור זמן", + "broadcastFideRatingCategory": "קטגוריית דירוג FIDE", + "broadcastOptionalDetails": "פרטים אופציונאליים", + "broadcastPastBroadcasts": "הקרנות עבר", + "broadcastAllBroadcastsByMonth": "צפו בכל ההקרנות לפי חודש", "broadcastNbBroadcasts": "{count, plural, =1{הקרנה {count}} =2{{count} הקרנות} many{{count} הקרנות} other{{count} הקרנות}}", "challengeChallengesX": "הזמנות למשחק: {param1}", "challengeChallengeToPlay": "הזמינו למשחק", @@ -254,6 +265,7 @@ "preferencesNotifyWeb": "דפדפן", "preferencesNotifyDevice": "מכשיר", "preferencesBellNotificationSound": "השמע צליל עבור התראות פעמון", + "preferencesBlindfold": "משחק עיוור", "puzzlePuzzles": "פאזלים", "puzzlePuzzleThemes": "חידות נושאיות", "puzzleRecommended": "מומלץ", @@ -459,7 +471,7 @@ "settingsCloseAccount": "סגירת החשבון", "settingsManagedAccountCannotBeClosed": "חשבונך מנוהל, ולכן לא ניתן לסגור אותו.", "settingsClosingIsDefinitive": "הסגירה היא סופית. אין דרך חזרה. האם את/ה בטוח/ה?", - "settingsCantOpenSimilarAccount": "לא תוכל/י לפתוח חשבון חדש עם אותו השם, אפילו בשינוי אותיות קטנות לגדולות והפוך. ", + "settingsCantOpenSimilarAccount": "לא תוכל/י לפתוח חשבון חדש עם אותו השם, אפילו בשינוי אותיות קטנות לגדולות והפוך.", "settingsChangedMindDoNotCloseAccount": "שיניתי את דעתי, אל תסגרו את החשבון שלי", "settingsCloseAccountExplanation": "האם אכן ברצונך לסגור את חשבונך? סגירת חשבונך היא החלטה סופית. לעולם לא יהיה אפשר להתחבר לחשבון הזה שוב.", "settingsThisAccountIsClosed": "החשבון הזה סגור.", @@ -571,7 +583,6 @@ "replayMode": "מצב הרצת המסעים", "realtimeReplay": "זמן אמת", "byCPL": "עפ\"י CPL", - "openStudy": "פתח לוח למידה", "enable": "הפעלה", "bestMoveArrow": "חץ המהלך הטוב ביותר", "showVariationArrows": "הצגת חצי ההמשכים האלטרנטיביים", @@ -779,7 +790,6 @@ "block": "חסמו", "blocked": "חסום", "unblock": "בטל חסימה", - "followsYou": "עוקב/ת אחריך", "xStartedFollowingY": "{param1} התחיל/ה לעקוב אחרי {param2}", "more": "עוד", "memberSince": "רשום/ה מ", @@ -1533,6 +1543,7 @@ "studyPlayAgain": "הפעל שוב", "studyWhatWouldYouPlay": "מה הייתם משחקים בעמדה הזו?", "studyYouCompletedThisLesson": "מזל טוב! סיימתם את השיעור.", + "studyPerPage": "{param} לכל עמוד", "studyNbChapters": "{count, plural, =1{פרק {count}} =2{{count} פרקים} many{{count} פרקים} other{{count} פרקים}}", "studyNbGames": "{count, plural, =1{{count} משחק} =2{{count} משחקים} many{{count} משחקים} other{{count} משחקים}}", "studyNbMembers": "{count, plural, =1{משתמש אחד} =2{{count} משתמשים} many{{count} משתמשים} other{{count} משתמשים}}", diff --git a/lib/l10n/lila_hi.arb b/lib/l10n/lila_hi.arb index e003d2369a..b1f19b914f 100644 --- a/lib/l10n/lila_hi.arb +++ b/lib/l10n/lila_hi.arb @@ -1,26 +1,26 @@ { + "mobileAllGames": "सारे गेम्स", + "mobileAreYouSure": "क्या आप सुनिश्चित हैं?", + "mobileCancelTakebackOffer": "Takeback प्रस्ताव रद्द करें", + "mobileFeedbackButton": "फीडबैक", + "mobileHideVariation": "वेरिएशन छुपाए", "mobileHomeTab": "होम", - "mobilePuzzlesTab": "पज़ल", - "mobileToolsTab": "टूल्स", - "mobileWatchTab": "देखें", - "mobileSettingsTab": "सेटिंग", + "mobileLiveStreamers": "लाइव स्ट्रीमर्स", "mobileMustBeLoggedIn": "इस पेज को देखने के लिए आपको login करना होगा", - "mobileFeedbackButton": "फीडबैक", + "mobileNoSearchResults": "कोई परिणाम नहीं", "mobileOkButton": "ओके", + "mobilePuzzlesTab": "पज़ल", "mobileSettingsHapticFeedback": "कंपन फीडबैक", "mobileSettingsImmersiveMode": "इमर्सिव मोड", - "mobileAllGames": "सारे गेम्स", - "mobileNoSearchResults": "कोई परिणाम नहीं", - "mobileAreYouSure": "क्या आप सुनिश्चित हैं?", - "mobileSharePuzzle": "पज़ल शरीर करें", - "mobileShareGameURL": "गेम URL शेयर करें", + "mobileSettingsTab": "सेटिंग", "mobileShareGamePGN": "PGN शेयर करें", + "mobileShareGameURL": "गेम URL शेयर करें", "mobileSharePositionAsFEN": "पोजीशन FEN के रूप में शेयर करें", - "mobileShowVariations": "वेरिएशन देखें", - "mobileHideVariation": "वेरिएशन छुपाए", + "mobileSharePuzzle": "पज़ल शरीर करें", "mobileShowComments": "कमेंट्स देखें", - "mobileCancelTakebackOffer": "Takeback प्रस्ताव रद्द करें", - "mobileLiveStreamers": "लाइव स्ट्रीमर्स", + "mobileShowVariations": "वेरिएशन देखें", + "mobileToolsTab": "टूल्स", + "mobileWatchTab": "देखें", "activityActivity": "कार्यकलाप", "activityHostedALiveStream": "एक लाइव स्ट्रीम होस्ट किया गया", "activityRankedInSwissTournament": "#{param1} स्थान {param2} मे", @@ -479,7 +479,6 @@ "replayMode": "रीप्ले मोड", "realtimeReplay": "रियल टाइम", "byCPL": "CPL द्वारा", - "openStudy": "अध्ययन खोलो", "enable": "सक्षम करें", "bestMoveArrow": "सर्वश्रेष्ठ चाल तीर", "showVariationArrows": "विविधता वाले तीर दिखाएँ", @@ -683,7 +682,6 @@ "block": "अवस्र्द्ध (ब्लॉक) करें", "blocked": "अवस्र्द्ध (ब्लॉक) कर दिया गया", "unblock": "अवस्र्द्ध (ब्लॉक) न करें", - "followsYou": "आपका अनुसरण कर रहे हैं", "xStartedFollowingY": "{param1} ने {param2} का अनुसरण करना शुरू किया", "more": "अधिक", "memberSince": "सदस्य बनने की तारीख", diff --git a/lib/l10n/lila_hr.arb b/lib/l10n/lila_hr.arb index 2e5ceee98b..c40eddef64 100644 --- a/lib/l10n/lila_hr.arb +++ b/lib/l10n/lila_hr.arb @@ -43,6 +43,7 @@ "broadcastDeleteAllGamesOfThisRound": "Izbriši sve igre ovog kola. Izvor mora biti aktivan kako bi ih se ponovno stvorilo.", "broadcastDeleteTournament": "Izbriši ovaj turnir", "broadcastNbBroadcasts": "{count, plural, =1{{count} prijenos} few{{count} prijenosa} other{{count} prijenosa}}", + "challengeChallengesX": "Izazova: {param1}", "challengeChallengeToPlay": "Poziv na partiju", "challengeChallengeDeclined": "Izazov odbijen", "challengeChallengeAccepted": "Izazov prihvaćen!", @@ -158,7 +159,7 @@ "preferencesNotifyTournamentSoon": "Turnir započinje ubrzo", "preferencesNotifyTimeAlarm": "Sat za dopisivanje ističe", "preferencesNotifyBell": "Obavijest zvonom unutar Lichessa", - "preferencesNotifyPush": "Obavijest uređaja kada niste na Lichessu", + "preferencesNotifyPush": "Obavijest uređaja kada niste na Lichess-u", "preferencesNotifyWeb": "Preglednik", "preferencesNotifyDevice": "Uređaj", "preferencesBellNotificationSound": "Obavijest kao zvuk", @@ -477,7 +478,6 @@ "replayMode": "Repriza partije", "realtimeReplay": "U stvarnom vremenu", "byCPL": "Po SDP", - "openStudy": "Otvori studiju", "enable": "Omogući", "bestMoveArrow": "Strelica za najbolji potez", "showVariationArrows": "Pokaži strelice varijacija", @@ -532,7 +532,7 @@ "changeUsernameNotSame": "Jedino se veličina slova može promijeniti. Primjerice, ''johndoe'' u ''JohnDoe''.", "changeUsernameDescription": "Promijeni korisničko ime. Ovo možeš učiniti samo jednom i samo možeš promijeniti veličinu slova svog korisničkog imena.", "signupUsernameHint": "Obavezno odaberi obiteljsko korisničko ime. Ne možeš ga kasnije promijeniti i svi računi s neprikladnim korisničkim imenima bit će zatvoreni!", - "signupEmailHint": "Koristit ćemo ga samo za ponovno postavljanje lozinke.", + "signupEmailHint": "Koristiti ćemo ga samo za ponovno postavljanje lozinke.", "password": "Lozinka", "changePassword": "Promijeni lozinku", "changeEmail": "Promijeni email", @@ -540,7 +540,7 @@ "passwordReset": "Resetiraj lozinku", "forgotPassword": "Zaboravio/la si lozinku?", "error_weakPassword": "Ova je lozinka iznimno česta i previše je lako pogoditi.", - "error_namePassword": "Molimo da ne koristitiš svoje korisničko ime kao lozinku.", + "error_namePassword": "Molimo da ne koristiš svoje korisničko ime kao lozinku.", "blankedPassword": "Koristio si istu lozinku na drugom mjestu, a to je mjesto ugroženo. Kako bismo osigurali sigurnost tvoga Lichess računa, potrebno je da postaviš novu lozinku. Hvala na razumijevanju.", "youAreLeavingLichess": "Odlazite sa Lichess-a", "neverTypeYourPassword": "Nikada nemojte upisivati svoju Lichess lozinku na drugom mjestu!", @@ -679,7 +679,6 @@ "block": "Blokiraj", "blocked": "Blokirani", "unblock": "Odblokiraj", - "followsYou": "Prati te", "xStartedFollowingY": "{param1} je počeo pratiti {param2}", "more": "Više", "memberSince": "Član od", @@ -737,7 +736,9 @@ "ifNoneLeaveEmpty": "Nemaš rejting? Ostavi polje prazno", "profile": "Profil", "editProfile": "Uredi profil", + "realName": "Puno ime", "biography": "Životopis", + "countryRegion": "Država ili regija", "thankYou": "Hvala!", "socialMediaLinks": "Linkovi društvenih mreža", "oneUrlPerLine": "Jedan URL po liniji.", @@ -897,6 +898,8 @@ "keyGoToStartOrEnd": "idi na početak/kraj", "keyShowOrHideComments": "pokaži/sakrij komentare", "keyEnterOrExitVariation": "otvori/zatvori varijantu", + "keyRequestComputerAnalysis": "Zatraži računalnu analizu, Uči na svojim greškama", + "keyNextLearnFromYourMistakes": "Sljedeće (Uči na svojim greškama)", "newTournament": "Novi turnir", "tournamentHomeTitle": "Šahovski turniri s različitim vremenima partije i varijantama", "tournamentHomeDescription": "Igraj brze turnire! Pridruži se turniru ili stvori svoj turnir. Bullet, Blitz, Klasični šah, Šah 960 (Fischerov nasumični šah), Kralj na centru, Tri šaha, i još više opcija za neograničenu šahovsku zabavu.", @@ -969,6 +972,9 @@ "transparent": "Prozirna", "deviceTheme": "Tema uređaja", "backgroundImageUrl": "URL pozadinske slike:", + "board": "Ploča", + "size": "Veličina", + "brightness": "Svjetlina", "pieceSet": "Set figura", "embedInYourWebsite": "Ugradi u svoju stranicu", "usernameAlreadyUsed": "Ovo korisničko ime je već u uporabi, molimo probaj s drugim.", @@ -983,6 +989,7 @@ "invalidFen": "Nevaljan FEN", "custom": "Prilagođeno", "notifications": "Obavijesti", + "notificationsX": "Obavijesti: {param1}", "perfRatingX": "Rejting: {param}", "practiceWithComputer": "Vježbaj sa kompjuterom", "anotherWasX": "Drugi potez je {param}", @@ -1027,6 +1034,7 @@ "playVariationToCreateConditionalPremoves": "Odigraj varijantu da stvoriš uvjetni predpotez", "noConditionalPremoves": "Nema uvjetnih predpoteza", "playX": "Igraj {param}", + "clickHereToReadIt": "Klikni ovdje da pročitaš", "sorry": "Oprosti :(", "weHadToTimeYouOutForAWhile": "Trebali smo te na neko vrijeme izbaciti.", "why": "Zašto?", @@ -1099,7 +1107,7 @@ "minimumRatedGames": "Minimalni broj rejting partija", "minimumRating": "Minimalni rejting", "maximumWeeklyRating": "Maksimalni tjedni rejting", - "positionInputHelp": "Zalijepite važeći FEN da biste započeli svaku igru s određene pozicije.\nRadi samo za standardne igre, ne i za varijante.\nMožete koristiti {param} za generiranje FEN pozicije, a zatim ga zalijepite ovdje.\nOstavite prazno za početak igre s normalne početne pozicije.", + "positionInputHelp": "Zalijepite važeći FEN da biste započeli svaku igru s određene pozicije.\nRadi samo za standardne igre, ne i za varijante.\nMožete koristiti {param} za generiranje FEN pozicije, zatim ga zalijepite ovdje.\nOstavite prazno za početak igre s normalne početne pozicije.", "cancelSimul": "Otkaži simultanku", "simulHostcolor": "Boja domaćina u svakoj igri", "estimatedStart": "Predviđeno vrijeme početka", @@ -1141,6 +1149,7 @@ "closingAccountWithdrawAppeal": "Zatvaranje računa će povući vašu žalbu", "ourEventTips": "Naši savjeti za organizaciju događaja", "lichessPatronInfo": "Lichess je dobrotvorni i potpuno besplatan softver otvorenog koda.\nSvi operativni troškovi, razvoj i sadržaj financiraju se isključivo donacijama korisnika.", + "stats": "Statistika", "opponentLeftCounter": "{count, plural, =1{Tvoj protivnik je napustio igru. Možes potvrditi pobjedu za {count} sekundu.} few{Tvoj protivnik je napustio igru. Možes potvrditi pobjedu za {count} sekunde.} other{Tvoj protivnik je napustio igru. Možes potvrditi pobjedu za {count} sekundi.}}", "mateInXHalfMoves": "{count, plural, =1{Mat u {count} međupotezu} few{Mat u {count} međupoteza} other{Mat u {count} međupoteza}}", "nbBlunders": "{count, plural, =1{{count} gruba greška} few{{count} grube greške} other{{count} grubih grešaka}}", diff --git a/lib/l10n/lila_hu.arb b/lib/l10n/lila_hu.arb index 008dd28309..99a9e341b7 100644 --- a/lib/l10n/lila_hu.arb +++ b/lib/l10n/lila_hu.arb @@ -1,46 +1,46 @@ { + "mobileAllGames": "Összes játszma", + "mobileAreYouSure": "Biztos vagy benne?", + "mobileBlindfoldMode": "Vakjátszma mód", + "mobileCancelTakebackOffer": "Visszalépés kérésének visszavonása", + "mobileClearButton": "Törlés", + "mobileCorrespondenceClearSavedMove": "Mentett lépés törlése", + "mobileCustomGameJoinAGame": "Csatlakozás játszmához", + "mobileFeedbackButton": "Visszajelzés", + "mobileGreeting": "Üdv {param}!", + "mobileGreetingWithoutName": "Üdv", + "mobileHideVariation": "Változatok elrejtése", "mobileHomeTab": "Kezdőlap", - "mobilePuzzlesTab": "Feladvány", - "mobileToolsTab": "Eszközök", - "mobileWatchTab": "Néznivaló", - "mobileSettingsTab": "Beállítás", + "mobileLiveStreamers": "Lichess streamerek", "mobileMustBeLoggedIn": "Az oldal megtekintéséhez be kell jelentkezned.", - "mobileSystemColors": "Rendszerszínek", - "mobileFeedbackButton": "Visszajelzés", + "mobileNoSearchResults": "Nincs találat", + "mobileNotFollowingAnyUser": "Jelenleg nem követsz senkit.", "mobileOkButton": "OK", + "mobilePlayersMatchingSearchTerm": "Játékosok {param} felhasználónévvel", + "mobilePrefMagnifyDraggedPiece": "Mozdított bábu nagyítása", + "mobilePuzzleStormConfirmEndRun": "Befejezed a futamot?", + "mobilePuzzleStormFilterNothingToShow": "Nincs megjeleníthető elem, változtasd meg a szűrőket", + "mobilePuzzleStormSubtitle": "Oldd meg a lehető legtöbb feladványt 3 perc alatt.", + "mobilePuzzleStreakAbortWarning": "A jelenlegi sorozatod elveszik és az eredményedet rögzítjük.", + "mobilePuzzleThemesSubtitle": "Oldj feladványokat kedvenc megnyitásaid kapcsán vagy válassz egy tematikát.", + "mobilePuzzlesTab": "Feladvány", + "mobileRecentSearches": "Keresési előzmények", "mobileSettingsHapticFeedback": "Érintésalapú visszajelzés", "mobileSettingsImmersiveMode": "Teljes képernyős mód", "mobileSettingsImmersiveModeSubtitle": "A rendszer gombjainak elrejtése játék közben. Kapcsold be, ha zavarnak a rendszer navigációs mozdulatai a képernyő sarkainál. A játszmaképernyőn és a Puzzle Storm képernyőjén működik.", - "mobileNotFollowingAnyUser": "Jelenleg nem követsz senkit.", - "mobileAllGames": "Összes játszma", - "mobileRecentSearches": "Keresési előzmények", - "mobileClearButton": "Törlés", - "mobilePlayersMatchingSearchTerm": "Játékosok {param} felhasználónévvel", - "mobileNoSearchResults": "Nincs találat", - "mobileAreYouSure": "Biztos vagy benne?", - "mobilePuzzleStreakAbortWarning": "A jelenlegi sorozatod elveszik és az eredményedet rögzítjük.", - "mobileSharePuzzle": "Feladvány megosztása", - "mobileShareGameURL": "Játszma URL megosztása", + "mobileSettingsTab": "Beállítás", "mobileShareGamePGN": "PGN megosztása", + "mobileShareGameURL": "Játszma URL megosztása", "mobileSharePositionAsFEN": "Állás megosztása FEN-ként", - "mobileShowVariations": "Változatok megjelenítése", - "mobileHideVariation": "Változatok elrejtése", + "mobileSharePuzzle": "Feladvány megosztása", "mobileShowComments": "Megjegyzések megjelenítése", - "mobilePuzzleStormConfirmEndRun": "Befejezed a futamot?", - "mobilePuzzleStormFilterNothingToShow": "Nincs megjeleníthető elem, változtasd meg a szűrőket", - "mobileCancelTakebackOffer": "Visszalépés kérésének visszavonása", - "mobileWaitingForOpponentToJoin": "Várakozás az ellenfél csatlakozására...", - "mobileBlindfoldMode": "Vakjátszma mód", - "mobileLiveStreamers": "Lichess streamerek", - "mobileCustomGameJoinAGame": "Csatlakozás játszmához", - "mobileCorrespondenceClearSavedMove": "Mentett lépés törlése", - "mobileSomethingWentWrong": "Hiba történt.", "mobileShowResult": "Eredmény mutatása", - "mobilePuzzleThemesSubtitle": "Oldj feladványokat kedvenc megnyitásaid kapcsán vagy válassz egy tematikát.", - "mobilePuzzleStormSubtitle": "Oldd meg a lehető legtöbb feladványt 3 perc alatt.", - "mobileGreeting": "Üdv {param}!", - "mobileGreetingWithoutName": "Üdv", - "mobilePrefMagnifyDraggedPiece": "Mozdított bábu nagyítása", + "mobileShowVariations": "Változatok megjelenítése", + "mobileSomethingWentWrong": "Hiba történt.", + "mobileSystemColors": "Rendszerszínek", + "mobileToolsTab": "Eszközök", + "mobileWaitingForOpponentToJoin": "Várakozás az ellenfél csatlakozására...", + "mobileWatchTab": "Néznivaló", "activityActivity": "Aktivitás", "activityHostedALiveStream": "Élőben közvetített", "activityRankedInSwissTournament": "Helyezés: {param1} / {param2}", @@ -209,6 +209,7 @@ "preferencesNotifyWeb": "Böngésző", "preferencesNotifyDevice": "Eszköz", "preferencesBellNotificationSound": "Hangjelzés", + "preferencesBlindfold": "Vakjátszma mód", "puzzlePuzzles": "Feladványok", "puzzlePuzzleThemes": "Feladvány témák", "puzzleRecommended": "Ajánlott", @@ -524,7 +525,6 @@ "replayMode": "Visszajátszás", "realtimeReplay": "Valós idejű", "byCPL": "CPL", - "openStudy": "Tanulmány megnyitása", "enable": "Engedélyezve", "bestMoveArrow": "Legjobb lépés mutatása", "showVariationArrows": "Változatok nyilainak megjelenítése", @@ -730,7 +730,6 @@ "block": "Letiltás", "blocked": "Letiltva", "unblock": "Letiltás feloldása", - "followsYou": "Követ téged", "xStartedFollowingY": "{param1} {param2} követője lett", "more": "Több", "memberSince": "Tagság kezdete:", diff --git a/lib/l10n/lila_hy.arb b/lib/l10n/lila_hy.arb index 0cc0a39ab0..03b1c78625 100644 --- a/lib/l10n/lila_hy.arb +++ b/lib/l10n/lila_hy.arb @@ -460,7 +460,6 @@ "replayMode": "Դիտել կրկնապատկերը", "realtimeReplay": "Ինչպես պարտիայում", "byCPL": "Ըստ սխալների", - "openStudy": "Բացել ուսուցումը", "enable": "Միացնել", "bestMoveArrow": "Լավագույն քայլի սլաքը", "showVariationArrows": "Ցուցադրել տարբերակների սլաքները", @@ -664,7 +663,6 @@ "block": "Արգելափակել", "blocked": "Արգելափակված է", "unblock": "Հանել արգելափակումը", - "followsYou": "Հետևում են ձեզ", "xStartedFollowingY": "{param1}-ը այժմ հետևում է {param2}-ին", "more": "Ավելին", "memberSince": "Անդամ է՝ սկսած", diff --git a/lib/l10n/lila_id.arb b/lib/l10n/lila_id.arb index 3af7438340..d3eb0ab68b 100644 --- a/lib/l10n/lila_id.arb +++ b/lib/l10n/lila_id.arb @@ -1,4 +1,21 @@ { + "mobileAllGames": "Semua permainan", + "mobileAreYouSure": "Apa kamu yakin?", + "mobileFeedbackButton": "Ulas balik", + "mobileGreeting": "Halo, {param}", + "mobileGreetingWithoutName": "Halo", + "mobileHideVariation": "Sembunyikan variasi", + "mobileHomeTab": "Beranda", + "mobileOkButton": "Oke", + "mobilePuzzlesTab": "Teka-teki", + "mobileSettingsTab": "Pengaturan", + "mobileShareGamePGN": "Bagikan GPN", + "mobileShareGameURL": "Bagikan URL permainan", + "mobileSharePuzzle": "Bagikan teka-teki ini", + "mobileShowComments": "Tampilkan komentar", + "mobileShowResult": "Tampilkan hasil", + "mobileShowVariations": "Tampilkan variasi", + "mobileWatchTab": "Tontonan", "activityActivity": "Aktivitas", "activityHostedALiveStream": "Host streaming langsung", "activityRankedInSwissTournament": "Peringkat #{param1} di {param2}", @@ -462,7 +479,6 @@ "replayMode": "Mode Putar Ulang", "realtimeReplay": "Langsung", "byCPL": "Secara CPL", - "openStudy": "Buka studi", "enable": "Aktifkan", "bestMoveArrow": "Panah Langkah terbaik", "showVariationArrows": "Tampilkan variasi panah", @@ -669,7 +685,6 @@ "block": "Blokir", "blocked": "Diblokir", "unblock": "Buka blokir", - "followsYou": "Mengikuti anda", "xStartedFollowingY": "{param1} mulai mengikuti {param2}", "more": "Lainnya", "memberSince": "Anggota sejak", diff --git a/lib/l10n/lila_it.arb b/lib/l10n/lila_it.arb index 7bced24990..12900b6999 100644 --- a/lib/l10n/lila_it.arb +++ b/lib/l10n/lila_it.arb @@ -1,47 +1,48 @@ { + "mobileAllGames": "Tutte le partite", + "mobileAreYouSure": "Sei sicuro?", + "mobileBlindfoldMode": "Alla cieca", + "mobileCancelTakebackOffer": "Annulla richiesta di ritiro mossa", + "mobileClearButton": "Elimina", + "mobileCorrespondenceClearSavedMove": "Cancella mossa salvata", + "mobileCustomGameJoinAGame": "Unisciti a una partita", + "mobileFeedbackButton": "Suggerimenti", + "mobileGreeting": "Ciao, {param}", + "mobileGreetingWithoutName": "Ciao", + "mobileHideVariation": "Nascondi variante", "mobileHomeTab": "Home", - "mobilePuzzlesTab": "Tattiche", - "mobileToolsTab": "Strumenti", - "mobileWatchTab": "Guarda", - "mobileSettingsTab": "Settaggi", + "mobileLiveStreamers": "Streamer in diretta", "mobileMustBeLoggedIn": "Devi aver effettuato l'accesso per visualizzare questa pagina.", - "mobileSystemColors": "Tema app", - "mobileFeedbackButton": "Suggerimenti", + "mobileNoSearchResults": "Nessun risultato", + "mobileNotFollowingAnyUser": "Non stai seguendo nessun utente.", "mobileOkButton": "Ok", + "mobilePlayersMatchingSearchTerm": "Giocatori con \"{param}\"", + "mobilePrefMagnifyDraggedPiece": "Ingrandisci il pezzo trascinato", + "mobilePuzzleStormConfirmEndRun": "Vuoi terminare questa serie?", + "mobilePuzzleStormFilterNothingToShow": "Nessun risultato, per favore modifica i filtri", + "mobilePuzzleStormNothingToShow": "Niente da mostrare. Gioca ad alcune partite di Puzzle Storm.", + "mobilePuzzleStormSubtitle": "Risolvi il maggior numero di puzzle in tre minuti.", + "mobilePuzzleStreakAbortWarning": "Perderai la tua serie corrente e il tuo punteggio verrà salvato.", + "mobilePuzzleThemesSubtitle": ".", + "mobilePuzzlesTab": "Tattiche", + "mobileRecentSearches": "Ricerche recenti", "mobileSettingsHapticFeedback": "Feedback tattile", "mobileSettingsImmersiveMode": "Modalità immersiva", "mobileSettingsImmersiveModeSubtitle": "Nascondi la UI di sistema mentre giochi. Attiva se i gesti di navigazione ai bordi dello schermo ti danno fastidio. Si applica alla schermata di gioco e Puzzle Storm.", - "mobileNotFollowingAnyUser": "Non stai seguendo nessun utente.", - "mobileAllGames": "Tutte le partite", - "mobileRecentSearches": "Ricerche recenti", - "mobileClearButton": "Elimina", - "mobilePlayersMatchingSearchTerm": "Giocatori con \"{param}\"", - "mobileNoSearchResults": "Nessun risultato", - "mobileAreYouSure": "Sei sicuro?", - "mobilePuzzleStreakAbortWarning": "Perderai la tua serie corrente e il tuo punteggio verrà salvato.", - "mobilePuzzleStormNothingToShow": "Niente da mostrare. Gioca ad alcune partite di Puzzle Storm.", - "mobileSharePuzzle": "Condividi questa tattica", - "mobileShareGameURL": "Condividi URL della partita", + "mobileSettingsTab": "Settaggi", "mobileShareGamePGN": "Condividi PGN", + "mobileShareGameURL": "Condividi URL della partita", "mobileSharePositionAsFEN": "Condividi posizione come FEN", - "mobileShowVariations": "Mostra varianti", - "mobileHideVariation": "Nascondi variante", + "mobileSharePuzzle": "Condividi questa tattica", "mobileShowComments": "Mostra commenti", - "mobilePuzzleStormConfirmEndRun": "Vuoi terminare questa serie?", - "mobilePuzzleStormFilterNothingToShow": "Nessun risultato, per favore modifica i filtri", - "mobileCancelTakebackOffer": "Annulla richiesta di ritiro mossa", - "mobileWaitingForOpponentToJoin": "In attesa dell'avversario...", - "mobileBlindfoldMode": "Alla cieca", - "mobileLiveStreamers": "Streamer in diretta", - "mobileCustomGameJoinAGame": "Unisciti a una partita", - "mobileCorrespondenceClearSavedMove": "Cancella mossa salvata", - "mobileSomethingWentWrong": "Si è verificato un errore.", "mobileShowResult": "Mostra il risultato", - "mobilePuzzleThemesSubtitle": ".", - "mobilePuzzleStormSubtitle": "Risolvi il maggior numero di puzzle in tre minuti.", - "mobileGreeting": "Ciao, {param}", - "mobileGreetingWithoutName": "Ciao", - "mobilePrefMagnifyDraggedPiece": "Ingrandisci il pezzo trascinato", + "mobileShowVariations": "Mostra varianti", + "mobileSomethingWentWrong": "Si è verificato un errore.", + "mobileSystemColors": "Tema app", + "mobileTheme": "Tema", + "mobileToolsTab": "Strumenti", + "mobileWaitingForOpponentToJoin": "In attesa dell'avversario...", + "mobileWatchTab": "Guarda", "activityActivity": "Attività", "activityHostedALiveStream": "Ha ospitato una diretta", "activityRankedInSwissTournament": "Classificato #{param1} in {param2}", @@ -109,6 +110,37 @@ "broadcastAgeThisYear": "Età quest'anno", "broadcastUnrated": "Non classificato", "broadcastRecentTournaments": "Tornei recenti", + "broadcastOpenLichess": "Apri con Lichess", + "broadcastTeams": "Squadre", + "broadcastBoards": "Scacchiere", + "broadcastOverview": "Panoramica", + "broadcastSubscribeTitle": "Iscriviti per ricevere notifiche sull'inizio di ogni round. Puoi attivare o disattivare la campanella o le notifiche push per le dirette nelle preferenze del tuo account.", + "broadcastUploadImage": "Carica immagine del torneo", + "broadcastNoBoardsYet": "Non sono ancora presenti scacchiere. Esse compariranno non appena i giochi saranno stati caricati.", + "broadcastBoardsCanBeLoaded": "Le scacchiere possono essere caricate con una sorgente o tramite {param}", + "broadcastStartsAfter": "Inizia tra {param}", + "broadcastStartVerySoon": "Questa trasmissione inizierà a breve.", + "broadcastNotYetStarted": "Questa trasmissione non è ancora cominciata.", + "broadcastOfficialWebsite": "Sito web ufficiale", + "broadcastStandings": "Classifica", + "broadcastOfficialStandings": "Classifica Ufficiale", + "broadcastIframeHelp": "Altre opzioni si trovano nella {param}", + "broadcastWebmastersPage": "pagina dei gestori web", + "broadcastPgnSourceHelp": "Una sorgente PGN pubblica per questo round. Viene offerta anche un'{param} per una sincronizzazione più rapida ed efficiente.", + "broadcastEmbedThisBroadcast": "Incorpora questa trasmissione nel tuo sito web", + "broadcastEmbedThisRound": "Incorpora {param} nel tuo sito web", + "broadcastRatingDiff": "Differenza di punteggio", + "broadcastGamesThisTournament": "Partite in questo torneo", + "broadcastScore": "Punteggio", + "broadcastAllTeams": "Tutte le squadre", + "broadcastTournamentFormat": "Formato del torneo", + "broadcastTournamentLocation": "Luogo del Torneo", + "broadcastTopPlayers": "Giocatori migliori", + "broadcastTimezone": "Fuso orario", + "broadcastFideRatingCategory": "Categoria di punteggio FIDE", + "broadcastOptionalDetails": "Dettagli facoltativi", + "broadcastPastBroadcasts": "Trasmissioni precedenti", + "broadcastAllBroadcastsByMonth": "Visualizza tutte le trasmissioni per mese", "broadcastNbBroadcasts": "{count, plural, =1{{count} diretta} other{{count} dirette}}", "challengeChallengesX": "Sfide: {param1}", "challengeChallengeToPlay": "Sfida a una partita", @@ -233,6 +265,7 @@ "preferencesNotifyWeb": "Browser", "preferencesNotifyDevice": "Dispositivo", "preferencesBellNotificationSound": "Tono notifica", + "preferencesBlindfold": "Alla cieca", "puzzlePuzzles": "Problemi", "puzzlePuzzleThemes": "Problemi a tema", "puzzleRecommended": "Consigliati", @@ -550,7 +583,6 @@ "replayMode": "Modalità replay", "realtimeReplay": "In tempo reale", "byCPL": "Per CPL", - "openStudy": "Apri studio", "enable": "Abilita", "bestMoveArrow": "Freccia per la mossa migliore", "showVariationArrows": "Mostra le frecce delle varianti", @@ -641,6 +673,7 @@ "rank": "Posizione", "rankX": "Posizione: {param}", "gamesPlayed": "Partite giocate", + "ok": "OK", "cancel": "Annulla", "whiteTimeOut": "Il bianco ha esaurito il tempo", "blackTimeOut": "Il nero ha esaurito il tempo", @@ -757,7 +790,6 @@ "block": "Blocca", "blocked": "Bloccato", "unblock": "Sblocca", - "followsYou": "Ti segue", "xStartedFollowingY": "{param1} ha iniziato a seguire {param2}", "more": "Altro", "memberSince": "Membro dal", @@ -1263,6 +1295,7 @@ "showMeEverything": "Mostra tutto", "lichessPatronInfo": "Lichess è un software open source completamente gratuito e libero\nTutti i costi operativi, lo sviluppo e i contenuti sono finanziati esclusivamente dalle donazioni degli utenti.", "nothingToSeeHere": "Niente da vedere qui al momento.", + "stats": "Statistiche", "opponentLeftCounter": "{count, plural, =1{Il tuo avversario ha lasciato la partita. Puoi reclamare la vittoria fra {count} secondo.} other{Il tuo avversario ha lasciato la partita. Puoi reclamare la vittoria fra {count} secondi.}}", "mateInXHalfMoves": "{count, plural, =1{Matto in {count} semi-mossa} other{Matto in {count} semi-mosse}}", "nbBlunders": "{count, plural, =1{{count} errore grave} other{{count} errori gravi}}", @@ -1510,6 +1543,7 @@ "studyPlayAgain": "Gioca di nuovo", "studyWhatWouldYouPlay": "Cosa giocheresti in questa posizione?", "studyYouCompletedThisLesson": "Congratulazioni! Hai completato questa lezione.", + "studyPerPage": "{param} per pagina", "studyNbChapters": "{count, plural, =1{{count} capitolo} other{{count} capitoli}}", "studyNbGames": "{count, plural, =1{{count} partita} other{{count} partite}}", "studyNbMembers": "{count, plural, =1{{count} membro} other{{count} membri}}", diff --git a/lib/l10n/lila_ja.arb b/lib/l10n/lila_ja.arb index 3aa1a04e0e..c22e797a86 100644 --- a/lib/l10n/lila_ja.arb +++ b/lib/l10n/lila_ja.arb @@ -1,47 +1,47 @@ { + "mobileAllGames": "すべて", + "mobileAreYouSure": "本当にいいですか?", + "mobileBlindfoldMode": "めかくしモード", + "mobileCancelTakebackOffer": "待ったをキャンセル", + "mobileClearButton": "クリア", + "mobileCorrespondenceClearSavedMove": "保存した手を削除", + "mobileCustomGameJoinAGame": "ゲームに参加", + "mobileFeedbackButton": "フィードバック", + "mobileGreeting": "こんにちは {param} さん", + "mobileGreetingWithoutName": "こんにちは", + "mobileHideVariation": "変化手順を隠す", "mobileHomeTab": "ホーム", - "mobilePuzzlesTab": "問題", - "mobileToolsTab": "ツール", - "mobileWatchTab": "見る", - "mobileSettingsTab": "設定", + "mobileLiveStreamers": "ライブ配信者", "mobileMustBeLoggedIn": "このページを見るにはログインが必要です。", - "mobileSystemColors": "OS と同じ色設定", - "mobileFeedbackButton": "フィードバック", + "mobileNoSearchResults": "検索結果なし", + "mobileNotFollowingAnyUser": "誰もフォローしていません。", "mobileOkButton": "OK", + "mobilePlayersMatchingSearchTerm": "「{param}」を含むプレイヤー", + "mobilePrefMagnifyDraggedPiece": "ドラッグ中の駒を拡大", + "mobilePuzzleStormConfirmEndRun": "このストームを終了しますか?", + "mobilePuzzleStormFilterNothingToShow": "条件に合う問題がありません。フィルターを変更してください", + "mobilePuzzleStormNothingToShow": "データがありません。まず問題ストームをプレイして。", + "mobilePuzzleStormSubtitle": "3 分間でできるだけ多くの問題を解いてください。", + "mobilePuzzleStreakAbortWarning": "現在の連続正解が終わり、スコアが保存されます。", + "mobilePuzzleThemesSubtitle": "お気に入りのオープニングやテーマの問題が選べます。", + "mobilePuzzlesTab": "問題", + "mobileRecentSearches": "最近の検索", "mobileSettingsHapticFeedback": "振動フィードバック", "mobileSettingsImmersiveMode": "没入モード", "mobileSettingsImmersiveModeSubtitle": "対局中にシステム用の UI を隠します。画面端のナビゲーションなどがじゃまな人はこれを使ってください。対局と問題ストームの画面に適用されます。", - "mobileNotFollowingAnyUser": "誰もフォローしていません。", - "mobileAllGames": "すべて", - "mobileRecentSearches": "最近の検索", - "mobileClearButton": "クリア", - "mobilePlayersMatchingSearchTerm": "「{param}」を含むプレイヤー", - "mobileNoSearchResults": "検索結果なし", - "mobileAreYouSure": "本当にいいですか?", - "mobilePuzzleStreakAbortWarning": "現在の連続正解が終わり、スコアが保存されます。", - "mobilePuzzleStormNothingToShow": "データがありません。まず問題ストームをプレイして。", - "mobileSharePuzzle": "この問題を共有する", - "mobileShareGameURL": "ゲーム URLを共有", + "mobileSettingsTab": "設定", "mobileShareGamePGN": "PGN を共有", + "mobileShareGameURL": "ゲーム URLを共有", "mobileSharePositionAsFEN": "局面を FEN で共有", - "mobileShowVariations": "変化手順を表示", - "mobileHideVariation": "変化手順を隠す", + "mobileSharePuzzle": "この問題を共有する", "mobileShowComments": "コメントを表示", - "mobilePuzzleStormConfirmEndRun": "このストームを終了しますか?", - "mobilePuzzleStormFilterNothingToShow": "条件に合う問題がありません。フィルターを変更してください", - "mobileCancelTakebackOffer": "待ったをキャンセル", - "mobileWaitingForOpponentToJoin": "対戦相手の参加を待っています…", - "mobileBlindfoldMode": "めかくしモード", - "mobileLiveStreamers": "ライブ配信者", - "mobileCustomGameJoinAGame": "ゲームに参加", - "mobileCorrespondenceClearSavedMove": "保存した手を削除", - "mobileSomethingWentWrong": "問題が発生しました。", "mobileShowResult": "結果を表示", - "mobilePuzzleThemesSubtitle": "お気に入りのオープニングやテーマの問題が選べます。", - "mobilePuzzleStormSubtitle": "3 分間でできるだけ多くの問題を解いてください。", - "mobileGreeting": "こんにちは {param} さん", - "mobileGreetingWithoutName": "こんにちは", - "mobilePrefMagnifyDraggedPiece": "ドラッグ中の駒を拡大", + "mobileShowVariations": "変化手順を表示", + "mobileSomethingWentWrong": "問題が発生しました。", + "mobileSystemColors": "OS と同じ色設定", + "mobileToolsTab": "ツール", + "mobileWaitingForOpponentToJoin": "対戦相手の参加を待っています…", + "mobileWatchTab": "見る", "activityActivity": "活動", "activityHostedALiveStream": "ライブ配信", "activityRankedInSwissTournament": "{param1} 位({param2})", @@ -121,6 +121,7 @@ "broadcastNotYetStarted": "中継はまだ始まっていません。", "broadcastOfficialWebsite": "公式サイト", "broadcastStandings": "順位", + "broadcastOfficialStandings": "公式順位", "broadcastIframeHelp": "他のオプションは {param} にあります", "broadcastWebmastersPage": "ウェブ管理者のページ", "broadcastPgnSourceHelp": "このラウンドについて公表されたリアルタイムの PGN です。{param} も利用でき、高速かつ高効率の同期が行なえます。", @@ -129,6 +130,15 @@ "broadcastRatingDiff": "レーティングの差", "broadcastGamesThisTournament": "このトーナメントの対局", "broadcastScore": "スコア", + "broadcastAllTeams": "すべてのチーム", + "broadcastTournamentFormat": "トーナメント形式", + "broadcastTournamentLocation": "開催地", + "broadcastTopPlayers": "トッププレイヤー", + "broadcastTimezone": "タイムゾーン", + "broadcastFideRatingCategory": "FIDE レーティング カテゴリー", + "broadcastOptionalDetails": "その他詳細(オプション)", + "broadcastPastBroadcasts": "過去の中継", + "broadcastAllBroadcastsByMonth": "すべての中継を月別に表示", "broadcastNbBroadcasts": "{count, plural, other{{count} ブロードキャスト}}", "challengeChallengesX": "チャレンジ:{param1}", "challengeChallengeToPlay": "対局を申し込む", @@ -253,6 +263,7 @@ "preferencesNotifyWeb": "ブラウザ", "preferencesNotifyDevice": "デバイス", "preferencesBellNotificationSound": "ベル通知の音", + "preferencesBlindfold": "めかくしモード", "puzzlePuzzles": "タクティクス問題", "puzzlePuzzleThemes": "問題のテーマ", "puzzleRecommended": "おすすめ", @@ -570,7 +581,6 @@ "replayMode": "再現の方式", "realtimeReplay": "リアルタイム", "byCPL": "評価値で", - "openStudy": "研究を開く", "enable": "解析する", "bestMoveArrow": "最善手を表示", "showVariationArrows": "変化手順の矢印を表示", @@ -778,7 +788,6 @@ "block": "ブロックする", "blocked": "ブロック済", "unblock": "ブロックを外す", - "followsYou": "あなたをフォローしています", "xStartedFollowingY": "{param1} が {param2} のフォローを開始", "more": "さらに表示", "memberSince": "登録日", @@ -1531,6 +1540,7 @@ "studyPlayAgain": "もう一度プレイ", "studyWhatWouldYouPlay": "この局面、あなたならどう指す?", "studyYouCompletedThisLesson": "おめでとう ! このレッスンを修了しました。", + "studyPerPage": "{param} 件/ページ", "studyNbChapters": "{count, plural, other{{count} 章}}", "studyNbGames": "{count, plural, other{{count} 局}}", "studyNbMembers": "{count, plural, other{{count} メンバー}}", diff --git a/lib/l10n/lila_kk.arb b/lib/l10n/lila_kk.arb index 590624babb..4a083b5256 100644 --- a/lib/l10n/lila_kk.arb +++ b/lib/l10n/lila_kk.arb @@ -1,33 +1,33 @@ { + "mobileAllGames": "Барлық ойындар", + "mobileAreYouSure": "Растайсыз ба?", + "mobileClearButton": "Өшіру", + "mobileFeedbackButton": "Пікір айту", + "mobileGreeting": "Ассаламу ғалейкүм, {param}", + "mobileGreetingWithoutName": "Ассаламу ғалейкүм", "mobileHomeTab": "Үйге", - "mobilePuzzlesTab": "Жұмбақ", - "mobileToolsTab": "Құрал", - "mobileWatchTab": "Бақылау", - "mobileSettingsTab": "Баптау", "mobileMustBeLoggedIn": "Бұл бетті көру үшін тіркелгіге кіріңіз.", - "mobileSystemColors": "Жүйе түстері", - "mobileFeedbackButton": "Пікір айту", + "mobileNoSearchResults": "Нәтиже жоқ", + "mobileNotFollowingAnyUser": "Сіз әзір ешкіге серік емессіз.", "mobileOkButton": "Иә", + "mobilePlayersMatchingSearchTerm": "Атауында \"{param}\" бар ойыншылар", + "mobilePuzzleStormNothingToShow": "Нәтиже әзір жоқ. Жұмбақ Дауылын ойнап көріңіз.", + "mobilePuzzleStormSubtitle": "3 минутта барынша көп жұмбақ шешіп көр.", + "mobilePuzzleStreakAbortWarning": "Қазіргі тізбектен айрыласыз, нәтиже сақталады.", + "mobilePuzzleThemesSubtitle": "Өз бастауларыңызға негізделген жұмбақтар, не кез-келген тақырып.", + "mobilePuzzlesTab": "Жұмбақ", + "mobileRecentSearches": "Кейінгі іздеулер", "mobileSettingsHapticFeedback": "Дірілмен білдіру", "mobileSettingsImmersiveMode": "Оқшау көрініс", "mobileSettingsImmersiveModeSubtitle": "Ойын кезінде жүйенің элементтерін жасыру. Экран жиегіндегі жүйенің навигация қимыл белгілері сізге кедергі келтірсе - қолданарлық жағдай. Ойын мен Жұмбақ Дауылы кезінде жұмыс істейді.", - "mobileNotFollowingAnyUser": "Сіз әзір ешкіге серік емессіз.", - "mobileAllGames": "Барлық ойындар", - "mobileRecentSearches": "Кейінгі іздеулер", - "mobileClearButton": "Өшіру", - "mobilePlayersMatchingSearchTerm": "Атауында \"{param}\" бар ойыншылар", - "mobileNoSearchResults": "Нәтиже жоқ", - "mobileAreYouSure": "Растайсыз ба?", - "mobilePuzzleStreakAbortWarning": "Қазіргі тізбектен айрыласыз, нәтиже сақталады.", - "mobilePuzzleStormNothingToShow": "Нәтиже әзір жоқ. Жұмбақ Дауылын ойнап көріңіз.", - "mobileSharePuzzle": "Бұл жұмбақты тарату", - "mobileShareGameURL": "Ойын сілтемесін тарату", + "mobileSettingsTab": "Баптау", "mobileShareGamePGN": "PGN тарату", + "mobileShareGameURL": "Ойын сілтемесін тарату", + "mobileSharePuzzle": "Бұл жұмбақты тарату", "mobileShowResult": "Нәтижесін көру", - "mobilePuzzleThemesSubtitle": "Өз бастауларыңызға негізделген жұмбақтар, не кез-келген тақырып.", - "mobilePuzzleStormSubtitle": "3 минутта барынша көп жұмбақ шешіп көр.", - "mobileGreeting": "Ассаламу ғалейкүм, {param}", - "mobileGreetingWithoutName": "Ассаламу ғалейкүм", + "mobileSystemColors": "Жүйе түстері", + "mobileToolsTab": "Құрал", + "mobileWatchTab": "Бақылау", "activityActivity": "Белсенділігі", "activityHostedALiveStream": "Стрим бастады", "activityRankedInSwissTournament": "{param2}-да {param1}-нші орында", @@ -511,7 +511,6 @@ "replayMode": "Ойнату тәртібі", "realtimeReplay": "Өз қарқыны", "byCPL": "CPL сәйкес", - "openStudy": "Зерттеуді ашу", "enable": "Қосу", "bestMoveArrow": "Үздік жүрісті нұсқағыш", "showVariationArrows": "Тармақта нұсқағышты көрсету", @@ -714,7 +713,6 @@ "block": "Бұғаттау", "blocked": "Бұғатталған", "unblock": "Бұғаттан шығару", - "followsYou": "Сізге серік", "xStartedFollowingY": "{param1} {param2} серігі болды", "more": "Жаю", "memberSince": "Тіркелген күні", diff --git a/lib/l10n/lila_ko.arb b/lib/l10n/lila_ko.arb index 969f8a6181..7c83b06ed5 100644 --- a/lib/l10n/lila_ko.arb +++ b/lib/l10n/lila_ko.arb @@ -1,47 +1,48 @@ { + "mobileAllGames": "모든 대국", + "mobileAreYouSure": "확실하십니까?", + "mobileBlindfoldMode": "기물 가리기", + "mobileCancelTakebackOffer": "무르기 요청 취소", + "mobileClearButton": "지우기", + "mobileCorrespondenceClearSavedMove": "저장된 수 삭제", + "mobileCustomGameJoinAGame": "게임 참가", + "mobileFeedbackButton": "피드백", + "mobileGreeting": "안녕하세요, {param}", + "mobileGreetingWithoutName": "안녕하세요", + "mobileHideVariation": "바리에이션 숨기기", "mobileHomeTab": "홈", - "mobilePuzzlesTab": "퍼즐", - "mobileToolsTab": "도구", - "mobileWatchTab": "관람", - "mobileSettingsTab": "설정", + "mobileLiveStreamers": "방송 중인 스트리머", "mobileMustBeLoggedIn": "이 페이지를 보려면 로그인해야 합니다.", - "mobileSystemColors": "시스템 색상", - "mobileFeedbackButton": "피드백", + "mobileNoSearchResults": "결과 없음", + "mobileNotFollowingAnyUser": "팔로우한 사용자가 없습니다.", "mobileOkButton": "확인", + "mobilePlayersMatchingSearchTerm": "닉네임에 \"{param}\"가 포함된 플레이어", + "mobilePrefMagnifyDraggedPiece": "드래그한 기물 확대하기", + "mobilePuzzleStormConfirmEndRun": "이 도전을 종료하시겠습니까?", + "mobilePuzzleStormFilterNothingToShow": "표시할 것이 없습니다. 필터를 변경해 주세요", + "mobilePuzzleStormNothingToShow": "표시할 것이 없습니다. 먼저 퍼즐 스톰을 플레이하세요.", + "mobilePuzzleStormSubtitle": "3분 이내에 최대한 많은 퍼즐을 해결하십시오.", + "mobilePuzzleStreakAbortWarning": "현재 연속 해결 기록을 잃고 점수는 저장됩니다.", + "mobilePuzzleThemesSubtitle": "당신이 가장 좋아하는 오프닝으로부터의 퍼즐을 플레이하거나, 테마를 선택하십시오.", + "mobilePuzzlesTab": "퍼즐", + "mobileRecentSearches": "최근 검색어", "mobileSettingsHapticFeedback": "터치 시 진동", "mobileSettingsImmersiveMode": "전체 화면 모드", "mobileSettingsImmersiveModeSubtitle": "플레이 중 시스템 UI를 숨깁니다. 화면 가장자리의 시스템 내비게이션 제스처가 불편하다면 사용하세요. 대국과 퍼즐 스톰 화면에서 적용됩니다.", - "mobileNotFollowingAnyUser": "팔로우한 사용자가 없습니다.", - "mobileAllGames": "모든 대국", - "mobileRecentSearches": "최근 검색어", - "mobileClearButton": "지우기", - "mobilePlayersMatchingSearchTerm": "닉네임에 \"{param}\"가 포함된 플레이어", - "mobileNoSearchResults": "결과 없음", - "mobileAreYouSure": "확실하십니까?", - "mobilePuzzleStreakAbortWarning": "현재 연속 해결 기록을 잃고 점수는 저장됩니다.", - "mobilePuzzleStormNothingToShow": "표시할 것이 없습니다. 먼저 퍼즐 스톰을 플레이하세요.", - "mobileSharePuzzle": "이 퍼즐 공유", - "mobileShareGameURL": "게임 URL 공유", + "mobileSettingsTab": "설정", "mobileShareGamePGN": "PGN 공유", + "mobileShareGameURL": "게임 URL 공유", "mobileSharePositionAsFEN": "FEN으로 공유", - "mobileShowVariations": "바리에이션 보이기", - "mobileHideVariation": "바리에이션 숨기기", + "mobileSharePuzzle": "이 퍼즐 공유", "mobileShowComments": "댓글 보기", - "mobilePuzzleStormConfirmEndRun": "이 도전을 종료하시겠습니까?", - "mobilePuzzleStormFilterNothingToShow": "표시할 것이 없습니다. 필터를 변경해 주세요", - "mobileCancelTakebackOffer": "무르기 요청 취소", - "mobileWaitingForOpponentToJoin": "상대 참가를 기다리는 중...", - "mobileBlindfoldMode": "기물 가리기", - "mobileLiveStreamers": "방송 중인 스트리머", - "mobileCustomGameJoinAGame": "게임 참가", - "mobileCorrespondenceClearSavedMove": "저장된 수 삭제", - "mobileSomethingWentWrong": "문제가 발생했습니다.", "mobileShowResult": "결과 표시", - "mobilePuzzleThemesSubtitle": "당신이 가장 좋아하는 오프닝으로부터의 퍼즐을 플레이하거나, 테마를 선택하십시오.", - "mobilePuzzleStormSubtitle": "3분 이내에 최대한 많은 퍼즐을 해결하십시오.", - "mobileGreeting": "안녕하세요, {param}", - "mobileGreetingWithoutName": "안녕하세요", - "mobilePrefMagnifyDraggedPiece": "드래그한 기물 확대하기", + "mobileShowVariations": "바리에이션 보이기", + "mobileSomethingWentWrong": "문제가 발생했습니다.", + "mobileSystemColors": "시스템 색상", + "mobileTheme": "테마", + "mobileToolsTab": "도구", + "mobileWaitingForOpponentToJoin": "상대 참가를 기다리는 중...", + "mobileWatchTab": "관람", "activityActivity": "활동", "activityHostedALiveStream": "라이브 스트리밍을 함", "activityRankedInSwissTournament": "{param2}에서 {param1}등", @@ -52,14 +53,14 @@ "activityPlayedNbGames": "{count, plural, other{총 {count} 회의 {param2} 게임을 하였습니다.}}", "activityPostedNbMessages": "{count, plural, other{{param2} 에 총 {count} 개의 글을 게시하였습니다.}}", "activityPlayedNbMoves": "{count, plural, other{수 {count}개를 둠}}", - "activityInNbCorrespondenceGames": "{count, plural, other{{count}개의 통신전에서}}", - "activityCompletedNbGames": "{count, plural, other{{count} 번의 통신전을 완료하셨습니다.}}", - "activityCompletedNbVariantGames": "{count, plural, other{{count} {param2} 긴 대국전을 완료함}}", + "activityInNbCorrespondenceGames": "{count, plural, other{{count}개의 통신 대국에서}}", + "activityCompletedNbGames": "{count, plural, other{{count}번의 통신 대국을 완료함}}", + "activityCompletedNbVariantGames": "{count, plural, other{{count}번의 {param2} 통신 대국을 완료함}}", "activityFollowedNbPlayers": "{count, plural, other{{count} 명을 팔로우 개시}}", "activityGainedNbFollowers": "{count, plural, other{{count} 명의 신규 팔로워를 얻음}}", "activityHostedNbSimuls": "{count, plural, other{{count} 번의 동시대국을 주최함}}", "activityJoinedNbSimuls": "{count, plural, other{{count} 번의 동시대국에 참가함}}", - "activityCreatedNbStudies": "{count, plural, other{공부 {count}개 작성함}}", + "activityCreatedNbStudies": "{count, plural, other{새 연구 {count}개 작성함}}", "activityCompetedInNbTournaments": "{count, plural, other{{count} 번 토너먼트에 참가함}}", "activityRankedInTournament": "{count, plural, other{{count} 위 (상위 {param2}%) ({param4} 에서 {param3} 국)}}", "activityCompetedInNbSwissTournaments": "{count, plural, other{{count} 번 토너먼트에 참가함}}", @@ -71,7 +72,7 @@ "broadcastNewBroadcast": "새 실시간 방송", "broadcastSubscribedBroadcasts": "구독 중인 방송", "broadcastAboutBroadcasts": "방송에 대해서", - "broadcastHowToUseLichessBroadcasts": "리체스 방송을 사용하는 방법.", + "broadcastHowToUseLichessBroadcasts": "Lichess 방송을 사용하는 방법.", "broadcastTheNewRoundHelp": "새로운 라운드에는 이전 라운드와 동일한 구성원과 기여자가 있을 것입니다.", "broadcastAddRound": "라운드 추가", "broadcastOngoing": "진행중", @@ -95,7 +96,7 @@ "broadcastDeleteRound": "라운드 삭제", "broadcastDefinitivelyDeleteRound": "라운드와 해당 게임을 완전히 삭제합니다.", "broadcastDeleteAllGamesOfThisRound": "이 라운드의 모든 게임을 삭제합니다. 다시 생성하려면 소스가 활성화되어 있어야 합니다.", - "broadcastEditRoundStudy": "경기 공부 편집", + "broadcastEditRoundStudy": "경기 연구 편집", "broadcastDeleteTournament": "이 토너먼트 삭제", "broadcastDefinitivelyDeleteTournament": "토너먼트 전체의 모든 라운드와 게임을 완전히 삭제합니다.", "broadcastShowScores": "게임 결과에 따라 플레이어 점수 표시", @@ -122,14 +123,24 @@ "broadcastNotYetStarted": "아직 방송이 시작을 하지 않았습니다.", "broadcastOfficialWebsite": "공식 웹사이트", "broadcastStandings": "순위", + "broadcastOfficialStandings": "공식 순위", "broadcastIframeHelp": "{param}에서 더 많은 정보를 확인하실 수 있습니다", "broadcastWebmastersPage": "웹마스터 페이지", "broadcastPgnSourceHelp": "이 라운드의 공개된, 실시간 PGN 소스 입니다. 보다 더 빠르고 효율적인 동기화를 위해 {param}도 제공됩니다.", "broadcastEmbedThisBroadcast": "이 방송을 웹사이트에 삽입하세요", - "broadcastEmbedThisRound": "{param}을(를) 웹사이트에 삼입하세요", + "broadcastEmbedThisRound": "{param}을(를) 웹사이트에 삽입하세요", "broadcastRatingDiff": "레이팅 차이", "broadcastGamesThisTournament": "이 토너먼트의 게임들", "broadcastScore": "점수", + "broadcastAllTeams": "모든 팀", + "broadcastTournamentFormat": "토너먼트 형식", + "broadcastTournamentLocation": "토너먼트 장소", + "broadcastTopPlayers": "상위 플레이어들", + "broadcastTimezone": "시간대", + "broadcastFideRatingCategory": "FIDE 레이팅 범주", + "broadcastOptionalDetails": "선택적 세부 정보", + "broadcastPastBroadcasts": "과거 방송들", + "broadcastAllBroadcastsByMonth": "월별 방송들 모두 보기", "broadcastNbBroadcasts": "{count, plural, other{{count} 방송}}", "challengeChallengesX": "도전: {param1}", "challengeChallengeToPlay": "도전 신청", @@ -144,9 +155,9 @@ "challengeXOnlyAcceptsChallengesFromFriends": "{param}님은 친구인 상대만 도전을 받아들입니다.", "challengeDeclineGeneric": "지금 도전을 받지 않습니다.", "challengeDeclineLater": "시간이 맞지 않습니다. 나중에 다시 요청해주세요.", - "challengeDeclineTooFast": "시간이 너무 짧습니다. 더 긴 게임으로 신청해주세요.", - "challengeDeclineTooSlow": "시간이 너무 깁니다. 더 빠른 게임으로 신청해주세요.", - "challengeDeclineTimeControl": "이 시간으로는 도전을 받지 않습니다.", + "challengeDeclineTooFast": "시간이 너무 짧습니다. 더 느린 게임으로 신청해주세요.", + "challengeDeclineTooSlow": "시간이 너무 깁니다. 더 빠른 게임으로 다시 신청해주세요.", + "challengeDeclineTimeControl": "이 시간 제한으로는 도전을 받지 않겠습니다.", "challengeDeclineRated": "대신 레이팅 대전을 신청해주세요.", "challengeDeclineCasual": "대신 캐주얼 대전을 신청해주세요.", "challengeDeclineStandard": "지금은 변형 체스 도전을 받지 않고 있습니다.", @@ -189,20 +200,20 @@ "perfStatNow": "지금", "preferencesPreferences": "설정", "preferencesDisplay": "화면", - "preferencesPrivacy": "프라이버시", - "preferencesNotifications": "공지 사항", + "preferencesPrivacy": "보안", + "preferencesNotifications": "알림", "preferencesPieceAnimation": "기물 움직임 애니메이션", "preferencesMaterialDifference": "기물 차이", "preferencesBoardHighlights": "보드 하이라이트 (마지막 수 및 체크)", "preferencesPieceDestinations": "기물 착지점 (유효한 움직임 및 미리두기)", "preferencesBoardCoordinates": "보드 좌표 (A-H, 1-8)", - "preferencesMoveListWhilePlaying": "피스 움직임 기록", + "preferencesMoveListWhilePlaying": "기물 움직임 기록", "preferencesPgnPieceNotation": "PGN 기물표기방식", - "preferencesChessPieceSymbol": "체스 말 기호", + "preferencesChessPieceSymbol": "체스 기물 기호", "preferencesPgnLetter": "알파벳 (K, Q, R, B, N)", "preferencesZenMode": "젠 모드", "preferencesShowPlayerRatings": "플레이어 레이팅 보기", - "preferencesShowFlairs": "플레이어 레이팅 보기", + "preferencesShowFlairs": "플레이어 아이콘 보기", "preferencesExplainShowPlayerRatings": "체스에 집중할 수 있도록 웹사이트에서 레이팅을 모두 숨깁니다. 경기는 여전히 레이팅에 반영될 것이며, 눈으로 보이는 정보에만 영향을 줍니다.", "preferencesDisplayBoardResizeHandle": "보드 크기 재조정 핸들 보이기", "preferencesOnlyOnInitialPosition": "초기 상태에서만", @@ -218,21 +229,21 @@ "preferencesClickTwoSquares": "현재 위치와 원하는 위치에 클릭하기", "preferencesDragPiece": "드래그", "preferencesBothClicksAndDrag": "아무 방법으로", - "preferencesPremovesPlayingDuringOpponentTurn": "미리두기 (상대 턴일 때 수를 두기)", + "preferencesPremovesPlayingDuringOpponentTurn": "미리두기 (상대 차례일 때 수를 두기)", "preferencesTakebacksWithOpponentApproval": "무르기 (상대 승인과 함께)", "preferencesInCasualGamesOnly": "캐주얼 모드에서만", "preferencesPromoteToQueenAutomatically": "퀸으로 자동 승진", "preferencesExplainPromoteToQueenAutomatically": "일시적으로 자동 승진을 끄기 위해 승진하는 동안 를 누르세요", - "preferencesWhenPremoving": "미리둘 때만", + "preferencesWhenPremoving": "미리두기 때만", "preferencesClaimDrawOnThreefoldRepetitionAutomatically": "3회 동형반복시 자동으로 무승부 요청", "preferencesWhenTimeRemainingLessThanThirtySeconds": "남은 시간이 30초 미만일 때만", - "preferencesMoveConfirmation": "피스를 움직이기 전에 물음", + "preferencesMoveConfirmation": "수 확인", "preferencesExplainCanThenBeTemporarilyDisabled": "경기 도중 보드 메뉴에서 비활성화될 수 있습니다.", - "preferencesInCorrespondenceGames": "통신전", - "preferencesCorrespondenceAndUnlimited": "통신과 무제한", + "preferencesInCorrespondenceGames": "통신 대국", + "preferencesCorrespondenceAndUnlimited": "통신 대국과 무제한", "preferencesConfirmResignationAndDrawOffers": "기권 또는 무승부 제안시 물음", "preferencesCastleByMovingTheKingTwoSquaresOrOntoTheRook": "캐슬링 방법", - "preferencesCastleByMovingTwoSquares": "왕을 2칸 옮기기", + "preferencesCastleByMovingTwoSquares": "킹을 2칸 옮기기", "preferencesCastleByMovingOntoTheRook": "킹을 룩한테 이동", "preferencesInputMovesWithTheKeyboard": "키보드 입력", "preferencesInputMovesWithVoice": "음성으로 기물 이동", @@ -244,16 +255,17 @@ "preferencesNotifyStreamStart": "스트리머가 생방송 시작", "preferencesNotifyInboxMsg": "새로운 받은 편지함 메시지", "preferencesNotifyForumMention": "포럼 댓글에서 당신이 언급됨", - "preferencesNotifyInvitedStudy": "스터디 초대", - "preferencesNotifyGameEvent": "통신전 업데이트", + "preferencesNotifyInvitedStudy": "연구 초대", + "preferencesNotifyGameEvent": "통신 대국 업데이트", "preferencesNotifyChallenge": "도전 과제", "preferencesNotifyTournamentSoon": "곧 토너먼트 시작할 때", - "preferencesNotifyTimeAlarm": "통신전 시간 곧 만료됨", - "preferencesNotifyBell": "리체스 내에서 벨 알림", - "preferencesNotifyPush": "리체스를 사용하지 않을 때 기기 알림", + "preferencesNotifyTimeAlarm": "통신 대국 시간 곧 만료됨", + "preferencesNotifyBell": "Lichess 내에서 벨 알림", + "preferencesNotifyPush": "Lichess를 사용하지 않을 때 기기 알림", "preferencesNotifyWeb": "브라우저", "preferencesNotifyDevice": "기기 정보", "preferencesBellNotificationSound": "벨 알림 음", + "preferencesBlindfold": "기물 가리기", "puzzlePuzzles": "퍼즐", "puzzlePuzzleThemes": "퍼즐 테마", "puzzleRecommended": "추천", @@ -270,7 +282,7 @@ "puzzleUpVote": "퍼즐 추천", "puzzleDownVote": "퍼즐 비추천", "puzzleYourPuzzleRatingWillNotChange": "당신의 퍼즐 레이팅은 바뀌지 않을 것입니다. 퍼즐은 경쟁이 아니라는 걸 기억하세요. 레이팅은 당신의 현재 수준에 맞는 퍼즐을 선택하도록 돕습니다.", - "puzzleFindTheBestMoveForWhite": "백의 최고의 수를 찾아보세요.", + "puzzleFindTheBestMoveForWhite": "백의 최선 수를 찾아보세요.", "puzzleFindTheBestMoveForBlack": "흑의 최선 수를 찾아보세요.", "puzzleToGetPersonalizedPuzzles": "개인화된 퍼즐을 위해선:", "puzzlePuzzleId": "퍼즐 {param}", @@ -505,15 +517,15 @@ "forceDraw": "무승부 선언", "talkInChat": "건전한 채팅을 해주세요!", "theFirstPersonToComeOnThisUrlWillPlayWithYou": "이 URL로 가장 먼저 들어온 사람과 체스를 두게 됩니다.", - "whiteResigned": "백 기권함", - "blackResigned": "흑 기권함", + "whiteResigned": "백이 기권하였습니다", + "blackResigned": "흑이 기권하였습니다", "whiteLeftTheGame": "백이 게임을 나갔습니다", "blackLeftTheGame": "흑이 게임을 나갔습니다", - "whiteDidntMove": "백이 두지 않음", + "whiteDidntMove": "백이 수를 두지 않음", "blackDidntMove": "흑이 수를 두지 않음", "requestAComputerAnalysis": "컴퓨터 분석 요청하기", "computerAnalysis": "컴퓨터 분석", - "computerAnalysisAvailable": "컴퓨터 분석이 가능합니다.", + "computerAnalysisAvailable": "컴퓨터 분석 가능", "computerAnalysisDisabled": "컴퓨터 분석 비활성화됨", "analysis": "분석", "depthX": "{param} 수까지 탐색", @@ -525,14 +537,14 @@ "goDeeper": "더 깊게 분석하기", "showThreat": "위험요소 표시", "inLocalBrowser": "브라우저에서", - "toggleLocalEvaluation": "로컬 분석 토글", - "promoteVariation": "변형 승격", + "toggleLocalEvaluation": "로컬 분석 전환", + "promoteVariation": "바리에이션 승격하기", "makeMainLine": "주 라인으로 하기", "deleteFromHere": "여기서부터 삭제", "collapseVariations": "바리에이션 축소하기", "expandVariations": "바리에이션 확장하기", - "forceVariation": "변화 강제하기", - "copyVariationPgn": "변동 PGN 복사", + "forceVariation": "바리에이션 강제하기", + "copyVariationPgn": "바리에이션 PGN 복사", "move": "수", "variantLoss": "변형 체스에서 패배", "variantWin": "변형 체스에서 승리", @@ -571,7 +583,6 @@ "replayMode": "게임 다시보기", "realtimeReplay": "실시간", "byCPL": "센티폰 손실", - "openStudy": "연구를 시작하기", "enable": "활성화", "bestMoveArrow": "최선의 수 화살표", "showVariationArrows": "바리에이션 화살표 표시하기", @@ -602,7 +613,7 @@ "computersAreNotAllowedToPlay": "컴퓨터나 컴퓨터 지원을 받는 플레이어들은 게임 참가가 금지되어 있습니다. 게임 중 체스 엔진이나, 데이터베이스나, 주변 플레이어들로부터 도움을 받지 마십시오. 이와 더불어 다중 계정 소유는 권장하지 않으며 지나치게 많은 계정들을 사용할 시 계정들이 차단될 수 있습니다.", "games": "게임", "forum": "포럼", - "xPostedInForumY": "{param1}(이)가 {param2} 쓰레드에 글을 씀", + "xPostedInForumY": "{param1}(이)가 {param2} 주제에 글을 씀", "latestForumPosts": "최근 포럼 글", "players": "플레이어", "friends": "친구들", @@ -610,18 +621,18 @@ "discussions": "대화", "today": "오늘", "yesterday": "어제", - "minutesPerSide": "양쪽 시간(분)", + "minutesPerSide": "제한 시간(분)", "variant": "게임 종류", "variants": "변형", - "timeControl": "제한 시간(분)", - "realTime": "차례 없음", - "correspondence": "긴 대국", + "timeControl": "시간 제한", + "realTime": "실시간", + "correspondence": "통신 대국", "daysPerTurn": "수당 일수", "oneDay": "1일", "time": "시간", "rating": "레이팅", "ratingStats": "레이팅 통계", - "username": "유저네임", + "username": "사용자 이름", "usernameOrEmail": "사용자 이름이나 이메일 주소", "changeUsername": "사용자 이름 변경", "changeUsernameNotSame": "글자의 대소문자 변경만 가능합니다 예: \"chulsoo\"에서 \"ChulSoo\"로.", @@ -662,6 +673,7 @@ "rank": "순위", "rankX": "순위: {param}등", "gamesPlayed": "게임", + "ok": "확인", "cancel": "취소", "whiteTimeOut": "백 시간 초과", "blackTimeOut": "흑 시간 초과", @@ -758,7 +770,7 @@ "toStudy": "연구", "importGame": "게임 불러오기", "importGameExplanation": "게임의 PGN 을 붙여넣으면, 브라우저에서의 리플레이, 컴퓨터 해석, 게임챗, 공유가능 URL을 얻습니다.", - "importGameCaveat": "변형은 지워집니다. 변형을 유지하려면 스터디를 통해 PGN을 가져오세요.", + "importGameCaveat": "변형은 지워집니다. 변형을 유지하려면 연구를 통해 PGN을 가져오세요.", "importGameDataPrivacyWarning": "이 PGN은 모두가 볼 수 있게 됩니다. 비공개로 게임을 불러오려면, 연구 기능을 이용하세요.", "thisIsAChessCaptcha": "자동 기입을 방지하기 위한 체스 퀴즈입니다.", "clickOnTheBoardToMakeYourMove": "보드를 클릭해서 체스 퍼즐을 풀고 당신이 사람임을 알려주세요.", @@ -778,7 +790,6 @@ "block": "차단", "blocked": "차단됨", "unblock": "차단 해제", - "followsYou": "팔로워", "xStartedFollowingY": "{param1}(이)가 {param2}(을)를 팔로우했습니다", "more": "더보기", "memberSince": "가입 시기:", @@ -847,7 +858,7 @@ "socialMediaLinks": "소셜 미디어 링크", "oneUrlPerLine": "한 줄에 당 URL 1개", "inlineNotation": "기보법 가로쓰기", - "makeAStudy": "안전하게 보관하고 공유하려면 공부를 만들어 보세요.", + "makeAStudy": "안전하게 보관하고 공유하려면 연구를 만들어 보세요.", "clearSavedMoves": "저장된 움직임 삭제", "previouslyOnLichessTV": "이전 방송", "onlinePlayers": "접속한 플레이어", @@ -911,7 +922,7 @@ "privacyPolicy": "개인정보취급방침", "letOtherPlayersFollowYou": "다른 사람이 팔로우할 수 있게 함", "letOtherPlayersChallengeYou": "다른 사람이 나에게 도전할 수 있게 함", - "letOtherPlayersInviteYouToStudy": "다른 플레이어들이 나를 학습에 초대할 수 있음", + "letOtherPlayersInviteYouToStudy": "다른 플레이어들이 나를 연구에 초대할 수 있음", "sound": "소리", "none": "없음", "fast": "빠르게", @@ -940,7 +951,7 @@ "clock": "시계", "opponent": "상대", "learnMenu": "배우기", - "studyMenu": "공부", + "studyMenu": "연구", "practice": "연습", "community": "커뮤니티", "tools": "도구", @@ -1026,7 +1037,7 @@ "variationArrowsInfo": "변형 화살표를 사용하면 이동 목록을 사용하지 않고 탐색이 가능합니다.", "playSelectedMove": "선택한 수 두기", "newTournament": "새로운 토너먼트", - "tournamentHomeTitle": "다양한 제한시간과 게임방식을 지원하는 체스 토너먼트", + "tournamentHomeTitle": "다양한 시간 제한과 변형을 지원하는 체스 토너먼트", "tournamentHomeDescription": "빠른 체스 토너먼트를 즐겨 보세요! 공식 일정이 잡힌 토너먼트에 참가할 수도, 당신만의 토너먼트를 만들 수도 있습니다. 불릿, 블리츠, 클래식, 체스960, 언덕의 왕, 3체크를 비롯하여 다양한 게임방식을 즐길 수 있습니다.", "tournamentNotFound": "토너먼트를 찾을 수 없습니다", "tournamentDoesNotExist": "존재하지 않는 토너먼트입니다.", @@ -1080,7 +1091,7 @@ "fullFeatured": "모든 기능을 지원합니다", "phoneAndTablet": "스마트폰과 태블릿 지원", "bulletBlitzClassical": "불릿, 블리츠, 클래식 방식 지원", - "correspondenceChess": "긴 대국 체스", + "correspondenceChess": "통신 체스", "onlineAndOfflinePlay": "온라인/오프라인 게임 모두 지원", "viewTheSolution": "정답 보기", "followAndChallengeFriends": "친구를 팔로우하고 도전하기", @@ -1148,13 +1159,13 @@ "tryAnotherMoveForBlack": "흑의 또 다른 수를 찾아보세요", "solution": "해답", "waitingForAnalysis": "분석을 기다리는 중", - "noMistakesFoundForWhite": "백에게 악수는 없었습니다", + "noMistakesFoundForWhite": "백에게 실수는 없었습니다", "noMistakesFoundForBlack": "흑에게 실수는 없었습니다", - "doneReviewingWhiteMistakes": "백의 악수 체크가 종료됨", + "doneReviewingWhiteMistakes": "백의 실수 탐색이 종료됨", "doneReviewingBlackMistakes": "흑의 실수 탐색이 종료됨", "doItAgain": "다시 하기", - "reviewWhiteMistakes": "백의 악수를 체크", - "reviewBlackMistakes": "흑의 실수 리뷰", + "reviewWhiteMistakes": "백의 실수 탐색하기", + "reviewBlackMistakes": "흑의 실수 탐색하기", "advantage": "이점", "opening": "오프닝", "middlegame": "미들게임", @@ -1164,7 +1175,7 @@ "playVariationToCreateConditionalPremoves": "기물을 움직여 조건적인 수를 만들기", "noConditionalPremoves": "조건적인 수가 없습니다", "playX": "{param} 를 둠", - "showUnreadLichessMessage": "리체스로부터 비공개 메시지를 받았습니다.", + "showUnreadLichessMessage": "Lichess로부터 비공개 메시지를 받았습니다.", "clickHereToReadIt": "클릭하여 읽기", "sorry": "죄송합니다 :(", "weHadToTimeYouOutForAWhile": "짧은 시간동안 정지를 받으셨습니다.", @@ -1196,14 +1207,14 @@ "blitzDesc": "빠른 게임: 3에서 8분", "rapidDesc": "래피드 게임: 8 ~ 25분", "classicalDesc": "클래시컬 게임: 25분 이상", - "correspondenceDesc": "통신전: 한 수당 하루 또는 수 일", + "correspondenceDesc": "통신 대국: 한 수당 하루 또는 며칠", "puzzleDesc": "체스 전술 트레이너", "important": "중요!", "yourQuestionMayHaveBeenAnswered": "{param1}에 원하시는 답변이 있을 수 있습니다.", "inTheFAQ": "F.A.Q", "toReportSomeoneForCheatingOrBadBehavior": "{param1}에서 엔진 사용이나 부적절한 행동을 신고하십시오.", "useTheReportForm": "사용자 신고", - "toRequestSupport": "{param1}에서 리체스에 문의하실 수 있습니다.", + "toRequestSupport": "{param1}에서 문의하실 수 있습니다.", "tryTheContactPage": "연락처", "makeSureToRead": "{param1}를 꼭 읽으세요", "theForumEtiquette": "포럼 에티켓", @@ -1248,7 +1259,7 @@ "simulDescription": "동시대국 설명", "simulDescriptionHelp": "참가자들에게 하고 싶은 말이 있나요?", "markdownAvailable": "추가로 {param} 문법을 사용하실 수 있습니다.", - "embedsAvailable": "포함할 게임 URL 또는 스터디 챕터 URL을 붙여넣으세요.", + "embedsAvailable": "포함할 게임 URL 또는 연구 챕터 URL을 붙여넣으세요.", "inYourLocalTimezone": "본인의 현지 시간대 기준", "tournChat": "토너먼트 채팅", "noChat": "채팅 없음", @@ -1276,7 +1287,7 @@ "youCantStartNewGame": "이 게임이 끝나기 전까지 새 게임을 시작할 수 없습니다.", "since": "부터", "until": "까지", - "lichessDbExplanation": "모든 리체스 플레이어의 레이팅 게임 샘플", + "lichessDbExplanation": "모든 Lichess 플레이어의 레이팅 게임 샘플", "switchSides": "색 바꾸기", "closingAccountWithdrawAppeal": "계정을 폐쇄하면 이의 제기는 자동으로 취소됩니다", "ourEventTips": "이벤트 준비를 위한 팁", @@ -1307,7 +1318,7 @@ "nbPlaying": "{count, plural, other{플레이 중인 게임 {count}개}}", "giveNbSeconds": "{count, plural, other{{count}초 더 주기}}", "nbTournamentPoints": "{count, plural, other{{count} 토너먼트 점수}}", - "nbStudies": "{count, plural, other{{count} 공부}}", + "nbStudies": "{count, plural, other{{count} 연구}}", "nbSimuls": "{count, plural, other{{count} 다면기}}", "moreThanNbRatedGames": "{count, plural, other{레이팅전 {count} 국 이상}}", "moreThanNbPerfRatedGames": "{count, plural, other{{param2} 레이팅전 {count} 국 이상}}", @@ -1382,14 +1393,14 @@ "stormPlayedNbRunsOfPuzzleStorm": "{count, plural, other{{param2} 중{count}개 플레이함}}", "streamerLichessStreamers": "Lichess 스트리머", "studyPrivate": "비공개", - "studyMyStudies": "내 공부", - "studyStudiesIContributeTo": "내가 기여한 공부", - "studyMyPublicStudies": "내 공개 공부", - "studyMyPrivateStudies": "내 개인 공부", - "studyMyFavoriteStudies": "내가 즐겨찾는 공부", - "studyWhatAreStudies": "공부가 무엇인가요?", - "studyAllStudies": "모든 공부", - "studyStudiesCreatedByX": "{param}이(가) 만든 공부", + "studyMyStudies": "내 연구", + "studyStudiesIContributeTo": "내가 기여한 연구", + "studyMyPublicStudies": "내 공개 연구", + "studyMyPrivateStudies": "내 비공개 연구", + "studyMyFavoriteStudies": "내가 즐겨찾는 연구", + "studyWhatAreStudies": "연구란 무엇인가요?", + "studyAllStudies": "모든 연구", + "studyStudiesCreatedByX": "{param}이(가) 만든 연구", "studyNoneYet": "아직 없음", "studyHot": "인기있는", "studyDateAddedNewest": "추가된 날짜(새로운 순)", @@ -1399,13 +1410,13 @@ "studyAlphabetical": "알파벳 순", "studyAddNewChapter": "새 챕터 추가하기", "studyAddMembers": "멤버 추가", - "studyInviteToTheStudy": "공부에 초대", - "studyPleaseOnlyInvitePeopleYouKnow": "당신이 아는 사람들이나 공부에 적극적으로 참여하고 싶은 사람들만 초대하세요.", + "studyInviteToTheStudy": "연구에 초대", + "studyPleaseOnlyInvitePeopleYouKnow": "당신이 아는 사람들이나 연구에 적극적으로 참여하고 싶은 사람들만 초대하세요.", "studySearchByUsername": "사용자 이름으로 검색", "studySpectator": "관전자", "studyContributor": "기여자", "studyKick": "강제 퇴장", - "studyLeaveTheStudy": "공부 나가기", + "studyLeaveTheStudy": "연구 나가기", "studyYouAreNowAContributor": "당신은 이제 기여자입니다", "studyYouAreNowASpectator": "당신은 이제 관전자입니다", "studyPgnTags": "PGN 태그", @@ -1416,7 +1427,7 @@ "studyCommentThisMove": "이 수에 댓글 달기", "studyAnnotateWithGlyphs": "기호로 주석 달기", "studyTheChapterIsTooShortToBeAnalysed": "분석되기 너무 짧은 챕터입니다.", - "studyOnlyContributorsCanRequestAnalysis": "공부 기여자들만이 컴퓨터 분석을 요청할 수 있습니다.", + "studyOnlyContributorsCanRequestAnalysis": "연구 기여자만이 컴퓨터 분석을 요청할 수 있습니다.", "studyGetAFullComputerAnalysis": "메인라인에 대한 전체 서버 컴퓨터 분석을 가져옵니다.", "studyMakeSureTheChapterIsComplete": "챕터가 완료되었는지 확인하세요. 분석은 한번만 요청할 수 있습니다.", "studyAllSyncMembersRemainOnTheSamePosition": "동기화된 모든 멤버들은 같은 포지션을 공유합니다", @@ -1429,22 +1440,22 @@ "studyLast": "마지막", "studyShareAndExport": "공유 및 내보내기", "studyCloneStudy": "복제", - "studyStudyPgn": "공부 PGN", + "studyStudyPgn": "연구 PGN", "studyDownloadAllGames": "모든 게임 다운로드", "studyChapterPgn": "챕터 PGN", "studyCopyChapterPgn": "PGN 복사", "studyDownloadGame": "게임 다운로드", - "studyStudyUrl": "공부 URL", + "studyStudyUrl": "연구 URL", "studyCurrentChapterUrl": "현재 챕터 URL", "studyYouCanPasteThisInTheForumToEmbed": "포럼에 공유하려면 이 주소를 붙여넣으세요", "studyStartAtInitialPosition": "처음 포지션에서 시작", "studyStartAtX": "{param}에서 시작", "studyEmbedInYourWebsite": "웹사이트 또는 블로그에 공유하기", "studyReadMoreAboutEmbedding": "공유에 대한 상세 정보", - "studyOnlyPublicStudiesCanBeEmbedded": "공개 공부들만 공유할 수 있습니다!", + "studyOnlyPublicStudiesCanBeEmbedded": "공개 연구만 공유할 수 있습니다!", "studyOpen": "열기", "studyXBroughtToYouByY": "{param1}. {param2}에서 가져옴", - "studyStudyNotFound": "공부를 찾을 수 없습니다", + "studyStudyNotFound": "연구를 찾을 수 없음", "studyEditChapter": "챕터 편집하기", "studyNewChapter": "새 챕터", "studyImportFromChapterX": "{param}에서 가져오기", @@ -1453,7 +1464,7 @@ "studyPinnedChapterComment": "챕터 댓글 고정하기", "studySaveChapter": "챕터 저장", "studyClearAnnotations": "주석 지우기", - "studyClearVariations": "파생 초기화", + "studyClearVariations": "바리에이션 초기화", "studyDeleteChapter": "챕터 지우기", "studyDeleteThisChapter": "이 챕터를 지울까요? 되돌릴 수 없습니다!", "studyClearAllCommentsInThisChapter": "이 챕터의 모든 코멘트와 기호를 지울까요?", @@ -1474,8 +1485,8 @@ "studyUrlOfTheGame": "한 줄에 하나씩, 게임의 URL", "studyLoadAGameFromXOrY": "{param1} 또는 {param2}에서 게임 로드", "studyCreateChapter": "챕터 만들기", - "studyCreateStudy": "공부 만들기", - "studyEditStudy": "공부 편집하기", + "studyCreateStudy": "연구 만들기", + "studyEditStudy": "연구 편집하기", "studyVisibility": "공개 설정", "studyPublic": "공개", "studyUnlisted": "비공개", @@ -1493,10 +1504,10 @@ "studyStart": "시작", "studySave": "저장", "studyClearChat": "채팅 기록 지우기", - "studyDeleteTheStudyChatHistory": "공부 채팅 히스토리를 지울까요? 되돌릴 수 없습니다!", - "studyDeleteStudy": "공부 삭제", - "studyConfirmDeleteStudy": "모든 공부를 삭제할까요? 복구할 수 없습니다! 확인을 위해서 공부의 이름을 입력하세요: {param}", - "studyWhereDoYouWantToStudyThat": "어디에서 공부하시겠습니까?", + "studyDeleteTheStudyChatHistory": "연구 채팅 기록을 삭제할까요? 되돌릴 수 없습니다!", + "studyDeleteStudy": "연구 삭제", + "studyConfirmDeleteStudy": "모든 연구를 삭제할까요? 복구할 수 없습니다! 확인을 위해서 연구의 이름을 입력하세요: {param}", + "studyWhereDoYouWantToStudyThat": "어디에서 연구를 시작하시겠습니까?", "studyGoodMove": "좋은 수", "studyMistake": "실수", "studyBrilliantMove": "매우 좋은 수", @@ -1517,13 +1528,13 @@ "studyDevelopment": "발전", "studyInitiative": "주도권", "studyAttack": "공격", - "studyCounterplay": "카운터플레이", + "studyCounterplay": "반격", "studyTimeTrouble": "시간이 부족함", "studyWithCompensation": "보상이 있음", "studyWithTheIdea": "아이디어", "studyNextChapter": "다음 챕터", "studyPrevChapter": "이전 챕터", - "studyStudyActions": "공부 액션", + "studyStudyActions": "연구 작업", "studyTopics": "주제", "studyMyTopics": "내 주제", "studyPopularTopics": "인기 주제", @@ -1532,6 +1543,7 @@ "studyPlayAgain": "다시 플레이", "studyWhatWouldYouPlay": "이 포지션에서 무엇을 하시겠습니까?", "studyYouCompletedThisLesson": "축하합니다! 이 레슨을 완료했습니다.", + "studyPerPage": "페이지 당 {param}개", "studyNbChapters": "{count, plural, other{{count} 챕터}}", "studyNbGames": "{count, plural, other{{count} 게임}}", "studyNbMembers": "{count, plural, other{멤버 {count}명}}", diff --git a/lib/l10n/lila_lb.arb b/lib/l10n/lila_lb.arb index a3a90fdb0a..ba01cf997c 100644 --- a/lib/l10n/lila_lb.arb +++ b/lib/l10n/lila_lb.arb @@ -1,30 +1,30 @@ { - "mobilePuzzlesTab": "Aufgaben", - "mobileMustBeLoggedIn": "Du muss ageloggt si fir dës Säit ze gesinn.", - "mobileSystemColors": "Systemsfaarwen", + "mobileAllGames": "All Partien", + "mobileAreYouSure": "Bass de sécher?", + "mobileBlindfoldMode": "Blann", "mobileFeedbackButton": "Feedback", + "mobileGreeting": "Moien, {param}", + "mobileGreetingWithoutName": "Moien", + "mobileHideVariation": "Variante verstoppen", + "mobileMustBeLoggedIn": "Du muss ageloggt si fir dës Säit ze gesinn.", + "mobileNoSearchResults": "Keng Resultater", "mobileOkButton": "OK", + "mobilePlayersMatchingSearchTerm": "Spiller mat „{param}“", + "mobilePrefMagnifyDraggedPiece": "Gezunne Figur vergréisseren", + "mobilePuzzleStormSubtitle": "Léis sou vill Aufgabe wéi méiglech an 3 Minutten.", + "mobilePuzzleThemesSubtitle": "Maach Aufgaben aus denge Liiblingserëffnungen oder sich dir een Theema eraus.", + "mobilePuzzlesTab": "Aufgaben", "mobileSettingsHapticFeedback": "Haptesche Feedback", "mobileSettingsImmersiveMode": "Immersive Modus", - "mobileAllGames": "All Partien", - "mobilePlayersMatchingSearchTerm": "Spiller mat „{param}“", - "mobileNoSearchResults": "Keng Resultater", - "mobileAreYouSure": "Bass de sécher?", - "mobileSharePuzzle": "Dës Aufgab deelen", - "mobileShareGameURL": "URL vun der Partie deelen", "mobileShareGamePGN": "PGN deelen", + "mobileShareGameURL": "URL vun der Partie deelen", "mobileSharePositionAsFEN": "Stellung als FEN deelen", - "mobileShowVariations": "Variante weisen", - "mobileHideVariation": "Variante verstoppen", + "mobileSharePuzzle": "Dës Aufgab deelen", "mobileShowComments": "Kommentarer weisen", - "mobileBlindfoldMode": "Blann", - "mobileSomethingWentWrong": "Et ass eppes schifgaang.", "mobileShowResult": "Resultat weisen", - "mobilePuzzleThemesSubtitle": "Maach Aufgaben aus denge Liiblingserëffnungen oder sich dir een Theema eraus.", - "mobilePuzzleStormSubtitle": "Léis sou vill Aufgabe wéi méiglech an 3 Minutten.", - "mobileGreeting": "Moien, {param}", - "mobileGreetingWithoutName": "Moien", - "mobilePrefMagnifyDraggedPiece": "Gezunne Figur vergréisseren", + "mobileShowVariations": "Variante weisen", + "mobileSomethingWentWrong": "Et ass eppes schifgaang.", + "mobileSystemColors": "Systemsfaarwen", "activityActivity": "Verlaf", "activityHostedALiveStream": "Huet live gestreamt", "activityRankedInSwissTournament": "Huet sech als #{param1} an {param2} placéiert", @@ -87,9 +87,17 @@ "broadcastUploadImage": "Turnéierbild eroplueden", "broadcastStartsAfter": "Fänkt no {param} un", "broadcastOfficialWebsite": "Offiziell Websäit", + "broadcastOfficialStandings": "Offizielle Stand", "broadcastIframeHelp": "Méi Optiounen op der {param}", "broadcastWebmastersPage": "Webmaster-Säit", "broadcastGamesThisTournament": "Partien an dësem Turnéier", + "broadcastAllTeams": "All Ekippen", + "broadcastTournamentFormat": "Turnéierformat", + "broadcastTournamentLocation": "Turnéierplaz", + "broadcastTopPlayers": "Topspiller", + "broadcastTimezone": "Zäitzon", + "broadcastFideRatingCategory": "FIDE-Wäertungskategorie", + "broadcastOptionalDetails": "Fakultativ Detailler", "broadcastNbBroadcasts": "{count, plural, =1{{count} Iwwerdroung} other{{count} Iwwerdroungen}}", "challengeChallengesX": "Erausfuerderungen: {param1}", "challengeChallengeToPlay": "Erausfuerderung zu enger Partie", @@ -213,6 +221,7 @@ "preferencesNotifyWeb": "Web-Browser", "preferencesNotifyDevice": "Gerät", "preferencesBellNotificationSound": "Glacken-Notifikatiounstoun", + "preferencesBlindfold": "Blann", "puzzlePuzzles": "Aufgaben", "puzzlePuzzleThemes": "Aufgabentheemen", "puzzleRecommended": "Recommandéiert", @@ -530,7 +539,6 @@ "replayMode": "Replay-Modus", "realtimeReplay": "Echtzäit", "byCPL": "No CPL", - "openStudy": "Studie opmaachen", "enable": "Aktivéieren", "bestMoveArrow": "Beschten Zuch Feil", "showVariationArrows": "Variantefeiler weisen", @@ -736,7 +744,6 @@ "block": "Blockéieren", "blocked": "Geblockt", "unblock": "Spär ophiewen", - "followsYou": "Followt dir", "xStartedFollowingY": "{param1} followt elo {param2}", "more": "Méi", "memberSince": "Member säit", @@ -1471,6 +1478,7 @@ "studyPlayAgain": "Nach eng Kéier spillen", "studyWhatWouldYouPlay": "Wat géifs du an dëser Positioun spillen?", "studyYouCompletedThisLesson": "Gudd gemaach! Du hues dës Übung ofgeschloss.", + "studyPerPage": "{param} pro Säit", "studyNbChapters": "{count, plural, =1{{count} Kapitel} other{{count} Kapitel}}", "studyNbGames": "{count, plural, =1{{count} Partie} other{{count} Partien}}", "studyNbMembers": "{count, plural, =1{{count} Member} other{{count} Memberen}}", diff --git a/lib/l10n/lila_lt.arb b/lib/l10n/lila_lt.arb index 23888f099d..2d69582878 100644 --- a/lib/l10n/lila_lt.arb +++ b/lib/l10n/lila_lt.arb @@ -23,6 +23,7 @@ "broadcastBroadcasts": "Transliacijos", "broadcastMyBroadcasts": "Mano transliacijos", "broadcastLiveBroadcasts": "Vykstančios turnyrų transliacijos", + "broadcastBroadcastCalendar": "Transliavimo kalendorius", "broadcastNewBroadcast": "Nauja transliacija", "broadcastSubscribedBroadcasts": "Prenumeruojamos transliacijos", "broadcastAboutBroadcasts": "Apie transliacijas", @@ -42,6 +43,7 @@ "broadcastSourceSingleUrl": "PGN šaltinio URL", "broadcastSourceUrlHelp": "URL, į kurį „Lichess“ kreipsis gauti PGN atnaujinimus. Privalo būti viešai pasiekiamas internete.", "broadcastSourceGameIds": "Iki 64 Lichess žaidimo ID, atskirtų tarpais.", + "broadcastStartDateTimeZone": "Turnyro pradžia vietos laiku: {param}", "broadcastStartDateHelp": "Neprivaloma; tik jeigu žinote, kada prasideda renginys", "broadcastCurrentGameUrl": "Dabartinio žaidimo adresas", "broadcastDownloadAllRounds": "Atsisiųsti visus raundus", @@ -63,6 +65,37 @@ "broadcastAgeThisYear": "Amžius šiemet", "broadcastUnrated": "Nereitinguota(s)", "broadcastRecentTournaments": "Neseniai sukurti turnyrai", + "broadcastOpenLichess": "Atverti Lichess-e", + "broadcastTeams": "Komandos", + "broadcastBoards": "Lentos", + "broadcastOverview": "Apžvalga", + "broadcastSubscribeTitle": "Užsakykite pranešimą apie kiekvieno turo pradžią. Paskyros nustatymuose galite perjungti transliacijų skambėjimo signalą arba tiesioginius pranešimus.", + "broadcastUploadImage": "Įkelkite turnyro paveikslėlį", + "broadcastNoBoardsYet": "Dar nėra lentų. Jos bus rodomos, kai bus įkeltos partijos.", + "broadcastBoardsCanBeLoaded": "Lentas galima įkelti iš šaltinio arba per {param}", + "broadcastStartsAfter": "Pradedama po {param}", + "broadcastStartVerySoon": "Transliacija prasidės visai netrukus.", + "broadcastNotYetStarted": "Transliacija dar neprasidėjo.", + "broadcastOfficialWebsite": "Oficialus tinklapis", + "broadcastStandings": "Rezultatai", + "broadcastOfficialStandings": "Oficialūs rezultatai", + "broadcastIframeHelp": "Daugiau parinkčių {param}", + "broadcastWebmastersPage": "žiniatinklio valdytojų puslapis", + "broadcastPgnSourceHelp": "Viešas realaus laiko PGN šaltinis šiam turui. Taip pat siūlome {param} greitesniam ir efektyvesniam sinchronizavimui.", + "broadcastEmbedThisBroadcast": "Įterpkite šią transliaciją į savo svetainę", + "broadcastEmbedThisRound": "Įterpkite {param} į savo svetainę", + "broadcastRatingDiff": "Reitingo skirtumas", + "broadcastGamesThisTournament": "Partijos šiame turnyre", + "broadcastScore": "Taškų skaičius", + "broadcastAllTeams": "Visos komandos", + "broadcastTournamentFormat": "Turnyro formatas", + "broadcastTournamentLocation": "Turnyro vieta", + "broadcastTopPlayers": "Geriausi žaidėjai", + "broadcastTimezone": "Laiko juosta", + "broadcastFideRatingCategory": "FIDE reitingo kategorija", + "broadcastOptionalDetails": "Papildoma informacija", + "broadcastPastBroadcasts": "Ankstesnės transliacijos", + "broadcastAllBroadcastsByMonth": "Rodyti visas transliacijas pagal mėnesį", "broadcastNbBroadcasts": "{count, plural, =1{{count} transliacija} few{{count} transliacijos} many{{count} transliacijos} other{{count} transliacijų}}", "challengeChallengesX": "Iššūkiai: {param1}", "challengeChallengeToPlay": "Iškelti iššūkį", @@ -504,7 +537,6 @@ "replayMode": "Peržiūros režimas", "realtimeReplay": "Realiu laiku", "byCPL": "Pagal įvertį", - "openStudy": "Atverti studiją", "enable": "Įjungti", "bestMoveArrow": "Geriausio ėjimo rodyklė", "showVariationArrows": "Rodyti variacijų rodykles", @@ -595,6 +627,7 @@ "rank": "Rangas", "rankX": "Reitingas: {param}", "gamesPlayed": "sužaistos partijos", + "ok": "OK", "cancel": "Atšaukti", "whiteTimeOut": "Baigėsi laikas baltiesiems", "blackTimeOut": "Baigėsi laikas juodiesiems", @@ -711,7 +744,6 @@ "block": "Blokuoti", "blocked": "Užblokuotas", "unblock": "Atblokuoti", - "followsYou": "Seka jus", "xStartedFollowingY": "{param1} pradėjo sekti {param2}", "more": "Daugiau", "memberSince": "Narys nuo", @@ -1217,6 +1249,7 @@ "showMeEverything": "Rodyti viską", "lichessPatronInfo": "Lichess yra labdara ir pilnai atviro kodo/libre projektas.\nVisos veikimo išlaidos, programavimas ir turinys yra padengti išskirtinai tik vartotojų parama.", "nothingToSeeHere": "Nieko naujo.", + "stats": "Statistika", "opponentLeftCounter": "{count, plural, =1{Jūsų varžovas paliko partiją. Galite reikalauti pergalės už {count} sekundės.} few{Jūsų varžovas paliko partiją. Galėsite prisiimti pergalę už {count} sekundžių.} many{Jūsų varžovas paliko partiją. Galėsite prisiimti pergalę už {count} sekundžių.} other{Jūsų varžovas paliko partiją. Galėsite prisiimti pergalę už {count} sekundžių.}}", "mateInXHalfMoves": "{count, plural, =1{Matas už {count} pus-ėjimo} few{Matas už {count} pus-ėjimų} many{Matas už {count} pus-ėjimų} other{Matas už {count} pus-ėjimų}}", "nbBlunders": "{count, plural, =1{{count} šiurkšti klaida} few{{count} šiurkščios klaidos} many{{count} šiurkščios klaidos} other{{count} šiurkščių klaidų}}", @@ -1464,6 +1497,7 @@ "studyPlayAgain": "Žaisti dar kartą", "studyWhatWouldYouPlay": "Ar norėtumėte žaisti nuo šios pozicijos?", "studyYouCompletedThisLesson": "Sveikiname! Jūs pabaigėte šią pamoką.", + "studyPerPage": "{param} puslapyje", "studyNbChapters": "{count, plural, =1{{count} skyrius} few{{count} skyriai} many{{count} skyrių} other{{count} skyrių}}", "studyNbGames": "{count, plural, =1{{count} partija} few{{count} partijos} many{{count} partijų} other{{count} partijų}}", "studyNbMembers": "{count, plural, =1{{count} narys} few{{count} nariai} many{{count} narių} other{{count} narių}}", diff --git a/lib/l10n/lila_lv.arb b/lib/l10n/lila_lv.arb index 625fc889e6..6cd1e1d8ab 100644 --- a/lib/l10n/lila_lv.arb +++ b/lib/l10n/lila_lv.arb @@ -474,7 +474,6 @@ "replayMode": "Atkārtojuma režīms", "realtimeReplay": "Reāllaikā", "byCPL": "Pēc CPL", - "openStudy": "Atvērt izpēti", "enable": "Iespējot", "bestMoveArrow": "Labākā gājiena bulta", "evaluationGauge": "Novērtējuma rādītājs", @@ -676,7 +675,6 @@ "block": "Bloķēt", "blocked": "Bloķētie", "unblock": "Atbloķēt", - "followsYou": "Sekotāji", "xStartedFollowingY": "{param1} sāka sekot {param2}", "more": "Vairāk", "memberSince": "Dalībnieks kopš", diff --git a/lib/l10n/lila_mk.arb b/lib/l10n/lila_mk.arb index 4d2d31a540..e2408222e1 100644 --- a/lib/l10n/lila_mk.arb +++ b/lib/l10n/lila_mk.arb @@ -1,7 +1,7 @@ { - "mobileSystemColors": "Системски бои", "mobileFeedbackButton": "Повратна информација", "mobileSettingsHapticFeedback": "Тактилен фидбек", + "mobileSystemColors": "Системски бои", "activityActivity": "Активност", "activityHostedALiveStream": "Емитуваше во живо", "activityRankedInSwissTournament": "Ранг #{param1} во {param2}", @@ -311,7 +311,6 @@ "replayMode": "Режим на реприза", "realtimeReplay": "Во реално време", "byCPL": "По CPL", - "openStudy": "Отвори студија", "enable": "Овозможи", "bestMoveArrow": "Стрелка за најдобар потег", "evaluationGauge": "Мерач за проценка", @@ -513,7 +512,6 @@ "block": "Блокирај", "blocked": "Блокиран", "unblock": "Одблокирај", - "followsYou": "Те следи", "xStartedFollowingY": "{param1} почна да го следи {param2}", "more": "Повеќе", "memberSince": "Член од", @@ -1020,5 +1018,11 @@ "availableInNbLanguages": "{count, plural, =1{Достапно на {count} јазик!} other{Достапно на {count} јазици!}}", "nbSecondsToPlayTheFirstMove": "{count, plural, =1{{count} секунда за првиот потег} other{{count} секунди за првиот потег}}", "nbSeconds": "{count, plural, =1{{count} секунда} other{{count} секунди}}", - "andSaveNbPremoveLines": "{count, plural, =1{и заштеди {count} пред-потег} other{и заштеди {count} пред-потези}}" + "andSaveNbPremoveLines": "{count, plural, =1{и заштеди {count} пред-потег} other{и заштеди {count} пред-потези}}", + "studyNext": "Следно", + "studyEmbedInYourWebsite": "Вгради во твојот сајт", + "studySave": "Зачувај", + "studyGoodMove": "Добар потег", + "studyMistake": "Грешка", + "studyBlunder": "Глупа грешка" } \ No newline at end of file diff --git a/lib/l10n/lila_nb.arb b/lib/l10n/lila_nb.arb index eb27f36f40..4af4ab2488 100644 --- a/lib/l10n/lila_nb.arb +++ b/lib/l10n/lila_nb.arb @@ -1,47 +1,48 @@ { + "mobileAllGames": "Alle partier", + "mobileAreYouSure": "Er du sikker?", + "mobileBlindfoldMode": "Blindsjakk", + "mobileCancelTakebackOffer": "Avbryt tilbud om å angre", + "mobileClearButton": "Tøm", + "mobileCorrespondenceClearSavedMove": "Fjern lagret trekk", + "mobileCustomGameJoinAGame": "Bli med på et parti", + "mobileFeedbackButton": "Tilbakemeldinger", + "mobileGreeting": "Hei, {param}", + "mobileGreetingWithoutName": "Hei", + "mobileHideVariation": "Skjul variant", "mobileHomeTab": "Hjem", - "mobilePuzzlesTab": "Nøtter", - "mobileToolsTab": "Verktøy", - "mobileWatchTab": "Se", - "mobileSettingsTab": "Valg", + "mobileLiveStreamers": "Direktestrømmere", "mobileMustBeLoggedIn": "Du må være logget inn for å vise denne siden.", - "mobileSystemColors": "Systemfarger", - "mobileFeedbackButton": "Tilbakemeldinger", + "mobileNoSearchResults": "Ingen treff", + "mobileNotFollowingAnyUser": "Du følger ingen brukere.", "mobileOkButton": "Ok", + "mobilePlayersMatchingSearchTerm": "Spillere med «{param}»", + "mobilePrefMagnifyDraggedPiece": "Forstørr brikker når de dras", + "mobilePuzzleStormConfirmEndRun": "Vil du avslutte denne runden?", + "mobilePuzzleStormFilterNothingToShow": "Ingenting her, endre filteret", + "mobilePuzzleStormNothingToShow": "Ingenting her. Spill noen runder med Puzzle Storm.", + "mobilePuzzleStormSubtitle": "Løs så mange sjakknøtter du klarer i løpet av 3 minutter.", + "mobilePuzzleStreakAbortWarning": "Du mister rekken og poengsummen din blir lagret.", + "mobilePuzzleThemesSubtitle": "Spill sjakknøtter fra favorittåpningene dine, eller velg et tema.", + "mobilePuzzlesTab": "Nøtter", + "mobileRecentSearches": "Nylige søk", "mobileSettingsHapticFeedback": "Haptiske tilbakemeldinger", "mobileSettingsImmersiveMode": "Fordypelsesmodus", "mobileSettingsImmersiveModeSubtitle": "Skjul systemgrensesnittet mens du spiller. Bruk dette hvis du blir forstyrret av systemets navigasjonsgester på skjermkanten. Gjelder for partier og Puzzle Storm.", - "mobileNotFollowingAnyUser": "Du følger ingen brukere.", - "mobileAllGames": "Alle partier", - "mobileRecentSearches": "Nylige søk", - "mobileClearButton": "Tøm", - "mobilePlayersMatchingSearchTerm": "Spillere med «{param}»", - "mobileNoSearchResults": "Ingen treff", - "mobileAreYouSure": "Er du sikker?", - "mobilePuzzleStreakAbortWarning": "Du mister rekken og poengsummen din blir lagret.", - "mobilePuzzleStormNothingToShow": "Ingenting her. Spill noen runder med Puzzle Storm.", - "mobileSharePuzzle": "Del denne sjakknøtten", - "mobileShareGameURL": "Del URL til partiet", + "mobileSettingsTab": "Valg", "mobileShareGamePGN": "Del PGN", + "mobileShareGameURL": "Del URL til partiet", "mobileSharePositionAsFEN": "Del stillingen som FEN", - "mobileShowVariations": "Vis varianter", - "mobileHideVariation": "Skjul variant", + "mobileSharePuzzle": "Del denne sjakknøtten", "mobileShowComments": "Vis kommentarer", - "mobilePuzzleStormConfirmEndRun": "Vil du avslutte denne runden?", - "mobilePuzzleStormFilterNothingToShow": "Ingenting her, endre filteret", - "mobileCancelTakebackOffer": "Avbryt tilbud om å angre", - "mobileWaitingForOpponentToJoin": "Venter på motstanderen ...", - "mobileBlindfoldMode": "Blindsjakk", - "mobileLiveStreamers": "Direktestrømmere", - "mobileCustomGameJoinAGame": "Bli med på et parti", - "mobileCorrespondenceClearSavedMove": "Fjern lagret trekk", - "mobileSomethingWentWrong": "Noe gikk galt.", "mobileShowResult": "Vis resultat", - "mobilePuzzleThemesSubtitle": "Spill sjakknøtter fra favorittåpningene dine, eller velg et tema.", - "mobilePuzzleStormSubtitle": "Løs så mange sjakknøtter du klarer i løpet av 3 minutter.", - "mobileGreeting": "Hei, {param}", - "mobileGreetingWithoutName": "Hei", - "mobilePrefMagnifyDraggedPiece": "Forstørr brikker når de dras", + "mobileShowVariations": "Vis varianter", + "mobileSomethingWentWrong": "Noe gikk galt.", + "mobileSystemColors": "Systemfarger", + "mobileTheme": "Tema", + "mobileToolsTab": "Verktøy", + "mobileWaitingForOpponentToJoin": "Venter på motstanderen ...", + "mobileWatchTab": "Se", "activityActivity": "Aktivitet", "activityHostedALiveStream": "Startet en direktestrøm", "activityRankedInSwissTournament": "Ble nummer {param1} i {param2}", @@ -122,6 +123,7 @@ "broadcastNotYetStarted": "Overføringen har ikke startet.", "broadcastOfficialWebsite": "Offisiell nettside", "broadcastStandings": "Resultatliste", + "broadcastOfficialStandings": "Offisiell tabell", "broadcastIframeHelp": "Flere alternativer på {param}", "broadcastWebmastersPage": "administratorens side", "broadcastPgnSourceHelp": "En offentlig PGN-kilde i sanntid for denne runden. Vi tilbyr også en {param} for raskere og mer effektiv synkronisering.", @@ -130,6 +132,15 @@ "broadcastRatingDiff": "Ratingdifferanse", "broadcastGamesThisTournament": "Partier i denne turneringen", "broadcastScore": "Poengsum", + "broadcastAllTeams": "Alle lag", + "broadcastTournamentFormat": "Turneringsformat", + "broadcastTournamentLocation": "Turneringssted", + "broadcastTopPlayers": "Toppspillere", + "broadcastTimezone": "Tidssone", + "broadcastFideRatingCategory": "FIDE-ratingkategori", + "broadcastOptionalDetails": "Valgfrie detaljer", + "broadcastPastBroadcasts": "Tidligere overføringer", + "broadcastAllBroadcastsByMonth": "Vis alle overføringer etter måned", "broadcastNbBroadcasts": "{count, plural, =1{{count} overføring} other{{count} overføringer}}", "challengeChallengesX": "Utfordringer: {param1}", "challengeChallengeToPlay": "Utfordre til et parti", @@ -254,6 +265,7 @@ "preferencesNotifyWeb": "Nettleser", "preferencesNotifyDevice": "Enhet", "preferencesBellNotificationSound": "Bjellevarsel med lyd", + "preferencesBlindfold": "Blindsjakk", "puzzlePuzzles": "Sjakknøtter", "puzzlePuzzleThemes": "Temaer for sjakknøtter", "puzzleRecommended": "Anbefalt", @@ -571,7 +583,6 @@ "replayMode": "Gjennomspilling", "realtimeReplay": "Sanntid", "byCPL": "Etter CBT", - "openStudy": "Åpne studie", "enable": "Aktiver", "bestMoveArrow": "Pil for beste trekk", "showVariationArrows": "Vis variantpiler", @@ -662,6 +673,7 @@ "rank": "Rangering", "rankX": "Plassering: {param}", "gamesPlayed": "partier spilt", + "ok": "OK", "cancel": "Avbryt", "whiteTimeOut": "Tiden er ute for hvit", "blackTimeOut": "Tiden er ute for svart", @@ -778,7 +790,6 @@ "block": "Blokker", "blocked": "Blokkert", "unblock": "Fjern blokkering", - "followsYou": "Følger deg", "xStartedFollowingY": "{param1} begynte å følge {param2}", "more": "Mer", "memberSince": "Medlem siden", @@ -1532,6 +1543,7 @@ "studyPlayAgain": "Spill igjen", "studyWhatWouldYouPlay": "Hva vil du spille i denne stillingen?", "studyYouCompletedThisLesson": "Gratulerer! Du har fullført denne leksjonen.", + "studyPerPage": "{param} per side", "studyNbChapters": "{count, plural, =1{{count} kapittel} other{{count} kapitler}}", "studyNbGames": "{count, plural, =1{{count} parti} other{{count} partier}}", "studyNbMembers": "{count, plural, =1{{count} medlem} other{{count} medlemmer}}", diff --git a/lib/l10n/lila_nl.arb b/lib/l10n/lila_nl.arb index a9fab4dd7b..9af856ec1a 100644 --- a/lib/l10n/lila_nl.arb +++ b/lib/l10n/lila_nl.arb @@ -1,47 +1,48 @@ { + "mobileAllGames": "Alle partijen", + "mobileAreYouSure": "Weet je het zeker?", + "mobileBlindfoldMode": "Geblinddoekt", + "mobileCancelTakebackOffer": "Terugnameaanbod annuleren", + "mobileClearButton": "Wissen", + "mobileCorrespondenceClearSavedMove": "Opgeslagen zet wissen", + "mobileCustomGameJoinAGame": "Een partij beginnen", + "mobileFeedbackButton": "Feedback", + "mobileGreeting": "Hallo, {param}", + "mobileGreetingWithoutName": "Hallo", + "mobileHideVariation": "Verberg varianten", "mobileHomeTab": "Startscherm", - "mobilePuzzlesTab": "Puzzels", - "mobileToolsTab": "Gereedschap", - "mobileWatchTab": "Kijken", - "mobileSettingsTab": "Instellingen", + "mobileLiveStreamers": "Live streamers", "mobileMustBeLoggedIn": "Je moet ingelogd zijn om deze pagina te bekijken.", - "mobileSystemColors": "Systeemkleuren", - "mobileFeedbackButton": "Feedback", + "mobileNoSearchResults": "Geen resultaten", + "mobileNotFollowingAnyUser": "U volgt geen gebruiker.", "mobileOkButton": "OK", + "mobilePlayersMatchingSearchTerm": "Spelers met \"{param}\"", + "mobilePrefMagnifyDraggedPiece": "Versleept stuk vergroot weergeven", + "mobilePuzzleStormConfirmEndRun": "Wil je deze reeks beëindigen?", + "mobilePuzzleStormFilterNothingToShow": "Niets te tonen, wijzig de filters", + "mobilePuzzleStormNothingToShow": "Niets om te tonen. Speel een aantal reeksen Puzzle Storm.", + "mobilePuzzleStormSubtitle": "Los zoveel mogelijk puzzels op in 3 minuten.", + "mobilePuzzleStreakAbortWarning": "Je verliest je huidige reeks en de score wordt opgeslagen.", + "mobilePuzzleThemesSubtitle": "Speel puzzels uit je favorieten openingen, of kies een thema.", + "mobilePuzzlesTab": "Puzzels", + "mobileRecentSearches": "Recente zoekopdrachten", "mobileSettingsHapticFeedback": "Haptische feedback", "mobileSettingsImmersiveMode": "Volledig scherm-modus", "mobileSettingsImmersiveModeSubtitle": "Systeem-UI verbergen tijdens het spelen. Gebruik dit als je last hebt van de navigatiegebaren aan de randen van het scherm. Dit is van toepassing op spel- en Puzzle Storm schermen.", - "mobileNotFollowingAnyUser": "U volgt geen gebruiker.", - "mobileAllGames": "Alle partijen", - "mobileRecentSearches": "Recente zoekopdrachten", - "mobileClearButton": "Wissen", - "mobilePlayersMatchingSearchTerm": "Spelers met \"{param}\"", - "mobileNoSearchResults": "Geen resultaten", - "mobileAreYouSure": "Weet je het zeker?", - "mobilePuzzleStreakAbortWarning": "Je verliest je huidige reeks en de score wordt opgeslagen.", - "mobilePuzzleStormNothingToShow": "Niets om te tonen. Speel een aantal reeksen Puzzle Storm.", - "mobileSharePuzzle": "Deze puzzel delen", - "mobileShareGameURL": "Partij URL delen", + "mobileSettingsTab": "Instellingen", "mobileShareGamePGN": "PGN delen", + "mobileShareGameURL": "Partij URL delen", "mobileSharePositionAsFEN": "Stelling delen als FEN", - "mobileShowVariations": "Toon varianten", - "mobileHideVariation": "Verberg varianten", + "mobileSharePuzzle": "Deze puzzel delen", "mobileShowComments": "Opmerkingen weergeven", - "mobilePuzzleStormConfirmEndRun": "Wil je deze reeks beëindigen?", - "mobilePuzzleStormFilterNothingToShow": "Niets te tonen, wijzig de filters", - "mobileCancelTakebackOffer": "Terugnameaanbod annuleren", - "mobileWaitingForOpponentToJoin": "Wachten op een tegenstander...", - "mobileBlindfoldMode": "Geblinddoekt", - "mobileLiveStreamers": "Live streamers", - "mobileCustomGameJoinAGame": "Een partij beginnen", - "mobileCorrespondenceClearSavedMove": "Opgeslagen zet wissen", - "mobileSomethingWentWrong": "Er is iets fout gegaan.", "mobileShowResult": "Toon resultaat", - "mobilePuzzleThemesSubtitle": "Speel puzzels uit je favorieten openingen, of kies een thema.", - "mobilePuzzleStormSubtitle": "Los zoveel mogelijk puzzels op in 3 minuten.", - "mobileGreeting": "Hallo, {param}", - "mobileGreetingWithoutName": "Hallo", - "mobilePrefMagnifyDraggedPiece": "Versleept stuk vergroot weergeven", + "mobileShowVariations": "Toon varianten", + "mobileSomethingWentWrong": "Er is iets fout gegaan.", + "mobileSystemColors": "Systeemkleuren", + "mobileTheme": "Thema", + "mobileToolsTab": "Gereedschap", + "mobileWaitingForOpponentToJoin": "Wachten op een tegenstander...", + "mobileWatchTab": "Kijken", "activityActivity": "Activiteit", "activityHostedALiveStream": "Heeft een live stream gehost", "activityRankedInSwissTournament": "Eindigde #{param1} in {param2}", @@ -121,6 +122,7 @@ "broadcastNotYetStarted": "De uitzending is nog niet begonnen.", "broadcastOfficialWebsite": "Officiële website", "broadcastStandings": "Klassement", + "broadcastOfficialStandings": "Officiële standen", "broadcastIframeHelp": "Meer opties voor de {param}", "broadcastWebmastersPage": "pagina van de webmaster", "broadcastPgnSourceHelp": "Een publieke real-time PGN-bron voor deze ronde. We bieden ook een {param} aan voor een snellere en efficiëntere synchronisatie.", @@ -129,6 +131,15 @@ "broadcastRatingDiff": "Ratingverschil", "broadcastGamesThisTournament": "Partijen in dit toernooi", "broadcastScore": "Score", + "broadcastAllTeams": "Alle teams", + "broadcastTournamentFormat": "Toernooivorm", + "broadcastTournamentLocation": "Toernooilocatie", + "broadcastTopPlayers": "Topspelers", + "broadcastTimezone": "Tijdzone", + "broadcastFideRatingCategory": "FIDE-rating categorie", + "broadcastOptionalDetails": "Optionele info", + "broadcastPastBroadcasts": "Afgelopen uitzendingen", + "broadcastAllBroadcastsByMonth": "Alle uitzendingen per maand weergeven", "broadcastNbBroadcasts": "{count, plural, =1{{count} uitzending} other{{count} uitzendingen}}", "challengeChallengesX": "Uitdagingen: {param1}", "challengeChallengeToPlay": "Uitdagen voor een partij", @@ -253,6 +264,7 @@ "preferencesNotifyWeb": "Browser", "preferencesNotifyDevice": "Apparaat", "preferencesBellNotificationSound": "Meldingsgeluid", + "preferencesBlindfold": "Geblinddoekt", "puzzlePuzzles": "Puzzels", "puzzlePuzzleThemes": "Puzzelthema's", "puzzleRecommended": "Aanbevolen", @@ -570,7 +582,6 @@ "replayMode": "Terugspeelmodus", "realtimeReplay": "Realtime", "byCPL": "Door CPL", - "openStudy": "Open Study", "enable": "Aanzetten", "bestMoveArrow": "Beste zet-pijl", "showVariationArrows": "Toon variantpijlen", @@ -681,6 +692,7 @@ "abortGame": "Partij afbreken", "gameAborted": "Partij afgebroken", "standard": "Standaard", + "customPosition": "Aangepaste positie", "unlimited": "Onbeperkt", "mode": "Instelling", "casual": "Vrijblijvend", @@ -777,7 +789,6 @@ "block": "Blokkeren", "blocked": "Geblokkeerd", "unblock": "Deblokkeren", - "followsYou": "Volgt u", "xStartedFollowingY": "{param1} volgt nu {param2}", "more": "Meer", "memberSince": "Lid sinds", @@ -1531,6 +1542,7 @@ "studyPlayAgain": "Opnieuw spelen", "studyWhatWouldYouPlay": "Wat zou je in deze stelling spelen?", "studyYouCompletedThisLesson": "Gefeliciteerd! Je hebt deze les voltooid.", + "studyPerPage": "{param} per pagina", "studyNbChapters": "{count, plural, =1{{count} hoofdstuk} other{{count} hoofdstukken}}", "studyNbGames": "{count, plural, =1{{count} Partij} other{{count} Partijen}}", "studyNbMembers": "{count, plural, =1{{count} Deelnemer} other{{count} Deelnemers}}", diff --git a/lib/l10n/lila_nn.arb b/lib/l10n/lila_nn.arb index 219c3a6d6f..e5dff01baa 100644 --- a/lib/l10n/lila_nn.arb +++ b/lib/l10n/lila_nn.arb @@ -1,47 +1,48 @@ { + "mobileAllGames": "Alle spel", + "mobileAreYouSure": "Er du sikker?", + "mobileBlindfoldMode": "Blindsjakk", + "mobileCancelTakebackOffer": "Avbryt tilbud om angrerett", + "mobileClearButton": "Tøm", + "mobileCorrespondenceClearSavedMove": "Fjern lagra trekk", + "mobileCustomGameJoinAGame": "Bli med på eit parti", + "mobileFeedbackButton": "Tilbakemelding", + "mobileGreeting": "Hei {param}", + "mobileGreetingWithoutName": "Hei", + "mobileHideVariation": "Skjul variant", "mobileHomeTab": "Startside", - "mobilePuzzlesTab": "Oppgåver", - "mobileToolsTab": "Verktøy", - "mobileWatchTab": "Sjå", - "mobileSettingsTab": "Innstillingar", + "mobileLiveStreamers": "Direkte strøymarar", "mobileMustBeLoggedIn": "Du må vera innlogga for å sjå denne sida.", - "mobileSystemColors": "Systemfargar", - "mobileFeedbackButton": "Tilbakemelding", + "mobileNoSearchResults": "Ingen resultat", + "mobileNotFollowingAnyUser": "Du følgjer ingen brukarar.", "mobileOkButton": "Ok", + "mobilePlayersMatchingSearchTerm": "Spelarar med \"{param}\"", + "mobilePrefMagnifyDraggedPiece": "Forstørr brikke som vert trekt", + "mobilePuzzleStormConfirmEndRun": "Vil du avslutte dette løpet?", + "mobilePuzzleStormFilterNothingToShow": "Ikkje noko å syna, ver venleg å endre filtera", + "mobilePuzzleStormNothingToShow": "Ikkje noko å visa. Spel nokre omgangar Puzzle Storm.", + "mobilePuzzleStormSubtitle": "Løys så mange oppgåver som du maktar på tre minutt.", + "mobilePuzzleStreakAbortWarning": "Du vil mista din noverande vinstrekke og poengsummen din vert lagra.", + "mobilePuzzleThemesSubtitle": "Spel oppgåver frå favorittopningane dine, eller velg eit tema.", + "mobilePuzzlesTab": "Oppgåver", + "mobileRecentSearches": "Nylege søk", "mobileSettingsHapticFeedback": "Haptisk tilbakemelding", "mobileSettingsImmersiveMode": "Immersiv modus", "mobileSettingsImmersiveModeSubtitle": "Skjul system-UI mens du spelar. Bruk dette dersom systemet sine navigasjonsrørsler ved skjermkanten forstyrrar deg. Gjelder skjermbileta for spel og oppgåvestorm.", - "mobileNotFollowingAnyUser": "Du følgjer ingen brukarar.", - "mobileAllGames": "Alle spel", - "mobileRecentSearches": "Nylege søk", - "mobileClearButton": "Tøm", - "mobilePlayersMatchingSearchTerm": "Spelarar med \"{param}\"", - "mobileNoSearchResults": "Ingen resultat", - "mobileAreYouSure": "Er du sikker?", - "mobilePuzzleStreakAbortWarning": "Du vil mista din noverande vinstrekke og poengsummen din vert lagra.", - "mobilePuzzleStormNothingToShow": "Ikkje noko å visa. Spel nokre omgangar Puzzle Storm.", - "mobileSharePuzzle": "Del denne oppgåva", - "mobileShareGameURL": "Del URLen til partiet", + "mobileSettingsTab": "Innstillingar", "mobileShareGamePGN": "Del PGN", + "mobileShareGameURL": "Del URLen til partiet", "mobileSharePositionAsFEN": "Del stilling som FEN", - "mobileShowVariations": "Vis variantpilar", - "mobileHideVariation": "Skjul variant", + "mobileSharePuzzle": "Del denne oppgåva", "mobileShowComments": "Vis kommentarar", - "mobilePuzzleStormConfirmEndRun": "Vil du avslutte dette løpet?", - "mobilePuzzleStormFilterNothingToShow": "Ikkje noko å syna, ver venleg å endre filtera", - "mobileCancelTakebackOffer": "Avbryt tilbud om angrerett", - "mobileWaitingForOpponentToJoin": "Ventar på motspelar...", - "mobileBlindfoldMode": "Blindsjakk", - "mobileLiveStreamers": "Direkte strøymarar", - "mobileCustomGameJoinAGame": "Bli med på eit parti", - "mobileCorrespondenceClearSavedMove": "Fjern lagra trekk", - "mobileSomethingWentWrong": "Det oppsto ein feil.", "mobileShowResult": "Vis resultat", - "mobilePuzzleThemesSubtitle": "Spel oppgåver frå favorittopningane dine, eller velg eit tema.", - "mobilePuzzleStormSubtitle": "Løys så mange oppgåver som du maktar på tre minutt.", - "mobileGreeting": "Hei {param}", - "mobileGreetingWithoutName": "Hei", - "mobilePrefMagnifyDraggedPiece": "Forstørr brikke som vert trekt", + "mobileShowVariations": "Vis variantpilar", + "mobileSomethingWentWrong": "Det oppsto ein feil.", + "mobileSystemColors": "Systemfargar", + "mobileTheme": "Tema", + "mobileToolsTab": "Verktøy", + "mobileWaitingForOpponentToJoin": "Ventar på motspelar...", + "mobileWatchTab": "Sjå", "activityActivity": "Aktivitet", "activityHostedALiveStream": "Starta en direktestraum", "activityRankedInSwissTournament": "Vart nr. {param1} i {param2}", @@ -83,7 +84,7 @@ "broadcastTournamentName": "Turneringsnamn", "broadcastTournamentDescription": "Kortfatta skildring av turneringa", "broadcastFullDescription": "Full omtale av arrangementet", - "broadcastFullDescriptionHelp": "Valfri lang omtale av overføringa. {param1} er tilgjengeleg. Omtalen må vera kortare enn {param2} teikn.", + "broadcastFullDescriptionHelp": "Valfri lang omtale av turneringa. {param1} er tilgjengeleg. Omtalen må vera kortare enn {param2} teikn.", "broadcastSourceSingleUrl": "PGN kjelde-URL", "broadcastSourceUrlHelp": "Lenke som Lichess vil hente PGN-oppdateringar frå. Den må vera offentleg tilgjengeleg på internett.", "broadcastSourceGameIds": "Opp til 64 Lichess spel-ID'ar, skilde med mellomrom.", @@ -122,6 +123,7 @@ "broadcastNotYetStarted": "Sendinga har førebels ikkje starta.", "broadcastOfficialWebsite": "Offisiell nettside", "broadcastStandings": "Resultat", + "broadcastOfficialStandings": "Offisiell tabell", "broadcastIframeHelp": "Fleire alternativ på {param}", "broadcastWebmastersPage": "administratoren si side", "broadcastPgnSourceHelp": "Ei offentleg PGN-kjelde i sanntid for denne runden. Vi tilbyr og ei {param} for raskare og meir effektiv synkronisering.", @@ -130,6 +132,15 @@ "broadcastRatingDiff": "Rangeringsdiff", "broadcastGamesThisTournament": "Spel i denne turneringa", "broadcastScore": "Poengskår", + "broadcastAllTeams": "Alle lag", + "broadcastTournamentFormat": "Turneringsformat", + "broadcastTournamentLocation": "Turneringsstad", + "broadcastTopPlayers": "Toppspelarar", + "broadcastTimezone": "Tidssone", + "broadcastFideRatingCategory": "FIDE-ratingkategori", + "broadcastOptionalDetails": "Valfrie detaljar", + "broadcastPastBroadcasts": "Tidlegare overføringar", + "broadcastAllBroadcastsByMonth": "Vis alle overføringar etter månad", "broadcastNbBroadcasts": "{count, plural, =1{{count} sending} other{{count} sendingar}}", "challengeChallengesX": "Utfordringar: {param1}", "challengeChallengeToPlay": "Utfordra til eit parti", @@ -254,6 +265,7 @@ "preferencesNotifyWeb": "Nettlesar", "preferencesNotifyDevice": "Eining", "preferencesBellNotificationSound": "Varsellyd", + "preferencesBlindfold": "Blindsjakk", "puzzlePuzzles": "Taktikkoppgåver", "puzzlePuzzleThemes": "Oppgåvetema", "puzzleRecommended": "Anbefalt", @@ -571,7 +583,6 @@ "replayMode": "Modus for å spele oppatt", "realtimeReplay": "Sanntid", "byCPL": "CPL", - "openStudy": "Opne studie", "enable": "Aktiver", "bestMoveArrow": "Pil for beste trekk", "showVariationArrows": "Vis variantpiler", @@ -662,6 +673,7 @@ "rank": "Rangering", "rankX": "Plassering: {param}", "gamesPlayed": "Spelte parti", + "ok": "Ok", "cancel": "Avbryt", "whiteTimeOut": "Tida er ute for kvit", "blackTimeOut": "Tida er ute for svart", @@ -778,7 +790,6 @@ "block": "Blokkér", "blocked": "Blokkert", "unblock": "Fjern blokkering", - "followsYou": "Følgjer deg", "xStartedFollowingY": "{param1} byrja å følgja {param2}", "more": "Meir", "memberSince": "Medlem sidan", @@ -1532,6 +1543,7 @@ "studyPlayAgain": "Spel på ny", "studyWhatWouldYouPlay": "Kva vil du spela i denne stillinga?", "studyYouCompletedThisLesson": "Gratulerar! Du har fullført denne leksjonen.", + "studyPerPage": "{param} per side", "studyNbChapters": "{count, plural, =1{{count} kapittel} other{{count} kapittel}}", "studyNbGames": "{count, plural, =1{{count} parti} other{{count} parti}}", "studyNbMembers": "{count, plural, =1{{count} medlem} other{{count} medlemar}}", diff --git a/lib/l10n/lila_pl.arb b/lib/l10n/lila_pl.arb index 50d5609c67..dcb008584b 100644 --- a/lib/l10n/lila_pl.arb +++ b/lib/l10n/lila_pl.arb @@ -1,47 +1,48 @@ { + "mobileAllGames": "Wszystkie partie", + "mobileAreYouSure": "Jesteś pewien?", + "mobileBlindfoldMode": "Gra na ślepo", + "mobileCancelTakebackOffer": "Anuluj prośbę cofnięcia ruchu", + "mobileClearButton": "Wyczyść", + "mobileCorrespondenceClearSavedMove": "Usuń zapisany ruch", + "mobileCustomGameJoinAGame": "Dołącz do partii", + "mobileFeedbackButton": "Opinie", + "mobileGreeting": "Witaj {param}", + "mobileGreetingWithoutName": "Witaj", + "mobileHideVariation": "Ukryj wariant", "mobileHomeTab": "Start", - "mobilePuzzlesTab": "Zadania", - "mobileToolsTab": "Narzędzia", - "mobileWatchTab": "Oglądaj", - "mobileSettingsTab": "Ustawienia", + "mobileLiveStreamers": "Aktywni streamerzy", "mobileMustBeLoggedIn": "Musisz być zalogowany, aby wyświetlić tę stronę.", - "mobileSystemColors": "Kolory systemowe", - "mobileFeedbackButton": "Opinie", + "mobileNoSearchResults": "Brak wyników", + "mobileNotFollowingAnyUser": "Nie obserwujesz żadnego gracza.", "mobileOkButton": "OK", + "mobilePlayersMatchingSearchTerm": "Gracze pasujący do \"{param}\"", + "mobilePrefMagnifyDraggedPiece": "Powiększ przeciąganą bierkę", + "mobilePuzzleStormConfirmEndRun": "Czy chcesz zakończyć tę serię?", + "mobilePuzzleStormFilterNothingToShow": "Brak wyników, zmień proszę filtry", + "mobilePuzzleStormNothingToShow": "Nic do wyświetlenia. Rozegraj kilka serii.", + "mobilePuzzleStormSubtitle": "Rozwiąż jak najwięcej zadań w ciągu 3 minut.", + "mobilePuzzleStreakAbortWarning": "Przerwiesz swoją dobrą passę, a Twój wynik zostanie zapisany.", + "mobilePuzzleThemesSubtitle": "Rozwiąż zadania z ulubionego debiutu lub wybierz motyw.", + "mobilePuzzlesTab": "Zadania", + "mobileRecentSearches": "Ostatnio wyszukiwane", "mobileSettingsHapticFeedback": "Wibracja przy dotknięciu", "mobileSettingsImmersiveMode": "Tryb pełnoekranowy", "mobileSettingsImmersiveModeSubtitle": "Ukryj interfejs użytkownika podczas gry. Użyj tego, jeśli rozpraszają Cię elementy nawigacji systemu na krawędziach ekranu. Dotyczy ekranów gry i rozwiązywania zadań.", - "mobileNotFollowingAnyUser": "Nie obserwujesz żadnego gracza.", - "mobileAllGames": "Wszystkie partie", - "mobileRecentSearches": "Ostatnio wyszukiwane", - "mobileClearButton": "Wyczyść", - "mobilePlayersMatchingSearchTerm": "Gracze pasujący do \"{param}\"", - "mobileNoSearchResults": "Brak wyników", - "mobileAreYouSure": "Jesteś pewien?", - "mobilePuzzleStreakAbortWarning": "Przerwiesz swoją dobrą passę, a Twój wynik zostanie zapisany.", - "mobilePuzzleStormNothingToShow": "Nic do wyświetlenia. Rozegraj kilka serii.", - "mobileSharePuzzle": "Udostępnij to zadanie", - "mobileShareGameURL": "Udostępnij adres URL partii", + "mobileSettingsTab": "Ustawienia", "mobileShareGamePGN": "Udostępnij PGN", + "mobileShareGameURL": "Udostępnij adres URL partii", "mobileSharePositionAsFEN": "Udostępnij pozycję jako FEN", - "mobileShowVariations": "Pokaż warianty", - "mobileHideVariation": "Ukryj wariant", + "mobileSharePuzzle": "Udostępnij to zadanie", "mobileShowComments": "Pokaż komentarze", - "mobilePuzzleStormConfirmEndRun": "Czy chcesz zakończyć tę serię?", - "mobilePuzzleStormFilterNothingToShow": "Brak wyników, zmień proszę filtry", - "mobileCancelTakebackOffer": "Anuluj prośbę cofnięcia ruchu", - "mobileWaitingForOpponentToJoin": "Oczekiwanie na dołączenie przeciwnika...", - "mobileBlindfoldMode": "Gra na ślepo", - "mobileLiveStreamers": "Aktywni streamerzy", - "mobileCustomGameJoinAGame": "Dołącz do partii", - "mobileCorrespondenceClearSavedMove": "Usuń zapisany ruch", - "mobileSomethingWentWrong": "Coś poszło nie tak.", "mobileShowResult": "Pokaż wynik", - "mobilePuzzleThemesSubtitle": "Rozwiąż zadania z ulubionego debiutu lub wybierz motyw.", - "mobilePuzzleStormSubtitle": "Rozwiąż jak najwięcej zadań w ciągu 3 minut.", - "mobileGreeting": "Witaj {param}", - "mobileGreetingWithoutName": "Witaj", - "mobilePrefMagnifyDraggedPiece": "Powiększ przeciąganą bierkę", + "mobileShowVariations": "Pokaż warianty", + "mobileSomethingWentWrong": "Coś poszło nie tak.", + "mobileSystemColors": "Kolory systemowe", + "mobileTheme": "Motyw", + "mobileToolsTab": "Narzędzia", + "mobileWaitingForOpponentToJoin": "Oczekiwanie na dołączenie przeciwnika...", + "mobileWatchTab": "Oglądaj", "activityActivity": "Aktywność", "activityHostedALiveStream": "Udostępnił stream na żywo", "activityRankedInSwissTournament": "{param1} miejsce w {param2}", @@ -122,6 +123,7 @@ "broadcastNotYetStarted": "Transmisja jeszcze się nie rozpoczęła.", "broadcastOfficialWebsite": "Oficjalna strona", "broadcastStandings": "Klasyfikacja", + "broadcastOfficialStandings": "Oficjalna klasyfikacja", "broadcastIframeHelp": "Więcej opcji na {param}", "broadcastWebmastersPage": "stronie webmasterów", "broadcastPgnSourceHelp": "Publiczne źródło PGN w czasie rzeczywistym dla tej rundy. Oferujemy również {param} dla szybszej i skuteczniejszej synchronizacji.", @@ -130,6 +132,15 @@ "broadcastRatingDiff": "Różnica rankingu", "broadcastGamesThisTournament": "Partie w tym turnieju", "broadcastScore": "Wynik", + "broadcastAllTeams": "Wszystkie kluby", + "broadcastTournamentFormat": "Format turnieju", + "broadcastTournamentLocation": "Lokalizacja turnieju", + "broadcastTopPlayers": "Najlepsi gracze", + "broadcastTimezone": "Strefa czasowa", + "broadcastFideRatingCategory": "Kategoria rankingu FIDE", + "broadcastOptionalDetails": "Opcjonalne szczegóły", + "broadcastPastBroadcasts": "Poprzednie transmisje", + "broadcastAllBroadcastsByMonth": "Zobacz wszystkie transmisje w danym miesiącu", "broadcastNbBroadcasts": "{count, plural, =1{{count} transmisja} few{{count} transmisje} many{{count} transmisji} other{{count} transmisji}}", "challengeChallengesX": "Wyzwania: {param1}", "challengeChallengeToPlay": "Zaproś do gry", @@ -254,6 +265,7 @@ "preferencesNotifyWeb": "Przeglądarka", "preferencesNotifyDevice": "Urządzenie", "preferencesBellNotificationSound": "Dźwięk powiadomień", + "preferencesBlindfold": "Gra na ślepo", "puzzlePuzzles": "Zadania szachowe", "puzzlePuzzleThemes": "Motywy zadań", "puzzleRecommended": "Polecane", @@ -531,7 +543,7 @@ "deleteFromHere": "Usuń od tego miejsca", "collapseVariations": "Zwiń warianty", "expandVariations": "Rozwiń warianty", - "forceVariation": "Przedstaw jako wariant", + "forceVariation": "Zamień w wariant", "copyVariationPgn": "Skopiuj wariant PGN", "move": "Ruch", "variantLoss": "Wariant przegrywający", @@ -558,7 +570,7 @@ "openingExplorer": "Biblioteka otwarć", "openingEndgameExplorer": "Biblioteka otwarć i końcówek", "xOpeningExplorer": "Biblioteka otwarć {param}", - "playFirstOpeningEndgameExplorerMove": "Zagraj pierwsze posunięcie z przeglądarki otwarć/końcówek", + "playFirstOpeningEndgameExplorerMove": "Zagraj pierwsze posunięcie z biblioteki otwarć", "winPreventedBy50MoveRule": "Bez wygranej ze względu na regułę 50 ruchów", "lossSavedBy50MoveRule": "Bez przegranej ze względu na regułę 50 ruchów", "winOr50MovesByPriorMistake": "Zwycięstwo lub 50 posunięć bez rozstrzygnięcia", @@ -571,7 +583,6 @@ "replayMode": "Tryb odtwarzania", "realtimeReplay": "Jak w grze", "byCPL": "Wg SCP", - "openStudy": "Otwórz opracowanie", "enable": "Włącz", "bestMoveArrow": "Strzałka najlepszego ruchu", "showVariationArrows": "Pokaż strzałki wariantów", @@ -779,7 +790,6 @@ "block": "Zablokuj", "blocked": "Zablokowany", "unblock": "Odblokuj", - "followsYou": "Obserwuje Cię", "xStartedFollowingY": "{param1} obserwuje {param2}", "more": "Więcej", "memberSince": "Zarejestrowano", @@ -1533,6 +1543,7 @@ "studyPlayAgain": "Odtwórz ponownie", "studyWhatWouldYouPlay": "Co byś zagrał w tej pozycji?", "studyYouCompletedThisLesson": "Gratulacje! Ukończono tę lekcję.", + "studyPerPage": "{param} na stronie", "studyNbChapters": "{count, plural, =1{{count} rozdział} few{{count} rozdziały} many{{count} rozdziałów} other{{count} rozdziałów}}", "studyNbGames": "{count, plural, =1{{count} partia} few{{count} partie} many{{count} partii} other{{count} partii}}", "studyNbMembers": "{count, plural, =1{{count} uczestnik} few{{count} uczestników} many{{count} uczestników} other{{count} uczestników}}", diff --git a/lib/l10n/lila_pt.arb b/lib/l10n/lila_pt.arb index 3f4896412e..1e0f65b0d3 100644 --- a/lib/l10n/lila_pt.arb +++ b/lib/l10n/lila_pt.arb @@ -1,47 +1,47 @@ { + "mobileAllGames": "Todos os jogos", + "mobileAreYouSure": "Tens a certeza?", + "mobileBlindfoldMode": "De olhos vendados", + "mobileCancelTakebackOffer": "Cancelar pedido de voltar", + "mobileClearButton": "Limpar", + "mobileCorrespondenceClearSavedMove": "Limpar movimento salvo", + "mobileCustomGameJoinAGame": "Entrar num jogo", + "mobileFeedbackButton": "Feedback", + "mobileGreeting": "Olá, {param}", + "mobileGreetingWithoutName": "Olá", + "mobileHideVariation": "Ocultar variação", "mobileHomeTab": "Início", - "mobilePuzzlesTab": "Problemas", - "mobileToolsTab": "Tools", - "mobileWatchTab": "Assistir", - "mobileSettingsTab": "Definições", + "mobileLiveStreamers": "Streamers em direto", "mobileMustBeLoggedIn": "Tem de iniciar sessão para visualizar esta página.", - "mobileSystemColors": "Cores do sistema", - "mobileFeedbackButton": "Feedback", + "mobileNoSearchResults": "Sem resultados", + "mobileNotFollowingAnyUser": "Não segues nenhum utilizador.", "mobileOkButton": "OK", + "mobilePlayersMatchingSearchTerm": "Jogadores com \"{param}\"", + "mobilePrefMagnifyDraggedPiece": "Ampliar peça arrastada", + "mobilePuzzleStormConfirmEndRun": "Queres terminar esta corrida?", + "mobilePuzzleStormFilterNothingToShow": "Nada para mostrar, por favor, altera os filtros", + "mobilePuzzleStormNothingToShow": "Nada para mostrar. Joga alguns Puzzle Storm.", + "mobilePuzzleStormSubtitle": "Resolve quantos problemas for possível em 3 minutos.", + "mobilePuzzleStreakAbortWarning": "Perderas a tua sequência atual e a pontuação será salva.", + "mobilePuzzleThemesSubtitle": "Joga problemas das tuas aberturas favoritas, ou escolhe um tema.", + "mobilePuzzlesTab": "Problemas", + "mobileRecentSearches": "Pesquisas recentes", "mobileSettingsHapticFeedback": "Feedback tátil", "mobileSettingsImmersiveMode": "Modo imersivo", "mobileSettingsImmersiveModeSubtitle": "Ocultar a interface do sistema durante o jogo. Utiliza esta opção se sentires incomodado com os gestos de navegação do sistema nas extremidades do ecrã. Aplica-se aos ecrãs de jogo e do Puzzle Storm.", - "mobileNotFollowingAnyUser": "Não segues nenhum utilizador.", - "mobileAllGames": "Todos os jogos", - "mobileRecentSearches": "Pesquisas recentes", - "mobileClearButton": "Limpar", - "mobilePlayersMatchingSearchTerm": "Jogadores com \"{param}\"", - "mobileNoSearchResults": "Sem resultados", - "mobileAreYouSure": "Tens a certeza?", - "mobilePuzzleStreakAbortWarning": "Perderas a tua sequência atual e a pontuação será salva.", - "mobilePuzzleStormNothingToShow": "Nada para mostrar. Joga alguns Puzzle Storm.", - "mobileSharePuzzle": "Partilhar este problema", - "mobileShareGameURL": "Partilhar o URL do jogo", + "mobileSettingsTab": "Definições", "mobileShareGamePGN": "Partilhar PGN", + "mobileShareGameURL": "Partilhar o URL do jogo", "mobileSharePositionAsFEN": "Partilhar posição como FEN", - "mobileShowVariations": "Mostrar variações", - "mobileHideVariation": "Ocultar variação", + "mobileSharePuzzle": "Partilhar este problema", "mobileShowComments": "Mostrar comentários", - "mobilePuzzleStormConfirmEndRun": "Queres terminar esta corrida?", - "mobilePuzzleStormFilterNothingToShow": "Nada para mostrar, por favor, altera os filtros", - "mobileCancelTakebackOffer": "Cancelar pedido de voltar", - "mobileWaitingForOpponentToJoin": "À espera do adversário entrar...", - "mobileBlindfoldMode": "De olhos vendados", - "mobileLiveStreamers": "Streamers em direto", - "mobileCustomGameJoinAGame": "Entrar num jogo", - "mobileCorrespondenceClearSavedMove": "Limpar movimento salvo", - "mobileSomethingWentWrong": "Algo deu errado.", "mobileShowResult": "Mostrar resultado", - "mobilePuzzleThemesSubtitle": "Joga problemas das tuas aberturas favoritas, ou escolhe um tema.", - "mobilePuzzleStormSubtitle": "Resolve quantos problemas for possível em 3 minutos.", - "mobileGreeting": "Olá, {param}", - "mobileGreetingWithoutName": "Olá", - "mobilePrefMagnifyDraggedPiece": "Ampliar peça arrastada", + "mobileShowVariations": "Mostrar variações", + "mobileSomethingWentWrong": "Algo deu errado.", + "mobileSystemColors": "Cores do sistema", + "mobileToolsTab": "Tools", + "mobileWaitingForOpponentToJoin": "À espera do adversário entrar...", + "mobileWatchTab": "Assistir", "activityActivity": "Atividade", "activityHostedALiveStream": "Criou uma livestream", "activityRankedInSwissTournament": "Classificado #{param1} em {param2}", @@ -122,6 +122,7 @@ "broadcastNotYetStarted": "A transmissão ainda não começou.", "broadcastOfficialWebsite": "Website oficial", "broadcastStandings": "Classificações", + "broadcastOfficialStandings": "Classificações oficiais", "broadcastIframeHelp": "Mais opções na {param}", "broadcastWebmastersPage": "página webmasters", "broadcastPgnSourceHelp": "Uma fonte PGN pública em tempo real para esta ronda. Oferecemos também a {param} para uma sincronização mais rápida e eficiente.", @@ -130,6 +131,15 @@ "broadcastRatingDiff": "Diferença de Elo", "broadcastGamesThisTournament": "Jogos deste torneio", "broadcastScore": "Pontuação", + "broadcastAllTeams": "Todas as equipas", + "broadcastTournamentFormat": "Formato do torneio", + "broadcastTournamentLocation": "Localização do Torneio", + "broadcastTopPlayers": "Melhores jogadores", + "broadcastTimezone": "Fuso horário", + "broadcastFideRatingCategory": "Categoria do Elo FIDE", + "broadcastOptionalDetails": "Detalhes opcionais", + "broadcastPastBroadcasts": "Transmissões anteriores", + "broadcastAllBroadcastsByMonth": "Ver todas as transmissões por mês", "broadcastNbBroadcasts": "{count, plural, =1{{count} transmissão} other{{count} transmissões}}", "challengeChallengesX": "Desafios: {param1}", "challengeChallengeToPlay": "Desafiar para jogar", @@ -254,6 +264,7 @@ "preferencesNotifyWeb": "Navegador", "preferencesNotifyDevice": "Dispositivo", "preferencesBellNotificationSound": "Som da notificação", + "preferencesBlindfold": "De olhos vendados", "puzzlePuzzles": "Problemas", "puzzlePuzzleThemes": "Temas de problemas", "puzzleRecommended": "Recomendado", @@ -571,7 +582,6 @@ "replayMode": "Modo de repetição", "realtimeReplay": "Tempo real", "byCPL": "Por CPL", - "openStudy": "Abrir estudo", "enable": "Ativar", "bestMoveArrow": "Seta de melhor movimento", "showVariationArrows": "Ver setas de variação", @@ -662,6 +672,7 @@ "rank": "Classificação", "rankX": "Posição: {param}", "gamesPlayed": "Partidas jogadas", + "ok": "OK", "cancel": "Cancelar", "whiteTimeOut": "Acabou o tempo das brancas", "blackTimeOut": "Acabou o tempo das pretas", @@ -767,7 +778,7 @@ "whiteCheckmatesInOneMove": "As brancas dão mate em um movimento", "blackCheckmatesInOneMove": "As pretas dão mate em um movimento", "retry": "Tentar novamente", - "reconnecting": "Reconectando", + "reconnecting": "A reconectar", "noNetwork": "Desligado", "favoriteOpponents": "Adversários favoritos", "follow": "Seguir", @@ -778,7 +789,6 @@ "block": "Bloquear", "blocked": "Bloqueado", "unblock": "Desbloquear", - "followsYou": "Segue-te", "xStartedFollowingY": "{param1} começou a seguir {param2}", "more": "Mais", "memberSince": "Membro desde", @@ -1532,6 +1542,7 @@ "studyPlayAgain": "Jogar novamente", "studyWhatWouldYouPlay": "O que jogaria nessa situação?", "studyYouCompletedThisLesson": "Parabéns! Completou esta lição.", + "studyPerPage": "{param} por página", "studyNbChapters": "{count, plural, =1{{count} capítulo} other{{count} capítulos}}", "studyNbGames": "{count, plural, =1{{count} Jogo} other{{count} Jogos}}", "studyNbMembers": "{count, plural, =1{{count} membro} other{{count} membros}}", diff --git a/lib/l10n/lila_pt_BR.arb b/lib/l10n/lila_pt_BR.arb index e099ca8d11..dd1117d7ed 100644 --- a/lib/l10n/lila_pt_BR.arb +++ b/lib/l10n/lila_pt_BR.arb @@ -1,47 +1,48 @@ { + "mobileAllGames": "Todos os jogos", + "mobileAreYouSure": "Você tem certeza?", + "mobileBlindfoldMode": "Venda", + "mobileCancelTakebackOffer": "Cancelar oferta de revanche", + "mobileClearButton": "Limpar", + "mobileCorrespondenceClearSavedMove": "Limpar movimento salvos", + "mobileCustomGameJoinAGame": "Entrar em um jogo", + "mobileFeedbackButton": "Comentários", + "mobileGreeting": "Olá, {param}", + "mobileGreetingWithoutName": "Olá", + "mobileHideVariation": "Ocultar variante forçada", "mobileHomeTab": "Início", - "mobilePuzzlesTab": "Problemas", - "mobileToolsTab": "Ferramentas", - "mobileWatchTab": "Assistir", - "mobileSettingsTab": "Ajustes", + "mobileLiveStreamers": "Streamers do Lichess", "mobileMustBeLoggedIn": "Você precisa estar logado para ver essa pagina.", - "mobileSystemColors": "Cores do sistema", - "mobileFeedbackButton": "Comentários", + "mobileNoSearchResults": "Sem Resultados", + "mobileNotFollowingAnyUser": "Você não está seguindo nenhum usuário.", "mobileOkButton": "Ok", + "mobilePlayersMatchingSearchTerm": "Usuários com \"{param}\"", + "mobilePrefMagnifyDraggedPiece": "Ampliar peça segurada", + "mobilePuzzleStormConfirmEndRun": "Você quer terminar o turno?", + "mobilePuzzleStormFilterNothingToShow": "Nada para mostrar aqui, por favor, altere os filtros", + "mobilePuzzleStormNothingToShow": "Nada para mostrar aqui. Jogue algumas rodadas da Puzzle Storm.", + "mobilePuzzleStormSubtitle": "Resolva quantos quebra-cabeças for possível em 3 minutos.", + "mobilePuzzleStreakAbortWarning": "Você perderá a sua sequência atual e sua pontuação será salva.", + "mobilePuzzleThemesSubtitle": "Jogue quebra-cabeças de suas aberturas favoritas, ou escolha um tema.", + "mobilePuzzlesTab": "Problemas", + "mobileRecentSearches": "Pesquisas recentes", "mobileSettingsHapticFeedback": "Vibrar ao trocar", "mobileSettingsImmersiveMode": "Modo imersivo", "mobileSettingsImmersiveModeSubtitle": "Ocultar a “interface” do sistema durante a reprodução. Use isto se você estiver incomodado com gestor de navegação do sistema nas bordas da tela. Aplica-se às telas dos jogos e desafios.", - "mobileNotFollowingAnyUser": "Você não está seguindo nenhum usuário.", - "mobileAllGames": "Todos os jogos", - "mobileRecentSearches": "Pesquisas recentes", - "mobileClearButton": "Limpar", - "mobilePlayersMatchingSearchTerm": "Usuários com \"{param}\"", - "mobileNoSearchResults": "Sem Resultados", - "mobileAreYouSure": "Você tem certeza?", - "mobilePuzzleStreakAbortWarning": "Você perderá a sua sequência atual e sua pontuação será salva.", - "mobilePuzzleStormNothingToShow": "Nada para mostrar aqui. Jogue algumas rodadas da Puzzle Storm.", - "mobileSharePuzzle": "Compartilhar este quebra-cabeça", - "mobileShareGameURL": "Compartilhar URL do jogo", + "mobileSettingsTab": "Ajustes", "mobileShareGamePGN": "Compartilhar PGN", + "mobileShareGameURL": "Compartilhar URL do jogo", "mobileSharePositionAsFEN": "Compartilhar posição como FEN", - "mobileShowVariations": "Mostrar setas de variantes", - "mobileHideVariation": "Ocultar variante forçada", + "mobileSharePuzzle": "Compartilhar este quebra-cabeça", "mobileShowComments": "Mostrar comentários", - "mobilePuzzleStormConfirmEndRun": "Você quer terminar o turno?", - "mobilePuzzleStormFilterNothingToShow": "Nada para mostrar aqui, por favor, altere os filtros", - "mobileCancelTakebackOffer": "Cancelar oferta de revanche", - "mobileWaitingForOpponentToJoin": "Esperando por um oponente...", - "mobileBlindfoldMode": "Venda", - "mobileLiveStreamers": "Streamers do Lichess", - "mobileCustomGameJoinAGame": "Entrar em um jogo", - "mobileCorrespondenceClearSavedMove": "Limpar movimento salvos", - "mobileSomethingWentWrong": "Houve algum problema.", "mobileShowResult": "Mostrar resultado", - "mobilePuzzleThemesSubtitle": "Jogue quebra-cabeças de suas aberturas favoritas, ou escolha um tema.", - "mobilePuzzleStormSubtitle": "Resolva quantos quebra-cabeças for possível em 3 minutos.", - "mobileGreeting": "Olá, {param}", - "mobileGreetingWithoutName": "Olá", - "mobilePrefMagnifyDraggedPiece": "Ampliar peça segurada", + "mobileShowVariations": "Mostrar setas de variantes", + "mobileSomethingWentWrong": "Houve algum problema.", + "mobileSystemColors": "Cores do sistema", + "mobileTheme": "Tema", + "mobileToolsTab": "Ferramentas", + "mobileWaitingForOpponentToJoin": "Esperando por um oponente...", + "mobileWatchTab": "Assistir", "activityActivity": "Atividade", "activityHostedALiveStream": "Iniciou uma transmissão ao vivo", "activityRankedInSwissTournament": "Classificado #{param1} entre {param2}", @@ -122,6 +123,7 @@ "broadcastNotYetStarted": "A transmissão ainda não começou.", "broadcastOfficialWebsite": "Site oficial", "broadcastStandings": "Classificação", + "broadcastOfficialStandings": "Classificação oficial", "broadcastIframeHelp": "Mais opções na {param}", "broadcastWebmastersPage": "página dos webmasters", "broadcastPgnSourceHelp": "Uma fonte PGN pública ao vivo desta rodada. Há também a {param} para uma sincronização mais rápida e eficiente.", @@ -130,6 +132,15 @@ "broadcastRatingDiff": "Diferência de pontos", "broadcastGamesThisTournament": "Jogos neste torneio", "broadcastScore": "Pontuação", + "broadcastAllTeams": "Todas as equipes", + "broadcastTournamentFormat": "Formato do torneio", + "broadcastTournamentLocation": "Local do torneio", + "broadcastTopPlayers": "Melhores jogadores", + "broadcastTimezone": "Fuso horário", + "broadcastFideRatingCategory": "Categoria de rating FIDE", + "broadcastOptionalDetails": "Detalhes opcionais", + "broadcastPastBroadcasts": "Transmissões passadas", + "broadcastAllBroadcastsByMonth": "Ver todas as transmissões por mês", "broadcastNbBroadcasts": "{count, plural, =1{{count} transmissão} other{{count} transmissões}}", "challengeChallengesX": "Desafios: {param1}", "challengeChallengeToPlay": "Desafiar para jogar", @@ -254,6 +265,7 @@ "preferencesNotifyWeb": "Navegador", "preferencesNotifyDevice": "Dispositivo", "preferencesBellNotificationSound": "Som da notificação", + "preferencesBlindfold": "Às cegas", "puzzlePuzzles": "Quebra-cabeças", "puzzlePuzzleThemes": "Temas de quebra-cabeça", "puzzleRecommended": "Recomendado", @@ -468,7 +480,7 @@ "toInviteSomeoneToPlayGiveThisUrl": "Para convidar alguém para jogar, envie este URL", "gameOver": "Fim da partida", "waitingForOpponent": "Aguardando oponente", - "orLetYourOpponentScanQrCode": "Ou deixe seu oponente ler este QR Code", + "orLetYourOpponentScanQrCode": "Ou deixe seu oponente ler este código QR", "waiting": "Aguardando", "yourTurn": "Sua vez", "aiNameLevelAiLevel": "{param1} nível {param2}", @@ -509,8 +521,8 @@ "blackResigned": "Pretas desistiram", "whiteLeftTheGame": "Brancas deixaram a partida", "blackLeftTheGame": "Pretas deixaram a partida", - "whiteDidntMove": "As brancas não se moveram", - "blackDidntMove": "As pretas não se moveram", + "whiteDidntMove": "Brancas não moveram", + "blackDidntMove": "Pretas não moveram", "requestAComputerAnalysis": "Solicitar uma análise do computador", "computerAnalysis": "Análise do computador", "computerAnalysisAvailable": "Análise de computador disponível", @@ -529,8 +541,8 @@ "promoteVariation": "Promover variante", "makeMainLine": "Transformar em linha principal", "deleteFromHere": "Excluir a partir daqui", - "collapseVariations": "Esconder variantes", - "expandVariations": "Mostrar variantes", + "collapseVariations": "Recolher variações", + "expandVariations": "Expandir variações", "forceVariation": "Variante forçada", "copyVariationPgn": "Copiar PGN da variante", "move": "Movimentos", @@ -550,7 +562,7 @@ "recentGames": "Partidas recentes", "topGames": "Melhores partidas", "masterDbExplanation": "Duas milhões de partidas de jogadores com pontuação FIDE acima de {param1}, desde {param2} a {param3}", - "dtzWithRounding": "DTZ50\" com arredondamento, baseado no número de meias-jogadas até a próxima captura ou jogada de peão", + "dtzWithRounding": "DTZ50\" com arredondamento, baseado no número de lances até a próxima captura ou movimento de peão", "noGameFound": "Nenhuma partida encontrada", "maxDepthReached": "Profundidade máxima alcançada!", "maybeIncludeMoreGamesFromThePreferencesMenu": "Talvez você queira incluir mais jogos a partir do menu de preferências", @@ -571,7 +583,6 @@ "replayMode": "Rever a partida", "realtimeReplay": "Tempo Real", "byCPL": "Por erros", - "openStudy": "Abrir estudo", "enable": "Ativar", "bestMoveArrow": "Seta de melhor movimento", "showVariationArrows": "Mostrar setas das variantes", @@ -756,7 +767,7 @@ "orUploadPgnFile": "Ou carregue um arquivo PGN", "fromPosition": "A partir da posição", "continueFromHere": "Continuar daqui", - "toStudy": "Estudo", + "toStudy": "Estudar", "importGame": "Importar partida", "importGameExplanation": "Após colar uma partida em PGN você poderá revisá-la interativamente, consultar uma análise de computador, utilizar o chat e compartilhar um link.", "importGameCaveat": "As variantes serão apagadas. Para salvá-las, importe o PGN em um estudo.", @@ -779,7 +790,6 @@ "block": "Bloquear", "blocked": "Bloqueado", "unblock": "Desbloquear", - "followsYou": "Segue você", "xStartedFollowingY": "{param1} começou a seguir {param2}", "more": "Mais", "memberSince": "Membro desde", @@ -821,7 +831,7 @@ "blackWins": "Pretas venceram", "drawRate": "Taxa de empates", "draws": "Empates", - "nextXTournament": "Próximo torneio {param}:", + "nextXTournament": "Próximo torneio de {param}:", "averageOpponent": "Pontuação média adversários", "boardEditor": "Editor de tabuleiro", "setTheBoard": "Defina a posição", @@ -906,7 +916,7 @@ "newPasswordAgain": "Nova senha (novamente)", "newPasswordsDontMatch": "As novas senhas não correspondem", "newPasswordStrength": "Senha forte", - "clockInitialTime": "Tempo de relógio", + "clockInitialTime": "Tempo inicial no relógio", "clockIncrement": "Incremento do relógio", "privacy": "Privacidade", "privacyPolicy": "Política de privacidade", @@ -1298,7 +1308,7 @@ "nbDays": "{count, plural, =1{{count} dias} other{{count} dias}}", "nbHours": "{count, plural, =1{{count} horas} other{{count} horas}}", "nbMinutes": "{count, plural, =1{{count} minuto} other{{count} minutos}}", - "rankIsUpdatedEveryNbMinutes": "{count, plural, =1{O ranking é atualizado a cada {count} minutos} other{O ranking é atualizado a cada {count} minutos}}", + "rankIsUpdatedEveryNbMinutes": "{count, plural, =1{O ranking é atualizado a cada {count} minuto} other{O ranking é atualizado a cada {count} minutos}}", "nbPuzzles": "{count, plural, =1{{count} quebra-cabeça} other{{count} problemas}}", "nbGamesWithYou": "{count, plural, =1{{count} partidas contra você} other{{count} partidas contra você}}", "nbRated": "{count, plural, =1{{count} valendo pontos} other{{count} valendo pontos}}", @@ -1533,6 +1543,7 @@ "studyPlayAgain": "Jogar novamente", "studyWhatWouldYouPlay": "O que você jogaria nessa posição?", "studyYouCompletedThisLesson": "Parabéns! Você completou essa lição.", + "studyPerPage": "{param} por página", "studyNbChapters": "{count, plural, =1{{count} Capítulo} other{{count} Capítulos}}", "studyNbGames": "{count, plural, =1{{count} Jogo} other{{count} Jogos}}", "studyNbMembers": "{count, plural, =1{{count} Membro} other{{count} Membros}}", diff --git a/lib/l10n/lila_ro.arb b/lib/l10n/lila_ro.arb index b47783dbb8..6710eb2dbe 100644 --- a/lib/l10n/lila_ro.arb +++ b/lib/l10n/lila_ro.arb @@ -1,47 +1,48 @@ { + "mobileAllGames": "Toate jocurile", + "mobileAreYouSure": "Ești sigur?", + "mobileBlindfoldMode": "Legat la ochi", + "mobileCancelTakebackOffer": "Anulați propunerea de revanșă", + "mobileClearButton": "Resetare", + "mobileCorrespondenceClearSavedMove": "Șterge mutarea salvată", + "mobileCustomGameJoinAGame": "Alătură-te unui joc", + "mobileFeedbackButton": "Feedback", + "mobileGreeting": "Salut, {param}", + "mobileGreetingWithoutName": "Salut", + "mobileHideVariation": "Ascunde variațiile", "mobileHomeTab": "Acasă", - "mobilePuzzlesTab": "Puzzle-uri", - "mobileToolsTab": "Unelte", - "mobileWatchTab": "Vizionează", - "mobileSettingsTab": "Setări", + "mobileLiveStreamers": "Fluxuri live", "mobileMustBeLoggedIn": "Trebuie să te autentifici pentru a accesa această pagină.", - "mobileSystemColors": "Culori sistem", - "mobileFeedbackButton": "Feedback", + "mobileNoSearchResults": "Niciun rezultat", + "mobileNotFollowingAnyUser": "Nu urmărești niciun utilizator.", "mobileOkButton": "OK", + "mobilePlayersMatchingSearchTerm": "Jucători cu \"{param}\"", + "mobilePrefMagnifyDraggedPiece": "Mărește piesa trasă", + "mobilePuzzleStormConfirmEndRun": "Vrei să termini acest run?", + "mobilePuzzleStormFilterNothingToShow": "Nimic de afișat, vă rugăm să schimbați filtrele", + "mobilePuzzleStormNothingToShow": "Nimic de arătat. Jucați câteva partide de Puzzle Storm.", + "mobilePuzzleStormSubtitle": "Rezolvă cât mai multe puzzle-uri în 3 minute.", + "mobilePuzzleStreakAbortWarning": "Îți vei pierde streak-ul actual iar scorul va fi salvat.", + "mobilePuzzleThemesSubtitle": "Joacă puzzle-uri din deschiderile tale preferate sau alege o temă.", + "mobilePuzzlesTab": "Puzzle-uri", + "mobileRecentSearches": "Căutări recente", "mobileSettingsHapticFeedback": "Control tactil", "mobileSettingsImmersiveMode": "Mod imersiv", "mobileSettingsImmersiveModeSubtitle": "Ascunde interfața de utilizator a sistemului în timpul jocului. Folosește această opțiune dacă ești deranjat de gesturile de navigare ale sistemului la marginile ecranului. Se aplică pentru ecranele de joc și Puzzle Storm.", - "mobileNotFollowingAnyUser": "Nu urmărești niciun utilizator.", - "mobileAllGames": "Toate jocurile", - "mobileRecentSearches": "Căutări recente", - "mobileClearButton": "Resetare", - "mobilePlayersMatchingSearchTerm": "Jucători cu \"{param}\"", - "mobileNoSearchResults": "Niciun rezultat", - "mobileAreYouSure": "Ești sigur?", - "mobilePuzzleStreakAbortWarning": "Îți vei pierde streak-ul actual iar scorul va fi salvat.", - "mobilePuzzleStormNothingToShow": "Nimic de arătat. Jucați câteva partide de Puzzle Storm.", - "mobileSharePuzzle": "Distribuie acest puzzle", - "mobileShareGameURL": "Distribuie URL-ul jocului", + "mobileSettingsTab": "Setări", "mobileShareGamePGN": "Distribuie PGN", + "mobileShareGameURL": "Distribuie URL-ul jocului", "mobileSharePositionAsFEN": "Distribuie poziția ca FEN", - "mobileShowVariations": "Arată variațiile", - "mobileHideVariation": "Ascunde variațiile", + "mobileSharePuzzle": "Distribuie acest puzzle", "mobileShowComments": "Afişează сomentarii", - "mobilePuzzleStormConfirmEndRun": "Vrei să termini acest run?", - "mobilePuzzleStormFilterNothingToShow": "Nimic de afișat, vă rugăm să schimbați filtrele", - "mobileCancelTakebackOffer": "Anulați propunerea de revanșă", - "mobileWaitingForOpponentToJoin": "În așteptarea unui jucător...", - "mobileBlindfoldMode": "Legat la ochi", - "mobileLiveStreamers": "Fluxuri live", - "mobileCustomGameJoinAGame": "Alătură-te unui joc", - "mobileCorrespondenceClearSavedMove": "Șterge mutarea salvată", - "mobileSomethingWentWrong": "Ceva nu a mers bine. :(", "mobileShowResult": "Arată rezultatul", - "mobilePuzzleThemesSubtitle": "Joacă puzzle-uri din deschiderile tale preferate sau alege o temă.", - "mobilePuzzleStormSubtitle": "Rezolvă cât mai multe puzzle-uri în 3 minute.", - "mobileGreeting": "Salut, {param}", - "mobileGreetingWithoutName": "Salut", - "mobilePrefMagnifyDraggedPiece": "Mărește piesa trasă", + "mobileShowVariations": "Arată variațiile", + "mobileSomethingWentWrong": "Ceva nu a mers bine. :(", + "mobileSystemColors": "Culori sistem", + "mobileTheme": "Tema", + "mobileToolsTab": "Unelte", + "mobileWaitingForOpponentToJoin": "În așteptarea unui jucător...", + "mobileWatchTab": "Vizionează", "activityActivity": "Activitate", "activityHostedALiveStream": "A găzduit un live stream", "activityRankedInSwissTournament": "Evaluat #{param1} în {param2}", @@ -110,7 +111,12 @@ "broadcastOpenLichess": "Deschide în Lichess", "broadcastTeams": "Echipe", "broadcastStandings": "Clasament", + "broadcastOfficialStandings": "Clasament oficial", "broadcastScore": "Scor", + "broadcastAllTeams": "Toate echipele", + "broadcastTournamentFormat": "Format turneu", + "broadcastTournamentLocation": "Locație turneu", + "broadcastTimezone": "Fus orar", "broadcastNbBroadcasts": "{count, plural, =1{{count} transmisiune} few{{count} transmisiuni} other{{count} de transmisiuni}}", "challengeChallengesX": "Provocări: {param1}", "challengeChallengeToPlay": "Provoacă la o partidă", @@ -235,6 +241,7 @@ "preferencesNotifyWeb": "Navigator", "preferencesNotifyDevice": "Dispozitiv", "preferencesBellNotificationSound": "Sunet de notificare", + "preferencesBlindfold": "Legat la ochi", "puzzlePuzzles": "Probleme de șah", "puzzlePuzzleThemes": "Teme pentru problemele de șah", "puzzleRecommended": "Recomandare", @@ -430,7 +437,7 @@ "puzzleThemeXRayAttackDescription": "O piesă atacă sau apară un patrat, printr-o piesă inamică.", "puzzleThemeZugzwang": "Zugzwang", "puzzleThemeZugzwangDescription": "Adversarul este limitat în mișcările pe care le poate face, iar toate mișcările îi înrăutățesc poziția.", - "puzzleThemeMix": "Amestec sănătos", + "puzzleThemeMix": "Mixt", "puzzleThemeMixDescription": "Un pic din toate. Nu știi la ce să te aștepți, așa că rămâi gata pentru orice! La fel ca în jocurile reale.", "puzzleThemePlayerGames": "Partide jucători", "puzzleThemePlayerGamesDescription": "Caută puzzle-uri generate din partidele tale sau din partidele unui alt jucător.", @@ -552,7 +559,6 @@ "replayMode": "Modul de reluare", "realtimeReplay": "În timp real", "byCPL": "După CPL", - "openStudy": "Studiu deschis", "enable": "Activează", "bestMoveArrow": "Săgeată pentru cea mai bună mutare", "showVariationArrows": "Afișează săgețile variației", @@ -643,6 +649,7 @@ "rank": "Clasificare", "rankX": "Loc în clasament: {param}", "gamesPlayed": "Partide jucate", + "ok": "OK", "cancel": "Anulare", "whiteTimeOut": "Timpul pentru alb a expirat", "blackTimeOut": "Timpul pentru negru a expirat", @@ -759,7 +766,6 @@ "block": "Blocare", "blocked": "Blocat", "unblock": "Deblocare", - "followsYou": "Vă urmărește", "xStartedFollowingY": "{param1} a început să vă urmărească {param2}", "more": "Mai mult", "memberSince": "Membru din", @@ -1513,6 +1519,7 @@ "studyPlayAgain": "Joacă din nou", "studyWhatWouldYouPlay": "Ce ai juca în această poziție?", "studyYouCompletedThisLesson": "Felicitări! Ai terminat această lecție.", + "studyPerPage": "{param} pe pagină", "studyNbChapters": "{count, plural, =1{{count} capitol} few{{count} capitole} other{{count} capitole}}", "studyNbGames": "{count, plural, =1{{count} partidă} few{{count} partide} other{{count} partide}}", "studyNbMembers": "{count, plural, =1{{count} membru} few{{count} membri} other{{count} membri}}", diff --git a/lib/l10n/lila_ru.arb b/lib/l10n/lila_ru.arb index 9e5ff2160a..2f09718b5b 100644 --- a/lib/l10n/lila_ru.arb +++ b/lib/l10n/lila_ru.arb @@ -1,47 +1,48 @@ { + "mobileAllGames": "Все игры", + "mobileAreYouSure": "Вы уверены?", + "mobileBlindfoldMode": "Игра вслепую", + "mobileCancelTakebackOffer": "Отменить предложение о возврате хода", + "mobileClearButton": "Очистить", + "mobileCorrespondenceClearSavedMove": "Очистить сохранённый ход", + "mobileCustomGameJoinAGame": "Присоединиться к игре", + "mobileFeedbackButton": "Отзыв", + "mobileGreeting": "Привет, {param}", + "mobileGreetingWithoutName": "Привет", + "mobileHideVariation": "Скрыть варианты", "mobileHomeTab": "Главная", - "mobilePuzzlesTab": "Задачи", - "mobileToolsTab": "Анализ", - "mobileWatchTab": "Просмотр", - "mobileSettingsTab": "Настройки", + "mobileLiveStreamers": "Стримеры в эфире", "mobileMustBeLoggedIn": "Вы должны войти для просмотра этой страницы.", - "mobileSystemColors": "Цвет интерфейса", - "mobileFeedbackButton": "Отзыв", + "mobileNoSearchResults": "Ничего не найденo", + "mobileNotFollowingAnyUser": "Вы не подписаны на других пользователей.", "mobileOkButton": "ОК", + "mobilePlayersMatchingSearchTerm": "Игроки, содержащие «{param}»", + "mobilePrefMagnifyDraggedPiece": "Увеличивать перетаскиваемую фигуру", + "mobilePuzzleStormConfirmEndRun": "Хотите закончить эту попытку?", + "mobilePuzzleStormFilterNothingToShow": "Ничего не найдено, измените фильтры, пожалуйста", + "mobilePuzzleStormNothingToShow": "Ничего нет. Сыграйте несколько попыток.", + "mobilePuzzleStormSubtitle": "Решите как можно больше задач за 3 минуты.", + "mobilePuzzleStreakAbortWarning": "Вы потеряете свою текущую серию, и результаты будут сохранены.", + "mobilePuzzleThemesSubtitle": "Решайте задачи по вашим любимым дебютам или выберите тему.", + "mobilePuzzlesTab": "Задачи", + "mobileRecentSearches": "Последние запросы", "mobileSettingsHapticFeedback": "Виброотклик", "mobileSettingsImmersiveMode": "Полноэкранный режим", "mobileSettingsImmersiveModeSubtitle": "Скрывать интерфейс во время игры. Воспользуйтесь, если вам мешает навигация по краям экрана. Применяется в режиме партий и задач.", - "mobileNotFollowingAnyUser": "Вы не подписаны на других пользователей.", - "mobileAllGames": "Все игры", - "mobileRecentSearches": "Последние запросы", - "mobileClearButton": "Очистить", - "mobilePlayersMatchingSearchTerm": "Игроки, содержащие «{param}»", - "mobileNoSearchResults": "Ничего не найденo", - "mobileAreYouSure": "Вы уверены?", - "mobilePuzzleStreakAbortWarning": "Вы потеряете свою текущую серию, и результаты будут сохранены.", - "mobilePuzzleStormNothingToShow": "Ничего нет. Сыграйте несколько попыток.", - "mobileSharePuzzle": "Поделиться задачей", - "mobileShareGameURL": "Поделиться ссылкой на игру", + "mobileSettingsTab": "Настройки", "mobileShareGamePGN": "Поделиться PGN", + "mobileShareGameURL": "Поделиться ссылкой на игру", "mobileSharePositionAsFEN": "Поделиться FEN", - "mobileShowVariations": "Показывать варианты", - "mobileHideVariation": "Скрыть варианты", + "mobileSharePuzzle": "Поделиться задачей", "mobileShowComments": "Показать комментарии", - "mobilePuzzleStormConfirmEndRun": "Хотите закончить эту попытку?", - "mobilePuzzleStormFilterNothingToShow": "Ничего не найдено, измените фильтры, пожалуйста", - "mobileCancelTakebackOffer": "Отменить предложение о возврате хода", - "mobileWaitingForOpponentToJoin": "Ожидание соперника...", - "mobileBlindfoldMode": "Игра вслепую", - "mobileLiveStreamers": "Стримеры в эфире", - "mobileCustomGameJoinAGame": "Присоединиться к игре", - "mobileCorrespondenceClearSavedMove": "Очистить сохранённый ход", - "mobileSomethingWentWrong": "Что-то пошло не так.", "mobileShowResult": "Показать результат", - "mobilePuzzleThemesSubtitle": "Решайте задачи по вашим любимым дебютам или выберите тему.", - "mobilePuzzleStormSubtitle": "Решите как можно больше задач за 3 минуты.", - "mobileGreeting": "Привет, {param}", - "mobileGreetingWithoutName": "Привет", - "mobilePrefMagnifyDraggedPiece": "Увеличивать перетаскиваемую фигуру", + "mobileShowVariations": "Показывать варианты", + "mobileSomethingWentWrong": "Что-то пошло не так.", + "mobileSystemColors": "Цвет интерфейса", + "mobileTheme": "Оформление", + "mobileToolsTab": "Анализ", + "mobileWaitingForOpponentToJoin": "Ожидание соперника...", + "mobileWatchTab": "Просмотр", "activityActivity": "Активность", "activityHostedALiveStream": "Проведён стрим", "activityRankedInSwissTournament": "Занято {param1} место в {param2}", @@ -122,6 +123,7 @@ "broadcastNotYetStarted": "Трансляция ещё не началась.", "broadcastOfficialWebsite": "Официальный веб-сайт", "broadcastStandings": "Турнирная таблица", + "broadcastOfficialStandings": "Официальная турнирная таблица", "broadcastIframeHelp": "Больше опций на {param}", "broadcastWebmastersPage": "странице веб-мастера", "broadcastPgnSourceHelp": "Публичный PGN-источник для этого раунда в реальном времени. Мы также предлагаем {param} для более быстрой и эффективной синхронизации.", @@ -130,6 +132,15 @@ "broadcastRatingDiff": "Разница в рейтингах", "broadcastGamesThisTournament": "Партии этого турнира", "broadcastScore": "Очки", + "broadcastAllTeams": "Все клубы", + "broadcastTournamentFormat": "Формат турнира", + "broadcastTournamentLocation": "Местоположение турнира", + "broadcastTopPlayers": "Лучшие игроки", + "broadcastTimezone": "Часовой пояс", + "broadcastFideRatingCategory": "Категория рейтинга FIDE", + "broadcastOptionalDetails": "Необязательные данные", + "broadcastPastBroadcasts": "Завершённые трансляции", + "broadcastAllBroadcastsByMonth": "Просмотр всех трансляций за месяц", "broadcastNbBroadcasts": "{count, plural, =1{{count} трансляция} few{{count} трансляции} many{{count} трансляций} other{{count} трансляций}}", "challengeChallengesX": "Вызовов: {param1}", "challengeChallengeToPlay": "Вызвать на игру", @@ -254,6 +265,7 @@ "preferencesNotifyWeb": "Браузер", "preferencesNotifyDevice": "Устройство", "preferencesBellNotificationSound": "Звук колокольчика уведомлений", + "preferencesBlindfold": "Игра вслепую", "puzzlePuzzles": "Задачи", "puzzlePuzzleThemes": "Темы задач", "puzzleRecommended": "Рекомендуемые", @@ -327,7 +339,7 @@ "puzzleStrengthDescription": "Вы показываете лучшие результаты в этих темах", "puzzlePlayedXTimes": "{count, plural, =1{Решено {count} раз} few{Решено {count} раза} many{Решено {count} раз} other{Решено {count} раз}}", "puzzleNbPointsBelowYourPuzzleRating": "{count, plural, =1{Один балл ниже вашего рейтинга в задачах} few{{count} баллов ниже вашего рейтинга в задачах} many{{count} баллов ниже вашего рейтинга в задачах} other{{count} баллов ниже вашего рейтинга в задачах}}", - "puzzleNbPointsAboveYourPuzzleRating": "{count, plural, =1{Один балл выше вашего рейтинга в пазлах} few{{count} баллов выше вашего рейтинга в задачах} many{{count} баллов выше вашего рейтинга в задачах} other{{count} баллов выше вашего рейтинга в задачах}}", + "puzzleNbPointsAboveYourPuzzleRating": "{count, plural, =1{Один балл выше вашего рейтинга в задачах} few{{count} баллов выше вашего рейтинга в задачах} many{{count} баллов выше вашего рейтинга в задачах} other{{count} баллов выше вашего рейтинга в задачах}}", "puzzleNbPlayed": "{count, plural, =1{{count} решена} few{{count} решены} many{{count} решены} other{{count} решено}}", "puzzleNbToReplay": "{count, plural, =1{{count} повторить} few{{count} повторить} many{{count} повторить} other{{count} повторить}}", "puzzleThemeAdvancedPawn": "Продвинутая пешка", @@ -571,7 +583,6 @@ "replayMode": "Смотреть в повторе", "realtimeReplay": "Как в партии", "byCPL": "По ошибкам", - "openStudy": "Открыть в студии", "enable": "Включить", "bestMoveArrow": "Показывать лучшие ходы стрелками", "showVariationArrows": "Показать стрелки вариантов", @@ -662,6 +673,7 @@ "rank": "Ранг", "rankX": "Место: {param}", "gamesPlayed": "Сыграно партий", + "ok": "ОК", "cancel": "Отменить", "whiteTimeOut": "Белые просрочили время", "blackTimeOut": "Чёрные просрочили время", @@ -778,7 +790,6 @@ "block": "Заблокировать", "blocked": "Заблокированные", "unblock": "Разблокировать", - "followsYou": "Подписан на вас", "xStartedFollowingY": "{param1} подписался на {param2}", "more": "Ещё", "memberSince": "Дата регистрации", @@ -1296,7 +1307,7 @@ "nbBookmarks": "{count, plural, =1{{count} отмеченная} few{{count} отмеченные} many{{count} отмеченных} other{{count} отмеченных}}", "nbDays": "{count, plural, =1{{count} день} few{{count} дня} many{{count} дней} other{{count} дней}}", "nbHours": "{count, plural, =1{{count} час} few{{count} часа} many{{count} часов} other{{count} часов}}", - "nbMinutes": "{count, plural, =1{{count} одна минута} few{{count} минуты} many{{count} минут} other{{count} минут}}", + "nbMinutes": "{count, plural, =1{{count} Одна минута} few{{count} Минуты} many{{count} минут} other{{count} минут}}", "rankIsUpdatedEveryNbMinutes": "{count, plural, =1{Место обновляется ежеминутно} few{Место обновляется каждые {count} минуты} many{Место обновляется каждые {count} минут} other{Место обновляется каждые {count} минут}}", "nbPuzzles": "{count, plural, =1{{count} задача} few{{count} задачи} many{{count} задач} other{{count} задач}}", "nbGamesWithYou": "{count, plural, =1{{count} партия с вами} few{{count} партии с вами} many{{count} партий с вами} other{{count} партий с вами}}", @@ -1532,6 +1543,7 @@ "studyPlayAgain": "Сыграть снова", "studyWhatWouldYouPlay": "Как бы вы сыграли в этой позиции?", "studyYouCompletedThisLesson": "Поздравляем! Вы прошли этот урок.", + "studyPerPage": "{param} на страницу", "studyNbChapters": "{count, plural, =1{{count} глава} few{{count} главы} many{{count} глав} other{{count} глав}}", "studyNbGames": "{count, plural, =1{{count} партия} few{{count} партии} many{{count} партий} other{{count} партий}}", "studyNbMembers": "{count, plural, =1{{count} участник} few{{count} участника} many{{count} участников} other{{count} участников}}", diff --git a/lib/l10n/lila_sk.arb b/lib/l10n/lila_sk.arb index 99b8f7ecab..571fce1b2e 100644 --- a/lib/l10n/lila_sk.arb +++ b/lib/l10n/lila_sk.arb @@ -1,47 +1,48 @@ { + "mobileAllGames": "Všetky partie", + "mobileAreYouSure": "Ste si istý?", + "mobileBlindfoldMode": "Naslepo", + "mobileCancelTakebackOffer": "Zrušiť žiadosť o vrátenie ťahu", + "mobileClearButton": "Odstrániť", + "mobileCorrespondenceClearSavedMove": "Vymazať uložený ťah", + "mobileCustomGameJoinAGame": "Pripojiť sa k partii", + "mobileFeedbackButton": "Spätná väzba", + "mobileGreeting": "Ahoj, {param}", + "mobileGreetingWithoutName": "Ahoj", + "mobileHideVariation": "Skryť varianty", "mobileHomeTab": "Domov", - "mobilePuzzlesTab": "Úlohy", - "mobileToolsTab": "Nástroje", - "mobileWatchTab": "Sledovať", - "mobileSettingsTab": "Nastavenia", + "mobileLiveStreamers": "Vysielajúci strímeri", "mobileMustBeLoggedIn": "Na zobrazenie tejto stránky musíte byť prihlásený.", - "mobileSystemColors": "Farby operačného systému", - "mobileFeedbackButton": "Spätná väzba", + "mobileNoSearchResults": "Nič sa nenašlo", + "mobileNotFollowingAnyUser": "Nesledujete žiadneho používateľa.", "mobileOkButton": "OK", + "mobilePlayersMatchingSearchTerm": "Hráči s \"{param}\"", + "mobilePrefMagnifyDraggedPiece": "Zväčšiť uchopenú figúrku", + "mobilePuzzleStormConfirmEndRun": "Chcete ukončiť tento pokus?", + "mobilePuzzleStormFilterNothingToShow": "Niet čo zobraziť, prosím, zmeňte filtre", + "mobilePuzzleStormNothingToShow": "Niet čo zobraziť. Zahrajte si niekoľko kôl Puzzle Storm.", + "mobilePuzzleStormSubtitle": "Vyriešte čo najviac úloh za 3 minúty.", + "mobilePuzzleStreakAbortWarning": "Stratíte svoju aktuálnu sériu a vaše skóre sa uloží.", + "mobilePuzzleThemesSubtitle": "Riešte úlohy zo svojich obľúbených otvorení alebo si vyberte tému.", + "mobilePuzzlesTab": "Úlohy", + "mobileRecentSearches": "Posledné vyhľadávania", "mobileSettingsHapticFeedback": "Vibrovanie zariadenia", "mobileSettingsImmersiveMode": "Režim celej obrazovky", "mobileSettingsImmersiveModeSubtitle": "Skrytie používateľského rozhrania systému počas hrania. Túto funkciu použite, ak vám prekážajú navigačné gestá systému na okrajoch obrazovky. Vzťahuje sa na obrazovku počas partie a Puzzle Storm.", - "mobileNotFollowingAnyUser": "Nesledujete žiadneho používateľa.", - "mobileAllGames": "Všetky partie", - "mobileRecentSearches": "Posledné vyhľadávania", - "mobileClearButton": "Odstrániť", - "mobilePlayersMatchingSearchTerm": "Hráči s \"{param}\"", - "mobileNoSearchResults": "Nič sa nenašlo", - "mobileAreYouSure": "Ste si istý?", - "mobilePuzzleStreakAbortWarning": "Stratíte svoju aktuálnu sériu a vaše skóre sa uloží.", - "mobilePuzzleStormNothingToShow": "Niet čo zobraziť. Zahrajte si niekoľko kôl Puzzle Storm.", - "mobileSharePuzzle": "Zdieľať túto úlohu", - "mobileShareGameURL": "Zdieľať URL partie", + "mobileSettingsTab": "Nastavenia", "mobileShareGamePGN": "Zdieľať PGN", + "mobileShareGameURL": "Zdieľať URL partie", "mobileSharePositionAsFEN": "Zdieľať pozíciu vo formáte FEN", - "mobileShowVariations": "Zobraziť varianty", - "mobileHideVariation": "Skryť varianty", + "mobileSharePuzzle": "Zdieľať túto úlohu", "mobileShowComments": "Zobraziť komentáre", - "mobilePuzzleStormConfirmEndRun": "Chcete ukončiť tento pokus?", - "mobilePuzzleStormFilterNothingToShow": "Niet čo zobraziť, prosím, zmeňte filtre", - "mobileCancelTakebackOffer": "Zrušiť žiadosť o vrátenie ťahu", - "mobileWaitingForOpponentToJoin": "Čaká sa na pripojenie súpera...", - "mobileBlindfoldMode": "Naslepo", - "mobileLiveStreamers": "Vysielajúci strímeri", - "mobileCustomGameJoinAGame": "Pripojiť sa k partii", - "mobileCorrespondenceClearSavedMove": "Vymazať uložený ťah", - "mobileSomethingWentWrong": "Došlo k chybe.", "mobileShowResult": "Zobraziť výsledok", - "mobilePuzzleThemesSubtitle": "Riešte úlohy zo svojich obľúbených otvorení alebo si vyberte tému.", - "mobilePuzzleStormSubtitle": "Vyriešte čo najviac úloh za 3 minúty.", - "mobileGreeting": "Ahoj, {param}", - "mobileGreetingWithoutName": "Ahoj", - "mobilePrefMagnifyDraggedPiece": "Zväčšiť uchopenú figúrku", + "mobileShowVariations": "Zobraziť varianty", + "mobileSomethingWentWrong": "Došlo k chybe.", + "mobileSystemColors": "Farby operačného systému", + "mobileTheme": "Vzhľad", + "mobileToolsTab": "Nástroje", + "mobileWaitingForOpponentToJoin": "Čaká sa na pripojenie súpera...", + "mobileWatchTab": "Sledovať", "activityActivity": "Aktivita", "activityHostedALiveStream": "Vysielal naživo", "activityRankedInSwissTournament": "Umiestnený ako #{param1} v {param2}", @@ -117,10 +118,12 @@ "broadcastUploadImage": "Nahrať obrázok pre turnaj", "broadcastNoBoardsYet": "Zatiaľ žiadne šachovnice. Objavia sa po nahratí partií.", "broadcastBoardsCanBeLoaded": "Šachovnice možno načítať pomocou zdroja alebo pomocou {param}", + "broadcastStartsAfter": "Začína po {param}", "broadcastStartVerySoon": "Vysielanie sa začne čoskoro.", "broadcastNotYetStarted": "Vysielanie sa ešte nezačalo.", "broadcastOfficialWebsite": "Oficiálna webstránka", "broadcastStandings": "Poradie", + "broadcastOfficialStandings": "Oficiálne poradie", "broadcastIframeHelp": "Viac možností nájdete na {param}", "broadcastWebmastersPage": "stránke tvorcu", "broadcastPgnSourceHelp": "Verejný zdroj PGN v reálnom čase pre toto kolo. Ponúkame tiež {param} na rýchlejšiu a efektívnejšiu synchronizáciu.", @@ -129,6 +132,15 @@ "broadcastRatingDiff": "Ratingový rozdiel", "broadcastGamesThisTournament": "Partie tohto turnaja", "broadcastScore": "Skóre", + "broadcastAllTeams": "Všetky tímy", + "broadcastTournamentFormat": "Formát turnaja", + "broadcastTournamentLocation": "Miesto konania turnaja", + "broadcastTopPlayers": "Najlepší hráči", + "broadcastTimezone": "Časové pásmo", + "broadcastFideRatingCategory": "Kategória FIDE ratingu", + "broadcastOptionalDetails": "Nepovinné údaje", + "broadcastPastBroadcasts": "Predchádzajúce vysielania", + "broadcastAllBroadcastsByMonth": "Zobraziť všetky vysielania podľa mesiacov", "broadcastNbBroadcasts": "{count, plural, =1{{count} vysielanie} few{{count} vysielania} many{{count} vysielaní} other{{count} vysielaní}}", "challengeChallengesX": "Výzvy: {param1}", "challengeChallengeToPlay": "Vyzvať na partiu", @@ -253,6 +265,7 @@ "preferencesNotifyWeb": "Prehliadač", "preferencesNotifyDevice": "Zariadenie", "preferencesBellNotificationSound": "Zvuk upozornenia", + "preferencesBlindfold": "Naslepo", "puzzlePuzzles": "Šachové úlohy", "puzzlePuzzleThemes": "Kategórie úloh", "puzzleRecommended": "Odporúčané", @@ -570,7 +583,6 @@ "replayMode": "Mód prehrávania", "realtimeReplay": "Ako pri hre", "byCPL": "CHYBY", - "openStudy": "Otvoriť štúdie", "enable": "Povoliť analýzu", "bestMoveArrow": "Šípka pre najlepší ťah", "showVariationArrows": "Zobraziť šípky variantov", @@ -778,7 +790,6 @@ "block": "Blokovať", "blocked": "Blokovaný", "unblock": "Odblokovať", - "followsYou": "Sleduje Vás", "xStartedFollowingY": "{param1} začal sledovať {param2}", "more": "Viac", "memberSince": "Členom od", @@ -1532,6 +1543,7 @@ "studyPlayAgain": "Hrať znova", "studyWhatWouldYouPlay": "Čo by ste hrali v tejto pozícii?", "studyYouCompletedThisLesson": "Gratulujeme! Túto lekciu ste ukončili.", + "studyPerPage": "{param} na stránku", "studyNbChapters": "{count, plural, =1{{count} Kapitola} few{{count} Kapitoly} many{{count} Kapitol} other{{count} Kapitol}}", "studyNbGames": "{count, plural, =1{{count} Partia} few{{count} Partie} many{{count} Partií} other{{count} Partií}}", "studyNbMembers": "{count, plural, =1{{count} Člen} few{{count} Členovia} many{{count} Členov} other{{count} Členov}}", diff --git a/lib/l10n/lila_sl.arb b/lib/l10n/lila_sl.arb index 15ca5cafb3..59190d2975 100644 --- a/lib/l10n/lila_sl.arb +++ b/lib/l10n/lila_sl.arb @@ -1,19 +1,21 @@ { - "mobileHomeTab": "Domov", - "mobilePuzzlesTab": "Problemi", - "mobileToolsTab": "Orodja", - "mobileWatchTab": "Glej", - "mobileSettingsTab": "Nastavitve", - "mobileMustBeLoggedIn": "Predenj lahko dostopaš do te strani, se je potrebno prijaviti.", - "mobileSystemColors": "Barve sistema", + "mobileBlindfoldMode": "Šah z zavezanimi očmi", "mobileFeedbackButton": "Povratne informacije", - "mobileOkButton": "OK", - "mobileShowResult": "Pokaži rezultat", - "mobilePuzzleThemesSubtitle": "Igrajte uganke iz svojih najljubših otvoritev ali izberite temo.", - "mobilePuzzleStormSubtitle": "V 3 minutah rešite čim več ugank.", "mobileGreeting": "Pozdravljeni {param}", "mobileGreetingWithoutName": "Živjo", + "mobileHomeTab": "Domov", + "mobileMustBeLoggedIn": "Predenj lahko dostopaš do te strani, se je potrebno prijaviti.", + "mobileOkButton": "OK", "mobilePrefMagnifyDraggedPiece": "Povečaj vlečeno figuro", + "mobilePuzzleStormSubtitle": "V 3 minutah rešite čim več ugank.", + "mobilePuzzleThemesSubtitle": "Igrajte uganke iz svojih najljubših otvoritev ali izberite temo.", + "mobilePuzzlesTab": "Problemi", + "mobileSettingsTab": "Nastavitve", + "mobileShowResult": "Pokaži rezultat", + "mobileSystemColors": "Barve sistema", + "mobileTheme": "Tema", + "mobileToolsTab": "Orodja", + "mobileWatchTab": "Glej", "activityActivity": "Aktivnost", "activityHostedALiveStream": "Gostil prenos v živo", "activityRankedInSwissTournament": "Uvrščen #{param1} v {param2}", @@ -71,6 +73,16 @@ "broadcastDefinitivelyDeleteTournament": "Dokončno izbrišite celoten turnir, vse njegove kroge in vse njegove igre.", "broadcastShowScores": "Prikaži rezultate igralcev na podlagi rezultatov igre", "broadcastReplacePlayerTags": "Izbirno: zamenjajte imena igralcev, ratinge in nazive", + "broadcastOfficialStandings": "Uradna lestvica", + "broadcastAllTeams": "Vse ekipe", + "broadcastTournamentFormat": "Oblika turnirja", + "broadcastTournamentLocation": "Lokacija turnirja", + "broadcastTopPlayers": "Najboljši igralci", + "broadcastTimezone": "Časovni pas", + "broadcastFideRatingCategory": "FIDE rating kategorija", + "broadcastOptionalDetails": "Neobvezne podrobnosti", + "broadcastPastBroadcasts": "Pretekle oddaje", + "broadcastAllBroadcastsByMonth": "Oglejte si vse oddaje po mesecih", "broadcastNbBroadcasts": "{count, plural, =1{{count} oddaja} =2{{count} oddaji} few{{count} oddaje} other{{count} oddaj}}", "challengeChallengesX": "Izzivi:{param1}", "challengeChallengeToPlay": "Izzovi na partijo", @@ -192,6 +204,7 @@ "preferencesNotifyWeb": "Brskalnik", "preferencesNotifyDevice": "Naprava", "preferencesBellNotificationSound": "Zvok obvestila zvonca", + "preferencesBlindfold": "Šah z zavezanimi očmi", "puzzlePuzzles": "Šahovski problemi", "puzzlePuzzleThemes": "Teme ugank", "puzzleRecommended": "Priporočeno", @@ -509,7 +522,6 @@ "replayMode": "Način predvajanja", "realtimeReplay": "Realnočasovno", "byCPL": "Za stotinko kmeta", - "openStudy": "Odpri študij", "enable": "Omogoči", "bestMoveArrow": "Puščica najboljše poteze", "showVariationArrows": "Prikaži puščice z variacijami", @@ -717,7 +729,6 @@ "block": "Blokiraj", "blocked": "Blokiran", "unblock": "Odblokiraj", - "followsYou": "Sledi vam", "xStartedFollowingY": "{param1} je začel slediti {param2}", "more": "Več", "memberSince": "Član od", @@ -1455,6 +1466,7 @@ "studyPlayAgain": "Igrajte ponovno", "studyWhatWouldYouPlay": "Kaj bi igrali v tem položaju?", "studyYouCompletedThisLesson": "Čestitke! Končali ste to lekcijo.", + "studyPerPage": "{param} na stran", "studyNbChapters": "{count, plural, =1{{count} Poglavje} =2{{count} Poglavji} few{{count} Poglavja} other{{count} poglavij}}", "studyNbGames": "{count, plural, =1{{count} Partija} =2{{count} Partiji} few{{count} Partije} other{{count} Partij}}", "studyNbMembers": "{count, plural, =1{{count} Član} =2{{count} Člana} few{{count} Člani} other{{count} Članov}}", diff --git a/lib/l10n/lila_sq.arb b/lib/l10n/lila_sq.arb index aaa9671822..ce29860b66 100644 --- a/lib/l10n/lila_sq.arb +++ b/lib/l10n/lila_sq.arb @@ -1,44 +1,46 @@ { + "mobileAllGames": "Krejt lojërat", + "mobileAreYouSure": "Jeni i sigurt?", + "mobileBlindfoldMode": "Me sytë lidhur", + "mobileCancelTakebackOffer": "Anulojeni ofertën për prapakthim", + "mobileClearButton": "Spastroje", + "mobileCorrespondenceClearSavedMove": "Spastroje lëvizjen e ruajtur", + "mobileCustomGameJoinAGame": "Merrni pjesë në një lojë", + "mobileFeedbackButton": "Përshtypje", + "mobileGreeting": "Tungjatjeta, {param}", + "mobileGreetingWithoutName": "Tungjatjeta", + "mobileHideVariation": "Fshihe variantin", "mobileHomeTab": "Kreu", - "mobileToolsTab": "Mjete", - "mobileWatchTab": "Shiheni", - "mobileSettingsTab": "Rregullime", + "mobileLiveStreamers": "Transmetues drejtpërsëdrejti", "mobileMustBeLoggedIn": "Që të shihni këtë faqe, duhet të keni bërë hyrjen në llogari.", - "mobileSystemColors": "Ngjyra sistemi", - "mobileFeedbackButton": "Përshtypje", - "mobileOkButton": "OK", - "mobileSettingsHapticFeedback": "Dridhje gjatë lëvizjesh", - "mobileSettingsImmersiveModeSubtitle": "Fshihni ndërfaqen e sistemit teksa luani. Përdoreni këtë nëse ju bezdisin gjeste sistemi për lëvizjet në skaje të ekranit. Ka vend për lojëra dhe skena Puzzle Storm.", + "mobileNoSearchResults": "S’ka përfundime", "mobileNotFollowingAnyUser": "S’ndiqni ndonjë përdorues.", - "mobileAllGames": "Krejt lojërat", - "mobileRecentSearches": "Kërkime së fundi", - "mobileClearButton": "Spastroje", + "mobileOkButton": "OK", "mobilePlayersMatchingSearchTerm": "Lojëtarë me “{param}”", - "mobileNoSearchResults": "S’ka përfundime", - "mobileAreYouSure": "Jeni i sigurt?", + "mobilePrefMagnifyDraggedPiece": "Zmadho gurin e tërhequr", + "mobilePuzzleStormConfirmEndRun": "Doni të përfundohen ku raund?", + "mobilePuzzleStormFilterNothingToShow": "S’ka gjë për t’u shfaqur, ju lutemi, ndryshoni filtrat", "mobilePuzzleStormNothingToShow": "S’ka gjë për shfaqje. Luani ndonjë raund Puzzle Storm.", - "mobileSharePuzzle": "Ndajeni këtë ushtrim me të tjerët", - "mobileShareGameURL": "Ndani URL loje me të tjerë", + "mobilePuzzleStormSubtitle": "Zgjidhni sa më shumë puzzle-e të mundeni brenda 3 minutash.", + "mobilePuzzleThemesSubtitle": "Luani puzzle-e nga hapjet tuaja të parapëlqyera, ose zgjidhni një temë.", + "mobilePuzzlesTab": "Ushtrime", + "mobileRecentSearches": "Kërkime së fundi", + "mobileSettingsHapticFeedback": "Dridhje gjatë lëvizjesh", + "mobileSettingsImmersiveModeSubtitle": "Fshihni ndërfaqen e sistemit teksa luani. Përdoreni këtë nëse ju bezdisin gjeste sistemi për lëvizjet në skaje të ekranit. Ka vend për lojëra dhe skena Puzzle Storm.", + "mobileSettingsTab": "Rregullime", "mobileShareGamePGN": "Ndani PGN me të tjerë", + "mobileShareGameURL": "Ndani URL loje me të tjerë", "mobileSharePositionAsFEN": "Tregojuni të tjerëve pozicionin si FEN", - "mobileShowVariations": "Shfaq variante", - "mobileHideVariation": "Fshihe variantin", + "mobileSharePuzzle": "Ndajeni këtë ushtrim me të tjerët", "mobileShowComments": "Shfaq komente", - "mobilePuzzleStormConfirmEndRun": "Doni të përfundohen ku raund?", - "mobilePuzzleStormFilterNothingToShow": "S’ka gjë për t’u shfaqur, ju lutemi, ndryshoni filtrat", - "mobileCancelTakebackOffer": "Anulojeni ofertën për prapakthim", - "mobileWaitingForOpponentToJoin": "Po pritet që të vijë kundërshtari…", - "mobileBlindfoldMode": "Me sytë lidhur", - "mobileLiveStreamers": "Transmetues drejtpërsëdrejti", - "mobileCustomGameJoinAGame": "Merrni pjesë në një lojë", - "mobileCorrespondenceClearSavedMove": "Spastroje lëvizjen e ruajtur", - "mobileSomethingWentWrong": "Diç shkoi ters.", "mobileShowResult": "Shfaq përfundimin", - "mobilePuzzleThemesSubtitle": "Luani puzzle-e nga hapjet tuaja të parapëlqyera, ose zgjidhni një temë.", - "mobilePuzzleStormSubtitle": "Zgjidhni sa më shumë puzzle-e të mundeni brenda 3 minutash.", - "mobileGreeting": "Tungjatjeta, {param}", - "mobileGreetingWithoutName": "Tungjatjeta", - "mobilePrefMagnifyDraggedPiece": "Zmadho gurin e tërhequr", + "mobileShowVariations": "Shfaq variante", + "mobileSomethingWentWrong": "Diç shkoi ters.", + "mobileSystemColors": "Ngjyra sistemi", + "mobileTheme": "Temë", + "mobileToolsTab": "Mjete", + "mobileWaitingForOpponentToJoin": "Po pritet që të vijë kundërshtari…", + "mobileWatchTab": "Shiheni", "activityActivity": "Aktiviteti", "activityHostedALiveStream": "Priti një transmetim të drejtpërdrejtë", "activityRankedInSwissTournament": "Renditur #{param1} në {param2}", @@ -123,8 +125,18 @@ "broadcastEmbedThisRound": "Trupëzojeni {param} në sajtin tuaj", "broadcastGamesThisTournament": "Lojëra në këtë turne", "broadcastScore": "Përfundim", + "broadcastAllTeams": "Krejt ekipet", + "broadcastTournamentFormat": "Format turneu", + "broadcastTournamentLocation": "Vendndodhje Turney", + "broadcastTopPlayers": "Lojtarët kryesues", + "broadcastTimezone": "Zonë kohore", + "broadcastFideRatingCategory": "Kategori vlerësimi FIDE", + "broadcastOptionalDetails": "Hollësi opsionale", + "broadcastPastBroadcasts": "Transmetime të kaluara", + "broadcastAllBroadcastsByMonth": "Shihni krejt transmetimet sipas muajsh", "broadcastNbBroadcasts": "{count, plural, =1{{count} transmetim} other{{count} transmetime}}", - "challengeChallengeToPlay": "Sfidoni në një lojë", + "challengeChallengesX": "Sfida: {param1}", + "challengeChallengeToPlay": "Sfidoni me një lojë", "challengeChallengeDeclined": "Sfida u refuzua", "challengeChallengeAccepted": "Sfida u pranua!", "challengeChallengeCanceled": "Sfida u anulua.", @@ -241,6 +253,7 @@ "preferencesNotifyWeb": "Shfletues", "preferencesNotifyDevice": "Pajisje", "preferencesBellNotificationSound": "Tingull zileje njoftimesh", + "preferencesBlindfold": "Me sytë lidhur", "puzzlePuzzles": "Ushtrime", "puzzlePuzzleThemes": "Tema ushtrimesh", "puzzleRecommended": "Të rekomanduara", @@ -555,7 +568,6 @@ "replayMode": "Mënyra përsëritje", "realtimeReplay": "Aty për aty", "byCPL": "nga CPL", - "openStudy": "Studim i hapur", "enable": "Aktivizoje", "bestMoveArrow": "Shigjetë e lëvizjes më të mirë", "showVariationArrows": "Shfaq shigjeta variacionesh", @@ -763,7 +775,6 @@ "block": "Bllokoje", "blocked": "I bllokuar", "unblock": "Zhbllokoje", - "followsYou": "Ju ndjek juve", "xStartedFollowingY": "{param1} nisi të ndjekë {param2}", "more": "Më shumë", "memberSince": "Anëtar që prej", @@ -1504,6 +1515,7 @@ "studyPlayAgain": "Riluaje", "studyWhatWouldYouPlay": "Ç’lëvizje do të bënit në këtë pozicion?", "studyYouCompletedThisLesson": "Përgëzime! E mbaruat këtë mësim.", + "studyPerPage": "{param} për faqe", "studyNbChapters": "{count, plural, =1{{count} Kapitull} other{{count} Kapituj}}", "studyNbGames": "{count, plural, =1{{count} Lojë} other{{count} Lojëra}}", "studyNbMembers": "{count, plural, =1{{count} Anëtar} other{{count} Anëtarë}}", diff --git a/lib/l10n/lila_sr.arb b/lib/l10n/lila_sr.arb index cf5b6788f4..07bee68b5c 100644 --- a/lib/l10n/lila_sr.arb +++ b/lib/l10n/lila_sr.arb @@ -402,7 +402,6 @@ "replayMode": "Понављање партије", "realtimeReplay": "Као уживо", "byCPL": "По рачунару", - "openStudy": "Отвори проуку", "enable": "Укључи", "bestMoveArrow": "Стрелица за најбољи потез", "showVariationArrows": "Прикажи стрелице за варијацију", @@ -608,7 +607,6 @@ "block": "Блокирај", "blocked": "Блокиран", "unblock": "Одблокирај", - "followsYou": "Прате тебе", "xStartedFollowingY": "{param1} је почео/ла пратити {param2}", "more": "Више", "memberSince": "Члан од", @@ -1237,6 +1235,9 @@ "studyDeleteTheStudyChatHistory": "Избриши историју ћаскања студије? Нема повратка назад!", "studyDeleteStudy": "Избриши студију", "studyWhereDoYouWantToStudyThat": "Где желите то проучити?", + "studyGoodMove": "Добар потез", + "studyMistake": "Грешка", + "studyBlunder": "Груба грешка", "studyNbChapters": "{count, plural, =1{{count} Поглавље} few{{count} Поглављa} other{{count} Поглављa}}", "studyNbGames": "{count, plural, =1{{count} Партија} few{{count} Партијe} other{{count} Партија}}", "studyNbMembers": "{count, plural, =1{{count} Члан} few{{count} Чланa} other{{count} Чланова}}", diff --git a/lib/l10n/lila_sv.arb b/lib/l10n/lila_sv.arb index de574ce557..b438affd01 100644 --- a/lib/l10n/lila_sv.arb +++ b/lib/l10n/lila_sv.arb @@ -1,28 +1,30 @@ { - "mobileHomeTab": "Hem", - "mobilePuzzlesTab": "Problem", - "mobileToolsTab": "Verktyg", - "mobileWatchTab": "Titta", - "mobileSystemColors": "Systemets färger", - "mobileOkButton": "OK", "mobileAllGames": "Alla spel", - "mobileRecentSearches": "Senaste sökningar", - "mobileClearButton": "Rensa", - "mobilePlayersMatchingSearchTerm": "Spelare med \"{param}\"", - "mobileNoSearchResults": "Inga resultat", "mobileAreYouSure": "Är du säker?", - "mobileSharePuzzle": "Dela detta schackproblem", - "mobileShareGameURL": "Dela parti-URL", - "mobileShareGamePGN": "Dela PGN", - "mobileShowVariations": "Visa variationer", - "mobileHideVariation": "Dölj variationer", - "mobileShowComments": "Visa kommentarer", "mobileBlindfoldMode": "I blindo", + "mobileClearButton": "Rensa", "mobileCustomGameJoinAGame": "Gå med i spel", - "mobileSomethingWentWrong": "Något gick fel.", - "mobileShowResult": "Visa resultat", "mobileGreeting": "Hej {param}", "mobileGreetingWithoutName": "Hej", + "mobileHideVariation": "Dölj variationer", + "mobileHomeTab": "Hem", + "mobileNoSearchResults": "Inga resultat", + "mobileNotFollowingAnyUser": "Du följer inte någon användare.", + "mobileOkButton": "OK", + "mobilePlayersMatchingSearchTerm": "Spelare med \"{param}\"", + "mobilePuzzlesTab": "Problem", + "mobileRecentSearches": "Senaste sökningar", + "mobileShareGamePGN": "Dela PGN", + "mobileShareGameURL": "Dela parti-URL", + "mobileSharePositionAsFEN": "Dela position som FEN", + "mobileSharePuzzle": "Dela detta schackproblem", + "mobileShowComments": "Visa kommentarer", + "mobileShowResult": "Visa resultat", + "mobileShowVariations": "Visa variationer", + "mobileSomethingWentWrong": "Något gick fel.", + "mobileSystemColors": "Systemets färger", + "mobileToolsTab": "Verktyg", + "mobileWatchTab": "Titta", "activityActivity": "Aktivitet", "activityHostedALiveStream": "Var värd för en direktsänd videosändning", "activityRankedInSwissTournament": "Rankad #{param1} i {param2}", @@ -197,6 +199,7 @@ "preferencesNotifyWeb": "Webbläsare", "preferencesNotifyDevice": "Enhet", "preferencesBellNotificationSound": "Klock-notisljud", + "preferencesBlindfold": "I blindo", "puzzlePuzzles": "Problem", "puzzlePuzzleThemes": "Teman för schackproblem", "puzzleRecommended": "Rekommenderad", @@ -512,7 +515,6 @@ "replayMode": "Uppspelningsläge", "realtimeReplay": "Realtid", "byCPL": "CPL", - "openStudy": "Öppna studie", "enable": "Aktivera", "bestMoveArrow": "Pil som anger bästa drag", "showVariationArrows": "Visa variationspilar", @@ -717,7 +719,6 @@ "block": "Blockera", "blocked": "Blockerad", "unblock": "Avblockera", - "followsYou": "Följer dig", "xStartedFollowingY": "{param1} började följa {param2}", "more": "Visa mer", "memberSince": "Medlem sedan", diff --git a/lib/l10n/lila_tr.arb b/lib/l10n/lila_tr.arb index 64d6191c48..0e3a512927 100644 --- a/lib/l10n/lila_tr.arb +++ b/lib/l10n/lila_tr.arb @@ -1,47 +1,47 @@ { + "mobileAllGames": "Tüm oyunlar", + "mobileAreYouSure": "Emin misiniz?", + "mobileBlindfoldMode": "Körleme modu", + "mobileCancelTakebackOffer": "Geri alma teklifini iptal et", + "mobileClearButton": "Temizle", + "mobileCorrespondenceClearSavedMove": "Kayıtlı hamleyi sil", + "mobileCustomGameJoinAGame": "Bir oyuna katıl", + "mobileFeedbackButton": "Geri bildirimde bulun", + "mobileGreeting": "Merhaba, {param}", + "mobileGreetingWithoutName": "Merhaba", + "mobileHideVariation": "Varyasyonu gizle", "mobileHomeTab": "Ana sayfa", - "mobilePuzzlesTab": "Bulmacalar", - "mobileToolsTab": "Araçlar", - "mobileWatchTab": "İzle", - "mobileSettingsTab": "Ayarlar", + "mobileLiveStreamers": "Canlı yayıncılar", "mobileMustBeLoggedIn": "Bu sayfayı görüntülemek için giriş yapmalısınız.", - "mobileSystemColors": "Sistem renkleri", - "mobileFeedbackButton": "Geri bildirimde bulun", + "mobileNoSearchResults": "Sonuç bulunamadı", + "mobileNotFollowingAnyUser": "Hiçbir kullanıcıyı takip etmiyorsunuz.", "mobileOkButton": "Tamam", + "mobilePlayersMatchingSearchTerm": "\"{param}\" ile başlayan oyuncularla", + "mobilePrefMagnifyDraggedPiece": "Sürüklenen parçayı büyüt", + "mobilePuzzleStormConfirmEndRun": "Bu oyunu bitirmek istiyor musun?", + "mobilePuzzleStormFilterNothingToShow": "Gösterilecek bir şey yok, lütfen filtreleri değiştirin", + "mobilePuzzleStormNothingToShow": "Gösterilcek bir şey yok. Birkaç kez Bulmaca Fırtınası oyunu oynayın.", + "mobilePuzzleStormSubtitle": "3 dakika içerisinde mümkün olduğunca çok bulmaca çözün.", + "mobilePuzzleStreakAbortWarning": "Mevcut serinizi kaybedeceksiniz ve puanınız kaydedilecektir.", + "mobilePuzzleThemesSubtitle": "En sevdiğiniz açılışlardan bulmacalar oynayın veya bir tema seçin.", + "mobilePuzzlesTab": "Bulmacalar", + "mobileRecentSearches": "Son aramalar", "mobileSettingsHapticFeedback": "Titreşimli geri bildirim", "mobileSettingsImmersiveMode": "Sürükleyici mod", "mobileSettingsImmersiveModeSubtitle": "Oynarken sistem arayüzünü gizle. Ekranın kenarlarındaki sistemin gezinme hareketlerinden rahatsızsan bunu kullan. Bu ayar, oyun ve Bulmaca Fırtınası ekranlarına uygulanır.", - "mobileNotFollowingAnyUser": "Hiçbir kullanıcıyı takip etmiyorsunuz.", - "mobileAllGames": "Tüm oyunlar", - "mobileRecentSearches": "Son aramalar", - "mobileClearButton": "Temizle", - "mobilePlayersMatchingSearchTerm": "\"{param}\" ile başlayan oyuncularla", - "mobileNoSearchResults": "Sonuç bulunamadı", - "mobileAreYouSure": "Emin misiniz?", - "mobilePuzzleStreakAbortWarning": "Mevcut serinizi kaybedeceksiniz ve puanınız kaydedilecektir.", - "mobilePuzzleStormNothingToShow": "Gösterilcek bir şey yok. Birkaç kez Bulmaca Fırtınası oyunu oynayın.", - "mobileSharePuzzle": "Bulmacayı paylaş", - "mobileShareGameURL": "Oyun linkini paylaş", + "mobileSettingsTab": "Ayarlar", "mobileShareGamePGN": "PGN'yi paylaş", + "mobileShareGameURL": "Oyun linkini paylaş", "mobileSharePositionAsFEN": "Konumu FEN olarak paylaş", - "mobileShowVariations": "Varyasyonları göster", - "mobileHideVariation": "Varyasyonu gizle", + "mobileSharePuzzle": "Bulmacayı paylaş", "mobileShowComments": "Yorumları göster", - "mobilePuzzleStormConfirmEndRun": "Bu oyunu bitirmek istiyor musun?", - "mobilePuzzleStormFilterNothingToShow": "Gösterilecek bir şey yok, lütfen filtreleri değiştirin", - "mobileCancelTakebackOffer": "Geri alma teklifini iptal et", - "mobileWaitingForOpponentToJoin": "Rakip bekleniyor...", - "mobileBlindfoldMode": "Körleme modu", - "mobileLiveStreamers": "Canlı yayıncılar", - "mobileCustomGameJoinAGame": "Bir oyuna katıl", - "mobileCorrespondenceClearSavedMove": "Kayıtlı hamleyi sil", - "mobileSomethingWentWrong": "Birşeyler ters gitti.", "mobileShowResult": "Sonucu göster", - "mobilePuzzleThemesSubtitle": "En sevdiğiniz açılışlardan bulmacalar oynayın veya bir tema seçin.", - "mobilePuzzleStormSubtitle": "3 dakika içerisinde mümkün olduğunca çok bulmaca çözün.", - "mobileGreeting": "Merhaba, {param}", - "mobileGreetingWithoutName": "Merhaba", - "mobilePrefMagnifyDraggedPiece": "Sürüklenen parçayı büyüt", + "mobileShowVariations": "Varyasyonları göster", + "mobileSomethingWentWrong": "Birşeyler ters gitti.", + "mobileSystemColors": "Sistem renkleri", + "mobileToolsTab": "Araçlar", + "mobileWaitingForOpponentToJoin": "Rakip bekleniyor...", + "mobileWatchTab": "İzle", "activityActivity": "Son Etkinlikler", "activityHostedALiveStream": "Canlı yayın yaptı", "activityRankedInSwissTournament": "{param2} katılımcıları arasında #{param1}. oldu", @@ -122,13 +122,24 @@ "broadcastNotYetStarted": "Yayın henüz başlamadı.", "broadcastOfficialWebsite": "Resmî site", "broadcastStandings": "Sıralamalar", + "broadcastOfficialStandings": "Resmi Sıralamalar", "broadcastIframeHelp": "{param}nda daha fazla seçenek", + "broadcastWebmastersPage": "ağ yöneticileri sayfası", "broadcastPgnSourceHelp": "Bu turun açık, gerçek zamanlı PGN kaynağı. Daha hızlı ve verimli senkronizasyon için {param}'ımız da bulunmaktadır.", "broadcastEmbedThisBroadcast": "İnternet sitenizde bu yayını gömülü paylaşın", "broadcastEmbedThisRound": "{param}u İnternet sitenizde gömülü paylaşın", "broadcastRatingDiff": "Puan farkı", "broadcastGamesThisTournament": "Bu turnuvadaki maçlar", "broadcastScore": "Skor", + "broadcastAllTeams": "Tüm takımlar", + "broadcastTournamentFormat": "Turnuva biçimi", + "broadcastTournamentLocation": "Turnuva Konumu", + "broadcastTopPlayers": "En iyi oyuncular", + "broadcastTimezone": "Zaman dilimi", + "broadcastFideRatingCategory": "FIDE derecelendirme kategorisi", + "broadcastOptionalDetails": "İsteğe bağlı ayrıntılar", + "broadcastPastBroadcasts": "Geçmiş yayınlar", + "broadcastAllBroadcastsByMonth": "Tüm yayınları aylara göre görüntüleyin", "broadcastNbBroadcasts": "{count, plural, =1{{count} canlı turnuva} other{{count} canlı turnuva}}", "challengeChallengesX": "{param1} karşılaşmaları", "challengeChallengeToPlay": "Oyun teklif et", @@ -253,6 +264,7 @@ "preferencesNotifyWeb": "Tarayıcı", "preferencesNotifyDevice": "Cihaz", "preferencesBellNotificationSound": "Çan bildirimi sesi", + "preferencesBlindfold": "Körleme modu", "puzzlePuzzles": "Bulmacalar", "puzzlePuzzleThemes": "Bulmaca temaları", "puzzleRecommended": "Önerilen", @@ -570,7 +582,6 @@ "replayMode": "Tekrar modu", "realtimeReplay": "Gerçek Zamanlı", "byCPL": "CPL ile", - "openStudy": "Çalışma oluştur", "enable": "Etkinleştir", "bestMoveArrow": "En iyi hamle imleci", "showVariationArrows": "Varyasyon oklarını göster", @@ -661,6 +672,7 @@ "rank": "Sıralama", "rankX": "Sıralama: {param}", "gamesPlayed": "Oynanmış oyunlar", + "ok": "Tamam", "cancel": "İptal et", "whiteTimeOut": "Beyazın zamanı tükendi", "blackTimeOut": "Siyahın zamanı tükendi", @@ -777,7 +789,6 @@ "block": "Engelle", "blocked": "Engellendi", "unblock": "Engeli kaldır", - "followsYou": "Sizi takip ediyor", "xStartedFollowingY": "{param1}, {param2} isimli oyuncuyu takip etmeye başladı", "more": "Daha fazla", "memberSince": "Üyelik tarihi", @@ -1531,6 +1542,7 @@ "studyPlayAgain": "Tekrar oyna", "studyWhatWouldYouPlay": "Burada hangi hamleyi yapardınız?", "studyYouCompletedThisLesson": "Tebrikler! Bu dersi tamamlandınız.", + "studyPerPage": "Sayfa başına {param}", "studyNbChapters": "{count, plural, =1{{count} Bölüm} other{{count} Bölüm}}", "studyNbGames": "{count, plural, =1{{count} oyun} other{{count} Oyun}}", "studyNbMembers": "{count, plural, =1{{count} Üye} other{{count} Üye}}", diff --git a/lib/l10n/lila_uk.arb b/lib/l10n/lila_uk.arb index 122ff1be47..2fa93fc4d3 100644 --- a/lib/l10n/lila_uk.arb +++ b/lib/l10n/lila_uk.arb @@ -1,47 +1,48 @@ { + "mobileAllGames": "Усі ігри", + "mobileAreYouSure": "Ви впевнені?", + "mobileBlindfoldMode": "Наосліп", + "mobileCancelTakebackOffer": "Скасувати пропозицію повернення ходу", + "mobileClearButton": "Очистити", + "mobileCorrespondenceClearSavedMove": "Очистити збережений хід", + "mobileCustomGameJoinAGame": "Приєднатися до гри", + "mobileFeedbackButton": "Відгук", + "mobileGreeting": "Привіт, {param}", + "mobileGreetingWithoutName": "Привіт", + "mobileHideVariation": "Сховати варіанти", "mobileHomeTab": "Головна", - "mobilePuzzlesTab": "Задачі", - "mobileToolsTab": "Інструм.", - "mobileWatchTab": "Дивитися", - "mobileSettingsTab": "Налашт.", + "mobileLiveStreamers": "Стримери в прямому етері", "mobileMustBeLoggedIn": "Ви повинні ввійти, аби переглянути цю сторінку.", - "mobileSystemColors": "Системні кольори", - "mobileFeedbackButton": "Відгук", + "mobileNoSearchResults": "Немає результатів ", + "mobileNotFollowingAnyUser": "Ви ні на кого не підписані.", "mobileOkButton": "Гаразд", + "mobilePlayersMatchingSearchTerm": "Гравці з «{param}»", + "mobilePrefMagnifyDraggedPiece": "Збільшувати розмір фігури при перетягуванні", + "mobilePuzzleStormConfirmEndRun": "Ви хочете закінчити цю серію?", + "mobilePuzzleStormFilterNothingToShow": "Нічого не знайдено, будь ласка, змініть фільтри", + "mobilePuzzleStormNothingToShow": "Нічого показати. Зіграйте в гру Puzzle Storm.", + "mobilePuzzleStormSubtitle": "Розв'яжіть якомога більше задач за 3 хвилини.", + "mobilePuzzleStreakAbortWarning": "Ви втратите поточну серію, і ваш рахунок буде збережено.", + "mobilePuzzleThemesSubtitle": "Розв'язуйте задачі з улюбленими дебютами або обирайте тему.", + "mobilePuzzlesTab": "Задачі", + "mobileRecentSearches": "Недавні пошуки", "mobileSettingsHapticFeedback": "Вібрація при ході", "mobileSettingsImmersiveMode": "Повноекранний режим", "mobileSettingsImmersiveModeSubtitle": "Приховати інтерфейс системи під час гри. Використовуйте, якщо вас турбують навігаційні жести системи по краях екрану. Застосовується до екранів гри та задач.", - "mobileNotFollowingAnyUser": "Ви ні на кого не підписані.", - "mobileAllGames": "Усі ігри", - "mobileRecentSearches": "Недавні пошуки", - "mobileClearButton": "Очистити", - "mobilePlayersMatchingSearchTerm": "Гравці з «{param}»", - "mobileNoSearchResults": "Немає результатів ", - "mobileAreYouSure": "Ви впевнені?", - "mobilePuzzleStreakAbortWarning": "Ви втратите поточну серію, і ваш рахунок буде збережено.", - "mobilePuzzleStormNothingToShow": "Нічого показати. Зіграйте в гру Puzzle Storm.", - "mobileSharePuzzle": "Поділитися задачею", - "mobileShareGameURL": "Поділитися посиланням на гру", + "mobileSettingsTab": "Налашт.", "mobileShareGamePGN": "Поділитися PGN", + "mobileShareGameURL": "Поділитися посиланням на гру", "mobileSharePositionAsFEN": "Поділитися FEN", - "mobileShowVariations": "Показати варіанти", - "mobileHideVariation": "Сховати варіанти", + "mobileSharePuzzle": "Поділитися задачею", "mobileShowComments": "Показати коментарі", - "mobilePuzzleStormConfirmEndRun": "Ви хочете закінчити цю серію?", - "mobilePuzzleStormFilterNothingToShow": "Нічого не знайдено, будь ласка, змініть фільтри", - "mobileCancelTakebackOffer": "Скасувати пропозицію повернення ходу", - "mobileWaitingForOpponentToJoin": "Очікування на суперника...", - "mobileBlindfoldMode": "Наосліп", - "mobileLiveStreamers": "Стримери в прямому етері", - "mobileCustomGameJoinAGame": "Приєднатися до гри", - "mobileCorrespondenceClearSavedMove": "Очистити збережений хід", - "mobileSomethingWentWrong": "Щось пішло не так.", "mobileShowResult": "Показати результат", - "mobilePuzzleThemesSubtitle": "Розв'язуйте задачі з улюбленими дебютами або обирайте тему.", - "mobilePuzzleStormSubtitle": "Розв'яжіть якомога більше задач за 3 хвилини.", - "mobileGreeting": "Привіт, {param}", - "mobileGreetingWithoutName": "Привіт", - "mobilePrefMagnifyDraggedPiece": "Збільшувати розмір фігури при перетягуванні", + "mobileShowVariations": "Показати варіанти", + "mobileSomethingWentWrong": "Щось пішло не так.", + "mobileSystemColors": "Системні кольори", + "mobileTheme": "Тема", + "mobileToolsTab": "Інструм.", + "mobileWaitingForOpponentToJoin": "Очікування на суперника...", + "mobileWatchTab": "Дивитися", "activityActivity": "Активність", "activityHostedALiveStream": "Проведено пряму трансляцію", "activityRankedInSwissTournament": "Зайняв #{param1} місце в {param2}", @@ -108,7 +109,31 @@ "broadcastAgeThisYear": "Вік цього року", "broadcastUnrated": "Без рейтингу", "broadcastRecentTournaments": "Нещодавні турніри", + "broadcastOpenLichess": "Відкрити в Lichess", + "broadcastTeams": "Команди", + "broadcastBoards": "Дошки", + "broadcastOverview": "Огляд", + "broadcastUploadImage": "Завантажити зображення турніру", + "broadcastNoBoardsYet": "Ще немає дощок. Вони з'являться, коли ігри будуть завантажені.", + "broadcastStartVerySoon": "Трансляція розпочнеться дуже скоро.", + "broadcastNotYetStarted": "Трансляція ще не розпочалася.", "broadcastOfficialWebsite": "Офіційний вебсайт", + "broadcastStandings": "Турнірна таблиця", + "broadcastOfficialStandings": "Офіційна турнірна таблиця", + "broadcastIframeHelp": "Більше опцій на {param}", + "broadcastEmbedThisBroadcast": "Вбудувати цю трансляцію на своєму сайті", + "broadcastEmbedThisRound": "Вбудувати {param} на своєму сайті", + "broadcastRatingDiff": "Різниця у рейтингу", + "broadcastGamesThisTournament": "Ігри в цьому турнірі", + "broadcastScore": "Очки", + "broadcastAllTeams": "Усі команди", + "broadcastTournamentFormat": "Формат турніру", + "broadcastTournamentLocation": "Місце турніру", + "broadcastTopPlayers": "Найкращі гравці", + "broadcastTimezone": "Часовий пояс", + "broadcastFideRatingCategory": "Категорія рейтингу FIDE", + "broadcastOptionalDetails": "Додаткові деталі", + "broadcastPastBroadcasts": "Минулі трансляції", "broadcastNbBroadcasts": "{count, plural, =1{{count} трансляція} few{{count} трансляції} many{{count} трансляцій} other{{count} трансляцій}}", "challengeChallengesX": "Виклики: {param1}", "challengeChallengeToPlay": "Виклик на гру", @@ -208,7 +233,7 @@ "preferencesMoveConfirmation": "Підтвердження ходу", "preferencesExplainCanThenBeTemporarilyDisabled": "Можна вимкнути під час гри в меню дошки", "preferencesInCorrespondenceGames": "У заочних партіях", - "preferencesCorrespondenceAndUnlimited": "За листуванням та необмежені", + "preferencesCorrespondenceAndUnlimited": "Заочні та необмежені", "preferencesConfirmResignationAndDrawOffers": "Підтверджувати повернення ходу та пропозиції нічий", "preferencesCastleByMovingTheKingTwoSquaresOrOntoTheRook": "Спосіб рокіровки", "preferencesCastleByMovingTwoSquares": "Перемістити короля на два поля", @@ -233,6 +258,7 @@ "preferencesNotifyWeb": "Браузер", "preferencesNotifyDevice": "Пристрій", "preferencesBellNotificationSound": "Звук сповіщення", + "preferencesBlindfold": "Наосліп", "puzzlePuzzles": "Задачі", "puzzlePuzzleThemes": "Теми задач", "puzzleRecommended": "Рекомендовані", @@ -550,7 +576,6 @@ "replayMode": "Режим повтору", "realtimeReplay": "У реальному часі", "byCPL": "Цікаве", - "openStudy": "Почати дослідження", "enable": "Увімкнути", "bestMoveArrow": "Стрілка «Найкращий хід»", "showVariationArrows": "Показати стрілки для варіантів", @@ -758,7 +783,6 @@ "block": "Заблокувати", "blocked": "Заблоковано", "unblock": "Розблокувати", - "followsYou": "Спостерігає за вами", "xStartedFollowingY": "{param1} починає спостерігати за {param2}", "more": "Більше", "memberSince": "Зареєстрований з", @@ -1512,6 +1536,7 @@ "studyPlayAgain": "Грати знову", "studyWhatWouldYouPlay": "Що б ви грали в цій позиції?", "studyYouCompletedThisLesson": "Вітаємо! Ви завершили цей урок.", + "studyPerPage": "{param} на сторінку", "studyNbChapters": "{count, plural, =1{{count} розділ} few{{count} розділи} many{{count} розділів} other{{count} розділи}}", "studyNbGames": "{count, plural, =1{{count} Партія} few{{count} Партії} many{{count} Партій} other{{count} Партій}}", "studyNbMembers": "{count, plural, =1{{count} учасник} few{{count} учасники} many{{count} учасників} other{{count} учасників}}", diff --git a/lib/l10n/lila_vi.arb b/lib/l10n/lila_vi.arb index e164ed8b4d..475b276a39 100644 --- a/lib/l10n/lila_vi.arb +++ b/lib/l10n/lila_vi.arb @@ -1,47 +1,48 @@ { + "mobileAllGames": "Tất cả ván đấu", + "mobileAreYouSure": "Bạn chắc chứ?", + "mobileBlindfoldMode": "Bịt mắt", + "mobileCancelTakebackOffer": "Hủy đề nghị đi lại", + "mobileClearButton": "Xóa", + "mobileCorrespondenceClearSavedMove": "Xóa nước cờ đã lưu", + "mobileCustomGameJoinAGame": "Tham gia một ván cờ", + "mobileFeedbackButton": "Phản hồi", + "mobileGreeting": "Xin chào, {param}", + "mobileGreetingWithoutName": "Xin chào", + "mobileHideVariation": "Ẩn các biến", "mobileHomeTab": "Trang chủ", - "mobilePuzzlesTab": "Câu đố", - "mobileToolsTab": "Công cụ", - "mobileWatchTab": "Xem", - "mobileSettingsTab": "Cài đặt", + "mobileLiveStreamers": "Các Streamer phát trực tiếp", "mobileMustBeLoggedIn": "Bạn phải đăng nhập để xem trang này.", - "mobileSystemColors": "Màu hệ thống", - "mobileFeedbackButton": "Phản hồi", + "mobileNoSearchResults": "Không có kết quả", + "mobileNotFollowingAnyUser": "Bạn chưa theo dõi người dùng nào.", "mobileOkButton": "OK", + "mobilePlayersMatchingSearchTerm": "chơi với \"{param}\"", + "mobilePrefMagnifyDraggedPiece": "Phóng to quân cờ được kéo", + "mobilePuzzleStormConfirmEndRun": "Bạn có muốn kết thúc lượt chạy này không?", + "mobilePuzzleStormFilterNothingToShow": "Không có gì để hiển thị, vui lòng thay đổi bộ lọc", + "mobilePuzzleStormNothingToShow": "Không có gì để xem. Chơi một vài ván Puzzle Storm.", + "mobilePuzzleStormSubtitle": "Giải càng nhiều câu đố càng tốt trong 3 phút.", + "mobilePuzzleStreakAbortWarning": "Bạn sẽ mất chuỗi hiện tại và điểm của bạn sẽ được lưu.", + "mobilePuzzleThemesSubtitle": "Giải câu đố từ những khai cuộc yêu thích của bạn hoặc chọn một chủ đề.", + "mobilePuzzlesTab": "Câu đố", + "mobileRecentSearches": "Tìm kiếm gần đây", "mobileSettingsHapticFeedback": "Rung phản hồi", "mobileSettingsImmersiveMode": "Chế độ toàn màn hình", "mobileSettingsImmersiveModeSubtitle": "Ẩn UI hệ thống trong khi chơi. Sử dụng điều này nếu bạn bị làm phiền bởi các cử chỉ điều hướng của hệ thống ở các cạnh của màn hình. Áp dụng cho màn hình ván đấu và Puzzle Strom.", - "mobileNotFollowingAnyUser": "Bạn chưa theo dõi người dùng nào.", - "mobileAllGames": "Tất cả ván đấu", - "mobileRecentSearches": "Tìm kiếm gần đây", - "mobileClearButton": "Xóa", - "mobilePlayersMatchingSearchTerm": "chơi với \"{param}\"", - "mobileNoSearchResults": "Không có kết quả", - "mobileAreYouSure": "Bạn chắc chứ?", - "mobilePuzzleStreakAbortWarning": "Bạn sẽ mất chuỗi hiện tại và điểm của bạn sẽ được lưu.", - "mobilePuzzleStormNothingToShow": "Không có gì để xem. Chơi một vài ván Puzzle Storm.", - "mobileSharePuzzle": "Chia sẻ câu đố này", - "mobileShareGameURL": "Chia sẻ URL ván cờ", + "mobileSettingsTab": "Cài đặt", "mobileShareGamePGN": "Chia sẻ tập tin PGN", + "mobileShareGameURL": "Chia sẻ URL ván cờ", "mobileSharePositionAsFEN": "Chia sẻ thế cờ dạng FEN", - "mobileShowVariations": "Hiện các biến", - "mobileHideVariation": "Ẩn các biến", + "mobileSharePuzzle": "Chia sẻ câu đố này", "mobileShowComments": "Hiển thị bình luận", - "mobilePuzzleStormConfirmEndRun": "Bạn có muốn kết thúc lượt chạy này không?", - "mobilePuzzleStormFilterNothingToShow": "Không có gì để hiển thị, vui lòng thay đổi bộ lọc", - "mobileCancelTakebackOffer": "Hủy đề nghị đi lại", - "mobileWaitingForOpponentToJoin": "Đang chờ đối thủ tham gia...", - "mobileBlindfoldMode": "Bịt mắt", - "mobileLiveStreamers": "Các Streamer phát trực tiếp", - "mobileCustomGameJoinAGame": "Tham gia một ván cờ", - "mobileCorrespondenceClearSavedMove": "Xóa nước cờ đã lưu", - "mobileSomethingWentWrong": "Đã xảy ra lỗi.", "mobileShowResult": "Xem kết quả", - "mobilePuzzleThemesSubtitle": "Giải câu đố từ những khai cuộc yêu thích của bạn hoặc chọn một chủ đề.", - "mobilePuzzleStormSubtitle": "Giải càng nhiều câu đố càng tốt trong 3 phút.", - "mobileGreeting": "Xin chào, {param}", - "mobileGreetingWithoutName": "Xin chào", - "mobilePrefMagnifyDraggedPiece": "Phóng to quân cờ được kéo", + "mobileShowVariations": "Hiện các biến", + "mobileSomethingWentWrong": "Đã xảy ra lỗi.", + "mobileSystemColors": "Màu hệ thống", + "mobileTheme": "Giao diện", + "mobileToolsTab": "Công cụ", + "mobileWaitingForOpponentToJoin": "Đang chờ đối thủ tham gia...", + "mobileWatchTab": "Xem", "activityActivity": "Hoạt động", "activityHostedALiveStream": "Đã phát trực tiếp", "activityRankedInSwissTournament": "Đứng hạng {param1} trong giải {param2}", @@ -122,6 +123,7 @@ "broadcastNotYetStarted": "Chương trình phát sóng vẫn chưa bắt đầu.", "broadcastOfficialWebsite": "Website chính thức", "broadcastStandings": "Bảng xếp hạng", + "broadcastOfficialStandings": "Bảng xếp hạng Chính thức", "broadcastIframeHelp": "Thêm tùy chọn trên {param}", "broadcastWebmastersPage": "trang nhà phát triển web", "broadcastPgnSourceHelp": "Nguồn PGN công khai, thời gian thực cho vòng này. Chúng tôi cũng cung cấp {param} để đồng bộ hóa nhanh hơn và hiệu quả hơn.", @@ -130,6 +132,15 @@ "broadcastRatingDiff": "Độ thay đổi hệ số", "broadcastGamesThisTournament": "Các ván đấu trong giải này", "broadcastScore": "Điểm số", + "broadcastAllTeams": "Tất cả đội", + "broadcastTournamentFormat": "Điều lệ giải đấu", + "broadcastTournamentLocation": "Địa điểm tổ chức giải đấu", + "broadcastTopPlayers": "Những kỳ thủ hàng đầu", + "broadcastTimezone": "Múi giờ", + "broadcastFideRatingCategory": "Thể loại xếp hạng FIDE", + "broadcastOptionalDetails": "Tùy chọn chi tiết", + "broadcastPastBroadcasts": "Các phát sóng đã qua", + "broadcastAllBroadcastsByMonth": "Xem tất cả phát sóng theo tháng", "broadcastNbBroadcasts": "{count, plural, other{{count} phát sóng}}", "challengeChallengesX": "Số thách đấu: {param1}", "challengeChallengeToPlay": "Thách đấu một ván cờ", @@ -254,6 +265,7 @@ "preferencesNotifyWeb": "Trình duyệt", "preferencesNotifyDevice": "Thiết bị", "preferencesBellNotificationSound": "Âm thanh chuông báo", + "preferencesBlindfold": "Bịt mắt", "puzzlePuzzles": "Câu đố", "puzzlePuzzleThemes": "Chủ đề câu đố", "puzzleRecommended": "Được đề xuất", @@ -571,7 +583,6 @@ "replayMode": "Chế độ xem lại", "realtimeReplay": "Thời gian thực", "byCPL": "Theo phần trăm mất tốt (CPL)", - "openStudy": "Mở nghiên cứu", "enable": "Bật", "bestMoveArrow": "Mũi tên chỉ nước đi tốt nhất", "showVariationArrows": "Hiển thị mũi tên biến", @@ -779,7 +790,6 @@ "block": "Chặn", "blocked": "Đã chặn", "unblock": "Bỏ chặn", - "followsYou": "Theo dõi bạn", "xStartedFollowingY": "{param1} đã bắt đầu theo dõi {param2}", "more": "Xem thêm", "memberSince": "Thành viên từ", @@ -1533,6 +1543,7 @@ "studyPlayAgain": "Chơi lại", "studyWhatWouldYouPlay": "Bạn sẽ làm gì ở thế cờ này?", "studyYouCompletedThisLesson": "Chúc mừng! Bạn đã hoàn thành bài học này.", + "studyPerPage": "{param} mỗi trang", "studyNbChapters": "{count, plural, other{{count} Chương}}", "studyNbGames": "{count, plural, other{{count} Ván cờ}}", "studyNbMembers": "{count, plural, other{{count} Thành viên}}", diff --git a/lib/l10n/lila_zh.arb b/lib/l10n/lila_zh.arb index f2d025b20e..6f6c6f6e75 100644 --- a/lib/l10n/lila_zh.arb +++ b/lib/l10n/lila_zh.arb @@ -1,47 +1,48 @@ { + "mobileAllGames": "所有对局", + "mobileAreYouSure": "你确定吗?", + "mobileBlindfoldMode": "盲棋", + "mobileCancelTakebackOffer": "取消悔棋请求", + "mobileClearButton": "清空", + "mobileCorrespondenceClearSavedMove": "清除已保存的着法", + "mobileCustomGameJoinAGame": "加入一局游戏", + "mobileFeedbackButton": "问题反馈", + "mobileGreeting": "你好,{param}", + "mobileGreetingWithoutName": "你好!", + "mobileHideVariation": "隐藏变着", "mobileHomeTab": "主页", - "mobilePuzzlesTab": "谜题", - "mobileToolsTab": "工具", - "mobileWatchTab": "观看", - "mobileSettingsTab": "设置", + "mobileLiveStreamers": "主播", "mobileMustBeLoggedIn": "您必须登录才能浏览此页面。", - "mobileSystemColors": "系统颜色", - "mobileFeedbackButton": "问题反馈", + "mobileNoSearchResults": "无结果", + "mobileNotFollowingAnyUser": "你没有关注任何用户。", "mobileOkButton": "好", + "mobilePlayersMatchingSearchTerm": "包含\"{param}\"名称的棋手", + "mobilePrefMagnifyDraggedPiece": "放大正在拖动的棋子", + "mobilePuzzleStormConfirmEndRun": "你想结束这组吗?", + "mobilePuzzleStormFilterNothingToShow": "没有结果,请更改筛选条件", + "mobilePuzzleStormNothingToShow": "没有记录。 请下几组 Puzzle Storm。", + "mobilePuzzleStormSubtitle": "在3分钟内解决尽可能多的谜题。", + "mobilePuzzleStreakAbortWarning": "你将失去你目前的连胜,你的分数将被保存。", + "mobilePuzzleThemesSubtitle": "从你最喜欢的开局解决谜题,或选择一个主题。", + "mobilePuzzlesTab": "谜题", + "mobileRecentSearches": "最近搜索", "mobileSettingsHapticFeedback": "震动反馈", "mobileSettingsImmersiveMode": "沉浸模式", "mobileSettingsImmersiveModeSubtitle": "下棋时隐藏系统界面。 如果您的操作受到屏幕边缘的系统导航手势干扰,请使用此功能。 适用于棋局和 Puzzle Storm 界面。", - "mobileNotFollowingAnyUser": "你没有关注任何用户。", - "mobileAllGames": "所有对局", - "mobileRecentSearches": "最近搜索", - "mobileClearButton": "清空", - "mobilePlayersMatchingSearchTerm": "包含\"{param}\"名称的棋手", - "mobileNoSearchResults": "无结果", - "mobileAreYouSure": "你确定吗?", - "mobilePuzzleStreakAbortWarning": "你将失去你目前的连胜,你的分数将被保存。", - "mobilePuzzleStormNothingToShow": "没有记录。 请下几组 Puzzle Storm。", - "mobileSharePuzzle": "分享这个谜题", - "mobileShareGameURL": "分享棋局链接", + "mobileSettingsTab": "设置", "mobileShareGamePGN": "分享 PGN", + "mobileShareGameURL": "分享棋局链接", "mobileSharePositionAsFEN": "保存局面为 FEN", - "mobileShowVariations": "显示变着", - "mobileHideVariation": "隐藏变着", + "mobileSharePuzzle": "分享这个谜题", "mobileShowComments": "显示评论", - "mobilePuzzleStormConfirmEndRun": "你想结束这组吗?", - "mobilePuzzleStormFilterNothingToShow": "没有显示,请更改过滤器", - "mobileCancelTakebackOffer": "取消悔棋请求", - "mobileWaitingForOpponentToJoin": "正在等待对手加入...", - "mobileBlindfoldMode": "盲棋", - "mobileLiveStreamers": "主播", - "mobileCustomGameJoinAGame": "加入一局游戏", - "mobileCorrespondenceClearSavedMove": "清除已保存的着法", - "mobileSomethingWentWrong": "出了一些问题。", "mobileShowResult": "显示结果", - "mobilePuzzleThemesSubtitle": "从你最喜欢的开局解决谜题,或选择一个主题。", - "mobilePuzzleStormSubtitle": "在3分钟内解决尽可能多的谜题。", - "mobileGreeting": "你好,{param}", - "mobileGreetingWithoutName": "你好!", - "mobilePrefMagnifyDraggedPiece": "放大正在拖动的棋子", + "mobileShowVariations": "显示变着", + "mobileSomethingWentWrong": "出了一些问题。", + "mobileSystemColors": "系统颜色", + "mobileTheme": "主题", + "mobileToolsTab": "工具", + "mobileWaitingForOpponentToJoin": "正在等待对手加入...", + "mobileWatchTab": "观看", "activityActivity": "动态", "activityHostedALiveStream": "主持了直播", "activityRankedInSwissTournament": "在 {param2} 中获得第 #{param1} 名", @@ -107,6 +108,8 @@ "broadcastAgeThisYear": "今年的年龄", "broadcastUnrated": "未评级", "broadcastRecentTournaments": "最近的比赛", + "broadcastPastBroadcasts": "结束的转播", + "broadcastAllBroadcastsByMonth": "按月查看所有转播", "broadcastNbBroadcasts": "{count, plural, other{{count} 直播}}", "challengeChallengesX": "挑战: {param1}", "challengeChallengeToPlay": "发起挑战", @@ -231,6 +234,7 @@ "preferencesNotifyWeb": "浏览器通知", "preferencesNotifyDevice": "设备通知", "preferencesBellNotificationSound": "通知铃声", + "preferencesBlindfold": "盲棋", "puzzlePuzzles": "谜题", "puzzlePuzzleThemes": "训练主题", "puzzleRecommended": "我们推荐:", @@ -548,7 +552,6 @@ "replayMode": "回放模式", "realtimeReplay": "实时回放", "byCPL": "按厘兵损失", - "openStudy": "进入研讨室", "enable": "启用", "bestMoveArrow": "最佳着法指示", "showVariationArrows": "显示变着箭头", @@ -755,7 +758,6 @@ "block": "加入黑名单", "blocked": "已加入黑名单", "unblock": "移出黑名单", - "followsYou": "关注了你", "xStartedFollowingY": "{param1}开始关注{param2}", "more": "更多", "memberSince": "注册日期", diff --git a/lib/l10n/lila_zh_TW.arb b/lib/l10n/lila_zh_TW.arb index 6f8ad37247..50468d8f23 100644 --- a/lib/l10n/lila_zh_TW.arb +++ b/lib/l10n/lila_zh_TW.arb @@ -1,47 +1,47 @@ { + "mobileAllGames": "所有棋局", + "mobileAreYouSure": "您確定嗎?", + "mobileBlindfoldMode": "盲棋", + "mobileCancelTakebackOffer": "取消悔棋請求", + "mobileClearButton": "清除", + "mobileCorrespondenceClearSavedMove": "清除已儲存移動", + "mobileCustomGameJoinAGame": "加入棋局", + "mobileFeedbackButton": "問題反饋", + "mobileGreeting": "您好, {param}", + "mobileGreetingWithoutName": "您好", + "mobileHideVariation": "隱藏變體", "mobileHomeTab": "首頁", - "mobilePuzzlesTab": "謎題", - "mobileToolsTab": "工具", - "mobileWatchTab": "觀戰", - "mobileSettingsTab": "設定", + "mobileLiveStreamers": "Lichess 實況主", "mobileMustBeLoggedIn": "你必須登入才能查看此頁面。", - "mobileSystemColors": "系統顏色", - "mobileFeedbackButton": "問題反饋", + "mobileNoSearchResults": "沒有任何搜尋結果", + "mobileNotFollowingAnyUser": "您未被任何使用者追蹤。", "mobileOkButton": "確認", + "mobilePlayersMatchingSearchTerm": "名稱包含「{param}」的玩家", + "mobilePrefMagnifyDraggedPiece": "放大被拖曳的棋子", + "mobilePuzzleStormConfirmEndRun": "是否中斷於此?", + "mobilePuzzleStormFilterNothingToShow": "沒有內容可顯示,請更改篩選條件", + "mobilePuzzleStormNothingToShow": "沒有內容可顯示。您可以進行一些 Puzzle Storm 。", + "mobilePuzzleStormSubtitle": "在三分鐘內解開盡可能多的謎題", + "mobilePuzzleStreakAbortWarning": "這將失去目前的連勝並且將儲存目前成績。", + "mobilePuzzleThemesSubtitle": "從您喜歡的開局進行謎題,或選擇一個主題。", + "mobilePuzzlesTab": "謎題", + "mobileRecentSearches": "搜尋紀錄", "mobileSettingsHapticFeedback": "震動回饋", "mobileSettingsImmersiveMode": "沉浸模式", "mobileSettingsImmersiveModeSubtitle": "在下棋和 Puzzle Storm 時隱藏系統界面。如果您受到螢幕邊緣的系統導航手勢干擾,可以使用此功能。", - "mobileNotFollowingAnyUser": "您未被任何使用者追蹤。", - "mobileAllGames": "所有棋局", - "mobileRecentSearches": "搜尋紀錄", - "mobileClearButton": "清除", - "mobilePlayersMatchingSearchTerm": "名稱包含「{param}」的玩家", - "mobileNoSearchResults": "沒有任何搜尋結果", - "mobileAreYouSure": "您確定嗎?", - "mobilePuzzleStreakAbortWarning": "這將失去目前的連勝並且將儲存目前成績。", - "mobilePuzzleStormNothingToShow": "沒有內容可顯示。您可以進行一些 Puzzle Storm 。", - "mobileSharePuzzle": "分享這個謎題", - "mobileShareGameURL": "分享對局網址", + "mobileSettingsTab": "設定", "mobileShareGamePGN": "分享 PGN", + "mobileShareGameURL": "分享對局網址", "mobileSharePositionAsFEN": "以 FEN 分享棋局位置", - "mobileShowVariations": "顯示變體", - "mobileHideVariation": "隱藏變體", + "mobileSharePuzzle": "分享這個謎題", "mobileShowComments": "顯示留言", - "mobilePuzzleStormConfirmEndRun": "是否中斷於此?", - "mobilePuzzleStormFilterNothingToShow": "沒有內容可顯示,請更改篩選條件", - "mobileCancelTakebackOffer": "取消悔棋請求", - "mobileWaitingForOpponentToJoin": "正在等待對手加入...", - "mobileBlindfoldMode": "盲棋", - "mobileLiveStreamers": "Lichess 實況主", - "mobileCustomGameJoinAGame": "加入棋局", - "mobileCorrespondenceClearSavedMove": "清除已儲存移動", - "mobileSomethingWentWrong": "發生了一些問題。", "mobileShowResult": "顯示結果", - "mobilePuzzleThemesSubtitle": "從您喜歡的開局進行謎題,或選擇一個主題。", - "mobilePuzzleStormSubtitle": "在三分鐘內解開盡可能多的謎題", - "mobileGreeting": "您好, {param}", - "mobileGreetingWithoutName": "您好", - "mobilePrefMagnifyDraggedPiece": "放大被拖曳的棋子", + "mobileShowVariations": "顯示變體", + "mobileSomethingWentWrong": "發生了一些問題。", + "mobileSystemColors": "系統顏色", + "mobileToolsTab": "工具", + "mobileWaitingForOpponentToJoin": "正在等待對手加入...", + "mobileWatchTab": "觀戰", "activityActivity": "活動", "activityHostedALiveStream": "主持一個現場直播", "activityRankedInSwissTournament": "在{param2}中排名 {param1}", @@ -116,6 +116,8 @@ "broadcastSubscribeTitle": "訂閱以在每輪開始時獲得通知。您可以在帳戶設定中切換直播的鈴聲或推播通知。", "broadcastUploadImage": "上傳錦標賽圖片", "broadcastNoBoardsYet": "尚無棋局。這些棋局將在對局上傳後顯示。", + "broadcastBoardsCanBeLoaded": "棋盤能夠以輸入源投放或是利用{param}", + "broadcastStartsAfter": "於{param}開始", "broadcastStartVerySoon": "直播即將開始。", "broadcastNotYetStarted": "直播尚未開始。", "broadcastOfficialWebsite": "官網", @@ -128,6 +130,15 @@ "broadcastRatingDiff": "評級差異", "broadcastGamesThisTournament": "此比賽的對局", "broadcastScore": "分數", + "broadcastAllTeams": "所有團隊", + "broadcastTournamentFormat": "錦標賽格式", + "broadcastTournamentLocation": "錦標賽地點", + "broadcastTopPlayers": "排行榜", + "broadcastTimezone": "時區", + "broadcastFideRatingCategory": "FIDE 評級類別", + "broadcastOptionalDetails": "其他細節", + "broadcastPastBroadcasts": "直播紀錄", + "broadcastAllBroadcastsByMonth": "以月份顯示所有直播", "broadcastNbBroadcasts": "{count, plural, other{{count} 個直播}}", "challengeChallengesX": "挑戰: {param1}", "challengeChallengeToPlay": "邀請對弈", @@ -252,6 +263,7 @@ "preferencesNotifyWeb": "瀏覽器通知", "preferencesNotifyDevice": "設備通知", "preferencesBellNotificationSound": "通知鈴聲", + "preferencesBlindfold": "盲棋", "puzzlePuzzles": "謎題", "puzzlePuzzleThemes": "謎題主題", "puzzleRecommended": "推薦", @@ -569,7 +581,6 @@ "replayMode": "重播模式", "realtimeReplay": "實時", "byCPL": "以厘兵損失", - "openStudy": "開啟研究", "enable": "啟用", "bestMoveArrow": "最佳移動的箭頭", "showVariationArrows": "顯示變體箭頭", @@ -776,7 +787,6 @@ "block": "加入黑名單", "blocked": "已加入黑名單", "unblock": "移除出黑名單", - "followsYou": "關注您", "xStartedFollowingY": "{param1}開始關注{param2}", "more": "更多", "memberSince": "註冊日期", diff --git a/lib/src/view/game/game_settings.dart b/lib/src/view/game/game_settings.dart index d8eed2bd0c..b797510639 100644 --- a/lib/src/view/game/game_settings.dart +++ b/lib/src/view/game/game_settings.dart @@ -87,7 +87,7 @@ class GameSettings extends ConsumerWidget { }, ), SwitchSettingTile( - title: Text(context.l10n.mobileBlindfoldMode), + title: Text(context.l10n.preferencesBlindfold), value: gamePrefs.blindfoldMode ?? false, onChanged: (value) { ref.read(gamePreferencesProvider.notifier).toggleBlindfoldMode(); diff --git a/translation/source/mobile.xml b/translation/source/mobile.xml index afc8d841bf..3d336b4a68 100644 --- a/translation/source/mobile.xml +++ b/translation/source/mobile.xml @@ -2,7 +2,6 @@ All games Are you sure? - Blindfold Cancel takeback offer Clear Clear saved move From 342d0b58ffafd17aba4d3de597475e56a0ce6d11 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 12 Dec 2024 12:27:49 +0100 Subject: [PATCH 902/979] Tweak cupertino players tab color --- lib/src/view/broadcast/broadcast_players_tab.dart | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_players_tab.dart b/lib/src/view/broadcast/broadcast_players_tab.dart index 9088af4e2d..4b366f9d53 100644 --- a/lib/src/view/broadcast/broadcast_players_tab.dart +++ b/lib/src/view/broadcast/broadcast_players_tab.dart @@ -1,6 +1,7 @@ import 'dart:math'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; @@ -179,9 +180,15 @@ class _PlayersListState extends ConsumerState { final player = players[index - 1]; return Container( decoration: BoxDecoration( - color: index.isEven - ? Theme.of(context).colorScheme.surfaceContainerLow - : Theme.of(context).colorScheme.surfaceContainerHigh, + color: Theme.of(context).platform == TargetPlatform.iOS + ? index.isEven + ? CupertinoColors.secondarySystemBackground + .resolveFrom(context) + : CupertinoColors.tertiarySystemBackground + .resolveFrom(context) + : index.isEven + ? Theme.of(context).colorScheme.surfaceContainerLow + : Theme.of(context).colorScheme.surfaceContainerHigh, ), child: Row( crossAxisAlignment: CrossAxisAlignment.center, From 0b8c1d96d34736a79db81fa440a72d0859c99804 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 12 Dec 2024 12:28:16 +0100 Subject: [PATCH 903/979] Bump version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 41986d6ae3..0bbb83d620 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: lichess_mobile description: Lichess mobile app V2 publish_to: "none" -version: 0.13.7+001307 # See README.md for details about versioning +version: 0.13.8+001308 # See README.md for details about versioning environment: sdk: ">=3.5.0 <4.0.0" From 0af48bdc156a67f61e476c30dd592024f418384f Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Thu, 12 Dec 2024 13:02:27 +0100 Subject: [PATCH 904/979] Use slugs instead of round url to share game pgn --- lib/src/model/broadcast/broadcast.dart | 3 ++- .../model/broadcast/broadcast_repository.dart | 3 ++- .../view/broadcast/broadcast_boards_tab.dart | 17 ++++++++++++----- .../broadcast/broadcast_game_bottom_bar.dart | 15 +++++++-------- .../view/broadcast/broadcast_game_screen.dart | 18 ++++++++++++------ .../view/broadcast/broadcast_list_screen.dart | 3 ++- .../view/broadcast/broadcast_round_screen.dart | 2 ++ 7 files changed, 39 insertions(+), 22 deletions(-) diff --git a/lib/src/model/broadcast/broadcast.dart b/lib/src/model/broadcast/broadcast.dart index a1ad6be868..1e64188614 100644 --- a/lib/src/model/broadcast/broadcast.dart +++ b/lib/src/model/broadcast/broadcast.dart @@ -48,6 +48,7 @@ class BroadcastTournamentData with _$BroadcastTournamentData { const factory BroadcastTournamentData({ required BroadcastTournamentId id, required String name, + required String slug, required String? imageUrl, required String? description, required BroadcastTournamentInformation information, @@ -78,11 +79,11 @@ class BroadcastRound with _$BroadcastRound { const factory BroadcastRound({ required BroadcastRoundId id, required String name, + required String slug, required RoundStatus status, required DateTime? startsAt, required DateTime? finishedAt, required bool startsAfterPrevious, - required String? url, }) = _BroadcastRound; } diff --git a/lib/src/model/broadcast/broadcast_repository.dart b/lib/src/model/broadcast/broadcast_repository.dart index c5f943d719..13d2adfe60 100644 --- a/lib/src/model/broadcast/broadcast_repository.dart +++ b/lib/src/model/broadcast/broadcast_repository.dart @@ -99,6 +99,7 @@ BroadcastTournamentData _tournamentDataFromPick( BroadcastTournamentData( id: pick('id').asBroadcastTournamentIdOrThrow(), name: pick('name').asStringOrThrow(), + slug: pick('slug').asStringOrThrow(), imageUrl: pick('image').asStringOrNull(), description: pick('description').asStringOrNull(), information: ( @@ -149,11 +150,11 @@ BroadcastRound _roundFromPick(RequiredPick pick) { return BroadcastRound( id: pick('id').asBroadcastRoundIdOrThrow(), name: pick('name').asStringOrThrow(), + slug: pick('slug').asStringOrThrow(), status: status, startsAt: pick('startsAt').asDateTimeFromMillisecondsOrNull(), finishedAt: pick('finishedAt').asDateTimeFromMillisecondsOrNull(), startsAfterPrevious: pick('startsAfterPrevious').asBoolOrFalse(), - url: pick('url').asStringOrNull(), ); } diff --git a/lib/src/view/broadcast/broadcast_boards_tab.dart b/lib/src/view/broadcast/broadcast_boards_tab.dart index fa1b3dde91..adb77112c8 100644 --- a/lib/src/view/broadcast/broadcast_boards_tab.dart +++ b/lib/src/view/broadcast/broadcast_boards_tab.dart @@ -26,9 +26,11 @@ const _kPlayerWidgetPadding = EdgeInsets.symmetric(vertical: 5.0); class BroadcastBoardsTab extends ConsumerWidget { const BroadcastBoardsTab({ required this.roundId, + required this.tournamentSlug, }); final BroadcastRoundId roundId; + final String tournamentSlug; @override Widget build(BuildContext context, WidgetRef ref) { @@ -59,7 +61,8 @@ class BroadcastBoardsTab extends ConsumerWidget { games: value.games.values.toIList(), roundId: roundId, title: value.round.name, - roundUrl: value.round.url, + tournamentSlug: tournamentSlug, + roundSlug: value.round.slug, ), AsyncError(:final error) => SliverFillRemaining( child: Center( @@ -77,19 +80,22 @@ class BroadcastPreview extends StatelessWidget { required this.roundId, required this.games, required this.title, - required this.roundUrl, + required this.tournamentSlug, + required this.roundSlug, }); const BroadcastPreview.loading() : roundId = const BroadcastRoundId(''), games = null, title = '', - roundUrl = null; + tournamentSlug = '', + roundSlug = ''; final BroadcastRoundId roundId; final IList? games; final String title; - final String? roundUrl; + final String tournamentSlug; + final String roundSlug; @override Widget build(BuildContext context) { @@ -140,7 +146,8 @@ class BroadcastPreview extends StatelessWidget { builder: (context) => BroadcastGameScreen( roundId: roundId, gameId: game.id, - roundUrl: roundUrl, + tournamentSlug: tournamentSlug, + roundSlug: roundSlug, title: title, ), ); diff --git a/lib/src/view/broadcast/broadcast_game_bottom_bar.dart b/lib/src/view/broadcast/broadcast_game_bottom_bar.dart index d2973ec3d9..e408303f91 100644 --- a/lib/src/view/broadcast/broadcast_game_bottom_bar.dart +++ b/lib/src/view/broadcast/broadcast_game_bottom_bar.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_game_controller.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_repository.dart'; -import 'package:lichess_mobile/src/model/broadcast/broadcast_round_controller.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; import 'package:lichess_mobile/src/model/game/game_share_service.dart'; import 'package:lichess_mobile/src/network/http.dart'; @@ -19,19 +18,19 @@ class BroadcastGameBottomBar extends ConsumerWidget { const BroadcastGameBottomBar({ required this.roundId, required this.gameId, - this.roundUrl, + this.tournamentSlug, + this.roundSlug, }); final BroadcastRoundId roundId; final BroadcastGameId gameId; - final String? roundUrl; + final String? tournamentSlug; + final String? roundSlug; @override Widget build(BuildContext context, WidgetRef ref) { final ctrlProvider = broadcastGameControllerProvider(roundId, gameId); final broadcastGameState = ref.watch(ctrlProvider).requireValue; - final broadcastRoundState = - ref.watch(broadcastRoundControllerProvider(roundId)); return BottomBar( children: [ @@ -41,15 +40,15 @@ class BroadcastGameBottomBar extends ConsumerWidget { showAdaptiveActionSheet( context: context, actions: [ - if (roundUrl != null || broadcastRoundState.hasValue) + if (tournamentSlug != null && roundSlug != null) BottomSheetAction( makeLabel: (context) => Text(context.l10n.mobileShareGameURL), onPressed: (context) async { launchShareDialog( context, - uri: Uri.parse( - '${roundUrl ?? broadcastRoundState.requireValue.round.url}/$gameId', + uri: lichessUri( + '/broadcast/$tournamentSlug/$roundSlug/$roundId/$gameId', ), ); }, diff --git a/lib/src/view/broadcast/broadcast_game_screen.dart b/lib/src/view/broadcast/broadcast_game_screen.dart index 68f860f08e..ef028d43f1 100644 --- a/lib/src/view/broadcast/broadcast_game_screen.dart +++ b/lib/src/view/broadcast/broadcast_game_screen.dart @@ -33,13 +33,15 @@ import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; class BroadcastGameScreen extends ConsumerStatefulWidget { final BroadcastRoundId roundId; final BroadcastGameId gameId; - final String? roundUrl; + final String? tournamentSlug; + final String? roundSlug; final String? title; const BroadcastGameScreen({ required this.roundId, required this.gameId, - this.roundUrl, + this.tournamentSlug, + this.roundSlug, this.title, }); @@ -117,7 +119,8 @@ class _BroadcastGameScreenState extends ConsumerState AsyncData() => _Body( widget.roundId, widget.gameId, - widget.roundUrl, + widget.tournamentSlug, + widget.roundSlug, tabController: _tabController, ), AsyncError(:final error) => Center( @@ -133,13 +136,15 @@ class _Body extends ConsumerWidget { const _Body( this.roundId, this.gameId, - this.roundUrl, { + this.tournamentSlug, + this.roundSlug, { required this.tabController, }); final BroadcastRoundId roundId; final BroadcastGameId gameId; - final String? roundUrl; + final String? tournamentSlug; + final String? roundSlug; final TabController tabController; @override @@ -206,7 +211,8 @@ class _Body extends ConsumerWidget { bottomBar: BroadcastGameBottomBar( roundId: roundId, gameId: gameId, - roundUrl: roundUrl, + tournamentSlug: tournamentSlug, + roundSlug: roundSlug, ), children: [ _OpeningExplorerTab(roundId, gameId), diff --git a/lib/src/view/broadcast/broadcast_list_screen.dart b/lib/src/view/broadcast/broadcast_list_screen.dart index 0274768015..172835cefa 100644 --- a/lib/src/view/broadcast/broadcast_list_screen.dart +++ b/lib/src/view/broadcast/broadcast_list_screen.dart @@ -220,6 +220,7 @@ class BroadcastGridItem extends StatefulWidget { tour: BroadcastTournamentData( id: BroadcastTournamentId(''), name: '', + slug: '', imageUrl: null, description: '', information: ( @@ -234,11 +235,11 @@ class BroadcastGridItem extends StatefulWidget { round: BroadcastRound( id: BroadcastRoundId(''), name: '', + slug: '', status: RoundStatus.finished, startsAt: null, finishedAt: null, startsAfterPrevious: false, - url: null, ), group: null, roundToLinkId: BroadcastRoundId(''), diff --git a/lib/src/view/broadcast/broadcast_round_screen.dart b/lib/src/view/broadcast/broadcast_round_screen.dart index 085e8e4e16..26e5c6a2c8 100644 --- a/lib/src/view/broadcast/broadcast_round_screen.dart +++ b/lib/src/view/broadcast/broadcast_round_screen.dart @@ -117,6 +117,7 @@ class _BroadcastRoundScreenState extends ConsumerState sliver: switch (asyncTournament) { AsyncData(:final value) => BroadcastBoardsTab( roundId: _selectedRoundId ?? value.defaultRoundId, + tournamentSlug: widget.broadcast.tour.slug, ), _ => const SliverFillRemaining( child: SizedBox.shrink(), @@ -181,6 +182,7 @@ class _BroadcastRoundScreenState extends ConsumerState sliver: switch (asyncTournament) { AsyncData(:final value) => BroadcastBoardsTab( roundId: _selectedRoundId ?? value.defaultRoundId, + tournamentSlug: widget.broadcast.tour.slug, ), _ => const SliverFillRemaining( child: SizedBox.shrink(), From 56075dc42454133d77d1d4f2317de09ff0454077 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Thu, 12 Dec 2024 13:03:52 +0100 Subject: [PATCH 905/979] Rename federation ID to names variable --- lib/src/model/broadcast/broadcast_federation.dart | 2 +- lib/src/view/broadcast/broadcast_player_results_screen.dart | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/src/model/broadcast/broadcast_federation.dart b/lib/src/model/broadcast/broadcast_federation.dart index 1deb73bb2e..267b53fc69 100644 --- a/lib/src/model/broadcast/broadcast_federation.dart +++ b/lib/src/model/broadcast/broadcast_federation.dart @@ -1,4 +1,4 @@ -const fedIdToName = { +const federationIdToName = { 'FID': 'FIDE', 'USA': 'United States of America', 'IND': 'India', diff --git a/lib/src/view/broadcast/broadcast_player_results_screen.dart b/lib/src/view/broadcast/broadcast_player_results_screen.dart index cb53bc66ef..10b20b40d7 100644 --- a/lib/src/view/broadcast/broadcast_player_results_screen.dart +++ b/lib/src/view/broadcast/broadcast_player_results_screen.dart @@ -157,7 +157,8 @@ class _Body extends ConsumerWidget { const SizedBox(width: 5), Flexible( child: Text( - fedIdToName[player.federation!]!, + federationIdToName[ + player.federation!]!, style: const TextStyle( fontSize: 18.0, ), From 9a6a4004237aac6fac86c87555d7ecf9da7d49dc Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 12 Dec 2024 14:51:24 +0100 Subject: [PATCH 906/979] Fix broadcast clock update issues --- .../broadcast/broadcast_game_controller.dart | 7 ++++- .../model/broadcast/broadcast_repository.dart | 28 +++++++++++++---- .../broadcast/broadcast_round_controller.dart | 1 + .../view/broadcast/broadcast_game_screen.dart | 30 +++++++++++-------- 4 files changed, 47 insertions(+), 19 deletions(-) diff --git a/lib/src/model/broadcast/broadcast_game_controller.dart b/lib/src/model/broadcast/broadcast_game_controller.dart index 3ac22f1e48..79ffab5cb1 100644 --- a/lib/src/model/broadcast/broadcast_game_controller.dart +++ b/lib/src/model/broadcast/broadcast_game_controller.dart @@ -153,6 +153,8 @@ class BroadcastGameController extends _$BroadcastGameController // check provider is still mounted if (key == _key) { + final curState = state.requireValue; + final wasOnLivePath = curState.broadcastLivePath == curState.currentPath; final game = PgnGame.parsePgn(pgn); final pgnHeaders = IMap(game.headers); final rootComments = @@ -167,14 +169,17 @@ class BroadcastGameController extends _$BroadcastGameController _root = newRoot; + final newCurrentPath = + wasOnLivePath ? broadcastPath : curState.currentPath; state = AsyncData( state.requireValue.copyWith( + currentPath: newCurrentPath, pgnHeaders: pgnHeaders, pgnRootComments: rootComments, broadcastPath: broadcastPath, root: _root.view, lastMove: lastMove, - clocks: _getClocks(state.requireValue.currentPath), + clocks: _getClocks(newCurrentPath), ), ); } diff --git a/lib/src/model/broadcast/broadcast_repository.dart b/lib/src/model/broadcast/broadcast_repository.dart index 67dec06f47..1a6c49a13c 100644 --- a/lib/src/model/broadcast/broadcast_repository.dart +++ b/lib/src/model/broadcast/broadcast_repository.dart @@ -177,29 +177,47 @@ MapEntry gameFromPick( /// The amount of time that the player whose turn it is has been thinking since his last move final thinkTime = pick('thinkTime').asDurationFromSecondsOrNull() ?? Duration.zero; + final fen = + pick('fen').asStringOrNull() ?? Variant.standard.initialPosition.fen; + final playingSide = Setup.parseFen(fen).turn; return MapEntry( pick('id').asBroadcastGameIdOrThrow(), BroadcastGame( id: pick('id').asBroadcastGameIdOrThrow(), players: IMap({ - Side.white: _playerFromPick(pick('players', 0).required()), - Side.black: _playerFromPick(pick('players', 1).required()), + Side.white: _playerFromPick( + pick('players', 0).required(), + isPlaying: playingSide == Side.white, + thinkingTime: thinkTime, + ), + Side.black: _playerFromPick( + pick('players', 1).required(), + isPlaying: playingSide == Side.black, + thinkingTime: thinkTime, + ), }), fen: pick('fen').asStringOrNull() ?? Variant.standard.initialPosition.fen, lastMove: pick('lastMove').asUciMoveOrNull(), status: status, - updatedClockAt: DateTime.now().subtract(thinkTime), + updatedClockAt: DateTime.now(), ), ); } -BroadcastPlayer _playerFromPick(RequiredPick pick) { +BroadcastPlayer _playerFromPick( + RequiredPick pick, { + required bool isPlaying, + required Duration thinkingTime, +}) { + final clock = pick('clock').asDurationFromCentiSecondsOrNull(); + final updatedClock = + clock != null && isPlaying ? clock - thinkingTime : clock; return BroadcastPlayer( name: pick('name').asStringOrThrow(), title: pick('title').asStringOrNull(), rating: pick('rating').asIntOrNull(), - clock: pick('clock').asDurationFromCentiSecondsOrNull(), + clock: updatedClock, federation: pick('fed').asStringOrNull(), fideId: pick('fideId').asFideIdOrNull(), ); diff --git a/lib/src/model/broadcast/broadcast_round_controller.dart b/lib/src/model/broadcast/broadcast_round_controller.dart index 680e26ddd8..fd9569cf77 100644 --- a/lib/src/model/broadcast/broadcast_round_controller.dart +++ b/lib/src/model/broadcast/broadcast_round_controller.dart @@ -181,6 +181,7 @@ class BroadcastRoundController extends _$BroadcastRoundController { ), }, ), + updatedClockAt: DateTime.now(), ), ), ), diff --git a/lib/src/view/broadcast/broadcast_game_screen.dart b/lib/src/view/broadcast/broadcast_game_screen.dart index 55a01f0352..b9b367fe09 100644 --- a/lib/src/view/broadcast/broadcast_game_screen.dart +++ b/lib/src/view/broadcast/broadcast_game_screen.dart @@ -369,7 +369,13 @@ class _PlayerWidget extends ConsumerWidget { final broadcastGameState = ref .watch(broadcastGameControllerProvider(roundId, gameId)) .requireValue; - final clocks = broadcastGameState.clocks; + // TODO + // we'll probably want to remove this and get the game state from a single controller + // this won't work with deep links for instance + final game = ref.watch( + broadcastRoundControllerProvider(roundId) + .select((round) => round.requireValue.games[gameId]!), + ); final isCursorOnLiveMove = broadcastGameState.currentPath == broadcastGameState.broadcastLivePath; final sideToMove = broadcastGameState.position.turn; @@ -377,15 +383,14 @@ class _PlayerWidget extends ConsumerWidget { _PlayerWidgetPosition.bottom => broadcastGameState.pov, _PlayerWidgetPosition.top => broadcastGameState.pov.opposite, }; - final clock = (sideToMove == side) ? clocks?.parentClock : clocks?.clock; - - final game = ref.watch( - broadcastRoundControllerProvider(roundId) - .select((round) => round.requireValue.games[gameId]!), - ); final player = game.players[side]!; + final liveClock = isCursorOnLiveMove ? player.clock : null; final gameStatus = game.status; + final pastClocks = broadcastGameState.clocks; + final pastClock = + (sideToMove == side) ? pastClocks?.parentClock : pastClocks?.clock; + return Container( color: Theme.of(context).platform == TargetPlatform.iOS ? Styles.cupertinoCardColor.resolveFrom(context) @@ -418,7 +423,7 @@ class _PlayerWidget extends ConsumerWidget { const TextStyle().copyWith(fontWeight: FontWeight.bold), ), ), - if (clock != null) + if (liveClock != null || pastClock != null) Container( height: kAnalysisBoardHeaderOrFooterHeight, color: (side == sideToMove) @@ -429,9 +434,9 @@ class _PlayerWidget extends ConsumerWidget { child: Padding( padding: const EdgeInsets.symmetric(horizontal: 6.0), child: Center( - child: isCursorOnLiveMove + child: liveClock != null ? CountdownClockBuilder( - timeLeft: clock, + timeLeft: liveClock, active: side == sideToMove, builder: (context, timeLeft) => _Clock( timeLeft: timeLeft, @@ -439,11 +444,10 @@ class _PlayerWidget extends ConsumerWidget { isLive: true, ), tickInterval: const Duration(seconds: 1), - clockUpdatedAt: - side == sideToMove ? game.updatedClockAt : null, + clockUpdatedAt: game.updatedClockAt, ) : _Clock( - timeLeft: clock, + timeLeft: pastClock!, isSideToMove: side == sideToMove, isLive: false, ), From 6d9d0c082711209a8418c01c588ab4d25100f8bc Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 12 Dec 2024 15:15:39 +0100 Subject: [PATCH 907/979] Bump version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 0bbb83d620..9c5eb9c2e4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: lichess_mobile description: Lichess mobile app V2 publish_to: "none" -version: 0.13.8+001308 # See README.md for details about versioning +version: 0.13.9+001309 # See README.md for details about versioning environment: sdk: ">=3.5.0 <4.0.0" From cf3a3dec184f3207662167aea3f879f828ef271e Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Thu, 12 Dec 2024 15:26:23 +0100 Subject: [PATCH 908/979] Restore list color order for broadcast players --- lib/src/view/broadcast/broadcast_player_results_screen.dart | 4 ++-- lib/src/view/broadcast/broadcast_players_tab.dart | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_player_results_screen.dart b/lib/src/view/broadcast/broadcast_player_results_screen.dart index b345d9cbc5..ab90cf1e46 100644 --- a/lib/src/view/broadcast/broadcast_player_results_screen.dart +++ b/lib/src/view/broadcast/broadcast_player_results_screen.dart @@ -232,12 +232,12 @@ class _Body extends ConsumerWidget { }, child: ColoredBox( color: Theme.of(context).platform == TargetPlatform.iOS - ? (index - 1).isEven + ? index.isEven ? CupertinoColors.secondarySystemBackground .resolveFrom(context) : CupertinoColors.tertiarySystemBackground .resolveFrom(context) - : (index - 1).isEven + : index.isEven ? Theme.of(context) .colorScheme .surfaceContainerLow diff --git a/lib/src/view/broadcast/broadcast_players_tab.dart b/lib/src/view/broadcast/broadcast_players_tab.dart index ed4c936411..13585c8422 100644 --- a/lib/src/view/broadcast/broadcast_players_tab.dart +++ b/lib/src/view/broadcast/broadcast_players_tab.dart @@ -197,12 +197,12 @@ class _PlayersListState extends ConsumerState { }, child: ColoredBox( color: Theme.of(context).platform == TargetPlatform.iOS - ? (index - 1).isEven + ? index.isEven ? CupertinoColors.secondarySystemBackground .resolveFrom(context) : CupertinoColors.tertiarySystemBackground .resolveFrom(context) - : (index - 1).isEven + : index.isEven ? Theme.of(context).colorScheme.surfaceContainerLow : Theme.of(context).colorScheme.surfaceContainerHigh, child: Row( From 1b2ed9925306cd195b85fdf6a565bda6e6140e14 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Thu, 12 Dec 2024 16:15:43 +0100 Subject: [PATCH 909/979] Use circular indicator instead of board shimmer --- lib/src/view/broadcast/broadcast_boards_tab.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/src/view/broadcast/broadcast_boards_tab.dart b/lib/src/view/broadcast/broadcast_boards_tab.dart index adb77112c8..e5ab9e9153 100644 --- a/lib/src/view/broadcast/broadcast_boards_tab.dart +++ b/lib/src/view/broadcast/broadcast_boards_tab.dart @@ -69,7 +69,11 @@ class BroadcastBoardsTab extends ConsumerWidget { child: Text('Could not load broadcast: $error'), ), ), - _ => const BroadcastPreview.loading() + _ => const SliverFillRemaining( + child: Center( + child: CircularProgressIndicator.adaptive(), + ), + ), }, ); } From 28a5e398ed8dc72e0b5f5b038cf12b5856461199 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Thu, 12 Dec 2024 16:30:43 +0100 Subject: [PATCH 910/979] Listen only to the name of the round --- lib/src/view/broadcast/broadcast_game_screen.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_game_screen.dart b/lib/src/view/broadcast/broadcast_game_screen.dart index ef028d43f1..2f8fb62a7b 100644 --- a/lib/src/view/broadcast/broadcast_game_screen.dart +++ b/lib/src/view/broadcast/broadcast_game_screen.dart @@ -81,15 +81,15 @@ class _BroadcastGameScreenState extends ConsumerState Widget build(BuildContext context) { final broadcastGameState = ref .watch(broadcastGameControllerProvider(widget.roundId, widget.gameId)); - final broadcastRoundState = - ref.watch(broadcastRoundControllerProvider(widget.roundId)); + final title = ref.watch( + broadcastRoundControllerProvider(widget.roundId) + .select((round) => round.value?.round.name), + ); return PlatformScaffold( appBar: PlatformAppBar( title: Text( - widget.title ?? - broadcastRoundState.value?.round.name ?? - 'BroadcastGame', + widget.title ?? title ?? 'BroadcastGame', overflow: TextOverflow.ellipsis, maxLines: 1, ), From 560b01ff864df2bbee2cdf716ab85345a183200b Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 12 Dec 2024 16:43:19 +0100 Subject: [PATCH 911/979] Add prod track --- .github/workflows/deploy_play_store.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/deploy_play_store.yml b/.github/workflows/deploy_play_store.yml index 94e4194305..da63783196 100644 --- a/.github/workflows/deploy_play_store.yml +++ b/.github/workflows/deploy_play_store.yml @@ -11,6 +11,7 @@ on: options: - internal - alpha + - production # Declare default permissions as read only. permissions: read-all From 792bb667055b00b1f810828d2d7bf61ab6d930d9 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 12 Dec 2024 18:30:32 +0100 Subject: [PATCH 912/979] Fix android fastfile --- android/fastlane/Fastfile | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/android/fastlane/Fastfile b/android/fastlane/Fastfile index 63d6088225..f3f523549b 100644 --- a/android/fastlane/Fastfile +++ b/android/fastlane/Fastfile @@ -29,6 +29,13 @@ platform :android do end end +platform :android do + desc "Upload a production version to Google Play" + lane :production do + deploy_to_play_store('production') + end +end + def deploy_to_play_store(track) sh "flutter build appbundle -v --obfuscate --split-debug-info=./build/app/outputs/bundle/release/symbols --dart-define=cronetHttpNoPlay=true --dart-define=LICHESS_HOST=lichess.org --dart-define=LICHESS_WS_HOST=socket.lichess.org --dart-define=LICHESS_WS_SECRET=#{ENV['WS_SECRET']}" upload_to_play_store( From e845066a63ac41f693b4b3428f9541035140c939 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Fri, 13 Dec 2024 10:54:02 +0100 Subject: [PATCH 913/979] Fix color of BOT title --- lib/src/view/broadcast/broadcast_player_widget.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/view/broadcast/broadcast_player_widget.dart b/lib/src/view/broadcast/broadcast_player_widget.dart index dd25b72236..b2f081f63a 100644 --- a/lib/src/view/broadcast/broadcast_player_widget.dart +++ b/lib/src/view/broadcast/broadcast_player_widget.dart @@ -37,7 +37,7 @@ class BroadcastPlayerWidget extends ConsumerWidget { title!, style: TextStyle( color: (title == 'BOT') - ? context.lichessColors.purple + ? context.lichessColors.fancy : context.lichessColors.brag, fontWeight: FontWeight.bold, ), From 56835122d06db405f7e129631594f0d4f01f8b74 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 13 Dec 2024 11:19:54 +0100 Subject: [PATCH 914/979] Improve broadcast list screen --- lib/src/utils/image.dart | 3 + .../view/broadcast/broadcast_list_screen.dart | 275 ++++++++++-------- 2 files changed, 153 insertions(+), 125 deletions(-) diff --git a/lib/src/utils/image.dart b/lib/src/utils/image.dart index 15c3447e1b..15802e13e4 100644 --- a/lib/src/utils/image.dart +++ b/lib/src/utils/image.dart @@ -128,6 +128,9 @@ class ImageColorWorker { filter: false, ); final Hct sourceColor = Hct.fromInt(scoredResults.first); + if (sourceColor.tone > 90) { + sourceColor.tone = 90; + } final scheme = SchemeFidelity( sourceColorHct: sourceColor, isDark: false, diff --git a/lib/src/view/broadcast/broadcast_list_screen.dart b/lib/src/view/broadcast/broadcast_list_screen.dart index c6602c0775..299ebc8beb 100644 --- a/lib/src/view/broadcast/broadcast_list_screen.dart +++ b/lib/src/view/broadcast/broadcast_list_screen.dart @@ -22,7 +22,8 @@ import 'package:lichess_mobile/src/widgets/shimmer.dart'; const kDefaultBroadcastImage = AssetImage('assets/images/broadcast_image.png'); const kBroadcastGridItemBorderRadius = BorderRadius.all(Radius.circular(16.0)); -const kBroadcastGridItemContentPadding = EdgeInsets.symmetric(horizontal: 16.0); +const kBroadcastGridItemContentPadding = + EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0); /// A screen that displays a paginated list of broadcasts. class BroadcastListScreen extends StatelessWidget { @@ -121,9 +122,12 @@ class _BodyState extends ConsumerState<_Body> { ); return const Center(child: Text('Could not load broadcast tournaments')); } - - final isTablet = isTabletOrLarger(context); - final itemsByRow = isTablet ? 2 : 1; + final screenWidth = MediaQuery.sizeOf(context).width; + final itemsByRow = screenWidth >= 1200 + ? 3 + : screenWidth >= 800 + ? 2 + : 1; const loadingItems = 12; final itemsCount = broadcasts.requireValue.past.length + (broadcasts.isLoading ? loadingItems : 0); @@ -132,7 +136,7 @@ class _BodyState extends ConsumerState<_Body> { crossAxisCount: itemsByRow, crossAxisSpacing: 16.0, mainAxisSpacing: 16.0, - childAspectRatio: 1.45, + childAspectRatio: 1.4, ); final sections = [ @@ -341,9 +345,10 @@ class _BroadcastGridItemState extends State { _cardColors?.primaryContainer ?? defaultBackgroundColor; final titleColor = _cardColors?.onPrimaryContainer; final subTitleColor = - _cardColors?.onPrimaryContainer.withValues(alpha: 0.7) ?? - textShade(context, 0.7); + _cardColors?.onPrimaryContainer.withValues(alpha: 0.8) ?? + textShade(context, 0.8); final liveColor = _cardColors?.error ?? LichessColors.red; + final isTablet = isTabletOrLarger(context); return GestureDetector( onTap: () { @@ -358,139 +363,159 @@ class _BroadcastGridItemState extends State { onTapDown: (_) => _onTapDown(), onTapCancel: _onTapCancel, onTapUp: (_) => _onTapCancel(), - child: AnimatedContainer( - duration: const Duration(milliseconds: 500), - clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - borderRadius: kBroadcastGridItemBorderRadius, - color: backgroundColor, - boxShadow: Theme.of(context).platform == TargetPlatform.iOS - ? null - : kElevationToShadow[1], - ), - child: Stack( - children: [ - ShaderMask( - blendMode: BlendMode.dstOut, - shaderCallback: (bounds) { - return LinearGradient( - begin: const Alignment(0.0, 0.5), - end: Alignment.bottomCenter, - colors: [ - backgroundColor.withValues(alpha: 0.0), - backgroundColor.withValues(alpha: 1.0), - ], - stops: const [0.5, 1.10], - tileMode: TileMode.clamp, - ).createShader(bounds); - }, - child: AnimatedOpacity( - duration: const Duration(milliseconds: 100), - opacity: _tapDown ? 1.0 : 0.7, - child: AspectRatio( - aspectRatio: 2.0, - child: _imageProvider != null - ? Image( - image: _imageProvider!, - frameBuilder: - (context, child, frame, wasSynchronouslyLoaded) { - if (wasSynchronouslyLoaded) { - return child; - } - return AnimatedOpacity( - duration: const Duration(milliseconds: 500), - opacity: frame == null ? 0 : 1, - child: child, - ); - }, - errorBuilder: (context, error, stackTrace) => - const Image(image: kDefaultBroadcastImage), - ) - : const SizedBox.shrink(), - ), + child: AnimatedOpacity( + opacity: _tapDown ? 1.0 : 0.9, + duration: const Duration(milliseconds: 100), + child: Container( + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + borderRadius: kBroadcastGridItemBorderRadius, + boxShadow: Theme.of(context).platform == TargetPlatform.iOS + ? null + : kElevationToShadow[1], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + AspectRatio( + aspectRatio: 2.0, + child: _imageProvider != null + ? Image( + image: _imageProvider!, + frameBuilder: ( + context, + child, + frame, + wasSynchronouslyLoaded, + ) { + if (wasSynchronouslyLoaded) { + return child; + } + return AnimatedOpacity( + duration: const Duration(milliseconds: 500), + opacity: frame == null ? 0 : 1, + child: child, + ); + }, + errorBuilder: (context, error, stackTrace) => + const Image(image: kDefaultBroadcastImage), + ) + : const SizedBox.shrink(), ), - ), - Positioned( - left: 0, - right: 0, - bottom: 12.0, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (widget.broadcast.round.startsAt != null || - widget.broadcast.isLive) - Padding( - padding: kBroadcastGridItemContentPadding, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - widget.broadcast.round.name, - style: TextStyle( - fontSize: 12, - color: subTitleColor, - ), - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), - const SizedBox(width: 4.0), - if (widget.broadcast.isLive) + Expanded( + child: ColoredBox( + color: backgroundColor, + child: Padding( + padding: kBroadcastGridItemContentPadding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (widget.broadcast.round.startsAt != null) ...[ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + widget.broadcast.round.name, + style: TextStyle( + fontSize: 13, + color: subTitleColor, + ), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + const SizedBox(width: 6.0), + Text( + relativeDate( + widget.broadcast.round.startsAt!, + ), + style: TextStyle( + fontSize: 13, + color: subTitleColor, + ), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + const Spacer(), + if (widget.broadcast.isLive) + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.circle, + size: 16, + color: liveColor, + shadows: const [ + Shadow( + color: Colors.black54, + offset: Offset(0, 1), + blurRadius: 2, + ), + ], + ), + const SizedBox(width: 4.0), + Text( + 'LIVE', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: liveColor, + shadows: const [ + Shadow( + color: Colors.black54, + offset: Offset(0, 1), + blurRadius: 2, + ), + ], + ), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ], + ), + const SizedBox(height: 2.0), + ], Text( - 'LIVE', + widget.broadcast.title, + maxLines: 2, + overflow: TextOverflow.ellipsis, style: TextStyle( - fontSize: 15, + color: titleColor, fontWeight: FontWeight.bold, - color: liveColor, + height: 1.0, + fontSize: 16, ), - overflow: TextOverflow.ellipsis, - ) - else - Text( - relativeDate(widget.broadcast.round.startsAt!), + ), + ], + ), + if (widget.broadcast.tour.information.players != + null) ...[ + SizedBox(height: isTablet ? 0.0 : 8.0), + Flexible( + child: Text( + widget.broadcast.tour.information.players!, style: TextStyle( - fontSize: 12, + fontSize: 13, color: subTitleColor, + letterSpacing: -0.2, ), overflow: TextOverflow.ellipsis, maxLines: 1, ), + ), ], - ), - ), - Padding( - padding: kBroadcastGridItemContentPadding.add( - const EdgeInsets.symmetric(vertical: 3.0), - ), - child: Text( - widget.broadcast.title, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: titleColor, - fontWeight: FontWeight.bold, - height: 1.0, - fontSize: 16, - ), + ], ), ), - if (widget.broadcast.tour.information.players != null) - Padding( - padding: kBroadcastGridItemContentPadding, - child: Text( - widget.broadcast.tour.information.players!, - style: TextStyle( - fontSize: 12, - color: subTitleColor, - letterSpacing: -0.2, - ), - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), - ), - ], + ), ), - ), - ], + ], + ), ), ), ); From 6b3bc8a32ccf0280937d9cf7967450447c36802d Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 13 Dec 2024 11:30:03 +0100 Subject: [PATCH 915/979] Fix lint --- lib/src/navigation.dart | 12 +++---- lib/src/view/engine/engine_depth.dart | 3 +- .../view/settings/settings_tab_screen.dart | 2 +- pubspec.lock | 36 +++++++++---------- 4 files changed, 27 insertions(+), 26 deletions(-) diff --git a/lib/src/navigation.dart b/lib/src/navigation.dart index b9239c44d7..be4c4b8581 100644 --- a/lib/src/navigation.dart +++ b/lib/src/navigation.dart @@ -420,20 +420,20 @@ class _TabSwitchingViewState extends State<_TabSwitchingView> { class _MaterialTabView extends ConsumerStatefulWidget { const _MaterialTabView({ - // ignore: unused_element + // ignore: unused_element_parameter super.key, required this.tab, this.builder, this.navigatorKey, - // ignore: unused_element + // ignore: unused_element_parameter this.routes, - // ignore: unused_element + // ignore: unused_element_parameter this.onGenerateRoute, - // ignore: unused_element + // ignore: unused_element_parameter this.onUnknownRoute, - // ignore: unused_element + // ignore: unused_element_parameter this.navigatorObservers = const [], - // ignore: unused_element + // ignore: unused_element_parameter this.restorationScopeId, }); diff --git a/lib/src/view/engine/engine_depth.dart b/lib/src/view/engine/engine_depth.dart index dc4dd5c654..83cec2197f 100644 --- a/lib/src/view/engine/engine_depth.dart +++ b/lib/src/view/engine/engine_depth.dart @@ -35,7 +35,8 @@ class EngineDepth extends ConsumerWidget { width: 240, backgroundColor: Theme.of(context).platform == TargetPlatform.android - ? Theme.of(context).dialogBackgroundColor + ? DialogTheme.of(context).backgroundColor ?? + Theme.of(context).colorScheme.surfaceContainerHigh : CupertinoDynamicColor.resolve( CupertinoColors.tertiarySystemBackground, context, diff --git a/lib/src/view/settings/settings_tab_screen.dart b/lib/src/view/settings/settings_tab_screen.dart index af1101463b..15d755668c 100644 --- a/lib/src/view/settings/settings_tab_screen.dart +++ b/lib/src/view/settings/settings_tab_screen.dart @@ -437,7 +437,7 @@ class _Body extends ConsumerWidget { } String _getSizeString(int? bytes) => - '${_bytesToMB(bytes ?? (0)).toStringAsFixed(2)}MB'; + '${_bytesToMB(bytes ?? 0).toStringAsFixed(2)}MB'; double _bytesToMB(int bytes) => bytes * 0.000001; diff --git a/pubspec.lock b/pubspec.lock index 1844c28a42..f388312315 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -74,10 +74,10 @@ packages: dependency: "direct main" description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.12.0" auto_size_text: dependency: "direct main" description: @@ -90,10 +90,10 @@ packages: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" build: dependency: transitive description: @@ -226,10 +226,10 @@ packages: dependency: "direct main" description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" code_builder: dependency: transitive description: @@ -418,10 +418,10 @@ packages: dependency: "direct dev" description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.2" fast_immutable_collections: dependency: "direct main" description: @@ -863,18 +863,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" + sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec url: "https://pub.dev" source: hosted - version: "10.0.7" + version: "10.0.8" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 url: "https://pub.dev" source: hosted - version: "3.0.8" + version: "3.0.9" leak_tracker_testing: dependency: transitive description: @@ -1031,10 +1031,10 @@ packages: dependency: "direct main" description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" path_parsing: dependency: transitive description: @@ -1453,10 +1453,10 @@ packages: dependency: transitive description: name: string_scanner - sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" + sha256: "0bd04f5bb74fcd6ff0606a888a30e917af9bd52820b178eaa464beb11dca84b6" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" synchronized: dependency: transitive description: @@ -1637,10 +1637,10 @@ packages: dependency: transitive description: name: vm_service - sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b + sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" url: "https://pub.dev" source: hosted - version: "14.3.0" + version: "14.3.1" wakelock_plus: dependency: "direct main" description: From c2f59b62f2fdc6ea136593aa7e2a9ebeac9b61ab Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Fri, 13 Dec 2024 11:33:40 +0100 Subject: [PATCH 916/979] Make players results accessible from broadcast game screen --- .../view/broadcast/broadcast_boards_tab.dart | 9 +- .../view/broadcast/broadcast_game_screen.dart | 143 ++++++++++-------- .../broadcast_player_results_screen.dart | 1 + .../broadcast/broadcast_round_screen.dart | 2 + 4 files changed, 94 insertions(+), 61 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_boards_tab.dart b/lib/src/view/broadcast/broadcast_boards_tab.dart index e5ab9e9153..7f0df075f7 100644 --- a/lib/src/view/broadcast/broadcast_boards_tab.dart +++ b/lib/src/view/broadcast/broadcast_boards_tab.dart @@ -25,10 +25,12 @@ const _kPlayerWidgetPadding = EdgeInsets.symmetric(vertical: 5.0); /// A tab that displays the live games of a broadcast round. class BroadcastBoardsTab extends ConsumerWidget { const BroadcastBoardsTab({ + required this.tournamentId, required this.roundId, required this.tournamentSlug, }); + final BroadcastTournamentId tournamentId; final BroadcastRoundId roundId; final String tournamentSlug; @@ -59,6 +61,7 @@ class BroadcastBoardsTab extends ConsumerWidget { ) : BroadcastPreview( games: value.games.values.toIList(), + tournamentId: tournamentId, roundId: roundId, title: value.round.name, tournamentSlug: tournamentSlug, @@ -81,6 +84,7 @@ class BroadcastBoardsTab extends ConsumerWidget { class BroadcastPreview extends StatelessWidget { const BroadcastPreview({ + required this.tournamentId, required this.roundId, required this.games, required this.title, @@ -89,12 +93,14 @@ class BroadcastPreview extends StatelessWidget { }); const BroadcastPreview.loading() - : roundId = const BroadcastRoundId(''), + : tournamentId = const BroadcastTournamentId(''), + roundId = const BroadcastRoundId(''), games = null, title = '', tournamentSlug = '', roundSlug = ''; + final BroadcastTournamentId tournamentId; final BroadcastRoundId roundId; final IList? games; final String title; @@ -148,6 +154,7 @@ class BroadcastPreview extends StatelessWidget { context, title: title, builder: (context) => BroadcastGameScreen( + tournamentId: tournamentId, roundId: roundId, gameId: game.id, tournamentSlug: tournamentSlug, diff --git a/lib/src/view/broadcast/broadcast_game_screen.dart b/lib/src/view/broadcast/broadcast_game_screen.dart index 2f8fb62a7b..d11bce316b 100644 --- a/lib/src/view/broadcast/broadcast_game_screen.dart +++ b/lib/src/view/broadcast/broadcast_game_screen.dart @@ -21,6 +21,7 @@ import 'package:lichess_mobile/src/view/analysis/analysis_layout.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_game_bottom_bar.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_game_settings.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_game_tree_view.dart'; +import 'package:lichess_mobile/src/view/broadcast/broadcast_player_results_screen.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_player_widget.dart'; import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; import 'package:lichess_mobile/src/view/engine/engine_lines.dart'; @@ -31,6 +32,7 @@ import 'package:lichess_mobile/src/widgets/pgn.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; class BroadcastGameScreen extends ConsumerStatefulWidget { + final BroadcastTournamentId tournamentId; final BroadcastRoundId roundId; final BroadcastGameId gameId; final String? tournamentSlug; @@ -38,6 +40,7 @@ class BroadcastGameScreen extends ConsumerStatefulWidget { final String? title; const BroadcastGameScreen({ + required this.tournamentId, required this.roundId, required this.gameId, this.tournamentSlug, @@ -117,6 +120,7 @@ class _BroadcastGameScreenState extends ConsumerState ), body: switch (broadcastGameState) { AsyncData() => _Body( + widget.tournamentId, widget.roundId, widget.gameId, widget.tournamentSlug, @@ -134,6 +138,7 @@ class _BroadcastGameScreenState extends ConsumerState class _Body extends ConsumerWidget { const _Body( + this.tournamentId, this.roundId, this.gameId, this.tournamentSlug, @@ -141,6 +146,7 @@ class _Body extends ConsumerWidget { required this.tabController, }); + final BroadcastTournamentId tournamentId; final BroadcastRoundId roundId; final BroadcastGameId gameId; final String? tournamentSlug; @@ -169,11 +175,13 @@ class _Body extends ConsumerWidget { borderRadius, ), boardHeader: _PlayerWidget( + tournamentId: tournamentId, roundId: roundId, gameId: gameId, widgetPosition: _PlayerWidgetPosition.top, ), boardFooter: _PlayerWidget( + tournamentId: tournamentId, roundId: roundId, gameId: gameId, widgetPosition: _PlayerWidgetPosition.bottom, @@ -361,11 +369,13 @@ enum _PlayerWidgetPosition { bottom, top } class _PlayerWidget extends ConsumerWidget { const _PlayerWidget({ + required this.tournamentId, required this.roundId, required this.gameId, required this.widgetPosition, }); + final BroadcastTournamentId tournamentId; final BroadcastRoundId roundId; final BroadcastGameId gameId; final _PlayerWidgetPosition widgetPosition; @@ -392,71 +402,84 @@ class _PlayerWidget extends ConsumerWidget { final player = game.players[side]!; final gameStatus = game.status; - return Container( - color: Theme.of(context).platform == TargetPlatform.iOS - ? Styles.cupertinoCardColor.resolveFrom(context) - : Theme.of(context).colorScheme.surfaceContainer, - padding: const EdgeInsets.only(left: 8.0), - child: Row( - children: [ - if (game.isOver) ...[ - Text( - (gameStatus == BroadcastResult.draw) - ? '½' - : (gameStatus == BroadcastResult.whiteWins) - ? side == Side.white - ? '1' - : '0' - : side == Side.black - ? '1' - : '0', - style: const TextStyle().copyWith(fontWeight: FontWeight.bold), - ), - const SizedBox(width: 16.0), - ], - Expanded( - child: BroadcastPlayerWidget( - federation: player.federation, - title: player.title, - name: player.name, - rating: player.rating, - textStyle: - const TextStyle().copyWith(fontWeight: FontWeight.bold), - ), + return GestureDetector( + onTap: () { + pushPlatformRoute( + context, + builder: (context) => BroadcastPlayerResultsScreen( + tournamentId, + (player.fideId != null) ? player.fideId!.toString() : player.name, + player.title, + player.name, ), - if (clock != null) - Container( - height: kAnalysisBoardHeaderOrFooterHeight, - color: (side == sideToMove) - ? isCursorOnLiveMove - ? Theme.of(context).colorScheme.tertiaryContainer - : Theme.of(context).colorScheme.secondaryContainer - : Colors.transparent, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 6.0), - child: Center( - child: isCursorOnLiveMove - ? CountdownClockBuilder( - timeLeft: clock, - active: side == sideToMove, - builder: (context, timeLeft) => _Clock( - timeLeft: timeLeft, + ); + }, + child: Container( + color: Theme.of(context).platform == TargetPlatform.iOS + ? Styles.cupertinoCardColor.resolveFrom(context) + : Theme.of(context).colorScheme.surfaceContainer, + padding: const EdgeInsets.only(left: 8.0), + child: Row( + children: [ + if (game.isOver) ...[ + Text( + (gameStatus == BroadcastResult.draw) + ? '½' + : (gameStatus == BroadcastResult.whiteWins) + ? side == Side.white + ? '1' + : '0' + : side == Side.black + ? '1' + : '0', + style: const TextStyle().copyWith(fontWeight: FontWeight.bold), + ), + const SizedBox(width: 16.0), + ], + Expanded( + child: BroadcastPlayerWidget( + federation: player.federation, + title: player.title, + name: player.name, + rating: player.rating, + textStyle: + const TextStyle().copyWith(fontWeight: FontWeight.bold), + ), + ), + if (clock != null) + Container( + height: kAnalysisBoardHeaderOrFooterHeight, + color: (side == sideToMove) + ? isCursorOnLiveMove + ? Theme.of(context).colorScheme.tertiaryContainer + : Theme.of(context).colorScheme.secondaryContainer + : Colors.transparent, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 6.0), + child: Center( + child: isCursorOnLiveMove + ? CountdownClockBuilder( + timeLeft: clock, + active: side == sideToMove, + builder: (context, timeLeft) => _Clock( + timeLeft: timeLeft, + isSideToMove: side == sideToMove, + isLive: true, + ), + tickInterval: const Duration(seconds: 1), + clockUpdatedAt: + side == sideToMove ? game.updatedClockAt : null, + ) + : _Clock( + timeLeft: clock, isSideToMove: side == sideToMove, - isLive: true, + isLive: false, ), - tickInterval: const Duration(seconds: 1), - clockUpdatedAt: - side == sideToMove ? game.updatedClockAt : null, - ) - : _Clock( - timeLeft: clock, - isSideToMove: side == sideToMove, - isLive: false, - ), + ), ), ), - ), - ], + ], + ), ), ); } diff --git a/lib/src/view/broadcast/broadcast_player_results_screen.dart b/lib/src/view/broadcast/broadcast_player_results_screen.dart index ab90cf1e46..6b494ee144 100644 --- a/lib/src/view/broadcast/broadcast_player_results_screen.dart +++ b/lib/src/view/broadcast/broadcast_player_results_screen.dart @@ -225,6 +225,7 @@ class _Body extends ConsumerWidget { pushPlatformRoute( context, builder: (context) => BroadcastGameScreen( + tournamentId: tournamentId, roundId: playerResult.roundId, gameId: playerResult.gameId, ), diff --git a/lib/src/view/broadcast/broadcast_round_screen.dart b/lib/src/view/broadcast/broadcast_round_screen.dart index 946b040ffd..b98465cf54 100644 --- a/lib/src/view/broadcast/broadcast_round_screen.dart +++ b/lib/src/view/broadcast/broadcast_round_screen.dart @@ -117,6 +117,7 @@ class _BroadcastRoundScreenState extends ConsumerState cupertinoTabSwitcher: tabSwitcher, sliver: switch (asyncTournament) { AsyncData(:final value) => BroadcastBoardsTab( + tournamentId: _selectedTournamentId, roundId: _selectedRoundId ?? value.defaultRoundId, tournamentSlug: widget.broadcast.tour.slug, ), @@ -186,6 +187,7 @@ class _BroadcastRoundScreenState extends ConsumerState _TabView( sliver: switch (asyncTournament) { AsyncData(:final value) => BroadcastBoardsTab( + tournamentId: _selectedTournamentId, roundId: _selectedRoundId ?? value.defaultRoundId, tournamentSlug: widget.broadcast.tour.slug, ), From 9a97a80551e816e8f739da7d84017eb4e3517894 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 13 Dec 2024 11:36:57 +0100 Subject: [PATCH 917/979] Upgrade dependencies --- ios/Podfile.lock | 4 +- .../xcshareddata/xcschemes/Runner.xcscheme | 1 + lib/l10n/l10n_af.dart | 2 +- lib/l10n/l10n_ar.dart | 2 +- lib/l10n/l10n_az.dart | 2 +- lib/l10n/l10n_be.dart | 2 +- lib/l10n/l10n_bg.dart | 2 +- lib/l10n/l10n_bn.dart | 2 +- lib/l10n/l10n_br.dart | 2 +- lib/l10n/l10n_bs.dart | 2 +- lib/l10n/l10n_ca.dart | 2 +- lib/l10n/l10n_cs.dart | 2 +- lib/l10n/l10n_da.dart | 2 +- lib/l10n/l10n_de.dart | 2 +- lib/l10n/l10n_el.dart | 2 +- lib/l10n/l10n_en.dart | 2 +- lib/l10n/l10n_eo.dart | 2 +- lib/l10n/l10n_es.dart | 2 +- lib/l10n/l10n_et.dart | 2 +- lib/l10n/l10n_eu.dart | 2 +- lib/l10n/l10n_fa.dart | 2 +- lib/l10n/l10n_fi.dart | 2 +- lib/l10n/l10n_fo.dart | 2 +- lib/l10n/l10n_fr.dart | 2 +- lib/l10n/l10n_ga.dart | 2 +- lib/l10n/l10n_gl.dart | 2 +- lib/l10n/l10n_gsw.dart | 2 +- lib/l10n/l10n_he.dart | 2 +- lib/l10n/l10n_hi.dart | 2 +- lib/l10n/l10n_hr.dart | 2 +- lib/l10n/l10n_hu.dart | 2 +- lib/l10n/l10n_hy.dart | 2 +- lib/l10n/l10n_id.dart | 2 +- lib/l10n/l10n_it.dart | 2 +- lib/l10n/l10n_ja.dart | 2 +- lib/l10n/l10n_kk.dart | 2 +- lib/l10n/l10n_ko.dart | 2 +- lib/l10n/l10n_lb.dart | 2 +- lib/l10n/l10n_lt.dart | 2 +- lib/l10n/l10n_lv.dart | 2 +- lib/l10n/l10n_mk.dart | 2 +- lib/l10n/l10n_nb.dart | 2 +- lib/l10n/l10n_nl.dart | 2 +- lib/l10n/l10n_nn.dart | 2 +- lib/l10n/l10n_pl.dart | 2 +- lib/l10n/l10n_pt.dart | 2 +- lib/l10n/l10n_ro.dart | 2 +- lib/l10n/l10n_ru.dart | 2 +- lib/l10n/l10n_sk.dart | 2 +- lib/l10n/l10n_sl.dart | 2 +- lib/l10n/l10n_sq.dart | 2 +- lib/l10n/l10n_sr.dart | 2 +- lib/l10n/l10n_sv.dart | 2 +- lib/l10n/l10n_tr.dart | 2 +- lib/l10n/l10n_uk.dart | 2 +- lib/l10n/l10n_vi.dart | 2 +- lib/l10n/l10n_zh.dart | 2 +- pubspec.lock | 52 +++++++++++-------- 58 files changed, 88 insertions(+), 79 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 43c1fe8af7..a98f1319e3 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -235,7 +235,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: app_settings: 3507c575c2b18a462c99948f61d5de21d4420999 AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73 - connectivity_plus: b21496ab28d1324eb59885d888a4d83b98531f01 + connectivity_plus: 2256d3e20624a7749ed21653aafe291a46446fee cupertino_http: 94ac07f5ff090b8effa6c5e2c47871d48ab7c86c device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe Firebase: cf1b19f21410b029b6786a54e9764a0cacad3c99 @@ -251,7 +251,7 @@ SPEC CHECKSUMS: FirebaseRemoteConfigInterop: e75e348953352a000331eb77caf01e424248e176 FirebaseSessions: 3f56f177d9e53a85021d16b31f9a111849d1dd8b Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - flutter_appauth: 240be31017bab05dc0ad10a863886d49eba477aa + flutter_appauth: 914057fda669db5073d3ca9d94ea932e7df3c964 flutter_local_notifications: 395056b3175ba4f08480a7c5de30cd36d69827e4 flutter_native_splash: 576fbd69b830a63594ae678396fa17e43abbc5f8 flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13 diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 5e31d3d342..c53e2b314e 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -48,6 +48,7 @@ ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" + enableGPUValidationMode = "1" allowLocationSimulation = "YES"> diff --git a/lib/l10n/l10n_af.dart b/lib/l10n/l10n_af.dart index cf973a2915..181bce4439 100644 --- a/lib/l10n/l10n_af.dart +++ b/lib/l10n/l10n_af.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint diff --git a/lib/l10n/l10n_ar.dart b/lib/l10n/l10n_ar.dart index 282165317c..bb47cee37c 100644 --- a/lib/l10n/l10n_ar.dart +++ b/lib/l10n/l10n_ar.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint diff --git a/lib/l10n/l10n_az.dart b/lib/l10n/l10n_az.dart index c3ae3a2155..ae1f41229e 100644 --- a/lib/l10n/l10n_az.dart +++ b/lib/l10n/l10n_az.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint diff --git a/lib/l10n/l10n_be.dart b/lib/l10n/l10n_be.dart index 17898397ce..8acbd28514 100644 --- a/lib/l10n/l10n_be.dart +++ b/lib/l10n/l10n_be.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint diff --git a/lib/l10n/l10n_bg.dart b/lib/l10n/l10n_bg.dart index 3b5edf1dcc..cae0aa16dc 100644 --- a/lib/l10n/l10n_bg.dart +++ b/lib/l10n/l10n_bg.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint diff --git a/lib/l10n/l10n_bn.dart b/lib/l10n/l10n_bn.dart index f86d89cfb1..a0f887efa9 100644 --- a/lib/l10n/l10n_bn.dart +++ b/lib/l10n/l10n_bn.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint diff --git a/lib/l10n/l10n_br.dart b/lib/l10n/l10n_br.dart index 6ea61db6fd..64bce249d3 100644 --- a/lib/l10n/l10n_br.dart +++ b/lib/l10n/l10n_br.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint diff --git a/lib/l10n/l10n_bs.dart b/lib/l10n/l10n_bs.dart index 8eb0a68c0a..9bd31aa929 100644 --- a/lib/l10n/l10n_bs.dart +++ b/lib/l10n/l10n_bs.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint diff --git a/lib/l10n/l10n_ca.dart b/lib/l10n/l10n_ca.dart index 4959fce700..0166ab1f8d 100644 --- a/lib/l10n/l10n_ca.dart +++ b/lib/l10n/l10n_ca.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint diff --git a/lib/l10n/l10n_cs.dart b/lib/l10n/l10n_cs.dart index 953aa04b93..b555e7e8d2 100644 --- a/lib/l10n/l10n_cs.dart +++ b/lib/l10n/l10n_cs.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint diff --git a/lib/l10n/l10n_da.dart b/lib/l10n/l10n_da.dart index 4c1a01e34f..3f1a2f13dc 100644 --- a/lib/l10n/l10n_da.dart +++ b/lib/l10n/l10n_da.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint diff --git a/lib/l10n/l10n_de.dart b/lib/l10n/l10n_de.dart index dc6551fb6d..e1749b95cf 100644 --- a/lib/l10n/l10n_de.dart +++ b/lib/l10n/l10n_de.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint diff --git a/lib/l10n/l10n_el.dart b/lib/l10n/l10n_el.dart index b95720f1dc..addade1faa 100644 --- a/lib/l10n/l10n_el.dart +++ b/lib/l10n/l10n_el.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint diff --git a/lib/l10n/l10n_en.dart b/lib/l10n/l10n_en.dart index fe34e1ef9d..3660156fb1 100644 --- a/lib/l10n/l10n_en.dart +++ b/lib/l10n/l10n_en.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint diff --git a/lib/l10n/l10n_eo.dart b/lib/l10n/l10n_eo.dart index d59d32157b..4dd1f7f82f 100644 --- a/lib/l10n/l10n_eo.dart +++ b/lib/l10n/l10n_eo.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint diff --git a/lib/l10n/l10n_es.dart b/lib/l10n/l10n_es.dart index 7f16383c64..7bef9b9f89 100644 --- a/lib/l10n/l10n_es.dart +++ b/lib/l10n/l10n_es.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint diff --git a/lib/l10n/l10n_et.dart b/lib/l10n/l10n_et.dart index 188bb10f2e..71beea3739 100644 --- a/lib/l10n/l10n_et.dart +++ b/lib/l10n/l10n_et.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint diff --git a/lib/l10n/l10n_eu.dart b/lib/l10n/l10n_eu.dart index baf1b5453b..9b89889e46 100644 --- a/lib/l10n/l10n_eu.dart +++ b/lib/l10n/l10n_eu.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint diff --git a/lib/l10n/l10n_fa.dart b/lib/l10n/l10n_fa.dart index b627c91041..f0da4ed520 100644 --- a/lib/l10n/l10n_fa.dart +++ b/lib/l10n/l10n_fa.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint diff --git a/lib/l10n/l10n_fi.dart b/lib/l10n/l10n_fi.dart index faa1f1e95c..39e99a72f0 100644 --- a/lib/l10n/l10n_fi.dart +++ b/lib/l10n/l10n_fi.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint diff --git a/lib/l10n/l10n_fo.dart b/lib/l10n/l10n_fo.dart index 91354d0b75..5f9e35c397 100644 --- a/lib/l10n/l10n_fo.dart +++ b/lib/l10n/l10n_fo.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint diff --git a/lib/l10n/l10n_fr.dart b/lib/l10n/l10n_fr.dart index 228c302998..11f5d9b755 100644 --- a/lib/l10n/l10n_fr.dart +++ b/lib/l10n/l10n_fr.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint diff --git a/lib/l10n/l10n_ga.dart b/lib/l10n/l10n_ga.dart index a0e67ed4bf..0ded134343 100644 --- a/lib/l10n/l10n_ga.dart +++ b/lib/l10n/l10n_ga.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint diff --git a/lib/l10n/l10n_gl.dart b/lib/l10n/l10n_gl.dart index 4617d10b8a..92e301ce96 100644 --- a/lib/l10n/l10n_gl.dart +++ b/lib/l10n/l10n_gl.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint diff --git a/lib/l10n/l10n_gsw.dart b/lib/l10n/l10n_gsw.dart index a1aeb7c6d8..0626a57a85 100644 --- a/lib/l10n/l10n_gsw.dart +++ b/lib/l10n/l10n_gsw.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint diff --git a/lib/l10n/l10n_he.dart b/lib/l10n/l10n_he.dart index fb6d7d98f9..2a19066bd6 100644 --- a/lib/l10n/l10n_he.dart +++ b/lib/l10n/l10n_he.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint diff --git a/lib/l10n/l10n_hi.dart b/lib/l10n/l10n_hi.dart index 509e3007bb..1089b85e5e 100644 --- a/lib/l10n/l10n_hi.dart +++ b/lib/l10n/l10n_hi.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint diff --git a/lib/l10n/l10n_hr.dart b/lib/l10n/l10n_hr.dart index 306ac9b54d..96aed2325a 100644 --- a/lib/l10n/l10n_hr.dart +++ b/lib/l10n/l10n_hr.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint diff --git a/lib/l10n/l10n_hu.dart b/lib/l10n/l10n_hu.dart index f18daabe27..3b63c2e273 100644 --- a/lib/l10n/l10n_hu.dart +++ b/lib/l10n/l10n_hu.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint diff --git a/lib/l10n/l10n_hy.dart b/lib/l10n/l10n_hy.dart index 828d8a92fd..911097d35c 100644 --- a/lib/l10n/l10n_hy.dart +++ b/lib/l10n/l10n_hy.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint diff --git a/lib/l10n/l10n_id.dart b/lib/l10n/l10n_id.dart index 2a90f49d5f..1bdb10a61c 100644 --- a/lib/l10n/l10n_id.dart +++ b/lib/l10n/l10n_id.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint diff --git a/lib/l10n/l10n_it.dart b/lib/l10n/l10n_it.dart index cec456b4ac..bc73454ef6 100644 --- a/lib/l10n/l10n_it.dart +++ b/lib/l10n/l10n_it.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint diff --git a/lib/l10n/l10n_ja.dart b/lib/l10n/l10n_ja.dart index e7d9800353..29619a0d4e 100644 --- a/lib/l10n/l10n_ja.dart +++ b/lib/l10n/l10n_ja.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint diff --git a/lib/l10n/l10n_kk.dart b/lib/l10n/l10n_kk.dart index dbc3125f02..727edf0c74 100644 --- a/lib/l10n/l10n_kk.dart +++ b/lib/l10n/l10n_kk.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint diff --git a/lib/l10n/l10n_ko.dart b/lib/l10n/l10n_ko.dart index edcaacb157..9c698c86b2 100644 --- a/lib/l10n/l10n_ko.dart +++ b/lib/l10n/l10n_ko.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint diff --git a/lib/l10n/l10n_lb.dart b/lib/l10n/l10n_lb.dart index a226306021..e37362b2d4 100644 --- a/lib/l10n/l10n_lb.dart +++ b/lib/l10n/l10n_lb.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint diff --git a/lib/l10n/l10n_lt.dart b/lib/l10n/l10n_lt.dart index f0e557895f..08d6821c40 100644 --- a/lib/l10n/l10n_lt.dart +++ b/lib/l10n/l10n_lt.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint diff --git a/lib/l10n/l10n_lv.dart b/lib/l10n/l10n_lv.dart index e2a9300ac3..f9d0d5a4e5 100644 --- a/lib/l10n/l10n_lv.dart +++ b/lib/l10n/l10n_lv.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint diff --git a/lib/l10n/l10n_mk.dart b/lib/l10n/l10n_mk.dart index 04034cc1dc..221885b319 100644 --- a/lib/l10n/l10n_mk.dart +++ b/lib/l10n/l10n_mk.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint diff --git a/lib/l10n/l10n_nb.dart b/lib/l10n/l10n_nb.dart index 4f4f77f6ea..8d013480ec 100644 --- a/lib/l10n/l10n_nb.dart +++ b/lib/l10n/l10n_nb.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint diff --git a/lib/l10n/l10n_nl.dart b/lib/l10n/l10n_nl.dart index a2945123fd..c4bb227c01 100644 --- a/lib/l10n/l10n_nl.dart +++ b/lib/l10n/l10n_nl.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint diff --git a/lib/l10n/l10n_nn.dart b/lib/l10n/l10n_nn.dart index ab60607f82..0c6607a137 100644 --- a/lib/l10n/l10n_nn.dart +++ b/lib/l10n/l10n_nn.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint diff --git a/lib/l10n/l10n_pl.dart b/lib/l10n/l10n_pl.dart index 8a925c1515..5c65084a47 100644 --- a/lib/l10n/l10n_pl.dart +++ b/lib/l10n/l10n_pl.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint diff --git a/lib/l10n/l10n_pt.dart b/lib/l10n/l10n_pt.dart index 457c60f3f5..9830a8e16f 100644 --- a/lib/l10n/l10n_pt.dart +++ b/lib/l10n/l10n_pt.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint diff --git a/lib/l10n/l10n_ro.dart b/lib/l10n/l10n_ro.dart index 89d5b08c53..718c012ba0 100644 --- a/lib/l10n/l10n_ro.dart +++ b/lib/l10n/l10n_ro.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint diff --git a/lib/l10n/l10n_ru.dart b/lib/l10n/l10n_ru.dart index 1aa28e3191..f2fa43cb33 100644 --- a/lib/l10n/l10n_ru.dart +++ b/lib/l10n/l10n_ru.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint diff --git a/lib/l10n/l10n_sk.dart b/lib/l10n/l10n_sk.dart index e2c919cdb0..583b32904e 100644 --- a/lib/l10n/l10n_sk.dart +++ b/lib/l10n/l10n_sk.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint diff --git a/lib/l10n/l10n_sl.dart b/lib/l10n/l10n_sl.dart index 1b1df04618..e15a054703 100644 --- a/lib/l10n/l10n_sl.dart +++ b/lib/l10n/l10n_sl.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint diff --git a/lib/l10n/l10n_sq.dart b/lib/l10n/l10n_sq.dart index dc19c47545..75a258658a 100644 --- a/lib/l10n/l10n_sq.dart +++ b/lib/l10n/l10n_sq.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint diff --git a/lib/l10n/l10n_sr.dart b/lib/l10n/l10n_sr.dart index a758a58517..8a62cce0e5 100644 --- a/lib/l10n/l10n_sr.dart +++ b/lib/l10n/l10n_sr.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint diff --git a/lib/l10n/l10n_sv.dart b/lib/l10n/l10n_sv.dart index d9b08e8ad6..a4556fe246 100644 --- a/lib/l10n/l10n_sv.dart +++ b/lib/l10n/l10n_sv.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint diff --git a/lib/l10n/l10n_tr.dart b/lib/l10n/l10n_tr.dart index c9cfdab80b..231c9e01af 100644 --- a/lib/l10n/l10n_tr.dart +++ b/lib/l10n/l10n_tr.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint diff --git a/lib/l10n/l10n_uk.dart b/lib/l10n/l10n_uk.dart index 6a1e4fa4a9..cc799d8f31 100644 --- a/lib/l10n/l10n_uk.dart +++ b/lib/l10n/l10n_uk.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint diff --git a/lib/l10n/l10n_vi.dart b/lib/l10n/l10n_vi.dart index deb1db3bc2..c9d167cbd3 100644 --- a/lib/l10n/l10n_vi.dart +++ b/lib/l10n/l10n_vi.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint diff --git a/lib/l10n/l10n_zh.dart b/lib/l10n/l10n_zh.dart index 8dc61905f6..8d15ade474 100644 --- a/lib/l10n/l10n_zh.dart +++ b/lib/l10n/l10n_zh.dart @@ -1,5 +1,5 @@ +// ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'l10n.dart'; // ignore_for_file: type=lint diff --git a/pubspec.lock b/pubspec.lock index f388312315..4ab4422b73 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -58,10 +58,10 @@ packages: dependency: transitive description: name: archive - sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d + sha256: "08064924cbf0ab88280a0c3f60db9dd24fec693927e725ecb176f16c629d1cb8" url: "https://pub.dev" source: hosted - version: "3.6.1" + version: "4.0.1" args: dependency: transitive description: @@ -250,10 +250,10 @@ packages: dependency: "direct main" description: name: connectivity_plus - sha256: "876849631b0c7dc20f8b471a2a03142841b482438e3b707955464f5ffca3e4c3" + sha256: e0817759ec6d2d8e57eb234e6e57d2173931367a865850c7acea40d4b4f9c27d url: "https://pub.dev" source: hosted - version: "6.1.0" + version: "6.1.1" connectivity_plus_platform_interface: dependency: transitive description: @@ -386,18 +386,18 @@ packages: dependency: "direct main" description: name: device_info_plus - sha256: f545ffbadee826f26f2e1a0f0cbd667ae9a6011cc0f77c0f8f00a969655e6e95 + sha256: "4fa68e53e26ab17b70ca39f072c285562cfc1589df5bb1e9295db90f6645f431" url: "https://pub.dev" source: hosted - version: "11.1.1" + version: "11.2.0" device_info_plus_platform_interface: dependency: transitive description: name: device_info_plus_platform_interface - sha256: "282d3cf731045a2feb66abfe61bbc40870ae50a3ed10a4d3d217556c35c8c2ba" + sha256: "0b04e02b30791224b31969eb1b50d723498f402971bff3630bca2ba839bd1ed2" url: "https://pub.dev" source: hosted - version: "7.0.1" + version: "7.0.2" dynamic_color: dependency: "direct main" description: @@ -535,10 +535,10 @@ packages: dependency: "direct main" description: name: flutter_appauth - sha256: "354a9df0254ccb64b5c4031a8b2fe376b1214fb3f957f768248f4437c289308f" + sha256: "0aa449d8991f70e7847d55b8bff0890fb41dc62c1d8526337e4073e806813bcb" url: "https://pub.dev" source: hosted - version: "8.0.2" + version: "8.0.3" flutter_appauth_platform_interface: dependency: transitive description: @@ -807,10 +807,10 @@ packages: dependency: "direct main" description: name: image - sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d + sha256: "599d08e369969bdf83138f5b4e0a7e823d3f992f23b8a64dd626877c37013533" url: "https://pub.dev" source: hosted - version: "4.3.0" + version: "4.4.0" intl: dependency: "direct main" description: @@ -1015,18 +1015,18 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: da8d9ac8c4b1df253d1a328b7bf01ae77ef132833479ab40763334db13b91cce + sha256: "70c421fe9d9cc1a9a7f3b05ae56befd469fe4f8daa3b484823141a55442d858d" url: "https://pub.dev" source: hosted - version: "8.1.1" + version: "8.1.2" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - sha256: ac1f4a4847f1ade8e6a87d1f39f5d7c67490738642e2542f559ec38c37489a66 + sha256: a5ef9986efc7bf772f2696183a3992615baa76c1ffb1189318dd8803778fb05b url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" path: dependency: "direct main" description: @@ -1131,6 +1131,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.1" + posix: + dependency: transitive + description: + name: posix + sha256: a0117dc2167805aa9125b82eee515cc891819bac2f538c83646d355b16f58b9a + url: "https://pub.dev" + source: hosted + version: "6.0.1" pub_semver: dependency: "direct main" description: @@ -1215,18 +1223,18 @@ packages: dependency: "direct main" description: name: share_plus - sha256: "9c9bafd4060728d7cdb2464c341743adbd79d327cb067ec7afb64583540b47c8" + sha256: "6327c3f233729374d0abaafd61f6846115b2a481b4feddd8534211dc10659400" url: "https://pub.dev" source: hosted - version: "10.1.2" + version: "10.1.3" share_plus_platform_interface: dependency: transitive description: name: share_plus_platform_interface - sha256: c57c0bbfec7142e3a0f55633be504b796af72e60e3c791b44d5a017b985f7a48 + sha256: cc012a23fc2d479854e6c80150696c4a5f5bb62cb89af4de1c505cf78d0a5d0b url: "https://pub.dev" source: hosted - version: "5.0.1" + version: "5.0.2" shared_preferences: dependency: "direct main" description: @@ -1501,10 +1509,10 @@ packages: dependency: transitive description: name: timing - sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.2" typed_data: dependency: transitive description: From 387c057951177a23f50441145ef476b35cfb6e57 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Fri, 13 Dec 2024 12:16:18 +0100 Subject: [PATCH 918/979] Fix null value error on broadcast game screen --- .../broadcast_player_results_screen.dart | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_player_results_screen.dart b/lib/src/view/broadcast/broadcast_player_results_screen.dart index 6b494ee144..ccd962576d 100644 --- a/lib/src/view/broadcast/broadcast_player_results_screen.dart +++ b/lib/src/view/broadcast/broadcast_player_results_screen.dart @@ -184,14 +184,15 @@ class _Body extends ConsumerWidget { mainAxisAlignment: MainAxisAlignment.center, spacing: cardSpacing, children: [ - SizedBox( - width: statWidth, - child: StatCard( - context.l10n.broadcastScore, - value: - '${player.score!.toStringAsFixed((player.score! == player.score!.roundToDouble()) ? 0 : 1)} / ${player.played}', + if (player.score != null) + SizedBox( + width: statWidth, + child: StatCard( + context.l10n.broadcastScore, + value: + '${player.score!.toStringAsFixed((player.score! == player.score!.roundToDouble()) ? 0 : 1)} / ${player.played}', + ), ), - ), if (player.performance != null) SizedBox( width: statWidth, From 1cb097bb6db686f87b62f9b701a354e71be24016 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Fri, 13 Dec 2024 12:16:39 +0100 Subject: [PATCH 919/979] Check if player information is loaded --- lib/src/view/broadcast/broadcast_game_screen.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/src/view/broadcast/broadcast_game_screen.dart b/lib/src/view/broadcast/broadcast_game_screen.dart index d11bce316b..2c6bac99b1 100644 --- a/lib/src/view/broadcast/broadcast_game_screen.dart +++ b/lib/src/view/broadcast/broadcast_game_screen.dart @@ -397,8 +397,11 @@ class _PlayerWidget extends ConsumerWidget { final game = ref.watch( broadcastRoundControllerProvider(roundId) - .select((round) => round.requireValue.games[gameId]!), + .select((round) => round.value?.games[gameId]), ); + + if (game == null) return const SizedBox.shrink(); + final player = game.players[side]!; final gameStatus = game.status; From 4e9ad4526df696aca9c38f9fd08807e3bf264cd9 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Fri, 13 Dec 2024 18:05:32 +0100 Subject: [PATCH 920/979] Use providers with selectAsync to get values from broadcast round controller --- .../view/broadcast/broadcast_game_screen.dart | 39 +++++++++---------- .../broadcast_game_screen_providers.dart | 30 ++++++++++++++ 2 files changed, 49 insertions(+), 20 deletions(-) create mode 100644 lib/src/view/broadcast/broadcast_game_screen_providers.dart diff --git a/lib/src/view/broadcast/broadcast_game_screen.dart b/lib/src/view/broadcast/broadcast_game_screen.dart index 00ff38a81a..4c721c2d9f 100644 --- a/lib/src/view/broadcast/broadcast_game_screen.dart +++ b/lib/src/view/broadcast/broadcast_game_screen.dart @@ -7,7 +7,6 @@ import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/analysis/analysis_preferences.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_game_controller.dart'; -import 'package:lichess_mobile/src/model/broadcast/broadcast_round_controller.dart'; import 'package:lichess_mobile/src/model/common/chess.dart'; import 'package:lichess_mobile/src/model/common/eval.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; @@ -19,6 +18,7 @@ import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/analysis/analysis_layout.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_game_bottom_bar.dart'; +import 'package:lichess_mobile/src/view/broadcast/broadcast_game_screen_providers.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_game_settings.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_game_tree_view.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_player_results_screen.dart'; @@ -82,17 +82,21 @@ class _BroadcastGameScreenState extends ConsumerState @override Widget build(BuildContext context) { - final broadcastGameState = ref - .watch(broadcastGameControllerProvider(widget.roundId, widget.gameId)); - final title = ref.watch( - broadcastRoundControllerProvider(widget.roundId) - .select((round) => round.value?.round.name), + final broadcastGameState = ref.watch( + broadcastGameProvider(widget.roundId, widget.gameId), ); + final broadcastGamePgn = ref + .watch(broadcastGameControllerProvider(widget.roundId, widget.gameId)); + final title = widget.title ?? + (switch (ref.watch(broadcastGameScreenTitleProvider(widget.roundId))) { + AsyncData(value: final title) => title, + _ => 'Broadcast Game', + }); return PlatformScaffold( appBar: PlatformAppBar( title: Text( - widget.title ?? title ?? 'BroadcastGame', + title, overflow: TextOverflow.ellipsis, maxLines: 1, ), @@ -102,7 +106,7 @@ class _BroadcastGameScreenState extends ConsumerState controller: _tabController, ), AppBarIconButton( - onPressed: (broadcastGameState.hasValue) + onPressed: (broadcastGamePgn.hasValue) ? () { pushPlatformRoute( context, @@ -118,8 +122,8 @@ class _BroadcastGameScreenState extends ConsumerState ), ], ), - body: switch (broadcastGameState) { - AsyncData() => _Body( + body: switch ((broadcastGameState, broadcastGamePgn)) { + (AsyncData(), AsyncData()) => _Body( widget.tournamentId, widget.roundId, widget.gameId, @@ -127,7 +131,10 @@ class _BroadcastGameScreenState extends ConsumerState widget.roundSlug, tabController: _tabController, ), - AsyncError(:final error) => Center( + (AsyncError(:final error), _) => Center( + child: Text('Cannot load broadcast game: $error'), + ), + (_, AsyncError(:final error)) => Center( child: Text('Cannot load broadcast game: $error'), ), _ => const Center(child: CircularProgressIndicator.adaptive()), @@ -385,15 +392,7 @@ class _PlayerWidget extends ConsumerWidget { final broadcastGameState = ref .watch(broadcastGameControllerProvider(roundId, gameId)) .requireValue; - // TODO - // we'll probably want to remove this and get the game state from a single controller - // this won't work with deep links for instance - final game = ref.watch( - broadcastRoundControllerProvider(roundId) - .select((round) => round.value?.games[gameId]), - ); - - if (game == null) return const SizedBox.shrink(); + final game = ref.watch(broadcastGameProvider(roundId, gameId)).requireValue; final isCursorOnLiveMove = broadcastGameState.currentPath == broadcastGameState.broadcastLivePath; diff --git a/lib/src/view/broadcast/broadcast_game_screen_providers.dart b/lib/src/view/broadcast/broadcast_game_screen_providers.dart new file mode 100644 index 0000000000..a78879d2ba --- /dev/null +++ b/lib/src/view/broadcast/broadcast_game_screen_providers.dart @@ -0,0 +1,30 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; +import 'package:lichess_mobile/src/model/broadcast/broadcast_round_controller.dart'; +import 'package:lichess_mobile/src/model/common/id.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'broadcast_game_screen_providers.g.dart'; + +@riverpod +Future broadcastGame( + Ref ref, + BroadcastRoundId roundId, + BroadcastGameId gameId, +) { + return ref.watch( + broadcastRoundControllerProvider(roundId) + .selectAsync((round) => round.games[gameId]!), + ); +} + +@riverpod +Future broadcastGameScreenTitle( + Ref ref, + BroadcastRoundId roundId, +) { + return ref.watch( + broadcastRoundControllerProvider(roundId) + .selectAsync((round) => round.round.name), + ); +} From 7c4098cd360bb3c3fa5bbdb76d4c85ab26502146 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Fri, 13 Dec 2024 18:21:19 +0100 Subject: [PATCH 921/979] Improve score sorting on broadcast tab players --- lib/src/view/broadcast/broadcast_players_tab.dart | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/src/view/broadcast/broadcast_players_tab.dart b/lib/src/view/broadcast/broadcast_players_tab.dart index 13585c8422..fb0255204d 100644 --- a/lib/src/view/broadcast/broadcast_players_tab.dart +++ b/lib/src/view/broadcast/broadcast_players_tab.dart @@ -87,7 +87,12 @@ class _PlayersListState extends ConsumerState { (BroadcastPlayerExtended a, BroadcastPlayerExtended b) { if (a.score == null) return 1; if (b.score == null) return -1; - return b.score!.compareTo(a.score!); + final value = b.score!.compareTo(a.score!); + if (value == 0) { + return a.played.compareTo(b.played); + } else { + return value; + } } }; From b623b3feb87e9052a815fcb3af46e73257f3cd27 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Sun, 15 Dec 2024 15:43:25 +0100 Subject: [PATCH 922/979] Remove useless Column widget --- .../broadcast_player_results_screen.dart | 502 +++++++++--------- 1 file changed, 243 insertions(+), 259 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_player_results_screen.dart b/lib/src/view/broadcast/broadcast_player_results_screen.dart index ccd962576d..d867b341c3 100644 --- a/lib/src/view/broadcast/broadcast_player_results_screen.dart +++ b/lib/src/view/broadcast/broadcast_player_results_screen.dart @@ -75,286 +75,270 @@ class _Body extends ConsumerWidget { 28.0, ); - return Column( - children: [ - Expanded( - child: ListView.builder( - itemCount: playerResults.games.length + 1, - itemBuilder: (context, index) { - if (index == 0) { - return Padding( - padding: Styles.bodyPadding, - child: Column( + return ListView.builder( + itemCount: playerResults.games.length + 1, + itemBuilder: (context, index) { + if (index == 0) { + return Padding( + padding: Styles.bodyPadding, + child: Column( + spacing: cardSpacing, + children: [ + if (fideData.ratings.standard != null && + fideData.ratings.rapid != null && + fideData.ratings.blitz != null) + Row( + mainAxisAlignment: MainAxisAlignment.center, spacing: cardSpacing, children: [ - if (fideData.ratings.standard != null && - fideData.ratings.rapid != null && - fideData.ratings.blitz != null) - Row( - mainAxisAlignment: MainAxisAlignment.center, - spacing: cardSpacing, - children: [ - if (fideData.ratings.standard != null) - SizedBox( - width: statWidth, - child: StatCard( - context.l10n.classical, - value: - fideData.ratings.standard.toString(), - ), - ), - if (fideData.ratings.rapid != null) - SizedBox( - width: statWidth, - child: StatCard( - context.l10n.rapid, - value: fideData.ratings.rapid.toString(), - ), - ), - if (fideData.ratings.blitz != null) - SizedBox( - width: statWidth, - child: StatCard( - context.l10n.blitz, - value: fideData.ratings.blitz.toString(), - ), - ), - ], + if (fideData.ratings.standard != null) + SizedBox( + width: statWidth, + child: StatCard( + context.l10n.classical, + value: fideData.ratings.standard.toString(), + ), ), - if (fideData.birthYear != null && - player.federation != null && - player.fideId != null) - Row( - mainAxisAlignment: MainAxisAlignment.center, - spacing: cardSpacing, - children: [ - if (fideData.birthYear != null) - SizedBox( - width: statWidth, - child: StatCard( - context.l10n.broadcastAgeThisYear, - value: (DateTime.now().year - - fideData.birthYear!) - .toString(), - ), - ), - if (player.federation != null) - SizedBox( - width: statWidth, - child: StatCard( - context.l10n.broadcastFederation, - child: Row( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - SvgPicture.network( - lichessFideFedSrc( - player.federation!, - ), - height: 12, - httpClient: - ref.read(defaultClientProvider), - ), - const SizedBox(width: 5), - Flexible( - child: Text( - federationIdToName[ - player.federation!]!, - style: const TextStyle( - fontSize: 18.0, - ), - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ), - ), - if (player.fideId != null) - SizedBox( - width: statWidth, - child: StatCard( - 'FIDE ID', - value: player.fideId!.toString(), - ), - ), - ], + if (fideData.ratings.rapid != null) + SizedBox( + width: statWidth, + child: StatCard( + context.l10n.rapid, + value: fideData.ratings.rapid.toString(), + ), ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - spacing: cardSpacing, - children: [ - if (player.score != null) - SizedBox( - width: statWidth, - child: StatCard( - context.l10n.broadcastScore, - value: - '${player.score!.toStringAsFixed((player.score! == player.score!.roundToDouble()) ? 0 : 1)} / ${player.played}', - ), - ), - if (player.performance != null) - SizedBox( - width: statWidth, - child: StatCard( - context.l10n.performance, - value: player.performance.toString(), - ), - ), - if (player.ratingDiff != null) - SizedBox( - width: statWidth, - child: StatCard( - context.l10n.broadcastRatingDiff, - child: ProgressionWidget( - player.ratingDiff!, - fontSize: 18.0, - ), - ), - ), - ], - ), - ], - ), - ); - } - - final playerResult = playerResults.games[index - 1]; - - return GestureDetector( - onTap: () { - pushPlatformRoute( - context, - builder: (context) => BroadcastGameScreen( - tournamentId: tournamentId, - roundId: playerResult.roundId, - gameId: playerResult.gameId, - ), - ); - }, - child: ColoredBox( - color: Theme.of(context).platform == TargetPlatform.iOS - ? index.isEven - ? CupertinoColors.secondarySystemBackground - .resolveFrom(context) - : CupertinoColors.tertiarySystemBackground - .resolveFrom(context) - : index.isEven - ? Theme.of(context) - .colorScheme - .surfaceContainerLow - : Theme.of(context) - .colorScheme - .surfaceContainerHigh, - child: Padding( - padding: _kTableRowPadding, - child: Row( - children: [ + if (fideData.ratings.blitz != null) SizedBox( - width: indexWidth, - child: Center( - child: Text( - index.toString(), - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - ), + width: statWidth, + child: StatCard( + context.l10n.blitz, + value: fideData.ratings.blitz.toString(), ), ), - Expanded( - flex: 5, - child: BroadcastPlayerWidget( - federation: playerResult.opponent.federation, - title: playerResult.opponent.title, - name: playerResult.opponent.name, + ], + ), + if (fideData.birthYear != null && + player.federation != null && + player.fideId != null) + Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: cardSpacing, + children: [ + if (fideData.birthYear != null) + SizedBox( + width: statWidth, + child: StatCard( + context.l10n.broadcastAgeThisYear, + value: + (DateTime.now().year - fideData.birthYear!) + .toString(), ), ), - Expanded( - flex: 3, - child: (playerResult.opponent.rating != null) - ? Center( + if (player.federation != null) + SizedBox( + width: statWidth, + child: StatCard( + context.l10n.broadcastFederation, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.network( + lichessFideFedSrc( + player.federation!, + ), + height: 12, + httpClient: + ref.read(defaultClientProvider), + ), + const SizedBox(width: 5), + Flexible( child: Text( - playerResult.opponent.rating.toString(), + federationIdToName[player.federation!]!, + style: const TextStyle( + fontSize: 18.0, + ), + overflow: TextOverflow.ellipsis, ), - ) - : const SizedBox.shrink(), - ), - SizedBox( - width: 30, - child: Center( - child: Container( - width: 15, - height: 15, - decoration: BoxDecoration( - border: (Theme.of(context).brightness == - Brightness.light && - playerResult.color == - Side.white || - Theme.of(context).brightness == - Brightness.dark && - playerResult.color == - Side.black) - ? Border.all( - width: 2.0, - color: Theme.of(context) - .colorScheme - .outline, - ) - : null, - shape: BoxShape.circle, - color: switch (playerResult.color) { - Side.white => - Colors.white.withValues(alpha: 0.9), - Side.black => - Colors.black.withValues(alpha: 0.9), - }, - ), + ), + ], ), ), ), + if (player.fideId != null) SizedBox( - width: 30, - child: Center( - child: Text( - switch (playerResult.points) { - BroadcastPoints.one => '1', - BroadcastPoints.half => '½', - BroadcastPoints.zero => '0', - _ => '*' - }, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: switch (playerResult.points) { - BroadcastPoints.one => - context.lichessColors.good, - BroadcastPoints.zero => - context.lichessColors.error, - _ => null - }, - ), - ), + width: statWidth, + child: StatCard( + 'FIDE ID', + value: player.fideId!.toString(), ), ), - if (showRatingDiff) - SizedBox( - width: 38, - child: (playerResult.ratingDiff != null) - ? ProgressionWidget( - playerResult.ratingDiff!, - fontSize: 14, - ) - : null, + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: cardSpacing, + children: [ + if (player.score != null) + SizedBox( + width: statWidth, + child: StatCard( + context.l10n.broadcastScore, + value: + '${player.score!.toStringAsFixed((player.score! == player.score!.roundToDouble()) ? 0 : 1)} / ${player.played}', + ), + ), + if (player.performance != null) + SizedBox( + width: statWidth, + child: StatCard( + context.l10n.performance, + value: player.performance.toString(), + ), + ), + if (player.ratingDiff != null) + SizedBox( + width: statWidth, + child: StatCard( + context.l10n.broadcastRatingDiff, + child: ProgressionWidget( + player.ratingDiff!, + fontSize: 18.0, ), - ], + ), + ), + ], + ), + ], + ), + ); + } + + final playerResult = playerResults.games[index - 1]; + + return GestureDetector( + onTap: () { + pushPlatformRoute( + context, + builder: (context) => BroadcastGameScreen( + tournamentId: tournamentId, + roundId: playerResult.roundId, + gameId: playerResult.gameId, + ), + ); + }, + child: ColoredBox( + color: Theme.of(context).platform == TargetPlatform.iOS + ? index.isEven + ? CupertinoColors.secondarySystemBackground + .resolveFrom(context) + : CupertinoColors.tertiarySystemBackground + .resolveFrom(context) + : index.isEven + ? Theme.of(context).colorScheme.surfaceContainerLow + : Theme.of(context).colorScheme.surfaceContainerHigh, + child: Padding( + padding: _kTableRowPadding, + child: Row( + children: [ + SizedBox( + width: indexWidth, + child: Center( + child: Text( + index.toString(), + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), ), ), - ), - ); - }, + Expanded( + flex: 5, + child: BroadcastPlayerWidget( + federation: playerResult.opponent.federation, + title: playerResult.opponent.title, + name: playerResult.opponent.name, + ), + ), + Expanded( + flex: 3, + child: (playerResult.opponent.rating != null) + ? Center( + child: Text( + playerResult.opponent.rating.toString(), + ), + ) + : const SizedBox.shrink(), + ), + SizedBox( + width: 30, + child: Center( + child: Container( + width: 15, + height: 15, + decoration: BoxDecoration( + border: (Theme.of(context).brightness == + Brightness.light && + playerResult.color == Side.white || + Theme.of(context).brightness == + Brightness.dark && + playerResult.color == Side.black) + ? Border.all( + width: 2.0, + color: + Theme.of(context).colorScheme.outline, + ) + : null, + shape: BoxShape.circle, + color: switch (playerResult.color) { + Side.white => + Colors.white.withValues(alpha: 0.9), + Side.black => + Colors.black.withValues(alpha: 0.9), + }, + ), + ), + ), + ), + SizedBox( + width: 30, + child: Center( + child: Text( + switch (playerResult.points) { + BroadcastPoints.one => '1', + BroadcastPoints.half => '½', + BroadcastPoints.zero => '0', + _ => '*' + }, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: switch (playerResult.points) { + BroadcastPoints.one => + context.lichessColors.good, + BroadcastPoints.zero => + context.lichessColors.error, + _ => null + }, + ), + ), + ), + ), + if (showRatingDiff) + SizedBox( + width: 38, + child: (playerResult.ratingDiff != null) + ? ProgressionWidget( + playerResult.ratingDiff!, + fontSize: 14, + ) + : null, + ), + ], + ), + ), ), - ), - ], + ); + }, ); case AsyncError(:final error): return Center(child: Text('Cannot load player data: $error')); From f4e44ba182e6c406e509af897ae515886b6a5d93 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Sun, 15 Dec 2024 10:17:08 +0100 Subject: [PATCH 923/979] Revert "Improve broadcast list screen" This reverts commit 56835122d06db405f7e129631594f0d4f01f8b74. --- lib/src/utils/image.dart | 3 - .../view/broadcast/broadcast_list_screen.dart | 275 ++++++++---------- 2 files changed, 125 insertions(+), 153 deletions(-) diff --git a/lib/src/utils/image.dart b/lib/src/utils/image.dart index 15802e13e4..15c3447e1b 100644 --- a/lib/src/utils/image.dart +++ b/lib/src/utils/image.dart @@ -128,9 +128,6 @@ class ImageColorWorker { filter: false, ); final Hct sourceColor = Hct.fromInt(scoredResults.first); - if (sourceColor.tone > 90) { - sourceColor.tone = 90; - } final scheme = SchemeFidelity( sourceColorHct: sourceColor, isDark: false, diff --git a/lib/src/view/broadcast/broadcast_list_screen.dart b/lib/src/view/broadcast/broadcast_list_screen.dart index 299ebc8beb..c6602c0775 100644 --- a/lib/src/view/broadcast/broadcast_list_screen.dart +++ b/lib/src/view/broadcast/broadcast_list_screen.dart @@ -22,8 +22,7 @@ import 'package:lichess_mobile/src/widgets/shimmer.dart'; const kDefaultBroadcastImage = AssetImage('assets/images/broadcast_image.png'); const kBroadcastGridItemBorderRadius = BorderRadius.all(Radius.circular(16.0)); -const kBroadcastGridItemContentPadding = - EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0); +const kBroadcastGridItemContentPadding = EdgeInsets.symmetric(horizontal: 16.0); /// A screen that displays a paginated list of broadcasts. class BroadcastListScreen extends StatelessWidget { @@ -122,12 +121,9 @@ class _BodyState extends ConsumerState<_Body> { ); return const Center(child: Text('Could not load broadcast tournaments')); } - final screenWidth = MediaQuery.sizeOf(context).width; - final itemsByRow = screenWidth >= 1200 - ? 3 - : screenWidth >= 800 - ? 2 - : 1; + + final isTablet = isTabletOrLarger(context); + final itemsByRow = isTablet ? 2 : 1; const loadingItems = 12; final itemsCount = broadcasts.requireValue.past.length + (broadcasts.isLoading ? loadingItems : 0); @@ -136,7 +132,7 @@ class _BodyState extends ConsumerState<_Body> { crossAxisCount: itemsByRow, crossAxisSpacing: 16.0, mainAxisSpacing: 16.0, - childAspectRatio: 1.4, + childAspectRatio: 1.45, ); final sections = [ @@ -345,10 +341,9 @@ class _BroadcastGridItemState extends State { _cardColors?.primaryContainer ?? defaultBackgroundColor; final titleColor = _cardColors?.onPrimaryContainer; final subTitleColor = - _cardColors?.onPrimaryContainer.withValues(alpha: 0.8) ?? - textShade(context, 0.8); + _cardColors?.onPrimaryContainer.withValues(alpha: 0.7) ?? + textShade(context, 0.7); final liveColor = _cardColors?.error ?? LichessColors.red; - final isTablet = isTabletOrLarger(context); return GestureDetector( onTap: () { @@ -363,159 +358,139 @@ class _BroadcastGridItemState extends State { onTapDown: (_) => _onTapDown(), onTapCancel: _onTapCancel, onTapUp: (_) => _onTapCancel(), - child: AnimatedOpacity( - opacity: _tapDown ? 1.0 : 0.9, - duration: const Duration(milliseconds: 100), - child: Container( - clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - borderRadius: kBroadcastGridItemBorderRadius, - boxShadow: Theme.of(context).platform == TargetPlatform.iOS - ? null - : kElevationToShadow[1], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - AspectRatio( - aspectRatio: 2.0, - child: _imageProvider != null - ? Image( - image: _imageProvider!, - frameBuilder: ( - context, - child, - frame, - wasSynchronouslyLoaded, - ) { - if (wasSynchronouslyLoaded) { - return child; - } - return AnimatedOpacity( - duration: const Duration(milliseconds: 500), - opacity: frame == null ? 0 : 1, - child: child, - ); - }, - errorBuilder: (context, error, stackTrace) => - const Image(image: kDefaultBroadcastImage), - ) - : const SizedBox.shrink(), + child: AnimatedContainer( + duration: const Duration(milliseconds: 500), + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + borderRadius: kBroadcastGridItemBorderRadius, + color: backgroundColor, + boxShadow: Theme.of(context).platform == TargetPlatform.iOS + ? null + : kElevationToShadow[1], + ), + child: Stack( + children: [ + ShaderMask( + blendMode: BlendMode.dstOut, + shaderCallback: (bounds) { + return LinearGradient( + begin: const Alignment(0.0, 0.5), + end: Alignment.bottomCenter, + colors: [ + backgroundColor.withValues(alpha: 0.0), + backgroundColor.withValues(alpha: 1.0), + ], + stops: const [0.5, 1.10], + tileMode: TileMode.clamp, + ).createShader(bounds); + }, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 100), + opacity: _tapDown ? 1.0 : 0.7, + child: AspectRatio( + aspectRatio: 2.0, + child: _imageProvider != null + ? Image( + image: _imageProvider!, + frameBuilder: + (context, child, frame, wasSynchronouslyLoaded) { + if (wasSynchronouslyLoaded) { + return child; + } + return AnimatedOpacity( + duration: const Duration(milliseconds: 500), + opacity: frame == null ? 0 : 1, + child: child, + ); + }, + errorBuilder: (context, error, stackTrace) => + const Image(image: kDefaultBroadcastImage), + ) + : const SizedBox.shrink(), + ), ), - Expanded( - child: ColoredBox( - color: backgroundColor, - child: Padding( - padding: kBroadcastGridItemContentPadding, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (widget.broadcast.round.startsAt != null) ...[ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - widget.broadcast.round.name, - style: TextStyle( - fontSize: 13, - color: subTitleColor, - ), - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), - const SizedBox(width: 6.0), - Text( - relativeDate( - widget.broadcast.round.startsAt!, - ), - style: TextStyle( - fontSize: 13, - color: subTitleColor, - ), - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), - const Spacer(), - if (widget.broadcast.isLive) - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.circle, - size: 16, - color: liveColor, - shadows: const [ - Shadow( - color: Colors.black54, - offset: Offset(0, 1), - blurRadius: 2, - ), - ], - ), - const SizedBox(width: 4.0), - Text( - 'LIVE', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - color: liveColor, - shadows: const [ - Shadow( - color: Colors.black54, - offset: Offset(0, 1), - blurRadius: 2, - ), - ], - ), - overflow: TextOverflow.ellipsis, - ), - ], - ), - ], - ), - const SizedBox(height: 2.0), - ], + ), + Positioned( + left: 0, + right: 0, + bottom: 12.0, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (widget.broadcast.round.startsAt != null || + widget.broadcast.isLive) + Padding( + padding: kBroadcastGridItemContentPadding, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + widget.broadcast.round.name, + style: TextStyle( + fontSize: 12, + color: subTitleColor, + ), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + const SizedBox(width: 4.0), + if (widget.broadcast.isLive) Text( - widget.broadcast.title, - maxLines: 2, - overflow: TextOverflow.ellipsis, + 'LIVE', style: TextStyle( - color: titleColor, + fontSize: 15, fontWeight: FontWeight.bold, - height: 1.0, - fontSize: 16, + color: liveColor, ), - ), - ], - ), - if (widget.broadcast.tour.information.players != - null) ...[ - SizedBox(height: isTablet ? 0.0 : 8.0), - Flexible( - child: Text( - widget.broadcast.tour.information.players!, + overflow: TextOverflow.ellipsis, + ) + else + Text( + relativeDate(widget.broadcast.round.startsAt!), style: TextStyle( - fontSize: 13, + fontSize: 12, color: subTitleColor, - letterSpacing: -0.2, ), overflow: TextOverflow.ellipsis, maxLines: 1, ), - ), ], - ], + ), + ), + Padding( + padding: kBroadcastGridItemContentPadding.add( + const EdgeInsets.symmetric(vertical: 3.0), + ), + child: Text( + widget.broadcast.title, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: titleColor, + fontWeight: FontWeight.bold, + height: 1.0, + fontSize: 16, + ), ), ), - ), + if (widget.broadcast.tour.information.players != null) + Padding( + padding: kBroadcastGridItemContentPadding, + child: Text( + widget.broadcast.tour.information.players!, + style: TextStyle( + fontSize: 12, + color: subTitleColor, + letterSpacing: -0.2, + ), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + ], ), - ], - ), + ), + ], ), ), ); From a17836ac2ad4325d8097bcdc62dbf259df521285 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Sun, 15 Dec 2024 13:35:54 +0100 Subject: [PATCH 924/979] Sort and display broadcast by tier --- lib/src/model/broadcast/broadcast.dart | 2 + .../model/broadcast/broadcast_repository.dart | 1 + lib/src/utils/image.dart | 33 +- .../view/broadcast/broadcast_list_screen.dart | 432 +++++++++++------- lib/src/view/watch/watch_tab_screen.dart | 2 +- 5 files changed, 289 insertions(+), 181 deletions(-) diff --git a/lib/src/model/broadcast/broadcast.dart b/lib/src/model/broadcast/broadcast.dart index fff18f2d78..38a82ff9fd 100644 --- a/lib/src/model/broadcast/broadcast.dart +++ b/lib/src/model/broadcast/broadcast.dart @@ -50,6 +50,8 @@ class BroadcastTournamentData with _$BroadcastTournamentData { required String name, required String? imageUrl, required String? description, + // PRIVATE=-1, NORMAL=3, HIGH=4, BEST=5 + int? tier, required BroadcastTournamentInformation information, }) = _BroadcastTournamentData; } diff --git a/lib/src/model/broadcast/broadcast_repository.dart b/lib/src/model/broadcast/broadcast_repository.dart index 1a6c49a13c..583d08ac72 100644 --- a/lib/src/model/broadcast/broadcast_repository.dart +++ b/lib/src/model/broadcast/broadcast_repository.dart @@ -89,6 +89,7 @@ BroadcastTournamentData _tournamentDataFromPick( BroadcastTournamentData( id: pick('id').asBroadcastTournamentIdOrThrow(), name: pick('name').asStringOrThrow(), + tier: pick('tier').asIntOrNull(), imageUrl: pick('image').asStringOrNull(), description: pick('description').asStringOrNull(), information: ( diff --git a/lib/src/utils/image.dart b/lib/src/utils/image.dart index 15c3447e1b..a11c152c40 100644 --- a/lib/src/utils/image.dart +++ b/lib/src/utils/image.dart @@ -10,7 +10,6 @@ typedef ImageColors = ({ Uint8List? image, int primaryContainer, int onPrimaryContainer, - int error, }); /// A worker that quantizes an image and returns a minimal color scheme associated @@ -114,30 +113,46 @@ class ImageColorWorker { resized.buffer.asUint32List(), 32, ); - // debugPrint( + // print( // 'Decoding and quantization took: ${stopwatch0.elapsedMilliseconds}ms', // ); final Map colorToCount = quantizerResult.colorToCount.map( (int key, int value) => MapEntry(_getArgbFromAbgr(key), value), ); - final List scoredResults = Score.score( + final significantColors = Map.from(colorToCount) + ..removeWhere((key, value) => value < 10); + final meanTone = colorToCount.entries.fold( + 0, + (double previousValue, MapEntry element) => + previousValue + Hct.fromInt(element.key).tone * element.value, + ) / + colorToCount.values.fold( + 0, + (int previousValue, int element) => previousValue + element, + ); + + final int scoredResult = Score.score( colorToCount, desired: 1, - fallbackColorARGB: 0xFF000000, + fallbackColorARGB: 0xFFFFFFFF, filter: false, - ); - final Hct sourceColor = Hct.fromInt(scoredResults.first); - final scheme = SchemeFidelity( + ).first; + final Hct sourceColor = Hct.fromInt(scoredResult); + if ((meanTone - sourceColor.tone).abs() > 20) { + sourceColor.tone = meanTone; + } + final scheme = (significantColors.length <= 10 + ? SchemeMonochrome.new + : SchemeFidelity.new)( sourceColorHct: sourceColor, - isDark: false, + isDark: sourceColor.tone < 50, contrastLevel: 0.0, ); final result = ( image: image == null ? bytes : null, primaryContainer: scheme.primaryContainer, onPrimaryContainer: scheme.onPrimaryContainer, - error: scheme.error, ); sendPort.send((id, result)); } catch (e) { diff --git a/lib/src/view/broadcast/broadcast_list_screen.dart b/lib/src/view/broadcast/broadcast_list_screen.dart index c6602c0775..6dd3e8ceca 100644 --- a/lib/src/view/broadcast/broadcast_list_screen.dart +++ b/lib/src/view/broadcast/broadcast_list_screen.dart @@ -15,14 +15,13 @@ import 'package:lichess_mobile/src/utils/image.dart'; import 'package:lichess_mobile/src/utils/l10n.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; -import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_round_screen.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; const kDefaultBroadcastImage = AssetImage('assets/images/broadcast_image.png'); const kBroadcastGridItemBorderRadius = BorderRadius.all(Radius.circular(16.0)); -const kBroadcastGridItemContentPadding = EdgeInsets.symmetric(horizontal: 16.0); +const kBroadcastGridItemContentPadding = EdgeInsets.symmetric(horizontal: 12.0); /// A screen that displays a paginated list of broadcasts. class BroadcastListScreen extends StatelessWidget { @@ -122,91 +121,150 @@ class _BodyState extends ConsumerState<_Body> { return const Center(child: Text('Could not load broadcast tournaments')); } - final isTablet = isTabletOrLarger(context); - final itemsByRow = isTablet ? 2 : 1; + final screenWidth = MediaQuery.sizeOf(context).width; + final itemsByRow = screenWidth >= 1200 + ? 3 + : screenWidth >= 700 + ? 2 + : 1; const loadingItems = 12; - final itemsCount = broadcasts.requireValue.past.length + + final pastItemsCount = broadcasts.requireValue.past.length + (broadcasts.isLoading ? loadingItems : 0); - final gridDelegate = SliverGridDelegateWithFixedCrossAxisCount( + final highTierGridDelegate = SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: itemsByRow, crossAxisSpacing: 16.0, mainAxisSpacing: 16.0, childAspectRatio: 1.45, ); + final lowTierGridDelegate = SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: itemsByRow + 1, + crossAxisSpacing: 16.0, + mainAxisSpacing: 16.0, + childAspectRatio: screenWidth >= 1200 + ? 1.4 + : screenWidth >= 700 + ? 1.3 + : 1.0, + ); + final sections = [ - (context.l10n.broadcastOngoing, broadcasts.value!.active), - (context.l10n.broadcastCompleted, broadcasts.value!.past), + ('ongoing', context.l10n.broadcastOngoing, broadcasts.value!.active), + ('past', context.l10n.broadcastCompleted, broadcasts.value!.past), ]; + final activeHighTier = broadcasts.value!.active + .where( + (broadcast) => + broadcast.tour.tier != null && broadcast.tour.tier! >= 4, + ) + .toList(); + + final activeLowTier = broadcasts.value!.active + .where( + (broadcast) => + broadcast.tour.tier == null || broadcast.tour.tier! < 4, + ) + .toList(); + return RefreshIndicator.adaptive( edgeOffset: Theme.of(context).platform == TargetPlatform.iOS ? MediaQuery.paddingOf(context).top + 16.0 : 0, key: _refreshIndicatorKey, onRefresh: () async => ref.refresh(broadcastsPaginatorProvider), - child: CustomScrollView( - controller: _scrollController, - slivers: [ - for (final section in sections) - SliverMainAxisGroup( - key: ValueKey(section), - slivers: [ - if (Theme.of(context).platform == TargetPlatform.iOS) - CupertinoSliverNavigationBar( - automaticallyImplyLeading: false, - leading: null, - largeTitle: AutoSizeText( - section.$1, - maxLines: 1, - minFontSize: 14, - overflow: TextOverflow.ellipsis, - ), - transitionBetweenRoutes: false, - ) - else - SliverAppBar( - automaticallyImplyLeading: false, - title: AutoSizeText( - section.$1, - maxLines: 1, - minFontSize: 14, - overflow: TextOverflow.ellipsis, + child: Shimmer( + child: CustomScrollView( + controller: _scrollController, + slivers: [ + for (final section in sections) + SliverMainAxisGroup( + key: ValueKey(section), + slivers: [ + if (Theme.of(context).platform == TargetPlatform.iOS) + CupertinoSliverNavigationBar( + automaticallyImplyLeading: false, + leading: null, + largeTitle: AutoSizeText( + section.$2, + maxLines: 1, + minFontSize: 14, + overflow: TextOverflow.ellipsis, + ), + transitionBetweenRoutes: false, + ) + else + SliverAppBar( + automaticallyImplyLeading: false, + title: AutoSizeText( + section.$2, + maxLines: 1, + minFontSize: 14, + overflow: TextOverflow.ellipsis, + ), + pinned: true, ), - pinned: true, - ), - SliverPadding( - padding: Theme.of(context).platform == TargetPlatform.iOS - ? Styles.horizontalBodyPadding - : Styles.bodySectionPadding, - sliver: SliverGrid.builder( - gridDelegate: gridDelegate, - itemBuilder: (context, index) => (broadcasts.isLoading && - index >= itemsCount - loadingItems) - ? Shimmer( - child: ShimmerLoading( - isLoading: true, - child: BroadcastGridItem.loading(_worker!), - ), - ) - : BroadcastGridItem( + if (section.$1 == 'ongoing') ...[ + if (activeHighTier.isNotEmpty) + SliverPadding( + padding: + Theme.of(context).platform == TargetPlatform.iOS + ? Styles.horizontalBodyPadding + : Styles.bodySectionPadding, + sliver: SliverGrid.builder( + gridDelegate: highTierGridDelegate, + itemBuilder: (context, index) => BroadcastCard( worker: _worker!, - broadcast: section.$2[index], + broadcast: activeHighTier[index], ), - itemCount: section.$2.length, - ), - ), - ], - ), - ], + itemCount: activeHighTier.length, + ), + ), + if (activeLowTier.isNotEmpty) + SliverPadding( + padding: Styles.bodySectionPadding, + sliver: SliverGrid.builder( + gridDelegate: lowTierGridDelegate, + itemBuilder: (context, index) => BroadcastCard( + worker: _worker!, + broadcast: activeLowTier[index], + ), + itemCount: activeLowTier.length, + ), + ), + ] else + SliverPadding( + padding: Theme.of(context).platform == TargetPlatform.iOS + ? Styles.horizontalBodyPadding + : Styles.bodySectionPadding, + sliver: SliverGrid.builder( + gridDelegate: lowTierGridDelegate, + itemBuilder: (context, index) => + (broadcasts.isLoading && + index >= pastItemsCount - loadingItems) + ? ShimmerLoading( + isLoading: true, + child: BroadcastCard.loading(_worker!), + ) + : BroadcastCard( + worker: _worker!, + broadcast: section.$3[index], + ), + itemCount: section.$3.length, + ), + ), + ], + ), + ], + ), ), ); } } -class BroadcastGridItem extends StatefulWidget { - const BroadcastGridItem({ +class BroadcastCard extends StatefulWidget { + const BroadcastCard({ required this.broadcast, required this.worker, super.key, @@ -215,7 +273,7 @@ class BroadcastGridItem extends StatefulWidget { final Broadcast broadcast; final ImageColorWorker worker; - const BroadcastGridItem.loading(this.worker) + const BroadcastCard.loading(this.worker) : broadcast = const Broadcast( tour: BroadcastTournamentData( id: BroadcastTournamentId(''), @@ -244,17 +302,16 @@ class BroadcastGridItem extends StatefulWidget { ); @override - State createState() => _BroadcastGridItemState(); + State createState() => _BroadcastCartState(); } typedef _CardColors = ({ Color primaryContainer, Color onPrimaryContainer, - Color error, }); -final Map _colorsCache = {}; +final Map _colorsCache = {}; -Future<(_CardColors, Uint8List?)?> _computeImageColors( +Future<(_CardColors?, Uint8List?)?> _computeImageColors( ImageColorWorker worker, String imageUrl, [ Uint8List? image, @@ -264,11 +321,10 @@ Future<(_CardColors, Uint8List?)?> _computeImageColors( fileExtension: 'webp', ); if (response != null) { - final (:image, :primaryContainer, :onPrimaryContainer, :error) = response; + final (:image, :primaryContainer, :onPrimaryContainer) = response; final cardColors = ( primaryContainer: Color(primaryContainer), onPrimaryContainer: Color(onPrimaryContainer), - error: Color(error), ); _colorsCache[NetworkImage(imageUrl)] = cardColors; return (cardColors, image); @@ -276,7 +332,7 @@ Future<(_CardColors, Uint8List?)?> _computeImageColors( return null; } -class _BroadcastGridItemState extends State { +class _BroadcastCartState extends State { _CardColors? _cardColors; ImageProvider? _imageProvider; bool _tapDown = false; @@ -290,7 +346,7 @@ class _BroadcastGridItemState extends State { void didChangeDependencies() { super.didChangeDependencies(); final cachedColors = _colorsCache[imageProvider]; - if (cachedColors != null) { + if (_colorsCache.containsKey(imageProvider)) { _cardColors = cachedColors; _imageProvider = imageProvider; } else { @@ -341,9 +397,13 @@ class _BroadcastGridItemState extends State { _cardColors?.primaryContainer ?? defaultBackgroundColor; final titleColor = _cardColors?.onPrimaryContainer; final subTitleColor = - _cardColors?.onPrimaryContainer.withValues(alpha: 0.7) ?? - textShade(context, 0.7); - final liveColor = _cardColors?.error ?? LichessColors.red; + _cardColors?.onPrimaryContainer.withValues(alpha: 0.8) ?? + textShade(context, 0.8); + final bgHsl = HSLColor.fromColor(backgroundColor); + final liveHsl = HSLColor.fromColor(LichessColors.red); + final liveColor = + (bgHsl.lightness <= 0.5 ? liveHsl.withLightness(0.9) : liveHsl) + .toColor(); return GestureDetector( onTap: () { @@ -358,42 +418,46 @@ class _BroadcastGridItemState extends State { onTapDown: (_) => _onTapDown(), onTapCancel: _onTapCancel, onTapUp: (_) => _onTapCancel(), - child: AnimatedContainer( - duration: const Duration(milliseconds: 500), - clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - borderRadius: kBroadcastGridItemBorderRadius, - color: backgroundColor, - boxShadow: Theme.of(context).platform == TargetPlatform.iOS - ? null - : kElevationToShadow[1], - ), - child: Stack( - children: [ - ShaderMask( - blendMode: BlendMode.dstOut, - shaderCallback: (bounds) { - return LinearGradient( - begin: const Alignment(0.0, 0.5), - end: Alignment.bottomCenter, - colors: [ - backgroundColor.withValues(alpha: 0.0), - backgroundColor.withValues(alpha: 1.0), - ], - stops: const [0.5, 1.10], - tileMode: TileMode.clamp, - ).createShader(bounds); - }, - child: AnimatedOpacity( - duration: const Duration(milliseconds: 100), - opacity: _tapDown ? 1.0 : 0.7, + child: AnimatedOpacity( + opacity: _tapDown ? 1.0 : 0.85, + duration: const Duration(milliseconds: 100), + child: AnimatedContainer( + duration: const Duration(milliseconds: 500), + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + borderRadius: kBroadcastGridItemBorderRadius, + color: backgroundColor, + boxShadow: Theme.of(context).platform == TargetPlatform.iOS + ? null + : kElevationToShadow[1], + ), + child: Stack( + children: [ + ShaderMask( + blendMode: BlendMode.dstOut, + shaderCallback: (bounds) { + return LinearGradient( + begin: const Alignment(0.0, 0.5), + end: Alignment.bottomCenter, + colors: [ + backgroundColor.withValues(alpha: 0.0), + backgroundColor.withValues(alpha: 1.0), + ], + stops: const [0.5, 1.10], + tileMode: TileMode.clamp, + ).createShader(bounds); + }, child: AspectRatio( aspectRatio: 2.0, child: _imageProvider != null ? Image( image: _imageProvider!, - frameBuilder: - (context, child, frame, wasSynchronouslyLoaded) { + frameBuilder: ( + context, + child, + frame, + wasSynchronouslyLoaded, + ) { if (wasSynchronouslyLoaded) { return child; } @@ -409,88 +473,114 @@ class _BroadcastGridItemState extends State { : const SizedBox.shrink(), ), ), - ), - Positioned( - left: 0, - right: 0, - bottom: 12.0, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (widget.broadcast.round.startsAt != null || - widget.broadcast.isLive) - Padding( - padding: kBroadcastGridItemContentPadding, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - widget.broadcast.round.name, - style: TextStyle( - fontSize: 12, - color: subTitleColor, - ), - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), - const SizedBox(width: 4.0), - if (widget.broadcast.isLive) + Positioned( + left: 0, + right: 0, + bottom: 8.0, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (widget.broadcast.round.startsAt != null) + Padding( + padding: kBroadcastGridItemContentPadding, + child: Row( + children: [ Text( - 'LIVE', + widget.broadcast.round.name, style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.bold, - color: liveColor, - ), - overflow: TextOverflow.ellipsis, - ) - else - Text( - relativeDate(widget.broadcast.round.startsAt!), - style: TextStyle( - fontSize: 12, + fontSize: 13, color: subTitleColor, ), overflow: TextOverflow.ellipsis, maxLines: 1, ), - ], - ), - ), - Padding( - padding: kBroadcastGridItemContentPadding.add( - const EdgeInsets.symmetric(vertical: 3.0), - ), - child: Text( - widget.broadcast.title, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: titleColor, - fontWeight: FontWeight.bold, - height: 1.0, - fontSize: 16, + const SizedBox(width: 4.0), + Flexible( + child: Text( + relativeDate(widget.broadcast.round.startsAt!), + style: TextStyle( + fontSize: 13, + color: subTitleColor, + ), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + if (widget.broadcast.isLive) ...[ + const Spacer(flex: 3), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.circle, + size: 16, + color: liveColor, + shadows: const [ + Shadow( + color: Colors.black54, + offset: Offset(0, 1), + blurRadius: 2, + ), + ], + ), + const SizedBox(width: 4.0), + Text( + 'LIVE', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: liveColor, + shadows: const [ + Shadow( + color: Colors.black54, + offset: Offset(0, 1), + blurRadius: 2, + ), + ], + ), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ], + ], + ), ), - ), - ), - if (widget.broadcast.tour.information.players != null) Padding( - padding: kBroadcastGridItemContentPadding, + padding: kBroadcastGridItemContentPadding.add( + const EdgeInsets.symmetric(vertical: 3.0), + ), child: Text( - widget.broadcast.tour.information.players!, + widget.broadcast.title, + maxLines: 2, + overflow: TextOverflow.ellipsis, style: TextStyle( - fontSize: 12, - color: subTitleColor, - letterSpacing: -0.2, + color: titleColor, + fontWeight: FontWeight.bold, + height: 1.0, + fontSize: 16, ), - overflow: TextOverflow.ellipsis, - maxLines: 1, ), ), - ], + if (widget.broadcast.tour.information.players != null) + Padding( + padding: kBroadcastGridItemContentPadding, + child: Text( + widget.broadcast.tour.information.players!, + style: TextStyle( + fontSize: 12, + color: subTitleColor, + letterSpacing: -0.2, + ), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + ], + ), ), - ), - ], + ], + ), ), ), ); diff --git a/lib/src/view/watch/watch_tab_screen.dart b/lib/src/view/watch/watch_tab_screen.dart index 6dccdb2aa0..4ce43c81f3 100644 --- a/lib/src/view/watch/watch_tab_screen.dart +++ b/lib/src/view/watch/watch_tab_screen.dart @@ -177,7 +177,7 @@ class _BodyState extends ConsumerState<_Body> { try { await preCacheBroadcastImages( context, - broadcasts: current.value!.active.take(10), + broadcasts: current.value!.active, worker: _worker!, ); } finally { From c77dd2e36ffa3cb299e2953f2ecd8871ac529f31 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Sun, 15 Dec 2024 15:49:28 +0100 Subject: [PATCH 925/979] Fix image color extraction perf issues --- lib/src/utils/image.dart | 43 ++--- .../view/broadcast/broadcast_list_screen.dart | 166 +++++++++++------- pubspec.lock | 2 +- pubspec.yaml | 1 - .../broadcasts_list_screen_test.dart | 10 +- 5 files changed, 117 insertions(+), 105 deletions(-) diff --git a/lib/src/utils/image.dart b/lib/src/utils/image.dart index a11c152c40..58a31d6b2d 100644 --- a/lib/src/utils/image.dart +++ b/lib/src/utils/image.dart @@ -1,13 +1,10 @@ import 'dart:async'; import 'dart:isolate'; -import 'package:flutter/foundation.dart'; -import 'package:http/http.dart' as http; -import 'package:image/image.dart' as img; +import 'dart:typed_data'; import 'package:material_color_utilities/material_color_utilities.dart'; typedef ImageColors = ({ - Uint8List? image, int primaryContainer, int onPrimaryContainer, }); @@ -15,6 +12,9 @@ typedef ImageColors = ({ /// A worker that quantizes an image and returns a minimal color scheme associated /// with the image. /// +/// It is the responsibility of the caller to provide a scaled down version of the +/// image to the worker to avoid a costly quantization process. +/// /// The worker is created by calling [ImageColorWorker.spawn], and the computation /// is run in a separate isolate. class ImageColorWorker { @@ -26,23 +26,13 @@ class ImageColorWorker { bool get closed => _closed; - /// Returns a minimal color scheme associated with the image at the given [url], or - /// the given [image] if provided. - /// - /// The [fileExtension] parameter is optional and is used to specify the file - /// extension of the image at the given [url] if it is known. It will speed up - /// the decoding process, as otherwise the worker will check the image data - /// against all supported decoders. - Future getImageColors( - String url, { - Uint8List? image, - String? fileExtension, - }) async { + /// Returns a minimal color scheme associated with the given [image]. + Future getImageColors(Uint32List image) async { if (_closed) throw StateError('Closed'); final completer = Completer.sync(); final id = _idCounter++; _activeRequests[id] = completer; - _commands.send((id, url, image, fileExtension)); + _commands.send((id, image)); return await completer.future; } @@ -98,24 +88,11 @@ class ImageColorWorker { receivePort.close(); return; } - final (int id, String url, Uint8List? image, String? extension) = - message as (int, String, Uint8List?, String?); + final (int id, Uint32List image) = message as (int, Uint32List); try { - final bytes = image ?? await http.readBytes(Uri.parse(url)); // final stopwatch0 = Stopwatch()..start(); - final decoder = extension != null - ? img.findDecoderForNamedImage('.$extension') - : img.findDecoderForData(bytes); - final decoded = decoder!.decode(bytes); - final resized = img.copyResize(decoded!, width: 112); final QuantizerResult quantizerResult = - await QuantizerCelebi().quantize( - resized.buffer.asUint32List(), - 32, - ); - // print( - // 'Decoding and quantization took: ${stopwatch0.elapsedMilliseconds}ms', - // ); + await QuantizerCelebi().quantize(image, 32); final Map colorToCount = quantizerResult.colorToCount.map( (int key, int value) => MapEntry(_getArgbFromAbgr(key), value), @@ -149,8 +126,8 @@ class ImageColorWorker { isDark: sourceColor.tone < 50, contrastLevel: 0.0, ); + // print('Quantize and scoring took: ${stopwatch0.elapsedMilliseconds}ms'); final result = ( - image: image == null ? bytes : null, primaryContainer: scheme.primaryContainer, onPrimaryContainer: scheme.onPrimaryContainer, ); diff --git a/lib/src/view/broadcast/broadcast_list_screen.dart b/lib/src/view/broadcast/broadcast_list_screen.dart index 6dd3e8ceca..ff6eb953ce 100644 --- a/lib/src/view/broadcast/broadcast_list_screen.dart +++ b/lib/src/view/broadcast/broadcast_list_screen.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:typed_data'; +import 'dart:ui' as ui; import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/cupertino.dart'; @@ -311,30 +312,27 @@ typedef _CardColors = ({ }); final Map _colorsCache = {}; -Future<(_CardColors?, Uint8List?)?> _computeImageColors( +Future<_CardColors?> _computeImageColors( ImageColorWorker worker, - String imageUrl, [ - Uint8List? image, -]) async { - final response = await worker.getImageColors( - imageUrl, - fileExtension: 'webp', - ); + String imageUrl, + ByteData imageBytes, +) async { + final response = + await worker.getImageColors(imageBytes.buffer.asUint32List()); if (response != null) { - final (:image, :primaryContainer, :onPrimaryContainer) = response; + final (:primaryContainer, :onPrimaryContainer) = response; final cardColors = ( primaryContainer: Color(primaryContainer), onPrimaryContainer: Color(onPrimaryContainer), ); _colorsCache[NetworkImage(imageUrl)] = cardColors; - return (cardColors, image); + return cardColors; } return null; } class _BroadcastCartState extends State { _CardColors? _cardColors; - ImageProvider? _imageProvider; bool _tapDown = false; String? get imageUrl => widget.broadcast.tour.imageUrl; @@ -348,31 +346,28 @@ class _BroadcastCartState extends State { final cachedColors = _colorsCache[imageProvider]; if (_colorsCache.containsKey(imageProvider)) { _cardColors = cachedColors; - _imageProvider = imageProvider; - } else { - if (imageUrl != null) { - _fetchImageAndColors(NetworkImage(imageUrl!)); - } else { - _imageProvider = kDefaultBroadcastImage; - } + } else if (imageUrl != null) { + _getImageColors(NetworkImage(imageUrl!)); } } - Future _fetchImageAndColors(NetworkImage provider) async { + Future _getImageColors(NetworkImage provider) async { if (!mounted) return; if (Scrollable.recommendDeferredLoadingForContext(context)) { SchedulerBinding.instance.scheduleFrameCallback((_) { - scheduleMicrotask(() => _fetchImageAndColors(provider)); + scheduleMicrotask(() => _getImageColors(provider)); }); } else if (widget.worker.closed == false) { - final response = await _computeImageColors(widget.worker, provider.url); + await precacheImage(provider, context); + final ui.Image scaledImage = await _imageProviderToScaled(provider); + final imageBytes = await scaledImage.toByteData(); + final response = + await _computeImageColors(widget.worker, provider.url, imageBytes!); if (response != null) { - final (cardColors, image) = response; if (mounted) { setState(() { - _imageProvider = image != null ? MemoryImage(image) : imageProvider; - _cardColors = cardColors; + _cardColors = response; }); } } @@ -449,28 +444,26 @@ class _BroadcastCartState extends State { }, child: AspectRatio( aspectRatio: 2.0, - child: _imageProvider != null - ? Image( - image: _imageProvider!, - frameBuilder: ( - context, - child, - frame, - wasSynchronouslyLoaded, - ) { - if (wasSynchronouslyLoaded) { - return child; - } - return AnimatedOpacity( - duration: const Duration(milliseconds: 500), - opacity: frame == null ? 0 : 1, - child: child, - ); - }, - errorBuilder: (context, error, stackTrace) => - const Image(image: kDefaultBroadcastImage), - ) - : const SizedBox.shrink(), + child: Image( + image: imageProvider, + frameBuilder: ( + context, + child, + frame, + wasSynchronouslyLoaded, + ) { + if (wasSynchronouslyLoaded) { + return child; + } + return AnimatedOpacity( + duration: const Duration(milliseconds: 500), + opacity: frame == null ? 0 : 1, + child: child, + ); + }, + errorBuilder: (context, error, stackTrace) => + const Image(image: kDefaultBroadcastImage), + ), ), ), Positioned( @@ -587,6 +580,7 @@ class _BroadcastCartState extends State { } } +/// Pre-cache images and extract colors for broadcasts. Future preCacheBroadcastImages( BuildContext context, { required Iterable broadcasts, @@ -597,22 +591,70 @@ Future preCacheBroadcastImages( if (imageUrl != null) { final provider = NetworkImage(imageUrl); await precacheImage(provider, context); - final imageStream = provider.resolve(ImageConfiguration.empty); - final Completer completer = Completer(); - final ImageStreamListener listener = ImageStreamListener( - (imageInfo, synchronousCall) async { - final bytes = await imageInfo.image.toByteData(); - if (!completer.isCompleted) { - completer.complete(bytes?.buffer.asUint8List()); - } - }, - ); - imageStream.addListener(listener); - final imageBytes = await completer.future; - imageStream.removeListener(listener); - if (imageBytes != null) { - await _computeImageColors(worker, imageUrl, imageBytes); - } + final ui.Image scaledImage = await _imageProviderToScaled(provider); + final imageBytes = await scaledImage.toByteData(); + await _computeImageColors(worker, imageUrl, imageBytes!); } } } + +// Scale image size down to reduce computation time of color extraction. +Future _imageProviderToScaled(ImageProvider imageProvider) async { + const double maxDimension = 112.0; + final ImageStream stream = imageProvider.resolve( + const ImageConfiguration(size: Size(maxDimension, maxDimension)), + ); + final Completer imageCompleter = Completer(); + late ImageStreamListener listener; + late ui.Image scaledImage; + Timer? loadFailureTimeout; + + listener = ImageStreamListener( + (ImageInfo info, bool sync) async { + loadFailureTimeout?.cancel(); + stream.removeListener(listener); + final ui.Image image = info.image; + final int width = image.width; + final int height = image.height; + double paintWidth = width.toDouble(); + double paintHeight = height.toDouble(); + assert(width > 0 && height > 0); + + final bool rescale = width > maxDimension || height > maxDimension; + if (rescale) { + paintWidth = + (width > height) ? maxDimension : (maxDimension / height) * width; + paintHeight = + (height > width) ? maxDimension : (maxDimension / width) * height; + } + final ui.PictureRecorder pictureRecorder = ui.PictureRecorder(); + final Canvas canvas = Canvas(pictureRecorder); + paintImage( + canvas: canvas, + rect: Rect.fromLTRB(0, 0, paintWidth, paintHeight), + image: image, + filterQuality: FilterQuality.none, + ); + + final ui.Picture picture = pictureRecorder.endRecording(); + scaledImage = + await picture.toImage(paintWidth.toInt(), paintHeight.toInt()); + imageCompleter.complete(info.image); + }, + onError: (Object exception, StackTrace? stackTrace) { + stream.removeListener(listener); + throw Exception('Failed to render image: $exception'); + }, + ); + + loadFailureTimeout = Timer(const Duration(seconds: 5), () { + stream.removeListener(listener); + imageCompleter.completeError( + TimeoutException('Timeout occurred trying to load image'), + ); + }); + + stream.addListener(listener); + await imageCompleter.future; + return scaledImage; +} diff --git a/pubspec.lock b/pubspec.lock index 4ab4422b73..5d52eebcbf 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -804,7 +804,7 @@ packages: source: hosted version: "0.1.0" image: - dependency: "direct main" + dependency: transitive description: name: image sha256: "599d08e369969bdf83138f5b4e0a7e823d3f992f23b8a64dd626877c37013533" diff --git a/pubspec.yaml b/pubspec.yaml index 9c5eb9c2e4..f2a051efab 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -52,7 +52,6 @@ dependencies: flutter_svg: ^2.0.10+1 freezed_annotation: ^2.2.0 http: ^1.1.0 - image: ^4.3.0 intl: ^0.19.0 json_annotation: ^4.7.0 linkify: ^5.0.0 diff --git a/test/view/broadcast/broadcasts_list_screen_test.dart b/test/view/broadcast/broadcasts_list_screen_test.dart index 1c3e093c6f..fb845378c6 100644 --- a/test/view/broadcast/broadcasts_list_screen_test.dart +++ b/test/view/broadcast/broadcasts_list_screen_test.dart @@ -20,11 +20,7 @@ class FakeImageColorWorker implements ImageColorWorker { bool get closed => false; @override - Future getImageColors( - String url, { - Uint8List? image, - String? fileExtension, - }) { + Future getImageColors(Uint32List image) { return Future.value(null); } } @@ -73,7 +69,7 @@ void main() { // wait for broadcast tournaments to load await tester.pump(const Duration(milliseconds: 100)); - expect(find.byType(BroadcastGridItem), findsAtLeast(1)); + expect(find.byType(BroadcastCard), findsAtLeast(1)); }); }, ); @@ -103,8 +99,6 @@ void main() { await tester.pump(const Duration(milliseconds: 100)); await tester.scrollUntilVisible(find.text('Completed'), 200.0); - - await tester.pumpAndSettle(); }); }, ); From 10cb3366b6296430c94941ea0b0424ea3a33fdf5 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Sun, 15 Dec 2024 17:52:37 +0100 Subject: [PATCH 926/979] Don't show a default title if title is not yet loaded --- .../view/broadcast/broadcast_game_screen.dart | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_game_screen.dart b/lib/src/view/broadcast/broadcast_game_screen.dart index 4c721c2d9f..bcd3ad5c78 100644 --- a/lib/src/view/broadcast/broadcast_game_screen.dart +++ b/lib/src/view/broadcast/broadcast_game_screen.dart @@ -87,19 +87,24 @@ class _BroadcastGameScreenState extends ConsumerState ); final broadcastGamePgn = ref .watch(broadcastGameControllerProvider(widget.roundId, widget.gameId)); - final title = widget.title ?? - (switch (ref.watch(broadcastGameScreenTitleProvider(widget.roundId))) { - AsyncData(value: final title) => title, - _ => 'Broadcast Game', - }); + final title = (widget.title != null) + ? Text( + widget.title!, + overflow: TextOverflow.ellipsis, + maxLines: 1, + ) + : switch (ref.watch(broadcastGameScreenTitleProvider(widget.roundId))) { + AsyncData(value: final title) => Text( + title, + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + _ => const SizedBox.shrink(), + }; return PlatformScaffold( appBar: PlatformAppBar( - title: Text( - title, - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), + title: title, actions: [ AppBarAnalysisTabIndicator( tabs: tabs, From b410cbe0c30144478774062cde2c4a44f60e4729 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Sun, 15 Dec 2024 19:02:11 +0100 Subject: [PATCH 927/979] Rename some broadcast variables --- lib/src/view/broadcast/broadcast_game_screen.dart | 13 +++++++------ .../broadcast/broadcast_game_screen_providers.dart | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_game_screen.dart b/lib/src/view/broadcast/broadcast_game_screen.dart index bcd3ad5c78..9883ad9c3e 100644 --- a/lib/src/view/broadcast/broadcast_game_screen.dart +++ b/lib/src/view/broadcast/broadcast_game_screen.dart @@ -82,10 +82,10 @@ class _BroadcastGameScreenState extends ConsumerState @override Widget build(BuildContext context) { - final broadcastGameState = ref.watch( - broadcastGameProvider(widget.roundId, widget.gameId), + final broadcastRoundGameState = ref.watch( + broadcastRoundGameProvider(widget.roundId, widget.gameId), ); - final broadcastGamePgn = ref + final broadcastGameState = ref .watch(broadcastGameControllerProvider(widget.roundId, widget.gameId)); final title = (widget.title != null) ? Text( @@ -111,7 +111,7 @@ class _BroadcastGameScreenState extends ConsumerState controller: _tabController, ), AppBarIconButton( - onPressed: (broadcastGamePgn.hasValue) + onPressed: (broadcastGameState.hasValue) ? () { pushPlatformRoute( context, @@ -127,7 +127,7 @@ class _BroadcastGameScreenState extends ConsumerState ), ], ), - body: switch ((broadcastGameState, broadcastGamePgn)) { + body: switch ((broadcastRoundGameState, broadcastGameState)) { (AsyncData(), AsyncData()) => _Body( widget.tournamentId, widget.roundId, @@ -397,7 +397,8 @@ class _PlayerWidget extends ConsumerWidget { final broadcastGameState = ref .watch(broadcastGameControllerProvider(roundId, gameId)) .requireValue; - final game = ref.watch(broadcastGameProvider(roundId, gameId)).requireValue; + final game = + ref.watch(broadcastRoundGameProvider(roundId, gameId)).requireValue; final isCursorOnLiveMove = broadcastGameState.currentPath == broadcastGameState.broadcastLivePath; diff --git a/lib/src/view/broadcast/broadcast_game_screen_providers.dart b/lib/src/view/broadcast/broadcast_game_screen_providers.dart index a78879d2ba..8490ee3988 100644 --- a/lib/src/view/broadcast/broadcast_game_screen_providers.dart +++ b/lib/src/view/broadcast/broadcast_game_screen_providers.dart @@ -7,7 +7,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'broadcast_game_screen_providers.g.dart'; @riverpod -Future broadcastGame( +Future broadcastRoundGame( Ref ref, BroadcastRoundId roundId, BroadcastGameId gameId, From aa2508fcc832c95c7b55142cf610aeeca65dc3d7 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Sun, 15 Dec 2024 20:01:34 +0100 Subject: [PATCH 928/979] Broadcast list screen fixes --- lib/src/utils/l10n.dart | 8 ++++---- .../view/broadcast/broadcast_list_screen.dart | 18 +++++++++++++----- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/lib/src/utils/l10n.dart b/lib/src/utils/l10n.dart index bf7b6adc33..3f76593481 100644 --- a/lib/src/utils/l10n.dart +++ b/lib/src/utils/l10n.dart @@ -45,8 +45,8 @@ Text l10nWithWidget( } final _dayFormatter = DateFormat.E().add_jm(); -final _monthFormatter = DateFormat.MMMd().add_Hm(); -final _dateFormatterWithYear = DateFormat.yMMMd().add_Hm(); +final _monthFormatter = DateFormat.MMMd(); +final _dateFormatterWithYear = DateFormat.yMMMd(); String relativeDate(DateTime date) { final diff = date.difference(DateTime.now()); @@ -55,9 +55,9 @@ String relativeDate(DateTime date) { ? diff.inHours == 0 ? 'in ${diff.inMinutes} minute${diff.inMinutes > 1 ? 's' : ''}' // TODO translate with https://github.com/lichess-org/lila/blob/65b28ea8e43e0133df6c7ed40e03c2954f247d1e/translation/source/timeago.xml#L8 : 'in ${diff.inHours} hour${diff.inHours > 1 ? 's' : ''}' // TODO translate with https://github.com/lichess-org/lila/blob/65b28ea8e43e0133df6c7ed40e03c2954f247d1e/translation/source/timeago.xml#L12 - : diff.inDays <= 7 + : diff.inDays.abs() <= 7 ? _dayFormatter.format(date) - : diff.inDays < 365 + : diff.inDays.abs() < 365 ? _monthFormatter.format(date) : _dateFormatterWithYear.format(date); } diff --git a/lib/src/view/broadcast/broadcast_list_screen.dart b/lib/src/view/broadcast/broadcast_list_screen.dart index ff6eb953ce..f44ab4645b 100644 --- a/lib/src/view/broadcast/broadcast_list_screen.dart +++ b/lib/src/view/broadcast/broadcast_list_screen.dart @@ -257,6 +257,12 @@ class _BodyState extends ConsumerState<_Body> { ), ], ), + const SliverSafeArea( + top: false, + sliver: SliverToBoxAdapter( + child: SizedBox(height: 16.0), + ), + ), ], ), ), @@ -477,22 +483,25 @@ class _BroadcastCartState extends State { Padding( padding: kBroadcastGridItemContentPadding, child: Row( + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, children: [ Text( widget.broadcast.round.name, style: TextStyle( - fontSize: 13, + fontSize: 14, color: subTitleColor, + letterSpacing: -0.2, ), overflow: TextOverflow.ellipsis, maxLines: 1, ), - const SizedBox(width: 4.0), - Flexible( + const SizedBox(width: 5.0), + Expanded( child: Text( relativeDate(widget.broadcast.round.startsAt!), style: TextStyle( - fontSize: 13, + fontSize: 12, color: subTitleColor, ), overflow: TextOverflow.ellipsis, @@ -500,7 +509,6 @@ class _BroadcastCartState extends State { ), ), if (widget.broadcast.isLive) ...[ - const Spacer(flex: 3), Row( mainAxisSize: MainAxisSize.min, children: [ From ed670950598bfa684ba0e1f3ce1c0f7cbaba4e41 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Sun, 15 Dec 2024 20:20:23 +0100 Subject: [PATCH 929/979] Increase dart version; update page width to 100 --- analysis_options.yaml | 3 + lib/src/app.dart | 151 ++--- lib/src/binding.dart | 10 +- lib/src/constants.dart | 13 +- lib/src/db/database.dart | 21 +- lib/src/db/openings_database.dart | 11 +- lib/src/db/secure_storage.dart | 6 +- lib/src/init.dart | 34 +- lib/src/intl.dart | 10 +- lib/src/localizations.dart | 15 +- lib/src/log.dart | 33 +- .../model/account/account_preferences.dart | 85 +-- lib/src/model/account/account_repository.dart | 70 +- .../model/analysis/analysis_controller.dart | 317 ++++----- .../model/analysis/analysis_preferences.dart | 63 +- lib/src/model/analysis/opening_service.dart | 12 +- .../analysis/server_analysis_service.dart | 15 +- lib/src/model/auth/auth_controller.dart | 7 +- lib/src/model/auth/auth_repository.dart | 22 +- lib/src/model/auth/auth_session.dart | 9 +- lib/src/model/auth/session_storage.dart | 4 +- .../board_editor/board_editor_controller.dart | 79 +-- lib/src/model/broadcast/broadcast.dart | 48 +- .../broadcast/broadcast_game_controller.dart | 161 ++--- .../model/broadcast/broadcast_providers.dart | 19 +- .../model/broadcast/broadcast_repository.dart | 131 ++-- .../broadcast/broadcast_round_controller.dart | 84 +-- lib/src/model/challenge/challenge.dart | 197 +++--- .../challenge/challenge_preferences.dart | 18 +- .../model/challenge/challenge_repository.dart | 30 +- .../model/challenge/challenge_service.dart | 66 +- lib/src/model/challenge/challenges.dart | 13 +- lib/src/model/clock/chess_clock.dart | 9 +- .../model/clock/clock_tool_controller.dart | 94 +-- lib/src/model/common/chess.dart | 51 +- lib/src/model/common/chess960.dart | 4 +- lib/src/model/common/eval.dart | 88 +-- lib/src/model/common/game.dart | 8 +- lib/src/model/common/id.dart | 36 +- lib/src/model/common/node.dart | 208 +++--- lib/src/model/common/perf.dart | 12 +- lib/src/model/common/preloaded_data.dart | 15 +- .../model/common/service/sound_service.dart | 41 +- lib/src/model/common/socket.dart | 16 +- lib/src/model/common/speed.dart | 4 +- lib/src/model/common/time_increment.dart | 18 +- lib/src/model/common/uci.dart | 65 +- .../coordinate_training_controller.dart | 35 +- .../coordinate_training_preferences.dart | 4 +- .../correspondence_game_storage.dart | 68 +- .../correspondence_service.dart | 62 +- .../offline_correspondence_game.dart | 6 +- lib/src/model/engine/engine.dart | 66 +- lib/src/model/engine/evaluation_service.dart | 48 +- lib/src/model/engine/uci_protocol.dart | 24 +- lib/src/model/engine/work.dart | 20 +- lib/src/model/game/archived_game.dart | 97 ++- lib/src/model/game/chat_controller.dart | 60 +- lib/src/model/game/game.dart | 109 ++- lib/src/model/game/game_controller.dart | 524 ++++++-------- lib/src/model/game/game_filter.dart | 42 +- lib/src/model/game/game_history.dart | 123 ++-- lib/src/model/game/game_preferences.dart | 19 +- lib/src/model/game/game_repository.dart | 48 +- .../model/game/game_repository_providers.dart | 9 +- lib/src/model/game/game_share_service.dart | 50 +- lib/src/model/game/game_socket_events.dart | 65 +- lib/src/model/game/game_status.dart | 4 +- lib/src/model/game/game_storage.dart | 59 +- lib/src/model/game/material_diff.dart | 18 +- lib/src/model/game/over_the_board_game.dart | 6 +- lib/src/model/game/playable_game.dart | 78 +-- lib/src/model/game/player.dart | 8 +- lib/src/model/lobby/create_game_service.dart | 59 +- lib/src/model/lobby/game_seek.dart | 60 +- .../model/lobby/game_setup_preferences.dart | 44 +- lib/src/model/lobby/lobby_numbers.dart | 5 +- lib/src/model/lobby/lobby_repository.dart | 14 +- .../notifications/notification_service.dart | 96 +-- .../model/notifications/notifications.dart | 143 ++-- .../opening_explorer/opening_explorer.dart | 32 +- .../opening_explorer_preferences.dart | 133 ++-- .../opening_explorer_repository.dart | 93 +-- .../over_the_board/over_the_board_clock.dart | 37 +- .../over_the_board_game_controller.dart | 69 +- lib/src/model/puzzle/puzzle.dart | 53 +- lib/src/model/puzzle/puzzle_activity.dart | 14 +- .../model/puzzle/puzzle_batch_storage.dart | 140 ++-- lib/src/model/puzzle/puzzle_controller.dart | 219 ++---- lib/src/model/puzzle/puzzle_difficulty.dart | 5 +- lib/src/model/puzzle/puzzle_opening.dart | 22 +- lib/src/model/puzzle/puzzle_preferences.dart | 13 +- lib/src/model/puzzle/puzzle_providers.dart | 34 +- lib/src/model/puzzle/puzzle_repository.dart | 142 ++-- lib/src/model/puzzle/puzzle_service.dart | 105 ++- lib/src/model/puzzle/puzzle_session.dart | 41 +- lib/src/model/puzzle/puzzle_storage.dart | 22 +- lib/src/model/puzzle/puzzle_streak.dart | 3 +- lib/src/model/puzzle/puzzle_theme.dart | 23 +- lib/src/model/puzzle/storm.dart | 32 +- lib/src/model/puzzle/storm_controller.dart | 83 +-- lib/src/model/puzzle/streak_storage.dart | 12 +- lib/src/model/relation/online_friends.dart | 18 +- .../model/relation/relation_repository.dart | 20 +- lib/src/model/settings/board_preferences.dart | 145 ++-- lib/src/model/settings/brightness.dart | 9 +- .../model/settings/general_preferences.dart | 30 +- lib/src/model/settings/home_preferences.dart | 26 +- .../settings/over_the_board_preferences.dart | 16 +- .../model/settings/preferences_storage.dart | 25 +- lib/src/model/study/study.dart | 51 +- lib/src/model/study/study_controller.dart | 214 +++--- lib/src/model/study/study_filter.dart | 29 +- lib/src/model/study/study_list_paginator.dart | 26 +- lib/src/model/study/study_preferences.dart | 17 +- lib/src/model/study/study_repository.dart | 34 +- lib/src/model/tv/featured_player.dart | 7 +- lib/src/model/tv/live_tv_channels.dart | 32 +- lib/src/model/tv/tv_channel.dart | 7 +- lib/src/model/tv/tv_controller.dart | 69 +- lib/src/model/tv/tv_repository.dart | 18 +- lib/src/model/user/leaderboard.dart | 9 +- lib/src/model/user/profile.dart | 50 +- lib/src/model/user/search_history.dart | 11 +- lib/src/model/user/user.dart | 164 ++--- lib/src/model/user/user_repository.dart | 112 ++- .../model/user/user_repository_providers.dart | 53 +- lib/src/navigation.dart | 66 +- lib/src/network/connectivity.dart | 47 +- lib/src/network/http.dart | 122 +--- lib/src/network/socket.dart | 155 ++--- lib/src/styles/lichess_colors.dart | 9 +- lib/src/styles/lichess_icons.dart | 3 +- lib/src/styles/puzzle_icons.dart | 299 ++++---- lib/src/styles/social_icons.dart | 6 +- lib/src/styles/styles.dart | 52 +- lib/src/utils/async_value.dart | 4 +- lib/src/utils/badge_service.dart | 4 +- lib/src/utils/chessboard.dart | 8 +- lib/src/utils/color_palette.dart | 5 +- lib/src/utils/focus_detector.dart | 17 +- lib/src/utils/gestures_exclusion.dart | 42 +- lib/src/utils/image.dart | 46 +- lib/src/utils/immersive_mode.dart | 25 +- lib/src/utils/json.dart | 16 +- lib/src/utils/l10n.dart | 223 +++--- lib/src/utils/navigation.dart | 58 +- lib/src/utils/rate_limit.dart | 4 +- lib/src/utils/screen.dart | 3 +- lib/src/utils/share.dart | 8 +- lib/src/utils/system.dart | 6 +- lib/src/view/account/edit_profile_screen.dart | 180 ++--- lib/src/view/account/profile_screen.dart | 63 +- lib/src/view/account/rating_pref_aware.dart | 6 +- lib/src/view/analysis/analysis_board.dart | 82 +-- lib/src/view/analysis/analysis_layout.dart | 202 +++--- lib/src/view/analysis/analysis_screen.dart | 145 ++-- lib/src/view/analysis/analysis_settings.dart | 69 +- .../view/analysis/analysis_share_screen.dart | 211 +++--- lib/src/view/analysis/server_analysis.dart | 424 +++++------- lib/src/view/analysis/stockfish_settings.dart | 45 +- lib/src/view/analysis/tree_view.dart | 12 +- .../view/board_editor/board_editor_menu.dart | 64 +- .../board_editor/board_editor_screen.dart | 242 +++---- .../view/broadcast/broadcast_boards_tab.dart | 90 ++- .../broadcast/broadcast_game_bottom_bar.dart | 54 +- .../view/broadcast/broadcast_game_screen.dart | 302 ++++----- .../broadcast/broadcast_game_settings.dart | 55 +- .../broadcast/broadcast_game_tree_view.dart | 5 +- .../view/broadcast/broadcast_list_screen.dart | 251 +++---- .../broadcast/broadcast_overview_tab.dart | 115 ++-- .../broadcast/broadcast_player_widget.dart | 14 +- .../view/broadcast/broadcast_players_tab.dart | 141 ++-- .../broadcast/broadcast_round_screen.dart | 291 ++++---- lib/src/view/clock/clock_settings.dart | 84 ++- lib/src/view/clock/clock_tool_screen.dart | 114 ++-- lib/src/view/clock/custom_clock_settings.dart | 37 +- .../coordinate_display.dart | 49 +- .../coordinate_training_screen.dart | 215 ++---- .../offline_correspondence_game_screen.dart | 185 ++--- lib/src/view/engine/engine_depth.dart | 111 ++- lib/src/view/engine/engine_gauge.dart | 194 +++--- lib/src/view/engine/engine_lines.dart | 89 +-- lib/src/view/game/archived_game_screen.dart | 150 ++--- .../game/correspondence_clock_widget.dart | 57 +- lib/src/view/game/game_body.dart | 582 +++++++--------- lib/src/view/game/game_common_widgets.dart | 163 ++--- lib/src/view/game/game_list_tile.dart | 260 +++---- lib/src/view/game/game_loading_board.dart | 34 +- lib/src/view/game/game_player.dart | 136 ++-- lib/src/view/game/game_result_dialog.dart | 176 ++--- lib/src/view/game/game_screen.dart | 194 +++--- lib/src/view/game/game_screen_providers.dart | 53 +- lib/src/view/game/game_settings.dart | 30 +- lib/src/view/game/message_screen.dart | 104 +-- .../offline_correspondence_games_screen.dart | 33 +- lib/src/view/game/ping_rating.dart | 23 +- lib/src/view/game/status_l10n.dart | 16 +- lib/src/view/home/home_tab_screen.dart | 459 +++++-------- .../opening_explorer_screen.dart | 171 ++--- .../opening_explorer_settings.dart | 150 ++--- .../opening_explorer_view.dart | 102 ++- .../opening_explorer_widgets.dart | 194 ++---- .../configure_over_the_board_game.dart | 80 +-- .../over_the_board/over_the_board_screen.dart | 182 +++-- lib/src/view/play/challenge_list_item.dart | 116 ++-- lib/src/view/play/common_play_widgets.dart | 83 ++- .../view/play/create_challenge_screen.dart | 221 +++--- .../view/play/create_custom_game_screen.dart | 429 ++++++------ lib/src/view/play/create_game_options.dart | 86 +-- lib/src/view/play/ongoing_games_screen.dart | 51 +- lib/src/view/play/online_bots_screen.dart | 256 +++---- lib/src/view/play/play_screen.dart | 9 +- lib/src/view/play/quick_game_button.dart | 92 ++- lib/src/view/play/quick_game_matrix.dart | 123 ++-- lib/src/view/play/time_control_modal.dart | 186 ++--- lib/src/view/puzzle/dashboard_screen.dart | 87 +-- lib/src/view/puzzle/opening_screen.dart | 157 ++--- .../view/puzzle/puzzle_feedback_widget.dart | 118 ++-- .../view/puzzle/puzzle_history_screen.dart | 123 ++-- lib/src/view/puzzle/puzzle_screen.dart | 320 ++++----- .../view/puzzle/puzzle_session_widget.dart | 176 +++-- .../view/puzzle/puzzle_settings_screen.dart | 10 +- lib/src/view/puzzle/puzzle_tab_screen.dart | 546 +++++++-------- lib/src/view/puzzle/puzzle_themes_screen.dart | 169 ++--- lib/src/view/puzzle/storm_clock.dart | 75 +-- lib/src/view/puzzle/storm_dashboard.dart | 94 +-- lib/src/view/puzzle/storm_screen.dart | 487 ++++++------- lib/src/view/puzzle/streak_screen.dart | 192 +++--- lib/src/view/relation/following_screen.dart | 69 +- .../settings/account_preferences_screen.dart | 586 +++++++--------- .../settings/app_background_mode_screen.dart | 22 +- .../view/settings/board_settings_screen.dart | 161 ++--- lib/src/view/settings/board_theme_screen.dart | 76 +-- lib/src/view/settings/piece_set_screen.dart | 44 +- .../view/settings/settings_tab_screen.dart | 143 ++-- .../view/settings/sound_settings_screen.dart | 42 +- lib/src/view/settings/theme_screen.dart | 110 ++- .../view/settings/toggle_sound_button.dart | 7 +- lib/src/view/study/study_bottom_bar.dart | 225 +++---- lib/src/view/study/study_gamebook.dart | 48 +- lib/src/view/study/study_list_screen.dart | 173 ++--- lib/src/view/study/study_screen.dart | 373 ++++------ lib/src/view/study/study_settings.dart | 49 +- lib/src/view/study/study_tree_view.dart | 16 +- lib/src/view/tools/load_position_screen.dart | 29 +- lib/src/view/tools/tools_tab_screen.dart | 165 +++-- .../view/user/challenge_requests_screen.dart | 83 +-- lib/src/view/user/game_history_screen.dart | 242 +++---- lib/src/view/user/leaderboard_screen.dart | 95 +-- lib/src/view/user/leaderboard_widget.dart | 27 +- lib/src/view/user/perf_cards.dart | 77 +-- lib/src/view/user/perf_stats_screen.dart | 547 ++++++--------- lib/src/view/user/player_screen.dart | 74 +- lib/src/view/user/recent_games.dart | 72 +- lib/src/view/user/search_screen.dart | 152 ++--- lib/src/view/user/user_activity.dart | 278 +++----- lib/src/view/user/user_profile.dart | 64 +- lib/src/view/user/user_screen.dart | 74 +- .../view/watch/live_tv_channels_screen.dart | 30 +- lib/src/view/watch/streamer_screen.dart | 63 +- lib/src/view/watch/tv_screen.dart | 158 ++--- lib/src/view/watch/watch_tab_screen.dart | 254 +++---- lib/src/widgets/adaptive_action_sheet.dart | 72 +- lib/src/widgets/adaptive_autocomplete.dart | 135 ++-- lib/src/widgets/adaptive_bottom_sheet.dart | 35 +- lib/src/widgets/adaptive_choice_picker.dart | 154 +++-- lib/src/widgets/adaptive_date_picker.dart | 3 +- lib/src/widgets/adaptive_text_field.dart | 8 +- lib/src/widgets/board_carousel_item.dart | 92 +-- lib/src/widgets/board_preview.dart | 67 +- lib/src/widgets/board_table.dart | 144 ++-- lib/src/widgets/board_thumbnail.dart | 101 ++- lib/src/widgets/bottom_bar.dart | 18 +- lib/src/widgets/bottom_bar_button.dart | 52 +- lib/src/widgets/buttons.dart | 242 +++---- lib/src/widgets/clock.dart | 72 +- lib/src/widgets/expanded_section.dart | 14 +- lib/src/widgets/feedback.dart | 149 ++-- lib/src/widgets/filter.dart | 20 +- lib/src/widgets/list.dart | 372 +++++----- lib/src/widgets/misc.dart | 23 +- lib/src/widgets/move_list.dart | 193 +++--- lib/src/widgets/non_linear_slider.dart | 32 +- lib/src/widgets/pgn.dart | 637 +++++++----------- lib/src/widgets/platform.dart | 76 +-- lib/src/widgets/platform_alert_dialog.dart | 31 +- lib/src/widgets/platform_scaffold.dart | 60 +- lib/src/widgets/platform_search_bar.dart | 56 +- lib/src/widgets/progression_widget.dart | 17 +- lib/src/widgets/settings.dart | 128 ++-- lib/src/widgets/shimmer.dart | 73 +- lib/src/widgets/stat_card.dart | 29 +- lib/src/widgets/user_full_name.dart | 37 +- lib/src/widgets/user_list_tile.dart | 52 +- lib/src/widgets/yes_no_dialog.dart | 10 +- pubspec.lock | 16 +- pubspec.yaml | 4 +- 298 files changed, 10828 insertions(+), 16966 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index b6dfa1feec..07d7adc1b8 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -26,6 +26,9 @@ analyzer: plugins: - custom_lint +formatter: + page_width: 100 + linter: rules: prefer_single_quotes: true diff --git a/lib/src/app.dart b/lib/src/app.dart index 0cb59123d7..158855a519 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -26,23 +26,20 @@ class AppInitializationScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - ref.listen>( - preloadedDataProvider, - (_, state) { - if (state.hasValue || state.hasError) { - FlutterNativeSplash.remove(); - } - }, - ); + ref.listen>(preloadedDataProvider, (_, state) { + if (state.hasValue || state.hasError) { + FlutterNativeSplash.remove(); + } + }); - return ref.watch(preloadedDataProvider).when( + return ref + .watch(preloadedDataProvider) + .when( data: (_) => const Application(), // loading screen is handled by the native splash screen loading: () => const SizedBox.shrink(), error: (err, st) { - debugPrint( - 'SEVERE: [App] could not initialize app; $err\n$st', - ); + debugPrint('SEVERE: [App] could not initialize app; $err\n$st'); return const SizedBox.shrink(); }, ); @@ -88,8 +85,7 @@ class _AppState extends ConsumerState { // Play registered moves whenever the app comes back online. if (prevWasOffline && currentIsOnline) { - final nbMovesPlayed = - await ref.read(correspondenceServiceProvider).playRegisteredMoves(); + final nbMovesPlayed = await ref.read(correspondenceServiceProvider).playRegisteredMoves(); if (nbMovesPlayed > 0) { ref.invalidate(ongoingGamesProvider); } @@ -125,9 +121,7 @@ class _AppState extends ConsumerState { final generalPrefs = ref.watch(generalPreferencesProvider); final brightness = ref.watch(currentBrightnessProvider); - final boardTheme = ref.watch( - boardPreferencesProvider.select((state) => state.boardTheme), - ); + final boardTheme = ref.watch(boardPreferencesProvider.select((state) => state.boardTheme)); final remainingHeight = estimateRemainingHeightLeftBoard(context); @@ -135,12 +129,10 @@ class _AppState extends ConsumerState { builder: (lightColorScheme, darkColorScheme) { // TODO remove this workaround when the dynamic_color colorScheme bug is fixed // See: https://github.com/material-foundation/flutter-packages/issues/574 - final ( - fixedLightScheme, - fixedDarkScheme - ) = lightColorScheme != null && darkColorScheme != null - ? _generateDynamicColourSchemes(lightColorScheme, darkColorScheme) - : (null, null); + final (fixedLightScheme, fixedDarkScheme) = + lightColorScheme != null && darkColorScheme != null + ? _generateDynamicColourSchemes(lightColorScheme, darkColorScheme) + : (null, null); final isTablet = isTabletOrLarger(context); @@ -151,33 +143,29 @@ class _AppState extends ConsumerState { generalPrefs.systemColors && dynamicColorScheme != null ? dynamicColorScheme : ColorScheme.fromSeed( - seedColor: boardTheme.colors.darkSquare, - brightness: brightness, - ); + seedColor: boardTheme.colors.darkSquare, + brightness: brightness, + ); final cupertinoThemeData = CupertinoThemeData( primaryColor: colorScheme.primary, primaryContrastingColor: colorScheme.onPrimary, brightness: brightness, textTheme: CupertinoTheme.of(context).textTheme.copyWith( - primaryColor: colorScheme.primary, - textStyle: CupertinoTheme.of(context) - .textTheme - .textStyle - .copyWith(color: Styles.cupertinoLabelColor), - navTitleTextStyle: CupertinoTheme.of(context) - .textTheme - .navTitleTextStyle - .copyWith(color: Styles.cupertinoTitleColor), - navLargeTitleTextStyle: CupertinoTheme.of(context) - .textTheme - .navLargeTitleTextStyle - .copyWith(color: Styles.cupertinoTitleColor), - ), + primaryColor: colorScheme.primary, + textStyle: CupertinoTheme.of( + context, + ).textTheme.textStyle.copyWith(color: Styles.cupertinoLabelColor), + navTitleTextStyle: CupertinoTheme.of( + context, + ).textTheme.navTitleTextStyle.copyWith(color: Styles.cupertinoTitleColor), + navLargeTitleTextStyle: CupertinoTheme.of( + context, + ).textTheme.navLargeTitleTextStyle.copyWith(color: Styles.cupertinoTitleColor), + ), scaffoldBackgroundColor: Styles.cupertinoScaffoldColor, - barBackgroundColor: isTablet - ? Styles.cupertinoTabletAppBarColor - : Styles.cupertinoAppBarColor, + barBackgroundColor: + isTablet ? Styles.cupertinoTabletAppBarColor : Styles.cupertinoAppBarColor, ); return MaterialApp( @@ -187,47 +175,40 @@ class _AppState extends ConsumerState { locale: generalPrefs.locale, theme: ThemeData.from( colorScheme: colorScheme, - textTheme: Theme.of(context).platform == TargetPlatform.iOS - ? brightness == Brightness.light - ? Typography.blackCupertino - : Styles.whiteCupertinoTextTheme - : null, + textTheme: + Theme.of(context).platform == TargetPlatform.iOS + ? brightness == Brightness.light + ? Typography.blackCupertino + : Styles.whiteCupertinoTextTheme + : null, ).copyWith( cupertinoOverrideTheme: cupertinoThemeData, navigationBarTheme: NavigationBarTheme.of(context).copyWith( - height: remainingHeight < kSmallRemainingHeightLeftBoardThreshold - ? 60 - : null, + height: remainingHeight < kSmallRemainingHeightLeftBoardThreshold ? 60 : null, ), - extensions: [ - lichessCustomColors.harmonized(colorScheme), - ], + extensions: [lichessCustomColors.harmonized(colorScheme)], ), themeMode: switch (generalPrefs.themeMode) { BackgroundThemeMode.light => ThemeMode.light, BackgroundThemeMode.dark => ThemeMode.dark, BackgroundThemeMode.system => ThemeMode.system, }, - builder: Theme.of(context).platform == TargetPlatform.iOS - ? (context, child) { - return CupertinoTheme( - data: cupertinoThemeData, - child: IconTheme.merge( - data: IconThemeData( - color: CupertinoTheme.of(context) - .textTheme - .textStyle - .color, + builder: + Theme.of(context).platform == TargetPlatform.iOS + ? (context, child) { + return CupertinoTheme( + data: cupertinoThemeData, + child: IconTheme.merge( + data: IconThemeData( + color: CupertinoTheme.of(context).textTheme.textStyle.color, + ), + child: Material(child: child), ), - child: Material(child: child), - ), - ); - } - : null, + ); + } + : null, home: const BottomNavScaffold(), - navigatorObservers: [ - rootNavPageRouteObserver, - ], + navigatorObservers: [rootNavPageRouteObserver], ); }, ); @@ -249,28 +230,24 @@ class _AppState extends ConsumerState { final lightAdditionalColours = _extractAdditionalColours(lightBase); final darkAdditionalColours = _extractAdditionalColours(darkBase); - final lightScheme = - _insertAdditionalColours(lightBase, lightAdditionalColours); + final lightScheme = _insertAdditionalColours(lightBase, lightAdditionalColours); final darkScheme = _insertAdditionalColours(darkBase, darkAdditionalColours); return (lightScheme.harmonized(), darkScheme.harmonized()); } List _extractAdditionalColours(ColorScheme scheme) => [ - scheme.surface, - scheme.surfaceDim, - scheme.surfaceBright, - scheme.surfaceContainerLowest, - scheme.surfaceContainerLow, - scheme.surfaceContainer, - scheme.surfaceContainerHigh, - scheme.surfaceContainerHighest, - ]; + scheme.surface, + scheme.surfaceDim, + scheme.surfaceBright, + scheme.surfaceContainerLowest, + scheme.surfaceContainerLow, + scheme.surfaceContainer, + scheme.surfaceContainerHigh, + scheme.surfaceContainerHighest, +]; -ColorScheme _insertAdditionalColours( - ColorScheme scheme, - List additionalColours, -) => +ColorScheme _insertAdditionalColours(ColorScheme scheme, List additionalColours) => scheme.copyWith( surface: additionalColours[0], surfaceDim: additionalColours[1], diff --git a/lib/src/binding.dart b/lib/src/binding.dart index 6b42a43f8f..4b0baa6fb1 100644 --- a/lib/src/binding.dart +++ b/lib/src/binding.dart @@ -131,13 +131,10 @@ class AppLichessBinding extends LichessBinding { @override Future initializeFirebase() async { - await Firebase.initializeApp( - options: DefaultFirebaseOptions.currentPlatform, - ); + await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); if (kReleaseMode) { - FlutterError.onError = - FirebaseCrashlytics.instance.recordFlutterFatalError; + FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError; PlatformDispatcher.instance.onError = (error, stack) { FirebaseCrashlytics.instance.recordError(error, stack, fatal: true); return true; @@ -154,8 +151,7 @@ class AppLichessBinding extends LichessBinding { } @override - Stream get firebaseMessagingOnMessage => - FirebaseMessaging.onMessage; + Stream get firebaseMessagingOnMessage => FirebaseMessaging.onMessage; @override Stream get firebaseMessagingOnMessageOpenedApp => diff --git a/lib/src/constants.dart b/lib/src/constants.dart index 7d3bbf0b19..240db4322b 100644 --- a/lib/src/constants.dart +++ b/lib/src/constants.dart @@ -1,10 +1,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -const kLichessHost = String.fromEnvironment( - 'LICHESS_HOST', - defaultValue: 'lichess.dev', -); +const kLichessHost = String.fromEnvironment('LICHESS_HOST', defaultValue: 'lichess.dev'); const kLichessWSHost = String.fromEnvironment( 'LICHESS_WS_HOST', @@ -26,8 +23,7 @@ const kLichessOpeningExplorerHost = String.fromEnvironment( defaultValue: 'explorer.lichess.ovh', ); -const kLichessDevUser = - String.fromEnvironment('LICHESS_DEV_USER', defaultValue: 'lichess'); +const kLichessDevUser = String.fromEnvironment('LICHESS_DEV_USER', defaultValue: 'lichess'); const kLichessDevPassword = String.fromEnvironment('LICHESS_DEV_PASSWORD'); const kLichessClientId = 'lichess_mobile'; @@ -50,9 +46,8 @@ const kFlexGoldenRatioBase = 100000000000; const kFlexGoldenRatio = 161803398875; /// Use same box shadows as material widgets with elevation 1. -final List boardShadows = defaultTargetPlatform == TargetPlatform.iOS - ? [] - : kElevationToShadow[1]!; +final List boardShadows = + defaultTargetPlatform == TargetPlatform.iOS ? [] : kElevationToShadow[1]!; const kCardTextScaleFactor = 1.64; const kMaxClockTextScaleFactor = 1.94; diff --git a/lib/src/db/database.dart b/lib/src/db/database.dart index a5e17f389a..bde1c7f7ba 100644 --- a/lib/src/db/database.dart +++ b/lib/src/db/database.dart @@ -35,13 +35,8 @@ Future sqliteVersion(Ref ref) async { Future _getDatabaseVersion(Database db) async { try { - final versionStr = (await db.rawQuery('SELECT sqlite_version()')) - .first - .values - .first - .toString(); - final versionCells = - versionStr.split('.').map((i) => int.parse(i)).toList(); + final versionStr = (await db.rawQuery('SELECT sqlite_version()')).first.values.first.toString(); + final versionCells = versionStr.split('.').map((i) => int.parse(i)).toList(); return versionCells[0] * 100000 + versionCells[1] * 1000 + versionCells[2]; } catch (_) { return null; @@ -71,11 +66,7 @@ Future openAppDatabase(DatabaseFactory dbFactory, String path) async { _deleteOldEntries(db, 'puzzle', puzzleTTL), _deleteOldEntries(db, 'correspondence_game', corresGameTTL), _deleteOldEntries(db, 'game', gameTTL), - _deleteOldEntries( - db, - 'chat_read_messages', - chatReadMessagesTTL, - ), + _deleteOldEntries(db, 'chat_read_messages', chatReadMessagesTTL), ]); }, onCreate: (db, version) async { @@ -190,11 +181,7 @@ Future _deleteOldEntries(Database db, String table, Duration ttl) async { return; } - await db.delete( - table, - where: 'lastModified < ?', - whereArgs: [date.toIso8601String()], - ); + await db.delete(table, where: 'lastModified < ?', whereArgs: [date.toIso8601String()]); } Future _doesTableExist(Database db, String table) async { diff --git a/lib/src/db/openings_database.dart b/lib/src/db/openings_database.dart index 5b0e72aa3c..c9056620b4 100644 --- a/lib/src/db/openings_database.dart +++ b/lib/src/db/openings_database.dart @@ -45,17 +45,12 @@ Future _openDb(String path) async { }); // Copy from asset - final ByteData data = - await rootBundle.load(p.url.join('assets', 'chess_openings.db')); - final List bytes = - data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes); + final ByteData data = await rootBundle.load(p.url.join('assets', 'chess_openings.db')); + final List bytes = data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes); // Write and flush the bytes written await File(path).writeAsBytes(bytes, flush: true); } - return databaseFactory.openDatabase( - path, - options: OpenDatabaseOptions(readOnly: true), - ); + return databaseFactory.openDatabase(path, options: OpenDatabaseOptions(readOnly: true)); } diff --git a/lib/src/db/secure_storage.dart b/lib/src/db/secure_storage.dart index 67bd33b4d4..f0bb643a63 100644 --- a/lib/src/db/secure_storage.dart +++ b/lib/src/db/secure_storage.dart @@ -1,9 +1,9 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart'; AndroidOptions _getAndroidOptions() => const AndroidOptions( - encryptedSharedPreferences: true, - sharedPreferencesName: 'org.lichess.mobile.secure', - ); + encryptedSharedPreferences: true, + sharedPreferencesName: 'org.lichess.mobile.secure', +); class SecureStorage extends FlutterSecureStorage { const SecureStorage._({super.aOptions}); diff --git a/lib/src/init.dart b/lib/src/init.dart index 788d8040b4..145117567f 100644 --- a/lib/src/init.dart +++ b/lib/src/init.dart @@ -31,8 +31,7 @@ Future setupFirstLaunch() async { final appVersion = Version.parse(pInfo.version); final installedVersion = prefs.getString('installed_version'); - if (installedVersion == null || - Version.parse(installedVersion) != appVersion) { + if (installedVersion == null || Version.parse(installedVersion) != appVersion) { prefs.setString('installed_version', appVersion.canonicalizedVersion); } @@ -62,8 +61,7 @@ Future initializeLocalNotifications(Locale locale) async { ], ), ), - onDidReceiveNotificationResponse: - NotificationService.onDidReceiveNotificationResponse, + onDidReceiveNotificationResponse: NotificationService.onDidReceiveNotificationResponse, // onDidReceiveBackgroundNotificationResponse: notificationTapBackground, ); } @@ -74,8 +72,7 @@ Future preloadPieceImages() async { BoardPrefs boardPrefs = BoardPrefs.defaults; if (storedPrefs != null) { try { - boardPrefs = - BoardPrefs.fromJson(jsonDecode(storedPrefs) as Map); + boardPrefs = BoardPrefs.fromJson(jsonDecode(storedPrefs) as Map); } catch (e) { _logger.warning('Failed to decode board preferences: $e'); } @@ -95,13 +92,10 @@ Future androidDisplayInitialization(WidgetsBinding widgetsBinding) async { await DynamicColorPlugin.getCorePalette().then((value) { setCorePalette(value); - if (getCorePalette() != null && - prefs.getString(PrefCategory.board.storageKey) == null) { + if (getCorePalette() != null && prefs.getString(PrefCategory.board.storageKey) == null) { prefs.setString( PrefCategory.board.storageKey, - jsonEncode( - BoardPrefs.defaults.copyWith(boardTheme: BoardTheme.system), - ), + jsonEncode(BoardPrefs.defaults.copyWith(boardTheme: BoardTheme.system)), ); } }); @@ -130,17 +124,13 @@ Future androidDisplayInitialization(WidgetsBinding widgetsBinding) async { final List supported = await FlutterDisplayMode.supported; final DisplayMode active = await FlutterDisplayMode.active; - final List sameResolution = supported - .where( - (DisplayMode m) => m.width == active.width && m.height == active.height, - ) - .toList() - ..sort( - (DisplayMode a, DisplayMode b) => b.refreshRate.compareTo(a.refreshRate), - ); - - final DisplayMode mostOptimalMode = - sameResolution.isNotEmpty ? sameResolution.first : active; + final List sameResolution = + supported + .where((DisplayMode m) => m.width == active.width && m.height == active.height) + .toList() + ..sort((DisplayMode a, DisplayMode b) => b.refreshRate.compareTo(a.refreshRate)); + + final DisplayMode mostOptimalMode = sameResolution.isNotEmpty ? sameResolution.first : active; // This setting is per session. await FlutterDisplayMode.setPreferredMode(mostOptimalMode); diff --git a/lib/src/intl.dart b/lib/src/intl.dart index bda5051faf..b93d216850 100644 --- a/lib/src/intl.dart +++ b/lib/src/intl.dart @@ -12,11 +12,11 @@ Future setupIntl(WidgetsBinding widgetsBinding) async { final systemLocale = widgetsBinding.platformDispatcher.locale; // Get locale from shared preferences, if any - final json = LichessBinding.instance.sharedPreferences - .getString(PrefCategory.general.storageKey); - final generalPref = json != null - ? GeneralPrefs.fromJson(jsonDecode(json) as Map) - : GeneralPrefs.defaults; + final json = LichessBinding.instance.sharedPreferences.getString(PrefCategory.general.storageKey); + final generalPref = + json != null + ? GeneralPrefs.fromJson(jsonDecode(json) as Map) + : GeneralPrefs.defaults; final prefsLocale = generalPref.locale; final locale = prefsLocale ?? systemLocale; diff --git a/lib/src/localizations.dart b/lib/src/localizations.dart index d89448ccb3..62767000ad 100644 --- a/lib/src/localizations.dart +++ b/lib/src/localizations.dart @@ -5,10 +5,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'localizations.g.dart'; -typedef ActiveLocalizations = ({ - Locale locale, - AppLocalizations strings, -}); +typedef ActiveLocalizations = ({Locale locale, AppLocalizations strings}); @Riverpod(keepAlive: true) class Localizations extends _$Localizations { @@ -32,16 +29,10 @@ class Localizations extends _$Localizations { ActiveLocalizations _getLocale(GeneralPrefs prefs) { if (prefs.locale != null) { - return ( - locale: prefs.locale!, - strings: lookupAppLocalizations(prefs.locale!), - ); + return (locale: prefs.locale!, strings: lookupAppLocalizations(prefs.locale!)); } final locale = WidgetsBinding.instance.platformDispatcher.locale; - return ( - locale: locale, - strings: lookupAppLocalizations(locale), - ); + return (locale: locale, strings: lookupAppLocalizations(locale)); } } diff --git a/lib/src/log.dart b/lib/src/log.dart index 77eb73912c..5526448da9 100644 --- a/lib/src/log.dart +++ b/lib/src/log.dart @@ -5,10 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:logging/logging.dart'; // to see http requests and websocket connections in terminal -const _loggersToShowInTerminal = { - 'HttpClient', - 'Socket', -}; +const _loggersToShowInTerminal = {'HttpClient', 'Socket'}; /// Setup logging void setupLogging() { @@ -24,13 +21,11 @@ void setupLogging() { stackTrace: record.stackTrace, ); - if (_loggersToShowInTerminal.contains(record.loggerName) && - record.level >= Level.INFO) { + if (_loggersToShowInTerminal.contains(record.loggerName) && record.level >= Level.INFO) { debugPrint('[${record.loggerName}] ${record.message}'); } - if (!_loggersToShowInTerminal.contains(record.loggerName) && - record.level >= Level.WARNING) { + if (!_loggersToShowInTerminal.contains(record.loggerName) && record.level >= Level.WARNING) { debugPrint('[${record.loggerName}] ${record.message}'); } }); @@ -41,22 +36,12 @@ class ProviderLogger extends ProviderObserver { final _logger = Logger('Provider'); @override - void didAddProvider( - ProviderBase provider, - Object? value, - ProviderContainer container, - ) { - _logger.info( - '${provider.name ?? provider.runtimeType} initialized', - value, - ); + void didAddProvider(ProviderBase provider, Object? value, ProviderContainer container) { + _logger.info('${provider.name ?? provider.runtimeType} initialized', value); } @override - void didDisposeProvider( - ProviderBase provider, - ProviderContainer container, - ) { + void didDisposeProvider(ProviderBase provider, ProviderContainer container) { _logger.info('${provider.name ?? provider.runtimeType} disposed'); } @@ -67,10 +52,6 @@ class ProviderLogger extends ProviderObserver { StackTrace stackTrace, ProviderContainer container, ) { - _logger.severe( - '${provider.name ?? provider.runtimeType} error', - error, - stackTrace, - ); + _logger.severe('${provider.name ?? provider.runtimeType} error', error, stackTrace); } } diff --git a/lib/src/model/account/account_preferences.dart b/lib/src/model/account/account_preferences.dart index 27d1488df1..9984025ac0 100644 --- a/lib/src/model/account/account_preferences.dart +++ b/lib/src/model/account/account_preferences.dart @@ -10,40 +10,39 @@ import 'account_repository.dart'; part 'account_preferences.g.dart'; -typedef AccountPrefState = ({ - // game display - Zen zenMode, - PieceNotation pieceNotation, - BooleanPref showRatings, - // game behavior - BooleanPref premove, - AutoQueen autoQueen, - AutoThreefold autoThreefold, - Takeback takeback, - BooleanPref confirmResign, - SubmitMove submitMove, - // clock - Moretime moretime, - BooleanPref clockSound, - // privacy - BooleanPref follow, - Challenge challenge, -}); +typedef AccountPrefState = + ({ + // game display + Zen zenMode, + PieceNotation pieceNotation, + BooleanPref showRatings, + // game behavior + BooleanPref premove, + AutoQueen autoQueen, + AutoThreefold autoThreefold, + Takeback takeback, + BooleanPref confirmResign, + SubmitMove submitMove, + // clock + Moretime moretime, + BooleanPref clockSound, + // privacy + BooleanPref follow, + Challenge challenge, + }); /// A provider that tells if the user wants to see ratings in the app. @Riverpod(keepAlive: true) Future showRatingsPref(Ref ref) async { return ref.watch( - accountPreferencesProvider - .selectAsync((state) => state?.showRatings.value ?? true), + accountPreferencesProvider.selectAsync((state) => state?.showRatings.value ?? true), ); } @Riverpod(keepAlive: true) Future clockSound(Ref ref) async { return ref.watch( - accountPreferencesProvider - .selectAsync((state) => state?.clockSound.value ?? true), + accountPreferencesProvider.selectAsync((state) => state?.clockSound.value ?? true), ); } @@ -51,8 +50,7 @@ Future clockSound(Ref ref) async { Future pieceNotation(Ref ref) async { return ref.watch( accountPreferencesProvider.selectAsync( - (state) => - state?.pieceNotation ?? defaultAccountPreferences.pieceNotation, + (state) => state?.pieceNotation ?? defaultAccountPreferences.pieceNotation, ), ); } @@ -68,9 +66,7 @@ final defaultAccountPreferences = ( moretime: Moretime.always, clockSound: const BooleanPref(true), confirmResign: const BooleanPref(true), - submitMove: SubmitMove({ - SubmitMoveChoice.correspondence, - }), + submitMove: SubmitMove({SubmitMoveChoice.correspondence}), follow: const BooleanPref(true), challenge: Challenge.registered, ); @@ -90,41 +86,31 @@ class AccountPreferences extends _$AccountPreferences { } try { - return ref.withClient( - (client) => AccountRepository(client).getPreferences(), - ); + return ref.withClient((client) => AccountRepository(client).getPreferences()); } catch (e) { - debugPrint( - '[AccountPreferences] Error getting account preferences: $e', - ); + debugPrint('[AccountPreferences] Error getting account preferences: $e'); return defaultAccountPreferences; } } Future setZen(Zen value) => _setPref('zen', value); - Future setPieceNotation(PieceNotation value) => - _setPref('pieceNotation', value); + Future setPieceNotation(PieceNotation value) => _setPref('pieceNotation', value); Future setShowRatings(BooleanPref value) => _setPref('ratings', value); Future setPremove(BooleanPref value) => _setPref('premove', value); Future setTakeback(Takeback value) => _setPref('takeback', value); Future setAutoQueen(AutoQueen value) => _setPref('autoQueen', value); - Future setAutoThreefold(AutoThreefold value) => - _setPref('autoThreefold', value); + Future setAutoThreefold(AutoThreefold value) => _setPref('autoThreefold', value); Future setMoretime(Moretime value) => _setPref('moretime', value); - Future setClockSound(BooleanPref value) => - _setPref('clockSound', value); - Future setConfirmResign(BooleanPref value) => - _setPref('confirmResign', value); + Future setClockSound(BooleanPref value) => _setPref('clockSound', value); + Future setConfirmResign(BooleanPref value) => _setPref('confirmResign', value); Future setSubmitMove(SubmitMove value) => _setPref('submitMove', value); Future setFollow(BooleanPref value) => _setPref('follow', value); Future setChallenge(Challenge value) => _setPref('challenge', value); Future _setPref(String key, AccountPref value) async { await Future.delayed(const Duration(milliseconds: 200)); - await ref.withClient( - (client) => AccountRepository(client).setPreference(key, value), - ); + await ref.withClient((client) => AccountRepository(client).setPreference(key, value)); ref.invalidateSelf(); } } @@ -427,8 +413,7 @@ enum Challenge implements AccountPref { } class SubmitMove implements AccountPref { - SubmitMove(Iterable choices) - : choices = ISet(choices.toSet()); + SubmitMove(Iterable choices) : choices = ISet(choices.toSet()); final ISet choices; @@ -446,10 +431,8 @@ class SubmitMove implements AccountPref { return choices.map((choice) => choice.label(context)).join(', '); } - factory SubmitMove.fromInt(int value) => SubmitMove( - SubmitMoveChoice.values - .where((choice) => _bitPresent(value, choice.value)), - ); + factory SubmitMove.fromInt(int value) => + SubmitMove(SubmitMoveChoice.values.where((choice) => _bitPresent(value, choice.value))); } enum SubmitMoveChoice { diff --git a/lib/src/model/account/account_repository.dart b/lib/src/model/account/account_repository.dart index 2ab4a6370a..443199d070 100644 --- a/lib/src/model/account/account_repository.dart +++ b/lib/src/model/account/account_repository.dart @@ -62,44 +62,25 @@ class AccountRepository { final Logger _log = Logger('AccountRepository'); Future getProfile() { - return client.readJson( - Uri(path: '/api/account'), - mapper: User.fromServerJson, - ); + return client.readJson(Uri(path: '/api/account'), mapper: User.fromServerJson); } Future saveProfile(Map profile) async { final uri = Uri(path: '/account/profile'); - final response = await client.post( - uri, - headers: {'Accept': 'application/json'}, - body: profile, - ); + final response = await client.post(uri, headers: {'Accept': 'application/json'}, body: profile); if (response.statusCode >= 400) { - throw http.ClientException( - 'Failed to post save profile: ${response.statusCode}', - uri, - ); + throw http.ClientException('Failed to post save profile: ${response.statusCode}', uri); } } Future> getOngoingGames({int? nb}) { return client.readJson( - Uri( - path: '/api/account/playing', - queryParameters: nb != null - ? { - 'nb': nb.toString(), - } - : null, - ), + Uri(path: '/api/account/playing', queryParameters: nb != null ? {'nb': nb.toString()} : null), mapper: (Map json) { final list = json['nowPlaying']; if (list is! List) { - _log.severe( - 'Could not read json object as {nowPlaying: []}: expected a list.', - ); + _log.severe('Could not read json object as {nowPlaying: []}: expected a list.'); throw Exception('Could not read json object as {nowPlaying: []}'); } return list @@ -114,9 +95,7 @@ class AccountRepository { return client.readJson( Uri(path: '/api/account/preferences'), mapper: (Map json) { - return _accountPreferencesFromPick( - pick(json, 'prefs').required(), - ); + return _accountPreferencesFromPick(pick(json, 'prefs').required()); }, ); } @@ -127,43 +106,24 @@ class AccountRepository { final response = await client.post(uri, body: {prefKey: pref.toFormData}); if (response.statusCode >= 400) { - throw http.ClientException( - 'Failed to set preference: ${response.statusCode}', - uri, - ); + throw http.ClientException('Failed to set preference: ${response.statusCode}', uri); } } } AccountPrefState _accountPreferencesFromPick(RequiredPick pick) { return ( - zenMode: Zen.fromInt( - pick('zen').asIntOrThrow(), - ), - pieceNotation: PieceNotation.fromInt( - pick('pieceNotation').asIntOrThrow(), - ), + zenMode: Zen.fromInt(pick('zen').asIntOrThrow()), + pieceNotation: PieceNotation.fromInt(pick('pieceNotation').asIntOrThrow()), showRatings: BooleanPref.fromInt(pick('ratings').asIntOrThrow()), premove: BooleanPref(pick('premove').asBoolOrThrow()), - autoQueen: AutoQueen.fromInt( - pick('autoQueen').asIntOrThrow(), - ), - autoThreefold: AutoThreefold.fromInt( - pick('autoThreefold').asIntOrThrow(), - ), - takeback: Takeback.fromInt( - pick('takeback').asIntOrThrow(), - ), - moretime: Moretime.fromInt( - pick('moretime').asIntOrThrow(), - ), + autoQueen: AutoQueen.fromInt(pick('autoQueen').asIntOrThrow()), + autoThreefold: AutoThreefold.fromInt(pick('autoThreefold').asIntOrThrow()), + takeback: Takeback.fromInt(pick('takeback').asIntOrThrow()), + moretime: Moretime.fromInt(pick('moretime').asIntOrThrow()), clockSound: BooleanPref(pick('clockSound').asBoolOrThrow()), - confirmResign: BooleanPref.fromInt( - pick('confirmResign').asIntOrThrow(), - ), - submitMove: SubmitMove.fromInt( - pick('submitMove').asIntOrThrow(), - ), + confirmResign: BooleanPref.fromInt(pick('confirmResign').asIntOrThrow()), + submitMove: SubmitMove.fromInt(pick('submitMove').asIntOrThrow()), follow: BooleanPref(pick('follow').asBoolOrThrow()), challenge: Challenge.fromInt(pick('challenge').asIntOrThrow()), ); diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index 4ea9640325..6e6bdb08be 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -29,11 +29,7 @@ part 'analysis_controller.g.dart'; final _dateFormat = DateFormat('yyyy.MM.dd'); -typedef StandaloneAnalysis = ({ - String pgn, - Variant variant, - bool isComputerAnalysisAllowed, -}); +typedef StandaloneAnalysis = ({String pgn, Variant variant, bool isComputerAnalysisAllowed}); @freezed class AnalysisOptions with _$AnalysisOptions { @@ -51,8 +47,7 @@ class AnalysisOptions with _$AnalysisOptions { } @riverpod -class AnalysisController extends _$AnalysisController - implements PgnTreeNotifier { +class AnalysisController extends _$AnalysisController implements PgnTreeNotifier { late Root _root; late Variant _variant; @@ -71,8 +66,7 @@ class AnalysisController extends _$AnalysisController late final Division? division; if (options.gameId != null) { - final game = - await ref.watch(archivedGameProvider(id: options.gameId!).future); + final game = await ref.watch(archivedGameProvider(id: options.gameId!).future); _variant = game.meta.variant; pgn = game.makePgn(); opening = game.data.opening; @@ -91,27 +85,30 @@ class AnalysisController extends _$AnalysisController final game = PgnGame.parsePgn( pgn, - initHeaders: () => options.isLichessGameAnalysis - ? {} - : { - 'Event': '?', - 'Site': '?', - 'Date': _dateFormat.format(DateTime.now()), - 'Round': '?', - 'White': '?', - 'Black': '?', - 'Result': '*', - 'WhiteElo': '?', - 'BlackElo': '?', - }, + initHeaders: + () => + options.isLichessGameAnalysis + ? {} + : { + 'Event': '?', + 'Site': '?', + 'Date': _dateFormat.format(DateTime.now()), + 'Round': '?', + 'White': '?', + 'Black': '?', + 'Result': '*', + 'WhiteElo': '?', + 'BlackElo': '?', + }, ); final pgnHeaders = IMap(game.headers); final rootComments = IList(game.comments.map((c) => PgnComment.fromPgn(c))); - final isComputerAnalysisAllowed = options.isLichessGameAnalysis - ? pgnHeaders['Result'] != '*' - : options.standalone!.isComputerAnalysisAllowed; + final isComputerAnalysisAllowed = + options.isLichessGameAnalysis + ? pgnHeaders['Result'] != '*' + : options.standalone!.isComputerAnalysisAllowed; final List> openingFutures = []; @@ -122,8 +119,7 @@ class AnalysisController extends _$AnalysisController onVisitNode: (root, branch, isMainline) { if (isMainline && options.initialMoveCursor != null && - branch.position.ply <= - root.position.ply + options.initialMoveCursor!) { + branch.position.ply <= root.position.ply + options.initialMoveCursor!) { path = path + branch.id; lastMove = branch.sanMove.move; } @@ -134,26 +130,27 @@ class AnalysisController extends _$AnalysisController ); // wait for the opening to be fetched to recompute the branch opening - Future.wait(openingFutures).then((list) { - bool hasOpening = false; - for (final updated in list) { - if (updated != null) { - hasOpening = true; - final (path, opening) = updated; - _root.updateAt(path, (node) => node.opening = opening); - } - } - return hasOpening; - }).then((hasOpening) { - if (hasOpening) { - scheduleMicrotask(() { - _setPath(state.requireValue.currentPath); + Future.wait(openingFutures) + .then((list) { + bool hasOpening = false; + for (final updated in list) { + if (updated != null) { + hasOpening = true; + final (path, opening) = updated; + _root.updateAt(path, (node) => node.opening = opening); + } + } + return hasOpening; + }) + .then((hasOpening) { + if (hasOpening) { + scheduleMicrotask(() { + _setPath(state.requireValue.currentPath); + }); + } }); - } - }); - final currentPath = - options.initialMoveCursor == null ? _root.mainlinePath : path; + final currentPath = options.initialMoveCursor == null ? _root.mainlinePath : path; final currentNode = _root.nodeAt(currentPath); // don't use ref.watch here: we don't want to invalidate state when the @@ -168,12 +165,10 @@ class AnalysisController extends _$AnalysisController if (isEngineAllowed) { evaluationService.disposeEngine(); } - serverAnalysisService.lastAnalysisEvent - .removeListener(_listenToServerAnalysisEvents); + serverAnalysisService.lastAnalysisEvent.removeListener(_listenToServerAnalysisEvents); }); - serverAnalysisService.lastAnalysisEvent - .addListener(_listenToServerAnalysisEvents); + serverAnalysisService.lastAnalysisEvent.addListener(_listenToServerAnalysisEvents); final analysisState = AnalysisState( variant: _variant, @@ -198,35 +193,31 @@ class AnalysisController extends _$AnalysisController if (analysisState.isEngineAvailable) { evaluationService .initEngine( - _evaluationContext, - options: EvaluationOptions( - multiPv: prefs.numEvalLines, - cores: prefs.numEngineCores, - searchTime: prefs.engineSearchTime, - ), - ) + _evaluationContext, + options: EvaluationOptions( + multiPv: prefs.numEvalLines, + cores: prefs.numEngineCores, + searchTime: prefs.engineSearchTime, + ), + ) .then((_) { - _startEngineEvalTimer = Timer(const Duration(milliseconds: 250), () { - _startEngineEval(); - }); - }); + _startEngineEvalTimer = Timer(const Duration(milliseconds: 250), () { + _startEngineEval(); + }); + }); } return analysisState; } - EvaluationContext get _evaluationContext => EvaluationContext( - variant: _variant, - initialPosition: _root.position, - ); + EvaluationContext get _evaluationContext => + EvaluationContext(variant: _variant, initialPosition: _root.position); void onUserMove(NormalMove move, {bool shouldReplace = false}) { if (!state.requireValue.position.isLegal(move)) return; if (isPromotionPawnMove(state.requireValue.position, move)) { - state = AsyncValue.data( - state.requireValue.copyWith(promotionMove: move), - ); + state = AsyncValue.data(state.requireValue.copyWith(promotionMove: move)); return; } @@ -236,11 +227,7 @@ class AnalysisController extends _$AnalysisController replace: shouldReplace, ); if (newPath != null) { - _setPath( - newPath, - shouldRecomputeRootView: isNewNode, - shouldForceShowVariation: true, - ); + _setPath(newPath, shouldRecomputeRootView: isNewNode, shouldForceShowVariation: true); } } @@ -260,8 +247,7 @@ class AnalysisController extends _$AnalysisController final curState = state.requireValue; if (!curState.currentNode.hasChild) return; _setPath( - curState.currentPath + - _root.nodeAt(curState.currentPath).children.first.id, + curState.currentPath + _root.nodeAt(curState.currentPath).children.first.id, replaying: true, ); } @@ -307,8 +293,7 @@ class AnalysisController extends _$AnalysisController void expandVariations(UciPath path) { final node = _root.nodeAt(path); - final childrenToShow = - _root.isOnMainline(path) ? node.children.skip(1) : node.children; + final childrenToShow = _root.isOnMainline(path) ? node.children.skip(1) : node.children; for (final child in childrenToShow) { child.isCollapsed = false; @@ -335,10 +320,7 @@ class AnalysisController extends _$AnalysisController _root.promoteAt(path, toMainline: toMainline); final curState = state.requireValue; state = AsyncData( - curState.copyWith( - isOnMainline: _root.isOnMainline(curState.currentPath), - root: _root.view, - ), + curState.copyWith(isOnMainline: _root.isOnMainline(curState.currentPath), root: _root.view), ); } @@ -352,17 +334,13 @@ class AnalysisController extends _$AnalysisController /// /// Acts both on local evaluation and server analysis. Future toggleComputerAnalysis() async { - await ref - .read(analysisPreferencesProvider.notifier) - .toggleEnableComputerAnalysis(); + await ref.read(analysisPreferencesProvider.notifier).toggleEnableComputerAnalysis(); final curState = state.requireValue; final engineWasAvailable = curState.isEngineAvailable; state = AsyncData( - curState.copyWith( - isComputerAnalysisEnabled: !curState.isComputerAnalysisEnabled, - ), + curState.copyWith(isComputerAnalysisEnabled: !curState.isComputerAnalysisEnabled), ); final computerAllowed = state.requireValue.isComputerAnalysisEnabled; @@ -373,9 +351,7 @@ class AnalysisController extends _$AnalysisController /// Toggles the local evaluation on/off. Future toggleLocalEvaluation() async { - await ref - .read(analysisPreferencesProvider.notifier) - .toggleEnableLocalEvaluation(); + await ref.read(analysisPreferencesProvider.notifier).toggleEnableLocalEvaluation(); state = AsyncData( state.requireValue.copyWith( @@ -385,7 +361,9 @@ class AnalysisController extends _$AnalysisController if (state.requireValue.isEngineAvailable) { final prefs = ref.read(analysisPreferencesProvider); - await ref.read(evaluationServiceProvider).initEngine( + await ref + .read(evaluationServiceProvider) + .initEngine( _evaluationContext, options: EvaluationOptions( multiPv: prefs.numEvalLines, @@ -401,11 +379,11 @@ class AnalysisController extends _$AnalysisController } void setNumEvalLines(int numEvalLines) { - ref - .read(analysisPreferencesProvider.notifier) - .setNumEvalLines(numEvalLines); + ref.read(analysisPreferencesProvider.notifier).setNumEvalLines(numEvalLines); - ref.read(evaluationServiceProvider).setOptions( + ref + .read(evaluationServiceProvider) + .setOptions( EvaluationOptions( multiPv: numEvalLines, cores: ref.read(analysisPreferencesProvider).numEngineCores, @@ -418,8 +396,7 @@ class AnalysisController extends _$AnalysisController final curState = state.requireValue; state = AsyncData( curState.copyWith( - currentNode: - AnalysisCurrentNode.fromNode(_root.nodeAt(curState.currentPath)), + currentNode: AnalysisCurrentNode.fromNode(_root.nodeAt(curState.currentPath)), ), ); @@ -427,11 +404,11 @@ class AnalysisController extends _$AnalysisController } void setEngineCores(int numEngineCores) { - ref - .read(analysisPreferencesProvider.notifier) - .setEngineCores(numEngineCores); + ref.read(analysisPreferencesProvider.notifier).setEngineCores(numEngineCores); - ref.read(evaluationServiceProvider).setOptions( + ref + .read(evaluationServiceProvider) + .setOptions( EvaluationOptions( multiPv: ref.read(analysisPreferencesProvider).numEvalLines, cores: numEngineCores, @@ -443,11 +420,11 @@ class AnalysisController extends _$AnalysisController } void setEngineSearchTime(Duration searchTime) { - ref - .read(analysisPreferencesProvider.notifier) - .setEngineSearchTime(searchTime); + ref.read(analysisPreferencesProvider.notifier).setEngineSearchTime(searchTime); - ref.read(evaluationServiceProvider).setOptions( + ref + .read(evaluationServiceProvider) + .setOptions( EvaluationOptions( multiPv: ref.read(analysisPreferencesProvider).numEvalLines, cores: ref.read(analysisPreferencesProvider).numEngineCores, @@ -505,9 +482,7 @@ class AnalysisController extends _$AnalysisController final (currentNode, opening) = _nodeOpeningAt(_root, path); // always show variation if the user plays a move - if (shouldForceShowVariation && - currentNode is Branch && - currentNode.isCollapsed) { + if (shouldForceShowVariation && currentNode is Branch && currentNode.isCollapsed) { _root.updateAt(path, (node) { if (node is Branch) node.isCollapsed = false; }); @@ -516,9 +491,8 @@ class AnalysisController extends _$AnalysisController // root view is only used to display move list, so we need to // recompute the root view only when the nodelist length changes // or a variation is hidden/shown - final rootView = shouldForceShowVariation || shouldRecomputeRootView - ? _root.view - : curState.root; + final rootView = + shouldForceShowVariation || shouldRecomputeRootView ? _root.view : curState.root; final isForward = path.size > curState.currentPath.size; if (currentNode is Branch) { @@ -526,9 +500,7 @@ class AnalysisController extends _$AnalysisController if (isForward) { final isCheck = currentNode.sanMove.isCheck; if (currentNode.sanMove.isCapture) { - ref - .read(moveFeedbackServiceProvider) - .captureFeedback(check: isCheck); + ref.read(moveFeedbackServiceProvider).captureFeedback(check: isCheck); } else { ref.read(moveFeedbackServiceProvider).moveFeedback(check: isCheck); } @@ -581,18 +553,14 @@ class AnalysisController extends _$AnalysisController } } - Future<(UciPath, FullOpening)?> _fetchOpening( - Node fromNode, - UciPath path, - ) async { + Future<(UciPath, FullOpening)?> _fetchOpening(Node fromNode, UciPath path) async { if (!kOpeningAllowedVariants.contains(_variant)) return null; final moves = fromNode.branchesOn(path).map((node) => node.sanMove.move); if (moves.isEmpty) return null; if (moves.length > 40) return null; - final opening = - await ref.read(openingServiceProvider).fetchFromMoves(moves); + final opening = await ref.read(openingServiceProvider).fetchFromMoves(moves); if (opening != null) { return (path, opening); } @@ -605,9 +573,7 @@ class AnalysisController extends _$AnalysisController final curState = state.requireValue; if (curState.currentPath == path) { state = AsyncData( - curState.copyWith( - currentNode: AnalysisCurrentNode.fromNode(_root.nodeAt(path)), - ), + curState.copyWith(currentNode: AnalysisCurrentNode.fromNode(_root.nodeAt(path))), ); } } @@ -623,9 +589,7 @@ class AnalysisController extends _$AnalysisController initialPositionEval: _root.eval, shouldEmit: (work) => work.path == state.valueOrNull?.currentPath, ) - ?.forEach( - (t) => _root.updateAt(t.$1.path, (node) => node.eval = t.$2), - ); + ?.forEach((t) => _root.updateAt(t.$1.path, (node) => node.eval = t.$2)); } void _debouncedStartEngineEval() { @@ -640,26 +604,22 @@ class AnalysisController extends _$AnalysisController final curState = state.requireValue; state = AsyncData( curState.copyWith( - currentNode: - AnalysisCurrentNode.fromNode(_root.nodeAt(curState.currentPath)), + currentNode: AnalysisCurrentNode.fromNode(_root.nodeAt(curState.currentPath)), ), ); } void _listenToServerAnalysisEvents() { - final event = - ref.read(serverAnalysisServiceProvider).lastAnalysisEvent.value; + final event = ref.read(serverAnalysisServiceProvider).lastAnalysisEvent.value; if (event != null && event.$1 == state.requireValue.gameId) { _mergeOngoingAnalysis(_root, event.$2.tree); state = AsyncData( state.requireValue.copyWith( acplChartData: _makeAcplChartData(), - playersAnalysis: event.$2.analysis != null - ? ( - white: event.$2.analysis!.white, - black: event.$2.analysis!.black - ) - : null, + playersAnalysis: + event.$2.analysis != null + ? (white: event.$2.analysis!.white, black: event.$2.analysis!.black) + : null, root: _root.view, ), ); @@ -670,19 +630,18 @@ class AnalysisController extends _$AnalysisController final eval = n2['eval'] as Map?; final cp = eval?['cp'] as int?; final mate = eval?['mate'] as int?; - final pgnEval = cp != null - ? PgnEvaluation.pawns(pawns: cpToPawns(cp)) - : mate != null + final pgnEval = + cp != null + ? PgnEvaluation.pawns(pawns: cpToPawns(cp)) + : mate != null ? PgnEvaluation.mate(mate: mate) : null; final glyphs = n2['glyphs'] as List?; final glyph = glyphs?.first as Map?; final comments = n2['comments'] as List?; - final comment = - (comments?.first as Map?)?['text'] as String?; + final comment = (comments?.first as Map?)?['text'] as String?; final children = n2['children'] as List? ?? []; - final pgnComment = - pgnEval != null ? PgnComment(eval: pgnEval, text: comment) : null; + final pgnComment = pgnEval != null ? PgnComment(eval: pgnEval, text: comment) : null; if (n1 is Branch) { if (pgnComment != null) { if (n1.lichessAnalysisComments == null) { @@ -723,34 +682,32 @@ class AnalysisController extends _$AnalysisController } final list = _root.mainline .map( - (node) => ( - node.position.isCheckmate, - node.position.turn, - node.lichessAnalysisComments - ?.firstWhereOrNull((c) => c.eval != null) - ?.eval - ), - ) - .map( - (el) { - final (isCheckmate, side, eval) = el; - return eval != null - ? ExternalEval( + (node) => ( + node.position.isCheckmate, + node.position.turn, + node.lichessAnalysisComments?.firstWhereOrNull((c) => c.eval != null)?.eval, + ), + ) + .map((el) { + final (isCheckmate, side, eval) = el; + return eval != null + ? ExternalEval( cp: eval.pawns != null ? cpFromPawns(eval.pawns!) : null, mate: eval.mate, depth: eval.depth, ) - : ExternalEval( + : ExternalEval( cp: null, // hack to display checkmate as the max eval - mate: isCheckmate - ? side == Side.white - ? -1 - : 1 - : null, + mate: + isCheckmate + ? side == Side.white + ? -1 + : 1 + : null, ); - }, - ).toList(growable: false); + }) + .toList(growable: false); return list.isEmpty ? null : IList(list); } } @@ -833,10 +790,8 @@ class AnalysisState with _$AnalysisState { /// Whether the analysis is for a lichess game. bool get isLichessGameAnalysis => gameId != null; - IMap> get validMoves => makeLegalMoves( - currentNode.position, - isChess960: variant == Variant.chess960, - ); + IMap> get validMoves => + makeLegalMoves(currentNode.position, isChess960: variant == Variant.chess960); /// Whether the user can request server analysis. /// @@ -852,17 +807,14 @@ class AnalysisState with _$AnalysisState { /// Whether an evaluation can be available bool get hasAvailableEval => isEngineAvailable || - (isComputerAnalysisAllowedAndEnabled && - acplChartData != null && - acplChartData!.isNotEmpty); + (isComputerAnalysisAllowedAndEnabled && acplChartData != null && acplChartData!.isNotEmpty); bool get isComputerAnalysisAllowedAndEnabled => isComputerAnalysisAllowed && isComputerAnalysisEnabled; /// Whether the engine is allowed for this analysis and variant. bool get isEngineAllowed => - isComputerAnalysisAllowedAndEnabled && - engineSupportedVariants.contains(variant); + isComputerAnalysisAllowedAndEnabled && engineSupportedVariants.contains(variant); /// Whether the engine is available for evaluation bool get isEngineAvailable => isEngineAllowed && isLocalEvaluationEnabled; @@ -872,11 +824,11 @@ class AnalysisState with _$AnalysisState { bool get canGoBack => currentPath.size > UciPath.empty.size; EngineGaugeParams get engineGaugeParams => ( - orientation: pov, - isLocalEngineAvailable: isEngineAvailable, - position: position, - savedEval: currentNode.eval ?? currentNode.serverEval, - ); + orientation: pov, + isLocalEngineAvailable: isEngineAvailable, + position: position, + savedEval: currentNode.eval ?? currentNode.serverEval, + ); } @freezed @@ -925,14 +877,13 @@ class AnalysisCurrentNode with _$AnalysisCurrentNode { /// /// For now we only trust the eval coming from lichess analysis. ExternalEval? get serverEval { - final pgnEval = - lichessAnalysisComments?.firstWhereOrNull((c) => c.eval != null)?.eval; + final pgnEval = lichessAnalysisComments?.firstWhereOrNull((c) => c.eval != null)?.eval; return pgnEval != null ? ExternalEval( - cp: pgnEval.pawns != null ? cpFromPawns(pgnEval.pawns!) : null, - mate: pgnEval.mate, - depth: pgnEval.depth, - ) + cp: pgnEval.pawns != null ? cpFromPawns(pgnEval.pawns!) : null, + mate: pgnEval.mate, + depth: pgnEval.depth, + ) : null; } } diff --git a/lib/src/model/analysis/analysis_preferences.dart b/lib/src/model/analysis/analysis_preferences.dart index c3b682536b..0916da00a0 100644 --- a/lib/src/model/analysis/analysis_preferences.dart +++ b/lib/src/model/analysis/analysis_preferences.dart @@ -7,8 +7,7 @@ part 'analysis_preferences.freezed.dart'; part 'analysis_preferences.g.dart'; @riverpod -class AnalysisPreferences extends _$AnalysisPreferences - with PreferencesStorage { +class AnalysisPreferences extends _$AnalysisPreferences with PreferencesStorage { // ignore: avoid_public_notifier_properties @override final prefCategory = PrefCategory.analysis; @@ -18,8 +17,7 @@ class AnalysisPreferences extends _$AnalysisPreferences AnalysisPrefs get defaults => AnalysisPrefs.defaults; @override - AnalysisPrefs fromJson(Map json) => - AnalysisPrefs.fromJson(json); + AnalysisPrefs fromJson(Map json) => AnalysisPrefs.fromJson(json); @override AnalysisPrefs build() { @@ -27,77 +25,41 @@ class AnalysisPreferences extends _$AnalysisPreferences } Future toggleEnableComputerAnalysis() { - return save( - state.copyWith( - enableComputerAnalysis: !state.enableComputerAnalysis, - ), - ); + return save(state.copyWith(enableComputerAnalysis: !state.enableComputerAnalysis)); } Future toggleEnableLocalEvaluation() { - return save( - state.copyWith( - enableLocalEvaluation: !state.enableLocalEvaluation, - ), - ); + return save(state.copyWith(enableLocalEvaluation: !state.enableLocalEvaluation)); } Future toggleShowEvaluationGauge() { - return save( - state.copyWith( - showEvaluationGauge: !state.showEvaluationGauge, - ), - ); + return save(state.copyWith(showEvaluationGauge: !state.showEvaluationGauge)); } Future toggleAnnotations() { - return save( - state.copyWith( - showAnnotations: !state.showAnnotations, - ), - ); + return save(state.copyWith(showAnnotations: !state.showAnnotations)); } Future togglePgnComments() { - return save( - state.copyWith( - showPgnComments: !state.showPgnComments, - ), - ); + return save(state.copyWith(showPgnComments: !state.showPgnComments)); } Future toggleShowBestMoveArrow() { - return save( - state.copyWith( - showBestMoveArrow: !state.showBestMoveArrow, - ), - ); + return save(state.copyWith(showBestMoveArrow: !state.showBestMoveArrow)); } Future setNumEvalLines(int numEvalLines) { assert(numEvalLines >= 0 && numEvalLines <= 3); - return save( - state.copyWith( - numEvalLines: numEvalLines, - ), - ); + return save(state.copyWith(numEvalLines: numEvalLines)); } Future setEngineCores(int numEngineCores) { assert(numEngineCores >= 1 && numEngineCores <= maxEngineCores); - return save( - state.copyWith( - numEngineCores: numEngineCores, - ), - ); + return save(state.copyWith(numEngineCores: numEngineCores)); } Future setEngineSearchTime(Duration engineSearchTime) { - return save( - state.copyWith( - engineSearchTime: engineSearchTime, - ), - ); + return save(state.copyWith(engineSearchTime: engineSearchTime)); } } @@ -113,8 +75,7 @@ class AnalysisPrefs with _$AnalysisPrefs implements Serializable { required bool showAnnotations, required bool showPgnComments, @Assert('numEvalLines >= 0 && numEvalLines <= 3') required int numEvalLines, - @Assert('numEngineCores >= 1 && numEngineCores <= maxEngineCores') - required int numEngineCores, + @Assert('numEngineCores >= 1 && numEngineCores <= maxEngineCores') required int numEngineCores, @JsonKey( defaultValue: _searchTimeDefault, fromJson: _searchTimeFromJson, diff --git a/lib/src/model/analysis/opening_service.dart b/lib/src/model/analysis/opening_service.dart index 3d1f67a739..74c8a8bb9c 100644 --- a/lib/src/model/analysis/opening_service.dart +++ b/lib/src/model/analysis/opening_service.dart @@ -30,17 +30,9 @@ class OpeningService { Future fetchFromMoves(Iterable moves) async { final db = await _db; final movesString = moves - .map( - (move) => altCastles.containsKey(move.uci) - ? altCastles[move.uci] - : move.uci, - ) + .map((move) => altCastles.containsKey(move.uci) ? altCastles[move.uci] : move.uci) .join(' '); - final list = await db.query( - 'openings', - where: 'uci = ?', - whereArgs: [movesString], - ); + final list = await db.query('openings', where: 'uci = ?', whereArgs: [movesString]); final first = list.firstOrNull; if (first != null) { diff --git a/lib/src/model/analysis/server_analysis_service.dart b/lib/src/model/analysis/server_analysis_service.dart index ed73593547..c7926e294b 100644 --- a/lib/src/model/analysis/server_analysis_service.dart +++ b/lib/src/model/analysis/server_analysis_service.dart @@ -33,8 +33,7 @@ class ServerAnalysisService { ValueListenable get currentAnalysis => _currentAnalysis; /// The last analysis progress event received from the server. - ValueListenable<(GameAnyId, ServerEvalEvent)?> get lastAnalysisEvent => - _analysisProgress; + ValueListenable<(GameAnyId, ServerEvalEvent)?> get lastAnalysisEvent => _analysisProgress; /// Request server analysis for a game. /// @@ -51,8 +50,7 @@ class ServerAnalysisService { socketClient.stream.listen( (event) { if (event.topic == 'analysisProgress') { - final data = - ServerEvalEvent.fromJson(event.data as Map); + final data = ServerEvalEvent.fromJson(event.data as Map); _analysisProgress.value = (id, data); @@ -69,13 +67,11 @@ class ServerAnalysisService { _socketSubscription = null; }, cancelOnError: true, - ) + ), ); try { - await ref.withClient( - (client) => GameRepository(client).requestServerAnalysis(id.gameId), - ); + await ref.withClient((client) => GameRepository(client).requestServerAnalysis(id.gameId)); _currentAnalysis.value = id.gameId; } catch (e) { _socketSubscription?.$2.cancel(); @@ -100,8 +96,7 @@ class CurrentAnalysis extends _$CurrentAnalysis { } void _listener() { - final gameId = - ref.read(serverAnalysisServiceProvider).currentAnalysis.value; + final gameId = ref.read(serverAnalysisServiceProvider).currentAnalysis.value; if (state != gameId) { state = gameId; } diff --git a/lib/src/model/auth/auth_controller.dart b/lib/src/model/auth/auth_controller.dart index b4ced58fa7..52cedd91b1 100644 --- a/lib/src/model/auth/auth_controller.dart +++ b/lib/src/model/auth/auth_controller.dart @@ -21,8 +21,7 @@ class AuthController extends _$AuthController { final appAuth = ref.read(appAuthProvider); try { - final session = await ref - .withClient((client) => AuthRepository(client, appAuth).signIn()); + final session = await ref.withClient((client) => AuthRepository(client, appAuth).signIn()); await ref.read(authSessionProvider.notifier).update(session); @@ -47,9 +46,7 @@ class AuthController extends _$AuthController { try { await ref.read(notificationServiceProvider).unregister(); - await ref.withClient( - (client) => AuthRepository(client, appAuth).signOut(), - ); + await ref.withClient((client) => AuthRepository(client, appAuth).signOut()); await ref.read(authSessionProvider.notifier).delete(); // force reconnect to the current socket await ref.read(socketPoolProvider).currentClient.connect(); diff --git a/lib/src/model/auth/auth_repository.dart b/lib/src/model/auth/auth_repository.dart index 9d364ad31c..242d68ed45 100644 --- a/lib/src/model/auth/auth_repository.dart +++ b/lib/src/model/auth/auth_repository.dart @@ -21,11 +21,9 @@ FlutterAppAuth appAuth(Ref ref) { } class AuthRepository { - AuthRepository( - LichessClient client, - FlutterAppAuth appAuth, - ) : _client = client, - _appAuth = appAuth; + AuthRepository(LichessClient client, FlutterAppAuth appAuth) + : _client = client, + _appAuth = appAuth; final LichessClient _client; final Logger _log = Logger('AuthRepository'); @@ -61,25 +59,17 @@ class AuthRepository { final user = await _client.readJson( Uri(path: '/api/account'), - headers: { - 'Authorization': 'Bearer ${signBearerToken(token)}', - }, + headers: {'Authorization': 'Bearer ${signBearerToken(token)}'}, mapper: User.fromServerJson, ); - return AuthSessionState( - token: token, - user: user.lightUser, - ); + return AuthSessionState(token: token, user: user.lightUser); } Future signOut() async { final url = Uri(path: '/api/token'); final response = await _client.delete(Uri(path: '/api/token')); if (response.statusCode >= 400) { - throw http.ClientException( - 'Failed to delete token: ${response.statusCode}', - url, - ); + throw http.ClientException('Failed to delete token: ${response.statusCode}', url); } } } diff --git a/lib/src/model/auth/auth_session.dart b/lib/src/model/auth/auth_session.dart index d104df6edc..a5ae274926 100644 --- a/lib/src/model/auth/auth_session.dart +++ b/lib/src/model/auth/auth_session.dart @@ -30,11 +30,8 @@ class AuthSession extends _$AuthSession { @Freezed(fromJson: true, toJson: true) class AuthSessionState with _$AuthSessionState { - const factory AuthSessionState({ - required LightUser user, - required String token, - }) = _AuthSessionState; + const factory AuthSessionState({required LightUser user, required String token}) = + _AuthSessionState; - factory AuthSessionState.fromJson(Map json) => - _$AuthSessionStateFromJson(json); + factory AuthSessionState.fromJson(Map json) => _$AuthSessionStateFromJson(json); } diff --git a/lib/src/model/auth/session_storage.dart b/lib/src/model/auth/session_storage.dart index 0509aa3e64..2b33ba16b7 100644 --- a/lib/src/model/auth/session_storage.dart +++ b/lib/src/model/auth/session_storage.dart @@ -21,9 +21,7 @@ class SessionStorage { Future read() async { final string = await SecureStorage.instance.read(key: kSessionStorageKey); if (string != null) { - return AuthSessionState.fromJson( - jsonDecode(string) as Map, - ); + return AuthSessionState.fromJson(jsonDecode(string) as Map); } return null; } diff --git a/lib/src/model/board_editor/board_editor_controller.dart b/lib/src/model/board_editor/board_editor_controller.dart index 0b45be6ffb..3a6061a4ed 100644 --- a/lib/src/model/board_editor/board_editor_controller.dart +++ b/lib/src/model/board_editor/board_editor_controller.dart @@ -32,10 +32,7 @@ class BoardEditorController extends _$BoardEditorController { } void updateMode(EditorPointerMode mode, [Piece? pieceToAddOnEdit]) { - state = state.copyWith( - editorPointerMode: mode, - pieceToAddOnEdit: pieceToAddOnEdit, - ); + state = state.copyWith(editorPointerMode: mode, pieceToAddOnEdit: pieceToAddOnEdit); } void discardPiece(Square square) { @@ -44,9 +41,7 @@ class BoardEditorController extends _$BoardEditorController { void movePiece(Square? origin, Square destination, Piece piece) { if (origin != destination) { - _updatePosition( - state.pieces.remove(origin ?? destination).add(destination, piece), - ); + _updatePosition(state.pieces.remove(origin ?? destination).add(destination, piece)); } } @@ -60,9 +55,7 @@ class BoardEditorController extends _$BoardEditorController { } void flipBoard() { - state = state.copyWith( - orientation: state.orientation.opposite, - ); + state = state.copyWith(orientation: state.orientation.opposite); } void setSideToPlay(Side side) { @@ -85,8 +78,7 @@ class BoardEditorController extends _$BoardEditorController { /// For en passant to be possible, there needs to be an adjacent pawn which has moved two squares forward. /// So the two squares behind must be empty void checkEnPassant(Square square, int fileOffset) { - final adjacentSquare = - Square.fromCoords(square.file.offset(fileOffset)!, square.rank); + final adjacentSquare = Square.fromCoords(square.file.offset(fileOffset)!, square.rank); final targetSquare = Square.fromCoords( square.file.offset(fileOffset)!, square.rank.offset(side == Side.white ? 1 : -1)!, @@ -100,8 +92,7 @@ class BoardEditorController extends _$BoardEditorController { board.roleAt(adjacentSquare) == Role.pawn && board.sideAt(targetSquare) == null && board.sideAt(originSquare) == null) { - enPassantOptions = - enPassantOptions.union(SquareSet.fromSquare(targetSquare)); + enPassantOptions = enPassantOptions.union(SquareSet.fromSquare(targetSquare)); } } @@ -119,9 +110,7 @@ class BoardEditorController extends _$BoardEditorController { } void toggleEnPassantSquare(Square square) { - state = state.copyWith( - enPassantSquare: state.enPassantSquare == square ? null : square, - ); + state = state.copyWith(enPassantSquare: state.enPassantSquare == square ? null : square); } void _updatePosition(IMap pieces) { @@ -137,38 +126,29 @@ class BoardEditorController extends _$BoardEditorController { switch (castlingSide) { case CastlingSide.king: state = state.copyWith( - castlingRights: - state.castlingRights.add(CastlingRight.whiteKing, allowed), + castlingRights: state.castlingRights.add(CastlingRight.whiteKing, allowed), ); case CastlingSide.queen: state = state.copyWith( - castlingRights: - state.castlingRights.add(CastlingRight.whiteQueen, allowed), + castlingRights: state.castlingRights.add(CastlingRight.whiteQueen, allowed), ); } case Side.black: switch (castlingSide) { case CastlingSide.king: state = state.copyWith( - castlingRights: - state.castlingRights.add(CastlingRight.blackKing, allowed), + castlingRights: state.castlingRights.add(CastlingRight.blackKing, allowed), ); case CastlingSide.queen: state = state.copyWith( - castlingRights: - state.castlingRights.add(CastlingRight.blackQueen, allowed), + castlingRights: state.castlingRights.add(CastlingRight.blackQueen, allowed), ); } } } } -enum CastlingRight { - whiteKing, - whiteQueen, - blackKing, - blackQueen, -} +enum CastlingRight { whiteKing, whiteQueen, blackKing, blackQueen } @freezed class BoardEditorState with _$BoardEditorState { @@ -189,17 +169,16 @@ class BoardEditorState with _$BoardEditorState { required Piece? pieceToAddOnEdit, }) = _BoardEditorState; - bool isCastlingAllowed(Side side, CastlingSide castlingSide) => - switch (side) { - Side.white => switch (castlingSide) { - CastlingSide.king => castlingRights[CastlingRight.whiteKing]!, - CastlingSide.queen => castlingRights[CastlingRight.whiteQueen]!, - }, - Side.black => switch (castlingSide) { - CastlingSide.king => castlingRights[CastlingRight.blackKing]!, - CastlingSide.queen => castlingRights[CastlingRight.blackQueen]!, - }, - }; + bool isCastlingAllowed(Side side, CastlingSide castlingSide) => switch (side) { + Side.white => switch (castlingSide) { + CastlingSide.king => castlingRights[CastlingRight.whiteKing]!, + CastlingSide.queen => castlingRights[CastlingRight.whiteQueen]!, + }, + Side.black => switch (castlingSide) { + CastlingSide.king => castlingRights[CastlingRight.blackKing]!, + CastlingSide.queen => castlingRights[CastlingRight.blackQueen]!, + }, + }; /// Returns the castling rights part of the FEN string. /// @@ -211,15 +190,15 @@ class BoardEditorState with _$BoardEditorState { final Board board = Board.parseFen(writeFen(pieces.unlock)); for (final side in Side.values) { final backrankKing = SquareSet.backrankOf(side) & board.kings; - final rooksAndKings = (board.bySide(side) & SquareSet.backrankOf(side)) & - (board.rooks | board.kings); + final rooksAndKings = + (board.bySide(side) & SquareSet.backrankOf(side)) & (board.rooks | board.kings); for (final castlingSide in CastlingSide.values) { - final candidate = castlingSide == CastlingSide.king - ? rooksAndKings.squares.lastOrNull - : rooksAndKings.squares.firstOrNull; - final isCastlingPossible = candidate != null && - board.rooks.has(candidate) && - backrankKing.singleSquare != null; + final candidate = + castlingSide == CastlingSide.king + ? rooksAndKings.squares.lastOrNull + : rooksAndKings.squares.firstOrNull; + final isCastlingPossible = + candidate != null && board.rooks.has(candidate) && backrankKing.singleSquare != null; switch ((side, castlingSide)) { case (Side.white, CastlingSide.king): hasRook[CastlingRight.whiteKing] = isCastlingPossible; diff --git a/lib/src/model/broadcast/broadcast.dart b/lib/src/model/broadcast/broadcast.dart index 38a82ff9fd..aeb754ebad 100644 --- a/lib/src/model/broadcast/broadcast.dart +++ b/lib/src/model/broadcast/broadcast.dart @@ -5,11 +5,7 @@ import 'package:lichess_mobile/src/model/common/id.dart'; part 'broadcast.freezed.dart'; -typedef BroadcastList = ({ - IList active, - IList past, - int? nextPage, -}); +typedef BroadcastList = ({IList active, IList past, int? nextPage}); enum BroadcastResult { whiteWins, blackWins, draw, ongoing, noResultPgnTag } @@ -56,24 +52,19 @@ class BroadcastTournamentData with _$BroadcastTournamentData { }) = _BroadcastTournamentData; } -typedef BroadcastTournamentInformation = ({ - String? format, - String? timeControl, - String? players, - String? location, - BroadcastTournamentDates? dates, - Uri? website, -}); - -typedef BroadcastTournamentDates = ({ - DateTime startsAt, - DateTime? endsAt, -}); - -typedef BroadcastTournamentGroup = ({ - BroadcastTournamentId id, - String name, -}); +typedef BroadcastTournamentInformation = + ({ + String? format, + String? timeControl, + String? players, + String? location, + BroadcastTournamentDates? dates, + Uri? website, + }); + +typedef BroadcastTournamentDates = ({DateTime startsAt, DateTime? endsAt}); + +typedef BroadcastTournamentGroup = ({BroadcastTournamentId id, String name}); @freezed class BroadcastRound with _$BroadcastRound { @@ -87,10 +78,7 @@ class BroadcastRound with _$BroadcastRound { }) = _BroadcastRound; } -typedef BroadcastRoundWithGames = ({ - BroadcastRound round, - BroadcastRoundGames games, -}); +typedef BroadcastRoundWithGames = ({BroadcastRound round, BroadcastRoundGames games}); typedef BroadcastRoundGames = IMap; @@ -141,8 +129,4 @@ class BroadcastPlayerExtended with _$BroadcastPlayerExtended { }) = _BroadcastPlayerExtended; } -enum RoundStatus { - live, - finished, - upcoming, -} +enum RoundStatus { live, finished, upcoming } diff --git a/lib/src/model/broadcast/broadcast_game_controller.dart b/lib/src/model/broadcast/broadcast_game_controller.dart index 79ffab5cb1..ee14654f14 100644 --- a/lib/src/model/broadcast/broadcast_game_controller.dart +++ b/lib/src/model/broadcast/broadcast_game_controller.dart @@ -29,8 +29,7 @@ part 'broadcast_game_controller.freezed.dart'; part 'broadcast_game_controller.g.dart'; @riverpod -class BroadcastGameController extends _$BroadcastGameController - implements PgnTreeNotifier { +class BroadcastGameController extends _$BroadcastGameController implements PgnTreeNotifier { static Uri broadcastSocketUri(BroadcastRoundId broadcastRoundId) => Uri(path: 'study/$broadcastRoundId/socket/v6'); @@ -49,10 +48,7 @@ class BroadcastGameController extends _$BroadcastGameController Object? _key = Object(); @override - Future build( - BroadcastRoundId roundId, - BroadcastGameId gameId, - ) async { + Future build(BroadcastRoundId roundId, BroadcastGameId gameId) async { _socketClient = ref .watch(socketPoolProvider) .open(BroadcastGameController.broadcastSocketUri(roundId)); @@ -126,18 +122,18 @@ class BroadcastGameController extends _$BroadcastGameController if (broadcastState.isLocalEvaluationEnabled) { evaluationService .initEngine( - _evaluationContext, - options: EvaluationOptions( - multiPv: prefs.numEvalLines, - cores: prefs.numEngineCores, - searchTime: ref.read(analysisPreferencesProvider).engineSearchTime, - ), - ) + _evaluationContext, + options: EvaluationOptions( + multiPv: prefs.numEvalLines, + cores: prefs.numEngineCores, + searchTime: ref.read(analysisPreferencesProvider).engineSearchTime, + ), + ) .then((_) { - _startEngineEvalTimer = Timer(const Duration(milliseconds: 250), () { - _startEngineEval(); - }); - }); + _startEngineEvalTimer = Timer(const Duration(milliseconds: 250), () { + _startEngineEval(); + }); + }); } return broadcastState; @@ -157,8 +153,7 @@ class BroadcastGameController extends _$BroadcastGameController final wasOnLivePath = curState.broadcastLivePath == curState.currentPath; final game = PgnGame.parsePgn(pgn); final pgnHeaders = IMap(game.headers); - final rootComments = - IList(game.comments.map((c) => PgnComment.fromPgn(c))); + final rootComments = IList(game.comments.map((c) => PgnComment.fromPgn(c))); final newRoot = Root.fromPgnGame(game); @@ -169,8 +164,7 @@ class BroadcastGameController extends _$BroadcastGameController _root = newRoot; - final newCurrentPath = - wasOnLivePath ? broadcastPath : curState.currentPath; + final newCurrentPath = wasOnLivePath ? broadcastPath : curState.currentPath; state = AsyncData( state.requireValue.copyWith( currentPath: newCurrentPath, @@ -199,8 +193,7 @@ class BroadcastGameController extends _$BroadcastGameController } void _handleAddNodeEvent(SocketEvent event) { - final broadcastGameId = - pick(event.data, 'p', 'chapterId').asBroadcastGameIdOrThrow(); + final broadcastGameId = pick(event.data, 'p', 'chapterId').asBroadcastGameIdOrThrow(); // We check if the event is for this game if (broadcastGameId != gameId) return; @@ -215,18 +208,14 @@ class BroadcastGameController extends _$BroadcastGameController // The path for the node that was received final path = pick(event.data, 'p', 'path').asUciPathOrThrow(); final uciMove = pick(event.data, 'n', 'uci').asUciMoveOrThrow(); - final clock = - pick(event.data, 'n', 'clock').asDurationFromCentiSecondsOrNull(); + final clock = pick(event.data, 'n', 'clock').asDurationFromCentiSecondsOrNull(); final (newPath, isNewNode) = _root.addMoveAt(path, uciMove, clock: clock); if (newPath != null && isNewNode == false) { _root.updateAt(newPath, (node) { if (node is Branch) { - node.comments = [ - ...node.comments ?? [], - PgnComment(clock: clock), - ]; + node.comments = [...node.comments ?? [], PgnComment(clock: clock)]; } }); } @@ -245,30 +234,22 @@ class BroadcastGameController extends _$BroadcastGameController } void _handleSetTagsEvent(SocketEvent event) { - final broadcastGameId = - pick(event.data, 'chapterId').asBroadcastGameIdOrThrow(); + final broadcastGameId = pick(event.data, 'chapterId').asBroadcastGameIdOrThrow(); // We check if the event is for this game if (broadcastGameId != gameId) return; - final pgnHeadersEntries = pick(event.data, 'tags').asListOrThrow( - (header) => MapEntry( - header(0).asStringOrThrow(), - header(1).asStringOrThrow(), - ), - ); + final pgnHeadersEntries = pick( + event.data, + 'tags', + ).asListOrThrow((header) => MapEntry(header(0).asStringOrThrow(), header(1).asStringOrThrow())); - final pgnHeaders = - state.requireValue.pgnHeaders.addEntries(pgnHeadersEntries); - state = AsyncData( - state.requireValue.copyWith(pgnHeaders: pgnHeaders), - ); + final pgnHeaders = state.requireValue.pgnHeaders.addEntries(pgnHeadersEntries); + state = AsyncData(state.requireValue.copyWith(pgnHeaders: pgnHeaders)); } - EvaluationContext get _evaluationContext => EvaluationContext( - variant: Variant.standard, - initialPosition: _root.position, - ); + EvaluationContext get _evaluationContext => + EvaluationContext(variant: Variant.standard, initialPosition: _root.position); void onUserMove(NormalMove move) { if (!state.hasValue) return; @@ -280,16 +261,9 @@ class BroadcastGameController extends _$BroadcastGameController return; } - final (newPath, isNewNode) = _root.addMoveAt( - state.requireValue.currentPath, - move, - ); + final (newPath, isNewNode) = _root.addMoveAt(state.requireValue.currentPath, move); if (newPath != null) { - _setPath( - newPath, - shouldRecomputeRootView: isNewNode, - shouldForceShowVariation: true, - ); + _setPath(newPath, shouldRecomputeRootView: isNewNode, shouldForceShowVariation: true); } } @@ -344,9 +318,7 @@ class BroadcastGameController extends _$BroadcastGameController void toggleBoard() { if (!state.hasValue) return; - state = AsyncData( - state.requireValue.copyWith(pov: state.requireValue.pov.opposite), - ); + state = AsyncData(state.requireValue.copyWith(pov: state.requireValue.pov.opposite)); } void userPrevious() { @@ -407,9 +379,7 @@ class BroadcastGameController extends _$BroadcastGameController Future toggleLocalEvaluation() async { if (!state.hasValue) return; - ref - .read(analysisPreferencesProvider.notifier) - .toggleEnableLocalEvaluation(); + ref.read(analysisPreferencesProvider.notifier).toggleEnableLocalEvaluation(); state = AsyncData( state.requireValue.copyWith( @@ -419,13 +389,14 @@ class BroadcastGameController extends _$BroadcastGameController if (state.requireValue.isLocalEvaluationEnabled) { final prefs = ref.read(analysisPreferencesProvider); - await ref.read(evaluationServiceProvider).initEngine( + await ref + .read(evaluationServiceProvider) + .initEngine( _evaluationContext, options: EvaluationOptions( multiPv: prefs.numEvalLines, cores: prefs.numEngineCores, - searchTime: - ref.read(analysisPreferencesProvider).engineSearchTime, + searchTime: ref.read(analysisPreferencesProvider).engineSearchTime, ), ); _startEngineEval(); @@ -438,11 +409,11 @@ class BroadcastGameController extends _$BroadcastGameController void setNumEvalLines(int numEvalLines) { if (!state.hasValue) return; - ref - .read(analysisPreferencesProvider.notifier) - .setNumEvalLines(numEvalLines); + ref.read(analysisPreferencesProvider.notifier).setNumEvalLines(numEvalLines); - ref.read(evaluationServiceProvider).setOptions( + ref + .read(evaluationServiceProvider) + .setOptions( EvaluationOptions( multiPv: numEvalLines, cores: ref.read(analysisPreferencesProvider).numEngineCores, @@ -454,9 +425,7 @@ class BroadcastGameController extends _$BroadcastGameController state = AsyncData( state.requireValue.copyWith( - currentNode: AnalysisCurrentNode.fromNode( - _root.nodeAt(state.requireValue.currentPath), - ), + currentNode: AnalysisCurrentNode.fromNode(_root.nodeAt(state.requireValue.currentPath)), ), ); @@ -464,11 +433,11 @@ class BroadcastGameController extends _$BroadcastGameController } void setEngineCores(int numEngineCores) { - ref - .read(analysisPreferencesProvider.notifier) - .setEngineCores(numEngineCores); + ref.read(analysisPreferencesProvider.notifier).setEngineCores(numEngineCores); - ref.read(evaluationServiceProvider).setOptions( + ref + .read(evaluationServiceProvider) + .setOptions( EvaluationOptions( multiPv: ref.read(analysisPreferencesProvider).numEvalLines, cores: numEngineCores, @@ -480,11 +449,11 @@ class BroadcastGameController extends _$BroadcastGameController } void setEngineSearchTime(Duration searchTime) { - ref - .read(analysisPreferencesProvider.notifier) - .setEngineSearchTime(searchTime); + ref.read(analysisPreferencesProvider.notifier).setEngineSearchTime(searchTime); - ref.read(evaluationServiceProvider).setOptions( + ref + .read(evaluationServiceProvider) + .setOptions( EvaluationOptions( multiPv: ref.read(analysisPreferencesProvider).numEvalLines, cores: ref.read(analysisPreferencesProvider).numEngineCores, @@ -508,9 +477,7 @@ class BroadcastGameController extends _$BroadcastGameController final currentNode = _root.nodeAt(path); // always show variation if the user plays a move - if (shouldForceShowVariation && - currentNode is Branch && - currentNode.isCollapsed) { + if (shouldForceShowVariation && currentNode is Branch && currentNode.isCollapsed) { _root.updateAt(path, (node) { if (node is Branch) node.isCollapsed = false; }); @@ -519,9 +486,8 @@ class BroadcastGameController extends _$BroadcastGameController // root view is only used to display move list, so we need to // recompute the root view only when the nodelist length changes // or a variation is hidden/shown - final rootView = shouldForceShowVariation || shouldRecomputeRootView - ? _root.view - : state.requireValue.root; + final rootView = + shouldForceShowVariation || shouldRecomputeRootView ? _root.view : state.requireValue.root; final isForward = path.size > state.requireValue.currentPath.size; if (currentNode is Branch) { @@ -529,9 +495,7 @@ class BroadcastGameController extends _$BroadcastGameController if (isForward) { final isCheck = currentNode.sanMove.isCheck; if (currentNode.sanMove.isCapture) { - ref - .read(moveFeedbackServiceProvider) - .captureFeedback(check: isCheck); + ref.read(moveFeedbackServiceProvider).captureFeedback(check: isCheck); } else { ref.read(moveFeedbackServiceProvider).moveFeedback(check: isCheck); } @@ -588,9 +552,7 @@ class BroadcastGameController extends _$BroadcastGameController initialPositionEval: _root.eval, shouldEmit: (work) => work.path == state.valueOrNull?.currentPath, ) - ?.forEach( - (t) => _root.updateAt(t.$1.path, (node) => node.eval = t.$2), - ); + ?.forEach((t) => _root.updateAt(t.$1.path, (node) => node.eval = t.$2)); } void _debouncedStartEngineEval() { @@ -606,9 +568,7 @@ class BroadcastGameController extends _$BroadcastGameController // update the current node with last cached eval state = AsyncData( state.requireValue.copyWith( - currentNode: AnalysisCurrentNode.fromNode( - _root.nodeAt(state.requireValue.currentPath), - ), + currentNode: AnalysisCurrentNode.fromNode(_root.nodeAt(state.requireValue.currentPath)), ), ); } @@ -675,8 +635,7 @@ class BroadcastGameState with _$BroadcastGameState { IList? pgnRootComments, }) = _BroadcastGameState; - IMap> get validMoves => - makeLegalMoves(currentNode.position); + IMap> get validMoves => makeLegalMoves(currentNode.position); Position get position => currentNode.position; bool get canGoNext => currentNode.hasChild; @@ -689,9 +648,9 @@ class BroadcastGameState with _$BroadcastGameState { UciPath? get broadcastLivePath => isOngoing ? broadcastPath : null; EngineGaugeParams get engineGaugeParams => ( - orientation: pov, - isLocalEngineAvailable: isLocalEvaluationEnabled, - position: position, - savedEval: currentNode.eval ?? currentNode.serverEval, - ); + orientation: pov, + isLocalEngineAvailable: isLocalEvaluationEnabled, + position: position, + savedEval: currentNode.eval ?? currentNode.serverEval, + ); } diff --git a/lib/src/model/broadcast/broadcast_providers.dart b/lib/src/model/broadcast/broadcast_providers.dart index a7e02edc8d..b23e20213d 100644 --- a/lib/src/model/broadcast/broadcast_providers.dart +++ b/lib/src/model/broadcast/broadcast_providers.dart @@ -34,13 +34,11 @@ class BroadcastsPaginator extends _$BroadcastsPaginator { (client) => BroadcastRepository(client).getBroadcasts(page: nextPage), ); - state = AsyncData( - ( - active: broadcastList.active, - past: broadcastList.past.addAll(broadcastListNewPage.past), - nextPage: broadcastListNewPage.nextPage, - ), - ); + state = AsyncData(( + active: broadcastList.active, + past: broadcastList.past.addAll(broadcastListNewPage.past), + nextPage: broadcastListNewPage.nextPage, + )); } } @@ -50,8 +48,7 @@ Future broadcastTournament( BroadcastTournamentId broadcastTournamentId, ) { return ref.withClient( - (client) => - BroadcastRepository(client).getTournament(broadcastTournamentId), + (client) => BroadcastRepository(client).getTournament(broadcastTournamentId), ); } @@ -60,9 +57,7 @@ Future> broadcastPlayers( Ref ref, BroadcastTournamentId tournamentId, ) { - return ref.withClient( - (client) => BroadcastRepository(client).getPlayers(tournamentId), - ); + return ref.withClient((client) => BroadcastRepository(client).getPlayers(tournamentId)); } @Riverpod(keepAlive: true) diff --git a/lib/src/model/broadcast/broadcast_repository.dart b/lib/src/model/broadcast/broadcast_repository.dart index 583d08ac72..e3c0d4d6ef 100644 --- a/lib/src/model/broadcast/broadcast_repository.dart +++ b/lib/src/model/broadcast/broadcast_repository.dart @@ -14,26 +14,19 @@ class BroadcastRepository { Future getBroadcasts({int page = 1}) { return client.readJson( - Uri( - path: '/api/broadcast/top', - queryParameters: {'page': page.toString()}, - ), + Uri(path: '/api/broadcast/top', queryParameters: {'page': page.toString()}), mapper: _makeBroadcastResponseFromJson, ); } - Future getTournament( - BroadcastTournamentId broadcastTournamentId, - ) { + Future getTournament(BroadcastTournamentId broadcastTournamentId) { return client.readJson( Uri(path: 'api/broadcast/$broadcastTournamentId'), mapper: _makeTournamentFromJson, ); } - Future getRound( - BroadcastRoundId broadcastRoundId, - ) { + Future getRound(BroadcastRoundId broadcastRoundId) { return client.readJson( Uri(path: 'api/broadcast/-/-/$broadcastRoundId'), // The path parameters with - are the broadcast tournament and round slugs @@ -42,16 +35,11 @@ class BroadcastRepository { ); } - Future getGamePgn( - BroadcastRoundId roundId, - BroadcastGameId gameId, - ) { + Future getGamePgn(BroadcastRoundId roundId, BroadcastGameId gameId) { return client.read(Uri(path: 'api/study/$roundId/$gameId.pgn')); } - Future> getPlayers( - BroadcastTournamentId tournamentId, - ) { + Future> getPlayers(BroadcastTournamentId tournamentId) { return client.readJsonList( Uri(path: '/broadcast/$tournamentId/players'), mapper: _makePlayerFromJson, @@ -59,14 +47,10 @@ class BroadcastRepository { } } -BroadcastList _makeBroadcastResponseFromJson( - Map json, -) { +BroadcastList _makeBroadcastResponseFromJson(Map json) { return ( active: pick(json, 'active').asListOrThrow(_broadcastFromPick).toIList(), - past: pick(json, 'past', 'currentPageResults') - .asListOrThrow(_broadcastFromPick) - .toIList(), + past: pick(json, 'past', 'currentPageResults').asListOrThrow(_broadcastFromPick).toIList(), nextPage: pick(json, 'past', 'nextPage').asIntOrNull(), ); } @@ -78,46 +62,37 @@ Broadcast _broadcastFromPick(RequiredPick pick) { tour: _tournamentDataFromPick(pick('tour').required()), round: _roundFromPick(pick('round').required()), group: pick('group').asStringOrNull(), - roundToLinkId: - pick('roundToLink', 'id').asBroadcastRoundIddOrNull() ?? roundId, + roundToLinkId: pick('roundToLink', 'id').asBroadcastRoundIddOrNull() ?? roundId, ); } -BroadcastTournamentData _tournamentDataFromPick( - RequiredPick pick, -) => - BroadcastTournamentData( - id: pick('id').asBroadcastTournamentIdOrThrow(), - name: pick('name').asStringOrThrow(), - tier: pick('tier').asIntOrNull(), - imageUrl: pick('image').asStringOrNull(), - description: pick('description').asStringOrNull(), - information: ( - format: pick('info', 'format').asStringOrNull(), - timeControl: pick('info', 'tc').asStringOrNull(), - players: pick('info', 'players').asStringOrNull(), - location: pick('info', 'location').asStringOrNull(), - dates: pick('dates').letOrNull( - (pick) => ( - startsAt: pick(0).asDateTimeFromMillisecondsOrThrow(), - endsAt: pick(1).asDateTimeFromMillisecondsOrNull(), - ), - ), - website: pick('info', 'website') - .letOrNull((p) => Uri.tryParse(p.asStringOrThrow())), +BroadcastTournamentData _tournamentDataFromPick(RequiredPick pick) => BroadcastTournamentData( + id: pick('id').asBroadcastTournamentIdOrThrow(), + name: pick('name').asStringOrThrow(), + tier: pick('tier').asIntOrNull(), + imageUrl: pick('image').asStringOrNull(), + description: pick('description').asStringOrNull(), + information: ( + format: pick('info', 'format').asStringOrNull(), + timeControl: pick('info', 'tc').asStringOrNull(), + players: pick('info', 'players').asStringOrNull(), + location: pick('info', 'location').asStringOrNull(), + dates: pick('dates').letOrNull( + (pick) => ( + startsAt: pick(0).asDateTimeFromMillisecondsOrThrow(), + endsAt: pick(1).asDateTimeFromMillisecondsOrNull(), ), - ); + ), + website: pick('info', 'website').letOrNull((p) => Uri.tryParse(p.asStringOrThrow())), + ), +); -BroadcastTournament _makeTournamentFromJson( - Map json, -) { +BroadcastTournament _makeTournamentFromJson(Map json) { return BroadcastTournament( data: _tournamentDataFromPick(pick(json, 'tour').required()), rounds: pick(json, 'rounds').asListOrThrow(_roundFromPick).toIList(), defaultRoundId: pick(json, 'defaultRoundId').asBroadcastRoundIdOrThrow(), - group: pick(json, 'group', 'tours') - .asListOrNull(_tournamentGroupFromPick) - ?.toIList(), + group: pick(json, 'group', 'tours').asListOrNull(_tournamentGroupFromPick)?.toIList(), ); } @@ -131,9 +106,10 @@ BroadcastTournamentGroup _tournamentGroupFromPick(RequiredPick pick) { BroadcastRound _roundFromPick(RequiredPick pick) { final live = pick('ongoing').asBoolOrFalse(); final finished = pick('finished').asBoolOrFalse(); - final status = live - ? RoundStatus.live - : finished + final status = + live + ? RoundStatus.live + : finished ? RoundStatus.finished : RoundStatus.upcoming; @@ -153,33 +129,29 @@ BroadcastRoundWithGames _makeRoundWithGamesFromJson(Map json) { return (round: _roundFromPick(round), games: _gamesFromPick(games)); } -BroadcastRoundGames _gamesFromPick( - RequiredPick pick, -) => +BroadcastRoundGames _gamesFromPick(RequiredPick pick) => IMap.fromEntries(pick.asListOrThrow(gameFromPick)); -MapEntry gameFromPick( - RequiredPick pick, -) { +MapEntry gameFromPick(RequiredPick pick) { final stringStatus = pick('status').asStringOrNull(); - final status = (stringStatus == null) - ? BroadcastResult.noResultPgnTag - : switch (stringStatus) { - '½-½' => BroadcastResult.draw, - '1-0' => BroadcastResult.whiteWins, - '0-1' => BroadcastResult.blackWins, - '*' => BroadcastResult.ongoing, - _ => throw FormatException( - "value $stringStatus can't be interpreted as a broadcast result", - ) - }; + final status = + (stringStatus == null) + ? BroadcastResult.noResultPgnTag + : switch (stringStatus) { + '½-½' => BroadcastResult.draw, + '1-0' => BroadcastResult.whiteWins, + '0-1' => BroadcastResult.blackWins, + '*' => BroadcastResult.ongoing, + _ => + throw FormatException( + "value $stringStatus can't be interpreted as a broadcast result", + ), + }; /// The amount of time that the player whose turn it is has been thinking since his last move - final thinkTime = - pick('thinkTime').asDurationFromSecondsOrNull() ?? Duration.zero; - final fen = - pick('fen').asStringOrNull() ?? Variant.standard.initialPosition.fen; + final thinkTime = pick('thinkTime').asDurationFromSecondsOrNull() ?? Duration.zero; + final fen = pick('fen').asStringOrNull() ?? Variant.standard.initialPosition.fen; final playingSide = Setup.parseFen(fen).turn; return MapEntry( @@ -212,8 +184,7 @@ BroadcastPlayer _playerFromPick( required Duration thinkingTime, }) { final clock = pick('clock').asDurationFromCentiSecondsOrNull(); - final updatedClock = - clock != null && isPlaying ? clock - thinkingTime : clock; + final updatedClock = clock != null && isPlaying ? clock - thinkingTime : clock; return BroadcastPlayer( name: pick('name').asStringOrThrow(), title: pick('title').asStringOrNull(), diff --git a/lib/src/model/broadcast/broadcast_round_controller.dart b/lib/src/model/broadcast/broadcast_round_controller.dart index fd9569cf77..4596c417bc 100644 --- a/lib/src/model/broadcast/broadcast_round_controller.dart +++ b/lib/src/model/broadcast/broadcast_round_controller.dart @@ -33,9 +33,7 @@ class BroadcastRoundController extends _$BroadcastRoundController { Object? _key = Object(); @override - Future build( - BroadcastRoundId broadcastRoundId, - ) async { + Future build(BroadcastRoundId broadcastRoundId) async { _socketClient = ref .watch(socketPoolProvider) .open(BroadcastRoundController.broadcastSocketUri(broadcastRoundId)); @@ -70,9 +68,7 @@ class BroadcastRoundController extends _$BroadcastRoundController { _debouncer.dispose(); }); - return ref.withClient( - (client) => BroadcastRepository(client).getRound(broadcastRoundId), - ); + return ref.withClient((client) => BroadcastRepository(client).getRound(broadcastRoundId)); } Future _syncRound() async { @@ -114,36 +110,29 @@ class BroadcastRoundController extends _$BroadcastRoundController { // We check that the event we received is for the last move of the game if (currentPath.value != '!') return; - final broadcastGameId = - pick(event.data, 'p', 'chapterId').asBroadcastGameIdOrThrow(); + final broadcastGameId = pick(event.data, 'p', 'chapterId').asBroadcastGameIdOrThrow(); final fen = pick(event.data, 'n', 'fen').asStringOrThrow(); final playingSide = Setup.parseFen(fen).turn; - state = AsyncData( - ( - round: state.requireValue.round, - games: state.requireValue.games.update( - broadcastGameId, - (broadcastGame) => broadcastGame.copyWith( - players: IMap( - { - playingSide: broadcastGame.players[playingSide]!, - playingSide.opposite: - broadcastGame.players[playingSide.opposite]!.copyWith( - clock: pick(event.data, 'n', 'clock') - .asDurationFromCentiSecondsOrNull(), - ), - }, + state = AsyncData(( + round: state.requireValue.round, + games: state.requireValue.games.update( + broadcastGameId, + (broadcastGame) => broadcastGame.copyWith( + players: IMap({ + playingSide: broadcastGame.players[playingSide]!, + playingSide.opposite: broadcastGame.players[playingSide.opposite]!.copyWith( + clock: pick(event.data, 'n', 'clock').asDurationFromCentiSecondsOrNull(), ), - fen: fen, - lastMove: pick(event.data, 'n', 'uci').asUciMoveOrThrow(), - updatedClockAt: DateTime.now(), - ), + }), + fen: fen, + lastMove: pick(event.data, 'n', 'uci').asUciMoveOrThrow(), + updatedClockAt: DateTime.now(), ), ), - ); + )); } void _handleAddChapterEvent(SocketEvent event) { @@ -152,39 +141,32 @@ class BroadcastRoundController extends _$BroadcastRoundController { void _handleChaptersEvent(SocketEvent event) { final games = pick(event.data).asListOrThrow(gameFromPick); - state = AsyncData( - (round: state.requireValue.round, games: IMap.fromEntries(games)), - ); + state = AsyncData((round: state.requireValue.round, games: IMap.fromEntries(games))); } void _handleClockEvent(SocketEvent event) { - final broadcastGameId = - pick(event.data, 'p', 'chapterId').asBroadcastGameIdOrThrow(); + final broadcastGameId = pick(event.data, 'p', 'chapterId').asBroadcastGameIdOrThrow(); final relayClocks = pick(event.data, 'p', 'relayClocks'); // We check that the clocks for the broadcast game preview have been updated else we do nothing if (relayClocks.value == null) return; - state = AsyncData( - ( - round: state.requireValue.round, - games: state.requireValue.games.update( - broadcastGameId, - (broadcastsGame) => broadcastsGame.copyWith( - players: IMap( - { - Side.white: broadcastsGame.players[Side.white]!.copyWith( - clock: relayClocks(0).asDurationFromCentiSecondsOrNull(), - ), - Side.black: broadcastsGame.players[Side.black]!.copyWith( - clock: relayClocks(1).asDurationFromCentiSecondsOrNull(), - ), - }, + state = AsyncData(( + round: state.requireValue.round, + games: state.requireValue.games.update( + broadcastGameId, + (broadcastsGame) => broadcastsGame.copyWith( + players: IMap({ + Side.white: broadcastsGame.players[Side.white]!.copyWith( + clock: relayClocks(0).asDurationFromCentiSecondsOrNull(), + ), + Side.black: broadcastsGame.players[Side.black]!.copyWith( + clock: relayClocks(1).asDurationFromCentiSecondsOrNull(), ), - updatedClockAt: DateTime.now(), - ), + }), + updatedClockAt: DateTime.now(), ), ), - ); + )); } } diff --git a/lib/src/model/challenge/challenge.dart b/lib/src/model/challenge/challenge.dart index 7fc0ac749a..78dea6630f 100644 --- a/lib/src/model/challenge/challenge.dart +++ b/lib/src/model/challenge/challenge.dart @@ -23,16 +23,13 @@ abstract mixin class BaseChallenge { SideChoice get sideChoice; String? get initialFen; - TimeIncrement? get timeIncrement => clock != null - ? TimeIncrement(clock!.time.inSeconds, clock!.increment.inSeconds) - : null; + TimeIncrement? get timeIncrement => + clock != null ? TimeIncrement(clock!.time.inSeconds, clock!.increment.inSeconds) : null; Perf get perf => Perf.fromVariantAndSpeed( - variant, - timeIncrement != null - ? Speed.fromTimeIncrement(timeIncrement!) - : Speed.correspondence, - ); + variant, + timeIncrement != null ? Speed.fromTimeIncrement(timeIncrement!) : Speed.correspondence, + ); } /// A challenge already created server-side. @@ -59,8 +56,7 @@ class Challenge with _$Challenge, BaseChallenge implements BaseChallenge { ChallengeDirection? direction, }) = _Challenge; - factory Challenge.fromJson(Map json) => - _$ChallengeFromJson(json); + factory Challenge.fromJson(Map json) => _$ChallengeFromJson(json); factory Challenge.fromServerJson(Map json) { return _challengeFromPick(pick(json).required()); @@ -77,30 +73,32 @@ class Challenge with _$Challenge, BaseChallenge implements BaseChallenge { final time = switch (timeControl) { ChallengeTimeControlType.clock => () { - final minutes = switch (clock!.time.inSeconds) { - 15 => '¼', - 30 => '½', - 45 => '¾', - 90 => '1.5', - _ => clock!.time.inMinutes, - }; - return '$minutes+${clock!.increment.inSeconds}'; - }(), + final minutes = switch (clock!.time.inSeconds) { + 15 => '¼', + 30 => '½', + 45 => '¾', + 90 => '1.5', + _ => clock!.time.inMinutes, + }; + return '$minutes+${clock!.increment.inSeconds}'; + }(), ChallengeTimeControlType.correspondence => '${l10n.daysPerTurn}: $days', ChallengeTimeControlType.unlimited => '∞', }; final variantStr = variant == Variant.standard ? '' : ' • ${variant.label}'; - final sidePiece = sideChoice == SideChoice.black - ? '♔ ' - : sideChoice == SideChoice.white + final sidePiece = + sideChoice == SideChoice.black + ? '♔ ' + : sideChoice == SideChoice.white ? '♚ ' : ''; - final side = sideChoice == SideChoice.black - ? l10n.white - : sideChoice == SideChoice.white + final side = + sideChoice == SideChoice.black + ? l10n.white + : sideChoice == SideChoice.white ? l10n.black : l10n.randomColor; @@ -112,15 +110,10 @@ class Challenge with _$Challenge, BaseChallenge implements BaseChallenge { /// A challenge request to play a game with another user. @freezed -class ChallengeRequest - with _$ChallengeRequest, BaseChallenge - implements BaseChallenge { +class ChallengeRequest with _$ChallengeRequest, BaseChallenge implements BaseChallenge { const ChallengeRequest._(); - @Assert( - 'clock != null || days != null', - 'Either clock or days must be set but not both.', - ) + @Assert('clock != null || days != null', 'Either clock or days must be set but not both.') const factory ChallengeRequest({ required LightUser destUser, required Variant variant, @@ -134,17 +127,16 @@ class ChallengeRequest @override Speed get speed => Speed.fromTimeIncrement( - TimeIncrement( - clock != null ? clock!.time.inSeconds : 0, - clock != null ? clock!.increment.inSeconds : 0, - ), - ); + TimeIncrement( + clock != null ? clock!.time.inSeconds : 0, + clock != null ? clock!.increment.inSeconds : 0, + ), + ); Map get toRequestBody { return { if (clock != null) 'clock.limit': clock!.time.inSeconds.toString(), - if (clock != null) - 'clock.increment': clock!.increment.inSeconds.toString(), + if (clock != null) 'clock.increment': clock!.increment.inSeconds.toString(), if (days != null) 'days': days.toString(), 'rated': variant == Variant.fromPosition ? 'false' : rated.toString(), 'variant': variant.name, @@ -154,24 +146,11 @@ class ChallengeRequest } } -enum ChallengeDirection { - outward, - inward, -} +enum ChallengeDirection { outward, inward } -enum ChallengeStatus { - created, - offline, - canceled, - declined, - accepted, -} +enum ChallengeStatus { created, offline, canceled, declined, accepted } -enum ChallengeTimeControlType { - unlimited, - clock, - correspondence, -} +enum ChallengeTimeControlType { unlimited, clock, correspondence } enum ChallengeDeclineReason { generic, @@ -187,26 +166,21 @@ enum ChallengeDeclineReason { onlyBot; String label(AppLocalizations l10n) => switch (this) { - ChallengeDeclineReason.generic => l10n.challengeDeclineGeneric, - ChallengeDeclineReason.later => l10n.challengeDeclineLater, - ChallengeDeclineReason.tooFast => l10n.challengeDeclineTooFast, - ChallengeDeclineReason.tooSlow => l10n.challengeDeclineTooSlow, - ChallengeDeclineReason.timeControl => l10n.challengeDeclineTimeControl, - ChallengeDeclineReason.rated => l10n.challengeDeclineRated, - ChallengeDeclineReason.casual => l10n.challengeDeclineCasual, - ChallengeDeclineReason.standard => l10n.challengeDeclineStandard, - ChallengeDeclineReason.variant => l10n.challengeDeclineVariant, - ChallengeDeclineReason.noBot => l10n.challengeDeclineNoBot, - ChallengeDeclineReason.onlyBot => l10n.challengeDeclineOnlyBot, - }; + ChallengeDeclineReason.generic => l10n.challengeDeclineGeneric, + ChallengeDeclineReason.later => l10n.challengeDeclineLater, + ChallengeDeclineReason.tooFast => l10n.challengeDeclineTooFast, + ChallengeDeclineReason.tooSlow => l10n.challengeDeclineTooSlow, + ChallengeDeclineReason.timeControl => l10n.challengeDeclineTimeControl, + ChallengeDeclineReason.rated => l10n.challengeDeclineRated, + ChallengeDeclineReason.casual => l10n.challengeDeclineCasual, + ChallengeDeclineReason.standard => l10n.challengeDeclineStandard, + ChallengeDeclineReason.variant => l10n.challengeDeclineVariant, + ChallengeDeclineReason.noBot => l10n.challengeDeclineNoBot, + ChallengeDeclineReason.onlyBot => l10n.challengeDeclineOnlyBot, + }; } -typedef ChallengeUser = ({ - LightUser user, - int? rating, - bool? provisionalRating, - int? lagRating, -}); +typedef ChallengeUser = ({LightUser user, int? rating, bool? provisionalRating, int? lagRating}); extension ChallengeExtension on Pick { ChallengeDirection asChallengeDirectionOrThrow() { @@ -226,9 +200,7 @@ extension ChallengeExtension on Pick { ); } } - throw PickException( - "value $value at $debugParsingExit can't be casted to ChallengeDirection", - ); + throw PickException("value $value at $debugParsingExit can't be casted to ChallengeDirection"); } ChallengeDirection? asChallengeDirectionOrNull() { @@ -263,9 +235,7 @@ extension ChallengeExtension on Pick { ); } } - throw PickException( - "value $value at $debugParsingExit can't be casted to ChallengeStatus", - ); + throw PickException("value $value at $debugParsingExit can't be casted to ChallengeStatus"); } ChallengeStatus? asChallengeStatusOrNull() { @@ -329,9 +299,7 @@ extension ChallengeExtension on Pick { ); } } - throw PickException( - "value $value at $debugParsingExit can't be casted to SideChoice", - ); + throw PickException("value $value at $debugParsingExit can't be casted to SideChoice"); } SideChoice? asSideChoiceOrNull() { @@ -351,9 +319,11 @@ extension ChallengeExtension on Pick { if (value is String) { return ChallengeDeclineReason.values.firstWhere( (element) => element.name.toLowerCase() == value, - orElse: () => throw PickException( - "value $value at $debugParsingExit can't be casted to ChallengeDeclineReason: invalid string.", - ), + orElse: + () => + throw PickException( + "value $value at $debugParsingExit can't be casted to ChallengeDeclineReason: invalid string.", + ), ); } throw PickException( @@ -379,42 +349,33 @@ Challenge _challengeFromPick(RequiredPick pick) { status: pick('status').asChallengeStatusOrThrow(), variant: pick('variant').asVariantOrThrow(), speed: pick('speed').asSpeedOrThrow(), - timeControl: - pick('timeControl', 'type').asChallengeTimeControlTypeOrThrow(), - clock: pick('timeControl').letOrThrow( - (clockPick) { - final time = clockPick('limit').asDurationFromSecondsOrNull(); - final increment = clockPick('increment').asDurationFromSecondsOrNull(); - return time != null && increment != null - ? (time: time, increment: increment) - : null; - }, - ), + timeControl: pick('timeControl', 'type').asChallengeTimeControlTypeOrThrow(), + clock: pick('timeControl').letOrThrow((clockPick) { + final time = clockPick('limit').asDurationFromSecondsOrNull(); + final increment = clockPick('increment').asDurationFromSecondsOrNull(); + return time != null && increment != null ? (time: time, increment: increment) : null; + }), days: pick('timeControl', 'daysPerTurn').asIntOrNull(), rated: pick('rated').asBoolOrThrow(), sideChoice: pick('color').asSideChoiceOrThrow(), - challenger: pick('challenger').letOrNull( - (challengerPick) { - final challengerUser = pick('challenger').asLightUserOrThrow(); - return ( - user: challengerUser, - rating: challengerPick('rating').asIntOrNull(), - provisionalRating: challengerPick('provisional').asBoolOrNull(), - lagRating: challengerPick('lag').asIntOrNull(), - ); - }, - ), - destUser: pick('destUser').letOrNull( - (destPick) { - final destUser = pick('destUser').asLightUserOrThrow(); - return ( - user: destUser, - rating: destPick('rating').asIntOrNull(), - provisionalRating: destPick('provisional').asBoolOrNull(), - lagRating: destPick('lag').asIntOrNull(), - ); - }, - ), + challenger: pick('challenger').letOrNull((challengerPick) { + final challengerUser = pick('challenger').asLightUserOrThrow(); + return ( + user: challengerUser, + rating: challengerPick('rating').asIntOrNull(), + provisionalRating: challengerPick('provisional').asBoolOrNull(), + lagRating: challengerPick('lag').asIntOrNull(), + ); + }), + destUser: pick('destUser').letOrNull((destPick) { + final destUser = pick('destUser').asLightUserOrThrow(); + return ( + user: destUser, + rating: destPick('rating').asIntOrNull(), + provisionalRating: destPick('provisional').asBoolOrNull(), + lagRating: destPick('lag').asIntOrNull(), + ); + }), initialFen: pick('initialFen').asStringOrNull(), direction: pick('direction').asChallengeDirectionOrNull(), declineReason: pick('declineReasonKey').asDeclineReasonOrNull(), diff --git a/lib/src/model/challenge/challenge_preferences.dart b/lib/src/model/challenge/challenge_preferences.dart index 76453530e9..dbbe66db28 100644 --- a/lib/src/model/challenge/challenge_preferences.dart +++ b/lib/src/model/challenge/challenge_preferences.dart @@ -22,8 +22,7 @@ class ChallengePreferences extends _$ChallengePreferences ChallengePrefs defaults({LightUser? user}) => ChallengePrefs.defaults; @override - ChallengePrefs fromJson(Map json) => - ChallengePrefs.fromJson(json); + ChallengePrefs fromJson(Map json) => ChallengePrefs.fromJson(json); @override ChallengePrefs build() { @@ -77,14 +76,10 @@ class ChallengePrefs with _$ChallengePrefs implements Serializable { sideChoice: SideChoice.random, ); - Speed get speed => timeControl == ChallengeTimeControlType.clock - ? Speed.fromTimeIncrement( - TimeIncrement( - clock.time.inSeconds, - clock.increment.inSeconds, - ), - ) - : Speed.correspondence; + Speed get speed => + timeControl == ChallengeTimeControlType.clock + ? Speed.fromTimeIncrement(TimeIncrement(clock.time.inSeconds, clock.increment.inSeconds)) + : Speed.correspondence; ChallengeRequest makeRequest(LightUser destUser, [String? initialFen]) { return ChallengeRequest( @@ -92,8 +87,7 @@ class ChallengePrefs with _$ChallengePrefs implements Serializable { variant: variant, timeControl: timeControl, clock: timeControl == ChallengeTimeControlType.clock ? clock : null, - days: - timeControl == ChallengeTimeControlType.correspondence ? days : null, + days: timeControl == ChallengeTimeControlType.correspondence ? days : null, rated: rated, sideChoice: sideChoice, initialFen: initialFen, diff --git a/lib/src/model/challenge/challenge_repository.dart b/lib/src/model/challenge/challenge_repository.dart index 17593a0cb5..7510bae576 100644 --- a/lib/src/model/challenge/challenge_repository.dart +++ b/lib/src/model/challenge/challenge_repository.dart @@ -16,10 +16,7 @@ ChallengeRepository challengeRepository(Ref ref) { return ChallengeRepository(ref.read(lichessClientProvider)); } -typedef ChallengesList = ({ - IList inward, - IList outward, -}); +typedef ChallengesList = ({IList inward, IList outward}); class ChallengeRepository { const ChallengeRepository(this.client); @@ -42,10 +39,7 @@ class ChallengeRepository { Future show(ChallengeId id) async { final uri = Uri(path: '/api/challenge/$id/show'); - return client.readJson( - uri, - mapper: Challenge.fromServerJson, - ); + return client.readJson(uri, mapper: Challenge.fromServerJson); } Future create(ChallengeRequest challenge) async { @@ -62,25 +56,16 @@ class ChallengeRepository { final response = await client.post(uri); if (response.statusCode >= 400) { - throw http.ClientException( - 'Failed to accept challenge: ${response.statusCode}', - uri, - ); + throw http.ClientException('Failed to accept challenge: ${response.statusCode}', uri); } } Future decline(ChallengeId id, {ChallengeDeclineReason? reason}) async { final uri = Uri(path: '/api/challenge/$id/decline'); - final response = await client.post( - uri, - body: reason != null ? {'reason': reason.name} : null, - ); + final response = await client.post(uri, body: reason != null ? {'reason': reason.name} : null); if (response.statusCode >= 400) { - throw http.ClientException( - 'Failed to decline challenge: ${response.statusCode}', - uri, - ); + throw http.ClientException('Failed to decline challenge: ${response.statusCode}', uri); } } @@ -89,10 +74,7 @@ class ChallengeRepository { final response = await client.post(uri); if (response.statusCode >= 400) { - throw http.ClientException( - 'Failed to cancel challenge: ${response.statusCode}', - uri, - ); + throw http.ClientException('Failed to cancel challenge: ${response.statusCode}', uri); } } } diff --git a/lib/src/model/challenge/challenge_service.dart b/lib/src/model/challenge/challenge_service.dart index a670d924ea..29d7b1c68a 100644 --- a/lib/src/model/challenge/challenge_service.dart +++ b/lib/src/model/challenge/challenge_service.dart @@ -41,15 +41,14 @@ class ChallengeService { StreamSubscription? _socketSubscription; /// The stream of challenge events that are received from the server. - static Stream get stream => socketGlobalStream.map( - (event) { - if (event.topic != 'challenges') return null; - final listPick = pick(event.data).required(); - final inward = listPick('in').asListOrEmpty(Challenge.fromPick); - final outward = listPick('out').asListOrEmpty(Challenge.fromPick); - return (inward: inward.lock, outward: outward.lock); - }, - ).whereNotNull(); + static Stream get stream => + socketGlobalStream.map((event) { + if (event.topic != 'challenges') return null; + final listPick = pick(event.data).required(); + final inward = listPick('in').asListOrEmpty(Challenge.fromPick); + final outward = listPick('out').asListOrEmpty(Challenge.fromPick); + return (inward: inward.lock, outward: outward.lock); + }).whereNotNull(); /// Start listening to challenge events from the server. void start() { @@ -75,21 +74,16 @@ class ChallengeService { await Future.wait( prevInwardIds .whereNot((challengeId) => currentInwardIds.contains(challengeId)) - .map( - (id) async => await notificationService.cancel(id.value.hashCode), - ), + .map((id) async => await notificationService.cancel(id.value.hashCode)), ); // new incoming challenges await Future.wait( - _current?.inward - .whereNot((challenge) => prevInwardIds.contains(challenge.id)) - .map( - (challenge) async { - return await notificationService - .show(ChallengeNotification(challenge)); - }, - ) ?? + _current?.inward.whereNot((challenge) => prevInwardIds.contains(challenge.id)).map(( + challenge, + ) async { + return await notificationService.show(ChallengeNotification(challenge)); + }) ?? >[], ); } @@ -100,10 +94,7 @@ class ChallengeService { } /// Handle a local notification response when the app is in the foreground. - Future onNotificationResponse( - String? actionid, - Challenge challenge, - ) async { + Future onNotificationResponse(String? actionid, Challenge challenge) async { final challengeId = challenge.id; switch (actionid) { @@ -111,9 +102,7 @@ class ChallengeService { final repo = ref.read(challengeRepositoryProvider); await repo.accept(challengeId); - final fullId = await repo - .show(challengeId) - .then((challenge) => challenge.gameFullId); + final fullId = await repo.show(challengeId).then((challenge) => challenge.gameFullId); final context = ref.read(currentNavigatorKeyProvider).currentContext; if (context == null || !context.mounted) break; @@ -134,17 +123,18 @@ class ChallengeService { if (context == null || !context.mounted) break; showAdaptiveActionSheet( context: context, - actions: ChallengeDeclineReason.values - .map( - (reason) => BottomSheetAction( - makeLabel: (context) => Text(reason.label(context.l10n)), - onPressed: (_) { - final repo = ref.read(challengeRepositoryProvider); - repo.decline(challengeId, reason: reason); - }, - ), - ) - .toList(), + actions: + ChallengeDeclineReason.values + .map( + (reason) => BottomSheetAction( + makeLabel: (context) => Text(reason.label(context.l10n)), + onPressed: (_) { + final repo = ref.read(challengeRepositoryProvider); + repo.decline(challengeId, reason: reason); + }, + ), + ) + .toList(), ); case null: diff --git a/lib/src/model/challenge/challenges.dart b/lib/src/model/challenge/challenges.dart index ff788bffc8..8fa8c495c7 100644 --- a/lib/src/model/challenge/challenges.dart +++ b/lib/src/model/challenge/challenges.dart @@ -15,8 +15,7 @@ class Challenges extends _$Challenges { @override Future build() async { - _subscription = - ChallengeService.stream.listen((list) => state = AsyncValue.data(list)); + _subscription = ChallengeService.stream.listen((list) => state = AsyncValue.data(list)); ref.onDispose(() { _subscription?.cancel(); @@ -24,12 +23,10 @@ class Challenges extends _$Challenges { final session = ref.watch(authSessionProvider); if (session == null) { - return Future.value( - ( - inward: const IList.empty(), - outward: const IList.empty(), - ), - ); + return Future.value(( + inward: const IList.empty(), + outward: const IList.empty(), + )); } return ref.read(challengeRepositoryProvider).list(); diff --git a/lib/src/model/clock/chess_clock.dart b/lib/src/model/clock/chess_clock.dart index b4560932e2..02e75bfcf0 100644 --- a/lib/src/model/clock/chess_clock.dart +++ b/lib/src/model/clock/chess_clock.dart @@ -15,9 +15,9 @@ class ChessClock { this.emergencyThreshold, this.onFlag, this.onEmergency, - }) : _whiteTime = ValueNotifier(whiteTime), - _blackTime = ValueNotifier(blackTime), - _activeSide = Side.white; + }) : _whiteTime = ValueNotifier(whiteTime), + _blackTime = ValueNotifier(blackTime), + _activeSide = Side.white; /// The threshold at which the clock will call [onEmergency] if provided. final Duration? emergencyThreshold; @@ -176,8 +176,7 @@ class ChessClock { _shouldPlayEmergencyFeedback = false; _nextEmergency = clock.now().add(_emergencyDelay); onEmergency?.call(_activeSide); - } else if (emergencyThreshold != null && - timeLeft > emergencyThreshold! * 1.5) { + } else if (emergencyThreshold != null && timeLeft > emergencyThreshold! * 1.5) { _shouldPlayEmergencyFeedback = true; } } diff --git a/lib/src/model/clock/clock_tool_controller.dart b/lib/src/model/clock/clock_tool_controller.dart index 524b543a9d..4f559511df 100644 --- a/lib/src/model/clock/clock_tool_controller.dart +++ b/lib/src/model/clock/clock_tool_controller.dart @@ -23,11 +23,7 @@ class ClockToolController extends _$ClockToolController { whiteIncrement: increment, blackIncrement: increment, ); - _clock = ChessClock( - whiteTime: time, - blackTime: time, - onFlag: _onFlagged, - ); + _clock = ChessClock(whiteTime: time, blackTime: time, onFlag: _onFlagged); ref.onDispose(() { _clock.dispose(); @@ -65,9 +61,7 @@ class ClockToolController extends _$ClockToolController { _clock.startSide(playerType.opposite); _clock.incTime( playerType, - playerType == Side.white - ? state.options.whiteIncrement - : state.options.blackIncrement, + playerType == Side.white ? state.options.whiteIncrement : state.options.blackIncrement, ); } @@ -77,21 +71,14 @@ class ClockToolController extends _$ClockToolController { } _clock.setTimes( - whiteTime: playerType == Side.white - ? duration + state.options.whiteIncrement - : null, - blackTime: playerType == Side.black - ? duration + state.options.blackIncrement - : null, + whiteTime: playerType == Side.white ? duration + state.options.whiteIncrement : null, + blackTime: playerType == Side.black ? duration + state.options.blackIncrement : null, ); } void updateOptions(TimeIncrement timeIncrement) { final options = ClockOptions.fromTimeIncrement(timeIncrement); - _clock.setTimes( - whiteTime: options.whiteTime, - blackTime: options.blackTime, - ); + _clock.setTimes(whiteTime: options.whiteTime, blackTime: options.blackTime); state = state.copyWith( options: options, whiteTime: _clock.whiteTime, @@ -99,28 +86,16 @@ class ClockToolController extends _$ClockToolController { ); } - void updateOptionsCustom( - TimeIncrement clock, - Side player, - ) { + void updateOptionsCustom(TimeIncrement clock, Side player) { final options = ClockOptions( - whiteTime: player == Side.white - ? Duration(seconds: clock.time) - : state.options.whiteTime, - blackTime: player == Side.black - ? Duration(seconds: clock.time) - : state.options.blackTime, - whiteIncrement: player == Side.white - ? Duration(seconds: clock.increment) - : state.options.whiteIncrement, - blackIncrement: player == Side.black - ? Duration(seconds: clock.increment) - : state.options.blackIncrement, - ); - _clock.setTimes( - whiteTime: options.whiteTime, - blackTime: options.blackTime, + whiteTime: player == Side.white ? Duration(seconds: clock.time) : state.options.whiteTime, + blackTime: player == Side.black ? Duration(seconds: clock.time) : state.options.blackTime, + whiteIncrement: + player == Side.white ? Duration(seconds: clock.increment) : state.options.whiteIncrement, + blackIncrement: + player == Side.black ? Duration(seconds: clock.increment) : state.options.blackIncrement, ); + _clock.setTimes(whiteTime: options.whiteTime, blackTime: options.blackTime); state = ClockState( options: options, whiteTime: _clock.whiteTime, @@ -129,14 +104,10 @@ class ClockToolController extends _$ClockToolController { ); } - void setBottomPlayer(Side playerType) => - state = state.copyWith(bottomPlayer: playerType); + void setBottomPlayer(Side playerType) => state = state.copyWith(bottomPlayer: playerType); void reset() { - _clock.setTimes( - whiteTime: state.options.whiteTime, - blackTime: state.options.whiteTime, - ); + _clock.setTimes(whiteTime: state.options.whiteTime, blackTime: state.options.whiteTime); state = state.copyWith( whiteTime: _clock.whiteTime, blackTime: _clock.blackTime, @@ -176,24 +147,22 @@ class ClockOptions with _$ClockOptions { required Duration blackIncrement, }) = _ClockOptions; - factory ClockOptions.fromTimeIncrement(TimeIncrement timeIncrement) => - ClockOptions( - whiteTime: Duration(seconds: timeIncrement.time), - blackTime: Duration(seconds: timeIncrement.time), - whiteIncrement: Duration(seconds: timeIncrement.increment), - blackIncrement: Duration(seconds: timeIncrement.increment), - ); + factory ClockOptions.fromTimeIncrement(TimeIncrement timeIncrement) => ClockOptions( + whiteTime: Duration(seconds: timeIncrement.time), + blackTime: Duration(seconds: timeIncrement.time), + whiteIncrement: Duration(seconds: timeIncrement.increment), + blackIncrement: Duration(seconds: timeIncrement.increment), + ); factory ClockOptions.fromSeparateTimeIncrements( TimeIncrement playerTop, TimeIncrement playerBottom, - ) => - ClockOptions( - whiteTime: Duration(seconds: playerTop.time), - blackTime: Duration(seconds: playerBottom.time), - whiteIncrement: Duration(seconds: playerTop.increment), - blackIncrement: Duration(seconds: playerBottom.increment), - ); + ) => ClockOptions( + whiteTime: Duration(seconds: playerTop.time), + blackTime: Duration(seconds: playerBottom.time), + whiteIncrement: Duration(seconds: playerTop.increment), + blackIncrement: Duration(seconds: playerBottom.increment), + ); } @freezed @@ -216,14 +185,11 @@ class ClockState with _$ClockState { ValueListenable getDuration(Side playerType) => playerType == Side.white ? whiteTime : blackTime; - int getMovesCount(Side playerType) => - playerType == Side.white ? whiteMoves : blackMoves; + int getMovesCount(Side playerType) => playerType == Side.white ? whiteMoves : blackMoves; - bool isPlayersTurn(Side playerType) => - started && activeSide == playerType && flagged == null; + bool isPlayersTurn(Side playerType) => started && activeSide == playerType && flagged == null; - bool isPlayersMoveAllowed(Side playerType) => - isPlayersTurn(playerType) && !paused; + bool isPlayersMoveAllowed(Side playerType) => isPlayersTurn(playerType) && !paused; bool isActivePlayer(Side playerType) => isPlayersTurn(playerType) && !paused; diff --git a/lib/src/model/common/chess.dart b/lib/src/model/common/chess.dart index 74e521ef97..a26b8b3789 100644 --- a/lib/src/model/common/chess.dart +++ b/lib/src/model/common/chess.dart @@ -15,13 +15,9 @@ typedef UCIMove = String; @Freezed(fromJson: true, toJson: true) class SanMove with _$SanMove { const SanMove._(); - const factory SanMove( - String san, - @MoveConverter() Move move, - ) = _SanMove; + const factory SanMove(String san, @MoveConverter() Move move) = _SanMove; - factory SanMove.fromJson(Map json) => - _$SanMoveFromJson(json); + factory SanMove.fromJson(Map json) => _$SanMoveFromJson(json); bool get isCheck => san.contains('+'); bool get isCapture => san.contains('x'); @@ -39,12 +35,7 @@ class MoveConverter implements JsonConverter { } /// Alternative castling uci notations. -const altCastles = { - 'e1a1': 'e1c1', - 'e1h1': 'e1g1', - 'e8a8': 'e8c8', - 'e8h8': 'e8g8', -}; +const altCastles = {'e1a1': 'e1c1', 'e1h1': 'e1g1', 'e8a8': 'e8c8', 'e8h8': 'e8g8'}; /// Returns `true` if the move is a pawn promotion move that is not yet promoted. bool isPromotionPawnMove(Position position, NormalMove move) { @@ -180,24 +171,16 @@ sealed class Opening { @Freezed(fromJson: true, toJson: true) class LightOpening with _$LightOpening implements Opening { const LightOpening._(); - const factory LightOpening({ - required String eco, - required String name, - }) = _LightOpening; + const factory LightOpening({required String eco, required String name}) = _LightOpening; - factory LightOpening.fromJson(Map json) => - _$LightOpeningFromJson(json); + factory LightOpening.fromJson(Map json) => _$LightOpeningFromJson(json); } @Freezed(fromJson: true, toJson: true) class Division with _$Division { - const factory Division({ - double? middlegame, - double? endgame, - }) = _Division; + const factory Division({double? middlegame, double? endgame}) = _Division; - factory Division.fromJson(Map json) => - _$DivisionFromJson(json); + factory Division.fromJson(Map json) => _$DivisionFromJson(json); } @freezed @@ -228,9 +211,7 @@ extension ChessExtension on Pick { ); } } - throw PickException( - "value $value at $debugParsingExit can't be casted to Move", - ); + throw PickException("value $value at $debugParsingExit can't be casted to Move"); } Move? asUciMoveOrNull() { @@ -251,14 +232,12 @@ extension ChessExtension on Pick { return value == 'white' ? Side.white : value == 'black' - ? Side.black - : throw PickException( - "value $value at $debugParsingExit can't be casted to Side: invalid string.", - ); + ? Side.black + : throw PickException( + "value $value at $debugParsingExit can't be casted to Side: invalid string.", + ); } - throw PickException( - "value $value at $debugParsingExit can't be casted to Side", - ); + throw PickException("value $value at $debugParsingExit can't be casted to Side"); } Side? asSideOrNull() { @@ -299,9 +278,7 @@ extension ChessExtension on Pick { return variant; } } - throw PickException( - "value $value at $debugParsingExit can't be casted to Variant", - ); + throw PickException("value $value at $debugParsingExit can't be casted to Variant"); } Variant? asVariantOrNull() { diff --git a/lib/src/model/common/chess960.dart b/lib/src/model/common/chess960.dart index c1390ec352..6daf1bd857 100644 --- a/lib/src/model/common/chess960.dart +++ b/lib/src/model/common/chess960.dart @@ -8,9 +8,7 @@ Position randomChess960Position() { final rank8 = _positions[_random.nextInt(_positions.length)]; return Chess.fromSetup( - Setup.parseFen( - '$rank8/pppppppp/8/8/8/8/PPPPPPPP/${rank8.toUpperCase()} w KQkq - 0 1', - ), + Setup.parseFen('$rank8/pppppppp/8/8/8/8/PPPPPPPP/${rank8.toUpperCase()} w KQkq - 0 1'), ); } diff --git a/lib/src/model/common/eval.dart b/lib/src/model/common/eval.dart index 46976bc307..a1ef6181d0 100644 --- a/lib/src/model/common/eval.dart +++ b/lib/src/model/common/eval.dart @@ -39,8 +39,7 @@ class ExternalEval with _$ExternalEval implements Eval { ); } - factory ExternalEval.fromJson(Map json) => - _$ExternalEvalFromJson(json); + factory ExternalEval.fromJson(Map json) => _$ExternalEvalFromJson(json); @override String get evalString => _evalString(cp, mate); @@ -120,11 +119,7 @@ class ClientEval with _$ClientEval implements Eval { @freezed class PvData with _$PvData { const PvData._(); - const factory PvData({ - required IList moves, - int? mate, - int? cp, - }) = _PvData; + const factory PvData({required IList moves, int? mate, int? cp}) = _PvData; String get evalString => _evalString(cp, mate); @@ -158,11 +153,7 @@ class PvData with _$PvData { MoveWithWinningChances? _firstMoveWithWinningChances(Side sideToMove) { final uciMove = (moves.isNotEmpty) ? Move.parse(moves.first) : null; return (uciMove != null) - ? ( - move: uciMove, - winningChances: - _toPov(sideToMove, _toWhiteWinningChances(cp, mate)), - ) + ? (move: uciMove, winningChances: _toPov(sideToMove, _toWhiteWinningChances(cp, mate))) : null; } } @@ -182,59 +173,55 @@ ISet computeBestMoveShapes( const winningDiffScaleFactor = 2.5; final bestMove = moves[0]; - final winningDiffComparedToBestMove = - bestMove.winningChances - moves[index].winningChances; + final winningDiffComparedToBestMove = bestMove.winningChances - moves[index].winningChances; // Force minimum scale if the best move is significantly better than this move if (winningDiffComparedToBestMove > 0.3) { return minScale; } return clampDouble( - math.max( - minScale, - maxScale - winningDiffScaleFactor * winningDiffComparedToBestMove, - ), + math.max(minScale, maxScale - winningDiffScaleFactor * winningDiffComparedToBestMove), 0, 1, ); } return ISet( - moves.mapIndexed( - (i, m) { - final move = m.move; - // Same colors as in the Web UI with a slightly different opacity - // The best move has a different color than the other moves - final color = Color((i == 0) ? 0x66003088 : 0x664A4A4A); - switch (move) { - case NormalMove(from: _, to: _, promotion: final promRole): - return [ - Arrow( - color: color, - orig: move.from, - dest: move.to, - scale: scaleArrowAgainstBestMove(i), - ), - if (promRole != null) + moves + .mapIndexed((i, m) { + final move = m.move; + // Same colors as in the Web UI with a slightly different opacity + // The best move has a different color than the other moves + final color = Color((i == 0) ? 0x66003088 : 0x664A4A4A); + switch (move) { + case NormalMove(from: _, to: _, promotion: final promRole): + return [ + Arrow( + color: color, + orig: move.from, + dest: move.to, + scale: scaleArrowAgainstBestMove(i), + ), + if (promRole != null) + PieceShape( + color: color, + orig: move.to, + pieceAssets: pieceAssets, + piece: Piece(color: sideToMove, role: promRole), + ), + ]; + case DropMove(role: final role, to: _): + return [ PieceShape( color: color, orig: move.to, pieceAssets: pieceAssets, - piece: Piece(color: sideToMove, role: promRole), + opacity: 0.5, + piece: Piece(color: sideToMove, role: role), ), - ]; - case DropMove(role: final role, to: _): - return [ - PieceShape( - color: color, - orig: move.to, - pieceAssets: pieceAssets, - opacity: 0.5, - piece: Piece(color: sideToMove, role: role), - ), - ]; - } - }, - ).expand((e) => e), + ]; + } + }) + .expand((e) => e), ); } @@ -242,8 +229,7 @@ double cpToPawns(int cp) => cp / 100; int cpFromPawns(double pawns) => (pawns * 100).round(); -double cpWinningChances(int cp) => - _rawWinningChances(math.min(math.max(-1000, cp), 1000)); +double cpWinningChances(int cp) => _rawWinningChances(math.min(math.max(-1000, cp), 1000)); double mateWinningChances(int mate) { final cp = (21 - math.min(10, mate.abs())) * 100; diff --git a/lib/src/model/common/game.dart b/lib/src/model/common/game.dart index 024ced5571..f9dfefeb92 100644 --- a/lib/src/model/common/game.dart +++ b/lib/src/model/common/game.dart @@ -7,8 +7,8 @@ enum SideChoice { black; String label(AppLocalizations l10n) => switch (this) { - SideChoice.white => l10n.white, - SideChoice.random => l10n.randomColor, - SideChoice.black => l10n.black, - }; + SideChoice.white => l10n.white, + SideChoice.random => l10n.randomColor, + SideChoice.black => l10n.black, + }; } diff --git a/lib/src/model/common/id.dart b/lib/src/model/common/id.dart index b41a4e82a6..2d0d75ea6e 100644 --- a/lib/src/model/common/id.dart +++ b/lib/src/model/common/id.dart @@ -73,9 +73,7 @@ extension IDPick on Pick { if (value is String) { return UserId(value); } - throw PickException( - "value $value at $debugParsingExit can't be casted to UserId", - ); + throw PickException("value $value at $debugParsingExit can't be casted to UserId"); } UserId? asUserIdOrNull() { @@ -92,9 +90,7 @@ extension IDPick on Pick { if (value is String) { return GameId(value); } - throw PickException( - "value $value at $debugParsingExit can't be casted to GameId", - ); + throw PickException("value $value at $debugParsingExit can't be casted to GameId"); } GameId? asGameIdOrNull() { @@ -111,9 +107,7 @@ extension IDPick on Pick { if (value is String) { return GameFullId(value); } - throw PickException( - "value $value at $debugParsingExit can't be casted to GameId", - ); + throw PickException("value $value at $debugParsingExit can't be casted to GameId"); } GameFullId? asGameFullIdOrNull() { @@ -130,9 +124,7 @@ extension IDPick on Pick { if (value is String) { return PuzzleId(value); } - throw PickException( - "value $value at $debugParsingExit can't be casted to PuzzleId", - ); + throw PickException("value $value at $debugParsingExit can't be casted to PuzzleId"); } PuzzleId? asPuzzleIdOrNull() { @@ -149,9 +141,7 @@ extension IDPick on Pick { if (value is String) { return ChallengeId(value); } - throw PickException( - "value $value at $debugParsingExit can't be casted to ChallengeId", - ); + throw PickException("value $value at $debugParsingExit can't be casted to ChallengeId"); } ChallengeId? asChallengeIdOrNull() { @@ -187,9 +177,7 @@ extension IDPick on Pick { if (value is String) { return BroadcastRoundId(value); } - throw PickException( - "value $value at $debugParsingExit can't be casted to BroadcastRoundId", - ); + throw PickException("value $value at $debugParsingExit can't be casted to BroadcastRoundId"); } BroadcastRoundId? asBroadcastRoundIddOrNull() { @@ -206,9 +194,7 @@ extension IDPick on Pick { if (value is String) { return BroadcastGameId(value); } - throw PickException( - "value $value at $debugParsingExit can't be casted to BroadcastGameId", - ); + throw PickException("value $value at $debugParsingExit can't be casted to BroadcastGameId"); } BroadcastGameId? asBroadcastGameIddOrNull() { @@ -225,9 +211,7 @@ extension IDPick on Pick { if (value is String) { return StudyId(value); } - throw PickException( - "value $value at $debugParsingExit can't be casted to StudyId", - ); + throw PickException("value $value at $debugParsingExit can't be casted to StudyId"); } FideId asFideIdOrThrow() { @@ -235,9 +219,7 @@ extension IDPick on Pick { if (value is String) { return FideId(value); } - throw PickException( - "value $value at $debugParsingExit can't be casted to FideId", - ); + throw PickException("value $value at $debugParsingExit can't be casted to FideId"); } FideId? asFideIdOrNull() { diff --git a/lib/src/model/common/node.dart b/lib/src/model/common/node.dart index fd971cc615..5aed5f5540 100644 --- a/lib/src/model/common/node.dart +++ b/lib/src/model/common/node.dart @@ -19,11 +19,7 @@ final _logger = Logger('Node'); /// It should not be directly used in a riverpod state, because it is mutable. /// It can be converted into an immutable [ViewNode], using the [view] getter. abstract class Node { - Node({ - required this.position, - this.eval, - this.opening, - }); + Node({required this.position, this.eval, this.opening}); final Position position; @@ -194,17 +190,11 @@ abstract class Node { } /// Adds a list of nodes at the given path and returns the new path. - UciPath? addNodesAt( - UciPath path, - Iterable newNodes, { - bool prepend = false, - }) { + UciPath? addNodesAt(UciPath path, Iterable newNodes, {bool prepend = false}) { final node = newNodes.elementAtOrNull(0); if (node == null) return path; final (newPath, _) = addNodeAt(path, node, prepend: prepend); - return newPath != null - ? addNodesAt(newPath, newNodes.skip(1), prepend: prepend) - : null; + return newPath != null ? addNodesAt(newPath, newNodes.skip(1), prepend: prepend) : null; } /// Adds a new node with that [Move] at the given path. @@ -225,12 +215,12 @@ abstract class Node { }) { final pos = nodeAt(path).position; - final potentialAltCastlingMove = move is NormalMove && + final potentialAltCastlingMove = + move is NormalMove && pos.board.roleAt(move.from) == Role.king && pos.board.roleAt(move.to) != Role.rook; - final convertedMove = - potentialAltCastlingMove ? convertAltCastlingMove(move) ?? move : move; + final convertedMove = potentialAltCastlingMove ? convertAltCastlingMove(move) ?? move : move; final (newPos, newSan) = pos.makeSan(convertedMove); final newNode = Branch( @@ -245,9 +235,7 @@ abstract class Node { /// castling move and converts it to the corresponding standard castling move if so. Move? convertAltCastlingMove(Move move) { return altCastles.containsValue(move.uci) - ? Move.parse( - altCastles.entries.firstWhere((e) => e.value == move.uci).key, - ) + ? Move.parse(altCastles.entries.firstWhere((e) => e.value == move.uci).key) : move; } @@ -310,51 +298,35 @@ abstract class Node { /// Export the tree to a PGN string. /// /// Optionally, headers and initial game comments can be provided. - String makePgn([ - IMap? headers, - IList? rootComments, - ]) { + String makePgn([IMap? headers, IList? rootComments]) { final pgnNode = PgnNode(); - final List<({Node from, PgnNode to})> stack = [ - (from: this, to: pgnNode), - ]; + final List<({Node from, PgnNode to})> stack = [(from: this, to: pgnNode)]; while (stack.isNotEmpty) { final frame = stack.removeLast(); - for (int childIdx = 0; - childIdx < frame.from.children.length; - childIdx++) { + for (int childIdx = 0; childIdx < frame.from.children.length; childIdx++) { final childFrom = frame.from.children[childIdx]; final childTo = PgnChildNode( PgnNodeData( san: childFrom.sanMove.san, - startingComments: childFrom.startingComments - ?.map((c) => c.makeComment()) - .toList(), + startingComments: childFrom.startingComments?.map((c) => c.makeComment()).toList(), comments: - (childFrom.lichessAnalysisComments ?? childFrom.comments)?.map( - (c) { - final eval = childFrom.eval; - final pgnEval = eval?.cp != null - ? PgnEvaluation.pawns( - pawns: cpToPawns(eval!.cp!), - depth: eval.depth, - ) - : eval?.mate != null - ? PgnEvaluation.mate( - mate: eval!.mate, - depth: eval.depth, - ) - : c.eval; - return PgnComment( - text: c.text, - shapes: c.shapes, - clock: c.clock, - emt: c.emt, - eval: pgnEval, - ).makeComment(); - }, - ).toList(), + (childFrom.lichessAnalysisComments ?? childFrom.comments)?.map((c) { + final eval = childFrom.eval; + final pgnEval = + eval?.cp != null + ? PgnEvaluation.pawns(pawns: cpToPawns(eval!.cp!), depth: eval.depth) + : eval?.mate != null + ? PgnEvaluation.mate(mate: eval!.mate, depth: eval.depth) + : c.eval; + return PgnComment( + text: c.text, + shapes: c.shapes, + clock: c.clock, + emt: c.emt, + eval: pgnEval, + ).makeComment(); + }).toList(), nags: childFrom.nags, ), ); @@ -419,18 +391,18 @@ class Branch extends Node { @override ViewBranch get view => ViewBranch( - position: position, - sanMove: sanMove, - eval: eval, - opening: opening, - children: IList(children.map((child) => child.view)), - isComputerVariation: isComputerVariation, - isCollapsed: isCollapsed, - lichessAnalysisComments: lichessAnalysisComments?.lock, - startingComments: startingComments?.lock, - comments: comments?.lock, - nags: nags?.lock, - ); + position: position, + sanMove: sanMove, + eval: eval, + opening: opening, + children: IList(children.map((child) => child.view)), + isComputerVariation: isComputerVariation, + isCollapsed: isCollapsed, + lichessAnalysisComments: lichessAnalysisComments?.lock, + startingComments: startingComments?.lock, + comments: comments?.lock, + nags: nags?.lock, + ); /// Gets the branch at the given path @override @@ -443,9 +415,7 @@ class Branch extends Node { if (lichessAnalysisComments == null) { lichessAnalysisComments = [c]; } else { - final existing = lichessAnalysisComments?.firstWhereOrNull( - (e) => e.text == c.text, - ); + final existing = lichessAnalysisComments?.firstWhereOrNull((e) => e.text == c.text); if (existing == null) { lichessAnalysisComments?.add(c); } @@ -455,9 +425,7 @@ class Branch extends Node { if (startingComments == null) { startingComments = [c]; } else { - final existing = startingComments?.firstWhereOrNull( - (e) => e.text == c.text, - ); + final existing = startingComments?.firstWhereOrNull((e) => e.text == c.text); if (existing == null) { startingComments?.add(c); } @@ -467,9 +435,7 @@ class Branch extends Node { if (comments == null) { comments = [c]; } else { - final existing = comments?.firstWhereOrNull( - (e) => e.text == c.text, - ); + final existing = comments?.firstWhereOrNull((e) => e.text == c.text); if (existing == null) { comments?.add(c); } @@ -484,19 +450,16 @@ class Branch extends Node { /// Gets the first available clock from the comments. Duration? get clock { - final clockComment = (lichessAnalysisComments ?? comments) - ?.firstWhereOrNull((c) => c.clock != null); + final clockComment = (lichessAnalysisComments ?? comments)?.firstWhereOrNull( + (c) => c.clock != null, + ); return clockComment?.clock; } /// Gets the first available external eval from the comments. ExternalEval? get externalEval { - final comment = (lichessAnalysisComments ?? comments)?.firstWhereOrNull( - (c) => c.eval != null, - ); - return comment?.eval != null - ? ExternalEval.fromPgnEval(comment!.eval!) - : null; + final comment = (lichessAnalysisComments ?? comments)?.firstWhereOrNull((c) => c.eval != null); + return comment?.eval != null ? ExternalEval.fromPgnEval(comment!.eval!) : null; } @override @@ -509,35 +472,27 @@ class Branch extends Node { /// /// Represents the initial position, where no move has been played yet. class Root extends Node { - Root({ - required super.position, - super.eval, - }); + Root({required super.position, super.eval}); @override ViewRoot get view => ViewRoot( - position: position, - eval: eval, - children: IList(children.map((child) => child.view)), - ); + position: position, + eval: eval, + children: IList(children.map((child) => child.view)), + ); /// Creates a flat game tree from a PGN string. /// /// Assumes that the PGN string is valid and that the moves are legal. factory Root.fromPgnMoves(String pgn) { Position position = Chess.initial; - final root = Root( - position: position, - ); + final root = Root(position: position); Node current = root; final moves = pgn.split(' '); for (final san in moves) { final move = position.parseSan(san); position = position.playUnchecked(move!); - final nextNode = Branch( - sanMove: SanMove(san, move), - position: position, - ); + final nextNode = Branch(sanMove: SanMove(san, move), position: position); current.addChild(nextNode); current = nextNode; } @@ -554,9 +509,7 @@ class Root extends Node { bool hideVariations = false, void Function(Root root, Branch branch, bool isMainline)? onVisitNode, }) { - final root = Root( - position: PgnGame.startingPosition(game.headers), - ); + final root = Root(position: PgnGame.startingPosition(game.headers)); final List<({PgnNode from, Node to, int nesting})> stack = [ (from: game.moves, to: root, nesting: 1), @@ -564,9 +517,7 @@ class Root extends Node { while (stack.isNotEmpty) { final frame = stack.removeLast(); - for (int childIdx = 0; - childIdx < frame.from.children.length; - childIdx++) { + for (int childIdx = 0; childIdx < frame.from.children.length; childIdx++) { final childFrom = frame.from.children[childIdx]; final move = frame.to.position.parseSan(childFrom.data.san); if (move != null) { @@ -579,35 +530,30 @@ class Root extends Node { position: newPos, isCollapsed: frame.nesting > 2 || hideVariations && childIdx > 0, isComputerVariation: isLichessAnalysis && childIdx > 0, - lichessAnalysisComments: - isLichessAnalysis ? comments?.toList() : null, - startingComments: isLichessAnalysis - ? null - : childFrom.data.startingComments - ?.map(PgnComment.fromPgn) - .toList(), + lichessAnalysisComments: isLichessAnalysis ? comments?.toList() : null, + startingComments: + isLichessAnalysis + ? null + : childFrom.data.startingComments?.map(PgnComment.fromPgn).toList(), comments: isLichessAnalysis ? null : comments?.toList(), nags: childFrom.data.nags, ); frame.to.addChild(branch); - stack.add( - ( - from: childFrom, - to: branch, - nesting: frame.from.children.length == 1 || - // mainline continuation - (childIdx == 0 && frame.nesting == 1) - ? frame.nesting - : frame.nesting + 1, - ), - ); + stack.add(( + from: childFrom, + to: branch, + nesting: + frame.from.children.length == 1 || + // mainline continuation + (childIdx == 0 && frame.nesting == 1) + ? frame.nesting + : frame.nesting + 1, + )); onVisitNode?.call(root, branch, isMainline); } else { - _logger.warning( - 'Invalid move: ${childFrom.data.san}, on position: ${frame.to.position}', - ); + _logger.warning('Invalid move: ${childFrom.data.san}, on position: ${frame.to.position}'); } } } @@ -707,14 +653,16 @@ class ViewBranch extends ViewNode with _$ViewBranch { bool get hasTextComment => textComments.isNotEmpty; Duration? get clock { - final clockComment = (lichessAnalysisComments ?? comments) - ?.firstWhereOrNull((c) => c.clock != null); + final clockComment = (lichessAnalysisComments ?? comments)?.firstWhereOrNull( + (c) => c.clock != null, + ); return clockComment?.clock; } Duration? get elapsedMoveTime { - final clockComment = (lichessAnalysisComments ?? comments) - ?.firstWhereOrNull((c) => c.emt != null); + final clockComment = (lichessAnalysisComments ?? comments)?.firstWhereOrNull( + (c) => c.emt != null, + ); return clockComment?.emt; } diff --git a/lib/src/model/common/perf.dart b/lib/src/model/common/perf.dart index 5df2cbe0e5..8b297cad48 100644 --- a/lib/src/model/common/perf.dart +++ b/lib/src/model/common/perf.dart @@ -73,11 +73,11 @@ enum Perf { static final IMap nameMap = IMap(Perf.values.asNameMap()); } -String _titleKey(String title) => - title.toLowerCase().replaceAll(RegExp('[ -_]'), ''); +String _titleKey(String title) => title.toLowerCase().replaceAll(RegExp('[ -_]'), ''); -final IMap _lowerCaseTitleMap = - Perf.nameMap.map((key, value) => MapEntry(_titleKey(value.title), value)); +final IMap _lowerCaseTitleMap = Perf.nameMap.map( + (key, value) => MapEntry(_titleKey(value.title), value), +); extension PerfExtension on Pick { Perf asPerfOrThrow() { @@ -104,9 +104,7 @@ extension PerfExtension on Pick { return perf; } } - throw PickException( - "value $value at $debugParsingExit can't be casted to Perf", - ); + throw PickException("value $value at $debugParsingExit can't be casted to Perf"); } Perf? asPerfOrNull() { diff --git a/lib/src/model/common/preloaded_data.dart b/lib/src/model/common/preloaded_data.dart index 95b3cbfc05..da0531202a 100644 --- a/lib/src/model/common/preloaded_data.dart +++ b/lib/src/model/common/preloaded_data.dart @@ -12,13 +12,14 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'preloaded_data.g.dart'; -typedef PreloadedData = ({ - PackageInfo packageInfo, - BaseDeviceInfo deviceInfo, - AuthSessionState? userSession, - String sri, - int engineMaxMemoryInMb, -}); +typedef PreloadedData = + ({ + PackageInfo packageInfo, + BaseDeviceInfo deviceInfo, + AuthSessionState? userSession, + String sri, + int engineMaxMemoryInMb, + }); @Riverpod(keepAlive: true) Future preloadedData(Ref ref) async { diff --git a/lib/src/model/common/service/sound_service.dart b/lib/src/model/common/service/sound_service.dart index ad398c86ea..c7da648e31 100644 --- a/lib/src/model/common/service/sound_service.dart +++ b/lib/src/model/common/service/sound_service.dart @@ -17,16 +17,7 @@ final _soundEffectPlugin = SoundEffect(); final _logger = Logger('SoundService'); // Must match name of files in assets/sounds/standard -enum Sound { - move, - capture, - lowTime, - dong, - error, - confirmation, - puzzleStormEnd, - clock, -} +enum Sound { move, capture, lowTime, dong, error, confirmation, puzzleStormEnd, clock } @Riverpod(keepAlive: true) SoundService soundService(Ref ref) { @@ -40,14 +31,9 @@ final _extension = defaultTargetPlatform == TargetPlatform.iOS ? 'aifc' : 'mp3'; const Set _emtpySet = {}; /// Loads all sounds of the given [SoundTheme]. -Future _loadAllSounds( - SoundTheme soundTheme, { - Set excluded = _emtpySet, -}) async { +Future _loadAllSounds(SoundTheme soundTheme, {Set excluded = _emtpySet}) async { await Future.wait( - Sound.values - .where((s) => !excluded.contains(s)) - .map((sound) => _loadSound(soundTheme, sound)), + Sound.values.where((s) => !excluded.contains(s)).map((sound) => _loadSound(soundTheme, sound)), ); } @@ -79,14 +65,14 @@ class SoundService { /// This should be called once when the app starts. static Future initialize() async { try { - final stored = LichessBinding.instance.sharedPreferences - .getString(PrefCategory.general.storageKey); - final theme = (stored != null - ? GeneralPrefs.fromJson( - jsonDecode(stored) as Map, - ) - : GeneralPrefs.defaults) - .soundTheme; + final stored = LichessBinding.instance.sharedPreferences.getString( + PrefCategory.general.storageKey, + ); + final theme = + (stored != null + ? GeneralPrefs.fromJson(jsonDecode(stored) as Map) + : GeneralPrefs.defaults) + .soundTheme; await _soundEffectPlugin.initialize(); await _loadAllSounds(theme); } catch (e) { @@ -109,10 +95,7 @@ class SoundService { /// This will release the previous sounds and load the new ones. /// /// If [playSound] is true, a move sound will be played. - Future changeTheme( - SoundTheme theme, { - bool playSound = false, - }) async { + Future changeTheme(SoundTheme theme, {bool playSound = false}) async { await _soundEffectPlugin.release(); await _soundEffectPlugin.initialize(); await _loadSound(theme, Sound.move); diff --git a/lib/src/model/common/socket.dart b/lib/src/model/common/socket.dart index 696d140d46..cb258fee08 100644 --- a/lib/src/model/common/socket.dart +++ b/lib/src/model/common/socket.dart @@ -21,10 +21,7 @@ class SocketEvent with _$SocketEvent { factory SocketEvent.fromJson(Map json) { if (json['t'] == null) { if (json['v'] != null) { - return SocketEvent( - topic: '_version', - version: json['v'] as int, - ); + return SocketEvent(topic: '_version', version: json['v'] as int); } else { assert(false, 'Unsupported socket event json: $json'); return pong; @@ -34,16 +31,9 @@ class SocketEvent with _$SocketEvent { if (topic == 'n') { return SocketEvent( topic: topic, - data: { - 'nbPlayers': json['d'] as int, - 'nbGames': json['r'] as int, - }, + data: {'nbPlayers': json['d'] as int, 'nbGames': json['r'] as int}, ); } - return SocketEvent( - topic: topic, - data: json['d'], - version: json['v'] as int?, - ); + return SocketEvent(topic: topic, data: json['d'], version: json['v'] as int?); } } diff --git a/lib/src/model/common/speed.dart b/lib/src/model/common/speed.dart index b608fdf9aa..b3656c12f2 100644 --- a/lib/src/model/common/speed.dart +++ b/lib/src/model/common/speed.dart @@ -47,9 +47,7 @@ extension SpeedExtension on Pick { final speed = Speed.nameMap[value]; if (speed != null) return speed; } - throw PickException( - "value $value at $debugParsingExit can't be casted to Speed", - ); + throw PickException("value $value at $debugParsingExit can't be casted to Speed"); } Speed? asSpeedOrNull() { diff --git a/lib/src/model/common/time_increment.dart b/lib/src/model/common/time_increment.dart index 8359b98b8c..adc4558ae7 100644 --- a/lib/src/model/common/time_increment.dart +++ b/lib/src/model/common/time_increment.dart @@ -5,13 +5,12 @@ import 'package:lichess_mobile/src/model/common/speed.dart'; /// A pair of time and increment in seconds used as game clock @immutable class TimeIncrement { - const TimeIncrement(this.time, this.increment) - : assert(time >= 0 && increment >= 0); + const TimeIncrement(this.time, this.increment) : assert(time >= 0 && increment >= 0); TimeIncrement.fromDurations(Duration time, Duration increment) - : time = time.inSeconds, - increment = increment.inSeconds, - assert(time >= Duration.zero && increment >= Duration.zero); + : time = time.inSeconds, + increment = increment.inSeconds, + assert(time >= Duration.zero && increment >= Duration.zero); /// Clock initial time in seconds final int time; @@ -20,13 +19,10 @@ class TimeIncrement { final int increment; TimeIncrement.fromJson(Map json) - : time = json['time'] as int, - increment = json['increment'] as int; + : time = json['time'] as int, + increment = json['increment'] as int; - Map toJson() => { - 'time': time, - 'increment': increment, - }; + Map toJson() => {'time': time, 'increment': increment}; /// Returns the estimated duration of the game, with increment * 40 added to /// the initial time. diff --git a/lib/src/model/common/uci.dart b/lib/src/model/common/uci.dart index cc8c8b6716..85185f268c 100644 --- a/lib/src/model/common/uci.dart +++ b/lib/src/model/common/uci.dart @@ -32,43 +32,25 @@ class UciCharPair with _$UciCharPair { } factory UciCharPair.fromMove(Move move) => switch (move) { - NormalMove(from: final f, to: final t, promotion: final p) => - UciCharPair( - String.fromCharCode(35 + f), - String.fromCharCode( - p != null - ? 35 + 64 + 8 * _promotionRoles.indexOf(p) + t.file - : 35 + t, - ), - ), - DropMove(to: final t, role: final r) => UciCharPair( - String.fromCharCode(35 + t), - String.fromCharCode(35 + 64 + 8 * 5 + _dropRoles.indexOf(r)), - ), - }; - - factory UciCharPair.fromJson(Map json) => - _$UciCharPairFromJson(json); + NormalMove(from: final f, to: final t, promotion: final p) => UciCharPair( + String.fromCharCode(35 + f), + String.fromCharCode(p != null ? 35 + 64 + 8 * _promotionRoles.indexOf(p) + t.file : 35 + t), + ), + DropMove(to: final t, role: final r) => UciCharPair( + String.fromCharCode(35 + t), + String.fromCharCode(35 + 64 + 8 * 5 + _dropRoles.indexOf(r)), + ), + }; + + factory UciCharPair.fromJson(Map json) => _$UciCharPairFromJson(json); @override String toString() => '$a$b'; } -const _promotionRoles = [ - Role.queen, - Role.rook, - Role.bishop, - Role.knight, - Role.king, -]; - -const _dropRoles = [ - Role.queen, - Role.rook, - Role.bishop, - Role.knight, - Role.pawn, -]; +const _promotionRoles = [Role.queen, Role.rook, Role.bishop, Role.knight, Role.king]; + +const _dropRoles = [Role.queen, Role.rook, Role.bishop, Role.knight, Role.pawn]; /// Compact representation of a path to a game node made from concatenated /// UciCharPair strings. @@ -111,22 +93,17 @@ class UciPath with _$UciPath { int get size => value.length ~/ 2; - UciCharPair? get head => - value.isEmpty ? null : UciCharPair(value[0], value[1]); + UciCharPair? get head => value.isEmpty ? null : UciCharPair(value[0], value[1]); - UciCharPair? get last => value.isEmpty - ? null - : UciCharPair(value[value.length - 2], value[value.length - 1]); + UciCharPair? get last => + value.isEmpty ? null : UciCharPair(value[value.length - 2], value[value.length - 1]); - UciPath get tail => - value.isEmpty ? UciPath.empty : UciPath(value.substring(2)); + UciPath get tail => value.isEmpty ? UciPath.empty : UciPath(value.substring(2)); - UciPath get penultimate => value.isEmpty - ? UciPath.empty - : UciPath(value.substring(0, value.length - 2)); + UciPath get penultimate => + value.isEmpty ? UciPath.empty : UciPath(value.substring(0, value.length - 2)); bool get isEmpty => value.isEmpty; - factory UciPath.fromJson(Map json) => - _$UciPathFromJson(json); + factory UciPath.fromJson(Map json) => _$UciPathFromJson(json); } diff --git a/lib/src/model/coordinate_training/coordinate_training_controller.dart b/lib/src/model/coordinate_training/coordinate_training_controller.dart index f0e7bf5d93..94dbbbc228 100644 --- a/lib/src/model/coordinate_training/coordinate_training_controller.dart +++ b/lib/src/model/coordinate_training/coordinate_training_controller.dart @@ -28,9 +28,7 @@ class CoordinateTrainingController extends _$CoordinateTrainingController { final sideChoice = ref.watch( coordinateTrainingPreferencesProvider.select((value) => value.sideChoice), ); - return CoordinateTrainingState( - orientation: _getOrientation(sideChoice), - ); + return CoordinateTrainingState(orientation: _getOrientation(sideChoice)); } void startTraining(Duration? timeLimit) { @@ -51,18 +49,14 @@ class CoordinateTrainingController extends _$CoordinateTrainingController { if (state.timeLimit != null && _stopwatch.elapsed > state.timeLimit!) { _finishTraining(); } else { - state = state.copyWith( - elapsed: _stopwatch.elapsed, - ); + state = state.copyWith(elapsed: _stopwatch.elapsed); } }); } void _finishTraining() { // TODO save score in local storage here (and display high score and/or average score in UI) - final orientation = _getOrientation( - ref.read(coordinateTrainingPreferencesProvider).sideChoice, - ); + final orientation = _getOrientation(ref.read(coordinateTrainingPreferencesProvider).sideChoice); _updateTimer?.cancel(); state = CoordinateTrainingState( lastGuess: state.lastGuess, @@ -72,18 +66,16 @@ class CoordinateTrainingController extends _$CoordinateTrainingController { } void abortTraining() { - final orientation = _getOrientation( - ref.read(coordinateTrainingPreferencesProvider).sideChoice, - ); + final orientation = _getOrientation(ref.read(coordinateTrainingPreferencesProvider).sideChoice); _updateTimer?.cancel(); state = CoordinateTrainingState(orientation: orientation); } Side _getOrientation(SideChoice choice) => switch (choice) { - SideChoice.white => Side.white, - SideChoice.black => Side.black, - SideChoice.random => _randomSide(), - }; + SideChoice.white => Side.white, + SideChoice.black => Side.black, + SideChoice.random => _randomSide(), + }; /// Generate a random side Side _randomSide() => Side.values[Random().nextInt(Side.values.length)]; @@ -109,9 +101,7 @@ class CoordinateTrainingController extends _$CoordinateTrainingController { ); } - state = state.copyWith( - lastGuess: correctGuess ? Guess.correct : Guess.incorrect, - ); + state = state.copyWith(lastGuess: correctGuess ? Guess.correct : Guess.incorrect); } } @@ -132,7 +122,8 @@ class CoordinateTrainingState with _$CoordinateTrainingState { bool get trainingActive => elapsed != null; - double? get timeFractionElapsed => (elapsed != null && timeLimit != null) - ? elapsed!.inMilliseconds / timeLimit!.inMilliseconds - : null; + double? get timeFractionElapsed => + (elapsed != null && timeLimit != null) + ? elapsed!.inMilliseconds / timeLimit!.inMilliseconds + : null; } diff --git a/lib/src/model/coordinate_training/coordinate_training_preferences.dart b/lib/src/model/coordinate_training/coordinate_training_preferences.dart index 435d97d672..09273ce082 100644 --- a/lib/src/model/coordinate_training/coordinate_training_preferences.dart +++ b/lib/src/model/coordinate_training/coordinate_training_preferences.dart @@ -85,9 +85,7 @@ enum TrainingMode { } @Freezed(fromJson: true, toJson: true) -class CoordinateTrainingPrefs - with _$CoordinateTrainingPrefs - implements Serializable { +class CoordinateTrainingPrefs with _$CoordinateTrainingPrefs implements Serializable { const CoordinateTrainingPrefs._(); const factory CoordinateTrainingPrefs({ diff --git a/lib/src/model/correspondence/correspondence_game_storage.dart b/lib/src/model/correspondence/correspondence_game_storage.dart index bfa4881f9c..fe1a259177 100644 --- a/lib/src/model/correspondence/correspondence_game_storage.dart +++ b/lib/src/model/correspondence/correspondence_game_storage.dart @@ -14,30 +14,27 @@ import 'offline_correspondence_game.dart'; part 'correspondence_game_storage.g.dart'; @Riverpod(keepAlive: true) -Future correspondenceGameStorage( - Ref ref, -) async { +Future correspondenceGameStorage(Ref ref) async { final db = await ref.watch(databaseProvider.future); return CorrespondenceGameStorage(db, ref); } @riverpod -Future> - offlineOngoingCorrespondenceGames(Ref ref) async { +Future> offlineOngoingCorrespondenceGames( + Ref ref, +) async { final session = ref.watch(authSessionProvider); // cannot use ref.watch because it would create a circular dependency // as we invalidate this provider in the storage save and delete methods final storage = await ref.read(correspondenceGameStorageProvider.future); final data = await storage.fetchOngoingGames(session?.user.id); - return data.sort( - (a, b) { - final aIsMyTurn = a.$2.isMyTurn; - final bIsMyTurn = b.$2.isMyTurn; - if (aIsMyTurn && !bIsMyTurn) return -1; - if (!aIsMyTurn && bIsMyTurn) return 1; - return b.$1.compareTo(a.$1); - }, - ); + return data.sort((a, b) { + final aIsMyTurn = a.$2.isMyTurn; + final bIsMyTurn = b.$2.isMyTurn; + if (aIsMyTurn && !bIsMyTurn) return -1; + if (!aIsMyTurn && bIsMyTurn) return 1; + return b.$1.compareTo(a.$1); + }); } const kCorrespondenceStorageTable = 'correspondence_game'; @@ -50,16 +47,11 @@ class CorrespondenceGameStorage { final Ref ref; /// Fetches all ongoing correspondence games, sorted by time left. - Future> fetchOngoingGames( - UserId? userId, - ) async { + Future> fetchOngoingGames(UserId? userId) async { final list = await _db.query( kCorrespondenceStorageTable, where: 'userId = ? AND data LIKE ?', - whereArgs: [ - '${userId ?? kCorrespondenceStorageAnonId}', - '%"status":"started"%', - ], + whereArgs: ['${userId ?? kCorrespondenceStorageAnonId}', '%"status":"started"%'], ); return _decodeGames(list).sort((a, b) { @@ -76,8 +68,9 @@ class CorrespondenceGameStorage { } /// Fetches all correspondence games with a registered move. - Future> - fetchGamesWithRegisteredMove(UserId? userId) async { + Future> fetchGamesWithRegisteredMove( + UserId? userId, + ) async { try { final list = await _db.query( kCorrespondenceStorageTable, @@ -88,10 +81,7 @@ class CorrespondenceGameStorage { final list = await _db.query( kCorrespondenceStorageTable, where: 'userId = ? AND data LIKE ?', - whereArgs: [ - '${userId ?? kCorrespondenceStorageAnonId}', - '%status":"started"%', - ], + whereArgs: ['${userId ?? kCorrespondenceStorageAnonId}', '%status":"started"%'], ); return _decodeGames(list).where((e) { @@ -101,9 +91,7 @@ class CorrespondenceGameStorage { } } - Future fetch({ - required GameId gameId, - }) async { + Future fetch({required GameId gameId}) async { final list = await _db.query( kCorrespondenceStorageTable, where: 'gameId = ?', @@ -126,16 +114,12 @@ class CorrespondenceGameStorage { Future save(OfflineCorrespondenceGame game) async { try { - await _db.insert( - kCorrespondenceStorageTable, - { - 'userId': game.me.user?.id.toString() ?? kCorrespondenceStorageAnonId, - 'gameId': game.id.toString(), - 'lastModified': DateTime.now().toIso8601String(), - 'data': jsonEncode(game.toJson()), - }, - conflictAlgorithm: ConflictAlgorithm.replace, - ); + await _db.insert(kCorrespondenceStorageTable, { + 'userId': game.me.user?.id.toString() ?? kCorrespondenceStorageAnonId, + 'gameId': game.id.toString(), + 'lastModified': DateTime.now().toIso8601String(), + 'data': jsonEncode(game.toJson()), + }, conflictAlgorithm: ConflictAlgorithm.replace); ref.invalidate(offlineOngoingCorrespondenceGamesProvider); } catch (e) { debugPrint('[CorrespondenceGameStorage] failed to save game: $e'); @@ -151,9 +135,7 @@ class CorrespondenceGameStorage { ref.invalidate(offlineOngoingCorrespondenceGamesProvider); } - IList<(DateTime, OfflineCorrespondenceGame)> _decodeGames( - List> list, - ) { + IList<(DateTime, OfflineCorrespondenceGame)> _decodeGames(List> list) { return list.map((e) { final lmString = e['lastModified'] as String?; final raw = e['data'] as String?; diff --git a/lib/src/model/correspondence/correspondence_service.dart b/lib/src/model/correspondence/correspondence_service.dart index 05ad5f800a..f8fa995758 100644 --- a/lib/src/model/correspondence/correspondence_service.dart +++ b/lib/src/model/correspondence/correspondence_service.dart @@ -28,10 +28,7 @@ part 'correspondence_service.g.dart'; @Riverpod(keepAlive: true) CorrespondenceService correspondenceService(Ref ref) { - return CorrespondenceService( - Logger('CorrespondenceService'), - ref: ref, - ); + return CorrespondenceService(Logger('CorrespondenceService'), ref: ref); } /// Services that manages correspondence games. @@ -68,8 +65,7 @@ class CorrespondenceService { await playRegisteredMoves(); - final storedOngoingGames = - await (await _storage).fetchOngoingGames(_session?.user.id); + final storedOngoingGames = await (await _storage).fetchOngoingGames(_session?.user.id); ref.withClient((client) async { try { @@ -113,16 +109,11 @@ class CorrespondenceService { final games = await (await _storage) .fetchGamesWithRegisteredMove(_session?.user.id) - .then( - (games) => games.map((e) => e.$2).toList(), - ); + .then((games) => games.map((e) => e.$2).toList()); WebSocket.userAgent = ref.read(userAgentProvider); - final Map wsHeaders = _session != null - ? { - 'Authorization': 'Bearer ${signBearerToken(_session!.token)}', - } - : {}; + final Map wsHeaders = + _session != null ? {'Authorization': 'Bearer ${signBearerToken(_session!.token)}'} : {}; int movesPlayed = 0; @@ -134,14 +125,14 @@ class CorrespondenceService { WebSocket? socket; StreamSubscription? streamSubscription; try { - socket = await WebSocket.connect(uri.toString(), headers: wsHeaders) - .timeout(const Duration(seconds: 5)); + socket = await WebSocket.connect( + uri.toString(), + headers: wsHeaders, + ).timeout(const Duration(seconds: 5)); - final eventStream = socket.where((e) => e != '0').map( - (e) => SocketEvent.fromJson( - jsonDecode(e as String) as Map, - ), - ); + final eventStream = socket + .where((e) => e != '0') + .map((e) => SocketEvent.fromJson(jsonDecode(e as String) as Map)); final Completer gameCompleter = Completer(); final Completer movePlayedCompleter = Completer(); @@ -149,14 +140,11 @@ class CorrespondenceService { streamSubscription = eventStream.listen((event) { switch (event.topic) { case 'full': - final playableGame = GameFullEvent.fromJson( - event.data as Map, - ).game; + final playableGame = GameFullEvent.fromJson(event.data as Map).game; gameCompleter.complete(playableGame); case 'move': - final moveEvent = - MoveEvent.fromJson(event.data as Map); + final moveEvent = MoveEvent.fromJson(event.data as Map); // move acknowledged if (moveEvent.uci == gameToSync.registeredMoveAtPgn!.$2.uci) { movesPlayed++; @@ -174,31 +162,21 @@ class CorrespondenceService { socket.add( jsonEncode({ 't': 'move', - 'd': { - 'u': gameToSync.registeredMoveAtPgn!.$2.uci, - }, + 'd': {'u': gameToSync.registeredMoveAtPgn!.$2.uci}, }), ); await movePlayedCompleter.future.timeout(const Duration(seconds: 3)); - (await ref.read(correspondenceGameStorageProvider.future)).save( - gameToSync.copyWith( - registeredMoveAtPgn: null, - ), - ); + (await ref.read( + correspondenceGameStorageProvider.future, + )).save(gameToSync.copyWith(registeredMoveAtPgn: null)); } else { - _log.info( - 'Cannot play game ${gameToSync.id} move because its state has changed', - ); + _log.info('Cannot play game ${gameToSync.id} move because its state has changed'); updateGame(gameToSync.fullId, playableGame); } } catch (e, s) { - _log.severe( - 'Failed to sync correspondence game ${gameToSync.id}', - e, - s, - ); + _log.severe('Failed to sync correspondence game ${gameToSync.id}', e, s); } finally { streamSubscription?.cancel(); socket?.close(); diff --git a/lib/src/model/correspondence/offline_correspondence_game.dart b/lib/src/model/correspondence/offline_correspondence_game.dart index d7bcb1599f..83a1d6a354 100644 --- a/lib/src/model/correspondence/offline_correspondence_game.dart +++ b/lib/src/model/correspondence/offline_correspondence_game.dart @@ -25,8 +25,7 @@ class OfflineCorrespondenceGame required GameId id, required GameFullId fullId, required GameMeta meta, - @JsonKey(fromJson: stepsFromJson, toJson: stepsToJson) - required IList steps, + @JsonKey(fromJson: stepsFromJson, toJson: stepsToJson) required IList steps, CorrespondenceClockData? clock, String? initialFen, required bool rated, @@ -61,8 +60,7 @@ class OfflineCorrespondenceGame bool get isMyTurn => sideToMove == youAre; - Duration? myTimeLeft(DateTime lastModifiedTime) => - estimatedTimeLeft(youAre, lastModifiedTime); + Duration? myTimeLeft(DateTime lastModifiedTime) => estimatedTimeLeft(youAre, lastModifiedTime); Duration? estimatedTimeLeft(Side side, DateTime lastModifiedTime) { final timeLeft = side == Side.white ? clock?.white : clock?.black; diff --git a/lib/src/model/engine/engine.dart b/lib/src/model/engine/engine.dart index ef8163ba28..9833f06658 100644 --- a/lib/src/model/engine/engine.dart +++ b/lib/src/model/engine/engine.dart @@ -7,14 +7,7 @@ import 'package:stockfish/stockfish.dart'; import 'uci_protocol.dart'; import 'work.dart'; -enum EngineState { - initial, - loading, - idle, - computing, - error, - disposed, -} +enum EngineState { initial, loading, idle, computing, error, disposed } abstract class Engine { ValueListenable get state; @@ -44,36 +37,36 @@ class StockfishEngine implements Engine { @override Stream start(Work work) { - _log.info( - 'engine start at ply ${work.ply} and path ${work.path}', - ); + _log.info('engine start at ply ${work.ply} and path ${work.path}'); _protocol.compute(work); if (_stockfish == null) { - stockfishAsync().then((stockfish) { - _state.value = EngineState.loading; - _stockfish = stockfish; - _stdoutSubscription = stockfish.stdout.listen((line) { - _protocol.received(line); - }); - stockfish.state.addListener(_stockfishStateListener); - _protocol.isComputing.addListener(() { - if (_protocol.isComputing.value) { - _state.value = EngineState.computing; - } else { - _state.value = EngineState.idle; - } - }); - _protocol.connected((String cmd) { - stockfish.stdin = cmd; - }); - _protocol.engineName.then((name) { - _name = name; - }); - }).catchError((Object e, StackTrace s) { - _log.severe('error loading stockfish', e, s); - _state.value = EngineState.error; - }); + stockfishAsync() + .then((stockfish) { + _state.value = EngineState.loading; + _stockfish = stockfish; + _stdoutSubscription = stockfish.stdout.listen((line) { + _protocol.received(line); + }); + stockfish.state.addListener(_stockfishStateListener); + _protocol.isComputing.addListener(() { + if (_protocol.isComputing.value) { + _state.value = EngineState.computing; + } else { + _state.value = EngineState.idle; + } + }); + _protocol.connected((String cmd) { + stockfish.stdin = cmd; + }); + _protocol.engineName.then((name) { + _name = name; + }); + }) + .catchError((Object e, StackTrace s) { + _log.severe('error loading stockfish', e, s); + _state.value = EngineState.error; + }); } return _protocol.evalStream.where((e) => e.$1 == work); @@ -100,8 +93,7 @@ class StockfishEngine implements Engine { @override Future dispose() { _log.fine('disposing engine'); - if (_stockfish == null || - _stockfish?.state.value == StockfishState.disposed) { + if (_stockfish == null || _stockfish?.state.value == StockfishState.disposed) { return Future.value(); } _stdoutSubscription?.cancel(); diff --git a/lib/src/model/engine/evaluation_service.dart b/lib/src/model/engine/evaluation_service.dart index 0fc2cf0680..e2006aac28 100644 --- a/lib/src/model/engine/evaluation_service.dart +++ b/lib/src/model/engine/evaluation_service.dart @@ -21,14 +21,9 @@ part 'evaluation_service.g.dart'; part 'evaluation_service.freezed.dart'; final maxEngineCores = max(Platform.numberOfProcessors - 1, 1); -final defaultEngineCores = - min((Platform.numberOfProcessors / 2).ceil(), maxEngineCores); +final defaultEngineCores = min((Platform.numberOfProcessors / 2).ceil(), maxEngineCores); -const engineSupportedVariants = { - Variant.standard, - Variant.chess960, - Variant.fromPosition, -}; +const engineSupportedVariants = {Variant.standard, Variant.chess960, Variant.fromPosition}; /// A service to evaluate chess positions using an engine. class EvaluationService { @@ -47,11 +42,9 @@ class EvaluationService { searchTime: const Duration(seconds: 10), ); - static const _defaultState = - (engineName: 'Stockfish', state: EngineState.initial, eval: null); + static const _defaultState = (engineName: 'Stockfish', state: EngineState.initial, eval: null); - final ValueNotifier _state = - ValueNotifier(_defaultState); + final ValueNotifier _state = ValueNotifier(_defaultState); ValueListenable get state => _state; /// Initialize the engine with the given context and options. @@ -82,7 +75,7 @@ class EvaluationService { _state.value = ( engineName: _engine!.name, state: _engine!.state.value, - eval: _state.value.eval + eval: _state.value.eval, ); } }); @@ -141,8 +134,7 @@ class EvaluationService { ); // cancel evaluation if we already have a cached eval at max search time - final cachedEval = - work.steps.isEmpty ? initialPositionEval : work.evalCache; + final cachedEval = work.steps.isEmpty ? initialPositionEval : work.evalCache; if (cachedEval != null && cachedEval.searchTime >= _options.searchTime) { _state.value = ( engineName: _state.value.engineName, @@ -152,19 +144,14 @@ class EvaluationService { return null; } - final evalStream = engine.start(work).throttle( - const Duration(milliseconds: 200), - trailing: true, - ); + final evalStream = engine + .start(work) + .throttle(const Duration(milliseconds: 200), trailing: true); evalStream.forEach((t) { final (work, eval) = t; if (shouldEmit(work)) { - _state.value = ( - engineName: _state.value.engineName, - state: _state.value.state, - eval: eval, - ); + _state.value = (engineName: _state.value.engineName, state: _state.value.state, eval: eval); } }); @@ -178,8 +165,7 @@ class EvaluationService { @Riverpod(keepAlive: true) EvaluationService evaluationService(Ref ref) { - final maxMemory = - ref.read(preloadedDataProvider).requireValue.engineMaxMemoryInMb; + final maxMemory = ref.read(preloadedDataProvider).requireValue.engineMaxMemoryInMb; final service = EvaluationService(ref, maxMemory: maxMemory); ref.onDispose(() { @@ -188,11 +174,7 @@ EvaluationService evaluationService(Ref ref) { return service; } -typedef EngineEvaluationState = ({ - String engineName, - EngineState state, - ClientEval? eval -}); +typedef EngineEvaluationState = ({String engineName, EngineState state, ClientEval? eval}); /// A provider that holds the state of the engine and the current evaluation. @riverpod @@ -220,10 +202,8 @@ class EngineEvaluation extends _$EngineEvaluation { @freezed class EvaluationContext with _$EvaluationContext { - const factory EvaluationContext({ - required Variant variant, - required Position initialPosition, - }) = _EvaluationContext; + const factory EvaluationContext({required Variant variant, required Position initialPosition}) = + _EvaluationContext; } @freezed diff --git a/lib/src/model/engine/uci_protocol.dart b/lib/src/model/engine/uci_protocol.dart index f34a0f270e..5692f8f40f 100644 --- a/lib/src/model/engine/uci_protocol.dart +++ b/lib/src/model/engine/uci_protocol.dart @@ -13,12 +13,7 @@ const minDepth = 6; const maxPlies = 245; class UCIProtocol { - UCIProtocol() - : _options = { - 'Threads': '1', - 'Hash': '16', - 'MultiPV': '1', - }; + UCIProtocol() : _options = {'Threads': '1', 'Hash': '16', 'MultiPV': '1'}; final _log = Logger('UCIProtocol'); final Map _options; @@ -95,9 +90,7 @@ class UCIProtocol { _work = null; _swapWork(); return; - } else if (_work != null && - _stopRequested != true && - parts.first == 'info') { + } else if (_work != null && _stopRequested != true && parts.first == 'info') { int depth = 0; int nodes = 0; int multiPv = 1; @@ -120,8 +113,7 @@ class UCIProtocol { isMate = parts[++i] == 'mate'; povEv = int.parse(parts[++i]); if (i + 1 < parts.length && - (parts[i + 1] == 'lowerbound' || - parts[i + 1] == 'upperbound')) { + (parts[i + 1] == 'lowerbound' || parts[i + 1] == 'upperbound')) { evalType = parts[++i]; } case 'pv': @@ -142,11 +134,7 @@ class UCIProtocol { // However non-primary pvs may only have an upperbound. if (evalType != null && multiPv == 1) return; - final pvData = PvData( - moves: IList(moves), - cp: isMate ? null : ev, - mate: isMate ? ev : null, - ); + final pvData = PvData(moves: IList(moves), cp: isMate ? null : ev, mate: isMate ? ev : null); if (multiPv == 1) { _currentEval = ClientEval( @@ -206,9 +194,7 @@ class UCIProtocol { _work!.initialPosition.fen, 'moves', ..._work!.steps.map( - (s) => _work!.variant == Variant.chess960 - ? s.sanMove.move.uci - : s.castleSafeUCI, + (s) => _work!.variant == Variant.chess960 ? s.sanMove.move.uci : s.castleSafeUCI, ), ].join(' '), ); diff --git a/lib/src/model/engine/work.dart b/lib/src/model/engine/work.dart index 7bb491ab09..8cab1baa06 100644 --- a/lib/src/model/engine/work.dart +++ b/lib/src/model/engine/work.dart @@ -40,18 +40,11 @@ class Work with _$Work { class Step with _$Step { const Step._(); - const factory Step({ - required Position position, - required SanMove sanMove, - ClientEval? eval, - }) = _Step; + const factory Step({required Position position, required SanMove sanMove, ClientEval? eval}) = + _Step; factory Step.fromNode(Branch node) { - return Step( - position: node.position, - sanMove: node.sanMove, - eval: node.eval, - ); + return Step(position: node.position, sanMove: node.sanMove, eval: node.eval); } /// Stockfish in chess960 mode always needs a "king takes rook" UCI notation. @@ -69,9 +62,4 @@ class Step with _$Step { } } -const _castleMoves = { - 'e1c1': 'e1a1', - 'e1g1': 'e1h1', - 'e8c8': 'e8a8', - 'e8g8': 'e8h8', -}; +const _castleMoves = {'e1c1': 'e1a1', 'e1g1': 'e1h1', 'e8c8': 'e8a8', 'e8g8': 'e8h8'}; diff --git a/lib/src/model/game/archived_game.dart b/lib/src/model/game/archived_game.dart index 2be53c5065..d62aca0bae 100644 --- a/lib/src/model/game/archived_game.dart +++ b/lib/src/model/game/archived_game.dart @@ -19,10 +19,7 @@ import 'player.dart'; part 'archived_game.freezed.dart'; part 'archived_game.g.dart'; -typedef ClockData = ({ - Duration initial, - Duration increment, -}); +typedef ClockData = ({Duration initial, Duration increment}); /// A lichess game exported from the API. /// @@ -32,9 +29,7 @@ typedef ClockData = ({ /// See also [PlayableGame] for a game owned by the current user and that can be /// played unless finished. @Freezed(fromJson: true, toJson: true) -class ArchivedGame - with _$ArchivedGame, BaseGame, IndexableSteps - implements BaseGame { +class ArchivedGame with _$ArchivedGame, BaseGame, IndexableSteps implements BaseGame { const ArchivedGame._(); @Assert('steps.isNotEmpty') @@ -43,14 +38,10 @@ class ArchivedGame required GameMeta meta, // TODO refactor to not include this field required LightArchivedGame data, - @JsonKey(fromJson: stepsFromJson, toJson: stepsToJson) - required IList steps, + @JsonKey(fromJson: stepsFromJson, toJson: stepsToJson) required IList steps, String? initialFen, required GameStatus status, - @JsonKey( - defaultValue: GameSource.unknown, - unknownEnumValue: GameSource.unknown, - ) + @JsonKey(defaultValue: GameSource.unknown, unknownEnumValue: GameSource.unknown) required GameSource source, Side? winner, @@ -72,20 +63,21 @@ class ArchivedGame } /// Create an archived game from a local storage JSON. - factory ArchivedGame.fromJson(Map json) => - _$ArchivedGameFromJson(json); + factory ArchivedGame.fromJson(Map json) => _$ArchivedGameFromJson(json); /// Player point of view. Null if spectating. - Player? get me => youAre == null - ? null - : youAre == Side.white + Player? get me => + youAre == null + ? null + : youAre == Side.white ? white : black; /// Opponent point of view. Null if spectating. - Player? get opponent => youAre == null - ? null - : youAre == Side.white + Player? get opponent => + youAre == null + ? null + : youAre == Side.white ? black : white; } @@ -134,10 +126,7 @@ class LightArchivedGame with _$LightArchivedGame { _$LightArchivedGameFromJson(json); String get clockDisplay { - return TimeIncrement( - clock?.initial.inSeconds ?? 0, - clock?.increment.inSeconds ?? 0, - ).display; + return TimeIncrement(clock?.initial.inSeconds ?? 0, clock?.increment.inSeconds ?? 0).display; } } @@ -150,10 +139,7 @@ IList? gameEvalsFromPick(RequiredPick pick) { bestMove: p0('best').asStringOrNull(), variation: p0('variation').asStringOrNull(), judgment: p0('judgment').letOrNull( - (j) => ( - name: j('name').asStringOrThrow(), - comment: j('comment').asStringOrThrow(), - ), + (j) => (name: j('name').asStringOrThrow(), comment: j('comment').asStringOrThrow()), ), ), ) @@ -162,9 +148,9 @@ IList? gameEvalsFromPick(RequiredPick pick) { ArchivedGame _archivedGameFromPick(RequiredPick pick) { final data = _lightArchivedGameFromPick(pick); - final clocks = pick('clocks').asListOrNull( - (p0) => Duration(milliseconds: p0.asIntOrThrow() * 10), - ); + final clocks = pick( + 'clocks', + ).asListOrNull((p0) => Duration(milliseconds: p0.asIntOrThrow() * 10)); final division = pick('division').letOrNull(_divisionFromPick); final initialFen = pick('initialFen').asStringOrNull(); @@ -177,21 +163,21 @@ ArchivedGame _archivedGameFromPick(RequiredPick pick) { speed: data.speed, perf: data.perf, rated: data.rated, - clock: data.clock != null - ? ( - initial: data.clock!.initial, - increment: data.clock!.increment, - emergency: null, - moreTime: null - ) - : null, + clock: + data.clock != null + ? ( + initial: data.clock!.initial, + increment: data.clock!.increment, + emergency: null, + moreTime: null, + ) + : null, opening: data.opening, division: division, ), - source: pick('source').letOrThrow( - (pick) => - GameSource.nameMap[pick.asStringOrThrow()] ?? GameSource.unknown, - ), + source: pick( + 'source', + ).letOrThrow((pick) => GameSource.nameMap[pick.asStringOrThrow()] ?? GameSource.unknown), data: data, status: data.status, winner: data.winner, @@ -202,10 +188,10 @@ ArchivedGame _archivedGameFromPick(RequiredPick pick) { steps: pick('moves').letOrThrow((it) { final moves = it.asStringOrThrow().split(' '); // assume lichess always send initialFen with fromPosition and chess960 - Position position = (data.variant == Variant.fromPosition || - data.variant == Variant.chess960) - ? Chess.fromSetup(Setup.parseFen(initialFen!)) - : data.variant.initialPosition; + Position position = + (data.variant == Variant.fromPosition || data.variant == Variant.chess960) + ? Chess.fromSetup(Setup.parseFen(initialFen!)) + : data.variant.initialPosition; int index = 0; final List steps = [GameStep(position: position)]; Duration? clock = data.clock?.initial; @@ -237,10 +223,9 @@ LightArchivedGame _lightArchivedGameFromPick(RequiredPick pick) { return LightArchivedGame( id: pick('id').asGameIdOrThrow(), fullId: pick('fullId').asGameFullIdOrNull(), - source: pick('source').letOrNull( - (pick) => - GameSource.nameMap[pick.asStringOrThrow()] ?? GameSource.unknown, - ), + source: pick( + 'source', + ).letOrNull((pick) => GameSource.nameMap[pick.asStringOrThrow()] ?? GameSource.unknown), rated: pick('rated').asBoolOrThrow(), speed: pick('speed').asSpeedOrThrow(), perf: pick('perf').asPerfOrThrow(), @@ -259,10 +244,7 @@ LightArchivedGame _lightArchivedGameFromPick(RequiredPick pick) { } LightOpening _openingFromPick(RequiredPick pick) { - return LightOpening( - eco: pick('eco').asStringOrThrow(), - name: pick('name').asStringOrThrow(), - ); + return LightOpening(eco: pick('eco').asStringOrThrow(), name: pick('name').asStringOrThrow()); } ClockData _clockDataFromPick(RequiredPick pick) { @@ -277,8 +259,7 @@ Player _playerFromUserGamePick(RequiredPick pick) { return Player( user: pick('user').asLightUserOrNull(), name: _removeRatingFromName(originalName), - rating: - pick('rating').asIntOrNull() ?? _extractRatingFromName(originalName), + rating: pick('rating').asIntOrNull() ?? _extractRatingFromName(originalName), ratingDiff: pick('ratingDiff').asIntOrNull(), aiLevel: pick('aiLevel').asIntOrNull(), analysis: pick('analysis').letOrNull(_playerAnalysisFromPick), diff --git a/lib/src/model/game/chat_controller.dart b/lib/src/model/game/chat_controller.dart index 8c0f294945..108a73fd36 100644 --- a/lib/src/model/game/chat_controller.dart +++ b/lib/src/model/game/chat_controller.dart @@ -25,8 +25,7 @@ class ChatController extends _$ChatController { @override Future build(GameFullId id) async { - _socketClient = - ref.read(socketPoolProvider).open(GameController.gameSocketUri(id)); + _socketClient = ref.read(socketPoolProvider).open(GameController.gameSocketUri(id)); _subscription?.cancel(); _subscription = _socketClient.stream.listen(_handleSocketEvent); @@ -38,9 +37,7 @@ class ChatController extends _$ChatController { final messages = await _socketClient.stream .firstWhere((event) => event.topic == 'full') .then( - (event) => pick(event.data, 'chat', 'lines') - .asListOrNull(_messageFromPick) - ?.toIList(), + (event) => pick(event.data, 'chat', 'lines').asListOrNull(_messageFromPick)?.toIList(), ); final readMessagesCount = await _getReadMessagesCount(); @@ -53,10 +50,7 @@ class ChatController extends _$ChatController { /// Sends a message to the chat. void sendMessage(String message) { - _socketClient.send( - 'talk', - message, - ); + _socketClient.send('talk', message); } /// Resets the unread messages count to 0 and saves the number of read messages. @@ -64,9 +58,7 @@ class ChatController extends _$ChatController { if (state.hasValue) { await _setReadMessagesCount(state.requireValue.messages.length); } - state = state.whenData( - (s) => s.copyWith(unreadMessages: 0), - ); + state = state.whenData((s) => s.copyWith(unreadMessages: 0)); } Future _getReadMessagesCount() async { @@ -82,34 +74,24 @@ class ChatController extends _$ChatController { Future _setReadMessagesCount(int count) async { final db = await ref.read(databaseProvider.future); - await db.insert( - _tableName, - { - 'id': _storeKey(id), - 'lastModified': DateTime.now().toIso8601String(), - 'nbRead': count, - }, - conflictAlgorithm: ConflictAlgorithm.replace, - ); + await db.insert(_tableName, { + 'id': _storeKey(id), + 'lastModified': DateTime.now().toIso8601String(), + 'nbRead': count, + }, conflictAlgorithm: ConflictAlgorithm.replace); } Future _setMessages(IList messages) async { final readMessagesCount = await _getReadMessagesCount(); state = state.whenData( - (s) => s.copyWith( - messages: messages, - unreadMessages: messages.length - readMessagesCount, - ), + (s) => s.copyWith(messages: messages, unreadMessages: messages.length - readMessagesCount), ); } void _addMessage(Message message) { state = state.whenData( - (s) => s.copyWith( - messages: s.messages.add(message), - unreadMessages: s.unreadMessages + 1, - ), + (s) => s.copyWith(messages: s.messages.add(message), unreadMessages: s.unreadMessages + 1), ); } @@ -117,9 +99,7 @@ class ChatController extends _$ChatController { if (!state.hasValue) return; if (event.topic == 'full') { - final messages = pick(event.data, 'chat', 'lines') - .asListOrNull(_messageFromPick) - ?.toIList(); + final messages = pick(event.data, 'chat', 'lines').asListOrNull(_messageFromPick)?.toIList(); if (messages != null) { _setMessages(messages); } @@ -135,18 +115,11 @@ class ChatController extends _$ChatController { class ChatState with _$ChatState { const ChatState._(); - const factory ChatState({ - required IList messages, - required int unreadMessages, - }) = _ChatState; + const factory ChatState({required IList messages, required int unreadMessages}) = + _ChatState; } -typedef Message = ({ - String? username, - String message, - bool troll, - bool deleted, -}); +typedef Message = ({String? username, String message, bool troll, bool deleted}); Message _messageFromPick(RequiredPick pick) { return ( @@ -158,8 +131,7 @@ Message _messageFromPick(RequiredPick pick) { } bool isSpam(Message message) { - return spamRegex.hasMatch(message.message) || - followMeRegex.hasMatch(message.message); + return spamRegex.hasMatch(message.message) || followMeRegex.hasMatch(message.message); } final RegExp spamRegex = RegExp( diff --git a/lib/src/model/game/game.dart b/lib/src/model/game/game.dart index 5e0a3750fe..08866bf79d 100644 --- a/lib/src/model/game/game.dart +++ b/lib/src/model/game/game.dart @@ -78,16 +78,11 @@ abstract mixin class BaseGame { for (var i = 1; i < steps.length; i++) { final step = steps[i]; final eval = evals?.elementAtOrNull(i - 1); - final pgnEval = eval?.cp != null - ? PgnEvaluation.pawns( - pawns: cpToPawns(eval!.cp!), - depth: eval.depth, - ) - : eval?.mate != null - ? PgnEvaluation.mate( - mate: eval!.mate, - depth: eval.depth, - ) + final pgnEval = + eval?.cp != null + ? PgnEvaluation.pawns(pawns: cpToPawns(eval!.cp!), depth: eval.depth) + : eval?.mate != null + ? PgnEvaluation.mate(mate: eval!.mate, depth: eval.depth) : null; final clock = clocks?.elementAtOrNull(i - 1); Duration? emt; @@ -106,17 +101,11 @@ abstract mixin class BaseGame { } } - final comment = eval != null || clock != null - ? PgnComment( - text: eval?.judgment?.comment, - clock: clock, - emt: emt, - eval: pgnEval, - ) - : null; - final nag = eval?.judgment != null - ? _judgmentNameToNag(eval!.judgment!.name) - : null; + final comment = + eval != null || clock != null + ? PgnComment(text: eval?.judgment?.comment, clock: clock, emt: emt, eval: pgnEval) + : null; + final nag = eval?.judgment != null ? _judgmentNameToNag(eval!.judgment!.name) : null; final nextNode = Branch( sanMove: step.sanMove!, position: step.position, @@ -134,10 +123,7 @@ abstract mixin class BaseGame { for (final san in moves) { final move = position.parseSan(san); position = position.playUnchecked(move!); - final child = Branch( - sanMove: SanMove(san, move), - position: position, - ); + final child = Branch(sanMove: SanMove(san, move), position: position); variationNode.addChild(child); variationNode = child; } @@ -149,11 +135,11 @@ abstract mixin class BaseGame { } int? _judgmentNameToNag(String name) => switch (name) { - 'Inaccuracy' => 6, - 'Mistake' => 2, - 'Blunder' => 4, - String() => null, - }; + 'Inaccuracy' => 6, + 'Mistake' => 2, + 'Blunder' => 4, + String() => null, + }; String makePgn() { final node = makeTree(); @@ -162,35 +148,31 @@ abstract mixin class BaseGame { 'Event': '${meta.rated ? 'Rated' : ''} ${meta.perf.title} game', 'Site': lichessUri('/$id').toString(), 'Date': _dateFormat.format(meta.createdAt), - 'White': white.user?.name ?? + 'White': + white.user?.name ?? white.name ?? - (white.aiLevel != null - ? 'Stockfish level ${white.aiLevel}' - : 'Anonymous'), - 'Black': black.user?.name ?? + (white.aiLevel != null ? 'Stockfish level ${white.aiLevel}' : 'Anonymous'), + 'Black': + black.user?.name ?? black.name ?? - (black.aiLevel != null - ? 'Stockfish level ${black.aiLevel}' - : 'Anonymous'), - 'Result': status.value >= GameStatus.mate.value - ? winner == null - ? '½-½' - : winner == Side.white + (black.aiLevel != null ? 'Stockfish level ${black.aiLevel}' : 'Anonymous'), + 'Result': + status.value >= GameStatus.mate.value + ? winner == null + ? '½-½' + : winner == Side.white ? '1-0' : '0-1' - : '*', + : '*', if (white.rating != null) 'WhiteElo': white.rating!.toString(), if (black.rating != null) 'BlackElo': black.rating!.toString(), if (white.ratingDiff != null) - 'WhiteRatingDiff': - '${white.ratingDiff! > 0 ? '+' : ''}${white.ratingDiff!}', + 'WhiteRatingDiff': '${white.ratingDiff! > 0 ? '+' : ''}${white.ratingDiff!}', if (black.ratingDiff != null) - 'BlackRatingDiff': - '${black.ratingDiff! > 0 ? '+' : ''}${black.ratingDiff!}', + 'BlackRatingDiff': '${black.ratingDiff! > 0 ? '+' : ''}${black.ratingDiff!}', 'Variant': meta.variant.label, if (meta.clock != null) - 'TimeControl': - '${meta.clock!.initial.inSeconds}+${meta.clock!.increment.inSeconds}', + 'TimeControl': '${meta.clock!.initial.inSeconds}+${meta.clock!.increment.inSeconds}', if (initialFen != null) 'FEN': initialFen!, if (meta.opening != null) 'ECO': meta.opening!.eco, if (meta.opening != null) 'Opening': meta.opening!.name, @@ -202,13 +184,9 @@ abstract mixin class BaseGame { /// A mixin that provides methods to access game data at a specific step. mixin IndexableSteps on BaseGame { - String get sanMoves => steps - .where((e) => e.sanMove != null) - .map((e) => e.sanMove!.san) - .join(' '); + String get sanMoves => steps.where((e) => e.sanMove != null).map((e) => e.sanMove!.san).join(' '); - MaterialDiffSide? materialDiffAt(int cursor, Side side) => - steps[cursor].diff?.bySide(side); + MaterialDiffSide? materialDiffAt(int cursor, Side side) => steps[cursor].diff?.bySide(side); GameStep stepAt(int cursor) => steps[cursor]; @@ -220,11 +198,9 @@ mixin IndexableSteps on BaseGame { Position positionAt(int cursor) => steps[cursor].position; - Duration? archivedWhiteClockAt(int cursor) => - steps[cursor].archivedWhiteClock; + Duration? archivedWhiteClockAt(int cursor) => steps[cursor].archivedWhiteClock; - Duration? archivedBlackClockAt(int cursor) => - steps[cursor].archivedBlackClock; + Duration? archivedBlackClockAt(int cursor) => steps[cursor].archivedBlackClock; Move? get lastMove { return steps.last.sanMove?.move; @@ -238,8 +214,7 @@ mixin IndexableSteps on BaseGame { int get lastPly => steps.last.position.ply; - MaterialDiffSide? lastMaterialDiffAt(Side side) => - steps.last.diff?.bySide(side); + MaterialDiffSide? lastMaterialDiffAt(Side side) => steps.last.diff?.bySide(side); } enum GameSource { @@ -303,7 +278,8 @@ class GameMeta with _$GameMeta { /// Time added to the clock by the "add more time" button. Duration? moreTime, - })? clock, + })? + clock, int? daysPerTurn, int? startedAtTurn, ISet? rules, @@ -315,16 +291,13 @@ class GameMeta with _$GameMeta { Division? division, }) = _GameMeta; - factory GameMeta.fromJson(Map json) => - _$GameMetaFromJson(json); + factory GameMeta.fromJson(Map json) => _$GameMetaFromJson(json); } @Freezed(fromJson: true, toJson: true) class CorrespondenceClockData with _$CorrespondenceClockData { - const factory CorrespondenceClockData({ - required Duration white, - required Duration black, - }) = _CorrespondenceClockData; + const factory CorrespondenceClockData({required Duration white, required Duration black}) = + _CorrespondenceClockData; factory CorrespondenceClockData.fromJson(Map json) => _$CorrespondenceClockDataFromJson(json); diff --git a/lib/src/model/game/game_controller.dart b/lib/src/model/game/game_controller.dart index 88e637a761..dc29a0444c 100644 --- a/lib/src/model/game/game_controller.dart +++ b/lib/src/model/game/game_controller.dart @@ -63,8 +63,7 @@ class GameController extends _$GameController { /// Last socket version received int? _socketEventVersion; - static Uri gameSocketUri(GameFullId gameFullId) => - Uri(path: '/play/$gameFullId/v6'); + static Uri gameSocketUri(GameFullId gameFullId) => Uri(path: '/play/$gameFullId/v6'); ChessClock? _clock; late final SocketClient _socketClient; @@ -73,8 +72,7 @@ class GameController extends _$GameController { Future build(GameFullId gameFullId) { final socketPool = ref.watch(socketPoolProvider); - _socketClient = - socketPool.open(gameSocketUri(gameFullId), forceReconnect: true); + _socketClient = socketPool.open(gameSocketUri(gameFullId), forceReconnect: true); _socketEventVersion = null; _socketSubscription?.cancel(); _socketSubscription = _socketClient.stream.listen(_handleSocketEvent); @@ -87,85 +85,76 @@ class GameController extends _$GameController { _clock?.dispose(); }); - return _socketClient.stream.firstWhere((e) => e.topic == 'full').then( - (event) async { - final fullEvent = - GameFullEvent.fromJson(event.data as Map); + return _socketClient.stream.firstWhere((e) => e.topic == 'full').then((event) async { + final fullEvent = GameFullEvent.fromJson(event.data as Map); - PlayableGame game = fullEvent.game; + PlayableGame game = fullEvent.game; - if (fullEvent.game.finished) { - if (fullEvent.game.meta.speed == Speed.correspondence) { - ref.invalidate(ongoingGamesProvider); - ref - .read(correspondenceServiceProvider) - .updateGame(gameFullId, fullEvent.game); - } - - final result = await _getPostGameData(); - game = result.fold( - (data) => _mergePostGameData(game, data, rewriteSteps: true), - (e, s) { - _logger.warning('Could not get post game data: $e', e, s); - return game; - }); - await _storeGame(game); + if (fullEvent.game.finished) { + if (fullEvent.game.meta.speed == Speed.correspondence) { + ref.invalidate(ongoingGamesProvider); + ref.read(correspondenceServiceProvider).updateGame(gameFullId, fullEvent.game); } - _socketEventVersion = fullEvent.socketEventVersion; + final result = await _getPostGameData(); + game = result.fold((data) => _mergePostGameData(game, data, rewriteSteps: true), (e, s) { + _logger.warning('Could not get post game data: $e', e, s); + return game; + }); + await _storeGame(game); + } - // Play "dong" sound when this is a new game and we're playing it (not spectating) - final isMyGame = game.youAre != null; - final noMovePlayed = game.steps.length == 1; - if (isMyGame && noMovePlayed && game.status == GameStatus.started) { - ref.read(soundServiceProvider).play(Sound.dong); - } + _socketEventVersion = fullEvent.socketEventVersion; - if (game.playable) { - _appLifecycleListener = AppLifecycleListener( - onResume: () { - // socket client should never be disposed here, but in case it is - // we can safely skip the resync - if (!_socketClient.isDisposed && _socketClient.isConnected) { - _resyncGameData(); - } - }, - ); + // Play "dong" sound when this is a new game and we're playing it (not spectating) + final isMyGame = game.youAre != null; + final noMovePlayed = game.steps.length == 1; + if (isMyGame && noMovePlayed && game.status == GameStatus.started) { + ref.read(soundServiceProvider).play(Sound.dong); + } - if (game.clock != null) { - _clock = ChessClock( - whiteTime: game.clock!.white, - blackTime: game.clock!.black, - emergencyThreshold: game.meta.clock?.emergency, - onEmergency: onClockEmergency, - onFlag: onFlag, - ); - if (game.clock!.running) { - final pos = game.lastPosition; - if (pos.fullmoves > 1) { - _clock!.startSide(pos.turn); - } + if (game.playable) { + _appLifecycleListener = AppLifecycleListener( + onResume: () { + // socket client should never be disposed here, but in case it is + // we can safely skip the resync + if (!_socketClient.isDisposed && _socketClient.isConnected) { + _resyncGameData(); + } + }, + ); + + if (game.clock != null) { + _clock = ChessClock( + whiteTime: game.clock!.white, + blackTime: game.clock!.black, + emergencyThreshold: game.meta.clock?.emergency, + onEmergency: onClockEmergency, + onFlag: onFlag, + ); + if (game.clock!.running) { + final pos = game.lastPosition; + if (pos.fullmoves > 1) { + _clock!.startSide(pos.turn); } } } + } - return GameState( - gameFullId: gameFullId, - game: game, - stepCursor: game.steps.length - 1, - liveClock: _liveClock, - ); - }, - ); + return GameState( + gameFullId: gameFullId, + game: game, + stepCursor: game.steps.length - 1, + liveClock: _liveClock, + ); + }); } void userMove(NormalMove move, {bool? isDrop, bool? isPremove}) { final curState = state.requireValue; if (isPromotionPawnMove(curState.game.lastPosition, move)) { - state = AsyncValue.data( - curState.copyWith(promotionMove: move), - ); + state = AsyncValue.data(curState.copyWith(promotionMove: move)); return; } @@ -181,9 +170,7 @@ class GameController extends _$GameController { state = AsyncValue.data( curState.copyWith( - game: curState.game.copyWith( - steps: curState.game.steps.add(newStep), - ), + game: curState.game.copyWith(steps: curState.game.steps.add(newStep)), stepCursor: curState.stepCursor + 1, moveToConfirm: shouldConfirmMove ? move : null, promotionMove: null, @@ -199,8 +186,7 @@ class GameController extends _$GameController { isPremove: isPremove ?? false, // same logic as web client // we want to send client lag only at the beginning of the game when the clock is not running yet - withLag: - curState.game.clock != null && curState.activeClockSide == null, + withLag: curState.game.clock != null && curState.activeClockSide == null, ); } } @@ -208,9 +194,7 @@ class GameController extends _$GameController { void onPromotionSelection(Role? role) { final curState = state.requireValue; if (role == null) { - state = AsyncValue.data( - curState.copyWith(promotionMove: null), - ); + state = AsyncValue.data(curState.copyWith(promotionMove: null)); return; } if (curState.promotionMove == null) { @@ -231,9 +215,7 @@ class GameController extends _$GameController { } state = AsyncValue.data( curState.copyWith( - game: curState.game.copyWith( - steps: curState.game.steps.removeLast(), - ), + game: curState.game.copyWith(steps: curState.game.steps.removeLast()), stepCursor: curState.stepCursor - 1, moveToConfirm: null, ), @@ -249,11 +231,7 @@ class GameController extends _$GameController { return; } - state = AsyncValue.data( - curState.copyWith( - moveToConfirm: null, - ), - ); + state = AsyncValue.data(curState.copyWith(moveToConfirm: null)); _sendMoveToSocket( moveToConfirm, isPremove: false, @@ -266,21 +244,12 @@ class GameController extends _$GameController { /// Set or unset a premove. void setPremove(NormalMove? move) { final curState = state.requireValue; - state = AsyncValue.data( - curState.copyWith( - premove: move, - ), - ); + state = AsyncValue.data(curState.copyWith(premove: move)); } void cursorAt(int cursor) { if (state.hasValue) { - state = AsyncValue.data( - state.requireValue.copyWith( - stepCursor: cursor, - premove: null, - ), - ); + state = AsyncValue.data(state.requireValue.copyWith(stepCursor: cursor, premove: null)); final san = state.requireValue.game.stepAt(cursor).sanMove?.san; if (san != null) { _playReplayMoveSound(san); @@ -322,28 +291,21 @@ class GameController extends _$GameController { void toggleMoveConfirmation() { final curState = state.requireValue; state = AsyncValue.data( - curState.copyWith( - moveConfirmSettingOverride: - !(curState.moveConfirmSettingOverride ?? true), - ), + curState.copyWith(moveConfirmSettingOverride: !(curState.moveConfirmSettingOverride ?? true)), ); } void toggleZenMode() { final curState = state.requireValue; state = AsyncValue.data( - curState.copyWith( - zenModeGameSetting: !(curState.zenModeGameSetting ?? false), - ), + curState.copyWith(zenModeGameSetting: !(curState.zenModeGameSetting ?? false)), ); } void toggleAutoQueen() { final curState = state.requireValue; state = AsyncValue.data( - curState.copyWith( - autoQueenSettingOverride: !(curState.autoQueenSettingOverride ?? true), - ), + curState.copyWith(autoQueenSettingOverride: !(curState.autoQueenSettingOverride ?? true)), ); } @@ -425,12 +387,8 @@ class GameController extends _$GameController { } /// Gets the live game clock if available. - LiveGameClock? get _liveClock => _clock != null - ? ( - white: _clock!.whiteTime, - black: _clock!.blackTime, - ) - : null; + LiveGameClock? get _liveClock => + _clock != null ? (white: _clock!.whiteTime, black: _clock!.blackTime) : null; /// Update the internal clock on clock server event void _updateClock({ @@ -447,23 +405,19 @@ class GameController extends _$GameController { } } - void _sendMoveToSocket( - Move move, { - required bool isPremove, - required bool withLag, - }) { + void _sendMoveToSocket(Move move, {required bool isPremove, required bool withLag}) { final thinkTime = _clock?.stop(); - final moveTime = _clock != null - ? isPremove == true - ? Duration.zero - : thinkTime - : null; + final moveTime = + _clock != null + ? isPremove == true + ? Duration.zero + : thinkTime + : null; _socketClient.send( 'move', { 'u': move.uci, - if (moveTime != null) - 's': (moveTime.inMilliseconds * 0.1).round().toRadixString(36), + if (moveTime != null) 's': (moveTime.inMilliseconds * 0.1).round().toRadixString(36), }, ackable: true, withLag: _clock != null && (moveTime == null || withLag), @@ -474,8 +428,7 @@ class GameController extends _$GameController { /// Move feedback while playing void _playMoveFeedback(SanMove sanMove, {bool skipAnimationDelay = false}) { - final animationDuration = - ref.read(boardPreferencesProvider).pieceAnimationDuration; + final animationDuration = ref.read(boardPreferencesProvider).pieceAnimationDuration; final delay = animationDuration ~/ 2; @@ -527,9 +480,7 @@ class GameController extends _$GameController { return; } if (event.version! > currentEventVersion + 1) { - _logger.warning( - 'Event gap detected from $currentEventVersion to ${event.version}', - ); + _logger.warning('Event gap detected from $currentEventVersion to ${event.version}'); _resyncGameData(); } _socketEventVersion = event.version; @@ -558,10 +509,7 @@ class GameController extends _$GameController { _resyncGameData(); return; } - final reloadEvent = SocketEvent( - topic: data['t'] as String, - data: data['d'], - ); + final reloadEvent = SocketEvent(topic: data['t'] as String, data: data['d']); _handleSocketTopic(reloadEvent); } else { _resyncGameData(); @@ -569,11 +517,9 @@ class GameController extends _$GameController { // Full game data, received after a (re)connection to game socket case 'full': - final fullEvent = - GameFullEvent.fromJson(event.data as Map); + final fullEvent = GameFullEvent.fromJson(event.data as Map); - if (_socketEventVersion != null && - fullEvent.socketEventVersion < _socketEventVersion!) { + if (_socketEventVersion != null && fullEvent.socketEventVersion < _socketEventVersion!) { return; } _socketEventVersion = fullEvent.socketEventVersion; @@ -630,27 +576,24 @@ class GameController extends _$GameController { ); newState = newState.copyWith( - game: newState.game.copyWith( - steps: newState.game.steps.add(newStep), - ), + game: newState.game.copyWith(steps: newState.game.steps.add(newStep)), ); if (!curState.isReplaying) { - newState = newState.copyWith( - stepCursor: newState.stepCursor + 1, - ); + newState = newState.copyWith(stepCursor: newState.stepCursor + 1); _playMoveFeedback(sanMove); } } if (data.clock != null) { - final lag = newState.game.playable && newState.game.isMyTurn - // my own clock doesn't need to be compensated for - ? Duration.zero - // server will send the lag only if it's more than 10ms - // default lag of 10ms is also used by web client - : data.clock?.lag ?? const Duration(milliseconds: 10); + final lag = + newState.game.playable && newState.game.isMyTurn + // my own clock doesn't need to be compensated for + ? Duration.zero + // server will send the lag only if it's more than 10ms + // default lag of 10ms is also used by web client + : data.clock?.lag ?? const Duration(milliseconds: 10); _updateClock( white: data.clock!.white, @@ -676,9 +619,7 @@ class GameController extends _$GameController { if (newState.game.expiration != null) { if (newState.game.steps.length > 2) { - newState = newState.copyWith.game( - expiration: null, - ); + newState = newState.copyWith.game(expiration: null); } else { newState = newState.copyWith.game( expiration: ( @@ -692,9 +633,7 @@ class GameController extends _$GameController { if (curState.game.meta.speed == Speed.correspondence) { ref.invalidate(ongoingGamesProvider); - ref - .read(correspondenceServiceProvider) - .updateGame(gameFullId, newState.game); + ref.read(correspondenceServiceProvider).updateGame(gameFullId, newState.game); } if (!curState.isReplaying && @@ -703,8 +642,7 @@ class GameController extends _$GameController { scheduleMicrotask(() { final postMovePremove = state.valueOrNull?.premove; final postMovePosition = state.valueOrNull?.game.lastPosition; - if (postMovePremove != null && - postMovePosition?.isLegal(postMovePremove) == true) { + if (postMovePremove != null && postMovePosition?.isLegal(postMovePremove) == true) { userMove(postMovePremove, isPremove: true); } }); @@ -714,20 +652,15 @@ class GameController extends _$GameController { // End game event case 'endData': - final endData = - GameEndEvent.fromJson(event.data as Map); + final endData = GameEndEvent.fromJson(event.data as Map); final curState = state.requireValue; GameState newState = curState.copyWith( game: curState.game.copyWith( status: endData.status, winner: endData.winner, boosted: endData.boosted, - white: curState.game.white.copyWith( - ratingDiff: endData.ratingDiff?.white, - ), - black: curState.game.black.copyWith( - ratingDiff: endData.ratingDiff?.black, - ), + white: curState.game.white.copyWith(ratingDiff: endData.ratingDiff?.white), + black: curState.game.black.copyWith(ratingDiff: endData.ratingDiff?.black), ), premove: null, ); @@ -752,45 +685,42 @@ class GameController extends _$GameController { if (curState.game.meta.speed == Speed.correspondence) { ref.invalidate(ongoingGamesProvider); - ref - .read(correspondenceServiceProvider) - .updateGame(gameFullId, newState.game); + ref.read(correspondenceServiceProvider).updateGame(gameFullId, newState.game); } state = AsyncValue.data(newState); if (!newState.game.aborted) { _getPostGameData().then((result) { - result.fold((data) { - final game = _mergePostGameData(state.requireValue.game, data); - state = AsyncValue.data( - state.requireValue.copyWith(game: game), - ); - _storeGame(game); - }, (e, s) { - _logger.warning('Could not get post game data', e, s); - }); + result.fold( + (data) { + final game = _mergePostGameData(state.requireValue.game, data); + state = AsyncValue.data(state.requireValue.copyWith(game: game)); + _storeGame(game); + }, + (e, s) { + _logger.warning('Could not get post game data', e, s); + }, + ); }); } case 'clockInc': final data = event.data as Map; final side = pick(data['color']).asSideOrNull(); - final newClock = pick(data['total']) - .letOrNull((it) => Duration(milliseconds: it.asIntOrThrow() * 10)); + final newClock = pick( + data['total'], + ).letOrNull((it) => Duration(milliseconds: it.asIntOrThrow() * 10)); final curState = state.requireValue; if (side != null && newClock != null) { _clock?.setTime(side, newClock); // sync game clock object even if it's not used to display the clock - final newState = side == Side.white - ? curState.copyWith.game.clock!( - white: newClock, - ) - : curState.copyWith.game.clock!( - black: newClock, - ); + final newState = + side == Side.white + ? curState.copyWith.game.clock!(white: newClock) + : curState.copyWith.game.clock!(black: newClock); state = AsyncValue.data(newState); } @@ -803,25 +733,17 @@ class GameController extends _$GameController { final opponent = curState.game.youAre?.opposite; GameState newState = curState; if (whiteOnGame != null) { - newState = newState.copyWith.game( - white: newState.game.white.setOnGame(whiteOnGame), - ); + newState = newState.copyWith.game(white: newState.game.white.setOnGame(whiteOnGame)); if (opponent == Side.white && whiteOnGame == true) { _opponentLeftCountdownTimer?.cancel(); - newState = newState.copyWith( - opponentLeftCountdown: null, - ); + newState = newState.copyWith(opponentLeftCountdown: null); } } if (blackOnGame != null) { - newState = newState.copyWith.game( - black: newState.game.black.setOnGame(blackOnGame), - ); + newState = newState.copyWith.game(black: newState.game.black.setOnGame(blackOnGame)); if (opponent == Side.black && blackOnGame == true) { _opponentLeftCountdownTimer?.cancel(); - newState = newState.copyWith( - opponentLeftCountdown: null, - ); + newState = newState.copyWith(opponentLeftCountdown: null); } } state = AsyncValue.data(newState); @@ -834,12 +756,8 @@ class GameController extends _$GameController { GameState newState = state.requireValue; final youAre = newState.game.youAre; newState = newState.copyWith.game( - white: youAre == Side.white - ? newState.game.white - : newState.game.white.setGone(isGone), - black: youAre == Side.black - ? newState.game.black - : newState.game.black.setGone(isGone), + white: youAre == Side.white ? newState.game.white : newState.game.white.setGone(isGone), + black: youAre == Side.black ? newState.game.black : newState.game.black.setGone(isGone), ); state = AsyncValue.data(newState); @@ -847,41 +765,25 @@ class GameController extends _$GameController { // before claiming victory is possible case 'goneIn': final timeLeft = Duration(seconds: event.data as int); - state = AsyncValue.data( - state.requireValue.copyWith( - opponentLeftCountdown: timeLeft, - ), - ); + state = AsyncValue.data(state.requireValue.copyWith(opponentLeftCountdown: timeLeft)); _opponentLeftCountdownTimer?.cancel(); - _opponentLeftCountdownTimer = Timer.periodic( - const Duration(seconds: 1), - (_) { - final curState = state.requireValue; - final opponentLeftCountdown = curState.opponentLeftCountdown; - if (opponentLeftCountdown == null) { - _opponentLeftCountdownTimer?.cancel(); - } else if (!curState.canShowClaimWinCountdown) { + _opponentLeftCountdownTimer = Timer.periodic(const Duration(seconds: 1), (_) { + final curState = state.requireValue; + final opponentLeftCountdown = curState.opponentLeftCountdown; + if (opponentLeftCountdown == null) { + _opponentLeftCountdownTimer?.cancel(); + } else if (!curState.canShowClaimWinCountdown) { + _opponentLeftCountdownTimer?.cancel(); + state = AsyncValue.data(curState.copyWith(opponentLeftCountdown: null)); + } else { + final newTime = opponentLeftCountdown - const Duration(seconds: 1); + if (newTime <= Duration.zero) { _opponentLeftCountdownTimer?.cancel(); - state = AsyncValue.data( - curState.copyWith( - opponentLeftCountdown: null, - ), - ); - } else { - final newTime = - opponentLeftCountdown - const Duration(seconds: 1); - if (newTime <= Duration.zero) { - _opponentLeftCountdownTimer?.cancel(); - state = AsyncValue.data( - curState.copyWith(opponentLeftCountdown: null), - ); - } - state = AsyncValue.data( - curState.copyWith(opponentLeftCountdown: newTime), - ); + state = AsyncValue.data(curState.copyWith(opponentLeftCountdown: null)); } - }, - ); + state = AsyncValue.data(curState.copyWith(opponentLeftCountdown: newTime)); + } + }); // Event sent when a player adds or cancels a draw offer case 'drawOffer': @@ -889,9 +791,8 @@ class GameController extends _$GameController { final curState = state.requireValue; state = AsyncValue.data( curState.copyWith( - lastDrawOfferAtPly: side != null && side == curState.game.youAre - ? curState.game.lastPly - : null, + lastDrawOfferAtPly: + side != null && side == curState.game.youAre ? curState.game.lastPly : null, game: curState.game.copyWith( white: curState.game.white.copyWith( offeringDraw: side == null ? null : side == Side.white, @@ -912,12 +813,8 @@ class GameController extends _$GameController { state = AsyncValue.data( curState.copyWith( game: curState.game.copyWith( - white: curState.game.white.copyWith( - proposingTakeback: white ?? false, - ), - black: curState.game.black.copyWith( - proposingTakeback: black ?? false, - ), + white: curState.game.white.copyWith(proposingTakeback: white ?? false), + black: curState.game.black.copyWith(proposingTakeback: black ?? false), ), ), ); @@ -943,34 +840,21 @@ class GameController extends _$GameController { // sending another rematch offer, which should not happen case 'rematchTaken': final nextId = pick(event.data).asGameIdOrThrow(); - state = AsyncValue.data( - state.requireValue.copyWith.game( - rematch: nextId, - ), - ); + state = AsyncValue.data(state.requireValue.copyWith.game(rematch: nextId)); // Event sent after a rematch is taken, to redirect to the new game case 'redirect': final data = event.data as Map; final fullId = pick(data['id']).asGameFullIdOrThrow(); - state = AsyncValue.data( - state.requireValue.copyWith( - redirectGameId: fullId, - ), - ); + state = AsyncValue.data(state.requireValue.copyWith(redirectGameId: fullId)); case 'analysisProgress': - final data = - ServerEvalEvent.fromJson(event.data as Map); + final data = ServerEvalEvent.fromJson(event.data as Map); final curState = state.requireValue; state = AsyncValue.data( curState.copyWith.game( - white: curState.game.white.copyWith( - analysis: data.analysis?.white, - ), - black: curState.game.black.copyWith( - analysis: data.analysis?.black, - ), + white: curState.game.white.copyWith(analysis: data.analysis?.white), + black: curState.game.black.copyWith(analysis: data.analysis?.black), evals: data.evals, ), ); @@ -988,15 +872,14 @@ class GameController extends _$GameController { FutureResult _getPostGameData() { return Result.capture( - ref.withClient( - (client) => GameRepository(client).getGame(gameFullId.gameId), - ), + ref.withClient((client) => GameRepository(client).getGame(gameFullId.gameId)), ); } PlayableGame _mergePostGameData( PlayableGame game, ArchivedGame data, { + /// Whether to rewrite the steps with the clock data from the archived game /// /// This should not be done when the game has just finished, because we @@ -1006,44 +889,35 @@ class GameController extends _$GameController { IList newSteps = game.steps; if (rewriteSteps && game.meta.clock != null && data.clocks != null) { final initialTime = game.meta.clock!.initial; - newSteps = game.steps.mapIndexed((index, element) { - if (index == 0) { - return element.copyWith( - archivedWhiteClock: initialTime, - archivedBlackClock: initialTime, - ); - } - final prevClock = index > 1 ? data.clocks![index - 2] : initialTime; - final stepClock = data.clocks![index - 1]; - return element.copyWith( - archivedWhiteClock: index.isOdd ? stepClock : prevClock, - archivedBlackClock: index.isEven ? stepClock : prevClock, - ); - }).toIList(); + newSteps = + game.steps.mapIndexed((index, element) { + if (index == 0) { + return element.copyWith( + archivedWhiteClock: initialTime, + archivedBlackClock: initialTime, + ); + } + final prevClock = index > 1 ? data.clocks![index - 2] : initialTime; + final stepClock = data.clocks![index - 1]; + return element.copyWith( + archivedWhiteClock: index.isOdd ? stepClock : prevClock, + archivedBlackClock: index.isEven ? stepClock : prevClock, + ); + }).toIList(); } return game.copyWith( steps: newSteps, clocks: data.clocks, - meta: game.meta.copyWith( - opening: data.meta.opening, - division: data.meta.division, - ), - white: game.white.copyWith( - analysis: data.white.analysis, - ), - black: game.black.copyWith( - analysis: data.black.analysis, - ), + meta: game.meta.copyWith(opening: data.meta.opening, division: data.meta.division), + white: game.white.copyWith(analysis: data.white.analysis), + black: game.black.copyWith(analysis: data.black.analysis), evals: data.evals, ); } } -typedef LiveGameClock = ({ - ValueListenable white, - ValueListenable black, -}); +typedef LiveGameClock = ({ValueListenable white, ValueListenable black}); @freezed class GameState with _$GameState { @@ -1082,34 +956,25 @@ class GameState with _$GameState { /// The [Position] and its legal moves at the current cursor. (Position, IMap>) get currentPosition { final position = game.positionAt(stepCursor); - final legalMoves = makeLegalMoves( - position, - isChess960: game.meta.variant == Variant.chess960, - ); + final legalMoves = makeLegalMoves(position, isChess960: game.meta.variant == Variant.chess960); return (position, legalMoves); } /// Whether the zen mode is active - bool get isZenModeActive => - game.playable ? isZenModeEnabled : game.prefs?.zenMode == Zen.yes; + bool get isZenModeActive => game.playable ? isZenModeEnabled : game.prefs?.zenMode == Zen.yes; /// Whether zen mode is enabled by account preference or local game setting bool get isZenModeEnabled => - zenModeGameSetting ?? - game.prefs?.zenMode == Zen.yes || game.prefs?.zenMode == Zen.gameAuto; + zenModeGameSetting ?? game.prefs?.zenMode == Zen.yes || game.prefs?.zenMode == Zen.gameAuto; bool get canPremove => - game.meta.speed != Speed.correspondence && - (game.prefs?.enablePremove ?? true); - bool get canAutoQueen => - autoQueenSettingOverride ?? (game.prefs?.autoQueen == AutoQueen.always); + game.meta.speed != Speed.correspondence && (game.prefs?.enablePremove ?? true); + bool get canAutoQueen => autoQueenSettingOverride ?? (game.prefs?.autoQueen == AutoQueen.always); bool get canAutoQueenOnPremove => autoQueenSettingOverride ?? - (game.prefs?.autoQueen == AutoQueen.always || - game.prefs?.autoQueen == AutoQueen.premove); + (game.prefs?.autoQueen == AutoQueen.always || game.prefs?.autoQueen == AutoQueen.premove); bool get shouldConfirmResignAndDrawOffer => game.prefs?.confirmResign ?? true; - bool get shouldConfirmMove => - moveConfirmSettingOverride ?? game.prefs?.submitMove ?? false; + bool get shouldConfirmMove => moveConfirmSettingOverride ?? game.prefs?.submitMove ?? false; bool get isReplaying => stepCursor < game.steps.length - 1; bool get canGoForward => stepCursor < game.steps.length - 1; @@ -1120,23 +985,19 @@ class GameState with _$GameState { game.meta.speed != Speed.correspondence && (game.source == GameSource.lobby || game.source == GameSource.pool); - bool get canOfferDraw => - game.drawable && (lastDrawOfferAtPly ?? -99) < game.lastPly - 20; + bool get canOfferDraw => game.drawable && (lastDrawOfferAtPly ?? -99) < game.lastPly - 20; bool get canShowClaimWinCountdown => !game.isMyTurn && game.resignable && - (game.meta.rules == null || - !game.meta.rules!.contains(GameRule.noClaimWin)); + (game.meta.rules == null || !game.meta.rules!.contains(GameRule.noClaimWin)); bool get canOfferRematch => game.rematch == null && game.rematchable && (game.finished || (game.aborted && - (!game.meta.rated || - !{GameSource.lobby, GameSource.pool} - .contains(game.source)))) && + (!game.meta.rated || !{GameSource.lobby, GameSource.pool}.contains(game.source)))) && game.boosted != true; /// Time left to move for the active player if an expiration is set @@ -1144,8 +1005,8 @@ class GameState with _$GameState { if (!game.playable || game.expiration == null) { return null; } - final timeLeft = game.expiration!.movedAt.difference(DateTime.now()) + - game.expiration!.timeToMove; + final timeLeft = + game.expiration!.movedAt.difference(DateTime.now()) + game.expiration!.timeToMove; if (timeLeft.isNegative) { return Duration.zero; @@ -1169,19 +1030,20 @@ class GameState with _$GameState { String get analysisPgn => game.makePgn(); - AnalysisOptions get analysisOptions => game.finished - ? AnalysisOptions( - orientation: game.youAre ?? Side.white, - initialMoveCursor: stepCursor, - gameId: gameFullId.gameId, - ) - : AnalysisOptions( - orientation: game.youAre ?? Side.white, - initialMoveCursor: stepCursor, - standalone: ( - pgn: game.makePgn(), - variant: game.meta.variant, - isComputerAnalysisAllowed: false, - ), - ); + AnalysisOptions get analysisOptions => + game.finished + ? AnalysisOptions( + orientation: game.youAre ?? Side.white, + initialMoveCursor: stepCursor, + gameId: gameFullId.gameId, + ) + : AnalysisOptions( + orientation: game.youAre ?? Side.white, + initialMoveCursor: stepCursor, + standalone: ( + pgn: game.makePgn(), + variant: game.meta.variant, + isComputerAnalysisAllowed: false, + ), + ); } diff --git a/lib/src/model/game/game_filter.dart b/lib/src/model/game/game_filter.dart index 218f5f6ea1..3b218a74a4 100644 --- a/lib/src/model/game/game_filter.dart +++ b/lib/src/model/game/game_filter.dart @@ -16,43 +16,39 @@ class GameFilter extends _$GameFilter { return filter ?? const GameFilterState(); } - void setFilter(GameFilterState filter) => state = state.copyWith( - perfs: filter.perfs, - side: filter.side, - ); + void setFilter(GameFilterState filter) => + state = state.copyWith(perfs: filter.perfs, side: filter.side); } @freezed class GameFilterState with _$GameFilterState { const GameFilterState._(); - const factory GameFilterState({ - @Default(ISet.empty()) ISet perfs, - Side? side, - }) = _GameFilterState; + const factory GameFilterState({@Default(ISet.empty()) ISet perfs, Side? side}) = + _GameFilterState; /// Returns a translated label of the selected filters. String selectionLabel(BuildContext context) { final fields = [side, perfs]; - final labels = fields - .map( - (field) => field is ISet - ? field.map((e) => e.shortTitle).join(', ') - : (field as Side?) != null - ? field == Side.white - ? context.l10n.white - : context.l10n.black - : null, - ) - .where((label) => label != null && label.isNotEmpty) - .toList(); + final labels = + fields + .map( + (field) => + field is ISet + ? field.map((e) => e.shortTitle).join(', ') + : (field as Side?) != null + ? field == Side.white + ? context.l10n.white + : context.l10n.black + : null, + ) + .where((label) => label != null && label.isNotEmpty) + .toList(); return labels.isEmpty ? 'All' : labels.join(', '); } int get count { final fields = [perfs, side]; - return fields - .where((field) => field is Iterable ? field.isNotEmpty : field != null) - .length; + return fields.where((field) => field is Iterable ? field.isNotEmpty : field != null).length; } } diff --git a/lib/src/model/game/game_history.dart b/lib/src/model/game/game_history.dart index 50c4122e6d..9191de56bb 100644 --- a/lib/src/model/game/game_history.dart +++ b/lib/src/model/game/game_history.dart @@ -34,13 +34,11 @@ const _nbPerPage = 20; /// stored locally are fetched instead. @riverpod Future> myRecentGames(Ref ref) async { - final online = await ref - .watch(connectivityChangesProvider.selectAsync((c) => c.isOnline)); + final online = await ref.watch(connectivityChangesProvider.selectAsync((c) => c.isOnline)); final session = ref.watch(authSessionProvider); if (session != null && online) { return ref.withClientCacheFor( - (client) => GameRepository(client) - .getUserGames(session.user.id, max: kNumberOfRecentGames), + (client) => GameRepository(client).getUserGames(session.user.id, max: kNumberOfRecentGames), const Duration(hours: 1), ); } else { @@ -49,21 +47,19 @@ Future> myRecentGames(Ref ref) async { return storage .page(userId: session?.user.id, max: kNumberOfRecentGames) .then( - (value) => value - // we can assume that `youAre` is not null either for logged - // in users or for anonymous users - .map((e) => (game: e.game.data, pov: e.game.youAre ?? Side.white)) - .toIList(), + (value) => + value + // we can assume that `youAre` is not null either for logged + // in users or for anonymous users + .map((e) => (game: e.game.data, pov: e.game.youAre ?? Side.white)) + .toIList(), ); } } /// A provider that fetches the recent games from the server for a given user. @riverpod -Future> userRecentGames( - Ref ref, { - required UserId userId, -}) { +Future> userRecentGames(Ref ref, {required UserId userId}) { return ref.withClientCacheFor( (client) => GameRepository(client).getUserGames(userId), // cache is important because the associated widget is in a [ListView] and @@ -81,20 +77,13 @@ Future> userRecentGames( /// If the user is not logged in, or there is no connectivity, the number of games /// stored locally are fetched instead. @riverpod -Future userNumberOfGames( - Ref ref, - LightUser? user, { - required bool isOnline, -}) async { +Future userNumberOfGames(Ref ref, LightUser? user, {required bool isOnline}) async { final session = ref.watch(authSessionProvider); return user != null - ? ref.watch( - userProvider(id: user.id).selectAsync((u) => u.count?.all ?? 0), - ) + ? ref.watch(userProvider(id: user.id).selectAsync((u) => u.count?.all ?? 0)) : session != null && isOnline - ? ref.watch(accountProvider.selectAsync((u) => u?.count?.all ?? 0)) - : (await ref.watch(gameStorageProvider.future)) - .count(userId: user?.id); + ? ref.watch(accountProvider.selectAsync((u) => u?.count?.all ?? 0)) + : (await ref.watch(gameStorageProvider.future)).count(userId: user?.id); } /// A provider that paginates the game history for a given user, or the current app user if no user is provided. @@ -108,6 +97,7 @@ class UserGameHistory extends _$UserGameHistory { @override Future build( UserId? userId, { + /// Whether the history is requested in an online context. Applicable only /// when [userId] is null. /// @@ -123,25 +113,23 @@ class UserGameHistory extends _$UserGameHistory { }); final session = ref.watch(authSessionProvider); - final online = await ref - .watch(connectivityChangesProvider.selectAsync((c) => c.isOnline)); + final online = await ref.watch(connectivityChangesProvider.selectAsync((c) => c.isOnline)); final storage = await ref.watch(gameStorageProvider.future); final id = userId ?? session?.user.id; - final recentGames = id != null && online - ? ref.withClient( - (client) => GameRepository(client).getUserGames(id, filter: filter), - ) - : storage.page(userId: id, filter: filter).then( - (value) => value - // we can assume that `youAre` is not null either for logged - // in users or for anonymous users - .map( - (e) => - (game: e.game.data, pov: e.game.youAre ?? Side.white), - ) - .toIList(), - ); + final recentGames = + id != null && online + ? ref.withClient((client) => GameRepository(client).getUserGames(id, filter: filter)) + : storage + .page(userId: id, filter: filter) + .then( + (value) => + value + // we can assume that `youAre` is not null either for logged + // in users or for anonymous users + .map((e) => (game: e.game.data, pov: e.game.youAre ?? Side.white)) + .toIList(), + ); _list.addAll(await recentGames); @@ -165,42 +153,36 @@ class UserGameHistory extends _$UserGameHistory { Result.capture( userId != null ? ref.withClient( - (client) => GameRepository(client).getUserGames( - userId!, - max: _nbPerPage, - until: _list.last.game.createdAt, - filter: currentVal.filter, - ), - ) + (client) => GameRepository(client).getUserGames( + userId!, + max: _nbPerPage, + until: _list.last.game.createdAt, + filter: currentVal.filter, + ), + ) : currentVal.online && currentVal.session != null - ? ref.withClient( - (client) => GameRepository(client).getUserGames( - currentVal.session!.user.id, - max: _nbPerPage, - until: _list.last.game.createdAt, - filter: currentVal.filter, - ), - ) - : (await ref.watch(gameStorageProvider.future)) - .page(max: _nbPerPage, until: _list.last.game.createdAt) - .then( - (value) => value + ? ref.withClient( + (client) => GameRepository(client).getUserGames( + currentVal.session!.user.id, + max: _nbPerPage, + until: _list.last.game.createdAt, + filter: currentVal.filter, + ), + ) + : (await ref.watch(gameStorageProvider.future)) + .page(max: _nbPerPage, until: _list.last.game.createdAt) + .then( + (value) => + value // we can assume that `youAre` is not null either for logged // in users or for anonymous users - .map( - (e) => ( - game: e.game.data, - pov: e.game.youAre ?? Side.white - ), - ) + .map((e) => (game: e.game.data, pov: e.game.youAre ?? Side.white)) .toIList(), - ), + ), ).fold( (value) { if (value.isEmpty) { - state = AsyncData( - currentVal.copyWith(hasMore: false, isLoading: false), - ); + state = AsyncData(currentVal.copyWith(hasMore: false, isLoading: false)); return; } @@ -215,8 +197,7 @@ class UserGameHistory extends _$UserGameHistory { ); }, (error, stackTrace) { - state = - AsyncData(currentVal.copyWith(isLoading: false, hasError: true)); + state = AsyncData(currentVal.copyWith(isLoading: false, hasError: true)); }, ); } diff --git a/lib/src/model/game/game_preferences.dart b/lib/src/model/game/game_preferences.dart index d95a1e4795..31311b132a 100644 --- a/lib/src/model/game/game_preferences.dart +++ b/lib/src/model/game/game_preferences.dart @@ -7,8 +7,7 @@ part 'game_preferences.g.dart'; /// Local game preferences, defined client-side only. @riverpod -class GamePreferences extends _$GamePreferences - with PreferencesStorage { +class GamePreferences extends _$GamePreferences with PreferencesStorage { // ignore: avoid_public_notifier_properties @override final prefCategory = PrefCategory.game; @@ -31,23 +30,15 @@ class GamePreferences extends _$GamePreferences } Future toggleBlindfoldMode() { - return save( - state.copyWith(blindfoldMode: !(state.blindfoldMode ?? false)), - ); + return save(state.copyWith(blindfoldMode: !(state.blindfoldMode ?? false))); } } @Freezed(fromJson: true, toJson: true) class GamePrefs with _$GamePrefs implements Serializable { - const factory GamePrefs({ - bool? enableChat, - bool? blindfoldMode, - }) = _GamePrefs; + const factory GamePrefs({bool? enableChat, bool? blindfoldMode}) = _GamePrefs; - static const defaults = GamePrefs( - enableChat: true, - ); + static const defaults = GamePrefs(enableChat: true); - factory GamePrefs.fromJson(Map json) => - _$GamePrefsFromJson(json); + factory GamePrefs.fromJson(Map json) => _$GamePrefsFromJson(json); } diff --git a/lib/src/model/game/game_repository.dart b/lib/src/model/game/game_repository.dart index 84f56e4ad2..8d6fc24b59 100644 --- a/lib/src/model/game/game_repository.dart +++ b/lib/src/model/game/game_repository.dart @@ -15,10 +15,7 @@ class GameRepository { Future getGame(GameId id) { return client.readJson( - Uri( - path: '/game/export/$id', - queryParameters: {'clocks': '1', 'accuracy': '1'}, - ), + Uri(path: '/game/export/$id', queryParameters: {'clocks': '1', 'accuracy': '1'}), headers: {'Accept': 'application/json'}, mapper: ArchivedGame.fromServerJson, ); @@ -26,14 +23,9 @@ class GameRepository { Future requestServerAnalysis(GameId id) async { final uri = Uri(path: '/$id/request-analysis'); - final response = await client.post( - Uri(path: '/$id/request-analysis'), - ); + final response = await client.post(Uri(path: '/$id/request-analysis')); if (response.statusCode >= 400) { - throw http.ClientException( - 'Failed to request analysis: ${response.statusCode}', - uri, - ); + throw http.ClientException('Failed to request analysis: ${response.statusCode}', uri); } } @@ -53,8 +45,7 @@ class GameRepository { path: '/api/games/user/$userId', queryParameters: { 'max': max.toString(), - if (until != null) - 'until': until.millisecondsSinceEpoch.toString(), + if (until != null) 'until': until.millisecondsSinceEpoch.toString(), 'moves': 'false', 'lastFen': 'true', 'accuracy': 'true', @@ -68,15 +59,16 @@ class GameRepository { mapper: LightArchivedGame.fromServerJson, ) .then( - (value) => value - .map( - (e) => ( - game: e, - // we know here user is not null for at least one of the players - pov: e.white.user?.id == userId ? Side.white : Side.black, - ), - ) - .toIList(), + (value) => + value + .map( + (e) => ( + game: e, + // we know here user is not null for at least one of the players + pov: e.white.user?.id == userId ? Side.white : Side.black, + ), + ) + .toIList(), ); } @@ -86,22 +78,14 @@ class GameRepository { return Future.value(IList()); } return client.readJsonList( - Uri( - path: '/api/mobile/my-games', - queryParameters: { - 'ids': ids.join(','), - }, - ), + Uri(path: '/api/mobile/my-games', queryParameters: {'ids': ids.join(',')}), mapper: PlayableGame.fromServerJson, ); } Future> getGamesByIds(ISet ids) { return client.postReadNdJsonList( - Uri( - path: '/api/games/export/_ids', - queryParameters: {'moves': 'false', 'lastFen': 'true'}, - ), + Uri(path: '/api/games/export/_ids', queryParameters: {'moves': 'false', 'lastFen': 'true'}), headers: {'Accept': 'application/x-ndjson'}, body: ids.join(','), mapper: LightArchivedGame.fromServerJson, diff --git a/lib/src/model/game/game_repository_providers.dart b/lib/src/model/game/game_repository_providers.dart index fdc69944aa..c9a95b6e43 100644 --- a/lib/src/model/game/game_repository_providers.dart +++ b/lib/src/model/game/game_repository_providers.dart @@ -23,11 +23,6 @@ Future archivedGame(Ref ref, {required GameId id}) async { } @riverpod -Future> gamesById( - Ref ref, { - required ISet ids, -}) { - return ref.withClient( - (client) => GameRepository(client).getGamesByIds(ids), - ); +Future> gamesById(Ref ref, {required ISet ids}) { + return ref.withClient((client) => GameRepository(client).getGamesByIds(ids)); } diff --git a/lib/src/model/game/game_share_service.dart b/lib/src/model/game/game_share_service.dart index b182564e67..b0996ffc8d 100644 --- a/lib/src/model/game/game_share_service.dart +++ b/lib/src/model/game/game_share_service.dart @@ -25,12 +25,7 @@ class GameShareService { Future rawPgn(GameId id) async { final resp = await _ref.withClient( (client) => client - .get( - Uri( - path: '/game/export/$id', - queryParameters: {'evals': '0', 'clocks': '0'}, - ), - ) + .get(Uri(path: '/game/export/$id', queryParameters: {'evals': '0', 'clocks': '0'})) .timeout(const Duration(seconds: 1)), ); if (resp.statusCode != 200) { @@ -43,12 +38,7 @@ class GameShareService { Future annotatedPgn(GameId id) async { final resp = await _ref.withClient( (client) => client - .get( - Uri( - path: '/game/export/$id', - queryParameters: {'literate': '1'}, - ), - ) + .get(Uri(path: '/game/export/$id', queryParameters: {'literate': '1'})) .timeout(const Duration(seconds: 1)), ); if (resp.statusCode != 200) { @@ -58,11 +48,7 @@ class GameShareService { } /// Fetches the GIF screenshot of a position and launches the share dialog. - Future screenshotPosition( - Side orientation, - String fen, - Move? lastMove, - ) async { + Future screenshotPosition(Side orientation, String fen, Move? lastMove) async { final boardTheme = _ref.read(boardPreferencesProvider).boardTheme; final pieceTheme = _ref.read(boardPreferencesProvider).pieceSet; final resp = await _ref @@ -82,9 +68,10 @@ class GameShareService { /// Fetches the GIF animation of a game. Future gameGif(GameId id, Side orientation) async { final boardPreferences = _ref.read(boardPreferencesProvider); - final boardTheme = boardPreferences.boardTheme == BoardTheme.system - ? BoardTheme.brown - : boardPreferences.boardTheme; + final boardTheme = + boardPreferences.boardTheme == BoardTheme.system + ? BoardTheme.brown + : boardPreferences.boardTheme; final pieceTheme = boardPreferences.pieceSet; final resp = await _ref .read(defaultClientProvider) @@ -101,26 +88,21 @@ class GameShareService { } /// Fetches the GIF animation of a study chapter. - Future chapterGif( - StringId id, - StringId chapterId, - ) async { + Future chapterGif(StringId id, StringId chapterId) async { final boardPreferences = _ref.read(boardPreferencesProvider); - final boardTheme = boardPreferences.boardTheme == BoardTheme.system - ? BoardTheme.brown - : boardPreferences.boardTheme; + final boardTheme = + boardPreferences.boardTheme == BoardTheme.system + ? BoardTheme.brown + : boardPreferences.boardTheme; final pieceTheme = boardPreferences.pieceSet; final resp = await _ref .read(lichessClientProvider) .get( - lichessUri( - '/study/$id/$chapterId.gif', - { - 'theme': boardTheme.gifApiName, - 'piece': pieceTheme.name, - }, - ), + lichessUri('/study/$id/$chapterId.gif', { + 'theme': boardTheme.gifApiName, + 'piece': pieceTheme.name, + }), ) .timeout(const Duration(seconds: 1)); if (resp.statusCode != 200) { diff --git a/lib/src/model/game/game_socket_events.dart b/lib/src/model/game/game_socket_events.dart index cfd8dd10e8..bd4463a9bf 100644 --- a/lib/src/model/game/game_socket_events.dart +++ b/lib/src/model/game/game_socket_events.dart @@ -15,10 +15,8 @@ part 'game_socket_events.freezed.dart'; @freezed class GameFullEvent with _$GameFullEvent { - const factory GameFullEvent({ - required PlayableGame game, - required int socketEventVersion, - }) = _GameFullEvent; + const factory GameFullEvent({required PlayableGame game, required int socketEventVersion}) = + _GameFullEvent; factory GameFullEvent.fromJson(Map json) { return GameFullEvent( @@ -41,12 +39,7 @@ class MoveEvent with _$MoveEvent { bool? blackOfferingDraw, GameStatus? status, Side? winner, - ({ - Duration white, - Duration black, - Duration? lag, - DateTime at, - })? clock, + ({Duration white, Duration black, Duration? lag, DateTime at})? clock, }) = _MoveEvent; factory MoveEvent.fromJson(Map json) => @@ -87,8 +80,7 @@ MoveEvent _socketMoveEventFromPick(RequiredPick pick) { at: clock.now(), white: it('white').asDurationFromSecondsOrThrow(), black: it('black').asDurationFromSecondsOrThrow(), - lag: it('lag') - .letOrNull((it) => Duration(milliseconds: it.asIntOrThrow() * 10)), + lag: it('lag').letOrNull((it) => Duration(milliseconds: it.asIntOrThrow() * 10)), ), ), ); @@ -114,12 +106,9 @@ GameEndEvent _gameEndEventFromPick(RequiredPick pick) { return GameEndEvent( status: pick('status').asGameStatusOrThrow(), winner: pick('winner').asSideOrNull(), - ratingDiff: pick('ratingDiff').letOrNull( - (it) => ( - white: it('white').asIntOrThrow(), - black: it('black').asIntOrThrow(), - ), - ), + ratingDiff: pick( + 'ratingDiff', + ).letOrNull((it) => (white: it('white').asIntOrThrow(), black: it('black').asIntOrThrow())), boosted: pick('boosted').asBoolOrNull(), clock: pick('clock').letOrNull( (it) => ( @@ -170,12 +159,10 @@ ServerEvalEvent _serverEvalEventFromPick(RequiredPick pick) { final glyph = glyphs?.first as Map?; final comments = node['comments'] as List?; final comment = comments?.first as Map?; - final judgment = glyph != null && comment != null - ? ( - name: _nagToJugdmentName(glyph['id'] as int), - comment: comment['text'] as String, - ) - : null; + final judgment = + glyph != null && comment != null + ? (name: _nagToJugdmentName(glyph['id'] as int), comment: comment['text'] as String) + : null; final variation = nextVariation; @@ -243,29 +230,19 @@ ServerEvalEvent _serverEvalEventFromPick(RequiredPick pick) { ), ), isAnalysisComplete: !isAnalysisIncomplete, - division: pick('division').letOrNull( - (it) => ( - middle: it('middle').asIntOrNull(), - end: it('end').asIntOrNull(), - ), - ), + division: pick( + 'division', + ).letOrNull((it) => (middle: it('middle').asIntOrNull(), end: it('end').asIntOrNull())), ); } String _nagToJugdmentName(int nag) => switch (nag) { - 6 => 'Inaccuracy', - 2 => 'Mistake', - 4 => 'Blunder', - int() => '', - }; + 6 => 'Inaccuracy', + 2 => 'Mistake', + 4 => 'Blunder', + int() => '', +}; -typedef ServerAnalysis = ({ - GameId id, - PlayerAnalysis white, - PlayerAnalysis black, -}); +typedef ServerAnalysis = ({GameId id, PlayerAnalysis white, PlayerAnalysis black}); -typedef GameDivision = ({ - int? middle, - int? end, -}); +typedef GameDivision = ({int? middle, int? end}); diff --git a/lib/src/model/game/game_status.dart b/lib/src/model/game/game_status.dart index b091550940..19242a75ca 100644 --- a/lib/src/model/game/game_status.dart +++ b/lib/src/model/game/game_status.dart @@ -55,9 +55,7 @@ extension GameExtension on Pick { return gameStatus; } } - throw PickException( - "value $value at $debugParsingExit can't be casted to GameStatus", - ); + throw PickException("value $value at $debugParsingExit can't be casted to GameStatus"); } GameStatus? asGameStatusOrNull() { diff --git a/lib/src/model/game/game_storage.dart b/lib/src/model/game/game_storage.dart index 5995a5e150..1a5fddc1e7 100644 --- a/lib/src/model/game/game_storage.dart +++ b/lib/src/model/game/game_storage.dart @@ -19,19 +19,13 @@ Future gameStorage(Ref ref) async { const kGameStorageTable = 'game'; -typedef StoredGame = ({ - UserId userId, - DateTime lastModified, - ArchivedGame game, -}); +typedef StoredGame = ({UserId userId, DateTime lastModified, ArchivedGame game}); class GameStorage { const GameStorage(this._db); final Database _db; - Future count({ - UserId? userId, - }) async { + Future count({UserId? userId}) async { final list = await _db.query( kGameStorageTable, where: 'userId = ?', @@ -48,14 +42,8 @@ class GameStorage { }) async { final list = await _db.query( kGameStorageTable, - where: [ - 'userId = ?', - if (until != null) 'lastModified < ?', - ].join(' AND '), - whereArgs: [ - userId ?? kStorageAnonId, - if (until != null) until.toIso8601String(), - ], + where: ['userId = ?', if (until != null) 'lastModified < ?'].join(' AND '), + whereArgs: [userId ?? kStorageAnonId, if (until != null) until.toIso8601String()], orderBy: 'lastModified DESC', limit: max, ); @@ -65,9 +53,7 @@ class GameStorage { final raw = e['data']! as String; final json = jsonDecode(raw); if (json is! Map) { - throw const FormatException( - '[GameStorage] cannot fetch game: expected an object', - ); + throw const FormatException('[GameStorage] cannot fetch game: expected an object'); } return ( userId: UserId(e['userId']! as String), @@ -75,17 +61,12 @@ class GameStorage { game: ArchivedGame.fromJson(json), ); }) - .where( - (e) => - filter.perfs.isEmpty || filter.perfs.contains(e.game.meta.perf), - ) + .where((e) => filter.perfs.isEmpty || filter.perfs.contains(e.game.meta.perf)) .where((e) => filter.side == null || filter.side == e.game.youAre) .toIList(); } - Future fetch({ - required GameId gameId, - }) async { + Future fetch({required GameId gameId}) async { final list = await _db.query( kGameStorageTable, where: 'gameId = ?', @@ -97,9 +78,7 @@ class GameStorage { if (raw != null) { final json = jsonDecode(raw); if (json is! Map) { - throw const FormatException( - '[GameStorage] cannot fetch game: expected an object', - ); + throw const FormatException('[GameStorage] cannot fetch game: expected an object'); } return ArchivedGame.fromJson(json); } @@ -107,23 +86,15 @@ class GameStorage { } Future save(ArchivedGame game) async { - await _db.insert( - kGameStorageTable, - { - 'userId': game.me?.user?.id.toString() ?? kStorageAnonId, - 'gameId': game.id.toString(), - 'lastModified': DateTime.now().toIso8601String(), - 'data': jsonEncode(game.toJson()), - }, - conflictAlgorithm: ConflictAlgorithm.replace, - ); + await _db.insert(kGameStorageTable, { + 'userId': game.me?.user?.id.toString() ?? kStorageAnonId, + 'gameId': game.id.toString(), + 'lastModified': DateTime.now().toIso8601String(), + 'data': jsonEncode(game.toJson()), + }, conflictAlgorithm: ConflictAlgorithm.replace); } Future delete(GameId gameId) async { - await _db.delete( - kGameStorageTable, - where: 'gameId = ?', - whereArgs: [gameId.toString()], - ); + await _db.delete(kGameStorageTable, where: 'gameId = ?', whereArgs: [gameId.toString()]); } } diff --git a/lib/src/model/game/material_diff.dart b/lib/src/model/game/material_diff.dart index 52623a11c0..b0bde7bd79 100644 --- a/lib/src/model/game/material_diff.dart +++ b/lib/src/model/game/material_diff.dart @@ -26,10 +26,8 @@ const IMap pieceScores = IMapConst({ class MaterialDiff with _$MaterialDiff { const MaterialDiff._(); - const factory MaterialDiff({ - required MaterialDiffSide black, - required MaterialDiffSide white, - }) = _MaterialDiff; + const factory MaterialDiff({required MaterialDiffSide black, required MaterialDiffSide white}) = + _MaterialDiff; factory MaterialDiff.fromBoard(Board board, {Board? startingPosition}) { int score = 0; @@ -37,11 +35,9 @@ class MaterialDiff with _$MaterialDiff { final IMap whiteCount = board.materialCount(Side.white); final IMap blackStartingCount = - startingPosition?.materialCount(Side.black) ?? - Board.standard.materialCount(Side.black); + startingPosition?.materialCount(Side.black) ?? Board.standard.materialCount(Side.black); final IMap whiteStartingCount = - startingPosition?.materialCount(Side.white) ?? - Board.standard.materialCount(Side.white); + startingPosition?.materialCount(Side.white) ?? Board.standard.materialCount(Side.white); IMap subtractPieceCounts( IMap startingCount, @@ -52,10 +48,8 @@ class MaterialDiff with _$MaterialDiff { ); } - final IMap blackCapturedPieces = - subtractPieceCounts(whiteStartingCount, whiteCount); - final IMap whiteCapturedPieces = - subtractPieceCounts(blackStartingCount, blackCount); + final IMap blackCapturedPieces = subtractPieceCounts(whiteStartingCount, whiteCount); + final IMap whiteCapturedPieces = subtractPieceCounts(blackStartingCount, blackCount); Map count; Map black; diff --git a/lib/src/model/game/over_the_board_game.dart b/lib/src/model/game/over_the_board_game.dart index daa88b95b0..20633e646e 100644 --- a/lib/src/model/game/over_the_board_game.dart +++ b/lib/src/model/game/over_the_board_game.dart @@ -15,8 +15,7 @@ part 'over_the_board_game.g.dart'; /// /// See [PlayableGame] for a game that is played online. @Freezed(fromJson: true, toJson: true) -abstract class OverTheBoardGame - with _$OverTheBoardGame, BaseGame, IndexableSteps { +abstract class OverTheBoardGame with _$OverTheBoardGame, BaseGame, IndexableSteps { const OverTheBoardGame._(); @override @@ -34,8 +33,7 @@ abstract class OverTheBoardGame @Assert('steps.isNotEmpty') factory OverTheBoardGame({ - @JsonKey(fromJson: stepsFromJson, toJson: stepsToJson) - required IList steps, + @JsonKey(fromJson: stepsFromJson, toJson: stepsToJson) required IList steps, required GameMeta meta, required String? initialFen, required GameStatus status, diff --git a/lib/src/model/game/playable_game.dart b/lib/src/model/game/playable_game.dart index 937ccaf03c..abd6ea85e7 100644 --- a/lib/src/model/game/playable_game.dart +++ b/lib/src/model/game/playable_game.dart @@ -30,9 +30,7 @@ part 'playable_game.freezed.dart'; /// See also: /// - [ArchivedGame] for a game that is finished and not owned by the current user. @freezed -class PlayableGame - with _$PlayableGame, BaseGame, IndexableSteps - implements BaseGame { +class PlayableGame with _$PlayableGame, BaseGame, IndexableSteps implements BaseGame { const PlayableGame._(); @Assert('steps.isNotEmpty') @@ -76,16 +74,18 @@ class PlayableGame } /// Player of the playing point of view. Null if spectating. - Player? get me => youAre == null - ? null - : youAre == Side.white + Player? get me => + youAre == null + ? null + : youAre == Side.white ? white : black; /// Opponent from the playing point of view. Null if spectating. - Player? get opponent => youAre == null - ? null - : youAre == Side.white + Player? get opponent => + youAre == null + ? null + : youAre == Side.white ? black : white; @@ -110,12 +110,8 @@ class PlayableGame (meta.rules == null || !meta.rules!.contains(GameRule.noAbort)); bool get resignable => playable && !abortable; bool get drawable => - playable && - lastPosition.fullmoves >= 2 && - !(me?.offeringDraw == true) && - !hasAI; - bool get rematchable => - meta.rules == null || !meta.rules!.contains(GameRule.noRematch); + playable && lastPosition.fullmoves >= 2 && !(me?.offeringDraw == true) && !hasAI; + bool get rematchable => meta.rules == null || !meta.rules!.contains(GameRule.noRematch); bool get canTakeback => takebackable && playable && @@ -131,8 +127,7 @@ class PlayableGame (meta.rules == null || !meta.rules!.contains(GameRule.noClaimWin)); bool get userAnalysable => - finished && steps.length > 4 || - (playable && (clock == null || youAre == null)); + finished && steps.length > 4 || (playable && (clock == null || youAre == null)); ArchivedGame toArchivedGame({required DateTime finishedAt}) { return ArchivedGame( @@ -153,12 +148,10 @@ class PlayableGame status: status, white: white, black: black, - clock: meta.clock != null - ? ( - initial: meta.clock!.initial, - increment: meta.clock!.increment, - ) - : null, + clock: + meta.clock != null + ? (initial: meta.clock!.initial, increment: meta.clock!.increment) + : null, opening: meta.opening, ), initialFen: initialFen, @@ -224,17 +217,15 @@ PlayableGame _playableGameFromPick(RequiredPick pick) { return PlayableGame( id: requiredGamePick('id').asGameIdOrThrow(), meta: meta, - source: requiredGamePick('source').letOrThrow( - (pick) => - GameSource.nameMap[pick.asStringOrThrow()] ?? GameSource.unknown, - ), + source: requiredGamePick( + 'source', + ).letOrThrow((pick) => GameSource.nameMap[pick.asStringOrThrow()] ?? GameSource.unknown), initialFen: initialFen, steps: steps.toIList(), white: pick('white').letOrThrow(_playerFromUserGamePick), black: pick('black').letOrThrow(_playerFromUserGamePick), clock: pick('clock').letOrNull(_playableClockDataFromPick), - correspondenceClock: - pick('correspondence').letOrNull(_correspondenceClockDataFromPick), + correspondenceClock: pick('correspondence').letOrNull(_correspondenceClockDataFromPick), status: pick('game', 'status').asGameStatusOrThrow(), winner: pick('game', 'winner').asSideOrNull(), boosted: pick('game', 'boosted').asBoolOrNull(), @@ -243,16 +234,14 @@ PlayableGame _playableGameFromPick(RequiredPick pick) { takebackable: pick('takebackable').asBoolOrFalse(), youAre: pick('youAre').asSideOrNull(), prefs: pick('prefs').letOrNull(_gamePrefsFromPick), - expiration: pick('expiration').letOrNull( - (it) { - final idle = it('idleMillis').asDurationFromMilliSecondsOrThrow(); - return ( - idle: idle, - timeToMove: it('millisToMove').asDurationFromMilliSecondsOrThrow(), - movedAt: DateTime.now().subtract(idle), - ); - }, - ), + expiration: pick('expiration').letOrNull((it) { + final idle = it('idleMillis').asDurationFromMilliSecondsOrThrow(); + return ( + idle: idle, + timeToMove: it('millisToMove').asDurationFromMilliSecondsOrThrow(), + movedAt: DateTime.now().subtract(idle), + ); + }), rematch: pick('game', 'rematch').asGameIdOrNull(), ); } @@ -272,14 +261,11 @@ GameMeta _playableGameMetaFromPick(RequiredPick pick) { moreTime: cPick('moretime').asDurationFromSecondsOrNull(), ), ), - daysPerTurn: pick('correspondence') - .letOrNull((ccPick) => ccPick('daysPerTurn').asIntOrThrow()), + daysPerTurn: pick('correspondence').letOrNull((ccPick) => ccPick('daysPerTurn').asIntOrThrow()), startedAtTurn: pick('game', 'startedAtTurn').asIntOrNull(), rules: pick('game', 'rules').letOrNull( (it) => ISet( - pick.asListOrThrow( - (e) => GameRule.nameMap[e.asStringOrThrow()] ?? GameRule.unknown, - ), + pick.asListOrThrow((e) => GameRule.nameMap[e.asStringOrThrow()] ?? GameRule.unknown), ), ), division: pick('division').letOrNull(_divisionFromPick), @@ -317,9 +303,7 @@ PlayableClockData _playableClockDataFromPick(RequiredPick pick) { running: pick('running').asBoolOrThrow(), white: pick('white').asDurationFromSecondsOrThrow(), black: pick('black').asDurationFromSecondsOrThrow(), - lag: pick('lag').letOrNull( - (it) => Duration(milliseconds: it.asIntOrThrow() * 10), - ), + lag: pick('lag').letOrNull((it) => Duration(milliseconds: it.asIntOrThrow() * 10)), at: DateTime.now(), ); } diff --git a/lib/src/model/game/player.dart b/lib/src/model/game/player.dart index e3abfebb38..8607e2a5c6 100644 --- a/lib/src/model/game/player.dart +++ b/lib/src/model/game/player.dart @@ -50,10 +50,7 @@ class Player with _$Player { user?.name ?? name ?? (aiLevel != null - ? context.l10n.aiNameLevelAiLevel( - 'Stockfish', - aiLevel.toString(), - ) + ? context.l10n.aiNameLevelAiLevel('Stockfish', aiLevel.toString()) : context.l10n.anonymous); Player setOnGame(bool onGame) { @@ -77,6 +74,5 @@ class PlayerAnalysis with _$PlayerAnalysis { int? accuracy, }) = _PlayerAnalysis; - factory PlayerAnalysis.fromJson(Map json) => - _$PlayerAnalysisFromJson(json); + factory PlayerAnalysis.fromJson(Map json) => _$PlayerAnalysisFromJson(json); } diff --git a/lib/src/model/lobby/create_game_service.dart b/lib/src/model/lobby/create_game_service.dart index fea652f63a..fef13347f5 100644 --- a/lib/src/model/lobby/create_game_service.dart +++ b/lib/src/model/lobby/create_game_service.dart @@ -17,11 +17,8 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'create_game_service.g.dart'; -typedef ChallengeResponse = ({ - GameFullId? gameFullId, - Challenge? challenge, - ChallengeDeclineReason? declineReason, -}); +typedef ChallengeResponse = + ({GameFullId? gameFullId, Challenge? challenge, ChallengeDeclineReason? declineReason}); /// A provider for the [CreateGameService]. @riverpod @@ -50,7 +47,8 @@ class CreateGameService { ChallengeId, StreamSubscription, // socket connects events StreamSubscription, // socket events - )? _challengeConnection; + )? + _challengeConnection; Timer? _challengePingTimer; @@ -90,8 +88,7 @@ class CreateGameService { } try { - await LobbyRepository(lichessClient) - .createSeek(actualSeek, sri: socketClient.sri); + await LobbyRepository(lichessClient).createSeek(actualSeek, sri: socketClient.sri); } catch (e) { _log.warning('Failed to create seek', e); // if the completer is not yet completed, complete it with an error @@ -108,10 +105,9 @@ class CreateGameService { _log.info('Creating new correspondence game'); await ref.withClient( - (client) => LobbyRepository(client).createSeek( - seek, - sri: ref.read(preloadedDataProvider).requireValue.sri, - ), + (client) => LobbyRepository( + client, + ).createSeek(seek, sri: ref.read(preloadedDataProvider).requireValue.sri), ); } @@ -120,9 +116,7 @@ class CreateGameService { /// Will listen to the challenge socket and await the response from the destinated user. /// Returns the challenge, along with [GameFullId] if the challenge was accepted, /// or the [ChallengeDeclineReason] if the challenge was declined. - Future newRealTimeChallenge( - ChallengeRequest challengeReq, - ) async { + Future newRealTimeChallenge(ChallengeRequest challengeReq) async { assert(challengeReq.timeControl == ChallengeTimeControlType.clock); if (_challengeConnection != null) { @@ -130,8 +124,7 @@ class CreateGameService { } // ensure the pending connection is closed in any case - final completer = Completer() - ..future.whenComplete(dispose); + final completer = Completer()..future.whenComplete(dispose); try { _log.info('Creating new challenge game'); @@ -162,21 +155,17 @@ class CreateGameService { try { final updatedChallenge = await repo.show(challenge.id); if (updatedChallenge.gameFullId != null) { - completer.complete( - ( - gameFullId: updatedChallenge.gameFullId, - challenge: null, - declineReason: null, - ), - ); + completer.complete(( + gameFullId: updatedChallenge.gameFullId, + challenge: null, + declineReason: null, + )); } else if (updatedChallenge.status == ChallengeStatus.declined) { - completer.complete( - ( - gameFullId: null, - challenge: challenge, - declineReason: updatedChallenge.declineReason, - ), - ); + completer.complete(( + gameFullId: null, + challenge: challenge, + declineReason: updatedChallenge.declineReason, + )); } } catch (e) { _log.warning('Failed to reload challenge', e); @@ -199,16 +188,12 @@ class CreateGameService { /// /// Returns the created challenge immediately. If the challenge is accepted, /// a notification will be sent to the user when the game starts. - Future newCorrespondenceChallenge( - ChallengeRequest challenge, - ) async { + Future newCorrespondenceChallenge(ChallengeRequest challenge) async { assert(challenge.timeControl == ChallengeTimeControlType.correspondence); _log.info('Creating new correspondence challenge'); - return ref.withClient( - (client) => ChallengeRepository(client).create(challenge), - ); + return ref.withClient((client) => ChallengeRepository(client).create(challenge)); } /// Cancel the current game creation. diff --git a/lib/src/model/lobby/game_seek.dart b/lib/src/model/lobby/game_seek.dart index b44d7ce262..ec9e8386b6 100644 --- a/lib/src/model/lobby/game_seek.dart +++ b/lib/src/model/lobby/game_seek.dart @@ -26,10 +26,7 @@ class GameSeek with _$GameSeek { 'ratingDelta == null || ratingRange == null', 'Rating delta and rating range cannot be used together', ) - @Assert( - 'clock != null || days != null', - 'Either clock or days must be set', - ) + @Assert('clock != null || days != null', 'Either clock or days must be set') const factory GameSeek({ (Duration time, Duration increment)? clock, int? days, @@ -46,10 +43,7 @@ class GameSeek with _$GameSeek { /// Construct a game seek from a predefined time control. factory GameSeek.fastPairing(TimeIncrement setup, AuthSessionState? session) { return GameSeek( - clock: ( - Duration(seconds: setup.time), - Duration(seconds: setup.increment), - ), + clock: (Duration(seconds: setup.time), Duration(seconds: setup.increment)), rated: session != null, ); } @@ -63,8 +57,7 @@ class GameSeek with _$GameSeek { ), rated: account != null && setup.customRated, variant: setup.customVariant, - ratingRange: - account != null ? setup.ratingRangeFromCustom(account) : null, + ratingRange: account != null ? setup.ratingRangeFromCustom(account) : null, ); } @@ -74,25 +67,19 @@ class GameSeek with _$GameSeek { days: setup.customDaysPerTurn, rated: account != null && setup.customRated, variant: setup.customVariant, - ratingRange: - account != null ? setup.ratingRangeFromCustom(account) : null, + ratingRange: account != null ? setup.ratingRangeFromCustom(account) : null, ); } /// Construct a game seek from a playable game to find a new opponent, using /// the same time control, variant and rated status. - factory GameSeek.newOpponentFromGame( - PlayableGame game, - GameSetupPrefs setup, - ) { + factory GameSeek.newOpponentFromGame(PlayableGame game, GameSetupPrefs setup) { return GameSeek( - clock: game.meta.clock != null - ? (game.meta.clock!.initial, game.meta.clock!.increment) - : null, + clock: + game.meta.clock != null ? (game.meta.clock!.initial, game.meta.clock!.increment) : null, rated: game.meta.rated, variant: game.meta.variant, - ratingDelta: - game.source == GameSource.lobby ? setup.customRatingDelta : null, + ratingDelta: game.source == GameSource.lobby ? setup.customRatingDelta : null, ); } @@ -109,27 +96,20 @@ class GameSeek with _$GameSeek { return copyWith(ratingRange: range, ratingDelta: null); } - TimeIncrement? get timeIncrement => clock != null - ? TimeIncrement( - clock!.$1.inSeconds, - clock!.$2.inSeconds, - ) - : null; + TimeIncrement? get timeIncrement => + clock != null ? TimeIncrement(clock!.$1.inSeconds, clock!.$2.inSeconds) : null; Perf get perf => Perf.fromVariantAndSpeed( - variant ?? Variant.standard, - timeIncrement != null - ? Speed.fromTimeIncrement(timeIncrement!) - : Speed.correspondence, - ); + variant ?? Variant.standard, + timeIncrement != null ? Speed.fromTimeIncrement(timeIncrement!) : Speed.correspondence, + ); Map get requestBody => { - if (clock != null) 'time': (clock!.$1.inSeconds / 60).toString(), - if (clock != null) 'increment': clock!.$2.inSeconds.toString(), - if (days != null) 'days': days.toString(), - 'rated': rated.toString(), - if (variant != null) 'variant': variant!.name, - if (ratingRange != null) - 'ratingRange': '${ratingRange!.$1}-${ratingRange!.$2}', - }; + if (clock != null) 'time': (clock!.$1.inSeconds / 60).toString(), + if (clock != null) 'increment': clock!.$2.inSeconds.toString(), + if (days != null) 'days': days.toString(), + 'rated': rated.toString(), + if (variant != null) 'variant': variant!.name, + if (ratingRange != null) 'ratingRange': '${ratingRange!.$1}-${ratingRange!.$2}', + }; } diff --git a/lib/src/model/lobby/game_setup_preferences.dart b/lib/src/model/lobby/game_setup_preferences.dart index 6e0103195c..ca9da1e218 100644 --- a/lib/src/model/lobby/game_setup_preferences.dart +++ b/lib/src/model/lobby/game_setup_preferences.dart @@ -22,8 +22,7 @@ class GameSetupPreferences extends _$GameSetupPreferences GameSetupPrefs defaults({LightUser? user}) => GameSetupPrefs.defaults; @override - GameSetupPrefs fromJson(Map json) => - GameSetupPrefs.fromJson(json); + GameSetupPrefs fromJson(Map json) => GameSetupPrefs.fromJson(json); @override GameSetupPrefs build() { @@ -91,17 +90,10 @@ class GameSetupPrefs with _$GameSetupPrefs implements Serializable { customDaysPerTurn: 3, ); - Speed get speedFromCustom => Speed.fromTimeIncrement( - TimeIncrement( - customTimeSeconds, - customIncrementSeconds, - ), - ); + Speed get speedFromCustom => + Speed.fromTimeIncrement(TimeIncrement(customTimeSeconds, customIncrementSeconds)); - Perf get perfFromCustom => Perf.fromVariantAndSpeed( - customVariant, - speedFromCustom, - ); + Perf get perfFromCustom => Perf.fromVariantAndSpeed(customVariant, speedFromCustom); /// Returns the rating range for the custom setup, or null if the user /// doesn't have a rating for the custom setup perf. @@ -123,33 +115,9 @@ class GameSetupPrefs with _$GameSetupPrefs implements Serializable { } } -const kSubtractingRatingRange = [ - -500, - -450, - -400, - -350, - -300, - -250, - -200, - -150, - -100, - -50, - 0, -]; +const kSubtractingRatingRange = [-500, -450, -400, -350, -300, -250, -200, -150, -100, -50, 0]; -const kAddingRatingRange = [ - 0, - 50, - 100, - 150, - 200, - 250, - 300, - 350, - 400, - 450, - 500, -]; +const kAddingRatingRange = [0, 50, 100, 150, 200, 250, 300, 350, 400, 450, 500]; const kAvailableTimesInSeconds = [ 0, diff --git a/lib/src/model/lobby/lobby_numbers.dart b/lib/src/model/lobby/lobby_numbers.dart index b7bf6df761..88b1fea700 100644 --- a/lib/src/model/lobby/lobby_numbers.dart +++ b/lib/src/model/lobby/lobby_numbers.dart @@ -22,10 +22,7 @@ class LobbyNumbers extends _$LobbyNumbers { _socketSubscription = socketGlobalStream.listen((event) { if (event.topic == 'n') { final data = event.data as Map; - state = ( - nbPlayers: data['nbPlayers']!, - nbGames: data['nbGames']!, - ); + state = (nbPlayers: data['nbPlayers']!, nbGames: data['nbGames']!); } }); diff --git a/lib/src/model/lobby/lobby_repository.dart b/lib/src/model/lobby/lobby_repository.dart index db59a23602..4565a91b72 100644 --- a/lib/src/model/lobby/lobby_repository.dart +++ b/lib/src/model/lobby/lobby_repository.dart @@ -15,9 +15,7 @@ part 'lobby_repository.g.dart'; @riverpod Future> correspondenceChallenges(Ref ref) { - return ref.withClient( - (client) => LobbyRepository(client).getCorrespondenceChallenges(), - ); + return ref.withClient((client) => LobbyRepository(client).getCorrespondenceChallenges()); } class LobbyRepository { @@ -29,10 +27,7 @@ class LobbyRepository { final uri = Uri(path: '/api/board/seek', queryParameters: {'sri': sri}); final response = await client.post(uri, body: seek.requestBody); if (response.statusCode >= 400) { - throw http.ClientException( - 'Failed to create seek: ${response.statusCode}', - uri, - ); + throw http.ClientException('Failed to create seek: ${response.statusCode}', uri); } } @@ -40,10 +35,7 @@ class LobbyRepository { final uri = Uri(path: '/api/board/seek', queryParameters: {'sri': sri}); final response = await client.delete(uri); if (response.statusCode >= 400) { - throw http.ClientException( - 'Failed to cancel seek: ${response.statusCode}', - uri, - ); + throw http.ClientException('Failed to cancel seek: ${response.statusCode}', uri); } } diff --git a/lib/src/model/notifications/notification_service.dart b/lib/src/model/notifications/notification_service.dart index b49a82a152..01f7119b51 100644 --- a/lib/src/model/notifications/notification_service.dart +++ b/lib/src/model/notifications/notification_service.dart @@ -25,8 +25,7 @@ final _logger = Logger('NotificationService'); /// A provider instance of the [FlutterLocalNotificationsPlugin]. @Riverpod(keepAlive: true) -FlutterLocalNotificationsPlugin notificationDisplay(Ref _) => - FlutterLocalNotificationsPlugin(); +FlutterLocalNotificationsPlugin notificationDisplay(Ref _) => FlutterLocalNotificationsPlugin(); /// A provider instance of the [NotificationService]. @Riverpod(keepAlive: true) @@ -54,12 +53,11 @@ class NotificationService { StreamSubscription? _fcmTokenRefreshSubscription; /// The connectivity changes stream subscription. - ProviderSubscription>? - _connectivitySubscription; + ProviderSubscription>? _connectivitySubscription; /// The stream controller for notification responses. - static final StreamController - _responseStreamController = StreamController.broadcast(); + static final StreamController _responseStreamController = + StreamController.broadcast(); /// The stream subscription for notification responses. StreamSubscription? _responseStreamSubscription; @@ -82,8 +80,7 @@ class NotificationService { /// and after [LichessBinding.initializeNotifications] has been called. Future start() async { // listen for connectivity changes to register device once the app is online - _connectivitySubscription = - _ref.listen(connectivityChangesProvider, (prev, current) async { + _connectivitySubscription = _ref.listen(connectivityChangesProvider, (prev, current) async { if (current.value?.isOnline == true && !_registeredDevice) { try { await registerDevice(); @@ -95,8 +92,7 @@ class NotificationService { }); // Listen for incoming messages while the app is in the foreground. - LichessBinding.instance.firebaseMessagingOnMessage - .listen((RemoteMessage message) { + LichessBinding.instance.firebaseMessagingOnMessage.listen((RemoteMessage message) { _processFcmMessage(message, fromBackground: false); }); @@ -118,9 +114,9 @@ class NotificationService { ); // Listen for token refresh and update the token on the server accordingly. - _fcmTokenRefreshSubscription = LichessBinding - .instance.firebaseMessaging.onTokenRefresh - .listen((String token) { + _fcmTokenRefreshSubscription = LichessBinding.instance.firebaseMessaging.onTokenRefresh.listen(( + String token, + ) { _registerToken(token); }); @@ -134,12 +130,12 @@ class NotificationService { } // Handle any other interaction that caused the app to open when in background. - LichessBinding.instance.firebaseMessagingOnMessageOpenedApp - .listen(_handleFcmMessageOpenedApp); + LichessBinding.instance.firebaseMessagingOnMessageOpenedApp.listen(_handleFcmMessageOpenedApp); // start listening for notification responses - _responseStreamSubscription = - _responseStreamController.stream.listen(_dispatchNotificationResponse); + _responseStreamSubscription = _responseStreamController.stream.listen( + _dispatchNotificationResponse, + ); } /// Shows a notification. @@ -184,22 +180,15 @@ class NotificationService { switch (notification) { case CorresGameUpdateNotification(fullId: final gameFullId): - _ref - .read(correspondenceServiceProvider) - .onNotificationResponse(gameFullId); + _ref.read(correspondenceServiceProvider).onNotificationResponse(gameFullId); case ChallengeNotification(challenge: final challenge): - _ref.read(challengeServiceProvider).onNotificationResponse( - response.actionId, - challenge, - ); + _ref.read(challengeServiceProvider).onNotificationResponse(response.actionId, challenge); } } /// Function called by the notification plugin when a notification has been tapped on. static void onDidReceiveNotificationResponse(NotificationResponse response) { - _logger.fine( - 'received local notification ${response.id} response in foreground.', - ); + _logger.fine('received local notification ${response.id} response in foreground.'); _responseStreamController.add(response); } @@ -214,9 +203,7 @@ class NotificationService { // TODO: handle other notification types case UnhandledFcmMessage(data: final data): - _logger.warning( - 'Received unhandled FCM notification type: ${data['lichess.type']}', - ); + _logger.warning('Received unhandled FCM notification type: ${data['lichess.type']}'); case MalformedFcmMessage(data: final data): _logger.severe('Received malformed FCM message: $data'); @@ -238,6 +225,7 @@ class NotificationService { /// badge count according to the value held by the server. Future _processFcmMessage( RemoteMessage message, { + /// Whether the message was received while the app was in the background. required bool fromBackground, }) async { @@ -249,34 +237,24 @@ class NotificationService { switch (parsedMessage) { case CorresGameUpdateFcmMessage( - fullId: final fullId, - game: final game, - notification: final notification - ): + fullId: final fullId, + game: final game, + notification: final notification, + ): if (game != null) { - await _ref.read(correspondenceServiceProvider).onServerUpdateEvent( - fullId, - game, - fromBackground: fromBackground, - ); + await _ref + .read(correspondenceServiceProvider) + .onServerUpdateEvent(fullId, game, fromBackground: fromBackground); } if (fromBackground == false && notification != null) { - await show( - CorresGameUpdateNotification( - fullId, - notification.title!, - notification.body!, - ), - ); + await show(CorresGameUpdateNotification(fullId, notification.title!, notification.body!)); } // TODO: handle other notification types case UnhandledFcmMessage(data: final data): - _logger.warning( - 'Received unhandled FCM notification type: ${data['lichess.type']}', - ); + _logger.warning('Received unhandled FCM notification type: ${data['lichess.type']}'); case MalformedFcmMessage(data: final data): _logger.severe('Received malformed FCM message: $data'); @@ -316,17 +294,14 @@ class NotificationService { return; } try { - await _ref.withClient( - (client) => client.post(Uri(path: '/mobile/unregister')), - ); + await _ref.withClient((client) => client.post(Uri(path: '/mobile/unregister'))); } catch (e, st) { _logger.severe('could not unregister device; $e', e, st); } } Future _registerToken(String token) async { - final settings = await LichessBinding.instance.firebaseMessaging - .getNotificationSettings(); + final settings = await LichessBinding.instance.firebaseMessaging.getNotificationSettings(); if (settings.authorizationStatus == AuthorizationStatus.denied) { return; } @@ -336,18 +311,14 @@ class NotificationService { return; } try { - await _ref.withClient( - (client) => client.post(Uri(path: '/mobile/register/firebase/$token')), - ); + await _ref.withClient((client) => client.post(Uri(path: '/mobile/register/firebase/$token'))); } catch (e, st) { _logger.severe('could not register device; $e', e, st); } } @pragma('vm:entry-point') - static Future _firebaseMessagingBackgroundHandler( - RemoteMessage message, - ) async { + static Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { // create a new provider scope for the background isolate final ref = ProviderContainer(); @@ -356,10 +327,7 @@ class NotificationService { await ref.read(preloadedDataProvider.future); try { - await ref.read(notificationServiceProvider)._processFcmMessage( - message, - fromBackground: true, - ); + await ref.read(notificationServiceProvider)._processFcmMessage(message, fromBackground: true); ref.dispose(); } catch (e) { diff --git a/lib/src/model/notifications/notifications.dart b/lib/src/model/notifications/notifications.dart index 1b3fdd6dfd..771887faa8 100644 --- a/lib/src/model/notifications/notifications.dart +++ b/lib/src/model/notifications/notifications.dart @@ -48,11 +48,10 @@ sealed class FcmMessage { final round = message.data['lichess.round'] as String?; if (gameFullId != null) { final fullId = GameFullId(gameFullId); - final game = round != null - ? PlayableGame.fromServerJson( - jsonDecode(round) as Map, - ) - : null; + final game = + round != null + ? PlayableGame.fromServerJson(jsonDecode(round) as Map) + : null; return CorresGameUpdateFcmMessage( fullId, game: game, @@ -71,11 +70,7 @@ sealed class FcmMessage { /// An [FcmMessage] that represents a correspondence game update. @immutable class CorresGameUpdateFcmMessage extends FcmMessage { - const CorresGameUpdateFcmMessage( - this.fullId, { - required this.game, - required this.notification, - }); + const CorresGameUpdateFcmMessage(this.fullId, {required this.game, required this.notification}); final GameFullId fullId; final PlayableGame? game; @@ -137,10 +132,7 @@ sealed class LocalNotification { /// /// See [LocalNotification.fromJson] where the [channelId] is used to determine the /// concrete type of the notification, to be able to deserialize it. - Map get payload => { - 'channel': channelId, - ..._concretePayload, - }; + Map get payload => {'channel': channelId, ..._concretePayload}; /// The actual payload of the notification. /// @@ -173,8 +165,8 @@ sealed class LocalNotification { /// are generated server side and are included in the FCM message's [RemoteMessage.notification] field. class CorresGameUpdateNotification extends LocalNotification { const CorresGameUpdateNotification(this.fullId, String title, String body) - : _title = title, - _body = body; + : _title = title, + _body = body; final GameFullId fullId; @@ -196,10 +188,10 @@ class CorresGameUpdateNotification extends LocalNotification { @override Map get _concretePayload => { - 'fullId': fullId.toJson(), - 'title': _title, - 'body': _body, - }; + 'fullId': fullId.toJson(), + 'title': _title, + 'body': _body, + }; @override String title(_) => _title; @@ -209,15 +201,15 @@ class CorresGameUpdateNotification extends LocalNotification { @override NotificationDetails details(AppLocalizations l10n) => NotificationDetails( - android: AndroidNotificationDetails( - channelId, - l10n.preferencesNotifyGameEvent, - importance: Importance.high, - priority: Priority.defaultPriority, - autoCancel: true, - ), - iOS: DarwinNotificationDetails(threadIdentifier: channelId), - ); + android: AndroidNotificationDetails( + channelId, + l10n.preferencesNotifyGameEvent, + importance: Importance.high, + priority: Priority.defaultPriority, + autoCancel: true, + ), + iOS: DarwinNotificationDetails(threadIdentifier: channelId), + ); } /// A notification for a received challenge. @@ -230,8 +222,7 @@ class ChallengeNotification extends LocalNotification { final Challenge challenge; factory ChallengeNotification.fromJson(Map json) { - final challenge = - Challenge.fromJson(json['challenge'] as Map); + final challenge = Challenge.fromJson(json['challenge'] as Map); return ChallengeNotification(challenge); } @@ -242,76 +233,66 @@ class ChallengeNotification extends LocalNotification { int get id => challenge.id.value.hashCode; @override - Map get _concretePayload => { - 'challenge': challenge.toJson(), - }; + Map get _concretePayload => {'challenge': challenge.toJson()}; @override - String title(AppLocalizations _) => - '${challenge.challenger!.user.name} challenges you!'; + String title(AppLocalizations _) => '${challenge.challenger!.user.name} challenges you!'; @override String body(AppLocalizations l10n) => challenge.description(l10n); @override NotificationDetails details(AppLocalizations l10n) => NotificationDetails( - android: AndroidNotificationDetails( - channelId, - l10n.preferencesNotifyChallenge, - importance: Importance.max, - priority: Priority.high, - autoCancel: false, - actions: [ - if (challenge.variant.isPlaySupported) - AndroidNotificationAction( - 'accept', - l10n.accept, - icon: const DrawableResourceAndroidBitmap('tick'), - showsUserInterface: true, - contextual: true, - ), - AndroidNotificationAction( - 'decline', - l10n.decline, - icon: const DrawableResourceAndroidBitmap('cross'), - showsUserInterface: true, - contextual: true, - ), - ], + android: AndroidNotificationDetails( + channelId, + l10n.preferencesNotifyChallenge, + importance: Importance.max, + priority: Priority.high, + autoCancel: false, + actions: [ + if (challenge.variant.isPlaySupported) + AndroidNotificationAction( + 'accept', + l10n.accept, + icon: const DrawableResourceAndroidBitmap('tick'), + showsUserInterface: true, + contextual: true, + ), + AndroidNotificationAction( + 'decline', + l10n.decline, + icon: const DrawableResourceAndroidBitmap('cross'), + showsUserInterface: true, + contextual: true, ), - iOS: DarwinNotificationDetails( - threadIdentifier: channelId, - categoryIdentifier: challenge.variant.isPlaySupported + ], + ), + iOS: DarwinNotificationDetails( + threadIdentifier: channelId, + categoryIdentifier: + challenge.variant.isPlaySupported ? darwinPlayableVariantCategoryId : darwinUnplayableVariantCategoryId, - ), - ); + ), + ); - static const darwinPlayableVariantCategoryId = - 'challenge-notification-playable-variant'; + static const darwinPlayableVariantCategoryId = 'challenge-notification-playable-variant'; - static const darwinUnplayableVariantCategoryId = - 'challenge-notification-unplayable-variant'; + static const darwinUnplayableVariantCategoryId = 'challenge-notification-unplayable-variant'; - static DarwinNotificationCategory darwinPlayableVariantCategory( - AppLocalizations l10n, - ) => + static DarwinNotificationCategory darwinPlayableVariantCategory(AppLocalizations l10n) => DarwinNotificationCategory( darwinPlayableVariantCategoryId, actions: [ DarwinNotificationAction.plain( 'accept', l10n.accept, - options: { - DarwinNotificationActionOption.foreground, - }, + options: {DarwinNotificationActionOption.foreground}, ), DarwinNotificationAction.plain( 'decline', l10n.decline, - options: { - DarwinNotificationActionOption.foreground, - }, + options: {DarwinNotificationActionOption.foreground}, ), ], options: { @@ -319,18 +300,14 @@ class ChallengeNotification extends LocalNotification { }, ); - static DarwinNotificationCategory darwinUnplayableVariantCategory( - AppLocalizations l10n, - ) => + static DarwinNotificationCategory darwinUnplayableVariantCategory(AppLocalizations l10n) => DarwinNotificationCategory( darwinUnplayableVariantCategoryId, actions: [ DarwinNotificationAction.plain( 'decline', l10n.decline, - options: { - DarwinNotificationActionOption.foreground, - }, + options: {DarwinNotificationActionOption.foreground}, ), ], options: { diff --git a/lib/src/model/opening_explorer/opening_explorer.dart b/lib/src/model/opening_explorer/opening_explorer.dart index 6a8b0ca281..4b62f41823 100644 --- a/lib/src/model/opening_explorer/opening_explorer.dart +++ b/lib/src/model/opening_explorer/opening_explorer.dart @@ -9,11 +9,7 @@ import 'package:lichess_mobile/src/model/opening_explorer/opening_explorer_prefe part 'opening_explorer.freezed.dart'; part 'opening_explorer.g.dart'; -enum OpeningDatabase { - master, - lichess, - player, -} +enum OpeningDatabase { master, lichess, player } @Freezed(fromJson: true) class OpeningExplorerEntry with _$OpeningExplorerEntry { @@ -30,12 +26,8 @@ class OpeningExplorerEntry with _$OpeningExplorerEntry { int? queuePosition, }) = _OpeningExplorerEntry; - factory OpeningExplorerEntry.empty() => const OpeningExplorerEntry( - white: 0, - draws: 0, - black: 0, - moves: IList.empty(), - ); + factory OpeningExplorerEntry.empty() => + const OpeningExplorerEntry(white: 0, draws: 0, black: 0, moves: IList.empty()); factory OpeningExplorerEntry.fromJson(Map json) => _$OpeningExplorerEntryFromJson(json); @@ -57,8 +49,7 @@ class OpeningMove with _$OpeningMove { OpeningExplorerGame? game, }) = _OpeningMove; - factory OpeningMove.fromJson(Map json) => - _$OpeningMoveFromJson(json); + factory OpeningMove.fromJson(Map json) => _$OpeningMoveFromJson(json); int get games { return white + draws + black; @@ -86,16 +77,10 @@ class OpeningExplorerGame with _$OpeningExplorerGame { return OpeningExplorerGame( id: pick('id').asGameIdOrThrow(), white: pick('white').letOrThrow( - (pick) => ( - name: pick('name').asStringOrThrow(), - rating: pick('rating').asIntOrThrow() - ), + (pick) => (name: pick('name').asStringOrThrow(), rating: pick('rating').asIntOrThrow()), ), black: pick('black').letOrThrow( - (pick) => ( - name: pick('name').asStringOrThrow(), - rating: pick('rating').asIntOrThrow() - ), + (pick) => (name: pick('name').asStringOrThrow(), rating: pick('rating').asIntOrThrow()), ), uci: pick('uci').asStringOrNull(), winner: pick('winner').asStringOrNull(), @@ -111,10 +96,7 @@ class OpeningExplorerGame with _$OpeningExplorerGame { } } -enum GameMode { - casual, - rated, -} +enum GameMode { casual, rated } @freezed class OpeningExplorerCacheKey with _$OpeningExplorerCacheKey { diff --git a/lib/src/model/opening_explorer/opening_explorer_preferences.dart b/lib/src/model/opening_explorer/opening_explorer_preferences.dart index 833b70af6c..f3dcbc1188 100644 --- a/lib/src/model/opening_explorer/opening_explorer_preferences.dart +++ b/lib/src/model/opening_explorer/opening_explorer_preferences.dart @@ -18,84 +18,76 @@ class OpeningExplorerPreferences extends _$OpeningExplorerPreferences final prefCategory = PrefCategory.openingExplorer; @override - OpeningExplorerPrefs defaults({LightUser? user}) => - OpeningExplorerPrefs.defaults(user: user); + OpeningExplorerPrefs defaults({LightUser? user}) => OpeningExplorerPrefs.defaults(user: user); @override - OpeningExplorerPrefs fromJson(Map json) => - OpeningExplorerPrefs.fromJson(json); + OpeningExplorerPrefs fromJson(Map json) => OpeningExplorerPrefs.fromJson(json); @override OpeningExplorerPrefs build() { return fetch(); } - Future setDatabase(OpeningDatabase db) => save( - state.copyWith(db: db), - ); + Future setDatabase(OpeningDatabase db) => save(state.copyWith(db: db)); Future setMasterDbSince(int year) => save(state.copyWith(masterDb: state.masterDb.copyWith(sinceYear: year))); Future toggleLichessDbSpeed(Speed speed) => save( - state.copyWith( - lichessDb: state.lichessDb.copyWith( - speeds: state.lichessDb.speeds.contains(speed) + state.copyWith( + lichessDb: state.lichessDb.copyWith( + speeds: + state.lichessDb.speeds.contains(speed) ? state.lichessDb.speeds.remove(speed) : state.lichessDb.speeds.add(speed), - ), - ), - ); + ), + ), + ); Future toggleLichessDbRating(int rating) => save( - state.copyWith( - lichessDb: state.lichessDb.copyWith( - ratings: state.lichessDb.ratings.contains(rating) + state.copyWith( + lichessDb: state.lichessDb.copyWith( + ratings: + state.lichessDb.ratings.contains(rating) ? state.lichessDb.ratings.remove(rating) : state.lichessDb.ratings.add(rating), - ), - ), - ); - - Future setLichessDbSince(DateTime since) => save( - state.copyWith(lichessDb: state.lichessDb.copyWith(since: since)), - ); - - Future setPlayerDbUsernameOrId(String username) => save( - state.copyWith( - playerDb: state.playerDb.copyWith( - username: username, - ), - ), - ); - - Future setPlayerDbSide(Side side) => save( - state.copyWith(playerDb: state.playerDb.copyWith(side: side)), - ); + ), + ), + ); + + Future setLichessDbSince(DateTime since) => + save(state.copyWith(lichessDb: state.lichessDb.copyWith(since: since))); + + Future setPlayerDbUsernameOrId(String username) => + save(state.copyWith(playerDb: state.playerDb.copyWith(username: username))); + + Future setPlayerDbSide(Side side) => + save(state.copyWith(playerDb: state.playerDb.copyWith(side: side))); Future togglePlayerDbSpeed(Speed speed) => save( - state.copyWith( - playerDb: state.playerDb.copyWith( - speeds: state.playerDb.speeds.contains(speed) + state.copyWith( + playerDb: state.playerDb.copyWith( + speeds: + state.playerDb.speeds.contains(speed) ? state.playerDb.speeds.remove(speed) : state.playerDb.speeds.add(speed), - ), - ), - ); + ), + ), + ); Future togglePlayerDbGameMode(GameMode gameMode) => save( - state.copyWith( - playerDb: state.playerDb.copyWith( - gameModes: state.playerDb.gameModes.contains(gameMode) + state.copyWith( + playerDb: state.playerDb.copyWith( + gameModes: + state.playerDb.gameModes.contains(gameMode) ? state.playerDb.gameModes.remove(gameMode) : state.playerDb.gameModes.add(gameMode), - ), - ), - ); + ), + ), + ); - Future setPlayerDbSince(DateTime since) => save( - state.copyWith(playerDb: state.playerDb.copyWith(since: since)), - ); + Future setPlayerDbSince(DateTime since) => + save(state.copyWith(playerDb: state.playerDb.copyWith(since: since))); } @Freezed(fromJson: true, toJson: true) @@ -109,13 +101,12 @@ class OpeningExplorerPrefs with _$OpeningExplorerPrefs implements Serializable { required PlayerDb playerDb, }) = _OpeningExplorerPrefs; - factory OpeningExplorerPrefs.defaults({LightUser? user}) => - OpeningExplorerPrefs( - db: OpeningDatabase.master, - masterDb: MasterDb.defaults, - lichessDb: LichessDb.defaults, - playerDb: PlayerDb.defaults(user: user), - ); + factory OpeningExplorerPrefs.defaults({LightUser? user}) => OpeningExplorerPrefs( + db: OpeningDatabase.master, + masterDb: MasterDb.defaults, + lichessDb: LichessDb.defaults, + playerDb: PlayerDb.defaults(user: user), + ); factory OpeningExplorerPrefs.fromJson(Map json) { return _$OpeningExplorerPrefsFromJson(json); @@ -126,9 +117,7 @@ class OpeningExplorerPrefs with _$OpeningExplorerPrefs implements Serializable { class MasterDb with _$MasterDb { const MasterDb._(); - const factory MasterDb({ - required int sinceYear, - }) = _MasterDb; + const factory MasterDb({required int sinceYear}) = _MasterDb; static const kEarliestYear = 1952; static final now = DateTime.now(); @@ -163,17 +152,7 @@ class LichessDb with _$LichessDb { Speed.classical, Speed.correspondence, }); - static const kAvailableRatings = ISetConst({ - 400, - 1000, - 1200, - 1400, - 1600, - 1800, - 2000, - 2200, - 2500, - }); + static const kAvailableRatings = ISetConst({400, 1000, 1200, 1400, 1600, 1800, 2000, 2200, 2500}); static final earliestDate = DateTime.utc(2012, 12); static final now = DateTime.now(); static const kDaysInAYear = 365; @@ -223,12 +202,12 @@ class PlayerDb with _$PlayerDb { 'All time': earliestDate, }; factory PlayerDb.defaults({LightUser? user}) => PlayerDb( - username: user?.name, - side: Side.white, - speeds: kAvailableSpeeds, - gameModes: GameMode.values.toISet(), - since: earliestDate, - ); + username: user?.name, + side: Side.white, + speeds: kAvailableSpeeds, + gameModes: GameMode.values.toISet(), + since: earliestDate, + ); factory PlayerDb.fromJson(Map json) { return _$PlayerDbFromJson(json); diff --git a/lib/src/model/opening_explorer/opening_explorer_repository.dart b/lib/src/model/opening_explorer/opening_explorer_repository.dart index a6b0921fc5..3a78c779ac 100644 --- a/lib/src/model/opening_explorer/opening_explorer_repository.dart +++ b/lib/src/model/opening_explorer/opening_explorer_repository.dart @@ -18,9 +18,7 @@ class OpeningExplorer extends _$OpeningExplorer { StreamSubscription? _openingExplorerSubscription; @override - Future<({OpeningExplorerEntry entry, bool isIndexing})?> build({ - required String fen, - }) async { + Future<({OpeningExplorerEntry entry, bool isIndexing})?> build({required String fen}) async { await ref.debounce(const Duration(milliseconds: 300)); ref.onDispose(() { _openingExplorerSubscription?.cancel(); @@ -30,15 +28,12 @@ class OpeningExplorer extends _$OpeningExplorer { final client = ref.read(defaultClientProvider); switch (prefs.db) { case OpeningDatabase.master: - final openingExplorer = - await OpeningExplorerRepository(client).getMasterDatabase( - fen, - since: prefs.masterDb.sinceYear, - ); + final openingExplorer = await OpeningExplorerRepository( + client, + ).getMasterDatabase(fen, since: prefs.masterDb.sinceYear); return (entry: openingExplorer, isIndexing: false); case OpeningDatabase.lichess: - final openingExplorer = - await OpeningExplorerRepository(client).getLichessDatabase( + final openingExplorer = await OpeningExplorerRepository(client).getLichessDatabase( fen, speeds: prefs.lichessDb.speeds, ratings: prefs.lichessDb.ratings, @@ -46,8 +41,7 @@ class OpeningExplorer extends _$OpeningExplorer { ); return (entry: openingExplorer, isIndexing: false); case OpeningDatabase.player: - final openingExplorerStream = - await OpeningExplorerRepository(client).getPlayerDatabase( + final openingExplorerStream = await OpeningExplorerRepository(client).getPlayerDatabase( fen, // null check handled by widget usernameOrId: prefs.playerDb.username!, @@ -58,16 +52,15 @@ class OpeningExplorer extends _$OpeningExplorer { ); _openingExplorerSubscription = openingExplorerStream.listen( - (openingExplorer) => state = - AsyncValue.data((entry: openingExplorer, isIndexing: true)), - onDone: () => state.value != null - ? state = AsyncValue.data( - (entry: state.value!.entry, isIndexing: false), - ) - : state = AsyncValue.error( - 'No opening explorer data returned for player ${prefs.playerDb.username}', - StackTrace.current, - ), + (openingExplorer) => state = AsyncValue.data((entry: openingExplorer, isIndexing: true)), + onDone: + () => + state.value != null + ? state = AsyncValue.data((entry: state.value!.entry, isIndexing: false)) + : state = AsyncValue.error( + 'No opening explorer data returned for player ${prefs.playerDb.username}', + StackTrace.current, + ), ); return null; } @@ -79,19 +72,12 @@ class OpeningExplorerRepository { final Client client; - Future getMasterDatabase( - String fen, { - int? since, - }) { + Future getMasterDatabase(String fen, {int? since}) { return client.readJson( - Uri.https( - kLichessOpeningExplorerHost, - '/masters', - { - 'fen': fen, - if (since != null) 'since': since.toString(), - }, - ), + Uri.https(kLichessOpeningExplorerHost, '/masters', { + 'fen': fen, + if (since != null) 'since': since.toString(), + }), mapper: OpeningExplorerEntry.fromJson, ); } @@ -103,17 +89,12 @@ class OpeningExplorerRepository { DateTime? since, }) { return client.readJson( - Uri.https( - kLichessOpeningExplorerHost, - '/lichess', - { - 'fen': fen, - if (speeds.isNotEmpty) - 'speeds': speeds.map((speed) => speed.name).join(','), - if (ratings.isNotEmpty) 'ratings': ratings.join(','), - if (since != null) 'since': '${since.year}-${since.month}', - }, - ), + Uri.https(kLichessOpeningExplorerHost, '/lichess', { + 'fen': fen, + if (speeds.isNotEmpty) 'speeds': speeds.map((speed) => speed.name).join(','), + if (ratings.isNotEmpty) 'ratings': ratings.join(','), + if (since != null) 'since': '${since.year}-${since.month}', + }), mapper: OpeningExplorerEntry.fromJson, ); } @@ -127,20 +108,14 @@ class OpeningExplorerRepository { DateTime? since, }) { return client.readNdJsonStream( - Uri.https( - kLichessOpeningExplorerHost, - '/player', - { - 'fen': fen, - 'player': usernameOrId, - 'color': color.name, - if (speeds.isNotEmpty) - 'speeds': speeds.map((speed) => speed.name).join(','), - if (gameModes.isNotEmpty) - 'modes': gameModes.map((gameMode) => gameMode.name).join(','), - if (since != null) 'since': '${since.year}-${since.month}', - }, - ), + Uri.https(kLichessOpeningExplorerHost, '/player', { + 'fen': fen, + 'player': usernameOrId, + 'color': color.name, + if (speeds.isNotEmpty) 'speeds': speeds.map((speed) => speed.name).join(','), + if (gameModes.isNotEmpty) 'modes': gameModes.map((gameMode) => gameMode.name).join(','), + if (since != null) 'since': '${since.year}-${since.month}', + }), mapper: OpeningExplorerEntry.fromJson, ); } diff --git a/lib/src/model/over_the_board/over_the_board_clock.dart b/lib/src/model/over_the_board/over_the_board_clock.dart index 2821527d75..390aee8e86 100644 --- a/lib/src/model/over_the_board/over_the_board_clock.dart +++ b/lib/src/model/over_the_board/over_the_board_clock.dart @@ -19,8 +19,7 @@ class OverTheBoardClock extends _$OverTheBoardClock { OverTheBoardClockState build() { _updateTimer = Timer.periodic(const Duration(milliseconds: 100), (_) { if (_stopwatch.isRunning) { - final newTime = - state.timeLeft(state.activeClock!)! - _stopwatch.elapsed; + final newTime = state.timeLeft(state.activeClock!)! - _stopwatch.elapsed; if (state.activeClock == Side.white) { state = state.copyWith(whiteTimeLeft: newTime); @@ -29,9 +28,7 @@ class OverTheBoardClock extends _$OverTheBoardClock { } if (newTime <= Duration.zero) { - state = state.copyWith( - flagSide: state.activeClock, - ); + state = state.copyWith(flagSide: state.activeClock); } _stopwatch.reset(); @@ -43,10 +40,7 @@ class OverTheBoardClock extends _$OverTheBoardClock { }); return OverTheBoardClockState.fromTimeIncrement( - TimeIncrement( - const Duration(minutes: 5).inSeconds, - const Duration(seconds: 3).inSeconds, - ), + TimeIncrement(const Duration(minutes: 5).inSeconds, const Duration(seconds: 3).inSeconds), ); } @@ -64,8 +58,7 @@ class OverTheBoardClock extends _$OverTheBoardClock { void switchSide({required Side newSideToMove, required bool addIncrement}) { if (state.timeIncrement.isInfinite || state.flagSide != null) return; - final increment = - Duration(seconds: addIncrement ? state.timeIncrement.increment : 0); + final increment = Duration(seconds: addIncrement ? state.timeIncrement.increment : 0); if (newSideToMove == Side.black) { state = state.copyWith( whiteTimeLeft: state.whiteTimeLeft! + increment, @@ -88,9 +81,7 @@ class OverTheBoardClock extends _$OverTheBoardClock { void pause() { if (_stopwatch.isRunning) { - state = state.copyWith( - activeClock: null, - ); + state = state.copyWith(activeClock: null); _stopwatch.reset(); _stopwatch.stop(); } @@ -100,9 +91,7 @@ class OverTheBoardClock extends _$OverTheBoardClock { _stopwatch.reset(); _stopwatch.start(); - state = state.copyWith( - activeClock: newSideToMove, - ); + state = state.copyWith(activeClock: newSideToMove); } } @@ -118,12 +107,11 @@ class OverTheBoardClockState with _$OverTheBoardClockState { required Side? flagSide, }) = _OverTheBoardClockState; - factory OverTheBoardClockState.fromTimeIncrement( - TimeIncrement timeIncrement, - ) { - final initialTime = timeIncrement.isInfinite - ? null - : Duration(seconds: max(timeIncrement.time, timeIncrement.increment)); + factory OverTheBoardClockState.fromTimeIncrement(TimeIncrement timeIncrement) { + final initialTime = + timeIncrement.isInfinite + ? null + : Duration(seconds: max(timeIncrement.time, timeIncrement.increment)); return OverTheBoardClockState( timeIncrement: timeIncrement, @@ -136,6 +124,5 @@ class OverTheBoardClockState with _$OverTheBoardClockState { bool get active => activeClock != null || flagSide != null; - Duration? timeLeft(Side side) => - side == Side.white ? whiteTimeLeft : blackTimeLeft; + Duration? timeLeft(Side side) => side == Side.white ? whiteTimeLeft : blackTimeLeft; } diff --git a/lib/src/model/over_the_board/over_the_board_game_controller.dart b/lib/src/model/over_the_board/over_the_board_game_controller.dart index 8f7741059e..5f4e75c7ff 100644 --- a/lib/src/model/over_the_board/over_the_board_game_controller.dart +++ b/lib/src/model/over_the_board/over_the_board_game_controller.dart @@ -20,22 +20,16 @@ part 'over_the_board_game_controller.g.dart'; class OverTheBoardGameController extends _$OverTheBoardGameController { @override OverTheBoardGameState build() => OverTheBoardGameState.fromVariant( - Variant.standard, - Speed.fromTimeIncrement(const TimeIncrement(0, 0)), - ); + Variant.standard, + Speed.fromTimeIncrement(const TimeIncrement(0, 0)), + ); void startNewGame(Variant variant, TimeIncrement timeIncrement) { - state = OverTheBoardGameState.fromVariant( - variant, - Speed.fromTimeIncrement(timeIncrement), - ); + state = OverTheBoardGameState.fromVariant(variant, Speed.fromTimeIncrement(timeIncrement)); } void rematch() { - state = OverTheBoardGameState.fromVariant( - state.game.meta.variant, - state.game.meta.speed, - ); + state = OverTheBoardGameState.fromVariant(state.game.meta.variant, state.game.meta.speed); } void makeMove(NormalMove move) { @@ -44,8 +38,7 @@ class OverTheBoardGameController extends _$OverTheBoardGameController { return; } - final (newPos, newSan) = - state.currentPosition.makeSan(Move.parse(move.uci)!); + final (newPos, newSan) = state.currentPosition.makeSan(Move.parse(move.uci)!); final sanMove = SanMove(newSan, move); final newStep = GameStep( position: newPos, @@ -67,17 +60,10 @@ class OverTheBoardGameController extends _$OverTheBoardGameController { if (state.currentPosition.isCheckmate) { state = state.copyWith( - game: state.game.copyWith( - status: GameStatus.mate, - winner: state.turn.opposite, - ), + game: state.game.copyWith(status: GameStatus.mate, winner: state.turn.opposite), ); } else if (state.currentPosition.isStalemate) { - state = state.copyWith( - game: state.game.copyWith( - status: GameStatus.stalemate, - ), - ); + state = state.copyWith(game: state.game.copyWith(status: GameStatus.stalemate)); } _moveFeedback(sanMove); @@ -98,10 +84,7 @@ class OverTheBoardGameController extends _$OverTheBoardGameController { void onFlag(Side side) { state = state.copyWith( - game: state.game.copyWith( - status: GameStatus.outoftime, - winner: side.opposite, - ), + game: state.game.copyWith(status: GameStatus.outoftime, winner: side.opposite), ); } @@ -113,9 +96,7 @@ class OverTheBoardGameController extends _$OverTheBoardGameController { void goBack() { if (state.canGoBack) { - state = state.copyWith( - stepCursor: state.stepCursor - 1, - ); + state = state.copyWith(stepCursor: state.stepCursor - 1); } } @@ -139,20 +120,12 @@ class OverTheBoardGameState with _$OverTheBoardGameState { @Default(null) NormalMove? promotionMove, }) = _OverTheBoardGameState; - factory OverTheBoardGameState.fromVariant( - Variant variant, - Speed speed, - ) { - final position = variant == Variant.chess960 - ? randomChess960Position() - : variant.initialPosition; + factory OverTheBoardGameState.fromVariant(Variant variant, Speed speed) { + final position = + variant == Variant.chess960 ? randomChess960Position() : variant.initialPosition; return OverTheBoardGameState( game: OverTheBoardGame( - steps: [ - GameStep( - position: position, - ), - ].lock, + steps: [GameStep(position: position)].lock, status: GameStatus.started, initialFen: position.fen, meta: GameMeta( @@ -169,21 +142,17 @@ class OverTheBoardGameState with _$OverTheBoardGameState { Position get currentPosition => game.stepAt(stepCursor).position; Side get turn => currentPosition.turn; bool get finished => game.finished; - NormalMove? get lastMove => stepCursor > 0 - ? NormalMove.fromUci(game.steps[stepCursor].sanMove!.move.uci) - : null; + NormalMove? get lastMove => + stepCursor > 0 ? NormalMove.fromUci(game.steps[stepCursor].sanMove!.move.uci) : null; - IMap> get legalMoves => makeLegalMoves( - currentPosition, - isChess960: game.meta.variant == Variant.chess960, - ); + IMap> get legalMoves => + makeLegalMoves(currentPosition, isChess960: game.meta.variant == Variant.chess960); MaterialDiffSide? currentMaterialDiff(Side side) { return game.steps[stepCursor].diff?.bySide(side); } - List get moves => - game.steps.skip(1).map((e) => e.sanMove!.san).toList(growable: false); + List get moves => game.steps.skip(1).map((e) => e.sanMove!.san).toList(growable: false); bool get canGoForward => stepCursor < game.steps.length - 1; bool get canGoBack => stepCursor > 0; diff --git a/lib/src/model/puzzle/puzzle.dart b/lib/src/model/puzzle/puzzle.dart index d62db26e5b..8b8562e0fa 100644 --- a/lib/src/model/puzzle/puzzle.dart +++ b/lib/src/model/puzzle/puzzle.dart @@ -32,8 +32,7 @@ class Puzzle with _$Puzzle { if (isCheckmate) { return true; } - if (uci != solutionUci && - (!altCastles.containsKey(uci) || altCastles[uci] != solutionUci)) { + if (uci != solutionUci && (!altCastles.containsKey(uci) || altCastles[uci] != solutionUci)) { return false; } } @@ -56,8 +55,7 @@ class PuzzleData with _$PuzzleData { Side get sideToMove => initialPly.isEven ? Side.black : Side.white; - factory PuzzleData.fromJson(Map json) => - _$PuzzleDataFromJson(json); + factory PuzzleData.fromJson(Map json) => _$PuzzleDataFromJson(json); } @Freezed(fromJson: true, toJson: true) @@ -70,17 +68,13 @@ class PuzzleGlicko with _$PuzzleGlicko { bool? provisional, }) = _PuzzleGlicko; - factory PuzzleGlicko.fromJson(Map json) => - _$PuzzleGlickoFromJson(json); + factory PuzzleGlicko.fromJson(Map json) => _$PuzzleGlickoFromJson(json); } @freezed class PuzzleRound with _$PuzzleRound { - const factory PuzzleRound({ - required PuzzleId id, - required int ratingDiff, - required bool win, - }) = _PuzzleRound; + const factory PuzzleRound({required PuzzleId id, required int ratingDiff, required bool win}) = + _PuzzleRound; } @Freezed(fromJson: true, toJson: true) @@ -94,32 +88,23 @@ class PuzzleGame with _$PuzzleGame { required String pgn, }) = _PuzzleGame; - factory PuzzleGame.fromJson(Map json) => - _$PuzzleGameFromJson(json); + factory PuzzleGame.fromJson(Map json) => _$PuzzleGameFromJson(json); } @Freezed(fromJson: true, toJson: true) class PuzzleGamePlayer with _$PuzzleGamePlayer { - const factory PuzzleGamePlayer({ - required Side side, - required String name, - String? title, - }) = _PuzzleGamePlayer; - - factory PuzzleGamePlayer.fromJson(Map json) => - _$PuzzleGamePlayerFromJson(json); + const factory PuzzleGamePlayer({required Side side, required String name, String? title}) = + _PuzzleGamePlayer; + + factory PuzzleGamePlayer.fromJson(Map json) => _$PuzzleGamePlayerFromJson(json); } @Freezed(fromJson: true, toJson: true) class PuzzleSolution with _$PuzzleSolution { - const factory PuzzleSolution({ - required PuzzleId id, - required bool win, - required bool rated, - }) = _PuzzleSolution; + const factory PuzzleSolution({required PuzzleId id, required bool win, required bool rated}) = + _PuzzleSolution; - factory PuzzleSolution.fromJson(Map json) => - _$PuzzleSolutionFromJson(json); + factory PuzzleSolution.fromJson(Map json) => _$PuzzleSolutionFromJson(json); } @freezed @@ -152,8 +137,7 @@ class LitePuzzle with _$LitePuzzle { required int rating, }) = _LitePuzzle; - factory LitePuzzle.fromJson(Map json) => - _$LitePuzzleFromJson(json); + factory LitePuzzle.fromJson(Map json) => _$LitePuzzleFromJson(json); (Side, String, Move) get preview { final pos1 = Chess.fromSetup(Setup.parseFen(fen)); @@ -195,11 +179,7 @@ class PuzzleHistoryEntry with _$PuzzleHistoryEntry { Duration? solvingTime, }) = _PuzzleHistoryEntry; - factory PuzzleHistoryEntry.fromLitePuzzle( - LitePuzzle puzzle, - bool win, - Duration duration, - ) { + factory PuzzleHistoryEntry.fromLitePuzzle(LitePuzzle puzzle, bool win, Duration duration) { final (_, fen, move) = puzzle.preview; return PuzzleHistoryEntry( date: DateTime.now(), @@ -212,6 +192,5 @@ class PuzzleHistoryEntry with _$PuzzleHistoryEntry { ); } - (String, Side, Move) get preview => - (fen, Chess.fromSetup(Setup.parseFen(fen)).turn, lastMove); + (String, Side, Move) get preview => (fen, Chess.fromSetup(Setup.parseFen(fen)).turn, lastMove); } diff --git a/lib/src/model/puzzle/puzzle_activity.dart b/lib/src/model/puzzle/puzzle_activity.dart index 79b245488f..2e4d9a7e25 100644 --- a/lib/src/model/puzzle/puzzle_activity.dart +++ b/lib/src/model/puzzle/puzzle_activity.dart @@ -45,9 +45,7 @@ class PuzzleActivity extends _$PuzzleActivity { ); } - Map> _groupByDay( - Iterable list, - ) { + Map> _groupByDay(Iterable list) { final map = >{}; for (final entry in list) { final date = DateTime(entry.date.year, entry.date.month, entry.date.day); @@ -68,15 +66,12 @@ class PuzzleActivity extends _$PuzzleActivity { state = AsyncData(currentVal.copyWith(isLoading: true)); Result.capture( ref.withClient( - (client) => PuzzleRepository(client) - .puzzleActivity(_nbPerPage, before: _list.last.date), + (client) => PuzzleRepository(client).puzzleActivity(_nbPerPage, before: _list.last.date), ), ).fold( (value) { if (value.isEmpty) { - state = AsyncData( - currentVal.copyWith(hasMore: false, isLoading: false), - ); + state = AsyncData(currentVal.copyWith(hasMore: false, isLoading: false)); return; } _list.addAll(value); @@ -90,8 +85,7 @@ class PuzzleActivity extends _$PuzzleActivity { ); }, (error, stackTrace) { - state = - AsyncData(currentVal.copyWith(isLoading: false, hasError: true)); + state = AsyncData(currentVal.copyWith(isLoading: false, hasError: true)); }, ); } diff --git a/lib/src/model/puzzle/puzzle_batch_storage.dart b/lib/src/model/puzzle/puzzle_batch_storage.dart index b35969535a..15eb38c3b1 100644 --- a/lib/src/model/puzzle/puzzle_batch_storage.dart +++ b/lib/src/model/puzzle/puzzle_batch_storage.dart @@ -42,10 +42,7 @@ class PuzzleBatchStorage { userId = ? AND angle = ? ''', - whereArgs: [ - userId ?? _anonUserKey, - angle.key, - ], + whereArgs: [userId ?? _anonUserKey, angle.key], ); final raw = list.firstOrNull?['data'] as String?; @@ -67,15 +64,11 @@ class PuzzleBatchStorage { required PuzzleBatch data, PuzzleAngle angle = const PuzzleTheme(PuzzleThemeKey.mix), }) async { - await _db.insert( - _tableName, - { - 'userId': userId ?? _anonUserKey, - 'angle': angle.key, - 'data': jsonEncode(data.toJson()), - }, - conflictAlgorithm: ConflictAlgorithm.replace, - ); + await _db.insert(_tableName, { + 'userId': userId ?? _anonUserKey, + 'angle': angle.key, + 'data': jsonEncode(data.toJson()), + }, conflictAlgorithm: ConflictAlgorithm.replace); _ref.invalidateSelf(); } @@ -89,24 +82,17 @@ class PuzzleBatchStorage { userId = ? AND angle = ? ''', - whereArgs: [ - userId ?? _anonUserKey, - angle.key, - ], + whereArgs: [userId ?? _anonUserKey, angle.key], ); _ref.invalidateSelf(); } /// Fetches all saved puzzles batches (except mix) for the given user. - Future> fetchAll({ - required UserId? userId, - }) async { + Future> fetchAll({required UserId? userId}) async { final list = await _db.query( _tableName, where: 'userId = ?', - whereArgs: [ - userId ?? _anonUserKey, - ], + whereArgs: [userId ?? _anonUserKey], orderBy: 'lastModified DESC', ); return list @@ -134,87 +120,74 @@ class PuzzleBatchStorage { .toIList(); } - Future> fetchSavedThemes({ - required UserId? userId, - }) async { + Future> fetchSavedThemes({required UserId? userId}) async { final list = await _db.query( _tableName, where: 'userId = ?', - whereArgs: [ - userId ?? _anonUserKey, - ], + whereArgs: [userId ?? _anonUserKey], ); - return list.fold>( - IMap(const {}), - (acc, map) { - final angle = map['angle'] as String?; - final raw = map['data'] as String?; - - final theme = angle != null ? puzzleThemeNameMap.get(angle) : null; - - if (theme != null) { - int? count; - if (raw != null) { - final json = jsonDecode(raw); - if (json is! Map) { - throw const FormatException( - '[PuzzleBatchStorage] cannot fetch puzzles: expected an object', - ); - } - final data = PuzzleBatch.fromJson(json); - count = data.unsolved.length; + return list.fold>(IMap(const {}), (acc, map) { + final angle = map['angle'] as String?; + final raw = map['data'] as String?; + + final theme = angle != null ? puzzleThemeNameMap.get(angle) : null; + + if (theme != null) { + int? count; + if (raw != null) { + final json = jsonDecode(raw); + if (json is! Map) { + throw const FormatException( + '[PuzzleBatchStorage] cannot fetch puzzles: expected an object', + ); } - return count != null ? acc.add(theme, count) : acc; + final data = PuzzleBatch.fromJson(json); + count = data.unsolved.length; } + return count != null ? acc.add(theme, count) : acc; + } - return acc; - }, - ); + return acc; + }); } - Future> fetchSavedOpenings({ - required UserId? userId, - }) async { + Future> fetchSavedOpenings({required UserId? userId}) async { final list = await _db.query( _tableName, where: 'userId = ?', - whereArgs: [ - userId ?? _anonUserKey, - ], + whereArgs: [userId ?? _anonUserKey], ); - return list.fold>( - IMap(const {}), - (acc, map) { - final angle = map['angle'] as String?; - final raw = map['data'] as String?; + return list.fold>(IMap(const {}), (acc, map) { + final angle = map['angle'] as String?; + final raw = map['data'] as String?; - final openingKey = angle != null - ? switch (PuzzleAngle.fromKey(angle)) { + final openingKey = + angle != null + ? switch (PuzzleAngle.fromKey(angle)) { PuzzleTheme(themeKey: _) => null, PuzzleOpening(key: final key) => key, } - : null; - - if (openingKey != null) { - int? count; - if (raw != null) { - final json = jsonDecode(raw); - if (json is! Map) { - throw const FormatException( - '[PuzzleBatchStorage] cannot fetch puzzles: expected an object', - ); - } - final data = PuzzleBatch.fromJson(json); - count = data.unsolved.length; + : null; + + if (openingKey != null) { + int? count; + if (raw != null) { + final json = jsonDecode(raw); + if (json is! Map) { + throw const FormatException( + '[PuzzleBatchStorage] cannot fetch puzzles: expected an object', + ); } - return count != null ? acc.add(openingKey, count) : acc; + final data = PuzzleBatch.fromJson(json); + count = data.unsolved.length; } + return count != null ? acc.add(openingKey, count) : acc; + } - return acc; - }, - ); + return acc; + }); } } @@ -225,6 +198,5 @@ class PuzzleBatch with _$PuzzleBatch { required IList unsolved, }) = _PuzzleBatch; - factory PuzzleBatch.fromJson(Map json) => - _$PuzzleBatchFromJson(json); + factory PuzzleBatch.fromJson(Map json) => _$PuzzleBatchFromJson(json); } diff --git a/lib/src/model/puzzle/puzzle_controller.dart b/lib/src/model/puzzle/puzzle_controller.dart index b590cea4ee..0353e41c9a 100644 --- a/lib/src/model/puzzle/puzzle_controller.dart +++ b/lib/src/model/puzzle/puzzle_controller.dart @@ -42,14 +42,10 @@ class PuzzleController extends _$PuzzleController { final _engineEvalDebounce = Debouncer(const Duration(milliseconds: 100)); - Future get _service => ref.read(puzzleServiceFactoryProvider)( - queueLength: kPuzzleLocalQueueLength, - ); + Future get _service => + ref.read(puzzleServiceFactoryProvider)(queueLength: kPuzzleLocalQueueLength); @override - PuzzleState build( - PuzzleContext initialContext, { - PuzzleStreak? initialStreak, - }) { + PuzzleState build(PuzzleContext initialContext, {PuzzleStreak? initialStreak}) { final evaluationService = ref.read(evaluationServiceProvider); ref.onDispose(() { @@ -69,27 +65,19 @@ class PuzzleController extends _$PuzzleController { return _loadNewContext(initialContext, initialStreak); } - PuzzleRepository _repository(LichessClient client) => - PuzzleRepository(client); + PuzzleRepository _repository(LichessClient client) => PuzzleRepository(client); Future _updateUserRating() async { try { - final data = await ref.withClient( - (client) => _repository(client).selectBatch(nb: 0), - ); + final data = await ref.withClient((client) => _repository(client).selectBatch(nb: 0)); final glicko = data.glicko; if (glicko != null) { - state = state.copyWith( - glicko: glicko, - ); + state = state.copyWith(glicko: glicko); } } catch (_) {} } - PuzzleState _loadNewContext( - PuzzleContext context, - PuzzleStreak? streak, - ) { + PuzzleState _loadNewContext(PuzzleContext context, PuzzleStreak? streak) { final root = Root.fromPgnMoves(context.puzzle.game.pgn); _gameTree = root.nodeAt(root.mainlinePath.penultimate) as Branch; @@ -100,9 +88,7 @@ class PuzzleController extends _$PuzzleController { // enable solution button after 4 seconds _enableSolutionButtonTimer = Timer(const Duration(seconds: 4), () { - state = state.copyWith( - canViewSolution: true, - ); + state = state.copyWith(canViewSolution: true); }); final initialPath = UciPath.fromId(_gameTree.children.first.id); @@ -120,9 +106,7 @@ class PuzzleController extends _$PuzzleController { initialPath: initialPath, currentPath: UciPath.empty, node: _gameTree.view, - pov: _gameTree.nodeAt(initialPath).position.ply.isEven - ? Side.white - : Side.black, + pov: _gameTree.nodeAt(initialPath).position.ply.isEven ? Side.white : Side.black, canViewSolution: false, resultSent: false, isChangingDifficulty: false, @@ -144,19 +128,15 @@ class PuzzleController extends _$PuzzleController { if (state.mode == PuzzleMode.play) { final nodeList = _gameTree.branchesOn(state.currentPath).toList(); - final movesToTest = - nodeList.sublist(state.initialPath.size).map((e) => e.sanMove); + final movesToTest = nodeList.sublist(state.initialPath.size).map((e) => e.sanMove); final isGoodMove = state.puzzle.testSolution(movesToTest); if (isGoodMove) { - state = state.copyWith( - feedback: PuzzleFeedback.good, - ); + state = state.copyWith(feedback: PuzzleFeedback.good); final isCheckmate = movesToTest.last.san.endsWith('#'); - final nextUci = - state.puzzle.puzzle.solution.getOrNull(movesToTest.length); + final nextUci = state.puzzle.puzzle.solution.getOrNull(movesToTest.length); // checkmate is always a win if (isCheckmate) { _completePuzzle(); @@ -171,9 +151,7 @@ class PuzzleController extends _$PuzzleController { _completePuzzle(); } } else { - state = state.copyWith( - feedback: PuzzleFeedback.bad, - ); + state = state.copyWith(feedback: PuzzleFeedback.bad); _onFailOrWin(PuzzleResult.lose); if (initialStreak == null) { await Future.delayed(const Duration(milliseconds: 500)); @@ -198,17 +176,13 @@ class PuzzleController extends _$PuzzleController { void userNext() { _viewSolutionTimer?.cancel(); _goToNextNode(replaying: true); - state = state.copyWith( - viewedSolutionRecently: false, - ); + state = state.copyWith(viewedSolutionRecently: false); } void userPrevious() { _viewSolutionTimer?.cancel(); _goToPreviousNode(replaying: true); - state = state.copyWith( - viewedSolutionRecently: false, - ); + state = state.copyWith(viewedSolutionRecently: false); } void viewSolution() { @@ -216,15 +190,11 @@ class PuzzleController extends _$PuzzleController { _mergeSolution(); - state = state.copyWith( - node: _gameTree.branchAt(state.currentPath).view, - ); + state = state.copyWith(node: _gameTree.branchAt(state.currentPath).view); _onFailOrWin(PuzzleResult.lose); - state = state.copyWith( - mode: PuzzleMode.view, - ); + state = state.copyWith(mode: PuzzleMode.view); Timer(const Duration(milliseconds: 800), () { _goToNextNode(); @@ -248,22 +218,16 @@ class PuzzleController extends _$PuzzleController { } Future changeDifficulty(PuzzleDifficulty difficulty) async { - state = state.copyWith( - isChangingDifficulty: true, - ); + state = state.copyWith(isChangingDifficulty: true); - await ref - .read(puzzlePreferencesProvider.notifier) - .setDifficulty(difficulty); + await ref.read(puzzlePreferencesProvider.notifier).setDifficulty(difficulty); final nextPuzzle = (await _service).resetBatch( userId: initialContext.userId, angle: initialContext.angle, ); - state = state.copyWith( - isChangingDifficulty: false, - ); + state = state.copyWith(isChangingDifficulty: false); return nextPuzzle; } @@ -275,9 +239,7 @@ class PuzzleController extends _$PuzzleController { } void saveStreakResultLocally() { - ref.read(streakStorageProvider(initialContext.userId)).saveActiveStreak( - state.streak!, - ); + ref.read(streakStorageProvider(initialContext.userId)).saveActiveStreak(state.streak!); } void _sendStreakResult() { @@ -286,42 +248,25 @@ class PuzzleController extends _$PuzzleController { if (initialContext.userId != null) { final streak = state.streak?.index; if (streak != null && streak > 0) { - ref.withClient( - (client) => _repository(client).postStreakRun(streak), - ); + ref.withClient((client) => _repository(client).postStreakRun(streak)); } } } - FutureResult retryFetchNextStreakPuzzle( - PuzzleStreak streak, - ) async { - state = state.copyWith( - nextPuzzleStreakFetchIsRetrying: true, - ); + FutureResult retryFetchNextStreakPuzzle(PuzzleStreak streak) async { + state = state.copyWith(nextPuzzleStreakFetchIsRetrying: true); final result = await _fetchNextStreakPuzzle(streak); - state = state.copyWith( - nextPuzzleStreakFetchIsRetrying: false, - ); + state = state.copyWith(nextPuzzleStreakFetchIsRetrying: false); result.match( onSuccess: (nextContext) { if (nextContext != null) { - state = state.copyWith( - streak: streak.copyWith( - index: streak.index + 1, - ), - ); + state = state.copyWith(streak: streak.copyWith(index: streak.index + 1)); } else { // no more puzzle - state = state.copyWith( - streak: streak.copyWith( - index: streak.index + 1, - finished: true, - ), - ); + state = state.copyWith(streak: streak.copyWith(index: streak.index + 1, finished: true)); } }, ); @@ -332,25 +277,24 @@ class PuzzleController extends _$PuzzleController { FutureResult _fetchNextStreakPuzzle(PuzzleStreak streak) { return streak.nextId != null ? Result.capture( - ref.withClient( - (client) => _repository(client).fetch(streak.nextId!).then( - (puzzle) => PuzzleContext( - angle: const PuzzleTheme(PuzzleThemeKey.mix), - puzzle: puzzle, - userId: initialContext.userId, - ), + ref.withClient( + (client) => _repository(client) + .fetch(streak.nextId!) + .then( + (puzzle) => PuzzleContext( + angle: const PuzzleTheme(PuzzleThemeKey.mix), + puzzle: puzzle, + userId: initialContext.userId, ), - ), - ) + ), + ), + ) : Future.value(Result.value(null)); } void _goToNextNode({bool replaying = false}) { if (state.node.children.isEmpty) return; - _setPath( - state.currentPath + state.node.children.first.id, - replaying: replaying, - ); + _setPath(state.currentPath + state.node.children.first.id, replaying: replaying); } void _goToPreviousNode({bool replaying = false}) { @@ -358,19 +302,14 @@ class PuzzleController extends _$PuzzleController { } Future _completePuzzle() async { - state = state.copyWith( - mode: PuzzleMode.view, - ); + state = state.copyWith(mode: PuzzleMode.view); await _onFailOrWin(state.result ?? PuzzleResult.win); } Future _onFailOrWin(PuzzleResult result) async { if (state.resultSent) return; - state = state.copyWith( - result: result, - resultSent: true, - ); + state = state.copyWith(result: result, resultSent: true); final soundService = ref.read(soundServiceProvider); @@ -386,27 +325,16 @@ class PuzzleController extends _$PuzzleController { ), ); - state = state.copyWith( - nextContext: next, - ); + state = state.copyWith(nextContext: next); ref - .read( - puzzleSessionProvider(initialContext.userId, initialContext.angle) - .notifier, - ) - .addAttempt( - state.puzzle.puzzle.id, - win: result == PuzzleResult.win, - ); + .read(puzzleSessionProvider(initialContext.userId, initialContext.angle).notifier) + .addAttempt(state.puzzle.puzzle.id, win: result == PuzzleResult.win); final rounds = next?.rounds; if (rounds != null) { ref - .read( - puzzleSessionProvider(initialContext.userId, initialContext.angle) - .notifier, - ) + .read(puzzleSessionProvider(initialContext.userId, initialContext.angle).notifier) .setRatingDiffs(rounds); } @@ -425,9 +353,7 @@ class PuzzleController extends _$PuzzleController { state = state.copyWith( mode: PuzzleMode.view, node: _gameTree.branchAt(state.currentPath).view, - streak: state.streak!.copyWith( - finished: true, - ), + streak: state.streak!.copyWith(finished: true), ); _sendStreakResult(); } else { @@ -442,20 +368,15 @@ class PuzzleController extends _$PuzzleController { soundService.play(Sound.confirmation); loadPuzzle( nextContext, - nextStreak: - state.streak!.copyWith(index: state.streak!.index + 1), + nextStreak: state.streak!.copyWith(index: state.streak!.index + 1), ); } else { // no more puzzle - state = state.copyWith.streak!( - finished: true, - ); + state = state.copyWith.streak!(finished: true); } }, onError: (error, _) { - state = state.copyWith( - nextPuzzleStreakFetchError: true, - ); + state = state.copyWith(nextPuzzleStreakFetchError: true); }, ); } @@ -463,11 +384,7 @@ class PuzzleController extends _$PuzzleController { } } - void _setPath( - UciPath path, { - bool replaying = false, - bool firstMove = false, - }) { + void _setPath(UciPath path, {bool replaying = false, bool firstMove = false}) { final pathChange = state.currentPath != path; final newNode = _gameTree.branchAt(path).view; final sanMove = newNode.sanMove; @@ -504,9 +421,7 @@ class PuzzleController extends _$PuzzleController { } void toggleLocalEvaluation() { - state = state.copyWith( - isLocalEvalEnabled: !state.isLocalEvalEnabled, - ); + state = state.copyWith(isLocalEvalEnabled: !state.isLocalEvalEnabled); if (state.isLocalEvalEnabled) { ref.read(evaluationServiceProvider).initEngine(state.evaluationContext); _startEngineEval(); @@ -518,8 +433,7 @@ class PuzzleController extends _$PuzzleController { String makePgn() { final initPosition = _gameTree.nodeAt(state.initialPath).position; var currentPosition = initPosition; - final pgnMoves = state.puzzle.puzzle.solution.fold>([], - (List acc, move) { + final pgnMoves = state.puzzle.puzzle.solution.fold>([], (List acc, move) { final moveObj = Move.parse(move); if (moveObj != null) { final String san; @@ -545,11 +459,11 @@ class PuzzleController extends _$PuzzleController { shouldEmit: (work) => work.path == state.currentPath, ) ?.forEach((t) { - final (work, eval) = t; - _gameTree.updateAt(work.path, (node) { - node.eval = eval; - }); - }), + final (work, eval) = t; + _gameTree.updateAt(work.path, (node) { + node.eval = eval; + }); + }), ); } @@ -572,15 +486,7 @@ class PuzzleController extends _$PuzzleController { final move = Move.parse(uci); final (pos, nodes) = previous; final (newPos, newSan) = pos.makeSan(move!); - return ( - newPos, - nodes.add( - Branch( - position: newPos, - sanMove: SanMove(newSan, move), - ), - ), - ); + return (newPos, nodes.add(Branch(position: newPos, sanMove: SanMove(newSan, move)))); }, ); _gameTree.addNodesAt(state.initialPath, newNodes, prepend: true); @@ -627,16 +533,13 @@ class PuzzleState with _$PuzzleState { return mode == PuzzleMode.view && isLocalEvalEnabled; } - EvaluationContext get evaluationContext => EvaluationContext( - variant: Variant.standard, - initialPosition: initialPosition, - ); + EvaluationContext get evaluationContext => + EvaluationContext(variant: Variant.standard, initialPosition: initialPosition); Position get position => node.position; String get fen => node.position.fen; bool get canGoNext => mode == PuzzleMode.view && node.children.isNotEmpty; - bool get canGoBack => - mode == PuzzleMode.view && currentPath.size > initialPath.size; + bool get canGoBack => mode == PuzzleMode.view && currentPath.size > initialPath.size; IMap> get validMoves => makeLegalMoves(position); } diff --git a/lib/src/model/puzzle/puzzle_difficulty.dart b/lib/src/model/puzzle/puzzle_difficulty.dart index bcda61e0f5..06012507e9 100644 --- a/lib/src/model/puzzle/puzzle_difficulty.dart +++ b/lib/src/model/puzzle/puzzle_difficulty.dart @@ -13,8 +13,9 @@ enum PuzzleDifficulty { const PuzzleDifficulty(this.ratingDelta); } -final IMap puzzleDifficultyNameMap = - IMap(PuzzleDifficulty.values.asNameMap()); +final IMap puzzleDifficultyNameMap = IMap( + PuzzleDifficulty.values.asNameMap(), +); String puzzleDifficultyL10n(BuildContext context, PuzzleDifficulty difficulty) { switch (difficulty) { diff --git a/lib/src/model/puzzle/puzzle_opening.dart b/lib/src/model/puzzle/puzzle_opening.dart index 51858a28a0..6c3a88cab1 100644 --- a/lib/src/model/puzzle/puzzle_opening.dart +++ b/lib/src/model/puzzle/puzzle_opening.dart @@ -5,18 +5,10 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'puzzle_opening.g.dart'; -typedef PuzzleOpeningFamily = ({ - String key, - String name, - int count, - IList openings, -}); +typedef PuzzleOpeningFamily = + ({String key, String name, int count, IList openings}); -typedef PuzzleOpeningData = ({ - String key, - String name, - int count, -}); +typedef PuzzleOpeningData = ({String key, String name, int count}); /// Returns a flattened list of openings with their respective counts. @riverpod @@ -26,13 +18,7 @@ Future> flatOpeningsList(Ref ref) async { .map( (f) => [ (key: f.key, name: f.name, count: f.count), - ...f.openings.map( - (o) => ( - key: o.key, - name: '${f.name}: ${o.name}', - count: o.count, - ), - ), + ...f.openings.map((o) => (key: o.key, name: '${f.name}: ${o.name}', count: o.count)), ], ) .expand((e) => e) diff --git a/lib/src/model/puzzle/puzzle_preferences.dart b/lib/src/model/puzzle/puzzle_preferences.dart index 25cf6be0b9..3f71177c44 100644 --- a/lib/src/model/puzzle/puzzle_preferences.dart +++ b/lib/src/model/puzzle/puzzle_preferences.dart @@ -9,8 +9,7 @@ part 'puzzle_preferences.freezed.dart'; part 'puzzle_preferences.g.dart'; @riverpod -class PuzzlePreferences extends _$PuzzlePreferences - with SessionPreferencesStorage { +class PuzzlePreferences extends _$PuzzlePreferences with SessionPreferencesStorage { // ignore: avoid_public_notifier_properties @override final prefCategory = PrefCategory.puzzle; @@ -47,12 +46,8 @@ class PuzzlePrefs with _$PuzzlePrefs implements Serializable { @Default(false) bool autoNext, }) = _PuzzlePrefs; - factory PuzzlePrefs.defaults({UserId? id}) => PuzzlePrefs( - id: id, - difficulty: PuzzleDifficulty.normal, - autoNext: false, - ); + factory PuzzlePrefs.defaults({UserId? id}) => + PuzzlePrefs(id: id, difficulty: PuzzleDifficulty.normal, autoNext: false); - factory PuzzlePrefs.fromJson(Map json) => - _$PuzzlePrefsFromJson(json); + factory PuzzlePrefs.fromJson(Map json) => _$PuzzlePrefsFromJson(json); } diff --git a/lib/src/model/puzzle/puzzle_providers.dart b/lib/src/model/puzzle/puzzle_providers.dart index b742ffdf2a..7923986f87 100644 --- a/lib/src/model/puzzle/puzzle_providers.dart +++ b/lib/src/model/puzzle/puzzle_providers.dart @@ -31,16 +31,10 @@ Future nextPuzzle(Ref ref, PuzzleAngle angle) async { // be invalidated multiple times when the user scrolls the list) ref.cacheFor(const Duration(minutes: 1)); - return puzzleService.nextPuzzle( - userId: session?.user.id, - angle: angle, - ); + return puzzleService.nextPuzzle(userId: session?.user.id, angle: angle); } -typedef InitialStreak = ({ - PuzzleStreak streak, - Puzzle puzzle, -}); +typedef InitialStreak = ({PuzzleStreak streak, Puzzle puzzle}); /// Fetches the active streak from the local storage if available, otherwise fetches it from the server. @riverpod @@ -49,17 +43,12 @@ Future streak(Ref ref) async { final streakStorage = ref.watch(streakStorageProvider(session?.user.id)); final activeStreak = await streakStorage.loadActiveStreak(); if (activeStreak != null) { - final puzzle = await ref - .read(puzzleProvider(activeStreak.streak[activeStreak.index]).future); + final puzzle = await ref.read(puzzleProvider(activeStreak.streak[activeStreak.index]).future); - return ( - streak: activeStreak, - puzzle: puzzle, - ); + return (streak: activeStreak, puzzle: puzzle); } - final rsp = - await ref.withClient((client) => PuzzleRepository(client).streak()); + final rsp = await ref.withClient((client) => PuzzleRepository(client).streak()); return ( streak: PuzzleStreak( @@ -110,19 +99,14 @@ Future> savedThemeBatches(Ref ref) async { } @riverpod -Future> savedOpeningBatches( - Ref ref, -) async { +Future> savedOpeningBatches(Ref ref) async { final session = ref.watch(authSessionProvider); final storage = await ref.watch(puzzleBatchStorageProvider.future); return storage.fetchSavedOpenings(userId: session?.user.id); } @riverpod -Future puzzleDashboard( - Ref ref, - int days, -) async { +Future puzzleDashboard(Ref ref, int days) async { final session = ref.watch(authSessionProvider); if (session == null) return null; return ref.withClientCacheFor( @@ -143,9 +127,7 @@ Future?> puzzleRecentActivity(Ref ref) async { @riverpod Future stormDashboard(Ref ref, UserId id) async { - return ref.withClient( - (client) => PuzzleRepository(client).stormDashboard(id), - ); + return ref.withClient((client) => PuzzleRepository(client).stormDashboard(id)); } @riverpod diff --git a/lib/src/model/puzzle/puzzle_repository.dart b/lib/src/model/puzzle/puzzle_repository.dart index 19929d9d1a..ca334d58a4 100644 --- a/lib/src/model/puzzle/puzzle_repository.dart +++ b/lib/src/model/puzzle/puzzle_repository.dart @@ -36,10 +36,7 @@ class PuzzleRepository { return client.readJson( Uri( path: '/api/puzzle/batch/${angle.key}', - queryParameters: { - 'nb': nb.toString(), - 'difficulty': difficulty.name, - }, + queryParameters: {'nb': nb.toString(), 'difficulty': difficulty.name}, ), mapper: _decodeBatchResponse, ); @@ -54,32 +51,18 @@ class PuzzleRepository { return client.postReadJson( Uri( path: '/api/puzzle/batch/${angle.key}', - queryParameters: { - 'nb': nb.toString(), - 'difficulty': difficulty.name, - }, + queryParameters: {'nb': nb.toString(), 'difficulty': difficulty.name}, ), headers: {'Content-type': 'application/json'}, body: jsonEncode({ - 'solutions': solved - .map( - (e) => { - 'id': e.id, - 'win': e.win, - 'rated': e.rated, - }, - ) - .toList(), + 'solutions': solved.map((e) => {'id': e.id, 'win': e.win, 'rated': e.rated}).toList(), }), mapper: _decodeBatchResponse, ); } Future fetch(PuzzleId id) { - return client.readJson( - Uri(path: '/api/puzzle/$id'), - mapper: _puzzleFromJson, - ); + return client.readJson(Uri(path: '/api/puzzle/$id'), mapper: _puzzleFromJson); } Future streak() { @@ -88,11 +71,7 @@ class PuzzleRepository { mapper: (Map json) { return PuzzleStreakResponse( puzzle: _puzzleFromPick(pick(json).required()), - streak: IList( - pick(json['streak']).asStringOrThrow().split(' ').map( - (e) => PuzzleId(e), - ), - ), + streak: IList(pick(json['streak']).asStringOrThrow().split(' ').map((e) => PuzzleId(e))), timestamp: DateTime.now(), ); }, @@ -103,10 +82,7 @@ class PuzzleRepository { final uri = Uri(path: '/api/streak/$run'); final response = await client.post(uri); if (response.statusCode >= 400) { - throw http.ClientException( - 'Failed to post streak run: ${response.statusCode}', - uri, - ); + throw http.ClientException('Failed to post streak run: ${response.statusCode}', uri); } } @@ -115,9 +91,7 @@ class PuzzleRepository { Uri(path: '/api/storm'), mapper: (Map json) { return PuzzleStormResponse( - puzzles: IList( - pick(json['puzzles']).asListOrThrow(_litePuzzleFromPick), - ), + puzzles: IList(pick(json['puzzles']).asListOrThrow(_litePuzzleFromPick)), highscore: pick(json['high']).letOrNull(_stormHighScoreFromPick), key: pick(json['key']).asStringOrNull(), timestamp: DateTime.now(), @@ -155,15 +129,8 @@ class PuzzleRepository { Future daily() { return client - .readJson( - Uri(path: '/api/puzzle/daily'), - mapper: _puzzleFromJson, - ) - .then( - (puzzle) => puzzle.copyWith( - isDailyPuzzle: true, - ), - ); + .readJson(Uri(path: '/api/puzzle/daily'), mapper: _puzzleFromJson) + .then((puzzle) => puzzle.copyWith(isDailyPuzzle: true)); } Future puzzleDashboard(int days) { @@ -173,17 +140,13 @@ class PuzzleRepository { ); } - Future> puzzleActivity( - int max, { - DateTime? before, - }) { + Future> puzzleActivity(int max, {DateTime? before}) { return client.readNdJsonList( Uri( path: '/api/puzzle/activity', queryParameters: { 'max': max.toString(), - if (before != null) - 'before': before.millisecondsSinceEpoch.toString(), + if (before != null) 'before': before.millisecondsSinceEpoch.toString(), }, ), mapper: _puzzleActivityFromJson, @@ -228,13 +191,9 @@ class PuzzleRepository { }), ), glicko: pick(json['glicko']).letOrNull(_puzzleGlickoFromPick), - rounds: pick(json['rounds']).letOrNull( - (p0) => IList( - p0.asListOrNull( - (p1) => _puzzleRoundFromPick(p1), - ), - ), - ), + rounds: pick( + json['rounds'], + ).letOrNull((p0) => IList(p0.asListOrNull((p1) => _puzzleRoundFromPick(p1)))), ); } } @@ -284,15 +243,12 @@ class PuzzleStormResponse with _$PuzzleStormResponse { PuzzleHistoryEntry _puzzleActivityFromJson(Map json) => _historyPuzzleFromPick(pick(json).required()); -Puzzle _puzzleFromJson(Map json) => - _puzzleFromPick(pick(json).required()); +Puzzle _puzzleFromJson(Map json) => _puzzleFromPick(pick(json).required()); PuzzleDashboard _puzzleDashboardFromJson(Map json) => _puzzleDashboardFromPick(pick(json).required()); -IMap _puzzleThemeFromJson( - Map json, -) => +IMap _puzzleThemeFromJson(Map json) => _puzzleThemeFromPick(pick(json).required()); IList _puzzleOpeningFromJson(Map json) => @@ -317,20 +273,17 @@ StormDashboard _stormDashboardFromPick(RequiredPick pick) { month: pick('high', 'month').asIntOrThrow(), week: pick('high', 'week').asIntOrThrow(), ), - dayHighscores: pick('days') - .asListOrThrow((p0) => _stormDayFromPick(p0, dateFormat)) - .toIList(), + dayHighscores: pick('days').asListOrThrow((p0) => _stormDayFromPick(p0, dateFormat)).toIList(), ); } -StormDayScore _stormDayFromPick(RequiredPick pick, DateFormat format) => - StormDayScore( - runs: pick('runs').asIntOrThrow(), - score: pick('score').asIntOrThrow(), - time: pick('time').asIntOrThrow(), - highest: pick('highest').asIntOrThrow(), - day: format.parse(pick('_id').asStringOrThrow()), - ); +StormDayScore _stormDayFromPick(RequiredPick pick, DateFormat format) => StormDayScore( + runs: pick('runs').asIntOrThrow(), + score: pick('score').asIntOrThrow(), + time: pick('time').asIntOrThrow(), + highest: pick('highest').asIntOrThrow(), + day: format.parse(pick('_id').asStringOrThrow()), +); LitePuzzle _litePuzzleFromPick(RequiredPick pick) { return LitePuzzle( @@ -357,8 +310,7 @@ PuzzleData _puzzleDatafromPick(RequiredPick pick) { plays: pick('plays').asIntOrThrow(), initialPly: pick('initialPly').asIntOrThrow(), solution: pick('solution').asListOrThrow((p0) => p0.asStringOrThrow()).lock, - themes: - pick('themes').asListOrThrow((p0) => p0.asStringOrThrow()).toSet().lock, + themes: pick('themes').asListOrThrow((p0) => p0.asStringOrThrow()).toSet().lock, ); } @@ -384,14 +336,10 @@ PuzzleGame _puzzleGameFromPick(RequiredPick pick) { perf: pick('perf', 'key').asPerfOrThrow(), rated: pick('rated').asBoolOrThrow(), white: pick('players').letOrThrow( - (it) => it - .asListOrThrow(_puzzlePlayerFromPick) - .firstWhere((p) => p.side == Side.white), + (it) => it.asListOrThrow(_puzzlePlayerFromPick).firstWhere((p) => p.side == Side.white), ), black: pick('players').letOrThrow( - (it) => it - .asListOrThrow(_puzzlePlayerFromPick) - .firstWhere((p) => p.side == Side.black), + (it) => it.asListOrThrow(_puzzlePlayerFromPick).firstWhere((p) => p.side == Side.black), ), pgn: pick('pgn').asStringOrThrow(), ); @@ -417,29 +365,24 @@ PuzzleHistoryEntry _historyPuzzleFromPick(RequiredPick pick) { } PuzzleDashboard _puzzleDashboardFromPick(RequiredPick pick) => PuzzleDashboard( - global: PuzzleDashboardData( - nb: pick('global')('nb').asIntOrThrow(), - firstWins: pick('global')('firstWins').asIntOrThrow(), - replayWins: pick('global')('replayWins').asIntOrThrow(), - performance: pick('global')('performance').asIntOrThrow(), - theme: PuzzleThemeKey.mix, - ), - themes: pick('themes') + global: PuzzleDashboardData( + nb: pick('global')('nb').asIntOrThrow(), + firstWins: pick('global')('firstWins').asIntOrThrow(), + replayWins: pick('global')('replayWins').asIntOrThrow(), + performance: pick('global')('performance').asIntOrThrow(), + theme: PuzzleThemeKey.mix, + ), + themes: + pick('themes') .asMapOrThrow>() .keys .map( - (key) => _puzzleDashboardDataFromPick( - pick('themes')(key)('results').required(), - key, - ), + (key) => _puzzleDashboardDataFromPick(pick('themes')(key)('results').required(), key), ) .toIList(), - ); +); -PuzzleDashboardData _puzzleDashboardDataFromPick( - RequiredPick results, - String themeKey, -) => +PuzzleDashboardData _puzzleDashboardDataFromPick(RequiredPick results, String themeKey) => PuzzleDashboardData( nb: results('nb').asIntOrThrow(), firstWins: results('firstWins').asIntOrThrow(), @@ -457,8 +400,7 @@ IMap _puzzleThemeFromPick(RequiredPick pick) { return PuzzleThemeData( count: listPick('count').asIntOrThrow(), desc: listPick('desc').asStringOrThrow(), - key: themeMap[listPick('key').asStringOrThrow()] ?? - PuzzleThemeKey.unsupported, + key: themeMap[listPick('key').asStringOrThrow()] ?? PuzzleThemeKey.unsupported, name: listPick('name').asStringOrThrow(), ); }) @@ -486,9 +428,7 @@ IList _puzzleOpeningFromPick(RequiredPick pick) { key: familyPick('key').asStringOrThrow(), name: familyPick('name').asStringOrThrow(), count: familyPick('count').asIntOrThrow(), - openings: openings != null - ? openings.toIList() - : IList(const []), + openings: openings != null ? openings.toIList() : IList(const []), ); }).toIList(); } diff --git a/lib/src/model/puzzle/puzzle_service.dart b/lib/src/model/puzzle/puzzle_service.dart index c5d7cb04bf..9dd9196076 100644 --- a/lib/src/model/puzzle/puzzle_service.dart +++ b/lib/src/model/puzzle/puzzle_service.dart @@ -26,9 +26,7 @@ const kPuzzleLocalQueueLength = 50; @Riverpod(keepAlive: true) Future puzzleService(Ref ref) { - return ref.read(puzzleServiceFactoryProvider)( - queueLength: kPuzzleLocalQueueLength, - ); + return ref.read(puzzleServiceFactoryProvider)(queueLength: kPuzzleLocalQueueLength); } @Riverpod(keepAlive: true) @@ -90,15 +88,16 @@ class PuzzleService { }) { return Result.release( _syncAndLoadData(userId, angle).map( - (data) => data.$1 != null && data.$1!.unsolved.isNotEmpty - ? PuzzleContext( - puzzle: data.$1!.unsolved[0], - angle: angle, - userId: userId, - glicko: data.$2, - rounds: data.$3, - ) - : null, + (data) => + data.$1 != null && data.$1!.unsolved.isNotEmpty + ? PuzzleContext( + puzzle: data.$1!.unsolved[0], + angle: angle, + userId: userId, + glicko: data.$2, + rounds: data.$3, + ) + : null, ), ); } @@ -114,18 +113,14 @@ class PuzzleService { PuzzleAngle angle = const PuzzleTheme(PuzzleThemeKey.mix), }) async { puzzleStorage.save(puzzle: puzzle); - final data = await batchStorage.fetch( - userId: userId, - angle: angle, - ); + final data = await batchStorage.fetch(userId: userId, angle: angle); if (data != null) { await batchStorage.save( userId: userId, angle: angle, data: PuzzleBatch( solved: IList([...data.solved, solution]), - unsolved: - data.unsolved.removeWhere((e) => e.puzzle.id == solution.id), + unsolved: data.unsolved.removeWhere((e) => e.puzzle.id == solution.id), ), ); } @@ -142,10 +137,7 @@ class PuzzleService { } /// Deletes the puzzle batch of [angle] from the local storage. - Future deleteBatch({ - required UserId? userId, - required PuzzleAngle angle, - }) async { + Future deleteBatch({required UserId? userId, required PuzzleAngle angle}) async { await batchStorage.delete(userId: userId, angle: angle); } @@ -157,15 +149,11 @@ class PuzzleService { /// /// This method should never fail, as if the network is down it will fallback /// to the local database. - FutureResult<(PuzzleBatch?, PuzzleGlicko?, IList?)> - _syncAndLoadData( + FutureResult<(PuzzleBatch?, PuzzleGlicko?, IList?)> _syncAndLoadData( UserId? userId, PuzzleAngle angle, ) async { - final data = await batchStorage.fetch( - userId: userId, - angle: angle, - ); + final data = await batchStorage.fetch(userId: userId, angle: angle); final unsolved = data?.unsolved ?? IList(const []); final solved = data?.solved ?? IList(const []); @@ -182,48 +170,37 @@ class PuzzleService { final batchResponse = _ref.withClient( (client) => Result.capture( solved.isNotEmpty && userId != null - ? PuzzleRepository(client).solveBatch( - nb: deficit, - solved: solved, - angle: angle, - difficulty: difficulty, - ) - : PuzzleRepository(client).selectBatch( - nb: deficit, - angle: angle, - difficulty: difficulty, - ), + ? PuzzleRepository( + client, + ).solveBatch(nb: deficit, solved: solved, angle: angle, difficulty: difficulty) + : PuzzleRepository( + client, + ).selectBatch(nb: deficit, angle: angle, difficulty: difficulty), ), ); return batchResponse .fold( - (value) => Result.value( - ( - PuzzleBatch( - solved: IList(const []), - unsolved: IList([...unsolved, ...value.puzzles]), - ), - value.glicko, - value.rounds, - true, // should save the batch - ), - ), - - // we don't need to save the batch if the request failed - (_, __) => Result.value((data, null, null, false)), - ) + (value) => Result.value(( + PuzzleBatch( + solved: IList(const []), + unsolved: IList([...unsolved, ...value.puzzles]), + ), + value.glicko, + value.rounds, + true, // should save the batch + )), + + // we don't need to save the batch if the request failed + (_, __) => Result.value((data, null, null, false)), + ) .flatMap((tuple) async { - final (newBatch, glicko, rounds, shouldSave) = tuple; - if (newBatch != null && shouldSave) { - await batchStorage.save( - userId: userId, - angle: angle, - data: newBatch, - ); - } - return Result.value((newBatch, glicko, rounds)); - }); + final (newBatch, glicko, rounds, shouldSave) = tuple; + if (newBatch != null && shouldSave) { + await batchStorage.save(userId: userId, angle: angle, data: newBatch); + } + return Result.value((newBatch, glicko, rounds)); + }); } return Result.value((data, null, null)); diff --git a/lib/src/model/puzzle/puzzle_session.dart b/lib/src/model/puzzle/puzzle_session.dart index 5e1aee26ea..572d302ffc 100644 --- a/lib/src/model/puzzle/puzzle_session.dart +++ b/lib/src/model/puzzle/puzzle_session.dart @@ -38,8 +38,7 @@ class PuzzleSession extends _$PuzzleSession { addIfNotFound: true, ); final newState = d.copyWith( - attempts: - newAttempts.length > maxSize ? newAttempts.sublist(1) : newAttempts, + attempts: newAttempts.length > maxSize ? newAttempts.sublist(1) : newAttempts, lastUpdatedAt: DateTime.now(), ); state = newState; @@ -50,19 +49,18 @@ class PuzzleSession extends _$PuzzleSession { Future setRatingDiffs(Iterable rounds) async { await _update((d) { final newState = d.copyWith( - attempts: d.attempts.map((a) { - final round = rounds.firstWhereOrNull((r) => r.id == a.id); - return round != null ? a.copyWith(ratingDiff: round.ratingDiff) : a; - }).toIList(), + attempts: + d.attempts.map((a) { + final round = rounds.firstWhereOrNull((r) => r.id == a.id); + return round != null ? a.copyWith(ratingDiff: round.ratingDiff) : a; + }).toIList(), ); state = newState; return newState; }); } - Future _update( - PuzzleSessionData Function(PuzzleSessionData d) update, - ) async { + Future _update(PuzzleSessionData Function(PuzzleSessionData d) update) async { await _store.setString(_storageKey, jsonEncode(update(state).toJson())); } @@ -71,13 +69,10 @@ class PuzzleSession extends _$PuzzleSession { if (stored == null) { return PuzzleSessionData.initial(angle: angle); } - return PuzzleSessionData.fromJson( - jsonDecode(stored) as Map, - ); + return PuzzleSessionData.fromJson(jsonDecode(stored) as Map); } - SharedPreferencesWithCache get _store => - LichessBinding.instance.sharedPreferences; + SharedPreferencesWithCache get _store => LichessBinding.instance.sharedPreferences; String get _storageKey => 'puzzle_session.${userId ?? '**anon**'}'; } @@ -90,9 +85,7 @@ class PuzzleSessionData with _$PuzzleSessionData { required DateTime lastUpdatedAt, }) = _PuzzleSession; - factory PuzzleSessionData.initial({ - required PuzzleAngle angle, - }) { + factory PuzzleSessionData.initial({required PuzzleAngle angle}) { return PuzzleSessionData( angle: angle, attempts: IList(const []), @@ -104,9 +97,7 @@ class PuzzleSessionData with _$PuzzleSessionData { try { return _$PuzzleSessionDataFromJson(json); } catch (e) { - return PuzzleSessionData.initial( - angle: const PuzzleTheme(PuzzleThemeKey.mix), - ); + return PuzzleSessionData.initial(angle: const PuzzleTheme(PuzzleThemeKey.mix)); } } } @@ -115,14 +106,10 @@ class PuzzleSessionData with _$PuzzleSessionData { class PuzzleAttempt with _$PuzzleAttempt { const PuzzleAttempt._(); - const factory PuzzleAttempt({ - required PuzzleId id, - required bool win, - int? ratingDiff, - }) = _PuzzleAttempt; + const factory PuzzleAttempt({required PuzzleId id, required bool win, int? ratingDiff}) = + _PuzzleAttempt; - factory PuzzleAttempt.fromJson(Map json) => - _$PuzzleAttemptFromJson(json); + factory PuzzleAttempt.fromJson(Map json) => _$PuzzleAttemptFromJson(json); String? get ratingDiffString { if (ratingDiff == null) return null; diff --git a/lib/src/model/puzzle/puzzle_storage.dart b/lib/src/model/puzzle/puzzle_storage.dart index acd3426f92..c11fc70ae0 100644 --- a/lib/src/model/puzzle/puzzle_storage.dart +++ b/lib/src/model/puzzle/puzzle_storage.dart @@ -22,9 +22,7 @@ class PuzzleStorage { const PuzzleStorage(this._db); final Database _db; - Future fetch({ - required PuzzleId puzzleId, - }) async { + Future fetch({required PuzzleId puzzleId}) async { final list = await _db.query( _tableName, where: 'puzzleId = ?', @@ -45,17 +43,11 @@ class PuzzleStorage { return null; } - Future save({ - required Puzzle puzzle, - }) async { - await _db.insert( - _tableName, - { - 'puzzleId': puzzle.puzzle.id.toString(), - 'lastModified': DateTime.now().toIso8601String(), - 'data': jsonEncode(puzzle.toJson()), - }, - conflictAlgorithm: ConflictAlgorithm.replace, - ); + Future save({required Puzzle puzzle}) async { + await _db.insert(_tableName, { + 'puzzleId': puzzle.puzzle.id.toString(), + 'lastModified': DateTime.now().toIso8601String(), + 'data': jsonEncode(puzzle.toJson()), + }, conflictAlgorithm: ConflictAlgorithm.replace); } } diff --git a/lib/src/model/puzzle/puzzle_streak.dart b/lib/src/model/puzzle/puzzle_streak.dart index c950bc9681..68a03913dc 100644 --- a/lib/src/model/puzzle/puzzle_streak.dart +++ b/lib/src/model/puzzle/puzzle_streak.dart @@ -21,6 +21,5 @@ class PuzzleStreak with _$PuzzleStreak { PuzzleId? get nextId => streak.getOrNull(index + 1); - factory PuzzleStreak.fromJson(Map json) => - _$PuzzleStreakFromJson(json); + factory PuzzleStreak.fromJson(Map json) => _$PuzzleStreakFromJson(json); } diff --git a/lib/src/model/puzzle/puzzle_theme.dart b/lib/src/model/puzzle/puzzle_theme.dart index 0ddf5236e3..0a223fbb01 100644 --- a/lib/src/model/puzzle/puzzle_theme.dart +++ b/lib/src/model/puzzle/puzzle_theme.dart @@ -402,8 +402,7 @@ enum PuzzleThemeKey { } } -final IMap puzzleThemeNameMap = - IMap(PuzzleThemeKey.values.asNameMap()); +final IMap puzzleThemeNameMap = IMap(PuzzleThemeKey.values.asNameMap()); typedef PuzzleThemeCategory = (String, List); @@ -412,12 +411,7 @@ IList puzzleThemeCategories(Ref ref) { final l10n = ref.watch(localizationsProvider); return IList([ - ( - l10n.strings.puzzleRecommended, - [ - PuzzleThemeKey.mix, - ], - ), + (l10n.strings.puzzleRecommended, [PuzzleThemeKey.mix]), ( l10n.strings.puzzlePhases, [ @@ -504,20 +498,11 @@ IList puzzleThemeCategories(Ref ref) { ), ( l10n.strings.puzzleLengths, - [ - PuzzleThemeKey.oneMove, - PuzzleThemeKey.short, - PuzzleThemeKey.long, - PuzzleThemeKey.veryLong, - ], + [PuzzleThemeKey.oneMove, PuzzleThemeKey.short, PuzzleThemeKey.long, PuzzleThemeKey.veryLong], ), ( l10n.strings.puzzleOrigin, - [ - PuzzleThemeKey.master, - PuzzleThemeKey.masterVsMaster, - PuzzleThemeKey.superGM, - ], + [PuzzleThemeKey.master, PuzzleThemeKey.masterVsMaster, PuzzleThemeKey.superGM], ), ]); } diff --git a/lib/src/model/puzzle/storm.dart b/lib/src/model/puzzle/storm.dart index 105a1c4093..949b8a9840 100644 --- a/lib/src/model/puzzle/storm.dart +++ b/lib/src/model/puzzle/storm.dart @@ -36,11 +36,12 @@ class StormRunStats with _$StormRunStats { IList historyFilter(StormFilter filter) { return history .where( - (e) => (filter.slow && filter.failed) - ? (!e.win && slowPuzzleIds.any((id) => id == e.id)) - : (filter.slow - ? slowPuzzleIds.any((id) => id == e.id) - : (!filter.failed || !e.win)), + (e) => + (filter.slow && filter.failed) + ? (!e.win && slowPuzzleIds.any((id) => id == e.id)) + : (filter.slow + ? slowPuzzleIds.any((id) => id == e.id) + : (!filter.failed || !e.win)), ) .toIList(); } @@ -56,12 +57,7 @@ class StormFilter { StormFilter(slow: slow ?? this.slow, failed: failed ?? this.failed); } -enum StormNewHighType { - day, - week, - month, - allTime, -} +enum StormNewHighType { day, week, month, allTime } @freezed class StormDashboard with _$StormDashboard { @@ -84,14 +80,12 @@ class StormDayScore with _$StormDayScore { @freezed class StormNewHigh with _$StormNewHigh { - const factory StormNewHigh({ - required StormNewHighType key, - required int prev, - }) = _StormNewHigh; + const factory StormNewHigh({required StormNewHighType key, required int prev}) = _StormNewHigh; } -final IMap stormNewHighTypeMap = - IMap(StormNewHighType.values.asNameMap()); +final IMap stormNewHighTypeMap = IMap( + StormNewHighType.values.asNameMap(), +); extension StormExtension on Pick { StormNewHighType asStormNewHighTypeOrThrow() { @@ -104,9 +98,7 @@ extension StormExtension on Pick { return stormNewHighTypeMap[value]!; } } - throw PickException( - "value $value at $debugParsingExit can't be casted to StormNewHighType", - ); + throw PickException("value $value at $debugParsingExit can't be casted to StormNewHighType"); } StormNewHighType? asStormNewHighTypeOrNull() { diff --git a/lib/src/model/puzzle/storm_controller.dart b/lib/src/model/puzzle/storm_controller.dart index 544c890db9..edd18949f8 100644 --- a/lib/src/model/puzzle/storm_controller.dart +++ b/lib/src/model/puzzle/storm_controller.dart @@ -105,12 +105,7 @@ class StormController extends _$StormController { } await Future.delayed(moveDelay); - _addMove( - state.expectedMove!, - ComboState.increase, - runStarted: true, - userMove: false, - ); + _addMove(state.expectedMove!, ComboState.increase, runStarted: true, userMove: false); } else { state = state.copyWith(errors: state.errors + 1); ref.read(soundServiceProvider).play(Sound.error); @@ -149,16 +144,11 @@ class StormController extends _$StormController { if (session != null) { final res = await ref.withClient( (client) => Result.capture( - PuzzleRepository(client) - .postStormRun(stats) - .timeout(const Duration(seconds: 2)), + PuzzleRepository(client).postStormRun(stats).timeout(const Duration(seconds: 2)), ), ); - final newState = state.copyWith( - stats: stats, - mode: StormMode.ended, - ); + final newState = state.copyWith(stats: stats, mode: StormMode.ended); res.match( onSuccess: (newHigh) { @@ -173,10 +163,7 @@ class StormController extends _$StormController { }, ); } else { - state = state.copyWith( - stats: stats, - mode: StormMode.ended, - ); + state = state.copyWith(stats: stats, mode: StormMode.ended); } } @@ -206,12 +193,7 @@ class StormController extends _$StormController { ), ); await Future.delayed(moveDelay); - _addMove( - state.expectedMove!, - ComboState.noChange, - runStarted: true, - userMove: false, - ); + _addMove(state.expectedMove!, ComboState.noChange, runStarted: true, userMove: false); } void _addMove( @@ -242,24 +224,18 @@ class StormController extends _$StormController { ), promotionMove: null, ); - Future.delayed( - userMove ? Duration.zero : const Duration(milliseconds: 250), () { + Future.delayed(userMove ? Duration.zero : const Duration(milliseconds: 250), () { if (pos.board.pieceAt(move.to) != null) { - ref - .read(moveFeedbackServiceProvider) - .captureFeedback(check: state.position.isCheck); + ref.read(moveFeedbackServiceProvider).captureFeedback(check: state.position.isCheck); } else { - ref - .read(moveFeedbackServiceProvider) - .moveFeedback(check: state.position.isCheck); + ref.read(moveFeedbackServiceProvider).moveFeedback(check: state.position.isCheck); } }); } StormRunStats _getStats() { final wins = state.history.where((e) => e.win == true).toList(); - final mean = state.history.sumBy((e) => e.solvingTime!.inSeconds) / - state.history.length; + final mean = state.history.sumBy((e) => e.solvingTime!.inSeconds) / state.history.length; final threshold = mean * 1.5; return StormRunStats( moves: state.moves, @@ -268,23 +244,26 @@ class StormController extends _$StormController { comboBest: state.combo.best, time: state.clock.endAt!, timePerMove: mean, - highest: wins.isNotEmpty - ? wins.map((e) => e.rating).reduce( - (maxRating, rating) => rating > maxRating ? rating : maxRating, - ) - : 0, + highest: + wins.isNotEmpty + ? wins + .map((e) => e.rating) + .reduce((maxRating, rating) => rating > maxRating ? rating : maxRating) + : 0, history: state.history, - slowPuzzleIds: state.history - .where((e) => e.solvingTime!.inSeconds > threshold) - .map((e) => e.id) - .toIList(), + slowPuzzleIds: + state.history + .where((e) => e.solvingTime!.inSeconds > threshold) + .map((e) => e.id) + .toIList(), ); } void _pushToHistory({required bool success}) { - final timeTaken = state.lastSolvedTime != null - ? DateTime.now().difference(state.lastSolvedTime!) - : DateTime.now().difference(state.clock.startAt!); + final timeTaken = + state.lastSolvedTime != null + ? DateTime.now().difference(state.lastSolvedTime!) + : DateTime.now().difference(state.clock.startAt!); state = state.copyWith( history: state.history.add( PuzzleHistoryEntry.fromLitePuzzle(state.puzzle, success, timeTaken), @@ -353,8 +332,7 @@ class StormState with _$StormState { Move? get expectedMove => Move.parse(puzzle.solution[moveIndex + 1]); - Move? get lastMove => - moveIndex == -1 ? null : Move.parse(puzzle.solution[moveIndex]); + Move? get lastMove => moveIndex == -1 ? null : Move.parse(puzzle.solution[moveIndex]); bool get isOver => moveIndex >= puzzle.solution.length - 1; @@ -370,10 +348,7 @@ enum ComboState { increase, reset, noChange } class StormCombo with _$StormCombo { const StormCombo._(); - const factory StormCombo({ - required int current, - required int best, - }) = _StormCombo; + const factory StormCombo({required int current, required int best}) = _StormCombo; /// List representing the bonus awared at each level static const levelBonus = [3, 5, 6, 10]; @@ -394,8 +369,7 @@ class StormCombo with _$StormCombo { /// Returns the level of the `current + 1` combo count int nextLevel() { - final lvl = - levelsAndBonus.indexWhere((element) => element.level > current + 1); + final lvl = levelsAndBonus.indexWhere((element) => element.level > current + 1); return lvl >= 0 ? lvl - 1 : levelsAndBonus.length - 1; } @@ -407,8 +381,7 @@ class StormCombo with _$StormCombo { final lvl = getNext ? nextLevel() : currentLevel(); final lastLevel = levelsAndBonus.last; if (lvl >= levelsAndBonus.length - 1) { - final range = - lastLevel.level - levelsAndBonus[levelsAndBonus.length - 2].level; + final range = lastLevel.level - levelsAndBonus[levelsAndBonus.length - 2].level; return (((currentCombo - lastLevel.level) / range) * 100) % 100; } final bounds = [levelsAndBonus[lvl].level, levelsAndBonus[lvl + 1].level]; diff --git a/lib/src/model/puzzle/streak_storage.dart b/lib/src/model/puzzle/streak_storage.dart index dbe0101a90..a28e6519a5 100644 --- a/lib/src/model/puzzle/streak_storage.dart +++ b/lib/src/model/puzzle/streak_storage.dart @@ -26,24 +26,18 @@ class StreakStorage { return null; } - return PuzzleStreak.fromJson( - jsonDecode(stored) as Map, - ); + return PuzzleStreak.fromJson(jsonDecode(stored) as Map); } Future saveActiveStreak(PuzzleStreak streak) async { - await _store.setString( - _storageKey, - jsonEncode(streak), - ); + await _store.setString(_storageKey, jsonEncode(streak)); } Future clearActiveStreak() async { await _store.remove(_storageKey); } - SharedPreferencesWithCache get _store => - LichessBinding.instance.sharedPreferences; + SharedPreferencesWithCache get _store => LichessBinding.instance.sharedPreferences; String get _storageKey => 'puzzle_streak.${userId ?? '**anon**'}'; } diff --git a/lib/src/model/relation/online_friends.dart b/lib/src/model/relation/online_friends.dart index 8f1f4a3980..be5f41498f 100644 --- a/lib/src/model/relation/online_friends.dart +++ b/lib/src/model/relation/online_friends.dart @@ -22,9 +22,7 @@ class OnlineFriends extends _$OnlineFriends { final state = _socketClient.stream .firstWhere((e) => e.topic == 'following_onlines') - .then( - (event) => _parseFriendsList(event.data as List), - ); + .then((event) => _parseFriendsList(event.data as List)); await _socketClient.firstConnection; @@ -70,9 +68,7 @@ class OnlineFriends extends _$OnlineFriends { switch (event.topic) { case 'following_onlines': - state = AsyncValue.data( - _parseFriendsList(event.data as List), - ); + state = AsyncValue.data(_parseFriendsList(event.data as List)); case 'following_enters': final data = _parseFriend(event.data.toString()); @@ -80,9 +76,7 @@ class OnlineFriends extends _$OnlineFriends { case 'following_leaves': final data = _parseFriend(event.data.toString()); - state = AsyncValue.data( - state.requireValue.removeWhere((v) => v.id == data.id), - ); + state = AsyncValue.data(state.requireValue.removeWhere((v) => v.id == data.id)); } } @@ -92,11 +86,7 @@ class OnlineFriends extends _$OnlineFriends { final splitted = friend.split(' '); final name = splitted.length > 1 ? splitted[1] : splitted[0]; final title = splitted.length > 1 ? splitted[0] : null; - return LightUser( - id: UserId.fromUserName(name), - name: name, - title: title, - ); + return LightUser(id: UserId.fromUserName(name), name: name, title: title); } IList _parseFriendsList(List friends) { diff --git a/lib/src/model/relation/relation_repository.dart b/lib/src/model/relation/relation_repository.dart index b82ee33325..218babb3d3 100644 --- a/lib/src/model/relation/relation_repository.dart +++ b/lib/src/model/relation/relation_repository.dart @@ -22,10 +22,7 @@ class RelationRepository { final response = await client.post(uri); if (response.statusCode >= 400) { - throw http.ClientException( - 'Failed to follow user: ${response.statusCode}', - uri, - ); + throw http.ClientException('Failed to follow user: ${response.statusCode}', uri); } } @@ -34,10 +31,7 @@ class RelationRepository { final response = await client.post(uri); if (response.statusCode >= 400) { - throw http.ClientException( - 'Failed to unfollow user: ${response.statusCode}', - uri, - ); + throw http.ClientException('Failed to unfollow user: ${response.statusCode}', uri); } } @@ -46,10 +40,7 @@ class RelationRepository { final response = await client.post(uri); if (response.statusCode >= 400) { - throw http.ClientException( - 'Failed to block user: ${response.statusCode}', - uri, - ); + throw http.ClientException('Failed to block user: ${response.statusCode}', uri); } } @@ -58,10 +49,7 @@ class RelationRepository { final response = await client.post(uri); if (response.statusCode >= 400) { - throw http.ClientException( - 'Failed to unblock user: ${response.statusCode}', - uri, - ); + throw http.ClientException('Failed to unblock user: ${response.statusCode}', uri); } } } diff --git a/lib/src/model/settings/board_preferences.dart b/lib/src/model/settings/board_preferences.dart index 1c4f87319e..65dd2b3f1b 100644 --- a/lib/src/model/settings/board_preferences.dart +++ b/lib/src/model/settings/board_preferences.dart @@ -12,8 +12,7 @@ part 'board_preferences.freezed.dart'; part 'board_preferences.g.dart'; @riverpod -class BoardPreferences extends _$BoardPreferences - with PreferencesStorage { +class BoardPreferences extends _$BoardPreferences with PreferencesStorage { // ignore: avoid_public_notifier_properties @override PrefCategory get prefCategory => PrefCategory.board; @@ -48,9 +47,7 @@ class BoardPreferences extends _$BoardPreferences Future toggleImmersiveModeWhilePlaying() { return save( - state.copyWith( - immersiveModeWhilePlaying: !(state.immersiveModeWhilePlaying ?? false), - ), + state.copyWith(immersiveModeWhilePlaying: !(state.immersiveModeWhilePlaying ?? false)), ); } @@ -75,35 +72,23 @@ class BoardPreferences extends _$BoardPreferences } Future toggleMagnifyDraggedPiece() { - return save( - state.copyWith( - magnifyDraggedPiece: !state.magnifyDraggedPiece, - ), - ); + return save(state.copyWith(magnifyDraggedPiece: !state.magnifyDraggedPiece)); } Future setDragTargetKind(DragTargetKind dragTargetKind) { return save(state.copyWith(dragTargetKind: dragTargetKind)); } - Future setMaterialDifferenceFormat( - MaterialDifferenceFormat materialDifferenceFormat, - ) { - return save( - state.copyWith(materialDifferenceFormat: materialDifferenceFormat), - ); + Future setMaterialDifferenceFormat(MaterialDifferenceFormat materialDifferenceFormat) { + return save(state.copyWith(materialDifferenceFormat: materialDifferenceFormat)); } Future setClockPosition(ClockPosition clockPosition) { - return save( - state.copyWith(clockPosition: clockPosition), - ); + return save(state.copyWith(clockPosition: clockPosition)); } Future toggleEnableShapeDrawings() { - return save( - state.copyWith(enableShapeDrawings: !state.enableShapeDrawings), - ); + return save(state.copyWith(enableShapeDrawings: !state.enableShapeDrawings)); } Future setShapeColor(ShapeColor shapeColor) { @@ -130,24 +115,15 @@ class BoardPrefs with _$BoardPrefs implements Serializable { ) required MaterialDifferenceFormat materialDifferenceFormat, required ClockPosition clockPosition, - @JsonKey( - defaultValue: PieceShiftMethod.either, - unknownEnumValue: PieceShiftMethod.either, - ) + @JsonKey(defaultValue: PieceShiftMethod.either, unknownEnumValue: PieceShiftMethod.either) required PieceShiftMethod pieceShiftMethod, /// Whether to enable shape drawings on the board for games and puzzles. @JsonKey(defaultValue: true) required bool enableShapeDrawings, @JsonKey(defaultValue: true) required bool magnifyDraggedPiece, - @JsonKey( - defaultValue: DragTargetKind.circle, - unknownEnumValue: DragTargetKind.circle, - ) + @JsonKey(defaultValue: DragTargetKind.circle, unknownEnumValue: DragTargetKind.circle) required DragTargetKind dragTargetKind, - @JsonKey( - defaultValue: ShapeColor.green, - unknownEnumValue: ShapeColor.green, - ) + @JsonKey(defaultValue: ShapeColor.green, unknownEnumValue: ShapeColor.green) required ShapeColor shapeColor, @JsonKey(defaultValue: false) required bool showBorder, }) = _BoardPrefs; @@ -175,12 +151,10 @@ class BoardPrefs with _$BoardPrefs implements Serializable { return ChessboardSettings( pieceAssets: pieceSet.assets, colorScheme: boardTheme.colors, - border: showBorder - ? BoardBorder( - color: darken(boardTheme.colors.darkSquare, 0.2), - width: 16.0, - ) - : null, + border: + showBorder + ? BoardBorder(color: darken(boardTheme.colors.darkSquare, 0.2), width: 16.0) + : null, showValidMoves: showLegalMoves, showLastMove: boardHighlights, enableCoordinates: coordinates, @@ -189,10 +163,7 @@ class BoardPrefs with _$BoardPrefs implements Serializable { dragFeedbackOffset: Offset(0.0, magnifyDraggedPiece ? -1.0 : 0.0), dragTargetKind: dragTargetKind, pieceShiftMethod: pieceShiftMethod, - drawShape: DrawShapeOptions( - enable: enableShapeDrawings, - newShapeColor: shapeColor.color, - ), + drawShape: DrawShapeOptions(enable: enableShapeDrawings, newShapeColor: shapeColor.color), ); } @@ -211,14 +182,12 @@ enum ShapeColor { blue, yellow; - Color get color => Color( - switch (this) { - ShapeColor.green => 0x15781B, - ShapeColor.red => 0x882020, - ShapeColor.blue => 0x003088, - ShapeColor.yellow => 0xe68f00, - }, - ).withAlpha(0xAA); + Color get color => Color(switch (this) { + ShapeColor.green => 0x15781B, + ShapeColor.red => 0x882020, + ShapeColor.blue => 0x003088, + ShapeColor.yellow => 0xe68f00, + }).withAlpha(0xAA); } /// The chessboard theme. @@ -306,27 +275,29 @@ enum BoardTheme { } } - Widget get thumbnail => this == BoardTheme.system - ? SizedBox( - height: 44, - width: 44 * 6, - child: Row( - children: [ - for (final c in const [1, 2, 3, 4, 5, 6]) - Container( - width: 44, - color: c.isEven - ? BoardTheme.system.colors.darkSquare - : BoardTheme.system.colors.lightSquare, - ), - ], - ), - ) - : Image.asset( - 'assets/board-thumbnails/$name.jpg', - height: 44, - errorBuilder: (context, o, st) => const SizedBox.shrink(), - ); + Widget get thumbnail => + this == BoardTheme.system + ? SizedBox( + height: 44, + width: 44 * 6, + child: Row( + children: [ + for (final c in const [1, 2, 3, 4, 5, 6]) + Container( + width: 44, + color: + c.isEven + ? BoardTheme.system.colors.darkSquare + : BoardTheme.system.colors.lightSquare, + ), + ], + ), + ) + : Image.asset( + 'assets/board-thumbnails/$name.jpg', + height: 44, + errorBuilder: (context, o, st) => const SizedBox.shrink(), + ); } enum MaterialDifferenceFormat { @@ -334,20 +305,18 @@ enum MaterialDifferenceFormat { capturedPieces(label: 'Captured pieces'), hidden(label: 'Hidden'); - const MaterialDifferenceFormat({ - required this.label, - }); + const MaterialDifferenceFormat({required this.label}); final String label; bool get visible => this != MaterialDifferenceFormat.hidden; String l10n(AppLocalizations l10n) => switch (this) { - //TODO: Add l10n - MaterialDifferenceFormat.materialDifference => materialDifference.label, - MaterialDifferenceFormat.capturedPieces => capturedPieces.label, - MaterialDifferenceFormat.hidden => hidden.label, - }; + //TODO: Add l10n + MaterialDifferenceFormat.materialDifference => materialDifference.label, + MaterialDifferenceFormat.capturedPieces => capturedPieces.label, + MaterialDifferenceFormat.hidden => hidden.label, + }; } enum ClockPosition { @@ -356,13 +325,13 @@ enum ClockPosition { // TODO: l10n String get label => switch (this) { - ClockPosition.left => 'Left', - ClockPosition.right => 'Right', - }; + ClockPosition.left => 'Left', + ClockPosition.right => 'Right', + }; } String dragTargetKindLabel(DragTargetKind kind) => switch (kind) { - DragTargetKind.circle => 'Circle', - DragTargetKind.square => 'Square', - DragTargetKind.none => 'None', - }; + DragTargetKind.circle => 'Circle', + DragTargetKind.square => 'Square', + DragTargetKind.none => 'None', +}; diff --git a/lib/src/model/settings/brightness.dart b/lib/src/model/settings/brightness.dart index f232bbc6b8..6cc6f93b6c 100644 --- a/lib/src/model/settings/brightness.dart +++ b/lib/src/model/settings/brightness.dart @@ -9,14 +9,9 @@ part 'brightness.g.dart'; class CurrentBrightness extends _$CurrentBrightness { @override Brightness build() { - final themeMode = ref.watch( - generalPreferencesProvider.select( - (state) => state.themeMode, - ), - ); + final themeMode = ref.watch(generalPreferencesProvider.select((state) => state.themeMode)); - WidgetsBinding.instance.platformDispatcher.onPlatformBrightnessChanged = - () { + WidgetsBinding.instance.platformDispatcher.onPlatformBrightnessChanged = () { WidgetsBinding.instance.handlePlatformBrightnessChanged(); if (themeMode == BackgroundThemeMode.system) { state = WidgetsBinding.instance.platformDispatcher.platformBrightness; diff --git a/lib/src/model/settings/general_preferences.dart b/lib/src/model/settings/general_preferences.dart index 3e87d9c6cf..70f18c3e69 100644 --- a/lib/src/model/settings/general_preferences.dart +++ b/lib/src/model/settings/general_preferences.dart @@ -9,8 +9,7 @@ part 'general_preferences.freezed.dart'; part 'general_preferences.g.dart'; @riverpod -class GeneralPreferences extends _$GeneralPreferences - with PreferencesStorage { +class GeneralPreferences extends _$GeneralPreferences with PreferencesStorage { // ignore: avoid_public_notifier_properties @override final prefCategory = PrefCategory.general; @@ -20,8 +19,7 @@ class GeneralPreferences extends _$GeneralPreferences GeneralPrefs get defaults => GeneralPrefs.defaults; @override - GeneralPrefs fromJson(Map json) => - GeneralPrefs.fromJson(json); + GeneralPrefs fromJson(Map json) => GeneralPrefs.fromJson(json); @override GeneralPrefs build() { @@ -53,14 +51,10 @@ class GeneralPreferences extends _$GeneralPreferences if (state.systemColors == false) { final boardTheme = ref.read(boardPreferencesProvider).boardTheme; if (boardTheme == BoardTheme.system) { - await ref - .read(boardPreferencesProvider.notifier) - .setBoardTheme(BoardTheme.brown); + await ref.read(boardPreferencesProvider.notifier).setBoardTheme(BoardTheme.brown); } } else { - await ref - .read(boardPreferencesProvider.notifier) - .setBoardTheme(BoardTheme.system); + await ref.read(boardPreferencesProvider.notifier).setBoardTheme(BoardTheme.system); } } } @@ -68,10 +62,10 @@ class GeneralPreferences extends _$GeneralPreferences Map? _localeToJson(Locale? locale) { return locale != null ? { - 'languageCode': locale.languageCode, - 'countryCode': locale.countryCode, - 'scriptCode': locale.scriptCode, - } + 'languageCode': locale.languageCode, + 'countryCode': locale.countryCode, + 'scriptCode': locale.scriptCode, + } : null; } @@ -89,14 +83,10 @@ Locale? _localeFromJson(Map? json) { @Freezed(fromJson: true, toJson: true) class GeneralPrefs with _$GeneralPrefs implements Serializable { const factory GeneralPrefs({ - @JsonKey( - unknownEnumValue: BackgroundThemeMode.system, - defaultValue: BackgroundThemeMode.system, - ) + @JsonKey(unknownEnumValue: BackgroundThemeMode.system, defaultValue: BackgroundThemeMode.system) required BackgroundThemeMode themeMode, required bool isSoundEnabled, - @JsonKey(unknownEnumValue: SoundTheme.standard) - required SoundTheme soundTheme, + @JsonKey(unknownEnumValue: SoundTheme.standard) required SoundTheme soundTheme, @JsonKey(defaultValue: 0.8) required double masterVolume, /// Should enable system color palette (android 12+ only) diff --git a/lib/src/model/settings/home_preferences.dart b/lib/src/model/settings/home_preferences.dart index 31e2eefff0..ff9fa70910 100644 --- a/lib/src/model/settings/home_preferences.dart +++ b/lib/src/model/settings/home_preferences.dart @@ -6,8 +6,7 @@ part 'home_preferences.freezed.dart'; part 'home_preferences.g.dart'; @riverpod -class HomePreferences extends _$HomePreferences - with PreferencesStorage { +class HomePreferences extends _$HomePreferences with PreferencesStorage { // ignore: avoid_public_notifier_properties @override PrefCategory get prefCategory => PrefCategory.home; @@ -26,32 +25,23 @@ class HomePreferences extends _$HomePreferences Future toggleWidget(EnabledWidget widget) { final newState = state.copyWith( - enabledWidgets: state.enabledWidgets.contains(widget) - ? state.enabledWidgets.difference({widget}) - : state.enabledWidgets.union({widget}), + enabledWidgets: + state.enabledWidgets.contains(widget) + ? state.enabledWidgets.difference({widget}) + : state.enabledWidgets.union({widget}), ); return save(newState); } } -enum EnabledWidget { - hello, - perfCards, - quickPairing, -} +enum EnabledWidget { hello, perfCards, quickPairing } @Freezed(fromJson: true, toJson: true) class HomePrefs with _$HomePrefs implements Serializable { - const factory HomePrefs({ - required Set enabledWidgets, - }) = _HomePrefs; + const factory HomePrefs({required Set enabledWidgets}) = _HomePrefs; static const defaults = HomePrefs( - enabledWidgets: { - EnabledWidget.hello, - EnabledWidget.perfCards, - EnabledWidget.quickPairing, - }, + enabledWidgets: {EnabledWidget.hello, EnabledWidget.perfCards, EnabledWidget.quickPairing}, ); factory HomePrefs.fromJson(Map json) { diff --git a/lib/src/model/settings/over_the_board_preferences.dart b/lib/src/model/settings/over_the_board_preferences.dart index 7565119845..4fe2b72f7c 100644 --- a/lib/src/model/settings/over_the_board_preferences.dart +++ b/lib/src/model/settings/over_the_board_preferences.dart @@ -17,8 +17,7 @@ class OverTheBoardPreferences extends _$OverTheBoardPreferences OverTheBoardPrefs get defaults => OverTheBoardPrefs.defaults; @override - OverTheBoardPrefs fromJson(Map json) => - OverTheBoardPrefs.fromJson(json); + OverTheBoardPrefs fromJson(Map json) => OverTheBoardPrefs.fromJson(json); @override OverTheBoardPrefs build() { @@ -26,15 +25,11 @@ class OverTheBoardPreferences extends _$OverTheBoardPreferences } Future toggleFlipPiecesAfterMove() { - return save( - state.copyWith(flipPiecesAfterMove: !state.flipPiecesAfterMove), - ); + return save(state.copyWith(flipPiecesAfterMove: !state.flipPiecesAfterMove)); } Future toggleSymmetricPieces() { - return save( - state.copyWith(symmetricPieces: !state.symmetricPieces), - ); + return save(state.copyWith(symmetricPieces: !state.symmetricPieces)); } } @@ -47,10 +42,7 @@ class OverTheBoardPrefs with _$OverTheBoardPrefs implements Serializable { required bool symmetricPieces, }) = _OverTheBoardPrefs; - static const defaults = OverTheBoardPrefs( - flipPiecesAfterMove: false, - symmetricPieces: false, - ); + static const defaults = OverTheBoardPrefs(flipPiecesAfterMove: false, symmetricPieces: false); factory OverTheBoardPrefs.fromJson(Map json) { return _$OverTheBoardPrefsFromJson(json); diff --git a/lib/src/model/settings/preferences_storage.dart b/lib/src/model/settings/preferences_storage.dart index a167695c54..9234927cde 100644 --- a/lib/src/model/settings/preferences_storage.dart +++ b/lib/src/model/settings/preferences_storage.dart @@ -41,22 +41,21 @@ mixin PreferencesStorage on AutoDisposeNotifier { PrefCategory get prefCategory; Future save(T value) async { - await LichessBinding.instance.sharedPreferences - .setString(prefCategory.storageKey, jsonEncode(value.toJson())); + await LichessBinding.instance.sharedPreferences.setString( + prefCategory.storageKey, + jsonEncode(value.toJson()), + ); state = value; } T fetch() { - final stored = LichessBinding.instance.sharedPreferences - .getString(prefCategory.storageKey); + final stored = LichessBinding.instance.sharedPreferences.getString(prefCategory.storageKey); if (stored == null) { return defaults; } try { - return fromJson( - jsonDecode(stored) as Map, - ); + return fromJson(jsonDecode(stored) as Map); } catch (e) { _logger.warning('Failed to decode $prefCategory preferences: $e'); return defaults; @@ -65,8 +64,7 @@ mixin PreferencesStorage on AutoDisposeNotifier { } /// A [Notifier] mixin to provide a way to store and retrieve preferences per session. -mixin SessionPreferencesStorage - on AutoDisposeNotifier { +mixin SessionPreferencesStorage on AutoDisposeNotifier { T fromJson(Map json); T defaults({LightUser? user}); @@ -84,15 +82,14 @@ mixin SessionPreferencesStorage T fetch() { final session = ref.watch(authSessionProvider); - final stored = LichessBinding.instance.sharedPreferences - .getString(key(prefCategory.storageKey, session)); + final stored = LichessBinding.instance.sharedPreferences.getString( + key(prefCategory.storageKey, session), + ); if (stored == null) { return defaults(user: session?.user); } try { - return fromJson( - jsonDecode(stored) as Map, - ); + return fromJson(jsonDecode(stored) as Map); } catch (e) { _logger.warning('Failed to decode $prefCategory preferences: $e'); return defaults(user: session?.user); diff --git a/lib/src/model/study/study.dart b/lib/src/model/study/study.dart index ae939d4761..2a45906358 100644 --- a/lib/src/model/study/study.dart +++ b/lib/src/model/study/study.dart @@ -35,8 +35,7 @@ class Study with _$Study { required IList deviationComments, }) = _Study; - StudyChapterMeta get currentChapterMeta => - chapters.firstWhere((c) => c.id == chapter.id); + StudyChapterMeta get currentChapterMeta => chapters.firstWhere((c) => c.id == chapter.id); factory Study.fromServerJson(Map json) { return _studyFromPick(pick(json).required()); @@ -66,22 +65,18 @@ Study _studyFromPick(RequiredPick pick) { chat: study('features', 'chat').asBoolOrFalse(), sticky: study('features', 'sticky').asBoolOrFalse(), ), - topics: - study('topics').asListOrThrow((pick) => pick.asStringOrThrow()).lock, - chapters: study('chapters') - .asListOrThrow((pick) => StudyChapterMeta.fromJson(pick.asMapOrThrow())) - .lock, + topics: study('topics').asListOrThrow((pick) => pick.asStringOrThrow()).lock, + chapters: + study( + 'chapters', + ).asListOrThrow((pick) => StudyChapterMeta.fromJson(pick.asMapOrThrow())).lock, chapter: StudyChapter.fromJson(study('chapter').asMapOrThrow()), hints: hints.lock, deviationComments: deviationComments.lock, ); } -typedef StudyFeatures = ({ - bool cloneable, - bool chat, - bool sticky, -}); +typedef StudyFeatures = ({bool cloneable, bool chat, bool sticky}); @Freezed(fromJson: true) class StudyChapter with _$StudyChapter { @@ -93,18 +88,13 @@ class StudyChapter with _$StudyChapter { @JsonKey(defaultValue: false) required bool practise, required int? conceal, @JsonKey(defaultValue: false) required bool gamebook, - @JsonKey(fromJson: studyChapterFeaturesFromJson) - required StudyChapterFeatures features, + @JsonKey(fromJson: studyChapterFeaturesFromJson) required StudyChapterFeatures features, }) = _StudyChapter; - factory StudyChapter.fromJson(Map json) => - _$StudyChapterFromJson(json); + factory StudyChapter.fromJson(Map json) => _$StudyChapterFromJson(json); } -typedef StudyChapterFeatures = ({ - bool computer, - bool explorer, -}); +typedef StudyChapterFeatures = ({bool computer, bool explorer}); StudyChapterFeatures studyChapterFeaturesFromJson(Map json) { return ( @@ -129,9 +119,7 @@ class StudyChapterSetup with _$StudyChapterSetup { } Variant _variantFromJson(Map json) { - return Variant.values.firstWhereOrNull( - (v) => v.name == json['key'], - )!; + return Variant.values.firstWhereOrNull((v) => v.name == json['key'])!; } @Freezed(fromJson: true) @@ -144,8 +132,7 @@ class StudyChapterMeta with _$StudyChapterMeta { required String? fen, }) = _StudyChapterMeta; - factory StudyChapterMeta.fromJson(Map json) => - _$StudyChapterMetaFromJson(json); + factory StudyChapterMeta.fromJson(Map json) => _$StudyChapterMetaFromJson(json); } @Freezed(fromJson: true) @@ -157,8 +144,7 @@ class StudyPageData with _$StudyPageData { required String name, required bool liked, required int likes, - @JsonKey(fromJson: DateTime.fromMillisecondsSinceEpoch) - required DateTime updatedAt, + @JsonKey(fromJson: DateTime.fromMillisecondsSinceEpoch) required DateTime updatedAt, required LightUser? owner, required IList topics, required IList members, @@ -166,19 +152,14 @@ class StudyPageData with _$StudyPageData { required String? flair, }) = _StudyPageData; - factory StudyPageData.fromJson(Map json) => - _$StudyPageDataFromJson(json); + factory StudyPageData.fromJson(Map json) => _$StudyPageDataFromJson(json); } @Freezed(fromJson: true) class StudyMember with _$StudyMember { const StudyMember._(); - const factory StudyMember({ - required LightUser user, - required String role, - }) = _StudyMember; + const factory StudyMember({required LightUser user, required String role}) = _StudyMember; - factory StudyMember.fromJson(Map json) => - _$StudyMemberFromJson(json); + factory StudyMember.fromJson(Map json) => _$StudyMemberFromJson(json); } diff --git a/lib/src/model/study/study_controller.dart b/lib/src/model/study/study_controller.dart index c77ccf64f4..6e2c2e6e73 100644 --- a/lib/src/model/study/study_controller.dart +++ b/lib/src/model/study/study_controller.dart @@ -72,19 +72,11 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { } Future goToChapter(StudyChapterId chapterId) async { - state = AsyncValue.data( - await _fetchChapter( - state.requireValue.study.id, - chapterId: chapterId, - ), - ); + state = AsyncValue.data(await _fetchChapter(state.requireValue.study.id, chapterId: chapterId)); _ensureItsOurTurnIfGamebook(); } - Future _fetchChapter( - StudyId id, { - StudyChapterId? chapterId, - }) async { + Future _fetchChapter(StudyId id, {StudyChapterId? chapterId}) async { final (study, pgn) = await ref .read(studyRepositoryProvider) .getStudy(id: id, chapterId: chapterId); @@ -134,8 +126,7 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { pgnRootComments: rootComments, lastMove: lastMove, pov: orientation, - isComputerAnalysisAllowed: - study.chapter.features.computer && !study.chapter.gamebook, + isComputerAnalysisAllowed: study.chapter.features.computer && !study.chapter.gamebook, isLocalEvaluationEnabled: prefs.enableLocalEvaluation, gamebookActive: study.chapter.gamebook, pgn: pgn, @@ -147,18 +138,18 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { evaluationService .initEngine( - _evaluationContext(studyState.variant), - options: EvaluationOptions( - multiPv: prefs.numEvalLines, - cores: prefs.numEngineCores, - searchTime: ref.read(analysisPreferencesProvider).engineSearchTime, - ), - ) + _evaluationContext(studyState.variant), + options: EvaluationOptions( + multiPv: prefs.numEvalLines, + cores: prefs.numEngineCores, + searchTime: ref.read(analysisPreferencesProvider).engineSearchTime, + ), + ) .then((_) { - _startEngineEvalTimer = Timer(const Duration(milliseconds: 250), () { - _startEngineEval(); - }); - }); + _startEngineEvalTimer = Timer(const Duration(milliseconds: 250), () { + _startEngineEval(); + }); + }); } return studyState; @@ -170,9 +161,7 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { final liked = state.requireValue.study.liked; _socketClient.send('like', {'liked': !liked}); state = AsyncValue.data( - state.requireValue.copyWith( - study: state.requireValue.study.copyWith(liked: !liked), - ), + state.requireValue.copyWith(study: state.requireValue.study.copyWith(liked: !liked)), ); }); } @@ -184,14 +173,12 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { } switch (event.topic) { case 'liking': - final data = - (event.data as Map)['l'] as Map; + final data = (event.data as Map)['l'] as Map; final likes = data['likes'] as int; final bool meLiked = data['me'] as bool; state = AsyncValue.data( state.requireValue.copyWith( - study: - state.requireValue.study.copyWith(liked: meLiked, likes: likes), + study: state.requireValue.study.copyWith(liked: meLiked, likes: likes), ), ); } @@ -210,10 +197,8 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { } } - EvaluationContext _evaluationContext(Variant variant) => EvaluationContext( - variant: variant, - initialPosition: _root.position, - ); + EvaluationContext _evaluationContext(Variant variant) => + EvaluationContext(variant: variant, initialPosition: _root.position); void onUserMove(NormalMove move) { if (!state.hasValue || state.requireValue.position == null) return; @@ -225,14 +210,9 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { return; } - final (newPath, isNewNode) = - _root.addMoveAt(state.requireValue.currentPath, move); + final (newPath, isNewNode) = _root.addMoveAt(state.requireValue.currentPath, move); if (newPath != null) { - _setPath( - newPath, - shouldRecomputeRootView: isNewNode, - shouldForceShowVariation: true, - ); + _setPath(newPath, shouldRecomputeRootView: isNewNode, shouldForceShowVariation: true); } if (state.requireValue.gamebookActive) { @@ -332,8 +312,7 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { final node = _root.nodeAt(path); - final childrenToShow = - _root.isOnMainline(path) ? node.children.skip(1) : node.children; + final childrenToShow = _root.isOnMainline(path) ? node.children.skip(1) : node.children; for (final child in childrenToShow) { child.isCollapsed = false; @@ -363,10 +342,7 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { if (state == null) return; _root.promoteAt(path, toMainline: toMainline); this.state = AsyncValue.data( - state.copyWith( - isOnMainline: _root.isOnMainline(state.currentPath), - root: _root.view, - ), + state.copyWith(isOnMainline: _root.isOnMainline(state.currentPath), root: _root.view), ); } @@ -381,9 +357,7 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { Future toggleLocalEvaluation() async { if (!state.hasValue) return; - ref - .read(analysisPreferencesProvider.notifier) - .toggleEnableLocalEvaluation(); + ref.read(analysisPreferencesProvider.notifier).toggleEnableLocalEvaluation(); state = AsyncValue.data( state.requireValue.copyWith( @@ -393,13 +367,14 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { if (state.requireValue.isEngineAvailable) { final prefs = ref.read(analysisPreferencesProvider); - await ref.read(evaluationServiceProvider).initEngine( + await ref + .read(evaluationServiceProvider) + .initEngine( _evaluationContext(state.requireValue.variant), options: EvaluationOptions( multiPv: prefs.numEvalLines, cores: prefs.numEngineCores, - searchTime: - ref.read(analysisPreferencesProvider).engineSearchTime, + searchTime: ref.read(analysisPreferencesProvider).engineSearchTime, ), ); _startEngineEval(); @@ -412,11 +387,11 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { void setNumEvalLines(int numEvalLines) { if (!state.hasValue) return; - ref - .read(analysisPreferencesProvider.notifier) - .setNumEvalLines(numEvalLines); + ref.read(analysisPreferencesProvider.notifier).setNumEvalLines(numEvalLines); - ref.read(evaluationServiceProvider).setOptions( + ref + .read(evaluationServiceProvider) + .setOptions( EvaluationOptions( multiPv: numEvalLines, cores: ref.read(analysisPreferencesProvider).numEngineCores, @@ -428,9 +403,7 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { state = AsyncValue.data( state.requireValue.copyWith( - currentNode: StudyCurrentNode.fromNode( - _root.nodeAt(state.requireValue.currentPath), - ), + currentNode: StudyCurrentNode.fromNode(_root.nodeAt(state.requireValue.currentPath)), ), ); @@ -438,11 +411,11 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { } void setEngineCores(int numEngineCores) { - ref - .read(analysisPreferencesProvider.notifier) - .setEngineCores(numEngineCores); + ref.read(analysisPreferencesProvider.notifier).setEngineCores(numEngineCores); - ref.read(evaluationServiceProvider).setOptions( + ref + .read(evaluationServiceProvider) + .setOptions( EvaluationOptions( multiPv: ref.read(analysisPreferencesProvider).numEvalLines, cores: numEngineCores, @@ -454,11 +427,11 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { } void setEngineSearchTime(Duration searchTime) { - ref - .read(analysisPreferencesProvider.notifier) - .setEngineSearchTime(searchTime); + ref.read(analysisPreferencesProvider.notifier).setEngineSearchTime(searchTime); - ref.read(evaluationServiceProvider).setOptions( + ref + .read(evaluationServiceProvider) + .setOptions( EvaluationOptions( multiPv: ref.read(analysisPreferencesProvider).numEvalLines, cores: ref.read(analysisPreferencesProvider).numEngineCores, @@ -482,9 +455,7 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { final currentNode = _root.nodeAt(path); // always show variation if the user plays a move - if (shouldForceShowVariation && - currentNode is Branch && - currentNode.isCollapsed) { + if (shouldForceShowVariation && currentNode is Branch && currentNode.isCollapsed) { _root.updateAt(path, (node) { if (node is Branch) node.isCollapsed = false; }); @@ -493,9 +464,7 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { // root view is only used to display move list, so we need to // recompute the root view only when the nodelist length changes // or a variation is hidden/shown - final rootView = shouldForceShowVariation || shouldRecomputeRootView - ? _root.view - : state.root; + final rootView = shouldForceShowVariation || shouldRecomputeRootView ? _root.view : state.root; final isForward = path.size > state.currentPath.size; if (currentNode is Branch) { @@ -503,9 +472,7 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { if (isForward) { final isCheck = currentNode.sanMove.isCheck; if (currentNode.sanMove.isCapture) { - ref - .read(moveFeedbackServiceProvider) - .captureFeedback(check: isCheck); + ref.read(moveFeedbackServiceProvider).captureFeedback(check: isCheck); } else { ref.read(moveFeedbackServiceProvider).moveFeedback(check: isCheck); } @@ -558,12 +525,9 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { _root.branchesOn(state.currentPath).map(Step.fromNode), // Note: AnalysisController passes _root.eval as initialPositionEval here, // but for studies this leads to false positive cache hits when switching between chapters. - shouldEmit: (work) => - work.path == this.state.valueOrNull?.currentPath, + shouldEmit: (work) => work.path == this.state.valueOrNull?.currentPath, ) - ?.forEach( - (t) => _root.updateAt(t.$1.path, (node) => node.eval = t.$2), - ); + ?.forEach((t) => _root.updateAt(t.$1.path, (node) => node.eval = t.$2)); } void _debouncedStartEngineEval() { @@ -580,21 +544,13 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { // update the current node with last cached eval state = AsyncValue.data( state.requireValue.copyWith( - currentNode: StudyCurrentNode.fromNode( - _root.nodeAt(state.requireValue.currentPath), - ), + currentNode: StudyCurrentNode.fromNode(_root.nodeAt(state.requireValue.currentPath)), ), ); } } -enum GamebookState { - startLesson, - findTheMove, - correctMove, - incorrectMove, - lessonComplete -} +enum GamebookState { startLesson, findTheMove, correctMove, incorrectMove, lessonComplete } @freezed class StudyState with _$StudyState { @@ -645,9 +601,8 @@ class StudyState with _$StudyState { IList? pgnRootComments, }) = _StudyState; - IMap> get validMoves => currentNode.position != null - ? makeLegalMoves(currentNode.position!) - : const IMap.empty(); + IMap> get validMoves => + currentNode.position != null ? makeLegalMoves(currentNode.position!) : const IMap.empty(); /// Whether the engine is available for evaluation bool get isEngineAvailable => @@ -655,28 +610,25 @@ class StudyState with _$StudyState { engineSupportedVariants.contains(variant) && isLocalEvaluationEnabled; - bool get isOpeningExplorerAvailable => - !gamebookActive && study.chapter.features.explorer; + bool get isOpeningExplorerAvailable => !gamebookActive && study.chapter.features.explorer; - EngineGaugeParams? get engineGaugeParams => isEngineAvailable - ? ( - orientation: pov, - isLocalEngineAvailable: isEngineAvailable, - position: position!, - savedEval: currentNode.eval, - ) - : null; + EngineGaugeParams? get engineGaugeParams => + isEngineAvailable + ? ( + orientation: pov, + isLocalEngineAvailable: isEngineAvailable, + position: position!, + savedEval: currentNode.eval, + ) + : null; Position? get position => currentNode.position; StudyChapter get currentChapter => study.chapter; bool get canGoNext => currentNode.children.isNotEmpty; bool get canGoBack => currentPath.size > UciPath.empty.size; - String get currentChapterTitle => study.chapters - .firstWhere( - (chapter) => chapter.id == currentChapter.id, - ) - .name; + String get currentChapterTitle => + study.chapters.firstWhere((chapter) => chapter.id == currentChapter.id).name; bool get hasNextChapter => study.chapter.id != study.chapters.last.id; bool get isAtEndOfChapter => isOnMainline && currentNode.children.isEmpty; @@ -684,22 +636,20 @@ class StudyState with _$StudyState { bool get isAtStartOfChapter => currentPath.isEmpty; String? get gamebookComment { - final comment = - (currentNode.isRoot ? pgnRootComments : currentNode.comments) - ?.map((comment) => comment.text) - .nonNulls - .join('\n'); + final comment = (currentNode.isRoot ? pgnRootComments : currentNode.comments) + ?.map((comment) => comment.text) + .nonNulls + .join('\n'); return comment?.isNotEmpty == true ? comment : gamebookState == GamebookState.incorrectMove - ? gamebookDeviationComment - : null; + ? gamebookDeviationComment + : null; } String? get gamebookHint => study.hints.getOrNull(currentPath.size); - String? get gamebookDeviationComment => - study.deviationComments.getOrNull(currentPath.size); + String? get gamebookDeviationComment => study.deviationComments.getOrNull(currentPath.size); GamebookState get gamebookState { if (isAtEndOfChapter) return GamebookState.lessonComplete; @@ -710,22 +660,20 @@ class StudyState with _$StudyState { return myTurn ? GamebookState.findTheMove : isOnMainline - ? GamebookState.correctMove - : GamebookState.incorrectMove; + ? GamebookState.correctMove + : GamebookState.incorrectMove; } - bool get isIntroductoryChapter => - currentNode.isRoot && currentNode.children.isEmpty; + bool get isIntroductoryChapter => currentNode.isRoot && currentNode.children.isEmpty; IList get pgnShapes => IList( - (currentNode.isRoot ? pgnRootComments : currentNode.comments) - ?.map((comment) => comment.shapes) - .flattened, - ); + (currentNode.isRoot ? pgnRootComments : currentNode.comments) + ?.map((comment) => comment.shapes) + .flattened, + ); - PlayerSide get playerSide => gamebookActive - ? (pov == Side.white ? PlayerSide.white : PlayerSide.black) - : PlayerSide.both; + PlayerSide get playerSide => + gamebookActive ? (pov == Side.white ? PlayerSide.white : PlayerSide.black) : PlayerSide.both; } @freezed @@ -745,11 +693,7 @@ class StudyCurrentNode with _$StudyCurrentNode { }) = _StudyCurrentNode; factory StudyCurrentNode.illegalPosition() { - return const StudyCurrentNode( - position: null, - children: [], - isRoot: true, - ); + return const StudyCurrentNode(position: null, children: [], isRoot: true); } factory StudyCurrentNode.fromNode(Node node) { diff --git a/lib/src/model/study/study_filter.dart b/lib/src/model/study/study_filter.dart index 45246f3dcf..7cefc08f35 100644 --- a/lib/src/model/study/study_filter.dart +++ b/lib/src/model/study/study_filter.dart @@ -14,13 +14,13 @@ enum StudyCategory { likes; String l10n(AppLocalizations l10n) => switch (this) { - StudyCategory.all => l10n.studyAllStudies, - StudyCategory.mine => l10n.studyMyStudies, - StudyCategory.member => l10n.studyStudiesIContributeTo, - StudyCategory.public => l10n.studyMyPublicStudies, - StudyCategory.private => l10n.studyMyPrivateStudies, - StudyCategory.likes => l10n.studyMyFavoriteStudies, - }; + StudyCategory.all => l10n.studyAllStudies, + StudyCategory.mine => l10n.studyMyStudies, + StudyCategory.member => l10n.studyStudiesIContributeTo, + StudyCategory.public => l10n.studyMyPublicStudies, + StudyCategory.private => l10n.studyMyPrivateStudies, + StudyCategory.likes => l10n.studyMyFavoriteStudies, + }; } enum StudyListOrder { @@ -31,12 +31,12 @@ enum StudyListOrder { updated; String l10n(AppLocalizations l10n) => switch (this) { - StudyListOrder.hot => l10n.studyHot, - StudyListOrder.newest => l10n.studyDateAddedNewest, - StudyListOrder.oldest => l10n.studyDateAddedOldest, - StudyListOrder.updated => l10n.studyRecentlyUpdated, - StudyListOrder.popular => l10n.studyMostPopular, - }; + StudyListOrder.hot => l10n.studyHot, + StudyListOrder.newest => l10n.studyDateAddedNewest, + StudyListOrder.oldest => l10n.studyDateAddedOldest, + StudyListOrder.updated => l10n.studyRecentlyUpdated, + StudyListOrder.popular => l10n.studyMostPopular, + }; } @riverpod @@ -44,8 +44,7 @@ class StudyFilter extends _$StudyFilter { @override StudyFilterState build() => const StudyFilterState(); - void setCategory(StudyCategory category) => - state = state.copyWith(category: category); + void setCategory(StudyCategory category) => state = state.copyWith(category: category); void setOrder(StudyListOrder order) => state = state.copyWith(order: order); } diff --git a/lib/src/model/study/study_list_paginator.dart b/lib/src/model/study/study_list_paginator.dart index 4684a3ecd0..0a24a1999d 100644 --- a/lib/src/model/study/study_list_paginator.dart +++ b/lib/src/model/study/study_list_paginator.dart @@ -12,10 +12,7 @@ typedef StudyList = ({IList studies, int? nextPage}); @riverpod class StudyListPaginator extends _$StudyListPaginator { @override - Future build({ - required StudyFilterState filter, - String? search, - }) async { + Future build({required StudyFilterState filter, String? search}) async { return _nextPage(); } @@ -25,12 +22,10 @@ class StudyListPaginator extends _$StudyListPaginator { final newStudyPage = await _nextPage(); - state = AsyncData( - ( - nextPage: newStudyPage.nextPage, - studies: studyList.studies.addAll(newStudyPage.studies), - ), - ); + state = AsyncData(( + nextPage: newStudyPage.nextPage, + studies: studyList.studies.addAll(newStudyPage.studies), + )); } Future _nextPage() async { @@ -38,14 +33,7 @@ class StudyListPaginator extends _$StudyListPaginator { final repo = ref.read(studyRepositoryProvider); return search == null - ? repo.getStudies( - category: filter.category, - order: filter.order, - page: nextPage, - ) - : repo.searchStudies( - query: search!, - page: nextPage, - ); + ? repo.getStudies(category: filter.category, order: filter.order, page: nextPage) + : repo.searchStudies(query: search!, page: nextPage); } } diff --git a/lib/src/model/study/study_preferences.dart b/lib/src/model/study/study_preferences.dart index fe3fb2f7fa..d309eeb7ab 100644 --- a/lib/src/model/study/study_preferences.dart +++ b/lib/src/model/study/study_preferences.dart @@ -6,8 +6,7 @@ part 'study_preferences.freezed.dart'; part 'study_preferences.g.dart'; @riverpod -class StudyPreferences extends _$StudyPreferences - with PreferencesStorage { +class StudyPreferences extends _$StudyPreferences with PreferencesStorage { // ignore: avoid_public_notifier_properties @override final prefCategory = PrefCategory.study; @@ -25,11 +24,7 @@ class StudyPreferences extends _$StudyPreferences } Future toggleShowVariationArrows() { - return save( - state.copyWith( - showVariationArrows: !state.showVariationArrows, - ), - ); + return save(state.copyWith(showVariationArrows: !state.showVariationArrows)); } } @@ -37,13 +32,9 @@ class StudyPreferences extends _$StudyPreferences class StudyPrefs with _$StudyPrefs implements Serializable { const StudyPrefs._(); - const factory StudyPrefs({ - required bool showVariationArrows, - }) = _StudyPrefs; + const factory StudyPrefs({required bool showVariationArrows}) = _StudyPrefs; - static const defaults = StudyPrefs( - showVariationArrows: false, - ); + static const defaults = StudyPrefs(showVariationArrows: false); factory StudyPrefs.fromJson(Map json) { return _$StudyPrefsFromJson(json); diff --git a/lib/src/model/study/study_repository.dart b/lib/src/model/study/study_repository.dart index c0c2cc4886..19cad44f07 100644 --- a/lib/src/model/study/study_repository.dart +++ b/lib/src/model/study/study_repository.dart @@ -35,14 +35,8 @@ class StudyRepository { ); } - Future searchStudies({ - required String query, - int page = 1, - }) { - return _requestStudies( - path: 'search', - queryParameters: {'page': page.toString(), 'q': query}, - ); + Future searchStudies({required String query, int page = 1}) { + return _requestStudies(path: 'search', queryParameters: {'page': page.toString(), 'q': query}); } Future _requestStudies({ @@ -50,22 +44,18 @@ class StudyRepository { required Map queryParameters, }) { return client.readJson( - Uri( - path: '/study/$path', - queryParameters: queryParameters, - ), + Uri(path: '/study/$path', queryParameters: queryParameters), headers: {'Accept': 'application/json'}, mapper: (Map json) { - final paginator = - pick(json, 'paginator').asMapOrThrow(); + final paginator = pick(json, 'paginator').asMapOrThrow(); return ( - studies: pick(paginator, 'currentPageResults') - .asListOrThrow( - (pick) => StudyPageData.fromJson(pick.asMapOrThrow()), - ) - .toIList(), - nextPage: pick(paginator, 'nextPage').asIntOrNull() + studies: + pick( + paginator, + 'currentPageResults', + ).asListOrThrow((pick) => StudyPageData.fromJson(pick.asMapOrThrow())).toIList(), + nextPage: pick(paginator, 'nextPage').asIntOrNull(), ); }, ); @@ -78,9 +68,7 @@ class StudyRepository { final study = await client.readJson( Uri( path: (chapterId != null) ? '/study/$id/$chapterId' : '/study/$id', - queryParameters: { - 'chapters': '1', - }, + queryParameters: {'chapters': '1'}, ), headers: {'Accept': 'application/json'}, mapper: Study.fromServerJson, diff --git a/lib/src/model/tv/featured_player.dart b/lib/src/model/tv/featured_player.dart index aa1f431395..070b3171cc 100644 --- a/lib/src/model/tv/featured_player.dart +++ b/lib/src/model/tv/featured_player.dart @@ -29,8 +29,7 @@ class FeaturedPlayer with _$FeaturedPlayer { } Player get asPlayer => Player( - user: - LightUser(id: UserId(name.toLowerCase()), name: name, title: title), - rating: rating, - ); + user: LightUser(id: UserId(name.toLowerCase()), name: name, title: title), + rating: rating, + ); } diff --git a/lib/src/model/tv/live_tv_channels.dart b/lib/src/model/tv/live_tv_channels.dart index 95c77bb3ff..e7df4cf9e2 100644 --- a/lib/src/model/tv/live_tv_channels.dart +++ b/lib/src/model/tv/live_tv_channels.dart @@ -48,19 +48,16 @@ class LiveTvChannels extends _$LiveTvChannels { } Future> _doStartWatching() async { - final repoGames = - await ref.withClient((client) => TvRepository(client).channels()); + final repoGames = await ref.withClient((client) => TvRepository(client).channels()); - _socketClient = - ref.read(socketPoolProvider).open(Uri(path: kDefaultSocketRoute)); + _socketClient = ref.read(socketPoolProvider).open(Uri(path: kDefaultSocketRoute)); await _socketClient.firstConnection; _socketWatch(repoGames); _socketReadySubscription?.cancel(); _socketReadySubscription = _socketClient.connectedStream.listen((_) async { - final repoGames = - await ref.withClient((client) => TvRepository(client).channels()); + final repoGames = await ref.withClient((client) => TvRepository(client).channels()); _socketWatch(repoGames); }); @@ -89,19 +86,13 @@ class LiveTvChannels extends _$LiveTvChannels { _socketClient.send('startWatchingTvChannels', null); _socketClient.send( 'startWatching', - games.entries - .where((e) => TvChannel.values.contains(e.key)) - .map((e) => e.value.id) - .join(' '), + games.entries.where((e) => TvChannel.values.contains(e.key)).map((e) => e.value.id).join(' '), ); } void _handleSocketEvent(SocketEvent event) { if (!state.hasValue) { - assert( - false, - 'received a SocketEvent while LiveTvChannels state is null', - ); + assert(false, 'received a SocketEvent while LiveTvChannels state is null'); return; } @@ -109,18 +100,15 @@ class LiveTvChannels extends _$LiveTvChannels { case 'fen': final json = event.data as Map; final fenEvent = FenSocketEvent.fromJson(json); - final snapshots = - state.requireValue.values.where((s) => s.id == fenEvent.id); + final snapshots = state.requireValue.values.where((s) => s.id == fenEvent.id); if (snapshots.isNotEmpty) { state = AsyncValue.data( state.requireValue.updateAll( - (key, value) => value.id == fenEvent.id - ? value.copyWith( - fen: fenEvent.fen, - lastMove: fenEvent.lastMove, - ) - : value, + (key, value) => + value.id == fenEvent.id + ? value.copyWith(fen: fenEvent.fen, lastMove: fenEvent.lastMove) + : value, ), ); } diff --git a/lib/src/model/tv/tv_channel.dart b/lib/src/model/tv/tv_channel.dart index 3f70ea4203..72aa16e279 100644 --- a/lib/src/model/tv/tv_channel.dart +++ b/lib/src/model/tv/tv_channel.dart @@ -26,8 +26,7 @@ enum TvChannel { final String label; final IconData icon; - static final IMap nameMap = - IMap(TvChannel.values.asNameMap()); + static final IMap nameMap = IMap(TvChannel.values.asNameMap()); } extension TvChannelExtension on Pick { @@ -41,9 +40,7 @@ extension TvChannelExtension on Pick { return TvChannel.nameMap[value]!; } } - throw PickException( - "value $value at $debugParsingExit can't be casted to TvChannel", - ); + throw PickException("value $value at $debugParsingExit can't be casted to TvChannel"); } TvChannel? asTvChannelOrNull() { diff --git a/lib/src/model/tv/tv_controller.dart b/lib/src/model/tv/tv_controller.dart index 78b6eeacc4..c445844d75 100644 --- a/lib/src/model/tv/tv_controller.dart +++ b/lib/src/model/tv/tv_controller.dart @@ -30,10 +30,7 @@ class TvController extends _$TvController { int? _socketEventVersion; @override - Future build( - TvChannel channel, - (GameId id, Side orientation)? initialGame, - ) async { + Future build(TvChannel channel, (GameId id, Side orientation)? initialGame) async { ref.onDispose(() { _socketSubscription?.cancel(); }); @@ -52,9 +49,7 @@ class TvController extends _$TvController { _socketSubscription?.cancel(); } - Future _connectWebsocket( - (GameId id, Side orientation)? game, - ) async { + Future _connectWebsocket((GameId id, Side orientation)? game) async { GameId id; Side orientation; @@ -62,29 +57,22 @@ class TvController extends _$TvController { id = game.$1; orientation = game.$2; } else { - final channels = - await ref.withClient((client) => TvRepository(client).channels()); + final channels = await ref.withClient((client) => TvRepository(client).channels()); final channelGame = channels[channel]!; id = channelGame.id; orientation = channelGame.side ?? Side.white; } - final socketClient = ref.read(socketPoolProvider).open( - Uri( - path: '/watch/$id/${orientation.name}/v6', - ), - forceReconnect: true, - ); + final socketClient = ref + .read(socketPoolProvider) + .open(Uri(path: '/watch/$id/${orientation.name}/v6'), forceReconnect: true); _socketSubscription?.cancel(); _socketEventVersion = null; _socketSubscription = socketClient.stream.listen(_handleSocketEvent); - return socketClient.stream - .firstWhere((e) => e.topic == 'full') - .then((event) { - final fullEvent = - GameFullEvent.fromJson(event.data as Map); + return socketClient.stream.firstWhere((e) => e.topic == 'full').then((event) { + final fullEvent = GameFullEvent.fromJson(event.data as Map); _socketEventVersion = fullEvent.socketEventVersion; @@ -101,21 +89,15 @@ class TvController extends _$TvController { state = AsyncValue.data(newState); } - bool canGoBack() => - state.mapOrNull(data: (d) => d.value.stepCursor > 0) ?? false; + bool canGoBack() => state.mapOrNull(data: (d) => d.value.stepCursor > 0) ?? false; bool canGoForward() => - state.mapOrNull( - data: (d) => d.value.stepCursor < d.value.game.steps.length - 1, - ) ?? - false; + state.mapOrNull(data: (d) => d.value.stepCursor < d.value.game.steps.length - 1) ?? false; void toggleBoard() { if (state.hasValue) { final curState = state.requireValue; - state = AsyncValue.data( - curState.copyWith(orientation: curState.orientation.opposite), - ); + state = AsyncValue.data(curState.copyWith(orientation: curState.orientation.opposite)); } } @@ -123,9 +105,7 @@ class TvController extends _$TvController { if (state.hasValue) { final curState = state.requireValue; if (curState.stepCursor < curState.game.steps.length - 1) { - state = AsyncValue.data( - curState.copyWith(stepCursor: curState.stepCursor + 1), - ); + state = AsyncValue.data(curState.copyWith(stepCursor: curState.stepCursor + 1)); final san = curState.game.stepAt(curState.stepCursor + 1).sanMove?.san; if (san != null) { _playReplayMoveSound(san); @@ -138,9 +118,7 @@ class TvController extends _$TvController { if (state.hasValue) { final curState = state.requireValue; if (curState.stepCursor > 0) { - state = AsyncValue.data( - curState.copyWith(stepCursor: curState.stepCursor - 1), - ); + state = AsyncValue.data(curState.copyWith(stepCursor: curState.stepCursor - 1)); final san = curState.game.stepAt(curState.stepCursor - 1).sanMove?.san; if (san != null) { _playReplayMoveSound(san); @@ -181,10 +159,7 @@ class TvController extends _$TvController { void _handleSocketTopic(SocketEvent event) { if (!state.hasValue) { - assert( - false, - 'received a game SocketEvent while TvState is null', - ); + assert(false, 'received a game SocketEvent while TvState is null'); return; } @@ -203,9 +178,7 @@ class TvController extends _$TvController { ); TvState newState = curState.copyWith( - game: curState.game.copyWith( - steps: curState.game.steps.add(newStep), - ), + game: curState.game.copyWith(steps: curState.game.steps.add(newStep)), ); if (newState.game.clock != null && data.clock != null) { @@ -217,9 +190,7 @@ class TvController extends _$TvController { ); } if (!curState.isReplaying) { - newState = newState.copyWith( - stepCursor: newState.stepCursor + 1, - ); + newState = newState.copyWith(stepCursor: newState.stepCursor + 1); if (data.san.contains('x')) { _soundService.play(Sound.capture); @@ -231,13 +202,9 @@ class TvController extends _$TvController { state = AsyncData(newState); case 'endData': - final endData = - GameEndEvent.fromJson(event.data as Map); + final endData = GameEndEvent.fromJson(event.data as Map); TvState newState = state.requireValue.copyWith( - game: state.requireValue.game.copyWith( - status: endData.status, - winner: endData.winner, - ), + game: state.requireValue.game.copyWith(status: endData.status, winner: endData.winner), ); if (endData.clock != null) { newState = newState.copyWith.game.clock!( diff --git a/lib/src/model/tv/tv_repository.dart b/lib/src/model/tv/tv_repository.dart index aaff5f5914..480c1c721d 100644 --- a/lib/src/model/tv/tv_repository.dart +++ b/lib/src/model/tv/tv_repository.dart @@ -17,10 +17,7 @@ class TvRepository { final http.Client client; Future channels() { - return client.readJson( - Uri(path: '/api/tv/channels'), - mapper: _tvGamesFromJson, - ); + return client.readJson(Uri(path: '/api/tv/channels'), mapper: _tvGamesFromJson); } } @@ -33,12 +30,11 @@ TvChannels _tvGamesFromJson(Map json) { }); } -TvGame _tvGameFromJson(Map json) => - _tvGameFromPick(pick(json).required()); +TvGame _tvGameFromJson(Map json) => _tvGameFromPick(pick(json).required()); TvGame _tvGameFromPick(RequiredPick pick) => TvGame( - user: pick('user').asLightUserOrThrow(), - rating: pick('rating').asIntOrNull(), - id: pick('gameId').asGameIdOrThrow(), - side: pick('color').asSideOrNull(), - ); + user: pick('user').asLightUserOrThrow(), + rating: pick('rating').asIntOrNull(), + id: pick('gameId').asGameIdOrThrow(), + side: pick('color').asSideOrNull(), +); diff --git a/lib/src/model/user/leaderboard.dart b/lib/src/model/user/leaderboard.dart index dc4b73d822..1df7f17760 100644 --- a/lib/src/model/user/leaderboard.dart +++ b/lib/src/model/user/leaderboard.dart @@ -39,11 +39,6 @@ class LeaderboardUser with _$LeaderboardUser { required int progress, }) = _LeaderboardUser; - LightUser get lightUser => LightUser( - id: id, - name: username, - title: title, - flair: flair, - isPatron: patron, - ); + LightUser get lightUser => + LightUser(id: id, name: username, title: title, flair: flair, isPatron: patron); } diff --git a/lib/src/model/user/profile.dart b/lib/src/model/user/profile.dart index 20ae4ace7f..e737faa535 100644 --- a/lib/src/model/user/profile.dart +++ b/lib/src/model/user/profile.dart @@ -28,55 +28,49 @@ class Profile with _$Profile { factory Profile.fromPick(RequiredPick pick) { const lineSplitter = LineSplitter(); - final rawLinks = pick('links') - .letOrNull((e) => lineSplitter.convert(e.asStringOrThrow())); + final rawLinks = pick('links').letOrNull((e) => lineSplitter.convert(e.asStringOrThrow())); return Profile( - country: - pick('flag').asStringOrNull() ?? pick('country').asStringOrNull(), + country: pick('flag').asStringOrNull() ?? pick('country').asStringOrNull(), location: pick('location').asStringOrNull(), bio: pick('bio').asStringOrNull(), realName: pick('realName').asStringOrNull(), fideRating: pick('fideRating').asIntOrNull(), uscfRating: pick('uscfRating').asIntOrNull(), ecfRating: pick('ecfRating').asIntOrNull(), - links: rawLinks - ?.where((e) => e.trim().isNotEmpty) - .map((e) { - final link = SocialLink.fromUrl(e); - if (link == null) { - final uri = Uri.tryParse(e); - if (uri != null) { - return SocialLink(site: null, url: uri); - } - return null; - } - return link; - }) - .nonNulls - .toIList(), + links: + rawLinks + ?.where((e) => e.trim().isNotEmpty) + .map((e) { + final link = SocialLink.fromUrl(e); + if (link == null) { + final uri = Uri.tryParse(e); + if (uri != null) { + return SocialLink(site: null, url: uri); + } + return null; + } + return link; + }) + .nonNulls + .toIList(), ); } } @freezed class SocialLink with _$SocialLink { - const factory SocialLink({ - required LinkSite? site, - required Uri url, - }) = _SocialLink; + const factory SocialLink({required LinkSite? site, required Uri url}) = _SocialLink; const SocialLink._(); static SocialLink? fromUrl(String url) { - final updatedUrl = url.startsWith('http://') || url.startsWith('https://') - ? url - : 'https://$url'; + final updatedUrl = + url.startsWith('http://') || url.startsWith('https://') ? url : 'https://$url'; final uri = Uri.tryParse(updatedUrl); if (uri == null) return null; final host = uri.host.replaceAll(RegExp(r'www\.'), ''); - final site = - LinkSite.values.firstWhereOrNull((e) => e.domains.contains(host)); + final site = LinkSite.values.firstWhereOrNull((e) => e.domains.contains(host)); return site != null ? SocialLink(site: site, url: uri) : null; } diff --git a/lib/src/model/user/search_history.dart b/lib/src/model/user/search_history.dart index 52f2047cd4..70f5e6b6a8 100644 --- a/lib/src/model/user/search_history.dart +++ b/lib/src/model/user/search_history.dart @@ -17,8 +17,7 @@ class SearchHistory extends _$SearchHistory { String _storageKey(AuthSessionState? session) => 'search.history.${session?.user.id ?? '**anon**'}'; - SharedPreferencesWithCache get _prefs => - LichessBinding.instance.sharedPreferences; + SharedPreferencesWithCache get _prefs => LichessBinding.instance.sharedPreferences; @override SearchHistoryState build() { @@ -26,9 +25,7 @@ class SearchHistory extends _$SearchHistory { final stored = _prefs.getString(_storageKey(session)); return stored != null - ? SearchHistoryState.fromJson( - jsonDecode(stored) as Map, - ) + ? SearchHistoryState.fromJson(jsonDecode(stored) as Map) : SearchHistoryState(history: IList()); } @@ -57,9 +54,7 @@ class SearchHistory extends _$SearchHistory { @Freezed(fromJson: true, toJson: true) class SearchHistoryState with _$SearchHistoryState { - const factory SearchHistoryState({ - required IList history, - }) = _SearchHistoryState; + const factory SearchHistoryState({required IList history}) = _SearchHistoryState; factory SearchHistoryState.fromJson(Map json) => _$SearchHistoryStateFromJson(json); diff --git a/lib/src/model/user/user.dart b/lib/src/model/user/user.dart index 1f71c8bf9a..4df1287c7c 100644 --- a/lib/src/model/user/user.dart +++ b/lib/src/model/user/user.dart @@ -22,8 +22,7 @@ class LightUser with _$LightUser { bool? isOnline, }) = _LightUser; - factory LightUser.fromJson(Map json) => - _$LightUserFromJson(json); + factory LightUser.fromJson(Map json) => _$LightUserFromJson(json); } extension LightUserExtension on Pick { @@ -34,8 +33,8 @@ extension LightUserExtension on Pick { return value; } if (value is Map) { - final name = requiredPick('username').asStringOrNull() ?? - requiredPick('name').asStringOrThrow(); + final name = + requiredPick('username').asStringOrNull() ?? requiredPick('name').asStringOrThrow(); return LightUser( id: requiredPick('id').asUserIdOrThrow(), @@ -46,9 +45,7 @@ extension LightUserExtension on Pick { isOnline: requiredPick('online').asBoolOrNull(), ); } - throw PickException( - "value $value at $debugParsingExit can't be casted to LightUser", - ); + throw PickException("value $value at $debugParsingExit can't be casted to LightUser"); } LightUser? asLightUserOrNull() { @@ -88,20 +85,13 @@ class User with _$User { bool? canChallenge, }) = _User; - LightUser get lightUser => LightUser( - id: id, - name: username, - title: title, - isPatron: isPatron, - flair: flair, - ); + LightUser get lightUser => + LightUser(id: id, name: username, title: title, isPatron: isPatron, flair: flair); - factory User.fromServerJson(Map json) => - User.fromPick(pick(json).required()); + factory User.fromServerJson(Map json) => User.fromPick(pick(json).required()); factory User.fromPick(RequiredPick pick) { - final receivedPerfsMap = - pick('perfs').asMapOrEmpty>(); + final receivedPerfsMap = pick('perfs').asMapOrEmpty>(); return User( id: pick('id').asUserIdOrThrow(), username: pick('username').asStringOrThrow(), @@ -155,32 +145,28 @@ class UserGameCount with _$UserGameCount { UserGameCount.fromPick(pick(json).required()); factory UserGameCount.fromPick(RequiredPick pick) => UserGameCount( - all: pick('all').asIntOrThrow(), - // TODO(#454): enable rest of fields when needed for filtering - // rated: pick('rated').asIntOrThrow(), - // ai: pick('ai').asIntOrThrow(), - // draw: pick('draw').asIntOrThrow(), - // drawH: pick('drawH').asIntOrThrow(), - // win: pick('win').asIntOrThrow(), - // winH: pick('winH').asIntOrThrow(), - // loss: pick('loss').asIntOrThrow(), - // lossH: pick('lossH').asIntOrThrow(), - // bookmark: pick('bookmark').asIntOrThrow(), - // playing: pick('playing').asIntOrThrow(), - // imported: pick('import').asIntOrThrow(), - // me: pick('me').asIntOrThrow(), - ); + all: pick('all').asIntOrThrow(), + // TODO(#454): enable rest of fields when needed for filtering + // rated: pick('rated').asIntOrThrow(), + // ai: pick('ai').asIntOrThrow(), + // draw: pick('draw').asIntOrThrow(), + // drawH: pick('drawH').asIntOrThrow(), + // win: pick('win').asIntOrThrow(), + // winH: pick('winH').asIntOrThrow(), + // loss: pick('loss').asIntOrThrow(), + // lossH: pick('lossH').asIntOrThrow(), + // bookmark: pick('bookmark').asIntOrThrow(), + // playing: pick('playing').asIntOrThrow(), + // imported: pick('import').asIntOrThrow(), + // me: pick('me').asIntOrThrow(), + ); } @freezed class PlayTime with _$PlayTime { - const factory PlayTime({ - required Duration total, - required Duration tv, - }) = _PlayTime; + const factory PlayTime({required Duration total, required Duration tv}) = _PlayTime; - factory PlayTime.fromJson(Map json) => - PlayTime.fromPick(pick(json).required()); + factory PlayTime.fromJson(Map json) => PlayTime.fromPick(pick(json).required()); factory PlayTime.fromPick(RequiredPick pick) { return PlayTime( @@ -203,25 +189,24 @@ class UserPerf with _$UserPerf { bool? provisional, }) = _UserPerf; - factory UserPerf.fromJson(Map json) => - UserPerf.fromPick(pick(json).required()); + factory UserPerf.fromJson(Map json) => UserPerf.fromPick(pick(json).required()); factory UserPerf.fromPick(RequiredPick pick) => UserPerf( - rating: pick('rating').asIntOrThrow(), - ratingDeviation: pick('rd').asIntOrThrow(), - progression: pick('prog').asIntOrThrow(), - games: pick('games').asIntOrNull(), - runs: pick('runs').asIntOrNull(), - provisional: pick('prov').asBoolOrNull(), - ); + rating: pick('rating').asIntOrThrow(), + ratingDeviation: pick('rd').asIntOrThrow(), + progression: pick('prog').asIntOrThrow(), + games: pick('games').asIntOrNull(), + runs: pick('runs').asIntOrNull(), + provisional: pick('prov').asBoolOrNull(), + ); factory UserPerf.fromJsonStreak(Map json) => UserPerf( - rating: UserActivityStreak.fromJson(json).score, - ratingDeviation: 0, - progression: 0, - runs: UserActivityStreak.fromJson(json).runs, - provisional: null, - ); + rating: UserActivityStreak.fromJson(json).score, + ratingDeviation: 0, + progression: 0, + runs: UserActivityStreak.fromJson(json).runs, + provisional: null, + ); int get numberOfGamesOrRuns => games ?? runs ?? 0; } @@ -239,11 +224,11 @@ class UserStatus with _$UserStatus { UserStatus.fromPick(pick(json).required()); factory UserStatus.fromPick(RequiredPick pick) => UserStatus( - id: pick('id').asUserIdOrThrow(), - name: pick('name').asStringOrThrow(), - online: pick('online').asBoolOrNull(), - playing: pick('playing').asBoolOrNull(), - ); + id: pick('id').asUserIdOrThrow(), + name: pick('name').asStringOrThrow(), + online: pick('online').asBoolOrNull(), + playing: pick('playing').asBoolOrNull(), + ); } @freezed @@ -260,31 +245,25 @@ class UserActivityTournament with _$UserActivityTournament { factory UserActivityTournament.fromJson(Map json) => UserActivityTournament.fromPick(pick(json).required()); - factory UserActivityTournament.fromPick(RequiredPick pick) => - UserActivityTournament( - id: pick('tournament', 'id').asStringOrThrow(), - name: pick('tournament', 'name').asStringOrThrow(), - nbGames: pick('nbGames').asIntOrThrow(), - score: pick('score').asIntOrThrow(), - rank: pick('rank').asIntOrThrow(), - rankPercent: pick('rankPercent').asIntOrThrow(), - ); + factory UserActivityTournament.fromPick(RequiredPick pick) => UserActivityTournament( + id: pick('tournament', 'id').asStringOrThrow(), + name: pick('tournament', 'name').asStringOrThrow(), + nbGames: pick('nbGames').asIntOrThrow(), + score: pick('score').asIntOrThrow(), + rank: pick('rank').asIntOrThrow(), + rankPercent: pick('rankPercent').asIntOrThrow(), + ); } @freezed class UserActivityStreak with _$UserActivityStreak { - const factory UserActivityStreak({ - required int runs, - required int score, - }) = _UserActivityStreak; + const factory UserActivityStreak({required int runs, required int score}) = _UserActivityStreak; factory UserActivityStreak.fromJson(Map json) => UserActivityStreak.fromPick(pick(json).required()); - factory UserActivityStreak.fromPick(RequiredPick pick) => UserActivityStreak( - runs: pick('runs').asIntOrThrow(), - score: pick('score').asIntOrThrow(), - ); + factory UserActivityStreak.fromPick(RequiredPick pick) => + UserActivityStreak(runs: pick('runs').asIntOrThrow(), score: pick('score').asIntOrThrow()); } @freezed @@ -301,12 +280,12 @@ class UserActivityScore with _$UserActivityScore { UserActivityScore.fromPick(pick(json).required()); factory UserActivityScore.fromPick(RequiredPick pick) => UserActivityScore( - win: pick('win').asIntOrThrow(), - loss: pick('loss').asIntOrThrow(), - draw: pick('draw').asIntOrThrow(), - ratingBefore: pick('rp', 'before').asIntOrThrow(), - ratingAfter: pick('rp', 'after').asIntOrThrow(), - ); + win: pick('win').asIntOrThrow(), + loss: pick('loss').asIntOrThrow(), + draw: pick('draw').asIntOrThrow(), + ratingBefore: pick('rp', 'before').asIntOrThrow(), + ratingAfter: pick('rp', 'after').asIntOrThrow(), + ); } @freezed @@ -410,13 +389,10 @@ class UserPerfGame with _$UserPerfGame { String? opponentTitle, }) = _UserPerfGame; - LightUser? get opponent => opponentId != null && opponentName != null - ? LightUser( - id: UserId(opponentId!), - name: opponentName!, - title: opponentTitle, - ) - : null; + LightUser? get opponent => + opponentId != null && opponentName != null + ? LightUser(id: UserId(opponentId!), name: opponentName!, title: opponentTitle) + : null; } @immutable @@ -424,10 +400,7 @@ class UserRatingHistoryPerf { final Perf perf; final IList points; - const UserRatingHistoryPerf({ - required this.perf, - required this.points, - }); + const UserRatingHistoryPerf({required this.perf, required this.points}); } @immutable @@ -435,8 +408,5 @@ class UserRatingHistoryPoint { final DateTime date; final int elo; - const UserRatingHistoryPoint({ - required this.date, - required this.elo, - }); + const UserRatingHistoryPoint({required this.date, required this.elo}); } diff --git a/lib/src/model/user/user_repository.dart b/lib/src/model/user/user_repository.dart index 858ff76695..0fc6598b88 100644 --- a/lib/src/model/user/user_repository.dart +++ b/lib/src/model/user/user_repository.dart @@ -17,19 +17,13 @@ class UserRepository { Future getUser(UserId id, {bool withCanChallenge = false}) { return client.readJson( - Uri( - path: '/api/user/$id', - queryParameters: withCanChallenge ? {'challenge': 'true'} : null, - ), + Uri(path: '/api/user/$id', queryParameters: withCanChallenge ? {'challenge': 'true'} : null), mapper: User.fromServerJson, ); } Future> getOnlineBots() { - return client.readNdJsonList( - Uri(path: '/api/bot/online'), - mapper: User.fromServerJson, - ); + return client.readNdJsonList(Uri(path: '/api/bot/online'), mapper: User.fromServerJson); } Future getPerfStats(UserId id, Perf perf) { @@ -41,40 +35,25 @@ class UserRepository { Future> getUsersStatuses(ISet ids) { return client.readJsonList( - Uri( - path: '/api/users/status', - queryParameters: {'ids': ids.join(',')}, - ), + Uri(path: '/api/users/status', queryParameters: {'ids': ids.join(',')}), mapper: UserStatus.fromJson, ); } Future> getActivity(UserId id) { - return client.readJsonList( - Uri(path: '/api/user/$id/activity'), - mapper: _userActivityFromJson, - ); + return client.readJsonList(Uri(path: '/api/user/$id/activity'), mapper: _userActivityFromJson); } Future> getLiveStreamers() { - return client.readJsonList( - Uri(path: '/api/streamer/live'), - mapper: _streamersFromJson, - ); + return client.readJsonList(Uri(path: '/api/streamer/live'), mapper: _streamersFromJson); } Future> getTop1() { - return client.readJson( - Uri(path: '/api/player/top/1/standard'), - mapper: _top1FromJson, - ); + return client.readJson(Uri(path: '/api/player/top/1/standard'), mapper: _top1FromJson); } Future getLeaderboard() { - return client.readJson( - Uri(path: '/api/player'), - mapper: _leaderboardFromJson, - ); + return client.readJson(Uri(path: '/api/player'), mapper: _leaderboardFromJson); } Future> autocompleteUser(String term) { @@ -95,14 +74,10 @@ class UserRepository { } } -UserRatingHistoryPerf? _ratingHistoryFromJson( - Map json, -) => +UserRatingHistoryPerf? _ratingHistoryFromJson(Map json) => _ratingHistoryFromPick(pick(json).required()); -UserRatingHistoryPerf? _ratingHistoryFromPick( - RequiredPick pick, -) { +UserRatingHistoryPerf? _ratingHistoryFromPick(RequiredPick pick) { final perf = pick('name').asPerfOrNull(); if (perf == null) { @@ -111,17 +86,14 @@ UserRatingHistoryPerf? _ratingHistoryFromPick( return UserRatingHistoryPerf( perf: perf, - points: pick('points').asListOrThrow((point) { - final values = point.asListOrThrow((point) => point.asIntOrThrow()); - return UserRatingHistoryPoint( - date: DateTime.utc( - values[0], - values[1] + 1, - values[2], - ), - elo: values[3], - ); - }).toIList(), + points: + pick('points').asListOrThrow((point) { + final values = point.asListOrThrow((point) => point.asIntOrThrow()); + return UserRatingHistoryPoint( + date: DateTime.utc(values[0], values[1] + 1, values[2]), + elo: values[3], + ); + }).toIList(), ); } @@ -130,17 +102,14 @@ IList _autocompleteFromJson(Map json) => _autocompleteFromPick(pick(json).required()); IList _autocompleteFromPick(RequiredPick pick) { - return pick('result') - .asListOrThrow((userPick) => userPick.asLightUserOrThrow()) - .toIList(); + return pick('result').asListOrThrow((userPick) => userPick.asLightUserOrThrow()).toIList(); } UserActivity _userActivityFromJson(Map json) => _userActivityFromPick(pick(json).required()); UserActivity _userActivityFromPick(RequiredPick pick) { - final receivedGamesMap = - pick('games').asMapOrEmpty>(); + final receivedGamesMap = pick('games').asMapOrEmpty>(); final games = IMap({ for (final entry in receivedGamesMap.entries) @@ -148,8 +117,10 @@ UserActivity _userActivityFromPick(RequiredPick pick) { Perf.nameMap.get(entry.key)!: UserActivityScore.fromJson(entry.value), }); - final bestTour = pick('tournaments', 'best') - .asListOrNull((p0) => UserActivityTournament.fromPick(p0)); + final bestTour = pick( + 'tournaments', + 'best', + ).asListOrNull((p0) => UserActivityTournament.fromPick(p0)); return UserActivity( startTime: pick('interval', 'start').asDateTimeFromMillisecondsOrThrow(), @@ -162,12 +133,10 @@ UserActivity _userActivityFromPick(RequiredPick pick) { puzzles: pick('puzzles', 'score').letOrNull(UserActivityScore.fromPick), streak: pick('streak').letOrNull(UserActivityStreak.fromPick), storm: pick('storm').letOrNull(UserActivityStreak.fromPick), - correspondenceEnds: pick('correspondenceEnds', 'score') - .letOrNull(UserActivityScore.fromPick), + correspondenceEnds: pick('correspondenceEnds', 'score').letOrNull(UserActivityScore.fromPick), correspondenceMovesNb: pick('correspondenceMoves', 'nb').asIntOrNull(), - correspondenceGamesNb: pick('correspondenceMoves', 'games') - .asListOrNull((p) => p('id').asStringOrThrow()) - ?.length, + correspondenceGamesNb: + pick('correspondenceMoves', 'games').asListOrNull((p) => p('id').asStringOrThrow())?.length, ); } @@ -214,11 +183,8 @@ UserPerfStats _userPerfStatsFromPick(RequiredPick pick) { maxPlayStreak: playStreak('nb', 'max').letOrNull(_userStreakFromPick), curTimeStreak: playStreak('time', 'cur').letOrNull(_userStreakFromPick), maxTimeStreak: playStreak('time', 'max').letOrNull(_userStreakFromPick), - worstLosses: IList( - stat('worstLosses', 'results').asListOrNull(_userPerfGameFromPick), - ), - bestWins: - IList(stat('bestWins', 'results').asListOrNull(_userPerfGameFromPick)), + worstLosses: IList(stat('worstLosses', 'results').asListOrNull(_userPerfGameFromPick)), + bestWins: IList(stat('bestWins', 'results').asListOrNull(_userPerfGameFromPick)), ); } @@ -272,8 +238,7 @@ UserPerfGame _userPerfGameFromPick(RequiredPick pick) { ); } -Streamer _streamersFromJson(Map json) => - _streamersFromPick(pick(json).required()); +Streamer _streamersFromJson(Map json) => _streamersFromPick(pick(json).required()); Streamer _streamersFromPick(RequiredPick pick) { final stream = pick('stream'); @@ -306,8 +271,7 @@ Leaderboard _leaderBoardFromPick(RequiredPick pick) { ultrabullet: pick('ultraBullet').asListOrEmpty(_leaderboardUserFromPick), crazyhouse: pick('crazyhouse').asListOrEmpty(_leaderboardUserFromPick), chess960: pick('chess960').asListOrEmpty(_leaderboardUserFromPick), - kingOfThehill: - pick('kingOfTheHill').asListOrEmpty(_leaderboardUserFromPick), + kingOfThehill: pick('kingOfTheHill').asListOrEmpty(_leaderboardUserFromPick), threeCheck: pick('threeCheck').asListOrEmpty(_leaderboardUserFromPick), antichess: pick('antichess').asListOrEmpty(_leaderboardUserFromPick), atomic: pick('atomic').asListOrEmpty(_leaderboardUserFromPick), @@ -326,14 +290,14 @@ LeaderboardUser _leaderboardUserFromPick(RequiredPick pick) { flair: pick('flair').asStringOrNull(), patron: pick('patron').asBoolOrNull(), online: pick('online').asBoolOrNull(), - rating: pick('perfs') - .letOrThrow((perfsPick) => perfsPick(prefMap.keys.first, 'rating')) - .asIntOrThrow(), - progress: pick('perfs') - .letOrThrow( - (prefsPick) => prefsPick(prefMap.keys.first, 'progress'), - ) - .asIntOrThrow(), + rating: + pick( + 'perfs', + ).letOrThrow((perfsPick) => perfsPick(prefMap.keys.first, 'rating')).asIntOrThrow(), + progress: + pick( + 'perfs', + ).letOrThrow((prefsPick) => prefsPick(prefMap.keys.first, 'progress')).asIntOrThrow(), ); } diff --git a/lib/src/model/user/user_repository_providers.dart b/lib/src/model/user/user_repository_providers.dart index a1221aa58a..c950b4d469 100644 --- a/lib/src/model/user/user_repository_providers.dart +++ b/lib/src/model/user/user_repository_providers.dart @@ -16,9 +16,7 @@ const _kAutoCompleteDebounceTimer = Duration(milliseconds: 300); @riverpod Future user(Ref ref, {required UserId id}) async { - return ref.withClient( - (client) => UserRepository(client).getUser(id), - ); + return ref.withClient((client) => UserRepository(client).getUser(id)); } @riverpod @@ -36,41 +34,23 @@ Future> userActivity(Ref ref, {required UserId id}) async { @riverpod Future<(User, UserStatus)> userAndStatus(Ref ref, {required UserId id}) async { - return ref.withClient( - (client) async { - final repo = UserRepository(client); - return Future.wait( - [ - repo.getUser(id, withCanChallenge: true), - repo.getUsersStatuses({id}.lock), - ], - eagerError: true, - ).then( - (value) => (value[0] as User, (value[1] as IList).first), - ); - }, - ); + return ref.withClient((client) async { + final repo = UserRepository(client); + return Future.wait([ + repo.getUser(id, withCanChallenge: true), + repo.getUsersStatuses({id}.lock), + ], eagerError: true).then((value) => (value[0] as User, (value[1] as IList).first)); + }); } @riverpod -Future userPerfStats( - Ref ref, { - required UserId id, - required Perf perf, -}) async { - return ref.withClient( - (client) => UserRepository(client).getPerfStats(id, perf), - ); +Future userPerfStats(Ref ref, {required UserId id, required Perf perf}) async { + return ref.withClient((client) => UserRepository(client).getPerfStats(id, perf)); } @riverpod -Future> userStatuses( - Ref ref, { - required ISet ids, -}) async { - return ref.withClient( - (client) => UserRepository(client).getUsersStatuses(ids), - ); +Future> userStatuses(Ref ref, {required ISet ids}) async { + return ref.withClient((client) => UserRepository(client).getUsersStatuses(ids)); } @riverpod @@ -107,16 +87,11 @@ Future> autoCompleteUser(Ref ref, String term) async { throw Exception('Cancelled'); } - return ref.withClient( - (client) => UserRepository(client).autocompleteUser(term), - ); + return ref.withClient((client) => UserRepository(client).autocompleteUser(term)); } @riverpod -Future> userRatingHistory( - Ref ref, { - required UserId id, -}) async { +Future> userRatingHistory(Ref ref, {required UserId id}) async { return ref.withClientCacheFor( (client) => UserRepository(client).getRatingHistory(id), const Duration(minutes: 1), diff --git a/lib/src/navigation.dart b/lib/src/navigation.dart index be4c4b8581..26ff184dd6 100644 --- a/lib/src/navigation.dart +++ b/lib/src/navigation.dart @@ -64,8 +64,7 @@ enum BottomTab { } } -final currentBottomTabProvider = - StateProvider((ref) => BottomTab.home); +final currentBottomTabProvider = StateProvider((ref) => BottomTab.home); final currentNavigatorKeyProvider = Provider>((ref) { final currentTab = ref.watch(currentBottomTabProvider); @@ -111,8 +110,7 @@ final toolsScrollController = ScrollController(debugLabel: 'ToolsScroll'); final watchScrollController = ScrollController(debugLabel: 'WatchScroll'); final settingsScrollController = ScrollController(debugLabel: 'SettingsScroll'); -final RouteObserver> rootNavPageRouteObserver = - RouteObserver>(); +final RouteObserver> rootNavPageRouteObserver = RouteObserver>(); final _cupertinoTabController = CupertinoTabController(); @@ -132,17 +130,10 @@ class BottomNavScaffold extends ConsumerWidget { switch (Theme.of(context).platform) { case TargetPlatform.android: return Scaffold( - body: _TabSwitchingView( - currentTab: currentTab, - tabBuilder: _androidTabBuilder, - ), + body: _TabSwitchingView(currentTab: currentTab, tabBuilder: _androidTabBuilder), bottomNavigationBar: Consumer( builder: (context, ref, _) { - final isOnline = ref - .watch(connectivityChangesProvider) - .valueOrNull - ?.isOnline ?? - true; + final isOnline = ref.watch(connectivityChangesProvider).valueOrNull?.isOnline ?? true; return NavigationBar( selectedIndex: currentTab.index, destinations: [ @@ -152,16 +143,13 @@ class BottomNavScaffold extends ConsumerWidget { label: tab.label(context.l10n), ), ], - onDestinationSelected: (i) => - _onItemTapped(ref, i, isOnline: isOnline), + onDestinationSelected: (i) => _onItemTapped(ref, i, isOnline: isOnline), ); }, ), ); case TargetPlatform.iOS: - final isOnline = - ref.watch(connectivityChangesProvider).valueOrNull?.isOnline ?? - true; + final isOnline = ref.watch(connectivityChangesProvider).valueOrNull?.isOnline ?? true; return CupertinoTabScaffold( tabBuilder: _iOSTabBuilder, controller: _cupertinoTabController, @@ -191,11 +179,7 @@ class BottomNavScaffold extends ConsumerWidget { void _onItemTapped(WidgetRef ref, int index, {required bool isOnline}) { if (index == BottomTab.watch.index && !isOnline) { _cupertinoTabController.index = ref.read(currentBottomTabProvider).index; - showPlatformSnackbar( - ref.context, - 'Not available in offline mode', - type: SnackBarType.info, - ); + showPlatformSnackbar(ref.context, 'Not available in offline mode', type: SnackBarType.info); return; } @@ -306,10 +290,7 @@ Widget _iOSTabBuilder(BuildContext context, int index) { /// A widget laying out multiple tabs with only one active tab being built /// at a time and on stage. Off stage tabs' animations are stopped. class _TabSwitchingView extends StatefulWidget { - const _TabSwitchingView({ - required this.currentTab, - required this.tabBuilder, - }); + const _TabSwitchingView({required this.currentTab, required this.tabBuilder}); final BottomTab currentTab; final IndexedWidgetBuilder tabBuilder; @@ -352,24 +333,19 @@ class _TabSwitchingViewState extends State<_TabSwitchingView> { if (tabFocusNodes.length != BottomTab.values.length) { if (tabFocusNodes.length > BottomTab.values.length) { discardedNodes.addAll(tabFocusNodes.sublist(BottomTab.values.length)); - tabFocusNodes.removeRange( - BottomTab.values.length, - tabFocusNodes.length, - ); + tabFocusNodes.removeRange(BottomTab.values.length, tabFocusNodes.length); } else { tabFocusNodes.addAll( List.generate( BottomTab.values.length - tabFocusNodes.length, (int index) => FocusScopeNode( - debugLabel: - '$BottomNavScaffold Tab ${index + tabFocusNodes.length}', + debugLabel: '$BottomNavScaffold Tab ${index + tabFocusNodes.length}', ), ), ); } } - FocusScope.of(context) - .setFirstFocus(tabFocusNodes[widget.currentTab.index]); + FocusScope.of(context).setFirstFocus(tabFocusNodes[widget.currentTab.index]); } @override @@ -401,9 +377,7 @@ class _TabSwitchingViewState extends State<_TabSwitchingView> { node: tabFocusNodes[index], child: Builder( builder: (BuildContext context) { - return shouldBuildTab[index] - ? widget.tabBuilder(context, index) - : Container(); + return shouldBuildTab[index] ? widget.tabBuilder(context, index) : Container(); }, ), ), @@ -490,11 +464,12 @@ class _MaterialTabViewState extends ConsumerState<_MaterialTabView> { final currentTab = ref.watch(currentBottomTabProvider); final enablePopHandler = currentTab == widget.tab; return NavigatorPopHandler( - onPopWithResult: enablePopHandler - ? (_) { - widget.navigatorKey?.currentState?.maybePop(); - } - : null, + onPopWithResult: + enablePopHandler + ? (_) { + widget.navigatorKey?.currentState?.maybePop(); + } + : null, enabled: enablePopHandler, child: Navigator( key: widget.navigatorKey, @@ -515,10 +490,7 @@ class _MaterialTabViewState extends ConsumerState<_MaterialTabView> { routeBuilder = widget.routes![name]; } if (routeBuilder != null) { - return MaterialPageRoute( - builder: routeBuilder, - settings: settings, - ); + return MaterialPageRoute(builder: routeBuilder, settings: settings); } if (widget.onGenerateRoute != null) { return widget.onGenerateRoute!(settings); diff --git a/lib/src/network/connectivity.dart b/lib/src/network/connectivity.dart index bf340e467a..f85dbce3cf 100644 --- a/lib/src/network/connectivity.dart +++ b/lib/src/network/connectivity.dart @@ -42,20 +42,15 @@ class ConnectivityChanges extends _$ConnectivityChanges { }); _connectivitySubscription?.cancel(); - _connectivitySubscription = - _connectivity.onConnectivityChanged.listen((result) { + _connectivitySubscription = _connectivity.onConnectivityChanged.listen((result) { _connectivityChangesDebouncer(() => _onConnectivityChange(result)); }); final AppLifecycleState? appState = WidgetsBinding.instance.lifecycleState; - _appLifecycleListener = AppLifecycleListener( - onStateChange: _onAppLifecycleChange, - ); + _appLifecycleListener = AppLifecycleListener(onStateChange: _onAppLifecycleChange); - return _connectivity - .checkConnectivity() - .then((r) => _getConnectivityStatus(r, appState)); + return _connectivity.checkConnectivity().then((r) => _getConnectivityStatus(r, appState)); } Future _onAppLifecycleChange(AppLifecycleState appState) async { @@ -64,9 +59,9 @@ class ConnectivityChanges extends _$ConnectivityChanges { } if (appState == AppLifecycleState.resumed) { - final newConn = await _connectivity - .checkConnectivity() - .then((r) => _getConnectivityStatus(r, appState)); + final newConn = await _connectivity.checkConnectivity().then( + (r) => _getConnectivityStatus(r, appState), + ); state = AsyncValue.data(newConn); } else { @@ -88,12 +83,7 @@ class ConnectivityChanges extends _$ConnectivityChanges { if (newIsOnline != wasOnline) { _logger.info('Connectivity status: $result, isOnline: $isOnline'); - state = AsyncValue.data( - ( - isOnline: newIsOnline, - appState: state.valueOrNull?.appState, - ), - ); + state = AsyncValue.data((isOnline: newIsOnline, appState: state.valueOrNull?.appState)); } } @@ -101,19 +91,13 @@ class ConnectivityChanges extends _$ConnectivityChanges { List result, AppLifecycleState? appState, ) async { - final status = ( - isOnline: await isOnline(_defaultClient), - appState: appState, - ); + final status = (isOnline: await isOnline(_defaultClient), appState: appState); _logger.info('Connectivity status: $result, isOnline: ${status.isOnline}'); return status; } } -typedef ConnectivityStatus = ({ - bool isOnline, - AppLifecycleState? appState, -}); +typedef ConnectivityStatus = ({bool isOnline, AppLifecycleState? appState}); final _internetCheckUris = [ Uri.parse('https://www.gstatic.com/generate_204'), @@ -126,10 +110,10 @@ Future isOnline(Client client) { try { int remaining = _internetCheckUris.length; final futures = _internetCheckUris.map( - (uri) => client.head(uri).timeout(const Duration(seconds: 10)).then( - (response) => true, - onError: (_) => false, - ), + (uri) => client + .head(uri) + .timeout(const Duration(seconds: 10)) + .then((response) => true, onError: (_) => false), ); for (final future in futures) { future.then((value) { @@ -169,10 +153,7 @@ extension AsyncValueConnectivity on AsyncValue { /// offline: () => 'Offline', /// ); /// ``` - R whenIs({ - required R Function() online, - required R Function() offline, - }) { + R whenIs({required R Function() online, required R Function() offline}) { return maybeWhen( skipLoadingOnReload: true, data: (status) => status.isOnline ? online() : offline(), diff --git a/lib/src/network/http.dart b/lib/src/network/http.dart index 4c24ab6cb5..fb0c115f04 100644 --- a/lib/src/network/http.dart +++ b/lib/src/network/http.dart @@ -58,9 +58,10 @@ class HttpClientFactory { } if (Platform.isIOS || Platform.isMacOS) { - final config = URLSessionConfiguration.ephemeralSessionConfiguration() - ..cache = URLCache.withCapacity(memoryCapacity: _maxCacheSize) - ..httpAdditionalHeaders = {'User-Agent': userAgent}; + final config = + URLSessionConfiguration.ephemeralSessionConfiguration() + ..cache = URLCache.withCapacity(memoryCapacity: _maxCacheSize) + ..httpAdditionalHeaders = {'User-Agent': userAgent}; return CupertinoClient.fromSessionConfiguration(config); } @@ -118,14 +119,8 @@ String userAgent(Ref ref) { } /// Creates a user-agent string with the app version, build number, and device info and possibly the user ID if a user is logged in. -String makeUserAgent( - PackageInfo info, - BaseDeviceInfo deviceInfo, - String sri, - LightUser? user, -) { - final base = - 'Lichess Mobile/${info.version} as:${user?.id ?? 'anon'} sri:$sri'; +String makeUserAgent(PackageInfo info, BaseDeviceInfo deviceInfo, String sri, LightUser? user) { + final base = 'Lichess Mobile/${info.version} as:${user?.id ?? 'anon'} sri:$sri'; if (deviceInfo is AndroidDeviceInfo) { return '$base os:Android/${deviceInfo.version.release} dev:${deviceInfo.model}'; @@ -180,9 +175,7 @@ class LichessClient implements Client { session?.user, ); - _logger.info( - '${request.method} ${request.url} ${request.headers['User-Agent']}', - ); + _logger.info('${request.method} ${request.url} ${request.headers['User-Agent']}'); try { final response = await _inner.send(request); @@ -204,11 +197,7 @@ class LichessClient implements Client { Future _checkSessionToken(AuthSessionState session) async { final defaultClient = _ref.read(defaultClientProvider); final data = await defaultClient - .postReadJson( - lichessUri('/api/token/test'), - mapper: (json) => json, - body: session.token, - ) + .postReadJson(lichessUri('/api/token/test'), mapper: (json) => json, body: session.token) .timeout(const Duration(seconds: 5)); if (data[session.token] == null) { _logger.fine('Session is not active. Deleting it.'); @@ -233,17 +222,11 @@ class LichessClient implements Client { } @override - Future head( - Uri url, { - Map? headers, - }) => + Future head(Uri url, {Map? headers}) => _sendUnstreamed('HEAD', url, headers); @override - Future get( - Uri url, { - Map? headers, - }) => + Future get(Uri url, {Map? headers}) => _sendUnstreamed('GET', url, headers); @override @@ -252,16 +235,10 @@ class LichessClient implements Client { Map? headers, Object? body, Encoding? encoding, - }) => - _sendUnstreamed('POST', url, headers, body, encoding); + }) => _sendUnstreamed('POST', url, headers, body, encoding); @override - Future put( - Uri url, { - Map? headers, - Object? body, - Encoding? encoding, - }) => + Future put(Uri url, {Map? headers, Object? body, Encoding? encoding}) => _sendUnstreamed('PUT', url, headers, body, encoding); @override @@ -270,8 +247,7 @@ class LichessClient implements Client { Map? headers, Object? body, Encoding? encoding, - }) => - _sendUnstreamed('PATCH', url, headers, body, encoding); + }) => _sendUnstreamed('PATCH', url, headers, body, encoding); @override Future delete( @@ -279,8 +255,7 @@ class LichessClient implements Client { Map? headers, Object? body, Encoding? encoding, - }) => - _sendUnstreamed('DELETE', url, headers, body, encoding); + }) => _sendUnstreamed('DELETE', url, headers, body, encoding); @override Future read(Uri url, {Map? headers}) async { @@ -332,12 +307,7 @@ class ServerException extends ClientException { final int statusCode; final Map? jsonError; - ServerException( - this.statusCode, - super.message, - Uri super.url, - this.jsonError, - ); + ServerException(this.statusCode, super.message, Uri super.url, this.jsonError); } /// Throws an error if [response] is not successful. @@ -387,19 +357,13 @@ extension ClientExtension on Client { final json = jsonUtf8Decoder.convert(response.bodyBytes); if (json is! Map) { _logger.severe('Could not read JSON object as $T: expected an object.'); - throw ClientException( - 'Could not read JSON object as $T: expected an object.', - url, - ); + throw ClientException('Could not read JSON object as $T: expected an object.', url); } try { return mapper(json); } catch (e, st) { _logger.severe('Could not read JSON object as $T: $e', e, st); - throw ClientException( - 'Could not read JSON object as $T: $e\n$st', - url, - ); + throw ClientException('Could not read JSON object as $T: $e\n$st', url); } } @@ -419,20 +383,14 @@ extension ClientExtension on Client { final json = jsonUtf8Decoder.convert(response.bodyBytes); if (json is! List) { _logger.severe('Could not read JSON object as List: expected a list.'); - throw ClientException( - 'Could not read JSON object as List: expected a list.', - url, - ); + throw ClientException('Could not read JSON object as List: expected a list.', url); } final List list = []; for (final e in json) { if (e is! Map) { _logger.severe('Could not read JSON object as $T: expected an object.'); - throw ClientException( - 'Could not read JSON object as $T: expected an object.', - url, - ); + throw ClientException('Could not read JSON object as $T: expected an object.', url); } try { final mapped = mapper(e); @@ -486,19 +444,13 @@ extension ClientExtension on Client { throw ServerException(response.statusCode, '$message.', url, null); } try { - return response.stream - .map(utf8.decode) - .where((e) => e.isNotEmpty && e != '\n') - .map((e) { + return response.stream.map(utf8.decode).where((e) => e.isNotEmpty && e != '\n').map((e) { final json = jsonDecode(e) as Map; return mapper(json); }); } catch (e) { _logger.severe('Could not read nd-json object as $T.'); - throw ClientException( - 'Could not read nd-json object as $T: $e', - url, - ); + throw ClientException('Could not read nd-json object as $T: $e', url); } } @@ -515,25 +467,18 @@ extension ClientExtension on Client { Encoding? encoding, required T Function(Map) mapper, }) async { - final response = - await post(url, headers: headers, body: body, encoding: encoding); + final response = await post(url, headers: headers, body: body, encoding: encoding); _checkResponseSuccess(url, response); final json = jsonUtf8Decoder.convert(response.bodyBytes); if (json is! Map) { _logger.severe('Could not read json object as $T: expected an object.'); - throw ClientException( - 'Could not read json object as $T: expected an object.', - url, - ); + throw ClientException('Could not read json object as $T: expected an object.', url); } try { return mapper(json); } catch (e, st) { _logger.severe('Could not read json as $T: $e', e, st); - throw ClientException( - 'Could not read json as $T: $e', - url, - ); + throw ClientException('Could not read json as $T: $e', url); } } @@ -550,21 +495,17 @@ extension ClientExtension on Client { Encoding? encoding, required T Function(Map) mapper, }) async { - final response = - await post(url, headers: headers, body: body, encoding: encoding); + final response = await post(url, headers: headers, body: body, encoding: encoding); _checkResponseSuccess(url, response); return _readNdJsonList(response, mapper); } - IList _readNdJsonList( - Response response, - T Function(Map) mapper, - ) { + IList _readNdJsonList(Response response, T Function(Map) mapper) { try { return IList( - LineSplitter.split(utf8.decode(response.bodyBytes)) - .where((e) => e.isNotEmpty && e != '\n') - .map((e) { + LineSplitter.split( + utf8.decode(response.bodyBytes), + ).where((e) => e.isNotEmpty && e != '\n').map((e) { final json = jsonDecode(e) as Map; return mapper(json); }), @@ -600,10 +541,7 @@ extension ClientRefExtension on Ref { /// /// If [fn] throws with a [SocketException], the provider is not kept alive, this /// allows to retry the request later. - Future withClientCacheFor( - Future Function(LichessClient) fn, - Duration duration, - ) async { + Future withClientCacheFor(Future Function(LichessClient) fn, Duration duration) async { final link = keepAlive(); final timer = Timer(duration, link.close); final client = read(lichessClientProvider); diff --git a/lib/src/network/socket.dart b/lib/src/network/socket.dart index fdb9d7eed0..93aacb93b2 100644 --- a/lib/src/network/socket.dart +++ b/lib/src/network/socket.dart @@ -34,11 +34,7 @@ const _kDisconnectOnBackgroundTimeout = Duration(minutes: 5); final _logger = Logger('Socket'); /// Set of topics that are allowed to be broadcasted to the global stream. -const _globalSocketStreamAllowedTopics = { - 'n', - 'message', - 'challenges', -}; +const _globalSocketStreamAllowedTopics = {'n', 'message', 'challenges'}; final _globalStreamController = StreamController.broadcast(); @@ -51,24 +47,21 @@ final _globalStreamController = StreamController.broadcast(); final socketGlobalStream = _globalStreamController.stream; /// Creates a WebSocket URI for the lichess server. -Uri lichessWSUri( - String unencodedPath, [ - Map? queryParameters, -]) => +Uri lichessWSUri(String unencodedPath, [Map? queryParameters]) => kLichessWSHost.startsWith('localhost') ? Uri( - scheme: 'ws', - host: kLichessWSHost.split(':')[0], - port: int.parse(kLichessWSHost.split(':')[1]), - path: unencodedPath, - queryParameters: queryParameters, - ) + scheme: 'ws', + host: kLichessWSHost.split(':')[0], + port: int.parse(kLichessWSHost.split(':')[1]), + path: unencodedPath, + queryParameters: queryParameters, + ) : Uri( - scheme: 'wss', - host: kLichessWSHost, - path: unencodedPath, - queryParameters: queryParameters, - ); + scheme: 'wss', + host: kLichessWSHost, + path: unencodedPath, + queryParameters: queryParameters, + ); /// A lichess WebSocket client. /// @@ -127,13 +120,9 @@ class SocketClient { final VoidCallback? onStreamCancel; late final StreamController _streamController = - StreamController.broadcast( - onListen: onStreamListen, - onCancel: onStreamCancel, - ); + StreamController.broadcast(onListen: onStreamListen, onCancel: onStreamCancel); - late final StreamController _socketOpenController = - StreamController.broadcast(); + late final StreamController _socketOpenController = StreamController.broadcast(); Completer _firstConnection = Completer(); @@ -203,13 +192,9 @@ class SocketClient { final session = getSession(); final uri = lichessWSUri(route.path); - final Map headers = session != null - ? { - 'Authorization': 'Bearer ${signBearerToken(session.token)}', - } - : {}; - WebSocket.userAgent = - makeUserAgent(packageInfo, deviceInfo, sri, session?.user); + final Map headers = + session != null ? {'Authorization': 'Bearer ${signBearerToken(session.token)}'} : {}; + WebSocket.userAgent = makeUserAgent(packageInfo, deviceInfo, sri, session?.user); _logger.info('Creating WebSocket connection to $route'); @@ -225,14 +210,14 @@ class SocketClient { _channel = channel; _socketStreamSubscription?.cancel(); - _socketStreamSubscription = channel.stream.map((raw) { - if (raw == '0') { - return SocketEvent.pong; - } - return SocketEvent.fromJson( - jsonDecode(raw as String) as Map, - ); - }).listen(_handleEvent); + _socketStreamSubscription = channel.stream + .map((raw) { + if (raw == '0') { + return SocketEvent.pong; + } + return SocketEvent.fromJson(jsonDecode(raw as String) as Map); + }) + .listen(_handleEvent); _logger.fine('WebSocket connection to $route established.'); @@ -258,12 +243,7 @@ class SocketClient { } /// Sends a message to the websocket. - void send( - String topic, - Object? data, { - bool? ackable, - bool? withLag, - }) { + void send(String topic, Object? data, {bool? ackable, bool? withLag}) { Map message; if (ackable == true) { @@ -281,10 +261,7 @@ class SocketClient { message = { 't': topic, if (data != null && data is Map) - 'd': { - ...data, - if (withLag == true) 'l': _averageLag.value.inMilliseconds, - } + 'd': {...data, if (withLag == true) 'l': _averageLag.value.inMilliseconds} else if (data != null) 'd': data, }; @@ -323,22 +300,23 @@ class SocketClient { /// /// Returns a [Future] that completes when the connection is closed. Future _disconnect() { - final future = _sink?.close().then((_) { - _logger.fine('WebSocket connection to $route was properly closed.'); - if (isDisposed) { - return; - } - _averageLag.value = Duration.zero; - }).catchError((Object? error) { - _logger.warning( - 'WebSocket connection to $route could not be closed: $error', - error, - ); - if (isDisposed) { - return; - } - _averageLag.value = Duration.zero; - }) ?? + final future = + _sink + ?.close() + .then((_) { + _logger.fine('WebSocket connection to $route was properly closed.'); + if (isDisposed) { + return; + } + _averageLag.value = Duration.zero; + }) + .catchError((Object? error) { + _logger.warning('WebSocket connection to $route could not be closed: $error', error); + if (isDisposed) { + return; + } + _averageLag.value = Duration.zero; + }) ?? Future.value(); _channel = null; _socketStreamSubscription?.cancel(); @@ -378,10 +356,7 @@ class SocketClient { void _sendPing() { _sink?.add( _pongCount % 10 == 2 - ? jsonEncode({ - 't': 'p', - 'l': (_averageLag.value.inMilliseconds * 0.1).round(), - }) + ? jsonEncode({'t': 'p', 'l': (_averageLag.value.inMilliseconds * 0.1).round()}) : 'p', ); _lastPing = DateTime.now(); @@ -396,8 +371,7 @@ class SocketClient { _schedulePing(pingDelay); _pongCount++; final currentLag = Duration( - milliseconds: - math.min(DateTime.now().difference(_lastPing).inMilliseconds, 10000), + milliseconds: math.min(DateTime.now().difference(_lastPing).inMilliseconds, 10000), ); // Average first 4 pings, then switch to decaying average. @@ -413,9 +387,7 @@ class SocketClient { _averageLag.value = Duration.zero; connect(); } else { - _logger.warning( - 'Scheduled reconnect after $delay failed since client is disposed.', - ); + _logger.warning('Scheduled reconnect after $delay failed since client is disposed.'); } }); } @@ -428,8 +400,7 @@ class SocketClient { } void _resendAcks() { - final resendCutoff = - DateTime.now().subtract(const Duration(milliseconds: 2500)); + final resendCutoff = DateTime.now().subtract(const Duration(milliseconds: 2500)); for (final (at, _, ack) in _acks) { if (at.isBefore(resendCutoff)) { _sink?.add(jsonEncode(ack)); @@ -452,10 +423,7 @@ class SocketClient { /// When a requested client is disposed, the pool will automatically reconnect /// the default client. class SocketPool { - SocketPool( - this._ref, { - this.idleTimeout = _kIdleTimeout, - }) { + SocketPool(this._ref, {this.idleTimeout = _kIdleTimeout}) { // Create a default socket client. This one is never disposed. final client = SocketClient( _currentRoute, @@ -503,10 +471,7 @@ class SocketPool { /// It will use an existing connection if it is already active, unless /// [forceReconnect] is set to true. /// Any other active connection will be closed. - SocketClient open( - Uri route, { - bool? forceReconnect, - }) { + SocketClient open(Uri route, {bool? forceReconnect}) { _currentRoute = route; if (_pool[route] == null) { @@ -579,15 +544,12 @@ SocketPool socketPool(Ref ref) { final appLifecycleListener = AppLifecycleListener( onHide: () { closeInBackgroundTimer?.cancel(); - closeInBackgroundTimer = Timer( - _kDisconnectOnBackgroundTimeout, - () { - _logger.info( - 'App is in background for ${_kDisconnectOnBackgroundTimeout.inMinutes}m, closing socket.', - ); - pool.currentClient.close(); - }, - ); + closeInBackgroundTimer = Timer(_kDisconnectOnBackgroundTimeout, () { + _logger.info( + 'App is in background for ${_kDisconnectOnBackgroundTimeout.inMinutes}m, closing socket.', + ); + pool.currentClient.close(); + }); }, onShow: () { closeInBackgroundTimer?.cancel(); @@ -650,8 +612,7 @@ class WebSocketChannelFactory { Map? headers, Duration timeout = const Duration(seconds: 10), }) async { - final socket = - await WebSocket.connect(url, headers: headers).timeout(timeout); + final socket = await WebSocket.connect(url, headers: headers).timeout(timeout); return IOWebSocketChannel(socket); } diff --git a/lib/src/styles/lichess_colors.dart b/lib/src/styles/lichess_colors.dart index d082947f4c..83e8a0ecaf 100644 --- a/lib/src/styles/lichess_colors.dart +++ b/lib/src/styles/lichess_colors.dart @@ -9,8 +9,7 @@ class LichessColors { // http://mmbitson.com // primary: blue - static const MaterialColor primary = - MaterialColor(_primaryPrimaryValue, { + static const MaterialColor primary = MaterialColor(_primaryPrimaryValue, { 50: Color(0xFFE4EFF9), 100: Color(0xFFBBD7F1), 200: Color(0xFF8DBCE8), @@ -25,8 +24,7 @@ class LichessColors { static const int _primaryPrimaryValue = 0xFF1B78D0; // secondary: green - static const MaterialColor secondary = - MaterialColor(_secondaryPrimaryValue, { + static const MaterialColor secondary = MaterialColor(_secondaryPrimaryValue, { 50: Color(0xFFECF3E5), 100: Color(0xFFD0E0BD), 200: Color(0xFFB1CC92), @@ -41,8 +39,7 @@ class LichessColors { static const int _secondaryPrimaryValue = 0xFF629924; // accent: orange - static const MaterialColor accent = - MaterialColor(_accentPrimaryValue, { + static const MaterialColor accent = MaterialColor(_accentPrimaryValue, { 50: Color(0xFFFAEAE0), 100: Color(0xFFF3CAB3), 200: Color(0xFFEBA780), diff --git a/lib/src/styles/lichess_icons.dart b/lib/src/styles/lichess_icons.dart index 12ed2cb177..67675461d1 100644 --- a/lib/src/styles/lichess_icons.dart +++ b/lib/src/styles/lichess_icons.dart @@ -11,7 +11,7 @@ /// fonts: /// - asset: fonts/LichessIcons.ttf /// -/// +/// /// * Font Awesome 4, Copyright (C) 2016 by Dave Gandy /// Author: Dave Gandy /// License: SIL () @@ -33,6 +33,7 @@ class LichessIcons { static const _kFontFam = 'LichessIcons'; static const String? _kFontPkg = null; + // dart format off static const IconData patron = IconData(0xe800, fontFamily: _kFontFam, fontPackage: _kFontPkg); static const IconData target = IconData(0xe801, fontFamily: _kFontFam, fontPackage: _kFontPkg); static const IconData blitz = IconData(0xe802, fontFamily: _kFontFam, fontPackage: _kFontPkg); diff --git a/lib/src/styles/puzzle_icons.dart b/lib/src/styles/puzzle_icons.dart index 04d37141b7..2405640dad 100644 --- a/lib/src/styles/puzzle_icons.dart +++ b/lib/src/styles/puzzle_icons.dart @@ -6,118 +6,189 @@ class PuzzleIcons { static const _kFontFam = 'LichessPuzzleIcons'; static const String? _kFontPkg = null; - static const IconData clearance = - IconData(0xe000, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData queensideAttack = - IconData(0xe001, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData bishopEndgame = - IconData(0xe002, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData short = - IconData(0xe003, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData backRankMate = - IconData(0xe004, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData advancedPawn = - IconData(0xe005, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData doubleCheck = - IconData(0xe006, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData mate = - IconData(0xe007, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData rookEndgame = - IconData(0xe008, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData sacrifice = - IconData(0xe009, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData promotion = - IconData(0xe00a, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData knightEndgame = - IconData(0xe00b, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData skewer = - IconData(0xe00c, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData master = - IconData(0xe00d, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData hookMate = - IconData(0xe00e, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData exposedKing = - IconData(0xe00f, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData intermezzo = - IconData(0xe010, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData interference = - IconData(0xe011, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData doubleBishopMate = - IconData(0xe012, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData hangingPiece = - IconData(0xe013, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData superGM = - IconData(0xe014, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData equality = - IconData(0xe015, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData castling = - IconData(0xe016, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData underPromotion = - IconData(0xe017, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData discoveredAttack = - IconData(0xe018, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData pin = - IconData(0xe019, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData endgame = - IconData(0xe01a, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData defensiveMove = - IconData(0xe01b, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData advantage = - IconData(0xe01c, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData mix = - IconData(0xe01d, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData oneMove = - IconData(0xe01e, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData anastasiaMate = - IconData(0xe01f, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData middlegame = - IconData(0xe020, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData fork = - IconData(0xe021, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData masterVsMaster = - IconData(0xe022, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData quietMove = - IconData(0xe023, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData long = - IconData(0xe024, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData smotheredMate = - IconData(0xe025, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData bodenMate = - IconData(0xe026, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData kingsideAttack = - IconData(0xe027, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData playerGames = - IconData(0xe028, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData pawnEndgame = - IconData(0xe029, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData zugzwang = - IconData(0xe02a, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData deflection = - IconData(0xe02b, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData trappedPiece = - IconData(0xe02c, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData capturingDefender = - IconData(0xe02d, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData queenEndgame = - IconData(0xe02e, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData arabianMate = - IconData(0xe02f, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData enPassant = - IconData(0xe030, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData attackingF2F7 = - IconData(0xe031, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData veryLong = - IconData(0xe032, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData xRayAttack = - IconData(0xe033, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData queenRookEndgame = - IconData(0xe034, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData attraction = - IconData(0xe035, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData crushing = - IconData(0xe036, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData opening = - IconData(0xe037, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData dovetailMate = - IconData(0xe038, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData clearance = IconData(0xe000, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData queensideAttack = IconData( + 0xe001, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData bishopEndgame = IconData( + 0xe002, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData short = IconData(0xe003, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData backRankMate = IconData( + 0xe004, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData advancedPawn = IconData( + 0xe005, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData doubleCheck = IconData( + 0xe006, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData mate = IconData(0xe007, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData rookEndgame = IconData( + 0xe008, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData sacrifice = IconData(0xe009, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData promotion = IconData(0xe00a, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData knightEndgame = IconData( + 0xe00b, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData skewer = IconData(0xe00c, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData master = IconData(0xe00d, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData hookMate = IconData(0xe00e, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData exposedKing = IconData( + 0xe00f, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData intermezzo = IconData( + 0xe010, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData interference = IconData( + 0xe011, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData doubleBishopMate = IconData( + 0xe012, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData hangingPiece = IconData( + 0xe013, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData superGM = IconData(0xe014, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData equality = IconData(0xe015, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData castling = IconData(0xe016, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData underPromotion = IconData( + 0xe017, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData discoveredAttack = IconData( + 0xe018, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData pin = IconData(0xe019, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData endgame = IconData(0xe01a, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData defensiveMove = IconData( + 0xe01b, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData advantage = IconData(0xe01c, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData mix = IconData(0xe01d, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData oneMove = IconData(0xe01e, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData anastasiaMate = IconData( + 0xe01f, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData middlegame = IconData( + 0xe020, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData fork = IconData(0xe021, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData masterVsMaster = IconData( + 0xe022, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData quietMove = IconData(0xe023, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData long = IconData(0xe024, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData smotheredMate = IconData( + 0xe025, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData bodenMate = IconData(0xe026, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData kingsideAttack = IconData( + 0xe027, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData playerGames = IconData( + 0xe028, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData pawnEndgame = IconData( + 0xe029, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData zugzwang = IconData(0xe02a, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData deflection = IconData( + 0xe02b, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData trappedPiece = IconData( + 0xe02c, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData capturingDefender = IconData( + 0xe02d, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData queenEndgame = IconData( + 0xe02e, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData arabianMate = IconData( + 0xe02f, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData enPassant = IconData(0xe030, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData attackingF2F7 = IconData( + 0xe031, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData veryLong = IconData(0xe032, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData xRayAttack = IconData( + 0xe033, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData queenRookEndgame = IconData( + 0xe034, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData attraction = IconData( + 0xe035, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData crushing = IconData(0xe036, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData opening = IconData(0xe037, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData dovetailMate = IconData( + 0xe038, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); } diff --git a/lib/src/styles/social_icons.dart b/lib/src/styles/social_icons.dart index 7712fe8c39..9363012338 100644 --- a/lib/src/styles/social_icons.dart +++ b/lib/src/styles/social_icons.dart @@ -29,8 +29,6 @@ class SocialIcons { static const _kFontFam = 'SocialIcons'; static const String? _kFontPkg = null; - static const IconData youtube = - IconData(0xf167, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData twitch = - IconData(0xf1e8, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData youtube = IconData(0xf167, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData twitch = IconData(0xf1e8, fontFamily: _kFontFam, fontPackage: _kFontPkg); } diff --git a/lib/src/styles/styles.dart b/lib/src/styles/styles.dart index 697c86a33d..6959840530 100644 --- a/lib/src/styles/styles.dart +++ b/lib/src/styles/styles.dart @@ -8,14 +8,8 @@ import 'package:lichess_mobile/src/styles/lichess_colors.dart'; abstract class Styles { // text static const bold = TextStyle(fontWeight: FontWeight.bold); - static const title = TextStyle( - fontSize: 20.0, - fontWeight: FontWeight.bold, - ); - static const subtitle = TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - ); + static const title = TextStyle(fontSize: 20.0, fontWeight: FontWeight.bold); + static const subtitle = TextStyle(fontSize: 16, fontWeight: FontWeight.w500); static final callout = TextStyle( fontSize: defaultTargetPlatform == TargetPlatform.iOS ? 20 : 18, letterSpacing: defaultTargetPlatform == TargetPlatform.iOS ? -0.41 : null, @@ -32,29 +26,16 @@ abstract class Styles { letterSpacing: defaultTargetPlatform == TargetPlatform.iOS ? -0.41 : null, fontWeight: FontWeight.bold, ); - static const boardPreviewTitle = TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ); + static const boardPreviewTitle = TextStyle(fontSize: 16, fontWeight: FontWeight.bold); static const subtitleOpacity = 0.7; - static const timeControl = TextStyle( - letterSpacing: 1.2, - ); - static const formLabel = TextStyle( - fontWeight: FontWeight.bold, - ); - static const formError = TextStyle( - color: LichessColors.red, - ); + static const timeControl = TextStyle(letterSpacing: 1.2); + static const formLabel = TextStyle(fontWeight: FontWeight.bold); + static const formError = TextStyle(color: LichessColors.red); static const formDescription = TextStyle(fontSize: 12); // padding - static const cupertinoAppBarTrailingWidgetPadding = - EdgeInsetsDirectional.only( - end: 8.0, - ); - static const bodyPadding = - EdgeInsets.symmetric(vertical: 16.0, horizontal: 16.0); + static const cupertinoAppBarTrailingWidgetPadding = EdgeInsetsDirectional.only(end: 8.0); + static const bodyPadding = EdgeInsets.symmetric(vertical: 16.0, horizontal: 16.0); static const verticalBodyPadding = EdgeInsets.symmetric(vertical: 16.0); static const horizontalBodyPadding = EdgeInsets.symmetric(horizontal: 16.0); static const sectionBottomPadding = EdgeInsets.only(bottom: 16.0); @@ -62,11 +43,7 @@ abstract class Styles { static const bodySectionPadding = EdgeInsets.all(16.0); /// Horizontal and bottom padding for the body section. - static const bodySectionBottomPadding = EdgeInsets.only( - bottom: 16.0, - left: 16.0, - right: 16.0, - ); + static const bodySectionBottomPadding = EdgeInsets.only(bottom: 16.0, left: 16.0, right: 16.0); // colors static Color? expansionTileColor(BuildContext context) => @@ -77,8 +54,7 @@ abstract class Styles { color: Color(0xE6F9F9F9), darkColor: Color.fromARGB(210, 36, 36, 38), ); - static const cupertinoTabletAppBarColor = - CupertinoDynamicColor.withBrightness( + static const cupertinoTabletAppBarColor = CupertinoDynamicColor.withBrightness( color: Color(0xFFF9F9F9), darkColor: Color.fromARGB(255, 36, 36, 36), ); @@ -207,10 +183,7 @@ abstract class Styles { // from: // https://github.com/flutter/flutter/blob/796c8ef79279f9c774545b3771238c3098dbefab/packages/flutter/lib/src/cupertino/bottom_tab_bar.dart#L17 static const CupertinoDynamicColor cupertinoDefaultTabBarBorderColor = - CupertinoDynamicColor.withBrightness( - color: Color(0x4D000000), - darkColor: Color(0x29000000), - ); + CupertinoDynamicColor.withBrightness(color: Color(0x4D000000), darkColor: Color(0x29000000)); } /// Retrieve the default text color and apply an opacity to it. @@ -313,6 +286,5 @@ const lichessCustomColors = CustomColors( ); extension CustomColorsBuildContext on BuildContext { - CustomColors get lichessColors => - Theme.of(this).extension() ?? lichessCustomColors; + CustomColors get lichessColors => Theme.of(this).extension() ?? lichessCustomColors; } diff --git a/lib/src/utils/async_value.dart b/lib/src/utils/async_value.dart index 3dd5db17ca..b7aba0b226 100644 --- a/lib/src/utils/async_value.dart +++ b/lib/src/utils/async_value.dart @@ -7,9 +7,7 @@ extension AsyncValueUI on AsyncValue { if (!isRefreshing && hasError) { switch (Theme.of(context).platform) { case TargetPlatform.android: - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(error.toString())), - ); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(error.toString()))); case TargetPlatform.iOS: showCupertinoSnackBar( context: context, diff --git a/lib/src/utils/badge_service.dart b/lib/src/utils/badge_service.dart index 9fb7a8403f..67a8c3e809 100644 --- a/lib/src/utils/badge_service.dart +++ b/lib/src/utils/badge_service.dart @@ -19,9 +19,7 @@ class BadgeService { } try { - await _channel.invokeMethod('setBadge', { - 'badge': value, - }); + await _channel.invokeMethod('setBadge', {'badge': value}); } on PlatformException catch (e) { _log.severe(e); } diff --git a/lib/src/utils/chessboard.dart b/lib/src/utils/chessboard.dart index 3519e92b40..af26a57636 100644 --- a/lib/src/utils/chessboard.dart +++ b/lib/src/utils/chessboard.dart @@ -6,15 +6,13 @@ import 'package:flutter/widgets.dart'; /// This method clears the cache before loading the images. Future precachePieceImages(PieceSet pieceSet) async { try { - final devicePixelRatio = WidgetsBinding - .instance.platformDispatcher.implicitView?.devicePixelRatio ?? - 1.0; + final devicePixelRatio = + WidgetsBinding.instance.platformDispatcher.implicitView?.devicePixelRatio ?? 1.0; ChessgroundImages.instance.clear(); for (final asset in pieceSet.assets.values) { - await ChessgroundImages.instance - .load(asset, devicePixelRatio: devicePixelRatio); + await ChessgroundImages.instance.load(asset, devicePixelRatio: devicePixelRatio); debugPrint('Preloaded piece image: ${asset.assetName}'); } } catch (e) { diff --git a/lib/src/utils/color_palette.dart b/lib/src/utils/color_palette.dart index 4ecbb07e12..6ecdd18d82 100644 --- a/lib/src/utils/color_palette.dart +++ b/lib/src/utils/color_palette.dart @@ -21,10 +21,7 @@ void setCorePalette(CorePalette? palette) { _boardColorScheme = ChessboardColorScheme( darkSquare: darkSquare, lightSquare: lightSquare, - background: SolidColorChessboardBackground( - lightSquare: lightSquare, - darkSquare: darkSquare, - ), + background: SolidColorChessboardBackground(lightSquare: lightSquare, darkSquare: darkSquare), whiteCoordBackground: SolidColorChessboardBackground( lightSquare: lightSquare, darkSquare: darkSquare, diff --git a/lib/src/utils/focus_detector.dart b/lib/src/utils/focus_detector.dart index 40d5800eed..25cc261294 100644 --- a/lib/src/utils/focus_detector.dart +++ b/lib/src/utils/focus_detector.dart @@ -52,8 +52,7 @@ class FocusDetector extends StatefulWidget { _FocusDetectorState createState() => _FocusDetectorState(); } -class _FocusDetectorState extends State - with WidgetsBindingObserver { +class _FocusDetectorState extends State with WidgetsBindingObserver { final _visibilityDetectorKey = UniqueKey(); /// Counter to keep track of the visibility changes. @@ -101,13 +100,13 @@ class _FocusDetectorState extends State @override Widget build(BuildContext context) => VisibilityDetector( - key: _visibilityDetectorKey, - onVisibilityChanged: (visibilityInfo) { - final visibleFraction = visibilityInfo.visibleFraction; - _notifyVisibilityStatusChange(visibleFraction); - }, - child: widget.child, - ); + key: _visibilityDetectorKey, + onVisibilityChanged: (visibilityInfo) { + final visibleFraction = visibilityInfo.visibleFraction; + _notifyVisibilityStatusChange(visibleFraction); + }, + child: widget.child, + ); /// Notifies changes in the widget's visibility. void _notifyVisibilityStatusChange(double newVisibleFraction) { diff --git a/lib/src/utils/gestures_exclusion.dart b/lib/src/utils/gestures_exclusion.dart index bbcb139c6c..b8e0b410a2 100644 --- a/lib/src/utils/gestures_exclusion.dart +++ b/lib/src/utils/gestures_exclusion.dart @@ -47,10 +47,7 @@ class AndroidGesturesExclusionWidget extends StatelessWidget { return FocusDetector( onFocusGained: () { if (shouldExcludeGesturesOnFocusGained?.call() ?? true) { - setAndroidBoardGesturesExclusion( - boardKey, - withImmersiveMode: shouldSetImmersiveMode, - ); + setAndroidBoardGesturesExclusion(boardKey, withImmersiveMode: shouldSetImmersiveMode); } }, onFocusLost: () { @@ -126,8 +123,7 @@ Future clearAndroidBoardGesturesExclusion() async { } class GesturesExclusion { - static const _channel = - MethodChannel('mobile.lichess.org/gestures_exclusion'); + static const _channel = MethodChannel('mobile.lichess.org/gestures_exclusion'); const GesturesExclusion._(); @@ -138,24 +134,23 @@ class GesturesExclusion { return; } - final rectsAsMaps = rects - .map( - (r) => r.hasNaN || r.isInfinite - ? null - : { - 'left': r.left.floor(), - 'top': r.top.floor(), - 'right': r.right.floor(), - 'bottom': r.bottom.floor(), - }, - ) - .toList(); + final rectsAsMaps = + rects + .map( + (r) => + r.hasNaN || r.isInfinite + ? null + : { + 'left': r.left.floor(), + 'top': r.top.floor(), + 'right': r.right.floor(), + 'bottom': r.bottom.floor(), + }, + ) + .toList(); try { - await _channel.invokeMethod( - 'setSystemGestureExclusionRects', - rectsAsMaps, - ); + await _channel.invokeMethod('setSystemGestureExclusionRects', rectsAsMaps); } on PlatformException catch (e) { debugPrint('Failed to set rects: ${e.message}'); } @@ -167,8 +162,7 @@ class GesturesExclusion { } try { - await _channel - .invokeMethod('setSystemGestureExclusionRects', []); + await _channel.invokeMethod('setSystemGestureExclusionRects', []); } on PlatformException catch (e) { debugPrint('Failed to clear rects: ${e.message}'); } diff --git a/lib/src/utils/image.dart b/lib/src/utils/image.dart index 58a31d6b2d..690f1dd4bb 100644 --- a/lib/src/utils/image.dart +++ b/lib/src/utils/image.dart @@ -4,10 +4,7 @@ import 'dart:typed_data'; import 'package:material_color_utilities/material_color_utilities.dart'; -typedef ImageColors = ({ - int primaryContainer, - int onPrimaryContainer, -}); +typedef ImageColors = ({int primaryContainer, int onPrimaryContainer}); /// A worker that quantizes an image and returns a minimal color scheme associated /// with the image. @@ -41,12 +38,7 @@ class ImageColorWorker { final connection = Completer<(ReceivePort, SendPort)>.sync(); initPort.handler = (dynamic initialMessage) { final commandPort = initialMessage as SendPort; - connection.complete( - ( - ReceivePort.fromRawReceivePort(initPort), - commandPort, - ), - ); + connection.complete((ReceivePort.fromRawReceivePort(initPort), commandPort)); }; try { @@ -56,8 +48,7 @@ class ImageColorWorker { rethrow; } - final (ReceivePort receivePort, SendPort sendPort) = - await connection.future; + final (ReceivePort receivePort, SendPort sendPort) = await connection.future; return ImageColorWorker._(receivePort, sendPort); } @@ -79,10 +70,7 @@ class ImageColorWorker { if (_closed && _activeRequests.isEmpty) _responses.close(); } - static void _handleCommandsToIsolate( - ReceivePort receivePort, - SendPort sendPort, - ) { + static void _handleCommandsToIsolate(ReceivePort receivePort, SendPort sendPort) { receivePort.listen((message) async { if (message == 'shutdown') { receivePort.close(); @@ -91,15 +79,14 @@ class ImageColorWorker { final (int id, Uint32List image) = message as (int, Uint32List); try { // final stopwatch0 = Stopwatch()..start(); - final QuantizerResult quantizerResult = - await QuantizerCelebi().quantize(image, 32); + final QuantizerResult quantizerResult = await QuantizerCelebi().quantize(image, 32); final Map colorToCount = quantizerResult.colorToCount.map( - (int key, int value) => - MapEntry(_getArgbFromAbgr(key), value), + (int key, int value) => MapEntry(_getArgbFromAbgr(key), value), ); final significantColors = Map.from(colorToCount) ..removeWhere((key, value) => value < 10); - final meanTone = colorToCount.entries.fold( + final meanTone = + colorToCount.entries.fold( 0, (double previousValue, MapEntry element) => previousValue + Hct.fromInt(element.key).tone * element.value, @@ -109,19 +96,18 @@ class ImageColorWorker { (int previousValue, int element) => previousValue + element, ); - final int scoredResult = Score.score( - colorToCount, - desired: 1, - fallbackColorARGB: 0xFFFFFFFF, - filter: false, - ).first; + final int scoredResult = + Score.score( + colorToCount, + desired: 1, + fallbackColorARGB: 0xFFFFFFFF, + filter: false, + ).first; final Hct sourceColor = Hct.fromInt(scoredResult); if ((meanTone - sourceColor.tone).abs() > 20) { sourceColor.tone = meanTone; } - final scheme = (significantColors.length <= 10 - ? SchemeMonochrome.new - : SchemeFidelity.new)( + final scheme = (significantColors.length <= 10 ? SchemeMonochrome.new : SchemeFidelity.new)( sourceColorHct: sourceColor, isDark: sourceColor.tone < 50, contrastLevel: 0.0, diff --git a/lib/src/utils/immersive_mode.dart b/lib/src/utils/immersive_mode.dart index e99b460606..54738fdc94 100644 --- a/lib/src/utils/immersive_mode.dart +++ b/lib/src/utils/immersive_mode.dart @@ -11,11 +11,7 @@ import 'package:wakelock_plus/wakelock_plus.dart'; /// force the device to stay awake. class ImmersiveModeWidget extends StatelessWidget { /// Create a new immersive mode widget, that enables immersive mode when focused. - const ImmersiveModeWidget({ - required this.child, - this.shouldEnableOnFocusGained, - super.key, - }); + const ImmersiveModeWidget({required this.child, this.shouldEnableOnFocusGained, super.key}); final Widget child; @@ -40,11 +36,7 @@ class ImmersiveModeWidget extends StatelessWidget { /// A widget that enables wakelock when focused. class WakelockWidget extends StatelessWidget { /// Create a new wakelock widget, that enables wakelock when focused. - const WakelockWidget({ - required this.child, - this.shouldEnableOnFocusGained, - super.key, - }); + const WakelockWidget({required this.child, this.shouldEnableOnFocusGained, super.key}); final Widget child; @@ -91,17 +83,18 @@ class ImmersiveMode { Future disable() async { final wakeFuture = WakelockPlus.disable(); - final androidInfo = defaultTargetPlatform == TargetPlatform.android - ? await DeviceInfoPlugin().androidInfo - : null; + final androidInfo = + defaultTargetPlatform == TargetPlatform.android + ? await DeviceInfoPlugin().androidInfo + : null; final setUiModeFuture = androidInfo == null || androidInfo.version.sdkInt >= 29 ? SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge) : SystemChrome.setEnabledSystemUIMode( - SystemUiMode.manual, - overlays: SystemUiOverlay.values, - ); + SystemUiMode.manual, + overlays: SystemUiOverlay.values, + ); return Future.wait([wakeFuture, setUiModeFuture]).then((_) {}); } diff --git a/lib/src/utils/json.dart b/lib/src/utils/json.dart index b8c013b980..2bc6852ebe 100644 --- a/lib/src/utils/json.dart +++ b/lib/src/utils/json.dart @@ -25,9 +25,7 @@ extension TimeExtension on Pick { if (value is int) { return DateTime.fromMillisecondsSinceEpoch(value); } - throw PickException( - "value $value at $debugParsingExit can't be casted to DateTime", - ); + throw PickException("value $value at $debugParsingExit can't be casted to DateTime"); } /// Matches a DateTime from milliseconds since unix epoch. @@ -51,9 +49,7 @@ extension TimeExtension on Pick { } else if (value is double) { return Duration(milliseconds: (value * 1000).toInt()); } - throw PickException( - "value $value at $debugParsingExit can't be casted to Duration", - ); + throw PickException("value $value at $debugParsingExit can't be casted to Duration"); } /// Matches a Duration from seconds @@ -75,9 +71,7 @@ extension TimeExtension on Pick { if (value is int) { return Duration(milliseconds: value * 10); } - throw PickException( - "value $value at $debugParsingExit can't be casted to Duration", - ); + throw PickException("value $value at $debugParsingExit can't be casted to Duration"); } Duration? asDurationFromCentiSecondsOrNull() { @@ -98,9 +92,7 @@ extension TimeExtension on Pick { if (value is int) { return Duration(milliseconds: value); } - throw PickException( - "value $value at $debugParsingExit can't be casted to Duration", - ); + throw PickException("value $value at $debugParsingExit can't be casted to Duration"); } Duration? asDurationFromMilliSecondsOrNull() { diff --git a/lib/src/utils/l10n.dart b/lib/src/utils/l10n.dart index 3f76593481..2090626a20 100644 --- a/lib/src/utils/l10n.dart +++ b/lib/src/utils/l10n.dart @@ -32,13 +32,8 @@ Text l10nWithWidget( children: [ if (parts[0].isNotEmpty) TextSpan(text: parts[0], style: textStyle), if (parts[0] != localizedStringWithPlaceholder) - WidgetSpan( - child: widget, - alignment: PlaceholderAlignment.middle, - style: textStyle, - ), - if (parts.length > 1 && parts[1].isNotEmpty) - TextSpan(text: parts[1], style: textStyle), + WidgetSpan(child: widget, alignment: PlaceholderAlignment.middle, style: textStyle), + if (parts.length > 1 && parts[1].isNotEmpty) TextSpan(text: parts[1], style: textStyle), ], ), ); @@ -56,10 +51,10 @@ String relativeDate(DateTime date) { ? 'in ${diff.inMinutes} minute${diff.inMinutes > 1 ? 's' : ''}' // TODO translate with https://github.com/lichess-org/lila/blob/65b28ea8e43e0133df6c7ed40e03c2954f247d1e/translation/source/timeago.xml#L8 : 'in ${diff.inHours} hour${diff.inHours > 1 ? 's' : ''}' // TODO translate with https://github.com/lichess-org/lila/blob/65b28ea8e43e0133df6c7ed40e03c2954f247d1e/translation/source/timeago.xml#L12 : diff.inDays.abs() <= 7 - ? _dayFormatter.format(date) - : diff.inDays.abs() < 365 - ? _monthFormatter.format(date) - : _dateFormatterWithYear.format(date); + ? _dayFormatter.format(date) + : diff.inDays.abs() < 365 + ? _monthFormatter.format(date) + : _dateFormatterWithYear.format(date); } /// Returns a localized locale name. @@ -68,106 +63,106 @@ String relativeDate(DateTime date) { /// /// Not all of these are actually supported in the app currently, but this way we won't have to check the lila code again when we add more languages. String localeToLocalizedName(Locale locale) => switch (locale) { - Locale(languageCode: 'en', countryCode: 'GB') => 'English', - Locale(languageCode: 'af', countryCode: 'ZA') => 'Afrikaans', - Locale(languageCode: 'an', countryCode: 'ES') => 'Aragonés', - Locale(languageCode: 'ar', countryCode: 'SA') => 'العربية', - Locale(languageCode: 'as', countryCode: 'IN') => 'অসমীয়া', - Locale(languageCode: 'av', countryCode: 'DA') => 'авар мацӀ', - Locale(languageCode: 'az', countryCode: 'AZ') => 'Azərbaycanca', - Locale(languageCode: 'be', countryCode: 'BY') => 'Беларуская', - Locale(languageCode: 'bg', countryCode: 'BG') => 'български език', - Locale(languageCode: 'bn', countryCode: 'BD') => 'বাংলা', - Locale(languageCode: 'br', countryCode: 'FR') => 'Brezhoneg', - Locale(languageCode: 'bs', countryCode: 'BA') => 'Bosanski', - Locale(languageCode: 'ca', countryCode: 'ES') => 'Català, valencià', - Locale(languageCode: 'ckb', countryCode: 'IR') => 'کوردی سۆرانی', - Locale(languageCode: 'co', countryCode: 'FR') => 'Corsu', - Locale(languageCode: 'cs', countryCode: 'CZ') => 'Čeština', - Locale(languageCode: 'cv', countryCode: 'CU') => 'чӑваш чӗлхи', - Locale(languageCode: 'cy', countryCode: 'GB') => 'Cymraeg', - Locale(languageCode: 'da', countryCode: 'DK') => 'Dansk', - Locale(languageCode: 'de', countryCode: 'DE') => 'Deutsch', - Locale(languageCode: 'el', countryCode: 'GR') => 'Ελληνικά', - Locale(languageCode: 'en', countryCode: 'US') => 'English (US)', - Locale(languageCode: 'eo', countryCode: 'UY') => 'Esperanto', - Locale(languageCode: 'es', countryCode: 'ES') => 'Español', - Locale(languageCode: 'et', countryCode: 'EE') => 'Eesti keel', - Locale(languageCode: 'eu', countryCode: 'ES') => 'Euskara', - Locale(languageCode: 'fa', countryCode: 'IR') => 'فارسی', - Locale(languageCode: 'fi', countryCode: 'FI') => 'Suomen kieli', - Locale(languageCode: 'fo', countryCode: 'FO') => 'Føroyskt', - Locale(languageCode: 'fr', countryCode: 'FR') => 'Français', - Locale(languageCode: 'frp', countryCode: 'IT') => 'Arpitan', - Locale(languageCode: 'fy', countryCode: 'NL') => 'Frysk', - Locale(languageCode: 'ga', countryCode: 'IE') => 'Gaeilge', - Locale(languageCode: 'gd', countryCode: 'GB') => 'Gàidhlig', - Locale(languageCode: 'gl', countryCode: 'ES') => 'Galego', - Locale(languageCode: 'gsw', countryCode: 'CH') => 'Schwizerdütsch', - Locale(languageCode: 'gu', countryCode: 'IN') => 'ગુજરાતી', - Locale(languageCode: 'he', countryCode: 'IL') => 'עִבְרִית', - Locale(languageCode: 'hi', countryCode: 'IN') => 'हिन्दी, हिंदी', - Locale(languageCode: 'hr', countryCode: 'HR') => 'Hrvatski', - Locale(languageCode: 'hu', countryCode: 'HU') => 'Magyar', - Locale(languageCode: 'hy', countryCode: 'AM') => 'Հայերեն', - Locale(languageCode: 'ia', countryCode: 'IA') => 'Interlingua', - Locale(languageCode: 'id', countryCode: 'ID') => 'Bahasa Indonesia', - Locale(languageCode: 'io', countryCode: 'EN') => 'Ido', - Locale(languageCode: 'is', countryCode: 'IS') => 'Íslenska', - Locale(languageCode: 'it', countryCode: 'IT') => 'Italiano', - Locale(languageCode: 'ja', countryCode: 'JP') => '日本語', - Locale(languageCode: 'jbo', countryCode: 'EN') => 'Lojban', - Locale(languageCode: 'jv', countryCode: 'ID') => 'Basa Jawa', - Locale(languageCode: 'ka', countryCode: 'GE') => 'ქართული', - Locale(languageCode: 'kab', countryCode: 'DZ') => 'Taqvaylit', - Locale(languageCode: 'kk', countryCode: 'KZ') => 'қазақша', - Locale(languageCode: 'kmr', countryCode: 'TR') => 'Kurdî (Kurmancî)', - Locale(languageCode: 'kn', countryCode: 'IN') => 'ಕನ್ನಡ', - Locale(languageCode: 'ko', countryCode: 'KR') => '한국어', - Locale(languageCode: 'ky', countryCode: 'KG') => 'кыргызча', - Locale(languageCode: 'la', countryCode: 'LA') => 'Lingua Latina', - Locale(languageCode: 'lb', countryCode: 'LU') => 'Lëtzebuergesch', - Locale(languageCode: 'lt', countryCode: 'LT') => 'Lietuvių kalba', - Locale(languageCode: 'lv', countryCode: 'LV') => 'Latviešu valoda', - Locale(languageCode: 'mg', countryCode: 'MG') => 'Fiteny malagasy', - Locale(languageCode: 'mk', countryCode: 'MK') => 'македонски јази', - Locale(languageCode: 'ml', countryCode: 'IN') => 'മലയാളം', - Locale(languageCode: 'mn', countryCode: 'MN') => 'монгол', - Locale(languageCode: 'mr', countryCode: 'IN') => 'मराठी', - Locale(languageCode: 'nb', countryCode: 'NO') => 'Norsk bokmål', - Locale(languageCode: 'ne', countryCode: 'NP') => 'नेपाली', - Locale(languageCode: 'nl', countryCode: 'NL') => 'Nederlands', - Locale(languageCode: 'nn', countryCode: 'NO') => 'Norsk nynorsk', - Locale(languageCode: 'pi', countryCode: 'IN') => 'पालि', - Locale(languageCode: 'pl', countryCode: 'PL') => 'Polski', - Locale(languageCode: 'ps', countryCode: 'AF') => 'پښتو', - Locale(languageCode: 'pt', countryCode: 'PT') => 'Português', - Locale(languageCode: 'pt', countryCode: 'BR') => 'Português (BR)', - Locale(languageCode: 'ro', countryCode: 'RO') => 'Română', - Locale(languageCode: 'ru', countryCode: 'RU') => 'русский язык', - Locale(languageCode: 'ry', countryCode: 'UA') => 'Русинська бисїда', - Locale(languageCode: 'sa', countryCode: 'IN') => 'संस्कृत', - Locale(languageCode: 'sk', countryCode: 'SK') => 'Slovenčina', - Locale(languageCode: 'sl', countryCode: 'SI') => 'Slovenščina', - Locale(languageCode: 'so', countryCode: 'SO') => 'Af Soomaali', - Locale(languageCode: 'sq', countryCode: 'AL') => 'Shqip', - Locale(languageCode: 'sr', countryCode: 'SP') => 'Српски језик', - Locale(languageCode: 'sv', countryCode: 'SE') => 'Svenska', - Locale(languageCode: 'sw', countryCode: 'KE') => 'Kiswahili', - Locale(languageCode: 'ta', countryCode: 'IN') => 'தமிழ்', - Locale(languageCode: 'tg', countryCode: 'TJ') => 'тоҷикӣ', - Locale(languageCode: 'th', countryCode: 'TH') => 'ไทย', - Locale(languageCode: 'tk', countryCode: 'TM') => 'Türkmençe', - Locale(languageCode: 'tl', countryCode: 'PH') => 'Tagalog', - Locale(languageCode: 'tp', countryCode: 'TP') => 'Toki pona', - Locale(languageCode: 'tr', countryCode: 'TR') => 'Türkçe', - Locale(languageCode: 'uk', countryCode: 'UA') => 'українська', - Locale(languageCode: 'ur', countryCode: 'PK') => 'اُردُو', - Locale(languageCode: 'uz', countryCode: 'UZ') => 'oʻzbekcha', - Locale(languageCode: 'vi', countryCode: 'VN') => 'Tiếng Việt', - Locale(languageCode: 'yo', countryCode: 'NG') => 'Yorùbá', - Locale(languageCode: 'zh', countryCode: 'CN') => '中文', - Locale(languageCode: 'zh', countryCode: 'TW') => '繁體中文', - Locale(languageCode: 'zu', countryCode: 'ZA') => 'isiZulu', - _ => locale.toString(), - }; + Locale(languageCode: 'en', countryCode: 'GB') => 'English', + Locale(languageCode: 'af', countryCode: 'ZA') => 'Afrikaans', + Locale(languageCode: 'an', countryCode: 'ES') => 'Aragonés', + Locale(languageCode: 'ar', countryCode: 'SA') => 'العربية', + Locale(languageCode: 'as', countryCode: 'IN') => 'অসমীয়া', + Locale(languageCode: 'av', countryCode: 'DA') => 'авар мацӀ', + Locale(languageCode: 'az', countryCode: 'AZ') => 'Azərbaycanca', + Locale(languageCode: 'be', countryCode: 'BY') => 'Беларуская', + Locale(languageCode: 'bg', countryCode: 'BG') => 'български език', + Locale(languageCode: 'bn', countryCode: 'BD') => 'বাংলা', + Locale(languageCode: 'br', countryCode: 'FR') => 'Brezhoneg', + Locale(languageCode: 'bs', countryCode: 'BA') => 'Bosanski', + Locale(languageCode: 'ca', countryCode: 'ES') => 'Català, valencià', + Locale(languageCode: 'ckb', countryCode: 'IR') => 'کوردی سۆرانی', + Locale(languageCode: 'co', countryCode: 'FR') => 'Corsu', + Locale(languageCode: 'cs', countryCode: 'CZ') => 'Čeština', + Locale(languageCode: 'cv', countryCode: 'CU') => 'чӑваш чӗлхи', + Locale(languageCode: 'cy', countryCode: 'GB') => 'Cymraeg', + Locale(languageCode: 'da', countryCode: 'DK') => 'Dansk', + Locale(languageCode: 'de', countryCode: 'DE') => 'Deutsch', + Locale(languageCode: 'el', countryCode: 'GR') => 'Ελληνικά', + Locale(languageCode: 'en', countryCode: 'US') => 'English (US)', + Locale(languageCode: 'eo', countryCode: 'UY') => 'Esperanto', + Locale(languageCode: 'es', countryCode: 'ES') => 'Español', + Locale(languageCode: 'et', countryCode: 'EE') => 'Eesti keel', + Locale(languageCode: 'eu', countryCode: 'ES') => 'Euskara', + Locale(languageCode: 'fa', countryCode: 'IR') => 'فارسی', + Locale(languageCode: 'fi', countryCode: 'FI') => 'Suomen kieli', + Locale(languageCode: 'fo', countryCode: 'FO') => 'Føroyskt', + Locale(languageCode: 'fr', countryCode: 'FR') => 'Français', + Locale(languageCode: 'frp', countryCode: 'IT') => 'Arpitan', + Locale(languageCode: 'fy', countryCode: 'NL') => 'Frysk', + Locale(languageCode: 'ga', countryCode: 'IE') => 'Gaeilge', + Locale(languageCode: 'gd', countryCode: 'GB') => 'Gàidhlig', + Locale(languageCode: 'gl', countryCode: 'ES') => 'Galego', + Locale(languageCode: 'gsw', countryCode: 'CH') => 'Schwizerdütsch', + Locale(languageCode: 'gu', countryCode: 'IN') => 'ગુજરાતી', + Locale(languageCode: 'he', countryCode: 'IL') => 'עִבְרִית', + Locale(languageCode: 'hi', countryCode: 'IN') => 'हिन्दी, हिंदी', + Locale(languageCode: 'hr', countryCode: 'HR') => 'Hrvatski', + Locale(languageCode: 'hu', countryCode: 'HU') => 'Magyar', + Locale(languageCode: 'hy', countryCode: 'AM') => 'Հայերեն', + Locale(languageCode: 'ia', countryCode: 'IA') => 'Interlingua', + Locale(languageCode: 'id', countryCode: 'ID') => 'Bahasa Indonesia', + Locale(languageCode: 'io', countryCode: 'EN') => 'Ido', + Locale(languageCode: 'is', countryCode: 'IS') => 'Íslenska', + Locale(languageCode: 'it', countryCode: 'IT') => 'Italiano', + Locale(languageCode: 'ja', countryCode: 'JP') => '日本語', + Locale(languageCode: 'jbo', countryCode: 'EN') => 'Lojban', + Locale(languageCode: 'jv', countryCode: 'ID') => 'Basa Jawa', + Locale(languageCode: 'ka', countryCode: 'GE') => 'ქართული', + Locale(languageCode: 'kab', countryCode: 'DZ') => 'Taqvaylit', + Locale(languageCode: 'kk', countryCode: 'KZ') => 'қазақша', + Locale(languageCode: 'kmr', countryCode: 'TR') => 'Kurdî (Kurmancî)', + Locale(languageCode: 'kn', countryCode: 'IN') => 'ಕನ್ನಡ', + Locale(languageCode: 'ko', countryCode: 'KR') => '한국어', + Locale(languageCode: 'ky', countryCode: 'KG') => 'кыргызча', + Locale(languageCode: 'la', countryCode: 'LA') => 'Lingua Latina', + Locale(languageCode: 'lb', countryCode: 'LU') => 'Lëtzebuergesch', + Locale(languageCode: 'lt', countryCode: 'LT') => 'Lietuvių kalba', + Locale(languageCode: 'lv', countryCode: 'LV') => 'Latviešu valoda', + Locale(languageCode: 'mg', countryCode: 'MG') => 'Fiteny malagasy', + Locale(languageCode: 'mk', countryCode: 'MK') => 'македонски јази', + Locale(languageCode: 'ml', countryCode: 'IN') => 'മലയാളം', + Locale(languageCode: 'mn', countryCode: 'MN') => 'монгол', + Locale(languageCode: 'mr', countryCode: 'IN') => 'मराठी', + Locale(languageCode: 'nb', countryCode: 'NO') => 'Norsk bokmål', + Locale(languageCode: 'ne', countryCode: 'NP') => 'नेपाली', + Locale(languageCode: 'nl', countryCode: 'NL') => 'Nederlands', + Locale(languageCode: 'nn', countryCode: 'NO') => 'Norsk nynorsk', + Locale(languageCode: 'pi', countryCode: 'IN') => 'पालि', + Locale(languageCode: 'pl', countryCode: 'PL') => 'Polski', + Locale(languageCode: 'ps', countryCode: 'AF') => 'پښتو', + Locale(languageCode: 'pt', countryCode: 'PT') => 'Português', + Locale(languageCode: 'pt', countryCode: 'BR') => 'Português (BR)', + Locale(languageCode: 'ro', countryCode: 'RO') => 'Română', + Locale(languageCode: 'ru', countryCode: 'RU') => 'русский язык', + Locale(languageCode: 'ry', countryCode: 'UA') => 'Русинська бисїда', + Locale(languageCode: 'sa', countryCode: 'IN') => 'संस्कृत', + Locale(languageCode: 'sk', countryCode: 'SK') => 'Slovenčina', + Locale(languageCode: 'sl', countryCode: 'SI') => 'Slovenščina', + Locale(languageCode: 'so', countryCode: 'SO') => 'Af Soomaali', + Locale(languageCode: 'sq', countryCode: 'AL') => 'Shqip', + Locale(languageCode: 'sr', countryCode: 'SP') => 'Српски језик', + Locale(languageCode: 'sv', countryCode: 'SE') => 'Svenska', + Locale(languageCode: 'sw', countryCode: 'KE') => 'Kiswahili', + Locale(languageCode: 'ta', countryCode: 'IN') => 'தமிழ்', + Locale(languageCode: 'tg', countryCode: 'TJ') => 'тоҷикӣ', + Locale(languageCode: 'th', countryCode: 'TH') => 'ไทย', + Locale(languageCode: 'tk', countryCode: 'TM') => 'Türkmençe', + Locale(languageCode: 'tl', countryCode: 'PH') => 'Tagalog', + Locale(languageCode: 'tp', countryCode: 'TP') => 'Toki pona', + Locale(languageCode: 'tr', countryCode: 'TR') => 'Türkçe', + Locale(languageCode: 'uk', countryCode: 'UA') => 'українська', + Locale(languageCode: 'ur', countryCode: 'PK') => 'اُردُو', + Locale(languageCode: 'uz', countryCode: 'UZ') => 'oʻzbekcha', + Locale(languageCode: 'vi', countryCode: 'VN') => 'Tiếng Việt', + Locale(languageCode: 'yo', countryCode: 'NG') => 'Yorùbá', + Locale(languageCode: 'zh', countryCode: 'CN') => '中文', + Locale(languageCode: 'zh', countryCode: 'TW') => '繁體中文', + Locale(languageCode: 'zu', countryCode: 'ZA') => 'isiZulu', + _ => locale.toString(), +}; diff --git a/lib/src/utils/navigation.dart b/lib/src/utils/navigation.dart index 80e054b7e8..11dea40fe4 100644 --- a/lib/src/utils/navigation.dart +++ b/lib/src/utils/navigation.dart @@ -60,33 +60,20 @@ Future pushPlatformRoute( bool fullscreenDialog = false, String? title, }) { - assert( - screen != null || builder != null, - 'Either screen or builder must be provided.', - ); + assert(screen != null || builder != null, 'Either screen or builder must be provided.'); return Navigator.of(context, rootNavigator: rootNavigator).push( Theme.of(context).platform == TargetPlatform.iOS ? builder != null - ? CupertinoPageRoute( - builder: builder, - title: title, - fullscreenDialog: fullscreenDialog, - ) + ? CupertinoPageRoute(builder: builder, title: title, fullscreenDialog: fullscreenDialog) : CupertinoScreenRoute( - screen: screen!, - title: title, - fullscreenDialog: fullscreenDialog, - ) + screen: screen!, + title: title, + fullscreenDialog: fullscreenDialog, + ) : builder != null - ? MaterialPageRoute( - builder: builder, - fullscreenDialog: fullscreenDialog, - ) - : MaterialScreenRoute( - screen: screen!, - fullscreenDialog: fullscreenDialog, - ), + ? MaterialPageRoute(builder: builder, fullscreenDialog: fullscreenDialog) + : MaterialScreenRoute(screen: screen!, fullscreenDialog: fullscreenDialog), ); } @@ -107,30 +94,17 @@ Future pushReplacementPlatformRoute( bool fullscreenDialog = false, String? title, }) { - return Navigator.of( - context, - rootNavigator: rootNavigator, - ).pushReplacement( + return Navigator.of(context, rootNavigator: rootNavigator).pushReplacement( Theme.of(context).platform == TargetPlatform.iOS ? builder != null - ? CupertinoPageRoute( - builder: builder, - title: title, - fullscreenDialog: fullscreenDialog, - ) + ? CupertinoPageRoute(builder: builder, title: title, fullscreenDialog: fullscreenDialog) : CupertinoScreenRoute( - screen: screen!, - title: title, - fullscreenDialog: fullscreenDialog, - ) + screen: screen!, + title: title, + fullscreenDialog: fullscreenDialog, + ) : builder != null - ? MaterialPageRoute( - builder: builder, - fullscreenDialog: fullscreenDialog, - ) - : MaterialScreenRoute( - screen: screen!, - fullscreenDialog: fullscreenDialog, - ), + ? MaterialPageRoute(builder: builder, fullscreenDialog: fullscreenDialog) + : MaterialScreenRoute(screen: screen!, fullscreenDialog: fullscreenDialog), ); } diff --git a/lib/src/utils/rate_limit.dart b/lib/src/utils/rate_limit.dart index 5033ea8076..901de4eff3 100644 --- a/lib/src/utils/rate_limit.dart +++ b/lib/src/utils/rate_limit.dart @@ -4,9 +4,7 @@ class Debouncer { final Duration delay; Timer? _timer; - Debouncer( - this.delay, - ); + Debouncer(this.delay); void call(void Function() action) { _timer?.cancel(); diff --git a/lib/src/utils/screen.dart b/lib/src/utils/screen.dart index eb4ba411a3..1266a6e1e5 100644 --- a/lib/src/utils/screen.dart +++ b/lib/src/utils/screen.dart @@ -7,8 +7,7 @@ double estimateRemainingHeightLeftBoard(BuildContext context) { final padding = MediaQuery.paddingOf(context); final safeViewportHeight = size.height - padding.top - padding.bottom; final boardSize = size.width; - final appBarHeight = - Theme.of(context).platform == TargetPlatform.iOS ? 44.0 : 56.0; + final appBarHeight = Theme.of(context).platform == TargetPlatform.iOS ? 44.0 : 56.0; return safeViewportHeight - boardSize - appBarHeight - kBottomBarHeight; } diff --git a/lib/src/utils/share.dart b/lib/src/utils/share.dart index 167290a4cc..1f7a2e33a9 100644 --- a/lib/src/utils/share.dart +++ b/lib/src/utils/share.dart @@ -7,6 +7,7 @@ import 'package:share_plus/share_plus.dart'; /// in order to make it work on iPads. Future launchShareDialog( BuildContext context, { + /// The uri to share. Uri? uri, @@ -26,12 +27,7 @@ Future launchShareDialog( if (uri != null) { return Share.shareUri(uri); } else if (files != null) { - return Share.shareXFiles( - files, - subject: subject, - text: text, - sharePositionOrigin: origin, - ); + return Share.shareXFiles(files, subject: subject, text: text, sharePositionOrigin: origin); } else if (text != null) { return Share.share(text, subject: subject, sharePositionOrigin: origin); } diff --git a/lib/src/utils/system.dart b/lib/src/utils/system.dart index f63a2e5869..77fbb21e7a 100644 --- a/lib/src/utils/system.dart +++ b/lib/src/utils/system.dart @@ -31,8 +31,7 @@ class System { Future clearUserData() async { if (Platform.isAndroid) { try { - final result = - await _channel.invokeMethod('clearApplicationUserData'); + final result = await _channel.invokeMethod('clearApplicationUserData'); return result ?? false; } on PlatformException catch (e) { debugPrint('Failed to clear user data: ${e.message}'); @@ -45,8 +44,7 @@ class System { } /// A provider that returns OS version of an Android device. -final androidVersionProvider = - FutureProvider((ref) async { +final androidVersionProvider = FutureProvider((ref) async { if (!Platform.isAndroid) { return null; } diff --git a/lib/src/view/account/edit_profile_screen.dart b/lib/src/view/account/edit_profile_screen.dart index c567d5e362..d0329a0b23 100644 --- a/lib/src/view/account/edit_profile_screen.dart +++ b/lib/src/view/account/edit_profile_screen.dart @@ -23,9 +23,7 @@ class EditProfileScreen extends StatelessWidget { @override Widget build(BuildContext context) { return PlatformScaffold( - appBar: PlatformAppBar( - title: Text(context.l10n.editProfile), - ), + appBar: PlatformAppBar(title: Text(context.l10n.editProfile)), body: _Body(), ); } @@ -38,9 +36,7 @@ class _Body extends ConsumerWidget { return account.when( data: (data) { if (data == null) { - return Center( - child: Text(context.l10n.mobileMustBeLoggedIn), - ); + return Center(child: Text(context.l10n.mobileMustBeLoggedIn)); } return Padding( padding: Styles.bodyPadding, @@ -88,10 +84,7 @@ class _EditProfileFormState extends ConsumerState<_EditProfileForm> { final _cupertinoTextFieldDecoration = BoxDecoration( color: CupertinoColors.tertiarySystemBackground, - border: Border.all( - color: CupertinoColors.systemGrey4, - width: 1, - ), + border: Border.all(color: CupertinoColors.systemGrey4, width: 1), borderRadius: BorderRadius.circular(8), ); @@ -99,8 +92,7 @@ class _EditProfileFormState extends ConsumerState<_EditProfileForm> { @override Widget build(BuildContext context) { - final String? initialLinks = - widget.user.profile?.links?.map((e) => e.url).join('\r\n'); + final String? initialLinks = widget.user.profile?.links?.map((e) => e.url).join('\r\n'); return Form( key: _formKey, @@ -110,9 +102,7 @@ class _EditProfileFormState extends ConsumerState<_EditProfileForm> { label: context.l10n.biography, initialValue: widget.user.profile?.bio, formKey: 'bio', - controller: TextEditingController( - text: widget.user.profile?.bio, - ), + controller: TextEditingController(text: widget.user.profile?.bio), description: context.l10n.biographyDescription, maxLength: 400, maxLines: 6, @@ -133,17 +123,16 @@ class _EditProfileFormState extends ConsumerState<_EditProfileForm> { AdaptiveAutoComplete( cupertinoDecoration: _cupertinoTextFieldDecoration, textInputAction: TextInputAction.next, - initialValue: field.value != null - ? TextEditingValue(text: countries[field.value]!) - : null, + initialValue: + field.value != null + ? TextEditingValue(text: countries[field.value]!) + : null, optionsBuilder: (TextEditingValue value) { if (value.text.isEmpty) { return const Iterable.empty(); } return _countries.where((String option) { - return option - .toLowerCase() - .contains(value.text.toLowerCase()); + return option.toLowerCase().contains(value.text.toLowerCase()); }); }, onSelected: (String selection) { @@ -161,9 +150,7 @@ class _EditProfileFormState extends ConsumerState<_EditProfileForm> { _textField( label: context.l10n.location, initialValue: widget.user.profile?.location, - controller: TextEditingController( - text: widget.user.profile?.location, - ), + controller: TextEditingController(text: widget.user.profile?.location), formKey: 'location', maxLength: 80, ), @@ -171,18 +158,14 @@ class _EditProfileFormState extends ConsumerState<_EditProfileForm> { label: context.l10n.realName, initialValue: widget.user.profile?.realName, formKey: 'realName', - controller: TextEditingController( - text: widget.user.profile?.realName, - ), + controller: TextEditingController(text: widget.user.profile?.realName), maxLength: 20, ), _numericField( label: context.l10n.xRating('FIDE'), initialValue: widget.user.profile?.fideRating, formKey: 'fideRating', - controller: TextEditingController( - text: widget.user.profile?.fideRating?.toString(), - ), + controller: TextEditingController(text: widget.user.profile?.fideRating?.toString()), validator: (value) { if (value != null && (value < 1400 || value > 3000)) { return 'Rating must be between 1400 and 3000'; @@ -194,9 +177,7 @@ class _EditProfileFormState extends ConsumerState<_EditProfileForm> { label: context.l10n.xRating('USCF'), initialValue: widget.user.profile?.uscfRating, formKey: 'uscfRating', - controller: TextEditingController( - text: widget.user.profile?.uscfRating?.toString(), - ), + controller: TextEditingController(text: widget.user.profile?.uscfRating?.toString()), validator: (value) { if (value != null && (value < 100 || value > 3000)) { return 'Rating must be between 100 and 3000'; @@ -208,9 +189,7 @@ class _EditProfileFormState extends ConsumerState<_EditProfileForm> { label: context.l10n.xRating('ECF'), initialValue: widget.user.profile?.ecfRating, formKey: 'ecfRating', - controller: TextEditingController( - text: widget.user.profile?.ecfRating?.toString(), - ), + controller: TextEditingController(text: widget.user.profile?.ecfRating?.toString()), textInputAction: TextInputAction.done, validator: (value) { if (value != null && (value < 0 || value > 3000)) { @@ -237,55 +216,52 @@ class _EditProfileFormState extends ConsumerState<_EditProfileForm> { builder: (context, snapshot) { return FatButton( semanticsLabel: context.l10n.apply, - onPressed: snapshot.connectionState == ConnectionState.waiting - ? null - : () async { - if (_formKey.currentState!.validate()) { - _formKey.currentState!.save(); - _formData.removeWhere((key, value) { - return value == null; - }); - final future = Result.capture( - ref.withClient( - (client) => - AccountRepository(client).saveProfile( - _formData.map( - (key, value) => - MapEntry(key, value.toString()), + onPressed: + snapshot.connectionState == ConnectionState.waiting + ? null + : () async { + if (_formKey.currentState!.validate()) { + _formKey.currentState!.save(); + _formData.removeWhere((key, value) { + return value == null; + }); + final future = Result.capture( + ref.withClient( + (client) => AccountRepository(client).saveProfile( + _formData.map((key, value) => MapEntry(key, value.toString())), ), ), - ), - ); + ); - setState(() { - _pendingSaveProfile = future; - }); + setState(() { + _pendingSaveProfile = future; + }); - final result = await future; + final result = await future; - result.match( - onError: (err, __) { - if (context.mounted) { - showPlatformSnackbar( - context, - 'Something went wrong', - type: SnackBarType.error, - ); - } - }, - onSuccess: (_) { - if (context.mounted) { - ref.invalidate(accountProvider); - showPlatformSnackbar( - context, - context.l10n.success, - type: SnackBarType.success, - ); - } - }, - ); - } - }, + result.match( + onError: (err, __) { + if (context.mounted) { + showPlatformSnackbar( + context, + 'Something went wrong', + type: SnackBarType.error, + ); + } + }, + onSuccess: (_) { + if (context.mounted) { + ref.invalidate(accountProvider); + showPlatformSnackbar( + context, + context.l10n.success, + type: SnackBarType.success, + ); + } + }, + ); + } + }, child: Text(context.l10n.apply), ); }, @@ -324,17 +300,15 @@ class _EditProfileFormState extends ConsumerState<_EditProfileForm> { maxLines: maxLines, cupertinoDecoration: _cupertinoTextFieldDecoration.copyWith( border: Border.all( - color: field.errorText == null - ? CupertinoColors.systemGrey4 - : context.lichessColors.error, + color: + field.errorText == null + ? CupertinoColors.systemGrey4 + : context.lichessColors.error, width: 1, ), ), - materialDecoration: field.errorText != null - ? InputDecoration( - errorText: field.errorText, - ) - : null, + materialDecoration: + field.errorText != null ? InputDecoration(errorText: field.errorText) : null, textInputAction: textInputAction, controller: controller, onChanged: (value) { @@ -345,14 +319,10 @@ class _EditProfileFormState extends ConsumerState<_EditProfileForm> { const SizedBox(height: 6.0), Text(description, style: Styles.formDescription), ], - if (Theme.of(context).platform == TargetPlatform.iOS && - field.errorText != null) + if (Theme.of(context).platform == TargetPlatform.iOS && field.errorText != null) Padding( padding: const EdgeInsets.only(top: 6.0), - child: Text( - field.errorText!, - style: Styles.formError, - ), + child: Text(field.errorText!, style: Styles.formError), ), ], ); @@ -387,31 +357,25 @@ class _EditProfileFormState extends ConsumerState<_EditProfileForm> { keyboardType: TextInputType.number, cupertinoDecoration: _cupertinoTextFieldDecoration.copyWith( border: Border.all( - color: field.errorText == null - ? CupertinoColors.systemGrey4 - : context.lichessColors.error, + color: + field.errorText == null + ? CupertinoColors.systemGrey4 + : context.lichessColors.error, width: 1, ), ), - materialDecoration: field.errorText != null - ? InputDecoration( - errorText: field.errorText, - ) - : null, + materialDecoration: + field.errorText != null ? InputDecoration(errorText: field.errorText) : null, textInputAction: textInputAction, controller: controller, onChanged: (value) { field.didChange(int.tryParse(value)); }, ), - if (Theme.of(context).platform == TargetPlatform.iOS && - field.errorText != null) + if (Theme.of(context).platform == TargetPlatform.iOS && field.errorText != null) Padding( padding: const EdgeInsets.only(top: 6.0), - child: Text( - field.errorText!, - style: Styles.formError, - ), + child: Text(field.errorText!, style: Styles.formError), ), ], ); diff --git a/lib/src/view/account/profile_screen.dart b/lib/src/view/account/profile_screen.dart index e94baae744..805d031e7f 100644 --- a/lib/src/view/account/profile_screen.dart +++ b/lib/src/view/account/profile_screen.dart @@ -24,9 +24,9 @@ class ProfileScreen extends ConsumerWidget { return PlatformScaffold( appBar: PlatformAppBar( title: account.when( - data: (user) => user == null - ? const SizedBox.shrink() - : UserFullNameWidget(user: user.lightUser), + data: + (user) => + user == null ? const SizedBox.shrink() : UserFullNameWidget(user: user.lightUser), loading: () => const SizedBox.shrink(), error: (error, _) => const SizedBox.shrink(), ), @@ -34,19 +34,14 @@ class ProfileScreen extends ConsumerWidget { AppBarIconButton( icon: const Icon(Icons.edit), semanticsLabel: context.l10n.editProfile, - onPressed: () => pushPlatformRoute( - context, - builder: (_) => const EditProfileScreen(), - ), + onPressed: () => pushPlatformRoute(context, builder: (_) => const EditProfileScreen()), ), ], ), body: account.when( data: (user) { if (user == null) { - return Center( - child: Text(context.l10n.mobileMustBeLoggedIn), - ); + return Center(child: Text(context.l10n.mobileMustBeLoggedIn)); } return ListView( children: [ @@ -59,9 +54,7 @@ class ProfileScreen extends ConsumerWidget { }, loading: () => const Center(child: CircularProgressIndicator()), error: (error, _) { - return FullScreenRetryRequest( - onRetry: () => ref.invalidate(accountProvider), - ); + return FullScreenRetryRequest(onRetry: () => ref.invalidate(accountProvider)); }, ), ); @@ -84,31 +77,33 @@ class AccountPerfCards extends ConsumerWidget { return const SizedBox.shrink(); } }, - loading: () => Shimmer( - child: Padding( - padding: padding ?? Styles.bodySectionPadding, - child: SizedBox( - height: 106, - child: ListView.separated( - padding: const EdgeInsets.symmetric(vertical: 3.0), - scrollDirection: Axis.horizontal, - itemCount: 5, - separatorBuilder: (context, index) => const SizedBox(width: 10), - itemBuilder: (context, index) => ShimmerLoading( - isLoading: true, - child: Container( - width: 100, - height: 100, - decoration: BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(10.0), - ), + loading: + () => Shimmer( + child: Padding( + padding: padding ?? Styles.bodySectionPadding, + child: SizedBox( + height: 106, + child: ListView.separated( + padding: const EdgeInsets.symmetric(vertical: 3.0), + scrollDirection: Axis.horizontal, + itemCount: 5, + separatorBuilder: (context, index) => const SizedBox(width: 10), + itemBuilder: + (context, index) => ShimmerLoading( + isLoading: true, + child: Container( + width: 100, + height: 100, + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(10.0), + ), + ), + ), ), ), ), ), - ), - ), error: (error, stack) => const SizedBox.shrink(), ); } diff --git a/lib/src/view/account/rating_pref_aware.dart b/lib/src/view/account/rating_pref_aware.dart index 46a0289348..a71faf9acc 100644 --- a/lib/src/view/account/rating_pref_aware.dart +++ b/lib/src/view/account/rating_pref_aware.dart @@ -9,11 +9,7 @@ class RatingPrefAware extends ConsumerWidget { /// in their settings. /// /// Optionally, a different [orElse] widget can be displayed if ratings are disabled. - const RatingPrefAware({ - required this.child, - this.orElse, - super.key, - }); + const RatingPrefAware({required this.child, this.orElse, super.key}); final Widget child; final Widget? orElse; diff --git a/lib/src/view/analysis/analysis_board.dart b/lib/src/view/analysis/analysis_board.dart index 8f37f0fe54..d454dd8ecc 100644 --- a/lib/src/view/analysis/analysis_board.dart +++ b/lib/src/view/analysis/analysis_board.dart @@ -43,35 +43,28 @@ class AnalysisBoardState extends ConsumerState { final boardPrefs = ref.watch(boardPreferencesProvider); final analysisPrefs = ref.watch(analysisPreferencesProvider); final enableComputerAnalysis = analysisPrefs.enableComputerAnalysis; - final showBestMoveArrow = - enableComputerAnalysis && analysisPrefs.showBestMoveArrow; - final showAnnotationsOnBoard = - enableComputerAnalysis && analysisPrefs.showAnnotations; - final evalBestMoves = enableComputerAnalysis - ? ref.watch( - engineEvaluationProvider.select((s) => s.eval?.bestMoves), - ) - : null; + final showBestMoveArrow = enableComputerAnalysis && analysisPrefs.showBestMoveArrow; + final showAnnotationsOnBoard = enableComputerAnalysis && analysisPrefs.showAnnotations; + final evalBestMoves = + enableComputerAnalysis + ? ref.watch(engineEvaluationProvider.select((s) => s.eval?.bestMoves)) + : null; final currentNode = analysisState.currentNode; - final annotation = - showAnnotationsOnBoard ? makeAnnotation(currentNode.nags) : null; + final annotation = showAnnotationsOnBoard ? makeAnnotation(currentNode.nags) : null; - final bestMoves = enableComputerAnalysis - ? evalBestMoves ?? currentNode.eval?.bestMoves - : null; + final bestMoves = enableComputerAnalysis ? evalBestMoves ?? currentNode.eval?.bestMoves : null; final sanMove = currentNode.sanMove; - final ISet bestMoveShapes = showBestMoveArrow && - analysisState.isEngineAvailable && - bestMoves != null - ? computeBestMoveShapes( - bestMoves, - currentNode.position.turn, - boardPrefs.pieceSet.assets, - ) - : ISet(); + final ISet bestMoveShapes = + showBestMoveArrow && analysisState.isEngineAvailable && bestMoves != null + ? computeBestMoveShapes( + bestMoves, + currentNode.position.turn, + boardPrefs.pieceSet.assets, + ) + : ISet(); return Chessboard( size: widget.boardSize, @@ -79,44 +72,39 @@ class AnalysisBoardState extends ConsumerState { lastMove: analysisState.lastMove as NormalMove?, orientation: analysisState.pov, game: GameData( - playerSide: analysisState.position.isGameOver - ? PlayerSide.none - : analysisState.position.turn == Side.white + playerSide: + analysisState.position.isGameOver + ? PlayerSide.none + : analysisState.position.turn == Side.white ? PlayerSide.white : PlayerSide.black, isCheck: boardPrefs.boardHighlights && analysisState.position.isCheck, sideToMove: analysisState.position.turn, validMoves: analysisState.validMoves, promotionMove: analysisState.promotionMove, - onMove: (move, {isDrop, captured}) => - ref.read(ctrlProvider.notifier).onUserMove( - move, - shouldReplace: widget.shouldReplaceChildOnUserMove, - ), - onPromotionSelection: (role) => - ref.read(ctrlProvider.notifier).onPromotionSelection(role), + onMove: + (move, {isDrop, captured}) => ref + .read(ctrlProvider.notifier) + .onUserMove(move, shouldReplace: widget.shouldReplaceChildOnUserMove), + onPromotionSelection: (role) => ref.read(ctrlProvider.notifier).onPromotionSelection(role), ), shapes: userShapes.union(bestMoveShapes), annotations: showAnnotationsOnBoard && sanMove != null && annotation != null ? altCastles.containsKey(sanMove.move.uci) - ? IMap({ - Move.parse(altCastles[sanMove.move.uci]!)!.to: annotation, - }) + ? IMap({Move.parse(altCastles[sanMove.move.uci]!)!.to: annotation}) : IMap({sanMove.move.to: annotation}) : null, settings: boardPrefs.toBoardSettings().copyWith( - borderRadius: widget.borderRadius, - boxShadow: widget.borderRadius != null - ? boardShadows - : const [], - drawShape: DrawShapeOptions( - enable: widget.enableDrawingShapes, - onCompleteShape: _onCompleteShape, - onClearShapes: _onClearShapes, - newShapeColor: boardPrefs.shapeColor.color, - ), - ), + borderRadius: widget.borderRadius, + boxShadow: widget.borderRadius != null ? boardShadows : const [], + drawShape: DrawShapeOptions( + enable: widget.enableDrawingShapes, + onCompleteShape: _onCompleteShape, + onClearShapes: _onClearShapes, + newShapeColor: boardPrefs.shapeColor.color, + ), + ), ); } diff --git a/lib/src/view/analysis/analysis_layout.dart b/lib/src/view/analysis/analysis_layout.dart index 40b9edf93c..ee43d4a8b3 100644 --- a/lib/src/view/analysis/analysis_layout.dart +++ b/lib/src/view/analysis/analysis_layout.dart @@ -11,16 +11,10 @@ import 'package:lichess_mobile/src/widgets/platform.dart'; /// The height of the board header or footer in the analysis layout. const kAnalysisBoardHeaderOrFooterHeight = 26.0; -typedef BoardBuilder = Widget Function( - BuildContext context, - double boardSize, - BorderRadius? boardRadius, -); +typedef BoardBuilder = + Widget Function(BuildContext context, double boardSize, BorderRadius? boardRadius); -typedef EngineGaugeBuilder = Widget Function( - BuildContext context, - Orientation orientation, -); +typedef EngineGaugeBuilder = Widget Function(BuildContext context, Orientation orientation); enum AnalysisTab { opening(Icons.explore), @@ -45,11 +39,7 @@ enum AnalysisTab { /// Indicator for the analysis tab, typically shown in the app bar. class AppBarAnalysisTabIndicator extends StatefulWidget { - const AppBarAnalysisTabIndicator({ - required this.tabs, - required this.controller, - super.key, - }); + const AppBarAnalysisTabIndicator({required this.tabs, required this.controller, super.key}); final TabController controller; @@ -60,12 +50,10 @@ class AppBarAnalysisTabIndicator extends StatefulWidget { final List tabs; @override - State createState() => - _AppBarAnalysisTabIndicatorState(); + State createState() => _AppBarAnalysisTabIndicatorState(); } -class _AppBarAnalysisTabIndicatorState - extends State { +class _AppBarAnalysisTabIndicatorState extends State { @override void didChangeDependencies() { super.didChangeDependencies(); @@ -102,15 +90,16 @@ class _AppBarAnalysisTabIndicatorState onPressed: () { showAdaptiveActionSheet( context: context, - actions: widget.tabs.map((tab) { - return BottomSheetAction( - leading: Icon(tab.icon), - makeLabel: (context) => Text(tab.l10n(context.l10n)), - onPressed: (_) { - widget.controller.animateTo(widget.tabs.indexOf(tab)); - }, - ); - }).toList(), + actions: + widget.tabs.map((tab) { + return BottomSheetAction( + leading: Icon(tab.icon), + makeLabel: (context) => Text(tab.l10n(context.l10n)), + onPressed: (_) { + widget.controller.animateTo(widget.tabs.indexOf(tab)); + }, + ); + }).toList(), ); }, ); @@ -181,25 +170,23 @@ class AnalysisLayout extends StatelessWidget { bottom: false, child: LayoutBuilder( builder: (context, constraints) { - final orientation = constraints.maxWidth > constraints.maxHeight - ? Orientation.landscape - : Orientation.portrait; + final orientation = + constraints.maxWidth > constraints.maxHeight + ? Orientation.landscape + : Orientation.portrait; final isTablet = isTabletOrLarger(context); - const tabletBoardRadius = - BorderRadius.all(Radius.circular(4.0)); + const tabletBoardRadius = BorderRadius.all(Radius.circular(4.0)); if (orientation == Orientation.landscape) { - final headerAndFooterHeight = (boardHeader != null - ? kAnalysisBoardHeaderOrFooterHeight - : 0.0) + - (boardFooter != null - ? kAnalysisBoardHeaderOrFooterHeight - : 0.0); - final sideWidth = constraints.biggest.longestSide - - constraints.biggest.shortestSide; - final defaultBoardSize = constraints.biggest.shortestSide - - (kTabletBoardTableSidePadding * 2); - final boardSize = (sideWidth >= 250 + final headerAndFooterHeight = + (boardHeader != null ? kAnalysisBoardHeaderOrFooterHeight : 0.0) + + (boardFooter != null ? kAnalysisBoardHeaderOrFooterHeight : 0.0); + final sideWidth = + constraints.biggest.longestSide - constraints.biggest.shortestSide; + final defaultBoardSize = + constraints.biggest.shortestSide - (kTabletBoardTableSidePadding * 2); + final boardSize = + (sideWidth >= 250 ? defaultBoardSize : constraints.biggest.longestSide / kGoldenRatio - (kTabletBoardTableSidePadding * 2)) - @@ -214,15 +201,15 @@ class AnalysisLayout extends StatelessWidget { if (boardHeader != null) Container( decoration: BoxDecoration( - borderRadius: isTablet - ? tabletBoardRadius.copyWith( - bottomLeft: Radius.zero, - bottomRight: Radius.zero, - ) - : null, + borderRadius: + isTablet + ? tabletBoardRadius.copyWith( + bottomLeft: Radius.zero, + bottomRight: Radius.zero, + ) + : null, ), - clipBehavior: - isTablet ? Clip.hardEdge : Clip.none, + clipBehavior: isTablet ? Clip.hardEdge : Clip.none, child: SizedBox( height: kAnalysisBoardHeaderOrFooterHeight, width: boardSize, @@ -232,24 +219,22 @@ class AnalysisLayout extends StatelessWidget { boardBuilder( context, boardSize, - isTablet && - boardHeader == null && - boardFooter != null + isTablet && boardHeader == null && boardFooter != null ? tabletBoardRadius : null, ), if (boardFooter != null) Container( decoration: BoxDecoration( - borderRadius: isTablet - ? tabletBoardRadius.copyWith( - topLeft: Radius.zero, - topRight: Radius.zero, - ) - : null, + borderRadius: + isTablet + ? tabletBoardRadius.copyWith( + topLeft: Radius.zero, + topRight: Radius.zero, + ) + : null, ), - clipBehavior: - isTablet ? Clip.hardEdge : Clip.none, + clipBehavior: isTablet ? Clip.hardEdge : Clip.none, height: kAnalysisBoardHeaderOrFooterHeight, width: boardSize, child: boardFooter, @@ -258,10 +243,7 @@ class AnalysisLayout extends StatelessWidget { ), if (engineGaugeBuilder != null) ...[ const SizedBox(width: 4.0), - engineGaugeBuilder!( - context, - Orientation.landscape, - ), + engineGaugeBuilder!(context, Orientation.landscape), ], const SizedBox(width: 16.0), Expanded( @@ -273,14 +255,9 @@ class AnalysisLayout extends StatelessWidget { Expanded( child: PlatformCard( clipBehavior: Clip.hardEdge, - borderRadius: const BorderRadius.all( - Radius.circular(4.0), - ), + borderRadius: const BorderRadius.all(Radius.circular(4.0)), semanticContainer: false, - child: TabBarView( - controller: tabController, - children: children, - ), + child: TabBarView(controller: tabController, children: children), ), ), ], @@ -291,13 +268,12 @@ class AnalysisLayout extends StatelessWidget { ); } else { final defaultBoardSize = constraints.biggest.shortestSide; - final remainingHeight = - constraints.maxHeight - defaultBoardSize; - final isSmallScreen = - remainingHeight < kSmallRemainingHeightLeftBoardThreshold; - final boardSize = isTablet || isSmallScreen - ? defaultBoardSize - kTabletBoardTableSidePadding * 2 - : defaultBoardSize; + final remainingHeight = constraints.maxHeight - defaultBoardSize; + final isSmallScreen = remainingHeight < kSmallRemainingHeightLeftBoardThreshold; + final boardSize = + isTablet || isSmallScreen + ? defaultBoardSize - kTabletBoardTableSidePadding * 2 + : defaultBoardSize; return Column( mainAxisAlignment: MainAxisAlignment.center, @@ -305,55 +281,49 @@ class AnalysisLayout extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.center, children: [ if (engineGaugeBuilder != null) - engineGaugeBuilder!( - context, - Orientation.portrait, - ), + engineGaugeBuilder!(context, Orientation.portrait), if (engineLines != null) engineLines!, Padding( - padding: isTablet - ? const EdgeInsets.all( - kTabletBoardTableSidePadding, - ) - : EdgeInsets.zero, + padding: + isTablet + ? const EdgeInsets.all(kTabletBoardTableSidePadding) + : EdgeInsets.zero, child: Column( children: [ if (boardHeader != null) Container( decoration: BoxDecoration( - borderRadius: isTablet - ? tabletBoardRadius.copyWith( - bottomLeft: Radius.zero, - bottomRight: Radius.zero, - ) - : null, + borderRadius: + isTablet + ? tabletBoardRadius.copyWith( + bottomLeft: Radius.zero, + bottomRight: Radius.zero, + ) + : null, ), - clipBehavior: - isTablet ? Clip.hardEdge : Clip.none, + clipBehavior: isTablet ? Clip.hardEdge : Clip.none, height: kAnalysisBoardHeaderOrFooterHeight, child: boardHeader, ), boardBuilder( context, boardSize, - isTablet && - boardHeader == null && - boardFooter != null + isTablet && boardHeader == null && boardFooter != null ? tabletBoardRadius : null, ), if (boardFooter != null) Container( decoration: BoxDecoration( - borderRadius: isTablet - ? tabletBoardRadius.copyWith( - topLeft: Radius.zero, - topRight: Radius.zero, - ) - : null, + borderRadius: + isTablet + ? tabletBoardRadius.copyWith( + topLeft: Radius.zero, + topRight: Radius.zero, + ) + : null, ), - clipBehavior: - isTablet ? Clip.hardEdge : Clip.none, + clipBehavior: isTablet ? Clip.hardEdge : Clip.none, height: kAnalysisBoardHeaderOrFooterHeight, child: boardFooter, ), @@ -362,15 +332,13 @@ class AnalysisLayout extends StatelessWidget { ), Expanded( child: Padding( - padding: isTablet - ? const EdgeInsets.symmetric( - horizontal: kTabletBoardTableSidePadding, - ) - : EdgeInsets.zero, - child: TabBarView( - controller: tabController, - children: children, - ), + padding: + isTablet + ? const EdgeInsets.symmetric( + horizontal: kTabletBoardTableSidePadding, + ) + : EdgeInsets.zero, + child: TabBarView(controller: tabController, children: children), ), ), ], diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index edd2ce3d01..24b7517fd5 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -34,10 +34,7 @@ import 'tree_view.dart'; final _logger = Logger('AnalysisScreen'); class AnalysisScreen extends ConsumerStatefulWidget { - const AnalysisScreen({ - required this.options, - this.enableDrawingShapes = true, - }); + const AnalysisScreen({required this.options, this.enableDrawingShapes = true}); final AnalysisOptions options; @@ -62,11 +59,7 @@ class _AnalysisScreenState extends ConsumerState if (widget.options.gameId != null) AnalysisTab.summary, ]; - _tabController = TabController( - vsync: this, - initialIndex: 1, - length: tabs.length, - ); + _tabController = TabController(vsync: this, initialIndex: 1, length: tabs.length); } @override @@ -84,10 +77,7 @@ class _AnalysisScreenState extends ConsumerState final appBarActions = [ if (prefs.enableComputerAnalysis) EngineDepth(defaultEval: asyncState.valueOrNull?.currentNode.eval), - AppBarAnalysisTabIndicator( - tabs: tabs, - controller: _tabController, - ), + AppBarAnalysisTabIndicator(tabs: tabs, controller: _tabController), AppBarIconButton( onPressed: () { pushPlatformRoute( @@ -105,10 +95,7 @@ class _AnalysisScreenState extends ConsumerState case AsyncData(:final value): return PlatformScaffold( resizeToAvoidBottomInset: false, - appBar: PlatformAppBar( - title: _Title(variant: value.variant), - actions: appBarActions, - ), + appBar: PlatformAppBar(title: _Title(variant: value.variant), actions: appBarActions), body: _Body( options: widget.options, controller: _tabController, @@ -146,10 +133,7 @@ class _Title extends StatelessWidget { return Row( mainAxisSize: MainAxisSize.min, children: [ - if (!excludedIcons.contains(variant)) ...[ - Icon(variant.icon), - const SizedBox(width: 5.0), - ], + if (!excludedIcons.contains(variant)) ...[Icon(variant.icon), const SizedBox(width: 5.0)], Flexible( child: AutoSizeText( context.l10n.analysis, @@ -164,11 +148,7 @@ class _Title extends StatelessWidget { } class _Body extends ConsumerWidget { - const _Body({ - required this.options, - required this.controller, - required this.enableDrawingShapes, - }); + const _Body({required this.options, required this.controller, required this.enableDrawingShapes}); final TabController controller; final AnalysisOptions options; @@ -189,51 +169,49 @@ class _Body extends ConsumerWidget { return AnalysisLayout( tabController: controller, - boardBuilder: (context, boardSize, borderRadius) => AnalysisBoard( - options, - boardSize, - borderRadius: borderRadius, - enableDrawingShapes: enableDrawingShapes, - ), - engineGaugeBuilder: hasEval && showEvaluationGauge - ? (context, orientation) { - return orientation == Orientation.portrait - ? EngineGauge( + boardBuilder: + (context, boardSize, borderRadius) => AnalysisBoard( + options, + boardSize, + borderRadius: borderRadius, + enableDrawingShapes: enableDrawingShapes, + ), + engineGaugeBuilder: + hasEval && showEvaluationGauge + ? (context, orientation) { + return orientation == Orientation.portrait + ? EngineGauge( displayMode: EngineGaugeDisplayMode.horizontal, params: analysisState.engineGaugeParams, ) - : Container( + : Container( clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4.0), - ), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(4.0)), child: EngineGauge( displayMode: EngineGaugeDisplayMode.vertical, params: analysisState.engineGaugeParams, ), ); - } - : null, - engineLines: isEngineAvailable && numEvalLines > 0 - ? EngineLines( - onTapMove: ref.read(ctrlProvider.notifier).onUserMove, - clientEval: currentNode.eval, - isGameOver: currentNode.position.isGameOver, - ) - : null, + } + : null, + engineLines: + isEngineAvailable && numEvalLines > 0 + ? EngineLines( + onTapMove: ref.read(ctrlProvider.notifier).onUserMove, + clientEval: currentNode.eval, + isGameOver: currentNode.position.isGameOver, + ) + : null, bottomBar: _BottomBar(options: options), children: [ OpeningExplorerView( position: currentNode.position, - opening: kOpeningAllowedVariants.contains(analysisState.variant) - ? analysisState.currentNode.isRoot - ? LightOpening( - eco: '', - name: context.l10n.startPosition, - ) - : analysisState.currentNode.opening ?? - analysisState.currentBranchOpening - : null, + opening: + kOpeningAllowedVariants.contains(analysisState.variant) + ? analysisState.currentNode.isRoot + ? LightOpening(eco: '', name: context.l10n.startPosition) + : analysisState.currentNode.opening ?? analysisState.currentBranchOpening + : null, onMoveSelected: (move) { ref.read(ctrlProvider.notifier).onUserMove(move); }, @@ -270,8 +248,7 @@ class _BottomBar extends ConsumerWidget { icon: CupertinoIcons.arrow_2_squarepath, ), RepeatButton( - onLongPress: - analysisState.canGoBack ? () => _moveBackward(ref) : null, + onLongPress: analysisState.canGoBack ? () => _moveBackward(ref) : null, child: BottomBarButton( key: const ValueKey('goto-previous'), onTap: analysisState.canGoBack ? () => _moveBackward(ref) : null, @@ -306,23 +283,18 @@ class _BottomBar extends ConsumerWidget { BottomSheetAction( makeLabel: (context) => Text(context.l10n.flipBoard), onPressed: (context) { - ref - .read(analysisControllerProvider(options).notifier) - .toggleBoard(); + ref.read(analysisControllerProvider(options).notifier).toggleBoard(); }, ), BottomSheetAction( makeLabel: (context) => Text(context.l10n.boardEditor), onPressed: (context) { - final analysisState = - ref.read(analysisControllerProvider(options)).requireValue; + final analysisState = ref.read(analysisControllerProvider(options)).requireValue; final boardFen = analysisState.position.fen; pushPlatformRoute( context, title: context.l10n.boardEditor, - builder: (_) => BoardEditorScreen( - initialFen: boardFen, - ), + builder: (_) => BoardEditorScreen(initialFen: boardFen), ); }, ), @@ -339,45 +311,34 @@ class _BottomBar extends ConsumerWidget { BottomSheetAction( makeLabel: (context) => Text(context.l10n.mobileSharePositionAsFEN), onPressed: (_) { - final analysisState = - ref.read(analysisControllerProvider(options)).requireValue; - launchShareDialog( - context, - text: analysisState.position.fen, - ); + final analysisState = ref.read(analysisControllerProvider(options)).requireValue; + launchShareDialog(context, text: analysisState.position.fen); }, ), if (options.gameId != null) BottomSheetAction( - makeLabel: (context) => - Text(context.l10n.screenshotCurrentPosition), + makeLabel: (context) => Text(context.l10n.screenshotCurrentPosition), onPressed: (_) async { final gameId = options.gameId!; - final analysisState = - ref.read(analysisControllerProvider(options)).requireValue; + final analysisState = ref.read(analysisControllerProvider(options)).requireValue; try { - final image = - await ref.read(gameShareServiceProvider).screenshotPosition( - analysisState.pov, - analysisState.position.fen, - analysisState.lastMove, - ); + final image = await ref + .read(gameShareServiceProvider) + .screenshotPosition( + analysisState.pov, + analysisState.position.fen, + analysisState.lastMove, + ); if (context.mounted) { launchShareDialog( context, files: [image], - subject: context.l10n.puzzleFromGameLink( - lichessUri('/$gameId').toString(), - ), + subject: context.l10n.puzzleFromGameLink(lichessUri('/$gameId').toString()), ); } } catch (e) { if (context.mounted) { - showPlatformSnackbar( - context, - 'Failed to get GIF', - type: SnackBarType.error, - ); + showPlatformSnackbar(context, 'Failed to get GIF', type: SnackBarType.error); } } }, diff --git a/lib/src/view/analysis/analysis_settings.dart b/lib/src/view/analysis/analysis_settings.dart index 6c893a3539..896ac838be 100644 --- a/lib/src/view/analysis/analysis_settings.dart +++ b/lib/src/view/analysis/analysis_settings.dart @@ -29,9 +29,7 @@ class AnalysisSettings extends ConsumerWidget { switch (asyncState) { case AsyncData(:final value): return PlatformScaffold( - appBar: PlatformAppBar( - title: Text(context.l10n.settingsSettings), - ), + appBar: PlatformAppBar(title: Text(context.l10n.settingsSettings)), body: ListView( children: [ ListSection( @@ -46,9 +44,10 @@ class AnalysisSettings extends ConsumerWidget { ), AnimatedCrossFade( duration: const Duration(milliseconds: 300), - crossFadeState: value.isComputerAnalysisAllowedAndEnabled - ? CrossFadeState.showSecond - : CrossFadeState.showFirst, + crossFadeState: + value.isComputerAnalysisAllowedAndEnabled + ? CrossFadeState.showSecond + : CrossFadeState.showFirst, firstChild: const SizedBox.shrink(), secondChild: ListSection( margin: EdgeInsets.zero, @@ -58,30 +57,38 @@ class AnalysisSettings extends ConsumerWidget { SwitchSettingTile( title: Text(context.l10n.evaluationGauge), value: prefs.showEvaluationGauge, - onChanged: (value) => ref - .read(analysisPreferencesProvider.notifier) - .toggleShowEvaluationGauge(), + onChanged: + (value) => + ref + .read(analysisPreferencesProvider.notifier) + .toggleShowEvaluationGauge(), ), SwitchSettingTile( title: Text(context.l10n.toggleGlyphAnnotations), value: prefs.showAnnotations, - onChanged: (_) => ref - .read(analysisPreferencesProvider.notifier) - .toggleAnnotations(), + onChanged: + (_) => + ref + .read(analysisPreferencesProvider.notifier) + .toggleAnnotations(), ), SwitchSettingTile( title: Text(context.l10n.mobileShowComments), value: prefs.showPgnComments, - onChanged: (_) => ref - .read(analysisPreferencesProvider.notifier) - .togglePgnComments(), + onChanged: + (_) => + ref + .read(analysisPreferencesProvider.notifier) + .togglePgnComments(), ), SwitchSettingTile( title: Text(context.l10n.bestMoveArrow), value: prefs.showBestMoveArrow, - onChanged: (value) => ref - .read(analysisPreferencesProvider.notifier) - .toggleShowBestMoveArrow(), + onChanged: + (value) => + ref + .read(analysisPreferencesProvider.notifier) + .toggleShowBestMoveArrow(), ), ], ), @@ -90,9 +97,10 @@ class AnalysisSettings extends ConsumerWidget { ), AnimatedCrossFade( duration: const Duration(milliseconds: 300), - crossFadeState: value.isComputerAnalysisAllowedAndEnabled - ? CrossFadeState.showSecond - : CrossFadeState.showFirst, + crossFadeState: + value.isComputerAnalysisAllowedAndEnabled + ? CrossFadeState.showSecond + : CrossFadeState.showFirst, firstChild: const SizedBox.shrink(), secondChild: StockfishSettingsWidget( onToggleLocalEvaluation: () { @@ -113,21 +121,20 @@ class AnalysisSettings extends ConsumerWidget { children: [ PlatformListTile( title: Text(context.l10n.openingExplorer), - onTap: () => showAdaptiveBottomSheet( - context: context, - isScrollControlled: true, - showDragHandle: true, - isDismissible: true, - builder: (_) => const OpeningExplorerSettings(), - ), + onTap: + () => showAdaptiveBottomSheet( + context: context, + isScrollControlled: true, + showDragHandle: true, + isDismissible: true, + builder: (_) => const OpeningExplorerSettings(), + ), ), SwitchSettingTile( title: Text(context.l10n.sound), value: isSoundEnabled, onChanged: (value) { - ref - .read(generalPreferencesProvider.notifier) - .toggleSoundEnabled(); + ref.read(generalPreferencesProvider.notifier).toggleSoundEnabled(); }, ), ], diff --git a/lib/src/view/analysis/analysis_share_screen.dart b/lib/src/view/analysis/analysis_share_screen.dart index 63552ffe2e..24ed1b3170 100644 --- a/lib/src/view/analysis/analysis_share_screen.dart +++ b/lib/src/view/analysis/analysis_share_screen.dart @@ -24,20 +24,13 @@ class AnalysisShareScreen extends StatelessWidget { @override Widget build(BuildContext context) { return PlatformScaffold( - appBar: PlatformAppBar( - title: Text(context.l10n.studyShareAndExport), - ), + appBar: PlatformAppBar(title: Text(context.l10n.studyShareAndExport)), body: _EditPgnTagsForm(options), ); } } -const Set _ratingHeaders = { - 'WhiteElo', - 'BlackElo', - 'WhiteRatingDiff', - 'BlackRatingDiff', -}; +const Set _ratingHeaders = {'WhiteElo', 'BlackElo', 'WhiteRatingDiff', 'BlackRatingDiff'}; class _EditPgnTagsForm extends ConsumerStatefulWidget { const _EditPgnTagsForm(this.options); @@ -63,10 +56,7 @@ class _EditPgnTagsFormState extends ConsumerState<_EditPgnTagsForm> { _focusNodes[entry.key] = FocusNode(); _focusNodes[entry.key]!.addListener(() { if (!_focusNodes[entry.key]!.hasFocus) { - ref.read(ctrlProvider.notifier).updatePgnHeader( - entry.key, - _controllers[entry.key]!.text, - ); + ref.read(ctrlProvider.notifier).updatePgnHeader(entry.key, _controllers[entry.key]!.text); } }); } @@ -86,8 +76,7 @@ class _EditPgnTagsFormState extends ConsumerState<_EditPgnTagsForm> { @override Widget build(BuildContext context) { final ctrlProvider = analysisControllerProvider(widget.options); - final pgnHeaders = - ref.watch(ctrlProvider.select((c) => c.requireValue.pgnHeaders)); + final pgnHeaders = ref.watch(ctrlProvider.select((c) => c.requireValue.pgnHeaders)); final showRatingAsync = ref.watch(showRatingsPrefProvider); void focusAndSelectNextField(int index, IMap pgnHeaders) { @@ -127,46 +116,44 @@ class _EditPgnTagsFormState extends ConsumerState<_EditPgnTagsForm> { child: ListView( children: [ Column( - children: pgnHeaders.entries - .where( - (e) => showRatings || !_ratingHeaders.contains(e.key), - ) - .mapIndexed((index, e) { - return _EditablePgnField( - entry: e, - controller: _controllers[e.key]!, - focusNode: _focusNodes[e.key]!, - onTap: () { - _controllers[e.key]!.selection = TextSelection( - baseOffset: 0, - extentOffset: _controllers[e.key]!.text.length, - ); - if (e.key == 'Result') { - _showResultChoicePicker( - e, - context: context, - onEntryChanged: () { - focusAndSelectNextField(index, pgnHeaders); - }, - ); - } else if (e.key == 'Date') { - _showDatePicker( - e, - context: context, - onEntryChanged: () { - focusAndSelectNextField(index, pgnHeaders); - }, - ); - } - }, - onSubmitted: (value) { - ref - .read(ctrlProvider.notifier) - .updatePgnHeader(e.key, value); - focusAndSelectNextField(index, pgnHeaders); - }, - ); - }).toList(), + children: + pgnHeaders.entries + .where((e) => showRatings || !_ratingHeaders.contains(e.key)) + .mapIndexed((index, e) { + return _EditablePgnField( + entry: e, + controller: _controllers[e.key]!, + focusNode: _focusNodes[e.key]!, + onTap: () { + _controllers[e.key]!.selection = TextSelection( + baseOffset: 0, + extentOffset: _controllers[e.key]!.text.length, + ); + if (e.key == 'Result') { + _showResultChoicePicker( + e, + context: context, + onEntryChanged: () { + focusAndSelectNextField(index, pgnHeaders); + }, + ); + } else if (e.key == 'Date') { + _showDatePicker( + e, + context: context, + onEntryChanged: () { + focusAndSelectNextField(index, pgnHeaders); + }, + ); + } + }, + onSubmitted: (value) { + ref.read(ctrlProvider.notifier).updatePgnHeader(e.key, value); + focusAndSelectNextField(index, pgnHeaders); + }, + ); + }) + .toList(), ), Padding( padding: const EdgeInsets.all(24.0), @@ -177,13 +164,10 @@ class _EditPgnTagsFormState extends ConsumerState<_EditPgnTagsForm> { onPressed: () { launchShareDialog( context, - text: ref - .read( - analysisControllerProvider( - widget.options, - ).notifier, - ) - .makeExportPgn(), + text: + ref + .read(analysisControllerProvider(widget.options).notifier) + .makeExportPgn(), ); }, child: Text(context.l10n.mobileShareGamePGN), @@ -209,51 +193,47 @@ class _EditPgnTagsFormState extends ConsumerState<_EditPgnTagsForm> { if (Theme.of(context).platform == TargetPlatform.iOS) { return showCupertinoModalPopup( context: context, - builder: (BuildContext context) => Container( - height: 216, - padding: const EdgeInsets.only(top: 6.0), - margin: EdgeInsets.only( - bottom: MediaQuery.viewInsetsOf(context).bottom, - ), - color: CupertinoColors.systemBackground.resolveFrom(context), - child: SafeArea( - top: false, - child: CupertinoDatePicker( - mode: CupertinoDatePickerMode.date, - initialDateTime: entry.value.isNotEmpty - ? _dateFormatter.parse(entry.value) - : DateTime.now(), - onDateTimeChanged: (DateTime newDateTime) { - final newDate = _dateFormatter.format(newDateTime); - ref.read(ctrlProvider.notifier).updatePgnHeader( - entry.key, - newDate, - ); - _controllers[entry.key]!.text = newDate; - }, + builder: + (BuildContext context) => Container( + height: 216, + padding: const EdgeInsets.only(top: 6.0), + margin: EdgeInsets.only(bottom: MediaQuery.viewInsetsOf(context).bottom), + color: CupertinoColors.systemBackground.resolveFrom(context), + child: SafeArea( + top: false, + child: CupertinoDatePicker( + mode: CupertinoDatePickerMode.date, + initialDateTime: + entry.value.isNotEmpty ? _dateFormatter.parse(entry.value) : DateTime.now(), + onDateTimeChanged: (DateTime newDateTime) { + final newDate = _dateFormatter.format(newDateTime); + ref.read(ctrlProvider.notifier).updatePgnHeader(entry.key, newDate); + _controllers[entry.key]!.text = newDate; + }, + ), + ), ), - ), - ), ).then((_) { onEntryChanged(); }); } else { return showDatePicker( - context: context, - initialDate: entry.value.isNotEmpty - ? _dateFormatter.parse(entry.value) - : DateTime.now(), - firstDate: DateTime(1900), - lastDate: DateTime(2100), - ).then((date) { - if (date != null) { - final formatted = _dateFormatter.format(date); - ref.read(ctrlProvider.notifier).updatePgnHeader(entry.key, formatted); - _controllers[entry.key]!.text = formatted; - } - }).then((_) { - onEntryChanged(); - }); + context: context, + initialDate: + entry.value.isNotEmpty ? _dateFormatter.parse(entry.value) : DateTime.now(), + firstDate: DateTime(1900), + lastDate: DateTime(2100), + ) + .then((date) { + if (date != null) { + final formatted = _dateFormatter.format(date); + ref.read(ctrlProvider.notifier).updatePgnHeader(entry.key, formatted); + _controllers[entry.key]!.text = formatted; + } + }) + .then((_) { + onEntryChanged(); + }); } } @@ -269,13 +249,8 @@ class _EditPgnTagsFormState extends ConsumerState<_EditPgnTagsForm> { labelBuilder: (choice) => Text(choice), onSelectedItemChanged: (choice) { ref - .read( - analysisControllerProvider(widget.options).notifier, - ) - .updatePgnHeader( - entry.key, - choice, - ); + .read(analysisControllerProvider(widget.options).notifier) + .updatePgnHeader(entry.key, choice); _controllers[entry.key]!.text = choice; }, ).then((_) { @@ -302,18 +277,14 @@ class _EditablePgnField extends StatelessWidget { @override Widget build(BuildContext context) { return Padding( - padding: - Styles.horizontalBodyPadding.add(const EdgeInsets.only(bottom: 8.0)), + padding: Styles.horizontalBodyPadding.add(const EdgeInsets.only(bottom: 8.0)), child: Row( children: [ SizedBox( width: 110, child: Text( entry.key, - style: const TextStyle( - fontWeight: FontWeight.bold, - overflow: TextOverflow.ellipsis, - ), + style: const TextStyle(fontWeight: FontWeight.bold, overflow: TextOverflow.ellipsis), ), ), const SizedBox(width: 8), @@ -322,17 +293,15 @@ class _EditablePgnField extends StatelessWidget { focusNode: focusNode, cupertinoDecoration: BoxDecoration( color: CupertinoColors.tertiarySystemBackground, - border: Border.all( - color: CupertinoColors.systemGrey4, - width: 1, - ), + border: Border.all(color: CupertinoColors.systemGrey4, width: 1), borderRadius: BorderRadius.circular(8), ), controller: controller, textInputAction: TextInputAction.next, - keyboardType: entry.key == 'WhiteElo' || entry.key == 'BlackElo' - ? TextInputType.number - : TextInputType.text, + keyboardType: + entry.key == 'WhiteElo' || entry.key == 'BlackElo' + ? TextInputType.number + : TextInputType.text, onTap: onTap, onSubmitted: onSubmitted, ), diff --git a/lib/src/view/analysis/server_analysis.dart b/lib/src/view/analysis/server_analysis.dart index bf9ee77493..729ee8aa8f 100644 --- a/lib/src/view/analysis/server_analysis.dart +++ b/lib/src/view/analysis/server_analysis.dart @@ -31,8 +31,7 @@ class ServerAnalysisSummary extends ConsumerWidget { final canShowGameSummary = ref.watch( ctrlProvider.select((value) => value.requireValue.canShowGameSummary), ); - final pgnHeaders = ref - .watch(ctrlProvider.select((value) => value.requireValue.pgnHeaders)); + final pgnHeaders = ref.watch(ctrlProvider.select((value) => value.requireValue.pgnHeaders)); final currentGameAnalysis = ref.watch(currentAnalysisProvider); if (analysisPrefs.enableComputerAnalysis == false || !canShowGameSummary) { @@ -61,179 +60,138 @@ class ServerAnalysisSummary extends ConsumerWidget { return playersAnalysis != null ? ListView( - children: [ - if (currentGameAnalysis == options.gameId) - const Padding( - padding: EdgeInsets.only(top: 16.0), - child: WaitingForServerAnalysis(), - ), - AcplChart(options), - Center( - child: SizedBox( - width: math.min(MediaQuery.sizeOf(context).width, 500), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Table( - defaultVerticalAlignment: - TableCellVerticalAlignment.middle, - columnWidths: const { - 0: FlexColumnWidth(1), - 1: FlexColumnWidth(1), - 2: FlexColumnWidth(1), - }, - children: [ - TableRow( - decoration: const BoxDecoration( - border: Border( - bottom: BorderSide(color: Colors.grey), + children: [ + if (currentGameAnalysis == options.gameId) + const Padding(padding: EdgeInsets.only(top: 16.0), child: WaitingForServerAnalysis()), + AcplChart(options), + Center( + child: SizedBox( + width: math.min(MediaQuery.sizeOf(context).width, 500), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Table( + defaultVerticalAlignment: TableCellVerticalAlignment.middle, + columnWidths: const { + 0: FlexColumnWidth(1), + 1: FlexColumnWidth(1), + 2: FlexColumnWidth(1), + }, + children: [ + TableRow( + decoration: const BoxDecoration( + border: Border(bottom: BorderSide(color: Colors.grey)), + ), + children: [ + _SummaryPlayerName(Side.white, pgnHeaders), + Center( + child: Text( + pgnHeaders.get('Result') ?? '', + style: const TextStyle(fontWeight: FontWeight.bold), ), ), + _SummaryPlayerName(Side.black, pgnHeaders), + ], + ), + if (playersAnalysis.white.accuracy != null && + playersAnalysis.black.accuracy != null) + TableRow( children: [ - _SummaryPlayerName(Side.white, pgnHeaders), + _SummaryNumber('${playersAnalysis.white.accuracy}%'), Center( + heightFactor: 1.8, + child: Text(context.l10n.accuracy, softWrap: true), + ), + _SummaryNumber('${playersAnalysis.black.accuracy}%'), + ], + ), + for (final item in [ + ( + playersAnalysis.white.inaccuracies.toString(), + context.l10n.nbInaccuracies(2).replaceAll('2', '').trim().capitalize(), + playersAnalysis.black.inaccuracies.toString(), + ), + ( + playersAnalysis.white.mistakes.toString(), + context.l10n.nbMistakes(2).replaceAll('2', '').trim().capitalize(), + playersAnalysis.black.mistakes.toString(), + ), + ( + playersAnalysis.white.blunders.toString(), + context.l10n.nbBlunders(2).replaceAll('2', '').trim().capitalize(), + playersAnalysis.black.blunders.toString(), + ), + ]) + TableRow( + children: [ + _SummaryNumber(item.$1), + Center(heightFactor: 1.2, child: Text(item.$2, softWrap: true)), + _SummaryNumber(item.$3), + ], + ), + if (playersAnalysis.white.acpl != null && playersAnalysis.black.acpl != null) + TableRow( + children: [ + _SummaryNumber(playersAnalysis.white.acpl.toString()), + Center( + heightFactor: 1.5, child: Text( - pgnHeaders.get('Result') ?? '', - style: const TextStyle( - fontWeight: FontWeight.bold, - ), + context.l10n.averageCentipawnLoss, + softWrap: true, + textAlign: TextAlign.center, ), ), - _SummaryPlayerName(Side.black, pgnHeaders), + _SummaryNumber(playersAnalysis.black.acpl.toString()), ], ), - if (playersAnalysis.white.accuracy != null && - playersAnalysis.black.accuracy != null) - TableRow( - children: [ - _SummaryNumber( - '${playersAnalysis.white.accuracy}%', - ), - Center( - heightFactor: 1.8, - child: Text( - context.l10n.accuracy, - softWrap: true, - ), - ), - _SummaryNumber( - '${playersAnalysis.black.accuracy}%', - ), - ], - ), - for (final item in [ - ( - playersAnalysis.white.inaccuracies.toString(), - context.l10n - .nbInaccuracies(2) - .replaceAll('2', '') - .trim() - .capitalize(), - playersAnalysis.black.inaccuracies.toString() - ), - ( - playersAnalysis.white.mistakes.toString(), - context.l10n - .nbMistakes(2) - .replaceAll('2', '') - .trim() - .capitalize(), - playersAnalysis.black.mistakes.toString() - ), - ( - playersAnalysis.white.blunders.toString(), - context.l10n - .nbBlunders(2) - .replaceAll('2', '') - .trim() - .capitalize(), - playersAnalysis.black.blunders.toString() - ), - ]) - TableRow( - children: [ - _SummaryNumber(item.$1), - Center( - heightFactor: 1.2, - child: Text( - item.$2, - softWrap: true, - ), - ), - _SummaryNumber(item.$3), - ], - ), - if (playersAnalysis.white.acpl != null && - playersAnalysis.black.acpl != null) - TableRow( - children: [ - _SummaryNumber( - playersAnalysis.white.acpl.toString(), - ), - Center( - heightFactor: 1.5, - child: Text( - context.l10n.averageCentipawnLoss, - softWrap: true, - textAlign: TextAlign.center, - ), - ), - _SummaryNumber( - playersAnalysis.black.acpl.toString(), - ), - ], - ), - ], - ), + ], ), ), ), - ], - ) + ), + ], + ) : Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Spacer(), - if (currentGameAnalysis == options.gameId) - const Center( - child: Padding( - padding: EdgeInsets.symmetric(vertical: 16.0), - child: WaitingForServerAnalysis(), - ), - ) - else - Center( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0), - child: Builder( - builder: (context) { - Future? pendingRequest; - return StatefulBuilder( - builder: (context, setState) { - return FutureBuilder( - future: pendingRequest, - builder: (context, snapshot) { - return SecondaryButton( - semanticsLabel: - context.l10n.requestAComputerAnalysis, - onPressed: ref.watch(authSessionProvider) == - null - ? () { + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Spacer(), + if (currentGameAnalysis == options.gameId) + const Center( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 16.0), + child: WaitingForServerAnalysis(), + ), + ) + else + Center( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Builder( + builder: (context) { + Future? pendingRequest; + return StatefulBuilder( + builder: (context, setState) { + return FutureBuilder( + future: pendingRequest, + builder: (context, snapshot) { + return SecondaryButton( + semanticsLabel: context.l10n.requestAComputerAnalysis, + onPressed: + ref.watch(authSessionProvider) == null + ? () { showPlatformSnackbar( context, - context - .l10n.youNeedAnAccountToDoThat, + context.l10n.youNeedAnAccountToDoThat, ); } - : snapshot.connectionState == - ConnectionState.waiting - ? null - : () { - setState(() { - pendingRequest = ref - .read(ctrlProvider.notifier) - .requestServerAnalysis() - .catchError((Object e) { + : snapshot.connectionState == ConnectionState.waiting + ? null + : () { + setState(() { + pendingRequest = ref + .read(ctrlProvider.notifier) + .requestServerAnalysis() + .catchError((Object e) { if (context.mounted) { showPlatformSnackbar( context, @@ -242,23 +200,21 @@ class ServerAnalysisSummary extends ConsumerWidget { ); } }); - }); - }, - child: Text( - context.l10n.requestAComputerAnalysis, - ), - ); - }, - ); - }, - ); - }, - ), + }); + }, + child: Text(context.l10n.requestAComputerAnalysis), + ); + }, + ); + }, + ); + }, ), ), - const Spacer(), - ], - ); + ), + const Spacer(), + ], + ); } } @@ -271,11 +227,7 @@ class WaitingForServerAnalysis extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.max, children: [ - Image.asset( - 'assets/images/stockfish/icon.png', - width: 30, - height: 30, - ), + Image.asset('assets/images/stockfish/icon.png', width: 30, height: 30), const SizedBox(width: 8.0), Text(context.l10n.waitingForAnalysis), const SizedBox(width: 8.0), @@ -291,12 +243,7 @@ class _SummaryNumber extends StatelessWidget { @override Widget build(BuildContext context) { - return Center( - child: Text( - data, - softWrap: true, - ), - ); + return Center(child: Text(data, softWrap: true)); } } @@ -307,12 +254,12 @@ class _SummaryPlayerName extends StatelessWidget { @override Widget build(BuildContext context) { - final playerTitle = side == Side.white - ? pgnHeaders.get('WhiteTitle') - : pgnHeaders.get('BlackTitle'); - final playerName = side == Side.white - ? pgnHeaders.get('White') ?? context.l10n.white - : pgnHeaders.get('Black') ?? context.l10n.black; + final playerTitle = + side == Side.white ? pgnHeaders.get('WhiteTitle') : pgnHeaders.get('BlackTitle'); + final playerName = + side == Side.white + ? pgnHeaders.get('White') ?? context.l10n.white + : pgnHeaders.get('Black') ?? context.l10n.black; final brightness = Theme.of(context).brightness; @@ -329,15 +276,13 @@ class _SummaryPlayerName extends StatelessWidget { ? CupertinoIcons.circle : CupertinoIcons.circle_filled : brightness == Brightness.light - ? CupertinoIcons.circle_filled - : CupertinoIcons.circle, + ? CupertinoIcons.circle_filled + : CupertinoIcons.circle, size: 14, ), Text( '${playerTitle != null ? '$playerTitle ' : ''}$playerName', - style: const TextStyle( - fontWeight: FontWeight.bold, - ), + style: const TextStyle(fontWeight: FontWeight.bold), textAlign: TextAlign.center, softWrap: true, ), @@ -366,25 +311,21 @@ class AcplChart extends ConsumerWidget { final aboveLineColor = brightness == Brightness.light ? black : white; VerticalLine phaseVerticalBar(double x, String label) => VerticalLine( - x: x, - color: const Color(0xFF707070), - strokeWidth: 0.5, - label: VerticalLineLabel( - style: TextStyle( - fontSize: 10, - color: Theme.of(context) - .textTheme - .labelMedium - ?.color - ?.withValues(alpha: 0.3), - ), - labelResolver: (line) => label, - padding: const EdgeInsets.only(right: 1), - alignment: Alignment.topRight, - direction: LabelDirection.vertical, - show: true, - ), - ); + x: x, + color: const Color(0xFF707070), + strokeWidth: 0.5, + label: VerticalLineLabel( + style: TextStyle( + fontSize: 10, + color: Theme.of(context).textTheme.labelMedium?.color?.withValues(alpha: 0.3), + ), + labelResolver: (line) => label, + padding: const EdgeInsets.only(right: 1), + alignment: Alignment.topRight, + direction: LabelDirection.vertical, + show: true, + ), + ); final state = ref.watch(analysisControllerProvider(options)).requireValue; final data = state.acplChartData; @@ -397,9 +338,7 @@ class AcplChart extends ConsumerWidget { } final spots = data - .mapIndexed( - (i, e) => FlSpot(i.toDouble(), e.winningChances(Side.white)), - ) + .mapIndexed((i, e) => FlSpot(i.toDouble(), e.winningChances(Side.white))) .toList(growable: false); final divisionLines = []; @@ -408,10 +347,7 @@ class AcplChart extends ConsumerWidget { if (state.division!.middlegame! > 0) { divisionLines.add(phaseVerticalBar(0.0, context.l10n.opening)); divisionLines.add( - phaseVerticalBar( - state.division!.middlegame! - 1, - context.l10n.middlegame, - ), + phaseVerticalBar(state.division!.middlegame! - 1, context.l10n.middlegame), ); } else { divisionLines.add(phaseVerticalBar(0.0, context.l10n.middlegame)); @@ -420,19 +356,9 @@ class AcplChart extends ConsumerWidget { if (state.division?.endgame != null) { if (state.division!.endgame! > 0) { - divisionLines.add( - phaseVerticalBar( - state.division!.endgame! - 1, - context.l10n.endgame, - ), - ); + divisionLines.add(phaseVerticalBar(state.division!.endgame! - 1, context.l10n.endgame)); } else { - divisionLines.add( - phaseVerticalBar( - 0.0, - context.l10n.endgame, - ), - ); + divisionLines.add(phaseVerticalBar(0.0, context.l10n.endgame)); } } return Center( @@ -444,23 +370,19 @@ class AcplChart extends ConsumerWidget { LineChartData( lineTouchData: LineTouchData( enabled: false, - touchCallback: - (FlTouchEvent event, LineTouchResponse? touchResponse) { + touchCallback: (FlTouchEvent event, LineTouchResponse? touchResponse) { if (event is FlTapDownEvent || event is FlPanUpdateEvent || event is FlLongPressMoveUpdate) { final touchX = event.localPosition!.dx; - final chartWidth = context.size!.width - - 32; // Insets on both sides of the chart of 16 + final chartWidth = + context.size!.width - 32; // Insets on both sides of the chart of 16 final minX = spots.first.x; final maxX = spots.last.x; - final touchXDataValue = - minX + (touchX / chartWidth) * (maxX - minX); + final touchXDataValue = minX + (touchX / chartWidth) * (maxX - minX); final closestSpot = spots.reduce( - (a, b) => (a.x - touchXDataValue).abs() < - (b.x - touchXDataValue).abs() - ? a - : b, + (a, b) => + (a.x - touchXDataValue).abs() < (b.x - touchXDataValue).abs() ? a : b, ); final closestNodeIndex = closestSpot.x.round(); ref @@ -477,19 +399,9 @@ class AcplChart extends ConsumerWidget { isCurved: false, barWidth: 1, color: mainLineColor.withValues(alpha: 0.7), - aboveBarData: BarAreaData( - show: true, - color: aboveLineColor, - applyCutOffY: true, - ), - belowBarData: BarAreaData( - show: true, - color: belowLineColor, - applyCutOffY: true, - ), - dotData: const FlDotData( - show: false, - ), + aboveBarData: BarAreaData(show: true, color: aboveLineColor, applyCutOffY: true), + belowBarData: BarAreaData(show: true, color: belowLineColor, applyCutOffY: true), + dotData: const FlDotData(show: false), ), ], extraLinesData: ExtraLinesData( diff --git a/lib/src/view/analysis/stockfish_settings.dart b/lib/src/view/analysis/stockfish_settings.dart index a76795dad9..7448e430a6 100644 --- a/lib/src/view/analysis/stockfish_settings.dart +++ b/lib/src/view/analysis/stockfish_settings.dart @@ -39,18 +39,14 @@ class StockfishSettingsWidget extends ConsumerWidget { title: Text.rich( TextSpan( text: 'Search time: ', - style: const TextStyle( - fontWeight: FontWeight.normal, - ), + style: const TextStyle(fontWeight: FontWeight.normal), children: [ TextSpan( - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - ), - text: prefs.engineSearchTime.inSeconds == 3600 - ? '∞' - : '${prefs.engineSearchTime.inSeconds}s', + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18), + text: + prefs.engineSearchTime.inSeconds == 3600 + ? '∞' + : '${prefs.engineSearchTime.inSeconds}s', ), ], ), @@ -58,25 +54,18 @@ class StockfishSettingsWidget extends ConsumerWidget { subtitle: NonLinearSlider( labelBuilder: (value) => value == 3600 ? '∞' : '${value}s', value: prefs.engineSearchTime.inSeconds, - values: - kAvailableEngineSearchTimes.map((e) => e.inSeconds).toList(), - onChangeEnd: (value) => - onSetEngineSearchTime(Duration(seconds: value.toInt())), + values: kAvailableEngineSearchTimes.map((e) => e.inSeconds).toList(), + onChangeEnd: (value) => onSetEngineSearchTime(Duration(seconds: value.toInt())), ), ), PlatformListTile( title: Text.rich( TextSpan( text: '${context.l10n.multipleLines}: ', - style: const TextStyle( - fontWeight: FontWeight.normal, - ), + style: const TextStyle(fontWeight: FontWeight.normal), children: [ TextSpan( - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - ), + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18), text: prefs.numEvalLines.toString(), ), ], @@ -93,15 +82,10 @@ class StockfishSettingsWidget extends ConsumerWidget { title: Text.rich( TextSpan( text: '${context.l10n.cpus}: ', - style: const TextStyle( - fontWeight: FontWeight.normal, - ), + style: const TextStyle(fontWeight: FontWeight.normal), children: [ TextSpan( - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - ), + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18), text: prefs.numEngineCores.toString(), ), ], @@ -109,10 +93,7 @@ class StockfishSettingsWidget extends ConsumerWidget { ), subtitle: NonLinearSlider( value: prefs.numEngineCores, - values: List.generate( - maxEngineCores, - (index) => index + 1, - ), + values: List.generate(maxEngineCores, (index) => index + 1), onChangeEnd: (value) => onSetEngineCores(value.toInt()), ), ), diff --git a/lib/src/view/analysis/tree_view.dart b/lib/src/view/analysis/tree_view.dart index 7087ecba0d..983cff10dd 100644 --- a/lib/src/view/analysis/tree_view.dart +++ b/lib/src/view/analysis/tree_view.dart @@ -15,17 +15,14 @@ class AnalysisTreeView extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final ctrlProvider = analysisControllerProvider(options); - final root = - ref.watch(ctrlProvider.select((value) => value.requireValue.root)); - final currentPath = ref - .watch(ctrlProvider.select((value) => value.requireValue.currentPath)); + final root = ref.watch(ctrlProvider.select((value) => value.requireValue.root)); + final currentPath = ref.watch(ctrlProvider.select((value) => value.requireValue.currentPath)); final pgnRootComments = ref.watch( ctrlProvider.select((value) => value.requireValue.pgnRootComments), ); final prefs = ref.watch(analysisPreferencesProvider); // enable computer analysis takes effect here only if it's a lichess game - final enableComputerAnalysis = - !options.isLichessGameAnalysis || prefs.enableComputerAnalysis; + final enableComputerAnalysis = !options.isLichessGameAnalysis || prefs.enableComputerAnalysis; return ListView( padding: EdgeInsets.zero, @@ -37,8 +34,7 @@ class AnalysisTreeView extends ConsumerWidget { notifier: ref.read(ctrlProvider.notifier), shouldShowComputerVariations: enableComputerAnalysis, shouldShowComments: enableComputerAnalysis && prefs.showPgnComments, - shouldShowAnnotations: - enableComputerAnalysis && prefs.showAnnotations, + shouldShowAnnotations: enableComputerAnalysis && prefs.showAnnotations, ), ], ); diff --git a/lib/src/view/board_editor/board_editor_menu.dart b/lib/src/view/board_editor/board_editor_menu.dart index 6f35d17525..a8e60ec4cc 100644 --- a/lib/src/view/board_editor/board_editor_menu.dart +++ b/lib/src/view/board_editor/board_editor_menu.dart @@ -23,21 +23,20 @@ class BoardEditorMenu extends ConsumerWidget { padding: Styles.horizontalBodyPadding, child: Wrap( spacing: 8.0, - children: Side.values.map((side) { - return ChoiceChip( - label: Text( - side == Side.white - ? context.l10n.whitePlays - : context.l10n.blackPlays, - ), - selected: editorState.sideToPlay == side, - onSelected: (selected) { - if (selected) { - ref.read(editorController.notifier).setSideToPlay(side); - } - }, - ); - }).toList(), + children: + Side.values.map((side) { + return ChoiceChip( + label: Text( + side == Side.white ? context.l10n.whitePlays : context.l10n.blackPlays, + ), + selected: editorState.sideToPlay == side, + onSelected: (selected) { + if (selected) { + ref.read(editorController.notifier).setSideToPlay(side); + } + }, + ); + }).toList(), ), ), Padding( @@ -53,25 +52,17 @@ class BoardEditorMenu extends ConsumerWidget { SizedBox( width: 100.0, child: Text( - side == Side.white - ? context.l10n.white - : context.l10n.black, + side == Side.white ? context.l10n.white : context.l10n.black, maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ...[CastlingSide.king, CastlingSide.queen].map((castlingSide) { return ChoiceChip( - label: Text( - castlingSide == CastlingSide.king ? 'O-O' : 'O-O-O', - ), + label: Text(castlingSide == CastlingSide.king ? 'O-O' : 'O-O-O'), selected: editorState.isCastlingAllowed(side, castlingSide), onSelected: (selected) { - ref.read(editorController.notifier).setCastling( - side, - castlingSide, - selected, - ); + ref.read(editorController.notifier).setCastling(side, castlingSide, selected); }, ); }), @@ -86,17 +77,16 @@ class BoardEditorMenu extends ConsumerWidget { ), Wrap( spacing: 8.0, - children: editorState.enPassantOptions.squares.map((square) { - return ChoiceChip( - label: Text(square.name), - selected: editorState.enPassantSquare == square, - onSelected: (selected) { - ref - .read(editorController.notifier) - .toggleEnPassantSquare(square); - }, - ); - }).toList(), + children: + editorState.enPassantOptions.squares.map((square) { + return ChoiceChip( + label: Text(square.name), + selected: editorState.enPassantSquare == square, + onSelected: (selected) { + ref.read(editorController.notifier).toggleEnPassantSquare(square); + }, + ); + }).toList(), ), ], ], diff --git a/lib/src/view/board_editor/board_editor_screen.dart b/lib/src/view/board_editor/board_editor_screen.dart index e5a66bd187..2a06aae43c 100644 --- a/lib/src/view/board_editor/board_editor_screen.dart +++ b/lib/src/view/board_editor/board_editor_screen.dart @@ -28,9 +28,7 @@ class BoardEditorScreen extends StatelessWidget { @override Widget build(BuildContext context) { return PlatformScaffold( - appBar: PlatformAppBar( - title: Text(context.l10n.boardEditor), - ), + appBar: PlatformAppBar(title: Text(context.l10n.boardEditor)), body: _Body(initialFen), ); } @@ -43,8 +41,7 @@ class _Body extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final boardEditorState = - ref.watch(boardEditorControllerProvider(initialFen)); + final boardEditorState = ref.watch(boardEditorControllerProvider(initialFen)); return Column( children: [ @@ -57,16 +54,14 @@ class _Body extends ConsumerWidget { final defaultBoardSize = constraints.biggest.shortestSide; final isTablet = isTabletOrLarger(context); - final remainingHeight = - constraints.maxHeight - defaultBoardSize; - final isSmallScreen = - remainingHeight < kSmallRemainingHeightLeftBoardThreshold; - final boardSize = isTablet || isSmallScreen - ? defaultBoardSize - kTabletBoardTableSidePadding * 2 - : defaultBoardSize; + final remainingHeight = constraints.maxHeight - defaultBoardSize; + final isSmallScreen = remainingHeight < kSmallRemainingHeightLeftBoardThreshold; + final boardSize = + isTablet || isSmallScreen + ? defaultBoardSize - kTabletBoardTableSidePadding * 2 + : defaultBoardSize; - final direction = - aspectRatio > 1 ? Axis.horizontal : Axis.vertical; + final direction = aspectRatio > 1 ? Axis.horizontal : Axis.vertical; return Flex( direction: direction, @@ -133,21 +128,20 @@ class _BoardEditor extends ConsumerWidget { pieces: pieces, orientation: orientation, settings: boardPrefs.toBoardSettings().copyWith( - borderRadius: isTablet - ? const BorderRadius.all(Radius.circular(4.0)) - : BorderRadius.zero, - boxShadow: isTablet ? boardShadows : const [], - ), + borderRadius: isTablet ? const BorderRadius.all(Radius.circular(4.0)) : BorderRadius.zero, + boxShadow: isTablet ? boardShadows : const [], + ), pointerMode: editorState.editorPointerMode, - onDiscardedPiece: (Square square) => ref - .read(boardEditorControllerProvider(initialFen).notifier) - .discardPiece(square), - onDroppedPiece: (Square? origin, Square dest, Piece piece) => ref - .read(boardEditorControllerProvider(initialFen).notifier) - .movePiece(origin, dest, piece), - onEditedSquare: (Square square) => ref - .read(boardEditorControllerProvider(initialFen).notifier) - .editSquare(square), + onDiscardedPiece: + (Square square) => + ref.read(boardEditorControllerProvider(initialFen).notifier).discardPiece(square), + onDroppedPiece: + (Square? origin, Square dest, Piece piece) => ref + .read(boardEditorControllerProvider(initialFen).notifier) + .movePiece(origin, dest, piece), + onEditedSquare: + (Square square) => + ref.read(boardEditorControllerProvider(initialFen).notifier).editSquare(square), ); } } @@ -187,9 +181,8 @@ class _PieceMenuState extends ConsumerState<_PieceMenu> { return Container( clipBehavior: Clip.hardEdge, decoration: BoxDecoration( - borderRadius: widget.isTablet - ? const BorderRadius.all(Radius.circular(4.0)) - : BorderRadius.zero, + borderRadius: + widget.isTablet ? const BorderRadius.all(Radius.circular(4.0)) : BorderRadius.zero, boxShadow: widget.isTablet ? boardShadows : const [], ), child: ColoredBox( @@ -204,81 +197,69 @@ class _PieceMenuState extends ConsumerState<_PieceMenu> { height: squareSize, child: ColoredBox( key: Key('drag-button-${widget.side.name}'), - color: editorState.editorPointerMode == EditorPointerMode.drag - ? context.lichessColors.good - : Colors.transparent, + color: + editorState.editorPointerMode == EditorPointerMode.drag + ? context.lichessColors.good + : Colors.transparent, child: GestureDetector( - onTap: () => ref - .read(editorController.notifier) - .updateMode(EditorPointerMode.drag), - child: Icon( - CupertinoIcons.hand_draw, - size: 0.9 * squareSize, - ), + onTap: + () => ref.read(editorController.notifier).updateMode(EditorPointerMode.drag), + child: Icon(CupertinoIcons.hand_draw, size: 0.9 * squareSize), ), ), ), - ...Role.values.map( - (role) { - final piece = Piece(role: role, color: widget.side); - final pieceWidget = PieceWidget( - piece: piece, - size: squareSize, - pieceAssets: boardPrefs.pieceSet.assets, - ); + ...Role.values.map((role) { + final piece = Piece(role: role, color: widget.side); + final pieceWidget = PieceWidget( + piece: piece, + size: squareSize, + pieceAssets: boardPrefs.pieceSet.assets, + ); - return ColoredBox( - key: Key( - 'piece-button-${piece.color.name}-${piece.role.name}', - ), - color: ref - .read( - boardEditorControllerProvider( - widget.initialFen, - ), - ) - .activePieceOnEdit == - piece - ? Theme.of(context).colorScheme.primary - : Colors.transparent, - child: GestureDetector( - child: Draggable( - data: Piece(role: role, color: widget.side), - feedback: PieceDragFeedback( - piece: piece, - squareSize: squareSize, - pieceAssets: boardPrefs.pieceSet.assets, - ), - child: pieceWidget, - onDragEnd: (_) => ref - .read(editorController.notifier) - .updateMode(EditorPointerMode.drag), + return ColoredBox( + key: Key('piece-button-${piece.color.name}-${piece.role.name}'), + color: + ref.read(boardEditorControllerProvider(widget.initialFen)).activePieceOnEdit == + piece + ? Theme.of(context).colorScheme.primary + : Colors.transparent, + child: GestureDetector( + child: Draggable( + data: Piece(role: role, color: widget.side), + feedback: PieceDragFeedback( + piece: piece, + squareSize: squareSize, + pieceAssets: boardPrefs.pieceSet.assets, ), - onTap: () => ref - .read(editorController.notifier) - .updateMode(EditorPointerMode.edit, piece), + child: pieceWidget, + onDragEnd: + (_) => + ref.read(editorController.notifier).updateMode(EditorPointerMode.drag), ), - ); - }, - ), + onTap: + () => ref + .read(editorController.notifier) + .updateMode(EditorPointerMode.edit, piece), + ), + ); + }), SizedBox( key: Key('delete-button-${widget.side.name}'), width: squareSize, height: squareSize, child: ColoredBox( - color: editorState.deletePiecesActive - ? context.lichessColors.error - : Colors.transparent, + color: + editorState.deletePiecesActive + ? context.lichessColors.error + : Colors.transparent, child: GestureDetector( - onTap: () => { - ref - .read(editorController.notifier) - .updateMode(EditorPointerMode.edit, null), - }, - child: Icon( - CupertinoIcons.delete, - size: 0.8 * squareSize, - ), + onTap: + () => { + ref + .read(editorController.notifier) + .updateMode(EditorPointerMode.edit, null), + }, + child: Icon(CupertinoIcons.delete, size: 0.8 * squareSize), ), ), ), @@ -303,61 +284,54 @@ class _BottomBar extends ConsumerWidget { children: [ BottomBarButton( label: context.l10n.menu, - onTap: () => showAdaptiveBottomSheet( - context: context, - builder: (BuildContext context) => BoardEditorMenu( - initialFen: initialFen, - ), - showDragHandle: true, - constraints: BoxConstraints( - minHeight: MediaQuery.sizeOf(context).height * 0.5, - ), - ), + onTap: + () => showAdaptiveBottomSheet( + context: context, + builder: (BuildContext context) => BoardEditorMenu(initialFen: initialFen), + showDragHandle: true, + constraints: BoxConstraints(minHeight: MediaQuery.sizeOf(context).height * 0.5), + ), icon: Icons.tune, ), BottomBarButton( key: const Key('flip-button'), label: context.l10n.flipBoard, - onTap: ref - .read(boardEditorControllerProvider(initialFen).notifier) - .flipBoard, + onTap: ref.read(boardEditorControllerProvider(initialFen).notifier).flipBoard, icon: CupertinoIcons.arrow_2_squarepath, ), BottomBarButton( label: context.l10n.analysis, key: const Key('analysis-board-button'), - onTap: editorState.pgn != null && - // 1 condition (of many) where stockfish segfaults - pieceCount > 0 && - pieceCount <= 32 - ? () { - pushPlatformRoute( - context, - rootNavigator: true, - builder: (context) => AnalysisScreen( - options: AnalysisOptions( - orientation: editorState.orientation, - standalone: ( - pgn: editorState.pgn!, - isComputerAnalysisAllowed: true, - variant: Variant.fromPosition, - ), - ), - ), - ); - } - : null, + onTap: + editorState.pgn != null && + // 1 condition (of many) where stockfish segfaults + pieceCount > 0 && + pieceCount <= 32 + ? () { + pushPlatformRoute( + context, + rootNavigator: true, + builder: + (context) => AnalysisScreen( + options: AnalysisOptions( + orientation: editorState.orientation, + standalone: ( + pgn: editorState.pgn!, + isComputerAnalysisAllowed: true, + variant: Variant.fromPosition, + ), + ), + ), + ); + } + : null, icon: Icons.biotech, ), BottomBarButton( label: context.l10n.mobileSharePositionAsFEN, - onTap: () => launchShareDialog( - context, - text: editorState.fen, - ), - icon: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoIcons.share - : Icons.share, + onTap: () => launchShareDialog(context, text: editorState.fen), + icon: + Theme.of(context).platform == TargetPlatform.iOS ? CupertinoIcons.share : Icons.share, ), ], ); diff --git a/lib/src/view/broadcast/broadcast_boards_tab.dart b/lib/src/view/broadcast/broadcast_boards_tab.dart index 269f03d455..a58bebff61 100644 --- a/lib/src/view/broadcast/broadcast_boards_tab.dart +++ b/lib/src/view/broadcast/broadcast_boards_tab.dart @@ -24,17 +24,15 @@ const _kPlayerWidgetPadding = EdgeInsets.symmetric(vertical: 5.0); /// A tab that displays the live games of a broadcast round. class BroadcastBoardsTab extends ConsumerWidget { - const BroadcastBoardsTab({ - required this.roundId, - required this.broadcastTitle, - }); + const BroadcastBoardsTab({required this.roundId, required this.broadcastTitle}); final BroadcastRoundId roundId; final String broadcastTitle; @override Widget build(BuildContext context, WidgetRef ref) { - final edgeInsets = MediaQuery.paddingOf(context) - + final edgeInsets = + MediaQuery.paddingOf(context) - (Theme.of(context).platform == TargetPlatform.iOS ? EdgeInsets.only(top: MediaQuery.paddingOf(context).top) : EdgeInsets.zero) + @@ -44,8 +42,9 @@ class BroadcastBoardsTab extends ConsumerWidget { return SliverPadding( padding: edgeInsets, sliver: switch (round) { - AsyncData(:final value) => value.games.isEmpty - ? SliverPadding( + AsyncData(:final value) => + value.games.isEmpty + ? SliverPadding( padding: const EdgeInsets.only(top: 16.0), sliver: SliverToBoxAdapter( child: Column( @@ -57,22 +56,16 @@ class BroadcastBoardsTab extends ConsumerWidget { ), ), ) - : BroadcastPreview( + : BroadcastPreview( games: value.games.values.toIList(), roundId: roundId, broadcastTitle: broadcastTitle, roundTitle: value.round.name, ), AsyncError(:final error) => SliverFillRemaining( - child: Center( - child: Text('Could not load broadcast: $error'), - ), - ), - _ => const SliverFillRemaining( - child: Center( - child: CircularProgressIndicator.adaptive(), - ), - ), + child: Center(child: Text('Could not load broadcast: $error')), + ), + _ => const SliverFillRemaining(child: Center(child: CircularProgressIndicator.adaptive())), }, ); } @@ -86,11 +79,9 @@ class BroadcastPreview extends StatelessWidget { required this.roundTitle, }); - const BroadcastPreview.loading({ - required this.roundId, - required this.broadcastTitle, - }) : games = null, - roundTitle = null; + const BroadcastPreview.loading({required this.roundId, required this.broadcastTitle}) + : games = null, + roundTitle = null; final BroadcastRoundId roundId; final IList? games; @@ -108,7 +99,8 @@ class BroadcastPreview extends StatelessWidget { final headerAndFooterHeight = textHeight + _kPlayerWidgetPadding.vertical; final numberOfBoardsByRow = isTabletOrLarger(context) ? 3 : 2; final screenWidth = MediaQuery.sizeOf(context).width; - final boardWidth = (screenWidth - + final boardWidth = + (screenWidth - Styles.horizontalBodyPadding.horizontal - (numberOfBoardsByRow - 1) * boardSpacing) / numberOfBoardsByRow; @@ -143,12 +135,13 @@ class BroadcastPreview extends StatelessWidget { pushPlatformRoute( context, title: roundTitle, - builder: (context) => BroadcastGameScreen( - roundId: roundId, - gameId: game.id, - broadcastTitle: broadcastTitle, - roundTitle: roundTitle!, - ), + builder: + (context) => BroadcastGameScreen( + roundId: roundId, + gameId: game.id, + broadcastTitle: broadcastTitle, + roundTitle: roundTitle!, + ), ); }, orientation: Side.white, @@ -175,9 +168,7 @@ class BroadcastPreview extends StatelessWidget { } class _PlayerWidgetLoading extends StatelessWidget { - const _PlayerWidgetLoading({ - required this.width, - }); + const _PlayerWidgetLoading({required this.width}); final double width; @@ -189,10 +180,7 @@ class _PlayerWidgetLoading extends StatelessWidget { padding: _kPlayerWidgetPadding, child: Container( height: _kPlayerWidgetTextStyle.fontSize, - decoration: BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(5), - ), + decoration: BoxDecoration(color: Colors.black, borderRadius: BorderRadius.circular(5)), ), ), ); @@ -240,26 +228,26 @@ class _PlayerWidget extends StatelessWidget { (gameStatus == BroadcastResult.draw) ? '½' : (gameStatus == BroadcastResult.whiteWins) - ? side == Side.white - ? '1' - : '0' - : side == Side.black - ? '1' - : '0', - style: - const TextStyle().copyWith(fontWeight: FontWeight.bold), + ? side == Side.white + ? '1' + : '0' + : side == Side.black + ? '1' + : '0', + style: const TextStyle().copyWith(fontWeight: FontWeight.bold), ) else if (player.clock != null) CountdownClockBuilder( timeLeft: player.clock!, active: side == playingSide, - builder: (context, timeLeft) => Text( - timeLeft.toHoursMinutesSeconds(), - style: TextStyle( - color: (side == playingSide) ? Colors.orange[900] : null, - fontFeatures: const [FontFeature.tabularFigures()], - ), - ), + builder: + (context, timeLeft) => Text( + timeLeft.toHoursMinutesSeconds(), + style: TextStyle( + color: (side == playingSide) ? Colors.orange[900] : null, + fontFeatures: const [FontFeature.tabularFigures()], + ), + ), tickInterval: const Duration(seconds: 1), clockUpdatedAt: game.updatedClockAt, ), diff --git a/lib/src/view/broadcast/broadcast_game_bottom_bar.dart b/lib/src/view/broadcast/broadcast_game_bottom_bar.dart index a2c7a5b804..a9e7ee843d 100644 --- a/lib/src/view/broadcast/broadcast_game_bottom_bar.dart +++ b/lib/src/view/broadcast/broadcast_game_bottom_bar.dart @@ -56,14 +56,10 @@ class BroadcastGameBottomBar extends ConsumerWidget { onPressed: (context) async { try { final pgn = await ref.withClient( - (client) => BroadcastRepository(client) - .getGamePgn(roundId, gameId), + (client) => BroadcastRepository(client).getGamePgn(roundId, gameId), ); if (context.mounted) { - launchShareDialog( - context, - text: pgn, - ); + launchShareDialog(context, text: pgn); } } catch (e) { if (context.mounted) { @@ -80,16 +76,11 @@ class BroadcastGameBottomBar extends ConsumerWidget { makeLabel: (context) => const Text('GIF'), onPressed: (_) async { try { - final gif = - await ref.read(gameShareServiceProvider).chapterGif( - roundId, - gameId, - ); + final gif = await ref + .read(gameShareServiceProvider) + .chapterGif(roundId, gameId); if (context.mounted) { - launchShareDialog( - context, - files: [gif], - ); + launchShareDialog(context, files: [gif]); } } catch (e) { debugPrint(e.toString()); @@ -106,42 +97,33 @@ class BroadcastGameBottomBar extends ConsumerWidget { ], ); }, - icon: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoIcons.share - : Icons.share, + icon: + Theme.of(context).platform == TargetPlatform.iOS ? CupertinoIcons.share : Icons.share, ), BottomBarButton( label: context.l10n.flipBoard, onTap: () { - ref - .read( - ctrlProvider.notifier, - ) - .toggleBoard(); + ref.read(ctrlProvider.notifier).toggleBoard(); }, icon: CupertinoIcons.arrow_2_squarepath, ), RepeatButton( - onLongPress: - broadcastGameState.canGoBack ? () => _moveBackward(ref) : null, + onLongPress: broadcastGameState.canGoBack ? () => _moveBackward(ref) : null, child: BottomBarButton( key: const ValueKey('goto-previous'), - onTap: - broadcastGameState.canGoBack ? () => _moveBackward(ref) : null, + onTap: broadcastGameState.canGoBack ? () => _moveBackward(ref) : null, label: 'Previous', icon: CupertinoIcons.chevron_back, showTooltip: false, ), ), RepeatButton( - onLongPress: - broadcastGameState.canGoNext ? () => _moveForward(ref) : null, + onLongPress: broadcastGameState.canGoNext ? () => _moveForward(ref) : null, child: BottomBarButton( key: const ValueKey('goto-next'), icon: CupertinoIcons.chevron_forward, label: context.l10n.next, - onTap: - broadcastGameState.canGoNext ? () => _moveForward(ref) : null, + onTap: broadcastGameState.canGoNext ? () => _moveForward(ref) : null, showTooltip: false, ), ), @@ -149,10 +131,8 @@ class BroadcastGameBottomBar extends ConsumerWidget { ); } - void _moveForward(WidgetRef ref) => ref - .read(broadcastGameControllerProvider(roundId, gameId).notifier) - .userNext(); - void _moveBackward(WidgetRef ref) => ref - .read(broadcastGameControllerProvider(roundId, gameId).notifier) - .userPrevious(); + void _moveForward(WidgetRef ref) => + ref.read(broadcastGameControllerProvider(roundId, gameId).notifier).userNext(); + void _moveBackward(WidgetRef ref) => + ref.read(broadcastGameControllerProvider(roundId, gameId).notifier).userPrevious(); } diff --git a/lib/src/view/broadcast/broadcast_game_screen.dart b/lib/src/view/broadcast/broadcast_game_screen.dart index b9b367fe09..078f6e5d73 100644 --- a/lib/src/view/broadcast/broadcast_game_screen.dart +++ b/lib/src/view/broadcast/broadcast_game_screen.dart @@ -44,8 +44,7 @@ class BroadcastGameScreen extends ConsumerStatefulWidget { }); @override - ConsumerState createState() => - _BroadcastGameScreenState(); + ConsumerState createState() => _BroadcastGameScreenState(); } class _BroadcastGameScreenState extends ConsumerState @@ -57,16 +56,9 @@ class _BroadcastGameScreenState extends ConsumerState void initState() { super.initState(); - tabs = [ - AnalysisTab.opening, - AnalysisTab.moves, - ]; + tabs = [AnalysisTab.opening, AnalysisTab.moves]; - _tabController = TabController( - vsync: this, - initialIndex: 1, - length: tabs.length, - ); + _tabController = TabController(vsync: this, initialIndex: 1, length: tabs.length); } @override @@ -77,33 +69,25 @@ class _BroadcastGameScreenState extends ConsumerState @override Widget build(BuildContext context) { - final broadcastGameState = ref - .watch(broadcastGameControllerProvider(widget.roundId, widget.gameId)); + final broadcastGameState = ref.watch( + broadcastGameControllerProvider(widget.roundId, widget.gameId), + ); return PlatformScaffold( appBar: PlatformAppBar( - title: Text( - widget.roundTitle, - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), + title: Text(widget.roundTitle, overflow: TextOverflow.ellipsis, maxLines: 1), actions: [ - AppBarAnalysisTabIndicator( - tabs: tabs, - controller: _tabController, - ), + AppBarAnalysisTabIndicator(tabs: tabs, controller: _tabController), AppBarIconButton( - onPressed: (broadcastGameState.hasValue) - ? () { - pushPlatformRoute( - context, - screen: BroadcastGameSettings( - widget.roundId, - widget.gameId, - ), - ); - } - : null, + onPressed: + (broadcastGameState.hasValue) + ? () { + pushPlatformRoute( + context, + screen: BroadcastGameSettings(widget.roundId, widget.gameId), + ); + } + : null, semanticsLabel: context.l10n.settingsSettings, icon: const Icon(Icons.settings), ), @@ -111,15 +95,13 @@ class _BroadcastGameScreenState extends ConsumerState ), body: switch (broadcastGameState) { AsyncData() => _Body( - widget.roundId, - widget.gameId, - widget.broadcastTitle, - widget.roundTitle, - tabController: _tabController, - ), - AsyncError(:final error) => Center( - child: Text('Cannot load broadcast game: $error'), - ), + widget.roundId, + widget.gameId, + widget.broadcastTitle, + widget.roundTitle, + tabController: _tabController, + ), + AsyncError(:final error) => Center(child: Text('Cannot load broadcast game: $error')), _ => const Center(child: CircularProgressIndicator.adaptive()), }, ); @@ -143,9 +125,7 @@ class _Body extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final broadcastState = ref - .watch(broadcastGameControllerProvider(roundId, gameId)) - .requireValue; + final broadcastState = ref.watch(broadcastGameControllerProvider(roundId, gameId)).requireValue; final analysisPrefs = ref.watch(analysisPreferencesProvider); final showEvaluationGauge = analysisPrefs.showEvaluationGauge; final numEvalLines = analysisPrefs.numEvalLines; @@ -156,12 +136,9 @@ class _Body extends ConsumerWidget { return AnalysisLayout( tabController: tabController, - boardBuilder: (context, boardSize, borderRadius) => _BroadcastBoard( - roundId, - gameId, - boardSize, - borderRadius, - ), + boardBuilder: + (context, boardSize, borderRadius) => + _BroadcastBoard(roundId, gameId, boardSize, borderRadius), boardHeader: _PlayerWidget( roundId: roundId, gameId: gameId, @@ -172,55 +149,46 @@ class _Body extends ConsumerWidget { gameId: gameId, widgetPosition: _PlayerWidgetPosition.bottom, ), - engineGaugeBuilder: isLocalEvaluationEnabled && showEvaluationGauge - ? (context, orientation) { - return orientation == Orientation.portrait - ? EngineGauge( + engineGaugeBuilder: + isLocalEvaluationEnabled && showEvaluationGauge + ? (context, orientation) { + return orientation == Orientation.portrait + ? EngineGauge( displayMode: EngineGaugeDisplayMode.horizontal, params: engineGaugeParams, ) - : Container( + : Container( clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4.0), - ), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(4.0)), child: EngineGauge( displayMode: EngineGaugeDisplayMode.vertical, params: engineGaugeParams, ), ); - } - : null, - engineLines: isLocalEvaluationEnabled && numEvalLines > 0 - ? EngineLines( - clientEval: currentNode.eval, - isGameOver: currentNode.position.isGameOver, - onTapMove: ref - .read( - broadcastGameControllerProvider(roundId, gameId).notifier, - ) - .onUserMove, - ) - : null, + } + : null, + engineLines: + isLocalEvaluationEnabled && numEvalLines > 0 + ? EngineLines( + clientEval: currentNode.eval, + isGameOver: currentNode.position.isGameOver, + onTapMove: + ref.read(broadcastGameControllerProvider(roundId, gameId).notifier).onUserMove, + ) + : null, bottomBar: BroadcastGameBottomBar( roundId: roundId, gameId: gameId, broadcastTitle: broadcastTitle, roundTitle: roundTitle, ), - children: [ - _OpeningExplorerTab(roundId, gameId), - BroadcastGameTreeView(roundId, gameId), - ], + children: [_OpeningExplorerTab(roundId, gameId), BroadcastGameTreeView(roundId, gameId)], ); } } class _OpeningExplorerTab extends ConsumerWidget { - const _OpeningExplorerTab( - this.roundId, - this.gameId, - ); + const _OpeningExplorerTab(this.roundId, this.gameId); final BroadcastRoundId roundId; final BroadcastGameId gameId; @@ -238,12 +206,7 @@ class _OpeningExplorerTab extends ConsumerWidget { } class _BroadcastBoard extends ConsumerStatefulWidget { - const _BroadcastBoard( - this.roundId, - this.gameId, - this.boardSize, - this.borderRadius, - ); + const _BroadcastBoard(this.roundId, this.gameId, this.boardSize, this.borderRadius); final BroadcastRoundId roundId; final BroadcastGameId gameId; @@ -259,15 +222,12 @@ class _BroadcastBoardState extends ConsumerState<_BroadcastBoard> { @override Widget build(BuildContext context) { - final ctrlProvider = - broadcastGameControllerProvider(widget.roundId, widget.gameId); + final ctrlProvider = broadcastGameControllerProvider(widget.roundId, widget.gameId); final broadcastAnalysisState = ref.watch(ctrlProvider).requireValue; final boardPrefs = ref.watch(boardPreferencesProvider); final analysisPrefs = ref.watch(analysisPreferencesProvider); - final evalBestMoves = ref.watch( - engineEvaluationProvider.select((s) => s.eval?.bestMoves), - ); + final evalBestMoves = ref.watch(engineEvaluationProvider.select((s) => s.eval?.bestMoves)); final currentNode = broadcastAnalysisState.currentNode; final annotation = makeAnnotation(currentNode.nags); @@ -276,15 +236,16 @@ class _BroadcastBoardState extends ConsumerState<_BroadcastBoard> { final sanMove = currentNode.sanMove; - final ISet bestMoveShapes = analysisPrefs.showBestMoveArrow && - broadcastAnalysisState.isLocalEvaluationEnabled && - bestMoves != null - ? computeBestMoveShapes( - bestMoves, - currentNode.position.turn, - boardPrefs.pieceSet.assets, - ) - : ISet(); + final ISet bestMoveShapes = + analysisPrefs.showBestMoveArrow && + broadcastAnalysisState.isLocalEvaluationEnabled && + bestMoves != null + ? computeBestMoveShapes( + bestMoves, + currentNode.position.turn, + boardPrefs.pieceSet.assets, + ) + : ISet(); return Chessboard( size: widget.boardSize, @@ -292,42 +253,36 @@ class _BroadcastBoardState extends ConsumerState<_BroadcastBoard> { lastMove: broadcastAnalysisState.lastMove as NormalMove?, orientation: broadcastAnalysisState.pov, game: GameData( - playerSide: broadcastAnalysisState.position.isGameOver - ? PlayerSide.none - : broadcastAnalysisState.position.turn == Side.white + playerSide: + broadcastAnalysisState.position.isGameOver + ? PlayerSide.none + : broadcastAnalysisState.position.turn == Side.white ? PlayerSide.white : PlayerSide.black, - isCheck: boardPrefs.boardHighlights && - broadcastAnalysisState.position.isCheck, + isCheck: boardPrefs.boardHighlights && broadcastAnalysisState.position.isCheck, sideToMove: broadcastAnalysisState.position.turn, validMoves: broadcastAnalysisState.validMoves, promotionMove: broadcastAnalysisState.promotionMove, - onMove: (move, {isDrop, captured}) => - ref.read(ctrlProvider.notifier).onUserMove(move), - onPromotionSelection: (role) => - ref.read(ctrlProvider.notifier).onPromotionSelection(role), + onMove: (move, {isDrop, captured}) => ref.read(ctrlProvider.notifier).onUserMove(move), + onPromotionSelection: (role) => ref.read(ctrlProvider.notifier).onPromotionSelection(role), ), shapes: userShapes.union(bestMoveShapes), annotations: analysisPrefs.showAnnotations && sanMove != null && annotation != null ? altCastles.containsKey(sanMove.move.uci) - ? IMap({ - Move.parse(altCastles[sanMove.move.uci]!)!.to: annotation, - }) + ? IMap({Move.parse(altCastles[sanMove.move.uci]!)!.to: annotation}) : IMap({sanMove.move.to: annotation}) : null, settings: boardPrefs.toBoardSettings().copyWith( - borderRadius: widget.borderRadius, - boxShadow: widget.borderRadius != null - ? boardShadows - : const [], - drawShape: DrawShapeOptions( - enable: boardPrefs.enableShapeDrawings, - onCompleteShape: _onCompleteShape, - onClearShapes: _onClearShapes, - newShapeColor: boardPrefs.shapeColor.color, - ), - ), + borderRadius: widget.borderRadius, + boxShadow: widget.borderRadius != null ? boardShadows : const [], + drawShape: DrawShapeOptions( + enable: boardPrefs.enableShapeDrawings, + onCompleteShape: _onCompleteShape, + onClearShapes: _onClearShapes, + newShapeColor: boardPrefs.shapeColor.color, + ), + ), ); } @@ -354,11 +309,7 @@ class _BroadcastBoardState extends ConsumerState<_BroadcastBoard> { enum _PlayerWidgetPosition { bottom, top } class _PlayerWidget extends ConsumerWidget { - const _PlayerWidget({ - required this.roundId, - required this.gameId, - required this.widgetPosition, - }); + const _PlayerWidget({required this.roundId, required this.gameId, required this.widgetPosition}); final BroadcastRoundId roundId; final BroadcastGameId gameId; @@ -366,15 +317,15 @@ class _PlayerWidget extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final broadcastGameState = ref - .watch(broadcastGameControllerProvider(roundId, gameId)) - .requireValue; + final broadcastGameState = + ref.watch(broadcastGameControllerProvider(roundId, gameId)).requireValue; // TODO // we'll probably want to remove this and get the game state from a single controller // this won't work with deep links for instance final game = ref.watch( - broadcastRoundControllerProvider(roundId) - .select((round) => round.requireValue.games[gameId]!), + broadcastRoundControllerProvider( + roundId, + ).select((round) => round.requireValue.games[gameId]!), ); final isCursorOnLiveMove = broadcastGameState.currentPath == broadcastGameState.broadcastLivePath; @@ -388,13 +339,13 @@ class _PlayerWidget extends ConsumerWidget { final gameStatus = game.status; final pastClocks = broadcastGameState.clocks; - final pastClock = - (sideToMove == side) ? pastClocks?.parentClock : pastClocks?.clock; + final pastClock = (sideToMove == side) ? pastClocks?.parentClock : pastClocks?.clock; return Container( - color: Theme.of(context).platform == TargetPlatform.iOS - ? Styles.cupertinoCardColor.resolveFrom(context) - : Theme.of(context).colorScheme.surfaceContainer, + color: + Theme.of(context).platform == TargetPlatform.iOS + ? Styles.cupertinoCardColor.resolveFrom(context) + : Theme.of(context).colorScheme.surfaceContainer, padding: const EdgeInsets.only(left: 8.0), child: Row( children: [ @@ -403,12 +354,12 @@ class _PlayerWidget extends ConsumerWidget { (gameStatus == BroadcastResult.draw) ? '½' : (gameStatus == BroadcastResult.whiteWins) - ? side == Side.white - ? '1' - : '0' - : side == Side.black - ? '1' - : '0', + ? side == Side.white + ? '1' + : '0' + : side == Side.black + ? '1' + : '0', style: const TextStyle().copyWith(fontWeight: FontWeight.bold), ), const SizedBox(width: 16.0), @@ -419,38 +370,40 @@ class _PlayerWidget extends ConsumerWidget { title: player.title, name: player.name, rating: player.rating, - textStyle: - const TextStyle().copyWith(fontWeight: FontWeight.bold), + textStyle: const TextStyle().copyWith(fontWeight: FontWeight.bold), ), ), if (liveClock != null || pastClock != null) Container( height: kAnalysisBoardHeaderOrFooterHeight, - color: (side == sideToMove) - ? isCursorOnLiveMove - ? Theme.of(context).colorScheme.tertiaryContainer - : Theme.of(context).colorScheme.secondaryContainer - : Colors.transparent, + color: + (side == sideToMove) + ? isCursorOnLiveMove + ? Theme.of(context).colorScheme.tertiaryContainer + : Theme.of(context).colorScheme.secondaryContainer + : Colors.transparent, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 6.0), child: Center( - child: liveClock != null - ? CountdownClockBuilder( - timeLeft: liveClock, - active: side == sideToMove, - builder: (context, timeLeft) => _Clock( - timeLeft: timeLeft, + child: + liveClock != null + ? CountdownClockBuilder( + timeLeft: liveClock, + active: side == sideToMove, + builder: + (context, timeLeft) => _Clock( + timeLeft: timeLeft, + isSideToMove: side == sideToMove, + isLive: true, + ), + tickInterval: const Duration(seconds: 1), + clockUpdatedAt: game.updatedClockAt, + ) + : _Clock( + timeLeft: pastClock!, isSideToMove: side == sideToMove, - isLive: true, + isLive: false, ), - tickInterval: const Duration(seconds: 1), - clockUpdatedAt: game.updatedClockAt, - ) - : _Clock( - timeLeft: pastClock!, - isSideToMove: side == sideToMove, - isLive: false, - ), ), ), ), @@ -461,11 +414,7 @@ class _PlayerWidget extends ConsumerWidget { } class _Clock extends StatelessWidget { - const _Clock({ - required this.timeLeft, - required this.isSideToMove, - required this.isLive, - }); + const _Clock({required this.timeLeft, required this.isSideToMove, required this.isLive}); final Duration timeLeft; final bool isSideToMove; @@ -476,11 +425,12 @@ class _Clock extends StatelessWidget { return Text( timeLeft.toHoursMinutesSeconds(), style: TextStyle( - color: isSideToMove - ? isLive - ? Theme.of(context).colorScheme.onTertiaryContainer - : Theme.of(context).colorScheme.onSecondaryContainer - : null, + color: + isSideToMove + ? isLive + ? Theme.of(context).colorScheme.onTertiaryContainer + : Theme.of(context).colorScheme.onSecondaryContainer + : null, fontFeatures: const [FontFeature.tabularFigures()], ), ); diff --git a/lib/src/view/broadcast/broadcast_game_settings.dart b/lib/src/view/broadcast/broadcast_game_settings.dart index 4fd699e371..10d27db672 100644 --- a/lib/src/view/broadcast/broadcast_game_settings.dart +++ b/lib/src/view/broadcast/broadcast_game_settings.dart @@ -21,8 +21,7 @@ class BroadcastGameSettings extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final broacdcastGameAnalysisController = - broadcastGameControllerProvider(roundId, gameId); + final broacdcastGameAnalysisController = broadcastGameControllerProvider(roundId, gameId); final analysisPrefs = ref.watch(analysisPreferencesProvider); final isSoundEnabled = ref.watch( @@ -34,34 +33,31 @@ class BroadcastGameSettings extends ConsumerWidget { body: ListView( children: [ StockfishSettingsWidget( - onToggleLocalEvaluation: () => ref - .read(broacdcastGameAnalysisController.notifier) - .toggleLocalEvaluation(), - onSetEngineSearchTime: (value) => ref - .read(broacdcastGameAnalysisController.notifier) - .setEngineSearchTime(value), - onSetNumEvalLines: (value) => ref - .read(broacdcastGameAnalysisController.notifier) - .setNumEvalLines(value), - onSetEngineCores: (value) => ref - .read(broacdcastGameAnalysisController.notifier) - .setEngineCores(value), + onToggleLocalEvaluation: + () => ref.read(broacdcastGameAnalysisController.notifier).toggleLocalEvaluation(), + onSetEngineSearchTime: + (value) => + ref.read(broacdcastGameAnalysisController.notifier).setEngineSearchTime(value), + onSetNumEvalLines: + (value) => + ref.read(broacdcastGameAnalysisController.notifier).setNumEvalLines(value), + onSetEngineCores: + (value) => + ref.read(broacdcastGameAnalysisController.notifier).setEngineCores(value), ), ListSection( children: [ SwitchSettingTile( title: Text(context.l10n.toggleGlyphAnnotations), value: analysisPrefs.showAnnotations, - onChanged: (_) => ref - .read(analysisPreferencesProvider.notifier) - .toggleAnnotations(), + onChanged: + (_) => ref.read(analysisPreferencesProvider.notifier).toggleAnnotations(), ), SwitchSettingTile( title: Text(context.l10n.mobileShowComments), value: analysisPrefs.showPgnComments, - onChanged: (_) => ref - .read(analysisPreferencesProvider.notifier) - .togglePgnComments(), + onChanged: + (_) => ref.read(analysisPreferencesProvider.notifier).togglePgnComments(), ), ], ), @@ -69,21 +65,20 @@ class BroadcastGameSettings extends ConsumerWidget { children: [ PlatformListTile( title: Text(context.l10n.openingExplorer), - onTap: () => showAdaptiveBottomSheet( - context: context, - isScrollControlled: true, - showDragHandle: true, - isDismissible: true, - builder: (_) => const OpeningExplorerSettings(), - ), + onTap: + () => showAdaptiveBottomSheet( + context: context, + isScrollControlled: true, + showDragHandle: true, + isDismissible: true, + builder: (_) => const OpeningExplorerSettings(), + ), ), SwitchSettingTile( title: Text(context.l10n.sound), value: isSoundEnabled, onChanged: (value) { - ref - .read(generalPreferencesProvider.notifier) - .toggleSoundEnabled(); + ref.read(generalPreferencesProvider.notifier).toggleSoundEnabled(); }, ), ], diff --git a/lib/src/view/broadcast/broadcast_game_tree_view.dart b/lib/src/view/broadcast/broadcast_game_tree_view.dart index f691d03d5e..81c062ab17 100644 --- a/lib/src/view/broadcast/broadcast_game_tree_view.dart +++ b/lib/src/view/broadcast/broadcast_game_tree_view.dart @@ -8,10 +8,7 @@ import 'package:lichess_mobile/src/widgets/pgn.dart'; const kOpeningHeaderHeight = 32.0; class BroadcastGameTreeView extends ConsumerWidget { - const BroadcastGameTreeView( - this.roundId, - this.gameId, - ); + const BroadcastGameTreeView(this.roundId, this.gameId); final BroadcastRoundId roundId; final BroadcastGameId gameId; diff --git a/lib/src/view/broadcast/broadcast_list_screen.dart b/lib/src/view/broadcast/broadcast_list_screen.dart index f44ab4645b..ea46fd6082 100644 --- a/lib/src/view/broadcast/broadcast_list_screen.dart +++ b/lib/src/view/broadcast/broadcast_list_screen.dart @@ -37,21 +37,19 @@ class BroadcastListScreen extends StatelessWidget { maxLines: 1, ); return PlatformWidget( - androidBuilder: (_) => Scaffold( - body: const _Body(), - appBar: AppBar(title: title), - ), - iosBuilder: (_) => CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - middle: title, - automaticBackgroundVisibility: false, - backgroundColor: Styles.cupertinoAppBarColor - .resolveFrom(context) - .withValues(alpha: 0.0), - border: null, - ), - child: const _Body(), - ), + androidBuilder: (_) => Scaffold(body: const _Body(), appBar: AppBar(title: title)), + iosBuilder: + (_) => CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + middle: title, + automaticBackgroundVisibility: false, + backgroundColor: Styles.cupertinoAppBarColor + .resolveFrom(context) + .withValues(alpha: 0.0), + border: null, + ), + child: const _Body(), + ), ); } } @@ -67,8 +65,7 @@ class _BodyState extends ConsumerState<_Body> { final ScrollController _scrollController = ScrollController(); ImageColorWorker? _worker; - final GlobalKey _refreshIndicatorKey = - GlobalKey(); + final GlobalKey _refreshIndicatorKey = GlobalKey(); @override void initState() { @@ -95,8 +92,7 @@ class _BodyState extends ConsumerState<_Body> { } void _scrollListener() { - if (_scrollController.position.pixels >= - _scrollController.position.maxScrollExtent - 300) { + if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 300) { final broadcastList = ref.read(broadcastsPaginatorProvider); if (!broadcastList.isLoading) { @@ -110,27 +106,24 @@ class _BodyState extends ConsumerState<_Body> { final broadcasts = ref.watch(broadcastsPaginatorProvider); if (_worker == null || (!broadcasts.hasValue && broadcasts.isLoading)) { - return const Center( - child: CircularProgressIndicator.adaptive(), - ); + return const Center(child: CircularProgressIndicator.adaptive()); } if (!broadcasts.hasValue && broadcasts.isLoading) { - debugPrint( - 'SEVERE: [BroadcastsListScreen] could not load broadcast tournaments', - ); + debugPrint('SEVERE: [BroadcastsListScreen] could not load broadcast tournaments'); return const Center(child: Text('Could not load broadcast tournaments')); } final screenWidth = MediaQuery.sizeOf(context).width; - final itemsByRow = screenWidth >= 1200 - ? 3 - : screenWidth >= 700 + final itemsByRow = + screenWidth >= 1200 + ? 3 + : screenWidth >= 700 ? 2 : 1; const loadingItems = 12; - final pastItemsCount = broadcasts.requireValue.past.length + - (broadcasts.isLoading ? loadingItems : 0); + final pastItemsCount = + broadcasts.requireValue.past.length + (broadcasts.isLoading ? loadingItems : 0); final highTierGridDelegate = SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: itemsByRow, @@ -143,9 +136,10 @@ class _BodyState extends ConsumerState<_Body> { crossAxisCount: itemsByRow + 1, crossAxisSpacing: 16.0, mainAxisSpacing: 16.0, - childAspectRatio: screenWidth >= 1200 - ? 1.4 - : screenWidth >= 700 + childAspectRatio: + screenWidth >= 1200 + ? 1.4 + : screenWidth >= 700 ? 1.3 : 1.0, ); @@ -155,24 +149,21 @@ class _BodyState extends ConsumerState<_Body> { ('past', context.l10n.broadcastCompleted, broadcasts.value!.past), ]; - final activeHighTier = broadcasts.value!.active - .where( - (broadcast) => - broadcast.tour.tier != null && broadcast.tour.tier! >= 4, - ) - .toList(); + final activeHighTier = + broadcasts.value!.active + .where((broadcast) => broadcast.tour.tier != null && broadcast.tour.tier! >= 4) + .toList(); - final activeLowTier = broadcasts.value!.active - .where( - (broadcast) => - broadcast.tour.tier == null || broadcast.tour.tier! < 4, - ) - .toList(); + final activeLowTier = + broadcasts.value!.active + .where((broadcast) => broadcast.tour.tier == null || broadcast.tour.tier! < 4) + .toList(); return RefreshIndicator.adaptive( - edgeOffset: Theme.of(context).platform == TargetPlatform.iOS - ? MediaQuery.paddingOf(context).top + 16.0 - : 0, + edgeOffset: + Theme.of(context).platform == TargetPlatform.iOS + ? MediaQuery.paddingOf(context).top + 16.0 + : 0, key: _refreshIndicatorKey, onRefresh: () async => ref.refresh(broadcastsPaginatorProvider), child: Shimmer( @@ -215,10 +206,9 @@ class _BodyState extends ConsumerState<_Body> { : Styles.bodySectionPadding, sliver: SliverGrid.builder( gridDelegate: highTierGridDelegate, - itemBuilder: (context, index) => BroadcastCard( - worker: _worker!, - broadcast: activeHighTier[index], - ), + itemBuilder: + (context, index) => + BroadcastCard(worker: _worker!, broadcast: activeHighTier[index]), itemCount: activeHighTier.length, ), ), @@ -227,31 +217,28 @@ class _BodyState extends ConsumerState<_Body> { padding: Styles.bodySectionPadding, sliver: SliverGrid.builder( gridDelegate: lowTierGridDelegate, - itemBuilder: (context, index) => BroadcastCard( - worker: _worker!, - broadcast: activeLowTier[index], - ), + itemBuilder: + (context, index) => + BroadcastCard(worker: _worker!, broadcast: activeLowTier[index]), itemCount: activeLowTier.length, ), ), ] else SliverPadding( - padding: Theme.of(context).platform == TargetPlatform.iOS - ? Styles.horizontalBodyPadding - : Styles.bodySectionPadding, + padding: + Theme.of(context).platform == TargetPlatform.iOS + ? Styles.horizontalBodyPadding + : Styles.bodySectionPadding, sliver: SliverGrid.builder( gridDelegate: lowTierGridDelegate, - itemBuilder: (context, index) => - (broadcasts.isLoading && - index >= pastItemsCount - loadingItems) - ? ShimmerLoading( - isLoading: true, - child: BroadcastCard.loading(_worker!), - ) - : BroadcastCard( - worker: _worker!, - broadcast: section.$3[index], - ), + itemBuilder: + (context, index) => + (broadcasts.isLoading && index >= pastItemsCount - loadingItems) + ? ShimmerLoading( + isLoading: true, + child: BroadcastCard.loading(_worker!), + ) + : BroadcastCard(worker: _worker!, broadcast: section.$3[index]), itemCount: section.$3.length, ), ), @@ -259,9 +246,7 @@ class _BodyState extends ConsumerState<_Body> { ), const SliverSafeArea( top: false, - sliver: SliverToBoxAdapter( - child: SizedBox(height: 16.0), - ), + sliver: SliverToBoxAdapter(child: SizedBox(height: 16.0)), ), ], ), @@ -271,51 +256,44 @@ class _BodyState extends ConsumerState<_Body> { } class BroadcastCard extends StatefulWidget { - const BroadcastCard({ - required this.broadcast, - required this.worker, - super.key, - }); + const BroadcastCard({required this.broadcast, required this.worker, super.key}); final Broadcast broadcast; final ImageColorWorker worker; const BroadcastCard.loading(this.worker) - : broadcast = const Broadcast( - tour: BroadcastTournamentData( - id: BroadcastTournamentId(''), - name: '', - imageUrl: null, - description: '', - information: ( - format: null, - timeControl: null, - players: null, - website: null, - location: null, - dates: null, - ), + : broadcast = const Broadcast( + tour: BroadcastTournamentData( + id: BroadcastTournamentId(''), + name: '', + imageUrl: null, + description: '', + information: ( + format: null, + timeControl: null, + players: null, + website: null, + location: null, + dates: null, ), - round: BroadcastRound( - id: BroadcastRoundId(''), - name: '', - status: RoundStatus.finished, - startsAt: null, - finishedAt: null, - startsAfterPrevious: false, - ), - group: null, - roundToLinkId: BroadcastRoundId(''), - ); + ), + round: BroadcastRound( + id: BroadcastRoundId(''), + name: '', + status: RoundStatus.finished, + startsAt: null, + finishedAt: null, + startsAfterPrevious: false, + ), + group: null, + roundToLinkId: BroadcastRoundId(''), + ); @override State createState() => _BroadcastCartState(); } -typedef _CardColors = ({ - Color primaryContainer, - Color onPrimaryContainer, -}); +typedef _CardColors = ({Color primaryContainer, Color onPrimaryContainer}); final Map _colorsCache = {}; Future<_CardColors?> _computeImageColors( @@ -323,8 +301,7 @@ Future<_CardColors?> _computeImageColors( String imageUrl, ByteData imageBytes, ) async { - final response = - await worker.getImageColors(imageBytes.buffer.asUint32List()); + final response = await worker.getImageColors(imageBytes.buffer.asUint32List()); if (response != null) { final (:primaryContainer, :onPrimaryContainer) = response; final cardColors = ( @@ -368,8 +345,7 @@ class _BroadcastCartState extends State { await precacheImage(provider, context); final ui.Image scaledImage = await _imageProviderToScaled(provider); final imageBytes = await scaledImage.toByteData(); - final response = - await _computeImageColors(widget.worker, provider.url, imageBytes!); + final response = await _computeImageColors(widget.worker, provider.url, imageBytes!); if (response != null) { if (mounted) { setState(() { @@ -394,17 +370,13 @@ class _BroadcastCartState extends State { Theme.of(context).platform == TargetPlatform.iOS ? Styles.cupertinoCardColor.resolveFrom(context) : Theme.of(context).colorScheme.surfaceContainer; - final backgroundColor = - _cardColors?.primaryContainer ?? defaultBackgroundColor; + final backgroundColor = _cardColors?.primaryContainer ?? defaultBackgroundColor; final titleColor = _cardColors?.onPrimaryContainer; final subTitleColor = - _cardColors?.onPrimaryContainer.withValues(alpha: 0.8) ?? - textShade(context, 0.8); + _cardColors?.onPrimaryContainer.withValues(alpha: 0.8) ?? textShade(context, 0.8); final bgHsl = HSLColor.fromColor(backgroundColor); final liveHsl = HSLColor.fromColor(LichessColors.red); - final liveColor = - (bgHsl.lightness <= 0.5 ? liveHsl.withLightness(0.9) : liveHsl) - .toColor(); + final liveColor = (bgHsl.lightness <= 0.5 ? liveHsl.withLightness(0.9) : liveHsl).toColor(); return GestureDetector( onTap: () { @@ -412,8 +384,7 @@ class _BroadcastCartState extends State { context, title: widget.broadcast.title, rootNavigator: true, - builder: (context) => - BroadcastRoundScreen(broadcast: widget.broadcast), + builder: (context) => BroadcastRoundScreen(broadcast: widget.broadcast), ); }, onTapDown: (_) => _onTapDown(), @@ -428,9 +399,8 @@ class _BroadcastCartState extends State { decoration: BoxDecoration( borderRadius: kBroadcastGridItemBorderRadius, color: backgroundColor, - boxShadow: Theme.of(context).platform == TargetPlatform.iOS - ? null - : kElevationToShadow[1], + boxShadow: + Theme.of(context).platform == TargetPlatform.iOS ? null : kElevationToShadow[1], ), child: Stack( children: [ @@ -452,12 +422,7 @@ class _BroadcastCartState extends State { aspectRatio: 2.0, child: Image( image: imageProvider, - frameBuilder: ( - context, - child, - frame, - wasSynchronouslyLoaded, - ) { + frameBuilder: (context, child, frame, wasSynchronouslyLoaded) { if (wasSynchronouslyLoaded) { return child; } @@ -467,8 +432,8 @@ class _BroadcastCartState extends State { child: child, ); }, - errorBuilder: (context, error, stackTrace) => - const Image(image: kDefaultBroadcastImage), + errorBuilder: + (context, error, stackTrace) => const Image(image: kDefaultBroadcastImage), ), ), ), @@ -500,10 +465,7 @@ class _BroadcastCartState extends State { Expanded( child: Text( relativeDate(widget.broadcast.round.startsAt!), - style: TextStyle( - fontSize: 12, - color: subTitleColor, - ), + style: TextStyle(fontSize: 12, color: subTitleColor), overflow: TextOverflow.ellipsis, maxLines: 1, ), @@ -568,11 +530,7 @@ class _BroadcastCartState extends State { padding: kBroadcastGridItemContentPadding, child: Text( widget.broadcast.tour.information.players!, - style: TextStyle( - fontSize: 12, - color: subTitleColor, - letterSpacing: -0.2, - ), + style: TextStyle(fontSize: 12, color: subTitleColor, letterSpacing: -0.2), overflow: TextOverflow.ellipsis, maxLines: 1, ), @@ -630,10 +588,8 @@ Future _imageProviderToScaled(ImageProvider imageProvider) async { final bool rescale = width > maxDimension || height > maxDimension; if (rescale) { - paintWidth = - (width > height) ? maxDimension : (maxDimension / height) * width; - paintHeight = - (height > width) ? maxDimension : (maxDimension / width) * height; + paintWidth = (width > height) ? maxDimension : (maxDimension / height) * width; + paintHeight = (height > width) ? maxDimension : (maxDimension / width) * height; } final ui.PictureRecorder pictureRecorder = ui.PictureRecorder(); final Canvas canvas = Canvas(pictureRecorder); @@ -645,8 +601,7 @@ Future _imageProviderToScaled(ImageProvider imageProvider) async { ); final ui.Picture picture = pictureRecorder.endRecording(); - scaledImage = - await picture.toImage(paintWidth.toInt(), paintHeight.toInt()); + scaledImage = await picture.toImage(paintWidth.toInt(), paintHeight.toInt()); imageCompleter.complete(info.image); }, onError: (Object exception, StackTrace? stackTrace) { @@ -657,9 +612,7 @@ Future _imageProviderToScaled(ImageProvider imageProvider) async { loadFailureTimeout = Timer(const Duration(seconds: 5), () { stream.removeListener(listener); - imageCompleter.completeError( - TimeoutException('Timeout occurred trying to load image'), - ); + imageCompleter.completeError(TimeoutException('Timeout occurred trying to load image')); }); stream.addListener(listener); diff --git a/lib/src/view/broadcast/broadcast_overview_tab.dart b/lib/src/view/broadcast/broadcast_overview_tab.dart index 0208fed424..36b68cd26e 100644 --- a/lib/src/view/broadcast/broadcast_overview_tab.dart +++ b/lib/src/view/broadcast/broadcast_overview_tab.dart @@ -16,18 +16,15 @@ final _dateFormatter = DateFormat.MMMd(); /// A tab that displays the overview of a broadcast. class BroadcastOverviewTab extends ConsumerWidget { - const BroadcastOverviewTab({ - required this.broadcast, - required this.tournamentId, - super.key, - }); + const BroadcastOverviewTab({required this.broadcast, required this.tournamentId, super.key}); final Broadcast broadcast; final BroadcastTournamentId tournamentId; @override Widget build(BuildContext context, WidgetRef ref) { - final edgeInsets = MediaQuery.paddingOf(context) - + final edgeInsets = + MediaQuery.paddingOf(context) - (Theme.of(context).platform == TargetPlatform.iOS ? EdgeInsets.only(top: MediaQuery.paddingOf(context).top) : EdgeInsets.zero) + @@ -41,62 +38,51 @@ class BroadcastOverviewTab extends ConsumerWidget { return SliverPadding( padding: edgeInsets, sliver: SliverList( - delegate: SliverChildListDelegate( - [ - if (tournament.data.imageUrl != null) ...[ - Image.network(tournament.data.imageUrl!), - const SizedBox(height: 16.0), + delegate: SliverChildListDelegate([ + if (tournament.data.imageUrl != null) ...[ + Image.network(tournament.data.imageUrl!), + const SizedBox(height: 16.0), + ], + Wrap( + alignment: WrapAlignment.center, + children: [ + if (information.dates != null) + _BroadcastOverviewCard( + CupertinoIcons.calendar, + information.dates!.endsAt == null + ? _dateFormatter.format(information.dates!.startsAt) + : '${_dateFormatter.format(information.dates!.startsAt)} - ${_dateFormatter.format(information.dates!.endsAt!)}', + ), + if (information.format != null) + _BroadcastOverviewCard(Icons.emoji_events, '${information.format}'), + if (information.timeControl != null) + _BroadcastOverviewCard( + CupertinoIcons.stopwatch_fill, + '${information.timeControl}', + ), + if (information.location != null) + _BroadcastOverviewCard(Icons.public, '${information.location}'), + if (information.players != null) + _BroadcastOverviewCard(Icons.person, '${information.players}'), + if (information.website != null) + _BroadcastOverviewCard( + Icons.link, + context.l10n.broadcastOfficialWebsite, + information.website, + ), ], - Wrap( - alignment: WrapAlignment.center, - children: [ - if (information.dates != null) - _BroadcastOverviewCard( - CupertinoIcons.calendar, - information.dates!.endsAt == null - ? _dateFormatter.format(information.dates!.startsAt) - : '${_dateFormatter.format(information.dates!.startsAt)} - ${_dateFormatter.format(information.dates!.endsAt!)}', - ), - if (information.format != null) - _BroadcastOverviewCard( - Icons.emoji_events, - '${information.format}', - ), - if (information.timeControl != null) - _BroadcastOverviewCard( - CupertinoIcons.stopwatch_fill, - '${information.timeControl}', - ), - if (information.location != null) - _BroadcastOverviewCard( - Icons.public, - '${information.location}', - ), - if (information.players != null) - _BroadcastOverviewCard( - Icons.person, - '${information.players}', - ), - if (information.website != null) - _BroadcastOverviewCard( - Icons.link, - context.l10n.broadcastOfficialWebsite, - information.website, - ), - ], + ), + if (description != null) ...[ + const SizedBox(height: 16), + MarkdownBody( + data: description, + onTapLink: (text, url, title) { + if (url == null) return; + launchUrl(Uri.parse(url)); + }, ), - if (description != null) ...[ - const SizedBox(height: 16), - MarkdownBody( - data: description, - onTapLink: (text, url, title) { - if (url == null) return; - launchUrl(Uri.parse(url)); - }, - ), - ], ], - ), + ]), ), ); case AsyncError(:final error): @@ -137,20 +123,13 @@ class _BroadcastOverviewCard extends StatelessWidget { child: Row( mainAxisSize: MainAxisSize.min, children: [ - Icon( - iconData, - color: website != null - ? Theme.of(context).colorScheme.primary - : null, - ), + Icon(iconData, color: website != null ? Theme.of(context).colorScheme.primary : null), const SizedBox(width: 10), Flexible( child: Text( text, style: TextStyle( - color: website != null - ? Theme.of(context).colorScheme.primary - : null, + color: website != null ? Theme.of(context).colorScheme.primary : null, ), ), ), diff --git a/lib/src/view/broadcast/broadcast_player_widget.dart b/lib/src/view/broadcast/broadcast_player_widget.dart index 598cc62f46..9097313394 100644 --- a/lib/src/view/broadcast/broadcast_player_widget.dart +++ b/lib/src/view/broadcast/broadcast_player_widget.dart @@ -42,20 +42,10 @@ class BroadcastPlayerWidget extends ConsumerWidget { ), const SizedBox(width: 5), ], - Flexible( - child: Text( - name, - style: textStyle, - overflow: TextOverflow.ellipsis, - ), - ), + Flexible(child: Text(name, style: textStyle, overflow: TextOverflow.ellipsis)), if (rating != null) ...[ const SizedBox(width: 5), - Text( - rating.toString(), - style: const TextStyle(), - overflow: TextOverflow.ellipsis, - ), + Text(rating.toString(), style: const TextStyle(), overflow: TextOverflow.ellipsis), ], ], ); diff --git a/lib/src/view/broadcast/broadcast_players_tab.dart b/lib/src/view/broadcast/broadcast_players_tab.dart index 4b366f9d53..985e510b01 100644 --- a/lib/src/view/broadcast/broadcast_players_tab.dart +++ b/lib/src/view/broadcast/broadcast_players_tab.dart @@ -20,7 +20,8 @@ class BroadcastPlayersTab extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final edgeInsets = MediaQuery.paddingOf(context) - + final edgeInsets = + MediaQuery.paddingOf(context) - (Theme.of(context).platform == TargetPlatform.iOS ? EdgeInsets.only(top: MediaQuery.paddingOf(context).top) : EdgeInsets.zero) + @@ -30,16 +31,10 @@ class BroadcastPlayersTab extends ConsumerWidget { return switch (players) { AsyncData(value: final players) => PlayersList(players), AsyncError(:final error) => SliverPadding( - padding: edgeInsets, - sliver: SliverFillRemaining( - child: Center(child: Text('Cannot load players data: $error')), - ), - ), - _ => const SliverFillRemaining( - child: Center( - child: CircularProgressIndicator.adaptive(), - ), - ), + padding: edgeInsets, + sliver: SliverFillRemaining(child: Center(child: Text('Cannot load players data: $error'))), + ), + _ => const SliverFillRemaining(child: Center(child: CircularProgressIndicator.adaptive())), }; } } @@ -52,8 +47,7 @@ const _kTableRowPadding = EdgeInsets.symmetric( horizontal: _kTableRowHorizontalPadding, vertical: _kTableRowVerticalPadding, ); -const _kHeaderTextStyle = - TextStyle(fontWeight: FontWeight.bold, overflow: TextOverflow.ellipsis); +const _kHeaderTextStyle = TextStyle(fontWeight: FontWeight.bold, overflow: TextOverflow.ellipsis); class PlayersList extends ConsumerStatefulWidget { const PlayersList(this.players); @@ -72,20 +66,17 @@ class _PlayersListState extends ConsumerState { void sort(_SortingTypes newSort, {bool toggleReverse = false}) { final compare = switch (newSort) { _SortingTypes.player => - (BroadcastPlayerExtended a, BroadcastPlayerExtended b) => - a.name.compareTo(b.name), - _SortingTypes.elo => - (BroadcastPlayerExtended a, BroadcastPlayerExtended b) { - if (a.rating == null) return 1; - if (b.rating == null) return -1; - return b.rating!.compareTo(a.rating!); - }, - _SortingTypes.score => - (BroadcastPlayerExtended a, BroadcastPlayerExtended b) { - if (a.score == null) return 1; - if (b.score == null) return -1; - return b.score!.compareTo(a.score!); - } + (BroadcastPlayerExtended a, BroadcastPlayerExtended b) => a.name.compareTo(b.name), + _SortingTypes.elo => (BroadcastPlayerExtended a, BroadcastPlayerExtended b) { + if (a.rating == null) return 1; + if (b.rating == null) return -1; + return b.rating!.compareTo(a.rating!); + }, + _SortingTypes.score => (BroadcastPlayerExtended a, BroadcastPlayerExtended b) { + if (a.score == null) return 1; + if (b.score == null) return -1; + return b.score!.compareTo(a.score!); + }, }; setState(() { @@ -130,48 +121,43 @@ class _PlayersListState extends ConsumerState { Expanded( child: _TableTitleCell( title: Text(context.l10n.player, style: _kHeaderTextStyle), - onTap: () => sort( - _SortingTypes.player, - toggleReverse: currentSort == _SortingTypes.player, - ), - sortIcon: (currentSort == _SortingTypes.player) - ? (reverse - ? Icons.keyboard_arrow_up - : Icons.keyboard_arrow_down) - : null, + onTap: + () => sort( + _SortingTypes.player, + toggleReverse: currentSort == _SortingTypes.player, + ), + sortIcon: + (currentSort == _SortingTypes.player) + ? (reverse ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down) + : null, ), ), SizedBox( width: eloWidth, child: _TableTitleCell( title: const Text('Elo', style: _kHeaderTextStyle), - onTap: () => sort( - _SortingTypes.elo, - toggleReverse: currentSort == _SortingTypes.elo, - ), - sortIcon: (currentSort == _SortingTypes.elo) - ? (reverse - ? Icons.keyboard_arrow_up - : Icons.keyboard_arrow_down) - : null, + onTap: + () => + sort(_SortingTypes.elo, toggleReverse: currentSort == _SortingTypes.elo), + sortIcon: + (currentSort == _SortingTypes.elo) + ? (reverse ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down) + : null, ), ), SizedBox( width: scoreWidth, child: _TableTitleCell( - title: Text( - context.l10n.broadcastScore, - style: _kHeaderTextStyle, - ), - onTap: () => sort( - _SortingTypes.score, - toggleReverse: currentSort == _SortingTypes.score, - ), - sortIcon: (currentSort == _SortingTypes.score) - ? (reverse - ? Icons.keyboard_arrow_up - : Icons.keyboard_arrow_down) - : null, + title: Text(context.l10n.broadcastScore, style: _kHeaderTextStyle), + onTap: + () => sort( + _SortingTypes.score, + toggleReverse: currentSort == _SortingTypes.score, + ), + sortIcon: + (currentSort == _SortingTypes.score) + ? (reverse ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down) + : null, ), ), ], @@ -180,13 +166,12 @@ class _PlayersListState extends ConsumerState { final player = players[index - 1]; return Container( decoration: BoxDecoration( - color: Theme.of(context).platform == TargetPlatform.iOS - ? index.isEven - ? CupertinoColors.secondarySystemBackground - .resolveFrom(context) - : CupertinoColors.tertiarySystemBackground - .resolveFrom(context) - : index.isEven + color: + Theme.of(context).platform == TargetPlatform.iOS + ? index.isEven + ? CupertinoColors.secondarySystemBackground.resolveFrom(context) + : CupertinoColors.tertiarySystemBackground.resolveFrom(context) + : index.isEven ? Theme.of(context).colorScheme.surfaceContainerLow : Theme.of(context).colorScheme.surfaceContainerHigh, ), @@ -223,11 +208,12 @@ class _PlayersListState extends ConsumerState { width: scoreWidth, child: Padding( padding: _kTableRowPadding, - child: (player.score != null) - ? Text( - '${player.score!.toStringAsFixed((player.score! == player.score!.roundToDouble()) ? 0 : 1)}/${player.played}', - ) - : const SizedBox.shrink(), + child: + (player.score != null) + ? Text( + '${player.score!.toStringAsFixed((player.score! == player.score!.roundToDouble()) ? 0 : 1)}/${player.played}', + ) + : const SizedBox.shrink(), ), ), ], @@ -240,11 +226,7 @@ class _PlayersListState extends ConsumerState { } class _TableTitleCell extends StatelessWidget { - const _TableTitleCell({ - required this.title, - required this.onTap, - this.sortIcon, - }); + const _TableTitleCell({required this.title, required this.onTap, this.sortIcon}); final Widget title; final void Function() onTap; @@ -262,16 +244,11 @@ class _TableTitleCell extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Expanded( - child: title, - ), + Expanded(child: title), if (sortIcon != null) Text( String.fromCharCode(sortIcon!.codePoint), - style: _kHeaderTextStyle.copyWith( - fontSize: 16, - fontFamily: sortIcon!.fontFamily, - ), + style: _kHeaderTextStyle.copyWith(fontSize: 16, fontFamily: sortIcon!.fontFamily), ), ], ), diff --git a/lib/src/view/broadcast/broadcast_round_screen.dart b/lib/src/view/broadcast/broadcast_round_screen.dart index d3fae35352..bd042a96f6 100644 --- a/lib/src/view/broadcast/broadcast_round_screen.dart +++ b/lib/src/view/broadcast/broadcast_round_screen.dart @@ -106,44 +106,38 @@ class _BroadcastRoundScreenState extends ConsumerState Expanded( child: switch (asyncRound) { AsyncData(value: final _) => switch (selectedTab) { - _CupertinoView.overview => _TabView( - cupertinoTabSwitcher: tabSwitcher, - sliver: BroadcastOverviewTab( - broadcast: widget.broadcast, - tournamentId: _selectedTournamentId, - ), - ), - _CupertinoView.boards => _TabView( - cupertinoTabSwitcher: tabSwitcher, - sliver: switch (asyncTournament) { - AsyncData(:final value) => BroadcastBoardsTab( - roundId: _selectedRoundId ?? value.defaultRoundId, - broadcastTitle: widget.broadcast.title, - ), - _ => const SliverFillRemaining( - child: SizedBox.shrink(), - ), - }, - ), - _CupertinoView.players => _TabView( - cupertinoTabSwitcher: tabSwitcher, - sliver: BroadcastPlayersTab( - tournamentId: _selectedTournamentId, - ), + _CupertinoView.overview => _TabView( + cupertinoTabSwitcher: tabSwitcher, + sliver: BroadcastOverviewTab( + broadcast: widget.broadcast, + tournamentId: _selectedTournamentId, + ), + ), + _CupertinoView.boards => _TabView( + cupertinoTabSwitcher: tabSwitcher, + sliver: switch (asyncTournament) { + AsyncData(:final value) => BroadcastBoardsTab( + roundId: _selectedRoundId ?? value.defaultRoundId, + broadcastTitle: widget.broadcast.title, ), - }, - _ => const Center( - child: CircularProgressIndicator.adaptive(), + _ => const SliverFillRemaining(child: SizedBox.shrink()), + }, ), + _CupertinoView.players => _TabView( + cupertinoTabSwitcher: tabSwitcher, + sliver: BroadcastPlayersTab(tournamentId: _selectedTournamentId), + ), + }, + _ => const Center(child: CircularProgressIndicator.adaptive()), }, ), switch (asyncTournament) { AsyncData(:final value) => _BottomBar( - tournament: value, - roundId: _selectedRoundId ?? value.defaultRoundId, - setTournamentId: setTournamentId, - setRoundId: setRoundId, - ), + tournament: value, + roundId: _selectedRoundId ?? value.defaultRoundId, + setTournamentId: setTournamentId, + setRoundId: setRoundId, + ), _ => const BottomBar.empty(), }, ], @@ -175,43 +169,35 @@ class _BroadcastRoundScreenState extends ConsumerState ), body: switch (asyncRound) { AsyncData(value: final _) => TabBarView( - controller: _tabController, - children: [ - _TabView( - sliver: BroadcastOverviewTab( - broadcast: widget.broadcast, - tournamentId: _selectedTournamentId, - ), - ), - _TabView( - sliver: switch (asyncTournament) { - AsyncData(:final value) => BroadcastBoardsTab( - roundId: _selectedRoundId ?? value.defaultRoundId, - broadcastTitle: widget.broadcast.title, - ), - _ => const SliverFillRemaining( - child: SizedBox.shrink(), - ), - }, + controller: _tabController, + children: [ + _TabView( + sliver: BroadcastOverviewTab( + broadcast: widget.broadcast, + tournamentId: _selectedTournamentId, ), - _TabView( - sliver: BroadcastPlayersTab( - tournamentId: _selectedTournamentId, + ), + _TabView( + sliver: switch (asyncTournament) { + AsyncData(:final value) => BroadcastBoardsTab( + roundId: _selectedRoundId ?? value.defaultRoundId, + broadcastTitle: widget.broadcast.title, ), - ), - ], - ), - _ => const Center( - child: CircularProgressIndicator(), - ) + _ => const SliverFillRemaining(child: SizedBox.shrink()), + }, + ), + _TabView(sliver: BroadcastPlayersTab(tournamentId: _selectedTournamentId)), + ], + ), + _ => const Center(child: CircularProgressIndicator()), }, bottomNavigationBar: switch (asyncTournament) { AsyncData(:final value) => _BottomBar( - tournament: value, - roundId: _selectedRoundId ?? value.defaultRoundId, - setTournamentId: setTournamentId, - setRoundId: setRoundId, - ), + tournament: value, + roundId: _selectedRoundId ?? value.defaultRoundId, + setTournamentId: setTournamentId, + setRoundId: setRoundId, + ), _ => const BottomBar.empty(), }, ); @@ -219,8 +205,7 @@ class _BroadcastRoundScreenState extends ConsumerState @override Widget build(BuildContext context) { - final asyncTour = - ref.watch(broadcastTournamentProvider(_selectedTournamentId)); + final asyncTour = ref.watch(broadcastTournamentProvider(_selectedTournamentId)); const loadingRound = AsyncValue.loading(); @@ -229,15 +214,11 @@ class _BroadcastRoundScreenState extends ConsumerState // Eagerly initalize the round controller so it stays alive when switching tabs // and to know if the round has games to show final round = ref.watch( - broadcastRoundControllerProvider( - _selectedRoundId ?? tournament.defaultRoundId, - ), + broadcastRoundControllerProvider(_selectedRoundId ?? tournament.defaultRoundId), ); ref.listen( - broadcastRoundControllerProvider( - _selectedRoundId ?? tournament.defaultRoundId, - ), + broadcastRoundControllerProvider(_selectedRoundId ?? tournament.defaultRoundId), (_, round) { if (round.hasValue && !roundLoaded) { roundLoaded = true; @@ -253,27 +234,21 @@ class _BroadcastRoundScreenState extends ConsumerState ); return PlatformWidget( - androidBuilder: (context) => - _androidBuilder(context, asyncTour, round), + androidBuilder: (context) => _androidBuilder(context, asyncTour, round), iosBuilder: (context) => _iosBuilder(context, asyncTour, round), ); case _: return PlatformWidget( - androidBuilder: (context) => - _androidBuilder(context, asyncTour, loadingRound), - iosBuilder: (context) => - _iosBuilder(context, asyncTour, loadingRound), + androidBuilder: (context) => _androidBuilder(context, asyncTour, loadingRound), + iosBuilder: (context) => _iosBuilder(context, asyncTour, loadingRound), ); } } } class _TabView extends StatelessWidget { - const _TabView({ - required this.sliver, - this.cupertinoTabSwitcher, - }); + const _TabView({required this.sliver, this.cupertinoTabSwitcher}); final Widget sliver; final Widget? cupertinoTabSwitcher; @@ -285,8 +260,7 @@ class _TabView extends StatelessWidget { slivers: [ if (cupertinoTabSwitcher != null) SliverPadding( - padding: Styles.bodyPadding + - EdgeInsets.only(top: MediaQuery.paddingOf(context).top), + padding: Styles.bodyPadding + EdgeInsets.only(top: MediaQuery.paddingOf(context).top), sliver: SliverToBoxAdapter(child: cupertinoTabSwitcher), ), sliver, @@ -315,78 +289,73 @@ class _BottomBar extends ConsumerWidget { children: [ if (tournament.group != null) AdaptiveTextButton( - onPressed: () => showAdaptiveBottomSheet( - context: context, - showDragHandle: true, - isScrollControlled: true, - isDismissible: true, - builder: (_) => DraggableScrollableSheet( - initialChildSize: 0.4, - maxChildSize: 0.4, - minChildSize: 0.1, - snap: true, - expand: false, - builder: (context, scrollController) { - return _TournamentSelectorMenu( - tournament: tournament, - group: tournament.group!, - scrollController: scrollController, - setTournamentId: setTournamentId, - ); - }, - ), - ), + onPressed: + () => showAdaptiveBottomSheet( + context: context, + showDragHandle: true, + isScrollControlled: true, + isDismissible: true, + builder: + (_) => DraggableScrollableSheet( + initialChildSize: 0.4, + maxChildSize: 0.4, + minChildSize: 0.1, + snap: true, + expand: false, + builder: (context, scrollController) { + return _TournamentSelectorMenu( + tournament: tournament, + group: tournament.group!, + scrollController: scrollController, + setTournamentId: setTournamentId, + ); + }, + ), + ), child: Text( - tournament.group! - .firstWhere((g) => g.id == tournament.data.id) - .name, + tournament.group!.firstWhere((g) => g.id == tournament.data.id).name, maxLines: 1, overflow: TextOverflow.ellipsis, ), ), AdaptiveTextButton( - onPressed: () => showAdaptiveBottomSheet( - context: context, - showDragHandle: true, - isScrollControlled: true, - isDismissible: true, - builder: (_) => DraggableScrollableSheet( - initialChildSize: 0.6, - maxChildSize: 0.6, - snap: true, - expand: false, - builder: (context, scrollController) { - return _RoundSelectorMenu( - selectedRoundId: roundId, - rounds: tournament.rounds, - scrollController: scrollController, - setRoundId: setRoundId, - ); - }, - ), - ), + onPressed: + () => showAdaptiveBottomSheet( + context: context, + showDragHandle: true, + isScrollControlled: true, + isDismissible: true, + builder: + (_) => DraggableScrollableSheet( + initialChildSize: 0.6, + maxChildSize: 0.6, + snap: true, + expand: false, + builder: (context, scrollController) { + return _RoundSelectorMenu( + selectedRoundId: roundId, + rounds: tournament.rounds, + scrollController: scrollController, + setRoundId: setRoundId, + ); + }, + ), + ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Flexible( child: Text( - tournament.rounds - .firstWhere((round) => round.id == roundId) - .name, + tournament.rounds.firstWhere((round) => round.id == roundId).name, maxLines: 1, overflow: TextOverflow.ellipsis, ), ), const SizedBox(width: 5.0), - switch (tournament.rounds - .firstWhere((round) => round.id == roundId) - .status) { - RoundStatus.finished => - Icon(Icons.check, color: context.lichessColors.good), - RoundStatus.live => - Icon(Icons.circle, color: context.lichessColors.error), - RoundStatus.upcoming => - const Icon(Icons.calendar_month, color: Colors.grey), + switch (tournament.rounds.firstWhere((round) => round.id == roundId).status) { + RoundStatus.finished => Icon(Icons.check, color: context.lichessColors.good), + RoundStatus.live => Icon(Icons.circle, color: context.lichessColors.error), + RoundStatus.upcoming => const Icon(Icons.calendar_month, color: Colors.grey), }, ], ), @@ -424,10 +393,7 @@ class _RoundSelectorState extends ConsumerState<_RoundSelectorMenu> { // Scroll to the current round WidgetsBinding.instance.addPostFrameCallback((_) { if (currentRoundKey.currentContext != null) { - Scrollable.ensureVisible( - currentRoundKey.currentContext!, - alignment: 0.5, - ); + Scrollable.ensureVisible(currentRoundKey.currentContext!, alignment: 0.5); } }); @@ -445,32 +411,17 @@ class _RoundSelectorState extends ConsumerState<_RoundSelectorMenu> { if (round.startsAt != null || round.startsAfterPrevious) ...[ Text( round.startsAt != null - ? round.startsAt! - .difference(DateTime.now()) - .inDays - .abs() < - 30 + ? round.startsAt!.difference(DateTime.now()).inDays.abs() < 30 ? _dateFormatMonth.format(round.startsAt!) : _dateFormatYearMonth.format(round.startsAt!) - : context.l10n.broadcastStartsAfter( - widget.rounds[index - 1].name, - ), + : context.l10n.broadcastStartsAfter(widget.rounds[index - 1].name), ), const SizedBox(width: 5.0), ], switch (round.status) { - RoundStatus.finished => Icon( - Icons.check, - color: context.lichessColors.good, - ), - RoundStatus.live => Icon( - Icons.circle, - color: context.lichessColors.error, - ), - RoundStatus.upcoming => const Icon( - Icons.calendar_month, - color: Colors.grey, - ), + RoundStatus.finished => Icon(Icons.check, color: context.lichessColors.good), + RoundStatus.live => Icon(Icons.circle, color: context.lichessColors.error), + RoundStatus.upcoming => const Icon(Icons.calendar_month, color: Colors.grey), }, ], ), @@ -498,8 +449,7 @@ class _TournamentSelectorMenu extends ConsumerStatefulWidget { final void Function(BroadcastTournamentId) setTournamentId; @override - ConsumerState<_TournamentSelectorMenu> createState() => - _TournamentSelectorState(); + ConsumerState<_TournamentSelectorMenu> createState() => _TournamentSelectorState(); } class _TournamentSelectorState extends ConsumerState<_TournamentSelectorMenu> { @@ -510,10 +460,7 @@ class _TournamentSelectorState extends ConsumerState<_TournamentSelectorMenu> { // Scroll to the current tournament WidgetsBinding.instance.addPostFrameCallback((_) { if (currentTournamentKey.currentContext != null) { - Scrollable.ensureVisible( - currentTournamentKey.currentContext!, - alignment: 0.5, - ); + Scrollable.ensureVisible(currentTournamentKey.currentContext!, alignment: 0.5); } }); @@ -522,9 +469,7 @@ class _TournamentSelectorState extends ConsumerState<_TournamentSelectorMenu> { children: [ for (final tournament in widget.group) PlatformListTile( - key: tournament.id == widget.tournament.data.id - ? currentTournamentKey - : null, + key: tournament.id == widget.tournament.data.id ? currentTournamentKey : null, selected: tournament.id == widget.tournament.data.id, title: Text(tournament.name), onTap: () { diff --git a/lib/src/view/clock/clock_settings.dart b/lib/src/view/clock/clock_settings.dart index decea94991..3b3e44723f 100644 --- a/lib/src/view/clock/clock_settings.dart +++ b/lib/src/view/clock/clock_settings.dart @@ -24,9 +24,10 @@ class ClockSettings extends ConsumerWidget { ); return Padding( - padding: orientation == Orientation.portrait - ? const EdgeInsets.symmetric(vertical: 10.0) - : const EdgeInsets.symmetric(horizontal: 10.0), + padding: + orientation == Orientation.portrait + ? const EdgeInsets.symmetric(vertical: 10.0) + : const EdgeInsets.symmetric(horizontal: 10.0), child: (orientation == Orientation.portrait ? Row.new : Column.new)( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ @@ -34,64 +35,57 @@ class ClockSettings extends ConsumerWidget { IconButton( tooltip: context.l10n.reset, iconSize: _iconSize, - onPressed: buttonsEnabled - ? () { - ref.read(clockToolControllerProvider.notifier).reset(); - } - : null, + onPressed: + buttonsEnabled + ? () { + ref.read(clockToolControllerProvider.notifier).reset(); + } + : null, icon: const Icon(Icons.refresh), ), IconButton( tooltip: context.l10n.settingsSettings, iconSize: _iconSize, - onPressed: buttonsEnabled - ? () { - final double screenHeight = - MediaQuery.sizeOf(context).height; - showAdaptiveBottomSheet( - context: context, - isScrollControlled: true, - showDragHandle: true, - constraints: BoxConstraints( - maxHeight: screenHeight - (screenHeight / 10), - ), - builder: (BuildContext context) { - final options = ref.watch( - clockToolControllerProvider - .select((value) => value.options), - ); - return TimeControlModal( - excludeUltraBullet: true, - value: TimeIncrement( - options.whiteTime.inSeconds, - options.whiteIncrement.inSeconds, - ), - onSelected: (choice) { - ref - .read(clockToolControllerProvider.notifier) - .updateOptions(choice); - }, - ); - }, - ); - } - : null, + onPressed: + buttonsEnabled + ? () { + final double screenHeight = MediaQuery.sizeOf(context).height; + showAdaptiveBottomSheet( + context: context, + isScrollControlled: true, + showDragHandle: true, + constraints: BoxConstraints(maxHeight: screenHeight - (screenHeight / 10)), + builder: (BuildContext context) { + final options = ref.watch( + clockToolControllerProvider.select((value) => value.options), + ); + return TimeControlModal( + excludeUltraBullet: true, + value: TimeIncrement( + options.whiteTime.inSeconds, + options.whiteIncrement.inSeconds, + ), + onSelected: (choice) { + ref.read(clockToolControllerProvider.notifier).updateOptions(choice); + }, + ); + }, + ); + } + : null, icon: const Icon(Icons.settings), ), IconButton( iconSize: _iconSize, // TODO: translate tooltip: 'Toggle sound', - onPressed: () => ref - .read(generalPreferencesProvider.notifier) - .toggleSoundEnabled(), + onPressed: () => ref.read(generalPreferencesProvider.notifier).toggleSoundEnabled(), icon: Icon(isSoundEnabled ? Icons.volume_up : Icons.volume_off), ), IconButton( tooltip: context.l10n.close, iconSize: _iconSize, - onPressed: - buttonsEnabled ? () => Navigator.of(context).pop() : null, + onPressed: buttonsEnabled ? () => Navigator.of(context).pop() : null, icon: const Icon(Icons.home), ), ], diff --git a/lib/src/view/clock/clock_tool_screen.dart b/lib/src/view/clock/clock_tool_screen.dart index b9962a0802..be5b873708 100644 --- a/lib/src/view/clock/clock_tool_screen.dart +++ b/lib/src/view/clock/clock_tool_screen.dart @@ -79,18 +79,20 @@ class ClockTile extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final colorScheme = Theme.of(context).colorScheme; - final backgroundColor = clockState.isFlagged(playerType) - ? context.lichessColors.error - : !clockState.paused && clockState.isPlayersTurn(playerType) + final backgroundColor = + clockState.isFlagged(playerType) + ? context.lichessColors.error + : !clockState.paused && clockState.isPlayersTurn(playerType) ? colorScheme.primary : clockState.activeSide == playerType - ? colorScheme.secondaryContainer - : colorScheme.surfaceContainer; + ? colorScheme.secondaryContainer + : colorScheme.surfaceContainer; final clockStyle = ClockStyle( - textColor: clockState.activeSide == playerType - ? colorScheme.onSecondaryContainer - : colorScheme.onSurface, + textColor: + clockState.activeSide == playerType + ? colorScheme.onSecondaryContainer + : colorScheme.onSurface, activeTextColor: colorScheme.onPrimary, emergencyTextColor: Colors.white, backgroundColor: Colors.transparent, @@ -99,10 +101,7 @@ class ClockTile extends ConsumerWidget { ); return RotatedBox( - quarterTurns: - orientation == Orientation.portrait && position == TilePosition.top - ? 2 - : 0, + quarterTurns: orientation == Orientation.portrait && position == TilePosition.top ? 2 : 0, child: Stack( alignment: Alignment.center, fit: StackFit.expand, @@ -111,25 +110,22 @@ class ClockTile extends ConsumerWidget { color: backgroundColor, child: InkWell( splashFactory: NoSplash.splashFactory, - onTap: !clockState.started - ? () { - ref - .read(clockToolControllerProvider.notifier) - .setBottomPlayer( - position == TilePosition.bottom - ? Side.white - : Side.black, - ); - } - : null, - onTapDown: clockState.started && - clockState.isPlayersMoveAllowed(playerType) - ? (_) { - ref - .read(clockToolControllerProvider.notifier) - .onTap(playerType); - } - : null, + onTap: + !clockState.started + ? () { + ref + .read(clockToolControllerProvider.notifier) + .setBottomPlayer( + position == TilePosition.bottom ? Side.white : Side.black, + ); + } + : null, + onTapDown: + clockState.started && clockState.isPlayersMoveAllowed(playerType) + ? (_) { + ref.read(clockToolControllerProvider.notifier).onTap(playerType); + } + : null, child: Padding( padding: const EdgeInsets.all(40), child: Column( @@ -152,9 +148,10 @@ class ClockTile extends ConsumerWidget { }, ), secondChild: const Icon(Icons.flag), - crossFadeState: clockState.isFlagged(playerType) - ? CrossFadeState.showSecond - : CrossFadeState.showFirst, + crossFadeState: + clockState.isFlagged(playerType) + ? CrossFadeState.showSecond + : CrossFadeState.showFirst, ), ), ], @@ -186,32 +183,31 @@ class ClockTile extends ConsumerWidget { iconSize: 32, icon: Icons.tune, color: clockStyle.textColor, - onTap: clockState.started - ? null - : () => showAdaptiveBottomSheet( + onTap: + clockState.started + ? null + : () => showAdaptiveBottomSheet( context: context, - builder: (BuildContext context) => - CustomClockSettings( - player: playerType, - clock: playerType == Side.white - ? TimeIncrement.fromDurations( - clockState.options.whiteTime, - clockState.options.whiteIncrement, - ) - : TimeIncrement.fromDurations( - clockState.options.blackTime, - clockState.options.blackIncrement, - ), - onSubmit: ( - Side player, - TimeIncrement clock, - ) { - Navigator.of(context).pop(); - ref - .read(clockToolControllerProvider.notifier) - .updateOptionsCustom(clock, player); - }, - ), + builder: + (BuildContext context) => CustomClockSettings( + player: playerType, + clock: + playerType == Side.white + ? TimeIncrement.fromDurations( + clockState.options.whiteTime, + clockState.options.whiteIncrement, + ) + : TimeIncrement.fromDurations( + clockState.options.blackTime, + clockState.options.blackIncrement, + ), + onSubmit: (Side player, TimeIncrement clock) { + Navigator.of(context).pop(); + ref + .read(clockToolControllerProvider.notifier) + .updateOptionsCustom(clock, player); + }, + ), ), ), ), diff --git a/lib/src/view/clock/custom_clock_settings.dart b/lib/src/view/clock/custom_clock_settings.dart index 3ac2172a1a..b1051d5732 100644 --- a/lib/src/view/clock/custom_clock_settings.dart +++ b/lib/src/view/clock/custom_clock_settings.dart @@ -10,11 +10,7 @@ import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/non_linear_slider.dart'; class CustomClockSettings extends StatefulWidget { - const CustomClockSettings({ - required this.onSubmit, - required this.player, - required this.clock, - }); + const CustomClockSettings({required this.onSubmit, required this.player, required this.clock}); final Side player; final TimeIncrement clock; @@ -50,10 +46,7 @@ class _CustomClockSettingsState extends State { child: FatButton( semanticsLabel: context.l10n.apply, child: Text(context.l10n.apply), - onPressed: () => widget.onSubmit( - widget.player, - TimeIncrement(time, increment), - ), + onPressed: () => widget.onSubmit(widget.player, TimeIncrement(time, increment)), ), ), ], @@ -84,29 +77,29 @@ class _PlayerTimeSlider extends StatelessWidget { value: clock.time, values: kAvailableTimesInSeconds, labelBuilder: _clockTimeLabel, - onChange: Theme.of(context).platform == TargetPlatform.iOS - ? (num value) { - updateTime(value.toInt()); - } - : null, + onChange: + Theme.of(context).platform == TargetPlatform.iOS + ? (num value) { + updateTime(value.toInt()); + } + : null, onChangeEnd: (num value) { updateTime(value.toInt()); }, ), ), PlatformListTile( - title: Text( - '${context.l10n.increment}: ${context.l10n.nbSeconds(clock.increment)}', - ), + title: Text('${context.l10n.increment}: ${context.l10n.nbSeconds(clock.increment)}'), subtitle: NonLinearSlider( value: clock.increment, values: kAvailableIncrementsInSeconds, labelBuilder: (num sec) => sec.toString(), - onChange: Theme.of(context).platform == TargetPlatform.iOS - ? (num value) { - updateIncrement(value.toInt()); - } - : null, + onChange: + Theme.of(context).platform == TargetPlatform.iOS + ? (num value) { + updateIncrement(value.toInt()); + } + : null, onChangeEnd: (num value) { updateIncrement(value.toInt()); }, diff --git a/lib/src/view/coordinate_training/coordinate_display.dart b/lib/src/view/coordinate_training/coordinate_display.dart index 9e4275b0ec..b9b9ede150 100644 --- a/lib/src/view/coordinate_training/coordinate_display.dart +++ b/lib/src/view/coordinate_training/coordinate_display.dart @@ -10,18 +10,14 @@ const double _kCurrCoordOpacity = 0.9; const double _kNextCoordOpacity = 0.7; class CoordinateDisplay extends ConsumerStatefulWidget { - const CoordinateDisplay({ - required this.currentCoord, - required this.nextCoord, - }); + const CoordinateDisplay({required this.currentCoord, required this.nextCoord}); final Square currentCoord; final Square nextCoord; @override - ConsumerState createState() => - CoordinateDisplayState(); + ConsumerState createState() => CoordinateDisplayState(); } class CoordinateDisplayState extends ConsumerState @@ -34,34 +30,27 @@ class CoordinateDisplayState extends ConsumerState late final Animation _scaleAnimation = Tween( begin: _kNextCoordScale, end: 1.0, - ).animate( - CurvedAnimation(parent: _controller, curve: Curves.linear), - ); + ).animate(CurvedAnimation(parent: _controller, curve: Curves.linear)); late final Animation _currCoordSlideInAnimation = Tween( begin: _kNextCoordFractionalTranslation, end: Offset.zero, - ).animate( - CurvedAnimation(parent: _controller, curve: Curves.linear), - ); + ).animate(CurvedAnimation(parent: _controller, curve: Curves.linear)); late final Animation _nextCoordSlideInAnimation = Tween( begin: const Offset(0.5, 0), end: Offset.zero, - ).animate( - CurvedAnimation(parent: _controller, curve: Curves.linear), - ); + ).animate(CurvedAnimation(parent: _controller, curve: Curves.linear)); late final Animation _currCoordOpacityAnimation = Tween( begin: _kNextCoordOpacity, end: _kCurrCoordOpacity, - ).animate( - CurvedAnimation(parent: _controller, curve: Curves.linear), - ); + ).animate(CurvedAnimation(parent: _controller, curve: Curves.linear)); - late final Animation _nextCoordFadeInAnimation = - Tween(begin: 0.0, end: _kNextCoordOpacity) - .animate(CurvedAnimation(parent: _controller, curve: Curves.easeIn)); + late final Animation _nextCoordFadeInAnimation = Tween( + begin: 0.0, + end: _kNextCoordOpacity, + ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeIn)); @override Widget build(BuildContext context) { @@ -73,13 +62,7 @@ class CoordinateDisplayState extends ConsumerState color: Colors.white.withValues(alpha: 0.9), fontWeight: FontWeight.bold, fontFeatures: [const FontFeature.tabularFigures()], - shadows: const [ - Shadow( - color: Colors.black, - offset: Offset(0, 5), - blurRadius: 40.0, - ), - ], + shadows: const [Shadow(color: Colors.black, offset: Offset(0, 5), blurRadius: 40.0)], ); return IgnorePointer( @@ -91,10 +74,7 @@ class CoordinateDisplayState extends ConsumerState position: _currCoordSlideInAnimation, child: ScaleTransition( scale: _scaleAnimation, - child: Text( - trainingState.currentCoord?.name ?? '', - style: textStyle, - ), + child: Text(trainingState.currentCoord?.name ?? '', style: textStyle), ), ), ), @@ -106,10 +86,7 @@ class CoordinateDisplayState extends ConsumerState translation: _kNextCoordFractionalTranslation, child: Transform.scale( scale: _kNextCoordScale, - child: Text( - trainingState.nextCoord?.name ?? '', - style: textStyle, - ), + child: Text(trainingState.nextCoord?.name ?? '', style: textStyle), ), ), ), diff --git a/lib/src/view/coordinate_training/coordinate_training_screen.dart b/lib/src/view/coordinate_training/coordinate_training_screen.dart index 02b9b3d7a7..f4d8e65b44 100644 --- a/lib/src/view/coordinate_training/coordinate_training_screen.dart +++ b/lib/src/view/coordinate_training/coordinate_training_screen.dart @@ -36,11 +36,11 @@ class CoordinateTrainingScreen extends StatelessWidget { AppBarIconButton( icon: const Icon(Icons.settings), semanticsLabel: context.l10n.settingsSettings, - onPressed: () => showAdaptiveBottomSheet( - context: context, - builder: (BuildContext context) => - const _CoordinateTrainingMenu(), - ), + onPressed: + () => showAdaptiveBottomSheet( + context: context, + builder: (BuildContext context) => const _CoordinateTrainingMenu(), + ), ), ], ), @@ -74,26 +74,26 @@ class _BodyState extends ConsumerState<_Body> { final IMap squareHighlights = { - if (trainingState.trainingActive) - if (trainingPrefs.mode == TrainingMode.findSquare) ...{ - if (highlightLastGuess != null) ...{ - highlightLastGuess!: SquareHighlight( - details: HighlightDetails( - solidColor: (trainingState.lastGuess == Guess.correct - ? context.lichessColors.good - : context.lichessColors.error) - .withValues(alpha: 0.5), + if (trainingState.trainingActive) + if (trainingPrefs.mode == TrainingMode.findSquare) ...{ + if (highlightLastGuess != null) ...{ + highlightLastGuess!: SquareHighlight( + details: HighlightDetails( + solidColor: (trainingState.lastGuess == Guess.correct + ? context.lichessColors.good + : context.lichessColors.error) + .withValues(alpha: 0.5), + ), + ), + }, + } else ...{ + trainingState.currentCoord!: SquareHighlight( + details: HighlightDetails( + solidColor: context.lichessColors.good.withValues(alpha: 0.5), + ), ), - ), - }, - } else ...{ - trainingState.currentCoord!: SquareHighlight( - details: HighlightDetails( - solidColor: context.lichessColors.good.withValues(alpha: 0.5), - ), - ), - }, - }.lock; + }, + }.lock; return SafeArea( bottom: false, @@ -106,16 +106,14 @@ class _BodyState extends ConsumerState<_Body> { final defaultBoardSize = constraints.biggest.shortestSide; final isTablet = isTabletOrLarger(context); - final remainingHeight = - constraints.maxHeight - defaultBoardSize; - final isSmallScreen = - remainingHeight < kSmallRemainingHeightLeftBoardThreshold; - final boardSize = isTablet || isSmallScreen - ? defaultBoardSize - kTabletBoardTableSidePadding * 2 - : defaultBoardSize; + final remainingHeight = constraints.maxHeight - defaultBoardSize; + final isSmallScreen = remainingHeight < kSmallRemainingHeightLeftBoardThreshold; + final boardSize = + isTablet || isSmallScreen + ? defaultBoardSize - kTabletBoardTableSidePadding * 2 + : defaultBoardSize; - final direction = - aspectRatio > 1 ? Axis.horizontal : Axis.vertical; + final direction = aspectRatio > 1 ? Axis.horizontal : Axis.vertical; return Flex( direction: direction, @@ -127,11 +125,11 @@ class _BodyState extends ConsumerState<_Body> { children: [ _TimeBar( maxWidth: boardSize, - timeFractionElapsed: - trainingState.timeFractionElapsed, - color: trainingState.lastGuess == Guess.incorrect - ? context.lichessColors.error - : context.lichessColors.good, + timeFractionElapsed: trainingState.timeFractionElapsed, + color: + trainingState.lastGuess == Guess.incorrect + ? context.lichessColors.error + : context.lichessColors.good, ), _TrainingBoard( boardSize: boardSize, @@ -146,11 +144,8 @@ class _BodyState extends ConsumerState<_Body> { _ScoreAndTrainingButton( scoreSize: boardSize / 8, score: trainingState.score, - onPressed: ref - .read( - coordinateTrainingControllerProvider.notifier, - ) - .abortTraining, + onPressed: + ref.read(coordinateTrainingControllerProvider.notifier).abortTraining, label: 'Abort Training', ) else if (trainingState.lastScore != null) @@ -159,12 +154,8 @@ class _BodyState extends ConsumerState<_Body> { score: trainingState.lastScore!, onPressed: () { ref - .read( - coordinateTrainingControllerProvider.notifier, - ) - .startTraining( - trainingPrefs.timeChoice.duration, - ); + .read(coordinateTrainingControllerProvider.notifier) + .startTraining(trainingPrefs.timeChoice.duration); }, label: 'New Training', ) @@ -174,13 +165,8 @@ class _BodyState extends ConsumerState<_Body> { child: _Button( onPressed: () { ref - .read( - coordinateTrainingControllerProvider - .notifier, - ) - .startTraining( - trainingPrefs.timeChoice.duration, - ); + .read(coordinateTrainingControllerProvider.notifier) + .startTraining(trainingPrefs.timeChoice.duration); }, label: 'Start Training', ), @@ -212,9 +198,7 @@ class _BodyState extends ConsumerState<_Body> { } void _onGuess(Square square) { - ref - .read(coordinateTrainingControllerProvider.notifier) - .guessCoordinate(square); + ref.read(coordinateTrainingControllerProvider.notifier).guessCoordinate(square); setState(() { highlightLastGuess = square; @@ -230,11 +214,7 @@ class _BodyState extends ConsumerState<_Body> { } class _TimeBar extends StatelessWidget { - const _TimeBar({ - required this.maxWidth, - required this.timeFractionElapsed, - required this.color, - }); + const _TimeBar({required this.maxWidth, required this.timeFractionElapsed, required this.color}); final double maxWidth; final double? timeFractionElapsed; @@ -247,9 +227,7 @@ class _TimeBar extends StatelessWidget { child: SizedBox( width: maxWidth * (timeFractionElapsed ?? 0.0), height: 15.0, - child: ColoredBox( - color: color, - ), + child: ColoredBox(color: color), ), ); } @@ -271,24 +249,18 @@ class _CoordinateTrainingMenu extends ConsumerWidget { children: [ Padding( padding: const EdgeInsets.all(8.0), - child: Text( - context.l10n.preferencesDisplay, - style: Styles.sectionTitle, - ), + child: Text(context.l10n.preferencesDisplay, style: Styles.sectionTitle), ), SwitchSettingTile( title: const Text('Show Coordinates'), value: trainingPrefs.showCoordinates, - onChanged: ref - .read(coordinateTrainingPreferencesProvider.notifier) - .setShowCoordinates, + onChanged: + ref.read(coordinateTrainingPreferencesProvider.notifier).setShowCoordinates, ), SwitchSettingTile( title: const Text('Show Pieces'), value: trainingPrefs.showPieces, - onChanged: ref - .read(coordinateTrainingPreferencesProvider.notifier) - .setShowPieces, + onChanged: ref.read(coordinateTrainingPreferencesProvider.notifier).setShowPieces, ), ], ), @@ -321,14 +293,12 @@ class _ScoreAndTrainingButton extends ConsumerWidget { _Score( score: score, size: scoreSize, - color: trainingState.lastGuess == Guess.incorrect - ? context.lichessColors.error - : context.lichessColors.good, - ), - _Button( - label: label, - onPressed: onPressed, + color: + trainingState.lastGuess == Guess.incorrect + ? context.lichessColors.error + : context.lichessColors.good, ), + _Button(label: label, onPressed: onPressed), ], ), ); @@ -336,11 +306,7 @@ class _ScoreAndTrainingButton extends ConsumerWidget { } class _Score extends StatelessWidget { - const _Score({ - required this.size, - required this.color, - required this.score, - }); + const _Score({required this.size, required this.color, required this.score}); final int score; @@ -351,16 +317,10 @@ class _Score extends StatelessWidget { @override Widget build(BuildContext context) { return Padding( - padding: const EdgeInsets.only( - top: 10.0, - left: 10.0, - right: 10.0, - ), + padding: const EdgeInsets.only(top: 10.0, left: 10.0, right: 10.0), child: Container( decoration: BoxDecoration( - borderRadius: const BorderRadius.all( - Radius.circular(4.0), - ), + borderRadius: const BorderRadius.all(Radius.circular(4.0)), color: color, ), width: size, @@ -368,10 +328,7 @@ class _Score extends StatelessWidget { child: Center( child: Text( score.toString(), - style: Styles.bold.copyWith( - color: Colors.white, - fontSize: 24.0, - ), + style: Styles.bold.copyWith(color: Colors.white, fontSize: 24.0), ), ), ), @@ -380,10 +337,7 @@ class _Score extends StatelessWidget { } class _Button extends StatelessWidget { - const _Button({ - required this.onPressed, - required this.label, - }); + const _Button({required this.onPressed, required this.label}); final VoidCallback onPressed; final String label; @@ -393,10 +347,7 @@ class _Button extends StatelessWidget { return FatButton( semanticsLabel: label, onPressed: onPressed, - child: Text( - label, - style: Styles.bold, - ), + child: Text(label, style: Styles.bold), ); } } @@ -420,11 +371,7 @@ class SettingsBottomSheet extends ConsumerWidget { choiceLabel: (choice) => Text(choice.label(context.l10n)), onSelected: (choice, selected) { if (selected) { - ref - .read( - coordinateTrainingPreferencesProvider.notifier, - ) - .setSideChoice(choice); + ref.read(coordinateTrainingPreferencesProvider.notifier).setSideChoice(choice); } }, ), @@ -440,11 +387,7 @@ class SettingsBottomSheet extends ConsumerWidget { choiceLabel: (choice) => choice.label(context.l10n), onSelected: (choice, selected) { if (selected) { - ref - .read( - coordinateTrainingPreferencesProvider.notifier, - ) - .setTimeChoice(choice); + ref.read(coordinateTrainingPreferencesProvider.notifier).setTimeChoice(choice); } }, ), @@ -490,29 +433,25 @@ class _TrainingBoardState extends ConsumerState<_TrainingBoard> { children: [ ChessboardEditor( size: widget.boardSize, - pieces: readFen( - trainingPrefs.showPieces ? kInitialFEN : kEmptyFEN, - ), + pieces: readFen(trainingPrefs.showPieces ? kInitialFEN : kEmptyFEN), squareHighlights: widget.squareHighlights, orientation: widget.orientation, settings: boardPrefs.toBoardSettings().copyWith( - enableCoordinates: trainingPrefs.showCoordinates, - borderRadius: widget.isTablet + enableCoordinates: trainingPrefs.showCoordinates, + borderRadius: + widget.isTablet ? const BorderRadius.all(Radius.circular(4.0)) : BorderRadius.zero, - boxShadow: - widget.isTablet ? boardShadows : const [], - ), + boxShadow: widget.isTablet ? boardShadows : const [], + ), pointerMode: EditorPointerMode.edit, onEditedSquare: (square) { - if (trainingState.trainingActive && - trainingPrefs.mode == TrainingMode.findSquare) { + if (trainingState.trainingActive && trainingPrefs.mode == TrainingMode.findSquare) { widget.onGuess(square); } }, ), - if (trainingState.trainingActive && - trainingPrefs.mode == TrainingMode.findSquare) + if (trainingState.trainingActive && trainingPrefs.mode == TrainingMode.findSquare) CoordinateDisplay( currentCoord: trainingState.currentCoord!, nextCoord: trainingState.nextCoord!, @@ -557,21 +496,13 @@ Future _coordinateTrainingInfoDialogBuilder(BuildContext context) { text: ' • You can analyse a game more effectively if you can quickly recognise coordinates.\n', ), - TextSpan( - text: '\n', - ), - TextSpan( - text: 'Find Square\n', - style: TextStyle(fontWeight: FontWeight.bold), - ), + TextSpan(text: '\n'), + TextSpan(text: 'Find Square\n', style: TextStyle(fontWeight: FontWeight.bold)), TextSpan( text: 'A coordinate appears on the board and you must click on the corresponding square.\n', ), - TextSpan( - text: - 'You have 30 seconds to correctly map as many squares as possible!\n', - ), + TextSpan(text: 'You have 30 seconds to correctly map as many squares as possible!\n'), ], ), ), diff --git a/lib/src/view/correspondence/offline_correspondence_game_screen.dart b/lib/src/view/correspondence/offline_correspondence_game_screen.dart index c72c357b11..472a6ed00b 100644 --- a/lib/src/view/correspondence/offline_correspondence_game_screen.dart +++ b/lib/src/view/correspondence/offline_correspondence_game_screen.dart @@ -27,20 +27,15 @@ import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; class OfflineCorrespondenceGameScreen extends StatefulWidget { - const OfflineCorrespondenceGameScreen({ - required this.initialGame, - super.key, - }); + const OfflineCorrespondenceGameScreen({required this.initialGame, super.key}); final (DateTime, OfflineCorrespondenceGame) initialGame; @override - State createState() => - _OfflineCorrespondenceGameScreenState(); + State createState() => _OfflineCorrespondenceGameScreenState(); } -class _OfflineCorrespondenceGameScreenState - extends State { +class _OfflineCorrespondenceGameScreenState extends State { late (DateTime, OfflineCorrespondenceGame) currentGame; @override @@ -60,11 +55,7 @@ class _OfflineCorrespondenceGameScreenState final (lastModified, game) = currentGame; return PlatformScaffold( appBar: PlatformAppBar(title: _Title(game)), - body: _Body( - game: game, - lastModified: lastModified, - onGameChanged: goToNextGame, - ), + body: _Body(game: game, lastModified: lastModified, onGameChanged: goToNextGame), ); } } @@ -75,20 +66,14 @@ class _Title extends StatelessWidget { @override Widget build(BuildContext context) { - final mode = - game.rated ? ' • ${context.l10n.rated}' : ' • ${context.l10n.casual}'; + final mode = game.rated ? ' • ${context.l10n.rated}' : ' • ${context.l10n.casual}'; return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - game.perf.icon, - color: DefaultTextStyle.of(context).style.color, - ), + Icon(game.perf.icon, color: DefaultTextStyle.of(context).style.color), const SizedBox(width: 4.0), if (game.daysPerTurn != null) - Text( - '${context.l10n.nbDays(game.daysPerTurn!)}$mode', - ) + Text('${context.l10n.nbDays(game.daysPerTurn!)}$mode') else Text('∞$mode'), ], @@ -97,11 +82,7 @@ class _Title extends StatelessWidget { } class _Body extends ConsumerStatefulWidget { - const _Body({ - required this.game, - required this.lastModified, - required this.onGameChanged, - }); + const _Body({required this.game, required this.lastModified, required this.onGameChanged}); final OfflineCorrespondenceGame game; final DateTime lastModified; @@ -143,13 +124,10 @@ class _BodyState extends ConsumerState<_Body> { @override Widget build(BuildContext context) { final materialDifference = ref.watch( - boardPreferencesProvider.select( - (prefs) => prefs.materialDifferenceFormat, - ), + boardPreferencesProvider.select((prefs) => prefs.materialDifferenceFormat), ); - final offlineOngoingGames = - ref.watch(offlineOngoingCorrespondenceGamesProvider); + final offlineOngoingGames = ref.watch(offlineOngoingCorrespondenceGamesProvider); final position = game.positionAt(stepCursor); final sideToMove = position.turn; @@ -157,48 +135,39 @@ class _BodyState extends ConsumerState<_Body> { final black = GamePlayer( player: game.black, - materialDiff: materialDifference.visible - ? game.materialDiffAt(stepCursor, Side.black) - : null, + materialDiff: materialDifference.visible ? game.materialDiffAt(stepCursor, Side.black) : null, materialDifferenceFormat: materialDifference, shouldLinkToUserProfile: false, mePlaying: youAre == Side.black, - confirmMoveCallbacks: youAre == Side.black && moveToConfirm != null - ? ( - confirm: confirmMove, - cancel: cancelMove, - ) - : null, - clock: youAre == Side.black && - game.estimatedTimeLeft(Side.black, widget.lastModified) != null - ? CorrespondenceClock( - duration: - game.estimatedTimeLeft(Side.black, widget.lastModified)!, - active: activeClockSide == Side.black, - ) - : null, + confirmMoveCallbacks: + youAre == Side.black && moveToConfirm != null + ? (confirm: confirmMove, cancel: cancelMove) + : null, + clock: + youAre == Side.black && game.estimatedTimeLeft(Side.black, widget.lastModified) != null + ? CorrespondenceClock( + duration: game.estimatedTimeLeft(Side.black, widget.lastModified)!, + active: activeClockSide == Side.black, + ) + : null, ); final white = GamePlayer( player: game.white, - materialDiff: materialDifference.visible - ? game.materialDiffAt(stepCursor, Side.white) - : null, + materialDiff: materialDifference.visible ? game.materialDiffAt(stepCursor, Side.white) : null, materialDifferenceFormat: materialDifference, shouldLinkToUserProfile: false, mePlaying: youAre == Side.white, - confirmMoveCallbacks: youAre == Side.white && moveToConfirm != null - ? ( - confirm: confirmMove, - cancel: cancelMove, - ) - : null, - clock: game.estimatedTimeLeft(Side.white, widget.lastModified) != null - ? CorrespondenceClock( - duration: - game.estimatedTimeLeft(Side.white, widget.lastModified)!, - active: activeClockSide == Side.white, - ) - : null, + confirmMoveCallbacks: + youAre == Side.white && moveToConfirm != null + ? (confirm: confirmMove, cancel: cancelMove) + : null, + clock: + game.estimatedTimeLeft(Side.white, widget.lastModified) != null + ? CorrespondenceClock( + duration: game.estimatedTimeLeft(Side.white, widget.lastModified)!, + active: activeClockSide == Side.white, + ) + : null, ); final topPlayer = youAre == Side.white ? black : white; @@ -214,17 +183,15 @@ class _BodyState extends ConsumerState<_Body> { fen: position.fen, lastMove: game.moveAt(stepCursor) as NormalMove?, gameData: GameData( - playerSide: game.playable && !isReplaying - ? youAre == Side.white - ? PlayerSide.white - : PlayerSide.black - : PlayerSide.none, + playerSide: + game.playable && !isReplaying + ? youAre == Side.white + ? PlayerSide.white + : PlayerSide.black + : PlayerSide.none, isCheck: position.isCheck, sideToMove: sideToMove, - validMoves: makeLegalMoves( - position, - isChess960: game.variant == Variant.chess960, - ), + validMoves: makeLegalMoves(position, isChess960: game.variant == Variant.chess960), promotionMove: promotionMove, onMove: (move, {isDrop, captured}) { onUserMove(move); @@ -233,10 +200,7 @@ class _BodyState extends ConsumerState<_Body> { ), topTable: topPlayer, bottomTable: bottomPlayer, - moves: game.steps - .skip(1) - .map((e) => e.sanMove!.san) - .toList(growable: false), + moves: game.steps.skip(1).map((e) => e.sanMove!.san).toList(growable: false), currentMoveIndex: stepCursor, onSelectMove: (moveIndex) { // ref.read(ctrlProvider.notifier).cursorAt(moveIndex); @@ -260,17 +224,18 @@ class _BodyState extends ConsumerState<_Body> { onTap: () { pushPlatformRoute( context, - builder: (_) => AnalysisScreen( - options: AnalysisOptions( - orientation: game.youAre, - standalone: ( - pgn: game.makePgn(), - isComputerAnalysisAllowed: false, - variant: game.variant, + builder: + (_) => AnalysisScreen( + options: AnalysisOptions( + orientation: game.youAre, + standalone: ( + pgn: game.makePgn(), + isComputerAnalysisAllowed: false, + variant: game.variant, + ), + initialMoveCursor: stepCursor, + ), ), - initialMoveCursor: stepCursor, - ), - ), ); }, icon: Icons.biotech, @@ -285,8 +250,8 @@ class _BodyState extends ConsumerState<_Body> { .firstWhereOrNull((g) => g.$2.isMyTurn); return nextTurn != null ? () { - widget.onGameChanged(nextTurn); - } + widget.onGameChanged(nextTurn); + } : null; }, orElse: () => null, @@ -294,18 +259,17 @@ class _BodyState extends ConsumerState<_Body> { ), BottomBarButton( label: context.l10n.mobileCorrespondenceClearSavedMove, - onTap: game.registeredMoveAtPgn != null - ? () { - showConfirmDialog( - context, - title: Text( - context.l10n.mobileCorrespondenceClearSavedMove, - ), - isDestructiveAction: true, - onConfirm: (_) => deleteRegisteredMove(), - ); - } - : null, + onTap: + game.registeredMoveAtPgn != null + ? () { + showConfirmDialog( + context, + title: Text(context.l10n.mobileCorrespondenceClearSavedMove), + isDestructiveAction: true, + onConfirm: (_) => deleteRegisteredMove(), + ); + } + : null, icon: Icons.save, ), RepeatButton( @@ -370,9 +334,7 @@ class _BodyState extends ConsumerState<_Body> { setState(() { moveToConfirm = (game.sanMoves, move); - game = game.copyWith( - steps: game.steps.add(newStep), - ); + game = game.copyWith(steps: game.steps.add(newStep)); promotionMove = null; stepCursor = stepCursor + 1; }); @@ -395,9 +357,7 @@ class _BodyState extends ConsumerState<_Body> { Future confirmMove() async { setState(() { - game = game.copyWith( - registeredMoveAtPgn: (moveToConfirm!.$1, moveToConfirm!.$2), - ); + game = game.copyWith(registeredMoveAtPgn: (moveToConfirm!.$1, moveToConfirm!.$2)); moveToConfirm = null; }); @@ -409,19 +369,14 @@ class _BodyState extends ConsumerState<_Body> { setState(() { moveToConfirm = null; stepCursor = stepCursor - 1; - game = game.copyWith( - steps: game.steps.removeLast(), - ); + game = game.copyWith(steps: game.steps.removeLast()); }); } Future deleteRegisteredMove() async { setState(() { stepCursor = stepCursor - 1; - game = game.copyWith( - steps: game.steps.removeLast(), - registeredMoveAtPgn: null, - ); + game = game.copyWith(steps: game.steps.removeLast(), registeredMoveAtPgn: null); }); final storage = await ref.read(correspondenceGameStorageProvider.future); diff --git a/lib/src/view/engine/engine_depth.dart b/lib/src/view/engine/engine_depth.dart index 83cec2197f..e9a03a036d 100644 --- a/lib/src/view/engine/engine_depth.dart +++ b/lib/src/view/engine/engine_depth.dart @@ -18,62 +18,60 @@ class EngineDepth extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final depth = ref.watch( - engineEvaluationProvider.select((value) => value.eval?.depth), - ) ?? + final depth = + ref.watch(engineEvaluationProvider.select((value) => value.eval?.depth)) ?? defaultEval?.depth; return depth != null ? AppBarTextButton( - onPressed: () { - showPopover( - context: context, - bodyBuilder: (context) { - return _StockfishInfo(defaultEval); - }, - direction: PopoverDirection.top, - width: 240, - backgroundColor: + onPressed: () { + showPopover( + context: context, + bodyBuilder: (context) { + return _StockfishInfo(defaultEval); + }, + direction: PopoverDirection.top, + width: 240, + backgroundColor: + Theme.of(context).platform == TargetPlatform.android + ? DialogTheme.of(context).backgroundColor ?? + Theme.of(context).colorScheme.surfaceContainerHigh + : CupertinoDynamicColor.resolve( + CupertinoColors.tertiarySystemBackground, + context, + ), + transitionDuration: Duration.zero, + popoverTransitionBuilder: (_, child) => child, + ); + }, + child: RepaintBoundary( + child: Container( + width: 20.0, + height: 20.0, + padding: const EdgeInsets.all(2.0), + decoration: BoxDecoration( + color: Theme.of(context).platform == TargetPlatform.android - ? DialogTheme.of(context).backgroundColor ?? - Theme.of(context).colorScheme.surfaceContainerHigh - : CupertinoDynamicColor.resolve( - CupertinoColors.tertiarySystemBackground, - context, - ), - transitionDuration: Duration.zero, - popoverTransitionBuilder: (_, child) => child, - ); - }, - child: RepaintBoundary( - child: Container( - width: 20.0, - height: 20.0, - padding: const EdgeInsets.all(2.0), - decoration: BoxDecoration( - color: Theme.of(context).platform == TargetPlatform.android - ? Theme.of(context).colorScheme.secondary - : CupertinoTheme.of(context).primaryColor, - borderRadius: BorderRadius.circular(4.0), - ), - child: FittedBox( - fit: BoxFit.contain, - child: Text( - '${math.min(99, depth)}', - style: TextStyle( - color: Theme.of(context).platform == - TargetPlatform.android - ? Theme.of(context).colorScheme.onSecondary - : CupertinoTheme.of(context).primaryContrastingColor, - fontFeatures: const [ - FontFeature.tabularFigures(), - ], - ), + ? Theme.of(context).colorScheme.secondary + : CupertinoTheme.of(context).primaryColor, + borderRadius: BorderRadius.circular(4.0), + ), + child: FittedBox( + fit: BoxFit.contain, + child: Text( + '${math.min(99, depth)}', + style: TextStyle( + color: + Theme.of(context).platform == TargetPlatform.android + ? Theme.of(context).colorScheme.onSecondary + : CupertinoTheme.of(context).primaryContrastingColor, + fontFeatures: const [FontFeature.tabularFigures()], ), ), ), ), - ) + ), + ) : const SizedBox.shrink(); } } @@ -85,29 +83,22 @@ class _StockfishInfo extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final (engineName: engineName, eval: eval, state: engineState) = - ref.watch(engineEvaluationProvider); + final (engineName: engineName, eval: eval, state: engineState) = ref.watch( + engineEvaluationProvider, + ); final currentEval = eval ?? defaultEval; - final knps = engineState == EngineState.computing - ? ', ${eval?.knps.round()}kn/s' - : ''; + final knps = engineState == EngineState.computing ? ', ${eval?.knps.round()}kn/s' : ''; final depth = currentEval?.depth ?? 0; return Column( mainAxisSize: MainAxisSize.min, children: [ PlatformListTile( - leading: Image.asset( - 'assets/images/stockfish/icon.png', - width: 44, - height: 44, - ), + leading: Image.asset('assets/images/stockfish/icon.png', width: 44, height: 44), title: Text(engineName), - subtitle: Text( - context.l10n.depthX('$depth$knps'), - ), + subtitle: Text(context.l10n.depthX('$depth$knps')), ), ], ); diff --git a/lib/src/view/engine/engine_gauge.dart b/lib/src/view/engine/engine_gauge.dart index 0da958228c..438cd880d2 100644 --- a/lib/src/view/engine/engine_gauge.dart +++ b/lib/src/view/engine/engine_gauge.dart @@ -14,29 +14,24 @@ const Color _kEvalGaugeBackgroundColor = Color(0xFF444444); const Color _kEvalGaugeValueColorDarkBg = Color(0xEEEEEEEE); const Color _kEvalGaugeValueColorLightBg = Color(0xFFFFFFFF); -enum EngineGaugeDisplayMode { - vertical, - horizontal, -} +enum EngineGaugeDisplayMode { vertical, horizontal } -typedef EngineGaugeParams = ({ - bool isLocalEngineAvailable, +typedef EngineGaugeParams = + ({ + bool isLocalEngineAvailable, - /// Only used for vertical display mode. - Side orientation, + /// Only used for vertical display mode. + Side orientation, - /// Position to evaluate. - Position position, + /// Position to evaluate. + Position position, - /// Saved evaluation to display when the current evaluation is not available. - Eval? savedEval, -}); + /// Saved evaluation to display when the current evaluation is not available. + Eval? savedEval, + }); class EngineGauge extends ConsumerWidget { - const EngineGauge({ - required this.displayMode, - required this.params, - }); + const EngineGauge({required this.displayMode, required this.params}); final EngineGaugeDisplayMode displayMode; @@ -46,8 +41,8 @@ class EngineGauge extends ConsumerWidget { Theme.of(context).platform == TargetPlatform.iOS ? _kEvalGaugeBackgroundColor : brightness == Brightness.dark - ? lighten(Theme.of(context).colorScheme.surface, .07) - : lighten(Theme.of(context).colorScheme.onSurface, .17); + ? lighten(Theme.of(context).colorScheme.surface, .07) + : lighten(Theme.of(context).colorScheme.onSurface, .17); static Color valueColor(BuildContext context, Brightness brightness) => Theme.of(context).platform == TargetPlatform.iOS @@ -55,34 +50,33 @@ class EngineGauge extends ConsumerWidget { ? _kEvalGaugeValueColorDarkBg : _kEvalGaugeValueColorLightBg : brightness == Brightness.dark - ? darken(Theme.of(context).colorScheme.onSurface, .03) - : darken(Theme.of(context).colorScheme.surface, .01); + ? darken(Theme.of(context).colorScheme.onSurface, .03) + : darken(Theme.of(context).colorScheme.surface, .01); @override Widget build(BuildContext context, WidgetRef ref) { - final localEval = params.isLocalEngineAvailable - ? ref.watch(engineEvaluationProvider).eval - : null; + final localEval = + params.isLocalEngineAvailable ? ref.watch(engineEvaluationProvider).eval : null; return localEval != null ? _EvalGauge( - displayMode: displayMode, - position: params.position, - orientation: params.orientation, - eval: localEval, - ) + displayMode: displayMode, + position: params.position, + orientation: params.orientation, + eval: localEval, + ) : params.savedEval != null - ? _EvalGauge( - displayMode: displayMode, - position: params.position, - orientation: params.orientation, - eval: params.savedEval, - ) - : _EvalGauge( - displayMode: displayMode, - position: params.position, - orientation: params.orientation, - ); + ? _EvalGauge( + displayMode: displayMode, + position: params.position, + orientation: params.orientation, + eval: params.savedEval, + ) + : _EvalGauge( + displayMode: displayMode, + position: params.position, + orientation: params.orientation, + ); } } @@ -121,23 +115,25 @@ class _EvalGaugeState extends ConsumerState<_EvalGauge> { final brightness = ref.watch(currentBrightnessProvider); final TextDirection textDirection = Directionality.of(context); - final evalDisplay = widget.position.outcome != null - ? widget.position.outcome!.winner == null - ? widget.position.isStalemate - ? context.l10n.stalemate - : context.l10n.insufficientMaterial - : widget.position.isCheckmate + final evalDisplay = + widget.position.outcome != null + ? widget.position.outcome!.winner == null + ? widget.position.isStalemate + ? context.l10n.stalemate + : context.l10n.insufficientMaterial + : widget.position.isCheckmate ? context.l10n.checkmate : context.l10n.variantEnding - : widget.eval?.evalString; + : widget.eval?.evalString; - final toValue = widget.position.outcome != null - ? widget.position.outcome!.winner == null - ? 0.5 - : widget.position.outcome!.winner == Side.white + final toValue = + widget.position.outcome != null + ? widget.position.outcome!.winner == null + ? 0.5 + : widget.position.outcome!.winner == Side.white ? 1.0 : 0.0 - : widget.animationValue; + : widget.animationValue; return TweenAnimationBuilder( tween: Tween(begin: fromValue, end: toValue), @@ -149,56 +145,44 @@ class _EvalGaugeState extends ConsumerState<_EvalGauge> { value: evalDisplay ?? context.l10n.loadingEngine, child: RepaintBoundary( child: Container( - constraints: widget.displayMode == EngineGaugeDisplayMode.vertical - ? const BoxConstraints( - minWidth: kEvalGaugeSize, - minHeight: double.infinity, - ) - : const BoxConstraints( - minWidth: double.infinity, - minHeight: kEvalGaugeSize, - ), - width: widget.displayMode == EngineGaugeDisplayMode.vertical - ? kEvalGaugeSize - : null, - height: widget.displayMode == EngineGaugeDisplayMode.vertical - ? null - : kEvalGaugeSize, + constraints: + widget.displayMode == EngineGaugeDisplayMode.vertical + ? const BoxConstraints(minWidth: kEvalGaugeSize, minHeight: double.infinity) + : const BoxConstraints(minWidth: double.infinity, minHeight: kEvalGaugeSize), + width: widget.displayMode == EngineGaugeDisplayMode.vertical ? kEvalGaugeSize : null, + height: widget.displayMode == EngineGaugeDisplayMode.vertical ? null : kEvalGaugeSize, child: CustomPaint( - painter: widget.displayMode == EngineGaugeDisplayMode.vertical - ? _EvalGaugeVerticalPainter( - orientation: widget.orientation, - backgroundColor: - EngineGauge.backgroundColor(context, brightness), - valueColor: EngineGauge.valueColor(context, brightness), - value: value, - ) - : _EvalGaugeHorizontalPainter( - backgroundColor: - EngineGauge.backgroundColor(context, brightness), - valueColor: EngineGauge.valueColor(context, brightness), - value: value, - textDirection: textDirection, - ), - child: widget.displayMode == EngineGaugeDisplayMode.vertical - ? const SizedBox.shrink() - : Align( - alignment: toValue >= 0.5 - ? Alignment.centerLeft - : Alignment.centerRight, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 4.0), - child: Text( - evalDisplay ?? '', - style: TextStyle( - color: - toValue >= 0.5 ? Colors.black : Colors.white, - fontSize: kEvalGaugeFontSize, - fontWeight: FontWeight.bold, + painter: + widget.displayMode == EngineGaugeDisplayMode.vertical + ? _EvalGaugeVerticalPainter( + orientation: widget.orientation, + backgroundColor: EngineGauge.backgroundColor(context, brightness), + valueColor: EngineGauge.valueColor(context, brightness), + value: value, + ) + : _EvalGaugeHorizontalPainter( + backgroundColor: EngineGauge.backgroundColor(context, brightness), + valueColor: EngineGauge.valueColor(context, brightness), + value: value, + textDirection: textDirection, + ), + child: + widget.displayMode == EngineGaugeDisplayMode.vertical + ? const SizedBox.shrink() + : Align( + alignment: toValue >= 0.5 ? Alignment.centerLeft : Alignment.centerRight, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: Text( + evalDisplay ?? '', + style: TextStyle( + color: toValue >= 0.5 ? Colors.black : Colors.white, + fontSize: kEvalGaugeFontSize, + fontWeight: FontWeight.bold, + ), ), ), ), - ), ), ), ), @@ -223,9 +207,10 @@ class _EvalGaugeHorizontalPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { - final Paint paint = Paint() - ..color = backgroundColor - ..style = PaintingStyle.fill; + final Paint paint = + Paint() + ..color = backgroundColor + ..style = PaintingStyle.fill; canvas.drawRect(Offset.zero & size, paint); paint.color = valueColor; @@ -272,9 +257,10 @@ class _EvalGaugeVerticalPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { - final Paint paint = Paint() - ..color = backgroundColor - ..style = PaintingStyle.fill; + final Paint paint = + Paint() + ..color = backgroundColor + ..style = PaintingStyle.fill; canvas.drawRect(Offset.zero & size, paint); paint.color = valueColor; diff --git a/lib/src/view/engine/engine_lines.dart b/lib/src/view/engine/engine_lines.dart index 96ffd7fcc1..1e31077c34 100644 --- a/lib/src/view/engine/engine_lines.dart +++ b/lib/src/view/engine/engine_lines.dart @@ -11,46 +11,31 @@ import 'package:lichess_mobile/src/view/engine/engine_gauge.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; class EngineLines extends ConsumerWidget { - const EngineLines({ - required this.onTapMove, - required this.clientEval, - required this.isGameOver, - }); + const EngineLines({required this.onTapMove, required this.clientEval, required this.isGameOver}); final void Function(NormalMove move) onTapMove; final ClientEval? clientEval; final bool isGameOver; @override Widget build(BuildContext context, WidgetRef ref) { - final numEvalLines = ref.watch( - analysisPreferencesProvider.select( - (p) => p.numEvalLines, - ), - ); + final numEvalLines = ref.watch(analysisPreferencesProvider.select((p) => p.numEvalLines)); final engineEval = ref.watch(engineEvaluationProvider).eval; final eval = engineEval ?? clientEval; - final emptyLines = List.filled( - numEvalLines, - const Engineline.empty(), - ); + final emptyLines = List.filled(numEvalLines, const Engineline.empty()); - final content = isGameOver - ? emptyLines - : (eval != null - ? eval.pvs - .take(numEvalLines) - .map( - (pv) => Engineline(onTapMove, eval.position, pv), - ) - .toList() - : emptyLines); + final content = + isGameOver + ? emptyLines + : (eval != null + ? eval.pvs + .take(numEvalLines) + .map((pv) => Engineline(onTapMove, eval.position, pv)) + .toList() + : emptyLines); if (content.length < numEvalLines) { - final padding = List.filled( - numEvalLines - content.length, - const Engineline.empty(), - ); + final padding = List.filled(numEvalLines - content.length, const Engineline.empty()); content.addAll(padding); } @@ -64,16 +49,12 @@ class EngineLines extends ConsumerWidget { } class Engineline extends ConsumerWidget { - const Engineline( - this.onTapMove, - this.fromPosition, - this.pvData, - ); + const Engineline(this.onTapMove, this.fromPosition, this.pvData); const Engineline.empty() - : onTapMove = null, - pvData = const PvData(moves: IListConst([])), - fromPosition = Chess.initial; + : onTapMove = null, + pvData = const PvData(moves: IListConst([])), + fromPosition = Chess.initial; final void Function(NormalMove move)? onTapMove; final Position fromPosition; @@ -82,16 +63,12 @@ class Engineline extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { if (pvData.moves.isEmpty) { - return const SizedBox( - height: kEvalGaugeSize, - child: SizedBox.shrink(), - ); + return const SizedBox(height: kEvalGaugeSize, child: SizedBox.shrink()); } - final pieceNotation = ref.watch(pieceNotationProvider).maybeWhen( - data: (value) => value, - orElse: () => defaultAccountPreferences.pieceNotation, - ); + final pieceNotation = ref + .watch(pieceNotationProvider) + .maybeWhen(data: (value) => value, orElse: () => defaultAccountPreferences.pieceNotation); final lineBuffer = StringBuffer(); int ply = fromPosition.ply + 1; @@ -100,8 +77,8 @@ class Engineline extends ConsumerWidget { ply.isOdd ? '${(ply / 2).ceil()}. $s ' : i == 0 - ? '${(ply / 2).ceil()}... $s ' - : '$s ', + ? '${(ply / 2).ceil()}... $s ' + : '$s ', ); ply += 1; }); @@ -122,21 +99,17 @@ class Engineline extends ConsumerWidget { children: [ Container( decoration: BoxDecoration( - color: pvData.winningSide == Side.black - ? EngineGauge.backgroundColor(context, brightness) - : EngineGauge.valueColor(context, brightness), + color: + pvData.winningSide == Side.black + ? EngineGauge.backgroundColor(context, brightness) + : EngineGauge.valueColor(context, brightness), borderRadius: BorderRadius.circular(4.0), ), - padding: const EdgeInsets.symmetric( - horizontal: 4.0, - vertical: 2.0, - ), + padding: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 2.0), child: Text( evalString, style: TextStyle( - color: pvData.winningSide == Side.black - ? Colors.white - : Colors.black, + color: pvData.winningSide == Side.black ? Colors.white : Colors.black, fontSize: kEvalGaugeFontSize, fontWeight: FontWeight.w600, ), @@ -149,9 +122,7 @@ class Engineline extends ConsumerWidget { maxLines: 1, softWrap: false, style: TextStyle( - fontFamily: pieceNotation == PieceNotation.symbol - ? 'ChessFont' - : null, + fontFamily: pieceNotation == PieceNotation.symbol ? 'ChessFont' : null, ), overflow: TextOverflow.ellipsis, ), diff --git a/lib/src/view/game/archived_game_screen.dart b/lib/src/view/game/archived_game_screen.dart index 88b720a6f1..230e346c66 100644 --- a/lib/src/view/game/archived_game_screen.dart +++ b/lib/src/view/game/archived_game_screen.dart @@ -45,27 +45,15 @@ class ArchivedGameScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { if (gameData != null) { - return _Body( - gameData: gameData, - orientation: orientation, - initialCursor: initialCursor, - ); + return _Body(gameData: gameData, orientation: orientation, initialCursor: initialCursor); } else { - return _LoadGame( - gameId: gameId!, - orientation: orientation, - initialCursor: initialCursor, - ); + return _LoadGame(gameId: gameId!, orientation: orientation, initialCursor: initialCursor); } } } class _LoadGame extends ConsumerWidget { - const _LoadGame({ - required this.gameId, - required this.orientation, - required this.initialCursor, - }); + const _LoadGame({required this.gameId, required this.orientation, required this.initialCursor}); final GameId gameId; final Side orientation; @@ -76,21 +64,11 @@ class _LoadGame extends ConsumerWidget { final game = ref.watch(archivedGameProvider(id: gameId)); return game.when( data: (game) { - return _Body( - gameData: game.data, - orientation: orientation, - initialCursor: initialCursor, - ); + return _Body(gameData: game.data, orientation: orientation, initialCursor: initialCursor); }, - loading: () => _Body( - gameData: null, - orientation: orientation, - initialCursor: initialCursor, - ), + loading: () => _Body(gameData: null, orientation: orientation, initialCursor: initialCursor), error: (error, stackTrace) { - debugPrint( - 'SEVERE: [ArchivedGameScreen] could not load game; $error\n$stackTrace', - ); + debugPrint('SEVERE: [ArchivedGameScreen] could not load game; $error\n$stackTrace'); switch (error) { case ServerException _ when error.statusCode == 404: return _Body( @@ -113,12 +91,7 @@ class _LoadGame extends ConsumerWidget { } class _Body extends StatelessWidget { - const _Body({ - required this.gameData, - required this.orientation, - this.initialCursor, - this.error, - }); + const _Body({required this.gameData, required this.orientation, this.initialCursor, this.error}); final LightArchivedGame? gameData; final Object? error; @@ -129,12 +102,9 @@ class _Body extends StatelessWidget { Widget build(BuildContext context) { return PlatformScaffold( appBar: PlatformAppBar( - title: gameData != null - ? _GameTitle(gameData: gameData!) - : const SizedBox.shrink(), + title: gameData != null ? _GameTitle(gameData: gameData!) : const SizedBox.shrink(), actions: [ - if (gameData == null && error == null) - const PlatformAppBarLoadingIndicator(), + if (gameData == null && error == null) const PlatformAppBarLoadingIndicator(), const ToggleSoundButton(), ], ), @@ -159,9 +129,7 @@ class _Body extends StatelessWidget { } class _GameTitle extends StatelessWidget { - const _GameTitle({ - required this.gameData, - }); + const _GameTitle({required this.gameData}); final LightArchivedGame gameData; @@ -173,22 +141,14 @@ class _GameTitle extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ if (gameData.source == GameSource.import) - Icon( - Icons.cloud_upload, - color: DefaultTextStyle.of(context).style.color, - ) + Icon(Icons.cloud_upload, color: DefaultTextStyle.of(context).style.color) else - Icon( - gameData.perf.icon, - color: DefaultTextStyle.of(context).style.color, - ), + Icon(gameData.perf.icon, color: DefaultTextStyle.of(context).style.color), const SizedBox(width: 4.0), if (gameData.source == GameSource.import) Text('Import • ${_dateFormat.format(gameData.createdAt)}') else - Text( - '${gameData.clockDisplay} • ${_dateFormat.format(gameData.lastMoveAt)}', - ), + Text('${gameData.clockDisplay} • ${_dateFormat.format(gameData.lastMoveAt)}'), ], ); } @@ -212,39 +172,26 @@ class _BoardBody extends ConsumerWidget { final gameData = archivedGameData; if (gameData == null) { - return BoardTable.empty( - showMoveListPlaceholder: true, - errorMessage: error?.toString(), - ); + return BoardTable.empty(showMoveListPlaceholder: true, errorMessage: error?.toString()); } if (initialCursor != null) { ref.listen(gameCursorProvider(gameData.id), (prev, cursor) { if (prev?.isLoading == true && cursor.hasValue) { - ref - .read(gameCursorProvider(gameData.id).notifier) - .cursorAt(initialCursor!); + ref.read(gameCursorProvider(gameData.id).notifier).cursorAt(initialCursor!); } }); } final isBoardTurned = ref.watch(isBoardTurnedProvider); final gameCursor = ref.watch(gameCursorProvider(gameData.id)); - final black = GamePlayer( - key: const ValueKey('black-player'), - player: gameData.black, - ); - final white = GamePlayer( - key: const ValueKey('white-player'), - player: gameData.white, - ); + final black = GamePlayer(key: const ValueKey('black-player'), player: gameData.black); + final white = GamePlayer(key: const ValueKey('white-player'), player: gameData.white); final topPlayer = orientation == Side.white ? black : white; final bottomPlayer = orientation == Side.white ? white : black; final loadingBoard = BoardTable( orientation: (isBoardTurned ? orientation.opposite : orientation), - fen: initialCursor == null - ? gameData.lastFen ?? kEmptyBoardFEN - : kEmptyBoardFEN, + fen: initialCursor == null ? gameData.lastFen ?? kEmptyBoardFEN : kEmptyBoardFEN, topTable: topPlayer, bottomTable: bottomPlayer, showMoveListPlaceholder: true, @@ -268,7 +215,8 @@ class _BoardBody extends ConsumerWidget { materialDiff: game.materialDiffAt(cursor, Side.white), ); - final topPlayerIsBlack = orientation == Side.white && !isBoardTurned || + final topPlayerIsBlack = + orientation == Side.white && !isBoardTurned || orientation == Side.black && isBoardTurned; final topPlayer = topPlayerIsBlack ? black : white; final bottomPlayer = topPlayerIsBlack ? white : black; @@ -281,23 +229,16 @@ class _BoardBody extends ConsumerWidget { lastMove: game.moveAt(cursor) as NormalMove?, topTable: topPlayer, bottomTable: bottomPlayer, - moves: game.steps - .skip(1) - .map((e) => e.sanMove!.san) - .toList(growable: false), + moves: game.steps.skip(1).map((e) => e.sanMove!.san).toList(growable: false), currentMoveIndex: cursor, onSelectMove: (moveIndex) { - ref - .read(gameCursorProvider(gameData.id).notifier) - .cursorAt(moveIndex); + ref.read(gameCursorProvider(gameData.id).notifier).cursorAt(moveIndex); }, ); }, loading: () => loadingBoard, error: (error, stackTrace) { - debugPrint( - 'SEVERE: [ArchivedGameScreen] could not load game; $error\n$stackTrace', - ); + debugPrint('SEVERE: [ArchivedGameScreen] could not load game; $error\n$stackTrace'); return loadingBoard; }, ); @@ -349,11 +290,7 @@ class _BottomBar extends ConsumerWidget { return BottomBar( children: [ - BottomBarButton( - label: context.l10n.menu, - onTap: showGameMenu, - icon: Icons.menu, - ), + BottomBarButton(label: context.l10n.menu, onTap: showGameMenu, icon: Icons.menu), if (gameCursor.hasValue) BottomBarButton( label: context.l10n.mobileShowResult, @@ -361,29 +298,30 @@ class _BottomBar extends ConsumerWidget { onTap: () { showAdaptiveDialog( context: context, - builder: (context) => - ArchivedGameResultDialog(game: gameCursor.requireValue.$1), + builder: (context) => ArchivedGameResultDialog(game: gameCursor.requireValue.$1), barrierDismissible: true, ); }, ), BottomBarButton( label: context.l10n.gameAnalysis, - onTap: gameCursor.hasValue - ? () { - final cursor = gameCursor.requireValue.$2; - pushPlatformRoute( - context, - builder: (context) => AnalysisScreen( - options: AnalysisOptions( - orientation: orientation, - gameId: gameData.id, - initialMoveCursor: cursor, - ), - ), - ); - } - : null, + onTap: + gameCursor.hasValue + ? () { + final cursor = gameCursor.requireValue.$2; + pushPlatformRoute( + context, + builder: + (context) => AnalysisScreen( + options: AnalysisOptions( + orientation: orientation, + gameId: gameData.id, + initialMoveCursor: cursor, + ), + ), + ); + } + : null, icon: Icons.biotech, ), RepeatButton( @@ -419,8 +357,6 @@ class _BottomBar extends ConsumerWidget { void _cursorBackward(WidgetRef ref) { if (archivedGameData == null) return; - ref - .read(gameCursorProvider(archivedGameData!.id).notifier) - .cursorBackward(); + ref.read(gameCursorProvider(archivedGameData!.id).notifier).cursorBackward(); } } diff --git a/lib/src/view/game/correspondence_clock_widget.dart b/lib/src/view/game/correspondence_clock_widget.dart index 73f4e2a076..8743daf3c8 100644 --- a/lib/src/view/game/correspondence_clock_widget.dart +++ b/lib/src/view/game/correspondence_clock_widget.dart @@ -18,16 +18,10 @@ class CorrespondenceClock extends ConsumerStatefulWidget { /// Callback when the clock reaches zero. final VoidCallback? onFlag; - const CorrespondenceClock({ - required this.duration, - required this.active, - this.onFlag, - super.key, - }); + const CorrespondenceClock({required this.duration, required this.active, this.onFlag, super.key}); @override - ConsumerState createState() => - _CorrespondenceClockState(); + ConsumerState createState() => _CorrespondenceClockState(); } const _period = Duration(seconds: 1); @@ -95,27 +89,24 @@ class _CorrespondenceClockState extends ConsumerState { final mins = timeLeft.inMinutes.remainder(60); final secs = timeLeft.inSeconds.remainder(60).toString().padLeft(2, '0'); final brightness = ref.watch(currentBrightnessProvider); - final clockStyle = brightness == Brightness.dark - ? ClockStyle.darkThemeStyle - : ClockStyle.lightThemeStyle; + final clockStyle = + brightness == Brightness.dark ? ClockStyle.darkThemeStyle : ClockStyle.lightThemeStyle; final remainingHeight = estimateRemainingHeightLeftBoard(context); - final daysStr = days > 1 - ? context.l10n.nbDays(days) - : days == 1 + final daysStr = + days > 1 + ? context.l10n.nbDays(days) + : days == 1 ? context.l10n.oneDay : ''; - final hoursStr = - days > 0 && hours > 0 ? ' ${context.l10n.nbHours(hours)}' : ''; + final hoursStr = days > 0 && hours > 0 ? ' ${context.l10n.nbHours(hours)}' : ''; return RepaintBoundary( child: Container( decoration: BoxDecoration( borderRadius: const BorderRadius.all(Radius.circular(5.0)), - color: widget.active - ? clockStyle.activeBackgroundColor - : clockStyle.backgroundColor, + color: widget.active ? clockStyle.activeBackgroundColor : clockStyle.backgroundColor, ), child: Padding( padding: const EdgeInsets.symmetric(vertical: 3.0, horizontal: 5.0), @@ -127,19 +118,10 @@ class _CorrespondenceClockState extends ConsumerState { text: TextSpan( text: '$daysStr$hoursStr', style: TextStyle( - color: widget.active - ? clockStyle.activeTextColor - : clockStyle.textColor, + color: widget.active ? clockStyle.activeTextColor : clockStyle.textColor, fontSize: 18, - height: - remainingHeight < kSmallRemainingHeightLeftBoardThreshold - ? 1.0 - : null, - fontFeatures: days == 0 - ? const [ - FontFeature.tabularFigures(), - ] - : null, + height: remainingHeight < kSmallRemainingHeightLeftBoardThreshold ? 1.0 : null, + fontFeatures: days == 0 ? const [FontFeature.tabularFigures()] : null, ), children: [ if (days == 0) ...[ @@ -147,17 +129,14 @@ class _CorrespondenceClockState extends ConsumerState { TextSpan( text: ':', style: TextStyle( - color: widget.active && - timeLeft.inSeconds.remainder(2) == 0 - ? clockStyle.activeTextColor.withValues(alpha: 0.5) - : null, + color: + widget.active && timeLeft.inSeconds.remainder(2) == 0 + ? clockStyle.activeTextColor.withValues(alpha: 0.5) + : null, ), ), TextSpan(text: mins.toString().padLeft(2, '0')), - if (hours == 0) ...[ - const TextSpan(text: ':'), - TextSpan(text: secs), - ], + if (hours == 0) ...[const TextSpan(text: ':'), TextSpan(text: secs)], ], ], ), diff --git a/lib/src/view/game/game_body.dart b/lib/src/view/game/game_body.dart index 955f629a39..187006c047 100644 --- a/lib/src/view/game/game_body.dart +++ b/lib/src/view/game/game_body.dart @@ -94,20 +94,11 @@ class GameBody extends ConsumerWidget { ref.listen( ctrlProvider, - (prev, state) => _stateListener( - prev, - state, - context: context, - ref: ref, - ), + (prev, state) => _stateListener(prev, state, context: context, ref: ref), ); final boardPreferences = ref.watch(boardPreferencesProvider); - final blindfoldMode = ref.watch( - gamePreferencesProvider.select( - (prefs) => prefs.blindfoldMode, - ), - ); + final blindfoldMode = ref.watch(gamePreferencesProvider.select((prefs) => prefs.blindfoldMode)); final gameStateAsync = ref.watch(ctrlProvider); @@ -115,122 +106,110 @@ class GameBody extends ConsumerWidget { data: (gameState) { final (position, legalMoves) = gameState.currentPosition; final youAre = gameState.game.youAre ?? Side.white; - final archivedBlackClock = - gameState.game.archivedBlackClockAt(gameState.stepCursor); - final archivedWhiteClock = - gameState.game.archivedWhiteClockAt(gameState.stepCursor); + final archivedBlackClock = gameState.game.archivedBlackClockAt(gameState.stepCursor); + final archivedWhiteClock = gameState.game.archivedWhiteClockAt(gameState.stepCursor); final black = GamePlayer( player: gameState.game.black, - materialDiff: boardPreferences.materialDifferenceFormat.visible - ? gameState.game.materialDiffAt(gameState.stepCursor, Side.black) - : null, + materialDiff: + boardPreferences.materialDifferenceFormat.visible + ? gameState.game.materialDiffAt(gameState.stepCursor, Side.black) + : null, materialDifferenceFormat: boardPreferences.materialDifferenceFormat, - timeToMove: gameState.game.sideToMove == Side.black - ? gameState.timeToMove - : null, + timeToMove: gameState.game.sideToMove == Side.black ? gameState.timeToMove : null, mePlaying: youAre == Side.black, zenMode: gameState.isZenModeActive, clockPosition: boardPreferences.clockPosition, confirmMoveCallbacks: youAre == Side.black && gameState.moveToConfirm != null ? ( - confirm: () { - ref.read(ctrlProvider.notifier).confirmMove(); - }, - cancel: () { - ref.read(ctrlProvider.notifier).cancelMove(); - }, - ) + confirm: () { + ref.read(ctrlProvider.notifier).confirmMove(); + }, + cancel: () { + ref.read(ctrlProvider.notifier).cancelMove(); + }, + ) : null, - clock: archivedBlackClock != null - ? Clock( - timeLeft: archivedBlackClock, - active: false, - ) - : gameState.liveClock != null + clock: + archivedBlackClock != null + ? Clock(timeLeft: archivedBlackClock, active: false) + : gameState.liveClock != null ? RepaintBoundary( - child: ValueListenableBuilder( - key: blackClockKey, - valueListenable: gameState.liveClock!.black, - builder: (context, value, _) { - return Clock( - timeLeft: value, - active: gameState.activeClockSide == Side.black, - emergencyThreshold: youAre == Side.black - ? gameState.game.meta.clock?.emergency - : null, - ); - }, - ), - ) - : gameState.game.correspondenceClock != null - ? CorrespondenceClock( - duration: gameState.game.correspondenceClock!.black, + child: ValueListenableBuilder( + key: blackClockKey, + valueListenable: gameState.liveClock!.black, + builder: (context, value, _) { + return Clock( + timeLeft: value, active: gameState.activeClockSide == Side.black, - onFlag: () => - ref.read(ctrlProvider.notifier).onFlag(), - ) - : null, + emergencyThreshold: + youAre == Side.black ? gameState.game.meta.clock?.emergency : null, + ); + }, + ), + ) + : gameState.game.correspondenceClock != null + ? CorrespondenceClock( + duration: gameState.game.correspondenceClock!.black, + active: gameState.activeClockSide == Side.black, + onFlag: () => ref.read(ctrlProvider.notifier).onFlag(), + ) + : null, ); final white = GamePlayer( player: gameState.game.white, - materialDiff: boardPreferences.materialDifferenceFormat.visible - ? gameState.game.materialDiffAt(gameState.stepCursor, Side.white) - : null, + materialDiff: + boardPreferences.materialDifferenceFormat.visible + ? gameState.game.materialDiffAt(gameState.stepCursor, Side.white) + : null, materialDifferenceFormat: boardPreferences.materialDifferenceFormat, - timeToMove: gameState.game.sideToMove == Side.white - ? gameState.timeToMove - : null, + timeToMove: gameState.game.sideToMove == Side.white ? gameState.timeToMove : null, mePlaying: youAre == Side.white, zenMode: gameState.isZenModeActive, clockPosition: boardPreferences.clockPosition, confirmMoveCallbacks: youAre == Side.white && gameState.moveToConfirm != null ? ( - confirm: () { - ref.read(ctrlProvider.notifier).confirmMove(); - }, - cancel: () { - ref.read(ctrlProvider.notifier).cancelMove(); - }, - ) + confirm: () { + ref.read(ctrlProvider.notifier).confirmMove(); + }, + cancel: () { + ref.read(ctrlProvider.notifier).cancelMove(); + }, + ) : null, - clock: archivedWhiteClock != null - ? Clock( - timeLeft: archivedWhiteClock, - active: false, - ) - : gameState.liveClock != null + clock: + archivedWhiteClock != null + ? Clock(timeLeft: archivedWhiteClock, active: false) + : gameState.liveClock != null ? RepaintBoundary( - child: ValueListenableBuilder( - key: whiteClockKey, - valueListenable: gameState.liveClock!.white, - builder: (context, value, _) { - return Clock( - timeLeft: value, - active: gameState.activeClockSide == Side.white, - emergencyThreshold: youAre == Side.white - ? gameState.game.meta.clock?.emergency - : null, - ); - }, - ), - ) - : gameState.game.correspondenceClock != null - ? CorrespondenceClock( - duration: gameState.game.correspondenceClock!.white, + child: ValueListenableBuilder( + key: whiteClockKey, + valueListenable: gameState.liveClock!.white, + builder: (context, value, _) { + return Clock( + timeLeft: value, active: gameState.activeClockSide == Side.white, - onFlag: () => - ref.read(ctrlProvider.notifier).onFlag(), - ) - : null, + emergencyThreshold: + youAre == Side.white ? gameState.game.meta.clock?.emergency : null, + ); + }, + ), + ) + : gameState.game.correspondenceClock != null + ? CorrespondenceClock( + duration: gameState.game.correspondenceClock!.white, + active: gameState.activeClockSide == Side.white, + onFlag: () => ref.read(ctrlProvider.notifier).onFlag(), + ) + : null, ); final isBoardTurned = ref.watch(isBoardTurnedProvider); - final topPlayerIsBlack = youAre == Side.white && !isBoardTurned || - youAre == Side.black && isBoardTurned; + final topPlayerIsBlack = + youAre == Side.white && !isBoardTurned || youAre == Side.black && isBoardTurned; final topPlayer = topPlayerIsBlack ? black : white; final bottomPlayer = topPlayerIsBlack ? white : black; @@ -243,8 +222,7 @@ class GameBody extends ConsumerWidget { final content = WakelockWidget( shouldEnableOnFocusGained: () => gameState.game.playable, child: PopScope( - canPop: gameState.game.meta.speed == Speed.correspondence || - !gameState.game.playable, + canPop: gameState.game.meta.speed == Speed.correspondence || !gameState.game.playable, child: Column( children: [ Expanded( @@ -255,14 +233,12 @@ class GameBody extends ConsumerWidget { boardSettingsOverrides: BoardSettingsOverrides( animationDuration: animationDuration, autoQueenPromotion: gameState.canAutoQueen, - autoQueenPromotionOnPremove: - gameState.canAutoQueenOnPremove, + autoQueenPromotionOnPremove: gameState.canAutoQueenOnPremove, blindfoldMode: blindfoldMode, ), orientation: isBoardTurned ? youAre.opposite : youAre, fen: position.fen, - lastMove: gameState.game.moveAt(gameState.stepCursor) - as NormalMove?, + lastMove: gameState.game.moveAt(gameState.stepCursor) as NormalMove?, gameData: GameData( playerSide: gameState.game.playable && !gameState.isReplaying @@ -270,40 +246,32 @@ class GameBody extends ConsumerWidget { ? PlayerSide.white : PlayerSide.black : PlayerSide.none, - isCheck: boardPreferences.boardHighlights && - position.isCheck, + isCheck: boardPreferences.boardHighlights && position.isCheck, sideToMove: position.turn, validMoves: legalMoves, promotionMove: gameState.promotionMove, onMove: (move, {isDrop}) { - ref.read(ctrlProvider.notifier).userMove( - move, - isDrop: isDrop, - ); + ref.read(ctrlProvider.notifier).userMove(move, isDrop: isDrop); }, onPromotionSelection: (role) { - ref - .read(ctrlProvider.notifier) - .onPromotionSelection(role); + ref.read(ctrlProvider.notifier).onPromotionSelection(role); }, - premovable: gameState.canPremove - ? ( - onSetPremove: (move) { - ref - .read(ctrlProvider.notifier) - .setPremove(move); - }, - premove: gameState.premove, - ) - : null, + premovable: + gameState.canPremove + ? ( + onSetPremove: (move) { + ref.read(ctrlProvider.notifier).setPremove(move); + }, + premove: gameState.premove, + ) + : null, ), topTable: topPlayer, - bottomTable: gameState.canShowClaimWinCountdown && - gameState.opponentLeftCountdown != null - ? _ClaimWinCountdown( - duration: gameState.opponentLeftCountdown!, - ) - : bottomPlayer, + bottomTable: + gameState.canShowClaimWinCountdown && + gameState.opponentLeftCountdown != null + ? _ClaimWinCountdown(duration: gameState.opponentLeftCountdown!) + : bottomPlayer, moves: gameState.game.steps .skip(1) .map((e) => e.sanMove!.san) @@ -328,42 +296,33 @@ class GameBody extends ConsumerWidget { return Theme.of(context).platform == TargetPlatform.android ? AndroidGesturesExclusionWidget( - boardKey: boardKey, - shouldExcludeGesturesOnFocusGained: () => - gameState.game.meta.speed != Speed.correspondence && - gameState.game.playable, - shouldSetImmersiveMode: - boardPreferences.immersiveModeWhilePlaying ?? false, - child: content, - ) + boardKey: boardKey, + shouldExcludeGesturesOnFocusGained: + () => + gameState.game.meta.speed != Speed.correspondence && gameState.game.playable, + shouldSetImmersiveMode: boardPreferences.immersiveModeWhilePlaying ?? false, + child: content, + ) : content; }, - loading: () => PopScope( - canPop: true, - child: Column( - children: [ - Expanded( - child: SafeArea( - bottom: false, - child: loadingBoardWidget, - ), - ), - _GameBottomBar( - id: id, - onLoadGameCallback: onLoadGameCallback, - onNewOpponentCallback: onNewOpponentCallback, + loading: + () => PopScope( + canPop: true, + child: Column( + children: [ + Expanded(child: SafeArea(bottom: false, child: loadingBoardWidget)), + _GameBottomBar( + id: id, + onLoadGameCallback: onLoadGameCallback, + onNewOpponentCallback: onNewOpponentCallback, + ), + ], ), - ], - ), - ), + ), error: (e, s) { - debugPrint( - 'SEVERE: [GameBody] could not load game data; $e\n$s', - ); + debugPrint('SEVERE: [GameBody] could not load game data; $e\n$s'); return const PopScope( - child: LoadGameError( - 'Sorry, we could not load the game. Please try again later.', - ), + child: LoadGameError('Sorry, we could not load the game. Please try again later.'), ); }, ); @@ -379,17 +338,15 @@ class GameBody extends ConsumerWidget { // If the game is no longer playable, show the game end dialog. // We want to show it only once, whether the game is already finished on // first load or not. - if ((prev?.hasValue != true || - prev!.requireValue.game.playable == true) && + if ((prev?.hasValue != true || prev!.requireValue.game.playable == true) && state.requireValue.game.playable == false) { Timer(const Duration(milliseconds: 500), () { if (context.mounted) { showAdaptiveDialog( context: context, - builder: (context) => GameResultDialog( - id: id, - onNewOpponentCallback: onNewOpponentCallback, - ), + builder: + (context) => + GameResultDialog(id: id, onNewOpponentCallback: onNewOpponentCallback), barrierDismissible: true, ); } @@ -397,8 +354,7 @@ class GameBody extends ConsumerWidget { } // true when the game was loaded, playable, and just finished - if (prev?.valueOrNull?.game.playable == true && - state.requireValue.game.playable == false) { + if (prev?.valueOrNull?.game.playable == true && state.requireValue.game.playable == false) { clearAndroidBoardGesturesExclusion(); } // true when the game was not loaded: handles rematches @@ -408,8 +364,7 @@ class GameBody extends ConsumerWidget { setAndroidBoardGesturesExclusion( boardKey, withImmersiveMode: - ref.read(boardPreferencesProvider).immersiveModeWhilePlaying ?? - false, + ref.read(boardPreferencesProvider).immersiveModeWhilePlaying ?? false, ); } } @@ -417,8 +372,7 @@ class GameBody extends ConsumerWidget { if (prev?.hasValue == true && state.hasValue) { // Opponent is gone long enough to show the claim win dialog. - if (!prev!.requireValue.game.canClaimWin && - state.requireValue.game.canClaimWin) { + if (!prev!.requireValue.game.canClaimWin && state.requireValue.game.canClaimWin) { if (context.mounted) { showAdaptiveDialog( context: context, @@ -453,26 +407,27 @@ class _GameBottomBar extends ConsumerWidget { final ongoingGames = ref.watch(ongoingGamesProvider); final gamePrefs = ref.watch(gamePreferencesProvider); final gameStateAsync = ref.watch(gameControllerProvider(id)); - final chatStateAsync = gamePrefs.enableChat == true - ? ref.watch(chatControllerProvider(id)) - : null; + final chatStateAsync = + gamePrefs.enableChat == true ? ref.watch(chatControllerProvider(id)) : null; return BottomBar( children: gameStateAsync.when( data: (gameState) { - final isChatEnabled = - chatStateAsync != null && !gameState.isZenModeActive; - - final chatUnreadLabel = isChatEnabled - ? chatStateAsync.maybeWhen( - data: (s) => s.unreadMessages > 0 - ? (s.unreadMessages < 10) - ? s.unreadMessages.toString() - : '9+' - : null, - orElse: () => null, - ) - : null; + final isChatEnabled = chatStateAsync != null && !gameState.isZenModeActive; + + final chatUnreadLabel = + isChatEnabled + ? chatStateAsync.maybeWhen( + data: + (s) => + s.unreadMessages > 0 + ? (s.unreadMessages < 10) + ? s.unreadMessages.toString() + : '9+' + : null, + orElse: () => null, + ) + : null; return [ BottomBarButton( @@ -488,43 +443,37 @@ class _GameBottomBar extends ConsumerWidget { onTap: () { showAdaptiveDialog( context: context, - builder: (context) => GameResultDialog( - id: id, - onNewOpponentCallback: onNewOpponentCallback, - ), + builder: + (context) => + GameResultDialog(id: id, onNewOpponentCallback: onNewOpponentCallback), barrierDismissible: true, ); }, icon: Icons.info_outline, ), - if (gameState.game.playable && - gameState.game.opponent?.offeringDraw == true) + if (gameState.game.playable && gameState.game.opponent?.offeringDraw == true) BottomBarButton( label: context.l10n.yourOpponentOffersADraw, highlighted: true, onTap: () { showAdaptiveDialog( context: context, - builder: (context) => _GameNegotiationDialog( - title: Text(context.l10n.yourOpponentOffersADraw), - onAccept: () { - ref - .read(gameControllerProvider(id).notifier) - .offerOrAcceptDraw(); - }, - onDecline: () { - ref - .read(gameControllerProvider(id).notifier) - .cancelOrDeclineDraw(); - }, - ), + builder: + (context) => _GameNegotiationDialog( + title: Text(context.l10n.yourOpponentOffersADraw), + onAccept: () { + ref.read(gameControllerProvider(id).notifier).offerOrAcceptDraw(); + }, + onDecline: () { + ref.read(gameControllerProvider(id).notifier).cancelOrDeclineDraw(); + }, + ), barrierDismissible: true, ); }, icon: Icons.handshake_outlined, ) - else if (gameState.game.playable && - gameState.game.isThreefoldRepetition == true) + else if (gameState.game.playable && gameState.game.isThreefoldRepetition == true) BottomBarButton( label: context.l10n.threefoldRepetition, highlighted: true, @@ -537,27 +486,23 @@ class _GameBottomBar extends ConsumerWidget { }, icon: Icons.handshake_outlined, ) - else if (gameState.game.playable && - gameState.game.opponent?.proposingTakeback == true) + else if (gameState.game.playable && gameState.game.opponent?.proposingTakeback == true) BottomBarButton( label: context.l10n.yourOpponentProposesATakeback, highlighted: true, onTap: () { showAdaptiveDialog( context: context, - builder: (context) => _GameNegotiationDialog( - title: Text(context.l10n.yourOpponentProposesATakeback), - onAccept: () { - ref - .read(gameControllerProvider(id).notifier) - .acceptTakeback(); - }, - onDecline: () { - ref - .read(gameControllerProvider(id).notifier) - .cancelOrDeclineTakeback(); - }, - ), + builder: + (context) => _GameNegotiationDialog( + title: Text(context.l10n.yourOpponentProposesATakeback), + onAccept: () { + ref.read(gameControllerProvider(id).notifier).acceptTakeback(); + }, + onDecline: () { + ref.read(gameControllerProvider(id).notifier).cancelOrDeclineTakeback(); + }, + ), barrierDismissible: true, ); }, @@ -570,8 +515,7 @@ class _GameBottomBar extends ConsumerWidget { onTap: () { pushPlatformRoute( context, - builder: (_) => - AnalysisScreen(options: gameState.analysisOptions), + builder: (_) => AnalysisScreen(options: gameState.analysisOptions), ); }, ) @@ -579,23 +523,20 @@ class _GameBottomBar extends ConsumerWidget { gameState.game.meta.speed == Speed.ultraBullet) BottomBarButton( label: context.l10n.resign, - onTap: gameState.game.resignable - ? gameState.shouldConfirmResignAndDrawOffer - ? () => _showConfirmDialog( + onTap: + gameState.game.resignable + ? gameState.shouldConfirmResignAndDrawOffer + ? () => _showConfirmDialog( context, description: Text(context.l10n.resignTheGame), onConfirm: () { - ref - .read(gameControllerProvider(id).notifier) - .resignGame(); + ref.read(gameControllerProvider(id).notifier).resignGame(); }, ) - : () { - ref - .read(gameControllerProvider(id).notifier) - .resignGame(); - } - : null, + : () { + ref.read(gameControllerProvider(id).notifier).resignGame(); + } + : null, icon: Icons.flag, ) else @@ -606,8 +547,7 @@ class _GameBottomBar extends ConsumerWidget { }, icon: CupertinoIcons.arrow_2_squarepath, ), - if (gameState.game.meta.speed == Speed.correspondence && - !gameState.game.finished) + if (gameState.game.meta.speed == Speed.correspondence && !gameState.game.finished) BottomBarButton( label: 'Go to the next game', icon: Icons.skip_next, @@ -616,50 +556,45 @@ class _GameBottomBar extends ConsumerWidget { final nextTurn = games .whereNot((g) => g.fullId == id) .firstWhereOrNull((g) => g.isMyTurn); - return nextTurn != null - ? () => onLoadGameCallback(nextTurn.fullId) - : null; + return nextTurn != null ? () => onLoadGameCallback(nextTurn.fullId) : null; }, orElse: () => null, ), ), BottomBarButton( label: context.l10n.chat, - onTap: isChatEnabled - ? () { - pushPlatformRoute( - context, - builder: (BuildContext context) { - return MessageScreen( - title: UserFullNameWidget( - user: gameState.game.opponent?.user, - ), - me: gameState.game.me?.user, - id: id, - ); - }, - ); - } - : null, - icon: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoIcons.chat_bubble - : Icons.chat_bubble_outline, + onTap: + isChatEnabled + ? () { + pushPlatformRoute( + context, + builder: (BuildContext context) { + return MessageScreen( + title: UserFullNameWidget(user: gameState.game.opponent?.user), + me: gameState.game.me?.user, + id: id, + ); + }, + ); + } + : null, + icon: + Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoIcons.chat_bubble + : Icons.chat_bubble_outline, badgeLabel: chatUnreadLabel, ), RepeatButton( - onLongPress: - gameState.canGoBackward ? () => _moveBackward(ref) : null, + onLongPress: gameState.canGoBackward ? () => _moveBackward(ref) : null, child: BottomBarButton( - onTap: - gameState.canGoBackward ? () => _moveBackward(ref) : null, + onTap: gameState.canGoBackward ? () => _moveBackward(ref) : null, label: 'Previous', icon: CupertinoIcons.chevron_back, showTooltip: false, ), ), RepeatButton( - onLongPress: - gameState.canGoForward ? () => _moveForward(ref) : null, + onLongPress: gameState.canGoForward ? () => _moveForward(ref) : null, child: BottomBarButton( onTap: gameState.canGoForward ? () => _moveForward(ref) : null, label: context.l10n.next, @@ -694,15 +629,13 @@ class _GameBottomBar extends ConsumerWidget { ref.read(isBoardTurnedProvider.notifier).toggle(); }, ), - if (gameState.game.playable && - gameState.game.meta.speed == Speed.correspondence) + if (gameState.game.playable && gameState.game.meta.speed == Speed.correspondence) BottomSheetAction( makeLabel: (context) => Text(context.l10n.analysis), onPressed: (context) { pushPlatformRoute( context, - builder: (_) => - AnalysisScreen(options: gameState.analysisOptions), + builder: (_) => AnalysisScreen(options: gameState.analysisOptions), ); }, ), @@ -715,11 +648,10 @@ class _GameBottomBar extends ConsumerWidget { ), if (gameState.game.meta.clock != null && gameState.game.canGiveTime) BottomSheetAction( - makeLabel: (context) => Text( - context.l10n.giveNbSeconds( - gameState.game.meta.clock!.moreTime?.inSeconds ?? 15, - ), - ), + makeLabel: + (context) => Text( + context.l10n.giveNbSeconds(gameState.game.meta.clock!.moreTime?.inSeconds ?? 15), + ), onPressed: (context) { ref.read(gameControllerProvider(id).notifier).moreTime(); }, @@ -733,50 +665,43 @@ class _GameBottomBar extends ConsumerWidget { ), if (gameState.game.me?.proposingTakeback == true) BottomSheetAction( - makeLabel: (context) => - Text(context.l10n.mobileCancelTakebackOffer), + makeLabel: (context) => Text(context.l10n.mobileCancelTakebackOffer), isDestructiveAction: true, onPressed: (context) { - ref - .read(gameControllerProvider(id).notifier) - .cancelOrDeclineTakeback(); + ref.read(gameControllerProvider(id).notifier).cancelOrDeclineTakeback(); }, ), if (gameState.canOfferDraw) BottomSheetAction( makeLabel: (context) => Text(context.l10n.offerDraw), - onPressed: gameState.shouldConfirmResignAndDrawOffer - ? (context) => _showConfirmDialog( + onPressed: + gameState.shouldConfirmResignAndDrawOffer + ? (context) => _showConfirmDialog( context, description: Text(context.l10n.offerDraw), onConfirm: () { - ref - .read(gameControllerProvider(id).notifier) - .offerOrAcceptDraw(); + ref.read(gameControllerProvider(id).notifier).offerOrAcceptDraw(); }, ) - : (context) { - ref - .read(gameControllerProvider(id).notifier) - .offerOrAcceptDraw(); - }, + : (context) { + ref.read(gameControllerProvider(id).notifier).offerOrAcceptDraw(); + }, ), if (gameState.game.resignable) BottomSheetAction( makeLabel: (context) => Text(context.l10n.resign), - onPressed: gameState.shouldConfirmResignAndDrawOffer - ? (context) => _showConfirmDialog( + onPressed: + gameState.shouldConfirmResignAndDrawOffer + ? (context) => _showConfirmDialog( context, description: Text(context.l10n.resignTheGame), onConfirm: () { - ref - .read(gameControllerProvider(id).notifier) - .resignGame(); + ref.read(gameControllerProvider(id).notifier).resignGame(); }, ) - : (context) { - ref.read(gameControllerProvider(id).notifier).resignGame(); - }, + : (context) { + ref.read(gameControllerProvider(id).notifier).resignGame(); + }, ), if (gameState.game.canClaimWin) ...[ BottomSheetAction( @@ -803,15 +728,12 @@ class _GameBottomBar extends ConsumerWidget { ref.read(gameControllerProvider(id).notifier).declineRematch(); }, ) - else if (gameState.canOfferRematch && - gameState.game.opponent?.onGame == true) + else if (gameState.canOfferRematch && gameState.game.opponent?.onGame == true) BottomSheetAction( makeLabel: (context) => Text(context.l10n.rematch), dismissOnPress: true, onPressed: (context) { - ref - .read(gameControllerProvider(id).notifier) - .proposeOrAcceptRematch(); + ref.read(gameControllerProvider(id).notifier).proposeOrAcceptRematch(); }, ), if (gameState.canGetNewOpponent) @@ -822,8 +744,7 @@ class _GameBottomBar extends ConsumerWidget { if (gameState.game.finished) ...makeFinishedGameShareActions( gameState.game, - currentGamePosition: - gameState.game.positionAt(gameState.stepCursor), + currentGamePosition: gameState.game.positionAt(gameState.stepCursor), lastMove: gameState.game.moveAt(gameState.stepCursor), orientation: gameState.game.youAre ?? Side.white, context: context, @@ -840,14 +761,15 @@ class _GameBottomBar extends ConsumerWidget { }) async { final result = await showAdaptiveDialog( context: context, - builder: (context) => YesNoDialog( - title: Text(context.l10n.mobileAreYouSure), - content: description, - onYes: () { - return Navigator.of(context).pop(true); - }, - onNo: () => Navigator.of(context).pop(false), - ), + builder: + (context) => YesNoDialog( + title: Text(context.l10n.mobileAreYouSure), + content: description, + onYes: () { + return Navigator.of(context).pop(true); + }, + onNo: () => Navigator.of(context).pop(false), + ), ); if (result == true) { onConfirm(); @@ -881,23 +803,15 @@ class _GameNegotiationDialog extends StatelessWidget { return PlatformAlertDialog( content: title, actions: [ - PlatformDialogAction( - onPressed: accept, - child: Text(context.l10n.accept), - ), - PlatformDialogAction( - onPressed: decline, - child: Text(context.l10n.decline), - ), + PlatformDialogAction(onPressed: accept, child: Text(context.l10n.accept)), + PlatformDialogAction(onPressed: decline, child: Text(context.l10n.decline)), ], ); } } class _ThreefoldDialog extends ConsumerWidget { - const _ThreefoldDialog({ - required this.id, - }); + const _ThreefoldDialog({required this.id}); final GameFullId id; @@ -917,23 +831,15 @@ class _ThreefoldDialog extends ConsumerWidget { return PlatformAlertDialog( content: content, actions: [ - PlatformDialogAction( - onPressed: accept, - child: Text(context.l10n.claimADraw), - ), - PlatformDialogAction( - onPressed: decline, - child: Text(context.l10n.cancel), - ), + PlatformDialogAction(onPressed: accept, child: Text(context.l10n.claimADraw)), + PlatformDialogAction(onPressed: decline, child: Text(context.l10n.cancel)), ], ); } } class _ClaimWinDialog extends ConsumerWidget { - const _ClaimWinDialog({ - required this.id, - }); + const _ClaimWinDialog({required this.id}); final GameFullId id; @@ -972,9 +878,7 @@ class _ClaimWinDialog extends ConsumerWidget { } class _ClaimWinCountdown extends StatelessWidget { - const _ClaimWinCountdown({ - required this.duration, - }); + const _ClaimWinCountdown({required this.duration}); final Duration duration; diff --git a/lib/src/view/game/game_common_widgets.dart b/lib/src/view/game/game_common_widgets.dart index b5aff24c69..81cfed3c1c 100644 --- a/lib/src/view/game/game_common_widgets.dart +++ b/lib/src/view/game/game_common_widgets.dart @@ -27,13 +27,7 @@ import 'ping_rating.dart'; final _gameTitledateFormat = DateFormat.yMMMd(); class GameAppBar extends ConsumerWidget { - const GameAppBar({ - this.id, - this.seek, - this.challenge, - this.lastMoveAt, - super.key, - }); + const GameAppBar({this.id, this.seek, this.challenge, this.lastMoveAt, super.key}); final GameSeek? seek; final ChallengeRequest? challenge; @@ -49,36 +43,35 @@ class GameAppBar extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final shouldPreventGoingBackAsync = id != null - ? ref.watch(shouldPreventGoingBackProvider(id!)) - : const AsyncValue.data(true); + final shouldPreventGoingBackAsync = + id != null ? ref.watch(shouldPreventGoingBackProvider(id!)) : const AsyncValue.data(true); return PlatformAppBar( leading: shouldPreventGoingBackAsync.maybeWhen( data: (prevent) => prevent ? pingRating : null, orElse: () => pingRating, ), - title: id != null - ? _StandaloneGameTitle(id: id!, lastMoveAt: lastMoveAt) - : seek != null + title: + id != null + ? _StandaloneGameTitle(id: id!, lastMoveAt: lastMoveAt) + : seek != null ? _LobbyGameTitle(seek: seek!) : challenge != null - ? _ChallengeGameTitle(challenge: challenge!) - : const SizedBox.shrink(), + ? _ChallengeGameTitle(challenge: challenge!) + : const SizedBox.shrink(), actions: [ const ToggleSoundButton(), if (id != null) AppBarIconButton( - onPressed: () => showAdaptiveBottomSheet( - context: context, - isDismissible: true, - isScrollControlled: true, - showDragHandle: true, - constraints: BoxConstraints( - minHeight: MediaQuery.sizeOf(context).height * 0.5, - ), - builder: (_) => GameSettings(id: id!), - ), + onPressed: + () => showAdaptiveBottomSheet( + context: context, + isDismissible: true, + isScrollControlled: true, + showDragHandle: true, + constraints: BoxConstraints(minHeight: MediaQuery.sizeOf(context).height * 0.5), + builder: (_) => GameSettings(id: id!), + ), semanticsLabel: context.l10n.settingsSettings, icon: const Icon(Icons.settings), ), @@ -106,12 +99,13 @@ List makeFinishedGameShareActions( isDismissible: true, isScrollControlled: true, showDragHandle: true, - builder: (context) => GameShareBottomSheet( - game: game, - currentGamePosition: currentGamePosition, - orientation: orientation, - lastMove: lastMove, - ), + builder: + (context) => GameShareBottomSheet( + game: game, + currentGamePosition: currentGamePosition, + orientation: orientation, + lastMove: lastMove, + ), ); }, ), @@ -140,10 +134,7 @@ class GameShareBottomSheet extends ConsumerWidget { icon: CupertinoIcons.link, closeOnPressed: false, onPressed: () { - launchShareDialog( - context, - uri: lichessUri('/${game.id}'), - ); + launchShareDialog(context, uri: lichessUri('/${game.id}')); }, child: Text(context.l10n.mobileShareGameURL), ), @@ -166,20 +157,14 @@ class GameShareBottomSheet extends ConsumerWidget { launchShareDialog( context, files: [gif], - subject: '${game.meta.perf.title} • ${context.l10n.resVsX( - game.white.fullName(context), - game.black.fullName(context), - )}', + subject: + '${game.meta.perf.title} • ${context.l10n.resVsX(game.white.fullName(context), game.black.fullName(context))}', ); } } catch (e) { debugPrint(e.toString()); if (context.mounted) { - showPlatformSnackbar( - context, - 'Failed to get GIF', - type: SnackBarType.error, - ); + showPlatformSnackbar(context, 'Failed to get GIF', type: SnackBarType.error); } } }, @@ -201,11 +186,7 @@ class GameShareBottomSheet extends ConsumerWidget { try { final image = await ref .read(gameShareServiceProvider) - .screenshotPosition( - orientation, - currentGamePosition.fen, - lastMove, - ); + .screenshotPosition(orientation, currentGamePosition.fen, lastMove); if (context.mounted) { launchShareDialog( context, @@ -217,11 +198,7 @@ class GameShareBottomSheet extends ConsumerWidget { } } catch (e) { if (context.mounted) { - showPlatformSnackbar( - context, - 'Failed to get GIF', - type: SnackBarType.error, - ); + showPlatformSnackbar(context, 'Failed to get GIF', type: SnackBarType.error); } } }, @@ -240,22 +217,13 @@ class GameShareBottomSheet extends ConsumerWidget { child: Text('PGN: ${context.l10n.downloadAnnotated}'), onPressed: () async { try { - final pgn = await ref - .read(gameShareServiceProvider) - .annotatedPgn(game.id); + final pgn = await ref.read(gameShareServiceProvider).annotatedPgn(game.id); if (context.mounted) { - launchShareDialog( - context, - text: pgn, - ); + launchShareDialog(context, text: pgn); } } catch (e) { if (context.mounted) { - showPlatformSnackbar( - context, - 'Failed to get PGN', - type: SnackBarType.error, - ); + showPlatformSnackbar(context, 'Failed to get PGN', type: SnackBarType.error); } } }, @@ -275,21 +243,13 @@ class GameShareBottomSheet extends ConsumerWidget { child: Text('PGN: ${context.l10n.downloadRaw}'), onPressed: () async { try { - final pgn = - await ref.read(gameShareServiceProvider).rawPgn(game.id); + final pgn = await ref.read(gameShareServiceProvider).rawPgn(game.id); if (context.mounted) { - launchShareDialog( - context, - text: pgn, - ); + launchShareDialog(context, text: pgn); } } catch (e) { if (context.mounted) { - showPlatformSnackbar( - context, - 'Failed to get PGN', - type: SnackBarType.error, - ); + showPlatformSnackbar(context, 'Failed to get PGN', type: SnackBarType.error); } } }, @@ -308,15 +268,11 @@ class _LobbyGameTitle extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final mode = - seek.rated ? ' • ${context.l10n.rated}' : ' • ${context.l10n.casual}'; + final mode = seek.rated ? ' • ${context.l10n.rated}' : ' • ${context.l10n.casual}'; return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - seek.perf.icon, - color: DefaultTextStyle.of(context).style.color, - ), + Icon(seek.perf.icon, color: DefaultTextStyle.of(context).style.color), const SizedBox(width: 4.0), Text('${seek.timeIncrement?.display}$mode'), ], @@ -325,41 +281,29 @@ class _LobbyGameTitle extends ConsumerWidget { } class _ChallengeGameTitle extends ConsumerWidget { - const _ChallengeGameTitle({ - required this.challenge, - }); + const _ChallengeGameTitle({required this.challenge}); final ChallengeRequest challenge; @override Widget build(BuildContext context, WidgetRef ref) { - final mode = challenge.rated - ? ' • ${context.l10n.rated}' - : ' • ${context.l10n.casual}'; + final mode = challenge.rated ? ' • ${context.l10n.rated}' : ' • ${context.l10n.casual}'; return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - challenge.perf.icon, - color: DefaultTextStyle.of(context).style.color, - ), + Icon(challenge.perf.icon, color: DefaultTextStyle.of(context).style.color), const SizedBox(width: 4.0), if (challenge.timeIncrement != null) Text('${challenge.timeIncrement?.display}$mode') else if (challenge.days != null) - Text( - '${context.l10n.nbDays(challenge.days!)}$mode', - ), + Text('${context.l10n.nbDays(challenge.days!)}$mode'), ], ); } } class _StandaloneGameTitle extends ConsumerWidget { - const _StandaloneGameTitle({ - required this.id, - this.lastMoveAt, - }); + const _StandaloneGameTitle({required this.id, this.lastMoveAt}); final GameFullId id; @@ -370,21 +314,14 @@ class _StandaloneGameTitle extends ConsumerWidget { final metaAsync = ref.watch(gameMetaProvider(id)); return metaAsync.maybeWhen( data: (meta) { - final mode = meta.rated - ? ' • ${context.l10n.rated}' - : ' • ${context.l10n.casual}'; + final mode = meta.rated ? ' • ${context.l10n.rated}' : ' • ${context.l10n.casual}'; - final info = lastMoveAt != null - ? ' • ${_gameTitledateFormat.format(lastMoveAt!)}' - : mode; + final info = lastMoveAt != null ? ' • ${_gameTitledateFormat.format(lastMoveAt!)}' : mode; return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - meta.perf.icon, - color: DefaultTextStyle.of(context).style.color, - ), + Icon(meta.perf.icon, color: DefaultTextStyle.of(context).style.color), const SizedBox(width: 4.0), if (meta.clock != null) Expanded( @@ -404,11 +341,7 @@ class _StandaloneGameTitle extends ConsumerWidget { ) else Expanded( - child: AutoSizeText( - '${meta.perf.title}$info', - maxLines: 1, - minFontSize: 14.0, - ), + child: AutoSizeText('${meta.perf.title}$info', maxLines: 1, minFontSize: 14.0), ), ], ); diff --git a/lib/src/view/game/game_list_tile.dart b/lib/src/view/game/game_list_tile.dart index d0f9d2de20..9aa7014cf9 100644 --- a/lib/src/view/game/game_list_tile.dart +++ b/lib/src/view/game/game_list_tile.dart @@ -61,26 +61,26 @@ class GameListTile extends StatelessWidget { isDismissible: true, isScrollControlled: true, showDragHandle: true, - builder: (context) => _ContextMenu( - game: game, - mySide: mySide, - oppponentTitle: opponentTitle, - icon: icon, - subtitle: subtitle, - trailing: trailing, - ), + builder: + (context) => _ContextMenu( + game: game, + mySide: mySide, + oppponentTitle: opponentTitle, + icon: icon, + subtitle: subtitle, + trailing: trailing, + ), ); }, leading: icon != null ? Icon(icon) : null, title: opponentTitle, - subtitle: subtitle != null - ? DefaultTextStyle.merge( - child: subtitle!, - style: TextStyle( - color: textShade(context, Styles.subtitleOpacity), - ), - ) - : null, + subtitle: + subtitle != null + ? DefaultTextStyle.merge( + child: subtitle!, + style: TextStyle(color: textShade(context, Styles.subtitleOpacity)), + ) + : null, trailing: trailing, padding: padding, ); @@ -114,25 +114,18 @@ class _ContextMenu extends ConsumerWidget { return BottomSheetScrollableContainer( children: [ Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0).add( - const EdgeInsets.only(bottom: 8.0), - ), + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + ).add(const EdgeInsets.only(bottom: 8.0)), child: Text( - context.l10n.resVsX( - game.white.fullName(context), - game.black.fullName(context), - ), - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - letterSpacing: -0.5, - ), + context.l10n.resVsX(game.white.fullName(context), game.black.fullName(context)), + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, letterSpacing: -0.5), ), ), Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0).add( - const EdgeInsets.only(bottom: 8.0), - ), + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + ).add(const EdgeInsets.only(bottom: 8.0)), child: LayoutBuilder( builder: (context, constraints) { return IntrinsicHeight( @@ -141,8 +134,7 @@ class _ContextMenu extends ConsumerWidget { children: [ if (game.lastFen != null) BoardThumbnail( - size: constraints.maxWidth - - (constraints.maxWidth / 1.618), + size: constraints.maxWidth - (constraints.maxWidth / 1.618), fen: game.lastFen!, orientation: mySide, lastMove: game.lastMove, @@ -160,17 +152,12 @@ class _ContextMenu extends ConsumerWidget { children: [ Text( '${game.clockDisplay} • ${game.rated ? context.l10n.rated : context.l10n.casual}', - style: const TextStyle( - fontWeight: FontWeight.w500, - ), + style: const TextStyle(fontWeight: FontWeight.w500), ), Text( _dateFormatter.format(game.lastMoveAt), style: TextStyle( - color: textShade( - context, - Styles.subtitleOpacity, - ), + color: textShade(context, Styles.subtitleOpacity), fontSize: 12, ), ), @@ -189,9 +176,10 @@ class _ContextMenu extends ConsumerWidget { winner: game.winner, ), style: TextStyle( - color: game.winner == null - ? customColors?.brag - : game.winner == mySide + color: + game.winner == null + ? customColors?.brag + : game.winner == mySide ? customColors?.good : customColors?.error, ), @@ -201,10 +189,7 @@ class _ContextMenu extends ConsumerWidget { game.opening!.name, maxLines: 2, style: TextStyle( - color: textShade( - context, - Styles.subtitleOpacity, - ), + color: textShade(context, Styles.subtitleOpacity), fontSize: 12, ), overflow: TextOverflow.ellipsis, @@ -221,33 +206,29 @@ class _ContextMenu extends ConsumerWidget { ), BottomSheetContextMenuAction( icon: Icons.biotech, - onPressed: game.variant.isReadSupported - ? () { - pushPlatformRoute( - context, - builder: (context) => AnalysisScreen( - options: AnalysisOptions( - orientation: orientation, - gameId: game.id, - ), - ), - ); - } - : () { - showPlatformSnackbar( - context, - 'This variant is not supported yet.', - type: SnackBarType.info, - ); - }, + onPressed: + game.variant.isReadSupported + ? () { + pushPlatformRoute( + context, + builder: + (context) => AnalysisScreen( + options: AnalysisOptions(orientation: orientation, gameId: game.id), + ), + ); + } + : () { + showPlatformSnackbar( + context, + 'This variant is not supported yet.', + type: SnackBarType.info, + ); + }, child: Text(context.l10n.gameAnalysis), ), BottomSheetContextMenuAction( onPressed: () { - launchShareDialog( - context, - uri: lichessUri('/${game.id}'), - ); + launchShareDialog(context, uri: lichessUri('/${game.id}')); }, icon: CupertinoIcons.link, closeOnPressed: false, @@ -272,20 +253,14 @@ class _ContextMenu extends ConsumerWidget { launchShareDialog( context, files: [gif], - subject: '${game.perf.title} • ${context.l10n.resVsX( - game.white.fullName(context), - game.black.fullName(context), - )}', + subject: + '${game.perf.title} • ${context.l10n.resVsX(game.white.fullName(context), game.black.fullName(context))}', ); } } catch (e) { debugPrint(e.toString()); if (context.mounted) { - showPlatformSnackbar( - context, - 'Failed to get GIF', - type: SnackBarType.error, - ); + showPlatformSnackbar(context, 'Failed to get GIF', type: SnackBarType.error); } } }, @@ -307,11 +282,7 @@ class _ContextMenu extends ConsumerWidget { try { final image = await ref .read(gameShareServiceProvider) - .screenshotPosition( - orientation, - game.lastFen!, - game.lastMove, - ); + .screenshotPosition(orientation, game.lastFen!, game.lastMove); if (context.mounted) { launchShareDialog( context, @@ -323,11 +294,7 @@ class _ContextMenu extends ConsumerWidget { } } catch (e) { if (context.mounted) { - showPlatformSnackbar( - context, - 'Failed to get GIF', - type: SnackBarType.error, - ); + showPlatformSnackbar(context, 'Failed to get GIF', type: SnackBarType.error); } } }, @@ -346,22 +313,13 @@ class _ContextMenu extends ConsumerWidget { child: Text('PGN: ${context.l10n.downloadAnnotated}'), onPressed: () async { try { - final pgn = await ref - .read(gameShareServiceProvider) - .annotatedPgn(game.id); + final pgn = await ref.read(gameShareServiceProvider).annotatedPgn(game.id); if (context.mounted) { - launchShareDialog( - context, - text: pgn, - ); + launchShareDialog(context, text: pgn); } } catch (e) { if (context.mounted) { - showPlatformSnackbar( - context, - 'Failed to get PGN', - type: SnackBarType.error, - ); + showPlatformSnackbar(context, 'Failed to get PGN', type: SnackBarType.error); } } }, @@ -381,21 +339,13 @@ class _ContextMenu extends ConsumerWidget { child: Text('PGN: ${context.l10n.downloadRaw}'), onPressed: () async { try { - final pgn = - await ref.read(gameShareServiceProvider).rawPgn(game.id); + final pgn = await ref.read(gameShareServiceProvider).rawPgn(game.id); if (context.mounted) { - launchShareDialog( - context, - text: pgn, - ); + launchShareDialog(context, text: pgn); } } catch (e) { if (context.mounted) { - showPlatformSnackbar( - context, - 'Failed to get PGN', - type: SnackBarType.error, - ); + showPlatformSnackbar(context, 'Failed to get PGN', type: SnackBarType.error); } } }, @@ -409,11 +359,7 @@ class _ContextMenu extends ConsumerWidget { /// A list tile that shows extended game info including a result icon and analysis icon. class ExtendedGameListTile extends StatelessWidget { - const ExtendedGameListTile({ - required this.item, - this.userId, - this.padding, - }); + const ExtendedGameListTile({required this.item, this.userId, this.padding}); final LightArchivedGameWithPov item; final UserId? userId; @@ -427,27 +373,14 @@ class ExtendedGameListTile extends StatelessWidget { final opponent = youAre == Side.white ? game.black : game.white; Widget getResultIcon(LightArchivedGame game, Side mySide) { - if (game.status == GameStatus.aborted || - game.status == GameStatus.noStart) { - return const Icon( - CupertinoIcons.xmark_square_fill, - color: LichessColors.grey, - ); + if (game.status == GameStatus.aborted || game.status == GameStatus.noStart) { + return const Icon(CupertinoIcons.xmark_square_fill, color: LichessColors.grey); } else { return game.winner == null - ? Icon( - CupertinoIcons.equal_square_fill, - color: context.lichessColors.brag, - ) + ? Icon(CupertinoIcons.equal_square_fill, color: context.lichessColors.brag) : game.winner == mySide - ? Icon( - CupertinoIcons.plus_square_fill, - color: context.lichessColors.good, - ) - : Icon( - CupertinoIcons.minus_square_fill, - color: context.lichessColors.error, - ); + ? Icon(CupertinoIcons.plus_square_fill, color: context.lichessColors.good) + : Icon(CupertinoIcons.minus_square_fill, color: context.lichessColors.error); } } @@ -455,32 +388,32 @@ class ExtendedGameListTile extends StatelessWidget { game: game, mySide: youAre, padding: padding, - onTap: game.variant.isReadSupported - ? () { - pushPlatformRoute( - context, - rootNavigator: true, - builder: (context) => game.fullId != null - ? GameScreen( - initialGameId: game.fullId, - loadingFen: game.lastFen, - loadingLastMove: game.lastMove, - loadingOrientation: youAre, - lastMoveAt: game.lastMoveAt, - ) - : ArchivedGameScreen( - gameData: game, - orientation: youAre, - ), - ); - } - : () { - showPlatformSnackbar( - context, - 'This variant is not supported yet.', - type: SnackBarType.info, - ); - }, + onTap: + game.variant.isReadSupported + ? () { + pushPlatformRoute( + context, + rootNavigator: true, + builder: + (context) => + game.fullId != null + ? GameScreen( + initialGameId: game.fullId, + loadingFen: game.lastFen, + loadingLastMove: game.lastMove, + loadingOrientation: youAre, + lastMoveAt: game.lastMoveAt, + ) + : ArchivedGameScreen(gameData: game, orientation: youAre), + ); + } + : () { + showPlatformSnackbar( + context, + 'This variant is not supported yet.', + type: SnackBarType.info, + ); + }, icon: game.perf.icon, opponentTitle: UserFullNameWidget.player( user: opponent.user, @@ -492,10 +425,7 @@ class ExtendedGameListTile extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ if (me.analysis != null) ...[ - Icon( - CupertinoIcons.chart_bar_alt_fill, - color: textShade(context, 0.5), - ), + Icon(CupertinoIcons.chart_bar_alt_fill, color: textShade(context, 0.5)), const SizedBox(width: 5), ], getResultIcon(game, youAre), diff --git a/lib/src/view/game/game_loading_board.dart b/lib/src/view/game/game_loading_board.dart index bf0a216b2b..0f7a3252c1 100644 --- a/lib/src/view/game/game_loading_board.dart +++ b/lib/src/view/game/game_loading_board.dart @@ -46,10 +46,7 @@ class LobbyScreenLoadingContent extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ - Icon( - seek.perf.icon, - color: DefaultTextStyle.of(context).style.color, - ), + Icon(seek.perf.icon, color: DefaultTextStyle.of(context).style.color), const SizedBox(width: 8.0), Text( seek.timeIncrement?.display ?? @@ -171,12 +168,7 @@ class ChallengeLoadingContent extends StatelessWidget { } class StandaloneGameLoadingBoard extends StatelessWidget { - const StandaloneGameLoadingBoard({ - this.fen, - this.lastMove, - this.orientation, - super.key, - }); + const StandaloneGameLoadingBoard({this.fen, this.lastMove, this.orientation, super.key}); final String? fen; final Side? orientation; @@ -234,10 +226,7 @@ class LoadGameError extends StatelessWidget { /// A board that shows a message that a challenge has been declined. class ChallengeDeclinedBoard extends StatelessWidget { - const ChallengeDeclinedBoard({ - required this.declineReason, - required this.challenge, - }); + const ChallengeDeclinedBoard({required this.declineReason, required this.challenge}); final String declineReason; final Challenge challenge; @@ -272,10 +261,7 @@ class ChallengeDeclinedBoard extends StatelessWidget { ), const SizedBox(height: 8.0), Divider(height: 26.0, thickness: 0.0, color: textColor), - Text( - declineReason, - style: const TextStyle(fontStyle: FontStyle.italic), - ), + Text(declineReason, style: const TextStyle(fontStyle: FontStyle.italic)), Divider(height: 26.0, thickness: 0.0, color: textColor), if (challenge.destUser != null) Align( @@ -284,9 +270,7 @@ class ChallengeDeclinedBoard extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ const Text(' — '), - UserFullNameWidget( - user: challenge.destUser?.user, - ), + UserFullNameWidget(user: challenge.destUser?.user), if (challenge.destUser?.lagRating != null) ...[ const SizedBox(width: 6.0), LagIndicator( @@ -329,13 +313,9 @@ class _LobbyNumbers extends ConsumerWidget { if (lobbyNumbers == null) { return Column( children: [ - Text( - context.l10n.nbPlayers(0).replaceAll('0', '...'), - ), + Text(context.l10n.nbPlayers(0).replaceAll('0', '...')), const SizedBox(height: 8.0), - Text( - context.l10n.nbGamesInPlay(0).replaceAll('0', '...'), - ), + Text(context.l10n.nbGamesInPlay(0).replaceAll('0', '...')), ], ); } else { diff --git a/lib/src/view/game/game_player.dart b/lib/src/view/game/game_player.dart index b46198dea2..e73dc5b354 100644 --- a/lib/src/view/game/game_player.dart +++ b/lib/src/view/game/game_player.dart @@ -58,8 +58,7 @@ class GamePlayer extends StatelessWidget { @override Widget build(BuildContext context) { final remaingHeight = estimateRemainingHeightLeftBoard(context); - final playerFontSize = - remaingHeight <= kSmallRemainingHeightLeftBoardThreshold ? 14.0 : 16.0; + final playerFontSize = remaingHeight <= kSmallRemainingHeightLeftBoardThreshold ? 14.0 : 16.0; final playerWidget = Column( mainAxisAlignment: MainAxisAlignment.center, @@ -67,9 +66,10 @@ class GamePlayer extends StatelessWidget { children: [ if (!zenMode) Row( - mainAxisAlignment: clockPosition == ClockPosition.right - ? MainAxisAlignment.start - : MainAxisAlignment.end, + mainAxisAlignment: + clockPosition == ClockPosition.right + ? MainAxisAlignment.start + : MainAxisAlignment.end, children: [ if (player.user != null) ...[ Icon( @@ -92,11 +92,11 @@ class GamePlayer extends StatelessWidget { player.user!.title!, style: TextStyle( fontSize: playerFontSize, - fontWeight: - player.user?.title == 'BOT' ? null : FontWeight.bold, - color: player.user?.title == 'BOT' - ? context.lichessColors.fancy - : context.lichessColors.brag, + fontWeight: player.user?.title == 'BOT' ? null : FontWeight.bold, + color: + player.user?.title == 'BOT' + ? context.lichessColors.fancy + : context.lichessColors.brag, ), ), const SizedBox(width: 5), @@ -105,10 +105,7 @@ class GamePlayer extends StatelessWidget { child: Text( player.displayName(context), overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: playerFontSize, - fontWeight: FontWeight.w600, - ), + style: TextStyle(fontSize: playerFontSize, fontWeight: FontWeight.w600), ), ), if (player.user?.flair != null) ...[ @@ -124,26 +121,22 @@ class GamePlayer extends StatelessWidget { RatingPrefAware( child: Text.rich( TextSpan( - text: - ' ${player.rating}${player.provisional == true ? '?' : ''}', + text: ' ${player.rating}${player.provisional == true ? '?' : ''}', children: [ if (player.ratingDiff != null) TextSpan( - text: - ' ${player.ratingDiff! > 0 ? '+' : ''}${player.ratingDiff}', + text: ' ${player.ratingDiff! > 0 ? '+' : ''}${player.ratingDiff}', style: TextStyle( - color: player.ratingDiff! > 0 - ? context.lichessColors.good - : context.lichessColors.error, + color: + player.ratingDiff! > 0 + ? context.lichessColors.good + : context.lichessColors.error, ), ), ], ), overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 14, - color: textShade(context, 0.7), - ), + style: TextStyle(fontSize: 14, color: textShade(context, 0.7)), ), ), ], @@ -164,8 +157,7 @@ class GamePlayer extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ - if (clock != null && clockPosition == ClockPosition.left) - Flexible(flex: 3, child: clock!), + if (clock != null && clockPosition == ClockPosition.left) Flexible(flex: 3, child: clock!), if (mePlaying && confirmMoveCallbacks != null) Expanded( flex: 7, @@ -182,38 +174,35 @@ class GamePlayer extends StatelessWidget { flex: 7, child: Padding( padding: const EdgeInsets.only(right: 16.0), - child: shouldLinkToUserProfile - ? GestureDetector( - onTap: player.user != null - ? () { - pushPlatformRoute( - context, - builder: (context) => mePlaying - ? const ProfileScreen() - : UserScreen( - user: player.user!, - ), - ); - } - : null, - child: playerWidget, - ) - : playerWidget, + child: + shouldLinkToUserProfile + ? GestureDetector( + onTap: + player.user != null + ? () { + pushPlatformRoute( + context, + builder: + (context) => + mePlaying + ? const ProfileScreen() + : UserScreen(user: player.user!), + ); + } + : null, + child: playerWidget, + ) + : playerWidget, ), ), - if (clock != null && clockPosition == ClockPosition.right) - Flexible(flex: 3, child: clock!), + if (clock != null && clockPosition == ClockPosition.right) Flexible(flex: 3, child: clock!), ], ); } } class ConfirmMove extends StatelessWidget { - const ConfirmMove({ - required this.onConfirm, - required this.onCancel, - super.key, - }); + const ConfirmMove({required this.onConfirm, required this.onCancel, super.key}); final VoidCallback onConfirm; final VoidCallback onCancel; @@ -253,11 +242,7 @@ class ConfirmMove extends StatelessWidget { } class MoveExpiration extends ConsumerStatefulWidget { - const MoveExpiration({ - required this.timeToMove, - required this.mePlaying, - super.key, - }); + const MoveExpiration({required this.timeToMove, required this.mePlaying, super.key}); final Duration timeToMove; final bool mePlaying; @@ -318,13 +303,9 @@ class _MoveExpirationState extends ConsumerState { return secs <= 20 ? Text( - context.l10n.nbSecondsToPlayTheFirstMove(secs), - style: TextStyle( - color: widget.mePlaying && emerg - ? context.lichessColors.error - : null, - ), - ) + context.l10n.nbSecondsToPlayTheFirstMove(secs), + style: TextStyle(color: widget.mePlaying && emerg ? context.lichessColors.error : null), + ) : const Text(''); } } @@ -347,24 +328,17 @@ class MaterialDifferenceDisplay extends StatelessWidget { return materialDifferenceFormat?.visible ?? true ? Row( - children: [ - for (final role in Role.values) - for (int i = 0; i < piecesToRender[role]!; i++) - Icon( - _iconByRole[role], - size: 13, - color: Colors.grey, - ), - const SizedBox(width: 3), - Text( - style: const TextStyle( - fontSize: 13, - color: Colors.grey, - ), - materialDiff.score > 0 ? '+${materialDiff.score}' : '', - ), - ], - ) + children: [ + for (final role in Role.values) + for (int i = 0; i < piecesToRender[role]!; i++) + Icon(_iconByRole[role], size: 13, color: Colors.grey), + const SizedBox(width: 3), + Text( + style: const TextStyle(fontSize: 13, color: Colors.grey), + materialDiff.score > 0 ? '+${materialDiff.score}' : '', + ), + ], + ) : const SizedBox.shrink(); } } diff --git a/lib/src/view/game/game_result_dialog.dart b/lib/src/view/game/game_result_dialog.dart index a4e6522d53..6c456921ba 100644 --- a/lib/src/view/game/game_result_dialog.dart +++ b/lib/src/view/game/game_result_dialog.dart @@ -23,11 +23,7 @@ import 'package:lichess_mobile/src/widgets/pgn.dart'; import 'status_l10n.dart'; class GameResultDialog extends ConsumerStatefulWidget { - const GameResultDialog({ - required this.id, - required this.onNewOpponentCallback, - super.key, - }); + const GameResultDialog({required this.id, required this.onNewOpponentCallback, super.key}); final GameFullId id; @@ -46,19 +42,18 @@ Widget _adaptiveDialog(BuildContext context, Widget content) { ); final screenWidth = MediaQuery.of(context).size.width; - final paddedContent = Padding( - padding: const EdgeInsets.all(16.0), - child: content, - ); + final paddedContent = Padding(padding: const EdgeInsets.all(16.0), child: content); return Dialog( - backgroundColor: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoDynamicColor.resolve(dialogColor, context) - : null, + backgroundColor: + Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoDynamicColor.resolve(dialogColor, context) + : null, child: SizedBox( width: min(screenWidth, kMaterialPopupMenuMaxWidth), - child: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoPopupSurface(child: paddedContent) - : paddedContent, + child: + Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoPopupSurface(child: paddedContent) + : paddedContent, ), ); } @@ -108,10 +103,7 @@ class _GameEndDialogState extends ConsumerState { children: [ const Padding( padding: EdgeInsets.only(bottom: 15.0), - child: Text( - 'Your opponent has offered a rematch', - textAlign: TextAlign.center, - ), + child: Text('Your opponent has offered a rematch', textAlign: TextAlign.center), ), Padding( padding: const EdgeInsets.only(bottom: 15.0), @@ -122,9 +114,7 @@ class _GameEndDialogState extends ConsumerState { semanticsLabel: context.l10n.rematch, child: const Text('Accept rematch'), onPressed: () { - ref - .read(ctrlProvider.notifier) - .proposeOrAcceptRematch(); + ref.read(ctrlProvider.notifier).proposeOrAcceptRematch(); }, ), SecondaryButton( @@ -139,9 +129,10 @@ class _GameEndDialogState extends ConsumerState { ), ], ), - crossFadeState: gameState.game.opponent?.offeringRematch ?? false - ? CrossFadeState.showSecond - : CrossFadeState.showFirst, + crossFadeState: + gameState.game.opponent?.offeringRematch ?? false + ? CrossFadeState.showSecond + : CrossFadeState.showFirst, ), if (gameState.game.me?.offeringRematch == true) SecondaryButton( @@ -149,40 +140,32 @@ class _GameEndDialogState extends ConsumerState { onPressed: () { ref.read(ctrlProvider.notifier).declineRematch(); }, - child: Text( - context.l10n.cancelRematchOffer, - textAlign: TextAlign.center, - ), + child: Text(context.l10n.cancelRematchOffer, textAlign: TextAlign.center), ) else if (gameState.canOfferRematch) SecondaryButton( semanticsLabel: context.l10n.rematch, - onPressed: _activateButtons && - gameState.game.opponent?.onGame == true && - gameState.game.opponent?.offeringRematch != true - ? () { - ref.read(ctrlProvider.notifier).proposeOrAcceptRematch(); - } - : null, - child: Text( - context.l10n.rematch, - textAlign: TextAlign.center, - ), + onPressed: + _activateButtons && + gameState.game.opponent?.onGame == true && + gameState.game.opponent?.offeringRematch != true + ? () { + ref.read(ctrlProvider.notifier).proposeOrAcceptRematch(); + } + : null, + child: Text(context.l10n.rematch, textAlign: TextAlign.center), ), if (gameState.canGetNewOpponent) SecondaryButton( semanticsLabel: context.l10n.newOpponent, - onPressed: _activateButtons - ? () { - Navigator.of(context) - .popUntil((route) => route is! PopupRoute); - widget.onNewOpponentCallback(gameState.game); - } - : null, - child: Text( - context.l10n.newOpponent, - textAlign: TextAlign.center, - ), + onPressed: + _activateButtons + ? () { + Navigator.of(context).popUntil((route) => route is! PopupRoute); + widget.onNewOpponentCallback(gameState.game); + } + : null, + child: Text(context.l10n.newOpponent, textAlign: TextAlign.center), ), if (gameState.game.userAnalysable) SecondaryButton( @@ -190,15 +173,10 @@ class _GameEndDialogState extends ConsumerState { onPressed: () { pushPlatformRoute( context, - builder: (_) => AnalysisScreen( - options: gameState.analysisOptions, - ), + builder: (_) => AnalysisScreen(options: gameState.analysisOptions), ); }, - child: Text( - context.l10n.analysis, - textAlign: TextAlign.center, - ), + child: Text(context.l10n.analysis, textAlign: TextAlign.center), ), ], ); @@ -217,11 +195,7 @@ class ArchivedGameResultDialog extends StatelessWidget { final content = Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - GameResult(game: game), - const SizedBox(height: 16.0), - PlayerSummary(game: game), - ], + children: [GameResult(game: game), const SizedBox(height: 16.0), PlayerSummary(game: game)], ); return _adaptiveDialog(context, content); @@ -229,11 +203,7 @@ class ArchivedGameResultDialog extends StatelessWidget { } class OverTheBoardGameResultDialog extends StatelessWidget { - const OverTheBoardGameResultDialog({ - super.key, - required this.game, - required this.onRematch, - }); + const OverTheBoardGameResultDialog({super.key, required this.game, required this.onRematch}); final OverTheBoardGame game; @@ -249,32 +219,27 @@ class OverTheBoardGameResultDialog extends StatelessWidget { SecondaryButton( semanticsLabel: context.l10n.rematch, onPressed: onRematch, - child: Text( - context.l10n.rematch, - textAlign: TextAlign.center, - ), + child: Text(context.l10n.rematch, textAlign: TextAlign.center), ), SecondaryButton( semanticsLabel: context.l10n.analysis, onPressed: () { pushPlatformRoute( context, - builder: (_) => AnalysisScreen( - options: AnalysisOptions( - orientation: Side.white, - standalone: ( - pgn: game.makePgn(), - isComputerAnalysisAllowed: true, - variant: game.meta.variant, + builder: + (_) => AnalysisScreen( + options: AnalysisOptions( + orientation: Side.white, + standalone: ( + pgn: game.makePgn(), + isComputerAnalysisAllowed: true, + variant: game.meta.variant, + ), + ), ), - ), - ), ); }, - child: Text( - context.l10n.analysis, - textAlign: TextAlign.center, - ), + child: Text(context.l10n.analysis, textAlign: TextAlign.center), ), ], ); @@ -298,22 +263,14 @@ class PlayerSummary extends ConsumerWidget { return const SizedBox.shrink(); } - Widget makeStatCol( - int value, - String Function(int count) labelFn, - Color? color, - ) { + Widget makeStatCol(int value, String Function(int count) labelFn, Color? color) { return Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( value.toString(), - style: TextStyle( - fontSize: 18.0, - color: color, - fontWeight: FontWeight.bold, - ), + style: TextStyle(fontSize: 18.0, color: color, fontWeight: FontWeight.bold), ), const SizedBox(height: 4.0), FittedBox( @@ -358,9 +315,10 @@ class GameResult extends StatelessWidget { @override Widget build(BuildContext context) { - final showWinner = game.winner != null - ? ' • ${game.winner == Side.white ? context.l10n.whiteIsVictorious : context.l10n.blackIsVictorious}' - : ''; + final showWinner = + game.winner != null + ? ' • ${game.winner == Side.white ? context.l10n.whiteIsVictorious : context.l10n.blackIsVictorious}' + : ''; return Column( mainAxisSize: MainAxisSize.min, @@ -370,27 +328,15 @@ class GameResult extends StatelessWidget { game.winner == null ? '½-½' : game.winner == Side.white - ? '1-0' - : '0-1', - style: const TextStyle( - fontSize: 18.0, - fontWeight: FontWeight.bold, - ), + ? '1-0' + : '0-1', + style: const TextStyle(fontSize: 18.0, fontWeight: FontWeight.bold), textAlign: TextAlign.center, ), const SizedBox(height: 6.0), Text( - '${gameStatusL10n( - context, - variant: game.meta.variant, - status: game.status, - lastPosition: game.lastPosition, - winner: game.winner, - isThreefoldRepetition: game.isThreefoldRepetition, - )}$showWinner', - style: const TextStyle( - fontStyle: FontStyle.italic, - ), + '${gameStatusL10n(context, variant: game.meta.variant, status: game.status, lastPosition: game.lastPosition, winner: game.winner, isThreefoldRepetition: game.isThreefoldRepetition)}$showWinner', + style: const TextStyle(fontStyle: FontStyle.italic), textAlign: TextAlign.center, ), ], diff --git a/lib/src/view/game/game_screen.dart b/lib/src/view/game/game_screen.dart index 7e394c3587..7b806549f5 100644 --- a/lib/src/view/game/game_screen.dart +++ b/lib/src/view/game/game_screen.dart @@ -39,9 +39,9 @@ class GameScreen extends ConsumerStatefulWidget { this.lastMoveAt, super.key, }) : assert( - initialGameId != null || seek != null || challenge != null, - 'Either a seek, a challenge or an initial game id must be provided.', - ); + initialGameId != null || seek != null || challenge != null, + 'Either a seek, a challenge or an initial game id must be provided.', + ); // tweak final GameSeek? seek; @@ -95,9 +95,7 @@ class _GameScreenState extends ConsumerState with RouteAware { @override void didPop() { - if (mounted && - (widget.source == _GameSource.lobby || - widget.source == _GameSource.challenge)) { + if (mounted && (widget.source == _GameSource.lobby || widget.source == _GameSource.challenge)) { ref.invalidate(myRecentGamesProvider); ref.invalidate(accountProvider); } @@ -106,105 +104,99 @@ class _GameScreenState extends ConsumerState with RouteAware { @override Widget build(BuildContext context) { - final provider = currentGameProvider( - widget.seek, - widget.challenge, - widget.initialGameId, - ); - - return ref.watch(provider).when( - data: (data) { - final ( - gameFullId: gameId, - challenge: challenge, - declineReason: declineReason - ) = data; - final body = gameId != null - ? GameBody( - id: gameId, - loadingBoardWidget: StandaloneGameLoadingBoard( - fen: widget.loadingFen, - lastMove: widget.loadingLastMove, - orientation: widget.loadingOrientation, - ), - whiteClockKey: _whiteClockKey, - blackClockKey: _blackClockKey, - boardKey: _boardKey, - onLoadGameCallback: (id) { - ref.read(provider.notifier).loadGame(id); - }, - onNewOpponentCallback: (game) { - if (widget.source == _GameSource.lobby) { - ref.read(provider.notifier).newOpponent(); - } else { - final savedSetup = ref.read(gameSetupPreferencesProvider); - pushReplacementPlatformRoute( - context, - rootNavigator: true, - builder: (_) => GameScreen( - seek: GameSeek.newOpponentFromGame(game, savedSetup), + final provider = currentGameProvider(widget.seek, widget.challenge, widget.initialGameId); + + return ref + .watch(provider) + .when( + data: (data) { + final (gameFullId: gameId, challenge: challenge, declineReason: declineReason) = data; + final body = + gameId != null + ? GameBody( + id: gameId, + loadingBoardWidget: StandaloneGameLoadingBoard( + fen: widget.loadingFen, + lastMove: widget.loadingLastMove, + orientation: widget.loadingOrientation, ), + whiteClockKey: _whiteClockKey, + blackClockKey: _blackClockKey, + boardKey: _boardKey, + onLoadGameCallback: (id) { + ref.read(provider.notifier).loadGame(id); + }, + onNewOpponentCallback: (game) { + if (widget.source == _GameSource.lobby) { + ref.read(provider.notifier).newOpponent(); + } else { + final savedSetup = ref.read(gameSetupPreferencesProvider); + pushReplacementPlatformRoute( + context, + rootNavigator: true, + builder: + (_) => GameScreen( + seek: GameSeek.newOpponentFromGame(game, savedSetup), + ), + ); + } + }, + ) + : widget.challenge != null && challenge != null + ? ChallengeDeclinedBoard( + challenge: challenge, + declineReason: + declineReason != null + ? declineReason.label(context.l10n) + : ChallengeDeclineReason.generic.label(context.l10n), + ) + : const LoadGameError('Could not create the game.'); + return PlatformScaffold( + resizeToAvoidBottomInset: false, + appBar: GameAppBar(id: gameId, lastMoveAt: widget.lastMoveAt), + body: body, + ); + }, + loading: () { + final loadingBoard = + widget.seek != null + ? LobbyScreenLoadingContent( + widget.seek!, + () => ref.read(createGameServiceProvider).cancelSeek(), + ) + : widget.challenge != null + ? ChallengeLoadingContent( + widget.challenge!, + () => ref.read(createGameServiceProvider).cancelChallenge(), + ) + : const StandaloneGameLoadingBoard(); + + return PlatformScaffold( + resizeToAvoidBottomInset: false, + appBar: GameAppBar(seek: widget.seek, lastMoveAt: widget.lastMoveAt), + body: PopScope(canPop: false, child: loadingBoard), + ); + }, + error: (e, s) { + debugPrint('SEVERE: [GameScreen] could not create game; $e\n$s'); + + // lichess sends a 400 response if user has disallowed challenges + final message = + e is ServerException && e.statusCode == 400 + ? LoadGameError( + 'Could not create the game: ${e.jsonError?['error'] as String?}', + ) + : const LoadGameError( + 'Sorry, we could not create the game. Please try again later.', ); - } - }, - ) - : widget.challenge != null && challenge != null - ? ChallengeDeclinedBoard( - challenge: challenge, - declineReason: declineReason != null - ? declineReason.label(context.l10n) - : ChallengeDeclineReason.generic.label(context.l10n), - ) - : const LoadGameError('Could not create the game.'); - return PlatformScaffold( - resizeToAvoidBottomInset: false, - appBar: GameAppBar(id: gameId, lastMoveAt: widget.lastMoveAt), - body: body, - ); - }, - loading: () { - final loadingBoard = widget.seek != null - ? LobbyScreenLoadingContent( - widget.seek!, - () => ref.read(createGameServiceProvider).cancelSeek(), - ) - : widget.challenge != null - ? ChallengeLoadingContent( - widget.challenge!, - () => ref.read(createGameServiceProvider).cancelChallenge(), - ) - : const StandaloneGameLoadingBoard(); - - return PlatformScaffold( - resizeToAvoidBottomInset: false, - appBar: GameAppBar(seek: widget.seek, lastMoveAt: widget.lastMoveAt), - body: PopScope( - canPop: false, - child: loadingBoard, - ), - ); - }, - error: (e, s) { - debugPrint( - 'SEVERE: [GameScreen] could not create game; $e\n$s', - ); - - // lichess sends a 400 response if user has disallowed challenges - final message = e is ServerException && e.statusCode == 400 - ? LoadGameError( - 'Could not create the game: ${e.jsonError?['error'] as String?}', - ) - : const LoadGameError( - 'Sorry, we could not create the game. Please try again later.', - ); - final body = PopScope(child: message); + final body = PopScope(child: message); - return PlatformScaffold( - appBar: GameAppBar(seek: widget.seek, lastMoveAt: widget.lastMoveAt), - body: body, + return PlatformScaffold( + appBar: GameAppBar(seek: widget.seek, lastMoveAt: widget.lastMoveAt), + body: body, + ); + }, ); - }, - ); } } diff --git a/lib/src/view/game/game_screen_providers.dart b/lib/src/view/game/game_screen_providers.dart index 285750cd9f..8f138ae38c 100644 --- a/lib/src/view/game/game_screen_providers.dart +++ b/lib/src/view/game/game_screen_providers.dart @@ -17,11 +17,7 @@ part 'game_screen_providers.g.dart'; @riverpod class CurrentGame extends _$CurrentGame { @override - Future build( - GameSeek? seek, - ChallengeRequest? challenge, - GameFullId? gameId, - ) { + Future build(GameSeek? seek, ChallengeRequest? challenge, GameFullId? gameId) { assert( gameId != null || seek != null || challenge != null, 'Either a seek, challenge or a game id must be provided.', @@ -37,9 +33,7 @@ class CurrentGame extends _$CurrentGame { return service.newRealTimeChallenge(challenge); } - return Future.value( - (gameFullId: gameId!, challenge: null, declineReason: null), - ); + return Future.value((gameFullId: gameId!, challenge: null, declineReason: null)); } /// Search for a new opponent (lobby only). @@ -48,17 +42,16 @@ class CurrentGame extends _$CurrentGame { final service = ref.read(createGameServiceProvider); state = const AsyncValue.loading(); state = AsyncValue.data( - await service.newLobbyGame(seek!).then( - (id) => (gameFullId: id, challenge: null, declineReason: null), - ), + await service + .newLobbyGame(seek!) + .then((id) => (gameFullId: id, challenge: null, declineReason: null)), ); } } /// Load a game from its id. void loadGame(GameFullId id) { - state = - AsyncValue.data((gameFullId: id, challenge: null, declineReason: null)); + state = AsyncValue.data((gameFullId: id, challenge: null, declineReason: null)); } } @@ -77,45 +70,33 @@ class IsBoardTurned extends _$IsBoardTurned { @riverpod Future shouldPreventGoingBack(Ref ref, GameFullId gameId) { return ref.watch( - gameControllerProvider(gameId).selectAsync( - (state) => - state.game.meta.speed != Speed.correspondence && state.game.playable, - ), + gameControllerProvider( + gameId, + ).selectAsync((state) => state.game.meta.speed != Speed.correspondence && state.game.playable), ); } /// User game preferences, defined server-side. @riverpod -Future< - ({ - ServerGamePrefs? prefs, - bool shouldConfirmMove, - bool isZenModeEnabled, - bool canAutoQueen - })> userGamePrefs(Ref ref, GameFullId gameId) async { +Future<({ServerGamePrefs? prefs, bool shouldConfirmMove, bool isZenModeEnabled, bool canAutoQueen})> +userGamePrefs(Ref ref, GameFullId gameId) async { final prefs = await ref.watch( gameControllerProvider(gameId).selectAsync((state) => state.game.prefs), ); final shouldConfirmMove = await ref.watch( - gameControllerProvider(gameId).selectAsync( - (state) => state.shouldConfirmMove, - ), + gameControllerProvider(gameId).selectAsync((state) => state.shouldConfirmMove), ); final isZenModeEnabled = await ref.watch( - gameControllerProvider(gameId).selectAsync( - (state) => state.isZenModeEnabled, - ), + gameControllerProvider(gameId).selectAsync((state) => state.isZenModeEnabled), ); final canAutoQueen = await ref.watch( - gameControllerProvider(gameId).selectAsync( - (state) => state.canAutoQueen, - ), + gameControllerProvider(gameId).selectAsync((state) => state.canAutoQueen), ); return ( prefs: prefs, shouldConfirmMove: shouldConfirmMove, isZenModeEnabled: isZenModeEnabled, - canAutoQueen: canAutoQueen + canAutoQueen: canAutoQueen, ); } @@ -124,7 +105,5 @@ Future< /// This is data that won't change during the game. @riverpod Future gameMeta(Ref ref, GameFullId gameId) async { - return await ref.watch( - gameControllerProvider(gameId).selectAsync((state) => state.game.meta), - ); + return await ref.watch(gameControllerProvider(gameId).selectAsync((state) => state.game.meta)); } diff --git a/lib/src/view/game/game_settings.dart b/lib/src/view/game/game_settings.dart index b797510639..592709eb63 100644 --- a/lib/src/view/game/game_settings.dart +++ b/lib/src/view/game/game_settings.dart @@ -29,32 +29,22 @@ class GameSettings extends ConsumerWidget { return [ if (data.prefs?.submitMove == true) SwitchSettingTile( - title: Text( - context.l10n.preferencesMoveConfirmation, - ), + title: Text(context.l10n.preferencesMoveConfirmation), value: data.shouldConfirmMove, onChanged: (value) { - ref - .read(gameControllerProvider(id).notifier) - .toggleMoveConfirmation(); + ref.read(gameControllerProvider(id).notifier).toggleMoveConfirmation(); }, ), if (data.prefs?.autoQueen == AutoQueen.always) SwitchSettingTile( - title: Text( - context.l10n.preferencesPromoteToQueenAutomatically, - ), + title: Text(context.l10n.preferencesPromoteToQueenAutomatically), value: data.canAutoQueen, onChanged: (value) { - ref - .read(gameControllerProvider(id).notifier) - .toggleAutoQueen(); + ref.read(gameControllerProvider(id).notifier).toggleAutoQueen(); }, ), SwitchSettingTile( - title: Text( - context.l10n.preferencesZenMode, - ), + title: Text(context.l10n.preferencesZenMode), value: data.isZenModeEnabled, onChanged: (value) { ref.read(gameControllerProvider(id).notifier).toggleZenMode(); @@ -69,17 +59,11 @@ class GameSettings extends ConsumerWidget { title: const Text('Board settings'), trailing: const Icon(CupertinoIcons.chevron_right), onTap: () { - pushPlatformRoute( - context, - fullscreenDialog: true, - screen: const BoardSettingsScreen(), - ); + pushPlatformRoute(context, fullscreenDialog: true, screen: const BoardSettingsScreen()); }, ), SwitchSettingTile( - title: Text( - context.l10n.toggleTheChat, - ), + title: Text(context.l10n.toggleTheChat), value: gamePrefs.enableChat ?? false, onChanged: (value) { ref.read(gamePreferencesProvider.notifier).toggleChat(); diff --git a/lib/src/view/game/message_screen.dart b/lib/src/view/game/message_screen.dart index 00187cc040..2e21301209 100644 --- a/lib/src/view/game/message_screen.dart +++ b/lib/src/view/game/message_screen.dart @@ -19,11 +19,7 @@ class MessageScreen extends ConsumerStatefulWidget { final Widget title; final LightUser? me; - const MessageScreen({ - required this.id, - required this.title, - this.me, - }); + const MessageScreen({required this.id, required this.title, this.me}); @override ConsumerState createState() => _MessageScreenState(); @@ -54,10 +50,7 @@ class _MessageScreenState extends ConsumerState with RouteAware { @override Widget build(BuildContext context) { return PlatformScaffold( - appBar: PlatformAppBar( - title: widget.title, - centerTitle: true, - ), + appBar: PlatformAppBar(title: widget.title, centerTitle: true), body: _Body(me: widget.me, id: widget.id), ); } @@ -67,10 +60,7 @@ class _Body extends ConsumerWidget { final GameFullId id; final LightUser? me; - const _Body({ - required this.id, - required this.me, - }); + const _Body({required this.id, required this.me}); @override Widget build(BuildContext context, WidgetRef ref) { @@ -84,9 +74,8 @@ class _Body extends ConsumerWidget { onTap: () => FocusScope.of(context).unfocus(), child: chatStateAsync.when( data: (chatState) { - final selectedMessages = chatState.messages - .where((m) => !m.troll && !m.deleted && !isSpam(m)) - .toList(); + final selectedMessages = + chatState.messages.where((m) => !m.troll && !m.deleted && !isSpam(m)).toList(); final messagesCount = selectedMessages.length; return ListView.builder( // remove the automatic bottom padding of the ListView, which on iOS @@ -100,23 +89,13 @@ class _Body extends ConsumerWidget { return (message.username == 'lichess') ? _MessageAction(message: message.message) : (message.username == me?.name) - ? _MessageBubble( - you: true, - message: message.message, - ) - : _MessageBubble( - you: false, - message: message.message, - ); + ? _MessageBubble(you: true, message: message.message) + : _MessageBubble(you: false, message: message.message); }, ); }, - loading: () => const Center( - child: CircularProgressIndicator(), - ), - error: (error, _) => Center( - child: Text(error.toString()), - ), + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, _) => Center(child: Text(error.toString())), ), ), ), @@ -138,10 +117,10 @@ class _MessageBubble extends ConsumerWidget { ? Theme.of(context).colorScheme.primaryContainer : CupertinoColors.systemGrey4.resolveFrom(context) : you - ? Theme.of(context).colorScheme.primaryContainer - : brightness == Brightness.light - ? lighten(LichessColors.grey) - : darken(LichessColors.grey, 0.5); + ? Theme.of(context).colorScheme.primaryContainer + : brightness == Brightness.light + ? lighten(LichessColors.grey) + : darken(LichessColors.grey, 0.5); Color _textColor(BuildContext context, Brightness brightness) => Theme.of(context).platform == TargetPlatform.iOS @@ -149,10 +128,10 @@ class _MessageBubble extends ConsumerWidget { ? Theme.of(context).colorScheme.onPrimaryContainer : CupertinoColors.label.resolveFrom(context) : you - ? Theme.of(context).colorScheme.onPrimaryContainer - : brightness == Brightness.light - ? Colors.black - : Colors.white; + ? Theme.of(context).colorScheme.onPrimaryContainer + : brightness == Brightness.light + ? Colors.black + : Colors.white; @override Widget build(BuildContext context, WidgetRef ref) { @@ -170,12 +149,7 @@ class _MessageBubble extends ConsumerWidget { borderRadius: BorderRadius.circular(16.0), color: _bubbleColor(context, brightness), ), - child: Text( - message, - style: TextStyle( - color: _textColor(context, brightness), - ), - ), + child: Text(message, style: TextStyle(color: _textColor(context, brightness))), ), ), ); @@ -227,40 +201,36 @@ class _ChatBottomBarState extends ConsumerState<_ChatBottomBar> { final session = ref.watch(authSessionProvider); final sendButton = ValueListenableBuilder( valueListenable: _textController, - builder: (context, value, child) => PlatformIconButton( - onTap: session != null && value.text.isNotEmpty - ? () { - ref - .read(chatControllerProvider(widget.id).notifier) - .sendMessage(_textController.text); - _textController.clear(); - } - : null, - icon: Icons.send, - padding: EdgeInsets.zero, - semanticsLabel: context.l10n.send, - ), + builder: + (context, value, child) => PlatformIconButton( + onTap: + session != null && value.text.isNotEmpty + ? () { + ref + .read(chatControllerProvider(widget.id).notifier) + .sendMessage(_textController.text); + _textController.clear(); + } + : null, + icon: Icons.send, + padding: EdgeInsets.zero, + semanticsLabel: context.l10n.send, + ), ); - final placeholder = - session != null ? context.l10n.talkInChat : context.l10n.loginToChat; + final placeholder = session != null ? context.l10n.talkInChat : context.l10n.loginToChat; return SafeArea( top: false, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), child: AdaptiveTextField( materialDecoration: InputDecoration( - contentPadding: - const EdgeInsets.symmetric(vertical: 10.0, horizontal: 15.0), + contentPadding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 15.0), suffixIcon: sendButton, - border: const OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(20.0)), - ), + border: const OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(20.0))), hintText: placeholder, ), cupertinoDecoration: BoxDecoration( - border: Border.all( - color: CupertinoColors.separator.resolveFrom(context), - ), + border: Border.all(color: CupertinoColors.separator.resolveFrom(context)), borderRadius: const BorderRadius.all(Radius.circular(30.0)), ), placeholder: placeholder, diff --git a/lib/src/view/game/offline_correspondence_games_screen.dart b/lib/src/view/game/offline_correspondence_games_screen.dart index e4f8dcc356..d5e51b49a6 100644 --- a/lib/src/view/game/offline_correspondence_games_screen.dart +++ b/lib/src/view/game/offline_correspondence_games_screen.dart @@ -35,17 +35,15 @@ class _Body extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final offlineGames = ref.watch(offlineOngoingCorrespondenceGamesProvider); return offlineGames.maybeWhen( - data: (data) => ListView( - children: [ - const SizedBox(height: 8.0), - ...data.map( - (game) => OfflineCorrespondenceGamePreview( - game: game.$2, - lastModified: game.$1, - ), + data: + (data) => ListView( + children: [ + const SizedBox(height: 8.0), + ...data.map( + (game) => OfflineCorrespondenceGamePreview(game: game.$2, lastModified: game.$1), + ), + ], ), - ], - ), orElse: () => const SizedBox.shrink(), ); } @@ -69,10 +67,7 @@ class OfflineCorrespondenceGamePreview extends ConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - UserFullNameWidget( - user: game.opponent.user, - style: Styles.boardPreviewTitle, - ), + UserFullNameWidget(user: game.opponent.user, style: Styles.boardPreviewTitle), if (game.myTimeLeft(lastModified) != null) Text( timeago.format( @@ -80,20 +75,14 @@ class OfflineCorrespondenceGamePreview extends ConsumerWidget { allowFromNow: true, ), ), - Icon( - game.perf.icon, - size: 40, - color: DefaultTextStyle.of(context).style.color, - ), + Icon(game.perf.icon, size: 40, color: DefaultTextStyle.of(context).style.color), ], ), onTap: () { pushPlatformRoute( context, rootNavigator: true, - builder: (_) => OfflineCorrespondenceGameScreen( - initialGame: (lastModified, game), - ), + builder: (_) => OfflineCorrespondenceGameScreen(initialGame: (lastModified, game)), ); }, ); diff --git a/lib/src/view/game/ping_rating.dart b/lib/src/view/game/ping_rating.dart index 4ee011d17f..f38a98645b 100644 --- a/lib/src/view/game/ping_rating.dart +++ b/lib/src/view/game/ping_rating.dart @@ -12,19 +12,16 @@ final pingRatingProvider = Provider.autoDispose((ref) { return ping == 0 ? 0 : ping < 150 - ? 4 - : ping < 300 - ? 3 - : ping < 500 - ? 2 - : 1; + ? 4 + : ping < 300 + ? 3 + : ping < 500 + ? 2 + : 1; }); class SocketPingRating extends ConsumerWidget { - const SocketPingRating({ - required this.size, - super.key, - }); + const SocketPingRating({required this.size, super.key}); final double size; @@ -32,10 +29,6 @@ class SocketPingRating extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final pingRating = ref.watch(pingRatingProvider); - return LagIndicator( - lagRating: pingRating, - size: size, - showLoadingIndicator: true, - ); + return LagIndicator(lagRating: pingRating, size: size, showLoadingIndicator: true); } } diff --git a/lib/src/view/game/status_l10n.dart b/lib/src/view/game/status_l10n.dart index 2b74280e86..a08a3d003c 100644 --- a/lib/src/view/game/status_l10n.dart +++ b/lib/src/view/game/status_l10n.dart @@ -20,9 +20,7 @@ String gameStatusL10n( case GameStatus.mate: return context.l10n.checkmate; case GameStatus.resign: - return winner == Side.black - ? context.l10n.whiteResigned - : context.l10n.blackResigned; + return winner == Side.black ? context.l10n.whiteResigned : context.l10n.blackResigned; case GameStatus.stalemate: return context.l10n.stalemate; case GameStatus.timeout: @@ -31,8 +29,8 @@ String gameStatusL10n( ? '${context.l10n.whiteLeftTheGame} • ${context.l10n.draw}' : '${context.l10n.blackLeftTheGame} • ${context.l10n.draw}' : winner == Side.black - ? context.l10n.whiteLeftTheGame - : context.l10n.blackLeftTheGame; + ? context.l10n.whiteLeftTheGame + : context.l10n.blackLeftTheGame; case GameStatus.draw: if (lastPosition.isInsufficientMaterial) { return '${context.l10n.insufficientMaterial} • ${context.l10n.draw}'; @@ -47,12 +45,10 @@ String gameStatusL10n( ? '${context.l10n.whiteTimeOut} • ${context.l10n.draw}' : '${context.l10n.blackTimeOut} • ${context.l10n.draw}' : winner == Side.black - ? context.l10n.whiteTimeOut - : context.l10n.blackTimeOut; + ? context.l10n.whiteTimeOut + : context.l10n.blackTimeOut; case GameStatus.noStart: - return winner == Side.black - ? context.l10n.whiteDidntMove - : context.l10n.blackDidntMove; + return winner == Side.black ? context.l10n.whiteDidntMove : context.l10n.blackDidntMove; case GameStatus.unknownFinish: return context.l10n.finished; case GameStatus.cheat: diff --git a/lib/src/view/home/home_tab_screen.dart b/lib/src/view/home/home_tab_screen.dart index 2754be744d..31179d5972 100644 --- a/lib/src/view/home/home_tab_screen.dart +++ b/lib/src/view/home/home_tab_screen.dart @@ -78,31 +78,19 @@ class _HomeScreenState extends ConsumerState with RouteAware { data: (status) { final session = ref.watch(authSessionProvider); final ongoingGames = ref.watch(ongoingGamesProvider); - final emptyRecent = ref.watch(myRecentGamesProvider).maybeWhen( - data: (data) => data.isEmpty, - orElse: () => false, - ); + final emptyRecent = ref + .watch(myRecentGamesProvider) + .maybeWhen(data: (data) => data.isEmpty, orElse: () => false); final isTablet = isTabletOrLarger(context); // Show the welcome screen if there are no recent games and no stored games // (i.e. first installation, or the user has never played a game) - final widgets = emptyRecent - ? _welcomeScreenWidgets( - session: session, - status: status, - isTablet: isTablet, - ) - : isTablet - ? _tabletWidgets( - session: session, - status: status, - ongoingGames: ongoingGames, - ) - : _handsetWidgets( - session: session, - status: status, - ongoingGames: ongoingGames, - ); + final widgets = + emptyRecent + ? _welcomeScreenWidgets(session: session, status: status, isTablet: isTablet) + : isTablet + ? _tabletWidgets(session: session, status: status, ongoingGames: ongoingGames) + : _handsetWidgets(session: session, status: status, ongoingGames: ongoingGames); if (Theme.of(context).platform == TargetPlatform.iOS) { return CupertinoPageScaffold( @@ -113,26 +101,19 @@ class _HomeScreenState extends ConsumerState with RouteAware { controller: homeScrollController, slivers: [ CupertinoSliverNavigationBar( - padding: const EdgeInsetsDirectional.only( - start: 16.0, - end: 8.0, - ), + padding: const EdgeInsetsDirectional.only(start: 16.0, end: 8.0), largeTitle: Text(context.l10n.mobileHomeTab), leading: CupertinoButton( alignment: Alignment.centerLeft, padding: EdgeInsets.zero, onPressed: () { - ref.read(editModeProvider.notifier).state = - !isEditing; + ref.read(editModeProvider.notifier).state = !isEditing; }, child: Text(isEditing ? 'Done' : 'Edit'), ), trailing: const Row( mainAxisSize: MainAxisSize.min, - children: [ - _ChallengeScreenButton(), - _PlayerScreenButton(), - ], + children: [_ChallengeScreenButton(), _PlayerScreenButton()], ), ), CupertinoSliverRefreshControl( @@ -141,9 +122,7 @@ class _HomeScreenState extends ConsumerState with RouteAware { const SliverToBoxAdapter(child: ConnectivityBanner()), SliverSafeArea( top: false, - sliver: SliverList( - delegate: SliverChildListDelegate(widgets), - ), + sliver: SliverList(delegate: SliverChildListDelegate(widgets)), ), ], ), @@ -153,8 +132,7 @@ class _HomeScreenState extends ConsumerState with RouteAware { right: 8.0, child: FloatingActionButton.extended( backgroundColor: CupertinoTheme.of(context).primaryColor, - foregroundColor: - CupertinoTheme.of(context).primaryContrastingColor, + foregroundColor: CupertinoTheme.of(context).primaryContrastingColor, onPressed: () { pushPlatformRoute( context, @@ -178,9 +156,7 @@ class _HomeScreenState extends ConsumerState with RouteAware { onPressed: () { ref.read(editModeProvider.notifier).state = !isEditing; }, - icon: Icon( - isEditing ? Icons.save_outlined : Icons.app_registration, - ), + icon: Icon(isEditing ? Icons.save_outlined : Icons.app_registration), tooltip: isEditing ? 'Save' : 'Edit', ), const _ChallengeScreenButton(), @@ -193,27 +169,20 @@ class _HomeScreenState extends ConsumerState with RouteAware { child: Column( children: [ const ConnectivityBanner(), - Expanded( - child: ListView( - controller: homeScrollController, - children: widgets, - ), - ), + Expanded(child: ListView(controller: homeScrollController, children: widgets)), ], ), ), - floatingActionButton: isTablet - ? null - : FloatingActionButton.extended( - onPressed: () { - pushPlatformRoute( - context, - builder: (_) => const PlayScreen(), - ); - }, - icon: const Icon(Icons.add), - label: Text(context.l10n.play), - ), + floatingActionButton: + isTablet + ? null + : FloatingActionButton.extended( + onPressed: () { + pushPlatformRoute(context, builder: (_) => const PlayScreen()); + }, + icon: const Icon(Icons.add), + label: Text(context.l10n.play), + ), ); } }, @@ -228,26 +197,17 @@ class _HomeScreenState extends ConsumerState with RouteAware { required AsyncValue> ongoingGames, }) { return [ - const _EditableWidget( - widget: EnabledWidget.hello, - shouldShow: true, - child: _HelloWidget(), - ), + const _EditableWidget(widget: EnabledWidget.hello, shouldShow: true, child: _HelloWidget()), if (status.isOnline) _EditableWidget( widget: EnabledWidget.perfCards, shouldShow: session != null, - child: const AccountPerfCards( - padding: Styles.horizontalBodyPadding, - ), + child: const AccountPerfCards(padding: Styles.horizontalBodyPadding), ), _EditableWidget( widget: EnabledWidget.quickPairing, shouldShow: status.isOnline, - child: const Padding( - padding: Styles.bodySectionPadding, - child: QuickGameMatrix(), - ), + child: const Padding(padding: Styles.bodySectionPadding, child: QuickGameMatrix()), ), if (status.isOnline) _OngoingGamesCarousel(ongoingGames, maxGamesToShow: 20) @@ -270,17 +230,15 @@ class _HomeScreenState extends ConsumerState with RouteAware { Padding( padding: Styles.horizontalBodyPadding, child: LichessMessage( - style: Theme.of(context).platform == TargetPlatform.iOS - ? const TextStyle(fontSize: 18) - : Theme.of(context).textTheme.bodyLarge, + style: + Theme.of(context).platform == TargetPlatform.iOS + ? const TextStyle(fontSize: 18) + : Theme.of(context).textTheme.bodyLarge, textAlign: TextAlign.center, ), ), const SizedBox(height: 24.0), - if (session == null) ...[ - const Center(child: _SignInWidget()), - const SizedBox(height: 16.0), - ], + if (session == null) ...[const Center(child: _SignInWidget()), const SizedBox(height: 16.0)], if (Theme.of(context).platform != TargetPlatform.iOS && (session == null || session.user.isPatron != true)) ...[ Center( @@ -309,14 +267,8 @@ class _HomeScreenState extends ConsumerState with RouteAware { if (isTablet) Row( children: [ - const Flexible( - child: _TabletCreateAGameSection(), - ), - Flexible( - child: Column( - children: welcomeWidgets, - ), - ), + const Flexible(child: _TabletCreateAGameSection()), + Flexible(child: Column(children: welcomeWidgets)), ], ) else ...[ @@ -324,10 +276,7 @@ class _HomeScreenState extends ConsumerState with RouteAware { const _EditableWidget( widget: EnabledWidget.quickPairing, shouldShow: true, - child: Padding( - padding: Styles.bodySectionPadding, - child: QuickGameMatrix(), - ), + child: Padding(padding: Styles.bodySectionPadding, child: QuickGameMatrix()), ), ...welcomeWidgets, ], @@ -340,18 +289,12 @@ class _HomeScreenState extends ConsumerState with RouteAware { required AsyncValue> ongoingGames, }) { return [ - const _EditableWidget( - widget: EnabledWidget.hello, - shouldShow: true, - child: _HelloWidget(), - ), + const _EditableWidget(widget: EnabledWidget.hello, shouldShow: true, child: _HelloWidget()), if (status.isOnline) _EditableWidget( widget: EnabledWidget.perfCards, shouldShow: session != null, - child: const AccountPerfCards( - padding: Styles.bodySectionPadding, - ), + child: const AccountPerfCards(padding: Styles.bodySectionPadding), ), Row( crossAxisAlignment: CrossAxisAlignment.start, @@ -362,14 +305,9 @@ class _HomeScreenState extends ConsumerState with RouteAware { const SizedBox(height: 8.0), const _TabletCreateAGameSection(), if (status.isOnline) - _OngoingGamesPreview( - ongoingGames, - maxGamesToShow: 5, - ) + _OngoingGamesPreview(ongoingGames, maxGamesToShow: 5) else - const _OfflineCorrespondencePreview( - maxGamesToShow: 5, - ), + const _OfflineCorrespondencePreview(maxGamesToShow: 5), ], ), ), @@ -377,10 +315,7 @@ class _HomeScreenState extends ConsumerState with RouteAware { child: Column( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.start, - children: [ - SizedBox(height: 8.0), - RecentGamesWidget(), - ], + children: [SizedBox(height: 8.0), RecentGamesWidget()], ), ), ], @@ -406,9 +341,10 @@ class _SignInWidget extends ConsumerWidget { return SecondaryButton( semanticsLabel: context.l10n.signIn, - onPressed: authController.isLoading - ? null - : () => ref.read(authControllerProvider.notifier).signIn(), + onPressed: + authController.isLoading + ? null + : () => ref.read(authControllerProvider.notifier).signIn(), child: Text(context.l10n.signIn), ); } @@ -428,11 +364,7 @@ class _SignInWidget extends ConsumerWidget { /// This parameter is only active when the user is not in edit mode, as we /// always want to display the widget in edit mode. class _EditableWidget extends ConsumerWidget { - const _EditableWidget({ - required this.child, - required this.widget, - required this.shouldShow, - }); + const _EditableWidget({required this.child, required this.widget, required this.shouldShow}); final Widget child; final EnabledWidget widget; @@ -450,30 +382,23 @@ class _EditableWidget extends ConsumerWidget { return isEditing ? Row( - mainAxisSize: MainAxisSize.max, - children: [ - Padding( - padding: const EdgeInsets.only(left: 8.0), - child: Checkbox.adaptive( - value: isEnabled, - onChanged: (_) { - ref - .read(homePreferencesProvider.notifier) - .toggleWidget(widget); - }, - ), - ), - Expanded( - child: IgnorePointer( - ignoring: isEditing, - child: child, - ), + mainAxisSize: MainAxisSize.max, + children: [ + Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Checkbox.adaptive( + value: isEnabled, + onChanged: (_) { + ref.read(homePreferencesProvider.notifier).toggleWidget(widget); + }, ), - ], - ) + ), + Expanded(child: IgnorePointer(ignoring: isEditing, child: child)), + ], + ) : isEnabled - ? child - : const SizedBox.shrink(); + ? child + : const SizedBox.shrink(); } } @@ -483,42 +408,33 @@ class _HelloWidget extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final session = ref.watch(authSessionProvider); - final style = Theme.of(context).platform == TargetPlatform.iOS - ? const TextStyle(fontSize: 20) - : Theme.of(context).textTheme.bodyLarge; + final style = + Theme.of(context).platform == TargetPlatform.iOS + ? const TextStyle(fontSize: 20) + : Theme.of(context).textTheme.bodyLarge; - final iconSize = - Theme.of(context).platform == TargetPlatform.iOS ? 26.0 : 24.0; + final iconSize = Theme.of(context).platform == TargetPlatform.iOS ? 26.0 : 24.0; // fetch the account user to be sure we have the latest data (flair, etc.) - final accountUser = ref.watch(accountProvider).maybeWhen( - data: (data) => data?.lightUser, - orElse: () => null, - ); + final accountUser = ref + .watch(accountProvider) + .maybeWhen(data: (data) => data?.lightUser, orElse: () => null); final user = accountUser ?? session?.user; return Padding( - padding: - Styles.horizontalBodyPadding.add(Styles.sectionBottomPadding).add( - const EdgeInsets.only(top: 8.0), - ), + padding: Styles.horizontalBodyPadding + .add(Styles.sectionBottomPadding) + .add(const EdgeInsets.only(top: 8.0)), child: GestureDetector( onTap: () { ref.invalidate(accountActivityProvider); - pushPlatformRoute( - context, - builder: (context) => const ProfileScreen(), - ); + pushPlatformRoute(context, builder: (context) => const ProfileScreen()); }, child: Row( mainAxisSize: MainAxisSize.min, children: [ - Icon( - Icons.wb_sunny, - size: iconSize, - color: context.lichessColors.brag, - ), + Icon(Icons.wb_sunny, size: iconSize, color: context.lichessColors.brag), const SizedBox(width: 5.0), if (user != null) l10nWithWidget( @@ -546,15 +462,9 @@ class _TabletCreateAGameSection extends StatelessWidget { _EditableWidget( widget: EnabledWidget.quickPairing, shouldShow: true, - child: Padding( - padding: Styles.bodySectionPadding, - child: QuickGameMatrix(), - ), - ), - Padding( - padding: Styles.bodySectionPadding, - child: QuickGameButton(), + child: Padding(padding: Styles.bodySectionPadding, child: QuickGameMatrix()), ), + Padding(padding: Styles.bodySectionPadding, child: QuickGameButton()), CreateGameOptions(), ], ); @@ -577,21 +487,23 @@ class _OngoingGamesCarousel extends ConsumerWidget { } return _GamesCarousel( list: data, - builder: (game) => _GamePreviewCarouselItem( - game: game, - onTap: () { - pushPlatformRoute( - context, - rootNavigator: true, - builder: (context) => GameScreen( - initialGameId: game.fullId, - loadingFen: game.fen, - loadingOrientation: game.orientation, - loadingLastMove: game.lastMove, - ), - ); - }, - ), + builder: + (game) => _GamePreviewCarouselItem( + game: game, + onTap: () { + pushPlatformRoute( + context, + rootNavigator: true, + builder: + (context) => GameScreen( + initialGameId: game.fullId, + loadingFen: game.fen, + loadingOrientation: game.orientation, + loadingLastMove: game.lastMove, + ), + ); + }, + ), moreScreenBuilder: (_) => const OngoingGamesScreen(), maxGamesToShow: maxGamesToShow, ); @@ -608,8 +520,7 @@ class _OfflineCorrespondenceCarousel extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final offlineCorresGames = - ref.watch(offlineOngoingCorrespondenceGamesProvider); + final offlineCorresGames = ref.watch(offlineOngoingCorrespondenceGamesProvider); return offlineCorresGames.maybeWhen( data: (data) { if (data.isEmpty) { @@ -617,32 +528,31 @@ class _OfflineCorrespondenceCarousel extends ConsumerWidget { } return _GamesCarousel( list: data, - builder: (el) => _GamePreviewCarouselItem( - game: OngoingGame( - id: el.$2.id, - fullId: el.$2.fullId, - orientation: el.$2.orientation, - fen: el.$2.lastPosition.fen, - perf: el.$2.perf, - speed: el.$2.speed, - variant: el.$2.variant, - opponent: el.$2.opponent.user, - isMyTurn: el.$2.isMyTurn, - opponentRating: el.$2.opponent.rating, - opponentAiLevel: el.$2.opponent.aiLevel, - lastMove: el.$2.lastMove, - secondsLeft: el.$2.myTimeLeft(el.$1)?.inSeconds, - ), - onTap: () { - pushPlatformRoute( - context, - rootNavigator: true, - builder: (_) => OfflineCorrespondenceGameScreen( - initialGame: (el.$1, el.$2), + builder: + (el) => _GamePreviewCarouselItem( + game: OngoingGame( + id: el.$2.id, + fullId: el.$2.fullId, + orientation: el.$2.orientation, + fen: el.$2.lastPosition.fen, + perf: el.$2.perf, + speed: el.$2.speed, + variant: el.$2.variant, + opponent: el.$2.opponent.user, + isMyTurn: el.$2.isMyTurn, + opponentRating: el.$2.opponent.rating, + opponentAiLevel: el.$2.opponent.aiLevel, + lastMove: el.$2.lastMove, + secondsLeft: el.$2.myTimeLeft(el.$1)?.inSeconds, ), - ); - }, - ), + onTap: () { + pushPlatformRoute( + context, + rootNavigator: true, + builder: (_) => OfflineCorrespondenceGameScreen(initialGame: (el.$1, el.$2)), + ); + }, + ), moreScreenBuilder: (_) => const OfflineCorrespondenceGamesScreen(), maxGamesToShow: maxGamesToShow, ); @@ -748,36 +658,25 @@ class _GamePreviewCarouselItem extends StatelessWidget { Row( children: [ if (game.isMyTurn) ...const [ - Icon( - Icons.timer, - size: 16.0, - color: Colors.white, - ), + Icon(Icons.timer, size: 16.0, color: Colors.white), SizedBox(width: 4.0), ], Text( game.secondsLeft != null && game.isMyTurn ? timeago.format( - DateTime.now().add( - Duration(seconds: game.secondsLeft!), - ), - allowFromNow: true, - ) + DateTime.now().add(Duration(seconds: game.secondsLeft!)), + allowFromNow: true, + ) : game.isMyTurn - ? context.l10n.yourTurn - : context.l10n.waitingForOpponent, - style: Theme.of(context).platform == TargetPlatform.iOS - ? const TextStyle( - fontSize: 12, - fontWeight: FontWeight.w500, - ) - : TextStyle( - fontSize: Theme.of(context) - .textTheme - .labelMedium - ?.fontSize, - fontWeight: FontWeight.w500, - ), + ? context.l10n.yourTurn + : context.l10n.waitingForOpponent, + style: + Theme.of(context).platform == TargetPlatform.iOS + ? const TextStyle(fontSize: 12, fontWeight: FontWeight.w500) + : TextStyle( + fontSize: Theme.of(context).textTheme.labelMedium?.fontSize, + fontWeight: FontWeight.w500, + ), ), ], ), @@ -827,17 +726,13 @@ class _OfflineCorrespondencePreview extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final offlineCorresGames = - ref.watch(offlineOngoingCorrespondenceGamesProvider); + final offlineCorresGames = ref.watch(offlineOngoingCorrespondenceGamesProvider); return offlineCorresGames.maybeWhen( data: (data) { return PreviewGameList( list: data, maxGamesToShow: maxGamesToShow, - builder: (el) => OfflineCorrespondenceGamePreview( - game: el.$2, - lastModified: el.$1, - ), + builder: (el) => OfflineCorrespondenceGamePreview(game: el.$2, lastModified: el.$1), moreScreenBuilder: (_) => const OfflineCorrespondenceGamesScreen(), ); }, @@ -868,9 +763,7 @@ class PreviewGameList extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( - padding: Styles.horizontalBodyPadding.add( - const EdgeInsets.only(top: 16.0), - ), + padding: Styles.horizontalBodyPadding.add(const EdgeInsets.only(top: 16.0)), child: Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -912,24 +805,27 @@ class _PlayerScreenButton extends ConsumerWidget { final connectivity = ref.watch(connectivityChangesProvider); return connectivity.maybeWhen( - data: (connectivity) => AppBarIconButton( - icon: const Icon(Icons.group_outlined), - semanticsLabel: context.l10n.players, - onPressed: !connectivity.isOnline - ? null - : () { - pushPlatformRoute( - context, - title: context.l10n.players, - builder: (_) => const PlayerScreen(), - ); - }, - ), - orElse: () => AppBarIconButton( - icon: const Icon(Icons.group_outlined), - semanticsLabel: context.l10n.players, - onPressed: null, - ), + data: + (connectivity) => AppBarIconButton( + icon: const Icon(Icons.group_outlined), + semanticsLabel: context.l10n.players, + onPressed: + !connectivity.isOnline + ? null + : () { + pushPlatformRoute( + context, + title: context.l10n.players, + builder: (_) => const PlayerScreen(), + ); + }, + ), + orElse: + () => AppBarIconButton( + icon: const Icon(Icons.group_outlined), + semanticsLabel: context.l10n.players, + onPressed: null, + ), ); } } @@ -950,26 +846,29 @@ class _ChallengeScreenButton extends ConsumerWidget { final count = challenges.valueOrNull?.inward.length; return connectivity.maybeWhen( - data: (connectivity) => AppBarNotificationIconButton( - icon: const Icon(LichessIcons.crossed_swords, size: 18.0), - semanticsLabel: context.l10n.preferencesNotifyChallenge, - onPressed: !connectivity.isOnline - ? null - : () { - ref.invalidate(challengesProvider); - pushPlatformRoute( - context, - title: context.l10n.preferencesNotifyChallenge, - builder: (_) => const ChallengeRequestsScreen(), - ); - }, - count: count ?? 0, - ), - orElse: () => AppBarIconButton( - icon: const Icon(LichessIcons.crossed_swords, size: 18.0), - semanticsLabel: context.l10n.preferencesNotifyChallenge, - onPressed: null, - ), + data: + (connectivity) => AppBarNotificationIconButton( + icon: const Icon(LichessIcons.crossed_swords, size: 18.0), + semanticsLabel: context.l10n.preferencesNotifyChallenge, + onPressed: + !connectivity.isOnline + ? null + : () { + ref.invalidate(challengesProvider); + pushPlatformRoute( + context, + title: context.l10n.preferencesNotifyChallenge, + builder: (_) => const ChallengeRequestsScreen(), + ); + }, + count: count ?? 0, + ), + orElse: + () => AppBarIconButton( + icon: const Icon(LichessIcons.crossed_swords, size: 18.0), + semanticsLabel: context.l10n.preferencesNotifyChallenge, + onPressed: null, + ), ); } } diff --git a/lib/src/view/opening_explorer/opening_explorer_screen.dart b/lib/src/view/opening_explorer/opening_explorer_screen.dart index 2c9722ba5a..6aa6c1d9a9 100644 --- a/lib/src/view/opening_explorer/opening_explorer_screen.dart +++ b/lib/src/view/opening_explorer/opening_explorer_screen.dart @@ -36,39 +36,35 @@ class OpeningExplorerScreen extends ConsumerWidget { final body = switch (ref.watch(ctrlProvider)) { AsyncData(value: final state) => _Body(options: options, state: state), AsyncError(:final error) => Center( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Text(error.toString()), - ), - ), + child: Padding(padding: const EdgeInsets.all(16.0), child: Text(error.toString())), + ), _ => const CenterLoadingIndicator(), }; return PlatformWidget( - androidBuilder: (_) => Scaffold( - body: body, - appBar: AppBar( - title: Text(context.l10n.openingExplorer), - bottom: _MoveList(options: options), - ), - ), - iosBuilder: (_) => CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - middle: Text(context.l10n.openingExplorer), - automaticBackgroundVisibility: false, - border: null, - ), - child: body, - ), + androidBuilder: + (_) => Scaffold( + body: body, + appBar: AppBar( + title: Text(context.l10n.openingExplorer), + bottom: _MoveList(options: options), + ), + ), + iosBuilder: + (_) => CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + middle: Text(context.l10n.openingExplorer), + automaticBackgroundVisibility: false, + border: null, + ), + child: body, + ), ); } } class _Body extends ConsumerWidget { - const _Body({ - required this.options, - required this.state, - }); + const _Body({required this.options, required this.state}); final AnalysisOptions options; final AnalysisState state; @@ -83,28 +79,29 @@ class _Body extends ConsumerWidget { children: [ if (Theme.of(context).platform == TargetPlatform.iOS) Padding( - padding: isTablet - ? const EdgeInsets.symmetric( - horizontal: kTabletBoardTableSidePadding, - ) - : EdgeInsets.zero, + padding: + isTablet + ? const EdgeInsets.symmetric(horizontal: kTabletBoardTableSidePadding) + : EdgeInsets.zero, child: _MoveList(options: options), ), Expanded( child: LayoutBuilder( builder: (context, constraints) { - final orientation = constraints.maxWidth > constraints.maxHeight - ? Orientation.landscape - : Orientation.portrait; + final orientation = + constraints.maxWidth > constraints.maxHeight + ? Orientation.landscape + : Orientation.portrait; if (orientation == Orientation.landscape) { - final sideWidth = constraints.biggest.longestSide - - constraints.biggest.shortestSide; - final defaultBoardSize = constraints.biggest.shortestSide - - (kTabletBoardTableSidePadding * 2); - final boardSize = sideWidth >= 250 - ? defaultBoardSize - : constraints.biggest.longestSide / kGoldenRatio - - (kTabletBoardTableSidePadding * 2); + final sideWidth = + constraints.biggest.longestSide - constraints.biggest.shortestSide; + final defaultBoardSize = + constraints.biggest.shortestSide - (kTabletBoardTableSidePadding * 2); + final boardSize = + sideWidth >= 250 + ? defaultBoardSize + : constraints.biggest.longestSide / kGoldenRatio - + (kTabletBoardTableSidePadding * 2); return Row( mainAxisSize: MainAxisSize.max, children: [ @@ -129,21 +126,14 @@ class _Body extends ConsumerWidget { Expanded( child: PlatformCard( clipBehavior: Clip.hardEdge, - borderRadius: const BorderRadius.all( - Radius.circular(4.0), - ), - margin: const EdgeInsets.all( - kTabletBoardTableSidePadding, - ), + borderRadius: const BorderRadius.all(Radius.circular(4.0)), + margin: const EdgeInsets.all(kTabletBoardTableSidePadding), semanticContainer: false, child: OpeningExplorerView( position: state.position, onMoveSelected: (move) { ref - .read( - analysisControllerProvider(options) - .notifier, - ) + .read(analysisControllerProvider(options).notifier) .onUserMove(move); }, ), @@ -156,20 +146,18 @@ class _Body extends ConsumerWidget { ); } else { final defaultBoardSize = constraints.biggest.shortestSide; - final remainingHeight = - constraints.maxHeight - defaultBoardSize; - final isSmallScreen = - remainingHeight < kSmallRemainingHeightLeftBoardThreshold; - final boardSize = isTablet || isSmallScreen - ? defaultBoardSize - kTabletBoardTableSidePadding * 2 - : defaultBoardSize; + final remainingHeight = constraints.maxHeight - defaultBoardSize; + final isSmallScreen = remainingHeight < kSmallRemainingHeightLeftBoardThreshold; + final boardSize = + isTablet || isSmallScreen + ? defaultBoardSize - kTabletBoardTableSidePadding * 2 + : defaultBoardSize; return ListView( - padding: isTablet - ? const EdgeInsets.symmetric( - horizontal: kTabletBoardTableSidePadding, - ) - : EdgeInsets.zero, + padding: + isTablet + ? const EdgeInsets.symmetric(horizontal: kTabletBoardTableSidePadding) + : EdgeInsets.zero, children: [ GestureDetector( // disable scrolling when dragging the board @@ -182,19 +170,12 @@ class _Body extends ConsumerWidget { ), OpeningExplorerView( position: state.position, - opening: state.currentNode.isRoot - ? LightOpening( - eco: '', - name: context.l10n.startPosition, - ) - : state.currentNode.opening ?? - state.currentBranchOpening, + opening: + state.currentNode.isRoot + ? LightOpening(eco: '', name: context.l10n.startPosition) + : state.currentNode.opening ?? state.currentBranchOpening, onMoveSelected: (move) { - ref - .read( - analysisControllerProvider(options).notifier, - ) - .onUserMove(move); + ref.read(analysisControllerProvider(options).notifier).onUserMove(move); }, scrollable: false, ), @@ -234,17 +215,13 @@ class _MoveList extends ConsumerWidget implements PreferredSizeWidget { final currentMoveIndex = state.currentNode.position.ply; return MoveList( - inlineDecoration: Theme.of(context).platform == TargetPlatform.iOS - ? BoxDecoration( - color: Styles.cupertinoAppBarColor.resolveFrom(context), - border: const Border( - bottom: BorderSide( - color: Color(0x4D000000), - width: 0.0, - ), - ), - ) - : null, + inlineDecoration: + Theme.of(context).platform == TargetPlatform.iOS + ? BoxDecoration( + color: Styles.cupertinoAppBarColor.resolveFrom(context), + border: const Border(bottom: BorderSide(color: Color(0x4D000000), width: 0.0)), + ) + : null, type: MoveListType.inline, slicedMoves: slicedMoves, currentMoveIndex: currentMoveIndex, @@ -265,13 +242,10 @@ class _BottomBar extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final db = ref - .watch(openingExplorerPreferencesProvider.select((value) => value.db)); + final db = ref.watch(openingExplorerPreferencesProvider.select((value) => value.db)); final ctrlProvider = analysisControllerProvider(options); - final canGoBack = - ref.watch(ctrlProvider.select((value) => value.requireValue.canGoBack)); - final canGoNext = - ref.watch(ctrlProvider.select((value) => value.requireValue.canGoNext)); + final canGoBack = ref.watch(ctrlProvider.select((value) => value.requireValue.canGoBack)); + final canGoNext = ref.watch(ctrlProvider.select((value) => value.requireValue.canGoNext)); final dbLabel = switch (db) { OpeningDatabase.master => 'Masters', @@ -284,13 +258,14 @@ class _BottomBar extends ConsumerWidget { BottomBarButton( label: dbLabel, showLabel: true, - onTap: () => showAdaptiveBottomSheet( - context: context, - isScrollControlled: true, - showDragHandle: true, - isDismissible: true, - builder: (_) => const OpeningExplorerSettings(), - ), + onTap: + () => showAdaptiveBottomSheet( + context: context, + isScrollControlled: true, + showDragHandle: true, + isDismissible: true, + builder: (_) => const OpeningExplorerSettings(), + ), icon: Icons.tune, ), BottomBarButton( diff --git a/lib/src/view/opening_explorer/opening_explorer_settings.dart b/lib/src/view/opening_explorer/opening_explorer_settings.dart index 15c11c63da..3a31eb28c2 100644 --- a/lib/src/view/opening_explorer/opening_explorer_settings.dart +++ b/lib/src/view/opening_explorer/opening_explorer_settings.dart @@ -29,9 +29,10 @@ class OpeningExplorerSettings extends ConsumerWidget { (key) => ChoiceChip( label: Text(key), selected: prefs.masterDb.sinceYear == MasterDb.datesMap[key], - onSelected: (_) => ref - .read(openingExplorerPreferencesProvider.notifier) - .setMasterDbSince(MasterDb.datesMap[key]!), + onSelected: + (_) => ref + .read(openingExplorerPreferencesProvider.notifier) + .setMasterDbSince(MasterDb.datesMap[key]!), ), ) .toList(growable: false), @@ -49,19 +50,14 @@ class OpeningExplorerSettings extends ConsumerWidget { (speed) => FilterChip( label: Text( String.fromCharCode(speed.icon.codePoint), - style: TextStyle( - fontFamily: speed.icon.fontFamily, - fontSize: 18.0, - ), + style: TextStyle(fontFamily: speed.icon.fontFamily, fontSize: 18.0), ), - tooltip: Perf.fromVariantAndSpeed( - Variant.standard, - speed, - ).title, + tooltip: Perf.fromVariantAndSpeed(Variant.standard, speed).title, selected: prefs.lichessDb.speeds.contains(speed), - onSelected: (_) => ref - .read(openingExplorerPreferencesProvider.notifier) - .toggleLichessDbSpeed(speed), + onSelected: + (_) => ref + .read(openingExplorerPreferencesProvider.notifier) + .toggleLichessDbSpeed(speed), ), ) .toList(growable: false), @@ -75,15 +71,17 @@ class OpeningExplorerSettings extends ConsumerWidget { .map( (rating) => FilterChip( label: Text(rating.toString()), - tooltip: rating == 400 - ? '400-1000' - : rating == 2500 + tooltip: + rating == 400 + ? '400-1000' + : rating == 2500 ? '2500+' : '$rating-${rating + 200}', selected: prefs.lichessDb.ratings.contains(rating), - onSelected: (_) => ref - .read(openingExplorerPreferencesProvider.notifier) - .toggleLichessDbRating(rating), + onSelected: + (_) => ref + .read(openingExplorerPreferencesProvider.notifier) + .toggleLichessDbRating(rating), ), ) .toList(growable: false), @@ -98,9 +96,10 @@ class OpeningExplorerSettings extends ConsumerWidget { (key) => ChoiceChip( label: Text(key), selected: prefs.lichessDb.since == LichessDb.datesMap[key], - onSelected: (_) => ref - .read(openingExplorerPreferencesProvider.notifier) - .setLichessDbSince(LichessDb.datesMap[key]!), + onSelected: + (_) => ref + .read(openingExplorerPreferencesProvider.notifier) + .setLichessDbSince(LichessDb.datesMap[key]!), ), ) .toList(growable: false), @@ -112,26 +111,26 @@ class OpeningExplorerSettings extends ConsumerWidget { title: Text.rich( TextSpan( text: '${context.l10n.player}: ', - style: const TextStyle( - fontWeight: FontWeight.normal, - ), + style: const TextStyle(fontWeight: FontWeight.normal), children: [ TextSpan( - recognizer: TapGestureRecognizer() - ..onTap = () => pushPlatformRoute( - context, - fullscreenDialog: true, - builder: (_) => SearchScreen( - onUserTap: (user) => { - ref - .read( - openingExplorerPreferencesProvider.notifier, - ) - .setPlayerDbUsernameOrId(user.name), - Navigator.of(context).pop(), - }, - ), - ), + recognizer: + TapGestureRecognizer() + ..onTap = + () => pushPlatformRoute( + context, + fullscreenDialog: true, + builder: + (_) => SearchScreen( + onUserTap: + (user) => { + ref + .read(openingExplorerPreferencesProvider.notifier) + .setPlayerDbUsernameOrId(user.name), + Navigator.of(context).pop(), + }, + ), + ), style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 18, @@ -155,9 +154,10 @@ class OpeningExplorerSettings extends ConsumerWidget { Side.black => const Text('Black'), }, selected: prefs.playerDb.side == side, - onSelected: (_) => ref - .read(openingExplorerPreferencesProvider.notifier) - .setPlayerDbSide(side), + onSelected: + (_) => ref + .read(openingExplorerPreferencesProvider.notifier) + .setPlayerDbSide(side), ), ) .toList(growable: false), @@ -172,19 +172,14 @@ class OpeningExplorerSettings extends ConsumerWidget { (speed) => FilterChip( label: Text( String.fromCharCode(speed.icon.codePoint), - style: TextStyle( - fontFamily: speed.icon.fontFamily, - fontSize: 18.0, - ), + style: TextStyle(fontFamily: speed.icon.fontFamily, fontSize: 18.0), ), - tooltip: Perf.fromVariantAndSpeed( - Variant.standard, - speed, - ).title, + tooltip: Perf.fromVariantAndSpeed(Variant.standard, speed).title, selected: prefs.playerDb.speeds.contains(speed), - onSelected: (_) => ref - .read(openingExplorerPreferencesProvider.notifier) - .togglePlayerDbSpeed(speed), + onSelected: + (_) => ref + .read(openingExplorerPreferencesProvider.notifier) + .togglePlayerDbSpeed(speed), ), ) .toList(growable: false), @@ -197,16 +192,15 @@ class OpeningExplorerSettings extends ConsumerWidget { children: GameMode.values .map( (gameMode) => FilterChip( - label: Text( - switch (gameMode) { - GameMode.casual => 'Casual', - GameMode.rated => 'Rated', - }, - ), + label: Text(switch (gameMode) { + GameMode.casual => 'Casual', + GameMode.rated => 'Rated', + }), selected: prefs.playerDb.gameModes.contains(gameMode), - onSelected: (_) => ref - .read(openingExplorerPreferencesProvider.notifier) - .togglePlayerDbGameMode(gameMode), + onSelected: + (_) => ref + .read(openingExplorerPreferencesProvider.notifier) + .togglePlayerDbGameMode(gameMode), ), ) .toList(growable: false), @@ -221,9 +215,10 @@ class OpeningExplorerSettings extends ConsumerWidget { (key) => ChoiceChip( label: Text(key), selected: prefs.playerDb.since == PlayerDb.datesMap[key], - onSelected: (_) => ref - .read(openingExplorerPreferencesProvider.notifier) - .setPlayerDbSince(PlayerDb.datesMap[key]!), + onSelected: + (_) => ref + .read(openingExplorerPreferencesProvider.notifier) + .setPlayerDbSince(PlayerDb.datesMap[key]!), ), ) .toList(growable: false), @@ -241,23 +236,26 @@ class OpeningExplorerSettings extends ConsumerWidget { ChoiceChip( label: const Text('Masters'), selected: prefs.db == OpeningDatabase.master, - onSelected: (_) => ref - .read(openingExplorerPreferencesProvider.notifier) - .setDatabase(OpeningDatabase.master), + onSelected: + (_) => ref + .read(openingExplorerPreferencesProvider.notifier) + .setDatabase(OpeningDatabase.master), ), ChoiceChip( label: const Text('Lichess'), selected: prefs.db == OpeningDatabase.lichess, - onSelected: (_) => ref - .read(openingExplorerPreferencesProvider.notifier) - .setDatabase(OpeningDatabase.lichess), + onSelected: + (_) => ref + .read(openingExplorerPreferencesProvider.notifier) + .setDatabase(OpeningDatabase.lichess), ), ChoiceChip( label: Text(context.l10n.player), selected: prefs.db == OpeningDatabase.player, - onSelected: (_) => ref - .read(openingExplorerPreferencesProvider.notifier) - .setDatabase(OpeningDatabase.player), + onSelected: + (_) => ref + .read(openingExplorerPreferencesProvider.notifier) + .setDatabase(OpeningDatabase.player), ), ], ), diff --git a/lib/src/view/opening_explorer/opening_explorer_view.dart b/lib/src/view/opening_explorer/opening_explorer_view.dart index 00c358ff46..c577a252c8 100644 --- a/lib/src/view/opening_explorer/opening_explorer_view.dart +++ b/lib/src/view/opening_explorer/opening_explorer_view.dart @@ -45,9 +45,7 @@ class _OpeningExplorerState extends ConsumerState { @override Widget build(BuildContext context) { if (widget.position.ply >= 50) { - return Center( - child: Text(context.l10n.maxDepthReached), - ); + return Center(child: Text(context.l10n.maxDepthReached)); } final prefs = ref.watch(openingExplorerPreferencesProvider); @@ -59,22 +57,15 @@ class _OpeningExplorerState extends ConsumerState { ); } - final cacheKey = OpeningExplorerCacheKey( - fen: widget.position.fen, - prefs: prefs, - ); + final cacheKey = OpeningExplorerCacheKey(fen: widget.position.fen, prefs: prefs); final cacheOpeningExplorer = cache[cacheKey]; - final openingExplorerAsync = cacheOpeningExplorer != null - ? AsyncValue.data( - (entry: cacheOpeningExplorer, isIndexing: false), - ) - : ref.watch( - openingExplorerProvider(fen: widget.position.fen), - ); + final openingExplorerAsync = + cacheOpeningExplorer != null + ? AsyncValue.data((entry: cacheOpeningExplorer, isIndexing: false)) + : ref.watch(openingExplorerProvider(fen: widget.position.fen)); if (cacheOpeningExplorer == null) { - ref.listen(openingExplorerProvider(fen: widget.position.fen), - (_, curAsync) { + ref.listen(openingExplorerProvider(fen: widget.position.fen), (_, curAsync) { curAsync.whenData((cur) { if (cur != null && !cur.isIndexing) { cache[cacheKey] = cur.entry; @@ -89,7 +80,8 @@ class _OpeningExplorerState extends ConsumerState { return _ExplorerListView( scrollable: widget.scrollable, isLoading: true, - children: lastExplorerWidgets ?? + children: + lastExplorerWidgets ?? [ const Shimmer( child: ShimmerLoading( @@ -107,8 +99,7 @@ class _OpeningExplorerState extends ConsumerState { final ply = widget.position.ply; final children = [ - if (widget.opening != null) - OpeningNameHeader(opening: widget.opening!), + if (widget.opening != null) OpeningNameHeader(opening: widget.opening!), OpeningExplorerMoveTable( moves: value.entry.moves, whiteWins: value.entry.white, @@ -122,40 +113,34 @@ class _OpeningExplorerState extends ConsumerState { key: const Key('topGamesHeader'), child: Text(context.l10n.topGames), ), - ...List.generate( - topGames.length, - (int index) { - return OpeningExplorerGameTile( - key: Key('top-game-${topGames.get(index).id}'), - game: topGames.get(index), - color: index.isEven - ? Theme.of(context).colorScheme.surfaceContainerLow - : Theme.of(context).colorScheme.surfaceContainerHigh, - ply: ply, - ); - }, - growable: false, - ), + ...List.generate(topGames.length, (int index) { + return OpeningExplorerGameTile( + key: Key('top-game-${topGames.get(index).id}'), + game: topGames.get(index), + color: + index.isEven + ? Theme.of(context).colorScheme.surfaceContainerLow + : Theme.of(context).colorScheme.surfaceContainerHigh, + ply: ply, + ); + }, growable: false), ], if (recentGames != null && recentGames.isNotEmpty) ...[ OpeningExplorerHeaderTile( key: const Key('recentGamesHeader'), child: Text(context.l10n.recentGames), ), - ...List.generate( - recentGames.length, - (int index) { - return OpeningExplorerGameTile( - key: Key('recent-game-${recentGames.get(index).id}'), - game: recentGames.get(index), - color: index.isEven - ? Theme.of(context).colorScheme.surfaceContainerLow - : Theme.of(context).colorScheme.surfaceContainerHigh, - ply: ply, - ); - }, - growable: false, - ), + ...List.generate(recentGames.length, (int index) { + return OpeningExplorerGameTile( + key: Key('recent-game-${recentGames.get(index).id}'), + game: recentGames.get(index), + color: + index.isEven + ? Theme.of(context).colorScheme.surfaceContainerLow + : Theme.of(context).colorScheme.surfaceContainerHigh, + ply: ply, + ); + }, growable: false), ], ]; @@ -167,32 +152,23 @@ class _OpeningExplorerState extends ConsumerState { children: children, ); case AsyncError(:final error): - debugPrint( - 'SEVERE: [OpeningExplorerView] could not load opening explorer data; $error', - ); + debugPrint('SEVERE: [OpeningExplorerView] could not load opening explorer data; $error'); final connectivity = ref.watch(connectivityChangesProvider); // TODO l10n final message = connectivity.whenIs( online: () => 'Could not load opening explorer data.', offline: () => 'Opening Explorer is not available offline.', ); - return Center( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Text(message), - ), - ); + return Center(child: Padding(padding: const EdgeInsets.all(16.0), child: Text(message))); case _: return _ExplorerListView( scrollable: widget.scrollable, isLoading: true, - children: lastExplorerWidgets ?? + children: + lastExplorerWidgets ?? [ const Shimmer( - child: ShimmerLoading( - isLoading: true, - child: OpeningExplorerMoveTable.loading(), - ), + child: ShimmerLoading(isLoading: true, child: OpeningExplorerMoveTable.loading()), ), ], ); @@ -221,9 +197,7 @@ class _ExplorerListView extends StatelessWidget { duration: const Duration(milliseconds: 400), curve: Curves.fastOutSlowIn, opacity: isLoading ? 0.20 : 0.0, - child: ColoredBox( - color: brightness == Brightness.dark ? Colors.black : Colors.white, - ), + child: ColoredBox(color: brightness == Brightness.dark ? Colors.black : Colors.white), ), ), ); diff --git a/lib/src/view/opening_explorer/opening_explorer_widgets.dart b/lib/src/view/opening_explorer/opening_explorer_widgets.dart index cad14f93fb..c953f2d83c 100644 --- a/lib/src/view/opening_explorer/opening_explorer_widgets.dart +++ b/lib/src/view/opening_explorer/opening_explorer_widgets.dart @@ -38,17 +38,12 @@ class OpeningNameHeader extends StatelessWidget { Widget build(BuildContext context) { return Container( padding: _kTableRowPadding, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.secondaryContainer, - ), + decoration: BoxDecoration(color: Theme.of(context).colorScheme.secondaryContainer), child: GestureDetector( - onTap: opening.name == context.l10n.startPosition - ? null - : () => launchUrl( - Uri.parse( - 'https://lichess.org/opening/${opening.name}', - ), - ), + onTap: + opening.name == context.l10n.startPosition + ? null + : () => launchUrl(Uri.parse('https://lichess.org/opening/${opening.name}')), child: Row( children: [ if (opening.name != context.l10n.startPosition) ...[ @@ -87,13 +82,13 @@ class OpeningExplorerMoveTable extends ConsumerWidget { }) : _isLoading = false; const OpeningExplorerMoveTable.loading() - : _isLoading = true, - moves = const IListConst([]), - whiteWins = 0, - draws = 0, - blackWins = 0, - isIndexing = false, - onMoveSelected = null; + : _isLoading = true, + moves = const IListConst([]), + whiteWins = 0, + draws = 0, + blackWins = 0, + isIndexing = false, + onMoveSelected = null; final IList moves; final int whiteWins; @@ -124,9 +119,7 @@ class OpeningExplorerMoveTable extends ConsumerWidget { columnWidths: columnWidths, children: [ TableRow( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.secondaryContainer, - ), + decoration: BoxDecoration(color: Theme.of(context).colorScheme.secondaryContainer), children: [ Padding( padding: _kTableRowPadding, @@ -155,56 +148,49 @@ class OpeningExplorerMoveTable extends ConsumerWidget { ), ], ), - ...List.generate( - moves.length, - (int index) { - final move = moves.get(index); - final percentGames = ((move.games / games) * 100).round(); - return TableRow( - decoration: BoxDecoration( - color: index.isEven - ? Theme.of(context).colorScheme.surfaceContainerLow - : Theme.of(context).colorScheme.surfaceContainerHigh, + ...List.generate(moves.length, (int index) { + final move = moves.get(index); + final percentGames = ((move.games / games) * 100).round(); + return TableRow( + decoration: BoxDecoration( + color: + index.isEven + ? Theme.of(context).colorScheme.surfaceContainerLow + : Theme.of(context).colorScheme.surfaceContainerHigh, + ), + children: [ + TableRowInkWell( + onTap: () => onMoveSelected?.call(NormalMove.fromUci(move.uci)), + child: Padding(padding: _kTableRowPadding, child: Text(move.san)), ), - children: [ - TableRowInkWell( - onTap: () => - onMoveSelected?.call(NormalMove.fromUci(move.uci)), - child: Padding( - padding: _kTableRowPadding, - child: Text(move.san), - ), + TableRowInkWell( + onTap: () => onMoveSelected?.call(NormalMove.fromUci(move.uci)), + child: Padding( + padding: _kTableRowPadding, + child: Text('${formatNum(move.games)} ($percentGames%)'), ), - TableRowInkWell( - onTap: () => - onMoveSelected?.call(NormalMove.fromUci(move.uci)), - child: Padding( - padding: _kTableRowPadding, - child: Text('${formatNum(move.games)} ($percentGames%)'), - ), - ), - TableRowInkWell( - onTap: () => - onMoveSelected?.call(NormalMove.fromUci(move.uci)), - child: Padding( - padding: _kTableRowPadding, - child: _WinPercentageChart( - whiteWins: move.white, - draws: move.draws, - blackWins: move.black, - ), + ), + TableRowInkWell( + onTap: () => onMoveSelected?.call(NormalMove.fromUci(move.uci)), + child: Padding( + padding: _kTableRowPadding, + child: _WinPercentageChart( + whiteWins: move.white, + draws: move.draws, + blackWins: move.black, ), ), - ], - ); - }, - ), + ), + ], + ); + }), if (moves.isNotEmpty) TableRow( decoration: BoxDecoration( - color: moves.length.isEven - ? Theme.of(context).colorScheme.surfaceContainerLow - : Theme.of(context).colorScheme.surfaceContainerHigh, + color: + moves.length.isEven + ? Theme.of(context).colorScheme.surfaceContainerLow + : Theme.of(context).colorScheme.surfaceContainerHigh, ), children: [ Container( @@ -212,10 +198,7 @@ class OpeningExplorerMoveTable extends ConsumerWidget { alignment: Alignment.centerLeft, child: const Icon(Icons.functions), ), - Padding( - padding: _kTableRowPadding, - child: Text('${formatNum(games)} (100%)'), - ), + Padding(padding: _kTableRowPadding, child: Text('${formatNum(games)} (100%)')), Padding( padding: _kTableRowPadding, child: _WinPercentageChart( @@ -228,27 +211,17 @@ class OpeningExplorerMoveTable extends ConsumerWidget { ) else TableRow( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainerLow, - ), + decoration: BoxDecoration(color: Theme.of(context).colorScheme.surfaceContainerLow), children: [ Padding( padding: _kTableRowPadding, child: Text( String.fromCharCode(Icons.not_interested_outlined.codePoint), - style: TextStyle( - fontFamily: Icons.not_interested_outlined.fontFamily, - ), + style: TextStyle(fontFamily: Icons.not_interested_outlined.fontFamily), ), ), - Padding( - padding: _kTableRowPadding, - child: Text(context.l10n.noGameFound), - ), - const Padding( - padding: _kTableRowPadding, - child: SizedBox.shrink(), - ), + Padding(padding: _kTableRowPadding, child: Text(context.l10n.noGameFound)), + const Padding(padding: _kTableRowPadding, child: SizedBox.shrink()), ], ), ], @@ -307,16 +280,13 @@ class IndexingIndicator extends StatefulWidget { State createState() => _IndexingIndicatorState(); } -class _IndexingIndicatorState extends State - with TickerProviderStateMixin { +class _IndexingIndicatorState extends State with TickerProviderStateMixin { late AnimationController controller; @override void initState() { - controller = AnimationController( - vsync: this, - duration: const Duration(seconds: 3), - )..addListener(() { + controller = AnimationController(vsync: this, duration: const Duration(seconds: 3)) + ..addListener(() { setState(() {}); }); controller.repeat(); @@ -354,9 +324,7 @@ class OpeningExplorerHeaderTile extends StatelessWidget { return Container( width: double.infinity, padding: _kTableRowPadding, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.secondaryContainer, - ), + decoration: BoxDecoration(color: Theme.of(context).colorScheme.secondaryContainer), child: child, ); } @@ -376,12 +344,10 @@ class OpeningExplorerGameTile extends ConsumerStatefulWidget { final int ply; @override - ConsumerState createState() => - _OpeningExplorerGameTileState(); + ConsumerState createState() => _OpeningExplorerGameTileState(); } -class _OpeningExplorerGameTileState - extends ConsumerState { +class _OpeningExplorerGameTileState extends ConsumerState { @override Widget build(BuildContext context) { const widthResultBox = 50.0; @@ -394,11 +360,12 @@ class _OpeningExplorerGameTileState onTap: () { pushPlatformRoute( context, - builder: (_) => ArchivedGameScreen( - gameId: widget.game.id, - orientation: Side.white, - initialCursor: widget.ply, - ), + builder: + (_) => ArchivedGameScreen( + gameId: widget.game.id, + orientation: Side.white, + initialCursor: widget.ply, + ), ); }, child: Row( @@ -416,14 +383,8 @@ class _OpeningExplorerGameTileState child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - widget.game.white.name, - overflow: TextOverflow.ellipsis, - ), - Text( - widget.game.black.name, - overflow: TextOverflow.ellipsis, - ), + Text(widget.game.white.name, overflow: TextOverflow.ellipsis), + Text(widget.game.black.name, overflow: TextOverflow.ellipsis), ], ), ), @@ -440,9 +401,7 @@ class _OpeningExplorerGameTileState child: const Text( '1-0', textAlign: TextAlign.center, - style: TextStyle( - color: Colors.black, - ), + style: TextStyle(color: Colors.black), ), ) else if (widget.game.winner == 'black') @@ -456,9 +415,7 @@ class _OpeningExplorerGameTileState child: const Text( '0-1', textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white, - ), + style: TextStyle(color: Colors.white), ), ) else @@ -472,18 +429,14 @@ class _OpeningExplorerGameTileState child: const Text( '½-½', textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white, - ), + style: TextStyle(color: Colors.white), ), ), if (widget.game.month != null) ...[ const SizedBox(width: 10.0), Text( widget.game.month!, - style: const TextStyle( - fontFeatures: [FontFeature.tabularFigures()], - ), + style: const TextStyle(fontFeatures: [FontFeature.tabularFigures()]), ), ], if (widget.game.speed != null) ...[ @@ -510,8 +463,7 @@ class _WinPercentageChart extends StatelessWidget { final int draws; final int blackWins; - int percentGames(int games) => - ((games / (whiteWins + draws + blackWins)) * 100).round(); + int percentGames(int games) => ((games / (whiteWins + draws + blackWins)) * 100).round(); String label(int percent) => percent < 20 ? '' : '$percent%'; @override diff --git a/lib/src/view/over_the_board/configure_over_the_board_game.dart b/lib/src/view/over_the_board/configure_over_the_board_game.dart index 1cee182f10..7836038b61 100644 --- a/lib/src/view/over_the_board/configure_over_the_board_game.dart +++ b/lib/src/view/over_the_board/configure_over_the_board_game.dart @@ -15,19 +15,14 @@ import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/non_linear_slider.dart'; import 'package:lichess_mobile/src/widgets/settings.dart'; -void showConfigureGameSheet( - BuildContext context, { - required bool isDismissible, -}) { +void showConfigureGameSheet(BuildContext context, {required bool isDismissible}) { final double screenHeight = MediaQuery.sizeOf(context).height; showAdaptiveBottomSheet( context: context, isScrollControlled: true, showDragHandle: true, isDismissible: isDismissible, - constraints: BoxConstraints( - maxHeight: screenHeight - (screenHeight / 10), - ), + constraints: BoxConstraints(maxHeight: screenHeight - (screenHeight / 10)), builder: (BuildContext context) { return const _ConfigureOverTheBoardGameSheet(); }, @@ -42,8 +37,7 @@ class _ConfigureOverTheBoardGameSheet extends ConsumerStatefulWidget { _ConfigureOverTheBoardGameSheetState(); } -class _ConfigureOverTheBoardGameSheetState - extends ConsumerState<_ConfigureOverTheBoardGameSheet> { +class _ConfigureOverTheBoardGameSheetState extends ConsumerState<_ConfigureOverTheBoardGameSheet> { late Variant chosenVariant; late TimeIncrement timeIncrement; @@ -59,19 +53,13 @@ class _ConfigureOverTheBoardGameSheetState void _setTotalTime(num seconds) { setState(() { - timeIncrement = TimeIncrement( - seconds.toInt(), - timeIncrement.increment, - ); + timeIncrement = TimeIncrement(seconds.toInt(), timeIncrement.increment); }); } void _setIncrement(num seconds) { setState(() { - timeIncrement = TimeIncrement( - timeIncrement.time, - seconds.toInt(), - ); + timeIncrement = TimeIncrement(timeIncrement.time, seconds.toInt()); }); } @@ -86,16 +74,16 @@ class _ConfigureOverTheBoardGameSheetState onTap: () { showChoicePicker( context, - choices: playSupportedVariants - .where( - (variant) => variant != Variant.fromPosition, - ) - .toList(), + choices: + playSupportedVariants + .where((variant) => variant != Variant.fromPosition) + .toList(), selectedItem: chosenVariant, labelBuilder: (Variant variant) => Text(variant.label), - onSelectedItemChanged: (Variant variant) => setState(() { - chosenVariant = variant; - }), + onSelectedItemChanged: + (Variant variant) => setState(() { + chosenVariant = variant; + }), ); }, ), @@ -105,10 +93,7 @@ class _ConfigureOverTheBoardGameSheetState text: '${context.l10n.minutesPerSide}: ', children: [ TextSpan( - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - ), + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18), text: clockLabelInMinutes(timeIncrement.time), ), ], @@ -118,9 +103,7 @@ class _ConfigureOverTheBoardGameSheetState value: timeIncrement.time, values: kAvailableTimesInSeconds, labelBuilder: clockLabelInMinutes, - onChange: Theme.of(context).platform == TargetPlatform.iOS - ? _setTotalTime - : null, + onChange: Theme.of(context).platform == TargetPlatform.iOS ? _setTotalTime : null, onChangeEnd: _setTotalTime, ), ), @@ -130,10 +113,7 @@ class _ConfigureOverTheBoardGameSheetState text: '${context.l10n.incrementInSeconds}: ', children: [ TextSpan( - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - ), + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18), text: timeIncrement.increment.toString(), ), ], @@ -142,28 +122,20 @@ class _ConfigureOverTheBoardGameSheetState subtitle: NonLinearSlider( value: timeIncrement.increment, values: kAvailableIncrementsInSeconds, - onChange: Theme.of(context).platform == TargetPlatform.iOS - ? _setIncrement - : null, + onChange: Theme.of(context).platform == TargetPlatform.iOS ? _setIncrement : null, onChangeEnd: _setIncrement, ), ), SecondaryButton( onPressed: () { + ref.read(overTheBoardClockProvider.notifier).setupClock(timeIncrement); ref - .read(overTheBoardClockProvider.notifier) - .setupClock(timeIncrement); - ref.read(overTheBoardGameControllerProvider.notifier).startNewGame( - chosenVariant, - timeIncrement, - ); + .read(overTheBoardGameControllerProvider.notifier) + .startNewGame(chosenVariant, timeIncrement); Navigator.pop(context); }, semanticsLabel: context.l10n.play, - child: Text( - context.l10n.play, - style: Styles.bold, - ), + child: Text(context.l10n.play, style: Styles.bold), ), ], ); @@ -193,16 +165,14 @@ class OverTheBoardDisplaySettings extends ConsumerWidget { SwitchSettingTile( title: const Text('Use symmetric pieces'), value: prefs.symmetricPieces, - onChanged: (_) => ref - .read(overTheBoardPreferencesProvider.notifier) - .toggleSymmetricPieces(), + onChanged: + (_) => ref.read(overTheBoardPreferencesProvider.notifier).toggleSymmetricPieces(), ), SwitchSettingTile( title: const Text('Flip pieces and oponent info after move'), value: prefs.flipPiecesAfterMove, - onChanged: (_) => ref - .read(overTheBoardPreferencesProvider.notifier) - .toggleFlipPiecesAfterMove(), + onChanged: + (_) => ref.read(overTheBoardPreferencesProvider.notifier).toggleFlipPiecesAfterMove(), ), ], ); diff --git a/lib/src/view/over_the_board/over_the_board_screen.dart b/lib/src/view/over_the_board/over_the_board_screen.dart index 7470cac66d..c4e5f82985 100644 --- a/lib/src/view/over_the_board/over_the_board_screen.dart +++ b/lib/src/view/over_the_board/over_the_board_screen.dart @@ -76,8 +76,7 @@ class _BodyState extends ConsumerState<_Body> { final overTheBoardPrefs = ref.watch(overTheBoardPreferencesProvider); - ref.listen(overTheBoardClockProvider.select((value) => value.flagSide), - (previous, flagSide) { + ref.listen(overTheBoardClockProvider.select((value) => value.flagSide), (previous, flagSide) { if (previous == null && flagSide != null) { ref.read(overTheBoardGameControllerProvider.notifier).onFlag(flagSide); } @@ -90,19 +89,18 @@ class _BodyState extends ConsumerState<_Body> { if (context.mounted) { showAdaptiveDialog( context: context, - builder: (context) => OverTheBoardGameResultDialog( - game: newGameState.game, - onRematch: () { - setState(() { - orientation = orientation.opposite; - ref - .read(overTheBoardGameControllerProvider.notifier) - .rematch(); - ref.read(overTheBoardClockProvider.notifier).restart(); - Navigator.pop(context); - }); - }, - ), + builder: + (context) => OverTheBoardGameResultDialog( + game: newGameState.game, + onRematch: () { + setState(() { + orientation = orientation.opposite; + ref.read(overTheBoardGameControllerProvider.notifier).rematch(); + ref.read(overTheBoardClockProvider.notifier).restart(); + Navigator.pop(context); + }); + }, + ), barrierDismissible: true, ); } @@ -121,40 +119,37 @@ class _BodyState extends ConsumerState<_Body> { key: _boardKey, topTable: _Player( side: orientation.opposite, - upsideDown: !overTheBoardPrefs.flipPiecesAfterMove || - orientation != gameState.turn, + upsideDown: + !overTheBoardPrefs.flipPiecesAfterMove || orientation != gameState.turn, clockKey: const ValueKey('topClock'), ), bottomTable: _Player( side: orientation, - upsideDown: overTheBoardPrefs.flipPiecesAfterMove && - orientation != gameState.turn, + upsideDown: + overTheBoardPrefs.flipPiecesAfterMove && orientation != gameState.turn, clockKey: const ValueKey('bottomClock'), ), orientation: orientation, fen: gameState.currentPosition.fen, lastMove: gameState.lastMove, gameData: GameData( - isCheck: boardPreferences.boardHighlights && - gameState.currentPosition.isCheck, - playerSide: gameState.game.finished - ? PlayerSide.none - : gameState.turn == Side.white + isCheck: boardPreferences.boardHighlights && gameState.currentPosition.isCheck, + playerSide: + gameState.game.finished + ? PlayerSide.none + : gameState.turn == Side.white ? PlayerSide.white : PlayerSide.black, sideToMove: gameState.turn, validMoves: gameState.legalMoves, - onPromotionSelection: ref - .read(overTheBoardGameControllerProvider.notifier) - .onPromotionSelection, + onPromotionSelection: + ref.read(overTheBoardGameControllerProvider.notifier).onPromotionSelection, promotionMove: gameState.promotionMove, onMove: (move, {isDrop}) { - ref.read(overTheBoardClockProvider.notifier).onMove( - newSideToMove: gameState.turn.opposite, - ); ref - .read(overTheBoardGameControllerProvider.notifier) - .makeMove(move); + .read(overTheBoardClockProvider.notifier) + .onMove(newSideToMove: gameState.turn.opposite); + ref.read(overTheBoardGameControllerProvider.notifier).makeMove(move); }, ), moves: gameState.moves, @@ -165,9 +160,8 @@ class _BodyState extends ConsumerState<_Body> { overTheBoardPrefs.flipPiecesAfterMove ? PieceOrientationBehavior.sideToPlay : PieceOrientationBehavior.opponentUpsideDown, - pieceAssets: overTheBoardPrefs.symmetricPieces - ? PieceSet.symmetric.assets - : null, + pieceAssets: + overTheBoardPrefs.symmetricPieces ? PieceSet.symmetric.assets : null, ), ), ), @@ -187,9 +181,7 @@ class _BodyState extends ConsumerState<_Body> { } class _BottomBar extends ConsumerWidget { - const _BottomBar({ - required this.onFlipBoard, - }); + const _BottomBar({required this.onFlipBoard}); final VoidCallback onFlipBoard; @@ -215,51 +207,46 @@ class _BottomBar extends ConsumerWidget { if (!clock.timeIncrement.isInfinite) BottomBarButton( label: clock.active ? 'Pause' : 'Resume', - onTap: gameState.finished - ? null - : () { - if (clock.active) { - ref.read(overTheBoardClockProvider.notifier).pause(); - } else { - ref - .read(overTheBoardClockProvider.notifier) - .resume(gameState.turn); - } - }, + onTap: + gameState.finished + ? null + : () { + if (clock.active) { + ref.read(overTheBoardClockProvider.notifier).pause(); + } else { + ref.read(overTheBoardClockProvider.notifier).resume(gameState.turn); + } + }, icon: clock.active ? CupertinoIcons.pause : CupertinoIcons.play, ), BottomBarButton( label: 'Previous', - onTap: gameState.canGoBack - ? () { - ref - .read(overTheBoardGameControllerProvider.notifier) - .goBack(); - if (clock.active) { - ref.read(overTheBoardClockProvider.notifier).switchSide( - newSideToMove: gameState.turn.opposite, - addIncrement: false, - ); + onTap: + gameState.canGoBack + ? () { + ref.read(overTheBoardGameControllerProvider.notifier).goBack(); + if (clock.active) { + ref + .read(overTheBoardClockProvider.notifier) + .switchSide(newSideToMove: gameState.turn.opposite, addIncrement: false); + } } - } - : null, + : null, icon: CupertinoIcons.chevron_back, ), BottomBarButton( label: 'Next', - onTap: gameState.canGoForward - ? () { - ref - .read(overTheBoardGameControllerProvider.notifier) - .goForward(); - if (clock.active) { - ref.read(overTheBoardClockProvider.notifier).switchSide( - newSideToMove: gameState.turn.opposite, - addIncrement: false, - ); + onTap: + gameState.canGoForward + ? () { + ref.read(overTheBoardGameControllerProvider.notifier).goForward(); + if (clock.active) { + ref + .read(overTheBoardClockProvider.notifier) + .switchSide(newSideToMove: gameState.turn.opposite, addIncrement: false); + } } - } - : null, + : null, icon: CupertinoIcons.chevron_forward, ), ], @@ -268,11 +255,7 @@ class _BottomBar extends ConsumerWidget { } class _Player extends ConsumerWidget { - const _Player({ - required this.clockKey, - required this.side, - required this.upsideDown, - }); + const _Player({required this.clockKey, required this.side, required this.upsideDown}); final Side side; @@ -287,40 +270,35 @@ class _Player extends ConsumerWidget { final clock = ref.watch(overTheBoardClockProvider); final brightness = ref.watch(currentBrightnessProvider); - final clockStyle = brightness == Brightness.dark - ? ClockStyle.darkThemeStyle - : ClockStyle.lightThemeStyle; + final clockStyle = + brightness == Brightness.dark ? ClockStyle.darkThemeStyle : ClockStyle.lightThemeStyle; return RotatedBox( quarterTurns: upsideDown ? 2 : 0, child: GamePlayer( player: Player( onGame: true, - user: LightUser( - id: UserId(side.name), - name: side.name.capitalize(), - ), + user: LightUser(id: UserId(side.name), name: side.name.capitalize()), ), - materialDiff: boardPreferences.materialDifferenceFormat.visible - ? gameState.currentMaterialDiff(side) - : null, + materialDiff: + boardPreferences.materialDifferenceFormat.visible + ? gameState.currentMaterialDiff(side) + : null, materialDifferenceFormat: boardPreferences.materialDifferenceFormat, shouldLinkToUserProfile: false, - clock: clock.timeIncrement.isInfinite - ? null - : Clock( - timeLeft: Duration( - milliseconds: max(0, clock.timeLeft(side)!.inMilliseconds), - ), - key: clockKey, - active: clock.activeClock == side, - clockStyle: clockStyle, - // https://github.com/lichess-org/mobile/issues/785#issuecomment-2183903498 - emergencyThreshold: Duration( - seconds: - (clock.timeIncrement.time * 0.125).clamp(10, 60).toInt(), + clock: + clock.timeIncrement.isInfinite + ? null + : Clock( + timeLeft: Duration(milliseconds: max(0, clock.timeLeft(side)!.inMilliseconds)), + key: clockKey, + active: clock.activeClock == side, + clockStyle: clockStyle, + // https://github.com/lichess-org/mobile/issues/785#issuecomment-2183903498 + emergencyThreshold: Duration( + seconds: (clock.timeIncrement.time * 0.125).clamp(10, 60).toInt(), + ), ), - ), ), ); } diff --git a/lib/src/view/play/challenge_list_item.dart b/lib/src/view/play/challenge_list_item.dart index cc17fb53b6..c8d55a696a 100644 --- a/lib/src/view/play/challenge_list_item.dart +++ b/lib/src/view/play/challenge_list_item.dart @@ -43,29 +43,24 @@ class ChallengeListItem extends ConsumerWidget { final me = ref.watch(authSessionProvider)?.user; final isMyChallenge = me != null && me.id == challengerUser.id; - final color = isMyChallenge - ? context.lichessColors.good.withValues(alpha: 0.2) - : null; + final color = isMyChallenge ? context.lichessColors.good.withValues(alpha: 0.2) : null; final isFromPosition = challenge.variant == Variant.fromPosition; final leading = Icon(challenge.perf.icon, size: 36); - final trailing = challenge.challenger?.lagRating != null - ? LagIndicator(lagRating: challenge.challenger!.lagRating!) - : null; - final title = isMyChallenge - // shows destUser if it exists, otherwise shows the challenger (me) - // if no destUser, it's an open challenge I sent - ? UserFullNameWidget( - user: challenge.destUser != null - ? challenge.destUser!.user - : challengerUser, - rating: challenge.destUser?.rating ?? challenge.challenger?.rating, - ) - : UserFullNameWidget( - user: challengerUser, - rating: challenge.challenger?.rating, - ); + final trailing = + challenge.challenger?.lagRating != null + ? LagIndicator(lagRating: challenge.challenger!.lagRating!) + : null; + final title = + isMyChallenge + // shows destUser if it exists, otherwise shows the challenger (me) + // if no destUser, it's an open challenge I sent + ? UserFullNameWidget( + user: challenge.destUser != null ? challenge.destUser!.user : challengerUser, + rating: challenge.destUser?.rating ?? challenge.challenger?.rating, + ) + : UserFullNameWidget(user: challengerUser, rating: challenge.challenger?.rating); final subtitle = Text(challenge.description(context.l10n)); final screenWidth = MediaQuery.sizeOf(context).width; @@ -73,9 +68,7 @@ class ChallengeListItem extends ConsumerWidget { return Container( color: color, child: Slidable( - enabled: onAccept != null || - onDecline != null || - (isMyChallenge && onCancel != null), + enabled: onAccept != null || onDecline != null || (isMyChallenge && onCancel != null), dragStartBehavior: DragStartBehavior.start, endActionPane: ActionPane( motion: const StretchMotion(), @@ -93,50 +86,45 @@ class ChallengeListItem extends ConsumerWidget { if (onDecline != null || (isMyChallenge && onCancel != null)) SlidableAction( icon: Icons.close, - onPressed: isMyChallenge - ? (_) => onCancel!() - : onDecline != null + onPressed: + isMyChallenge + ? (_) => onCancel!() + : onDecline != null ? (_) => onDecline!(null) : null, spacing: 8.0, backgroundColor: context.lichessColors.error, foregroundColor: Colors.white, - label: - isMyChallenge ? context.l10n.cancel : context.l10n.decline, + label: isMyChallenge ? context.l10n.cancel : context.l10n.decline, ), ], ), - child: isFromPosition - ? ExpansionTile( - childrenPadding: Styles.bodyPadding - .subtract(const EdgeInsets.only(top: 8.0)), - leading: leading, - title: title, - subtitle: subtitle, - children: [ - if (challenge.variant == Variant.fromPosition && - challenge.initialFen != null) - BoardThumbnail( - size: min( - 400, - screenWidth - 2 * Styles.bodyPadding.horizontal, + child: + isFromPosition + ? ExpansionTile( + childrenPadding: Styles.bodyPadding.subtract(const EdgeInsets.only(top: 8.0)), + leading: leading, + title: title, + subtitle: subtitle, + children: [ + if (challenge.variant == Variant.fromPosition && challenge.initialFen != null) + BoardThumbnail( + size: min(400, screenWidth - 2 * Styles.bodyPadding.horizontal), + orientation: + challenge.sideChoice == SideChoice.white ? Side.white : Side.black, + fen: challenge.initialFen!, + onTap: onPressed, ), - orientation: challenge.sideChoice == SideChoice.white - ? Side.white - : Side.black, - fen: challenge.initialFen!, - onTap: onPressed, - ), - ], - // onTap: onPressed, - ) - : AdaptiveListTile( - leading: leading, - title: title, - subtitle: subtitle, - trailing: trailing, - onTap: onPressed, - ), + ], + // onTap: onPressed, + ) + : AdaptiveListTile( + leading: leading, + title: title, + subtitle: subtitle, + trailing: trailing, + onTap: onPressed, + ), ), ); } @@ -164,13 +152,15 @@ class CorrespondenceChallengeListItem extends StatelessWidget { status: ChallengeStatus.created, variant: challenge.variant, speed: Speed.correspondence, - timeControl: challenge.days != null - ? ChallengeTimeControlType.correspondence - : ChallengeTimeControlType.unlimited, + timeControl: + challenge.days != null + ? ChallengeTimeControlType.correspondence + : ChallengeTimeControlType.unlimited, rated: challenge.rated, - sideChoice: challenge.side == null - ? SideChoice.random - : challenge.side == Side.white + sideChoice: + challenge.side == null + ? SideChoice.random + : challenge.side == Side.white ? SideChoice.white : SideChoice.black, days: challenge.days, diff --git a/lib/src/view/play/common_play_widgets.dart b/lib/src/view/play/common_play_widgets.dart index 846c7da11b..abe9d2b5e9 100644 --- a/lib/src/view/play/common_play_widgets.dart +++ b/lib/src/view/play/common_play_widgets.dart @@ -49,9 +49,7 @@ class _PlayRatingRangeState extends State { opacity: isRatingRangeAvailable ? 1 : 0.5, child: PlatformListTile( harmonizeCupertinoTitleStyle: true, - title: Text( - context.l10n.ratingRange, - ), + title: Text(context.l10n.ratingRange), subtitle: Row( mainAxisSize: MainAxisSize.max, children: [ @@ -61,27 +59,25 @@ class _PlayRatingRangeState extends State { NonLinearSlider( value: _subtract, values: kSubtractingRatingRange, - onChange: Theme.of(context).platform == TargetPlatform.iOS - ? (num value) { - setState(() { - _subtract = value.toInt(); - }); - } - : null, - onChangeEnd: isRatingRangeAvailable - ? (num value) { - widget.onRatingDeltaChange( - value.toInt(), - value == 0 && _add == 0 - ? kAddingRatingRange[1] - : _add, - ); - } - : null, - ), - Center( - child: Text('${_subtract == 0 ? '-' : ''}$_subtract'), + onChange: + Theme.of(context).platform == TargetPlatform.iOS + ? (num value) { + setState(() { + _subtract = value.toInt(); + }); + } + : null, + onChangeEnd: + isRatingRangeAvailable + ? (num value) { + widget.onRatingDeltaChange( + value.toInt(), + value == 0 && _add == 0 ? kAddingRatingRange[1] : _add, + ); + } + : null, ), + Center(child: Text('${_subtract == 0 ? '-' : ''}$_subtract')), ], ), ), @@ -98,28 +94,27 @@ class _PlayRatingRangeState extends State { NonLinearSlider( value: _add, values: kAddingRatingRange, - onChange: Theme.of(context).platform == TargetPlatform.iOS - ? (num value) { - setState(() { - _add = value.toInt(); - }); - } - : null, - onChangeEnd: isRatingRangeAvailable - ? (num value) { - widget.onRatingDeltaChange( - value == 0 && _subtract == 0 - ? kSubtractingRatingRange[ - kSubtractingRatingRange.length - 2] - : _subtract, - value.toInt(), - ); - } - : null, - ), - Center( - child: Text('+$_add'), + onChange: + Theme.of(context).platform == TargetPlatform.iOS + ? (num value) { + setState(() { + _add = value.toInt(); + }); + } + : null, + onChangeEnd: + isRatingRangeAvailable + ? (num value) { + widget.onRatingDeltaChange( + value == 0 && _subtract == 0 + ? kSubtractingRatingRange[kSubtractingRatingRange.length - 2] + : _subtract, + value.toInt(), + ); + } + : null, ), + Center(child: Text('+$_add')), ], ), ), diff --git a/lib/src/view/play/create_challenge_screen.dart b/lib/src/view/play/create_challenge_screen.dart index 4e1e0ad70f..932228d876 100644 --- a/lib/src/view/play/create_challenge_screen.dart +++ b/lib/src/view/play/create_challenge_screen.dart @@ -38,9 +38,7 @@ class CreateChallengeScreen extends StatelessWidget { @override Widget build(BuildContext context) { return PlatformScaffold( - appBar: PlatformAppBar( - title: Text(context.l10n.challengeChallengesX(user.name)), - ), + appBar: PlatformAppBar(title: Text(context.l10n.challengeChallengesX(user.name))), body: _ChallengeBody(user), ); } @@ -84,12 +82,12 @@ class _ChallengeBodyState extends ConsumerState<_ChallengeBody> { final isValidTimeControl = preferences.timeControl != ChallengeTimeControlType.clock || - preferences.clock.time > Duration.zero || - preferences.clock.increment > Duration.zero; + preferences.clock.time > Duration.zero || + preferences.clock.increment > Duration.zero; final isValidPosition = (fromPositionFenInput != null && fromPositionFenInput!.isNotEmpty) || - preferences.variant != Variant.fromPosition; + preferences.variant != Variant.fromPosition; return accountAsync.when( data: (account) { @@ -98,9 +96,10 @@ class _ChallengeBodyState extends ConsumerState<_ChallengeBody> { return Center( child: ListView( shrinkWrap: true, - padding: Theme.of(context).platform == TargetPlatform.iOS - ? Styles.sectionBottomPadding - : Styles.verticalBodyPadding, + padding: + Theme.of(context).platform == TargetPlatform.iOS + ? Styles.sectionBottomPadding + : Styles.verticalBodyPadding, children: [ PlatformListTile( harmonizeCupertinoTitleStyle: true, @@ -114,21 +113,14 @@ class _ChallengeBodyState extends ConsumerState<_ChallengeBody> { ChallengeTimeControlType.correspondence, ], selectedItem: preferences.timeControl, - labelBuilder: (ChallengeTimeControlType timeControl) => - Text( - switch (timeControl) { - ChallengeTimeControlType.clock => - context.l10n.realTime, - ChallengeTimeControlType.correspondence => - context.l10n.correspondence, - ChallengeTimeControlType.unlimited => - context.l10n.unlimited, - }, - ), + labelBuilder: + (ChallengeTimeControlType timeControl) => Text(switch (timeControl) { + ChallengeTimeControlType.clock => context.l10n.realTime, + ChallengeTimeControlType.correspondence => context.l10n.correspondence, + ChallengeTimeControlType.unlimited => context.l10n.unlimited, + }), onSelectedItemChanged: (ChallengeTimeControlType value) { - ref - .read(challengePreferencesProvider.notifier) - .setTimeControl(value); + ref.read(challengePreferencesProvider.notifier).setTimeControl(value); }, ); }, @@ -152,10 +144,7 @@ class _ChallengeBodyState extends ConsumerState<_ChallengeBody> { text: '${context.l10n.minutesPerSide}: ', children: [ TextSpan( - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - ), + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18), text: clockLabelInMinutes(seconds), ), ], @@ -168,10 +157,10 @@ class _ChallengeBodyState extends ConsumerState<_ChallengeBody> { onChange: Theme.of(context).platform == TargetPlatform.iOS ? (num value) { - setState(() { - seconds = value.toInt(); - }); - } + setState(() { + seconds = value.toInt(); + }); + } : null, onChangeEnd: (num value) { setState(() { @@ -192,8 +181,7 @@ class _ChallengeBodyState extends ConsumerState<_ChallengeBody> { ), Builder( builder: (context) { - int incrementSeconds = - preferences.clock.increment.inSeconds; + int incrementSeconds = preferences.clock.increment.inSeconds; return StatefulBuilder( builder: (context, setState) { return PlatformListTile( @@ -203,10 +191,7 @@ class _ChallengeBodyState extends ConsumerState<_ChallengeBody> { text: '${context.l10n.incrementInSeconds}: ', children: [ TextSpan( - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - ), + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18), text: incrementSeconds.toString(), ), ], @@ -218,10 +203,10 @@ class _ChallengeBodyState extends ConsumerState<_ChallengeBody> { onChange: Theme.of(context).platform == TargetPlatform.iOS ? (num value) { - setState(() { - incrementSeconds = value.toInt(); - }); - } + setState(() { + incrementSeconds = value.toInt(); + }); + } : null, onChangeEnd: (num value) { setState(() { @@ -253,10 +238,7 @@ class _ChallengeBodyState extends ConsumerState<_ChallengeBody> { text: '${context.l10n.daysPerTurn}: ', children: [ TextSpan( - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - ), + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18), text: _daysLabel(daysPerTurn), ), ], @@ -269,10 +251,10 @@ class _ChallengeBodyState extends ConsumerState<_ChallengeBody> { onChange: Theme.of(context).platform == TargetPlatform.iOS ? (num value) { - setState(() { - daysPerTurn = value.toInt(); - }); - } + setState(() { + daysPerTurn = value.toInt(); + }); + } : null, onChangeEnd: (num value) { setState(() { @@ -296,17 +278,11 @@ class _ChallengeBodyState extends ConsumerState<_ChallengeBody> { onPressed: () { showChoicePicker( context, - choices: [ - Variant.standard, - Variant.chess960, - Variant.fromPosition, - ], + choices: [Variant.standard, Variant.chess960, Variant.fromPosition], selectedItem: preferences.variant, labelBuilder: (Variant variant) => Text(variant.label), onSelectedItemChanged: (Variant variant) { - ref - .read(challengePreferencesProvider.notifier) - .setVariant(variant); + ref.read(challengePreferencesProvider.notifier).setVariant(variant); }, ); }, @@ -316,9 +292,7 @@ class _ChallengeBodyState extends ConsumerState<_ChallengeBody> { ExpandedSection( expand: preferences.variant == Variant.fromPosition, child: SmallBoardPreview( - orientation: preferences.sideChoice == SideChoice.black - ? Side.black - : Side.white, + orientation: preferences.sideChoice == SideChoice.black ? Side.black : Side.white, fen: fromPositionFenInput ?? kEmptyFen, description: AdaptiveTextField( maxLines: 5, @@ -330,8 +304,7 @@ class _ChallengeBodyState extends ConsumerState<_ChallengeBody> { ), ), ExpandedSection( - expand: preferences.rated == false || - preferences.variant == Variant.fromPosition, + expand: preferences.rated == false || preferences.variant == Variant.fromPosition, child: PlatformListTile( harmonizeCupertinoTitleStyle: true, title: Text(context.l10n.side), @@ -341,18 +314,13 @@ class _ChallengeBodyState extends ConsumerState<_ChallengeBody> { context, choices: SideChoice.values, selectedItem: preferences.sideChoice, - labelBuilder: (SideChoice side) => - Text(side.label(context.l10n)), + labelBuilder: (SideChoice side) => Text(side.label(context.l10n)), onSelectedItemChanged: (SideChoice side) { - ref - .read(challengePreferencesProvider.notifier) - .setSideChoice(side); + ref.read(challengePreferencesProvider.notifier).setSideChoice(side); }, ); }, - child: Text( - preferences.sideChoice.label(context.l10n), - ), + child: Text(preferences.sideChoice.label(context.l10n)), ), ), ), @@ -366,9 +334,7 @@ class _ChallengeBodyState extends ConsumerState<_ChallengeBody> { applyCupertinoTheme: true, value: preferences.rated, onChanged: (bool value) { - ref - .read(challengePreferencesProvider.notifier) - .setRated(value); + ref.read(challengePreferencesProvider.notifier).setRated(value); }, ), ), @@ -381,69 +347,56 @@ class _ChallengeBodyState extends ConsumerState<_ChallengeBody> { padding: const EdgeInsets.symmetric(horizontal: 20.0), child: FatButton( semanticsLabel: context.l10n.challengeChallengeToPlay, - onPressed: timeControl == ChallengeTimeControlType.clock - ? isValidTimeControl && isValidPosition - ? () { - pushPlatformRoute( - context, - rootNavigator: true, - builder: (BuildContext context) { - return GameScreen( - challenge: preferences.makeRequest( - widget.user, - preferences.variant != - Variant.fromPosition - ? null - : fromPositionFenInput, - ), - ); - }, - ); - } - : null - : timeControl == - ChallengeTimeControlType.correspondence && - snapshot.connectionState != - ConnectionState.waiting + onPressed: + timeControl == ChallengeTimeControlType.clock + ? isValidTimeControl && isValidPosition + ? () { + pushPlatformRoute( + context, + rootNavigator: true, + builder: (BuildContext context) { + return GameScreen( + challenge: preferences.makeRequest( + widget.user, + preferences.variant != Variant.fromPosition + ? null + : fromPositionFenInput, + ), + ); + }, + ); + } + : null + : timeControl == ChallengeTimeControlType.correspondence && + snapshot.connectionState != ConnectionState.waiting ? () async { - final createGameService = - ref.read(createGameServiceProvider); - _pendingCorrespondenceChallenge = - createGameService - .newCorrespondenceChallenge( - preferences.makeRequest( - widget.user, - preferences.variant != - Variant.fromPosition - ? null - : fromPositionFenInput, - ), - ); + final createGameService = ref.read(createGameServiceProvider); + _pendingCorrespondenceChallenge = createGameService + .newCorrespondenceChallenge( + preferences.makeRequest( + widget.user, + preferences.variant != Variant.fromPosition + ? null + : fromPositionFenInput, + ), + ); - await _pendingCorrespondenceChallenge!; + await _pendingCorrespondenceChallenge!; - if (!context.mounted) return; + if (!context.mounted) return; - Navigator.of(context).pop(); + Navigator.of(context).pop(); - // Switch to the home tab - ref - .read(currentBottomTabProvider.notifier) - .state = BottomTab.home; + // Switch to the home tab + ref.read(currentBottomTabProvider.notifier).state = BottomTab.home; - // Navigate to the challenges screen where - // the new correspondence challenge will be - // displayed - pushPlatformRoute( - context, - screen: const ChallengeRequestsScreen(), - ); - } + // Navigate to the challenges screen where + // the new correspondence challenge will be + // displayed + pushPlatformRoute(context, screen: const ChallengeRequestsScreen()); + } : null, - child: Text( - context.l10n.challengeChallengeToPlay, - style: Styles.bold, - ), + child: Text(context.l10n.challengeChallengeToPlay, style: Styles.bold), ), ); }, @@ -453,9 +406,7 @@ class _ChallengeBodyState extends ConsumerState<_ChallengeBody> { ); }, loading: () => const Center(child: CircularProgressIndicator.adaptive()), - error: (error, stackTrace) => const Center( - child: Text('Could not load account data'), - ), + error: (error, stackTrace) => const Center(child: Text('Could not load account data')), ); } @@ -467,11 +418,7 @@ class _ChallengeBodyState extends ConsumerState<_ChallengeBody> { _controller.text = data.text!; } catch (_, __) { if (mounted) { - showPlatformSnackbar( - context, - context.l10n.invalidFen, - type: SnackBarType.error, - ); + showPlatformSnackbar(context, context.l10n.invalidFen, type: SnackBarType.error); } } } diff --git a/lib/src/view/play/create_custom_game_screen.dart b/lib/src/view/play/create_custom_game_screen.dart index a1fa193a8f..e6fc127d3e 100644 --- a/lib/src/view/play/create_custom_game_screen.dart +++ b/lib/src/view/play/create_custom_game_screen.dart @@ -49,9 +49,7 @@ class CreateCustomGameScreen extends StatelessWidget { return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( automaticBackgroundVisibility: false, - backgroundColor: Styles.cupertinoAppBarColor - .resolveFrom(context) - .withValues(alpha: 0.0), + backgroundColor: Styles.cupertinoAppBarColor.resolveFrom(context).withValues(alpha: 0.0), border: null, ), child: const _CupertinoBody(), @@ -70,8 +68,7 @@ class _AndroidBody extends StatefulWidget { State<_AndroidBody> createState() => _AndroidBodyState(); } -class _AndroidBodyState extends State<_AndroidBody> - with TickerProviderStateMixin { +class _AndroidBodyState extends State<_AndroidBody> with TickerProviderStateMixin { late final TabController _tabController; @override @@ -179,17 +176,18 @@ class _CupertinoBodyState extends State<_CupertinoBody> { ); return NotificationListener( onNotification: handleScrollNotification, - child: _selectedSegment == _ViewMode.create - ? _TabView( - cupertinoTabSwitcher: tabSwitcher, - cupertinoHeaderOpacity: headerOpacity, - sliver: _CreateGameBody(setViewMode: setViewMode), - ) - : _TabView( - cupertinoTabSwitcher: tabSwitcher, - cupertinoHeaderOpacity: headerOpacity, - sliver: _ChallengesBody(setViewMode: setViewMode), - ), + child: + _selectedSegment == _ViewMode.create + ? _TabView( + cupertinoTabSwitcher: tabSwitcher, + cupertinoHeaderOpacity: headerOpacity, + sliver: _CreateGameBody(setViewMode: setViewMode), + ) + : _TabView( + cupertinoTabSwitcher: tabSwitcher, + cupertinoHeaderOpacity: headerOpacity, + sliver: _ChallengesBody(setViewMode: setViewMode), + ), ); } } @@ -207,7 +205,8 @@ class _TabView extends StatelessWidget { @override Widget build(BuildContext context) { - final edgeInsets = MediaQuery.paddingOf(context) - + final edgeInsets = + MediaQuery.paddingOf(context) - (cupertinoTabSwitcher != null ? EdgeInsets.only(top: MediaQuery.paddingOf(context).top) : EdgeInsets.zero) + @@ -224,29 +223,28 @@ class _TabView extends StatelessWidget { child: AnimatedContainer( duration: const Duration(milliseconds: 200), decoration: ShapeDecoration( - color: cupertinoHeaderOpacity == 1.0 - ? backgroundColor - : backgroundColor.withAlpha(0), + color: + cupertinoHeaderOpacity == 1.0 + ? backgroundColor + : backgroundColor.withAlpha(0), shape: LinearBorder.bottom( side: BorderSide( - color: cupertinoHeaderOpacity == 1.0 - ? const Color(0x4D000000) - : Colors.transparent, + color: + cupertinoHeaderOpacity == 1.0 + ? const Color(0x4D000000) + : Colors.transparent, width: 0.0, ), ), ), - padding: Styles.bodyPadding + - EdgeInsets.only(top: MediaQuery.paddingOf(context).top), + padding: + Styles.bodyPadding + EdgeInsets.only(top: MediaQuery.paddingOf(context).top), child: cupertinoTabSwitcher, ), ), ), ), - SliverPadding( - padding: edgeInsets, - sliver: sliver, - ), + SliverPadding(padding: edgeInsets, sliver: sliver), ], ); } @@ -270,8 +268,7 @@ class _ChallengesBodyState extends ConsumerState<_ChallengesBody> { void initState() { super.initState(); - socketClient = - ref.read(socketPoolProvider).open(Uri(path: '/lobby/socket/v5')); + socketClient = ref.read(socketPoolProvider).open(Uri(path: '/lobby/socket/v5')); _socketSubscription = socketClient.stream.listen((event) { switch (event.topic) { @@ -311,17 +308,15 @@ class _ChallengesBodyState extends ConsumerState<_ChallengesBody> { return challengesAsync.when( data: (challenges) { - final supportedChallenges = challenges - .where((challenge) => challenge.variant.isPlaySupported) - .toList(); + final supportedChallenges = + challenges.where((challenge) => challenge.variant.isPlaySupported).toList(); return SliverList.separated( itemCount: supportedChallenges.length, - separatorBuilder: (context, index) => - const PlatformDivider(height: 1, cupertinoHasLeading: true), + separatorBuilder: + (context, index) => const PlatformDivider(height: 1, cupertinoHasLeading: true), itemBuilder: (context, index) { final challenge = supportedChallenges[index]; - final isMySeek = - UserId.fromUserName(challenge.username) == session?.user.id; + final isMySeek = UserId.fromUserName(challenge.username) == session?.user.id; return CorrespondenceChallengeListItem( challenge: challenge, @@ -330,36 +325,29 @@ class _ChallengesBodyState extends ConsumerState<_ChallengesBody> { name: challenge.username, title: challenge.title, ), - onPressed: isMySeek - ? null - : session == null + onPressed: + isMySeek + ? null + : session == null ? () { - showPlatformSnackbar( - context, - context.l10n.youNeedAnAccountToDoThat, - ); - } + showPlatformSnackbar(context, context.l10n.youNeedAnAccountToDoThat); + } : () { - showConfirmDialog( - context, - title: Text(context.l10n.accept), - isDestructiveAction: true, - onConfirm: (_) { - socketClient.send( - 'joinSeek', - challenge.id.toString(), - ); - }, - ); - }, - onCancel: isMySeek - ? () { - socketClient.send( - 'cancelSeek', - challenge.id.toString(), - ); - } - : null, + showConfirmDialog( + context, + title: Text(context.l10n.accept), + isDestructiveAction: true, + onConfirm: (_) { + socketClient.send('joinSeek', challenge.id.toString()); + }, + ); + }, + onCancel: + isMySeek + ? () { + socketClient.send('cancelSeek', challenge.id.toString()); + } + : null, ); }, ); @@ -369,9 +357,10 @@ class _ChallengesBodyState extends ConsumerState<_ChallengesBody> { child: Center(child: CircularProgressIndicator.adaptive()), ); }, - error: (error, stack) => SliverFillRemaining( - child: Center(child: Text(context.l10n.mobileCustomGameJoinAGame)), - ), + error: + (error, stack) => SliverFillRemaining( + child: Center(child: Text(context.l10n.mobileCustomGameJoinAGame)), + ), ); } } @@ -392,8 +381,8 @@ class _CreateGameBodyState extends ConsumerState<_CreateGameBody> { Widget build(BuildContext context) { final accountAsync = ref.watch(accountProvider); final preferences = ref.watch(gameSetupPreferencesProvider); - final isValidTimeControl = preferences.customTimeSeconds > 0 || - preferences.customIncrementSeconds > 0; + final isValidTimeControl = + preferences.customTimeSeconds > 0 || preferences.customIncrementSeconds > 0; final realTimeSelector = [ Builder( @@ -408,10 +397,7 @@ class _CreateGameBodyState extends ConsumerState<_CreateGameBody> { text: '${context.l10n.minutesPerSide}: ', children: [ TextSpan( - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - ), + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18), text: clockLabelInMinutes(customTimeSeconds), ), ], @@ -421,13 +407,14 @@ class _CreateGameBodyState extends ConsumerState<_CreateGameBody> { value: customTimeSeconds, values: kAvailableTimesInSeconds, labelBuilder: clockLabelInMinutes, - onChange: Theme.of(context).platform == TargetPlatform.iOS - ? (num value) { - setState(() { - customTimeSeconds = value.toInt(); - }); - } - : null, + onChange: + Theme.of(context).platform == TargetPlatform.iOS + ? (num value) { + setState(() { + customTimeSeconds = value.toInt(); + }); + } + : null, onChangeEnd: (num value) { setState(() { customTimeSeconds = value.toInt(); @@ -454,10 +441,7 @@ class _CreateGameBodyState extends ConsumerState<_CreateGameBody> { text: '${context.l10n.incrementInSeconds}: ', children: [ TextSpan( - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - ), + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18), text: customIncrementSeconds.toString(), ), ], @@ -466,13 +450,14 @@ class _CreateGameBodyState extends ConsumerState<_CreateGameBody> { subtitle: NonLinearSlider( value: customIncrementSeconds, values: kAvailableIncrementsInSeconds, - onChange: Theme.of(context).platform == TargetPlatform.iOS - ? (num value) { - setState(() { - customIncrementSeconds = value.toInt(); - }); - } - : null, + onChange: + Theme.of(context).platform == TargetPlatform.iOS + ? (num value) { + setState(() { + customIncrementSeconds = value.toInt(); + }); + } + : null, onChangeEnd: (num value) { setState(() { customIncrementSeconds = value.toInt(); @@ -502,10 +487,7 @@ class _CreateGameBodyState extends ConsumerState<_CreateGameBody> { text: '${context.l10n.daysPerTurn}: ', children: [ TextSpan( - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - ), + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18), text: _daysLabel(daysPerTurn), ), ], @@ -515,13 +497,14 @@ class _CreateGameBodyState extends ConsumerState<_CreateGameBody> { value: daysPerTurn, values: kAvailableDaysPerTurn, labelBuilder: _daysLabel, - onChange: Theme.of(context).platform == TargetPlatform.iOS - ? (num value) { - setState(() { - daysPerTurn = value.toInt(); - }); - } - : null, + onChange: + Theme.of(context).platform == TargetPlatform.iOS + ? (num value) { + setState(() { + daysPerTurn = value.toInt(); + }); + } + : null, onChangeEnd: (num value) { setState(() { daysPerTurn = value.toInt(); @@ -540,169 +523,151 @@ class _CreateGameBodyState extends ConsumerState<_CreateGameBody> { return accountAsync.when( data: (account) { - final timeControl = account == null - ? TimeControl.realTime - : preferences.customTimeControl; + final timeControl = account == null ? TimeControl.realTime : preferences.customTimeControl; - final userPerf = account?.perfs[timeControl == TimeControl.realTime - ? preferences.perfFromCustom - : Perf.correspondence]; + final userPerf = + account?.perfs[timeControl == TimeControl.realTime + ? preferences.perfFromCustom + : Perf.correspondence]; return SliverPadding( padding: Styles.sectionBottomPadding, sliver: SliverList( - delegate: SliverChildListDelegate( - [ - if (account != null) - PlatformListTile( - harmonizeCupertinoTitleStyle: true, - title: Text(context.l10n.timeControl), - trailing: AdaptiveTextButton( - onPressed: () { - showChoicePicker( - context, - choices: [ - TimeControl.realTime, - TimeControl.correspondence, - ], - selectedItem: preferences.customTimeControl, - labelBuilder: (TimeControl timeControl) => Text( - timeControl == TimeControl.realTime - ? context.l10n.realTime - : context.l10n.correspondence, - ), - onSelectedItemChanged: (TimeControl value) { - ref - .read(gameSetupPreferencesProvider.notifier) - .setCustomTimeControl(value); - }, - ); - }, - child: Text( - preferences.customTimeControl == TimeControl.realTime - ? context.l10n.realTime - : context.l10n.correspondence, - ), - ), - ), - if (timeControl == TimeControl.realTime) - ...realTimeSelector - else - ...correspondenceSelector, + delegate: SliverChildListDelegate([ + if (account != null) PlatformListTile( harmonizeCupertinoTitleStyle: true, - title: Text(context.l10n.variant), + title: Text(context.l10n.timeControl), trailing: AdaptiveTextButton( onPressed: () { showChoicePicker( context, - choices: [Variant.standard, Variant.chess960], - selectedItem: preferences.customVariant, - labelBuilder: (Variant variant) => Text(variant.label), - onSelectedItemChanged: (Variant variant) { + choices: [TimeControl.realTime, TimeControl.correspondence], + selectedItem: preferences.customTimeControl, + labelBuilder: + (TimeControl timeControl) => Text( + timeControl == TimeControl.realTime + ? context.l10n.realTime + : context.l10n.correspondence, + ), + onSelectedItemChanged: (TimeControl value) { ref .read(gameSetupPreferencesProvider.notifier) - .setCustomVariant(variant); + .setCustomTimeControl(value); }, ); }, - child: Text(preferences.customVariant.label), - ), - ), - ExpandedSection( - expand: preferences.customRated == false, - child: PlatformListTile( - harmonizeCupertinoTitleStyle: true, - title: Text(context.l10n.side), - trailing: AdaptiveTextButton( - onPressed: null, - child: Text(SideChoice.random.label(context.l10n)), + child: Text( + preferences.customTimeControl == TimeControl.realTime + ? context.l10n.realTime + : context.l10n.correspondence, ), ), ), - if (account != null) - PlatformListTile( - harmonizeCupertinoTitleStyle: true, - title: Text(context.l10n.rated), - trailing: Switch.adaptive( - applyCupertinoTheme: true, - value: preferences.customRated, - onChanged: (bool value) { - ref - .read(gameSetupPreferencesProvider.notifier) - .setCustomRated(value); + if (timeControl == TimeControl.realTime) + ...realTimeSelector + else + ...correspondenceSelector, + PlatformListTile( + harmonizeCupertinoTitleStyle: true, + title: Text(context.l10n.variant), + trailing: AdaptiveTextButton( + onPressed: () { + showChoicePicker( + context, + choices: [Variant.standard, Variant.chess960], + selectedItem: preferences.customVariant, + labelBuilder: (Variant variant) => Text(variant.label), + onSelectedItemChanged: (Variant variant) { + ref.read(gameSetupPreferencesProvider.notifier).setCustomVariant(variant); }, - ), + ); + }, + child: Text(preferences.customVariant.label), + ), + ), + ExpandedSection( + expand: preferences.customRated == false, + child: PlatformListTile( + harmonizeCupertinoTitleStyle: true, + title: Text(context.l10n.side), + trailing: AdaptiveTextButton( + onPressed: null, + child: Text(SideChoice.random.label(context.l10n)), ), - if (userPerf != null) - PlayRatingRange( - perf: userPerf, - ratingDelta: preferences.customRatingDelta, - onRatingDeltaChange: (int subtract, int add) { - ref - .read(gameSetupPreferencesProvider.notifier) - .setCustomRatingRange(subtract, add); + ), + ), + if (account != null) + PlatformListTile( + harmonizeCupertinoTitleStyle: true, + title: Text(context.l10n.rated), + trailing: Switch.adaptive( + applyCupertinoTheme: true, + value: preferences.customRated, + onChanged: (bool value) { + ref.read(gameSetupPreferencesProvider.notifier).setCustomRated(value); }, ), - const SizedBox(height: 20), - FutureBuilder( - future: _pendingCreateGame, - builder: (context, snapshot) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 20.0), - child: FatButton( - semanticsLabel: context.l10n.createAGame, - onPressed: timeControl == TimeControl.realTime - ? isValidTimeControl - ? () { + ), + if (userPerf != null) + PlayRatingRange( + perf: userPerf, + ratingDelta: preferences.customRatingDelta, + onRatingDeltaChange: (int subtract, int add) { + ref + .read(gameSetupPreferencesProvider.notifier) + .setCustomRatingRange(subtract, add); + }, + ), + const SizedBox(height: 20), + FutureBuilder( + future: _pendingCreateGame, + builder: (context, snapshot) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: FatButton( + semanticsLabel: context.l10n.createAGame, + onPressed: + timeControl == TimeControl.realTime + ? isValidTimeControl + ? () { pushPlatformRoute( context, rootNavigator: true, builder: (BuildContext context) { return GameScreen( - seek: GameSeek.custom( - preferences, - account, - ), + seek: GameSeek.custom(preferences, account), ); }, ); } - : null - : snapshot.connectionState == - ConnectionState.waiting - ? null - : () async { - _pendingCreateGame = ref - .read(createGameServiceProvider) - .newCorrespondenceGame( - GameSeek.correspondence( - preferences, - account, - ), - ); + : null + : snapshot.connectionState == ConnectionState.waiting + ? null + : () async { + _pendingCreateGame = ref + .read(createGameServiceProvider) + .newCorrespondenceGame( + GameSeek.correspondence(preferences, account), + ); - await _pendingCreateGame; - widget.setViewMode(_ViewMode.challenges); - }, - child: - Text(context.l10n.createAGame, style: Styles.bold), - ), - ); - }, - ), - ], - ), + await _pendingCreateGame; + widget.setViewMode(_ViewMode.challenges); + }, + child: Text(context.l10n.createAGame, style: Styles.bold), + ), + ); + }, + ), + ]), ), ); }, - loading: () => const SliverFillRemaining( - child: Center(child: CircularProgressIndicator.adaptive()), - ), - error: (error, stackTrace) => const SliverFillRemaining( - child: Center( - child: Text('Could not load account data'), - ), - ), + loading: + () => + const SliverFillRemaining(child: Center(child: CircularProgressIndicator.adaptive())), + error: + (error, stackTrace) => + const SliverFillRemaining(child: Center(child: Text('Could not load account data'))), ); } } diff --git a/lib/src/view/play/create_game_options.dart b/lib/src/view/play/create_game_options.dart index c00a745c86..c5c1bdb528 100644 --- a/lib/src/view/play/create_game_options.dart +++ b/lib/src/view/play/create_game_options.dart @@ -18,37 +18,38 @@ class CreateGameOptions extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final isOnline = - ref.watch(connectivityChangesProvider).valueOrNull?.isOnline ?? false; + final isOnline = ref.watch(connectivityChangesProvider).valueOrNull?.isOnline ?? false; return Column( children: [ _Section( children: [ _CreateGamePlatformButton( - onTap: isOnline - ? () { - ref.invalidate(accountProvider); - pushPlatformRoute( - context, - title: context.l10n.custom, - builder: (_) => const CreateCustomGameScreen(), - ); - } - : null, + onTap: + isOnline + ? () { + ref.invalidate(accountProvider); + pushPlatformRoute( + context, + title: context.l10n.custom, + builder: (_) => const CreateCustomGameScreen(), + ); + } + : null, icon: Icons.tune, label: context.l10n.custom, ), _CreateGamePlatformButton( - onTap: isOnline - ? () { - pushPlatformRoute( - context, - title: context.l10n.onlineBots, - builder: (_) => const OnlineBotsScreen(), - ); - } - : null, + onTap: + isOnline + ? () { + pushPlatformRoute( + context, + title: context.l10n.onlineBots, + builder: (_) => const OnlineBotsScreen(), + ); + } + : null, icon: Icons.computer, label: context.l10n.onlineBots, ), @@ -76,35 +77,23 @@ class CreateGameOptions extends ConsumerWidget { } class _Section extends StatelessWidget { - const _Section({ - required this.children, - }); + const _Section({required this.children}); final List children; @override Widget build(BuildContext context) { return Theme.of(context).platform == TargetPlatform.iOS - ? ListSection( - hasLeading: true, - children: children, - ) + ? ListSection(hasLeading: true, children: children) : Padding( - padding: Styles.horizontalBodyPadding.add(Styles.sectionTopPadding), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: children, - ), - ); + padding: Styles.horizontalBodyPadding.add(Styles.sectionTopPadding), + child: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: children), + ); } } class _CreateGamePlatformButton extends StatelessWidget { - const _CreateGamePlatformButton({ - required this.icon, - required this.label, - required this.onTap, - }); + const _CreateGamePlatformButton({required this.icon, required this.label, required this.onTap}); final IconData icon; @@ -116,18 +105,11 @@ class _CreateGamePlatformButton extends StatelessWidget { Widget build(BuildContext context) { return Theme.of(context).platform == TargetPlatform.iOS ? PlatformListTile( - leading: Icon( - icon, - size: 28, - ), - trailing: const CupertinoListTileChevron(), - title: Text(label, style: Styles.mainListTileTitle), - onTap: onTap, - ) - : ElevatedButton.icon( - onPressed: onTap, - icon: Icon(icon), - label: Text(label), - ); + leading: Icon(icon, size: 28), + trailing: const CupertinoListTileChevron(), + title: Text(label, style: Styles.mainListTileTitle), + onTap: onTap, + ) + : ElevatedButton.icon(onPressed: onTap, icon: Icon(icon), label: Text(label)); } } diff --git a/lib/src/view/play/ongoing_games_screen.dart b/lib/src/view/play/ongoing_games_screen.dart index 7856985951..d9843a22a1 100644 --- a/lib/src/view/play/ongoing_games_screen.dart +++ b/lib/src/view/play/ongoing_games_screen.dart @@ -17,26 +17,18 @@ class OngoingGamesScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return ConsumerPlatformWidget( - ref: ref, - androidBuilder: _buildAndroid, - iosBuilder: _buildIos, - ); + return ConsumerPlatformWidget(ref: ref, androidBuilder: _buildAndroid, iosBuilder: _buildIos); } Widget _buildIos(BuildContext context, WidgetRef ref) { - return CupertinoPageScaffold( - navigationBar: const CupertinoNavigationBar(), - child: _Body(), - ); + return CupertinoPageScaffold(navigationBar: const CupertinoNavigationBar(), child: _Body()); } Widget _buildAndroid(BuildContext context, WidgetRef ref) { final ongoingGames = ref.watch(ongoingGamesProvider); return Scaffold( appBar: ongoingGames.maybeWhen( - data: (data) => - AppBar(title: Text(context.l10n.nbGamesInPlay(data.length))), + data: (data) => AppBar(title: Text(context.l10n.nbGamesInPlay(data.length))), orElse: () => AppBar(title: const SizedBox.shrink()), ), body: _Body(), @@ -49,12 +41,13 @@ class _Body extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final ongoingGames = ref.watch(ongoingGamesProvider); return ongoingGames.maybeWhen( - data: (data) => ListView( - children: [ - const SizedBox(height: 8.0), - ...data.map((game) => OngoingGamePreview(game: game)), - ], - ), + data: + (data) => ListView( + children: [ + const SizedBox(height: 8.0), + ...data.map((game) => OngoingGamePreview(game: game)), + ], + ), orElse: () => const SizedBox.shrink(), ); } @@ -84,17 +77,10 @@ class OngoingGamePreview extends ConsumerWidget { Icon( game.perf.icon, size: 34, - color: DefaultTextStyle.of(context) - .style - .color - ?.withValues(alpha: 0.6), + color: DefaultTextStyle.of(context).style.color?.withValues(alpha: 0.6), ), if (game.secondsLeft != null && game.secondsLeft! > 0) - Text( - game.isMyTurn - ? context.l10n.yourTurn - : context.l10n.waitingForOpponent, - ), + Text(game.isMyTurn ? context.l10n.yourTurn : context.l10n.waitingForOpponent), if (game.isMyTurn && game.secondsLeft != null) Text( timeago.format( @@ -108,12 +94,13 @@ class OngoingGamePreview extends ConsumerWidget { pushPlatformRoute( context, rootNavigator: true, - builder: (context) => GameScreen( - initialGameId: game.fullId, - loadingFen: game.fen, - loadingOrientation: game.orientation, - loadingLastMove: game.lastMove, - ), + builder: + (context) => GameScreen( + initialGameId: game.fullId, + loadingFen: game.fen, + loadingOrientation: game.orientation, + loadingLastMove: game.lastMove, + ), ).then((_) { if (context.mounted) { ref.invalidate(ongoingGamesProvider); diff --git a/lib/src/view/play/online_bots_screen.dart b/lib/src/view/play/online_bots_screen.dart index 356b555380..3961253ee8 100644 --- a/lib/src/view/play/online_bots_screen.dart +++ b/lib/src/view/play/online_bots_screen.dart @@ -24,23 +24,14 @@ import 'package:url_launcher/url_launcher.dart'; // TODO(#796): remove when Leela featured bots special challenges are ready // https://github.com/lichess-org/mobile/issues/796 -const _disabledBots = { - 'leelaknightodds', - 'leelaqueenodds', - 'leelaqueenforknight', - 'leelarookodds', -}; +const _disabledBots = {'leelaknightodds', 'leelaqueenodds', 'leelaqueenforknight', 'leelarookodds'}; -final _onlineBotsProvider = - FutureProvider.autoDispose>((ref) async { +final _onlineBotsProvider = FutureProvider.autoDispose>((ref) async { return ref.withClientCacheFor( (client) => UserRepository(client).getOnlineBots().then( - (bots) => bots - .whereNot( - (bot) => _disabledBots.contains(bot.id.value.toLowerCase()), - ) - .toIList(), - ), + (bots) => + bots.whereNot((bot) => _disabledBots.contains(bot.id.value.toLowerCase())).toIList(), + ), const Duration(hours: 5), ); }); @@ -51,9 +42,7 @@ class OnlineBotsScreen extends StatelessWidget { @override Widget build(BuildContext context) { return PlatformScaffold( - appBar: PlatformAppBar( - title: Text(context.l10n.onlineBots), - ), + appBar: PlatformAppBar(title: Text(context.l10n.onlineBots)), body: _Body(), ); } @@ -65,128 +54,111 @@ class _Body extends ConsumerWidget { final onlineBots = ref.watch(_onlineBotsProvider); return onlineBots.when( - data: (data) => ListView.separated( - itemCount: data.length, - separatorBuilder: (context, index) => - Theme.of(context).platform == TargetPlatform.iOS - ? Divider( - height: 0, - thickness: 0, - // equals to _kNotchedPaddingWithoutLeading constant - // See: https://github.com/flutter/flutter/blob/89ea49204b37523a16daec53b5e6fae70995929d/packages/flutter/lib/src/cupertino/list_tile.dart#L24 - indent: 28, - color: CupertinoDynamicColor.resolve( - CupertinoColors.separator, - context, - ), - ) - : const SizedBox.shrink(), - itemBuilder: (context, index) { - final bot = data[index]; - return PlatformListTile( - isThreeLine: true, - trailing: Theme.of(context).platform == TargetPlatform.iOS - ? Row( - children: [ - if (bot.verified == true) ...[ - const Icon(Icons.verified_outlined), - const SizedBox(width: 5), - ], - const CupertinoListTileChevron(), - ], - ) - : bot.verified == true - ? const Icon(Icons.verified_outlined) - : null, - title: Padding( - padding: const EdgeInsets.only(right: 5.0), - child: UserFullNameWidget( - user: bot.lightUser, - style: const TextStyle(fontWeight: FontWeight.w600), - ), - ), - subtitle: Column( - children: [ - Row( - children: - [Perf.blitz, Perf.rapid, Perf.classical].map((perf) { - final rating = bot.perfs[perf]?.rating; - final nbGames = bot.perfs[perf]?.games ?? 0; - return Padding( - padding: const EdgeInsets.only( - right: 16.0, - top: 4.0, - bottom: 4.0, - ), - child: Row( - children: [ - Icon(perf.icon, size: 16), - const SizedBox(width: 4.0), - if (rating != null && nbGames > 0) - Text( - '$rating', - style: const TextStyle( - color: Colors.grey, - ), - ) - else - const Text(' - '), - ], - ), - ); - }).toList(), + data: + (data) => ListView.separated( + itemCount: data.length, + separatorBuilder: + (context, index) => + Theme.of(context).platform == TargetPlatform.iOS + ? Divider( + height: 0, + thickness: 0, + // equals to _kNotchedPaddingWithoutLeading constant + // See: https://github.com/flutter/flutter/blob/89ea49204b37523a16daec53b5e6fae70995929d/packages/flutter/lib/src/cupertino/list_tile.dart#L24 + indent: 28, + color: CupertinoDynamicColor.resolve(CupertinoColors.separator, context), + ) + : const SizedBox.shrink(), + itemBuilder: (context, index) { + final bot = data[index]; + return PlatformListTile( + isThreeLine: true, + trailing: + Theme.of(context).platform == TargetPlatform.iOS + ? Row( + children: [ + if (bot.verified == true) ...[ + const Icon(Icons.verified_outlined), + const SizedBox(width: 5), + ], + const CupertinoListTileChevron(), + ], + ) + : bot.verified == true + ? const Icon(Icons.verified_outlined) + : null, + title: Padding( + padding: const EdgeInsets.only(right: 5.0), + child: UserFullNameWidget( + user: bot.lightUser, + style: const TextStyle(fontWeight: FontWeight.w600), + ), ), - Text( - bot.profile?.bio ?? '', - maxLines: 2, - overflow: TextOverflow.ellipsis, + subtitle: Column( + children: [ + Row( + children: + [Perf.blitz, Perf.rapid, Perf.classical].map((perf) { + final rating = bot.perfs[perf]?.rating; + final nbGames = bot.perfs[perf]?.games ?? 0; + return Padding( + padding: const EdgeInsets.only(right: 16.0, top: 4.0, bottom: 4.0), + child: Row( + children: [ + Icon(perf.icon, size: 16), + const SizedBox(width: 4.0), + if (rating != null && nbGames > 0) + Text('$rating', style: const TextStyle(color: Colors.grey)) + else + const Text(' - '), + ], + ), + ); + }).toList(), + ), + Text(bot.profile?.bio ?? '', maxLines: 2, overflow: TextOverflow.ellipsis), + ], ), - ], - ), - onTap: () { - final session = ref.read(authSessionProvider); - if (session == null) { - showPlatformSnackbar( - context, - context.l10n.challengeRegisterToSendChallenges, - type: SnackBarType.error, - ); - return; - } - pushPlatformRoute( - context, - title: context.l10n.challengeChallengesX(bot.lightUser.name), - builder: (context) => CreateChallengeScreen(bot.lightUser), - ); - }, - onLongPress: () { - showAdaptiveBottomSheet( - context: context, - useRootNavigator: true, - isDismissible: true, - isScrollControlled: true, - showDragHandle: true, - builder: (context) => _ContextMenu(bot: bot), + onTap: () { + final session = ref.read(authSessionProvider); + if (session == null) { + showPlatformSnackbar( + context, + context.l10n.challengeRegisterToSendChallenges, + type: SnackBarType.error, + ); + return; + } + pushPlatformRoute( + context, + title: context.l10n.challengeChallengesX(bot.lightUser.name), + builder: (context) => CreateChallengeScreen(bot.lightUser), + ); + }, + onLongPress: () { + showAdaptiveBottomSheet( + context: context, + useRootNavigator: true, + isDismissible: true, + isScrollControlled: true, + showDragHandle: true, + builder: (context) => _ContextMenu(bot: bot), + ); + }, ); }, - ); - }, - ), + ), loading: () => const Center(child: CircularProgressIndicator.adaptive()), error: (e, s) { debugPrint('Could not load bots: $e'); - return FullScreenRetryRequest( - onRetry: () => ref.refresh(_onlineBotsProvider), - ); + return FullScreenRetryRequest(onRetry: () => ref.refresh(_onlineBotsProvider)); }, ); } } class _ContextMenu extends ConsumerWidget { - const _ContextMenu({ - required this.bot, - }); + const _ContextMenu({required this.bot}); final User bot; @@ -195,16 +167,11 @@ class _ContextMenu extends ConsumerWidget { return BottomSheetScrollableContainer( children: [ Padding( - padding: Styles.horizontalBodyPadding.add( - const EdgeInsets.only(bottom: 16.0), - ), + padding: Styles.horizontalBodyPadding.add(const EdgeInsets.only(bottom: 16.0)), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - UserFullNameWidget( - user: bot.lightUser, - style: Styles.title, - ), + UserFullNameWidget(user: bot.lightUser, style: Styles.title), const SizedBox(height: 8.0), if (bot.profile?.bio != null) Linkify( @@ -213,22 +180,16 @@ class _ContextMenu extends ConsumerWidget { final username = link.originText.substring(1); pushPlatformRoute( context, - builder: (ctx) => UserScreen( - user: LightUser( - id: UserId.fromUserName(username), - name: username, - ), - ), + builder: + (ctx) => UserScreen( + user: LightUser(id: UserId.fromUserName(username), name: username), + ), ); } else { launchUrl(Uri.parse(link.url)); } }, - linkifiers: const [ - UrlLinkifier(), - EmailLinkifier(), - UserTagLinkifier(), - ], + linkifiers: const [UrlLinkifier(), EmailLinkifier(), UserTagLinkifier()], text: bot.profile!.bio!, maxLines: 20, overflow: TextOverflow.ellipsis, @@ -243,12 +204,7 @@ class _ContextMenu extends ConsumerWidget { const PlatformDivider(), BottomSheetContextMenuAction( onPressed: () { - pushPlatformRoute( - context, - builder: (context) => UserScreen( - user: bot.lightUser, - ), - ); + pushPlatformRoute(context, builder: (context) => UserScreen(user: bot.lightUser)); }, icon: Icons.person, child: Text(context.l10n.profile), diff --git a/lib/src/view/play/play_screen.dart b/lib/src/view/play/play_screen.dart index 5db3f41f9c..d206c3052c 100644 --- a/lib/src/view/play/play_screen.dart +++ b/lib/src/view/play/play_screen.dart @@ -13,17 +13,12 @@ class PlayScreen extends StatelessWidget { @override Widget build(BuildContext context) { return PlatformScaffold( - appBar: PlatformAppBar( - title: Text(context.l10n.play), - ), + appBar: PlatformAppBar(title: Text(context.l10n.play)), body: const SafeArea( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Padding( - padding: Styles.horizontalBodyPadding, - child: QuickGameButton(), - ), + Padding(padding: Styles.horizontalBodyPadding, child: QuickGameButton()), CreateGameOptions(), ], ), diff --git a/lib/src/view/play/quick_game_button.dart b/lib/src/view/play/quick_game_button.dart index c587b32aee..7c20db1ecf 100644 --- a/lib/src/view/play/quick_game_button.dart +++ b/lib/src/view/play/quick_game_button.dart @@ -21,8 +21,7 @@ class QuickGameButton extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final playPrefs = ref.watch(gameSetupPreferencesProvider); final session = ref.watch(authSessionProvider); - final isOnline = - ref.watch(connectivityChangesProvider).valueOrNull?.isOnline ?? false; + final isOnline = ref.watch(connectivityChangesProvider).valueOrNull?.isOnline ?? false; return Row( children: [ @@ -51,9 +50,7 @@ class QuickGameButton extends ConsumerWidget { context: context, isScrollControlled: true, showDragHandle: true, - constraints: BoxConstraints( - maxHeight: screenHeight - (screenHeight / 10), - ), + constraints: BoxConstraints(maxHeight: screenHeight - (screenHeight / 10)), builder: (BuildContext context) { return TimeControlModal( value: playPrefs.quickPairingTimeIncrement, @@ -68,52 +65,53 @@ class QuickGameButton extends ConsumerWidget { }, ), ), - if (Theme.of(context).platform == TargetPlatform.android) - const SizedBox(width: 8.0), + if (Theme.of(context).platform == TargetPlatform.android) const SizedBox(width: 8.0), Expanded( flex: kFlexGoldenRatio, - child: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoButton.tinted( - padding: const EdgeInsets.symmetric( - horizontal: 8.0, - vertical: 16.0, - ), - onPressed: isOnline - ? () { - pushPlatformRoute( - context, - rootNavigator: true, - builder: (_) => GameScreen( - seek: GameSeek.fastPairing( - playPrefs.quickPairingTimeIncrement, - session, - ), - ), - ); - } - : null, - child: Text(context.l10n.play, style: Styles.bold), - ) - : FilledButton( - onPressed: isOnline - ? () { - pushPlatformRoute( - context, - rootNavigator: true, - builder: (_) => GameScreen( - seek: GameSeek.fastPairing( - playPrefs.quickPairingTimeIncrement, - session, - ), - ), - ); - } - : null, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), + child: + Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoButton.tinted( + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 16.0), + onPressed: + isOnline + ? () { + pushPlatformRoute( + context, + rootNavigator: true, + builder: + (_) => GameScreen( + seek: GameSeek.fastPairing( + playPrefs.quickPairingTimeIncrement, + session, + ), + ), + ); + } + : null, child: Text(context.l10n.play, style: Styles.bold), + ) + : FilledButton( + onPressed: + isOnline + ? () { + pushPlatformRoute( + context, + rootNavigator: true, + builder: + (_) => GameScreen( + seek: GameSeek.fastPairing( + playPrefs.quickPairingTimeIncrement, + session, + ), + ), + ); + } + : null, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Text(context.l10n.play, style: Styles.bold), + ), ), - ), ), ], ); diff --git a/lib/src/view/play/quick_game_matrix.dart b/lib/src/view/play/quick_game_matrix.dart index f8753b27d0..d7aaff84ab 100644 --- a/lib/src/view/play/quick_game_matrix.dart +++ b/lib/src/view/play/quick_game_matrix.dart @@ -21,9 +21,8 @@ class QuickGameMatrix extends StatelessWidget { @override Widget build(BuildContext context) { final brightness = Theme.of(context).brightness; - final logoColor = brightness == Brightness.light - ? const Color(0x0F000000) - : const Color(0x80FFFFFF); + final logoColor = + brightness == Brightness.light ? const Color(0x0F000000) : const Color(0x80FFFFFF); return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -41,35 +40,18 @@ class QuickGameMatrix extends StatelessWidget { child: const Column( children: [ _SectionChoices( - choices: [ - TimeIncrement(60, 0), - TimeIncrement(120, 1), - TimeIncrement(180, 0), - ], + choices: [TimeIncrement(60, 0), TimeIncrement(120, 1), TimeIncrement(180, 0)], ), SizedBox(height: _kMatrixSpacing), _SectionChoices( - choices: [ - TimeIncrement(180, 2), - TimeIncrement(300, 0), - TimeIncrement(300, 3), - ], + choices: [TimeIncrement(180, 2), TimeIncrement(300, 0), TimeIncrement(300, 3)], ), SizedBox(height: _kMatrixSpacing), _SectionChoices( - choices: [ - TimeIncrement(600, 0), - TimeIncrement(600, 5), - TimeIncrement(900, 10), - ], + choices: [TimeIncrement(600, 0), TimeIncrement(600, 5), TimeIncrement(900, 10)], ), SizedBox(height: _kMatrixSpacing), - _SectionChoices( - choices: [ - TimeIncrement(1800, 0), - TimeIncrement(1800, 20), - ], - ), + _SectionChoices(choices: [TimeIncrement(1800, 0), TimeIncrement(1800, 20)]), ], ), ), @@ -86,41 +68,37 @@ class _SectionChoices extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final session = ref.watch(authSessionProvider); - final isOnline = - ref.watch(connectivityChangesProvider).valueOrNull?.isOnline ?? false; - final choiceWidgets = choices - .mapIndexed((index, choice) { - return [ - Expanded( - child: _ChoiceChip( - key: ValueKey(choice), - label: Text( - choice.display, - style: const TextStyle( - fontWeight: FontWeight.w500, - fontSize: 20.0, + final isOnline = ref.watch(connectivityChangesProvider).valueOrNull?.isOnline ?? false; + final choiceWidgets = + choices + .mapIndexed((index, choice) { + return [ + Expanded( + child: _ChoiceChip( + key: ValueKey(choice), + label: Text( + choice.display, + style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 20.0), + ), + speed: choice.speed, + onTap: + isOnline + ? () { + pushPlatformRoute( + context, + rootNavigator: true, + builder: + (_) => GameScreen(seek: GameSeek.fastPairing(choice, session)), + ); + } + : null, ), ), - speed: choice.speed, - onTap: isOnline - ? () { - pushPlatformRoute( - context, - rootNavigator: true, - builder: (_) => GameScreen( - seek: GameSeek.fastPairing(choice, session), - ), - ); - } - : null, - ), - ), - if (index < choices.length - 1) - const SizedBox(width: _kMatrixSpacing), - ]; - }) - .flattened - .toList(); + if (index < choices.length - 1) const SizedBox(width: _kMatrixSpacing), + ]; + }) + .flattened + .toList(); return IntrinsicHeight( child: Row( @@ -132,14 +110,15 @@ class _SectionChoices extends ConsumerWidget { Expanded( child: _ChoiceChip( label: Text(context.l10n.custom), - onTap: isOnline - ? () { - pushPlatformRoute( - context, - builder: (_) => const CreateCustomGameScreen(), - ); - } - : null, + onTap: + isOnline + ? () { + pushPlatformRoute( + context, + builder: (_) => const CreateCustomGameScreen(), + ); + } + : null, ), ), ], @@ -150,12 +129,7 @@ class _SectionChoices extends ConsumerWidget { } class _ChoiceChip extends StatefulWidget { - const _ChoiceChip({ - required this.label, - this.speed, - required this.onTap, - super.key, - }); + const _ChoiceChip({required this.label, this.speed, required this.onTap, super.key}); final Widget label; final Speed? speed; @@ -168,9 +142,10 @@ class _ChoiceChip extends StatefulWidget { class _ChoiceChipState extends State<_ChoiceChip> { @override Widget build(BuildContext context) { - final cardColor = Theme.of(context).platform == TargetPlatform.iOS - ? Styles.cupertinoCardColor.resolveFrom(context).withValues(alpha: 0.7) - : Theme.of(context).colorScheme.surfaceContainer.withValues(alpha: 0.7); + final cardColor = + Theme.of(context).platform == TargetPlatform.iOS + ? Styles.cupertinoCardColor.resolveFrom(context).withValues(alpha: 0.7) + : Theme.of(context).colorScheme.surfaceContainer.withValues(alpha: 0.7); return Opacity( opacity: widget.onTap != null ? 1.0 : 0.5, diff --git a/lib/src/view/play/time_control_modal.dart b/lib/src/view/play/time_control_modal.dart index 4abe8ea2a9..5ff39f8fa2 100644 --- a/lib/src/view/play/time_control_modal.dart +++ b/lib/src/view/play/time_control_modal.dart @@ -34,10 +34,7 @@ class TimeControlModal extends ConsumerWidget { return BottomSheetScrollableContainer( padding: Styles.bodyPadding, children: [ - Text( - context.l10n.timeControl, - style: Styles.title, - ), + Text(context.l10n.timeControl, style: Styles.title), const SizedBox(height: 26.0), _SectionChoices( value, @@ -47,10 +44,7 @@ class TimeControlModal extends ConsumerWidget { const TimeIncrement(60, 1), const TimeIncrement(120, 1), ], - title: const _SectionTitle( - title: 'Bullet', - icon: LichessIcons.bullet, - ), + title: const _SectionTitle(title: 'Bullet', icon: LichessIcons.bullet), onSelected: onSelected, ), const SizedBox(height: 20.0), @@ -62,10 +56,7 @@ class TimeControlModal extends ConsumerWidget { TimeIncrement(300, 0), TimeIncrement(300, 3), ], - title: const _SectionTitle( - title: 'Blitz', - icon: LichessIcons.blitz, - ), + title: const _SectionTitle(title: 'Blitz', icon: LichessIcons.blitz), onSelected: onSelected, ), const SizedBox(height: 20.0), @@ -77,10 +68,7 @@ class TimeControlModal extends ConsumerWidget { TimeIncrement(900, 0), TimeIncrement(900, 10), ], - title: const _SectionTitle( - title: 'Rapid', - icon: LichessIcons.rapid, - ), + title: const _SectionTitle(title: 'Rapid', icon: LichessIcons.rapid), onSelected: onSelected, ), const SizedBox(height: 20.0), @@ -92,20 +80,14 @@ class TimeControlModal extends ConsumerWidget { TimeIncrement(1800, 20), TimeIncrement(3600, 0), ], - title: const _SectionTitle( - title: 'Classical', - icon: LichessIcons.classical, - ), + title: const _SectionTitle(title: 'Classical', icon: LichessIcons.classical), onSelected: onSelected, ), const SizedBox(height: 20.0), Theme( data: Theme.of(context).copyWith(dividerColor: Colors.transparent), child: ExpansionTile( - title: _SectionTitle( - title: context.l10n.custom, - icon: Icons.tune, - ), + title: _SectionTitle(title: context.l10n.custom, icon: Icons.tune), tilePadding: EdgeInsets.zero, minTileHeight: 0, children: [ @@ -124,23 +106,20 @@ class TimeControlModal extends ConsumerWidget { value: custom.time, values: kAvailableTimesInSeconds, labelBuilder: clockLabelInMinutes, - onChange: Theme.of(context).platform == - TargetPlatform.iOS - ? (num value) { - setState(() { - custom = TimeIncrement( - value.toInt(), - custom.increment, - ); - }); - } - : null, + onChange: + Theme.of(context).platform == TargetPlatform.iOS + ? (num value) { + setState(() { + custom = TimeIncrement( + value.toInt(), + custom.increment, + ); + }); + } + : null, onChangeEnd: (num value) { setState(() { - custom = TimeIncrement( - value.toInt(), - custom.increment, - ); + custom = TimeIncrement(value.toInt(), custom.increment); }); }, ), @@ -150,8 +129,7 @@ class TimeControlModal extends ConsumerWidget { child: Center( child: Text( custom.display, - style: - Styles.timeControl.merge(Styles.bold), + style: Styles.timeControl.merge(Styles.bold), ), ), ), @@ -159,23 +137,17 @@ class TimeControlModal extends ConsumerWidget { child: NonLinearSlider( value: custom.increment, values: kAvailableIncrementsInSeconds, - onChange: Theme.of(context).platform == - TargetPlatform.iOS - ? (num value) { - setState(() { - custom = TimeIncrement( - custom.time, - value.toInt(), - ); - }); - } - : null, + onChange: + Theme.of(context).platform == TargetPlatform.iOS + ? (num value) { + setState(() { + custom = TimeIncrement(custom.time, value.toInt()); + }); + } + : null, onChangeEnd: (num value) { setState(() { - custom = TimeIncrement( - custom.time, - value.toInt(), - ); + custom = TimeIncrement(custom.time, value.toInt()); }); }, ), @@ -183,14 +155,9 @@ class TimeControlModal extends ConsumerWidget { ], ), SecondaryButton( - onPressed: custom.isInfinite - ? null - : () => onSelected(custom), + onPressed: custom.isInfinite ? null : () => onSelected(custom), semanticsLabel: 'OK', - child: Text( - context.l10n.mobileOkButton, - style: Styles.bold, - ), + child: Text(context.l10n.mobileOkButton, style: Styles.bold), ), ], ); @@ -221,46 +188,38 @@ class _SectionChoices extends StatelessWidget { @override Widget build(BuildContext context) { - final choiceWidgets = choices - .mapIndexed((index, choice) { - return [ - Expanded( - child: _ChoiceChip( - key: ValueKey(choice), - label: Text(choice.display, style: Styles.bold), - selected: selected == choice, - onSelected: (bool selected) { - if (selected) onSelected(choice); - }, - ), - ), - if (index < choices.length - 1) const SizedBox(width: 10), - ]; - }) - .flattened - .toList(); + final choiceWidgets = + choices + .mapIndexed((index, choice) { + return [ + Expanded( + child: _ChoiceChip( + key: ValueKey(choice), + label: Text(choice.display, style: Styles.bold), + selected: selected == choice, + onSelected: (bool selected) { + if (selected) onSelected(choice); + }, + ), + ), + if (index < choices.length - 1) const SizedBox(width: 10), + ]; + }) + .flattened + .toList(); if (choices.length < 4) { final placeHolders = [ const [SizedBox(width: 10)], for (int i = choices.length; i < 4; i++) - [ - const Expanded(child: SizedBox(width: 10)), - if (i < 3) const SizedBox(width: 10), - ], + [const Expanded(child: SizedBox(width: 10)), if (i < 3) const SizedBox(width: 10)], ]; choiceWidgets.addAll(placeHolders.flattened); } return Column( crossAxisAlignment: CrossAxisAlignment.start, - children: [ - title, - const SizedBox(height: 10), - Row( - children: choiceWidgets, - ), - ], + children: [title, const SizedBox(height: 10), Row(children: choiceWidgets)], ); } } @@ -281,36 +240,24 @@ class _ChoiceChip extends StatelessWidget { Widget build(BuildContext context) { return Container( decoration: BoxDecoration( - color: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoColors.secondarySystemGroupedBackground - .resolveFrom(context) - : Theme.of(context).colorScheme.surfaceContainerHighest, + color: + Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoColors.secondarySystemGroupedBackground.resolveFrom(context) + : Theme.of(context).colorScheme.surfaceContainerHighest, borderRadius: const BorderRadius.all(Radius.circular(5.0)), - border: selected - ? Border.fromBorderSide( - BorderSide( - color: Theme.of(context).colorScheme.primary, - width: 2.0, - ), - ) - : const Border.fromBorderSide( - BorderSide( - color: Colors.transparent, - width: 2.0, - ), - ), + border: + selected + ? Border.fromBorderSide( + BorderSide(color: Theme.of(context).colorScheme.primary, width: 2.0), + ) + : const Border.fromBorderSide(BorderSide(color: Colors.transparent, width: 2.0)), ), child: AdaptiveInkWell( borderRadius: const BorderRadius.all(Radius.circular(5.0)), onTap: () => onSelected(true), child: Padding( padding: const EdgeInsets.symmetric(vertical: 10.0), - child: Center( - child: DefaultTextStyle.merge( - style: Styles.timeControl, - child: label, - ), - ), + child: Center(child: DefaultTextStyle.merge(style: Styles.timeControl, child: label)), ), ), ); @@ -326,17 +273,12 @@ class _SectionTitle extends StatelessWidget { @override Widget build(BuildContext context) { return IconTheme( - data: CupertinoIconThemeData( - color: CupertinoColors.systemGrey.resolveFrom(context), - ), + data: CupertinoIconThemeData(color: CupertinoColors.systemGrey.resolveFrom(context)), child: Row( children: [ Icon(icon, size: 20.0), const SizedBox(width: 10), - Text( - title, - style: _titleStyle, - ), + Text(title, style: _titleStyle), ], ), ); diff --git a/lib/src/view/puzzle/dashboard_screen.dart b/lib/src/view/puzzle/dashboard_screen.dart index 7cb172be93..49a09e9067 100644 --- a/lib/src/view/puzzle/dashboard_screen.dart +++ b/lib/src/view/puzzle/dashboard_screen.dart @@ -26,10 +26,7 @@ class PuzzleDashboardScreen extends StatelessWidget { Widget build(BuildContext context) { return const PlatformScaffold( body: _Body(), - appBar: PlatformAppBar( - title: SizedBox.shrink(), - actions: [DaysSelector()], - ), + appBar: PlatformAppBar(title: SizedBox.shrink(), actions: [DaysSelector()]), ); } } @@ -39,27 +36,21 @@ class _Body extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return ListView( - children: [ - PuzzleDashboardWidget(), - ], - ); + return ListView(children: [PuzzleDashboardWidget()]); } } class PuzzleDashboardWidget extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final puzzleDashboard = - ref.watch(puzzleDashboardProvider(ref.watch(daysProvider).days)); + final puzzleDashboard = ref.watch(puzzleDashboardProvider(ref.watch(daysProvider).days)); return puzzleDashboard.when( data: (dashboard) { if (dashboard == null) { return const SizedBox.shrink(); } - final chartData = - dashboard.themes.take(9).sortedBy((e) => e.theme.name).toList(); + final chartData = dashboard.themes.take(9).sortedBy((e) => e.theme.name).toList(); return ListSection( header: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -67,9 +58,7 @@ class PuzzleDashboardWidget extends ConsumerWidget { Text(context.l10n.puzzlePuzzleDashboard), Text( context.l10n.puzzlePuzzleDashboardDescription, - style: Styles.subtitle.copyWith( - color: textShade(context, Styles.subtitleOpacity), - ), + style: Styles.subtitle.copyWith(color: textShade(context, Styles.subtitleOpacity)), ), ], ), @@ -77,14 +66,12 @@ class PuzzleDashboardWidget extends ConsumerWidget { cupertinoAdditionalDividerMargin: -14, children: [ Padding( - padding: Theme.of(context).platform == TargetPlatform.iOS - ? EdgeInsets.zero - : Styles.horizontalBodyPadding, + padding: + Theme.of(context).platform == TargetPlatform.iOS + ? EdgeInsets.zero + : Styles.horizontalBodyPadding, child: StatCardRow([ - StatCard( - context.l10n.performance, - value: dashboard.global.performance.toString(), - ), + StatCard(context.l10n.performance, value: dashboard.global.performance.toString()), StatCard( context.l10n .puzzleNbPlayed(dashboard.global.nb) @@ -95,8 +82,7 @@ class PuzzleDashboardWidget extends ConsumerWidget { ), StatCard( context.l10n.puzzleSolved.capitalize(), - value: - '${((dashboard.global.firstWins / dashboard.global.nb) * 100).round()}%', + value: '${((dashboard.global.firstWins / dashboard.global.nb) * 100).round()}%', ), ]), ), @@ -104,10 +90,7 @@ class PuzzleDashboardWidget extends ConsumerWidget { Padding( padding: const EdgeInsets.all(10.0), child: AspectRatio( - aspectRatio: - MediaQuery.sizeOf(context).width > FormFactor.desktop - ? 2.8 - : 1.2, + aspectRatio: MediaQuery.sizeOf(context).width > FormFactor.desktop ? 2.8 : 1.2, child: PuzzleChart(chartData), ), ), @@ -115,18 +98,13 @@ class PuzzleDashboardWidget extends ConsumerWidget { ); }, error: (e, s) { - debugPrint( - 'SEVERE: [PuzzleDashboardWidget] could not load puzzle dashboard; $e\n$s', - ); + debugPrint('SEVERE: [PuzzleDashboardWidget] could not load puzzle dashboard; $e\n$s'); return Padding( padding: Styles.bodySectionPadding, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - context.l10n.puzzlePuzzleDashboard, - style: Styles.sectionTitle, - ), + Text(context.l10n.puzzlePuzzleDashboard, style: Styles.sectionTitle), if (e is ClientException && e.message.contains('404')) Text(context.l10n.puzzleNoPuzzlesToShow) else @@ -185,8 +163,7 @@ class PuzzleChart extends StatelessWidget { @override Widget build(BuildContext context) { - final radarColor = - Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5); + final radarColor = Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5); final chartColor = Theme.of(context).colorScheme.tertiary; return RadarChart( RadarChartData( @@ -198,14 +175,13 @@ class PuzzleChart extends StatelessWidget { RadarDataSet( fillColor: chartColor.withValues(alpha: 0.2), borderColor: chartColor, - dataEntries: puzzleData - .map((theme) => RadarEntry(value: theme.performance.toDouble())) - .toList(), + dataEntries: + puzzleData.map((theme) => RadarEntry(value: theme.performance.toDouble())).toList(), ), ], - getTitle: (index, angle) => RadarChartTitle( - text: puzzleData[index].theme.l10n(context.l10n).name, - ), + getTitle: + (index, angle) => + RadarChartTitle(text: puzzleData[index].theme.l10n(context.l10n).name), titleTextStyle: const TextStyle(fontSize: 10), titlePositionPercentageOffset: 0.09, tickCount: 3, @@ -224,17 +200,18 @@ class DaysSelector extends ConsumerWidget { final day = ref.watch(daysProvider); return session != null ? AppBarTextButton( - onPressed: () => showChoicePicker( - context, - choices: Days.values, - selectedItem: day, - labelBuilder: (t) => Text(_daysL10n(context, t)), - onSelectedItemChanged: (newDay) { - ref.read(daysProvider.notifier).state = newDay; - }, - ), - child: Text(_daysL10n(context, day)), - ) + onPressed: + () => showChoicePicker( + context, + choices: Days.values, + selectedItem: day, + labelBuilder: (t) => Text(_daysL10n(context, t)), + onSelectedItemChanged: (newDay) { + ref.read(daysProvider.notifier).state = newDay; + }, + ), + child: Text(_daysL10n(context, day)), + ) : const SizedBox.shrink(); } } diff --git a/lib/src/view/puzzle/opening_screen.dart b/lib/src/view/puzzle/opening_screen.dart index b2386aab32..725614b11d 100644 --- a/lib/src/view/puzzle/opening_screen.dart +++ b/lib/src/view/puzzle/opening_screen.dart @@ -14,18 +14,18 @@ import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'puzzle_screen.dart'; -final _openingsProvider = FutureProvider.autoDispose< - (bool, IMap, IList?)>((ref) async { - final connectivity = await ref.watch(connectivityChangesProvider.future); - final savedOpenings = await ref.watch(savedOpeningBatchesProvider.future); - IList? onlineOpenings; - try { - onlineOpenings = await ref.watch(puzzleOpeningsProvider.future); - } catch (e) { - onlineOpenings = null; - } - return (connectivity.isOnline, savedOpenings, onlineOpenings); -}); +final _openingsProvider = + FutureProvider.autoDispose<(bool, IMap, IList?)>((ref) async { + final connectivity = await ref.watch(connectivityChangesProvider.future); + final savedOpenings = await ref.watch(savedOpeningBatchesProvider.future); + IList? onlineOpenings; + try { + onlineOpenings = await ref.watch(puzzleOpeningsProvider.future); + } catch (e) { + onlineOpenings = null; + } + return (connectivity.isOnline, savedOpenings, onlineOpenings); + }); class OpeningThemeScreen extends StatelessWidget { const OpeningThemeScreen({super.key}); @@ -33,9 +33,7 @@ class OpeningThemeScreen extends StatelessWidget { @override Widget build(BuildContext context) { return PlatformScaffold( - appBar: PlatformAppBar( - title: Text(context.l10n.puzzlePuzzlesByOpenings), - ), + appBar: PlatformAppBar(title: Text(context.l10n.puzzlePuzzlesByOpenings)), body: const _Body(), ); } @@ -46,11 +44,10 @@ class _Body extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final titleStyle = Theme.of(context).platform == TargetPlatform.iOS - ? TextStyle( - color: CupertinoTheme.of(context).textTheme.textStyle.color, - ) - : null; + final titleStyle = + Theme.of(context).platform == TargetPlatform.iOS + ? TextStyle(color: CupertinoTheme.of(context).textTheme.textStyle.color) + : null; final openings = ref.watch(_openingsProvider); return openings.when( @@ -60,10 +57,7 @@ class _Body extends ConsumerWidget { return ListView( children: [ for (final openingFamily in onlineOpenings) - _OpeningFamily( - openingFamily: openingFamily, - titleStyle: titleStyle, - ), + _OpeningFamily(openingFamily: openingFamily, titleStyle: titleStyle), ], ); } else { @@ -93,10 +87,7 @@ class _Body extends ConsumerWidget { } class _OpeningFamily extends ConsumerWidget { - const _OpeningFamily({ - required this.openingFamily, - required this.titleStyle, - }); + const _OpeningFamily({required this.openingFamily, required this.titleStyle}); final PuzzleOpeningFamily openingFamily; final TextStyle? titleStyle; @@ -105,62 +96,49 @@ class _OpeningFamily extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { return Theme( data: Theme.of(context).copyWith(dividerColor: Colors.transparent), - child: openingFamily.openings.isNotEmpty - ? ExpansionTile( - title: Text( - openingFamily.name, - overflow: TextOverflow.ellipsis, - style: titleStyle, - ), - subtitle: Text( - '${openingFamily.count}', - style: TextStyle( - color: textShade(context, Styles.subtitleOpacity), + child: + openingFamily.openings.isNotEmpty + ? ExpansionTile( + title: Text(openingFamily.name, overflow: TextOverflow.ellipsis, style: titleStyle), + subtitle: Text( + '${openingFamily.count}', + style: TextStyle(color: textShade(context, Styles.subtitleOpacity)), ), - ), - children: [ - ListSection( - children: [ - _OpeningTile( - name: openingFamily.name, - openingKey: openingFamily.key, - count: openingFamily.count, - titleStyle: titleStyle, - ), - ...openingFamily.openings.map( - (opening) => _OpeningTile( - name: opening.name, - openingKey: opening.key, - count: opening.count, + children: [ + ListSection( + children: [ + _OpeningTile( + name: openingFamily.name, + openingKey: openingFamily.key, + count: openingFamily.count, titleStyle: titleStyle, ), - ), - ], - ), - ], - ) - : ListTile( - title: Text( - openingFamily.name, - overflow: TextOverflow.ellipsis, - style: titleStyle, - ), - subtitle: Text( - '${openingFamily.count}', - style: TextStyle( - color: textShade(context, 0.5), + ...openingFamily.openings.map( + (opening) => _OpeningTile( + name: opening.name, + openingKey: opening.key, + count: opening.count, + titleStyle: titleStyle, + ), + ), + ], + ), + ], + ) + : ListTile( + title: Text(openingFamily.name, overflow: TextOverflow.ellipsis, style: titleStyle), + subtitle: Text( + '${openingFamily.count}', + style: TextStyle(color: textShade(context, 0.5)), ), + onTap: () { + pushPlatformRoute( + context, + rootNavigator: true, + builder: (context) => PuzzleScreen(angle: PuzzleOpening(openingFamily.key)), + ); + }, ), - onTap: () { - pushPlatformRoute( - context, - rootNavigator: true, - builder: (context) => PuzzleScreen( - angle: PuzzleOpening(openingFamily.key), - ), - ); - }, - ), ); } } @@ -181,27 +159,14 @@ class _OpeningTile extends StatelessWidget { @override Widget build(BuildContext context) { return PlatformListTile( - leading: Theme.of(context).platform == TargetPlatform.iOS - ? null - : const SizedBox.shrink(), - title: Text( - name, - overflow: TextOverflow.ellipsis, - style: titleStyle, - ), - trailing: Text( - '$count', - style: TextStyle( - color: textShade(context, Styles.subtitleOpacity), - ), - ), + leading: Theme.of(context).platform == TargetPlatform.iOS ? null : const SizedBox.shrink(), + title: Text(name, overflow: TextOverflow.ellipsis, style: titleStyle), + trailing: Text('$count', style: TextStyle(color: textShade(context, Styles.subtitleOpacity))), onTap: () { pushPlatformRoute( context, rootNavigator: true, - builder: (context) => PuzzleScreen( - angle: PuzzleOpening(openingKey), - ), + builder: (context) => PuzzleScreen(angle: PuzzleOpening(openingKey)), ); }, ); diff --git a/lib/src/view/puzzle/puzzle_feedback_widget.dart b/lib/src/view/puzzle/puzzle_feedback_widget.dart index 0b0cf7b568..02729e5ea0 100644 --- a/lib/src/view/puzzle/puzzle_feedback_widget.dart +++ b/lib/src/view/puzzle/puzzle_feedback_widget.dart @@ -11,11 +11,7 @@ import 'package:lichess_mobile/src/utils/string.dart'; import 'package:lichess_mobile/src/view/account/rating_pref_aware.dart'; class PuzzleFeedbackWidget extends ConsumerWidget { - const PuzzleFeedbackWidget({ - required this.puzzle, - required this.state, - required this.onStreak, - }); + const PuzzleFeedbackWidget({required this.puzzle, required this.state, required this.onStreak}); final Puzzle puzzle; final PuzzleState state; @@ -23,71 +19,54 @@ class PuzzleFeedbackWidget extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final pieceSet = - ref.watch(boardPreferencesProvider.select((value) => value.pieceSet)); - final boardTheme = - ref.watch(boardPreferencesProvider.select((state) => state.boardTheme)); + final pieceSet = ref.watch(boardPreferencesProvider.select((value) => value.pieceSet)); + final boardTheme = ref.watch(boardPreferencesProvider.select((state) => state.boardTheme)); final brightness = ref.watch(currentBrightnessProvider); - final piece = - state.pov == Side.white ? PieceKind.whiteKing : PieceKind.blackKing; + final piece = state.pov == Side.white ? PieceKind.whiteKing : PieceKind.blackKing; final asset = pieceSet.assets[piece]!; switch (state.mode) { case PuzzleMode.view: - final puzzleRating = - context.l10n.puzzleRatingX(puzzle.puzzle.rating.toString()); - final playedXTimes = context.l10n - .puzzlePlayedXTimes(puzzle.puzzle.plays) - .localizeNumbers(); + final puzzleRating = context.l10n.puzzleRatingX(puzzle.puzzle.rating.toString()); + final playedXTimes = context.l10n.puzzlePlayedXTimes(puzzle.puzzle.plays).localizeNumbers(); return _FeedbackTile( - leading: state.result == PuzzleResult.win - ? Icon(Icons.check, size: 36, color: context.lichessColors.good) - : null, - title: onStreak && state.result == PuzzleResult.lose - ? const Text( - 'GAME OVER', - style: TextStyle( - fontSize: 24, - letterSpacing: 2.0, - ), - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - ) - : Text( - state.result == PuzzleResult.win - ? context.l10n.puzzlePuzzleSuccess - : context.l10n.puzzlePuzzleComplete, - overflow: TextOverflow.ellipsis, - ), - subtitle: onStreak && state.result == PuzzleResult.lose - ? null - : RatingPrefAware( - orElse: Text( - '$playedXTimes.', + leading: + state.result == PuzzleResult.win + ? Icon(Icons.check, size: 36, color: context.lichessColors.good) + : null, + title: + onStreak && state.result == PuzzleResult.lose + ? const Text( + 'GAME OVER', + style: TextStyle(fontSize: 24, letterSpacing: 2.0), + textAlign: TextAlign.center, overflow: TextOverflow.ellipsis, - maxLines: 2, - ), - child: Text( - '$puzzleRating. $playedXTimes.', + ) + : Text( + state.result == PuzzleResult.win + ? context.l10n.puzzlePuzzleSuccess + : context.l10n.puzzlePuzzleComplete, overflow: TextOverflow.ellipsis, - maxLines: 2, ), - ), + subtitle: + onStreak && state.result == PuzzleResult.lose + ? null + : RatingPrefAware( + orElse: Text('$playedXTimes.', overflow: TextOverflow.ellipsis, maxLines: 2), + child: Text( + '$puzzleRating. $playedXTimes.', + overflow: TextOverflow.ellipsis, + maxLines: 2, + ), + ), ); case PuzzleMode.load: case PuzzleMode.play: if (state.feedback == PuzzleFeedback.bad) { return _FeedbackTile( - leading: Icon( - Icons.close, - size: 36, - color: context.lichessColors.error, - ), - title: Text( - context.l10n.puzzleNotTheMove, - overflow: TextOverflow.ellipsis, - ), + leading: Icon(Icons.close, size: 36, color: context.lichessColors.error), + title: Text(context.l10n.puzzleNotTheMove, overflow: TextOverflow.ellipsis), subtitle: Text( context.l10n.puzzleTrySomethingElse, overflow: TextOverflow.ellipsis, @@ -96,8 +75,7 @@ class PuzzleFeedbackWidget extends ConsumerWidget { ); } else if (state.feedback == PuzzleFeedback.good) { return _FeedbackTile( - leading: - Icon(Icons.check, size: 36, color: context.lichessColors.good), + leading: Icon(Icons.check, size: 36, color: context.lichessColors.good), title: Text(context.l10n.puzzleBestMove), subtitle: Text(context.l10n.puzzleKeepGoing), ); @@ -106,9 +84,10 @@ class PuzzleFeedbackWidget extends ConsumerWidget { leading: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(4.0), - color: brightness == Brightness.light - ? boardTheme.colors.lightSquare - : boardTheme.colors.darkSquare, + color: + brightness == Brightness.light + ? boardTheme.colors.lightSquare + : boardTheme.colors.darkSquare, ), child: Padding( padding: const EdgeInsets.all(2.0), @@ -121,10 +100,7 @@ class PuzzleFeedbackWidget extends ConsumerWidget { ), ), ), - title: Text( - context.l10n.yourTurn, - overflow: TextOverflow.ellipsis, - ), + title: Text(context.l10n.yourTurn, overflow: TextOverflow.ellipsis), subtitle: Text( state.pov == Side.white ? context.l10n.puzzleFindTheBestMoveForWhite @@ -139,11 +115,7 @@ class PuzzleFeedbackWidget extends ConsumerWidget { } class _FeedbackTile extends StatelessWidget { - const _FeedbackTile({ - this.leading, - required this.title, - this.subtitle, - }); + const _FeedbackTile({this.leading, required this.title, this.subtitle}); final Widget? leading; final Widget title; @@ -155,10 +127,7 @@ class _FeedbackTile extends StatelessWidget { return Row( children: [ - if (leading != null) ...[ - leading!, - const SizedBox(width: 16.0), - ], + if (leading != null) ...[leading!, const SizedBox(width: 16.0)], Flexible( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -167,8 +136,7 @@ class _FeedbackTile extends StatelessWidget { children: [ DefaultTextStyle.merge( style: TextStyle( - fontSize: - defaultFontSize != null ? defaultFontSize * 1.2 : null, + fontSize: defaultFontSize != null ? defaultFontSize * 1.2 : null, fontWeight: FontWeight.bold, ), child: title, diff --git a/lib/src/view/puzzle/puzzle_history_screen.dart b/lib/src/view/puzzle/puzzle_history_screen.dart index ba97ba3263..b5561ae58d 100644 --- a/lib/src/view/puzzle/puzzle_history_screen.dart +++ b/lib/src/view/puzzle/puzzle_history_screen.dart @@ -32,8 +32,7 @@ class PuzzleHistoryPreview extends ConsumerWidget { return _PreviewBoardsGrid( rowGap: 16, builder: (crossAxisCount, boardWidth) { - final cappedHistory = - maxRows != null ? history.take(crossAxisCount * maxRows!) : history; + final cappedHistory = maxRows != null ? history.take(crossAxisCount * maxRows!) : history; return cappedHistory.map((e) { final (fen, side, lastMove) = e.preview; @@ -43,10 +42,9 @@ class PuzzleHistoryPreview extends ConsumerWidget { pushPlatformRoute( context, rootNavigator: true, - builder: (_) => PuzzleScreen( - angle: const PuzzleTheme(PuzzleThemeKey.mix), - puzzleId: e.id, - ), + builder: + (_) => + PuzzleScreen(angle: const PuzzleTheme(PuzzleThemeKey.mix), puzzleId: e.id), ); }, orientation: side, @@ -54,11 +52,7 @@ class PuzzleHistoryPreview extends ConsumerWidget { lastMove: lastMove, footer: Padding( padding: const EdgeInsets.only(top: 2.0), - child: Row( - children: [ - _PuzzleResult(e), - ], - ), + child: Row(children: [_PuzzleResult(e)]), ), ); }).toList(); @@ -104,8 +98,7 @@ class _BodyState extends ConsumerState<_Body> { } void _scrollListener() { - if (_scrollController.position.pixels >= - _scrollController.position.maxScrollExtent - 300) { + if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 300) { final currentState = ref.read(puzzleActivityProvider).valueOrNull; if (currentState != null && !currentState.isLoading) { ref.read(puzzleActivityProvider.notifier).getNext(); @@ -120,17 +113,11 @@ class _BodyState extends ConsumerState<_Body> { return historyState.when( data: (state) { if (state.hasError) { - showPlatformSnackbar( - context, - 'Error loading history', - type: SnackBarType.error, - ); + showPlatformSnackbar(context, 'Error loading history', type: SnackBarType.error); } - final crossAxisCount = - MediaQuery.sizeOf(context).width > FormFactor.tablet ? 4 : 2; + final crossAxisCount = MediaQuery.sizeOf(context).width > FormFactor.tablet ? 4 : 2; final columnsGap = _kPuzzlePadding * crossAxisCount + _kPuzzlePadding; - final boardWidth = - (MediaQuery.sizeOf(context).width - columnsGap) / crossAxisCount; + final boardWidth = (MediaQuery.sizeOf(context).width - columnsGap) / crossAxisCount; // List prepared for the ListView.builder. // It includes the date headers, and puzzles are sliced into rows of `crossAxisCount` length. @@ -158,29 +145,22 @@ class _BodyState extends ConsumerState<_Body> { return Padding( padding: const EdgeInsets.only(right: _kPuzzlePadding), child: Row( - children: element - .map( - (e) => PuzzleHistoryBoard( - e as PuzzleHistoryEntry, - boardWidth, - ), - ) - .toList(), + children: + element + .map((e) => PuzzleHistoryBoard(e as PuzzleHistoryEntry, boardWidth)) + .toList(), ), ); } else if (element is DateTime) { - final title = DateTime.now().difference(element).inDays >= 15 - ? _dateFormatter.format(element) - : timeago.format(element); + final title = + DateTime.now().difference(element).inDays >= 15 + ? _dateFormatter.format(element) + : timeago.format(element); return Padding( - padding: const EdgeInsets.only(left: _kPuzzlePadding) - .add(Styles.sectionTopPadding), + padding: const EdgeInsets.only(left: _kPuzzlePadding).add(Styles.sectionTopPadding), child: Text( title, - style: const TextStyle( - fontWeight: FontWeight.w600, - fontSize: 18, - ), + style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 18), ), ); } else { @@ -190,9 +170,7 @@ class _BodyState extends ConsumerState<_Body> { ); }, error: (e, s) { - debugPrint( - 'SEVERE: [PuzzleHistoryScreen] could not load puzzle history', - ); + debugPrint('SEVERE: [PuzzleHistoryScreen] could not load puzzle history'); return const Center(child: Text('Could not load Puzzle History')); }, loading: () => const CenterLoadingIndicator(), @@ -221,19 +199,15 @@ class PuzzleHistoryBoard extends ConsumerWidget { pushPlatformRoute( context, rootNavigator: true, - builder: (ctx) => PuzzleScreen( - angle: const PuzzleTheme(PuzzleThemeKey.mix), - puzzleId: puzzle.id, - ), + builder: + (ctx) => + PuzzleScreen(angle: const PuzzleTheme(PuzzleThemeKey.mix), puzzleId: puzzle.id), ); }, orientation: turn, fen: fen, lastMove: lastMove, - footer: Padding( - padding: const EdgeInsets.only(top: 2), - child: _PuzzleResult(puzzle), - ), + footer: Padding(padding: const EdgeInsets.only(top: 2), child: _PuzzleResult(puzzle)), ), ); } @@ -247,14 +221,12 @@ class _PuzzleResult extends StatelessWidget { @override Widget build(BuildContext context) { return ColoredBox( - color: entry.win - ? context.lichessColors.good.withValues(alpha: 0.7) - : context.lichessColors.error.withValues(alpha: 0.7), + color: + entry.win + ? context.lichessColors.good.withValues(alpha: 0.7) + : context.lichessColors.error.withValues(alpha: 0.7), child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 1, - horizontal: 3, - ), + padding: const EdgeInsets.symmetric(vertical: 1, horizontal: 3), child: Row( children: [ Text( @@ -274,24 +246,13 @@ class _PuzzleResult extends StatelessWidget { Text( '${entry.solvingTime!.inSeconds}s', overflow: TextOverflow.fade, - style: const TextStyle( - color: Colors.white, - fontSize: 10, - height: 1.0, - ), + style: const TextStyle(color: Colors.white, fontSize: 10, height: 1.0), ) else Text( - (entry.win - ? context.l10n.puzzleSolved - : context.l10n.puzzleFailed) - .toUpperCase(), + (entry.win ? context.l10n.puzzleSolved : context.l10n.puzzleFailed).toUpperCase(), overflow: TextOverflow.fade, - style: const TextStyle( - fontSize: 10, - color: Colors.white, - height: 1.0, - ), + style: const TextStyle(fontSize: 10, color: Colors.white, height: 1.0), ), ], ), @@ -304,32 +265,26 @@ class _PreviewBoardsGrid extends StatelessWidget { final List Function(int, double) builder; final double rowGap; - const _PreviewBoardsGrid({ - required this.builder, - required this.rowGap, - }); + const _PreviewBoardsGrid({required this.builder, required this.rowGap}); @override Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { - final crossAxisCount = constraints.maxWidth > 600 - ? 4 - : constraints.maxWidth > 450 + final crossAxisCount = + constraints.maxWidth > 600 + ? 4 + : constraints.maxWidth > 450 ? 3 : 2; const columnGap = 12.0; final boardWidth = - (constraints.maxWidth - (columnGap * crossAxisCount - columnGap)) / - crossAxisCount; + (constraints.maxWidth - (columnGap * crossAxisCount - columnGap)) / crossAxisCount; final boards = builder(crossAxisCount, boardWidth); return LayoutGrid( columnSizes: List.generate(crossAxisCount, (_) => 1.fr), - rowSizes: List.generate( - (boards.length / crossAxisCount).ceil(), - (_) => auto, - ), + rowSizes: List.generate((boards.length / crossAxisCount).ceil(), (_) => auto), rowGap: rowGap, columnGap: columnGap, children: boards, diff --git a/lib/src/view/puzzle/puzzle_screen.dart b/lib/src/view/puzzle/puzzle_screen.dart index a76e2d36e7..25df10f270 100644 --- a/lib/src/view/puzzle/puzzle_screen.dart +++ b/lib/src/view/puzzle/puzzle_screen.dart @@ -49,11 +49,7 @@ class PuzzleScreen extends ConsumerStatefulWidget { /// Creates a new puzzle screen. /// /// If [puzzleId] is provided, the screen will load the puzzle with that id. Otherwise, it will load the next puzzle from the queue. - const PuzzleScreen({ - required this.angle, - this.puzzleId, - super.key, - }); + const PuzzleScreen({required this.angle, this.puzzleId, super.key}); final PuzzleAngle angle; final PuzzleId? puzzleId; @@ -91,37 +87,32 @@ class _PuzzleScreenState extends ConsumerState with RouteAware { return WakelockWidget( child: PlatformScaffold( appBar: PlatformAppBar( - actions: const [ - ToggleSoundButton(), - _PuzzleSettingsButton(), - ], + actions: const [ToggleSoundButton(), _PuzzleSettingsButton()], title: _Title(angle: widget.angle), ), - body: widget.puzzleId != null - ? _LoadPuzzleFromId(angle: widget.angle, id: widget.puzzleId!) - : _LoadNextPuzzle(angle: widget.angle), + body: + widget.puzzleId != null + ? _LoadPuzzleFromId(angle: widget.angle, id: widget.puzzleId!) + : _LoadNextPuzzle(angle: widget.angle), ), ); } } class _Title extends ConsumerWidget { - const _Title({ - required this.angle, - }); + const _Title({required this.angle}); final PuzzleAngle angle; @override Widget build(BuildContext context, WidgetRef ref) { return switch (angle) { - PuzzleTheme(themeKey: final key) => key == PuzzleThemeKey.mix - ? Text(context.l10n.puzzleDesc) - : Text(key.l10n(context.l10n).name), + PuzzleTheme(themeKey: final key) => + key == PuzzleThemeKey.mix + ? Text(context.l10n.puzzleDesc) + : Text(key.l10n(context.l10n).name), PuzzleOpening(key: final key) => ref - .watch( - puzzleOpeningNameProvider(key), - ) + .watch(puzzleOpeningNameProvider(key)) .when( data: (data) => Text(data), loading: () => const SizedBox.shrink(), @@ -151,22 +142,14 @@ class _LoadNextPuzzle extends ConsumerWidget { ), ); } else { - return _Body( - initialPuzzleContext: data, - ); + return _Body(initialPuzzleContext: data); } }, loading: () => const Center(child: CircularProgressIndicator.adaptive()), error: (e, s) { - debugPrint( - 'SEVERE: [PuzzleScreen] could not load next puzzle; $e\n$s', - ); + debugPrint('SEVERE: [PuzzleScreen] could not load next puzzle; $e\n$s'); return Center( - child: BoardTable( - fen: kEmptyFen, - orientation: Side.white, - errorMessage: e.toString(), - ), + child: BoardTable(fen: kEmptyFen, orientation: Side.white, errorMessage: e.toString()), ); }, ); @@ -194,23 +177,20 @@ class _LoadPuzzleFromId extends ConsumerWidget { ), ); }, - loading: () => const Column( - children: [ - Expanded( - child: SafeArea( - bottom: false, - child: BoardTable.empty( - showEngineGaugePlaceholder: true, + loading: + () => const Column( + children: [ + Expanded( + child: SafeArea( + bottom: false, + child: BoardTable.empty(showEngineGaugePlaceholder: true), + ), ), - ), + BottomBar.empty(), + ], ), - BottomBar.empty(), - ], - ), error: (e, s) { - debugPrint( - 'SEVERE: [PuzzleScreen] could not load next puzzle; $e\n$s', - ); + debugPrint('SEVERE: [PuzzleScreen] could not load next puzzle; $e\n$s'); return Column( children: [ Expanded( @@ -232,9 +212,7 @@ class _LoadPuzzleFromId extends ConsumerWidget { } class _Body extends ConsumerWidget { - const _Body({ - required this.initialPuzzleContext, - }); + const _Body({required this.initialPuzzleContext}); final PuzzleContext initialPuzzleContext; @@ -245,11 +223,8 @@ class _Body extends ConsumerWidget { final boardPreferences = ref.watch(boardPreferencesProvider); - final currentEvalBest = ref.watch( - engineEvaluationProvider.select((s) => s.eval?.bestMove), - ); - final evalBestMove = - (currentEvalBest ?? puzzleState.node.eval?.bestMove) as NormalMove?; + final currentEvalBest = ref.watch(engineEvaluationProvider.select((s) => s.eval?.bestMove)); + final evalBestMove = (currentEvalBest ?? puzzleState.node.eval?.bestMove) as NormalMove?; return Column( children: [ @@ -261,16 +236,15 @@ class _Body extends ConsumerWidget { fen: puzzleState.fen, lastMove: puzzleState.lastMove as NormalMove?, gameData: GameData( - playerSide: puzzleState.mode == PuzzleMode.load || - puzzleState.position.isGameOver - ? PlayerSide.none - : puzzleState.mode == PuzzleMode.view + playerSide: + puzzleState.mode == PuzzleMode.load || puzzleState.position.isGameOver + ? PlayerSide.none + : puzzleState.mode == PuzzleMode.view ? PlayerSide.both : puzzleState.pov == Side.white - ? PlayerSide.white - : PlayerSide.black, - isCheck: boardPreferences.boardHighlights && - puzzleState.position.isCheck, + ? PlayerSide.white + : PlayerSide.black, + isCheck: boardPreferences.boardHighlights && puzzleState.position.isCheck, sideToMove: puzzleState.position.turn, validMoves: puzzleState.validMoves, promotionMove: puzzleState.promotionMove, @@ -281,23 +255,25 @@ class _Body extends ConsumerWidget { ref.read(ctrlProvider.notifier).onPromotionSelection(role); }, ), - shapes: puzzleState.isEngineEnabled && evalBestMove != null - ? ISet([ - Arrow( - color: const Color(0x40003088), - orig: evalBestMove.from, - dest: evalBestMove.to, - ), - ]) - : null, - engineGauge: puzzleState.isEngineEnabled - ? ( - orientation: puzzleState.pov, - isLocalEngineAvailable: true, - position: puzzleState.position, - savedEval: puzzleState.node.eval, - ) - : null, + shapes: + puzzleState.isEngineEnabled && evalBestMove != null + ? ISet([ + Arrow( + color: const Color(0x40003088), + orig: evalBestMove.from, + dest: evalBestMove.to, + ), + ]) + : null, + engineGauge: + puzzleState.isEngineEnabled + ? ( + orientation: puzzleState.pov, + isLocalEngineAvailable: true, + position: puzzleState.position, + savedEval: puzzleState.node.eval, + ) + : null, showEngineGaugePlaceholder: true, topTable: Center( child: PuzzleFeedbackWidget( @@ -312,9 +288,7 @@ class _Body extends ConsumerWidget { if (puzzleState.glicko != null) RatingPrefAware( child: Padding( - padding: const EdgeInsets.only( - top: 10.0, - ), + padding: const EdgeInsets.only(top: 10.0), child: Row( children: [ Text(context.l10n.rating), @@ -322,7 +296,8 @@ class _Body extends ConsumerWidget { TweenAnimationBuilder( tween: Tween( begin: puzzleState.glicko!.rating, - end: puzzleState.nextContext?.glicko?.rating ?? + end: + puzzleState.nextContext?.glicko?.rating ?? puzzleState.glicko!.rating, ), duration: const Duration(milliseconds: 500), @@ -349,20 +324,14 @@ class _Body extends ConsumerWidget { ), ), ), - _BottomBar( - initialPuzzleContext: initialPuzzleContext, - ctrlProvider: ctrlProvider, - ), + _BottomBar(initialPuzzleContext: initialPuzzleContext, ctrlProvider: ctrlProvider), ], ); } } class _BottomBar extends ConsumerWidget { - const _BottomBar({ - required this.initialPuzzleContext, - required this.ctrlProvider, - }); + const _BottomBar({required this.initialPuzzleContext, required this.ctrlProvider}); final PuzzleContext initialPuzzleContext; final PuzzleControllerProvider ctrlProvider; @@ -393,9 +362,10 @@ class _BottomBar extends ConsumerWidget { icon: Icons.help, label: context.l10n.viewTheSolution, showLabel: true, - onTap: puzzleState.canViewSolution - ? () => ref.read(ctrlProvider.notifier).viewSolution() - : null, + onTap: + puzzleState.canViewSolution + ? () => ref.read(ctrlProvider.notifier).viewSolution() + : null, ), if (puzzleState.mode == PuzzleMode.view) BottomBarButton( @@ -417,8 +387,7 @@ class _BottomBar extends ConsumerWidget { if (puzzleState.mode == PuzzleMode.view) RepeatButton( triggerDelays: _repeatTriggerDelays, - onLongPress: - puzzleState.canGoBack ? () => _moveBackward(ref) : null, + onLongPress: puzzleState.canGoBack ? () => _moveBackward(ref) : null, child: BottomBarButton( onTap: puzzleState.canGoBack ? () => _moveBackward(ref) : null, label: 'Previous', @@ -440,12 +409,10 @@ class _BottomBar extends ConsumerWidget { ), if (puzzleState.mode == PuzzleMode.view) BottomBarButton( - onTap: puzzleState.mode == PuzzleMode.view && - puzzleState.nextContext != null - ? () => ref - .read(ctrlProvider.notifier) - .loadPuzzle(puzzleState.nextContext!) - : null, + onTap: + puzzleState.mode == PuzzleMode.view && puzzleState.nextContext != null + ? () => ref.read(ctrlProvider.notifier).loadPuzzle(puzzleState.nextContext!) + : null, highlighted: true, label: context.l10n.puzzleContinueTraining, icon: CupertinoIcons.play_arrow_solid, @@ -464,8 +431,7 @@ class _BottomBar extends ConsumerWidget { onPressed: (context) { launchShareDialog( context, - text: lichessUri('/training/${puzzleState.puzzle.puzzle.id}') - .toString(), + text: lichessUri('/training/${puzzleState.puzzle.puzzle.id}').toString(), ); }, ), @@ -474,24 +440,24 @@ class _BottomBar extends ConsumerWidget { onPressed: (context) { pushPlatformRoute( context, - builder: (context) => AnalysisScreen( - options: AnalysisOptions( - orientation: puzzleState.pov, - standalone: ( - pgn: ref.read(ctrlProvider.notifier).makePgn(), - isComputerAnalysisAllowed: true, - variant: Variant.standard, + builder: + (context) => AnalysisScreen( + options: AnalysisOptions( + orientation: puzzleState.pov, + standalone: ( + pgn: ref.read(ctrlProvider.notifier).makePgn(), + isComputerAnalysisAllowed: true, + variant: Variant.standard, + ), + initialMoveCursor: 0, + ), ), - initialMoveCursor: 0, - ), - ), ); }, ), BottomSheetAction( - makeLabel: (context) => Text( - context.l10n.puzzleFromGameLink(puzzleState.puzzle.game.id.value), - ), + makeLabel: + (context) => Text(context.l10n.puzzleFromGameLink(puzzleState.puzzle.game.id.value)), onPressed: (_) async { final game = await ref.read( archivedGameProvider(id: puzzleState.puzzle.game.id).future, @@ -499,11 +465,12 @@ class _BottomBar extends ConsumerWidget { if (context.mounted) { pushPlatformRoute( context, - builder: (context) => ArchivedGameScreen( - gameData: game.data, - orientation: puzzleState.pov, - initialCursor: puzzleState.puzzle.puzzle.initialPly + 1, - ), + builder: + (context) => ArchivedGameScreen( + gameData: game.data, + orientation: puzzleState.pov, + initialCursor: puzzleState.puzzle.puzzle.initialPly + 1, + ), ); } }, @@ -522,65 +489,57 @@ class _BottomBar extends ConsumerWidget { } class _DifficultySelector extends ConsumerWidget { - const _DifficultySelector({ - required this.initialPuzzleContext, - required this.ctrlProvider, - }); + const _DifficultySelector({required this.initialPuzzleContext, required this.ctrlProvider}); final PuzzleContext initialPuzzleContext; final PuzzleControllerProvider ctrlProvider; @override Widget build(BuildContext context, WidgetRef ref) { - final difficulty = ref.watch( - puzzlePreferencesProvider.select((state) => state.difficulty), - ); + final difficulty = ref.watch(puzzlePreferencesProvider.select((state) => state.difficulty)); final state = ref.watch(ctrlProvider); final connectivity = ref.watch(connectivityChangesProvider); return connectivity.when( - data: (data) => StatefulBuilder( - builder: (BuildContext context, StateSetter setState) { - PuzzleDifficulty selectedDifficulty = difficulty; - return BottomBarButton( - icon: Icons.tune, - label: puzzleDifficultyL10n(context, difficulty), - tooltip: context.l10n.puzzleDifficultyLevel, - showLabel: true, - onTap: !data.isOnline || state.isChangingDifficulty - ? null - : () { - showChoicePicker( - context, - choices: PuzzleDifficulty.values, - selectedItem: difficulty, - labelBuilder: (t) => - Text(puzzleDifficultyL10n(context, t)), - onSelectedItemChanged: (PuzzleDifficulty? d) { - if (d != null) { - setState(() { - selectedDifficulty = d; + data: + (data) => StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + PuzzleDifficulty selectedDifficulty = difficulty; + return BottomBarButton( + icon: Icons.tune, + label: puzzleDifficultyL10n(context, difficulty), + tooltip: context.l10n.puzzleDifficultyLevel, + showLabel: true, + onTap: + !data.isOnline || state.isChangingDifficulty + ? null + : () { + showChoicePicker( + context, + choices: PuzzleDifficulty.values, + selectedItem: difficulty, + labelBuilder: (t) => Text(puzzleDifficultyL10n(context, t)), + onSelectedItemChanged: (PuzzleDifficulty? d) { + if (d != null) { + setState(() { + selectedDifficulty = d; + }); + } + }, + ).then((_) async { + if (selectedDifficulty == difficulty) { + return; + } + final nextContext = await ref + .read(ctrlProvider.notifier) + .changeDifficulty(selectedDifficulty); + if (context.mounted && nextContext != null) { + ref.read(ctrlProvider.notifier).loadPuzzle(nextContext); + } }); - } - }, - ).then( - (_) async { - if (selectedDifficulty == difficulty) { - return; - } - final nextContext = await ref - .read(ctrlProvider.notifier) - .changeDifficulty(selectedDifficulty); - if (context.mounted && nextContext != null) { - ref - .read(ctrlProvider.notifier) - .loadPuzzle(nextContext); - } - }, - ); - }, - ); - }, - ), + }, + ); + }, + ), loading: () => const ButtonLoadingIndicator(), error: (_, __) => const SizedBox.shrink(), ); @@ -593,16 +552,15 @@ class _PuzzleSettingsButton extends StatelessWidget { @override Widget build(BuildContext context) { return AppBarIconButton( - onPressed: () => showAdaptiveBottomSheet( - context: context, - isDismissible: true, - isScrollControlled: true, - showDragHandle: true, - constraints: BoxConstraints( - minHeight: MediaQuery.sizeOf(context).height * 0.5, - ), - builder: (_) => const PuzzleSettingsScreen(), - ), + onPressed: + () => showAdaptiveBottomSheet( + context: context, + isDismissible: true, + isScrollControlled: true, + showDragHandle: true, + constraints: BoxConstraints(minHeight: MediaQuery.sizeOf(context).height * 0.5), + builder: (_) => const PuzzleSettingsScreen(), + ), semanticsLabel: context.l10n.settingsSettings, icon: const Icon(Icons.settings), ); diff --git a/lib/src/view/puzzle/puzzle_session_widget.dart b/lib/src/view/puzzle/puzzle_session_widget.dart index 37d8511b92..841c622b05 100644 --- a/lib/src/view/puzzle/puzzle_session_widget.dart +++ b/lib/src/view/puzzle/puzzle_session_widget.dart @@ -14,17 +14,13 @@ import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/view/account/rating_pref_aware.dart'; class PuzzleSessionWidget extends ConsumerStatefulWidget { - const PuzzleSessionWidget({ - required this.initialPuzzleContext, - required this.ctrlProvider, - }); + const PuzzleSessionWidget({required this.initialPuzzleContext, required this.ctrlProvider}); final PuzzleContext initialPuzzleContext; final PuzzleControllerProvider ctrlProvider; @override - ConsumerState createState() => - PuzzleSessionWidgetState(); + ConsumerState createState() => PuzzleSessionWidgetState(); } class PuzzleSessionWidgetState extends ConsumerState { @@ -36,9 +32,7 @@ class PuzzleSessionWidgetState extends ConsumerState { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { if (lastAttemptKey.currentContext != null) { - Scrollable.ensureVisible( - lastAttemptKey.currentContext!, - ); + Scrollable.ensureVisible(lastAttemptKey.currentContext!); } }); } @@ -60,10 +54,7 @@ class PuzzleSessionWidgetState extends ConsumerState { @override Widget build(BuildContext context) { final session = ref.watch( - puzzleSessionProvider( - widget.initialPuzzleContext.userId, - widget.initialPuzzleContext.angle, - ), + puzzleSessionProvider(widget.initialPuzzleContext.userId, widget.initialPuzzleContext.angle), ); final puzzleState = ref.watch(widget.ctrlProvider); final brightness = ref.watch(currentBrightnessProvider); @@ -78,13 +69,13 @@ class PuzzleSessionWidgetState extends ConsumerState { final remainingSpace = estimateRemainingHeightLeftBoard(context); final estimatedTableHeight = remainingSpace / 2; const estimatedRatingWidgetHeight = 33.0; - final estimatedWidgetHeight = - estimatedTableHeight - estimatedRatingWidgetHeight; - final maxHeight = orientation == Orientation.portrait - ? estimatedWidgetHeight >= 60 - ? 60.0 - : 26.0 - : 60.0; + final estimatedWidgetHeight = estimatedTableHeight - estimatedRatingWidgetHeight; + final maxHeight = + orientation == Orientation.portrait + ? estimatedWidgetHeight >= 60 + ? 60.0 + : 26.0 + : 60.0; return ConstrainedBox( constraints: BoxConstraints(minHeight: 26.0, maxHeight: maxHeight), @@ -101,36 +92,33 @@ class PuzzleSessionWidgetState extends ConsumerState { isLoading: loadingPuzzleId == attempt.id, brightness: brightness, attempt: attempt, - onTap: puzzleState.puzzle.puzzle.id != attempt.id && - loadingPuzzleId == null - ? (id) async { - final provider = puzzleProvider(id); - setState(() { - loadingPuzzleId = id; - }); - try { - final puzzle = await ref.read(provider.future); - final nextContext = PuzzleContext( - userId: widget.initialPuzzleContext.userId, - angle: widget.initialPuzzleContext.angle, - puzzle: puzzle, - ); - - ref - .read(widget.ctrlProvider.notifier) - .loadPuzzle(nextContext); - } finally { - if (mounted) { - setState(() { - loadingPuzzleId = null; - }); + onTap: + puzzleState.puzzle.puzzle.id != attempt.id && loadingPuzzleId == null + ? (id) async { + final provider = puzzleProvider(id); + setState(() { + loadingPuzzleId = id; + }); + try { + final puzzle = await ref.read(provider.future); + final nextContext = PuzzleContext( + userId: widget.initialPuzzleContext.userId, + angle: widget.initialPuzzleContext.angle, + puzzle: puzzle, + ); + + ref.read(widget.ctrlProvider.notifier).loadPuzzle(nextContext); + } finally { + if (mounted) { + setState(() { + loadingPuzzleId = null; + }); + } } } - } - : null, + : null, ), - if (puzzleState.mode == PuzzleMode.view || - currentAttempt == null) + if (puzzleState.mode == PuzzleMode.view || currentAttempt == null) _SessionItem( isCurrent: currentAttempt == null, isLoading: false, @@ -163,15 +151,17 @@ class _SessionItem extends StatelessWidget { final Brightness brightness; final void Function(PuzzleId id)? onTap; - Color get good => brightness == Brightness.light - ? LichessColors.good.shade300 - : defaultTargetPlatform == TargetPlatform.iOS + Color get good => + brightness == Brightness.light + ? LichessColors.good.shade300 + : defaultTargetPlatform == TargetPlatform.iOS ? LichessColors.good.shade600 : LichessColors.good.shade400; - Color get error => brightness == Brightness.light - ? LichessColors.error.shade300 - : defaultTargetPlatform == TargetPlatform.iOS + Color get error => + brightness == Brightness.light + ? LichessColors.error.shade300 + : defaultTargetPlatform == TargetPlatform.iOS ? LichessColors.error.shade600 : LichessColors.error.shade400; @@ -188,55 +178,28 @@ class _SessionItem extends StatelessWidget { height: 26, padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), decoration: BoxDecoration( - color: isCurrent - ? Colors.grey - : attempt != null + color: + isCurrent + ? Colors.grey + : attempt != null ? attempt!.win ? good.harmonizeWith(colorScheme.primary) : error.harmonizeWith(colorScheme.primary) : next, borderRadius: const BorderRadius.all(Radius.circular(5)), ), - child: isLoading - ? const Padding( - padding: EdgeInsets.all(2.0), - child: FittedBox( - fit: BoxFit.cover, - child: CircularProgressIndicator.adaptive( - backgroundColor: Colors.white, + child: + isLoading + ? const Padding( + padding: EdgeInsets.all(2.0), + child: FittedBox( + fit: BoxFit.cover, + child: CircularProgressIndicator.adaptive(backgroundColor: Colors.white), ), - ), - ) - : attempt?.ratingDiff != null && attempt!.ratingDiff != 0 + ) + : attempt?.ratingDiff != null && attempt!.ratingDiff != 0 ? RatingPrefAware( - orElse: Icon( - attempt != null - ? attempt!.win - ? Icons.check - : Icons.close - : null, - color: Colors.white, - size: 18, - ), - child: Padding( - padding: const EdgeInsets.all(2.0), - child: FittedBox( - fit: BoxFit.fitHeight, - child: Text( - attempt!.ratingDiffString!, - maxLines: 1, - style: const TextStyle( - color: Colors.white, - height: 1, - fontFeatures: [ - FontFeature.tabularFigures(), - ], - ), - ), - ), - ), - ) - : Icon( + orElse: Icon( attempt != null ? attempt!.win ? Icons.check @@ -245,6 +208,31 @@ class _SessionItem extends StatelessWidget { color: Colors.white, size: 18, ), + child: Padding( + padding: const EdgeInsets.all(2.0), + child: FittedBox( + fit: BoxFit.fitHeight, + child: Text( + attempt!.ratingDiffString!, + maxLines: 1, + style: const TextStyle( + color: Colors.white, + height: 1, + fontFeatures: [FontFeature.tabularFigures()], + ), + ), + ), + ), + ) + : Icon( + attempt != null + ? attempt!.win + ? Icons.check + : Icons.close + : null, + color: Colors.white, + size: 18, + ), ), ); } diff --git a/lib/src/view/puzzle/puzzle_settings_screen.dart b/lib/src/view/puzzle/puzzle_settings_screen.dart index 33e14c9518..14a57c25df 100644 --- a/lib/src/view/puzzle/puzzle_settings_screen.dart +++ b/lib/src/view/puzzle/puzzle_settings_screen.dart @@ -13,9 +13,7 @@ class PuzzleSettingsScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final autoNext = ref.watch( - puzzlePreferencesProvider.select((value) => value.autoNext), - ); + final autoNext = ref.watch(puzzlePreferencesProvider.select((value) => value.autoNext)); return BottomSheetScrollableContainer( children: [ SwitchSettingTile( @@ -29,11 +27,7 @@ class PuzzleSettingsScreen extends ConsumerWidget { title: const Text('Board settings'), trailing: const Icon(CupertinoIcons.chevron_right), onTap: () { - pushPlatformRoute( - context, - fullscreenDialog: true, - screen: const BoardSettingsScreen(), - ); + pushPlatformRoute(context, fullscreenDialog: true, screen: const BoardSettingsScreen()); }, ), ], diff --git a/lib/src/view/puzzle/puzzle_tab_screen.dart b/lib/src/view/puzzle/puzzle_tab_screen.dart index 43645c36b5..a870b218b9 100644 --- a/lib/src/view/puzzle/puzzle_tab_screen.dart +++ b/lib/src/view/puzzle/puzzle_tab_screen.dart @@ -73,10 +73,7 @@ Widget _buildMainListItem( ? Styles.sectionTopPadding : EdgeInsets.zero, ), - child: Text( - context.l10n.puzzleDesc, - style: Styles.sectionTitle, - ), + child: Text(context.l10n.puzzleDesc, style: Styles.sectionTitle), ); case 2: return const DailyPuzzle(); @@ -87,9 +84,7 @@ Widget _buildMainListItem( pushPlatformRoute( context, rootNavigator: true, - builder: (context) => const PuzzleScreen( - angle: PuzzleTheme(PuzzleThemeKey.mix), - ), + builder: (context) => const PuzzleScreen(angle: PuzzleTheme(PuzzleThemeKey.mix)), ); }, ); @@ -113,10 +108,7 @@ Widget _buildMainListRemovedItem( BuildContext context, Animation animation, ) { - return SizeTransition( - sizeFactor: animation, - child: PuzzleAnglePreview(angle: angle), - ); + return SizeTransition(sizeFactor: animation, child: PuzzleAnglePreview(angle: angle)); } // display the main body list for cupertino devices, as a workaround @@ -131,8 +123,7 @@ class _CupertinoTabBody extends ConsumerStatefulWidget { } class _CupertinoTabBodyState extends ConsumerState<_CupertinoTabBody> { - final GlobalKey _listKey = - GlobalKey(); + final GlobalKey _listKey = GlobalKey(); late SliverAnimatedListModel _angles; @override @@ -176,17 +167,8 @@ class _CupertinoTabBodyState extends ConsumerState<_CupertinoTabBody> { Widget build(BuildContext context) { final isTablet = isTabletOrLarger(context); - Widget buildItem( - BuildContext context, - int index, - Animation animation, - ) => - _buildMainListItem( - context, - index, - animation, - (index) => _angles[index], - ); + Widget buildItem(BuildContext context, int index, Animation animation) => + _buildMainListItem(context, index, animation, (index) => _angles[index]); if (isTablet) { return Row( @@ -197,16 +179,11 @@ class _CupertinoTabBodyState extends ConsumerState<_CupertinoTabBody> { controller: puzzlesScrollController, slivers: [ CupertinoSliverNavigationBar( - padding: const EdgeInsetsDirectional.only( - start: 16.0, - end: 8.0, - ), + padding: const EdgeInsetsDirectional.only(start: 16.0, end: 8.0), largeTitle: Text(context.l10n.puzzles), trailing: const Row( mainAxisSize: MainAxisSize.min, - children: [ - _DashboardButton(), - ], + children: [_DashboardButton()], ), ), const SliverToBoxAdapter(child: ConnectivityBanner()), @@ -229,18 +206,13 @@ class _CupertinoTabBodyState extends ConsumerState<_CupertinoTabBody> { ), Expanded( child: CupertinoPageScaffold( - backgroundColor: - CupertinoColors.systemBackground.resolveFrom(context), + backgroundColor: CupertinoColors.systemBackground.resolveFrom(context), navigationBar: CupertinoNavigationBar( transitionBetweenRoutes: false, middle: Text(context.l10n.puzzleHistory), trailing: const _HistoryButton(), ), - child: ListView( - children: const [ - PuzzleHistoryWidget(showHeader: false), - ], - ), + child: ListView(children: const [PuzzleHistoryWidget(showHeader: false)]), ), ), ], @@ -252,18 +224,11 @@ class _CupertinoTabBodyState extends ConsumerState<_CupertinoTabBody> { controller: puzzlesScrollController, slivers: [ CupertinoSliverNavigationBar( - padding: const EdgeInsetsDirectional.only( - start: 16.0, - end: 8.0, - ), + padding: const EdgeInsetsDirectional.only(start: 16.0, end: 8.0), largeTitle: Text(context.l10n.puzzles), trailing: const Row( mainAxisSize: MainAxisSize.min, - children: [ - _DashboardButton(), - SizedBox(width: 6.0), - _HistoryButton(), - ], + children: [_DashboardButton(), SizedBox(width: 6.0), _HistoryButton()], ), ), const SliverToBoxAdapter(child: ConnectivityBanner()), @@ -335,17 +300,8 @@ class _MaterialTabBodyState extends ConsumerState<_MaterialTabBody> { Widget build(BuildContext context) { final isTablet = isTabletOrLarger(context); - Widget buildItem( - BuildContext context, - int index, - Animation animation, - ) => - _buildMainListItem( - context, - index, - animation, - (index) => _angles[index], - ); + Widget buildItem(BuildContext context, int index, Animation animation) => + _buildMainListItem(context, index, animation, (index) => _angles[index]); return PopScope( canPop: false, @@ -357,45 +313,37 @@ class _MaterialTabBodyState extends ConsumerState<_MaterialTabBody> { child: Scaffold( appBar: AppBar( title: Text(context.l10n.puzzles), - actions: const [ - _DashboardButton(), - _HistoryButton(), - ], + actions: const [_DashboardButton(), _HistoryButton()], ), - body: isTablet - ? Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: AnimatedList( - key: _listKey, - initialItemCount: _angles.length, - controller: puzzlesScrollController, - itemBuilder: buildItem, - ), - ), - Expanded( - child: ListView( - children: const [ - PuzzleHistoryWidget(), - ], + body: + isTablet + ? Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: AnimatedList( + key: _listKey, + initialItemCount: _angles.length, + controller: puzzlesScrollController, + itemBuilder: buildItem, + ), ), - ), - ], - ) - : Column( - children: [ - const ConnectivityBanner(), - Expanded( - child: AnimatedList( - key: _listKey, - controller: puzzlesScrollController, - initialItemCount: _angles.length, - itemBuilder: buildItem, + Expanded(child: ListView(children: const [PuzzleHistoryWidget()])), + ], + ) + : Column( + children: [ + const ConnectivityBanner(), + Expanded( + child: AnimatedList( + key: _listKey, + controller: puzzlesScrollController, + initialItemCount: _angles.length, + itemBuilder: buildItem, + ), ), - ), - ], - ), + ], + ), ), ); } @@ -417,21 +365,24 @@ class _PuzzleMenuListTile extends StatelessWidget { @override Widget build(BuildContext context) { return PlatformListTile( - padding: Theme.of(context).platform == TargetPlatform.iOS - ? const EdgeInsets.symmetric(vertical: 10.0, horizontal: 14.0) - : null, + padding: + Theme.of(context).platform == TargetPlatform.iOS + ? const EdgeInsets.symmetric(vertical: 10.0, horizontal: 14.0) + : null, leading: Icon( icon, size: Styles.mainListTileIconSize, - color: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoTheme.of(context).primaryColor - : Theme.of(context).colorScheme.primary, + color: + Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoTheme.of(context).primaryColor + : Theme.of(context).colorScheme.primary, ), title: Text(title, style: Styles.mainListTileTitle), subtitle: Text(subtitle, maxLines: 3), - trailing: Theme.of(context).platform == TargetPlatform.iOS - ? const CupertinoListTileChevron() - : null, + trailing: + Theme.of(context).platform == TargetPlatform.iOS + ? const CupertinoListTileChevron() + : null, onTap: onTap, ); } @@ -465,19 +416,21 @@ class _PuzzleMenu extends ConsumerWidget { child: _PuzzleMenuListTile( icon: LichessIcons.streak, title: 'Puzzle Streak', - subtitle: context.l10n.puzzleStreakDescription.characters + subtitle: + context.l10n.puzzleStreakDescription.characters .takeWhile((c) => c != '.') .toString() + (context.l10n.puzzleStreakDescription.contains('.') ? '.' : ''), - onTap: isOnline - ? () { - pushPlatformRoute( - context, - rootNavigator: true, - builder: (context) => const StreakScreen(), - ); - } - : null, + onTap: + isOnline + ? () { + pushPlatformRoute( + context, + rootNavigator: true, + builder: (context) => const StreakScreen(), + ); + } + : null, ), ), Opacity( @@ -486,15 +439,16 @@ class _PuzzleMenu extends ConsumerWidget { icon: LichessIcons.storm, title: 'Puzzle Storm', subtitle: context.l10n.mobilePuzzleStormSubtitle, - onTap: isOnline - ? () { - pushPlatformRoute( - context, - rootNavigator: true, - builder: (context) => const StormScreen(), - ); - } - : null, + onTap: + isOnline + ? () { + pushPlatformRoute( + context, + rootNavigator: true, + builder: (context) => const StormScreen(), + ); + } + : null, ), ), ], @@ -522,10 +476,7 @@ class PuzzleHistoryWidget extends ConsumerWidget { children: [ Center( child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 16.0, - horizontal: 8.0, - ), + padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 8.0), child: Text(context.l10n.puzzleNoPuzzlesToShow), ), ), @@ -533,57 +484,49 @@ class PuzzleHistoryWidget extends ConsumerWidget { ); } - final maxItems = isTablet - ? _kNumberOfHistoryItemsOnTablet - : _kNumberOfHistoryItemsOnHandset; + final maxItems = + isTablet ? _kNumberOfHistoryItemsOnTablet : _kNumberOfHistoryItemsOnHandset; return ListSection( - cupertinoBackgroundColor: - CupertinoPageScaffoldBackgroundColor.maybeOf(context), + cupertinoBackgroundColor: CupertinoPageScaffoldBackgroundColor.maybeOf(context), cupertinoClipBehavior: Clip.none, header: showHeader ? Text(context.l10n.puzzleHistory) : null, - headerTrailing: showHeader - ? NoPaddingTextButton( - onPressed: () => pushPlatformRoute( - context, - builder: (context) => const PuzzleHistoryScreen(), - ), - child: Text( - context.l10n.more, - ), - ) - : null, + headerTrailing: + showHeader + ? NoPaddingTextButton( + onPressed: + () => pushPlatformRoute( + context, + builder: (context) => const PuzzleHistoryScreen(), + ), + child: Text(context.l10n.more), + ) + : null, children: [ Padding( - padding: Theme.of(context).platform == TargetPlatform.iOS - ? EdgeInsets.zero - : Styles.horizontalBodyPadding, - child: PuzzleHistoryPreview( - recentActivity.take(maxItems).toIList(), - maxRows: 5, - ), + padding: + Theme.of(context).platform == TargetPlatform.iOS + ? EdgeInsets.zero + : Styles.horizontalBodyPadding, + child: PuzzleHistoryPreview(recentActivity.take(maxItems).toIList(), maxRows: 5), ), ], ); }, error: (e, s) { - debugPrint( - 'SEVERE: [PuzzleHistoryWidget] could not load puzzle history', - ); + debugPrint('SEVERE: [PuzzleHistoryWidget] could not load puzzle history'); return const Padding( padding: Styles.bodySectionPadding, child: Text('Could not load Puzzle history.'), ); }, - loading: () => Shimmer( - child: ShimmerLoading( - isLoading: true, - child: ListSection.loading( - itemsNumber: 5, - header: true, + loading: + () => Shimmer( + child: ShimmerLoading( + isLoading: true, + child: ListSection.loading(itemsNumber: 5, header: true), + ), ), - ), - ), ); } } @@ -597,14 +540,17 @@ class _DashboardButton extends ConsumerWidget { if (session == null) { return const SizedBox.shrink(); } - final onPressed = ref.watch(connectivityChangesProvider).whenIs( - online: () => () { - pushPlatformRoute( - context, - title: context.l10n.puzzlePuzzleDashboard, - builder: (_) => const PuzzleDashboardScreen(), - ); - }, + final onPressed = ref + .watch(connectivityChangesProvider) + .whenIs( + online: + () => () { + pushPlatformRoute( + context, + title: context.l10n.puzzlePuzzleDashboard, + builder: (_) => const PuzzleDashboardScreen(), + ); + }, offline: () => null, ); @@ -625,14 +571,17 @@ class _HistoryButton extends ConsumerWidget { if (session == null) { return const SizedBox.shrink(); } - final onPressed = ref.watch(connectivityChangesProvider).whenIs( - online: () => () { - pushPlatformRoute( - context, - title: context.l10n.puzzleHistory, - builder: (_) => const PuzzleHistoryScreen(), - ); - }, + final onPressed = ref + .watch(connectivityChangesProvider) + .whenIs( + online: + () => () { + pushPlatformRoute( + context, + title: context.l10n.puzzleHistory, + builder: (_) => const PuzzleHistoryScreen(), + ); + }, offline: () => null, ); return AppBarIconButton( @@ -656,8 +605,7 @@ class DailyPuzzle extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final isOnline = - ref.watch(connectivityChangesProvider).valueOrNull?.isOnline ?? false; + final isOnline = ref.watch(connectivityChangesProvider).valueOrNull?.isOnline ?? false; final puzzle = ref.watch(dailyPuzzleProvider); return puzzle.when( @@ -674,14 +622,9 @@ class DailyPuzzle extends ConsumerWidget { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text(context.l10n.puzzlePuzzleOfTheDay, style: Styles.boardPreviewTitle), Text( - context.l10n.puzzlePuzzleOfTheDay, - style: Styles.boardPreviewTitle, - ), - Text( - context.l10n - .puzzlePlayedXTimes(data.puzzle.plays) - .localizeNumbers(), + context.l10n.puzzlePlayedXTimes(data.puzzle.plays).localizeNumbers(), style: _puzzlePreviewSubtitleStyle(context), ), ], @@ -689,10 +632,7 @@ class DailyPuzzle extends ConsumerWidget { Icon( Icons.today, size: 34, - color: DefaultTextStyle.of(context) - .style - .color - ?.withValues(alpha: 0.6), + color: DefaultTextStyle.of(context).style.color?.withValues(alpha: 0.6), ), Text( data.puzzle.sideToMove == Side.white @@ -706,28 +646,28 @@ class DailyPuzzle extends ConsumerWidget { pushPlatformRoute( context, rootNavigator: true, - builder: (context) => PuzzleScreen( - angle: const PuzzleTheme(PuzzleThemeKey.mix), - puzzleId: data.puzzle.id, - ), + builder: + (context) => PuzzleScreen( + angle: const PuzzleTheme(PuzzleThemeKey.mix), + puzzleId: data.puzzle.id, + ), ); }, ); }, - loading: () => isOnline - ? const Shimmer( - child: ShimmerLoading( - isLoading: true, - child: SmallBoardPreview.loading(), - ), - ) - : const SizedBox.shrink(), + loading: + () => + isOnline + ? const Shimmer( + child: ShimmerLoading(isLoading: true, child: SmallBoardPreview.loading()), + ) + : const SizedBox.shrink(), error: (error, _) { return isOnline ? const Padding( - padding: Styles.bodySectionPadding, - child: Text('Could not load the daily puzzle.'), - ) + padding: Styles.bodySectionPadding, + child: Text('Could not load the daily puzzle.'), + ) : const SizedBox.shrink(); }, ); @@ -751,118 +691,104 @@ class PuzzleAnglePreview extends ConsumerWidget { return loading ? const Shimmer( - child: ShimmerLoading( - isLoading: true, - child: SmallBoardPreview.loading(), - ), - ) + child: ShimmerLoading(isLoading: true, child: SmallBoardPreview.loading()), + ) : Slidable( - dragStartBehavior: DragStartBehavior.start, - enabled: angle != const PuzzleTheme(PuzzleThemeKey.mix), - endActionPane: ActionPane( - motion: const StretchMotion(), + dragStartBehavior: DragStartBehavior.start, + enabled: angle != const PuzzleTheme(PuzzleThemeKey.mix), + endActionPane: ActionPane( + motion: const StretchMotion(), + children: [ + SlidableAction( + icon: Icons.delete, + onPressed: (context) async { + final service = await ref.read(puzzleServiceProvider.future); + if (context.mounted) { + service.deleteBatch( + userId: ref.read(authSessionProvider)?.user.id, + angle: angle, + ); + } + }, + spacing: 8.0, + backgroundColor: context.lichessColors.error, + foregroundColor: Colors.white, + label: context.l10n.delete, + ), + ], + ), + child: SmallBoardPreview( + orientation: preview?.orientation ?? Side.white, + fen: preview?.initialFen ?? kEmptyFen, + lastMove: preview?.initialMove, + description: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - SlidableAction( - icon: Icons.delete, - onPressed: (context) async { - final service = - await ref.read(puzzleServiceProvider.future); - if (context.mounted) { - service.deleteBatch( - userId: ref.read(authSessionProvider)?.user.id, - angle: angle, - ); - } + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: switch (angle) { + PuzzleTheme(themeKey: final themeKey) => [ + Text( + themeKey.l10n(context.l10n).name, + style: Styles.boardPreviewTitle, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + Text( + themeKey.l10n(context.l10n).description, + maxLines: 3, + overflow: TextOverflow.ellipsis, + style: TextStyle( + height: 1.2, + fontSize: 12.0, + color: DefaultTextStyle.of(context).style.color?.withValues(alpha: 0.6), + ), + ), + ], + PuzzleOpening(key: final openingKey) => [ + Text( + flatOpenings.valueOrNull + ?.firstWhere( + (o) => o.key == openingKey, + orElse: + () => ( + key: openingKey, + name: openingKey.replaceAll('_', ''), + count: 0, + ), + ) + .name ?? + openingKey.replaceAll('_', ' '), + style: Styles.boardPreviewTitle, + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + ], + }, + ), + Icon( + switch (angle) { + PuzzleTheme(themeKey: final themeKey) => themeKey.icon, + PuzzleOpening() => PuzzleIcons.opening, }, - spacing: 8.0, - backgroundColor: context.lichessColors.error, - foregroundColor: Colors.white, - label: context.l10n.delete, + size: 34, + color: DefaultTextStyle.of(context).style.color?.withValues(alpha: 0.6), ), + if (puzzle != null) + Text( + puzzle.puzzle.sideToMove == Side.white + ? context.l10n.whitePlays + : context.l10n.blackPlays, + ) + else + const Text('No puzzles available, please go online to fetch them.'), ], ), - child: SmallBoardPreview( - orientation: preview?.orientation ?? Side.white, - fen: preview?.initialFen ?? kEmptyFen, - lastMove: preview?.initialMove, - description: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: switch (angle) { - PuzzleTheme(themeKey: final themeKey) => [ - Text( - themeKey.l10n(context.l10n).name, - style: Styles.boardPreviewTitle, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - Text( - themeKey.l10n(context.l10n).description, - maxLines: 3, - overflow: TextOverflow.ellipsis, - style: TextStyle( - height: 1.2, - fontSize: 12.0, - color: DefaultTextStyle.of(context) - .style - .color - ?.withValues(alpha: 0.6), - ), - ), - ], - PuzzleOpening(key: final openingKey) => [ - Text( - flatOpenings.valueOrNull - ?.firstWhere( - (o) => o.key == openingKey, - orElse: () => ( - key: openingKey, - name: openingKey.replaceAll( - '_', - '', - ), - count: 0 - ), - ) - .name ?? - openingKey.replaceAll('_', ' '), - style: Styles.boardPreviewTitle, - maxLines: 3, - overflow: TextOverflow.ellipsis, - ), - ], - }, - ), - Icon( - switch (angle) { - PuzzleTheme(themeKey: final themeKey) => themeKey.icon, - PuzzleOpening() => PuzzleIcons.opening, - }, - size: 34, - color: DefaultTextStyle.of(context) - .style - .color - ?.withValues(alpha: 0.6), - ), - if (puzzle != null) - Text( - puzzle.puzzle.sideToMove == Side.white - ? context.l10n.whitePlays - : context.l10n.blackPlays, - ) - else - const Text( - 'No puzzles available, please go online to fetch them.', - ), - ], - ), - onTap: puzzle != null ? onTap : null, - ), - ); + onTap: puzzle != null ? onTap : null, + ), + ); } return puzzle.maybeWhen( diff --git a/lib/src/view/puzzle/puzzle_themes_screen.dart b/lib/src/view/puzzle/puzzle_themes_screen.dart index 5f080eae17..9d90db4ab6 100644 --- a/lib/src/view/puzzle/puzzle_themes_screen.dart +++ b/lib/src/view/puzzle/puzzle_themes_screen.dart @@ -18,12 +18,8 @@ import 'puzzle_screen.dart'; @riverpod final _themesProvider = FutureProvider.autoDispose< - ( - bool, - IMap, - IMap?, - bool, - )>((ref) async { + (bool, IMap, IMap?, bool) +>((ref) async { final connectivity = await ref.watch(connectivityChangesProvider.future); final savedThemes = await ref.watch(savedThemeBatchesProvider.future); IMap? onlineThemes; @@ -33,12 +29,7 @@ final _themesProvider = FutureProvider.autoDispose< onlineThemes = null; } final savedOpenings = await ref.watch(savedOpeningBatchesProvider.future); - return ( - connectivity.isOnline, - savedThemes, - onlineThemes, - savedOpenings.isNotEmpty - ); + return (connectivity.isOnline, savedThemes, onlineThemes, savedOpenings.isNotEmpty); }); class PuzzleThemesScreen extends StatelessWidget { @@ -47,9 +38,7 @@ class PuzzleThemesScreen extends StatelessWidget { @override Widget build(BuildContext context) { return PlatformScaffold( - appBar: PlatformAppBar( - title: Text(context.l10n.puzzlePuzzleThemes), - ), + appBar: PlatformAppBar(title: Text(context.l10n.puzzlePuzzleThemes)), body: const _Body(), ); } @@ -63,21 +52,20 @@ class _Body extends ConsumerWidget { // skip recommended category since we display it on the puzzle tab screen final list = ref.watch(puzzleThemeCategoriesProvider).skip(1).toList(); final themes = ref.watch(_themesProvider); - final expansionTileColor = Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoColors.secondaryLabel.resolveFrom(context) - : null; + final expansionTileColor = + Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoColors.secondaryLabel.resolveFrom(context) + : null; return themes.when( data: (data) { - final (hasConnectivity, savedThemes, onlineThemes, hasSavedOpenings) = - data; + final (hasConnectivity, savedThemes, onlineThemes, hasSavedOpenings) = data; final openingsAvailable = hasConnectivity || hasSavedOpenings; return ListView( children: [ Theme( - data: - Theme.of(context).copyWith(dividerColor: Colors.transparent), + data: Theme.of(context).copyWith(dividerColor: Colors.transparent), child: Opacity( opacity: openingsAvailable ? 1 : 0.5, child: ExpansionTile( @@ -85,14 +73,15 @@ class _Body extends ConsumerWidget { collapsedIconColor: expansionTileColor, title: Text(context.l10n.puzzleByOpenings), trailing: const Icon(Icons.keyboard_arrow_right), - onExpansionChanged: openingsAvailable - ? (expanded) { - pushPlatformRoute( - context, - builder: (ctx) => const OpeningThemeScreen(), - ); - } - : null, + onExpansionChanged: + openingsAvailable + ? (expanded) { + pushPlatformRoute( + context, + builder: (ctx) => const OpeningThemeScreen(), + ); + } + : null, ), ), ), @@ -107,8 +96,7 @@ class _Body extends ConsumerWidget { ); }, loading: () => const Center(child: CircularProgressIndicator.adaptive()), - error: (error, stack) => - const Center(child: Text('Could not load themes.')), + error: (error, stack) => const Center(child: Text('Could not load themes.')), ); } } @@ -130,10 +118,7 @@ class _Category extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final themeCountStyle = TextStyle( fontSize: 12, - color: textShade( - context, - Styles.subtitleOpacity, - ), + color: textShade(context, Styles.subtitleOpacity), ); final (categoryName, themes) = category; @@ -145,79 +130,67 @@ class _Category extends ConsumerWidget { children: [ ListSection( hasLeading: true, - children: themes.map( - (theme) { - final isThemeAvailable = - hasConnectivity || savedThemes.containsKey(theme); + children: + themes.map((theme) { + final isThemeAvailable = hasConnectivity || savedThemes.containsKey(theme); - return Opacity( - opacity: isThemeAvailable ? 1 : 0.5, - child: PlatformListTile( - leading: Icon(theme.icon), - trailing: hasConnectivity && - onlineThemes?.containsKey(theme) == true - ? Padding( - padding: const EdgeInsets.only(left: 6.0), - child: Text( - '${onlineThemes![theme]!.count}', - style: themeCountStyle, - ), - ) - : savedThemes.containsKey(theme) - ? Padding( + return Opacity( + opacity: isThemeAvailable ? 1 : 0.5, + child: PlatformListTile( + leading: Icon(theme.icon), + trailing: + hasConnectivity && onlineThemes?.containsKey(theme) == true + ? Padding( padding: const EdgeInsets.only(left: 6.0), child: Text( - '${savedThemes[theme]!}', + '${onlineThemes![theme]!.count}', style: themeCountStyle, ), ) - : null, - title: Padding( - padding: Theme.of(context).platform == TargetPlatform.iOS - ? const EdgeInsets.only(top: 6.0) - : EdgeInsets.zero, - child: Text( - theme.l10n(context.l10n).name, - style: Theme.of(context).platform == TargetPlatform.iOS - ? TextStyle( - color: CupertinoTheme.of(context) - .textTheme - .textStyle - .color, + : savedThemes.containsKey(theme) + ? Padding( + padding: const EdgeInsets.only(left: 6.0), + child: Text('${savedThemes[theme]!}', style: themeCountStyle), ) - : null, + : null, + title: Padding( + padding: + Theme.of(context).platform == TargetPlatform.iOS + ? const EdgeInsets.only(top: 6.0) + : EdgeInsets.zero, + child: Text( + theme.l10n(context.l10n).name, + style: + Theme.of(context).platform == TargetPlatform.iOS + ? TextStyle( + color: CupertinoTheme.of(context).textTheme.textStyle.color, + ) + : null, + ), ), - ), - subtitle: Padding( - padding: const EdgeInsets.only(bottom: 6.0), - child: Text( - theme.l10n(context.l10n).description, - maxLines: 10, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: textShade( - context, - Styles.subtitleOpacity, - ), + subtitle: Padding( + padding: const EdgeInsets.only(bottom: 6.0), + child: Text( + theme.l10n(context.l10n).description, + maxLines: 10, + overflow: TextOverflow.ellipsis, + style: TextStyle(color: textShade(context, Styles.subtitleOpacity)), ), ), + isThreeLine: true, + onTap: + isThemeAvailable + ? () { + pushPlatformRoute( + context, + rootNavigator: true, + builder: (context) => PuzzleScreen(angle: PuzzleTheme(theme)), + ); + } + : null, ), - isThreeLine: true, - onTap: isThemeAvailable - ? () { - pushPlatformRoute( - context, - rootNavigator: true, - builder: (context) => PuzzleScreen( - angle: PuzzleTheme(theme), - ), - ); - } - : null, - ), - ); - }, - ).toList(), + ); + }).toList(), ), ], ), diff --git a/lib/src/view/puzzle/storm_clock.dart b/lib/src/view/puzzle/storm_clock.dart index c02d586663..be4dbd0e7f 100644 --- a/lib/src/view/puzzle/storm_clock.dart +++ b/lib/src/view/puzzle/storm_clock.dart @@ -16,23 +16,20 @@ class StormClockWidget extends StatefulWidget { _ClockState createState() => _ClockState(); } -class _ClockState extends State - with SingleTickerProviderStateMixin { +class _ClockState extends State with SingleTickerProviderStateMixin { // ignore: avoid-late-keyword late AnimationController _controller; // ignore: avoid-late-keyword - late final Animation _bonusFadeAnimation = - Tween(begin: 1.0, end: 0.0).animate( - CurvedAnimation(parent: _controller, curve: Curves.easeOut), - ); + late final Animation _bonusFadeAnimation = Tween( + begin: 1.0, + end: 0.0, + ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut)); // ignore: avoid-late-keyword late final Animation _bonusSlideAnimation = Tween( begin: const Offset(0.7, 0.0), end: const Offset(0.7, -1.0), - ).animate( - CurvedAnimation(parent: _controller, curve: Curves.easeOut), - ); + ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut)); StreamSubscription<(Duration, int?)>? streamSubscription; @@ -46,10 +43,8 @@ class _ClockState extends State // declaring as late final causes an error because the widget is being disposed // after the clock start - _controller = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 1500), - )..addStatusListener((status) { + _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 1500)) + ..addStatusListener((status) { if (status == AnimationStatus.completed) { setState(() { currentBonusSeconds = null; @@ -98,9 +93,8 @@ class _ClockState extends State @override Widget build(BuildContext build) { final brightness = Theme.of(context).brightness; - final clockStyle = brightness == Brightness.dark - ? _ClockStyle.darkThemeStyle - : _ClockStyle.lightThemeStyle; + final clockStyle = + brightness == Brightness.dark ? _ClockStyle.darkThemeStyle : _ClockStyle.lightThemeStyle; final minutes = time.inMinutes.remainder(60).toString().padLeft(2, '0'); final seconds = time.inSeconds.remainder(60).toString().padLeft(2, '0'); @@ -120,9 +114,10 @@ class _ClockState extends State child: Text( '${currentBonusSeconds! > 0 ? '+' : ''}$currentBonusSeconds', style: TextStyle( - color: currentBonusSeconds! < 0 - ? context.lichessColors.error - : context.lichessColors.good, + color: + currentBonusSeconds! < 0 + ? context.lichessColors.error + : context.lichessColors.good, fontWeight: FontWeight.bold, fontSize: 20, fontFeatures: const [FontFeature.tabularFigures()], @@ -139,20 +134,17 @@ class _ClockState extends State ), duration: const Duration(milliseconds: 500), builder: (context, Duration value, _) { - final minutes = - value.inMinutes.remainder(60).toString().padLeft(2, '0'); - final seconds = - value.inSeconds.remainder(60).toString().padLeft(2, '0'); + final minutes = value.inMinutes.remainder(60).toString().padLeft(2, '0'); + final seconds = value.inSeconds.remainder(60).toString().padLeft(2, '0'); return Text( '$minutes:$seconds', style: TextStyle( - color: currentBonusSeconds! < 0 - ? context.lichessColors.error - : context.lichessColors.good, + color: + currentBonusSeconds! < 0 + ? context.lichessColors.error + : context.lichessColors.good, fontSize: _kClockFontSize, - fontFeatures: const [ - FontFeature.tabularFigures(), - ], + fontFeatures: const [FontFeature.tabularFigures()], ), ); }, @@ -161,13 +153,9 @@ class _ClockState extends State Text( '$minutes:$seconds', style: TextStyle( - color: isActive - ? clockStyle.activeTextColor - : clockStyle.textColor, + color: isActive ? clockStyle.activeTextColor : clockStyle.textColor, fontSize: _kClockFontSize, - fontFeatures: const [ - FontFeature.tabularFigures(), - ], + fontFeatures: const [FontFeature.tabularFigures()], ), ), ], @@ -178,19 +166,10 @@ class _ClockState extends State } enum _ClockStyle { - darkThemeStyle( - textColor: Colors.grey, - activeTextColor: Colors.white, - ), - lightThemeStyle( - textColor: Colors.grey, - activeTextColor: Colors.black, - ); - - const _ClockStyle({ - required this.textColor, - required this.activeTextColor, - }); + darkThemeStyle(textColor: Colors.grey, activeTextColor: Colors.white), + lightThemeStyle(textColor: Colors.grey, activeTextColor: Colors.black); + + const _ClockStyle({required this.textColor, required this.activeTextColor}); final Color textColor; final Color activeTextColor; diff --git a/lib/src/view/puzzle/storm_dashboard.dart b/lib/src/view/puzzle/storm_dashboard.dart index 1d7e7ee5d0..25ebd5cfa1 100644 --- a/lib/src/view/puzzle/storm_dashboard.dart +++ b/lib/src/view/puzzle/storm_dashboard.dart @@ -54,44 +54,23 @@ class _Body extends ConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( - padding: - Styles.sectionTopPadding.add(Styles.horizontalBodyPadding), - child: StatCardRow( - [ - StatCard( - context.l10n.stormAllTime, - value: data.highScore.allTime.toString(), - ), - StatCard( - context.l10n.stormThisMonth, - value: data.highScore.month.toString(), - ), - ], - ), + padding: Styles.sectionTopPadding.add(Styles.horizontalBodyPadding), + child: StatCardRow([ + StatCard(context.l10n.stormAllTime, value: data.highScore.allTime.toString()), + StatCard(context.l10n.stormThisMonth, value: data.highScore.month.toString()), + ]), ), Padding( - padding: - Styles.sectionTopPadding.add(Styles.horizontalBodyPadding), - child: StatCardRow( - [ - StatCard( - context.l10n.stormThisWeek, - value: data.highScore.week.toString(), - ), - StatCard( - context.l10n.today, - value: data.highScore.day.toString(), - ), - ], - ), + padding: Styles.sectionTopPadding.add(Styles.horizontalBodyPadding), + child: StatCardRow([ + StatCard(context.l10n.stormThisWeek, value: data.highScore.week.toString()), + StatCard(context.l10n.today, value: data.highScore.day.toString()), + ]), ), if (data.dayHighscores.isNotEmpty) ...[ Padding( padding: Styles.bodySectionPadding, - child: Text( - context.l10n.stormBestRunOfDay, - style: Styles.sectionTitle, - ), + child: Text(context.l10n.stormBestRunOfDay, style: Styles.sectionTitle), ), Padding( padding: Styles.horizontalBodyPadding, @@ -100,22 +79,10 @@ class _Body extends ConsumerWidget { children: [ TableRow( children: [ - Text( - textAlign: TextAlign.center, - context.l10n.stormScore, - ), - Text( - textAlign: TextAlign.center, - context.l10n.stormTime, - ), - Text( - textAlign: TextAlign.center, - context.l10n.stormHighestSolved, - ), - Text( - textAlign: TextAlign.center, - context.l10n.stormRuns, - ), + Text(textAlign: TextAlign.center, context.l10n.stormScore), + Text(textAlign: TextAlign.center, context.l10n.stormTime), + Text(textAlign: TextAlign.center, context.l10n.stormHighestSolved), + Text(textAlign: TextAlign.center, context.l10n.stormRuns), ], ), ], @@ -133,10 +100,8 @@ class _Body extends ConsumerWidget { child: Padding( padding: Styles.horizontalBodyPadding, child: Text( - dateFormat - .format(data.dayHighscores[entryIndex].day), - style: - const TextStyle(fontWeight: FontWeight.w600), + dateFormat.format(data.dayHighscores[entryIndex].day), + style: const TextStyle(fontWeight: FontWeight.w600), ), ), ); @@ -144,20 +109,15 @@ class _Body extends ConsumerWidget { // Data row final entryIndex = (index - 1) ~/ 2; return Padding( - padding: const EdgeInsets.symmetric( - horizontal: 15, - vertical: 10, - ), + padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 10), child: Table( - defaultVerticalAlignment: - TableCellVerticalAlignment.middle, + defaultVerticalAlignment: TableCellVerticalAlignment.middle, children: [ TableRow( children: [ Text( textAlign: TextAlign.center, - data.dayHighscores[entryIndex].score - .toString(), + data.dayHighscores[entryIndex].score.toString(), style: TextStyle( color: context.lichessColors.brag, fontWeight: FontWeight.bold, @@ -169,13 +129,11 @@ class _Body extends ConsumerWidget { ), Text( textAlign: TextAlign.center, - data.dayHighscores[entryIndex].highest - .toString(), + data.dayHighscores[entryIndex].highest.toString(), ), Text( textAlign: TextAlign.center, - data.dayHighscores[entryIndex].runs - .toString(), + data.dayHighscores[entryIndex].runs.toString(), ), ], ), @@ -187,17 +145,13 @@ class _Body extends ConsumerWidget { ), ), ] else - Center( - child: Text(context.l10n.mobilePuzzleStormNothingToShow), - ), + Center(child: Text(context.l10n.mobilePuzzleStormNothingToShow)), ], ), ); }, error: (e, s) { - debugPrint( - 'SEVERE: [StormDashboardModel] could not load storm dashboard; $e\n$s', - ); + debugPrint('SEVERE: [StormDashboardModel] could not load storm dashboard; $e\n$s'); return const SafeArea(child: Text('Could not load dashboard')); }, loading: () => _Loading(), diff --git a/lib/src/view/puzzle/storm_screen.dart b/lib/src/view/puzzle/storm_screen.dart index 63e263fc89..2f4b7867e1 100644 --- a/lib/src/view/puzzle/storm_screen.dart +++ b/lib/src/view/puzzle/storm_screen.dart @@ -71,9 +71,7 @@ class _Load extends ConsumerWidget { }, loading: () => const CenterLoadingIndicator(), error: (e, s) { - debugPrint( - 'SEVERE: [PuzzleStormScreen] could not load streak; $e\n$s', - ); + debugPrint('SEVERE: [PuzzleStormScreen] could not load streak; $e\n$s'); return Center( child: BoardTable( topTable: kEmptyWidget, @@ -124,14 +122,15 @@ class _Body extends ConsumerWidget { final NavigatorState navigator = Navigator.of(context); final shouldPop = await showAdaptiveDialog( context: context, - builder: (context) => YesNoDialog( - title: Text(context.l10n.mobileAreYouSure), - content: Text(context.l10n.mobilePuzzleStormConfirmEndRun), - onYes: () { - return Navigator.of(context).pop(true); - }, - onNo: () => Navigator.of(context).pop(false), - ), + builder: + (context) => YesNoDialog( + title: Text(context.l10n.mobileAreYouSure), + content: Text(context.l10n.mobilePuzzleStormConfirmEndRun), + onYes: () { + return Navigator.of(context).pop(true); + }, + onNo: () => Navigator.of(context).pop(false), + ), ); if (shouldPop ?? false) { navigator.pop(); @@ -149,23 +148,23 @@ class _Body extends ConsumerWidget { lastMove: stormState.lastMove as NormalMove?, fen: stormState.position.fen, gameData: GameData( - playerSide: !stormState.firstMovePlayed || - stormState.mode == StormMode.ended || - stormState.position.isGameOver - ? PlayerSide.none - : stormState.pov == Side.white + playerSide: + !stormState.firstMovePlayed || + stormState.mode == StormMode.ended || + stormState.position.isGameOver + ? PlayerSide.none + : stormState.pov == Side.white ? PlayerSide.white : PlayerSide.black, - isCheck: boardPreferences.boardHighlights && - stormState.position.isCheck, + isCheck: boardPreferences.boardHighlights && stormState.position.isCheck, sideToMove: stormState.position.turn, validMoves: stormState.validMoves, promotionMove: stormState.promotionMove, - onMove: (move, {isDrop, captured}) => - ref.read(ctrlProvider.notifier).onUserMove(move), - onPromotionSelection: (role) => ref - .read(ctrlProvider.notifier) - .onPromotionSelection(role), + onMove: + (move, {isDrop, captured}) => + ref.read(ctrlProvider.notifier).onUserMove(move), + onPromotionSelection: + (role) => ref.read(ctrlProvider.notifier).onPromotionSelection(role), ), topTable: _TopTable(data), bottomTable: _Combo(stormState.combo), @@ -180,14 +179,12 @@ class _Body extends ConsumerWidget { return Theme.of(context).platform == TargetPlatform.android ? AndroidGesturesExclusionWidget( - boardKey: boardKey, - shouldExcludeGesturesOnFocusGained: () => - stormState.mode == StormMode.initial || - stormState.mode == StormMode.running, - shouldSetImmersiveMode: - boardPreferences.immersiveModeWhilePlaying ?? false, - child: content, - ) + boardKey: boardKey, + shouldExcludeGesturesOnFocusGained: + () => stormState.mode == StormMode.initial || stormState.mode == StormMode.running, + shouldSetImmersiveMode: boardPreferences.immersiveModeWhilePlaying ?? false, + child: content, + ) : content; } } @@ -201,57 +198,31 @@ Future _stormInfoDialogBuilder(BuildContext context) { text: TextSpan( style: DefaultTextStyle.of(context).style, children: const [ - TextSpan( - text: '\n', - ), + TextSpan(text: '\n'), TextSpan( text: 'Each puzzle grants one point. The goal is to get as many points as you can before the time runs out.', ), - TextSpan( - text: '\n\n', - ), - TextSpan( - text: 'Combo bar\n', - style: TextStyle(fontSize: 18), - ), + TextSpan(text: '\n\n'), + TextSpan(text: 'Combo bar\n', style: TextStyle(fontSize: 18)), TextSpan( text: 'Each correct ', children: [ - TextSpan( - text: 'move', - style: TextStyle(fontWeight: FontWeight.bold), - ), + TextSpan(text: 'move', style: TextStyle(fontWeight: FontWeight.bold)), TextSpan( text: ' fills the combo bar. When the bar is full, you get a time bonus, and you increase the value of the next bonus.', ), ], ), - TextSpan( - text: '\n\n', - ), - TextSpan( - text: 'Bonus values:\n', - ), - TextSpan( - text: '• 5 moves: +3s\n', - ), - TextSpan( - text: '• 12 moves: +5s\n', - ), - TextSpan( - text: '• 20 moves: +7s\n', - ), - TextSpan( - text: '• 30 moves: +10s\n', - ), - TextSpan( - text: '• Then +10s every 10 other moves.\n', - ), - TextSpan( - text: '\n', - ), + TextSpan(text: '\n\n'), + TextSpan(text: 'Bonus values:\n'), + TextSpan(text: '• 5 moves: +3s\n'), + TextSpan(text: '• 12 moves: +5s\n'), + TextSpan(text: '• 20 moves: +7s\n'), + TextSpan(text: '• 30 moves: +10s\n'), + TextSpan(text: '• Then +10s every 10 other moves.\n'), + TextSpan(text: '\n'), TextSpan( text: 'When you play a wrong move, the combo bar is depleted, and you lose 10 seconds.', @@ -291,8 +262,7 @@ class _TopTable extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final stormState = - ref.watch(stormControllerProvider(data.puzzles, data.timestamp)); + final stormState = ref.watch(stormControllerProvider(data.puzzles, data.timestamp)); return Padding( padding: const EdgeInsets.symmetric(horizontal: 10), child: Row( @@ -311,10 +281,7 @@ class _TopTable extends ConsumerWidget { context.l10n.stormMoveToStart, maxLines: 1, overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), Text( stormState.pov == Side.white @@ -322,20 +289,14 @@ class _TopTable extends ConsumerWidget { : context.l10n.stormYouPlayTheBlackPiecesInAllPuzzles, maxLines: 2, overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontSize: 12, - ), + style: const TextStyle(fontSize: 12), ), ], ), ), ) else ...[ - Icon( - LichessIcons.storm, - size: 50.0, - color: context.lichessColors.brag, - ), + Icon(LichessIcons.storm, size: 50.0, color: context.lichessColors.brag), const SizedBox(width: 8), Text( stormState.numSolved.toString(), @@ -363,8 +324,7 @@ class _Combo extends ConsumerStatefulWidget { ConsumerState<_Combo> createState() => _ComboState(); } -class _ComboState extends ConsumerState<_Combo> - with SingleTickerProviderStateMixin { +class _ComboState extends ConsumerState<_Combo> with SingleTickerProviderStateMixin { late AnimationController _controller; @override @@ -388,14 +348,12 @@ class _ComboState extends ConsumerState<_Combo> if (ref.read(boardPreferencesProvider).hapticFeedback) { HapticFeedback.heavyImpact(); } - _controller.animateTo(1.0, curve: Curves.easeInOut).then( - (_) async { - await Future.delayed(const Duration(milliseconds: 300)); - if (mounted) { - _controller.value = 0; - } - }, - ); + _controller.animateTo(1.0, curve: Curves.easeInOut).then((_) async { + await Future.delayed(const Duration(milliseconds: 300)); + if (mounted) { + _controller.value = 0; + } + }); return; } _controller.animateTo(newVal, curve: Curves.easeIn); @@ -419,120 +377,113 @@ class _ComboState extends ConsumerState<_Combo> ); return AnimatedBuilder( animation: _controller, - builder: (context, child) => LayoutBuilder( - builder: (context, constraints) { - return Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - widget.combo.current.toString(), - style: TextStyle( - fontSize: 26, - height: 1.0, - fontWeight: FontWeight.bold, - color: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoTheme.of(context) - .textTheme - .textStyle - .color - : null, - ), - ), - Text( - context.l10n.stormCombo, - style: TextStyle( - color: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoTheme.of(context) - .textTheme - .textStyle - .color - : null, - ), - ), - ], - ), - ), - SizedBox( - width: constraints.maxWidth * 0.65, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SizedBox( - height: 25, - child: Container( - decoration: BoxDecoration( - boxShadow: _controller.value == 1.0 - ? [ - BoxShadow( - color: - indicatorColor.withValues(alpha: 0.3), - blurRadius: 10.0, - spreadRadius: 2.0, - ), - ] - : [], + builder: + (context, child) => LayoutBuilder( + builder: (context, constraints) { + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + widget.combo.current.toString(), + style: TextStyle( + fontSize: 26, + height: 1.0, + fontWeight: FontWeight.bold, + color: + Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoTheme.of(context).textTheme.textStyle.color + : null, + ), ), - child: ClipRRect( - borderRadius: - const BorderRadius.all(Radius.circular(3.0)), - child: LinearProgressIndicator( - value: _controller.value, - valueColor: - AlwaysStoppedAnimation(indicatorColor), + Text( + context.l10n.stormCombo, + style: TextStyle( + color: + Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoTheme.of(context).textTheme.textStyle.color + : null, ), ), - ), + ], ), - const SizedBox(height: 4), - Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: - StormCombo.levelBonus.mapIndexed((index, level) { - final isCurrentLevel = index < lvl; - return AnimatedContainer( - alignment: Alignment.center, - curve: Curves.easeIn, - duration: const Duration(milliseconds: 1000), - width: 28 * - MediaQuery.textScalerOf(context).scale(14) / - 14, - height: 24 * - MediaQuery.textScalerOf(context).scale(14) / - 14, - decoration: isCurrentLevel - ? BoxDecoration( - color: comboShades[index], - borderRadius: const BorderRadius.all( - Radius.circular(3.0), - ), - ) - : null, - child: Text( - '${level}s', - style: TextStyle( - color: isCurrentLevel - ? Theme.of(context).colorScheme.onSecondary - : null, + ), + SizedBox( + width: constraints.maxWidth * 0.65, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + height: 25, + child: Container( + decoration: BoxDecoration( + boxShadow: + _controller.value == 1.0 + ? [ + BoxShadow( + color: indicatorColor.withValues(alpha: 0.3), + blurRadius: 10.0, + spreadRadius: 2.0, + ), + ] + : [], + ), + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(3.0)), + child: LinearProgressIndicator( + value: _controller.value, + valueColor: AlwaysStoppedAnimation(indicatorColor), + ), ), ), - ); - }).toList(), + ), + const SizedBox(height: 4), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: + StormCombo.levelBonus.mapIndexed((index, level) { + final isCurrentLevel = index < lvl; + return AnimatedContainer( + alignment: Alignment.center, + curve: Curves.easeIn, + duration: const Duration(milliseconds: 1000), + width: 28 * MediaQuery.textScalerOf(context).scale(14) / 14, + height: 24 * MediaQuery.textScalerOf(context).scale(14) / 14, + decoration: + isCurrentLevel + ? BoxDecoration( + color: comboShades[index], + borderRadius: const BorderRadius.all( + Radius.circular(3.0), + ), + ) + : null, + child: Text( + '${level}s', + style: TextStyle( + color: + isCurrentLevel + ? Theme.of(context).colorScheme.onSecondary + : null, + ), + ), + ); + }).toList(), + ), + ], ), - ], - ), - ), - const SizedBox(width: 10.0), - ], - ); - }, - ), + ), + const SizedBox(width: 10.0), + ], + ); + }, + ), ); } @@ -550,9 +501,7 @@ class _ComboState extends ConsumerState<_Combo> final double newR = (r - i * step).clamp(0, 255); final double newG = (g - i * step).clamp(0, 255); final double newB = (b - i * step).clamp(0, 255); - shades.add( - Color.from(alpha: baseColor.a, red: newR, green: newG, blue: newB), - ); + shades.add(Color.from(alpha: baseColor.a, red: newR, green: newG, blue: newB)); } // Generate lighter shades @@ -560,9 +509,7 @@ class _ComboState extends ConsumerState<_Combo> final double newR = (r + i * step).clamp(0, 255); final double newG = (g + i * step).clamp(0, 255); final double newB = (b + i * step).clamp(0, 255); - shades.add( - Color.from(alpha: baseColor.a, red: newR, green: newG, blue: newB), - ); + shades.add(Color.from(alpha: baseColor.a, red: newR, green: newG, blue: newB)); } if (light) { @@ -604,13 +551,14 @@ class _BottomBar extends ConsumerWidget { icon: LichessIcons.flag, label: context.l10n.stormEndRun.split('(').first.trimRight(), showLabel: true, - onTap: stormState.puzzleIndex >= 1 - ? () { - if (stormState.clock.startAt != null) { - stormState.clock.sendEnd(); + onTap: + stormState.puzzleIndex >= 1 + ? () { + if (stormState.clock.startAt != null) { + stormState.clock.sendEnd(); + } } - } - : null, + : null, ), if (stormState.mode == StormMode.ended && stormState.stats != null) BottomBarButton( @@ -657,33 +605,24 @@ class _RunStatsPopupState extends ConsumerState<_RunStatsPopup> { @override Widget build(BuildContext context) { final puzzleList = widget.stats.historyFilter(filter); - final highScoreWidgets = widget.stats.newHigh != null - ? [ - const SizedBox(height: 16), - ListTile( - leading: Icon( - LichessIcons.storm, - size: 46, - color: context.lichessColors.brag, - ), - title: Text( - newHighTitle(context, widget.stats.newHigh!), - style: Styles.sectionTitle.copyWith( - color: context.lichessColors.brag, + final highScoreWidgets = + widget.stats.newHigh != null + ? [ + const SizedBox(height: 16), + ListTile( + leading: Icon(LichessIcons.storm, size: 46, color: context.lichessColors.brag), + title: Text( + newHighTitle(context, widget.stats.newHigh!), + style: Styles.sectionTitle.copyWith(color: context.lichessColors.brag), ), - ), - subtitle: Text( - context.l10n.stormPreviousHighscoreWasX( - widget.stats.newHigh!.prev.toString(), - ), - style: TextStyle( - color: context.lichessColors.brag, + subtitle: Text( + context.l10n.stormPreviousHighscoreWasX(widget.stats.newHigh!.prev.toString()), + style: TextStyle(color: context.lichessColors.brag), ), ), - ), - const SizedBox(height: 10), - ] - : null; + const SizedBox(height: 10), + ] + : null; return SafeArea( child: ListView( @@ -691,34 +630,20 @@ class _RunStatsPopupState extends ConsumerState<_RunStatsPopup> { if (highScoreWidgets != null) ...highScoreWidgets, ListSection( cupertinoAdditionalDividerMargin: 6, - header: Text( - '${widget.stats.score} ${context.l10n.stormPuzzlesSolved}', - ), + header: Text('${widget.stats.score} ${context.l10n.stormPuzzlesSolved}'), children: [ - _StatsRow( - context.l10n.stormMoves, - widget.stats.moves.toString(), - ), + _StatsRow(context.l10n.stormMoves, widget.stats.moves.toString()), _StatsRow( context.l10n.accuracy, '${(((widget.stats.moves - widget.stats.errors) / widget.stats.moves) * 100).toStringAsFixed(2)}%', ), - _StatsRow( - context.l10n.stormCombo, - widget.stats.comboBest.toString(), - ), - _StatsRow( - context.l10n.stormTime, - '${widget.stats.time.inSeconds}s', - ), + _StatsRow(context.l10n.stormCombo, widget.stats.comboBest.toString()), + _StatsRow(context.l10n.stormTime, '${widget.stats.time.inSeconds}s'), _StatsRow( context.l10n.stormTimePerMove, '${widget.stats.timePerMove.toStringAsFixed(1)}s', ), - _StatsRow( - context.l10n.stormHighestSolved, - widget.stats.highest.toString(), - ), + _StatsRow(context.l10n.stormHighestSolved, widget.stats.highest.toString()), ], ), const SizedBox(height: 10.0), @@ -741,23 +666,19 @@ class _RunStatsPopupState extends ConsumerState<_RunStatsPopup> { children: [ Row( children: [ - Text( - context.l10n.stormPuzzlesPlayed, - style: Styles.sectionTitle, - ), + Text(context.l10n.stormPuzzlesPlayed, style: Styles.sectionTitle), const Spacer(), Tooltip( excludeFromSemantics: true, message: context.l10n.stormFailedPuzzles, child: PlatformIconButton( semanticsLabel: context.l10n.stormFailedPuzzles, - icon: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoIcons.clear_fill - : Icons.close, - onTap: () => setState( - () => - filter = filter.copyWith(failed: !filter.failed), - ), + icon: + Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoIcons.clear_fill + : Icons.close, + onTap: + () => setState(() => filter = filter.copyWith(failed: !filter.failed)), highlighted: filter.failed, ), ), @@ -766,12 +687,11 @@ class _RunStatsPopupState extends ConsumerState<_RunStatsPopup> { excludeFromSemantics: true, child: PlatformIconButton( semanticsLabel: context.l10n.stormSlowPuzzles, - icon: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoIcons.hourglass - : Icons.hourglass_bottom, - onTap: () => setState( - () => filter = filter.copyWith(slow: !filter.slow), - ), + icon: + Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoIcons.hourglass + : Icons.hourglass_bottom, + onTap: () => setState(() => filter = filter.copyWith(slow: !filter.slow)), highlighted: filter.slow, ), ), @@ -781,10 +701,7 @@ class _RunStatsPopupState extends ConsumerState<_RunStatsPopup> { if (puzzleList.isNotEmpty) PuzzleHistoryPreview(puzzleList) else - Center( - child: - Text(context.l10n.mobilePuzzleStormFilterNothingToShow), - ), + Center(child: Text(context.l10n.mobilePuzzleStormFilterNothingToShow)), ], ), ), @@ -819,10 +736,7 @@ class _StatsRow extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 8.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(label), - if (value != null) Text(value!), - ], + children: [Text(label), if (value != null) Text(value!)], ), ); } @@ -855,11 +769,10 @@ class _StormDashboardButton extends ConsumerWidget { return const SizedBox.shrink(); } - void _showDashboard(BuildContext context, AuthSessionState session) => - pushPlatformRoute( - context, - rootNavigator: true, - fullscreenDialog: true, - builder: (_) => StormDashboardModal(user: session.user), - ); + void _showDashboard(BuildContext context, AuthSessionState session) => pushPlatformRoute( + context, + rootNavigator: true, + fullscreenDialog: true, + builder: (_) => StormDashboardModal(user: session.user), + ); } diff --git a/lib/src/view/puzzle/streak_screen.dart b/lib/src/view/puzzle/streak_screen.dart index db086e705c..f41847b616 100644 --- a/lib/src/view/puzzle/streak_screen.dart +++ b/lib/src/view/puzzle/streak_screen.dart @@ -39,10 +39,7 @@ class StreakScreen extends StatelessWidget { Widget build(BuildContext context) { return const WakelockWidget( child: PlatformScaffold( - appBar: PlatformAppBar( - actions: [ToggleSoundButton()], - title: Text('Puzzle Streak'), - ), + appBar: PlatformAppBar(actions: [ToggleSoundButton()], title: Text('Puzzle Streak')), body: _Load(), ), ); @@ -70,9 +67,7 @@ class _Load extends ConsumerWidget { }, loading: () => const Center(child: CircularProgressIndicator.adaptive()), error: (e, s) { - debugPrint( - 'SEVERE: [StreakScreen] could not load streak; $e\n$s', - ); + debugPrint('SEVERE: [StreakScreen] could not load streak; $e\n$s'); return Center( child: BoardTable( topTable: kEmptyWidget, @@ -88,29 +83,28 @@ class _Load extends ConsumerWidget { } class _Body extends ConsumerWidget { - const _Body({ - required this.initialPuzzleContext, - required this.streak, - }); + const _Body({required this.initialPuzzleContext, required this.streak}); final PuzzleContext initialPuzzleContext; final PuzzleStreak streak; @override Widget build(BuildContext context, WidgetRef ref) { - final ctrlProvider = - puzzleControllerProvider(initialPuzzleContext, initialStreak: streak); + final ctrlProvider = puzzleControllerProvider(initialPuzzleContext, initialStreak: streak); final puzzleState = ref.watch(ctrlProvider); - ref.listen(ctrlProvider.select((s) => s.nextPuzzleStreakFetchError), - (_, shouldShowDialog) { + ref.listen(ctrlProvider.select((s) => s.nextPuzzleStreakFetchError), ( + _, + shouldShowDialog, + ) { if (shouldShowDialog) { showAdaptiveDialog( context: context, - builder: (context) => _RetryFetchPuzzleDialog( - initialPuzzleContext: initialPuzzleContext, - streak: streak, - ), + builder: + (context) => _RetryFetchPuzzleDialog( + initialPuzzleContext: initialPuzzleContext, + streak: streak, + ), ); } }); @@ -125,14 +119,14 @@ class _Body extends ConsumerWidget { fen: puzzleState.fen, lastMove: puzzleState.lastMove as NormalMove?, gameData: GameData( - playerSide: puzzleState.mode == PuzzleMode.load || - puzzleState.position.isGameOver - ? PlayerSide.none - : puzzleState.mode == PuzzleMode.view + playerSide: + puzzleState.mode == PuzzleMode.load || puzzleState.position.isGameOver + ? PlayerSide.none + : puzzleState.mode == PuzzleMode.view ? PlayerSide.both : puzzleState.pov == Side.white - ? PlayerSide.white - : PlayerSide.black, + ? PlayerSide.white + : PlayerSide.black, isCheck: puzzleState.position.isCheck, sideToMove: puzzleState.position.turn, validMoves: puzzleState.validMoves, @@ -146,9 +140,7 @@ class _Body extends ConsumerWidget { ), topTable: Center( child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 10.0, - ), + padding: const EdgeInsets.symmetric(horizontal: 10.0), child: PuzzleFeedbackWidget( puzzle: puzzleState.puzzle, state: puzzleState, @@ -157,21 +149,13 @@ class _Body extends ConsumerWidget { ), ), bottomTable: Padding( - padding: const EdgeInsets.only( - top: 10.0, - left: 10.0, - right: 10.0, - ), + padding: const EdgeInsets.only(top: 10.0, left: 10.0, right: 10.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Row( children: [ - Icon( - LichessIcons.streak, - size: 40.0, - color: context.lichessColors.brag, - ), + Icon(LichessIcons.streak, size: 40.0, color: context.lichessColors.brag), const SizedBox(width: 8.0), Text( puzzleState.streak!.index.toString(), @@ -183,11 +167,7 @@ class _Body extends ConsumerWidget { ), ], ), - Text( - context.l10n.puzzleRatingX( - puzzleState.puzzle.puzzle.rating.toString(), - ), - ), + Text(context.l10n.puzzleRatingX(puzzleState.puzzle.puzzle.rating.toString())), ], ), ), @@ -195,10 +175,7 @@ class _Body extends ConsumerWidget { ), ), ), - _BottomBar( - initialPuzzleContext: initialPuzzleContext, - streak: streak, - ), + _BottomBar(initialPuzzleContext: initialPuzzleContext, streak: streak), ], ); @@ -211,16 +188,16 @@ class _Body extends ConsumerWidget { final NavigatorState navigator = Navigator.of(context); final shouldPop = await showAdaptiveDialog( context: context, - builder: (context) => YesNoDialog( - title: Text(context.l10n.mobileAreYouSure), - content: - const Text('No worries, your score will be saved locally.'), - onYes: () { - ref.read(ctrlProvider.notifier).saveStreakResultLocally(); - return Navigator.of(context).pop(true); - }, - onNo: () => Navigator.of(context).pop(false), - ), + builder: + (context) => YesNoDialog( + title: Text(context.l10n.mobileAreYouSure), + content: const Text('No worries, your score will be saved locally.'), + onYes: () { + ref.read(ctrlProvider.notifier).saveStreakResultLocally(); + return Navigator.of(context).pop(true); + }, + onNo: () => Navigator.of(context).pop(false), + ), ); if (shouldPop ?? false) { navigator.pop(); @@ -232,18 +209,14 @@ class _Body extends ConsumerWidget { } class _BottomBar extends ConsumerWidget { - const _BottomBar({ - required this.initialPuzzleContext, - required this.streak, - }); + const _BottomBar({required this.initialPuzzleContext, required this.streak}); final PuzzleContext initialPuzzleContext; final PuzzleStreak streak; @override Widget build(BuildContext context, WidgetRef ref) { - final ctrlProvider = - puzzleControllerProvider(initialPuzzleContext, initialStreak: streak); + final ctrlProvider = puzzleControllerProvider(initialPuzzleContext, initialStreak: streak); final puzzleState = ref.watch(ctrlProvider); return BottomBar( @@ -260,42 +233,42 @@ class _BottomBar extends ConsumerWidget { icon: Icons.skip_next, label: context.l10n.skipThisMove, showLabel: true, - onTap: puzzleState.streak!.hasSkipped || - puzzleState.mode == PuzzleMode.view - ? null - : () => ref.read(ctrlProvider.notifier).skipMove(), + onTap: + puzzleState.streak!.hasSkipped || puzzleState.mode == PuzzleMode.view + ? null + : () => ref.read(ctrlProvider.notifier).skipMove(), ), if (puzzleState.streak!.finished) BottomBarButton( onTap: () { launchShareDialog( context, - text: lichessUri( - '/training/${puzzleState.puzzle.puzzle.id}', - ).toString(), + text: lichessUri('/training/${puzzleState.puzzle.puzzle.id}').toString(), ); }, label: 'Share this puzzle', - icon: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoIcons.share - : Icons.share, + icon: + Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoIcons.share + : Icons.share, ), if (puzzleState.streak!.finished) BottomBarButton( onTap: () { pushPlatformRoute( context, - builder: (context) => AnalysisScreen( - options: AnalysisOptions( - orientation: puzzleState.pov, - standalone: ( - pgn: ref.read(ctrlProvider.notifier).makePgn(), - isComputerAnalysisAllowed: true, - variant: Variant.standard, + builder: + (context) => AnalysisScreen( + options: AnalysisOptions( + orientation: puzzleState.pov, + standalone: ( + pgn: ref.read(ctrlProvider.notifier).makePgn(), + isComputerAnalysisAllowed: true, + variant: Variant.standard, + ), + initialMoveCursor: 0, + ), ), - initialMoveCursor: 0, - ), - ), ); }, label: context.l10n.analysis, @@ -303,25 +276,23 @@ class _BottomBar extends ConsumerWidget { ), if (puzzleState.streak!.finished) BottomBarButton( - onTap: puzzleState.canGoBack - ? () => ref.read(ctrlProvider.notifier).userPrevious() - : null, + onTap: + puzzleState.canGoBack ? () => ref.read(ctrlProvider.notifier).userPrevious() : null, label: 'Previous', icon: CupertinoIcons.chevron_back, ), if (puzzleState.streak!.finished) BottomBarButton( - onTap: puzzleState.canGoNext - ? () => ref.read(ctrlProvider.notifier).userNext() - : null, + onTap: puzzleState.canGoNext ? () => ref.read(ctrlProvider.notifier).userNext() : null, label: context.l10n.next, icon: CupertinoIcons.chevron_forward, ), if (puzzleState.streak!.finished) BottomBarButton( - onTap: ref.read(streakProvider).isLoading == false - ? () => ref.invalidate(streakProvider) - : null, + onTap: + ref.read(streakProvider).isLoading == false + ? () => ref.invalidate(streakProvider) + : null, highlighted: true, label: context.l10n.puzzleNewStreak, icon: CupertinoIcons.play_arrow_solid, @@ -333,25 +304,23 @@ class _BottomBar extends ConsumerWidget { Future _streakInfoDialogBuilder(BuildContext context) { return showAdaptiveDialog( context: context, - builder: (context) => PlatformAlertDialog( - title: Text(context.l10n.aboutX('Puzzle Streak')), - content: Text(context.l10n.puzzleStreakDescription), - actions: [ - PlatformDialogAction( - onPressed: () => Navigator.of(context).pop(), - child: Text(context.l10n.mobileOkButton), + builder: + (context) => PlatformAlertDialog( + title: Text(context.l10n.aboutX('Puzzle Streak')), + content: Text(context.l10n.puzzleStreakDescription), + actions: [ + PlatformDialogAction( + onPressed: () => Navigator.of(context).pop(), + child: Text(context.l10n.mobileOkButton), + ), + ], ), - ], - ), ); } } class _RetryFetchPuzzleDialog extends ConsumerWidget { - const _RetryFetchPuzzleDialog({ - required this.initialPuzzleContext, - required this.streak, - }); + const _RetryFetchPuzzleDialog({required this.initialPuzzleContext, required this.streak}); final PuzzleContext initialPuzzleContext; final PuzzleStreak streak; @@ -361,8 +330,7 @@ class _RetryFetchPuzzleDialog extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final ctrlProvider = - puzzleControllerProvider(initialPuzzleContext, initialStreak: streak); + final ctrlProvider = puzzleControllerProvider(initialPuzzleContext, initialStreak: streak); final state = ref.watch(ctrlProvider); Future retryStreakNext() async { @@ -375,18 +343,18 @@ class _RetryFetchPuzzleDialog extends ConsumerWidget { Navigator.of(context).pop(); } if (data != null) { - ref.read(ctrlProvider.notifier).loadPuzzle( + ref + .read(ctrlProvider.notifier) + .loadPuzzle( data, - nextStreak: - state.streak!.copyWith(index: state.streak!.index + 1), + nextStreak: state.streak!.copyWith(index: state.streak!.index + 1), ); } }, ); } - final canRetry = state.nextPuzzleStreakFetchError && - !state.nextPuzzleStreakFetchIsRetrying; + final canRetry = state.nextPuzzleStreakFetchError && !state.nextPuzzleStreakFetchIsRetrying; return PlatformAlertDialog( title: const Text(title), diff --git a/lib/src/view/relation/following_screen.dart b/lib/src/view/relation/following_screen.dart index b27b7ad5f3..8504b58e28 100644 --- a/lib/src/view/relation/following_screen.dart +++ b/lib/src/view/relation/following_screen.dart @@ -18,21 +18,20 @@ import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:lichess_mobile/src/widgets/user_list_tile.dart'; -final _getFollowingAndOnlinesProvider = - FutureProvider.autoDispose<(IList, IList)>((ref) async { - final following = await ref.watch(followingProvider.future); - final onlines = await ref.watch(onlineFriendsProvider.future); - return (following, onlines); -}); +final _getFollowingAndOnlinesProvider = FutureProvider.autoDispose<(IList, IList)>( + (ref) async { + final following = await ref.watch(followingProvider.future); + final onlines = await ref.watch(onlineFriendsProvider.future); + return (following, onlines); + }, +); class FollowingScreen extends StatelessWidget { const FollowingScreen({super.key}); @override Widget build(BuildContext context) { return PlatformScaffold( - appBar: PlatformAppBar( - title: Text(context.l10n.friends), - ), + appBar: PlatformAppBar(title: Text(context.l10n.friends)), body: const _Body(), ); } @@ -43,9 +42,7 @@ class _Body extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final followingAndOnlines = ref.watch( - _getFollowingAndOnlinesProvider, - ); + final followingAndOnlines = ref.watch(_getFollowingAndOnlinesProvider); return followingAndOnlines.when( data: (data) { @@ -53,21 +50,19 @@ class _Body extends ConsumerWidget { return StatefulBuilder( builder: (BuildContext context, StateSetter setState) { if (following.isEmpty) { - return Center( - child: Text(context.l10n.mobileNotFollowingAnyUser), - ); + return Center(child: Text(context.l10n.mobileNotFollowingAnyUser)); } return SafeArea( child: ColoredBox( - color: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoColors.systemBackground.resolveFrom(context) - : Colors.transparent, + color: + Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoColors.systemBackground.resolveFrom(context) + : Colors.transparent, child: ListView.separated( itemCount: following.length, - separatorBuilder: (context, index) => const PlatformDivider( - height: 1, - cupertinoHasLeading: true, - ), + separatorBuilder: + (context, index) => + const PlatformDivider(height: 1, cupertinoHasLeading: true), itemBuilder: (context, index) { final user = following[index]; return Slidable( @@ -80,14 +75,11 @@ class _Body extends ConsumerWidget { onPressed: (BuildContext context) async { final oldState = following; setState(() { - following = following.removeWhere( - (v) => v.id == user.id, - ); + following = following.removeWhere((v) => v.id == user.id); }); try { await ref.withClient( - (client) => RelationRepository(client) - .unfollow(user.id), + (client) => RelationRepository(client).unfollow(user.id), ); } catch (_) { setState(() { @@ -106,14 +98,13 @@ class _Body extends ConsumerWidget { child: UserListTile.fromUser( user, _isOnline(user, data.$2), - onTap: () => { - pushPlatformRoute( - context, - builder: (context) => UserScreen( - user: user.lightUser, - ), - ), - }, + onTap: + () => { + pushPlatformRoute( + context, + builder: (context) => UserScreen(user: user.lightUser), + ), + }, ), ); }, @@ -124,12 +115,8 @@ class _Body extends ConsumerWidget { ); }, error: (error, stackTrace) { - debugPrint( - 'SEVERE: [FollowingScreen] could not load following users; $error\n$stackTrace', - ); - return FullScreenRetryRequest( - onRetry: () => ref.invalidate(followingProvider), - ); + debugPrint('SEVERE: [FollowingScreen] could not load following users; $error\n$stackTrace'); + return FullScreenRetryRequest(onRetry: () => ref.invalidate(followingProvider)); }, loading: () => const CenterLoadingIndicator(), ); diff --git a/lib/src/view/settings/account_preferences_screen.dart b/lib/src/view/settings/account_preferences_screen.dart index f6797646d3..83c65bfa7a 100644 --- a/lib/src/view/settings/account_preferences_screen.dart +++ b/lib/src/view/settings/account_preferences_screen.dart @@ -14,12 +14,10 @@ class AccountPreferencesScreen extends ConsumerStatefulWidget { const AccountPreferencesScreen({super.key}); @override - ConsumerState createState() => - _AccountPreferencesScreenState(); + ConsumerState createState() => _AccountPreferencesScreenState(); } -class _AccountPreferencesScreenState - extends ConsumerState { +class _AccountPreferencesScreenState extends ConsumerState { bool isLoading = false; Future _setPref(Future Function() f) async { @@ -42,9 +40,7 @@ class _AccountPreferencesScreenState final content = accountPrefs.when( data: (data) { if (data == null) { - return Center( - child: Text(context.l10n.mobileMustBeLoggedIn), - ); + return Center(child: Text(context.l10n.mobileMustBeLoggedIn)); } return ListView( @@ -54,9 +50,7 @@ class _AccountPreferencesScreenState hasLeading: false, children: [ SettingsListTile( - settingsLabel: Text( - context.l10n.preferencesZenMode, - ), + settingsLabel: Text(context.l10n.preferencesZenMode), settingsValue: data.zenMode.label(context), showCupertinoTrailingValue: false, onTap: () { @@ -66,17 +60,16 @@ class _AccountPreferencesScreenState choices: Zen.values, selectedItem: data.zenMode, labelBuilder: (t) => Text(t.label(context)), - onSelectedItemChanged: isLoading - ? null - : (Zen? value) { - _setPref( - () => ref - .read( - accountPreferencesProvider.notifier, - ) - .setZen(value ?? data.zenMode), - ); - }, + onSelectedItemChanged: + isLoading + ? null + : (Zen? value) { + _setPref( + () => ref + .read(accountPreferencesProvider.notifier) + .setZen(value ?? data.zenMode), + ); + }, ); } else { pushPlatformRoute( @@ -88,9 +81,7 @@ class _AccountPreferencesScreenState }, ), SettingsListTile( - settingsLabel: Text( - context.l10n.preferencesPgnPieceNotation, - ), + settingsLabel: Text(context.l10n.preferencesPgnPieceNotation), settingsValue: data.pieceNotation.label(context), showCupertinoTrailingValue: false, onTap: () { @@ -100,26 +91,22 @@ class _AccountPreferencesScreenState choices: PieceNotation.values, selectedItem: data.pieceNotation, labelBuilder: (t) => Text(t.label(context)), - onSelectedItemChanged: isLoading - ? null - : (PieceNotation? value) { - _setPref( - () => ref - .read( - accountPreferencesProvider.notifier, - ) - .setPieceNotation( - value ?? data.pieceNotation, - ), - ); - }, + onSelectedItemChanged: + isLoading + ? null + : (PieceNotation? value) { + _setPref( + () => ref + .read(accountPreferencesProvider.notifier) + .setPieceNotation(value ?? data.pieceNotation), + ); + }, ); } else { pushPlatformRoute( context, title: context.l10n.preferencesPgnPieceNotation, - builder: (context) => - const PieceNotationSettingsScreen(), + builder: (context) => const PieceNotationSettingsScreen(), ); } }, @@ -132,57 +119,53 @@ class _AccountPreferencesScreenState textAlign: TextAlign.justify, ), value: data.showRatings.value, - onChanged: isLoading - ? null - : (value) { - _setPref( - () => ref - .read(accountPreferencesProvider.notifier) - .setShowRatings(BooleanPref(value)), - ); - }, + onChanged: + isLoading + ? null + : (value) { + _setPref( + () => ref + .read(accountPreferencesProvider.notifier) + .setShowRatings(BooleanPref(value)), + ); + }, ), ], ), ListSection( - header: - SettingsSectionTitle(context.l10n.preferencesGameBehavior), + header: SettingsSectionTitle(context.l10n.preferencesGameBehavior), hasLeading: false, children: [ SwitchSettingTile( - title: Text( - context.l10n.preferencesPremovesPlayingDuringOpponentTurn, - ), + title: Text(context.l10n.preferencesPremovesPlayingDuringOpponentTurn), value: data.premove.value, - onChanged: isLoading - ? null - : (value) { - _setPref( - () => ref - .read(accountPreferencesProvider.notifier) - .setPremove(BooleanPref(value)), - ); - }, + onChanged: + isLoading + ? null + : (value) { + _setPref( + () => ref + .read(accountPreferencesProvider.notifier) + .setPremove(BooleanPref(value)), + ); + }, ), SwitchSettingTile( - title: Text( - context.l10n.preferencesConfirmResignationAndDrawOffers, - ), + title: Text(context.l10n.preferencesConfirmResignationAndDrawOffers), value: data.confirmResign.value, - onChanged: isLoading - ? null - : (value) { - _setPref( - () => ref - .read(accountPreferencesProvider.notifier) - .setConfirmResign(BooleanPref(value)), - ); - }, + onChanged: + isLoading + ? null + : (value) { + _setPref( + () => ref + .read(accountPreferencesProvider.notifier) + .setConfirmResign(BooleanPref(value)), + ); + }, ), SettingsListTile( - settingsLabel: Text( - context.l10n.preferencesTakebacksWithOpponentApproval, - ), + settingsLabel: Text(context.l10n.preferencesTakebacksWithOpponentApproval), settingsValue: data.takeback.label(context), showCupertinoTrailingValue: false, onTap: () { @@ -192,32 +175,28 @@ class _AccountPreferencesScreenState choices: Takeback.values, selectedItem: data.takeback, labelBuilder: (t) => Text(t.label(context)), - onSelectedItemChanged: isLoading - ? null - : (Takeback? value) { - _setPref( - () => ref - .read( - accountPreferencesProvider.notifier, - ) - .setTakeback(value ?? data.takeback), - ); - }, + onSelectedItemChanged: + isLoading + ? null + : (Takeback? value) { + _setPref( + () => ref + .read(accountPreferencesProvider.notifier) + .setTakeback(value ?? data.takeback), + ); + }, ); } else { pushPlatformRoute( context, - title: context - .l10n.preferencesTakebacksWithOpponentApproval, + title: context.l10n.preferencesTakebacksWithOpponentApproval, builder: (context) => const TakebackSettingsScreen(), ); } }, ), SettingsListTile( - settingsLabel: Text( - context.l10n.preferencesPromoteToQueenAutomatically, - ), + settingsLabel: Text(context.l10n.preferencesPromoteToQueenAutomatically), settingsValue: data.autoQueen.label(context), showCupertinoTrailingValue: false, onTap: () { @@ -227,23 +206,21 @@ class _AccountPreferencesScreenState choices: AutoQueen.values, selectedItem: data.autoQueen, labelBuilder: (t) => Text(t.label(context)), - onSelectedItemChanged: isLoading - ? null - : (AutoQueen? value) { - _setPref( - () => ref - .read( - accountPreferencesProvider.notifier, - ) - .setAutoQueen(value ?? data.autoQueen), - ); - }, + onSelectedItemChanged: + isLoading + ? null + : (AutoQueen? value) { + _setPref( + () => ref + .read(accountPreferencesProvider.notifier) + .setAutoQueen(value ?? data.autoQueen), + ); + }, ); } else { pushPlatformRoute( context, - title: - context.l10n.preferencesPromoteToQueenAutomatically, + title: context.l10n.preferencesPromoteToQueenAutomatically, builder: (context) => const AutoQueenSettingsScreen(), ); } @@ -251,8 +228,7 @@ class _AccountPreferencesScreenState ), SettingsListTile( settingsLabel: Text( - context.l10n - .preferencesClaimDrawOnThreefoldRepetitionAutomatically, + context.l10n.preferencesClaimDrawOnThreefoldRepetitionAutomatically, ), settingsValue: data.autoThreefold.label(context), showCupertinoTrailingValue: false, @@ -263,35 +239,28 @@ class _AccountPreferencesScreenState choices: AutoThreefold.values, selectedItem: data.autoThreefold, labelBuilder: (t) => Text(t.label(context)), - onSelectedItemChanged: isLoading - ? null - : (AutoThreefold? value) { - _setPref( - () => ref - .read( - accountPreferencesProvider.notifier, - ) - .setAutoThreefold( - value ?? data.autoThreefold, - ), - ); - }, + onSelectedItemChanged: + isLoading + ? null + : (AutoThreefold? value) { + _setPref( + () => ref + .read(accountPreferencesProvider.notifier) + .setAutoThreefold(value ?? data.autoThreefold), + ); + }, ); } else { pushPlatformRoute( context, - title: context.l10n - .preferencesClaimDrawOnThreefoldRepetitionAutomatically, - builder: (context) => - const AutoThreefoldSettingsScreen(), + title: context.l10n.preferencesClaimDrawOnThreefoldRepetitionAutomatically, + builder: (context) => const AutoThreefoldSettingsScreen(), ); } }, ), SettingsListTile( - settingsLabel: Text( - context.l10n.preferencesMoveConfirmation, - ), + settingsLabel: Text(context.l10n.preferencesMoveConfirmation), settingsValue: data.submitMove.label(context), showCupertinoTrailingValue: false, onTap: () { @@ -310,8 +279,7 @@ class _AccountPreferencesScreenState } }); }, - explanation: context - .l10n.preferencesExplainCanThenBeTemporarilyDisabled, + explanation: context.l10n.preferencesExplainCanThenBeTemporarilyDisabled, ), ], ), @@ -320,9 +288,7 @@ class _AccountPreferencesScreenState hasLeading: false, children: [ SettingsListTile( - settingsLabel: Text( - context.l10n.preferencesGiveMoreTime, - ), + settingsLabel: Text(context.l10n.preferencesGiveMoreTime), settingsValue: data.moretime.label(context), showCupertinoTrailingValue: false, onTap: () { @@ -332,17 +298,16 @@ class _AccountPreferencesScreenState choices: Moretime.values, selectedItem: data.moretime, labelBuilder: (t) => Text(t.label(context)), - onSelectedItemChanged: isLoading - ? null - : (Moretime? value) { - _setPref( - () => ref - .read( - accountPreferencesProvider.notifier, - ) - .setMoretime(value ?? data.moretime), - ); - }, + onSelectedItemChanged: + isLoading + ? null + : (Moretime? value) { + _setPref( + () => ref + .read(accountPreferencesProvider.notifier) + .setMoretime(value ?? data.moretime), + ); + }, ); } else { pushPlatformRoute( @@ -354,18 +319,18 @@ class _AccountPreferencesScreenState }, ), SwitchSettingTile( - title: - Text(context.l10n.preferencesSoundWhenTimeGetsCritical), + title: Text(context.l10n.preferencesSoundWhenTimeGetsCritical), value: data.clockSound.value, - onChanged: isLoading - ? null - : (value) { - _setPref( - () => ref - .read(accountPreferencesProvider.notifier) - .setClockSound(BooleanPref(value)), - ); - }, + onChanged: + isLoading + ? null + : (value) { + _setPref( + () => ref + .read(accountPreferencesProvider.notifier) + .setClockSound(BooleanPref(value)), + ); + }, ), ], ), @@ -374,24 +339,21 @@ class _AccountPreferencesScreenState hasLeading: false, children: [ SwitchSettingTile( - title: Text( - context.l10n.letOtherPlayersFollowYou, - ), + title: Text(context.l10n.letOtherPlayersFollowYou), value: data.follow.value, - onChanged: isLoading - ? null - : (value) { - _setPref( - () => ref - .read(accountPreferencesProvider.notifier) - .setFollow(BooleanPref(value)), - ); - }, + onChanged: + isLoading + ? null + : (value) { + _setPref( + () => ref + .read(accountPreferencesProvider.notifier) + .setFollow(BooleanPref(value)), + ); + }, ), SettingsListTile( - settingsLabel: Text( - context.l10n.letOtherPlayersChallengeYou, - ), + settingsLabel: Text(context.l10n.letOtherPlayersChallengeYou), settingsValue: data.challenge.label(context), showCupertinoTrailingValue: false, onTap: () { @@ -401,17 +363,16 @@ class _AccountPreferencesScreenState choices: Challenge.values, selectedItem: data.challenge, labelBuilder: (t) => Text(t.label(context)), - onSelectedItemChanged: isLoading - ? null - : (Challenge? value) { - _setPref( - () => ref - .read( - accountPreferencesProvider.notifier, - ) - .setChallenge(value ?? data.challenge), - ); - }, + onSelectedItemChanged: + isLoading + ? null + : (Challenge? value) { + _setPref( + () => ref + .read(accountPreferencesProvider.notifier) + .setChallenge(value ?? data.challenge), + ); + }, ); } else { pushPlatformRoute( @@ -429,18 +390,14 @@ class _AccountPreferencesScreenState }, loading: () => const Center(child: CircularProgressIndicator()), error: (err, _) { - return FullScreenRetryRequest( - onRetry: () => ref.invalidate(accountPreferencesProvider), - ); + return FullScreenRetryRequest(onRetry: () => ref.invalidate(accountPreferencesProvider)); }, ); return PlatformScaffold( appBar: PlatformAppBar( title: Text(context.l10n.preferencesPreferences), - actions: [ - if (isLoading) const PlatformAppBarLoadingIndicator(), - ], + actions: [if (isLoading) const PlatformAppBarLoadingIndicator()], ), body: content, ); @@ -463,15 +420,12 @@ class _ZenSettingsScreenState extends ConsumerState { return accountPrefs.when( data: (data) { if (data == null) { - return Center( - child: Text(context.l10n.mobileMustBeLoggedIn), - ); + return Center(child: Text(context.l10n.mobileMustBeLoggedIn)); } return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( - trailing: - isLoading ? const CircularProgressIndicator.adaptive() : null, + trailing: isLoading ? const CircularProgressIndicator.adaptive() : null, ), child: SafeArea( child: ListView( @@ -480,22 +434,23 @@ class _ZenSettingsScreenState extends ConsumerState { choices: Zen.values, selectedItem: data.zenMode, titleBuilder: (t) => Text(t.label(context)), - onSelectedItemChanged: isLoading - ? null - : (Zen? v) async { - setState(() { - isLoading = true; - }); - try { - await ref - .read(accountPreferencesProvider.notifier) - .setZen(v ?? data.zenMode); - } finally { + onSelectedItemChanged: + isLoading + ? null + : (Zen? v) async { setState(() { - isLoading = false; + isLoading = true; }); - } - }, + try { + await ref + .read(accountPreferencesProvider.notifier) + .setZen(v ?? data.zenMode); + } finally { + setState(() { + isLoading = false; + }); + } + }, ), ], ), @@ -512,12 +467,10 @@ class PieceNotationSettingsScreen extends ConsumerStatefulWidget { const PieceNotationSettingsScreen({super.key}); @override - ConsumerState createState() => - _PieceNotationSettingsScreenState(); + ConsumerState createState() => _PieceNotationSettingsScreenState(); } -class _PieceNotationSettingsScreenState - extends ConsumerState { +class _PieceNotationSettingsScreenState extends ConsumerState { Future? _pendingSetPieceNotation; @override @@ -526,9 +479,7 @@ class _PieceNotationSettingsScreenState return accountPrefs.when( data: (data) { if (data == null) { - return Center( - child: Text(context.l10n.mobileMustBeLoggedIn), - ); + return Center(child: Text(context.l10n.mobileMustBeLoggedIn)); } return FutureBuilder( @@ -536,9 +487,10 @@ class _PieceNotationSettingsScreenState builder: (context, snapshot) { return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( - trailing: snapshot.connectionState == ConnectionState.waiting - ? const CircularProgressIndicator.adaptive() - : null, + trailing: + snapshot.connectionState == ConnectionState.waiting + ? const CircularProgressIndicator.adaptive() + : null, ), child: SafeArea( child: ListView( @@ -547,17 +499,17 @@ class _PieceNotationSettingsScreenState choices: PieceNotation.values, selectedItem: data.pieceNotation, titleBuilder: (t) => Text(t.label(context)), - onSelectedItemChanged: snapshot.connectionState == - ConnectionState.waiting - ? null - : (PieceNotation? v) { - final future = ref - .read(accountPreferencesProvider.notifier) - .setPieceNotation(v ?? data.pieceNotation); - setState(() { - _pendingSetPieceNotation = future; - }); - }, + onSelectedItemChanged: + snapshot.connectionState == ConnectionState.waiting + ? null + : (PieceNotation? v) { + final future = ref + .read(accountPreferencesProvider.notifier) + .setPieceNotation(v ?? data.pieceNotation); + setState(() { + _pendingSetPieceNotation = future; + }); + }, ), ], ), @@ -576,12 +528,10 @@ class TakebackSettingsScreen extends ConsumerStatefulWidget { const TakebackSettingsScreen({super.key}); @override - ConsumerState createState() => - _TakebackSettingsScreenState(); + ConsumerState createState() => _TakebackSettingsScreenState(); } -class _TakebackSettingsScreenState - extends ConsumerState { +class _TakebackSettingsScreenState extends ConsumerState { bool isLoading = false; @override @@ -590,15 +540,12 @@ class _TakebackSettingsScreenState return accountPrefs.when( data: (data) { if (data == null) { - return Center( - child: Text(context.l10n.mobileMustBeLoggedIn), - ); + return Center(child: Text(context.l10n.mobileMustBeLoggedIn)); } return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( - trailing: - isLoading ? const CircularProgressIndicator.adaptive() : null, + trailing: isLoading ? const CircularProgressIndicator.adaptive() : null, ), child: SafeArea( child: ListView( @@ -607,22 +554,23 @@ class _TakebackSettingsScreenState choices: Takeback.values, selectedItem: data.takeback, titleBuilder: (t) => Text(t.label(context)), - onSelectedItemChanged: isLoading - ? null - : (Takeback? v) async { - setState(() { - isLoading = true; - }); - try { - await ref - .read(accountPreferencesProvider.notifier) - .setTakeback(v ?? data.takeback); - } finally { + onSelectedItemChanged: + isLoading + ? null + : (Takeback? v) async { setState(() { - isLoading = false; + isLoading = true; }); - } - }, + try { + await ref + .read(accountPreferencesProvider.notifier) + .setTakeback(v ?? data.takeback); + } finally { + setState(() { + isLoading = false; + }); + } + }, ), ], ), @@ -639,12 +587,10 @@ class AutoQueenSettingsScreen extends ConsumerStatefulWidget { const AutoQueenSettingsScreen({super.key}); @override - ConsumerState createState() => - _AutoQueenSettingsScreenState(); + ConsumerState createState() => _AutoQueenSettingsScreenState(); } -class _AutoQueenSettingsScreenState - extends ConsumerState { +class _AutoQueenSettingsScreenState extends ConsumerState { Future? _pendingSetAutoQueen; @override @@ -653,9 +599,7 @@ class _AutoQueenSettingsScreenState return accountPrefs.when( data: (data) { if (data == null) { - return Center( - child: Text(context.l10n.mobileMustBeLoggedIn), - ); + return Center(child: Text(context.l10n.mobileMustBeLoggedIn)); } return FutureBuilder( @@ -663,9 +607,10 @@ class _AutoQueenSettingsScreenState builder: (context, snapshot) { return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( - trailing: snapshot.connectionState == ConnectionState.waiting - ? const CircularProgressIndicator.adaptive() - : null, + trailing: + snapshot.connectionState == ConnectionState.waiting + ? const CircularProgressIndicator.adaptive() + : null, ), child: SafeArea( child: ListView( @@ -678,13 +623,13 @@ class _AutoQueenSettingsScreenState snapshot.connectionState == ConnectionState.waiting ? null : (AutoQueen? v) { - final future = ref - .read(accountPreferencesProvider.notifier) - .setAutoQueen(v ?? data.autoQueen); - setState(() { - _pendingSetAutoQueen = future; - }); - }, + final future = ref + .read(accountPreferencesProvider.notifier) + .setAutoQueen(v ?? data.autoQueen); + setState(() { + _pendingSetAutoQueen = future; + }); + }, ), ], ), @@ -703,12 +648,10 @@ class AutoThreefoldSettingsScreen extends ConsumerStatefulWidget { const AutoThreefoldSettingsScreen({super.key}); @override - ConsumerState createState() => - _AutoThreefoldSettingsScreenState(); + ConsumerState createState() => _AutoThreefoldSettingsScreenState(); } -class _AutoThreefoldSettingsScreenState - extends ConsumerState { +class _AutoThreefoldSettingsScreenState extends ConsumerState { Future? _pendingSetAutoThreefold; @override @@ -717,9 +660,7 @@ class _AutoThreefoldSettingsScreenState return accountPrefs.when( data: (data) { if (data == null) { - return Center( - child: Text(context.l10n.mobileMustBeLoggedIn), - ); + return Center(child: Text(context.l10n.mobileMustBeLoggedIn)); } return FutureBuilder( @@ -727,9 +668,10 @@ class _AutoThreefoldSettingsScreenState builder: (context, snapshot) { return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( - trailing: snapshot.connectionState == ConnectionState.waiting - ? const CircularProgressIndicator.adaptive() - : null, + trailing: + snapshot.connectionState == ConnectionState.waiting + ? const CircularProgressIndicator.adaptive() + : null, ), child: SafeArea( child: ListView( @@ -738,17 +680,17 @@ class _AutoThreefoldSettingsScreenState choices: AutoThreefold.values, selectedItem: data.autoThreefold, titleBuilder: (t) => Text(t.label(context)), - onSelectedItemChanged: snapshot.connectionState == - ConnectionState.waiting - ? null - : (AutoThreefold? v) { - final future = ref - .read(accountPreferencesProvider.notifier) - .setAutoThreefold(v ?? data.autoThreefold); - setState(() { - _pendingSetAutoThreefold = future; - }); - }, + onSelectedItemChanged: + snapshot.connectionState == ConnectionState.waiting + ? null + : (AutoThreefold? v) { + final future = ref + .read(accountPreferencesProvider.notifier) + .setAutoThreefold(v ?? data.autoThreefold); + setState(() { + _pendingSetAutoThreefold = future; + }); + }, ), ], ), @@ -767,12 +709,10 @@ class MoretimeSettingsScreen extends ConsumerStatefulWidget { const MoretimeSettingsScreen({super.key}); @override - ConsumerState createState() => - _MoretimeSettingsScreenState(); + ConsumerState createState() => _MoretimeSettingsScreenState(); } -class _MoretimeSettingsScreenState - extends ConsumerState { +class _MoretimeSettingsScreenState extends ConsumerState { Future? _pendingSetMoretime; @override @@ -781,9 +721,7 @@ class _MoretimeSettingsScreenState return accountPrefs.when( data: (data) { if (data == null) { - return Center( - child: Text(context.l10n.mobileMustBeLoggedIn), - ); + return Center(child: Text(context.l10n.mobileMustBeLoggedIn)); } return FutureBuilder( @@ -791,9 +729,10 @@ class _MoretimeSettingsScreenState builder: (context, snapshot) { return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( - trailing: snapshot.connectionState == ConnectionState.waiting - ? const CircularProgressIndicator.adaptive() - : null, + trailing: + snapshot.connectionState == ConnectionState.waiting + ? const CircularProgressIndicator.adaptive() + : null, ), child: SafeArea( child: ListView( @@ -802,16 +741,16 @@ class _MoretimeSettingsScreenState choices: Moretime.values, selectedItem: data.moretime, titleBuilder: (t) => Text(t.label(context)), - onSelectedItemChanged: snapshot.connectionState == - ConnectionState.waiting - ? null - : (Moretime? v) { - setState(() { - _pendingSetMoretime = ref - .read(accountPreferencesProvider.notifier) - .setMoretime(v ?? data.moretime); - }); - }, + onSelectedItemChanged: + snapshot.connectionState == ConnectionState.waiting + ? null + : (Moretime? v) { + setState(() { + _pendingSetMoretime = ref + .read(accountPreferencesProvider.notifier) + .setMoretime(v ?? data.moretime); + }); + }, ), ], ), @@ -830,12 +769,10 @@ class _ChallengeSettingsScreen extends ConsumerStatefulWidget { const _ChallengeSettingsScreen(); @override - ConsumerState<_ChallengeSettingsScreen> createState() => - _ChallengeSettingsScreenState(); + ConsumerState<_ChallengeSettingsScreen> createState() => _ChallengeSettingsScreenState(); } -class _ChallengeSettingsScreenState - extends ConsumerState<_ChallengeSettingsScreen> { +class _ChallengeSettingsScreenState extends ConsumerState<_ChallengeSettingsScreen> { Future? _pendingSetChallenge; @override @@ -844,9 +781,7 @@ class _ChallengeSettingsScreenState return accountPrefs.when( data: (data) { if (data == null) { - return Center( - child: Text(context.l10n.mobileMustBeLoggedIn), - ); + return Center(child: Text(context.l10n.mobileMustBeLoggedIn)); } return FutureBuilder( @@ -854,9 +789,10 @@ class _ChallengeSettingsScreenState builder: (context, snapshot) { return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( - trailing: snapshot.connectionState == ConnectionState.waiting - ? const CircularProgressIndicator.adaptive() - : null, + trailing: + snapshot.connectionState == ConnectionState.waiting + ? const CircularProgressIndicator.adaptive() + : null, ), child: SafeArea( child: ListView( @@ -869,13 +805,13 @@ class _ChallengeSettingsScreenState snapshot.connectionState == ConnectionState.waiting ? null : (Challenge? v) { - final future = ref - .read(accountPreferencesProvider.notifier) - .setChallenge(v ?? data.challenge); - setState(() { - _pendingSetChallenge = future; - }); - }, + final future = ref + .read(accountPreferencesProvider.notifier) + .setChallenge(v ?? data.challenge); + setState(() { + _pendingSetChallenge = future; + }); + }, ), ], ), diff --git a/lib/src/view/settings/app_background_mode_screen.dart b/lib/src/view/settings/app_background_mode_screen.dart index e24084c562..b268bb78a8 100644 --- a/lib/src/view/settings/app_background_mode_screen.dart +++ b/lib/src/view/settings/app_background_mode_screen.dart @@ -11,24 +11,15 @@ class AppBackgroundModeScreen extends StatelessWidget { @override Widget build(BuildContext context) { - return PlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, - ); + return PlatformWidget(androidBuilder: _androidBuilder, iosBuilder: _iosBuilder); } Widget _androidBuilder(BuildContext context) { - return Scaffold( - appBar: AppBar(title: Text(context.l10n.background)), - body: _Body(), - ); + return Scaffold(appBar: AppBar(title: Text(context.l10n.background)), body: _Body()); } Widget _iosBuilder(BuildContext context) { - return CupertinoPageScaffold( - navigationBar: const CupertinoNavigationBar(), - child: _Body(), - ); + return CupertinoPageScaffold(navigationBar: const CupertinoNavigationBar(), child: _Body()); } static String themeTitle(BuildContext context, BackgroundThemeMode theme) { @@ -46,9 +37,7 @@ class AppBackgroundModeScreen extends StatelessWidget { class _Body extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final themeMode = ref.watch( - generalPreferencesProvider.select((state) => state.themeMode), - ); + final themeMode = ref.watch(generalPreferencesProvider.select((state) => state.themeMode)); void onChanged(BackgroundThemeMode? value) => ref .read(generalPreferencesProvider.notifier) @@ -60,8 +49,7 @@ class _Body extends ConsumerWidget { ChoicePicker( choices: BackgroundThemeMode.values, selectedItem: themeMode, - titleBuilder: (t) => - Text(AppBackgroundModeScreen.themeTitle(context, t)), + titleBuilder: (t) => Text(AppBackgroundModeScreen.themeTitle(context, t)), onSelectedItemChanged: onChanged, ), ], diff --git a/lib/src/view/settings/board_settings_screen.dart b/lib/src/view/settings/board_settings_screen.dart index 48532353d1..61d3be6fda 100644 --- a/lib/src/view/settings/board_settings_screen.dart +++ b/lib/src/view/settings/board_settings_screen.dart @@ -19,24 +19,15 @@ class BoardSettingsScreen extends StatelessWidget { @override Widget build(BuildContext context) { - return PlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, - ); + return PlatformWidget(androidBuilder: _androidBuilder, iosBuilder: _iosBuilder); } Widget _androidBuilder(BuildContext context) { - return Scaffold( - appBar: AppBar(title: Text(context.l10n.board)), - body: const _Body(), - ); + return Scaffold(appBar: AppBar(title: Text(context.l10n.board)), body: const _Body()); } Widget _iosBuilder(BuildContext context) { - return const CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar(), - child: _Body(), - ); + return const CupertinoPageScaffold(navigationBar: CupertinoNavigationBar(), child: _Body()); } } @@ -58,8 +49,7 @@ class _Body extends ConsumerWidget { children: [ SettingsListTile( settingsLabel: Text(context.l10n.preferencesHowDoYouMovePieces), - settingsValue: - pieceShiftMethodl10n(context, boardPrefs.pieceShiftMethod), + settingsValue: pieceShiftMethodl10n(context, boardPrefs.pieceShiftMethod), showCupertinoTrailingValue: false, onTap: () { if (Theme.of(context).platform == TargetPlatform.android) { @@ -71,17 +61,14 @@ class _Body extends ConsumerWidget { onSelectedItemChanged: (PieceShiftMethod? value) { ref .read(boardPreferencesProvider.notifier) - .setPieceShiftMethod( - value ?? PieceShiftMethod.either, - ); + .setPieceShiftMethod(value ?? PieceShiftMethod.either); }, ); } else { pushPlatformRoute( context, title: context.l10n.preferencesHowDoYouMovePieces, - builder: (context) => - const PieceShiftMethodSettingsScreen(), + builder: (context) => const PieceShiftMethodSettingsScreen(), ); } }, @@ -90,9 +77,7 @@ class _Body extends ConsumerWidget { title: Text(context.l10n.mobilePrefMagnifyDraggedPiece), value: boardPrefs.magnifyDraggedPiece, onChanged: (value) { - ref - .read(boardPreferencesProvider.notifier) - .toggleMagnifyDraggedPiece(); + ref.read(boardPreferencesProvider.notifier).toggleMagnifyDraggedPiece(); }, ), SettingsListTile( @@ -112,9 +97,7 @@ class _Body extends ConsumerWidget { onSelectedItemChanged: (DragTargetKind? value) { ref .read(boardPreferencesProvider.notifier) - .setDragTargetKind( - value ?? DragTargetKind.circle, - ); + .setDragTargetKind(value ?? DragTargetKind.circle); }, ); } else { @@ -137,20 +120,14 @@ class _Body extends ConsumerWidget { textAlign: TextAlign.justify, ), onChanged: (value) { - ref - .read(boardPreferencesProvider.notifier) - .toggleHapticFeedback(); + ref.read(boardPreferencesProvider.notifier).toggleHapticFeedback(); }, ), SwitchSettingTile( - title: Text( - context.l10n.preferencesPieceAnimation, - ), + title: Text(context.l10n.preferencesPieceAnimation), value: boardPrefs.pieceAnimation, onChanged: (value) { - ref - .read(boardPreferencesProvider.notifier) - .togglePieceAnimation(); + ref.read(boardPreferencesProvider.notifier).togglePieceAnimation(); }, ), SwitchSettingTile( @@ -164,9 +141,7 @@ class _Body extends ConsumerWidget { ), value: boardPrefs.enableShapeDrawings, onChanged: (value) { - ref - .read(boardPreferencesProvider.notifier) - .toggleEnableShapeDrawings(); + ref.read(boardPreferencesProvider.notifier).toggleEnableShapeDrawings(); }, ), ], @@ -176,25 +151,26 @@ class _Body extends ConsumerWidget { hasLeading: false, showDivider: false, children: [ - if (Theme.of(context).platform == TargetPlatform.android && - !isTabletOrLarger(context)) + if (Theme.of(context).platform == TargetPlatform.android && !isTabletOrLarger(context)) androidVersionAsync.maybeWhen( - data: (version) => version != null && version.sdkInt >= 29 - ? SwitchSettingTile( - title: Text(context.l10n.mobileSettingsImmersiveMode), - subtitle: Text( - context.l10n.mobileSettingsImmersiveModeSubtitle, - textAlign: TextAlign.justify, - maxLines: 5, - ), - value: boardPrefs.immersiveModeWhilePlaying ?? false, - onChanged: (value) { - ref - .read(boardPreferencesProvider.notifier) - .toggleImmersiveModeWhilePlaying(); - }, - ) - : const SizedBox.shrink(), + data: + (version) => + version != null && version.sdkInt >= 29 + ? SwitchSettingTile( + title: Text(context.l10n.mobileSettingsImmersiveMode), + subtitle: Text( + context.l10n.mobileSettingsImmersiveModeSubtitle, + textAlign: TextAlign.justify, + maxLines: 5, + ), + value: boardPrefs.immersiveModeWhilePlaying ?? false, + onChanged: (value) { + ref + .read(boardPreferencesProvider.notifier) + .toggleImmersiveModeWhilePlaying(); + }, + ) + : const SizedBox.shrink(), orElse: () => const SizedBox.shrink(), ), SettingsListTile( @@ -208,9 +184,10 @@ class _Body extends ConsumerWidget { choices: ClockPosition.values, selectedItem: boardPrefs.clockPosition, labelBuilder: (t) => Text(t.label), - onSelectedItemChanged: (ClockPosition? value) => ref - .read(boardPreferencesProvider.notifier) - .setClockPosition(value ?? ClockPosition.right), + onSelectedItemChanged: + (ClockPosition? value) => ref + .read(boardPreferencesProvider.notifier) + .setClockPosition(value ?? ClockPosition.right), ); } else { pushPlatformRoute( @@ -222,31 +199,22 @@ class _Body extends ConsumerWidget { }, ), SwitchSettingTile( - title: Text( - context.l10n.preferencesPieceDestinations, - ), + title: Text(context.l10n.preferencesPieceDestinations), value: boardPrefs.showLegalMoves, onChanged: (value) { - ref - .read(boardPreferencesProvider.notifier) - .toggleShowLegalMoves(); + ref.read(boardPreferencesProvider.notifier).toggleShowLegalMoves(); }, ), SwitchSettingTile( - title: Text( - context.l10n.preferencesBoardHighlights, - ), + title: Text(context.l10n.preferencesBoardHighlights), value: boardPrefs.boardHighlights, onChanged: (value) { - ref - .read(boardPreferencesProvider.notifier) - .toggleBoardHighlights(); + ref.read(boardPreferencesProvider.notifier).toggleBoardHighlights(); }, ), SettingsListTile( settingsLabel: const Text('Material'), //TODO: l10n - settingsValue: boardPrefs.materialDifferenceFormat - .l10n(AppLocalizations.of(context)), + settingsValue: boardPrefs.materialDifferenceFormat.l10n(AppLocalizations.of(context)), onTap: () { if (Theme.of(context).platform == TargetPlatform.android) { showChoicePicker( @@ -254,20 +222,18 @@ class _Body extends ConsumerWidget { choices: MaterialDifferenceFormat.values, selectedItem: boardPrefs.materialDifferenceFormat, labelBuilder: (t) => Text(t.label), - onSelectedItemChanged: (MaterialDifferenceFormat? value) => - ref + onSelectedItemChanged: + (MaterialDifferenceFormat? value) => ref .read(boardPreferencesProvider.notifier) .setMaterialDifferenceFormat( - value ?? - MaterialDifferenceFormat.materialDifference, + value ?? MaterialDifferenceFormat.materialDifference, ), ); } else { pushPlatformRoute( context, title: 'Material', - builder: (context) => - const MaterialDifferenceFormatScreen(), + builder: (context) => const MaterialDifferenceFormatScreen(), ); } }, @@ -285,9 +251,7 @@ class PieceShiftMethodSettingsScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final pieceShiftMethod = ref.watch( - boardPreferencesProvider.select( - (state) => state.pieceShiftMethod, - ), + boardPreferencesProvider.select((state) => state.pieceShiftMethod), ); void onChanged(PieceShiftMethod? value) { @@ -323,9 +287,8 @@ class BoardClockPositionScreen extends ConsumerWidget { final clockPosition = ref.watch( boardPreferencesProvider.select((state) => state.clockPosition), ); - void onChanged(ClockPosition? value) => ref - .read(boardPreferencesProvider.notifier) - .setClockPosition(value ?? ClockPosition.right); + void onChanged(ClockPosition? value) => + ref.read(boardPreferencesProvider.notifier).setClockPosition(value ?? ClockPosition.right); return CupertinoPageScaffold( navigationBar: const CupertinoNavigationBar(), child: SafeArea( @@ -350,13 +313,11 @@ class MaterialDifferenceFormatScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final materialDifferenceFormat = ref.watch( - boardPreferencesProvider - .select((state) => state.materialDifferenceFormat), + boardPreferencesProvider.select((state) => state.materialDifferenceFormat), ); - void onChanged(MaterialDifferenceFormat? value) => - ref.read(boardPreferencesProvider.notifier).setMaterialDifferenceFormat( - value ?? MaterialDifferenceFormat.materialDifference, - ); + void onChanged(MaterialDifferenceFormat? value) => ref + .read(boardPreferencesProvider.notifier) + .setMaterialDifferenceFormat(value ?? MaterialDifferenceFormat.materialDifference); return CupertinoPageScaffold( navigationBar: const CupertinoNavigationBar(), child: ListView( @@ -379,15 +340,11 @@ class DragTargetKindSettingsScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final dragTargetKind = ref.watch( - boardPreferencesProvider.select( - (state) => state.dragTargetKind, - ), + boardPreferencesProvider.select((state) => state.dragTargetKind), ); void onChanged(DragTargetKind? value) { - ref - .read(boardPreferencesProvider.notifier) - .setDragTargetKind(value ?? DragTargetKind.circle); + ref.read(boardPreferencesProvider.notifier).setDragTargetKind(value ?? DragTargetKind.circle); } return CupertinoPageScaffold( @@ -396,11 +353,8 @@ class DragTargetKindSettingsScreen extends ConsumerWidget { child: ListView( children: [ Padding( - padding: - Styles.horizontalBodyPadding.add(Styles.sectionTopPadding), - child: const Text( - 'How the target square is highlighted when dragging a piece.', - ), + padding: Styles.horizontalBodyPadding.add(Styles.sectionTopPadding), + child: const Text('How the target square is highlighted when dragging a piece.'), ), ChoicePicker( notchedTile: true, @@ -416,10 +370,7 @@ class DragTargetKindSettingsScreen extends ConsumerWidget { } } -String pieceShiftMethodl10n( - BuildContext context, - PieceShiftMethod pieceShiftMethod, -) => +String pieceShiftMethodl10n(BuildContext context, PieceShiftMethod pieceShiftMethod) => switch (pieceShiftMethod) { // TODO add this to mobile translations PieceShiftMethod.either => 'Either tap or drag', diff --git a/lib/src/view/settings/board_theme_screen.dart b/lib/src/view/settings/board_theme_screen.dart index c005919905..6e3e128a1f 100644 --- a/lib/src/view/settings/board_theme_screen.dart +++ b/lib/src/view/settings/board_theme_screen.dart @@ -13,60 +13,46 @@ class BoardThemeScreen extends StatelessWidget { @override Widget build(BuildContext context) { - return PlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, - ); + return PlatformWidget(androidBuilder: _androidBuilder, iosBuilder: _iosBuilder); } Widget _androidBuilder(BuildContext context) { - return Scaffold( - appBar: AppBar(title: Text(context.l10n.board)), - body: _Body(), - ); + return Scaffold(appBar: AppBar(title: Text(context.l10n.board)), body: _Body()); } Widget _iosBuilder(BuildContext context) { - return CupertinoPageScaffold( - navigationBar: const CupertinoNavigationBar(), - child: _Body(), - ); + return CupertinoPageScaffold(navigationBar: const CupertinoNavigationBar(), child: _Body()); } } class _Body extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final boardTheme = - ref.watch(boardPreferencesProvider.select((p) => p.boardTheme)); + final boardTheme = ref.watch(boardPreferencesProvider.select((p) => p.boardTheme)); - final hasSystemColors = - ref.watch(generalPreferencesProvider.select((p) => p.systemColors)); + final hasSystemColors = ref.watch(generalPreferencesProvider.select((p) => p.systemColors)); - final androidVersion = ref.watch(androidVersionProvider).whenOrNull( - data: (v) => v, - ); + final androidVersion = ref.watch(androidVersionProvider).whenOrNull(data: (v) => v); - final choices = BoardTheme.values - .where( - (t) => - t != BoardTheme.system || - (hasSystemColors && - androidVersion != null && - androidVersion.sdkInt >= 31), - ) - .toList(); + final choices = + BoardTheme.values + .where( + (t) => + t != BoardTheme.system || + (hasSystemColors && androidVersion != null && androidVersion.sdkInt >= 31), + ) + .toList(); - void onChanged(BoardTheme? value) => ref - .read(boardPreferencesProvider.notifier) - .setBoardTheme(value ?? BoardTheme.brown); + void onChanged(BoardTheme? value) => + ref.read(boardPreferencesProvider.notifier).setBoardTheme(value ?? BoardTheme.brown); - final checkedIcon = Theme.of(context).platform == TargetPlatform.android - ? const Icon(Icons.check) - : Icon( - CupertinoIcons.check_mark_circled_solid, - color: CupertinoTheme.of(context).primaryColor, - ); + final checkedIcon = + Theme.of(context).platform == TargetPlatform.android + ? const Icon(Icons.check) + : Icon( + CupertinoIcons.check_mark_circled_solid, + color: CupertinoTheme.of(context).primaryColor, + ); return SafeArea( child: ListView.separated( @@ -80,15 +66,13 @@ class _Body extends ConsumerWidget { onTap: () => onChanged(t), ); }, - separatorBuilder: (_, __) => PlatformDivider( - height: 1, - // on iOS: 14 (default indent) + 16 (padding) - indent: - Theme.of(context).platform == TargetPlatform.iOS ? 14 + 16 : null, - color: Theme.of(context).platform == TargetPlatform.iOS - ? null - : Colors.transparent, - ), + separatorBuilder: + (_, __) => PlatformDivider( + height: 1, + // on iOS: 14 (default indent) + 16 (padding) + indent: Theme.of(context).platform == TargetPlatform.iOS ? 14 + 16 : null, + color: Theme.of(context).platform == TargetPlatform.iOS ? null : Colors.transparent, + ), itemCount: choices.length, ), ); diff --git a/lib/src/view/settings/piece_set_screen.dart b/lib/src/view/settings/piece_set_screen.dart index dcaad14fe2..a28a3163e9 100644 --- a/lib/src/view/settings/piece_set_screen.dart +++ b/lib/src/view/settings/piece_set_screen.dart @@ -53,34 +53,30 @@ class _PieceSetScreenState extends ConsumerState { return PlatformScaffold( appBar: PlatformAppBar( title: Text(context.l10n.pieceSet), - actions: [ - if (isLoading) const PlatformAppBarLoadingIndicator(), - ], + actions: [if (isLoading) const PlatformAppBarLoadingIndicator()], ), body: SafeArea( child: ListView.separated( itemCount: PieceSet.values.length, - separatorBuilder: (_, __) => PlatformDivider( - height: 1, - // on iOS: 14 (default indent) + 16 (padding) - indent: Theme.of(context).platform == TargetPlatform.iOS - ? 14 + 16 - : null, - color: Theme.of(context).platform == TargetPlatform.iOS - ? null - : Colors.transparent, - ), + separatorBuilder: + (_, __) => PlatformDivider( + height: 1, + // on iOS: 14 (default indent) + 16 (padding) + indent: Theme.of(context).platform == TargetPlatform.iOS ? 14 + 16 : null, + color: Theme.of(context).platform == TargetPlatform.iOS ? null : Colors.transparent, + ), itemBuilder: (context, index) { final pieceSet = PieceSet.values[index]; return PlatformListTile( - trailing: boardPrefs.pieceSet == pieceSet - ? Theme.of(context).platform == TargetPlatform.android - ? const Icon(Icons.check) - : Icon( - CupertinoIcons.check_mark_circled_solid, - color: CupertinoTheme.of(context).primaryColor, - ) - : null, + trailing: + boardPrefs.pieceSet == pieceSet + ? Theme.of(context).platform == TargetPlatform.android + ? const Icon(Icons.check) + : Icon( + CupertinoIcons.check_mark_circled_solid, + color: CupertinoTheme.of(context).primaryColor, + ) + : null, title: Text(pieceSet.label), subtitle: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 264), @@ -89,11 +85,7 @@ class _PieceSetScreenState extends ConsumerState { boardPrefs.boardTheme.thumbnail, Row( children: [ - for (final img in getPieceImages(pieceSet)) - Image( - image: img, - height: 44, - ), + for (final img in getPieceImages(pieceSet)) Image(image: img, height: 44), ], ), ], diff --git a/lib/src/view/settings/settings_tab_screen.dart b/lib/src/view/settings/settings_tab_screen.dart index 15d755668c..f1dbdc0cfb 100644 --- a/lib/src/view/settings/settings_tab_screen.dart +++ b/lib/src/view/settings/settings_tab_screen.dart @@ -54,9 +54,7 @@ class SettingsTabScreen extends ConsumerWidget { } }, child: Scaffold( - appBar: AppBar( - title: Text(context.l10n.settingsSettings), - ), + appBar: AppBar(title: Text(context.l10n.settingsSettings)), body: SafeArea(child: _Body()), ), ); @@ -66,13 +64,8 @@ class SettingsTabScreen extends ConsumerWidget { return CupertinoPageScaffold( child: CustomScrollView( slivers: [ - CupertinoSliverNavigationBar( - largeTitle: Text(context.l10n.settingsSettings), - ), - SliverSafeArea( - top: false, - sliver: _Body(), - ), + CupertinoSliverNavigationBar(largeTitle: Text(context.l10n.settingsSettings)), + SliverSafeArea(top: false, sliver: _Body()), ], ), ); @@ -92,36 +85,34 @@ class _Body extends ConsumerWidget { final boardPrefs = ref.watch(boardPreferencesProvider); final authController = ref.watch(authControllerProvider); final userSession = ref.watch(authSessionProvider); - final packageInfo = - ref.read(preloadedDataProvider).requireValue.packageInfo; + final packageInfo = ref.read(preloadedDataProvider).requireValue.packageInfo; final dbSize = ref.watch(getDbSizeInBytesProvider); final Widget? donateButton = userSession == null || userSession.user.isPatron != true ? PlatformListTile( - leading: Icon( - LichessIcons.patron, - semanticLabel: context.l10n.patronLichessPatron, - color: context.lichessColors.brag, - ), - title: Text( - context.l10n.patronDonate, - style: TextStyle(color: context.lichessColors.brag), - ), - trailing: Theme.of(context).platform == TargetPlatform.iOS - ? const CupertinoListTileChevron() - : null, - onTap: () { - launchUrl(Uri.parse('https://lichess.org/patron')); - }, - ) + leading: Icon( + LichessIcons.patron, + semanticLabel: context.l10n.patronLichessPatron, + color: context.lichessColors.brag, + ), + title: Text( + context.l10n.patronDonate, + style: TextStyle(color: context.lichessColors.brag), + ), + trailing: + Theme.of(context).platform == TargetPlatform.iOS + ? const CupertinoListTileChevron() + : null, + onTap: () { + launchUrl(Uri.parse('https://lichess.org/patron')); + }, + ) : null; final List content = [ ListSection( - header: userSession != null - ? UserFullNameWidget(user: userSession.user) - : null, + header: userSession != null ? UserFullNameWidget(user: userSession.user) : null, hasLeading: true, showDivider: true, children: [ @@ -129,9 +120,10 @@ class _Body extends ConsumerWidget { PlatformListTile( leading: const Icon(Icons.person_outline), title: Text(context.l10n.profile), - trailing: Theme.of(context).platform == TargetPlatform.iOS - ? const CupertinoListTileChevron() - : null, + trailing: + Theme.of(context).platform == TargetPlatform.iOS + ? const CupertinoListTileChevron() + : null, onTap: () { ref.invalidate(accountActivityProvider); pushPlatformRoute( @@ -144,9 +136,10 @@ class _Body extends ConsumerWidget { PlatformListTile( leading: const Icon(Icons.manage_accounts_outlined), title: Text(context.l10n.preferencesPreferences), - trailing: Theme.of(context).platform == TargetPlatform.iOS - ? const CupertinoListTileChevron() - : null, + trailing: + Theme.of(context).platform == TargetPlatform.iOS + ? const CupertinoListTileChevron() + : null, onTap: () { pushPlatformRoute( context, @@ -183,8 +176,7 @@ class _Body extends ConsumerWidget { }, ), ], - if (Theme.of(context).platform == TargetPlatform.android && - donateButton != null) + if (Theme.of(context).platform == TargetPlatform.android && donateButton != null) donateButton, ], ), @@ -208,21 +200,18 @@ class _Body extends ConsumerWidget { SettingsListTile( icon: const Icon(Icons.brightness_medium_outlined), settingsLabel: Text(context.l10n.background), - settingsValue: AppBackgroundModeScreen.themeTitle( - context, - generalPrefs.themeMode, - ), + settingsValue: AppBackgroundModeScreen.themeTitle(context, generalPrefs.themeMode), onTap: () { if (Theme.of(context).platform == TargetPlatform.android) { showChoicePicker( context, choices: BackgroundThemeMode.values, selectedItem: generalPrefs.themeMode, - labelBuilder: (t) => - Text(AppBackgroundModeScreen.themeTitle(context, t)), - onSelectedItemChanged: (BackgroundThemeMode? value) => ref - .read(generalPreferencesProvider.notifier) - .setThemeMode(value ?? BackgroundThemeMode.system), + labelBuilder: (t) => Text(AppBackgroundModeScreen.themeTitle(context, t)), + onSelectedItemChanged: + (BackgroundThemeMode? value) => ref + .read(generalPreferencesProvider.notifier) + .setThemeMode(value ?? BackgroundThemeMode.system), ); } else { pushPlatformRoute( @@ -236,22 +225,18 @@ class _Body extends ConsumerWidget { SettingsListTile( icon: const Icon(Icons.palette_outlined), settingsLabel: Text(context.l10n.mobileTheme), - settingsValue: - '${boardPrefs.boardTheme.label} / ${boardPrefs.pieceSet.label}', + settingsValue: '${boardPrefs.boardTheme.label} / ${boardPrefs.pieceSet.label}', onTap: () { - pushPlatformRoute( - context, - title: 'Theme', - builder: (context) => const ThemeScreen(), - ); + pushPlatformRoute(context, title: 'Theme', builder: (context) => const ThemeScreen()); }, ), PlatformListTile( leading: const Icon(LichessIcons.chess_board), title: Text(context.l10n.board), - trailing: Theme.of(context).platform == TargetPlatform.iOS - ? const CupertinoListTileChevron() - : null, + trailing: + Theme.of(context).platform == TargetPlatform.iOS + ? const CupertinoListTileChevron() + : null, onTap: () { pushPlatformRoute( context, @@ -271,12 +256,11 @@ class _Body extends ConsumerWidget { showChoicePicker( context, choices: kSupportedLocales, - selectedItem: - generalPrefs.locale ?? Localizations.localeOf(context), + selectedItem: generalPrefs.locale ?? Localizations.localeOf(context), labelBuilder: (t) => Text(localeToLocalizedName(t)), - onSelectedItemChanged: (Locale? locale) => ref - .read(generalPreferencesProvider.notifier) - .setLocale(locale), + onSelectedItemChanged: + (Locale? locale) => + ref.read(generalPreferencesProvider.notifier).setLocale(locale), ); } else { AppSettings.openAppSettings(); @@ -360,11 +344,11 @@ class _Body extends ConsumerWidget { PlatformListTile( leading: const Icon(Icons.storage_outlined), title: const Text('Local database size'), - subtitle: Theme.of(context).platform == TargetPlatform.iOS - ? null - : Text(_getSizeString(dbSize.value)), - additionalInfo: - dbSize.hasValue ? Text(_getSizeString(dbSize.value)) : null, + subtitle: + Theme.of(context).platform == TargetPlatform.iOS + ? null + : Text(_getSizeString(dbSize.value)), + additionalInfo: dbSize.hasValue ? Text(_getSizeString(dbSize.value)) : null, ), ], ), @@ -375,10 +359,7 @@ class _Body extends ConsumerWidget { children: [ LichessMessage(style: Theme.of(context).textTheme.bodyMedium), const SizedBox(height: 10), - Text( - 'v${packageInfo.version}', - style: Theme.of(context).textTheme.bodySmall, - ), + Text('v${packageInfo.version}', style: Theme.of(context).textTheme.bodySmall), ], ), ), @@ -411,18 +392,14 @@ class _Body extends ConsumerWidget { title: Text(context.l10n.logOut), actions: [ TextButton( - style: TextButton.styleFrom( - textStyle: Theme.of(context).textTheme.labelLarge, - ), + style: TextButton.styleFrom(textStyle: Theme.of(context).textTheme.labelLarge), child: Text(context.l10n.cancel), onPressed: () { Navigator.of(context).pop(); }, ), TextButton( - style: TextButton.styleFrom( - textStyle: Theme.of(context).textTheme.labelLarge, - ), + style: TextButton.styleFrom(textStyle: Theme.of(context).textTheme.labelLarge), child: Text(context.l10n.mobileOkButton), onPressed: () async { Navigator.of(context).pop(); @@ -436,8 +413,7 @@ class _Body extends ConsumerWidget { } } - String _getSizeString(int? bytes) => - '${_bytesToMB(bytes ?? 0).toStringAsFixed(2)}MB'; + String _getSizeString(int? bytes) => '${_bytesToMB(bytes ?? 0).toStringAsFixed(2)}MB'; double _bytesToMB(int bytes) => bytes * 0.000001; @@ -454,9 +430,10 @@ class _OpenInNewIcon extends StatelessWidget { return Icon( Icons.open_in_new, size: 18, - color: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoColors.systemGrey2.resolveFrom(context) - : null, + color: + Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoColors.systemGrey2.resolveFrom(context) + : null, ); } } diff --git a/lib/src/view/settings/sound_settings_screen.dart b/lib/src/view/settings/sound_settings_screen.dart index 75010c06c4..19648fbb44 100644 --- a/lib/src/view/settings/sound_settings_screen.dart +++ b/lib/src/view/settings/sound_settings_screen.dart @@ -8,43 +8,22 @@ import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/settings.dart'; -const kMasterVolumeValues = [ - 0.0, - 0.1, - 0.2, - 0.3, - 0.4, - 0.5, - 0.6, - 0.7, - 0.8, - 0.9, - 1.0, -]; +const kMasterVolumeValues = [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]; class SoundSettingsScreen extends StatelessWidget { const SoundSettingsScreen({super.key}); @override Widget build(BuildContext context) { - return PlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, - ); + return PlatformWidget(androidBuilder: _androidBuilder, iosBuilder: _iosBuilder); } Widget _androidBuilder(BuildContext context) { - return Scaffold( - appBar: AppBar(title: Text(context.l10n.sound)), - body: _Body(), - ); + return Scaffold(appBar: AppBar(title: Text(context.l10n.sound)), body: _Body()); } Widget _iosBuilder(BuildContext context) { - return CupertinoPageScaffold( - navigationBar: const CupertinoNavigationBar(), - child: _Body(), - ); + return CupertinoPageScaffold(navigationBar: const CupertinoNavigationBar(), child: _Body()); } } @@ -61,13 +40,8 @@ class _Body extends ConsumerWidget { final generalPrefs = ref.watch(generalPreferencesProvider); void onChanged(SoundTheme? value) { - ref - .read(generalPreferencesProvider.notifier) - .setSoundTheme(value ?? SoundTheme.standard); - ref.read(soundServiceProvider).changeTheme( - value ?? SoundTheme.standard, - playSound: true, - ); + ref.read(generalPreferencesProvider.notifier).setSoundTheme(value ?? SoundTheme.standard); + ref.read(soundServiceProvider).changeTheme(value ?? SoundTheme.standard, playSound: true); } return ListView( @@ -79,9 +53,7 @@ class _Body extends ConsumerWidget { value: generalPrefs.masterVolume, values: kMasterVolumeValues, onChangeEnd: (value) { - ref - .read(generalPreferencesProvider.notifier) - .setMasterVolume(value); + ref.read(generalPreferencesProvider.notifier).setMasterVolume(value); }, labelBuilder: volumeLabel, ), diff --git a/lib/src/view/settings/theme_screen.dart b/lib/src/view/settings/theme_screen.dart index 2e16832dc7..2e1307a8e0 100644 --- a/lib/src/view/settings/theme_screen.dart +++ b/lib/src/view/settings/theme_screen.dart @@ -23,24 +23,18 @@ class ThemeScreen extends StatelessWidget { @override Widget build(BuildContext context) { - return PlatformScaffold( - appBar: const PlatformAppBar(title: Text('Theme')), - body: _Body(), - ); + return PlatformScaffold(appBar: const PlatformAppBar(title: Text('Theme')), body: _Body()); } } -String shapeColorL10n( - BuildContext context, - ShapeColor shapeColor, -) => - // TODO add l10n - switch (shapeColor) { - ShapeColor.green => 'Green', - ShapeColor.red => 'Red', - ShapeColor.blue => 'Blue', - ShapeColor.yellow => 'Yellow', - }; +String shapeColorL10n(BuildContext context, ShapeColor shapeColor) => +// TODO add l10n +switch (shapeColor) { + ShapeColor.green => 'Green', + ShapeColor.red => 'Red', + ShapeColor.blue => 'Blue', + ShapeColor.yellow => 'Yellow', +}; class _Body extends ConsumerWidget { @override @@ -60,33 +54,26 @@ class _Body extends ConsumerWidget { constraints.biggest.shortestSide - horizontalPadding * 2, ); return Padding( - padding: const EdgeInsets.symmetric( - horizontal: horizontalPadding, - vertical: 16, - ), + padding: const EdgeInsets.symmetric(horizontal: horizontalPadding, vertical: 16), child: Center( child: Chessboard.fixed( size: boardSize, orientation: Side.white, lastMove: const NormalMove(from: Square.e2, to: Square.e4), - fen: - 'rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1', - shapes: { - Circle( - color: boardPrefs.shapeColor.color, - orig: Square.fromName('b8'), - ), - Arrow( - color: boardPrefs.shapeColor.color, - orig: Square.fromName('b8'), - dest: Square.fromName('c6'), - ), - }.lock, + fen: 'rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1', + shapes: + { + Circle(color: boardPrefs.shapeColor.color, orig: Square.fromName('b8')), + Arrow( + color: boardPrefs.shapeColor.color, + orig: Square.fromName('b8'), + dest: Square.fromName('c6'), + ), + }.lock, settings: boardPrefs.toBoardSettings().copyWith( - borderRadius: - const BorderRadius.all(Radius.circular(4.0)), - boxShadow: boardShadows, - ), + borderRadius: const BorderRadius.all(Radius.circular(4.0)), + boxShadow: boardShadows, + ), ), ), ); @@ -97,18 +84,18 @@ class _Body extends ConsumerWidget { children: [ if (Theme.of(context).platform == TargetPlatform.android) androidVersionAsync.maybeWhen( - data: (version) => version != null && version.sdkInt >= 31 - ? SwitchSettingTile( - leading: const Icon(Icons.colorize_outlined), - title: Text(context.l10n.mobileSystemColors), - value: generalPrefs.systemColors, - onChanged: (value) { - ref - .read(generalPreferencesProvider.notifier) - .toggleSystemColors(); - }, - ) - : const SizedBox.shrink(), + data: + (version) => + version != null && version.sdkInt >= 31 + ? SwitchSettingTile( + leading: const Icon(Icons.colorize_outlined), + title: Text(context.l10n.mobileSystemColors), + value: generalPrefs.systemColors, + onChanged: (value) { + ref.read(generalPreferencesProvider.notifier).toggleSystemColors(); + }, + ) + : const SizedBox.shrink(), orElse: () => const SizedBox.shrink(), ), SettingsListTile( @@ -144,23 +131,16 @@ class _Body extends ConsumerWidget { context, choices: ShapeColor.values, selectedItem: boardPrefs.shapeColor, - labelBuilder: (t) => Text.rich( - TextSpan( - children: [ + labelBuilder: + (t) => Text.rich( TextSpan( - text: shapeColorL10n(context, t), + children: [ + TextSpan(text: shapeColorL10n(context, t)), + const TextSpan(text: ' '), + WidgetSpan(child: Container(width: 15, height: 15, color: t.color)), + ], ), - const TextSpan(text: ' '), - WidgetSpan( - child: Container( - width: 15, - height: 15, - color: t.color, - ), - ), - ], - ), - ), + ), onSelectedItemChanged: (ShapeColor? value) { ref .read(boardPreferencesProvider.notifier) @@ -171,9 +151,7 @@ class _Body extends ConsumerWidget { ), SwitchSettingTile( leading: const Icon(Icons.location_on), - title: Text( - context.l10n.preferencesBoardCoordinates, - ), + title: Text(context.l10n.preferencesBoardCoordinates), value: boardPrefs.coordinates, onChanged: (value) { ref.read(boardPreferencesProvider.notifier).toggleCoordinates(); diff --git a/lib/src/view/settings/toggle_sound_button.dart b/lib/src/view/settings/toggle_sound_button.dart index b3abc10487..f22b286bbf 100644 --- a/lib/src/view/settings/toggle_sound_button.dart +++ b/lib/src/view/settings/toggle_sound_button.dart @@ -10,16 +10,13 @@ class ToggleSoundButton extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final isSoundEnabled = ref.watch( - generalPreferencesProvider.select( - (prefs) => prefs.isSoundEnabled, - ), + generalPreferencesProvider.select((prefs) => prefs.isSoundEnabled), ); return AppBarIconButton( // TODO: i18n semanticsLabel: 'Toggle sound', - onPressed: () => - ref.read(generalPreferencesProvider.notifier).toggleSoundEnabled(), + onPressed: () => ref.read(generalPreferencesProvider.notifier).toggleSoundEnabled(), icon: Icon(isSoundEnabled ? Icons.volume_up : Icons.volume_off), ); } diff --git a/lib/src/view/study/study_bottom_bar.dart b/lib/src/view/study/study_bottom_bar.dart index ab2de9d8c6..4a1cf84ccb 100644 --- a/lib/src/view/study/study_bottom_bar.dart +++ b/lib/src/view/study/study_bottom_bar.dart @@ -14,18 +14,14 @@ import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; class StudyBottomBar extends ConsumerWidget { - const StudyBottomBar({ - required this.id, - }); + const StudyBottomBar({required this.id}); final StudyId id; @override Widget build(BuildContext context, WidgetRef ref) { final gamebook = ref.watch( - studyControllerProvider(id).select( - (s) => s.requireValue.gamebookActive, - ), + studyControllerProvider(id).select((s) => s.requireValue.gamebookActive), ); return gamebook ? _GamebookBottomBar(id: id) : _AnalysisBottomBar(id: id); @@ -33,9 +29,7 @@ class StudyBottomBar extends ConsumerWidget { } class _AnalysisBottomBar extends ConsumerWidget { - const _AnalysisBottomBar({ - required this.id, - }); + const _AnalysisBottomBar({required this.id}); final StudyId id; @@ -46,12 +40,10 @@ class _AnalysisBottomBar extends ConsumerWidget { return const BottomBar(children: []); } - final onGoForward = state.canGoNext - ? ref.read(studyControllerProvider(id).notifier).userNext - : null; - final onGoBack = state.canGoBack - ? ref.read(studyControllerProvider(id).notifier).userPrevious - : null; + final onGoForward = + state.canGoNext ? ref.read(studyControllerProvider(id).notifier).userNext : null; + final onGoBack = + state.canGoBack ? ref.read(studyControllerProvider(id).notifier).userPrevious : null; return BottomBar( children: [ @@ -60,9 +52,7 @@ class _AnalysisBottomBar extends ConsumerWidget { id: id, chapterId: state.study.chapter.id, hasNextChapter: state.hasNextChapter, - blink: !state.isIntroductoryChapter && - state.isAtEndOfChapter && - state.hasNextChapter, + blink: !state.isIntroductoryChapter && state.isAtEndOfChapter && state.hasNextChapter, ), RepeatButton( onLongPress: onGoBack, @@ -92,9 +82,7 @@ class _AnalysisBottomBar extends ConsumerWidget { } class _GamebookBottomBar extends ConsumerWidget { - const _GamebookBottomBar({ - required this.id, - }); + const _GamebookBottomBar({required this.id}); final StudyId id; @@ -107,77 +95,75 @@ class _GamebookBottomBar extends ConsumerWidget { _ChapterButton(state: state), ...switch (state.gamebookState) { GamebookState.findTheMove => [ - if (!state.currentNode.isRoot) - BottomBarButton( - onTap: ref.read(studyControllerProvider(id).notifier).reset, - icon: Icons.skip_previous, - label: 'Back', - showLabel: true, - ), + if (!state.currentNode.isRoot) BottomBarButton( - icon: Icons.help, - label: context.l10n.viewTheSolution, + onTap: ref.read(studyControllerProvider(id).notifier).reset, + icon: Icons.skip_previous, + label: 'Back', showLabel: true, - onTap: ref - .read(studyControllerProvider(id).notifier) - .showGamebookSolution, ), - ], + BottomBarButton( + icon: Icons.help, + label: context.l10n.viewTheSolution, + showLabel: true, + onTap: ref.read(studyControllerProvider(id).notifier).showGamebookSolution, + ), + ], GamebookState.startLesson || GamebookState.correctMove => [ + BottomBarButton( + onTap: ref.read(studyControllerProvider(id).notifier).userNext, + icon: Icons.play_arrow, + label: context.l10n.studyNext, + showLabel: true, + blink: state.gamebookComment != null && !state.isIntroductoryChapter, + ), + ], + GamebookState.incorrectMove => [ + BottomBarButton( + onTap: ref.read(studyControllerProvider(id).notifier).userPrevious, + label: context.l10n.retry, + showLabel: true, + icon: Icons.refresh, + blink: state.gamebookComment != null, + ), + ], + GamebookState.lessonComplete => [ + if (!state.isIntroductoryChapter) BottomBarButton( - onTap: ref.read(studyControllerProvider(id).notifier).userNext, - icon: Icons.play_arrow, - label: context.l10n.studyNext, + onTap: ref.read(studyControllerProvider(id).notifier).reset, + icon: Icons.refresh, + label: context.l10n.studyPlayAgain, showLabel: true, - blink: state.gamebookComment != null && - !state.isIntroductoryChapter, ), - ], - GamebookState.incorrectMove => [ + _NextChapterButton( + id: id, + chapterId: state.study.chapter.id, + hasNextChapter: state.hasNextChapter, + blink: !state.isIntroductoryChapter && state.hasNextChapter, + ), + if (!state.isIntroductoryChapter) BottomBarButton( onTap: - ref.read(studyControllerProvider(id).notifier).userPrevious, - label: context.l10n.retry, + () => pushPlatformRoute( + context, + rootNavigator: true, + builder: + (context) => AnalysisScreen( + options: AnalysisOptions( + orientation: state.pov, + standalone: ( + pgn: state.pgn, + isComputerAnalysisAllowed: true, + variant: state.variant, + ), + ), + ), + ), + icon: Icons.biotech, + label: context.l10n.analysis, showLabel: true, - icon: Icons.refresh, - blink: state.gamebookComment != null, ), - ], - GamebookState.lessonComplete => [ - if (!state.isIntroductoryChapter) - BottomBarButton( - onTap: ref.read(studyControllerProvider(id).notifier).reset, - icon: Icons.refresh, - label: context.l10n.studyPlayAgain, - showLabel: true, - ), - _NextChapterButton( - id: id, - chapterId: state.study.chapter.id, - hasNextChapter: state.hasNextChapter, - blink: !state.isIntroductoryChapter && state.hasNextChapter, - ), - if (!state.isIntroductoryChapter) - BottomBarButton( - onTap: () => pushPlatformRoute( - context, - rootNavigator: true, - builder: (context) => AnalysisScreen( - options: AnalysisOptions( - orientation: state.pov, - standalone: ( - pgn: state.pgn, - isComputerAnalysisAllowed: true, - variant: state.variant, - ), - ), - ), - ), - icon: Icons.biotech, - label: context.l10n.analysis, - showLabel: true, - ), - ], + ], }, ], ); @@ -217,19 +203,18 @@ class _NextChapterButtonState extends ConsumerState<_NextChapterButton> { return isLoading ? const Center(child: CircularProgressIndicator()) : BottomBarButton( - onTap: widget.hasNextChapter - ? () { - ref - .read(studyControllerProvider(widget.id).notifier) - .nextChapter(); + onTap: + widget.hasNextChapter + ? () { + ref.read(studyControllerProvider(widget.id).notifier).nextChapter(); setState(() => isLoading = true); } - : null, - icon: Icons.play_arrow, - label: context.l10n.studyNextChapter, - showLabel: true, - blink: widget.blink, - ); + : null, + icon: Icons.play_arrow, + label: context.l10n.studyNextChapter, + showLabel: true, + blink: widget.blink, + ); } } @@ -241,24 +226,26 @@ class _ChapterButton extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { return BottomBarButton( - onTap: () => showAdaptiveBottomSheet( - context: context, - showDragHandle: true, - isScrollControlled: true, - isDismissible: true, - builder: (_) => DraggableScrollableSheet( - initialChildSize: 0.6, - maxChildSize: 0.6, - snap: true, - expand: false, - builder: (context, scrollController) { - return _StudyChaptersMenu( - id: state.study.id, - scrollController: scrollController, - ); - }, - ), - ), + onTap: + () => showAdaptiveBottomSheet( + context: context, + showDragHandle: true, + isScrollControlled: true, + isDismissible: true, + builder: + (_) => DraggableScrollableSheet( + initialChildSize: 0.6, + maxChildSize: 0.6, + snap: true, + expand: false, + builder: (context, scrollController) { + return _StudyChaptersMenu( + id: state.study.id, + scrollController: scrollController, + ); + }, + ), + ), label: context.l10n.studyNbChapters(state.study.chapters.length), showLabel: true, icon: Icons.menu_book, @@ -267,10 +254,7 @@ class _ChapterButton extends ConsumerWidget { } class _StudyChaptersMenu extends ConsumerStatefulWidget { - const _StudyChaptersMenu({ - required this.id, - required this.scrollController, - }); + const _StudyChaptersMenu({required this.id, required this.scrollController}); final StudyId id; final ScrollController scrollController; @@ -289,10 +273,7 @@ class _StudyChaptersMenuState extends ConsumerState<_StudyChaptersMenu> { // Scroll to the current chapter WidgetsBinding.instance.addPostFrameCallback((_) { if (currentChapterKey.currentContext != null) { - Scrollable.ensureVisible( - currentChapterKey.currentContext!, - alignment: 0.5, - ); + Scrollable.ensureVisible(currentChapterKey.currentContext!, alignment: 0.5); } }); @@ -309,14 +290,10 @@ class _StudyChaptersMenuState extends ConsumerState<_StudyChaptersMenu> { const SizedBox(height: 16), for (final chapter in state.study.chapters) PlatformListTile( - key: chapter.id == state.currentChapter.id - ? currentChapterKey - : null, + key: chapter.id == state.currentChapter.id ? currentChapterKey : null, title: Text(chapter.name, maxLines: 2), onTap: () { - ref.read(studyControllerProvider(widget.id).notifier).goToChapter( - chapter.id, - ); + ref.read(studyControllerProvider(widget.id).notifier).goToChapter(chapter.id); Navigator.of(context).pop(); }, selected: chapter.id == state.currentChapter.id, diff --git a/lib/src/view/study/study_gamebook.dart b/lib/src/view/study/study_gamebook.dart index 67b6d17583..c12c09ed5e 100644 --- a/lib/src/view/study/study_gamebook.dart +++ b/lib/src/view/study/study_gamebook.dart @@ -8,9 +8,7 @@ import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:url_launcher/url_launcher.dart'; class StudyGamebook extends StatelessWidget { - const StudyGamebook( - this.id, - ); + const StudyGamebook(this.id); final StudyId id; @@ -26,10 +24,7 @@ class StudyGamebook extends StatelessWidget { padding: const EdgeInsets.all(10), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - _Comment(id: id), - _Hint(id: id), - ], + children: [_Comment(id: id), _Hint(id: id)], ), ), ), @@ -41,9 +36,7 @@ class StudyGamebook extends StatelessWidget { } class _Comment extends ConsumerWidget { - const _Comment({ - required this.id, - }); + const _Comment({required this.id}); final StudyId id; @@ -51,14 +44,14 @@ class _Comment extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final state = ref.watch(studyControllerProvider(id)).requireValue; - final comment = state.gamebookComment ?? + final comment = + state.gamebookComment ?? switch (state.gamebookState) { GamebookState.findTheMove => context.l10n.studyWhatWouldYouPlay, GamebookState.correctMove => context.l10n.studyGoodMove, GamebookState.incorrectMove => context.l10n.puzzleNotTheMove, - GamebookState.lessonComplete => - context.l10n.studyYouCompletedThisLesson, - _ => '' + GamebookState.lessonComplete => context.l10n.studyYouCompletedThisLesson, + _ => '', }; return Expanded( @@ -68,9 +61,7 @@ class _Comment extends ConsumerWidget { padding: const EdgeInsets.only(right: 5), child: Linkify( text: comment, - style: const TextStyle( - fontSize: 16, - ), + style: const TextStyle(fontSize: 16), onOpen: (link) async { launchUrl(Uri.parse(link.url)); }, @@ -83,9 +74,7 @@ class _Comment extends ConsumerWidget { } class _Hint extends ConsumerStatefulWidget { - const _Hint({ - required this.id, - }); + const _Hint({required this.id}); final StudyId id; @@ -98,15 +87,15 @@ class _HintState extends ConsumerState<_Hint> { @override Widget build(BuildContext context) { - final hint = - ref.watch(studyControllerProvider(widget.id)).requireValue.gamebookHint; + final hint = ref.watch(studyControllerProvider(widget.id)).requireValue.gamebookHint; return hint == null ? const SizedBox.shrink() : SizedBox( - height: 40, - child: showHint - ? Center(child: Text(hint)) - : TextButton( + height: 40, + child: + showHint + ? Center(child: Text(hint)) + : TextButton( onPressed: () { setState(() { showHint = true; @@ -114,7 +103,7 @@ class _HintState extends ConsumerState<_Hint> { }, child: Text(context.l10n.getAHint), ), - ); + ); } } @@ -159,10 +148,7 @@ class GamebookButton extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Text( label, - style: TextStyle( - fontSize: 16.0, - color: highlighted ? primary : null, - ), + style: TextStyle(fontSize: 16.0, color: highlighted ? primary : null), ), ), ], diff --git a/lib/src/view/study/study_list_screen.dart b/lib/src/view/study/study_list_screen.dart index f209737c6d..a824f99ab8 100644 --- a/lib/src/view/study/study_list_screen.dart +++ b/lib/src/view/study/study_list_screen.dart @@ -32,9 +32,7 @@ class StudyListScreen extends ConsumerWidget { final isLoggedIn = ref.watch(authSessionProvider)?.user.id != null; final filter = ref.watch(studyFilterProvider); - final title = Text( - isLoggedIn ? filter.category.l10n(context.l10n) : context.l10n.studyMenu, - ); + final title = Text(isLoggedIn ? filter.category.l10n(context.l10n) : context.l10n.studyMenu); return PlatformScaffold( appBar: PlatformAppBar( @@ -44,14 +42,13 @@ class StudyListScreen extends ConsumerWidget { icon: const Icon(Icons.tune), // TODO: translate semanticsLabel: 'Filter studies', - onPressed: () => showAdaptiveBottomSheet( - context: context, - isScrollControlled: true, - showDragHandle: true, - builder: (_) => _StudyFilterSheet( - isLoggedIn: isLoggedIn, - ), - ), + onPressed: + () => showAdaptiveBottomSheet( + context: context, + isScrollControlled: true, + showDragHandle: true, + builder: (_) => _StudyFilterSheet(isLoggedIn: isLoggedIn), + ), ), ], ), @@ -79,8 +76,8 @@ class _StudyFilterSheet extends ConsumerWidget { choices: StudyCategory.values, choiceSelected: (choice) => filter.category == choice, choiceLabel: (category) => Text(category.l10n(context.l10n)), - onSelected: (value, selected) => - ref.read(studyFilterProvider.notifier).setCategory(value), + onSelected: + (value, selected) => ref.read(studyFilterProvider.notifier).setCategory(value), ), const PlatformDivider(thickness: 1, indent: 0), const SizedBox(height: 10.0), @@ -92,8 +89,7 @@ class _StudyFilterSheet extends ConsumerWidget { choices: StudyListOrder.values, choiceSelected: (choice) => filter.order == choice, choiceLabel: (order) => Text(order.l10n(context.l10n)), - onSelected: (value, selected) => - ref.read(studyFilterProvider.notifier).setOrder(value), + onSelected: (value, selected) => ref.read(studyFilterProvider.notifier).setOrder(value), ), ], ); @@ -101,9 +97,7 @@ class _StudyFilterSheet extends ConsumerWidget { } class _Body extends ConsumerStatefulWidget { - const _Body({ - required this.filter, - }); + const _Body({required this.filter}); final StudyFilterState filter; @@ -121,10 +115,7 @@ class _BodyState extends ConsumerState<_Body> { bool requestedNextPage = false; StudyListPaginatorProvider get paginatorProvider => - StudyListPaginatorProvider( - filter: widget.filter, - search: search, - ); + StudyListPaginatorProvider(filter: widget.filter, search: search); @override void initState() { @@ -142,8 +133,7 @@ class _BodyState extends ConsumerState<_Body> { void _scrollListener() { if (!requestedNextPage && - _scrollController.position.pixels >= - _scrollController.position.maxScrollExtent - 300) { + _scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 300) { final studiesList = ref.read(paginatorProvider); if (!studiesList.isLoading) { @@ -176,10 +166,11 @@ class _BodyState extends ConsumerState<_Body> { padding: Styles.bodySectionPadding, child: PlatformSearchBar( controller: _searchController, - onClear: () => setState(() { - search = null; - _searchController.clear(); - }), + onClear: + () => setState(() { + search = null; + _searchController.clear(); + }), hintText: search ?? context.l10n.searchSearch, onSubmitted: (term) { setState(() { @@ -195,29 +186,23 @@ class _BodyState extends ConsumerState<_Body> { shrinkWrap: true, itemCount: studies.studies.length + 1, controller: _scrollController, - separatorBuilder: (context, index) => index == 0 - ? const SizedBox.shrink() - : Theme.of(context).platform == TargetPlatform.iOS - ? const PlatformDivider( - height: 1, - cupertinoHasLeading: true, - ) - : const PlatformDivider( - height: 1, - color: Colors.transparent, - ), - itemBuilder: (context, index) => index == 0 - ? searchBar - : _StudyListItem(study: studies.studies[index - 1]), + separatorBuilder: + (context, index) => + index == 0 + ? const SizedBox.shrink() + : Theme.of(context).platform == TargetPlatform.iOS + ? const PlatformDivider(height: 1, cupertinoHasLeading: true) + : const PlatformDivider(height: 1, color: Colors.transparent), + itemBuilder: + (context, index) => + index == 0 ? searchBar : _StudyListItem(study: studies.studies[index - 1]), ); }, loading: () { return Column( children: [ searchBar, - const Expanded( - child: Center(child: CircularProgressIndicator.adaptive()), - ), + const Expanded(child: Center(child: CircularProgressIndicator.adaptive())), ], ); }, @@ -230,41 +215,29 @@ class _BodyState extends ConsumerState<_Body> { } class _StudyListItem extends StatelessWidget { - const _StudyListItem({ - required this.study, - }); + const _StudyListItem({required this.study}); final StudyPageData study; @override Widget build(BuildContext context) { return PlatformListTile( - padding: Theme.of(context).platform == TargetPlatform.iOS - ? const EdgeInsets.symmetric( - horizontal: 14.0, - vertical: 12.0, - ) - : null, - leading: _StudyFlair( - flair: study.flair, - size: 30, - ), + padding: + Theme.of(context).platform == TargetPlatform.iOS + ? const EdgeInsets.symmetric(horizontal: 14.0, vertical: 12.0) + : null, + leading: _StudyFlair(flair: study.flair, size: 30), title: Column( crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - study.name, - overflow: TextOverflow.ellipsis, - maxLines: 2, - ), - ], + children: [Text(study.name, overflow: TextOverflow.ellipsis, maxLines: 2)], ), subtitle: _StudySubtitle(study: study), - onTap: () => pushPlatformRoute( - context, - rootNavigator: true, - builder: (context) => StudyScreen(id: study.id), - ), + onTap: + () => pushPlatformRoute( + context, + rootNavigator: true, + builder: (context) => StudyScreen(id: study.id), + ), onLongPress: () { showAdaptiveBottomSheet( context: context, @@ -272,9 +245,7 @@ class _StudyListItem extends StatelessWidget { isDismissible: true, isScrollControlled: true, showDragHandle: true, - constraints: BoxConstraints( - minHeight: MediaQuery.sizeOf(context).height * 0.5, - ), + constraints: BoxConstraints(minHeight: MediaQuery.sizeOf(context).height * 0.5), builder: (context) => _ContextMenu(study: study), ); }, @@ -283,9 +254,7 @@ class _StudyListItem extends StatelessWidget { } class _ContextMenu extends ConsumerWidget { - const _ContextMenu({ - required this.study, - }); + const _ContextMenu({required this.study}); final StudyPageData study; @@ -303,9 +272,7 @@ class _ContextMenu extends ConsumerWidget { } class _StudyChapters extends StatelessWidget { - const _StudyChapters({ - required this.study, - }); + const _StudyChapters({required this.study}); final StudyPageData study; @@ -326,9 +293,7 @@ class _StudyChapters extends StatelessWidget { size: DefaultTextStyle.of(context).style.fontSize, ), ), - TextSpan( - text: ' $chapter', - ), + TextSpan(text: ' $chapter'), ], ), ), @@ -339,9 +304,7 @@ class _StudyChapters extends StatelessWidget { } class _StudyMembers extends StatelessWidget { - const _StudyMembers({ - required this.study, - }); + const _StudyMembers({required this.study}); final StudyPageData study; @@ -359,19 +322,14 @@ class _StudyMembers extends StatelessWidget { WidgetSpan( alignment: PlaceholderAlignment.middle, child: Icon( - member.role == 'w' - ? LichessIcons.radio_tower_lichess - : Icons.remove_red_eye, + member.role == 'w' ? LichessIcons.radio_tower_lichess : Icons.remove_red_eye, size: DefaultTextStyle.of(context).style.fontSize, ), ), const TextSpan(text: ' '), WidgetSpan( alignment: PlaceholderAlignment.bottom, - child: UserFullNameWidget( - user: member.user, - showFlair: false, - ), + child: UserFullNameWidget(user: member.user, showFlair: false), ), ], ), @@ -391,18 +349,15 @@ class _StudyFlair extends StatelessWidget { @override Widget build(BuildContext context) { - final iconIfNoFlair = Icon( - LichessIcons.study, - size: size, - ); + final iconIfNoFlair = Icon(LichessIcons.study, size: size); return (flair != null) ? CachedNetworkImage( - imageUrl: lichessFlairSrc(flair!), - errorWidget: (_, __, ___) => iconIfNoFlair, - width: size, - height: size, - ) + imageUrl: lichessFlairSrc(flair!), + errorWidget: (_, __, ___) => iconIfNoFlair, + width: size, + height: size, + ) : iconIfNoFlair; } } @@ -419,28 +374,18 @@ class _StudySubtitle extends StatelessWidget { children: [ WidgetSpan( alignment: PlaceholderAlignment.middle, - child: Icon( - study.liked ? Icons.favorite : Icons.favorite_outline, - size: 14, - ), + child: Icon(study.liked ? Icons.favorite : Icons.favorite_outline, size: 14), ), TextSpan(text: ' ${study.likes}'), const TextSpan(text: ' • '), if (study.owner != null) ...[ WidgetSpan( alignment: PlaceholderAlignment.middle, - child: UserFullNameWidget( - user: study.owner, - showFlair: false, - ), + child: UserFullNameWidget(user: study.owner, showFlair: false), ), const TextSpan(text: ' • '), ], - TextSpan( - text: timeago.format( - study.updatedAt, - ), - ), + TextSpan(text: timeago.format(study.updatedAt)), ], ), ); diff --git a/lib/src/view/study/study_screen.dart b/lib/src/view/study/study_screen.dart index ed66346eb7..9024b0a01b 100644 --- a/lib/src/view/study/study_screen.dart +++ b/lib/src/view/study/study_screen.dart @@ -48,59 +48,44 @@ class StudyScreen extends ConsumerWidget { final boardPrefs = ref.watch(boardPreferencesProvider); switch (ref.watch(studyControllerProvider(id))) { case AsyncData(:final value): - return _StudyScreen( - id: id, - studyState: value, - ); + return _StudyScreen(id: id, studyState: value); case AsyncError(:final error, :final stackTrace): _logger.severe('Cannot load study: $error', stackTrace); return PlatformScaffold( - appBar: const PlatformAppBar( - title: Text(''), - ), + appBar: const PlatformAppBar(title: Text('')), body: DefaultTabController( length: 1, child: AnalysisLayout( - boardBuilder: (context, boardSize, borderRadius) => - Chessboard.fixed( - size: boardSize, - settings: boardPrefs.toBoardSettings().copyWith( + boardBuilder: + (context, boardSize, borderRadius) => Chessboard.fixed( + size: boardSize, + settings: boardPrefs.toBoardSettings().copyWith( borderRadius: borderRadius, - boxShadow: borderRadius != null - ? boardShadows - : const [], + boxShadow: borderRadius != null ? boardShadows : const [], ), - orientation: Side.white, - fen: kEmptyFEN, - ), - children: const [ - Center( - child: Text('Failed to load study.'), - ), - ], + orientation: Side.white, + fen: kEmptyFEN, + ), + children: const [Center(child: Text('Failed to load study.'))], ), ), ); case _: return PlatformScaffold( - appBar: const PlatformAppBar( - title: Text(''), - ), + appBar: const PlatformAppBar(title: Text('')), body: DefaultTabController( length: 1, child: AnalysisLayout( - boardBuilder: (context, boardSize, borderRadius) => - Chessboard.fixed( - size: boardSize, - settings: boardPrefs.toBoardSettings().copyWith( + boardBuilder: + (context, boardSize, borderRadius) => Chessboard.fixed( + size: boardSize, + settings: boardPrefs.toBoardSettings().copyWith( borderRadius: borderRadius, - boxShadow: borderRadius != null - ? boardShadows - : const [], + boxShadow: borderRadius != null ? boardShadows : const [], ), - orientation: Side.white, - fen: kEmptyFEN, - ), + orientation: Side.white, + fen: kEmptyFEN, + ), children: const [SizedBox.shrink()], ), ), @@ -110,10 +95,7 @@ class StudyScreen extends ConsumerWidget { } class _StudyScreen extends ConsumerStatefulWidget { - const _StudyScreen({ - required this.id, - required this.studyState, - }); + const _StudyScreen({required this.id, required this.studyState}); final StudyId id; final StudyState studyState; @@ -122,8 +104,7 @@ class _StudyScreen extends ConsumerStatefulWidget { ConsumerState<_StudyScreen> createState() => _StudyScreenState(); } -class _StudyScreenState extends ConsumerState<_StudyScreen> - with TickerProviderStateMixin { +class _StudyScreenState extends ConsumerState<_StudyScreen> with TickerProviderStateMixin { late List tabs; late TabController _tabController; @@ -136,11 +117,7 @@ class _StudyScreenState extends ConsumerState<_StudyScreen> AnalysisTab.moves, ]; - _tabController = TabController( - vsync: this, - initialIndex: tabs.length - 1, - length: tabs.length, - ); + _tabController = TabController(vsync: this, initialIndex: tabs.length - 1, length: tabs.length); } @override @@ -151,10 +128,7 @@ class _StudyScreenState extends ConsumerState<_StudyScreen> // anymore, we keep the tabs as they are. // In theory, studies mixing chapters with and without opening explorer should be pretty rare. if (tabs.length < 2 && widget.studyState.isOpeningExplorerAvailable) { - tabs = [ - AnalysisTab.opening, - AnalysisTab.moves, - ]; + tabs = [AnalysisTab.opening, AnalysisTab.moves]; _tabController = TabController( vsync: this, initialIndex: tabs.length - 1, @@ -181,11 +155,7 @@ class _StudyScreenState extends ConsumerState<_StudyScreen> overflow: TextOverflow.ellipsis, ), actions: [ - if (tabs.length > 1) - AppBarAnalysisTabIndicator( - tabs: tabs, - controller: _tabController, - ), + if (tabs.length > 1) AppBarAnalysisTabIndicator(tabs: tabs, controller: _tabController), _StudyMenu(id: widget.id), ], ), @@ -211,25 +181,19 @@ class _StudyMenu extends ConsumerWidget { icon: Icons.settings, label: context.l10n.settingsSettings, onPressed: () { - pushPlatformRoute( - context, - screen: StudySettings(id), - ); + pushPlatformRoute(context, screen: StudySettings(id)); }, ), AppBarMenuAction( icon: state.study.liked ? Icons.favorite : Icons.favorite_border, - label: state.study.liked - ? context.l10n.studyUnlike - : context.l10n.studyLike, + label: state.study.liked ? context.l10n.studyUnlike : context.l10n.studyLike, onPressed: () { ref.read(studyControllerProvider(id).notifier).toggleLike(); }, ), AppBarMenuAction( - icon: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoIcons.share - : Icons.share, + icon: + Theme.of(context).platform == TargetPlatform.iOS ? CupertinoIcons.share : Icons.share, label: context.l10n.studyShareAndExport, onPressed: () { showAdaptiveActionSheet( @@ -238,21 +202,15 @@ class _StudyMenu extends ConsumerWidget { BottomSheetAction( makeLabel: (context) => Text(context.l10n.studyStudyUrl), onPressed: (context) async { - launchShareDialog( - context, - uri: lichessUri('/study/${state.study.id}'), - ); + launchShareDialog(context, uri: lichessUri('/study/${state.study.id}')); }, ), BottomSheetAction( - makeLabel: (context) => - Text(context.l10n.studyCurrentChapterUrl), + makeLabel: (context) => Text(context.l10n.studyCurrentChapterUrl), onPressed: (context) async { launchShareDialog( context, - uri: lichessUri( - '/study/${state.study.id}/${state.study.chapter.id}', - ), + uri: lichessUri('/study/${state.study.id}/${state.study.chapter.id}'), ); }, ), @@ -261,15 +219,11 @@ class _StudyMenu extends ConsumerWidget { makeLabel: (context) => Text(context.l10n.studyStudyPgn), onPressed: (context) async { try { - final pgn = - await ref.read(studyRepositoryProvider).getStudyPgn( - state.study.id, - ); + final pgn = await ref + .read(studyRepositoryProvider) + .getStudyPgn(state.study.id); if (context.mounted) { - launchShareDialog( - context, - text: pgn, - ); + launchShareDialog(context, text: pgn); } } catch (e) { if (context.mounted) { @@ -285,32 +239,23 @@ class _StudyMenu extends ConsumerWidget { BottomSheetAction( makeLabel: (context) => Text(context.l10n.studyChapterPgn), onPressed: (context) async { - launchShareDialog( - context, - text: state.pgn, - ); + launchShareDialog(context, text: state.pgn); }, ), if (state.position != null) BottomSheetAction( - makeLabel: (context) => - Text(context.l10n.screenshotCurrentPosition), + makeLabel: (context) => Text(context.l10n.screenshotCurrentPosition), onPressed: (_) async { try { final image = await ref .read(gameShareServiceProvider) - .screenshotPosition( - state.pov, - state.position!.fen, - state.lastMove, - ); + .screenshotPosition(state.pov, state.position!.fen, state.lastMove); if (context.mounted) { launchShareDialog( context, files: [image], subject: context.l10n.puzzleFromGameLink( - lichessUri('/study/${state.study.id}') - .toString(), + lichessUri('/study/${state.study.id}').toString(), ), ); } @@ -329,11 +274,9 @@ class _StudyMenu extends ConsumerWidget { makeLabel: (context) => const Text('GIF'), onPressed: (_) async { try { - final gif = - await ref.read(gameShareServiceProvider).chapterGif( - state.study.id, - state.study.chapter.id, - ); + final gif = await ref + .read(gameShareServiceProvider) + .chapterGif(state.study.id, state.study.chapter.id); if (context.mounted) { launchShareDialog( context, @@ -366,11 +309,7 @@ class _StudyMenu extends ConsumerWidget { } class _Body extends ConsumerWidget { - const _Body({ - required this.id, - required this.tabController, - required this.tabs, - }); + const _Body({required this.id, required this.tabController, required this.tabs}); final StudyId id; final TabController tabController; @@ -384,14 +323,11 @@ class _Body extends ConsumerWidget { return DefaultTabController( length: 1, child: AnalysisLayout( - boardBuilder: (context, boardSize, borderRadius) => SizedBox.square( - dimension: boardSize, - child: Center( - child: Text( - '${variant.label} is not supported yet.', + boardBuilder: + (context, boardSize, borderRadius) => SizedBox.square( + dimension: boardSize, + child: Center(child: Text('${variant.label} is not supported yet.')), ), - ), - ), children: const [SizedBox.shrink()], ), ); @@ -411,68 +347,55 @@ class _Body extends ConsumerWidget { return AnalysisLayout( tabController: tabController, - boardBuilder: (context, boardSize, borderRadius) => _StudyBoard( - id: id, - boardSize: boardSize, - borderRadius: borderRadius, - ), - engineGaugeBuilder: isComputerAnalysisAllowed && - showEvaluationGauge && - engineGaugeParams != null - ? (context, orientation) { - return orientation == Orientation.portrait - ? EngineGauge( + boardBuilder: + (context, boardSize, borderRadius) => + _StudyBoard(id: id, boardSize: boardSize, borderRadius: borderRadius), + engineGaugeBuilder: + isComputerAnalysisAllowed && showEvaluationGauge && engineGaugeParams != null + ? (context, orientation) { + return orientation == Orientation.portrait + ? EngineGauge( displayMode: EngineGaugeDisplayMode.horizontal, params: engineGaugeParams, ) - : Container( + : Container( clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4.0), - ), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(4.0)), child: EngineGauge( displayMode: EngineGaugeDisplayMode.vertical, params: engineGaugeParams, ), ); - } - : null, - engineLines: isComputerAnalysisAllowed && - isLocalEvaluationEnabled && - numEvalLines > 0 - ? EngineLines( - clientEval: currentNode.eval, - isGameOver: currentNode.position?.isGameOver ?? false, - onTapMove: ref - .read( - studyControllerProvider(id).notifier, - ) - .onUserMove, - ) - : null, + } + : null, + engineLines: + isComputerAnalysisAllowed && isLocalEvaluationEnabled && numEvalLines > 0 + ? EngineLines( + clientEval: currentNode.eval, + isGameOver: currentNode.position?.isGameOver ?? false, + onTapMove: ref.read(studyControllerProvider(id).notifier).onUserMove, + ) + : null, bottomBar: StudyBottomBar(id: id), - children: tabs.map((tab) { - switch (tab) { - case AnalysisTab.opening: - if (studyState.isOpeningExplorerAvailable && - studyState.currentNode.position != null) { - return OpeningExplorerView( - position: studyState.currentNode.position!, - onMoveSelected: (move) { - ref - .read(studyControllerProvider(id).notifier) - .onUserMove(move); - }, - ); - } else { - return const Center( - child: Text('Opening explorer not available.'), - ); + children: + tabs.map((tab) { + switch (tab) { + case AnalysisTab.opening: + if (studyState.isOpeningExplorerAvailable && + studyState.currentNode.position != null) { + return OpeningExplorerView( + position: studyState.currentNode.position!, + onMoveSelected: (move) { + ref.read(studyControllerProvider(id).notifier).onUserMove(move); + }, + ); + } else { + return const Center(child: Text('Opening explorer not available.')); + } + case _: + return bottomChild; } - case _: - return bottomChild; - } - }).toList(), + }).toList(), ); } } @@ -486,21 +409,13 @@ extension on PgnCommentShape { CommentShapeColor.yellow => ShapeColor.yellow, }; return from != to - ? Arrow( - color: shapeColor.color, - orig: from, - dest: to, - ) + ? Arrow(color: shapeColor.color, orig: from, dest: to) : Circle(color: shapeColor.color, orig: from); } } class _StudyBoard extends ConsumerStatefulWidget { - const _StudyBoard({ - required this.id, - required this.boardSize, - this.borderRadius, - }); + const _StudyBoard({required this.id, required this.boardSize, this.borderRadius}); final StudyId id; @@ -519,9 +434,7 @@ class _StudyBoardState extends ConsumerState<_StudyBoard> { Widget build(BuildContext context) { // Clear shapes when switching to a new chapter. // This avoids "leftover" shapes from the previous chapter when the engine has not evaluated the new position yet. - ref.listen( - studyControllerProvider(widget.id).select((state) => state.hasValue), - (prev, next) { + ref.listen(studyControllerProvider(widget.id).select((state) => state.hasValue), (prev, next) { if (prev != next) { setState(() { userShapes = ISet(); @@ -530,34 +443,24 @@ class _StudyBoardState extends ConsumerState<_StudyBoard> { }); final boardPrefs = ref.watch(boardPreferencesProvider); - final studyState = - ref.watch(studyControllerProvider(widget.id)).requireValue; + final studyState = ref.watch(studyControllerProvider(widget.id)).requireValue; final currentNode = studyState.currentNode; final position = currentNode.position; - final showVariationArrows = ref.watch( - studyPreferencesProvider.select( - (prefs) => prefs.showVariationArrows, - ), - ) && + final showVariationArrows = + ref.watch(studyPreferencesProvider.select((prefs) => prefs.showVariationArrows)) && !studyState.gamebookActive && currentNode.children.length > 1; - final pgnShapes = ISet( - studyState.pgnShapes.map((shape) => shape.chessground), - ); + final pgnShapes = ISet(studyState.pgnShapes.map((shape) => shape.chessground)); final variationArrows = ISet( showVariationArrows ? currentNode.children.mapIndexed((i, move) { - final color = Colors.white.withValues(alpha: i == 0 ? 0.9 : 0.5); - return Arrow( - color: color, - orig: (move as NormalMove).from, - dest: move.to, - ); - }).toList() + final color = Colors.white.withValues(alpha: i == 0 ? 0.9 : 0.5); + return Arrow(color: color, orig: (move as NormalMove).from, dest: move.to); + }).toList() : [], ); @@ -566,20 +469,16 @@ class _StudyBoardState extends ConsumerState<_StudyBoard> { ); final showBestMoveArrow = ref.watch( - analysisPreferencesProvider.select( - (value) => value.showBestMoveArrow, - ), - ); - final bestMoves = ref.watch( - engineEvaluationProvider.select((s) => s.eval?.bestMoves), + analysisPreferencesProvider.select((value) => value.showBestMoveArrow), ); + final bestMoves = ref.watch(engineEvaluationProvider.select((s) => s.eval?.bestMoves)); final ISet bestMoveShapes = showBestMoveArrow && studyState.isEngineAvailable && bestMoves != null ? computeBestMoveShapes( - bestMoves, - currentNode.position!.turn, - boardPrefs.pieceSet.assets, - ) + bestMoves, + currentNode.position!.turn, + boardPrefs.pieceSet.assets, + ) : ISet(); final sanMove = currentNode.sanMove; @@ -588,53 +487,41 @@ class _StudyBoardState extends ConsumerState<_StudyBoard> { return Chessboard( size: widget.boardSize, settings: boardPrefs.toBoardSettings().copyWith( - borderRadius: widget.borderRadius, - boxShadow: widget.borderRadius != null - ? boardShadows - : const [], - drawShape: DrawShapeOptions( - enable: true, - onCompleteShape: _onCompleteShape, - onClearShapes: _onClearShapes, - newShapeColor: boardPrefs.shapeColor.color, - ), - ), - fen: studyState.position?.board.fen ?? - studyState.study.currentChapterMeta.fen ?? - kInitialFEN, + borderRadius: widget.borderRadius, + boxShadow: widget.borderRadius != null ? boardShadows : const [], + drawShape: DrawShapeOptions( + enable: true, + onCompleteShape: _onCompleteShape, + onClearShapes: _onClearShapes, + newShapeColor: boardPrefs.shapeColor.color, + ), + ), + fen: studyState.position?.board.fen ?? studyState.study.currentChapterMeta.fen ?? kInitialFEN, lastMove: studyState.lastMove as NormalMove?, orientation: studyState.pov, - shapes: pgnShapes - .union(userShapes) - .union(variationArrows) - .union(bestMoveShapes), + shapes: pgnShapes.union(userShapes).union(variationArrows).union(bestMoveShapes), annotations: showAnnotationsOnBoard && sanMove != null && annotation != null ? altCastles.containsKey(sanMove.move.uci) - ? IMap({ - Move.parse(altCastles[sanMove.move.uci]!)!.to: annotation, - }) + ? IMap({Move.parse(altCastles[sanMove.move.uci]!)!.to: annotation}) : IMap({sanMove.move.to: annotation}) : null, - game: position != null - ? GameData( - playerSide: studyState.playerSide, - isCheck: position.isCheck, - sideToMove: position.turn, - validMoves: makeLegalMoves(position), - promotionMove: studyState.promotionMove, - onMove: (move, {isDrop, captured}) { - ref - .read(studyControllerProvider(widget.id).notifier) - .onUserMove(move); - }, - onPromotionSelection: (role) { - ref - .read(studyControllerProvider(widget.id).notifier) - .onPromotionSelection(role); - }, - ) - : null, + game: + position != null + ? GameData( + playerSide: studyState.playerSide, + isCheck: position.isCheck, + sideToMove: position.turn, + validMoves: makeLegalMoves(position), + promotionMove: studyState.promotionMove, + onMove: (move, {isDrop, captured}) { + ref.read(studyControllerProvider(widget.id).notifier).onUserMove(move); + }, + onPromotionSelection: (role) { + ref.read(studyControllerProvider(widget.id).notifier).onPromotionSelection(role); + }, + ) + : null, ); } diff --git a/lib/src/view/study/study_settings.dart b/lib/src/view/study/study_settings.dart index 74bc0eef8a..ee8c4ac90a 100644 --- a/lib/src/view/study/study_settings.dart +++ b/lib/src/view/study/study_settings.dart @@ -34,37 +34,33 @@ class StudySettings extends ConsumerWidget { ); return PlatformScaffold( - appBar: PlatformAppBar( - title: Text(context.l10n.settingsSettings), - ), + appBar: PlatformAppBar(title: Text(context.l10n.settingsSettings)), body: ListView( children: [ if (isComputerAnalysisAllowed) StockfishSettingsWidget( - onToggleLocalEvaluation: () => - ref.read(studyController.notifier).toggleLocalEvaluation(), - onSetEngineSearchTime: (value) => - ref.read(studyController.notifier).setEngineSearchTime(value), - onSetNumEvalLines: (value) => - ref.read(studyController.notifier).setNumEvalLines(value), - onSetEngineCores: (value) => - ref.read(studyController.notifier).setEngineCores(value), + onToggleLocalEvaluation: + () => ref.read(studyController.notifier).toggleLocalEvaluation(), + onSetEngineSearchTime: + (value) => ref.read(studyController.notifier).setEngineSearchTime(value), + onSetNumEvalLines: + (value) => ref.read(studyController.notifier).setNumEvalLines(value), + onSetEngineCores: (value) => ref.read(studyController.notifier).setEngineCores(value), ), ListSection( children: [ SwitchSettingTile( title: Text(context.l10n.showVariationArrows), value: studyPrefs.showVariationArrows, - onChanged: (value) => ref - .read(studyPreferencesProvider.notifier) - .toggleShowVariationArrows(), + onChanged: + (value) => + ref.read(studyPreferencesProvider.notifier).toggleShowVariationArrows(), ), SwitchSettingTile( title: Text(context.l10n.toggleGlyphAnnotations), value: analysisPrefs.showAnnotations, - onChanged: (_) => ref - .read(analysisPreferencesProvider.notifier) - .toggleAnnotations(), + onChanged: + (_) => ref.read(analysisPreferencesProvider.notifier).toggleAnnotations(), ), ], ), @@ -72,21 +68,20 @@ class StudySettings extends ConsumerWidget { children: [ PlatformListTile( title: Text(context.l10n.openingExplorer), - onTap: () => showAdaptiveBottomSheet( - context: context, - isScrollControlled: true, - showDragHandle: true, - isDismissible: true, - builder: (_) => const OpeningExplorerSettings(), - ), + onTap: + () => showAdaptiveBottomSheet( + context: context, + isScrollControlled: true, + showDragHandle: true, + isDismissible: true, + builder: (_) => const OpeningExplorerSettings(), + ), ), SwitchSettingTile( title: Text(context.l10n.sound), value: isSoundEnabled, onChanged: (value) { - ref - .read(generalPreferencesProvider.notifier) - .toggleSoundEnabled(); + ref.read(generalPreferencesProvider.notifier).toggleSoundEnabled(); }, ), ], diff --git a/lib/src/view/study/study_tree_view.dart b/lib/src/view/study/study_tree_view.dart index 031a2c599c..b6ff6857a9 100644 --- a/lib/src/view/study/study_tree_view.dart +++ b/lib/src/view/study/study_tree_view.dart @@ -11,30 +11,24 @@ import 'package:lichess_mobile/src/widgets/pgn.dart'; const kNextChapterButtonHeight = 32.0; class StudyTreeView extends ConsumerWidget { - const StudyTreeView( - this.id, - ); + const StudyTreeView(this.id); final StudyId id; @override Widget build(BuildContext context, WidgetRef ref) { - final root = ref.watch( - studyControllerProvider(id) - .select((value) => value.requireValue.root), - ) ?? + final root = + ref.watch(studyControllerProvider(id).select((value) => value.requireValue.root)) ?? // If root is null, the study chapter's position is illegal. // We still want to display the root comments though, so create a dummy root. const ViewRoot(position: Chess.initial, children: IList.empty()); final currentPath = ref.watch( - studyControllerProvider(id) - .select((value) => value.requireValue.currentPath), + studyControllerProvider(id).select((value) => value.requireValue.currentPath), ); final pgnRootComments = ref.watch( - studyControllerProvider(id) - .select((value) => value.requireValue.pgnRootComments), + studyControllerProvider(id).select((value) => value.requireValue.pgnRootComments), ); final analysisPrefs = ref.watch(analysisPreferencesProvider); diff --git a/lib/src/view/tools/load_position_screen.dart b/lib/src/view/tools/load_position_screen.dart index 3c48bf6ad7..3b6e9cbe78 100644 --- a/lib/src/view/tools/load_position_screen.dart +++ b/lib/src/view/tools/load_position_screen.dart @@ -19,9 +19,7 @@ class LoadPositionScreen extends StatelessWidget { @override Widget build(BuildContext context) { return PlatformScaffold( - appBar: PlatformAppBar( - title: Text(context.l10n.loadPosition), - ), + appBar: PlatformAppBar(title: Text(context.l10n.loadPosition)), body: const _Body(), ); } @@ -81,28 +79,27 @@ class _BodyState extends State<_Body> { children: [ FatButton( semanticsLabel: context.l10n.analysis, - onPressed: parsedInput != null - ? () => pushPlatformRoute( + onPressed: + parsedInput != null + ? () => pushPlatformRoute( context, rootNavigator: true, - builder: (context) => AnalysisScreen( - options: parsedInput!.options, - ), + builder: (context) => AnalysisScreen(options: parsedInput!.options), ) - : null, + : null, child: Text(context.l10n.analysis), ), const SizedBox(height: 16.0), FatButton( semanticsLabel: context.l10n.boardEditor, - onPressed: parsedInput != null - ? () => pushPlatformRoute( + onPressed: + parsedInput != null + ? () => pushPlatformRoute( context, rootNavigator: true, - builder: (context) => - BoardEditorScreen(initialFen: parsedInput!.fen), + builder: (context) => BoardEditorScreen(initialFen: parsedInput!.fen), ) - : null, + : null, child: Text(context.l10n.boardEditor), ), ], @@ -137,7 +134,7 @@ class _BodyState extends State<_Body> { isComputerAnalysisAllowed: true, variant: Variant.standard, ), - ) + ), ); } catch (_, __) {} @@ -170,7 +167,7 @@ class _BodyState extends State<_Body> { variant: rule != null ? Variant.fromRule(rule) : Variant.standard, ), initialMoveCursor: mainlineMoves.isEmpty ? 0 : 1, - ) + ), ); } catch (_, __) {} diff --git a/lib/src/view/tools/tools_tab_screen.dart b/lib/src/view/tools/tools_tab_screen.dart index f00a896a22..40e2abde94 100644 --- a/lib/src/view/tools/tools_tab_screen.dart +++ b/lib/src/view/tools/tools_tab_screen.dart @@ -42,15 +42,8 @@ class ToolsTabScreen extends ConsumerWidget { } }, child: Scaffold( - appBar: AppBar( - title: Text(context.l10n.tools), - ), - body: const Column( - children: [ - ConnectivityBanner(), - Expanded(child: _Body()), - ], - ), + appBar: AppBar(title: Text(context.l10n.tools)), + body: const Column(children: [ConnectivityBanner(), Expanded(child: _Body())]), ), ); } @@ -62,10 +55,7 @@ class ToolsTabScreen extends ConsumerWidget { slivers: [ CupertinoSliverNavigationBar(largeTitle: Text(context.l10n.tools)), const SliverToBoxAdapter(child: ConnectivityBanner()), - const SliverSafeArea( - top: false, - sliver: _Body(), - ), + const SliverSafeArea(top: false, sliver: _Body()), ], ), ); @@ -73,11 +63,7 @@ class ToolsTabScreen extends ConsumerWidget { } class _ToolsButton extends StatelessWidget { - const _ToolsButton({ - required this.icon, - required this.title, - required this.onTap, - }); + const _ToolsButton({required this.icon, required this.title, required this.onTap}); final IconData icon; @@ -87,31 +73,32 @@ class _ToolsButton extends StatelessWidget { @override Widget build(BuildContext context) { - final tilePadding = Theme.of(context).platform == TargetPlatform.iOS - ? const EdgeInsets.symmetric(vertical: 8.0) - : EdgeInsets.zero; + final tilePadding = + Theme.of(context).platform == TargetPlatform.iOS + ? const EdgeInsets.symmetric(vertical: 8.0) + : EdgeInsets.zero; return Padding( - padding: Theme.of(context).platform == TargetPlatform.android - ? const EdgeInsets.only(bottom: 16.0) - : EdgeInsets.zero, + padding: + Theme.of(context).platform == TargetPlatform.android + ? const EdgeInsets.only(bottom: 16.0) + : EdgeInsets.zero, child: Opacity( opacity: onTap == null ? 0.5 : 1.0, child: PlatformListTile( leading: Icon( icon, size: Styles.mainListTileIconSize, - color: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoTheme.of(context).primaryColor - : Theme.of(context).colorScheme.primary, - ), - title: Padding( - padding: tilePadding, - child: Text(title, style: Styles.callout), + color: + Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoTheme.of(context).primaryColor + : Theme.of(context).colorScheme.primary, ), - trailing: Theme.of(context).platform == TargetPlatform.iOS - ? const CupertinoListTileChevron() - : null, + title: Padding(padding: tilePadding, child: Text(title, style: Styles.callout)), + trailing: + Theme.of(context).platform == TargetPlatform.iOS + ? const CupertinoListTileChevron() + : null, onTap: onTap, ), ), @@ -124,96 +111,97 @@ class _Body extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final isOnline = - ref.watch(connectivityChangesProvider).valueOrNull?.isOnline ?? false; + final isOnline = ref.watch(connectivityChangesProvider).valueOrNull?.isOnline ?? false; final content = [ - if (Theme.of(context).platform == TargetPlatform.android) - const SizedBox(height: 16.0), + if (Theme.of(context).platform == TargetPlatform.android) const SizedBox(height: 16.0), ListSection( hasLeading: true, children: [ _ToolsButton( icon: Icons.upload_file, title: context.l10n.loadPosition, - onTap: () => pushPlatformRoute( - context, - builder: (context) => const LoadPositionScreen(), - ), + onTap: + () => pushPlatformRoute(context, builder: (context) => const LoadPositionScreen()), ), _ToolsButton( icon: Icons.biotech, title: context.l10n.analysis, - onTap: () => pushPlatformRoute( - context, - rootNavigator: true, - builder: (context) => const AnalysisScreen( - options: AnalysisOptions( - orientation: Side.white, - standalone: ( - pgn: '', - isComputerAnalysisAllowed: true, - variant: Variant.standard, - ), - ), - ), - ), - ), - _ToolsButton( - icon: Icons.explore_outlined, - title: context.l10n.openingExplorer, - onTap: isOnline - ? () => pushPlatformRoute( - context, - rootNavigator: true, - builder: (context) => const OpeningExplorerScreen( + onTap: + () => pushPlatformRoute( + context, + rootNavigator: true, + builder: + (context) => const AnalysisScreen( options: AnalysisOptions( orientation: Side.white, standalone: ( pgn: '', - isComputerAnalysisAllowed: false, + isComputerAnalysisAllowed: true, variant: Variant.standard, ), ), ), + ), + ), + _ToolsButton( + icon: Icons.explore_outlined, + title: context.l10n.openingExplorer, + onTap: + isOnline + ? () => pushPlatformRoute( + context, + rootNavigator: true, + builder: + (context) => const OpeningExplorerScreen( + options: AnalysisOptions( + orientation: Side.white, + standalone: ( + pgn: '', + isComputerAnalysisAllowed: false, + variant: Variant.standard, + ), + ), + ), ) - : null, + : null, ), if (isOnline) _ToolsButton( icon: LichessIcons.study, title: context.l10n.studyMenu, - onTap: () => pushPlatformRoute( - context, - builder: (context) => const StudyListScreen(), - ), + onTap: + () => pushPlatformRoute(context, builder: (context) => const StudyListScreen()), ), _ToolsButton( icon: Icons.edit_outlined, title: context.l10n.boardEditor, - onTap: () => pushPlatformRoute( - context, - builder: (context) => const BoardEditorScreen(), - rootNavigator: true, - ), + onTap: + () => pushPlatformRoute( + context, + builder: (context) => const BoardEditorScreen(), + rootNavigator: true, + ), ), _ToolsButton( icon: Icons.where_to_vote_outlined, title: 'Coordinate Training', // TODO l10n - onTap: () => pushPlatformRoute( - context, - rootNavigator: true, - builder: (context) => const CoordinateTrainingScreen(), - ), + onTap: + () => pushPlatformRoute( + context, + rootNavigator: true, + builder: (context) => const CoordinateTrainingScreen(), + ), ), _ToolsButton( icon: Icons.alarm, title: context.l10n.clock, - onTap: () => pushPlatformRoute( - context, - builder: (context) => const ClockToolScreen(), - rootNavigator: true, - ), + onTap: + () => pushPlatformRoute( + context, + builder: (context) => const ClockToolScreen(), + rootNavigator: true, + ), ), ], ), @@ -221,9 +209,6 @@ class _Body extends ConsumerWidget { return Theme.of(context).platform == TargetPlatform.iOS ? SliverList(delegate: SliverChildListDelegate(content)) - : ListView( - controller: puzzlesScrollController, - children: content, - ); + : ListView(controller: puzzlesScrollController, children: content); } } diff --git a/lib/src/view/user/challenge_requests_screen.dart b/lib/src/view/user/challenge_requests_screen.dart index f00783d2eb..990dd32b00 100644 --- a/lib/src/view/user/challenge_requests_screen.dart +++ b/lib/src/view/user/challenge_requests_screen.dart @@ -21,9 +21,7 @@ class ChallengeRequestsScreen extends StatelessWidget { @override Widget build(BuildContext context) { return PlatformScaffold( - appBar: PlatformAppBar( - title: Text(context.l10n.preferencesNotifyChallenge), - ), + appBar: PlatformAppBar(title: Text(context.l10n.preferencesNotifyChallenge)), body: _Body(), ); } @@ -45,8 +43,8 @@ class _Body extends ConsumerWidget { return ListView.separated( itemCount: list.length, - separatorBuilder: (context, index) => - const PlatformDivider(height: 1, cupertinoHasLeading: true), + separatorBuilder: + (context, index) => const PlatformDivider(height: 1, cupertinoHasLeading: true), itemBuilder: (context, index) { final challenge = list[index]; final user = challenge.challenger?.user; @@ -56,52 +54,43 @@ class _Body extends ConsumerWidget { Future acceptChallenge(BuildContext context) async { final challengeRepo = ref.read(challengeRepositoryProvider); await challengeRepo.accept(challenge.id); - final fullId = await challengeRepo.show(challenge.id).then( - (challenge) => challenge.gameFullId, - ); + final fullId = await challengeRepo + .show(challenge.id) + .then((challenge) => challenge.gameFullId); if (!context.mounted) return; pushPlatformRoute( context, rootNavigator: true, builder: (BuildContext context) { - return GameScreen( - initialGameId: fullId, - ); + return GameScreen(initialGameId: fullId); }, ); } - Future declineChallenge( - ChallengeDeclineReason? reason, - ) async { - ref - .read(challengeRepositoryProvider) - .decline(challenge.id, reason: reason); - ref - .read(notificationServiceProvider) - .cancel(challenge.id.value.hashCode); + Future declineChallenge(ChallengeDeclineReason? reason) async { + ref.read(challengeRepositoryProvider).decline(challenge.id, reason: reason); + ref.read(notificationServiceProvider).cancel(challenge.id.value.hashCode); } void confirmDialog() { showAdaptiveActionSheet( context: context, - title: challenge.variant.isPlaySupported - ? const Text('Do you accept the challenge?') - : null, + title: + challenge.variant.isPlaySupported + ? const Text('Do you accept the challenge?') + : null, actions: [ if (challenge.variant.isPlaySupported) BottomSheetAction( makeLabel: (context) => Text(context.l10n.accept), - leading: - Icon(Icons.check, color: context.lichessColors.good), + leading: Icon(Icons.check, color: context.lichessColors.good), isDefaultAction: true, onPressed: (context) => acceptChallenge(context), ), ...ChallengeDeclineReason.values.map( (reason) => BottomSheetAction( makeLabel: (context) => Text(reason.label(context.l10n)), - leading: - Icon(Icons.close, color: context.lichessColors.error), + leading: Icon(Icons.close, color: context.lichessColors.error), isDestructiveAction: true, onPressed: (_) { declineChallenge(reason); @@ -113,33 +102,30 @@ class _Body extends ConsumerWidget { } void showMissingAccountMessage() { - showPlatformSnackbar( - context, - context.l10n.youNeedAnAccountToDoThat, - ); + showPlatformSnackbar(context, context.l10n.youNeedAnAccountToDoThat); } return ChallengeListItem( challenge: challenge, challengerUser: user, - onPressed: challenge.direction == ChallengeDirection.inward - ? session == null - ? showMissingAccountMessage - : confirmDialog - : null, - onAccept: challenge.direction == ChallengeDirection.outward || - !challenge.variant.isPlaySupported - ? null - : session == null + onPressed: + challenge.direction == ChallengeDirection.inward + ? session == null + ? showMissingAccountMessage + : confirmDialog + : null, + onAccept: + challenge.direction == ChallengeDirection.outward || + !challenge.variant.isPlaySupported + ? null + : session == null ? showMissingAccountMessage : () => acceptChallenge(context), - onCancel: challenge.direction == ChallengeDirection.outward - ? () => - ref.read(challengeRepositoryProvider).cancel(challenge.id) - : null, - onDecline: challenge.direction == ChallengeDirection.inward - ? declineChallenge - : null, + onCancel: + challenge.direction == ChallengeDirection.outward + ? () => ref.read(challengeRepositoryProvider).cancel(challenge.id) + : null, + onDecline: challenge.direction == ChallengeDirection.inward ? declineChallenge : null, ); }, ); @@ -147,8 +133,7 @@ class _Body extends ConsumerWidget { loading: () { return const Center(child: CircularProgressIndicator.adaptive()); }, - error: (error, stack) => - const Center(child: Text('Error loading challenges')), + error: (error, stack) => const Center(child: Text('Error loading challenges')), ); } } diff --git a/lib/src/view/user/game_history_screen.dart b/lib/src/view/user/game_history_screen.dart index 93cd532e69..5ed1dc176d 100644 --- a/lib/src/view/user/game_history_screen.dart +++ b/lib/src/view/user/game_history_screen.dart @@ -30,16 +30,15 @@ class GameHistoryScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final filtersInUse = ref.watch(gameFilterProvider(filter: gameFilter)); - final nbGamesAsync = ref.watch( - userNumberOfGamesProvider(user, isOnline: isOnline), - ); - final title = filtersInUse.count == 0 - ? nbGamesAsync.when( - data: (nbGames) => Text(context.l10n.nbGames(nbGames)), - loading: () => const ButtonLoadingIndicator(), - error: (e, s) => Text(context.l10n.mobileAllGames), - ) - : Text(filtersInUse.selectionLabel(context)); + final nbGamesAsync = ref.watch(userNumberOfGamesProvider(user, isOnline: isOnline)); + final title = + filtersInUse.count == 0 + ? nbGamesAsync.when( + data: (nbGames) => Text(context.l10n.nbGames(nbGames)), + loading: () => const ButtonLoadingIndicator(), + error: (e, s) => Text(context.l10n.mobileAllGames), + ) + : Text(filtersInUse.selectionLabel(context)); final filterBtn = AppBarIconButton( icon: Badge.count( backgroundColor: Theme.of(context).colorScheme.secondary, @@ -52,38 +51,31 @@ class GameHistoryScreen extends ConsumerWidget { child: const Icon(Icons.tune), ), semanticsLabel: context.l10n.filterGames, - onPressed: () => showAdaptiveBottomSheet( - context: context, - isScrollControlled: true, - builder: (_) => _FilterGames( - filter: ref.read(gameFilterProvider(filter: gameFilter)), - user: user, - ), - ).then((value) { - if (value != null) { - ref - .read(gameFilterProvider(filter: gameFilter).notifier) - .setFilter(value); - } - }), + onPressed: + () => showAdaptiveBottomSheet( + context: context, + isScrollControlled: true, + builder: + (_) => _FilterGames( + filter: ref.read(gameFilterProvider(filter: gameFilter)), + user: user, + ), + ).then((value) { + if (value != null) { + ref.read(gameFilterProvider(filter: gameFilter).notifier).setFilter(value); + } + }), ); return PlatformScaffold( - appBar: PlatformAppBar( - title: title, - actions: [filterBtn], - ), + appBar: PlatformAppBar(title: title, actions: [filterBtn]), body: _Body(user: user, isOnline: isOnline, gameFilter: gameFilter), ); } } class _Body extends ConsumerStatefulWidget { - const _Body({ - required this.user, - required this.isOnline, - required this.gameFilter, - }); + const _Body({required this.user, required this.isOnline, required this.gameFilter}); final LightUser? user; final bool isOnline; @@ -110,8 +102,7 @@ class _BodyState extends ConsumerState<_Body> { } void _scrollListener() { - if (_scrollController.position.pixels >= - _scrollController.position.maxScrollExtent - 300) { + if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 300) { final state = ref.read( userGameHistoryProvider( widget.user?.id, @@ -143,14 +134,9 @@ class _BodyState extends ConsumerState<_Body> { @override Widget build(BuildContext context) { - final gameFilterState = - ref.watch(gameFilterProvider(filter: widget.gameFilter)); + final gameFilterState = ref.watch(gameFilterProvider(filter: widget.gameFilter)); final gameListState = ref.watch( - userGameHistoryProvider( - widget.user?.id, - isOnline: widget.isOnline, - filter: gameFilterState, - ), + userGameHistoryProvider(widget.user?.id, isOnline: widget.isOnline, filter: gameFilterState), ); return gameListState.when( @@ -159,64 +145,45 @@ class _BodyState extends ConsumerState<_Body> { return list.isEmpty ? const Padding( - padding: EdgeInsets.symmetric(vertical: 32.0), - child: Center( - child: Text( - 'No games found', - ), - ), - ) + padding: EdgeInsets.symmetric(vertical: 32.0), + child: Center(child: Text('No games found')), + ) : ListView.separated( - controller: _scrollController, - separatorBuilder: (context, index) => - Theme.of(context).platform == TargetPlatform.iOS - ? const PlatformDivider( - height: 1, - cupertinoHasLeading: true, - ) - : const PlatformDivider( - height: 1, - color: Colors.transparent, - ), - itemCount: list.length + (state.isLoading ? 1 : 0), - itemBuilder: (context, index) { - if (state.isLoading && index == list.length) { - return const Padding( - padding: EdgeInsets.symmetric(vertical: 32.0), - child: CenterLoadingIndicator(), - ); - } else if (state.hasError && - state.hasMore && - index == list.length) { - // TODO: add a retry button - return const Padding( - padding: EdgeInsets.symmetric(vertical: 32.0), - child: Center( - child: Text( - 'Could not load more games', - ), - ), - ); - } - - return ExtendedGameListTile( - item: list[index], - userId: widget.user?.id, - // see: https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/cupertino/list_tile.dart#L30 for horizontal padding value - padding: Theme.of(context).platform == TargetPlatform.iOS - ? const EdgeInsets.symmetric( - horizontal: 14.0, - vertical: 12.0, - ) - : null, + controller: _scrollController, + separatorBuilder: + (context, index) => + Theme.of(context).platform == TargetPlatform.iOS + ? const PlatformDivider(height: 1, cupertinoHasLeading: true) + : const PlatformDivider(height: 1, color: Colors.transparent), + itemCount: list.length + (state.isLoading ? 1 : 0), + itemBuilder: (context, index) { + if (state.isLoading && index == list.length) { + return const Padding( + padding: EdgeInsets.symmetric(vertical: 32.0), + child: CenterLoadingIndicator(), + ); + } else if (state.hasError && state.hasMore && index == list.length) { + // TODO: add a retry button + return const Padding( + padding: EdgeInsets.symmetric(vertical: 32.0), + child: Center(child: Text('Could not load more games')), ); - }, - ); + } + + return ExtendedGameListTile( + item: list[index], + userId: widget.user?.id, + // see: https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/cupertino/list_tile.dart#L30 for horizontal padding value + padding: + Theme.of(context).platform == TargetPlatform.iOS + ? const EdgeInsets.symmetric(horizontal: 14.0, vertical: 12.0) + : null, + ); + }, + ); }, error: (e, s) { - debugPrint( - 'SEVERE: [GameHistoryScreen] could not load game list', - ); + debugPrint('SEVERE: [GameHistoryScreen] could not load game list'); return const Center(child: Text('Could not load Game History')); }, loading: () => const CenterLoadingIndicator(), @@ -225,10 +192,7 @@ class _BodyState extends ConsumerState<_Body> { } class _FilterGames extends ConsumerStatefulWidget { - const _FilterGames({ - required this.filter, - required this.user, - }); + const _FilterGames({required this.filter, required this.user}); final GameFilterState filter; final LightUser? user; @@ -269,15 +233,16 @@ class _FilterGamesState extends ConsumerState<_FilterGames> { final session = ref.read(authSessionProvider); final userId = widget.user?.id ?? session?.user.id; - final Widget filters = userId != null - ? ref.watch(userProvider(id: userId)).when( - data: (user) => perfFilter(availablePerfs(user)), - loading: () => const Center( - child: CircularProgressIndicator.adaptive(), - ), - error: (_, __) => perfFilter(gamePerfs), - ) - : perfFilter(gamePerfs); + final Widget filters = + userId != null + ? ref + .watch(userProvider(id: userId)) + .when( + data: (user) => perfFilter(availablePerfs(user)), + loading: () => const Center(child: CircularProgressIndicator.adaptive()), + error: (_, __) => perfFilter(gamePerfs), + ) + : perfFilter(gamePerfs); return BottomSheetScrollableContainer( padding: const EdgeInsets.all(16.0), @@ -291,15 +256,15 @@ class _FilterGamesState extends ConsumerState<_FilterGames> { filterType: FilterType.singleChoice, choices: Side.values, choiceSelected: (choice) => filter.side == choice, - choiceLabel: (t) => switch (t) { - Side.white => Text(context.l10n.white), - Side.black => Text(context.l10n.black), - }, - onSelected: (value, selected) => setState( - () { - filter = filter.copyWith(side: selected ? value : null); - }, - ), + choiceLabel: + (t) => switch (t) { + Side.white => Text(context.l10n.white), + Side.black => Text(context.l10n.black), + }, + onSelected: + (value, selected) => setState(() { + filter = filter.copyWith(side: selected ? value : null); + }), ), Row( mainAxisAlignment: MainAxisAlignment.end, @@ -320,31 +285,30 @@ class _FilterGamesState extends ConsumerState<_FilterGames> { } List availablePerfs(User user) { - final perfs = gamePerfs.where((perf) { - final p = user.perfs[perf]; - return p != null && p.numberOfGamesOrRuns > 0; - }).toList(growable: false); + final perfs = gamePerfs + .where((perf) { + final p = user.perfs[perf]; + return p != null && p.numberOfGamesOrRuns > 0; + }) + .toList(growable: false); perfs.sort( - (p1, p2) => user.perfs[p2]!.numberOfGamesOrRuns - .compareTo(user.perfs[p1]!.numberOfGamesOrRuns), + (p1, p2) => + user.perfs[p2]!.numberOfGamesOrRuns.compareTo(user.perfs[p1]!.numberOfGamesOrRuns), ); return perfs; } Widget perfFilter(List choices) => Filter( - filterName: context.l10n.variant, - filterType: FilterType.multipleChoice, - choices: choices, - choiceSelected: (choice) => filter.perfs.contains(choice), - choiceLabel: (t) => Text(t.shortTitle), - onSelected: (value, selected) => setState( - () { - filter = filter.copyWith( - perfs: selected - ? filter.perfs.add(value) - : filter.perfs.remove(value), - ); - }, - ), - ); + filterName: context.l10n.variant, + filterType: FilterType.multipleChoice, + choices: choices, + choiceSelected: (choice) => filter.perfs.contains(choice), + choiceLabel: (t) => Text(t.shortTitle), + onSelected: + (value, selected) => setState(() { + filter = filter.copyWith( + perfs: selected ? filter.perfs.add(value) : filter.perfs.remove(value), + ); + }), + ); } diff --git a/lib/src/view/user/leaderboard_screen.dart b/lib/src/view/user/leaderboard_screen.dart index c544eecc24..db62d23f29 100644 --- a/lib/src/view/user/leaderboard_screen.dart +++ b/lib/src/view/user/leaderboard_screen.dart @@ -21,9 +21,7 @@ class LeaderboardScreen extends StatelessWidget { @override Widget build(BuildContext context) { return PlatformScaffold( - appBar: PlatformAppBar( - title: Text(context.l10n.leaderboard), - ), + appBar: PlatformAppBar(title: Text(context.l10n.leaderboard)), body: const _Body(), ); } @@ -42,43 +40,15 @@ class _Body extends ConsumerWidget { _Leaderboard(data.bullet, LichessIcons.bullet, 'BULLET'), _Leaderboard(data.blitz, LichessIcons.blitz, 'BLITZ'), _Leaderboard(data.rapid, LichessIcons.rapid, 'RAPID'), - _Leaderboard( - data.classical, - LichessIcons.classical, - 'CLASSICAL', - ), - _Leaderboard( - data.ultrabullet, - LichessIcons.ultrabullet, - 'ULTRA BULLET', - ), - _Leaderboard( - data.crazyhouse, - LichessIcons.h_square, - 'CRAZYHOUSE', - ), - _Leaderboard( - data.chess960, - LichessIcons.die_six, - 'CHESS 960', - ), - _Leaderboard( - data.kingOfThehill, - LichessIcons.bullet, - 'KING OF THE HILL', - ), - _Leaderboard( - data.threeCheck, - LichessIcons.three_check, - 'THREE CHECK', - ), + _Leaderboard(data.classical, LichessIcons.classical, 'CLASSICAL'), + _Leaderboard(data.ultrabullet, LichessIcons.ultrabullet, 'ULTRA BULLET'), + _Leaderboard(data.crazyhouse, LichessIcons.h_square, 'CRAZYHOUSE'), + _Leaderboard(data.chess960, LichessIcons.die_six, 'CHESS 960'), + _Leaderboard(data.kingOfThehill, LichessIcons.bullet, 'KING OF THE HILL'), + _Leaderboard(data.threeCheck, LichessIcons.three_check, 'THREE CHECK'), _Leaderboard(data.atomic, LichessIcons.atom, 'ATOMIC'), _Leaderboard(data.horde, LichessIcons.horde, 'HORDE'), - _Leaderboard( - data.antichess, - LichessIcons.antichess, - 'ANTICHESS', - ), + _Leaderboard(data.antichess, LichessIcons.antichess, 'ANTICHESS'), _Leaderboard( data.racingKings, LichessIcons.racing_kings, @@ -91,17 +61,10 @@ class _Body extends ConsumerWidget { child: SingleChildScrollView( child: LayoutBuilder( builder: (context, constraints) { - final crossAxisCount = - math.min(3, (constraints.maxWidth / 300).floor()); + final crossAxisCount = math.min(3, (constraints.maxWidth / 300).floor()); return LayoutGrid( - columnSizes: List.generate( - crossAxisCount, - (_) => 1.fr, - ), - rowSizes: List.generate( - (list.length / crossAxisCount).ceil(), - (_) => auto, - ), + columnSizes: List.generate(crossAxisCount, (_) => 1.fr), + rowSizes: List.generate((list.length / crossAxisCount).ceil(), (_) => auto), children: list, ); }, @@ -110,8 +73,7 @@ class _Body extends ConsumerWidget { ); }, loading: () => const Center(child: CircularProgressIndicator.adaptive()), - error: (error, stack) => - const Center(child: Text('Could not load leaderboard.')), + error: (error, stack) => const Center(child: Text('Could not load leaderboard.')), ); } } @@ -134,19 +96,12 @@ class LeaderboardListTile extends StatelessWidget { child: UserFullNameWidget(user: user.lightUser), ), subtitle: perfIcon != null ? Text(user.rating.toString()) : null, - trailing: perfIcon != null - ? _Progress(user.progress) - : Text(user.rating.toString()), + trailing: perfIcon != null ? _Progress(user.progress) : Text(user.rating.toString()), ); } void _handleTap(BuildContext context) { - pushPlatformRoute( - context, - builder: (context) => UserScreen( - user: user.lightUser, - ), - ); + pushPlatformRoute(context, builder: (context) => UserScreen(user: user.lightUser)); } } @@ -162,22 +117,16 @@ class _Progress extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ Icon( - progress > 0 - ? LichessIcons.arrow_full_upperright - : LichessIcons.arrow_full_lowerright, + progress > 0 ? LichessIcons.arrow_full_upperright : LichessIcons.arrow_full_lowerright, size: 16, - color: progress > 0 - ? context.lichessColors.good - : context.lichessColors.error, + color: progress > 0 ? context.lichessColors.good : context.lichessColors.error, ), Text( '${progress.abs()}', maxLines: 1, style: TextStyle( fontSize: 12, - color: progress > 0 - ? context.lichessColors.good - : context.lichessColors.error, + color: progress > 0 ? context.lichessColors.good : context.lichessColors.error, ), ), ], @@ -186,12 +135,7 @@ class _Progress extends StatelessWidget { } class _Leaderboard extends StatelessWidget { - const _Leaderboard( - this.userList, - this.iconData, - this.title, { - this.showDivider = true, - }); + const _Leaderboard(this.userList, this.iconData, this.title, {this.showDivider = true}); final List userList; final IconData iconData; final String title; @@ -211,8 +155,7 @@ class _Leaderboard extends StatelessWidget { Text(title), ], ), - children: - userList.map((user) => LeaderboardListTile(user: user)).toList(), + children: userList.map((user) => LeaderboardListTile(user: user)).toList(), ), ); } diff --git a/lib/src/view/user/leaderboard_widget.dart b/lib/src/view/user/leaderboard_widget.dart index 2f35923970..6ab071ad7e 100644 --- a/lib/src/view/user/leaderboard_widget.dart +++ b/lib/src/view/user/leaderboard_widget.dart @@ -22,9 +22,7 @@ class LeaderboardWidget extends ConsumerWidget { data: (data) { return ListSection( hasLeading: true, - header: Text( - context.l10n.leaderboard, - ), + header: Text(context.l10n.leaderboard), headerTrailing: NoPaddingTextButton( onPressed: () { pushPlatformRoute( @@ -33,16 +31,11 @@ class LeaderboardWidget extends ConsumerWidget { builder: (context) => const LeaderboardScreen(), ); }, - child: Text( - context.l10n.more, - ), + child: Text(context.l10n.more), ), children: [ for (final entry in data.entries) - LeaderboardListTile( - user: entry.value, - perfIcon: entry.key.icon, - ), + LeaderboardListTile(user: entry.value, perfIcon: entry.key.icon), ], ); }, @@ -55,15 +48,13 @@ class LeaderboardWidget extends ConsumerWidget { child: Text('Could not load leaderboard.'), ); }, - loading: () => Shimmer( - child: ShimmerLoading( - isLoading: true, - child: ListSection.loading( - itemsNumber: 5, - header: true, + loading: + () => Shimmer( + child: ShimmerLoading( + isLoading: true, + child: ListSection.loading(itemsNumber: 5, header: true), + ), ), - ), - ), ); } } diff --git a/lib/src/view/user/perf_cards.dart b/lib/src/view/user/perf_cards.dart index 8aea390e7d..32dff9b364 100644 --- a/lib/src/view/user/perf_cards.dart +++ b/lib/src/view/user/perf_cards.dart @@ -15,12 +15,7 @@ import 'package:lichess_mobile/src/widgets/rating.dart'; /// A widget that displays the performance cards of a user. class PerfCards extends StatelessWidget { - const PerfCards({ - required this.user, - required this.isMe, - this.padding, - super.key, - }); + const PerfCards({required this.user, required this.isMe, this.padding, super.key}); final User user; @@ -31,32 +26,34 @@ class PerfCards extends StatelessWidget { @override Widget build(BuildContext context) { const puzzlePerfsSet = {Perf.puzzle, Perf.streak, Perf.storm}; - final List gamePerfs = Perf.values.where((element) { - if (puzzlePerfsSet.contains(element)) { - return false; - } - final p = user.perfs[element]; - return p != null && - p.numberOfGamesOrRuns > 0 && - p.ratingDeviation < kClueLessDeviation; - }).toList(growable: false); + final List gamePerfs = Perf.values + .where((element) { + if (puzzlePerfsSet.contains(element)) { + return false; + } + final p = user.perfs[element]; + return p != null && p.numberOfGamesOrRuns > 0 && p.ratingDeviation < kClueLessDeviation; + }) + .toList(growable: false); gamePerfs.sort( - (p1, p2) => user.perfs[p2]!.numberOfGamesOrRuns - .compareTo(user.perfs[p1]!.numberOfGamesOrRuns), + (p1, p2) => + user.perfs[p2]!.numberOfGamesOrRuns.compareTo(user.perfs[p1]!.numberOfGamesOrRuns), ); - final List puzzlePerfs = Perf.values.where((element) { - if (!puzzlePerfsSet.contains(element)) { - return false; - } - final p = user.perfs[element]; - return p != null && p.numberOfGamesOrRuns > 0; - }).toList(growable: false); + final List puzzlePerfs = Perf.values + .where((element) { + if (!puzzlePerfsSet.contains(element)) { + return false; + } + final p = user.perfs[element]; + return p != null && p.numberOfGamesOrRuns > 0; + }) + .toList(growable: false); puzzlePerfs.sort( - (p1, p2) => user.perfs[p2]!.numberOfGamesOrRuns - .compareTo(user.perfs[p1]!.numberOfGamesOrRuns), + (p1, p2) => + user.perfs[p2]!.numberOfGamesOrRuns.compareTo(user.perfs[p1]!.numberOfGamesOrRuns), ); final userPerfs = [...gamePerfs, ...puzzlePerfs]; @@ -84,18 +81,13 @@ class PerfCards extends StatelessWidget { child: PlatformCard( child: AdaptiveInkWell( borderRadius: const BorderRadius.all(Radius.circular(10)), - onTap: isPerfWithoutStats - ? null - : () => _handlePerfCardTap(context, perf), + onTap: isPerfWithoutStats ? null : () => _handlePerfCardTap(context, perf), child: Padding( padding: const EdgeInsets.all(6.0), child: Column( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - Text( - perf.shortTitle, - style: TextStyle(color: textShade(context, 0.7)), - ), + Text(perf.shortTitle, style: TextStyle(color: textShade(context, 0.7))), Icon(perf.icon, color: textShade(context, 0.6)), Row( mainAxisAlignment: MainAxisAlignment.center, @@ -114,17 +106,19 @@ class PerfCards extends StatelessWidget { userPerf.progression > 0 ? LichessIcons.arrow_full_upperright : LichessIcons.arrow_full_lowerright, - color: userPerf.progression > 0 - ? context.lichessColors.good - : context.lichessColors.error, + color: + userPerf.progression > 0 + ? context.lichessColors.good + : context.lichessColors.error, size: 12, ), Text( userPerf.progression.abs().toString(), style: TextStyle( - color: userPerf.progression > 0 - ? context.lichessColors.good - : context.lichessColors.error, + color: + userPerf.progression > 0 + ? context.lichessColors.good + : context.lichessColors.error, fontSize: 11, ), ), @@ -153,10 +147,7 @@ class PerfCards extends StatelessWidget { case Perf.storm: return StormDashboardModal(user: user.lightUser); default: - return PerfStatsScreen( - user: user, - perf: perf, - ); + return PerfStatsScreen(user: user, perf: perf); } }, ); diff --git a/lib/src/view/user/perf_stats_screen.dart b/lib/src/view/user/perf_stats_screen.dart index b26f90f383..0c4bf74f56 100644 --- a/lib/src/view/user/perf_stats_screen.dart +++ b/lib/src/view/user/perf_stats_screen.dart @@ -43,11 +43,7 @@ const _defaultValueFontSize = 18.0; const _mainValueStyle = TextStyle(fontWeight: FontWeight.bold, fontSize: 30); class PerfStatsScreen extends StatelessWidget { - const PerfStatsScreen({ - required this.user, - required this.perf, - super.key, - }); + const PerfStatsScreen({required this.user, required this.perf, super.key}); final User user; final Perf perf; @@ -55,10 +51,7 @@ class PerfStatsScreen extends StatelessWidget { @override Widget build(BuildContext context) { return PlatformScaffold( - appBar: PlatformAppBar( - androidTitleSpacing: 0, - title: _Title(user: user, perf: perf), - ), + appBar: PlatformAppBar(androidTitleSpacing: 0, title: _Title(user: user, perf: perf)), body: _Body(user: user, perf: perf), ); } @@ -72,60 +65,62 @@ class _Title extends StatelessWidget { @override Widget build(BuildContext context) { - final allPerfs = Perf.values.where((element) { - if ([perf, Perf.storm, Perf.streak, Perf.fromPosition] - .contains(element)) { - return false; - } - final p = user.perfs[element]; - return p != null && - p.games != null && - p.games! > 0 && - p.ratingDeviation < kClueLessDeviation; - }).toList(growable: false); + final allPerfs = Perf.values + .where((element) { + if ([perf, Perf.storm, Perf.streak, Perf.fromPosition].contains(element)) { + return false; + } + final p = user.perfs[element]; + return p != null && + p.games != null && + p.games! > 0 && + p.ratingDeviation < kClueLessDeviation; + }) + .toList(growable: false); return AppBarTextButton( child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(perf.icon), - Text( - ' ${context.l10n.perfStatPerfStats(perf.title)}', - overflow: TextOverflow.ellipsis, - ), + Text(' ${context.l10n.perfStatPerfStats(perf.title)}', overflow: TextOverflow.ellipsis), const Icon(Icons.arrow_drop_down), ], ), onPressed: () { showAdaptiveActionSheet( context: context, - actions: allPerfs.map((p) { - return BottomSheetAction( - makeLabel: (context) => Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - p.icon, - color: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoTheme.of(context).primaryColor - : null, - ), - const SizedBox(width: 6), - Text( - context.l10n.perfStatPerfStats(p.title), - overflow: TextOverflow.ellipsis, - ), - ], - ), - onPressed: (ctx) { - pushReplacementPlatformRoute( - context, - builder: (ctx) { - return PerfStatsScreen(user: user, perf: p); + actions: allPerfs + .map((p) { + return BottomSheetAction( + makeLabel: + (context) => Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + p.icon, + color: + Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoTheme.of(context).primaryColor + : null, + ), + const SizedBox(width: 6), + Text( + context.l10n.perfStatPerfStats(p.title), + overflow: TextOverflow.ellipsis, + ), + ], + ), + onPressed: (ctx) { + pushReplacementPlatformRoute( + context, + builder: (ctx) { + return PerfStatsScreen(user: user, perf: p); + }, + ); }, ); - }, - ); - }).toList(growable: false), + }) + .toList(growable: false), ); }, ); @@ -133,10 +128,7 @@ class _Title extends StatelessWidget { } class _Body extends ConsumerWidget { - const _Body({ - required this.user, - required this.perf, - }); + const _Body({required this.user, required this.perf}); final User user; final Perf perf; @@ -157,11 +149,11 @@ class _Body extends ConsumerWidget { children: [ ratingHistory.when( data: (ratingHistoryData) { - final ratingHistoryPerfData = ratingHistoryData - .firstWhereOrNull((element) => element.perf == perf); + final ratingHistoryPerfData = ratingHistoryData.firstWhereOrNull( + (element) => element.perf == perf, + ); - if (ratingHistoryPerfData == null || - ratingHistoryPerfData.points.isEmpty) { + if (ratingHistoryPerfData == null || ratingHistoryPerfData.points.isEmpty) { return const SizedBox.shrink(); } return _EloChart(ratingHistoryPerfData); @@ -180,10 +172,7 @@ class _Body extends ConsumerWidget { crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [ - Text( - '${context.l10n.rating} ', - style: Styles.sectionTitle, - ), + Text('${context.l10n.rating} ', style: Styles.sectionTitle), RatingWidget( rating: data.rating, deviation: data.deviation, @@ -197,38 +186,35 @@ class _Body extends ConsumerWidget { Text( (loggedInUser != null && loggedInUser.user.id == user.id) ? context.l10n.youAreBetterThanPercentOfPerfTypePlayers( - '${data.percentile!.toStringAsFixed(2)}%', - perf.title, - ) + '${data.percentile!.toStringAsFixed(2)}%', + perf.title, + ) : context.l10n.userIsBetterThanPercentOfPerfTypePlayers( - user.username, - '${data.percentile!.toStringAsFixed(2)}%', - perf.title, - ), + user.username, + '${data.percentile!.toStringAsFixed(2)}%', + perf.title, + ), style: TextStyle(color: textShade(context, 0.7)), ), subStatSpace, // The number '12' here is not arbitrary, since the API returns the progression for the last 12 games (as far as I know). StatCard( - context.l10n - .perfStatProgressOverLastXGames('12') - .replaceAll(':', ''), + context.l10n.perfStatProgressOverLastXGames('12').replaceAll(':', ''), child: ProgressionWidget(data.progress), ), StatCardRow([ if (data.rank != null) StatCard( context.l10n.rank, - value: data.rank == null - ? '?' - : NumberFormat.decimalPattern( - Intl.getCurrentLocale(), - ).format(data.rank), + value: + data.rank == null + ? '?' + : NumberFormat.decimalPattern( + Intl.getCurrentLocale(), + ).format(data.rank), ), StatCard( - context.l10n - .perfStatRatingDeviation('') - .replaceAll(': .', ''), + context.l10n.perfStatRatingDeviation('').replaceAll(': .', ''), value: data.deviation.toStringAsFixed(2), ), ]), @@ -263,11 +249,12 @@ class _Body extends ConsumerWidget { onTap: () { pushPlatformRoute( context, - builder: (context) => GameHistoryScreen( - user: user.lightUser, - isOnline: true, - gameFilter: GameFilterState(perfs: ISet({perf})), - ), + builder: + (context) => GameHistoryScreen( + user: user.lightUser, + isOnline: true, + gameFilter: GameFilterState(perfs: ISet({perf})), + ), ); }, child: Padding( @@ -277,8 +264,7 @@ class _Body extends ConsumerWidget { textBaseline: TextBaseline.alphabetic, children: [ Text( - '${context.l10n.perfStatTotalGames} ' - .localizeNumbers(), + '${context.l10n.perfStatTotalGames} '.localizeNumbers(), style: Styles.sectionTitle, ), Text( @@ -286,12 +272,8 @@ class _Body extends ConsumerWidget { style: _mainValueStyle, ), Text( - String.fromCharCode( - Icons.arrow_forward_ios.codePoint, - ), - style: Styles.sectionTitle.copyWith( - fontFamily: 'MaterialIcons', - ), + String.fromCharCode(Icons.arrow_forward_ios.codePoint), + style: Styles.sectionTitle.copyWith(fontFamily: 'MaterialIcons'), ), ], ), @@ -330,47 +312,32 @@ class _Body extends ConsumerWidget { StatCardRow([ StatCard( context.l10n.rated, - child: _PercentageValueWidget( - data.ratedGames, - data.totalGames, - ), + child: _PercentageValueWidget(data.ratedGames, data.totalGames), ), StatCard( context.l10n.tournament, - child: _PercentageValueWidget( - data.tournamentGames, - data.totalGames, - ), + child: _PercentageValueWidget(data.tournamentGames, data.totalGames), ), StatCard( context.l10n.perfStatBerserkedGames.replaceAll( ' ${context.l10n.games.toLowerCase()}', '', ), - child: _PercentageValueWidget( - data.berserkGames, - data.totalGames, - ), + child: _PercentageValueWidget(data.berserkGames, data.totalGames), ), StatCard( context.l10n.perfStatDisconnections, - child: _PercentageValueWidget( - data.disconnections, - data.totalGames, - ), + child: _PercentageValueWidget(data.disconnections, data.totalGames), ), ]), StatCardRow([ StatCard( context.l10n.averageOpponent, - value: data.avgOpponent == null - ? '?' - : data.avgOpponent.toString(), + value: data.avgOpponent == null ? '?' : data.avgOpponent.toString(), ), StatCard( context.l10n.perfStatTimeSpentPlaying, - value: data.timePlayed - .toDaysHoursMinutes(AppLocalizations.of(context)), + value: data.timePlayed.toDaysHoursMinutes(AppLocalizations.of(context)), ), ]), StatCard( @@ -403,10 +370,7 @@ class _Body extends ConsumerWidget { games: data.bestWins!, perf: perf, user: user, - header: Text( - context.l10n.perfStatBestRated, - style: Styles.sectionTitle, - ), + header: Text(context.l10n.perfStatBestRated, style: Styles.sectionTitle), ), ], ], @@ -414,9 +378,7 @@ class _Body extends ConsumerWidget { ); }, error: (error, stackTrace) { - debugPrint( - 'SEVERE: [PerfStatsScreen] could not load data; $error\n$stackTrace', - ); + debugPrint('SEVERE: [PerfStatsScreen] could not load data; $error\n$stackTrace'); return const Center(child: Text('Could not load user stats.')); }, loading: () => const CenterLoadingIndicator(), @@ -441,10 +403,7 @@ class _UserGameWidget extends StatelessWidget { return game == null ? Text('?', style: defaultDateStyle) - : Text( - _dateFormatter.format(game!.finishedAt), - style: defaultDateStyle, - ); + : Text(_dateFormatter.format(game!.finishedAt), style: defaultDateStyle); } } @@ -460,15 +419,15 @@ class _RatingWidget extends StatelessWidget { return (rating == null) ? const Text('?', style: TextStyle(fontSize: _defaultValueFontSize)) : Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - rating.toString(), - style: TextStyle(fontSize: _defaultValueFontSize, color: color), - ), - _UserGameWidget(game), - ], - ); + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + rating.toString(), + style: TextStyle(fontSize: _defaultValueFontSize, color: color), + ), + _UserGameWidget(game), + ], + ); } } @@ -478,12 +437,7 @@ class _PercentageValueWidget extends StatelessWidget { final Color? color; final bool isShaded; - const _PercentageValueWidget( - this.value, - this.denominator, { - this.color, - this.isShaded = false, - }); + const _PercentageValueWidget(this.value, this.denominator, {this.color, this.isShaded = false}); String _getPercentageString(num numerator, num denominator) { return '${((numerator / denominator) * 100).round()}%'; @@ -502,9 +456,10 @@ class _PercentageValueWidget extends StatelessWidget { _getPercentageString(value, denominator), style: TextStyle( fontSize: _defaultValueFontSize, - color: isShaded - ? textShade(context, _customOpacity / 2) - : textShade(context, _customOpacity), + color: + isShaded + ? textShade(context, _customOpacity / 2) + : textShade(context, _customOpacity), ), ), ], @@ -528,83 +483,76 @@ class _StreakWidget extends StatelessWidget { color: textShade(context, _customOpacity), ); - final longestStreakStr = - context.l10n.perfStatLongestStreak('').replaceAll(':', ''); - final currentStreakStr = - context.l10n.perfStatCurrentStreak('').replaceAll(':', ''); - - final List streakWidgets = - [maxStreak, curStreak].mapIndexed((index, streak) { - final streakTitle = Text( - index == 0 ? longestStreakStr : currentStreakStr, - style: streakTitleStyle, - ); - - if (streak == null || streak.isValueEmpty) { - return Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - streakTitle, - Text( - '-', - style: const TextStyle(fontSize: _defaultValueFontSize), - semanticsLabel: context.l10n.none, - ), - ], - ), - ); - } + final longestStreakStr = context.l10n.perfStatLongestStreak('').replaceAll(':', ''); + final currentStreakStr = context.l10n.perfStatCurrentStreak('').replaceAll(':', ''); - final Text valueText = streak.map( - timeStreak: (UserTimeStreak streak) { - return Text( - streak.timePlayed.toDaysHoursMinutes(AppLocalizations.of(context)), - style: valueStyle, - textAlign: TextAlign.center, - ); - }, - gameStreak: (UserGameStreak streak) { - return Text( - context.l10n.nbGames(streak.gamesPlayed), - style: valueStyle, - textAlign: TextAlign.center, + final List streakWidgets = [maxStreak, curStreak] + .mapIndexed((index, streak) { + final streakTitle = Text( + index == 0 ? longestStreakStr : currentStreakStr, + style: streakTitleStyle, ); - }, - ); - return Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - streakTitle, - valueText, - if (streak.startGame != null && streak.endGame != null) - Column( + if (streak == null || streak.isValueEmpty) { + return Expanded( + child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const SizedBox(height: 5.0), - _UserGameWidget(streak.startGame), - Icon( - Icons.arrow_downward_rounded, - color: textShade(context, _customOpacity), + streakTitle, + Text( + '-', + style: const TextStyle(fontSize: _defaultValueFontSize), + semanticsLabel: context.l10n.none, ), - _UserGameWidget(streak.endGame), ], ), - ], - ), - ); - }).toList(growable: false); + ); + } + + final Text valueText = streak.map( + timeStreak: (UserTimeStreak streak) { + return Text( + streak.timePlayed.toDaysHoursMinutes(AppLocalizations.of(context)), + style: valueStyle, + textAlign: TextAlign.center, + ); + }, + gameStreak: (UserGameStreak streak) { + return Text( + context.l10n.nbGames(streak.gamesPlayed), + style: valueStyle, + textAlign: TextAlign.center, + ); + }, + ); + + return Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + streakTitle, + valueText, + if (streak.startGame != null && streak.endGame != null) + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 5.0), + _UserGameWidget(streak.startGame), + Icon(Icons.arrow_downward_rounded, color: textShade(context, _customOpacity)), + _UserGameWidget(streak.endGame), + ], + ), + ], + ), + ); + }) + .toList(growable: false); return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const SizedBox(height: 5.0), - Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: streakWidgets, - ), + Row(mainAxisAlignment: MainAxisAlignment.spaceAround, children: streakWidgets), ], ); } @@ -637,35 +585,23 @@ class _GameListWidget extends ConsumerWidget { final list = await ref.withClient( (client) => GameRepository(client).getGamesByIds(gameIds), ); - final gameData = - list.firstWhereOrNull((g) => g.id == game.gameId); - if (context.mounted && - gameData != null && - gameData.variant.isReadSupported) { + final gameData = list.firstWhereOrNull((g) => g.id == game.gameId); + if (context.mounted && gameData != null && gameData.variant.isReadSupported) { pushPlatformRoute( context, rootNavigator: true, - builder: (context) => ArchivedGameScreen( - gameData: gameData, - orientation: user.id == gameData.white.user?.id - ? Side.white - : Side.black, - ), + builder: + (context) => ArchivedGameScreen( + gameData: gameData, + orientation: user.id == gameData.white.user?.id ? Side.white : Side.black, + ), ); } else if (context.mounted && gameData != null) { - showPlatformSnackbar( - context, - 'This variant is not supported yet', - ); + showPlatformSnackbar(context, 'This variant is not supported yet'); } }, - playerTitle: UserFullNameWidget( - user: game.opponent, - rating: game.opponentRating, - ), - subtitle: Text( - _dateFormatter.format(game.finishedAt), - ), + playerTitle: UserFullNameWidget(user: game.opponent, rating: game.opponentRating), + subtitle: Text(_dateFormatter.format(game.finishedAt)), ), ], ); @@ -673,11 +609,7 @@ class _GameListWidget extends ConsumerWidget { } class _GameListTile extends StatelessWidget { - const _GameListTile({ - required this.playerTitle, - this.subtitle, - this.onTap, - }); + const _GameListTile({required this.playerTitle, this.subtitle, this.onTap}); final Widget playerTitle; final Widget? subtitle; @@ -688,14 +620,13 @@ class _GameListTile extends StatelessWidget { return PlatformListTile( onTap: onTap, title: playerTitle, - subtitle: subtitle != null - ? DefaultTextStyle.merge( - child: subtitle!, - style: TextStyle( - color: textShade(context, Styles.subtitleOpacity), - ), - ) - : null, + subtitle: + subtitle != null + ? DefaultTextStyle.merge( + child: subtitle!, + style: TextStyle(color: textShade(context, Styles.subtitleOpacity)), + ) + : null, ); } } @@ -714,11 +645,8 @@ class _EloChartState extends State<_EloChart> { late List _allFlSpot; - List get _flSpot => _allFlSpot - .where( - (element) => element.x >= _minX && element.x <= _maxX, - ) - .toList(); + List get _flSpot => + _allFlSpot.where((element) => element.x >= _minX && element.x <= _maxX).toList(); IList get _points => widget.value.points; @@ -726,24 +654,21 @@ class _EloChartState extends State<_EloChart> { DateTime get _lastDate => _points.last.date; - double get _minY => - (_flSpot.map((e) => e.y).reduce(min) / 100).floorToDouble() * 100; + double get _minY => (_flSpot.map((e) => e.y).reduce(min) / 100).floorToDouble() * 100; - double get _maxY => - (_flSpot.map((e) => e.y).reduce(max) / 100).ceilToDouble() * 100; + double get _maxY => (_flSpot.map((e) => e.y).reduce(max) / 100).ceilToDouble() * 100; - double get _minX => - _startDate(_selectedRange).difference(_firstDate).inDays.toDouble(); + double get _minX => _startDate(_selectedRange).difference(_firstDate).inDays.toDouble(); double get _maxX => _allFlSpot.last.x; DateTime _startDate(DateRange dateRange) => switch (dateRange) { - DateRange.oneWeek => _lastDate.subtract(const Duration(days: 7)), - DateRange.oneMonth => _lastDate.copyWith(month: _lastDate.month - 1), - DateRange.threeMonths => _lastDate.copyWith(month: _lastDate.month - 3), - DateRange.oneYear => _lastDate.copyWith(year: _lastDate.year - 1), - DateRange.allTime => _firstDate, - }; + DateRange.oneWeek => _lastDate.subtract(const Duration(days: 7)), + DateRange.oneMonth => _lastDate.copyWith(month: _lastDate.month - 1), + DateRange.threeMonths => _lastDate.copyWith(month: _lastDate.month - 3), + DateRange.oneYear => _lastDate.copyWith(year: _lastDate.year - 1), + DateRange.allTime => _firstDate, + }; bool _dateIsInRange(DateRange dateRange) => _firstDate.isBefore(_startDate(dateRange)) || @@ -766,22 +691,20 @@ class _EloChartState extends State<_EloChart> { j += 1; } else { pointsHistoryRatingCompleted.add( - UserRatingHistoryPoint( - date: currentDate, - elo: _points[j - 1].elo, - ), + UserRatingHistoryPoint(date: currentDate, elo: _points[j - 1].elo), ); } } - _allFlSpot = pointsHistoryRatingCompleted - .map( - (element) => FlSpot( - element.date.difference(_firstDate).inDays.toDouble(), - element.elo.toDouble(), - ), - ) - .toList(); + _allFlSpot = + pointsHistoryRatingCompleted + .map( + (element) => FlSpot( + element.date.difference(_firstDate).inDays.toDouble(), + element.elo.toDouble(), + ), + ) + .toList(); if (_dateIsInRange(DateRange.threeMonths)) { _selectedRange = DateRange.threeMonths; @@ -796,8 +719,7 @@ class _EloChartState extends State<_EloChart> { @override Widget build(BuildContext context) { - final borderColor = - Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5); + final borderColor = Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5); final chartColor = Theme.of(context).colorScheme.tertiary; final chartDateFormatter = switch (_selectedRange) { DateRange.oneWeek => DateFormat.MMMd(), @@ -807,24 +729,18 @@ class _EloChartState extends State<_EloChart> { DateRange.allTime => DateFormat.yMMM(), }; - String formatDateFromTimestamp(double nbDays) => chartDateFormatter.format( - _firstDate.add(Duration(days: nbDays.toInt())), - ); + String formatDateFromTimestamp(double nbDays) => + chartDateFormatter.format(_firstDate.add(Duration(days: nbDays.toInt()))); String formatDateFromTimestampForTooltip(double nbDays) => - DateFormat.yMMMd().format( - _firstDate.add(Duration(days: nbDays.toInt())), - ); + DateFormat.yMMMd().format(_firstDate.add(Duration(days: nbDays.toInt()))); Widget leftTitlesWidget(double value, TitleMeta meta) { return SideTitleWidget( axisSide: meta.axisSide, child: Text( value.toInt().toString(), - style: const TextStyle( - color: Colors.grey, - fontSize: 10, - ), + style: const TextStyle(color: Colors.grey, fontSize: 10), ), ); } @@ -836,10 +752,7 @@ class _EloChartState extends State<_EloChart> { axisSide: meta.axisSide, child: Text( formatDateFromTimestamp(value), - style: const TextStyle( - color: Colors.grey, - fontSize: 10, - ), + style: const TextStyle(color: Colors.grey, fontSize: 10), ), ); } @@ -849,9 +762,7 @@ class _EloChartState extends State<_EloChart> { Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - const SizedBox( - width: 25, - ), + const SizedBox(width: 25), ...DateRange.values .where((dateRange) => _dateIsInRange(dateRange)) .map( @@ -880,10 +791,7 @@ class _EloChartState extends State<_EloChart> { spots: _flSpot, dotData: const FlDotData(show: false), color: chartColor, - belowBarData: BarAreaData( - color: chartColor.withValues(alpha: 0.2), - show: true, - ), + belowBarData: BarAreaData(color: chartColor.withValues(alpha: 0.2), show: true), barWidth: 1.5, ), ], @@ -897,10 +805,7 @@ class _EloChartState extends State<_EloChart> { gridData: FlGridData( show: true, drawVerticalLine: false, - getDrawingHorizontalLine: (value) => FlLine( - color: borderColor, - strokeWidth: 0.5, - ), + getDrawingHorizontalLine: (value) => FlLine(color: borderColor, strokeWidth: 0.5), ), lineTouchData: LineTouchData( touchSpotThreshold: double.infinity, @@ -916,13 +821,8 @@ class _EloChartState extends State<_EloChart> { Styles.bold, children: [ TextSpan( - text: formatDateFromTimestampForTooltip( - touchedSpot.x, - ), - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 10, - ), + text: formatDateFromTimestampForTooltip(touchedSpot.x), + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 10), ), ], ), @@ -933,17 +833,11 @@ class _EloChartState extends State<_EloChart> { getTouchedSpotIndicator: (barData, spotIndexes) { return spotIndexes.map((spotIndex) { return TouchedSpotIndicatorData( - FlLine( - color: chartColor, - strokeWidth: 2, - ), + FlLine(color: chartColor, strokeWidth: 2), FlDotData( show: true, getDotPainter: (spot, percent, barData, index) { - return FlDotCirclePainter( - radius: 5, - color: chartColor, - ); + return FlDotCirclePainter(radius: 5, color: chartColor); }, ), ); @@ -951,10 +845,8 @@ class _EloChartState extends State<_EloChart> { }, ), titlesData: FlTitlesData( - rightTitles: - const AxisTitles(sideTitles: SideTitles(showTitles: false)), - topTitles: - const AxisTitles(sideTitles: SideTitles(showTitles: false)), + rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), + topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, @@ -983,11 +875,7 @@ class _EloChartState extends State<_EloChart> { } class _RangeButton extends StatelessWidget { - const _RangeButton({ - required this.text, - required this.onPressed, - this.selected = false, - }); + const _RangeButton({required this.text, required this.onPressed, this.selected = false}); final String text; final VoidCallback onPressed; @@ -1005,8 +893,7 @@ class _RangeButton extends StatelessWidget { onTap: onPressed, child: Center( child: Padding( - padding: - const EdgeInsets.symmetric(vertical: 5.0, horizontal: 10.0), + padding: const EdgeInsets.symmetric(vertical: 5.0, horizontal: 10.0), child: Text(text), ), ), @@ -1024,10 +911,10 @@ enum DateRange { @override String toString() => switch (this) { - DateRange.oneWeek => '1W', - DateRange.oneMonth => '1M', - DateRange.threeMonths => '3M', - DateRange.oneYear => '1Y', - DateRange.allTime => 'ALL', - }; + DateRange.oneWeek => '1W', + DateRange.oneMonth => '1M', + DateRange.threeMonths => '3M', + DateRange.oneYear => '1Y', + DateRange.allTime => 'ALL', + }; } diff --git a/lib/src/view/user/player_screen.dart b/lib/src/view/user/player_screen.dart index a947bacaf4..ef9408f0d3 100644 --- a/lib/src/view/user/player_screen.dart +++ b/lib/src/view/user/player_screen.dart @@ -35,9 +35,7 @@ class PlayerScreen extends ConsumerWidget { } }, child: PlatformScaffold( - appBar: PlatformAppBar( - title: Text(context.l10n.players), - ), + appBar: PlatformAppBar(title: Text(context.l10n.players)), body: _Body(), ), ); @@ -51,10 +49,7 @@ class _Body extends ConsumerWidget { return ListView( children: [ - const Padding( - padding: Styles.bodySectionPadding, - child: _SearchButton(), - ), + const Padding(padding: Styles.bodySectionPadding, child: _SearchButton()), if (session != null) _OnlineFriendsWidget(), RatingPrefAware(child: LeaderboardWidget()), ], @@ -72,19 +67,18 @@ class _SearchButton extends StatelessWidget { @override Widget build(BuildContext context) { - void onUserTap(LightUser user) => pushPlatformRoute( - context, - builder: (ctx) => UserScreen(user: user), - ); + void onUserTap(LightUser user) => + pushPlatformRoute(context, builder: (ctx) => UserScreen(user: user)); return PlatformSearchBar( hintText: context.l10n.searchSearch, focusNode: AlwaysDisabledFocusNode(), - onTap: () => pushPlatformRoute( - context, - fullscreenDialog: true, - builder: (_) => SearchScreen(onUserTap: onUserTap), - ), + onTap: + () => pushPlatformRoute( + context, + fullscreenDialog: true, + builder: (_) => SearchScreen(onUserTap: onUserTap), + ), ); } } @@ -98,21 +92,18 @@ class _OnlineFriendsWidget extends ConsumerWidget { data: (data) { return ListSection( header: Text(context.l10n.nbFriendsOnline(data.length)), - headerTrailing: data.isEmpty - ? null - : NoPaddingTextButton( - onPressed: () => _handleTap(context, data), - child: Text( - context.l10n.more, + headerTrailing: + data.isEmpty + ? null + : NoPaddingTextButton( + onPressed: () => _handleTap(context, data), + child: Text(context.l10n.more), ), - ), children: [ if (data.isEmpty) PlatformListTile( title: Text(context.l10n.friends), - trailing: const Icon( - Icons.chevron_right, - ), + trailing: const Icon(Icons.chevron_right), onTap: () => _handleTap(context, data), ), for (final user in data) @@ -121,13 +112,12 @@ class _OnlineFriendsWidget extends ConsumerWidget { padding: const EdgeInsets.only(right: 5.0), child: UserFullNameWidget(user: user), ), - onTap: () => pushPlatformRoute( - context, - title: user.name, - builder: (_) => UserScreen( - user: user, - ), - ), + onTap: + () => pushPlatformRoute( + context, + title: user.name, + builder: (_) => UserScreen(user: user), + ), ), ], ); @@ -136,19 +126,15 @@ class _OnlineFriendsWidget extends ConsumerWidget { debugPrint( 'SEVERE: [PlayerScreen] could not load following online users; $error\n $stackTrace', ); - return const Center( - child: Text('Could not load online friends'), - ); + return const Center(child: Text('Could not load online friends')); }, - loading: () => Shimmer( - child: ShimmerLoading( - isLoading: true, - child: ListSection.loading( - itemsNumber: 3, - header: true, + loading: + () => Shimmer( + child: ShimmerLoading( + isLoading: true, + child: ListSection.loading(itemsNumber: 3, header: true), + ), ), - ), - ), ); } diff --git a/lib/src/view/user/recent_games.dart b/lib/src/view/user/recent_games.dart index 7a7651e7cb..ecae5bcb05 100644 --- a/lib/src/view/user/recent_games.dart +++ b/lib/src/view/user/recent_games.dart @@ -28,16 +28,15 @@ class RecentGamesWidget extends ConsumerWidget { final session = ref.watch(authSessionProvider); final userId = user?.id ?? session?.user.id; - final recentGames = user != null - ? ref.watch(userRecentGamesProvider(userId: user!.id)) - : ref.watch(myRecentGamesProvider); + final recentGames = + user != null + ? ref.watch(userRecentGamesProvider(userId: user!.id)) + : ref.watch(myRecentGamesProvider); - final nbOfGames = ref + final nbOfGames = + ref .watch( - userNumberOfGamesProvider( - user, - isOnline: connectivity.valueOrNull?.isOnline == true, - ), + userNumberOfGamesProvider(user, isOnline: connectivity.valueOrNull?.isOnline == true), ) .valueOrNull ?? 0; @@ -50,45 +49,42 @@ class RecentGamesWidget extends ConsumerWidget { return ListSection( header: Text(context.l10n.recentGames), hasLeading: true, - headerTrailing: nbOfGames > data.length - ? NoPaddingTextButton( - onPressed: () { - pushPlatformRoute( - context, - builder: (context) => GameHistoryScreen( - user: user, - isOnline: connectivity.valueOrNull?.isOnline == true, - ), - ); - }, - child: Text( - context.l10n.more, - ), - ) - : null, - children: data.map((item) { - return ExtendedGameListTile(item: item, userId: userId); - }).toList(), + headerTrailing: + nbOfGames > data.length + ? NoPaddingTextButton( + onPressed: () { + pushPlatformRoute( + context, + builder: + (context) => GameHistoryScreen( + user: user, + isOnline: connectivity.valueOrNull?.isOnline == true, + ), + ); + }, + child: Text(context.l10n.more), + ) + : null, + children: + data.map((item) { + return ExtendedGameListTile(item: item, userId: userId); + }).toList(), ); }, error: (error, stackTrace) { - debugPrint( - 'SEVERE: [RecentGames] could not recent games; $error\n$stackTrace', - ); + debugPrint('SEVERE: [RecentGames] could not recent games; $error\n$stackTrace'); return const Padding( padding: Styles.bodySectionPadding, child: Text('Could not load recent games.'), ); }, - loading: () => Shimmer( - child: ShimmerLoading( - isLoading: true, - child: ListSection.loading( - itemsNumber: 10, - header: true, + loading: + () => Shimmer( + child: ShimmerLoading( + isLoading: true, + child: ListSection.loading(itemsNumber: 10, header: true), + ), ), - ), - ), ); } } diff --git a/lib/src/view/user/search_screen.dart b/lib/src/view/user/search_screen.dart index 790729686f..9640201853 100644 --- a/lib/src/view/user/search_screen.dart +++ b/lib/src/view/user/search_screen.dart @@ -17,9 +17,7 @@ import 'package:lichess_mobile/src/widgets/user_list_tile.dart'; const _kSaveHistoryDebouncTimer = Duration(seconds: 2); class SearchScreen extends ConsumerStatefulWidget { - const SearchScreen({ - this.onUserTap, - }); + const SearchScreen({this.onUserTap}); final void Function(LightUser)? onUserTap; @@ -80,27 +78,26 @@ class _SearchScreenState extends ConsumerState { final body = _Body(_term, setSearchText, widget.onUserTap); return PlatformWidget( - androidBuilder: (context) => Scaffold( - appBar: AppBar( - toolbarHeight: 80, // Custom height to fit the search bar - title: searchBar, - ), - body: body, - ), - iosBuilder: (context) => CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - automaticallyImplyLeading: false, - middle: SizedBox( - height: 36.0, - child: searchBar, + androidBuilder: + (context) => Scaffold( + appBar: AppBar( + toolbarHeight: 80, // Custom height to fit the search bar + title: searchBar, + ), + body: body, ), - trailing: NoPaddingTextButton( - child: Text(context.l10n.close), - onPressed: () => Navigator.pop(context), + iosBuilder: + (context) => CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + automaticallyImplyLeading: false, + middle: SizedBox(height: 36.0, child: searchBar), + trailing: NoPaddingTextButton( + child: Text(context.l10n.close), + onPressed: () => Navigator.pop(context), + ), + ), + child: body, ), - ), - child: body, - ), ); } } @@ -114,34 +111,33 @@ class _Body extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { if (term != null) { - return SafeArea( - child: _UserList(term!, onUserTap), - ); + return SafeArea(child: _UserList(term!, onUserTap)); } else { final searchHistory = ref.watch(searchHistoryProvider).history; return SafeArea( child: SingleChildScrollView( - child: searchHistory.isEmpty - ? kEmptyWidget - : ListSection( - header: Text(context.l10n.mobileRecentSearches), - headerTrailing: NoPaddingTextButton( - child: Text(context.l10n.mobileClearButton), - onPressed: () => - ref.read(searchHistoryProvider.notifier).clear(), + child: + searchHistory.isEmpty + ? kEmptyWidget + : ListSection( + header: Text(context.l10n.mobileRecentSearches), + headerTrailing: NoPaddingTextButton( + child: Text(context.l10n.mobileClearButton), + onPressed: () => ref.read(searchHistoryProvider.notifier).clear(), + ), + showDividerBetweenTiles: true, + hasLeading: true, + children: + searchHistory + .map( + (term) => PlatformListTile( + leading: const Icon(Icons.history), + title: Text(term), + onTap: () => onRecentSearchTap(term), + ), + ) + .toList(), ), - showDividerBetweenTiles: true, - hasLeading: true, - children: searchHistory - .map( - (term) => PlatformListTile( - leading: const Icon(Icons.history), - title: Text(term), - onTap: () => onRecentSearchTap(term), - ), - ) - .toList(), - ), ), ); } @@ -159,36 +155,39 @@ class _UserList extends ConsumerWidget { final autoComplete = ref.watch(autoCompleteUserProvider(term)); return SingleChildScrollView( child: autoComplete.when( - data: (userList) => userList.isNotEmpty - ? ListSection( - header: Row( - children: [ - const Icon(Icons.person), - const SizedBox(width: 8), - Text(context.l10n.mobilePlayersMatchingSearchTerm(term)), - ], - ), - hasLeading: true, - showDividerBetweenTiles: true, - children: userList - .map( - (user) => UserListTile.fromLightUser( - user, - onTap: () { - if (onUserTap != null) { - onUserTap!.call(user); - } - }, + data: + (userList) => + userList.isNotEmpty + ? ListSection( + header: Row( + children: [ + const Icon(Icons.person), + const SizedBox(width: 8), + Text(context.l10n.mobilePlayersMatchingSearchTerm(term)), + ], ), + hasLeading: true, + showDividerBetweenTiles: true, + children: + userList + .map( + (user) => UserListTile.fromLightUser( + user, + onTap: () { + if (onUserTap != null) { + onUserTap!.call(user); + } + }, + ), + ) + .toList(), ) - .toList(), - ) - : Column( - children: [ - const SizedBox(height: 16.0), - Center(child: Text(context.l10n.mobileNoSearchResults)), - ], - ), + : Column( + children: [ + const SizedBox(height: 16.0), + Center(child: Text(context.l10n.mobileNoSearchResults)), + ], + ), error: (e, _) { debugPrint('Error loading search results: $e'); return const Column( @@ -198,12 +197,7 @@ class _UserList extends ConsumerWidget { ], ); }, - loading: () => const Column( - children: [ - SizedBox(height: 16.0), - CenterLoadingIndicator(), - ], - ), + loading: () => const Column(children: [SizedBox(height: 16.0), CenterLoadingIndicator()]), ), ); } diff --git a/lib/src/view/user/user_activity.dart b/lib/src/view/user/user_activity.dart index 99d073b7af..ab57c5c966 100644 --- a/lib/src/view/user/user_activity.dart +++ b/lib/src/view/user/user_activity.dart @@ -22,9 +22,10 @@ class UserActivityWidget extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final activity = user != null - ? ref.watch(userActivityProvider(id: user!.id)) - : ref.watch(accountActivityProvider); + final activity = + user != null + ? ref.watch(userActivityProvider(id: user!.id)) + : ref.watch(accountActivityProvider); return activity.when( data: (data) { @@ -33,30 +34,23 @@ class UserActivityWidget extends ConsumerWidget { return const SizedBox.shrink(); } return ListSection( - header: - Text(context.l10n.activityActivity, style: Styles.sectionTitle), + header: Text(context.l10n.activityActivity, style: Styles.sectionTitle), hasLeading: true, - children: nonEmptyActivities - .take(10) - .map((entry) => UserActivityEntry(entry: entry)) - .toList(), + children: + nonEmptyActivities.take(10).map((entry) => UserActivityEntry(entry: entry)).toList(), ); }, error: (error, stackTrace) { - debugPrint( - 'SEVERE: [UserScreen] could not load user activity; $error\n$stackTrace', - ); + debugPrint('SEVERE: [UserScreen] could not load user activity; $error\n$stackTrace'); return const Text('Could not load user activity'); }, - loading: () => Shimmer( - child: ShimmerLoading( - isLoading: true, - child: ListSection.loading( - itemsNumber: 10, - header: true, + loading: + () => Shimmer( + child: ShimmerLoading( + isLoading: true, + child: ListSection.loading(itemsNumber: 10, header: true), + ), ), - ), - ), ); } } @@ -70,8 +64,7 @@ class UserActivityEntry extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); final leadingIconSize = theme.platform == TargetPlatform.iOS ? 26.0 : 36.0; - final emptySubtitle = - theme.platform == TargetPlatform.iOS ? const SizedBox.shrink() : null; + final emptySubtitle = theme.platform == TargetPlatform.iOS ? const SizedBox.shrink() : null; final redColor = theme.extension()?.error; final greenColor = theme.extension()?.good; @@ -80,68 +73,45 @@ class UserActivityEntry extends ConsumerWidget { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Padding( - padding: const EdgeInsets.only( - left: 14.0, - top: 16.0, - right: 14.0, - bottom: 4.0, - ), + padding: const EdgeInsets.only(left: 14.0, top: 16.0, right: 14.0, bottom: 4.0), child: Text( _dateFormatter.format(entry.startTime), - style: TextStyle( - color: context.lichessColors.brag, - fontWeight: FontWeight.bold, - ), + style: TextStyle(color: context.lichessColors.brag, fontWeight: FontWeight.bold), ), ), if (entry.games != null) for (final gameEntry in entry.games!.entries) _UserActivityListTile( - leading: Icon( - gameEntry.key.icon, - size: leadingIconSize, - ), + leading: Icon(gameEntry.key.icon, size: leadingIconSize), title: context.l10n.activityPlayedNbGames( - gameEntry.value.win + - gameEntry.value.draw + - gameEntry.value.loss, + gameEntry.value.win + gameEntry.value.draw + gameEntry.value.loss, gameEntry.key.title, ), subtitle: RatingPrefAware( child: Row( children: [ - RatingWidget( - deviation: 0, - rating: gameEntry.value.ratingAfter, - ), + RatingWidget(deviation: 0, rating: gameEntry.value.ratingAfter), const SizedBox(width: 3), - if (gameEntry.value.ratingAfter - - gameEntry.value.ratingBefore != - 0) ...[ + if (gameEntry.value.ratingAfter - gameEntry.value.ratingBefore != 0) ...[ Icon( - gameEntry.value.ratingAfter - - gameEntry.value.ratingBefore > - 0 + gameEntry.value.ratingAfter - gameEntry.value.ratingBefore > 0 ? LichessIcons.arrow_full_upperright : LichessIcons.arrow_full_lowerright, - color: gameEntry.value.ratingAfter - - gameEntry.value.ratingBefore > - 0 - ? greenColor - : redColor, + color: + gameEntry.value.ratingAfter - gameEntry.value.ratingBefore > 0 + ? greenColor + : redColor, size: 12, ), Text( - (gameEntry.value.ratingAfter - - gameEntry.value.ratingBefore) + (gameEntry.value.ratingAfter - gameEntry.value.ratingBefore) .abs() .toString(), style: TextStyle( - color: gameEntry.value.ratingAfter - - gameEntry.value.ratingBefore > - 0 - ? greenColor - : redColor, + color: + gameEntry.value.ratingAfter - gameEntry.value.ratingBefore > 0 + ? greenColor + : redColor, fontSize: 11, ), ), @@ -157,46 +127,31 @@ class UserActivityEntry extends ConsumerWidget { ), if (entry.puzzles != null) _UserActivityListTile( - leading: Icon( - LichessIcons.target, - size: leadingIconSize, - ), - title: context.l10n.activitySolvedNbPuzzles( - entry.puzzles!.win + entry.puzzles!.loss, - ), + leading: Icon(LichessIcons.target, size: leadingIconSize), + title: context.l10n.activitySolvedNbPuzzles(entry.puzzles!.win + entry.puzzles!.loss), subtitle: RatingPrefAware( child: Row( children: [ - RatingWidget( - deviation: 0, - rating: entry.puzzles!.ratingAfter, - ), + RatingWidget(deviation: 0, rating: entry.puzzles!.ratingAfter), const SizedBox(width: 3), - if (entry.puzzles!.ratingAfter - - entry.puzzles!.ratingBefore != - 0) ...[ + if (entry.puzzles!.ratingAfter - entry.puzzles!.ratingBefore != 0) ...[ Icon( - entry.puzzles!.ratingAfter - entry.puzzles!.ratingBefore > - 0 + entry.puzzles!.ratingAfter - entry.puzzles!.ratingBefore > 0 ? LichessIcons.arrow_full_upperright : LichessIcons.arrow_full_lowerright, - color: entry.puzzles!.ratingAfter - - entry.puzzles!.ratingBefore > - 0 - ? greenColor - : redColor, + color: + entry.puzzles!.ratingAfter - entry.puzzles!.ratingBefore > 0 + ? greenColor + : redColor, size: 12, ), Text( - (entry.puzzles!.ratingAfter - entry.puzzles!.ratingBefore) - .abs() - .toString(), + (entry.puzzles!.ratingAfter - entry.puzzles!.ratingBefore).abs().toString(), style: TextStyle( - color: entry.puzzles!.ratingAfter - - entry.puzzles!.ratingBefore > - 0 - ? greenColor - : redColor, + color: + entry.puzzles!.ratingAfter - entry.puzzles!.ratingBefore > 0 + ? greenColor + : redColor, fontSize: 11, ), ), @@ -212,44 +167,21 @@ class UserActivityEntry extends ConsumerWidget { ), if (entry.streak != null) _UserActivityListTile( - leading: Icon( - LichessIcons.streak, - size: leadingIconSize, - ), - title: context.l10n.stormPlayedNbRunsOfPuzzleStorm( - entry.streak!.runs, - 'Puzzle Streak', - ), + leading: Icon(LichessIcons.streak, size: leadingIconSize), + title: context.l10n.stormPlayedNbRunsOfPuzzleStorm(entry.streak!.runs, 'Puzzle Streak'), subtitle: emptySubtitle, - trailing: BriefGameResultBox( - win: entry.streak!.score, - draw: 0, - loss: 0, - ), + trailing: BriefGameResultBox(win: entry.streak!.score, draw: 0, loss: 0), ), if (entry.storm != null) _UserActivityListTile( - leading: Icon( - LichessIcons.storm, - size: leadingIconSize, - ), - title: context.l10n.stormPlayedNbRunsOfPuzzleStorm( - entry.storm!.runs, - 'Puzzle Storm', - ), + leading: Icon(LichessIcons.storm, size: leadingIconSize), + title: context.l10n.stormPlayedNbRunsOfPuzzleStorm(entry.storm!.runs, 'Puzzle Storm'), subtitle: emptySubtitle, - trailing: BriefGameResultBox( - win: entry.storm!.score, - draw: 0, - loss: 0, - ), + trailing: BriefGameResultBox(win: entry.storm!.score, draw: 0, loss: 0), ), if (entry.correspondenceEnds != null) _UserActivityListTile( - leading: Icon( - LichessIcons.correspondence, - size: leadingIconSize, - ), + leading: Icon(LichessIcons.correspondence, size: leadingIconSize), title: context.l10n.activityCompletedNbGames( entry.correspondenceEnds!.win + entry.correspondenceEnds!.draw + @@ -262,49 +194,34 @@ class UserActivityEntry extends ConsumerWidget { loss: entry.correspondenceEnds!.loss, ), ), - if (entry.correspondenceMovesNb != null && - entry.correspondenceGamesNb != null) + if (entry.correspondenceMovesNb != null && entry.correspondenceGamesNb != null) _UserActivityListTile( - leading: Icon( - LichessIcons.correspondence, - size: leadingIconSize, - ), - title: context.l10n.activityPlayedNbMoves( - entry.correspondenceMovesNb!, - ), + leading: Icon(LichessIcons.correspondence, size: leadingIconSize), + title: context.l10n.activityPlayedNbMoves(entry.correspondenceMovesNb!), subtitle: Text( - context.l10n.activityInNbCorrespondenceGames( - entry.correspondenceGamesNb!, - ), + context.l10n.activityInNbCorrespondenceGames(entry.correspondenceGamesNb!), ), ), if (entry.tournamentNb != null) _UserActivityListTile( - leading: Icon( - Icons.emoji_events, - size: leadingIconSize, - ), - title: context.l10n.activityCompetedInNbTournaments( - entry.tournamentNb!, - ), - subtitle: entry.bestTournament != null - ? Text( - context.l10n.activityRankedInTournament( - entry.bestTournament!.rank, - entry.bestTournament!.rankPercent.toString(), - entry.bestTournament!.nbGames.toString(), - entry.bestTournament!.name, - ), - maxLines: 2, - ) - : emptySubtitle, + leading: Icon(Icons.emoji_events, size: leadingIconSize), + title: context.l10n.activityCompetedInNbTournaments(entry.tournamentNb!), + subtitle: + entry.bestTournament != null + ? Text( + context.l10n.activityRankedInTournament( + entry.bestTournament!.rank, + entry.bestTournament!.rankPercent.toString(), + entry.bestTournament!.nbGames.toString(), + entry.bestTournament!.name, + ), + maxLines: 2, + ) + : emptySubtitle, ), if (entry.followInNb != null) _UserActivityListTile( - leading: Icon( - Icons.thumb_up, - size: leadingIconSize, - ), + leading: Icon(Icons.thumb_up, size: leadingIconSize), title: context.l10n.activityGainedNbFollowers(entry.followInNb!), subtitle: emptySubtitle, ), @@ -314,12 +231,7 @@ class UserActivityEntry extends ConsumerWidget { } class _UserActivityListTile extends StatelessWidget { - const _UserActivityListTile({ - required this.title, - this.subtitle, - this.trailing, - this.leading, - }); + const _UserActivityListTile({required this.title, this.subtitle, this.trailing, this.leading}); final String title; final Widget? subtitle; @@ -347,10 +259,7 @@ const _gameStatsFontStyle = TextStyle( ); class _ResultBox extends StatelessWidget { - const _ResultBox({ - required this.number, - required this.color, - }); + const _ResultBox({required this.number, required this.color}); final int number; final Color color; @@ -368,10 +277,7 @@ class _ResultBox extends StatelessWidget { padding: const EdgeInsets.all(1.0), child: FittedBox( fit: BoxFit.contain, - child: Text( - number.toString(), - style: _gameStatsFontStyle, - ), + child: Text(number.toString(), style: _gameStatsFontStyle), ), ), ); @@ -379,11 +285,7 @@ class _ResultBox extends StatelessWidget { } class BriefGameResultBox extends StatelessWidget { - const BriefGameResultBox({ - required this.win, - required this.draw, - required this.loss, - }); + const BriefGameResultBox({required this.win, required this.draw, required this.loss}); final int win; final int draw; @@ -395,39 +297,27 @@ class BriefGameResultBox extends StatelessWidget { padding: const EdgeInsets.only(left: 5.0), child: SizedBox( height: 20, - width: (win != 0 ? 1 : 0) * _boxSize + + width: + (win != 0 ? 1 : 0) * _boxSize + (draw != 0 ? 1 : 0) * _boxSize + (loss != 0 ? 1 : 0) * _boxSize + - ((win != 0 ? 1 : 0) + - (draw != 0 ? 1 : 0) + - (loss != 0 ? 1 : 0) - - 1) * - _spaceWidth, + ((win != 0 ? 1 : 0) + (draw != 0 ? 1 : 0) + (loss != 0 ? 1 : 0) - 1) * _spaceWidth, child: Row( children: [ if (win != 0) _ResultBox( number: win, - color: Theme.of(context).extension()?.good ?? - LichessColors.green, - ), - if (win != 0 && draw != 0) - const SizedBox( - width: _spaceWidth, - ), - if (draw != 0) - _ResultBox( - number: draw, - color: context.lichessColors.brag, + color: Theme.of(context).extension()?.good ?? LichessColors.green, ), + if (win != 0 && draw != 0) const SizedBox(width: _spaceWidth), + if (draw != 0) _ResultBox(number: draw, color: context.lichessColors.brag), if ((draw != 0 && loss != 0) || (win != 0 && loss != 0)) - const SizedBox( - width: _spaceWidth, - ), + const SizedBox(width: _spaceWidth), if (loss != 0) _ResultBox( number: loss, - color: Theme.of(context).extension()?.error ?? + color: + Theme.of(context).extension()?.error ?? context.lichessColors.error, ), ], diff --git a/lib/src/view/user/user_profile.dart b/lib/src/view/user/user_profile.dart index 78862eb3cd..24a05b38c0 100644 --- a/lib/src/view/user/user_profile.dart +++ b/lib/src/view/user/user_profile.dart @@ -24,10 +24,7 @@ import 'countries.dart'; const _userNameStyle = TextStyle(fontSize: 20, fontWeight: FontWeight.w500); class UserProfileWidget extends ConsumerWidget { - const UserProfileWidget({ - required this.user, - this.bioMaxLines = 10, - }); + const UserProfileWidget({required this.user, this.bioMaxLines = 10}); final User user; @@ -36,12 +33,10 @@ class UserProfileWidget extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final userFullName = user.profile?.realName != null - ? Text( - user.profile!.realName!, - style: _userNameStyle, - ) - : null; + final userFullName = + user.profile?.realName != null + ? Text(user.profile!.realName!, style: _userNameStyle) + : null; return Padding( padding: Styles.horizontalBodyPadding.add(Styles.sectionTopPadding), @@ -54,9 +49,7 @@ class UserProfileWidget extends ConsumerWidget { child: Row( children: [ Icon(Icons.error, color: context.lichessColors.error), - const SizedBox( - width: 5, - ), + const SizedBox(width: 5), Flexible( child: Text( context.l10n.thisAccountViolatedTos, @@ -70,10 +63,7 @@ class UserProfileWidget extends ConsumerWidget { ), ), if (userFullName != null) - Padding( - padding: const EdgeInsets.only(bottom: 5), - child: userFullName, - ), + Padding(padding: const EdgeInsets.only(bottom: 5), child: userFullName), if (user.profile?.bio != null) Linkify( onOpen: (link) async { @@ -81,52 +71,37 @@ class UserProfileWidget extends ConsumerWidget { final username = link.originText.substring(1); pushPlatformRoute( context, - builder: (ctx) => UserScreen( - user: LightUser( - id: UserId.fromUserName(username), - name: username, - ), - ), + builder: + (ctx) => UserScreen( + user: LightUser(id: UserId.fromUserName(username), name: username), + ), ); } else { launchUrl(Uri.parse(link.url)); } }, - linkifiers: const [ - UrlLinkifier(), - EmailLinkifier(), - UserTagLinkifier(), - ], + linkifiers: const [UrlLinkifier(), EmailLinkifier(), UserTagLinkifier()], text: user.profile!.bio!.replaceAll('\n', ' '), maxLines: bioMaxLines, style: bioStyle, overflow: TextOverflow.ellipsis, - linkStyle: const TextStyle( - color: Colors.blueAccent, - decoration: TextDecoration.none, - ), + linkStyle: const TextStyle(color: Colors.blueAccent, decoration: TextDecoration.none), ), const SizedBox(height: 10), if (user.profile?.fideRating != null) Padding( padding: const EdgeInsets.only(bottom: 5), - child: Text( - '${context.l10n.xRating('FIDE')}: ${user.profile!.fideRating}', - ), + child: Text('${context.l10n.xRating('FIDE')}: ${user.profile!.fideRating}'), ), if (user.profile?.uscfRating != null) Padding( padding: const EdgeInsets.only(bottom: 5), - child: Text( - '${context.l10n.xRating('USCF')}: ${user.profile!.uscfRating}', - ), + child: Text('${context.l10n.xRating('USCF')}: ${user.profile!.uscfRating}'), ), if (user.profile?.ecfRating != null) Padding( padding: const EdgeInsets.only(bottom: 5), - child: Text( - '${context.l10n.xRating('ECF')}: ${user.profile!.ecfRating}', - ), + child: Text('${context.l10n.xRating('ECF')}: ${user.profile!.ecfRating}'), ), if (user.profile != null) Padding( @@ -134,9 +109,7 @@ class UserProfileWidget extends ConsumerWidget { child: Location(profile: user.profile!), ), if (user.createdAt != null) - Text( - '${context.l10n.memberSince} ${DateFormat.yMMMMd().format(user.createdAt!)}', - ), + Text('${context.l10n.memberSince} ${DateFormat.yMMMMd().format(user.createdAt!)}'), if (user.seenAt != null) ...[ const SizedBox(height: 5), Text(context.l10n.lastSeenActive(timeago.format(user.seenAt!))), @@ -145,8 +118,7 @@ class UserProfileWidget extends ConsumerWidget { const SizedBox(height: 5), Text( context.l10n.tpTimeSpentPlaying( - user.playTime!.total - .toDaysHoursMinutes(AppLocalizations.of(context)), + user.playTime!.total.toDaysHoursMinutes(AppLocalizations.of(context)), ), ), ], diff --git a/lib/src/view/user/user_screen.dart b/lib/src/view/user/user_screen.dart index 7ae829104d..416ed1a3d9 100644 --- a/lib/src/view/user/user_screen.dart +++ b/lib/src/view/user/user_screen.dart @@ -53,9 +53,7 @@ class _UserScreenState extends ConsumerState { user: updatedLightUser ?? widget.user, shouldShowOnline: updatedLightUser != null, ), - actions: [ - if (isLoading) const PlatformAppBarLoadingIndicator(), - ], + actions: [if (isLoading) const PlatformAppBarLoadingIndicator()], ), body: asyncUser.when( data: (data) => _UserProfileListView(data.$1, isLoading, setIsLoading), @@ -91,22 +89,15 @@ class _UserProfileListView extends ConsumerWidget { final session = ref.watch(authSessionProvider); if (user.disabled == true) { - return Center( - child: Text( - context.l10n.settingsThisAccountIsClosed, - style: Styles.bold, - ), - ); + return Center(child: Text(context.l10n.settingsThisAccountIsClosed, style: Styles.bold)); } - Future userAction( - Future Function(LichessClient client) action, - ) async { + Future userAction(Future Function(LichessClient client) action) async { setIsLoading(true); try { - await ref.withClient(action).then( - (_) => ref.invalidate(userAndStatusProvider(id: user.id)), - ); + await ref + .withClient(action) + .then((_) => ref.invalidate(userAndStatusProvider(id: user.id))); } finally { setIsLoading(false); } @@ -127,8 +118,7 @@ class _UserProfileListView extends ConsumerWidget { onTap: () { pushPlatformRoute( context, - builder: (context) => - CreateChallengeScreen(user.lightUser), + builder: (context) => CreateChallengeScreen(user.lightUser), ); }, ), @@ -136,56 +126,46 @@ class _UserProfileListView extends ConsumerWidget { PlatformListTile( leading: const Icon(Icons.person_add), title: Text(context.l10n.follow), - onTap: isLoading - ? null - : () => userAction( - (client) => - RelationRepository(client).follow(user.id), - ), + onTap: + isLoading + ? null + : () => + userAction((client) => RelationRepository(client).follow(user.id)), ) else if (user.following == true) PlatformListTile( leading: const Icon(Icons.person_remove), title: Text(context.l10n.unfollow), - onTap: isLoading - ? null - : () => userAction( - (client) => - RelationRepository(client).unfollow(user.id), - ), + onTap: + isLoading + ? null + : () => + userAction((client) => RelationRepository(client).unfollow(user.id)), ), if (user.following != true && user.blocking != true) PlatformListTile( leading: const Icon(Icons.block), title: Text(context.l10n.block), - onTap: isLoading - ? null - : () => userAction( - (client) => - RelationRepository(client).block(user.id), - ), + onTap: + isLoading + ? null + : () => userAction((client) => RelationRepository(client).block(user.id)), ) else if (user.blocking == true) PlatformListTile( leading: const Icon(Icons.block), title: Text(context.l10n.unblock), - onTap: isLoading - ? null - : () => userAction( - (client) => - RelationRepository(client).unblock(user.id), - ), + onTap: + isLoading + ? null + : () => + userAction((client) => RelationRepository(client).unblock(user.id)), ), PlatformListTile( leading: const Icon(Icons.report_problem), title: Text(context.l10n.reportXToModerators(user.username)), onTap: () { - launchUrl( - lichessUri('/report', { - 'username': user.id, - 'login': session.user.id, - }), - ); + launchUrl(lichessUri('/report', {'username': user.id, 'login': session.user.id})); }, ), ], diff --git a/lib/src/view/watch/live_tv_channels_screen.dart b/lib/src/view/watch/live_tv_channels_screen.dart index 2b3eec375a..fdb96f412f 100644 --- a/lib/src/view/watch/live_tv_channels_screen.dart +++ b/lib/src/view/watch/live_tv_channels_screen.dart @@ -26,9 +26,7 @@ class LiveTvChannelsScreen extends ConsumerWidget { } }, child: const PlatformScaffold( - appBar: PlatformAppBar( - title: Text('Lichess TV'), - ), + appBar: PlatformAppBar(title: Text('Lichess TV')), body: _Body(), ), ); @@ -56,10 +54,9 @@ class _Body extends ConsumerWidget { pushPlatformRoute( context, rootNavigator: true, - builder: (_) => TvScreen( - channel: game.channel, - initialGame: (game.id, game.orientation), - ), + builder: + (_) => + TvScreen(channel: game.channel, initialGame: (game.id, game.orientation)), ); }, orientation: game.orientation, @@ -70,15 +67,8 @@ class _Body extends ConsumerWidget { mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - Text( - game.channel.label, - style: Styles.boardPreviewTitle, - ), - Icon( - game.channel.icon, - color: context.lichessColors.brag, - size: 30, - ), + Text(game.channel.label, style: Styles.boardPreviewTitle), + Icon(game.channel.icon, color: context.lichessColors.brag, size: 30), UserFullNameWidget.player( user: game.player.asPlayer.user, aiLevel: game.player.asPlayer.aiLevel, @@ -90,12 +80,8 @@ class _Body extends ConsumerWidget { }, ); }, - loading: () => const Center( - child: CircularProgressIndicator(), - ), - error: (error, stackTrace) => Center( - child: Text(error.toString()), - ), + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, stackTrace) => Center(child: Text(error.toString())), ); } } diff --git a/lib/src/view/watch/streamer_screen.dart b/lib/src/view/watch/streamer_screen.dart index ff83ce9183..4ab5b4ebbf 100644 --- a/lib/src/view/watch/streamer_screen.dart +++ b/lib/src/view/watch/streamer_screen.dart @@ -21,21 +21,13 @@ class StreamerScreen extends StatelessWidget { Widget _buildAndroid(BuildContext context) { return Scaffold( - appBar: AppBar( - title: Text(context.l10n.mobileLiveStreamers), - ), + appBar: AppBar(title: Text(context.l10n.mobileLiveStreamers)), body: ListView( children: [ ListSection( showDividerBetweenTiles: true, children: streamers - .map( - (e) => StreamerListTile( - streamer: e, - showSubtitle: true, - maxSubtitleLines: 4, - ), - ) + .map((e) => StreamerListTile(streamer: e, showSubtitle: true, maxSubtitleLines: 4)) .toList(growable: false), ), ], @@ -45,9 +37,7 @@ class StreamerScreen extends StatelessWidget { Widget _buildIos(BuildContext context) { return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - middle: Text(context.l10n.mobileLiveStreamers), - ), + navigationBar: CupertinoNavigationBar(middle: Text(context.l10n.mobileLiveStreamers)), child: CustomScrollView( slivers: [ SliverSafeArea( @@ -55,15 +45,16 @@ class StreamerScreen extends StatelessWidget { delegate: SliverChildListDelegate([ ListSection( hasLeading: true, - children: streamers - .map( - (e) => StreamerListTile( - streamer: e, - showSubtitle: true, - maxSubtitleLines: 4, - ), - ) - .toList(), + children: + streamers + .map( + (e) => StreamerListTile( + streamer: e, + showSubtitle: true, + maxSubtitleLines: 4, + ), + ) + .toList(), ), ]), ), @@ -89,19 +80,16 @@ class StreamerListTile extends StatelessWidget { Widget build(BuildContext context) { return PlatformListTile( onTap: () async { - final url = - streamer.platform == 'twitch' ? streamer.twitch : streamer.youTube; - if (!await launchUrl( - Uri.parse(url!), - mode: LaunchMode.externalApplication, - )) { + final url = streamer.platform == 'twitch' ? streamer.twitch : streamer.youTube; + if (!await launchUrl(Uri.parse(url!), mode: LaunchMode.externalApplication)) { debugPrint('ERROR: [StreamerWidget] Could not launch $url'); } }, leading: Padding( - padding: Theme.of(context).platform == TargetPlatform.android - ? const EdgeInsets.all(5.0) - : EdgeInsets.zero, + padding: + Theme.of(context).platform == TargetPlatform.android + ? const EdgeInsets.all(5.0) + : EdgeInsets.zero, child: Image.network(streamer.image), ), title: Padding( @@ -111,22 +99,15 @@ class StreamerListTile extends StatelessWidget { if (streamer.title != null) ...[ Text( streamer.title!, - style: TextStyle( - color: context.lichessColors.brag, - fontWeight: FontWeight.bold, - ), + style: TextStyle(color: context.lichessColors.brag, fontWeight: FontWeight.bold), ), const SizedBox(width: 5), ], - Flexible( - child: Text(streamer.username, overflow: TextOverflow.ellipsis), - ), + Flexible(child: Text(streamer.username, overflow: TextOverflow.ellipsis)), ], ), ), - subtitle: showSubtitle - ? Text(streamer.status, maxLines: maxSubtitleLines) - : null, + subtitle: showSubtitle ? Text(streamer.status, maxLines: maxSubtitleLines) : null, isThreeLine: showSubtitle && maxSubtitleLines >= 2, trailing: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, diff --git a/lib/src/view/watch/tv_screen.dart b/lib/src/view/watch/tv_screen.dart index 98df340be2..64e2a639c0 100644 --- a/lib/src/view/watch/tv_screen.dart +++ b/lib/src/view/watch/tv_screen.dart @@ -27,8 +27,7 @@ class TvScreen extends ConsumerStatefulWidget { } class _TvScreenState extends ConsumerState { - TvControllerProvider get _tvGameCtrl => - tvControllerProvider(widget.channel, widget.initialGame); + TvControllerProvider get _tvGameCtrl => tvControllerProvider(widget.channel, widget.initialGame); final _whiteClockKey = GlobalKey(debugLabel: 'whiteClockOnTvScreen'); final _blackClockKey = GlobalKey(debugLabel: 'blackClockOnTvScreen'); @@ -47,9 +46,7 @@ class _TvScreenState extends ConsumerState { child: PlatformScaffold( appBar: PlatformAppBar( title: Text('${widget.channel.label} TV'), - actions: const [ - ToggleSoundButton(), - ], + actions: const [ToggleSoundButton()], ), body: _Body( widget.channel, @@ -86,47 +83,46 @@ class _Body extends ConsumerWidget { child: asyncGame.when( data: (gameState) { final game = gameState.game; - final position = - gameState.game.positionAt(gameState.stepCursor); + final position = gameState.game.positionAt(gameState.stepCursor); final blackPlayerWidget = GamePlayer( player: game.black.setOnGame(true), - clock: gameState.game.clock != null - ? CountdownClockBuilder( - key: blackClockKey, - timeLeft: gameState.game.clock!.black, - delay: gameState.game.clock!.lag ?? - const Duration(milliseconds: 10), - clockUpdatedAt: gameState.game.clock!.at, - active: gameState.activeClockSide == Side.black, - builder: (context, timeLeft) { - return Clock( - timeLeft: timeLeft, - active: gameState.activeClockSide == Side.black, - ); - }, - ) - : null, + clock: + gameState.game.clock != null + ? CountdownClockBuilder( + key: blackClockKey, + timeLeft: gameState.game.clock!.black, + delay: gameState.game.clock!.lag ?? const Duration(milliseconds: 10), + clockUpdatedAt: gameState.game.clock!.at, + active: gameState.activeClockSide == Side.black, + builder: (context, timeLeft) { + return Clock( + timeLeft: timeLeft, + active: gameState.activeClockSide == Side.black, + ); + }, + ) + : null, materialDiff: game.lastMaterialDiffAt(Side.black), ); final whitePlayerWidget = GamePlayer( player: game.white.setOnGame(true), - clock: gameState.game.clock != null - ? CountdownClockBuilder( - key: whiteClockKey, - timeLeft: gameState.game.clock!.white, - clockUpdatedAt: gameState.game.clock!.at, - delay: gameState.game.clock!.lag ?? - const Duration(milliseconds: 10), - active: gameState.activeClockSide == Side.white, - builder: (context, timeLeft) { - return Clock( - timeLeft: timeLeft, - active: gameState.activeClockSide == Side.white, - ); - }, - ) - : null, + clock: + gameState.game.clock != null + ? CountdownClockBuilder( + key: whiteClockKey, + timeLeft: gameState.game.clock!.white, + clockUpdatedAt: gameState.game.clock!.at, + delay: gameState.game.clock!.lag ?? const Duration(milliseconds: 10), + active: gameState.activeClockSide == Side.white, + builder: (context, timeLeft) { + return Clock( + timeLeft: timeLeft, + active: gameState.activeClockSide == Side.white, + ); + }, + ) + : null, materialDiff: game.lastMaterialDiffAt(Side.white), ); @@ -136,31 +132,25 @@ class _Body extends ConsumerWidget { boardSettingsOverrides: const BoardSettingsOverrides( animationDuration: Duration.zero, ), - topTable: gameState.orientation == Side.white - ? blackPlayerWidget - : whitePlayerWidget, - bottomTable: gameState.orientation == Side.white - ? whitePlayerWidget - : blackPlayerWidget, - moves: game.steps - .skip(1) - .map((e) => e.sanMove!.san) - .toList(growable: false), + topTable: + gameState.orientation == Side.white ? blackPlayerWidget : whitePlayerWidget, + bottomTable: + gameState.orientation == Side.white ? whitePlayerWidget : blackPlayerWidget, + moves: game.steps.skip(1).map((e) => e.sanMove!.san).toList(growable: false), currentMoveIndex: gameState.stepCursor, lastMove: game.moveAt(gameState.stepCursor), ); }, - loading: () => const BoardTable( - topTable: kEmptyWidget, - bottomTable: kEmptyWidget, - orientation: Side.white, - fen: kEmptyFEN, - showMoveListPlaceholder: true, - ), + loading: + () => const BoardTable( + topTable: kEmptyWidget, + bottomTable: kEmptyWidget, + orientation: Side.white, + fen: kEmptyFEN, + showMoveListPlaceholder: true, + ), error: (err, stackTrace) { - debugPrint( - 'SEVERE: [TvScreen] could not load stream; $err\n$stackTrace', - ); + debugPrint('SEVERE: [TvScreen] could not load stream; $err\n$stackTrace'); return const BoardTable( topTable: kEmptyWidget, bottomTable: kEmptyWidget, @@ -173,20 +163,14 @@ class _Body extends ConsumerWidget { ), ), ), - _BottomBar( - tvChannel: channel, - game: initialGame, - ), + _BottomBar(tvChannel: channel, game: initialGame), ], ); } } class _BottomBar extends ConsumerWidget { - const _BottomBar({ - required this.tvChannel, - required this.game, - }); + const _BottomBar({required this.tvChannel, required this.game}); final TvChannel tvChannel; final (GameId id, Side orientation)? game; @@ -200,42 +184,34 @@ class _BottomBar extends ConsumerWidget { icon: CupertinoIcons.arrow_2_squarepath, ), RepeatButton( - onLongPress: ref - .read(tvControllerProvider(tvChannel, game).notifier) - .canGoBack() - ? () => _moveBackward(ref) - : null, + onLongPress: + ref.read(tvControllerProvider(tvChannel, game).notifier).canGoBack() + ? () => _moveBackward(ref) + : null, child: BottomBarButton( key: const ValueKey('goto-previous'), - onTap: ref - .read( - tvControllerProvider(tvChannel, game).notifier, - ) - .canGoBack() - ? () => _moveBackward(ref) - : null, + onTap: + ref.read(tvControllerProvider(tvChannel, game).notifier).canGoBack() + ? () => _moveBackward(ref) + : null, label: 'Previous', icon: CupertinoIcons.chevron_back, showTooltip: false, ), ), RepeatButton( - onLongPress: ref - .read(tvControllerProvider(tvChannel, game).notifier) - .canGoForward() - ? () => _moveForward(ref) - : null, + onLongPress: + ref.read(tvControllerProvider(tvChannel, game).notifier).canGoForward() + ? () => _moveForward(ref) + : null, child: BottomBarButton( key: const ValueKey('goto-next'), icon: CupertinoIcons.chevron_forward, label: context.l10n.next, - onTap: ref - .read( - tvControllerProvider(tvChannel, game).notifier, - ) - .canGoForward() - ? () => _moveForward(ref) - : null, + onTap: + ref.read(tvControllerProvider(tvChannel, game).notifier).canGoForward() + ? () => _moveForward(ref) + : null, showTooltip: false, ), ), diff --git a/lib/src/view/watch/watch_tab_screen.dart b/lib/src/view/watch/watch_tab_screen.dart index 4ce43c81f3..45daf9673b 100644 --- a/lib/src/view/watch/watch_tab_screen.dart +++ b/lib/src/view/watch/watch_tab_screen.dart @@ -38,30 +38,26 @@ const _featuredChannelsSet = ISetConst({ TvChannel.rapid, }); -final featuredChannelsProvider = - FutureProvider.autoDispose>((ref) async { - return ref.withClientCacheFor( - (client) async { - final channels = await TvRepository(client).channels(); - return channels.entries - .where((channel) => _featuredChannelsSet.contains(channel.key)) - .map( - (entry) => TvGameSnapshot( - channel: entry.key, - id: entry.value.id, - orientation: entry.value.side ?? Side.white, - player: FeaturedPlayer( - name: entry.value.user.name, - title: entry.value.user.title, - side: entry.value.side ?? Side.white, - rating: entry.value.rating, - ), +final featuredChannelsProvider = FutureProvider.autoDispose>((ref) async { + return ref.withClientCacheFor((client) async { + final channels = await TvRepository(client).channels(); + return channels.entries + .where((channel) => _featuredChannelsSet.contains(channel.key)) + .map( + (entry) => TvGameSnapshot( + channel: entry.key, + id: entry.value.id, + orientation: entry.value.side ?? Side.white, + player: FeaturedPlayer( + name: entry.value.user.name, + title: entry.value.user.title, + side: entry.value.side ?? Side.white, + rating: entry.value.rating, ), - ) - .toIList(); - }, - const Duration(minutes: 5), - ); + ), + ) + .toIList(); + }, const Duration(minutes: 5)); }); class WatchTabScreen extends ConsumerStatefulWidget { @@ -82,11 +78,7 @@ class _WatchScreenState extends ConsumerState { } }); - return ConsumerPlatformWidget( - ref: ref, - androidBuilder: _buildAndroid, - iosBuilder: _buildIos, - ); + return ConsumerPlatformWidget(ref: ref, androidBuilder: _buildAndroid, iosBuilder: _buildIos); } Widget _buildAndroid(BuildContext context, WidgetRef ref) { @@ -98,9 +90,7 @@ class _WatchScreenState extends ConsumerState { } }, child: Scaffold( - appBar: AppBar( - title: Text(context.l10n.watch), - ), + appBar: AppBar(title: Text(context.l10n.watch)), body: OrientationBuilder( builder: (context, orientation) { return RefreshIndicator( @@ -122,18 +112,10 @@ class _WatchScreenState extends ConsumerState { controller: watchScrollController, slivers: [ const CupertinoSliverNavigationBar( - padding: EdgeInsetsDirectional.only( - start: 16.0, - end: 8.0, - ), - ), - CupertinoSliverRefreshControl( - onRefresh: refreshData, - ), - SliverSafeArea( - top: false, - sliver: _Body(orientation), + padding: EdgeInsetsDirectional.only(start: 16.0, end: 8.0), ), + CupertinoSliverRefreshControl(onRefresh: refreshData), + SliverSafeArea(top: false, sliver: _Body(orientation)), ], ); }, @@ -193,31 +175,27 @@ class _BodyState extends ConsumerState<_Body> { final featuredChannels = ref.watch(featuredChannelsProvider); final streamers = ref.watch(liveStreamersProvider); - final content = widget.orientation == Orientation.portrait - ? [ - _BroadcastWidget(broadcastList), - _WatchTvWidget(featuredChannels), - _StreamerWidget(streamers), - ] - : [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded(child: _BroadcastWidget(broadcastList)), - Expanded(child: _WatchTvWidget(featuredChannels)), - ], - ), - _StreamerWidget(streamers), - ]; + final content = + widget.orientation == Orientation.portrait + ? [ + _BroadcastWidget(broadcastList), + _WatchTvWidget(featuredChannels), + _StreamerWidget(streamers), + ] + : [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(child: _BroadcastWidget(broadcastList)), + Expanded(child: _WatchTvWidget(featuredChannels)), + ], + ), + _StreamerWidget(streamers), + ]; return Theme.of(context).platform == TargetPlatform.iOS - ? SliverList( - delegate: SliverChildListDelegate(content), - ) - : ListView( - controller: watchScrollController, - children: content, - ); + ? SliverList(delegate: SliverChildListDelegate(content)) + : ListView(controller: watchScrollController, children: content); } } @@ -244,48 +222,38 @@ class _BroadcastWidget extends ConsumerWidget { header: Text(context.l10n.broadcastBroadcasts), headerTrailing: NoPaddingTextButton( onPressed: () { - pushPlatformRoute( - context, - builder: (context) => const BroadcastListScreen(), - ); + pushPlatformRoute(context, builder: (context) => const BroadcastListScreen()); }, - child: Text( - context.l10n.more, - ), + child: Text(context.l10n.more), ), children: [ - ...CombinedIterableView([data.active, data.past]) - .take(numberOfItems) - .map((broadcast) => _BroadcastTile(broadcast: broadcast)), + ...CombinedIterableView([ + data.active, + data.past, + ]).take(numberOfItems).map((broadcast) => _BroadcastTile(broadcast: broadcast)), ], ); }, error: (error, stackTrace) { - debugPrint( - 'SEVERE: [BroadcastWidget] could not load broadcast data; $error\n $stackTrace', - ); + debugPrint('SEVERE: [BroadcastWidget] could not load broadcast data; $error\n $stackTrace'); return const Padding( padding: Styles.bodySectionPadding, child: Text('Could not load broadcasts'), ); }, - loading: () => Shimmer( - child: ShimmerLoading( - isLoading: true, - child: ListSection.loading( - itemsNumber: numberOfItems, - header: true, + loading: + () => Shimmer( + child: ShimmerLoading( + isLoading: true, + child: ListSection.loading(itemsNumber: numberOfItems, header: true), + ), ), - ), - ), ); } } class _BroadcastTile extends ConsumerWidget { - const _BroadcastTile({ - required this.broadcast, - }); + const _BroadcastTile({required this.broadcast}); final Broadcast broadcast; @@ -308,10 +276,7 @@ class _BroadcastTile extends ConsumerWidget { const SizedBox(width: 5.0), Text( 'LIVE', - style: TextStyle( - color: context.lichessColors.error, - fontWeight: FontWeight.bold, - ), + style: TextStyle(color: context.lichessColors.error, fontWeight: FontWeight.bold), ), ] else if (broadcast.round.startsAt != null) ...[ const SizedBox(width: 5.0), @@ -321,11 +286,7 @@ class _BroadcastTile extends ConsumerWidget { ), title: Padding( padding: const EdgeInsets.only(right: 5.0), - child: Text( - broadcast.title, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), + child: Text(broadcast.title, maxLines: 1, overflow: TextOverflow.ellipsis), ), ); } @@ -347,50 +308,48 @@ class _WatchTvWidget extends ConsumerWidget { header: const Text('Lichess TV'), hasLeading: true, headerTrailing: NoPaddingTextButton( - onPressed: () => pushPlatformRoute( - context, - builder: (context) => const LiveTvChannelsScreen(), - ).then((_) => _refreshData(ref)), - child: Text( - context.l10n.more, - ), + onPressed: + () => pushPlatformRoute( + context, + builder: (context) => const LiveTvChannelsScreen(), + ).then((_) => _refreshData(ref)), + child: Text(context.l10n.more), ), - children: data.map((snapshot) { - return PlatformListTile( - leading: Icon(snapshot.channel.icon), - title: Text(snapshot.channel.label), - subtitle: UserFullNameWidget.player( - user: snapshot.player.asPlayer.user, - aiLevel: snapshot.player.asPlayer.aiLevel, - rating: snapshot.player.rating, - ), - onTap: () => pushPlatformRoute( - context, - rootNavigator: true, - builder: (context) => TvScreen(channel: snapshot.channel), - ).then((_) => _refreshData(ref)), - ); - }).toList(growable: false), + children: data + .map((snapshot) { + return PlatformListTile( + leading: Icon(snapshot.channel.icon), + title: Text(snapshot.channel.label), + subtitle: UserFullNameWidget.player( + user: snapshot.player.asPlayer.user, + aiLevel: snapshot.player.asPlayer.aiLevel, + rating: snapshot.player.rating, + ), + onTap: + () => pushPlatformRoute( + context, + rootNavigator: true, + builder: (context) => TvScreen(channel: snapshot.channel), + ).then((_) => _refreshData(ref)), + ); + }) + .toList(growable: false), ); }, error: (error, stackTrace) { - debugPrint( - 'SEVERE: [StreamerWidget] could not load channels data; $error\n $stackTrace', - ); + debugPrint('SEVERE: [StreamerWidget] could not load channels data; $error\n $stackTrace'); return const Padding( padding: Styles.bodySectionPadding, child: Text('Could not load TV channels'), ); }, - loading: () => Shimmer( - child: ShimmerLoading( - isLoading: true, - child: ListSection.loading( - itemsNumber: 4, - header: true, + loading: + () => Shimmer( + child: ShimmerLoading( + isLoading: true, + child: ListSection.loading(itemsNumber: 4, header: true), + ), ), - ), - ), ); } } @@ -413,13 +372,12 @@ class _StreamerWidget extends ConsumerWidget { header: Text(context.l10n.streamerLichessStreamers), hasLeading: true, headerTrailing: NoPaddingTextButton( - onPressed: () => pushPlatformRoute( - context, - builder: (context) => StreamerScreen(streamers: data), - ), - child: Text( - context.l10n.more, - ), + onPressed: + () => pushPlatformRoute( + context, + builder: (context) => StreamerScreen(streamers: data), + ), + child: Text(context.l10n.more), ), children: [ ...data @@ -429,23 +387,19 @@ class _StreamerWidget extends ConsumerWidget { ); }, error: (error, stackTrace) { - debugPrint( - 'SEVERE: [StreamerWidget] could not load streamer data; $error\n $stackTrace', - ); + debugPrint('SEVERE: [StreamerWidget] could not load streamer data; $error\n $stackTrace'); return const Padding( padding: Styles.bodySectionPadding, child: Text('Could not load live streamers'), ); }, - loading: () => Shimmer( - child: ShimmerLoading( - isLoading: true, - child: ListSection.loading( - itemsNumber: numberOfItems, - header: true, + loading: + () => Shimmer( + child: ShimmerLoading( + isLoading: true, + child: ListSection.loading(itemsNumber: numberOfItems, header: true), + ), ), - ), - ), ); } } diff --git a/lib/src/widgets/adaptive_action_sheet.dart b/lib/src/widgets/adaptive_action_sheet.dart index 50ad66219b..c7187a47d5 100644 --- a/lib/src/widgets/adaptive_action_sheet.dart +++ b/lib/src/widgets/adaptive_action_sheet.dart @@ -64,18 +64,14 @@ Future showConfirmDialog( title: title, actions: [ TextButton( - style: TextButton.styleFrom( - textStyle: Theme.of(context).textTheme.labelLarge, - ), + style: TextButton.styleFrom(textStyle: Theme.of(context).textTheme.labelLarge), child: Text(context.l10n.cancel), onPressed: () { Navigator.of(context).pop(); }, ), TextButton( - style: TextButton.styleFrom( - textStyle: Theme.of(context).textTheme.labelLarge, - ), + style: TextButton.styleFrom(textStyle: Theme.of(context).textTheme.labelLarge), child: Text(context.l10n.mobileOkButton), onPressed: () { Navigator.of(context).pop(); @@ -101,28 +97,29 @@ Future showCupertinoActionSheet({ builder: (BuildContext context) { return CupertinoActionSheet( title: title, - actions: actions - .map( - // Builder is used to retrieve the context immediately surrounding the button - // This is necessary to get the correct context for the iPad share dialog - // which needs the position of the action to display the share dialog - (action) => Builder( - builder: (context) { - return CupertinoActionSheetAction( - onPressed: () { - if (action.dismissOnPress) { - Navigator.of(context).pop(); - } - action.onPressed(context); + actions: + actions + .map( + // Builder is used to retrieve the context immediately surrounding the button + // This is necessary to get the correct context for the iPad share dialog + // which needs the position of the action to display the share dialog + (action) => Builder( + builder: (context) { + return CupertinoActionSheetAction( + onPressed: () { + if (action.dismissOnPress) { + Navigator.of(context).pop(); + } + action.onPressed(context); + }, + isDestructiveAction: action.isDestructiveAction, + isDefaultAction: action.isDefaultAction, + child: action.makeLabel(context), + ); }, - isDestructiveAction: action.isDestructiveAction, - isDefaultAction: action.isDefaultAction, - child: action.makeLabel(context), - ); - }, - ), - ) - .toList(), + ), + ) + .toList(), cancelButton: CupertinoActionSheetAction( isDefaultAction: true, onPressed: () { @@ -141,8 +138,7 @@ Future showMaterialActionSheet({ required List actions, bool isDismissible = true, }) { - final actionTextStyle = - Theme.of(context).textTheme.titleMedium ?? const TextStyle(fontSize: 18); + final actionTextStyle = Theme.of(context).textTheme.titleMedium ?? const TextStyle(fontSize: 18); final screenWidth = MediaQuery.of(context).size.width; return showDialog( @@ -158,20 +154,13 @@ Future showMaterialActionSheet({ mainAxisSize: MainAxisSize.min, children: [ if (title != null) ...[ - Padding( - padding: const EdgeInsets.all(16.0), - child: Center(child: title), - ), + Padding(padding: const EdgeInsets.all(16.0), child: Center(child: title)), ], ...actions.mapIndexed((index, action) { return InkWell( borderRadius: BorderRadius.vertical( - top: Radius.circular( - index == 0 ? 28 : 0, - ), - bottom: Radius.circular( - index == actions.length - 1 ? 28 : 0, - ), + top: Radius.circular(index == 0 ? 28 : 0), + bottom: Radius.circular(index == actions.length - 1 ? 28 : 0), ), onTap: () { if (action.dismissOnPress) { @@ -190,9 +179,8 @@ Future showMaterialActionSheet({ Expanded( child: DefaultTextStyle( style: actionTextStyle, - textAlign: action.leading != null - ? TextAlign.start - : TextAlign.center, + textAlign: + action.leading != null ? TextAlign.start : TextAlign.center, child: action.makeLabel(context), ), ), diff --git a/lib/src/widgets/adaptive_autocomplete.dart b/lib/src/widgets/adaptive_autocomplete.dart index a561aad408..20fc79a014 100644 --- a/lib/src/widgets/adaptive_autocomplete.dart +++ b/lib/src/widgets/adaptive_autocomplete.dart @@ -25,77 +25,74 @@ class AdaptiveAutoComplete extends StatelessWidget { Widget build(BuildContext context) { return Theme.of(context).platform == TargetPlatform.iOS ? RawAutocomplete( - initialValue: initialValue, - optionsBuilder: optionsBuilder, - fieldViewBuilder: ( - BuildContext context, - TextEditingController textEditingController, - FocusNode focusNode, - VoidCallback onFieldSubmitted, - ) { - return CupertinoTextField( - controller: textEditingController, - decoration: cupertinoDecoration, - textInputAction: textInputAction, - focusNode: focusNode, - onSubmitted: (String value) { - onFieldSubmitted(); - }, - ); - }, - optionsViewBuilder: ( - BuildContext context, - AutocompleteOnSelected onSelected, - Iterable options, - ) { - return Align( - alignment: Alignment.topLeft, - child: ColoredBox( - color: CupertinoColors.secondarySystemGroupedBackground - .resolveFrom(context), - child: SizedBox( - height: 200.0, - child: ListView.builder( - padding: EdgeInsets.zero, - itemCount: options.length, - itemBuilder: (BuildContext context, int index) { - final T option = options.elementAt(index); - return AdaptiveInkWell( - onTap: () { - onSelected(option); - }, - child: ListTile( - title: Text(displayStringForOption(option)), - ), - ); - }, - ), + initialValue: initialValue, + optionsBuilder: optionsBuilder, + fieldViewBuilder: ( + BuildContext context, + TextEditingController textEditingController, + FocusNode focusNode, + VoidCallback onFieldSubmitted, + ) { + return CupertinoTextField( + controller: textEditingController, + decoration: cupertinoDecoration, + textInputAction: textInputAction, + focusNode: focusNode, + onSubmitted: (String value) { + onFieldSubmitted(); + }, + ); + }, + optionsViewBuilder: ( + BuildContext context, + AutocompleteOnSelected onSelected, + Iterable options, + ) { + return Align( + alignment: Alignment.topLeft, + child: ColoredBox( + color: CupertinoColors.secondarySystemGroupedBackground.resolveFrom(context), + child: SizedBox( + height: 200.0, + child: ListView.builder( + padding: EdgeInsets.zero, + itemCount: options.length, + itemBuilder: (BuildContext context, int index) { + final T option = options.elementAt(index); + return AdaptiveInkWell( + onTap: () { + onSelected(option); + }, + child: ListTile(title: Text(displayStringForOption(option))), + ); + }, ), ), - ); - }, - onSelected: onSelected, - ) + ), + ); + }, + onSelected: onSelected, + ) : Autocomplete( - initialValue: initialValue, - optionsBuilder: optionsBuilder, - onSelected: onSelected, - displayStringForOption: displayStringForOption, - fieldViewBuilder: ( - BuildContext context, - TextEditingController textEditingController, - FocusNode focusNode, - VoidCallback onFieldSubmitted, - ) { - return TextField( - controller: textEditingController, - textInputAction: textInputAction, - focusNode: focusNode, - onSubmitted: (String value) { - onFieldSubmitted(); - }, - ); - }, - ); + initialValue: initialValue, + optionsBuilder: optionsBuilder, + onSelected: onSelected, + displayStringForOption: displayStringForOption, + fieldViewBuilder: ( + BuildContext context, + TextEditingController textEditingController, + FocusNode focusNode, + VoidCallback onFieldSubmitted, + ) { + return TextField( + controller: textEditingController, + textInputAction: textInputAction, + focusNode: focusNode, + onSubmitted: (String value) { + onFieldSubmitted(); + }, + ); + }, + ); } } diff --git a/lib/src/widgets/adaptive_bottom_sheet.dart b/lib/src/widgets/adaptive_bottom_sheet.dart index 6404c44dd3..16e964563a 100644 --- a/lib/src/widgets/adaptive_bottom_sheet.dart +++ b/lib/src/widgets/adaptive_bottom_sheet.dart @@ -21,20 +21,20 @@ Future showAdaptiveBottomSheet({ isScrollControlled: isScrollControlled, useRootNavigator: useRootNavigator, useSafeArea: useSafeArea, - shape: Theme.of(context).platform == TargetPlatform.iOS - ? const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(10.0), - ), - ) - : null, + shape: + Theme.of(context).platform == TargetPlatform.iOS + ? const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(10.0)), + ) + : null, constraints: constraints, - backgroundColor: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoDynamicColor.resolve( - CupertinoColors.tertiarySystemGroupedBackground, - context, - ) - : null, + backgroundColor: + Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoDynamicColor.resolve( + CupertinoColors.tertiarySystemGroupedBackground, + context, + ) + : null, elevation: Theme.of(context).platform == TargetPlatform.iOS ? 0 : null, builder: builder, ); @@ -63,9 +63,7 @@ class BottomSheetScrollableContainer extends StatelessWidget { child: SingleChildScrollView( controller: scrollController, padding: padding, - child: ListBody( - children: children, - ), + child: ListBody(children: children), ), ); } @@ -87,8 +85,9 @@ class BottomSheetContextMenuAction extends StatelessWidget { @override Widget build(BuildContext context) { return PlatformListTile( - cupertinoBackgroundColor: - CupertinoColors.tertiarySystemGroupedBackground.resolveFrom(context), + cupertinoBackgroundColor: CupertinoColors.tertiarySystemGroupedBackground.resolveFrom( + context, + ), leading: Icon(icon), title: child, onTap: () { diff --git a/lib/src/widgets/adaptive_choice_picker.dart b/lib/src/widgets/adaptive_choice_picker.dart index 933ceaeff8..89a65fa1ad 100644 --- a/lib/src/widgets/adaptive_choice_picker.dart +++ b/lib/src/widgets/adaptive_choice_picker.dart @@ -25,32 +25,28 @@ Future showChoicePicker( scrollable: true, content: Builder( builder: (context) { - final List choiceWidgets = choices.map((value) { - return RadioListTile( - title: labelBuilder(value), - value: value, - groupValue: selectedItem, - onChanged: (value) { - if (value != null && onSelectedItemChanged != null) { - onSelectedItemChanged(value); - Navigator.of(context).pop(); - } - }, - ); - }).toList(growable: false); + final List choiceWidgets = choices + .map((value) { + return RadioListTile( + title: labelBuilder(value), + value: value, + groupValue: selectedItem, + onChanged: (value) { + if (value != null && onSelectedItemChanged != null) { + onSelectedItemChanged(value); + Navigator.of(context).pop(); + } + }, + ); + }) + .toList(growable: false); return choiceWidgets.length >= 10 ? SizedBox( - width: double.maxFinite, - height: deviceHeight * 0.6, - child: ListView( - shrinkWrap: true, - children: choiceWidgets, - ), - ) - : Column( - mainAxisSize: MainAxisSize.min, - children: choiceWidgets, - ); + width: double.maxFinite, + height: deviceHeight * 0.6, + child: ListView(shrinkWrap: true, children: choiceWidgets), + ) + : Column(mainAxisSize: MainAxisSize.min, children: choiceWidgets); }, ), actions: [ @@ -68,17 +64,18 @@ Future showChoicePicker( context: context, builder: (context) { return CupertinoActionSheet( - actions: choices.map((value) { - return CupertinoActionSheetAction( - onPressed: () { - if (onSelectedItemChanged != null) { - onSelectedItemChanged(value); - } - Navigator.of(context).pop(); - }, - child: labelBuilder(value), - ); - }).toList(), + actions: + choices.map((value) { + return CupertinoActionSheetAction( + onPressed: () { + if (onSelectedItemChanged != null) { + onSelectedItemChanged(value); + } + Navigator.of(context).pop(); + }, + child: labelBuilder(value), + ); + }).toList(), cancelButton: CupertinoActionSheetAction( isDefaultAction: true, onPressed: () => Navigator.of(context).pop(), @@ -94,8 +91,7 @@ Future showChoicePicker( return NotificationListener( onNotification: (ScrollEndNotification notification) { if (onSelectedItemChanged != null) { - final index = - (notification.metrics as FixedExtentMetrics).itemIndex; + final index = (notification.metrics as FixedExtentMetrics).itemIndex; onSelectedItemChanged(choices[index]); } return false; @@ -110,11 +106,10 @@ Future showChoicePicker( scrollController: FixedExtentScrollController( initialItem: choices.indexWhere((t) => t == selectedItem), ), - children: choices.map((value) { - return Center( - child: labelBuilder(value), - ); - }).toList(), + children: + choices.map((value) { + return Center(child: labelBuilder(value)); + }).toList(), onSelectedItemChanged: (_) {}, ), ), @@ -144,46 +139,47 @@ Future?> showMultipleChoicesPicker( builder: (BuildContext context, StateSetter setState) { return Column( mainAxisSize: MainAxisSize.min, - children: choices.map((choice) { - return CheckboxListTile.adaptive( - title: labelBuilder(choice), - value: items.contains(choice), - onChanged: (value) { - if (value != null) { - setState(() { - items = value - ? items.union({choice}) - : items.difference({choice}); - }); - } - }, - ); - }).toList(growable: false), + children: choices + .map((choice) { + return CheckboxListTile.adaptive( + title: labelBuilder(choice), + value: items.contains(choice), + onChanged: (value) { + if (value != null) { + setState(() { + items = value ? items.union({choice}) : items.difference({choice}); + }); + } + }, + ); + }) + .toList(growable: false), ); }, ), - actions: Theme.of(context).platform == TargetPlatform.iOS - ? [ - CupertinoDialogAction( - onPressed: () => Navigator.of(context).pop(), - child: Text(context.l10n.cancel), - ), - CupertinoDialogAction( - isDefaultAction: true, - child: Text(context.l10n.mobileOkButton), - onPressed: () => Navigator.of(context).pop(items), - ), - ] - : [ - TextButton( - child: Text(context.l10n.cancel), - onPressed: () => Navigator.of(context).pop(), - ), - TextButton( - child: Text(context.l10n.mobileOkButton), - onPressed: () => Navigator.of(context).pop(items), - ), - ], + actions: + Theme.of(context).platform == TargetPlatform.iOS + ? [ + CupertinoDialogAction( + onPressed: () => Navigator.of(context).pop(), + child: Text(context.l10n.cancel), + ), + CupertinoDialogAction( + isDefaultAction: true, + child: Text(context.l10n.mobileOkButton), + onPressed: () => Navigator.of(context).pop(items), + ), + ] + : [ + TextButton( + child: Text(context.l10n.cancel), + onPressed: () => Navigator.of(context).pop(), + ), + TextButton( + child: Text(context.l10n.mobileOkButton), + onPressed: () => Navigator.of(context).pop(items), + ), + ], ); }, ); diff --git a/lib/src/widgets/adaptive_date_picker.dart b/lib/src/widgets/adaptive_date_picker.dart index 0ab55691f7..1bbc828592 100644 --- a/lib/src/widgets/adaptive_date_picker.dart +++ b/lib/src/widgets/adaptive_date_picker.dart @@ -18,8 +18,7 @@ void showAdaptiveDatePicker( return SizedBox( height: 250, child: CupertinoDatePicker( - backgroundColor: - CupertinoColors.systemBackground.resolveFrom(context), + backgroundColor: CupertinoColors.systemBackground.resolveFrom(context), initialDateTime: initialDate, minimumDate: firstDate, maximumDate: lastDate, diff --git a/lib/src/widgets/adaptive_text_field.dart b/lib/src/widgets/adaptive_text_field.dart index 3e77a84155..483f814dea 100644 --- a/lib/src/widgets/adaptive_text_field.dart +++ b/lib/src/widgets/adaptive_text_field.dart @@ -40,8 +40,7 @@ class AdaptiveTextField extends StatelessWidget { final void Function(String)? onChanged; final void Function(String)? onSubmitted; final GestureTapCallback? onTap; - final Widget? - suffix; //used only for iOS, suffix should be put in InputDecoration for android + final Widget? suffix; //used only for iOS, suffix should be put in InputDecoration for android final BoxDecoration? cupertinoDecoration; final InputDecoration? materialDecoration; @@ -75,10 +74,7 @@ class AdaptiveTextField extends StatelessWidget { maxLines: maxLines, maxLength: maxLength, expands: expands, - decoration: materialDecoration ?? - InputDecoration( - hintText: placeholder, - ), + decoration: materialDecoration ?? InputDecoration(hintText: placeholder), controller: controller, focusNode: focusNode, textInputAction: textInputAction, diff --git a/lib/src/widgets/board_carousel_item.dart b/lib/src/widgets/board_carousel_item.dart index 16d2896753..a6e12f65c1 100644 --- a/lib/src/widgets/board_carousel_item.dart +++ b/lib/src/widgets/board_carousel_item.dart @@ -9,8 +9,7 @@ import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; -const _kBoardCarouselItemMargin = - EdgeInsets.symmetric(vertical: 8.0, horizontal: 6.0); +const _kBoardCarouselItemMargin = EdgeInsets.symmetric(vertical: 8.0, horizontal: 6.0); class BoardCarouselItem extends ConsumerWidget { const BoardCarouselItem({ @@ -51,13 +50,13 @@ class BoardCarouselItem extends ConsumerWidget { return LayoutBuilder( builder: (context, constraints) { - final boardSize = constraints.biggest.shortestSide - - _kBoardCarouselItemMargin.horizontal; + final boardSize = constraints.biggest.shortestSide - _kBoardCarouselItemMargin.horizontal; final card = PlatformCard( color: backgroundColor, - margin: Theme.of(context).platform == TargetPlatform.iOS - ? EdgeInsets.zero - : _kBoardCarouselItemMargin, + margin: + Theme.of(context).platform == TargetPlatform.iOS + ? EdgeInsets.zero + : _kBoardCarouselItemMargin, child: AdaptiveInkWell( splashColor: splashColor, borderRadius: BorderRadius.circular(10), @@ -101,9 +100,7 @@ class BoardCarouselItem extends ConsumerWidget { left: 0, bottom: 8, child: DefaultTextStyle.merge( - style: const TextStyle( - color: Colors.white, - ), + style: const TextStyle(color: Colors.white), child: description, ), ), @@ -114,20 +111,17 @@ class BoardCarouselItem extends ConsumerWidget { return Theme.of(context).platform == TargetPlatform.iOS ? Padding( - padding: _kBoardCarouselItemMargin, - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8.0), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.05), - blurRadius: 6.0, - ), - ], - ), - child: card, + padding: _kBoardCarouselItemMargin, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8.0), + boxShadow: [ + BoxShadow(color: Colors.black.withValues(alpha: 0.05), blurRadius: 6.0), + ], ), - ) + child: card, + ), + ) : card; }, ); @@ -178,10 +172,10 @@ class BoardsPageView extends StatefulWidget { this.scrollBehavior, this.padding = EdgeInsets.zero, }) : childrenDelegate = SliverChildBuilderDelegate( - itemBuilder, - findChildIndexCallback: findChildIndexCallback, - childCount: itemCount, - ); + itemBuilder, + findChildIndexCallback: findChildIndexCallback, + childCount: itemCount, + ); final bool allowImplicitScrolling; final String? restorationId; @@ -241,11 +235,8 @@ class _BoardsPageViewState extends State { case Axis.horizontal: assert(debugCheckHasDirectionality(context)); final TextDirection textDirection = Directionality.of(context); - final AxisDirection axisDirection = - textDirectionToAxisDirection(textDirection); - return widget.reverse - ? flipAxisDirection(axisDirection) - : axisDirection; + final AxisDirection axisDirection = textDirectionToAxisDirection(textDirection); + return widget.reverse ? flipAxisDirection(axisDirection) : axisDirection; case Axis.vertical: return widget.reverse ? AxisDirection.up : AxisDirection.down; } @@ -259,9 +250,8 @@ class _BoardsPageViewState extends State { ).applyTo( widget.pageSnapping ? _kPagePhysics.applyTo( - widget.physics ?? - widget.scrollBehavior?.getScrollPhysics(context), - ) + widget.physics ?? widget.scrollBehavior?.getScrollPhysics(context), + ) : widget.physics ?? widget.scrollBehavior?.getScrollPhysics(context), ); @@ -285,8 +275,8 @@ class _BoardsPageViewState extends State { controller: _controller, physics: physics, restorationId: widget.restorationId, - scrollBehavior: widget.scrollBehavior ?? - ScrollConfiguration.of(context).copyWith(scrollbars: false), + scrollBehavior: + widget.scrollBehavior ?? ScrollConfiguration.of(context).copyWith(scrollbars: false), viewportBuilder: (BuildContext context, ViewportOffset position) { return Viewport( cacheExtent: 0, @@ -309,11 +299,8 @@ class _BoardsPageViewState extends State { } class _SliverFillViewport extends StatelessWidget { - const _SliverFillViewport({ - required this.delegate, - this.viewportFraction = 1.0, - this.padding, - }) : assert(viewportFraction > 0.0); + const _SliverFillViewport({required this.delegate, this.viewportFraction = 1.0, this.padding}) + : assert(viewportFraction > 0.0); final double viewportFraction; @@ -333,8 +320,7 @@ class _SliverFillViewport extends StatelessWidget { } } -class _SliverFillViewportRenderObjectWidget - extends SliverMultiBoxAdaptorWidget { +class _SliverFillViewportRenderObjectWidget extends SliverMultiBoxAdaptorWidget { const _SliverFillViewportRenderObjectWidget({ required super.delegate, this.viewportFraction = 1.0, @@ -344,28 +330,18 @@ class _SliverFillViewportRenderObjectWidget @override RenderSliverFillViewport createRenderObject(BuildContext context) { - final SliverMultiBoxAdaptorElement element = - context as SliverMultiBoxAdaptorElement; - return RenderSliverFillViewport( - childManager: element, - viewportFraction: viewportFraction, - ); + final SliverMultiBoxAdaptorElement element = context as SliverMultiBoxAdaptorElement; + return RenderSliverFillViewport(childManager: element, viewportFraction: viewportFraction); } @override - void updateRenderObject( - BuildContext context, - RenderSliverFillViewport renderObject, - ) { + void updateRenderObject(BuildContext context, RenderSliverFillViewport renderObject) { renderObject.viewportFraction = viewportFraction; } } class _ForceImplicitScrollPhysics extends ScrollPhysics { - const _ForceImplicitScrollPhysics({ - required this.allowImplicitScrolling, - super.parent, - }); + const _ForceImplicitScrollPhysics({required this.allowImplicitScrolling, super.parent}); @override _ForceImplicitScrollPhysics applyTo(ScrollPhysics? ancestor) { diff --git a/lib/src/widgets/board_preview.dart b/lib/src/widgets/board_preview.dart index c52b9e22b6..531bc0a6d9 100644 --- a/lib/src/widgets/board_preview.dart +++ b/lib/src/widgets/board_preview.dart @@ -18,14 +18,13 @@ class SmallBoardPreview extends ConsumerStatefulWidget { this.onTap, }) : _showLoadingPlaceholder = false; - const SmallBoardPreview.loading({ - this.padding, - }) : orientation = Side.white, - fen = kEmptyFEN, - lastMove = null, - description = const SizedBox.shrink(), - onTap = null, - _showLoadingPlaceholder = true; + const SmallBoardPreview.loading({this.padding}) + : orientation = Side.white, + fen = kEmptyFEN, + lastMove = null, + description = const SizedBox.shrink(), + onTap = null, + _showLoadingPlaceholder = true; /// Side by which the board is oriented. final Side orientation; @@ -57,21 +56,19 @@ class _SmallBoardPreviewState extends ConsumerState { final content = LayoutBuilder( builder: (context, constraints) { - final boardSize = constraints.biggest.shortestSide - - (constraints.biggest.shortestSide / 1.618); + final boardSize = + constraints.biggest.shortestSide - (constraints.biggest.shortestSide / 1.618); return Container( decoration: BoxDecoration( - color: _isPressed - ? CupertinoDynamicColor.resolve( - CupertinoColors.systemGrey5, - context, - ) - : null, + color: + _isPressed + ? CupertinoDynamicColor.resolve(CupertinoColors.systemGrey5, context) + : null, ), child: Padding( - padding: widget.padding ?? - Styles.horizontalBodyPadding - .add(const EdgeInsets.symmetric(vertical: 8.0)), + padding: + widget.padding ?? + Styles.horizontalBodyPadding.add(const EdgeInsets.symmetric(vertical: 8.0)), child: SizedBox( height: boardSize, child: Row( @@ -93,8 +90,7 @@ class _SmallBoardPreviewState extends ConsumerState { lastMove: widget.lastMove as NormalMove?, settings: ChessboardSettings( enableCoordinates: false, - borderRadius: - const BorderRadius.all(Radius.circular(4.0)), + borderRadius: const BorderRadius.all(Radius.circular(4.0)), boxShadow: boardShadows, animationDuration: const Duration(milliseconds: 150), pieceAssets: boardPrefs.pieceSet.assets, @@ -116,8 +112,7 @@ class _SmallBoardPreviewState extends ConsumerState { width: double.infinity, decoration: const BoxDecoration( color: Colors.black, - borderRadius: - BorderRadius.all(Radius.circular(4.0)), + borderRadius: BorderRadius.all(Radius.circular(4.0)), ), ), const SizedBox(height: 4.0), @@ -126,8 +121,7 @@ class _SmallBoardPreviewState extends ConsumerState { width: MediaQuery.sizeOf(context).width / 3, decoration: const BoxDecoration( color: Colors.black, - borderRadius: - BorderRadius.all(Radius.circular(4.0)), + borderRadius: BorderRadius.all(Radius.circular(4.0)), ), ), ], @@ -137,8 +131,7 @@ class _SmallBoardPreviewState extends ConsumerState { width: 44.0, decoration: const BoxDecoration( color: Colors.black, - borderRadius: - BorderRadius.all(Radius.circular(4.0)), + borderRadius: BorderRadius.all(Radius.circular(4.0)), ), ), Container( @@ -146,8 +139,7 @@ class _SmallBoardPreviewState extends ConsumerState { width: double.infinity, decoration: const BoxDecoration( color: Colors.black, - borderRadius: - BorderRadius.all(Radius.circular(4.0)), + borderRadius: BorderRadius.all(Radius.circular(4.0)), ), ), ], @@ -166,16 +158,13 @@ class _SmallBoardPreviewState extends ConsumerState { return widget.onTap != null ? Theme.of(context).platform == TargetPlatform.iOS ? GestureDetector( - onTapDown: (_) => setState(() => _isPressed = true), - onTapUp: (_) => setState(() => _isPressed = false), - onTapCancel: () => setState(() => _isPressed = false), - onTap: widget.onTap, - child: content, - ) - : InkWell( - onTap: widget.onTap, - child: content, - ) + onTapDown: (_) => setState(() => _isPressed = true), + onTapUp: (_) => setState(() => _isPressed = false), + onTapCancel: () => setState(() => _isPressed = false), + onTap: widget.onTap, + child: content, + ) + : InkWell(onTap: widget.onTap, child: content) : content; } } diff --git a/lib/src/widgets/board_table.dart b/lib/src/widgets/board_table.dart index 375961ee68..a9d8f06f02 100644 --- a/lib/src/widgets/board_table.dart +++ b/lib/src/widgets/board_table.dart @@ -44,30 +44,30 @@ class BoardTable extends ConsumerStatefulWidget { this.zenMode = false, super.key, }) : assert( - moves == null || currentMoveIndex != null, - 'You must provide `currentMoveIndex` along with `moves`', - ); + moves == null || currentMoveIndex != null, + 'You must provide `currentMoveIndex` along with `moves`', + ); /// Creates an empty board table (useful for loading). const BoardTable.empty({ this.showMoveListPlaceholder = false, this.showEngineGaugePlaceholder = false, this.errorMessage, - }) : fen = kEmptyBoardFEN, - orientation = Side.white, - gameData = null, - lastMove = null, - boardSettingsOverrides = null, - topTable = const SizedBox.shrink(), - bottomTable = const SizedBox.shrink(), - shapes = null, - engineGauge = null, - moves = null, - currentMoveIndex = null, - onSelectMove = null, - boardOverlay = null, - boardKey = null, - zenMode = false; + }) : fen = kEmptyBoardFEN, + orientation = Side.white, + gameData = null, + lastMove = null, + boardSettingsOverrides = null, + topTable = const SizedBox.shrink(), + bottomTable = const SizedBox.shrink(), + shapes = null, + engineGauge = null, + moves = null, + currentMoveIndex = null, + onSelectMove = null, + boardOverlay = null, + boardKey = null, + zenMode = false; final String fen; @@ -132,39 +132,40 @@ class _BoardTableState extends ConsumerState { return LayoutBuilder( builder: (context, constraints) { - final orientation = constraints.maxWidth > constraints.maxHeight - ? Orientation.landscape - : Orientation.portrait; + final orientation = + constraints.maxWidth > constraints.maxHeight + ? Orientation.landscape + : Orientation.portrait; final isTablet = isTabletOrLarger(context); final defaultSettings = boardPrefs.toBoardSettings().copyWith( - borderRadius: isTablet - ? const BorderRadius.all(Radius.circular(4.0)) - : BorderRadius.zero, - boxShadow: isTablet ? boardShadows : const [], - drawShape: DrawShapeOptions( - enable: boardPrefs.enableShapeDrawings, - onCompleteShape: _onCompleteShape, - onClearShapes: _onClearShapes, - newShapeColor: boardPrefs.shapeColor.color, - ), - ); + borderRadius: isTablet ? const BorderRadius.all(Radius.circular(4.0)) : BorderRadius.zero, + boxShadow: isTablet ? boardShadows : const [], + drawShape: DrawShapeOptions( + enable: boardPrefs.enableShapeDrawings, + onCompleteShape: _onCompleteShape, + onClearShapes: _onClearShapes, + newShapeColor: boardPrefs.shapeColor.color, + ), + ); - final settings = widget.boardSettingsOverrides != null - ? widget.boardSettingsOverrides!.merge(defaultSettings) - : defaultSettings; + final settings = + widget.boardSettingsOverrides != null + ? widget.boardSettingsOverrides!.merge(defaultSettings) + : defaultSettings; final shapes = userShapes.union(widget.shapes ?? ISet()); final slicedMoves = widget.moves?.asMap().entries.slices(2); if (orientation == Orientation.landscape) { - final defaultBoardSize = constraints.biggest.shortestSide - - (kTabletBoardTableSidePadding * 2); + final defaultBoardSize = + constraints.biggest.shortestSide - (kTabletBoardTableSidePadding * 2); final sideWidth = constraints.biggest.longestSide - defaultBoardSize; - final boardSize = sideWidth >= 250 - ? defaultBoardSize - : constraints.biggest.longestSide / kGoldenRatio - - (kTabletBoardTableSidePadding * 2); + final boardSize = + sideWidth >= 250 + ? defaultBoardSize + : constraints.biggest.longestSide / kGoldenRatio - + (kTabletBoardTableSidePadding * 2); return Padding( padding: const EdgeInsets.all(kTabletBoardTableSidePadding), child: Row( @@ -222,30 +223,25 @@ class _BoardTableState extends ConsumerState { ); } else { final defaultBoardSize = constraints.biggest.shortestSide; - final double boardSize = isTablet - ? defaultBoardSize - kTabletBoardTableSidePadding * 2 - : defaultBoardSize; + final double boardSize = + isTablet ? defaultBoardSize - kTabletBoardTableSidePadding * 2 : defaultBoardSize; // vertical space left on portrait mode to check if we can display the // move list - final verticalSpaceLeftBoardOnPortrait = - constraints.biggest.height - boardSize; + final verticalSpaceLeftBoardOnPortrait = constraints.biggest.height - boardSize; return Column( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.center, children: [ - if (!widget.zenMode && - slicedMoves != null && - verticalSpaceLeftBoardOnPortrait >= 130) + if (!widget.zenMode && slicedMoves != null && verticalSpaceLeftBoardOnPortrait >= 130) MoveList( type: MoveListType.inline, slicedMoves: slicedMoves, currentMoveIndex: widget.currentMoveIndex ?? 0, onSelectMove: widget.onSelectMove, ) - else if (widget.showMoveListPlaceholder && - verticalSpaceLeftBoardOnPortrait >= 130) + else if (widget.showMoveListPlaceholder && verticalSpaceLeftBoardOnPortrait >= 130) const SizedBox(height: 40), Expanded( child: Padding( @@ -257,11 +253,10 @@ class _BoardTableState extends ConsumerState { ), if (widget.engineGauge != null) Padding( - padding: isTablet - ? const EdgeInsets.symmetric( - horizontal: kTabletBoardTableSidePadding, - ) - : EdgeInsets.zero, + padding: + isTablet + ? const EdgeInsets.symmetric(horizontal: kTabletBoardTableSidePadding) + : EdgeInsets.zero, child: EngineGauge( params: widget.engineGauge!, displayMode: EngineGaugeDisplayMode.horizontal, @@ -270,11 +265,10 @@ class _BoardTableState extends ConsumerState { else if (widget.showEngineGaugePlaceholder) const SizedBox(height: kEvalGaugeSize), Padding( - padding: isTablet - ? const EdgeInsets.symmetric( - horizontal: kTabletBoardTableSidePadding, - ) - : EdgeInsets.zero, + padding: + isTablet + ? const EdgeInsets.symmetric(horizontal: kTabletBoardTableSidePadding) + : EdgeInsets.zero, child: _BoardWidget( size: boardSize, boardPrefs: boardPrefs, @@ -386,15 +380,7 @@ class _BoardWidget extends StatelessWidget { } else if (error != null) { return SizedBox.square( dimension: size, - child: Stack( - children: [ - board, - _ErrorWidget( - errorMessage: error!, - boardSize: size, - ), - ], - ), + child: Stack(children: [board, _ErrorWidget(errorMessage: error!, boardSize: size)]), ); } @@ -403,10 +389,7 @@ class _BoardWidget extends StatelessWidget { } class _ErrorWidget extends StatelessWidget { - const _ErrorWidget({ - required this.errorMessage, - required this.boardSize, - }); + const _ErrorWidget({required this.errorMessage, required this.boardSize}); final double boardSize; final String errorMessage; @@ -419,16 +402,13 @@ class _ErrorWidget extends StatelessWidget { padding: const EdgeInsets.all(16.0), child: Container( decoration: BoxDecoration( - color: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoColors.secondarySystemBackground - .resolveFrom(context) - : Theme.of(context).colorScheme.surface, + color: + Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoColors.secondarySystemBackground.resolveFrom(context) + : Theme.of(context).colorScheme.surface, borderRadius: const BorderRadius.all(Radius.circular(10.0)), ), - child: Padding( - padding: const EdgeInsets.all(10.0), - child: Text(errorMessage), - ), + child: Padding(padding: const EdgeInsets.all(10.0), child: Text(errorMessage)), ), ), ), diff --git a/lib/src/widgets/board_thumbnail.dart b/lib/src/widgets/board_thumbnail.dart index 2095e4d797..0607cad0dc 100644 --- a/lib/src/widgets/board_thumbnail.dart +++ b/lib/src/widgets/board_thumbnail.dart @@ -18,15 +18,12 @@ class BoardThumbnail extends ConsumerStatefulWidget { this.animationDuration, }); - const BoardThumbnail.loading({ - required this.size, - this.header, - this.footer, - }) : orientation = Side.white, - fen = kInitialFEN, - lastMove = null, - onTap = null, - animationDuration = null; + const BoardThumbnail.loading({required this.size, this.header, this.footer}) + : orientation = Side.white, + fen = kInitialFEN, + lastMove = null, + onTap = null, + animationDuration = null; /// Size of the board. final double size; @@ -72,56 +69,58 @@ class _BoardThumbnailState extends ConsumerState { Widget build(BuildContext context) { final boardPrefs = ref.watch(boardPreferencesProvider); - final board = widget.animationDuration != null - ? Chessboard.fixed( - size: widget.size, - fen: widget.fen, - orientation: widget.orientation, - lastMove: widget.lastMove as NormalMove?, - settings: ChessboardSettings( + final board = + widget.animationDuration != null + ? Chessboard.fixed( + size: widget.size, + fen: widget.fen, + orientation: widget.orientation, + lastMove: widget.lastMove as NormalMove?, + settings: ChessboardSettings( + enableCoordinates: false, + borderRadius: const BorderRadius.all(Radius.circular(4.0)), + boxShadow: boardShadows, + animationDuration: widget.animationDuration!, + pieceAssets: boardPrefs.pieceSet.assets, + colorScheme: boardPrefs.boardTheme.colors, + ), + ) + : StaticChessboard( + size: widget.size, + fen: widget.fen, + orientation: widget.orientation, + lastMove: widget.lastMove as NormalMove?, enableCoordinates: false, borderRadius: const BorderRadius.all(Radius.circular(4.0)), boxShadow: boardShadows, - animationDuration: widget.animationDuration!, pieceAssets: boardPrefs.pieceSet.assets, colorScheme: boardPrefs.boardTheme.colors, - ), - ) - : StaticChessboard( - size: widget.size, - fen: widget.fen, - orientation: widget.orientation, - lastMove: widget.lastMove as NormalMove?, - enableCoordinates: false, - borderRadius: const BorderRadius.all(Radius.circular(4.0)), - boxShadow: boardShadows, - pieceAssets: boardPrefs.pieceSet.assets, - colorScheme: boardPrefs.boardTheme.colors, - ); - - final maybeTappableBoard = widget.onTap != null - ? GestureDetector( - onTap: widget.onTap, - onTapDown: (_) => _onTapDown(), - onTapCancel: _onTapCancel, - onTapUp: (_) => _onTapCancel(), - child: AnimatedScale( - scale: scale, - duration: const Duration(milliseconds: 100), - child: board, - ), - ) - : board; + ); + + final maybeTappableBoard = + widget.onTap != null + ? GestureDetector( + onTap: widget.onTap, + onTapDown: (_) => _onTapDown(), + onTapCancel: _onTapCancel, + onTapUp: (_) => _onTapCancel(), + child: AnimatedScale( + scale: scale, + duration: const Duration(milliseconds: 100), + child: board, + ), + ) + : board; return widget.header != null || widget.footer != null ? Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (widget.header != null) widget.header!, - maybeTappableBoard, - if (widget.footer != null) widget.footer!, - ], - ) + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (widget.header != null) widget.header!, + maybeTappableBoard, + if (widget.footer != null) widget.footer!, + ], + ) : maybeTappableBoard; } } diff --git a/lib/src/widgets/bottom_bar.dart b/lib/src/widgets/bottom_bar.dart index 5a96c93e4e..d08bb3428e 100644 --- a/lib/src/widgets/bottom_bar.dart +++ b/lib/src/widgets/bottom_bar.dart @@ -13,9 +13,9 @@ class BottomBar extends StatelessWidget { }); const BottomBar.empty() - : children = const [], - expandChildren = true, - mainAxisAlignment = MainAxisAlignment.spaceAround; + : children = const [], + expandChildren = true, + mainAxisAlignment = MainAxisAlignment.spaceAround; /// Children to display in the bottom bar's [Row]. Typically instances of [BottomBarButton]. final List children; @@ -38,9 +38,10 @@ class BottomBar extends StatelessWidget { child: Row( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: mainAxisAlignment, - children: expandChildren - ? children.map((child) => Expanded(child: child)).toList() - : children, + children: + expandChildren + ? children.map((child) => Expanded(child: child)).toList() + : children, ), ), ), @@ -52,9 +53,8 @@ class BottomBar extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6.0), child: Row( mainAxisAlignment: mainAxisAlignment, - children: expandChildren - ? children.map((child) => Expanded(child: child)).toList() - : children, + children: + expandChildren ? children.map((child) => Expanded(child: child)).toList() : children, ), ); } diff --git a/lib/src/widgets/bottom_bar_button.dart b/lib/src/widgets/bottom_bar_button.dart index 7fb33f1461..ae9247711e 100644 --- a/lib/src/widgets/bottom_bar_button.dart +++ b/lib/src/widgets/bottom_bar_button.dart @@ -36,9 +36,10 @@ class BottomBarButton extends StatelessWidget { Widget build(BuildContext context) { final primary = Theme.of(context).colorScheme.primary; - final labelFontSize = Theme.of(context).platform == TargetPlatform.iOS - ? 11.0 - : Theme.of(context).textTheme.bodySmall?.fontSize; + final labelFontSize = + Theme.of(context).platform == TargetPlatform.iOS + ? 11.0 + : Theme.of(context).textTheme.bodySmall?.fontSize; return Semantics( container: true, @@ -49,9 +50,7 @@ class BottomBarButton extends StatelessWidget { child: Tooltip( excludeFromSemantics: true, message: label, - triggerMode: showTooltip - ? TooltipTriggerMode.longPress - : TooltipTriggerMode.manual, + triggerMode: showTooltip ? TooltipTriggerMode.longPress : TooltipTriggerMode.manual, child: AdaptiveInkWell( borderRadius: BorderRadius.zero, onTap: onTap, @@ -65,9 +64,8 @@ class BottomBarButton extends StatelessWidget { _BlinkIcon( badgeLabel: badgeLabel, icon: icon, - color: highlighted - ? primary - : Theme.of(context).iconTheme.color ?? Colors.black, + color: + highlighted ? primary : Theme.of(context).iconTheme.color ?? Colors.black, ) else Badge( @@ -103,11 +101,7 @@ class BottomBarButton extends StatelessWidget { } class _BlinkIcon extends StatefulWidget { - const _BlinkIcon({ - this.badgeLabel, - required this.icon, - required this.color, - }); + const _BlinkIcon({this.badgeLabel, required this.icon, required this.color}); final String? badgeLabel; final IconData icon; @@ -117,8 +111,7 @@ class _BlinkIcon extends StatefulWidget { _BlinkIconState createState() => _BlinkIconState(); } -class _BlinkIconState extends State<_BlinkIcon> - with SingleTickerProviderStateMixin { +class _BlinkIconState extends State<_BlinkIcon> with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation _colorAnimation; @@ -126,20 +119,16 @@ class _BlinkIconState extends State<_BlinkIcon> void initState() { super.initState(); - _controller = AnimationController( - duration: const Duration(milliseconds: 500), - vsync: this, - ); + _controller = AnimationController(duration: const Duration(milliseconds: 500), vsync: this); - _colorAnimation = - ColorTween(begin: widget.color, end: null).animate(_controller) - ..addStatusListener((status) { - if (_controller.status == AnimationStatus.completed) { - _controller.reverse(); - } else if (_controller.status == AnimationStatus.dismissed) { - _controller.forward(); - } - }); + _colorAnimation = ColorTween(begin: widget.color, end: null).animate(_controller) + ..addStatusListener((status) { + if (_controller.status == AnimationStatus.completed) { + _controller.reverse(); + } else if (_controller.status == AnimationStatus.dismissed) { + _controller.forward(); + } + }); _controller.forward(); } @@ -163,10 +152,7 @@ class _BlinkIconState extends State<_BlinkIcon> ), isLabelVisible: widget.badgeLabel != null, label: widget.badgeLabel != null ? Text(widget.badgeLabel!) : null, - child: Icon( - widget.icon, - color: _colorAnimation.value ?? Colors.transparent, - ), + child: Icon(widget.icon, color: _colorAnimation.value ?? Colors.transparent), ); }, ); diff --git a/lib/src/widgets/buttons.dart b/lib/src/widgets/buttons.dart index 38d0193698..8d0e7eff1a 100644 --- a/lib/src/widgets/buttons.dart +++ b/lib/src/widgets/buttons.dart @@ -29,12 +29,10 @@ class FatButton extends StatelessWidget { button: true, label: semanticsLabel, excludeSemantics: true, - child: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoButton.tinted(onPressed: onPressed, child: child) - : FilledButton( - onPressed: onPressed, - child: child, - ), + child: + Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoButton.tinted(onPressed: onPressed, child: child) + : FilledButton(onPressed: onPressed, child: child), ); } } @@ -60,25 +58,20 @@ class SecondaryButton extends StatefulWidget { State createState() => _SecondaryButtonState(); } -class _SecondaryButtonState extends State - with SingleTickerProviderStateMixin { +class _SecondaryButtonState extends State with SingleTickerProviderStateMixin { late final AnimationController _controller; late final Animation _animation; @override void initState() { super.initState(); - _controller = AnimationController( - duration: const Duration(seconds: 1), - vsync: this, - ); + _controller = AnimationController(duration: const Duration(seconds: 1), vsync: this); _animation = (defaultTargetPlatform == TargetPlatform.iOS - ? Tween(begin: 0.5, end: 1.0) - : Tween(begin: 0.0, end: 0.3)) - .animate(_controller) - ..addListener(() { - setState(() {}); - }); + ? Tween(begin: 0.5, end: 1.0) + : Tween(begin: 0.0, end: 0.3)) + .animate(_controller)..addListener(() { + setState(() {}); + }); if (widget.glowing) { _controller.repeat(reverse: true); @@ -111,40 +104,38 @@ class _SecondaryButtonState extends State button: true, label: widget.semanticsLabel, excludeSemantics: true, - child: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoButton( - color: widget.glowing - ? CupertinoTheme.of(context) - .primaryColor - .withValues(alpha: _animation.value) - : null, - onPressed: widget.onPressed, - child: widget.child, - ) - : OutlinedButton( - onPressed: widget.onPressed, - style: OutlinedButton.styleFrom( - textStyle: widget.textStyle, - backgroundColor: widget.glowing - ? Theme.of(context) - .colorScheme - .primary - .withValues(alpha: _animation.value) - : null, + child: + Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoButton( + color: + widget.glowing + ? CupertinoTheme.of( + context, + ).primaryColor.withValues(alpha: _animation.value) + : null, + onPressed: widget.onPressed, + child: widget.child, + ) + : OutlinedButton( + onPressed: widget.onPressed, + style: OutlinedButton.styleFrom( + textStyle: widget.textStyle, + backgroundColor: + widget.glowing + ? Theme.of( + context, + ).colorScheme.primary.withValues(alpha: _animation.value) + : null, + ), + child: widget.child, ), - child: widget.child, - ), ); } } /// Platform agnostic text button to appear in the app bar. class AppBarTextButton extends StatelessWidget { - const AppBarTextButton({ - required this.child, - required this.onPressed, - super.key, - }); + const AppBarTextButton({required this.child, required this.onPressed, super.key}); final VoidCallback? onPressed; final Widget child; @@ -152,15 +143,8 @@ class AppBarTextButton extends StatelessWidget { @override Widget build(BuildContext context) { return Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoButton( - padding: EdgeInsets.zero, - onPressed: onPressed, - child: child, - ) - : TextButton( - onPressed: onPressed, - child: child, - ); + ? CupertinoButton(padding: EdgeInsets.zero, onPressed: onPressed, child: child) + : TextButton(onPressed: onPressed, child: child); } } @@ -181,16 +165,12 @@ class AppBarIconButton extends StatelessWidget { Widget build(BuildContext context) { return Theme.of(context).platform == TargetPlatform.iOS ? CupertinoIconButton( - padding: EdgeInsets.zero, - semanticsLabel: semanticsLabel, - onPressed: onPressed, - icon: icon, - ) - : IconButton( - tooltip: semanticsLabel, - icon: icon, - onPressed: onPressed, - ); + padding: EdgeInsets.zero, + semanticsLabel: semanticsLabel, + onPressed: onPressed, + icon: icon, + ) + : IconButton(tooltip: semanticsLabel, icon: icon, onPressed: onPressed); } } @@ -231,11 +211,7 @@ class AppBarNotificationIconButton extends StatelessWidget { } class AdaptiveTextButton extends StatelessWidget { - const AdaptiveTextButton({ - required this.child, - required this.onPressed, - super.key, - }); + const AdaptiveTextButton({required this.child, required this.onPressed, super.key}); final VoidCallback? onPressed; final Widget child; @@ -243,25 +219,15 @@ class AdaptiveTextButton extends StatelessWidget { @override Widget build(BuildContext context) { return Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoButton( - onPressed: onPressed, - child: child, - ) - : TextButton( - onPressed: onPressed, - child: child, - ); + ? CupertinoButton(onPressed: onPressed, child: child) + : TextButton(onPressed: onPressed, child: child); } } /// Button that explicitly reduce padding, thus does not conform to accessibility /// guidelines. So use sparingly. class NoPaddingTextButton extends StatelessWidget { - const NoPaddingTextButton({ - required this.child, - required this.onPressed, - super.key, - }); + const NoPaddingTextButton({required this.child, required this.onPressed, super.key}); final VoidCallback? onPressed; final Widget child; @@ -269,21 +235,16 @@ class NoPaddingTextButton extends StatelessWidget { @override Widget build(BuildContext context) { return Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoButton( - padding: EdgeInsets.zero, - onPressed: onPressed, - minSize: 23, - child: child, - ) + ? CupertinoButton(padding: EdgeInsets.zero, onPressed: onPressed, minSize: 23, child: child) : TextButton( - onPressed: onPressed, - style: TextButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 5.0), - minimumSize: Size.zero, - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), - child: child, - ); + onPressed: onPressed, + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 5.0), + minimumSize: Size.zero, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + child: child, + ); } } @@ -316,10 +277,7 @@ class CupertinoIconButton extends StatelessWidget { child: CupertinoButton( padding: padding, onPressed: onPressed, - child: IconTheme.merge( - data: const IconThemeData(size: 24.0), - child: icon, - ), + child: IconTheme.merge(data: const IconThemeData(size: 24.0), child: icon), ), ); } @@ -360,11 +318,11 @@ class AdaptiveInkWell extends StatelessWidget { onTapCancel: onTapCancel, onLongPress: onLongPress, borderRadius: borderRadius, - splashColor: platform == TargetPlatform.iOS - ? splashColor ?? CupertinoColors.systemGrey5.resolveFrom(context) - : splashColor, - splashFactory: - platform == TargetPlatform.iOS ? NoSplash.splashFactory : null, + splashColor: + platform == TargetPlatform.iOS + ? splashColor ?? CupertinoColors.systemGrey5.resolveFrom(context) + : splashColor, + splashFactory: platform == TargetPlatform.iOS ? NoSplash.splashFactory : null, child: child, ); } @@ -467,10 +425,7 @@ class PlatformIconButton extends StatelessWidget { this.color, this.iconSize, this.padding, - }) : assert( - color == null || !highlighted, - 'Cannot provide both color and highlighted', - ); + }) : assert(color == null || !highlighted, 'Cannot provide both color and highlighted'); final IconData icon; final String semanticsLabel; @@ -499,18 +454,12 @@ class PlatformIconButton extends StatelessWidget { case TargetPlatform.iOS: final themeData = CupertinoTheme.of(context); return CupertinoTheme( - data: themeData.copyWith( - primaryColor: themeData.textTheme.textStyle.color, - ), + data: themeData.copyWith(primaryColor: themeData.textTheme.textStyle.color), child: CupertinoIconButton( onPressed: onTap, semanticsLabel: semanticsLabel, padding: padding, - icon: Icon( - icon, - color: highlighted ? themeData.primaryColor : color, - size: iconSize, - ), + icon: Icon(icon, color: highlighted ? themeData.primaryColor : color, size: iconSize), ), ); default: @@ -542,18 +491,19 @@ class PlatformAppBarMenuButton extends StatelessWidget { @override Widget build(BuildContext context) { if (Theme.of(context).platform == TargetPlatform.iOS) { - final menuActions = actions.map((action) { - return CupertinoContextMenuAction( - onPressed: () { - if (action.dismissOnPress) { - Navigator.of(context).pop(); - } - action.onPressed(); - }, - trailingIcon: action.icon, - child: Text(action.label), - ); - }).toList(); + final menuActions = + actions.map((action) { + return CupertinoContextMenuAction( + onPressed: () { + if (action.dismissOnPress) { + Navigator.of(context).pop(); + } + action.onPressed(); + }, + trailingIcon: action.icon, + child: Text(action.label), + ); + }).toList(); return AppBarIconButton( onPressed: () { showPopover( @@ -565,30 +515,26 @@ class PlatformAppBarMenuButton extends StatelessWidget { width: _kMenuWidth, child: IntrinsicHeight( child: ClipRRect( - borderRadius: - const BorderRadius.all(Radius.circular(13.0)), + borderRadius: const BorderRadius.all(Radius.circular(13.0)), child: ColoredBox( color: CupertinoDynamicColor.resolve( CupertinoContextMenu.kBackgroundColor, context, ), child: ScrollConfiguration( - behavior: ScrollConfiguration.of(context) - .copyWith(scrollbars: false), + behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false), child: CupertinoScrollbar( child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ menuActions.first, - for (final Widget action - in menuActions.skip(1)) + for (final Widget action in menuActions.skip(1)) DecoratedBox( decoration: BoxDecoration( border: Border( top: BorderSide( - color: - CupertinoDynamicColor.resolve( + color: CupertinoDynamicColor.resolve( _kBorderColor, context, ), @@ -623,17 +569,17 @@ class PlatformAppBarMenuButton extends StatelessWidget { } return MenuAnchor( - menuChildren: actions.map((action) { - return MenuItemButton( - leadingIcon: Icon(action.icon), - semanticsLabel: action.label, - closeOnActivate: action.dismissOnPress, - onPressed: action.onPressed, - child: Text(action.label), - ); - }).toList(), - builder: - (BuildContext context, MenuController controller, Widget? child) { + menuChildren: + actions.map((action) { + return MenuItemButton( + leadingIcon: Icon(action.icon), + semanticsLabel: action.label, + closeOnActivate: action.dismissOnPress, + onPressed: action.onPressed, + child: Text(action.label), + ); + }).toList(), + builder: (BuildContext context, MenuController controller, Widget? child) { return AppBarIconButton( onPressed: () { if (controller.isOpen) { diff --git a/lib/src/widgets/clock.dart b/lib/src/widgets/clock.dart index 7de23af3e0..091bb04d49 100644 --- a/lib/src/widgets/clock.dart +++ b/lib/src/widgets/clock.dart @@ -46,20 +46,16 @@ class Clock extends StatelessWidget { final mins = timeLeft.inMinutes.remainder(60); final secs = timeLeft.inSeconds.remainder(60).toString().padLeft(2, '0'); final showTenths = timeLeft < _showTenthsThreshold; - final isEmergency = - emergencyThreshold != null && timeLeft <= emergencyThreshold!; + final isEmergency = emergencyThreshold != null && timeLeft <= emergencyThreshold!; final remainingHeight = estimateRemainingHeightLeftBoard(context); - final hoursDisplay = - padLeft ? hours.toString().padLeft(2, '0') : hours.toString(); - final minsDisplay = - padLeft ? mins.toString().padLeft(2, '0') : mins.toString(); + final hoursDisplay = padLeft ? hours.toString().padLeft(2, '0') : hours.toString(); + final minsDisplay = padLeft ? mins.toString().padLeft(2, '0') : mins.toString(); final brightness = Theme.of(context).brightness; - final activeClockStyle = clockStyle ?? - (brightness == Brightness.dark - ? ClockStyle.darkThemeStyle - : ClockStyle.lightThemeStyle); + final activeClockStyle = + clockStyle ?? + (brightness == Brightness.dark ? ClockStyle.darkThemeStyle : ClockStyle.lightThemeStyle); return LayoutBuilder( builder: (context, constraints) { @@ -69,11 +65,12 @@ class Clock extends StatelessWidget { return Container( decoration: BoxDecoration( borderRadius: const BorderRadius.all(Radius.circular(5.0)), - color: active - ? isEmergency - ? activeClockStyle.emergencyBackgroundColor - : activeClockStyle.activeBackgroundColor - : activeClockStyle.backgroundColor, + color: + active + ? isEmergency + ? activeClockStyle.emergencyBackgroundColor + : activeClockStyle.activeBackgroundColor + : activeClockStyle.backgroundColor, ), child: Padding( padding: const EdgeInsets.symmetric(vertical: 3.0, horizontal: 5.0), @@ -81,40 +78,31 @@ class Clock extends StatelessWidget { maxScaleFactor: kMaxClockTextScaleFactor, child: RichText( text: TextSpan( - text: hours > 0 - ? '$hoursDisplay:${mins.toString().padLeft(2, '0')}:$secs' - : '$minsDisplay:$secs', + text: + hours > 0 + ? '$hoursDisplay:${mins.toString().padLeft(2, '0')}:$secs' + : '$minsDisplay:$secs', style: TextStyle( - color: active - ? isEmergency - ? activeClockStyle.emergencyTextColor - : activeClockStyle.activeTextColor - : activeClockStyle.textColor, + color: + active + ? isEmergency + ? activeClockStyle.emergencyTextColor + : activeClockStyle.activeTextColor + : activeClockStyle.textColor, fontSize: _kClockFontSize * fontScaleFactor, - height: remainingHeight < - kSmallRemainingHeightLeftBoardThreshold - ? 1.0 - : null, - fontFeatures: const [ - FontFeature.tabularFigures(), - ], + height: remainingHeight < kSmallRemainingHeightLeftBoardThreshold ? 1.0 : null, + fontFeatures: const [FontFeature.tabularFigures()], ), children: [ if (showTenths) TextSpan( - text: - '.${timeLeft.inMilliseconds.remainder(1000) ~/ 100}', - style: TextStyle( - fontSize: _kClockTenthFontSize * fontScaleFactor, - ), + text: '.${timeLeft.inMilliseconds.remainder(1000) ~/ 100}', + style: TextStyle(fontSize: _kClockTenthFontSize * fontScaleFactor), ), if (!active && timeLeft < const Duration(seconds: 1)) TextSpan( - text: - '${timeLeft.inMilliseconds.remainder(1000) ~/ 10 % 10}', - style: TextStyle( - fontSize: _kClockHundredsFontSize * fontScaleFactor, - ), + text: '${timeLeft.inMilliseconds.remainder(1000) ~/ 10 % 10}', + style: TextStyle(fontSize: _kClockHundredsFontSize * fontScaleFactor), ), ], ), @@ -309,8 +297,6 @@ class _CountdownClockState extends State { @override Widget build(BuildContext context) { - return RepaintBoundary( - child: widget.builder(context, timeLeft), - ); + return RepaintBoundary(child: widget.builder(context, timeLeft)); } } diff --git a/lib/src/widgets/expanded_section.dart b/lib/src/widgets/expanded_section.dart index 6f8709033f..169d589c12 100644 --- a/lib/src/widgets/expanded_section.dart +++ b/lib/src/widgets/expanded_section.dart @@ -10,8 +10,7 @@ class ExpandedSection extends StatefulWidget { _ExpandedSectionState createState() => _ExpandedSectionState(); } -class _ExpandedSectionState extends State - with SingleTickerProviderStateMixin { +class _ExpandedSectionState extends State with SingleTickerProviderStateMixin { late AnimationController expandController; late Animation animation; @@ -23,10 +22,7 @@ class _ExpandedSectionState extends State value: widget.expand ? 1.0 : 0.0, duration: const Duration(milliseconds: 300), ); - animation = CurvedAnimation( - parent: expandController, - curve: Curves.fastOutSlowIn, - ); + animation = CurvedAnimation(parent: expandController, curve: Curves.fastOutSlowIn); } void _runExpandCheck() { @@ -51,10 +47,6 @@ class _ExpandedSectionState extends State @override Widget build(BuildContext context) { - return SizeTransition( - axisAlignment: 1.0, - sizeFactor: animation, - child: widget.child, - ); + return SizeTransition(axisAlignment: 1.0, sizeFactor: animation, child: widget.child); } } diff --git a/lib/src/widgets/feedback.dart b/lib/src/widgets/feedback.dart index 5c64cdb703..7cb4583903 100644 --- a/lib/src/widgets/feedback.dart +++ b/lib/src/widgets/feedback.dart @@ -25,10 +25,7 @@ class LagIndicator extends StatelessWidget { /// Whether to show a loading indicator when the lag rating is 0. final bool showLoadingIndicator; - static const spinKit = SpinKitThreeBounce( - color: Colors.grey, - size: 15, - ); + static const spinKit = SpinKitThreeBounce(color: Colors.grey, size: 15); static const cupertinoLevels = { 0: CupertinoColors.systemRed, @@ -37,12 +34,7 @@ class LagIndicator extends StatelessWidget { 3: CupertinoColors.systemGreen, }; - static const materialLevels = { - 0: Colors.red, - 1: Colors.yellow, - 2: Colors.green, - 3: Colors.green, - }; + static const materialLevels = {0: Colors.red, 1: Colors.yellow, 2: Colors.green, 3: Colors.green}; @override Widget build(BuildContext context) { @@ -56,15 +48,15 @@ class LagIndicator extends StatelessWidget { maxValue: 4, value: lagRating, size: size, - inactiveColor: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoDynamicColor.resolve( - CupertinoColors.systemGrey, - context, - ).withValues(alpha: 0.2) - : Colors.grey.withValues(alpha: 0.2), - levels: Theme.of(context).platform == TargetPlatform.iOS - ? cupertinoLevels - : materialLevels, + inactiveColor: + Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoDynamicColor.resolve( + CupertinoColors.systemGrey, + context, + ).withValues(alpha: 0.2) + : Colors.grey.withValues(alpha: 0.2), + levels: + Theme.of(context).platform == TargetPlatform.iOS ? cupertinoLevels : materialLevels, ), if (showLoadingIndicator && lagRating == 0) spinKit, ], @@ -88,9 +80,10 @@ class ConnectivityBanner extends ConsumerWidget { } return Container( height: 45, - color: theme.platform == TargetPlatform.iOS - ? cupertinoTheme.scaffoldBackgroundColor - : theme.colorScheme.surfaceContainer, + color: + theme.platform == TargetPlatform.iOS + ? cupertinoTheme.scaffoldBackgroundColor + : theme.colorScheme.surfaceContainer, child: Padding( padding: Styles.horizontalBodyPadding, child: Row( @@ -103,9 +96,8 @@ class ConnectivityBanner extends ConsumerWidget { maxLines: 2, overflow: TextOverflow.ellipsis, style: TextStyle( - color: theme.platform == TargetPlatform.iOS - ? null - : theme.colorScheme.onSurface, + color: + theme.platform == TargetPlatform.iOS ? null : theme.colorScheme.onSurface, ), ), ), @@ -130,9 +122,7 @@ class ButtonLoadingIndicator extends StatelessWidget { return const SizedBox( height: 20, width: 20, - child: CircularProgressIndicator.adaptive( - strokeWidth: 2, - ), + child: CircularProgressIndicator.adaptive(strokeWidth: 2), ); } } @@ -143,9 +133,7 @@ class CenterLoadingIndicator extends StatelessWidget { @override Widget build(BuildContext context) { - return const Center( - child: CircularProgressIndicator.adaptive(), - ); + return const Center(child: CircularProgressIndicator.adaptive()); } } @@ -154,10 +142,7 @@ class CenterLoadingIndicator extends StatelessWidget { /// This widget is intended to be used when a request fails and the user can /// retry it. class FullScreenRetryRequest extends StatelessWidget { - const FullScreenRetryRequest({ - super.key, - required this.onRetry, - }); + const FullScreenRetryRequest({super.key, required this.onRetry}); final VoidCallback onRetry; @@ -169,10 +154,7 @@ class FullScreenRetryRequest extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ - Text( - context.l10n.mobileSomethingWentWrong, - style: Styles.sectionTitle, - ), + Text(context.l10n.mobileSomethingWentWrong, style: Styles.sectionTitle), const SizedBox(height: 10), SecondaryButton( onPressed: onRetry, @@ -185,11 +167,7 @@ class FullScreenRetryRequest extends StatelessWidget { } } -enum SnackBarType { - error, - info, - success, -} +enum SnackBarType { error, info, success } void showPlatformSnackbar( BuildContext context, @@ -202,19 +180,13 @@ void showPlatformSnackbar( SnackBar( content: Text( message, - style: type == SnackBarType.error - ? const TextStyle(color: Colors.white) - : null, + style: type == SnackBarType.error ? const TextStyle(color: Colors.white) : null, ), backgroundColor: type == SnackBarType.error ? Colors.red : null, ), ); case TargetPlatform.iOS: - showCupertinoSnackBar( - context: context, - message: message, - type: type, - ); + showCupertinoSnackBar(context: context, message: message, type: type); default: assert(false, 'Unexpected platform ${Theme.of(context).platform}'); } @@ -228,30 +200,28 @@ void showCupertinoSnackBar({ Duration duration = const Duration(milliseconds: 4000), }) { final overlayEntry = OverlayEntry( - builder: (context) => Positioned( - // default iOS tab bar height + 10 - bottom: 60.0, - left: 8.0, - right: 8.0, - child: _CupertinoSnackBarManager( - snackBar: CupertinoSnackBar( - message: message, - backgroundColor: (type == SnackBarType.error - ? context.lichessColors.error - : type == SnackBarType.success + builder: + (context) => Positioned( + // default iOS tab bar height + 10 + bottom: 60.0, + left: 8.0, + right: 8.0, + child: _CupertinoSnackBarManager( + snackBar: CupertinoSnackBar( + message: message, + backgroundColor: (type == SnackBarType.error + ? context.lichessColors.error + : type == SnackBarType.success ? context.lichessColors.good : CupertinoColors.systemGrey.resolveFrom(context)) - .withValues(alpha: 0.6), - textStyle: const TextStyle(color: Colors.white), + .withValues(alpha: 0.6), + textStyle: const TextStyle(color: Colors.white), + ), + duration: duration, + ), ), - duration: duration, - ), - ), - ); - Future.delayed( - duration + _snackBarAnimationDuration * 2, - overlayEntry.remove, ); + Future.delayed(duration + _snackBarAnimationDuration * 2, overlayEntry.remove); Overlay.of(context).insert(overlayEntry); } @@ -260,11 +230,7 @@ class CupertinoSnackBar extends StatelessWidget { final TextStyle? textStyle; final Color? backgroundColor; - const CupertinoSnackBar({ - required this.message, - this.textStyle, - this.backgroundColor, - }); + const CupertinoSnackBar({required this.message, this.textStyle, this.backgroundColor}); @override Widget build(BuildContext context) { @@ -273,15 +239,8 @@ class CupertinoSnackBar extends StatelessWidget { child: ColoredBox( color: backgroundColor ?? CupertinoColors.systemGrey, child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 12.0, - ), - child: Text( - message, - style: textStyle, - textAlign: TextAlign.center, - ), + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0), + child: Text(message, style: textStyle, textAlign: TextAlign.center), ), ), ); @@ -294,10 +253,7 @@ class _CupertinoSnackBarManager extends StatefulWidget { final CupertinoSnackBar snackBar; final Duration duration; - const _CupertinoSnackBarManager({ - required this.snackBar, - required this.duration, - }); + const _CupertinoSnackBarManager({required this.snackBar, required this.duration}); @override State<_CupertinoSnackBarManager> createState() => _CupertinoSnackBarState(); @@ -310,14 +266,11 @@ class _CupertinoSnackBarState extends State<_CupertinoSnackBarManager> { void initState() { super.initState(); Future.microtask(() => setState(() => _show = true)); - Future.delayed( - widget.duration, - () { - if (mounted) { - setState(() => _show = false); - } - }, - ); + Future.delayed(widget.duration, () { + if (mounted) { + setState(() => _show = false); + } + }); } @override diff --git a/lib/src/widgets/filter.dart b/lib/src/widgets/filter.dart index cfabd0de83..4c204784a5 100644 --- a/lib/src/widgets/filter.dart +++ b/lib/src/widgets/filter.dart @@ -58,17 +58,17 @@ class Filter extends StatelessWidget { .map( (choice) => switch (filterType) { FilterType.singleChoice => ChoiceChip( - label: choiceLabel(choice), - selected: choiceSelected(choice), - onSelected: (value) => onSelected(choice, value), - showCheckmark: showCheckmark, - ), + label: choiceLabel(choice), + selected: choiceSelected(choice), + onSelected: (value) => onSelected(choice, value), + showCheckmark: showCheckmark, + ), FilterType.multipleChoice => FilterChip( - label: choiceLabel(choice), - selected: choiceSelected(choice), - onSelected: (value) => onSelected(choice, value), - showCheckmark: showCheckmark, - ), + label: choiceLabel(choice), + selected: choiceSelected(choice), + onSelected: (value) => onSelected(choice, value), + showCheckmark: showCheckmark, + ), }, ) .toList(growable: false), diff --git a/lib/src/widgets/list.dart b/lib/src/widgets/list.dart index 3d16e587b2..d7f4afe2d1 100644 --- a/lib/src/widgets/list.dart +++ b/lib/src/widgets/list.dart @@ -22,24 +22,19 @@ class ListSection extends StatelessWidget { this.cupertinoClipBehavior = Clip.hardEdge, }) : _isLoading = false; - ListSection.loading({ - required int itemsNumber, - bool header = false, - this.margin, - }) : children = [ - for (int i = 0; i < itemsNumber; i++) const SizedBox.shrink(), - ], - headerTrailing = null, - header = header ? const SizedBox.shrink() : null, - hasLeading = false, - showDivider = false, - showDividerBetweenTiles = false, - dense = false, - cupertinoAdditionalDividerMargin = null, - cupertinoBackgroundColor = null, - cupertinoBorderRadius = null, - cupertinoClipBehavior = Clip.hardEdge, - _isLoading = true; + ListSection.loading({required int itemsNumber, bool header = false, this.margin}) + : children = [for (int i = 0; i < itemsNumber; i++) const SizedBox.shrink()], + headerTrailing = null, + header = header ? const SizedBox.shrink() : null, + hasLeading = false, + showDivider = false, + showDividerBetweenTiles = false, + dense = false, + cupertinoAdditionalDividerMargin = null, + cupertinoBackgroundColor = null, + cupertinoBorderRadius = null, + cupertinoClipBehavior = Clip.hardEdge, + _isLoading = true; /// Usually a list of [PlatformListTile] widgets final List children; @@ -81,139 +76,125 @@ class ListSection extends StatelessWidget { case TargetPlatform.android: return _isLoading ? Padding( - padding: margin ?? Styles.sectionBottomPadding, - child: Column( - children: [ - if (header != null) - Padding( - padding: const EdgeInsets.symmetric( - vertical: 10.0, - horizontal: 16.0, - ), - child: Container( - width: double.infinity, - height: 25, - decoration: const BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.all(Radius.circular(16)), - ), + padding: margin ?? Styles.sectionBottomPadding, + child: Column( + children: [ + if (header != null) + Padding( + padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 16.0), + child: Container( + width: double.infinity, + height: 25, + decoration: const BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.all(Radius.circular(16)), ), ), - for (int i = 0; i < children.length; i++) - Padding( - padding: const EdgeInsets.symmetric( - vertical: 10.0, - horizontal: 16.0, - ), - child: Container( - width: double.infinity, - height: 50, - decoration: const BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.all(Radius.circular(10)), - ), + ), + for (int i = 0; i < children.length; i++) + Padding( + padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 16.0), + child: Container( + width: double.infinity, + height: 50, + decoration: const BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.all(Radius.circular(10)), ), ), - ], - ), - ) + ), + ], + ), + ) : Padding( - padding: margin ?? Styles.sectionBottomPadding, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (header != null) - ListTile( - dense: true, - title: DefaultTextStyle.merge( - style: Styles.sectionTitle, - child: header!, - ), - trailing: headerTrailing, - ), - if (showDividerBetweenTiles) - ...ListTile.divideTiles( - context: context, - tiles: children, - ) - else - ...children, - if (showDivider) - const Padding( - padding: EdgeInsets.only(top: 10.0), - child: Divider(thickness: 0), - ), - ], - ), - ); + padding: margin ?? Styles.sectionBottomPadding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (header != null) + ListTile( + dense: true, + title: DefaultTextStyle.merge(style: Styles.sectionTitle, child: header!), + trailing: headerTrailing, + ), + if (showDividerBetweenTiles) + ...ListTile.divideTiles(context: context, tiles: children) + else + ...children, + if (showDivider) + const Padding( + padding: EdgeInsets.only(top: 10.0), + child: Divider(thickness: 0), + ), + ], + ), + ); case TargetPlatform.iOS: return _isLoading ? Padding( - padding: margin ?? Styles.bodySectionPadding, - child: Column( - children: [ - if (header != null) - // ignore: avoid-wrapping-in-padding - Padding( - padding: const EdgeInsets.only(top: 10.0, bottom: 16.0), - child: Container( - width: double.infinity, - height: 24, - decoration: const BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.all(Radius.circular(16)), - ), + padding: margin ?? Styles.bodySectionPadding, + child: Column( + children: [ + if (header != null) + // ignore: avoid-wrapping-in-padding + Padding( + padding: const EdgeInsets.only(top: 10.0, bottom: 16.0), + child: Container( + width: double.infinity, + height: 24, + decoration: const BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.all(Radius.circular(16)), ), ), - Container( - width: double.infinity, - height: children.length * 54, - decoration: const BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.all(Radius.circular(10)), - ), ), - ], - ), - ) + Container( + width: double.infinity, + height: children.length * 54, + decoration: const BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.all(Radius.circular(10)), + ), + ), + ], + ), + ) : Padding( - padding: margin ?? Styles.bodySectionPadding, - child: Column( - children: [ - if (header != null) - Padding( - padding: const EdgeInsets.only(bottom: 6.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - DefaultTextStyle.merge( - style: Styles.sectionTitle, - child: header!, - ), - if (headerTrailing != null) headerTrailing!, - ], - ), - ), - CupertinoListSection.insetGrouped( - clipBehavior: cupertinoClipBehavior, - backgroundColor: cupertinoBackgroundColor ?? - CupertinoTheme.of(context).scaffoldBackgroundColor, - decoration: BoxDecoration( - color: cupertinoBackgroundColor ?? - Styles.cupertinoCardColor.resolveFrom(context), - borderRadius: cupertinoBorderRadius ?? - const BorderRadius.all(Radius.circular(10.0)), + padding: margin ?? Styles.bodySectionPadding, + child: Column( + children: [ + if (header != null) + Padding( + padding: const EdgeInsets.only(bottom: 6.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + DefaultTextStyle.merge(style: Styles.sectionTitle, child: header!), + if (headerTrailing != null) headerTrailing!, + ], ), - separatorColor: - Styles.cupertinoSeparatorColor.resolveFrom(context), - margin: EdgeInsets.zero, - hasLeading: hasLeading, - additionalDividerMargin: cupertinoAdditionalDividerMargin, - children: children, ), - ], - ), - ); + CupertinoListSection.insetGrouped( + clipBehavior: cupertinoClipBehavior, + backgroundColor: + cupertinoBackgroundColor ?? + CupertinoTheme.of(context).scaffoldBackgroundColor, + decoration: BoxDecoration( + color: + cupertinoBackgroundColor ?? + Styles.cupertinoCardColor.resolveFrom(context), + borderRadius: + cupertinoBorderRadius ?? const BorderRadius.all(Radius.circular(10.0)), + ), + separatorColor: Styles.cupertinoSeparatorColor.resolveFrom(context), + margin: EdgeInsets.zero, + hasLeading: hasLeading, + additionalDividerMargin: cupertinoAdditionalDividerMargin, + children: children, + ), + ], + ), + ); default: assert(false, 'Unexpected platform ${Theme.of(context).platform}'); return const SizedBox.shrink(); @@ -249,21 +230,21 @@ class PlatformDivider extends StatelessWidget { Widget build(BuildContext context) { return Theme.of(context).platform == TargetPlatform.android ? Divider( - height: height, - thickness: thickness, - indent: indent, - endIndent: endIndent, - color: color, - ) + height: height, + thickness: thickness, + indent: indent, + endIndent: endIndent, + color: color, + ) : Divider( - height: height, - thickness: thickness ?? 0.0, - // see: - // https://github.com/flutter/flutter/blob/bff6b93683de8be01d53a39b6183f230518541ac/packages/flutter/lib/src/cupertino/list_section.dart#L53 - indent: indent ?? (cupertinoHasLeading ? 14 + 44.0 : 14.0), - endIndent: endIndent, - color: color ?? CupertinoColors.separator.resolveFrom(context), - ); + height: height, + thickness: thickness ?? 0.0, + // see: + // https://github.com/flutter/flutter/blob/bff6b93683de8be01d53a39b6183f230518541ac/packages/flutter/lib/src/cupertino/list_section.dart#L53 + indent: indent ?? (cupertinoHasLeading ? 14 + 44.0 : 14.0), + endIndent: endIndent, + color: color ?? CupertinoColors.separator.resolveFrom(context), + ); } } @@ -328,14 +309,13 @@ class PlatformListTile extends StatelessWidget { leading: leading, title: title, iconColor: Theme.of(context).colorScheme.outline, - subtitle: subtitle != null - ? DefaultTextStyle.merge( - child: subtitle!, - style: TextStyle( - color: textShade(context, Styles.subtitleOpacity), - ), - ) - : null, + subtitle: + subtitle != null + ? DefaultTextStyle.merge( + child: subtitle!, + style: TextStyle(color: textShade(context, Styles.subtitleOpacity)), + ) + : null, trailing: trailing, dense: dense, visualDensity: visualDensity, @@ -347,31 +327,27 @@ class PlatformListTile extends StatelessWidget { ); case TargetPlatform.iOS: return IconTheme( - data: CupertinoIconThemeData( - color: CupertinoColors.systemGrey.resolveFrom(context), - ), + data: CupertinoIconThemeData(color: CupertinoColors.systemGrey.resolveFrom(context)), child: GestureDetector( onLongPress: onLongPress, child: CupertinoListTile.notched( - backgroundColor: selected == true - ? CupertinoColors.systemGrey4.resolveFrom(context) - : cupertinoBackgroundColor, + backgroundColor: + selected == true + ? CupertinoColors.systemGrey4.resolveFrom(context) + : cupertinoBackgroundColor, leading: leading, - title: harmonizeCupertinoTitleStyle - ? DefaultTextStyle.merge( - // see: https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/cupertino/list_tile.dart - style: const TextStyle( - fontWeight: FontWeight.w600, - fontSize: 16.0, - ), - child: title, - ) - : title, + title: + harmonizeCupertinoTitleStyle + ? DefaultTextStyle.merge( + // see: https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/cupertino/list_tile.dart + style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 16.0), + child: title, + ) + : title, subtitle: subtitle, - trailing: trailing ?? - (selected == true - ? const Icon(CupertinoIcons.check_mark_circled_solid) - : null), + trailing: + trailing ?? + (selected == true ? const Icon(CupertinoIcons.check_mark_circled_solid) : null), additionalInfo: additionalInfo, padding: padding, onTap: onTap, @@ -415,9 +391,8 @@ class AdaptiveListTile extends StatelessWidget { color: Colors.transparent, child: Theme( data: Theme.of(context).copyWith( - splashFactory: Theme.of(context).platform == TargetPlatform.iOS - ? NoSplash.splashFactory - : null, + splashFactory: + Theme.of(context).platform == TargetPlatform.iOS ? NoSplash.splashFactory : null, ), child: ListTile( leading: leading, @@ -431,11 +406,8 @@ class AdaptiveListTile extends StatelessWidget { } } -typedef RemovedItemBuilder = Widget Function( - T item, - BuildContext context, - Animation animation, -); +typedef RemovedItemBuilder = + Widget Function(T item, BuildContext context, Animation animation); /// Keeps a Dart [List] in sync with an [AnimatedList] or [SliverAnimatedList]. /// @@ -447,8 +419,8 @@ class AnimatedListModel { required this.removedItemBuilder, Iterable? initialItems, int? itemsOffset, - }) : _items = List.from(initialItems ?? []), - itemsOffset = itemsOffset ?? 0; + }) : _items = List.from(initialItems ?? []), + itemsOffset = itemsOffset ?? 0; final GlobalKey listKey; final RemovedItemBuilder removedItemBuilder; @@ -470,12 +442,9 @@ class AnimatedListModel { E removeAt(int index) { final E removedItem = _items.removeAt(index - itemsOffset); if (removedItem != null) { - _animatedList!.removeItem( - index, - (BuildContext context, Animation animation) { - return removedItemBuilder(removedItem, context, animation); - }, - ); + _animatedList!.removeItem(index, (BuildContext context, Animation animation) { + return removedItemBuilder(removedItem, context, animation); + }); } return removedItem; } @@ -497,8 +466,8 @@ class SliverAnimatedListModel { required this.removedItemBuilder, Iterable? initialItems, int? itemsOffset, - }) : _items = List.from(initialItems ?? []), - itemsOffset = itemsOffset ?? 0; + }) : _items = List.from(initialItems ?? []), + itemsOffset = itemsOffset ?? 0; final GlobalKey listKey; final RemovedItemBuilder removedItemBuilder; @@ -520,12 +489,9 @@ class SliverAnimatedListModel { E removeAt(int index) { final E removedItem = _items.removeAt(index - itemsOffset); if (removedItem != null) { - _animatedList!.removeItem( - index, - (BuildContext context, Animation animation) { - return removedItemBuilder(removedItem, context, animation); - }, - ); + _animatedList!.removeItem(index, (BuildContext context, Animation animation) { + return removedItemBuilder(removedItem, context, animation); + }); } return removedItem; } diff --git a/lib/src/widgets/misc.dart b/lib/src/widgets/misc.dart index fe1e3e6a80..5bce81f586 100644 --- a/lib/src/widgets/misc.dart +++ b/lib/src/widgets/misc.dart @@ -4,11 +4,7 @@ import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:url_launcher/url_launcher.dart'; class LichessMessage extends StatefulWidget { - const LichessMessage({ - super.key, - this.style, - this.textAlign = TextAlign.start, - }); + const LichessMessage({super.key, this.style, this.textAlign = TextAlign.start}); final TextStyle? style; final TextAlign textAlign; @@ -38,10 +34,7 @@ class _LichessMessageState extends State { @override Widget build(BuildContext context) { - final trans = context.l10n.xIsAFreeYLibreOpenSourceChessServer( - 'Lichess', - context.l10n.really, - ); + final trans = context.l10n.xIsAFreeYLibreOpenSourceChessServer('Lichess', context.l10n.really); final regexp = RegExp(r'''^([^(]*\()([^)]*)(\).*)$'''); final match = regexp.firstMatch(trans); final List spans = []; @@ -50,9 +43,7 @@ class _LichessMessageState extends State { spans.add( TextSpan( text: match[i], - style: i == 2 - ? TextStyle(color: Theme.of(context).colorScheme.primary) - : null, + style: i == 2 ? TextStyle(color: Theme.of(context).colorScheme.primary) : null, recognizer: i == 2 ? _recognizer : null, ), ); @@ -61,12 +52,6 @@ class _LichessMessageState extends State { spans.add(TextSpan(text: trans)); } - return Text.rich( - TextSpan( - style: widget.style, - children: spans, - ), - textAlign: widget.textAlign, - ); + return Text.rich(TextSpan(style: widget.style, children: spans), textAlign: widget.textAlign); } } diff --git a/lib/src/widgets/move_list.dart b/lib/src/widgets/move_list.dart index 631f2127e3..00ef39b310 100644 --- a/lib/src/widgets/move_list.dart +++ b/lib/src/widgets/move_list.dart @@ -48,10 +48,7 @@ class _MoveListState extends ConsumerState { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { if (currentMoveKey.currentContext != null) { - Scrollable.ensureVisible( - currentMoveKey.currentContext!, - alignment: 0.5, - ); + Scrollable.ensureVisible(currentMoveKey.currentContext!, alignment: 0.5); } }); } @@ -79,107 +76,93 @@ class _MoveListState extends ConsumerState { @override Widget build(BuildContext context) { - final pieceNotation = ref.watch(pieceNotationProvider).maybeWhen( - data: (value) => value, - orElse: () => defaultAccountPreferences.pieceNotation, - ); + final pieceNotation = ref + .watch(pieceNotationProvider) + .maybeWhen(data: (value) => value, orElse: () => defaultAccountPreferences.pieceNotation); return widget.type == MoveListType.inline ? Container( - decoration: widget.inlineDecoration, - padding: const EdgeInsets.only(left: 5), - height: _kMoveListHeight, - width: double.infinity, + decoration: widget.inlineDecoration, + padding: const EdgeInsets.only(left: 5), + height: _kMoveListHeight, + width: double.infinity, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: widget.slicedMoves + .mapIndexed( + (index, moves) => Container( + margin: const EdgeInsets.only(right: 10), + child: Row( + children: [ + InlineMoveCount( + pieceNotation: pieceNotation, + count: index + 1, + color: widget.inlineColor, + ), + ...moves.map((move) { + // cursor index starts at 0, move index starts at 1 + final isCurrentMove = widget.currentMoveIndex == move.key + 1; + return InlineMoveItem( + key: isCurrentMove ? currentMoveKey : null, + move: move, + color: widget.inlineColor, + pieceNotation: pieceNotation, + current: isCurrentMove, + onSelectMove: widget.onSelectMove, + ); + }), + ], + ), + ), + ) + .toList(growable: false), + ), + ), + ) + : PlatformCard( + child: Padding( + padding: const EdgeInsets.all(16.0), child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( + child: Column( children: widget.slicedMoves .mapIndexed( - (index, moves) => Container( - margin: const EdgeInsets.only(right: 10), - child: Row( - children: [ - InlineMoveCount( - pieceNotation: pieceNotation, - count: index + 1, - color: widget.inlineColor, - ), - ...moves.map( - (move) { - // cursor index starts at 0, move index starts at 1 - final isCurrentMove = - widget.currentMoveIndex == move.key + 1; - return InlineMoveItem( - key: isCurrentMove ? currentMoveKey : null, - move: move, - color: widget.inlineColor, - pieceNotation: pieceNotation, - current: isCurrentMove, - onSelectMove: widget.onSelectMove, - ); - }, + (index, moves) => Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + StackedMoveCount(count: index + 1), + Expanded( + child: Row( + children: [ + ...moves.map((move) { + // cursor index starts at 0, move index starts at 1 + final isCurrentMove = widget.currentMoveIndex == move.key + 1; + return Expanded( + child: StackedMoveItem( + key: isCurrentMove ? currentMoveKey : null, + move: move, + current: isCurrentMove, + onSelectMove: widget.onSelectMove, + ), + ); + }), + ], ), - ], - ), + ), + ], ), ) .toList(growable: false), ), ), - ) - : PlatformCard( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: SingleChildScrollView( - child: Column( - children: widget.slicedMoves - .mapIndexed( - (index, moves) => Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - StackedMoveCount(count: index + 1), - Expanded( - child: Row( - children: [ - ...moves.map( - (move) { - // cursor index starts at 0, move index starts at 1 - final isCurrentMove = - widget.currentMoveIndex == - move.key + 1; - return Expanded( - child: StackedMoveItem( - key: isCurrentMove - ? currentMoveKey - : null, - move: move, - current: isCurrentMove, - onSelectMove: widget.onSelectMove, - ), - ); - }, - ), - ], - ), - ), - ], - ), - ) - .toList(growable: false), - ), - ), - ), - ); + ), + ); } } class InlineMoveCount extends StatelessWidget { - const InlineMoveCount({ - required this.count, - required this.pieceNotation, - this.color, - }); + const InlineMoveCount({required this.count, required this.pieceNotation, this.color}); final PieceNotation pieceNotation; final int count; @@ -194,10 +177,8 @@ class InlineMoveCount extends StatelessWidget { '$count.', style: TextStyle( fontWeight: FontWeight.w500, - color: color?.withValues(alpha: _moveListOpacity) ?? - textShade(context, _moveListOpacity), - fontFamily: - pieceNotation == PieceNotation.symbol ? 'ChessFont' : null, + color: color?.withValues(alpha: _moveListOpacity) ?? textShade(context, _moveListOpacity), + fontFamily: pieceNotation == PieceNotation.symbol ? 'ChessFont' : null, ), ), ); @@ -230,14 +211,14 @@ class InlineMoveItem extends StatelessWidget { child: Text( move.value, style: TextStyle( - fontFamily: - pieceNotation == PieceNotation.symbol ? 'ChessFont' : null, + fontFamily: pieceNotation == PieceNotation.symbol ? 'ChessFont' : null, fontWeight: current == true ? FontWeight.bold : FontWeight.w500, - color: current != true - ? color != null - ? color!.withValues(alpha: _moveListOpacity) - : textShade(context, _moveListOpacity) - : Theme.of(context).colorScheme.primary, + color: + current != true + ? color != null + ? color!.withValues(alpha: _moveListOpacity) + : textShade(context, _moveListOpacity) + : Theme.of(context).colorScheme.primary, ), ), ), @@ -256,22 +237,14 @@ class StackedMoveCount extends StatelessWidget { width: 40.0, child: Text( '$count.', - style: TextStyle( - fontWeight: FontWeight.w600, - color: textShade(context, _moveListOpacity), - ), + style: TextStyle(fontWeight: FontWeight.w600, color: textShade(context, _moveListOpacity)), ), ); } } class StackedMoveItem extends StatelessWidget { - const StackedMoveItem({ - required this.move, - this.current, - this.onSelectMove, - super.key, - }); + const StackedMoveItem({required this.move, this.current, this.onSelectMove, super.key}); final MapEntry move; final bool? current; diff --git a/lib/src/widgets/non_linear_slider.dart b/lib/src/widgets/non_linear_slider.dart index 31a9f4a335..288f54f716 100644 --- a/lib/src/widgets/non_linear_slider.dart +++ b/lib/src/widgets/non_linear_slider.dart @@ -9,8 +9,8 @@ class NonLinearSlider extends StatefulWidget { this.onChange, this.onChangeEnd, super.key, - }) : assert(values.length > 1), - assert(values.contains(value)); + }) : assert(values.length > 1), + assert(values.contains(value)); final num value; final List values; @@ -46,27 +46,25 @@ class _NonLinearSliderState extends State { @override Widget build(BuildContext context) { return Opacity( - opacity: Theme.of(context).platform != TargetPlatform.iOS || - widget.onChangeEnd != null - ? 1 - : 0.5, + opacity: + Theme.of(context).platform != TargetPlatform.iOS || widget.onChangeEnd != null ? 1 : 0.5, child: Slider.adaptive( value: _index.toDouble(), min: 0, max: widget.values.length.toDouble() - 1, divisions: widget.values.length - 1, - label: widget.labelBuilder?.call(widget.values[_index]) ?? - widget.values[_index].toString(), - onChanged: widget.onChangeEnd != null - ? (double value) { - final newIndex = value.toInt(); - setState(() { - _index = newIndex; - }); + label: widget.labelBuilder?.call(widget.values[_index]) ?? widget.values[_index].toString(), + onChanged: + widget.onChangeEnd != null + ? (double value) { + final newIndex = value.toInt(); + setState(() { + _index = newIndex; + }); - widget.onChange?.call(widget.values[_index]); - } - : null, + widget.onChange?.call(widget.values[_index]); + } + : null, onChangeEnd: (double value) { widget.onChangeEnd?.call(widget.values[_index]); }, diff --git a/lib/src/widgets/pgn.dart b/lib/src/widgets/pgn.dart index bee99a263d..4c39e43570 100644 --- a/lib/src/widgets/pgn.dart +++ b/lib/src/widgets/pgn.dart @@ -54,30 +54,12 @@ Annotation? makeAnnotation(Iterable? nags) { return null; } return switch (nag) { - 1 => const Annotation( - symbol: '!', - color: Colors.lightGreen, - ), - 3 => const Annotation( - symbol: '!!', - color: Colors.teal, - ), - 5 => const Annotation( - symbol: '!?', - color: Colors.purple, - ), - 6 => const Annotation( - symbol: '?!', - color: LichessColors.cyan, - ), - 2 => const Annotation( - symbol: '?', - color: mistakeColor, - ), - 4 => const Annotation( - symbol: '??', - color: blunderColor, - ), + 1 => const Annotation(symbol: '!', color: Colors.lightGreen), + 3 => const Annotation(symbol: '!!', color: Colors.teal), + 5 => const Annotation(symbol: '!?', color: Colors.purple), + 6 => const Annotation(symbol: '?!', color: LichessColors.cyan), + 2 => const Annotation(symbol: '?', color: mistakeColor), + 4 => const Annotation(symbol: '??', color: blunderColor), int() => null, }; } @@ -145,8 +127,7 @@ class DebouncedPgnTreeView extends ConsumerStatefulWidget { final bool shouldShowComments; @override - ConsumerState createState() => - _DebouncedPgnTreeViewState(); + ConsumerState createState() => _DebouncedPgnTreeViewState(); } class _DebouncedPgnTreeViewState extends ConsumerState { @@ -244,35 +225,33 @@ class _DebouncedPgnTreeViewState extends ConsumerState { /// and ultimately evaluated in the [InlineMove] widget. /// /// Grouped in this record to improve readability. -typedef _PgnTreeViewParams = ({ - /// Path to the currently selected move in the tree. - UciPath pathToCurrentMove, +typedef _PgnTreeViewParams = + ({ + /// Path to the currently selected move in the tree. + UciPath pathToCurrentMove, - /// Path to the last live move in the tree if it is a broadcast game - UciPath? pathToBroadcastLiveMove, + /// Path to the last live move in the tree if it is a broadcast game + UciPath? pathToBroadcastLiveMove, - /// Whether to show analysis variations. - bool shouldShowComputerVariations, + /// Whether to show analysis variations. + bool shouldShowComputerVariations, - /// Whether to show NAG annotations like '!' and '??'. - bool shouldShowAnnotations, + /// Whether to show NAG annotations like '!' and '??'. + bool shouldShowAnnotations, - /// Whether to show comments associated with the moves. - bool shouldShowComments, + /// Whether to show comments associated with the moves. + bool shouldShowComments, - /// Key that will we assigned to the widget corresponding to [pathToCurrentMove]. - /// Can be used e.g. to ensure that the current move is visible on the screen. - GlobalKey currentMoveKey, + /// Key that will we assigned to the widget corresponding to [pathToCurrentMove]. + /// Can be used e.g. to ensure that the current move is visible on the screen. + GlobalKey currentMoveKey, - /// Callbacks for when the user interacts with the tree view, e.g. selecting a different move. - PgnTreeNotifier notifier, -}); + /// Callbacks for when the user interacts with the tree view, e.g. selecting a different move. + PgnTreeNotifier notifier, + }); /// Filter node children when computer analysis is disabled -IList _filteredChildren( - ViewNode node, - bool shouldShowComputerVariations, -) { +IList _filteredChildren(ViewNode node, bool shouldShowComputerVariations) { return node.children .where((c) => shouldShowComputerVariations || !c.isComputerVariation) .toIList(); @@ -292,27 +271,19 @@ bool _displaySideLineAsInline(ViewBranch node, [int depth = 0]) { /// Returns whether this node has a sideline that should not be displayed inline. bool _hasNonInlineSideLine(ViewNode node, _PgnTreeViewParams params) { final children = _filteredChildren(node, params.shouldShowComputerVariations); - return children.length > 2 || - (children.length == 2 && !_displaySideLineAsInline(children[1])); + return children.length > 2 || (children.length == 2 && !_displaySideLineAsInline(children[1])); } /// Splits the mainline into parts, where each part is a sequence of moves that are displayed on the same line. /// /// A part ends when a mainline node has a sideline that should not be displayed inline. -Iterable> _mainlineParts( - ViewRoot root, - _PgnTreeViewParams params, -) => +Iterable> _mainlineParts(ViewRoot root, _PgnTreeViewParams params) => [root, ...root.mainline] .splitAfter((n) => _hasNonInlineSideLine(n, params)) .takeWhile((nodes) => nodes.firstOrNull?.children.isNotEmpty == true); class _PgnTreeView extends StatefulWidget { - const _PgnTreeView({ - required this.root, - required this.rootComments, - required this.params, - }); + const _PgnTreeView({required this.root, required this.rootComments, required this.params}); /// Root of the PGN tree final ViewRoot root; @@ -327,18 +298,19 @@ class _PgnTreeView extends StatefulWidget { } /// A record that holds the rendered parts of a subtree. -typedef _CachedRenderedSubtree = ({ - /// The mainline part of the subtree. - _MainLinePart mainLinePart, - - /// The sidelines part of the subtree. - /// - /// This is nullable since the very last mainline part might not have any sidelines. - _IndentedSideLines? sidelines, - - /// Whether the subtree contains the current move. - bool containsCurrentMove, -}); +typedef _CachedRenderedSubtree = + ({ + /// The mainline part of the subtree. + _MainLinePart mainLinePart, + + /// The sidelines part of the subtree. + /// + /// This is nullable since the very last mainline part might not have any sidelines. + _IndentedSideLines? sidelines, + + /// Whether the subtree contains the current move. + bool containsCurrentMove, + }); class _PgnTreeViewState extends State<_PgnTreeView> { /// Caches the result of [_mainlineParts], it only needs to be recalculated when the root changes, @@ -362,68 +334,62 @@ class _PgnTreeViewState extends State<_PgnTreeView> { return path; } - List<_CachedRenderedSubtree> _buildChangedSubtrees({ - required bool fullRebuild, - }) { + List<_CachedRenderedSubtree> _buildChangedSubtrees({required bool fullRebuild}) { var path = UciPath.empty; - return mainlineParts.mapIndexed( - (i, mainlineNodes) { - final mainlineInitialPath = path; - - final sidelineInitialPath = UciPath.join( - path, - UciPath.fromIds( - mainlineNodes - .take(mainlineNodes.length - 1) - .map((n) => n.children.first.id), - ), - ); - - path = sidelineInitialPath; - if (mainlineNodes.last.children.isNotEmpty) { - path = path + mainlineNodes.last.children.first.id; - } - - final mainlinePartOfCurrentPath = _mainlinePartOfCurrentPath(); - final containsCurrentMove = - mainlinePartOfCurrentPath.size > mainlineInitialPath.size && - mainlinePartOfCurrentPath.size <= path.size; - - if (fullRebuild || - subtrees[i].containsCurrentMove || - containsCurrentMove) { - // Skip the first node which is the continuation of the mainline - final sidelineNodes = mainlineNodes.last.children.skip(1); - return ( - mainLinePart: _MainLinePart( - params: widget.params, - initialPath: mainlineInitialPath, - nodes: mainlineNodes, + return mainlineParts + .mapIndexed((i, mainlineNodes) { + final mainlineInitialPath = path; + + final sidelineInitialPath = UciPath.join( + path, + UciPath.fromIds( + mainlineNodes.take(mainlineNodes.length - 1).map((n) => n.children.first.id), ), - sidelines: sidelineNodes.isNotEmpty - ? _IndentedSideLines( - sidelineNodes, - parent: mainlineNodes.last, - params: widget.params, - initialPath: sidelineInitialPath, - nesting: 1, - ) - : null, - containsCurrentMove: containsCurrentMove, ); - } else { - // Avoid expensive rebuilds ([State.build]) of the entire PGN tree by caching parts of the tree that did not change across a path change - return subtrees[i]; - } - }, - ).toList(growable: false); + + path = sidelineInitialPath; + if (mainlineNodes.last.children.isNotEmpty) { + path = path + mainlineNodes.last.children.first.id; + } + + final mainlinePartOfCurrentPath = _mainlinePartOfCurrentPath(); + final containsCurrentMove = + mainlinePartOfCurrentPath.size > mainlineInitialPath.size && + mainlinePartOfCurrentPath.size <= path.size; + + if (fullRebuild || subtrees[i].containsCurrentMove || containsCurrentMove) { + // Skip the first node which is the continuation of the mainline + final sidelineNodes = mainlineNodes.last.children.skip(1); + return ( + mainLinePart: _MainLinePart( + params: widget.params, + initialPath: mainlineInitialPath, + nodes: mainlineNodes, + ), + sidelines: + sidelineNodes.isNotEmpty + ? _IndentedSideLines( + sidelineNodes, + parent: mainlineNodes.last, + params: widget.params, + initialPath: sidelineInitialPath, + nesting: 1, + ) + : null, + containsCurrentMove: containsCurrentMove, + ); + } else { + // Avoid expensive rebuilds ([State.build]) of the entire PGN tree by caching parts of the tree that did not change across a path change + return subtrees[i]; + } + }) + .toList(growable: false); } void _updateLines({required bool fullRebuild}) { setState(() { if (fullRebuild) { - mainlineParts = - _mainlineParts(widget.root, widget.params).toList(growable: false); + mainlineParts = _mainlineParts(widget.root, widget.params).toList(growable: false); } subtrees = _buildChangedSubtrees(fullRebuild: fullRebuild); @@ -440,13 +406,12 @@ class _PgnTreeViewState extends State<_PgnTreeView> { void didUpdateWidget(covariant _PgnTreeView oldWidget) { super.didUpdateWidget(oldWidget); _updateLines( - fullRebuild: oldWidget.root != widget.root || + fullRebuild: + oldWidget.root != widget.root || oldWidget.params.shouldShowComputerVariations != widget.params.shouldShowComputerVariations || - oldWidget.params.shouldShowComments != - widget.params.shouldShowComments || - oldWidget.params.shouldShowAnnotations != - widget.params.shouldShowAnnotations, + oldWidget.params.shouldShowComments != widget.params.shouldShowComments || + oldWidget.params.shouldShowAnnotations != widget.params.shouldShowAnnotations, ); } @@ -463,21 +428,9 @@ class _PgnTreeViewState extends State<_PgnTreeView> { SizedBox.shrink(key: widget.params.currentMoveKey), if (widget.params.shouldShowComments && rootComments.isNotEmpty) - Text.rich( - TextSpan( - children: _comments( - rootComments, - textStyle: _baseTextStyle, - ), - ), - ), + Text.rich(TextSpan(children: _comments(rootComments, textStyle: _baseTextStyle))), ...subtrees - .map( - (part) => [ - part.mainLinePart, - if (part.sidelines != null) part.sidelines!, - ], - ) + .map((part) => [part.mainLinePart, if (part.sidelines != null) part.sidelines!]) .flattened, ], ), @@ -502,48 +455,34 @@ List _buildInlineSideLine({ var path = initialPath; return [ if (followsComment) const WidgetSpan(child: SizedBox(width: 4.0)), - ...sidelineNodes.mapIndexedAndLast( - (i, node, last) { - final pathToNode = path; - path = path + node.id; - - return [ - if (i == 0) ...[ - if (followsComment) const WidgetSpan(child: SizedBox(width: 4.0)), - TextSpan( - text: '(', - style: textStyle, - ), - ], - ..._moveWithComment( - node, - lineInfo: ( - type: _LineType.inlineSideline, - startLine: i == 0 || - (params.shouldShowComments && - sidelineNodes[i - 1].hasTextComment), - pathToLine: initialPath, - ), - pathToNode: pathToNode, - textStyle: textStyle, - params: params, + ...sidelineNodes.mapIndexedAndLast((i, node, last) { + final pathToNode = path; + path = path + node.id; + + return [ + if (i == 0) ...[ + if (followsComment) const WidgetSpan(child: SizedBox(width: 4.0)), + TextSpan(text: '(', style: textStyle), + ], + ..._moveWithComment( + node, + lineInfo: ( + type: _LineType.inlineSideline, + startLine: i == 0 || (params.shouldShowComments && sidelineNodes[i - 1].hasTextComment), + pathToLine: initialPath, ), - if (last) - TextSpan( - text: ')', - style: textStyle, - ), - ]; - }, - ).flattened, + pathToNode: pathToNode, + textStyle: textStyle, + params: params, + ), + if (last) TextSpan(text: ')', style: textStyle), + ]; + }).flattened, const WidgetSpan(child: SizedBox(width: 4.0)), ]; } -const _baseTextStyle = TextStyle( - fontSize: 16.0, - height: 1.5, -); +const _baseTextStyle = TextStyle(fontSize: 16.0, height: 1.5); /// The different types of lines (move sequences) that are displayed in the tree view. enum _LineType { @@ -625,52 +564,41 @@ class _SideLinePart extends ConsumerWidget { final moves = [ ..._moveWithComment( nodes.first, - lineInfo: ( - type: _LineType.sideline, - startLine: true, - pathToLine: initialPath, - ), + lineInfo: (type: _LineType.sideline, startLine: true, pathToLine: initialPath), firstMoveKey: firstMoveKey, pathToNode: initialPath, textStyle: textStyle, params: params, ), - ...nodes.take(nodes.length - 1).map( - (node) { - final moves = [ - ..._moveWithComment( - node.children.first, - lineInfo: ( - type: _LineType.sideline, - startLine: params.shouldShowComments && node.hasTextComment, - pathToLine: initialPath, - ), - pathToNode: path, + ...nodes.take(nodes.length - 1).map((node) { + final moves = [ + ..._moveWithComment( + node.children.first, + lineInfo: ( + type: _LineType.sideline, + startLine: params.shouldShowComments && node.hasTextComment, + pathToLine: initialPath, + ), + pathToNode: path, + textStyle: textStyle, + params: params, + ), + if (node.children.length == 2 && _displaySideLineAsInline(node.children[1])) + ..._buildInlineSideLine( + followsComment: node.children.first.hasTextComment, + firstNode: node.children[1], + parent: node, + initialPath: path, textStyle: textStyle, params: params, ), - if (node.children.length == 2 && - _displaySideLineAsInline(node.children[1])) - ..._buildInlineSideLine( - followsComment: node.children.first.hasTextComment, - firstNode: node.children[1], - parent: node, - initialPath: path, - textStyle: textStyle, - params: params, - ), - ]; - path = path + node.children.first.id; - return moves; - }, - ).flattened, + ]; + path = path + node.children.first.id; + return moves; + }).flattened, ]; - return Text.rich( - TextSpan( - children: moves, - ), - ); + return Text.rich(TextSpan(children: moves)); } } @@ -684,11 +612,7 @@ class _SideLinePart extends ConsumerWidget { /// |- 1... Nc6 <-- sideline part /// 2. Nf3 Nc6 (2... a5) 3. Bc4 <-- mainline part class _MainLinePart extends ConsumerWidget { - const _MainLinePart({ - required this.initialPath, - required this.params, - required this.nodes, - }); + const _MainLinePart({required this.initialPath, required this.params, required this.nodes}); final UciPath initialPath; @@ -698,56 +622,46 @@ class _MainLinePart extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final textStyle = _baseTextStyle.copyWith( - color: _textColor(context, 0.9), - ); + final textStyle = _baseTextStyle.copyWith(color: _textColor(context, 0.9)); var path = initialPath; return Text.rich( TextSpan( children: nodes .takeWhile( - (node) => - _filteredChildren(node, params.shouldShowComputerVariations) - .isNotEmpty, + (node) => _filteredChildren(node, params.shouldShowComputerVariations).isNotEmpty, ) - .mapIndexed( - (i, node) { - final children = _filteredChildren( - node, - params.shouldShowComputerVariations, - ); - final mainlineNode = children.first; - final moves = [ - _moveWithComment( - mainlineNode, - lineInfo: ( - type: _LineType.mainline, - startLine: i == 0 || - (params.shouldShowComments && - (node as ViewBranch).hasTextComment), - pathToLine: initialPath, - ), - pathToNode: path, + .mapIndexed((i, node) { + final children = _filteredChildren(node, params.shouldShowComputerVariations); + final mainlineNode = children.first; + final moves = [ + _moveWithComment( + mainlineNode, + lineInfo: ( + type: _LineType.mainline, + startLine: + i == 0 || + (params.shouldShowComments && (node as ViewBranch).hasTextComment), + pathToLine: initialPath, + ), + pathToNode: path, + textStyle: textStyle, + params: params, + ), + if (children.length == 2 && _displaySideLineAsInline(children[1])) ...[ + _buildInlineSideLine( + followsComment: mainlineNode.hasTextComment, + firstNode: children[1], + parent: node, + initialPath: path, textStyle: textStyle, params: params, ), - if (children.length == 2 && - _displaySideLineAsInline(children[1])) ...[ - _buildInlineSideLine( - followsComment: mainlineNode.hasTextComment, - firstNode: children[1], - parent: node, - initialPath: path, - textStyle: textStyle, - params: params, - ), - ], - ]; - path = path + mainlineNode.id; - return moves.flattened; - }, - ) + ], + ]; + path = path + mainlineNode.id; + return moves.flattened; + }) .flattened .toList(growable: false), ), @@ -833,11 +747,12 @@ class _IndentPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { if (sideLineStartPositions.isNotEmpty) { - final paint = Paint() - ..strokeWidth = 1.5 - ..color = color - ..strokeCap = StrokeCap.round - ..style = PaintingStyle.stroke; + final paint = + Paint() + ..strokeWidth = 1.5 + ..color = color + ..strokeCap = StrokeCap.round + ..style = PaintingStyle.stroke; final origin = Offset(-padding, 0); @@ -913,18 +828,18 @@ class _IndentedSideLinesState extends State<_IndentedSideLines> { (_) => GlobalKey(), ); WidgetsBinding.instance.addPostFrameCallback((_) { - final RenderBox? columnBox = - _columnKey.currentContext?.findRenderObject() as RenderBox?; - final Offset rowOffset = - columnBox?.localToGlobal(Offset.zero) ?? Offset.zero; - - final positions = _sideLinesStartKeys.map((key) { - final context = key.currentContext; - final renderBox = context?.findRenderObject() as RenderBox?; - final height = renderBox?.size.height ?? 0; - final offset = renderBox?.localToGlobal(Offset.zero) ?? Offset.zero; - return Offset(offset.dx, offset.dy + height / 2) - rowOffset; - }).toList(growable: false); + final RenderBox? columnBox = _columnKey.currentContext?.findRenderObject() as RenderBox?; + final Offset rowOffset = columnBox?.localToGlobal(Offset.zero) ?? Offset.zero; + + final positions = _sideLinesStartKeys + .map((key) { + final context = key.currentContext; + final renderBox = context?.findRenderObject() as RenderBox?; + final height = renderBox?.size.height ?? 0; + final offset = renderBox?.localToGlobal(Offset.zero) ?? Offset.zero; + return Offset(offset.dx, offset.dy + height / 2) - rowOffset; + }) + .toList(growable: false); setState(() { _sideLineStartPositions = positions; @@ -932,8 +847,7 @@ class _IndentedSideLinesState extends State<_IndentedSideLines> { }); } - bool get _hasCollapsedLines => - widget.sideLines.any((node) => node.isCollapsed); + bool get _hasCollapsedLines => widget.sideLines.any((node) => node.isCollapsed); Iterable get _expandedSidelines => widget.sideLines.whereNot((node) => node.isCollapsed); @@ -1001,18 +915,11 @@ class _IndentedSideLinesState extends State<_IndentedSideLines> { } } -Color? _textColor( - BuildContext context, - double opacity, { - int? nag, -}) { - final defaultColor = Theme.of(context).platform == TargetPlatform.android - ? Theme.of(context).textTheme.bodyLarge?.color?.withValues(alpha: opacity) - : CupertinoTheme.of(context) - .textTheme - .textStyle - .color - ?.withValues(alpha: opacity); +Color? _textColor(BuildContext context, double opacity, {int? nag}) { + final defaultColor = + Theme.of(context).platform == TargetPlatform.android + ? Theme.of(context).textTheme.bodyLarge?.color?.withValues(alpha: opacity) + : CupertinoTheme.of(context).textTheme.textStyle.color?.withValues(alpha: opacity); return nag != null && nag > 0 ? nagColor(nag) : defaultColor; } @@ -1048,59 +955,45 @@ class InlineMove extends ConsumerWidget { bool get isBroadcastLiveMove => params.pathToBroadcastLiveMove == path; - BoxDecoration? _boxDecoration( - BuildContext context, - bool isCurrentMove, - bool isLiveMove, - ) { + BoxDecoration? _boxDecoration(BuildContext context, bool isCurrentMove, bool isLiveMove) { return (isCurrentMove || isLiveMove) ? BoxDecoration( - color: isCurrentMove - ? Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoColors.systemGrey3.resolveFrom(context) - : Theme.of(context).focusColor - : null, - shape: BoxShape.rectangle, - borderRadius: borderRadius, - border: - isLiveMove ? Border.all(width: 2, color: Colors.orange) : null, - ) + color: + isCurrentMove + ? Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoColors.systemGrey3.resolveFrom(context) + : Theme.of(context).focusColor + : null, + shape: BoxShape.rectangle, + borderRadius: borderRadius, + border: isLiveMove ? Border.all(width: 2, color: Colors.orange) : null, + ) : null; } @override Widget build(BuildContext context, WidgetRef ref) { - final pieceNotation = ref.watch(pieceNotationProvider).maybeWhen( - data: (value) => value, - orElse: () => defaultAccountPreferences.pieceNotation, - ); - final moveFontFamily = - pieceNotation == PieceNotation.symbol ? 'ChessFont' : null; + final pieceNotation = ref + .watch(pieceNotationProvider) + .maybeWhen(data: (value) => value, orElse: () => defaultAccountPreferences.pieceNotation); + final moveFontFamily = pieceNotation == PieceNotation.symbol ? 'ChessFont' : null; final moveTextStyle = textStyle.copyWith( fontFamily: moveFontFamily, - fontWeight: lineInfo.type == _LineType.inlineSideline - ? FontWeight.normal - : FontWeight.w600, + fontWeight: lineInfo.type == _LineType.inlineSideline ? FontWeight.normal : FontWeight.w600, ); - final indexTextStyle = textStyle.copyWith( - color: _textColor(context, 0.6), - ); + final indexTextStyle = textStyle.copyWith(color: _textColor(context, 0.6)); - final indexText = branch.position.ply.isOdd - ? TextSpan( - text: '${(branch.position.ply / 2).ceil()}. ', - style: indexTextStyle, - ) - : (lineInfo.startLine - ? TextSpan( - text: '${(branch.position.ply / 2).ceil()}… ', - style: indexTextStyle, - ) - : null); - - final moveWithNag = branch.sanMove.san + + final indexText = + branch.position.ply.isOdd + ? TextSpan(text: '${(branch.position.ply / 2).ceil()}. ', style: indexTextStyle) + : (lineInfo.startLine + ? TextSpan(text: '${(branch.position.ply / 2).ceil()}… ', style: indexTextStyle) + : null); + + final moveWithNag = + branch.sanMove.san + (branch.nags != null && params.shouldShowAnnotations ? moveAnnotationChar(branch.nags!) : ''); @@ -1118,15 +1011,17 @@ class InlineMove extends ConsumerWidget { isDismissible: true, isScrollControlled: true, showDragHandle: true, - builder: (context) => _MoveContextMenu( - notifier: params.notifier, - title: ply.isOdd - ? '${(ply / 2).ceil()}. $moveWithNag' - : '${(ply / 2).ceil()}... $moveWithNag', - path: path, - branch: branch, - lineInfo: lineInfo, - ), + builder: + (context) => _MoveContextMenu( + notifier: params.notifier, + title: + ply.isOdd + ? '${(ply / 2).ceil()}. $moveWithNag' + : '${(ply / 2).ceil()}... $moveWithNag', + path: path, + branch: branch, + lineInfo: lineInfo, + ), ); }, child: Container( @@ -1135,17 +1030,11 @@ class InlineMove extends ConsumerWidget { child: Text.rich( TextSpan( children: [ - if (indexText != null) ...[ - indexText, - ], + if (indexText != null) ...[indexText], TextSpan( text: moveWithNag, style: moveTextStyle.copyWith( - color: _textColor( - context, - isCurrentMove ? 1 : 0.9, - nag: nag, - ), + color: _textColor(context, isCurrentMove ? 1 : 0.9, nag: nag), ), ), ], @@ -1181,10 +1070,7 @@ class _MoveContextMenu extends ConsumerWidget { mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - title, - style: Theme.of(context).textTheme.titleLarge, - ), + Text(title, style: Theme.of(context).textTheme.titleLarge), if (branch.clock != null) Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -1192,14 +1078,11 @@ class _MoveContextMenu extends ConsumerWidget { Row( mainAxisSize: MainAxisSize.min, children: [ - const Icon( - Icons.punch_clock, - ), + const Icon(Icons.punch_clock), const SizedBox(width: 4.0), Text( branch.clock!.toHoursMinutesSeconds( - showTenths: - branch.clock! < const Duration(minutes: 1), + showTenths: branch.clock! < const Duration(minutes: 1), ), ), ], @@ -1209,14 +1092,9 @@ class _MoveContextMenu extends ConsumerWidget { Row( mainAxisSize: MainAxisSize.min, children: [ - const Icon( - Icons.hourglass_bottom, - ), + const Icon(Icons.hourglass_bottom), const SizedBox(width: 4.0), - Text( - branch.elapsedMoveTime! - .toHoursMinutesSeconds(showTenths: true), - ), + Text(branch.elapsedMoveTime!.toHoursMinutesSeconds(showTenths: true)), ], ), ], @@ -1227,13 +1105,8 @@ class _MoveContextMenu extends ConsumerWidget { ), if (branch.hasTextComment) Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 8.0, - ), - child: Text( - branch.textComments.join(' '), - ), + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: Text(branch.textComments.join(' ')), ), const PlatformDivider(indent: 0), if (lineInfo.type != _LineType.mainline) ...[ @@ -1263,17 +1136,9 @@ class _MoveContextMenu extends ConsumerWidget { } } -List _comments( - Iterable comments, { - required TextStyle textStyle, -}) => - comments - .map( - (comment) => TextSpan( - text: comment, - style: textStyle.copyWith( - fontSize: textStyle.fontSize! - 2.0, - ), - ), - ) - .toList(growable: false); +List _comments(Iterable comments, {required TextStyle textStyle}) => comments + .map( + (comment) => + TextSpan(text: comment, style: textStyle.copyWith(fontSize: textStyle.fontSize! - 2.0)), + ) + .toList(growable: false); diff --git a/lib/src/widgets/platform.dart b/lib/src/widgets/platform.dart index 35e6783100..5fbc4e265b 100644 --- a/lib/src/widgets/platform.dart +++ b/lib/src/widgets/platform.dart @@ -5,11 +5,7 @@ import 'package:lichess_mobile/src/styles/styles.dart'; /// A simple widget that builds different things on different platforms. class PlatformWidget extends StatelessWidget { - const PlatformWidget({ - super.key, - required this.androidBuilder, - required this.iosBuilder, - }); + const PlatformWidget({super.key, required this.androidBuilder, required this.iosBuilder}); final WidgetBuilder androidBuilder; final WidgetBuilder iosBuilder; @@ -28,10 +24,7 @@ class PlatformWidget extends StatelessWidget { } } -typedef ConsumerWidgetBuilder = Widget Function( - BuildContext context, - WidgetRef ref, -); +typedef ConsumerWidgetBuilder = Widget Function(BuildContext context, WidgetRef ref); /// A widget that builds different things on different platforms with riverpod. class ConsumerPlatformWidget extends StatelessWidget { @@ -94,39 +87,38 @@ class PlatformCard extends StatelessWidget { Widget build(BuildContext context) { return MediaQuery.withClampedTextScaling( maxScaleFactor: kCardTextScaleFactor, - child: Theme.of(context).platform == TargetPlatform.iOS - ? Card( - margin: margin ?? EdgeInsets.zero, - elevation: elevation ?? 0, - color: color ?? Styles.cupertinoCardColor.resolveFrom(context), - shadowColor: shadowColor, - shape: borderRadius != null - ? RoundedRectangleBorder( - borderRadius: borderRadius!, - ) - : const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(10.0)), - ), - semanticContainer: semanticContainer, - clipBehavior: clipBehavior, - child: child, - ) - : Card( - shape: borderRadius != null - ? RoundedRectangleBorder( - borderRadius: borderRadius!, - ) - : const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(10.0)), - ), - color: color, - shadowColor: shadowColor, - semanticContainer: semanticContainer, - elevation: elevation, - margin: margin, - clipBehavior: clipBehavior, - child: child, - ), + child: + Theme.of(context).platform == TargetPlatform.iOS + ? Card( + margin: margin ?? EdgeInsets.zero, + elevation: elevation ?? 0, + color: color ?? Styles.cupertinoCardColor.resolveFrom(context), + shadowColor: shadowColor, + shape: + borderRadius != null + ? RoundedRectangleBorder(borderRadius: borderRadius!) + : const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(10.0)), + ), + semanticContainer: semanticContainer, + clipBehavior: clipBehavior, + child: child, + ) + : Card( + shape: + borderRadius != null + ? RoundedRectangleBorder(borderRadius: borderRadius!) + : const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(10.0)), + ), + color: color, + shadowColor: shadowColor, + semanticContainer: semanticContainer, + elevation: elevation, + margin: margin, + clipBehavior: clipBehavior, + child: child, + ), ); } } diff --git a/lib/src/widgets/platform_alert_dialog.dart b/lib/src/widgets/platform_alert_dialog.dart index 7b135aacc3..9738f40f27 100644 --- a/lib/src/widgets/platform_alert_dialog.dart +++ b/lib/src/widgets/platform_alert_dialog.dart @@ -23,16 +23,9 @@ class PlatformAlertDialog extends StatelessWidget { @override Widget build(BuildContext context) { return PlatformWidget( - androidBuilder: (context) => AlertDialog( - title: title, - content: content, - actions: actions, - ), - iosBuilder: (context) => CupertinoAlertDialog( - title: title, - content: content, - actions: actions, - ), + androidBuilder: (context) => AlertDialog(title: title, content: content, actions: actions), + iosBuilder: + (context) => CupertinoAlertDialog(title: title, content: content, actions: actions), ); } } @@ -62,16 +55,14 @@ class PlatformDialogAction extends StatelessWidget { @override Widget build(BuildContext context) { return PlatformWidget( - androidBuilder: (context) => TextButton( - onPressed: onPressed, - child: child, - ), - iosBuilder: (context) => CupertinoDialogAction( - onPressed: onPressed, - isDefaultAction: cupertinoIsDefaultAction, - isDestructiveAction: cupertinoIsDestructiveAction, - child: child, - ), + androidBuilder: (context) => TextButton(onPressed: onPressed, child: child), + iosBuilder: + (context) => CupertinoDialogAction( + onPressed: onPressed, + isDefaultAction: cupertinoIsDefaultAction, + isDestructiveAction: cupertinoIsDestructiveAction, + child: child, + ), ); } } diff --git a/lib/src/widgets/platform_scaffold.dart b/lib/src/widgets/platform_scaffold.dart index e8a0913e9c..0edfc5d194 100644 --- a/lib/src/widgets/platform_scaffold.dart +++ b/lib/src/widgets/platform_scaffold.dart @@ -2,10 +2,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; -const kCupertinoAppBarWithActionPadding = EdgeInsetsDirectional.only( - start: 16.0, - end: 8.0, -); +const kCupertinoAppBarWithActionPadding = EdgeInsetsDirectional.only(start: 16.0, end: 8.0); /// Displays an [AppBar] for Android and a [CupertinoNavigationBar] for iOS. /// @@ -56,19 +53,13 @@ class PlatformAppBar extends StatelessWidget { return CupertinoNavigationBar( padding: actions.isNotEmpty ? kCupertinoAppBarWithActionPadding : null, middle: title, - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: actions, - ), + trailing: Row(mainAxisSize: MainAxisSize.min, children: actions), ); } @override Widget build(BuildContext context) { - return PlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, - ); + return PlatformWidget(androidBuilder: _androidBuilder, iosBuilder: _iosBuilder); } } @@ -80,25 +71,21 @@ class PlatformAppBarLoadingIndicator extends StatelessWidget { Widget build(BuildContext context) { return PlatformWidget( iosBuilder: (_) => const CircularProgressIndicator.adaptive(), - androidBuilder: (_) => const Padding( - padding: EdgeInsets.only(right: 16), - child: SizedBox( - height: 24, - width: 24, - child: Center( - child: CircularProgressIndicator(), + androidBuilder: + (_) => const Padding( + padding: EdgeInsets.only(right: 16), + child: SizedBox( + height: 24, + width: 24, + child: Center(child: CircularProgressIndicator()), + ), ), - ), - ), ); } } -class _CupertinoNavBarWrapper extends StatelessWidget - implements ObstructingPreferredSizeWidget { - const _CupertinoNavBarWrapper({ - required this.child, - }); +class _CupertinoNavBarWrapper extends StatelessWidget implements ObstructingPreferredSizeWidget { + const _CupertinoNavBarWrapper({required this.child}); final Widget child; @@ -106,8 +93,7 @@ class _CupertinoNavBarWrapper extends StatelessWidget Widget build(BuildContext context) => child; @override - Size get preferredSize => - const Size.fromHeight(kMinInteractiveDimensionCupertino); + Size get preferredSize => const Size.fromHeight(kMinInteractiveDimensionCupertino); /// True if the navigation bar's background color has no transparency. @override @@ -145,12 +131,10 @@ class PlatformScaffold extends StatelessWidget { Widget _androidBuilder(BuildContext context) { return Scaffold( resizeToAvoidBottomInset: resizeToAvoidBottomInset, - appBar: appBar != null - ? PreferredSize( - preferredSize: const Size.fromHeight(kToolbarHeight), - child: appBar!, - ) - : null, + appBar: + appBar != null + ? PreferredSize(preferredSize: const Size.fromHeight(kToolbarHeight), child: appBar!) + : null, body: body, ); } @@ -158,17 +142,13 @@ class PlatformScaffold extends StatelessWidget { Widget _iosBuilder(BuildContext context) { return CupertinoPageScaffold( resizeToAvoidBottomInset: resizeToAvoidBottomInset, - navigationBar: - appBar != null ? _CupertinoNavBarWrapper(child: appBar!) : null, + navigationBar: appBar != null ? _CupertinoNavBarWrapper(child: appBar!) : null, child: body, ); } @override Widget build(BuildContext context) { - return PlatformWidget( - androidBuilder: _androidBuilder, - iosBuilder: _iosBuilder, - ); + return PlatformWidget(androidBuilder: _androidBuilder, iosBuilder: _iosBuilder); } } diff --git a/lib/src/widgets/platform_search_bar.dart b/lib/src/widgets/platform_search_bar.dart index a5b67b1e86..bc493bce12 100644 --- a/lib/src/widgets/platform_search_bar.dart +++ b/lib/src/widgets/platform_search_bar.dart @@ -45,34 +45,34 @@ class PlatformSearchBar extends StatelessWidget { @override Widget build(BuildContext context) { return PlatformWidget( - androidBuilder: (context) => SearchBar( - controller: controller, - leading: const Icon(Icons.search), - trailing: [ - if (controller?.text.isNotEmpty == true) - IconButton( - onPressed: onClear ?? () => controller?.clear(), - tooltip: 'Clear', - icon: const Icon( - Icons.close, - ), - ), - ], - onTap: onTap, - focusNode: focusNode, - onSubmitted: onSubmitted, - hintText: hintText, - autoFocus: autoFocus, - ), - iosBuilder: (context) => CupertinoSearchTextField( - controller: controller, - onTap: onTap, - focusNode: focusNode, - onSuffixTap: onClear, - onSubmitted: onSubmitted, - placeholder: hintText, - autofocus: autoFocus, - ), + androidBuilder: + (context) => SearchBar( + controller: controller, + leading: const Icon(Icons.search), + trailing: [ + if (controller?.text.isNotEmpty == true) + IconButton( + onPressed: onClear ?? () => controller?.clear(), + tooltip: 'Clear', + icon: const Icon(Icons.close), + ), + ], + onTap: onTap, + focusNode: focusNode, + onSubmitted: onSubmitted, + hintText: hintText, + autoFocus: autoFocus, + ), + iosBuilder: + (context) => CupertinoSearchTextField( + controller: controller, + onTap: onTap, + focusNode: focusNode, + onSuffixTap: onClear, + onSubmitted: onSubmitted, + placeholder: hintText, + autofocus: autoFocus, + ), ); } } diff --git a/lib/src/widgets/progression_widget.dart b/lib/src/widgets/progression_widget.dart index 544c48ecb1..64352d2b10 100644 --- a/lib/src/widgets/progression_widget.dart +++ b/lib/src/widgets/progression_widget.dart @@ -17,30 +17,21 @@ class ProgressionWidget extends StatelessWidget { children: [ if (progress != 0) ...[ Icon( - progress > 0 - ? LichessIcons.arrow_full_upperright - : LichessIcons.arrow_full_lowerright, + progress > 0 ? LichessIcons.arrow_full_upperright : LichessIcons.arrow_full_lowerright, size: fontSize, - color: progress > 0 - ? context.lichessColors.good - : context.lichessColors.error, + color: progress > 0 ? context.lichessColors.good : context.lichessColors.error, ), Text( progress.abs().toString(), style: TextStyle( - color: progress > 0 - ? context.lichessColors.good - : context.lichessColors.error, + color: progress > 0 ? context.lichessColors.good : context.lichessColors.error, fontSize: fontSize, ), ), ] else Text( '0', - style: TextStyle( - color: textShade(context, _customOpacity), - fontSize: fontSize, - ), + style: TextStyle(color: textShade(context, _customOpacity), fontSize: fontSize), ), ], ); diff --git a/lib/src/widgets/settings.dart b/lib/src/widgets/settings.dart index 2100a27873..b9daf76648 100644 --- a/lib/src/widgets/settings.dart +++ b/lib/src/widgets/settings.dart @@ -43,24 +43,21 @@ class SettingsListTile extends StatelessWidget { leading: icon, title: _SettingsTitle(title: settingsLabel), additionalInfo: showCupertinoTrailingValue ? Text(settingsValue) : null, - subtitle: Theme.of(context).platform == TargetPlatform.android - ? Text( - settingsValue, - style: TextStyle( - color: textShade(context, Styles.subtitleOpacity), - ), - ) - : explanation != null + subtitle: + Theme.of(context).platform == TargetPlatform.android + ? Text( + settingsValue, + style: TextStyle(color: textShade(context, Styles.subtitleOpacity)), + ) + : explanation != null ? Text(explanation!, maxLines: 5) : null, onTap: onTap, - trailing: Theme.of(context).platform == TargetPlatform.iOS - ? const CupertinoListTileChevron() - : explanation != null - ? _SettingsInfoTooltip( - message: explanation!, - child: const Icon(Icons.info_outline), - ) + trailing: + Theme.of(context).platform == TargetPlatform.iOS + ? const CupertinoListTileChevron() + : explanation != null + ? _SettingsInfoTooltip(message: explanation!, child: const Icon(Icons.info_outline)) : null, ), ); @@ -89,11 +86,7 @@ class SwitchSettingTile extends StatelessWidget { leading: leading, title: _SettingsTitle(title: title), subtitle: subtitle, - trailing: Switch.adaptive( - value: value, - onChanged: onChanged, - applyCupertinoTheme: true, - ), + trailing: Switch.adaptive(value: value, onChanged: onChanged, applyCupertinoTheme: true), ); } } @@ -127,8 +120,7 @@ class _SliderSettingsTileState extends State { min: 0, max: widget.values.length.toDouble() - 1, divisions: widget.values.length - 1, - label: widget.labelBuilder?.call(widget.values[_index]) ?? - widget.values[_index].toString(), + label: widget.labelBuilder?.call(widget.values[_index]) ?? widget.values[_index].toString(), onChanged: (value) { final newIndex = value.toInt(); setState(() { @@ -143,9 +135,10 @@ class _SliderSettingsTileState extends State { return PlatformListTile( leading: widget.icon, title: slider, - trailing: widget.labelBuilder != null - ? Text(widget.labelBuilder!.call(widget.values[_index])) - : null, + trailing: + widget.labelBuilder != null + ? Text(widget.labelBuilder!.call(widget.values[_index])) + : null, ); } } @@ -159,22 +152,20 @@ class SettingsSectionTitle extends StatelessWidget { Widget build(BuildContext context) { return Text( title, - style: Theme.of(context).platform == TargetPlatform.iOS - ? TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: CupertinoColors.secondaryLabel.resolveFrom(context), - ) - : null, + style: + Theme.of(context).platform == TargetPlatform.iOS + ? TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: CupertinoColors.secondaryLabel.resolveFrom(context), + ) + : null, ); } } class _SettingsInfoTooltip extends StatelessWidget { - const _SettingsInfoTooltip({ - required this.message, - required this.child, - }); + const _SettingsInfoTooltip({required this.message, required this.child}); final String message; final Widget child; @@ -200,20 +191,13 @@ class _SettingsTitle extends StatelessWidget { Widget build(BuildContext context) { return DefaultTextStyle.merge( // forces iOS default font size - style: Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoTheme.of(context).textTheme.textStyle.copyWith( - fontSize: 17.0, - ) - : null, + style: + Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoTheme.of(context).textTheme.textStyle.copyWith(fontSize: 17.0) + : null, maxLines: 2, overflow: TextOverflow.ellipsis, - child: Text.rich( - TextSpan( - children: [ - title.textSpan ?? TextSpan(text: title.data), - ], - ), - ), + child: Text.rich(TextSpan(children: [title.textSpan ?? TextSpan(text: title.data)])), ); } } @@ -268,9 +252,7 @@ class ChoicePicker extends StatelessWidget { title: titleBuilder(value), subtitle: subtitleBuilder?.call(value), leading: leadingBuilder?.call(value), - onTap: onSelectedItemChanged != null - ? () => onSelectedItemChanged!(value) - : null, + onTap: onSelectedItemChanged != null ? () => onSelectedItemChanged!(value) : null, ); }); return Opacity( @@ -278,47 +260,45 @@ class ChoicePicker extends StatelessWidget { child: Column( children: [ if (showDividerBetweenTiles) - ...ListTile.divideTiles( - context: context, - tiles: tiles, - ) + ...ListTile.divideTiles(context: context, tiles: tiles) else ...tiles, ], ), ); case TargetPlatform.iOS: - final tileConstructor = - notchedTile ? CupertinoListTile.notched : CupertinoListTile.new; + final tileConstructor = notchedTile ? CupertinoListTile.notched : CupertinoListTile.new; return Padding( padding: margin ?? Styles.bodySectionPadding, child: Opacity( opacity: onSelectedItemChanged != null ? 1.0 : 0.5, child: CupertinoListSection.insetGrouped( - backgroundColor: - CupertinoTheme.of(context).scaffoldBackgroundColor, + backgroundColor: CupertinoTheme.of(context).scaffoldBackgroundColor, decoration: BoxDecoration( color: Styles.cupertinoCardColor.resolveFrom(context), borderRadius: const BorderRadius.all(Radius.circular(10.0)), ), - separatorColor: - Styles.cupertinoSeparatorColor.resolveFrom(context), + separatorColor: Styles.cupertinoSeparatorColor.resolveFrom(context), margin: EdgeInsets.zero, additionalDividerMargin: notchedTile ? null : 6.0, hasLeading: leadingBuilder != null, - children: choices.map((value) { - return tileConstructor( - trailing: selectedItem == value - ? const Icon(CupertinoIcons.check_mark_circled_solid) - : null, - title: titleBuilder(value), - subtitle: subtitleBuilder?.call(value), - leading: leadingBuilder?.call(value), - onTap: onSelectedItemChanged != null - ? () => onSelectedItemChanged!(value) - : null, - ); - }).toList(growable: false), + children: choices + .map((value) { + return tileConstructor( + trailing: + selectedItem == value + ? const Icon(CupertinoIcons.check_mark_circled_solid) + : null, + title: titleBuilder(value), + subtitle: subtitleBuilder?.call(value), + leading: leadingBuilder?.call(value), + onTap: + onSelectedItemChanged != null + ? () => onSelectedItemChanged!(value) + : null, + ); + }) + .toList(growable: false), ), ), ); diff --git a/lib/src/widgets/shimmer.dart b/lib/src/widgets/shimmer.dart index a7d3e566d7..b4e86f3be3 100644 --- a/lib/src/widgets/shimmer.dart +++ b/lib/src/widgets/shimmer.dart @@ -5,10 +5,7 @@ class Shimmer extends StatefulWidget { return context.findAncestorStateOfType(); } - const Shimmer({ - super.key, - this.child, - }); + const Shimmer({super.key, this.child}); final Widget? child; @@ -30,26 +27,21 @@ class ShimmerState extends State with SingleTickerProviderStateMixin { } LinearGradient get gradient => LinearGradient( - colors: _defaultGradient.colors, - stops: _defaultGradient.stops, - begin: _defaultGradient.begin, - end: _defaultGradient.end, - transform: - _SlidingGradientTransform(slidePercent: _shimmerController.value), - ); + colors: _defaultGradient.colors, + stops: _defaultGradient.stops, + begin: _defaultGradient.begin, + end: _defaultGradient.end, + transform: _SlidingGradientTransform(slidePercent: _shimmerController.value), + ); Listenable get shimmerChanges => _shimmerController; - bool get isSized => - (context.findRenderObject() as RenderBox?)?.hasSize ?? false; + bool get isSized => (context.findRenderObject() as RenderBox?)?.hasSize ?? false; // ignore: cast_nullable_to_non_nullable Size get size => (context.findRenderObject() as RenderBox).size; - Offset getDescendantOffset({ - required RenderBox descendant, - Offset offset = Offset.zero, - }) { + Offset getDescendantOffset({required RenderBox descendant, Offset offset = Offset.zero}) { // ignore: cast_nullable_to_non_nullable final shimmerBox = context.findRenderObject() as RenderBox; return descendant.localToGlobal(offset, ancestor: shimmerBox); @@ -76,11 +68,7 @@ class ShimmerState extends State with SingleTickerProviderStateMixin { } class ShimmerLoading extends StatefulWidget { - const ShimmerLoading({ - super.key, - required this.isLoading, - required this.child, - }); + const ShimmerLoading({super.key, required this.isLoading, required this.child}); final bool isLoading; final Widget child; @@ -131,12 +119,13 @@ class _ShimmerLoadingState extends State { final shimmerSize = shimmer.size; final gradient = shimmer.gradient; final renderObject = context.findRenderObject(); - final offsetWithinShimmer = renderObject != null - ? shimmer.getDescendantOffset( - // ignore: cast_nullable_to_non_nullable - descendant: renderObject as RenderBox, - ) - : Offset.zero; + final offsetWithinShimmer = + renderObject != null + ? shimmer.getDescendantOffset( + // ignore: cast_nullable_to_non_nullable + descendant: renderObject as RenderBox, + ) + : Offset.zero; return ShaderMask( blendMode: BlendMode.srcATop, @@ -156,41 +145,23 @@ class _ShimmerLoadingState extends State { } const lightShimmerGradient = LinearGradient( - colors: [ - Color(0xFFE3E3E6), - Color(0xFFECECEE), - Color(0xFFE3E3E6), - ], - stops: [ - 0.1, - 0.3, - 0.4, - ], + colors: [Color(0xFFE3E3E6), Color(0xFFECECEE), Color(0xFFE3E3E6)], + stops: [0.1, 0.3, 0.4], begin: Alignment(-1.0, -0.3), end: Alignment(1.0, 0.3), tileMode: TileMode.clamp, ); const darkShimmerGradient = LinearGradient( - colors: [ - Color(0xFF333333), - Color(0xFF3c3c3c), - Color(0xFF333333), - ], - stops: [ - 0.1, - 0.3, - 0.4, - ], + colors: [Color(0xFF333333), Color(0xFF3c3c3c), Color(0xFF333333)], + stops: [0.1, 0.3, 0.4], begin: Alignment(-1.0, -0.3), end: Alignment(1.0, 0.3), tileMode: TileMode.clamp, ); class _SlidingGradientTransform extends GradientTransform { - const _SlidingGradientTransform({ - required this.slidePercent, - }); + const _SlidingGradientTransform({required this.slidePercent}); final double slidePercent; diff --git a/lib/src/widgets/stat_card.dart b/lib/src/widgets/stat_card.dart index 04527305c3..92c5e3fb18 100644 --- a/lib/src/widgets/stat_card.dart +++ b/lib/src/widgets/stat_card.dart @@ -33,8 +33,7 @@ class StatCard extends StatelessWidget { fontSize: statFontSize ?? _defaultStatFontSize, ); - final defaultValueStyle = - TextStyle(fontSize: valueFontSize ?? _defaultValueFontSize); + final defaultValueStyle = TextStyle(fontSize: valueFontSize ?? _defaultValueFontSize); return Padding( padding: padding ?? EdgeInsets.zero, @@ -48,18 +47,10 @@ class StatCard extends StatelessWidget { FittedBox( alignment: Alignment.center, fit: BoxFit.scaleDown, - child: Text( - stat, - style: defaultStatStyle, - textAlign: TextAlign.center, - ), + child: Text(stat, style: defaultStatStyle, textAlign: TextAlign.center), ), if (value != null) - Text( - value!, - style: defaultValueStyle, - textAlign: TextAlign.center, - ) + Text(value!, style: defaultValueStyle, textAlign: TextAlign.center) else if (child != null) child! else @@ -83,9 +74,7 @@ class StatCardRow extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, crossAxisAlignment: CrossAxisAlignment.stretch, - children: _divideRow(cards) - .map((e) => Expanded(child: e)) - .toList(growable: false), + children: _divideRow(cards).map((e) => Expanded(child: e)).toList(growable: false), ), ); } @@ -100,14 +89,8 @@ Iterable _divideRow(Iterable elements) { } Widget wrapElement(Widget el) { - return Container( - margin: const EdgeInsets.only(right: 8), - child: el, - ); + return Container(margin: const EdgeInsets.only(right: 8), child: el); } - return [ - ...list.take(list.length - 1).map(wrapElement), - list.last, - ]; + return [...list.take(list.length - 1).map(wrapElement), list.last]; } diff --git a/lib/src/widgets/user_full_name.dart b/lib/src/widgets/user_full_name.dart index 87fc25dc85..f869aeedf0 100644 --- a/lib/src/widgets/user_full_name.dart +++ b/lib/src/widgets/user_full_name.dart @@ -59,12 +59,10 @@ class UserFullNameWidget extends ConsumerWidget { orElse: () => false, ); - final displayName = user?.name ?? + final displayName = + user?.name ?? (aiLevel != null - ? context.l10n.aiNameLevelAiLevel( - 'Stockfish', - aiLevel.toString(), - ) + ? context.l10n.aiNameLevelAiLevel('Stockfish', aiLevel.toString()) : context.l10n.anonymous); return Row( mainAxisSize: MainAxisSize.min, @@ -74,8 +72,7 @@ class UserFullNameWidget extends ConsumerWidget { padding: const EdgeInsets.only(right: 5), child: Icon( user?.isOnline == true ? Icons.cloud : Icons.cloud_off, - size: style?.fontSize ?? - DefaultTextStyle.of(context).style.fontSize, + size: style?.fontSize ?? DefaultTextStyle.of(context).style.fontSize, color: user?.isOnline == true ? context.lichessColors.good : null, ), ), @@ -84,8 +81,7 @@ class UserFullNameWidget extends ConsumerWidget { padding: const EdgeInsets.only(right: 5), child: Icon( LichessIcons.patron, - size: style?.fontSize ?? - DefaultTextStyle.of(context).style.fontSize, + size: style?.fontSize ?? DefaultTextStyle.of(context).style.fontSize, color: style?.color ?? DefaultTextStyle.of(context).style.color, semanticLabel: context.l10n.patronLichessPatron, ), @@ -94,37 +90,26 @@ class UserFullNameWidget extends ConsumerWidget { Text( user!.title!, style: (style ?? const TextStyle()).copyWith( - color: user?.title == 'BOT' - ? context.lichessColors.fancy - : context.lichessColors.brag, + color: + user?.title == 'BOT' ? context.lichessColors.fancy : context.lichessColors.brag, fontWeight: user?.title == 'BOT' ? null : FontWeight.bold, ), ), const SizedBox(width: 5), ], Flexible( - child: Text( - displayName, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: style, - ), + child: Text(displayName, maxLines: 1, overflow: TextOverflow.ellipsis, style: style), ), if (showFlair && user?.flair != null) ...[ const SizedBox(width: 5), CachedNetworkImage( imageUrl: lichessFlairSrc(user!.flair!), errorWidget: (_, __, ___) => kEmptyWidget, - width: - style?.fontSize ?? DefaultTextStyle.of(context).style.fontSize, - height: - style?.fontSize ?? DefaultTextStyle.of(context).style.fontSize, + width: style?.fontSize ?? DefaultTextStyle.of(context).style.fontSize, + height: style?.fontSize ?? DefaultTextStyle.of(context).style.fontSize, ), ], - if (shouldShowRating && ratingStr != null) ...[ - const SizedBox(width: 5), - Text(ratingStr), - ], + if (shouldShowRating && ratingStr != null) ...[const SizedBox(width: 5), Text(ratingStr)], ], ); } diff --git a/lib/src/widgets/user_list_tile.dart b/lib/src/widgets/user_list_tile.dart index 6e6690eb1c..0346e9288f 100644 --- a/lib/src/widgets/user_list_tile.dart +++ b/lib/src/widgets/user_list_tile.dart @@ -21,11 +21,7 @@ class UserListTile extends StatelessWidget { this.userPerfs, ); - factory UserListTile.fromUser( - User user, - bool isOnline, { - VoidCallback? onTap, - }) { + factory UserListTile.fromUser(User user, bool isOnline, {VoidCallback? onTap}) { return UserListTile._( user.username, user.title, @@ -62,9 +58,7 @@ class UserListTile extends StatelessWidget { Widget build(BuildContext context) { return PlatformListTile( onTap: onTap != null ? () => onTap?.call() : null, - padding: Theme.of(context).platform == TargetPlatform.iOS - ? Styles.bodyPadding - : null, + padding: Theme.of(context).platform == TargetPlatform.iOS ? Styles.bodyPadding : null, leading: Icon( isOnline == true ? Icons.cloud : Icons.cloud_off, color: isOnline == true ? context.lichessColors.good : null, @@ -74,29 +68,17 @@ class UserListTile extends StatelessWidget { child: Row( children: [ if (isPatron == true) ...[ - Icon( - LichessIcons.patron, - semanticLabel: context.l10n.patronLichessPatron, - ), + Icon(LichessIcons.patron, semanticLabel: context.l10n.patronLichessPatron), const SizedBox(width: 5), ], if (title != null) ...[ Text( title!, - style: TextStyle( - color: context.lichessColors.brag, - fontWeight: FontWeight.bold, - ), + style: TextStyle(color: context.lichessColors.brag, fontWeight: FontWeight.bold), ), const SizedBox(width: 5), ], - Flexible( - child: Text( - maxLines: 1, - overflow: TextOverflow.ellipsis, - username, - ), - ), + Flexible(child: Text(maxLines: 1, overflow: TextOverflow.ellipsis, username)), if (flair != null) ...[ const SizedBox(width: 5), CachedNetworkImage( @@ -121,31 +103,23 @@ class _UserRating extends StatelessWidget { @override Widget build(BuildContext context) { - List userPerfs = Perf.values.where((element) { - final p = perfs[element]; - return p != null && - p.numberOfGamesOrRuns > 0 && - p.ratingDeviation < kClueLessDeviation; - }).toList(growable: false); + List userPerfs = Perf.values + .where((element) { + final p = perfs[element]; + return p != null && p.numberOfGamesOrRuns > 0 && p.ratingDeviation < kClueLessDeviation; + }) + .toList(growable: false); if (userPerfs.isEmpty) return const SizedBox.shrink(); userPerfs.sort( - (p1, p2) => perfs[p1]! - .numberOfGamesOrRuns - .compareTo(perfs[p2]!.numberOfGamesOrRuns), + (p1, p2) => perfs[p1]!.numberOfGamesOrRuns.compareTo(perfs[p2]!.numberOfGamesOrRuns), ); userPerfs = userPerfs.reversed.toList(); final rating = perfs[userPerfs.first]?.rating.toString() ?? '?'; final icon = userPerfs.first.icon; - return Row( - children: [ - Icon(icon, size: 16), - const SizedBox(width: 5), - Text(rating), - ], - ); + return Row(children: [Icon(icon, size: 16), const SizedBox(width: 5), Text(rating)]); } } diff --git a/lib/src/widgets/yes_no_dialog.dart b/lib/src/widgets/yes_no_dialog.dart index fb1d0ff1b5..ad5053d0fc 100644 --- a/lib/src/widgets/yes_no_dialog.dart +++ b/lib/src/widgets/yes_no_dialog.dart @@ -25,14 +25,8 @@ class YesNoDialog extends StatelessWidget { title: title, content: content, actions: [ - PlatformDialogAction( - onPressed: onNo, - child: Text(context.l10n.no), - ), - PlatformDialogAction( - onPressed: onYes, - child: Text(context.l10n.yes), - ), + PlatformDialogAction(onPressed: onNo, child: Text(context.l10n.no)), + PlatformDialogAction(onPressed: onYes, child: Text(context.l10n.yes)), ], ); } diff --git a/pubspec.lock b/pubspec.lock index 5d52eebcbf..ac336549b1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -807,10 +807,10 @@ packages: dependency: transitive description: name: image - sha256: "599d08e369969bdf83138f5b4e0a7e823d3f992f23b8a64dd626877c37013533" + sha256: "20842a5ad1555be624c314b0c0cc0566e8ece412f61e859a42efeb6d4101a26c" url: "https://pub.dev" source: hosted - version: "4.4.0" + version: "4.5.0" intl: dependency: "direct main" description: @@ -1247,18 +1247,18 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: "7f172d1b06de5da47b6264c2692ee2ead20bbbc246690427cdb4fc301cd0c549" + sha256: "02a7d8a9ef346c9af715811b01fbd8e27845ad2c41148eefd31321471b41863d" url: "https://pub.dev" source: hosted - version: "2.3.4" + version: "2.4.0" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "07e050c7cd39bad516f8d64c455f04508d09df104be326d8c02551590a0d513d" + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" url: "https://pub.dev" source: hosted - version: "2.5.3" + version: "2.5.4" shared_preferences_linux: dependency: transitive description: @@ -1738,5 +1738,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.5.0 <4.0.0" - flutter: ">=3.27.0-0.1.pre" + dart: ">=3.7.0-209.1.beta <4.0.0" + flutter: ">=3.28.0-0.1.pre" diff --git a/pubspec.yaml b/pubspec.yaml index f2a051efab..959364fde0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,9 +5,9 @@ publish_to: "none" version: 0.13.9+001309 # See README.md for details about versioning environment: - sdk: ">=3.5.0 <4.0.0" + sdk: '^3.7.0-209.1.beta' # We're using the beta channel for the flutter version - flutter: "3.27.0-0.1.pre" + flutter: "3.28.0-0.1.pre" dependencies: app_settings: ^5.1.1 From 080fa070237c0024cfc020d61d98b70fa9a4a29e Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Sun, 15 Dec 2024 20:27:50 +0100 Subject: [PATCH 930/979] Format test folder --- test/app_test.dart | 44 +- test/binding.dart | 94 ++-- test/example_data.dart | 45 +- test/fake_crashlytics.dart | 4 +- test/model/auth/auth_controller_test.dart | 49 +- test/model/auth/fake_auth_repository.dart | 7 +- .../challenge/challenge_repository_test.dart | 10 +- .../challenge/challenge_service_test.dart | 98 +--- test/model/common/node_test.dart | 233 ++------- .../common/service/fake_sound_service.dart | 5 +- test/model/common/time_increment_test.dart | 5 +- .../correspondence_game_storage_test.dart | 30 +- test/model/game/game_repository_test.dart | 6 +- test/model/game/game_socket_events_test.dart | 15 +- test/model/game/game_socket_example_data.dart | 37 +- test/model/game/game_storage_test.dart | 95 +--- test/model/game/game_test.dart | 43 +- test/model/game/material_diff_test.dart | 3 +- .../fake_notification_display.dart | 3 +- .../notification_service_test.dart | 117 ++--- .../puzzle/puzzle_batch_storage_test.dart | 88 +--- test/model/puzzle/puzzle_repository_test.dart | 28 +- test/model/puzzle/puzzle_service_test.dart | 179 ++----- test/model/puzzle/puzzle_storage_test.dart | 21 +- .../relation/relation_repository_test.dart | 10 +- test/model/study/study_repository_test.dart | 183 +++---- test/model/tv/tv_repository_test.dart | 10 +- test/model/user/user_repository_test.dart | 48 +- test/network/fake_websocket_channel.dart | 24 +- test/network/http_test.dart | 186 +++---- test/network/socket_test.dart | 36 +- test/test_container.dart | 7 +- test/test_helpers.dart | 59 +-- test/test_provider_scope.dart | 68 +-- test/utils/duration_test.dart | 23 +- test/utils/fake_connectivity.dart | 6 +- test/utils/l10n_test.dart | 20 +- test/utils/rate_limit_test.dart | 6 +- test/view/analysis/analysis_layout_test.dart | 221 ++++----- test/view/analysis/analysis_screen_test.dart | 161 ++----- .../board_editor_screen_test.dart | 181 ++----- .../broadcasts_list_screen_test.dart | 104 ++-- .../coordinate_training_screen_test.dart | 87 +--- test/view/game/archived_game_screen_test.dart | 212 +++----- test/view/game/game_screen_test.dart | 69 +-- test/view/home/home_tab_screen_test.dart | 107 ++-- .../opening_explorer_screen_test.dart | 254 ++++------ .../over_the_board_screen_test.dart | 61 +-- test/view/puzzle/example_data.dart | 54 +-- .../puzzle/puzzle_history_screen_test.dart | 95 ++-- test/view/puzzle/puzzle_screen_test.dart | 456 ++++++++---------- test/view/puzzle/puzzle_tab_screen_test.dart | 180 ++----- test/view/puzzle/storm_screen_test.dart | 188 +++----- test/view/puzzle/streak_screen_test.dart | 86 ++-- .../settings/settings_tab_screen_test.dart | 133 +++-- test/view/study/study_list_screen_test.dart | 10 +- test/view/study/study_screen_test.dart | 153 ++---- test/view/user/leaderboard_screen_test.dart | 41 +- test/view/user/leaderboard_widget_test.dart | 54 +-- test/view/user/perf_stats_screen_test.dart | 119 ++--- test/view/user/search_screen_test.dart | 140 +++--- test/view/user/user_screen_test.dart | 47 +- test/widgets/adaptive_choice_picker_test.dart | 184 ++++--- test/widgets/board_table_test.dart | 230 ++++----- test/widgets/clock_test.dart | 25 +- 65 files changed, 1898 insertions(+), 3699 deletions(-) diff --git a/test/app_test.dart b/test/app_test.dart index 6ec425e5d2..916147efbb 100644 --- a/test/app_test.dart +++ b/test/app_test.dart @@ -14,51 +14,35 @@ import 'test_provider_scope.dart'; void main() { testWidgets('App loads', (tester) async { - final app = await makeTestProviderScope( - tester, - child: const Application(), - ); + final app = await makeTestProviderScope(tester, child: const Application()); await tester.pumpWidget(app); expect(find.byType(MaterialApp), findsOneWidget); }); - testWidgets('App loads with system theme, which defaults to light', - (tester) async { - final app = await makeTestProviderScope( - tester, - child: const Application(), - ); + testWidgets('App loads with system theme, which defaults to light', (tester) async { + final app = await makeTestProviderScope(tester, child: const Application()); await tester.pumpWidget(app); - expect( - Theme.of(tester.element(find.byType(MaterialApp))).brightness, - Brightness.light, - ); + expect(Theme.of(tester.element(find.byType(MaterialApp))).brightness, Brightness.light); }); - testWidgets( - 'App will delete a stored session on startup if one request return 401', - (tester) async { + testWidgets('App will delete a stored session on startup if one request return 401', ( + tester, + ) async { int tokenTestRequests = 0; final mockClient = MockClient((request) async { if (request.url.path == '/api/token/test') { tokenTestRequests++; - return mockResponse( - ''' + return mockResponse(''' { "${fakeSession.token}": null } - ''', - 200, - ); + ''', 200); } else if (request.url.path == '/api/account') { - return mockResponse( - '{"error": "Unauthorized"}', - 401, - ); + return mockResponse('{"error": "Unauthorized"}', 401); } return mockResponse('', 404); }); @@ -68,8 +52,7 @@ void main() { child: const Application(), userSession: fakeSession, overrides: [ - httpClientFactoryProvider - .overrideWith((ref) => FakeHttpClientFactory(() => mockClient)), + httpClientFactoryProvider.overrideWith((ref) => FakeHttpClientFactory(() => mockClient)), ], ); @@ -98,10 +81,7 @@ void main() { }); testWidgets('Bottom navigation', (tester) async { - final app = await makeTestProviderScope( - tester, - child: const Application(), - ); + final app = await makeTestProviderScope(tester, child: const Application()); await tester.pumpWidget(app); diff --git a/test/binding.dart b/test/binding.dart index 229f180c2a..6462d79266 100644 --- a/test/binding.dart +++ b/test/binding.dart @@ -36,8 +36,7 @@ class TestLichessBinding extends LichessBinding { } /// The single instance of the binding. - static TestLichessBinding get instance => - LichessBinding.checkInstance(_instance); + static TestLichessBinding get instance => LichessBinding.checkInstance(_instance); static TestLichessBinding? _instance; @override @@ -47,9 +46,7 @@ class TestLichessBinding extends LichessBinding { } /// Set the initial values for shared preferences. - Future setInitialSharedPreferencesValues( - Map values, - ) async { + Future setInitialSharedPreferencesValues(Map values) async { for (final entry in values.entries) { if (entry.value is String) { await sharedPreferences.setString(entry.key, entry.value as String); @@ -60,10 +57,7 @@ class TestLichessBinding extends LichessBinding { } else if (entry.value is int) { await sharedPreferences.setInt(entry.key, entry.value as int); } else if (entry.value is List) { - await sharedPreferences.setStringList( - entry.key, - entry.value as List, - ); + await sharedPreferences.setStringList(entry.key, entry.value as List); } else { throw ArgumentError.value( entry.value, @@ -105,8 +99,7 @@ class TestLichessBinding extends LichessBinding { } @override - Stream get firebaseMessagingOnMessage => - firebaseMessaging.onMessage.stream; + Stream get firebaseMessagingOnMessage => firebaseMessaging.onMessage.stream; @override Stream get firebaseMessagingOnMessageOpenedApp => @@ -201,15 +194,16 @@ class FakeSharedPreferences implements SharedPreferencesWithCache { } } -typedef FirebaseMessagingRequestPermissionCall = ({ - bool alert, - bool announcement, - bool badge, - bool carPlay, - bool criticalAlert, - bool provisional, - bool sound, -}); +typedef FirebaseMessagingRequestPermissionCall = + ({ + bool alert, + bool announcement, + bool badge, + bool carPlay, + bool criticalAlert, + bool provisional, + bool sound, + }); class FakeFirebaseMessaging extends Fake implements FirebaseMessaging { /// Whether [requestPermission] will grant permission. @@ -253,43 +247,30 @@ class FakeFirebaseMessaging extends Fake implements FirebaseMessaging { bool provisional = false, bool sound = true, }) async { - _requestPermissionCalls.add( - ( - alert: alert, - announcement: announcement, - badge: badge, - carPlay: carPlay, - criticalAlert: criticalAlert, - provisional: provisional, - sound: sound, - ), - ); + _requestPermissionCalls.add(( + alert: alert, + announcement: announcement, + badge: badge, + carPlay: carPlay, + criticalAlert: criticalAlert, + provisional: provisional, + sound: sound, + )); return _notificationSettings = NotificationSettings( - alert: alert - ? AppleNotificationSetting.enabled - : AppleNotificationSetting.disabled, - announcement: announcement - ? AppleNotificationSetting.enabled - : AppleNotificationSetting.disabled, - authorizationStatus: _willGrantPermission - ? AuthorizationStatus.authorized - : AuthorizationStatus.denied, - badge: badge - ? AppleNotificationSetting.enabled - : AppleNotificationSetting.disabled, - carPlay: carPlay - ? AppleNotificationSetting.enabled - : AppleNotificationSetting.disabled, + alert: alert ? AppleNotificationSetting.enabled : AppleNotificationSetting.disabled, + announcement: + announcement ? AppleNotificationSetting.enabled : AppleNotificationSetting.disabled, + authorizationStatus: + _willGrantPermission ? AuthorizationStatus.authorized : AuthorizationStatus.denied, + badge: badge ? AppleNotificationSetting.enabled : AppleNotificationSetting.disabled, + carPlay: carPlay ? AppleNotificationSetting.enabled : AppleNotificationSetting.disabled, lockScreen: AppleNotificationSetting.enabled, notificationCenter: AppleNotificationSetting.enabled, showPreviews: AppleShowPreviewSetting.whenAuthenticated, timeSensitive: AppleNotificationSetting.disabled, - criticalAlert: criticalAlert - ? AppleNotificationSetting.enabled - : AppleNotificationSetting.disabled, - sound: sound - ? AppleNotificationSetting.enabled - : AppleNotificationSetting.disabled, + criticalAlert: + criticalAlert ? AppleNotificationSetting.enabled : AppleNotificationSetting.disabled, + sound: sound ? AppleNotificationSetting.enabled : AppleNotificationSetting.disabled, ); } @@ -311,8 +292,7 @@ class FakeFirebaseMessaging extends Fake implements FirebaseMessaging { _tokenController.add(token); } - final StreamController _tokenController = - StreamController.broadcast(); + final StreamController _tokenController = StreamController.broadcast(); @override Future getToken({String? vapidKey}) async { @@ -338,13 +318,11 @@ class FakeFirebaseMessaging extends Fake implements FirebaseMessaging { /// /// Call [StreamController.add] to simulate a user press on a notification message /// sent by FCM. - StreamController onMessageOpenedApp = - StreamController.broadcast(); + StreamController onMessageOpenedApp = StreamController.broadcast(); /// Controller for [onBackgroundMessage]. /// /// Call [StreamController.add] to simulate a message received from FCM while /// the application is in background. - StreamController onBackgroundMessage = - StreamController.broadcast(); + StreamController onBackgroundMessage = StreamController.broadcast(); } diff --git a/test/example_data.dart b/test/example_data.dart index 91a85cd077..51d5b681bc 100644 --- a/test/example_data.dart +++ b/test/example_data.dart @@ -11,30 +11,23 @@ import 'package:lichess_mobile/src/model/game/material_diff.dart'; import 'package:lichess_mobile/src/model/game/player.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; -List generateArchivedGames({ - int count = 100, - String? username, -}) { +List generateArchivedGames({int count = 100, String? username}) { return List.generate(count, (index) { final id = GameId('game${index.toString().padLeft(4, '0')}'); final whitePlayer = Player( - user: username != null && index.isEven - ? LightUser( - id: UserId.fromUserName(username), - name: username, - ) - : username != null + user: + username != null && index.isEven + ? LightUser(id: UserId.fromUserName(username), name: username) + : username != null ? const LightUser(id: UserId('whiteId'), name: 'White') : null, rating: username != null ? 1500 : null, ); final blackPlayer = Player( - user: username != null && index.isOdd - ? LightUser( - id: UserId.fromUserName(username), - name: username, - ) - : username != null + user: + username != null && index.isOdd + ? LightUser(id: UserId.fromUserName(username), name: username) + : username != null ? const LightUser(id: UserId('blackId'), name: 'Black') : null, rating: username != null ? 1500 : null, @@ -60,22 +53,18 @@ List generateArchivedGames({ status: GameStatus.started, white: whitePlayer, black: blackPlayer, - clock: ( - initial: const Duration(minutes: 2), - increment: const Duration(seconds: 3), - ), - ), - steps: _makeSteps( - 'e4 Nc6 Bc4 e6 a3 g6 Nf3 Bg7 c3 Nge7 d3 O-O Be3 Na5 Ba2 b6 Qd2', + clock: (initial: const Duration(minutes: 2), increment: const Duration(seconds: 3)), ), + steps: _makeSteps('e4 Nc6 Bc4 e6 a3 g6 Nf3 Bg7 c3 Nge7 d3 O-O Be3 Na5 Ba2 b6 Qd2'), status: GameStatus.started, white: whitePlayer, black: blackPlayer, - youAre: username != null - ? index.isEven - ? Side.white - : Side.black - : null, + youAre: + username != null + ? index.isEven + ? Side.white + : Side.black + : null, ); }); } diff --git a/test/fake_crashlytics.dart b/test/fake_crashlytics.dart index dff3795c76..9821735fc5 100644 --- a/test/fake_crashlytics.dart +++ b/test/fake_crashlytics.dart @@ -57,9 +57,7 @@ class FakeCrashlytics implements FirebaseCrashlytics { }) async {} @override - Future recordFlutterFatalError( - FlutterErrorDetails flutterErrorDetails, - ) async {} + Future recordFlutterFatalError(FlutterErrorDetails flutterErrorDetails) async {} @override Future sendUnsentReports() async {} diff --git a/test/model/auth/auth_controller_test.dart b/test/model/auth/auth_controller_test.dart index a70502a290..5dd222ea93 100644 --- a/test/model/auth/auth_controller_test.dart +++ b/test/model/auth/auth_controller_test.dart @@ -30,26 +30,17 @@ void main() { const testUserSession = AuthSessionState( token: 'testToken', - user: LightUser( - id: UserId('test'), - name: 'test', - title: 'GM', - isPatron: true, - ), + user: LightUser(id: UserId('test'), name: 'test', title: 'GM', isPatron: true), ); const loading = AsyncLoading(); const nullData = AsyncData(null); final client = MockClient((request) { if (request.url.path == '/api/account') { - return mockResponse( - mockApiAccountResponse(testUserSession.user.name), - 200, - ); + return mockResponse(mockApiAccountResponse(testUserSession.user.name), 200); } else if (request.method == 'DELETE' && request.url.path == '/api/token') { return mockResponse('ok', 200); - } else if (request.method == 'POST' && - request.url.path == '/mobile/unregister') { + } else if (request.method == 'POST' && request.url.path == '/mobile/unregister') { return mockResponse('ok', 200); } return mockResponse('', 404); @@ -75,20 +66,17 @@ void main() { group('AuthController', () { test('sign in', () async { - when(() => mockSessionStorage.read()) - .thenAnswer((_) => Future.value(null)); - when(() => mockFlutterAppAuth.authorizeAndExchangeCode(any())) - .thenAnswer((_) => Future.value(signInResponse)); + when(() => mockSessionStorage.read()).thenAnswer((_) => Future.value(null)); when( - () => mockSessionStorage.write(any()), - ).thenAnswer((_) => Future.value(null)); + () => mockFlutterAppAuth.authorizeAndExchangeCode(any()), + ).thenAnswer((_) => Future.value(signInResponse)); + when(() => mockSessionStorage.write(any())).thenAnswer((_) => Future.value(null)); final container = await makeContainer( overrides: [ appAuthProvider.overrideWithValue(mockFlutterAppAuth), sessionStorageProvider.overrideWithValue(mockSessionStorage), - httpClientFactoryProvider - .overrideWith((_) => FakeHttpClientFactory(() => client)), + httpClientFactoryProvider.overrideWith((_) => FakeHttpClientFactory(() => client)), ], ); @@ -114,17 +102,12 @@ void main() { verifyNoMoreInteractions(listener); // it should successfully write the session - verify( - () => mockSessionStorage.write(testUserSession), - ).called(1); + verify(() => mockSessionStorage.write(testUserSession)).called(1); }); test('sign out', () async { - when(() => mockSessionStorage.read()) - .thenAnswer((_) => Future.value(testUserSession)); - when( - () => mockSessionStorage.delete(), - ).thenAnswer((_) => Future.value(null)); + when(() => mockSessionStorage.read()).thenAnswer((_) => Future.value(testUserSession)); + when(() => mockSessionStorage.delete()).thenAnswer((_) => Future.value(null)); int tokenDeleteCount = 0; int unregisterCount = 0; @@ -133,8 +116,7 @@ void main() { if (request.method == 'DELETE' && request.url.path == '/api/token') { tokenDeleteCount++; return mockResponse('ok', 200); - } else if (request.method == 'POST' && - request.url.path == '/mobile/unregister') { + } else if (request.method == 'POST' && request.url.path == '/mobile/unregister') { unregisterCount++; return mockResponse('ok', 200); } @@ -145,8 +127,7 @@ void main() { overrides: [ appAuthProvider.overrideWithValue(mockFlutterAppAuth), sessionStorageProvider.overrideWithValue(mockSessionStorage), - httpClientFactoryProvider - .overrideWith((_) => FakeHttpClientFactory(() => client)), + httpClientFactoryProvider.overrideWith((_) => FakeHttpClientFactory(() => client)), ], userSession: testUserSession, ); @@ -176,9 +157,7 @@ void main() { expect(unregisterCount, 1, reason: 'device should be unregistered'); // session should be deleted - verify( - () => mockSessionStorage.delete(), - ).called(1); + verify(() => mockSessionStorage.delete()).called(1); }); }); } diff --git a/test/model/auth/fake_auth_repository.dart b/test/model/auth/fake_auth_repository.dart index c82b5b48da..4bcbe9eac8 100644 --- a/test/model/auth/fake_auth_repository.dart +++ b/test/model/auth/fake_auth_repository.dart @@ -28,9 +28,4 @@ final fakeUser = User( }), ); -const _fakePerf = UserPerf( - rating: 1500, - ratingDeviation: 0, - progression: 0, - games: 0, -); +const _fakePerf = UserPerf(rating: 1500, ratingDeviation: 0, progression: 0, games: 0); diff --git a/test/model/challenge/challenge_repository_test.dart b/test/model/challenge/challenge_repository_test.dart index b40f62c9a6..21532543c9 100644 --- a/test/model/challenge/challenge_repository_test.dart +++ b/test/model/challenge/challenge_repository_test.dart @@ -14,10 +14,7 @@ void main() { test('list', () async { final mockClient = MockClient((request) { if (request.url.path == '/api/challenge') { - return mockResponse( - challengesList, - 200, - ); + return mockResponse(challengesList, 200); } return mockResponse('', 404); }); @@ -36,10 +33,7 @@ void main() { test('show', () async { final mockClient = MockClient((request) { if (request.url.path == '/api/challenge/H9fIRZUk/show') { - return mockResponse( - challenge, - 200, - ); + return mockResponse(challenge, 200); } return mockResponse('', 404); }); diff --git a/test/model/challenge/challenge_service_test.dart b/test/model/challenge/challenge_service_test.dart index 25c637853a..b882a278e5 100644 --- a/test/model/challenge/challenge_service_test.dart +++ b/test/model/challenge/challenge_service_test.dart @@ -17,8 +17,7 @@ import '../../network/socket_test.dart'; import '../../test_container.dart'; import '../auth/fake_session_storage.dart'; -class NotificationDisplayMock extends Mock - implements FlutterLocalNotificationsPlugin {} +class NotificationDisplayMock extends Mock implements FlutterLocalNotificationsPlugin {} void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -31,15 +30,14 @@ void main() { test('exposes a challenges stream', () async { final fakeChannel = FakeWebSocketChannel(); - final socketClient = - makeTestSocketClient(FakeWebSocketChannelFactory((_) => fakeChannel)); + final socketClient = makeTestSocketClient(FakeWebSocketChannelFactory((_) => fakeChannel)); await socketClient.connect(); await socketClient.firstConnection; fakeChannel.addIncomingMessages([ ''' {"t": "challenges", "d": {"in": [ { "socketVersion": 0, "id": "H9fIRZUk", "url": "https://lichess.org/H9fIRZUk", "status": "created", "challenger": { "id": "bot1", "name": "Bot1", "rating": 1500, "title": "BOT", "provisional": true, "online": true, "lag": 4 }, "destUser": { "id": "bobby", "name": "Bobby", "rating": 1635, "title": "GM", "provisional": true, "online": true, "lag": 4 }, "variant": { "key": "standard", "name": "Standard", "short": "Std" }, "rated": true, "speed": "rapid", "timeControl": { "type": "clock", "limit": 600, "increment": 0, "show": "10+0" }, "color": "random", "finalColor": "black", "perf": { "icon": "", "name": "Rapid" }, "direction": "in" } ] }, "v": 0 } -''' +''', ]); await expectLater( @@ -52,23 +50,13 @@ void main() { id: ChallengeId('H9fIRZUk'), status: ChallengeStatus.created, challenger: ( - user: LightUser( - id: UserId('bot1'), - name: 'Bot1', - title: 'BOT', - isOnline: true, - ), + user: LightUser(id: UserId('bot1'), name: 'Bot1', title: 'BOT', isOnline: true), rating: 1500, provisionalRating: true, lagRating: 4, ), destUser: ( - user: LightUser( - id: UserId('bobby'), - name: 'Bobby', - title: 'GM', - isOnline: true, - ), + user: LightUser(id: UserId('bobby'), name: 'Bobby', title: 'GM', isOnline: true), rating: 1635, provisionalRating: true, lagRating: 4, @@ -77,10 +65,7 @@ void main() { rated: true, speed: Speed.rapid, timeControl: ChallengeTimeControlType.clock, - clock: ( - time: Duration(seconds: 600), - increment: Duration.zero, - ), + clock: (time: Duration(seconds: 600), increment: Duration.zero), sideChoice: SideChoice.random, direction: ChallengeDirection.inward, ), @@ -93,23 +78,15 @@ void main() { socketClient.close(); }); - test('Listen to socket and show a notification for any new challenge', - () async { + test('Listen to socket and show a notification for any new challenge', () async { when( - () => notificationDisplayMock.show( - any(), - any(), - any(), - any(), - payload: any(named: 'payload'), - ), + () => + notificationDisplayMock.show(any(), any(), any(), any(), payload: any(named: 'payload')), ).thenAnswer((_) => Future.value()); final container = await makeContainer( userSession: fakeSession, - overrides: [ - notificationDisplayProvider.overrideWithValue(notificationDisplayMock), - ], + overrides: [notificationDisplayProvider.overrideWithValue(notificationDisplayMock)], ); final notificationService = container.read(notificationServiceProvider); @@ -117,8 +94,7 @@ void main() { fakeAsync((async) { final fakeChannel = FakeWebSocketChannel(); - final socketClient = - makeTestSocketClient(FakeWebSocketChannelFactory((_) => fakeChannel)); + final socketClient = makeTestSocketClient(FakeWebSocketChannelFactory((_) => fakeChannel)); socketClient.connect(); notificationService.start(); challengeService.start(); @@ -130,7 +106,7 @@ void main() { fakeChannel.addIncomingMessages([ ''' {"t": "challenges", "d": {"in": [ { "socketVersion": 0, "id": "H9fIRZUk", "url": "https://lichess.org/H9fIRZUk", "status": "created", "challenger": { "id": "bot1", "name": "Bot1", "rating": 1500, "title": "BOT", "provisional": true, "online": true, "lag": 4 }, "destUser": { "id": "bobby", "name": "Bobby", "rating": 1635, "title": "GM", "provisional": true, "online": true, "lag": 4 }, "variant": { "key": "standard", "name": "Standard", "short": "Std" }, "rated": true, "speed": "rapid", "timeControl": { "type": "clock", "limit": 600, "increment": 0, "show": "10+0" }, "color": "random", "finalColor": "black", "perf": { "icon": "", "name": "Rapid" }, "direction": "in" } ] }, "v": 0 } -''' +''', ]); async.flushMicrotasks(); @@ -149,27 +125,15 @@ void main() { expectLater( result.captured[0], isA() - .having( - (details) => details.android?.channelId, - 'channelId', - 'challenge', - ) - .having( - (d) => d.android?.importance, - 'importance', - Importance.max, - ) - .having( - (d) => d.android?.priority, - 'priority', - Priority.high, - ), + .having((details) => details.android?.channelId, 'channelId', 'challenge') + .having((d) => d.android?.importance, 'importance', Importance.max) + .having((d) => d.android?.priority, 'priority', Priority.high), ); fakeChannel.addIncomingMessages([ ''' {"t": "challenges", "d": {"in": [ { "socketVersion": 0, "id": "H9fIRZUk", "url": "https://lichess.org/H9fIRZUk", "status": "created", "challenger": { "id": "bot1", "name": "Bot1", "rating": 1500, "title": "BOT", "provisional": true, "online": true, "lag": 4 }, "destUser": { "id": "bobby", "name": "Bobby", "rating": 1635, "title": "GM", "provisional": true, "online": true, "lag": 4 }, "variant": { "key": "standard", "name": "Standard", "short": "Std" }, "rated": true, "speed": "rapid", "timeControl": { "type": "clock", "limit": 600, "increment": 0, "show": "10+0" }, "color": "random", "finalColor": "black", "perf": { "icon": "", "name": "Rapid" }, "direction": "in" } ] }, "v": 0 } -''' +''', ]); async.flushMicrotasks(); @@ -193,26 +157,15 @@ void main() { test('Cancels the notification for any missing challenge', () async { when( - () => notificationDisplayMock.show( - any(), - any(), - any(), - any(), - payload: any(named: 'payload'), - ), + () => + notificationDisplayMock.show(any(), any(), any(), any(), payload: any(named: 'payload')), ).thenAnswer((_) => Future.value()); - when( - () => notificationDisplayMock.cancel( - any(), - ), - ).thenAnswer((_) => Future.value()); + when(() => notificationDisplayMock.cancel(any())).thenAnswer((_) => Future.value()); final container = await makeContainer( userSession: fakeSession, - overrides: [ - notificationDisplayProvider.overrideWithValue(notificationDisplayMock), - ], + overrides: [notificationDisplayProvider.overrideWithValue(notificationDisplayMock)], ); final notificationService = container.read(notificationServiceProvider); @@ -220,8 +173,7 @@ void main() { fakeAsync((async) { final fakeChannel = FakeWebSocketChannel(); - final socketClient = - makeTestSocketClient(FakeWebSocketChannelFactory((_) => fakeChannel)); + final socketClient = makeTestSocketClient(FakeWebSocketChannelFactory((_) => fakeChannel)); socketClient.connect(); notificationService.start(); challengeService.start(); @@ -233,7 +185,7 @@ void main() { fakeChannel.addIncomingMessages([ ''' {"t": "challenges", "d": {"in": [ { "socketVersion": 0, "id": "H9fIRZUk", "url": "https://lichess.org/H9fIRZUk", "status": "created", "challenger": { "id": "bot1", "name": "Bot1", "rating": 1500, "title": "BOT", "provisional": true, "online": true, "lag": 4 }, "destUser": { "id": "bobby", "name": "Bobby", "rating": 1635, "title": "GM", "provisional": true, "online": true, "lag": 4 }, "variant": { "key": "standard", "name": "Standard", "short": "Std" }, "rated": true, "speed": "rapid", "timeControl": { "type": "clock", "limit": 600, "increment": 0, "show": "10+0" }, "color": "random", "finalColor": "black", "perf": { "icon": "", "name": "Rapid" }, "direction": "in" } ] }, "v": 0 } -''' +''', ]); async.flushMicrotasks(); @@ -251,15 +203,13 @@ void main() { fakeChannel.addIncomingMessages([ ''' {"t": "challenges", "d": {"in": [] }, "v": 0 } -''' +''', ]); async.flushMicrotasks(); verify( - () => notificationDisplayMock.cancel( - const ChallengeId('H9fIRZUk').hashCode, - ), + () => notificationDisplayMock.cancel(const ChallengeId('H9fIRZUk').hashCode), ).called(1); // closing the socket client to be able to flush the timers diff --git a/test/model/common/node_test.dart b/test/model/common/node_test.dart index 8459d79ff1..c8897014f4 100644 --- a/test/model/common/node_test.dart +++ b/test/model/common/node_test.dart @@ -20,10 +20,7 @@ void main() { child.position.fen, equals('rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1'), ); - expect( - child.position, - equals(Chess.initial.playUnchecked(Move.parse('e2e4')!)), - ); + expect(child.position, equals(Chess.initial.playUnchecked(Move.parse('e2e4')!))); }); test('Root.fromPgnGame, flat', () { @@ -40,8 +37,7 @@ void main() { expect(root.position, equals(Chess.initial)); expect(root.children.length, equals(1)); expect(root.mainline.length, equals(5)); - final nodeWithVariation = - root.nodeAt(UciPath.fromUciMoves(['e2e4', 'd7d5', 'e4d5'])); + final nodeWithVariation = root.nodeAt(UciPath.fromUciMoves(['e2e4', 'd7d5', 'e4d5'])); expect(nodeWithVariation.children.length, 2); expect(nodeWithVariation.children[1].sanMove.san, equals('Nf6')); expect(nodeWithVariation.children[1].children.length, 2); @@ -53,10 +49,7 @@ void main() { final nodeList = root.nodesOn(path).toList(); expect(nodeList.length, equals(2)); expect(nodeList[0], equals(root)); - expect( - nodeList[1], - equals(root.nodeAt(path) as Branch), - ); + expect(nodeList[1], equals(root.nodeAt(path) as Branch)); }); test('branchesOn, simple', () { @@ -64,29 +57,21 @@ void main() { final path = UciPath.fromId(UciCharPair.fromUci('e2e4')); final nodeList = root.branchesOn(path); expect(nodeList.length, equals(1)); - expect( - nodeList.first, - equals(root.nodeAt(path) as Branch), - ); + expect(nodeList.first, equals(root.nodeAt(path) as Branch)); }); test('branchesOn, with variation', () { final root = Root.fromPgnMoves('e4 e5 Nf3'); final move = Move.parse('b1c3')!; final (newPath, _) = root.addMoveAt( - UciPath.fromIds( - [UciCharPair.fromUci('e2e4'), UciCharPair.fromUci('e7e5')].lock, - ), + UciPath.fromIds([UciCharPair.fromUci('e2e4'), UciCharPair.fromUci('e7e5')].lock), move, ); final newNode = root.nodeAt(newPath!); // mainline has not changed expect(root.mainline.length, equals(3)); - expect( - root.mainline.last, - equals(root.nodeAt(root.mainlinePath) as Branch), - ); + expect(root.mainline.last, equals(root.nodeAt(root.mainlinePath) as Branch)); final nodeList = root.branchesOn(newPath); expect(nodeList.length, equals(3)); @@ -110,9 +95,7 @@ void main() { final move = Move.parse('b1c3')!; final (newPath, _) = root.addMoveAt( - UciPath.fromIds( - [UciCharPair.fromUci('e2e4'), UciCharPair.fromUci('e7e5')].lock, - ), + UciPath.fromIds([UciCharPair.fromUci('e2e4'), UciCharPair.fromUci('e7e5')].lock), move, ); @@ -120,13 +103,8 @@ void main() { }); test('add child', () { - final root = Root( - position: Chess.initial, - ); - final child = Branch( - sanMove: SanMove('e4', Move.parse('e2e4')!), - position: Chess.initial, - ); + final root = Root(position: Chess.initial); + final child = Branch(sanMove: SanMove('e4', Move.parse('e2e4')!), position: Chess.initial); root.addChild(child); expect(root.children.length, equals(1)); expect(root.children.first, equals(child)); @@ -134,10 +112,7 @@ void main() { test('prepend child', () { final root = Root.fromPgnMoves('e4 e5'); - final child = Branch( - sanMove: SanMove('d4', Move.parse('d2d4')!), - position: Chess.initial, - ); + final child = Branch(sanMove: SanMove('d4', Move.parse('d2d4')!), position: Chess.initial); root.prependChild(child); expect(root.children.length, equals(2)); expect(root.children.first, equals(child)); @@ -152,12 +127,10 @@ void main() { test('nodeAtOrNull', () { final root = Root.fromPgnMoves('e4 e5'); - final branch = - root.nodeAtOrNull(UciPath.fromId(UciCharPair.fromUci('e2e4'))); + final branch = root.nodeAtOrNull(UciPath.fromId(UciCharPair.fromUci('e2e4'))); expect(branch, equals(root.children.first)); - final branch2 = - root.nodeAtOrNull(UciPath.fromId(UciCharPair.fromUci('b1c3'))); + final branch2 = root.nodeAtOrNull(UciPath.fromId(UciCharPair.fromUci('b1c3'))); expect(branch2, isNull); }); @@ -170,28 +143,18 @@ void main() { test('branchAt from branch', () { final root = Root.fromPgnMoves('e4 e5 Nf3'); final branch = root.branchAt(UciPath.fromId(UciCharPair.fromUci('e2e4'))); - final branch2 = - branch!.branchAt(UciPath.fromId(UciCharPair.fromUci('e7e5'))); + final branch2 = branch!.branchAt(UciPath.fromId(UciCharPair.fromUci('e7e5'))); expect(branch2, equals(branch.children.first)); }); test('updateAt', () { final root = Root.fromPgnMoves('e4 e5'); - final branch = Branch( - sanMove: SanMove('Nc6', Move.parse('b8c6')!), - position: Chess.initial, - ); + final branch = Branch(sanMove: SanMove('Nc6', Move.parse('b8c6')!), position: Chess.initial); final fromPath = UciPath.fromId(UciCharPair.fromUci('e2e4')); final (nodePath, _) = root.addNodeAt(fromPath, branch); - expect( - root.branchesOn(nodePath!), - equals([ - root.children.first, - branch, - ]), - ); + expect(root.branchesOn(nodePath!), equals([root.children.first, branch])); final eval = ClientEval( position: branch.position, @@ -209,22 +172,13 @@ void main() { node.eval = eval; }); - expect( - root.branchesOn(nodePath), - equals([ - root.children.first, - newNode!, - ]), - ); + expect(root.branchesOn(nodePath), equals([root.children.first, newNode!])); }); test('updateAll', () { final root = Root.fromPgnMoves('e4 e5 Nf3'); - expect( - root.mainline.map((n) => n.eval), - equals([null, null, null]), - ); + expect(root.mainline.map((n) => n.eval), equals([null, null, null])); final eval = ClientEval( position: root.position, @@ -242,28 +196,20 @@ void main() { node.eval = eval; }); - expect( - root.mainline.map((n) => n.eval), - equals([eval, eval, eval]), - ); + expect(root.mainline.map((n) => n.eval), equals([eval, eval, eval])); }); test('addNodeAt', () { final root = Root.fromPgnMoves('e4 e5'); - final branch = Branch( - sanMove: SanMove('Nc6', Move.parse('b8c6')!), - position: Chess.initial, + final branch = Branch(sanMove: SanMove('Nc6', Move.parse('b8c6')!), position: Chess.initial); + final (newPath, isNewNode) = root.addNodeAt( + UciPath.fromId(UciCharPair.fromUci('e2e4')), + branch, ); - final (newPath, isNewNode) = - root.addNodeAt(UciPath.fromId(UciCharPair.fromUci('e2e4')), branch); expect( newPath, - equals( - UciPath.fromIds( - IList([UciCharPair.fromUci('e2e4'), UciCharPair.fromUci('b8c6')]), - ), - ), + equals(UciPath.fromIds(IList([UciCharPair.fromUci('e2e4'), UciCharPair.fromUci('b8c6')]))), ); expect(isNewNode, isTrue); @@ -275,15 +221,8 @@ void main() { test('addNodeAt, prepend', () { final root = Root.fromPgnMoves('e4 e5'); - final branch = Branch( - sanMove: SanMove('Nc6', Move.parse('b8c6')!), - position: Chess.initial, - ); - root.addNodeAt( - UciPath.fromId(UciCharPair.fromUci('e2e4')), - branch, - prepend: true, - ); + final branch = Branch(sanMove: SanMove('Nc6', Move.parse('b8c6')!), position: Chess.initial); + root.addNodeAt(UciPath.fromId(UciCharPair.fromUci('e2e4')), branch, prepend: true); final testNode = root.nodeAt(UciPath.fromId(UciCharPair.fromUci('e2e4'))); expect(testNode.children.length, equals(2)); @@ -292,20 +231,15 @@ void main() { test('addNodeAt, with an existing node at path', () { final root = Root.fromPgnMoves('e4 e5'); - final branch = Branch( - sanMove: SanMove('e5', Move.parse('e7e5')!), - position: Chess.initial, + final branch = Branch(sanMove: SanMove('e5', Move.parse('e7e5')!), position: Chess.initial); + final (newPath, isNewNode) = root.addNodeAt( + UciPath.fromId(UciCharPair.fromUci('e2e4')), + branch, ); - final (newPath, isNewNode) = - root.addNodeAt(UciPath.fromId(UciCharPair.fromUci('e2e4')), branch); expect( newPath, - equals( - UciPath.fromIds( - IList([UciCharPair.fromUci('e2e4'), UciCharPair.fromUci('e7e5')]), - ), - ), + equals(UciPath.fromIds(IList([UciCharPair.fromUci('e2e4'), UciCharPair.fromUci('e7e5')]))), ); expect(isNewNode, isFalse); @@ -319,18 +253,9 @@ void main() { test('addNodesAt', () { final root = Root.fromPgnMoves('e4 e5'); - final branch = Branch( - sanMove: SanMove('Nc6', Move.parse('b8c6')!), - position: Chess.initial, - ); - final branch2 = Branch( - sanMove: SanMove('Na6', Move.parse('b8a6')!), - position: Chess.initial, - ); - root.addNodesAt( - UciPath.fromId(UciCharPair.fromUci('e2e4')), - [branch, branch2], - ); + final branch = Branch(sanMove: SanMove('Nc6', Move.parse('b8c6')!), position: Chess.initial); + final branch2 = Branch(sanMove: SanMove('Na6', Move.parse('b8a6')!), position: Chess.initial); + root.addNodesAt(UciPath.fromId(UciCharPair.fromUci('e2e4')), [branch, branch2]); final testNode = root.nodeAt(UciPath.fromId(UciCharPair.fromUci('e2e4'))); expect(testNode.children.length, equals(2)); @@ -341,23 +266,16 @@ void main() { test('addMoveAt', () { final root = Root.fromPgnMoves('e4 e5'); final move = Move.parse('b1c3')!; - final path = UciPath.fromIds( - [UciCharPair.fromUci('e2e4'), UciCharPair.fromUci('e7e5')].lock, - ); + final path = UciPath.fromIds([UciCharPair.fromUci('e2e4'), UciCharPair.fromUci('e7e5')].lock); final currentPath = root.mainlinePath; final (newPath, _) = root.addMoveAt(path, move); - expect( - newPath, - equals(currentPath + UciCharPair.fromMove(move)), - ); + expect(newPath, equals(currentPath + UciCharPair.fromMove(move))); final newNode = root.branchAt(newPath!); expect(newNode?.position.ply, equals(3)); expect(newNode?.sanMove, equals(SanMove('Nc3', move))); expect( newNode?.position.fen, - equals( - 'rnbqkbnr/pppp1ppp/8/4p3/4P3/2N5/PPPP1PPP/R1BQKBNR b KQkq - 1 2', - ), + equals('rnbqkbnr/pppp1ppp/8/4p3/4P3/2N5/PPPP1PPP/R1BQKBNR b KQkq - 1 2'), ); final testNode = root.nodeAt(path); @@ -365,17 +283,13 @@ void main() { expect(testNode.children.first.sanMove, equals(SanMove('Nc3', move))); expect( testNode.children.first.position.fen, - equals( - 'rnbqkbnr/pppp1ppp/8/4p3/4P3/2N5/PPPP1PPP/R1BQKBNR b KQkq - 1 2', - ), + equals('rnbqkbnr/pppp1ppp/8/4p3/4P3/2N5/PPPP1PPP/R1BQKBNR b KQkq - 1 2'), ); }); test('deleteAt', () { final root = Root.fromPgnMoves('e4 e5 Nf3'); - final path = UciPath.fromIds( - [UciCharPair.fromUci('e2e4'), UciCharPair.fromUci('e7e5')], - ); + final path = UciPath.fromIds([UciCharPair.fromUci('e2e4'), UciCharPair.fromUci('e7e5')]); root.deleteAt(path); expect(root.mainline.length, equals(1)); expect(root.mainline.last, equals(root.children.first)); @@ -396,44 +310,27 @@ void main() { root.promoteAt(path, toMainline: false); expect( root.mainline.map((n) => n.sanMove.san).toList(), - equals([ - 'e4', - 'd5', - 'exd5', - 'Nf6', - 'c4', - ]), + equals(['e4', 'd5', 'exd5', 'Nf6', 'c4']), ); expect( root.makePgn(), - equals( - '1. e4 d5 2. exd5 Nf6 ( 2... Qxd5 3. Nc3 ) 3. c4 ( 3. Nc3 ) *\n', - ), + equals('1. e4 d5 2. exd5 Nf6 ( 2... Qxd5 3. Nc3 ) 3. c4 ( 3. Nc3 ) *\n'), ); }); test('promoteAt, to mainline', () { const pgn = '1. e4 d5 2. exd5 Qxd5 (2... Nf6 3. c4 (3. Nc3)) 3. Nc3'; final root = Root.fromPgnGame(PgnGame.parsePgn(pgn)); - final path = - UciPath.fromUciMoves(['e2e4', 'd7d5', 'e4d5', 'g8f6', 'b1c3']); + final path = UciPath.fromUciMoves(['e2e4', 'd7d5', 'e4d5', 'g8f6', 'b1c3']); expect(root.nodeAt(path), isNotNull); root.promoteAt(path, toMainline: true); expect( root.mainline.map((n) => n.sanMove.san).toList(), - equals([ - 'e4', - 'd5', - 'exd5', - 'Nf6', - 'Nc3', - ]), + equals(['e4', 'd5', 'exd5', 'Nf6', 'Nc3']), ); expect( root.makePgn(), - equals( - '1. e4 d5 2. exd5 Nf6 ( 2... Qxd5 3. Nc3 ) 3. Nc3 ( 3. c4 ) *\n', - ), + equals('1. e4 d5 2. exd5 Nf6 ( 2... Qxd5 3. Nc3 ) 3. Nc3 ( 3. c4 ) *\n'), ); }); @@ -443,16 +340,8 @@ void main() { final path = UciPath.fromUciMoves(['d2d4']); expect(root.nodeAt(path), isNotNull); root.promoteAt(path, toMainline: false); - expect( - root.mainline.map((n) => n.sanMove.san).toList(), - equals(['d4']), - ); - expect( - root.makePgn(), - equals( - '1. d4 ( 1. e4 ) *\n', - ), - ); + expect(root.mainline.map((n) => n.sanMove.san).toList(), equals(['d4'])); + expect(root.makePgn(), equals('1. d4 ( 1. e4 ) *\n')); }); group('merge', () { @@ -538,15 +427,9 @@ void main() { expect(node1.clock, equals(node2.clock)); } // one new external eval - expect( - root2.mainline.where((n) => n.externalEval != null).length, - equals(1), - ); + expect(root2.mainline.where((n) => n.externalEval != null).length, equals(1)); // one old client eval preseved - expect( - root2.mainline.where((node) => node.eval != null).length, - equals(1), - ); + expect(root2.mainline.where((node) => node.eval != null).length, equals(1)); expect( root2.mainline.firstWhereOrNull((node) => node.eval != null)?.eval, equals(clientEval), @@ -576,19 +459,11 @@ void main() { } test('e1g1 -> e1h1', () { - makeTestAltCastlingMove( - '1. e4 e5 2. Nf3 Nf6 3. Bc4 Bc5 4. O-O', - 'e1g1', - 'e1h1', - ); + makeTestAltCastlingMove('1. e4 e5 2. Nf3 Nf6 3. Bc4 Bc5 4. O-O', 'e1g1', 'e1h1'); }); test('e8g8 -> e8h8', () { - makeTestAltCastlingMove( - '1. e4 e5 2. Nf3 Nf6 3. Bc4 Bc5 4. O-O O-O', - 'e8g8', - 'e8h8', - ); + makeTestAltCastlingMove('1. e4 e5 2. Nf3 Nf6 3. Bc4 Bc5 4. O-O O-O', 'e8g8', 'e8h8'); }); test('e1c1 -> e1a1', () { @@ -607,8 +482,7 @@ void main() { ); }); test('only convert king moves in altCastlingMove', () { - const pgn = - '1. e4 e5 2. Bc4 Qh4 3. Nf3 Qxh2 4. Ke2 Qxh1 5. Qe1 Qh5 6. Qh1'; + const pgn = '1. e4 e5 2. Bc4 Qh4 3. Nf3 Qxh2 4. Ke2 Qxh1 5. Qe1 Qh5 6. Qh1'; final root = Root.fromPgnGame(PgnGame.parsePgn(pgn)); final initialPng = root.makePgn(); final previousUciPath = root.mainlinePath.penultimate; @@ -617,11 +491,8 @@ void main() { expect(root.makePgn(), isNot(initialPng)); }); - test( - 'do not convert castling move if rook is on the alternative castling square', - () { - const pgn = - '[FEN "rnbqkbnr/pppppppp/8/8/8/2NBQ3/PPPPPPPP/2R1KBNR w KQkq - 0 1"]'; + test('do not convert castling move if rook is on the alternative castling square', () { + const pgn = '[FEN "rnbqkbnr/pppppppp/8/8/8/2NBQ3/PPPPPPPP/2R1KBNR w KQkq - 0 1"]'; final root = Root.fromPgnGame(PgnGame.parsePgn(pgn)); final initialPng = root.makePgn(); final previousUciPath = root.mainlinePath.penultimate; diff --git a/test/model/common/service/fake_sound_service.dart b/test/model/common/service/fake_sound_service.dart index 12f09936a0..d6b8ccafe0 100644 --- a/test/model/common/service/fake_sound_service.dart +++ b/test/model/common/service/fake_sound_service.dart @@ -6,10 +6,7 @@ class FakeSoundService implements SoundService { Future play(Sound sound) async {} @override - Future changeTheme( - SoundTheme theme, { - bool playSound = false, - }) async {} + Future changeTheme(SoundTheme theme, {bool playSound = false}) async {} @override Future release() async {} diff --git a/test/model/common/time_increment_test.dart b/test/model/common/time_increment_test.dart index 84839b3904..022c615a08 100644 --- a/test/model/common/time_increment_test.dart +++ b/test/model/common/time_increment_test.dart @@ -30,10 +30,7 @@ void main() { }); test('Estimated Duration', () { - expect( - const TimeIncrement(300, 5).estimatedDuration, - const Duration(seconds: 300 + 5 * 40), - ); + expect(const TimeIncrement(300, 5).estimatedDuration, const Duration(seconds: 300 + 5 * 40)); expect(const TimeIncrement(0, 0).estimatedDuration, Duration.zero); }); diff --git a/test/model/correspondence/correspondence_game_storage_test.dart b/test/model/correspondence/correspondence_game_storage_test.dart index 58e901f34c..9e5d10a527 100644 --- a/test/model/correspondence/correspondence_game_storage_test.dart +++ b/test/model/correspondence/correspondence_game_storage_test.dart @@ -20,16 +20,10 @@ void main() { test('save and fetch data', () async { final container = await makeContainer(); - final storage = - await container.read(correspondenceGameStorageProvider.future); + final storage = await container.read(correspondenceGameStorageProvider.future); await storage.save(corresGame); - expect( - storage.fetch( - gameId: gameId, - ), - completion(equals(corresGame)), - ); + expect(storage.fetch(gameId: gameId), completion(equals(corresGame))); }); }); } @@ -62,9 +56,7 @@ final corresGame = OfflineCorrespondenceGame( variant: Variant.standard, ), fullId: const GameFullId('g2bzFol8fgty'), - steps: _makeSteps( - 'e4 Nc6 Bc4 e6 a3 g6 Nf3 Bg7 c3 Nge7 d3 O-O Be3 Na5 Ba2 b6 Qd2', - ), + steps: _makeSteps('e4 Nc6 Bc4 e6 a3 g6 Nf3 Bg7 c3 Nge7 d3 O-O Be3 Na5 Ba2 b6 Qd2'), clock: const CorrespondenceClockData( white: Duration(days: 2, hours: 23, minutes: 59), black: Duration(days: 3), @@ -74,20 +66,8 @@ final corresGame = OfflineCorrespondenceGame( variant: Variant.standard, speed: Speed.correspondence, perf: Perf.classical, - white: const Player( - user: LightUser( - id: UserId('whiteId'), - name: 'White', - ), - rating: 1500, - ), - black: const Player( - user: LightUser( - id: UserId('blackId'), - name: 'Black', - ), - rating: 1500, - ), + white: const Player(user: LightUser(id: UserId('whiteId'), name: 'White'), rating: 1500), + black: const Player(user: LightUser(id: UserId('blackId'), name: 'Black'), rating: 1500), youAre: Side.white, daysPerTurn: 3, ); diff --git a/test/model/game/game_repository_test.dart b/test/model/game/game_repository_test.dart index 176bdb07df..5db4f3c474 100644 --- a/test/model/game/game_repository_test.dart +++ b/test/model/game/game_repository_test.dart @@ -46,11 +46,7 @@ void main() { {"id":"9WLmxmiB","rated":true,"variant":"standard","speed":"blitz","perf":"blitz","createdAt":1673553299064,"lastMoveAt":1673553615438,"status":"resign","players":{"white":{"user":{"name":"Dr-Alaakour","id":"dr-alaakour"},"rating":1806,"ratingDiff":5},"black":{"user":{"name":"Thibault","patron":true,"id":"thibault"},"rating":1772,"ratingDiff":-5}},"winner":"white","clock":{"initial":180,"increment":0,"totalTime":180},"lastFen":"2b1Q1k1/p1r4p/1p2p1p1/3pN3/2qP4/P4R2/1P3PPP/4R1K1 b - - 0 1"} '''; - final ids = ISet(const { - GameId('Huk88k3D'), - GameId('g2bzFol8'), - GameId('9WLmxmiB'), - }); + final ids = ISet(const {GameId('Huk88k3D'), GameId('g2bzFol8'), GameId('9WLmxmiB')}); final mockClient = MockClient((request) { if (request.url.path == '/api/games/export/_ids') { diff --git a/test/model/game/game_socket_events_test.dart b/test/model/game/game_socket_events_test.dart index 3bbae4e59b..a61db74930 100644 --- a/test/model/game/game_socket_events_test.dart +++ b/test/model/game/game_socket_events_test.dart @@ -14,19 +14,10 @@ void main() { final fullEvent = GameFullEvent.fromJson(json); final game = fullEvent.game; expect(game.id, const GameId('nV3DaALy')); - expect( - game.clock?.running, - true, - ); - expect( - game.clock?.white, - const Duration(seconds: 149, milliseconds: 50), - ); + expect(game.clock?.running, true); + expect(game.clock?.white, const Duration(seconds: 149, milliseconds: 50)); - expect( - game.clock?.black, - const Duration(seconds: 775, milliseconds: 940), - ); + expect(game.clock?.black, const Duration(seconds: 775, milliseconds: 940)); expect( game.meta, GameMeta( diff --git a/test/model/game/game_socket_example_data.dart b/test/model/game/game_socket_example_data.dart index f841c65cd5..272907e58f 100644 --- a/test/model/game/game_socket_example_data.dart +++ b/test/model/game/game_socket_example_data.dart @@ -1,20 +1,17 @@ import 'package:dartchess/dartchess.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; -typedef FullEventTestClock = ({ - bool running, - Duration initial, - Duration increment, - Duration? emerg, - Duration white, - Duration black, -}); +typedef FullEventTestClock = + ({ + bool running, + Duration initial, + Duration increment, + Duration? emerg, + Duration white, + Duration black, + }); -typedef FullEventTestCorrespondenceClock = ({ - Duration white, - Duration black, - int daysPerTurn, -}); +typedef FullEventTestCorrespondenceClock = ({Duration white, Duration black, int daysPerTurn}); String makeFullEvent( GameId id, @@ -34,8 +31,9 @@ String makeFullEvent( FullEventTestCorrespondenceClock? correspondenceClock, }) { final youAreStr = youAre != null ? '"youAre": "${youAre.name}",' : ''; - final clockStr = clock != null - ? ''' + final clockStr = + clock != null + ? ''' "clock": { "running": ${clock.running}, "initial": ${clock.initial.inSeconds}, @@ -46,17 +44,18 @@ String makeFullEvent( "moretime": 15 }, ''' - : ''; + : ''; - final correspondenceClockStr = correspondenceClock != null - ? ''' + final correspondenceClockStr = + correspondenceClock != null + ? ''' "correspondence": { "daysPerTurn": ${correspondenceClock.daysPerTurn}, "white": ${(correspondenceClock.white.inMilliseconds / 1000).toStringAsFixed(2)}, "black": ${(correspondenceClock.black.inMilliseconds / 1000).toStringAsFixed(2)} }, ''' - : ''; + : ''; return ''' { diff --git a/test/model/game/game_storage_test.dart b/test/model/game/game_storage_test.dart index acf60a92a2..201dd428b1 100644 --- a/test/model/game/game_storage_test.dart +++ b/test/model/game/game_storage_test.dart @@ -23,12 +23,7 @@ void main() { final storage = await container.read(gameStorageProvider.future); await storage.save(game); - expect( - storage.fetch( - gameId: gameId, - ), - completion(equals(game)), - ); + expect(storage.fetch(gameId: gameId), completion(equals(game))); }); test('paginate games', () async { @@ -46,11 +41,7 @@ void main() { expect(page1.length, 10); expect(page1.last.game.id, const GameId('game0090')); - final page2 = await storage.page( - userId: userId, - max: 10, - until: page1.last.lastModified, - ); + final page2 = await storage.page(userId: userId, max: 10, until: page1.last.lastModified); expect(page2.length, 10); expect(page2.last.game.id, const GameId('game0080')); }); @@ -94,43 +85,14 @@ final game = ArchivedGame( speed: Speed.blitz, rated: true, status: GameStatus.started, - white: const Player( - user: LightUser( - id: UserId('whiteId'), - name: 'White', - ), - rating: 1500, - ), - black: const Player( - user: LightUser( - id: UserId('blackId'), - name: 'Black', - ), - rating: 1500, - ), - clock: ( - initial: const Duration(minutes: 2), - increment: const Duration(seconds: 3), - ), - ), - steps: _makeSteps( - 'e4 Nc6 Bc4 e6 a3 g6 Nf3 Bg7 c3 Nge7 d3 O-O Be3 Na5 Ba2 b6 Qd2', + white: const Player(user: LightUser(id: UserId('whiteId'), name: 'White'), rating: 1500), + black: const Player(user: LightUser(id: UserId('blackId'), name: 'Black'), rating: 1500), + clock: (initial: const Duration(minutes: 2), increment: const Duration(seconds: 3)), ), + steps: _makeSteps('e4 Nc6 Bc4 e6 a3 g6 Nf3 Bg7 c3 Nge7 d3 O-O Be3 Na5 Ba2 b6 Qd2'), status: GameStatus.started, - white: const Player( - user: LightUser( - id: UserId('whiteId'), - name: 'White', - ), - rating: 1500, - ), - black: const Player( - user: LightUser( - id: UserId('blackId'), - name: 'Black', - ), - rating: 1500, - ), + white: const Player(user: LightUser(id: UserId('whiteId'), name: 'White'), rating: 1500), + black: const Player(user: LightUser(id: UserId('blackId'), name: 'Black'), rating: 1500), youAre: Side.white, ); @@ -155,43 +117,14 @@ final games = List.generate(100, (index) { speed: Speed.blitz, rated: true, status: GameStatus.started, - white: const Player( - user: LightUser( - id: UserId('whiteId'), - name: 'White', - ), - rating: 1500, - ), - black: const Player( - user: LightUser( - id: UserId('blackId'), - name: 'Black', - ), - rating: 1500, - ), - clock: ( - initial: const Duration(minutes: 2), - increment: const Duration(seconds: 3), - ), - ), - steps: _makeSteps( - 'e4 Nc6 Bc4 e6 a3 g6 Nf3 Bg7 c3 Nge7 d3 O-O Be3 Na5 Ba2 b6 Qd2', + white: const Player(user: LightUser(id: UserId('whiteId'), name: 'White'), rating: 1500), + black: const Player(user: LightUser(id: UserId('blackId'), name: 'Black'), rating: 1500), + clock: (initial: const Duration(minutes: 2), increment: const Duration(seconds: 3)), ), + steps: _makeSteps('e4 Nc6 Bc4 e6 a3 g6 Nf3 Bg7 c3 Nge7 d3 O-O Be3 Na5 Ba2 b6 Qd2'), status: GameStatus.started, - white: const Player( - user: LightUser( - id: UserId('whiteId'), - name: 'White', - ), - rating: 1500, - ), - black: const Player( - user: LightUser( - id: UserId('blackId'), - name: 'Black', - ), - rating: 1500, - ), + white: const Player(user: LightUser(id: UserId('whiteId'), name: 'White'), rating: 1500), + black: const Player(user: LightUser(id: UserId('blackId'), name: 'Black'), rating: 1500), youAre: Side.white, ); }); diff --git a/test/model/game/game_test.dart b/test/model/game/game_test.dart index e67e44c719..6fdd980b27 100644 --- a/test/model/game/game_test.dart +++ b/test/model/game/game_test.dart @@ -11,9 +11,7 @@ void main() { jsonDecode(_unfinishedGameJson) as Map, ); - expect( - game.makePgn(), - ''' + expect(game.makePgn(), ''' [Event "Rated Bullet game"] [Site "https://lichess.dev/Fn9UvVKF"] [Date "2024.01.25"] @@ -26,8 +24,7 @@ void main() { [TimeControl "120+1"] * -''', - ); +'''); }); test('makePgn, finished game', () { @@ -35,9 +32,7 @@ void main() { jsonDecode(_playableGameJson) as Map, ); - expect( - game.makePgn(), - ''' + expect(game.makePgn(), ''' [Event "Rated Bullet game"] [Site "https://lichess.dev/CCW6EEru"] [Date "2024.01.25"] @@ -52,15 +47,12 @@ void main() { [TimeControl "120+1"] 1. e4 e5 2. Nf3 Nc6 3. Bc4 Bc5 4. b4 Bxb4 5. c3 Ba5 6. d4 Bb6 7. Ba3 Nf6 8. Qb3 d6 9. Bxf7+ Kf8 10. O-O Qe7 11. Nxe5 Nxe5 12. dxe5 Be6 13. Bxe6 Nxe4 14. Re1 Nc5 15. Bxc5 Bxc5 16. Qxb7 Re8 17. Bh3 dxe5 18. Qf3+ Kg8 19. Nd2 Rf8 20. Qd5+ Rf7 21. Be6 Qxe6 22. Qxe6 1-0 -''', - ); +'''); }); test('toArchivedGame', () { for (final game in [_playableGameJson, _playable960GameJson]) { - final playableGame = PlayableGame.fromServerJson( - jsonDecode(game) as Map, - ); + final playableGame = PlayableGame.fromServerJson(jsonDecode(game) as Map); final now = DateTime.now(); final archivedGame = playableGame.toArchivedGame(finishedAt: now); @@ -83,16 +75,13 @@ void main() { archivedGame.data.clock, playableGame.meta.clock != null ? ( - initial: playableGame.meta.clock!.initial, - increment: playableGame.meta.clock!.increment, - ) + initial: playableGame.meta.clock!.initial, + increment: playableGame.meta.clock!.increment, + ) : null, ); expect(archivedGame.initialFen, playableGame.initialFen); - expect( - archivedGame.isThreefoldRepetition, - playableGame.isThreefoldRepetition, - ); + expect(archivedGame.isThreefoldRepetition, playableGame.isThreefoldRepetition); expect(archivedGame.status, playableGame.status); expect(archivedGame.winner, playableGame.winner); expect(archivedGame.white, playableGame.white); @@ -110,9 +99,7 @@ void main() { final game = ArchivedGame.fromServerJson( jsonDecode(_archivedGameJsonNoEvals) as Map, ); - expect( - game.makePgn(), - ''' + expect(game.makePgn(), ''' [Event "Rated Bullet game"] [Site "https://lichess.dev/CCW6EEru"] [Date "2024.01.25"] @@ -129,17 +116,14 @@ void main() { [Opening "Italian Game: Evans Gambit, Main Line"] 1. e4 { [%clk 0:02:00.03] } e5 { [%clk 0:02:00.03] } 2. Nf3 { [%emt 0:00:02.2] [%clk 0:01:58.83] } Nc6 { [%emt 0:00:02.92] [%clk 0:01:58.11] } 3. Bc4 { [%emt 0:00:03] [%clk 0:01:56.83] } Bc5 { [%emt 0:00:05.32] [%clk 0:01:53.79] } 4. b4 { [%emt 0:00:04.76] [%clk 0:01:53.07] } Bxb4 { [%emt 0:00:03.16] [%clk 0:01:51.63] } 5. c3 { [%emt 0:00:03.64] [%clk 0:01:50.43] } Ba5 { [%emt 0:00:02.2] [%clk 0:01:50.43] } 6. d4 { [%emt 0:00:02.44] [%clk 0:01:48.99] } Bb6 { [%emt 0:00:04.36] [%clk 0:01:47.07] } 7. Ba3 { [%emt 0:00:08.44] [%clk 0:01:41.55] } Nf6 { [%emt 0:00:03.24] [%clk 0:01:44.83] } 8. Qb3 { [%emt 0:00:02.36] [%clk 0:01:40.19] } d6 { [%emt 0:00:05.88] [%clk 0:01:39.95] } 9. Bxf7+ { [%emt 0:00:04.84] [%clk 0:01:36.35] } Kf8 { [%emt 0:00:01.72] [%clk 0:01:39.23] } 10. O-O { [%emt 0:00:07.72] [%clk 0:01:29.63] } Qe7 { [%emt 0:00:14.2] [%clk 0:01:26.03] } 11. Nxe5 { [%emt 0:00:11.48] [%clk 0:01:19.15] } Nxe5 { [%emt 0:00:04.2] [%clk 0:01:22.83] } 12. dxe5 { [%emt 0:00:02.52] [%clk 0:01:17.63] } Be6 { [%emt 0:00:09.24] [%clk 0:01:14.59] } 13. Bxe6 { [%emt 0:00:04.84] [%clk 0:01:13.79] } Nxe4 { [%emt 0:00:14.76] [%clk 0:01:00.83] } 14. Re1 { [%emt 0:00:08.92] [%clk 0:01:05.87] } Nc5 { [%emt 0:00:03.64] [%clk 0:00:58.19] } 15. Bxc5 { [%emt 0:00:03.24] [%clk 0:01:03.63] } Bxc5 { [%emt 0:00:02.68] [%clk 0:00:56.51] } 16. Qxb7 { [%emt 0:00:03.88] [%clk 0:01:00.75] } Re8 { [%emt 0:00:02.44] [%clk 0:00:55.07] } 17. Bh3 { [%emt 0:00:05] [%clk 0:00:56.75] } dxe5 { [%emt 0:00:08.04] [%clk 0:00:48.03] } 18. Qf3+ { [%emt 0:00:07.16] [%clk 0:00:50.59] } Kg8 { [%emt 0:00:03.88] [%clk 0:00:45.15] } 19. Nd2 { [%emt 0:00:06.12] [%clk 0:00:45.47] } Rf8 { [%emt 0:00:10.6] [%clk 0:00:35.55] } 20. Qd5+ { [%emt 0:00:06.76] [%clk 0:00:39.71] } Rf7 { [%emt 0:00:02.44] [%clk 0:00:34.11] } 21. Be6 { [%emt 0:00:08.36] [%clk 0:00:32.35] } Qxe6 { [%emt 0:00:03.88] [%clk 0:00:31.23] } 22. Qxe6 { [%emt 0:00:02.15] [%clk 0:00:31.2] } 1-0 -''', - ); +'''); }); test('makePgn, with evals and clocks', () { final game = ArchivedGame.fromServerJson( jsonDecode(_archivedGameJson) as Map, ); - expect( - game.makePgn(), - ''' + expect(game.makePgn(), ''' [Event "Rated Bullet game"] [Site "https://lichess.dev/CCW6EEru"] [Date "2024.01.25"] @@ -156,8 +140,7 @@ void main() { [Opening "Italian Game: Evans Gambit, Main Line"] 1. e4 { [%eval 0.32] [%clk 0:02:00.03] } e5 { [%eval 0.41] [%clk 0:02:00.03] } 2. Nf3 { [%eval 0.39] [%emt 0:00:02.2] [%clk 0:01:58.83] } Nc6 { [%eval 0.20] [%emt 0:00:02.92] [%clk 0:01:58.11] } 3. Bc4 { [%eval 0.17] [%emt 0:00:03] [%clk 0:01:56.83] } Bc5 { [%eval 0.21] [%emt 0:00:05.32] [%clk 0:01:53.79] } 4. b4 { [%eval -0.21] [%emt 0:00:04.76] [%clk 0:01:53.07] } Bxb4 { [%eval -0.14] [%emt 0:00:03.16] [%clk 0:01:51.63] } 5. c3 { [%eval -0.23] [%emt 0:00:03.64] [%clk 0:01:50.43] } Ba5 { [%eval -0.24] [%emt 0:00:02.2] [%clk 0:01:50.43] } 6. d4 { [%eval -0.24] [%emt 0:00:02.44] [%clk 0:01:48.99] } Bb6 \$6 { Inaccuracy. d6 was best. [%eval 0.52] [%emt 0:00:04.36] [%clk 0:01:47.07] } ( 6... d6 ) 7. Ba3 \$6 { Inaccuracy. Nxe5 was best. [%eval -0.56] [%emt 0:00:08.44] [%clk 0:01:41.55] } ( 7. Nxe5 ) 7... Nf6 \$4 { Blunder. d6 was best. [%eval 1.77] [%emt 0:00:03.24] [%clk 0:01:44.83] } ( 7... d6 ) 8. Qb3 \$4 { Blunder. dxe5 was best. [%eval -0.19] [%emt 0:00:02.36] [%clk 0:01:40.19] } ( 8. dxe5 Ng4 9. Qd5 Nh6 10. Nbd2 Ne7 11. Qd3 O-O 12. h3 d6 13. g4 Kh8 14. exd6 cxd6 ) 8... d6 { [%eval -0.16] [%emt 0:00:05.88] [%clk 0:01:39.95] } 9. Bxf7+ { [%eval -0.20] [%emt 0:00:04.84] [%clk 0:01:36.35] } Kf8 { [%eval -0.12] [%emt 0:00:01.72] [%clk 0:01:39.23] } 10. O-O \$2 { Mistake. Bd5 was best. [%eval -1.45] [%emt 0:00:07.72] [%clk 0:01:29.63] } ( 10. Bd5 Nxd5 ) 10... Qe7 \$4 { Blunder. Na5 was best. [%eval 0.72] [%emt 0:00:14.2] [%clk 0:01:26.03] } ( 10... Na5 11. Qd1 Kxf7 12. dxe5 dxe5 13. Nxe5+ Ke8 14. Nd2 Be6 15. Qa4+ Bd7 16. Qd1 Nc6 17. Ndc4 ) 11. Nxe5 \$6 { Inaccuracy. Bd5 was best. [%eval -0.36] [%emt 0:00:11.48] [%clk 0:01:19.15] } ( 11. Bd5 Nxd5 12. exd5 Na5 13. Qb4 exd4 14. cxd4 Kg8 15. Re1 Qf7 16. Ng5 Qg6 17. Nc3 h6 ) 11... Nxe5 { [%eval -0.41] [%emt 0:00:04.2] [%clk 0:01:22.83] } 12. dxe5 { [%eval -0.42] [%emt 0:00:02.52] [%clk 0:01:17.63] } Be6 \$4 { Blunder. Qxf7 was best. [%eval 5.93] [%emt 0:00:09.24] [%clk 0:01:14.59] } ( 12... Qxf7 13. exf6 gxf6 14. c4 Rg8 15. Nd2 Qh5 16. c5 Bh3 17. g3 Bxc5 18. Bxc5 dxc5 19. Rfe1 ) 13. Bxe6 { [%eval 5.89] [%emt 0:00:04.84] [%clk 0:01:13.79] } Nxe4 { [%eval 6.30] [%emt 0:00:14.76] [%clk 0:01:00.83] } 14. Re1 \$4 { Blunder. exd6 was best. [%eval -0.32] [%emt 0:00:08.92] [%clk 0:01:05.87] } ( 14. exd6 cxd6 15. Bd5 Nxf2 16. Nd2 g5 17. Nc4 Kg7 18. Nxb6 Qe3 19. Rxf2 Rhf8 20. Bf3 axb6 ) 14... Nc5 \$4 { Blunder. Bxf2+ was best. [%eval 6.02] [%emt 0:00:03.64] [%clk 0:00:58.19] } ( 14... Bxf2+ ) 15. Bxc5 { [%eval 5.81] [%emt 0:00:03.24] [%clk 0:01:03.63] } Bxc5 { [%eval 6.56] [%emt 0:00:02.68] [%clk 0:00:56.51] } 16. Qxb7 { [%eval 6.62] [%emt 0:00:03.88] [%clk 0:01:00.75] } Re8 \$4 { Checkmate is now unavoidable. g6 was best. [%eval #15] [%emt 0:00:02.44] [%clk 0:00:55.07] } ( 16... g6 17. Qxa8+ Kg7 18. Qd5 c6 19. Qb3 Rf8 20. Re2 Qh4 21. Qb7+ Kh6 22. Qb2 dxe5 23. Nd2 ) 17. Bh3 \$4 { Lost forced checkmate sequence. Qf3+ was best. [%eval 5.66] [%emt 0:00:05] [%clk 0:00:56.75] } ( 17. Qf3+ Qf6 18. exf6 g6 19. f7 Kg7 20. fxe8=Q Rxe8 21. Qf7+ Kh6 22. Qxe8 d5 23. g4 c6 ) 17... dxe5 { [%eval 5.74] [%emt 0:00:08.04] [%clk 0:00:48.03] } 18. Qf3+ { [%eval 5.66] [%emt 0:00:07.16] [%clk 0:00:50.59] } Kg8 { [%eval 5.80] [%emt 0:00:03.88] [%clk 0:00:45.15] } 19. Nd2 { [%eval 5.69] [%emt 0:00:06.12] [%clk 0:00:45.47] } Rf8 \$6 { Inaccuracy. g6 was best. [%eval 7.74] [%emt 0:00:10.6] [%clk 0:00:35.55] } ( 19... g6 20. Ne4 Kg7 21. Qe2 Rd8 22. a4 h5 23. Rad1 Rxd1 24. Rxd1 Bb6 25. Rd7 Qxd7 26. Bxd7 ) 20. Qd5+ { [%eval 7.39] [%emt 0:00:06.76] [%clk 0:00:39.71] } Rf7 { [%eval 7.43] [%emt 0:00:02.44] [%clk 0:00:34.11] } 21. Be6 { [%eval 6.15] [%emt 0:00:08.36] [%clk 0:00:32.35] } Qxe6 \$6 { Inaccuracy. Bxf2+ was best. [%eval 9.34] [%emt 0:00:03.88] [%clk 0:00:31.23] } ( 21... Bxf2+ 22. Kh1 Bxe1 23. Rxe1 g6 24. Rf1 Kg7 25. Rxf7+ Qxf7 26. Bxf7 Rf8 27. Be6 e4 28. Qxe4 ) 22. Qxe6 { [%eval 8.61] [%emt 0:00:02.15] [%clk 0:00:31.2] } 1-0 -''', - ); +'''); }); }); } diff --git a/test/model/game/material_diff_test.dart b/test/model/game/material_diff_test.dart index a6391a6b18..264f25b582 100644 --- a/test/model/game/material_diff_test.dart +++ b/test/model/game/material_diff_test.dart @@ -6,8 +6,7 @@ import 'package:lichess_mobile/src/model/game/material_diff.dart'; void main() { group('GameMaterialDiff', () { test('generation from board', () { - final Board board = - Board.parseFen('r5k1/3Q1pp1/2p4p/4P1b1/p3R3/3P4/6PP/R5K1'); + final Board board = Board.parseFen('r5k1/3Q1pp1/2p4p/4P1b1/p3R3/3P4/6PP/R5K1'); final MaterialDiff diff = MaterialDiff.fromBoard(board); expect(diff.bySide(Side.black).score, equals(-10)); diff --git a/test/model/notifications/fake_notification_display.dart b/test/model/notifications/fake_notification_display.dart index 47484acfa6..b4332ca865 100644 --- a/test/model/notifications/fake_notification_display.dart +++ b/test/model/notifications/fake_notification_display.dart @@ -1,8 +1,7 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_test/flutter_test.dart'; -class FakeNotificationDisplay extends Fake - implements FlutterLocalNotificationsPlugin { +class FakeNotificationDisplay extends Fake implements FlutterLocalNotificationsPlugin { final Map _activeNotifications = {}; @override diff --git a/test/model/notifications/notification_service_test.dart b/test/model/notifications/notification_service_test.dart index 472455172a..776d059fbe 100644 --- a/test/model/notifications/notification_service_test.dart +++ b/test/model/notifications/notification_service_test.dart @@ -18,8 +18,7 @@ import '../../test_container.dart'; import '../../test_helpers.dart'; import '../auth/fake_session_storage.dart'; -class NotificationDisplayMock extends Mock - implements FlutterLocalNotificationsPlugin {} +class NotificationDisplayMock extends Mock implements FlutterLocalNotificationsPlugin {} class CorrespondenceServiceMock extends Mock implements CorrespondenceService {} @@ -59,56 +58,49 @@ void main() { await notificationService.start(); - final calls = - testBinding.firebaseMessaging.verifyRequestPermissionCalls(); + final calls = testBinding.firebaseMessaging.verifyRequestPermissionCalls(); expect(calls, hasLength(1)); expect( calls.first, - equals( - ( - alert: true, - badge: true, - sound: true, - announcement: false, - carPlay: false, - criticalAlert: false, - provisional: false, - ), - ), + equals(( + alert: true, + badge: true, + sound: true, + announcement: false, + carPlay: false, + criticalAlert: false, + provisional: false, + )), ); }); test( - 'register device when online, token exists and permissions are granted and a session exists', - () async { - final container = await makeContainer( - userSession: fakeSession, - overrides: [ - lichessClientProvider.overrideWith( - (ref) => LichessClient(registerMockClient, ref), - ), - ], - ); + 'register device when online, token exists and permissions are granted and a session exists', + () async { + final container = await makeContainer( + userSession: fakeSession, + overrides: [ + lichessClientProvider.overrideWith((ref) => LichessClient(registerMockClient, ref)), + ], + ); - final notificationService = container.read(notificationServiceProvider); + final notificationService = container.read(notificationServiceProvider); - FakeAsync().run((async) { - notificationService.start(); + FakeAsync().run((async) { + notificationService.start(); - async.flushMicrotasks(); + async.flushMicrotasks(); - expect(registerDeviceCalls, 1); - }); - }); + expect(registerDeviceCalls, 1); + }); + }, + ); - test("don't try to register device when permissions are not granted", - () async { + test("don't try to register device when permissions are not granted", () async { final container = await makeContainer( userSession: fakeSession, overrides: [ - lichessClientProvider.overrideWith( - (ref) => LichessClient(registerMockClient, ref), - ), + lichessClientProvider.overrideWith((ref) => LichessClient(registerMockClient, ref)), ], ); @@ -128,9 +120,7 @@ void main() { test("don't try to register device when user is not logged in", () async { final container = await makeContainer( overrides: [ - lichessClientProvider.overrideWith( - (ref) => LichessClient(registerMockClient, ref), - ), + lichessClientProvider.overrideWith((ref) => LichessClient(registerMockClient, ref)), ], ); @@ -147,16 +137,12 @@ void main() { }); group('Correspondence game update notifications', () { - test('FCM message with associated notification will show it in foreground', - () async { + test('FCM message with associated notification will show it in foreground', () async { final container = await makeContainer( userSession: fakeSession, overrides: [ - lichessClientProvider.overrideWith( - (ref) => LichessClient(registerMockClient, ref), - ), - notificationDisplayProvider - .overrideWith((_) => notificationDisplayMock), + lichessClientProvider.overrideWith((ref) => LichessClient(registerMockClient, ref)), + notificationDisplayProvider.overrideWith((_) => notificationDisplayMock), ], ); @@ -181,10 +167,7 @@ void main() { testBinding.firebaseMessaging.onMessage.add( const RemoteMessage( - data: { - 'lichess.type': 'gameMove', - 'lichess.fullId': '9wlmxmibr9gh', - }, + data: {'lichess.type': 'gameMove', 'lichess.fullId': '9wlmxmibr9gh'}, notification: RemoteNotification( title: 'It is your turn!', body: 'Dr-Alaakour played a move', @@ -214,16 +197,8 @@ void main() { expect( result.captured[0], isA() - .having( - (d) => d.android?.importance, - 'importance', - Importance.high, - ) - .having( - (d) => d.android?.priority, - 'priority', - Priority.defaultPriority, - ), + .having((d) => d.android?.importance, 'importance', Importance.high) + .having((d) => d.android?.priority, 'priority', Priority.defaultPriority), ); }); }); @@ -232,13 +207,9 @@ void main() { final container = await makeContainer( userSession: fakeSession, overrides: [ - lichessClientProvider.overrideWith( - (ref) => LichessClient(registerMockClient, ref), - ), - notificationDisplayProvider - .overrideWith((_) => notificationDisplayMock), - correspondenceServiceProvider - .overrideWith((_) => correspondenceServiceMock), + lichessClientProvider.overrideWith((ref) => LichessClient(registerMockClient, ref)), + notificationDisplayProvider.overrideWith((_) => notificationDisplayMock), + correspondenceServiceProvider.overrideWith((_) => correspondenceServiceMock), ], ); @@ -310,13 +281,9 @@ void main() { final container = await makeContainer( userSession: fakeSession, overrides: [ - lichessClientProvider.overrideWith( - (ref) => LichessClient(registerMockClient, ref), - ), - notificationDisplayProvider - .overrideWith((_) => notificationDisplayMock), - correspondenceServiceProvider - .overrideWith((_) => correspondenceServiceMock), + lichessClientProvider.overrideWith((ref) => LichessClient(registerMockClient, ref)), + notificationDisplayProvider.overrideWith((_) => notificationDisplayMock), + correspondenceServiceProvider.overrideWith((_) => correspondenceServiceMock), ], ); diff --git a/test/model/puzzle/puzzle_batch_storage_test.dart b/test/model/puzzle/puzzle_batch_storage_test.dart index 0f493f97be..8ee1623183 100644 --- a/test/model/puzzle/puzzle_batch_storage_test.dart +++ b/test/model/puzzle/puzzle_batch_storage_test.dart @@ -21,17 +21,10 @@ void main() { final storage = await container.read(puzzleBatchStorageProvider.future); - await storage.save( - userId: null, - angle: const PuzzleTheme(PuzzleThemeKey.mix), - data: data, - ); + await storage.save(userId: null, angle: const PuzzleTheme(PuzzleThemeKey.mix), data: data); expect( - storage.fetch( - userId: null, - angle: const PuzzleTheme(PuzzleThemeKey.mix), - ), + storage.fetch(userId: null, angle: const PuzzleTheme(PuzzleThemeKey.mix)), completion(equals(data)), ); }); @@ -41,11 +34,7 @@ void main() { final storage = await container.read(puzzleBatchStorageProvider.future); - await storage.save( - userId: null, - angle: const PuzzleTheme(PuzzleThemeKey.mix), - data: data, - ); + await storage.save(userId: null, angle: const PuzzleTheme(PuzzleThemeKey.mix), data: data); await storage.save( userId: null, angle: const PuzzleTheme(PuzzleThemeKey.rookEndgame), @@ -76,27 +65,12 @@ void main() { final storage = await container.read(puzzleBatchStorageProvider.future); - await storage.save( - userId: null, - angle: const PuzzleOpening('test_opening'), - data: data, - ); - await storage.save( - userId: null, - angle: const PuzzleOpening('test_opening2'), - data: data, - ); + await storage.save(userId: null, angle: const PuzzleOpening('test_opening'), data: data); + await storage.save(userId: null, angle: const PuzzleOpening('test_opening2'), data: data); expect( storage.fetchSavedOpenings(userId: null), - completion( - equals( - IMap(const { - 'test_opening': 1, - 'test_opening2': 1, - }), - ), - ), + completion(equals(IMap(const {'test_opening': 1, 'test_opening2': 1}))), ); }); @@ -107,38 +81,18 @@ void main() { final storage = await container.read(puzzleBatchStorageProvider.future); Future save(PuzzleAngle angle, PuzzleBatch data, String timestamp) { - return database.insert( - 'puzzle_batchs', - { - 'userId': '**anon**', - 'angle': angle.key, - 'data': jsonEncode(data.toJson()), - 'lastModified': timestamp, - }, - conflictAlgorithm: ConflictAlgorithm.replace, - ); + return database.insert('puzzle_batchs', { + 'userId': '**anon**', + 'angle': angle.key, + 'data': jsonEncode(data.toJson()), + 'lastModified': timestamp, + }, conflictAlgorithm: ConflictAlgorithm.replace); } - await save( - const PuzzleTheme(PuzzleThemeKey.rookEndgame), - data, - '2021-01-02T00:00:00Z', - ); - await save( - const PuzzleTheme(PuzzleThemeKey.doubleBishopMate), - data, - '2021-01-03T00:00:00Z', - ); - await save( - const PuzzleOpening('test_opening'), - data, - '2021-01-04T00:00:00Z', - ); - await save( - const PuzzleOpening('test_opening2'), - data, - '2021-01-04T80:00:00Z', - ); + await save(const PuzzleTheme(PuzzleThemeKey.rookEndgame), data, '2021-01-02T00:00:00Z'); + await save(const PuzzleTheme(PuzzleThemeKey.doubleBishopMate), data, '2021-01-03T00:00:00Z'); + await save(const PuzzleOpening('test_opening'), data, '2021-01-04T00:00:00Z'); + await save(const PuzzleOpening('test_opening2'), data, '2021-01-04T80:00:00Z'); expect( storage.fetchAll(userId: null), @@ -176,14 +130,8 @@ final data = PuzzleBatch( id: GameId('PrlkCqOv'), perf: Perf.blitz, rated: true, - white: PuzzleGamePlayer( - side: Side.white, - name: 'user1', - ), - black: PuzzleGamePlayer( - side: Side.black, - name: 'user2', - ), + white: PuzzleGamePlayer(side: Side.white, name: 'user1'), + black: PuzzleGamePlayer(side: Side.black, name: 'user2'), pgn: 'e4 Nc6 Bc4 e6 a3 g6 Nf3 Bg7 c3 Nge7 d3 O-O Be3 Na5 Ba2 b6 Qd2', ), ), diff --git a/test/model/puzzle/puzzle_repository_test.dart b/test/model/puzzle/puzzle_repository_test.dart index d053bda7ac..da9a1663ef 100644 --- a/test/model/puzzle/puzzle_repository_test.dart +++ b/test/model/puzzle/puzzle_repository_test.dart @@ -32,12 +32,9 @@ void main() { test('selectBatch with glicko', () async { final mockClient = MockClient((request) { if (request.url.path == '/api/puzzle/batch/mix') { - return mockResponse( - ''' + return mockResponse(''' {"puzzles":[{"game":{"id":"PrlkCqOv","perf":{"key":"rapid","name":"Rapid"},"rated":true,"players":[{"userId":"silverjo","name":"silverjo (1777)","color":"white"},{"userId":"robyarchitetto","name":"Robyarchitetto (1742)","color":"black"}],"pgn":"e4 Nc6 Bc4 e6 a3 g6 Nf3 Bg7 c3 Nge7 d3 O-O Be3 Na5 Ba2 b6 Qd2 Bb7 Bh6 d5 e5 d4 Bxg7 Kxg7 Qf4 Bxf3 Qxf3 dxc3 Nxc3 Nac6 Qf6+ Kg8 Rd1 Nd4 O-O c5 Ne4 Nef5 Rd2 Qxf6 Nxf6+ Kg7 Re1 h5 h3 Rad8 b4 Nh4 Re3 Nhf5 Re1 a5 bxc5 bxc5 Bc4 Ra8 Rb1 Nh4 Rdb2 Nc6 Rb7 Nxe5 Bxe6 Kxf6 Bd5 Nf5 R7b6+ Kg7 Bxa8 Rxa8 R6b3 Nd4 Rb7 Nxd3 Rd1 Ne2+ Kh2 Ndf4 Rdd7 Rf8 Ra7 c4 Rxa5 c3 Rc5 Ne6 Rc4 Ra8 a4 Rb8 a5 Rb2 a6 c2","clock":"5+8"},"puzzle":{"id":"20yWT","rating":1859,"plays":551,"initialPly":93,"solution":["a6a7","b2a2","c4c2","a2a7","d7a7"],"themes":["endgame","long","advantage","advancedPawn"]}}],"glicko":{"rating":1834.54,"deviation":23.45}} -''', - 200, - ); +''', 200); } return mockResponse('', 404); }); @@ -56,12 +53,9 @@ void main() { test('selectBatch with rounds', () async { final mockClient = MockClient((request) { if (request.url.path == '/api/puzzle/batch/mix') { - return mockResponse( - ''' + return mockResponse(''' {"puzzles":[{"game":{"id":"PrlkCqOv","perf":{"key":"rapid","name":"Rapid"},"rated":true,"players":[{"userId":"silverjo","name":"silverjo (1777)","color":"white"},{"userId":"robyarchitetto","name":"Robyarchitetto (1742)","color":"black"}],"pgn":"e4 Nc6 Bc4 e6 a3 g6 Nf3 Bg7 c3 Nge7 d3 O-O Be3 Na5 Ba2 b6 Qd2 Bb7 Bh6 d5 e5 d4 Bxg7 Kxg7 Qf4 Bxf3 Qxf3 dxc3 Nxc3 Nac6 Qf6+ Kg8 Rd1 Nd4 O-O c5 Ne4 Nef5 Rd2 Qxf6 Nxf6+ Kg7 Re1 h5 h3 Rad8 b4 Nh4 Re3 Nhf5 Re1 a5 bxc5 bxc5 Bc4 Ra8 Rb1 Nh4 Rdb2 Nc6 Rb7 Nxe5 Bxe6 Kxf6 Bd5 Nf5 R7b6+ Kg7 Bxa8 Rxa8 R6b3 Nd4 Rb7 Nxd3 Rd1 Ne2+ Kh2 Ndf4 Rdd7 Rf8 Ra7 c4 Rxa5 c3 Rc5 Ne6 Rc4 Ra8 a4 Rb8 a5 Rb2 a6 c2","clock":"5+8"},"puzzle":{"id":"20yWT","rating":1859,"plays":551,"initialPly":93,"solution":["a6a7","b2a2","c4c2","a2a7","d7a7"],"themes":["endgame","long","advantage","advancedPawn"]}}],"glicko":{"rating":1834.54,"deviation":23.45}, "rounds": [{"id": "07jQK", "ratingDiff": 10, "win": true}, {"id": "06jOK", "ratingDiff": -40, "win": false}]} -''', - 200, - ); +''', 200); } return mockResponse('', 404); }); @@ -79,12 +73,9 @@ void main() { test('streak', () async { final mockClient = MockClient((request) { if (request.url.path == '/api/streak') { - return mockResponse( - ''' + return mockResponse(''' {"game":{"id":"3dwjUYP0","perf":{"key":"rapid","name":"Rapid"},"rated":true,"players":[{"userId":"suresh","name":"Suresh (1716)","color":"white"},{"userId":"yulia","name":"Yulia (1765)","color":"black"}],"pgn":"d4 Nf6 Nf3 e6 c4 d5 cxd5 Bb4+ Nc3 Nxd5 Bd2 c6 a3 Bxc3 Bxc3 Nd7 Ne5 Qc7 e3 O-O Bd3 Nxc3 bxc3 Nxe5 dxe5 Qxe5 O-O Qxc3 a4 b6 Rc1 Qf6 Rxc6 Bb7 Rc4 Rad8 Rd4 Qg5 g3 Rxd4 exd4 Qd5 f3 Rd8 Rf2 g6 Be4 Qd7 Bxb7 Qxb7 Kg2 Qd7 Rd2 e5 dxe5","clock":"10+0"},"puzzle":{"id":"9afDa","rating":642,"plays":13675,"initialPly":54,"solution":["d7d2","d1d2","d8d2"],"themes":["endgame","crushing","short"]},"angle":{"key":"mix","name":"Puzzle themes","desc":"A bit of everything. You don't know what to expect, so you remain ready for anything! Just like in real games."},"streak":"9afDa 4V5gW 3mslj 41adQ 2tu7D 9RkvX 0vy7p A4v8U 5ZOBZ 193w0 98fRK CeonU 7yLlT 5RSB1 1tHFC 0Vsh7 7VFdg Dw0Rn EL08H 4dfgu 9ZxSP DUs0d 55MLt 9kmiT 0H0mL 0tBRV 7J6hk 0TjRQ 4G3KC DVlXY 1160r B8UHS 9NmPL 70ujM DJc5M BwkrY 94ynq D9wc6 41QGW 5sDnM 6xRVq 0EkpQ 7nksF 35Umd 0lJjY BrA7Z 8iHjv 5ypqy 4seCY 1bKuj 27svg 6K2S9 5lR21 9WveK DseMX C9m8Q 0K2CK 73mQX Bey7R CFniS 2NMq3 1eKTu 6131w 9m4mG 1H3Oi 9FxX2 4zRod 1C05H 9iEBH 21pIt 95dod 01tg7 47p37 1sK7x 0nSaW BWD8D C6WCD 9h38Q AoWyN CPdp8 ATUTK EFWL2 7GrRe 6W1OR 538Mf CH2cU An8P5 9LrrA 1cIQP B56EI 32pBl 34nq9 1aS2z 3qxyU 4NGY7 9GCq2 C43lx 2W8WA 1bnwL 4I8D1 Dc1u5 BG3VT 3pC4h C5tQJ 3rM5l 6KF3m 6Xnj5 EUX2q 1qiVv 2UTkb 7AtYx CbRCh 5xs9Y BlYuY BGFSj E7AIl 5keIv 1431G 7KYgv 68F2M 16IRi 8cNr9 8g79l BBM7N CmgIo 6zoOr D6Zsx 20mtz"} -''', - 200, - ); +''', 200); } return mockResponse('', 404); }); @@ -100,12 +91,9 @@ void main() { test('puzzle dashboard', () async { final mockClient = MockClient((request) { if (request.url.path == '/api/puzzle/dashboard/30') { - return mockResponse( - ''' + return mockResponse(''' {"days":30,"global":{"nb":196,"firstWins":107,"replayWins":0,"puzzleRatingAvg":1607,"performance":1653},"themes":{"middlegame":{"theme":"Middlegame","results":{"nb":97,"firstWins":51,"replayWins":0,"puzzleRatingAvg":1608,"performance":1634}},"endgame":{"theme":"Endgame","results":{"nb":81,"firstWins":48,"replayWins":0,"puzzleRatingAvg":1604,"performance":1697}}}} - ''', - 200, - ); + ''', 200); } return mockResponse('', 404); }); diff --git a/test/model/puzzle/puzzle_service_test.dart b/test/model/puzzle/puzzle_service_test.dart index 141f851199..4d58402ad1 100644 --- a/test/model/puzzle/puzzle_service_test.dart +++ b/test/model/puzzle/puzzle_service_test.dart @@ -41,13 +41,9 @@ void main() { final container = await makeTestContainer(mockClient); final storage = await container.read(puzzleBatchStorageProvider.future); - final service = await container.read(puzzleServiceFactoryProvider)( - queueLength: 3, - ); + final service = await container.read(puzzleServiceFactoryProvider)(queueLength: 3); - final next = await service.nextPuzzle( - userId: null, - ); + final next = await service.nextPuzzle(userId: null); expect(nbReq, equals(1)); expect(next?.puzzle.puzzle.id, equals(const PuzzleId('20yWT'))); final data = await storage.fetch(userId: null); @@ -67,18 +63,11 @@ void main() { final container = await makeTestContainer(mockClient); final storage = await container.read(puzzleBatchStorageProvider.future); - final service = await container.read(puzzleServiceFactoryProvider)( - queueLength: 1, - ); + final service = await container.read(puzzleServiceFactoryProvider)(queueLength: 1); - await storage.save( - userId: null, - data: _makeUnsolvedPuzzles([const PuzzleId('pId3')]), - ); + await storage.save(userId: null, data: _makeUnsolvedPuzzles([const PuzzleId('pId3')])); - final next = await service.nextPuzzle( - userId: null, - ); + final next = await service.nextPuzzle(userId: null); expect(nbReq, equals(0)); expect(next?.puzzle.puzzle.id, equals(const PuzzleId('pId3'))); final data = await storage.fetch(userId: null); @@ -98,25 +87,17 @@ void main() { final container = await makeTestContainer(mockClient); final storage = await container.read(puzzleBatchStorageProvider.future); - final service = await container.read(puzzleServiceFactoryProvider)( - queueLength: 2, - ); - await storage.save( - userId: null, - data: _makeUnsolvedPuzzles([const PuzzleId('pId3')]), - ); + final service = await container.read(puzzleServiceFactoryProvider)(queueLength: 2); + await storage.save(userId: null, data: _makeUnsolvedPuzzles([const PuzzleId('pId3')])); - final next = await service.nextPuzzle( - userId: null, - ); + final next = await service.nextPuzzle(userId: null); expect(next?.puzzle.puzzle.id, equals(const PuzzleId('pId3'))); expect(nbReq, equals(1)); final data = await storage.fetch(userId: null); expect(data?.unsolved.length, equals(2)); }); - test('nextPuzzle will always get the first puzzle of unsolved queue', - () async { + test('nextPuzzle will always get the first puzzle of unsolved queue', () async { int nbReq = 0; final mockClient = MockClient((request) { nbReq++; @@ -125,44 +106,30 @@ void main() { final container = await makeTestContainer(mockClient); final storage = await container.read(puzzleBatchStorageProvider.future); - final service = await container.read(puzzleServiceFactoryProvider)( - queueLength: 1, - ); - await storage.save( - userId: null, - data: _makeUnsolvedPuzzles([const PuzzleId('pId3')]), - ); + final service = await container.read(puzzleServiceFactoryProvider)(queueLength: 1); + await storage.save(userId: null, data: _makeUnsolvedPuzzles([const PuzzleId('pId3')])); expect(nbReq, equals(0)); - final next = await service.nextPuzzle( - userId: null, - ); + final next = await service.nextPuzzle(userId: null); expect(next?.puzzle.puzzle.id, equals(const PuzzleId('pId3'))); final data = await storage.fetch(userId: null); expect(data?.unsolved.length, equals(1)); - final next2 = await service.nextPuzzle( - userId: null, - ); + final next2 = await service.nextPuzzle(userId: null); expect(next2?.puzzle.puzzle.id, equals(const PuzzleId('pId3'))); final data2 = await storage.fetch(userId: null); expect(data2?.unsolved.length, equals(1)); }); - test('nextPuzzle returns null is unsolved queue is empty and is offline', - () async { + test('nextPuzzle returns null is unsolved queue is empty and is offline', () async { final mockClient = MockClient((request) { throw const SocketException('offline'); }); final container = await makeTestContainer(mockClient); - final service = await container.read(puzzleServiceFactoryProvider)( - queueLength: 1, - ); + final service = await container.read(puzzleServiceFactoryProvider)(queueLength: 1); - final nextPuzzle = await service.nextPuzzle( - userId: null, - ); + final nextPuzzle = await service.nextPuzzle(userId: null); expect(nextPuzzle, isNull); }); @@ -179,25 +146,15 @@ void main() { final container = await makeTestContainer(mockClient); final storage = await container.read(puzzleBatchStorageProvider.future); - final service = await container.read(puzzleServiceFactoryProvider)( - queueLength: 1, - ); - await storage.save( - userId: null, - data: _makeUnsolvedPuzzles([const PuzzleId('pId3')]), - ); + final service = await container.read(puzzleServiceFactoryProvider)(queueLength: 1); + await storage.save(userId: null, data: _makeUnsolvedPuzzles([const PuzzleId('pId3')])); - final next = await service.nextPuzzle( - userId: const UserId('testUserId'), - ); + final next = await service.nextPuzzle(userId: const UserId('testUserId')); expect(next?.puzzle.puzzle.id, equals(const PuzzleId('20yWT'))); expect(nbReq, equals(1)); final data = await storage.fetch(userId: const UserId('testUserId')); - expect( - data?.unsolved.length, - equals(1), - ); + expect(data?.unsolved.length, equals(1)); }); test('different batch is saved per angle', () async { @@ -212,13 +169,8 @@ void main() { final container = await makeTestContainer(mockClient); final storage = await container.read(puzzleBatchStorageProvider.future); - final service = await container.read(puzzleServiceFactoryProvider)( - queueLength: 1, - ); - await storage.save( - userId: null, - data: _makeUnsolvedPuzzles([const PuzzleId('pId3')]), - ); + final service = await container.read(puzzleServiceFactoryProvider)(queueLength: 1); + await storage.save(userId: null, data: _makeUnsolvedPuzzles([const PuzzleId('pId3')])); final next = await service.nextPuzzle( angle: const PuzzleTheme(PuzzleThemeKey.opening), @@ -231,10 +183,7 @@ void main() { userId: null, angle: const PuzzleTheme(PuzzleThemeKey.opening), ); - expect( - data?.unsolved.length, - equals(1), - ); + expect(data?.unsolved.length, equals(1)); }); test('solve puzzle when online, no userId', () async { @@ -249,21 +198,12 @@ void main() { final container = await makeTestContainer(mockClient); final storage = await container.read(puzzleBatchStorageProvider.future); - final service = await container.read(puzzleServiceFactoryProvider)( - queueLength: 1, - ); - await storage.save( - userId: null, - data: _makeUnsolvedPuzzles([const PuzzleId('pId3')]), - ); + final service = await container.read(puzzleServiceFactoryProvider)(queueLength: 1); + await storage.save(userId: null, data: _makeUnsolvedPuzzles([const PuzzleId('pId3')])); final next = await service.solve( puzzle: samplePuzzle, - solution: const PuzzleSolution( - id: PuzzleId('pId3'), - win: true, - rated: true, - ), + solution: const PuzzleSolution(id: PuzzleId('pId3'), win: true, rated: true), userId: null, ); @@ -283,8 +223,7 @@ void main() { nbReq++; if (request.method == 'POST' && request.url.path == '/api/puzzle/batch/mix' && - request.body == - '{"solutions":[{"id":"pId3","win":true,"rated":true}]}') { + request.body == '{"solutions":[{"id":"pId3","win":true,"rated":true}]}') { return mockResponse( '''{"puzzles":[{"game":{"id":"PrlkCqOv","perf":{"key":"rapid","name":"Rapid"},"rated":true,"players":[{"userId":"silverjo","name":"silverjo (1777)","color":"white"},{"userId":"robyarchitetto","name":"Robyarchitetto (1742)","color":"black"}],"pgn":"e4 Nc6 Bc4 e6 a3 g6 Nf3 Bg7 c3 Nge7 d3 O-O Be3 Na5 Ba2 b6 Qd2 Bb7 Bh6 d5 e5 d4 Bxg7 Kxg7 Qf4 Bxf3 Qxf3 dxc3 Nxc3 Nac6 Qf6+ Kg8 Rd1 Nd4 O-O c5 Ne4 Nef5 Rd2 Qxf6 Nxf6+ Kg7 Re1 h5 h3 Rad8 b4 Nh4 Re3 Nhf5 Re1 a5 bxc5 bxc5 Bc4 Ra8 Rb1 Nh4 Rdb2 Nc6 Rb7 Nxe5 Bxe6 Kxf6 Bd5 Nf5 R7b6+ Kg7 Bxa8 Rxa8 R6b3 Nd4 Rb7 Nxd3 Rd1 Ne2+ Kh2 Ndf4 Rdd7 Rf8 Ra7 c4 Rxa5 c3 Rc5 Ne6 Rc4 Ra8 a4 Rb8 a5 Rb2 a6 c2","clock":"5+8"},"puzzle":{"id":"20yWT","rating":1859,"plays":551,"initialPly":93,"solution":["a6a7","b2a2","c4c2","a2a7","d7a7"],"themes":["endgame","long","advantage","advancedPawn"]}}], "glicko":{"rating":1834.54,"deviation":23.45},"rounds":[{"id": "pId3","ratingDiff": 10,"win": true}]}''', 200, @@ -295,9 +234,7 @@ void main() { final container = await makeTestContainer(mockClient); final storage = await container.read(puzzleBatchStorageProvider.future); - final service = await container.read(puzzleServiceFactoryProvider)( - queueLength: 1, - ); + final service = await container.read(puzzleServiceFactoryProvider)(queueLength: 1); await storage.save( userId: const UserId('testUserId'), data: _makeUnsolvedPuzzles([const PuzzleId('pId3')]), @@ -305,11 +242,7 @@ void main() { final next = await service.solve( puzzle: samplePuzzle, - solution: const PuzzleSolution( - id: PuzzleId('pId3'), - win: true, - rated: true, - ), + solution: const PuzzleSolution(id: PuzzleId('pId3'), win: true, rated: true), userId: const UserId('testUserId'), ); @@ -323,15 +256,7 @@ void main() { expect(next?.glicko?.deviation, equals(23.45)); expect( next?.rounds, - equals( - IList(const [ - PuzzleRound( - id: PuzzleId('pId3'), - ratingDiff: 10, - win: true, - ), - ]), - ), + equals(IList(const [PuzzleRound(id: PuzzleId('pId3'), ratingDiff: 10, win: true)])), ); }); @@ -344,19 +269,13 @@ void main() { final container = await makeTestContainer(mockClient); final storage = await container.read(puzzleBatchStorageProvider.future); - final service = await container.read(puzzleServiceFactoryProvider)( - queueLength: 2, - ); + final service = await container.read(puzzleServiceFactoryProvider)(queueLength: 2); await storage.save( userId: const UserId('testUserId'), - data: _makeUnsolvedPuzzles([ - const PuzzleId('pId3'), - const PuzzleId('pId4'), - ]), + data: _makeUnsolvedPuzzles([const PuzzleId('pId3'), const PuzzleId('pId4')]), ); - const solution = - PuzzleSolution(id: PuzzleId('pId3'), win: true, rated: true); + const solution = PuzzleSolution(id: PuzzleId('pId3'), win: true, rated: true); final next = await service.solve( puzzle: samplePuzzle, @@ -384,16 +303,11 @@ void main() { final container = await makeTestContainer(mockClient); final storage = await container.read(puzzleBatchStorageProvider.future); - final service = await container.read(puzzleServiceFactoryProvider)( - queueLength: 2, - ); + final service = await container.read(puzzleServiceFactoryProvider)(queueLength: 2); await storage.save( userId: const UserId('testUserId'), - data: _makeUnsolvedPuzzles([ - const PuzzleId('pId3'), - const PuzzleId('pId4'), - ]), + data: _makeUnsolvedPuzzles([const PuzzleId('pId3'), const PuzzleId('pId4')]), ); final next = await service.resetBatch(userId: const UserId('testUserId')); @@ -434,14 +348,8 @@ final samplePuzzle = Puzzle( id: GameId('PrlkCqOv'), perf: Perf.blitz, rated: true, - white: PuzzleGamePlayer( - side: Side.white, - name: 'user1', - ), - black: PuzzleGamePlayer( - side: Side.black, - name: 'user2', - ), + white: PuzzleGamePlayer(side: Side.white, name: 'user1'), + black: PuzzleGamePlayer(side: Side.black, name: 'user2'), pgn: 'e4 Nc6 Bc4 e6 a3 g6 Nf3 Bg7 c3 Nge7 d3 O-O Be3 Na5 Ba2 b6 Qd2', ), ); @@ -464,16 +372,9 @@ PuzzleBatch _makeUnsolvedPuzzles(List ids) { id: GameId('PrlkCqOv'), perf: Perf.blitz, rated: true, - white: PuzzleGamePlayer( - side: Side.white, - name: 'user1', - ), - black: PuzzleGamePlayer( - side: Side.black, - name: 'user2', - ), - pgn: - 'e4 Nc6 Bc4 e6 a3 g6 Nf3 Bg7 c3 Nge7 d3 O-O Be3 Na5 Ba2 b6 Qd2', + white: PuzzleGamePlayer(side: Side.white, name: 'user1'), + black: PuzzleGamePlayer(side: Side.black, name: 'user2'), + pgn: 'e4 Nc6 Bc4 e6 a3 g6 Nf3 Bg7 c3 Nge7 d3 O-O Be3 Na5 Ba2 b6 Qd2', ), ), ]), diff --git a/test/model/puzzle/puzzle_storage_test.dart b/test/model/puzzle/puzzle_storage_test.dart index 888150b5c0..07bc10bd64 100644 --- a/test/model/puzzle/puzzle_storage_test.dart +++ b/test/model/puzzle/puzzle_storage_test.dart @@ -28,15 +28,8 @@ void main() { final storage = await container.read(puzzleStorageProvider.future); - await storage.save( - puzzle: puzzle, - ); - expect( - storage.fetch( - puzzleId: const PuzzleId('pId3'), - ), - completion(equals(puzzle)), - ); + await storage.save(puzzle: puzzle); + expect(storage.fetch(puzzleId: const PuzzleId('pId3')), completion(equals(puzzle))); }); }); } @@ -54,14 +47,8 @@ final puzzle = Puzzle( id: GameId('PrlkCqOv'), perf: Perf.blitz, rated: true, - white: PuzzleGamePlayer( - side: Side.white, - name: 'user1', - ), - black: PuzzleGamePlayer( - side: Side.black, - name: 'user2', - ), + white: PuzzleGamePlayer(side: Side.white, name: 'user1'), + black: PuzzleGamePlayer(side: Side.black, name: 'user2'), pgn: 'e4 Nc6 Bc4 e6 a3 g6 Nf3 Bg7 c3 Nge7 d3 O-O Be3 Na5 Ba2 b6 Qd2', ), ); diff --git a/test/model/relation/relation_repository_test.dart b/test/model/relation/relation_repository_test.dart index afe1fb9580..98caa64a47 100644 --- a/test/model/relation/relation_repository_test.dart +++ b/test/model/relation/relation_repository_test.dart @@ -17,10 +17,7 @@ void main() { '''; final mockClient = MockClient((request) { if (request.url.path == '/api/rel/following') { - return mockResponse( - testRelationResponseMinimal, - 200, - ); + return mockResponse(testRelationResponseMinimal, 200); } return mockResponse('', 404); }); @@ -39,10 +36,7 @@ void main() { '''; final mockClient = MockClient((request) { if (request.url.path == '/api/rel/following') { - return mockResponse( - testRelationResponse, - 200, - ); + return mockResponse(testRelationResponse, 200); } return mockResponse('', 404); }); diff --git a/test/model/study/study_repository_test.dart b/test/model/study/study_repository_test.dart index 08fbde61c3..2f709fa087 100644 --- a/test/model/study/study_repository_test.dart +++ b/test/model/study/study_repository_test.dart @@ -326,15 +326,9 @@ void main() { final mockClient = MockClient((request) { if (request.url.path == '/study/JbWtuaeK/7OJXp679') { expect(request.url.queryParameters['chapters'], '1'); - return mockResponse( - response, - 200, - ); + return mockResponse(response, 200); } else if (request.url.path == '/api/study/JbWtuaeK/7OJXp679.pgn') { - return mockResponse( - 'pgn', - 200, - ); + return mockResponse('pgn', 200); } return mockResponse('', 404); }); @@ -357,79 +351,61 @@ void main() { liked: false, likes: 29, ownerId: const UserId('kyle-and-jess'), - features: ( - cloneable: false, - chat: true, - sticky: false, - ), + features: (cloneable: false, chat: true, sticky: false), topics: const IList.empty(), - chapters: IList( - const [ - StudyChapterMeta( - id: StudyChapterId('EgqyeQIp'), - name: 'Introduction', - fen: null, - ), - StudyChapterMeta( - id: StudyChapterId('z6tGV47W'), - name: 'Practice Your Thought Process', - fen: - '2k4r/p1p2p2/1p2b2p/1Pqn2r1/2B5/B1PP4/P4PPP/RN2Q1K1 b - - 6 20', - ), - StudyChapterMeta( - id: StudyChapterId('dTfxbccx'), - name: 'Practice Strategic Thinking', - fen: - 'r3r1k1/1b2b2p/pq4pB/1p3pN1/2p5/2P5/PPn1QPPP/3RR1K1 w - - 0 23', - ), - StudyChapterMeta( - id: StudyChapterId('B1U4pFdG'), - name: 'Calculate Fully', - fen: - '3r3r/1Rpk1p2/2p2q1p/Q2pp3/P2PP1n1/2P1B1Pp/5P2/1N3RK1 b - - 2 26', - ), - StudyChapterMeta( - id: StudyChapterId('NJLW7jil'), - name: 'Calculate Freely', - fen: '4k3/8/6p1/R1p1r1n1/P3Pp2/2N2r2/1PP1K1R1/8 b - - 2 39', - ), - StudyChapterMeta( - id: StudyChapterId('7OJXp679'), - name: 'Use a Timer', - fen: - 'r5k1/ppp2ppp/7r/4Nb2/3P4/1QN1PPq1/PP2B1P1/R4RK1 b - - 1 20', - ), - StudyChapterMeta( - id: StudyChapterId('Rgk6UlTP'), - name: 'Understand Your Mistakes', - fen: - 'r4rk1/1R3pb1/pR2N1p1/2q5/4p3/2P1P1Pp/Q2P1P1P/6K1 b - - 1 26', - ), - StudyChapterMeta( - id: StudyChapterId('VsdxmjCf'), - name: 'Adjusting Difficulty', - fen: - '3r4/k1pq1p1r/pp1p2p1/8/3P4/P1P2BP1/1P1N1Pp1/R3R1K1 b - - 0 1', - ), - StudyChapterMeta( - id: StudyChapterId('FHU6xhYs'), - name: 'Using Themes', - fen: - 'r2k3N/pbpp1Bpp/1p6/2b1p3/3n3q/P7/1PPP1RPP/RNB2QK1 b - - 3 12', - ), - StudyChapterMeta( - id: StudyChapterId('8FhO455h'), - name: 'Endurance Training', - fen: '8/1p5k/2qPQ2p/p5p1/5r1n/2B4P/5P2/4R1K1 w - - 3 41', - ), - StudyChapterMeta( - id: StudyChapterId('jWUEWsEf'), - name: 'Final Thoughts', - fen: - '8/1PP2PP1/PppPPppP/Pp1pp1pP/Pp4pP/1Pp2pP1/2PppP2/3PP3 w - - 0 1', - ), - ], - ), + chapters: IList(const [ + StudyChapterMeta(id: StudyChapterId('EgqyeQIp'), name: 'Introduction', fen: null), + StudyChapterMeta( + id: StudyChapterId('z6tGV47W'), + name: 'Practice Your Thought Process', + fen: '2k4r/p1p2p2/1p2b2p/1Pqn2r1/2B5/B1PP4/P4PPP/RN2Q1K1 b - - 6 20', + ), + StudyChapterMeta( + id: StudyChapterId('dTfxbccx'), + name: 'Practice Strategic Thinking', + fen: 'r3r1k1/1b2b2p/pq4pB/1p3pN1/2p5/2P5/PPn1QPPP/3RR1K1 w - - 0 23', + ), + StudyChapterMeta( + id: StudyChapterId('B1U4pFdG'), + name: 'Calculate Fully', + fen: '3r3r/1Rpk1p2/2p2q1p/Q2pp3/P2PP1n1/2P1B1Pp/5P2/1N3RK1 b - - 2 26', + ), + StudyChapterMeta( + id: StudyChapterId('NJLW7jil'), + name: 'Calculate Freely', + fen: '4k3/8/6p1/R1p1r1n1/P3Pp2/2N2r2/1PP1K1R1/8 b - - 2 39', + ), + StudyChapterMeta( + id: StudyChapterId('7OJXp679'), + name: 'Use a Timer', + fen: 'r5k1/ppp2ppp/7r/4Nb2/3P4/1QN1PPq1/PP2B1P1/R4RK1 b - - 1 20', + ), + StudyChapterMeta( + id: StudyChapterId('Rgk6UlTP'), + name: 'Understand Your Mistakes', + fen: 'r4rk1/1R3pb1/pR2N1p1/2q5/4p3/2P1P1Pp/Q2P1P1P/6K1 b - - 1 26', + ), + StudyChapterMeta( + id: StudyChapterId('VsdxmjCf'), + name: 'Adjusting Difficulty', + fen: '3r4/k1pq1p1r/pp1p2p1/8/3P4/P1P2BP1/1P1N1Pp1/R3R1K1 b - - 0 1', + ), + StudyChapterMeta( + id: StudyChapterId('FHU6xhYs'), + name: 'Using Themes', + fen: 'r2k3N/pbpp1Bpp/1p6/2b1p3/3n3q/P7/1PPP1RPP/RNB2QK1 b - - 3 12', + ), + StudyChapterMeta( + id: StudyChapterId('8FhO455h'), + name: 'Endurance Training', + fen: '8/1p5k/2qPQ2p/p5p1/5r1n/2B4P/5P2/4R1K1 w - - 3 41', + ), + StudyChapterMeta( + id: StudyChapterId('jWUEWsEf'), + name: 'Final Thoughts', + fen: '8/1PP2PP1/PppPPppP/Pp1pp1pP/Pp4pP/1Pp2pP1/2PppP2/3PP3 w - - 0 1', + ), + ]), chapter: const StudyChapter( id: StudyChapterId('7OJXp679'), setup: StudyChapterSetup( @@ -441,31 +417,30 @@ void main() { practise: false, conceal: null, gamebook: true, - features: ( - computer: false, - explorer: false, - ), + features: (computer: false, explorer: false), ), - hints: [ - 'The white king is not very safe. Can black increase the pressure on the king?', - null, - null, - null, - null, - null, - null, - null, - ].lock, - deviationComments: [ - null, - "Black has to be quick to jump on the initiative of white's king being vulnerable.", - null, - null, - null, - 'Keep the initiative going! Go for the king!', - null, - null, - ].lock, + hints: + [ + 'The white king is not very safe. Can black increase the pressure on the king?', + null, + null, + null, + null, + null, + null, + null, + ].lock, + deviationComments: + [ + null, + "Black has to be quick to jump on the initiative of white's king being vulnerable.", + null, + null, + null, + 'Keep the initiative going! Go for the king!', + null, + null, + ].lock, ), ); }); diff --git a/test/model/tv/tv_repository_test.dart b/test/model/tv/tv_repository_test.dart index 66c7fe2057..01cdc0dc2e 100644 --- a/test/model/tv/tv_repository_test.dart +++ b/test/model/tv/tv_repository_test.dart @@ -166,10 +166,7 @@ void main() { final mockClient = MockClient((request) { if (request.url.path == '/api/tv/channels') { - return mockResponse( - response, - 200, - ); + return mockResponse(response, 200); } return mockResponse('', 404); }); @@ -183,10 +180,7 @@ void main() { // supported channels only expect(result.length, 13); - expect( - result[TvChannel.best]?.user.name, - 'Chessisnotfair', - ); + expect(result[TvChannel.best]?.user.name, 'Chessisnotfair'); }); }); } diff --git a/test/model/user/user_repository_test.dart b/test/model/user/user_repository_test.dart index 87c1a6cf6a..afae7b35c0 100644 --- a/test/model/user/user_repository_test.dart +++ b/test/model/user/user_repository_test.dart @@ -18,8 +18,7 @@ void main() { test('json read, minimal example', () async { final mockClient = MockClient((request) { if (request.url.path == '/api/user/$testUserId') { - return mockResponse( - ''' + return mockResponse(''' { "id": "$testUserId", "username": "$testUserId", @@ -28,9 +27,7 @@ void main() { "perfs": { } } -''', - 200, - ); +''', 200); } return mockResponse('', 404); }); @@ -48,8 +45,7 @@ void main() { test('json read, full example', () async { final mockClient = MockClient((request) { if (request.url.path == '/api/user/$testUserId') { - return mockResponse( - ''' + return mockResponse(''' { "id": "$testUserId", "username": "$testUserId", @@ -87,9 +83,7 @@ void main() { "links": "http://test.com" } } -''', - 200, - ); +''', 200); } return mockResponse('', 404); }); @@ -113,8 +107,7 @@ void main() { test('json read, minimal example', () async { final mockClient = MockClient((request) { if (request.url.path == path) { - return mockResponse( - ''' + return mockResponse(''' { "user": { "name": "$testUserId" @@ -143,9 +136,7 @@ void main() { } } } -''', - 200, - ); +''', 200); } return mockResponse('', 404); }); @@ -163,8 +154,7 @@ void main() { test('json read, full example', () async { final mockClient = MockClient((request) { if (request.url.path == path) { - return mockResponse( - ''' + return mockResponse(''' { "user": { "name": "testOpponentName" @@ -400,9 +390,7 @@ void main() { } } } -''', - 200, - ); +''', 200); } return mockResponse('', 404); }); @@ -423,16 +411,11 @@ void main() { group('UserRepository.getUsersStatuses', () { test('json read, minimal example', () async { - final ids = ISet( - const {UserId('maia1'), UserId('maia5'), UserId('maia9')}, - ); + final ids = ISet(const {UserId('maia1'), UserId('maia5'), UserId('maia9')}); final mockClient = MockClient((request) { if (request.url.path == '/api/users/status') { - return mockResponse( - '[]', - 200, - ); + return mockResponse('[]', 200); } return mockResponse('', 404); }); @@ -447,13 +430,10 @@ void main() { }); test('json read, full example', () async { - final ids = ISet( - const {UserId('maia1'), UserId('maia5'), UserId('maia9')}, - ); + final ids = ISet(const {UserId('maia1'), UserId('maia5'), UserId('maia9')}); final mockClient = MockClient((request) { if (request.url.path == '/api/users/status') { - return mockResponse( - ''' + return mockResponse(''' [ { "id": "maia1", @@ -472,9 +452,7 @@ void main() { "online": true } ] -''', - 200, - ); +''', 200); } return mockResponse('', 404); }); diff --git a/test/network/fake_websocket_channel.dart b/test/network/fake_websocket_channel.dart index 88e3391d04..edc6b66814 100644 --- a/test/network/fake_websocket_channel.dart +++ b/test/network/fake_websocket_channel.dart @@ -114,25 +114,19 @@ class FakeWebSocketChannel implements WebSocketChannel { void pipe(StreamChannel other) {} @override - StreamChannel transform( - StreamChannelTransformer transformer, - ) { + StreamChannel transform(StreamChannelTransformer transformer) { // TODO: implement transform throw UnimplementedError(); } @override - StreamChannel transformSink( - StreamSinkTransformer transformer, - ) { + StreamChannel transformSink(StreamSinkTransformer transformer) { // TODO: implement transformSink throw UnimplementedError(); } @override - StreamChannel transformStream( - StreamTransformer transformer, - ) { + StreamChannel transformStream(StreamTransformer transformer) { // TODO: implement transformStream throw UnimplementedError(); } @@ -144,17 +138,13 @@ class FakeWebSocketChannel implements WebSocketChannel { } @override - StreamChannel changeSink( - StreamSink Function(StreamSink p1) change, - ) { + StreamChannel changeSink(StreamSink Function(StreamSink p1) change) { // TODO: implement changeSink throw UnimplementedError(); } @override - StreamChannel changeStream( - Stream Function(Stream p1) change, - ) { + StreamChannel changeStream(Stream Function(Stream p1) change) { // TODO: implement changeStream throw UnimplementedError(); } @@ -200,9 +190,7 @@ class FakeWebSocketSink implements WebSocketSink { @override Future close([int? closeCode, String? closeReason]) { - return Future.wait([ - _channel._incomingController.close(), - ]); + return Future.wait([_channel._incomingController.close()]); } @override diff --git a/test/network/http_test.dart b/test/network/http_test.dart index 488eaed8ef..c9b775b574 100644 --- a/test/network/http_test.dart +++ b/test/network/http_test.dart @@ -106,45 +106,27 @@ void main() { ]) { expect( () => method(Uri(path: '/will/return/500')), - throwsA( - isA() - .having((e) => e.statusCode, 'statusCode', 500), - ), + throwsA(isA().having((e) => e.statusCode, 'statusCode', 500)), ); expect( () => method(Uri(path: '/will/return/503')), - throwsA( - isA() - .having((e) => e.statusCode, 'statusCode', 503), - ), + throwsA(isA().having((e) => e.statusCode, 'statusCode', 503)), ); expect( () => method(Uri(path: '/will/return/400')), - throwsA( - isA() - .having((e) => e.statusCode, 'statusCode', 400), - ), + throwsA(isA().having((e) => e.statusCode, 'statusCode', 400)), ); expect( () => method(Uri(path: '/will/return/404')), - throwsA( - isA() - .having((e) => e.statusCode, 'statusCode', 404), - ), + throwsA(isA().having((e) => e.statusCode, 'statusCode', 404)), ); expect( () => method(Uri(path: '/will/return/401')), - throwsA( - isA() - .having((e) => e.statusCode, 'statusCode', 401), - ), + throwsA(isA().having((e) => e.statusCode, 'statusCode', 401)), ); expect( () => method(Uri(path: '/will/return/403')), - throwsA( - isA() - .having((e) => e.statusCode, 'statusCode', 403), - ), + throwsA(isA().having((e) => e.statusCode, 'statusCode', 403)), ); } }); @@ -158,37 +140,13 @@ void main() { ], ); final client = container.read(lichessClientProvider); - for (final method in [ - client.get, - client.post, - client.put, - client.patch, - client.delete, - ]) { - expect( - () => method(Uri(path: '/will/return/500')), - returnsNormally, - ); - expect( - () => method(Uri(path: '/will/return/503')), - returnsNormally, - ); - expect( - () => method(Uri(path: '/will/return/400')), - returnsNormally, - ); - expect( - () => method(Uri(path: '/will/return/404')), - returnsNormally, - ); - expect( - () => method(Uri(path: '/will/return/401')), - returnsNormally, - ); - expect( - () => method(Uri(path: '/will/return/403')), - returnsNormally, - ); + for (final method in [client.get, client.post, client.put, client.patch, client.delete]) { + expect(() => method(Uri(path: '/will/return/500')), returnsNormally); + expect(() => method(Uri(path: '/will/return/503')), returnsNormally); + expect(() => method(Uri(path: '/will/return/400')), returnsNormally); + expect(() => method(Uri(path: '/will/return/404')), returnsNormally); + expect(() => method(Uri(path: '/will/return/401')), returnsNormally); + expect(() => method(Uri(path: '/will/return/403')), returnsNormally); } }); @@ -203,23 +161,11 @@ void main() { final client = container.read(lichessClientProvider); expect( () => client.get(Uri(path: '/will/throw/socket/exception')), - throwsA( - isA().having( - (e) => e.message, - 'message', - 'no internet', - ), - ), + throwsA(isA().having((e) => e.message, 'message', 'no internet')), ); expect( () => client.get(Uri(path: '/will/throw/tls/exception')), - throwsA( - isA().having( - (e) => e.message, - 'message', - 'tls error', - ), - ), + throwsA(isA().having((e) => e.message, 'message', 'tls error')), ); }); @@ -249,8 +195,7 @@ void main() { ); }); - test('adds a signed bearer token when a session is available the request', - () async { + test('adds a signed bearer token when a session is available the request', () async { final container = await makeContainer( overrides: [ httpClientFactoryProvider.overrideWith((ref) { @@ -282,63 +227,62 @@ void main() { }); test( - 'when receiving a 401, will test session token and delete session if not valid anymore', - () async { - int nbTokenTestRequests = 0; - final container = await makeContainer( - overrides: [ - httpClientFactoryProvider.overrideWith((ref) { - return FakeHttpClientFactory(() => FakeClient()); - }), - defaultClientProvider.overrideWith((ref) { - return MockClient((request) async { - if (request.url.path == '/api/token/test') { - nbTokenTestRequests++; - final token = request.body.split(',')[0]; - final response = '{"$token": null}'; - return http.Response(response, 200); - } - return http.Response('', 404); - }); - }), - ], - userSession: const AuthSessionState( - token: 'test-token', - user: LightUser(id: UserId('test-user-id'), name: 'test-username'), - ), - ); + 'when receiving a 401, will test session token and delete session if not valid anymore', + () async { + int nbTokenTestRequests = 0; + final container = await makeContainer( + overrides: [ + httpClientFactoryProvider.overrideWith((ref) { + return FakeHttpClientFactory(() => FakeClient()); + }), + defaultClientProvider.overrideWith((ref) { + return MockClient((request) async { + if (request.url.path == '/api/token/test') { + nbTokenTestRequests++; + final token = request.body.split(',')[0]; + final response = '{"$token": null}'; + return http.Response(response, 200); + } + return http.Response('', 404); + }); + }), + ], + userSession: const AuthSessionState( + token: 'test-token', + user: LightUser(id: UserId('test-user-id'), name: 'test-username'), + ), + ); - fakeAsync((async) { - final session = container.read(authSessionProvider); - expect(session, isNotNull); + fakeAsync((async) { + final session = container.read(authSessionProvider); + expect(session, isNotNull); - final client = container.read(lichessClientProvider); - try { - client.get(Uri(path: '/will/return/401')); - } on ServerException catch (_) {} + final client = container.read(lichessClientProvider); + try { + client.get(Uri(path: '/will/return/401')); + } on ServerException catch (_) {} - async.flushMicrotasks(); + async.flushMicrotasks(); - final requests = FakeClient.verifyRequests(); - expect(requests.length, 1); - expect( - requests.first, - isA().having( - (r) => r.headers['Authorization'], - 'Authorization', - 'Bearer ${signBearerToken('test-token')}', - ), - ); + final requests = FakeClient.verifyRequests(); + expect(requests.length, 1); + expect( + requests.first, + isA().having( + (r) => r.headers['Authorization'], + 'Authorization', + 'Bearer ${signBearerToken('test-token')}', + ), + ); - expect(nbTokenTestRequests, 1); + expect(nbTokenTestRequests, 1); - expect(container.read(authSessionProvider), isNull); - }); - }); + expect(container.read(authSessionProvider), isNull); + }); + }, + ); - test( - 'when receiving a 401, will test session token and keep session if still valid', - () async { + test('when receiving a 401, will test session token and keep session if still valid', () async { int nbTokenTestRequests = 0; final container = await makeContainer( overrides: [ diff --git a/test/network/socket_test.dart b/test/network/socket_test.dart index c5cbe7d5d2..bf9d43595d 100644 --- a/test/network/socket_test.dart +++ b/test/network/socket_test.dart @@ -7,9 +7,7 @@ import 'package:package_info_plus/package_info_plus.dart'; import 'fake_websocket_channel.dart'; -SocketClient makeTestSocketClient( - FakeWebSocketChannelFactory fakeChannelFactory, -) { +SocketClient makeTestSocketClient(FakeWebSocketChannelFactory fakeChannelFactory) { final client = SocketClient( Uri(path: kDefaultSocketRoute), channelFactory: fakeChannelFactory, @@ -44,8 +42,7 @@ void main() { test('handles ping/pong', () async { final fakeChannel = FakeWebSocketChannel(); - final socketClient = - makeTestSocketClient(FakeWebSocketChannelFactory((_) => fakeChannel)); + final socketClient = makeTestSocketClient(FakeWebSocketChannelFactory((_) => fakeChannel)); socketClient.connect(); int sentPingCount = 0; @@ -133,11 +130,9 @@ void main() { }); test('computes average lag', () async { - final fakeChannel = - FakeWebSocketChannel(connectionLag: const Duration(milliseconds: 10)); + final fakeChannel = FakeWebSocketChannel(connectionLag: const Duration(milliseconds: 10)); - final socketClient = - makeTestSocketClient(FakeWebSocketChannelFactory((_) => fakeChannel)); + final socketClient = makeTestSocketClient(FakeWebSocketChannelFactory((_) => fakeChannel)); socketClient.connect(); // before the connection is ready the average lag is zero @@ -153,19 +148,13 @@ void main() { await expectLater(fakeChannel.stream, emits('0')); // after the ping/pong exchange the average lag is computed - expect( - socketClient.averageLag.value.inMilliseconds, - greaterThanOrEqualTo(10), - ); + expect(socketClient.averageLag.value.inMilliseconds, greaterThanOrEqualTo(10)); // wait for more ping/pong exchanges await expectLater(fakeChannel.stream, emitsInOrder(['0', '0', '0', '0'])); // average lag is still the same - expect( - socketClient.averageLag.value.inMilliseconds, - greaterThanOrEqualTo(10), - ); + expect(socketClient.averageLag.value.inMilliseconds, greaterThanOrEqualTo(10)); // increase the lag of the connection fakeChannel.connectionLag = const Duration(milliseconds: 100); @@ -174,10 +163,7 @@ void main() { await expectLater(fakeChannel.stream, emitsInOrder(['0', '0', '0', '0'])); // average lag should be higher - expect( - socketClient.averageLag.value.inMilliseconds, - greaterThanOrEqualTo(40), - ); + expect(socketClient.averageLag.value.inMilliseconds, greaterThanOrEqualTo(40)); await socketClient.close(); @@ -188,8 +174,7 @@ void main() { test('handles ackable messages', () async { final fakeChannel = FakeWebSocketChannel(); - final socketClient = - makeTestSocketClient(FakeWebSocketChannelFactory((_) => fakeChannel)); + final socketClient = makeTestSocketClient(FakeWebSocketChannelFactory((_) => fakeChannel)); socketClient.connect(); await socketClient.firstConnection; @@ -211,10 +196,7 @@ void main() { fakeChannel.addIncomingMessages(['{"t":"ack","d":1}']); // no more messages are expected - await expectLater( - fakeChannel.sentMessagesExceptPing, - emitsInOrder([]), - ); + await expectLater(fakeChannel.sentMessagesExceptPing, emitsInOrder([])); socketClient.close(); }); diff --git a/test/test_container.dart b/test/test_container.dart index 7e1ab64506..435d415720 100644 --- a/test/test_container.dart +++ b/test/test_container.dart @@ -49,9 +49,7 @@ Future makeContainer({ }) async { final binding = TestLichessBinding.ensureInitialized(); - FlutterSecureStorage.setMockInitialValues({ - kSRIStorageKey: 'test', - }); + FlutterSecureStorage.setMockInitialValues({kSRIStorageKey: 'test'}); final container = ProviderContainer( overrides: [ @@ -62,8 +60,7 @@ Future makeContainer({ return FakeNotificationDisplay(); }), databaseProvider.overrideWith((ref) async { - final db = - await openAppDatabase(databaseFactoryFfi, inMemoryDatabasePath); + final db = await openAppDatabase(databaseFactoryFfi, inMemoryDatabasePath); ref.onDispose(db.close); return db; }), diff --git a/test/test_helpers.dart b/test/test_helpers.dart index 3256f9b2ab..5dae185a43 100644 --- a/test/test_helpers.dart +++ b/test/test_helpers.dart @@ -37,19 +37,14 @@ const kTestSurfaces = [ /// iPhone 14 screen size. const kTestSurfaceSize = Size(_kTestScreenWidth, _kTestScreenHeight); -const kPlatformVariant = - TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS}); +const kPlatformVariant = TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS}); Matcher sameRequest(http.BaseRequest request) => _SameRequest(request); Matcher sameHeaders(Map headers) => _SameHeaders(headers); /// Mocks a surface with a given size. class TestSurface extends StatelessWidget { - const TestSurface({ - required this.child, - required this.size, - super.key, - }); + const TestSurface({required this.child, required this.size, super.key}); final Size size; final Widget child; @@ -58,11 +53,7 @@ class TestSurface extends StatelessWidget { Widget build(BuildContext context) { return MediaQuery( data: MediaQueryData(size: size), - child: SizedBox( - width: size.width, - height: size.height, - child: child, - ), + child: SizedBox(width: size.width, height: size.height, child: child), ); } } @@ -72,23 +63,12 @@ Future mockResponse( String body, int code, { Map headers = const {}, -}) => - Future.value( - http.Response( - body, - code, - headers: headers, - ), - ); +}) => Future.value(http.Response(body, code, headers: headers)); Future mockStreamedResponse(String body, int code) => - Future.value( - http.StreamedResponse(Stream.value(body).map(utf8.encode), code), - ); + Future.value(http.StreamedResponse(Stream.value(body).map(utf8.encode), code)); -Future mockHttpStreamFromIterable( - Iterable events, -) async { +Future mockHttpStreamFromIterable(Iterable events) async { return http.StreamedResponse( _streamFromFutures(events.map((e) => Future.value(utf8.encode(e)))), 200, @@ -115,24 +95,13 @@ Future meetsTapTargetGuideline(WidgetTester tester) async { } /// Returns the offset of a square on a board defined by [Rect]. -Offset squareOffset( - Square square, - Rect boardRect, { - Side orientation = Side.white, -}) { +Offset squareOffset(Square square, Rect boardRect, {Side orientation = Side.white}) { final squareSize = boardRect.width / 8; - final dx = - (orientation == Side.white ? square.file.value : 7 - square.file.value) * - squareSize; - final dy = - (orientation == Side.white ? 7 - square.rank.value : square.rank.value) * - squareSize; + final dx = (orientation == Side.white ? square.file.value : 7 - square.file.value) * squareSize; + final dy = (orientation == Side.white ? 7 - square.rank.value : square.rank.value) * squareSize; - return Offset( - dx + boardRect.left + squareSize / 2, - dy + boardRect.top + squareSize / 2, - ); + return Offset(dx + boardRect.left + squareSize / 2, dy + boardRect.top + squareSize / 2); } /// Plays a move on the board. @@ -144,13 +113,9 @@ Future playMove( Side orientation = Side.white, }) async { final rect = boardRect ?? tester.getRect(find.byType(Chessboard)); - await tester.tapAt( - squareOffset(Square.fromName(from), rect, orientation: orientation), - ); + await tester.tapAt(squareOffset(Square.fromName(from), rect, orientation: orientation)); await tester.pump(); - await tester.tapAt( - squareOffset(Square.fromName(to), rect, orientation: orientation), - ); + await tester.tapAt(squareOffset(Square.fromName(to), rect, orientation: orientation)); await tester.pump(); } diff --git a/test/test_provider_scope.dart b/test/test_provider_scope.dart index 81419fd5a0..05b4b91743 100644 --- a/test/test_provider_scope.dart +++ b/test/test_provider_scope.dart @@ -68,10 +68,7 @@ Future makeTestProviderScopeApp( localizationsDelegates: AppLocalizations.localizationsDelegates, home: home, builder: (context, child) { - return CupertinoTheme( - data: const CupertinoThemeData(), - child: Material(child: child), - ); + return CupertinoTheme(data: const CupertinoThemeData(), child: Material(child: child)); }, ), overrides: overrides, @@ -89,19 +86,18 @@ Future makeOfflineTestProviderScope( List? overrides, AuthSessionState? userSession, Map? defaultPreferences, -}) => - makeTestProviderScope( - tester, - child: child, - overrides: [ - httpClientFactoryProvider.overrideWith((ref) { - return FakeHttpClientFactory(() => offlineClient); - }), - ...overrides ?? [], - ], - userSession: userSession, - defaultPreferences: defaultPreferences, - ); +}) => makeTestProviderScope( + tester, + child: child, + overrides: [ + httpClientFactoryProvider.overrideWith((ref) { + return FakeHttpClientFactory(() => offlineClient); + }), + ...overrides ?? [], + ], + userSession: userSession, + defaultPreferences: defaultPreferences, +); /// Returns a [ProviderScope] and default mocks, ready for testing. /// @@ -134,28 +130,16 @@ Future makeTestProviderScope( // disable piece animation to simplify tests final defaultBoardPref = { - 'preferences.board': jsonEncode( - BoardPrefs.defaults - .copyWith( - pieceAnimation: false, - ) - .toJson(), - ), + 'preferences.board': jsonEncode(BoardPrefs.defaults.copyWith(pieceAnimation: false).toJson()), }; await binding.setInitialSharedPreferencesValues( - defaultPreferences != null - ? { - ...defaultBoardPref, - ...defaultPreferences, - } - : defaultBoardPref, + defaultPreferences != null ? {...defaultBoardPref, ...defaultPreferences} : defaultBoardPref, ); FlutterSecureStorage.setMockInitialValues({ kSRIStorageKey: 'test', - if (userSession != null) - kSessionStorageKey: jsonEncode(userSession.toJson()), + if (userSession != null) kSessionStorageKey: jsonEncode(userSession.toJson()), }); // TODO consider loading true fonts as well @@ -170,10 +154,7 @@ Future makeTestProviderScope( }), // ignore: scoped_providers_should_specify_dependencies databaseProvider.overrideWith((ref) async { - final testDb = await openAppDatabase( - databaseFactoryFfiNoIsolate, - inMemoryDatabasePath, - ); + final testDb = await openAppDatabase(databaseFactoryFfiNoIsolate, inMemoryDatabasePath); ref.onDispose(testDb.close); return testDb; }), @@ -226,23 +207,18 @@ Future makeTestProviderScope( }), ...overrides ?? [], ], - child: TestSurface( - size: surfaceSize, - child: child, - ), + child: TestSurface(size: surfaceSize, child: child), ); } -void _ignoreOverflowErrors( - FlutterErrorDetails details, { - bool forceReport = false, -}) { +void _ignoreOverflowErrors(FlutterErrorDetails details, {bool forceReport = false}) { bool isOverflowError = false; final exception = details.exception; if (exception is FlutterError) { - isOverflowError = exception.diagnostics - .any((e) => e.value.toString().contains('A RenderFlex overflowed by')); + isOverflowError = exception.diagnostics.any( + (e) => e.value.toString().contains('A RenderFlex overflowed by'), + ); } if (isOverflowError) { diff --git a/test/utils/duration_test.dart b/test/utils/duration_test.dart index 7405571d11..cacdc32270 100644 --- a/test/utils/duration_test.dart +++ b/test/utils/duration_test.dart @@ -24,23 +24,11 @@ void main() { group('DurationExtensions.toDaysHoursMinutes()', () { test('all values nonzero, plural', () { - testTimeStr( - mockAppLocalizations, - 2, - 2, - 2, - '2 days, 2 hours and 2 minutes', - ); + testTimeStr(mockAppLocalizations, 2, 2, 2, '2 days, 2 hours and 2 minutes'); }); test('all values nonzero, plural', () { - testTimeStr( - mockAppLocalizations, - 2, - 2, - 2, - '2 days, 2 hours and 2 minutes', - ); + testTimeStr(mockAppLocalizations, 2, 2, 2, '2 days, 2 hours and 2 minutes'); }); test('all values nonzero, single', () { @@ -84,7 +72,10 @@ void testTimeStr( int minutes, String expected, ) { - final timeStr = Duration(days: days, hours: hours, minutes: minutes) - .toDaysHoursMinutes(mockAppLocalizations); + final timeStr = Duration( + days: days, + hours: hours, + minutes: minutes, + ).toDaysHoursMinutes(mockAppLocalizations); expect(timeStr, expected); } diff --git a/test/utils/fake_connectivity.dart b/test/utils/fake_connectivity.dart index 3a920a4bdf..034e048467 100644 --- a/test/utils/fake_connectivity.dart +++ b/test/utils/fake_connectivity.dart @@ -12,10 +12,8 @@ class FakeConnectivity implements Connectivity { /// A broadcast stream controller of connectivity changes. /// /// This is used to simulate connectivity changes in tests. - static StreamController> controller = - StreamController.broadcast(); + static StreamController> controller = StreamController.broadcast(); @override - Stream> get onConnectivityChanged => - controller.stream; + Stream> get onConnectivityChanged => controller.stream; } diff --git a/test/utils/l10n_test.dart b/test/utils/l10n_test.dart index 72f178b3a7..2846a3b785 100644 --- a/test/utils/l10n_test.dart +++ b/test/utils/l10n_test.dart @@ -7,10 +7,7 @@ void main() { group('l10nWithWidget', () { const widget = Text('I am a widget'); test('placeholder in the middle', () { - final text = l10nWithWidget( - (_) => 'foo %s bar', - widget, - ); + final text = l10nWithWidget((_) => 'foo %s bar', widget); final children = (text.textSpan as TextSpan?)?.children; expect(children!.length, 3); expect((children[0] as TextSpan).text, 'foo '); @@ -19,18 +16,12 @@ void main() { }); test('no placeholder', () { - final text = l10nWithWidget( - (_) => 'foo bar', - widget, - ); + final text = l10nWithWidget((_) => 'foo bar', widget); expect(text.data, 'foo bar'); }); test('placeholder at the beginning', () { - final text = l10nWithWidget( - (_) => '%s foo bar', - widget, - ); + final text = l10nWithWidget((_) => '%s foo bar', widget); final children = (text.textSpan as TextSpan?)?.children; expect(children!.length, 2); expect((children[0] as WidgetSpan).child, widget); @@ -38,10 +29,7 @@ void main() { }); test('placeholder at the end', () { - final text = l10nWithWidget( - (_) => 'foo bar %s', - widget, - ); + final text = l10nWithWidget((_) => 'foo bar %s', widget); final children = (text.textSpan as TextSpan?)?.children; expect(children!.length, 2); expect((children[0] as TextSpan).text, 'foo bar '); diff --git a/test/utils/rate_limit_test.dart b/test/utils/rate_limit_test.dart index b652932bd3..78b2a54881 100644 --- a/test/utils/rate_limit_test.dart +++ b/test/utils/rate_limit_test.dart @@ -16,8 +16,7 @@ void main() { expect(called, true); }); - test('should not execute callback more than once if called multiple times', - () async { + test('should not execute callback more than once if called multiple times', () async { final debouncer = Debouncer(const Duration(milliseconds: 100)); var called = 0; debouncer(() { @@ -85,8 +84,7 @@ void main() { expect(called, 1); }); - test('should call the callback multiple times if delay is passed', - () async { + test('should call the callback multiple times if delay is passed', () async { final throttler = Throttler(const Duration(milliseconds: 100)); var called = 0; throttler(() { diff --git a/test/view/analysis/analysis_layout_test.dart b/test/view/analysis/analysis_layout_test.dart index fd0c130feb..e8ce54cb0f 100644 --- a/test/view/analysis/analysis_layout_test.dart +++ b/test/view/analysis/analysis_layout_test.dart @@ -11,136 +11,119 @@ import '../../test_helpers.dart'; import '../../test_provider_scope.dart'; void main() { - testWidgets( - 'board background size should match board size on all surfaces', - (WidgetTester tester) async { - for (final surface in kTestSurfaces) { - final app = await makeTestProviderScope( - key: ValueKey(surface), - tester, - child: MaterialApp( - home: DefaultTabController( - length: 1, - child: AnalysisLayout( - boardBuilder: (context, boardSize, boardRadius) { - return Chessboard.fixed( - size: boardSize, - fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR', - orientation: Side.white, - ); - }, - bottomBar: const SizedBox(height: kBottomBarHeight), - children: const [ - Center(child: Text('Analysis tab')), - ], - ), + testWidgets('board background size should match board size on all surfaces', ( + WidgetTester tester, + ) async { + for (final surface in kTestSurfaces) { + final app = await makeTestProviderScope( + key: ValueKey(surface), + tester, + child: MaterialApp( + home: DefaultTabController( + length: 1, + child: AnalysisLayout( + boardBuilder: (context, boardSize, boardRadius) { + return Chessboard.fixed( + size: boardSize, + fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR', + orientation: Side.white, + ); + }, + bottomBar: const SizedBox(height: kBottomBarHeight), + children: const [Center(child: Text('Analysis tab'))], ), ), - surfaceSize: surface, - ); - await tester.pumpWidget(app); + ), + surfaceSize: surface, + ); + await tester.pumpWidget(app); - final backgroundSize = tester.getSize( - find.byType(SolidColorChessboardBackground), - ); + final backgroundSize = tester.getSize(find.byType(SolidColorChessboardBackground)); - expect( - backgroundSize.width, - backgroundSize.height, - reason: 'Board background size is square on $surface', - ); + expect( + backgroundSize.width, + backgroundSize.height, + reason: 'Board background size is square on $surface', + ); - final boardSize = tester.getSize(find.byType(Chessboard)); + final boardSize = tester.getSize(find.byType(Chessboard)); - expect( - boardSize.width, - boardSize.height, - reason: 'Board size is square on $surface', - ); + expect(boardSize.width, boardSize.height, reason: 'Board size is square on $surface'); - expect( - boardSize, - backgroundSize, - reason: 'Board size should match background size on $surface', - ); - } - }, - variant: kPlatformVariant, - ); + expect( + boardSize, + backgroundSize, + reason: 'Board size should match background size on $surface', + ); + } + }, variant: kPlatformVariant); - testWidgets( - 'board size and table side size should be harmonious on all surfaces', - (WidgetTester tester) async { - for (final surface in kTestSurfaces) { - final app = await makeTestProviderScope( - key: ValueKey(surface), - tester, - child: MaterialApp( - home: DefaultTabController( - length: 1, - child: AnalysisLayout( - boardBuilder: (context, boardSize, boardRadius) { - return Chessboard.fixed( - size: boardSize, - fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR', - orientation: Side.white, - ); - }, - bottomBar: const SizedBox(height: kBottomBarHeight), - children: const [ - Center(child: Text('Analysis tab')), - ], - ), + testWidgets('board size and table side size should be harmonious on all surfaces', ( + WidgetTester tester, + ) async { + for (final surface in kTestSurfaces) { + final app = await makeTestProviderScope( + key: ValueKey(surface), + tester, + child: MaterialApp( + home: DefaultTabController( + length: 1, + child: AnalysisLayout( + boardBuilder: (context, boardSize, boardRadius) { + return Chessboard.fixed( + size: boardSize, + fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR', + orientation: Side.white, + ); + }, + bottomBar: const SizedBox(height: kBottomBarHeight), + children: const [Center(child: Text('Analysis tab'))], ), ), - surfaceSize: surface, - ); - await tester.pumpWidget(app); + ), + surfaceSize: surface, + ); + await tester.pumpWidget(app); - final isPortrait = surface.aspectRatio < 1.0; - final isTablet = surface.shortestSide > 600; - final boardSize = tester.getSize(find.byType(Chessboard)); + final isPortrait = surface.aspectRatio < 1.0; + final isTablet = surface.shortestSide > 600; + final boardSize = tester.getSize(find.byType(Chessboard)); - if (isPortrait) { - final expectedBoardSize = - isTablet ? surface.width - 32.0 : surface.width; - expect( - boardSize, - Size(expectedBoardSize, expectedBoardSize), - reason: 'Board size should match surface width on $surface', - ); - } else { - final tabBarViewSize = tester.getSize(find.byType(TabBarView)); - final goldenBoardSize = (surface.longestSide / kGoldenRatio) - 32.0; - final defaultBoardSize = - surface.shortestSide - kBottomBarHeight - 32.0; - final minBoardSize = min(goldenBoardSize, defaultBoardSize); - final maxBoardSize = max(goldenBoardSize, defaultBoardSize); - // TabBarView is inside a Card so we need to account for its padding - const cardPadding = 8.0; - final minSideWidth = min( - surface.longestSide - goldenBoardSize - 16.0 * 3 - cardPadding, - 250.0, - ); - expect( - boardSize.width, - greaterThanOrEqualTo(minBoardSize), - reason: 'Board size should be at least $minBoardSize on $surface', - ); - expect( - boardSize.width, - lessThanOrEqualTo(maxBoardSize), - reason: 'Board size should be at most $maxBoardSize on $surface', - ); - expect( - tabBarViewSize.width, - greaterThanOrEqualTo(minSideWidth), - reason: - 'Tab bar view width should be at least $minSideWidth on $surface', - ); - } + if (isPortrait) { + final expectedBoardSize = isTablet ? surface.width - 32.0 : surface.width; + expect( + boardSize, + Size(expectedBoardSize, expectedBoardSize), + reason: 'Board size should match surface width on $surface', + ); + } else { + final tabBarViewSize = tester.getSize(find.byType(TabBarView)); + final goldenBoardSize = (surface.longestSide / kGoldenRatio) - 32.0; + final defaultBoardSize = surface.shortestSide - kBottomBarHeight - 32.0; + final minBoardSize = min(goldenBoardSize, defaultBoardSize); + final maxBoardSize = max(goldenBoardSize, defaultBoardSize); + // TabBarView is inside a Card so we need to account for its padding + const cardPadding = 8.0; + final minSideWidth = min( + surface.longestSide - goldenBoardSize - 16.0 * 3 - cardPadding, + 250.0, + ); + expect( + boardSize.width, + greaterThanOrEqualTo(minBoardSize), + reason: 'Board size should be at least $minBoardSize on $surface', + ); + expect( + boardSize.width, + lessThanOrEqualTo(maxBoardSize), + reason: 'Board size should be at most $maxBoardSize on $surface', + ); + expect( + tabBarViewSize.width, + greaterThanOrEqualTo(minSideWidth), + reason: 'Tab bar view width should be at least $minSideWidth on $surface', + ); } - }, - variant: kPlatformVariant, - ); + } + }, variant: kPlatformVariant); } diff --git a/test/view/analysis/analysis_screen_test.dart b/test/view/analysis/analysis_screen_test.dart index 00ea3fcf06..df63b7eb08 100644 --- a/test/view/analysis/analysis_screen_test.dart +++ b/test/view/analysis/analysis_screen_test.dart @@ -44,12 +44,7 @@ void main() { expect(currentMove, findsOneWidget); expect( tester - .widget( - find.ancestor( - of: currentMove, - matching: find.byType(InlineMove), - ), - ) + .widget(find.ancestor(of: currentMove, matching: find.byType(InlineMove))) .isCurrentMove, isTrue, ); @@ -75,18 +70,11 @@ void main() { await tester.pump(const Duration(milliseconds: 1)); // cannot go forward - expect( - tester - .widget(find.byKey(const Key('goto-next'))) - .onTap, - isNull, - ); + expect(tester.widget(find.byKey(const Key('goto-next'))).onTap, isNull); // can go back expect( - tester - .widget(find.byKey(const Key('goto-previous'))) - .onTap, + tester.widget(find.byKey(const Key('goto-previous'))).onTap, isNotNull, ); @@ -98,12 +86,7 @@ void main() { expect(currentMove, findsOneWidget); expect( tester - .widget( - find.ancestor( - of: currentMove, - matching: find.byType(InlineMove), - ), - ) + .widget(find.ancestor(of: currentMove, matching: find.byType(InlineMove))) .isCurrentMove, isTrue, ); @@ -111,29 +94,18 @@ void main() { }); group('Analysis Tree View', () { - Future buildTree( - WidgetTester tester, - String pgn, - ) async { + Future buildTree(WidgetTester tester, String pgn) async { final app = await makeTestProviderScopeApp( tester, defaultPreferences: { PrefCategory.analysis.storageKey: jsonEncode( - AnalysisPrefs.defaults - .copyWith( - enableLocalEvaluation: false, - ) - .toJson(), + AnalysisPrefs.defaults.copyWith(enableLocalEvaluation: false).toJson(), ), }, home: AnalysisScreen( options: AnalysisOptions( orientation: Side.white, - standalone: ( - pgn: pgn, - isComputerAnalysisAllowed: false, - variant: Variant.standard, - ), + standalone: (pgn: pgn, isComputerAnalysisAllowed: false, variant: Variant.standard), ), enableDrawingShapes: false, ), @@ -144,12 +116,7 @@ void main() { } Text parentText(WidgetTester tester, String move) { - return tester.widget( - find.ancestor( - of: find.text(move), - matching: find.byType(Text), - ), - ); + return tester.widget(find.ancestor(of: find.text(move), matching: find.byType(Text))); } void expectSameLine(WidgetTester tester, Iterable moves) { @@ -158,23 +125,14 @@ void main() { for (final move in moves.skip(1)) { final moveText = find.text(move); expect(moveText, findsOneWidget); - expect( - parentText(tester, move), - line, - ); + expect(parentText(tester, move), line); } } - void expectDifferentLines( - WidgetTester tester, - List moves, - ) { + void expectDifferentLines(WidgetTester tester, List moves) { for (int i = 0; i < moves.length; i++) { for (int j = i + 1; j < moves.length; j++) { - expect( - parentText(tester, moves[i]), - isNot(parentText(tester, moves[j])), - ); + expect(parentText(tester, moves[i]), isNot(parentText(tester, moves[j]))); } } } @@ -182,33 +140,23 @@ void main() { testWidgets('displays short sideline as inline', (tester) async { await buildTree(tester, '1. e4 e5 (1... d5 2. exd5) 2. Nf3 *'); - final mainline = find.ancestor( - of: find.text('1. e4'), - matching: find.byType(Text), - ); + final mainline = find.ancestor(of: find.text('1. e4'), matching: find.byType(Text)); expect(mainline, findsOneWidget); expectSameLine(tester, ['1. e4', 'e5', '1… d5', '2. exd5', '2. Nf3']); }); testWidgets('displays long sideline on its own line', (tester) async { - await buildTree( - tester, - '1. e4 e5 (1... d5 2. exd5 Qxd5 3. Nc3 Qd8 4. d4 Nf6) 2. Nc3 *', - ); + await buildTree(tester, '1. e4 e5 (1... d5 2. exd5 Qxd5 3. Nc3 Qd8 4. d4 Nf6) 2. Nc3 *'); expectSameLine(tester, ['1. e4', 'e5']); - expectSameLine( - tester, - ['1… d5', '2. exd5', 'Qxd5', '3. Nc3', 'Qd8', '4. d4', 'Nf6'], - ); + expectSameLine(tester, ['1… d5', '2. exd5', 'Qxd5', '3. Nc3', 'Qd8', '4. d4', 'Nf6']); expectSameLine(tester, ['2. Nc3']); expectDifferentLines(tester, ['1. e4', '1… d5', '2. Nc3']); }); - testWidgets('displays sideline with branching on its own line', - (tester) async { + testWidgets('displays sideline with branching on its own line', (tester) async { await buildTree(tester, '1. e4 e5 (1... d5 2. exd5 (2. Nc3)) *'); expectSameLine(tester, ['1. e4', 'e5']); @@ -220,10 +168,7 @@ void main() { }); testWidgets('multiple sidelines', (tester) async { - await buildTree( - tester, - '1. e4 e5 (1... d5 2. exd5) (1... Nf6 2. e5) 2. Nf3 Nc6 (2... a5) *', - ); + await buildTree(tester, '1. e4 e5 (1... d5 2. exd5) (1... Nf6 2. e5) 2. Nf3 Nc6 (2... a5) *'); expectSameLine(tester, ['1. e4', 'e5']); expectSameLine(tester, ['1… d5', '2. exd5']); @@ -234,10 +179,7 @@ void main() { }); testWidgets('collapses lines with nesting > 2', (tester) async { - await buildTree( - tester, - '1. e4 e5 (1... d5 2. Nc3 (2. h4 h5 (2... Nc6 3. d3) (2... Qd7))) *', - ); + await buildTree(tester, '1. e4 e5 (1... d5 2. Nc3 (2. h4 h5 (2... Nc6 3. d3) (2... Qd7))) *'); expectSameLine(tester, ['1. e4', 'e5']); expectSameLine(tester, ['1… d5']); @@ -277,9 +219,9 @@ void main() { expect(find.text('2… Qd7'), findsNothing); }); - testWidgets( - 'Expanding one line does not expand the following one (regression test)', - (tester) async { + testWidgets('Expanding one line does not expand the following one (regression test)', ( + tester, + ) async { /// Will be rendered as: /// ------------------- /// 1. e4 e5 @@ -287,10 +229,7 @@ void main() { /// 2. Nf3 /// |- 2. a4 d5 (2... f5) /// ------------------- - await buildTree( - tester, - '1. e4 e5 (1... d5 2. Nf3 (2. Nc3)) 2. Nf3 (2. a4 d5 (2... f5))', - ); + await buildTree(tester, '1. e4 e5 (1... d5 2. Nf3 (2. Nc3)) 2. Nf3 (2. a4 d5 (2... f5))'); expect(find.byIcon(Icons.add_box), findsNothing); @@ -323,12 +262,8 @@ void main() { expect(find.text('2. a4'), findsNothing); }); - testWidgets('subtrees not part of the current mainline part are cached', - (tester) async { - await buildTree( - tester, - '1. e4 e5 (1... d5 2. exd5) (1... Nf6 2. e5) 2. Nf3 Nc6 (2... a5) *', - ); + testWidgets('subtrees not part of the current mainline part are cached', (tester) async { + await buildTree(tester, '1. e4 e5 (1... d5 2. exd5) (1... Nf6 2. e5) 2. Nf3 Nc6 (2... a5) *'); // will be rendered as: // ------------------- @@ -346,10 +281,7 @@ void main() { expect( tester .widgetList( - find.ancestor( - of: find.textContaining('Nc6'), - matching: find.byType(InlineMove), - ), + find.ancestor(of: find.textContaining('Nc6'), matching: find.byType(InlineMove)), ) .last .isCurrentMove, @@ -363,10 +295,7 @@ void main() { expect( tester .widgetList( - find.ancestor( - of: find.textContaining('Nf3'), - matching: find.byType(InlineMove), - ), + find.ancestor(of: find.textContaining('Nf3'), matching: find.byType(InlineMove)), ) .last .isCurrentMove, @@ -375,18 +304,12 @@ void main() { // first mainline part has not changed since the current move is // not part of it - expect( - identical(firstMainlinePart, parentText(tester, '1. e4')), - isTrue, - ); + expect(identical(firstMainlinePart, parentText(tester, '1. e4')), isTrue); final secondMainlinePartOnMoveNf3 = parentText(tester, '2. Nf3'); // second mainline part has changed since the current move is part of it - expect( - secondMainlinePart, - isNot(secondMainlinePartOnMoveNf3), - ); + expect(secondMainlinePart, isNot(secondMainlinePartOnMoveNf3)); await tester.tap(find.byKey(const Key('goto-previous'))); // need to wait for current move change debounce delay @@ -395,10 +318,7 @@ void main() { expect( tester .widgetList( - find.ancestor( - of: find.textContaining('e5'), - matching: find.byType(InlineMove), - ), + find.ancestor(of: find.textContaining('e5'), matching: find.byType(InlineMove)), ) .first .isCurrentMove, @@ -409,17 +329,11 @@ void main() { final secondMainlinePartOnMoveE5 = parentText(tester, '2. Nf3'); // first mainline part has changed since the current move is part of it - expect( - firstMainlinePart, - isNot(firstMainlinePartOnMoveE5), - ); + expect(firstMainlinePart, isNot(firstMainlinePartOnMoveE5)); // second mainline part has changed since the current move is not part of it // anymore - expect( - secondMainlinePartOnMoveNf3, - isNot(secondMainlinePartOnMoveE5), - ); + expect(secondMainlinePartOnMoveNf3, isNot(secondMainlinePartOnMoveE5)); await tester.tap(find.byKey(const Key('goto-previous'))); // need to wait for current move change debounce delay @@ -428,10 +342,7 @@ void main() { expect( tester .firstWidget( - find.ancestor( - of: find.textContaining('e4'), - matching: find.byType(InlineMove), - ), + find.ancestor(of: find.textContaining('e4'), matching: find.byType(InlineMove)), ) .isCurrentMove, isTrue, @@ -441,16 +352,10 @@ void main() { final secondMainlinePartOnMoveE4 = parentText(tester, '2. Nf3'); // first mainline part has changed since the current move is part of it - expect( - firstMainlinePartOnMoveE4, - isNot(firstMainlinePartOnMoveE5), - ); + expect(firstMainlinePartOnMoveE4, isNot(firstMainlinePartOnMoveE5)); // second mainline part has not changed since the current move is not part of it - expect( - identical(secondMainlinePartOnMoveE5, secondMainlinePartOnMoveE4), - isTrue, - ); + expect(identical(secondMainlinePartOnMoveE5, secondMainlinePartOnMoveE4), isTrue); }); }); } diff --git a/test/view/board_editor/board_editor_screen_test.dart b/test/view/board_editor/board_editor_screen_test.dart index 2e6b4381d1..320d486d8b 100644 --- a/test/view/board_editor/board_editor_screen_test.dart +++ b/test/view/board_editor/board_editor_screen_test.dart @@ -12,63 +12,42 @@ import '../../test_provider_scope.dart'; void main() { group('Board Editor', () { testWidgets('Displays initial FEN on start', (tester) async { - final app = await makeTestProviderScopeApp( - tester, - home: const BoardEditorScreen(), - ); + final app = await makeTestProviderScopeApp(tester, home: const BoardEditorScreen()); await tester.pumpWidget(app); - final editor = tester.widget( - find.byType(ChessboardEditor), - ); + final editor = tester.widget(find.byType(ChessboardEditor)); expect(editor.pieces, readFen(kInitialFEN)); expect(editor.orientation, Side.white); expect(editor.pointerMode, EditorPointerMode.drag); // Legal position, so allowed top open analysis board expect( - tester - .widget( - find.byKey(const Key('analysis-board-button')), - ) - .onTap, + tester.widget(find.byKey(const Key('analysis-board-button'))).onTap, isNotNull, ); }); testWidgets('Flip board', (tester) async { - final app = await makeTestProviderScopeApp( - tester, - home: const BoardEditorScreen(), - ); + final app = await makeTestProviderScopeApp(tester, home: const BoardEditorScreen()); await tester.pumpWidget(app); await tester.tap(find.byKey(const Key('flip-button'))); await tester.pump(); expect( - tester - .widget( - find.byType(ChessboardEditor), - ) - .orientation, + tester.widget(find.byType(ChessboardEditor)).orientation, Side.black, ); }); testWidgets('Side to play and castling rights', (tester) async { - final app = await makeTestProviderScopeApp( - tester, - home: const BoardEditorScreen(), - ); + final app = await makeTestProviderScopeApp(tester, home: const BoardEditorScreen()); await tester.pumpWidget(app); await tester.tap(find.byKey(const Key('flip-button'))); await tester.pump(); - final container = ProviderScope.containerOf( - tester.element(find.byType(ChessboardEditor)), - ); + final container = ProviderScope.containerOf(tester.element(find.byType(ChessboardEditor))); final controllerProvider = boardEditorControllerProvider(null); @@ -78,9 +57,7 @@ void main() { 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR b KQkq - 0 1', ); - container - .read(controllerProvider.notifier) - .setCastling(Side.white, CastlingSide.king, false); + container.read(controllerProvider.notifier).setCastling(Side.white, CastlingSide.king, false); expect( container.read(controllerProvider).fen, 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR b Qkq - 0 1', @@ -94,9 +71,7 @@ void main() { 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR b kq - 0 1', ); - container - .read(controllerProvider.notifier) - .setCastling(Side.black, CastlingSide.king, false); + container.read(controllerProvider.notifier).setCastling(Side.black, CastlingSide.king, false); expect( container.read(controllerProvider).fen, 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR b q - 0 1', @@ -110,9 +85,7 @@ void main() { 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR b - - 0 1', ); - container - .read(controllerProvider.notifier) - .setCastling(Side.white, CastlingSide.king, true); + container.read(controllerProvider.notifier).setCastling(Side.white, CastlingSide.king, true); expect( container.read(controllerProvider).fen, 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR b K - 0 1', @@ -120,15 +93,10 @@ void main() { }); testWidgets('Castling rights ignored when rook is missing', (tester) async { - final app = await makeTestProviderScopeApp( - tester, - home: const BoardEditorScreen(), - ); + final app = await makeTestProviderScopeApp(tester, home: const BoardEditorScreen()); await tester.pumpWidget(app); - final container = ProviderScope.containerOf( - tester.element(find.byType(ChessboardEditor)), - ); + final container = ProviderScope.containerOf(tester.element(find.byType(ChessboardEditor))); final controllerProvider = boardEditorControllerProvider(null); // Starting position, but with all rooks removed @@ -144,15 +112,10 @@ void main() { }); testWidgets('support chess960 castling rights', (tester) async { - final app = await makeTestProviderScopeApp( - tester, - home: const BoardEditorScreen(), - ); + final app = await makeTestProviderScopeApp(tester, home: const BoardEditorScreen()); await tester.pumpWidget(app); - final container = ProviderScope.containerOf( - tester.element(find.byType(ChessboardEditor)), - ); + final container = ProviderScope.containerOf(tester.element(find.byType(ChessboardEditor))); final controllerProvider = boardEditorControllerProvider(null); container @@ -165,17 +128,11 @@ void main() { ); }); - testWidgets('Castling rights ignored when king is not in backrank', - (tester) async { - final app = await makeTestProviderScopeApp( - tester, - home: const BoardEditorScreen(), - ); + testWidgets('Castling rights ignored when king is not in backrank', (tester) async { + final app = await makeTestProviderScopeApp(tester, home: const BoardEditorScreen()); await tester.pumpWidget(app); - final container = ProviderScope.containerOf( - tester.element(find.byType(ChessboardEditor)), - ); + final container = ProviderScope.containerOf(tester.element(find.byType(ChessboardEditor))); final controllerProvider = boardEditorControllerProvider(null); container @@ -188,37 +145,28 @@ void main() { ); }); - testWidgets('Possible en passant squares are calculated correctly', - (tester) async { - final app = await makeTestProviderScopeApp( - tester, - home: const BoardEditorScreen(), - ); + testWidgets('Possible en passant squares are calculated correctly', (tester) async { + final app = await makeTestProviderScopeApp(tester, home: const BoardEditorScreen()); await tester.pumpWidget(app); - final container = ProviderScope.containerOf( - tester.element(find.byType(ChessboardEditor)), - ); + final container = ProviderScope.containerOf(tester.element(find.byType(ChessboardEditor))); final controllerProvider = boardEditorControllerProvider(null); container .read(controllerProvider.notifier) .loadFen('1nbqkbn1/pppppppp/8/8/8/8/PPPPPPPP/1NBQKBN1'); - expect( - container.read(controllerProvider).enPassantOptions, - SquareSet.empty, - ); + expect(container.read(controllerProvider).enPassantOptions, SquareSet.empty); - container.read(controllerProvider.notifier).loadFen( - 'r1bqkbnr/4p1p1/3n4/pPppPppP/8/8/P1PP1P2/RNBQKBNR w KQkq - 0 1', - ); + container + .read(controllerProvider.notifier) + .loadFen('r1bqkbnr/4p1p1/3n4/pPppPppP/8/8/P1PP1P2/RNBQKBNR w KQkq - 0 1'); expect( container.read(controllerProvider).enPassantOptions, SquareSet.fromSquares([Square.a6, Square.c6, Square.f6]), ); - container.read(controllerProvider.notifier).loadFen( - 'rnbqkbnr/pp1p1p1p/8/8/PpPpPQpP/8/NPRP1PP1/2B1KBNR b Kkq - 0 1', - ); + container + .read(controllerProvider.notifier) + .loadFen('rnbqkbnr/pp1p1p1p/8/8/PpPpPQpP/8/NPRP1PP1/2B1KBNR b Kkq - 0 1'); container.read(controllerProvider.notifier).setSideToPlay(Side.black); expect( container.read(controllerProvider).enPassantOptions, @@ -227,15 +175,10 @@ void main() { }); testWidgets('Can drag pieces to new squares', (tester) async { - final app = await makeTestProviderScopeApp( - tester, - home: const BoardEditorScreen(), - ); + final app = await makeTestProviderScopeApp(tester, home: const BoardEditorScreen()); await tester.pumpWidget(app); - final container = ProviderScope.containerOf( - tester.element(find.byType(ChessboardEditor)), - ); + final container = ProviderScope.containerOf(tester.element(find.byType(ChessboardEditor))); final controllerProvider = boardEditorControllerProvider(null); // Two legal moves by white @@ -256,35 +199,23 @@ void main() { }); testWidgets('illegal position cannot be analyzed', (tester) async { - final app = await makeTestProviderScopeApp( - tester, - home: const BoardEditorScreen(), - ); + final app = await makeTestProviderScopeApp(tester, home: const BoardEditorScreen()); await tester.pumpWidget(app); // White queen "captures" white king => illegal position await dragFromTo(tester, 'd1', 'e1'); expect( - tester - .widget( - find.byKey(const Key('analysis-board-button')), - ) - .onTap, + tester.widget(find.byKey(const Key('analysis-board-button'))).onTap, isNull, ); }); testWidgets('Delete pieces via bin button', (tester) async { - final app = await makeTestProviderScopeApp( - tester, - home: const BoardEditorScreen(), - ); + final app = await makeTestProviderScopeApp(tester, home: const BoardEditorScreen()); await tester.pumpWidget(app); - final container = ProviderScope.containerOf( - tester.element(find.byType(ChessboardEditor)), - ); + final container = ProviderScope.containerOf(tester.element(find.byType(ChessboardEditor))); final controllerProvider = boardEditorControllerProvider(null); await tester.tap(find.byKey(const Key('delete-button-white'))); @@ -318,10 +249,7 @@ void main() { }); testWidgets('Add pieces via tap and pan', (tester) async { - final app = await makeTestProviderScopeApp( - tester, - home: const BoardEditorScreen(), - ); + final app = await makeTestProviderScopeApp(tester, home: const BoardEditorScreen()); await tester.pumpWidget(app); await tester.tap(find.byKey(const Key('piece-button-white-queen'))); @@ -330,9 +258,7 @@ void main() { await tapSquare(tester, 'h1'); await tapSquare(tester, 'h3'); - final container = ProviderScope.containerOf( - tester.element(find.byType(ChessboardEditor)), - ); + final container = ProviderScope.containerOf(tester.element(find.byType(ChessboardEditor))); final controllerProvider = boardEditorControllerProvider(null); expect( @@ -342,37 +268,27 @@ void main() { }); testWidgets('Drag pieces onto the board', (tester) async { - final app = await makeTestProviderScopeApp( - tester, - home: const BoardEditorScreen(), - ); + final app = await makeTestProviderScopeApp(tester, home: const BoardEditorScreen()); await tester.pumpWidget(app); // Start by pressing bin button, dragging a piece should override this await tester.tap(find.byKey(const Key('delete-button-black'))); await tester.pump(); - final pieceButtonOffset = - tester.getCenter(find.byKey(const Key('piece-button-white-pawn'))); + final pieceButtonOffset = tester.getCenter(find.byKey(const Key('piece-button-white-pawn'))); await tester.dragFrom( pieceButtonOffset, tester.getCenter(find.byKey(const Key('d3-empty'))) - pieceButtonOffset, ); await tester.dragFrom( pieceButtonOffset, - tester.getCenter(find.byKey(const Key('d1-whitequeen'))) - - pieceButtonOffset, + tester.getCenter(find.byKey(const Key('d1-whitequeen'))) - pieceButtonOffset, ); - final container = ProviderScope.containerOf( - tester.element(find.byType(ChessboardEditor)), - ); + final container = ProviderScope.containerOf(tester.element(find.byType(ChessboardEditor))); final controllerProvider = boardEditorControllerProvider(null); - expect( - container.read(controllerProvider).editorPointerMode, - EditorPointerMode.drag, - ); + expect(container.read(controllerProvider).editorPointerMode, EditorPointerMode.drag); expect( container.read(controllerProvider).fen, @@ -382,25 +298,14 @@ void main() { }); } -Future dragFromTo( - WidgetTester tester, - String from, - String to, -) async { +Future dragFromTo(WidgetTester tester, String from, String to) async { final fromOffset = squareOffset(tester, Square.fromName(from)); - await tester.dragFrom( - fromOffset, - squareOffset(tester, Square.fromName(to)) - fromOffset, - ); + await tester.dragFrom(fromOffset, squareOffset(tester, Square.fromName(to)) - fromOffset); await tester.pumpAndSettle(); } -Future panFromTo( - WidgetTester tester, - String from, - String to, -) async { +Future panFromTo(WidgetTester tester, String from, String to) async { final fromOffset = squareOffset(tester, Square.fromName(from)); await tester.timedDragFrom( diff --git a/test/view/broadcast/broadcasts_list_screen_test.dart b/test/view/broadcast/broadcasts_list_screen_test.dart index fb845378c6..2a90722cb6 100644 --- a/test/view/broadcast/broadcasts_list_screen_test.dart +++ b/test/view/broadcast/broadcasts_list_screen_test.dart @@ -45,63 +45,53 @@ final client = MockClient((request) { void main() { group('BroadcastListScreen', () { - testWidgets( - 'Displays broadcast tournament screen', - variant: kPlatformVariant, - (tester) async { - mockNetworkImagesFor(() async { - final app = await makeTestProviderScopeApp( - tester, - home: const BroadcastListScreen(), - overrides: [ - lichessClientProvider - .overrideWith((ref) => LichessClient(client, ref)), - broadcastImageWorkerFactoryProvider.overrideWith( - (ref) => FakeBroadcastImageWorkerFactory(), - ), - ], - ); - - await tester.pumpWidget(app); - - expect(find.byType(CircularProgressIndicator), findsOneWidget); - - // wait for broadcast tournaments to load - await tester.pump(const Duration(milliseconds: 100)); - - expect(find.byType(BroadcastCard), findsAtLeast(1)); - }); - }, - ); - - testWidgets( - 'Scroll broadcast tournament screen', - variant: kPlatformVariant, - (tester) async { - mockNetworkImagesFor(() async { - final app = await makeTestProviderScopeApp( - tester, - home: const BroadcastListScreen(), - overrides: [ - lichessClientProvider - .overrideWith((ref) => LichessClient(client, ref)), - broadcastImageWorkerFactoryProvider.overrideWith( - (ref) => FakeBroadcastImageWorkerFactory(), - ), - ], - ); - - await tester.pumpWidget(app); - - expect(find.byType(CircularProgressIndicator), findsOneWidget); - - // wait for broadcast tournaments to load - await tester.pump(const Duration(milliseconds: 100)); - - await tester.scrollUntilVisible(find.text('Completed'), 200.0); - }); - }, - ); + testWidgets('Displays broadcast tournament screen', variant: kPlatformVariant, (tester) async { + mockNetworkImagesFor(() async { + final app = await makeTestProviderScopeApp( + tester, + home: const BroadcastListScreen(), + overrides: [ + lichessClientProvider.overrideWith((ref) => LichessClient(client, ref)), + broadcastImageWorkerFactoryProvider.overrideWith( + (ref) => FakeBroadcastImageWorkerFactory(), + ), + ], + ); + + await tester.pumpWidget(app); + + expect(find.byType(CircularProgressIndicator), findsOneWidget); + + // wait for broadcast tournaments to load + await tester.pump(const Duration(milliseconds: 100)); + + expect(find.byType(BroadcastCard), findsAtLeast(1)); + }); + }); + + testWidgets('Scroll broadcast tournament screen', variant: kPlatformVariant, (tester) async { + mockNetworkImagesFor(() async { + final app = await makeTestProviderScopeApp( + tester, + home: const BroadcastListScreen(), + overrides: [ + lichessClientProvider.overrideWith((ref) => LichessClient(client, ref)), + broadcastImageWorkerFactoryProvider.overrideWith( + (ref) => FakeBroadcastImageWorkerFactory(), + ), + ], + ); + + await tester.pumpWidget(app); + + expect(find.byType(CircularProgressIndicator), findsOneWidget); + + // wait for broadcast tournaments to load + await tester.pump(const Duration(milliseconds: 100)); + + await tester.scrollUntilVisible(find.text('Completed'), 200.0); + }); + }); }); } diff --git a/test/view/coordinate_training/coordinate_training_screen_test.dart b/test/view/coordinate_training/coordinate_training_screen_test.dart index ab5f65e5b9..687a2b9829 100644 --- a/test/view/coordinate_training/coordinate_training_screen_test.dart +++ b/test/view/coordinate_training/coordinate_training_screen_test.dart @@ -11,25 +11,17 @@ import '../../test_provider_scope.dart'; void main() { group('Coordinate Training', () { - testWidgets('Initial state when started in FindSquare mode', - (tester) async { - final app = await makeTestProviderScopeApp( - tester, - home: const CoordinateTrainingScreen(), - ); + testWidgets('Initial state when started in FindSquare mode', (tester) async { + final app = await makeTestProviderScopeApp(tester, home: const CoordinateTrainingScreen()); await tester.pumpWidget(app); await tester.tap(find.text('Start Training')); await tester.pumpAndSettle(); - final container = ProviderScope.containerOf( - tester.element(find.byType(ChessboardEditor)), - ); + final container = ProviderScope.containerOf(tester.element(find.byType(ChessboardEditor))); final controllerProvider = coordinateTrainingControllerProvider; - final trainingPrefsNotifier = container.read( - coordinateTrainingPreferencesProvider.notifier, - ); + final trainingPrefsNotifier = container.read(coordinateTrainingPreferencesProvider.notifier); trainingPrefsNotifier.setMode(TrainingMode.findSquare); // This way all squares can be found via find.byKey(ValueKey('${square.name}-empty')) trainingPrefsNotifier.setShowPieces(false); @@ -41,34 +33,21 @@ void main() { expect(container.read(controllerProvider).trainingActive, true); // Current and next coordinate prompt should be displayed - expect( - find.text(container.read(controllerProvider).currentCoord!.name), - findsOneWidget, - ); - expect( - find.text(container.read(controllerProvider).nextCoord!.name), - findsOneWidget, - ); + expect(find.text(container.read(controllerProvider).currentCoord!.name), findsOneWidget); + expect(find.text(container.read(controllerProvider).nextCoord!.name), findsOneWidget); }); testWidgets('Tap wrong square', (tester) async { - final app = await makeTestProviderScopeApp( - tester, - home: const CoordinateTrainingScreen(), - ); + final app = await makeTestProviderScopeApp(tester, home: const CoordinateTrainingScreen()); await tester.pumpWidget(app); await tester.tap(find.text('Start Training')); await tester.pumpAndSettle(); - final container = ProviderScope.containerOf( - tester.element(find.byType(ChessboardEditor)), - ); + final container = ProviderScope.containerOf(tester.element(find.byType(ChessboardEditor))); final controllerProvider = coordinateTrainingControllerProvider; - final trainingPrefsNotifier = container.read( - coordinateTrainingPreferencesProvider.notifier, - ); + final trainingPrefsNotifier = container.read(coordinateTrainingPreferencesProvider.notifier); trainingPrefsNotifier.setMode(TrainingMode.findSquare); // This way all squares can be found via find.byKey(ValueKey('${square.name}-empty')) trainingPrefsNotifier.setShowPieces(false); @@ -77,8 +56,7 @@ void main() { final currentCoord = container.read(controllerProvider).currentCoord; final nextCoord = container.read(controllerProvider).nextCoord; - final wrongCoord = - Square.values[(currentCoord! + 1) % Square.values.length]; + final wrongCoord = Square.values[(currentCoord! + 1) % Square.values.length]; await tester.tap(find.byKey(ValueKey('${wrongCoord.name}-empty'))); await tester.pump(); @@ -88,36 +66,23 @@ void main() { expect(container.read(controllerProvider).nextCoord, nextCoord); expect(container.read(controllerProvider).trainingActive, true); - expect( - find.byKey(ValueKey('${wrongCoord.name}-highlight')), - findsOneWidget, - ); + expect(find.byKey(ValueKey('${wrongCoord.name}-highlight')), findsOneWidget); await tester.pump(const Duration(milliseconds: 300)); - expect( - find.byKey(ValueKey('${wrongCoord.name}-highlight')), - findsNothing, - ); + expect(find.byKey(ValueKey('${wrongCoord.name}-highlight')), findsNothing); }); testWidgets('Tap correct square', (tester) async { - final app = await makeTestProviderScopeApp( - tester, - home: const CoordinateTrainingScreen(), - ); + final app = await makeTestProviderScopeApp(tester, home: const CoordinateTrainingScreen()); await tester.pumpWidget(app); await tester.tap(find.text('Start Training')); await tester.pumpAndSettle(); - final container = ProviderScope.containerOf( - tester.element(find.byType(ChessboardEditor)), - ); + final container = ProviderScope.containerOf(tester.element(find.byType(ChessboardEditor))); final controllerProvider = coordinateTrainingControllerProvider; - final trainingPrefsNotifier = container.read( - coordinateTrainingPreferencesProvider.notifier, - ); + final trainingPrefsNotifier = container.read(coordinateTrainingPreferencesProvider.notifier); trainingPrefsNotifier.setMode(TrainingMode.findSquare); // This way all squares can be found via find.byKey(ValueKey('${square.name}-empty')) trainingPrefsNotifier.setShowPieces(false); @@ -129,29 +94,17 @@ void main() { await tester.tap(find.byKey(ValueKey('${currentCoord!.name}-empty'))); await tester.pump(); - expect( - find.byKey(ValueKey('${currentCoord.name}-highlight')), - findsOneWidget, - ); + expect(find.byKey(ValueKey('${currentCoord.name}-highlight')), findsOneWidget); expect(container.read(controllerProvider).score, 1); expect(container.read(controllerProvider).currentCoord, nextCoord); expect(container.read(controllerProvider).trainingActive, true); await tester.pumpAndSettle(const Duration(milliseconds: 300)); - expect( - find.byKey(ValueKey('${currentCoord.name}-highlight')), - findsNothing, - ); - - expect( - find.text(container.read(controllerProvider).currentCoord!.name), - findsOneWidget, - ); - expect( - find.text(container.read(controllerProvider).nextCoord!.name), - findsOneWidget, - ); + expect(find.byKey(ValueKey('${currentCoord.name}-highlight')), findsNothing); + + expect(find.text(container.read(controllerProvider).currentCoord!.name), findsOneWidget); + expect(find.text(container.read(controllerProvider).nextCoord!.name), findsOneWidget); }); }); } diff --git a/test/view/game/archived_game_screen_test.dart b/test/view/game/archived_game_screen_test.dart index cc09d0f7ed..8350079209 100644 --- a/test/view/game/archived_game_screen_test.dart +++ b/test/view/game/archived_game_screen_test.dart @@ -29,121 +29,78 @@ final client = MockClient((request) { void main() { group('ArchivedGameScreen', () { - testWidgets( - 'loads game data if only game id is provided', - (tester) async { - final app = await makeTestProviderScopeApp( - tester, - home: const ArchivedGameScreen( - gameId: GameId('qVChCOTc'), - orientation: Side.white, - ), - overrides: [ - lichessClientProvider - .overrideWith((ref) => LichessClient(client, ref)), - ], - ); - - await tester.pumpWidget(app); - - expect(find.byType(PieceWidget), findsNothing); - expect(find.byType(CircularProgressIndicator), findsOneWidget); - - // wait for game data loading - await tester.pump(const Duration(milliseconds: 100)); - - expect(find.byType(PieceWidget), findsNWidgets(25)); - expect(find.widgetWithText(GamePlayer, 'veloce'), findsOneWidget); - expect( - find.widgetWithText(GamePlayer, 'Stockfish level 1'), - findsOneWidget, - ); - }, - variant: kPlatformVariant, - ); - - testWidgets( - 'displays game data and last fen immediately, then moves', - (tester) async { - final app = await makeTestProviderScopeApp( - tester, - home: ArchivedGameScreen( - gameData: gameData, - orientation: Side.white, - ), - overrides: [ - lichessClientProvider - .overrideWith((ref) => LichessClient(client, ref)), - ], - ); - - await tester.pumpWidget(app); - - // data shown immediately - expect(find.byType(Chessboard), findsOneWidget); - expect(find.byType(PieceWidget), findsNWidgets(25)); - expect(find.widgetWithText(GamePlayer, 'veloce'), findsOneWidget); - expect( - find.widgetWithText(GamePlayer, 'Stockfish level 1'), - findsOneWidget, - ); - - // cannot interact with board - expect( - tester.widget(find.byType(Chessboard)).game, - null, - ); - - // moves are not loaded - expect(find.byType(MoveList), findsNothing); - expect( - tester - .widget( - find.byKey(const ValueKey('cursor-back')), - ) - .onTap, - isNull, - ); - - // wait for game steps loading - await tester.pump(const Duration(milliseconds: 100)); - // wait for move list ensureVisible animation to finish - await tester.pumpAndSettle(); + testWidgets('loads game data if only game id is provided', (tester) async { + final app = await makeTestProviderScopeApp( + tester, + home: const ArchivedGameScreen(gameId: GameId('qVChCOTc'), orientation: Side.white), + overrides: [lichessClientProvider.overrideWith((ref) => LichessClient(client, ref))], + ); + + await tester.pumpWidget(app); + + expect(find.byType(PieceWidget), findsNothing); + expect(find.byType(CircularProgressIndicator), findsOneWidget); + + // wait for game data loading + await tester.pump(const Duration(milliseconds: 100)); + + expect(find.byType(PieceWidget), findsNWidgets(25)); + expect(find.widgetWithText(GamePlayer, 'veloce'), findsOneWidget); + expect(find.widgetWithText(GamePlayer, 'Stockfish level 1'), findsOneWidget); + }, variant: kPlatformVariant); + + testWidgets('displays game data and last fen immediately, then moves', (tester) async { + final app = await makeTestProviderScopeApp( + tester, + home: ArchivedGameScreen(gameData: gameData, orientation: Side.white), + overrides: [lichessClientProvider.overrideWith((ref) => LichessClient(client, ref))], + ); + + await tester.pumpWidget(app); + + // data shown immediately + expect(find.byType(Chessboard), findsOneWidget); + expect(find.byType(PieceWidget), findsNWidgets(25)); + expect(find.widgetWithText(GamePlayer, 'veloce'), findsOneWidget); + expect(find.widgetWithText(GamePlayer, 'Stockfish level 1'), findsOneWidget); + + // cannot interact with board + expect(tester.widget(find.byType(Chessboard)).game, null); + + // moves are not loaded + expect(find.byType(MoveList), findsNothing); + expect( + tester.widget(find.byKey(const ValueKey('cursor-back'))).onTap, + isNull, + ); + + // wait for game steps loading + await tester.pump(const Duration(milliseconds: 100)); + // wait for move list ensureVisible animation to finish + await tester.pumpAndSettle(); - // same info still displayed - expect(find.byType(Chessboard), findsOneWidget); - expect(find.byType(PieceWidget), findsNWidgets(25)); - expect(find.widgetWithText(GamePlayer, 'veloce'), findsOneWidget); - expect( - find.widgetWithText(GamePlayer, 'Stockfish level 1'), - findsOneWidget, - ); - - // now with the clocks - expect(find.text('1:46', findRichText: true), findsNWidgets(1)); - expect(find.text('0:46', findRichText: true), findsNWidgets(1)); - - // moves are loaded - expect(find.byType(MoveList), findsOneWidget); - expect( - tester - .widget( - find.byKey(const ValueKey('cursor-back')), - ) - .onTap, - isNotNull, - ); - }, - variant: kPlatformVariant, - ); + // same info still displayed + expect(find.byType(Chessboard), findsOneWidget); + expect(find.byType(PieceWidget), findsNWidgets(25)); + expect(find.widgetWithText(GamePlayer, 'veloce'), findsOneWidget); + expect(find.widgetWithText(GamePlayer, 'Stockfish level 1'), findsOneWidget); + + // now with the clocks + expect(find.text('1:46', findRichText: true), findsNWidgets(1)); + expect(find.text('0:46', findRichText: true), findsNWidgets(1)); + + // moves are loaded + expect(find.byType(MoveList), findsOneWidget); + expect( + tester.widget(find.byKey(const ValueKey('cursor-back'))).onTap, + isNotNull, + ); + }, variant: kPlatformVariant); testWidgets('navigate game positions', (tester) async { final app = await makeTestProviderScopeApp( tester, - home: ArchivedGameScreen( - gameData: gameData, - orientation: Side.white, - ), + home: ArchivedGameScreen(gameData: gameData, orientation: Side.white), overrides: [ lichessClientProvider.overrideWith((ref) { return LichessClient(client, ref); @@ -168,23 +125,12 @@ void main() { .toList(); expect( - tester - .widget( - find.widgetWithText(InlineMoveItem, 'Qe1#'), - ) - .current, + tester.widget(find.widgetWithText(InlineMoveItem, 'Qe1#')).current, isTrue, ); // cannot go forward - expect( - tester - .widget( - find.byKey(const Key('cursor-forward')), - ) - .onTap, - isNull, - ); + expect(tester.widget(find.byKey(const Key('cursor-forward'))).onTap, isNull); for (var i = 0; i <= movesAfterE4.length; i++) { // go back in history @@ -195,25 +141,17 @@ void main() { // move list is updated final prevMoveIndex = i + 1; if (prevMoveIndex < movesAfterE4.length) { - final prevMove = - find.widgetWithText(InlineMoveItem, movesAfterE4[prevMoveIndex]); + final prevMove = find.widgetWithText(InlineMoveItem, movesAfterE4[prevMoveIndex]); expect(prevMove, findsAtLeastNWidgets(1)); expect( - tester - .widgetList(prevMove) - .any((e) => e.current ?? false), + tester.widgetList(prevMove).any((e) => e.current ?? false), isTrue, ); } } // cannot go backward anymore - expect( - tester - .widget(find.byKey(const Key('cursor-back'))) - .onTap, - isNull, - ); + expect(tester.widget(find.byKey(const Key('cursor-back'))).onTap, isNull); }); }); } @@ -234,11 +172,7 @@ final gameData = LightArchivedGame( status: GameStatus.mate, white: const Player(aiLevel: 1), black: const Player( - user: LightUser( - id: UserId('veloce'), - name: 'veloce', - isPatron: true, - ), + user: LightUser(id: UserId('veloce'), name: 'veloce', isPatron: true), rating: 1435, ), variant: Variant.standard, diff --git a/test/view/game/game_screen_test.dart b/test/view/game/game_screen_test.dart index 03dd851370..fb8ede009a 100644 --- a/test/view/game/game_screen_test.dart +++ b/test/view/game/game_screen_test.dart @@ -35,18 +35,14 @@ class MockSoundService extends Mock implements SoundService {} void main() { group('Loading', () { - testWidgets('a game directly with initialGameId', - (WidgetTester tester) async { + testWidgets('a game directly with initialGameId', (WidgetTester tester) async { final fakeSocket = FakeWebSocketChannel(); final app = await makeTestProviderScopeApp( tester, - home: const GameScreen( - initialGameId: GameFullId('qVChCOTcHSeW'), - ), + home: const GameScreen(initialGameId: GameFullId('qVChCOTcHSeW')), overrides: [ - lichessClientProvider - .overrideWith((ref) => LichessClient(client, ref)), + lichessClientProvider.overrideWith((ref) => LichessClient(client, ref)), webSocketChannelFactoryProvider.overrideWith((ref) { return FakeWebSocketChannelFactory((_) => fakeSocket); }), @@ -81,26 +77,20 @@ void main() { expect(find.text('Steven'), findsOneWidget); }); - testWidgets('a game from the pool with a seek', - (WidgetTester tester) async { + testWidgets('a game from the pool with a seek', (WidgetTester tester) async { final fakeLobbySocket = FakeWebSocketChannel(); final fakeGameSocket = FakeWebSocketChannel(); final app = await makeTestProviderScopeApp( tester, home: const GameScreen( - seek: GameSeek( - clock: (Duration(minutes: 3), Duration(seconds: 2)), - rated: true, - ), + seek: GameSeek(clock: (Duration(minutes: 3), Duration(seconds: 2)), rated: true), ), overrides: [ - lichessClientProvider - .overrideWith((ref) => LichessClient(client, ref)), + lichessClientProvider.overrideWith((ref) => LichessClient(client, ref)), webSocketChannelFactoryProvider.overrideWith((ref) { return FakeWebSocketChannelFactory( - (String url) => - url.contains('lobby') ? fakeLobbySocket : fakeGameSocket, + (String url) => url.contains('lobby') ? fakeLobbySocket : fakeGameSocket, ); }), ], @@ -197,8 +187,7 @@ void main() { expect(findClockWithTime('2:58'), findsOneWidget); }); - testWidgets('ticks immediately when resuming game', - (WidgetTester tester) async { + testWidgets('ticks immediately when resuming game', (WidgetTester tester) async { final fakeSocket = FakeWebSocketChannel(); await createTestGame( fakeSocket, @@ -340,9 +329,7 @@ void main() { black: Duration(minutes: 3), emerg: Duration(seconds: 30), ), - overrides: [ - soundServiceProvider.overrideWith((_) => mockSoundService), - ], + overrides: [soundServiceProvider.overrideWith((_) => mockSoundService)], ); expect( tester.widget(findClockWithTime('0:40')).emergencyThreshold, @@ -394,10 +381,7 @@ void main() { // flag messages are sent expectLater( fakeSocket.sentMessagesExceptPing, - emitsInOrder([ - '{"t":"flag","d":"black"}', - '{"t":"flag","d":"black"}', - ]), + emitsInOrder(['{"t":"flag","d":"black"}', '{"t":"flag","d":"black"}']), ); await tester.pump(const Duration(seconds: 1)); fakeSocket.addIncomingMessages([ @@ -422,14 +406,12 @@ void main() { }); group('Opening analysis', () { - testWidgets('is not possible for an unfinished real time game', - (WidgetTester tester) async { + testWidgets('is not possible for an unfinished real time game', (WidgetTester tester) async { final fakeSocket = FakeWebSocketChannel(); await createTestGame( fakeSocket, tester, - pgn: - 'e4 e5 Nf3 Nc6 Bc4 Nf6 Ng5 d5 exd5 Na5 Bb5+ c6 dxc6 bxc6 Qf3 Rb8 Bd3', + pgn: 'e4 e5 Nf3 Nc6 Bc4 Nf6 Ng5 d5 exd5 Na5 Bb5+ c6 dxc6 bxc6 Qf3 Rb8 Bd3', socketVersion: 0, ); expect(find.byType(Chessboard), findsOneWidget); @@ -439,14 +421,12 @@ void main() { expect(find.text('Analysis board'), findsNothing); }); - testWidgets('for an unfinished correspondence game', - (WidgetTester tester) async { + testWidgets('for an unfinished correspondence game', (WidgetTester tester) async { final fakeSocket = FakeWebSocketChannel(); await createTestGame( fakeSocket, tester, - pgn: - 'e4 e5 Nf3 Nc6 Bc4 Nf6 Ng5 d5 exd5 Na5 Bb5+ c6 dxc6 bxc6 Qf3 Rb8 Bd3', + pgn: 'e4 e5 Nf3 Nc6 Bc4 Nf6 Ng5 d5 exd5 Na5 Bb5+ c6 dxc6 bxc6 Qf3 Rb8 Bd3', clock: null, correspondenceClock: ( daysPerTurn: 3, @@ -499,10 +479,7 @@ void main() { await tester.tap(find.byIcon(LichessIcons.flow_cascade)); await tester.pumpAndSettle(); // wait for the moves tab menu to open expect(find.text('Moves played'), findsOneWidget); - expect( - find.text('Computer analysis'), - findsOneWidget, - ); // computer analysis is available + expect(find.text('Computer analysis'), findsOneWidget); // computer analysis is available }); }); } @@ -529,9 +506,8 @@ Future playMoveWithServerAck( }) async { await playMove(tester, from, to, orientation: orientation); final uci = '$from$to'; - final lagStr = clockAck.lag != null - ? ', "lag": ${(clockAck.lag!.inMilliseconds / 10).round()}' - : ''; + final lagStr = + clockAck.lag != null ? ', "lag": ${(clockAck.lag!.inMilliseconds / 10).round()}' : ''; await tester.pump(elapsedTime - const Duration(milliseconds: 1)); socket.addIncomingMessages([ '{"t": "move", "v": $socketVersion, "d": {"ply": $ply, "uci": "$uci", "san": "$san", "clock": {"white": ${(clockAck.white.inMilliseconds / 1000).toStringAsFixed(2)}, "black": ${(clockAck.black.inMilliseconds / 1000).toStringAsFixed(2)}$lagStr}}}', @@ -559,9 +535,7 @@ Future createTestGame( }) async { final app = await makeTestProviderScopeApp( tester, - home: const GameScreen( - initialGameId: GameFullId('qVChCOTcHSeW'), - ), + home: const GameScreen(initialGameId: GameFullId('qVChCOTcHSeW')), overrides: [ lichessClientProvider.overrideWith((ref) => LichessClient(client, ref)), webSocketChannelFactoryProvider.overrideWith((ref) { @@ -596,13 +570,10 @@ Future loadFinishedTestGame( List? overrides, }) async { final json = jsonDecode(serverFullEvent) as Map; - final gameId = - GameFullEvent.fromJson(json['d'] as Map).game.id; + final gameId = GameFullEvent.fromJson(json['d'] as Map).game.id; final app = await makeTestProviderScopeApp( tester, - home: GameScreen( - initialGameId: GameFullId('${gameId.value}test'), - ), + home: GameScreen(initialGameId: GameFullId('${gameId.value}test')), overrides: [ lichessClientProvider.overrideWith((ref) => LichessClient(client, ref)), webSocketChannelFactoryProvider.overrideWith((ref) { diff --git a/test/view/home/home_tab_screen_test.dart b/test/view/home/home_tab_screen_test.dart index aa9a6a7ad2..0546253d87 100644 --- a/test/view/home/home_tab_screen_test.dart +++ b/test/view/home/home_tab_screen_test.dart @@ -21,10 +21,7 @@ import '../../test_provider_scope.dart'; void main() { group('Home online', () { testWidgets('shows Play button', (tester) async { - final app = await makeTestProviderScope( - tester, - child: const Application(), - ); + final app = await makeTestProviderScope(tester, child: const Application()); await tester.pumpWidget(app); // wait for connectivity @@ -35,10 +32,7 @@ void main() { }); testWidgets('shows players button', (tester) async { - final app = await makeTestProviderScope( - tester, - child: const Application(), - ); + final app = await makeTestProviderScope(tester, child: const Application()); await tester.pumpWidget(app); // wait for connectivity @@ -84,10 +78,7 @@ void main() { }); testWidgets('shows quick pairing matrix', (tester) async { - final app = await makeTestProviderScope( - tester, - child: const Application(), - ); + final app = await makeTestProviderScope(tester, child: const Application()); await tester.pumpWidget(app); // wait for connectivity @@ -97,31 +88,24 @@ void main() { expect(find.byType(QuickGameMatrix), findsOneWidget); }); - testWidgets('no session, no stored game: shows welcome screen ', - (tester) async { - final app = await makeTestProviderScope( - tester, - child: const Application(), - ); + testWidgets('no session, no stored game: shows welcome screen ', (tester) async { + final app = await makeTestProviderScope(tester, child: const Application()); await tester.pumpWidget(app); // wait for connectivity expect(find.byType(CircularProgressIndicator), findsOneWidget); await tester.pumpAndSettle(); expect( - find.textContaining( - 'libre, no-ads, open source chess server.', - findRichText: true, - ), + find.textContaining('libre, no-ads, open source chess server.', findRichText: true), findsOneWidget, ); expect(find.text('Sign in'), findsOneWidget); expect(find.text('About Lichess...'), findsOneWidget); }); - testWidgets( - 'session, no played game: shows welcome screen but no sign in button', - (tester) async { + testWidgets('session, no played game: shows welcome screen but no sign in button', ( + tester, + ) async { int nbUserGamesRequests = 0; final mockClient = MockClient((request) async { if (request.url.path == '/api/games/user/testuser') { @@ -135,8 +119,7 @@ void main() { child: const Application(), userSession: fakeSession, overrides: [ - httpClientFactoryProvider - .overrideWith((ref) => FakeHttpClientFactory(() => mockClient)), + httpClientFactoryProvider.overrideWith((ref) => FakeHttpClientFactory(() => mockClient)), ], ); await tester.pumpWidget(app); @@ -146,26 +129,17 @@ void main() { expect(nbUserGamesRequests, 1); expect( - find.textContaining( - 'libre, no-ads, open source chess server.', - findRichText: true, - ), + find.textContaining('libre, no-ads, open source chess server.', findRichText: true), findsOneWidget, ); expect(find.text('About Lichess...'), findsOneWidget); }); - testWidgets('no session, with stored games: shows list of recent games', - (tester) async { - final app = await makeTestProviderScope( - tester, - child: const Application(), - ); + testWidgets('no session, with stored games: shows list of recent games', (tester) async { + final app = await makeTestProviderScope(tester, child: const Application()); await tester.pumpWidget(app); - final container = ProviderScope.containerOf( - tester.element(find.byType(Application)), - ); + final container = ProviderScope.containerOf(tester.element(find.byType(Application))); final storage = await container.read(gameStorageProvider.future); final games = generateArchivedGames(count: 3); for (final game in games) { @@ -181,8 +155,7 @@ void main() { expect(find.text('Anonymous'), findsNWidgets(3)); }); - testWidgets('session, with played games: shows recent games', - (tester) async { + testWidgets('session, with played games: shows recent games', (tester) async { int nbUserGamesRequests = 0; final mockClient = MockClient((request) async { if (request.url.path == '/api/games/user/testuser') { @@ -196,8 +169,7 @@ void main() { child: const Application(), userSession: fakeSession, overrides: [ - httpClientFactoryProvider - .overrideWith((ref) => FakeHttpClientFactory(() => mockClient)), + httpClientFactoryProvider.overrideWith((ref) => FakeHttpClientFactory(() => mockClient)), ], ); await tester.pumpWidget(app); @@ -215,10 +187,7 @@ void main() { group('Home offline', () { testWidgets('shows offline banner', (tester) async { - final app = await makeOfflineTestProviderScope( - tester, - child: const Application(), - ); + final app = await makeOfflineTestProviderScope(tester, child: const Application()); await tester.pumpWidget(app); // wait for connectivity @@ -229,10 +198,7 @@ void main() { }); testWidgets('shows Play button', (tester) async { - final app = await makeOfflineTestProviderScope( - tester, - child: const Application(), - ); + final app = await makeOfflineTestProviderScope(tester, child: const Application()); await tester.pumpWidget(app); @@ -244,10 +210,7 @@ void main() { }); testWidgets('shows disabled players button', (tester) async { - final app = await makeOfflineTestProviderScope( - tester, - child: const Application(), - ); + final app = await makeOfflineTestProviderScope(tester, child: const Application()); await tester.pumpWidget(app); @@ -268,39 +231,26 @@ void main() { ); }); - testWidgets('no session, no stored game: shows welcome screen ', - (tester) async { - final app = await makeTestProviderScope( - tester, - child: const Application(), - ); + testWidgets('no session, no stored game: shows welcome screen ', (tester) async { + final app = await makeTestProviderScope(tester, child: const Application()); await tester.pumpWidget(app); // wait for connectivity expect(find.byType(CircularProgressIndicator), findsOneWidget); await tester.pumpAndSettle(); expect( - find.textContaining( - 'libre, no-ads, open source chess server.', - findRichText: true, - ), + find.textContaining('libre, no-ads, open source chess server.', findRichText: true), findsOneWidget, ); expect(find.text('Sign in'), findsOneWidget); expect(find.text('About Lichess...'), findsOneWidget); }); - testWidgets('no session, with stored games: shows list of recent games', - (tester) async { - final app = await makeOfflineTestProviderScope( - tester, - child: const Application(), - ); + testWidgets('no session, with stored games: shows list of recent games', (tester) async { + final app = await makeOfflineTestProviderScope(tester, child: const Application()); await tester.pumpWidget(app); - final container = ProviderScope.containerOf( - tester.element(find.byType(Application)), - ); + final container = ProviderScope.containerOf(tester.element(find.byType(Application))); final storage = await container.read(gameStorageProvider.future); final games = generateArchivedGames(count: 3); for (final game in games) { @@ -316,8 +266,7 @@ void main() { expect(find.text('Anonymous'), findsNWidgets(3)); }); - testWidgets('session, with stored games: shows list of recent games', - (tester) async { + testWidgets('session, with stored games: shows list of recent games', (tester) async { final app = await makeOfflineTestProviderScope( tester, child: const Application(), @@ -325,9 +274,7 @@ void main() { ); await tester.pumpWidget(app); - final container = ProviderScope.containerOf( - tester.element(find.byType(Application)), - ); + final container = ProviderScope.containerOf(tester.element(find.byType(Application))); final storage = await container.read(gameStorageProvider.future); final games = generateArchivedGames(count: 3, username: 'testUser'); for (final game in games) { diff --git a/test/view/opening_explorer/opening_explorer_screen_test.dart b/test/view/opening_explorer/opening_explorer_screen_test.dart index 0ff43f2ff4..bfc9ca9015 100644 --- a/test/view/opening_explorer/opening_explorer_screen_test.dart +++ b/test/view/opening_explorer/opening_explorer_screen_test.dart @@ -41,179 +41,137 @@ void main() { const options = AnalysisOptions( orientation: Side.white, - standalone: ( - pgn: '', - isComputerAnalysisAllowed: false, - variant: Variant.standard, - ), + standalone: (pgn: '', isComputerAnalysisAllowed: false, variant: Variant.standard), ); const name = 'John'; - final user = LightUser( - id: UserId.fromUserName(name), - name: name, - ); + final user = LightUser(id: UserId.fromUserName(name), name: name); - final session = AuthSessionState( - user: user, - token: 'test-token', - ); + final session = AuthSessionState(user: user, token: 'test-token'); group('OpeningExplorerScreen', () { - testWidgets( - 'master opening explorer loads', - (WidgetTester tester) async { - final app = await makeTestProviderScopeApp( - tester, - home: const OpeningExplorerScreen(options: options), - overrides: [ - defaultClientProvider.overrideWithValue(mockClient), - ], - ); - await tester.pumpWidget(app); - // wait for analysis controller to load - expect(find.byType(CircularProgressIndicator), findsOneWidget); - await tester.pump(const Duration(milliseconds: 10)); + testWidgets('master opening explorer loads', (WidgetTester tester) async { + final app = await makeTestProviderScopeApp( + tester, + home: const OpeningExplorerScreen(options: options), + overrides: [defaultClientProvider.overrideWithValue(mockClient)], + ); + await tester.pumpWidget(app); + // wait for analysis controller to load + expect(find.byType(CircularProgressIndicator), findsOneWidget); + await tester.pump(const Duration(milliseconds: 10)); - // wait for opening explorer data to load (taking debounce delay into account) - await tester.pump(const Duration(milliseconds: 350)); + // wait for opening explorer data to load (taking debounce delay into account) + await tester.pump(const Duration(milliseconds: 350)); - final moves = [ - 'e4', - 'd4', - ]; - expect(find.byType(Table), findsOneWidget); - for (final move in moves) { - expect(find.widgetWithText(TableRowInkWell, move), findsOneWidget); - } + final moves = ['e4', 'd4']; + expect(find.byType(Table), findsOneWidget); + for (final move in moves) { + expect(find.widgetWithText(TableRowInkWell, move), findsOneWidget); + } - expect(find.widgetWithText(Container, 'Top games'), findsOneWidget); - expect(find.widgetWithText(Container, 'Recent games'), findsNothing); + expect(find.widgetWithText(Container, 'Top games'), findsOneWidget); + expect(find.widgetWithText(Container, 'Recent games'), findsNothing); - // TODO: make a custom scrollUntilVisible that works with the non-scrollable - // board widget + // TODO: make a custom scrollUntilVisible that works with the non-scrollable + // board widget - // await tester.scrollUntilVisible( - // find.text('Firouzja, A.'), - // 200, - // scrollable: explorerViewFinder, - // ); + // await tester.scrollUntilVisible( + // find.text('Firouzja, A.'), + // 200, + // scrollable: explorerViewFinder, + // ); - // expect( - // find.byType(OpeningExplorerGameTile), - // findsNWidgets(2), - // ); - }, - variant: kPlatformVariant, - ); + // expect( + // find.byType(OpeningExplorerGameTile), + // findsNWidgets(2), + // ); + }, variant: kPlatformVariant); - testWidgets( - 'lichess opening explorer loads', - (WidgetTester tester) async { - final app = await makeTestProviderScopeApp( - tester, - home: const OpeningExplorerScreen(options: options), - overrides: [ - defaultClientProvider.overrideWithValue(mockClient), - ], - defaultPreferences: { - SessionPreferencesStorage.key( - PrefCategory.openingExplorer.storageKey, - null, - ): jsonEncode( - OpeningExplorerPrefs.defaults() - .copyWith(db: OpeningDatabase.lichess) - .toJson(), - ), - }, - ); - await tester.pumpWidget(app); - // wait for analysis controller to load - expect(find.byType(CircularProgressIndicator), findsOneWidget); - await tester.pump(const Duration(milliseconds: 10)); + testWidgets('lichess opening explorer loads', (WidgetTester tester) async { + final app = await makeTestProviderScopeApp( + tester, + home: const OpeningExplorerScreen(options: options), + overrides: [defaultClientProvider.overrideWithValue(mockClient)], + defaultPreferences: { + SessionPreferencesStorage.key(PrefCategory.openingExplorer.storageKey, null): jsonEncode( + OpeningExplorerPrefs.defaults().copyWith(db: OpeningDatabase.lichess).toJson(), + ), + }, + ); + await tester.pumpWidget(app); + // wait for analysis controller to load + expect(find.byType(CircularProgressIndicator), findsOneWidget); + await tester.pump(const Duration(milliseconds: 10)); - // wait for opening explorer data to load (taking debounce delay into account) - await tester.pump(const Duration(milliseconds: 350)); + // wait for opening explorer data to load (taking debounce delay into account) + await tester.pump(const Duration(milliseconds: 350)); - final moves = [ - 'd4', - ]; - expect(find.byType(Table), findsOneWidget); - for (final move in moves) { - expect(find.widgetWithText(TableRowInkWell, move), findsOneWidget); - } + final moves = ['d4']; + expect(find.byType(Table), findsOneWidget); + for (final move in moves) { + expect(find.widgetWithText(TableRowInkWell, move), findsOneWidget); + } - expect(find.widgetWithText(Container, 'Top games'), findsNothing); - expect(find.widgetWithText(Container, 'Recent games'), findsOneWidget); + expect(find.widgetWithText(Container, 'Top games'), findsNothing); + expect(find.widgetWithText(Container, 'Recent games'), findsOneWidget); - // await tester.scrollUntilVisible( - // find.byType(OpeningExplorerGameTile), - // 200, - // scrollable: explorerViewFinder, - // ); + // await tester.scrollUntilVisible( + // find.byType(OpeningExplorerGameTile), + // 200, + // scrollable: explorerViewFinder, + // ); - // expect( - // find.byType(OpeningExplorerGameTile), - // findsOneWidget, - // ); - }, - variant: kPlatformVariant, - ); + // expect( + // find.byType(OpeningExplorerGameTile), + // findsOneWidget, + // ); + }, variant: kPlatformVariant); - testWidgets( - 'player opening explorer loads', - (WidgetTester tester) async { - final app = await makeTestProviderScopeApp( - tester, - home: const OpeningExplorerScreen(options: options), - overrides: [ - defaultClientProvider.overrideWithValue(mockClient), - ], - userSession: session, - defaultPreferences: { - SessionPreferencesStorage.key( - PrefCategory.openingExplorer.storageKey, - session, - ): jsonEncode( - OpeningExplorerPrefs.defaults(user: user) - .copyWith(db: OpeningDatabase.player) - .toJson(), - ), - }, - ); - await tester.pumpWidget(app); - // wait for analysis controller to load - expect(find.byType(CircularProgressIndicator), findsOneWidget); - await tester.pump(const Duration(milliseconds: 10)); + testWidgets('player opening explorer loads', (WidgetTester tester) async { + final app = await makeTestProviderScopeApp( + tester, + home: const OpeningExplorerScreen(options: options), + overrides: [defaultClientProvider.overrideWithValue(mockClient)], + userSession: session, + defaultPreferences: { + SessionPreferencesStorage.key( + PrefCategory.openingExplorer.storageKey, + session, + ): jsonEncode( + OpeningExplorerPrefs.defaults(user: user).copyWith(db: OpeningDatabase.player).toJson(), + ), + }, + ); + await tester.pumpWidget(app); + // wait for analysis controller to load + expect(find.byType(CircularProgressIndicator), findsOneWidget); + await tester.pump(const Duration(milliseconds: 10)); - // wait for opening explorer data to load (taking debounce delay into account) - await tester.pump(const Duration(milliseconds: 350)); + // wait for opening explorer data to load (taking debounce delay into account) + await tester.pump(const Duration(milliseconds: 350)); - final moves = [ - 'c4', - ]; - expect(find.byType(Table), findsOneWidget); - for (final move in moves) { - expect(find.widgetWithText(TableRowInkWell, move), findsOneWidget); - } + final moves = ['c4']; + expect(find.byType(Table), findsOneWidget); + for (final move in moves) { + expect(find.widgetWithText(TableRowInkWell, move), findsOneWidget); + } - expect(find.widgetWithText(Container, 'Top games'), findsNothing); - expect(find.widgetWithText(Container, 'Recent games'), findsOneWidget); + expect(find.widgetWithText(Container, 'Top games'), findsNothing); + expect(find.widgetWithText(Container, 'Recent games'), findsOneWidget); - // await tester.scrollUntilVisible( - // find.byType(OpeningExplorerGameTile), - // 200, - // scrollable: explorerViewFinder, - // ); + // await tester.scrollUntilVisible( + // find.byType(OpeningExplorerGameTile), + // 200, + // scrollable: explorerViewFinder, + // ); - // expect( - // find.byType(OpeningExplorerGameTile), - // findsOneWidget, - // ); - }, - variant: kPlatformVariant, - ); + // expect( + // find.byType(OpeningExplorerGameTile), + // findsOneWidget, + // ); + }, variant: kPlatformVariant); }); } diff --git a/test/view/over_the_board/over_the_board_screen_test.dart b/test/view/over_the_board/over_the_board_screen_test.dart index 720ccfba09..206f017347 100644 --- a/test/view/over_the_board/over_the_board_screen_test.dart +++ b/test/view/over_the_board/over_the_board_screen_test.dart @@ -17,10 +17,7 @@ import '../../test_provider_scope.dart'; void main() { group('Playing over the board (offline)', () { testWidgets('Checkmate and Rematch', (tester) async { - final boardRect = await initOverTheBoardGame( - tester, - const TimeIncrement(60, 5), - ); + final boardRect = await initOverTheBoardGame(tester, const TimeIncrement(60, 5)); // Default orientation is white at the bottom expect( @@ -40,28 +37,20 @@ void main() { await tester.tap(find.text('Rematch')); await tester.pumpAndSettle(); - final container = ProviderScope.containerOf( - tester.element(find.byType(Chessboard)), - ); + final container = ProviderScope.containerOf(tester.element(find.byType(Chessboard))); final gameState = container.read(overTheBoardGameControllerProvider); expect(gameState.game.steps.length, 1); expect(gameState.game.steps.first.position, Chess.initial); // Rematch should flip orientation - expect( - tester.getTopRight(find.byKey(const ValueKey('a1-whiterook'))), - boardRect.topRight, - ); + expect(tester.getTopRight(find.byKey(const ValueKey('a1-whiterook'))), boardRect.topRight); expect(activeClock(tester), null); }); testWidgets('Game ends when out of time', (tester) async { const time = Duration(seconds: 1); - await initOverTheBoardGame( - tester, - TimeIncrement(time.inSeconds, 0), - ); + await initOverTheBoardGame(tester, TimeIncrement(time.inSeconds, 0)); await playMove(tester, 'e2', 'e4'); await playMove(tester, 'e7', 'e5'); @@ -81,10 +70,7 @@ void main() { testWidgets('Pausing the clock', (tester) async { const time = Duration(seconds: 10); - await initOverTheBoardGame( - tester, - TimeIncrement(time.inSeconds, 0), - ); + await initOverTheBoardGame(tester, TimeIncrement(time.inSeconds, 0)); await playMove(tester, 'e2', 'e4'); await playMove(tester, 'e7', 'e5'); @@ -116,10 +102,7 @@ void main() { testWidgets('Go back and Forward', (tester) async { const time = Duration(seconds: 10); - await initOverTheBoardGame( - tester, - TimeIncrement(time.inSeconds, 0), - ); + await initOverTheBoardGame(tester, TimeIncrement(time.inSeconds, 0)); await playMove(tester, 'e2', 'e4'); await playMove(tester, 'e7', 'e5'); @@ -155,10 +138,7 @@ void main() { }); testWidgets('No clock if time is infinite', (tester) async { - await initOverTheBoardGame( - tester, - const TimeIncrement(0, 0), - ); + await initOverTheBoardGame(tester, const TimeIncrement(0, 0)); expect(find.byType(Clock), findsNothing); }); @@ -166,10 +146,7 @@ void main() { testWidgets('Clock logic', (tester) async { const time = Duration(minutes: 5); - await initOverTheBoardGame( - tester, - TimeIncrement(time.inSeconds, 3), - ); + await initOverTheBoardGame(tester, TimeIncrement(time.inSeconds, 3)); expect(activeClock(tester), null); @@ -199,23 +176,15 @@ void main() { }); } -Future initOverTheBoardGame( - WidgetTester tester, - TimeIncrement timeIncrement, -) async { - final app = await makeTestProviderScopeApp( - tester, - home: const OverTheBoardScreen(), - ); +Future initOverTheBoardGame(WidgetTester tester, TimeIncrement timeIncrement) async { + final app = await makeTestProviderScopeApp(tester, home: const OverTheBoardScreen()); await tester.pumpWidget(app); await tester.pumpAndSettle(); await tester.tap(find.text('Play')); await tester.pumpAndSettle(); - final container = ProviderScope.containerOf( - tester.element(find.byType(Chessboard)), - ); + final container = ProviderScope.containerOf(tester.element(find.byType(Chessboard))); container.read(overTheBoardClockProvider.notifier).setupClock(timeIncrement); await tester.pumpAndSettle(); @@ -241,16 +210,12 @@ Side? activeClock(WidgetTester tester, {Side orientation = Side.white}) { Clock findWhiteClock(WidgetTester tester, {Side orientation = Side.white}) { return tester.widget( - find.byKey( - ValueKey(orientation == Side.white ? 'bottomClock' : 'topClock'), - ), + find.byKey(ValueKey(orientation == Side.white ? 'bottomClock' : 'topClock')), ); } Clock findBlackClock(WidgetTester tester, {Side orientation = Side.white}) { return tester.widget( - find.byKey( - ValueKey(orientation == Side.white ? 'topClock' : 'bottomClock'), - ), + find.byKey(ValueKey(orientation == Side.white ? 'topClock' : 'bottomClock')), ); } diff --git a/test/view/puzzle/example_data.dart b/test/view/puzzle/example_data.dart index 8a7b6a0b8c..9bf19cde6b 100644 --- a/test/view/puzzle/example_data.dart +++ b/test/view/puzzle/example_data.dart @@ -11,21 +11,8 @@ final puzzle = Puzzle( initialPly: 40, plays: 68176, rating: 1984, - solution: IList(const [ - 'h4h2', - 'h1h2', - 'e5f3', - 'h2h3', - 'b4h4', - ]), - themes: ISet(const [ - 'middlegame', - 'attraction', - 'long', - 'mateIn3', - 'sacrifice', - 'doubleCheck', - ]), + solution: IList(const ['h4h2', 'h1h2', 'e5f3', 'h2h3', 'b4h4']), + themes: ISet(const ['middlegame', 'attraction', 'long', 'mateIn3', 'sacrifice', 'doubleCheck']), ), game: const PuzzleGame( rated: true, @@ -33,23 +20,12 @@ final puzzle = Puzzle( perf: Perf.blitz, pgn: 'e4 c5 Nf3 e6 c4 Nc6 d4 cxd4 Nxd4 Bc5 Nxc6 bxc6 Be2 Ne7 O-O Ng6 Nc3 Rb8 Kh1 Bb7 f4 d5 f5 Ne5 fxe6 fxe6 cxd5 cxd5 exd5 Bxd5 Qa4+ Bc6 Qf4 Bd6 Ne4 Bxe4 Qxe4 Rb4 Qe3 Qh4 Qxa7', - black: PuzzleGamePlayer( - side: Side.black, - name: 'CAMBIADOR', - ), - white: PuzzleGamePlayer( - side: Side.white, - name: 'arroyoM10', - ), + black: PuzzleGamePlayer(side: Side.black, name: 'CAMBIADOR'), + white: PuzzleGamePlayer(side: Side.white, name: 'arroyoM10'), ), ); -final batch = PuzzleBatch( - solved: IList(const []), - unsolved: IList([ - puzzle, - ]), -); +final batch = PuzzleBatch(solved: IList(const []), unsolved: IList([puzzle])); final puzzle2 = Puzzle( puzzle: PuzzleData( @@ -58,28 +34,14 @@ final puzzle2 = Puzzle( plays: 23890, initialPly: 88, solution: IList(const ['g4h4', 'h8h4', 'b4h4']), - themes: ISet(const { - 'endgame', - 'short', - 'crushing', - 'fork', - 'queenRookEndgame', - }), + themes: ISet(const {'endgame', 'short', 'crushing', 'fork', 'queenRookEndgame'}), ), game: const PuzzleGame( id: GameId('w32JTzEf'), perf: Perf.blitz, rated: true, - white: PuzzleGamePlayer( - side: Side.white, - name: 'Li', - title: null, - ), - black: PuzzleGamePlayer( - side: Side.black, - name: 'Gabriela', - title: null, - ), + white: PuzzleGamePlayer(side: Side.white, name: 'Li', title: null), + black: PuzzleGamePlayer(side: Side.black, name: 'Gabriela', title: null), pgn: 'e4 e5 Nf3 Nc6 Bb5 a6 Ba4 b5 Bb3 Nf6 c3 Nxe4 d4 exd4 cxd4 Qe7 O-O Qd8 Bd5 Nf6 Bb3 Bd6 Nc3 O-O Bg5 h6 Bh4 g5 Nxg5 hxg5 Bxg5 Kg7 Ne4 Be7 Bxf6+ Bxf6 Qg4+ Kh8 Qh5+ Kg8 Qg6+ Kh8 Qxf6+ Qxf6 Nxf6 Nxd4 Rfd1 Ne2+ Kh1 d6 Rd5 Kg7 Nh5+ Kh6 Rad1 Be6 R5d2 Bxb3 axb3 Kxh5 Rxe2 Rfe8 Red2 Re5 h3 Rae8 Kh2 Re2 Rd5+ Kg6 f4 Rxb2 R1d3 Ree2 Rg3+ Kf6 h4 Re4 Rg4 Rxb3 h5 Rbb4 h6 Rxf4 h7 Rxg4 h8=Q+ Ke7 Rd3', ), diff --git a/test/view/puzzle/puzzle_history_screen_test.dart b/test/view/puzzle/puzzle_history_screen_test.dart index caa034b73d..2f93dcaf4a 100644 --- a/test/view/puzzle/puzzle_history_screen_test.dart +++ b/test/view/puzzle/puzzle_history_screen_test.dart @@ -20,45 +20,36 @@ void main() { }); MockClient makeClient(int totalNumberOfPuzzles) => MockClient((request) { - if (request.url.path == '/api/puzzle/activity') { - final query = request.url.queryParameters; - final max = int.parse(query['max']!); - final beforeDateParam = query['before']; - final beforeDate = beforeDateParam != null + if (request.url.path == '/api/puzzle/activity') { + final query = request.url.queryParameters; + final max = int.parse(query['max']!); + final beforeDateParam = query['before']; + final beforeDate = + beforeDateParam != null ? DateTime.fromMillisecondsSinceEpoch(int.parse(beforeDateParam)) : null; - final totalAlreadyRequested = - mockActivityRequestsCount.values.fold(0, (p, e) => p + e); - - if (totalAlreadyRequested >= totalNumberOfPuzzles) { - return mockResponse('', 200); - } - - final key = - beforeDate != null ? DateFormat.yMd().format(beforeDate) : null; - - final nbPuzzles = math.min(max, totalNumberOfPuzzles); - mockActivityRequestsCount[key] = - (mockActivityRequestsCount[key] ?? 0) + nbPuzzles; - return mockResponse( - generateHistory(nbPuzzles, beforeDate), - 200, - ); - } else if (request.url.path == '/api/puzzle/batch/mix') { - return mockResponse(mockMixBatchResponse, 200); - } else if (request.url.path.startsWith('/api/puzzle')) { - return mockResponse( - ''' + final totalAlreadyRequested = mockActivityRequestsCount.values.fold(0, (p, e) => p + e); + + if (totalAlreadyRequested >= totalNumberOfPuzzles) { + return mockResponse('', 200); + } + + final key = beforeDate != null ? DateFormat.yMd().format(beforeDate) : null; + + final nbPuzzles = math.min(max, totalNumberOfPuzzles); + mockActivityRequestsCount[key] = (mockActivityRequestsCount[key] ?? 0) + nbPuzzles; + return mockResponse(generateHistory(nbPuzzles, beforeDate), 200); + } else if (request.url.path == '/api/puzzle/batch/mix') { + return mockResponse(mockMixBatchResponse, 200); + } else if (request.url.path.startsWith('/api/puzzle')) { + return mockResponse(''' {"game":{"id":"MNMYnEjm","perf":{"key":"classical","name":"Classical"},"rated":true,"players":[{"name":"Igor76","id":"igor76","color":"white","rating":2211},{"name":"dmitriy_duyun","id":"dmitriy_duyun","color":"black","rating":2180}],"pgn":"e4 c6 d4 d5 Nc3 g6 Nf3 Bg7 h3 dxe4 Nxe4 Nf6 Bd3 Nxe4 Bxe4 Nd7 O-O Nf6 Bd3 O-O Re1 Bf5 Bxf5 gxf5 c3 e6 Bg5 Qb6 Qc2 Rac8 Ne5 Qc7 Rad1 Nd7 Bf4 Nxe5 Bxe5 Bxe5 Rxe5 Rcd8 Qd2 Kh8 Rde1 Rg8 Qf4","clock":"20+15"},"puzzle":{"id":"0XqV2","rating":1929,"plays":93270,"solution":["f7f6","e5f5","c7g7","g2g3","e6f5"],"themes":["clearance","endgame","advantage","intermezzo","long"],"initialPly":44}} -''', - 200, - ); - } - return mockResponse('', 404); - }); - - testWidgets('Displays an initial list of puzzles', - (WidgetTester tester) async { +''', 200); + } + return mockResponse('', 404); + }); + + testWidgets('Displays an initial list of puzzles', (WidgetTester tester) async { final app = await makeTestProviderScopeApp( tester, home: const PuzzleHistoryScreen(), @@ -111,46 +102,30 @@ void main() { await tester.scrollUntilVisible( find.byWidgetPredicate( - (widget) => - widget is PuzzleHistoryBoard && widget.puzzle.id.value == 'Bnull20', + (widget) => widget is PuzzleHistoryBoard && widget.puzzle.id.value == 'Bnull20', description: 'last item of 1st page', ), 400, ); // next pages have 50 puzzles - expect( - mockActivityRequestsCount, - equals({ - null: 20, - '1/31/2024': 50, - }), - ); + expect(mockActivityRequestsCount, equals({null: 20, '1/31/2024': 50})); // by the time we've scrolled to the end the next puzzles are already here await tester.scrollUntilVisible( find.byWidgetPredicate( - (widget) => - widget is PuzzleHistoryBoard && widget.puzzle.id.value == 'B3150', + (widget) => widget is PuzzleHistoryBoard && widget.puzzle.id.value == 'B3150', description: 'last item of 2nd page', ), 1000, ); // one more page - expect( - mockActivityRequestsCount, - equals({ - null: 20, - '1/31/2024': 50, - '1/30/2024': 50, - }), - ); + expect(mockActivityRequestsCount, equals({null: 20, '1/31/2024': 50, '1/30/2024': 50})); await tester.scrollUntilVisible( find.byWidgetPredicate( - (widget) => - widget is PuzzleHistoryBoard && widget.puzzle.id.value == 'B3010', + (widget) => widget is PuzzleHistoryBoard && widget.puzzle.id.value == 'B3010', description: 'last item of 3rd page', ), 400, @@ -164,8 +139,7 @@ void main() { await tester.tap( find.byWidgetPredicate( - (widget) => - widget is PuzzleHistoryBoard && widget.puzzle.id.value == 'B3010', + (widget) => widget is PuzzleHistoryBoard && widget.puzzle.id.value == 'B3010', ), ); @@ -179,8 +153,7 @@ void main() { expect(find.byType(PuzzleHistoryScreen), findsOneWidget); expect( find.byWidgetPredicate( - (widget) => - widget is PuzzleHistoryBoard && widget.puzzle.id.value == 'B3010', + (widget) => widget is PuzzleHistoryBoard && widget.puzzle.id.value == 'B3010', ), findsOneWidget, ); diff --git a/test/view/puzzle/puzzle_screen_test.dart b/test/view/puzzle/puzzle_screen_test.dart index 8845f25da3..5789fcb318 100644 --- a/test/view/puzzle/puzzle_screen_test.dart +++ b/test/view/puzzle/puzzle_screen_test.dart @@ -26,12 +26,7 @@ class MockPuzzleStorage extends Mock implements PuzzleStorage {} void main() { setUpAll(() { - registerFallbackValue( - PuzzleBatch( - solved: IList(const []), - unsolved: IList([puzzle]), - ), - ); + registerFallbackValue(PuzzleBatch(solved: IList(const []), unsolved: IList([puzzle]))); registerFallbackValue(puzzle); }); @@ -39,74 +34,70 @@ void main() { final mockHistoryStorage = MockPuzzleStorage(); group('PuzzleScreen', () { - testWidgets( - 'meets accessibility guidelines', - variant: kPlatformVariant, - (WidgetTester tester) async { - final SemanticsHandle handle = tester.ensureSemantics(); + testWidgets('meets accessibility guidelines', variant: kPlatformVariant, ( + WidgetTester tester, + ) async { + final SemanticsHandle handle = tester.ensureSemantics(); - final app = await makeTestProviderScopeApp( - tester, - home: PuzzleScreen( - angle: const PuzzleTheme(PuzzleThemeKey.mix), - puzzleId: puzzle.puzzle.id, - ), - overrides: [ - puzzleBatchStorageProvider.overrideWith((ref) => mockBatchStorage), - puzzleStorageProvider.overrideWith((ref) => mockHistoryStorage), - ], - ); + final app = await makeTestProviderScopeApp( + tester, + home: PuzzleScreen( + angle: const PuzzleTheme(PuzzleThemeKey.mix), + puzzleId: puzzle.puzzle.id, + ), + overrides: [ + puzzleBatchStorageProvider.overrideWith((ref) => mockBatchStorage), + puzzleStorageProvider.overrideWith((ref) => mockHistoryStorage), + ], + ); - when(() => mockHistoryStorage.fetch(puzzleId: puzzle.puzzle.id)) - .thenAnswer((_) async => puzzle); + when( + () => mockHistoryStorage.fetch(puzzleId: puzzle.puzzle.id), + ).thenAnswer((_) async => puzzle); - await tester.pumpWidget(app); + await tester.pumpWidget(app); - // wait for the puzzle to load - await tester.pump(const Duration(milliseconds: 200)); + // wait for the puzzle to load + await tester.pump(const Duration(milliseconds: 200)); - await meetsTapTargetGuideline(tester); + await meetsTapTargetGuideline(tester); - await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); - handle.dispose(); - }, - ); + await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); + handle.dispose(); + }); - testWidgets( - 'Loads puzzle directly by passing a puzzleId', - variant: kPlatformVariant, - (tester) async { - final app = await makeTestProviderScopeApp( - tester, - home: PuzzleScreen( - angle: const PuzzleTheme(PuzzleThemeKey.mix), - puzzleId: puzzle.puzzle.id, - ), - overrides: [ - puzzleBatchStorageProvider.overrideWith((ref) => mockBatchStorage), - puzzleStorageProvider.overrideWith((ref) => mockHistoryStorage), - ], - ); + testWidgets('Loads puzzle directly by passing a puzzleId', variant: kPlatformVariant, ( + tester, + ) async { + final app = await makeTestProviderScopeApp( + tester, + home: PuzzleScreen( + angle: const PuzzleTheme(PuzzleThemeKey.mix), + puzzleId: puzzle.puzzle.id, + ), + overrides: [ + puzzleBatchStorageProvider.overrideWith((ref) => mockBatchStorage), + puzzleStorageProvider.overrideWith((ref) => mockHistoryStorage), + ], + ); - when(() => mockHistoryStorage.fetch(puzzleId: puzzle.puzzle.id)) - .thenAnswer((_) async => puzzle); + when( + () => mockHistoryStorage.fetch(puzzleId: puzzle.puzzle.id), + ).thenAnswer((_) async => puzzle); - await tester.pumpWidget(app); + await tester.pumpWidget(app); - // wait for the puzzle to load - await tester.pump(const Duration(milliseconds: 200)); + // wait for the puzzle to load + await tester.pump(const Duration(milliseconds: 200)); - expect(find.byType(Chessboard), findsOneWidget); - expect(find.text('Your turn'), findsOneWidget); - }, - ); + expect(find.byType(Chessboard), findsOneWidget); + expect(find.text('Your turn'), findsOneWidget); + }); testWidgets('Loads next puzzle when no puzzleId is passed', (tester) async { final app = await makeTestProviderScopeApp( tester, - home: const PuzzleScreen( - angle: PuzzleTheme(PuzzleThemeKey.mix), - ), + home: const PuzzleScreen(angle: PuzzleTheme(PuzzleThemeKey.mix)), overrides: [ puzzleBatchStorageProvider.overrideWith((ref) => mockBatchStorage), puzzleStorageProvider.overrideWith((ref) => mockHistoryStorage), @@ -114,14 +105,10 @@ void main() { ); when( - () => mockBatchStorage.fetch( - userId: null, - angle: const PuzzleTheme(PuzzleThemeKey.mix), - ), + () => mockBatchStorage.fetch(userId: null, angle: const PuzzleTheme(PuzzleThemeKey.mix)), ).thenAnswer((_) async => batch); - when(() => mockHistoryStorage.save(puzzle: any(named: 'puzzle'))) - .thenAnswer((_) async {}); + when(() => mockHistoryStorage.save(puzzle: any(named: 'puzzle'))).thenAnswer((_) async {}); await tester.pumpWidget(app); @@ -135,121 +122,117 @@ void main() { expect(find.text('Your turn'), findsOneWidget); }); - testWidgets( - 'solves a puzzle and loads the next one', - variant: kPlatformVariant, - (tester) async { - final mockClient = MockClient((request) { - if (request.url.path == '/api/puzzle/batch/mix') { - return mockResponse(batchOf1, 200); - } - return mockResponse('', 404); - }); + testWidgets('solves a puzzle and loads the next one', variant: kPlatformVariant, ( + tester, + ) async { + final mockClient = MockClient((request) { + if (request.url.path == '/api/puzzle/batch/mix') { + return mockResponse(batchOf1, 200); + } + return mockResponse('', 404); + }); - when(() => mockHistoryStorage.fetch(puzzleId: puzzle2.puzzle.id)) - .thenAnswer((_) async => puzzle2); + when( + () => mockHistoryStorage.fetch(puzzleId: puzzle2.puzzle.id), + ).thenAnswer((_) async => puzzle2); - final app = await makeTestProviderScopeApp( - tester, - home: PuzzleScreen( - angle: const PuzzleTheme(PuzzleThemeKey.mix), - puzzleId: puzzle2.puzzle.id, - ), - overrides: [ - lichessClientProvider.overrideWith((ref) { - return LichessClient(mockClient, ref); - }), - puzzleBatchStorageProvider.overrideWith((ref) { - return mockBatchStorage; - }), - puzzleStorageProvider.overrideWith((ref) => mockHistoryStorage), - ], - ); + final app = await makeTestProviderScopeApp( + tester, + home: PuzzleScreen( + angle: const PuzzleTheme(PuzzleThemeKey.mix), + puzzleId: puzzle2.puzzle.id, + ), + overrides: [ + lichessClientProvider.overrideWith((ref) { + return LichessClient(mockClient, ref); + }), + puzzleBatchStorageProvider.overrideWith((ref) { + return mockBatchStorage; + }), + puzzleStorageProvider.overrideWith((ref) => mockHistoryStorage), + ], + ); - Future saveDBReq() => mockBatchStorage.save( - userId: null, - angle: const PuzzleTheme(PuzzleThemeKey.mix), - data: any(named: 'data'), - ); - when(saveDBReq).thenAnswer((_) async {}); - when( - () => mockBatchStorage.fetch( - userId: null, - angle: const PuzzleTheme(PuzzleThemeKey.mix), - ), - ).thenAnswer((_) async => batch); + Future saveDBReq() => mockBatchStorage.save( + userId: null, + angle: const PuzzleTheme(PuzzleThemeKey.mix), + data: any(named: 'data'), + ); + when(saveDBReq).thenAnswer((_) async {}); + when( + () => mockBatchStorage.fetch(userId: null, angle: const PuzzleTheme(PuzzleThemeKey.mix)), + ).thenAnswer((_) async => batch); - when(() => mockHistoryStorage.save(puzzle: any(named: 'puzzle'))) - .thenAnswer((_) async {}); + when(() => mockHistoryStorage.save(puzzle: any(named: 'puzzle'))).thenAnswer((_) async {}); - await tester.pumpWidget(app); + await tester.pumpWidget(app); - // wait for the puzzle to load - await tester.pump(const Duration(milliseconds: 200)); + // wait for the puzzle to load + await tester.pump(const Duration(milliseconds: 200)); - expect(find.byType(Chessboard), findsOneWidget); - expect(find.text('Your turn'), findsOneWidget); + expect(find.byType(Chessboard), findsOneWidget); + expect(find.text('Your turn'), findsOneWidget); - // before the first move is played, puzzle is not interactable - expect(find.byKey(const Key('g4-blackrook')), findsOneWidget); - await tester.tap(find.byKey(const Key('g4-blackrook'))); - await tester.pump(); - expect(find.byKey(const Key('g4-selected')), findsNothing); + // before the first move is played, puzzle is not interactable + expect(find.byKey(const Key('g4-blackrook')), findsOneWidget); + await tester.tap(find.byKey(const Key('g4-blackrook'))); + await tester.pump(); + expect(find.byKey(const Key('g4-selected')), findsNothing); - const orientation = Side.black; + const orientation = Side.black; - // await for first move to be played - await tester.pump(const Duration(milliseconds: 1500)); + // await for first move to be played + await tester.pump(const Duration(milliseconds: 1500)); - // in play mode we don't see the continue button - expect(find.byIcon(CupertinoIcons.play_arrow_solid), findsNothing); - // in play mode we see the solution button - expect(find.byIcon(Icons.help), findsOneWidget); + // in play mode we don't see the continue button + expect(find.byIcon(CupertinoIcons.play_arrow_solid), findsNothing); + // in play mode we see the solution button + expect(find.byIcon(Icons.help), findsOneWidget); - expect(find.byKey(const Key('g4-blackrook')), findsOneWidget); - expect(find.byKey(const Key('h8-whitequeen')), findsOneWidget); + expect(find.byKey(const Key('g4-blackrook')), findsOneWidget); + expect(find.byKey(const Key('h8-whitequeen')), findsOneWidget); - await playMove(tester, 'g4', 'h4', orientation: orientation); + await playMove(tester, 'g4', 'h4', orientation: orientation); - expect(find.byKey(const Key('h4-blackrook')), findsOneWidget); - expect(find.text('Best move!'), findsOneWidget); + expect(find.byKey(const Key('h4-blackrook')), findsOneWidget); + expect(find.text('Best move!'), findsOneWidget); - // wait for line reply and move animation - await tester.pump(const Duration(milliseconds: 500)); - await tester.pumpAndSettle(); + // wait for line reply and move animation + await tester.pump(const Duration(milliseconds: 500)); + await tester.pumpAndSettle(); - expect(find.byKey(const Key('h4-whitequeen')), findsOneWidget); + expect(find.byKey(const Key('h4-whitequeen')), findsOneWidget); - await playMove(tester, 'b4', 'h4', orientation: orientation); + await playMove(tester, 'b4', 'h4', orientation: orientation); - expect(find.byKey(const Key('h4-blackrook')), findsOneWidget); - expect(find.text('Success!'), findsOneWidget); + expect(find.byKey(const Key('h4-blackrook')), findsOneWidget); + expect(find.text('Success!'), findsOneWidget); - // wait for move animation - await tester.pumpAndSettle(); + // wait for move animation + await tester.pumpAndSettle(); - // called once to save solution and once after fetching a new puzzle - verify(saveDBReq).called(2); + // called once to save solution and once after fetching a new puzzle + verify(saveDBReq).called(2); - expect(find.byIcon(CupertinoIcons.play_arrow_solid), findsOneWidget); - expect(find.byIcon(Icons.help), findsNothing); + expect(find.byIcon(CupertinoIcons.play_arrow_solid), findsOneWidget); + expect(find.byIcon(Icons.help), findsNothing); - await tester.tap(find.byIcon(CupertinoIcons.play_arrow_solid)); + await tester.tap(find.byIcon(CupertinoIcons.play_arrow_solid)); - // await for new puzzle load - await tester.pump(const Duration(milliseconds: 500)); + // await for new puzzle load + await tester.pump(const Duration(milliseconds: 500)); - expect(find.text('Success!'), findsNothing); - expect(find.text('Your turn'), findsOneWidget); + expect(find.text('Success!'), findsNothing); + expect(find.text('Your turn'), findsOneWidget); - // await for view solution timer - await tester.pump(const Duration(seconds: 4)); - }, - ); + // await for view solution timer + await tester.pump(const Duration(seconds: 4)); + }); for (final showRatings in [true, false]) { - testWidgets('fails a puzzle, (showRatings: $showRatings)', - variant: kPlatformVariant, (tester) async { + testWidgets('fails a puzzle, (showRatings: $showRatings)', variant: kPlatformVariant, ( + tester, + ) async { final mockClient = MockClient((request) { if (request.url.path == '/api/puzzle/batch/mix') { return mockResponse(batchOf1, 200); @@ -257,8 +240,9 @@ void main() { return mockResponse('', 404); }); - when(() => mockHistoryStorage.fetch(puzzleId: puzzle2.puzzle.id)) - .thenAnswer((_) async => puzzle2); + when( + () => mockHistoryStorage.fetch(puzzleId: puzzle2.puzzle.id), + ).thenAnswer((_) async => puzzle2); final app = await makeTestProviderScopeApp( tester, @@ -280,20 +264,16 @@ void main() { ], ); - when(() => mockHistoryStorage.save(puzzle: any(named: 'puzzle'))) - .thenAnswer((_) async {}); + when(() => mockHistoryStorage.save(puzzle: any(named: 'puzzle'))).thenAnswer((_) async {}); Future saveDBReq() => mockBatchStorage.save( - userId: null, - angle: const PuzzleTheme(PuzzleThemeKey.mix), - data: any(named: 'data'), - ); + userId: null, + angle: const PuzzleTheme(PuzzleThemeKey.mix), + data: any(named: 'data'), + ); when(saveDBReq).thenAnswer((_) async {}); when( - () => mockBatchStorage.fetch( - userId: null, - angle: const PuzzleTheme(PuzzleThemeKey.mix), - ), + () => mockBatchStorage.fetch(userId: null, angle: const PuzzleTheme(PuzzleThemeKey.mix)), ).thenAnswer((_) async => batch); await tester.pumpWidget(app); @@ -313,10 +293,7 @@ void main() { await playMove(tester, 'g4', 'f4', orientation: orientation); - expect( - find.text("That's not the move!"), - findsOneWidget, - ); + expect(find.text("That's not the move!"), findsOneWidget); // wait for move cancel and animation await tester.pump(const Duration(milliseconds: 500)); @@ -337,10 +314,7 @@ void main() { await playMove(tester, 'b4', 'h4', orientation: orientation); expect(find.byKey(const Key('h4-blackrook')), findsOneWidget); - expect( - find.text('Puzzle complete!'), - findsOneWidget, - ); + expect(find.text('Puzzle complete!'), findsOneWidget); final expectedPlayedXTimes = 'Played ${puzzle2.puzzle.plays.toString().localizeNumbers()} times.'; expect( @@ -360,94 +334,84 @@ void main() { }); } - testWidgets( - 'view solution', - variant: kPlatformVariant, - (tester) async { - final mockClient = MockClient((request) { - if (request.url.path == '/api/puzzle/batch/mix') { - return mockResponse(batchOf1, 200); - } - return mockResponse('', 404); - }); + testWidgets('view solution', variant: kPlatformVariant, (tester) async { + final mockClient = MockClient((request) { + if (request.url.path == '/api/puzzle/batch/mix') { + return mockResponse(batchOf1, 200); + } + return mockResponse('', 404); + }); - final app = await makeTestProviderScopeApp( - tester, - home: PuzzleScreen( - angle: const PuzzleTheme(PuzzleThemeKey.mix), - puzzleId: puzzle2.puzzle.id, - ), - overrides: [ - lichessClientProvider.overrideWith((ref) { - return LichessClient(mockClient, ref); - }), - puzzleBatchStorageProvider.overrideWith((ref) { - return mockBatchStorage; - }), - puzzleStorageProvider.overrideWith((ref) => mockHistoryStorage), - ], - ); + final app = await makeTestProviderScopeApp( + tester, + home: PuzzleScreen( + angle: const PuzzleTheme(PuzzleThemeKey.mix), + puzzleId: puzzle2.puzzle.id, + ), + overrides: [ + lichessClientProvider.overrideWith((ref) { + return LichessClient(mockClient, ref); + }), + puzzleBatchStorageProvider.overrideWith((ref) { + return mockBatchStorage; + }), + puzzleStorageProvider.overrideWith((ref) => mockHistoryStorage), + ], + ); - when(() => mockHistoryStorage.fetch(puzzleId: puzzle2.puzzle.id)) - .thenAnswer((_) async => puzzle2); + when( + () => mockHistoryStorage.fetch(puzzleId: puzzle2.puzzle.id), + ).thenAnswer((_) async => puzzle2); - when(() => mockHistoryStorage.save(puzzle: any(named: 'puzzle'))) - .thenAnswer((_) async {}); + when(() => mockHistoryStorage.save(puzzle: any(named: 'puzzle'))).thenAnswer((_) async {}); - Future saveDBReq() => mockBatchStorage.save( - userId: null, - angle: const PuzzleTheme(PuzzleThemeKey.mix), - data: any(named: 'data'), - ); - when(saveDBReq).thenAnswer((_) async {}); - when( - () => mockBatchStorage.fetch( - userId: null, - angle: const PuzzleTheme(PuzzleThemeKey.mix), - ), - ).thenAnswer((_) async => batch); + Future saveDBReq() => mockBatchStorage.save( + userId: null, + angle: const PuzzleTheme(PuzzleThemeKey.mix), + data: any(named: 'data'), + ); + when(saveDBReq).thenAnswer((_) async {}); + when( + () => mockBatchStorage.fetch(userId: null, angle: const PuzzleTheme(PuzzleThemeKey.mix)), + ).thenAnswer((_) async => batch); - await tester.pumpWidget(app); + await tester.pumpWidget(app); - // wait for the puzzle to load - await tester.pump(const Duration(milliseconds: 200)); + // wait for the puzzle to load + await tester.pump(const Duration(milliseconds: 200)); - expect(find.byType(Chessboard), findsOneWidget); - expect(find.text('Your turn'), findsOneWidget); + expect(find.byType(Chessboard), findsOneWidget); + expect(find.text('Your turn'), findsOneWidget); - // await for first move to be played and view solution button to appear - await tester.pump(const Duration(seconds: 5)); + // await for first move to be played and view solution button to appear + await tester.pump(const Duration(seconds: 5)); - expect(find.byKey(const Key('g4-blackrook')), findsOneWidget); + expect(find.byKey(const Key('g4-blackrook')), findsOneWidget); - expect(find.byIcon(Icons.help), findsOneWidget); - await tester.tap(find.byIcon(Icons.help)); + expect(find.byIcon(Icons.help), findsOneWidget); + await tester.tap(find.byIcon(Icons.help)); - // wait for solution replay animation to finish - await tester.pump(const Duration(seconds: 1)); - await tester.pumpAndSettle(); + // wait for solution replay animation to finish + await tester.pump(const Duration(seconds: 1)); + await tester.pumpAndSettle(); - expect(find.byKey(const Key('h4-blackrook')), findsOneWidget); - expect(find.byKey(const Key('h8-whitequeen')), findsOneWidget); - expect( - find.text('Puzzle complete!'), - findsOneWidget, - ); + expect(find.byKey(const Key('h4-blackrook')), findsOneWidget); + expect(find.byKey(const Key('h8-whitequeen')), findsOneWidget); + expect(find.text('Puzzle complete!'), findsOneWidget); - final nextMoveBtnEnabled = find.byWidgetPredicate( - (widget) => - widget is BottomBarButton && - widget.icon == CupertinoIcons.chevron_forward && - widget.enabled, - ); - expect(nextMoveBtnEnabled, findsOneWidget); + final nextMoveBtnEnabled = find.byWidgetPredicate( + (widget) => + widget is BottomBarButton && + widget.icon == CupertinoIcons.chevron_forward && + widget.enabled, + ); + expect(nextMoveBtnEnabled, findsOneWidget); - expect(find.byIcon(CupertinoIcons.play_arrow_solid), findsOneWidget); + expect(find.byIcon(CupertinoIcons.play_arrow_solid), findsOneWidget); - // called once to save solution and once after fetching a new puzzle - verify(saveDBReq).called(2); - }, - ); + // called once to save solution and once after fetching a new puzzle + verify(saveDBReq).called(2); + }); }); } diff --git a/test/view/puzzle/puzzle_tab_screen_test.dart b/test/view/puzzle/puzzle_tab_screen_test.dart index 051a41fd81..188a18c0db 100644 --- a/test/view/puzzle/puzzle_tab_screen_test.dart +++ b/test/view/puzzle/puzzle_tab_screen_test.dart @@ -35,12 +35,7 @@ class MockPuzzleBatchStorage extends Mock implements PuzzleBatchStorage {} void main() { setUpAll(() { - registerFallbackValue( - PuzzleBatch( - solved: IList(const []), - unsolved: IList([puzzle]), - ), - ); + registerFallbackValue(PuzzleBatch(solved: IList(const []), unsolved: IList([puzzle]))); }); final mockBatchStorage = MockPuzzleBatchStorage(); @@ -49,23 +44,14 @@ void main() { final SemanticsHandle handle = tester.ensureSemantics(); when( - () => mockBatchStorage.fetch( - userId: null, - angle: const PuzzleTheme(PuzzleThemeKey.mix), - ), + () => mockBatchStorage.fetch(userId: null, angle: const PuzzleTheme(PuzzleThemeKey.mix)), ).thenAnswer((_) async => batch); - when( - () => mockBatchStorage.fetchAll( - userId: null, - ), - ).thenAnswer((_) async => IList(const [])); + when(() => mockBatchStorage.fetchAll(userId: null)).thenAnswer((_) async => IList(const [])); final app = await makeTestProviderScopeApp( tester, home: const PuzzleTabScreen(), - overrides: [ - puzzleBatchStorageProvider.overrideWith((ref) => mockBatchStorage), - ], + overrides: [puzzleBatchStorageProvider.overrideWith((ref) => mockBatchStorage)], ); await tester.pumpWidget(app); @@ -87,16 +73,9 @@ void main() { testWidgets('shows puzzle menu', (WidgetTester tester) async { when( - () => mockBatchStorage.fetch( - userId: null, - angle: const PuzzleTheme(PuzzleThemeKey.mix), - ), + () => mockBatchStorage.fetch(userId: null, angle: const PuzzleTheme(PuzzleThemeKey.mix)), ).thenAnswer((_) async => batch); - when( - () => mockBatchStorage.fetchAll( - userId: null, - ), - ).thenAnswer((_) async => IList(const [])); + when(() => mockBatchStorage.fetchAll(userId: null)).thenAnswer((_) async => IList(const [])); final app = await makeTestProviderScopeApp( tester, home: const PuzzleTabScreen(), @@ -120,16 +99,9 @@ void main() { testWidgets('shows daily puzzle', (WidgetTester tester) async { when( - () => mockBatchStorage.fetch( - userId: null, - angle: const PuzzleTheme(PuzzleThemeKey.mix), - ), + () => mockBatchStorage.fetch(userId: null, angle: const PuzzleTheme(PuzzleThemeKey.mix)), ).thenAnswer((_) async => batch); - when( - () => mockBatchStorage.fetchAll( - userId: null, - ), - ).thenAnswer((_) async => IList(const [])); + when(() => mockBatchStorage.fetchAll(userId: null)).thenAnswer((_) async => IList(const [])); final app = await makeTestProviderScopeApp( tester, home: const PuzzleTabScreen(), @@ -150,31 +122,17 @@ void main() { await tester.pump(const Duration(milliseconds: 100)); expect(find.byType(DailyPuzzle), findsOneWidget); - expect( - find.widgetWithText(DailyPuzzle, 'Puzzle of the day'), - findsOneWidget, - ); - expect( - find.widgetWithText(DailyPuzzle, 'Played 93,270 times'), - findsOneWidget, - ); + expect(find.widgetWithText(DailyPuzzle, 'Puzzle of the day'), findsOneWidget); + expect(find.widgetWithText(DailyPuzzle, 'Played 93,270 times'), findsOneWidget); expect(find.widgetWithText(DailyPuzzle, 'Black to play'), findsOneWidget); }); group('tactical training preview', () { - testWidgets('shows first puzzle from unsolved batch', - (WidgetTester tester) async { + testWidgets('shows first puzzle from unsolved batch', (WidgetTester tester) async { when( - () => mockBatchStorage.fetch( - userId: null, - angle: const PuzzleTheme(PuzzleThemeKey.mix), - ), + () => mockBatchStorage.fetch(userId: null, angle: const PuzzleTheme(PuzzleThemeKey.mix)), ).thenAnswer((_) async => batch); - when( - () => mockBatchStorage.fetchAll( - userId: null, - ), - ).thenAnswer((_) async => IList(const [])); + when(() => mockBatchStorage.fetchAll(userId: null)).thenAnswer((_) async => IList(const [])); final app = await makeTestProviderScopeApp( tester, @@ -196,31 +154,24 @@ void main() { await tester.pump(const Duration(milliseconds: 100)); expect(find.byType(PuzzleAnglePreview), findsOneWidget); - expect( - find.widgetWithText(PuzzleAnglePreview, 'Healthy mix'), - findsOneWidget, - ); - final chessboard = find - .descendant( - of: find.byType(PuzzleAnglePreview), - matching: find.byType(Chessboard), - ) - .evaluate() - .first - .widget as Chessboard; - - expect( - chessboard.fen, - equals('4k2r/Q5pp/3bp3/4n3/1r5q/8/PP2B1PP/R1B2R1K b k - 0 21'), - ); + expect(find.widgetWithText(PuzzleAnglePreview, 'Healthy mix'), findsOneWidget); + final chessboard = + find + .descendant( + of: find.byType(PuzzleAnglePreview), + matching: find.byType(Chessboard), + ) + .evaluate() + .first + .widget + as Chessboard; + + expect(chessboard.fen, equals('4k2r/Q5pp/3bp3/4n3/1r5q/8/PP2B1PP/R1B2R1K b k - 0 21')); }); testWidgets('shows saved puzzle batches', (WidgetTester tester) async { when( - () => mockBatchStorage.fetch( - userId: null, - angle: const PuzzleTheme(PuzzleThemeKey.mix), - ), + () => mockBatchStorage.fetch(userId: null, angle: const PuzzleTheme(PuzzleThemeKey.mix)), ).thenAnswer((_) async => batch); when( () => mockBatchStorage.fetch( @@ -229,16 +180,9 @@ void main() { ), ).thenAnswer((_) async => batch); when( - () => mockBatchStorage.fetch( - userId: null, - angle: const PuzzleOpening('A00'), - ), + () => mockBatchStorage.fetch(userId: null, angle: const PuzzleOpening('A00')), ).thenAnswer((_) async => batch); - when( - () => mockBatchStorage.fetchAll( - userId: null, - ), - ).thenAnswer( + when(() => mockBatchStorage.fetchAll(userId: null)).thenAnswer( (_) async => IList(const [ (PuzzleTheme(PuzzleThemeKey.advancedPawn), 50), (PuzzleOpening('A00'), 50), @@ -264,49 +208,27 @@ void main() { // wait for the puzzles to load await tester.pump(const Duration(milliseconds: 100)); - await tester.scrollUntilVisible( - find.widgetWithText(PuzzleAnglePreview, 'A00'), - 200, - ); + await tester.scrollUntilVisible(find.widgetWithText(PuzzleAnglePreview, 'A00'), 200); expect(find.byType(PuzzleAnglePreview), findsNWidgets(3)); - expect( - find.widgetWithText(PuzzleAnglePreview, 'Healthy mix'), - findsOneWidget, - ); - expect( - find.widgetWithText(PuzzleAnglePreview, 'Advanced pawn'), - findsOneWidget, - ); - expect( - find.widgetWithText(PuzzleAnglePreview, 'A00'), - findsOneWidget, - ); + expect(find.widgetWithText(PuzzleAnglePreview, 'Healthy mix'), findsOneWidget); + expect(find.widgetWithText(PuzzleAnglePreview, 'Advanced pawn'), findsOneWidget); + expect(find.widgetWithText(PuzzleAnglePreview, 'A00'), findsOneWidget); }); testWidgets('delete a saved puzzle batch', (WidgetTester tester) async { - final testDb = await openAppDatabase( - databaseFactoryFfiNoIsolate, - inMemoryDatabasePath, - ); + final testDb = await openAppDatabase(databaseFactoryFfiNoIsolate, inMemoryDatabasePath); for (final (angle, timestamp) in [ (const PuzzleTheme(PuzzleThemeKey.mix), '2021-01-01T00:00:00Z'), - ( - const PuzzleTheme(PuzzleThemeKey.advancedPawn), - '2021-01-01T00:00:00Z' - ), + (const PuzzleTheme(PuzzleThemeKey.advancedPawn), '2021-01-01T00:00:00Z'), (const PuzzleOpening('A00'), '2021-01-02T00:00:00Z'), ]) { - await testDb.insert( - 'puzzle_batchs', - { - 'userId': '**anon**', - 'angle': angle.key, - 'data': jsonEncode(onePuzzleBatch.toJson()), - 'lastModified': timestamp, - }, - conflictAlgorithm: ConflictAlgorithm.replace, - ); + await testDb.insert('puzzle_batchs', { + 'userId': '**anon**', + 'angle': angle.key, + 'data': jsonEncode(onePuzzleBatch.toJson()), + 'lastModified': timestamp, + }, conflictAlgorithm: ConflictAlgorithm.replace); } final app = await makeTestProviderScopeApp( @@ -347,19 +269,13 @@ void main() { ); await tester.pumpAndSettle(); - expect( - find.widgetWithText(SlidableAction, 'Delete'), - findsOneWidget, - ); + expect(find.widgetWithText(SlidableAction, 'Delete'), findsOneWidget); await tester.tap(find.widgetWithText(SlidableAction, 'Delete')); await tester.pumpAndSettle(); - expect( - find.widgetWithText(PuzzleAnglePreview, 'A00'), - findsNothing, - ); + expect(find.widgetWithText(PuzzleAnglePreview, 'A00'), findsNothing); }); }); } @@ -383,14 +299,8 @@ final onePuzzleBatch = PuzzleBatch( id: GameId('PrlkCqOv'), perf: Perf.blitz, rated: true, - white: PuzzleGamePlayer( - side: Side.white, - name: 'user1', - ), - black: PuzzleGamePlayer( - side: Side.black, - name: 'user2', - ), + white: PuzzleGamePlayer(side: Side.white, name: 'user1'), + black: PuzzleGamePlayer(side: Side.black, name: 'user2'), pgn: 'e4 Nc6 Bc4 e6 a3 g6 Nf3 Bg7 c3 Nge7 d3 O-O Be3 Na5 Ba2 b6 Qd2', ), ), diff --git a/test/view/puzzle/storm_screen_test.dart b/test/view/puzzle/storm_screen_test.dart index 7f6e38996a..679eb5ddf1 100644 --- a/test/view/puzzle/storm_screen_test.dart +++ b/test/view/puzzle/storm_screen_test.dart @@ -23,137 +23,98 @@ final client = MockClient((request) { void main() { group('StormScreen', () { - testWidgets( - 'meets accessibility guidelines', - (tester) async { - final SemanticsHandle handle = tester.ensureSemantics(); - - final app = await makeTestProviderScopeApp( - tester, - home: const StormScreen(), - overrides: [ - stormProvider.overrideWith((ref) => mockStromRun), - lichessClientProvider - .overrideWith((ref) => LichessClient(client, ref)), - ], - ); - - await tester.pumpWidget(app); - - await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); - handle.dispose(); - }, - variant: kPlatformVariant, - ); - - testWidgets( - 'Load puzzle and play white pieces', - (tester) async { - final app = await makeTestProviderScopeApp( - tester, - home: const StormScreen(), - overrides: [ - stormProvider.overrideWith((ref) => mockStromRun), - lichessClientProvider - .overrideWith((ref) => LichessClient(client, ref)), - ], - ); - - await tester.pumpWidget(app); - - expect(find.byType(Chessboard), findsOneWidget); - expect( - find.text('You play the white pieces in all puzzles'), - findsWidgets, - ); - }, - variant: kPlatformVariant, - ); - - testWidgets( - 'Play one puzzle', - (tester) async { - final app = await makeTestProviderScopeApp( - tester, - home: const StormScreen(), - overrides: [ - stormProvider.overrideWith((ref) => mockStromRun), - lichessClientProvider - .overrideWith((ref) => LichessClient(client, ref)), - ], - ); - - await tester.pumpWidget(app); - - // before the first move is played, puzzle is not interactable - expect(find.byKey(const Key('h5-whiterook')), findsOneWidget); - await tester.tap(find.byKey(const Key('h5-whiterook'))); - await tester.pump(); - expect(find.byKey(const Key('h5-selected')), findsNothing); - - // wait for first move to be played - await tester.pump(const Duration(seconds: 1)); - - expect(find.byKey(const Key('g8-blackking')), findsOneWidget); - - await playMove( - tester, - 'h5', - 'h7', - orientation: Side.white, - ); - - await tester.pump(const Duration(milliseconds: 500)); - await tester.pumpAndSettle(); - expect(find.byKey(const Key('h7-whiterook')), findsOneWidget); - expect(find.byKey(const Key('d1-blackqueen')), findsOneWidget); - - await playMove( - tester, - 'e3', - 'g1', - orientation: Side.white, - ); - - await tester.pump(const Duration(milliseconds: 500)); - - // should have loaded next puzzle - expect(find.byKey(const Key('h6-blackking')), findsOneWidget); - }, - variant: kPlatformVariant, - ); + testWidgets('meets accessibility guidelines', (tester) async { + final SemanticsHandle handle = tester.ensureSemantics(); - testWidgets('shows end run result', (tester) async { final app = await makeTestProviderScopeApp( tester, home: const StormScreen(), overrides: [ stormProvider.overrideWith((ref) => mockStromRun), - lichessClientProvider - .overrideWith((ref) => LichessClient(client, ref)), + lichessClientProvider.overrideWith((ref) => LichessClient(client, ref)), ], ); await tester.pumpWidget(app); - // wait for first move to be played - await tester.pump(const Duration(seconds: 1)); + await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); + handle.dispose(); + }, variant: kPlatformVariant); - await playMove( + testWidgets('Load puzzle and play white pieces', (tester) async { + final app = await makeTestProviderScopeApp( tester, - 'h5', - 'h7', - orientation: Side.white, + home: const StormScreen(), + overrides: [ + stormProvider.overrideWith((ref) => mockStromRun), + lichessClientProvider.overrideWith((ref) => LichessClient(client, ref)), + ], ); + await tester.pumpWidget(app); + + expect(find.byType(Chessboard), findsOneWidget); + expect(find.text('You play the white pieces in all puzzles'), findsWidgets); + }, variant: kPlatformVariant); + + testWidgets('Play one puzzle', (tester) async { + final app = await makeTestProviderScopeApp( + tester, + home: const StormScreen(), + overrides: [ + stormProvider.overrideWith((ref) => mockStromRun), + lichessClientProvider.overrideWith((ref) => LichessClient(client, ref)), + ], + ); + + await tester.pumpWidget(app); + + // before the first move is played, puzzle is not interactable + expect(find.byKey(const Key('h5-whiterook')), findsOneWidget); + await tester.tap(find.byKey(const Key('h5-whiterook'))); + await tester.pump(); + expect(find.byKey(const Key('h5-selected')), findsNothing); + + // wait for first move to be played + await tester.pump(const Duration(seconds: 1)); + + expect(find.byKey(const Key('g8-blackking')), findsOneWidget); + + await playMove(tester, 'h5', 'h7', orientation: Side.white); + + await tester.pump(const Duration(milliseconds: 500)); + await tester.pumpAndSettle(); + expect(find.byKey(const Key('h7-whiterook')), findsOneWidget); + expect(find.byKey(const Key('d1-blackqueen')), findsOneWidget); + + await playMove(tester, 'e3', 'g1', orientation: Side.white); + await tester.pump(const Duration(milliseconds: 500)); - await playMove( + + // should have loaded next puzzle + expect(find.byKey(const Key('h6-blackking')), findsOneWidget); + }, variant: kPlatformVariant); + + testWidgets('shows end run result', (tester) async { + final app = await makeTestProviderScopeApp( tester, - 'e3', - 'g1', - orientation: Side.white, + home: const StormScreen(), + overrides: [ + stormProvider.overrideWith((ref) => mockStromRun), + lichessClientProvider.overrideWith((ref) => LichessClient(client, ref)), + ], ); + await tester.pumpWidget(app); + + // wait for first move to be played + await tester.pump(const Duration(seconds: 1)); + + await playMove(tester, 'h5', 'h7', orientation: Side.white); + + await tester.pump(const Duration(milliseconds: 500)); + await playMove(tester, 'e3', 'g1', orientation: Side.white); + await tester.pump(const Duration(milliseconds: 500)); // should have loaded next puzzle expect(find.byKey(const Key('h6-blackking')), findsOneWidget); @@ -170,8 +131,7 @@ void main() { home: const StormScreen(), overrides: [ stormProvider.overrideWith((ref) => mockStromRun), - lichessClientProvider - .overrideWith((ref) => LichessClient(client, ref)), + lichessClientProvider.overrideWith((ref) => LichessClient(client, ref)), ], ); diff --git a/test/view/puzzle/streak_screen_test.dart b/test/view/puzzle/streak_screen_test.dart index efdd87166e..866b3659cb 100644 --- a/test/view/puzzle/streak_screen_test.dart +++ b/test/view/puzzle/streak_screen_test.dart @@ -13,8 +13,7 @@ import '../../test_provider_scope.dart'; final client = MockClient((request) { if (request.url.path == '/api/streak') { - return mockResponse( - ''' + return mockResponse(''' { "game": { "id": "Xndtxsoa", @@ -65,12 +64,9 @@ final client = MockClient((request) { }, "streak": "MptxK 4CZxz kcN3a 1I9Ly kOx90 eTrkO G0tpf iwTxQ tg2IU TovLC 0miTI Jpmkf 8VqjS XftoM 70UGG lm8O8 R4Y49 76Llk XZOyq QUgzo dACnQ qFjLp ytKo4 6JIj1 SYz3x kEkib dkvMp Dk0Ln Ok3qk zbRCc fQSVb vmDLx VJw06 3up01 X9aHm EicvD 5lhwD fTJE0 08LZy XAsVO TVB8s VCLTk KH6zc CaByR E2dUi JOxJg Agtzu KwbY9 Rmcf7 k9jGo 0zTgd 5YCx8 BtqDp DQdRO ytwPd sHqWB 1WunB Fovke mmMDN UNcwu isI02 3sIJB mnuzi 4aaRt Jvkvj UsXO2 kLfmz gsC1H TADGH a0Jz6 oUPR2 1IOBO 9PUdj haSH3 wn5by 22fL0 CR3Wu FaBtd DorJu unTls qeu0r xo40H DssQ9 D6s6S hkWx4 GF7s5 rzREu vhsbo s1haw j9ckI ekJnL TvcVB a7T4o 1olwh pydoy rGs3G k5ljZ gowEl UNXOV XkaUw 10lYO 6Ufqg Q45go KxGe3 vgwIt lqoaX nBtOq uAo3e jsbpu JLtdz TGUcX PobG5 ScDAL YPEfv o52sU FV0lM evQzq qAny0 dkDJi 0AUNz uzI6q kh13r Rubxa ecY6Q T9EL2 TmBka DPT5t qmzEf dyo0g MsGbE hPkmk 3wZBI 7kpeT 6EKGn kozHL Vnaiz 6DzDP HQ5RQ 7Ilyn 9n7Pz PwtXo kgMG2 J7gat gXcxs 4YVfC e8jGb m71Kb 9OrKY z530i" } - ''', - 200, - ); + ''', 200); } else if (request.url.path == '/api/puzzle/4CZxz') { - return mockResponse( - ''' + return mockResponse(''' { "game": { "id": "MQOxq7Jl", @@ -114,12 +110,9 @@ final client = MockClient((request) { "initialPly": 87 } } - ''', - 200, - ); + ''', 200); } else if (request.url.path == '/api/puzzle/kcN3a') { - return mockResponse( - ''' + return mockResponse(''' { "game": { "id": "bEuHKQSa", @@ -161,12 +154,9 @@ final client = MockClient((request) { "initialPly": 36 } } - ''', - 200, - ); + ''', 200); } else if (request.url.path == '/api/puzzle/1I9Ly') { - return mockResponse( - ''' + return mockResponse(''' { "game": { "id": "DTmg6BsX", @@ -210,58 +200,48 @@ final client = MockClient((request) { "initialPly": 22 } } - ''', - 200, - ); + ''', 200); } return mockResponse('', 404); }); void main() { group('StreakScreen', () { - testWidgets( - 'meets accessibility guidelines', - (tester) async { - final SemanticsHandle handle = tester.ensureSemantics(); + testWidgets('meets accessibility guidelines', (tester) async { + final SemanticsHandle handle = tester.ensureSemantics(); - final app = await makeTestProviderScopeApp( - tester, - home: const StreakScreen(), - overrides: [ - lichessClientProvider - .overrideWith((ref) => LichessClient(client, ref)), - ], - ); + final app = await makeTestProviderScopeApp( + tester, + home: const StreakScreen(), + overrides: [lichessClientProvider.overrideWith((ref) => LichessClient(client, ref))], + ); - await tester.pumpWidget(app); + await tester.pumpWidget(app); - await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); - handle.dispose(); - }, - variant: kPlatformVariant, - ); + await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); + handle.dispose(); + }, variant: kPlatformVariant); testWidgets('Score is saved when exiting screen', (tester) async { final app = await makeTestProviderScopeApp( tester, home: Builder( - builder: (context) => PlatformScaffold( - appBar: const PlatformAppBar(title: Text('Test Streak Screen')), - body: FatButton( - semanticsLabel: 'Start Streak', - child: const Text('Start Streak'), - onPressed: () => pushPlatformRoute( - context, - rootNavigator: true, - builder: (context) => const StreakScreen(), + builder: + (context) => PlatformScaffold( + appBar: const PlatformAppBar(title: Text('Test Streak Screen')), + body: FatButton( + semanticsLabel: 'Start Streak', + child: const Text('Start Streak'), + onPressed: + () => pushPlatformRoute( + context, + rootNavigator: true, + builder: (context) => const StreakScreen(), + ), + ), ), - ), - ), ), - overrides: [ - lichessClientProvider - .overrideWith((ref) => LichessClient(client, ref)), - ], + overrides: [lichessClientProvider.overrideWith((ref) => LichessClient(client, ref))], ); await tester.pumpWidget(app); diff --git a/test/view/settings/settings_tab_screen_test.dart b/test/view/settings/settings_tab_screen_test.dart index b907dc94a0..2c4fdb627e 100644 --- a/test/view/settings/settings_tab_screen_test.dart +++ b/test/view/settings/settings_tab_screen_test.dart @@ -21,83 +21,60 @@ final client = MockClient((request) { void main() { group('SettingsTabScreen', () { - testWidgets( - 'meets accessibility guidelines', - (WidgetTester tester) async { - final SemanticsHandle handle = tester.ensureSemantics(); - - final app = await makeTestProviderScopeApp( - tester, - home: const SettingsTabScreen(), - ); - - await tester.pumpWidget(app); - - await meetsTapTargetGuideline(tester); - - await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); - - await expectLater(tester, meetsGuideline(textContrastGuideline)); - handle.dispose(); - }, - variant: kPlatformVariant, - ); - - testWidgets( - "don't show signOut if no session", - (WidgetTester tester) async { - final app = await makeTestProviderScopeApp( - tester, - home: const SettingsTabScreen(), - ); - - await tester.pumpWidget(app); - - expect(find.text('Sign out'), findsNothing); - }, - variant: kPlatformVariant, - ); - - testWidgets( - 'signout', - (WidgetTester tester) async { - final app = await makeTestProviderScopeApp( - tester, - home: const SettingsTabScreen(), - userSession: fakeSession, - overrides: [ - lichessClientProvider - .overrideWith((ref) => LichessClient(client, ref)), - getDbSizeInBytesProvider.overrideWith((_) => 1000), - ], - ); - - await tester.pumpWidget(app); - - expect(find.text('Sign out'), findsOneWidget); - - await tester.tap( - find.widgetWithText(PlatformListTile, 'Sign out'), - warnIfMissed: false, - ); - await tester.pumpAndSettle(); - - // confirm - if (debugDefaultTargetPlatformOverride == TargetPlatform.iOS) { - await tester - .tap(find.widgetWithText(CupertinoActionSheetAction, 'Sign out')); - } else { - await tester.tap(find.text('OK')); - } - await tester.pump(); - - expect(find.byType(CircularProgressIndicator), findsOneWidget); - // wait for sign out future - await tester.pump(const Duration(seconds: 1)); - - expect(find.text('Sign out'), findsNothing); - }, - variant: kPlatformVariant, - ); + testWidgets('meets accessibility guidelines', (WidgetTester tester) async { + final SemanticsHandle handle = tester.ensureSemantics(); + + final app = await makeTestProviderScopeApp(tester, home: const SettingsTabScreen()); + + await tester.pumpWidget(app); + + await meetsTapTargetGuideline(tester); + + await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); + + await expectLater(tester, meetsGuideline(textContrastGuideline)); + handle.dispose(); + }, variant: kPlatformVariant); + + testWidgets("don't show signOut if no session", (WidgetTester tester) async { + final app = await makeTestProviderScopeApp(tester, home: const SettingsTabScreen()); + + await tester.pumpWidget(app); + + expect(find.text('Sign out'), findsNothing); + }, variant: kPlatformVariant); + + testWidgets('signout', (WidgetTester tester) async { + final app = await makeTestProviderScopeApp( + tester, + home: const SettingsTabScreen(), + userSession: fakeSession, + overrides: [ + lichessClientProvider.overrideWith((ref) => LichessClient(client, ref)), + getDbSizeInBytesProvider.overrideWith((_) => 1000), + ], + ); + + await tester.pumpWidget(app); + + expect(find.text('Sign out'), findsOneWidget); + + await tester.tap(find.widgetWithText(PlatformListTile, 'Sign out'), warnIfMissed: false); + await tester.pumpAndSettle(); + + // confirm + if (debugDefaultTargetPlatformOverride == TargetPlatform.iOS) { + await tester.tap(find.widgetWithText(CupertinoActionSheetAction, 'Sign out')); + } else { + await tester.tap(find.text('OK')); + } + await tester.pump(); + + expect(find.byType(CircularProgressIndicator), findsOneWidget); + // wait for sign out future + await tester.pump(const Duration(seconds: 1)); + + expect(find.text('Sign out'), findsNothing); + }, variant: kPlatformVariant); }); } diff --git a/test/view/study/study_list_screen_test.dart b/test/view/study/study_list_screen_test.dart index 0cf063d7a9..d0dbd37d22 100644 --- a/test/view/study/study_list_screen_test.dart +++ b/test/view/study/study_list_screen_test.dart @@ -55,13 +55,11 @@ void main() { final requestedUrls = []; final mockClient = MockClient((request) { requestedUrls.add(request.url.toString()); - if (request.url.path == '/study/all/hot' && - request.url.queryParameters['page'] == '1') { + if (request.url.path == '/study/all/hot' && request.url.queryParameters['page'] == '1') { return mockResponse(kStudyAllHotPage1Response, 200); } else if (request.url.path == '/study/search') { if (request.url.queryParameters['q'] == 'Magnus') { - return mockResponse( - ''' + return mockResponse(''' { "paginator": { "currentPage": 1, @@ -91,9 +89,7 @@ void main() { "nbPages": 1 } } - ''', - 200, - ); + ''', 200); } } return mockResponse('', 404); diff --git a/test/view/study/study_screen_test.dart b/test/view/study/study_screen_test.dart index 5073bfd240..11b3e63f84 100644 --- a/test/view/study/study_screen_test.dart +++ b/test/view/study/study_screen_test.dart @@ -51,8 +51,7 @@ Study makeStudy({ ownerId: null, features: (cloneable: false, chat: false, sticky: false), topics: const IList.empty(), - chapters: chapters ?? - IList([StudyChapterMeta(id: chapter.id, name: '', fen: null)]), + chapters: chapters ?? IList([StudyChapterMeta(id: chapter.id, name: '', fen: null)]), chapter: chapter, hints: hints, deviationComments: deviationComments, @@ -64,19 +63,14 @@ void main() { testWidgets('Displays PGN moves and comments', (WidgetTester tester) async { final mockRepository = MockStudyRepository(); - when(() => mockRepository.getStudy(id: testId)).thenAnswer( - (_) async => - (makeStudy(), '{root comment} 1. e4 {wow} e5 {such chess}'), - ); + when( + () => mockRepository.getStudy(id: testId), + ).thenAnswer((_) async => (makeStudy(), '{root comment} 1. e4 {wow} e5 {such chess}')); final app = await makeTestProviderScopeApp( tester, home: const StudyScreen(id: testId), - overrides: [ - studyRepositoryProvider.overrideWith( - (ref) => mockRepository, - ), - ], + overrides: [studyRepositoryProvider.overrideWith((ref) => mockRepository)], ); await tester.pumpWidget(app); @@ -96,16 +90,8 @@ void main() { final studyChapter1 = makeStudy( chapter: makeChapter(id: const StudyChapterId('1')), chapters: IList(const [ - StudyChapterMeta( - id: StudyChapterId('1'), - name: 'Chapter 1', - fen: null, - ), - StudyChapterMeta( - id: StudyChapterId('2'), - name: 'Chapter 2', - fen: null, - ), + StudyChapterMeta(id: StudyChapterId('1'), name: 'Chapter 1', fen: null), + StudyChapterMeta(id: StudyChapterId('2'), name: 'Chapter 2', fen: null), ]), ); @@ -113,34 +99,20 @@ void main() { chapter: makeChapter(id: const StudyChapterId('2')), ); - when(() => mockRepository.getStudy(id: testId)).thenAnswer( - (_) async => (studyChapter1, '{pgn 1}'), - ); when( - () => mockRepository.getStudy( - id: testId, - chapterId: const StudyChapterId('1'), - ), - ).thenAnswer( - (_) async => (studyChapter1, '{pgn 1}'), - ); + () => mockRepository.getStudy(id: testId), + ).thenAnswer((_) async => (studyChapter1, '{pgn 1}')); when( - () => mockRepository.getStudy( - id: testId, - chapterId: const StudyChapterId('2'), - ), - ).thenAnswer( - (_) async => (studyChapter2, '{pgn 2}'), - ); + () => mockRepository.getStudy(id: testId, chapterId: const StudyChapterId('1')), + ).thenAnswer((_) async => (studyChapter1, '{pgn 1}')); + when( + () => mockRepository.getStudy(id: testId, chapterId: const StudyChapterId('2')), + ).thenAnswer((_) async => (studyChapter2, '{pgn 2}')); final app = await makeTestProviderScopeApp( tester, home: const StudyScreen(id: testId), - overrides: [ - studyRepositoryProvider.overrideWith( - (ref) => mockRepository, - ), - ], + overrides: [studyRepositoryProvider.overrideWith((ref) => mockRepository)], ); await tester.pumpWidget(app); // Wait for study to load @@ -172,17 +144,11 @@ void main() { await tester.pumpAndSettle(); expect( - find.descendant( - of: find.byType(Scrollable), - matching: find.text('Chapter 1'), - ), + find.descendant(of: find.byType(Scrollable), matching: find.text('Chapter 1')), findsOneWidget, ); expect( - find.descendant( - of: find.byType(Scrollable), - matching: find.text('Chapter 2'), - ), + find.descendant(of: find.byType(Scrollable), matching: find.text('Chapter 2')), findsOneWidget, ); @@ -201,24 +167,15 @@ void main() { final mockRepository = MockStudyRepository(); when(() => mockRepository.getStudy(id: testId)).thenAnswer( (_) async => ( - makeStudy( - chapter: makeChapter( - id: const StudyChapterId('1'), - orientation: Side.black, - ), - ), - '' + makeStudy(chapter: makeChapter(id: const StudyChapterId('1'), orientation: Side.black)), + '', ), ); final app = await makeTestProviderScopeApp( tester, home: const StudyScreen(id: testId), - overrides: [ - studyRepositoryProvider.overrideWith( - (ref) => mockRepository, - ), - ], + overrides: [studyRepositoryProvider.overrideWith((ref) => mockRepository)], ); await tester.pumpWidget(app); // Wait for study to load @@ -265,18 +222,14 @@ void main() { { We begin our lecture with an 'easy but not easy' example. White to play and win. } 1. Nd5!! { Brilliant! You noticed that the queen on c4 was kinda smothered. } (1. Ne2? { Not much to say after ...Qc7. }) 1... exd5 2. Rc3 Qa4 3. Rg3! { A fork, threatening Rg7 & b3. } { [%csl Gg7][%cal Gg3g7,Gd4g7,Gb2b3] } (3. Rxc8?? { Uh-oh! After Rc8, b3, there is the counter-sac Rxc2, which is winning for black!! } 3... Raxc8 4. b3 Rxc2!! 5. Qxc2 Qxd4 \$19) 3... g6 4. b3 \$18 { ...and the queen is trapped. GGs. If this was too hard for you, don't worry, there will be easier examples. } * - ''' + ''', ), ); final app = await makeTestProviderScopeApp( tester, home: const StudyScreen(id: testId), - overrides: [ - studyRepositoryProvider.overrideWith( - (ref) => mockRepository, - ), - ], + overrides: [studyRepositoryProvider.overrideWith((ref) => mockRepository)], ); await tester.pumpWidget(app); // Wait for study to load @@ -288,9 +241,7 @@ void main() { expect(find.text(introText), findsOneWidget); expect( - find.text( - 'Brilliant! You noticed that the queen on c4 was kinda smothered.', - ), + find.text('Brilliant! You noticed that the queen on c4 was kinda smothered.'), findsNothing, ); @@ -323,9 +274,7 @@ void main() { await playMove(tester, 'c3', 'd5'); expect( - find.text( - 'Brilliant! You noticed that the queen on c4 was kinda smothered.', - ), + find.text('Brilliant! You noticed that the queen on c4 was kinda smothered.'), findsOneWidget, ); @@ -333,19 +282,14 @@ void main() { await tester.pump(const Duration(seconds: 1)); expect( - find.text( - 'Brilliant! You noticed that the queen on c4 was kinda smothered.', - ), + find.text('Brilliant! You noticed that the queen on c4 was kinda smothered.'), findsOneWidget, ); await tester.tap(find.byTooltip('Next')); await tester.pump(); // Wait for opponent move to be played - expect( - find.text('What would you play in this position?'), - findsOneWidget, - ); + expect(find.text('What would you play in this position?'), findsOneWidget); await playMove(tester, 'f3', 'c3'); expect(find.text('Good move'), findsOneWidget); @@ -353,10 +297,7 @@ void main() { // No explicit feedback, so opponent move should be played automatically after delay await tester.pump(const Duration(seconds: 1)); - expect( - find.text('What would you play in this position?'), - findsOneWidget, - ); + expect(find.text('What would you play in this position?'), findsOneWidget); await playMove(tester, 'c3', 'g3'); expect(find.text('A fork, threatening Rg7 & b3.'), findsOneWidget); @@ -364,10 +305,7 @@ void main() { await tester.tap(find.byTooltip('Next')); await tester.pump(); // Wait for opponent move to be played - expect( - find.text('What would you play in this position?'), - findsOneWidget, - ); + expect(find.text('What would you play in this position?'), findsOneWidget); await playMove(tester, 'b2', 'b3'); @@ -383,8 +321,7 @@ void main() { expect(find.byTooltip('Analysis board'), findsOneWidget); }); - testWidgets('Interactive study hints and deviation comments', - (WidgetTester tester) async { + testWidgets('Interactive study hints and deviation comments', (WidgetTester tester) async { final mockRepository = MockStudyRepository(); when(() => mockRepository.getStudy(id: testId)).thenAnswer( (_) async => ( @@ -394,31 +331,17 @@ void main() { orientation: Side.white, gamebook: true, ), - hints: [ - 'Hint 1', - null, - null, - null, - ].lock, - deviationComments: [ - null, - 'Shown if any move other than d4 is played', - null, - null, - ].lock, + hints: ['Hint 1', null, null, null].lock, + deviationComments: [null, 'Shown if any move other than d4 is played', null, null].lock, ), - '1. e4 (1. d4 {Shown if d4 is played}) e5 2. Nf3' + '1. e4 (1. d4 {Shown if d4 is played}) e5 2. Nf3', ), ); final app = await makeTestProviderScopeApp( tester, home: const StudyScreen(id: testId), - overrides: [ - studyRepositoryProvider.overrideWith( - (ref) => mockRepository, - ), - ], + overrides: [studyRepositoryProvider.overrideWith((ref) => mockRepository)], ); await tester.pumpWidget(app); // Wait for study to load @@ -433,10 +356,7 @@ void main() { expect(find.text('Get a hint'), findsNothing); await playMove(tester, 'e2', 'e3'); - expect( - find.text('Shown if any move other than d4 is played'), - findsOneWidget, - ); + expect(find.text('Shown if any move other than d4 is played'), findsOneWidget); await tester.tap(find.byTooltip('Retry')); await tester.pump(); // Wait for move to be taken back @@ -458,10 +378,7 @@ void main() { // Wait for wrong move to be taken back await tester.pump(const Duration(seconds: 1)); - expect( - find.text('What would you play in this position?'), - findsOneWidget, - ); + expect(find.text('What would you play in this position?'), findsOneWidget); expect(find.text("That's not the move!"), findsNothing); }); }); diff --git a/test/view/user/leaderboard_screen_test.dart b/test/view/user/leaderboard_screen_test.dart index 40575d6b6d..70e18efd3a 100644 --- a/test/view/user/leaderboard_screen_test.dart +++ b/test/view/user/leaderboard_screen_test.dart @@ -16,36 +16,29 @@ final client = MockClient((request) { void main() { group('LeaderboardScreen', () { - testWidgets( - 'meets accessibility guidelines', - (WidgetTester tester) async { - final SemanticsHandle handle = tester.ensureSemantics(); + testWidgets('meets accessibility guidelines', (WidgetTester tester) async { + final SemanticsHandle handle = tester.ensureSemantics(); - final app = await makeTestProviderScopeApp( - tester, - overrides: [ - lichessClientProvider - .overrideWith((ref) => LichessClient(client, ref)), - ], - home: const LeaderboardScreen(), - ); + final app = await makeTestProviderScopeApp( + tester, + overrides: [lichessClientProvider.overrideWith((ref) => LichessClient(client, ref))], + home: const LeaderboardScreen(), + ); - await tester.pumpWidget(app); + await tester.pumpWidget(app); - await tester.pump(const Duration(milliseconds: 200)); + await tester.pump(const Duration(milliseconds: 200)); - // TODO find why it fails on android - // await meetsTapTargetGuideline(tester); + // TODO find why it fails on android + // await meetsTapTargetGuideline(tester); - await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); + await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); - if (debugDefaultTargetPlatformOverride == TargetPlatform.android) { - await expectLater(tester, meetsGuideline(textContrastGuideline)); - } - handle.dispose(); - }, - variant: kPlatformVariant, - ); + if (debugDefaultTargetPlatformOverride == TargetPlatform.android) { + await expectLater(tester, meetsGuideline(textContrastGuideline)); + } + handle.dispose(); + }, variant: kPlatformVariant); }); } diff --git a/test/view/user/leaderboard_widget_test.dart b/test/view/user/leaderboard_widget_test.dart index a34e0ebd5d..17611b545a 100644 --- a/test/view/user/leaderboard_widget_test.dart +++ b/test/view/user/leaderboard_widget_test.dart @@ -17,47 +17,31 @@ final client = MockClient((request) { void main() { group('LeaderboardWidget', () { - testWidgets( - 'accessibility and basic info showing test', - (WidgetTester tester) async { - final SemanticsHandle handle = tester.ensureSemantics(); - final app = await makeTestProviderScopeApp( - tester, - home: Column(children: [LeaderboardWidget()]), - overrides: [ - lichessClientProvider - .overrideWith((ref) => LichessClient(client, ref)), - ], - ); + testWidgets('accessibility and basic info showing test', (WidgetTester tester) async { + final SemanticsHandle handle = tester.ensureSemantics(); + final app = await makeTestProviderScopeApp( + tester, + home: Column(children: [LeaderboardWidget()]), + overrides: [lichessClientProvider.overrideWith((ref) => LichessClient(client, ref))], + ); - await tester.pumpWidget(app); + await tester.pumpWidget(app); - await tester.pump(const Duration(milliseconds: 50)); + await tester.pump(const Duration(milliseconds: 50)); - for (final name in [ - 'Svetlana', - 'Marcel', - 'Anthony', - 'Patoulatchi', - 'Cerdan', - ]) { - expect( - find.widgetWithText(LeaderboardListTile, name), - findsOneWidget, - ); - } + for (final name in ['Svetlana', 'Marcel', 'Anthony', 'Patoulatchi', 'Cerdan']) { + expect(find.widgetWithText(LeaderboardListTile, name), findsOneWidget); + } - // await meetsTapTargetGuideline(tester); + // await meetsTapTargetGuideline(tester); - // await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); + // await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); - // if (debugDefaultTargetPlatformOverride == TargetPlatform.android) { - // await expectLater(tester, meetsGuideline(textContrastGuideline)); - // } - handle.dispose(); - }, - variant: kPlatformVariant, - ); + // if (debugDefaultTargetPlatformOverride == TargetPlatform.android) { + // await expectLater(tester, meetsGuideline(textContrastGuideline)); + // } + handle.dispose(); + }, variant: kPlatformVariant); }); } diff --git a/test/view/user/perf_stats_screen_test.dart b/test/view/user/perf_stats_screen_test.dart index 33013947fd..95a8a02562 100644 --- a/test/view/user/perf_stats_screen_test.dart +++ b/test/view/user/perf_stats_screen_test.dart @@ -22,85 +22,68 @@ final client = MockClient((request) { void main() { group('PerfStatsScreen', () { - testWidgets( - 'meets accessibility guidelines', - (WidgetTester tester) async { - final SemanticsHandle handle = tester.ensureSemantics(); + testWidgets('meets accessibility guidelines', (WidgetTester tester) async { + final SemanticsHandle handle = tester.ensureSemantics(); - final app = await makeTestProviderScopeApp( - tester, - home: PerfStatsScreen( - user: fakeUser, - perf: testPerf, - ), - overrides: [ - lichessClientProvider.overrideWith((ref) { - return LichessClient(client, ref); - }), - ], - ); + final app = await makeTestProviderScopeApp( + tester, + home: PerfStatsScreen(user: fakeUser, perf: testPerf), + overrides: [ + lichessClientProvider.overrideWith((ref) { + return LichessClient(client, ref); + }), + ], + ); - await tester.pumpWidget(app); + await tester.pumpWidget(app); - // wait for auth state and perf stats - await tester.pump(const Duration(milliseconds: 50)); + // wait for auth state and perf stats + await tester.pump(const Duration(milliseconds: 50)); - await meetsTapTargetGuideline(tester); - await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); + await meetsTapTargetGuideline(tester); + await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); - if (debugDefaultTargetPlatformOverride == TargetPlatform.android) { - await expectLater(tester, meetsGuideline(textContrastGuideline)); - } - handle.dispose(); - }, - variant: kPlatformVariant, - ); + if (debugDefaultTargetPlatformOverride == TargetPlatform.android) { + await expectLater(tester, meetsGuideline(textContrastGuideline)); + } + handle.dispose(); + }, variant: kPlatformVariant); - testWidgets( - 'screen loads, required stats are shown', - (WidgetTester tester) async { - final app = await makeTestProviderScopeApp( - tester, - home: PerfStatsScreen( - user: fakeUser, - perf: testPerf, - ), - overrides: [ - lichessClientProvider.overrideWith((ref) { - return LichessClient(client, ref); - }), - ], - ); + testWidgets('screen loads, required stats are shown', (WidgetTester tester) async { + final app = await makeTestProviderScopeApp( + tester, + home: PerfStatsScreen(user: fakeUser, perf: testPerf), + overrides: [ + lichessClientProvider.overrideWith((ref) { + return LichessClient(client, ref); + }), + ], + ); - await tester.pumpWidget(app); + await tester.pumpWidget(app); - // wait for auth state and perf stats - await tester.pump(const Duration(milliseconds: 50)); + // wait for auth state and perf stats + await tester.pump(const Duration(milliseconds: 50)); - final requiredStatsValues = [ - '50.24', // Deviation - '20', // Progression in last 12 games - '0', // Berserked games - '0', // Tournament games - '3', // Rated games - '2', // Won games - '2', // Lost games - '1', // Drawn games - '1', // Disconnections - ]; + final requiredStatsValues = [ + '50.24', // Deviation + '20', // Progression in last 12 games + '0', // Berserked games + '0', // Tournament games + '3', // Rated games + '2', // Won games + '2', // Lost games + '1', // Drawn games + '1', // Disconnections + ]; - // rating - expect(find.text('1500'), findsOneWidget); + // rating + expect(find.text('1500'), findsOneWidget); - for (final val in requiredStatsValues) { - expect( - find.widgetWithText(PlatformCard, val), - findsAtLeastNWidgets(1), - ); - } - }, - variant: kPlatformVariant, - ); + for (final val in requiredStatsValues) { + expect(find.widgetWithText(PlatformCard, val), findsAtLeastNWidgets(1)); + } + }, variant: kPlatformVariant); }); } diff --git a/test/view/user/search_screen_test.dart b/test/view/user/search_screen_test.dart index 93579a2efa..eefe7e3c89 100644 --- a/test/view/user/search_screen_test.dart +++ b/test/view/user/search_screen_test.dart @@ -22,83 +22,69 @@ final client = MockClient((request) { void main() { group('SearchScreen', () { - testWidgets( - 'should see search results', - (WidgetTester tester) async { - final app = await makeTestProviderScopeApp( - tester, - home: const SearchScreen(), - overrides: [ - lichessClientProvider - .overrideWith((ref) => LichessClient(client, ref)), - ], - ); - - await tester.pumpWidget(app); - - final textFieldFinder = - debugDefaultTargetPlatformOverride == TargetPlatform.iOS - ? find.byType(CupertinoSearchTextField) - : find.byType(SearchBar); - - await tester.enterText(textFieldFinder, 'joh'); - - // await debouce call - await tester.pump(const Duration(milliseconds: 300)); - - expect(find.byType(CircularProgressIndicator), findsOneWidget); - - // await response - await tester.pumpAndSettle(const Duration(milliseconds: 100)); - - expect(find.byType(CircularProgressIndicator), findsNothing); - expect(find.text('Players with "joh"'), findsOneWidget); - expect(find.byType(UserListTile), findsNWidgets(2)); - expect(find.text('John Doe'), findsOneWidget); - expect(find.text('John Doe 2'), findsOneWidget); - - // await debouce call for saving search history - await tester.pump(const Duration(seconds: 2)); - }, - variant: kPlatformVariant, - ); - - testWidgets( - 'should see "no result" when search finds nothing', - (WidgetTester tester) async { - final app = await makeTestProviderScopeApp( - tester, - home: const SearchScreen(), - overrides: [ - lichessClientProvider - .overrideWith((ref) => LichessClient(client, ref)), - ], - ); - - await tester.pumpWidget(app); - - final textFieldFinder = - debugDefaultTargetPlatformOverride == TargetPlatform.iOS - ? find.byType(CupertinoSearchTextField) - : find.byType(SearchBar); - - await tester.enterText(textFieldFinder, 'johnny'); - // await debouce call - await tester.pump(const Duration(milliseconds: 300)); - - expect(find.byType(CircularProgressIndicator), findsOneWidget); - - // await response - await tester.pumpAndSettle(const Duration(milliseconds: 100)); - - expect(find.text('Players with "johnny"'), findsNothing); - expect(find.text('No results'), findsOneWidget); - - // await debouce call for saving search history - await tester.pump(const Duration(seconds: 2)); - }, - variant: kPlatformVariant, - ); + testWidgets('should see search results', (WidgetTester tester) async { + final app = await makeTestProviderScopeApp( + tester, + home: const SearchScreen(), + overrides: [lichessClientProvider.overrideWith((ref) => LichessClient(client, ref))], + ); + + await tester.pumpWidget(app); + + final textFieldFinder = + debugDefaultTargetPlatformOverride == TargetPlatform.iOS + ? find.byType(CupertinoSearchTextField) + : find.byType(SearchBar); + + await tester.enterText(textFieldFinder, 'joh'); + + // await debouce call + await tester.pump(const Duration(milliseconds: 300)); + + expect(find.byType(CircularProgressIndicator), findsOneWidget); + + // await response + await tester.pumpAndSettle(const Duration(milliseconds: 100)); + + expect(find.byType(CircularProgressIndicator), findsNothing); + expect(find.text('Players with "joh"'), findsOneWidget); + expect(find.byType(UserListTile), findsNWidgets(2)); + expect(find.text('John Doe'), findsOneWidget); + expect(find.text('John Doe 2'), findsOneWidget); + + // await debouce call for saving search history + await tester.pump(const Duration(seconds: 2)); + }, variant: kPlatformVariant); + + testWidgets('should see "no result" when search finds nothing', (WidgetTester tester) async { + final app = await makeTestProviderScopeApp( + tester, + home: const SearchScreen(), + overrides: [lichessClientProvider.overrideWith((ref) => LichessClient(client, ref))], + ); + + await tester.pumpWidget(app); + + final textFieldFinder = + debugDefaultTargetPlatformOverride == TargetPlatform.iOS + ? find.byType(CupertinoSearchTextField) + : find.byType(SearchBar); + + await tester.enterText(textFieldFinder, 'johnny'); + // await debouce call + await tester.pump(const Duration(milliseconds: 300)); + + expect(find.byType(CircularProgressIndicator), findsOneWidget); + + // await response + await tester.pumpAndSettle(const Duration(milliseconds: 100)); + + expect(find.text('Players with "johnny"'), findsNothing); + expect(find.text('No results'), findsOneWidget); + + // await debouce call for saving search history + await tester.pump(const Duration(seconds: 2)); + }, variant: kPlatformVariant); }); } diff --git a/test/view/user/user_screen_test.dart b/test/view/user/user_screen_test.dart index 91648e6db8..fdc11214e4 100644 --- a/test/view/user/user_screen_test.dart +++ b/test/view/user/user_screen_test.dart @@ -15,8 +15,7 @@ final client = MockClient((request) { } else if (request.url.path == '/api/user/$testUserId') { return mockResponse(testUserResponse, 200); } else if (request.url.path == '/api/users/status') { - return mockResponse( - ''' + return mockResponse(''' [ { "id": "$testUserId", @@ -24,9 +23,7 @@ final client = MockClient((request) { "online": true } ] -''', - 200, - ); +''', 200); } else if (request.url.path == '/api/user/$testUserId/activity') { return mockResponse(userActivityResponse, 200); } @@ -35,36 +32,26 @@ final client = MockClient((request) { void main() { group('UserScreen', () { - testWidgets( - 'should see activity and recent games', - (WidgetTester tester) async { - final app = await makeTestProviderScopeApp( - tester, - home: const UserScreen(user: testUser), - overrides: [ - lichessClientProvider - .overrideWith((ref) => LichessClient(client, ref)), - ], - ); + testWidgets('should see activity and recent games', (WidgetTester tester) async { + final app = await makeTestProviderScopeApp( + tester, + home: const UserScreen(user: testUser), + overrides: [lichessClientProvider.overrideWith((ref) => LichessClient(client, ref))], + ); - await tester.pumpWidget(app); + await tester.pumpWidget(app); - // wait for user request - await tester.pump(const Duration(milliseconds: 50)); + // wait for user request + await tester.pump(const Duration(milliseconds: 50)); - // full name at the top - expect( - find.text('John Doe'), - findsOneWidget, - ); + // full name at the top + expect(find.text('John Doe'), findsOneWidget); - // wait for recent games and activity - await tester.pump(const Duration(milliseconds: 50)); + // wait for recent games and activity + await tester.pump(const Duration(milliseconds: 50)); - expect(find.text('Activity'), findsOneWidget); - }, - variant: kPlatformVariant, - ); + expect(find.text('Activity'), findsOneWidget); + }, variant: kPlatformVariant); }); } diff --git a/test/widgets/adaptive_choice_picker_test.dart b/test/widgets/adaptive_choice_picker_test.dart index dfba25df4e..5cbaed910d 100644 --- a/test/widgets/adaptive_choice_picker_test.dart +++ b/test/widgets/adaptive_choice_picker_test.dart @@ -6,119 +6,103 @@ import 'package:lichess_mobile/src/widgets/adaptive_choice_picker.dart'; import '../test_helpers.dart'; -enum TestEnumLarge { - one, - two, - three, - four, - five, - six, - seven, - eight, - nine, - ten, - eleven -} +enum TestEnumLarge { one, two, three, four, five, six, seven, eight, nine, ten, eleven } enum TestEnumSmall { one, two, three } void main() { - testWidgets( - 'showChoicePicker call onSelectedItemChanged (large choices)', - (WidgetTester tester) async { - final List selectedItems = []; + testWidgets('showChoicePicker call onSelectedItemChanged (large choices)', ( + WidgetTester tester, + ) async { + final List selectedItems = []; - await tester.pumpWidget( - MaterialApp( - localizationsDelegates: AppLocalizations.localizationsDelegates, - home: Scaffold( - body: Builder( - builder: (context) { - return Center( - child: ElevatedButton( - child: const Text('Show picker'), - onPressed: () { - showChoicePicker( - context, - choices: TestEnumLarge.values, - selectedItem: TestEnumLarge.one, - labelBuilder: (choice) => Text(choice.name), - onSelectedItemChanged: (choice) { - selectedItems.add(choice); - }, - ); - }, - ), - ); - }, - ), + await tester.pumpWidget( + MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + home: Scaffold( + body: Builder( + builder: (context) { + return Center( + child: ElevatedButton( + child: const Text('Show picker'), + onPressed: () { + showChoicePicker( + context, + choices: TestEnumLarge.values, + selectedItem: TestEnumLarge.one, + labelBuilder: (choice) => Text(choice.name), + onSelectedItemChanged: (choice) { + selectedItems.add(choice); + }, + ); + }, + ), + ); + }, ), ), - ); + ), + ); - await tester.tap(find.text('Show picker')); - await tester.pumpAndSettle(); + await tester.tap(find.text('Show picker')); + await tester.pumpAndSettle(); - // with large choices (>= 6), on iOS the picker scrolls - if (debugDefaultTargetPlatformOverride == TargetPlatform.iOS) { - // scroll 2 items (2 * 40 height) - await tester.drag( - find.text('one'), - const Offset(0.0, -80.0), - warnIfMissed: false, - ); // has an IgnorePointer - expect(selectedItems, []); - await tester.pumpAndSettle(); // await for scroll ends - // only third item is selected as the scroll ends - expect(selectedItems, [TestEnumLarge.three]); - } else { - await tester.tap(find.text('three')); - expect(selectedItems, [TestEnumLarge.three]); - } - }, - variant: kPlatformVariant, - ); + // with large choices (>= 6), on iOS the picker scrolls + if (debugDefaultTargetPlatformOverride == TargetPlatform.iOS) { + // scroll 2 items (2 * 40 height) + await tester.drag( + find.text('one'), + const Offset(0.0, -80.0), + warnIfMissed: false, + ); // has an IgnorePointer + expect(selectedItems, []); + await tester.pumpAndSettle(); // await for scroll ends + // only third item is selected as the scroll ends + expect(selectedItems, [TestEnumLarge.three]); + } else { + await tester.tap(find.text('three')); + expect(selectedItems, [TestEnumLarge.three]); + } + }, variant: kPlatformVariant); - testWidgets( - 'showChoicePicker call onSelectedItemChanged (small choices)', - (WidgetTester tester) async { - final List selectedItems = []; + testWidgets('showChoicePicker call onSelectedItemChanged (small choices)', ( + WidgetTester tester, + ) async { + final List selectedItems = []; - await tester.pumpWidget( - MaterialApp( - localizationsDelegates: AppLocalizations.localizationsDelegates, - home: Scaffold( - body: Builder( - builder: (context) { - return Center( - child: ElevatedButton( - child: const Text('Show picker'), - onPressed: () { - showChoicePicker( - context, - choices: TestEnumSmall.values, - selectedItem: TestEnumSmall.one, - labelBuilder: (choice) => Text(choice.name), - onSelectedItemChanged: (choice) { - selectedItems.add(choice); - }, - ); - }, - ), - ); - }, - ), + await tester.pumpWidget( + MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + home: Scaffold( + body: Builder( + builder: (context) { + return Center( + child: ElevatedButton( + child: const Text('Show picker'), + onPressed: () { + showChoicePicker( + context, + choices: TestEnumSmall.values, + selectedItem: TestEnumSmall.one, + labelBuilder: (choice) => Text(choice.name), + onSelectedItemChanged: (choice) { + selectedItems.add(choice); + }, + ); + }, + ), + ); + }, ), ), - ); + ), + ); - await tester.tap(find.text('Show picker')); - await tester.pumpAndSettle(); + await tester.tap(find.text('Show picker')); + await tester.pumpAndSettle(); - // With small choices, on iOS the picker is an action sheet - await tester.tap(find.text('three')); - expect(selectedItems, [TestEnumSmall.three]); - }, - variant: kPlatformVariant, - ); + // With small choices, on iOS the picker is an action sheet + await tester.tap(find.text('three')); + expect(selectedItems, [TestEnumSmall.three]); + }, variant: kPlatformVariant); } diff --git a/test/widgets/board_table_test.dart b/test/widgets/board_table_test.dart index f49bf98090..3d260ede55 100644 --- a/test/widgets/board_table_test.dart +++ b/test/widgets/board_table_test.dart @@ -11,144 +11,120 @@ import '../test_helpers.dart'; import '../test_provider_scope.dart'; void main() { - testWidgets( - 'board background size should match board size on all surfaces', - (WidgetTester tester) async { - for (final surface in kTestSurfaces) { - final app = await makeTestProviderScope( - key: ValueKey(surface), - tester, - child: const MaterialApp( - home: BoardTable( - orientation: Side.white, - fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR', - topTable: Row( - mainAxisSize: MainAxisSize.max, - key: ValueKey('top_table'), - children: [ - Text('Top table'), - ], - ), - bottomTable: Row( - mainAxisSize: MainAxisSize.max, - key: ValueKey('bottom_table'), - children: [ - Text('Bottom table'), - ], - ), + testWidgets('board background size should match board size on all surfaces', ( + WidgetTester tester, + ) async { + for (final surface in kTestSurfaces) { + final app = await makeTestProviderScope( + key: ValueKey(surface), + tester, + child: const MaterialApp( + home: BoardTable( + orientation: Side.white, + fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR', + topTable: Row( + mainAxisSize: MainAxisSize.max, + key: ValueKey('top_table'), + children: [Text('Top table')], + ), + bottomTable: Row( + mainAxisSize: MainAxisSize.max, + key: ValueKey('bottom_table'), + children: [Text('Bottom table')], ), ), - surfaceSize: surface, - ); - await tester.pumpWidget(app); + ), + surfaceSize: surface, + ); + await tester.pumpWidget(app); - final backgroundSize = tester.getSize( - find.byType(SolidColorChessboardBackground), - ); + final backgroundSize = tester.getSize(find.byType(SolidColorChessboardBackground)); - expect( - backgroundSize.width, - backgroundSize.height, - reason: 'Board background size is square on $surface', - ); + expect( + backgroundSize.width, + backgroundSize.height, + reason: 'Board background size is square on $surface', + ); - final boardSize = tester.getSize(find.byType(Chessboard)); + final boardSize = tester.getSize(find.byType(Chessboard)); - expect( - boardSize.width, - boardSize.height, - reason: 'Board size is square on $surface', - ); + expect(boardSize.width, boardSize.height, reason: 'Board size is square on $surface'); - expect( - boardSize, - backgroundSize, - reason: 'Board size should match background size on $surface', - ); - } - }, - variant: kPlatformVariant, - ); + expect( + boardSize, + backgroundSize, + reason: 'Board size should match background size on $surface', + ); + } + }, variant: kPlatformVariant); - testWidgets( - 'board size and table side size should be harmonious on all surfaces', - (WidgetTester tester) async { - for (final surface in kTestSurfaces) { - final app = await makeTestProviderScope( - key: ValueKey(surface), - tester, - child: const MaterialApp( - home: BoardTable( - orientation: Side.white, - fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR', - topTable: Row( - mainAxisSize: MainAxisSize.max, - key: ValueKey('top_table'), - children: [ - Text('Top table'), - ], - ), - bottomTable: Row( - mainAxisSize: MainAxisSize.max, - key: ValueKey('bottom_table'), - children: [ - Text('Bottom table'), - ], - ), + testWidgets('board size and table side size should be harmonious on all surfaces', ( + WidgetTester tester, + ) async { + for (final surface in kTestSurfaces) { + final app = await makeTestProviderScope( + key: ValueKey(surface), + tester, + child: const MaterialApp( + home: BoardTable( + orientation: Side.white, + fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR', + topTable: Row( + mainAxisSize: MainAxisSize.max, + key: ValueKey('top_table'), + children: [Text('Top table')], + ), + bottomTable: Row( + mainAxisSize: MainAxisSize.max, + key: ValueKey('bottom_table'), + children: [Text('Bottom table')], ), ), - surfaceSize: surface, - ); - await tester.pumpWidget(app); + ), + surfaceSize: surface, + ); + await tester.pumpWidget(app); - final isPortrait = surface.aspectRatio < 1.0; - final isTablet = surface.shortestSide > 600; - final boardSize = tester.getSize(find.byType(Chessboard)); + final isPortrait = surface.aspectRatio < 1.0; + final isTablet = surface.shortestSide > 600; + final boardSize = tester.getSize(find.byType(Chessboard)); - if (isPortrait) { - final expectedBoardSize = - isTablet ? surface.width - 32.0 : surface.width; - expect( - boardSize, - Size(expectedBoardSize, expectedBoardSize), - reason: 'Board size should match surface width on $surface', - ); - } else { - final topTableSize = - tester.getSize(find.byKey(const ValueKey('top_table'))); - final bottomTableSize = - tester.getSize(find.byKey(const ValueKey('bottom_table'))); - final goldenBoardSize = (surface.longestSide / kGoldenRatio) - 32.0; - final defaultBoardSize = surface.shortestSide - 32.0; - final minBoardSize = min(goldenBoardSize, defaultBoardSize); - final maxBoardSize = max(goldenBoardSize, defaultBoardSize); - final minSideWidth = - min(surface.longestSide - goldenBoardSize - 16.0 * 3, 250.0); - expect( - boardSize.width, - greaterThanOrEqualTo(minBoardSize), - reason: 'Board size should be at least $minBoardSize on $surface', - ); - expect( - boardSize.width, - lessThanOrEqualTo(maxBoardSize), - reason: 'Board size should be at most $maxBoardSize on $surface', - ); - expect( - bottomTableSize.width, - greaterThanOrEqualTo(minSideWidth), - reason: - 'Bottom table width should be at least $minSideWidth on $surface', - ); - expect( - topTableSize.width, - greaterThanOrEqualTo(minSideWidth), - reason: - 'Top table width should be at least $minSideWidth on $surface', - ); - } + if (isPortrait) { + final expectedBoardSize = isTablet ? surface.width - 32.0 : surface.width; + expect( + boardSize, + Size(expectedBoardSize, expectedBoardSize), + reason: 'Board size should match surface width on $surface', + ); + } else { + final topTableSize = tester.getSize(find.byKey(const ValueKey('top_table'))); + final bottomTableSize = tester.getSize(find.byKey(const ValueKey('bottom_table'))); + final goldenBoardSize = (surface.longestSide / kGoldenRatio) - 32.0; + final defaultBoardSize = surface.shortestSide - 32.0; + final minBoardSize = min(goldenBoardSize, defaultBoardSize); + final maxBoardSize = max(goldenBoardSize, defaultBoardSize); + final minSideWidth = min(surface.longestSide - goldenBoardSize - 16.0 * 3, 250.0); + expect( + boardSize.width, + greaterThanOrEqualTo(minBoardSize), + reason: 'Board size should be at least $minBoardSize on $surface', + ); + expect( + boardSize.width, + lessThanOrEqualTo(maxBoardSize), + reason: 'Board size should be at most $maxBoardSize on $surface', + ); + expect( + bottomTableSize.width, + greaterThanOrEqualTo(minSideWidth), + reason: 'Bottom table width should be at least $minSideWidth on $surface', + ); + expect( + topTableSize.width, + greaterThanOrEqualTo(minSideWidth), + reason: 'Top table width should be at least $minSideWidth on $surface', + ); } - }, - variant: kPlatformVariant, - ); + } + }, variant: kPlatformVariant); } diff --git a/test/widgets/clock_test.dart b/test/widgets/clock_test.dart index 73e20078f6..555ca56d0d 100644 --- a/test/widgets/clock_test.dart +++ b/test/widgets/clock_test.dart @@ -5,26 +5,17 @@ import 'package:lichess_mobile/src/widgets/clock.dart'; void main() { group('Clock', () { - testWidgets('shows milliseconds when time < 1s and active is false', - (WidgetTester tester) async { + testWidgets('shows milliseconds when time < 1s and active is false', ( + WidgetTester tester, + ) async { await tester.pumpWidget( - const MaterialApp( - home: Clock( - timeLeft: Duration(seconds: 1), - active: true, - ), - ), + const MaterialApp(home: Clock(timeLeft: Duration(seconds: 1), active: true)), ); expect(find.text('0:01.0', findRichText: true), findsOneWidget); await tester.pumpWidget( - const MaterialApp( - home: Clock( - timeLeft: Duration(milliseconds: 988), - active: false, - ), - ), + const MaterialApp(home: Clock(timeLeft: Duration(milliseconds: 988), active: false)), duration: const Duration(milliseconds: 1000), ); @@ -77,8 +68,7 @@ void main() { expect(find.text('0:00.0'), findsOneWidget); }); - testWidgets('update time by changing widget configuration', - (WidgetTester tester) async { + testWidgets('update time by changing widget configuration', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: CountdownClockBuilder( @@ -115,8 +105,7 @@ void main() { expect(find.text('0:00.0'), findsOneWidget); }); - testWidgets('do not update if clockUpdatedAt is same', - (WidgetTester tester) async { + testWidgets('do not update if clockUpdatedAt is same', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: CountdownClockBuilder( From 131eaecfcd730ac6ae53cb6ce245880e1f6d7bd7 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Sun, 15 Dec 2024 20:36:21 +0100 Subject: [PATCH 931/979] Remove trailing commas rule --- analysis_options.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/analysis_options.yaml b/analysis_options.yaml index 07d7adc1b8..3934e2b4b4 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -31,6 +31,7 @@ formatter: linter: rules: + require_trailing_commas: false prefer_single_quotes: true always_use_package_imports: false avoid_redundant_argument_values: false From ed04b1e8b5699f76db347e071756d74fb9a7235f Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Sun, 15 Dec 2024 20:39:53 +0100 Subject: [PATCH 932/979] Fix lint --- lib/src/view/play/create_challenge_screen.dart | 2 +- lib/src/view/tools/load_position_screen.dart | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/view/play/create_challenge_screen.dart b/lib/src/view/play/create_challenge_screen.dart index 932228d876..62319a0df5 100644 --- a/lib/src/view/play/create_challenge_screen.dart +++ b/lib/src/view/play/create_challenge_screen.dart @@ -416,7 +416,7 @@ class _ChallengeBodyState extends ConsumerState<_ChallengeBody> { try { Chess.fromSetup(Setup.parseFen(data.text!.trim())); _controller.text = data.text!; - } catch (_, __) { + } catch (_) { if (mounted) { showPlatformSnackbar(context, context.l10n.invalidFen, type: SnackBarType.error); } diff --git a/lib/src/view/tools/load_position_screen.dart b/lib/src/view/tools/load_position_screen.dart index 3b6e9cbe78..9f753a7981 100644 --- a/lib/src/view/tools/load_position_screen.dart +++ b/lib/src/view/tools/load_position_screen.dart @@ -136,7 +136,7 @@ class _BodyState extends State<_Body> { ), ), ); - } catch (_, __) {} + } catch (_) {} // try to parse as PGN try { @@ -169,7 +169,7 @@ class _BodyState extends State<_Body> { initialMoveCursor: mainlineMoves.isEmpty ? 0 : 1, ), ); - } catch (_, __) {} + } catch (_) {} return null; } From dfadce4f2d4783bda70e862e1780a1a4299259dc Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 16 Dec 2024 10:05:59 +0100 Subject: [PATCH 933/979] Don't display round for past broadcast --- lib/src/utils/image.dart | 2 +- .../view/broadcast/broadcast_list_screen.dart | 72 ++++++++++--------- 2 files changed, 38 insertions(+), 36 deletions(-) diff --git a/lib/src/utils/image.dart b/lib/src/utils/image.dart index 690f1dd4bb..9208bb5e1e 100644 --- a/lib/src/utils/image.dart +++ b/lib/src/utils/image.dart @@ -79,7 +79,7 @@ class ImageColorWorker { final (int id, Uint32List image) = message as (int, Uint32List); try { // final stopwatch0 = Stopwatch()..start(); - final QuantizerResult quantizerResult = await QuantizerCelebi().quantize(image, 32); + final quantizerResult = await QuantizerCelebi().quantize(image, 32); final Map colorToCount = quantizerResult.colorToCount.map( (int key, int value) => MapEntry(_getArgbFromAbgr(key), value), ); diff --git a/lib/src/view/broadcast/broadcast_list_screen.dart b/lib/src/view/broadcast/broadcast_list_screen.dart index ea46fd6082..81d09d934b 100644 --- a/lib/src/view/broadcast/broadcast_list_screen.dart +++ b/lib/src/view/broadcast/broadcast_list_screen.dart @@ -444,13 +444,13 @@ class _BroadcastCartState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (widget.broadcast.round.startsAt != null) - Padding( - padding: kBroadcastGridItemContentPadding, - child: Row( - crossAxisAlignment: CrossAxisAlignment.baseline, - textBaseline: TextBaseline.alphabetic, - children: [ + Padding( + padding: kBroadcastGridItemContentPadding, + child: Row( + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + if (!widget.broadcast.isFinished) ...[ Text( widget.broadcast.round.name, style: TextStyle( @@ -462,6 +462,8 @@ class _BroadcastCartState extends State { maxLines: 1, ), const SizedBox(width: 5.0), + ], + if (widget.broadcast.round.startsAt != null) Expanded( child: Text( relativeDate(widget.broadcast.round.startsAt!), @@ -470,13 +472,28 @@ class _BroadcastCartState extends State { maxLines: 1, ), ), - if (widget.broadcast.isLive) ...[ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.circle, - size: 16, + if (widget.broadcast.isLive) ...[ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.circle, + size: 16, + color: liveColor, + shadows: const [ + Shadow( + color: Colors.black54, + offset: Offset(0, 1), + blurRadius: 2, + ), + ], + ), + const SizedBox(width: 4.0), + Text( + 'LIVE', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, color: liveColor, shadows: const [ Shadow( @@ -486,29 +503,14 @@ class _BroadcastCartState extends State { ), ], ), - const SizedBox(width: 4.0), - Text( - 'LIVE', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - color: liveColor, - shadows: const [ - Shadow( - color: Colors.black54, - offset: Offset(0, 1), - blurRadius: 2, - ), - ], - ), - overflow: TextOverflow.ellipsis, - ), - ], - ), - ], + overflow: TextOverflow.ellipsis, + ), + ], + ), ], - ), + ], ), + ), Padding( padding: kBroadcastGridItemContentPadding.add( const EdgeInsets.symmetric(vertical: 3.0), From d7268c195322d9695c917d56c24fb9ded5829a1f Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Mon, 16 Dec 2024 18:28:37 +0100 Subject: [PATCH 934/979] Use PNG images instead of SVG for FIDE federation flags in order to solve performance issues --- assets/images/fide-fed/4.0x/AFG.png | Bin 0 -> 2738 bytes assets/images/fide-fed/4.0x/AHO.png | Bin 0 -> 338 bytes assets/images/fide-fed/4.0x/ALB.png | Bin 0 -> 3423 bytes assets/images/fide-fed/4.0x/ALG.png | Bin 0 -> 1701 bytes assets/images/fide-fed/4.0x/AND.png | Bin 0 -> 2449 bytes assets/images/fide-fed/4.0x/ANG.png | Bin 0 -> 1696 bytes assets/images/fide-fed/4.0x/ANT.png | Bin 0 -> 2480 bytes assets/images/fide-fed/4.0x/ARG.png | Bin 0 -> 1610 bytes assets/images/fide-fed/4.0x/ARM.png | Bin 0 -> 308 bytes assets/images/fide-fed/4.0x/ARU.png | Bin 0 -> 1198 bytes assets/images/fide-fed/4.0x/AUS.png | Bin 0 -> 4145 bytes assets/images/fide-fed/4.0x/AUT.png | Bin 0 -> 307 bytes assets/images/fide-fed/4.0x/AZE.png | Bin 0 -> 1269 bytes assets/images/fide-fed/4.0x/BAH.png | Bin 0 -> 1471 bytes assets/images/fide-fed/4.0x/BAN.png | Bin 0 -> 1300 bytes assets/images/fide-fed/4.0x/BAR.png | Bin 0 -> 1313 bytes assets/images/fide-fed/4.0x/BDI.png | Bin 0 -> 2729 bytes assets/images/fide-fed/4.0x/BEL.png | Bin 0 -> 325 bytes assets/images/fide-fed/4.0x/BER.png | Bin 0 -> 2801 bytes assets/images/fide-fed/4.0x/BHU.png | Bin 0 -> 5690 bytes assets/images/fide-fed/4.0x/BIH.png | Bin 0 -> 2834 bytes assets/images/fide-fed/4.0x/BIZ.png | Bin 0 -> 3510 bytes assets/images/fide-fed/4.0x/BLR.png | Bin 0 -> 4311 bytes assets/images/fide-fed/4.0x/BOL.png | Bin 0 -> 1679 bytes assets/images/fide-fed/4.0x/BOT.png | Bin 0 -> 326 bytes assets/images/fide-fed/4.0x/BRA.png | Bin 0 -> 3827 bytes assets/images/fide-fed/4.0x/BRN.png | Bin 0 -> 490 bytes assets/images/fide-fed/4.0x/BRU.png | Bin 0 -> 4036 bytes assets/images/fide-fed/4.0x/BUL.png | Bin 0 -> 315 bytes assets/images/fide-fed/4.0x/BUR.png | Bin 0 -> 1290 bytes assets/images/fide-fed/4.0x/CAF.png | Bin 0 -> 839 bytes assets/images/fide-fed/4.0x/CAM.png | Bin 0 -> 2532 bytes assets/images/fide-fed/4.0x/CAN.png | Bin 0 -> 1269 bytes assets/images/fide-fed/4.0x/CAY.png | Bin 0 -> 2479 bytes assets/images/fide-fed/4.0x/CHA.png | Bin 0 -> 295 bytes assets/images/fide-fed/4.0x/CHI.png | Bin 0 -> 839 bytes assets/images/fide-fed/4.0x/CHN.png | Bin 0 -> 1334 bytes assets/images/fide-fed/4.0x/CIV.png | Bin 0 -> 326 bytes assets/images/fide-fed/4.0x/CMR.png | Bin 0 -> 763 bytes assets/images/fide-fed/4.0x/COD.png | Bin 0 -> 1589 bytes assets/images/fide-fed/4.0x/COL.png | Bin 0 -> 315 bytes assets/images/fide-fed/4.0x/COM.png | Bin 0 -> 2382 bytes assets/images/fide-fed/4.0x/CPV.png | Bin 0 -> 2064 bytes assets/images/fide-fed/4.0x/CRC.png | Bin 0 -> 340 bytes assets/images/fide-fed/4.0x/CRO.png | Bin 0 -> 2286 bytes assets/images/fide-fed/4.0x/CUB.png | Bin 0 -> 1938 bytes assets/images/fide-fed/4.0x/CYP.png | Bin 0 -> 2558 bytes assets/images/fide-fed/4.0x/CZE.png | Bin 0 -> 963 bytes assets/images/fide-fed/4.0x/DEN.png | Bin 0 -> 399 bytes assets/images/fide-fed/4.0x/DJI.png | Bin 0 -> 1266 bytes assets/images/fide-fed/4.0x/DMA.png | Bin 0 -> 1606 bytes assets/images/fide-fed/4.0x/DOM.png | Bin 0 -> 1165 bytes assets/images/fide-fed/4.0x/ECU.png | Bin 0 -> 5637 bytes assets/images/fide-fed/4.0x/EGY.png | Bin 0 -> 1409 bytes assets/images/fide-fed/4.0x/ENG.png | Bin 0 -> 397 bytes assets/images/fide-fed/4.0x/ERI.png | Bin 0 -> 4332 bytes assets/images/fide-fed/4.0x/ESA.png | Bin 0 -> 1632 bytes assets/images/fide-fed/4.0x/ESP.png | Bin 0 -> 2224 bytes assets/images/fide-fed/4.0x/EST.png | Bin 0 -> 326 bytes assets/images/fide-fed/4.0x/ETH.png | Bin 0 -> 2353 bytes assets/images/fide-fed/4.0x/FAI.png | Bin 0 -> 546 bytes assets/images/fide-fed/4.0x/FID.png | Bin 0 -> 4853 bytes assets/images/fide-fed/4.0x/FIJ.png | Bin 0 -> 4361 bytes assets/images/fide-fed/4.0x/FIN.png | Bin 0 -> 477 bytes assets/images/fide-fed/4.0x/FRA.png | Bin 0 -> 325 bytes assets/images/fide-fed/4.0x/GAB.png | Bin 0 -> 315 bytes assets/images/fide-fed/4.0x/GAM.png | Bin 0 -> 348 bytes assets/images/fide-fed/4.0x/GCI.png | Bin 0 -> 897 bytes assets/images/fide-fed/4.0x/GEO.png | Bin 0 -> 1097 bytes assets/images/fide-fed/4.0x/GEQ.png | Bin 0 -> 1641 bytes assets/images/fide-fed/4.0x/GER.png | Bin 0 -> 311 bytes assets/images/fide-fed/4.0x/GHA.png | Bin 0 -> 896 bytes assets/images/fide-fed/4.0x/GRE.png | Bin 0 -> 507 bytes assets/images/fide-fed/4.0x/GRN.png | Bin 0 -> 2802 bytes assets/images/fide-fed/4.0x/GUA.png | Bin 0 -> 1524 bytes assets/images/fide-fed/4.0x/GUM.png | Bin 0 -> 4073 bytes assets/images/fide-fed/4.0x/GUY.png | Bin 0 -> 3160 bytes assets/images/fide-fed/4.0x/HAI.png | Bin 0 -> 1552 bytes assets/images/fide-fed/4.0x/HKG.png | Bin 0 -> 2559 bytes assets/images/fide-fed/4.0x/HON.png | Bin 0 -> 905 bytes assets/images/fide-fed/4.0x/HUN.png | Bin 0 -> 315 bytes assets/images/fide-fed/4.0x/INA.png | Bin 0 -> 308 bytes assets/images/fide-fed/4.0x/IND.png | Bin 0 -> 1514 bytes assets/images/fide-fed/4.0x/IOM.png | Bin 0 -> 3658 bytes assets/images/fide-fed/4.0x/IRI.png | Bin 0 -> 3740 bytes assets/images/fide-fed/4.0x/IRL.png | Bin 0 -> 310 bytes assets/images/fide-fed/4.0x/IRQ.png | Bin 0 -> 1225 bytes assets/images/fide-fed/4.0x/ISL.png | Bin 0 -> 466 bytes assets/images/fide-fed/4.0x/ISR.png | Bin 0 -> 1777 bytes assets/images/fide-fed/4.0x/ISV.png | Bin 0 -> 7093 bytes assets/images/fide-fed/4.0x/ITA.png | Bin 0 -> 313 bytes assets/images/fide-fed/4.0x/IVB.png | Bin 0 -> 5642 bytes assets/images/fide-fed/4.0x/JAM.png | Bin 0 -> 1077 bytes assets/images/fide-fed/4.0x/JCI.png | Bin 0 -> 2875 bytes assets/images/fide-fed/4.0x/JOR.png | Bin 0 -> 845 bytes assets/images/fide-fed/4.0x/JPN.png | Bin 0 -> 1339 bytes assets/images/fide-fed/4.0x/KAZ.png | Bin 0 -> 7084 bytes assets/images/fide-fed/4.0x/KEN.png | Bin 0 -> 2150 bytes assets/images/fide-fed/4.0x/KGZ.png | Bin 0 -> 4327 bytes assets/images/fide-fed/4.0x/KOR.png | Bin 0 -> 4445 bytes assets/images/fide-fed/4.0x/KOS.png | Bin 0 -> 2468 bytes assets/images/fide-fed/4.0x/KSA.png | Bin 0 -> 3999 bytes assets/images/fide-fed/4.0x/KUW.png | Bin 0 -> 721 bytes assets/images/fide-fed/4.0x/LAO.png | Bin 0 -> 1087 bytes assets/images/fide-fed/4.0x/LAT.png | Bin 0 -> 337 bytes assets/images/fide-fed/4.0x/LBA.png | Bin 0 -> 933 bytes assets/images/fide-fed/4.0x/LBN.png | Bin 0 -> 2424 bytes assets/images/fide-fed/4.0x/LBR.png | Bin 0 -> 1075 bytes assets/images/fide-fed/4.0x/LCA.png | Bin 0 -> 2083 bytes assets/images/fide-fed/4.0x/LES.png | Bin 0 -> 1391 bytes assets/images/fide-fed/4.0x/LIE.png | Bin 0 -> 1661 bytes assets/images/fide-fed/4.0x/LTU.png | Bin 0 -> 331 bytes assets/images/fide-fed/4.0x/LUX.png | Bin 0 -> 316 bytes assets/images/fide-fed/4.0x/MAC.png | Bin 0 -> 2579 bytes assets/images/fide-fed/4.0x/MAD.png | Bin 0 -> 324 bytes assets/images/fide-fed/4.0x/MAR.png | Bin 0 -> 1016 bytes assets/images/fide-fed/4.0x/MAS.png | Bin 0 -> 2213 bytes assets/images/fide-fed/4.0x/MAW.png | Bin 0 -> 2272 bytes assets/images/fide-fed/4.0x/MDA.png | Bin 0 -> 4177 bytes assets/images/fide-fed/4.0x/MDV.png | Bin 0 -> 938 bytes assets/images/fide-fed/4.0x/MEX.png | Bin 0 -> 1827 bytes assets/images/fide-fed/4.0x/MGL.png | Bin 0 -> 1578 bytes assets/images/fide-fed/4.0x/MKD.png | Bin 0 -> 2782 bytes assets/images/fide-fed/4.0x/MLI.png | Bin 0 -> 320 bytes assets/images/fide-fed/4.0x/MLT.png | Bin 0 -> 1295 bytes assets/images/fide-fed/4.0x/MNC.png | Bin 0 -> 309 bytes assets/images/fide-fed/4.0x/MNE.png | Bin 0 -> 4220 bytes assets/images/fide-fed/4.0x/MOZ.png | Bin 0 -> 1826 bytes assets/images/fide-fed/4.0x/MRI.png | Bin 0 -> 324 bytes assets/images/fide-fed/4.0x/MTN.png | Bin 0 -> 1761 bytes assets/images/fide-fed/4.0x/MYA.png | Bin 0 -> 1497 bytes assets/images/fide-fed/4.0x/NAM.png | Bin 0 -> 3639 bytes assets/images/fide-fed/4.0x/NCA.png | Bin 0 -> 1418 bytes assets/images/fide-fed/4.0x/NED.png | Bin 0 -> 338 bytes assets/images/fide-fed/4.0x/NEP.png | Bin 0 -> 2956 bytes assets/images/fide-fed/4.0x/NGR.png | Bin 0 -> 328 bytes assets/images/fide-fed/4.0x/NIG.png | Bin 0 -> 932 bytes assets/images/fide-fed/4.0x/NOR.png | Bin 0 -> 435 bytes assets/images/fide-fed/4.0x/NRU.png | Bin 0 -> 1378 bytes assets/images/fide-fed/4.0x/NZL.png | Bin 0 -> 3485 bytes assets/images/fide-fed/4.0x/OMA.png | Bin 0 -> 1293 bytes assets/images/fide-fed/4.0x/PAK.png | Bin 0 -> 1503 bytes assets/images/fide-fed/4.0x/PAN.png | Bin 0 -> 1395 bytes assets/images/fide-fed/4.0x/PAR.png | Bin 0 -> 1471 bytes assets/images/fide-fed/4.0x/PER.png | Bin 0 -> 2345 bytes assets/images/fide-fed/4.0x/PHI.png | Bin 0 -> 3008 bytes assets/images/fide-fed/4.0x/PLE.png | Bin 0 -> 518 bytes assets/images/fide-fed/4.0x/PLW.png | Bin 0 -> 1283 bytes assets/images/fide-fed/4.0x/PNG.png | Bin 0 -> 2370 bytes assets/images/fide-fed/4.0x/POL.png | Bin 0 -> 309 bytes assets/images/fide-fed/4.0x/POR.png | Bin 0 -> 2686 bytes assets/images/fide-fed/4.0x/PUR.png | Bin 0 -> 1894 bytes assets/images/fide-fed/4.0x/QAT.png | Bin 0 -> 747 bytes assets/images/fide-fed/4.0x/ROU.png | Bin 0 -> 331 bytes assets/images/fide-fed/4.0x/RSA.png | Bin 0 -> 2469 bytes assets/images/fide-fed/4.0x/RUS.png | Bin 0 -> 4311 bytes assets/images/fide-fed/4.0x/RWA.png | Bin 0 -> 1635 bytes assets/images/fide-fed/4.0x/SCO.png | Bin 0 -> 1193 bytes assets/images/fide-fed/4.0x/SEN.png | Bin 0 -> 1041 bytes assets/images/fide-fed/4.0x/SEY.png | Bin 0 -> 2175 bytes assets/images/fide-fed/4.0x/SGP.png | Bin 0 -> 1533 bytes assets/images/fide-fed/4.0x/SKN.png | Bin 0 -> 1852 bytes assets/images/fide-fed/4.0x/SLE.png | Bin 0 -> 319 bytes assets/images/fide-fed/4.0x/SLO.png | Bin 0 -> 1501 bytes assets/images/fide-fed/4.0x/SMR.png | Bin 0 -> 4285 bytes assets/images/fide-fed/4.0x/SOL.png | Bin 0 -> 2229 bytes assets/images/fide-fed/4.0x/SOM.png | Bin 0 -> 1040 bytes assets/images/fide-fed/4.0x/SRB.png | Bin 0 -> 4048 bytes assets/images/fide-fed/4.0x/SRI.png | Bin 0 -> 3240 bytes assets/images/fide-fed/4.0x/SSD.png | Bin 0 -> 1852 bytes assets/images/fide-fed/4.0x/STP.png | Bin 0 -> 1396 bytes assets/images/fide-fed/4.0x/SUD.png | Bin 0 -> 734 bytes assets/images/fide-fed/4.0x/SUI.png | Bin 0 -> 484 bytes assets/images/fide-fed/4.0x/SUR.png | Bin 0 -> 966 bytes assets/images/fide-fed/4.0x/SVK.png | Bin 0 -> 1917 bytes assets/images/fide-fed/4.0x/SWE.png | Bin 0 -> 464 bytes assets/images/fide-fed/4.0x/SWZ.png | Bin 0 -> 3369 bytes assets/images/fide-fed/4.0x/SYR.png | Bin 0 -> 1010 bytes assets/images/fide-fed/4.0x/TAN.png | Bin 0 -> 909 bytes assets/images/fide-fed/4.0x/THA.png | Bin 0 -> 346 bytes assets/images/fide-fed/4.0x/TJK.png | Bin 0 -> 1623 bytes assets/images/fide-fed/4.0x/TKM.png | Bin 0 -> 4836 bytes assets/images/fide-fed/4.0x/TLS.png | Bin 0 -> 2109 bytes assets/images/fide-fed/4.0x/TOG.png | Bin 0 -> 940 bytes assets/images/fide-fed/4.0x/TPE.png | Bin 0 -> 4819 bytes assets/images/fide-fed/4.0x/TTO.png | Bin 0 -> 3150 bytes assets/images/fide-fed/4.0x/TUN.png | Bin 0 -> 2015 bytes assets/images/fide-fed/4.0x/TUR.png | Bin 0 -> 1878 bytes assets/images/fide-fed/4.0x/UAE.png | Bin 0 -> 318 bytes assets/images/fide-fed/4.0x/UGA.png | Bin 0 -> 1574 bytes assets/images/fide-fed/4.0x/UKR.png | Bin 0 -> 308 bytes assets/images/fide-fed/4.0x/URU.png | Bin 0 -> 2364 bytes assets/images/fide-fed/4.0x/USA.png | Bin 0 -> 4163 bytes assets/images/fide-fed/4.0x/UZB.png | Bin 0 -> 1382 bytes assets/images/fide-fed/4.0x/VAN.png | Bin 0 -> 2343 bytes assets/images/fide-fed/4.0x/VEN.png | Bin 0 -> 1382 bytes assets/images/fide-fed/4.0x/VIE.png | Bin 0 -> 1177 bytes assets/images/fide-fed/4.0x/VIN.png | Bin 0 -> 1294 bytes assets/images/fide-fed/4.0x/W.png | Bin 0 -> 4311 bytes assets/images/fide-fed/4.0x/WLS.png | Bin 0 -> 7320 bytes assets/images/fide-fed/4.0x/YEM.png | Bin 0 -> 321 bytes assets/images/fide-fed/4.0x/ZAM.png | Bin 0 -> 1351 bytes assets/images/fide-fed/4.0x/ZIM.png | Bin 0 -> 2850 bytes lib/src/utils/lichess_assets.dart | 4 -- .../broadcast/broadcast_player_widget.dart | 9 +--- pubspec.lock | 40 ------------------ pubspec.yaml | 2 +- 207 files changed, 2 insertions(+), 53 deletions(-) create mode 100644 assets/images/fide-fed/4.0x/AFG.png create mode 100644 assets/images/fide-fed/4.0x/AHO.png create mode 100644 assets/images/fide-fed/4.0x/ALB.png create mode 100644 assets/images/fide-fed/4.0x/ALG.png create mode 100644 assets/images/fide-fed/4.0x/AND.png create mode 100644 assets/images/fide-fed/4.0x/ANG.png create mode 100644 assets/images/fide-fed/4.0x/ANT.png create mode 100644 assets/images/fide-fed/4.0x/ARG.png create mode 100644 assets/images/fide-fed/4.0x/ARM.png create mode 100644 assets/images/fide-fed/4.0x/ARU.png create mode 100644 assets/images/fide-fed/4.0x/AUS.png create mode 100644 assets/images/fide-fed/4.0x/AUT.png create mode 100644 assets/images/fide-fed/4.0x/AZE.png create mode 100644 assets/images/fide-fed/4.0x/BAH.png create mode 100644 assets/images/fide-fed/4.0x/BAN.png create mode 100644 assets/images/fide-fed/4.0x/BAR.png create mode 100644 assets/images/fide-fed/4.0x/BDI.png create mode 100644 assets/images/fide-fed/4.0x/BEL.png create mode 100644 assets/images/fide-fed/4.0x/BER.png create mode 100644 assets/images/fide-fed/4.0x/BHU.png create mode 100644 assets/images/fide-fed/4.0x/BIH.png create mode 100644 assets/images/fide-fed/4.0x/BIZ.png create mode 100644 assets/images/fide-fed/4.0x/BLR.png create mode 100644 assets/images/fide-fed/4.0x/BOL.png create mode 100644 assets/images/fide-fed/4.0x/BOT.png create mode 100644 assets/images/fide-fed/4.0x/BRA.png create mode 100644 assets/images/fide-fed/4.0x/BRN.png create mode 100644 assets/images/fide-fed/4.0x/BRU.png create mode 100644 assets/images/fide-fed/4.0x/BUL.png create mode 100644 assets/images/fide-fed/4.0x/BUR.png create mode 100644 assets/images/fide-fed/4.0x/CAF.png create mode 100644 assets/images/fide-fed/4.0x/CAM.png create mode 100644 assets/images/fide-fed/4.0x/CAN.png create mode 100644 assets/images/fide-fed/4.0x/CAY.png create mode 100644 assets/images/fide-fed/4.0x/CHA.png create mode 100644 assets/images/fide-fed/4.0x/CHI.png create mode 100644 assets/images/fide-fed/4.0x/CHN.png create mode 100644 assets/images/fide-fed/4.0x/CIV.png create mode 100644 assets/images/fide-fed/4.0x/CMR.png create mode 100644 assets/images/fide-fed/4.0x/COD.png create mode 100644 assets/images/fide-fed/4.0x/COL.png create mode 100644 assets/images/fide-fed/4.0x/COM.png create mode 100644 assets/images/fide-fed/4.0x/CPV.png create mode 100644 assets/images/fide-fed/4.0x/CRC.png create mode 100644 assets/images/fide-fed/4.0x/CRO.png create mode 100644 assets/images/fide-fed/4.0x/CUB.png create mode 100644 assets/images/fide-fed/4.0x/CYP.png create mode 100644 assets/images/fide-fed/4.0x/CZE.png create mode 100644 assets/images/fide-fed/4.0x/DEN.png create mode 100644 assets/images/fide-fed/4.0x/DJI.png create mode 100644 assets/images/fide-fed/4.0x/DMA.png create mode 100644 assets/images/fide-fed/4.0x/DOM.png create mode 100644 assets/images/fide-fed/4.0x/ECU.png create mode 100644 assets/images/fide-fed/4.0x/EGY.png create mode 100644 assets/images/fide-fed/4.0x/ENG.png create mode 100644 assets/images/fide-fed/4.0x/ERI.png create mode 100644 assets/images/fide-fed/4.0x/ESA.png create mode 100644 assets/images/fide-fed/4.0x/ESP.png create mode 100644 assets/images/fide-fed/4.0x/EST.png create mode 100644 assets/images/fide-fed/4.0x/ETH.png create mode 100644 assets/images/fide-fed/4.0x/FAI.png create mode 100644 assets/images/fide-fed/4.0x/FID.png create mode 100644 assets/images/fide-fed/4.0x/FIJ.png create mode 100644 assets/images/fide-fed/4.0x/FIN.png create mode 100644 assets/images/fide-fed/4.0x/FRA.png create mode 100644 assets/images/fide-fed/4.0x/GAB.png create mode 100644 assets/images/fide-fed/4.0x/GAM.png create mode 100644 assets/images/fide-fed/4.0x/GCI.png create mode 100644 assets/images/fide-fed/4.0x/GEO.png create mode 100644 assets/images/fide-fed/4.0x/GEQ.png create mode 100644 assets/images/fide-fed/4.0x/GER.png create mode 100644 assets/images/fide-fed/4.0x/GHA.png create mode 100644 assets/images/fide-fed/4.0x/GRE.png create mode 100644 assets/images/fide-fed/4.0x/GRN.png create mode 100644 assets/images/fide-fed/4.0x/GUA.png create mode 100644 assets/images/fide-fed/4.0x/GUM.png create mode 100644 assets/images/fide-fed/4.0x/GUY.png create mode 100644 assets/images/fide-fed/4.0x/HAI.png create mode 100644 assets/images/fide-fed/4.0x/HKG.png create mode 100644 assets/images/fide-fed/4.0x/HON.png create mode 100644 assets/images/fide-fed/4.0x/HUN.png create mode 100644 assets/images/fide-fed/4.0x/INA.png create mode 100644 assets/images/fide-fed/4.0x/IND.png create mode 100644 assets/images/fide-fed/4.0x/IOM.png create mode 100644 assets/images/fide-fed/4.0x/IRI.png create mode 100644 assets/images/fide-fed/4.0x/IRL.png create mode 100644 assets/images/fide-fed/4.0x/IRQ.png create mode 100644 assets/images/fide-fed/4.0x/ISL.png create mode 100644 assets/images/fide-fed/4.0x/ISR.png create mode 100644 assets/images/fide-fed/4.0x/ISV.png create mode 100644 assets/images/fide-fed/4.0x/ITA.png create mode 100644 assets/images/fide-fed/4.0x/IVB.png create mode 100644 assets/images/fide-fed/4.0x/JAM.png create mode 100644 assets/images/fide-fed/4.0x/JCI.png create mode 100644 assets/images/fide-fed/4.0x/JOR.png create mode 100644 assets/images/fide-fed/4.0x/JPN.png create mode 100644 assets/images/fide-fed/4.0x/KAZ.png create mode 100644 assets/images/fide-fed/4.0x/KEN.png create mode 100644 assets/images/fide-fed/4.0x/KGZ.png create mode 100644 assets/images/fide-fed/4.0x/KOR.png create mode 100644 assets/images/fide-fed/4.0x/KOS.png create mode 100644 assets/images/fide-fed/4.0x/KSA.png create mode 100644 assets/images/fide-fed/4.0x/KUW.png create mode 100644 assets/images/fide-fed/4.0x/LAO.png create mode 100644 assets/images/fide-fed/4.0x/LAT.png create mode 100644 assets/images/fide-fed/4.0x/LBA.png create mode 100644 assets/images/fide-fed/4.0x/LBN.png create mode 100644 assets/images/fide-fed/4.0x/LBR.png create mode 100644 assets/images/fide-fed/4.0x/LCA.png create mode 100644 assets/images/fide-fed/4.0x/LES.png create mode 100644 assets/images/fide-fed/4.0x/LIE.png create mode 100644 assets/images/fide-fed/4.0x/LTU.png create mode 100644 assets/images/fide-fed/4.0x/LUX.png create mode 100644 assets/images/fide-fed/4.0x/MAC.png create mode 100644 assets/images/fide-fed/4.0x/MAD.png create mode 100644 assets/images/fide-fed/4.0x/MAR.png create mode 100644 assets/images/fide-fed/4.0x/MAS.png create mode 100644 assets/images/fide-fed/4.0x/MAW.png create mode 100644 assets/images/fide-fed/4.0x/MDA.png create mode 100644 assets/images/fide-fed/4.0x/MDV.png create mode 100644 assets/images/fide-fed/4.0x/MEX.png create mode 100644 assets/images/fide-fed/4.0x/MGL.png create mode 100644 assets/images/fide-fed/4.0x/MKD.png create mode 100644 assets/images/fide-fed/4.0x/MLI.png create mode 100644 assets/images/fide-fed/4.0x/MLT.png create mode 100644 assets/images/fide-fed/4.0x/MNC.png create mode 100644 assets/images/fide-fed/4.0x/MNE.png create mode 100644 assets/images/fide-fed/4.0x/MOZ.png create mode 100644 assets/images/fide-fed/4.0x/MRI.png create mode 100644 assets/images/fide-fed/4.0x/MTN.png create mode 100644 assets/images/fide-fed/4.0x/MYA.png create mode 100644 assets/images/fide-fed/4.0x/NAM.png create mode 100644 assets/images/fide-fed/4.0x/NCA.png create mode 100644 assets/images/fide-fed/4.0x/NED.png create mode 100644 assets/images/fide-fed/4.0x/NEP.png create mode 100644 assets/images/fide-fed/4.0x/NGR.png create mode 100644 assets/images/fide-fed/4.0x/NIG.png create mode 100644 assets/images/fide-fed/4.0x/NOR.png create mode 100644 assets/images/fide-fed/4.0x/NRU.png create mode 100644 assets/images/fide-fed/4.0x/NZL.png create mode 100644 assets/images/fide-fed/4.0x/OMA.png create mode 100644 assets/images/fide-fed/4.0x/PAK.png create mode 100644 assets/images/fide-fed/4.0x/PAN.png create mode 100644 assets/images/fide-fed/4.0x/PAR.png create mode 100644 assets/images/fide-fed/4.0x/PER.png create mode 100644 assets/images/fide-fed/4.0x/PHI.png create mode 100644 assets/images/fide-fed/4.0x/PLE.png create mode 100644 assets/images/fide-fed/4.0x/PLW.png create mode 100644 assets/images/fide-fed/4.0x/PNG.png create mode 100644 assets/images/fide-fed/4.0x/POL.png create mode 100644 assets/images/fide-fed/4.0x/POR.png create mode 100644 assets/images/fide-fed/4.0x/PUR.png create mode 100644 assets/images/fide-fed/4.0x/QAT.png create mode 100644 assets/images/fide-fed/4.0x/ROU.png create mode 100644 assets/images/fide-fed/4.0x/RSA.png create mode 100644 assets/images/fide-fed/4.0x/RUS.png create mode 100644 assets/images/fide-fed/4.0x/RWA.png create mode 100644 assets/images/fide-fed/4.0x/SCO.png create mode 100644 assets/images/fide-fed/4.0x/SEN.png create mode 100644 assets/images/fide-fed/4.0x/SEY.png create mode 100644 assets/images/fide-fed/4.0x/SGP.png create mode 100644 assets/images/fide-fed/4.0x/SKN.png create mode 100644 assets/images/fide-fed/4.0x/SLE.png create mode 100644 assets/images/fide-fed/4.0x/SLO.png create mode 100644 assets/images/fide-fed/4.0x/SMR.png create mode 100644 assets/images/fide-fed/4.0x/SOL.png create mode 100644 assets/images/fide-fed/4.0x/SOM.png create mode 100644 assets/images/fide-fed/4.0x/SRB.png create mode 100644 assets/images/fide-fed/4.0x/SRI.png create mode 100644 assets/images/fide-fed/4.0x/SSD.png create mode 100644 assets/images/fide-fed/4.0x/STP.png create mode 100644 assets/images/fide-fed/4.0x/SUD.png create mode 100644 assets/images/fide-fed/4.0x/SUI.png create mode 100644 assets/images/fide-fed/4.0x/SUR.png create mode 100644 assets/images/fide-fed/4.0x/SVK.png create mode 100644 assets/images/fide-fed/4.0x/SWE.png create mode 100644 assets/images/fide-fed/4.0x/SWZ.png create mode 100644 assets/images/fide-fed/4.0x/SYR.png create mode 100644 assets/images/fide-fed/4.0x/TAN.png create mode 100644 assets/images/fide-fed/4.0x/THA.png create mode 100644 assets/images/fide-fed/4.0x/TJK.png create mode 100644 assets/images/fide-fed/4.0x/TKM.png create mode 100644 assets/images/fide-fed/4.0x/TLS.png create mode 100644 assets/images/fide-fed/4.0x/TOG.png create mode 100644 assets/images/fide-fed/4.0x/TPE.png create mode 100644 assets/images/fide-fed/4.0x/TTO.png create mode 100644 assets/images/fide-fed/4.0x/TUN.png create mode 100644 assets/images/fide-fed/4.0x/TUR.png create mode 100644 assets/images/fide-fed/4.0x/UAE.png create mode 100644 assets/images/fide-fed/4.0x/UGA.png create mode 100644 assets/images/fide-fed/4.0x/UKR.png create mode 100644 assets/images/fide-fed/4.0x/URU.png create mode 100644 assets/images/fide-fed/4.0x/USA.png create mode 100644 assets/images/fide-fed/4.0x/UZB.png create mode 100644 assets/images/fide-fed/4.0x/VAN.png create mode 100644 assets/images/fide-fed/4.0x/VEN.png create mode 100644 assets/images/fide-fed/4.0x/VIE.png create mode 100644 assets/images/fide-fed/4.0x/VIN.png create mode 100644 assets/images/fide-fed/4.0x/W.png create mode 100644 assets/images/fide-fed/4.0x/WLS.png create mode 100644 assets/images/fide-fed/4.0x/YEM.png create mode 100644 assets/images/fide-fed/4.0x/ZAM.png create mode 100644 assets/images/fide-fed/4.0x/ZIM.png diff --git a/assets/images/fide-fed/4.0x/AFG.png b/assets/images/fide-fed/4.0x/AFG.png new file mode 100644 index 0000000000000000000000000000000000000000..c8a7005e40f0fab7eadac4a5d950e516fc8d3c6e GIT binary patch literal 2738 zcmbW3={ppR8pg+rbsWi>eaR?mc3F=wnNqE*un;C=^9(QUK$eS=`VY)JYqT|I{yo%M1QPR@glz_)RyQ8zTuebrp`;m zXC!MsRhoi3!pp)Vhn<2B7r7*~jNG_zpT*-js;ok-DlT|9#~WO0zLxhr;N_?NCz(9c;m?YR=|MmofHK~1!8z6v}vw-207PVNEcWKHUMT{13Blx zC-LeRv>O0j|J$Gz4?N;;_EnJNj=!Oq?e8QfODAcpVI{A7Y;XTjpArK?V3wglofA_I z7h}$UYXA59`<(Im@85m@f^5t7_OQFkI*qfoeu#jpWZ|zaV5_SCJff&`?kzGAL;4gV zvGfx~^3@A4eFKq6=Z%bVl97St(3^h7VPJ}pBY)YqZvt_?x!>2t%ny%{unmqpf>7I< zZZ2flF+U4XV%w z2(@S~WLvV+^aWM9>gaWMT`Dzp;xqDSJ#b($Qtbdy-4lT~*GMFFpCRB;KN3K;1mb zbeU86M==`donSYV8Nr@gH=lO+C5%Obd9vp_R{S8nXFmQEe!iy=?13j$8|tvP(JCW}GJWr! zXxtTOb*a*qU<@qr)xYNDE9iexxTQtTeewL1%Mr_?(v4tbjGuD0+j{l-+qVM63qPb( z^6vHjImBO&D{2!YE_^NgPVXknvQg;IHoE3omGHWn_C>Ys#NAsz7(6{2aRQ9yt#c(g z8x*(TF`{yD*>6j6>H=KfoJP7`j-DfR4E{>-(Qe)u!+0m~p_V*1V>zpzSaGx~2Z!;! z78`u56e|^|#6u~1uJI&2pq~}~>`v;GryYhaHN>+!S0s+@ygK)0sdtBx?9$rVta@}? zlu>v5XgwWl6WiOli_FQlU%Qq_&5|^`bBq{Pf-w2-`JOxePEtl`#`*(|Zd4s1OV@Yc zL7L+AUZ59Uwcx(Ro=AP96oRyT^jXmO@MHPY)ViiV<9fgJ!69Y+spzw5ETii7aCwKy zf6-67>7*1e{ZNa>O)Kv2t@#c%Oyxtcd6hhgcv0tES4jlcvCg^*4HCnidKQ10^OQ!H zjd)WZ7dQnZ>XM@WWk<}&k^Qxmb0yb zpO%pQ@U`5+YPm=ImCt3npQk!(bqtWRt`^a?8To2Lmj}~UvT)e~u07x$N@A|b4)#g#gUyLQlJ`RS@26+e2HX-Zj3>TQgRY za)cOeAm49W4Sknk=xk57SsS+{Y*gHNc4Q0VRgpEnxH9P7EqEZNS|{}Ss?O|!y3NhF zQQvkS2!wlsA&-QU%S zUdnH9c4G!)^$K*H&a}uKD;q4F4_jcy2Kg z51`08&@th!Z<;z7e$gC38@-+$^05()8Y1JzDCiz5?9Z&WRq3Jz6 z+&oCaE2~K|*~?3GK78%^BTpZBI9jQhks_Drt8bw5X(BV+hJV?bta8FoXMdDAv{y^+ z*^yUdy%aOuk!I-xI_hYCyExn?CH1`>WaK}3W&=`MZMD@6I~M{bIKT#8hUC6UkhvBX(Qkm{$~JiQ%>$&;0DYi4Vgeg~+f9nsSEOeVH8$r;1^Nic!5 zCgE%Sp2D@9fr)P3>zV>!?a6Q;r zx|}9s-@bDvxu!=v(r{M2@fjPvy6H7|x6EPwMku2c>5_;R+-zl9k4AwHlMtg(SS|Sg z%sjliJZ@2wsoBOwRp$gK^g%B*67A(^TJ@3(T_jYd=+Ood;a@!^PIJ$=)Pfd?1e7tFJ_^7 Wp2bnWbzcI#%HZkh=d#Wzp$Pz6aAouW literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/ALB.png b/assets/images/fide-fed/4.0x/ALB.png new file mode 100644 index 0000000000000000000000000000000000000000..c7be70dc01fbf0181f1703fc3d56ba92c488b195 GIT binary patch literal 3423 zcmZ`+c{tQv8y*ZifNae%JMV*L9xrT-P~&oO7OY-}m!8>9*FUyxixxK_Cz>%FO6G z;JpAdm7zxz7r9E-$k4UflhCujPxC1%69Ovx$ZMj4|_YlQsYW?D|R;mQJi@~GA2qeEhtfj z$eAmJwzRb)DnR)-&#DkVgHL6OnnLx>&06gJGgaoBK_qq)tqk80BPX%s;B63#pbBd+ zi>wvJl45!A_51gO{UN2XThOZh(tD>j=)L#IHz(*z@w?gw?Jd!JxLm$GX_F=FZ+av+ z?Qu3pa9U8xM~BtO(iEGv!6~3B!6H*DdyV+tn~xTEx!IP(+XlN{hTDGgjJnB@nA07k zSZb5eZJH-gB@fbr0e4$ zN~pT=DJVXPObBKI)UHa5#mFAAT6&7zCWHKBd@?sy>Vrh zkKbwuA6e^mWBggr!IyZKnhUSiwHhHxQ``Y}>4<@4PmG>@Duj_Zo0ZM*^v|S!6V^uA zr=}agvb4FT^SGk`3FaFrkL*%P$%XFd5bIHtVG^t9@75h6aVQoeN|asnNANtU%~GcwR3e zVWgapOeE#$TD%eteLcy{cQmVK{Al~9|aEPsD=r)j|Zf*?3 z{WhDv%aWQy|<_qM(6b zmIKRfq=IY4#eQ5H4AO{`7uA&8SkRR{=E?o6~Vnw0ix$Yt?e}Zmf?Q@Cv=AXHCB@j^@Qk#f#d%=40bMk zcs?qWK7sjhPdCDD^82NF2sjvSB}`UJ?xDci2BS-PX9EbL{fAh~44H8$t!qx}OVX_2 zc7H*@07#pyqryifK_aBHjceMh*Uo`wp}1`(;!JTo5AuW@>=SY;I@h%rfQfKwSsvnyRBmNtF2?W#xS|;j%ZoE!pZxv zqn6R70x~mcwdKXk3SN(WLUNrJytF(U4FB0fF;13q&|j8pTLK7&p^ij9*OQJNOjjn5Oe96Xk`k&RXzy7>p)5}+xFe^freE!)s*oRk6+C< z^f@NpMKwc|r)5!#_$_@wuoHh#J($ImtY)z$>WW&EBrFkAzFTvLME9$LJ)&>^niGTj z+u^$Ys0;wu6gubKRoi+oZ_O}c)O_iQAlO}34S_z%7b^UC-LdDT=j>Pe5g{8YtXmvk z+0FMSQf+K!sq5L(i#ft^u`PC{g5V;O4`SYaIU}M3u9pLhvqJ20NxUVutc?Zs?a3th zH4R5yFYphZxzES`nSaj)70ACO)dijBU*oAy>U-M*uWJ|Q)nA@@noZ&}oITCE|CKoF z?}6)7EAgobWo(@qX1aL~Q-XQK?oVrI;v`zzbw3HrF9z@1Q>#BR`EQ*Ri$6S9lNem@ zRQsqh9}0X^eG>Lus%1{`jtY^_&_UnJ9%<8|1+gtmtmK{HRy04387a|A3{2`nQfPrx zA52>;cGUx8LuD7h&g_Bz?&RxmFpcee|M;7wyO4`&YZr3lWn}911Ml9MTgx^UZpVEc z7e_aQYo0L;@*5|4S5|_nBtv2Iwh{n=2|5*N%g1+^LP8+jF=2lAg#?|M!I6y7xlPTA z7E5F(@hClHuK+C#lAMan&O`SgoX{oJ440}j(5ug}WxA6#vX8}T<&5%%;ze3qin#9c zoU(Yet(DNOI1Zus)}ha^k^yaV%=+_=rndcb7oub(cIZp{AG3P|rE(nIp|12K*g*ANp{$4iYS{@NO@)o*%n6b1mMHdcT{&(N-ZC-On*Vf; zYaY;}8gG^5Bk?+7u3+t`jwq6m6oScE_~4r^pfANC?qn*@>(3$GE&j0k_C+AUG@D1Y zE(P!XxK}3+Fro1sZ>Xq*cOAObecDxt3^o2FQ762iLe&s?Mf)uaTRRR8)m&%l0m;=B zZ#hMaRfkb{a728?P~}DY#XRyfEgms@+m>$uGr8sbNv~BJqZRYu)tBtL+?=dS2}U!W zcl{9B(Wc52Yw}vu9NMrQ@9W!YR=p5C5PuJB84>hiRJ-QhW&J+SI`xzO_;~IgHCqwB zI^;CTFzaWPZqG5@&+kG@;IdL6ph(Jg_9;MA=I|EP+kkyhc5B@!GVG`oS~K}sgU+3v z##q_DNI$X49_Dor7O(C~z!}{TI`Z-Vp7!_;qcdp|Tx!+BKhpnU`uzfQ!C>C7?M1p^UAKnhm^?{sX2jb=m*` literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/ALG.png b/assets/images/fide-fed/4.0x/ALG.png new file mode 100644 index 0000000000000000000000000000000000000000..9adce557cc6ea531aa6a3f6610cbea180fa8154f GIT binary patch literal 1701 zcmaKtYd8}M7{|A@Epy$a$)ye=blhUdJ(Ig)N92CzNG=H(JJf8bV-0h5Ruhttj@#y% z$QmWLNUF0e9Xb=`wn<{h(f4!ChyVM3p7;IszQ5=H{)%&Vl95!B1ONasF3$FzyB@kL zX9?(Tg+&V^zFVTRUorpyGW{;#VQjVLZc{PE!8hgf`JYqpVM$Q{JRYwflMtI65tbOG ze?BSt=8}~X001>|vB!F)l`NJ7#CbU=5IJw$uD@$*iQ}Usy<9sa?0~dlTlhXiD>kjd zxcjPtt)9cd@Ua4fK9c$2xPhh(+nUoo@nH?@z-It)BH7Xz^qNSgj8}FwHR9_;>XM3VR zTJ9*xlq`7K8C`Aw8%h%_a0Hvgsyidw1-%W zg0jks%a{DuGIDo>hAUqe9~nln2AE$D4!s4&ShMDpxi@OZ3SyJEuU%NjJ&;JzaR#!B zK<$fHnc8V6-pb^&qaU2=pD<%+p9sRHZEy1S?+k^p&Xx&2Pft^BNvvH<=`yexVJ(h6 zXC+7(J)iuVed;k1RA!}iU(d0Msl4gB8nFfd<20idE3IK@Zr; z#*1~8iilh%$HCHnlnmNqEn4OA~kLDE6Qy!uQO)2a1P#kTi1cPyl@#UZ|dz(1B)pY|TA=3GWR5hmj zNfbs*GG!n*NS^($vmxsP-sZy=Y38N8p5T4%!IT5$vbS;JT1JX9m@ssIq5azGA#_Lv z&J5aeU)7}NZNDaWa!)<1ZnH(Du0r%|62TA3Q6JD|QaU4L2OR!J4Y%#!epDS*>DaE$ zUx%!U8-$yKc{P&*b7YlpH@DDGMOFGmY;?HpQ;+z3Od&nc*Bjf|sFmU&JiN3 zUUOOS;1W@#DNNKz<5MzOovNuN)$L#5EBK`G53Req;WB3-Jt+303-hB8I?Sv{k9PHH z^EC51q@%QGXkhH`NQMn@mVNom@wS)yayjk4CV#%}M=-6clR9?*?A<}*FRJYV^b3Ee z>~{oPFMY+V%fRaO^;M{JRn*)PX%1C!WyJQINyMzMPWMUXvTg)cjHm+1rr(k48-d5w z$n&W5dAp+vR2hl8RX+eavOIyvnj1=)B}6Dr9}v2ko#>}Ty)x2M>jWmt$Qx%Bj~mm^ zbFD8|?-Cyd15T!YKyK4Ge0?ME@l4d4aSG>;V)0$m)0XLK2fbejmNR8GLu0Mb zd*unnB8xRR{T8vQStQq$U!og$$8k!bn3`w-+pwIA5hBdeO|4)k(C8v+3w{>F+Cq8VlWT z#1I+G`n-;7PW{mra;Y%pkp%l9ln+-TFh|{`%tCD|uSo0k#lZ`rTL?I8I|OBt`~3f7 e_FwXMV~>mCxLR%8`0d>n18{M0w{NfuCH(^!;3aSX literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/AND.png b/assets/images/fide-fed/4.0x/AND.png new file mode 100644 index 0000000000000000000000000000000000000000..fd2f7d48047379341d864777196e2b9542a1a23d GIT binary patch literal 2449 zcmb7`c{~%28^>p%F>=j)eO)6)qm{F4*j%|2N@4DVF-$SXOqRs5FUc*EBjW3fkZaE5 zOq3(Ja?O#*)zA0u-ygr%?~l*(dA*+J_5Al7??;yAhMepo>;M3O)7Z$s`qZ6IWdve9 zjclHquKya&$N>)k05AV5COK3g^fW0PgtiZ|@%IQKxCFWb2n2$Px1SH*)dlCS;veXl zv7sY!+9+XcfVv)z~WF(6dkdl6^e*!=` z21ePuPYB98Fo{)a;n?fD4^10LE^16{|GQLVERSpI@86K8z%Zqf$yN6NT3JA)-6pSI zwOf}hYzD+fKtLL5_@D(qkDH`n>tRejAQMs8B+Qn(?B7m>m5vBW@v zo@Lh1m%9YY0j2had?pa7s~X~>V}ssrYPQ5Am36z@=0XGny4W&BFy?6M_uW4_ zdg1<(kwUMYmAk6gPRw6BtMAW8=XX>G@SMqAs12BlQ8j?!e4ouBpDKWO0?3Tej!LAJ zZ2A-S5A-@p8whPX$SDqJT+bC!d^kj}Zj&{A7k{12vW7xzyfc?0DUMDJ^a6AC69yb5 z#^Xx%l`gqmc~OzyQ&OMRYxhhQ>PcBb&PchmNLYnC<{yhDwGo0--yJ$s21|#<#+*G# z{JTUm=N|!P%VHS*%i^=GDejjOa=Myk5rOH^e|5EbM(PDSXRgOt3=3YM3gRK)#}JXe zc^{wa^TMNCzL+?R_C~3(VF$%=CW0g6CjX~2gggsoK1blJ}%6fz}klC2=JT@%AHx5qO zyeY0SrJ{Y%&d8&9M#;BEBo528IIq{~PT|(zm%TI(HMfSCf+k#xe<>0o?ynS%1~e65 zS*vgIdk8XKT9tlpys8H*rMBp7d{mEKo@-2VE+*XGvNc1ET*B(=RrbEF-S7-(-kjjy zJZ}^m72?SkEov#EUek6EMnFi-3yBN%C?n@p~rst<9 zxgEoJU74a-A#M4hmVMxKZ`;GUv*lbs$F5dF_`6T{>4Z;yfwrRhCVhNrK0fZ#+5zMn z7Y%2eS!VXmi)f+BwwzpxQh+8oNngulq^U6en<`j^r0P*L$3p8@gC-9f3E0rhb*ha; zRoGreg6|K|MKM|S+aXh>ptVR7@=vcQpx#C##sAA_ghRo>^*Fkz z<`H8JWtYyuNS~9GCj@q7C|9frlxb$F*m5J;q2n2S1brjLnSU69Or>A@uqaE%sl~6K=lzOEZ@}FziZ+1%f&+ zWSolAI_H}2U+H7lP%c=2<*YX-Ub8GmSV7p#X7(%dl49>gVxwP7=!9Y@| zJihP`%vDGW4?Y{Z)Z~v_8TD1qCyPp7@<9@P{|=92t6R+z7HM#oCI_uw047UwpW%Iy zBe=}3H+t6~qQ!7a=~rjKW;-C7OmSWD$%xufuS|bJ6Ja{sYck)l=O0@!R1cmkRgRBU ztgQcqu-#VW(%HQFqzL)DSz^GLuZMR(+x+8(VY1HlOJKvd5r>nsFDvjTt#k3 zP#53dXC5%u-;j8&cMH?D^m8;OGz1jiKfD@+oej|m*Ohfr!-YxQP>J&^caj1hS(8#C zl1D)m{c^}$QIUlYF^IdIYav`8fAyq59yEcf+*IK=^pa&W6F!P; z3ztFfBuGN-YWB~a+}*;-*hrpN-?_U~8>X){eC;d1y#KuHEg-;nf9`N=D$1mWDZKD)-v|b&}e%@-s|-Bk|^ZJC5Ra&RgMgBu&UhV)$5=b<$AF|j_Gai!sgR*vZD3RtS_X@u{)->{o@^Y!J+c7py? z7PFH9aU4}hCNSxY>#HN!kI{1q7(hHs>OA}IZ_PjtaQn7`y;S^KuJ>&WW>Hw&)ME9m zK?$=X7FX0WJ2`DhLyXdoPH`2a<;trLw-_od`Typ8_~635fLP# zQ1YIWPIVJTk1t==B1cey0*Ig&y~;H^-vsyo74APTJt#wGE^_F9VYM2g3}(Nq(&_Wn zHiu?S2h(~5ezv5grD3tyGd{`36?#Zx@)4w8Qr97-_{?d{7=Ioy>Iq gAL#I3z;VK2eX)>mmuJ%F6sG{jXmf*dJ!jH?08#C6e*gdg literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/ANG.png b/assets/images/fide-fed/4.0x/ANG.png new file mode 100644 index 0000000000000000000000000000000000000000..3ed31cb922a6dae50cf29fac4cf93118c8cd5a0f GIT binary patch literal 1696 zcma)7{Xf$Q0RC<>!<CT-d{SLQZ~l;oR^tQRGN2}dC9PZEGd-kc6DBgG!3H_ z>CQ_pvO~m$l{4hn^16!<^D?ief8aj%!}C0!&-48He4Zzl=tcNOK~n($fN$L0@T5JZ z?7>Y=c8}#F7el{FyjuVb0Km3i0W!c<8t$3MM3<9^zHt$W^x%YWfKI2I{}>xh3k{AB zH;+q*ES`7J1OQnxcRcRIg_6(3=^lPb2Ra=wg_CANqK}i6FdN=q5n_#3J$24Ex|bSV zQ(#lf4SSSntud@DKV<{<;!4qSA|Xa{5J8&XJ@Cj73T3f=Unq*EF-$ zTfaP9I*RT)HbcL^nU*n=(rdpaGQbQ)wVk2^yL2k(yHGd_c?#$m0!MYIV-Gg~ck7`1 zJTpebe?*T=cy1UsI-(;~8x%NZqXZRzpjb^rwSk`Rlb-;0o;wC|^4j&c`-)^Fe2s#4 z>tZF5W9DV7s4H-1gr~IJW(A)`frQ_G4l&F4;a7l7B(%#?@}89;a|@?*e)s}%h4eEx z3rlcf)~9`HJT8yi&!!y}DD$+c9!_Z~l>_Mub?HWyh^8>=N!QSuDw0E%Qk|VqEQ)!3 z*18&8Iq{_n#+6$HTUL~=bzdH?$|^jXKE`NUfAW^6X%MBw7yjt7HXI9nqH4fXqvX&~ z;7KHkjT0-g92a%Ysii{Rd1V|Yj-%qBWRl(UFp>7m`u7OgE%`i4%OJ=*x(ps{!icTk z%I9hD9Okv}K!)5$J5X>`5-rb=RSQGmY=&;vZ^Z%keRrQ$0mqxOTVCmeoR`>sRL4gC z^{{1L0<{ZiiQyT|tjk*NFPc~td*Et;(90nd2TR2_&~dH0vucZM+8iKYXDwnS3L@+o z>J+Q@QU_$yrxB&8HzNI@B96hwPg8F8^DFt<2I{;J(Z0W`#f~5J&Zz|D8p8Q?cBf6L zI}Vo~izWiQ#;oD1l{Oi%zbXGVH|+D~A9hY7eR)Pf0bkxepi)`9cINN!%2mdsj2!%G zgfrkmGYWs|N$m0%-JP2hv|TwggA6(2E&ySWim{FqRH_HE}p%>;8+p zdqyO_2-4`u*QQi+`u-4I@e7a37c;HqwUj1>+LU2M!|B;&z(P{ z<=jYKDICj$WQ*3_4<#3H8e&)ky93_=;H~I}Z6k zTw{G!_iF;}qm$LT)OkcuGos@NO{K9|h>ca!aWwkSGsV=WAEQ9jzZHMZcJ7%N31rg+>7us7XNtxwt^y!CwR?N`eV(M>KnCn8Um8R1I1oqaPM)c)@P aOEtEp91T7wgct9<3*heJg|Bgk*ppAiGQ_d0sCZx^*DG8*I0EMQGk3wjea)b^6%s|Ni zfeFw;AxV?LP-n_;r3XiGfY_8K7^sPXP(x@i25gg<%LaVk*0KHLLr6YkORK#!&2MH$ z(te-bd;8vg?|u7ewWf%O2#N2|4HNKP0CdExw*mj}_I0 zaqHX>sZ)oR`vkQ`x^WCCAfV-bj}OrQc-(D<9RXD}uw`Y-eMXIdupTgcp!+^y-C@a0 zWo=Uub9=ola6`QXA_v3t$?p4%e^$JX`Em2@fdTN>O>nP7dHa;W4460QZAA-YUx&n% z(7#Xf?Gr|c*Po1RzJ1M`@XT*ZqtHUW=3`227d)CX9v zjST=;tZ?xPTuFgh(aoQMSvk)4W;5*G06n@wNHC0kqG4Z09dP7*2bc4j*Y6l5N0MOo zA>WS08)gIJTzBMyE-7d?vt{RYA+y7 z9qY{djxA|*LwW6jkygz^syQ#(SIyJzSRV%|w~@b1`J(Y5b--jG%ge~hWk5JwOcp80 zVug*{V9i#jt`+UacQifQ^Y1{f?>CiYu?jh^By50pw~G<>%Zc#2h44ss(e}+P7&H-T zY85}MCUgQ)fv`sVn_P%#T@SMJ;DcS_eVag7HVdw%!YdEPY4-J|-)YYIj_)@nD+fmW z2>!iGq#TFy0T!*-#`z{-BTbzT&}0NSI0*bkO{-k!*j_lj`}Z2|PyQUP--Ly)$(Q}F zFW`l!rZwiD>G0E8^5uHNuYnbA+S~>JAOYB`ib+lR)fCvaRvTUQ78p9kIn7m*p};?Y zHg4KmTnkzUw6M&gDBF|a%tdWBDbiP*Cz={vxMu9v% zCSWTtzO{a>1pr_VuvnfQ4R|9KHZ6hRAo1C57c-GdInKb$if%ZMSDKUOL|y6I3nF@n z>k3ySNsN%+zAI)`a(NwC0sycO*e~CnhYTACd)^ZcFdF5RFmpNVJL;R9#}IkH ztp|NQ4nQ^VGLX|+M-S)_3Q4OWW`Y936yiUZAbJtx-SACL2gH2e)AP&YY}*dl0))sg zsxcK+5W5&&Tjspr(vze;@Kl`d&Id7Z@n&#)Rqp!ZSzhy$ou9xn@sN|RpinO;uY^|@ z!R%FHYU(R95UU8@;Bg9aWdS0`DMhy~@cA|v(oaF5t&p7uFU?ivtOqe&{{#s3@_VuZ zP=%-ma(`$F41l2n#1-lbQSkIIvBIe}ckV%YCVZ9wM^A|vnH)la!$5?L@5%}QKsGQ5 z@FayB#o?_XHz0omrpVMOI{*M5cNxmEAB=U|9vWof=>fNh>UW!vwmwX%bt0#rGbggb zxOT_=uSU~~FR=FZXR>wp6aauXNA2c7M7%E|{FVVF|K;GJCkeL6UnlVSqJLe+93slD z_$tQl2(?tQB`w+~=Ys+P2(?s_sDL5<#@dXzM3iOwEJu-LuYn~OS$BUd#W0uHK$+cB@P`Lsc7nPpxTdre@LH6&x&U5De*JfS*zHRrflAK zMYRBQtS=?;_^Y(B*Q=#M6V_zRGc3#QZp{fqm zYJdX?6SK$c=SXk4>xKFdUVfFs2S?D}QlY9I)&C=ALSke3>wZ-af^4-UrbVmne9-7` zsvYY~*?c0#Z|cFCjCl+y{z?;Fs;3}V&Q3@mEq0?;>SQtS^xv4YIaV`$ng#%qiLM6@ z5Hi`@pRTpRdhHqok&#Fh>ZcwD+6DkHckWF0%uE6weN;=ets>RdQWzD5<-!F`^=ke{ zZF@-xMKLi*^~S0PCG+QN?tIYcSrPST&QP|(-5vAVQ@v*oRUdt%i*C(RkW;gXuF1)S zOq!&NdI#2=910^Nv6q+YqF=uN0Ol@TL_O%;TNnLOT^)C$qNqQAUKjnK^W$pl#l@l? zSQNM~PzI$77U=4HFu?PI>QA4h{Oz|j*IT`JFO^%jYOc32x~8Cqn9X!cNg?>f7d265 z&CL~gzpP9XeJ!E;2mrutr)b(VY`0|IoaqHpT^&Wy(S~q77$pFJ{oXx_;^M%n?k$X^ z2?^9^W~!>g8w?WwfVz_>DO(8hI2j`F#v%5?p;w2Y&J#Yl`URO{pr(+ z$Wj_Z1^}S$#0kpYdBTzJ$5deVQPVuy9*zVl%`m2(~!hM70eOHVf0KisQNKt&e*Y%)m=~C)5GE}lkISd~F zfV#9a%Ga&)xPQ&3pHlJu`zl(b9LBBzu9(eqJ9?Dh=bvw}o9)I8k@t&=G&0OL{9*(E zq8`M@V=HLk`7u^2MX|Ad<9zU^03hl?aSQdJblEcMPMy-&D&O#{007h;J4X5XrcXGn zO-Z3*(lDhwYD literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/ARG.png b/assets/images/fide-fed/4.0x/ARG.png new file mode 100644 index 0000000000000000000000000000000000000000..fe9e94682969cca303c856294cdbdd98d5fa3cbe GIT binary patch literal 1610 zcmbtV`!~}K9G)@Fin;V=?sJP?ZxnJ|Mr4>*jnW0>uDPr>Y|L#Twcg;_DLa)!)!L&+~cCdCv3O=RD_0b;8<-!{lKg5J=n}gT8d2UI)ZL zMGvl+Ki~J41Y=xDAQ0H-7lkyf%8nnHa%3A9vNI7!rg(?m1W_my-9Q4K0}uMXI(ByDZ`*do}W=GZ7jOtTTwsnC)2}V!K*2B z>cnzn@F|m{ZVe{;A2tLT;f|zj0f#A=%pJY|tzVXUG|+JJ@OGuMkj8I9riE7_fY{eD zDRDevYxrqv0K9scSraG4c+uAyFIE3`YhuX|a}4{1oQM9A?5DT(kejCzjE|Ivn|sOG<^xVGF$M2(NtOR@$$w0M4^PE90op1$gF>Jz z_U@T)Vj%Q}2v6lPHo=53sEBriJCC3L6ezcG4wRBRVwyO^;qCVn;t$ z-LXsA+b&*wuZu*#gQ}v{Dnh<{#$9cGn1n7#0;LohR#9qXdyP#r`*G8yR;a6_$SF zuugElIDb!m)`YSS+pRX#8Am7sYt+WmJcHM8viob74FuIm{mI(*s*ZyPYKYnn7R#Yz zqyAZ3(ZZ?vIRa-X(YD z(Ih1sAe-{NeK*ClNN8u1s7hlh9FZtU!v}O_SN0BtgUT76B|( zxcnnM;wk_&gK;I|Ry-}I@S1@6uJzxUdXHLoaz;8l=?k2D;tw;rl#uQS^(+P3;pUty zE8wf~LQi$*EZ1=;IwjyqRkyOQUA)Ag*{OH46XUroJ}SxJn+rWEiACE(7)XD>>pfD8 zOvC`Ea1-aoPhQ$W*{|$ppUB@4tCcO#G7rrQ2zLPxX0ScYE>iP$l|vW-T;3GNyCpsK z1L+_ei^XkJY!~z_Z9qh^G1;6s4V=Csv%Z(jAR>Yi-90P);Vl{7stU-tEpU50-AUcwu0?K>BBF fIT%y+djN={d>QE-n^En99RS(eV9{0Qyzc%F6oK!> literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/ARM.png b/assets/images/fide-fed/4.0x/ARM.png new file mode 100644 index 0000000000000000000000000000000000000000..a08aefafd716e7da9d7600aba7d8bf4224d9d733 GIT binary patch literal 308 zcmeAS@N?(olHy`uVBq!ia0vp^2|(<@!3HEb(?2AGr~;43Vg?2#GZ1D}bzG(f6qGD+ zjVKAuPb(=;EJ|f4FE7{2%*!rLPAo{(%P&fw{mw=TsOX@li(^Q|oVRxzc@HpfFc{9L zm%e;5<;%v^Oq00n58XTCJHws-LpkdKt_D#CZH6$0H4G7q8<-OC3fwpsQ|=fbO@a=v lnbrCKo0ganZ41;Lb_S=|bseA1y9x9RgQu&X%Q~loCIAI^Rf7Nk literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/ARU.png b/assets/images/fide-fed/4.0x/ARU.png new file mode 100644 index 0000000000000000000000000000000000000000..fd8df3a8d4e27088b50a1327b424faa79801ed69 GIT binary patch literal 1198 zcmeAS@N?(olHy`uVBq!ia0vp^2|(<@!3HEb(?2AGr~;43Vg?2#GZ1D}bzG(f6qGD+ zjVKAuPb(=;EJ|f4FE7{2%*!rLPAo{(%P&fw{mw>;fq})^)5S5QV$R#y+5R%2GVSwk zp5Cxo=QLBS-lS`Blcba!HKlGRY`qd~vTb2o#A9n=-#<*p&LHEFAH)1Ewq$rRQ~eXK0J&3#K;lfGT&J^rSXvnne#Drwmn{=eJ{|9@JzJIhBd zm!nAK%>4zCN4omEna_8YuIRn#V(i>^^hQkM z(@DD*>wl_$+Q-Dy<``|{;n1q3nD^K4!rTuiW2Fq3rxgU%pMWFPhSN zEX=LT(CzuOlVV;WC;IDnQ@=9)SCKJz;ogTo`Y{LeR|8};2QSleopj(sx<)(?9#e%eznbiUwOa2#Ot(J zcu(Qo6s@_(UA1a#t>xHnXzM9oFHZR`ccb7;R(`sL^pofNwd9w4y1qC?&wVfc^nJ6;6z&}+;ZNMQ%yViz_dZ|zDU;DTuII}>txB#vy>5^Ehqw7R z?{<7u-L2&CtnQS1T;Qc1jte-^!%Cy#1;Uw(vjoA2)3zYbr(9A?3NA9Supw;;Yla&O n7#(ArHBoGVq#!;t)1I+}*=oDrv^^VuWetO;tDnm{r-UW|cvv7; literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/AUS.png b/assets/images/fide-fed/4.0x/AUS.png new file mode 100644 index 0000000000000000000000000000000000000000..c5ab755e48c86ee8cbabfe87b2e6f4f56d7d9cf0 GIT binary patch literal 4145 zcmV-15YF$3P)^1XmYDFig`g06af_k!C+>-Kjt_(A98h=K|g-a!}Bm}zx!S9 zUhiIe?e(sAZNcSnozelBqk<^S%2$C-mF>N-nbC}!!BJgToQ|W$dP92)F;9_ zTecvRNT@h@Qe9|ZAS5E<4UQlG2xn(&J({OU@7^zvmNuVh1I#(^?X7Nni=jht%gjVH z=yg3?*nEUSV`Kijc8!a#y~fR$7*HtG2Yq~;NKgMK6DRi9qlq982nY@J=S*q@Hb1W? zXW&2{T)KqN*cgZScwF}G#jtBvz1rAPgl^}~VdCL|vZjWD`SZCrWC%3{1!@6HOB0fk zLP$(}2QxDxof~LlW6rK!@3ZPNDFtIkQLtbE%Gz3td-dYQ)2FeWJsSi9o!g}?gx~DH z!HcP>*oK7xaPPO3MM-asg9y}M2%5)vI9$S*slQycRsmq(ALwzL!jOG})Rlj)wAh|tVT zEr0cD34i_TSH#5~YBif$3n9S4gXfV*7I8}46SvGv%wK&KfYJ>cIN!fNRoU6<6O4>R z#KZ)#cW*d$c9vQIKY=%@$sNkV&?0MTSf+*CQQ(f->@MS50B3{ zefo;lZP5k7hyUEVb&r66ReZes6pnij;1C;&z|fFtnT!iQKHQ3n1Eo@ZqEzZeW@aSA zhxgH%F~PyU{PtTEu78|RJ6<3XVHXv}iziNCzq3EF|Mv%nr%S;{@pou zZUH0h?`sVH9mkBpJu{OIetv3MrBX?J{9*k3mT~!VakJ}ci#*W*0992rgoppg_;-`B zTKO&A*RMy|p#wEHZ}P{mVdRI0qpYb>%X@n}l9?Gvc=+HZRV)xdc=%wBADx5g#;^H9 zDy62N0HKKq4zaN~|MU|E9XqP!Z{93t*syrQ!+&hi?7Xeg=yPN1)^yIE&1KW3Y4p#| z=IW$LRHmkKD?Xme)Kr|3k}&Gg15kwtVq@PTJw30{iog7F43aKFt_~fdo`suwdEu0t zjIpb$hU|_VnS_KSQd(Nsl51?!h!!pkY?9#7qdCa^UGP78l+tzUke8POP=4S5*4?@x zFf>%ldULGgoQO8W+*MK zBtHIdldh+&AW$kFd$rLr4F%&`gG-lg;o=h6D!N;XddGlvbc5hmXtf@oYlPjM@yZ>d zZ!?-jdk@ev?GDg0?G9)nojRFeXlT&5U^8iVKobcH^5oT5T^ko{ChZPr0B>(c-h0m< z3kzdLjgl~Wv; zb-@Cx=Y4|MSX{p{K%r0|6h895ysV78nKQYaoD6{2!2zd@8!745o5_>ca_Us$OOD#? z%+=L3T)%#Y>(}pKVq(amMZ4+R)dCwEbF#9oYpoukQmLe&Ke5<=FTNPfo;~3jjbB?_ z%%u?{xHf&dYW2#|5!a(fDW5QdWy=pXTJfAYTPP^FtNv|ZX^B%(l4{+-%#1(t^SSu? z>ll9XB}Wc~6BQMJKtO}GwiYQV*?juxE@sTw#F;a>TGid)BdAxeu20N@xOez4GKqw; zloS9ujvI%2W+vOO7|^fZ=j7+#ZnWZzjC>@LMeN#@rBU8FI9Qc(;^~P}F6ZXbrCfgP zRpw1`=fHsvu(!9WM`P;|9j&@fg+fV8%wDbPuGayn9p{K+4)$3ZtXBD3wxE zScrkSIgSYlbY8Qb$uoZa;{FA zM8(|q*^xAr#Kdn-GW?;^6brRVP5C_jwi^*2@$9sM(MxuRa> z&850pl?V_N^o_>)ugKPxo+&A+TqUuX>YN-d`1oM;uLROgf869m0DWL=Y)Ge0X7wHy zcI(!O!-wY*6%~Mi!J`gQdG>6b6ma*fEDql9f<&^26DKaUn$bIUWYVWkH2e3<9u*1% z*oKGG>(nWXU0qSu)No_rLh?rj@MVNQ+qci8bLaXmR9{G?Zj2fwsrR@rD=QLzf0suk zN+q}A;>hvygYWr=3jyWFKS4CUED@{dyH@i<>vg89sa^k&!#8t$ozX zOufBv%gj_K6PF)6h)g2kHE|((_Jp;%T}`1!4;$vq8-~5T6{AOcGG>eyHa6yb@WEib zy&W}VMOId(>K`=)1zepng?mR-yG)GT-EmG%=6tOc6Z?J6#fwEcy%4!v!Q#cguz$ad zq@)mf^soU?g-prZQ-nzx5fB;P1pQBUz>f`d|VjLVga_!m|$mOaD=k@C` z?As?JHdZr%SpCeoyK^V9zJ1k=-+9UudZneY=4c562QJll<3CP)sfe8>PQ*PcOBLKx zDyiAMqd`3;CWZ|j*CuP%s&bvgVi6*d5V2Uqnl;Dk$O#qI)#QhV*GU04UA~$T6ky!unUN`u6<`K|x-vZl{@<5z*0soH`Yu zF{T3S-1$C4D`VM zw|`?%l&f*9*tsty&JxzGJ4s$%<1^+b;p*CjcitI@;Q#E5sktB{@`p#^ZwH5txVUuT z=+PWL{WO4=Ub1J*m~Zg(R81~3GV)tge`A=N8)Ii@$&w|b7&}(w^n(Y_Gkf-CZrmuN ztgK2cD}d1FWla&i_S5~&^sef$1}H{V>@>WvR@=+JpqtT=>HsjPRt^j!CV zVZ(Y8A3p&X7h8?;;^KQOSg@0|Yfoq;rvEfNw+k|+reX{Xge+O|3kMILSC`9VH;9PX z#*G_g4bD(?#dF;Q9`eHvf5XMamMd3E=-vATyu23c+axv5{TKjHS^0p#z}1wMRgsV| z8G%61stY*H;r~SsFflQts!FwZE*6WB%N1G;B1)xh95`^kalt3>+>QZ{^RV$jwV_*E zjc;ONNI-xGadG3Zw(iiPz0+;R0e*fx>&zV6GKPi*tXw&fpMRc(o0}b1t}J2r@Rypc zr_EgPj2VOQ^XsW^Gn!0Ijhv8>M0K=a)v6<;q+~a8R$bbJ1H@tx0RbKa1$pY*lxAUX zZ$(1FG5YphM0It`;$>}O5FS3f54(5Irl8<14i59$%o~a>&{+@Y+}Vt{xbgV=yVMDy zjUKJK^X=`ec=_e-8pq7djhQy>73R);wzeKM6`jokN=qyG&wmOzcI+SI=iebI=?n=8 z$H>dO&4>|w)pBFTc#)QN6>o1xLPPzTH0fm?JgB92?`U0_s#A3O2oeYc?AsTPRO$w( z5}nf1^XS*_(K20e@jWUk9%v*7Oq%o^$;mBGduhw)d|rw%3|cm zuUpwF{h{->pn`(C%$v8pNwTG-m4t?FY-Nk|i(Xzsw{A_RxcFY9a@)40bL&>)a?dh4 zb%0PPAUN0;nJijkU9>*ld(WS=w0ZdWI5jH!tfI3qpnw1FtXeg(&TD2JBh7GvceS#u6IpU+4Y5xfB=QBQ$g)J9cDh$vXX`vmRh#Vu-D+g?g8X zjZFuZEgOTib%!U)Z`+o^)~)Fj6_ry|R8C1r1;xb`S`8z&1#~tMtE#Hu+O<-yT~mE? zP9PAla^-}2<@54xlazEuSLQW}fq}49oUgZFoSmJe#{Pp9D-I%)J=$XWFb%P-AeFk| z?rx`by?S%N;|&}acZj)jw;+|qsz1ipv#0(Y;NoJd{x)h*kf%CY>*QpOgG0xLPHi%J zTN8fhfVFE+GH1>f016B5GGxdKjvZ6&QxFOTdQBTCDk>)7=(PuI-h7tn(>H2_pL26dkxExkR#v5#4SA1KUS7?tSxE#0cwlYa0cU4x z1`To{BqUKIW&UZcZ8+Zi;-g{4b_G|GfR0jpO}Qs;fq|vT)5S5QV$R!{(K#ZaGVS|~ zjjI!H=V~s!qtl_*I+u{;qTXUCNfs~lgu3W`yQ#>OcAA6|2 z>%%SU0{=f9zhk-U%7d)ZJTvw(g_wAK`RKODf1TUW)9Y<6lzP^GVtu*&-qGDRe%@#0 zVaR2@VeYfI=C|0JmzTMI-JSM0<(s@x^(T{iJIff>oD?p2^-O5j;>Le-&+c%KR#lqb zGL31@P2*1Uy^#f1pP$=y@%O8=O>398+m#=*3DIf$U3>a!pCrdE&JD-HgEwZgUt7Y- zTzm7{WtI7{=5PL2iuNa84VnD=>6=h5Nts)@&dIOiHD>&|b937wHTeVnY9DV|AK1po zu|7hq;@pA@UJb_8Y7eWYi+ovXmZMqKzBnoQ!(`hPtGo(ilJxERZP#vlutP6HRpHNt zzYDH7+|IS^t9}!5v2;%B=g#*xx@^B%Ic>k5+`C$7Pm*TXTahIFwW^<9oKi^c=UDrW zF`56}u~{~S3-10trS@y~ew}?2fmfy_?$@09yJvs6wEJJ7$Cr&$EjFi3uq^2Kw>SQAxtf=Uv<%S9nwJbl8Jn5*a5t|n z%TD6m8GpMiUghy4IlHFn9~?K6)irPb__RRrCBMD$){ngtU3f)u!y4wlI{f=@jN|-Z z^YvSjyaZz}&sabE&B_1YmMJ_Jto&dX;#%Vp+3X_C$T0J;y~visr>DmxKDp5HF6Y2& zp9_0mWma9wWl*@gW%7|vZokUwU7tK;cbj9XvE^3Vt<7lxKQn_5Zg%+8`MjWiuFk5y zuHJ+GUl$$=f2_Vs;`iI)vi}dTh1eD%Xj>UyNtg-WT~Yb?-L0P}6NItm;+`n8llyC8#J31BOt`g2^nL$l=G@g~Tpx312TT<`yl$bE_J*uu&U)+) zY3~~k6tlIKG3~L}=d1ep?8@1tj~176=BR6ZTN3|PC0wC<$-B@MQaz`)q4?jm_y(gW$2l{jcvNfx3gEmdKI;Vst E0MkKETmS$7 literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/BAH.png b/assets/images/fide-fed/4.0x/BAH.png new file mode 100644 index 0000000000000000000000000000000000000000..431ca8acfbaccc90e141d9d759fc5b5ef561f6db GIT binary patch literal 1471 zcmV;w1wi_VP)_SVWARtB!Ys(@MFjACS5JU(@0z?A{Whslcg%*rU zs>To{7!&cqh|xrhTQokH7^B9hBpMMDjW0$;3N4{cvG%qzcYL@MOH1i;=gyh!^Valc z&M$NB|9{S%nT*I=u&{pgBjtML%%ToLjuUAG*BfnrpH|f+WoUaU5t$=pX9^SrfOs79 z;}2h7OYQL=ML}Is zhPJl~F(Ok!v|zZR0O`hMQMF;i4j3&heNROF z8O-KBM z`pubaB9fgMk2N&8VgkAnV++wMYf$FSlN}|cL{?VBY;254Ta5d1$D$~Y*Nq}CUy~gr zB}7h^Zk}uGaP0&DWJIBMH|*I0n$PAz@xsO9e_EmHDJWVDU!Rbj#$K-e=6Cnw&v5%v zSho$%HOfw755;TuPb>?L9fzCd!HY-1G-W4Bj;AW1s|!s~Q3JE?hVOomoo4V9%WEaj z&G#o^ZfT+|oxdPE&A@rK0=kOF6K!eoTzKV}w{A;&ZVkAW^A}+AURbakPW>V~P5=0! z0=oP18<>74?5u&7i?Y*Po3ARME5m@pM-#iH*WZ?%rgv(3nsTtME$+H&L3U}m2=9La z|F%KN0@-PL()?NhoucXRXgTb!ke%e}d_4hWCGgU-FxJJl7kqYUD`EUtI8q0ti)1J1 zn*x?^`oFHjj*W2gb8nn{@KFWKp93#H2e(X?on&A`hK#PmmJRT9H4OFf)9gZzMy!rjcRd`NG2>S9Yqt@ZP$)Y} z%4u;I6@VBq0&V*)%oVHnQwT6*CrUkctN}uxmM#tC3!^ltkEa7-(q!~~2T-O?mm5S9 z&9bln=YPbfrJ=2V8111=i4`}Xq_iMq{=&pNCi3!S4DTwMlh1I7oLscM)u@XWJ2s}< zIa2|hFZ3v{fN0c_A-y8yi?A|n2Kv5(h>4R@JCH}EtOCUF;b@!6QP(^OiZ35OvV~^Z zT>_nZ`rVHqMtb-{y+drR05NVndi4R6;@Nf{*4H#F8*a*nd-_33x&p#IeR2l* zqV1_dExF6)NWn7j3b_1|zVNj(b{J3vgnN4X^{WEHJ$>qE*Zls6d-{~?wE{%pxWt}* z)*L7E1WwO2Al%bCO2GfuB7*q79@#Vik)4CKeHUuky|VKJj#jX4Lu=#a?NQRxWoHSb Z{{U8tcMxHLT;>1(002ovPDHLkV1hzaw|oEq literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/BAN.png b/assets/images/fide-fed/4.0x/BAN.png new file mode 100644 index 0000000000000000000000000000000000000000..d11ce5590054707a9d2fdd08ae6612b7c42d2030 GIT binary patch literal 1300 zcmeAS@N?(olHy`uVBq!ia0vp^2|(<@!3HEb(?2AGr~;43Vg?2#GZ1D}bzG(f6qGD+ zjVKAuPb(=;EJ|f4FE7{2%*!rLPAo{(%P&fw{mw>;fq`YAr;B4q#hkZu&gO@NO0?If zzq{^}zx*24VS{VSgsiySdPQysC>HAHm~;d(cTAix;~Fow`8$t@j9WVFG*dHndGmV5j0#NXcYt)&d|4PrWt zeQrp(-8PBIxjw;M^6i2D>^)V+>kdD*nDf0rCI0xjMf;bT9h+pxv)nhvmnYB7n?qW zP4ArbWOd96rm9oCH8ybv7ppod){EC=UHx$BoW-)JCtC$>o!}FFB_XNZ{_QFAfn-&A z<>XWuoh#0-H%B@NemFE=&Q5AQ%e10@-C^?|PyWc|_hiq7-*LQ++!dD3AKlRpebKUY z+OfvQPA$K<>Hba&n1E(gRBsYX5%(=DTh+BZ|HRZjCgBZhFCEw!uYEzq>eF?L5C5KO zZF>6Q+QQv}9SoD1=l_jy3|g1K%y7f*c>Ci}iI~0<94-g4jq_Gq_1kO{_o?s|zvUw5 z9SuIckH5VC^yF{-X0qh%=P!1<#fx%^UU*mt3>l@_JX0QT z*0Zv?wbfxo!{JK*8^@v~ZrFYBX)F`t5G_#AKYr*M&-wG0`XXmEY`J|vIQ!1|Ar+ z*Suw}<-~eMn#Wo4R!3GFzmeL{X1<4>)f+B%9nsOq=G9Y8uGk*j7b4lg&~Lcshlh;o%6kcV4>Nd;fq`Y6r;B4q#hkaZ>V2l9${etN zFI#^7q^3XyR~B1P@Zq|s)0xXObMw@m%}6=>%lUNHvn64FFRx#_cCD%T-^FX|qh@a_ z%3C+hTWe8~@qq;Gz>S3sC-hCz8Osmct-fE0W3oZtORD!;}M)nF0TU_dGgj&caz?KxdvXRe!* zPbQ;${mrB4_e;H(PZS6abFkoPKUMwXNw%&-OTO0MEioo5ZLj-(omuy?Bu^rhWzKsO z&RHM#FWU0`^M1EI|MY(`iHYmA9-DgGWl>MVCN2AyulKCc&wal7u^rp78`1sqK73Bj z&HwWKu&8awJh@u28H(M`Pi<{>t?un9jazm8i>mS0mH@W{eZ4R57~Vg*(aYX-eR;uq zW8FVN=6@W2{yH?}-S6TP_jS1A?p0SFk^J)aWSp3N<-AGe)+<-Me7cay{Al(v{|NhI z-9}7r%x=V6aX-El-L!lUW7xZ#r_Igbiw&o--oCfj!#UePiakLhAo%`yZ}~Vmq2I^c z!!_>QZQFj9Md4`PhW#bamV8lcO<`#G z9)IG{L?Ow0`}P@_e*Jsqq!*)t@!4~aZqKohJsiJm@5#&!B@4SHS5DNsYjxZ4=lZ=Z z>))!%ZSO7LmhR`Leyp*GN$gDFhbze&BMTP1Id=Xhd*9Jp`*v8)J!4Ysw9}X66emN) z+n2K!XV+YPS@&;WW0CV|*U81Yjtq17nr&5Ip7b%y)X+@0$fUmd-Ap!zj021QEUCXU zzi^GwwPi07`8OK}Pns5Q6d1&Cpx8C@;_FST?inh%rxnbWmA-teV~T4eqjcDkM`f

    q}EMLHhI#m{L7dwlv%pGwm8LJ+Ur;J)>XZm&HjYI5LWdAPM(I8sBbMf`%dm4A#zG`|bX}W%C zzo2ISC8l?0iYLD6TXN%0;<3Byb{Y#=|DPtZY4a7EzcKEajAha~b6zvFadGfQu&{~V z`^9u^!mF1*vzb0Uw^UcZ$P!Rp!BA<*SC`N6A=V&a=tTKuD3 z?$a}?6Z4-Rxh*AF5IgUbhW>+J;w$vxXDn8Cv+rZ`>E3+S@thfZ`_@p&rPCEUWZeJv zCJN}@VBF!RagOogU+2^*@=D+Ji#Aq!vjMFs+ARCznM>WaeOFIToL|F`6K&sITb*|C zO5UX#a!XboUb6D;5(WSFjGV!H(yC(qA1vCz%{Du{x?qXKleE?GMH0Q5+6k47v!AkNO`rAHQQz@U1|vD>53}3@PqSN$Cxw8; O8-u5-pUXO@geCy_{$!#6 literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/BDI.png b/assets/images/fide-fed/4.0x/BDI.png new file mode 100644 index 0000000000000000000000000000000000000000..0411b99d144e3c7e2816adda11320c8e73c0786f GIT binary patch literal 2729 zcmV;a3Rd-rP)OC?4Z4C@M5*XXwbM@RQf#MX&5T`zb)1z>UDwa0Sf?u0 zx<>72b$8Y1*6yfvM>^d<+NvD^>qkW$VRZZ`&?15rL?D2CDS-qMlAQhHZN^=`l9${h zu;w>&{(0}+bM8IAmve94edj!m#*MR}u8xBj?nhLVS%iuTK&1ktQcrj1*@0LLWMu*6 z`h=}OtpNUdai2$#-Qj(L2%F5_JFg+!}irieH=xEP(^#4s13Zk4G>Vp9YAR^N1 ziBu{gKR?fRe58w5Oq7*H)Y;k3&4UwQ#r^w4^X7Se5F=GmQi#gS2X(J)0<1WFnkXj5 z^8*?w5*^|oSu%f>H*$PWa zB~f*?({3YX1Xxi}Ks0r#=N;rKiH#-7&u8W?Vom_j)379$d){HJBp`qfH$6Q}T}4DD zEUH;#sW;ZMR?M%K($hWfJZB{>ji|PkxqCE54K1!&Y~oN1OMobV+Yr71*<-UIiUOp@Z1qr+Rf`W*a zFDEK2Br==rw`VpJUA@Y;6>8IU;f5}CUZRA%31sy*iedU60<5B}Qc{hfhIt35p@Cm5 zahz?(Z{JST-tN53_I9Fe+w3DksU&*%(0QAEpfPGF<6#DI#T@nV^}qz67!*(%K&nO+ z^;p>733TNO(X?p;J0U%tNTXqIQ={SI`9L<~;)u?jV{X!DF_OAjP4ZHCKmXb!z$&rj zv6SDzbm_&z8kT~CpZ@fZ9b<2&FOD7KZ2?4qfqb|z7?_(W=q#Wqcc%>MS1SP=ieUj@ zb5B?qq=Y(3jc7?U(@6YChcIQrD+&E!v zZ9vnj!KTJd$Z5~9=W?^%1OR})$R8WSH)3P>M)-;Roa!hGY%E>`li7g3C7tqgSI|qn z_^kCaY;N3)E=!la&CDVpM!GHCP&KKLP?>-{ZJzL6GQ{n!>-fc2ml5-?LR{;-;?cfb z?7WD?%0y&ovV=Gv03hfW&=<-eCH6<{M~o!Kk>-MNL4O{HuC0Nh)d%txJ$9_E0|#LJ zr%~_?4n@@%9scm*KBUU#xurk#z4h4KxY^!tGZ2J@pn)LaBv>_O6+Vvq7&4K}Q8jjf zKv;Qx77nG}f{(e+IS%;a*%>(e#w~~-=!Y#B-Ii_~Xgz=(O*^2o=$ug{ECdY%BN?G? zQ6v6tJiciE!dZ1gf&+)QXMKdP()$yAVnHC@PTK5p=jUnjkWi6;?1pS-oeu!)dRyOH zkBr(3%&nS>yFGVVsqKdwjc4(~b$!3%GjHfo(s3kP*y9UtX87hSj|i?^V`Kb zA+6P{6+9u?g2t@D!`r73ptqni@agf+KtSF}cyQ|!8q5t$ZFXC_v8`#F%N8C^&jCY0 zAy!~lfMdH?Bk%no0)0x=I z)8-+wJ`**@8YcP%fb)~EJxG=%<6zW5O!b?}OlOy+3+qq50E-WYz4LD(kPEck$WGih z+((us3um-vnCTn@W*@A?R=Aje8ekz-^q4E3%S5Mylb~mG04D!38PCgRGSLYD zyPJ0-y(S$Mh6-jnhXDDy!wSIvQ)E3kFN|EuR389b(_Mps49xWo2G_3H`e3?0o7n71 zB>E@1)t*}kcuxA9JIy(SIBA?)?YWhJvA$#7Y0e>p`-Z#Ko?8i!`MCB`1q%fE1i96o zTL}?wCzkAV#MvhA4N*ZXpATbBTEGXY&@`~Df5$b`?YufyIOE1b5u?_M^} zT-bz~-UoJFuQh9(wkZT|rTdZT(V*vQO|@t-w*UZg>yKm2<_c)WOK>Q54`zp^K*Wg< z;~RruQSgv9bfiOtkzd0xnIj)hb%S` zzsb`f<j$mxil2NOUSm{)`Ez4apK*`e#e`_ zfZ+I9Lq0c}9Wa~y+kr#CEd(UXl2II2j9rnthP>|l4{D^IGxj^aUioJ4zL z1^&5e3{EYS1K_tAk1q|z$={DbN!wN2G2XFhQ%5=+YdV1P88RIHQz$xkRVaGd4~H|y zKvvU&zdZPGNFA}#Smej#BR3`&6C@Mt8mpsl$>V4-C&ubAs|D>7w^`cIVd($>oTxpF zR~GNXy(#|i+r1x2qvrqsLPQ}LB^ouP9ut{RYpw+Vw42*;^5{aW+wu^qb8lhYjK2c_ zB#2mt&2fE0NO4k`}XjQ2zK3T7<3kOY~0EDpYS z)~WvNz5gMK?i|Ob^L{{tPlUsIsti?Ft69r>SBxD2QQ|1`^@<6`GUWNI=U+3^$hVwS(iAF2oy%Hqnr&i^l@$Wu@nR|>U4EjSF@O@J~;i93_; zU~9xywr@r^H39Fx4-^%#(`gGgZvsn}0CjcDREjw<-V1vV74a3w2+0u8ayTVu96ie3 zPG1bO1dZXBqO@?K>r#{kOPd{TDM}ts(v!>iJM&pf(i?VZs&YA>YI^l5k;!DgJ(G!V zvtq@tPg5N^iQ1z_^NHHir$0^99zB{r_xLf;+R7(tmz0dGL~U=W0vqDuQUxGyu?|m$ zC2zSJK{9O1)p!dwISOydGKatpQD#*800000NkvXXu0mjfST-Yw literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/BEL.png b/assets/images/fide-fed/4.0x/BEL.png new file mode 100644 index 0000000000000000000000000000000000000000..80422e766ab2c287ab7f778a87831c256f1a8c0b GIT binary patch literal 325 zcmeAS@N?(olHy`uVBq!ia0vp^2|(<@!3HEb(?2AGr~;43Vg?2#GZ1D}bzG(f6qGD+ zjVKAuPb(=;EJ|f4FE7{2%*!rLPAo{(%P&fw{mw=TsOXZXi(^Q|oVOP@@*Xx2U^zHF zm}%9a)*b&3?6pYnVbx2TG?(pN!``%KJVooX|L@CQ`FG~bzm@Nc+iELTKKvufpv@4* ou!bRmaRXBV>jAEY!63l@_a4V78NTv8K;JQVy85}Sb4q9e0C;m?F#rGn literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/BER.png b/assets/images/fide-fed/4.0x/BER.png new file mode 100644 index 0000000000000000000000000000000000000000..4f05b1dbfe27336ed855f3d5a447c849717ac4f6 GIT binary patch literal 2801 zcmV3u#OH>rlOaf8CG)=AD>?CGe_)78FYJ1FV+m6|uEOQ^7 zvWKQ8EiF@AJ3iMmx9uHj7y^O@rXry75@vYLgLyE_Z~t(iP(zS`ad+l(&YUynH^1Na z-uwOhe)rzry(21^lp=&Idjj7FC(wef*2G&M9%p@O24+hW9YrJ(;V4*5P3m4spIhDL zx4v(!LNj+7;z6qr1fjz*y<`&RCl6y~N<5N1`>Ect6NlM?YUD6NpI<`J)I}YRbvq(9 z=bfcEWhsS^%*U9X1wc@sir1Duz@@Z5v10MPC>63Eub7ui!s3|;6z-eHDscgI4?Ko; z{fBrDk0tuxHU|D{2kP;od%RxGMy&bf0HMp5V9q;7;qMl3A!!c!FVX-A)?5`)^z9#c zX5Q!?5usGbcxK*ciVrSiwYrS@$urSz+=eptPNMd0A^MY#@VO@efGavfx7Vk;5l23< zl~sm5`2Twkq05(GIeUTPl%;J%gaxTtxALKGeIi=!`1AYb@q*VyY9E|NDMQI z+$iAZ#P^@@Hm`3!)~WI7c17IsrKd%>d*WLVUwfcyBBVDh&`T!vB>oqOL<3)LcTVUf z6Ei<~7%$F_M|SWqy7yCQ)*Dfc9LBZNS8a`nV2!HXX=v&BlB;pvOQqRxHBRV?r&^QV z5@+q!3Ci?4qul zZT+ty?T)D-aGe03Q6su#8u~vkKs;zwyC<}~h`sdG1j0jnTaTMEbE$gg1N2`WLF}zW zGy5^bb7v7YIHKKn=wu98N`*FcjUEo#-Yev$QUo!aZ|&e$iMIa_J50*bVQHJbN^X3 z@6K*F>CV`cS1M#Io{_+cC&zMay(-_>gyF~!h?NR7Dai;aGpQc`a2J~IdZaClG#<~U za?QIK)3Xq(ya|4KA>vsxSa;wj)=%HjV{7qdOB3&G|A9>(XEHx|7%wlqI{Q-HJ)Ee0 zTQH_)QMu+_bX#^J{>QB?MW3HMj^6hqI=SXhyLufF*5*!KBfU}LC9`I*_P|f9e{`$M zHbGjtrfvIk+lcTPlSuTzZLK1V$Fn;fJ7maU?p?H?-E;J1WnA357khR#>ZBy1XUsq> zk+gfwp1-}*>AJY(YH0oQimFeU#pW?%KtGq@;G^?um$J zP~**Kg1^MG7IXvjB_&Am^9fR`>F1+HZnM#F>QuM9cX$L`gCGc8+_VXGc{w#&9cLRF zu-j}{4<6+5H{UqDyqoW$yBQ*p2#X*fl}gdr?T7#sU??d;VzoNGJdc1IAj`=?Z?|Iu zynt%J*J#AiNux9v+rU6BYs--bhC|@hbB}qCLY?v>A@=hK-oIt= z9sRppzT1xhB~>~KE@$B_Q!>u)BIC{5Nw#mmr|}Yof*ee`D{am<_{34@KMk|hz_I+? zE|>520t9n0(}Ob6lz%{|LQj3QnfxEGFgYL%vCxFMz7|1nAT&E#uL;%dWumZ)2qCS@ z<-2_s6d(?R%F$@aqOM*`cwb){3^shsmx&5XppUv4wY>(VrJC^gF`TJ3qdI$-VVY&g zP5ruDzFP}0R##IvW(@MOGQ6G)!Ws~c(#Ic#T!Oa7Oti#Eecsn-t4eV+S?S-)mxp7= zkafp8l8njw2FJDf*mk3r4KtA9;e8YZ&QX$1J zK!K^og4Aq4t&oBMUNR|HvOXs#Ba`q+|D^G?DQNFfW8AcfqVK-D<$Abn7vvWl46(6T z>~<{u#Ke!?h*T=U680FGr#BE0GlqgwYq6S3kt)^1Oq_$kWF^Ol({% z<&_P{q!Rqpa*Fb^NLa8Mlg&o6-HudtYx~n~DnOvNv4)88_aYWa=vzOU;~CrN7xe^H z7Z2edoMzfTCd8WRp{8M=xKNTfmN9eS*zB2b8>RG7-?>)-w#(tZuM z6p(8!p!D1)LPS=ky>VN~*vsToA=?;Br>{EEfwYJ^bT%!O`YX(Nzn)RYwIo#7sJ1|ANCD0IBVcJJBq)^h^yk=pZWoSE zFERd1BZCUe#1=qdxB|WA>O?Z}$^gdX89QAUx7?Hm*gZL%&teJ@Nnz;4iQJQ?rNAEw zLNX}5LqSNhl{kktQvbWC&>rEm${z%n^8Ik+WyKi6`;wa|P^HYLkG?n0Y}FHF>U222S1+-8Ys^rADUgT=((D`Z+Nk!M4 zBQwZ}Y3xf}o{f7?XpwRKIi1BL;2Io2eqV2jh6Pd*B4Co(2{$U}SI|g&j+Ov>*OUHB zJOW&d+b+i4E_ejE7>@uK;}PIuJOW&dM}Uj*2yihT0WQWPz{Pk3xEPNB7vmA&Vmty| zj7NZr@d$7+9sw@KBf!OY1h^QF02kvC;9@)iT+Dv}GLPUns_S66r0qjWbklZ*z_f&&jEfJQ@%&xw$S14CSinqWxs zUY4+ks3Bk*hb*GN(?Fw$8bK5!3Jnb%()2u3Rae!msyh$o+ZDiOct9>^Z^Xl7*HMMzYgGNSs5;AN+3F!{x8W_K ztbB;B=0>>7nDOMp@5mG2+D&SWU3k%pkQ3K|TOw|*A)$@Ts@-bnPswcf8>IW|2kdh8 z=C~A~?45%c8YDMWLDjN^qJ6|7+Yro0srv80^>yT~HTdOQND9A15N6(oY3j&?Gi6_s;v?Ln-O2m0zN7bT4g)MlMAEEUA1-0z=sK_6}_QD6; zhd&iywQeFbIyGy1h2yar>bNl8ScMmTCyAclr?mTS!jTA;xB@jYK*+coQ#b>=z7aih z3R2>&w9HAuav4dE5!4>VH#Q*25-I8FS2w=VrJTDI{S#lI`ib8|uv>@(1X`59sFSLW z&B)f{LV$+91Sj?92$9pkHwm0QP=5kHw1Q~nOv;;YpdR`oqKP-r(Y=ILt3|8TqO-G; zd_GS)ou*o?;`=_5BoU29aU2KRwlTI}Lx1dp|zB)0qn$es0<-^C2Dpb(T>$P zo`$aLD2jrjD3nSiy1Kfk)oQ4!ieVU}QYnhXBHOobXJ}}ME3dqgX0r)EEEdCY90WnY zZJ89-|C(@Zf>0y^gUboFtVeWhf<_r3x10E{A3$XPkq|=a}n#}2~bFtJ#SAPA^bDuhBIY}-bXBpk&M^+R7T-U|2EIiL67K>rqHla|6LZLuB9w(Vh;&~p9PEDPbK;9nF)`uh411Od}DNu^R$3sZnbY1% ztzIXYOp@FD9KBCHK-4u4$ykm_0R#d161!e=y`_JOXsna*@o{3Y7{y|d#fukXSr(RM z;rl+eZ6S&x^?IFXG>RliSeAuh7z9B;DwU%3wa?P^%nvYnSD;U9XX1u$A_qR@R0o;m zr{g9%p*VC%#&X;wVy-Vys+vS1QGDOWG)*#@47P3K`#t~?b@JQ(iQ4#s^!N7@3WabS z2ha0R6a`6=$mhmrUhn}XKm83NOI9IjQM%S#hh;P%mP8T2_I!$Oxs0jgJSN`%*Z3Sn zD*Y12jQ|Dn&15?lQLoog6a`(^iO1vQ^La#3q*N+VF6<^b?+PNZG}Ym+Q!hM2G#W(^ z1ftO>>0}biG>9)-MNk;UsT9axb35aoTtmFG2V?DBG@n>YXJrKO(ra+vbPCB!Kg!fu zm(z%M9+ch3>>~V1id;@&@jp;+7zBZUD2fC@Fdez*y55fM8=DE&Zo||+k77QGFquMo z>=y)T7%$a@-Ke4sEXBI;gIKi+UJxK{dyd4v--31HH<*0-7x?iMiFAhaf<<^c{tIz@ zCxKDLOLSnD^7Q`dyEC%=m=_>#3?jtNrRjAM?*1bj+d@%8ghC;z)hf|w6wmY85s%_A zF}j&_*u=HEi9hl0#2@=NB>&Y3Q)R&_B7c8Blht}VJYD}7(%5#ofAY^5{U;HebT;Hi zi9i1c68`jk!-H>O{_b@%vj3QOL1A%}xbq;Hfy>BdvlJ&c5d;CQ>mo@KK@iYtwUA{Q zC6pkn58|%dO!wdX0iGmNy5cjhX)`&rfzmhs7P*{5u9whuZ@}KZf%-XD5c|r{iCq1E zN#1-9l~3LID*r#z$%o@WfLPd}5&k&M+_xyLe>>g%E3nKWx~>BtNfMf-(b?HatyUuu z)5*T?dU7AV6>oGq-q!W(T6pfO+MMiIjIi>psJ4N-;g@JD&!ubUQ;6@q9>?{l*XvX& z6(Z+dLVobHS=oQgyC8s(+8GS!&jL`CuO?StK|HgRXe=|GOto4qy1Tm>9UYyHcw||| zEl(lecPq(tKYUH;VD!yzqi6Hul$M@DqklP>Kl~(mcRy28QzQ}zTCEneOr*cMgjjy) zplm;C1+W|$KTrv)`%ZXlM?h8fU2Cc6U!rdJBdFcP<2t_Y(`+^ghuby(m&4MRE1sIB z;cQ+$#4CKMzMSuqx|NLs5L=8A6&y zEBt9R<8P<$aebLma0#-XC8c*U*^=o_rx+U>qqn!0$;nAFnGBkyP1kp9+eQ!sGMNmG zMq@fFP9~G=+_`gFG}AQ6o_scyc}tm>Ok!CU`V}7|2m(rL*U(wZAJjOdn?I5K0!Pte zY`Y@GdKz)?KCEB>yE%d9%SejOL^*;dbmE83qH0FyU3>-A!Zv(+3{`g9wf;?G1m&~v z1(m?Ds8wtDN(eJNPj zS%hXdfh^0@Pe~{gqVfFWB)@he@&VNT_r+1;f(&l}*DmAr-A%b%A=|S8*Au2UNM25^ zBuS##Y|_`)$DUp53EU}A`U&T6CiLVEq;(Ax$3%5)DqnmAOOQzpE~QebAj|CsK9k9` zGvnMS=_l^RAKFOm_y0H9GcO=eLYStBEX(*?pGEohUz5u3#tKELpL02_GcP9EGaq4O zGw%IgAvwAUIoSURxYy%|3sB+h1o~wp`!2z#O)xgHg-p-NX+Nwe%5){OTrML?5`rMm zYPF{8^|CBe+r1Il@o2vAE2LR#HqQn11a$?&osyqkA5p_L|D-syb;7!~{ZsZw8_v~WPNxwI} zK?3dY(RE!al?s9&U|AL&9UX*1Ap}7no6TaHCgJ|2q*lI#&i8%-Veu(c^LboE^hGDp zY&NH3_aF$S+b2CisDCl4s*+Bp z$>;Oa`vEV9pU~c1i4}$ch&Kwv8U+fYyNIv3f&AjLNzKt59B~(9xnXLtzoKDxqsEuf z*Vo6^ty@{Ra3MoOyTKg+Uqn$OIIf4PDg?fWBqop}F_Ot7wOS3|_v!BLrc$XOiXygc zW7{^dSZw<9j*gD$ZAil~5dZZi`ksAg+LlJwn@}cpqYxm@#n+#sK!6JGK$-s~YSj{c zAR(*&I2>kZ*G?p>iCLZ?n(bk#Wz#Wl z0jX4qa=AUpp65|47D=bml<&Fih)+Ho!6(3SBQ#<+k{9hV1g7Gw{ z3+d>bhi$jd)wIjU3aU2*54G^V+hStiTqM0k}jAswUuwR!WvIe|VA;uv^vyn%T$N#4%MBx^&mrWiNNRZckd$rP$YMX?T@y4z zal#+C5o7ljl6PE7Qw~!~4G^p5P~YGXtRRYH&wCf5C{V8z5ffR$;V`l+Q@`paoc^U~ z8y-XS9Q3K32rm`p8VgRQarq5&FIvX(j(vvE_I;I3kO%?+fJo58>(`BplHZ zMG@ckX|-Bpvsp@|66zh-5tuc)zWNBoZ5xq%m*~RfG@DJl?Hfq`-R1ktRP8`y;^J$G z55Iu8@MKD+MfZn4%Xq#>JRZj|3_Q<6(=>W}d&xifFv_NJh6_qF}&+}66rx)*TuGNIy*bb<#K46hU?g9 zvP>e|fvT!hiv^6_2th1`U(BI>=Vm%~t$*XbdX}M2?w}cpV_tkU&9mNv-qA&=R6>^J z>BF|MSPWfnZ`{OUvFZDbMgz;X5jHaw_WDl^ZZi`o_Ri&WuYhv&9U7mpE?J129-!h3ei~oLF%Cx z(%Y}X5oMy9isM6XZ}0T~?fLnGgftB?6voYu5j1Kn)tqt+Sw04v)N>7YKlyz2Q$%)S~>7HQWVc3 z^~g6#0j-Dc#1%wP!&p~-hT6OS7+u#H`QhF4zVm&^q2Io;WK8TqSo@gUfl)&{$XzG^EpUkW7O_@^NLN3RNCc;85bf@po_x>q zFbsoe_dM*kUv{W_|-7L4^6PKIXWk6F0lO2kHu z-8)_+6iHKWn8f38n$0F%qD|>5HxdENRjbjKpNsGNXxrA2c<8It=Pf69ET0)!Ng z5Ie!Xz5NFmwocxE53!y*>FZg6<2a0sjZvvo2*=tBs@EltNe z^PM!Dc6g~x4gnC~^CGb?e}wAbsRV*derkk-Q6MslU8ucD{`b$ZjXHj&2Y=Jkuy{Gp zc6X6Tq+Q0I7(QhEy^=$+26!IS?0(SSjnHv91DT~13zLkD3}YDP^a-^f2*7SKZ{Z@0 zTdpSc+>e2F*6Chz8)Yp+G8(1PXb`+2>lJ|H?&lAMsrw{aouu{`7vuivt+X0-T&sy3 z3NtV;06=qc4E53P9G1RkB|xo}q!?U+6ge4PZ(^5!MWEFg85yBmE~6+4$z*am2@(m2 zCz4Fv^JO|;ntT9z$(h7god;%%!qg<;XpGuLA3+(}L~_r@Sr76+&__4XyzL{%=UvJ0 zx<|;IwHh*g1bem-TKi=pM)9!pJL@pia>F!3?*{D@3~?3tqE8}`Ad^ng%x$4-**Wbk z#mOA8XawV{H;`HX+j4@TL^0lW6;5S}NdF>o3r;6AuN|Jsn}0*sn(Jr$>gTb*n{9*!k=}+p}dA zB!y~r0gY>JN6B;n5Paiv=wn+TqLUcfFss2Hjm%g8%Z*?pZbsA=LhA(@wMpdID!K>X z2|#h(9aJCqIqInwp+5IBnx|ewBArJ1;a$Y{ytv=?O(jPDAN~U+rqj{Ufubl>D`f;x z!W-I*cFUg-ZX6zWa3q+K0Hf7q`0+~!yU!qpVu;Dhh<02+;M*9vAL0M!&q)9OAJ9TV z!6&!q9C~;B=73Z9>XNgt-+eXVw_G$`5-1jnIJ-6?n^lAr=i%IWBi+yb=%5FDv@zo+ z!0|#Dsar_)Uj~5HEYf`L&q&_=I7}RTXjo75psjftCESjJ6j8u(+eNd=&dsDhdm+X1 z-;cUzC8>Y?{6Y6~v@xS7$o9fCv$vx}ms1`8G1x;$>;8@8_qT&RyF!)KHAt&r5^Udy zVYWytUQV-=r&O=evwRhGB}&(m_c9dL=j_PHTyQ`Fn6^esIvYbcomlDnsQGJ&2L3_) zMAz#YGO-KUYvFeE)7jJ4zL?2U%ult?^M|A0)QLX*!$Z>F(ZT@GC~G3T7_`QV}Z^xNNejtDqJ!9-P6E3jfkxM>si?{CGnZC7E5_H8Y}wqIYcXsokI zrx)W{zsIPL|Nh=AByY~=v`I`q1O&L_?YCzF0A9Q>Kq4U^7K@Oc9w7)IB*YVaeOl=C zB-@t1KMPu&1lu=Mk2+9Cm7BzXK)?P`AMfim$ZI1^KcuHi;qFf0)~!Co$Dct$LOI&o zHF)hcKP+4r#5;KH+Btah%`jg3j<1y1b@-i82Rf0&0iaSzQCN5zadD?{^X4P8v~(jn zx(p>HKO!ST$~#!CmO-iHw*xeU-A8_DkC6L8&~4-d_IWRCo6PR;;K(US1P) zI+E8v2|-vE17K8MejhC@-Msc=$L^t~rgPY183Z?tBOqD0uTXW~Mz$cR~@S21f01OUigU2m(9 zoI+O50p?^2v$kc=8vN~KvaLdL3fT+*U>mdcyFF_s6a_`Y zN~NOB+SaXCS-%h+U528fBxGbr(c1c$GhVHh!Q0yn-+tS}X=fk+z{gn)_6T*{Wpe$i@sp&+1elrFJ^q?pae!F`e z0N|s{i(?nGon@Cv*qk~$pF^dh&6-?Jn>8OFH?}`CbPD8hKg^!(gW}@5$jxoUz<}|c z69d7Ou7oiK00096dgSFbAv5zl`ubk*`a3$FAvw7M1qC-ik`%9>f$)#Aq_GIs?izCd zjLOQ|c~3(CaP(*!8XJY@5JnUw!pFPbbqNUL24LE>DM(156E%Z_2KK*Qsgwzh4+`?& zW!78)!ngroURAfYKE{$Ir;(CUiLS1GELaf8nWPzIX3DW_*>p}D1L4pq=Y=qC0F+88 z4jlLaix;0lYwKgw*LPvjqEb{;bU>ky^2QIBS^Ma(&I@5&07xVR3JPu@FRuw&t?{jB zZ|@+|)2n$^7lDBucPj;YbQ^ppr^-}YiMr%8J~R8f`I`&NK%AN ze_0OzSf6Iuf|BvpxXl6P^x%Opv&NV;e}7|UZT4&*#Ks0ACMFn-jSsPH+f|NPV<0%o zmEdpy0B&aO(Ib9lt+#g&t5#L><`72C1i>KzFgkPQzTjMO{P;aoR(1?~ECbqT=jT?TpL z#D4@|S*X==!FC2hah2mia998$5peL}Eo5icCU#^pU&O=&L#2`m zp3guM*jV6r5F8c&MFDknU1s}hYk%T=&diiUB%)KCQBhL`k8wl@4rjzHk&sz4&8(d} z_bW~rq-kc2fgl2f_g1!yY1~3`*ehT-37Iw1%oQk}!&$JS@o90h{Y0$_Cd^nKjA)yHcuF1|ZrX3Y^GI6Z){FfY9E#xwvxf4`nJ zYv#642nzCmSZp+F41{Cfj1L5-2cS|(K@cKbzutrB=rW|FRHC!EBfRKT?RRH4hRbJKyva7s8qu5k{AfbtC!e2bXHCcK(8kS*C7D3ksDefsZdDa z=0+eq+y`-S!kIM#Ng(_4jbjGEDUFyVGA)DDXmrr)DTIf6v;8F{+|1hb>)puDZ>A^m zmkDxLzoZ%{QQg? zjOg_wsF!ahgbKg_dVRBLW{rXH^*P%^m=FNc(q_ZcQ;ha@4U&^9keW)DLC%>Yyk>3P zx;aQln8s-{kObcQqwOI~2more48_HF@%rnfXl#6l+S;EG6;+C=st!R@MwdYfn>AZQ zaCra(Awp5n9b{+Mb5`H<^b8^`jb8@IY%roy$;_Iy?9VsibbYjK;?M1&7-=`16iUW_$6KC}r0mo;LRXx1!m6c#2SGSXNMZ@R%qRTX!G zktk67g_whlK@_d*zzE^&rQsn=Xd`Bcq*7lL6(vC~_hs8cLW~n|L&GZe701)3TGZBd;j_=K zL8~R9)k%=`hs`M2U5NCge+!;}@(LJ1_wQ?dhx=5Q>WU|-mXDv>P)!1i#h$BozJ0A)ZoaGHa6E_Dg#O2 kR9!Tt1!>T5Ba+kie`fFa1?)BiHUIzs07*qoM6N<$g0QL*kpKVy literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/BIZ.png b/assets/images/fide-fed/4.0x/BIZ.png new file mode 100644 index 0000000000000000000000000000000000000000..000bf146a4d1dfc42b6e75e06ad2d4157b901d40 GIT binary patch literal 3510 zcmV;n4N3BeP)&`bVFh&Ad(YlL3Jk72U~^3?A#SoLfasww&8%$dpLxCn*|7`VT4m_g^wdzr>0{A*HdW znzdg%f=nW(z3&{sY9o8g-bF4^P}x>Q&rmysjR%=vn#8=URBl-oi!n^gi4(1UyCeM4 zfV4CdAAGcc6YdxJ_|)q(+v~9pb>a1S=y%##ne|7Uo?$*H`a2I?_9wC@Eo*ox7Wp@#M_aH<>(Z_G}-HT z>C4AB(_TV&pp6A_H*>n9n9nM<@#mcFNX2r7Tm!5-_yisO&0G-(LPL zE~kgw+@^#G~<|W+3byMyk zMU_ZZ-x*p*x-iJY2(_A6{$4uL02#lX^9oPTe-i+=$331F!GS??b90%Vn9g+L3|brwRCb-j zCkY@+oyw7}d|d5Kq<88lch=E4(oJz=9?{Mjn9XKBulk7h3tl8tW5%S5BvfbM^2?(6 zU~4raBVM7U`^RbMlTW1+ePuDW-~=}2{hp1#`h0Axnrm@;T(~_>3L3vc5+LDN z{Q*3MH}luOoDfj;c?3^Ep`l7vtVlwkNM*;#*HOu|7mS<3^jsbrOqA7zCp3#Yv{NuB ze5mZhhzCThEWVY}&LWH&3uzNmxK$cNW^WJGtqo|EasZ@a8HZ~>;bij>Dq0IjkDkMg zE2r_+o27Jh4GAH848c>-gAb(=IH8fE#zO=tLs>fQUQ{v-Ue5^qr;F(o97_65*>5+hXFh%0I%n_HLq$diqZnv+k=WXmOk+w_+=i)*#~If`tr$um)sYYR0UKqNM3C zixO{Ve&Q`?CQik=_}4fG9O#Tzbg-oTP?W>&(b~dB3#8B7gB*rsUEKs&ET|pp z5&KRcw|E$yq-M7yft;n+(AQ9nKEgue++We(+k+_J{C`a)(~=yKNp;6b%36*Kz5@g` zz+%yItgD=5Ggk3w`4+B-%^B;0G$AJ3Za1>j*~kE?REDXu3!TJ8>)v;8g~c#ph-A=} zh`VU^lr@-aS|qYKCh3m? z;PrZca1MY_wVB(}9^+X30q)Cu2CY{1bIJQj2!0qR6U!-UJ%LeYB|s#;X!LwB6^TS+ zLG7z7L>Uo-+!T$otpWEi47fuGk$gsXaU1x~8z2&i&d+dBi!-*mj)PVke7n=^R;apD*TTZOy_}K#&jE@FSqtSi$ul9jvQZgQioKmNTqM=y?@f8x8 z2VJZVpU;QS=R>2>T`qm;Pe>yw0$nUgG!L-$G$5P1&q~>Q391A{4HlE3lnc=*^DPReY*QU zBlh1Qr~yZhG!q~a(`>Kj@f|t5aNq$(yx$c~k7wlD{D1yBQE$|+CGRl9BVJ-mI>PlT zj9LX@athFZb*EH6KB65N*9kp{9R|&JJd;D?Osx?*$lY{814=ddfX@dX zeRPJqZ(L2V+Q@*@L7X9l-l0w&{AeCIgA1t$E2Q06P5z;^xa}g^BhJ!PuBD`H*|%pz zKJfLB8ul7NmK}8FDfzl^0|T}`g3czAB+lT5+aAFdl1PZ!gjOEJ>-o>HWa^z9KO8RT zIS}~4eL!%qf`Y=^P^qN+DfeFbopvs@&7;N9K9 z!OqJmZCsAeBO=MN4RzQ-j5-UZU?XmC5B-C7TAJI@_J{D=s>3w&RAJUdkze~M*Qcyx zc*sNAC0hhPju!krpu2mRO`A@#;(<86Y0hVP`hPL#>SM*MH85)pT}@vTLu zi`&y4C)tukfGB`h))xvMeDDJU0M@-!#Nw-MWX3EaRBb{L^xfMF5=8{7pVQG}zX~sh zxhx}`u1W__PkWod(1`%_c2v<&xfi|i6h}|EscFoh-J#{ljJUCu$qyn=!ij<7&sju(GY}U~p)Qv)-!WNQ1NmCwbnT`oTsoZ^2 zF%=d5aVhT?I{@I+sZQ=&ae&Ql&ZE4wfXoStkpxI-?Q0-4V*!x^Eyz703XXlrmR+;? z&B_q0<~{<%0hBkX$UQuXW!Ey!k%Bw#$n~4x zgCFgHQRdILvGmanY+NE`ki)Gf3}pPy;~UZx+!Wrh+bu& zv8Rg1X1~O)d+T_7#x|BN+3R<~2S2!!{~=mh2H3W(617@JO0t=LS1-wyEX=w{c9p(G zY0EL@#ot7~)6PFjHlmdW5fhR~O=l^S49V;*dly?s0@3=(Y~Fl|75DC^p}`@vtRKYh zQqaXjN9(xn{$z4;CK4DZ$2Bs{!~a^ys;kx$6Ou?>R|RW6UCzVVFEHJDIRN%vCp&gj zv0=kEejE5v{N8|3~yW<@Y_W;pQ)dZMF(P^-`P5M2&C=Nbm6svO|(;U@C)n{m1P z^L6x#`9EX8|5yC32>;R;H$Y&<4G@@d0|aK=0D%$h4oV&Gf&@e|$=4$E8h+7?ryzkD kH$Y&<4G@@d0|e%O01~V|_|4;>%>V!Z07*qoM6N<$f_i$bPyhe` literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/BLR.png b/assets/images/fide-fed/4.0x/BLR.png new file mode 100644 index 0000000000000000000000000000000000000000..cc87102a65692d139abf9e7ccf3d8b20853b0382 GIT binary patch literal 4311 zcmV;|5Ge17P)FOb^tLZTL805u(jNz)iJP12~7Gzs=b>b1>8 zXpN~;oF=t_&a@OYHE5dzC0x@~xnxGf6a{1zL=fB+xh%`x+5P>o;ha6}a@jp+0e{Ui z!_1!D^PTgZ?>*o9y_e_xU;$Sw_!jd9_I!8Xe;u6p?tm-c@mLTF+3@qTc7JHd1hv`> zqw&$3%nMooUS4dZqh$dj*Xeo(V;^}haT)x7TOvR7|6kM&n-fBwjb845rW5K(Yl^qZ$V?D8t2XpM1i= zFak(R6Co!j7#SHp;Bx5=`cyC)EhsM^K|z5MwY8&exreiucL8kOD#n2Wp;)s5^TVjE9fd++L}g_hW$?9HGdw-5Yr(*P3Dwmq zyX!vtiS>)r5_Gh@zY0EffE!Godr z)vrRp=TlWpZ?6Fb1xl2Z4C4NMBLINUXQ8YtmLjOnJ{v(~Bo|3ZUI2hIX9lrjN7uCX z4GraD=gvjQ$q7byIG6I9UT;Buen0Z^?qhs>ZWY3u`T-FUJk-|4v-=y1u^(+HgNiMG-dFss#JL9ncFZsY1s7l8*%KI0x>Zy*t4e_ckddA-}rnM z-hMj_H8pVv3*%0673MK#0@$!Ygyv=`o_NBW_)f34ATO^UiHSE*R5SpCf%^9C+ZT$g zEMMaItFOk8m313>z3XqOUT?vN9}Yk&y@CAv`;>cb*dW5SYl)OTx*KyMfbH9T@%iVm z2nZl`_}5=+k(AVmg9rPd)!M(#k3a5>-~1+&IBsdtA~RDCwVD~5UZXMNz=3;6N@~S_ z|Endw4-Dj>vN8_aw)sxenmM>#0587ikB>i&hCo2x>g34>ShJ=b?d>{;ZT$Q^P*SoK z9FA2@snupY^Gqj(hQ=B1Ut60FYu2=*uyBC*P9R{Tv@{wozUa^R{U*b$0{H&-eNkAr zm|}fYSF7;WTlb*TP5mYp6)mBdkh^wum0A zh7Z7Mq81cH66bYx>allkk2|^ui9~?o$E|(H&ku*6ANevEjTTtG>8__I8|Ti&*qN?O zfpGzNc(71hyp*!w{_JNxNJzMW)29b1=A&5bf!y2>$mPp$>5>E`B}*Zbc@n?>@|V4g z2GSxUdHBgs0#RR|fcAD7e)?1DEHs;e;^K#hi@T23U%yLy9~j8N>C@2^Rmv0?_k<4} z3PozFh&VoZ@&W$v2PFXD`RBXv?z{JK=ujxOZ}$WH8}YMPR;$Mry}bsQ%|KWf7kzz3 zm--SB!Ncm+LZqY!v2mja$;n<0&oh}UICpLsM~?L4`gN;C^uY%Ykd)+wUAqDR0PEL# zWB>jTyz@@KOMRINjC#UxaeTD2$OtE^rA3R?tJ@u#nK3bZ?A;rLojd*E>ubLj*kWjC z9IdTdbafe^R2rdFn(*3dfryH-F4R}9jG?r27{S3D1O;&s9nFVSDwy%QgM%g%7Z2jZ z2_?F^>_ZU(0UHes62jxw>n&KhvXxPTfKdTdRmBlrt6pzGQc~-brsf0$0yeg76QihT z34}uTyCq{|W>i+HaO%_`%F2dkbn0X>Ph7n!CBotr6)I$8$QkPoV=CzBr^S?z!Leia zXB>P0(CN(3YAuw(|M=q}ELp-sa70 z{`U&(+ZO@=NKf}cW~LbB<;+A;8JhtO4GBa@`OX~!5)*GYR6UMhJ9C2$fe~uSQDBO7&xQG|d0n|@M<5v7}x z6HI*W?KPmd_~ESEZrLIR>l-bj)tYhk><|-OVjO?{>j3WEBdxK$dxIFcE^Pq>26B** zLB7ES1xmZ9=5%=aX)*D+re<`(CeZbI3uSg@W{TnOPhWx17Ql`ji-;Vq$z(xE$?U@h zd_D`Qsig4A%H}scI1@@r9uf%~9*>1*pQRrsqAh@|EV2`ol`0~cG8?R4FM_u>8Lqf^ zabXLfug{2@8dC2w5vZdj052~#W$`_Ke%Pt@ww`H6hYmeGhH1`T7_fcDb?bygUfNaA zYMqkeP2uf0OZ8Z`OhA18`fKepXD%Gn){-G>fq;#*Yo}&&%m%atFo8RF3=ZA->9Bk` z8F*`Hc{Btdm+L7rb#e^LS0ZdTDC(z&TT+Ci9rV2o%Vm{&SF4&sdB9R9zF00GIIDdZ*1_o9V$2V{4Fg7-O zFIFt}AX1TLGjQ#ieY|HnBqWeD^`W70rwsr)5DM88)w5{PHJ2G1F8k6V5q0v=(S>X_ zh`^dF@bmM4pP%dP>B7gy!({<{TcR}2aCrudj#AZ4Rh7!Ag)}UTiwzsBx#RJ1i_=li zj0_*5M5(*mfckp1Q|;*>Ab>-mu+Uk*Xb6CM(__aJ6k^j%NJ;U=<;xNPRwl(uFWqs9 z^V!rSB?M4kuXbxbU@|f?d?*5-Lzi?}1sMz$%Ay+@>Kt!oWP~a}AD`(H=nDxUA>^}; zj*inR4y3gP$mOm(B*Vj00R#ojEiIf5Y`tG+{h!I8B>>x-OC-)YBz=7b$_$dp=y{ZL z3EP*gql2CYPfGx8ZRE{Yt#U3ZXf^|_tz`ViHZZr)@tyCGCNJF(3M~QD)sdFR;>A2f zM>{tQYy)tQiU8u{1&E9!ncJ5y)3d&43&7TLv85!=2DSoNvEtDXfbBEtbY|4oI~N6> z3|a!vXv}DAB;NpMQ*^d;lch`fv;$jn0bA;Pef=11&Jk@-*fvGCX_Lq)bF`shjN-y? z-8wgAL9QY!j7ymTrKR*KG_(b9=FA{r2YYz1uyg04S+_NrEU2g;6Y*QOil;ey{_(;K z{scwcU~t}hcLdr3c<{hP;YsG?1l!gBP6t~m$`em`&&X(=F9-x|y!j?c#=3ZM1P>q5 z&tWhUj4LQ02}|MOT>R)qvoCHdFIT~2vd$nbmxUKzaDQQ(D|qQ8e@YGUM9l+TFham{r6_ocH>47zW5@ZIR505VT6To8KtK;G>oCTT7}x$(ODCRCvfy=IC67Ch~vym zIX?e<_JPOgz-WO+MMWH*ddeClG#IP}8n)iDBY1kUv3eNFN6ew})mg5qo#>ND!TxoR@^?D0dtZ1bj|FH$*yQLgH9E!K!4kL~~{BQt! z_H+}+t5$j8@Zm6Q*&-&2^c+E{G~v}(yV2M<=CDZ5wmx90-BKnO=_Mt35mb0vn9aby z{xytu-sz|0rvLJn#d!JU0OI)I!9L{WF6?0nBELU7hXVpb3Be^8gA92hi1}2a5&N)sfbeLSckV*6PsSp3MxH4zaO(?A{%S z-Ma(qRF%nO?dybqlu8qllUr%M3{$}^NqL3Bh#&l*ldv{eEKmktp)evRrw36{E!e;R z9ziLwSb#U)xJwjs2M2ReT)f1g+MtVDxAe%(?L}l{3x59dUP?Zj!(maXcCxZ=yJheJ zH?krwT~g!OXFCZeYXTJ&D#XTK$B7e4hdiRDCJl;;$el|zZx&NLP^K|DI*#MV6^Mzs zZf9DKk6W;FXBQ>x&NSRgOZn`x5xnwBH)R{J^mHE_KOQ+PuRbTIhq8yy(W4QxctbP6 z%ZrWU$0I1ERwk1LFTdRFbj^daa62#PZ-0A;?Ceg8^RsJL02&(;keKK=4XM+av13OU zVZQSDEd2AIF+`atlaR?g@zqxe`0nze#Bppa9~Upi z&gfuG2R@&LS6>Z8Tbm5Ixgiu{?H6B+B00IutvY`)=9HS)*JngpnjD7@_fbLsQBgdc zJQ;=Cx0fR~H-r*(eDlq_ICYAwyi88^LPbTK(=np(a4z0@%epbs2OmUH^13>m83zyc zVbdnL(=2bgn75sCV`KUF;~%4tp6+<#)|#5pZ#HgKp|jI~vuC3z=|F?QLe!^CPR8cVVjMUSf_3YruC?mvG2rS|Emp4d z#F8cU3vpGI3h%vV-MTnF-Wt08?svV2Sdk;t)v0mhNIzpyO;<7R0+_(}z9&LXP7pFP z#k6-!p9%(p1s5+`H}7U*p9dzeU<5D$e}4{s_(MNrXZvC8S|O#5%oXT#X4KV<;r#g_ zeDu)}1_u{bDb#`!fGs>d*;uzuX!8#otbY^L^^>7iPy7Q1>ozrYb!tYaBksh47Qj^C za99wD*zonW26KmpO&A?D+l4(I1w4`h`2U1&OO)mr{|DJOW?-2@`e^_F002ovPDHLk FV1nrP6o>!- literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/BOL.png b/assets/images/fide-fed/4.0x/BOL.png new file mode 100644 index 0000000000000000000000000000000000000000..6335fab640377a28af2c2384443f286c01ea0953 GIT binary patch literal 1679 zcmbuAi8tGc0)>B3Eh9**C6uSq=t3~Xw5g#&)NUT(8BeVxGNTfLlvrZzOKWSjRYt3| z)wD!PZ8f8@mIP6uiESdqs3x^l5raJQH_SQryWctYAGow1Jgz7yz!U%gP;z#1@H%wx zAx`qg4*MJ2TG-zrI{ioj02!;l0iCj|K^%_SaTvciZ$d;IIV3h5Ad|^ReDu$xun=N6 zk`RmgWyKl>0LOHl9qh0-%05s1H!7eHL={SBw!{-1`MiXvTxZ?Tn+|aN{DpKl73RNrNc|t?{DQ?U#ZKXB z{JAGWogtoBqk!Xc!oT|mvkK1AbMRV!HT<~C%B{Boc5mW=Lve;mip_s~sLK_u&G=F6 zbzf^TiIV*QXXfLlZ8YFZU!8= zRQa2C#^TDb6?I7;%g>BH`xnPgj?dpv4E!T>&y>8@Ux=9=y3oh9n%p~vOq~g4*Wg+E zsvT)Z$b#|V{PC_u9t5P4>LaLww26b2&Z`t_9g^5H-H1VyuZT>A7s?>6AX5VLk7d^^Rup?DT^b zCaZHm%(?V2xt7}WYi7M+f$G6dVydl$Tf#nYiq|v9Y2?Bd*|Xd4&gwp$;a}c*%iXlu zI+>k+3GbRaWvKB*rqth`A5?b={&UYrMm8aH2# z)P)Mas#iph#RokeZ9&;48+Ef+G!t}I8Lf=+DM|>?X}h8N*ImK<@-A;@`lY=!iSj7! zW$z5fYatNdLu)4Xh4eM;a%5U%4lq-iz6AjDYS2=ByX;%o_SiYoCZ5zXuLx;mOVrYV z2p=`x@D<}a%I4b05CZ3veErV=6b6MVi7hgGNsmu^~-nrGP=2^uT#S>R0Hg&t!;V(`E!7jY3o|rFIVx=jBOrnLkbmKxE zAiHxE{K7t%uWDpWG_iZLtzx=B!6)KC4;vN=Ro!|PX8rHI?6XWZPbp=X5ChUf(xZ_3 zNZIt{j-y1UxnlRKH+wxB@(s6N)$+b5B04~A?W7Y0%4uQC?G7+%*R}8R93P7e^Qyz% zoUzN;>}rsWecq)QoKs8~ai>MWuuJF}|LBeD>u106t;D^KdioLseQ_<5uBbBI=Mhz* zw=W}5NxODsuo9(%Zprn;MZWI(O$b@gyp{#`mCsUlm7dtj1Isgln%M~ImKE2s;=uO9d*69WX7=c6cF=!d&iXhr#{?y zJ>g|tbPgN`P#q__^XM}+dySH13=CYy zXOSiM19jl(kTvOuRqFY=Bt_mUrk;I+28tT`4L>Wlg=Jwe)i*@^`V+C4C}QS!Wh;tL@;h(N?<*})ga1%U*Or^zuc2< z0d?H4dj3Lkp4oAUyNm(u0{G2XyLbD%hncpgl4Tu%7Ma#pwQS$YNVJs@M}1>o$Sl;z TXukdo(0dG?u6{1-oD!M$P)JlJ*wxb z>!&)S*j}om))W>b$-HCCOhHOPV&kL+_AV^CuJZx%V!HYLqSI`g)PO=5JMYw(7y&b) zdU!7P98)4pvdUNVgtNUggK|SefF<*tQLTVb(aGQI>iALmMUUTVyCm>V?F9b0Diy19 z^bYhBpLVXH<-UShbCxOs!#6a?6Occd$90 zUFAButic}*L6=~8QXAW|tBFzDJihl*XAGvkP(su;!b8nOM;K+dIXAkS7b_?6QDdUV z?fR3Tjevv@E8F$eEK6#aRlc!3j(0!3mE)y3GewyC&w>nn;*BEtHZJiduL7Jk9% zCC3omEZ~P*V|liG3eDyaZy&`cMqB{`urj%YpX$#OrL?;(|M8dec*B#>}pqD)M8y+?FjpKZ(coY^Jq4+UIr!L1NLg zQhxE^zYwAx+Ie@fBZ_CrM%JA%A_@=%iMu8?^TgB|Gzy2?@;lz%z=1FF{VqqC+RCrC z>|wsXV#qRPr^stnsr*~*1RRn-Z86sRl)j{OQ^)*b#1e$SikET zzAV=VL>9Y4!C#IqK%sCjJM)~&GD<;WL0lKNBy><}h@{n`4u~uR1WW-+L1M$Ci~Mr> zS(3vnZp&M(YF2IA&BfM)K!|e{U!BdO&!Q$jXXufT5MpD^#Ad=2PEK`d12!50q5!?N zm)B>Pu`0P$mK#Bm1lI1@NmF~gzeO36;%XgXAr@xns)sBi3NSmyz_O%v$_)`TTl{Y+ z`E@58Dmr;Ivz8C?i_mL(W#9MMp3T%XC;3wpS-kS$U6fQ$c3VytVdCBS#XOr`g+}Rq z4f6!QDZpFj*^%Pu94?yaFEKnqk_5KA{0}%~cuPe=Vr|ML_Afl`S9gZbQ&7M2td?i8 z&wIR^bvi}v|M^d`+El)X;1gE6iq4)$mP{{^RbH#IvnsijDUl{lv`1s{(besf8G2KE zCp&Yh7#He(k#)tP(@#UhU7Rr$F(-Zw6~-b`HB+$LCHi_L%)Kf)yUcWT+o^Bt;Y>{@ z#$L}|7Vc0e9Q@BKKPEiH?9qMg7M0KL46jB&l+w<&tn)mPRYQaibD}!#U89(t=@bDN^w#qzp zp1I>t!F>L5vH*k8N{=yv66b&`It*>ZOd`Wov}@I9V;mUcRV+(fMM7M^8ECZiVs+SP z?rovURE~bKgUqC6R{b!ChN5`h|8pf7>5)j1M8hR{=g2)K4QDQ-l9g8H)$?eT4xY`f zV&%A2kK7q<6)@mDdot@h(JJ@OGr1i2T*P3i>~BK=NybW~2~(1p-oT8kc*3V^ky5&N zWX?l0_qI?}@-FF7X{d7=xqGUGbmJTfiak4u{OH(1uN5%BoY;PAP_8>;*hSbA=b0xs zoFelc_%#3vW+iazqH*kgy%dMTGkZW31!n3KShh5QMe`G}izW`&9YH`q7dx3YQ#%R0 zGkEpwGgQ{NHytxl>Un?1&%AmjS5z3ndAcl<3PbpiWn7McLFZX!jo&vO0B7q`27gm$$n$=UkU!QX{t9Tb*z_*|#O>fqC3 zb$ogZ^i#}y_l|TFlX^IDu>iBf%B*=IsMDbQzf;+N$n`YSXZ641Z6G>hX+j52mu67h z9eLIN;laD8LlH2UbP*%yHesDK!?jl7s{)S@$BLJXkru#HXYblkoqS$6q1Kd7dlxQ)h^ zUS9Zh5qn-V(%K|4Ej}BKO2gM2WY3FvEWbJR>Kg;>{}%-bjnaYo3b*{A0$MF-}d zrD){7<=$<^hC%}V)R@S_r?Xwgs=9oy40Z)O%5~iT)wF<|k7lRWy1cGbililnlbk5O z&2zOqRMyI0vy8nq9)IB|EhqG3#$>WMWg*?F3;gz_<>cx88>Q1=3T5NTT%Ih+q|4^I z?qOT}sw0Xu$7XpQAUhPIAQ7Q4VKiw5udVxT8d;OIJoj3OthqQbE`&w16ZylZ^#Ek+ zqKJ)BbM#cJEE)D4xIl4PG^OLwGKS@@nW=_)b zPmj$SRG1u^TKgE65Q-o`m%)lcbnxT#dp(jJ`eTS*s!V6yiJ4w?KKO7ZECC-iCbH^S z9-mx_^Qw-&x&0HgF7^Z`irP4K#bO$xRijq9Ud&qC&Afl0#-kCCHCfAvqPD@k!uOUO zMWgoYH0)Gozk%{*%|xF=rmY4UH;7;K&?`6$1M{8$jyuTp)Z}pPUXgaW-u_OD)R=hk zhwsUjx#zNpes6()Omx47CTf)q{?Uu$iaenL;jyWAp(a}qP|IfsEZr~LE@cl z&yzc;MppUHTVvQ+uJdaowO`|c3|G7zHN6`5H(chqO>dDterWTr_M*|_&Q~d~ezZ;N zbR@=wUUlu!b$exXeuq`XlO>sKF3j+u*!^@A5-zTKQ+UbscPu=kE%0`MOKKY3T_8 z%Mmj>MfOysvhMgyO1j;- zc#;z}AV@s2ZXa8}zi-%jTP$iGKAp{@MOgt&Wg3z2^eeyZASXQi`TzU@dq2PBx(pas z2m*wt6@!cRaD=KYyt?%jW>0k;84V+!Q90rX8qa1|u^`U<2Cm{lDnEIBGmUNDnYdRX z0N=_hWcS9mP^pFvQ>pI@WqYZPf)3x4LS)Mpv=pVR^F+yrviR&&9((qEkGht`%XiC1 zsc1G{pnm>llenXz2@9132qP8qQTJUNUi1ptJIPBu<% z;DN~vs09B-XdG>eW@mW@jeWsM$R3JOr+@*5`ZSrR80-puHHyC}H)<6CzK2L2ZjEJU zna*P`?MM>zk97=(Qxe!)KaS(=(d^Jy%g)Vev#8izF@;0TBbRmQEu-ptK||4C3T4x& z99e#ugU#`*JeD^mIv+;)2)LGm&G8g;M!Ed9(8az`c9c!wi}siy?jD_ti4kxm1HSnO zPvy{L4nYF;){kTL7qiA(=ffypgxwLLw3DK-czqx~8W>Yk(6t!tN-7Lv`Vq$76!d?M p8wv;fq}8y)5S5QV$R#^hJH+eB5n`6 z3-3<*Hf`NOCe99liwO^%dgf*{Iac~Jea=~Ke9D0RX$|)b>yzZDzyHLl z!ybPFpS*NFQnmN3GQZpMPR3WOreF8CJ~<-pi&FplMQg%*?f%`*tZCSl^-t_)npCDi z-xXPxtrhpLuC&iy&U|I+AIVCi-YfF1yJU_%tGssX=HBbK7)qZeOMaewIbzOS_Z+j_ z`S-Pt=gMkny6@ZDe?|W2rfSivZPHf0G(fRK>&ZMEI`BNE;N5<<2ILq6x^ z%OK@Tv z0VmlpT~a7g%El~6Iwx=+0P@OwDn4{?kg9TC?hL#qUcM_PyIGV#{aW&z^q zGV$}LT?I&>%fwqm#|e-?mx(t;Ckc>9yP3EI#{l7Ozt}pPqzDjCTTDE|t*B|Ct46p?}IFF^GdwAjR*sDeA+ zA$Yra(Vcv_!0v#iIlGA)#e$l@>8L>m-%uORKs(8CojgRR1&(smTnvSrnl04#>{V=Jwlfr=XyK@#Dwic3*fi9JA~w zYFa9xjoVN&QVDO|j(6|?+?V&kC@#f!&7iZq>%yIhCkPOa&*x+8*s&B86fk-6WKvU; zdlg0Zd`->!t1u3gpk-wcxN#^>D8h+17t-zV8B{G;f_d~ft{&Y;9Y2P^O(UA@>q080 zHz`0onx-*)_;3me3Ya=|D!IA2oe)|Bc3nMXv!15%&4qB9T?K9!O6;2hSd~>sJ23?f zj=PE6r+$h1@;=S>pX_paiv&2GfddCpP*A|MY10@uaA5mIBPA#b0CsgPRqwpVu|K|o zdF*(rx>Ix)nfFg2{l2^L^zRFfL|PVFcIMd+pM-LHs|1LrZ{NO5oH&t!f&xa49ElLE zpN@vN?BMIW9s((GLSX>Bg9m^RM0OONS6}Wvd1&2ph;025CmLQF&=*^fwsXd#=p>5nLK-Iiq1S>c z2yfnw@A@GKj|V+3m-HzU35*`zijj4kiMOi&@wCOnoluy46Yj^VIDvQY0HVA1VU`{x zc-t7l+l#O(PN3yvVjM05z}2H$<6RTDX#}A!wqn=TkurJ&P9zGd;IfyV!PT>SyNs

    n0&_ZBd9+O%d(+$b(Z&uiH4L?V21*AI}kjh@?sn&qny9uIxq zdYRH$PZQbx6#(vjmvPyvf1vo$SwxHW;vaq;uAbeoPgK$Sg{RPa^~8zAsQc(sQYVaS zHePAln8%J2E82^3tb)w@?`gh$8%?~!1$Z-qQbV&gu-?+%3HCjg#Ga* zp8J;t^qe&lcfb5|_8Z;%4M%@B2Q?TVecI#}J=?37ET{V256FN0xhC5je`Ow_^_x+2 z4YRz0%io=Q&VJ8I3r$=Fo@m_>TE+;M4^=2wQj#MyGKEHw9c|oSRaM1?4I5ZCcRsfr zJkFD4`_No2^7Hf2w5FFU>eqfj&C(SOWeEu>x7>KnScKn4$>X!hdg!|dpZARY6@Ph~ z$mSiOs^B&kv|jkpj?&KPyT11 zmz?3vlwYLxm$$eAXSBQ5ohBCt^Be#6`FwKX#EEk5+_|#4x>}x%F9+W&x29hqV~0!6 zy8m>XXiRQRze1M3^u`(cstZ@hZQTdUBTvnd`zB47J1-wDEC2d#i~2oZ9Al3(tA8(J zWw*)>Uqf1FhH#_cS5$f`E;dF5*Rfc<4_5*E`$>^2#&j{ zEkkcQ4%ZcEssD=JDes~6UO)UVm%h$9h7AF)&S~jtL2O784ty)XT5iHt<4 zTD+9-mqp~g_%vE(dg}+?Ld34MeR%fCD;YVg2SO+va*z*=I({BHf&yHtoqG0OCQfB3 z8H!4l(C{kfz9x!_idg^lJB@#T{N`U!I9+s=^4@{ z-FD76+5?f_E6j}P)vWwqheMmPcPbTLC7ygS-^2BqEzX z#(vx2U%c4>ycat&$3AJ|g@uJ^n#RpThw;2x1p@TkZl@aBiN{eoc@e5bCah^Na;S?x z{3eBO)@QT%!%Sw)^mA4JwtcK9{@^8!2|!R$`QB+6(%Hz1^N${)`8X`gqVLw-1O`M1 zj4>(PyNf5Dc!Hv$BBW(C``(VGRawx$D>QyJGr*P=nG}7J#qWQe!VQ1#;(RO7BJK{@ zVnxt@%0^gu-Ufxs?NjRGKg%}Q2p>}#Sv!Dp3R^^gLv-Q zKb~LE?M$1~&^~!8KmDPPRSPmWv?=?%Ok9m?vcU@D8f_5Wr=lwg&-r_j+C*Baqi}Ux zHg&D$ZL)2OB6#E09C9XDxOzLduQyq3oSKQ&RwtH5X3d(_q=|O~?K%%Qmx)KEli&lf zxYvf1e0ng7e}qNmPht!nBX}{8OQtyU#OjKI06sA4SP-j5q2WZIPMV_fVxSk92fakT z5=c`r?{^k8^3?KWd<;=3S+L~2CQZB}P&(tEb(}QuEw3x2Zt_wVvG6MjJFO`5qNlFF z9HFstY#yRqLHHD;1qb%o{3>*mtyTnpeyYwbt^ikS9vle)A+RK@H0#)DMcHjPT)Ee3 zUY_)Kr@GThaLXuSXn}w?|agjJuMA2%OUi+hFL1`jj(uoX@q&vD!#U3{M?tr zFwKWcQP5GSk~aHogI|V@FhF%P-IKwDi~!z2Hfn|>b+SR(FMYgFTFIZIClZeLjK2pF zDUpgKP0Q1X}$y|;sw(J)}ML|{w~v+O{aMa!aW*kXbM&!^-v z_17vwcZeNSF}_x*U#lT>*b$Dh#flJ>lHS4v3Y0q*8Zgr3XW;b$;gv+TYh?X6h8Ma_~^X{x)gL&~iN)r)nkazX0d z1~rRy^vfL9l}FfG73IgCOau+J?y=S1P%S(5qwFnR6Tct?uOOX%_jYh)JE-XrMF*==!Kqj9USoqJseNCkqF6FNTE#V49>(?a zBD@l-R&e+$hezrgig5?+&iU|eb0(g20cw}&9Qln0=_m+~#C@er+TAAM%^G&4f^}RF z9B&|j+CrV!fu^6aTAs~;ut{M_Jwgf|@TAdgtc{*0F%AmC>ouIX^!`tg3SK+Slskig z19F)<^$w=p`=jIwQ1ObJidPdWpo$8fAr?-!;p<1WAGlBhlHlzIo*@p>0IOVpfa;J# z(E*b+`=T5^U~|3j@tr`B)Tqai@0@XZ3SU}LR-TskcP2@_11es2qvc4n?ha;&pk|RybZ^|uR{`I3Hj3`x z&XAXk-ZWjCG36Bh#RYkP4pH%no6vfVlrbiHABV_J6=^8A zdf6Cr60u_XX2gxEv<`I zfR3Qzgv0XH28)(OS^23!%;@-2(vG>l(yZx*|qRUB{fJE&*CVd##C|jV=LN zL3^5bmjG>~ttQ?jKs(YJ6YmnBgJ`LVcL~r5G-u*n0(3Ht0~@!QELjm_$?{m20LkVe q{3l=kzvvPm$#e;jWV!@MGXDp@cBp_T{RDRa0000;fq`YZr;B4q#hkZuqBCM#B^vfm zzUynYJM#97-n}XpCDw|r?bvpy!89vJSX93^_h8jZ5qT}Hrj8Dd+^on*QdT<$s>J{9FwsT0{7n4S85%#U~EmZQ9IXmMks*w z;`2}M`@S(S-uGI}vY6$H9aAIk&+qKl9DgMKKed=?G0POABUdENcqEk%a5_D?`RKUo zJa+XbwUr@>Jq-owERT-%bDKE)Fg)MAm?xR#%G#ubsh12yzGxM!;62p8s$0A=t@QXY zzE!7fuk&|1zk2laQAK5S)fVSqw@xf7x8}a>5ePo6Q5*mH&03XdnRVanK`##{eSam`zOXZ?fa%wy1n@( zA#;=K`yHK$wx=J6$XIgk_c{4QJeoabX501O4{xwW`23zgcR0JYXexgb&>rjS zJN71S_P=~}VRk_YgP!lN$?FB8*0iuMfAGNb$DeeM>KihbB!6TH@O^n1_;Z0L|A|y@ zTbX-oYYn#+wx>UM%xGc0&Ma?f#zp3$odz$rUD&u}*NkM(8}l!$|K#Y*$na6Bk-_&rmr5Ybn#}qtNSXy;Kt2077LWNh-^6Be5xv|)mpQ}!J2XQb*J0DuO4qZ_JEK7 zQCL?Q)2^s9J+XRGoCOVYeeWH8pLOm1Gt0FsckVMtD=n3s?REHL_lKLV-)l8ZTp!;1 z_389;)_Jj$blTOu|16Z*#pZtg(9->P&#SvXSe&~d)A!^1vaLI^d1EguV`q`6s7Zd9 zeCfLX0@)X*!|YQ0Q|Ii6`4e(%UD}FE88VmL9DZA^(BBXtHLua!Z)du{__S&7<0Fc` z%UZG4i|qJxO`wh4d&l0R`db7q%uRncpK;$lCjGC6{DqG`o-@B8T=-r2r)QTk%$Bq^ z6bM*_&G_4M=Fsfr4<{E-h*@~`d9vEmy61C^4eYkp)Tax#Z2588RPe$^Ii>%*SO1)S zKcQZqr)R4C_uGLLmW+Ru|KFA4KKL#ARR3xrU6v<$6V#qPxNH1QC*3Tx^3BQ0iucw( z9iDVS?Psg&FP#+SZyvo>AU&9 z{{6N~D_P$cJ^i=LLh?r7gGlE60`VQ8?03TKE#Ld^t4|MouD)RNiCu^Jx9nDVsm}N5 t|Mtilj!7=NK0Ra;U%(vJV1NhxXV`m>k$KGArzXSnBHRwXTUJs}fGHNpA1hrn2b2!jg_xB9*TsM8!Detd@mLpR_@5 z=DLYL0)9C7Yu#A9+i$tZ#n`)ZUT%I{6Sur}mi+y>=9`VrNZqJ9=rXD5>h}YaO1G`y zjoQ>F8`D{1aaPuKmUY&3oxUjn4SVm&z4`dD&B3 z`9QAEqXp}^ukJI5S+~SC_fNT)%z{-*_$zP5Z!EfLp>=)l_F^^n$btr``sY_3zuL4x z_prsqHN^+c@xCv3>S1hjSv_q}+@@ETuROl+PJD&d%u1twM)Nv1^Rj)m8rzi3ZE!Kc(@)XD_U2mQqwT+1W0ZSNGZ; z#;iZG3=RLzpAIbjYEc$r)%lKpCqs#4R_hMNQ{3!04l;wTvP1=H?Vx zYn3nUJrVwlCqx>22oq->?|qnhUf^F@4 otCN3ZOIappGR$NmhWN|2=gjoS+0j?`0y6=Fr>mdKI;Vst02CE>1ONa4 literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/CAM.png b/assets/images/fide-fed/4.0x/CAM.png new file mode 100644 index 0000000000000000000000000000000000000000..e8f8d84187d31af7009488f85201adb6c3a5e561 GIT binary patch literal 2532 zcma)8_ct4g7mr!>Q2JD3_YpB`ORZ} z%$}|g0~ii9F}xjHxm)QRDC0#{NGmaXH5IQIFG-BV_e zM#_+j-2R~$;KEIEQr>!c)-KzDJ#W+(FubNib|4qLf%wo558k$4gp=_}iBlxw;zu)$ zmd=HpSGJ|^WakZ@pAf#$zn_O#O2{T9t10tkDD*R}pkPdCDr_9D`L6+OQ;`4BsSl5z z3>{|m@cBQYuZb;@AU)CiOKf(sS5l!?I1HcI=PtUQM`K}s*vwCp+A_KExu@5c?a`^^ zPF$2epjk=Dko;ukcuA2~pAtu^iM@N{%|lU^nz+aZJDH{jlr-UJ;iV#GcFEng-!h-@ zN0=3wL}sGZXx#@hr59)6c<;Sz=tqF1-c!Ed^i5ylF3{~pszC};`N1AaWnR?9rj0!j z=PMaAzO~)K8}`eTuA8{;sz`{c@gZ3R!#`4YfElTCpG`_OTa_@yCN(&pv-GRt+e4RD zHqTOBS8mOH1+Fo3QcQjl&o&>&l#v!D>zno(H(kHQ9iM-(+lkgWh^=gHmaVju&LbQM zO~KY0ake4CW4jL2{-2i2m~oZ27*rHxfS0MJz-8< zr$2u-+qZJ@@F3*uo4+mi|6N>~OK+*W*gBXsDazDB67gJ~+{pnutmEu+_PNUZX)4Zg zn+ZWpOu6# zE9s+KT6&j5083VX8hOqi-9Z8zj&}`yWBYO9JSfHcWn-x?GAAcz<}GQebujsN*>{%l zv_|0%^T02|^mmdD)G)k_UGd9t{Xe=IGOffZuHD;h>G@j7YnMTUy3!9w0D2@11dT$W z3d}%^k}KhBW}SJ~s?ULSp2)z1HOIj!7}!|*@m7{}RSlwff9Qg^vW^aV#Ksi&$)a|M zP)|VLyTKjJR_$IR!|Ceknv<7TC&Rhf?oOswa&2VqTzQ(uD`C(|OBv)VC8myeVEg*Y zQ&XAF?&(M4A0J+#g~`61`gGN;SD?<{MNEiHnXU7yxTPSlK7%zjD;!>3y_Q|8tgAcG zv5faiS}(Qu^7A{L79NukrYZ7+evh4<-Ot~DD4?4hp;>U2OX05S-eWgdl^b2Qwz4S+ z?{g8+zgKAndbPi={PTEM(phmW8m|>j9`=ZHoyR$~>(Z7YVK7*}WfKYKd11t(0Ebg? zQM@{>f37?07X=kb8Q+w!9bIQ&l1|kN%_ofS^D1gS6m~&m?k+{R(1`+;zJA2MF4=bo`*?D2x_q<^@!lxZ? z1Y_S3D_^bNRysIcOh$Tn-5(o+RwW)ndBpW5PLHT0LXwVn@|j9|d%Fes-!=O47-(#4 z%*xCJf-=|C)v-VeDO75mqrznl){JhSu zgurn)w9pK6nbDrYF;GP#9~V2L(2Z|#h-QBAr0;x;%+M%k;qbe6{O<#<1S{gVrUz4I zl2S%I4DH@a&8x$mcR=vD8PsDB+yfAl(r<~iHYn)~?NJziH0~Yc`bL?n<-J@3!e1q1 zZGpKEgl&-%Kz}3x)^yyFPj=f?z|wKA zus)ZZ+*~YGo5AIlLom#d$Z3#`!=^*<$ulXK0!c2=!`L$%mV& zwhoA(EECPRLl7n*z5vAnz46rH_Os3xP}%+&x8~HHSSDi! z+?<@8M@^Kham#0BLN$bDv9U-Lv;tG+@#`$TLM0t>F}WA5jTfz6vD#Wnmm<2ZmD8gJ zV}A5h??yY$rG~eKvTUM`u2}%zcF)hBoE~8agz&G$ua^}XcLG;;dS)u_IrlUQ&Kz#9 zgs;wKmb%#56K`r0_qudc(42s8m>_!#(7SiQ)XA=`nF2M>2gW!O98IWx4P9MDol{jK)z@96k@4{vN9TIEUq`RT%D0~9k}A9m z-uip5=>NJ$)S%r^Tm7k@uvVgmBt8s&Udfa_@`~8`B8n*0c`Tf3MPEFyH`n+Y*VNXZ z^5Uk%mNigV*x2R~Rj<>&75KUvq?>T8z>`_GmE`N|do|e-T$J{D zLg8j(4CNAbrm#57!67Ea5yhnDoDh5P?NTY-#m){426LnvO-xM8%*}Pxay_{%rzye% zg1!pqzRZ&u_}WWVFrrTcYyH4P!cbGhEqabNU?Ht__)@Lcir%@E^!2`mU|LAe@?A{{ zvjzm(lto@kEZ@642!aMdeGNx*=#GaT-#z=8^fx7J90Ukf8d8q7Qj(HM zL>k$dN>8X%qc;KooE_x1CxUTxx-tYCao9ZjnuWmTju|lyGr9ZZpKjraHP$!$FPr!K zq*vz+_~g|bBR#O4kAAY1Z4eV-yohx|7STz@PB*Sw|Fpr{{w${-)o>Y@a%$@-jc{ZK z_(C#+rp*cE0?!6;uIg)5a)F1mctW9INCgNlFRNnZF8_yhach1f{g+HA5Kx^MC@r5g zFt^GP;>*g!mtL93OvMu2*tU+r$3uTHkXNBO7j}$_a!m?@mAvsYGy{c-f3={XvnBa> zHN)-$l*hB?2wSy3PN}7t3m(BCs+x9rcnVF@dmxh3g#i1i$MTLMDhP)4u*>+NJX5{{DbBJ8t zNc7b(^#(SP7afK z97hoFOCQ@G+;(91{{6(0Ztn3y3@eA@EgRh7!SrgLdId)_4@NEO%wY`g)@AR^oXBbB zc3g1^4TnY6W4SQeRN0~7w%A=Lw+&p4XWz1Yk$B8VbdCSH$lk6gq}H$dW*~i&Ct)FK z+_a>(Mb}8MxcJDmtA!cU18U6%wB3J<*AOoC4JqpvUcOvDkxWVWJVJh#=zwyCswB@E zyf>C;Mn`F&dPI(J%zh9vDxsQla-)!tdk5Nof-INP;12Edc_)Y9eSFPpl|9+3v81maH3VmT$+d0M+@^N1asR;l zu-Z}4ki&h_9s6mhY3Yfi4~C-l40D0&F2Ka(`gP=nxQZ=iMQa`PNTN19{O0%XU9Hta zE2ccmZ$ZS+~hN5|2O$68Bj&%F)vyyh7YD3iDh zdjCFoQA>5-)^1XW@9ezr2xpNecR}R$^yYWTyCq$$`OP!uVYpeikK(?pOQzD&EmA$O4*RznBNj)?an^)%|UkdQ3K8qsJN}8xySUcBdf5!AWp0VbG zGqyZK1BX+sq*sz86ApFt1OI+xBfT~a9-v1>xb16%7C-zS`Wsfou{7u|G+nJMAjlu@ JSMPI#{SOQFX~_Tp literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/CAY.png b/assets/images/fide-fed/4.0x/CAY.png new file mode 100644 index 0000000000000000000000000000000000000000..89612f0cff39a985c6796e6c32a7a79e5c2860c0 GIT binary patch literal 2479 zcmV;g2~hTlP)Sanc|Lqj-?>tUZvl)Ak@-|YrGlk;S zueBQw-m(F|g^38^>+Q0qRLFQLA&PZRjzW=^PQ`~iX|1bAH9i{Mn&o&syWcK*cN${T znRDdNT|(X?3#mJjfzqJISMSfpCx&t9z;f0uorIgK;@bsP5Mf1Om==V)3x;4+!{7Y_oA%X+(3DmH0UL=M4ma=kD%xw|E z%~iq5MKN4RdxqCMja1K^NBP!Ia2r0fUEocHMknDhsH3II+VOZz?oWWh`fXayEup+xy&)+l}00Ze{xy zC-^Ki^Lnn`pKQ&w`E!?8bFF7=48Fj|d6B#_e`Jr(h?}v=@o{gq%keg6Q*w5n%zLLG zZ~Jh&g&638Z$p0bKyQWU4rk!oBM|m`sjDw^R|ei3FkiiYmt|;iPSGvd>8liXrz1#` z)TIDDp(_FU8hh^*cN+4_ohf~8wEpqqwA|&*pTdARs8fi}f!|VR2$K zGTHT{AT<>tnX90s2t0-(Dg!{~fyE+`df*pcdFucdFID$?ZsE#~zxEV+?CcbsUZhT-ASazJS>d`$b26}a1TZFo(O^r zfVzejo?M;ESBK8^YJPctl!kzf03H)Y_e&W{F9cY*D28``I|ES^tYeZSAxX_hwWs+h z?;L)fZiKkyBPun(f7hy~%7uSyo6pN{?7yW8XlKZ~tE1a4Vw;wXM=hCA8uS1({E$iM z=55p+$v|{>!!KzLvZs>JMTB%|ue_-2mhAi~EvH{EVzZ}3+62G6`U;cZeFE1;fwS4A zSX@K|vqXTZfVK0(crm{HktvD-fBan>7cW)!cqepP1+{f2i@#pR)@P&f4+%hN(9`fk z=JjsrtOYdun1yyp68_($vejn{gA;aiJ7MsQ4_Wc?Pm~vxVJbAD8W(lF+qrKSs16*Ev}!+QOrA-^pkShO z{sgK$cp&(0d?WlAtTS}@j;G3n*H%8*BVVin^mpKI#!*6q1bVY@_DGv?Ns_22D8!hP z%j%KyiEfBt)2vk_Mf{o4Z`7xnmzm_0SpT58^i*=#0j(xWsF zQ{Z*6g~z75Ag=p@sTw&Y8D`d&3N$Y}M6P=fF+Y55^S(?b5J z@xEWD@V5J?Uak1AY&6*@f*+=hW)`Z3BPe6#groA>h!LZ1ovK#JpX+wQIQPxOsNl4>S06{!kKWK5xRTkqD)c7K;o)B_cPr z5@k?hl0b&g>FYxbzIOW~)*m~;`S_>W=lFwrc;9UVp^tsiZ!5k#tY<{rhrIY{7Ujid zn2Il3g$PUg4EK@u*lky*_2~3j;JTMl_-P2r4`;N;LX;;!bxg^KvCA=)hT`53Lcpq} zoxUFwVE-A=?GG~lZ`7Sy*I@-@GMV@(x0DIPL#QY{My)g)gR&JtsUW|$6^;BXz5x^Y zKJz^HM}>80ySBas@2EHI@@K#`<1Uxg5fN&eh7<|~%eJI4Hms7Wn*B6ii)B>gbQ+r* z$SXXGQurQUuQ)DN8Thsn0WO!<+3nAOYe!!wuIg}H5CqmdJ_S!@9$%g*VcNZoPLm(hAw^3t4%o!c(z*mhS8xE-+XyIf{Wwyjo5k7b}lPV9&g483bCGD|%h z=gLsH26FJ|U_M?v5uI8=Q*|X*&YVRve3*@m^uxJ!`!nFqfZ##i6dZopdKa)*EEvzH zaV35O#}gOu>KT9BTx2{p;%AC0j^OR;iP}4sFHRb`cCD4a=uQ!qokj4@4d_CqSnXbG zwGcYxU5ur*cKI{lc0h4S4WI4JVsT4k(2S`5eyY)sR|iNZc#d|)oPSYCbZ+n@z<1-b_X8@;0}Q6~v{2@Jry92$L7CnfT`cs@PHzT_yF-%`W<6!W4@ByA&y{AXPhXJ|++IVr{1lB(n z4wh;pQH!zopVTzv<0@C8at}r(`XZN0V7ZF%NC$t)<9Zf>{5-X}0#_ z2blpa0Kl&v8p`Ke67W>H08mkyO?6c!g=N`P*3~dXtD+SZKI#ax+A;WO1_ReGT}i9< zkbXF)SMv{e0RXhVD%SlbmgkekA-|zhYOz=l#r9d+Vv*SW)oIplKG@^5b0baxZa}Z| zvi>b4G}x!z-%>6VRk828T)sMVu1D2QXE+6LFaxp*x|47U;9#5rI2fk@4#p{fgK-Mr tV4MOt7^eUZ#wmbpVe*yDFf7$_)N z;u=vBoS#-wo>-L1P+nfHmzkGcoSayYs+V7sKKq@G6j0F?PZ!6KiaBpD8ZtUCh%j9A zm)Oj;t0VQQtJaZ<(|eiB?-ch?6|~J|G1fpC9+@J$xCa|^NX7|DSVKB;TX_=s47TMWYM?7RR)ti zI6?%Jj9nv_B+j%qzdl*wU{Kli)%|bm*1PY%`}SL_dHLK(t06W*NGd8KFL{QB;}9p!beKeC6+eSPG^kDjFu=N!1wUZ5d!@agRLg`ct*jEfhF zZl4{^vG}K({<-b@jvl&HbjVrF*!zCdEi3mAFRtF`dM#hp|LEN0{%P$+Lf(u#%oZ{8 z_CNXs^xBT#r2VFzI^K+4-L@Z|Epwm#T>S8icpmrZwl`OoK4#yyFJxyNdGNtR>s;Bxxa%AY4dMEqc#mm@=ZrMNfVgGS`j+n!K&dHh#Gnpo!5QZC; pXZXf;SoZdzCIPe*K{j_&d{5%k!2A*xOJF8o@O1TaS?83{1OR>^Vu=6% literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/CHN.png b/assets/images/fide-fed/4.0x/CHN.png new file mode 100644 index 0000000000000000000000000000000000000000..47f623d6643bb2b3e525bbe1802da93d45e6eac6 GIT binary patch literal 1334 zcmeAS@N?(olHy`uVBq!ia0vp^2|(<@!3HEb(?2AGr~;43Vg?2#GZ1D}bzG(f6qGD+ zjVKAuPb(=;EJ|f4FE7{2%*!rLPAo{(%P&fw{mw>;fq~_)r;B4q#hkaZ{4-=iB@Wc* z|I0}$H@mGecS2i8SJK4`(V`(f4sSe{KJa956{#}J5_REs(u{DupcI$&Rq5-arRraE z9CcP)`KGXKUdWbS-`h)8o_u%i&AIu{6@4#l@!j~S>Qbi0nc^UmrdF8+3|#XC1IacZy?GX4`d^U>qd zHoh>0FxiW9l|`AFip4HYIrw09sE+=^^z@z+xqXk;C^Px!AN(@OOpRgjjy(@*&$IBz zu1VnCZkRqjp)^UTT!)3TIR5(%J(!ZUuF_&>^*W6XWN(^^dLjM9P+ z8NZ(<7o3X{+SIXGTdDkeQKpL3r0p*k7&GO4E>`b+&T^YCw7^Z7?c2{C{a4MO6o*`x z*0A*LbE|V1Em!?aw{RH=%=pWBxc&5w$BI+m7t8+V30*Vu%uZRuwEwTZR{T3yu+)ih z>GJF!`){o7>r7|;y8mojlW^aX|2rEl1)uic^MzGOePW{7InIq?*0%~&ne~5uvAZ~P z%KAB5X4(ZQ%*?L&*rI&$t;YwoRWq77A1?6>5{&t`pkYnv{uWbKokxAG73)I`z1H6K zo7|P~Y_rW%;kEg)(dspaKC>0y*PF8I5X;QQX|B#+U7jxS{SeKWUqAJb^I6#~|0KI# z`g6G$zn%Mi>t?m7^QUd!rlVtLGK0}P=uU9@>z4k9Yixb4v4rn0nelts^J$wlsj|$S zsh(;Q8#sOcX+d8}p8ANm&+nJ_aTUSXEe zjvsy#Zzl6?J}&dGcK41uZe|DGI^<_e{KKsH?#H2;LlZmZ<;ZaPhC2cS+hVccQ(?=r zz9*hdQ>QClRFK0$n%8iq*_{uzO?|vKE_bgh?+Jl*braS1Wf^QZ&#v&}YSJqQ zLCLkoJG?Wz8YWJUnejq(X8iw?pU+45?7P){^ZGMO1;srH4m@8yg^LwVo_XXvnd#8y zUek+Wp3hiM))Xh_wQtsbbm_O=)nK!wdrjZ$ezYvvQ`>hwV|mN$yuEGOo?1y)@0S~| ztp1$KtNNsT{hE1UI(8{(9~D)Z?rfa!Xr5Kt{FE0CtQY?t^*y}g(c4w^x00T`uxZW@ zImmE%GIzh*H@$t^Cv@?6)Hr^1uhx`$zg6ot-_(t4oAVV!B`4gMz4$w&H{;fUPeN+n zzX$zSmJU5vwL)Bh!THnq&(Q)*Zhhwh|7REL*e!8vNIB~LM_XRp?C8Dy9$M!bq(Azv z|0gdRsZz>hCLPs~w1wel24fd6k1uA`aN`c>5nCXk>tINAi9h^z_Tl;o5$E?*Ytn@-R_8g-=CR)Mepj^jxzy90b3gxDms6ki?R=fw{^r_>6%X%lHHb23 oGlVg$VTfSdz?8sxU@!>Sp9|%Rxwb2!66ie!Pgg&ebxsLQ0PnqE!~g&Q literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/CMR.png b/assets/images/fide-fed/4.0x/CMR.png new file mode 100644 index 0000000000000000000000000000000000000000..4e44a591a7152ce36abe35841fcb5a1dfc6a0d63 GIT binary patch literal 763 zcmeAS@N?(olHy`uVBq!ia0vp^2|(<@!3HEb(?2AGr~;43Vg?2#GZ1D}bzG(f6qGD+ zjVKAuPb(=;EJ|f4FE7{2%*!rLPAo{(%P&fw{mw=TsJg?`#WAE}&fD9z{=tDV$3MQ` zxxIB;L9~2?NuaxWL~n+J`s1`iejLq{^$%EB%=p37(;O(ACa#`7nU6=M%|%$e@WPJ+ z&QlnamhRol&nIy}LE_+t*M|Sk->9BkeeUPCIro1@mXd@43r(@ux)inL^f%b3b=;@8R9> z_r=Gb60hDUYqSVozy9*2zlStqT`BpZs3VwST{*H%t&fI_Zd4K z=T);0%`Yiy5P2p))uM9o$&cA2q<4ks`gSE%3=00T^WRU6 zoT*dvtE|~6_x<;OC09Q_%xk^6@{wZihlD*pd4Jf&CZy>keLeb)d-u0b&u!=F*j0!y zC`1<3)ZW_q`m4VGu1x(ZQ`P-U3|I8t{W~Tc`{8}=iS-POA?@45e%?-*`1IGc!r~`I zJ)g7P=KfdS6MNplY2mENNBegEP8FMtu)f9^Mv*_CF78c(!|j`Qp7>>{_kY9sSH`{8;epckA_~ zM)lvcHW&on`&>28LQKu8-DFn%;nlJq>^D&sjj#y+%MjMsDbH(D1xnrwp00i_>zopr E0KD@(lK=n! literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/COD.png b/assets/images/fide-fed/4.0x/COD.png new file mode 100644 index 0000000000000000000000000000000000000000..3339a170d868c8963c00959e44e4fd78188c9bcd GIT binary patch literal 1589 zcmV-52Fm$~P)A7c#%NjNq+?;z;6x2gnLK<9l+Q3FEOW5jL68&$ML<|~m)jo>)E&>tz4x4Z_rdx6 zw`cb}_A{7)eQ*?iv?^;eG3+$pT$riN48XjU!RDgf^@nMP3mNq2h6Hs0xz2J3f{a|LVps$~qZ<;` z3R>!@001m=?x!6p=%LjO3920sZ@+;@5^DhflVe?&HnIVK-kL%)WKhCNHzcS6Sn8;N zD0u&SER}~<0IYRGf=WR-&Pv7OTxSKecFMELYuSNX5B9eLW z0m2TLI`RVYr|g7)dmoafC7dC)=ik2zGe(^oa^f#8uEMGISt^%lEdI`JI*(e>x-|lQ zmjypP=@^megFrftUNrs?KA)41(Y6lellFKc@!IM4QS4gDd~B_lt(yUCl8mt%JQ$JT zCjbDrFZwb{GV(AxzK+?%4Ywv^)v;apqhl(wG4)`^ryCL$I}pFdi-a{ESnPM&{00C3 zkybZ~QVa0Hq_3za-`ltx>rZ`vE^iF=STtxkDrU)u3?IgB@W3W1%YDxP23S3@6d%ui z6H%h}e+#$IhQj(nY;Sl;b6=JOgKqwht1B6A)NN81007L4JByOJd6*b`N%iDSwXJOn{qULH9jV8sts-TZiY;az<+1u!9|84r#A zUDazd#+<~M$TnQ>P0%t06~V3>5}E?YmC9A`0IS7^CF2iZk86dNDRdJGx*?${fNZJi zt~Ou46{Stf@RV~O?AAWTb2(C_;Q@q_Zb+yOAkKCZnTaP9ZEfy&Jb&U-9K13Q+v3(? zZ^nyAj%gZvK5JYxV#M3%_C~9nMm3?R8xpDm$d;-UXa7g9rQ_M-JMd3W5&)p?#)Fu9 zpc2J%UPe|@4S+jEdLpR?-#0H-y&$z~2}RwIP< znIpw``sY2mf0m=_dq9-fi>77M;ghXc_e&AVnsbI6c(LOkcBj7r83HDJcNSh*|K(e_ z=!S%>+!!r`| z5AD;O_N8to791|a%(%0f7Z&QDZq%abD8dZFSvMqt>c_d^t{W0T5x{WK4T*pTFx+%Q zB47awN8OMxJ%Hh?8xp1kz;#2$lmNJH{9|YUTsK2wNB~?n>R}!L*Nrxq1;BNq59$E8 zZs?#2fa``HT>)_2utQS-TsQ2|bScku!=51n;JPsuzX5RFm;*5Yt{d}E1i*D;J|e(% z6G%j^o4_LUoJ&$vfa}Iw2Hyj?ZUTb{aNPtF;X=*?x$_9uO`s5sC_dMXdYIpcaouQx zSpZx&`k)Sg>xK@h0Jv`G(G>vK4LdXiz;(kO)d6tb7{ibOaNQV--vGF7%z+pH*Nu57 n0^qta9}(cX2_z!dOt6$!F|Aw>p1>?VU3=xbQm=ahIa5acBXfuQ{tRYjt^!nX? mi7RYGnDlz)=5FFt%10b!`!^x`%sQZ-7(8A5T-G@yGywo8rCU(| literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/COM.png b/assets/images/fide-fed/4.0x/COM.png new file mode 100644 index 0000000000000000000000000000000000000000..67d9023f06c884a15573251ce52f96a85676a317 GIT binary patch literal 2382 zcmV-U39tui|FpcvfKriy`LFo z*t6%J`{jIpzkAP~`&%TdPmM8do^YE0g%*+SM|!Ma05g*(a=c(bPit{WqyUwT9;{9u z!E4ibuYY&yaXG9S08p-xvobA;HR)rh(TdsN2--Ekgeec`}NG zj7S=EGDm81DQq?XzzFQk3FeKI30!y(;2=q^MW+lfQB?2A+sDU~kP(Sm=O(l!m%=Us z;Bhv@ADGC+5-}U>#cl&k6xDgMy>k%aG@ZnWn>4>xyS4UQo$B$jc3k%TFOz-S~n zFO9!m+k>j^fz@MWQW?vipU1j!%aBQ3dta&32Kab*@r%g0EQ_3rpZxK4J+DT^hi86A zPE`T!QW=`&#tz3C88Cz|C$DE@KzQes_Y&L!=7evuwez&l3GDpwZ!aVHkIa+Y>cS4ktI$-jKQ|SbR7Oa;fYVW$MZf$8OSVNzB^8iNf=IIe9(dz9IJg zB~D6*86ZKzXOli)S>#-^$v>E%&W3YaXw+-*l*w7~$e=J~5N}7$;r7FP?$#FBB&1Yb z!N2buce!lna22#RYAHV(KGUo{zov+d8A%vvxrN+mbIH-_b^QG3dQPeS*C{ckY*={o zLz|6rcG@-I<)CO*k6mQet~VN3oBkOMI!$X^eApBYabkz{qy8DWz-UT{_rfzg4_^ZUsk zwJdXcAyzYB{;=s5Cts`Ak#uEOyMqcf`2d&7ZZYe?s$QE777cKdNLfB=zQqI5R2L}M zR9PJ`E+mE*2aFO%-6J&3O(bUR;GGknP~1=^=-54tMFXaVP9W50pv43B=cbyqdAs*v zX4n*F4~gRsQ$8jxBvx3Jk2qg^h4|!^?9Dmadh>U7EE@1?aE!&y&4y+!6kjuIZ_sI2 zF#L74ys`mb4{uhCUci6e+{%=o@xrPF>UC-oGLm@rL;?>Q$^{?4yRm4%q@ZyYJKw6z zrBUBx)^5be&YRyner<$|;;UplxGIoLbI#t+B0i-h8=M(40|t8a=SNd-FhGyZETA)tbKte_m%Tp65RaZFlT#mxbfWSU|t#+<#thV~z z?(9RnxMu+~_WzR9!VF>5*~poqOlBrKXEvA_(9hF)RYOfvoz?g9Yl_et4b*7s`S{!) zg;CcDYPIzwWF)cT@XHsj=O)~lfPMni`K`YU{qNu=#ARUOHd>lgTD?I9v= zcf1|`EMS%aby~I6&i-N2v>&omPI#bQ2Pm7&q^1(s|6xQCi+PUom zn6~tB_au4NpXsac!%yx*h|hCWH`Q^b=sPsc=Bp;5@vYIbWY|WQ6}Bp+H z&8$q@#KXo~hfcn;M}1o?K9W3l{}Okq3K;GmX7=8kA+K31E7_BMr1keZRRw|xY>RJ= zp2c53B8DPXn;-wuH512P}8XoN|RRQkTmvmj(Q$%N~*}Sp1S4}>&^E@D3 z^&N-ro;GXuSNQUlg-4Vo<`0|B#DP(Qh=Z1>fB!G#{OWJzc-43OImFp$w<_W{=eIIM z8BAo~p-;508@G(Z`KPIFss$j(JCKDV;?XoWvFe3|?9V;Px#G)R78jkVX6e_Jy>jAB zv|AOSGc=Q?y2y;M$tXR%TH6(F9t8I3%ZbACK+C4s;)q!+9XT6!sf-u;4QF=vYh0_y zv1^mew?@zM?Ul?rRD*}MckKIHqr<9*GIb@(zTH4xjp~W^x#3e;6!Avu>wn!j*802N zXkgooeOxQg?zEuZdabvHO_g!gO?4#arZQL=%uv73*7lc!qWPh^kh~fdD@Om6CcPF_ zeF?sDZ$3Evd8dRZYYi;_s=_6rzAZMFCh82$94k1>y}Du|1Hw_ty>XLBd413%RQ09o z&OXfU?86+$OT$Cz&c%}JHj9YPRP*btrGyr`-an19t2)|UD&wu;GguVyCL{a@1MttB zBYcth73wy%LLH&3HLzq?rORAyEMa$fx`|Mqfs7w8iZEpmdZU4Ve4lL7r(joD;n+f# zi}UZ*J>XtD-vxLYG3uYBTfZG4M!h**z69BVRO^W{I~GNmYn7hcg2OXFQ91}^b_`OT z!qKO4N*rc@81=T{5Cg=hw;e$Z5Tm~H2xfp7_4d(a1EgBTqo@zmnimW(J5+-@QCxfEe}Nj|{}9?@450)b}j^2iE+K?_y94Pyhe`07*qoM6N<$fjzg#(owY}@F_wV{_sI7!eoZ10WKKx&c9fpIA0`A39(*M+q%`LZ+%zyx$*|47;a5V3a^%2PXj? zKSaeZ8k(PO1||+l%8-Ldo?#-O|K~)>7L94*-u0JI_j-RwCU}Fk)j@x#HE3&*gs3C+ zdKyIOc%Yo{67mUauE$FjrUenc&oR|SqY?nG3k#Y#KX z2C#cbQI08vDjwah^6nC%$+P^t1BO=BL&^p%&tW%Hi~N&1Thl6&_#6U-%Ng=94VivL z+PTaaSy@!PF1i>)GIPCUaq{dpD>9IHs^MxwBel+ID|v)`G_EPUT!IuwTu&aEq6~G7 zv_&LDRBG2*Sdv*pbJU9@Kl(FVRAN3^OR?tV>Q*^FJ(Pu>O4}oA8>}zO+VYwATo_ zV^K`L-D5)fB~mRqO7%steXcfiU!3}^3Xq*m<^HB@V^m#KPuIPxvAq_pacO{4#Pvq~ z(_cCP>C$KTtSix(=3ht|%Rv!&W&2(M3CsoeoA&OTt>|_!!<+Rj9X7BS@n&B-ZMmK| zSi=nT90&%lkU{;oV&EB}RmXsLB%StrP-EjKos=Btk-?SRMtA+FvM3xPS&AW)Yd4KNP zeppx{8v~RN%Kk3rTy|vq@uJ1d18cNny~sIx>;1&`=r!DwUOL0ImIkad11v>3kHU`d z57z0KzfQuNMCc}cS{9u1P1KVtu2tWSJZ&#s3EB~WsSMy=Q7B@=++G!VHER{76)X|V zlVB}LI4HCDGV3ZWSi>r1-cpU*<>Hv#@n9_8cXTB?iwktIUsLoKoG@TRwh-iFicY1ohlT* zYRU!#%%5$M%%n!?B4r@;vhL?A6-(5^YG(p+aW!ZSuMT|(#HT%%o%x6}VKF>a!IK)U zZd9(>>_l`QgniKYndboxNP8U5k8N!(KJ2}iBJ!M3a#*6Fy0Ro>w(X&h072fhb8$Xb zE;c|oaD`WqRk8C+uI^2f8DcAC&r9t1#qV!jiZj7Y*jQ(K%>pw5$T4NkV_=S69?})- zS}6Yf3aO%#Ke9TuYCrbFw?b%#BPE?BV$0z1ZJ`L2Q~m~sSfh@aM$wXwVta0>BC=tc zy**xO_cUIu0lz`4RUjv4P6?h`(=~nTcn{Vn1;KVwQTVa!@VkoP+0=gvXf47dc=Owx z<3Bd6!t5p1T56w9KKS&hNj~C}x5{Zq=%-y9yRceeI^5%I(q3y3KBF?!X4)Hu`cTW- zn!S753**;C`w^P@nTJf{8nY5Q+G|09dG+W>f1^#H+yFc*bpfN@uyp?Na^M*xXI+I2 ztCBf(bK9@_lMkIRp9BJ2SA272@;jzEDp<7@nZB_y)1ULwjq{}$KJ$3{>#4ivxjWRO zY7@JZoOw@S0zbVNpBp=ozF8+h%k7%`njxgN@xNAoi}QL literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/CRO.png b/assets/images/fide-fed/4.0x/CRO.png new file mode 100644 index 0000000000000000000000000000000000000000..faf263b2f2e26b48374d85e832fe6ecea3c0d761 GIT binary patch literal 2286 zcmVAE-r`o_`tESFiz#oW5Lu-D{E$js~PMHqfx)HN& zE3~x*f_6L0@?PX`#m50KnGDRHoXDf?B`m%BaaL9PgKFHDWMI{f6};fc;`x_raJyvy z9-fiJlP}E%mn)EHvCuSaCQ3^SF>h?|vOZo8@d9FE$lX-Qwv#gB6HMIitYFH^tN3Ty zLY^%;$Zg(Zr20;fB)jM>H}FAb4qHaVGr_ZwQp3G?3|8*+Z{R&?76XqBqWrDzRhL+n zAJ2njtJpPjE-&`EomY>0n4B2K)(38b#zq01zKC!E@2~k6<9x%xFN47dK+kaaZ2lee z_tdKLH;$M^+2lGFj;UmQc`851;XE{A4;y!D#-pNu=A;mTq6aO&1tGgUTn8=H}M~`8C)M!j+8dxelNW-}Z{?ctT zr`_O+PC#mqsWc_?rKt}`j6J9^+L7ai(BMD9UCv5A^`zsK!kOl(pj;kHSd*Lk?=9hr z%0$k-ZKsE~ni}IUdXMeHe_8`W(lApwzrN`-t9O6Pytpi?#%2)l$%n$e1EF`ot1rC8 z^dD|QQe-w-#$z$TY}YHan;rc2`;(M!{YrICTv8-ev(u3LUhHkv7$qNd;VCFaGeb6f z#+`Sp;O;CZZ|;ht))~!59W^&VrW_b3B=PWt#8u-CU@%l`%xAJ0VO z6DG;bkxTJlrn#}5AKEM=#7Pv~bB3cQ&+vNbJ-luxqi(=pUWypNjw4~5JQr9obu$~t z+`d-iS0Gk^b>|KWe!qfeju}ymCe9lB05GgaIA5QZsc(E1o4-BioO6z;*t=wm>C*!?hzAY-E z$H+7s@4pSLts;7T0r3KWvZsr<-TIrLm_dT+-4s&4IKWV9xV7~=dL6IAaOfbkv_Nxn zaFOU}=-wUwfD~%`4q?A7g?&l_d((&Un$wH_$6hAdjQnnpjTx2iFnDfW7xnP+XhHzJ zdU5;8GQK$NAtN!IytChsz3XM--q`_X&UCJJTpXvfrt)US47Nnx#7DJmeidh??2%D$ z>Qrdj-wCuK0ND5E=ZSRHk@Anii`xdF#%|~E1M@j&@4?U~7F?TaTqTmC2%d$`^VCxv z?|Sy{4~C!Y89JSTVdzq#qAqU8oky0Tx>{Ycu@ROm>0En5g`yP-3l$}>CJFwyTQY2yi%{yj&d*^&PNhkNSyOAleZCke#g_pF=62stP7d2wlAp5EdW5 zMsyp3ST*=NX56|3R;~;LT&}=mdMSB%fzPGn=BnqAmX?bGAA5|g`wcu&^^><|g3;C; z04(~dKVP4@@~tNprt9?cD(YXQc>6t&8Au`hwB;zZWtZsff9S(Q8bclPn=?UntR}006gSV%n}G zmW}=st?M>k_4Yyg&Jvc5T}t-Oc-ke?&z~R&V`OCZlcF1Oc_enX#j@^$Rw7<|h2*9q zSbKC2Otx-!;NdMX5pKB!GBd$$r*8AVd1-MmGdFnnyrqLb`)UlSXV2Bzcq4X&B*_g- za@3QOXeP0z1<7pUgwxIUwI1HAjiJ^Uf_bA0`M(ozy`kAT;6_0gAQ)YMU~~b3(FF)b z7a$m2fM9e1g3$#CMi(F$U4US80fNy52u2qm7+rv1bOD0-FU(KWXzrpUy8r+H07*qo IM6N<$f-^Z(i~s-t literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/CUB.png b/assets/images/fide-fed/4.0x/CUB.png new file mode 100644 index 0000000000000000000000000000000000000000..cb25d66e95f22967fbda3bebaff54fa9187eb2fa GIT binary patch literal 1938 zcmV;D2W|L?P)>6C3PmC!<` zc1yxCk~SwY{Q0b~k-3#l)w4kX&qhcMRbc9eFoL4)(`;fWR zT?meS-yG-nDL1-)p1;mt_Z*++oO3Pwa>;#iOR0}D?gHk~9$^UNkSK9@#kc4xXyvMu zp|_^ALKp%$dU3^)#ZU9U&(Ctd!=zx+Q&-v$V?dOF%+<7%{DeO(x=RmDX~nb#0LTdZ z`qmnD7d^*$N4D0w(uNxv5M|V4=YY3~9`8=ZO*XA`r42VVAj$>%t?Vgm<7oC)wKb&` zaT^fjy~P`8E%ovDj3Tu(r4_R_03afW{Nl^spsl!*%htte=t>(BG9Zdy%4BarGe6J$ zHetc6q^`6fDFdSX)1Akz;ve(QvT`Lfr4@=85aqb1lGYL*|8m}@RLD7xq6Pp=h!$R2 z(a4@vKO|^&DH(cBqPPK3KCyXtwy2$B*>%VWYNVJGsAfPEfBrfKcI@NRRb^_XNm*1k zU@D7%_0dPMKmR=9vSq5LOC_{1AezkS#P;;l*jrk_YSk*Oq0!WUC?>BL$G&|?)zw<3 zHXOPb5Jf64$I;V+sVIHR8ls-bLnlrUShI$arY7V-Ku;;Uq3aXyd4xrau(!2g+pz;o z>9ZPV7z3i1*RI9U+l%?0d-Rr~35GKuid0>V^WZ_moE*KSsEV#*z)Xbt`w0{lGTPpb zJT|7c9Mw?V05My+^<#Ntgwc);0(p6izx=XdagBuH1~~TbS9}nSoIlU-V~=s|fd`lz z7*I5}-Y9B-xO6F&&6_cosZ;j}z56bKHES4dXh0quR3(SjC~AOIQv;%irKU#7u(3oU zjKA^<{``E#dV3)pRxg(ZDEckP-QSN?RmJ4lvjn_erNYjL>@8rjYajBaN=@=9jba7} zE*F`Dg9x^%y+eV*LMA`>K#9~Ga8<{F8(@h{@c4%>a`MDF>dwE0V9YsY4DoeA77Lcj%9t9y;BtW^eRl2W z>O#oMy8f9+WU9zEF)@8iS!UwcF;$Cje_gnimJ%QDXWgk>dX)Xj_&#q&4Q3sc7##@b>)gUZpXXASHRm>YUB%CmO>J-7+S|aDp zB^)CWMobP4dcR9g-cyW=##xOs{Q%nIy;;6`b*dyF%SiX$3&QMnJB;`B5v;96zI=JM zI+I3{Lu|PG9$&xkD=t}=aVEnUN3v!=2#3RrwzqR_%NFD-SK|7J91IfNwvFK@o`CW3 zS*KJI&*()~2G1fzLb~dRlGHpv%*nz1#v7PcufE|uvft0u_3JgA6HKeapa)Kp+EE+% z=p!OSL(Eiq6WnehI&?Xo=Wm&J&|2a%Cd#3__$_vYmze=5;wqVMXIQn`g?Mc(T)zT zZQe}e;zdT9ng~ApFtXndfMB;Hm8*Mp;H=9_-QBOV?XK52=Uky`E+t9$TaZ*$g}c8W zd2o@OT@S>YhY4rQRu>gbi@^ z^lak&i#V@r_n1j=g)a*4Oi?$D`Z=iYQ{OugB5djj&{iGBI@^F#`kv+s>Vg z9XJqogIXSWgvm2!6j{@i?!SL(OJiwi(y{c$u=|3hL(I;`-r0$@p+RpcijfkXW^`M- zt+_d7(Pu7TYy$)VOKmNV?ruf*8yN}18em?xZfcL{`t^ECQ5C~Y5ye|>!FlKq87EK9 z`Njt$JpmRAw(Z-ocXlGUjX#4`3PTtmRafKe?M2MZ)mw_@=xTsz)hZk>yl}mwk%s6p z1_)VMIC^?maQbvwH$G_n1encO8yj)#*@Lin{xvtCp#jpSO;hEA;$p2*rUa^&BEmVh zY)nIF!4c?)+yMHfyu!KG#Utfa9J@noIZcZ^v>a^`srMHeg^uDZ5I2 z{9)1DuT*M5+y>0Q+Fl)QYQX%DJE+Ty2K;WxeYBSP_{aRuo2bUL27Ktcownjmj(bw` z>}@I$V?aP!KyThtytwko`SO8AnBn@{(0^3?EQ8jidTUB6%)ei}fn6m&PA@3YTT5E< YU-Y*R|Fv;T(8UO$Q literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/CYP.png b/assets/images/fide-fed/4.0x/CYP.png new file mode 100644 index 0000000000000000000000000000000000000000..61a28aa9c5e44242acc40e174715cd9e7974186f GIT binary patch literal 2558 zcmVZ9x|NA;Bw@k!lY;sy`$+H=EZtUhx+6!N$tMqS} z8;%f^zLb!RRrtnDM^dSsqT!qu$z){3CiQKgVI|21!NaqOG!YK6ni1r|r-V8b4ZnR+)d&93tjr<2&+F*>o>@11xQ4 zXvuq)jwA1!RUR_!E}|E|4w59R{zYqmwX*@kflag*zJ{=w0gzN`hD}<`@XR&%jk?Sw zdAOAU!frwG(l|M&z;5mUz}j9zZ_SrLV`&3Wqur7tCB$?HgJ?T|GGi(9P&j3+*!_U-FX zX@}u6Vl2LK8JKlNXroeb^cm5{O+y_RiTAJ=w2_mLG=4}bHNs)T(p-jFf1KW`0&K=6 z9F`usD?S6^IIpHAG?6iDPdNX{8z63FfUuc4@x%b{d`Uv3)*`932)p&$SH=P2r43?O z>diR%j07YvAb9fa7nt$TajPN7Ve1=sWg&1_O<*+*PDGN3x_$>iX}1mTI+<{*y&#LB zTy~9svWdvmOquj40A5PC6jPhX%2PoF8M*ZPxv@S%zOkd-(Z^ap2e&yk~A}H-vVsCv<=`;W*&H;8)2eof4!eQxoCXCQwW>A9+k!qL&0WxD)O8W-uXxpp_%s)dCgYY zH{8yfhBJpCL&0YB`yO^yIF}G}{H#qbrSS3%#(&HlkR)tI zJ*K)sgsl(nkWu(2UWFzk9&1+vruwf*yzl&bMRMZQ*HGcGg5(7#Yk;Iu6Tj+nw9#q( zWqtY+H0M1-6EYQ1!|c^j3f2w2&8qX2e*M9zLaL z4g7eXNNu~l9xAeOLw924aR@;ESKpH6ale<;U7N(ZR}JuA@Pc?V4a1_K1n((-LxLuO55RAuyyGGPzQwL z6Fwfzh%uO(N-@7_V+V*0`x1Tq%m1q*xxDjjcAHD} zzC$(xDyz#_u_lYUhBLsx8mNsq1g2zR))mW@+FALu7#g|v(ItFcbWm2kb0TD- zt-VFeTaqGfSe_xOYb(T`-gr*vTN>n&8x-9=UE=xekBXMoCb9JHSz`9}N#acPDMfOT z?Fk489>D{@&ZgJY&D(!{32$!=4Z0e+J2`vb;*}456s8p^uK-P0XPp;j@qWQPd)Lc(sKoA?YJb=w= zWmsSc?|k6e4ZECAcVaU25*ZQ8W7&7%?;l8gLlyI{TFizgUQ?v=0r@83?_;%E#Kvt8 zi~NIo#q!m2MOj6OSeLy*)YP38CR4BM+6|30VqNwMar(>&amT&$#s2&~;)U&xiH^=T zS+xg(OBP~%eI`EtVjniUjfk*l#w3hKt@h^B=@R<-`Y1bn6fc#Un|`$tKi{FB*RfhH zYu*u5vUO0*!PtVQUO;mU+xp})Vc6YLB`J)t<9%fql z4Dt#-;hMQObEM=DGcR9&R(tLv>sG6UgNOGsA!RZj?t6zDuU$djfse_UGLyI7e-VHM zb8e)$MaQD6-RDs5cPM5+Yg;qN%D$nxwgQz(#kjF)B#oZHe~Jpw`uH*-Wil)8&%)m? zkd5o#Bsy~B4`Q@+w6HFFCH1;$-gq&Ws+v=D_jHjq=~9|obQG5!#$vM&7oEVQ@l%Ni zi&3DZ=SQ*s144&~GcqQT<7G#vsx7Ckp^DM*$s8&9H^rre`1u8}V%bCN{`d`kw`nyy zUf6?L?cIO95Q5)tSxZk(7pw1jf(={l$Iz;0+}Jdxq-L@|KL=e?9dWUviHVAL%gzTk z`$Oh<*%5U5+HX7XpL;O&bdsDnk;sTRLWhSF;5Q7d))%wcgvHuNLt`~FggrQB(#Bo#c(fBCcl!2qz3-BPY*=*wJO{>Mi zTP_hL$G;LcET1kOd}^8KHT5W++K_RpA?SP@_%fG(fFP!%W>QvBLS@xSvgX~abXr5^ zXJf!G6}S3F>|(@YfWmkTP#BK^3ga2J# z_wOH0YnUPuiE?z5RdJK2I3oZcpU*Qn`GJ;}5$5ODW#8l}{wu&>dwZXro>#gC3%O7L z=o+l%QUM0K25Tr;02FEn1V)I(bPZ-umH>lPYM<_IU4t1^BmjzQSXkIF^CnMGu>gZq zYM*fUC6P#!bXwO?E>#PF;%`d2hVn2)fPt=|d`uMp;EDD;9zB+BveZ`mJjJERMM z;$P8AOWv+Qi2#GGtvxzByUiA@`(A?a>^z=R2+HR;mr79d%fJh>-OGk(EYjA7CvP^tPB2onC)t`^$0`%%H#xenV^`&8nfG|nMiP6BZ6>sk< zOckJ4Up}S@=qAaSUVYhAEkLinI;s+&S6@993DB!IkFo{m)tgJn0`%&wp(Fu%_11Br zfO<}N%scv7lXZ{hI9EU~8w|5ZJts2n@f>FaTqna5;`E~b#h46=1nAYvrxs3EKTMok ldiBz&z0Ep9#P#YG@fWnClJXBIP+$N6002ovPDHLkV1iTvtjYiY literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/DEN.png b/assets/images/fide-fed/4.0x/DEN.png new file mode 100644 index 0000000000000000000000000000000000000000..43912e8e84c086fc2863bd7212122db273191abf GIT binary patch literal 399 zcmeAS@N?(olHy`uVBq!ia0vp^2|(<@!3HEb(?2AGr~;43Vg?2#GZ1D}bzG(f6qGD+ zjVKAuPb(=;EJ|f4FE7{2%*!rLPAo{(%P&fw{mw>;fq_xR)5S5QV$R#!hF;8pA`TZj zRaUl6n4qfB@qkOGgn!*d+Z#6`GnIF+cBU|U8j9s9oGswE7i>R+UEGf6kA3y-J+kZx zJ7f9FO$Vh3S7R;|8V#)&pD(q72#$VGL^+B8V3_JN>rl&ksqP zXQh0*bLLo~%Js{imn(B%7&CisyzK=0hey*)@9eRgZ}PkHzlluwZ=n6po_}EayW{rv iAiK*Nq}q+--1xV%WRE@R`t}_dm<*n-elF{r5}E+Za)nR; literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/DJI.png b/assets/images/fide-fed/4.0x/DJI.png new file mode 100644 index 0000000000000000000000000000000000000000..0a29d21f2e6164cd709842bafee94a1db899f8b5 GIT binary patch literal 1266 zcmVR03jkn!6SNKLXb8Yzeg#G*4OX=cQrR^C0BaK_v<*^e8i2!m4o0T54Kipc0MItb zqU8V_v<+f$9RO$>MB*s`hjh;V-mq$kE-O6v3IMbXe(@cE!|yp5)i$_BSO5Ul22A{> zZE%Co035Upts^A>fVC3*8aJ3-7QJ$JAuRxh*=0M3wUVqIIveQ$0IaRUSi<0uwxIzf z0XS$I8c`YmY}*hwc=V^L9ork_0XWPq+d&jVZbM)JfVKew4!|MIHnc?m0EKNE9*4|^ zwh6!?Y&NuA0I+Stl)>X?L02N5whX{wb{R(F24)xAATEH{uOSi=2%0m2d3{qfpMT?(N;%@9Evc!;Rwl?|k@1OYj z`7czd22z6?BA$z^R(HPOZf)6*4-NUN%mZ`Q90R!+bJiTZrF?r&c)v0x60ovj|2;GW z_wL!J9Z+^0btqTMT>j@WpDccAH@}1Me)Hz`ZP>RDu3m*RXY9w)X*ho#Zry^HFX7-p zL1pev-PCs=-VCT#?bA>?{eSZT`ukyI#6JBzd?={IohfaV`0n}le6;v+$2K3p$d^}F zqrFjg7vW09zStVZ?yr$_cNAdSoMs>!W1-af|AgwzfZpEb%?ByXcc%Wc%<-;hKKQu? zD5jgHz7~1+2L#cLQ(sG48;~+n#Il23*?5PxG{Bkq(4{CRPy=eGK8($W01XK9)Yqci06q2fC^aByCW&PSb>nMLVt_OC zCo(5>^WQ+a0eb42Bh7&It#v*1Eg;2!q?u$O8)LDwC}a7qLZ|_?Q?Hxf8o~_FQ|}Gm z2I#5xhc5#X#RRe3pl-YmJQ?6j{mG?My7@kFZGfJ7v9xS}o_aa7WI&>rU?3MGQ_9G$ zCv-G5p!S5Ln=gl54bW4sh6V%l)T^b=fOs)ZEH|hduaF+7IpJhZ>Esh#g8jHGVd_5R$fein+C?Jyxw41(lgn#ye-(XeRwpW_=1-&L9Xl~KjcU>N-tJPkmV=4F7WOjqO?;g|J zJ&=7n5>kgES>$fJ4e177VNaG;;?FhsB^nmbB-Flc|FS?FiZk0z-w3ct^VP0aA<-oOy2i4Mj zbE$#cf+$evqUQHSpkmj`@6zzPz{s!Pqi2 zrF?FK@aR3nVh6)2Z_#RmRAY+FrK&}D(wC!r;P}i2v)H&Df=uh6!9UkUFyUKe1!CTrhwp; z5Fozc0jFyyqYgV^sd@OCOlO>HkGfUjq|2-#PdRa|5-kD~>CU?)nA#paKi+V&&GKK9 zB)!=V3%j0jFz0;ZJxlGB<8Did8N_<$ z?-WHcg{$_H_7Zc&jn(cQVe>Ex_Y`Q#K+%ZkcQsWqxc4ibV& zEXSU;y;0r&3qo=m>Oa9M1OL8M76~OCi-eFc==zZXO32~f)9TkbzS+*vMrDP&s~HFm znsa*2Pyw4@!W`E$tfGSf#s+N@?Q)4yE?d~p8QoT z*quPC?J*%d{DnRLO~-Hpr7@6GN41D?;v(jrtN+84jrSB4?+rC*2oKceS>uQyVgC*4&;+CVbA~U8~~Tg!5CC#)*vi^t59KN zgZ`e6aX4X`!p7iG-DdTtK()JripZbY7Ow1v)h|z%_FWeAL8oE~CXfThj3>`Su%B)2 z7$1(G)&r~-&ZN9r-rqmk0*87}f*0}yutG#eHxj#+V5m7`#-6CG``(n{7NE=o`ePW^ zjH*j&p|W)Veq?cm`84mXSR1F04j}Y@!?&(<{f0E)~+lc8qDn@jn>=iIRneXLy*=(i+Gd0)~47(puIMhdW z&7C5EC;fq_NJ)5S5QV$R#!zCJRg630Kj zKclz%^d_D6Q`RMLZ~b&_T0rI+lZuVA%(m@YGCk>0WO7nR)%6)yf_;S_pLDrwa?wof zYKE_r?p8A=riqh;RyHh{A`}<3twHDav3+MMz8*_b;9q?Hx!puJU+b~O4a!aSr zOUD1@QVuiN1eP(jWHKaD$lQ7VP0p*yavSC5Ul0ACzfRkx%qVer&}`fEV99N{`M>8c z|9?41<>a5FIh)1vK3%^gbC5~Amv`+PyIT&vp#s}aN1AV4%w%)Dv^D9wNPoyQA&r}d zZ_j0qjPWdbnI*5kZF)hr$(h9q1>E~(rp%jVe4NFiJhS7G6Tj)y)+uaz8ypX>ORAaZ zYV@!&=aPPqSOw)IVEvgo4@1M_n$9ri_WwE!eVOu z?NQ|)2H{4&eXsf_eRDd0_x+5hSqwZcZ(0@}37>HM?P8?>U#rc>r1j^k%#~9!`@{b5 zL#<7m{*-MO3f~AkWzX{T`+w@pD~q+)68HRAvK(knxx@MQ@xmXb2>_AI&Xd)hc`?6l zMXb2tZ*=9z$xRk3UuLpBQ&&vhG^N79M0Dc3xx(%&H^OUMcJsdvc(hu@%*y3S_xTgM z%VwsioRbUhIeA|HQ(}G7%hu4m^Z$Ju<0gctY1&$=_i;So+xfo{aT(+%bo*(Hm)4t{51m_!u{R_*}syf%M4Z3XX@$H;IS`n{tX2K7i zg4q%OoYMHN1b#W=ePx>2qr%Fi>d%izGH!|#vNeyGEun1vRsG=g-5%+RHM_-6v+kZ} z=a`i<7D}1=?__L+%YmR?-I$fRp*PR(^`?eisa0%AFvCUUS zZrQ38V$)w9JGZ1W{=b}mvT^0^R~<(#@i*rFWi)n^TD|S7L&L9wCoPTb>kq#dC&>oBf%HyXHp}pd^1-(?{j^{HQ6P=qJ+WI)z4*}Q$iB}k@XFD literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/ECU.png b/assets/images/fide-fed/4.0x/ECU.png new file mode 100644 index 0000000000000000000000000000000000000000..e3662e038b7e3d9362fe988ff19cbf87fdb8fd62 GIT binary patch literal 5637 zcmV+g7W(OlP)4D5!*@8kjT_j23QGTMky zK{i-XElvbEK{5tS?}IuroTM@4h01txjbrL-*#ylSz-)uc#W3p($ea4f`uK66>kNnw z!^^ip`%bW!MuZ}0LK8(zQ1JvSC&u*f0ngL2wuZ#wnkq6H zKoqn(6-9fdsw1jR0D}o!IWS`>%sCe<&Lbj*anM}_4eW)!Mrf_qtQBVgwADjyG1y$% z42j1zp(ImaHETqO!pMUBQHHY$vI1!(Lu@(?$uwkT!-DfQqRmz?JHTMkEG3&~V%7s) z2}sJtD5@EYp^XtmLOi5dH&bi1JgNeLUhp=9w+#l{V5l1+{>)-(HIFY>Ry2arvUW|b zf$F7Du~;K5&_jW@l0+CoI{b#q$ zU{CloN<4Rv?|2NWvFX$6=wV_)d_gwj9vr4RJm!7;=F2?>0gA5Ugp|zN;!U zj8V(wi@=-*MKi$e0cm6s4R%6%-T1OnDb2orWh-?1p=*THr(S>?zqN@hTN@5j9X8{h zBh%TBfGz}t5=%(RMF&2R4U`o@%~bg6_sU0;xA(!VuY));>f8O0?|@rYLa!HIe*^>v z)Nh0*?}W4jrHkO=JD_~wepRT@>4j(l8oJ@l-Ei5prA#e`iYXwZ58Q+#YG=x{JIx;#NmydaL+}60y#tQLJypo z3;sSh?;6czkvj=WJdjL4$_hq*}H8K_x`kUNq3cjF;4-g0vVuXU8BpfA8MC32htDmbdt)YDaYz_C;icV>${VEqxnsEgq9WS5 z;i_YyvjJAt!7tZCjS~u8P+VkVmvRl8jeqCm!F8-Ryv#1kZ6u{>6y?HDAM|@Obqt2! zi}T^7I@rF;M0aBjsq6q<0Z2E4;+979c5i@3&MUxd=%CDVvmT<+hk$sx0&n;#%+3;S z{_D31ghoc4M`G){@ZH4_o$>}oa}LGpy$r3%;^JEP`DS=zBYd+KYNx}1crmL|9rQXM zU`V=v!B`mq=@fj971UXp*v$jDoEA(9^!9003`d}$6MDAVnbMOcQW!+G4Denn3^#({ z(Fh2HjI8+kHJGiF@P@AmM0O;NQEIF-nm>(`6()veKM1qcM zcqj25vY3S=!{87EBJj5dT$Juf(3_)>w1iPiK^psEI0{yqNHiuAiAZd$i?Ac1Fxh=K zR%4?cqS4O^%C$d_B*wY-r>ptPlX2eNFqN4VeH6`xm%a(6wzG&cOtt_iPl|b)(los6 zplS+i?70%zwE!Xg1itVyn1{Ycs`qXt=M0eT45JMGFJkJ?#FKB}O&fSW@d9?6312_F z`A{ygJrNX7n$jppX)~&EnARZ3lc9QQKkscQ=g&{Xx$~yCF$!^VY%k~~7QHuO3TBgm zYp;a=es~HdcL$Z1Y0pfNY1sN}`Tjh3YAS?E6P$Zmgtkx>w|jrdL;K1Zh<31ZIGcEI z8%}EolLX!)`HCIH+5=FI!=?+2f zBA8X(&V3J+arGr&vq(rnN>7pK>yC~m%~&l#a@=sn(oP*L;Mqo zv`mUO*&H~93zk31%Adi>1MpB4Ae&ItIKc>P*axq#w{z;^4)SxrWJ;2jJ$i~nUjmer z4TGp{MML3o-dXnpPB7fa)h`UA*tZcW+QYl6AU^?|2aWwRsWZJs!rzH%uw!%Qp{gpe zU|x$Z|4C17v{lrI6V^mU}+ z4lX>QjI-z1amQMyHn(%h2~+q&r3H7vG1xta7FfS@HL;|T>@#6b5KbHb#gGJ5L1?n# zZTcC#1M?Ud$^xK33gJ`r^-1eWKtd8Q2ej?&z@XOYuAW8w>mOgk7V0FHoJt%4%L2`X zkV>Aw;#v7@^+A@?M)k}J=GT;wPy}50lQ74-KeWE2P{3aQiCE^tq9KSH1Q@Ep*(fln z?@GG+mIB&f0I+3TBR*%zb&>7}<%{^bA9oFdhS2T8?4y z!dfJYoh&n$Brr>mQXt0y*%nwlJsVqWFX>d`kadO)$5T`bLk_s0*^I3#&We56SiL2q zd4>WS0jVyK&C;QYIwH`O0NEmu>eB8*;mlOG6X)V|KR*&)=P82`V-+a2bFdmkT7ppJ z(T;B%M(B@f{vERfE{~ft7fxfQXApB_A4;Hg^pe6INTF5&@nf;tK&gN?Fmi)0z~zZP z9Aw<0!x7DQA$Ngd(bqqz+al}_81UqX437BbL*dLhfNew=Ur+RKpSc)YQ5ZojB`L%F zJg`U*NU*oH|E#)1QLo@9v+BSmxCuw%baZ;Do(^9ygvPaJECGrlXyGg>RF&;x>yCw-ep2g^703t-!mN9B zsfcijS030#X<-2Ge#VP1=2+S|tc9kS@&|}iC5ViqXH5q=)ybYs<+ziVlHgC|*9;N| z0{c~!06q}6Y}FQMdA$6`)v2n`*8zFOuurye)7__V(*tLI(jFfc^xKH3Aj=}A8Ib55 z|0q4C8Sa$kQkKjyz@ zzs~X2PO2Zw)ga3fdIsv~x8&O)B}-IY2_>~)G-VdKvBEF_u|6+M zB)}@$ozV%yjE{^+?u5ehM&z`%X)COW67J0+-}9w$`6A(7hJx?Yv@S%~`bxab2Zlu0 z!h;NX!M=BV`tTSj=Vk{++m%d8ARZ#9^awy#qjxZ18@^AA^3$=d1fl(A3f*5hpuCQM{vLq5N#ztx zsYDbFIPE1^Qm5idUQ6fd98N0RKy=-KZ5bqM^gh>?m zw*YTrDl!>$IHR&H(?-9(Do9@fhBBS2o!yEkCQuY@DN$=G}rGuj^Cp^@l@O0`~Qm(AjIEq)2l;oN?m%Q37_dgq2it7MW;-oEF%A6D+vD zj^x0FG+Uk|HD>X2Ex71_I`ZQ8aYF77*z+7X{&HX&{y`0USUmdu^b^;Q-nVy~X!q*p z2Lip_(Tas_&AFrBzjFQ>3{rHoDR*-%KE~Vfg|ESuTVQ7HKdBBbz!7@|0fqfxN;i?y z_hn8gyn*(&6!=dt?dQ<+Vc}lB%?H8lMnjmZE?hJEynatUimKlp06@Za#xL}&a9VK} z%NA;f0)=@o8e1)FZ=4H&pCX#8oFtCVr=zR{ArvAz8pW|4TIxZV8(_htC#bYPj#b`< z7~M_*&okHg8)}_5GSssZ@87}rE3n4L*V~Gl@34YBW7N-A&i^adT#=apkN#s8?={pM zc_EB}?l0-RT|X7bbo!R-pJCP7lkoaVnK`qWbH7o5AQX}S>K8As!F4yX)nGpohxp*i?j>|!!YNb(?fHMh2ZXfj!ieP z;M5KD7B6MW;ASSp+XyKpHruCDRN_L7#OU@rSQOoeix^$T$+UUqFgf1N&Q~V0I(|0o z%p2DRo7K+~_y3S{PtCj!u)EdH+$(P;nYN)PbXP&LV#H$bbNcZa^JIF3kNLAUu%k7f zipm&y`3mv4Kz}p`xhzeEZ3kwXouR0L7YALA8RYpIkv+#CB~=`Oz4W+e5t2>x3pwN} zeiTJvi&TwID#zgn;&cw-bPiFF)68S{{F1XzZW-6E+aErTwcBUwDGFeM{;J@y*Ov0_ zi|Uy+B{K`krg*vRk{~9Fb~-h)CdO%}hDgNBSn41pLS?dvjajEs9Bm=r7^7d%PRA@# z3PYrmeU2Iy`rjsGEk{t{J$thkA_^)-HN^w3MOi#|HQ)HsiyT|MhT5t%lusH&8gmqoND5qa z^YLtNo3FPR01%q+=(+}^1%am*9mAZ>4HPU%PvaXFKpY7ce`yid0mfyRVRdI}4-RgOExROAF)-bTPTG7lTcuL0Ce8(#}bBUh4O$ zRL_Bu(^FU$$P~;_aF6V@_SK2>|IR|s`z9J{XLIR>?HE*D#i2g|Zyh&>%C>Gw`ZNbk zN&sJO8qxyxpomKm7(n4&(p-oM;f5#`*JsX*Z~w#T{PeE>A$DiOB=29;c&0pm#TI}3AZE}O)%_1p9l20CxV zTHJPK@7g;md;p^gMFR$k{1Prjz>mr*Qe21@&EQ;?rgNi2YqLb}fWVV4p9MfZnKJUw z-n@JA)idcG5NK_Z=-MRVSf=g7F2uxY()dtuDFQ`)iK2mw@BvEte9YdpmlpTG@p(W+ zFhXwV(C^`fQF)almyU5wcYV!)cZHR<&H0?O{4TaPYOmB6GBBE_0PUTmZ99+SoaJ}X zz9pajpIGVsddBT)xdNI9Y?)ygXx9m8MJ69~n)oQ>slqpF7F2l@AB#C{7{w(-2K|maMYDD-4 zi_A$c-m~aAm^0QgFpoU8^jcEM? zv5NwHWP)i;1j!-eY0D>xiOeCtk5YDW@JI;*a49BUUr<9&e)gx)g-?VDCBWx$u&b(^ zbL#hC`H-;iP72ARm2Ywf(8;l^J|K^l*2y=ylU&;9Is^d#76i_z-@~q|a(pg*k0AGl z?q=u*P=Q^!ZfbnNkBrFoVIhXdBP?SLr|=TrC66dwBOCe2q#3}*x%>~ySjE>_jSZO` z4js1saocdDFku9ITqwXTEMXbjSxg53MA9SHl7R#s6xK0?x0ua;@!rJi*2Bc-hJX*C z01;H2`;FOQRO|?!8^M2o&(!~h9u7n}K=>RY?el=o+kpAE!%+ez7)J@1U>qf2f^n39 f3C2+ZCK&$*ksd)-0)+E-00000NkvXXu0mjfYz&+5 literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/EGY.png b/assets/images/fide-fed/4.0x/EGY.png new file mode 100644 index 0000000000000000000000000000000000000000..5ad1d5dd00925846b8355f7b15a5c30800f3dd47 GIT binary patch literal 1409 zcmbu9`#aMM9LK*iI+)vZLhjme>qv*?5+kz>%SsoQ#vYVqV`Orfkx05wb7|IbJ1m;x z5_N`R)HFp;O1dqNmP_lIMALE~+F5@?&+~qL-p})XKA#^y?}9@I{k1iXH30z7#s;8| zC_P$9fch5Y)F6F}`DxSudfV1$4EXlYhnvigS;t*r{8jP4p0Q!GX`NGWU&8U*lTOU09Yi{Z3Sg@Venr8~` zK8Jhtr4wH!%Kzj~Wmwsd=w56p9xgYhM5Ie}fI=d}0e?eSZ8P4xzcYDsG2lHHMgx3G zY4#saX3}-tjGVlN@3Opo%y%;fhb&d!4#3q(e{px}bGsPgi^0{=l>4D-GRd|PV?NJ> z&wKyob1`QTRJSw~Y;sBC%Te@f3fp>r0}*S&=R({PN|%55QZ9%yED7{Q4pIZqhQe?IrxWjZ}rt%~k zz%Tx$+tJXz2$4xbB)hYHW4`Iv|vP+M)uxEjl#F#N_Q{Cey@ z+n}>un6j;~(S#p}zH20>8*%jYDn%H+q7ARC74uhvwPQy7v!qL-&4IR&zDX!L(dG zwXT9pGwc`duIHNfSw=$l%>@~SNdf19WK_PmJxgv@nH4oC^KE_}cWqcZf9qry2urJp zPcfbfB}+Y@*T6_nL4R`pdK>#`qX_ifoIBhO6b1dROOpm|hnW{4QydTtcr! z#DnwxLnUz)tN!e_!g;%vBRSjerLI9t`3JL8FTS0f&~vTv&ZA3*+bj=V3iwE89>pFPJ2=Yr_XrCo`<@9hZfL z)C>!Z5WFXn^Z46pV1h_qBqL3H@VHbKG6Q~8l~Xd9Shs}F7ncqWxu1G6G751&%V4`A zZ)carQz{u%(+`*Q1ogt|f!kkijJXr#)`;L4FmP+aqd0WCK)cbUp}_N<-7D=5T=IFS z@;9lUESoPMaz@otMJ2)quN(YxZkj{4#Wx58GZ%BQdv rwd}bl!yhkLa_2>CG}tKi*R<>w2;R}PN4v5@S)TwFa}eF;8=dz*DUW?5 literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/ENG.png b/assets/images/fide-fed/4.0x/ENG.png new file mode 100644 index 0000000000000000000000000000000000000000..626793a3cfc5e9cb04a67358515af516c358a84a GIT binary patch literal 397 zcmeAS@N?(olHy`uVBq!ia0vp^2|(<@!3HEb(?2AGr~;43Vg?2#GZ1D}bzG(f6qGD+ zjVKAuPb(=;EJ|f4FE7{2%*!rLPAo{(%P&fw{mw>;fq_xV)5S5QV$R#!8-1A_1spC~ zKj}H()WjyB$ti15#J!9`ql;}tV+Y?H0cYk30uvNvbz>4f3*XBo zItPybef;6?C#&qTy?1%8pZ9ma_rl=Z7pDKQj2oB|SPyVDh%#t1gfXmPh#+2|_xWzfrNciK`7V)RdGW^)Yf*ic3K^$^O@SZe7f50 z({a@4r?&3au@*-isE$~7(Ne4yi69~Y0wH0^zTM>B-2Hvu`Qs)x2?@D38$rqUuiW=N z=e*~Ao}Ax#&U4NyY|NRZ{bPxRzb~`VVHcU;^dh{&bPYfi1rC%L`P&LBAI~?T$ijH{ z?W`c-fZlXkL_S(%=7aOC94xMWgO(W-#T~D>?D2?L=Dx6#n)?(k`BgXpBEDT;n5H(TWyU zKoBsOx)3dT+P5D=4MfTP$rq4aCQjV*exglL)tFHpWJY=Lb0txuEH0?<+B<^4SNo- zFU@7zGgr{~r@ch${TQY=D0y~WTxJ9TK@?E@5tNQFmf0Sx^DK@#QWH-|B z-m%1WeU_EEJHA2f13Rg??*nv`EQm&#SZfeXRgu%p*v`!+U{4#Q+SIk%mMV?{n%SiyPWRAh&qre{7y?Tn(dhtVPmqAZek_j1x!7og9!A)0jv zf`GZig=KywZGSn0p)d{iwKHgXdM|1ymb9;DdK5LR@o8H;835m|Itu=CCGKlx;C;86 zhDUed+fj?AsC~*?7iMBCaAKI^#JV68Q7;iW;p^9~5{yy(y|?jvV=j(W1;koI1P`_1 z-&srMP4me6`3iyuTM%^;jtlZ>d*v`{RO#2wK%A*j)Q}t?*$ikA1w)=40DY#F^h=95 z{_FSgzkeM6o`wPIb?#}zUsi|jA2p<3HVuG=|K0p3qCn9Tms0(McPW1AGMb((2f#c#13?t<{`Dxa*5Hs{+R~}Ae4C%;+x+7$QA3iT zoZHSrPB&tn?#4VT14Dt6)|V(0{@wNc${X?>h*muSp~^N6Ui%`uWbEQY6#JBb{!Ey7}QI z?*HxyzV^o!iVh7N@{T5{j{rd+_s7c-CD4K~j8oHy)cJ8P&m+b%(W57982wR^@(2)3 z5|T+qPB$Z%bU-|J6FScjl#Vbt|8_2t-GJ+gX}GVO34mbMVJdNf5Pu=t zxjFbgtflGMeFHwbWz&9IUOb3ok}*zAi?<;QqHQ5MFs`WY`W?h&W|3g3aljm4PQK?Ii z2HD>^2emVbY&Q}<-ofE-zk<@y|34q%(TnZe93-<2+qpT2CK*vLp+!~FFQ3*^P7tun znS^U?33U&b5vd(mO0<|t+slVA&+%X^aQ3;^`cehKgDt7O@hC8w5|#1@P(v}an2MZc z1VNzofe%SrQ%vL2pA1-DQ#Epbv=skGJ(IIw(vf}p61?wL)BaW!*7=#}^X-Hyy)DkD4m(WTNAd>_^J`5h>4 z!t;%J=(DV+sB!w~Ug+KV>w13n<616$wH1ivIkapV zTz{30Fs(1gSID-7nW(`?JZq|IWZk?FL%xHW-|R>!r;~ZpeDv8?8lK!k-rdWA_yW)R z{l!F^0;#;pxY1jpd^U+4i_N^h)S8+EMH>UiP9v82S&3`1(}1HZDVHj0OvN}g9Z@G? zUzUd<-$C2!mDE32h9C;KuADw-ee~8CXmfyYl@}nMayIsTq-d0xa^EVf z3o`MQ)zR|e!Be&uKI+B3EEn0T$M<0k&ees9?aYq9pGf6Z#t%tCId>7U4eL-=t^i$p z&P!7>+OHW5}^34A2`v5q^g4C6FI+IX~3WIoHBtQdr5yhDtEdqLP1QKK;RSpj)_@?O&$6P1V?7bpAarz1vYs2}5N+-$U;pqf zEOR|Td}DvS(d;&qi&hicunu)*>7ZqkjsSg*mB~L`O3R=36Rc?Mb5HSw30L=z!IfZ) zz=0NKZ@r#aOAtX8nfkzm7^kM8g%tc{U7@6=5vlbhjGyd3xcAaV=QaTltZ1ca<0nl1 z!7>!@*k>A=sc?*fdsh+j203x>2R(ApRnB^TJbKFc z{$gC~N!wrm@EVTYy$%0Ibyyc>VO@}kWv&P3>H@rP9_dr3W%B_tZ&?)Y z@JS0SbBP;47Rk77766mKzZgW3)-40?sOj7$Vyz)`uAaFkT;)YH%jEuaIibon8j_?% zM$9Rqm=QOd%1T)AM+od|!nL*p=c)o?-Y`1134OL5$)ZE&u@Y?z5I)v3ANY6HqW4%Z zmAK+j5%G+s;te4hWdi#e@!Sy46A5~WA zBGIbDx*!XsJ--W&Uh{5}glfZ(APj{kf+ z{@o2DGXCk5m}Cse0cvNI!#BT*-eV#C^67Z~c^;Zb1q5 z4=WX4+%YYi%1>#0$!;L`u9KvvE5s~%{8AKulml09Ce(FAB_+m5Vv;ee12vI`PU;`u zMdKg$;Q7{kCg0K5SoZTLGkw!~D*yGhekq`}Mymy?? z5ih}gO{iTCh&Be0ta@Bmm6CRGA=U+1{W7JA=Y(Uly;@1b6T4H|x;&F4CK;plg^VHJ zLH4bSNWW~_DYqA+%>e>?8gZMJE5v}f|adw z?y4trs5R*pC7k&ZlZ?~$nW}whE_pv)9zPLBJdx&otBPYk-9}`H#|l475|fP6;sEo^ z3`)0bK-BeL!iSHwvv2h?14c}J_>4+SGEU0}RL)fegC3qWmbmF0;B+;7_9Z46XVjPS z-F%)TCK+eWCxo98EINK#A8Drvr35iLa z(R09r#0-lOJ77X$M#iulFd;FcN9qokkeD$csRJe?W^5VI0TU8)`si`Mgv5*s@`S{U aFaHnWhH;??kk`-v0000{tcuZ;} z`&N!d%^VNqh!{o`Netz##aDkr&+~r2U(fUY@%_u2<>Bt6rm{x`001=?XYA=s`fu7< zS#dMB2G3vk4U)5W3;=97^xJYMJFdZIsTJ$+Pb@x)7)vHZ2LWU<*(fwJEan1%6l4?? z9ei)rat{E2q%K&yGZeN|;uDS!0$zRFI2wT)%7}>D{p#tZ0dWc|+(ZruK6Qhj&)jw4 zqZJfYPZxbjvR>tM-+*43S5$Pkso)@V2E+a?l0e?l$HXAg1?`|pT3wuUu%O^mV>k9A znzHr7-Gqj~spovuhyJS#G1F$K@~SrME=LV3`t*G;?9Lx5AfGF%gC(+yl;*QiaP&r{ zbd|wC69k9qko7f6+fPoo4}N*9zcVWdsbPuoqR+0_wkM9)^z|2uzKB6nRwk>@mJG=m zO62;bouQSwp4+ka2&?R8!%&9lf+us$$@qJoT++s)e`1xtB#7ii~^b@ad+!Dwu zkdJ>2aL#8P)!`J!cWT_zyYma>ea7#bpxtIpc)iK4J;hm|(bYUd12bg`6*?p?Upnfw zEA@3V884Pmx8haPd3+c?tu=IhShWMZd{%Fqe=(O;@-seU3<+`HpIjd?>iXic-=V-x zjU5Y(^IFWGTb&w3_VCZR$DA@$+uBLPF*_TD6+*HB;bHrONhBhX-^+I6^7pkdiq`%+ zZ`tC329KPc9%U;t3*Iwlv~1g-yBiC7;zJksSOmsZ*CwSpl2d~ZDgTg3sCd;I2T4=K z-)&?KQqgaZTSk&559aDvtQ+{$uR~v=IJNMR15__$RWQ9?x??3~b#bTR$=lIo@hG4A zhj4SXe4oJs2U=}iIt<35^t<1uOjWbuo7~Xs>EHxv^6mb!Hw%{$cU`Q{8NHx~+K=aa z@y=*1*d4$EDBSM@W80@@8n7~0}Ic*`mDzQH$$X@6CY&0G1H5pMX% z#m^O%!u!``zLAm>Z_urE5vHQSrj99`*xa_wesf)FcebqeTuc3kzCAqKyGgNpxGeB+ zPPWI(#hOS|8%5ZWfN0ZyS2|&4)RRQWhu~U^5tG)Mxx*mpy||sh$CtsQM)c}JHzn($ zsLJgpVJBKCCq`K#DT;Q@q*xFen^{sexdS7>h|W;B z0kskG;srFiQoT2@BTbd^_^aAyT*CGBO!=4UfR!e)sLs}t*KP)Kg+b%8ZG(uJSrT=) z;T&$l)d^{Axr!*uJWnDrkPh>r^;Yg*oE=36tJy|d#$d}O8zN>*Y zyg!|xBFa$crZ7<@ZsDs0~3&OQlxM1;va! zr1}1n-j3ei&8T6)-T(4(2I?nVAKc2+3pQ;^+>^mzB&JsEV>&63Xp$p>ZGBfN^CUdq*}@p( ziKZBjBu`Grs1Ka#h^(4a0p=l%U8a(!=g?V>SA6zLhow5aa^g~|XRjX*sd?>FH{|mB z+3^}&|98J(xWy0S=tm`*g~qBFnU&}mOsd<>U&Al^$pKFfDbG6Mt}=?%Y;I&ZX`C6WQrWX>*jETQdl7+KG7gM@w9{@8Q#4&)f#cAbOm(MG(BB zV@YCdDy)73-ZP|4I*KvCL0bSt0>&|tXjJ54lSa2Q4eY*w_XZa+hB;QR_s-G75|N8v%{w7&lUJ~S!<^uo#g63vOn?rX! zlo>Dg;XLZS=JAi+GP@oO006cB5r>M=6UbpG6=H%3xf><@@iq3ctJ4DyXjvkaw zHBJO{HOzFB4P{Ww?XFNlfj}C9X zqA-NA<3&Z$x^v{sU-Bb_Vah>7XT(IXSDaV_$3wtp;oqRO151T2Zy?77G2XR2!{`xC zU<5>%36B9hKFYa`{@*Jy(^42AvXNQH_3srcKe~!b<3k;zMX2Lws5kcbL+J0Qj~)07 zgOU6(vOs)tEV0L6qE<-;W{sD#jqUK1u=R~B2%L)5kYMV9gBPjl_M8Ix=%y=k<5Vsr zM_6`a#b3AE+MGi8o%KGTntO~yWq02KBP+9O$m806mC>X2yBX7WLGv^2MaM7Em}t^T zioHlW=#Bc2_^>uxepDo0ujIU00L z=$`B(_@o^K6qkzxKRz=?4$U%CAvsl zsz)uqm?~Ctgufy%zGV{x@>;|U>BR!WE|J;E&*dugZq2F`GW^*$;gJX*O(ylTxK&8DN>q9e8k;?QaBF#bFqJZuyZYp z!VPkF`tnj=x&qdh(uyZ~J7rKNl1lOD%L;*grgJXToV-Z5fZ=#TJ@*(r@f)wt6T{V? z9Yh79cu`PUsUE-@qJKLw@F6`1?_W_epjV0;X zmYJ@l_v#h;90MJF!f*o>>QqZ*wNy)<9L>fS{{&R)qVM1AAnu}T!djPD3PvG^1z66Q zt{t}+^9g&TVKVD!h9QX!0ERK*Yin^E$Jz7H&gQs1J34^698WDj0rD;^06SE+r z+c1&Y=1y54!|tS;DmOJ{pr zZB*`4XCmgJcE`ML4p&gpSG{-VIy3!h+;uqhc7v)HZQm7W*+7#c=dN>;_$8HeW>(rr z4sKk*k%GaOiwWuMZuEns$%x`9Qy<8Z~o}NF%GU4AfBpf-GEN0Lh5}*j`8eDCc2o2 z%Fc^je6_!2{~#O=5dRzpR`}%4bhfK#XOOkDO-k$X8IPew{*Cd`sz7yqFRM6A#AaY= z-gQwI)O~|0`0e(rrP{NDw;@CKlJ7IFmW0?~`=$|#<7a3N3HkQUD$fXyJg2*Blg!LX z^XxOb8ZdvvxThQJ<&=j(NmDRSBY`A&q557};ep~|C7RFV=ikER`uF?|w_mWxh)I;% z`oIw^m=%=E68OEZOezR3xcHvHYws!C-r6t=zbZ zuY)kn`4a_md(*X(5TOQ(b5rF7kJ*IDOnsC6(^DSHrZZ+N3gdL1 zJPHe@dhH-AahYjp%kYB(zzF9JXsp9N%xS(v+|CMcE3yM!v(wq7eKjN*! zVIVqcIa}BFQ=x+u>}=b2>yB$-966mUJYA!O1eb?5e0lKZHU zu%jA%Fy1)%v256XiU3wUdcHSz1cQyK|5kWiv`TB>ir4l^WLCS@oAg#jzRgXpN){vs zbYjkzmX=7|=vN{aNFU$_RJ5v9uXI~) z@0iGQx@Bcvsg51EXklq@5~qSo<-`)m$1dJbha#0@y3r|WaL-cn`y%+!o$^``KyzbL ze%GB=eNHLDWBZGD>IXZ{PNB6t=CaSfY>i{Oi2YXa`M=@&UoLM19J?&Rb!Xxi>+stE Nn44H4YmHrF{|2&_A3Xp7 literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/EST.png b/assets/images/fide-fed/4.0x/EST.png new file mode 100644 index 0000000000000000000000000000000000000000..8bc918f98bf54cb4ea2eb9138f2ee9daa5bc4745 GIT binary patch literal 326 zcmeAS@N?(olHy`uVBq!ia0vp^2|(<@!3HEb(?2AGr~;43Vg?2#GZ1D}bzG(f6qGD+ zjVKAuPb(=;EJ|f4FE7{2%*!rLPAo{(%P&fw{mw=TsOYk%i(^Q|oVRxlavnAiVRh8p z!Bjr)+Y_N6=Lx50G?!^rZ1~{s^!eXA+e0;98ZL`6XfuQ{tYL^?+`yE;dVs3|pTO(? z)nD2cpEY!Q-?YF_fM{(HgJZLG_dd)hes7mE$;y!^>tGJ5W~^DqTklcu!x-p222WQ% Jmvv4FO#r)~V|D-l literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/ETH.png b/assets/images/fide-fed/4.0x/ETH.png new file mode 100644 index 0000000000000000000000000000000000000000..56361d80415189c28c20f08a1bfe87fbb352d988 GIT binary patch literal 2353 zcmbVO`8yL17#|4@(J0EXGIG^iF>=e9D{?O}#~5;KGuIqRI+P=KWYIz8XhP-8-1lw9 z%tPFd~ zbHnYQ3ovrP007L&|G*@!^Zqg;c^<28kG<`W#0DeK9)RHBU?|GZ2jhka@PPWGJ+s%; zF8~1Sb;f!+)*(5xmnf|D{P}2tgJO!QZJWV4U3=yyFCS_|19eYZCzY`;NZKS8dNl(b zuXOa=HVlGLCNlASlEp?O(Z|3T8)i@nFYiT!ymbsqn}Qgk%yaLZ){oR=AF|$BU@jUxKffbfnc;ZWfntAd@(!`GAt=t!Pl-i!^q< z9?be`ZTVU?UzW^Q=R}@cD4_dHUWV#850m!ITT-5aqH?N3et~%}S*4Ls_h0?`cx%m_ z_mS=j{LlaT}`{S@hIMRrW03cw^R? zvskLWw>vXs;Yj|XdRG1SFt58!bFjsXlXfDHoYuP37T4j#*tt2a9te?Qx^#PPL{q6O zX(GdLLwhc;9QbLxtiRU89#o$gB+d&T_ccF>DT~raVT|Z!131|E170gUc^3ZxR*|9n z==0(zf(Cx9F1m|kmT)CY1gEY)(8Uic5*LA;Be6~21Ggl~J^r~j{^#odm!S+FuaEMIt z6)nXF)R*^F?x6SVF4GG;tq{yWPU6sb4bwMQ;FSMD>vl#S{FD_e{8{35Y16W`)fmXs zKZq9~;q3-sx2`B}5&rc2-PmgDrc|e#Jw8;@^U`;v6Yvah)r*U#0Q>KysI+5B9Q#07 z`b>esCTZ#fi_e8oY^3_bAo-AXnss+rT%%GT-a)wa^8*=xz!bc>s1Sd8kyq7*-z>VL zEiO6RLCci`oXX`H`7>q6A%fa^4-2-tMq;_6bP($6UyhAc*JY}m3>8F{WFERVozueHZl-=5Rh?YbF-G-)NDh}WY|&$;%{ql>_LFZ6cXH{SKjX!xyvG=R^!_}}Tr=9-qi&vB z4MH&I<4dXz$&$Wr9}<39NUd9{?~2#u{sCQo_b|Vz4j}1qTA`(An($^)2Z|{Ziv+8p z>fdy%CJu@(=wjHf;I?k(<(MRJl@sMF3r{~Sg z^KC~%kM?ER$7>Ui)%=5^7A`^3#{2tME7KF^w`GDi`Zc1&G*nZ7ZamFg5pCbECuLcl z1mqOb3?27TP*Z2UktUdf+y+mrlG(8^?Dc9Tc}#@V!xR`9Myl9dG-DDfxj_2Xdv{*% zUVZk=dwsqw{b3%cVECqayIy5tXtpH52fq0dgW`kfl^qPQ+j5V0INi@^|CV{PrP-Q! z_-fZPX&1jq^%?wbO@ONifF74yTO5Ro?Nghkkik;?FukyxzeiQxGl!iNyX3AH`w>D} z--IeiJvCHkG(*{p6%ZHJh8Y4wV5BpHB4nd|5{qF`;j$!vXx8{`D!1OE zOnU(vlDI|{6N2`RHC{6Ug?~aUco2pDHrew9uf;Y ztHX0d!g@%}@tGPmmEV(ov1?45NAv0zvL}9Rp^_ySOfY+0^^k1djU33!kf#je)gt77 z0tIE_3*?ulR)!q0ZzViIg?>8Z7M6pLT{SrymceNQsMvb$ki!zXhF==v5X{EZPtR`g z*}keJ%oli0VcYv9i<_1|5}hY~fGe&7VJ;_q>WtkYk0HK775%d+rDCh%dV{0I6DaV&DESDV#g~ zweB*5Zsg5_YU0x#$Q{6sqC)0$as1*4?t^M3@%Ycx`Id`caTU~r3GvpY>6*aL3dw!`a98W|OyOo;q%jZ=BgM@l4egx~l^2sd8nl4cIo3+ME2Z4s|_q)&5SiNZP-Tgk1 z>O>xZoW!@*2U)rsBrK%#oynmu4I*=T5U#F4mj*v>WtQ~nPcLV+hFk1-F~QJ33%EW1 zx=IeR4i@$02RX8rE6W$Yi%I#9iOW~d#pJzHQ-Fe*EM^N~mPrzR`W*Y!c7u&cU QznJ9=(@!G2i{?Bs{jB1 literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/FAI.png b/assets/images/fide-fed/4.0x/FAI.png new file mode 100644 index 0000000000000000000000000000000000000000..7dc2ac3aa2712833fab65dd48e9496ae02b3cce8 GIT binary patch literal 546 zcmeAS@N?(olHy`uVBq!ia0vp^2|(<@!3HEb(?2AGr~;43Vg?2#GZ1D}bzG(f6qGD+ zjVKAuPb(=;EJ|f4FE7{2%*!rLPAo{(%P&fw{mw>;fq`+or;B4q#hkZy485fbW!gX9 zpIPr^a&}tT3vP`aX4TvGF|M4};2Q67d|#)NIeTDNGi!U<>jd-dT~?w8U3ZE2n4Fzi zd|F}2THm66lk@4;dJp$~I8e{=aML1AS)!V`9F&l&xP|Y#Ddp_c3LTX{)aPT_L`I17qZ$fA>FbJ#n%2^0c=$@mgZ% z`CrN!pH}8TwLkOm&8V7RzhtF9tlKQT_D{}}zgLeH$(%LyjtNWdq z*Am*5f6JyRA#>rhM-OYZZecI0uH<3+|F5j&SIw{eb$$1$elNPhey=Y4NbKC4?X}@8 oYR4H2m=EwU7&Dw9nUTk^qt$alk=bo$VEi(8y85}Sb4q9e04svpwEzGB literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/FID.png b/assets/images/fide-fed/4.0x/FID.png new file mode 100644 index 0000000000000000000000000000000000000000..ac0a1bee0270eed7ce21e4f5acedda46530db3d9 GIT binary patch literal 4853 zcmW-lWmr^C8^)KEZdBm!0@57{OQ$r_-QBfx2#A2v-7KIWDIu#gOXmuxAl)FfG)w1u zd_T;Y>pEY~Gc(V9-@iGrS{h2k1hfPo5Qtbs`IQbZTLD8E4;OgiJ098oo4k~beLx^g z;eP{zMYfC;cuC`{VC1Xo>EIh+?QIVV2ngVH_HgyFwf3^-_4IbkKaijWfhgXoypq)m zgzdwE^fC;yy2ADMv}7Xos7x`Qi(}jQ7#0iUO?@u0o%2mD$yLukU`}sL_Z?T!)NtJO z-#btlnJNgKOZd%An@C8@@(vTu2Y-Ci5j$j|r8i0+6&F=gfg>n+RiL}2MSqA0*=<|C zSPH#Zyuj9W1?wGS5x)2OM1A=%Mv7}>mGHzQBA5Tgi|)lqazn?E{~ph-H7h4Xx6n*Y zWl)!57-!H<~E+JQFzK>h8~^! z%C2q3q?Z(oxhJzz5fKeJyX<_1m&L;4f{~mk_Bt0CUi#mW=dFxJ6|ytEi?R0gjLdye zWZ<><%+jlNWcClGArAH}POkH$Eif`bvEbO-% z2nmSOPLMxHZPCvl+J?0OXJ1#B1}B&4=MXscGqj~TD&Y@ga!eTUdv0F--v=jW!4Sho z2qb-ZxK&KD)K#2(5zNubN}10cb&H$xl#Y^?o25=mQCmeQVDW;=RG8QHX3;gJSrk>h z5+m20`9*=U7ID1ve_QZ1Vd~8i(rh$7xdaf=~{DVfisTPKeoI2i@Y^9^A8#dp(J!gARgiP0% zd#uEFN3JqfN>4Y=W4;CQ|@7WoRqPcMrq`s|*y zR0*?y4>a1LE6Y$ni@DDvx$?$4L z_|0U|&~RN&yva9;mBa2wbq@|x5ba96G*kBVzTB6*Wq$>k=?8{oN2Xr?m~C~&(P$FE zUa!#OloEN91zEnsEOqhtiG~p|G*Uy4l9QcIsg~n(Dw#Vg(&~6O(qMVz{yRX|##qw6 z(z~>I-g|p|?2Tjf@P*#zH1Gsw=hU-Kdbm<_VDVxn*f4;P#<<_CB{(r*kHX{pbbZb4 zClaWYhmcL|xfSx!M%$ZE(V}w8zz?g|qw@dJ(slI@7fzo`$2FD54y#FMuFl%K z_4X#@9OA38v(h4Yi-DP&IY`P>u+MTQY0m=lRsHc?&qkHEc)po7KjinO#76K_I0*n?QU zpfO^}ej!eVL8eq^brX&!T^Yr4U-&BOl7fP{f4oPXPmn;Z>S}|s&gpN}mE9vAJiXGxaodbkLP6 z1a9XQA{0yFB;SxY42^-JoBua?5L%9)HTMhs2Bbo zzQ#?5awU#FLqC6HwOBPjgYa2Tuy;5e&NCGYIxoH87ZPCQ6^n_QAzLesJQeJXuD^e% z+*)RB(kjb7e`y^Uh+V1^tL1b#X;iAJ;O0xFl*BSIEiGc7%RvmB_VH3T8wc+fMj=GX zX*Gq;IH8IPAGXZerm~qY8Q>#iwA=;bkV4{2)&BT!slHM!9G}`RTyfegWw?S}D=Dz* zc5hzuVK$+fl-h$Onjf0ZZxL-xfD`uUuJsM{8BxV)pTybyYlgjcZ%$6S%hNsZ!i%bg&#$gOg=E~(LTV|32OCIP1#FUOjzvU z!HPO(Zx#dnAU-3}4o&Xa|wvZ_sl zh1H_!b5sEP@3(KD%AV3OvrtpeXQI|m?9%s>6t5S$*9oZ2oa7xErJuHDeg8qp$-Pt7 zb>6SaCBL%P{ZmzAZeHu-;2YesYFld$MW+-}@=sw=A1=vhw<9;GTo*zD9%3vd(F0Mg zlte4L>x$}nD`w%8&$$TCILAc_rag5`w3S|)6HfZ^=uHdjnl7t4c+A4)r`a$uU?Ta7 z7o$?GZ6P}cJLNYH^v?SmC6BXJUlQn7$B_0~a^@aA zR^NVierF?oaTi!XBJJs*$4SlOZhm_=Br0{a<{CNtJ5%9YSVVkobjY8sa9F|mW*&dB*4N!9;UCSW_%|NRVSBS3enOG* zy{8wXNQ8hJq;-=&_^+FmXHh?@>qi4=y z&7|bk7H^*ZM;jkr$-rSve80K!S(YQrnI8yXy80TYp0n>GHWt@t z#sYik=3QP2Lxr7%q{f`wT(W00GAIZyVh#jVi`>#DXL}@Y2!8X<*0L-u-7=}PRJ5rs z$-!Q3CTi!V>m#hQ{TP2x(^9eYLqy0MMtjCsOInFpP`AnE-uAoFXd3Y*7Pyb;TE>H? z%Z}ahw`)S(W|7%lmPKtt3Y(75nP~n^x|^F;z8pDSZT*RnLZ$OvAw?>3RC!b5|FNXO ziQDHDo^6W+nw;);Mp>4|`N7S%srcgwa`4)h>2qIei9%LvBKWUjo=ACba42e}T7b$O z?`Xm@F0MAPdD#ZEbtu~@GUn<@pYI)9M>;SK>m&sVz*-)`a;xT<3>}x77!O`#uqKF* zd7J-ZVO)nu#d8@XiNLb8(9%4d$qu*E0 zcw0(KiXrp5CB3B8*Yn60OpPu$FEcj0S#gIQjT1uB7e7I$epAnA^aawyRCq=g$T&1dJ#+7Z7plIiu+zvi| zsw^1TB%#e0cXB^8`9(YqkQor;KTwB_!Uq3zux6JW|4s6sHtF)Zneoh9Ncr#g21NWC z+~l7O-v8}2Qy;dSxNa5|5@Dt9LV~bxKuUq3hK4K-;G@8TSTAQ373F}>U8*dm z2DdRnee}XR+K_%+b%?vG_8<-tf8qAi5iH7DzvaD>j>>}f1O7lcB_}%@tdn!^ZjpX@p+i>I-|u7+UVO{+16mz&8%E zx7*5`n7iMHbq^`Wbk>P~iqHYb+-Q%hdvIR=OK7(Rm>2B;LUR;a)z{zw(jAX8wtQgN zfa4GpQbRw{W>Xs* zlTp{_RK)b7Tmg{*pZrTbG{lpkZDiIDP#166CdSJCjvPRSKWyYWUk)!{%ACmPW)$rVJAYlfnc-rl#6Wg*6nXI#z4R*4TbQ^%9C60+AxQ%q z=xJ{demr`i`Hwt*nsc3%&FHabssb&Vq7V5!9$b*sc&zz^$H!OmwR!@u*0TD_P&bmb zgQ&hya?$`L#K@`8YNxbD-q~j4`*}SaObks4c%-|j!LB^SiaSK?MN@`rX+y>^Dpg;o zO7c-^CQcy*+kboRP%pA|D^E2=1A%B-a0)p&N(mr2_kHtpVYR>@5z3QM#WY}}!=yZd zHHSrSXL0UNueB4AE-!6$D)sil&uoGF#ldn|n}KR#66f#;4-3z?oG+rKdecb-OslB& zm|s>Lv0qA#(Eh#iKm3q9ngG4g(yeqIUOaE4{-l*KKDQ*~Z)NF6$+FRqVesdlP&5jD zfCk{`ciER@PoDK5qxcJ6uq{dY=0O}qM>h;M-x&0r*SOAH#P8Fd)o^CUwHK?V@!EMa zvSc6Rht3$8Cb?h6)65@1I$RxCGgMgu?*2L_PIFtl*~AbY@yb#Fyk3E2(rz;=HCH#T zx>e;_(Q$@IJtyNkYJ6*ebX{)!fj$4X#-#u9=gA383bNA3C`Sl87b6qT{&e_0bdg!I zmZEoJ)&BfxuyS@DL)nur)bqZdspt|VPGBOyc?defD=Jd|-JArVIu@VAXXtZ}=|nI< z>@7KIYKj__!9EQIGkJ--HZ!e3IO8-+3581-_rRs$=k}rIhp8=`7BRlH7Pf%bImH*J zBuB?vPeg8K6#yJMI`MKClysf_E)_P1X9JuOi+zKUQBGw#7NDB%-~E378JAV`$DeN7 z3%4OY!+vP!@K_YRuAs))Ai}Debz{7&5Vd((kJ07p_2u8Y;O?} z?Z@Sr?^hmpvLOL}STvoh`Y4pVzbNc(a4tgpdjAddngF=%{~?WN7)ncwtzYm{lGy4T zTm6;lBNqMp%-aE087lxtmwRM1(CO#nMo;q+C{Uv!kyL&OBgYbB-+5sEfNO(R~m+0{GE?{iO zcj1r=h%GJ}h1;{pbX-wYrNg%0fC&KowXzqjE?jMnti3sqLG71$3gDL4g{SCl310q< zuOrT?A;y&u4e-?)-a#9RqV-v=yLArQC6ne2m0s_*hS5Kj<~uO+7B2s3KcYqu-<@1% zSg8m2Yw?N+^&=9nwXqpq0P8>+8C`MH*pP9MCFf0QMiwFoJCZcF+YMQpVporiuwrW{f+Gc)!rhci2dtzm3 zmiCqQndX+cuSBJ(nVOo)lCpy$`wY&)%rM^{XoUBDB*7SY~fg*9p01p9%1N`u2#6_G3hzH=TB$k z>udPx@XvQu|NU)!Sifi-ZHy(Py)qr`FY&)!GxT_n&!#QDt9tJ>;yX_i?L0tyuW&*P zxm?^en~?)LH-uePA{8XO^}+y7ZJdhd!Tlum8(G2me>Vh)ixZ~8;tD5?x%s4wdmWdN z;e5Gd7CQ!YWm5De@(S-R8|sRVf!?fJG>*Vh70Dr^&|XfmC|BlSe~&G@d&KQd?!!4e z#Nx%m?R$vtIhYVb9v64dVnj&8WP>0Pm$&*&|HkXk0g?uevFLn}AfeeCFLLwE$qiuT z-64MA-MzT1T8jT?%Mp~$l?05rd8CYc4b9Y9Y>tZL^Ci#XqOiMX26k%e!|81^hzbiP zHS7tp=f+?zHB^$b4Qx%DgPX~F{0WxrICanL`?HAGe|?gmuqeKBXinSXdvOZuXHl?d z=KbtEb~`K1T%&vBVlJoV z)~juGi2!gtPsOlTKVo!f7d~3G6y>)^NuM&?Lf?=_@-wgCJT#PT??&@gcz0fo+DJk1 z%{n!zee33|TQrVfp_J6{C(-d3= z(E7WtS#_y^2SzUZTRI zaUL4VSMSZ^tCJ7%(wr|SRB7(Ykahv(t-gbefwUn{qd9xQqMWQ{3w&2DBE6Xpqn=;M zsSBxhrSGporT2i_Y&~$6&chdQ+M^{wC-&hqu#ZK#-?Bke%%wfE34g3Z?FcLU&gg>7&L|a{MQY zayEWG_^nt(j;}xcCVs^Cr+=-*E&jEsG6Qa7Hk(Z#^B|#J!~{01Zr202-L4 z05mX70ccSNZRYCE-SWGH)g>$xnt+DGITyHuvoSW5K-F1!N_u-OyRS@4JtWp3J z{T{B@H{mO1l0jXw++TsFM^FPJ$UD7mm7s^xkri)`A*`b}8Pn#W+W(D3If=UxpG9wD z+y7B)WzuTvG?p4E_+$gumwjk)omV7CaG&rDZj)bT<(6a2U%j7FgRw?iz1p^-?~9|X zy03;ttkWB)>4@mlfiv4@@Q6B__#R;teZ8MDy}r_+2<=1LxE&mpDA{r3T#d?SEZ;@u zV|}@A_XcDEEh)RHsc@Kz)m&ftAy-32@lq#G;?}>(eSRL4X!O+{dMkqTSE1TRkcX=y z+m=sYHt6jhQF++Fcs{{i+f^BFUDE0J%mMPFL86Vk60&|}Oy%-w$p z|9zYAnllxVwDJ;#_Hq(QA)|5oauX*vOkwfAhFi7X@OPlf0E`ap!lm7_>8H&h{-MDX z?LK5t&Ut7kK|k!_u+*LQ5z)2YYG^Q;Sn|msx<461_JB|VW49yk5L8LdY&4Oxd@acX zBYC~M4?k_4PN%j$wbJ!hq*8NpQP{EkKao7qvo#r$XHvBLpv8g`X9Zq!recm5%oEX@ z*mm&jpEBe^VipfZE@8^E{fIuWnfwpdkQ1{Sv&mFJPJ1bl#E{W=OnQzVH%?{Ir$?B# zdLJfJS*`SynT+ILz5J(at_eHWX4tz{KP0XYfbhpUuqOH`K9|WW5p(Z)1Mi*O&W5;CNvvBqj{EvQLi#H+DlO=k4MuX7uSFGmlxeFL6Z-H} zo}2kO@mH@~t*i8=8lR{s^em~orjg%Vg*|i7kab zDqJ(2e!I3Wv=>Z@s@4txz}ZpGlBpwjeqcwkqhd%|zuBUTiy)Kr8KLwGgjE7oi| zX6^mfic=R-=`wO5QLlzG<(uu~E?vcqm7iA5hKmU#_8&!yNzZZS%W2I0_y94V9kE)E zk5o>(TMJSK%t$E@M&bk42#6>`lx$D>W(ln#@^F<&D77=7{aJxF!5&!)9lBO3q>d)+ z3+3b*3hCBQP0lh~6bUW>9F)}DPl>}G3FgwCIXv!SBEIKf@~UvYqW>cV9pA%`E!)#R zd~Urt|28_kf$7V3@W}WTsGfP6`}S`{*4n?4-0f^gA2yb#h>jflVk!aNN~`r0cgn-` zpUWseIZw&xG_ty0=e}V@D4fkCri<{&azVSzi=?wQT-)M-sYpN?R6<6Hk+MWb>~rnW zW!NH4x5IQ@My_w=K}vN9yWV+*?5G&7R@Leij(E-ecfGgzsyKN*h0Y^xWkcLonx zF&i{zFOc}?aQvsg#*8N)W4jfb^~`27#k~VDI<@83WtZ?8eVz8B48*5^s|z@|8AJ9?9rcD-LqmU8mA z6dx}$8L1Lf`C@$gsL8cYLEgNSf^+uBH?_3t-o%!iFL89B#e31Xt$m+YS05(qrIiil z{6BKyTr! z!Z6gaPj7rbm`_|vAukM_Pv-SPt17xzXm#b;uds3Q7UfIr?JtRJ8Y|29v6MsGZ?igwLo#(Ac6r)g0G+kKgOWNTx$Ba+4Hq zts9GieUU$3{i9GQQB?jY13)UPwwzUz0i{CKj*#3s{tN~H8v(sd?KfiCPZ>|pxTgsM z3RFcXZ~4MxGLe>>jiZen?n+nkR&3zf?BxLX8~kbecEuOCE9ohi%F4LEWdLooTI6y$ z0NR=FAg{rO+I89gFbqZ`(RCd{aAcJ zm)-#U`t3mwi|IG%ty=4E)B(`%jwS0{#rdQxz#b?jj@y$7x{FtSyKX8{llA7p3T0XY zV61drcC+RwkwzVW($l|C5Nmyh2~eidQm}4^RkA{8*BWISeIL-!H0lfx0{n1Lc-AOq z$he07Y6buznGN zD=ur@aZsK_?)o^vgMyGbD%i4XFNzMmYqX(J2SD$4Kf4bc#^1x66%%J82m*e7et3F% zBA3fitJV1WR^9&BCm!K{N!biK-i_##;gpG8LEH+X(a7O*3D~*S+`lyH064el%=*|L z$+>X@C|{%p(CKvCxN(D#BS%(QHeQv{Of0*g;}jbJId3m{5BWpZeJ0ze z0|4MUa4ZuSEGHu~i~Rh2($mv%b92LNHX{;M{ULgZL5p6iBTrR`pH#`$gXa+8+l#1} z56JZDjIE2iRr(u!0K|fT$IyvPes3*hA~8NbK4fKOk(!#yo;`ao7%VUBx)&PgEcYg? zWq01|JCQ-2oq2Q4Laust!O_2+_4@1TM*hx-#9}-mCb0O>&n#N`F`k~D`1||g;^M-Q zBb9gB=`SA=+X*dPe%?}E1)AR_k5}BhDvO0D?G>w89oTdOYFiiny zV44EZz%&J*foTdr1Je|M2Bs+h4NOx28knX4G%)`MgLj8}$-G|&00000NkvXXu0mjf DLo8{W literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/FIN.png b/assets/images/fide-fed/4.0x/FIN.png new file mode 100644 index 0000000000000000000000000000000000000000..ea639f4cb8200a62d5659996f356dd14467fdeb5 GIT binary patch literal 477 zcmeAS@N?(olHy`uVBq!ia0vp^2|(<@!3HEb(?2AGr~;43Vg?2#GZ1D}bzG(f6qGD+ zjVKAuPb(=;EJ|f4FE7{2%*!rLPAo{(%P&fw{mw>;fq^m0)5S5QV$Rz;ihj(2BCQX9 z-)dW>rl7T85vTTwl}8pvy7BUvH6#gJ_~cIZ3~yM!GJAtcNJCQ_i(yWJNafD@S%2;` zz3aIA`JIo|%uCyHr8&j=eQnm7g{H;!xPRQXZ?#ePwbQ!-{LUO#*JUX6&S?;Rk;I%( z$$Mhgg^3#(4aynLupOx3Yq-fU!er`N4vhI!zp49KZqzXA10 hGkn(f1bXW)qnbtBe2=Z_4}ej_;OXk;vd$@?2>>R)wk-ev literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/FRA.png b/assets/images/fide-fed/4.0x/FRA.png new file mode 100644 index 0000000000000000000000000000000000000000..25b3d3368c9d704de47ce5ff14484679b69ee46d GIT binary patch literal 325 zcmeAS@N?(olHy`uVBq!ia0vp^2|(<@!3HEb(?2AGr~;43Vg?2#GZ1D}bzG(f6qGD+ zjVKAuPb(=;EJ|f4FE7{2%*!rLPAo{(%P&fw{mw=TsOXZXi(^Q|oVOP@3NkV4Xf#1yGb2(1UZrD)?^c{nztDnm{r-UW|8G>pt literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/GAB.png b/assets/images/fide-fed/4.0x/GAB.png new file mode 100644 index 0000000000000000000000000000000000000000..350963bee9d3d3c160f2f1e55eca9c2445645f80 GIT binary patch literal 315 zcmeAS@N?(olHy`uVBq!ia0vp^2|(<@!3HEb(?2AGr~;43Vg?2#GZ1D}bzG(f6qGD+ zjVKAuPb(=;EJ|f4FE7{2%*!rLPAo{(%P&fw{mw=TsOW^Ji(^Q|oVT|ec^e!ASPn+J zJmd=eae&{5Px9*m?}KV{f4-gb{g(Ebh5SEuGbOMd;A#+M&}Il@Si=y(xB;&~&bQm~ u6Rt4fSA=0q&b6;!#96iyVLpo1-wZ;c?1j87*(ZR0V(@hJb6Mw<&;$U>zgkED literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/GAM.png b/assets/images/fide-fed/4.0x/GAM.png new file mode 100644 index 0000000000000000000000000000000000000000..1f7eea0796de370ec424bebd8204208f5c3641d7 GIT binary patch literal 348 zcmeAS@N?(olHy`uVBq!ia0vp^2|(<@!3HEb(?2AGr~;43Vg?2#GZ1D}bzG(f6qGD+ zjVKAuPb(=;EJ|f4FE7{2%*!rLPAo{(%P&fw{mw=TsOY7qi(^Q|oVT|Qaxxi;xL$Ng zQ|Bl%)XnDnwovq?)wDvfoy(ip#qAnr?mQNue8!dkMl9n7rUcdlTn(ZO+6-X~YZxN1 z2qfRhv$su3FJ61qJaL6^3q)zoyyvgiy1&Wk`}jhXrL>6%V?Y-5ecn_5rDN{bY^7_Q zhak3^Juj~N+A&u)d+lnujhp=haJd56op1PauZgVld9^SR=xGK|S3j3^P6;fq|LN)5S5QV$R#S=d*(a!C^*r!CT z@K}^@HneXC|la~?(}xAq?_5R zPuSKjQkj;;Qp)sPR?LBiTjAIuzXv)?rdJCTAB>nkv0d$#y#A3X6RoxgJgE0PT=~o? z#(p--O@S9ST1VzgmD&=~l;XT#Z=A!2ulGN@%0;|bBd)N2rtk?Jt0)eJh7TSKJ{$BW zodyZ8d$0-jTwNEkvRL9&LbZG5nkC;Z2ZjEdp7Q5z?T_8RuVnJHPFF}$;y!Wb zwNs4r`iv|o0oTqmck{kjOk_U8T3{C2p>0%Tv`ay6?zf#1wU0C-n_L19t=yV$#&18% zVuiwu*Mh!X)7JJC`D`~QO8Z;YnJ>BdPd1;KE5~De?=j!Yr*dalOTE`!x)reb$>F-1 zMMh65-cD!VaGH73xBu+-SOt-N;8pH;aGvSJ+!dBd%Z1-Wy{PZn`4^~v(t4eFMvR{h zIHfE1f6%x$U5ke&Mf)&og?vuM`b`_Ry}z1kF?-#n_e@@H-eyN#*z7r5)*U$MCiiLg ztYC|Y?mlv}Uf5_jm@TXSP_JA+@7qqvSf?kdC#ubVow#74Je%p>?-#Z9_Lt2TMtr-a zRm$fqne$%bx9DjteTA)CRsbV<&60acm)3<$<@C_i{gkVDrhq_D15*TY3~UifIOCBL z<|fa5YE4PvnHQxVF4Bz0*0g-S^EW$mR^+FN)fby~tT{i^RF|b=HDgh9`f4{}U`XnU zKWdW`np9uCi*ZR_<%@c`_0Nn;FE@Wz<5u~^AN6lS4Wr_Ny?xv7O%~FfwjdK?;ZN4d XC8yGL=FR^M%svdBu6{1-oD!M literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/GEO.png b/assets/images/fide-fed/4.0x/GEO.png new file mode 100644 index 0000000000000000000000000000000000000000..ccf4c640da40422eba21ef3746f1bf486fe61750 GIT binary patch literal 1097 zcmeAS@N?(olHy`uVBq!ia0vp^2|(<@!3HEb(?2AGr~;43Vg?2#GZ1D}bzG(f6qGD+ zjVKAuPb(=;EJ|f4FE7{2%*!rLPAo{(%P&fw{mw>;fr0s&r;B4q#hkZueD#e3MUMZk zEm^$9?2+f5E8Yl?p#9`*MpCO5)us0o}IX}dD9k_H}9B2Lml+kw`^mwwpLm9l7*wS z;c@cudGpkY&-#^`7F4~u(Gvge#k2)&Y}@bN@OU;$wl?pf!OUzc##gWJmc`1>%r5(~ z^JSUf41Gst<}BZwi~x7F$9D65n1260aF(~AZk?T}wzezZb>+15#uBNFOvbG2(75XU zR7Pp(taCGaPF3#SowhM2SV{0U+rByb-j%RG3W6SIY#1J*)fDbM(s3?J@<`*)peM8h%!8-@Et{qHBHlCo8cJzmH;uV4T9_R=#~r`}eZBR!YlWl7!cDI0aR*H(HyoAoZ##e&Dj zXN7u4&bBSr{+tJf+D5~#=F^$8vQEVO&WWy@^4ulP-B_8)02w`YS2}3h_Gd|u&G)L7 z^K5nBzBk18={TP0U?7Zs2=*8EE84FVF)&Ihdw)&r!aYB0xuie#wYS{@w`@PXO+?XN ziNP2dmE69Zp5D0II=#9oe*Ud>|D+@0!$lq5Z(pvjKY7P8;RBl*-sT?fb2$;SdGcAN z*|Ym>ZG4{1%Hzz+I+Z`&sOom>voxdYB}^~Ya&nyhdGqHnGoEMO7alE&;=X?>WK8?))Q3s}}uP8_JlN`F#85@A?KNPo6Y59Jt$-nCN)+=B7DirpqMs z^gWTqK-`ll- z<3*gSjVYt`Zw>4sWm^pgM0hsZId>9pSVRU!3{GOfie1-KkxQD)7t5&#=v5L N!PC{xWt~$(69C_`?dt#l literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/GEQ.png b/assets/images/fide-fed/4.0x/GEQ.png new file mode 100644 index 0000000000000000000000000000000000000000..e770ebf7b8193b6169861a2c550c6fae75778d4c GIT binary patch literal 1641 zcmXAqdsvcp6vtn@WoDV0=50i^SSnsJWu|V4L)5(Wq%(Ae5_v(5N(h+CW*VB+L@hN! z%`RdW!^m=xhAD!fGD_20wOBeMG+0cLC8m4d^~d>~^Zh>0`Q6SRzw(`N5i6}+tN;L5 z85N1$1?F@xA}uXIF3Iil$6GIf(!$PP&_eEOj@BtaC}ARViNq3C>U?$RCFyAxm-^vYdx@fMT`0 z6>M2O*a}%yLs5_=sfUE^IlHF$hao&@`S$1kF5r^;!de(@#5SeZ-fP!;XIC+Y7OCs+ z!{3H`eeKTnivo7ZHaF~!w`H?V^oPA#Q2Fcd*}Xpz9CMhOi8d@M5moci_62tp|uQKgnb6Lb9cpUsW|WkZn5s>m4C06#8#O0wZDu+np`O#Li**B|i9f*;gpCRaPvMfPjDz zQFo%p#p$4p<;u!}ok0NvZOvt9}dyL9~+yN~{RWCXIIx_k``A+nKD=N8kt z^eS8FX}bp;*pVzHY1(IXcD(p;+~C_8)^iU(U$MP^K>lS1*p9IzEdDbX7gNo6r}r3vf$&hNA~69K42Vp6>C_ zYo&XOGwlsq@aSRl5N%0r8G8J&eE+>%9c4^(yI9fDB~LOG5HwSh6a^v;{_$8OWp$ml zPuNt^*a*jr3`X1sm@k%rqu+A(H$G4EKIg^aUq=2Tg+GNxYA3hSPyxZ2;gv67x7}-` zS4Z_b*hH&nvpfPt)yE%gUldkVRgv4)M)Zxm1Zv z=L@$tmX=!N3*BO4W1XaZWj8+DJQd0jj62ERVKHd#FFYPHCpR~Z$=pqTt0$%gx*^ZV znx=#3oehZ|tij1%HEld!=u}}S;mzz2`vJphe^XP_=Nh`T7_r9cjlO_Pp*ZStqc8*1 zBF?LIZ2mNjPPbcCMkbSqBfQuz?Us9|iB2n2KMk+cUi8;!Gb}xN@ctN|0P#m9fJhBo{o-!_j+=tZu$)YzwE@o zC(f;$QIBXX$vn*Lj=n)9|D#{MR)DV|7IdR+w*Q&kA7$H~xQSiRUM1{v;`C;kB{{eDT9(n)( literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/GER.png b/assets/images/fide-fed/4.0x/GER.png new file mode 100644 index 0000000000000000000000000000000000000000..0f9d5a036938118cf3bcd5dd9a87218b706d1715 GIT binary patch literal 311 zcmeAS@N?(olHy`uVBq!ia0vp^2|(<@!3HEb(?2AGr~;43Vg?2#GZ1D}bzG(f6qGD+ zjVKAuPb(=;EJ|f4FE7{2%*!rLPAo{(%P&fw{mw=TsOX5Ni(^Q|oVRxzc@G%yusAN) zs?SIb(BbTCz})-XgcZon(>=I>Y5>Di2g ql%W~4?c4SJ3M*MjvS|&&4aVXqR!iO2k|jX@FnGH9xvX;fq|LV)5S5QV$R!J{vMMHWsZM5 zZ`^+V@tNMr4z5WOiEjb~uH3wF^Fs0Fb&mCKuFVg2jjXs>T%xh5CnHi}+Dg}mEXR6{ z6C5cmp8xqAv+~Y=%kggy3OD|L&Tjww+IVyJ$@10q`{(apf2KliO^ITL%^TJ~?^PIP zvM5YuND*rAVVdB{V8rR5#(07#!NZk1qi(-vOB&CXz8GUwp2E~QUhV(zzP1zyjp`;>q0a=Cbhw8Hln7k^4zGCFYL zxz0S^Wy|a8ep&TC?3aAhCFXycVdFZLJ9AENe*LPNCsQ(EnX>!!WRI1OIT;qsd)1GB z_;>lu@sqpxB~v!<-~a3*gY>ntb;42mmvKDro32@!9Hhm-P$ONw@%$^Ut!Bh9Gr%Z;#;qON-O-rB2c1}jiwf0K*f~lg@HZd|h@V(>s<;9CD z2et-0oFKlow=QR0Ncp{xZ2NzTGM^UhSM6xt?Uf+GU7a7hwN*X5*{l9X!y{{j>uZ0T zeUp(s>QeOW=`S7T>F0h0N*n&acxsy4OW6t8$G;_6e4erOQeXF~+@q)WFJQW;=J1Tg`@stURFNv zw%ttV+ZXn%nQXK8)*t%1j3MbPV~oYy_Vcs$sj23Om&-k651uH-Skyn|;fq}8Z)5S5QV$R#!j(&$6Bpe>r zZ|G(_7`@|@irO5<^$CwWV~V(9UxnUT-N{|b*t#Ssb~Z;_&lBFKwb2WFejPG!_Lhpj zQ#SEos#WLXT~WV(oO~L4?qPMl1OI~yk_>4KGZ+)t8YCIE)%}g=2&}#0vSiKDqLW?x zA1aFNWO8q$zp-Af09682yY%Lwv$vk)>=F|YG>or%CS&Kf{@v`KMOW|l-iTK>2>tvXnX$%n`U+?}Nd3($1fB$5n zY8p1bZdbHzi!I`6LULKyAET1}OHy67-*_b|?|VDvyPExXm*y#EcE7W`Z9{%N?U#7G z>(_SQ2(Q39ZF;4(`(CE;|BwY+3bkG5_dbQEABB;H)<|8wI!Eq^;^*fXGlB)qAi49^ r%iND%{EsG7+A=&7VF5WE;fMU=pFB^mZxNaRj5r2QS3j3^P61c9CE2nhTZeUejV#+Z;0nfvFxbRUkT?aT3PK=IU?@94iV*H2 zA;y3W2^1tY6(Eps1c;$R3Vft+5ia8!mu(>|OV%x0)_v@1cV~7cf2fiIJ)+ZGtZL^sL)ZVQgtY(v<;hQ^LB~ z^30d<%#(GoW%q_jx+18Ht!Q8}q7d_V710*C_RLpMg90r+D*$q{h#JLvOKugDJ93?P zVF&=;ho|XFN>@nLKcS(rM`}_d- z_L?|+w+l_5nQc%QG6ex>3YaIU_`Wh>%?@Kp9v<)JWR+m0*mpe~SdulAR)JupggH$` zQ-)$!+BDQaY|)VDRylOD z8@WYf!j(a6xnTn3Qq1*>28K;Rh=6*U-@~z`x4q%miV}m4fK|8WuwqUX*`97A>N93g z*5~1;KlMx z-mFUFS5vCEr=WsFvqACNC`FedvG!0Ft4nfdm(5W>A5#?MQzX`uGez6iuJ(uV;)qX^8T<-p!t0(9iN+#8$|>`!<4MynJ<&H zG>GIlb#}caWaiI@vUuiTHl6Z_zd8^l0U3?}b=@|!IIJM}!xGPxX7RVf8LXI6#k~a; zBpjD#>e_9hdZn{ZhT=X?1`RrQnDG}&G`(YG^20t{=L}4X_!No19Li!%Ne*o>V_g7*ykN}jSo>0APO3bCpNSF;xE~9(GC`+Hpi=8GzZN*u+K}uh9cG$XH&J( zF+e>bh~Swk<5?ghNc}@nQv|A3I(V~m5``OzSiY-}qutiH)pNLrihy)`|J-~*atoUm z?&RHhdrp`t^hB-S#@2r(q8z8uah$DS8mD1O&~VHcDD+aAMLu|KA{D;al@dlFDj$&N z@}2PC(u`wlaqeXPmN{{YC>B(XnS^xyaqPtuw5p6%+pRzq;~uXZlj(ZV~^Q{z$} zJrPxlO|ADQN~CPLo3@Wnbf#?|n<-o7CQv2~cxJRC zDgtW!{W#%ytq^_!x3QKHEo!BRLr(1VXSE?P48MM zzr#)WZ7v#KwPBs4V#|+=Lem75%db27osiHBs*EeTX*OM!fx%L?vzuG;YN+tpd8s^; zcu4D#tTJ~^t>AWVC3Z=Th-QE5Bix4h4XBMkSg%^ ziVPkt&L5F)0ar-j)0cOh_MVssh$md8@TpS~drZ`Bh`&tJr|FO{ixf!{>M;)D}lCVH#{EtH?O~yG*VeTn7%*}Z< zqb?aY;v>K;s$(`CJ(LkJV##n((6}n|CYJOm+rXH@ld*>#8`K!l+LEJ|%=+u|?z z_k!J{W<40-EKe7&oVTAnOSW=V=21h-HpCNsS7a&E6FPZo?%uH@ODRm~;^D$F!+X_m zF=s*>A6>YEEe%QBzt78_mV~&K6&C`|@^o=u!C`)$b!0^5RlTq%wVCaUzhuLaWFFe@ zjf*-?R{|zG1KeLw&W*XXBN|!g2|<9%(i*vRQWNjhrt;uEudez~T?k0FhQ_=+y*F%L zntn`IdHS#j7;llkcN;X!qRO(o8m`T*AGr-0PE+`MzeUL*g}Hr7B{$~O7`H{qB5G$g z=^Y1GPoC+f+?o?M=?w)#BETeQXSS&hf+VZV<1EKO-(?Y68z?=|kIf*v>u!uStLx*P@2dq{`es6$3Y1jp2H-x8-$F#g1> zp9XPE(08@g7 zd17SkH}RsL6SImVy;eRdp|lTOk0M&YniJ^^<1dr(%u}$RrGN(EHi=L}{IxBHOhJ-U zBjfIF5*GHibgzP~X?=8SwY{fYrCuNCAHTh)Z@plT`kXtF^2CuSUlVXWzqdAs^iC#@S}_<+#_e0xm@W)0`e$VfQ$Mx^m$ z@hgFHiD0!v;)N0BI(M3>yx$q~*+bF7FdrZYV9!@*-DDwHBVqRr&kxwAs&wr((Y4!z zed_Q$y4|Y~tdVHlWWk=VAQ-5sGi+trniHn(Sv&2YSP*Sm-+6DbK(uRg?K08$ugLV3 z{IejUT|@1Tt>sj|z@cR>)PO+31tF|CWAVhSGdL;!59J9g#Lzj}q5uE@07*qoM6N<$ Eg4zj6EdT%j literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/GUA.png b/assets/images/fide-fed/4.0x/GUA.png new file mode 100644 index 0000000000000000000000000000000000000000..9ded98238b9067dd355bdf8c6166d4f331aa8ca7 GIT binary patch literal 1524 zcmbW1>pRm60LOnStJI>TW=AJQTaj6cMzN_ihEyn)TxOWna@#V;8L=`gmpw(a5s^#l zoM?2qEwN}>X13;(v2;;wN{bccmZO*FzrPpQ1Zn~R02uF$4Ve4L zIlLEv=5{f8g!JY+2901FwoaVpbD$SrS6sWRiWzu87j9SVrfi6eG6MK97k} zoJPTc2c@YQT&*TrzJ0cgC+qAjZg@#v-wvXN9fXe=0F7ZXqkRCtm@|(XTVTpeto@A5 zYUeEqG&(H+9tHtKyG*17=Hh?T@8;`j<>wL~Q@({yO%lelGfu=C-=VG!gbv z-|s^>U~wVKhdHooIAtN=RayaS+34+gxrqOp<-h}~Guret#jP*z1^qy1qgli4%>Gpk z)!N>5QGr|B()zACv;?G9shWntR}rG3tU~wuO@kI}oQ-;{(f5N`fydeS+2XwS*$S2& z=}14pRhsJ;kWK2^JPW$SSr3|JqE}F8DEbi+ET+h1jYfyvQiQs*?-nrkXquuArnr+2 zJKW9Q(0kJ=k8Yw;CGphiawucA-GBXK&cu-Zd@PwG+N0lQ~I!VbxBBI(9*u{_PvX#MgYQEA>aS zdNyjKrGS3On0fL>R*2J+uM|D{)6uwk6Dprg)LpE_M@470N3M>IT~ya5

    7>TX>XJ zE?qcVQv3zo9uX~dd_BT3s}cL$S9%5r1jA{Ye}$j(Ua?eWy_JVmB`e034hd}QhbDA_ zjP<)8^ZfGXM>g?}th~Rn1kN25EQk*-@9`!_OMgLb-H)?###5w^Y++IAG1fFwa|;0y zxx9MvrAmD%k{^eOJ<6(l9iCx{!YXzw0Yq*17y_6iBvuJMD=BjM`R&%lc}^(kI{5YF zmY-+a!mGd8B4B42x8BfKUwMa8-ehj;dL+DqtAu0f?JK^g7ReRiX<<3y*tWvZjoZ4^ z1Sd=-ykn;GkV;|L0)=+1$=`gfnR5vYhA8pH^&tx}!HhGO8EB7+g_(-Vgpi=-1)1!% zE+m^AKUh~G1MO8V!cfbbgm1mfGGFK~cZ5DXo6WsJ>n2lQPI{2wlZ1P3vWBdeHw|*R#GJS{l$oaN1eHt)DMC@)2 z-Hei|2`)gUBf3?V$JIBC@4ru7E*ej$bno%OZ4aS+UZU0}H^XMo747Z=vVjAaB8krC zX{;xW(CKnZmU&re44Rdp_2c_JD?vI1*0p{j6|}Gl^e+C`HsEy5ZY$k!8>=7Z1NwoI z?gwY4H9PNd8OW3I%`qp9X5~+}1Fy!tD&9zhIh?$)FXHiFRGH5gg~1tePbwa$K<+(7 zhjoRG9i!+q5~a=5=xW*fzI&ycHrbVU{BR|BWbRdsd_Sv$#?^HM@3M0iv-lq`zE39c zVNxk1_gG1zkm#QM!}>(I_495~wt;u>sn@J8^+UH?PsNGphX3EDf0r!7_-OnKbj>r} R;<;i2_&xsE`dyK%e*mlS%a8y7 literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/GUM.png b/assets/images/fide-fed/4.0x/GUM.png new file mode 100644 index 0000000000000000000000000000000000000000..eebcd75fb2834432a13167ae839da7b40bac2ba0 GIT binary patch literal 4073 zcmVFybiTlcYD=14oEjfw)YxPSs-4MZRiAVBtgU#lur_1^sPph8R{i;`E2e9x(q zd+Yt~d%yd>cm4f-w<0!?sdE{oK{8u`Z#ym%U@|TfU^0|5GtJ@AyfW_xX6f>+z@6LP zCAFaODxn`?%LAay{Py&AD?cdC-qq`Bhv44#0?qKnb)#R2H@xhFRv^yeA z*7E#t$qYdwI+}QJ!z;`_vMc!VMXU>i0AOtGNtS*1S8~e_nW^S^ zdz0~nD-zp_zb35UT^#Sfh?JQQfX(OOw)|Br*!S@zGdF}h0h3A(a@UrXSbbgql&hx^ zw*IfE3$Fn{o|J|2!Tc70ho1U7k6!0i5l>kruVrbqzRZ_Qo!OJtBuwy1lFG~ z#s0=}L_#d>z3uokzagl|SRP!0^PT4qBccJL*WCNrTd01&i8VMoLY9EZWe2&bU?Tz@ zBpKW9e~RUyyMot0C|}2#iuL`cFl*URH z?Obb2wz7o%r6*DET-Z-$e@hYh^*aZY70HP>-hCcw!mGztu&Qhg3sdH^G_%i|D~(CT_Utb~2vonHMY}!>?jb@2Awz(z z*Tc3*>3n+%=P?c2f9eXD)UiqIaPQQf}$N!E^v7$-}mEBeBYueB9=}++w zX*Z)v{Wtf@+?lAi&I2H=p`4k=LTe}r8B@g7hj$R}?gYT{;1Z-UsRLxf6bB2FZ{X_q z*(^WsEMaOm>nc9tzRY{5=&In&<8M>lQ;Q-LZcV$1nQ_wqus-n+{?B%!x3n?$z~>aE z=isr}hP8e}LH+@;zd4=hwmR57ZtAcrVe9p9+xGWSv;ZPG5&H{I1Y5t3&XKmG>~A^9 z;nrg`ds}d*4nSv5<31|8t7!JLu%==IIy$koXi7WF*-^h6hhk@JWCp@+MYtmHf4m(< z^V6$ZDUVAT_Id(LE$dS7@Gt)hn6M|QF!a~i#hWw@$1 zBupnj4fvROWH$g(j0?-+>x0);l)Oz|OdgNrJc>{AQQUTfvaU*2m94=W@CN1bVsf#| z_Fz8J63N{exA%XLETjH#5#Fa>!r^r@<#-`GvSwV|%AEsDpM=NMoWSYr0YLfTtzfeP zpy?X7jb6mOq&Y}JV3jSntWoS~+}rQvC)*QAawhS|qUZU=t|!>rSUBXfEO#sjE=%*u z4qn7sojXh?U}EJFBOwIp8LeLu5)o>6Fu$vzfX54dM_FeD^G98WBm_}dsr5FJ5|+%r z<^BY(=Hm|spX1%q)q_9G=|EXH2Y|$uI$}Co2bUX0m`Z@GX^g8mZlrTFkw(T1D%a)f z;xEOoF*|++uV3>LlVftwbe##h3zrhkx{3{iTf+HO-p`1&#;~$<75kbF40%W^M|PfjB2V5rj=`lo)EizJ=~x5 zBLo6(pqIjCqje{WaHd3!N7HrISA7hC%Nog(SNtnWv%Ww0&8!yW*?C6!)>_jorKS=v zvbD}gSS?6n(}t8=kUWnt)rePWa5v!7eB?xDlO>|ba-@+Rl}UL=U$B!v5UEBCc|Uo= zSmUpz5!crE^(*oA{H$c!9&&i9ZqGlJ=a zw)%4}cQIgE4+!^k1=C`P)z4WWBy{cko$pd|d@!0K!rgTi>5B`~2(b0`ZKi~4@bOyh za^pBwiYyr~%i%d?uJ8x2_yY(bffhhU$L{t7%UL~MEPi8hwRgK2Fu2Jw!eJQWPMbi? zf_Vh^`d*k66+Ym)rMecDzl49!0hHf84}kXE>j;3gpCUHrgCwDA0o2u>;``N~f@K5$ z^%~-(6-YwRamO;OP3;2}2sb8md#`8UwPA#51oUfpVU7XUwbh$Y?^%Qt5r&dF%IF1W zI#YBILPpmDh~s4_qm!{$*8(798Jok7eP0o_v{8dTUxXP1*uJd<1kPC5;O!l7T~3Ha zKVE^fcs||(N05X-3uvfX0Ge8nlM)e#)5#T-3A<{Fj75Z+y9=)O(@#nu`pOS;71B-7&eqjZ&rs2sRgAfYV@_OV) zeni)g@5G*AIB}Gz69`;?4RS^*A}15wG0^O+cNmke$6_CFZ8%}tGSs75gK2FL0yG4k z2k$`$fv)SIX#_GyLuTI^UMMP!Y2moijX0o=MTYCqJ@-mO;I8it{%~DK?=&3$Zj(J@ z=Jf#ejEXmXg;i45iL?&>k}MG;@OGa6PFC;ihEuB~?->oj446hhn=^vIspC3E>|Pd6oU9VcsMx$uvd80HBqWeWk41=Vx@3 zizr+F@%oU~K$qrasyQ;{G zF;2~_t=K@aa}>9vF5p=EamH-kz^Pe+I@bm6!oMMyOh7!7OF{Sf2^18e%$hu?T$tLo zib;-)M3P%*t1jTatQp9Dh&vo$X-qcl)>`~DGdb4Atb}nWlCjsA5OeLR(k0}@!MG|T z=e9XaKT%^U0Vm_rg6XymTkuRAXi&2$3c2z9hn}Rcbe3lRjN;~MbX~{eJB)gAKauH+ zk<@`pPQNar>4rH&R5xHd_BH@jspoD4yjU>R99`#%r#Zri=U28Apy_(QkB&}WLOhBz zP|d_}7a6W(I(x175+aBaJu}2}oq7fDc!s?`O z*DfYcp9VfJT2Cu_TP^DJI}ol>D5GcMKc0^wTaXem`#mQ)vXOC3d)QZCIO1zEv+*hF z@K$yZnBD`bVv?wdHkN{xHJ>5cjQCJx{9M%OOOUhXAj0F&13t9&IwobMqIWc)yW0_x zf|@fQ$z>Saq>&Sm!ea0r*@XY#dcyTaGF)}c9#zbggcH;}w>@ak`!pfPoC<#lruP7V zt=Y4=W81q3ua}f{Zxiw8b#!>0)T$%t^(*kD)_fW<_AXquPP%-p1T+bOjyFA(#sf`g z&1H1$u42ZbafCUo^wxLN@Ro6ewJ9Zyl34SH9|#cw4kc&N5*0&qTQfa>-pGW9(y=5s znWPIEd!lIfIOq*1w0WE~yTj29uK**QZ6t=Zphz0399V4%dO+j&vi0bijxI~Anz|s^gF*oz(F4Hf1*>M<#IkiSqXhgM{iikLZNHBS zkw$S-3V!YEZN;JV66rUM2xDA#svcCif0SG6M<*%&?N$I9BV*Y- zVc=;?vk_7R0OXIKMMhm28Fi%uwjIXy;{W1#_#UEc-Q>iT5^e7`4sV|Y-NlU@zIzpV zK*Oh~tem|FzZ`Pa4V^jQG&CXc)>VsXiH-%p{>n-+R=vvADMiHCyUyw5yGz?BTC@^> zhZh~edo#aBtt;VTtA0TUIRXGYs*P8#zMBqb1n4>~zy5%-$2Xl*+3v$l?4P>=PnmHJ zYh&JbDM}fCG1dKA2uT6}IvnBr?W)^xpK_X4{&5GTkAH@)4c!CU^DFsc?i+Y(I?)kq z%=<3+<7W<+$`^!?B> z2(l`ROW~bai)eQaeKsJ}a7hpV;FDEWPg}szq_Nz*dp(go9rPSBLQ=e{g->(muyfor zbP+nc+;hPtL%?Yq9XXbhH>GiH(KfOx4^tkO$|t#V=(LBKt=a|Qk|O}Xr>J~1`C2}j zeC^qVLmVML3IBh`WdcmbWdck_zd0b%-N`?0f8Q)!z7>dXYv`93m;Z^6xwt$8nT*Q> bn2i4eicg#1T+fa-00000NkvXXu0mjf{DRf% literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/GUY.png b/assets/images/fide-fed/4.0x/GUY.png new file mode 100644 index 0000000000000000000000000000000000000000..8ab03d3b9734e85ad49f0f27e755a3dbb2b6696a GIT binary patch literal 3160 zcmV-e45#ynP)2bX zzVGuquke>8@0m2$Rz&SBNT$kM&Lu*NI!cUL_|?YmSohjaZW?e0HI_;)R-X=czkV^O z0Dk`YXGY$7JF0WtSTbQJ8>jrljGO0^7}q!4{p;|k2lp#pkawyMlgUJKax&e!cjq^E zj$?SAJ9&86T!!_&9gE#eVP!tDEQPzj9S-&2B-?_#0vP~FlGwFt7Z)#{XWY2oqETz; z(>aMT112%yny2U*-GlSh1=Q44hPm4v2jQDR3G$yVnAu)y@n{%3I*HWO&2;T~gU4}6 zmN;H?l-;^yGIc4`*-WAAZ-+xTmDEKB*kiQvy52~oB)b|1^b^QfH;ij;oCk4_f)ML* zacx~C`%i2p{rF1qOMeP&&pQY%0RS%ABwp4VIcm1KPKb|%Q4dz8erc?s29<$Ijf(*^#rJN>gdDo}pdSr~m*(c8Ph#M*dglHc2Cgt4RM;q*o8m zKO0RShhp+A)3w0IIO zT_JMQbt)fMs33Z@z|?>(`*3%vMhF< zEjDn@=I{%G0I$x{@ZRq=sC<14E>sY8i$jdK$sj~~w3Zo*IdCG4jI6bsD$i=obF^Dp zBmh8#U1n~Hf&VIos*}d4S-(gmHl`UjP<4jb-$BAm&|dG=>>SSS&D)5{YG_H<+9fR( z0KhK8pUO>aQ22%GhX`b>)Y8AN;OEK)bh^ReD`Fo3Rh&mtt)+_n`DtWitsz(ceM`I8 zZfKPNPQIwI@OGKeHD9=_tH7pZBBO8hYmdB9B|z*$4)YXsn^$u|c@DdE$!yPF+u>ps zZ4v++^Mz+i43yg)x6z95=|Yj|5BYJ<{t$;k{1nGTufZhGVmI@3!7ehg*YMrhgUGb0 z>k&bl1pp|tN<33yAm8F1-@i0n!(U(5pl+oOO-=F&EA!crvz}eKpHpruX?^FzOh5zx zFi0{l>W%C-x`&hZ-Kk>Z$0A)4THqIaAr7iv`gk1Fmw7ZvvYlh+4v~?)hC`>eVV7); z+$1sym;eAW@NuP?B^74Zu|Y`!+g52Ad{rAo01&kG3xbm&W*p%8d6gP2lBr8!Yt|Yr zR2Q`U_d-BG1#q&d#=`tEBi8ymck3dMwoK&iku7x2{%|dQ%m$5T=+rsM%haWC;ADD4 z$1H*(0KhS`joBpz%Icl7MhzdmB{KJk);fj8P($=x^-CWUK+t%!I%5`_kFVrJ1ja0a zCICR8P2!m%1Nm0BbDsH_nq_|!(F9rt0QKb%_n2dnr@7Lr**(ci#bm7wLI+w-Pz3;o zbADcL<{OH zV8YX&>F?Eix+0fdx&H`T%pxQLIB7U@?$*UIa~=~&=iDg%m&vX6It~}?40+5VqyhkZ zZL%@1#6Y#=cFuo$Q)J$g!Eg$VpiXqmDH3LYCdsSW8M8<~zLKJv!k~5~ zuBE{04tLIcOwA|nh=>|L7X}>VSMi`@o}nNVF^jMW08k;x%q{j@)(*aN_JgVm#7=^Q zIgX@*1_Uc+5jFt;tTMb&Y9vjO6gIHGW9B^M>M)1^fKWfLm<}-$y}k+vY|J980yz1! z(#)a?vukZLw9eTNsxI{-s5xFCRdB~Fwq~tHr~jcmGqbpsQoFlL;c?D>aE+jz0Zl)z=9b1R zB1!;&BAdk1#Rjq!v4ydtRDAhKn4EK?s3GP)NO%^a@1bG50-6`Ih&Tbj_wvPnevX-Q z_?)vJ!~u?kwD{>DcxNW~i&>~&=o+;kgk5bGT0!6njf!-G1?NnmybA0RJUAv2d_n`n ze$eS=&|c@$KL~<~KAn>o((^`YEmh=~{^)9p6wB&CMy-X{OWn!Af&gzlui@?IBdEJg zq}MoUYKbF)p2(5zvbe&;@+$YDT|yjeSgd8@*f0-Coj6lzeAG)QX>KM*#0XF!$vjtL z;E2g>9|t7~Y+a#c=zuWWeP`NC6K}gS__HNz6?ys}8$C}+L$u`|2az*(DnZRq~d zDpHnc=^P)%j`>L(bJEuEXV?YUZm{qNy~#BJYkt0IkWj@#?8A=uf%eypo>p9Yj(vHl zZO-xzn*erTak+^VRdueZN?Z)ATO=}RTo^_=;!sGK4slZ*BKRN}iD6+8pi+`~p~S#p zg}uM}N`b8_wOl(Wg!W#Df|&d3lfS&{-kjv>zb8|d!X8~pz*8ebE`ZKrXI8O+LWRA* z>lPK8mWlM}7NS3290Ku=J0?!5u0BtxsjK8b2r58BD!_iDm6!BJY9-ILo}gt(C~3yq zt_;8&F4%$9Zt;0uAPI>8GO(hmjz#4r*UUDm6D(gSGIdgr|9NpR#6RIP+1JV0$`kC( zPi1G$hMpFdV38XI<8F77(hB-|n_Cn-jS_f5t0;|NVVgn}> ziCLp=Qjzv|k;Kb_VDH6&5I+^-rh&S<&r_TZHB*<|qILUV5i|h~8m+vjH&WBU-Y;Ds zqV-;{YCX+(+pEnL96XsGk?Dy67r>Xj@1%uK-Vu5FkpS9zaR9_mbBx;?q^v{xslp7X z07gmXl~N;n4DN^R(ajOxz3Y|$*n4Msx}Ueburi;0`CqalXFbI=XWRapK_*}VoUuyG zY)~aL{8|-TmWy0@d0PuKuC6c8coNh-yqcA{p6#u4DIfwIGFf@C#7LFG-cOmRX4Rh+ z`%1K!`n2$w^YmaBV0 z+!H>_eUuJ0+|`JYikAQu8Qv^2vZ=;x?|lZ#qeeo)JfCm-FKyGMw%{Q^kzHa=v4I~H z_TD2{?x&2$xlhBk?6sV$IvJYYcK}=h95dUP(_o*_d+%`UcG9)y@cMK-&;8!4b_?H} z{U^J1$(I~zE75?%-Y-yWt^qJ}ax}{pPeapfE@*${agj6c@zRE@ZAX*ZQ^Okt?(OKK zI;mK`WExXvF7?QDch*B?>Qeae;*oIlwL_vg(%J0>`u0gAeam)6j2HoUZdHHDp#}#H z;y|E?QKLqYmX=0hVj_CO1@`E^AS3%T&R3lZcON@6!uavy<=(w}^WjH}IC_2`nYtAI yQ@H1nLk$ta?yt7iWDAGl_U3Plh%}8~8vh3GS(yPayR+Zin^Lrnd6YK{peFz zP9)rro0hliiZ70ov3(>9In5P&S4rr~lm*EWGP+Zf#U$X&m+xz)C!hP*vZuG+cEAkH ze10i&`3X0he(mmj;!j@o&Y2vC%IQ;i03WNJbn<{rp}3kWNTxzi)LY=rGvKWcfcS59 zIuFn?urO=28$g<3I~%+Fe;`daKF?DDW8LccekL<_@#D>AbJO_4?@-whxr>|rs*E0g zRn={+*&Wt}Ltkn!ow;kP=W8K2e%J7-4p*5SI;2w-d8{gAf?@tt!XsaWd;g)Qz@!vv zVRlDp0Zybnm+f8Mmo~> zcf^PUPN`ie8}c=MumsnD*gqH!A?F)tzvkiGd~&PQik!Kf?>9ETuMp!UQ^zKY7*nQ>r`o~JWGJdsU!_siADipLaGI$G?wxI9Qo_w6R zwaR7Zm*?SFLWQ85mKQuvukeEF=9+8WKPe_#>M=J_1SQx&W!E}o#<#utHea$J){tOy zCVqoM@7@}?tHRf7+Bi5j*G0z;U-M;_T+n&*hw_QYZuGcIMpAuNfcSMrQ=#w z6upb}zC4gLb2}ZV(o#`cE{&?3$2rS+oUVWF>DBB$xD~3`OYDo7wX)8jS}@w2BIsEZ zVX|M3-%L>8ugZ~9ZdWkryP(I{C4YELWl~<}z-%u~{~SZfZanH@Deu&>qF(obpb!V| zP>&9UKer@_LG$Xn2xIC(-NgxEBR0OFKFaEdH5Gd=wO42~K%X?W0KF+j`oKDTC)?oh zp$LqDwciT27_+Q42bAI3xRb73a&7g5@DYr!a{X zV!uei;B;#?fkO-&Q;nIHLPNnvWYk7af($;nvr}TD*t9|jPDX6RW4Qa|&xHJ4=_#Ds zL-)qNadWI@g-NqMkMQ(eS9LlB@=>vbK0&0VO|)g?BXi_$S(|XS&o;8+k}8mL?x?cs zbwtXPaUJ5bmo9G|f=hoBk+0KKypTqED*pj#vMRgP=sssvLQZl3pAUh(Dhta}Sb3?> ze0J`Nd9=E5kPm&+1!Cxyn9w#$1-2>jI^(Vp*dSGnz>FDLlSuocBt%1P0ebAQ7HGQs zphm0WzETHFBk2;R0p#b1M`HYh?yMzzT@+P{t46GmiGB=tyWivq#t(-ymLN1N2}Scv zNq4MU5aCo`3Ux`aq*izro;E?xnR_LNr<6O##Hr-XATD~!T2&FsygTN)DIMbZqv@Bw zx$Snm;n-JyTGgJKr6;&ng|zba_CN)IW-tIP#xH-vD>LCdQTrkU&?EF@G>-&x_RKuo zo=4{nei(6k>lzcKW*2n^3IvQiD}t}HOt)Q=)9}f0YM9pMPemBz{_BY~5?VyC|IX1Z uW|R`W&&7jVdUcvU$XELRr>31$S1OPh$;`AD_g&n3bHLfb9adu(oboS(8Q}{6 literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/HKG.png b/assets/images/fide-fed/4.0x/HKG.png new file mode 100644 index 0000000000000000000000000000000000000000..8d05655c25891f5ae7a8c0ffd05b408d5627b7b3 GIT binary patch literal 2559 zcmb7`_ct5*8^=?PREwIuM(w>_;ZoNOiqzh;qSO^5Tq9OdH1;ax+G4c`YL8MSYO7i` zQrD^-Rk33w`F_*C;CsG5JkR@lo^zh_InQ}NpV#>$nVT9g-r>Fj000=FhI*D%b*6?P z9f+E5yC1s#5g@qYqmn&PvFpH_1=zma2p{7anTNHK zg*+8p!&=7oeZ=;5Fm5xO##w3IHEGJ#+1c>QS8vPht$d=_>8nfQSsSE%#|V8ZtNfdk z4mqKr_K@o9%LI+d=Y*A=qJv`O4SNef>Fa)Z=OE(ytvAYBc}PVW?7RTZ2y=qN@SfK5Tp-A>?r33tg@i_8m6 zaAwXPE}QwNAN5x!*1@bJj%v!q@B&^XBQ|`?=LtAEQ6gA1|~jj z*0ZgjoZQJxVynL1;>cfCj|dg$KchWFGj^E}$ zMc$jeo;GmX8ftNWt@oOe^4sObhkc@U+?EO|5*T}zTSQvrw|g$ekA6(Ozv_Ld9lL2q zqe%m_c)N7)aJ@Ki_Y+pb2|9Z1RVkUnQS zF1Ox(;mE^vi-|QjWp@|;Wp|fha&&FI;19>pt$TyVhBu->`0LZ%RwpG+{Ph>b)R8e= z4lc<;TtzV2y3?I$I4e`)`cRkQ=qPn$d_Q0(oCOG~bA(d+LxR!#w=d|X9vXy<%B!p9 z4+QN!$U>X!zZTYe#k*IBP!D;Pm9Axqa1};LWH!6e$6tgN$ivDtwXm`(Z)XHMm+80} zEsf||vczXRT3mRP8R`awx^TT)W0I>)G4Xvm*!DJQUXR2lppZ{scT2--9 zkEpT1%Uq}RuUBW9=NwAhnJb|i2cCyMbR7WV#_$X8t&#{Gig3Ds$JR~D**%5IO3Uy~ zy4?AOVfAM~B7bD*(Wxf@h<^3$oy<>hQ)aagx$V=_Co6V{rr%FvnEs520yLTyr%KA! z-Iu*aQ0>1WMfl!YDaGkBz3%aSnm|$cbL;&*Y0M=hd}(ej_LRV=z4@(43D%)KSP$FG zGv;9jMt&J7T_2NIBh^#4`Jhe3Lqe+DZ-VCzd}4r^e|33#S)rinJN9Kybys^%-Z0-u zP8$mg{++E<4~2UpLomP{T27iV`?OKEo+AHyRYln!h1H{@Ww?JNNj?6;#3e_OTzbGx z8-mn?6fd_m+RRfx4_P!JIIyPZe~E7%gy&QucStMqi)D!b#wt@6W9X;6Ore$p`#NjF zipj-P*~%6?)ROf~`M$~E;P)eQB%YbOo3Cymm5jF-m@@BUWt%?x5jVU$un|aOo|UDW zvivHj4_GqJo!$=_MKt&aPYbC>-Kk>%cKChOS5b!>9 zz8Wt{my~53-S`ywlhaO%!s+gi&pJ+oOl!E*4fZwqEG8B!k5^6DSbRET6W3;E(QEQ? zSon(Sss!0RSLLv4sKm8;T1QY+`Epa zV7iwk&mqp=kGDkXo%UPqc%@T^g~7(-vZW;#KXQpAb;Qy8JN6i#g{%sCC@tC9Ct`3L z5|V;m>w2a8`Oe{o5N^|A2I~|8-BwFkrrz#{AfB#0Ik}G{FwU%~w5@vd2`=IioE=W| z3!_B5V&XGis5B9PdW^$`^wWgANEgbud0WcGPemOzRx06E?&8jOb%)AyIYA8OHF4;2 z5*?lnqRqhqsqJ4o2>=Pi$7WC;T*09qurB9ml_{-rZ?oG9N6j&>TH^TUW4`(3ovou2 z{InsT+4p~^X;A^X`pgey&X*0Sd*P9)PlcIMv1&|hGpm)C6q!S|FE3MPjzVsRDOh>c zoVNsh74bqMyO}N%uMfnDqvDj%y^P2BZ+x^$oA5fT zEGXN@JTvODR@VMmq*?<&m$lg&uD(BLcQ7&Vlzf~*YZCfk+^!zLe7=NC1jQ^JRk})9 zu!)x|aC`cA93qyMA(sh$YJ{+(!NuU7+HzD_VcN*_n;V$xOLxGPCb>1efmmr0ZqYCd zeimC&e#Qh*q-hmnBXh_TW~<5?nGHOvz2EeBt!nCZ7dL*2c|l_2Ey|o49t_g5*J4!? z?#;|k5|jDzgxKNe7`Z1~kepIIHtB2B=&7VJZoaA44i;MBc^Y#H)6RrQnZTqd8r>f%&c%^?$%*B{y&Hk+^)))K;Gj`#Pigebq^1#ieBOmTXjALavaOHbl z!I!G*1U74$i^8dwH%HZ@iWTWO+TAQ-GD_`f$*|Vhxi<2#ZxfWCf%L{e+f2a2X@F9P jp^-?HithWrg~XKnR4$LnvZ;gmc>_Qnn(Ec-IHUdxdW-q5 literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/HON.png b/assets/images/fide-fed/4.0x/HON.png new file mode 100644 index 0000000000000000000000000000000000000000..afe4a38f9a714b2d9e5593da533deae1687bc46d GIT binary patch literal 905 zcmeAS@N?(olHy`uVBq!ia0vp^2|(<@!3HEb(?2AGr~;43Vg?2#GZ1D}bzG(f6qGD+ zjVKAuPb(=;EJ|f4FE7{2%*!rLPAo{(%P&fw{mw>;fq_}f)5S5QV$Rz;XT3uLWsZG( z9&=oKpTE9K%|3GSz;M&IPml^L^1$Qip5r4P; zFP}T}G^35b-fdyKcZQ|l`5nWQn__Z%<6Eya7#Yl;sGE_a+Otz$(4;b{{9w1I{i!=< zep6D^O78N1cGS?Aqj%oxh1KHwha$eXUf5xK|G*M?QC728UBa{T^@DzOM?{}j(qXmP z7j#T6^?px|Bm2wCK@tZ~_&>QJ#ul#&8wTQ7S=IyNGn=js&zDv#D?pJ=u1od0;i=zW9 zULCmTux;*-<@fu!6-A!yn2@GfG2NNnAoh*Ui6DoxT|1eZrr2=Lio5OdQ&9Tz_bh|* z?QHWN>G-V8Q>%$R7CGUf?hB{qI$N0c?1`Lup|nZx`TE^2;`@)jt~suAX@S9wu$dQ1 zA1S6g7Kt5EPj^fy5_3@lQzacI&j*1L)};U9M^wv6PGE?n_>KEZM$n$zW9mM@Y{cN{ L>gTe~DWM4fIL3tE literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/HUN.png b/assets/images/fide-fed/4.0x/HUN.png new file mode 100644 index 0000000000000000000000000000000000000000..c8d60ec6b01b9c5bee6ac164be8d10959b8be6b3 GIT binary patch literal 315 zcmeAS@N?(olHy`uVBq!ia0vp^2|(<@!3HEb(?2AGr~;43Vg?2#GZ1D}bzG(f6qGD+ zjVKAuPb(=;EJ|f4FE7{2%*!rLPAo{(%P&fw{mw=TsOW^Ji(^Q|oVT|ec^e!ASPojd zJZNE=yyNhBrNwe3nm3jh@4x%L^c+|53+8|87$O)qFeR`a;A#+M&}Il@Sc6yKw(;e7 vfh$ZT=m43SbN~D+eU25B99~HNX{s!uRGV6 zqr{M$(<04Lb44lFzUy!3dEWQ)KF{a3_kBOlhk~-RmJ|bv0RR9=8@Q!CPcb~gMGo<% zC~n>Rw*}TVZ&L;t- zo_5cJ02-QzCpCo8$)z42d%>D@rWW;tdfs5j_b(2NLdgs}oTwm2kY5T=JOW9FOSgRJ zq5#dMlxE=OF)r&#+&tLdn23nj?CfwWYp7qKR4@mv==uSCT5SC| zh#&QPT;i=WHWogD;A+r?6MNnX-s_kPzt$XH4NB{~O8H?Wp7rE=)FW5~57y3ko7DYqlT2TX9iQNoXykMwV(; zKjXX-D7W-E0~xs+*vgW2b)C*FWKdYWu@Z{2{aF=|443KYhPjQ*;i+HLdVlwsSKng^ zYoL;OlvM}Fmy~3F-7xRFO2n2y!_ytPcm-)ggRiayP0JVCdk z(a9n2mbI)?-(PX^^&YD#n`zpRsF}EQ`Ep!mr{3+`SX;GtwANr-WDvUfo3OU$YTru3 zr_CutVqL1E2130wiN$D@h&*;!>Z#VXg`(o(9A0QI(ct8sfyqf3DY6im-bFLxcb}N5 zt5k3&-_Ya5KG%8g_#TN_QBxIeaXhQns%GibopdjOC;ujx(PE}<7Pm%;!eF;=xF)o> zn?DvmkTOLPv7po?$_IEIvXkrOfIXeuBQ3$yOZzBzqL`Tb-c<< zv~no5AN{#+kd(Zc*kiAE_GriT_g*I-T{DboI)Z$wYnZdp72juA0DC{t0t>=$nD$x`(b7;swTQnHX8}%(Y&|X>j<1;@syq zhv+PUyD>rCfbxp4_Y)sWi)aHY%*geKOG|4L==K>K&yUKQ&Y8vP>)+UNNF+%m9U^a# zSyEMdfwgQDXR-Tw=qzV(pa}>-p)lno^SA`^8P4<=BLfN!Y?Q4Sjr|xz57g-n)X9%@ zb9S~heD{+kQPveB(_jk@P@+Vqjv74*y&MKz{pR*G9ll5 z1caq->h3|gwJ_jpEw5S>Tvfpt1f~Lts;c5J!}NtgrHh`y9X=yMKUrF-=k`-imf^(N jf+7&8nJ(y0@gf>PRuoe>g=@dV`_lj$n4RTw3ry_)zS^lO literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/IOM.png b/assets/images/fide-fed/4.0x/IOM.png new file mode 100644 index 0000000000000000000000000000000000000000..5aea22623f65958c8c7cb48a5b8664f4a359af19 GIT binary patch literal 3658 zcmV-Q4z=-#P)(gjW_9w5YGucvN~Y2Mua$V%40)rZstc z%sH{zx6wqCv?qi&j&d6tG-Jmib#S1ju@T$iV#b!1_I<~n5VMYe8kvlGvH48q*xO^X zaU*Z3R1BYKgK};zzfAd(v}x05H&YwtveQ${D!|PJ3ovcn%6S);9^0srNVwqT#dp!s zG%sDsjF(>qz(!t*RPIM$&}dfg+)0+3TaVkQ0Wy3b-vJ6^du+xmn0F-C)v;jZN;cKh zaP+g!h%GOFqCaZ|u6TMPn>dkTmyn>05A0d@WDZhD> zYF}Ttb*o2i%7Cyxg10|>yA{rj9Eqh^Oki#1&_gpKGp;RJSxss%vJDC(4i>0L{03IG5oIZUTA0HnMA3n^jyp=4Sf06uB zZ?baWrI%j9&CLy8rIN1>AI80^s#`v`h#_qf1P6dY3#r@eh)G(FOeRCy(!!khcw7R5 z$X8dec)=Nd(2mDpum?j%jKFj3Sn^MtU|3_Lxw_1}0*C-FwVGWedCZtqhG@_quvU&H zPbQTjS*aml z^jch9-8!Gs+u5N$b_~0=wkP&c)&c${(3lD}I%xS$jN03q!oor-Dk_MMjwT}`gN2LN z@X!5ejB>fkfo+$#v3n0~+qV-?TWhZF0zlLoDam@NlL{EiEnR^?Do~9jPcP zBILyvvA4IUtgMWhni`BoBT}gpy*!kA ziHL~k($>al(+K_Mn{NBq0W``XKX5G1ZP-9uTpU`hmP3aQQF7`O!G;G&Hb#_ii*A4RX1hyu3Ud931fT^JDhx z*~G`kQ(0EVro=?Po;sDNix+X!NeBrIN2Ad^(3bbinKX$+5AovhhyYtX$by4A%W5hs z$$sxWLO%bTiJ6%wd%5$W7Xz{I@^Y?RzRa#&yQr?NMx)W7QmJrqaw0bN&fYIKHy3ex zJLghTXechGrMw({Q4wx8ZZM*?wbNF6b29*Tc6Ky1HDP1(<9=jLVj`QiZDV3#VYhuu z_xI(&vIpZ_$k-EDeJ04dO(mBr$ZKc=Ong)jH)q2lyu3bL{|dv_ik zq}OAu)#3&?KgO2vt^ncT;iRRdF?H(H&N{hVj^g>}p_>BKNhJ8KTE#GDXF59W%)%X& zmEE#o4th1BcPuL6WJ(G;gO2iz8wn{b!C(b)UH9y?-@XliLZLvXd+?8+|MjoAwr39> z2E!BE%p5(MiO$Xx78c@iH*hqyv~o{9R^(wq8VdT zQ|Hg3hs^h35fP|^f}Yr3gIG++%P$id8A)<-GNGZNm`tYsJOT0oK)JM;pnz5aBcQwl z#E*{49*IRm15Njd@KZ!05!PeIJh8o;pdjWhTZT%dV)g3P0I1b!>}qPd@7z<;s{)KF zfZK1tH%DRfBFH-k1;e{<$G*Ovn|F^j9yS61T8GNXS3I<;3nsuRDhjPui;ay9g9Z%( z;QH08I6X$>-lEs@0WR&3z6M4gg@A8iLRt51IJUM@nv(<2xnSPUsc%1H!leCVDJ_21 zR<4~LGhTg_Q>RW59Ua|SUVP;WPThCysd**|neYd<@7~!qe-ahi$br545y`^PH(z1$g!8=rfe-I(xq?EWfP`iI``|&&ZrzH<`SZBy^JS*#bagqn2Ge-$2rFjD>SP zq^jD2KYbX^mK)4J9xz zkb?&gBA3h2_8^cm2YogtTq~1Nk&wV^cRL@z*4B;{t3DxdWeH#XYbckmy7K1_&ybLi zKv7W<$;ruG;)JR5=J8Hi8m*f)aV|I*16{YYLx&D!_UzfTH#87j)`P{#4D`tna9b>< zc=2M^@7VFsda+nctI?4`k{<)F$&)A3+SitDK?gOKK=B8y5{C)Qc_Zgii*PC z!I##Sj4t&S78baLkS%H#e8`^mJNFN_zF3 z9!H-NpaV#oG>PBs+lN%z)wRsbOm5!1$$|w7kVqt0SOsBc^)2=F^*A^s=>XK-|n)2B}( zkw}<3cPGATerA7{ zC#BDffJU(xmxKg{ySjEMKXT*GgX4{nx$JU$&#o?o(>=8;MOwV91am zKiiH}D&>WU*;JJ5g8GG|pZ}P;>}<2Y&y&(81TX?uVq?=V6m&fd!PkI_dNs+PPeCTL#?&D~Ce^X*rA!>{+ela(OMF@p5}}>!Ma+6HsJn=O z?}s_!?q7_lLxMy;0g=p`@|z9lTN)S=^e$z?-O*H5GSrNhq52YnuL0T4&P+adkTD@4 zh(sbxCKG#h?<6PtJpcWxrG$otaxpcPolBMwC(wyyZz8w=0B1u(iTM5Rv9PyCtyVLA z`gCk` z3VVBdtgWqaa&;wU`*yB5Kkfoc|3lC<;C`$-I`FQo+_=v2xVShD9XiC~HESr2 zjm0FOkLIM$jDY(YUSE$oHC&YHEniNZq-%t5#P`n8Mt`Eu3IO0+SI4l; zn|V7X2F=|MF_cOrX=!Q9U%s4cAt3;0{GlTZro9Du?m~Lpefs1SbU$*yv!sNtjvYg1 zZ_kTUreH7_IDY&%K`}90zH|xSidq6bgz;0r;076WU{}UdpBp_3`t%66hX@GI&80dq zkqvX^;Ns%KqD6}^h{X(Bvxc){!a2I*P7=p=G)Sw#T3DB#n0_I^JzVvA!ctQB)BO2} zL?UL)n1S`+!Tdp`Liu_MZ!a6cMHM&-bHk)Z2!0sXU8I2Y@?}y|QZSiJl$Mqvkw_RF zGJ!Kmax7F}WkJ7Ljr*MdaOcC0pr9c9{QOX>)qJvJ2bFvF5OV!GG7#jBXHV&u0^A-v znC$Fq^78U%YilEK?_Ofho~8R=iV07@S_AGO3kbl=%LCo*8>H_4jEHmR`eo-sKO6xa zfb(PxR;~ZVXdhcju8FWV_1}YuekZ_nnG8d_50|gcrzlH9aL%8FcIm0;cT-T6w>Lh4 zi;ye)s5pPV-|Y7vMZXiE-OGz9Q>I{0s|mk+S!j2jntnG0St}Im|KI~|Z{CdGeIIl8 zSNcRsvO6?MBpBP*TQZ-BMQq(L;tM=YX zjG{paUw;36e}3N|_r2%5_r2$y^W5`1?|D#<4B&LsoYVjSfKD3$Ga_a?Vjw6fh$ofn zq2s^F2VsE(07&ls8^GHdr6R=3TmD++{>I)e{z3MB&VZnxAdtJ4C(_a0#~I}9=lW(J z%n1O{i)+I)9tY>`?pZRsWl65gtr@5i9U0(9p``Z>@N)3h zvaB*Gw}@-hmvvg7oID_XxV#`$T%dv5ufY$IgFBLa`^u3if3?s+MriHD zd_>2SpaV8XnLbd_pafx@6cDUP+O%5y!}fXa+2<47M%I9Qj29%U*6s5NQ&VQmX9wJG zT|OfoG0jvqkO715^GoBa_P{wd$FgyS6MbtZ2_DVN#~zw&C;D`?Vc*jv`wrAk?;fcV z;6CDs5>Ew}{b)*Px#X;T;RSbG8vA*v)I5J$3Svh-aydD~Z+f(gP zQ+zY?XcmSXl!yUAUX=c$Lv0c*|0x&P=@XU__u9$$T;*m66#V$~83~B5bw_0T=-B)G zUQq?Fhh`@300RBc6lDepM|+zs^^u|vV2eh+k|F#2-OR8g@M8Pe11T2QXTGFW&YrbW zXoWSkPZOb0#D!6X#Kg@P@G^ z%MJB<%zMzTHe~`vqq(b3#N-9V(z-i3PS5<0h}h&(wU7eh)IxCTO?+Bu?h>u>FA!?( z3caWm%nb6%Iycd@umS}2f}rOGzCE~5Qds=LVQ{@QQAF5p{yJZ8(`4PY?_*}+ZuZHj%53t32T0nCbh6&UV<7`>VR>0~ibT-2 za}84ObxSUI3#y&tBWa=>HAOAbY|*yIf>zZ z?r_(*+}$^9JL62S4BpoJ=xMOcMG?ARYgTX69qr1zN{D9I>eun>%6k7qITVf-#X ze((O+PPSom6}_m|)ua?y{1tiKsyO>no%Q?r$tQeGv9HH^5=Sz|)>xAnI`6~h^0uvY z?4}HME^wdQ(j-%Xion+_K*#ZXm8ud&^+Qb!AVy9eoSSQYGOR%Fil1n)r>zwc9i*jS zZT%%OJg8lYjpSv*2uabrEsW~?!XVVLP}16Fh(@Q90?Uj$K#$8<&g5b8gJ&`k(IxKN zT=&xUcQq^Cch`II5JpD$hcb9n;+!~2MzLD3fAsce<7M@aA}$?;o92FjtkTKbS-x3) zLM+WTn6t2ub)2e+L>|Avsd&Fi;+MY#lZaPVqKq=N9_gtcWj?z){m|F9{p2%txED>2 zUmmikr64w)G=1cM35|=yE4(NOV?M|aa;eeXsT`DyT%=ojrOI*j9YcA2xit_CAXHTm zpD#J>*d~V@AZX&E;k-=wX~L^;OrpPc&9+}#{H~h=iySTQ)awp7kWx*{KE1&6?j+oku2m$BS@|O`~l=RIs)7R0!mt22l5~V$t|Tpl5`o5?R zM;3kHr}MdYx7d5SbGRcMK*k$N>+~9BOGq4!3df0A+f#kDYVPc*iKMI3TmB9Q=FBuP zgnyx!mo$s0&u_NLTTHM@eoeU0Y8n+C_HDzeAC74jv%sz=tGac{v|5|)wK3^(LqL`O zmM7keQYsp>;LBP@C?zt?%Kw7k;Bp;tBh>HN%}OAYQts$v++n>8!Um;GZSmJW$;e1B zV2s76OBmJDCh$SL4)QckGBQ!ahlg9ABmcUKwjI8eawCm%My-@#nH3H`3B@rA`r>l7 z>EmId2_2J@mB6j38uont*5e~sal198K+kYjtizcogsHnO57(9Lry$_v10vAhuGawxviva3hBhy+1`V&Q zgzTXj@4`eB%JhZPXGX@J{%{?2S^DLg4)%4hS=jR6tg|n?$!e8NasRUYbhE+v@5L6b zww|-hz+V61sOLj&^{@rb^6@gc{{EVB0lKg)RT8_+F#+&Uw(G8}9eXV^4-d!42r^wY zgjt#Ufn@&0p7YaD-lR!WVf~>-RB+x|su12QFwl=LGRcYKSp7DtopASoF-w|#7#0pT zSk>WNSg^~Tsr%A!2jCES@x~h4L7#k9q}jZ)YX~h;-WzWVT6+^9AEck`*zaTVxG-!* zj zZDf^~%$613R0y~YDfpFb0~MEmN=w1SrF*$~^2_$xws_a?N1Z{gqD`%zjf#!;B=&Fh zN~BWSO@&sP4|mYS-c6QWEXRti$Qr8#E?c&;4z6hoj*23$E<6B5j6m)cL$_IIH4WMf~4zDr1^K4u$S6YxFcufDB2aB{NT zcH#B&htbEcIvJN?8QoBrJgTCULYc#fsAJ&dg@SL+ux;9j<*FC)ja7xL5`-L{xlwFeTB1W zVNTT(3;_q5*0}7v&6a)n(pTC#@gnQV{VeTMygX4}v5KBVmaV+6_%dA=e-=&Jb9&I< z?m&)xLD9DEX`y#8?>*%7ceq-KkY&2(qw8W)gl9EDyyJ_&J>bhCe ze|WUI2vy!pgz*B#-%6I&Jk`Bu4MRj8*gdGY<+cAq2Frj3YlYeRm+OP+!KPK+_0JXS zmM|sn^`0%g!u=7v2qUhdFH71D&8~1hwF{(x=W-r$B!vWeq^2=)D6WX0%ViRh2f0FVqz<`YK73Kp?FIpXxNfEQJ4@Q_RZ;>b|*0% zJSaOLpGeQRh(prSt4mkn-zIDg&k-3sU4jn|k!nM$%A=i_k^ZcN#yJGHN?fhf&mmon zzw^p4d8+q`%(Syy@)muG{l53^${){qHgj=l7&4}(m)9CvZOdS4o8?CNpbt@(wa<#O_`EZvCA8 zYAge#`foC0)gZxuZn_K3zt_>^7O$?%1pj|G^^fEU6ud< literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/IRQ.png b/assets/images/fide-fed/4.0x/IRQ.png new file mode 100644 index 0000000000000000000000000000000000000000..53619bb3efdb8bf82bc5676d09830e70bedbafaf GIT binary patch literal 1225 zcmeAS@N?(olHy`uVBq!ia0vp^2|(<@!3HEb(?2AGr~;43Vg?2#GZ1D}bzG(f6qGD+ zjVKAuPb(=;EJ|f4FE7{2%*!rLPAo{(%P&fw{mw>;fq^B=)5S5QV$R!JwLVu$We$A& z{$AvXr{LUe83xmjM)9B7a&^|_vlp*Sv|YRNrpfke_EYs@%r33nmbG*0>{%NQ9hu_g zoo>~sXfYvjk@zX+xTMCF4K_!c3m!Z?X#3|^bL8Q}k2T*FpF98h{QdXk_wRpySG#}t zUCF10oN8V@u2oFs*1QTn%p8{)4w*CSLcM-`8A&Xzber&@l&lUJ;JmQ4?HS8vyPW9>U_-5@_nQW?_owLIxPs#akh1R>oX(?;de=hExVXHfB z*}F2EW#u;an&KU$519DgJNtXmd4Kto>)tE(nCahq|IP5>QoGwGC0qGEdHo0arD?xcW?{*LPdgZQoR!pF(4=(W>~4w6 zCY~pKl@~wZ$UGfXv+w8EZ((9@y%g&{7hRM8%e4Kv@tcp~@^_A$+icGpQMWtWOQAEK z*-QH0li$UyIlgh;VU=<(uQ{*r@Okudhr34hibD^JEVwKUZclUt>M2sj+}jZ_AYpFOO``iTkDY-FGJA0ru8-;iQ-I z-BvFaZuqDe#+Ac4#GhRoo)pT|(%eu6-{rC6D`jr#&%k@#A28@M)_X zgWt6;?@zxg3AlWb=fJ*VR=eaoJk_hd*>I<+_GQ`~7QN8@fu+0k=g0ou+dpFW^7&n# zE5Bj?#S^Rlef>Y{(gdb|&1bl)%^!WM@`&HY*MGe^QDU*(opiUO5586HxOUkzAu=Sx zf!Wn6 z&!rff>U4>AdN$5*3gF6U0H()Bi{Ep0?e0%lxSx}oyG#Ay?wR0RB-s$h%<@u@f_w?g c&i@##Od7tqJW=}#EQT07UHx3vIVCg!03EYFtN;K2 literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/ISL.png b/assets/images/fide-fed/4.0x/ISL.png new file mode 100644 index 0000000000000000000000000000000000000000..e296c3c9a77747522b33adc90fc6bb0d2da22cac GIT binary patch literal 466 zcmeAS@N?(olHy`uVBq!ia0vp^2|(<@!3HEb(?2AGr~;43Vg?2#GZ1D}bzG(f6qGD+ zjVKAuPb(=;EJ|f4FE7{2%*!rLPAo{(%P&fw{mw>;fq^mJ)5S5QV$Rz;hJH+rA`K6Z zr|HCe5j$4Ez@^Q#{?NiTd$?Fl_jZ01d!xr%xK-&Q+v3yvin$Ler5`%_2i0* zUmY&DuNvj8Td$`i1p>bH43K)^^?}aA@h--@^S05if6x-QM$l*W@)jQzlQpsq^(O zL+x`x7Gs7p3z`@mt}mWIOo1Cm0^^PFOiz<*SN=}60mcV| Mr>mdKI;Vst01=3<5C8xG literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/ISR.png b/assets/images/fide-fed/4.0x/ISR.png new file mode 100644 index 0000000000000000000000000000000000000000..1fecb98023015bdf25cbb382b632ce004dcb07a1 GIT binary patch literal 1777 zcmZWqc{JMz7yTurBE(W_W2t?e25m*gq*R*_T8)HOjUvVps#H*0gis>2M5$8-wZ&K_ zI+n_eQCmB<*4DA5weP!pRKL=a8vXwJ&irxTJ?EYG-Z}T&bMAZSaNPz5g+l=VfZ5_O z#A7BM0|z;ITt&&-Uf(Df=MoA4VB|M|w5`f@j+>{$u(!e-@A-yBc!u}@5fKpx|Db?S zFVA2f#Jv!5J|7JSfRnMd7%Qj9f~^8qoKt{o_YqZ@+`OcXckx%)^nQ=aa#OEP)=1D4 zdWdz%wsIntIXuECP^FqC95baaChZfYz27DgJD7px15iq=>|{%4b_5Kw_dF z=>puC%wpA%r%M_kjnShi~HQAD3N+riNEitj!wt4s{-sh{P#`>0=-*O)u9NBcALmfHX zSqK!`Zn)3!zvilhY*wdN^ykl{=W#fpSvOs=06r=z&|+sw!B~OLtUOi-LU(qKW=-)k zyU#uWL18pob2qsb+6H`hfa81}3VEa&g2fi05h<9BF;m(eyn06rgjOw;Gc2>yv21fw zo#Ad=1fiD}8@@I?YoX?g&gz?T392umhV6zv>lWJCMaQfkV2*c~Oap^2%!Np(M6B%@ zi^i&^aVt0spP4!8&6QvU&RUxPAfm5Png+=0QSvQR|a|1KwS%W-97QSIe2370oi8L8Qk!@L~(6! zQ~5!L}!07IcfY_{+T=Ll8^3u6<3m?yd{OcW|tvekgu^zK}BU zXULL_fuYLVjql5T!2-aC598ZQsogvTUU}?ByVMmQz3v?+7ypV4UfWDTI@YEez*^h+ z_yzrzR6S;u!iqKJ$Np5m{XaXFdX*rv{(JmA3`Ps0p{>)ZL$7eG7>~Odk{`u*W z5;!_0m!6wjcfz%Xr-nqnHEg3nC&c%6#RK1nh_q=v_K&>RzH~}oeU8f$i>UT25{k>} z=ulHe8N&nujc?BGBqT^QM=iWc5dK2QED<`qV;c4yL&DeP8JIu$b4FPiNlV-G_yXa_ zCLkY~^gNt0siy-5s^(u0771T6XL94>#BL*`*VYIrG5hpa8h|1VWrn)-fN}D~a=)YG z%5^9Rqzn}!_ts>nXew-CEl2-%WCvPX%YIAs5w2R-zcPK-ND!x|4-7Ae Q9ltBU7JD7@+htP1KPH4PeEEAY>b%LL3YE)y`rxJu$b3N?tY~$#T2hnRmHl(8qT|)k{ji}Y?X;tz<;q&>}yZx64j&c-A z6=>r~U2qR^3A1KYFYp`u*x?VbcOFD=RN?h{n6>H|5|d|7D1S}@dVAZ+i5n&)0N^NI z-w_VJ`rpW=d8h;DkXLXm27}=e3}bS1bu_c}Fk{jQLF50i0-6_-B(>f^u!ugVMZ@>IgVukHwCXY3=D9M~`)Z$iW zpfYC>m9m_)^z3OG(s-Ocb)2RnzoMg~8H3VJXP<}I2n`BBASca0NPwEcj#aF=`Ke2$ z!{^ahe~RXs{fO2|RIUng5}c@2!dRIO7bkj_bLT_6S(Ye0B;c92L{j*F@O&>BmUUCKT=_L|YB90qx8W6~J;F9U?Li5};yMf&rf>v$r~ltM1;4R;!z| zpYsv`eiF9+Y&EIjg9t!Il7T?83WrOgs=p+lzI6_3J{ zi7{+G^9$CYP+VLT{K4OzCDivOSqWoRcY7sHDgKSc%dflOHf33+^w?g8JBkpkm4s*- zNsSpRbE3wE#iXLE&oeS!l}ccCk^!S$iOnHWUS|iOrbElUKRkd&qd8~4Q(7Ms3I*}A zRxmH}Q*;`E%6dE14Gv`dUN9%qgi7i6g0^lqR)d^13$R?CET4K zk{#8C$>2Ab!zEFE+KwchX-0<7AT>?xZgg4!g+f38EG8wH$;OfPygr$lCI^Yp2Flvw zP=%}{|BA0-Fc>H3eH6zFKPJZd6U@f(gxQ@U+lv#q>b9Q|7oRj~8y=5`!cVv1>G_n* zhoC{pa59{K>^t*#*z0NE{T(^ktjp2 zWZC-rQ7V;_XfTQ?6L1!?Ec3!|zCpa91b{%Zii|`9pP#_}Ltcu?tz5k{c;q*)Pad~d z^bUGCS?gfc!T@9$_MeJo`7M9IYzdgK@9N4jN>f{jADpM4^DsN`^>@(-RdscDDY#z8@VPU~amR@Zhm>ward?2ffI$Orte}d%yGQ z1ud-?OaQ>~gKuN#-9mhrmqDwK>P82$41xd}wStU910JtLd7T}HTOuu9M|`9XrNTd; zqm@?jGE68H0(PfJZL^cP=_UY*hwo(hRS!+r@6iKqVC?@r31Oad>Q~$tjw$s)=H;&) z_w2LXe_^hED}n$uO%8%AYQlp22g|~f!z0U!Btct`o2q&{M!k|*2?oqYC8ru4I9<{i z>p^Oqp3p!w9sLS=4d0+(_1za_%ZrK;=P$jH+Uj@;%l<;3T*9Jk)rbdpyfW2|4je8K zK@iZW6zH`|imPp8p4qHKQ63>fuT|pqN~4X74w{;piHnOJnMtB3vggBBi1xiiLinih z?M{(bcl*dogS?E<$fu&g$<3?BAAA}b8WBYMh^`KoL|o+fWy0f?DXp;)6Q&`^qQ>qN zIo;|+r%{leXh5$Wo#C~u0<}Xq#AMw;LGIEEw&g{6z~?d0-^bscf0UGfasad%zXzOE za(B@nc^O8614h#qU40%p`#j8~gN@mhiERvr6J2GV1XLM7PAPibd%?dq?Pk++eG%34m8 zH&RyKO!cW+Ui;IxFttBHsKp;b7FXNI&oWch;J~O?5+6BAq}48RsBCy7^cyzD^RtJh(iP-%Z^T$Y3@qcymu5X>mGq8U^uD zI-aYWKif36l2$6 zpKRn%T^fh#K$ayW(Sb%;gjYOdwvi(h{zK^5<<+25(B7xSkaEozGCoWL0RRh@-AY4U7Dp@IMRwGH z?<6^?>XGOmG(d$$qa-fka|a~Zzs{bq4L(suWy$8y-nVFPJOYfSJ_LmZ2@S#&v=sye zN|gx_Q2d>|$7SWCRVGJ(zmXoq0NTPDT3@ba($}}ty)2J zxQ5sWE#bjx6lcOYpD3eJ3d~mh0h1sTrWJt9=~8V3D#1mLW(xyWfxrM2vJ5_--vc5; zG<5cPXzFm$)9+=#<|8dmPhr_G2w>1Dv6%FPEP9H#gvmqP3)X4cS~>%>*^DLg2lQ0G zNK#A_k->g1$j`EHpk!zy%BX2_(9-2*L6#Xw0+bMJfwMW4O9iDuAoNU>E6cDT%S?B_ z2fIrmIaW_(h=#(lVb(4VLaX+>NAHkGT~92UnA@k-_%IEd@I(+r5v5W|b;Swx{q?1H| zG<@4%JjAMn0oWWOC+qAi&b3UW$|c3gIva~~%|wJ~*k3%viag7R9>X>tr@DekjJxWB zjc>QxdHVTR=rjB6B@XrSl_lvcS(HCcc2W;`{EzSOOh*I&Te7TdzIol4=iNO$B>rMA z&VX2MHni}@cQyeqFfhRSXKx~06T-I5f5Pqd5E2|rb!92-%~jMCK1)Wd2ai`qrG)eZ z12Rx?+CgN9n)F1&CHH`qE;pwdon)mLFdLMdsIlP{W%ODF^&JW#v;T#-n9S&z@4qOC@*18g~8S_sEjSRX^BF@jJWl zmY&A2d=3c|uUTE&#pGeh#-B;b3DC03jhE_@W=- z{i6oz+gv2a=m-x|^TsE=czhC3p&Axunwgyv$YbC7E-OQ#S>7-4wYxTPO+h|a_QATW zEFOL20amLmtX5ih^pOX+X3iWo^efr0bP@M{{Z5t+iL3|;^UEb9v9=#JtXt2G8&{H- zVInqMizLBYpZ1|q38W_)=;(Fxevz7P!vmzHXEQK3gu~$&8Lz|XEV|0IOp4=yW<}C(ov^^B6xb{w*oN$;4SB@rqs=2Ai?D zZTNgXz<*{4504}_%^zR;M47vgFFknwU5?rHy;-ca-d|0 zJFf~KNlRoV8wm>wpRGJ9qB;dxpWHhcJ zM7@RgN*hLg-&0sjr4RwK#Of4(#xpuHio3FJ17voz?c|!|wRCoMW7HT(4oYO(Td!hw z_=^=$+4pm-C6o6HWr8efyrM){kechS2tlO~(9yxbz~mXvi-qK9#V5jsl_A7OYH@oc zB0@C0d(cC1TLz)C?*rg;a~s>+RXCh3L{a3cPru6havCXe4>=jrd-LL)1mw@jVved0 zfc?$Zk>5Y4XhYH%iLy3u>#EV6{f@Ppcr@dF!h~SnsMyY_+Ecju+~kC3@_PL{>?_(2 zKzhznR^Ik|(w2Oe`oU!0Icz39!9aJv7g3T~nyaDo_`zu%D1b(6AT&TlO_PJ97#$xL z8#&dJL~{P4EWdRtIrII2#!ufmL|;H6gEl)~f94h5@T7pCAS>K3W&?J)TwhqH^B%-j zCYU+e21i|ilvbW%Ze}_MT5O;UV`Z!!lX0|w;_-Ocf9Ny5cI6$Y1vP&xe34*%5QVKr z=@SOQ2UeREv&p}-%`4al1sfS0=;7%8KjRZ+1ht02fqn+u*WGjXxVpjNvMS!}(t}FFCx=UDv`Qd*L7^lo%%2`{x?DW|+%|4pG@G?6mV6<- z&P%|?ISKr>z6*~&h#keJ@p^rfdV)bGvnDxcjBHR)5TWs5{GYO?(GRM*HDM$B8V(Z> zWM-9R33i{IS3Y{32XDG>%zJ@BVXV6TyJLRa`pl#FMBlU>)tl=p_{I~@pI7b|ACzL# zCxR?9J1q&PZh-EZcD!B%xskDaa^wV0ymgEaqwWhEAEt0MaruHdbY z`T)YMjci&q{>sIZU;7?&-LrAZF5Yd~MaMt~H6=CNzWf$mI`KO71C8fZs-kp1tJ2CC z_Ds({U{^zIa3#kNzjsc#9s7>(q1Au*L=fm7w({r7As);P=1(`Jv#+X|8{TQ9Sd+>< z`O~u*PpNixMZEuXSZLL;v%Cd>6;T>2X7e~%omR)z?`~oDw{~#9_N9U_vj&Vg0?#dAaL3r*Z{oeD+Ub5xe@pHwa)*uvu> z4TKFhkmo(kUk_Cgpp=*u6T;&=D)`-C5;k2h`80FKx@BKjFPI_$*X6{b@HlW7BRH;| z1+vVlq={1p;8k9$|DoXF3QP38y3gB5G8Lvw3<>m zf`<#@hzJKB+?(Nd%+id60Y8G5>~IV7qZ9r#3< zfBn8d`SG_eYH)2W^;~g-lUd3B3uRs(*sa~P9Q_3@mvh2*%DU|2gd6zP_Cq8ZB<|gC z1x}BL8jqfZgU879H1OcYRTrhtM3@o*f*`OWVRXhX30Dyu7&xi?rnR@Q{n6LhcI#H2 zy88Rv9DFTk1=_nisP{DyYmDZ@L%aC2aPP=FMF-xYAh{WxRzX3InT$jOhs%Zu2~?70 z+(C6^=>-kU=M$;i^8;4q*g0G_OmeKA1(_y-Eh-kJ_HuOJYvbOLB#E*v2T^Jdub=j^ zd1(?&ZJpfr*AuJ@6Z!B*ck#_Rfm6EgGgVAE$O!-36~HLNx$nC;oDS~%&fN^hxNurL z7+pG)%@T(#N6={0+`8xnUMzi$YZYr*!)gFTdlTv)fyht|Cu{AfReIXGJsdnS45oR= zK~7rZ^-N{!b10!N*iD8aoWheeboYDd?)T6;=p`{)k05~TZW;GpVaZ9l>|RPbon)CL zu3t8vYo30K7?qc;|9U4{t(MKVP3_WGO=B}Qjlb~FJv_0u!4y59v#T5XKo38P>EuT- zomjlXe6Z`|Nk2ZYm@T~g%NN*~x}GKK`OFQ-!W?a(C9IqEsjE5KTtcU&huFXvKHB>+ z0Fu{#(A#%>h`u2ogEk-8v%=6OJ;LJae@kZKY0R6fRPlJrO^U=PLeGGYPmc|r>CZOq-Gj9qBxxLc&;gs5C3E+0UT0WT^5QqIy`cN1PN$PM zKiI>!(gl7L(@BKV!)Jv>6Ut7>10o~B`Q>BZn6d%@g~y7BG)16RYG@m7CtaIL*+4nf zVht99nOrf0gVlw&We*E;=h9ZQi;AK?Zdx6R%_$QVs-m^`CJL5q;?uuA%8AN$u3Hg8 zOIHOiZuu6HD2=O~)mE3_QNPLNN7Ve`%`P^YeoWSyUsLXO5N><{wMvOvCA00rZUO@K z($UsLR7}Fi`zN{_I9*=yqa{A8Y@%pL!`pYyBRV4dg2t}b>-qjS@0pV9CHqEf&C1n$ zR$Iunwl~@5DWXVQg3autR?|$$Ksh}P-R$-rASFDJop1b(_;3$Or9e@+m6{eeN9qE} zDY%V&e|wlP)d{Y@B7{9h2e3PRtV$_laFL4|g9llbY3=XkWWJsyi%Zba&1*Y*x$lNB zuvbvB^KoX+yPBfYk#zNY*k3%1+ar@2Bk{qTzZs)*;i)EC)nO!R9K773WXpAFx{hqQ8!)v zKCWFBgaBwX3I>LK^on!ITl+)a{O|9O5@#gZRL87XHFlTGwgWL_&s{=CPaj|ZNJU1F zh7MB#|Gp@Q$2P6MRHhlhB_#mhnLqrN{auG?59}dm(85YjFJDcsR8C&%{eF(B9)FIYviSgM(m;3av&#d7T3U&}jtLgJJmWLf5u z7jM90ZDi&A0QMdmA}_;8b(52*Pz`R6L|31O6Lk*erWq)%a*#XAKvIkjms`T&5{VAe zQgPbB6$=8`cYFv_U=#(n?HCh6mLB;8ZO6}v5Az|*P+V=rYL_skJ}01JD6O%xFw;z3 zvy0dWEh?2jYOH}Ja{`D6)evA(VlgV|==G4DYNV>c!GcT^$11JNPSVra-G|L)9n;U; z{55pB(m2^H5%-JPoM;BM3KZh>W@682pD(Q^VBWGjsqarjICJT?y4A&@mhtscjYfkd z^#LM7Gz?mO*qtKrQCf~xS}DjibF9KjT!fDFc>kwAfhNVsJ<}y~EEJs_COtt_Q*NJM`)?;j3m3@{59X{!k0>b$!y`ie|raA z@}s<8e-)3E8rZ4mVBgX439|JYzCx}15Rx*8wr)3xQ98U{3B6W z2@Z|8^u{=YOTGu33;+JdpR&udkI3N=UfR5sfC)a#*z?YR;B4DLsM$+Gl#Z4z7g3>F zsu~?EIdg}ns=-cFs1}`8LB(l1*{McO*4lA;)d(T$S-pd_(U0nDQBGVtu{M%|8X}RVYv`98-k)iYs5R;$H&E| zF6nqD!i*9S7!b&}*F8)&v+zki`UmmOn$`Z-o_dV1VDUauGHu$6}soMgFb0WkL0gS8i2!w{N4ZiI2#s$3U>8DD}f6e^X9 z?3@Jvy!!h*^zLH}NOQPn^(~VcP_0(;2epvZaX@208U^|#1|`l zUMyxb44n!5;jLW^I=vK@R&wL=d3XgC|8iiMT{p$Ds$l+?uFpRm0dBW@TybUd&z|Mk z?{A*cm+jMye>wvGf5SiACj7q*mkF3*TqaY7(8A5T-G@yGywqX|6iy8 literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/IVB.png b/assets/images/fide-fed/4.0x/IVB.png new file mode 100644 index 0000000000000000000000000000000000000000..ca02273a6f3aa7314c2e40aa2cb6edd8b0405983 GIT binary patch literal 5642 zcmV+l7WL_gP)ssWSPnA`{t>n>tv&ZHHJqVT~7ds(*ZeM(9yIaBoMlSFy>gP31>iHe{7ETT=F zlf#f>$9VbWFSz#F7tdI>S}n0?(OB;L?PSy+zKN-*h(LWkDuaRKn{VdtoX@>QpPsSY za|O}4bt`4FW^rWFBpk)X00@G>j2T1Ox$_>@u3gN?k=f@~OTvb~@y78S-FGj`^Guiv zhEsj}?SwoY(w8k`NL3XXk32#^7jv#vJtK$%OG}BLGY7|}O&lpKn4T`%lyt2)M}%+_14Lh@4uHn3^HSxIEtD(?m+Q)NWJSWhLo3+wsa|~xHup@djFRo zj=APtO6J@}D-o?D;+sTVam8>-OMmyBjfmDB|Mi|F+?#8{JYiJ0$2*;2!&g?4@yH{n z;^P5mYI1P*-EZ;eqwCJPZr>)vy?bk!JNH>mmxviNhO%qd64tN3ol&E{eInYsn${kF zI)=~5Swd0K$GF}8Z(XOe39(M=xK&;h4{@A-!&0E)$pz8KtL=0 z8zP1b$vE|cMWII zWa|K*FNl0#FAa}9MzFClENlGuu&mhFw(G4{H*daK!dq{Z;`0Shspqj{%~VvFPvJqMP+i+~!)OKu~G6hpY4xfB-h`KuKX~`%qK77W{J(rM{E={1m(CzPJ+AG%>~PSuY89rUBIK49q@*O07cV9-VLFdJ`5AZKIsK$^-+OO2K3|Y4 zuN;9Ogy-(jW5=S%GIh(A;n=v5TEIAUD)S$Dh;2E2x#ym@*|qD;cj&&)=rRavG~py! zdHI7p^UT$4#+MxqOhrZP&CaIw?z<6GD$yz&L1Q1>Q6s;GFF)YKEHxdXMbZ92aA1tJysh*gau4iT;LfD)(d|q#t*XX0w07#OA zO0C9bw-F4UB1Nt!Bt#A2`s-iqv{`C3zHGJ9_@_V7uyQ4`)rypyOh!==!kmRX@yu49 z9A5OT_x97l?%g%ap8Yg~2d8uAozuDMstXatFcEp{*W)NIrheJ7FcBL!a^a6};^}tx z8w?sg+WQ_)?|=8?4^penivC8oia%ZQ=PujDHdm9IOWm?%2$B?bHQT;orKKNz&U@gj zxSBIX>xdXOEYdX@CQTy0xS0N%H)EVK1&u3my&wnzq9le5tX7fY3^2gr1GS2qdp8iH z=cLAu_jtLq*~FA46LFr%(IF@>q|y$+6-j1>>YSa*IN4fPSTy?o6m8JccA`2q8;_u7 z>(-nl*2dD_-bS~ImD8W(y6fKP^byiaJHE+U`RQAs3wXT& zii?kQ(Ha1{YCC-#JXp`cgY~DpP7nkXCDe9Y6B9+8ER(1Wa=xa84@`+%knBe1_2USr zDD}46OArM#7tP_ei8J`~iiJeU4)VM%N{$oIsF#TM%M@n1!0M;g5yga5H)RbXnfd_R zoncugOZG_r+28-)fgnWgI63;sj7hgKFVD*8oFLaUxv94bytY-tc2#2J%?gZ3$wsSI z6PrC2RqZ-@IouGR0Dz{KjB=iz+wz;}6Bob}5UF#BeC!h0?(Dcn-WhsCI$CK*!FqKv z514vW(r%s}e<&7Gh%P z)c7Zh0o|Seh@1wddxG@0`5B(%rLitZW=sH`q(DePYFx3SqZzZ_`6 zY!0zsF}4{WV9J2e#3v+@k&!`teLbQmVzF2lm)(cLlw1HdCdPBvsNu?NJN*&@TszQ) zRt~b)72Rh1Q=!|B0EmnMO(6}hInxk!s7N)2Fj+-bTRIx6p{|UL#UBxGOhKp9QBhF= zK%LdhgzSD8f+`#meBen&*#Y^8ME4|HL{%J`@;}7tz zJ*mSziDpV%Q{6YR!pW4Cr z(8GgpcqM$|`0^SDd1fz{^mekoIhKhT?uhHPii#Ht=5k(YI)0T%YHBKKwHjHLv0ANc zZK?twQKM&3xt(l_kLg)1Hd$ktoO$Ne7~P%#h+GEkGe!{=8>FH}VO~xPmDM388LZ5S zu4lN`!L7MtxF{=^pRRogMN%V*BB4+Sm&=7L%M6Q6qQ&i^$?xW{PKD7ZbEIBmR+^RS zCV`PLPUaXJ7@#>p><_7kb`;eANWMsW&WTMEZzJ86+t^?GbFCdXk_|unb;V((dA-WnZ&E5(NqQ0 zr=?#HARsasx3)Q+4fO`h=413tRoGJ_@@P{wQK(sW$viIm_(e>Dhf%%yla`!{D2i=z z8V>Ej8w~PH&2ktbhM%rPQqmm#_#vza=GyP{ixMyg3WcT+*OQU2n&>aGDauNDecF+ zR10hCV{vOvujjX-+sAxm{eNE_v`$%sUAHI1-eW|rr7<=XmHcl=;v6VkvGjB zF*Gr}>oqKE0BW_G#Kc5eT3YZ~TPQa*k)4*+cE2Y|B+C_GOfL`VJ}0%!FgU?SoZ=@_ z)%D(TdvtpjBnS}df&qJ+ij8?1#^`M%NkLMQWe(Uh3`?+)qmoEVO9Q~~_mhy@pLzwx z_aDWst>FzNZ&nCn?*P-1ZCoPfF3}A(4_%9Q4(KI z&~j5>D-#BVs~Zm2yLsf_dUS?V5))74^n-#(a=(F$ke#qKd~Urykzs3MZSrG$m^3qw$iZ^YgYfnX1nv=G52#c;#3eUxyO- zT}Czrg8@ZR+DJ@JPNu&(2USQw78L|Vp2xF}CStKz02q;d9st8FJ_cBQOv`jp-Xf8zJ-)Ya2I%%9 z0BG|8nkbn~g%UH9EcDF{kY)_BxmwSNUd=fD0)MfFXE+)0DcEebwluU_t;XSS;PrY5 zg+la=3x5fqDn%e(FQfJPxir(khcz%i#Y4yzpi$BAp*yMl<2yySC!l>0wn`daPfBKy zt%;F+!?jSU8b5Da^n4OXg5!HPyDIl%Hk;Aw^<-sb5ex?L`~66gL~?R6BP^A$1@`of z;YUU@k6qk=K@UH@z{@|r)bfr=!@6L?Y5A8PKtNoU!@p`=og1|+2=abmtUS#r>dIv64^KEOebp8VKO z(g%WoEX&yK_6|+0z4@YkBe?RYjiF{QeT{y0Ib#@<+_98%c*X$s?(MpQuiFz~wYsU^ zZ)9@(V7xks13HQE8E$ILaGu6Vj?vEmjf(|wCdA_fzp1)-;$ma5T06cNn4O(Xd3iaC z7%p>^P%vsj6vo+UvM3Bs@=$27GD>SFA*f~1q^o)PMO*dwOrHB%FP$px$HbILo_ZD+-|-!>o`WZGHy!lXs(`gIZkIdPDDpX zW3^fl1cB=6YGhf)6ZBDh^esjVwy|iijjKj_xH#R(g)t5)j>^=BV!7v%yV>}jof$KR zVzs(4nL3WBZXJYIte8!<;c3)r4Gq5Y`T5vi^U|(oDf{vZ{*u~EZ4+;Z`q*sD{SHDwEWNDXo|}b0`b1IKU-&tB_Q% zasOxzw%mzYV_;kD8?37+CMbblCE=AK`(Z2^7RP(ZNp9iQqcNCtIsEv%u@v$V?&fIj zt2{iB9q*oWnF@22E)4_9B_OhU^IMC(F)Rgr}`W)<_VT8G1NV$s(v z379u;6hFCm4->NfN^JCr*~};VM^P7A!nW-WMo)p2pS;AOQ0MO`43_Nd4;UiK7B7nS z(pLwsG3m2_zd-1&6#ef%H zm`}94iYa;jBqcr+(PmWFq4@TYHL#H9pK0K?^B0n+$dol!;ZsFUI~zlq<25ZOP#uui z^4$@D&g+EP7=2LxSEt^Xf0s*kQTJ?h0K7!OFXtsh{Bv z5K{HwoqyFbeqbSsCXJ`Ds+KBmGl(bLP_IBVbFwDzs|mNUcZ-PI<>kkdDo9m5jc9uf zt;))b0(fFI#2LWl1)pDFd+i)*T^DfG#N7l#QIvZYvF3e=BS#v#7Xbj`wq&R zQ*ema@z7(nj4Yf_bHf3ym|$VV0MKj0z4B-G!-9)FjLF@B71{|N56UdMr!If(H$am%gmQeEA0QoXuW5Iz}#!4ON9yu;ngtmp^4!fJMVmTTHm(r zphHh4pgkl>Wc27hOq`fcZmt2fcnrO|37tkk5`9=50#>_#!;^%~nZf4GN7=FC*nj;E kjUNa&$LKLZ`1^|g2bljUJ_8w~7XSbN07*qoM6N<$fnoMN#S=>oyu5Ns^cd5KLVkRl*R{(&M0DMG+BUc4@a zO(1{3LIUYhc!lU0?8HuW;H^tzNN-rs3co%+I7dzM7@r<0xHJ%SvMw zP9CIhe8><0h*RtB9K62^wM@S(Hc7ke*R!`vY6Xb|CByFKaZu7^$`Pq{74G09 z_1@LXrKDOlzUKY9+I4q(P+fQer(e`6w5s7w$P3Xle2U_0R-xU2hfkr}?!e*-?0rzL zSkaZFLpcDgBS>!i3S9jOHw-DA>R)%fRX;dVgT^&FIZO5&(dx4hOGb{=VAD ziqcC`?d2r2JFvVpuKTgSkf5Y1OejZWECWU=P*NqCRF24v0f6!;$+U7rq`lZODanR% zM5GxoOi`6&Q#m4;0szHRl5ORPWFANtNl7l0Ba%4-hRLgvTq;LIDFBc^CAn6Pi1IVc zJSoWs<%lR}z%afl$tUHAXbk}PrzGE$Bcgp8#U~~Cs2maP88D11CHbrz5u*VB`%3a% zIU>eqa&0Op56TfSo&m#{Qc|9jBVsiGpkGOsgM2AR#CiZA&VZd4u=+@?oFoHOm&7y= zVdHhYOvE-$0+66fNt5D2IU-I20Q!`a6<5j;alW2SS1&0GHhVcToM*r=rj_J_azwle z0GLyfFUk>dt{{t?3efWE+ue_-#eP!t;D9ABhWk6#IPFiNV zq){D?_hId^{<2l@A_Hb_!S-`|jng`U^_Ol&z9P;85C&onF{vCGz9=a?jH6@^Cd7*u zG<;N!4BwPAQ3m8iIWl}uQg}@@Pp_%wQ8_YPD=Ae56h%2QTqr4Q)*;~4W*tRTjttvM z`fmo5ML9BTC@E}NCc)~KWo1;34AV-g0E3{MbeK?5C8{$B%1MuIB~^ZZWT3hpzxfL` z&ivT{%QUqr?98d}p4;sy?4AzASB?xFN($T8E8=AP`XcpmR8j3EWePyA56{k^@vS_* vPeal!E1m4^5~1!X-Hk~gyn^9jhe4(702VRD2hffdX~9Q3l?vJl%0ncE z1gtZb5JD0lJh&~$Nir3-E+_RZG_{_iOoP3u`o$wEV>Ox>Ai^QX)&858>QnFr^5wrKm~=rkp2H<&z*cAh}9WI(8v9{2IwwgydW`AbA7$s;Y56xE}fB zsUF)1y$Wy^DK{JE!z)O;^+phcMCaK%s!X`{3e-p|g5?U-;D>3-}`Q#ZQ~Lkignue$*Np~`LY6|Ich>~UWL;3m(G8v7Sy&O zC`}oten5yI+Cm6feuT_M5W)Zm*2$RjUhUUBReu2Q6VKs&{8`jsD6SZjmH`3>-lx8B zKIsc@#j)l-#5B9EM71JtC2CzYzAYib`xMk*00>;z&f>&;wSw&)JEn0H`X#q0@z_l) zL`;&V05vSrw)L;nUo{ust9vxX>ZSGVT#j#q@Lm+#Fnxy8Wbogj}coX&0 z=MmU{NOQcHo`$Qe2tifVW+2N~RgHVqI^_BW&7BEmQ>QJhU5aQ=)g0d!o&_tBk5pk_ z=cV@b9IS-_E{U|GoRknV6vPn`gi90z8AAIk1YWQsW{xLo|H}!_1@zlZl+;;;xPHF| z)370kT|b;%$go4N;3!^3U{_GoE@7n4eQ*UiQO9r zsWu$Ni^+TEC2T*uO7j46eFM!))^K|6-Gsk5t~tIZ(pud}Z%CM@2XS70hSb9f?TehW zJdnd-&nQB>ZKwee{}X9^d}bu7M?|UfCv1GxAHY+wh5D<18rRnHe0qDXT^1B8&{tKB zyQmDg?zHwnQ_N&6{w0ppixDgq&GFr7nLZEsv!gM8Cl?4TW)|`MTe>hniR%4rajpo=W z?%8*6%yo>>wJn&g3nFGm@I8`&bZG?hlpsp8gkV!qeFEXVR@4xP*;!;C*{6HVa7``l z)#do#JfJJyJSHDk#cFIbe$Z<>vDe?yZTTE{8QZnJcNPoz%ab(Rb{~zmFGW6~&zHq) z7rC`(u&r!I^*|uSOv?2^gj5C9D*;#dLa zV`K*)SS@5OUr6SPg_>-syyPhLwY%9{tFkj?nGjV7wYKMT*C_#z}K>_7U2`muY zWkt%KPv({~P5C9;Zer2r3bcFvYdh5+plSRJ)Q%2N>6|tSD1v}5&rC*J z5aoTq zhO?8WvJ}(sp}OLuBt!r(KAX%1BXL!f_V_^`{^)buH{4EYsfWgfbn?FEA#@-G(WM~d zDiDIeb7@45OE|V~!gAB~vHLN)yD1ojYhy`VAF%O|Fae^3kJaKXslfln+q&X=n^{Mf zmywb70^}+>jEM<%>=cFePaq^o|8eKM4S8O@|E*@bEB*b8$R1a=dPqzM3> zyB_Z!w&8tz8)`79Io@0_3Rh+6Ij`^Xks4Zmv;f7^c3xu>&Ye$@(yeXySS{|=8#L{> zE*L+7b`y)F3J^s;{uM1H75I1W*A<^Ss}R?tYcb{J0uXxl-+1r1AI0qfI^Qocwv=I= zGZRRF-9$GA3AUS1XWRFvpCZZSf?3lK%FZ@}}r zCurZPNa89R`g?a1FN0D5=w?gnpG0Zz(A`g8XG{Z)=KFyDLNJ@K7EZxDHs4S` zwwc#pnQ~=fl1c)Das?Q@hdv)^NH)&jt|0BUn-Y*+Vi|M-7^%eQ3+wdBWNj?LJidR6 zi+f|x2oSD0f&0O7{QI=m-Dn}%ReYNB8E63#Z8z~E4b8#%(DDJdr2+jA_El9=H)Rf< ziY=NSzF@WBC|=C4_x_=)<|uNiF)3AiS{aZPVAO6>XBFbwuo}~-VO>8nbkcg(mYfeQ zLrAsh%1JN%DuCfCyU`=aDlhJF#v46pHRa{vT>l{UpWL81RxkY~Kz|l=89kbgwZf7` z(gjF{-NcKbav7afe458emH^4No4E5Zcs*$vJ)*O?_=f3*^5}`A2rv-!qkR#ssl{Eq zURQlToLz|(C74F`Ud5-CgbJYL`?0a;5AQWJIg)Ukoe%*GFZ2B2xXO!3omFTgjkqyt zkLH4rxHgnvyKb7{Tnq~^80{ur!Zjyw7p=$t)_c0*6LH6&TYztZ-NcKbuVNXjA<`_s zH_dM1MfLgdRBpxl)OL^~n&b6W(Pog#rHd^O8d^&XnlASk){^S;aV!Gdllf@YB%wsc+cS3@JHI7*#VKB{zOjCn8)OI z78e&z?XgXd0$dDs6EC4p{!2^IdV+^O)E?ZeiciYEaGqZvz(r>_@zP~Wj}mEeYYrDP z({U6pA>;1bLHha{rb__Bc9WNjW9_m5zhO!?il+@v#TMPRlrr%$vMNilOzy1I5L8ud z_ElA9={_|~F_V7hEdz3yF_45l`ka=cGJ@}atUb8PmL5Pjb@N@#f$D?0-h{ukb`vj( z!kO)Vr*++Clvc0iaM78m?Qh0KxmfHbUX)fZo{dk__U9d-XtO0v0$i+i6EC3;|3k~F zbp${7H13$V1+W!P#kH{n^Vrcwa{TtAD763Wb=+$!yS~HL8+{7!f4k9#;`ZRF+}fut Z{SPsXA9{qH?ScRR002ovPDHLkV1gE8?^cysDM_r1WpFq1PbafK9 z(z@#4po5?wf=i|G>TtMQ;!B!%o^zi2dCvWZ1JOVduAh1H-~jlw46p(ab`*bH0UZMT z3D6)s0?1(yo&gL(34lQ;0Wb(900yB1z#x;U)yU>QIq>kXEy%m9)> zye?+|l0giXa{$R8`YSg8$sl@{JAh;m&sP|LWDwmI4j>ssnl}Kc>=51oq_RS&0g%cD zp$-s_BJ^^O)e~uH#g+a5IH|jZf=s*s{gSSLb&}V+-i}X4taK# z-ulT2d45h#O!yyIDTG^qYLz@V=)Q-wHUFb3hH&#HT&n@AtH9|gFgMqI-PRUxcnCC` zPVOqUDy-K#xW3K?F*XK_j;5DqW`Mmt;Qk)CyzB zl6Qcz4lj8FDEshI;Q%&#cZC7i@Xwb!fDPZf+yHF&{&Egr!w;4-02}^wWd^X}v-N5K z8{SO=1K99x>l?s^&!_(a*zo!GB7hBFOiuu8_+tADV8i=q_;fq~_Or;B4q#hkZu&iV^iN*v#B z{C11;OtIFqV3UC53w)(~t~5IYB_|!7-aSKtUuI9mb&V!>{j^ zV_m5WLmGZG=MM9gu1K9l7RJq=rt2GgsytwzU~YEk;rGLLZ*qEu?sQ+nd7IsUi}i_| z{jpC!ET#!_iY#BAIPJ9b{|!2am%jesviL%hVTJ$Pro}5e%N|Hgd{bpr_4RLc%A>-R z_xD;iZrQRj*<%$~e7*gj>H3Qo?!NtqbM3)0Mz7-H2njj2Z})5u80c89yLP?Z!G>?^ z>eHK&qZ(_OW~do`wD`BHjQ7~7Q`0n5TpQ~P1op@7=R5iImXQ0}8%hqTg@T`-pHnn9 zb94qg(MuFw#hoE-4|t@778{_KVztKON; z?EzlPBmeZ}-d?c3;-FNcF32q7iyC3UntXrF=QJ`n2@4mrI_cESo4X-1>EEBqZ%5f1 zJ{@AZwA}x&hDgL89Y&zjiu&aWSC~I}eMRKoo~o%weg6rDcr7=S?K}_>x9rM4nMk*c zB&K=u_%2;#9Wy(q;hSNR@Na!P1_`!Jix;O@ z?P7TRIAqnXg>$Cr94Jz9T)JxZ)am+(VOlDBlNi!gwmkY`(t1j$r)J)pKO&wDkB;{n zc&l>B{n?4-TCC+3K97`sb5E$X ztN8tUWy$Pi3wO`X`N;T4JL%uUM2ATlQ%xW5FKWHwF16s{0mh8ueSuophh#e!ygVZr zr2aEjaP3EnoAnY~g(M^yQ*Nm$@;~Tvc_1&f=-R(2%NiLg{O75B-*iVhYuf1_k3S@? z*;D1Z_MU*Mgv;|MDl%u!KFOOFEv&j+ICp#g;ho9s?RB!7u6I9ZX4mQFH?Ui_YRmTI z>z9`oV@h;0cG!0%1_T~dvLh@LZ6&!`TE(- zSNR;?zG|B1GwGk$E+@s7NuC+U`zKre=lsUN@ZeHu$)392Y2D)6|J*+SbX|#$-zNK6 zTiAXZC*2RO_|7+9Kjy%x+2+&at^@t|^_p%*ZS6F@5ZEn6QtM^gu_5Fm~4B1njJ)bBmyLR6yg_&#i$v!FKxc*-q!T(2uy=KkmE$^WyR=@0+L`|XeP h+c*yN;6-QdA3589`k=t`5@5l{;OXk;vd$@?2>=J1U<&{M literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/KAZ.png b/assets/images/fide-fed/4.0x/KAZ.png new file mode 100644 index 0000000000000000000000000000000000000000..8bce39af9fa169764f08d59871fcacddd6fc7dda GIT binary patch literal 7084 zcmW+*cQ{+^`;XBYZ>>;!Z(c?0z4s<&ZK0G_&7#z(J%Uhsub7QJl3KO5uM)Iqtx~(B zRw=*p{{A>Q*Olv>E6;O3_x)K(G%?a9BV{B7fk0%sIvQrc-3hpKh#vxuMi7We zR#!vSJhg6PuS>CaZK-3@NaOPCA;H;;{Lne zE{kw80iVzbuOGuHL|_qhIqBSxvWObN-Ji1Zu{);chG!7N%XLDmz;f7 z(GYW5?IwQCG>>=VTj@qlXxwtv|IULF6)rWNprZycvie2UHb1*biT!G36>gq9TM_r* zpmV^JjOZhzUoZ7k=O6VD%E5114M}mmF(u58T8w^SCGGHIw&*(7|DY-hf~H?DP}r$lN* z`~KyvA9p{@5yO=Xag7af=DwmViP=g3*~>$sOOXI7`A3_+I@(zCy3Hrjux zv+=*h+)qKrgdLbpN|78YfeU|wVS5rq5)SFoQ%s%+>~FdD$i%rshT~1zO8z_vwawj@ z!w&A=SN|u7V4G@?S2A+q4}V;fEUNKYRw&yMyq0;%s#TY)3;c#NR{a(33wT%1_npy> z?cG-W5N#hGVLmrZO!X?Rl2EqftINy86Gt8?tER<1!`f7O6^ergmKX~B^(wG-Ai@6R z;~YcCuP4MaW;ONm*1}aqveOaA5>&rM9ofydL6lEAO`(UCbuudI*JM~tMpe!dC&Vb| zz}YWA@jhr}%%)93sV7X@pH)v%rAtO5W{e%m^Jn&h&*C!Ygy-Zz2)aF>0c}-heN3~u#U!y4C+bNvC@6i!dlvV`goSE+5zDTjpSP$3`V%_nu6?v6gFv)zXGS3 zEz;ENi)8W!Sh2NgG!y2DDHNWQ4}(M}i%yvrpDeGFBikVbM2d=6W5udo{!;a;p@@Nv zE|j)5jI+}gRx>~a6o-atMweyK*FKFth-t;bu_ZvfF5v~u!y60RvxH}ACY->^aYxc9 zYh5W{vll61MoH2x5+~`7P?O{WjF-S|Cd8-bJE8 z#Of@)TE2t)S?rDI!Gb7j3udD%J*V4tj`&Uf?7WfDa3q77J>J3-5tpwbEKXtIs;&gMyb1et}1RA4hSfQpyum#dk|T6b>NzM8s)WW!eSm*ief}-*nN0oU%C}cCNRg6O22m2L z%+NhyM{Yf2v--Jn9!^Ge8`6&x-S|UUTgNnh9Q;B}P@~Lf#P)aV2$xA=xnRT-xtQp9 zL~#`N*!2%yK3mBR!;{MZh+}ombx?~r^jmXIMm=oOa3t1b0F*Us%6iTXSt-^-dTScv zLe4BC&f$+6{ElHI-m~W;@(T!M$l#4GnxB$G%)?n|Jq9&Mg-u|#FL~w%>9pCBEoX!b z-@UV)9^G^4H5EP!ANR(>1-|htWP6R9d`QS*su6fi$unOw!%FjTa_#ic>gA;Ug^k`6 z9@Qp0gCHURi}3MHz|Qcqo&~sf?&}G+n|^ggix-oqewZ+fvSXPP?&OR9aFf_-X7ZIg z{!N4biBlpj$a5SXAoX2HI;5{M>NZtsM9A`E1^pg_j{Y7l<)zxyNJz#>A;~hKRl;HG zd!HdObFaGbo|q&UA;P%O0GX~snDq3825J1_$P1~i=n0ENE(}#>_hjBn<6Q|Vjl?wo z(n>SN%1UI(qXex&`nI(WW(gDV4UROqmN&nR{4=~_v#(&W0jH?C!uu`{`{`k@slUr0 zxVP06Y?6Dp&?B&Dp(FNf$Nr|(kIKQma!y6~U#cIsU*BfzyHI>w^NvAi78(f9+3a$q z2!Xd1kY8{lzD6cB^;R|A{v@LPjFN@gAGmv|sh6w2wvO~9n`4XA?BPJIpVx^$z-Xa~ zm62Nz3rNvaXP4H@02k@sl9xV2jvY=m`qu(ps~;2S#sgX%RJjBeumYuW*K+yla#4oI zAiD;?9F`_h{H~ec5+|dKO(nC zSAiph1lKu?r6bh%9G6^M_h+?&*h9_TX|!U;$N?$`nw#NhZ~F3>z7N}&T|aJ~_;^v% zm|C?(KDFIbW+(6I0I=V^%9w|0i#>FY^G4miO>={}@H z+_0c#Mhd3x6@S~HDepJ{`jp&dvrX~r9_1b5E?42Zi8olP(T#p6>@@q&@ZId z7A~|4>-}#QKbZvlfaeXga#=fc%ia-OBqZ^!ul6nTyl>7h=mF;8Fox8AN*btB%kYsl z(lbOgTqWc5g2y6>&~=D&SjltDej$SiqDhq{6BT7>h|V-gom)4!*mKbp`#!=!9O;2B z!rK|V{p3%!RFDY~7~##e_clHYH0-D?5Y}CcZNc_1M9z#TC+3U;cyem)!(0TH);G5= zOpY>?hY8xQu7Dhi(G7&ZBeyWD4lH_P&6!b)*3abUJ=k;RzFpDb4?pXS$cw$r@T`P3 z{7aBtm48;7KUO_6WY)NL_!|~^?GyWE090TR0dP&jYi+P#D$K-y$LN0=!W=U?&avXz zYjs}zm*A9ZfU(B7@;#d2$Q|%v$O(Vup&6fZ+2h5$@!8#~eervTr-tw-xS{AL&NNOI z%Y8|J#aF!*bwv3>s)7lW(LwWzxxTu75paX7F(Yij#-iv`1~ygZA^k4z{SZ$bTeKJXHLC1V~KZ1$M9_` z^>lux8p$N8MRv8Zb4Natwrpj0JX@yLId`e107=IxK;&9pE{aW3i2f&EAuPs7&)AA+ z*(xsI_V^drT>SFo`o_L7+gL$n4qIss2O&O7R5i-(fl9+_Kbyi!opjuC`Jd*oaD-Ko zw3!CIrd*0~k?v!&xsFu^W zQ#o5^c+Tk{JoTqmw|RH0Cufn;jyT&x1%Ggf7?h6Zwe&-XweCVm$2`O4`JvCn5=F*1A7DRCPpvb`;jDk@Que6 zU&017d0`4Y04U^o8Sg~)W*6@$cYV68zkf8~-o$wd*r`YwWgOO0^HrK6h zRw$HLisNn~4bbosl1jVNn_1oa_S%mT)8`|u{Z-IKt%f{?*|PSyLdlu2IO@d4rv98bOP z4ZrZ6TQmf)aSp$Yak0;f+EH&4uZE0<=ZGRSwP9e3VZ08swD{T`AsvNkIfyK!<4-mt zVjA0iJrEMdP72ttw)B6jtK-fPf5Y>3>c!$JUi~sQp%Vfbk~Js4Sj%-@Bn}TasVaiN z4^l4q7qBu$iVpue5D<=-1F(1ZQ`1M??lpL>UCu|gDV+bH0wuxlQcl!y(=A%k5m zz&t-EFKyna#~1pa>EY5#S>)EZl_Vhbex@-!{>e`(E7H7pehqRH@O>m2Nkg9YRQI;= z?-KwQ0e4dCP01eyfZg#Rv^l(&SD?kxkVT`Zix&{kWIBIyY$E}+!q1kQn{)f;m_PY{8XwwT*#l*y>WlNkJo`$ ze2P)E!&@!2ZuEH_SXgYi9<|GNc`U}j?nK^Y8QxEhC2AzELfH+sj~HMIM5qM)mQmi^ zMo1G)SH~t$q{8FqAprPjCkvH-MT_SLWNDP4W~^qQtDZ*0%VBeixmx-A1CzrAx)y7- zyq9LS4OJ>j>*6;Se;AETZBZ3frvecqNxQSEWW*dfrp0#(KWdHN>-=EcB6w(1!LYTb z$(sm3Kd6RBN^IQ0D1YB@(^lk}Z}S{mQ1(1U10l1CEf@R7W%f zaOrkJ<@u}nZ?6@LAErNtgeeCGc8$gGv#%`M$*$!5Vnf z$vL9+p?g-SvIy-fH{e0gajQ2n)f&3L>v%Hq`c zog?k+3#`#m!FdK$79Q>fZh1H&fc+4M>Mrxxu&fu@%Bgp+>royYiRldd_Mh)3x#Acu z?6j;qiG=j=24tqJW=a5~3j+3}^LnJ&{(N}Bf##v(mk7KtxA^J=K;;px%U}9}+AiGM zq%fVW4rFEfc>(SUYb5VOSGyy-fSi%oZ4Y+WqYu|{)J`qMW`GgJ;f&&L5xam?%6gbQK_5qyS|u- zCB1V#z%nScDZ5rmrvLGCOPoDRzg^0?FoCR48$yPIy{C!Rp@5=cuMlUY&Y5t z$M!7=s@3Of(|B)L_-1Pd6;KBms7!jdb8XUTB`mokVaGNZ>%vR^5xUakEGC;1@+wNl z=E$L8B$#NSJ@VBR4y=c~L+8(So^F!>HQY>KzdI_@YmpaLA-}47&Ra3^754 z&3&;J^R`a0ehtAqPd2ei-d8y!)`qeJOd?zNfqj5h@WTqlw=%VjXD3M{v>^(|&!@j9 z&kH~GfL_Z6*$iGS+pH#f=D}1-Qn_XNYslPrfC1=1EytwVbU_tOC@js?G+)Cg+DQQFeVuQxzqgz9r+f+ z(t4P;`=kx*vFR$7=qF6F6Q(BfiM5k@1xw~yry*v}nCVJ>@)PoHZ6VUwR7vf zahJ@H(}B`wb1O?33TNs8`$bVp3=E&SI(fqroqy9%AY0n#hP>p8=vr!I&QBfD02p|j zD<9L~QqY???HzGZ|GF=~wVj`H?kd-4N5f_KO}L+S`k}%EN71i(xpT>q^6F~}JE5}x ziRu7uteafus!;=UP^RXqkHKix@|?c_5GUHX`sGL-b{G8ime#_h_O#i>M8ERdTU(?A zmXCi))^V$6RsrVocsYYiyvndfr!unutaLc(x#vK0AP5*AyH`fYqC%}2nGp!fC_p>U z>u`S{zA4>gJ&>>@`{usNS_SS+;c5$U9*3O<#@xk*G#*Q^<>7;PfybX44pV zl@t9VcBm>^Hr8Rszi^>hsB^U0bnUKQxJQMW$1E|U*8DM%F4X>~my)JGnle$pZhuC4 zCbP0uxX;F%CwB0|&KCCs+MMc3WqoQeNmYx?1aDBJvb8nUd(TKf-{CDsCUT^wovM5I$SW7qMR1oAd}9w^ld#_-(8qkWtra$;1NzOS}9#?~#8O-plKpD!tQ6 z=U3|e|9lvVj;WAijdx-kb6cQRc4GQuO+&@$H$FcH99-f50+;K&M;5a@T*EYh2YJsX z!LJb1V)UvgWhKDShHxaA1*>wP6| zMmDI`-A@s=zSZAScOyW^*nV6TETh>pR$** zuDU5TT8o6%-oL;SLW9Sj@UUA^5z_Y$t3A(j4Pv&3FIn#?X>{AA4mdULt59DLyO|!^ zWn+wqG)!kdPluhCcQo(pTLT6+8=aPk$}suL4^`_?AnvNbw~vN(en zo4J)>@<`2@VV)l%SvX0htHM#98Gis-%0xv4IL(>)w6Vi94r)V*;;I~3Cu_Zm#;gG@X4RQ>*IQR!py zRfCjBU7nwt)rgAw>}|-_zrU19zjLye`sNd>$>_?%7=a9N)0I>f!@99a`D7cPl)r=} z?TP~t1{cH{S z`(7UKvrS`M8E@Ml$}ZI_$!m25C+Fuo_df6BD$(|1i-z}G4)<60f_RGkbE?0ai=A^; zqSxw}+PAd^hMZ~TMSv{kvw`s4tF0U^y#Msc_>%Ij(?`a}x$$6X_&W8?%X~(&YuRl%$|FnU0HH|b-YEJL|54G2IEdT%j literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/KEN.png b/assets/images/fide-fed/4.0x/KEN.png new file mode 100644 index 0000000000000000000000000000000000000000..4ac40e7f8289fc3ac7a6f6473151cf19aa13221f GIT binary patch literal 2150 zcmZ`*c{J3I7XOYV+jz1srGzlZge+M`m||oZ6Ur7wlx+rsv1Av@E<%LKE)OB=*nTq7 zl)=P^6v@8+O!n>R_uo6`y+7{#+;h*p=X~xtcYBI7HQ;0yVFv(!6JZEPoucEZ7_y!@ z)oc$pod1Bgp*0Qw7*ziV=(299)TvY0@8&(fJ6KmgypyjBfXCw%+%O(EXD4qL1+4GG z+%+u`0AQgb;JOxpd8@fzo_sX^*aG_+MO9KM2}DdFTpD{|q9!SI7R{WRXP7F$_Rd|G zmDk2PLdN})IHoZ6mb-TvE0cj`lo%$Q@7EZ2r4iqy=Wutquo!iwFCypA9iEJ_Bo8st z#cX@ZB3BK*Pjg1-x&55cCy9U0eaBJ0UB`c))Kta~XaLQ{(h?kfQmah?jMGA{+gR3tqLi-eoEcm4ybtZrv5SUBOB7jMpj(>;Xqzm zMn;CyQD5YtlD557SqWl4^tX=dC!va(8bR)~E4GdIVE1R4vbBQhn4)#LxxaRXpWyfR zZ&d1vj8#0u4@#8<8ilRM4TwZb3CACWdKgX=x+4{n5)<%6t)l zFg?xbE|k&CDOyM%M4mDMi1(p#cdNlomw|~zojt$}Hy1N8tUr6LFrzzKR#x_}#msvJ6Do+rW^#RE{UwQq*j(y@ZO6$99wSWe(utA9dzdb&FN z@Ca!dF*<7XEFpo7i6Kzy0wBfk#t$|yRD{UaP_Z-1SLJ+&LNy=DIiku}0@_Yv_yh&1 zT|pSzxNi+?Be1gZ`+tykf*Ned;4~?cK-}0PNFWlM>l=KgfdG$ zTJ5S{@q~pP2We<(er@)V$jQkmSGS@?GO|=9I~jf?llM$?L3o`*?^JD{Z|c!Cavxv# z&$6FW#g>+q#`eT<^{EW3M~ldz^lO8++)dkeUw5w=o2riSpitn(4*fWp%8IAAJ>TlJ zctqsK(C5>rWl5Z9B2IwU_DBEo=8j{H3xrG)~AlvhL2{ zd>4P$%G-*jcl<*Q-uC|aO_(!IheKw57CYCPT$va^8k1k>6muk@pI zqkjmR+wz-RMV!PmN@Y0$8Xbp9cE?tCN0n>WE3$9$hh*?g$xd-)o&(jBJq)}apj&X8 z#e{Z_=CiT~ksgXkR|8}qUCj2@j0(BCyD1lvGECOCpij0%r_hP$1QjD#yo$j{ZbXiG zWB%=|X59X@_(9xIU~sPS5(k>kLEd~*@T|6d_7%c)9$v)Oo?++`srWva`K&)03PtL zr9;Z2my1g8kA4#vj7)Wh68c;d`XSe^7sHOOIn(Kg9E@E$6PqpkSOI_UlF#{G{$TyOcP+wkKd`lJN=o3y_le-OrSzu4lSRCTs}!$A7(|Em%OzG; z%i8w#Dvrb^*)0kE;Fd1PRQvM-_b>-L`hsm0GJu_d)wCd)Ebm2fvis_jJ!XfS^M*=G z|7pq+X7#`gv1-dV;DFDP&~nOYy0r!Tbe!za3H;UX;zn&mqamU+4ViD3GQSRG9i{5l6N59V;GJv?^#5LW4^kv`N z)vmH_Nmfhl0GaMe@gJAASyO{U7Tw(0X(0;sdTn^9-O2L#W<>$-uEvz6x0qRtI;^V> z5tImQF%3G-Z@wb>PhB~jk!UIm-|22)<5ml`c?L5mcM?CGG2rgZox#?Xp}91}IvB;n z)$W`*-O_mch5FF?ue4@tsy(jjqC*r`ACbTR9ER-31Z&=3+8Lo^X4-xoYzAH0)w6RB zKbyhR4y@fw(y)*YK{%oL6YyjGvR&X_-9eJKgE0raBzDcK%A%^4<|Sx~%*WY(^RL z7#IEe=}n*b!)~8itF#j~7N?MbfJ!q<>TWd2X_C7B*RY2haQ<(Y}wCr=m)KkSIXiDrp<(qTYJH7G^=|J~02KB)BY)>VnitmDCa Qr#}LKxM>Qn)^m*d4~0JavH$=8 literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/KGZ.png b/assets/images/fide-fed/4.0x/KGZ.png new file mode 100644 index 0000000000000000000000000000000000000000..cde59d53acd700eaa9bbbd63b3ff17958c10817d GIT binary patch literal 4327 zcma)Ac{r3`*q*VDEo2?S7+WYbHIcH#46+lln;1ft?E6}bvXf;j384{1LbhxXlYLDF z*~XT!W;fsb{`mg=uJ4a?&h?(_y{>cK=eeKzzMnVVP+yaUk&h7o0I+ClX&6&hd&unNlowjY z)7v?B>mDBfz~-y1q4qE^?{98UhP7#aj|!<}`Vd#O9;PFaA^;Lq%K#;a(-0%OUe^7!t=@gn?n zt+1xj&Has^`@}ruve}C8kF)1tbLWJ)^YbmP0(4a`bDi+?Iep|)*80bX8w={WXx2AWIJ?2fEroagp*F>rFV0Tu!H58M-s$w zz1${C6PyXays-oHdaFf`mh6l!j)yP}tSw_pUjwSgVY3{ZfRhN`w`;g(_eqjQ%1au) zxzFd!?@MDE_~ChQ#dZ_?YET7(9}n?I%O=7r`{#xJ%cXN+z{(8vKG+|Sxx>N3z3DP@ zBioEBL51irYa`O7V%_$T7P-Yba&ce;*!>uy^`YP4$Ari!kJJ&5=@6RTpyI(%iY2S+jV(;?HB zYYm0KIwS}StBaM}2$G80F z`7nrHzI)gQ%CyBVx_bf5nL7bi^ytz*p z**g-uhi;Vd=o+K3{Mf6@Xz^DgTLy0ob>_LVCQt1fRtx-!?_Xm!Un$iw2r-q)>I2?Dc)3Hg7>rHX^ag^7>03pf*D8qV@fq%^|Tn{^ia7!P0#g zYR!k5Y^wcQCckE+E?I?h;SVUm{61O=Ymn?X+EWg$`iL6tjPb20S}3W0cI(cZ&Vt8;!v0_0QXqa-;dxrx zRJcD=A0}pK4Xx;*U%fcv)b!Inf|Zien`GzCeQP`Qxx>fT0_{_dXla8z%25+6#UTCk zU15p1RxXFt9(G-7jrw@X$xYhoUf6PNhM?pMDXk|KIZ31vOo0Q#jZja_iH?sW8IRB9 zxy7#V_5KDNe4liE_-ko=wR2eAmpMToO-eIe@GxKW0uSe-qajF6+1X|bF6mf>AIB$q zY?z{yHCfDqxL=m#a_k*D6$DEfa;$e)Pa!NgtZuPUVB)YoboTwmN~ml+M9rfjtH87S zNt28^1%TZJJ3A99V@A^?s=}3fIl~g?CT{dkFGE6|;=84L;=>EI)>9#ItpXx}t}hVD z%!PPpux;09=}3rM!j>C{!hJd)c@`^|xNnfqjJt%*lR)Yh@-w0noR1oU*woo6cYZL^ ze(_P1_c#bd?qGxyd&k1B4Q{sxpLiy>o+gqxp8BZ`vvN}az?Otjo2xYHE;y`yvIiM> zu1`skO~fRo39-B+ak|rQesRWrQ>zq*^d6>wpb6<=n94svEK1vaV7 zMvCAT{~GT6G^RklA@|ma$E%SLolS!4#ofobOqW~C(motaGjAbT&|OiVk!7$|dpAw` zqQUZ&prjW|@?P*K<5ByM8eIJ{O1h%$nDwy0tV;^ui1wbTQDx*C=E6jS^Jd>ZBJcvi z6)I6)?f_lup=S1YX&B*>Svr`wh#S>X5o{ZK`?t}^RaUC7L@Yy?o|4@J6_P{fD;{xM zunl)IEPQ(mMj_C*%jjkSsibWEuaoT?BsYeExfNF3TS2uKo>Ja2@d)chj`7o7s3gN8A8idvCbujk+2) zH#<}V3#&(@M$>)v&=vA?->!vybr;M);l~fcl#ovYL_YqK*AMx`prbe+>j(7wGym8=ofeLITR#m_2rVc_+yBy3wrlJ+Q zK73*T4)(yc6F%rkbpRUpuBl1KT(V~2tA34m8Dh=p-)J;?x$&uit#=;{o5qud$5%Y# zUfz7W=we>^joaP(bzh0pKI6Dlw6uXIE9OF`O}p*3ixRaJiYAU*N!_3~MjfVQsprL6nI9c_XE=qc>nI-J zz115DGByr74(rv7uC>npGvBT7y+tNe!m_1sXbriKZCLlEDbxOKuIX~Re)ZkCOd2bl zw#OyospEk?OhCG%Uns?0I1#eDEsE14`a6e|P?yep@~V>IBc}pxR1bX$65cZ;=s&Eg z{h+nJ14uM?0-rp~9s-Lo{w6%1v%@u3b!qMP1D}h2FBwp?CpNArRHXQ7U0U ztj-weg|K6?=ru!dBvz*26laAMkRq_c|Ar ziuJWzDk5@%Lnbs&CpEz*+Po$l7z56wn6lRvyHACvbD{e)k7BSjWxUiTHfedh^f5a4 zy!{Q}tbFqu4iaxWcyf)bma(a0+x*#-;=;fY!}wJW;F68Zy}wdLzYA*z5!)HCWo2Rs z&}^4KaN6|csr3<2ddc?RWm}`7hTn56% zD3QQ4pI>ae<^u_m(0ov($bxR8pzvMoWV~lmG&XZ8%tC28TcbyQl;VMg(KYj}!ZlXS z&11K72fic*TAJzyH6~BE-Nekk@BgWL49O!bRGtKww{&k>8Q%))e)%i?FwP6^+GW=F zlW?nJ3%{YQ!rmrtpf+ehdbxUg7|lK`f1JoADh>aG;VpN#Y&@mP$d{nZUU)H*cknNZ z*1up*7oP>o+t*N`u*02C!fGonA;!b@>a0WJ%;0aI+IFi1qsuaXMfhEhc$%laa;V%v zsy1sGx66Fzt{(f~8}+fCIh-hRa=nwh)}@V)$JkPj0!-Lfzurj7{#Wy9tb7*N7Aw-k?sF{EO9SFn?8LJUGtcDv}-NYQs2efc=x$y zYe$;%2}PJGCF#Aus<}O2lq!A}=Km^-BBaQO#j>%#IHak4Wl~bUbb7-l)w51onmFy_ zLXN>}uM2Wrhiafc9(p?~w0e#OyEjiWIop@fij%jB5~k`Ddu)NTNqf@gm(G|tGn=P$ zcCPc}^T0>^ZDP9z6F&R+-ISQU@mOp-=?6Fm7YT!+GBjrT*GpC@oUOp2NZ`NxJToi4 zGVPaQQ%t5CY|JigaT)Qqyx8orEX95AwnLEL3%(cC$>`}SvlWeyR8#E#tf<9qUHW{! z$=%_c@ubl;fsh4sXi~6&#vA-gfm!w zQQQkx{COcenHDPd;{!iZviXnr!6N`Uno(VaMTF#QGP7K|QEY~CBAHHXHWK+KKHRQt z)+JrP5v|jQ@kpnrz{t_swSnw`ErYT2-|KI~9D*&MI@E+clc6xy~ad*Ulag(Z}^GxEbt|}LpHKj z)R?+j?K1|0xce*GDoL0mtgqo({#2y!{S`?e%3-CK&dJ=ub$yno`qM?0BOXZ_eXQ;8 z*QGB2n%-UxET!TQ%shyHKfH;q*O~3kXAR1ihB9#`r2UxkQKqzX%gXeP*NV^xT>**( zUIc*^pmZ^0!S>Or10Iqa}%ZN#u zs{fqPiPOQn5b)UjXwef!<`eOaMf-jo)pH^3d&7G={+$EKCNyOhTdAB^zwlOx<2!ZH?b`%EckJK4-&m1d8sC5aJ&DC)vazwD+}vEFxl z0F{-M8Esfs#MZ4_S^XR^U;sUR`qU_yMiC&DO7)!Iw{Kr+Zf>UR>};b9>q@AvucvF* zu2FS$HMzUHv-&$~)F^6fY!ob8FaZt@4tmb-)~y>c3`1dIVI-H!>EOYGf(`2`C@n1| zPft&>wY86ORE#u5<`!!Uvj>?|-0L-+38BYS&$R)75c{Yj}*(*FJXb@QLEuP;?pR0!%LF9J4f z*kI84iHV7H=FAzrIUzDKl8A_Q@7_(bX3ZjnLc!a>&cN2KTWQXmIi%HU>B5BzdRO}g zAACRz!_c~Q>nJ=toRmtXuB}F+;iZc+0qN=K2Av-r9Zh+8d1Pm2$0`pE4JEBsOKE9o zB$vwx0Qva%P-SH$F9XeqR;RZ1;j6E{A|j&0hY#yTdz&_GB8FjDS3Scp6dfJSD?H^y zKxt{ILFWet2h)=$Psqi^g;nn5vRNjR(XYS$N({qLcz8JLoP~u2Wo2a%!!Wd9!2&_yX@>;7I{pD5J3Bj4 ztJU<*JMXZ{TSGO4LP4WPk2Xk5R|mr|G)2 z*47#fPgk#A-L9@S2^c?qyunysp-|AqjT_q>E0-);qDM?>YN}vosLRW#?7QzMZ}e!& zmPjZI=)J$v@Fsi#c>E?v5$C+Pb1>-6)_KkJpZMhoA3^Nk)cKm726AO|Rmil}hW zBFdJ_IXS(p<2P4VDvymN<|PMm#m$>H4L)Eo3}Y}nUA}xd5fLkr8Z>ASJ$Ue-O&#qH zLAABDx)5^Do;`H&;zd2Bt>NaeW5@I+>osfE@a{CDQc>xK4RpoA!YIeTdgKfpO7$5T zyzMC}Dq{WKvSo|z`NmLqx_$e0Vi<-ZA|g12-JHVEuXZSvksAniog6)&>X=q46RzVs>AS9DwR@7N(w2J zN@{9q>QFA{2eH)DRI;_T)hT-O%{Qr{qJoNwipa^y$>?yi;p$bo=IHqPod42M@b0^$ zZEoge-_D&o+YC=X{P06vxkMtN9Xob#mMbFSzA7R=KOZw@%s@>|4K7`}1ZQVw`1$#v zprAlk$JNyp+1c6X-Mcqev1rK5#J#Cgp>1g3+{Reg1qNbpVj{#A7M$w0w6tK_v}s69 zP1Tjz+S=ms<;!q)cgK_|Q;?OFg?;<>Va}X6obBL6002~0R-&-55Tizo!qll#k(QRm zGU(E!OYrdU;4H8C&K=wyHx8Pb8a`&21U*AT;eO-@Am;XEw!Xd|W5({SGR8*Aiw@fC(*|TR67#PS|2BT0Q&&vzV_wETX!6b0m zvfy@Sq_5fYu6x>NI19IyLT@xT)2SP*jUKra>2TdBET5r`}m;o>ebFrZ4hp8 zap?8YM`qXCDFoD|rsD48$*)z1Mk2$_0p2Kbaz}}SE3{$>Bw7u6HB@3qSssQy$%VD@ zkEqLX@L&wTbBFVUY$|jL0rx{fP;>I+YmrS5A?vLGoc~J@>MiZstS8g9U|hjDOwLb& zgwX&1hNPsx?loT+ufpsEXliQko3k?*mCEF@TO=|Z^O=vFH@rBvaeiKkh|4?R&{&0D z^XJ2D?_QJJ)gH{gHni^7Utf#!H4+(iPWYUc^8ujDzCX54T7@c`-gusr1Tc)rZEFu^ zC!jt({k6zF?KuaJdJPiPw$Z{C`^J5O=GuBR<>i^!vi4zi0-oiVzJrBM?zrOSXOxTz z+rGHuJ`swX91~f^4Q3~x;8}~wWuE=(ED%IS%gG)TfaZexCa{P*%t`>GQlZF_`%5}5 zDDC+MN*o88MAq}}J#nwhd@lq6GZUbA_8etB`kPedZ>~Njm3OaM_*1PAn3VvLNQ5$b z7n8{&h>+iZq{-v~Kq;(EDz77$l>n=rc4)A$L3xjUCXrX-_$F$t>`fx86;c`Z_d%wB zSqTtZSioA{gqs6Kn?&BZq0>wv=Vdr_mzi8291MKZfTU92YrU%Uf-c6 zvg!{w_jfS4%=Tb*0^YI#lrl>k_X-m%qt3Dij*X7!xS{|I_Zjk9>P? zXXaMhE*w?0IDTXm#4__wR+&Pz$?)L_dtT0G7zWMF%|`1riU1-) zR8$l`{q$2_?L=Y`7QH{p7}X|)v$7f?Yd+@HCNeS-zP`RFDk?Hcw_pMk3I#$!La=`Q zdhFP-1KYN3n8M(Q+@b>mba&oe8{X!dl z`|Y=gkB`^=UcP)e5)%`7$@oXi67*1;_wD1Wc+as3qeI`}Tt7226N?rt(*3TfsX<0Y zhG6}?C*d16ZXiEDAM@tTLr_o<&Ye4_J7;BOh4l1vczb(umY0*9iB-FAKrJ@!D*yl( zuB}AOfv+HybJJa1Tnrx{A5>IS=*mJvLvi%zQN+c?p{lA1UwrWeL?Z6bHoOQpapD9b zA|jwtsc`b-NsJ#q9)5m)$j{H$)p2%qMs{{KTwJ)n^-SKIgy^%yAd&eVO9rW{uzmO6 zVQFW>seWT)BmDjSk(-;VEA#a9#Knsjk&}~yz`#JLR4UAzITQQ#?Sq|N+bc3&2In3h zI&=u3p`mDMYJyg)MOauE9zTAJ)2C0v$;nAqudJ*L)22;BU0oel(U|hlbbK*wAVg%I z58@-3N^IM?jI;9@hQZC7H{s~`#{;8_iwhDH6H!@NiTB=n532uLXmI-UX>8oMk+UtF z?tqGl3dm$Kj2%0cwG`di*%?=^TtP`m2?7HHp;Rh$=YoTS5g#8936JG|w=(naum4HH z6Il<=ZTLV;3}!kuVcGY8heXOfV{Xf#;5bSVxTIDm|d3=AANP;UhA_Vz}4 zdO8vk640+-KluCmw=26{0@P|XKKbMmq@<+a>eZ`ItJN4YW(+DTD|K~RZ^JWZ&S37` zxnN#e*0Ff;V*L2ykDNOS04+)tc0~Swlci#)C8qm2H9%Q~RSWzve8zwAYFA!fjy`?* zu*!;xis0(%ig(|A7YPXoy7JZwvbeYyfq{V_BE-hV;^U9o4sj(>FRw9w2{kn}m@#7p z;^X7d(9nPj7cOAw(xnIp2*BaPhoRBDSWWiy=~LXle;@1Ct%Fo5#rgB+buw<;xB&+T z2l)7K|7<9cN-=uM7z7P>0il4BZH?5_RLEp9Oqw(a_4W18YPC3Z>J+@Zyx{KcZcs;? zcR*%lCMHgts2ke=VD{|UICA6&jvYIOxpU`&h|sTJKO`n5V#J6M0DuJx7GTewJ-Rj$ zi3IWS@dyqM=GFODG*&#r*&mN1JLh-Y7CWFxX4PRE>y{>XSt>ATiYNR(oC-0IVXzgi zMm&obFUI!m+aY~v=*r53yY^YcUAOKXd!OqqhDq@=co;`a70 z9XWDDZx`U&wQGrpX#M*2 zC))nE%@nopYufzLdiw8AHq*bC$57(dLv;W0P10(OOO4ae(4d!sAs`@t)M_g4R~OeG~H0^DI0;;dCCl3z~0zlonccS(78Vw)b6q)IO=Dvt>&|Vx&@>tiO`krU)sMl0 z2a`gf(90JT6&0o1L(B!7_X$^5SJU9ZgIPwr{r21R{P}Z(SG%XDCp9)U($S+wbvx*T zf`X{7u8yBq%#4PH2AVl@CINq>TU)turNO)6&Ye3H9v*JgUCk?H*@FiU^s*&Ro;;aa zT3QTd3Yk8AI%zZ-+O%mCtzNxaaArYsL!;49LP7!&5$)Qwi`6H&Tuy0eX;e{BL9VW@ z1b{p|JoHi;a0TzAeTE#paN$BCB06~RpkCCpa^*@vhIIv$mzV3MVA!>57d1CGldrEY ztK84ek6Hv|W)YOO(olFB6B9#3FT&H-GJAV_I(P1z5EWep$;rvoqel+{UgZ8UlsSuQ z#A6786Suw^o{Gg{I(6!lE^2afbECU=?+O*)rEurY9a_3{DQUG@)&m z1Q-fWt*xyoCntwgDizh#)Ce}JtKyeme$l)0`9wU1U{vmCL*ePNWy_2*s4JtTrG@pJ zV2b(8==Ak3ho_%^{`vo#NXHoL-Mg1ULqmBdX*C3stP6Vh@F7xCQm|yn5@SXE$Dz|w j75s@8`13zxIvM{9l#_8@3|h$@00000NkvXXu0mjfz)z3X literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/KOS.png b/assets/images/fide-fed/4.0x/KOS.png new file mode 100644 index 0000000000000000000000000000000000000000..cd8a9ca0d2f58b131ce692c11a128478796d5a8e GIT binary patch literal 2468 zcmV;V30wAwP)!dM2 zTBS{!v{!?~9zt}T)T(N$mR3_G#(+Yp3KR%Ud9>wK64F544=3?E_VxAt01`|>9nyP! z?aiP6^7HSW^SkHk^ERr0w_#c0EI~lpfG6x z6ecZz!lVUIn6v;2lQIJ6b!t}JR~*;xfkjRXdQAc%l7^%Zz&L>ctF0;EvE`+4{pyyM zaNnGKN%~9#TBQR3SXWoY8+)1<^bE(?3-Zj^twsPIT~^AXngZrjZ)kEGQ`0X?L?#S_Pfn| z(cu~K-?V-XPp_E?Kx>DGwHx2XH55Bra;ye+)~{e%VI}}CzI~2AzjS)!^OC|$)~qN; zrGlmNr&8gx^71Bs92lWQ~5-s3|MM2v>m$^Au#hm+@f zS-Y~F3a1sgmiioaGrJG9ju~j3R?VYJ%f9OE$3=DlR@N4C|H7g%cCSCo#`>c)Uv-bz zw!d+fgAMI5ekv7Im)Uq?(|#V@u$%qI+NiB57}H*DO#%ClwejGF-8`{rKhTMlCvx8n-VrVsqHdboa+Dthd^-GW3Ka zB7vX~j8>+7mm{=;3gb$dtRaTZtrc=F*&zA?X$4Zk}~U|1j$iPCts zhlnWcaLFP-X;Bt`-BL?csV&C-;i)b*)*q#BFbKe7%gX2-2ypmhCmM~4AFsWOH}^Kt zI}nsQBmnYkMxI_XljmPLO;`|FFxx?{&B(t$Xa&Ha*RW~*9KQc>Wz2OQy*_^N{4pBO z_DCL+Y>Ti_ui=$HETg6>7l57nntAST4Kca)b_=vz3*SWh`}y}~^UQbd2B59m%OgMC z8#4o?M7A|xI2@rM*GyZt7mqKDJ=-wmM#mdyIjfF|_BU;6CjNlHg_gmQ(pD~H6JYiX zJD+^sPuy3O(M z_{Nr`at%I!CTIG$e1MI#<Oa(5k77&z-k=k#S=9&_va`O;hm8bn{ju-BQr0K zYu9Na0L-1*2|!8qP)Z1p8uRoz7DI^AoWTkCe%;wdwke2OmE03NB~p+|6=lDBP%%-b_YBL*Ta*d` zF{+})Wux5g;z(;T+YT>CX#a{n2lzueD(o&LLqM&HQhq&#Q|)6^eV#_4l5R?<@h$E*t;)w3=p@ExxmiB#i(7 zR|c)T-&~3iRdIjCWm)^$d$F7oZG}V?sC;fF>9(V9P+@oR)V$M_=k`mQCnKT?&#(cD zA;jN5oX?Tg_&@xUlcaqi;|iE~uc?%fpyg`?-IBD~t}v(dwl2Xv<+opH3iAkkDkP99kBA-_m&U^qEQvEoSZE~w0BO@ zR6(~x1omAjpRoVw&U~KR_f4L9>tU`8j_&<$KYlMmL4zWt?w67D0sws0HH9vZ<+h&( z!&;7f;bhz41^oWtBD^6zVpPS;AJ+iaUc_8hxZ1fLNuPq?+QK;)G>kbDBm#|Br*iB{ z5rQF9HMgV||J)@gu zr8xr%Mj{<4BS1}I7pp2q|Chv!TxDNnN=3>n!d)H@r85(gG+A^|h8UzJ6s1!@LKq}fy1Pb!A*FEu3F(fZL4*NR zYG_b$B;NJ?`_^0E_s6;C#yaQRwfFwTj?~prr=++=0Rn+2HQ+FPV15A%ID{0qlH2cF z|C`+5rk)@Wk<`Bd=20r*1zxgvshW5hxY>F6SbErke0+R_9bKJ0tu5Vch21>tvv*~0 zfk0G38Zae8U(`;vqgV2DQ*cfU7KglvAe+M0r;($kZ$sIr7o5K-7t4;nrkkF58~{gc6qm{>W#XSXro8HAfpz@#BBdH&8XEh2Na z-S#f*)TvKpq@F@4^DvM0pY-vx4>S-%S<*p#%d|gio)V#Aw1Sggk-LitgLQiU-;X!x z5?IAT6bl(a3P=#Bqm)`%^4U(+1%pyh$u}&9j6CoHj7L+f9+V)DKRYIRMjS-hFIQKA znd!t5FTpCNW7ho3D#f5&YC$kHIqdkgzfIur8wj=6=KA^7FA=a)WLW0-V{o5S8H@3psJHl3j*p8+Sr>-nEsTm`#vcX#3v|Cbtagnssg0TIRJF<1}O& z$)^$DPdqyq-)D2tl2>+H;+AJ?Oz$}mgvuWXB;FDA9rBlv;b*IZb(LxCiAq2-R}#o& zZ;3AZ1o*d(Zb7K`5B5Je6zp>Qu#5(s_Knds-qr-sGS$`9sGLcaj1kVyc2ViC0<*IZ!rH6#)>gX-yc*6>(L5W zQYyZ={%b)z4w7uGriflr)ZTd+L3~#S(WKGXn!sU3@o)ht!w85e;%a2Fj*B@u;j z&<%i@Z=r@K<Rjr}Qb=i`qir9KU=A zI4_Z_JTPUN=`c;dvqM|>zJvbIa zZ}0xL*`_VM;iIP|F&q(?Buy6h-VAN2QI#Hf81V{L!pil6(6WBqz3&KrPpg@A{gbG% zWmj1GaJU@Oys_*-K9B%u;kMwKh4$@?P#2c7`&y$TtNuNY4Q~lVoM^o8$1j~$z92Ks zKJs}QIIDm0$T5{^tkesHa}@q%v!OS(&Vc7(2snxb?Sbj6A!(_6Y> z-b}}Dhv99CnXpk2Z7#&hp!XpuS?t|5PO=XVQ9$LsP30U`kjX3v<^(;hlxUCmW`^#IpcoDhX; znAGncV#=`vpk4zu7hB>h6aJtS!o^H4kyJi+6Kt6UuN7buq+3!ho2c3RMN5=w2At19 z8|5G+O;b+sb8M$Yn_=j{{g3kf@i7iV%spXp|%di1Upv+}`MG`j@Wc&Z2`=n391$!B>p%7!K zwf1)yDxXGDT>SZR&RKi+AsuF#xIGaaFk4+yy4SotGOuYijTA7`A~#akwJt}0u6yf_ z;6_q@KOMOCNqWYkz=6H$^7D;6#eSyzo4ONE;kjsQ@@G?qpkQORO`ss`hylbZzP><= zq+Larm0CJh84(NAea$>pm|`xCINBU295nRGf_QDZzcu1asY{{Wpi!yIOOjb&THaX0 z0);#*D<20udA6*HJ+LqthVPgh$I2lrtfky6EIR7?hV&v*wc|20eto2Nik<$q=Rwr^ z_m2;L?@k*T;)fuHPKl60o0(^P!*WSx){+JWYNB#Z@*qT!8mLq9Sz>f5F>XGeUUKzX zuF*z+rPkj61~{X_PA9#HgEoTSncs5YyG*Uwp)Xu6^NwWv4we7tCk^SygP5)bZ1Nw2 z-z@adw_J3>pS=&kegtlGV$<^dKRhxvMCk$snbSKgxz?^wBfk`t&^HiW(s$V2adb;V zq|GTm5g4dh{mQnk&W3e}Ubx=NhG;m)T@wan;|0o2Nj=5Bh`-#{_j6*;!vM|$0a>6> zOnZHO$swMc$FfR_0x-4aNAWx3+Um?x8v80Ofamzx)mK5#|N167|9U2jrDf3;ZzXPYV`Gv@Ix)u1lWvJ|n`5Mzs_M`@ zTp4Zk6mM8*^!(RM3xjrH-dpRQzx=MkHmaSsfDkM837;7U@czt#g9rqwU#Hz=hIp+r z-aSDfnB@1aQRYg22k_|RNoYx2@qs;CV+S5`&cHkqT|%HCNNQ#ZxpJd0`!A#S@wHF@5s z(bY~-_W?96TgxEYG@8Le9`Z-`=kPx{NGmStGOa&X_dpADQ8`4U$-&hFKCfoDh285I!iL9gu<=t>XsT0HEwT~8^qnY;K->f>8>RTdsoTzpOY=gn zrEb#b&SqTqieP4yNCMf4EHMRW*)4dNO9_sDtYKyLN&8au)pze9jm!~?5WXe`2)~fA z_+07S<>boM@-qV<`Zn+I(VVFS+Qk$^`iFh(hJxNFmna>nOA z=PL(H4N}NM_|;;|z^ArI9kz~Nrwptg+#_o@ufveN`u62{15=>(neA9vmge_V;V~T9yEB0#cvK4mE z)*qkp=2K`Dpg-(B(vRhD)xYC4xb|~*{GVmP5ri%mJ$bYY);$h(Sq9BCx0S9XL=`X# zk^bGlj^NI(EsS_w779SEaUrGe z&w7rXciR!OP(>WXX#M(r(%EdfJ5fk57~JK1h!^P%*HJ2t{9$(VfL@yE_s{AoM;GT8 zJ9X0z1$WA|{i6Y+$Vm?|4Z4t=u?Qdm#P)&O4xjEPTU=gX40&chV1o*43o{r!#n%M` z(ZC&G+tN-fuoF{ptSP!2RzAB-AQ;lizVBKe)WRc!)PSw)U0?OeHY;Pd@M?0l^Y=7N zWR)7bmHXJe5hK)^%b5l-5ogfV898Y1$Okg%W$SjC{(-GDj@kQI!KPCpaXWAyTC5p= z@QU!LKxd%v`&9qHL_QvSzZ-eN*-@nc$ZN*Yau>`2?Q1_~9}#MGyy~`I(=Q{DvDv7X z+=~~cvc!DP@cc~!I4B|!fV@YKkbsBc=zV=HI_RT@@w11{WG{<)O;iD$0+tp7`_nBj zR^PRxv@6S`SvtyXtw{4U_ziuQ)@hySnUN)yHI%vzFafMVJ^5P#Hl_0M*ibRM-~*w% zbK+@;u9Mu!_me?x4FQ*U?T62TP42DjpI+Y{Ug$hVJw`%Z1O#bks7p5}+GDdMfYa>F zM|i`W`$YK9U&S%(@fDtO*p)?wEAwGo_ZcPhsdQ5BEEh&!Rqv?<&8q1N^~MSQ)m#ck=4nTPp)104 z4buq&fR8T`p8@jW0%(+%UB5exN@KO;r&P^&wjKb+^ zN%VJn;9z1Bv_9{NI4cz8{5zIdD36ToE$qGL?^6VJ6|~REb@h% zgt_pz7PDWG<4#;ka%6)aBvXwK74KcR98=ZK&pC=oj?EfV5rIGIKUjpn31Rypwe&3z z%ENYFu|46(I5*+uKO%$Q>$IvHb8#xb&V_YML@nZCdYn3(FcU7WwclPk@7+s}Tsh2Bh>#skeY ONJCWzR;m2r?f(F&FP*Uf literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/KUW.png b/assets/images/fide-fed/4.0x/KUW.png new file mode 100644 index 0000000000000000000000000000000000000000..806d8c628238bdb5953f724d1012713596b488cc GIT binary patch literal 721 zcmeAS@N?(olHy`uVBq!ia0vp^2|(<@!3HEb(?2AGr~;43Vg?2#GZ1D}bzG(f6qGD+ zjVKAuPb(=;EJ|f4FE7{2%*!rLPAo{(%P&fw{mw=Ts5;Km#WAE}&f6P%i!M8e9R0Xi zJMx;t9wCW24l2P*7ChrTAREuTV$oB@3MQ@>k8afLQI*~5JMpF@yMIqk=KHG=&%ZOA zJodjj=178B+~d>12}Ud@jTx>L7=HN4^y5dwZ`-}avIl1O9yC3%OOk=%xNQ%tXsJ78Ib)@;Vmn} zjyC2k(m+L7+`>Q&SzO{kb{UsEkX<%`^FbkRbP6*=%whW{%oQ=r3}=CMF}!7n>%V@X zm%$SU(mxHaEw(WnIZKL zR}hf#m}yEO{V9j_lVCB0>@@L$9dzW@4< z&o5?v3wBw_Zopu7SMu@g7c%9(%h@!YOb?iNebAo$sIq9$Po7XM#$R<`XCGnqS&gQkFU0~@E%zGe}4kwxi<{=fr{Jp zuB^A|3fm@wUd1etWI8Y!;iDbv6sFulTxktJngsVx5@I>OH^qn(!55u}7q_ z#_?}9yLZ{Tm+8Z++145dR>~$!6sr$l$;U;RI^N+hekOISMjq1PVY z%CEVrL>t28&zkiBam%nmt`R@zIX6$Bqe>vtcFp8BK z*2l&1#Kiwys{NrcTwib9C7J2#4lu{bP0l+XkKR$DSN literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/LAO.png b/assets/images/fide-fed/4.0x/LAO.png new file mode 100644 index 0000000000000000000000000000000000000000..a17f66bce0bc978a1f056159133e90974e3b8315 GIT binary patch literal 1087 zcmeAS@N?(olHy`uVBq!ia0vp^2|(<@!3HEb(?2AGr~;43Vg?2#GZ1D}bzG(f6qGD+ zjVKAuPb(=;EJ|f4FE7{2%*!rLPAo{(%P&fw{mw>;fr0sqr;B4q#hkadqCG?sC60YO zUbc3nZArjor&F7kEsIsz6yOv-RcWC@noEj{SCWg!Mc)u{K~cdaDe6j^K218OHtBI~ zG6-BGVyw#FIw`Gf=jye)l4Dy!lCEtszW;mX`}g-M+5W#fv(7qB|CxIYv$ApXFP3H2 zd=5R#0*@JvSTGpkXZF13*}U?6V$^4+5WRY3w+?;BBRyBtALy-)bc{(+I5h3YL7Q|j zh69EcE!*E$2VB3>Sl2%J2hUw&Ri=ouT-%o2oG^7#L(7e&1`=v@4|iP&ymDpx;w5{; z!t+9XJ0*hdGw~}w&nf2N&s!mWUdY6fnajs{jaNKS!sqR__r1l#y3LUOE@OX%{%NQ-@UUQgUyZWna#~)1iaQEX0W`+Y_tG7NfKYcXc z&!vlDdVbi#-P_}D#GOxPUw3b%7}(7>*ymVOt#i6@Z(-B!OE!jzt_%zsyW%>RZOgk` z4vd9@hsT0Jpq8l7~<3tG{j@db~q-(Kzo9x2I W|Iu+(U(NwDKZB>MpUXO@geCxy^x5VB literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/LAT.png b/assets/images/fide-fed/4.0x/LAT.png new file mode 100644 index 0000000000000000000000000000000000000000..5c51383708797408d8368b89b202c3e56c94e035 GIT binary patch literal 337 zcmeAS@N?(olHy`uVBq!ia0vp^2|(<@!3HEb(?2AGr~;43Vg?2#GZ1D}bzG(f6qGD+ zjVKAuPb(=;EJ|f4FE7{2%*!rLPAo{(%P&fw{mw=TsOX-ji(^Q|oVT|Od6^tVTm#$I zutrsN@ViFEN#8odnIHGrKUX~y${!KSxPd8w^#E6cD1$ab z7{eNd2;u~8=hnaA^_DI^XZPG`t^#gt$DUVwmEy{M{(ZT*&RXFX5^aGw>>K-ZKknlb T>ZF~3K4tK9^>bP0l+XkKV;o`L literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/LBA.png b/assets/images/fide-fed/4.0x/LBA.png new file mode 100644 index 0000000000000000000000000000000000000000..771b53c2fc3e17658030b1dfc466d0066c07ba39 GIT binary patch literal 933 zcmeAS@N?(olHy`uVBq!ia0vp^2|(<@!3HEb(?2AGr~;43Vg?2#GZ1D}bzG(f6qGD+ zjVKAuPb(=;EJ|f4FE7{2%*!rLPAo{(%P&fw{mw>;fq~h`)5S5QV$R!{w%*EtB5mhY z5(7LGS%OwG`f4xg=w(qk78j)PkAwTI%aQvN{JtGa&<*ZvUE9?9%`;?6q2QGX8d9bo z?GA$@!?#~Ic9dFlXp&Ae&6}IFn`T_p=J#er#CFSWJMj^SSJKBri3s^5@dS2 ze33U)s=}&Xt$^v$%PmhnS87f_{pr^)t@G#42Wd?`BB6Zw^5r*QY=mb_EijrXlHB9t zhS}^HvuA(4<8C*9`E@}L z#%ZY!KYS2)zh>RKvMc-yIyyQRUUSZA@KJlr_wVRYR;7s^$C*}L)e`Dtabek(n_N~V zwljwB@87>~tbQ`z)#GYy(oU$X-1)ffo~5OwaOgD2%T?B&1!3FBVuA&9$Qq@)Z|$H2oDeM zIR5y{FBg`5aUF~qKY!+5d0o2lYSyJ3v*>?veC?_-eD&7*>sf2Xwsc><99({_ufM-K z@BdBRjQo85g#jILi`TRM`6&<=8ToR@{Boe=#;sdhckkYvZ+~OA(}6Q*daNs^PoI9j zj#pMjrsc%@>lb{~nq%~y*DYs?YDtZ>u&^jlU9)y=sZK&&-Ml#e2P_>NZSC!WYr_~B z^7He7o|yOB;Mj%N9BUW^4_>))rTy=M-d^4>-@a|LwU_I!-x2$>(pb;n_C;rB=cJyA zCr^3`3a$*%+O&DI^6%fji;9bt_4W0QtgN)ArFK5HD5|UT14iJ5^v3BP%P$)oTM(i1 z=;1>~VEk!_b>Fyi$3<)EqUX;|nTlEetp8D^dWUxfgOOH450}DXrV}uxu;(9-&BZUW izMzOADZtMB%x(B@=7+Pk6TE;~jKR~@&t;ucLK6U{rju6y literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/LBN.png b/assets/images/fide-fed/4.0x/LBN.png new file mode 100644 index 0000000000000000000000000000000000000000..71fb1f642920df929e7cc7f55e7372dc30a1f22e GIT binary patch literal 2424 zcmb7``8yQe7srPvOV;eNl(EZ_tc`@B6vkGTY{MADM7F_5J{8$hwlMa6E3!0}riEsR z8Of4;!i;uJeJqN_Dt`^ zwCfxvnIos)md{@oa@{2i0ASVqYb^36)e6i-@o;nJaED-cI1(D_3qT@~>H+rx!+fA2 zzUslDeuWz#31%X@CD_C<3jGU>vd!_8jxPyY2>E(~1DLreoYHsIIteVV_YKjL-70L6 zq5xk@u*>^uiHFZdR7afUx}C?W;pnPl5)|G#{*@y;yMiMHSYDc>aGoQS`oVz27zvL| zCad&3{e^-PWzd^6?B?cCF?F7ZJej~Ic=3~Mip>vNhT2ulH-L}*tZZ`J{AvH=M9{MQ zJ+>p3hJ5AdDN=k5;&Bwo78|m9V&WKOnYQ!}%3lchT}^7r(6j}xWywva8X zSL7O!n^=!h0$Q%>@lhlTB%K*kp?lk{KJ0OBq5TDW+Q{pZEx)M&ru%ag7% zd)|?AHJ%(wP3pep(_FklQY*Lj@pICO)Zu)8$ASa2%Wwe*UGc5dQAzx+@lG)nn@VICE#meIM*nKa?tbDL&MIXWQW2k|ECBX1Zv{qh>R9Vw zrn&_fVEt4@b@)x3h=qQd7rFHL%HS^O*Y8WCkv7_SueHaZHSIo`k(n5qw#T0#CM2N; zX7ry6ZjD_@73p#TWdSRV5seIQo!cs^-!!U^;Rb$1+A{19!BF#aB}E5mk5X89)vxqp zPT_a&^G)Nu*~i3JovD<9+Xn|egBfL`E6&+HNwaI6QWFctLnTHU8w(4kq^sXxUvR%w z*9@Rn&Rca4I?qbTS_&%rIc%lu-Ho6MbFm*#X-mh&#+~28?39Gm#NNk`vDfKO>kAsG zb`fe0qNeWSf@L6%a3Nk@UJ)eq<0m%sYtXY}6r<-Q{k*S^FJR@8;d=Yt*|=RzDt@@8 zrBfi@>Bz&EK-M9949ZU`iUm6bCr$qi$32EnpR!pGOvjQy6ySlWX zS^|fV#5X10!K}E|*1neAr@(NhrOiytS$y9P>PP$%`mMOQ>GCu^hvyChlVHbNLD%2( zd9j`4>h7dE-pK1rsIJjtmLJo3t*F0C_@Ct!7W%8?PLG`i`kC{Z=a^eT zUd~tT;kwe~`;C%hX|nJbqL8jIr!l9VWMT3Z3Bq!3VV(9^KFLWXn~wRzudkB1t~y0_ zBa|^wNf_WJXzeD(=$@VZy1JH3^a@0zj=;#w^Er;)VuspITDq)oU>qZor%-SQXFEFS zi4a$=5jL&;pFnX?h^Es$&vUSc9Q(!;8pcQur5F43&&AO&1#0Xv)DQ7}$q9Shhe6Lg?#*)=i?Xq zy>tIuVE0Oz;|F-;)46O;pr`vtov5e%C!PZ-iY>Gq4|`&DrbvOsa$tVnFycYxOnz(D z!2K-cx_b{r$M0}RvE&7NDr~P^XNI*&);KEdL-+Zd8=5$ixJzLOPHYPnrZHGuS{^UX zscjgf8ssfjcE)r-X~1iy1wjX`JZ99-_iUl`qeB*UxoK6N?k4@}wJn{`n@GXFYqUE7 z{`dSNtQ#g4H^?@2z5)as9ANb#nG!GZ5?3b+DFD(!b7qsx+n7OEK2ZRFJN_GLp znxUHk#Af%OekRTYVTTe^kWPp)K+VzOA``F3urP^b+u*2d>uMW6P&X8vV1MP)g%9^Q z{u99~EVDWevM@+HA9@#fGd9nwsbgcjW4dCPeQdsoGAQ%Dvbj97LC2}3WoydG@KU%td%KgbJmOA!2=4Z%! z_$-IwQF!;i`|*llcp~Nb$7lS7QmI%s60~R|V*`CKXmCk|$NGXbEEjI?>VD&vnK1G+ zQmI~TaqDyM>|KnndWEf$vrd4pop!~aL$I1-{G8{nAyjydeL^ZRVI-p>pJI^u69>YJ z{nT2XtjDP&BE%;a6b&aYzpTuxjPyxUkaNjvO)?gMZ0EyVtu6o>qrBN$hH-n%PVQ#4 z@yrAvB|vj)MQg~UeZ}ZA+g3xN`&k-LV6rVFz z^SFA~L?x~zZnn0Y<%2(d#7feLhfnbtDmU3cqWAZkr3cy2;pR!@m9cgQaICP2h2=?= zs+2tZyCEh%pw;bK1Ljp5E|ff_E)it@DG}ZFEG`ZhcNHv#$im!`G7m4O!=Pf;7Rrka z9G0CX%0@!FnL88D-&SrHY@=}Bb;oZ&m7#)b&&ylOtB#@%s+4~_04L;>b(Jd9PwDMJ z_uTP|Y(d5y?ymF!Q{^T5MA*fALwEJ#80-#XfJS)e=Q)roBCqk{(EF&eEiB+&=D$ZK z(12r|ky2J`TFFP+Wi^`*n>?u`_M`Xzhm`oQ;y647+#R*o_`v#>shR+m<~HD0re2T! E1Ls|;rT_o{ literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/LBR.png b/assets/images/fide-fed/4.0x/LBR.png new file mode 100644 index 0000000000000000000000000000000000000000..367d77c2d1e747166ece5402aaf91fb858173b2b GIT binary patch literal 1075 zcmeAS@N?(olHy`uVBq!ia0vp^2|(<@!3HEb(?2AGr~;43Vg?2#GZ1D}bzG(f6qGD+ zjVKAuPb(=;EJ|f4FE7{2%*!rLPAo{(%P&fw{mw>;fr0ser;B4q#hkY{V?BZsW!OGE z&j~)Qb6M%+PCFIDnSNYmr_L|pcK3A@ENtSL`qFi}+0IdJB!~}*7h@8=~5^;taISs zGsaYwAgNajS;tug9y4_mG9KB%kYuHLf6YaX6DKYt*VM#)ld=DH{PBv*D^-roTehO% z^z`7uzrR$cOt~}N_V^`5TfeeC$E4M6>yyc7}yd7w@uJ^>{mXSI31#+swq-od2AAJ!8g-x!)HrDlwQj>5tl`sNNT^ zS2t_CtK0VcW6q+ze=Uz4+xI)hezQ&I#%6ZWzXH`W;~C@Li|yLwR`5BE@zv|K9X(UF z1z*iF)zG!w|GHZ5y3Cw&?sf8Z3>imtbJnkCm$a^Vvx_A+r{cm53y#e@S(TONr~2>P z`0(J`X#a$bTMj&(HQCnR;`ejggrp=nOUv>H1tp28sypsqN@b6G8A#F_gni=HrWar?e_U+y{U^YeWcM{d78c!WcH61Rt^W!suH zDf6@QzDq4zCN^>U^hd>ecUeUjF?MhCwD`NjHLGWXawTVxg`Sf@%iQa=wTlg;W>1)Z zzirW?YP;99j*WtB&M_I2+cZ5{GfbGYwDlPn($d2dww5hevql(bCPP9&0aIosZ$?%X zSAY8DberGio~$uDi^M!V3tzmc;helu(%D)5&#^y0?<*=VmOOlT@`{xwKmP1a+&#I3 z|HK)S14)?&U%c4fker@24P+`X(yv@$@$uK!;*&h5B6{JPnaq_Xi!>H3f4+5%L)PLK z6H_!)b}u)aIKxA>nWvIb@OE5#*oAtl^H>a%;x9a~zqGl)BMOjm$tl@NFE5ik5>QOXwj_^?IN9u`4( zLI)cCu>0=0-3m@M67D_78kToSg literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/LCA.png b/assets/images/fide-fed/4.0x/LCA.png new file mode 100644 index 0000000000000000000000000000000000000000..827610f5a08ad6064ca6a8cc64791d9a76a43a94 GIT binary patch literal 2083 zcmV+;2;BFHP)jt7=l| z%hYMDw6eikwMt?GR!oBe^+d9jOt451kbNLXltn>EFz6suN!ElgH%f4V>)d^~@%6n< z96}u5bFS*26zQD%a?Z*9`<&;T=Q;P69hfQ=VQp3^${9q2ODi+;})Ud1o`C012uPhbalGL!Q02|$qZHFZV>QH1=EGxhk zPdshqUSLT9$_rtx?pMFw4Y^t3+LH>F6rjRm%;(x+U5S{sq>AMPD0INe31&XeT67q) zoB*5L@wtCrU!Qh-ZZ0e?5Y(s?u#^BcjK=rCfdia7cTUScZNXk(DFK!h!JHg5egFP_ zYHMq$sj1P(r@JT)%!DP+i$F25Y1ez+r<8DHz{G6h$%g zpb80tHBt$%su+sKV!C(k+@Z0tankGm^a^a-Jb9UZzn|gZVL)~4J>i=Cdb5#A0KM_` z`~8fJj3mAGg>CSg7vb0+;CqXbm)YOnZZEkMXjAtTc9{Ah)kgwYtTD}m1`@oUJ<I;l0kw|At&Jj)LI7uiyC9kIbvoda_h3QU_;b&#hkY+6uR|ZK+2m1O6fz)1GoC7j zg0Ww3Pfw2_z^><`*PwEB@^W5%0e0`4_*{K`y%x&M&r<7@0VxE~8{dNm4{BRX^!#!M z?D^?~%c|Ntu}I-?m?KAy#OsvA7z-C*ejY3u+f*BkMh#B17#mlB_l>EQH`y>%U0u!S z=qLc+$%jYtr`zh8L%0BXgV@y6L}zEG;r!fE_{$$5CwuB;mU-aQ51=3+8g}Ex4O&`S zw0xxm##pETISDUTBpTn`Z20S+VczVAFZ;=I`1l>LC9Hh){-JdxaoZybp#rQafxNK? zaQpUcnwy&q=c`_awacep{?2FNx4VtUPM$oe^)jd3==ug|IpK=VuLlp1Xz$4H-eE!gg_vmo%{aU$>;PBgUtqk-^1!<@nb%p zPjhq3$$_N;H}eV;K<|@jY;2^brw4$A^WgY<$yc@0Auk6`?1wpH!%bgbpTW^5a!M*h zfLyye{>O+J-@<%#_EMULMbe{lq47X`Hl_CuJ?TfN{kd~d4h*49=oU%m{eF#=JWd4&j&xZO}u6yAKvXe`skFMb5S{#m^47!{uQ z3m|4pUJSD>}Mkgl#STrM?AI{Gfm^9&kZ_7Gu=O%))~3t1PareQzK60m2d z#08(e41=!$@`{6q}+IjgW7bXoSrknDG@XoG(^Uv0^+OaJr z8)H)i(EoEipS{HPEoRm}F&(;-F-)x(1wy(WL z==1%6>YG|);+I!U6F~F(ggbC_?HcE$6pibvy)dZ0d;E-v#@G}A3LNUzK%#fy=1v^; z3l`+50UJ~5HKUSEZgAS7aO>wV^0^sRPLHp@g#YY-(IM0J52alI z?GrOyufo7T(p_EZgxjI>Mb?xk+Ah6xl6C>gi=b?%4*EV!cU3b-r~${9@0t>`8YuGf&UkY;^?F9;``gCv)0HDH*Ece|g&h=(=^2@X%n&KJPR0nR)MrcYNq4{ZBx48tnuC0D{M1 zgA`0w0H?01$QlS?0-BO|u}ti01OUn#c&uk|TKU3Xk(Qzy<9AD=vznha{qG~qboe|TfE?C8+JD*6H%gLp zb+S&!dtAPi1($II%@JQDEt2SJ>O|xX?(tqXVaINz5<1X_1)`CFJ(OCi&h=9!n}ZJi zm#jTryv^fGL=bftvD?<&3)l9$x*9VWj4|Rf!b6JZ-o1M-Q79+QokJED77l&W<+W{hxn6@spGPke@fS0jYw}&sikxCt=ny(-Yud>5+5r_a742H!Pm6YI)*D!K( z!%m;Z*NrenMKk3F4xyiwrlzKH^YXmCy>|eZ`VC5URMh^BY>#i(Tt;7CVVt501fjZm zdaUr(5v5?%p5>JlJA3;-4okw$@8Nz;Mw*$aa4XuoW)i!;CZ9`7GXM(8%kTB~69(v> znOrv$gRrN1nVFd-B_)_MXJBLTM}SH;+Yk^grBx1JI%=Ux2ua1n#Gp*^-Ff;4hWPxH z<<)ojh`^8#Rea&%>QdBOTGxY9K#SlWo9$!_hqKb-b8-;AzP>$!gProTM@$+3Hk+-k z3@TwVdE?{by;JafHODfDot+)d*EcHMz*VqmRo&dY3;hTz5{U{-HMkYpk(`M9zCJ&x zRC@4$<}cqsHfS``#YH|g7eXQ}P4_-8*53w@nei( zu&TB;181hzfyfSQSeTKqmRgqk7q&-TFbWET-Ctf#Oyfkv#>G`tSA&>LCP0;Z>GW>k zkI&SHqo&2w;o-n6`QH$0YwIDAY+fR{!;hhLkuh;`wjEh(Aez_prKPJjwzd-spIQ$Z zXs)iUSiE}mO1QE{K2$3FE^lgSxhWRM2a`xsirvqPL{sjZmNbtW1I>ciqT=FXKDEo! zryKjDYnqzu?&jsy1XQ>O5(tn|Kl6;0SxBgj-l0;zA_l{=!Xw8#kLP7{|Mu+c?EI$u zyE3)AySpdW@j-vIqNTkT8h{3x=u|QoP?W8$8(jwmBLMUK2GWQ3@4aGP{Hlnam>2Kw zXd^69sJl;|Jo!-?RBs9t6%=4%CCzqk3 zh_u$sb&@5QB+Mmb$NhFrnaAPihx5bv2hQ_+-uHc;_j!MRGmr>Zup(3u006KX-1)+8 zqIU(Cm)ms(+)BvzAj1900027kT{60k9KGFAE!M>^)+ZVp8y`%<0P*qhM);@*a!4={ zV-!uoiWLeq?aL#`7IY)(C9EGSOn)7Xe z`eO-C9~4Lvh#wRKkoAF*^|#~RYh3|F0_oOxG>$?se_?BgEbYZ;Hu;u=;GBP<41ltP zBz=KZqRPnTV|HNJ=Vj?Cv#_?(%!n@UuqUi|^I_1a#kDM1 zYn>zFm>Hc8eDSL@DM|r5Rt_6BCMjv%EL9c?G(6&2o3X!R+>8A1-UP|~obpCniC&o9 zgOy{3*uqg2E6*z7WILS&XZ9|^o^CzADd^r;d$H3DjeqBO>HWKx%6W-f*;CU8ubiWT zh1d2zE`xq~B)mK}DT;JB{v@Wq2Uaf8Xz^vvmYrY_XSaVvUmfE>f z{byh2YDP`}Y*7`rNbj5ZK-y2WF7Du-<3FNvHenrW3hmwLvn{GT;=Ed(XI* zACaEmENQ9j5)IJ%ZviQSyVNeF-%IQ&g2t9>D5ljz@Ym9-{ei6hET4BT@W`r z50R0>pwRxHL6zM;o9*Y=uXPQx)l%Gr4BBI_yhY%YXe)^q<{;=l?D8_xD*gAlev>X1 zu}pv5m<(m5>)zJ1weEU5yXZt%x*pK1(+`qde0xzX9rXt+}pcuBdAb5!q_X8uXYLKb-=xTtg9+niL8 z&vK-^Zt&{p5b`r9?wze9WfS`z(a<;`cG z1oP&zMF=u6NIgV0xN%!Vb=Hx)ZVRzMv>jfEAx0KRUpMC(k7en%fCHCie0Hkjy(C;g z>}ekI%ZPox@O0cJFY?t0Vc`zKLwx*x-K`sOdduM<%Z6@+Nl)^gmGraf03x4$soLA* z(-d+2t7Xup5Rc%aCM5!IFC_Hepo9ED~>owzQeq zC1@4ab0`qlpFfHa_T> zA)VX+Di$>g*!%Hdr@feu)X_I%W5JnU=&YBi^9t-ch>f?ZJ?&0RzqX0WAZXIgZR$t< zGi=RLehkfy&%_1&+#(hwcC#Ow&>Ax_v(v`h1alb>47EMvsI?NF=J&xtK9w+A>Z5HS zd(E2gb+m9s8Zx3@Q_ZPa(g1p9j!vyDn2Oyim)<8Ej_BoR94oS{8*9bGzzQpkBnZr^ zLiHg05=zebF~vX?H{1$9`*nL literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/LTU.png b/assets/images/fide-fed/4.0x/LTU.png new file mode 100644 index 0000000000000000000000000000000000000000..197142273be95d9e5cf9fb4a2fa861a05c157a53 GIT binary patch literal 331 zcmeAS@N?(olHy`uVBq!ia0vp^2|(<@!3HEb(?2AGr~;43Vg?2#GZ1D}bzG(f6qGD+ zjVKAuPb(=;EJ|f4FE7{2%*!rLPAo{(%P&fw{mw=TsOW~Li(^Q|oVQmt@*Z*!aCPj_ zQ&U{ZzG@k>kW_)GpWwxlQw$I8UHQN}f9vf(cg}mySS`0<7gGZ30j>s725p8ghBXWk zj2rL>nC;ulc4^1n`BjH6S=AF#jAoql`o9;Xr~) Q8R$m_Pgg&ebxsLQ0DEs^rvLx| literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/LUX.png b/assets/images/fide-fed/4.0x/LUX.png new file mode 100644 index 0000000000000000000000000000000000000000..8e407908fa0b3e70c469bd6b4f7177efac794903 GIT binary patch literal 316 zcmeAS@N?(olHy`uVBq!ia0vp^2|(<@!3HEb(?2AGr~;43Vg?2#GZ1D}bzG(f6qGD+ zjVKAuPb(=;EJ|f4FE7{2%*!rLPAo{(%P&fw{mw=TsOY4pi(^Q|oVT|ec^d+FSRL&S z@(69{HWXQ~@Wc#eYpz*2_tw6)U4Qe?n_mt4wHd+~)-XgcZeU7aJ;2o<%79+- zirSYLmkP}wVuaFI{c`_;@B87L_dM_Oo}bP+?{l8pLd|$M#W(=~0FQ;au{{gDSYZyl z#LEBi-+lBigqyn}0RVROe}hfdphAvCibk5aL^_1|MMil)_60;mMJWd&f{~BB!+n** z9{cBS>xi)$i!F=|9MJ{z{20e2k|W|`xq zzS{*%{0G|R_*;ZFa&v0EV7Wz`mH{nOJN7A`NYA4q6v|y2G@v1azvCUXx=VH$IGzV9NH?-t$1WfLTVd}70lOGE- zWNRu7EenzWd>iCNV{2M21;6ROb;g^x$cjGD*&Jr>D67qx@pf zRZoIV+2YvQTX5$}PKq0`mt^;<*#a2F_>^TheqegRHcncy3zbHWr)z5C`ix#J4zi?q zsWOZwXR*_@`Hk)shPFxu&xz0%Af>IqBBKu7B(ed|C`bvk^~Gp<{@VqS5Pp!Hlvi{~ zGK;4l0uNgz{?wc@gtzT~Ag|ssaAI3Fx^ipQza+chR8!-Xa8I6wQo?M7<_GHts*Lc1 z%_b@s%PtVOYRf@;BtM0D8BZbgNa?#er9fil?Qvm?-Nw>73ZB{P^seYV3$>#>0GfJi z@8J@43sBJ;-&$1jhZj=I^No0n9RIoaZu2S9*s9Kg{uo1M5KiI~EvsY_pbGW>Fqa!A zJi7L!85jpU`P@^wwzzBF`y)EliZzw^cLbfn$qPv@FiDlh+?{%Z1DTJxL-Gi}jg}5v z5CIcEzu7{YM%Va&j|3r=i{V&d!dElcsWbf-oc%ET>~4!|3>rRL>jFmUA?o%w&Fn{N z%v-{3QnJh*bNd8<(TNr3pQ~S!z5%NfS*tT(4voZQzo zxluTNxkQ*O(#)41Ryi=uLGumGzE+y$s36vnu7fTNW1CZM!|D?x{?yrddZooqfP3}4 zPcVlf=p8X1pdzZZxHypTApf=Q_0i`Kuv$|qw-NFTlQzpW;}|XkvpQx_p&a8IxFAqD z;qQ%+eE1A|)T24<8I<9HE2yxv1vc#OuYZ%Rd@cIMINF2Xrmp+@%?pzEkQ?Z9fQcK}e=H3VMml`!M4O>A;e+*hNQ3&atdo8J89A2w=Lo4VWw}3xn zcazpYgqX;SLROBE+uetkBt4%fc5FiTR{ZhrJ0^cqzJ@`E%AhDJLu`uRIH9d^qUbnU z$B+Z)P!;VnD25nSDpNyB2ayJpyz+6W7p!pwn<@>cc)cm2*m2615B18;y!`FiosDsr zo01>=ni^{tdDUj^b1=Q{m`rz&xPI@-2FhV}`qLlBVy$4^&kZpQta&j%j18}@H^@Wq zu^w)%XwJRla6@sLQkvU0Y3^;DlAPHrDk0goEipuvAO{44+K1&KQ6*ZjBlHdXrbv#d)2+aKiJ-!3~XuBy?vzYs!kgI1wE4K!k(_e zb~sl=re~*~ef0B)y%JQ;nf}HI3*$7Zb<7f^czROCKu%l1n(KtSzZp}tR>KgV8iUGb_7j@h^v5ctJ#{=Q0A$&a|uW38so~1VV;lcH&(8xO5TK2 zv{s*4+5>@o94am!<)cZi+*rhT$Wv@yIFE1GS&Bx6Gj!23V24I(rT35unC#YMWkkY> z@4aI=J>3XZ*3A<1Ykki0A55AAXmxG4x;^cXkNd90jorRqK^E@23~P}h&lF7Gmkm(l1AYn_DAo8`A1;Y96gK zW@XF^_YHlp7j^JWD}`3M3Sx&Oy381SOtJp;c{!l;hPCXT7C@H<^9&VdWpnrM=pj zzpJpp{2*Q@x|cJ*@5Ff{d8h79;+c{lDGkCNabaRZa_cB4Uqp0ljH~X9u~alr@Lr_F zzRh{bPDk>x;LcfAKRXJ}=VLWV;3v|lztkyKsLh;!CbOlh99P*Yb-vSSgN``0-R-LD z&XJ+TK&M2nXU>3EYSZfVoyqBnb3dLe2RI;_q!z3ePzQip;B!D`5#b?EQXMKzWtOCh~^+y2H6^E-aa)(JinW-PJreePq-r*zVf+7XO<#{Tg$EjQYbb6_3 z_>mfni%e05w?C%s{Y-1r3@NzdxfBeZK6IGnVJYr+ z67)6rIN~n;fPW^v!)3>-Hm?o0{q#3v2g=DLdi{SuPViF=v-c0#|IB*m02U@t<2pmH Gr2hhaVE2ar literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/MAD.png b/assets/images/fide-fed/4.0x/MAD.png new file mode 100644 index 0000000000000000000000000000000000000000..9aecc1a2bfe9266b72135c8acb8b99f4857a93aa GIT binary patch literal 324 zcmeAS@N?(olHy`uVBq!ia0vp^2|(<@!3HEb(?2AGr~;43Vg?2#GZ1D}bzG(f6qGD+ zjVKAuPb(=;EJ|f4FE7{2%*!rLPAo{(%P&fw{mw=TsOX}ni(^Q|oVS+`@*ZN~VR01x zc)FQsh4Rs$jC;#^9FvweZr<`Kxx9SJ-RgDUPkyUEu#o>pIqLzg22loWhA@UT3=xbQ vm=eeouzmXD+?kJ+Cxv&|QNc8bi{5anUEyr?m~*BL=s5;YS3j3^P6;fq}Wz)5S5QV$R#y@f|+#G6(FR z-?M)6uBXg6S~@D)+g^ID#yr)hM;A}|K^(yd$wJqy* z)|m6sT$S666(;aK2(P@HAaSDV>+{z6cmA|4-7HzK^=tHt>rr#(w(VwG_ndbh>-|S1 zl0}WPy)8c%93v(G;-lclX$Cxax z3L4xosGYwsC#Xno+t07tHh0_PZTE@q602#Nm*k;0XKO$@>*Qnh89nR;Ii3fmM&61s zpOW;=miy$goR5l2RW+m!E&Mlu+22`e@sixfyF0wMuYM!xBlV51O{8iy5dDeHzHKZ~ z_%m$k*~2HF?=*ZP$7OkC{<)*=Q_>EE#Fl|`sZp2ob z(FD^sQIZ!d1%+oWS+8vo+_dg$-Y)U1>Fa&-7yp|0#bx>VRL(9beM;uHP{I zm)id5KkL`%tG?f5f6QlAN)NY%V5;K#&6O69el$g6 S(>MprwG5uFelF{r5}E*j%fm5PAUuWPAyKr5R?(qqjS7ZXn!aqFlW4Uy8pjEpw6r9ZjZVYhoUa5|dcesuV0}j79@0#zvxukHR8?fIOGo1@`t2o7EMCyQ}32-p_yg zyXTzW@80j;-#Nc?k03BtWPlPx8++L%cNjgQk~@Hh9Yp|{Q3Q|~MF5%6$Q{7{6HRb1 ziHFPN4x>i~v;cxegDy5!?l5}fz6oIPHItEAK|*vRAqEo$Uo(v@TCUc6QhX+m&rgJK zwchi7xf?JD_gV)`9cyIqtW*48WHoA)<4~dq##(Q#H*0az*eE$?;GLWV4xR|Tr=X=0 z7UoN(aZR82aut`~fauT`HvRDnrj9i_*ezBy*?BR1x<7^^UkB6NrggX;8_`U7P#eZt zuWn;{dv?%ja&tALeu+y0Oh~Tg(>41E^fNoyx8}z3>V_1m>+YUv$}9B%c<4H@bf~e} z1pBq0Pr1l@yN5Wm_t16HZgK5@(pgu}`LuMG`t}u0-`Qhhs@S_B7py!5kD-u`5m-IhMSlH4N^_PHV=xRvCDI(B^?Mm$ z&7{uy5lyYyUcB5y?dwbAinpAKv9FO-2aJeo;8gZ!4z~SYg_Ab_aaSDj-qM3SpHW6- zjW=!W8WN%!yR{QV$U7QA+PF&U8r?~qoyqwMAE$f`m^vVH;^l6(od=>^awv*0XZZvk z2{17&#dz0tf^cJ1)Yh(H#-enmAAWy9k^m#(>$};Go(`7ez+Kx#Mrs8wW}YM1-$aW^ zi?u`Lu>S9fE7!cpdhRrSdJ7u0h|aBx+#?ZuuqU3%8gEHsJjh5AAjDwmW-B-9B{?u# z)$AyYCg)%jPo!2bf94scjx{1kd;Y{lHW3%u#FcBF{Ppic*_@NmW1-jIkR(8$uVck4 ziqO{X+?IjOCb0X^V0IrGOv>6uKD8kAu$t>TTOh>14pD%2!t<eT^gm?)1us7)+7`(7M@p@`smrX8L(X53R#w)^M@vM*12a+t6$OE7v@^ z-lD@#Z)NQ($5=e;6d&!4WAn}gY8yPHg#SPzNq}!I`19jQmmO?l5^Fhg(XZFO)(#ch z^JCbaA7i%(0(|&Jp(FtW0cI>p=kk?74t95Kmz2;ScqF~cdk#f*vt`~Uh%>BNc#LPJ zm(yZ$>-J#3LloiLOTL5{Ozhj3OMs8#JY=@2UH1PVA?Xb$E)Alx#+!)XR=aIlijj!m zR;QPOi=O^B^M6sw+K)!C=KUlJHthj!q?*t`6ZK8*czbl9({}NzNp#YL&>*)4{y7D%HNjnU-SY zdW(*kzn?N-zq8ayaDYXD%*@`(ibx8vh| za|_#SRS$@j&#gFB*%X%s^1|zr5O3MO^V3^6yk(cehZQNPRU)U)2k?(WgYCAGvj$4e z1pv_4qUG5olNmpv#;I}p8!nxScsjkD&95E8LwB>7vRKtD{mUpeWDl3LLK)=Qf!V5d zD5@6z@*6gOoaoTbU+;Xu_Z^|Z-ds35&!xV7h0DiMMi0Hl-$Y95<t;Gj)ybu7q<(`or<`dpZ}SQQLT7Mj3yYRYFu~OSkJ6 zjCv|-yfM~z;jZhVzDY;HS7GEFh^9;8Zs@%*;03sqkwb2LN*oq*ozTEG0(>kqx4BVW z=gINXAPR~??JJ$WqCW_5-;O4hoHI~z&TzkmduULOr_0QMJ{|2A7RD%}t@nm9qPzhz zqX-}~iU2aB2p}`Up76whb=L0|+ys$OO}pG-^oVdAVZaK|A5c<|%qRlLj3R){2#x#p zDRY2~2%;z|0?3Gxf@DS!KxPyHWJai1@Uk)o$cR!2%Z!qOWJVD{W)uNrMiaDPf!r}X z)F_p(%qS^HW)uNrMiD?}G)=j=a>wvcqg28&qog32Q3Q|~MF5%6=%SCvNuU zhP9vo7lrUUzQ0qLHJStf@ap~!9wpOCWo{7~Vc{0xO!SY4!iM7jQBhHvgwP<8FE$LP zNessqZW+n|0D>Vl=BCK#qRsp`il+sXnK;*9#F}l5+TRMrl+YGA;nTc-TgSA%ZxP)k+DylJUUDgh#rF{xH&JcR5#K}qV&=2rWs zz3(hfTMW~Lvx!7>E-lSYvk>sP?S8$vBCv_-7xI^xq^Ld>*akNBICzji65M&A%aXuL z2c>=RImVROH~vr&vbq%=A$!#%FETYY%zO9!Hxt(}n34YZNrPt&EcEcPm%i=IGOI0+ zsw?|%raEMO&80?pjg%QODUr9Mw}%YmTSqsYAo_;qDcW7#C6B6;u@rEJoWaQfVP*Z- z-EBEqtrT{HE#H&Oo%s3u2m+-H z6sh4cZb;*2C~j*;huA1ON@4v2TAI{WmM^uyRrNbRHv61jOzjEB+<%@9R^QoggDh2P zD@jCGIO;-d3z_2RSJtpv8q|MU6UT7$B#XgdZIfkos5CQGny%f`uB(P&$ zIl|A~P`nmPfZ;6znqyCYD(8F&rq$uq77*b z|L*>sdky=Qawpv`28-GWXsWd08JIw--1P$v zExns$cso-bP&rZ1>&ONE049Re!JFOEw zYs>LOg}VS}*y8J%vE`4mR=KFQmc6x2xi}-fu85Do>afKXYQ&3&7hyHW^dECOuHC{B z3K@eChOItuM6?6Nzo}KWnQ~G>Ity6rdfhz_h5pgY{C%BzH^@J(s{QzQ+pQBLx=UlO z8e1_wcLmNrmYC6zD)71<>tOX{v3NM{r9ykF+IunRu<;n|muXxncTDFxXf&l{bVY(q zM@0(m#35!6hHj!F%E86*73+)LrGnp5<$^kbG&{8_kw2C>zQ?9t3pDHNa4 zJBoS$a*2qi^Skv4qOh%R!h^xTptbQ7^B$4f`M zK4huQV}sLf&E)mUkRR6xznrB3hxJW&Jbd8OSHz)5pI-c7**!Z(Q|L`sl-y1QN`l6F z@o#f6Yv3E;!F9VBLubuupdZsUPW-s2dLd-XURd2gAdv#Ti4;L2Me;X5keL0yD??#i zx#1azK!xc%$kbplUox|RF%QqK5aMdJxe}&$Lcd)PM}gcN2GbiLxw>RmCM_$8j*<>a z<>%Uf{#ly($)4smV_$#2M;joKMt{Xys`x`oIaznfao(5d0TnN`TrW@D-&u{Uu2!gH zG&bnOKmW*;EKH)g%&6Qo=hQd+PlSg48)&Fq_-w%lnT%D7zbmTIw+=QA6+1A4f9e@8 z6r|O{ms&0%8qVRw_S;Y(YfT!FE!8jwI*np;lu?9x~G|ZpaMbBQA4Y@{InXcX?|k7T5|UZ6$AMs5fuZSPl2kM!qDx?Pj^a0^NN z0vt_h$ytcgXFDEjtSIl76K989l3`|juL-6NT|gJsXS%OL0VxFbX~JV-NVu=y^@B{Z z1#_4EtXz^!NHw@$v@VOd^6A}hGWS4k&h^Kfp1JNgl&S6-1v-KyJEVnkf*h_*hKB1h zGqQ1(e&|BUT3dI$-fH@?c=AFA!@GNZ0Bd7rrpW|2^SyR%(Vi5vN+(}Q%Qeah^Moxv zO{U0M?+w^QU4c!{%@ou|0h^Q&UYwu)83>;+A~hkbDI_u&ou}G#2GOL1fMwAvVy3=+ z`=!0a+l^k>s=s|4e6mj%r37qJM2J-Z4-v23K0LMkw|z!F8W+bbYV&MO zA$)!os*G63@c53Yv=+zvJz0}4-usKlj9vy-hVghI?EV% zYVk)74|HaEV8nO6jg-GYAR*N1y0zLX*qSxWO#>w8(6)K0^ZDV%K!VBj1eniU$bpIT z9l+2-UPEW$9eY(KyyE{5N}+yK=C*Epsj5{H-j=6C6&PJ9F`w`Bf5?ilPOxo)OI`0p Q?so#PvAAqrf5|)LKhf16pa1{> literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/MDA.png b/assets/images/fide-fed/4.0x/MDA.png new file mode 100644 index 0000000000000000000000000000000000000000..b14b9af682f00fa7e8d238d50d385358386490ce GIT binary patch literal 4177 zcmV-X5U%fuP)c zK~#90?VNdVT-BAvf3IKfn_BADzSvqUS;qT{6Wc(*KvIJT90&nwiebqx1ctysGN}|< zYN}FGsT{rbH(e>~Zeuq2BZ z>4mbss;*MszUOY=z4zR6?zs|J^0bOh5Cj$FrSIrfJpiXJ_vl)`LD$-6FhhXem?1!C z%n+b6W(d$3GX&_283J_13;{Z0h5(&0Lx9ehAwXw*DF|o}K})~ht(Y1Z^=`mKh$dk$ z1xXcbxzITPW&;%D4Cl#6U^eO3#`A^l1iZAx$&pWUxc-|x%&V10{T7ac;02EbYz~MR zz~3(L+N*g)269NJOc*d=G^KHSBV4y3NPgbAox8FOzj)G%#VoQ~mGBC>IfM}@-taTM zDxYuK&T^lHLRTIK8jTc}NAS3Z2P4N0ZUI6eNE`&ys}O$OgzT)Pp!PCy%m&pk1gHvCp72@8f&lUUGP(kz2U4g6^1-(eYW@>i3zB0NYj-6k ztai^{FJ9-_t0doN=a@|&b*Kvg=4==9iemo3q?Kz=nhOHm?@j2amc)&7RzyJmZ zm`gx(z=8!KQqpLJ-5-r|`Q?EL>x~UMX>1Miep3(tSy9=xuZzCHG#Quqi|gR$fIbAM zgS2!;DD&Bgr?MPA6{a_wVpgG(ruIRk?C2GcZvpoR-LURri9j?sDv!m}#r(SLg!TIS zGPvv}Ts9*IPlnllG(>s6op3BeZ&sdAcB-HY0R|*i)a4->1geUiWM!4P^IcV;v^;vwdJA&Rt}0c}B`E&tO< zWrk;;3Mk6C`vH^@b*3d5MIHVwtNLhReFzXl4jvDa$tpCpM;MG} zID9IMqN+5W3S*qaWK5@rGg-|d!O*#df{`q}gK5IC48cf-{YOJcvO-H|goJbf5uhTl zv^I~nK#T&9h4Op{vkDy??Tm8K>>NJ)#7xU^aa{RVx7qR9hHU`RAaJD7hcwP2h$mpn zb{lz43(0hrNL*r8p@S83y`1cdQIKOL&oX{}Qwd!*K2;)oe5LQ&1S8zCOy*1v0V>FTL(0)KN*UeRR_ct+ROisVv{RzK^mZg-{sY zddE(0x0~pIhZ0X7Qnr^_1r8z!iNUyp0MwPc$#t3O)ng#g(HEpBD$N}cygAyDGZe{i zqJzf|UA)lxhdaA2?`30##t0C?W_QQB{$z7)?M5=z+c-vSoIT zbeoALB)o15vZB(|9wFaj8MnNthCT$S3LhUEptj6KGL_|IcMPK_P+yTlOGkt&x+RJl zMjKTsuw^k^dKhwsbau=Eq4`kp!EnAmFND=?;2k`>TIo9m@?2)DCIiR(QR1mAb>%Li zi7d&Kbhf}WKqn1pFJ^DyE7_Sb|>=riTDS7OZio zeh*gt9?%ADG7t@{R8(CN)Da-rptlCf4`{1F7D2MW@wGslCUXSKITm2j1SuyJ2B7dk_~are-3s}$#+PZh z2i85Qbu63%<`me|=WcGCakjlK?A$rtiX5kAQg8E z5*aYi_+&2b|Mig-V=#_>HCnV~8T_#gB|Lpc(y1jtHo~b@+9$6a%-vA%37Jb?quDIc zSCZzMROwk_TWU7Z)ia074*n9KznvW&cjK?Wf!h|<0}~TaT&3{v^8di;x3d1jCx}IS z>>RwG=(1I;v0Z{mN{+HYP9qsM(EqTF*lW{!GdA_E2vZvB8o(9-dkDf-RBMtra7}k9 z^%xU;PRFfj?s~9%kvXekv+7*>qZ9_uB=9rHLYf)(jt-gM5@98dtQS_)XSa`Tzi} z$M449mF4fR?Vw?2=^0z%iN}|*W^FG^mbUQr&YMxy!g0$R3zbXyXxVQ2tGaf+m>zFv zPaVAeGjIpM9Y73)*OHd6P4wra@g&VG z%y=kGj9&iL726QSEUhOOa_x1Ev@}oRh#bV0u@8B7!9jf{gWlYzseo8Jz4A$@u(41(J3;Q=_VlNgRKu^ z$$x{5t3SjMG_kj^k3zvphcQlzCyG-t^34*7_Rj5GQGW-)>>7@BZJ@nl830~y2W!_I z=YtP4Z>Yflt1fONHy5&!z|L)O^vL&dIl|1Zd!CBYXSsg8c9CGQIADZTJbOtWZ}0L@ zWMx7R=~QC+uOt-_V@D-dF6d;3tp`a^==3It+Oo8nVq}vkep;vS$vzY2Y#KMaxVU5& zRmCNkoa=b|ouBZ?rZDO3vn*JkV7J4eLog7mVQ<5qsH!|fRnZRet$WD~BslSLh}1f; zTEOK=k{A)a=*&wI>~T;ud)i0g40H+yQ+UPQK|oA#+#AI!SU6^hf(33@%@_=zz9*qB z*Y<0_Kgx>Bwy=8ni~QZ!9^+42ZpGjAM_zjVFWBs1ni{`?lqlog+y0G%A05CLokM2m zcBaIW&z^m4tY0g!c+nvL_gWg0Xrz2jf?&tF!`Y`Ay4;8fDqI;arq`Y#BxY#$XeXNT zu!Xz67Nu)9Tz4o%-_B#SA2`LuS2VMFxwdx{I8Go1e#6?DUlZwE%*XFNM0M^lE^<9i z_o;Wd{iY!1{7>oX)Rc58DUp0r+xQpFlTmE#s4C1~IY`H+-Vu$O-Z3~0-NI6`f{HI? zq1TxvVahUpz{wMf(-e3Wx__X(jQJ;jFX4xvXvwf#!(IzsTR!Pfl6>JsN(!GRGav#6 z+FElEgd{g@jB=vBiT8hGqh?TMrJSYb>rk`;9=JQq=HGUZl?7yJc>d}2NJ*#n0V;Wl znXB7McqX`t@9r<-hKeMW)e`%32jo1jr4P6v_s-$`83{K3PXWbF&3k&JV_2I;k|`pI3@)pMrUO-cd{8^G zmR}B?S8I8STcPJkKm;)l?z=V0fY3x%p1%9vv7p~OVHH~XY;4$gikeE9Q#Zo5ehF=H zh^i2|5ps&OtkF0;zPW%B+Z+TSCCQkq!%n<1ubWTWTLH*-=CbF_YTo}yvv>>dgFu;< z&$9(u9)T&8SoM+0jTa5U;61`E)saNRovj3gA{NZH>b~mK|8T_*LJhkox z@cs(~I<-}JcT%Qp&-kH^Qw@CxKxJ!7F;Rndx9D=i;CCTD)S40z;>WbVRTTDD1~O!o6dy1xeIi+;=a7+=sMRHm6Yo><%SChwwyQ2#A^L zhr$y_Bfk!)gZgghOhNSsAD!8xeNMH5-7`QaVkgg`Z9|f*(3k9_sy2n9Flz7tDpx~v z8zfRtRWNp&{=sx1xT8-)INjN?Rp093F_Qt!m|8TipMyujyt!jGE>kX|x0(CzjAEEz zLv^`fjQjsHFkNUb<+`<%i^7+lfG;~{2+$cb1n7(z0(8a<0Xk!b0G%;IfX1rX b=#2jd3SiZD>8&;200000NkvXXu0mjfk5bp~ literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/MDV.png b/assets/images/fide-fed/4.0x/MDV.png new file mode 100644 index 0000000000000000000000000000000000000000..8b550c99295d994f2aa0b0e797d96c9d04c5fc4d GIT binary patch literal 938 zcmeAS@N?(olHy`uVBq!ia0vp^2|(<@!3HEb(?2AGr~;43Vg?2#GZ1D}bzG(f6qGD+ zjVKAuPb(=;EJ|f4FE7{2%*!rLPAo{(%P&fw{mw>;fq~iF)5S5QV$Rz;@gBj6630K@ zKex>7pJ%RH7W409-QE=|W=vY7;NqrragiR6KGWiE)>w&Hk<10LtdbQ=&0Qng)TT;g z2p;y5aEZKlAw;V^^Pi`o@AkQ+D@RQ}*nIL6pf$dDw{Ai=Jn_C~M6 zX~xBtIaB6pY)U(v=INd^Me_2!`8z(ZlhwVef5H6S!4qFt{xFJOczM}Bh|NHP$70g7 z?o)e$a=PN#89w;b9bTGVx}y54KwYTnyn>HQxCMj-JFBWKKLs0a*_yUp_-L=_j9If4 z=j3EA;o4x9Wjb$4h_bGtYxM4GHybwhuXpA>@p_^^ z_O+(<1zx4Af2!}|-=LnUEu@s(d@ZWls&kp3|93{S zW9sc|YGtGqW~5ZUuyaV-Vwv*x^%aeSDyN;lGcqyG{nw$~@s62C??a}4)Pq$MroLnT zqPO9Geh2s8I1p!t&ASP<{~tW_S(JI?axH`Df=7+Jl{3~}n8mty(eI1mu3h1UOCPJH z)yBQOu_=q!tL%xepGuzyLT2!?8?AkaXpOr`SR(ikr8I^zWLVoy|8} z_IrQ0=Wba~Q3eLJ^!f)E)*6Z2(Q`ikLY%?j;I!How#EEFHEKsaY#!b>?D$n~D#HJK zmWXmuP}xGDV`r<%dR|OB^Xpl_|82aww)z*XUmZMg=6mYTmr+*^?+p4ISZ6o?m3#Gk z?zgpiuL7s}?9zP^e;7Iam`%~;jb|i2JpvPK9pgmVlk=vuO!xuJXAGXMelF{r5}E+E CgN}Lt literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/MEX.png b/assets/images/fide-fed/4.0x/MEX.png new file mode 100644 index 0000000000000000000000000000000000000000..c8b987c9c7ebe3e32c8e4c1687877a3398425fdf GIT binary patch literal 1827 zcmbtV`!^Gc1D{7;^Sbhmi)0>`Ez}HoPnaT09+5oqYHHqQ@@C0M;zn$Sv1T4cqcY{z z4f7~muP+)UY~GKn8X4C;=ldVrb3TuAK0kbZ`<&0cOD+x)V#;Cw06@aY5so}^|06nz z2py?t=w{Hr7U}360|4+t|0SQ64Fz-*D#hA+#a>2-#3lqp2Llok62RfNQ87UQk-=be zbZGhdIb{Gq=$RAT#vMagn_hAC`Jwd3*3(-NdHHX<6~%>uY(+~1m31_3=#noMfe;J@ zav`Mw-kv78w zSXw@W+wNHe(sFKD?HUa098`<^_&2vcW(!ZAD;GwJ6l0f)nIp#jjkw~MBx{4!0U31x z5p4kx(0{`t4Q!gZ9A0oKWi3=^roOWGKJ!L)U&7j z75WDFy>-v3A|MNDolf>60%z0?l(Xfg?F+CzPmf2(Rt|VnMDWZ0v+M%=d;+FU(W{^C>HkvrpYaY|Rb6YLl zRLwb)n3umhw2YMHyrsH*y)HFk4twi7sG`NWE@!c4a4Q#(m{^O!S-vYZ#ZSstHYP)M z+kC~AuZnaTyP;xlP_0QpX^vU$Fy{NLa^qBPHfI)I+Gu zi2!iIj$kn+4C{ygcvj&2Ta=2tMf_!u#96KRNdv`;LIb9cJ9rE>{AtwXa8+yK zm2|7(z#H?&aS#%vZub`iAz#{8uq*R}^m{_1L-6S@r*9602XYtX0;>SMm^=(Fw-6{$ zJ@K-RE}1rd!IrIlEG+YcB2uF|y`Hn?^LS>+&`5ziKzR~}0z0>1*Uh8F9pZp)*E9@? z#8wP=7-PF5Z}NLQpUgej>*Ni7X{o{L{xdR?jdKk%*$j4p{`bS2c9!`TcfWL0H-6kq z(a(8^7p^UrBpLmgGcvl`PC6HQ>?4C__`h5}5qo}}T-W)VCdKi5miyEAfdO$o-9S>s zNwlT)d0e_gYgw-H@i&Q@RkeKU=q8P_@Ws`!xN|dS5g&w7_SPKkWlflFvqBB!WrK91 z1N5T#HphF}yPG>e+XO}-{ikMI%>}n*Y8Jdy+yS_5C{?(8avw98c^t9eftai2l?)sydD!9 zJte#6jk$MclZ1KHN1l?Yg}C{YxgZ`#ZBMN1-LE%eA}}3(F8iCXS6%X>O3M7Gn4Lr3 zDZ5Ms%$_!+axC~^j{dWib$PwMGWgR3A|3c3;I{SS%?G6&wqxV0bK^tIdQx(C|7$jr z3cGltW1o-R$9=&d{_~gT_1L6yXJ^!p56J6bOV3 z{}h$eM?0FvZoywrKpOcaj@B1~!!dMnDxOSTy>DUW;#o*^-D?Q5O(D<({j~6^U}G>| zITLz!Z1Oh$!LdF<+$ee$g3T{SO#2LV;`La0Sc;P3dQfoOqwN;PB=R}Q8D+Q{C9&J( zQQRxpj-9EY(;o@@NEM-V&j;zeT4-J{;6ORsmJZn|`Qmnb s%va?X3+AB?&oA$AkO|1>{mKpxkO|{<}z}f7-OWljlv0a+=d*9h}BTi zWE0D6l95|P>*Dq_*O*EU&vQPW=Q$tV_kI5VPw$8K`Q5x=dtO=+A_)Khq%AEl4qJ)e zlEtnaTfW>={M?kjZ3i|B%3NZ_iMI zHZjbPE;NJy0H6j-%o)e1!f%B(L2^z9IXBRChsWo~9Y#a2{_zePK-2}8+H3IM0NsSl zysIjxE^eBlT}LP853^E5Bk+9~Q18lY%UPLoc9MOiNsho42iNH}GC9)E#E)!PEteB_ zqRw51`7^HK8{HuP#)rrAJu#bWI{fuRF;YP)2v*dyWgC{V3} zAO5r%wPzPl|A@qJ`aV4RzYku4#ED;&2ZD#LgGZsyv90~t3`e9>XMqG5bhGDihORH9HR+XgKIpuD# zqui3^mzPD)R291o>HMVC*Nu_2MX^>s2G#h+r;uX0mj5r-00ymlsHn=~!n-5_u;sk- z6%vRHAC$A!^KzvMdc^V5{96&iE8(=TK&eNR<60c0!7O*s*O*q#!TdRCmChZ)b1)9F z95jvE+4I<1XE{8t(ZdYOc;p;wy*kh*5=c?3c{H9EMWHNEviPem{dNXgG^3^vCnGy@ zSbXLHA>Wr&#tx+Z#V5V7um7$(xETCqhSi{kDq$Ur&3AW z#i#HrAMxOh)>xti@>Lh!1=t*HnZT;gu1&@Q)0;3p>=(G4GH|owX<~^ zUhwg!oI*{i#+k4Dxzpv6X$iWs+!=1wRg1RuJ(grSw!)8$y<1Rj-5Fu@6)&1oA1z7Y zylKn+38%~^Q3+3sCEUMX`;Ix?ceWqnQE<*d%J)r*lJNm^ySqhhHLhI&OtVYA^{n{D zG?*N@7{eCzYAa&JARc)w8=SY|Pd?5hb0;Xgcnun)A1Ax-Lxt>cWsNJ`NT?u MN% zl8xTrQ3&U^s!IiC#+8%R@9f98VN8U#G*(P7bn9ockcRwioAILJYhOroJ{w$C!s@VP zai|>ItQ`HcFS$<|w9MN(VTEUkFzj5@lym)k2w`DHHlz}^*s$i=))>O9Cq)eTjVy*R z7lAb6K;OwvJsk|O?EaEZZP4Orer`RUV7&%7o4Y`?6YLs4y?=alP(8UGV|*kLc&ozn zAy$4Jx+hNvTrV?QVDg#cN&}s6d10Y{>EgPVhDwt`^DHC3L=HO-xG6|+BH4I~+~kOX zklq~Kn)Y_9UfSsD#$TI22_`R$d zlW{A`s|++!|4WZo^r4dRrPB7Iht9u(3{TvCtkqc*Zojq5N>l@QQ_IVeO2kTXH|N(& z>Uje)Pt}|E-RkGK&|6*T_cQmSp_(|^w_!nJjyfUJ%jJ3?CmS3OwBAwqXrlC3s*RE+ zE}*8-s%ONRKecJh(DB?{+;; zcC2+NiW!Qa{+Nm^O)2*`G|Z?5^W1i!mk28+DexADoM5IX;@FW1-JH9s#3AJGW)|Eq z2A1x25qfeOQAy7yX*HUlp!|VuW;Gy~ljd96!v7>Bbl25sUo##!Dp05YFZTWg;xF;^ Vw*HgCpt)_%11!yLF$^<&!av~z=&Aq! literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/MKD.png b/assets/images/fide-fed/4.0x/MKD.png new file mode 100644 index 0000000000000000000000000000000000000000..40ca5211ca2e72353425754f2bd6e164aedb6e56 GIT binary patch literal 2782 zcmV<43L*80P)mO$>Gjr$OJLlZF zcXn#{B%7VNb6$IX=YG#Qzw^7ZTHiYKrn@0fffCb);OJV2kErEy%ZdX~(kPcUHVlVX z``aFaU^Oh+1LYT}&F@?G+IoD?R@0J$>QQMCp-ehylhd>$2M`XD}fICU5dM~s+AR6iiGSU(`2{c z5_f>-OHd7(?t$8m$tBMvIcB80NL7+**FwwpRcgqMlmvOX0@9%QZm7RHcT!o&F{9i) zDw0%p6*PYr&{S)&CVCVS6ZW4cCLwyvo4U}T>0400L84tQ_fg<-e5zDfe>E(=(@iZF zBp~vt{l}44L>ezxv<(_QkvpBL&&p59G2^=d!1)g{99`8PlrJx0(tCXtq89Ul9rNA?ZTDMu zzTc8#rXU6YXGt;+Ip$p=SC$^lT=7KzE6SIl>OINBBh$G>p(Mvl!39rClZGqB-;(hES#QMc#Y$kjjEZqme+SIjZ zH}rnGa1*T)J{%mXV`y-?Bn2v=d{w3aD&7ta8^vUu-k~54?9culPX8?P&M99l0;mk; zT5`-3#T}52AxT5q;q-2CEm#B1_dv}@$bGn@!`RbeMH3$vm*(F9joXSta?EIlThV6* z@a!PN3rZScc38YF>k!d*p`~hNM30Ece5OadFKrYtqeVji%9=sb2hsHy#0G_@ar!mU z=$RhzdUg&Q9he7QPeW*#yZq53LfVWwVJ-;PKv{e8rwzi(#orU^5(^JaA9R+W_=q@* zjdqLQGe?u}k6OEhy`=5Ir0nFu=#$WQlQ-?LLko#JG_(}TlD|V;;w#}Q%08Y#f zXWlW!9Te*aJ?^$tLB(q5e1`lF^mRCL1x&q^XE}(`Y>eRa5na|PTq1 zn#}87NLs@tyosjJL}Y`k0dh%(*W`5vxFJ54GRRDC(n$Xi5@#$8wGvqIvUkrO#7E%J z+fw@ktrSX|AiOMXG$%W}tlgVBWhwYRAXpaM_dyhOW1r(ul%7`#IyNfz6YWKo<6TOGiB3%@9lUqFe?o!$pJAcdp5wt3%Sc^-IdxYkL(pOr@5%S7&;!$-qFrwoKV4?adqpU z>)Gti*JIH8DY?!E2(L`NE?;S%E}hB0L(it{4P>YbI`_}bDyd)w2!zE$+buh@N1z^q z-p#`QBU@IeEA>9KOnirr3=@A&8ezt|eXYCv@%h+M5Cb@kFg*_ax4`)C}z3*Yfg;+7W!_sHbNmK z$u|QyNe%!y4Bif-zg90hY@zwG^FTUd&%nS|EIk2*k|bXYkaGm-$zASa?#j2i1FWve zFtP{ww_>{%^jnfVH-MAm7z}+IPCx9fe67^lI~rc${+qf+o`Aux*?;b*Bzb0loRX7d zXgi#`&wV|iW!7H~sFgwJE$+)by&Fz^-TrgGB*_y4$c~WV@4=}bq0Ixp)Cx4J{ZT=bn;N-nWiT9uV^7JYR~fLj22hn8fQdgr?~U%ixylhuQi4|DO9ZqYhrZ9j+2`ca z%Sw`^0o+U~6VJo(joE&7uPAGk3)T(RKv|1FZO~)Ta|4|HgM6M`l9XitRmlOE`iJ#N zlb5J$vQ%BuDj0`HlP}8WaT?HX7{HC>fC&3Ic8v%l@e`HF)zg)b1&Ih8-vE<;lh0!( zNda)bj-CFuh+uH~K4-5`xdXg#N#r6Cf#cU_27$?7mZX3i$${j*vAR+`a8U?UMztR+ kAuEjdCDkvCO&{?81K&k`O22whL;wH)07*qoM6N<$f?mNo_W%F@ literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/MLI.png b/assets/images/fide-fed/4.0x/MLI.png new file mode 100644 index 0000000000000000000000000000000000000000..506c53f263e8b37448183d8805534e574ccf701d GIT binary patch literal 320 zcmeAS@N?(olHy`uVBq!ia0vp^2|(<@!3HEb(?2AGr~;43Vg?2#GZ1D}bzG(f6qGD+ zjVKAuPb(=;EJ|f4FE7{2%*!rLPAo{(%P&fw{mw=TsOYSxi(^Q|oVOP@@*ZMfV0LtN zY(CQUW~y%B6e+H@9nbO^qkrA*t8L#Sx7+T$?bUz(k8Hbq_Ws?HZA=BM2e=wU8MGO~ l7}hXEFm7N<7;u3%?15`o`%b;f-3Ig+gQu&X%Q~loCID(YUF84( literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/MLT.png b/assets/images/fide-fed/4.0x/MLT.png new file mode 100644 index 0000000000000000000000000000000000000000..59bab487a324a78775f3c68aed374d4f192385b8 GIT binary patch literal 1295 zcmeAS@N?(olHy`uVBq!ia0vp^2|(<@!3HEb(?2AGr~;43Vg?2#GZ1D}bzG(f6qGD+ zjVKAuPb(=;EJ|f4FE7{2%*!rLPAo{(%P&fw{mw>;fq`X?r;B4q#hkad&gb6_VL0&d zeRbWo%O;VNw?$g#UOu&A!EOaZlLq1nv+6`Vs zpZr7nJT6NYT)4^5hKyD!g-`ol^<`bGwEe?3I!`Yz@19e(NK{H;U+wJI-|f@(e*e=U z$eEsBbN9;u_@t`&49>8jpfwv9B9|Zj8@)eZt*vQM zPf*vxhtoO3!n1)@nz3W=KBx-HY>Z=8@;>Qa!wnnui zcD$}y6ZHJs^XcDh|1OcPR-ekcMncJCTt{?qu)1;7)mGx?2VWG_A=ayfqWS5Hcx;1~zm@R+kzT5fvUn{>K*XPe; zXh^U)9+LW{Q*^=2I|&vti>4@bDLs4ktU#tcZ@c%r=N0pgCm1+*d2YXbv{1&(+`LdG z{mri3clBne|GpWxaQec^d5fnky8Lp&GEFZ{F)^_jbLX-;a5&CdHsy5cCma2uSDm3Z zE?=nH>$m>;;qSH6rcLWu#Bsck;lc?_1vXQ&FteF;RFdZnA@meZmY-}8|I&{gD zDPF2sTctKsi+E^G{JrN-O^`?#OUBwl8Fo$8@^^O>d7L-tIA{AVzIcOs$yQ)EeX_Z4 z?Zmg~5Yv&up68Wce%CHrzFg3$;LD4^B~v&Q<$g(eT^2EXXdd8NnZpqN<>i+5y)o}B z`26+fFS^g~d3nmg1c%*s)6&w~dV6^{>D>MuIVot-6sPR7j~*$#{Z{)fH@7dQT!uek z(|^mK5y{LayjqP98tLz{@0XcuaJGD{GK-_bB$W^?(U|M1987XHPdzSA(K%gqT%9v> z)~0RSj_pi6I{WtZ<9TzV_I$9nyY}S9|IG?(f4;fwox{)4&L`{S)p<>7t?9n^#`gB- z@9r)yjGb=LC+Kt`YwL=z)qAv@!tU+fvHs=T%?gSZ)sG}aoOj*T1`GNeukB@ciz6-70?>1lmk%imZ*oPl}mUG?w>CaSco-%_s z0U1R+{NK0k*X*1x|0gTe~DWM4fZX#Gx literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/MNC.png b/assets/images/fide-fed/4.0x/MNC.png new file mode 100644 index 0000000000000000000000000000000000000000..0d65a7eda014794f1456a2f437df22dc7902aaa0 GIT binary patch literal 309 zcmeAS@N?(olHy`uVBq!ia0vp^2|(<@!3HEb(?2AGr~;43Vg?2#GZ1D}bzG(f6qGD+ zjVKAuPb(=;EJ|f4FE7{2%*!rLPAo{(%P&fw{mw=TsOXTVi(^Q|oVT|fc@G%yupC^b z!KM5sleK|4S?;c5LT0J|`p<7G5AFHYuwR=YjA0E!1mgy#1l9vw4WbNW3gn#iu}=!% ero;#oXY6k0U_1IPP3Z^FHw>PxelF{r5}E)~Dpdvm literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/MNE.png b/assets/images/fide-fed/4.0x/MNE.png new file mode 100644 index 0000000000000000000000000000000000000000..cfbbfe52f00c1b40e1768e0991bd73b3cda4d310 GIT binary patch literal 4220 zcmV-?5QFcDP)bLIOn${*;9z z6rV^{7AF;_vhgLAI25EvB{m^Y5s(y%L%<7*ZICdwB+Hg8jb=16(kwmQ{kr=t_uhPH zSql=S1fzaZX?|5xHShK7`|dmcch9-+ocm_PkG^obWtGg3S$E({XH|fVan9T@Gi}nn z^{o1GMc`~HBFBT%+h=~5?yb{$c&h82x{}B}Fo}1D0IM!2V^#&om{kEXW>tWUSrs5- zRt3nIRRJ>Q@)kge&d+7cWi5dG;2P9#ugkg{uL7#|rc3R!pLHE{WkfQf*) zLO;{8j6LJg{&M588UfZ3i2KHnoAZdA%Vt>z3*sZ|(P0WNh1ky?gY@z#aa=YdzzT3b zz7a7V;12jC4<7+*!B~=ioWiU4FdX3g!6xiw6qhdv0qnZ`g^#(pjBo|g73h{m9t=^g zM0pBvph4QyNLL^|fqg^o1=}<0FOA5Nxd7Ie_#a+#;R9<4=}JoaqZ)BtV>c9NzuY1^ z6k<2^kTg=#AJ$1%QbaYrU~|F!6Ieg<%g;Clymb(<7~@tvE@U!!uu1al3Tj6`!jq^6 zCb3#zvB>+^pks@DLy6?6Wz^yJ1yAh``9xcaQ1RgFCw?~9^HY%7Eno$>ZyP{N2FSd+ zu-{HC&Nmj|NQ?Z4hjb-wQQ#IN(v{>#Jj7HR_05F~24O2e78G(azgz(j^OH8i|#v*p~!@nOpb%=cD z2>#9rY1@z{mf*$`(wn4pu!(r^B}_q~a|!^_(x_15e0(GReZ!d85LHv04SBo|t)cam z1?(5+PM?2!A6T$G3h4_>uZP&;BhDw*MJ5v$ByTR`ys?k8p;2Q#X=up3tpWn{cmXpU zpgl$Sl|`g0=o$BM^0G7SoNj|F5urvd#K?sh5o&NHW+nn_QF%pS+(!k1##a_Gp2Uvi zFyna;kb7%|BusJE1n5xXys-~;(}ibF7sVD?oKz$R_vT*G#RPvcAX-lFD-Pn;K4hvf z8;8&{k|g$Md@Urc8>BB0uB3QZ1#^AzoHor3C3=2-0O3fKFG%Z}##cj<#6{04^i_ip zYs77R1QmyPImMd{NETw;n|mS2=HadD0@y7@^x-D{+JIyx!WfI!@1olV|J@^q`8Mj= zY19vA@xMNWH*9GiZ4xb|wC7@sw)B4Fb(q`wPqk@o>! zG&OeAM{X_RkNTu7jr0WVWsMy4Na`AAO9`{LhTYd)dvsj@yQfZY|0J!w4g8G-l2Bt3 zgHupM%Nlj}5Z+ybL?7D+XLtqswG*Tp3z&+FZfc6}8lsVt2%zV#3Q0}l6a>ls#wp{U zLJES9u16z@S2SK(k%k(_k*E=u_{EUmwq6?lI!(7{q}`7J*A{6#S0}eAPc#=3^gAd| z5Z6<5TT_?_$gRsE-am29SCgl!#I=;>@s#jrL~=MJKGLLdG@^MTC90+*&z?Jzksnw? zZf%ajMCa~LY8vTDyaAW?Y@6Wf0`k9U~MJ(Q=CPB{qRz$U~g$ zQR(g>xc#)BM+wAmfUX<-lA^diz`L`8cjo}b^#OiSk=8Y0#6PP_Chn+EddEQLcL}*+ z50e_CFNl^BWWga_Ns+EVZ9O-*Uu?QAK=S2zf@_Nq8It)3zuzTUPLVl*bl_DVAQa?3 zIDYCWRsGgF^7jrR1F+iCI-X!EIm|$w*6{?ZEh-1OdxlO|$pqv+Fa}bb+Lj7bponW4 zzt15#o?x}XzotO)pL6H*_hQra7~tGDM)<@s-nBjCugcM$O~?&8w3ZUQ;@RDams5lg zi1&_>Z0zG>-+nVW5mQzzsyfFZJ%YfpPR)1rrFj|z{OPv;#(Uoc_K_LlULIWkC2q|hVqZGJccP8#)8ib~1N_yqx8dDV z0&9u><}kJW%~S1~wn0jezQixPq$?>-L6N()jDGakd3t3%Ar-=p=tDx%^MY%0)R zNbm)?B?qLyYKu?;bD)8mZUGSQsZ;SA{Oa%?cGt$)-glVw8&jgignR6M!ucHAdk(U- zJkMjtH?a^GhkLUYOB zlpLC=!(_086@n*LHgF&sVr_8+Gv7x1{s!_7Uq|1)6-;6=+7cd%k)8l;v7siOZj-;Z zh_|H(`x{+1M|Ven6u2MVKy)A^I@H2j8{ickl7_+QaVU&?w2nmtBOX|R*YDu%DAQb* zCpzTv(1Ew`C)>Wk+lOAlQQ+njv#k;@H7o2hgLp>syPF^9@#C+(JvSWX7rYsy15+8=<9-$C z2(VzZC2nX;TX%#t(42{pzCcM(!yfLSNBHPWH%)PhuDT#mOG%$xCbzjj{lS-MJ|CiM zDMAXs5-la@rXfG-VPZ{EOEIa%0y9wwjpV-?Hy}MpQq#n)$KgW`jU#Q6x@K&`aL3>) zOt;GzZ851Ou4!!CX;U{1(Q=Bl79k<6r!-#(Y5d^NqvcqO($4ZQ@mcqLmNJaax6}28TJI9*!T#p zOz|Bbz=x3^krq03yr^u!x zxg3KK$oGxGr>BWOx|imbBKi9#C~nM=HZ(~?BOGu`ilnKj`3~PzKfZpbGs{7pz4tcR zqSDZ~B?sXO+@iuQDBK=JYdWI#w})|Fs3QPV@sSHrH?I1;WFmlENr>iSg6;h@KRpe` z;yhoY_2&m^4f&WI{gk)&(w>fRN)D!BxO$DCI@9JwFZhY87=(qZ<2IW{1<{Pg$qAy_ zm|)ze`n?szlZ%MO1ZTG6^X;!sklR+K{jt4>)|FJ0x6uCT3HbL}GTG~8UYTt$l?))mv9ixnLugtbgW3D@^(dIN)v1YSWB{_`~I zg$4~z;e;t7K51kDOX~(HI1cQtA)@Tol4iCn#rf7E)wOxtofX(MfA)uoLDW<1o&6Mk zaTssdLn=t>n%?D<9VVt7Xr8RO0Nh?m$hBe|qwxD&+#ZS1F7ka7G{1Np@xEc&EDmsXJJC(7xWoXVu zAOtF>Xg}XT9BWY&mb=Rlca&x=m#~NoCG0g&c?)Ti+A@B;@)9wxg(bSwF!P> z+de*Z{i6s4uOt=zW5>=cEC6J^HuMKoa#!Wy?Sp6S76UH!x?bYYP*~$55SZAq@=Oi+ ze4U;<`Y4QhNKfJfg2H$}&z)t&?iH%vS;52|mr@w_i0di#+7hwjyOHlE!rjtSSeq|IsHAVHg25B`xDDZn7@)JJx_I|9f6o(xiK6oo1+VdVlv?64@a=|l*JwWIB)7BZ7Rw;2MEUklKF@~y7gOFWARIhC-?Mm z)949wEzQIPnF?TCiT|!qtc7SfB{$^a6cx*lSK*UKK!9`dK}jsdm=T}eEj@(CVz7d^ zu0aW;up|vb{`Ec7hJECGXI}3u7UISN8o&^;amD>orvj~^k~g>>cBR(;0b!x_%!Dd z*wzPMI$hfpC#!7g`&E+tbrx^zg<6U_7$QQQX&M5u4#F1r&N6;kVT}a^@{yVI8~@G$ zO79-V$txOD5eV=rE%e3ZVlIpp4rvAbl%e#(umXvq`YXx4(Aq`W~(BSqu7%fn~L@7aa z*9pvzKiplSWIh63Mb1ZvzjzsO9?u{bW6X*s+!vC+x`2rc@k)wYRGsv8HN{|&N)qmE zU>9T5Vs;6({|}dp076^P_9q5K`Q@fa8o=F@@2o4WYb>@i8eR-(z1%{4YXP;K zO?B(1;gS*H|AJhKVb?7BZ58VKLwbK{5QL!m#p5_(ih3-Ye9mj(vJ&7`#A6FMz)DGh z1$S5XmlXc2xGV*D74`Yqth?}J zkTI(QWXxI50nU~pxd$e*>dO^@v*qkJG0ze}jt6J`bAeSZKC3P$V^#&onEwSYFZrf8 SM_VQU0000$hO|gmE)M@R&+BzfA@}olnEQ1JH z%vKFpqiLdIV#H({v=M_YV#E$K`vchmldVnLSQ{N#(hWClYLI3H%I*E^k8>|n`h#=t zJ@=e@J?BZ@v_1Dd@B7X7x#vBn@B7~C+h2h;J_`TK!ovZ40)3Vb_08~Ufz#o*51&8) zBeP(`WAN5(PKV?!E(?gTw-Pot!dSXfA-Ri50s#KLz!;MLd7~2{xr-?U01W5AnkML6 zW_L*LVp;(J^1+@e*j^8p((DY&J*+@ObZ5)qQSh)Dpzzve>21~{?E>X6(;%mM&{ zet5bLUZ^&nm^`8epir)%}m+=8LKormYX~1uQ_klc1gGmjn6|fYT z4^+)u?GP|@)v^14Zs2xH14@447Vs_b*T^4tg@3NZcxzw9S>Tr#j(#vjTZO>Kzz&Ry z$rZ#p)_)B;2&xBVf_@9qL8MQ@rOc?;2YMOwE~pyRW3v5sLAgmVMkl<)lKc>8Rk(p{ z&>T=f8X7%*MZoxW^dC?i$Oozb4Mf?`=b+_ICny;OyfmSK63|~j%}Y?@J!t$S3Z6ud z-(w_x7N|9bJ|g8jXoJ&9N+to_pt<3bvp|0cA0PjnAz*zSs;)ru^U(Ua=Qk_~1mn&Nki+C# zeizUNj9xp0$AB`R9{AK$(5ZpBr=VlNPDmev(j~^BRaMZk4NisbWMJMiJo!c)n_f&K zJu43~^XWU=#OJrPGq7kU|Cm!?JmTCOF7ts-U^k|cAE5*|0W_LFCHaYJD9Az=eu383 zqM;8_|KW0Vb#LsQG+=cWJ`eVl$RaaN5va(Vc85wG8Yg1=urzGj<(W5FVD$=P_r_|HaqthQO zkXn(hx8|PUZULb4^U!T8mTLL(FN66;>8LC0@|`=i$bB0`uh6R+}y0yt5<7ycvzCu+S;n} z@^Zbudw1N)?*|2(PMF0dtp!?$f^)#jz)qi!RSgXUgFym;0Qvd(`2Bv0i;JnPtz~p{ zl%b&^0)YSn0|OlE@8`~;A#yL8oU6WycHlwaOiX7dFHx-r{2TZga2!~cm&a=d+Ue@= z;oEZ~)KuTi#j$Zh%tE0tZp`W@Gc-sr1fff}vUO(%`MtgDJAXc^051SrfxS_kl{!Qv zpc?oPSQP$Qv4c<&fE&U|tua}D zKGGXVYo@RB0VvPuWTcFM(rdq)CzW)hyC)xa`=Y#a`+@-F<_1Szd1Y{{rpC3~QoM~> z4r9LnA5gpC46S9S$?!XN^(71T3b+Ad;^Pk6dQ5uk+p)hCcph-~h%*U$1pE}(4rJKU zTS}2w0kbhCe(p_ZFDXsp1T4fDa=foHn32Q?xDR80VRk}V|6XCOfJ|T;#+dJ63hM+E zVhlNpEo;#gShGESCGdN^E^#m6_x-xo@|%6?`YLj>anrUcdlTn(ZO+6-X~YtRL9 zZr`83FEL9OyDW-w>HAy1D6?(FGy_Ey$d(-G?<@0XL~-M`7Ov&rxiSv{#}HYef&jDce0WgIcUADD>vhl&9MQ3-^I zCW0}V7@~&AACe#j{~#`BbOlScW**#dB(@i@JK}>7` zSv^>r+eJGJL9ny~h!&%kL#fdXCBwt2_oB8Zj&?{Ie>Vdr#sS#0|=1NMHE zIK+GtWOJ*r_nSC5i~i1*B;>l%%;uxt2j4K=}i;>1MA6H+waq?Q)Y#aC3K$lGJ{r z5om@a7hq6xn3?PquwJcE8N`r9(=2JqZa4e0hg1_EsTYIp+*undnkXSPdSoM7Ivv_r}8@Nf#) z`=uXAODi_^PxCpOiEY+t2gB&cDNBloFx>s#_BA!|m`q&K9c!)ywOU5$~5g zhRk?B&Md!yO_uOM;h|fpsA(nB{k#Y9IpKHznEuFB3Ja$8;aWv4wpbmK)CQK}v2}JE zuCA^{sWE(h!sb?E+l36#%N+|Ut0ESeTsXC?e#8e|*Bghu|C(_4R*K-cU6q2zt9hN+ zUf7EK5mMqD&tUN{1^B&sR7jdpxdn zYfn6`fVTqeG~L9WmLD5x*YI#{2IWn--O5t~+z3QJGqQ-AnC3wStVEjZSw zm%OZ_4F(idT8Jgv#)5hi^f1dZoY?zxGYV70{rBcSojPp4l#Y`&GD)^bi8%p4a52}) z3@0{O9C$w675X2*cnRxcTtPUVQ$p^if3$23d_AAltV^QzYpqEe@SuA4u z4;qic=~v>f4jt_$pN`00000NkvXXu0mjf D=nOM; literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/MYA.png b/assets/images/fide-fed/4.0x/MYA.png new file mode 100644 index 0000000000000000000000000000000000000000..4062fe3fa02a8844f3d44ab8bbac4d358e250c12 GIT binary patch literal 1497 zcmV;~1t$85P)lKpdg$fI1*jRU9xnl`nj3t0EmH@_B0vKZnV2mYzF_r+v zSOOSh31AH80?6zSzwj-SNW4_?EVPXVq+0cY|2-_HBF4P!0^ z%)H_kU&jOd378At1+3gT6ULmupMZbDcOjgHF=qk@hZq!TNv{tz_oQIaJ9&~ zm%A|LN`RiAaI6(fGueb7aon1@#1(-0TgPgzles=((6%0WZ=l`h;I0o&K?fM{E6 z_yNHtplVH(0X@nLTnHc_s9u}o)Ki0Xr&a2% zh&B;krx#wQcS+BSt8qw*gs!nJ9O?fMNU=hmTCYBjzZzRWCawJ?nUVCKSE^Q~rT?Vq{XglIfUt^HIml>h+n zd+2>c>i3%OGlJzMz_W>3+o@tI0d`_Xxu=|3`#z7)bXR6gC7^gkF>)Qb)Y{K+8~&ku)?0H1nzQ*{rXED`u4oK*`i$QlS12B((ip5Vq<9`ex{L-6La$M{oqm^}U| zR9X8@2EkVbA`0pBIAfSXfKXNflAWD&o6>h3!Rp;JgBq0=Z|vW)dafnig*csXl$NB^ z<&0qt0ZHa$H8O#TqaPzw`x;_5Z!Nja`0sxryyp!>-ad>(DE$AL73R1uWC{V|g29m6 zJ2Ad}7pBgf)g7!!QB_Qzzlh)yyD;6-cGsG$VE#sohypxUFM&9-5FZ-G#Np$RJG!aV z5-NRg9cqAmT_Mz{j6mh%`ldCCFh2qu8%se12d0|NA^2o1wZR7f-)w^-$lg~hG z1dg%?sMXR7^CLif>|rE^M-lsR(9pV$qWfSgxCin#R}8JY5at4wJItq`<&GtQF_r+v zSOOSh31EyRfH9T;##jOvV+ml4C4e!O0LJ_c{`<_B5vqA+00000NkvXXu0mjfuEepG literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/NAM.png b/assets/images/fide-fed/4.0x/NAM.png new file mode 100644 index 0000000000000000000000000000000000000000..d8bbdbae2e0a43185d3b341164725b3a84df28d0 GIT binary patch literal 3639 zcmV-74#@F|P)1eB+);x`ILr`2u#0>@67nNdPs*0-lqhb}MlwrBIs;0;H&+p-$ z^VRwNs_yyDIrkzNG4Ftur*sH^UssmRj^Y>ndm4fo4dtJFLdnLBXqC#xu8}x6VB_d$ zvQ4QNiTQ^xDh{}cIygSa_10N~@MU`_N+oE@6PO2Z@)2}~Xt!k^boH5z?nq}EvSG8sj1}0#i4%iz-+mQCV&>*&reB}FZIpNto?ccih4IB5-t8-6^t8j zk;WEB9ArwAX1XVsDyUlW^T}JdkectlGfN(AkqEgIF{a;TRIO4pS{r(MRO0T`z=2b* z;^|V$_<`r?<5P~h)s~#%P)=TpL?%_UX67#Tq)pei(V%po=zh;9^wKrL0zFwiC(5Xx zLZhK__ihT7ETOf&UhqCUf}qwL5Rk{bafbl-_}jPmPUgO28 zzr^w07ynMu+yBkzepdik{Z*`>0+=eO8n0d@PcElEGgGj>x`LoL;r-uFrM%jkoT5;^ zI6Ij)M;&MJtDiqM_y9fKAMnZC&GhYA$og;OG`2W!&Re0`8lDP;;D-W+v|$q{GRoi>Bia}&{Y;@jZu?npvA1y%L2W8^PfNJ~LM zhubn25~)-e0jlcVIh-|?P~Rdvoa+EkYdYSvN-e`)ssi9aT@Q*M_;q;jB@%c!Dul)V zHXJu+dwtv-%Lv`UalimV_w5tz9&q|b1ev*mnLGXv-fjwVib6VcoknBBjglau`d($- zU*-J$(wGhpzNvyrariL#v9TyiOFR8JM90PkU%8y1k3Pc2$w}CIfU;GJn?nN=23_Fv z^+?JT-amWIx7nkpZggeBkW5NC=zLon39}~;o zae|PYJF#)nE&2&6KzQ%Fyfz|@*<+3(m8x0!RXj~fdy{{E_g*hPI`TGy18*}X{4)PF z<#TK#PyV?G3-n~~?eIwrXrSa%l|m9jmaHsaXG3 zG7^cF@LmN>8Fs4O_U-L#Su`_}KdyX*etitpY)f7q*I#;xJ9FlsuBsC1;|PX*NQlAp z&dva|YSnypHIX?-B86RwXv_1@US;;!Bdq&6hVmLOp7E?C*tZm=TFQ-*U}~G3i3qyM z;#WUoR`PlP9POF`Xi_>DwG|QO$MScg2=p`TP-)d_Dw2~aT(JU8L-(exq9Bpr8xup& zwr%=>2gs|=VSV~suHU}^h`1Ohld2gWdX3a2OZmr@7r1z*KV=FZWHJ>K2A^lbp!579 zG>7U&SN%1Q4!o&?s`1Ak$(ui)`U@AtO21p!2L}_ncQ0<^#_40NS~XwfY$YXoHL6y_ zM!t9g*y^U0=W_?KCv7^vo%|)^2k8dvKldl_m-LAQcvawJ-~L`edplGQPa_nqTGj3u z9E-%ioimi)_K72>0C%3l3+!NM7YLk0)ZHpwcupQoMF8rAbMk(U>dMj$^|0w-rCI{_I?CRz3OnidhA) zvC*K>JU;O|rczebNLLANh^chMqL_wHFJvnOL87^rjouYRSEJ#_u9D=SfJRGn(i>;jnD zp;DEVk-umWRo{GL?vy%=S9CPNJ9i@W@X*I{?q{;$>^$;oy0*-1RskZzbw~c%7F1}p zRHmd*uyiS{wYBC2oQlxzX+RMHismg6iQi!t&*48XLt zf#-}F`j{B5Z;PM;`1`uDd`=X@`WY8gb!X0y7ZXF%jqd+uiiKBnG{JxP15z);wD^b0 zvusG8M`5k-$I>4nhybPvs@B?CidL?q{L@dZa~Xl3T)%xgp0B;8k2R|s+55v<{*;r1 zMk{XK-4-A$(39W2Gm`K=o<{NNqesb$i$hsd)ZH#b!z((P;O*OydKl@b+^r)Pp1ShRZuBQOUaLqM{($o z(D@05)Wd_I-~S%(S+n%9230MeUR=tdTPbL<%-Nw13ov@9Kkv_u!n;SCr@kL9BP1ly zT307VeuCgJWeUMNcVgGOw?39xdW?sO#HeQUFs0l~%2$Z0lA*o)Q<69oZFJ!KRD_ zR5V!6%&c7jOchiOSFVsZf4*Q(iHpILTz~ME^=tnYAC40dP-bqdQ6_I zbNw@hJ?He2zms@wA(f5dKk?WV^a6wjda|OOf~xNHY3|77qC6!o6t?c}^x3@m5!Y9f zeCa)_bbVW-4t7%JPaE=BLDgDQL&3Z6TJ7=fhxB;)Wu5E&{q?c4#YaiZSjfG`5({S5 zF(jq;>a@MQU5tW?BS*-Oi!;lE%3Ve8`j{9V7A18`1=}wzvwGLJMWWRj?5(P(pkV1z zt6oL`=rL)M&h-HS`q-J`!z5*L6}yMN>b(`s++Cz2unwhWI8)* zt@3#HPuRM;5}1^P-+~2t*H<-HuqAU5Ckm|aVT?{9wdTQh9X*=Refw~D_E~)_tss@8 zjCiVBtart=Gg#l7lK@*67XlL#@r#Qya{cCvI8GK?@4>CEV08kxzVL$1^?ePGHKi4# zvNAiF**ys-NK4>aub4DDeiVj*hJMpjz0bECo(z!k? zOdm@tNF^~Nmf9BUd+<(3EGoc5*Y`?DKw_&~{JY;&#^#JzP8a>nlBt;uiwWR5awH-9 z_Td;Fu8*Y^q_XMULh6(XOQmIY%r1b9g9H9+))=|IyrF=N=`oxyGyf~YER9(OaEXc{ zbl*N4`}G4ri&xoz^5Ho-S!Mq;M+#Pp=mNNm7(wX% z{W$jT4?K1KqlowBT$zksix(5HZXFVtZd1RsKA(;0af(p={>r3kL`0(6(E|q^TWLN((2qu8jkRdwPM??Ui(Q5cx z?ruKLT1=DLdRJU6O}7P*NTu{zvV?%OYmwO50Z>wRhYjiTxm?lp|E==>g>DJpG+@@_vEl4Ia_$?yjaFuBkAtmKd(Aur_+Q(sa^b9_$!-7u002ov JPDHLkV1mX3Hd6oq literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/NCA.png b/assets/images/fide-fed/4.0x/NCA.png new file mode 100644 index 0000000000000000000000000000000000000000..86f69d554c9a651a9bcb3496b998039bc26f544c GIT binary patch literal 1418 zcmbtU`#aMM9RAKhE``T&Fy+4F#9V75PMFO-Y@8mywCgF=Y8HhUvJk#3R(&P03336Lt-R) zPC_?1S;>`;7UI84f?HrB07${U6Li#xZ6qs&s*;G!EaegbrrMBAZ zAVY-^`dV{NKyv^I(1r(E^5@uhslCt}3y8b*&BxWB0CCH`?dBUD)*EEKvlmK4al#Dt zmvs8Z5Tx@K!OlA4oxkvfZ63SePTCc_Dy5pT9|Qxl(y55)$Nc8VivYT7igsi=L%X1~ z@wa;rlKq+KsvbJ2zWRZFqtO+tmZ7lmru9!r_xXi5TRC3u;~rf=LA%Xvn1zlt!5BN!c&SmxoH_(=`Ad@%%D5hrAO-^b13qgoOksHu@gu_o`$S zX+uF)ia`6b3P#K_u1OGK8Bx2C+nz9BEDnr8GGG zpDO$WHXAC9cK$+nR&pVkG0%X~O*!^=f;z2SHf^_rDq%r|?|)95i6 zpDSE{X}cMhsfksOksrBXvnlIG=s+T>dFr-GvHan@FrN>Ud#FAaEKKsm*u`H>vBm_a zi|}V15$_hR`aFDkX)6C%+2-wg)8D8?d|DyO0FJu4;8< zHb1eU#)RJFgEE(xMqf!}deaEV-k}C7!sl6`Ezi`SQrh-fINjGHXc7f_`;^qHaT|0f zfe0}=hZS^0pV_K>MVC4w5PhN`!%qTlr(}PBb!-JyW{O3wp0b{_VK3vbItzD)t+zf! zkJ1eR|NPS2qcGwI&(;*7HETXO=|tbkM0SkJl4#`>&rkTZ{#g*m>1*t>82C^&t>%d` zh^khqEB08{;%ZMIT(_JXeCC)4T|IaP(?eTImvvLRyT<5_j$4k(`|Sp1fRtBytiFmI z>tI57mj5XJ|M1~1ZsdTZjTXmi&fLy|4%(FXO znr8nn=9?6?iNRJv1Z7?SIS8nnCoxVxDxK+r(x5x7PwYGwUmTkl^H#00+iwREvR}?* z#WSmRRdGm*0^_`q>3y*$vy1D?4XzDp%`INx(Uak+IKa!}-biozJv)^hHY#bmd(vkE;qlkgV8?SP46-u0 y!8NwOK~b1#DB4%Vn7k%9rATQzTK*UwIsi+mZ<6QdCO=Ac6mUm*BbyNCvi<{mi;MOE literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/NED.png b/assets/images/fide-fed/4.0x/NED.png new file mode 100644 index 0000000000000000000000000000000000000000..819b66da07826dfb65ffeb9f4465e5b3c0bb67bb GIT binary patch literal 338 zcmeAS@N?(olHy`uVBq!ia0vp^2|(<@!3HEb(?2AGr~;43Vg?2#GZ1D}bzG(f6qGD+ zjVKAuPb(=;EJ|f4FE7{2%*!rLPAo{(%P&fw{mw=TsOY|@i(^Q|oVT|&aymJRxH^8{ zoWLj*)xaCkJ@sbWRL-A(^TJ@3(T_jYd=+Ood;a@!^PIJ$=)Pfd?1e7tFJ_^7 Wp2bnWbzcI#%HZkh=d#Wzp$Pz6aAouW literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/NEP.png b/assets/images/fide-fed/4.0x/NEP.png new file mode 100644 index 0000000000000000000000000000000000000000..b5ecefb0df03712cd162bdfbc5a129ec83378db4 GIT binary patch literal 2956 zcmV;73v={|P)yWGY_C{3NNcZRW0}u84}Lc*PPF7n5OtIq&{4ULs%^h8bYSzW?By z%k%us`^=p4zR&ZXR{^sEbqGjMYRSL&kw!&x{E=HhvP3NJiHr+vT1ht+8bA=01XG~_ zPebqlk%XT&v922i4WKPn)|18%P5`PSsP!V|bdiLZCf4WoXjlKyj#6IdGv=|4aLNFA z7*w{1WwOXCniCs8(g3P8>~Qi~M;fCzgg_YZ7dVwEl5}em=<|DO{APz@<3+w;F`MW_ z3BW+G?-faSS!7IL<4W{zoZedc@(n9l%aiOwL$v_)X9(RTV!5la1o}I5-rHMXCz)NO zvVk5H0o)FJDPmqEGO4B475X_YKU}+Wo)1XpId&36rGO-YZJ&sFgf|8HJN4?_7|?l~ z99FST?xFx77UWYA%PNti2=7SrZ`|r$#!$jSHZy}PT4PhdqfmK3#5~@^`urZZ`}YDo z%n{O9BM)%|kai&d5V33$v2^yFME|D21FW`KnMoFJkwGMv0UiVnh*+kGj4?D=p`YWi z5$-@vvYNI0iW~%G0_H;K=OT$cJ>|n6@o0*nCFMNNPL{BVSS|t#0DX^$Wv)na3s3s; z7rc_=>CLySWF5)u!ia7JrauJD$C|@c zyq@LkMlpXUofr866BRfHa6yMe%)j)uZ(pEM(##r)Q5<3|Y2v6k5CxLvs5$(mV`FG& zt~6?%zkQ802jhN`ghUaM#?;bWa4P_GiMUFemx=m`x7~B8iqD)>gD)q&0!IrT&p<Lae1k9=#lEmG4B_F}i;J)A(}II((XAWS>};nZ z$_QXFKji~@p(6nMK(i>dc$Y`6vKdqT7oh7zO`l$6Q`9T3aNF6l1a|3CrSSm_p`mCd z6StR?(0=mdD*M_$`z%_kR&|O9qLO5GvAjynLEj-_e#_r$4%Gr+&B>u{LIN!Z4Z60i zc*P1wLk9)bXhe)gG?R%7sj1htnfmo3@`V?$?%nIMm|L&Z9Fo~ZAoc)|WJk>*VR(Hm z%9p4X0A;gLvSkZ#Yu94x+n133{pr4E51Ps3)Q}hq^vKB}eC$|S#KjS}VFQ;kGSEv) z>m|r=WdN7PTCUd|)E0lOIn?+NJ~wY3hVXE1&demJS1-=J{<=%X&MjC#*};Q!+rAxR zbTozUyyLd8@7Ekcuh$%M{H^9t?IC=XPD_^1cESY52c^K3Zo00cmzANJOk7;EhWyc^ zJtFcv!OUj>8N^=Q^a&WAR%+2nk2q*TY7KxE6hyboOhWFyyWU1^d3hX;kEin7IZx>X z;GcA24nN}@ApoVo3@CX`sa1B5I%-5}9S7)TWgHzjl9J7v>t)cEo6C_ABRt;t0QYi| zG(W33I30-Di;Fq-$RnJ7<{2u!`NpXsWwTNE?zV2?1uhjCArq)&$9tl7;?I|U|rDIK05`gZOA3@QI z@*4Nq5mwNX)%3WU&Nu~3QYt0gEoY5{YpqrX6=?jC;aELm&Qsh3Y!tDq1evVV``hz8QyeGe)K2zK-23!r8+$9Ld7_LaSpv%E=${GJiY zhM_gEPpOnyp0e++c(fvRHwC;wh6G|)z)TVIG*8<1M?7AV9KadgbI&wIm8cKkLBIX2-Ux|-WdjNEuE-O|LF>xZA$%JmT zq6G)jb>l|bjCT~J+KeAh+{YiInM_m`6%h~_N#xY2bWN{5`X5d{FY!5o5|D)=3FEwW zRX&8%k#P9fv2j;F?#v<{1$JPZQYjy~BieQH3lGZ$`m>+U0NlbP+uYv6X~*iIbt-{ z>%2*P%>@1nNHC~PB9?pI7U2U_3xF{uhKe(1$R9I?qYpmFg=Nd?Ww7+CuQ=GN7kPsQ zadGu($2$e}BIzA@u89o;TD8AEtD#Vc1*v~ZzQLl@VqpwUkL?NM|XNZ{lHP~=7!)ZFYvapc-F)@xF(b1y-*bX1&{L-ZWl%G6F(V|6wBkl0* z{Q2a>#!|9*bDb44R}f$dI~dI&fYu-xA`|*F*l;t#X*&9RN(z;Qg#ZLZL}1IyBX8(X zir#w6GK7jVXDI#pYixOWj$eYVbNq=XxV&SB*F*|1jU6}^h9W{x+kAOS`uA~tW}JxQ zO_?jS8a9keTesG_-y7%%FENNu=nANFpoS>5^nh2LdKuSeMf{*~t2BNM`@MmNVg^|} zz)?WjgSO4rSM>F{HK8>Wv&dou#{eS1o*|Otdc~-j;d23i>p>XoSa~G|?At{WV(U7u zIpUK60LOw*^Vr6{oCLTDRHn#pyELt;`s1?!05GDnfUOMV6hJ31Wcu2YnLZr=xE_Rf zM-Vy#86xKBCYI$gJ|6&pK1qmOGZV>hCuEep-LxXT`10-zopr0N4v?;s5{u literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/NIG.png b/assets/images/fide-fed/4.0x/NIG.png new file mode 100644 index 0000000000000000000000000000000000000000..7a75a84a1cb447cc4e5fc4627a8b36fd5420a30e GIT binary patch literal 932 zcmeAS@N?(olHy`uVBq!ia0vp^2|ygg!3HE(Yh9cNq&N#aB8wRqn1Dh|Y>fYEr&j<4 zB}-f*N`mv#O3D+9QW?t2%k?tzvWt@w3sUv+i_&Mmvyoz8U^eu0aSW-L^Y)gvM@XW? zv5(JZpPp&?Xh~m6-|96ReN*@}be+%M+3nG!p|sFdV5jye0k2!37t~g{x`^Chz1?aU z;V2-n@}io|rm2%B_FX@BX6EV4r{`RY&wc+{_w0A{d7C{y(vO|isQV(pCm|{SmGQPL zyTUQXjzWecnFa~6nQGVUY@XNqs;I~uf5_0nbcKD&>M!Y%H}yTAT-FpZ&SYrVuhy2J z(^pWeUL$JVp|5j(8fQZN`~#0Zl%}Y=JrsGvE3e$7*b==f;I1{p;*F`3^-J7;Z&x_J zZ9~C-rm*93jD9cg&r_*c@y4PxcD*g}y16YHs3omV z>yDrH?K1D(3<6G1cO1$s-@^T1eW&J~b>Z9IPAg+<;w>z_ZCH9cFH`)0PYH7!YyFb1 z>S2R?7ER(q)$T z7#1RDr?7a1{c`S=+x2wi1Y1_uwl9=@TsQ5gk zmRYv~%70GXHlLM2s$OC9xs&M|KP-@6eX{*&txDw+lSS!yPArGL-hcM`&G7ooOidl$ z_-0c>F7^4hKNu=WG|hbB7W$r{=kD3rH4Cky)eh{Mw6*C?h)`<@SL+!ahDL`ijE%qc za>&1FIo}g7KVq7$_=9?3iJ7uX_a#OPPn`Jo50}D{xBUxmvyz?Qkdo{-@dDP9TP3bB R7z1+{gQu&X%Q~loCIHr@h!6k( literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/NOR.png b/assets/images/fide-fed/4.0x/NOR.png new file mode 100644 index 0000000000000000000000000000000000000000..bb080da527414804fa78c2e43c1a114c29a7c559 GIT binary patch literal 435 zcmeAS@N?(olHy`uVBq!ia0vp^2|(<@!3HEb(?2AGr~;43Vg?2#GZ1D}bzG(f6qGD+ zjVKAuPb(=;EJ|f4FE7{2%*!rLPAo{(%P&fw{mw>;fq~J%)5S5QV$Rz;2YC+$Ff=?2 zf63COz;mI6o7P&=%h%PW`@I)a z%9P{hzkRew_}S{Ww~If$)DYSH@$kOv?eV{FXz*4s{6EKFzgj;lV|<bP0l+XkKG&Yt{ literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/NRU.png b/assets/images/fide-fed/4.0x/NRU.png new file mode 100644 index 0000000000000000000000000000000000000000..28551edf92b447d029f1d0a337395275d37a18a9 GIT binary patch literal 1378 zcmeAS@N?(olHy`uVBq!ia0vp^2|(<@!3HEb(?2AGr~;43Vg?2#GZ1D}bzG(f6qGD+ zjVKAuPb(=;EJ|f4FE7{2%*!rLPAo{(%P&fw{mw>;fq~_{r;B4q#hkZu>V2l9${esi z{+FT8yYY&_PrCAE}AT@0SX+4m;x<6K6v-5ImtRw?fvgf>H0SF-~Ow7x9|P;>htN<``-V4 zv(s4MZ<~0h!5{g+_(gKL=+@69SnjR5|%I=^k8hFiiFR`;9aiQ4b~Y} z@ZVLgU!W+klZ6vu*JAAh2{JXF|Ad|29M6?@^$4UZl-`&HS}?%J*I46l~@gwzUl z-|R5GaA@mm^V(ywFN-&_MOj4Ld1@>tCwAuQIk~)@Y$4CDSicn zuHiGLupD*qO$rXvnkpV;5mq#D*)=^IIsKpN`X62upH#V}+&N#^?#s`$(y|i0&FlQ9 zT)k-j;8@yM_1cpZ>-=3e_HA!r+@9`IC>nZJr>9@; z+Z%&>0b5v(9`$?rap~rh6)9qxoR>GwnEL*E#ooBA`oGPaHiujMy)4Qn_Q$9H^R!2= zU-k;#W&Qb6GUDNk)2Dc>JwGdTcFt-!m=F-^;9YGFG<8Zy%(551e-)bkT(F6=-Dk_B zjDCfSawng?F;Tn0^v8-Kq_O3nIzB*|>j_GH=f_LCPZN zx%-*><5%UM-yGAwF)`Zfd#@cs*QwW$f1~s&e?Dh85hJ#I)0>UWX}2$OKX(^?ke~E+ z_HRv%eJ2`jJA1k^Gc2`_JNEY5hHq>CK6yG{`LZJGpYAlKOOw(k?ET8X-ZxKP$~rR6 zz5lb=^Xw?bHWd?-|E?7g8^TtW_C8Xpv3B>n`r+_TwfP$-r+(G(&;4>!y=b@Xo*gf> zm$k06=P?j75Zd&>Sy%7MExSKczFs{S$MolHNZ9rtJ=He%3U~jnw_deP=^Yy|kFLIN tJ4>_V>Y}Z zen^KP(h(5pU}!>K{P}*oc|Ue{?wy@GJ7><>-J57iaR z-JkW}^8FWr&D}x)07lJ!#vpA_B16Bt05frgIR^Q`;NBrn02~fiy8kFJ)Ym&0suUFB zkJ-=`qQ7_rVr<|Pk-we~cjX^%c`-A=LJ<=K894cz1tK{;+7k*2-U(V)XT0RGE)W@f zyX@-YR6q*n4B<(P<5JHyeXKYq&iT1HNXzC~ocZ^X;0%H4 za>CTyw8f^5YSN4$|4b)k%+Eliso{fLiCt=e**i~1bu#0O7;I>`gv6M}#mQ-bky1>qX zJ!{LlS6Afnr&=yATlPSC>l`;(sf3f`w+c$)SA&@zGnrT3z|v-<5t~6pPwl zOAuodQsB2j@jqbO5|bfeG@}TizALUfzDg2iqzjXv`&02gaX%yn==6-+ZW-N%&#wYMbsPeuzoW7 z4)=Zu{VCXb!#7S7>AV|r4hC~Nmil24Hy5VK3@M!pJd1e$V!cchpTC^^R#J9(K$%3X zsFlWlaN^fK=%acj?!~+#j7$-vOT(i{375I<+G4EdpS^xnqOISE~!jOtwH%gU5NDOcdC@8YH8REzp?#; zjFu7f;xFZ#1&3do4X^JDD-T95jKe5A&ptlCAljWt_ zMqS^?wBYVkZiOU>;DKh@Q(=&$sPEb;YU(wxHGRc7mdW;3lTpne&1bCcUdXOxK+oxp zID#54m-K9-6^Ra1@tz=4aEYYoq-#hkcW=v+Ktl1?bNq$UM)Bl{4C~|Z>w9h7!nCnu z&lxa(c&IWz*7^LulEMy-u5|oFtr?aan$2p*%DXHGYkH1pWaJ6eha}}TC4ZStun zpG=|Y`}r)3^xBh8-s;-9Xf5TxlN4*~5xI6prA}wc(2SvvD*JJE_-|*4sJ>Sj9`Gjc zBlqWuQs8mrTP>m=GQR9ACV@Z2 zySgWd<#;ds(Ny%eEgM>(qoH$V!L^Pm_!?{yR$X7aqKaTqrg6G~Fckej-r0$CRLjJN z-x7`jwD;7HCFG>YuxHESUv*-gANB+kT;C2yuOnpROwxMt)|!%>{^*@$N46gUxToK= zc{~I|uXS19^r$;ZyFT3r*~MsaGP}ROy8KpmU9jBii5OdeFkZlmct)yS|aCVdN`Lm5p9aV!N8=|{Vv%JHRsr~ zht+@fJ%lu9vjLxvFJNGTdCk}I&V2SLZtV>uur@wN5XbYz^$D1zP^~qeP`~9iS-BY$Mc6|FWNsdT0KBTM3=4NX7crD^&EOzXwJ;IwG6Yks_S)|m~QduF~;dlDz??+fsc?w*|VJx zXkp0sp~yf<@i{%_sB_y`cviAk_Uk?@m_Z7aXKept(;Ur-cgF*EvyyQb^_sUK?!pG` z5AtQH{u1|84|PB$b#XUr9_ZO#{3bfr*yiY_-EpDJ$*QsX|^VRncn z9@w2>SX8@QuNyp1Hqavzp`s?v}JBKO9hez#Y0STIl z=)kGl6#%2-u-Vn87oyt^6!U($5Vw>*@>u@$n3n4EG4}BH}vyPv67erGD*o6AnrFCP@^3KvQ1`NMxp~l+tDFt=lQg{1hcBkr)Om-Rz@D zDh04wo+P}L%o#h*RYoQjMh$4mmW}0J*<_-FNt}*xJ57CeGg8|^`a)}A(>~%(C#M5Q zMfkVD@8VE!gwCZIX?v;CB`3&_+^ok+HkVu*N!V{ePS~%P|0<@m?&jDB{}KWKql9nM zLR9xXK7RY}$@|E}pfOJaQ^Jv{h3)o*VJvZ~%G~;^Zvj?!KCdQmXNO52bO*Ba7zmW# zj42Ol2X!S$%Z{!s1zg;I#EYX?p9i+uP>d7oZorjrZ$#Qd10oudXIxY0jDx2FJ$mCx zTu(N_DW`m=r0hILK8!x{1^11>F;E*hvl~6_>NeJwO+4`#*^L)!r@jmnOoUps@IX$b zynr>Gsv*Nba<1Oeaa&G>XEXpJA8KUCH1pGya(As#KFSjjuGGWsa&GfTghil6V$fya zJ8km=`n%1*;l&n2n4qhZF+s zT&)ka&tF}6Sv7ehn^m;5=!4aI=QyeDO!L8ilkv+V!H=Z<%f3Tp$oz%HZyJ4P2cGZb zYi9_YTfxd5tlJy5Yci7TCd(-l<7jUAoNWS4oP4-GKOOJcaNgaeAVqcnVXVT=$nEIq zaGChjNZ>(1$3F>|+1Dq9ZsnSf7gc0wvDb!;-V=GAvb2=uquUbuWd50+D=rSFXL<#a zjO&%vO$n$N9ncrR-_p7mes|zF(_FJhYdbO35sgR6CHWw1MD|^3DxwFgYubItcTuI* zKGrKWl}A*kwy0=FCELwAP{Xa|jV8U}cm|Qu;!GZpF?mBy4a+(w zS_Y!28nG?7r=PkKSoaTxx_%Zx`RweJ#8j`A5p;Aa()KD7`eddpzJ2lS)imW=?M{m0 z=;$3<>6wsIaU}?rm1_AD-Y}Qrwwk~8M30v$F6_bdm5jQE7;F0Q0iwnuuy=t0@sSFa zhqiO%45Hl7M!n-q_Wnlx9O?w{k%=p?66dlS^CZl}5g0`+Sf+BEXTJ1#ptejNhA+W>aFkIT=qe{*(wS5sKfscJ^2f$i&`Xt?Rj`DofJEfTj^m*?}4Y6&6+2EpZgWa z3TlH>&F&m=Ya=NJ2V5F!&kPL^qb0+EW%NgCTRu~z% shjxXtJfe8VRNUEz{$H^E7nC&a{XH#t6vAbK{;fq`X~r;B4q#hkad@^ge!We$9N zfA{PCt+!W5h^6XG%*kiWIUTkbDA ze|PDv`;U*`+M%%fUvSSPA@eq=ppxrumrSm=NqMo!I>=9*-&c8OOAy-!TTAixhx;wI zWiAxBS^aI|=cm_aWM7Z{)XNtVwlFnyY5p=vt<;7e+j&{S*B#Q3xxD@CyO#&Ea#>Fw zva4o$^Ka>4`~Q|TmzO2GD`n0vSmr5tATM9J@*B@WUDLn!zf{b5XS&+c%v9*_hvt9G zJ!cNrd^Js~ejPirV3}kA?~AyNJb$;{=G3WLnLm4)HB;>oj^m%5W}79?xA$L@m;Nhl z=0P@|2{w0K{`~M~(TZ!i6T{H+QFz0xTNmB?m=4(g=ImLNv`E^#;pwTHImdRsi46Gu zDQibT>l2lEpbYQaSy+bqrB~|_Z$DW za(#KejReDtxKGzR%oq4KlcueH$UcJIk;pJ)b8Mk!jACsK? z{;2h5#gh5*x&jCIW@Q{ynDM64y(#tb=c|dX!RL82_dg3OHU^@?_2QeWSnqjGez~fqwc_x^ea}^* zZ*#1xX4jQb3)}u080K$ZOkw*smy?Zi?_Rm{N2)t?B7GhmmzUX{eB0h_^Q0O1dd}xh zF077T8^a&dJcZFlSY2>MR9Bhzj1!-^J?8z=ecHX)+D`G}9iM`;(J#t<6?Q$0o#fm5 za!&rSolnAAc77?{wC(+Y;^z^&rs+t1IQ&2U(F?&O)98=WrcU1bU*X=SPg{J=ge{p` zwq1ENe?P&^ezr7uo#e?v*#~oj+&*3L zT=VT)w%r1|8jCWe#=AFP-k7)d|ANO!zJHEas{Z-A@wMH8s}~FJ9^WxDrbu;;o|_}% z59T}`(y@`h33se&6v9=Wya8N+JLNh~up>7Y~wnAZw`L zfeT+`Cq_7Q}&`>pjC-)<#$Z?_^F?lmZBu)PkK&}Bg z13(X_!8*+lhuGf?-J)7%Cu~5=zB1QX=CU_OA!3lT1#10=3(b{Lpd8gPvpN+n>g=JE z4Sdn=PyJ7f2F7`beC|x3&~gg1sRYPmiH-O^3DhYCfVJrrr}-eNxCu^|w50(i5@?qY zxV~0+q@SH2;HSGQUA;O^J+`=xU2r%`(CpF>2?lFQ3x9Q#fJZrlfvM3_UmYWt4^%In zu>#KEuq85e+h>R|FIn=(VW(rl88W;RUqtmXEAzL*hsx{TXl&Fx8XBEQ`2Os>;Tj%x z#Yq|2244hJ@aJT_A^$qp|(^}zHA?|PDTTq9``t7 zU)5y43RH#AJl~kLECbe-?x^Ow@)p`&W1d(y-=V4RtFx9Xc(H#4VNRO`0h9ayQ|+sIg!uL!m`XstJ<4zHqbjS+SqP@!Zua=S@doB@t) zGAsRe{5tJ%A7kDjebXe;>x*Llx1=^zol>1w9k$v5pI04uCQHaRpF{kxQ~k_uZh_2b z4ba7S*|TYCRyO|oG3w`=vH*a|mb=g0fU~Xr**HiZu*u(uHWr`1jGZDY+E%&zU8 z+=H1LdEQd$da9LMS^Rd{82>UKn2NvCBo1-tj5CmlwH!&)FBz`=kjbvsCSI2aQwpBN zE1>W3_^wQ{2%UDiZJRi(^CK-IMF273E|f$&x1eJpty^P zNv#rFTNI%Kc1;RSz_TpRc`CJe)E{f_D2{YTPPG)sU|77lMp12Ie&YsgP5M;S}2Oq&8yka5UFh4?j0W?~0NN8<`(Ok`tR8RJ40lW5|~ z1q&8xaAVT2@vCU5!B|-kEIJx7CS#zG5+gVgga9K+L7?TUuWuHm9bj6K*Y~*d=G|Y? zq_np^C->KP?mO+{5s_x`w_6P z5XQrY!*DtWq3oVp9%L8b@qC2eKac+Y0A9Zymph!|kX?YwH59w=bPmcLPI1U8AXBP2)BC#J9g+3zNWBH2SVh`>c2%&0zx4HH8pQgQ}gDF z&-ITUeHr)h4I5Hnx9>zzkuI5(6h~4eh~o$@UHT4>AMZn6UWVxR*}dC$&&@xwab9*+glShUOF9zuC4>HSj3mD{|mBw^getD zezxlk1VR`an@>DIMaUvxY4erL7&RcdfR)XcMniD%VhPTiDOxs;35X#18W8>N2LST( zGjaR&9@uO-0D#KMT%0=f2S!I{(c3$PuC7lgElu2%sR7B4faoXt*s)xAy$52G4*-V_ znc(#vz>y>7Wn(M0jWLgr12b`QsS7j+?N z2^t=rg~ziG#l@d*st5t9tG|KGmIJ?kKCURWAgO?Yg3L9IXTOlKt0Jhw1W<+vpbQg0 z876=-TwmWb1_pw(^AL^M_$>@J8|^ei!`bccapg)c?L0(t`SQ>B?f28P(+~|SL6l(v zD8mF$h6$hy6F?azfHF(~WtafUFaeZd0um4o1~J#uBYPYsAOQil8=vfU+2b$)37B*^ zFzIkWn46O|4igXu-tR}CwH3VIkLl)SSz|E)ahP&CAjvFo2nt|NI-35DH=P&K=p}sZW3)VB-39^l#mYKP?u#GaB*EXhfgE zfVYI;z1519*Vd1nKaaPB#JvqyRe|^WMW>@a0fZp?>{*y^-9n094}N?c{P;M+6KmVc zB1Nx<$!>?`?p<>jz;cEWh!0-({X>9x9p2T|zrA^UXlKZUAR005Bm zbmY|5V)Kg^(3X^}K0YG@IkmOOYiR+OmnTUMsumC>qr4o}j*eAJiKU?-a>S%aH*HM7 zB6B|9s*7px`E$wRG69P$J@=17xaAj*Pu;r*@VumPnE(Lbc}zK-u|GMp8MzN0prEr8 z>3jFa9uJL-Ao%pDq@kGr0D-nPghodJ0AC$H4y(_HtjbEHZQF*;?d|xwt_~@>$Q~B) zj+A6z0uY&0&SXMvQxhzWjnHh|7<-Oz91PXfuy%JNbN_x!H8nw)nUO5E;`feD7GZW4 zM5~3hr)Sy3uOzjo2>BilChF@EeD(~w!-pk_uX+I_Ee+=S`Y#+zBF&eCo8Pe#BxZLg z33rrY0w}`-P=*Ph3==>ZCV(;fq~V>)5S5QV$R!J{u#4VWe$9N ze|w4P%-C69?p`a?j-8egJ?&Az^8(qPpzBQ?97pYA_&Hm}lvffAJF(_A~KhMAS z%!gYRW!fbrB?0#`3qAx)`S-6bEj`^+MM#8;_0U6uX{nz58#Ue8Pox;RCs^U*F8b z!&C6_k?XYS(=+q(mW8eM?Ef6l$7^6}>Z&8AJpFXyZL^Ecfp-7DM%Vb-?X#$2Yp~9q zd?#`?i^7FRk28)d?whyk)WH{yeDZcZsgXyL4AaumRNQX@-2sfZTeog)$dz7y{q(xN z*c_8pJZ1YC4&-KwSXZ-ISGV21rophjWG@TXw(WW90vrpb95V5}cI_HO^PTM*H#({a zEj*c#w0rf`l|fPswg(kYO|^RJHP`lFwZE19|Cvc!tKPr0nCKCpBPRP_S0?r&gCs zRDPAwi;Cm3kho^rOdV)*k}BkA_G+#i4IHr>=Y&@8`$gl zI(X3W|6G+OGTEekQm$8 zQnrw(JJzHywz22B{rY}>&-dd!@AI5Hutw-ashcHsLcD}Ez$@$q&PFN1R7OGQFMMa zYHcD37E5ENMCFMM79;RWfw?UxoLuAFk5(p? z=Vm$=MPd}wu(SDkWoNJTwsz9JzPcrd+KpZ*5CP*fqQw2t;WH@n?M<>#OC9GcRwFE1 z@*vmQY}i$mD-m}B0DV6JsZ+pn^{htj|5KsrCpgT`;eH&J>sa9Rtifm+q`PBAUe;%R z*L~BDR%OoQ(@py@&h@lkZjw24ZTEaHmMUzVa8X4>IxB&0Ytz_z#AAKKimCFuYjS$~ z^g^{1syuqKA&4VkfdjZCa-57!^u{E%%lnJgBDR4KV7@iF@xemq##O08eG^0Vv9=_ncWSKTlc=Nf zOHK0M!=jt%VYin5j?Y}JK>O1-$CF2A=gpj$XsoU88nA6GIHO<-^rg>5wgEJ^ry$5NI_Q*Ipvac%?W17%%C$BEqNyJ4`nKU8~88ygTlszvw06YuGO|h z9u!UvzP?O*e;B=aCrRCm0=}C7#hlfMygj_GVG?v$+;Ug3+AVyVKpUBr^8%s8M|^Jr zQ=8K!CLGQA74P#K%EO%!Zu_UkzN1tRgm)-k3B>O%-J)$1DqE@w>KGbz)Z8L+B6$U- z>_^;!HS;?`X2?S~O?!Gr@9+CJuA1;oXx%XtIw;>sj41$jqC?#*DY51y zT7N9t+jFYvPkGumP=WwdwMyDe)Raj%8g3RV6Z9AONIa(V{LuJ7U+icC?wLc3`FS_O z)Pkd3{q^5kBWj5xqq@S0XztWns4&ZZg_Lk6s!4g!Lb3DL($i2TQ?^)1meV(kr&YE&rx49`pG~=>PWHH(_N2vh5 z8d=#>ntBzc)79ZDEkyb@AnKnJ$^NR`Q^D(bRtemEOB()1Ub^PyQ0v(i|z;ViQea;Z^{&M!?` zkM#S%e(@a)m;h4HbHVPI&@Oq2_)~(_Yb&=v9wH$zIK#^w4Hj#tna_MwhcwED0|Y3= zvAiHMcHSesN%l`cG~8^fTKtoplBBl=-c`lr4PH?;zLW5bY5#jHKY$+pANbzf_dnun zbwy@okw4EF3p}A%LgB=*aSzgf-iPK3Y2v0fJX|D}<5H-F ziYlGaibFlC$y_mMlMWs9IK2pAfdpSy*hybsrYp5~NE1(4W7`*ca#;MhUhr{r_hYh8 zXD)Uv;W15|B%s~k!$vAcE6WTaInDr`UwG{{UdT(V4W&N~*}U%QmZV{ZSOLWN`gXp^ z_*3BuH|!B#QElLsyuQ67JFoI0SU zXKvNv%Ozw}T~zflP3)7;?>!X+kegiDb91W~atQXS3Wr34X>}{LTzZm^!Lf<(y)23P zFA2jn#bsI-I7s&Tp|`saE%Y6hBpD?)gPae~c221$1*}LHeaAJgNOp9)om>=zz-+ia zvL4^~?wH`$Z0xh8y)kqlROvuc1_LYQT0i8I`8oVtAEB?73Wmya2pu7;q1UZw=TrY#d3MF`d_+o&BDqI zV+sD2HFQu@qa4a6FjhgXXQyM1-t9CES@nsGcjaqJ?1GZigzys&)l>K~!>Jw|c6j95=LXb?ecC)#|Z5^;FbqK}1>$ z5mA&Nih>J>VgZ*{L@S~qAQVv(Vo1Ug2!zQ>mRXXS$t>^JKVA|DiOIZVW-=uCoRf3r z&As<`f4Sdz_rCkP_g%Gqd$~NAnN8k)2Yo~zB_iSv=qz1Z#N&%|sj@e8w@SR8#M&*TOqrj}{*tth}0k z+c<=TP=%Wri*s52-vbeMxTn$j zDM$hY^G-tzn1D*Z3j&8xN#h~mln#x3l~u`unc3v*wMWR^o=2OHq5A$?Q71f!N*rg} zpsoum)*a-Lg*jB#dc)#mPo_-)#GQrecW##q+w1&1y}W?=D|h4b>s_+dlj(3G)>ZZt zyLkA89eli{G?LXEw?tfkCO-eRg1)eKYoaCqfIpz~@&|>?e03*|I)AjPjcKAPKoivt zAG4PBg}tGh1ZX0APc0A5&t}t(!`}Bi_{w^qS90Z*JC~FG%OHm0YcgmDfFnQ966ux^+j}3d9 z7C^0~!j^i*7@lpSD|P|0FAa zzvm2cS6sy{6GxeKIi@i!fOreg%VvYO08}DUySinDr19W82$mGk62LmFWqQzslxLvQ zZX;N9JL38Z^w2hgoiZ|+)fpG_#li`UKPAoJKr0i*%TN$)|zSoD&qr~#KE_7A{c z)|^Epj70*KU`d6bv*iLmpqI}=4SfVDc@+FMv=e5d223#J#>Df6@%^$ZSUmMShNLER zTp)%qCqU4FerO8X=$BCa&POl4AC>-l^zx^UwDuT*+)P;f=#`nqLx?u=ODOZ`^jtQrs8dMf+gyM`&Zor*<^-MB_b0*E&c zsak^6{}1$|HtLEGF*|_z%a3_<74Q`y_79Pocg)!W@D%~ZnzK|dM{3`1P8yt=K<0zz zu>Fn8x#a92=50p-Ysfh*1ChGTjg4dhYl_kD2VJHsm72E^zYPMlpgX~mj*1@x31bYK zTIit%@$N-0{0(Zr1jPOkD)q)zdCom8jZH64WbKwx{_p3Y={@M8=;G!0|`1nccJ}cCMxx&kj?3~hmo2U z#)Z}oHEJ>^_%VPOi#FnexRI#-Mi-*P@MD&}Lr8x((Q-&cat* z(kZ_qL1=e?iUaYY23~i>C!wvJUfhEZQKbVmr=>TG(r**nE_DszX`1rQ&4`O}Ew6JsE@Bt!g(9TZYVD8iiu)}iRd_aWZhovOY)TpPEt z>yqoRJ(fYx75gt2b|M&;gOtuh4Za7luR+|~(S}Vko@mgJ>Sc)gdzvOk#0^8GT!)r^ z7kbs}KoD`PZ4AaONA)`oHRS%r@|~xlun61Zb8xQT*d;sB#+(5D3iN|_p+?L>N}oVH zd1#{-A&yVcMrAf<)5|l!^(FMX5S4aw^G<82+X(J##GQrIZ9*IMB6xQp=xD?K)~SCW zXm?Wf%tESOTY&^(x2yRv%ymK5RJ4%`(2M_!xU-DLuU?KeeAeQGti4}L5-M)9J~j;bOxGrC@S^( zklk=?+D6Ir3_SUJOt~M6nC^I0^2Mm6b0F~yaA%>?Zf%*tlLPT%Am}hAS^i@dNGt1#)E3y3a9rTL1U>Oin-3|K>QabB->>swjh<`j`S^#bI;^xn7y+tFQA5baRgESZ- z=x*clQLnnHmp0n#IGU=vJyb4yor=sQ=nXyVG?(x%q8$6eS@oguN`4O~g4@#rrY2Xq*URC>Z&Ci-%jk8nvtSZw zV&VmjSPT)NTE?JKgA3x-+*zXRGhuqj9||8!LHC8+ef z8xL$cE2Le_b(_DzHa!DRUig<9dn!kcEOUL1lw}~k1IHX$b_d#sXWQLO>nkavEF+Ve zmH&yj!##}_cYt_ukV6k4-aI2Q`B!KstpeKKW!96!#L=R~Agq#pj`Y6(y!pmzmM5oi zv+OZ@cto(TSx3nebMe{w4r1yGt)GItyU|83LJgdRRKJF})}vCcYfg4&@5J`lZ0fi7 z>@ab(XyfSOov5TUkNUzmy4YV{N%`Ctse0>O(EHAS3l|+mL&fK(YU#fy&B*LkVd7}f z?o8ORZUeSyv+*6YMa!RFN1Fn8_w1u&`cpW+{5snH^g3Dy5Uh1j_Uwy&Vd5xa1tUyc z{m%Q8&7M!7y0*K0>Qz{s>o;P1d=3qTMcwU7ujKzNc0xN3NjA^`0000;fq`+dr;B4q#hkadZ1WB|h#dK7 z_(*Nygd(=6?Q=9^l*>JvUCwRQys(7R^T&qrd$-N&PYTJd{yCHBv~UBPdiG*Fhp#*e z=NKL#6M4mK2CMx8cd~6^+hTm-+3EdB@wWO*5=tBmicJi}P>+6@Zav-p@9p;kA34r! zm2Z2^5K+is%b?9A$lf5@+`)dJ%i#!XLbLM`_Jn4qCyWWrj!&2qnjN07CY)6K#JpjN zz+;9DDFSuoA$P)R8;*LFJ>UF?!LvQRl5rCM`A>{IMmDFJ4Q_nUu{*Hf@a7#1&HU#d zF^WjeJIbhGW^@*D&_6KX|#u=XW)EvOlsiv;TJC&Q(qRtc5oF?y_?f)kwRhjHOa#UugQu&X J%Q~loCIIPz&R75d literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/PLW.png b/assets/images/fide-fed/4.0x/PLW.png new file mode 100644 index 0000000000000000000000000000000000000000..ec846bac455ed3f600554b4e69db6a23f1f05a21 GIT binary patch literal 1283 zcmeAS@N?(olHy`uVBq!ia0vp^2|(<@!3HEb(?2AGr~;43Vg?2#GZ1D}bzG(f6qGD+ zjVKAuPb(=;EJ|f4FE7{2%*!rLPAo{(%P&fw{mw>;fq`X$r;B4q#hkaZV{?L2C651p z{$)+9{WZSFuV(eub{Gg6-KNy;*y3d<$e(J;3D~?~>z8^LSJN~sblPM;P zDc`(o(b4L#A8WclzWq_~hhtJJ*TcNy>RSJ zp0oSdA`amTfwy^g&k_{fuXAdO&aG^_N9#6fII*nJ{m<+8enH0zQQ zP+G9{mG|knKSU%l*8ZBc`xw&+)(kH>|FC&W`cxlwnyR9Ekg54)gGCs9K zTf|W>!K_@rR7G=Rjc9qR(g{|d^j9mCJ!hU<^Au!a!-S*L+gm0@gH=B8O40_Z414Xa zr!Z&b@rYfWR?}KGmP~$njhC}h##W#@&L!vMoUW}!;Smi2jOuA|Tkg!7ld_OWJ^$Ua zbsHHJnJ(>;uDw_$cu{+q{;j;7%}fG6{C3!H;(zF~7Z|RB=dLWa-_#Ed*9pdNENto| zDxX=O-?Q-HiTCmm-}qu*>{<|zW!&di(kb(CevQ@5){7~|+3nVfHJOk2jjKQ8K52Mi z*nLU*<$>?@+cutw(^{r;?yu4xHaq_ELk*SwUe(I77IJl$w^V8$-JGu!5yzxsk)-Ws;Km6d0-yXAD{qaJMia)16Z?5>8b$`{*kBrA` zllp(x9hrAPJ)u0PagdOtDxDZ`wstdsYB zU+eLleS2VKgtp$%?X#=u7Pf>Qu)jD(#%#yB2a4xafq6qC2Sl6_MU#YIU}M4lBSOrO4;wm(q$kj)L>B3JY(Tz-Jd2lzZN zU6Vd+$^oI!TO}Qb_fFbDNsj`%fd%vrt4>t7br!_}wCPi)tqe7b)BTimNvqmp7`5fSLdLChG4%oG87kBU8W%cUS!+z&9%*ld| z3vel2tw+PdJj{>Cj~Kom6benK)$wA=NZ~1w+y%2O?GxR6%`d! zS67ptpU=&kH;IjnB@hVU_xp$3O7tM2RuPYY2!ujYz_P?|24$o`c9A5NR+WClHxE z;NIs3Tsx1*^Cs*YLA1{RfaK(4IyyQAohvLXq_MHlv%mb}t70|qKj8PM2Hq#A$|@J=roV;lHoogJzW*Akd?`YCvD>;SZ=x{=*d-na9Dz3k zdLrdrQ<8?rGDOl5nTf~`5vfPyO+>1Vd~?dk1y5Lij^!EP^?JE~|Gw4R!$vP9y#`lL zn5~km;z<_4fqr4mBs*y7&D3JjS9;-V9S;*ELgC>`jdm;^?FTx@06k> z&n>k~vIlRWoO}cL5ask&P|15A%K2VY4rnD}OBx814qPsmT)lc#4j(?Ogy;HNR>=DG>t)}*eIg>#)zu|CckYyp8#l_*rAsXb z;MlQa(%IQ5d3kwZ-@6j7bVF;@9G_`8{U3-NH;y9=zy-5EwyLU19z1w3WRca@)+S|T zWnnqa>-9=)ZLM^4bPUqv_xq*1ygV#j)8KY1G}_|uBt#Y>@&Y11LF61N5g#I=&7M7*nwlC`ty)E0T^+^6k#$8*!rU3)X|uCR(u}tVV}vbS zxKNUklKPIPOqn7D1qGHzY+72{pyR~E#E51IY;l$}G z$&nlo2sl3I`Y@nMde@2PPlzgLBp8&G=L9892EagXm9!S#4Gex0)K=IUfJKWI*{R+Z z9EE}z5QruvUB-sw79SrkH*VYrd!JymNVIxMwgq6vjvXQ*;`8}r$&w|Kl9J-MQ>{oP zywd~iVt^mBP)7Y%6EV*{=+92oO zxpPNKOG_PhWF%r2C20=;Af=_Hg96an+UmFib%}0DvZV?d_-<%jT^*^ZsU#;m^#5Qq z<0ur&)L13i5`g&lc$O_&Msaa5ix)3u!-fr{rl#8QC`WJ|Qqm#sSL$E5MZ9o(hfy%K z$Ep~V)G?|kXd=O&q@LRrcibiepaIt*B@ObeQqm*^X_U2LQj%j^lH(Oe`YFj&l{85~ zHo%}H(^XOH8Y;0^7jg1~y{iO3x z8J*|+>0F5K@L%vVf(Ej&ol*TIb}79Q>7Pd-q=7xs+SeV4atUx_L!nUePklTC;Vyn| z^1cCx+)XWEHa2b^;|F>+!Fd}~Xg|l1ur>zmJ>X-=JvoQeq$D}0Qpa{=9f<=_bJ;HzE8 zQ6yV_(9oGP(sDPP{|go)1YTiywp4RYM% zmgTb>Jw0il8;G_OtCoM;r zVM(T}%+$@eWSs`^fzWrf%4dw2%J5+p`D9KTrm;N(66mN&3x0m060?C)tcsZ6twLS8 zrrlm6$DecG3OA^CKtI%aaX0Qa%)UP0U_|3FOXhM{$F+K|s{htRwla$x2~kv!`cTt`o&9y=HtbA@r# zJR8i?E+W6uziFP1X)>#f;84_*!7q&q7{<=?4J^bEaNV^s>h?LSiK(W&w3o+O%BW4; z_9wh6(X@;xBU*5hr+5SKZqdpx9_W1>j8Xn8ZuIp;LS473H+=RFGaewWpC}GrqyWu; zgd&@^-hd~Hqr}qp$=d-Yrg62IgjU}sbJU%3^i+&;QcSGV(9Av757$Oo#Sqpb#}ndYhk=ziW{aNix-nJz=$BQ8*_@Js@gN;Uw#^nm)gY!rj>DJrI^|Yx>0as7&Eo-anu<4McPx%T*igH_gjR&WI9}m(#m=0d7 z2Fkx9Cq$>IYxGq3LIm7k$e#9GA=v^XGdob`ehRSxOsTZ`5%!)1X95U?DV*eSC607?z12aQdJ`5k%~4K9m$3%8B4+Z$Y#UVNEYxMf{vrg3=D-5%@Q1;c7B z#$d)9=2F=7H2QP-w`v=^g~RFA7SG%5jjZ51KrhU|F72V98aTh$AxO6=W+4S|4IOna{< zP=2;RPqj4-vb!N#4HG9`ONdDqG&o_b8g^?Q%RwQ)yR!N)pAxj)zd*UfNxDGw$tod{ zOqeqHGRmku2k5Wal8Se?e%LW^`#)V;XE1-ur z|8k1qy-GXvI}5JpkFmCokG`Udqpx6sL6M1m_WpXc2p00BI~7k{XZ0X1d~;jA`_HU2 zx6WH6|~+9vcOk8y;}U!x^`LZ*G}7)R{}71Zh2CTcUpD( zW`>*cC>1zc&>YjoEhALPadC~X8mgy}u&9Jgt$Wl^kmG=WXptcUne|SA==Elt{nuk9 zwO!d3f1y{d2#>#37$YBrA7(BI3tjhH4yR4c)LRde7XU87RJoW;-#gEHff*__;6kZ< zph|GE@kg4-*1k{q&m9ER^3TZvE2?HnY4n^6j*^c`7_NE>iL0B4xTbgvu6+xamc=W}h1-OkSYYKAe#S3uvmfEvrl% zwM|P-yhj8GQ-qJ|AfX!^bUh-;YUo9w4 zHOiwNI$GU-VO}@3BYS&F*_w%R5Fn?F@k%HNrcF)Wsw-Whyf8`r^9W{Nv+X0+4SQB! z_oe-kVON(__mg?qOuG|RT&vz0tCkx(?!1Ay&x2L1dwT}H?$a)(C~GxNpkqBdD^A-g zw@z*JK;5U~7L^5h$9PlRsYMBGU4SZ%!JNY68=`7$gS0z$u{MZwM`pxcXp7BO!B;ja6Z25c4}XgPyf**XY%g%Vkxbbc-c`#8ng5V zXSC4$3U(R3b6r{TGw?3xT1Vl{TB6H>t^4Elf{paB1ZZns&#&jL#N&@&)|34$B-CI7 zL$dbGk&E+{o-Pz;2QB z`fR#oqj@4x!ZAz=9rP!P9SWVdcsS*-GI@W5r$0oZtSqhXEJ-`e9DurGg)@cfP-Ji# z_&2KDpg{x1&E6Nfos3@-}UoftWN>1VVsDjgT6G$na257(>+%2OO;s1q3xRlte5q zfshIUp&fM)A2T|R)X`x&)(2{pcIarGmYNU@M8hCP5(yZx+08z(d;5p1L((K}cJFKV z?)TqjGw1yFeD|L7J7<4;g=td`NR>4e%96 z%x{yzFNXNfv-$7TRn~sFg`vJyCE_v+Q3(j+479PPzKSJ{`w*or zIpZ=6F$oAW#&v?q+Q*sk{YP@dWGH$~LkCL3plpf@-(+FI>zqm%AxD#jL6$q3RK)-U3CcR)`jjxw{S)Inj6S?f?#L3aWG1Oe`vY~ktq?PLwr{3cZtT?z=3Y8O~tWToOx zJ9dlK(I|uLmsfU%d`-H&5}Wq>$bHns8~+T*)~=qAUBEL-9hhZq^{X=>7Hs!0YlEAk z-y~MKjI2ZT%V8#FUL#VFt;_=YQB41d*@Kgc$RXORi@7<+^kqyJoj?<5lrlG9#OS zA!!0FFAqk%IQxuS>7u?#y~}fU^Ltd)tl=upH>&k12eM5;^YSe}4L&G44fV$R>2oNY z`58}6*oa$9RkiO}kWE0*uf-m()lb|z%?_%^luSGoPbiuw4{j__O%Vwsu9;ULkoL^lys#qVf67d?4lWF43 z@>IrW#eUN?oeNR2!^7cv<#T$I6ymOL|IFs2>lo@gt9X9{Wk8GYAcw%d6%H0miGF%H zRM*AbJ3KV2d;;POwDHJ^-7Nq58LT0{s(trIR9Vo2w^;epgAObM*l$S^{P7(>t6%CQ zpy6hAbVmbQYu7TX-6whR z^WSpd@a>FmKc;Hm10^N_^YXh7>?@xH$XMc{>;-SP#dhu_QI$fhl%R(-^;I1H+hmHG z4k+KD3`8v;-6=3>)Q@?nt5ag(P7n81dvJRts^9i8b!`VHFD|^7r3g&VRnCBbL{ZZL zKK(GC^7^W{`Y~bDqcV4r1wnu#jX_FwcxX7&Jx^q#O8`lbQ&~A8n?()WPJj#+c{(LsbEZa(o~sst8E5;{!QST)=I^ zzT@ffM;Ix&;Z^V{-C>-en1F28akkd3{plG(ZlyYkb7U8Ax&H@#cl-~@hrdRMYy!g7 z2TvT^%oYCRlTji?76CV%Jw#<~1>@Skl%qLA5tD$C?nc%fdopgm0M{c?2}lWgSlO_L zvJ<{cNh+z%Xx%QZX5dhy~>R@E^9-R*--GUrNSfIDS?@y0@A2 zbsLip-&2S_1tcFosSdpgnBDvy+iJ=g;W?#tJcgs2fS$((Nt8!~gC(6RT{*hjr+&+kJK%dUsnZ~1arD~9k1s3x_{_L| z>D;NW40*F=C|1qmf6V$~uf@?iFKgy=mQn zdk6AclNeizI9r1_fAh;P*b$$WuPLtj?9u0sBT$OPI(<3uqP_H;?}R<995SY@BH9cys)lKPp|&L=abnI`VQNpj^~t| zyv5aP3=Hy`*D(vCZGV;KUd&L6y#F_wJN;Qhtm|CnFC3?SGJFx*clH-!PTY=IoBRaZ z=o@iGt0QXJRRUW&ISow}m;DvE`1R7WqG-+g{~336?P&cax$3mV^k)q^hkwL=KfgC{ z|39veMMv*VEj@RJF>{hqZ_<*&p07C@g`FiF<}rWqHhTY`QQH3L`T182-qxQiu2he- zWk}Wh|Llx)LikEKOB?iQ|iRP^`dQph)^X#@$}A0y9BOkYox_vP5*DUWXq<5G-?^n*1&O;Xh@bST=><^aFH7{F0}k`GMf`yYmBG{1&t;ucLK6TqZAdEVinMj44$rjF6*2UngGp0VDkU~ literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/RSA.png b/assets/images/fide-fed/4.0x/RSA.png new file mode 100644 index 0000000000000000000000000000000000000000..953fb87de29a46fc49819c31d1792c72b17ce8f3 GIT binary patch literal 2469 zcmV;W30n4vP)#iQ!*-oR5xZa&XM>?3priyieHvh& z_}|Ye=*j$1W4}eE{CM-q_y2w227sY44dUW}4?X~DY549e{sK1DQ;OvNE8$+K1$1Qeg-lVh#vqb4uQSuWtF{yl9amowLEffE%yB9*s+v zNQWXg=+mhdBhA82vXd{YH&-P5rC{->8<;T-=FgAD#fv~?r5g$b$xs9b#oh+{&>Pj8 zU!QGmaZA>c?(hZGy8Hil{tpWzv^bTsfzvsRqM{gFzD$vhMF>#rZG1Nbo#{~;k6Bb| zmW7;Tr|}Z0^Yg8Ly1PcXd_4f}7k%~WAPyax2Rl)7hMgY>003QHagVk^)7&;9$s^tQ z&oZ~%`MB`6$s&qQ;%wk_x^VDdG%72jNXHT`#t0DLOpnrdtbK`w1(iTDfCS0>{+g|4 z?ypfU-@x0brpAE-2jp-#NQM#?DCQRWC<+U>9(H&noonkf7OPiG+zkh_Uc6QD>~B(O zieAjwz~vgiYp+RASs6n*mg$5_Qov@=p*cMXyf69W!zBH$tx2m`Q1eIzvp7a>*YDId zX3kIHpA3BYWjFTiTa3OwlA%l+6!7g3baX$Ar;4GW0L~$&$sKLg)@f`#zNj41K^QBxCQ`?@;KBZptKl1PN3P@vfN zo_VYGou?1xQWV84`0#KScJ1<`rY2%`e#R95_|^?Z9okik@eJ2x9c~!6AV8o@NdO?o4F{1+QQXr}(~z{GuznfqVnjs8;~+QW0}zw!D^yHbfB;KX z=rB~EcToa3XD1^pJ+j-JakFY5emwx9vy(jKiiT@1{uxqnOcu$0LP5v`04C;!FZyNQ zY-?I7b+dGOsQTq1MkE>e+s2b$t&lGj^mieo0t8s5f<<2eyAa$UbchH5eD8+iMc`ek z_}m%)pY079GB?P&LC6pjAV5|sEP5Rrig<9AvJW%#@Sdg&Q`JEH1`w=M5-P+604C;! z>i+oqJDW1&JuH<*Ivyc0Edm74q@!Jb9({5iH}v~N&YzmI?wVY2{M8aOjA<1B0B9P_ zyKh2&ya>GOIG;G=cMi0rC;1s_#@8%Py8r=3-S9{r56)%mM}E=3^Nm^eOs+WoYKa-Z zto!H*jcFHvroo&%f*TJmLBW^(pE%@q6!%vR&h+dD=03tdz%6Uq9UgVKhLjahIp zG|?9#rlY)N8%hkbW=nE(8?t9n!nm9A#9zp)+LWIwO9>q_zA(#Va_I{S8deVtG29h& zVP?VtOeE`tRGR4Vugdn zENskmx%@=S=0m}ZDo^)%S#A?`a-uAcy z8YYvgcGISYT&I%}eAWju`6EgX)Q(<%6!GzOydz3JAKUTr%gsHPFFzuFc#s5wJ*1Z` zU1T$E*R`g~5;+}+(X8LRxiM#8z)$q}EJ-GOOx5g;?#Qpx^a$aYs?Wz-UwNga`{Knu z&Z%%DK_xFjx4r@`dtix>f7}u_?)7b4^qOpX7G1tI!dd0T<22D%bh(t@e z^U>>SQbZKZ`SKYCJ#W0x)_m?()I`+iboWo%O*8;@V1vVeQ9`bu^wmYa@PXpwJ+C7h8--GV=FLzrVRPZ9VTk zu*KrJrPbD_TCJX0_woUP0t8)M{4CdVmPLPQY1jRdlFk`l|M@sVF2F=K#;sQGt^E8t zReQT)cq+$D{-Z+8F12v4N~V jaiViQ%g&C27L(=wC?spSK`+g=00000NkvXXu0mjfEb5`0 literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/RUS.png b/assets/images/fide-fed/4.0x/RUS.png new file mode 100644 index 0000000000000000000000000000000000000000..cc87102a65692d139abf9e7ccf3d8b20853b0382 GIT binary patch literal 4311 zcmV;|5Ge17P)FOb^tLZTL805u(jNz)iJP12~7Gzs=b>b1>8 zXpN~;oF=t_&a@OYHE5dzC0x@~xnxGf6a{1zL=fB+xh%`x+5P>o;ha6}a@jp+0e{Ui z!_1!D^PTgZ?>*o9y_e_xU;$Sw_!jd9_I!8Xe;u6p?tm-c@mLTF+3@qTc7JHd1hv`> zqw&$3%nMooUS4dZqh$dj*Xeo(V;^}haT)x7TOvR7|6kM&n-fBwjb845rW5K(Yl^qZ$V?D8t2XpM1i= zFak(R6Co!j7#SHp;Bx5=`cyC)EhsM^K|z5MwY8&exreiucL8kOD#n2Wp;)s5^TVjE9fd++L}g_hW$?9HGdw-5Yr(*P3Dwmq zyX!vtiS>)r5_Gh@zY0EffE!Godr z)vrRp=TlWpZ?6Fb1xl2Z4C4NMBLINUXQ8YtmLjOnJ{v(~Bo|3ZUI2hIX9lrjN7uCX z4GraD=gvjQ$q7byIG6I9UT;Buen0Z^?qhs>ZWY3u`T-FUJk-|4v-=y1u^(+HgNiMG-dFss#JL9ncFZsY1s7l8*%KI0x>Zy*t4e_ckddA-}rnM z-hMj_H8pVv3*%0673MK#0@$!Ygyv=`o_NBW_)f34ATO^UiHSE*R5SpCf%^9C+ZT$g zEMMaItFOk8m313>z3XqOUT?vN9}Yk&y@CAv`;>cb*dW5SYl)OTx*KyMfbH9T@%iVm z2nZl`_}5=+k(AVmg9rPd)!M(#k3a5>-~1+&IBsdtA~RDCwVD~5UZXMNz=3;6N@~S_ z|Endw4-Dj>vN8_aw)sxenmM>#0587ikB>i&hCo2x>g34>ShJ=b?d>{;ZT$Q^P*SoK z9FA2@snupY^Gqj(hQ=B1Ut60FYu2=*uyBC*P9R{Tv@{wozUa^R{U*b$0{H&-eNkAr zm|}fYSF7;WTlb*TP5mYp6)mBdkh^wum0A zh7Z7Mq81cH66bYx>allkk2|^ui9~?o$E|(H&ku*6ANevEjTTtG>8__I8|Ti&*qN?O zfpGzNc(71hyp*!w{_JNxNJzMW)29b1=A&5bf!y2>$mPp$>5>E`B}*Zbc@n?>@|V4g z2GSxUdHBgs0#RR|fcAD7e)?1DEHs;e;^K#hi@T23U%yLy9~j8N>C@2^Rmv0?_k<4} z3PozFh&VoZ@&W$v2PFXD`RBXv?z{JK=ujxOZ}$WH8}YMPR;$Mry}bsQ%|KWf7kzz3 zm--SB!Ncm+LZqY!v2mja$;n<0&oh}UICpLsM~?L4`gN;C^uY%Ykd)+wUAqDR0PEL# zWB>jTyz@@KOMRINjC#UxaeTD2$OtE^rA3R?tJ@u#nK3bZ?A;rLojd*E>ubLj*kWjC z9IdTdbafe^R2rdFn(*3dfryH-F4R}9jG?r27{S3D1O;&s9nFVSDwy%QgM%g%7Z2jZ z2_?F^>_ZU(0UHes62jxw>n&KhvXxPTfKdTdRmBlrt6pzGQc~-brsf0$0yeg76QihT z34}uTyCq{|W>i+HaO%_`%F2dkbn0X>Ph7n!CBotr6)I$8$QkPoV=CzBr^S?z!Leia zXB>P0(CN(3YAuw(|M=q}ELp-sa70 z{`U&(+ZO@=NKf}cW~LbB<;+A;8JhtO4GBa@`OX~!5)*GYR6UMhJ9C2$fe~uSQDBO7&xQG|d0n|@M<5v7}x z6HI*W?KPmd_~ESEZrLIR>l-bj)tYhk><|-OVjO?{>j3WEBdxK$dxIFcE^Pq>26B** zLB7ES1xmZ9=5%=aX)*D+re<`(CeZbI3uSg@W{TnOPhWx17Ql`ji-;Vq$z(xE$?U@h zd_D`Qsig4A%H}scI1@@r9uf%~9*>1*pQRrsqAh@|EV2`ol`0~cG8?R4FM_u>8Lqf^ zabXLfug{2@8dC2w5vZdj052~#W$`_Ke%Pt@ww`H6hYmeGhH1`T7_fcDb?bygUfNaA zYMqkeP2uf0OZ8Z`OhA18`fKepXD%Gn){-G>fq;#*Yo}&&%m%atFo8RF3=ZA->9Bk` z8F*`Hc{Btdm+L7rb#e^LS0ZdTDC(z&TT+Ci9rV2o%Vm{&SF4&sdB9R9zF00GIIDdZ*1_o9V$2V{4Fg7-O zFIFt}AX1TLGjQ#ieY|HnBqWeD^`W70rwsr)5DM88)w5{PHJ2G1F8k6V5q0v=(S>X_ zh`^dF@bmM4pP%dP>B7gy!({<{TcR}2aCrudj#AZ4Rh7!Ag)}UTiwzsBx#RJ1i_=li zj0_*5M5(*mfckp1Q|;*>Ab>-mu+Uk*Xb6CM(__aJ6k^j%NJ;U=<;xNPRwl(uFWqs9 z^V!rSB?M4kuXbxbU@|f?d?*5-Lzi?}1sMz$%Ay+@>Kt!oWP~a}AD`(H=nDxUA>^}; zj*inR4y3gP$mOm(B*Vj00R#ojEiIf5Y`tG+{h!I8B>>x-OC-)YBz=7b$_$dp=y{ZL z3EP*gql2CYPfGx8ZRE{Yt#U3ZXf^|_tz`ViHZZr)@tyCGCNJF(3M~QD)sdFR;>A2f zM>{tQYy)tQiU8u{1&E9!ncJ5y)3d&43&7TLv85!=2DSoNvEtDXfbBEtbY|4oI~N6> z3|a!vXv}DAB;NpMQ*^d;lch`fv;$jn0bA;Pef=11&Jk@-*fvGCX_Lq)bF`shjN-y? z-8wgAL9QY!j7ymTrKR*KG_(b9=FA{r2YYz1uyg04S+_NrEU2g;6Y*QOil;ey{_(;K z{scwcU~t}hcLdr3c<{hP;YsG?1l!gBP6t~m$`em`&&X(=F9-x|y!j?c#=3ZM1P>q5 z&tWhUj4LQ02}|MOT>R)qvoCHdFIT~2vd$nbmxUKzaDQQ(D|qQ8e@YGUM9l+TFham{r6_ocH>47zW5@ZIR505VT6To8KtK;G>oCTT7}x$(ODCRCvfy=IC67Ch~vym zIX?e<_JPOgz-WO+MMWH*ddeClG#IP}8n)iDBY1kUv3eNFN6ew})mg5qo#>ND!TxoR@^?D0dtZ1bj|FH$*yQLgH9E!K!4kL~~{BQt! z_H+}+t5$j8@Zm6Q*&-&2^c+E{G~v}(yV2M<=CDZ5wmx90-BKnO=_Mt35mb0vn9aby z{xytu-sz|0rvLJn#d!JU0OI)I!9L{WF6?0nBELU7hXVpb3Be^8gA92hi1}2a5&N)sfbeLSckV*6PsSp3MxH4zaO(?A{%S z-Ma(qRF%nO?dybqlu8qllUr%M3{$}^NqL3Bh#&l*ldv{eEKmktp)evRrw36{E!e;R z9ziLwSb#U)xJwjs2M2ReT)f1g+MtVDxAe%(?L}l{3x59dUP?Zj!(maXcCxZ=yJheJ zH?krwT~g!OXFCZeYXTJ&D#XTK$B7e4hdiRDCJl;;$el|zZx&NLP^K|DI*#MV6^Mzs zZf9DKk6W;FXBQ>x&NSRgOZn`x5xnwBH)R{J^mHE_KOQ+PuRbTIhq8yy(W4QxctbP6 z%ZrWU$0I1ERwk1LFTdRFbj^daa62#PZ-0A;?Ceg8^RsJL02&(;keKK=4XM+av13OU zVZQSDEd2AIF+`atlaR?g@zqxe`0nze#Bppa9~Upi z&gfuG2R@&LS6>Z8Tbm5Ixgiu{?H6B+B00IutvY`)=9HS)*JngpnjD7@_fbLsQBgdc zJQ;=Cx0fR~H-r*(eDlq_ICYAwyi88^LPbTK(=np(a4z0@%epbs2OmUH^13>m83zyc zVbdnL(=2bgn75sCV`KUF;~%4tp6+<#)|#5pZ#HgKp|jI~vuC3z=|F?QLe!^CPR8cVVjMUSf_3YruC?mvG2rS|Emp4d z#F8cU3vpGI3h%vV-MTnF-Wt08?svV2Sdk;t)v0mhNIzpyO;<7R0+_(}z9&LXP7pFP z#k6-!p9%(p1s5+`H}7U*p9dzeU<5D$e}4{s_(MNrXZvC8S|O#5%oXT#X4KV<;r#g_ zeDu)}1_u{bDb#`!fGs>d*;uzuX!8#otbY^L^^>7iPy7Q1>ozrYb!tYaBksh47Qj^C za99wD*zonW26KmpO&A?D+l4(I1w4`h`2U1&OO)mr{|DJOW?-2@`e^_F002ovPDHLk FV1nrP6o>!- literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/RWA.png b/assets/images/fide-fed/4.0x/RWA.png new file mode 100644 index 0000000000000000000000000000000000000000..4c58e5a70745039cb37dcf1b851023bcbf3f8f4c GIT binary patch literal 1635 zcmb7^c~H^`6vuyH=250urf9B}rL$%p>z(A0nvpJer0cbLB}XKP*?{4-CT3cBqY{;6 zhg#kmc%pfsf|f^^X=w#opn0s7in_G3&Ft)d`^WdaneV*!{&@4==c*grSwUW19smFZ z*hxnZNe+_er0iBnltayjeoql6eJ=n2*!;UdhwLBfNK8nSlTVZ<9vMXpjtm2cM4|x( zhrJLQ91&)Kk3`*^gQ^37^gPVb{xpd``^xwHUZQ5}!sh^TSU6rZTE)BZJMWccHAPC{ zO6h&mGFphTm7X$6E8z=H&e1f(qsEFB$HS+Q0SfOi(z6`6G- zh4L_RYP8ot%M1aaipMT;vjMFBZp|-Yv?q{7`yB0h&`%}tN&5{u*I(>%C6$TNG#~Yo z($}j(uU)#ex73>Bc{orYTzFfvw*#{1VabfLZa;4n>BTksfSIYY5o}-l$ku@733bor zST-#vA=utGz6h!EWe$4$vq!RC+$}8X;K}grY`{wXHtn1yBYp@ zqyyxPwh+bGshVLm-!3nWyrvx|#>;Q^=K{PJ=>CP3QNn6b{DU4^*z;MwOO>Rr zkA9;E`#M}GjnU@30cqk|1M<~one963$xHTN2%?$`39GqCNh-M8#rfs2dU?He`S5%C zH2t-MlHId_$=2ke@%JpwwR2}V!XOS7b;2y}!+AMCUz$K2$Gy4O2$F>yQbs1P-7{RA zOn%h!?W!;i4>ud+i_>j*QW@@`;wz0TLyk21^qRJ$a#^Mt*tDV2S8YEhIj!EWMPCyR4 z*H!yEhHG#>nIIh1BHzZLQf$w%o~3_^>rX75e#3Uji69k`WRCSq-$VZ4NtqDIvwGWB z0vGFNwmDt{v-)g&TIglTTbQSs>)G87ctYC*+|O)jeXP5{{Z~_;ZLZNrVvcN>_QlcN zE7If)dr;aH^FtiP75jID7K?Z_!^(OSu{wrT5KP2+neg2^)HNs?mUMm5{xTXQMU#K0 z)7mmiYaa$-Qj)c|@ADxcezGLja1eunBs)w)2#uCr<6NmKZ76kRN9^4i$t|OdN7_s5!qT`RL+IR4;ZavGEqO{+6RJT`ZYx$SG!+W#Emov5a} zL`IuVzcfRDPqSs*_pDzB?u&xqg0FzlTpcLq&T>;e>n40nc~_~5q70yV#W#MaLGU+n z)FK%>gWN(5E|k}<4u%c7fFoWA^;yYK|4(JB`GGK82X0^Mw@Gyyqx^Y)5c5M_9uM-{ zV=^V(ciEVgB?5UdSIfVoj)~hMSpwUQaJfqONDI1zy5+g+57t2*FchNI9%YI^Kd@(K vB3D@OuZn+Y_+J~sh+Q&>)_(Z^JJ&UK-aDYNOw^RH9{^xZaL4lFL5Y6>?D-20 literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/SCO.png b/assets/images/fide-fed/4.0x/SCO.png new file mode 100644 index 0000000000000000000000000000000000000000..5905f8dad6af34e87b6952ff5f115271628a6931 GIT binary patch literal 1193 zcmV;a1XlZrP)ut8pQqAUmOq6 z&otJQ;M$Q|EQ}O6+RkwR0Kgb<{^>AI+>B##%JJsDA{?^NzN;MVyUP)>EXUjD6hNAw zD=8P{@VE*92(FTHRSu8q0DxdA$*ppDqy&)0?@C${orfE(wP>h{O53N@0093gNtSYW zLcQ^X)E$-<7vgT)8gw^Tq9EkHL`_o=ve4aJiATrlq@VoWcmm-o6FWK?12$JjVM%Zr z09clVhN>vGE-%5u!BKqsH6>-gx+#kk;nuO$XxX?(+PMM#|7ISzH~BvaovV*o%fl%!iZGmdXp5Rc!LWI{PSb5?*IzbT2z_sKc{z#mF7 zt(+O-nM*-DURM$Y)N{v zC!3ZkIY9uVJ|qW#6k|B^-~&2tze6&m_CIsw0i;o-2-)bqHx<%y6acZIG1PVS;qjYM zU59i`Z#;pzu0E|d2DJqM03%-~(A4t^r|!H*N^UodD;NXL#fGu%>`U!EP1+R1Lq$oZ zpU`Qo06P?vWI{PS#sUD$DaoXAc#J8CN0*XJD~HEu1=xuVjbZ)i7m(d5X@`Pxc*q0* z0Lc`?crs;r7+N6{Kx0h_`a9~S-?p&RP!&agM?H3~TA=J0PB9)pIAo!7Plb~1t>CSb2Spo79K$JUZrBU4H&aK@)(!Id1uf@;M$)7SaPS*_T71FUcuV z6=b?E$8=v(PW#TcCh8eaCR_(VxfRe0N^+$j)5;Oy1tsMtXt+fC1P#i{5kXN&jub>) zIU*=3DL+k>`ZQH9D@OzcCC#ZIuPa9c1tsMd>-Y2rV!13A%Db=00000NkvXX Hu0mjfbXNmZ literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/SEN.png b/assets/images/fide-fed/4.0x/SEN.png new file mode 100644 index 0000000000000000000000000000000000000000..5e45af0a43317d27c0985f3057e9c543af0c608a GIT binary patch literal 1041 zcmeAS@N?(olHy`uVBq!ia0vp^2|(<@!3HEb(?2AGr~;43Vg?2#GZ1D}bzG(f6qGD+ zjVKAuPb(=;EJ|f4FE7{2%*!rLPAo{(%P&fw{mw>;fq{9Rr;B4q#hkaZynTcNMcV#n zaxe91Y;bb=z}U4*pyo)cij;8SV#kXaJr|E?96REoH1S>O!9z#ZN{g@Q;qK)&Td+kT z%fUeJ%86iRtpE*HL6+!Ki%ViA1Z6rd(VbKM_-EMJlxOE={ysbBUU8bp)lIR?f@zD- zzuj|rY0zAGhF2v8&diZKR;z@Ki#ISNMKBoZG)Ra!@Ng@%u?ie!>PTcfLRh%x3ez4t z*0s!TuMCu&vKV{geJeiMHCpv_Oo(y_T`hU7?9%xzCATcbtGQaY=DL;i9|>p|GP}f; zQOKfpPwiu{!rF#YVW0oxXR~uh6wmvoxL}{Q+&fK6eb-YfWRuxUteH-q*IeH;eeTB= z!PfpMOj^-rE;Q|Xxy^G{+Y*t0t7&{U!|twY^O(rUT*^@ArvBx}%gBFqAI`k^@T*%h zWq0wNhFj;gwk`G3w{Ba+Fnef=*_*>dM(?W|;h&BLgeSRQvod^FWADWIp;)7uiFc{(jro?6A0s#m?QC{fG*qXv3$7M_ zA0Oqx$S`5zlh(C2_`Lsl+(>?NZiZ8GCjYrNg6dCMl-DpY7`!k6+WRXjqIx$$8S4MG(P#UwA$r!>+PpI54{at{$ssX zYu~xo!51t=q$d?kdtA}S{nfmB;#@1;KdZeKvhG+ea--{QqUqC3!OK4`I>Hxx=2VnK zcJGDN3sizrCNDqst03jL%alctvMqH?|Jcb(*uZS?%sivzN0-HVi{HTP%;4$j=d#Wz Gp$Pz*4!Jx4 literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/SEY.png b/assets/images/fide-fed/4.0x/SEY.png new file mode 100644 index 0000000000000000000000000000000000000000..17c2bd979671195d62fd7c1e4c5fa86de3ead967 GIT binary patch literal 2175 zcmV-_2!QvAP)1yeZ5#{)<2d0FP(vDG1xnlpFR6lp;|d7Iuo#L5 zcfFpS*PT`JN&aUwd+zc5?wot>x!)cMs`^PYWVl0I`R@is7W9aAB{g&d>w)TlkKCes zF@k^na$EXMely?&K8pQV7U7E#6y6u0@acePH&R0_FpId!hcqKVmil;o`^%6i+7pfF z2c89%0ap`0l2#$;)hBi^c43!jH?-k0@F;K~`D1BAz^uEQDgDD?(QasiA6Nx^I{c$) z6M|}A+=?_N4HC9$U@n>_AJXIvc%kfd@|Jgqb|X1%08autQu~_Y379tSB0sCF7wtxJ zoCa0`$5a2R<3rGa$}Pyb4h}tgfp4R0^1OB14SW;JCLbI`fLrckXVp%)O|&Nrq?l`g>(&nCm=IL6VLPMd+!XBw zoCj6|2kjZo5pTf!DUB@MbWF4xP>&jNY?^#<00FYp$D7qJBHb>~iSWhABB0IQp@oN_ z9Y5N|xWyI++GGd}@Ddrh*E(i@l|K9dI(pW*2&RxJf zgibyPB;dJ~|K#ox17$?L=mvfQ_y7l7FAE7llSW@-!xj@tiK$`;ZH`C?c?0VH@@HHV z`s^Jvi8eI*fcehp|k2mp>WjF9Rmdisd$36EzaWPa> zKtVy|V^ay(wB#)&mzq>^3;`QaE1Btvi4$SvN_Dl*oeNTY71A^UCS

    @ugaGjM=Qs zF%@^*0ZW&vD}CWYkdx;*rV;SY=1N=>O}J#KM@=zpmWLR^m@)9kBkD?j_+c0^Li1Nm zAz=M?-{*nHFBmh@4{Qd03pns+0ddI8ghh+gmA-sA+@?=cX_$a)&rLROf5X@%i(qYz zW@Kc*;>GGpU$FviH?Af#Ou&KPSK^s&Ku+!eZBA0STrg*jy3*IIf$VHk%^M=XJJZkX zU!2u%U>EQNj*y4MK$g|4#p_ikJvY~~p>z|F;SRC$m7Sn{zZpP+zB!ISe!jZWeLlGB zE?b7wO@Qv6JxPpH4=<3-M zPBEVXY}R8%FnO|?xp=)WZ=N70;*l}|DfjFVsm+-%0amR_S_%-2ln6+xE9uE{2sw=&?7)Duf=V>1LgF$YbJ5B5P0H>}f z9KIf)ruQ&M-2-(qO$2D_*|Qf|hxzi5a4Ep+h4OO9$uW5V6L9Mm-Dgj8sac`%Qh+0! z3iY7?weFLIqSHQ!xEUXofMI*~gfC7uqo$i?Ai5M#S_*gGX_8@s2!*)S(#+L!0nWB6 z918~6-{a?P*C+HygG#s>gL~4)hV9v7wl*hxD)sqb>Qs}A>O*ha1v;7(np*>$xTf%7 zcYr;zpR3Z1*arYNm5b=crSrsG()uI<+id1Nws<+&I@r>-hjjFZwyurSLW< zIOb}NebDAMISC0EvS*JGZO)`gP*P%jDWIo^?oS&d8)8GyPhGD~0T65?D-*s~`UZO+)S>H*2bQb6>E z_|X+V4Lu6`WQBHlknq>R6Fn4ST#oD6qg9(TawIHSqOSD9LfseC;SI5=*&oR~YNVsI z$%AB$X*khCA?9C+>De=!HYfUmx}-!c4QNDvM?0O3N+k0*)TOYGV+34-e!l9EIBg{& z1jO|0sVA{E$K`?r3)Gdar4;bRhS+k(A32KG^(y?w-89Iq(SIX-;fAQ5Jz1#XCR&>l zeJU+0gYo0ztr$3pf7#)NFM9TT=enBZgta*Z1?oy)y&CfJB46ljyTIjB{>V|hp+{k_ z>jXimJAvh|FdW=7@>*D1eh3}{GV)=Q7b+^~&B|0WkAr`YY>012N4O{tN`=0fXhV?d z%4hcHdd3j^#!UW~d6N&jj4{{im$wt54cYYPAX002ovPDHLkV1l$F B1~32s literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/SGP.png b/assets/images/fide-fed/4.0x/SGP.png new file mode 100644 index 0000000000000000000000000000000000000000..7d7c4f9ce8a1d5e968f29f9b586f169fcf5d649c GIT binary patch literal 1533 zcmb`H`#aMM9LK*K(`+uwbyjJQLRpwfk5ZbuHMu7mG72HohLvINL`lS=Hp0hDOc#j`+kbhN>_un@98DdMa_ z_{G4Y>BA@hfLy^_nY&SMPZbjEm5IoXjfL?{2SP&iNszY1eiTkiQCZcW22na$hqaS4 zx{CAnsC9NnTH1TD5$a~Ikg9icf-Ss6J9KoovsGAjvC%1X4!vMr?@#^Mo&>A3$aIrd&0}epY1ht(FqoE`4p*61yvkUkB)T%NK4c!2QwiP+`Y3xq3>~> zbg>w!py0RIyCpq0mmu)Bcp3zHgvP~>Ba-jgmIxaTh8FRjt1}m)q|;K_bfjk<5F2Uy zhDdC_)k~Ty=4WIm>xwRz0C=MEB%1^y14jGO(cf(X$lYgtO|(`2zIl6=&2&j0bsoRA$oa@ zQ5y+ISf{(X5?)>QL;QeYhZ`PHXE!D$61&5)sr^Z3Nx{J_!3FS%QNPmX&(P&h6brfB znj(jtjFfz>Wp@q<>oIYZC;Vin{|o`MX~eJ1Ny{IvYRJvq@;tB3sD>m4K6i^iez3>@EoL0 zB9T>x_O!Fz7$%pLa_>9(KjSK`5BYfBDQWa^FR-^#@#TC_74Msxy1p9c>BWAW5;Zf# zVW>cT7pFe3w(sm|jMLxV5s>6QqM5sA7fkXEHXN=Br}Z({a&@5`Gb&vm7CWh{wG0R| zCKEv8dU|SG^Im_GT$NFdF-R4R;1)$_%F1F}C8k@st%y@`7aV{1a;XP4FJEmvuloXL zs6H)EY7>BO`MFAy{v3{gn4C0s{WIw9KX#FQWpH9^z%!yeJk{pC1YTWWp#cQnz;=`(nuv`hCMT zJFB1_99w^mrudnc?$*ITMdXXc!@TW@y)uHjND-Ob`WMAacM~nlKrYJkyFlPnkcabj z0o#;&CwhA_t!Rj%b0Mx--^w30mHpAldl{NKHtp?k$g~+H_wv`OMw#nJx@8nF_L zVVb2>mVgT1xICKtno!-a^+uoi26KlB zZ{+SObs-?YEO1ePS@k?&ym5(JtHFQ#cZmOJA7}BXCA#4CR6vZp2Yc3B5XV|+37*4ABkN;OjjRRVjev`>aLvI z247NKEi`r-8v`+7K+5AYw&NYy+UE>3(ush!**J?VYY9Q5!q9R%@Hy)PHp|;yVgA`B oHqhb(N-hCI|Hc1}*8dAu<+&K9Av7V+Kr-C`-rB*c!ooN4Z^{X}GXMYp literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/SKN.png b/assets/images/fide-fed/4.0x/SKN.png new file mode 100644 index 0000000000000000000000000000000000000000..db7a3cd1e9d6cf83d4c31b6ca95561601b51d220 GIT binary patch literal 1852 zcmV-C2gCS@P)R8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H12FXc8 zK~#90?VEjQ6K5R9KZ$AWMNCt%+E&xC)tBllLQ%vH_Kzs-+#j2vAi@TMFJp8dh-|Wr zD#+aIUc`Z2YFTv_+kEmUZ-bSap#66I$FaoUMQUL&iLB4> zWOKm-#+rwMV*NhYU5JD=;eATX#vpR^+Cs0zsuwz5w4dqY6VE7 z>x0IjZTH3`(XN*=ssu2K>x0Im-=PwVkz6k&x0IZn8WIWQC%-3B?ZVZiF{h{ zkS_}!kZB68wDkL+`X~5qZ{l1(%&gZ_P=IJ$f9;M5QA}J3%xs2L@4*l2!JM#BsEp;M z>o2|ePsrr=`{1)5;jNV~QjRE!Y~8vQtJNB@&zzhbN=r*s9cR&0_{#;Z&%v6NA_B}2 zJe-@|#@AEth1_V*w?XL&_-1!-%E`^mrM0z{S+iz^?X$eRoU2!_^5n@ARmNyG!S_c%woKIk0c z?ma=22p)D9-K5m|JR}y)3g^U$6ZrjpY&IJQ4<2O8mMxq+cMg}!g~Qqkz!IB4Qw<0MSU_CiL@b^Y11XDKTy3mfxzJmlr&@$A_%MdK`*3O`o|r<*|E9a(g2 zbMX9zkX%=Q+Ut6^+bt`=p+kohO@2hz`%QEmn8<@Yd5D81AW2e?7EiO|#*Jh8^y#wk zyLa!#JkDrcFU9aFNQjgoe zy1JST8#Z7zN4EgHeEBj?r;|H(?u3mialI5n3ZRAS^YZf8xpOCT=g!4$w<8Dwf*?OJ zQo6glS-yNZCr_RX{1%Vvr5H+pL{u(k&YVemdpjo6i-~G&ZDr%ejo9sWtX3<6AdsD% z&DgPH*|lpIU0q#qyIzW6Zo-MGTwcF^owl|%N=ixqsH>~v`0?Xm+YUd?#_M`12^YW! z_4;GSj!{xlLVtfh`}gk;+eS^-OG#)cLP-rr3*2ru07s7=rMI^?C?;3Kffugt+?Pw+ z$I}%}J|yT0FoNr)oIZV;zP`SY(t}p6m!i7>Mtn=<^ZD4fZ(ktytF^+32HjRcMsxj0 z>~=f7y}i1)UWzVPfDv6klBZ95RbSymgQfx)-Sxxh?RqJi36Kn~*E844p}GKR*T;sc z0;F9ZJE{nfcD-`q79j2V_)siBEUqssEacw3dvOm|2}X3ilrg;P*c9uZ<_I4CoN=8G zCw5~QvGwnK8@%%g96bfYo6l-$YRJva<;s;SvVB%oRw4)jU0q$V4ks*TGpv3ee*Ye( zIf7%N-$d7Mx!nBfHGJI}svJ)#FwmoBBQu8x9&0=948 zj^FQBdLDqWbG;m|tOcpH!nt_yBJ<|W1K|Gs`&_ti0jJX$h|ixtpG%i6DLjqja=ny@ zSAd!;9JkvY5WwMZ1a_YRT)TEn@#H6?>!pN!ArsG@OF5dGn`L8-jg8TFBq7qCOHW4@ z|5i=oeto`SCm+HHpv7CNii!#v8X9Ed<>lo}o;*4FL2LY$N>2j!04<(oZ8jSmA<(hSfu|!YG?)aK#8yIdQ~nfDk^AdYNDs7hy46}1_lO#;;EYJ<*?{ouRnF_6ke~F0|yR-On$1ZaH7Fd zx1T`?)_(w-KZ2~x;P%7cQg!}13DKV#u9sqh(3`NqF5^fVAtwqqzWS*GEQE0n)C2HJS*Jc761y qE$&SI+;9P literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/SLE.png b/assets/images/fide-fed/4.0x/SLE.png new file mode 100644 index 0000000000000000000000000000000000000000..7692087a3c4dec819d21af2ab07c7390abccf1bf GIT binary patch literal 319 zcmeAS@N?(olHy`uVBq!ia0vp^2|(<@!3HEb(?2AGr~;43Vg?2#GZ1D}bzG(f6qGD+ zjVKAuPb(=;EJ|f4FE7{2%*!rLPAo{(%P&fw{mw=TsOXHRi(^Q|oVT|Oc@G%yFgPy% zvock?VbhBlhjuKi=g`^v=4AG_Ih@5W8UL+gh+y2nl)!p`t3i}On<0#04PJrh@74BI z9rn@B=g4hd;3q&xIf{YT&b-rQ&0;3OHjwr=%xgAsFGyV{6c6+lgQu&X%Q~loCIF~Y BUOfN+ literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/SLO.png b/assets/images/fide-fed/4.0x/SLO.png new file mode 100644 index 0000000000000000000000000000000000000000..69fdb420374eb7f24ffada32e30c13cff4b374c6 GIT binary patch literal 1501 zcma)+X*ARe7{>oo$TqfS?6OPQ4U*-ev1cnocMQU0Uq;A8Vj9_(j2UZ|?9CKS_Pt0X z6UweIvcydicc?5~oqKK{?m73I`{8+?bDrNh@5f(~osAioU6>sJ0I-F*@x{Y-JB&Fi z%i(47-a!AXU~^Mi3<6V{Ni9F89dksnQlp3TQYm@IG2ehO02S>O&JKv22k zQB&JD4qnlAZf-?yt?dpj&p2>CQ_Wz7L&Xd)o3ZnxLL>xF8LD2yxs?N5<{&)b2$M83 ztLY)AXyed0qxIN(%av=*z{uS~$&*1J0iQZxJ=U$r9)@0 zbW1zyKTg1zs*P}LX1Pds-&B6f{&Etv)RR!XKdmnvvy{}Yi_ERdt#uN}FWm|nH+}gL zj%@H$Zmj|CmB6N5t-UBI$HWC~yC>)Mn;rn&2)5Hjy2KBS4H>AVj^+8I1>{ZYZVyiw z+}B{IuLT6jVl2#2P#5|!8`K)Zg|_EIBT;MA7BMF6O3RG5zVK?;@C6-b1?@t^uGQPy z3yyGEU6r~m2B$a$MzfL!-63;Q~J}W&bVd%Ark3oavNVq-{Ft*ei_*GX7 zrgb1=cBy)CDQ_;kr3e*h48E4nCQDayz>lg5%(rqQKF6f@8)UsmZ16uDt=2lgXfNTn z)5)&u#IumN5T0u@SRBPwN&2{1@5bWIQ;IIC5BJ|OC|1quW+{p4e?K^Hj7Z?59`oI6o>#8* zk}09EWAtELI8ToF<03-m{7Ub}7SElx#P8-uxJLO9TyiM0$>ba=zffFX^LH+)&*>=@ z8xJc#nI^86^z<+LLRnE254G3$wK}!HRnII#qO^-^S_+Xv(L-^`Ec9YNNW^*W09jSF z9FW?+(7j6y;!o;*WOjxGIdKZ3rsJvQ{=K$>ysxM-+yFT#D8$iLcCr%0Q+3HBDP0p9 zg?Z{xyg?u*p;9A(6K%SAS$E3kM&FUk(BJ4;{d#}?q5Ph~F4y5|^;%^4J!_a%Q<(zv z*yC--SyzI)-$DCo60JQ@;!E`s>Ha2a*gDkeW8d(!hfM)lQ}gl!_&`x7CEkem=2Ok} zryh|XBE}_YLCP{WMtDyY5>qFjw4&~K*IE92o8FSYeSw%ZBJgiIbAxV-|uwaElH z`3FUE+W%xPx~g{~^`v5Asr}gKu1cN%58rWUjRyhETj4hs&;#OdKRv@LaR{Dpi!!>J?>n7}eF|m2Yj!W#(qK}0pR5PvoV2?RL@#=qoTA3H77@_~;kQN O&47i8jd9I6H~e3+tF7n& literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/SMR.png b/assets/images/fide-fed/4.0x/SMR.png new file mode 100644 index 0000000000000000000000000000000000000000..415e146b1cad990e300f50633e7917ddd3fad2f2 GIT binary patch literal 4285 zcma)=XEz%T1I1(2R(sW^L~Co!#EjaTqQt1g-hxnjuNtBDYP4FTsHnZEU9)Nvo7$sF zr9AzAgXhJ)zjN-(`{tf=6Abj!DalyL0001`rUu05A8r3hgXG>nCv`lr{|{ao7QO%g zzSMuhd#GH-|1V@gsF@>-Jsl7Mc0O=GKtOV1y<|q< zzYU)P0<3<2#WeuvnG{e3uy|xVRY7VJctEw~ewsp^ruE*y^v!AwyK`PFV`tn^!|Z3H z)wfn6#W<^M(@K^? zi)TE>QDx5bWJJNYFYv#xGvB-T^=r+)^-yn%QWj1G;i0*AzjJhWEc=v*@g=PiHB9U4 z6BMrocYC(QhC}f7xRcd{dNXrxJ%mqFh0&K@GLAXlqm-`--PkyLgjk`T|5rnNLIP}i3oLOhW&e;ywR~jdj8v}_@=_L9^Xtlc%BDN9rUF;W9JpYgu$(R?8f6mP3-t) zW+8!z>HDhYuF+CQ4D9A+KJ2KO!Vi(wc40KSK_k+(%?d(?MN6?+S~gY+>~|s z0Mvx(I{fy&{GMZiiv&GuV)n++E2`D5@3WV@`g%rC5#GY9B}vQ8AI>VhEDqhWU`=(lf5?pePu*9cxWr9_995$ zTS`OYHP)Jv1p94^WONkktTQ&HC1x_D%=l4u&aR|MYlRF-R;#VxrRu<{ z(MEYuXY@*>u?v+3jMggP&d+4#`Dyz==RgdYX=-v}G=B4??Gq)CI~SQAE`qVbC41w1 z##rRNAVX?;0w;IAE?retS}-@?xeU#xK7w2U?*~*6ZS=>Fqerc&@_#xK`LsOQCqQ0W zBy#}>PRIJf?re@L+BjLA5d^tn;Hg0HK@|f<`*sDvKh_yvo;r#AHgmHmjIM6np|E_I^!(-*QC`Dh~q2B6>tnJ3*2Z4TA z)1!qDrv1g)^K)LsfisKv_@y69JzL!*rrhzvulqLifELL_B3wM5-l{EX4SAKyq!=4pX>mrv>&>B_f|d!%*()rtwU!nO#NFw3_bLf!RaMOrnIh%xRl0;`q_Az{R#i3 zY59UM5NN%MBeJ{8@ z2YJK*Klp&!gXf{Z)dYS-FuE(H+s|q$Bz#tCt9h_7Cb~Z5Hjm^*Loyeslk6Mkb)3(I ztU}ur&!UU%J~dw=Pt?3}w4gb-$TneQ{I#yM&&~q%3(_ULIglUG7WQkwDjJLGk`oWC zHaJ7f%3ADZdz1MlNwh0RICPSKnC|uOP#w;hTb*NWMMISc*9{fH%0Jcr7kO*sspmR7gAsGi8|7q5}v%P|J< zktXqqfyuNkVm0r92yV)1E$aEvW&kmaZo1AXeLDsIiMk9g3m_s?OITnn;PV`Uf8M+xB0pl zXzTAZhHXZ(I+^C56_xS(DH*HsG~*{-smB5pccFOClxDlu2o1f~#rPcBEkHyp)8t=N z!sWQTq{9N3Qd4ja9V3A7r3)Pw>jzJK=*)v?Lb@BNQK zHnHV?dd9a|EVQ3_DmGz2$8;C@`0_nmkkVR``gS9ut?GU1Wo4s$1zo{23XW9)YVFss zOy$1tn9Ryv7KJ#w&l#ga(OWcPPvzKHe&UD9^(X{cX#(<=S^v#z2P?tQRJw5C^UiQI zu)s!AT4yG)l#icVKxOmpZAiWnY)-%Fmo*qtO}rjtBIx`$g52 zf}{_ITz~UqTZ)gKm;3I^y!isN?uhyO#{xK0+ja>wP573H$oN}x40l7OIXR2KJ_vnw zofS#Ui;aj*fhc6#J2}O(>AhR0KQ#SIKUItQuDYFKsO6ef-ga=Yf=)ty~tNGCf*19~CcBgq!XqbZDaFgqEj;b+u!@^7B(&rwWO*Jl_W{4i*D_TmbFXIQ%!C3&!iEjEhG0j&IoGw4+h!uB4qHs># zvZhSedk&4vAbH)Xxp4W!L_ZZ3bRATm*UqKl+MKH{I*M_yq^UeHi#EGU3;Sj>`l>24 zbiaix^$iaQW#?r|M^bz8i+TDGFOp6c@d1M5(=BVs+tRh%8<##i>|!GMLCMt$cPVQx z5ah&OD?M)6R`J=Dp-)$_!fIS%J{lznKdp7A0#GkMMKVHTSrT~PK{unHtv^fitkO6? z*yQQ^@t9?X5!3&Rf;^gr>PK;^+mEFfWlj{kS#V>T5d-(bsI zg%(uT!+x2{C)y8WZ(EVF8O=#Kf}ePbj^Ae1d5vITBZp2XaC?^&o`Zr`=v@eX>McG- z1}_|FCXV)}b(4#eZ)xy$b4-Xh`Kq&1SmlSya?|4Zu@DYpHqmDoj0MpX=+)WXFBH{5 zowk~L9BnEbO1f3fE4AOsa5AcY#F%g}JjbFW2$yRN_g9i^WFomLbQna@yXN=iFc9a( zpz!q9wwwJE62z0gLTiNv2?VdYGpKl=+E>UmYre{aou3^l%ZTW4Q^i5a&-^-uN=CW` zYV0$-#$Z!hgavuX83rfwTfE4lsn9n7C%@m?;4?Tm?72j4@NT|YO0VSm_7Go%;2_jo zW<_SAQ>=kBEtp&hLBJD_4`f8*;R$JPWKTyW_f=fo2sfV2a<2AC|2*y&hhi77(>Mv_ zQ*DRTk`_3m7gpAhYnpJkwst`r*mTSBXEXac`gf@HqW~XsXXQ)?p#_ARh)@%P&?3PP zkFnh|j~uC%n42R=^j>o3e3tSB-QQ3OzqH%@poBm9ae%!oE!VetJq}eol9B4w=r8^o z*-0dxuKoITvW(euhkYIls8Q$#lrf^?6)dF>MdBs(D7(1!3b{Z>FL_Hra(O$l`ob$cgq2&Q z!6LbJS#W+)9}OEN2}DC-Il(!wgjZ6lF%xbto4j|*i)@iif?Hp-5AKAFh9JI82kJ+q{ zJqWtl5#Jr>qN{M;p?I)boUCUIq<8YP96gydyceeqlm$U(Sx{pxY%kK)5@S)?Q!6Xo zySsL63WavA;^Na$=q+b&ZR973B+ERDj;f4#^8zGK_nvlkf!j_@lcr27%dP6mM z^T((^mZOt3nyvUv%tpe%caMsf3b}J>>XvAJ8gkz(hyy!?-2gFn#y*2?7 zMT?o@3aw>vW`9!8uA?4JciekE%3+&3yG{eY^*33RAZ>IUXbwq;cIo-6p5%5e95~_& zLv*ZP^K)J(s&%#$jDHBzn$NQgYU>T-Di!hlq^zGQ7E#RRQdV{M3-)=HyAV7E%wG^JtP?yhS$ znnJtkCT%srHZ)BP&=;w7HO3FxrQK-CwjWFoH6~44Q^8R|UKAY=c`3ro?1$MqFfcQB zhPgA$4D?6%|PilVDPk620E$)4+g*+qcd3HmlW2RaFlkePqJw#b8w`Vdu_7 zN=p+Egt5L11{EN@-P z-@{^Yb_tv~(Z`Y{*L@Nn;LMo;GBb@FIdb1=yTt-kRVH$Bu7{=g;N%Gy&-wEiEL#@i zaICFugn9E?yb3u|D3x&M&LU>dRyrJSXc!kGhW z9AWk9o0ODv(A#T9r4o4QrSRSpxC}v;F8zk?ZZogG+Ch8!C;;*C3Jx4dX2XU!($kwU zo1I-b#_{866c@L<4OeQlKv&m7>g)U2zP*bF4{Q^U!JwkH)__*4V9lDFQ~P!yfKvlf zQ&p^AA4g41uhWYP3iMpLGR#ju4ZHl{j0`nK;{sN$yg_5*kjwLOa-vC2R#9KyKl$ba z*uH%hN00VlGD)}{qPU%eePQ?RMBaPv5kCCT#Eu=e{n15O;88AUDu{8ts7Q}8yysD$ z@HxkvlA>bo-Z{*gHRh#KDUh8V4cKD&S6`*m-#@mg7#y^+Z(lb?N=iET`=%|(b`|q)8)y;_b zeSpx$IHsPICQ8-&aT1&*(c$ZE1mC>SFDhC_Z;dZ&%Q_Hjsu<c`P<8%duG6zHCi@q zlrE6uubKf&x7&R>ZJ#r~6IW_utRs;>BMj z&kGfj5_Fmf$!=fD0JU16vvVOjodScQ*=x{!_z zTguy2Jt7p45?(|y1g%@AB{5NnMkBCkllw5_PFzpgi;DCp6fk$LnpLY}rEV=KNlNHp zPwB^8JGG8~lz!}1e3ENshdphDL;$VOvz-zB1aPVNVH8Y3JNI?>cnG^5rqq)upmk8%1QNM~zxw}Di_2_iY^=cI#gku6nLAfaTAEtcH{>3BOyjhCnUxiVMiU%w$jL3d zh|55~aifmtXxq;9>#zGcdbH2pChm6SqOj2Rc~&cY_F1p}br+pZ!E?_^XITA-x2u}* zSgdfO1%CS_6#i$j_~0f4ee+E^%a_NnYu6pBt8G_4|9l*W4y9l+naR$+KDBS;gy!b? zs8j-_rJbBTJAfd-)~)e;@If-)emg*6VY_U7d5Rn(zPu5domX}3I1NEbNlJ!>tYl^y z?FD(TJqb@dVdTJpWEL-u;>s1*HDOO#x-^D{hC$wZ^R|7TV6{SB-F?oU9iXx@8NFUX zUteH$WxfbMVu54L@Xrg4I#8odKaDyhzv>5Ng7UguDTvD!fR@8W8Hji-ep3;Uc_9$QTfA!ixmG0pTjVNYENE zTYsNF7as{*;YEVZ08#kA{H~4!-PDBN+XUBwTzHY7F<_<%FEZ^0%v9k;rpryu5g9Kty=|AZdUo{H|AOnWgtm zy@>GAxHQ05;Xl7(d!gx;Z~s~}?3rXhMEJmBHy|SX^q`6eKfMS&|J7N&UDXVmq2_0J z{}Qx&_nU?Y6Hs|0eq_YUk4O7}i17ZytpO3?{m=gZY9=RH1$I7@00000NkvXXu0mjf D#^6wC literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/SOM.png b/assets/images/fide-fed/4.0x/SOM.png new file mode 100644 index 0000000000000000000000000000000000000000..2b20f437887d90f587028db38e350133bbf0ab81 GIT binary patch literal 1040 zcmeAS@N?(olHy`uVBq!ia0vp^2|(<@!3HEb(?2AGr~;43Vg?2#GZ1D}bzG(f6qGD+ zjVKAuPb(=;EJ|f4FE7{2%*!rLPAo{(%P&fw{mw>;fq{9hr;B4q#hkaZJ^h0NMH=>R zGfC)->bUmEC9qR(jzrKRL$`p!hsWmHGYGc}#9I7eYhRmpXUF9uF5%~r7fBmx@fA9` zbP6_FT;a&tvP~~e>ej8STiX+>7I%YBw|H%_A#DJN_-_&)S5&Z5Q2~@@ZrDYnCl%TV}1Y?_b|P#a7h4U~WSV z?_B?$mpi-;+?O-mDd(GaAe8B!>5Vl0b9oD#9xdx$o^H0}JTqhcyJF_L>_e;@R(^b6 zD1PnGGgkvSeVzF!LV0@{_3gQKpW}34srkY7`&L5biJv!a&*HNFe^)|gm+s{s400>$ z13xiHiy!Dyo1t=nchZ5IbrzSn?{lw_P#0XuutDm9kNq9lw!MKFvjX>3^Yc98?d#Dj3y=9v5tL6VH|LsvY!++>>gy&^lDP?Vi2klcW&EnXS zHar%RshwcCvg(iAwwMil$BTWo`N;M3ifqU#dsgRY!>B&*z_soR)1U9P`#a~&)3&dI z+2;?s^oUIO_iggM*TrAo<*+4PsFmcqKRcpe^~xqOY2#NxNpl(!L`~X_tG{&nw;ug5 zwI%rc!q+xm`XY||)bdE0bCjQH4L7v7!2Flhtj5eadBg8nTX$UFdgRs>`Ts9{=J_Sw zNs957c-J4X_lBnvi5-ioI`zIAj5*ADBLSvwZTdkbIJJJ!qiUz)Tezn^if?kRCw TnN%xaZf5Xw^>bP0l+XkKYE;b; literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/SRB.png b/assets/images/fide-fed/4.0x/SRB.png new file mode 100644 index 0000000000000000000000000000000000000000..f1d2c048808c1ee623317fd7d21cbd5bbf9c7453 GIT binary patch literal 4048 zcmV;>4=?bEP)uPy&>JmQp$y z1}N7I(@xug0Rl~0N{2FZN~crWln!YcAgRec!HFSOBHypNukPCWdp)NgocH|C9`W434T=>ShSq3-Uv*XjXfP`QG?yPvorv;EOk-xy;>~`w(K^dc!+L;J3aJz? zc7>^CGzz;6MofWJzgp|UW$OV7@K$FLJGK+*T7|D{5U}dlixk>##anoe^oO^C8AVAA zRkZP6ISz*oW63LbXsUYvZYo81*G?99tfr|cLgD=5OuqaGl&XY!rQy)MXA(;V|ZJ-He+(P4UpT&`%sglSMFNfVkvZUnmS*Ox-{;FCA)xwlq)3@-(Ff zAEHwD0jWE$#u>P(9@-6|E&&ALFwqVJ_t-(AKZ#)uMiI^kYqnfZX`pz?wJId>d)Fl8t z2v~Uh2+>tH;&$6e4eTcaEn9CwE$7p%fzi_7d^# zO_cgw+HZC^Je9zSD6~CQT6f-ndu@THewQQXLYTh7x$_Qkov>J1Wc=v|F7Wb`WIY^6_?hlB;K&e>K!h@l!0t_u=>DUFz}KJv(F_o zBPb^V(xyqgOHj=F%pWr`QwInf*mJbum6MuN_fM2Pm%w{Rf#yzyzuG1d)o~LZQiAUZ zf{=@aMqq+iEeRVS(mqB+U}(@X;G%0_K5Jn~@qfSJWk%g=K{}8h){zx~8ScTZN+!<= zDkXuD>Oxo=2S(GJEm(NbwdjT*KOvYHQ)uCC!kWg^6d0!?^;T^N%TkPcXrzPh9__~Q z<|)|*`d|cRbfi=WU~sC1=}H_+m!N?f3S&+g*iMnW5BEHE1BGhCE9Z5gu9NVq%o~!H zyGq1k7L)T1fl@TZG;A-x)HD#_2MWiBR77K;9H5jUlZ+5IJuWUv1p*Y3K!J7Pi(mTNQnge7N-aIEC`sf(2UY49kNsb@dide#*;?u^ zJ2jt#t2496<3|BdZEd9A{uWAS&QX2x0OHyzYF!VJfsfHrv6(;c1o`I<5-ykVgh1== zB6IUT%xkVj<|pu^rMfG zGJH(a#3)xVizR~5aavkhuq+Gh?2oVyE;4+;Wah~*0MkEs7=XUt`4H;r{(5OIJ9P;_ z_N=G-wp;$o6ABGj7#0*Gl0-bu6aVm80M_if8qvR(Mlg@$qpwHui9KU>E9zpV6EPqh=KLNlE91%xl+2 zw{NB8>YV^oPv>a;aRT3vB%BMAFpzoE9pv+QbX}+O`s+B~oT`WVvQw8Hkhy6u)oPVk zECxW&$L_&TB>)hCAmZxSV-v4kZ}n{@nOO?Noy?&J5OOZ68TdQ{e15ha9*({5T3s|Pf@bEd3YdTo-sZS!ci*J|MbM0%7H{WnQ!G*ov%3pZ_$|Q`1aLjH78fzVDOVvAx#n4Waf!P{bk{i!nSpitD)eQZhO=%7!=W zr+DfV9wCuPAbpP$M}}x=UyW;535)4kt2cz2&jFeclu9K8ibOPqt^~Gi zBLqy0k5aCbF!tYs>f1*4v!A@AZFv7pB=5ML@cZt<6N21S4qZvSa+#Tt5lqiV=o-@Z z(KWr+>J6dxbASL{*APlzSr(RM5Q)Z##bV5jPaqYzFAqVsmFnhAmo(Khs2)E_8A(_v zbOD9A1)|9$O|7j^s-OaiArxp=#@;~9dw}nG#A9)M<$pmu(of* zYs+H%g6}YhhKUXA#uEaRAk*1Nx_uQ|D2$nI!Y!8(Xt;$^t<@Vs%>xMQ7BeFw7-k61 z_faT>5R9BZhv(R6O5q>+IW3V0rAHpUq;2KU&k6ehJpC-DQiMYxG+jeU@BB!EJ(NWpOkha>oY!0hY{`7>vcu8Y2H3%y^s_qFS-&{D0ZrK=0$ z+kbuWg+30C|NBE^GA+zaPorr%nx+v-rgF znx^4fA^ae~428&abiA&OZd$K4O&hu9_dmki*cd}!{wn^3^8tns zMD|@zsZ>JKm;Te^@i>-cVLLYN!Xj5f0CnlZ{^T_AOf$t|5kK&eQX)|pfH6Ibp_{by zZzuJ}TM_3+mfF}=tN_~IcpH)3*PtEz8HN(rR0_%nuGyoSPw# zl4vwaGM>P-ZM>?DoL*Uo{g3#QlL#SDuO2rN0y~#OE-aG0ZV%q7EM6pn^k086()WE- zX9rVlX|DUtI~o1Q2XSm0+qQ`(61a|woSP>iuZ*KibqT2^h{>wv`%8ZPW|JHXX%Ye1l z4sdQ{nAn?cqjY2lB_)pQ5KktN`K2=kC3X4VhSvNO6oiSAl2j^%@_fo8ql`WHeb&A6 z%{Yr^S(uxrW7`%&8+$0;x{oO(ky279l$h%7r1*}v($lw{`Po^L$r#yP+X)U2asK(A z5)OsI^N`X<872$z@(;i$KK`YezTm=hKjoFb7(ppTDK~}v{0o$yeV(z_6Qno2jn?k9 zoO$|bBF{XJ_nD8gdh1pWJo-b@Ep4p-+@~nq|4j;u1?;W8gw}WS(z9Pd3J2@H`^ooh zr#dl-XiAgn+rkUq{6tON8$r$20r4%p$aE8j?!O;7pT~dZS+v%9X2wUTzBIxdn=yt? z0?^vEj`{gHx_dS;Gc%19vIvq1ia&V*UV4e~_6UXOIihKskkiEZzxg)vqhmyFy_wFP zJ8G@o5Nh6s&88;WQ%y_`{w-DYea!1-=zg0^r?7}>74+lBFq9%#zYZ%ABb&`4gdme? zArgtv`JQ*9D}_G4h#DDTT}ra1S)=v6B^K?6aXWs7{da#!V#k&7)0DalqUpUi5}yna zZ35c}n0dy+eKCR=6PVtGUpj2vj8tG4I?-qZ(=<>@lJ_s3ehDcJw<;)pFTp}Z5^0u1 zl}2-N9f`iJ^-y1S>Jz~Jn~1ILV&RZMs!dTjVUXP8u<3RWJGpf4-@b1@1Ob&|iBhpd zwNk}#9GY9(@HcJ%AhyS)_cjkN>0{0tRHihXah;C$-;Ex<{L2tfrvNm|qUWxUV4e?A zm=%Pt@~9}u{J5a`CL6bZH7(n=(7t93&FM7BWRiF+N+KFTNXeRyei*UKrXnQs6Oh{L zGCwS^5|UN)lX=7bdT2LP2gEVh2` zUgBG>oKx~p=N%0_8m0FuUnJST9sQ(5Qz3-%!gRmqU6gWDERKxQn$2Q`!e~;Vg)G{4 z?jQ&Pn%?&7s1p{cf`#SA*!;ymxuS-iSE>6_(nu!R^1yv`54;`isW_p|ZldW7#G=?2 zYB~sk^gQ(DW>T39b3;QU`umAZnKjS=Inv#hspE{)wk3 z93I4-nZaAk6WiE>nNDM6+eq!~C)T?eVO)9Vg8w@W31EebT>j<6%bb+}8q7)n4Q3^P z2D1`CgINin!K?((U{(TXFe?Exn3Vt;%t`+h<%G4!eWE~0000O`DViD33T`@JsSjFH6>Qt=-j1ySlsgar(zy zN!YSwOY51{!hU8n(yq>4o%8#i^E06(?92asx|%oNRtZ#Ugeo&cIs=HPe__vDf+%%y!qsU;`&Epn zmydoT%GEpKGrylBz(S1=APqp|l1-bf9~yDz~r-)g{PxN?7Ro-o6c+vn4{cT-jJFxK0y82ru~ z@h&GF2YmFM^kMXuan+}W&q=dn1ORZEPP7xt=sdBEj$dhnHjm~zON&O67JZ}e_H&K6 zE!)hF3#3~zF6ewz&BN*ap?qg!(HIVck=LtdjDInfR0}wtigZc7v#@9amK!pcPPb%R z!25-w0C}b;K%OZIkY|bli3^)2X6STz&*E}P++#ufTL&0B?Z;7)Mv)3D`s?Vil5|gK zY~C@!*KaYX4!`>e8bi@IyZ_z6z`hEuc89L0+bCE8hBIky{)p}O<6|yDz$t5-;58$R zAFiNThOne$RL}6aJI1-TRdT2+!Mb{hA|@D1KxG)7f2E7%2}xs2@Zc?y$tn2TpC}9r zEBxK(4PHD58=K(aTTNo09Og^A$EfOFN!WR1wn_`O59l*ev^Ck=ed(mV((Y}I@XcK& zjUO7}?52}c-f)61exsjjTkUjhO%gS0w^$N-hGEM($ulntzI3DH^N+gt!WU}j_)U=V zqpSG!K|wqrxc|EzXaV*)txb z)VcR23D~W}wBT0^d38W?P`7Bat*!ATh9{gU6?>?aM)c+{(+qH`-N5Kl<59u>M>nU4+)v+qR`ENdloOmVVVZRQR|VfM9pwX8 zNhUPdvEDYmB|#B(^i0QTa66$d0@t;I%PBazKY;s|1OT7-h|b^K2KPRgiO9`gtgaC|`M7jO;IInjI?)Z+I zVIRTvBlx`x3i1Z9;O-slj#dRTDN^^(Ra$xoh$adte0 z*A1n9`}4z5C=EbWNaIJpOmO6+$h<)HbYv^ zaB2|N)}E#LlSil}b_f4PRDfMqBo zhCKLn4?%|$&9aCukMN_vp5ngFGjMFpqRUaR0!&vrO$Sx61(4No#>NF(*GYUT9O;FW zX8U=k1Kv7me|I|I`VF8c@XBE*DKl7+^s+*$1)#~}qsj8YyMr=Z*{>`;5y8+{9}RUT z=CQ?>qi_Yt&b!hXa0^?2aBmIwJv`3Z)jIgo{P9kcz6dncNJe5%7O;JPJPEILK+8(o z9i2HAq$PNHe|O3<=}M<4-#*03D#@XC53M-01Pq=-O{WLVyuwdf@+;fvfyK*+C@)TSPNEkYwxiN*yB{|Ru zkpz6}Hl5FWc9O6&L`2WsBm2FRBa5n-g$i z`J$gua4n$J>Efw>Ddp96=pF{sg!`TpG`Q@;9A|U6V}e!H@J<(8w?Q(|(#wQtP`565MGlKxE?V+^)(3#y=_V!=%KRXtVC8an z>`s$tYd1dc<<&?j?fF5jBqDQT{ue?)3IH(F>*Z*lJtOXm*p>e8|I)AY zc&!84d+Y_g`FQRX!D^3h@dcqk1t@p9Io9Q1$9iZ#=_C@F9n1xNaOw=`*?EGlU-%i* zhvkwQJo$6Mr|4V~<{t0=R`jZP0ZpRYL( z0H`~7D{y)!hU@@iE?~zH1mu7vxe$8El`iu z(l}2T%L>)YKmH&@D!bJ?o)qkRww6ErnwMdFX$hADyiKv3Z#pSR0W!mW9^WgN%D_MT z4D^9nSFl3$QvWXFs*1up?X&C1r_R8HG%guQ*Jf7mi??jyYRVQjh{{@|Uuqdo8y-=%%@|nQQ#>`{G#dtkFKcL$d43gY;#l=J|x(&-7*Y z0~7(ZTF^8OAy^x!C*YiaCMp3@YL7*wgzvu~*#B~f zt2~!peM=+-AW7@+?2p52cDm>j0^I}^*!6Wk+f%hUm7`z<09baB;3!K<+AI24->MM` zNFLvrjUO`L)#s|&O=C&Tb@o|f)GCQPYECJyl{Nc?JZr*8& z2zFrChdw;c_zTsza+nLeCk3c-syzJAaxB$AOnQ+1G>)_r;W26R&42c|Z-1Bv-cYgB z42nadYfdb08a?G;-(G>GxiN!D)CoUEFqPAGB)mrjsC24SqJjnZu&cHk`}I5sV4I@? zrUy|H+u2*dbYJTOjG&!H59Yk`T9Bil_Yy?`@=Q^HJW~`P&lCm7GerUNOi_S5QxqW2 zENKBE>H;r=T|!JM88r8$&yFow0n%QLhApvtXJgSgO(&H*hDa;(hh8kHr3l?&P`V~g z?dmDEY)kQfF9w-=)oEei5eoIo;e&Vg%v@Wk_m#~_w`2rJTaq20KFtTW&fatv9aQs} zquhy)sQljbQ>oC7 z8f6t3MmqzjX5rqP4&aCe_~E0~l-Ftu_p6vuFD(txbJB?7|B%TqOU4DgUnvTZXNm&k ang0izzDY{w$?}N+0000N33M*Uz#OFL4bQ+1}AiM6E?syLY#B$3OAAR(7h0zvK#3G8lm&pB^DWK5fZWV6Y8 zF3H*7%zoLkdH>IxC+C0O_c`w&F!yQU&YUQK)W?F4GVbPTfzXI`d_c72bwsR-6z1Uq zKyl+MTu9b}m+==Z2E}EuEa%YgOe@|@(n`KZ*8CUoU2oS6%g{3@Ulr$hL~UfT-i(56 zFWjV%G7DO9{XdDT9^TtCN5*gia$E`Ul=nuex(ca0{5I3VM+~iJ2GUy4+WXc~7(69w zxIx*K9pc`ZKjG;q_i@^jZAwj}kY+$m#(94Kz$>_2x~w4wMOPFy<(9MT@}INul7+-v zwph|6jntd)HN{O--tx|X#}!2|XL2j6Zm5xFRJS{wwS`MqUivfM501WAL;AnXe$*C)-&Tpc!y^1N3qf*B= z3h4&86u~R^uj7foJix-z-F)-%9gNRD+jsoA_m)z3V3zE%eKcm5aL=qKx$V?8)^z-h zocKAzYRSNm##tCV#T%=izu^0Zw&^^-=?)`G_Cr-%ROc_?gRhja^5{QUcA^%yFz4Qs zEFq2xFx5M+9hOL0RCMm8vi&V4^q4bU9tuNhLAil$ZoOg$x6N+k zx{`hP+-g!qUaPyw@{>1p7vX0dqeId-ikFYbRnv zv!HNK8t?8Z=g{f#34fcDah^Xs_%a@sd69Cq8m6-SZQk7bD_fp?Fz9)}ku!OGcim%b z_~<51bdBr#zI^Hd9$QpxM9F@5gw7o&KcZ^yFSz^ohqheK8>+-cn6{^9;jQ!&i4XJc@X3)17@e5cC?-&^Dchw&^_c*Sjd2)W+PRHnz3Q zkeUZgjGo}p!&Ovt?v*yC8Avr?dus_VZ2s1O$AwVXd9awB2aBcE%+ey<|HT%TAK!{k zPi}o;2c#L$84j>&!y4%+=RmlG;MOyZJknlGQ0+FQrV&Up;D>)&ZQ2xbPIn8H?QanZ zcbHPoNTe9>YV9`+e;y1_AkOiy`U#7D%_zk|AZU0^qp|hApf5@HyZOt;RHL z(Sr26X5yjEi1hzhEyZ*!H9!PT5f9a%2V1R_VJVgvAiNQ@NwuhjjiA`016qZ-2Do&z zOLh~VvK73}c@1eWOfx_a{tGo!gC0n{g~dr2Zh%NXO+2&(J+Im5Qlk>X3=rNZ+QfR) zuWbj{=!=QX!Ta8-X=hO4A}tT??#oH40N)pD!_9#J0000B(5!L>x{!#Rgpb{z& zDorU+!YeDqy}I@i={Clc!ORSYjxj4RYt6Q~Ez`Yv*B?*r-n%{b?s+-qJnMQs|J*&# zIrn#-`~BRV^PFce-}d6X>3z>nqVWcQabtittCGFGj$0N#fR^AL{+mB&3Etq3JkS!< z4B>=Ppx40GnT7nyd2mmd?DFKniiW>vurH*p;r4f;3 z6B?lc09Msx@^soYa^Lt}bm@dd*c22=|KVKv_TMF=biyG;0B8wTktP7N1S?1t0O}=N zA5%7zkg=o-0JRdfdrb->t!lpz2ZpMaJhqQ`WKasePu!p$&7 z$Dz4Hc=;KQcZ3(<%KxJP5HI29OVD{CeC+m{8IQ=?ojuTV(RnT5k0R{Qe>fMw@?(pW zKS6q$d0X(?=;NwpD189}Npt&7PQkJzaL+RHS{Hla&p}8wNoVQ?RJTN*+j|5`3e9Vc zj>D6A=4}AJ&g@5~V>hcIXLFw=33YAo`VJTvf{??jPEUn`e5lw7x5bb-F$u4I0F_O~ zeD4IG4;J4Jd$z%*wQ>3_1OZ@O!thmi`CVu@VSkLw4EW|lC|qr=e|?)V*2q8h$9nb& zIP?)@XU6a6*%~k#t7}4bCe-hSgF9k>gUu+&hwf_Y;RCE+4LwJVFs+i70=u?ARveYHM~=7 zz5arkg6=HoWob#T?f3oG2P@XWbc|22aSfF3ba`?Av1vOTIcDDG^TCB$xX6 zEXod^32+-eKurtW2pRM5+nb?12rE|@i(vbK@J8!ko%?C1tkHN~o&&A>;r&v$|88je z%8=pn8GDo3R(o|l5g;ObFm|9B=}@-|c5j8$lyF=AgV0$H&lMPZhcn&I$2r{vS4QBO z$KhN>^v*UpV8}T31!QFy*JKCf0z`%1KV$^na}?G+W!@(%18P2jkKcj=wa&-dR}I@< zh41#lqAc@1YaTW3f>Kl9@}TwFN^e6&g%2<|0*f-iZ}BTMFbqqwosDJZy`c~cT{Rv< zSV5Tpk>Mv2N~Rzker%)*(Ba2NssJ7S{74g^!?%VM0Xlr^2>+90*NcBsQ5b?NxuQ!a zBtiuQH%t*+n+zk1MV3uygiS%g4O0Y*^gsWzix2@ieCN0ppu=~ED*-xu_c#`y!xw`i z0Xlqf*cPC}7mF1v9|nXZQTAXqE=$djcb|yUN@+2F<8KCP|wjEZcv$K z>-YP`j`pXu68l>&3hI2*anBN;`-odrQ*S#Q_*7PlDNCxZ07Zgi_i!+%uGG560u;IXL62d|l2{HU zAi0S}_(8y=ZNf1O8lJa1Kq9^_Ad!FSyBGo{;;De0l zNt1*b-g*U1Vp?G9=@r(X{9Q#=pW%~VkTQ$IKF_Qk2KPMAtT_xPv^{%8CJ4vAW?Xf_ z`bFWlKRt)p+pjIYvaC?rpvLCOpQDvx^72w!7=Pcb^_w@Z;Ewe+xue>>0uzFZx>V*b zxHBnOf`*+qG6d<@{?;fq}8m)5S5QV$Rz;8@&!Y$TWP+ z-l*W@{VeE+?}e)ekDs{k=+)w#U9z7fTz7>P6m!gMP7-8#CCm{w>7M(S-*4Ud>~=h? zcX!);`to{(Gc)cTs845@!I;3-Ajy!1#*n=z{i9bWzV&2{;MQd;PX&ncv)>A=XA?@0 z^jo)X@wGSGXFIZd;Aq0sh$Ld>_0huobhzT_>1(gc&yR2USR%Id+2^;WH8%EJ|E0yQ zncTB^@*Q@cXWcI<>%t$uTyxp{zP!2ZyJzWgjt0yJco>Ws&M=@dyp6cCRvEq7{x)6v zh{3v*r>9=Pa%dW`xE8y@G)M?E>!d+t5EWV$$Y}WH~(~jQ_ j`_CwIp!*;0jauebP0l+XkK!X~va literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/SUR.png b/assets/images/fide-fed/4.0x/SUR.png new file mode 100644 index 0000000000000000000000000000000000000000..4efce2c1f795ad075afe5721b1c734ea13cd8ca5 GIT binary patch literal 966 zcmeAS@N?(olHy`uVBq!ia0vp^2|(<@!3HEb(?2AGr~;43Vg?2#GZ1D}bzG(f6qGD+ zjVKAuPb(=;EJ|f4FE7{2%*!rLPAo{(%P&fw{mw>;fq^;L)5S5QV$Rz+r?Z6vMcV7l zjXl*~`dnVZVy+d&wQHrTc~bnYSMis+TwPoRTt4s@ykEBBLaX*#jk3_f$4)Hr0hh9B z7t{uAS-P!lS+3gT*GgBn_?qNSs(PpTy!n7Y-X!DCcdOscaTY54bS#szjYqzeVJjcE zLK~~VVWy5m#v>aTk|G!kbs8kZ9C(m~JKWdLFEF1Xptv-Jv88}Z30eJ_mAm;@B(KOk z60oB+-mZvuIrRvcczG1;oYtD|feL&KHuW{XLmE>^J~*>%TWsKoWQXu{j` z%jI7;v~SI{4R740t#EDTN%`&R{|tk2O~f9E*-0JUwD{nPp-~pHaEvGUY^zrQJo!I*Hpw#Nz$B+Y2_c z>#KI{$uXL55r7WzHe(5ee|i6UiHCK=C%JcneUHV<>T|(PXFrJ>##E} zV2^e0veTMR;A+DWhMd?gx|eVI+@x$GIqzvJM@{nBV)}xv}eh_QRF(oyBHB^RKR%GS%Dhc4ztW!WWOUdM>;>bY>m5&;4yh zo8uCv#4ygDDbM$&itT@#HkaAdl^t01N^aBi3`GWwxSG&p9n&Zd zfw~0`ubk7q+h!)d=+ocd`xmq}oZu99n|RAJt+@ZKgktB3Lff!LF{Y&0R&nFaDjyz3 z$=-|KI5AGchBr3q$|-IEyUvK)*ZiJ;iWXM3{TfmA^!Rx};m$Z|Z~g6Mp^IeoS{8nh zHDlmqu~;j2#zXJChkruafkQt+rQap(oB2%dw?W4d$UpgqtL03WT{hf5imJj(y zIs&VO*D)d`$zwHt(mJf#X1RT^e}3tZW(QhA#g=}5vMNhG{pGT>q7Rq{89ZJ6T-G@y GGywo*u#_zT literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/SVK.png b/assets/images/fide-fed/4.0x/SVK.png new file mode 100644 index 0000000000000000000000000000000000000000..3d7b9d771f33f8e7282fde52a3de4cc35a502aac GIT binary patch literal 1917 zcma)7i9Ztz1Kya(y}sp~E3ad&jKUYWVy^ddM2;|5%NDsaXBJ+Cr6`mmG-*cEJ0!=* zOUTkt&c-XLV!J#&20~ZDRq=;3ucecfP5A?!cxft@ zH&|Q?!!8*x=2GwSezuZ%l%Bn}yuW!2MrUUprcBd>>WL7%KPK4MFNZ72swWE z{{mvvZj6B;!>6Z>!3Rd1;YX~lcj~$~QJHuV!_E#*gNF9Gc9X`fAT>~pisgx%!prFd zvhExwPIN@*$&$!*H@8iCmJJ8v={`aM;fPknyYGb!*#S5@4M5iA3!ZT``6a-$zS|lQ zdS=@sUT8}QB!Hp8EiGwp_0qz}Bggv1g~p?deO@E2&59D0Rsvj(;&{JtX7II^t8|5q z)X&}{nKx4sWb(PI0-R|~`=o^p(@ILs^?Yzk#UO_VSLfe?goCH|W3`cIuTIFd>`Z(M zYS7>;I@__oX2*Vt$qw2kg>%>T^k+&VLpJQEHpbvtSscI>`-Z1DZ(}tKm}VS7jW#>= zeq)TF1?V;>&;(ayq9+w0UKfdCRIIB!+T^bAoYqwXJFaO$w=5(j%bPeL{Rv z5Q;UhYj`TMDP87urKK#_Y?I;G6)4hD)Bg#m#{|md3PGBD3oHpT#&UgV%m6xiP&{Xu z+oZp(yPkIA%0de2i7>ar=*wqJz=Ub zi!@xHtiAOqDr1=oztcp2iEEUw@vb(j%rzAOGt-mry|`#CH=NOytHgUaRioZtKpVVU z-S(X_|JTxk{;zboN3b~3! zlbz?*1Y*1c4h?R~|Gt+nBnwJuO1ibiiBlu|E3Aina6?q8Pv@W=^r35(cl zwuwLK(hL-KLt4XhY^yACr z&HBd}8cL4$lwoacUH^S)SzpfI>YmoQ#)6kQvcE5r)nOB7_Ya^2=hB^3XU$i;P@zBA zyf(fibb%z;osr_Jt;20HnrC}~y~KEvOufqfc*0t!Hh+%=_wv5)*UN}FSdiauJtLVy zLhC5pamw%;pw|W;TC}N{MR238NsRR5CEZ%UDEk|M2bIKf4>6|tl4qqBVx*4Z?t^+Y zo?9f}R>{AxmE4hr?32?JZP1Z1G#>ji$eHBle_Ju+{O!P}r&&9S3kJ?%A<&UhFiiWJ zgR+==-d3CMA&5QW5Kd02U3?^jC9_8MR&;f`8~^+gtm`mK&-(CGA(Bo^<>1K5!Oq0u z%2zWmK5wQgiI`PIiJPMSBs(GMq-m5y{QO6q#JNN3zAUMLoy8y>_K~p4B0BcpOuh8% zhwBO^42zw2>|!SbXn$HDayB`W$zKLWsV2}xF}tTHMxSf-jFi1n8Jm2{_o?c?v)l}3|0_OLesZn`FCBsm%#ckjXx-WMzh5#?Z2tiyS2xTbJ<6hPIlMvr@P)3e zS83>*qHJ?K1eo?55LYpFygu6;o z6Yl=zC5u$1s;jAgMLpE$P}vRIto3>y8P&-}%qO8O)bTOXa8h>F#RqOLzc&`mqyU*DX0z)DNP_XfHcUL6hBoog)IiRh*4>p(8< zrdL?~0EWA^vBp zlRHEn9q(jDQ>h;{9+7rLdVga7~l literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/SWE.png b/assets/images/fide-fed/4.0x/SWE.png new file mode 100644 index 0000000000000000000000000000000000000000..e026d02144cd4c262d985199c75f595629cb5674 GIT binary patch literal 464 zcmeAS@N?(olHy`uVBq!ia0vp^2|(<@!3HEb(?2AGr~;43Vg?2#GZ1D}bzG(f6qGD+ zjVKAuPb(=;EJ|f4FE7{2%*!rLPAo{(%P&fw{mw>;fq^mB)5S5QV$R!JhF;8$A`K6> z8_ihYxW#bp(kDDZYjob~>bzx|c5R+R5!c3zU)i!ImzX6sCrAs32y$+cIKSc0x&06R zf9Z-n18lN=8EtA`dP2@_biW#O1n8R;E#{;>F0MkG?W*8dwwVMNVMR_%LnY%J`iOw zW;nxOz&KN`G;1 secs2dv9)1$b*zQQ=@99X2e!1HCnZ5E0@}?b&c91 zRwEtMPHMgG$M?(qaL(^}&bRYF|MNWOB$*mNVxZ-u1pojH`g%I%*L1oLJ?fj+nFjvb zeV!LSN!f2H4{hNJR>oN+k>95Tv33cz+Ars zHWCv{0oK-TO-QJ3ZhE`3!#hJ@S$bR>`jgV*tB2IiG5fEen&s9n8g^WbO>bdUO2NBe z4i1jfh0}mQGb@{6jpq`bwmxjxT9GzTI!}=57NG9UT^n zy-{XP2yA;55mC6>g!*gasaE@Eo~9+xw|ZtA%!&7km20G!{w9k7gC}bBu3V%VuaoRr z&kU@0%xjw-7?hclGf0@O5Vkad8`5oeUCQTAnPHU34V5{HUSMP}x#uQR;MvaA$?YTGZ%9^F7gRWuAb+(%JeoGCx2;AW@c9kU!+2C6gLLj?`|k+0 zls8(wBb2#WSXrGF3T!^P4#!?tot?@0eigl3UQ2G6h!%4_Z?-O{UY=@mUemKV%}%Oj|gHZ)|F0PSD?4yNGZgf<@I|B0Zn99m~1_lNp_@dwkA-{PE1cJDA z9iVk3wAgnQs$s^w>Cr8b5|2Q|UE-X`bHw>n6>odF@H+^b$duJEVscc(iz#VZOZ4NuA@J~;bq>efJ? zK>S@exK@X>t7x27+5=B9Ppqwn%8d#TZT8}h9l35J*%^6x!^x~-bqn6E?(Uhz#So|68LC6R*#mkF=Iay=}`$Cai0-)E2mp7WYFmda0+26J6Qu!D@ zG4*FqeagYnIN;G>W#@e&OENoIzz=VM@-x`*URTlh-pSe;X;WFKVSmE}dLo8-vC3Civh;WW|k z?ECn|@Xk~ydx z$1qPh2|>Yrtnakri;NG3In*5ucIX?NXd0Wxtj(FXV*T;dKYtcl>iCaxN-^Ptkev*H zzkluVOSaCij5iKN-i?9OEozoIEf;CUNn?rF^+z-=E-p49LH!fb zsM8^fwNvZ$8fER_3x^8$GlZ}O7Cf|T!~Ri-qx|8s7_d6HYalUrPYYQul07|_Sr=bZ z-ngV2UB!W7GL(WuN}p0QYXb9Nky{T?ywulSmB6Z^X2SORzolDvTN_txzzaD!H5cY} zv~Z{5sY<%Gmc@grz-7v!yi)wr+1lIM9%s7i!G$mU>Xm6FqPYT;Zqz& z%xVMK_5gwFj&Q4X_{>%f8(HlPU#NGXYJg9;f1i2J!p3$mEL>a{aTfG#G*4Sgi#+^v zx2&!%{rOUJMn=Y$!wL1hNp;l1XjH9@_PpYnYGV;1JmQ|Qo$=mBj0+FrX+3T#5^2QX zawn8t*E)TF<%pH|K5(?=K9!RmXuyR22``AkK4NU%{*B79Ba(}2Aw4_ZpzMHXU-i}8 z)KtUNlu1cRX=`h%WYXi_y?YuJ(-~;AwvUgba#3nb40)riL|a?i^&W;mmAvL^+^#M! z056WeT3K3IO|Pzw!;2$sYUu0J$ji%L8_m?xlA^obWl)Np{!WCyG8i0jXjH4rgR>;k zhrus4cY^b95HCXKg&=RNp{&)@?37QSWWaH&8Z6O5ZLyavb zp3rc9C-3g+isM_D-(JK5+Sk<}?caCklKaM|1&UY^ z$rBMMI5FBma0alM^tG>9D3{ys}{i>!x&$-S#iS2s7m-DM?Lwbuhw>xSSV zkb+b2$p723tZZp)YBIC6O@?5d{~QsBB%@KWis@lImjaWu zz3O_92w+J=1!X(#Cm*Et?YE^I47Bnv%54&J~_!D)08Cfb8*tDi zcGsBwY@`b{H)%@7t{H#8d|A|8YHC_osQRrs_x`(z3N!|z3xlzF&3<(LJ4?h7|6GBS zG#$?n`}na@_<9#Zv357a*r-@hS3Q&+dxHt~~`4dhjX5 zvf*Pba^YxB?Bows+PD}i$Iv*S8ZkOg79Q5fA(dCz5iVU-foBd?3B992hCBJ%w|5jK ziZPJnOmXZQG84u8Tx>$MtcH2X{jv>|AK-r^g~`&h0tsssz)+gC72`g51Ap0wiwj_r z)q6|uLKEF%FR_1*;14xIrtO7i9(6S2iakA2lvFMjRrM=M(Ih%@G9Sgu8#nq+Pj2l8 z2RyvkGl;%Q)4NJ%<{+dk-sMmEzw^HTN4OejDM6T1w@Sp(o9B(kI}trs0WuxPEsg)g a24nSYmSf@tIDdvAv%Uw?Z literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/SYR.png b/assets/images/fide-fed/4.0x/SYR.png new file mode 100644 index 0000000000000000000000000000000000000000..1a47478db8d65687554244bb776a43bdaddfa6cf GIT binary patch literal 1010 zcmeAS@N?(olHy`uVBq!ia0vp^2|(<@!3HEb(?2AGr~;43Vg?2#GZ1D}bzG(f6qGD+ zjVKAuPb(=;EJ|f4FE7{2%*!rLPAo{(%P&fw{mw>;fq}W+)5S5QV$Rz;)*jJ;GRHs0 zn`G7UZ(Vj&{f1LJUzV&=l;##c(S;8dWfTUOn>)^tDJi+)*xEEjAYe;Z)3pYF7S}+T zE|J?3@n0vJZR}N>b^T?+#ugF5V!wZ_|LlV;o@|j}ET@Bln-W8Q&7bwsA}A@Nr+; zw|1qR`q@t(jlP}x_TY<^;U?QfIelBAC&d3qER1Nn)*IgwzJ9k|*}_kWsR!y#%@uia z@9fl4sqLGpcX9XM7v7NeBWFj|-Q5?tW7p^<*sB?_9sSIcdrfWI+5+su`ER$r zbvyoi`IF=gB?31)Z*KB?7#6lhd&R6(ho1GHm6n|^H@~m(661^DuZriV{gc~JysP=u z%B(Z;=i;kl&MuZJv48Eh#w_gFkLcnGnT)6$r}J*-w;rErqv81f^uGn#kG4(O!}+^~ zKX-L*Mtp9&{|}~bNlM8VBCmArI$pZzxR+sqoWtQ5X1_b?jIX6KF8o*`DEo`g#nr`g z_VPEH9u5kJCp7-u{I}t2Q#ETu-QG1xOH7{1pEBHKE44vvd2jRAUeUr^7ArSTv1Qej z(`Bpu*m(N)+iA((S8q(1TjiH{%b0bo^)+xO0U z%;XL{HMjV`YU5nPIls>>?7gt)Yo@l(dLNH&f94O*izj($OiDf@`0IOB;=jy&QMWhb zckDT66KE2&BrE5HmXD~?=?SW0eB$e_t#sKosn{+I55A3T4?z4_Cuf^P-~EKxIhl6lK~hDVO)hf7XeQv_a|c7-L5vG)a=k{LA!I_{OHgcjlb{=2r$!S3j3^ HP64UP)g;CPF8 z;%o>F4ux(-vtEN(4Y^p?a9n-r_q_M+zW06aeeeI<^c~0J`~CiZA97rdMQR95$pMf0 z;msYmd{O_Jav5f3;NT$q`{s?@o#$M?0&nlZ{T}`GYPEjwtEQ zn*RFU9!yWe`nuKI@?Ss_t5@+_K$3QS5MByMdi5&a323(K_xJ0&zEZJzTUv@m=x}`# zrpN)Me)#kt`0&eR7#K*#;SUeP?ye^jkhba<7GPqc{&T7Yq-obfz51mk=90bfUH)p z;zU5!t5>luK&n59H33q+9xDQ*`e0%eAk{Y~HUUz7IE)LB>Z4&)fK(qHBLbxQ)@fUS zRNo#g3y|u~(2@YD-W-txNcH9kEkLS|ho%Cg`gl24fREL`+IhmCT5>*}NQoT#^v$tUGHgpC2IbKan^=F^m8*77Ht)~CVGO6AU+U{l7+v7wo)hdjQ!T7lQ!!H!->t7>nY7ZZj36NdyhK@${lauZbzft|#n*Q(`)eluRy*PY;T(L;a^}$27s#nQ@ z+b?eQ`3WZsPCuOM`x8zWoFtW%C!8=?pMa5(`r-Ek{M*CVT_1$C320Pbspt>i$0wXH zSeXFX^})nC0kZ3x6Watx_2Do+0aAT5j81@59~~nTAl0`{+Y=zww@1qp(7gIfQhf+o jngFTZ9FZnKsyEMH{(kq8N!lZ;00000NkvXXu0mjfJISno literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/THA.png b/assets/images/fide-fed/4.0x/THA.png new file mode 100644 index 0000000000000000000000000000000000000000..3a41ad8cc1c73b7266dc98ac56829ad388568180 GIT binary patch literal 346 zcmeAS@N?(olHy`uVBq!ia0vp^2|(<@!3HEb(?2AGr~;43Vg?2#GZ1D}bzG(f6qGD+ zjVKAuPb(=;EJ|f4FE7{2%*!rLPAo{(%P&fw{mw=TsOY(;i(^Q|oVRy2ayl7`I0Vk$ z(W1>Ew(eocr_=Ery{)$`dYb0;6>`0wsZelI+iB9y_x|^NXRM7ouwR=YjA0E!1mgy# z1l9vw4WbMnft-1#w{BL*h5c))99T`kFcvDXd0qdy vr4D-q4@)_s*$YVddD7sefz0RU>43A*|xUmQEnj@EHs-jZ=AH5H1{vamkVcjjmTu86Suhy0WYiKp#* z31k=@V#(d}LbS{tkjjppBdv7mKAdyT#dsZ38;{h;S*Bfa=#2Jona;pYp>@sG#?hfM z>2%dqme&Q2gOaEW>9UZWC($0l~von7Y__WJcrzPe`!y>)`9d zkb67Oz4>^d{(ltoQ(|$#m$f3ir-hQmF)c{cyK+*G)s2-rPd2AZ2qfM8YCSRS_1_Ud z()S=f%OtyF^hp#6*@Wt<9WqXxHPSI@=Z8r^qOv&3?E$Cp+hF1|>xE0`_ zb?>ISEFCSIp_J+eRfS<^n*zRiv=J$DZ}UdhtFTsTvc%>0#t|o&Z;l4j?=`xPQ|qctSY^V;5AV6&FQ8_|&68jQI72jy z^C+pWQzVp#nMN0QP}=BwRMD~i$xX!r#PEF~ZBf(kBHQHr(V&SuUtWK88Zv6_A-G8k zRs6tmC$_D_GBAhTklw2tx^7?+%p<%jAUFfkm2a+>Xc(3yZw_enboO|*Q*pVcu(GWR zlMMi&1(b$zJo!`R@}I%CcB_#cAC?Ha4Z*quG*pw#*d%xV6YXGU^Vww{qov9GT;d5P zm{ot&a@n={yQBDvw)9jzQ0mZHnXG|&(|$um!extM`UE{y7*Ov@-;;JrisB&JVWW86 zmh&#?*io_J*rd|6C@<*O+mVL`$Q$tRK@M%tS<}I(6BRpn{WZmv9V4Ezw@UR|q6WM~ z5t!;2=zyY8Cfa;?FOf^UNq?X_4K)+nsc9wA3*hb#8Z_Ti?x4K0YL_q=jD`is#g$4! z=1<6#mPL4{=3^pzGjEv!d%0jBo?k2e;-?EE+8s&vD(f1$I@ zMs9Xzj!f{zKO}zOr0b>30o6)#&hv;q%4b+^?BB*@+vCyB`JOMxBUxeGy4cT}aNOrHacN!ljM8Zxs{uB6f9F zZ@zU|$bA^P2w-(hy<@RE@z3-FQEN==w|=}7qDjtWPx-#13;L(GPk50OeOefA)qrz) zyaA{E?E6H*`9_3QEiNV1Nc=^eT#K_(p?EdH_$taSxK)P!@S1waJPm;_UQ$R?#bC6|4opr__!MH>EPVs6vb zNV)Ol=Xkka#`PQ`%3+E5q-1`~yu!m@f_WK_@)!Nur%MjVBB+RRuNVGhS(tDvP6}r8 zx!z(hKO~o!s#`dx!=LP>3i}#U5Bmu1j|)$pMa+D3a|vn*q({HL+lubxbJ>}-=zLe6 i4Z*PNf9~VIi>fq|Xs<_j)F~quPXS=M`+zs;T;4yGNz#`9 literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/TKM.png b/assets/images/fide-fed/4.0x/TKM.png new file mode 100644 index 0000000000000000000000000000000000000000..d5fac31c09da80416672266ef55acaf79a831737 GIT binary patch literal 4836 zcmVCx%rP0ygTg|WJ2x(U z$7Njzh&`qst;|M?I@mestQsF6b=Zf$&LpzfU^wA_m#TUGBIg_lBuI7Yn91plZ@Mm0 zeGQRyt8tTKNaj(T>@n=3#MmJfpQs>A{(=W1y8~3L&fz-mn;df%89>t9DUF|ZU1~ml zGm<2uYATV-Hee16U~K(wq-$;>|Gy6sTQo*|ZJxm$e$*P1fjv4xQb!U>>c}Njd=dNH zNHvEL>$C#tqrNE*t8e}&p~^}E6)_}L#Y`MSvA2`jJHiip_VK{yZo=EJoJ^-q`QjqQ zEi-%!oET?VTL zBvjqUL;dYUpD&VaS_9W!%2$HlBYD(EByLjdlg3BE=zw;vxfBc~G+N+S25k$!# zwPhz>ugn#%=jdpSBHi^dKDFEBdxvW|S|RXMeSwz-_tRB*HG_bE-~_MnLj#7B~4a zx!>;Px~3?v`U`A)qKko-Ug2+UQixo+oI{U3&OJBR@VlxYif*CCt|Qas!^jEa)6bdk zlOw<7-rWzL^4*x3rP5c%P4RVSF5MhJu2TyrVFxQ+@`9wCMzYhSa!roR%kBJRO%ujv z>e*ITM%Um$GP`!slDBwdph)4824ZR}nVx>?zVKPLeBw@m;)FL<6*p#8iK1hT|E)_3 zw>MqRmcavbWd{KGRo5%LaQ(M9km{u~)Bi4tf9?_cOr7w8!uSJ85Ewk90uXT9i9GrO zt5T11Z`T9-NL@={rB3_{o2b71->6%27wIDbDmGomiC;Z|Jjv+^HWbOGm049{Wwe=_ z8`dKV0tLrnS)`E)y=?rw;S||D+R5Kv^yzm;>JJbyfL##LD=i8`r3O=SI&VH!>dA`k zA^3Fyl@2Y(Uc$fdVv0^J+1>j{Jo`MOJNMB2yBAPI5q(nc*Q+e-e&4hoR79B@>esMm ztc&jKAOMTQb=3P~xL!$v|FH9cY2Q1XN^tj*8(0=;q&?a5E;qxuLk@}#jv-REuz(V9 z$sE%VfRKE$_u6n`i%ijVfWDKT%^(PbB$<#j)s71yRBgzB2ZAtt9bmf-msc$#W#z{e z@Obx@87rE$O3uJd@pb2y0;<&lo-2^)){uSf6npR%swkq0)9m#riL~VzJLp3c1ZtK| zw*ej=-`M#xJa78*(-BlkqiJbb!>sBtR}ftUilZ`Gxs9m1xWzN~VkYAhMYOPkq}dqR zXuIG(6xGYM1wQUoZ#1@{m+_thf06tkgtNMM=5EDh6U}F(c@+b@`IK{W=>4JwK zl|G1X-&@m1bwu4I)KVlpGP4o3U57CeT|3=jyAF5ggz-Tc$W0`Xa+4M>cp6=AK zveToXS)W8QAQJLhTz+dGsiQiEEV1XcN<6_MK5R| z!wK%(lVH^LkxOVa4O>)d9qg}@XuNq8$C23ndIg>Ubt9z_VJ0QqYWE#iOp_GuZoPrK zm)wYefa`gDYwsiD-q87i+-K@j=KcN6KDK`TGM4Pv!sn0Y2*fQu_2_om>^wz7p?X7x zwX zwX674^A*(k%K<1jCjYwUVSe2+v%5OiIHjAi?&n)slgKlQpAYRU^6G*xN-&944o`K| zaqBI|dG+-gZtO|XV@2@<`1*7E`RVnsGy3uNWDlQSu#q2JdJpAV2*0EuPg>07>;ljA z?cxUqo+ObUIXg)|pvaYqi6D7+&V>89{Sy~s?cKrWoDnvgG1hL&U{y${1&?bYL-f5E zVR>DOr-M~|WOW}_QRLqEDhhrTn26N`BGV1@WL(eVq0Zm)aOY;4gHAe_(8ojk5yFPjYON=oNeZLccP1e|yflr1ih>eNyEE^3)gacsJCMlLM!UpdJ@_?mbrevb61@isMc;j1mbO}XjPcCL(xHyDndE~W{p>69e(lBHKkO)aE zEtX08e8{1GC6AbV39{}1LMefGa=Wi z1@eiBkil?RCeY{7{-I`GukdI|dwj9ohW0_EfFJ4T2xh9}-K6~r-(BqR_?K66`-2B5 z1XT(d2iJ&yMjP9XEABg~zwGGoMq|k-ZG$I89)ZFn2j$fuw z)@s1X?M6YCxW0Id<8hN6OZ=FV?Ds%V;C&TlE;4OvWiTS+;^EK8=%&bMq-0!cZ>(e6 z8&8oqBBEV?J!-^9&L`pm{;Y)T35~FcWfEvPwBTy(K#kA2JaQ zfgQU->{<-dwGe7t*je?2n{w~JL~{&z z*wWC-?QsRvjaVF!L^iy1j8KKl=C&x0HOUONR?>1fNn!z1`E-(10n*(k@D~-X`_XP3 z0U{#>{AOvLP}Z8~VE241XeML;*AfUe6v-ttJQp@9`2{@vN)T8V0e2F z0gw2HGw-)Na}F`%%H+CLD%NJu%AF}|g2_m(hibb>u7~7$i0-L-cP^n3yC{dh(WI{{ z_+Hliyg)or)Cn&xkn7P&9-N*`JDK5y0^Jv%T0w777*ZJB@5fhV%{njWLPqu|xr~`^ zZOT4?6Y?{u;^$~iKT7u1U%8BJnhi9ePZ*)%1W3?39~RCjGGBD zEZI1QFs1R+P4MUq5w_`Jmb_~5d}Wj$uc#sViw*)m>g1R4D32_tB)-$3*YNSc`UJ($2i~*u%gT0m)Aupw=7)I$MHX1K&4$IA5>YC zGxlHqktfzcEC+x+Z2Zy{%#sdKs^i5&2kf@u2|__#;t zCuG~~7XoYxR|25o^4aQP#F)#yzEZk)Qj+QNYrur(Q!LEefSHhM)B>X7;aa5?tj}>- zn#*%A;Ad5*!PA*2mMk#1T;j6yAeDB3|0{3ge@>LsI9z0Cug8bB6&~W*D zIC>_;AxkH+bRyrfX_v!So*Cqmi9td+hudCEGOQ_xu1D*AZxa8`KKwb0L7&2(Y{^kK zYOy4f=Z?KY@f`l@Yh&~(%Z*xVx8WMjUQ?*DJ|Xg z?wk)e-;p)Z#b0Y;45-LH2RY@m`61r>cyBNYZYC@iM^=Y6zxOf0y@ z_WP;2G=t{|w0D%jhI1j-+(FJbGD^rL-JxR)mgav;gx*aHxblvVb4`3B1CKs~TVW7g zwEkqtp!+aUFsFSHo1T%S=0pI?r@UH)g5sc;g?v zw34-7x{mdX;_bT^FZK|@6>R```A+h~HbF@z5HI3+pe+3|>wR^k|LG}qbfn=w+X-1h zDKBU~H*`kiI<-KiXTsfF^y!T(YPpWwfx}R}8pU6arasBo=HD~aS3&k*56&fb5oi(# z`yRyE_g#$s00MADb()9EBD4)VWO{Uhmz>{+E8Y#nK2x7&bOm2E`C}=}lmMbk;n)$3 zY#U-(C2C~_f})c?Y=Wdgag=<|D8>E}u&17kAM825FI##ym=@eOGTj(&{56VDfD{;S z{F;ufGpT3E^w*1q7UyTDbpf=Rl0P%Xq+E4myachwfyM z6VGDo+=hMVb>zgeL^TJwER3(Yf#hp%5V~XqIthG%derbj3P+MS!ybC587SG5d1lA> z2nfbaw5ZMYKAVdcuA%0Ow;=fAI6MCix_?3K>IhEgCh`T1y4#kL>t9d$;0c0(CyCW> zMXbG=;l9@h7hIZd9i{VE73Wf}Im5UDtc*z2I)mn)ALiHf0T$o+A}cDwh^|HIun);+ zP=4hBVw-M8YPgD?uYU=>u8z>(UPac|$V)FiPx$_ww1h2u74yb-wu~!4jW{^AK+T8K zI4_7e|J=bZ*F;!%+Y%a=?gZCHthyZA7{JT*()hJ|uyX_a=8acy?|Yee&I7@t`qC`9 zgoYZmf!W=dbs;mZ0DrBClb4YFrTJR`mhMP0vNOqRS1!X}?SrS^z!gN<5 z&_r)nd=7fKjZ+ZN%50cFp)@1o3K-err{Ve}t|cJrF80X{b=8BFn}=pqSoul=QFSqf zR5INfoY}$o3zBg!$h9OS)kF5XM3H@i z7YQ#eZBw2J{#p}BanYkTJx|8w&j!qrDTzCtEue)R)SyfJn&j*YQ9NR+bI1V~QFB3@ zUkA*VDJ$XBQ6Hk};;Xg@#7!bgjTy7$5IkZRpR5@m9J{w}TOiNuehExJ| zCca9Giq%;p?ex7l+GgOZHHoz4k^L^tSzh{gp)pMXg&~EW-;|SV_ajIi!T9M@OCqfX zW>R8cYnYCQE5^O2d1lA7V4NU=99=9m#|4Z0#FLP5)GSOeDyC*djBSp zUDLEe01ALxhPQq!vvGb39mgbFM1mL+ps}gkN$pM506phnWM8W)R3h$IbL#K>480Mtl2@Kc$W%0`YP z4O>JsmI=^^6-gSl2o=i(06d+iu9AFTi+zq^x{HX508k_8;HxqlzCCs%X-roUaTB0X z@j?o_x5>yU`nC^#Bxrj_@aUh@LqyyK06degu96mH>ObunyaItE5)Us=Z4n6(0N|M^ zsC!N3#b@lOnG(|-uOaOooulA!HviO|Ou7nWcgKut3Kai|a8 zJNcM!KRo-41IgeUIyDo%vQA;k2k-vE z#Zz<5$I8P?fcx`e;lcCGuii(Ojc<`X*MYMc4$nqZs`QVayv1v;ce032PycNNn zpOjQJg`o)Y`F!lzvxi6|LQ6}FuGdJCgrdZ5NHDBMlA^%MB@TqiH?kH@?u82dcLa|+ zC-xp;NPyStW%K6EtX{pE&dyFQT(~gmwrOc;R8&+@T3X84vuAaUuPEx?aQS+K@QU_dV70^es?$=G&MD`YSk)0 zji>$l_tVFMe0+qchD^8r3cfX zp*HmnUiMGyEg(o zErP%iiM~&*8PY6<1f-{@)790bYul=-D)#Q(YqqXoc;^X@*0@-{$bv}H&_pyy^xd&; zSR)t`kdcu=AP|VT-QBx)$;-qZ)TvVzl^IRWQxbpN?P5)_!IKI)7zha3e<%3tO$VWW$N!ii<|Le% znaR?nONV}nii(&!cdo9%D=I3etE;o3&LN&(<>XM6g-g=U83+jO{Q)}Pmrw>Q9ePX* zpAT5JY#A3XUevYiz`y`&*RG|xx!Fu@QOGIq#un>WK*?&nhG79-ub11mZ$~|M=+GhR z>+2~lE~coch?Of>lAWCmKu=E(B_$jtc|=qovjn^AMZ>#{@mi(xhy>hV>t&=pZj!UF9tC8IPQBD|8&6{EgR0xPTW*ozzvy%${j1 za;942H4-MkY~+Mn1s%tka*fwWhyXK@Gu0cf5qAMbBWJoaUL$S-j7CoQKZ1_;rcdKF z;vyhFTcSy4p2YP^Q!0$zSj{nEcQ_t=pWz9ndXSJ3lUk)$J$iGgJT^hS=| z_-HHW2i;~}PiP32C0`;#vsCwNu8 zykok3uTQ)fRlt*xV>doNj3}VMtDYoZ?6beVKHk_ja7Z#k0<_5a!9x2MCW*wpiBFOd n8w*pEpM_mSKC*9Pl??t5y6G-6NHi%I00000NkvXXu0mjfN4EL| literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/TOG.png b/assets/images/fide-fed/4.0x/TOG.png new file mode 100644 index 0000000000000000000000000000000000000000..ef9cec3776dcc2aa97b067ac764a7717226496a1 GIT binary patch literal 940 zcmeAS@N?(olHy`uVBq!ia0vp^2|(<@!3HEb(?2AGr~;43Vg?2#GZ1D}bzG(f6qGD+ zjVKAuPb(=;EJ|f4FE7{2%*!rLPAo{(%P&fw{mw>;fq~i5)5S5QV$R!Jd%dFrC60f* z@4N2U_9Yx$CX6U$Dl3DJOSvMpm-#)11+k5R!^`XVrpKp$}wR}E7UnYOuL^~!fxsK`U8&`Bz zi6=>}F#m9)kReH?L4sZ37-I(#GgY-=;RXZg%ddjMn5OvoF-@A%yRy{1f??+x9{*pz zCx*xCS7pTt$sKs~(P2{RjJbXngI`Zl`IVXR^OlrPX=%srzaPTGy5>l|Pj9H$cR}+c zCvQU4KbfbqXNO<^Z6=-c`t_n^?pN6l1gPZfs&;%KnDX+q_s(a@Fa954HIPay+#Ot( zQns_jcYSz~TJzbtb2TKTn-?x%xD@~Wms>Yb#rod!YganDD6$f~eIvlOJuan&LiLYCmIV07rXn?hi8?9u=YT>WPaU9RFd#cl-84j`piMtG#FCJ99@ze=Mnz zIyPyN^l_C#i8UW91U~-UqH}cn!j~>$VgmPX?=JOGlDP8c+3Ts#7%W9aPZV3^$xe=r z=AL|aU8!d5C7AR}dm%y7hl!H~}ZgZX5t_?74#yVI9C*t7$anWxu- zhc-QzmF=34Qy5ZWgE7_i|908idfZp5?y$eJ=0`yn43p9Adt<(c>5%@Z%QvHeIgP>7 L)z4*}Q$iB}fhUsi literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/TPE.png b/assets/images/fide-fed/4.0x/TPE.png new file mode 100644 index 0000000000000000000000000000000000000000..9817a50880a00082db12bb4adbb0df6e3042e69f GIT binary patch literal 4819 zcmV;^5-jbBP)XAROlL%=>x&z$jj|%OMZ=ey-oY_UG)ierx!hJ*;P~ zwN)Y_+@Q%&H+2R6+qq#t6LZ6WCgz3#P0S4gnwT2~G%?zyu7ER~&Mac-;2k__9P_8`-*uAORHJ+ zT@vrEKSp+bIRKs81+e*@iF9h~-)MDrhU?ORth_S%Pg_lnu>yd}cSrEf%sUD2@xY#y zg(V>Y>o32c6cr;52kL+TbP?TfA2bNJn{J}oY~%0C_VDidVb<;tPb0RZ zeq2`uWaX8KTi@4(s5luhW4$PfNcr;Rk{#MX%HMrY#Nnuyj(Pt9$?X{>C39X85s@bs zd@n@BiSCy9av`tGl}&J(>&k$cOLo?FR9aS%nBwClyM3rstX%1|mi5$W$&KtGm21~Z zNx4Y^ZhKRRij!GOce$YE4W~(;0onQGe7ElmyN_maIq~*Z_sQnxM3RyU5GH&7FIdPE>d++5~x{ zbJO7I;YPuzQD{5egx{NQ{HZccKm0)1%X4UXc^J@jkHEcuf3zJtl3P&0%gYm3 zy*UXH_{-34teQ6hom=C)t~QDRHoL-IPkqjwpE7A?(DCuy5lpzF3-+`$$`&rfye9!| zhYq;+?Sm$$C1^D`axdcb^iybBwYp3%Bd3H-31>KyRg6jnZGyZR8FLd|!YIP%A3XSoke|ph& zAu3J`{TIri6=F;X@CJB`LXHIwp%vrqACdAw%C0qT<9OdVxH%xJFwa zJzu8I`Bq9R%=OZ;Wt@}ZnJ-9oSeTS8St4S$OJZuC_{1z0qT*!kKle7YxlZA71NQ!W zUR1qm20~x$Iwc|^WpBPIIUPGm)t6t^TgPg1$n8&jRy&}2VxDKRN!UMk(ioKOjH}xY89VvKgsCW za4c2_R-1#&+*1C&ETP`I`ay#T_+$;H&6}xMzMQ+F!+3a94-`dsd3pVH8jLI5P53EO zh>8<+@7E+fyI34WMUva2r<5&R^th zHcDb@o_x0Lgy?RWFUI03r^=}O^i#>Xsk7L#vLwH#QuO^62vKozF!6#@I?m$KD`55J zWB`UnhtZ+6A61`z25L24GoE!y=g7$%N-NAHoylk7?2Yv5(yGn_*PwUf-C1|>>WckL zyA*tjKbGlK8J?3Sp$%5og8BYo!bQv|F6kBKjjvBb-Db_DA3nH$2NW(T0M=)cSqEQvg?gXh35kgV)|hR zVzDs#w$1=-*`MZ|j+3~|fZT!#vhvCRh>i%sap3}voE+Q-#W=0y)6AXJ^F<7QW4W+UY)M``)=L27Xz6Qn)4yU}z z$^&!1!`suH|5+AKaC0wwo9Q{3UO=_k%BH<%xVb}fMBwHQfeos+%F`3YSb$ce#$Bhu zVs%hZQccG;u9?zw*%i>tpsPJDEUiYRcLx!~WNMJ!o6p@rVrm}yj%P8bS35#lc#~tS zz|UJxesLuakM2qQu}s!%PGafII~!bYsjLKdcPgr_wO5(D&ULGIT&CQ@&$IS8Bd3Jm zkPr~To{@oDH|K9dZCm=VWBDY;JpU!%>^aSkN!j%68jMz>X5ID_jK$Ri2YT_v;;~l^ zN&F`q1qCSO9$UKDg`(XdmoLfKScsX7|xd+`4o@?c;-acYK32 zyLW2Ai7y^w!W}m=uxDGc^2*qD;4J+jT64!Op`6|b!D*)Ls7&yVuTdjJ5fYL`aSTm z5EUmAXMgR|VqH28n0$8xP_xG8yM1Tydg5^$xw%xWTJ6eW8$|WSZ?GnxpxG0Tb0F~o zhfm}HHDPmC;?i)m)#jk{*bm6eEhQ|(kHmG4qP)M9$`3yzVC`qP_w9Sdd_P~TX2}sF z6;=ga2c=15@0Dd1GUb}W3J9g~CY&K)DSa{}{rx`prw)VU&C4`ZNXO#-Se=gV+1Z=sWcz5>xZ((AtmGZ>FQwxbT1K((6m4&a#lH zuWS;B)g}d#CreJ(ZerTK`?B=g(#qt?@3X{MWfKt*ha$2jsYo8)e7-i#v(NrbR;*Yl zcDr3fL`q9b<)McjlNBp!Chx6DX;N^<(_&4@sLf-`$dSU~e-q2TU+R=+IdW8Td-Rn2 zA$N+Syj-4pYe#K;*KI#}&HLm3;&KBNMP$_9Hqfy{cLYAAL-+Lpw5DnqWc>Sz9eJlUF7CJ709<@MJWUZEUCh7B7bWo2bjJawMf(k~AsJMxO8 zaLlZl^cIVh%y~t!!@|TkdW<-ViX`sC{k8QR`P_zU+7DO5<@*CvDp)mlWbKmi_tqa{ z_^fYH7A?X1nP)I<+*tGO4IIv{VsLvet~3udrJFDDXR|qaD4r=(CS9Q%6=TPaX8pEr z$@f*G?a=zNH0mIKR5~|sD5!KgY{|*!x|HsPh>k4)_~B4G=WU!knL1v!fu9v&Gpgx}}n-dlz%ABJVOSm0pK96ac)QdJ=njIyf9;<<%_a z4R0U75AXk_(NF&)BLm&|Z5_d%(srpL53DA56wt8=)tDqfrg8+G>JZ8jTv<+yD@(rA`_ZFtc9^Y>H59a^QCoJ{r`v zP8vTyKb9_io65se*yA~v)8z3&#DqF&JVpb9ve}hu)!cf~sJEbBBczqpR#a|oXo7=rWM^M7t(#g!kf#=vM;nx~`iuh{tjj+`8{?b=akvI1bxH}1a0Mm3;aD<1$( zUb@ffdPHJ5eE1LZXv5>M|NAABf}MZ>f>M}(E#-N%o#&xbYnk1BBKPfioy^L7089=G zOH;lfvofDyA$_i@ZO_WWe&GVT9+8|pTT`ddV81`#P$y_qyQ9Ke0+5)RM`g7YkC7uO zU$_XbC!YlMmH!JWojl?EgA|hfp8YQ<|gqU{h5QjsN)|Xb>oxfPxsP$;nTTq)IZwB>h2f&igkD#bE z_{@EU^7-?zXJk07CuVc)T(eU+9F#me16|La=!Xp9ldZ|*7%RA~cYE5l^l@JAPtfR} zDtLZ!KL8R_^Z9V&arCjVcusqe!YNa+pFLY|9jU6K`0*zwoG=0F@yj35SIuWOQ#Aca z9NF3UEq#ZwN=xkuxS3P?In|bHL8BWmBBm2}_Uiz^b8qeBXi6^LGiG3zFdpORF;so} zDPp((p@!w)L5w3t*6fPB^;Qb+ok+zyOOfj8KhAgbCYKRW}vyPiklw(!Z8WCDCV_<8LkwQp&SNt5jn+jl&RJD*yM z#p=M*!;NLLhcR_{cd*+q@7#rH(agU9~FlH>~wxRrW(ROwn zJ;$eUqloR+64Rzln0D^OnwW%ASXh(K!vnXjUD3zHV3;rgO{U%M%=e`E+B&mGa^ zpSEdo0{|37ShFRWw>~>uuV|!3tzzupa9(|45F#-5Uwio`{&XE9u12@8=FFTL&D7!D z8+9|@)wnJU_!SZOG5G>J4rOpKDVyy4a*QQaczWpY_tq29u?77jLl}R17h3px*2$Y+ zT*cSBPw`_?HYd&+DK0ltWwO%3&yzMSy%`YMh9Uhz>EFH0b1u?2)#%I0qHHF z9-?p{O+$?oDN=%<^x~Jh@BCwTpWT_+efNE4XJ#M6>@2uBg*gEL;I^_fbzqEcjAqHf z#^^yl-#z{rf-J9x006VrKaEM&_!XG3csA6`CG={bcPR2suor+tB314N_=R}f3Gz}2 z4E7(AJOeRj+?YW;r-C?!#cq5~QY|IU zb^v|(%&O5i(=)p)9*#B|LIv$YkaMOF9qQZPjp7kk()&Bz-1xN2PMJ2b=(UY!HAQ1; z9(ZvbndcyH)<;n>>r+3Y=?nJ`3=>wG;NKS7<9L3JIHK{Xsa@(H^-YjSZ90@7v%Naj zX?=kr)2(Zt^X?3<$;YjgfJLz+nC6?C785=>A7bQkA_;%~$u)AXcEU=Ghn=W}a9^iU zf^9^^?JTW8C69UIImHpMJN3g20b+HTqB<`LgpQV|1K4ZF0J-psqW+T&czdHG$ESA% z&tH1+B0*8|5@~6Wl^Xi^(E4j zwdpWY27`&IviRd`MMFXLbB4)yZ_ii3;o;#i%7gj=AiO!NrbdHD!ti`ISt5#W3NLMG zXb5ezU>W%pL;YGx4|wCPPE)#;n{FtEwGcKqoadMNZ=I^V8eR9 zX!K;i0pWgkv%kHcQWzUcmeH|%H!Wh0yACk{)^PFhkyB7m z$h>$T`gAYdhE+nBRX7Po$0uSk7k<{%xIKPSuu`1!b+r6;^C%frJ%H6BCVeV(-r2MM zVQuF<)Dk5LSrLA{OSPXI9>B_iAC~7}*fLMP`NMD*D+rIg;tMPN)KlVAy1UtXVScn> zYbCt~d4c2aCUdFdMdyB3=Y%e|+=Kcr*jDF8KWaI@@aVl-!poPIUS36_B0GmaSSi0+ zU^M(grlW;5%<*Gc#YFlO!K^@s*Q2c%BfrmYP79N3Yg1E|oHvhlzVudNU%-7 zciJ#bQ!g%f46andDV*3FWeaUFEN@&XH%QpDGyyI)79(14r$npQ$(RZXI0acaT~0?& zwH_zRDsmpcF-+6*^YLT;;B)7UNsks5y#F+pHku0V%y}Ck$6FUD*rRtQh&ueVJVW@r zlJn5;0D!$#+U2js%g-RX|6yC9Q)r*m|2TkvN`io?R}j1!$6>m0W?;(!`%%&0f8#t zL8-j_@>`8iM@PQaU&qbrl2)v1V{xt1BUIjV z>!mGpdDE*F(kC0;AOOzLmou*Hd$B$1zHAE?6%zxNLUj`p6JM%&JcQB4$8Y54=kNAv zZl#jR99qN;{>*(eC*Raj6+hp=vkcC1mZJlDb8=Oc8ol*bbRJn#Q*UMJKp5f;`-K&357zja@>n+*;^1{xSaOwCzqVuB%3;*V70ochUWMAJhZ0= zmVjd@+HfVG>YmBHKW9G9Bc5V%S-y%%OMRtq3st^jLdOna`aG_d~-KuTZ3fT$uqh`mMKj{_Pr$itE*Y z7vqRxyR6yePo-+0i~(4rlyp|BF0~KcpnAv(jIOybxN@zt$!=B3d(XC5XLj`}uX(}g zp#G=i7w7Ny%$|r0y4p2CoeKM$kYijmq(&dIycJF z_?4~!)0#+?jBmqPDp#muU9b*m5D?^2=Ay2?al4i?EC@o7!Kb8{+S=MmaKF}*S61$# zQeVuMKi6Q8%3#93vGH=+Y6`gAWFy{urDrE7*jl>KWL z8y9z-9H9!(tY743Kw`E#pt6@GPU&X@K^bfa6lzyau7H!{QrR^RwR94RG@ak8F{`rq z<43waO6`y4{uGL|)H9jK+nd1~9qhn2DXyceZrg~yY+FlIBe~1J_J*o(QWV*no=e?$ zo|Xpg&Ool_&X?8WHhFwkjU|kYC+W7Z%lN6`;m(kc2Hov9!+jEZh>&~^>@khCh;NLM z=eJgZSsGrkcV;4bUTJg_r|79?1Z?1yuyy zf-Bo#`J(sq<>lFL#W~=5q%Gt<4R_ia___1%`s%8Tj!rt6wrvo%WGghtdI=L4>yWrg zYY|{Xt^PJi6tB5H)4&&EZi>`|cQu}oFB|=AAwo3V732T5XrWZZ4>9H@|IGvZ{G?S? z@#`%IJh0D1NXc*fWo2ayAB*0dVQ>3p%hCU#{lPbfJaWNPU}^o|dk+o8G9q0`2`BJr zcVmh3WIP#EK=>`3oV2-hD>D*&ZrN6-_BY6hgBy<`c+EBc!`ED2(ZvYKzzzjeko}(_ zqS9qxW$RW$5L3`cA_V*!3nfY!{l`O%ygXb=D)%T6;{98IIox1RHac#Yq(arB_OTR@ zMb`GrPx&!yoi&49^4;85sgK$jJRehkZedljpVEXMC>1J)>Hq8JNkM@kaDj*>!C|B?J zeQ*WT-)|mV_SY|a>`9)|)cwCqb)TZEcy*V5qL3!59$Y!!~a_o#M0~l9R+r7}&WSF@cjt$*?7TMGJB_N<7d@_t@pqFM>?{1}W&>CH!$FY+- zV`zlx)#e;w@*nIf&olu1LqU4J zFe|n5K;Qp5WidddY^5A-thd0z+XLa~kwhXYq7k>rMbvFAFb#QAmzxr`U@W#tOFFC% z%+p&UbgwDZW$W^zFqI`d?KZxOqu!~z7a+#xEOYlwc#5Y?VP)nK(D%6^H4Op;TRfni zQWmQwXy|csr+)Tu1FVHbrtq0N*-76_@-p^h>wev;yBJS@jD9QgLe{En;VCZ;jeM3? ztWLBbrFJ=+YKw4w!Bc(@@`2D5mS2PytR*JVaqe)1F*vVU*K5_&aV|#s#VMP|u=r=4 zO@eWulS+Q!sA>2>Rcy}MpNEdylfyc?c2mK3k zMU4!DSC8-I!KTnZh3fGJQ&H7d{JxuI4!N7jttLQUvNB!pm(}}>d-W;5F~xSuvt#Ef z!u~YrgVE z;1@N`)mF*z?v`V%gJMHZfLqf!+wp1F6Jf$VpH{Z=2Y+fnL6{H|I}vR ze*Yn|Hr67q=t}0ES6vB{DGPE+c1OP&r7{;T(j#+|KG=_+@O>x}?!-|XaidamE86HG z@4g$H&ur~uf{*ckn-4hNbT8nI8xQ;QGZ7G#VpFIoWKvvKe- zVVggagPWw`n#%VB6p|FT(Q4>`CRVOu>qNn{X_N_4mplAPcVt+H*<zqe#(d3Zk>Jm8Zq?k^)1cuR1OowHq6;$bZ4DOM;%rCjZD=1z!;5>Yr!}k4IBZ}2}r&D_G-Ref{#1h7{UY|O$M^xk%-JZvd zMe$Gdcig*L3{?HV7BlW`-l#6ss=PoVoE_nu3&lR->Atu;DZzf*68cniHrLO`#YLOP zOL^Z^Fjh#bcD4MT)|L<;?Z7|)R%VIdia-j!*#}ld$wI%h`Aip{|AE+hm%(LV1y6oa zEdwvHASwnwIc@u{E=soh%f=vO*dd`hPI_UAD?22HT6p}B&lTJ(`}C> zEZj6aRDzUgF<<#>a~;1ql&#(H4(gY%X-UFl3NrWHpOT}ly)msz7nCVI^2}jXo*Nn< zzjg}SZO4Hs)-CJ97JFyKmfPjwWure3cy8TFqvpMLJcY?Ba4?vI(;RI6lALjjcc4-O z^YOXa2?!YAHUii*1}b4frE%AyO?ap^ZG9`{k;9_{@Y2IKWRHP-%=v>1CGgmq?%4q9 z2(DKTid_s0y7E43 z3K0En{kc`O!70STkQQQ!jFk+lj#xXY%8FDrxxkXkq7s+J9=?x)6qLZ8be1RM@LRLx z)!F?kvaiUd?&~e~x7bXFp9ZI;Kr?8V5a|}ss$j=KeI}&NCI;XyVUBjp!O6E`CVa#a zvx!^?{wO6rFF2#v$dDbg%61t+<#$5S_$k1 zmt3z7*^g?A)j$zS`?S2aO-8z*`t-w3H$)+{X_oKPUw>1?l&^L7XrHZXP|G!ZOq17= z>nR5bout`Xq$#RPq(u8@Vg93%r-Ck(4K}-J@go=JGQZP|$7kQ_#7~+r-?>03CQAFp z6dU#TW|^{^|Apap*(PIG)04??yzm*zb^7eVhFM8He{r>+_n&cn8 z6FacmsRIO{N^9(V2}PU8aodG=Nq3~R8gl>uCApCHT<}pTR^mJ4+p{6EpD$M~RA+Vh zfPuRTZeYwSYhj^hPO$5Ktp*r~Vtl aw!!17R|e|cL*{qj3=r&GY`NCn?7sp1IiVN; literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/TUR.png b/assets/images/fide-fed/4.0x/TUR.png new file mode 100644 index 0000000000000000000000000000000000000000..3f2682ae15e1d72e644ee0a81934cd643bd173e6 GIT binary patch literal 1878 zcmb7F`#aN%1O9BynxUaNxzu8%j0hu!vdw55F_K%6OOeZQ$z`s~(1^KoPDL)cB)PRP zLOWJNE|J_qF0;camvSu`Qb+&5_j$fQyzl!w@B7>P!~5Q%Se=!C%fkTxkT55cYj`-TMKej9GC`Esh;it#bjKa6(h8w4L<`0DRU zY5%W4yl#IPtjJxJeG{Nj-@@~qqRvqth;uh z%DbhcqtD@zd0pf)VdC*vTvTR7@wIsir22QX#^s#M>BJ^$1Tyfw{S22}SQ%F`dC`4o z{5-bw8c!Rk?ur@-#&INCu;s~@IC3Mj8F~P<i zrd8#cI$JP-{?VT7DmnC2w<)g?0lM3B*tUymLJpuT#eNzOY#Dtfaa&6*K?B_8@SG=% z@q3e9y!m_4mU3}qXC$J#x#L|;YfYqte(L?X(f&TuxIcuFZ@zV=8Qc6FmV_z?rU%dO z^VVq@RXrriemsw*$#seeBD1%@4T!HUPl$DDui60s@A0yz{zEB%;@zU3%wox)JgyDM ze&nA{E-{JG9THypeP#Vnj@AIzq+~ETy_^I@pwiPSGqYCVLljtPmbTl{eEevuh-a!- zM4zAJ?SHz&u1(sqUDh`xf6Sogb&74b5P-!!UjAbmq+lB2plW6#=#@`fdDTs7Kdyl% z`>ynEfclS?ec~gNdKtHBStoOLU_a>Z#v|WZc>04}?p=TdjS?PCXIIQ!cd( zlI*ktq2j3{yV=Y%TUD|tOu;lYYlgPX>Wr>2L_Kj7*`&D+MVu)=Q+1>^dDiq2z6xVT zKBtsV#I-oEc!qwU>w}l$LIpuXjn7j*zU-?L?Z1>P2J0vrTuYB?GrnSXWpkdhOorI@ z-tMU1+Z$lg9LQzj(uN^x;NHt|&Ywq^HR21_25DtRPDs@}?fpwd4}+cttNn}UV$}H= zTjm=S_a@e~wI9E+4|Y?^h-QmzUmL>@_!u$NI7oL@DZ#}CIc``k2&h2f1p=GKv+0BY zc6jEbNSpn+FekCKpVjBtlXvceLf$S#m4NGeY$29r5bN*oRaJQ^ z;4?+&Y}(NEls@q0n866REXbV1%@x})T((i;ixtQ&hTO`Kz729>G+BR*X9_GC@-W5F z1u7a70^s=0uc-xtmiDQ+Zq~Le`Qo`tX z`JGd!ldPE*IoW$c?rG^Vx8*@^_k_%ubW86_JXV>&qSjHK+VOhIl{rc{qIDIKV2mS zOeDr^xZZ6SFwI7%-kMGSxv7$;FJr;wFr5BM7#k*S3$@BPS6m;lV!%lmyce;0;N9*p zOs-9LCja0)`i`5zX|S(8^ZQ5=fg7Utz^CI-MQ+F|l!pE()&Re3{pf8N(YseEX59cu z1r6bW-SskYmS--Gz3i*P0Qj+3;KAx&zIByWC(8n@XBCbrt$w**Epga+W;?F)$@mJ* zVDA7eMY&g%=z_+3Is7?m0wf z85IxJ>=A!cAmZJyqfl6pxjbPiTnK|?6p#dS)@=**QIt;*JBW7!y}`MNL8PEIzmB&Q u@2Ou?YOXEgdUR^3`u)5x>RIuB9mWq3UqsW;r8?`^Q3B?sRwNeDE&e~ib7si^ literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/UAE.png b/assets/images/fide-fed/4.0x/UAE.png new file mode 100644 index 0000000000000000000000000000000000000000..8d26494e698064038f3a7934e2211e59e9f13fd5 GIT binary patch literal 318 zcmeAS@N?(olHy`uVBq!ia0vp^2|(<@!3HEb(?2AGr~;43Vg?2#GZ1D}bzG(f6qGD+ zjVKAuPb(=;EJ|f4FE7{2%*!rLPAo{(%P&fw{mw=TsOYq(i(^Q|oVT|QayA6;upT_p zrgQG!&uNBD)}l5MMW^S@G`?ZCecpkXssr~$8MGO~7}hXEFm7N;46n wbxoI8iBOAd+70!8O=0{EyNI_5qPv>0-i%XA`Nf4Aptl%2UHx3vIVCg!0C-VcL;wH) literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/UGA.png b/assets/images/fide-fed/4.0x/UGA.png new file mode 100644 index 0000000000000000000000000000000000000000..940f75a1fa42a706cd59010e82c7af9565be89e9 GIT binary patch literal 1574 zcmaJ>_gB&j6#s%N)J)S*G_`V;%8fiqvmv;0#h-Ar6wb_+*jAlo0OqA2rEyEVB6`!Z^-7WQhxO_!>(*YOg33gUS^=t zb_Ex%wjj{cK=QpK!&tEf#VcmhU#Ag13&pF_iM^b5J1mA&waBHdc{CUtSk9~CJw*oH z5qqnEaBIEua)dO$87X;n@xw^Y8Mw@jWV3ou$FhAThlw{W)HHLI&}kCRaH*i`@eZjb z$4yQ%M!Dgb7VIWauNM6;38)=-108*(ELFhZ|xN=cPv@3>#*GtYwv^ zV2_g~g6KmI2W-H&FT9 zFfxKI9{5-ankAGZ-+q*-fj+o>ikltM>0})XJ}gKgTXY8&H!(9y)$F8x$DOfaSo^ny zSp~bQ1$ibd&_Ut2Pnb_8WyT=I6UlVrE?#}@i%OUgdA}mw%<-~kcF!Z!b9__a`(1P6 zNK-K~(}+jcBZl*q$1N%+RL;hNH8l@gSXd}+<>%*1LZL+JWn13d+#9CCX^KKEgBDzM zB;mYq>zde;-CwvLoJE1Rw6TaDOMeH`-8!eP%W~VU3`=is@sC0CV$; zDsjUlS2qUzMIjJMSHG-FMB)jd`IK-jt!1{a>PzM1Ka|8$A`M!W&-jkbu3w+%nVKrT zc~dFVyn{e%b6iJ(zN)t2wj`L$qIr2~IXM)^T}KVqfEZIS#DtDI?mUfPt#j?UY}U6* znV;eSVRd!2+eiA)q1%c=X1PU0Im^q~(T~-P?R7F%`t4#J$$X_R=MJ{vOA=QlPiDS9c&$8W@y{?C3m>29%PLqu>9yM;pK_UYo`X{us@*X@Uc6QnbZqf#y zM~Y!~uJJ5(McdF2fbfQDC9|Y8(Xg@#cYxmP^N!lc% z8md*}T|YcFwPkmDtjNT^{h?B96W^HYQyc30UWK1hiDBdnd@gBlv8~`;QUzI(eRPC+ zJ4^VcBObvkFDTGwv-CoQfRX{yg|@3-9&oZ3cN!mFEHXx?Nhncav-&(6Hl%^G5W_Ms zG#FZ7g*I}P*T96dZYL-IyhxEzZyII){OR1+ekCOpa;L+k1rM(y<6B z7z_f0MMv8@I2^!H^QLWU0$n;9Q;s3h%^miE2KIpF?g9DtrU4?lDuR1qO*Te5>I%dp SfXx5#0NVDV%`+>%l>Y%Spv{f| literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/UKR.png b/assets/images/fide-fed/4.0x/UKR.png new file mode 100644 index 0000000000000000000000000000000000000000..760aacd40733dcce34df07147a2ed53f49966de4 GIT binary patch literal 308 zcmeAS@N?(olHy`uVBq!ia0vp^2|(<@!3HEb(?2AGr~;43Vg?2#GZ1D}bzG(f6qGD+ zjVKAuPb(=;EJ|f4FE7{2%*!rLPAo{(%P&fw{mw=TsOX@li(^Q|oVT|fc@G%yusAL^ z;WoACQg1cvf1|Kr%dX|u_x$#m;VxgWn<;_y09S)3gEm7L!y1MN#tmc&T>H69-f#sg dl}zJm_{*2hTF%6IpcCjB22WQ%mvv4FO#s`dRBZqN literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/URU.png b/assets/images/fide-fed/4.0x/URU.png new file mode 100644 index 0000000000000000000000000000000000000000..afcc53bd98a9de0887dc1ca34569f922cbbb0362 GIT binary patch literal 2364 zcmZuzX*d*&79MIO!YF%9Gc>mBOSY^T48n-qvG2QL@R6-%v>{8@FtTKfB2({c+CwJmyz?hb(U0~0P*zWVxq&)-(d2+<3I!UNz3@`Y?949uS zT(MvJ>WcA9j^=c=S1t)nsx{Ajl|!(L59}~hPF7*z1@dsP%F<8~42NTx^|=|H>#hRI zwvyC@&w*aQCyCwzS}pXx2xq-4PeI-eP-f*vgb{C{d>P=Opfyt(tjJt)msH61vZ@iG zAG7AdxBpSeg()g5aP?VOWm8;;H|LJAe}Fvw7io7iAI;AKTNtmf=UM+eD0k#H1FWpI z*^aK#2+w%$7Ik+ieG-2f!5xXTB(X12t9!r>5*{7^# ztH&t1E)JdWI~@0a4wsu!E&R>NfB2oryW4^isuB62Qyi8mFPxuXaZh(DezsY#xngjZ z4Ko&b|2&u%*1pWh{1!B+9!1#t{rccO)|q)&%v|K2t?buQP5_jZS<<##K2CKg^fjRg z55o14mN8Pz6|vT1CB-+T_4-0V{Jl!_VF7KJDMGM!Gbm-Q@(F}lgL>RKjD->^b*a? zyb0^@@d?;W6%f@v8cXq zB{>2E4tpA_Yf%Sy-x|AN`goeWa4d=7uerMyp}qe(sZUaPa8^99rv_?nT}o@uSj%3W zHcsRcckhP``qe~-?G*$>s;2BtEN#-tI(XJ+6_kfKPCRG?8#&jGZZ72luMe19QW(cb zgp05IeHi6yIK25|B{KGBz<0 z(q`D^jms@(T_ZcZ$*eXW*|4imeRHO{9DvE~cI)S@qL`QpN-FH+i{1^+OQxHxNcUE{ zw(;=~NS}eaIn4KuhTU|VX_a-dyX`S=soJw7C~qI_5b#q`Se8fK4QyL)+EN?Kb-;2U z1w7<>+}0Xje9vfO(!?q9gy=U-)U??i44Y$>mthkLGC-*eV5HmfFWrWnekZmnQBHFi z#F8b>LMn<@yo6j=TEvvRhg7-eV^W;c-;g~-!u#o66erKN9f^j|ii^Tw4wd$a9Ng$S zk6u2AO=GVauD62)QfZ#EStO!pM@lS+BW=ld`UMX?ILfHQ=M^RUPYza{LZrdEoQzbGs)g`^2qi%hj(WnVhL94G%$WDLRN(EKNgC-|{VP zbZ>6$QMnbNOfO{?%+eq*_)tk!Bq{NSC`_!BYp>U*ZaDCg7%OuuR3sth{I#WZ%H}AK z4Ck}n9CjxUL9gp$ZfEL3^i9qpZp_ca^>jf-OS6MuAWW>}X?Di2cynVTt~;bt{pU*L z4R;MDxmyzhC*Gg0Im(4Q61hACluV$)cK0P>OTeLa5Jlw31=GlWDQU+Vo^=u?a3 zuy>%!u|k8J;u`G@LAq7veCkEsQjq-R`3g5sX(l%Bax{9@`f@E|8~Dx@rP_LE(%6Sf zwKb^NxvJ<2Oc}i(Po$$`8`rl?bX|L2nmUWNHoo)r`&3X&R6teW z5a@2KWTEE*3k}sBoZkES|&*WhbM>0>bo^1i)(cfP#_pP%cdLeo z(O7y~+I7Ei)85>4i(6)bwd_5awyYXrW9 zp>GQfw*9|uRdG2gw}D#t=S#%7f(nC_NH4!-<655KN3X*zE&)2mjIWcUfTgCS#f%8G zUEuU>QL6~@lLTild`R)A14t? zN&k4hDK;lXdNEe?X&DJh@YFH%>8?V=v3GcCnQo?5_|({4bUx6KtDO6Ut7;klmK*`2 zJj2-04YcP%5`0G>8|mxYf-n8S2J3kc?h6+ZXTixM=dj0zdvzlU!M!o`m7_I!sY>fN zId>Dx76vXIcTlQJJ!5rvR$eCL0K5^jn%gJPZB)1}u=M9Q%imei-dquDt{(IL(84G7Sc7+Q%1xuh=(fk1R=>0AnS zfYQi^SQ|7--i7HQBaF6l&;~c!t-MPzGB+IEk~>}&oa4cN0HVbYwb^NpO>4&DUOZ~Y zkG5W>VZD(+MB3I@^bSIx`P5@Q-ooIG#9sTS4Az^NPcNBy3|+ji8mA=Urcdw~{*&vL zK#9c5x~|qtY{hIyfo*Ao>*&RQ2mb#qDjHaC@@;GDx{eAX{!9Lk$` zdG6nR-@l*Cf6Ouqze7eS3_R{yV{bFAi@fU_F3#%f{?mrGf0^mPg#YIoPtv8AC=2_`q5O@ET6-?nQ@rS0vTC(6603iHH+u| zlE#cFWAXKIC;db=f?%C7mo2)5*Ps3)3GtB(3iRXq^ehCyf}h;X6jkeQ;iH?1_yiPO zDQDY`!?d*O*?;sbm-DYl#MQb6-h6*I29t@*3xymzkuA}*wi$Twot+34f$W?TzS@6M zqA?2suWi|dTrQ)gwt=nNzO`xGmQTM$Z#1Ge7u^%v{kaoi(As{+P`It}bfID{IB4t+U44(~ZFc{ittjp}9p*MOCfD z9)(;^^w^^wKsgmhMfT7&V-oA8zdN zdwK)n8eogr)1&$5jfa>X9|=H4W*$P9i;f;Mlx^=k!sNJ-`1yMBbH-(iMw68$G9s9N zY+lEdxKTue2k}$JC0bhbR+^wdKeoQ|05c|!B`#_>KODb6Q*)b@#>+#?Ykylo%Dl-; zi5tbq%v@^gnrzxWD8QFH=1sm~N7xHxPap0*l))qSEu_3s;G^vaFqx(K^;Bjq`9)<^ z)ztI(t|K(Jv`I8s*+m>blS6Bpfv*mnq^hP~qA9*s#i3)FI4PC;^wfAWjU86Gx}i@7BL_;|U|(9|kEF<~M8kS3|ySm)YLYj*Z->NaJGlLD>H8yPxP2D@`}nZ8O>Dd z8UVya+r!)ztHSdKN7$ZB*9O+swhuEp2>r@Dx5??pQ1qiSq$WW;6Sbo+WzhP>e>C z#Q6Xgi-moMGnjX4EP8{H14qwFGyvZnIYYsfavGXi*zt9`&8p|nu`DvP3vqSPu>Z&z zQ6o1Ro%B^T{chY9_Q7fm=*aDZ0(>bctp?h2aG<{rSFToes{sfJ8bD!jIRO6sy(zD( z#m~o`+PWsO59qAv@8^ZCt`U`!5+^5xSo?R@c(}V_5iID9MqD&%YU|rmR9E}Fy|n0R zoAC4X5Z9@`GK{k-Lp#d8hQ{_LCt!d#GjE9^A$|;3u2$0A+$J_a9h!c=9*l_?!h)In!6I1D)iojr0!E{WHoXx~4_Es8dGYph ztNHQNMFhb@Lt_higAub?piOT;>#D)Ozc)`mn#$P=1<2)cnp@l2f5&2>srlE@L!TQ( z*rLC$Cr_+f!phWJ88FWvw)X}mYA_a z*|262t!)NOW;43lMlm&+%>o*=iiy#~xo1ft&T17hxs1x{dd%kbC&Oeiqmav)6g`}~ z??@ylzz;zXC@t4nX#k=k!&owZDzW2-(b}q~tfE#-Umd?06&c2?ged0Cj6+w~NLhsr zLFhYufVc)!R@IXTN5rdGCY+lN~1L``jjO-*G@9s7@-rKYx#t7X+TH6^9h96NE5J>Q+6p{e&*JzcR{ z0~B&OWZ0M`51GH_`(HFMV}~+- zW*m_bA;@HKIQ^Ve1!EQj#*G@nj49)oKQk5?GJZaN*=kT{76gI^_2;%3F|1lP6PZlL z@iRH%226*>$IFd7=1wN{_GxHcc9M0mNF4rkwh5s2a$d{nHS3ChGf&+ldMrbapT5is z8`JRh@!-|>{%JD@pS@7X6YG``6&Z%XXtr5CW@Z<%I&C%~K?4{V;7ipncX(%tN-A*I zI^*l@PHfaL@hRv`O>HB(`X)kx1~4UVr1-?yLe|>iQkPXSyr=8y4M+kYlgY%&M-T+Q zI&hLJS1YZWr_LIa$;{dF1$?~yfYm08_4imff9VR}96W{ENoi99P+Fnm-bdbIaG;;n z`<-wMEnaD?<@9*)S(o&|gZO37oO;-oWi4)j&gF>dL?X*{xe0aGWA!plR;>8yOomgwT-cpkcU9sbajo)P5V1zMuu366bh@0U$pXhV#W^bR@2|l zgVg(9VR+a;lnS}n9Cg-sd%Ce^<9oPiozc2z#1Tekjk89@+yAwPs+tB|oi*afqI0>~ z9?Tzk^Ml>EySa$=c!z~($2`Ar%)jWLo6Zd#OBp{plz;C1sauUkt>Ui53EdvBg7&oY zz&**l`N8fU4O+KqKCf-r)uZM{)~P<(2Xtk@tck2zHVZech9QFo@aFrwC28OfF(Y_* z)qFyO{TUSC%Zu;qkQ^t8iVWky70FDD9zpP+e!TGZPOBOp+85jPtLKxP6w8po19)!p z7nGLktTaAe?Q77S88O6;3+MH%yKJ7|>r3>wAFzAhaXdX-@%D7%{m=H<%)#j=FJd+e zXk9dH-~EHc`2ZPNd6ZSwwl``&o|8Bqps=`{tn4BT1|xZeWfJE@dmnJ{hfEX-1qH?B zect(CBLHf(llZr@vx+BP{*3v{pTXNxYf~eW$@qNNQR3(R8KpuYzOPbQKU@coWfD7k zz193krBYg*#Lg`&WqjgVato~w^LGBd);2v+No&cSyj0{wm1z3+Ipn&3hGxMfl#A8kJ% zzBkOvx$)gw!!PxCA-w?cHXZ2i%Ttf0k~noN;h}+Sc;;hqBfbLr>Jtu~zx@Wq~E2o?)^gOM%&*lV-%-n0J%r4>3%W`S)xZ1)Pi8LJdDVO%(; zv+{|Ljp(-Z9}^KmPQf)MMh_Q9!d)3Pd@v=Y)r<@eBI8`1SmSqQXh;C9ZS5x=s&x(G ziMP)9db{J_-qxoU4au7}Lh7iYM?BYJGa9Nc^y0kAWU0N5Ev0PKwH>|>j3AI^91 zZ$Vj;{!_2twD>*aNI`bS5db^m2!NeYdPa`!^)vI|GmcS&opGcfJL3p|opA)f&Nu>K zXO#KxZvD~TM*JruJ2-iUqYAPkjud2P909O1jsVyhrP|L=dIHPAzYWJI!p=BSkezV^ zz|J@VU}qcwurtcCJ>ML$w-NuzI7Sh6#*u>Tj3WSc#t{HJ;|PGA`9BpYW%=~kI%og@ N002ovPDHLkV1mTbAx8iJ literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/UZB.png b/assets/images/fide-fed/4.0x/UZB.png new file mode 100644 index 0000000000000000000000000000000000000000..3abeee4c08cc578bd5a692cd33dff5193db31aa8 GIT binary patch literal 1382 zcmeAS@N?(olHy`uVBq!ia0vp^2|(<@!3HEb(?2AGr~;43Vg?2#GZ1D}bzG(f6qGD+ zjVKAuPb(=;EJ|f4FE7{2%*!rLPAo{(%P&fw{mw>;fq~_-r;B4q#hkaZqH}^vWsciF z|58-7Yh8A@+fK1#4q^**3})msUH3FObU@r!<)TYm%>?$x&HX}2NslJ0I5~N=NC-0< zE|}nI*w-C1hsCu?L1FsYT-m$VHmyy*yKCEV@!%C@9{RWE@BIIK<;m@~|MUOP-}XP= zZ`Fp?7af#%LQ5J?`Z=&%OEdjrjdNAEeK3{_Sb6 zecJ27*=2jWcbalX|0y<3o2th?TkOo48MzwYH=TNYs`~H8Lvvnp@7;Cm`T4Lw`(m!^ zzu8yj+nm<7t!32=eYmAK%~>`$N9-0`n?=FvGOg90EG+K^bf*Pz+>u?RDd3fwy=?c* z?{|uX+$Nc}&$7H8JHsy7(~s$yMM+&oMCrH~k#VCVPE8X0^)3Z&!O# zh`fdC?E@DgzW%;H`K#oH)RQqlafWSw|FqxvV{_80VU@<7$NKCG^u^M@Pu*syX*%Z%tN4AN6 zHcpF+eY|&7rQ~MORMkB@3&M`QnOb?dBCSpRecof&u;74|X}-36{Kr2rL z>SrBZ_5Rb%t$iV3achddJyqAZsuRQGZgEA*C-vW@B)>K5U4uo}ed8@qD##aF^>x>c z-zB1+hZlT%Z9M1w%k!%rS4^1N@Z0=vo#f67qP-iRN3rLfJ@j36;g&aHDyP=nzpY`k zT88(_l2q}-f@LvT2iIuLd9`QqG*kBKpHGUkJC_(Ln#pgHaelog&o6%k>x1ssjB(b# zUe6Gnt&>v9W>WrDsE3dF!=m*(ubKHgPad9ll~0YgxnF-9!-GvxH)3Y~RG4C86ItJ~ z{r|N?OZTfU_`X_v$+1i9iRS(LeAdmh^GY*bx#H9jNtexH0=?=P7Dasr9tZ%#XZ6;r zQzDG#KQ-IzH8U^ud6vE0Y*pRMXSFtWmu$PPdu;8}bXPIqe?jc>*Fqj$y0rf7h8Yph zXP?^btzGahId=KgGtu2Od!h{IJ_@w5&3f`hxo&g)gjvn$7M0dMXG6s8^VTGLW`5j1 zZ@Pz+Ow8^%*Tv5y%soGQ+Dwtw_t)2^d_EMpX5KTu(Z#G*+*1z4qe`Y_E+O_;!vrcm>D;Iu?%YE*oaY&~9 z9J{uB^1{^}Cp_kS+wH8v>mij87|OT3tZ>`z>FZwXH8h)M5+K@|f2%r!tEHpoNbt=B z8^=Qi7GfNQD@;##e7d*bns~RczmyDri{%ZWC;tT3SxYJBNyxnBDEx7LUCg~}J0^d< zd*Px!OWf|Z&`?gdcJ89tNk#!13TRx&vKt|FjYX&>3Kdtn zuwoFDEml}i0ZB9pQDY>gVB#zBK@li|EN+*|tJIYgwOU|6TWLA_$6@G9nSo9_eQ;-f z$t0aKcg}ph-#zC$-{Tx{H&p!tQZ*Prh#p4(G7^4S17D4TF@V5n7hOzI)+!Kv-`JsCR^vNPwm$SiKrnt%BcvgDm)N zBRKoR(B2kO&s{qha4ZWef`8z+AXZKx8k;sB5(M-}h`7HVk&}hUcFLO9X6! zygxu|9emY0BI+9f$m!Ez!v+{U*z?2Fr=hqQ-hEfS|05h(0*MBQ>v2hSiU2hQMMS`| zWl&NAMx*CNZ@&!>KMX(q2vN{f4%K;Z`<3s9=mJ+2;378{UU~_}jPbm%u@Rnl0$zR@ zq=bApSq8>_&~Ds>@P}&@03Za+nFG%~2l4TqpMCZj%$*BIk3ty8QaCsr9Du23MAjEx z72qN^7M^)Vt%`)(Wb^B<@YGYVW(~Bq!cb`V7dYd<)&nx@it8%C#e@m);){@-?b&|l zkW$W+4lf- zaIBl;)g2ug0q&%y!}|4b*Il0NCr`pd55dPDtM~Un?NW#_Ky-j))g^u_z=b#8K}y)V z6&5appMHWkXn7Wn=RsD0l*(V+|+5I_aE@Z~#v@r82Ws;j{W6hUnPqzW(x^7n5x0wDxTQN}xEbg+1pt^#$_70BG(@^(6OJ8IOV&?5Q3A|_?-oOU zBlPbsS^6s7ssI-@n_9BE`wmj7z~17;aQ-~RgIoir96=V&=n)0D@a8+z*TaGZ@XkBx z{TL&XlKOc7NfQkyZ z|9=04@a8+TFO$iDmVd&zG@a$6eJUU-3hL_Ml~+`r7{G3aq$Jq8S9!f#wy3EBl!2|N zfXS2L!w)+r%=qzYT5|Ut4j)#Esj5=LH6In$&=bmOgK+W|o zj7C))A%zO8GerMGxm_Hw4V!ZX}A2ypZ##5)Zwbj-t8`0F%_5S-3UN!T=3&^@U z&$a;rkefFno12kq*CNek&(BLr)YGbIfqO!xPe-0Q)ixUR_VT$3YHWn$WX}*nW23SS zufD2Ml$WloOH3xvG6NI+XipEp`53%Fv!bOiI>hWVS-wQ z+U;#^?#oc231UaYqM7wGC*3dHW=r(0!EKkPwqG0sAZ|Y_$q^U4@Vm#jAT5RQJa{bm?jYV znee^P=MpmUqvIRx#rDfun5L__-WSFjU($8Fl{2-x=W6}POlVZ z34)z-aT-j5F%8V6_AcPSeh6;G}@W{Shmv?WeSxh5+DWm+Sun_oNEX(kWrXX zlRP0gRGjNZmlWVCF)1xo2G_;+->df5UQ_QAMkk!fT9Bm~%!WkoJq0hFHo_g*pkcO} zTe25sAGg}Ap$!^7@OuP!D>H<4Ta*N(PEI{_Yr(BC!XT`H6IXw9cm((=v~6ioRnVoS z%HUd(El$VZ9rczZOJ;WntTzI-O;s2Kwm;db2i-f!CJ>=*svRUC8?~c`L^U{;IL?{+ zn}+m&%(|jWM-^yh?Q&L8*5SmliNgpjEjz_E3h=ezuKi&haU)lu0T7&3rI3S!g0CHc#{f#+^Go%l|+b z*7fUQ>C!7p%S6jr$1=xhOOhqqdp99E%XJU!Lx63j?LgYhwA&D&L;Jb|pT|~4rbqrS zcWLfLQ=};rM(9SaIk;6bX_vB!vZ@mD6GjSxJ67Klh{8hTmMv;&8GC!|e{vS(L}+F$ zsPeMCi2$TA(s(}Sv7AFzyLFu2%Ru@XBIS;h-I;}%HenDcy)8^I@_+tMe{f(qqUrzu N002ovPDHLkV1g2+R$>4E literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/VEN.png b/assets/images/fide-fed/4.0x/VEN.png new file mode 100644 index 0000000000000000000000000000000000000000..4dfe9aa593a00a3e216b1d010dc8d32344553dd6 GIT binary patch literal 1382 zcmbu9`#aMM0LH)Ww=P&Zau0PnGjo~Hv0P$~g>^x>Oj*OsVxGCIh(;0>j!Vrhcq-R) z5yBa%Bgd2pZ51&jb}^);vJ6LmL(lWR@8@~m-`_XyDC)1Rng*Hx0BlA2cpcwl#HM}J zRX0-uw}kzP1fP&Z08n)NX@xx=HG4P9T}g=GqyS=c5*b5^0?1@C6dzAW#9|VnphOam zu?RN+0A(rC%Omg{bAg`j7Zhd0b`vsJ_oB6T^yE>la(0Jwy~!RTmDMr0BI$H0%qVh%Vxo(1>Ej7C1G%R8V_PQ-NSbx|5M&)a#ln^XG_wz;82$%!Abq|ap+ z)>+@--{)7ELaDq$$V_{<)7cxitj|hPTO^kPm|v7bj-Y-+MdLQuy)5nUvwNIKG-}Lv zEF5LnY+qtMKfEEV(6vo`oiLGR4We?r=jz%EKGxrvPB;+Px$$F1bmVxL;v&2}Rr?5> z-gg^kikVxnWTFhUntH$Rin9v7Wu$xJAp`%3ZDYQ+f)-OAtzUaS?|KwEB6&4@2W*xc zX%rF=0QeXhA88-ca3B6xGlg1;CF=w8ZlMUwu@Lt8%ZR8QrVYx^Fis=sQ;G;RD?pv> zf;2SV?*5ji=3{)mnUtz{a1PoR4~uav>aTB3_F7IUDlIbQO)U9tGi)`963j%_*o=3} zURnWLO!Qw&SW8Dw1@?~9whxo%$2^2V^8>xyX$$;O)6_yVf-y)O-M{vmYPw0-ZoUTK z&+Q9w$VB4vdU~@Ted7{(J7N%54`G_D-}KD&gr0Z_h$7!gZ+w zI&PNtPpF?%&}rZVF1XIytN`%ynby? zdD{1Hm!8bp>iZwpUe0gnK2#1#m$0D&GFzfS{=j|6;D)SVR5$l*w-^f=a>&wwFvbN}Jgx1AQPUf*#JOD5`m=R?Kztp!i&BHHDnyFDa9)|2M~qDEgFh7STGO2MW{nN#o{% zLWe7jJ@YxQW!2x@`=Nlo1QIP|=ZyRuMYKpBh(-ydm7&FiQC$a1*JA3)3rF~~gylj~ znBExvwgq3#AL{Ooy4=J5)(Ux7tSOpp0htJ@zBOGr1$DV}#$x43XIBUI)76=t81P2y zR4T#6ZaAJ!??iw9!8&+S-rv4jZu0fo$Xad+9E=E}nB7&(KK9F!saYP%WTFB_0yjSr NKq63H4-Q3S{SVu!lzRXG literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/VIE.png b/assets/images/fide-fed/4.0x/VIE.png new file mode 100644 index 0000000000000000000000000000000000000000..35bc7967d69ece9b3668957afbb18674c143288a GIT binary patch literal 1177 zcmeAS@N?(olHy`uVBq!ia0vp^2|(<@!3HEb(?2AGr~;43Vg?2#GZ1D}bzG(f6qGD+ zjVKAuPb(=;EJ|f4FE7{2%*!rLPAo{(%P&fw{mw>;fq_NM)5S5QV$R!H=ewm-C64cZ ze`e2nwVNuluZXgUrm!BIb|^3Q?xNRatzz3xm~HiT^vcYCyENp3y+)JxL8tz&3qow) zEVX^Ha=DYE?c0Oj6tp^=^1EjKd7(CW<{r66|4x`qzVWAO-t=1g=^JLgIdl5=%~`Z==uBCjBmoPEnP1b z-VXnL=)J1c_tg0RxA~Lq9Nl^Dvv0~Z?+^E0{ZUxDq|CVS)l?pXW6OGNn`ZUP{5Wly z6X(t7Gs!8O<&=C%;9i@1=h!*i4&+_QYWOl;wS&1t^wY)ldZ)4@UHFg9JI2r?fNv zeOq*Iz4BjI$)v0j-%It@8uL#tC=Bm?BjwNhljDYOLHZ(5PWxUpp+mgSji^< z)7+x?m>=9b7rF=iUCrXvZD~^fKEKvL>jozSL#*(Mx*2Xylh}4yMYz4*7A!7#T^7VmADt}(fUIqUIW zYrI!;qN=}PE5pw`1|F-#o1ykc1g|k&c~$TzIiHnP{RdY8V`-6vta?AI_4)%d4~8|A zZ(%gNYrA`^e!yRi-WS{DimN$Se6Khr5Y6~1J0BjLe;&AdUQ4}7|1WdlyXTLa zwEf_U=-r>ZJ#1@q`93}FkaOv~_3k|JVwm_u+*yZphq2(D@cC!_EJrCUEnh*Fj z#N;g~%rBY!E_k`!nK%2SE+>VZ2{fCQX)T;T#OQfUz}w5Ux}(U{{T`dC~BKHNTflcRot9NQd)|QH{M+iw(A<(>kW!CvuM7PA3Lk@e6usZ z`TeZXbI7HRWWPRB&)e6X@ad+y-ak~=yDMs<1PDe65R4Ka7$rb3N`PRL0Kq5$f>8nl zqXY;>2@s4DAQ&Y;FiL=6lmNjf0fJEi1fv89MhOs%^a3nLOxvwOdI7IoY~ZIgeZ%c8 zTniDOjntt>n~T%}=Kj$6{EE)VoQb=E|8BT0#OES>dfDLoHK~81+ydgWCbp~bf=?*)C;kjQ?!jdVh9R=iB9Q zUHZ5gNGTv~YaC6K&iLiD#>baxKLryDCQIp3QHK)|Qg&^5B9KzR@)+BucXMMp9r zjew1WjD_<-fzK`*T<{U^yDp5(MQ|hvOQjKTBdf9ar~mGpzwLDFXYdRBc2nbqk8pOY zkVZhW;c~2R-Pg5EVHoc(Ui)^xMKtVhTEEe^PO~A;BOr|cfP-BYJ=;LJ;aU%m)Co1%j?K98U0N{yxvOIRTUG@8qhB7qhxp4Hm-*skIa^a^Lg5!hLRycq?wKL13JF;c%!Wx{|ovCM+|1(kw=#4t;?<`%@ zb0ZGWJ=?cUvsDDsbP!ozk-|tUF*c6a#m-g6aX;RXK|NVj8%!rL$;_TK`#aZpu+=Hs4`f5?Cjel$*~SRLmrG&J`6$kxThlqY zWMG7HZDmJ#0R=u-@SjRPz947ur4oS=@a+8+cHSyHcc1@jMH3hSuXS5=HP%j|wKqD% zd*sUZco7%@5gpzitR2r!3|chERdewqFaiLc*_Gvyw(1+}L#;MX?+VWH0|65NFugld zF|^b*muX4!JzfM(KrHI;e1*f+-u5g*cgUIV@gi^n0Nxw0Xt^y<)NB;f7C|GB0$NO$ z(Z1q8nlqXY=%9}(MYTnTaw^8f$<07*qoM6N<$ Eg5ZH&=>Px# literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/W.png b/assets/images/fide-fed/4.0x/W.png new file mode 100644 index 0000000000000000000000000000000000000000..cc87102a65692d139abf9e7ccf3d8b20853b0382 GIT binary patch literal 4311 zcmV;|5Ge17P)FOb^tLZTL805u(jNz)iJP12~7Gzs=b>b1>8 zXpN~;oF=t_&a@OYHE5dzC0x@~xnxGf6a{1zL=fB+xh%`x+5P>o;ha6}a@jp+0e{Ui z!_1!D^PTgZ?>*o9y_e_xU;$Sw_!jd9_I!8Xe;u6p?tm-c@mLTF+3@qTc7JHd1hv`> zqw&$3%nMooUS4dZqh$dj*Xeo(V;^}haT)x7TOvR7|6kM&n-fBwjb845rW5K(Yl^qZ$V?D8t2XpM1i= zFak(R6Co!j7#SHp;Bx5=`cyC)EhsM^K|z5MwY8&exreiucL8kOD#n2Wp;)s5^TVjE9fd++L}g_hW$?9HGdw-5Yr(*P3Dwmq zyX!vtiS>)r5_Gh@zY0EffE!Godr z)vrRp=TlWpZ?6Fb1xl2Z4C4NMBLINUXQ8YtmLjOnJ{v(~Bo|3ZUI2hIX9lrjN7uCX z4GraD=gvjQ$q7byIG6I9UT;Buen0Z^?qhs>ZWY3u`T-FUJk-|4v-=y1u^(+HgNiMG-dFss#JL9ncFZsY1s7l8*%KI0x>Zy*t4e_ckddA-}rnM z-hMj_H8pVv3*%0673MK#0@$!Ygyv=`o_NBW_)f34ATO^UiHSE*R5SpCf%^9C+ZT$g zEMMaItFOk8m313>z3XqOUT?vN9}Yk&y@CAv`;>cb*dW5SYl)OTx*KyMfbH9T@%iVm z2nZl`_}5=+k(AVmg9rPd)!M(#k3a5>-~1+&IBsdtA~RDCwVD~5UZXMNz=3;6N@~S_ z|Endw4-Dj>vN8_aw)sxenmM>#0587ikB>i&hCo2x>g34>ShJ=b?d>{;ZT$Q^P*SoK z9FA2@snupY^Gqj(hQ=B1Ut60FYu2=*uyBC*P9R{Tv@{wozUa^R{U*b$0{H&-eNkAr zm|}fYSF7;WTlb*TP5mYp6)mBdkh^wum0A zh7Z7Mq81cH66bYx>allkk2|^ui9~?o$E|(H&ku*6ANevEjTTtG>8__I8|Ti&*qN?O zfpGzNc(71hyp*!w{_JNxNJzMW)29b1=A&5bf!y2>$mPp$>5>E`B}*Zbc@n?>@|V4g z2GSxUdHBgs0#RR|fcAD7e)?1DEHs;e;^K#hi@T23U%yLy9~j8N>C@2^Rmv0?_k<4} z3PozFh&VoZ@&W$v2PFXD`RBXv?z{JK=ujxOZ}$WH8}YMPR;$Mry}bsQ%|KWf7kzz3 zm--SB!Ncm+LZqY!v2mja$;n<0&oh}UICpLsM~?L4`gN;C^uY%Ykd)+wUAqDR0PEL# zWB>jTyz@@KOMRINjC#UxaeTD2$OtE^rA3R?tJ@u#nK3bZ?A;rLojd*E>ubLj*kWjC z9IdTdbafe^R2rdFn(*3dfryH-F4R}9jG?r27{S3D1O;&s9nFVSDwy%QgM%g%7Z2jZ z2_?F^>_ZU(0UHes62jxw>n&KhvXxPTfKdTdRmBlrt6pzGQc~-brsf0$0yeg76QihT z34}uTyCq{|W>i+HaO%_`%F2dkbn0X>Ph7n!CBotr6)I$8$QkPoV=CzBr^S?z!Leia zXB>P0(CN(3YAuw(|M=q}ELp-sa70 z{`U&(+ZO@=NKf}cW~LbB<;+A;8JhtO4GBa@`OX~!5)*GYR6UMhJ9C2$fe~uSQDBO7&xQG|d0n|@M<5v7}x z6HI*W?KPmd_~ESEZrLIR>l-bj)tYhk><|-OVjO?{>j3WEBdxK$dxIFcE^Pq>26B** zLB7ES1xmZ9=5%=aX)*D+re<`(CeZbI3uSg@W{TnOPhWx17Ql`ji-;Vq$z(xE$?U@h zd_D`Qsig4A%H}scI1@@r9uf%~9*>1*pQRrsqAh@|EV2`ol`0~cG8?R4FM_u>8Lqf^ zabXLfug{2@8dC2w5vZdj052~#W$`_Ke%Pt@ww`H6hYmeGhH1`T7_fcDb?bygUfNaA zYMqkeP2uf0OZ8Z`OhA18`fKepXD%Gn){-G>fq;#*Yo}&&%m%atFo8RF3=ZA->9Bk` z8F*`Hc{Btdm+L7rb#e^LS0ZdTDC(z&TT+Ci9rV2o%Vm{&SF4&sdB9R9zF00GIIDdZ*1_o9V$2V{4Fg7-O zFIFt}AX1TLGjQ#ieY|HnBqWeD^`W70rwsr)5DM88)w5{PHJ2G1F8k6V5q0v=(S>X_ zh`^dF@bmM4pP%dP>B7gy!({<{TcR}2aCrudj#AZ4Rh7!Ag)}UTiwzsBx#RJ1i_=li zj0_*5M5(*mfckp1Q|;*>Ab>-mu+Uk*Xb6CM(__aJ6k^j%NJ;U=<;xNPRwl(uFWqs9 z^V!rSB?M4kuXbxbU@|f?d?*5-Lzi?}1sMz$%Ay+@>Kt!oWP~a}AD`(H=nDxUA>^}; zj*inR4y3gP$mOm(B*Vj00R#ojEiIf5Y`tG+{h!I8B>>x-OC-)YBz=7b$_$dp=y{ZL z3EP*gql2CYPfGx8ZRE{Yt#U3ZXf^|_tz`ViHZZr)@tyCGCNJF(3M~QD)sdFR;>A2f zM>{tQYy)tQiU8u{1&E9!ncJ5y)3d&43&7TLv85!=2DSoNvEtDXfbBEtbY|4oI~N6> z3|a!vXv}DAB;NpMQ*^d;lch`fv;$jn0bA;Pef=11&Jk@-*fvGCX_Lq)bF`shjN-y? z-8wgAL9QY!j7ymTrKR*KG_(b9=FA{r2YYz1uyg04S+_NrEU2g;6Y*QOil;ey{_(;K z{scwcU~t}hcLdr3c<{hP;YsG?1l!gBP6t~m$`em`&&X(=F9-x|y!j?c#=3ZM1P>q5 z&tWhUj4LQ02}|MOT>R)qvoCHdFIT~2vd$nbmxUKzaDQQ(D|qQ8e@YGUM9l+TFham{r6_ocH>47zW5@ZIR505VT6To8KtK;G>oCTT7}x$(ODCRCvfy=IC67Ch~vym zIX?e<_JPOgz-WO+MMWH*ddeClG#IP}8n)iDBY1kUv3eNFN6ew})mg5qo#>ND!TxoR@^?D0dtZ1bj|FH$*yQLgH9E!K!4kL~~{BQt! z_H+}+t5$j8@Zm6Q*&-&2^c+E{G~v}(yV2M<=CDZ5wmx90-BKnO=_Mt35mb0vn9aby z{xytu-sz|0rvLJn#d!JU0OI)I!9L{WF6?0nBELU7hXVpb3Be^8gA92hi1}2a5&N)sfbeLSckV*6PsSp3MxH4zaO(?A{%S z-Ma(qRF%nO?dybqlu8qllUr%M3{$}^NqL3Bh#&l*ldv{eEKmktp)evRrw36{E!e;R z9ziLwSb#U)xJwjs2M2ReT)f1g+MtVDxAe%(?L}l{3x59dUP?Zj!(maXcCxZ=yJheJ zH?krwT~g!OXFCZeYXTJ&D#XTK$B7e4hdiRDCJl;;$el|zZx&NLP^K|DI*#MV6^Mzs zZf9DKk6W;FXBQ>x&NSRgOZn`x5xnwBH)R{J^mHE_KOQ+PuRbTIhq8yy(W4QxctbP6 z%ZrWU$0I1ERwk1LFTdRFbj^daa62#PZ-0A;?Ceg8^RsJL02&(;keKK=4XM+av13OU zVZQSDEd2AIF+`atlaR?g@zqxe`0nze#Bppa9~Upi z&gfuG2R@&LS6>Z8Tbm5Ixgiu{?H6B+B00IutvY`)=9HS)*JngpnjD7@_fbLsQBgdc zJQ;=Cx0fR~H-r*(eDlq_ICYAwyi88^LPbTK(=np(a4z0@%epbs2OmUH^13>m83zyc zVbdnL(=2bgn75sCV`KUF;~%4tp6+<#)|#5pZ#HgKp|jI~vuC3z=|F?QLe!^CPR8cVVjMUSf_3YruC?mvG2rS|Emp4d z#F8cU3vpGI3h%vV-MTnF-Wt08?svV2Sdk;t)v0mhNIzpyO;<7R0+_(}z9&LXP7pFP z#k6-!p9%(p1s5+`H}7U*p9dzeU<5D$e}4{s_(MNrXZvC8S|O#5%oXT#X4KV<;r#g_ zeDu)}1_u{bDb#`!fGs>d*;uzuX!8#otbY^L^^>7iPy7Q1>ozrYb!tYaBksh47Qj^C za99wD*zonW26KmpO&A?D+l4(I1w4`h`2U1&OO)mr{|DJOW?-2@`e^_F002ovPDHLk FV1nrP6o>!- literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/WLS.png b/assets/images/fide-fed/4.0x/WLS.png new file mode 100644 index 0000000000000000000000000000000000000000..01848f9c5bde8b6e4143f966966ddec3afd14f88 GIT binary patch literal 7320 zcmaJ`Wl$Sjutid=Deg{z6h5R7TnfeA3zTBTrFbC4-Q6967c1_xxI@t51b26r$NTyI zyqUXqXYQ{%v%BY<-3?P!mU)Loj)jDT^iEDzQvKC`e>GVQwAUNc;?(4SorA0n1PKX+ z_kRtUQLG5``bg$1_0?I!-rV`8k&_wH&!0cpt?Yh4OpF}N*zKJxGLD7Gk&q}EtQ(Zb7GygZ{KNb*9XfpiD5Ky} zJcne*Xy4jMA6=Ah`U(5dZu z)1>zYt(|>u0SYcGrnR+Ze!}aChl==$F&6E4$~V43I+fy3Ta2-Hd|f)2;5meK;wG zIT;y}rLs(=ZC594*~_Cg^v^XmDwM`J{Nvkxe;TI={h!&%tbf-c@o=LgzrHLfNY2_Z zdgoyG$C>NJ@vE6>lzPn3<~`b)qG7**l}5zVD6fi}Bp!?9VbGJ6HkU9Dt)G*?Xj%An z63sZ35^jivqt70N01MwpOFRbf^kL~5Q;J*3wvp)~^%n~=EHkQYr|(Kvbh|?vNN4v9 z1T(#R@>e#x##Uy2c_*p=xx7vA+tkAZb8>x68R&BZA z$XmQ}GQ(3zq8Qbt#sUZX+j0s+F>-%KbxA-eX~?Ick*@rqw&_oJEblZIp;bpRF|nwWMQWxk2=4hd5ko+M|0Og8?yS|SO-}>x=EcT zrKe@XXTnyY60`e}x>j%dNxagUtlKZ=D+IafB-(A{1<}o)HR|m(^3d)h83&tB>&+vqV&f&3o2bdOmzc@FJH! z%eK?;7_v`ZSi?R~KveIG|3q3BJ%SRhv~m)BCnB_Jn+`!}D=$Rmh`YU*q_XC=?4+Ad zJ*Q@7iMn`LJRk19wPaPsIese))3Cgqur)y;Gr_I(!lNIki_T?VmbPyDMv6G&(%9BVPL{BM19AKFlS-B*xAjJqv=O=1* zV6Inblh@ipE+X}lw~Yp*n$GyB<|WN(qS@Xh_xiFb$Lymu(DU0oK?csHEz7#Djj$=*Ihhm z22$U$p_-KND2UFoKY_*iak0^KoEB%#yqXAb++!x7Ku1Qu%{m;Wh#NTcTj3xRqMMaI z!4vL>vTvFh0F7*pX64R7oVch__)bJgWB_aJOZ0Pq_5_ z^qD{?%@6TR9M!i%{W zz40E3m0~%S?UZKe03iCkg~CyS_v~tdIt_%x73HK4vPX3sfohBb3?^ThRdBU<(T_;6 z=}7aM_eixmdOS|dm-S(vaz8HIJ^vT&c|aGQ?tfU-#UJ!N@Hls`L81A2zW=_wUjtj> zDUEFDexJtmByERO=!G1OnCRM7x>qTjq)i1yigqf>b(MM>aUqr6wmucPs3-Mxz9%m8 zm&eosRjLs7O;cPQo3517G_t(BjPr7W`gtE>#pG}j4Di3wLh=m&_c3(GAPpU1ple1t zwveQZTP9*+4}JYcgIS2{8wa*`>*`e027$u&$MlA>VxEANqF$dVyJcSV#HJd3l+_@i z`I8KWknudlOpL>Z&+V74@_>Jo!UOv|2HSlPCtC|IkJ6Q6TY7GEaY4iLBKDL)1pQ9k zvu!;!hL6F_x}^e41l%kMbp28^NIJ9nU5qQY;PW{2s_5Dh7MOc9rve`x?!O^^>blW1 z4SJJ*g8(#?%JJevH6%2w58d29$uZs1#ln)0(G`(hUL3g^pV$2i+N~BRw+3{~gw z(Q*txsiwD<)d{(^ZUuX$r3Zc*?{iFV9k9S)9q8h0lhxpG)6xg1Wsn9)HR)hS!RHC} z$ZvBX^btbUD{fJ`W)VUpE#&7D^|Zs?syYz=$x*N*NP4~e~4MZ~BQ&5vKq%JEf zB_gC;29ZFN(VcN1{4hzF#IA^i#LDw0CbAe$l9ZMHjy&<9S=*e3qTtw?YPFtN=Yeds?${yso<lE7fo7fmQP z+!+oAzj6o$Q$&_lDk?N_q9sHwIOg@N-4EtxgDN{O(T_hF^=N8)g}V57WW9?pu-MH^ z8+kxT=v}qGaSg^XcaM-OlW@4=icuA@1xPFy+vv8PZw?&>i;VgH5V;{sHh6eL%;p|( zK=PcuJA+Wh`uk&B=dHqaQ9YhG$HsMrpwEfh?Ty|HvL;xpnX1qLL<(6l zE^)Wv6u4PYa(L1zpUzl~fXTIf+!!pOR_7JTrT2qt#RkR`{$K}Dd}vst2{}DH1Ja~q zdz7+Oe!uV$Ro))@O;|v@LmXb{CPF!@uQ;5Xr6@5DHX^;)hMV=|RhK_bpe!W+=|4QE zIM!(Q0(I;;;av)nwb!Tpju8$p`XDhP7}s$&2ic6KaGSjDj7GA3xJFei^$2}GNGQl@ zx&SGXuo!(j7`JbZv*cge_#}=YKGe6uR>~~TR^vvBj;BFlQLW;8&15pM9_Fb=FEdIm^0kdXoCyX(&wC|x(bD`&t}*R z&0YcA)pL(mm-&slx{-{~6z~eH9H5A+%Ii2uZ;cGZMJr4Xc@4F1&AeI2yj95ST_PX! z8);@@rmkTz+bd`?@x1I^#?StpS{qZ|kjM(;SABvZo3RvOQB+Um>>paicNyX9FEE8k zu1d;nh0q-|BC}}X#sChPFR}gIIV!tO(r7ZKkl-fK0E)^K%6-;JmFMg}tzC(R63E6r zt=T=h+&lqL_vrMuD)i^rj)42gB21HdrWjdGujQf)+HD*4*>I+@%Y{hx&6~(rYHR)K zA{x(Q6{$n9X%y#9w*AGkwSsh5lswu#Y!Bbd)Yd0HpELCwfvd zA-^Yjqtv{Pul0O*AnEgfrClFrDemClfKBqzGu?=w|7yYRW^&6?%ijhOg%o^Xx6G+~ zTVCs%1tm?{B}bP5XGZFsvu-#++$HhLd@epXF%s?#9`q|tq$YAz)Z|mDa)KyCoB`yq zEI_9#0-qwuYK5K>s#LiS-;lOvTknUy_ehla!FjTyd2?;3e@-t?CTs^MYK|Bd7X8=_ z304f2X?Tj(61BSQ4WL4Wi`XbtkZW0yRvV+rF4NR`*5yGrq_cOL<3-!Tl9w02^R|CbWH)ZsGpp< z$468MlKoQd8=M60oB3C_r08pyS zo(#1pwpB+XAX!gtcfI1=tXbaWqsB+-s|;UNrojBd{EJ2vtbJ|UKm!a8r7}nlwM1(T z#(x>uW}IUK<^e<OL(yS9QoHU}XYw8P50 z@1I7W*@lg$PhPqX`)^$ICDv4=#D4jE6R5Vfhd+kCL3DMv-uW58?A?#_t$*VN1F;t-8(|meCC6xq4Tupp}UJ%Ow!>iCBGlTZ3)rCuaGS%$Zk2HMWAM=@Ipm*<@ z_K#hntqE^K?3`w26+IOxLRxC;&ZOyTRQI8*Px&&#cWA4O>_6`R^uyK83TP*~-^nt2 zY{Ch`02RNtA{C-Wpk{cg#KE|(<8yW)WcePX6&`aYG6T4R8tP+qwc>M3!8#$c_k#m0xWhTGYumdw|*NsZBnaik47qxF6k?F130)XyMhx0 zrrEc&W7tiPr`{@m`*_Qb@yz*aOqst`aFhmC#Wcn-uZtmi$8<-hTaH`S_n*O|H})>V zCYagn`WbgeW-@C{n&opw9n*~doO~SJW!?Fjm{72SC3hjdzQ++pgMU~s4)S4LH^%or z#|K~f(O5o*ZTE?*Ir#%%0PH!n>@cb|^z2ST7mw}GNP;iWUupYqpA87N2P@q%vG)w; zFI_Lw1Z6^cy*@IK5;&fo6OkYMqF_+p7-t5olwE2xl?T<;$4ZzCy0-SW+H^2-ugilw zPp^s*4J;9_4Qlw>FR^ssWM*-J%r2q3ufsS~E3^d83oc3KOgGKb0E8K*P??7inO9O3 ziGf^~_}(ks#p)>laj~!G@IBU4%;zN1GH)Q3|E4aPGWN8F~e#9jgk z(fA~3ivfzq4#xSfWN>TUOmef#L8AoC(UY^Q-v$&4t=IjR05Y4TRhrR7-sT6B{dyRW zUaTEwA~M}1j#^NvrIBBi$`A!%&2DDk71lo$JCgw$l*y? zq!>rX{a(JPkTbfI#hEZ%knF<=fT-yk`T%!#OptK^(f87|{w{R#>19n%!3ItjbSbi;`8-OsZSY6SxG z)#5R3&hd`s|8pI07-DdZk1Yan)NMN zu>hyqzadg1oAfVEEa9!XFYqB#o1x~Ds&2{HI_3x)_-Bb#@eTicWp;!qo=v2xex8ZP z*yC=<^R4ePZH{|<2Esg*<>ufvaFiB4_!%B%xC@Xq7l~*UXhX9CS5YAAbgj}0^)MjD zWT2teWb_id<=l&5Okp4ZiC|4AZCddIX|X^U6Gh|rE4Ek7M(4ck4GK;@NIRa)kwLAf|(1mgoS8vlq?e;9hUL%SW_HtHkHR!`o zIYbMg$Bn3;Rh!3CqMx1m#?S%5EnoQ=`^IKJV7i|F;n?ct z!6qW_NgND4a%`)Yo1i7o+aAdk>mysT^$4W{_-{7*iFL^V2Wk4G?jEuXx&pwQ(V(yA zzG)4s6nV|Ns-Gerrf@n$yLcyOBT%OSri_EdS!?-F=|&H%>$bT*+aK$If?i2P|D1W$ z&Ix?oP>B8()k4|@Hh>(JbfL#>Q&)VRF8l%q7JMi{GGZPzq>v%;e;qv#h=pwzb(uMR zVQAvmmOy^c2ek{X^z3oX@##%)7yc;Fxl<<2CfRQLySH8R+Vp?BSwH06tQ6SqJo}!C z17eyPa~cS1w(T6QzdsbXe8#EXVA~8$h zubH=y!Z6}eR^P9V25o$5gcMjm1M^rp2LiptKbsY&jyjzV<8Q&*SVAXPxXA<@8zAdDfo{ zg5#xTBbeKhkCR0Y?}|QB1-rMxP!}uHMYZrmUvEk3^Yd_CA|jLf<0AXT*~~uDOFH5v zX(u=X++4^#GESZ_vOp@jyx#5iy^P2>I!-R5Mi8PDql8hr|Ng_yNmZH}W4^xRro1VfTXgDV+QEnG99BB_^&&pxmeu9+1e|X(OKS^Bm!O9ZR4#sz5w=e zI+i^%ZgmU{xw}EK%7iz>3{2v(Ig+J()RRWHI(O}7uxeA!Zx8EL78-dSUj@|Cm$&Bc zI9R8}rCpD@wF`C;McIMHvEnvXQ(zDWQ}r?NR=gT!{mDKRobA{>?m4TVS7rGsf3&uG zT-PI%@dXgXDk#Cnk(}E(1+FTQg^hQXspC^(hSj5+>Ouij@7;`FRy(-t1C%}JpwAlb zmaZB}rG)ZWeJYx<4b!zaJQD`L7HtP>tZI&B?@WiMwcr4cC>u>eJ10Uj=f=l!ho%2|_C6*ETQ zyT7#b-vF=*C&y0e-XhP0?`qnng}n^5D-qx8GG1i?w>7cPpXo5KV6MEU($Fhkl)pTk zO+;eew-tS+2$dwR&Kf$*>m~w1Q`aWQN4lvyn=}a!N=G zDr+0=H+>tx+Px}a7^vMR?Ruh4!j|u$t+o8j&-a(gWZ5pY{9TS%DrLj^(#uB@orB1K z-`;!80I{qrIm4V`^Brf2O0hSe6C+k);!Ui9!m+emnmHum9Ey+)ezAQ)g?->Mo>|W^ zz-^0@+$&zvZmdUZ=9q(KX}8}xN^OowE|s_N9Y-5B5YtZSU<8M%+t@JHOyf;`kl?h2 z@CP2pjhAKd0}hh%$a` z5KRel&ibsTMjM9$&;$R!m%GjN{JSiP_S z&O8rZg!_8oMeDh&C&ZFGHNWBM@WZQ{7hRS^)Ft?wjw=}z+3=OXLsDhva}@R~b`gZ| z2M_d{@|n5KaCM%}!J0)b!K`veZoed3=VU|V8l0ZgXR*{7{12mEbiB(#yLDW7zmz(1 zdg0u9F!Z7xL@KS;u3+xEid<7p&=CJSK9`!)ubl(483L|uyw`G+E|szW>Cg$oRX*9G tjILdt+TVzpKiPNxpXJ*BzkfRuCAfQvS%OxbzHSwf#((P=A{aL?C9odgY7k}6W(Z?ggGb=*>zh?= z@w1=LdDpZcOaQ-H4Aai;EiZSxAhWpLH%Oay8-kWgQu&X%Q~lo FCIE`TUtItI literal 0 HcmV?d00001 diff --git a/assets/images/fide-fed/4.0x/ZAM.png b/assets/images/fide-fed/4.0x/ZAM.png new file mode 100644 index 0000000000000000000000000000000000000000..98334b17b428f14c3f4d7393ec6d4c4a96d75e93 GIT binary patch literal 1351 zcmeAS@N?(olHy`uVBq!ia0vp^2|(<@!3HEb(?2AGr~;43Vg?2#GZ1D}bzG(f6qGD+ zjVKAuPb(=;EJ|f4FE7{2%*!rLPAo{(%P&fw{mw>;fq~_Ur;B4q#hkZyJab~oWe7}|0;0ueBDBY^cpiN)Syol^^b8{FFQ(bfF#l(|0^y>GiUlmE8y z^a|tC-`VEeN!fTbZJxj0rl6Dwd;casH~;xZX{wdC>TkPJ@l`gved>Cj-->x3c%DD7 z;bf@Yu5;b4ad)2;SMIa)OP%Z3JH5&zYtQ7R^Q-?Si?8~#=JZUyc)7);f|X&%?SJgt zqOh<>Adxy-eJ4un>g$1 z+80kQ*)-?;2e(7}H^u0E-paerbmqCAAFb?doEDz-dwJ}G-SKNbbK3pi8r@yo>(?Lr zIK*SkZbteCCdvFc%)HjYLeCCD=K%j z`umM*=S%FZV3uVi1x_z2}fS^R8L& z;+L+TOHWMwe(7wQcTCjdA11wgpSQ31etcQQTV1uHmu1ptzckF>ed>$Ee7hsZpIe$t z)U)+;%5DED_WaYIZxYSY`H!4G%YATMW@fSPp!>ASR|>Z|izE5Of8I{|^(rDWQ0`$y z>q@58ywAKdS437XbeVb4s@?0#?JvJ=j81OVIx2X|(#`#a`ktMhYKJ2ZeO>!y>c(3;1e06h^FAG4 zoO5Q!+th5H5Aj{OVrCUf+%}#{{wyL@lh_#7Uu7PgFK%Jaj_k*+o0 zEO*bimowM)@Xl(BF2e(|T3;&O1Qx|!W3PE+AU^%%_7^^P{@&Taa<{R4dSzm-`qKO7 zE0bp1n^tV8$Y-DbgKLNKd%w?exVv2- z>8@0F6T!q*w|{|UiD+3hE=ECMDgrBCSpmUfQ4~=IR0awT4)gYpR|PBRV|bu5!!Mc4 z%)9U2d*+-u=YGBWo<&qtlyLa)VXy%f12Y+A1hEhVLCDO^Wb@|D+`D%VW&!U4)-PK$ z^T0w71OcF-p@EQ)5H4J}0M0-lF!ALIXAW3C{xEIYG;(uuiI0!Rs@)1s0M##1GBe?c z2lOR3HRXml$3<+BU?BPTmtkXC!9Gjpa=A&w6qk3LctF|`~Yr<-rzX=!kGgDdO%<1&!10e zX(^$hq0kIu0zVF~VCI4(59mv3YAV~eZ>OW9Lmafd$LN@02n^Z)09UVGB{(>k^73-L zz2Hx~z}ss$#4~*iG5~;9t0gWjj(z*~F~&yEo>+(pky|{|$6y0I=Iq(CL_|bz=gu8I z`T)L30O!#LS~5d60Knb5cL@&<=j_?DczHltBE0*S9O9Wil6pZe;C7CnwU=(@9B5q2r-FzuN&ZUqbgI*=jQxWHZ3Mg9o2E z78Vx9rAwFa^LvBjZ{fWNxOP*n8chRP44|#8MV*jipRZNVIX7X2GJ@?#(h6a>MB?Scq_yGHW*+Uh;h#7JS($?0Zt*Qb*bNqO}VW=)D?a6RGKCuLvlHRfq_(1RIqa8O3nix0i}irWW)@* z4Y+lqhD&FnneA4N<5&&d?a&+sj@@FqwW}+$Zr{ejTKXb^AP5{daDbgVccSa=CK`xl z7)FV&AcOvmo?f$gbMYr&=ZB{oOrHT7kKS~tVW=+_78ZQ`@y8St6fk|}Ob!B(h>LHg zjzNF28GftLIj-+L;P!O-V8T#e7A#mmSy>q&At96?hNL`$N-`b{_VONe3n0!wRE~q5 zK50^VLj&RH=*W>HM@UIY;nBEp`~gS+w1x`$iu0rm(6_g9CpH#MW~QW!O}?EhOw^(( zgLmG9>QkUU49@Lhx|Nd?SW2+3r6n$(eTK`YpFVSbZEY>V!NF8iR4@~9%KC5<*yr!pla1&3wJ20TZr)v{~X zE|Qazu@MBqfz{HAHwyag1$Ecg(-;;;XF@n2ndjz#~2teNLyH2;}#!} z&xH%v&X_S^PN#8@S1P`)-Re!Aw+bdF!7abuV|I%cF{7-E2~kl73G3PZu@dOFmovHHbTZS z3!(Az&jr0sC#5<`6c!c=K0ZDI;xv-cz9e-+ABPRkv^1=ooY30Bgdixr0&{=vmgx1+ zd=I=PLr)LP_J+%KP*o2$YrM&u{NsS(%n%kWT10tyIU6=?kgd#@!JsZGbW$i#fAIwx zzc%c=V7j|_4bXq9Kvf2B%!JUD;4l`P$APCC6cstMJo++rb`G)&uhnYNXf(2yJCY1) zE=XHbL$&X(x%Ovx_Z@MKSN%YI7X*0kEokcy|M}gI#74UMupL_qHG`TPl0tTNHsRsn zG&MEJTJA_QsF846dOCNeLUtvT*NQ>x?Kz<9>0NMdz6TS=gNrjv5a8{yE;{q`WfNGZ z(~*#nz={#!30N0`j^-%0&sMIhHO}_hqjCi z9D;&|EbR5`*HJ2!6c!c^xvW@F93JKq01lg%f!aFy;E#>ZLDF8t@!R} z4->zKz$;+w<|a;251|o0cI+5iwrt_Sg9iqe@hT$O1KN%r?fp^-^Oca14c;Cw%^mE= ziiXhjKu4$8HcS@h>(Dhf)0vlNkQa1yb+LQ*ZjzFcum}E#xIxEMkZgdyQ_T8nV((|m zIvRaI342M1GGS;-rSlX0GdD&f^fQV7ykRZ-{HJER@A zb}ctUy|HwD9p_2Yu^sD(rPWyUdL0h7?R2+aL0$7b7JDxT3U#VjO5g*Q%nN(Bi_QoRFUKXb-un>Iym5Y| zs#SP={q=z7<>%)U5)wjVV*?tsAt8aFpdjuxHnJT!gy}0Eqv@Tc_|-W(xo>KA3h{DHkNhk*72~ll{nxZnCIsE z5%1a6)YPC*D5$8YU=Cv6-ki4>kEj7sQ&ZWtZ5y3G{lwPaK>P z@#4i?udgF+8)W};RQbPTSUfC;1_JQ=h zF!yyiN;Z9l>;cD*AIIO{pNkhS61Eyj(npm)7z{Q*S63IYv9YXQzn-2)_euK_QvM1K zc5;em`WUoNxVpNU;NV~`U%t%DnefRc;68d!QG+n(rhU=T(R6lpf-kTT^Vm%{Oh`6B zTU#4ZQBfQ{dKBz{rI_9d`Aa}jFQ}-f2&GalPs)A*fU;ai!#ap4pB%%FyZ808j15@&T3rwj(Q;x$sm7 z(rUHD#>TRC?OIwjt>SY7f*4i)F!IC$YHMpzC=`^Jm%}8)N0^Mx;|x2NAolGSELb2a z-xpZJsPab$3#C#iq@|_7SOk`!!)x>r4I|0_0lNmFiO*}$R{#J207*qoM6N<$g34$! AoB#j- literal 0 HcmV?d00001 diff --git a/lib/src/utils/lichess_assets.dart b/lib/src/utils/lichess_assets.dart index a77fde17c0..a2f43bb50c 100644 --- a/lib/src/utils/lichess_assets.dart +++ b/lib/src/utils/lichess_assets.dart @@ -7,7 +7,3 @@ String lichessFlagSrc(String country) { String lichessFlairSrc(String flair) { return '$kLichessCDNHost/assets/flair/img/$flair.webp'; } - -String lichessFideFedSrc(String name) { - return '$kLichessCDNHost/assets/images/fide-fed/$name.svg'; -} diff --git a/lib/src/view/broadcast/broadcast_player_widget.dart b/lib/src/view/broadcast/broadcast_player_widget.dart index 9097313394..8e73f17394 100644 --- a/lib/src/view/broadcast/broadcast_player_widget.dart +++ b/lib/src/view/broadcast/broadcast_player_widget.dart @@ -1,9 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_svg/svg.dart'; -import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; -import 'package:lichess_mobile/src/utils/lichess_assets.dart'; class BroadcastPlayerWidget extends ConsumerWidget { const BroadcastPlayerWidget({ @@ -25,11 +22,7 @@ class BroadcastPlayerWidget extends ConsumerWidget { return Row( children: [ if (federation != null) ...[ - SvgPicture.network( - lichessFideFedSrc(federation!), - height: 12, - httpClient: ref.read(defaultClientProvider), - ), + Image.asset('assets/images/fide-fed/$federation.png', height: 12), const SizedBox(width: 5), ], if (title != null) ...[ diff --git a/pubspec.lock b/pubspec.lock index ac336549b1..efd5243f46 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -697,14 +697,6 @@ packages: url: "https://pub.dev" source: hosted version: "5.2.1" - flutter_svg: - dependency: "direct main" - description: - name: flutter_svg - sha256: "54900a1a1243f3c4a5506d853a2b5c2dbc38d5f27e52a52618a8054401431123" - url: "https://pub.dev" - source: hosted - version: "2.0.16" flutter_test: dependency: "direct dev" description: flutter @@ -1035,14 +1027,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" - path_parsing: - dependency: transitive - description: - name: path_parsing - sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" - url: "https://pub.dev" - source: hosted - version: "1.1.0" path_provider: dependency: transitive description: @@ -1601,30 +1585,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.5.1" - vector_graphics: - dependency: transitive - description: - name: vector_graphics - sha256: "27d5fefe86fb9aace4a9f8375b56b3c292b64d8c04510df230f849850d912cb7" - url: "https://pub.dev" - source: hosted - version: "1.1.15" - vector_graphics_codec: - dependency: transitive - description: - name: vector_graphics_codec - sha256: "2430b973a4ca3c4dbc9999b62b8c719a160100dcbae5c819bae0cacce32c9cdb" - url: "https://pub.dev" - source: hosted - version: "1.1.12" - vector_graphics_compiler: - dependency: transitive - description: - name: vector_graphics_compiler - sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad" - url: "https://pub.dev" - source: hosted - version: "1.1.16" vector_math: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 959364fde0..bca18321a4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -49,7 +49,6 @@ dependencies: url: https://github.com/veloce/flutter_slidable.git ref: 89b8384667d3b6c1c2967a8ff10846bcf0a170c7 flutter_spinkit: ^5.2.0 - flutter_svg: ^2.0.10+1 freezed_annotation: ^2.2.0 http: ^1.1.0 intl: ^0.19.0 @@ -106,6 +105,7 @@ flutter: - assets/chess_openings.db - assets/images/ - assets/images/stockfish/ + - assets/images/fide-fed/ - assets/sounds/futuristic/ - assets/sounds/lisp/ - assets/sounds/nes/ From da14205a7d7f5684afebc95d1bbbc45a28e6bda7 Mon Sep 17 00:00:00 2001 From: nunibye <38873297+nunibye@users.noreply.github.com> Date: Mon, 16 Dec 2024 23:06:26 -0800 Subject: [PATCH 935/979] changed edit profile screen to use widgets --- .gitignore | 3 + lib/src/view/account/edit_profile_screen.dart | 217 ++++++++++++------ 2 files changed, 147 insertions(+), 73 deletions(-) diff --git a/.gitignore b/.gitignore index ca77e71239..0625ef950f 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,6 @@ doc/api/ # FVM Version Cache .fvm/ + +# VS Code +.vscode/ diff --git a/lib/src/view/account/edit_profile_screen.dart b/lib/src/view/account/edit_profile_screen.dart index d0329a0b23..ec8f936c5e 100644 --- a/lib/src/view/account/edit_profile_screen.dart +++ b/lib/src/view/account/edit_profile_screen.dart @@ -16,6 +16,11 @@ import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:result_extensions/result_extensions.dart'; final _countries = countries.values.toList(); +final _cupertinoTextFieldDecoration = BoxDecoration( + color: CupertinoColors.tertiarySystemBackground, + border: Border.all(color: CupertinoColors.systemGrey4, width: 1), + borderRadius: BorderRadius.circular(8), +); class EditProfileScreen extends StatelessWidget { const EditProfileScreen({super.key}); @@ -24,7 +29,10 @@ class EditProfileScreen extends StatelessWidget { Widget build(BuildContext context) { return PlatformScaffold( appBar: PlatformAppBar(title: Text(context.l10n.editProfile)), - body: _Body(), + body: PopScope( + canPop: false, + child: _Body(), + ), ); } } @@ -38,14 +46,20 @@ class _Body extends ConsumerWidget { if (data == null) { return Center(child: Text(context.l10n.mobileMustBeLoggedIn)); } - return Padding( - padding: Styles.bodyPadding, - child: ListView( - children: [ - Text(context.l10n.allInformationIsPublicAndOptional), - const SizedBox(height: 16), - _EditProfileForm(data), - ], + return GestureDetector( + onTap: () => FocusScope.of(context).unfocus(), + child: Padding( + padding: Styles.bodyPadding.copyWith(top: 0, bottom: 0), + child: ListView( + keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, + children: [ + SizedBox(height: Styles.bodyPadding.top), + Text(context.l10n.allInformationIsPublicAndOptional), + const SizedBox(height: 16), + _EditProfileForm(data), + SizedBox(height: Styles.bodyPadding.bottom), + ], + ), ), ); }, @@ -82,30 +96,24 @@ class _EditProfileFormState extends ConsumerState<_EditProfileForm> { 'links': null, }; - final _cupertinoTextFieldDecoration = BoxDecoration( - color: CupertinoColors.tertiarySystemBackground, - border: Border.all(color: CupertinoColors.systemGrey4, width: 1), - borderRadius: BorderRadius.circular(8), - ); - Future? _pendingSaveProfile; @override Widget build(BuildContext context) { final String? initialLinks = widget.user.profile?.links?.map((e) => e.url).join('\r\n'); - return Form( key: _formKey, child: Column( children: [ - _textField( + _TextField( label: context.l10n.biography, initialValue: widget.user.profile?.bio, formKey: 'bio', - controller: TextEditingController(text: widget.user.profile?.bio), + formData: _formData, description: context.l10n.biographyDescription, maxLength: 400, maxLines: 6, + textInputAction: TextInputAction.newline, ), Padding( padding: const EdgeInsets.only(bottom: 16.0), @@ -147,25 +155,26 @@ class _EditProfileFormState extends ConsumerState<_EditProfileForm> { }, ), ), - _textField( + _TextField( label: context.l10n.location, initialValue: widget.user.profile?.location, - controller: TextEditingController(text: widget.user.profile?.location), + formData: _formData, formKey: 'location', maxLength: 80, ), - _textField( + _TextField( label: context.l10n.realName, initialValue: widget.user.profile?.realName, formKey: 'realName', - controller: TextEditingController(text: widget.user.profile?.realName), + formData: _formData, maxLength: 20, ), - _numericField( + + _NumericField( label: context.l10n.xRating('FIDE'), initialValue: widget.user.profile?.fideRating, formKey: 'fideRating', - controller: TextEditingController(text: widget.user.profile?.fideRating?.toString()), + formData: _formData, validator: (value) { if (value != null && (value < 1400 || value > 3000)) { return 'Rating must be between 1400 and 3000'; @@ -173,11 +182,11 @@ class _EditProfileFormState extends ConsumerState<_EditProfileForm> { return null; }, ), - _numericField( + _NumericField( label: context.l10n.xRating('USCF'), initialValue: widget.user.profile?.uscfRating, formKey: 'uscfRating', - controller: TextEditingController(text: widget.user.profile?.uscfRating?.toString()), + formData: _formData, validator: (value) { if (value != null && (value < 100 || value > 3000)) { return 'Rating must be between 100 and 3000'; @@ -185,12 +194,11 @@ class _EditProfileFormState extends ConsumerState<_EditProfileForm> { return null; }, ), - _numericField( + _NumericField( label: context.l10n.xRating('ECF'), initialValue: widget.user.profile?.ecfRating, formKey: 'ecfRating', - controller: TextEditingController(text: widget.user.profile?.ecfRating?.toString()), - textInputAction: TextInputAction.done, + formData: _formData, validator: (value) { if (value != null && (value < 0 || value > 3000)) { return 'Rating must be between 0 and 3000'; @@ -198,11 +206,11 @@ class _EditProfileFormState extends ConsumerState<_EditProfileForm> { return null; }, ), - _textField( + _TextField( label: context.l10n.socialMediaLinks, initialValue: initialLinks, formKey: 'links', - controller: TextEditingController(text: initialLinks), + formData: _formData, maxLength: 3000, maxLines: 4, textInputAction: TextInputAction.newline, @@ -257,6 +265,7 @@ class _EditProfileFormState extends ConsumerState<_EditProfileForm> { context.l10n.success, type: SnackBarType.success, ); + Navigator.of(context).pop(); } }, ); @@ -271,33 +280,61 @@ class _EditProfileFormState extends ConsumerState<_EditProfileForm> { ), ); } +} + +class _NumericField extends StatefulWidget { + final String label; + final int? initialValue; + final String formKey; + final String? Function(int?)? validator; + final TextInputAction textInputAction; + final Map formData; + const _NumericField({ + required this.label, + required this.initialValue, + required this.formKey, + required this.validator, + this.textInputAction = TextInputAction.next, + required this.formData, + }); + + @override + State<_NumericField> createState() => __NumericFieldState(); +} + +class __NumericFieldState extends State<_NumericField> { + final _controller = TextEditingController(); + @override + void initState() { + _controller.text = widget.initialValue?.toString() ?? ''; + super.initState(); + } - Widget _textField({ - required String label, - required String? initialValue, - required String formKey, - required TextEditingController controller, - String? description, - int? maxLength, - int? maxLines, - TextInputAction textInputAction = TextInputAction.next, - }) { + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(bottom: 16.0), - child: FormField( - initialValue: initialValue, + child: FormField( + initialValue: widget.initialValue, onSaved: (value) { - _formData[formKey] = value; + widget.formData[widget.formKey] = value; }, - builder: (FormFieldState field) { + validator: widget.validator, + builder: (FormFieldState field) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(label, style: Styles.formLabel), + Text(widget.label, style: Styles.formLabel), const SizedBox(height: 6.0), AdaptiveTextField( - maxLength: maxLength, - maxLines: maxLines, + controller: _controller, + keyboardType: TextInputType.number, cupertinoDecoration: _cupertinoTextFieldDecoration.copyWith( border: Border.all( color: @@ -309,16 +346,12 @@ class _EditProfileFormState extends ConsumerState<_EditProfileForm> { ), materialDecoration: field.errorText != null ? InputDecoration(errorText: field.errorText) : null, - textInputAction: textInputAction, - controller: controller, + textInputAction: widget.textInputAction, + onChanged: (value) { - field.didChange(value.trim()); + field.didChange(int.tryParse(value)); }, ), - if (description != null) ...[ - const SizedBox(height: 6.0), - Text(description, style: Styles.formDescription), - ], if (Theme.of(context).platform == TargetPlatform.iOS && field.errorText != null) Padding( padding: const EdgeInsets.only(top: 6.0), @@ -330,31 +363,66 @@ class _EditProfileFormState extends ConsumerState<_EditProfileForm> { ), ); } +} + +class _TextField extends StatefulWidget { + final String label; + final String? initialValue; + final String formKey; + final String? description; + final int? maxLength; + final int? maxLines; + final Map formData; + final TextInputAction textInputAction; + const _TextField({ + required this.label, + required this.initialValue, + required this.formKey, + required this.formData, + this.description, + this.maxLength, + this.maxLines, + this.textInputAction = TextInputAction.next, + }); + + @override + State<_TextField> createState() => __TextFieldState(); +} + +class __TextFieldState extends State<_TextField> { + final _controller = TextEditingController(); + @override + void initState() { + super.initState(); + _controller.text = widget.initialValue ?? ''; + } - Widget _numericField({ - required String label, - required int? initialValue, - required String formKey, - required TextEditingController controller, - required String? Function(int?)? validator, - TextInputAction textInputAction = TextInputAction.next, - }) { + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(bottom: 16.0), - child: FormField( - initialValue: initialValue, + child: FormField( + initialValue: widget.initialValue, onSaved: (value) { - _formData[formKey] = value; + widget.formData[widget.formKey] = value?.trim(); }, - validator: validator, - builder: (FormFieldState field) { + + builder: (FormFieldState field) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(label, style: Styles.formLabel), + Text(widget.label, style: Styles.formLabel), const SizedBox(height: 6.0), AdaptiveTextField( - keyboardType: TextInputType.number, + maxLength: widget.maxLength, + maxLines: widget.maxLines, + controller: _controller, cupertinoDecoration: _cupertinoTextFieldDecoration.copyWith( border: Border.all( color: @@ -366,12 +434,15 @@ class _EditProfileFormState extends ConsumerState<_EditProfileForm> { ), materialDecoration: field.errorText != null ? InputDecoration(errorText: field.errorText) : null, - textInputAction: textInputAction, - controller: controller, + textInputAction: widget.textInputAction, onChanged: (value) { - field.didChange(int.tryParse(value)); + field.didChange(value.trim()); }, ), + if (widget.description != null) ...[ + const SizedBox(height: 6.0), + Text(widget.description!, style: Styles.formDescription), + ], if (Theme.of(context).platform == TargetPlatform.iOS && field.errorText != null) Padding( padding: const EdgeInsets.only(top: 6.0), From 78bed529a08ef7b7b964389ad639a137e3e7f5d2 Mon Sep 17 00:00:00 2001 From: nunibye <38873297+nunibye@users.noreply.github.com> Date: Mon, 16 Dec 2024 23:13:58 -0800 Subject: [PATCH 936/979] lint --- lib/src/view/account/edit_profile_screen.dart | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/lib/src/view/account/edit_profile_screen.dart b/lib/src/view/account/edit_profile_screen.dart index ec8f936c5e..ef5d0c4cf3 100644 --- a/lib/src/view/account/edit_profile_screen.dart +++ b/lib/src/view/account/edit_profile_screen.dart @@ -29,10 +29,7 @@ class EditProfileScreen extends StatelessWidget { Widget build(BuildContext context) { return PlatformScaffold( appBar: PlatformAppBar(title: Text(context.l10n.editProfile)), - body: PopScope( - canPop: false, - child: _Body(), - ), + body: PopScope(canPop: false, child: _Body()), ); } } @@ -287,14 +284,12 @@ class _NumericField extends StatefulWidget { final int? initialValue; final String formKey; final String? Function(int?)? validator; - final TextInputAction textInputAction; final Map formData; const _NumericField({ required this.label, required this.initialValue, required this.formKey, required this.validator, - this.textInputAction = TextInputAction.next, required this.formData, }); @@ -346,7 +341,6 @@ class __NumericFieldState extends State<_NumericField> { ), materialDecoration: field.errorText != null ? InputDecoration(errorText: field.errorText) : null, - textInputAction: widget.textInputAction, onChanged: (value) { field.didChange(int.tryParse(value)); From 6c43166cf5c0ca40876a3986490bfab383ea6e43 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 17 Dec 2024 15:18:30 +0100 Subject: [PATCH 937/979] Display alert when leaving profile edit screen --- lib/src/view/account/edit_profile_screen.dart | 38 ++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/lib/src/view/account/edit_profile_screen.dart b/lib/src/view/account/edit_profile_screen.dart index ef5d0c4cf3..1cb1c0dbe6 100644 --- a/lib/src/view/account/edit_profile_screen.dart +++ b/lib/src/view/account/edit_profile_screen.dart @@ -12,6 +12,7 @@ import 'package:lichess_mobile/src/widgets/adaptive_autocomplete.dart'; import 'package:lichess_mobile/src/widgets/adaptive_text_field.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; +import 'package:lichess_mobile/src/widgets/platform_alert_dialog.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:result_extensions/result_extensions.dart'; @@ -25,11 +26,46 @@ final _cupertinoTextFieldDecoration = BoxDecoration( class EditProfileScreen extends StatelessWidget { const EditProfileScreen({super.key}); + Future _showBackDialog(BuildContext context) async { + return showAdaptiveDialog( + context: context, + builder: (context) { + return PlatformAlertDialog( + title: Text(context.l10n.mobileAreYouSure), + content: const Text('Your changes will be lost.'), + actions: [ + PlatformDialogAction( + child: Text(context.l10n.cancel), + onPressed: () => Navigator.of(context).pop(false), + ), + PlatformDialogAction( + child: Text(context.l10n.ok), + onPressed: () => Navigator.of(context).pop(true), + ), + ], + ); + }, + ); + } + @override Widget build(BuildContext context) { return PlatformScaffold( appBar: PlatformAppBar(title: Text(context.l10n.editProfile)), - body: PopScope(canPop: false, child: _Body()), + body: PopScope( + canPop: false, + onPopInvokedWithResult: (bool didPop, _) async { + if (didPop) { + return; + } + final NavigatorState navigator = Navigator.of(context); + final bool? shouldPop = await _showBackDialog(context); + if (shouldPop ?? false) { + navigator.pop(); + } + }, + child: _Body(), + ), ); } } From f319bb44ac87e3b2af00021a25b66b4553cd4ea7 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 17 Dec 2024 15:48:22 +0100 Subject: [PATCH 938/979] Rename assets --- assets/images/fide-fed/{4.0x => }/AFG.png | Bin assets/images/fide-fed/{4.0x => }/AHO.png | Bin assets/images/fide-fed/{4.0x => }/ALB.png | Bin assets/images/fide-fed/{4.0x => }/ALG.png | Bin assets/images/fide-fed/{4.0x => }/AND.png | Bin assets/images/fide-fed/{4.0x => }/ANG.png | Bin assets/images/fide-fed/{4.0x => }/ANT.png | Bin assets/images/fide-fed/{4.0x => }/ARG.png | Bin assets/images/fide-fed/{4.0x => }/ARM.png | Bin assets/images/fide-fed/{4.0x => }/ARU.png | Bin assets/images/fide-fed/{4.0x => }/AUS.png | Bin assets/images/fide-fed/{4.0x => }/AUT.png | Bin assets/images/fide-fed/{4.0x => }/AZE.png | Bin assets/images/fide-fed/{4.0x => }/BAH.png | Bin assets/images/fide-fed/{4.0x => }/BAN.png | Bin assets/images/fide-fed/{4.0x => }/BAR.png | Bin assets/images/fide-fed/{4.0x => }/BDI.png | Bin assets/images/fide-fed/{4.0x => }/BEL.png | Bin assets/images/fide-fed/{4.0x => }/BER.png | Bin assets/images/fide-fed/{4.0x => }/BHU.png | Bin assets/images/fide-fed/{4.0x => }/BIH.png | Bin assets/images/fide-fed/{4.0x => }/BIZ.png | Bin assets/images/fide-fed/{4.0x => }/BLR.png | Bin assets/images/fide-fed/{4.0x => }/BOL.png | Bin assets/images/fide-fed/{4.0x => }/BOT.png | Bin assets/images/fide-fed/{4.0x => }/BRA.png | Bin assets/images/fide-fed/{4.0x => }/BRN.png | Bin assets/images/fide-fed/{4.0x => }/BRU.png | Bin assets/images/fide-fed/{4.0x => }/BUL.png | Bin assets/images/fide-fed/{4.0x => }/BUR.png | Bin assets/images/fide-fed/{4.0x => }/CAF.png | Bin assets/images/fide-fed/{4.0x => }/CAM.png | Bin assets/images/fide-fed/{4.0x => }/CAN.png | Bin assets/images/fide-fed/{4.0x => }/CAY.png | Bin assets/images/fide-fed/{4.0x => }/CHA.png | Bin assets/images/fide-fed/{4.0x => }/CHI.png | Bin assets/images/fide-fed/{4.0x => }/CHN.png | Bin assets/images/fide-fed/{4.0x => }/CIV.png | Bin assets/images/fide-fed/{4.0x => }/CMR.png | Bin assets/images/fide-fed/{4.0x => }/COD.png | Bin assets/images/fide-fed/{4.0x => }/COL.png | Bin assets/images/fide-fed/{4.0x => }/COM.png | Bin assets/images/fide-fed/{4.0x => }/CPV.png | Bin assets/images/fide-fed/{4.0x => }/CRC.png | Bin assets/images/fide-fed/{4.0x => }/CRO.png | Bin assets/images/fide-fed/{4.0x => }/CUB.png | Bin assets/images/fide-fed/{4.0x => }/CYP.png | Bin assets/images/fide-fed/{4.0x => }/CZE.png | Bin assets/images/fide-fed/{4.0x => }/DEN.png | Bin assets/images/fide-fed/{4.0x => }/DJI.png | Bin assets/images/fide-fed/{4.0x => }/DMA.png | Bin assets/images/fide-fed/{4.0x => }/DOM.png | Bin assets/images/fide-fed/{4.0x => }/ECU.png | Bin assets/images/fide-fed/{4.0x => }/EGY.png | Bin assets/images/fide-fed/{4.0x => }/ENG.png | Bin assets/images/fide-fed/{4.0x => }/ERI.png | Bin assets/images/fide-fed/{4.0x => }/ESA.png | Bin assets/images/fide-fed/{4.0x => }/ESP.png | Bin assets/images/fide-fed/{4.0x => }/EST.png | Bin assets/images/fide-fed/{4.0x => }/ETH.png | Bin assets/images/fide-fed/{4.0x => }/FAI.png | Bin assets/images/fide-fed/{4.0x => }/FID.png | Bin assets/images/fide-fed/{4.0x => }/FIJ.png | Bin assets/images/fide-fed/{4.0x => }/FIN.png | Bin assets/images/fide-fed/{4.0x => }/FRA.png | Bin assets/images/fide-fed/{4.0x => }/GAB.png | Bin assets/images/fide-fed/{4.0x => }/GAM.png | Bin assets/images/fide-fed/{4.0x => }/GCI.png | Bin assets/images/fide-fed/{4.0x => }/GEO.png | Bin assets/images/fide-fed/{4.0x => }/GEQ.png | Bin assets/images/fide-fed/{4.0x => }/GER.png | Bin assets/images/fide-fed/{4.0x => }/GHA.png | Bin assets/images/fide-fed/{4.0x => }/GRE.png | Bin assets/images/fide-fed/{4.0x => }/GRN.png | Bin assets/images/fide-fed/{4.0x => }/GUA.png | Bin assets/images/fide-fed/{4.0x => }/GUM.png | Bin assets/images/fide-fed/{4.0x => }/GUY.png | Bin assets/images/fide-fed/{4.0x => }/HAI.png | Bin assets/images/fide-fed/{4.0x => }/HKG.png | Bin assets/images/fide-fed/{4.0x => }/HON.png | Bin assets/images/fide-fed/{4.0x => }/HUN.png | Bin assets/images/fide-fed/{4.0x => }/INA.png | Bin assets/images/fide-fed/{4.0x => }/IND.png | Bin assets/images/fide-fed/{4.0x => }/IOM.png | Bin assets/images/fide-fed/{4.0x => }/IRI.png | Bin assets/images/fide-fed/{4.0x => }/IRL.png | Bin assets/images/fide-fed/{4.0x => }/IRQ.png | Bin assets/images/fide-fed/{4.0x => }/ISL.png | Bin assets/images/fide-fed/{4.0x => }/ISR.png | Bin assets/images/fide-fed/{4.0x => }/ISV.png | Bin assets/images/fide-fed/{4.0x => }/ITA.png | Bin assets/images/fide-fed/{4.0x => }/IVB.png | Bin assets/images/fide-fed/{4.0x => }/JAM.png | Bin assets/images/fide-fed/{4.0x => }/JCI.png | Bin assets/images/fide-fed/{4.0x => }/JOR.png | Bin assets/images/fide-fed/{4.0x => }/JPN.png | Bin assets/images/fide-fed/{4.0x => }/KAZ.png | Bin assets/images/fide-fed/{4.0x => }/KEN.png | Bin assets/images/fide-fed/{4.0x => }/KGZ.png | Bin assets/images/fide-fed/{4.0x => }/KOR.png | Bin assets/images/fide-fed/{4.0x => }/KOS.png | Bin assets/images/fide-fed/{4.0x => }/KSA.png | Bin assets/images/fide-fed/{4.0x => }/KUW.png | Bin assets/images/fide-fed/{4.0x => }/LAO.png | Bin assets/images/fide-fed/{4.0x => }/LAT.png | Bin assets/images/fide-fed/{4.0x => }/LBA.png | Bin assets/images/fide-fed/{4.0x => }/LBN.png | Bin assets/images/fide-fed/{4.0x => }/LBR.png | Bin assets/images/fide-fed/{4.0x => }/LCA.png | Bin assets/images/fide-fed/{4.0x => }/LES.png | Bin assets/images/fide-fed/{4.0x => }/LIE.png | Bin assets/images/fide-fed/{4.0x => }/LTU.png | Bin assets/images/fide-fed/{4.0x => }/LUX.png | Bin assets/images/fide-fed/{4.0x => }/MAC.png | Bin assets/images/fide-fed/{4.0x => }/MAD.png | Bin assets/images/fide-fed/{4.0x => }/MAR.png | Bin assets/images/fide-fed/{4.0x => }/MAS.png | Bin assets/images/fide-fed/{4.0x => }/MAW.png | Bin assets/images/fide-fed/{4.0x => }/MDA.png | Bin assets/images/fide-fed/{4.0x => }/MDV.png | Bin assets/images/fide-fed/{4.0x => }/MEX.png | Bin assets/images/fide-fed/{4.0x => }/MGL.png | Bin assets/images/fide-fed/{4.0x => }/MKD.png | Bin assets/images/fide-fed/{4.0x => }/MLI.png | Bin assets/images/fide-fed/{4.0x => }/MLT.png | Bin assets/images/fide-fed/{4.0x => }/MNC.png | Bin assets/images/fide-fed/{4.0x => }/MNE.png | Bin assets/images/fide-fed/{4.0x => }/MOZ.png | Bin assets/images/fide-fed/{4.0x => }/MRI.png | Bin assets/images/fide-fed/{4.0x => }/MTN.png | Bin assets/images/fide-fed/{4.0x => }/MYA.png | Bin assets/images/fide-fed/{4.0x => }/NAM.png | Bin assets/images/fide-fed/{4.0x => }/NCA.png | Bin assets/images/fide-fed/{4.0x => }/NED.png | Bin assets/images/fide-fed/{4.0x => }/NEP.png | Bin assets/images/fide-fed/{4.0x => }/NGR.png | Bin assets/images/fide-fed/{4.0x => }/NIG.png | Bin assets/images/fide-fed/{4.0x => }/NOR.png | Bin assets/images/fide-fed/{4.0x => }/NRU.png | Bin assets/images/fide-fed/{4.0x => }/NZL.png | Bin assets/images/fide-fed/{4.0x => }/OMA.png | Bin assets/images/fide-fed/{4.0x => }/PAK.png | Bin assets/images/fide-fed/{4.0x => }/PAN.png | Bin assets/images/fide-fed/{4.0x => }/PAR.png | Bin assets/images/fide-fed/{4.0x => }/PER.png | Bin assets/images/fide-fed/{4.0x => }/PHI.png | Bin assets/images/fide-fed/{4.0x => }/PLE.png | Bin assets/images/fide-fed/{4.0x => }/PLW.png | Bin assets/images/fide-fed/{4.0x => }/PNG.png | Bin assets/images/fide-fed/{4.0x => }/POL.png | Bin assets/images/fide-fed/{4.0x => }/POR.png | Bin assets/images/fide-fed/{4.0x => }/PUR.png | Bin assets/images/fide-fed/{4.0x => }/QAT.png | Bin assets/images/fide-fed/{4.0x => }/ROU.png | Bin assets/images/fide-fed/{4.0x => }/RSA.png | Bin assets/images/fide-fed/{4.0x => }/RUS.png | Bin assets/images/fide-fed/{4.0x => }/RWA.png | Bin assets/images/fide-fed/{4.0x => }/SCO.png | Bin assets/images/fide-fed/{4.0x => }/SEN.png | Bin assets/images/fide-fed/{4.0x => }/SEY.png | Bin assets/images/fide-fed/{4.0x => }/SGP.png | Bin assets/images/fide-fed/{4.0x => }/SKN.png | Bin assets/images/fide-fed/{4.0x => }/SLE.png | Bin assets/images/fide-fed/{4.0x => }/SLO.png | Bin assets/images/fide-fed/{4.0x => }/SMR.png | Bin assets/images/fide-fed/{4.0x => }/SOL.png | Bin assets/images/fide-fed/{4.0x => }/SOM.png | Bin assets/images/fide-fed/{4.0x => }/SRB.png | Bin assets/images/fide-fed/{4.0x => }/SRI.png | Bin assets/images/fide-fed/{4.0x => }/SSD.png | Bin assets/images/fide-fed/{4.0x => }/STP.png | Bin assets/images/fide-fed/{4.0x => }/SUD.png | Bin assets/images/fide-fed/{4.0x => }/SUI.png | Bin assets/images/fide-fed/{4.0x => }/SUR.png | Bin assets/images/fide-fed/{4.0x => }/SVK.png | Bin assets/images/fide-fed/{4.0x => }/SWE.png | Bin assets/images/fide-fed/{4.0x => }/SWZ.png | Bin assets/images/fide-fed/{4.0x => }/SYR.png | Bin assets/images/fide-fed/{4.0x => }/TAN.png | Bin assets/images/fide-fed/{4.0x => }/THA.png | Bin assets/images/fide-fed/{4.0x => }/TJK.png | Bin assets/images/fide-fed/{4.0x => }/TKM.png | Bin assets/images/fide-fed/{4.0x => }/TLS.png | Bin assets/images/fide-fed/{4.0x => }/TOG.png | Bin assets/images/fide-fed/{4.0x => }/TPE.png | Bin assets/images/fide-fed/{4.0x => }/TTO.png | Bin assets/images/fide-fed/{4.0x => }/TUN.png | Bin assets/images/fide-fed/{4.0x => }/TUR.png | Bin assets/images/fide-fed/{4.0x => }/UAE.png | Bin assets/images/fide-fed/{4.0x => }/UGA.png | Bin assets/images/fide-fed/{4.0x => }/UKR.png | Bin assets/images/fide-fed/{4.0x => }/URU.png | Bin assets/images/fide-fed/{4.0x => }/USA.png | Bin assets/images/fide-fed/{4.0x => }/UZB.png | Bin assets/images/fide-fed/{4.0x => }/VAN.png | Bin assets/images/fide-fed/{4.0x => }/VEN.png | Bin assets/images/fide-fed/{4.0x => }/VIE.png | Bin assets/images/fide-fed/{4.0x => }/VIN.png | Bin assets/images/fide-fed/{4.0x => }/W.png | Bin assets/images/fide-fed/{4.0x => }/WLS.png | Bin assets/images/fide-fed/{4.0x => }/YEM.png | Bin assets/images/fide-fed/{4.0x => }/ZAM.png | Bin assets/images/fide-fed/{4.0x => }/ZIM.png | Bin 203 files changed, 0 insertions(+), 0 deletions(-) rename assets/images/fide-fed/{4.0x => }/AFG.png (100%) rename assets/images/fide-fed/{4.0x => }/AHO.png (100%) rename assets/images/fide-fed/{4.0x => }/ALB.png (100%) rename assets/images/fide-fed/{4.0x => }/ALG.png (100%) rename assets/images/fide-fed/{4.0x => }/AND.png (100%) rename assets/images/fide-fed/{4.0x => }/ANG.png (100%) rename assets/images/fide-fed/{4.0x => }/ANT.png (100%) rename assets/images/fide-fed/{4.0x => }/ARG.png (100%) rename assets/images/fide-fed/{4.0x => }/ARM.png (100%) rename assets/images/fide-fed/{4.0x => }/ARU.png (100%) rename assets/images/fide-fed/{4.0x => }/AUS.png (100%) rename assets/images/fide-fed/{4.0x => }/AUT.png (100%) rename assets/images/fide-fed/{4.0x => }/AZE.png (100%) rename assets/images/fide-fed/{4.0x => }/BAH.png (100%) rename assets/images/fide-fed/{4.0x => }/BAN.png (100%) rename assets/images/fide-fed/{4.0x => }/BAR.png (100%) rename assets/images/fide-fed/{4.0x => }/BDI.png (100%) rename assets/images/fide-fed/{4.0x => }/BEL.png (100%) rename assets/images/fide-fed/{4.0x => }/BER.png (100%) rename assets/images/fide-fed/{4.0x => }/BHU.png (100%) rename assets/images/fide-fed/{4.0x => }/BIH.png (100%) rename assets/images/fide-fed/{4.0x => }/BIZ.png (100%) rename assets/images/fide-fed/{4.0x => }/BLR.png (100%) rename assets/images/fide-fed/{4.0x => }/BOL.png (100%) rename assets/images/fide-fed/{4.0x => }/BOT.png (100%) rename assets/images/fide-fed/{4.0x => }/BRA.png (100%) rename assets/images/fide-fed/{4.0x => }/BRN.png (100%) rename assets/images/fide-fed/{4.0x => }/BRU.png (100%) rename assets/images/fide-fed/{4.0x => }/BUL.png (100%) rename assets/images/fide-fed/{4.0x => }/BUR.png (100%) rename assets/images/fide-fed/{4.0x => }/CAF.png (100%) rename assets/images/fide-fed/{4.0x => }/CAM.png (100%) rename assets/images/fide-fed/{4.0x => }/CAN.png (100%) rename assets/images/fide-fed/{4.0x => }/CAY.png (100%) rename assets/images/fide-fed/{4.0x => }/CHA.png (100%) rename assets/images/fide-fed/{4.0x => }/CHI.png (100%) rename assets/images/fide-fed/{4.0x => }/CHN.png (100%) rename assets/images/fide-fed/{4.0x => }/CIV.png (100%) rename assets/images/fide-fed/{4.0x => }/CMR.png (100%) rename assets/images/fide-fed/{4.0x => }/COD.png (100%) rename assets/images/fide-fed/{4.0x => }/COL.png (100%) rename assets/images/fide-fed/{4.0x => }/COM.png (100%) rename assets/images/fide-fed/{4.0x => }/CPV.png (100%) rename assets/images/fide-fed/{4.0x => }/CRC.png (100%) rename assets/images/fide-fed/{4.0x => }/CRO.png (100%) rename assets/images/fide-fed/{4.0x => }/CUB.png (100%) rename assets/images/fide-fed/{4.0x => }/CYP.png (100%) rename assets/images/fide-fed/{4.0x => }/CZE.png (100%) rename assets/images/fide-fed/{4.0x => }/DEN.png (100%) rename assets/images/fide-fed/{4.0x => }/DJI.png (100%) rename assets/images/fide-fed/{4.0x => }/DMA.png (100%) rename assets/images/fide-fed/{4.0x => }/DOM.png (100%) rename assets/images/fide-fed/{4.0x => }/ECU.png (100%) rename assets/images/fide-fed/{4.0x => }/EGY.png (100%) rename assets/images/fide-fed/{4.0x => }/ENG.png (100%) rename assets/images/fide-fed/{4.0x => }/ERI.png (100%) rename assets/images/fide-fed/{4.0x => }/ESA.png (100%) rename assets/images/fide-fed/{4.0x => }/ESP.png (100%) rename assets/images/fide-fed/{4.0x => }/EST.png (100%) rename assets/images/fide-fed/{4.0x => }/ETH.png (100%) rename assets/images/fide-fed/{4.0x => }/FAI.png (100%) rename assets/images/fide-fed/{4.0x => }/FID.png (100%) rename assets/images/fide-fed/{4.0x => }/FIJ.png (100%) rename assets/images/fide-fed/{4.0x => }/FIN.png (100%) rename assets/images/fide-fed/{4.0x => }/FRA.png (100%) rename assets/images/fide-fed/{4.0x => }/GAB.png (100%) rename assets/images/fide-fed/{4.0x => }/GAM.png (100%) rename assets/images/fide-fed/{4.0x => }/GCI.png (100%) rename assets/images/fide-fed/{4.0x => }/GEO.png (100%) rename assets/images/fide-fed/{4.0x => }/GEQ.png (100%) rename assets/images/fide-fed/{4.0x => }/GER.png (100%) rename assets/images/fide-fed/{4.0x => }/GHA.png (100%) rename assets/images/fide-fed/{4.0x => }/GRE.png (100%) rename assets/images/fide-fed/{4.0x => }/GRN.png (100%) rename assets/images/fide-fed/{4.0x => }/GUA.png (100%) rename assets/images/fide-fed/{4.0x => }/GUM.png (100%) rename assets/images/fide-fed/{4.0x => }/GUY.png (100%) rename assets/images/fide-fed/{4.0x => }/HAI.png (100%) rename assets/images/fide-fed/{4.0x => }/HKG.png (100%) rename assets/images/fide-fed/{4.0x => }/HON.png (100%) rename assets/images/fide-fed/{4.0x => }/HUN.png (100%) rename assets/images/fide-fed/{4.0x => }/INA.png (100%) rename assets/images/fide-fed/{4.0x => }/IND.png (100%) rename assets/images/fide-fed/{4.0x => }/IOM.png (100%) rename assets/images/fide-fed/{4.0x => }/IRI.png (100%) rename assets/images/fide-fed/{4.0x => }/IRL.png (100%) rename assets/images/fide-fed/{4.0x => }/IRQ.png (100%) rename assets/images/fide-fed/{4.0x => }/ISL.png (100%) rename assets/images/fide-fed/{4.0x => }/ISR.png (100%) rename assets/images/fide-fed/{4.0x => }/ISV.png (100%) rename assets/images/fide-fed/{4.0x => }/ITA.png (100%) rename assets/images/fide-fed/{4.0x => }/IVB.png (100%) rename assets/images/fide-fed/{4.0x => }/JAM.png (100%) rename assets/images/fide-fed/{4.0x => }/JCI.png (100%) rename assets/images/fide-fed/{4.0x => }/JOR.png (100%) rename assets/images/fide-fed/{4.0x => }/JPN.png (100%) rename assets/images/fide-fed/{4.0x => }/KAZ.png (100%) rename assets/images/fide-fed/{4.0x => }/KEN.png (100%) rename assets/images/fide-fed/{4.0x => }/KGZ.png (100%) rename assets/images/fide-fed/{4.0x => }/KOR.png (100%) rename assets/images/fide-fed/{4.0x => }/KOS.png (100%) rename assets/images/fide-fed/{4.0x => }/KSA.png (100%) rename assets/images/fide-fed/{4.0x => }/KUW.png (100%) rename assets/images/fide-fed/{4.0x => }/LAO.png (100%) rename assets/images/fide-fed/{4.0x => }/LAT.png (100%) rename assets/images/fide-fed/{4.0x => }/LBA.png (100%) rename assets/images/fide-fed/{4.0x => }/LBN.png (100%) rename assets/images/fide-fed/{4.0x => }/LBR.png (100%) rename assets/images/fide-fed/{4.0x => }/LCA.png (100%) rename assets/images/fide-fed/{4.0x => }/LES.png (100%) rename assets/images/fide-fed/{4.0x => }/LIE.png (100%) rename assets/images/fide-fed/{4.0x => }/LTU.png (100%) rename assets/images/fide-fed/{4.0x => }/LUX.png (100%) rename assets/images/fide-fed/{4.0x => }/MAC.png (100%) rename assets/images/fide-fed/{4.0x => }/MAD.png (100%) rename assets/images/fide-fed/{4.0x => }/MAR.png (100%) rename assets/images/fide-fed/{4.0x => }/MAS.png (100%) rename assets/images/fide-fed/{4.0x => }/MAW.png (100%) rename assets/images/fide-fed/{4.0x => }/MDA.png (100%) rename assets/images/fide-fed/{4.0x => }/MDV.png (100%) rename assets/images/fide-fed/{4.0x => }/MEX.png (100%) rename assets/images/fide-fed/{4.0x => }/MGL.png (100%) rename assets/images/fide-fed/{4.0x => }/MKD.png (100%) rename assets/images/fide-fed/{4.0x => }/MLI.png (100%) rename assets/images/fide-fed/{4.0x => }/MLT.png (100%) rename assets/images/fide-fed/{4.0x => }/MNC.png (100%) rename assets/images/fide-fed/{4.0x => }/MNE.png (100%) rename assets/images/fide-fed/{4.0x => }/MOZ.png (100%) rename assets/images/fide-fed/{4.0x => }/MRI.png (100%) rename assets/images/fide-fed/{4.0x => }/MTN.png (100%) rename assets/images/fide-fed/{4.0x => }/MYA.png (100%) rename assets/images/fide-fed/{4.0x => }/NAM.png (100%) rename assets/images/fide-fed/{4.0x => }/NCA.png (100%) rename assets/images/fide-fed/{4.0x => }/NED.png (100%) rename assets/images/fide-fed/{4.0x => }/NEP.png (100%) rename assets/images/fide-fed/{4.0x => }/NGR.png (100%) rename assets/images/fide-fed/{4.0x => }/NIG.png (100%) rename assets/images/fide-fed/{4.0x => }/NOR.png (100%) rename assets/images/fide-fed/{4.0x => }/NRU.png (100%) rename assets/images/fide-fed/{4.0x => }/NZL.png (100%) rename assets/images/fide-fed/{4.0x => }/OMA.png (100%) rename assets/images/fide-fed/{4.0x => }/PAK.png (100%) rename assets/images/fide-fed/{4.0x => }/PAN.png (100%) rename assets/images/fide-fed/{4.0x => }/PAR.png (100%) rename assets/images/fide-fed/{4.0x => }/PER.png (100%) rename assets/images/fide-fed/{4.0x => }/PHI.png (100%) rename assets/images/fide-fed/{4.0x => }/PLE.png (100%) rename assets/images/fide-fed/{4.0x => }/PLW.png (100%) rename assets/images/fide-fed/{4.0x => }/PNG.png (100%) rename assets/images/fide-fed/{4.0x => }/POL.png (100%) rename assets/images/fide-fed/{4.0x => }/POR.png (100%) rename assets/images/fide-fed/{4.0x => }/PUR.png (100%) rename assets/images/fide-fed/{4.0x => }/QAT.png (100%) rename assets/images/fide-fed/{4.0x => }/ROU.png (100%) rename assets/images/fide-fed/{4.0x => }/RSA.png (100%) rename assets/images/fide-fed/{4.0x => }/RUS.png (100%) rename assets/images/fide-fed/{4.0x => }/RWA.png (100%) rename assets/images/fide-fed/{4.0x => }/SCO.png (100%) rename assets/images/fide-fed/{4.0x => }/SEN.png (100%) rename assets/images/fide-fed/{4.0x => }/SEY.png (100%) rename assets/images/fide-fed/{4.0x => }/SGP.png (100%) rename assets/images/fide-fed/{4.0x => }/SKN.png (100%) rename assets/images/fide-fed/{4.0x => }/SLE.png (100%) rename assets/images/fide-fed/{4.0x => }/SLO.png (100%) rename assets/images/fide-fed/{4.0x => }/SMR.png (100%) rename assets/images/fide-fed/{4.0x => }/SOL.png (100%) rename assets/images/fide-fed/{4.0x => }/SOM.png (100%) rename assets/images/fide-fed/{4.0x => }/SRB.png (100%) rename assets/images/fide-fed/{4.0x => }/SRI.png (100%) rename assets/images/fide-fed/{4.0x => }/SSD.png (100%) rename assets/images/fide-fed/{4.0x => }/STP.png (100%) rename assets/images/fide-fed/{4.0x => }/SUD.png (100%) rename assets/images/fide-fed/{4.0x => }/SUI.png (100%) rename assets/images/fide-fed/{4.0x => }/SUR.png (100%) rename assets/images/fide-fed/{4.0x => }/SVK.png (100%) rename assets/images/fide-fed/{4.0x => }/SWE.png (100%) rename assets/images/fide-fed/{4.0x => }/SWZ.png (100%) rename assets/images/fide-fed/{4.0x => }/SYR.png (100%) rename assets/images/fide-fed/{4.0x => }/TAN.png (100%) rename assets/images/fide-fed/{4.0x => }/THA.png (100%) rename assets/images/fide-fed/{4.0x => }/TJK.png (100%) rename assets/images/fide-fed/{4.0x => }/TKM.png (100%) rename assets/images/fide-fed/{4.0x => }/TLS.png (100%) rename assets/images/fide-fed/{4.0x => }/TOG.png (100%) rename assets/images/fide-fed/{4.0x => }/TPE.png (100%) rename assets/images/fide-fed/{4.0x => }/TTO.png (100%) rename assets/images/fide-fed/{4.0x => }/TUN.png (100%) rename assets/images/fide-fed/{4.0x => }/TUR.png (100%) rename assets/images/fide-fed/{4.0x => }/UAE.png (100%) rename assets/images/fide-fed/{4.0x => }/UGA.png (100%) rename assets/images/fide-fed/{4.0x => }/UKR.png (100%) rename assets/images/fide-fed/{4.0x => }/URU.png (100%) rename assets/images/fide-fed/{4.0x => }/USA.png (100%) rename assets/images/fide-fed/{4.0x => }/UZB.png (100%) rename assets/images/fide-fed/{4.0x => }/VAN.png (100%) rename assets/images/fide-fed/{4.0x => }/VEN.png (100%) rename assets/images/fide-fed/{4.0x => }/VIE.png (100%) rename assets/images/fide-fed/{4.0x => }/VIN.png (100%) rename assets/images/fide-fed/{4.0x => }/W.png (100%) rename assets/images/fide-fed/{4.0x => }/WLS.png (100%) rename assets/images/fide-fed/{4.0x => }/YEM.png (100%) rename assets/images/fide-fed/{4.0x => }/ZAM.png (100%) rename assets/images/fide-fed/{4.0x => }/ZIM.png (100%) diff --git a/assets/images/fide-fed/4.0x/AFG.png b/assets/images/fide-fed/AFG.png similarity index 100% rename from assets/images/fide-fed/4.0x/AFG.png rename to assets/images/fide-fed/AFG.png diff --git a/assets/images/fide-fed/4.0x/AHO.png b/assets/images/fide-fed/AHO.png similarity index 100% rename from assets/images/fide-fed/4.0x/AHO.png rename to assets/images/fide-fed/AHO.png diff --git a/assets/images/fide-fed/4.0x/ALB.png b/assets/images/fide-fed/ALB.png similarity index 100% rename from assets/images/fide-fed/4.0x/ALB.png rename to assets/images/fide-fed/ALB.png diff --git a/assets/images/fide-fed/4.0x/ALG.png b/assets/images/fide-fed/ALG.png similarity index 100% rename from assets/images/fide-fed/4.0x/ALG.png rename to assets/images/fide-fed/ALG.png diff --git a/assets/images/fide-fed/4.0x/AND.png b/assets/images/fide-fed/AND.png similarity index 100% rename from assets/images/fide-fed/4.0x/AND.png rename to assets/images/fide-fed/AND.png diff --git a/assets/images/fide-fed/4.0x/ANG.png b/assets/images/fide-fed/ANG.png similarity index 100% rename from assets/images/fide-fed/4.0x/ANG.png rename to assets/images/fide-fed/ANG.png diff --git a/assets/images/fide-fed/4.0x/ANT.png b/assets/images/fide-fed/ANT.png similarity index 100% rename from assets/images/fide-fed/4.0x/ANT.png rename to assets/images/fide-fed/ANT.png diff --git a/assets/images/fide-fed/4.0x/ARG.png b/assets/images/fide-fed/ARG.png similarity index 100% rename from assets/images/fide-fed/4.0x/ARG.png rename to assets/images/fide-fed/ARG.png diff --git a/assets/images/fide-fed/4.0x/ARM.png b/assets/images/fide-fed/ARM.png similarity index 100% rename from assets/images/fide-fed/4.0x/ARM.png rename to assets/images/fide-fed/ARM.png diff --git a/assets/images/fide-fed/4.0x/ARU.png b/assets/images/fide-fed/ARU.png similarity index 100% rename from assets/images/fide-fed/4.0x/ARU.png rename to assets/images/fide-fed/ARU.png diff --git a/assets/images/fide-fed/4.0x/AUS.png b/assets/images/fide-fed/AUS.png similarity index 100% rename from assets/images/fide-fed/4.0x/AUS.png rename to assets/images/fide-fed/AUS.png diff --git a/assets/images/fide-fed/4.0x/AUT.png b/assets/images/fide-fed/AUT.png similarity index 100% rename from assets/images/fide-fed/4.0x/AUT.png rename to assets/images/fide-fed/AUT.png diff --git a/assets/images/fide-fed/4.0x/AZE.png b/assets/images/fide-fed/AZE.png similarity index 100% rename from assets/images/fide-fed/4.0x/AZE.png rename to assets/images/fide-fed/AZE.png diff --git a/assets/images/fide-fed/4.0x/BAH.png b/assets/images/fide-fed/BAH.png similarity index 100% rename from assets/images/fide-fed/4.0x/BAH.png rename to assets/images/fide-fed/BAH.png diff --git a/assets/images/fide-fed/4.0x/BAN.png b/assets/images/fide-fed/BAN.png similarity index 100% rename from assets/images/fide-fed/4.0x/BAN.png rename to assets/images/fide-fed/BAN.png diff --git a/assets/images/fide-fed/4.0x/BAR.png b/assets/images/fide-fed/BAR.png similarity index 100% rename from assets/images/fide-fed/4.0x/BAR.png rename to assets/images/fide-fed/BAR.png diff --git a/assets/images/fide-fed/4.0x/BDI.png b/assets/images/fide-fed/BDI.png similarity index 100% rename from assets/images/fide-fed/4.0x/BDI.png rename to assets/images/fide-fed/BDI.png diff --git a/assets/images/fide-fed/4.0x/BEL.png b/assets/images/fide-fed/BEL.png similarity index 100% rename from assets/images/fide-fed/4.0x/BEL.png rename to assets/images/fide-fed/BEL.png diff --git a/assets/images/fide-fed/4.0x/BER.png b/assets/images/fide-fed/BER.png similarity index 100% rename from assets/images/fide-fed/4.0x/BER.png rename to assets/images/fide-fed/BER.png diff --git a/assets/images/fide-fed/4.0x/BHU.png b/assets/images/fide-fed/BHU.png similarity index 100% rename from assets/images/fide-fed/4.0x/BHU.png rename to assets/images/fide-fed/BHU.png diff --git a/assets/images/fide-fed/4.0x/BIH.png b/assets/images/fide-fed/BIH.png similarity index 100% rename from assets/images/fide-fed/4.0x/BIH.png rename to assets/images/fide-fed/BIH.png diff --git a/assets/images/fide-fed/4.0x/BIZ.png b/assets/images/fide-fed/BIZ.png similarity index 100% rename from assets/images/fide-fed/4.0x/BIZ.png rename to assets/images/fide-fed/BIZ.png diff --git a/assets/images/fide-fed/4.0x/BLR.png b/assets/images/fide-fed/BLR.png similarity index 100% rename from assets/images/fide-fed/4.0x/BLR.png rename to assets/images/fide-fed/BLR.png diff --git a/assets/images/fide-fed/4.0x/BOL.png b/assets/images/fide-fed/BOL.png similarity index 100% rename from assets/images/fide-fed/4.0x/BOL.png rename to assets/images/fide-fed/BOL.png diff --git a/assets/images/fide-fed/4.0x/BOT.png b/assets/images/fide-fed/BOT.png similarity index 100% rename from assets/images/fide-fed/4.0x/BOT.png rename to assets/images/fide-fed/BOT.png diff --git a/assets/images/fide-fed/4.0x/BRA.png b/assets/images/fide-fed/BRA.png similarity index 100% rename from assets/images/fide-fed/4.0x/BRA.png rename to assets/images/fide-fed/BRA.png diff --git a/assets/images/fide-fed/4.0x/BRN.png b/assets/images/fide-fed/BRN.png similarity index 100% rename from assets/images/fide-fed/4.0x/BRN.png rename to assets/images/fide-fed/BRN.png diff --git a/assets/images/fide-fed/4.0x/BRU.png b/assets/images/fide-fed/BRU.png similarity index 100% rename from assets/images/fide-fed/4.0x/BRU.png rename to assets/images/fide-fed/BRU.png diff --git a/assets/images/fide-fed/4.0x/BUL.png b/assets/images/fide-fed/BUL.png similarity index 100% rename from assets/images/fide-fed/4.0x/BUL.png rename to assets/images/fide-fed/BUL.png diff --git a/assets/images/fide-fed/4.0x/BUR.png b/assets/images/fide-fed/BUR.png similarity index 100% rename from assets/images/fide-fed/4.0x/BUR.png rename to assets/images/fide-fed/BUR.png diff --git a/assets/images/fide-fed/4.0x/CAF.png b/assets/images/fide-fed/CAF.png similarity index 100% rename from assets/images/fide-fed/4.0x/CAF.png rename to assets/images/fide-fed/CAF.png diff --git a/assets/images/fide-fed/4.0x/CAM.png b/assets/images/fide-fed/CAM.png similarity index 100% rename from assets/images/fide-fed/4.0x/CAM.png rename to assets/images/fide-fed/CAM.png diff --git a/assets/images/fide-fed/4.0x/CAN.png b/assets/images/fide-fed/CAN.png similarity index 100% rename from assets/images/fide-fed/4.0x/CAN.png rename to assets/images/fide-fed/CAN.png diff --git a/assets/images/fide-fed/4.0x/CAY.png b/assets/images/fide-fed/CAY.png similarity index 100% rename from assets/images/fide-fed/4.0x/CAY.png rename to assets/images/fide-fed/CAY.png diff --git a/assets/images/fide-fed/4.0x/CHA.png b/assets/images/fide-fed/CHA.png similarity index 100% rename from assets/images/fide-fed/4.0x/CHA.png rename to assets/images/fide-fed/CHA.png diff --git a/assets/images/fide-fed/4.0x/CHI.png b/assets/images/fide-fed/CHI.png similarity index 100% rename from assets/images/fide-fed/4.0x/CHI.png rename to assets/images/fide-fed/CHI.png diff --git a/assets/images/fide-fed/4.0x/CHN.png b/assets/images/fide-fed/CHN.png similarity index 100% rename from assets/images/fide-fed/4.0x/CHN.png rename to assets/images/fide-fed/CHN.png diff --git a/assets/images/fide-fed/4.0x/CIV.png b/assets/images/fide-fed/CIV.png similarity index 100% rename from assets/images/fide-fed/4.0x/CIV.png rename to assets/images/fide-fed/CIV.png diff --git a/assets/images/fide-fed/4.0x/CMR.png b/assets/images/fide-fed/CMR.png similarity index 100% rename from assets/images/fide-fed/4.0x/CMR.png rename to assets/images/fide-fed/CMR.png diff --git a/assets/images/fide-fed/4.0x/COD.png b/assets/images/fide-fed/COD.png similarity index 100% rename from assets/images/fide-fed/4.0x/COD.png rename to assets/images/fide-fed/COD.png diff --git a/assets/images/fide-fed/4.0x/COL.png b/assets/images/fide-fed/COL.png similarity index 100% rename from assets/images/fide-fed/4.0x/COL.png rename to assets/images/fide-fed/COL.png diff --git a/assets/images/fide-fed/4.0x/COM.png b/assets/images/fide-fed/COM.png similarity index 100% rename from assets/images/fide-fed/4.0x/COM.png rename to assets/images/fide-fed/COM.png diff --git a/assets/images/fide-fed/4.0x/CPV.png b/assets/images/fide-fed/CPV.png similarity index 100% rename from assets/images/fide-fed/4.0x/CPV.png rename to assets/images/fide-fed/CPV.png diff --git a/assets/images/fide-fed/4.0x/CRC.png b/assets/images/fide-fed/CRC.png similarity index 100% rename from assets/images/fide-fed/4.0x/CRC.png rename to assets/images/fide-fed/CRC.png diff --git a/assets/images/fide-fed/4.0x/CRO.png b/assets/images/fide-fed/CRO.png similarity index 100% rename from assets/images/fide-fed/4.0x/CRO.png rename to assets/images/fide-fed/CRO.png diff --git a/assets/images/fide-fed/4.0x/CUB.png b/assets/images/fide-fed/CUB.png similarity index 100% rename from assets/images/fide-fed/4.0x/CUB.png rename to assets/images/fide-fed/CUB.png diff --git a/assets/images/fide-fed/4.0x/CYP.png b/assets/images/fide-fed/CYP.png similarity index 100% rename from assets/images/fide-fed/4.0x/CYP.png rename to assets/images/fide-fed/CYP.png diff --git a/assets/images/fide-fed/4.0x/CZE.png b/assets/images/fide-fed/CZE.png similarity index 100% rename from assets/images/fide-fed/4.0x/CZE.png rename to assets/images/fide-fed/CZE.png diff --git a/assets/images/fide-fed/4.0x/DEN.png b/assets/images/fide-fed/DEN.png similarity index 100% rename from assets/images/fide-fed/4.0x/DEN.png rename to assets/images/fide-fed/DEN.png diff --git a/assets/images/fide-fed/4.0x/DJI.png b/assets/images/fide-fed/DJI.png similarity index 100% rename from assets/images/fide-fed/4.0x/DJI.png rename to assets/images/fide-fed/DJI.png diff --git a/assets/images/fide-fed/4.0x/DMA.png b/assets/images/fide-fed/DMA.png similarity index 100% rename from assets/images/fide-fed/4.0x/DMA.png rename to assets/images/fide-fed/DMA.png diff --git a/assets/images/fide-fed/4.0x/DOM.png b/assets/images/fide-fed/DOM.png similarity index 100% rename from assets/images/fide-fed/4.0x/DOM.png rename to assets/images/fide-fed/DOM.png diff --git a/assets/images/fide-fed/4.0x/ECU.png b/assets/images/fide-fed/ECU.png similarity index 100% rename from assets/images/fide-fed/4.0x/ECU.png rename to assets/images/fide-fed/ECU.png diff --git a/assets/images/fide-fed/4.0x/EGY.png b/assets/images/fide-fed/EGY.png similarity index 100% rename from assets/images/fide-fed/4.0x/EGY.png rename to assets/images/fide-fed/EGY.png diff --git a/assets/images/fide-fed/4.0x/ENG.png b/assets/images/fide-fed/ENG.png similarity index 100% rename from assets/images/fide-fed/4.0x/ENG.png rename to assets/images/fide-fed/ENG.png diff --git a/assets/images/fide-fed/4.0x/ERI.png b/assets/images/fide-fed/ERI.png similarity index 100% rename from assets/images/fide-fed/4.0x/ERI.png rename to assets/images/fide-fed/ERI.png diff --git a/assets/images/fide-fed/4.0x/ESA.png b/assets/images/fide-fed/ESA.png similarity index 100% rename from assets/images/fide-fed/4.0x/ESA.png rename to assets/images/fide-fed/ESA.png diff --git a/assets/images/fide-fed/4.0x/ESP.png b/assets/images/fide-fed/ESP.png similarity index 100% rename from assets/images/fide-fed/4.0x/ESP.png rename to assets/images/fide-fed/ESP.png diff --git a/assets/images/fide-fed/4.0x/EST.png b/assets/images/fide-fed/EST.png similarity index 100% rename from assets/images/fide-fed/4.0x/EST.png rename to assets/images/fide-fed/EST.png diff --git a/assets/images/fide-fed/4.0x/ETH.png b/assets/images/fide-fed/ETH.png similarity index 100% rename from assets/images/fide-fed/4.0x/ETH.png rename to assets/images/fide-fed/ETH.png diff --git a/assets/images/fide-fed/4.0x/FAI.png b/assets/images/fide-fed/FAI.png similarity index 100% rename from assets/images/fide-fed/4.0x/FAI.png rename to assets/images/fide-fed/FAI.png diff --git a/assets/images/fide-fed/4.0x/FID.png b/assets/images/fide-fed/FID.png similarity index 100% rename from assets/images/fide-fed/4.0x/FID.png rename to assets/images/fide-fed/FID.png diff --git a/assets/images/fide-fed/4.0x/FIJ.png b/assets/images/fide-fed/FIJ.png similarity index 100% rename from assets/images/fide-fed/4.0x/FIJ.png rename to assets/images/fide-fed/FIJ.png diff --git a/assets/images/fide-fed/4.0x/FIN.png b/assets/images/fide-fed/FIN.png similarity index 100% rename from assets/images/fide-fed/4.0x/FIN.png rename to assets/images/fide-fed/FIN.png diff --git a/assets/images/fide-fed/4.0x/FRA.png b/assets/images/fide-fed/FRA.png similarity index 100% rename from assets/images/fide-fed/4.0x/FRA.png rename to assets/images/fide-fed/FRA.png diff --git a/assets/images/fide-fed/4.0x/GAB.png b/assets/images/fide-fed/GAB.png similarity index 100% rename from assets/images/fide-fed/4.0x/GAB.png rename to assets/images/fide-fed/GAB.png diff --git a/assets/images/fide-fed/4.0x/GAM.png b/assets/images/fide-fed/GAM.png similarity index 100% rename from assets/images/fide-fed/4.0x/GAM.png rename to assets/images/fide-fed/GAM.png diff --git a/assets/images/fide-fed/4.0x/GCI.png b/assets/images/fide-fed/GCI.png similarity index 100% rename from assets/images/fide-fed/4.0x/GCI.png rename to assets/images/fide-fed/GCI.png diff --git a/assets/images/fide-fed/4.0x/GEO.png b/assets/images/fide-fed/GEO.png similarity index 100% rename from assets/images/fide-fed/4.0x/GEO.png rename to assets/images/fide-fed/GEO.png diff --git a/assets/images/fide-fed/4.0x/GEQ.png b/assets/images/fide-fed/GEQ.png similarity index 100% rename from assets/images/fide-fed/4.0x/GEQ.png rename to assets/images/fide-fed/GEQ.png diff --git a/assets/images/fide-fed/4.0x/GER.png b/assets/images/fide-fed/GER.png similarity index 100% rename from assets/images/fide-fed/4.0x/GER.png rename to assets/images/fide-fed/GER.png diff --git a/assets/images/fide-fed/4.0x/GHA.png b/assets/images/fide-fed/GHA.png similarity index 100% rename from assets/images/fide-fed/4.0x/GHA.png rename to assets/images/fide-fed/GHA.png diff --git a/assets/images/fide-fed/4.0x/GRE.png b/assets/images/fide-fed/GRE.png similarity index 100% rename from assets/images/fide-fed/4.0x/GRE.png rename to assets/images/fide-fed/GRE.png diff --git a/assets/images/fide-fed/4.0x/GRN.png b/assets/images/fide-fed/GRN.png similarity index 100% rename from assets/images/fide-fed/4.0x/GRN.png rename to assets/images/fide-fed/GRN.png diff --git a/assets/images/fide-fed/4.0x/GUA.png b/assets/images/fide-fed/GUA.png similarity index 100% rename from assets/images/fide-fed/4.0x/GUA.png rename to assets/images/fide-fed/GUA.png diff --git a/assets/images/fide-fed/4.0x/GUM.png b/assets/images/fide-fed/GUM.png similarity index 100% rename from assets/images/fide-fed/4.0x/GUM.png rename to assets/images/fide-fed/GUM.png diff --git a/assets/images/fide-fed/4.0x/GUY.png b/assets/images/fide-fed/GUY.png similarity index 100% rename from assets/images/fide-fed/4.0x/GUY.png rename to assets/images/fide-fed/GUY.png diff --git a/assets/images/fide-fed/4.0x/HAI.png b/assets/images/fide-fed/HAI.png similarity index 100% rename from assets/images/fide-fed/4.0x/HAI.png rename to assets/images/fide-fed/HAI.png diff --git a/assets/images/fide-fed/4.0x/HKG.png b/assets/images/fide-fed/HKG.png similarity index 100% rename from assets/images/fide-fed/4.0x/HKG.png rename to assets/images/fide-fed/HKG.png diff --git a/assets/images/fide-fed/4.0x/HON.png b/assets/images/fide-fed/HON.png similarity index 100% rename from assets/images/fide-fed/4.0x/HON.png rename to assets/images/fide-fed/HON.png diff --git a/assets/images/fide-fed/4.0x/HUN.png b/assets/images/fide-fed/HUN.png similarity index 100% rename from assets/images/fide-fed/4.0x/HUN.png rename to assets/images/fide-fed/HUN.png diff --git a/assets/images/fide-fed/4.0x/INA.png b/assets/images/fide-fed/INA.png similarity index 100% rename from assets/images/fide-fed/4.0x/INA.png rename to assets/images/fide-fed/INA.png diff --git a/assets/images/fide-fed/4.0x/IND.png b/assets/images/fide-fed/IND.png similarity index 100% rename from assets/images/fide-fed/4.0x/IND.png rename to assets/images/fide-fed/IND.png diff --git a/assets/images/fide-fed/4.0x/IOM.png b/assets/images/fide-fed/IOM.png similarity index 100% rename from assets/images/fide-fed/4.0x/IOM.png rename to assets/images/fide-fed/IOM.png diff --git a/assets/images/fide-fed/4.0x/IRI.png b/assets/images/fide-fed/IRI.png similarity index 100% rename from assets/images/fide-fed/4.0x/IRI.png rename to assets/images/fide-fed/IRI.png diff --git a/assets/images/fide-fed/4.0x/IRL.png b/assets/images/fide-fed/IRL.png similarity index 100% rename from assets/images/fide-fed/4.0x/IRL.png rename to assets/images/fide-fed/IRL.png diff --git a/assets/images/fide-fed/4.0x/IRQ.png b/assets/images/fide-fed/IRQ.png similarity index 100% rename from assets/images/fide-fed/4.0x/IRQ.png rename to assets/images/fide-fed/IRQ.png diff --git a/assets/images/fide-fed/4.0x/ISL.png b/assets/images/fide-fed/ISL.png similarity index 100% rename from assets/images/fide-fed/4.0x/ISL.png rename to assets/images/fide-fed/ISL.png diff --git a/assets/images/fide-fed/4.0x/ISR.png b/assets/images/fide-fed/ISR.png similarity index 100% rename from assets/images/fide-fed/4.0x/ISR.png rename to assets/images/fide-fed/ISR.png diff --git a/assets/images/fide-fed/4.0x/ISV.png b/assets/images/fide-fed/ISV.png similarity index 100% rename from assets/images/fide-fed/4.0x/ISV.png rename to assets/images/fide-fed/ISV.png diff --git a/assets/images/fide-fed/4.0x/ITA.png b/assets/images/fide-fed/ITA.png similarity index 100% rename from assets/images/fide-fed/4.0x/ITA.png rename to assets/images/fide-fed/ITA.png diff --git a/assets/images/fide-fed/4.0x/IVB.png b/assets/images/fide-fed/IVB.png similarity index 100% rename from assets/images/fide-fed/4.0x/IVB.png rename to assets/images/fide-fed/IVB.png diff --git a/assets/images/fide-fed/4.0x/JAM.png b/assets/images/fide-fed/JAM.png similarity index 100% rename from assets/images/fide-fed/4.0x/JAM.png rename to assets/images/fide-fed/JAM.png diff --git a/assets/images/fide-fed/4.0x/JCI.png b/assets/images/fide-fed/JCI.png similarity index 100% rename from assets/images/fide-fed/4.0x/JCI.png rename to assets/images/fide-fed/JCI.png diff --git a/assets/images/fide-fed/4.0x/JOR.png b/assets/images/fide-fed/JOR.png similarity index 100% rename from assets/images/fide-fed/4.0x/JOR.png rename to assets/images/fide-fed/JOR.png diff --git a/assets/images/fide-fed/4.0x/JPN.png b/assets/images/fide-fed/JPN.png similarity index 100% rename from assets/images/fide-fed/4.0x/JPN.png rename to assets/images/fide-fed/JPN.png diff --git a/assets/images/fide-fed/4.0x/KAZ.png b/assets/images/fide-fed/KAZ.png similarity index 100% rename from assets/images/fide-fed/4.0x/KAZ.png rename to assets/images/fide-fed/KAZ.png diff --git a/assets/images/fide-fed/4.0x/KEN.png b/assets/images/fide-fed/KEN.png similarity index 100% rename from assets/images/fide-fed/4.0x/KEN.png rename to assets/images/fide-fed/KEN.png diff --git a/assets/images/fide-fed/4.0x/KGZ.png b/assets/images/fide-fed/KGZ.png similarity index 100% rename from assets/images/fide-fed/4.0x/KGZ.png rename to assets/images/fide-fed/KGZ.png diff --git a/assets/images/fide-fed/4.0x/KOR.png b/assets/images/fide-fed/KOR.png similarity index 100% rename from assets/images/fide-fed/4.0x/KOR.png rename to assets/images/fide-fed/KOR.png diff --git a/assets/images/fide-fed/4.0x/KOS.png b/assets/images/fide-fed/KOS.png similarity index 100% rename from assets/images/fide-fed/4.0x/KOS.png rename to assets/images/fide-fed/KOS.png diff --git a/assets/images/fide-fed/4.0x/KSA.png b/assets/images/fide-fed/KSA.png similarity index 100% rename from assets/images/fide-fed/4.0x/KSA.png rename to assets/images/fide-fed/KSA.png diff --git a/assets/images/fide-fed/4.0x/KUW.png b/assets/images/fide-fed/KUW.png similarity index 100% rename from assets/images/fide-fed/4.0x/KUW.png rename to assets/images/fide-fed/KUW.png diff --git a/assets/images/fide-fed/4.0x/LAO.png b/assets/images/fide-fed/LAO.png similarity index 100% rename from assets/images/fide-fed/4.0x/LAO.png rename to assets/images/fide-fed/LAO.png diff --git a/assets/images/fide-fed/4.0x/LAT.png b/assets/images/fide-fed/LAT.png similarity index 100% rename from assets/images/fide-fed/4.0x/LAT.png rename to assets/images/fide-fed/LAT.png diff --git a/assets/images/fide-fed/4.0x/LBA.png b/assets/images/fide-fed/LBA.png similarity index 100% rename from assets/images/fide-fed/4.0x/LBA.png rename to assets/images/fide-fed/LBA.png diff --git a/assets/images/fide-fed/4.0x/LBN.png b/assets/images/fide-fed/LBN.png similarity index 100% rename from assets/images/fide-fed/4.0x/LBN.png rename to assets/images/fide-fed/LBN.png diff --git a/assets/images/fide-fed/4.0x/LBR.png b/assets/images/fide-fed/LBR.png similarity index 100% rename from assets/images/fide-fed/4.0x/LBR.png rename to assets/images/fide-fed/LBR.png diff --git a/assets/images/fide-fed/4.0x/LCA.png b/assets/images/fide-fed/LCA.png similarity index 100% rename from assets/images/fide-fed/4.0x/LCA.png rename to assets/images/fide-fed/LCA.png diff --git a/assets/images/fide-fed/4.0x/LES.png b/assets/images/fide-fed/LES.png similarity index 100% rename from assets/images/fide-fed/4.0x/LES.png rename to assets/images/fide-fed/LES.png diff --git a/assets/images/fide-fed/4.0x/LIE.png b/assets/images/fide-fed/LIE.png similarity index 100% rename from assets/images/fide-fed/4.0x/LIE.png rename to assets/images/fide-fed/LIE.png diff --git a/assets/images/fide-fed/4.0x/LTU.png b/assets/images/fide-fed/LTU.png similarity index 100% rename from assets/images/fide-fed/4.0x/LTU.png rename to assets/images/fide-fed/LTU.png diff --git a/assets/images/fide-fed/4.0x/LUX.png b/assets/images/fide-fed/LUX.png similarity index 100% rename from assets/images/fide-fed/4.0x/LUX.png rename to assets/images/fide-fed/LUX.png diff --git a/assets/images/fide-fed/4.0x/MAC.png b/assets/images/fide-fed/MAC.png similarity index 100% rename from assets/images/fide-fed/4.0x/MAC.png rename to assets/images/fide-fed/MAC.png diff --git a/assets/images/fide-fed/4.0x/MAD.png b/assets/images/fide-fed/MAD.png similarity index 100% rename from assets/images/fide-fed/4.0x/MAD.png rename to assets/images/fide-fed/MAD.png diff --git a/assets/images/fide-fed/4.0x/MAR.png b/assets/images/fide-fed/MAR.png similarity index 100% rename from assets/images/fide-fed/4.0x/MAR.png rename to assets/images/fide-fed/MAR.png diff --git a/assets/images/fide-fed/4.0x/MAS.png b/assets/images/fide-fed/MAS.png similarity index 100% rename from assets/images/fide-fed/4.0x/MAS.png rename to assets/images/fide-fed/MAS.png diff --git a/assets/images/fide-fed/4.0x/MAW.png b/assets/images/fide-fed/MAW.png similarity index 100% rename from assets/images/fide-fed/4.0x/MAW.png rename to assets/images/fide-fed/MAW.png diff --git a/assets/images/fide-fed/4.0x/MDA.png b/assets/images/fide-fed/MDA.png similarity index 100% rename from assets/images/fide-fed/4.0x/MDA.png rename to assets/images/fide-fed/MDA.png diff --git a/assets/images/fide-fed/4.0x/MDV.png b/assets/images/fide-fed/MDV.png similarity index 100% rename from assets/images/fide-fed/4.0x/MDV.png rename to assets/images/fide-fed/MDV.png diff --git a/assets/images/fide-fed/4.0x/MEX.png b/assets/images/fide-fed/MEX.png similarity index 100% rename from assets/images/fide-fed/4.0x/MEX.png rename to assets/images/fide-fed/MEX.png diff --git a/assets/images/fide-fed/4.0x/MGL.png b/assets/images/fide-fed/MGL.png similarity index 100% rename from assets/images/fide-fed/4.0x/MGL.png rename to assets/images/fide-fed/MGL.png diff --git a/assets/images/fide-fed/4.0x/MKD.png b/assets/images/fide-fed/MKD.png similarity index 100% rename from assets/images/fide-fed/4.0x/MKD.png rename to assets/images/fide-fed/MKD.png diff --git a/assets/images/fide-fed/4.0x/MLI.png b/assets/images/fide-fed/MLI.png similarity index 100% rename from assets/images/fide-fed/4.0x/MLI.png rename to assets/images/fide-fed/MLI.png diff --git a/assets/images/fide-fed/4.0x/MLT.png b/assets/images/fide-fed/MLT.png similarity index 100% rename from assets/images/fide-fed/4.0x/MLT.png rename to assets/images/fide-fed/MLT.png diff --git a/assets/images/fide-fed/4.0x/MNC.png b/assets/images/fide-fed/MNC.png similarity index 100% rename from assets/images/fide-fed/4.0x/MNC.png rename to assets/images/fide-fed/MNC.png diff --git a/assets/images/fide-fed/4.0x/MNE.png b/assets/images/fide-fed/MNE.png similarity index 100% rename from assets/images/fide-fed/4.0x/MNE.png rename to assets/images/fide-fed/MNE.png diff --git a/assets/images/fide-fed/4.0x/MOZ.png b/assets/images/fide-fed/MOZ.png similarity index 100% rename from assets/images/fide-fed/4.0x/MOZ.png rename to assets/images/fide-fed/MOZ.png diff --git a/assets/images/fide-fed/4.0x/MRI.png b/assets/images/fide-fed/MRI.png similarity index 100% rename from assets/images/fide-fed/4.0x/MRI.png rename to assets/images/fide-fed/MRI.png diff --git a/assets/images/fide-fed/4.0x/MTN.png b/assets/images/fide-fed/MTN.png similarity index 100% rename from assets/images/fide-fed/4.0x/MTN.png rename to assets/images/fide-fed/MTN.png diff --git a/assets/images/fide-fed/4.0x/MYA.png b/assets/images/fide-fed/MYA.png similarity index 100% rename from assets/images/fide-fed/4.0x/MYA.png rename to assets/images/fide-fed/MYA.png diff --git a/assets/images/fide-fed/4.0x/NAM.png b/assets/images/fide-fed/NAM.png similarity index 100% rename from assets/images/fide-fed/4.0x/NAM.png rename to assets/images/fide-fed/NAM.png diff --git a/assets/images/fide-fed/4.0x/NCA.png b/assets/images/fide-fed/NCA.png similarity index 100% rename from assets/images/fide-fed/4.0x/NCA.png rename to assets/images/fide-fed/NCA.png diff --git a/assets/images/fide-fed/4.0x/NED.png b/assets/images/fide-fed/NED.png similarity index 100% rename from assets/images/fide-fed/4.0x/NED.png rename to assets/images/fide-fed/NED.png diff --git a/assets/images/fide-fed/4.0x/NEP.png b/assets/images/fide-fed/NEP.png similarity index 100% rename from assets/images/fide-fed/4.0x/NEP.png rename to assets/images/fide-fed/NEP.png diff --git a/assets/images/fide-fed/4.0x/NGR.png b/assets/images/fide-fed/NGR.png similarity index 100% rename from assets/images/fide-fed/4.0x/NGR.png rename to assets/images/fide-fed/NGR.png diff --git a/assets/images/fide-fed/4.0x/NIG.png b/assets/images/fide-fed/NIG.png similarity index 100% rename from assets/images/fide-fed/4.0x/NIG.png rename to assets/images/fide-fed/NIG.png diff --git a/assets/images/fide-fed/4.0x/NOR.png b/assets/images/fide-fed/NOR.png similarity index 100% rename from assets/images/fide-fed/4.0x/NOR.png rename to assets/images/fide-fed/NOR.png diff --git a/assets/images/fide-fed/4.0x/NRU.png b/assets/images/fide-fed/NRU.png similarity index 100% rename from assets/images/fide-fed/4.0x/NRU.png rename to assets/images/fide-fed/NRU.png diff --git a/assets/images/fide-fed/4.0x/NZL.png b/assets/images/fide-fed/NZL.png similarity index 100% rename from assets/images/fide-fed/4.0x/NZL.png rename to assets/images/fide-fed/NZL.png diff --git a/assets/images/fide-fed/4.0x/OMA.png b/assets/images/fide-fed/OMA.png similarity index 100% rename from assets/images/fide-fed/4.0x/OMA.png rename to assets/images/fide-fed/OMA.png diff --git a/assets/images/fide-fed/4.0x/PAK.png b/assets/images/fide-fed/PAK.png similarity index 100% rename from assets/images/fide-fed/4.0x/PAK.png rename to assets/images/fide-fed/PAK.png diff --git a/assets/images/fide-fed/4.0x/PAN.png b/assets/images/fide-fed/PAN.png similarity index 100% rename from assets/images/fide-fed/4.0x/PAN.png rename to assets/images/fide-fed/PAN.png diff --git a/assets/images/fide-fed/4.0x/PAR.png b/assets/images/fide-fed/PAR.png similarity index 100% rename from assets/images/fide-fed/4.0x/PAR.png rename to assets/images/fide-fed/PAR.png diff --git a/assets/images/fide-fed/4.0x/PER.png b/assets/images/fide-fed/PER.png similarity index 100% rename from assets/images/fide-fed/4.0x/PER.png rename to assets/images/fide-fed/PER.png diff --git a/assets/images/fide-fed/4.0x/PHI.png b/assets/images/fide-fed/PHI.png similarity index 100% rename from assets/images/fide-fed/4.0x/PHI.png rename to assets/images/fide-fed/PHI.png diff --git a/assets/images/fide-fed/4.0x/PLE.png b/assets/images/fide-fed/PLE.png similarity index 100% rename from assets/images/fide-fed/4.0x/PLE.png rename to assets/images/fide-fed/PLE.png diff --git a/assets/images/fide-fed/4.0x/PLW.png b/assets/images/fide-fed/PLW.png similarity index 100% rename from assets/images/fide-fed/4.0x/PLW.png rename to assets/images/fide-fed/PLW.png diff --git a/assets/images/fide-fed/4.0x/PNG.png b/assets/images/fide-fed/PNG.png similarity index 100% rename from assets/images/fide-fed/4.0x/PNG.png rename to assets/images/fide-fed/PNG.png diff --git a/assets/images/fide-fed/4.0x/POL.png b/assets/images/fide-fed/POL.png similarity index 100% rename from assets/images/fide-fed/4.0x/POL.png rename to assets/images/fide-fed/POL.png diff --git a/assets/images/fide-fed/4.0x/POR.png b/assets/images/fide-fed/POR.png similarity index 100% rename from assets/images/fide-fed/4.0x/POR.png rename to assets/images/fide-fed/POR.png diff --git a/assets/images/fide-fed/4.0x/PUR.png b/assets/images/fide-fed/PUR.png similarity index 100% rename from assets/images/fide-fed/4.0x/PUR.png rename to assets/images/fide-fed/PUR.png diff --git a/assets/images/fide-fed/4.0x/QAT.png b/assets/images/fide-fed/QAT.png similarity index 100% rename from assets/images/fide-fed/4.0x/QAT.png rename to assets/images/fide-fed/QAT.png diff --git a/assets/images/fide-fed/4.0x/ROU.png b/assets/images/fide-fed/ROU.png similarity index 100% rename from assets/images/fide-fed/4.0x/ROU.png rename to assets/images/fide-fed/ROU.png diff --git a/assets/images/fide-fed/4.0x/RSA.png b/assets/images/fide-fed/RSA.png similarity index 100% rename from assets/images/fide-fed/4.0x/RSA.png rename to assets/images/fide-fed/RSA.png diff --git a/assets/images/fide-fed/4.0x/RUS.png b/assets/images/fide-fed/RUS.png similarity index 100% rename from assets/images/fide-fed/4.0x/RUS.png rename to assets/images/fide-fed/RUS.png diff --git a/assets/images/fide-fed/4.0x/RWA.png b/assets/images/fide-fed/RWA.png similarity index 100% rename from assets/images/fide-fed/4.0x/RWA.png rename to assets/images/fide-fed/RWA.png diff --git a/assets/images/fide-fed/4.0x/SCO.png b/assets/images/fide-fed/SCO.png similarity index 100% rename from assets/images/fide-fed/4.0x/SCO.png rename to assets/images/fide-fed/SCO.png diff --git a/assets/images/fide-fed/4.0x/SEN.png b/assets/images/fide-fed/SEN.png similarity index 100% rename from assets/images/fide-fed/4.0x/SEN.png rename to assets/images/fide-fed/SEN.png diff --git a/assets/images/fide-fed/4.0x/SEY.png b/assets/images/fide-fed/SEY.png similarity index 100% rename from assets/images/fide-fed/4.0x/SEY.png rename to assets/images/fide-fed/SEY.png diff --git a/assets/images/fide-fed/4.0x/SGP.png b/assets/images/fide-fed/SGP.png similarity index 100% rename from assets/images/fide-fed/4.0x/SGP.png rename to assets/images/fide-fed/SGP.png diff --git a/assets/images/fide-fed/4.0x/SKN.png b/assets/images/fide-fed/SKN.png similarity index 100% rename from assets/images/fide-fed/4.0x/SKN.png rename to assets/images/fide-fed/SKN.png diff --git a/assets/images/fide-fed/4.0x/SLE.png b/assets/images/fide-fed/SLE.png similarity index 100% rename from assets/images/fide-fed/4.0x/SLE.png rename to assets/images/fide-fed/SLE.png diff --git a/assets/images/fide-fed/4.0x/SLO.png b/assets/images/fide-fed/SLO.png similarity index 100% rename from assets/images/fide-fed/4.0x/SLO.png rename to assets/images/fide-fed/SLO.png diff --git a/assets/images/fide-fed/4.0x/SMR.png b/assets/images/fide-fed/SMR.png similarity index 100% rename from assets/images/fide-fed/4.0x/SMR.png rename to assets/images/fide-fed/SMR.png diff --git a/assets/images/fide-fed/4.0x/SOL.png b/assets/images/fide-fed/SOL.png similarity index 100% rename from assets/images/fide-fed/4.0x/SOL.png rename to assets/images/fide-fed/SOL.png diff --git a/assets/images/fide-fed/4.0x/SOM.png b/assets/images/fide-fed/SOM.png similarity index 100% rename from assets/images/fide-fed/4.0x/SOM.png rename to assets/images/fide-fed/SOM.png diff --git a/assets/images/fide-fed/4.0x/SRB.png b/assets/images/fide-fed/SRB.png similarity index 100% rename from assets/images/fide-fed/4.0x/SRB.png rename to assets/images/fide-fed/SRB.png diff --git a/assets/images/fide-fed/4.0x/SRI.png b/assets/images/fide-fed/SRI.png similarity index 100% rename from assets/images/fide-fed/4.0x/SRI.png rename to assets/images/fide-fed/SRI.png diff --git a/assets/images/fide-fed/4.0x/SSD.png b/assets/images/fide-fed/SSD.png similarity index 100% rename from assets/images/fide-fed/4.0x/SSD.png rename to assets/images/fide-fed/SSD.png diff --git a/assets/images/fide-fed/4.0x/STP.png b/assets/images/fide-fed/STP.png similarity index 100% rename from assets/images/fide-fed/4.0x/STP.png rename to assets/images/fide-fed/STP.png diff --git a/assets/images/fide-fed/4.0x/SUD.png b/assets/images/fide-fed/SUD.png similarity index 100% rename from assets/images/fide-fed/4.0x/SUD.png rename to assets/images/fide-fed/SUD.png diff --git a/assets/images/fide-fed/4.0x/SUI.png b/assets/images/fide-fed/SUI.png similarity index 100% rename from assets/images/fide-fed/4.0x/SUI.png rename to assets/images/fide-fed/SUI.png diff --git a/assets/images/fide-fed/4.0x/SUR.png b/assets/images/fide-fed/SUR.png similarity index 100% rename from assets/images/fide-fed/4.0x/SUR.png rename to assets/images/fide-fed/SUR.png diff --git a/assets/images/fide-fed/4.0x/SVK.png b/assets/images/fide-fed/SVK.png similarity index 100% rename from assets/images/fide-fed/4.0x/SVK.png rename to assets/images/fide-fed/SVK.png diff --git a/assets/images/fide-fed/4.0x/SWE.png b/assets/images/fide-fed/SWE.png similarity index 100% rename from assets/images/fide-fed/4.0x/SWE.png rename to assets/images/fide-fed/SWE.png diff --git a/assets/images/fide-fed/4.0x/SWZ.png b/assets/images/fide-fed/SWZ.png similarity index 100% rename from assets/images/fide-fed/4.0x/SWZ.png rename to assets/images/fide-fed/SWZ.png diff --git a/assets/images/fide-fed/4.0x/SYR.png b/assets/images/fide-fed/SYR.png similarity index 100% rename from assets/images/fide-fed/4.0x/SYR.png rename to assets/images/fide-fed/SYR.png diff --git a/assets/images/fide-fed/4.0x/TAN.png b/assets/images/fide-fed/TAN.png similarity index 100% rename from assets/images/fide-fed/4.0x/TAN.png rename to assets/images/fide-fed/TAN.png diff --git a/assets/images/fide-fed/4.0x/THA.png b/assets/images/fide-fed/THA.png similarity index 100% rename from assets/images/fide-fed/4.0x/THA.png rename to assets/images/fide-fed/THA.png diff --git a/assets/images/fide-fed/4.0x/TJK.png b/assets/images/fide-fed/TJK.png similarity index 100% rename from assets/images/fide-fed/4.0x/TJK.png rename to assets/images/fide-fed/TJK.png diff --git a/assets/images/fide-fed/4.0x/TKM.png b/assets/images/fide-fed/TKM.png similarity index 100% rename from assets/images/fide-fed/4.0x/TKM.png rename to assets/images/fide-fed/TKM.png diff --git a/assets/images/fide-fed/4.0x/TLS.png b/assets/images/fide-fed/TLS.png similarity index 100% rename from assets/images/fide-fed/4.0x/TLS.png rename to assets/images/fide-fed/TLS.png diff --git a/assets/images/fide-fed/4.0x/TOG.png b/assets/images/fide-fed/TOG.png similarity index 100% rename from assets/images/fide-fed/4.0x/TOG.png rename to assets/images/fide-fed/TOG.png diff --git a/assets/images/fide-fed/4.0x/TPE.png b/assets/images/fide-fed/TPE.png similarity index 100% rename from assets/images/fide-fed/4.0x/TPE.png rename to assets/images/fide-fed/TPE.png diff --git a/assets/images/fide-fed/4.0x/TTO.png b/assets/images/fide-fed/TTO.png similarity index 100% rename from assets/images/fide-fed/4.0x/TTO.png rename to assets/images/fide-fed/TTO.png diff --git a/assets/images/fide-fed/4.0x/TUN.png b/assets/images/fide-fed/TUN.png similarity index 100% rename from assets/images/fide-fed/4.0x/TUN.png rename to assets/images/fide-fed/TUN.png diff --git a/assets/images/fide-fed/4.0x/TUR.png b/assets/images/fide-fed/TUR.png similarity index 100% rename from assets/images/fide-fed/4.0x/TUR.png rename to assets/images/fide-fed/TUR.png diff --git a/assets/images/fide-fed/4.0x/UAE.png b/assets/images/fide-fed/UAE.png similarity index 100% rename from assets/images/fide-fed/4.0x/UAE.png rename to assets/images/fide-fed/UAE.png diff --git a/assets/images/fide-fed/4.0x/UGA.png b/assets/images/fide-fed/UGA.png similarity index 100% rename from assets/images/fide-fed/4.0x/UGA.png rename to assets/images/fide-fed/UGA.png diff --git a/assets/images/fide-fed/4.0x/UKR.png b/assets/images/fide-fed/UKR.png similarity index 100% rename from assets/images/fide-fed/4.0x/UKR.png rename to assets/images/fide-fed/UKR.png diff --git a/assets/images/fide-fed/4.0x/URU.png b/assets/images/fide-fed/URU.png similarity index 100% rename from assets/images/fide-fed/4.0x/URU.png rename to assets/images/fide-fed/URU.png diff --git a/assets/images/fide-fed/4.0x/USA.png b/assets/images/fide-fed/USA.png similarity index 100% rename from assets/images/fide-fed/4.0x/USA.png rename to assets/images/fide-fed/USA.png diff --git a/assets/images/fide-fed/4.0x/UZB.png b/assets/images/fide-fed/UZB.png similarity index 100% rename from assets/images/fide-fed/4.0x/UZB.png rename to assets/images/fide-fed/UZB.png diff --git a/assets/images/fide-fed/4.0x/VAN.png b/assets/images/fide-fed/VAN.png similarity index 100% rename from assets/images/fide-fed/4.0x/VAN.png rename to assets/images/fide-fed/VAN.png diff --git a/assets/images/fide-fed/4.0x/VEN.png b/assets/images/fide-fed/VEN.png similarity index 100% rename from assets/images/fide-fed/4.0x/VEN.png rename to assets/images/fide-fed/VEN.png diff --git a/assets/images/fide-fed/4.0x/VIE.png b/assets/images/fide-fed/VIE.png similarity index 100% rename from assets/images/fide-fed/4.0x/VIE.png rename to assets/images/fide-fed/VIE.png diff --git a/assets/images/fide-fed/4.0x/VIN.png b/assets/images/fide-fed/VIN.png similarity index 100% rename from assets/images/fide-fed/4.0x/VIN.png rename to assets/images/fide-fed/VIN.png diff --git a/assets/images/fide-fed/4.0x/W.png b/assets/images/fide-fed/W.png similarity index 100% rename from assets/images/fide-fed/4.0x/W.png rename to assets/images/fide-fed/W.png diff --git a/assets/images/fide-fed/4.0x/WLS.png b/assets/images/fide-fed/WLS.png similarity index 100% rename from assets/images/fide-fed/4.0x/WLS.png rename to assets/images/fide-fed/WLS.png diff --git a/assets/images/fide-fed/4.0x/YEM.png b/assets/images/fide-fed/YEM.png similarity index 100% rename from assets/images/fide-fed/4.0x/YEM.png rename to assets/images/fide-fed/YEM.png diff --git a/assets/images/fide-fed/4.0x/ZAM.png b/assets/images/fide-fed/ZAM.png similarity index 100% rename from assets/images/fide-fed/4.0x/ZAM.png rename to assets/images/fide-fed/ZAM.png diff --git a/assets/images/fide-fed/4.0x/ZIM.png b/assets/images/fide-fed/ZIM.png similarity index 100% rename from assets/images/fide-fed/4.0x/ZIM.png rename to assets/images/fide-fed/ZIM.png From 11080c1983546ea4a9da451b1143f41836f7e20a Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 17 Dec 2024 15:53:26 +0100 Subject: [PATCH 939/979] Upgrade dependencies --- pubspec.lock | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index efd5243f46..ed5e1b50a3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -98,50 +98,50 @@ packages: dependency: transitive description: name: build - sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" build_config: dependency: transitive description: name: build_config - sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" build_daemon: dependency: transitive description: name: build_daemon - sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" + sha256: "294a2edaf4814a378725bfe6358210196f5ea37af89ecd81bfa32960113d4948" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.0.3" build_resolvers: dependency: transitive description: name: build_resolvers - sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" + sha256: "99d3980049739a985cf9b21f30881f46db3ebc62c5b8d5e60e27440876b1ba1e" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.3" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" + sha256: "74691599a5bc750dc96a6b4bfd48f7d9d66453eab04c7f4063134800d6a5c573" url: "https://pub.dev" source: hosted - version: "2.4.13" + version: "2.4.14" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 + sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" url: "https://pub.dev" source: hosted - version: "7.3.2" + version: "8.0.0" built_collection: dependency: transitive description: @@ -775,10 +775,10 @@ packages: dependency: transitive description: name: http_multi_server - sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.2" http_parser: dependency: transitive description: @@ -1437,10 +1437,10 @@ packages: dependency: "direct main" description: name: stream_transform - sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" string_scanner: dependency: transitive description: @@ -1629,10 +1629,10 @@ packages: dependency: transitive description: name: watcher - sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" web: dependency: transitive description: From 5ad924633effecd32aabed4450cc9e5fb81b3a23 Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Tue, 17 Dec 2024 17:14:34 +0100 Subject: [PATCH 940/979] Use the new flag asset --- .../view/broadcast/broadcast_player_results_screen.dart | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_player_results_screen.dart b/lib/src/view/broadcast/broadcast_player_results_screen.dart index 21e88e2326..2b1d14527b 100644 --- a/lib/src/view/broadcast/broadcast_player_results_screen.dart +++ b/lib/src/view/broadcast/broadcast_player_results_screen.dart @@ -4,15 +4,12 @@ import 'package:dartchess/dartchess.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_svg/svg.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_federation.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_providers.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; -import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; -import 'package:lichess_mobile/src/utils/lichess_assets.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_game_screen.dart'; import 'package:lichess_mobile/src/view/broadcast/broadcast_player_widget.dart'; @@ -129,10 +126,9 @@ class _Body extends ConsumerWidget { child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - SvgPicture.network( - lichessFideFedSrc(player.federation!), + Image.asset( + 'assets/images/fide-fed/${player.federation}.png', height: 12, - httpClient: ref.read(defaultClientProvider), ), const SizedBox(width: 5), Flexible( From 32403725faf4c619f52a32ea3b93eba8246efe3b Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Wed, 18 Dec 2024 10:50:28 +0100 Subject: [PATCH 941/979] Remove flip board from the analysis menu because it is already present as an icon in the bottom bar --- lib/src/view/analysis/analysis_screen.dart | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lib/src/view/analysis/analysis_screen.dart b/lib/src/view/analysis/analysis_screen.dart index 24b7517fd5..24c77895f8 100644 --- a/lib/src/view/analysis/analysis_screen.dart +++ b/lib/src/view/analysis/analysis_screen.dart @@ -280,12 +280,6 @@ class _BottomBar extends ConsumerWidget { return showAdaptiveActionSheet( context: context, actions: [ - BottomSheetAction( - makeLabel: (context) => Text(context.l10n.flipBoard), - onPressed: (context) { - ref.read(analysisControllerProvider(options).notifier).toggleBoard(); - }, - ), BottomSheetAction( makeLabel: (context) => Text(context.l10n.boardEditor), onPressed: (context) { From 485ec0ac8c806cd340321c832cff6477b3f7724e Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 18 Dec 2024 14:53:39 +0100 Subject: [PATCH 942/979] Fix sort and empty score column --- .../view/broadcast/broadcast_players_tab.dart | 48 +++++++++++-------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/lib/src/view/broadcast/broadcast_players_tab.dart b/lib/src/view/broadcast/broadcast_players_tab.dart index 713766979e..6c1345b31e 100644 --- a/lib/src/view/broadcast/broadcast_players_tab.dart +++ b/lib/src/view/broadcast/broadcast_players_tab.dart @@ -63,9 +63,27 @@ class PlayersList extends ConsumerStatefulWidget { class _PlayersListState extends ConsumerState { late IList players; - _SortingTypes currentSort = _SortingTypes.score; + late _SortingTypes currentSort; bool reverse = false; + @override + void initState() { + super.initState(); + players = widget.players; + currentSort = players.firstOrNull?.score != null ? _SortingTypes.score : _SortingTypes.elo; + sort(currentSort); + } + + @override + void didUpdateWidget(PlayersList oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.players != widget.players) { + players = widget.players; + currentSort = players.firstOrNull?.score != null ? _SortingTypes.score : _SortingTypes.elo; + sort(currentSort); + } + } + void sort(_SortingTypes newSort, {bool toggleReverse = false}) { final compare = switch (newSort) { _SortingTypes.player => @@ -99,27 +117,13 @@ class _PlayersListState extends ConsumerState { }); } - @override - void initState() { - super.initState(); - players = widget.players; - sort(_SortingTypes.score); - } - - @override - void didUpdateWidget(PlayersList oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.players != widget.players) { - players = widget.players; - sort(_SortingTypes.score); - } - } - @override Widget build(BuildContext context) { final double eloWidth = max(MediaQuery.sizeOf(context).width * 0.2, 100); final double scoreWidth = max(MediaQuery.sizeOf(context).width * 0.15, 90); + final firstPlayer = players.firstOrNull; + return SliverList.builder( itemCount: players.length + 1, itemBuilder: (context, index) { @@ -157,7 +161,10 @@ class _PlayersListState extends ConsumerState { SizedBox( width: scoreWidth, child: _TableTitleCell( - title: Text(context.l10n.broadcastScore, style: _kHeaderTextStyle), + title: Text( + firstPlayer?.score != null ? context.l10n.broadcastScore : context.l10n.games, + style: _kHeaderTextStyle, + ), onTap: () => sort( _SortingTypes.score, @@ -237,7 +244,10 @@ class _PlayersListState extends ConsumerState { '${player.score!.toStringAsFixed((player.score! == player.score!.roundToDouble()) ? 0 : 1)} / ${player.played}', ), ) - : null, + : Align( + alignment: Alignment.centerRight, + child: Text(player.played.toString()), + ), ), ), ], From b28a6b3ff35324dcc7b036cf5ffd0aa100465b8a Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 18 Dec 2024 16:48:13 +0100 Subject: [PATCH 943/979] Only show computer analysis section if allowed --- lib/src/view/analysis/analysis_settings.dart | 125 ++++++++++--------- 1 file changed, 63 insertions(+), 62 deletions(-) diff --git a/lib/src/view/analysis/analysis_settings.dart b/lib/src/view/analysis/analysis_settings.dart index 896ac838be..ca1f922a36 100644 --- a/lib/src/view/analysis/analysis_settings.dart +++ b/lib/src/view/analysis/analysis_settings.dart @@ -32,69 +32,70 @@ class AnalysisSettings extends ConsumerWidget { appBar: PlatformAppBar(title: Text(context.l10n.settingsSettings)), body: ListView( children: [ - ListSection( - header: SettingsSectionTitle(context.l10n.computerAnalysis), - children: [ - SwitchSettingTile( - title: Text(context.l10n.enable), - value: prefs.enableComputerAnalysis, - onChanged: (_) { - ref.read(ctrlProvider.notifier).toggleComputerAnalysis(); - }, - ), - AnimatedCrossFade( - duration: const Duration(milliseconds: 300), - crossFadeState: - value.isComputerAnalysisAllowedAndEnabled - ? CrossFadeState.showSecond - : CrossFadeState.showFirst, - firstChild: const SizedBox.shrink(), - secondChild: ListSection( - margin: EdgeInsets.zero, - cupertinoBorderRadius: BorderRadius.zero, - cupertinoClipBehavior: Clip.none, - children: [ - SwitchSettingTile( - title: Text(context.l10n.evaluationGauge), - value: prefs.showEvaluationGauge, - onChanged: - (value) => - ref - .read(analysisPreferencesProvider.notifier) - .toggleShowEvaluationGauge(), - ), - SwitchSettingTile( - title: Text(context.l10n.toggleGlyphAnnotations), - value: prefs.showAnnotations, - onChanged: - (_) => - ref - .read(analysisPreferencesProvider.notifier) - .toggleAnnotations(), - ), - SwitchSettingTile( - title: Text(context.l10n.mobileShowComments), - value: prefs.showPgnComments, - onChanged: - (_) => - ref - .read(analysisPreferencesProvider.notifier) - .togglePgnComments(), - ), - SwitchSettingTile( - title: Text(context.l10n.bestMoveArrow), - value: prefs.showBestMoveArrow, - onChanged: - (value) => - ref - .read(analysisPreferencesProvider.notifier) - .toggleShowBestMoveArrow(), - ), - ], + if (value.isComputerAnalysisAllowed) + ListSection( + header: SettingsSectionTitle(context.l10n.computerAnalysis), + children: [ + SwitchSettingTile( + title: Text(context.l10n.enable), + value: prefs.enableComputerAnalysis, + onChanged: (_) { + ref.read(ctrlProvider.notifier).toggleComputerAnalysis(); + }, ), - ), - ], - ), + AnimatedCrossFade( + duration: const Duration(milliseconds: 300), + crossFadeState: + value.isComputerAnalysisAllowedAndEnabled + ? CrossFadeState.showSecond + : CrossFadeState.showFirst, + firstChild: const SizedBox.shrink(), + secondChild: ListSection( + margin: EdgeInsets.zero, + cupertinoBorderRadius: BorderRadius.zero, + cupertinoClipBehavior: Clip.none, + children: [ + SwitchSettingTile( + title: Text(context.l10n.evaluationGauge), + value: prefs.showEvaluationGauge, + onChanged: + (value) => + ref + .read(analysisPreferencesProvider.notifier) + .toggleShowEvaluationGauge(), + ), + SwitchSettingTile( + title: Text(context.l10n.toggleGlyphAnnotations), + value: prefs.showAnnotations, + onChanged: + (_) => + ref + .read(analysisPreferencesProvider.notifier) + .toggleAnnotations(), + ), + SwitchSettingTile( + title: Text(context.l10n.mobileShowComments), + value: prefs.showPgnComments, + onChanged: + (_) => + ref + .read(analysisPreferencesProvider.notifier) + .togglePgnComments(), + ), + SwitchSettingTile( + title: Text(context.l10n.bestMoveArrow), + value: prefs.showBestMoveArrow, + onChanged: + (value) => + ref + .read(analysisPreferencesProvider.notifier) + .toggleShowBestMoveArrow(), + ), + ], + ), + ), + ], + ), AnimatedCrossFade( duration: const Duration(milliseconds: 300), crossFadeState: From 5ceb4e6d2c7f679c4bcba9b3927c9b185d09dada Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 18 Dec 2024 16:49:04 +0100 Subject: [PATCH 944/979] Bump version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index bca18321a4..6e2f559d44 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: lichess_mobile description: Lichess mobile app V2 publish_to: "none" -version: 0.13.9+001309 # See README.md for details about versioning +version: 0.13.10+001310 # See README.md for details about versioning environment: sdk: '^3.7.0-209.1.beta' From b75cd58b24a7da4f20f641669feabf44f9037e59 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 18 Dec 2024 17:13:53 +0100 Subject: [PATCH 945/979] Delay analysis first autoscroll Closes #1232 Closes #1270 --- lib/src/view/analysis/tree_view.dart | 22 ++++++++++------------ lib/src/widgets/pgn.dart | 22 +++++++++++++++------- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/lib/src/view/analysis/tree_view.dart b/lib/src/view/analysis/tree_view.dart index 983cff10dd..3ed473a38a 100644 --- a/lib/src/view/analysis/tree_view.dart +++ b/lib/src/view/analysis/tree_view.dart @@ -24,19 +24,17 @@ class AnalysisTreeView extends ConsumerWidget { // enable computer analysis takes effect here only if it's a lichess game final enableComputerAnalysis = !options.isLichessGameAnalysis || prefs.enableComputerAnalysis; - return ListView( + return SingleChildScrollView( padding: EdgeInsets.zero, - children: [ - DebouncedPgnTreeView( - root: root, - currentPath: currentPath, - pgnRootComments: pgnRootComments, - notifier: ref.read(ctrlProvider.notifier), - shouldShowComputerVariations: enableComputerAnalysis, - shouldShowComments: enableComputerAnalysis && prefs.showPgnComments, - shouldShowAnnotations: enableComputerAnalysis && prefs.showAnnotations, - ), - ], + child: DebouncedPgnTreeView( + root: root, + currentPath: currentPath, + pgnRootComments: pgnRootComments, + notifier: ref.read(ctrlProvider.notifier), + shouldShowComputerVariations: enableComputerAnalysis, + shouldShowComments: enableComputerAnalysis && prefs.showPgnComments, + shouldShowAnnotations: enableComputerAnalysis && prefs.showAnnotations, + ), ); } } diff --git a/lib/src/widgets/pgn.dart b/lib/src/widgets/pgn.dart index 4c39e43570..2b4b414982 100644 --- a/lib/src/widgets/pgn.dart +++ b/lib/src/widgets/pgn.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:chessground/chessground.dart'; import 'package:collection/collection.dart'; import 'package:dartchess/dartchess.dart'; @@ -140,25 +142,31 @@ class _DebouncedPgnTreeViewState extends ConsumerState { /// Path to the last live move in the tree if it is a broadcast game. When widget.broadcastLivePath changes rapidly, we debounce the change to avoid rebuilding the whole tree on every received move. late UciPath? pathToBroadcastLiveMove; + Timer? _scrollTimer; + @override void initState() { super.initState(); pathToCurrentMove = widget.currentPath; pathToBroadcastLiveMove = widget.broadcastLivePath; WidgetsBinding.instance.addPostFrameCallback((_) { - if (currentMoveKey.currentContext != null) { - Scrollable.ensureVisible( - currentMoveKey.currentContext!, - alignment: 0.5, - alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd, - ); - } + _scrollTimer?.cancel(); + _scrollTimer = Timer(const Duration(milliseconds: 500), () { + if (currentMoveKey.currentContext != null) { + Scrollable.ensureVisible( + currentMoveKey.currentContext!, + alignment: 0.5, + alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd, + ); + } + }); }); } @override void dispose() { _debounce.dispose(); + _scrollTimer?.cancel(); super.dispose(); } From 70d284c5f8e177d8d087d32a3b6fe43e7d1cb82d Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 19 Dec 2024 09:49:14 +0100 Subject: [PATCH 946/979] Translate relative date, improve broadcast list --- lib/l10n/app_en.arb | 124 +++++++ lib/l10n/l10n.dart | 114 ++++++ lib/l10n/l10n_af.dart | 177 +++++++++ lib/l10n/l10n_ar.dart | 237 ++++++++++++ lib/l10n/l10n_az.dart | 177 +++++++++ lib/l10n/l10n_be.dart | 207 +++++++++++ lib/l10n/l10n_bg.dart | 177 +++++++++ lib/l10n/l10n_bn.dart | 177 +++++++++ lib/l10n/l10n_br.dart | 216 +++++++++++ lib/l10n/l10n_bs.dart | 190 ++++++++++ lib/l10n/l10n_ca.dart | 177 +++++++++ lib/l10n/l10n_cs.dart | 207 +++++++++++ lib/l10n/l10n_da.dart | 177 +++++++++ lib/l10n/l10n_de.dart | 177 +++++++++ lib/l10n/l10n_el.dart | 177 +++++++++ lib/l10n/l10n_en.dart | 351 ++++++++++++++++++ lib/l10n/l10n_eo.dart | 177 +++++++++ lib/l10n/l10n_es.dart | 177 +++++++++ lib/l10n/l10n_et.dart | 177 +++++++++ lib/l10n/l10n_eu.dart | 177 +++++++++ lib/l10n/l10n_fa.dart | 177 +++++++++ lib/l10n/l10n_fi.dart | 177 +++++++++ lib/l10n/l10n_fo.dart | 177 +++++++++ lib/l10n/l10n_fr.dart | 177 +++++++++ lib/l10n/l10n_ga.dart | 216 +++++++++++ lib/l10n/l10n_gl.dart | 177 +++++++++ lib/l10n/l10n_gsw.dart | 177 +++++++++ lib/l10n/l10n_he.dart | 207 +++++++++++ lib/l10n/l10n_hi.dart | 177 +++++++++ lib/l10n/l10n_hr.dart | 190 ++++++++++ lib/l10n/l10n_hu.dart | 177 +++++++++ lib/l10n/l10n_hy.dart | 177 +++++++++ lib/l10n/l10n_id.dart | 162 ++++++++ lib/l10n/l10n_it.dart | 177 +++++++++ lib/l10n/l10n_ja.dart | 162 ++++++++ lib/l10n/l10n_kk.dart | 177 +++++++++ lib/l10n/l10n_ko.dart | 162 ++++++++ lib/l10n/l10n_lb.dart | 177 +++++++++ lib/l10n/l10n_lt.dart | 207 +++++++++++ lib/l10n/l10n_lv.dart | 190 ++++++++++ lib/l10n/l10n_mk.dart | 177 +++++++++ lib/l10n/l10n_nb.dart | 177 +++++++++ lib/l10n/l10n_nl.dart | 177 +++++++++ lib/l10n/l10n_nn.dart | 177 +++++++++ lib/l10n/l10n_pl.dart | 207 +++++++++++ lib/l10n/l10n_pt.dart | 351 ++++++++++++++++++ lib/l10n/l10n_ro.dart | 192 ++++++++++ lib/l10n/l10n_ru.dart | 207 +++++++++++ lib/l10n/l10n_sk.dart | 207 +++++++++++ lib/l10n/l10n_sl.dart | 207 +++++++++++ lib/l10n/l10n_sq.dart | 177 +++++++++ lib/l10n/l10n_sr.dart | 178 +++++++++ lib/l10n/l10n_sv.dart | 177 +++++++++ lib/l10n/l10n_tr.dart | 177 +++++++++ lib/l10n/l10n_uk.dart | 207 +++++++++++ lib/l10n/l10n_vi.dart | 162 ++++++++ lib/l10n/l10n_zh.dart | 321 ++++++++++++++++ lib/l10n/lila_af.arb | 20 +- lib/l10n/lila_ar.arb | 20 +- lib/l10n/lila_az.arb | 4 +- lib/l10n/lila_be.arb | 20 +- lib/l10n/lila_bg.arb | 20 +- lib/l10n/lila_bn.arb | 20 +- lib/l10n/lila_br.arb | 17 +- lib/l10n/lila_bs.arb | 17 +- lib/l10n/lila_ca.arb | 20 +- lib/l10n/lila_cs.arb | 20 +- lib/l10n/lila_da.arb | 20 +- lib/l10n/lila_de.arb | 20 +- lib/l10n/lila_el.arb | 20 +- lib/l10n/lila_en_US.arb | 20 +- lib/l10n/lila_eo.arb | 20 +- lib/l10n/lila_es.arb | 20 +- lib/l10n/lila_et.arb | 20 +- lib/l10n/lila_eu.arb | 20 +- lib/l10n/lila_fa.arb | 20 +- lib/l10n/lila_fi.arb | 20 +- lib/l10n/lila_fr.arb | 20 +- lib/l10n/lila_ga.arb | 17 +- lib/l10n/lila_gl.arb | 20 +- lib/l10n/lila_gsw.arb | 20 +- lib/l10n/lila_he.arb | 20 +- lib/l10n/lila_hi.arb | 20 +- lib/l10n/lila_hr.arb | 18 +- lib/l10n/lila_hu.arb | 20 +- lib/l10n/lila_hy.arb | 18 +- lib/l10n/lila_id.arb | 20 +- lib/l10n/lila_it.arb | 20 +- lib/l10n/lila_ja.arb | 20 +- lib/l10n/lila_kk.arb | 17 +- lib/l10n/lila_ko.arb | 20 +- lib/l10n/lila_lb.arb | 20 +- lib/l10n/lila_lt.arb | 20 +- lib/l10n/lila_lv.arb | 17 +- lib/l10n/lila_mk.arb | 17 +- lib/l10n/lila_nb.arb | 20 +- lib/l10n/lila_nl.arb | 20 +- lib/l10n/lila_nn.arb | 20 +- lib/l10n/lila_pl.arb | 20 +- lib/l10n/lila_pt.arb | 20 +- lib/l10n/lila_pt_BR.arb | 20 +- lib/l10n/lila_ro.arb | 20 +- lib/l10n/lila_ru.arb | 20 +- lib/l10n/lila_sk.arb | 20 +- lib/l10n/lila_sl.arb | 20 +- lib/l10n/lila_sq.arb | 20 +- lib/l10n/lila_sr.arb | 6 +- lib/l10n/lila_sv.arb | 20 +- lib/l10n/lila_tr.arb | 20 +- lib/l10n/lila_uk.arb | 20 +- lib/l10n/lila_vi.arb | 20 +- lib/l10n/lila_zh.arb | 20 +- lib/l10n/lila_zh_TW.arb | 20 +- lib/src/constants.dart | 1 + lib/src/utils/l10n.dart | 45 ++- .../view/broadcast/broadcast_list_screen.dart | 131 ++++--- lib/src/view/watch/watch_tab_screen.dart | 2 +- lib/src/widgets/platform.dart | 8 +- scripts/update-arb-from-crowdin.mjs | 1 + 119 files changed, 12000 insertions(+), 124 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 5e36f84c81..9f4613589e 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -567,6 +567,7 @@ "preferencesDisplayBoardResizeHandle": "Show board resize handle", "preferencesOnlyOnInitialPosition": "Only on initial position", "preferencesInGameOnly": "In-game only", + "preferencesExceptInGame": "Except in-game", "preferencesChessClock": "Chess clock", "preferencesTenthsOfSeconds": "Tenths of seconds", "preferencesWhenTimeRemainingLessThanTenSeconds": "When time remaining < 10 seconds", @@ -3164,5 +3165,128 @@ "type": "int" } } + }, + "timeagoJustNow": "just now", + "timeagoRightNow": "right now", + "timeagoCompleted": "completed", + "timeagoInNbSeconds": "{count, plural, =1{in {count} second} other{in {count} seconds}}", + "@timeagoInNbSeconds": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "timeagoInNbMinutes": "{count, plural, =1{in {count} minute} other{in {count} minutes}}", + "@timeagoInNbMinutes": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "timeagoInNbHours": "{count, plural, =1{in {count} hour} other{in {count} hours}}", + "@timeagoInNbHours": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "timeagoInNbDays": "{count, plural, =1{in {count} day} other{in {count} days}}", + "@timeagoInNbDays": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "timeagoInNbWeeks": "{count, plural, =1{in {count} week} other{in {count} weeks}}", + "@timeagoInNbWeeks": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "timeagoInNbMonths": "{count, plural, =1{in {count} month} other{in {count} months}}", + "@timeagoInNbMonths": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "timeagoInNbYears": "{count, plural, =1{in {count} year} other{in {count} years}}", + "@timeagoInNbYears": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "timeagoNbMinutesAgo": "{count, plural, =1{{count} minute ago} other{{count} minutes ago}}", + "@timeagoNbMinutesAgo": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "timeagoNbHoursAgo": "{count, plural, =1{{count} hour ago} other{{count} hours ago}}", + "@timeagoNbHoursAgo": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "timeagoNbDaysAgo": "{count, plural, =1{{count} day ago} other{{count} days ago}}", + "@timeagoNbDaysAgo": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "timeagoNbWeeksAgo": "{count, plural, =1{{count} week ago} other{{count} weeks ago}}", + "@timeagoNbWeeksAgo": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "timeagoNbMonthsAgo": "{count, plural, =1{{count} month ago} other{{count} months ago}}", + "@timeagoNbMonthsAgo": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "timeagoNbYearsAgo": "{count, plural, =1{{count} year ago} other{{count} years ago}}", + "@timeagoNbYearsAgo": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "timeagoNbMinutesRemaining": "{count, plural, =1{{count} minute remaining} other{{count} minutes remaining}}", + "@timeagoNbMinutesRemaining": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "timeagoNbHoursRemaining": "{count, plural, =1{{count} hour remaining} other{{count} hours remaining}}", + "@timeagoNbHoursRemaining": { + "placeholders": { + "count": { + "type": "int" + } + } } } \ No newline at end of file diff --git a/lib/l10n/l10n.dart b/lib/l10n/l10n.dart index 1fd742dfc6..0873ea0921 100644 --- a/lib/l10n/l10n.dart +++ b/lib/l10n/l10n.dart @@ -1512,6 +1512,12 @@ abstract class AppLocalizations { /// **'In-game only'** String get preferencesInGameOnly; + /// No description provided for @preferencesExceptInGame. + /// + /// In en, this message translates to: + /// **'Except in-game'** + String get preferencesExceptInGame; + /// No description provided for @preferencesChessClock. /// /// In en, this message translates to: @@ -9491,6 +9497,114 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'{count, plural, =1{Paste your PGN text here, up to {count} game} other{Paste your PGN text here, up to {count} games}}'** String studyPasteYourPgnTextHereUpToNbGames(int count); + + /// No description provided for @timeagoJustNow. + /// + /// In en, this message translates to: + /// **'just now'** + String get timeagoJustNow; + + /// No description provided for @timeagoRightNow. + /// + /// In en, this message translates to: + /// **'right now'** + String get timeagoRightNow; + + /// No description provided for @timeagoCompleted. + /// + /// In en, this message translates to: + /// **'completed'** + String get timeagoCompleted; + + /// No description provided for @timeagoInNbSeconds. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{in {count} second} other{in {count} seconds}}'** + String timeagoInNbSeconds(int count); + + /// No description provided for @timeagoInNbMinutes. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{in {count} minute} other{in {count} minutes}}'** + String timeagoInNbMinutes(int count); + + /// No description provided for @timeagoInNbHours. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{in {count} hour} other{in {count} hours}}'** + String timeagoInNbHours(int count); + + /// No description provided for @timeagoInNbDays. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{in {count} day} other{in {count} days}}'** + String timeagoInNbDays(int count); + + /// No description provided for @timeagoInNbWeeks. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{in {count} week} other{in {count} weeks}}'** + String timeagoInNbWeeks(int count); + + /// No description provided for @timeagoInNbMonths. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{in {count} month} other{in {count} months}}'** + String timeagoInNbMonths(int count); + + /// No description provided for @timeagoInNbYears. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{in {count} year} other{in {count} years}}'** + String timeagoInNbYears(int count); + + /// No description provided for @timeagoNbMinutesAgo. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{{count} minute ago} other{{count} minutes ago}}'** + String timeagoNbMinutesAgo(int count); + + /// No description provided for @timeagoNbHoursAgo. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{{count} hour ago} other{{count} hours ago}}'** + String timeagoNbHoursAgo(int count); + + /// No description provided for @timeagoNbDaysAgo. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{{count} day ago} other{{count} days ago}}'** + String timeagoNbDaysAgo(int count); + + /// No description provided for @timeagoNbWeeksAgo. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{{count} week ago} other{{count} weeks ago}}'** + String timeagoNbWeeksAgo(int count); + + /// No description provided for @timeagoNbMonthsAgo. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{{count} month ago} other{{count} months ago}}'** + String timeagoNbMonthsAgo(int count); + + /// No description provided for @timeagoNbYearsAgo. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{{count} year ago} other{{count} years ago}}'** + String timeagoNbYearsAgo(int count); + + /// No description provided for @timeagoNbMinutesRemaining. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{{count} minute remaining} other{{count} minutes remaining}}'** + String timeagoNbMinutesRemaining(int count); + + /// No description provided for @timeagoNbHoursRemaining. + /// + /// In en, this message translates to: + /// **'{count, plural, =1{{count} hour remaining} other{{count} hours remaining}}'** + String timeagoNbHoursRemaining(int count); } class _AppLocalizationsDelegate extends LocalizationsDelegate { diff --git a/lib/l10n/l10n_af.dart b/lib/l10n/l10n_af.dart index 181bce4439..eeadbe5be6 100644 --- a/lib/l10n/l10n_af.dart +++ b/lib/l10n/l10n_af.dart @@ -864,6 +864,9 @@ class AppLocalizationsAf extends AppLocalizations { @override String get preferencesInGameOnly => 'In-game only'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Skaakklok'; @@ -5499,4 +5502,178 @@ class AppLocalizationsAf extends AppLocalizations { ); return '$_temp0'; } + + @override + String get timeagoJustNow => 'sopas'; + + @override + String get timeagoRightNow => 'nou'; + + @override + String get timeagoCompleted => 'voltooi'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count sekondes', + one: 'in $count sekonde', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count minute', + one: 'in $count minuut', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count ure', + one: 'in $count uur', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count dae', + one: 'in $count dag', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count weke', + one: 'in $count week', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count maande', + one: 'in $count maand', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count jare', + one: 'in $count jaar', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minute gelede', + one: '$count minuut gelede', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count ure gelede', + one: '$count uur gelede', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count dae gelede', + one: '$count dag gelede', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count weke gelede', + one: '$count week gelede', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count maande gelede', + one: '$count maand gelede', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count jare gelede', + one: '$count jaar gelede', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'nog $count minute oor', + one: 'nog $count minuut oor', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'nog $count ure oor', + one: 'nog $count uur oor', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_ar.dart b/lib/l10n/l10n_ar.dart index bb47cee37c..63db2cd5a2 100644 --- a/lib/l10n/l10n_ar.dart +++ b/lib/l10n/l10n_ar.dart @@ -936,6 +936,9 @@ class AppLocalizationsAr extends AppLocalizations { @override String get preferencesInGameOnly => 'في اللعبة فقط'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'مؤقت الشطرنج'; @@ -5783,4 +5786,238 @@ class AppLocalizationsAr extends AppLocalizations { ); return '$_temp0'; } + + @override + String get timeagoJustNow => 'الان'; + + @override + String get timeagoRightNow => 'حاليا'; + + @override + String get timeagoCompleted => 'مكتمل'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'خلال $count ثانية', + many: 'خلال $count ثانية', + few: 'خلال $count ثوانٍ', + two: 'خلال ثانيتين', + one: 'خلال ثانية', + zero: 'خلال $count ثانية', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'خلال $count دقائق', + many: 'خلال $count دقائق', + few: 'خلال $count دقائق', + two: 'خلال $count دقيقتين', + one: 'خلال دقيقة', + zero: 'خلال $count دقيقة', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'خلال $count ساعة', + many: 'خلال $count ساعة', + few: 'خلال $count ساعات', + two: 'خلال ساعتين', + one: 'خلال ساعة', + zero: 'خلال $count ساعة', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'خلال $count يوم', + many: 'خلال $count يوم', + few: 'خلال $count أيام', + two: 'خلال $count يوم', + one: 'خلال $count يوم', + zero: 'خلال $count يوم', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'خلال $count اسبوع', + many: 'خلال $count اسبوع', + few: 'خلال $count اسبوع', + two: 'خلال $count اسبوع', + one: 'خلال $count اسبوع', + zero: 'خلال $count أسبوع', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'خلال $count شهر', + many: 'خلال $count شهر', + few: 'خلال $count شهر', + two: 'خلال $count شهر', + one: 'خلال $count شهر', + zero: 'خلال $count شهر', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'خلال $count سنة', + many: 'خلال $count سنة', + few: 'خلال $count سنة', + two: 'خلال $count سنة', + one: 'خلال $count سنة', + zero: 'خلال $count سنة', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'منذ $count دقيقة/دقائق مضت', + many: 'منذ $count دقيقة/دقائق مضت', + few: 'منذ $count دقيقة/دقائق مضت', + two: 'منذ $count دقيقتين', + one: 'منذ $count دقيقة', + zero: 'منذ $count دقيقة/دقائق مضت', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'منذ $count ساعة/ساعات مضت', + many: 'منذ $count ساعة/ساعات مضت', + few: 'منذ $count ساعة/ساعات مضت', + two: 'منذ $count ساعة/ساعات مضت', + one: 'منذ $count ساعة', + zero: 'منذ $count ساعة/ساعات مضت', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'منذ $count يوم/أيام', + many: 'منذ $count يوم/أيام', + few: 'منذ $count يوم/أيام', + two: 'منذ $count يوم/أيام', + one: 'منذ $count يوم', + zero: 'منذ $count يوم/أيام', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'منذ $count أسابيع', + many: 'منذ $count أسابيع', + few: 'منذ $count أسابيع', + two: 'منذ $count أسابيع', + one: 'منذ $count أسابيع', + zero: 'منذ $count أسابيع', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'منذ $count شهر', + many: 'منذ $count شهر', + few: 'منذ $count شهر', + two: 'منذ $count شهر', + one: 'منذ $count شهر', + zero: 'منذ $count شهر', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'منذ $count سنة', + many: 'منذ $count سنة', + few: 'منذ $count سنة', + two: 'منذ $count سنة', + one: 'منذ $count سنة', + zero: 'منذ $count سنة', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$countدقائق متبقية', + many: '$countدقيقة متبقية', + few: '$countدقائق متبقية', + two: '$countدقيقتان متبقيتان', + one: '$countدقيقة متبقية', + zero: '$countدقيقة متبقية', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$countساعة متبقية', + many: '$countساعة متبقية', + few: '$count ساعات متبقية', + two: '$countساعتان متبقيتان', + one: '$countساعة واحدة متبقية', + zero: '$countساعة متبقية', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_az.dart b/lib/l10n/l10n_az.dart index ae1f41229e..60b6f82133 100644 --- a/lib/l10n/l10n_az.dart +++ b/lib/l10n/l10n_az.dart @@ -864,6 +864,9 @@ class AppLocalizationsAz extends AppLocalizations { @override String get preferencesInGameOnly => 'In-game only'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Şahmat saatı'; @@ -5497,4 +5500,178 @@ class AppLocalizationsAz extends AppLocalizations { ); return '$_temp0'; } + + @override + String get timeagoJustNow => 'elə indi'; + + @override + String get timeagoRightNow => 'indicə'; + + @override + String get timeagoCompleted => 'completed'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count seconds', + one: 'in $count second', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count minutes', + one: 'in $count minute', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count hours', + one: 'in $count hour', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count days', + one: 'in $count day', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count weeks', + one: 'in $count week', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count months', + one: 'in $count month', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count years', + one: 'in $count year', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutes ago', + one: '$count minute ago', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hours ago', + one: '$count hour ago', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count days ago', + one: '$count day ago', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count weeks ago', + one: '$count week ago', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count months ago', + one: '$count month ago', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count years ago', + one: '$count year ago', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutes remaining', + one: '$count minute remaining', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hours remaining', + one: '$count hour remaining', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_be.dart b/lib/l10n/l10n_be.dart index 8acbd28514..81181c25f2 100644 --- a/lib/l10n/l10n_be.dart +++ b/lib/l10n/l10n_be.dart @@ -898,6 +898,9 @@ class AppLocalizationsBe extends AppLocalizations { @override String get preferencesInGameOnly => 'Выключна ў партыі'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Шахматны гадзіннік'; @@ -5635,4 +5638,208 @@ class AppLocalizationsBe extends AppLocalizations { ); return '$_temp0'; } + + @override + String get timeagoJustNow => 'зараз'; + + @override + String get timeagoRightNow => 'прама зараз'; + + @override + String get timeagoCompleted => 'завершана'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'праз $count секунд', + many: 'праз $count секунд', + few: 'праз $count секунды', + one: 'праз $count секунду', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'праз $count хвілін', + many: 'праз $count хвілін', + few: 'праз $count хвіліны', + one: 'праз $count хвіліну', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'праз $count гадзін', + many: 'праз $count гадзін', + few: 'праз $count гадзіны', + one: 'праз $count гадзіну', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'праз $count дзён', + many: 'праз $count дзён', + few: 'праз $count дні', + one: 'праз $count дзень', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'праз $count тыдняў', + many: 'праз $count тыдняў', + few: 'праз $count тыдні', + one: 'праз $count тыдзень', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'праз $count месяцаў', + many: 'праз $count месяцаў', + few: 'праз $count месяцы', + one: 'праз $count месяц', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'праз $count гадоў', + many: 'праз $count гадоў', + few: 'праз $count гады', + one: 'праз $count год', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count хвілін таму', + many: '$count хвілін таму', + few: '$count хвіліны таму', + one: '$count хвіліну таму', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count гадзін таму', + many: '$count гадзін таму', + few: '$count гадзіны таму', + one: '$count гадзіну таму', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count дзён таму', + many: '$count дзён таму', + few: '$count дні таму', + one: '$count дзень таму', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count тыдняў таму', + many: '$count тыдняў таму', + few: '$count тыдні таму', + one: '$count тыдзень таму', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count месяцаў таму', + many: '$count месяцаў таму', + few: '$count месяцы таму', + one: '$count месяц таму', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count гадоў таму', + many: '$count гадоў таму', + few: '$count гады таму', + one: '$count год таму', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Засталося $count хвіліны', + many: 'Засталося $count хвілін', + few: 'Засталося $count хвіліны', + one: 'Засталася $count хвіліна', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Засталося $count гадзіны', + many: 'Засталося $count гадзін', + few: 'Засталося $count гадзіны', + one: 'Засталася $count гадзіна', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_bg.dart b/lib/l10n/l10n_bg.dart index cae0aa16dc..7a35020880 100644 --- a/lib/l10n/l10n_bg.dart +++ b/lib/l10n/l10n_bg.dart @@ -864,6 +864,9 @@ class AppLocalizationsBg extends AppLocalizations { @override String get preferencesInGameOnly => 'Само по време на игра'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Шахматен часовник'; @@ -5499,4 +5502,178 @@ class AppLocalizationsBg extends AppLocalizations { ); return '$_temp0'; } + + @override + String get timeagoJustNow => 'току що'; + + @override + String get timeagoRightNow => 'точно сега'; + + @override + String get timeagoCompleted => 'завършено'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'след $count секунди', + one: 'след $count секунда', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'след $count минути', + one: 'след $count минута', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'след $count часа', + one: 'след $count час', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'след $count дни', + one: 'след $count ден', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'след $count седмици', + one: 'след $count седмица', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'след $count месеца', + one: 'след $count месец', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'след $count години', + one: 'след $count година', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'преди $count минути', + one: 'преди $count минута', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Преди $count часа', + one: 'преди $count час', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Преди $count дни', + one: 'преди $count ден', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'преди $count седмици', + one: 'преди $count седмица', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'преди $count месеца', + one: 'преди $count месец', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'преди $count години', + one: 'преди $count година', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'остават $count минути', + one: 'остава $count минутa', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'остават $count часа', + one: 'остава $count час', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_bn.dart b/lib/l10n/l10n_bn.dart index a0f887efa9..70def05f6b 100644 --- a/lib/l10n/l10n_bn.dart +++ b/lib/l10n/l10n_bn.dart @@ -864,6 +864,9 @@ class AppLocalizationsBn extends AppLocalizations { @override String get preferencesInGameOnly => 'In-game only'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'দাবার ঘড়ি'; @@ -5499,4 +5502,178 @@ class AppLocalizationsBn extends AppLocalizations { ); return '$_temp0'; } + + @override + String get timeagoJustNow => 'এখনই'; + + @override + String get timeagoRightNow => 'এই মুহূর্তে'; + + @override + String get timeagoCompleted => 'সম্পন্ন হয়েছে'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count সেকেন্ডের মধ্যে', + one: '$count সেকেন্ডের মধ্যে', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count মিনিটের মধ্যে', + one: '$count মিনিটের মধ্যে', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count ঘন্টার মধ্যে', + one: '$count ঘন্টার মধ্যে', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count দিনের মধ্যে', + one: '$count দিনের মধ্যে', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count সপ্তাহের মধ্যে', + one: '$count সপ্তাহের মধ্যে', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count মাসের মধ্যে', + one: '$count মাসের মধ্যে', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count বছরের মধ্যে', + one: '$count বছরের মধ্যে', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count মিনিট আগে', + one: '$count মিনিট আগে', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count ঘন্টা আগে', + one: '$count ঘণ্টা আগে', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count দিন আগে', + one: '$count দিন আগে', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count সপ্তাহ আগে', + one: '$count সপ্তাহ আগে', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count মাস আগে', + one: '$count মাস আগে', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count বছর আগে', + one: '$count বছর আগে', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count মিনিট বাকি', + one: '$count মিনিট বাকি', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count ঘন্টা বাকি', + one: '$count ঘন্টা বাকি', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_br.dart b/lib/l10n/l10n_br.dart index 64bce249d3..ae5d633667 100644 --- a/lib/l10n/l10n_br.dart +++ b/lib/l10n/l10n_br.dart @@ -915,6 +915,9 @@ class AppLocalizationsBr extends AppLocalizations { @override String get preferencesInGameOnly => 'In-game only'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Horolaj echedoù'; @@ -5689,4 +5692,217 @@ class AppLocalizationsBr extends AppLocalizations { ); return '$_temp0'; } + + @override + String get timeagoJustNow => 'bremañ'; + + @override + String get timeagoRightNow => 'bremañ'; + + @override + String get timeagoCompleted => 'completed'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'a-benn $count eilenn', + many: 'a-benn $count eilenn', + few: 'a-benn $count eilenn', + two: 'a-benn $count eilenn', + one: 'a-benn $count eilenn', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'a-benn $count munutenn', + many: 'a-benn $count munutenn', + few: 'a-benn $count munutenn', + two: 'a-benn $count vunutenn', + one: 'a-benn $count vunutenn', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'a-benn $count eur', + many: 'a-benn $count eur', + few: 'a-benn $count eur', + two: 'a-benn $count eur', + one: 'a-benn $count eur', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'a-benn $count deiz', + many: 'a-benn $count deiz', + few: 'a-benn $count deiz', + two: 'a-benn $count zeiz', + one: 'a-benn $count deiz', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'a-benn $count sizhun', + many: 'a-benn $count sizhun', + few: 'a-benn $count sizhun', + two: 'a-benn $count sizhun', + one: 'a-benn $count sizhun', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'a-benn $count miz', + many: 'a-benn $count miz', + few: 'a-benn $count miz', + two: 'a-benn $count viz', + one: 'a-benn $count miz', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'a-benn $count bloaz', + many: 'a-benn $count bloaz', + few: 'a-benn $count bloaz', + two: 'a-benn $count vloaz', + one: 'a-benn $count bloaz', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count munutenn zo', + many: '$count munutenn zo', + few: '$count munutenn zo', + two: '$count vunutenn zo', + one: '$count vunutenn zo', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count eur zo', + many: '$count eur zo', + few: '$count eur zo', + two: '$count eur zo', + one: '$count eur zo', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count deiz zo', + many: '$count deiz zo', + few: '$count deiz zo', + two: '$count zeiz zo', + one: '$count deiz zo', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count sizhun zo', + many: '$count sizhun zo', + few: '$count sizhun zo', + two: '$count sizhun zo', + one: '$count sizhun zo', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count miz zo', + many: '$count miz zo', + few: '$count miz zo', + two: '$count viz zo', + one: '$count miz zo', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count bloaz zo', + many: '$count bloaz zo', + few: '$count bloaz zo', + two: '$count vloaz zo', + one: '$count bloaz zo', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutes remaining', + one: '$count minute remaining', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hours remaining', + one: '$count hour remaining', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_bs.dart b/lib/l10n/l10n_bs.dart index 9bd31aa929..e6fe373180 100644 --- a/lib/l10n/l10n_bs.dart +++ b/lib/l10n/l10n_bs.dart @@ -882,6 +882,9 @@ class AppLocalizationsBs extends AppLocalizations { @override String get preferencesInGameOnly => 'Samo unutar igre'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Sat'; @@ -5570,4 +5573,191 @@ class AppLocalizationsBs extends AppLocalizations { ); return '$_temp0'; } + + @override + String get timeagoJustNow => 'upravo sada'; + + @override + String get timeagoRightNow => 'upravo sada'; + + @override + String get timeagoCompleted => 'completed'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'za $count sekundi', + few: 'za $count sekunde', + one: 'za $count sekundu', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'za $count minuta', + few: 'za $count minute', + one: 'za $count minutu', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'za $count sati', + few: 'za $count sata', + one: 'za $count sat', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'za $count dana', + few: 'za $count dana', + one: 'za $count dan', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'za $count sedmica', + few: 'za $count sedmice', + one: 'za $count sedmicu', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'za $count mjeseci', + few: 'za $count mjeseca', + one: 'za $count mjesec', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'za $count godina', + few: 'za $count godine', + one: 'za $count godinu', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'prije $count minuta', + few: 'prije $count minute', + one: 'prije $count minutu', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'prije $count sati', + few: 'prije $count sata', + one: 'prije $count sat', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'prije $count dana', + few: 'prije $count dana', + one: 'prije $count dan', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'prije $count sedmica', + few: 'prije $count sedmice', + one: 'prije $count sedmicu', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'prije $count mjeseci', + few: 'prije $count mjeseca', + one: 'prije $count mjesec', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'prije $count godina', + few: 'prije $count godine', + one: 'prije $count godinu', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutes remaining', + one: '$count minute remaining', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hours remaining', + one: '$count hour remaining', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_ca.dart b/lib/l10n/l10n_ca.dart index 0166ab1f8d..8f6589e01b 100644 --- a/lib/l10n/l10n_ca.dart +++ b/lib/l10n/l10n_ca.dart @@ -864,6 +864,9 @@ class AppLocalizationsCa extends AppLocalizations { @override String get preferencesInGameOnly => 'Només durant la partida'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Rellotge d\'escacs'; @@ -5499,4 +5502,178 @@ class AppLocalizationsCa extends AppLocalizations { ); return '$_temp0'; } + + @override + String get timeagoJustNow => 'ara mateix'; + + @override + String get timeagoRightNow => 'ara mateix'; + + @override + String get timeagoCompleted => 'completat'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'en $count segons', + one: 'en $count segon', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'en $count minuts', + one: 'en $count minut', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'en $count hores', + one: 'en $count hora', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'en $count dies', + one: 'en $count dia', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'en $count setmanes', + one: 'en $count setmana', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'en $count mesos', + one: 'en $count mes', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'en $count anys', + one: 'en $count any', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'fa $count minuts', + one: 'fa $count minut', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'fa $count hores', + one: 'fa $count hora', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'fa $count dies', + one: 'fa $count dia', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'fa $count setmanes', + one: 'fa $count setmana', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'fa $count mesos', + one: 'fa $count mes', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'fa $count anys', + one: 'fa $count any', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Queden $count minuts', + one: 'Queda $count minut', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Queden $count hores', + one: 'Queda $count hora', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_cs.dart b/lib/l10n/l10n_cs.dart index b555e7e8d2..2ea7f09df6 100644 --- a/lib/l10n/l10n_cs.dart +++ b/lib/l10n/l10n_cs.dart @@ -902,6 +902,9 @@ class AppLocalizationsCs extends AppLocalizations { @override String get preferencesInGameOnly => 'Pouze u partie'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Šachové hodiny'; @@ -5643,4 +5646,208 @@ class AppLocalizationsCs extends AppLocalizations { ); return '$_temp0'; } + + @override + String get timeagoJustNow => 'právě teď'; + + @override + String get timeagoRightNow => 'právě teď'; + + @override + String get timeagoCompleted => 'dokončeno'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'za $count sekund', + many: 'za $count sekund', + few: 'za $count sekundy', + one: 'za $count sekundu', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'za $count minut', + many: 'za $count minut', + few: 'za $count minuty', + one: 'za $count minutu', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'za $count hodin', + many: 'za $count hodin', + few: 'za $count hodiny', + one: 'za $count hodinu', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'za $count dnů', + many: 'za $count dnů', + few: 'za $count dny', + one: 'za $count den', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'za $count týdnů', + many: 'za $count týdnů', + few: 'za $count týdny', + one: 'za $count týden', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'za $count měsíců', + many: 'za $count měsíců', + few: 'za $count měsíce', + one: 'za $count měsíc', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'za $count let', + many: 'za $count let', + few: 'za $count roky', + one: 'za $count rok', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'před $count minutami', + many: 'před $count minutami', + few: 'před $count minutami', + one: 'před $count minutou', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'před $count hodinami', + many: 'před $count hodinami', + few: 'před $count hodinami', + one: 'před $count hodinou', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'před $count dny', + many: 'před $count dny', + few: 'před $count dny', + one: 'před $count dnem', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'před $count týdny', + many: 'před $count týdny', + few: 'před $count týdny', + one: 'před $count týdnem', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'před $count měsíci', + many: 'před $count měsíci', + few: 'před $count měsíci', + one: 'před $count měsícem', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'před $count lety', + many: 'před $count lety', + few: 'před $count lety', + one: 'před $count rokem', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Zbývá $count minut', + many: 'Zbývá $count minut', + few: 'Zbývají $count minuty', + one: 'Zbývá $count minuta', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Zbývá $count hodin', + many: 'Zbývá $count hodin', + few: 'Zbývají $count hodiny', + one: 'Zbývá $count hodina', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_da.dart b/lib/l10n/l10n_da.dart index 3f1a2f13dc..4c1466b1f9 100644 --- a/lib/l10n/l10n_da.dart +++ b/lib/l10n/l10n_da.dart @@ -864,6 +864,9 @@ class AppLocalizationsDa extends AppLocalizations { @override String get preferencesInGameOnly => 'Kun i spillet'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Skakur'; @@ -5499,4 +5502,178 @@ class AppLocalizationsDa extends AppLocalizations { ); return '$_temp0'; } + + @override + String get timeagoJustNow => 'for lidt siden'; + + @override + String get timeagoRightNow => 'netop nu'; + + @override + String get timeagoCompleted => 'afsluttet'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'om $count sekunder', + one: 'om $count sekund', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'om $count minutter', + one: 'om $count minut', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'om $count timer', + one: 'om $count time', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'om $count dage', + one: 'om $count dag', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'om $count uger', + one: 'om $count uge', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'om $count måneder', + one: 'om $count måned', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'om $count år', + one: 'om $count år', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutter siden', + one: '$count minut siden', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count timer siden', + one: '$count time siden', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count dage siden', + one: '$count dag siden', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count uger siden', + one: '$count uge siden', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count måneder siden', + one: '$count måned siden', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count år siden', + one: '$count år siden', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutter tilbage', + one: '$count minut tilbage', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count timer tilbage', + one: '$count time tilbage', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_de.dart b/lib/l10n/l10n_de.dart index e1749b95cf..660308d945 100644 --- a/lib/l10n/l10n_de.dart +++ b/lib/l10n/l10n_de.dart @@ -864,6 +864,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get preferencesInGameOnly => 'Nur während einer Partie'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Schachuhr'; @@ -5499,4 +5502,178 @@ class AppLocalizationsDe extends AppLocalizations { ); return '$_temp0'; } + + @override + String get timeagoJustNow => 'gerade eben'; + + @override + String get timeagoRightNow => 'gerade jetzt'; + + @override + String get timeagoCompleted => 'abgeschlossen'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count Sekunden', + one: 'in $count Sekunde', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count Minuten', + one: 'in $count Minute', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count Stunden', + one: 'in $count Stunde', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count Tagen', + one: 'in $count Tag', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count Wochen', + one: 'in $count Woche', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count Monaten', + one: 'in $count Monat', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count Jahren', + one: 'in $count Jahr', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'vor $count Minuten', + one: 'vor $count Minute', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'vor $count Stunden', + one: 'vor $count Stunde', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'vor $count Tagen', + one: 'vor $count Tag', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'vor $count Wochen', + one: 'vor $count Woche', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'vor $count Monaten', + one: 'vor $count Monat', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'vor $count Jahren', + one: 'vor $count Jahr', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Minuten verbleibend', + one: '$count Minute verbleibend', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Stunden verbleiben', + one: '$count Stunde verbleiben', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_el.dart b/lib/l10n/l10n_el.dart index addade1faa..88e3cf9bde 100644 --- a/lib/l10n/l10n_el.dart +++ b/lib/l10n/l10n_el.dart @@ -864,6 +864,9 @@ class AppLocalizationsEl extends AppLocalizations { @override String get preferencesInGameOnly => 'Μόνο κατά τη διάρκεια του παιχνιδιού'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Σκακιστικό χρονόμετρο'; @@ -5499,4 +5502,178 @@ class AppLocalizationsEl extends AppLocalizations { ); return '$_temp0'; } + + @override + String get timeagoJustNow => 'μόλις τώρα'; + + @override + String get timeagoRightNow => 'αυτή τη στιγμή'; + + @override + String get timeagoCompleted => 'ολοκληρώθηκε'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'σε $count δευτερόλεπτα', + one: 'σε $count δευτερόλεπτο', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'σε $count λεπτά', + one: 'σε $count λεπτό', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'σε $count ώρες', + one: 'σε $count ώρα', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'σε $count ημέρες', + one: 'σε $count ημέρα', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'σε $count εβδομάδες', + one: 'σε $count εβδομάδα', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'σε $count μήνες', + one: 'σε $count μήνα', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count έτη', + one: 'σε $count έτος', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count λεπτά πριν', + one: '$count λεπτό πριν', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count ώρες πριν', + one: '$count ώρα πριν', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count ημέρες πριν', + one: '$count μέρα πριν', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count εβδομάδες πριν', + one: '$count εβδομάδα πριν', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count μήνες πριν', + one: '$count μήνα πριν', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count χρόνια πριν', + one: '$count χρόνο πριν', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'απομένουν $count λεπτά', + one: 'απομένει $count λεπτό', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'απομένουν $count ώρες', + one: 'απομένει $count ώρα', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_en.dart b/lib/l10n/l10n_en.dart index 3660156fb1..8686a89b3c 100644 --- a/lib/l10n/l10n_en.dart +++ b/lib/l10n/l10n_en.dart @@ -864,6 +864,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get preferencesInGameOnly => 'In-game only'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Chess clock'; @@ -5497,6 +5500,180 @@ class AppLocalizationsEn extends AppLocalizations { ); return '$_temp0'; } + + @override + String get timeagoJustNow => 'just now'; + + @override + String get timeagoRightNow => 'right now'; + + @override + String get timeagoCompleted => 'completed'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count seconds', + one: 'in $count second', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count minutes', + one: 'in $count minute', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count hours', + one: 'in $count hour', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count days', + one: 'in $count day', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count weeks', + one: 'in $count week', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count months', + one: 'in $count month', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count years', + one: 'in $count year', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutes ago', + one: '$count minute ago', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hours ago', + one: '$count hour ago', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count days ago', + one: '$count day ago', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count weeks ago', + one: '$count week ago', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count months ago', + one: '$count month ago', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count years ago', + one: '$count year ago', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutes remaining', + one: '$count minute remaining', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hours remaining', + one: '$count hour remaining', + ); + return '$_temp0'; + } } /// The translations for English, as used in the United States (`en_US`). @@ -10994,4 +11171,178 @@ class AppLocalizationsEnUs extends AppLocalizationsEn { ); return '$_temp0'; } + + @override + String get timeagoJustNow => 'just now'; + + @override + String get timeagoRightNow => 'right now'; + + @override + String get timeagoCompleted => 'completed'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count seconds', + one: 'in $count second', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count minutes', + one: 'in $count minute', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count hours', + one: 'in $count hour', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count days', + one: 'in $count day', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count weeks', + one: 'in $count week', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count months', + one: 'in $count month', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count years', + one: 'in $count year', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutes ago', + one: '$count minute ago', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hours ago', + one: '$count hour ago', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count days ago', + one: '$count day ago', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count weeks ago', + one: '$count week ago', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count months ago', + one: '$count month ago', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count years ago', + one: '$count year ago', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutes remaining', + one: '$count minute remaining', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hours remaining', + one: '$count hour remaining', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_eo.dart b/lib/l10n/l10n_eo.dart index 4dd1f7f82f..fcb9e4cc78 100644 --- a/lib/l10n/l10n_eo.dart +++ b/lib/l10n/l10n_eo.dart @@ -864,6 +864,9 @@ class AppLocalizationsEo extends AppLocalizations { @override String get preferencesInGameOnly => 'Nur en ludo'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Ŝakhorloĝo'; @@ -5499,4 +5502,178 @@ class AppLocalizationsEo extends AppLocalizations { ); return '$_temp0'; } + + @override + String get timeagoJustNow => 'ĵus nun'; + + @override + String get timeagoRightNow => 'ĵuse'; + + @override + String get timeagoCompleted => 'finita'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'en $count sekundoj', + one: 'en $count sekundo', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'en $count minutoj', + one: 'en $count minuto', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'en $count horoj', + one: 'en $count horo', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'en $count tagoj', + one: 'en $count tago', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'en $count semajnoj', + one: 'en $count semajno', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'en $count monatoj', + one: 'en $count monato', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'en $count jaroj', + one: 'en $count jaro', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'antaŭ $count minutoj', + one: 'antaŭ $count minuto', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'antaŭ $count horoj', + one: 'antaŭ $count horo', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'antaŭ $count tagoj', + one: 'antaŭ $count tago', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'antaŭ $count semajnoj', + one: 'antaŭ $count semajno', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'antaŭ $count monatoj', + one: 'antaŭ $count monato', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'antaŭ $count jaroj', + one: 'antaŭ $count jaro', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutoj restas', + one: '$count minuto restas', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count horoj restas', + one: '$count horo restas', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_es.dart b/lib/l10n/l10n_es.dart index 7bef9b9f89..e8dd5f79ea 100644 --- a/lib/l10n/l10n_es.dart +++ b/lib/l10n/l10n_es.dart @@ -864,6 +864,9 @@ class AppLocalizationsEs extends AppLocalizations { @override String get preferencesInGameOnly => 'Solo durante la partida'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Reloj de ajedrez'; @@ -5499,4 +5502,178 @@ class AppLocalizationsEs extends AppLocalizations { ); return '$_temp0'; } + + @override + String get timeagoJustNow => 'ahora mismo'; + + @override + String get timeagoRightNow => 'ahora mismo'; + + @override + String get timeagoCompleted => 'completado'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'en $count segundos', + one: 'en $count segundo', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'en $count minutos', + one: 'en $count minuto', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'en $count horas', + one: 'en $count hora', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'en $count días', + one: 'en $count día', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'en $count semanas', + one: 'en $count semana', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'en $count meses', + one: 'en $count mes', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'en $count años', + one: 'en $count año', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'hace $count minutos', + one: 'hace $count minuto', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'hace $count horas', + one: 'hace $count hora', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'hace $count días', + one: 'hace $count día', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'hace $count semanas', + one: 'hace $count semana', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'hace $count meses', + one: 'hace $count mes', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'hace $count años', + one: 'hace $count año', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutos restantes', + one: '$count minutos restantes', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count horas restantes', + one: '$count horas restantes', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_et.dart b/lib/l10n/l10n_et.dart index 71beea3739..99c21d4fdd 100644 --- a/lib/l10n/l10n_et.dart +++ b/lib/l10n/l10n_et.dart @@ -864,6 +864,9 @@ class AppLocalizationsEt extends AppLocalizations { @override String get preferencesInGameOnly => 'In-game only'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Malekell'; @@ -5499,4 +5502,178 @@ class AppLocalizationsEt extends AppLocalizations { ); return '$_temp0'; } + + @override + String get timeagoJustNow => 'äsja'; + + @override + String get timeagoRightNow => 'praegu'; + + @override + String get timeagoCompleted => 'lõppenud'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count sekundi pärast', + one: '$count sekundi pärast', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minuti pärast', + one: '$count minuti pärast', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tunni pärast', + one: '$count tunni pärast', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count päeva pärast', + one: '$count päeva pärast', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count nädala pärast', + one: '$count nädala pärast', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count kuu pärast', + one: '$count kuu pärast', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count aasta pärast', + one: '$count aasta pärast', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutit tagasi', + one: '$count minut tagasi', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tundi tagasi', + one: '$count tund tagasi', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count päeva tagasi', + one: '$count päev tagasi', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count nädalat tagasi', + one: '$count nädal tagasi', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count kuud tagasi', + one: '$count kuu tagasi', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count aastat tagasi', + one: '$count aasta tagasi', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutit jäänud', + one: '$count minut jäänud', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tundi jäänud', + one: '$count tund jäänud', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_eu.dart b/lib/l10n/l10n_eu.dart index 9b89889e46..5e05fe1573 100644 --- a/lib/l10n/l10n_eu.dart +++ b/lib/l10n/l10n_eu.dart @@ -864,6 +864,9 @@ class AppLocalizationsEu extends AppLocalizations { @override String get preferencesInGameOnly => 'Partidan zehar bakarrik'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Xake-erlojua'; @@ -5499,4 +5502,178 @@ class AppLocalizationsEu extends AppLocalizations { ); return '$_temp0'; } + + @override + String get timeagoJustNow => 'orain'; + + @override + String get timeagoRightNow => 'orain'; + + @override + String get timeagoCompleted => 'amaituta'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count segundotan', + one: 'segundo ${count}en', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minututan', + one: 'minutu ${count}en', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count ordutan', + one: 'ordu ${count}en', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count egunetan', + one: 'egun ${count}en', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count egunetan', + one: 'aste ${count}en', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hilabetetan', + one: 'hilabete ${count}en', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count urtetan', + one: 'urte ${count}en', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'orain dela $count minutu', + one: 'orain dela minutu $count', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'orain dela $count ordu', + one: 'orain dela ordu $count', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'orain dela $count egun', + one: 'orain dela egun $count', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'orain dela $count aste', + one: 'orain dela aste $count', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'orain dela $count hilabete', + one: 'orain dela hilabete $count', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'orain dela $count urte', + one: 'orain dela urte $count', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutu falta dira', + one: 'Minutu $count falta da', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count ordu falta dira', + one: 'Ordu $count falta da', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_fa.dart b/lib/l10n/l10n_fa.dart index f0da4ed520..585153cf80 100644 --- a/lib/l10n/l10n_fa.dart +++ b/lib/l10n/l10n_fa.dart @@ -864,6 +864,9 @@ class AppLocalizationsFa extends AppLocalizations { @override String get preferencesInGameOnly => 'تنها در بازی'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'ساعت شطرنج'; @@ -5499,4 +5502,178 @@ class AppLocalizationsFa extends AppLocalizations { ); return '$_temp0'; } + + @override + String get timeagoJustNow => 'چند لحظه پیش'; + + @override + String get timeagoRightNow => 'هم‌اکنون'; + + @override + String get timeagoCompleted => 'کامل شده'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'تا $count ثانیهٔ دیگر', + one: 'تا $count ثانیهٔ دیگر', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'تا $count دقیقه دیگر', + one: 'تا $count دقیقه دیگر', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'تا $count ساعت دیگر', + one: 'تا $count ساعت دیگر', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'تا $count روز دیگر', + one: 'تا $count روز دیگر', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'تا $count هفته دیگر', + one: 'تا $count هفته دیگر', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'تا $count ماه دیگر', + one: 'تا $count ماه دیگر', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'تا $count سال دیگر', + one: 'تا $count سال دیگر', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count دقیقه پیش', + one: '$count دقیقه پیش', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count ساعت پیش', + one: '$count ساعت پیش', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count روز پیش', + one: '$count روز پیش', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count هفته پیش', + one: '$count هفته پیش', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count ماه پیش', + one: '$count ماه پیش', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count سال پیش', + one: '$count سال پیش', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count دقیقه باقی مانده', + one: '$count دقیقه باقی مانده', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count ساعت باقی مانده', + one: '$count ساعت باقی مانده', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_fi.dart b/lib/l10n/l10n_fi.dart index 39e99a72f0..76807dd677 100644 --- a/lib/l10n/l10n_fi.dart +++ b/lib/l10n/l10n_fi.dart @@ -864,6 +864,9 @@ class AppLocalizationsFi extends AppLocalizations { @override String get preferencesInGameOnly => 'Vain pelin aikana'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Shakkikello'; @@ -5499,4 +5502,178 @@ class AppLocalizationsFi extends AppLocalizations { ); return '$_temp0'; } + + @override + String get timeagoJustNow => 'juuri äsken'; + + @override + String get timeagoRightNow => 'juuri nyt'; + + @override + String get timeagoCompleted => 'suoritettu'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count sekunnin kuluttua', + one: '$count sekunnin kuluttua', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minuutin kuluttua', + one: '$count minuutin kuluttua', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tunnin kuluttua', + one: '$count tunnin kuluttua', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count päivän kuluttua', + one: '$count päivän kuluttua', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count viikon kuluttua', + one: '$count viikon kuluttua', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count kuukauden kuluttua', + one: '$count kuukauden kuluttua', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count vuoden kuluttua', + one: '$count vuoden kuluttua', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minuuttia sitten', + one: '$count minuutti sitten', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tuntia sitten', + one: '$count tunti sitten', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count päivää sitten', + one: '$count päivä sitten', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count viikkoa sitten', + one: '$count viikko sitten', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count kuukautta sitten', + one: '$count kuukausi sitten', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count vuotta sitten', + one: '$count vuosi sitten', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minuuttia jäljellä', + one: '$count minuutti jäljellä', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tuntia jäljellä', + one: '$count tunti jäljellä', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_fo.dart b/lib/l10n/l10n_fo.dart index 5f9e35c397..fea75bf8bf 100644 --- a/lib/l10n/l10n_fo.dart +++ b/lib/l10n/l10n_fo.dart @@ -864,6 +864,9 @@ class AppLocalizationsFo extends AppLocalizations { @override String get preferencesInGameOnly => 'In-game only'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Talvklokka'; @@ -5497,4 +5500,178 @@ class AppLocalizationsFo extends AppLocalizations { ); return '$_temp0'; } + + @override + String get timeagoJustNow => 'just now'; + + @override + String get timeagoRightNow => 'right now'; + + @override + String get timeagoCompleted => 'completed'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count seconds', + one: 'in $count second', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count minutes', + one: 'in $count minute', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count hours', + one: 'in $count hour', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count days', + one: 'in $count day', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count weeks', + one: 'in $count week', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count months', + one: 'in $count month', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count years', + one: 'in $count year', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutes ago', + one: '$count minute ago', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hours ago', + one: '$count hour ago', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count days ago', + one: '$count day ago', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count weeks ago', + one: '$count week ago', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count months ago', + one: '$count month ago', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count years ago', + one: '$count year ago', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutes remaining', + one: '$count minute remaining', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hours remaining', + one: '$count hour remaining', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_fr.dart b/lib/l10n/l10n_fr.dart index 11f5d9b755..5f495100be 100644 --- a/lib/l10n/l10n_fr.dart +++ b/lib/l10n/l10n_fr.dart @@ -864,6 +864,9 @@ class AppLocalizationsFr extends AppLocalizations { @override String get preferencesInGameOnly => 'Seulement durant la partie'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Pendule'; @@ -5499,4 +5502,178 @@ class AppLocalizationsFr extends AppLocalizations { ); return '$_temp0'; } + + @override + String get timeagoJustNow => 'Maintenant'; + + @override + String get timeagoRightNow => 'à l\'instant'; + + @override + String get timeagoCompleted => 'terminé'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'dans $count secondes', + one: 'dans $count seconde', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'dans $count minutes', + one: 'dans $count minute', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'dans $count heures', + one: 'dans $count heure', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'dans $count jours', + one: 'dans $count jour', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'dans $count semaines', + one: 'dans $count semaine', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'dans $count mois', + one: 'dans $count mois', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'dans $count ans', + one: 'dans $count an', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'il y a $count minutes', + one: 'il y a $count minute', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'il y a $count heures', + one: 'il y a $count heure', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'il y a $count jours', + one: 'il y a $count jour', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'il y a $count semaines', + one: 'il y a $count semaine', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'il y a $count mois', + one: 'il y a $count mois', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'il y a $count ans', + one: 'il y a $count an', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutes restantes', + one: '$count minute restante', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count heures restantes', + one: '$count heure restante', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_ga.dart b/lib/l10n/l10n_ga.dart index 0ded134343..a00459924c 100644 --- a/lib/l10n/l10n_ga.dart +++ b/lib/l10n/l10n_ga.dart @@ -915,6 +915,9 @@ class AppLocalizationsGa extends AppLocalizations { @override String get preferencesInGameOnly => 'In-game only'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Clog fichille'; @@ -5703,4 +5706,217 @@ class AppLocalizationsGa extends AppLocalizations { ); return '$_temp0'; } + + @override + String get timeagoJustNow => 'díreach anois'; + + @override + String get timeagoRightNow => 'anois'; + + @override + String get timeagoCompleted => 'completed'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'i $count soicind', + many: 'i $count soicind', + few: 'i $count soicind', + two: 'i $count soicind', + one: 'i soicind amháín', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'i $count nóiméad', + many: 'i $count nóiméad', + few: 'i $count nóiméad', + two: 'i $count nóiméad', + one: 'i nóiméad amháin', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'i $count uair', + many: 'i $count uair', + few: 'i $count uair', + two: 'i $count uair', + one: 'in uair amháin', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'i $count lá', + many: 'i $count lá', + few: 'i $count lá', + two: 'i $count lá', + one: 'i lá amháín', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'i $count seachtain', + many: 'i $count seachtain', + few: 'i $count seachtain', + two: 'i $count sheachtain', + one: 'i seachtain amháin', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'i $count mí', + many: 'i $count mí', + few: 'i $count mhí', + two: 'i $count mhí', + one: 'i gceann míosa', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'i gceann $count bliain', + many: 'i gceann $count mbliain', + few: 'i gceann $count bhliain', + two: 'i gceann $count bhliain', + one: 'i gceann bliana', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count nóiméad ó shin', + many: '$count nóiméad ó shin', + few: '$count nóiméad ó shin', + two: '$count nóiméad ó shin', + one: 'nóiméad amháin ó shin', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count uair ó shin', + many: '$count uair ó shin', + few: '$count uair ó shin', + two: '$count uair ó shin', + one: 'uair amháin ó shin', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count lá ó shin', + many: '$count lá ó shin', + few: '$count lá ó shin', + two: '$count lá ó shin', + one: 'lá amháin ó shin', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count seachtain ó shin', + many: '$count seachtain ó shin', + few: '$count sheachtain ó shin', + two: '$count sheachtain ó shin', + one: 'seachtain amháin ó shin', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count mí ó shin', + many: '$count mí ó shin', + few: '$count mí ó shin', + two: '$count mhí ó shin', + one: 'mí amháin ó shin', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count bliain ó shin', + many: '$count mbliain ó shin', + few: '$count mbliain ó shin', + two: '$count bhliain ó shin', + one: 'Bliain amháin ó shin', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutes remaining', + one: '$count minute remaining', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hours remaining', + one: '$count hour remaining', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_gl.dart b/lib/l10n/l10n_gl.dart index 92e301ce96..83efbaa00d 100644 --- a/lib/l10n/l10n_gl.dart +++ b/lib/l10n/l10n_gl.dart @@ -864,6 +864,9 @@ class AppLocalizationsGl extends AppLocalizations { @override String get preferencesInGameOnly => 'Só durante a partida'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Reloxo de xadrez'; @@ -5499,4 +5502,178 @@ class AppLocalizationsGl extends AppLocalizations { ); return '$_temp0'; } + + @override + String get timeagoJustNow => 'agora mesmo'; + + @override + String get timeagoRightNow => 'agora'; + + @override + String get timeagoCompleted => 'completado'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'en $count segundos', + one: 'en $count segundo', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'en $count minutos', + one: 'en $count minuto', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'en $count horas', + one: 'en $count hora', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'en $count días', + one: 'en $count día', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'en $count semanas', + one: 'en $count semana', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'en $count meses', + one: 'en $count mes', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'en $count anos', + one: 'en $count ano', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Hai $count minutos', + one: 'Hai $count minuto', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Hai $count horas', + one: 'Hai $count hora', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Hai $count días', + one: 'Hai $count día', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Hai $count semanas', + one: 'Hai $count semana', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Hai $count meses', + one: 'Hai $count mes', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Hai $count anos', + one: 'Hai $count ano', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutos restantes', + one: '$count minuto restante', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count horas restantes', + one: '$count hora restante', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_gsw.dart b/lib/l10n/l10n_gsw.dart index 0626a57a85..8694267e0c 100644 --- a/lib/l10n/l10n_gsw.dart +++ b/lib/l10n/l10n_gsw.dart @@ -864,6 +864,9 @@ class AppLocalizationsGsw extends AppLocalizations { @override String get preferencesInGameOnly => 'Nur im Schpiel'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Schachuhr'; @@ -5499,4 +5502,178 @@ class AppLocalizationsGsw extends AppLocalizations { ); return '$_temp0'; } + + @override + String get timeagoJustNow => 'grad jetzt'; + + @override + String get timeagoRightNow => 'genau jetzt'; + + @override + String get timeagoCompleted => 'beändet'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'i $count Sekunde', + one: 'i $count Sekunde', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count Minute', + one: 'in $count Minute', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'i $count Schtunde', + one: 'i $count Schtund', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'i $count Täg', + one: 'i $count Tag', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'i $count Wuche', + one: 'i $count Wuche', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'i $count Mönet', + one: 'i $count Monet', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'i $count Jahr', + one: 'i $count Jahr', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'vor $count Minute', + one: 'vor $count Minute', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'vor $count Schtunde', + one: 'vor $count Schtund', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'vor $count Täg', + one: 'vor $count Tag', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'vor $count Wuche', + one: 'vor $count Wuche', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'vor $count Mönet', + one: 'vor $count Monet', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'vor $count Jahr', + one: 'vor $count Jahr', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Minute blibed', + one: '$count Minute blibt', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Schtunde blibed', + one: '$count Schtund blibt', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_he.dart b/lib/l10n/l10n_he.dart index 2a19066bd6..a9879e0dfc 100644 --- a/lib/l10n/l10n_he.dart +++ b/lib/l10n/l10n_he.dart @@ -902,6 +902,9 @@ class AppLocalizationsHe extends AppLocalizations { @override String get preferencesInGameOnly => 'רק במהלך המשחק'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'שעון השחמט'; @@ -5643,4 +5646,208 @@ class AppLocalizationsHe extends AppLocalizations { ); return '$_temp0'; } + + @override + String get timeagoJustNow => 'בדיוק עכשיו'; + + @override + String get timeagoRightNow => 'עכשיו'; + + @override + String get timeagoCompleted => 'הושלם'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'עוד $count שניות', + many: 'עוד $count שניות', + two: 'עוד $count שניות', + one: 'עוד שנייה', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'עוד $count דקות', + many: 'עוד $count דקות', + two: 'עוד $count דקות', + one: 'עוד דקה $count', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'עוד $count שעות', + many: 'עוד $count שעות', + two: 'עוד $count שעות', + one: 'עוד שעה $count', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'עוד $count ימים', + many: 'עוד $count ימים', + two: 'עוד $count ימים', + one: 'עוד יום $count', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'עוד $count שבועות', + many: 'עוד $count שבועות', + two: 'עוד $count שבועות', + one: 'עוד שבוע $count', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'עוד $count חודשים', + many: 'עוד $count חודשים', + two: 'עוד $count חודשים', + one: 'עוד חודש $count', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'עוד $count שנים', + many: 'עוד $count שנים', + two: 'עוד $count שנים', + one: 'עוד שנה $count', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'לפני $count דקות', + many: 'לפני $count דקות', + two: 'לפני $count דקות', + one: 'לפני דקה $count', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'לפני $count שעות', + many: 'לפני $count שעות', + two: 'לפני $count שעות', + one: 'לפני שעה $count', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'לפני $count ימים', + many: 'לפני $count ימים', + two: 'לפני $count ימים', + one: 'לפני יום $count', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'לפני $count שבועות', + many: 'לפני $count שבועות', + two: 'לפני $count שבועות', + one: 'לפני שבוע $count', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'לפני $count חודשים', + many: 'לפני $count חודשים', + two: 'לפני $count חודשים', + one: 'לפני חודש $count', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'לפני $count שנים', + many: 'לפני $count שנים', + two: 'לפני $count שנים', + one: 'לפני שנה $count', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count דקות נותרו', + many: '$count דקות נותרו', + two: '$count דקות נותרו', + one: 'דקה $count נותרה', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count שעות נותרו', + many: '$count שעות נותרו', + two: '$count שעות נותרו', + one: 'שעה $count נותרה', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_hi.dart b/lib/l10n/l10n_hi.dart index 1089b85e5e..729b7cd889 100644 --- a/lib/l10n/l10n_hi.dart +++ b/lib/l10n/l10n_hi.dart @@ -864,6 +864,9 @@ class AppLocalizationsHi extends AppLocalizations { @override String get preferencesInGameOnly => 'केवल खेल में'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'शतरंज की घड़ी'; @@ -5497,4 +5500,178 @@ class AppLocalizationsHi extends AppLocalizations { ); return '$_temp0'; } + + @override + String get timeagoJustNow => 'अभी'; + + @override + String get timeagoRightNow => 'अभी'; + + @override + String get timeagoCompleted => 'पूर्ण'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count सेकंड में', + one: '$count सेकेंड में', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count मिनट में', + one: '$count मिनट में', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count घंटों में', + one: '$count घंटों में', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count दिनो में', + one: '$count दिन में', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count हफ़्तों में', + one: '$count हफ़्ते में', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count महीनो में', + one: '$count महीने बाद​', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count सालों में', + one: '$count साल में', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count मिनटों पहले', + one: '$count मिनट पहले', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count घंटे पहले', + one: '$count घंटे पहले', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count दिनों पहले', + one: '$count दिन पहले', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count सप्ताह पहले', + one: '$count सप्ताह पहले', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count महीने पहले', + one: '$count महीने पहले', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count वर्षों पहले', + one: '$count वर्ष पहले', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count मिनट बचे हैं', + one: '$count मिनट बचा है', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count घंटे बचे हैं', + one: '$count घंटा बचा है', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_hr.dart b/lib/l10n/l10n_hr.dart index 96aed2325a..73a4941508 100644 --- a/lib/l10n/l10n_hr.dart +++ b/lib/l10n/l10n_hr.dart @@ -882,6 +882,9 @@ class AppLocalizationsHr extends AppLocalizations { @override String get preferencesInGameOnly => 'Samo unutar igre'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Sat'; @@ -5568,4 +5571,191 @@ class AppLocalizationsHr extends AppLocalizations { ); return '$_temp0'; } + + @override + String get timeagoJustNow => 'upravo sada'; + + @override + String get timeagoRightNow => 'upravo sada'; + + @override + String get timeagoCompleted => 'završeno'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'za $count sekunda', + few: 'za $count sekundi', + one: 'za $count sekunda', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'za $count minuta', + few: 'za $count minute', + one: 'za $count minutu', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'za $count sati', + few: 'za $count sata', + one: 'za $count sat', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'za $count dana', + few: 'za $count dana', + one: 'za $count dan', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'za $count tjedana', + few: 'za $count tjedna', + one: 'za $count tjedan', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'za $count mjeseci', + few: 'za $count mjeseca', + one: 'za $count mjesec', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'za $count godina', + few: 'za $count godine', + one: 'za $count godinu', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'prije $count minuta', + few: 'prije $count minute', + one: 'prije $count minutu', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'prije $count sati', + few: 'prije $count sata', + one: 'prije $count sat', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'prije $count dana', + few: 'prije $count dana', + one: 'prije $count dan', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'prije $count tjedna', + few: 'prije $count tjedna', + one: 'prije $count tjedan', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'prije $count mjeseci', + few: 'prije $count mjeseca', + one: 'prije $count mjesec', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'prije $count godina', + few: 'prije $count godine', + one: 'prije $count godinu', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutes remaining', + one: '$count minute remaining', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hours remaining', + one: '$count hour remaining', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_hu.dart b/lib/l10n/l10n_hu.dart index 3b63c2e273..cc9a7917b2 100644 --- a/lib/l10n/l10n_hu.dart +++ b/lib/l10n/l10n_hu.dart @@ -864,6 +864,9 @@ class AppLocalizationsHu extends AppLocalizations { @override String get preferencesInGameOnly => 'Csak játék közben'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Sakkóra'; @@ -5499,4 +5502,178 @@ class AppLocalizationsHu extends AppLocalizations { ); return '$_temp0'; } + + @override + String get timeagoJustNow => 'épp most'; + + @override + String get timeagoRightNow => 'épp most'; + + @override + String get timeagoCompleted => 'befejeződött'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count másodperc múlva', + one: '$count másodperc múlva', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count perc múlva', + one: '$count perc múlva', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count óra múlva', + one: '$count óra múlva', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count nap múlva', + one: '$count nap múlva', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hét múlva', + one: '$count hét múlva', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hónap múlva', + one: '$count hónap múlva', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count év múlva', + one: '$count év múlva', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count perce', + one: '$count perce', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count órája', + one: '$count órája', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count napja', + one: '$count napja', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hete', + one: '$count hete', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hónapja', + one: '$count hónapja', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count éve', + one: '$count éve', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count perc van hátra', + one: '$count perc van hátra', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count óra van hátra', + one: '$count óra van hátra', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_hy.dart b/lib/l10n/l10n_hy.dart index 911097d35c..21a800e7fe 100644 --- a/lib/l10n/l10n_hy.dart +++ b/lib/l10n/l10n_hy.dart @@ -864,6 +864,9 @@ class AppLocalizationsHy extends AppLocalizations { @override String get preferencesInGameOnly => 'Միայն խաղի մեջ'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'շախմատի ժամացույց'; @@ -5499,4 +5502,178 @@ class AppLocalizationsHy extends AppLocalizations { ); return '$_temp0'; } + + @override + String get timeagoJustNow => 'հենց հիմա'; + + @override + String get timeagoRightNow => 'հենց հիմա'; + + @override + String get timeagoCompleted => 'ավարտված'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count վայրկյաններ հետո', + one: '$count վայրկյան հետո', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count րոպեներ հետո', + one: '$count րոպե հետո', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count ժամ հետո', + one: '$count ժամ հետո', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count օր հետո', + one: '$count օր հետո', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count շաբաթ հետո', + one: '$count շաբաթ հետո', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count ամիս հետո', + one: '$count ամիս հետո', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count տարի հետո', + one: '$count տարի հետո', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count րոպե առաջ', + one: '$count րոպե առաջ', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count ժամ առաջ', + one: '$count ժամ առաջ', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count օր առաջ', + one: '$count օր առաջ', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count շաբաթ առաջ', + one: '$count շաբաթ առաջ', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count ամիս առաջ', + one: '$count ամիս առաջ', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count տարի առաջ', + one: '$count տարի առաջ', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutes remaining', + one: '$count minute remaining', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hours remaining', + one: '$count hour remaining', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_id.dart b/lib/l10n/l10n_id.dart index 1bdb10a61c..4f0e02db18 100644 --- a/lib/l10n/l10n_id.dart +++ b/lib/l10n/l10n_id.dart @@ -847,6 +847,9 @@ class AppLocalizationsId extends AppLocalizations { @override String get preferencesInGameOnly => 'Hanya di dalam permainan'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Jam catur'; @@ -5429,4 +5432,163 @@ class AppLocalizationsId extends AppLocalizations { ); return '$_temp0'; } + + @override + String get timeagoJustNow => 'baru saja'; + + @override + String get timeagoRightNow => 'sekarang'; + + @override + String get timeagoCompleted => 'telah selesai'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'dalam $count detik', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'dalam $count menit', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'dalam $count jam', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'dalam $count hari', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'dalam $count minggu', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'dalam $count bulan', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'dalam $count tahun', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count menit yang lalu', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count jam yang lalu', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hari yang lalu', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minggu yang lalu', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count bulan yang lalu', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tahun yang lalu', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count menit tersisa', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count jam tersisa', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_it.dart b/lib/l10n/l10n_it.dart index bc73454ef6..62e049c87b 100644 --- a/lib/l10n/l10n_it.dart +++ b/lib/l10n/l10n_it.dart @@ -864,6 +864,9 @@ class AppLocalizationsIt extends AppLocalizations { @override String get preferencesInGameOnly => 'Solamente durante la partita'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Orologio'; @@ -5499,4 +5502,178 @@ class AppLocalizationsIt extends AppLocalizations { ); return '$_temp0'; } + + @override + String get timeagoJustNow => 'adesso'; + + @override + String get timeagoRightNow => 'adesso'; + + @override + String get timeagoCompleted => 'completato'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tra $count secondi', + one: 'tra $count secondo', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tra $count minuti', + one: 'tra $count minuto', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tra $count ore', + one: 'tra $count ora', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tra $count giorni', + one: 'tra $count giorno', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tra $count settimane', + one: 'tra $count settimana', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tra $count mesi', + one: 'tra $count mese', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'tra $count anni', + one: 'tra $count anno', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minuti fa', + one: '$count minuto fa', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count ore fa', + one: '$count ora fa', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count giorni fa', + one: '$count giorno fa', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count settimane fa', + one: '$count settimana fa', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count mesi fa', + one: '$count mese fa', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count anni fa', + one: '$count anno fa', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minuti rimanenti', + one: '$count minuto rimanente', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count ore rimanenti', + one: '$count ora rimanente', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_ja.dart b/lib/l10n/l10n_ja.dart index 29619a0d4e..6f03ec4a3f 100644 --- a/lib/l10n/l10n_ja.dart +++ b/lib/l10n/l10n_ja.dart @@ -845,6 +845,9 @@ class AppLocalizationsJa extends AppLocalizations { @override String get preferencesInGameOnly => '対局中のみ'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => '時間表示'; @@ -5427,4 +5430,163 @@ class AppLocalizationsJa extends AppLocalizations { ); return '$_temp0'; } + + @override + String get timeagoJustNow => 'たった今'; + + @override + String get timeagoRightNow => 'たった今'; + + @override + String get timeagoCompleted => '完了'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 秒後', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 分後', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 時間後', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 日後', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 週後', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count か月後', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 年後', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 分前', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 時間前', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 日前', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 週前', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count か月前', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 年前', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '残り $count 分', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '残り $count 時間', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_kk.dart b/lib/l10n/l10n_kk.dart index 727edf0c74..a3fb2fa76f 100644 --- a/lib/l10n/l10n_kk.dart +++ b/lib/l10n/l10n_kk.dart @@ -864,6 +864,9 @@ class AppLocalizationsKk extends AppLocalizations { @override String get preferencesInGameOnly => 'Ойында ғана'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Шахмат сағаты'; @@ -5499,4 +5502,178 @@ class AppLocalizationsKk extends AppLocalizations { ); return '$_temp0'; } + + @override + String get timeagoJustNow => 'жаңа ғана'; + + @override + String get timeagoRightNow => 'дәл қазір'; + + @override + String get timeagoCompleted => 'completed'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count секундта', + one: '$count секундта', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count минутта', + one: '$count минутта', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count сағатта', + one: '$count сағатта', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count күннен кейін', + one: '$count күннен кейін', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count аптадан кейін', + one: '$count аптадан кейін', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count айдан кейін', + one: '$count айдан кейін', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count жылдан кейін', + one: '$count жылдан кейін', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count минут бұрын', + one: '$count минут бұрын', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count сағат бұрын', + one: '$count сағат бұрын', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count күн бұрын', + one: '$count күн бұрын', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count апта бұрын', + one: '$count апта бұрын', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count ай бұрын', + one: '$count ай бұрын', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count жыл бұрын', + one: '$count жыл бұрын', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutes remaining', + one: '$count minute remaining', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hours remaining', + one: '$count hour remaining', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_ko.dart b/lib/l10n/l10n_ko.dart index 9c698c86b2..fe23b91549 100644 --- a/lib/l10n/l10n_ko.dart +++ b/lib/l10n/l10n_ko.dart @@ -845,6 +845,9 @@ class AppLocalizationsKo extends AppLocalizations { @override String get preferencesInGameOnly => '게임 도중에만 적용'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => '체스 시계'; @@ -5427,4 +5430,163 @@ class AppLocalizationsKo extends AppLocalizations { ); return '$_temp0'; } + + @override + String get timeagoJustNow => '방금'; + + @override + String get timeagoRightNow => '지금'; + + @override + String get timeagoCompleted => '종료됨'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count초 후', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count분 후', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count시간 후', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count일 후', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count주 후', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count개월 후', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count년 후', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count분 전', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count시간 전', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count일 전', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count주 전', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count개월 전', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count년 전', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count분 남음', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count시간 남음', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_lb.dart b/lib/l10n/l10n_lb.dart index e37362b2d4..1d8ae93639 100644 --- a/lib/l10n/l10n_lb.dart +++ b/lib/l10n/l10n_lb.dart @@ -864,6 +864,9 @@ class AppLocalizationsLb extends AppLocalizations { @override String get preferencesInGameOnly => 'Nëmmen während enger Partie'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Schachauer'; @@ -5499,4 +5502,178 @@ class AppLocalizationsLb extends AppLocalizations { ); return '$_temp0'; } + + @override + String get timeagoJustNow => 'elo grad'; + + @override + String get timeagoRightNow => 'elo'; + + @override + String get timeagoCompleted => 'eriwwer'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'an $count Sekonnen', + one: 'an $count Sekonn', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'an $count Minutten', + one: 'an $count Minutt', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'an $count Stonnen', + one: 'an $count Stonn', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'an $count Deeg', + one: 'an $count Dag', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'an $count Wochen', + one: 'an $count Woch', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'an $count Méint', + one: 'an $count Mount', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'an $count Joer', + one: 'an $count Joer', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'virun $count Minutten', + one: 'virun $count Minutt', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'virun $count Stonnen', + one: 'virun $count Stonn', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'virun $count Deeg', + one: 'virun $count Dag', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'virun $count Wochen', + one: 'virun $count Woch', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'virun $count Méint', + one: 'virun $count Mount', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'virun $count Joer', + one: 'virun $count Joer', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Minutten iwwereg', + one: '$count Minutt iwwereg', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count Stonnen iwwereg', + one: '$count Stonn iwwereg', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_lt.dart b/lib/l10n/l10n_lt.dart index 08d6821c40..be90c42bdb 100644 --- a/lib/l10n/l10n_lt.dart +++ b/lib/l10n/l10n_lt.dart @@ -900,6 +900,9 @@ class AppLocalizationsLt extends AppLocalizations { @override String get preferencesInGameOnly => 'Tik žaidimo metu'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Žaidimo laikrodis'; @@ -5641,4 +5644,208 @@ class AppLocalizationsLt extends AppLocalizations { ); return '$_temp0'; } + + @override + String get timeagoJustNow => 'ką tik'; + + @override + String get timeagoRightNow => 'dabar'; + + @override + String get timeagoCompleted => 'užbaigta'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'po $count sekundžių', + many: 'po $count sekundės', + few: 'po $count sekundžių', + one: 'po $count sekundės', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'po $count minučių', + many: 'po $count minutės', + few: 'po $count minučių', + one: 'po $count minutės', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'po $count valandų', + many: 'po $count valandos', + few: 'po $count valandų', + one: 'po $count valandos', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'po $count dienų', + many: 'po $count dienos', + few: 'po $count dienų', + one: 'po $count dienos', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'po $count savaičių', + many: 'po $count savaitės', + few: 'po $count savaičių', + one: 'po $count savaitės', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'po $count mėnesių', + many: 'po $count mėnesio', + few: 'po $count mėnesių', + one: 'po $count mėnesio', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'po $count metų', + many: 'po $count metų', + few: 'po $count metų', + one: 'po $count metų', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Prieš $count minučių', + many: 'Prieš $count minutės', + few: 'Prieš $count minutes', + one: 'Prieš $count minutę', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Prieš $count valandų', + many: 'Prieš $count valandos', + few: 'Prieš $count valandas', + one: 'Prieš $count valandą', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Prieš $count dienų', + many: 'Prieš $count dienos', + few: 'Prieš $count dienas', + one: 'Prieš $count dieną', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Prieš $count savaičių', + many: 'Prieš $count savaitės', + few: 'Prieš $count savaites', + one: 'Prieš $count savaitę', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Prieš $count mėnesių', + many: 'Prieš $count mėnesio', + few: 'Prieš $count mėnesius', + one: 'Prieš $count mėnesį', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Prieš $count metų', + many: 'Prieš $count metų', + few: 'Prieš $count metus', + one: 'Prieš $count metus', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Liko $count minučių', + many: 'Liko $count minučių', + few: 'Liko $count minutės', + one: 'Liko $count minutė', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Liko $count valandų', + many: 'Liko $count valandų', + few: 'Liko $count valandos', + one: 'Liko $count valanda', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_lv.dart b/lib/l10n/l10n_lv.dart index f9d0d5a4e5..6f03a65fca 100644 --- a/lib/l10n/l10n_lv.dart +++ b/lib/l10n/l10n_lv.dart @@ -881,6 +881,9 @@ class AppLocalizationsLv extends AppLocalizations { @override String get preferencesInGameOnly => 'In-game only'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Šaha pulkstenis'; @@ -5569,4 +5572,191 @@ class AppLocalizationsLv extends AppLocalizations { ); return '$_temp0'; } + + @override + String get timeagoJustNow => 'tikko'; + + @override + String get timeagoRightNow => 'tieši tagad'; + + @override + String get timeagoCompleted => 'completed'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'pēc $count sekundēm', + one: 'pēc $count sekundes', + zero: 'pēc $count sekundēm', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'pēc $count minūtēm', + one: 'pēc $count minūtes', + zero: 'pēc $count minūtēm', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'pēc $count stundām', + one: 'pēc $count stundas', + zero: 'pēc $count stundām', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'pēc $count dienām', + one: 'pēc $count dienas', + zero: 'pēc $count dienām', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'pēc $count nedēļām', + one: 'pēc $count nedēļas', + zero: 'pēc $count nedēļām', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'pēc $count mēnešiem', + one: 'pēc $count mēneša', + zero: 'pēc $count mēnešiem', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'pēc $count gadiem', + one: 'pēc $count gada', + zero: 'pēc $count gadiem', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'pirms $count minūtēm', + one: 'pirms $count minūtes', + zero: 'pirms $count minūtēm', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'pirms $count stundām', + one: 'pirms $count stundas', + zero: 'pirms $count stundām', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'pirms $count dienām', + one: 'pirms $count dienas', + zero: 'pirms $count dienām', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'pirms $count nedēļām', + one: 'pirms $count nedēļas', + zero: 'pirms $count nedēļām', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'pirms $count mēnešiem', + one: 'pirms $count mēneša', + zero: 'pirms $count mēnešiem', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'pirms $count gadiem', + one: 'pirms $count gada', + zero: 'pirms $count gadiem', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutes remaining', + one: '$count minute remaining', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hours remaining', + one: '$count hour remaining', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_mk.dart b/lib/l10n/l10n_mk.dart index 221885b319..97db7f6e43 100644 --- a/lib/l10n/l10n_mk.dart +++ b/lib/l10n/l10n_mk.dart @@ -864,6 +864,9 @@ class AppLocalizationsMk extends AppLocalizations { @override String get preferencesInGameOnly => 'In-game only'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Шаховски часовник'; @@ -5499,4 +5502,178 @@ class AppLocalizationsMk extends AppLocalizations { ); return '$_temp0'; } + + @override + String get timeagoJustNow => 'тукушто'; + + @override + String get timeagoRightNow => 'Тукушто'; + + @override + String get timeagoCompleted => 'completed'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'За $count секунди', + one: 'За $count секунди', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'За $count минути', + one: 'За $count минута', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'За $count часови', + one: 'За $count час', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'За $count денови', + one: 'За $count ден', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'За $count седмици', + one: 'За $count седмица', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'За $count месеци', + one: 'За $count месец', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'За $count години', + one: 'За $count година', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Пред $count минути', + one: 'Пред $count минута', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Пред $count часа', + one: 'пред $count час', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Пред $count дена', + one: 'пред $count ден', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Пред $count седмици', + one: 'Пред $count седмица', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Пред $count месеци', + one: 'Пред $count месец', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Пред $count години', + one: 'Пред $count година', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutes remaining', + one: '$count minute remaining', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hours remaining', + one: '$count hour remaining', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_nb.dart b/lib/l10n/l10n_nb.dart index 8d013480ec..ee9e5cc479 100644 --- a/lib/l10n/l10n_nb.dart +++ b/lib/l10n/l10n_nb.dart @@ -864,6 +864,9 @@ class AppLocalizationsNb extends AppLocalizations { @override String get preferencesInGameOnly => 'Bare under partier'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Sjakkur'; @@ -5499,4 +5502,178 @@ class AppLocalizationsNb extends AppLocalizations { ); return '$_temp0'; } + + @override + String get timeagoJustNow => 'om litt'; + + @override + String get timeagoRightNow => 'for litt siden'; + + @override + String get timeagoCompleted => 'fullført'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'om $count sekunder', + one: 'om $count sekund', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'om $count minutter', + one: 'om $count minutt', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'om $count timer', + one: 'om $count time', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'om $count døgn', + one: 'om $count døgn', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'om $count uker', + one: 'om $count uke', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'om $count måneder', + one: 'om $count måned', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'om $count år', + one: 'om $count år', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'for $count minutter siden', + one: 'for $count minutt siden', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'for $count timer siden', + one: 'for $count time siden', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'for $count døgn siden', + one: 'for $count døgn siden', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'for $count uker siden', + one: 'for $count uke siden', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'for $count måneder siden', + one: 'for $count måned siden', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'for $count år siden', + one: 'for $count år siden', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutter igjen', + one: '$count minutt igjen', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count timer igjen', + one: '$count time igjen', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_nl.dart b/lib/l10n/l10n_nl.dart index c4bb227c01..c7eb877818 100644 --- a/lib/l10n/l10n_nl.dart +++ b/lib/l10n/l10n_nl.dart @@ -864,6 +864,9 @@ class AppLocalizationsNl extends AppLocalizations { @override String get preferencesInGameOnly => 'Alleen tijdens partij'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Schaakklok'; @@ -5499,4 +5502,178 @@ class AppLocalizationsNl extends AppLocalizations { ); return '$_temp0'; } + + @override + String get timeagoJustNow => 'zojuist'; + + @override + String get timeagoRightNow => 'op dit moment'; + + @override + String get timeagoCompleted => 'voltooid'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'over $count seconden', + one: 'over $count seconde', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'over $count minuten', + one: 'over $count minuut', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'over $count uur', + one: 'over $count uur', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'over $count dagen', + one: 'over $count dag', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'over $count weken', + one: 'over $count week', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'over $count maanden', + one: 'over $count maand', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'over $count jaren', + one: 'over $count jaar', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minuten geleden', + one: '$count minuut geleden', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count uur geleden', + one: '$count uur geleden', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count dagen geleden', + one: '$count dag geleden', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count weken geleden', + one: '$count week geleden', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count maanden geleden', + one: '$count maand geleden', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count jaar geleden', + one: '$count jaar geleden', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minuten resterend', + one: '$count minuut resterend', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count uur resterend', + one: '$count uur resterend', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_nn.dart b/lib/l10n/l10n_nn.dart index 0c6607a137..2ebae3b71a 100644 --- a/lib/l10n/l10n_nn.dart +++ b/lib/l10n/l10n_nn.dart @@ -864,6 +864,9 @@ class AppLocalizationsNn extends AppLocalizations { @override String get preferencesInGameOnly => 'Berre under eit parti'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Sjakkur'; @@ -5499,4 +5502,178 @@ class AppLocalizationsNn extends AppLocalizations { ); return '$_temp0'; } + + @override + String get timeagoJustNow => 'for kort tid sidan'; + + @override + String get timeagoRightNow => 'nett no'; + + @override + String get timeagoCompleted => 'fullført'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'om $count sekund', + one: 'om $count sekund', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'om $count minutt', + one: 'om $count minutt', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'om $count timar', + one: 'om $count time', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'om $count dagar', + one: 'om $count dag', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'om $count veker', + one: 'om $count veke', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'om $count månader', + one: 'om $count månad', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'om $count år', + one: 'om $count år', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutt sidan', + one: '$count minutt sidan', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count timar sidan', + one: '$count time sidan', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count dagar sidan', + one: '$count dag sidan', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count veker sidan', + one: '$count veke sidan', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count månader sidan', + one: '$count månad sidan', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count år sidan', + one: '$count år sidan', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutt igjen', + one: '$count minutt igjen', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count timar igjen', + one: '$count time igjen', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_pl.dart b/lib/l10n/l10n_pl.dart index 5c65084a47..4abd844ab1 100644 --- a/lib/l10n/l10n_pl.dart +++ b/lib/l10n/l10n_pl.dart @@ -902,6 +902,9 @@ class AppLocalizationsPl extends AppLocalizations { @override String get preferencesInGameOnly => 'Tylko w partii'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Zegar szachowy'; @@ -5643,4 +5646,208 @@ class AppLocalizationsPl extends AppLocalizations { ); return '$_temp0'; } + + @override + String get timeagoJustNow => 'właśnie teraz'; + + @override + String get timeagoRightNow => 'w tej chwili'; + + @override + String get timeagoCompleted => 'ukończone'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'za $count sekund', + many: 'za $count sekund', + few: 'za $count sekundy', + one: 'za $count sekundę', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'za $count minut', + many: 'za $count minuty', + few: 'za $count minuty', + one: 'za $count minutę', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'za $count godzin', + many: 'za $count godzin', + few: 'za $count godziny', + one: 'za $count godzinę', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'za $count dni', + many: 'za $count dni', + few: 'za $count dni', + one: 'za $count dzień', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'za $count tygodni', + many: 'za $count tygodni', + few: 'za $count tygodnie', + one: 'za $count tydzień', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'za $count miesięcy', + many: 'za $count miesięcy', + few: 'za $count miesiące', + one: 'za $count miesiąc', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'za $count lat', + many: 'za $count lat', + few: 'za $count lata', + one: 'za $count rok', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minut temu', + many: '$count minut temu', + few: '$count minuty temu', + one: '$count minutę temu', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count godzin temu', + many: '$count godzin temu', + few: '$count godziny temu', + one: '$count godzinę temu', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count dni temu', + many: '$count dni temu', + few: '$count dni temu', + one: '$count dzień temu', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tygodni temu', + many: '$count tygodni temu', + few: '$count tygodnie temu', + one: '$count tydzień temu', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count miesięcy temu', + many: '$count miesięcy temu', + few: '$count miesiące temu', + one: '$count miesiąc temu', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count lat temu', + many: '$count lat temu', + few: '$count lata temu', + one: '$count rok temu', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Pozostało $count minut', + many: 'Pozostało $count minut', + few: 'Pozostały $count minuty', + one: 'Pozostała $count minuta', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Pozostało $count godzin', + many: 'Pozostało $count godzin', + few: 'Pozostały $count godziny', + one: 'Pozostała $count godzina', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_pt.dart b/lib/l10n/l10n_pt.dart index 9830a8e16f..bae067a3e3 100644 --- a/lib/l10n/l10n_pt.dart +++ b/lib/l10n/l10n_pt.dart @@ -864,6 +864,9 @@ class AppLocalizationsPt extends AppLocalizations { @override String get preferencesInGameOnly => 'Apenas em Jogo'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Relógio de xadrez'; @@ -5499,6 +5502,180 @@ class AppLocalizationsPt extends AppLocalizations { ); return '$_temp0'; } + + @override + String get timeagoJustNow => 'agora mesmo'; + + @override + String get timeagoRightNow => 'agora mesmo'; + + @override + String get timeagoCompleted => 'concluído'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'em $count segundos', + one: 'em $count segundos', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'em $count minutos', + one: 'dentro de $count minutos', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'em $count horas', + one: 'em $count hora', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'em $count dias', + one: 'em $count dia', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'em $count semanas', + one: 'em $count semana', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'em $count meses', + one: 'em $count mês', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'em $count anos', + one: 'em $count ano', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'há $count minutos', + one: 'há $count minuto', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'há $count horas', + one: 'há $count hora', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'há $count dias', + one: 'há $count dia', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'há $count semanas', + one: 'há $count semana', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'há $count meses', + one: 'há $count mês', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'há $count anos', + one: 'há $count ano', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutos restantes', + one: '$count minuto restante', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count horas restantes', + one: '$count hora restante', + ); + return '$_temp0'; + } } /// The translations for Portuguese, as used in Brazil (`pt_BR`). @@ -10996,4 +11173,178 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { ); return '$_temp0'; } + + @override + String get timeagoJustNow => 'agora há pouco'; + + @override + String get timeagoRightNow => 'agora mesmo'; + + @override + String get timeagoCompleted => 'concluído'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'em $count segundos', + one: 'em $count segundo', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'em $count minutos', + one: 'em $count minuto', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'em $count horas', + one: 'em $count hora', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'em $count dias', + one: 'em $count dia', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'em $count semanas', + one: 'em $count semana', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'em $count meses', + one: 'em $count mês', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'em $count anos', + one: 'em $count ano', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutos atrás', + one: '$count minuto atrás', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count horas atrás', + one: '$count hora atrás', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count dias atrás', + one: '$count dia atrás', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count semanas atrás', + one: '$count semana atrás', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count meses atrás', + one: '$count mês atrás', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count anos atrás', + one: '$count ano atrás', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutos restantes', + one: '$count minuto restante', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count horas restantes', + one: '$count hora restante', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_ro.dart b/lib/l10n/l10n_ro.dart index 718c012ba0..fd17d69044 100644 --- a/lib/l10n/l10n_ro.dart +++ b/lib/l10n/l10n_ro.dart @@ -883,6 +883,9 @@ class AppLocalizationsRo extends AppLocalizations { @override String get preferencesInGameOnly => 'Doar în joc'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Ceasul de șah'; @@ -5571,4 +5574,193 @@ class AppLocalizationsRo extends AppLocalizations { ); return '$_temp0'; } + + @override + String get timeagoJustNow => 'chiar acum'; + + @override + String get timeagoRightNow => 'chiar acum'; + + @override + String get timeagoCompleted => 'completat'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'în $count de secunde', + few: 'în $count secunde', + one: 'în $count secundă', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'în $count de minute', + few: 'în $count minute', + one: 'în $count minut', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'în $count de ore', + few: 'în $count ore', + one: 'în $count oră', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'în $count de zile', + few: 'în $count zile', + one: 'în $count zi', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'în $count de săptămâni', + few: 'în $count săptămâni', + one: 'în $count săptămână', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'în $count de luni', + few: 'în $count luni', + one: 'în $count lună', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'în $count de ani', + few: 'în $count ani', + one: 'în $count an', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'cu $count de minute în urmă', + few: 'cu $count minute în urmă', + one: 'cu $count minut în urmă', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'cu $count de ore în urmă', + few: 'cu $count ore în urmă', + one: 'cu $count oră în urmă', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'cu $count de zile în urmă', + few: 'cu $count zile în urmă', + one: 'cu $count zi în urmă', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'cu $count de săptămâni în urmă', + few: 'cu $count săptămâni în urmă', + one: 'cu $count săptămână în urmă', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'cu $count de luni în urmă', + few: 'cu $count luni în urmă', + one: 'cu $count lună în urmă', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'cu $count de ani în urmă', + few: 'cu $count ani în urmă', + one: 'cu $count an în urmă', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minute rămase', + few: '$count minute rămase', + one: '$count minut rămas', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count ore rămase', + few: '$count ore rămase', + one: '$count oră rămasă', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_ru.dart b/lib/l10n/l10n_ru.dart index f2fa43cb33..ae91030a3a 100644 --- a/lib/l10n/l10n_ru.dart +++ b/lib/l10n/l10n_ru.dart @@ -902,6 +902,9 @@ class AppLocalizationsRu extends AppLocalizations { @override String get preferencesInGameOnly => 'Только в игре'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Шахматные часы'; @@ -5643,4 +5646,208 @@ class AppLocalizationsRu extends AppLocalizations { ); return '$_temp0'; } + + @override + String get timeagoJustNow => 'только что'; + + @override + String get timeagoRightNow => 'прямо сейчас'; + + @override + String get timeagoCompleted => 'завершено'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'через $count секунд', + many: 'через $count секунд', + few: 'через $count секунды', + one: 'через $count секунду', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'через $count минут', + many: 'через $count минут', + few: 'через $count минуты', + one: 'через $count минуту', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'через $count часов', + many: 'через $count часов', + few: 'через $count часа', + one: 'через $count час', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'через $count дней', + many: 'через $count дней', + few: 'через $count дня', + one: 'через $count день', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'через $count недель', + many: 'через $count недель', + few: 'через $count недели', + one: 'через $count неделю', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'через $count месяцев', + many: 'через $count месяцев', + few: 'через $count месяца', + one: 'через $count месяц', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'через $count лет', + many: 'через $count лет', + few: 'через $count года', + one: 'через $count год', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count минут назад', + many: '$count минут назад', + few: '$count минуты назад', + one: '$count минуту назад', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count часов назад', + many: '$count часов назад', + few: '$count часа назад', + one: '$count час назад', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count дней назад', + many: '$count дней назад', + few: '$count дня назад', + one: '$count день назад', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count недель назад', + many: '$count недель назад', + few: '$count недели назад', + one: '$count неделю назад', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count месяцев назад', + many: '$count месяцев назад', + few: '$count месяца назад', + one: '$count месяц назад', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count лет назад', + many: '$count лет назад', + few: '$count года назад', + one: '$count год назад', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'осталось $count минут', + many: 'осталось $count минут', + few: 'осталось $count минуты', + one: 'осталась $count минута', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'осталось $count часов', + many: 'осталось $count часов', + few: 'осталось $count часа', + one: 'остался $count час', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_sk.dart b/lib/l10n/l10n_sk.dart index 583b32904e..ffe2584cea 100644 --- a/lib/l10n/l10n_sk.dart +++ b/lib/l10n/l10n_sk.dart @@ -902,6 +902,9 @@ class AppLocalizationsSk extends AppLocalizations { @override String get preferencesInGameOnly => 'Iba pri partii'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Šachové hodiny'; @@ -5643,4 +5646,208 @@ class AppLocalizationsSk extends AppLocalizations { ); return '$_temp0'; } + + @override + String get timeagoJustNow => 'práve teraz'; + + @override + String get timeagoRightNow => 'práve teraz'; + + @override + String get timeagoCompleted => 'ukončené'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'o $count sekúnd', + many: 'o $count sekúnd', + few: 'o $count sekundy', + one: 'o $count sekundu', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'o $count minút', + many: 'o $count minút', + few: 'o $count minút', + one: 'o $count minútu', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'o $count hodín', + many: 'o $count hodín', + few: 'o $count hodiny', + one: 'o $count hodinu', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'o $count dní', + many: 'o $count dní', + few: 'o $count dni', + one: 'o $count deň', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'o $count týždňov', + many: 'o $count týždňov', + few: 'o $count týždne', + one: 'o $count týždeň', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'o $count mesiacov', + many: 'o $count mesiacov', + few: 'o $count mesiace', + one: 'o $count mesiac', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'o $count rokov', + many: 'o $count rokov', + few: 'o $count roky', + one: 'o $count rok', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'pred $count minútami', + many: 'pred $count minútami', + few: 'pred $count minútami', + one: 'pred $count minútou', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'pred $count hodinami', + many: 'pred $count hodinami', + few: 'pred $count hodinami', + one: 'pred $count hodinou', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'pred $count dňami', + many: 'pred $count dňami', + few: 'pred $count dňami', + one: 'pred $count dňom', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'pred $count týždňami', + many: 'pred $count týždňami', + few: 'pred $count týždňami', + one: 'pred $count týždňom', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'pred $count mesiacmi', + many: 'pred $count mesiacmi', + few: 'pred $count mesiacmi', + one: 'pred $count mesiacom', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'pred $count rokmi', + many: 'pred $count rokmi', + few: 'pred $count rokmi', + one: 'pred $count rokom', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'ostáva $count minút', + many: 'ostáva $count minút', + few: 'ostávajú $count minúty', + one: 'ostáva $count minúta', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'ostáva $count hodín', + many: 'ostáva $count hodín', + few: 'ostávajú $count hodiny', + one: 'ostáva $count hodina', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_sl.dart b/lib/l10n/l10n_sl.dart index e15a054703..d68604b378 100644 --- a/lib/l10n/l10n_sl.dart +++ b/lib/l10n/l10n_sl.dart @@ -902,6 +902,9 @@ class AppLocalizationsSl extends AppLocalizations { @override String get preferencesInGameOnly => 'In-game only'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Šahovska ura'; @@ -5643,4 +5646,208 @@ class AppLocalizationsSl extends AppLocalizations { ); return '$_temp0'; } + + @override + String get timeagoJustNow => 'pravkar'; + + @override + String get timeagoRightNow => 'ta trenutek'; + + @override + String get timeagoCompleted => 'končano'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'čez $count sekund', + few: 'čez $count sekunde', + two: 'čez $count sekundi', + one: 'čez $count sekund', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'čez $count minut', + few: 'čez $count minute', + two: 'čez $count minuti', + one: 'čez $count minuto', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'čez $count ur', + few: 'čez $count ure', + two: 'čez $count uri', + one: 'čez $count uro', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'čez $count dni', + few: 'čez $count dnevih', + two: 'čez $count dneva', + one: 'čez $count dan', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'čez $count tednov', + few: 'čez $count tedne', + two: 'čez $count tedna', + one: 'čez $count teden', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'čez $count mesecev', + few: 'čez $count mesece', + two: 'čez $count meseca', + one: 'čez $count mesec', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'čez $count let', + few: 'čez $count leta', + two: 'čez $count leti', + one: 'čez $count leto', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Pred $count minutami', + few: 'Pred $count minutami', + two: 'Pred $count minutama', + one: 'Pred $count minuto', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Pred $count urami', + few: 'Pred $count urami', + two: 'Pred $count urama', + one: 'Pred $count uro', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Pred $count dnevi', + few: 'Pred $count dnevi', + two: 'Pred $count dnevoma', + one: 'Pred $count dnevom', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Pred $count tedni', + few: 'Pred $count tedni', + two: 'Pred $count tednoma', + one: 'Pred $count tednom', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Pred $count meseci', + few: 'Pred $count meseci', + two: 'Pred $count mesecema', + one: 'Pred $count mesecem', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Pred $count leti', + few: 'Pred $count leti', + two: 'Pred $count letoma', + one: 'Pred $count letom', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'še $count minut', + few: 'še $count minute', + two: 'še $count minuti', + one: 'še $count minuta', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'še $count ur', + few: 'še $count ure', + two: 'še $count uri', + one: 'še $count ura', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_sq.dart b/lib/l10n/l10n_sq.dart index 75a258658a..0be1d08cf9 100644 --- a/lib/l10n/l10n_sq.dart +++ b/lib/l10n/l10n_sq.dart @@ -864,6 +864,9 @@ class AppLocalizationsSq extends AppLocalizations { @override String get preferencesInGameOnly => 'In-game only'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Ora e shahut'; @@ -5499,4 +5502,178 @@ class AppLocalizationsSq extends AppLocalizations { ); return '$_temp0'; } + + @override + String get timeagoJustNow => 'tani'; + + @override + String get timeagoRightNow => 'pikërisht tani'; + + @override + String get timeagoCompleted => 'mbaroi'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'pas $count sekondave', + one: 'në $count sekondë', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'pas $count minutave', + one: 'në $count minutë', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'në $count orë', + one: 'në $count orë', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'pas $count ditëve', + one: 'në $count ditë', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'pas $count javë', + one: 'në $count javë', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'pas $count muajve', + one: 'në $count muaj', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'pas $count viteve', + one: 'në $count vit', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'para $count minutave', + one: '$count minutë më parë', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'para $count orëve', + one: '$count orë më parë', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'para $count ditëve', + one: '$count ditë më parë', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'para $count jave', + one: '$count javë më parë', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'para $count muajve', + one: '$count muaj më parë', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'para $count viteve', + one: '$count vit më parë', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Edhe $count minuta', + one: 'Edhe $count minutë', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Edhe $count orë', + one: 'Edhe $count orë', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_sr.dart b/lib/l10n/l10n_sr.dart index 8a62cce0e5..99baf28f50 100644 --- a/lib/l10n/l10n_sr.dart +++ b/lib/l10n/l10n_sr.dart @@ -880,6 +880,9 @@ class AppLocalizationsSr extends AppLocalizations { @override String get preferencesInGameOnly => 'In-game only'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Шаховски сат'; @@ -5557,4 +5560,179 @@ class AppLocalizationsSr extends AppLocalizations { ); return '$_temp0'; } + + @override + String get timeagoJustNow => 'управо сада'; + + @override + String get timeagoRightNow => 'управо сад'; + + @override + String get timeagoCompleted => 'завршено'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'за $count дана', + few: 'за $count сати', + one: 'за $count секунди', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count minutes', + one: 'in $count minute', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count hours', + one: 'in $count hour', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count days', + one: 'in $count day', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count weeks', + one: 'in $count week', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count months', + one: 'in $count month', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'in $count years', + one: 'in $count year', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutes ago', + one: '$count minute ago', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hours ago', + one: '$count hour ago', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count days ago', + one: '$count day ago', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count weeks ago', + one: '$count week ago', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count months ago', + one: '$count month ago', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count years ago', + one: '$count year ago', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minutes remaining', + one: '$count minute remaining', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hours remaining', + one: '$count hour remaining', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_sv.dart b/lib/l10n/l10n_sv.dart index a4556fe246..7a7dd5cbfa 100644 --- a/lib/l10n/l10n_sv.dart +++ b/lib/l10n/l10n_sv.dart @@ -864,6 +864,9 @@ class AppLocalizationsSv extends AppLocalizations { @override String get preferencesInGameOnly => 'Endast i parti'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Schack-klocka'; @@ -5499,4 +5502,178 @@ class AppLocalizationsSv extends AppLocalizations { ); return '$_temp0'; } + + @override + String get timeagoJustNow => 'just nu'; + + @override + String get timeagoRightNow => 'just nu'; + + @override + String get timeagoCompleted => 'slutfört'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'om $count sekunder', + one: 'om $count sekund', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'om $count minuter', + one: 'om $count minut', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'om $count timmar', + one: 'om $count timme', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'om $count dagar', + one: 'om $count dag', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'om $count veckor', + one: 'om $count vecka', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'om $count månader', + one: 'om $count månad', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'om $count år', + one: 'om $count år', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minuter sedan', + one: '$count minut sedan', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count timmar sedan', + one: '$count timme sedan', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count dagar sedan', + one: '$count dag sedan', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count veckor sedan', + one: '$count vecka sedan', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count månader sedan', + one: '$count månad sedan', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count år sedan', + one: '$count år sedan', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count minuter återstår', + one: '$count minut återstår', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count timmar återstår', + one: '$count timme återstår', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_tr.dart b/lib/l10n/l10n_tr.dart index 231c9e01af..b01b058a91 100644 --- a/lib/l10n/l10n_tr.dart +++ b/lib/l10n/l10n_tr.dart @@ -864,6 +864,9 @@ class AppLocalizationsTr extends AppLocalizations { @override String get preferencesInGameOnly => 'Sadece oyun sırasında'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Satranç saati'; @@ -5499,4 +5502,178 @@ class AppLocalizationsTr extends AppLocalizations { ); return '$_temp0'; } + + @override + String get timeagoJustNow => 'şu anda'; + + @override + String get timeagoRightNow => 'hemen şimdi'; + + @override + String get timeagoCompleted => 'tamamlanmış'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count saniyede', + one: '$count saniyede', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count dakikada', + one: '$count dakikada', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count saatte', + one: '$count saatte', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count günde', + one: '$count günde', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count haftada', + one: '$count haftada', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count ayda', + one: '$count ayda', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count yılda', + one: '$count yılda', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count dakika önce', + one: '$count dakika önce', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count saat önce', + one: '$count saat önce', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count gün önce', + one: '$count gün önce', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count hafta önce', + one: '$count hafta önce', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count ay önce', + one: '$count ay önce', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count yıl önce', + one: '$count yıl önce', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count dakika kaldı', + one: '$count dakika kaldı', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count saat kaldı', + one: '$count saat kaldı', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_uk.dart b/lib/l10n/l10n_uk.dart index cc799d8f31..fa9eb61c0a 100644 --- a/lib/l10n/l10n_uk.dart +++ b/lib/l10n/l10n_uk.dart @@ -902,6 +902,9 @@ class AppLocalizationsUk extends AppLocalizations { @override String get preferencesInGameOnly => 'Лише під час гри'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Шаховий годинник'; @@ -5643,4 +5646,208 @@ class AppLocalizationsUk extends AppLocalizations { ); return '$_temp0'; } + + @override + String get timeagoJustNow => 'щойно'; + + @override + String get timeagoRightNow => 'зараз'; + + @override + String get timeagoCompleted => 'завершено'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'за $count секунди', + many: 'за $count секунд', + few: 'за $count секунди', + one: 'за $count секунду', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'за $count хвилини', + many: 'за $count хвилин', + few: 'за $count хвилини', + one: 'за $count хвилину', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'за $count години', + many: 'за $count годин', + few: 'за $count години', + one: 'за $count годину', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'за $count дня', + many: 'за $count днів', + few: 'за $count дні', + one: 'за $count день', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'за $count тижня', + many: 'за $count тижнів', + few: 'за $count тижні', + one: 'за $count тиждень', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'за $count місяця', + many: 'за $count місяців', + few: 'за $count місяці', + one: 'за $count місяць', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'за $count року', + many: 'за $count років', + few: 'за $count роки', + one: 'за $count рік', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count хвилини тому', + many: '$count хвилин тому', + few: '$count хвилини тому', + one: '$count хвилину тому', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count години тому', + many: '$count годин тому', + few: '$count години тому', + one: '$count годину тому', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count дня тому', + many: '$count днів тому', + few: '$count дні тому', + one: '$count день тому', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count тижня тому', + many: '$count тижнів тому', + few: '$count тижні тому', + one: '$count тиждень тому', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count місяця тому', + many: '$count місяців тому', + few: '$count місяці тому', + one: '$count місяць тому', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count року тому', + many: '$count років тому', + few: '$count роки тому', + one: '$count рік тому', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'залишилося $count хвилини', + many: 'залишилося $count хвилин', + few: 'залишилося $count хвилини', + one: 'залишилася $count хвилина', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'залишилося $count години', + many: 'залишилося $count годин', + few: 'залишилося $count години', + one: 'залишилася $count година', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_vi.dart b/lib/l10n/l10n_vi.dart index c9d167cbd3..d3117af9cb 100644 --- a/lib/l10n/l10n_vi.dart +++ b/lib/l10n/l10n_vi.dart @@ -845,6 +845,9 @@ class AppLocalizationsVi extends AppLocalizations { @override String get preferencesInGameOnly => 'Chỉ trong ván cờ'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => 'Đồng hồ cờ vua'; @@ -5427,4 +5430,163 @@ class AppLocalizationsVi extends AppLocalizations { ); return '$_temp0'; } + + @override + String get timeagoJustNow => 'vừa mới đây'; + + @override + String get timeagoRightNow => 'ngay bây giờ'; + + @override + String get timeagoCompleted => 'đã hoàn thành'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'trong $count giây', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'trong $count phút', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'trong $count giờ', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'trong $count ngày', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'trong $count tuần', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'trong $count tháng', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'trong $count năm', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count phút trước', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count giờ trước', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count ngày trước', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tuần trước', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count tháng trước', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count năm trước', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'còn $count phút', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'còn $count giờ', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/l10n_zh.dart b/lib/l10n/l10n_zh.dart index 8d15ade474..d38b2f25dd 100644 --- a/lib/l10n/l10n_zh.dart +++ b/lib/l10n/l10n_zh.dart @@ -846,6 +846,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get preferencesInGameOnly => '仅在对局中'; + @override + String get preferencesExceptInGame => 'Except in-game'; + @override String get preferencesChessClock => '棋钟'; @@ -5428,6 +5431,165 @@ class AppLocalizationsZh extends AppLocalizations { ); return '$_temp0'; } + + @override + String get timeagoJustNow => '刚刚'; + + @override + String get timeagoRightNow => '刚刚'; + + @override + String get timeagoCompleted => '已完成'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '在 $count 秒内', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '在 $count 分钟内', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '在 $count 小时内', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '在 $count 天内', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '在 $count 周内', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '在 $count 月内', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '在 $count 年内', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 分钟前', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 小时前', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 天前', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 周前', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 月前', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count 年前', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '还剩 $count 分钟', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '还剩 $count 小时', + ); + return '$_temp0'; + } } /// The translations for Chinese, as used in Taiwan (`zh_TW`). @@ -10839,4 +11001,163 @@ class AppLocalizationsZhTw extends AppLocalizationsZh { ); return '$_temp0'; } + + @override + String get timeagoJustNow => '剛剛'; + + @override + String get timeagoRightNow => '現在'; + + @override + String get timeagoCompleted => '已結束'; + + @override + String timeagoInNbSeconds(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count秒後', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMinutes(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count分後', + ); + return '$_temp0'; + } + + @override + String timeagoInNbHours(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count小時後', + ); + return '$_temp0'; + } + + @override + String timeagoInNbDays(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count天後', + ); + return '$_temp0'; + } + + @override + String timeagoInNbWeeks(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count週後', + ); + return '$_temp0'; + } + + @override + String timeagoInNbMonths(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count個月後', + ); + return '$_temp0'; + } + + @override + String timeagoInNbYears(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count年後', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count分前', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count小時前', + ); + return '$_temp0'; + } + + @override + String timeagoNbDaysAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count天前', + ); + return '$_temp0'; + } + + @override + String timeagoNbWeeksAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count週前', + ); + return '$_temp0'; + } + + @override + String timeagoNbMonthsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count個月前', + ); + return '$_temp0'; + } + + @override + String timeagoNbYearsAgo(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count年前', + ); + return '$_temp0'; + } + + @override + String timeagoNbMinutesRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '剩下 $count 分鐘', + ); + return '$_temp0'; + } + + @override + String timeagoNbHoursRemaining(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '剩下 $count 小時', + ); + return '$_temp0'; + } } diff --git a/lib/l10n/lila_af.arb b/lib/l10n/lila_af.arb index ef72132e95..8fb26c6a3b 100644 --- a/lib/l10n/lila_af.arb +++ b/lib/l10n/lila_af.arb @@ -1474,5 +1474,23 @@ "studyNbChapters": "{count, plural, =1{{count} Hoofstuk} other{{count} Hoofstukke}}", "studyNbGames": "{count, plural, =1{{count} Wedstryd} other{{count} Wedstryde}}", "studyNbMembers": "{count, plural, =1{{count} Lid} other{{count} Lede}}", - "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Plak jou PGN teks hier, tot by {count} spel} other{Plak jou PGN teks hier, tot by {count} spelle}}" + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Plak jou PGN teks hier, tot by {count} spel} other{Plak jou PGN teks hier, tot by {count} spelle}}", + "timeagoJustNow": "sopas", + "timeagoRightNow": "nou", + "timeagoCompleted": "voltooi", + "timeagoInNbSeconds": "{count, plural, =1{in {count} sekonde} other{in {count} sekondes}}", + "timeagoInNbMinutes": "{count, plural, =1{in {count} minuut} other{in {count} minute}}", + "timeagoInNbHours": "{count, plural, =1{in {count} uur} other{in {count} ure}}", + "timeagoInNbDays": "{count, plural, =1{in {count} dag} other{in {count} dae}}", + "timeagoInNbWeeks": "{count, plural, =1{in {count} week} other{in {count} weke}}", + "timeagoInNbMonths": "{count, plural, =1{in {count} maand} other{in {count} maande}}", + "timeagoInNbYears": "{count, plural, =1{in {count} jaar} other{in {count} jare}}", + "timeagoNbMinutesAgo": "{count, plural, =1{{count} minuut gelede} other{{count} minute gelede}}", + "timeagoNbHoursAgo": "{count, plural, =1{{count} uur gelede} other{{count} ure gelede}}", + "timeagoNbDaysAgo": "{count, plural, =1{{count} dag gelede} other{{count} dae gelede}}", + "timeagoNbWeeksAgo": "{count, plural, =1{{count} week gelede} other{{count} weke gelede}}", + "timeagoNbMonthsAgo": "{count, plural, =1{{count} maand gelede} other{{count} maande gelede}}", + "timeagoNbYearsAgo": "{count, plural, =1{{count} jaar gelede} other{{count} jare gelede}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{nog {count} minuut oor} other{nog {count} minute oor}}", + "timeagoNbHoursRemaining": "{count, plural, =1{nog {count} uur oor} other{nog {count} ure oor}}" } \ No newline at end of file diff --git a/lib/l10n/lila_ar.arb b/lib/l10n/lila_ar.arb index 80958d1ef6..70584812da 100644 --- a/lib/l10n/lila_ar.arb +++ b/lib/l10n/lila_ar.arb @@ -1511,5 +1511,23 @@ "studyNbChapters": "{count, plural, =0{{count} فصل} =1{{count} فصل} =2{فصلان} few{{count} فصول} many{{count} فصل} other{{count} فصول}}", "studyNbGames": "{count, plural, =0{{count} مباراة} =1{{count} مباراة} =2{مبارتان} few{{count} مبارايات} many{{count} مباراة} other{{count} مباراة}}", "studyNbMembers": "{count, plural, =0{{count} عضو} =1{{count} عضو} =2{{count} عضو} few{{count} عضو} many{{count} عضو} other{{count} أعضاء}}", - "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =0{ألصق نص PGN هنا، حتى {count} مباراة} =1{الصق نص الPGN هنا، حتى {count} لعبة واحدة} =2{ألصق نص PGN هنا، حتى {count} مباراة} few{ألصق نص PGN هنا، حتى {count} مباراة} many{ألصق نص PGN هنا، حتى {count} مباراة} other{الصق الPGN هنا، حتى {count} العاب}}" + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =0{ألصق نص PGN هنا، حتى {count} مباراة} =1{الصق نص الPGN هنا، حتى {count} لعبة واحدة} =2{ألصق نص PGN هنا، حتى {count} مباراة} few{ألصق نص PGN هنا، حتى {count} مباراة} many{ألصق نص PGN هنا، حتى {count} مباراة} other{الصق الPGN هنا، حتى {count} العاب}}", + "timeagoJustNow": "الان", + "timeagoRightNow": "حاليا", + "timeagoCompleted": "مكتمل", + "timeagoInNbSeconds": "{count, plural, =0{خلال {count} ثانية} =1{خلال ثانية} =2{خلال ثانيتين} few{خلال {count} ثوانٍ} many{خلال {count} ثانية} other{خلال {count} ثانية}}", + "timeagoInNbMinutes": "{count, plural, =0{خلال {count} دقيقة} =1{خلال دقيقة} =2{خلال {count} دقيقتين} few{خلال {count} دقائق} many{خلال {count} دقائق} other{خلال {count} دقائق}}", + "timeagoInNbHours": "{count, plural, =0{خلال {count} ساعة} =1{خلال ساعة} =2{خلال ساعتين} few{خلال {count} ساعات} many{خلال {count} ساعة} other{خلال {count} ساعة}}", + "timeagoInNbDays": "{count, plural, =0{خلال {count} يوم} =1{خلال {count} يوم} =2{خلال {count} يوم} few{خلال {count} أيام} many{خلال {count} يوم} other{خلال {count} يوم}}", + "timeagoInNbWeeks": "{count, plural, =0{خلال {count} أسبوع} =1{خلال {count} اسبوع} =2{خلال {count} اسبوع} few{خلال {count} اسبوع} many{خلال {count} اسبوع} other{خلال {count} اسبوع}}", + "timeagoInNbMonths": "{count, plural, =0{خلال {count} شهر} =1{خلال {count} شهر} =2{خلال {count} شهر} few{خلال {count} شهر} many{خلال {count} شهر} other{خلال {count} شهر}}", + "timeagoInNbYears": "{count, plural, =0{خلال {count} سنة} =1{خلال {count} سنة} =2{خلال {count} سنة} few{خلال {count} سنة} many{خلال {count} سنة} other{خلال {count} سنة}}", + "timeagoNbMinutesAgo": "{count, plural, =0{منذ {count} دقيقة/دقائق مضت} =1{منذ {count} دقيقة} =2{منذ {count} دقيقتين} few{منذ {count} دقيقة/دقائق مضت} many{منذ {count} دقيقة/دقائق مضت} other{منذ {count} دقيقة/دقائق مضت}}", + "timeagoNbHoursAgo": "{count, plural, =0{منذ {count} ساعة/ساعات مضت} =1{منذ {count} ساعة} =2{منذ {count} ساعة/ساعات مضت} few{منذ {count} ساعة/ساعات مضت} many{منذ {count} ساعة/ساعات مضت} other{منذ {count} ساعة/ساعات مضت}}", + "timeagoNbDaysAgo": "{count, plural, =0{منذ {count} يوم/أيام} =1{منذ {count} يوم} =2{منذ {count} يوم/أيام} few{منذ {count} يوم/أيام} many{منذ {count} يوم/أيام} other{منذ {count} يوم/أيام}}", + "timeagoNbWeeksAgo": "{count, plural, =0{منذ {count} أسابيع} =1{منذ {count} أسابيع} =2{منذ {count} أسابيع} few{منذ {count} أسابيع} many{منذ {count} أسابيع} other{منذ {count} أسابيع}}", + "timeagoNbMonthsAgo": "{count, plural, =0{منذ {count} شهر} =1{منذ {count} شهر} =2{منذ {count} شهر} few{منذ {count} شهر} many{منذ {count} شهر} other{منذ {count} شهر}}", + "timeagoNbYearsAgo": "{count, plural, =0{منذ {count} سنة} =1{منذ {count} سنة} =2{منذ {count} سنة} few{منذ {count} سنة} many{منذ {count} سنة} other{منذ {count} سنة}}", + "timeagoNbMinutesRemaining": "{count, plural, =0{{count}دقيقة متبقية} =1{{count}دقيقة متبقية} =2{{count}دقيقتان متبقيتان} few{{count}دقائق متبقية} many{{count}دقيقة متبقية} other{{count}دقائق متبقية}}", + "timeagoNbHoursRemaining": "{count, plural, =0{{count}ساعة متبقية} =1{{count}ساعة واحدة متبقية} =2{{count}ساعتان متبقيتان} few{{count} ساعات متبقية} many{{count}ساعة متبقية} other{{count}ساعة متبقية}}" } \ No newline at end of file diff --git a/lib/l10n/lila_az.arb b/lib/l10n/lila_az.arb index b2853ea2a5..70237b703b 100644 --- a/lib/l10n/lila_az.arb +++ b/lib/l10n/lila_az.arb @@ -1247,5 +1247,7 @@ "studyNbChapters": "{count, plural, =1{{count} Fəsil} other{{count} Fəsil}}", "studyNbGames": "{count, plural, =1{{count} Oyun} other{{count} Oyun}}", "studyNbMembers": "{count, plural, =1{{count} Üzv} other{{count} Üzv}}", - "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{PGN mətninizi bura yapışdırın, ən çox {count} oyuna qədər} other{PGN mətninizi bura yapışdırın, ən çox {count} oyuna qədər}}" + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{PGN mətninizi bura yapışdırın, ən çox {count} oyuna qədər} other{PGN mətninizi bura yapışdırın, ən çox {count} oyuna qədər}}", + "timeagoJustNow": "elə indi", + "timeagoRightNow": "indicə" } \ No newline at end of file diff --git a/lib/l10n/lila_be.arb b/lib/l10n/lila_be.arb index 4d3cd2c043..18dd49a2cf 100644 --- a/lib/l10n/lila_be.arb +++ b/lib/l10n/lila_be.arb @@ -1414,5 +1414,23 @@ "studyNbChapters": "{count, plural, =1{{count} раздзел} few{{count} раздзелы} many{{count} раздзелаў} other{{count} раздзелаў}}", "studyNbGames": "{count, plural, =1{{count} партыя} few{{count} партыі} many{{count} партый} other{{count} партый}}", "studyNbMembers": "{count, plural, =1{{count} удзельнік} few{{count} удзельніка} many{{count} удзельнікаў} other{{count} удзельнікаў}}", - "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Устаўце сюды ваш PGN тэкст, не больш за {count} гульню} few{Устаўце сюды ваш PGN тэкст, не больш за {count} гульні} many{Устаўце сюды ваш PGN тэкст, не больш за {count} гульняў} other{Устаўце сюды ваш PGN тэкст, не больш за {count} гульняў}}" + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Устаўце сюды ваш PGN тэкст, не больш за {count} гульню} few{Устаўце сюды ваш PGN тэкст, не больш за {count} гульні} many{Устаўце сюды ваш PGN тэкст, не больш за {count} гульняў} other{Устаўце сюды ваш PGN тэкст, не больш за {count} гульняў}}", + "timeagoJustNow": "зараз", + "timeagoRightNow": "прама зараз", + "timeagoCompleted": "завершана", + "timeagoInNbSeconds": "{count, plural, =1{праз {count} секунду} few{праз {count} секунды} many{праз {count} секунд} other{праз {count} секунд}}", + "timeagoInNbMinutes": "{count, plural, =1{праз {count} хвіліну} few{праз {count} хвіліны} many{праз {count} хвілін} other{праз {count} хвілін}}", + "timeagoInNbHours": "{count, plural, =1{праз {count} гадзіну} few{праз {count} гадзіны} many{праз {count} гадзін} other{праз {count} гадзін}}", + "timeagoInNbDays": "{count, plural, =1{праз {count} дзень} few{праз {count} дні} many{праз {count} дзён} other{праз {count} дзён}}", + "timeagoInNbWeeks": "{count, plural, =1{праз {count} тыдзень} few{праз {count} тыдні} many{праз {count} тыдняў} other{праз {count} тыдняў}}", + "timeagoInNbMonths": "{count, plural, =1{праз {count} месяц} few{праз {count} месяцы} many{праз {count} месяцаў} other{праз {count} месяцаў}}", + "timeagoInNbYears": "{count, plural, =1{праз {count} год} few{праз {count} гады} many{праз {count} гадоў} other{праз {count} гадоў}}", + "timeagoNbMinutesAgo": "{count, plural, =1{{count} хвіліну таму} few{{count} хвіліны таму} many{{count} хвілін таму} other{{count} хвілін таму}}", + "timeagoNbHoursAgo": "{count, plural, =1{{count} гадзіну таму} few{{count} гадзіны таму} many{{count} гадзін таму} other{{count} гадзін таму}}", + "timeagoNbDaysAgo": "{count, plural, =1{{count} дзень таму} few{{count} дні таму} many{{count} дзён таму} other{{count} дзён таму}}", + "timeagoNbWeeksAgo": "{count, plural, =1{{count} тыдзень таму} few{{count} тыдні таму} many{{count} тыдняў таму} other{{count} тыдняў таму}}", + "timeagoNbMonthsAgo": "{count, plural, =1{{count} месяц таму} few{{count} месяцы таму} many{{count} месяцаў таму} other{{count} месяцаў таму}}", + "timeagoNbYearsAgo": "{count, plural, =1{{count} год таму} few{{count} гады таму} many{{count} гадоў таму} other{{count} гадоў таму}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{Засталася {count} хвіліна} few{Засталося {count} хвіліны} many{Засталося {count} хвілін} other{Засталося {count} хвіліны}}", + "timeagoNbHoursRemaining": "{count, plural, =1{Засталася {count} гадзіна} few{Засталося {count} гадзіны} many{Засталося {count} гадзін} other{Засталося {count} гадзіны}}" } \ No newline at end of file diff --git a/lib/l10n/lila_bg.arb b/lib/l10n/lila_bg.arb index 8d6ebe5654..830aa781cb 100644 --- a/lib/l10n/lila_bg.arb +++ b/lib/l10n/lila_bg.arb @@ -1474,5 +1474,23 @@ "studyNbChapters": "{count, plural, =1{{count} Глава} other{{count} Глави}}", "studyNbGames": "{count, plural, =1{{count} Игра} other{{count} Игри}}", "studyNbMembers": "{count, plural, =1{{count} Член} other{{count} Членове}}", - "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Постави твоя PGN текст тук, до {count} партия} other{Постави твоя PGN текст тук, до {count} партии}}" + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Постави твоя PGN текст тук, до {count} партия} other{Постави твоя PGN текст тук, до {count} партии}}", + "timeagoJustNow": "току що", + "timeagoRightNow": "точно сега", + "timeagoCompleted": "завършено", + "timeagoInNbSeconds": "{count, plural, =1{след {count} секунда} other{след {count} секунди}}", + "timeagoInNbMinutes": "{count, plural, =1{след {count} минута} other{след {count} минути}}", + "timeagoInNbHours": "{count, plural, =1{след {count} час} other{след {count} часа}}", + "timeagoInNbDays": "{count, plural, =1{след {count} ден} other{след {count} дни}}", + "timeagoInNbWeeks": "{count, plural, =1{след {count} седмица} other{след {count} седмици}}", + "timeagoInNbMonths": "{count, plural, =1{след {count} месец} other{след {count} месеца}}", + "timeagoInNbYears": "{count, plural, =1{след {count} година} other{след {count} години}}", + "timeagoNbMinutesAgo": "{count, plural, =1{преди {count} минута} other{преди {count} минути}}", + "timeagoNbHoursAgo": "{count, plural, =1{преди {count} час} other{Преди {count} часа}}", + "timeagoNbDaysAgo": "{count, plural, =1{преди {count} ден} other{Преди {count} дни}}", + "timeagoNbWeeksAgo": "{count, plural, =1{преди {count} седмица} other{преди {count} седмици}}", + "timeagoNbMonthsAgo": "{count, plural, =1{преди {count} месец} other{преди {count} месеца}}", + "timeagoNbYearsAgo": "{count, plural, =1{преди {count} година} other{преди {count} години}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{остава {count} минутa} other{остават {count} минути}}", + "timeagoNbHoursRemaining": "{count, plural, =1{остава {count} час} other{остават {count} часа}}" } \ No newline at end of file diff --git a/lib/l10n/lila_bn.arb b/lib/l10n/lila_bn.arb index c1683f7564..f31b663c60 100644 --- a/lib/l10n/lila_bn.arb +++ b/lib/l10n/lila_bn.arb @@ -1264,5 +1264,23 @@ "studyNbChapters": "{count, plural, =1{{count}টি অধ্যায়} other{{count}টি অধ্যায়}}", "studyNbGames": "{count, plural, =1{{count}টি খেলা} other{{count}টি খেলা}}", "studyNbMembers": "{count, plural, =1{{count} জন সদস্য} other{{count} জন সদস্য}}", - "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{PGN টেক্সট এখানে পেস্ট করুন, {count} টি খেলা পর্যন্ত} other{PGN টেক্সট এখানে পেস্ট করুন, {count} টি খেলা পর্যন্ত}}" + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{PGN টেক্সট এখানে পেস্ট করুন, {count} টি খেলা পর্যন্ত} other{PGN টেক্সট এখানে পেস্ট করুন, {count} টি খেলা পর্যন্ত}}", + "timeagoJustNow": "এখনই", + "timeagoRightNow": "এই মুহূর্তে", + "timeagoCompleted": "সম্পন্ন হয়েছে", + "timeagoInNbSeconds": "{count, plural, =1{{count} সেকেন্ডের মধ্যে} other{{count} সেকেন্ডের মধ্যে}}", + "timeagoInNbMinutes": "{count, plural, =1{{count} মিনিটের মধ্যে} other{{count} মিনিটের মধ্যে}}", + "timeagoInNbHours": "{count, plural, =1{{count} ঘন্টার মধ্যে} other{{count} ঘন্টার মধ্যে}}", + "timeagoInNbDays": "{count, plural, =1{{count} দিনের মধ্যে} other{{count} দিনের মধ্যে}}", + "timeagoInNbWeeks": "{count, plural, =1{{count} সপ্তাহের মধ্যে} other{{count} সপ্তাহের মধ্যে}}", + "timeagoInNbMonths": "{count, plural, =1{{count} মাসের মধ্যে} other{{count} মাসের মধ্যে}}", + "timeagoInNbYears": "{count, plural, =1{{count} বছরের মধ্যে} other{{count} বছরের মধ্যে}}", + "timeagoNbMinutesAgo": "{count, plural, =1{{count} মিনিট আগে} other{{count} মিনিট আগে}}", + "timeagoNbHoursAgo": "{count, plural, =1{{count} ঘণ্টা আগে} other{{count} ঘন্টা আগে}}", + "timeagoNbDaysAgo": "{count, plural, =1{{count} দিন আগে} other{{count} দিন আগে}}", + "timeagoNbWeeksAgo": "{count, plural, =1{{count} সপ্তাহ আগে} other{{count} সপ্তাহ আগে}}", + "timeagoNbMonthsAgo": "{count, plural, =1{{count} মাস আগে} other{{count} মাস আগে}}", + "timeagoNbYearsAgo": "{count, plural, =1{{count} বছর আগে} other{{count} বছর আগে}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{{count} মিনিট বাকি} other{{count} মিনিট বাকি}}", + "timeagoNbHoursRemaining": "{count, plural, =1{{count} ঘন্টা বাকি} other{{count} ঘন্টা বাকি}}" } \ No newline at end of file diff --git a/lib/l10n/lila_br.arb b/lib/l10n/lila_br.arb index 2fc6fead20..e8fa88e269 100644 --- a/lib/l10n/lila_br.arb +++ b/lib/l10n/lila_br.arb @@ -1132,5 +1132,20 @@ "studyNbChapters": "{count, plural, =1{{count} pennad} =2{{count} pennad} few{{count} pennad} many{{count} pennad} other{{count} pennad}}", "studyNbGames": "{count, plural, =1{{count} C'hoariadenn} =2{{count} C'hoariadenn} few{{count} C'hoariadenn} many{{count} C'hoariadenn} other{{count} C'hoariadenn}}", "studyNbMembers": "{count, plural, =1{{count} Ezel} =2{{count} Ezel} few{{count} Ezel} many{{count} Ezel} other{{count} Ezel}}", - "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Pegit testenn ho PGN amañ, betek {count} krogad} =2{Pegit testenn ho PGN amañ, betek {count} grogad} few{Pegit testenn ho PGN amañ, betek {count} krogadoù} many{Pegit testenn ho PGN amañ, betek {count} krogadoù} other{Pegit testenn ho PGN amañ, betek {count} krogadoù}}" + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Pegit testenn ho PGN amañ, betek {count} krogad} =2{Pegit testenn ho PGN amañ, betek {count} grogad} few{Pegit testenn ho PGN amañ, betek {count} krogadoù} many{Pegit testenn ho PGN amañ, betek {count} krogadoù} other{Pegit testenn ho PGN amañ, betek {count} krogadoù}}", + "timeagoJustNow": "bremañ", + "timeagoRightNow": "bremañ", + "timeagoInNbSeconds": "{count, plural, =1{a-benn {count} eilenn} =2{a-benn {count} eilenn} few{a-benn {count} eilenn} many{a-benn {count} eilenn} other{a-benn {count} eilenn}}", + "timeagoInNbMinutes": "{count, plural, =1{a-benn {count} vunutenn} =2{a-benn {count} vunutenn} few{a-benn {count} munutenn} many{a-benn {count} munutenn} other{a-benn {count} munutenn}}", + "timeagoInNbHours": "{count, plural, =1{a-benn {count} eur} =2{a-benn {count} eur} few{a-benn {count} eur} many{a-benn {count} eur} other{a-benn {count} eur}}", + "timeagoInNbDays": "{count, plural, =1{a-benn {count} deiz} =2{a-benn {count} zeiz} few{a-benn {count} deiz} many{a-benn {count} deiz} other{a-benn {count} deiz}}", + "timeagoInNbWeeks": "{count, plural, =1{a-benn {count} sizhun} =2{a-benn {count} sizhun} few{a-benn {count} sizhun} many{a-benn {count} sizhun} other{a-benn {count} sizhun}}", + "timeagoInNbMonths": "{count, plural, =1{a-benn {count} miz} =2{a-benn {count} viz} few{a-benn {count} miz} many{a-benn {count} miz} other{a-benn {count} miz}}", + "timeagoInNbYears": "{count, plural, =1{a-benn {count} bloaz} =2{a-benn {count} vloaz} few{a-benn {count} bloaz} many{a-benn {count} bloaz} other{a-benn {count} bloaz}}", + "timeagoNbMinutesAgo": "{count, plural, =1{{count} vunutenn zo} =2{{count} vunutenn zo} few{{count} munutenn zo} many{{count} munutenn zo} other{{count} munutenn zo}}", + "timeagoNbHoursAgo": "{count, plural, =1{{count} eur zo} =2{{count} eur zo} few{{count} eur zo} many{{count} eur zo} other{{count} eur zo}}", + "timeagoNbDaysAgo": "{count, plural, =1{{count} deiz zo} =2{{count} zeiz zo} few{{count} deiz zo} many{{count} deiz zo} other{{count} deiz zo}}", + "timeagoNbWeeksAgo": "{count, plural, =1{{count} sizhun zo} =2{{count} sizhun zo} few{{count} sizhun zo} many{{count} sizhun zo} other{{count} sizhun zo}}", + "timeagoNbMonthsAgo": "{count, plural, =1{{count} miz zo} =2{{count} viz zo} few{{count} miz zo} many{{count} miz zo} other{{count} miz zo}}", + "timeagoNbYearsAgo": "{count, plural, =1{{count} bloaz zo} =2{{count} vloaz zo} few{{count} bloaz zo} many{{count} bloaz zo} other{{count} bloaz zo}}" } \ No newline at end of file diff --git a/lib/l10n/lila_bs.arb b/lib/l10n/lila_bs.arb index 924910ac29..e61b14484f 100644 --- a/lib/l10n/lila_bs.arb +++ b/lib/l10n/lila_bs.arb @@ -1401,5 +1401,20 @@ "studyNbChapters": "{count, plural, =1{{count} Poglavlje} few{{count} Poglavlja} other{{count} Poglavlja}}", "studyNbGames": "{count, plural, =1{{count} Partija} few{{count} Partije} other{{count} Partija}}", "studyNbMembers": "{count, plural, =1{{count} Član} few{{count} Člana} other{{count} Članova}}", - "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Ovdje zalijepite svoj PGN tekst, do {count} partije} few{Ovdje zalijepite svoj PGN tekst, do {count} partije} other{Ovdje zalijepite svoj PGN tekst, do {count} partija}}" + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Ovdje zalijepite svoj PGN tekst, do {count} partije} few{Ovdje zalijepite svoj PGN tekst, do {count} partije} other{Ovdje zalijepite svoj PGN tekst, do {count} partija}}", + "timeagoJustNow": "upravo sada", + "timeagoRightNow": "upravo sada", + "timeagoInNbSeconds": "{count, plural, =1{za {count} sekundu} few{za {count} sekunde} other{za {count} sekundi}}", + "timeagoInNbMinutes": "{count, plural, =1{za {count} minutu} few{za {count} minute} other{za {count} minuta}}", + "timeagoInNbHours": "{count, plural, =1{za {count} sat} few{za {count} sata} other{za {count} sati}}", + "timeagoInNbDays": "{count, plural, =1{za {count} dan} few{za {count} dana} other{za {count} dana}}", + "timeagoInNbWeeks": "{count, plural, =1{za {count} sedmicu} few{za {count} sedmice} other{za {count} sedmica}}", + "timeagoInNbMonths": "{count, plural, =1{za {count} mjesec} few{za {count} mjeseca} other{za {count} mjeseci}}", + "timeagoInNbYears": "{count, plural, =1{za {count} godinu} few{za {count} godine} other{za {count} godina}}", + "timeagoNbMinutesAgo": "{count, plural, =1{prije {count} minutu} few{prije {count} minute} other{prije {count} minuta}}", + "timeagoNbHoursAgo": "{count, plural, =1{prije {count} sat} few{prije {count} sata} other{prije {count} sati}}", + "timeagoNbDaysAgo": "{count, plural, =1{prije {count} dan} few{prije {count} dana} other{prije {count} dana}}", + "timeagoNbWeeksAgo": "{count, plural, =1{prije {count} sedmicu} few{prije {count} sedmice} other{prije {count} sedmica}}", + "timeagoNbMonthsAgo": "{count, plural, =1{prije {count} mjesec} few{prije {count} mjeseca} other{prije {count} mjeseci}}", + "timeagoNbYearsAgo": "{count, plural, =1{prije {count} godinu} few{prije {count} godine} other{prije {count} godina}}" } \ No newline at end of file diff --git a/lib/l10n/lila_ca.arb b/lib/l10n/lila_ca.arb index 4d891e003b..6687a5cefd 100644 --- a/lib/l10n/lila_ca.arb +++ b/lib/l10n/lila_ca.arb @@ -1543,5 +1543,23 @@ "studyNbChapters": "{count, plural, =1{{count} Capítol} other{{count} Capítols}}", "studyNbGames": "{count, plural, =1{{count} Joc} other{{count} Jocs}}", "studyNbMembers": "{count, plural, =1{{count} Membre} other{{count} Membres}}", - "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Enganxa el teu PGN aquí, fins a {count} partida} other{Enganxa el teu PGN aquí, fins a {count} partides}}" + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Enganxa el teu PGN aquí, fins a {count} partida} other{Enganxa el teu PGN aquí, fins a {count} partides}}", + "timeagoJustNow": "ara mateix", + "timeagoRightNow": "ara mateix", + "timeagoCompleted": "completat", + "timeagoInNbSeconds": "{count, plural, =1{en {count} segon} other{en {count} segons}}", + "timeagoInNbMinutes": "{count, plural, =1{en {count} minut} other{en {count} minuts}}", + "timeagoInNbHours": "{count, plural, =1{en {count} hora} other{en {count} hores}}", + "timeagoInNbDays": "{count, plural, =1{en {count} dia} other{en {count} dies}}", + "timeagoInNbWeeks": "{count, plural, =1{en {count} setmana} other{en {count} setmanes}}", + "timeagoInNbMonths": "{count, plural, =1{en {count} mes} other{en {count} mesos}}", + "timeagoInNbYears": "{count, plural, =1{en {count} any} other{en {count} anys}}", + "timeagoNbMinutesAgo": "{count, plural, =1{fa {count} minut} other{fa {count} minuts}}", + "timeagoNbHoursAgo": "{count, plural, =1{fa {count} hora} other{fa {count} hores}}", + "timeagoNbDaysAgo": "{count, plural, =1{fa {count} dia} other{fa {count} dies}}", + "timeagoNbWeeksAgo": "{count, plural, =1{fa {count} setmana} other{fa {count} setmanes}}", + "timeagoNbMonthsAgo": "{count, plural, =1{fa {count} mes} other{fa {count} mesos}}", + "timeagoNbYearsAgo": "{count, plural, =1{fa {count} any} other{fa {count} anys}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{Queda {count} minut} other{Queden {count} minuts}}", + "timeagoNbHoursRemaining": "{count, plural, =1{Queda {count} hora} other{Queden {count} hores}}" } \ No newline at end of file diff --git a/lib/l10n/lila_cs.arb b/lib/l10n/lila_cs.arb index f5e4043905..2448c873ee 100644 --- a/lib/l10n/lila_cs.arb +++ b/lib/l10n/lila_cs.arb @@ -1513,5 +1513,23 @@ "studyNbChapters": "{count, plural, =1{{count} kapitola} few{{count} kapitoly} many{{count} kapitol} other{{count} kapitol}}", "studyNbGames": "{count, plural, =1{{count} hra} few{{count} hry} many{{count} her} other{{count} her}}", "studyNbMembers": "{count, plural, =1{{count} člen} few{{count} členi} many{{count} členů} other{{count} členů}}", - "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Vložte obsah vašeho PGN souboru (až {count} hra)} few{Vložte obsah vašeho PGN souboru (až {count} hry)} many{Vložte obsah vašeho PGN souboru (až {count} her)} other{Vložte obsah vašeho PGN souboru (až {count} her)}}" + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Vložte obsah vašeho PGN souboru (až {count} hra)} few{Vložte obsah vašeho PGN souboru (až {count} hry)} many{Vložte obsah vašeho PGN souboru (až {count} her)} other{Vložte obsah vašeho PGN souboru (až {count} her)}}", + "timeagoJustNow": "právě teď", + "timeagoRightNow": "právě teď", + "timeagoCompleted": "dokončeno", + "timeagoInNbSeconds": "{count, plural, =1{za {count} sekundu} few{za {count} sekundy} many{za {count} sekund} other{za {count} sekund}}", + "timeagoInNbMinutes": "{count, plural, =1{za {count} minutu} few{za {count} minuty} many{za {count} minut} other{za {count} minut}}", + "timeagoInNbHours": "{count, plural, =1{za {count} hodinu} few{za {count} hodiny} many{za {count} hodin} other{za {count} hodin}}", + "timeagoInNbDays": "{count, plural, =1{za {count} den} few{za {count} dny} many{za {count} dnů} other{za {count} dnů}}", + "timeagoInNbWeeks": "{count, plural, =1{za {count} týden} few{za {count} týdny} many{za {count} týdnů} other{za {count} týdnů}}", + "timeagoInNbMonths": "{count, plural, =1{za {count} měsíc} few{za {count} měsíce} many{za {count} měsíců} other{za {count} měsíců}}", + "timeagoInNbYears": "{count, plural, =1{za {count} rok} few{za {count} roky} many{za {count} let} other{za {count} let}}", + "timeagoNbMinutesAgo": "{count, plural, =1{před {count} minutou} few{před {count} minutami} many{před {count} minutami} other{před {count} minutami}}", + "timeagoNbHoursAgo": "{count, plural, =1{před {count} hodinou} few{před {count} hodinami} many{před {count} hodinami} other{před {count} hodinami}}", + "timeagoNbDaysAgo": "{count, plural, =1{před {count} dnem} few{před {count} dny} many{před {count} dny} other{před {count} dny}}", + "timeagoNbWeeksAgo": "{count, plural, =1{před {count} týdnem} few{před {count} týdny} many{před {count} týdny} other{před {count} týdny}}", + "timeagoNbMonthsAgo": "{count, plural, =1{před {count} měsícem} few{před {count} měsíci} many{před {count} měsíci} other{před {count} měsíci}}", + "timeagoNbYearsAgo": "{count, plural, =1{před {count} rokem} few{před {count} lety} many{před {count} lety} other{před {count} lety}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{Zbývá {count} minuta} few{Zbývají {count} minuty} many{Zbývá {count} minut} other{Zbývá {count} minut}}", + "timeagoNbHoursRemaining": "{count, plural, =1{Zbývá {count} hodina} few{Zbývají {count} hodiny} many{Zbývá {count} hodin} other{Zbývá {count} hodin}}" } \ No newline at end of file diff --git a/lib/l10n/lila_da.arb b/lib/l10n/lila_da.arb index 53ce39ecb3..a36e9c7030 100644 --- a/lib/l10n/lila_da.arb +++ b/lib/l10n/lila_da.arb @@ -1547,5 +1547,23 @@ "studyNbChapters": "{count, plural, =1{{count} kapitel} other{{count} kapitler}}", "studyNbGames": "{count, plural, =1{{count} parti} other{{count} partier}}", "studyNbMembers": "{count, plural, =1{{count} Medlem} other{{count} Medlemmer}}", - "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Indsæt din PGN-tekst her, op til {count} parti} other{Indsæt din PGN-tekst her, op til {count} partier}}" + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Indsæt din PGN-tekst her, op til {count} parti} other{Indsæt din PGN-tekst her, op til {count} partier}}", + "timeagoJustNow": "for lidt siden", + "timeagoRightNow": "netop nu", + "timeagoCompleted": "afsluttet", + "timeagoInNbSeconds": "{count, plural, =1{om {count} sekund} other{om {count} sekunder}}", + "timeagoInNbMinutes": "{count, plural, =1{om {count} minut} other{om {count} minutter}}", + "timeagoInNbHours": "{count, plural, =1{om {count} time} other{om {count} timer}}", + "timeagoInNbDays": "{count, plural, =1{om {count} dag} other{om {count} dage}}", + "timeagoInNbWeeks": "{count, plural, =1{om {count} uge} other{om {count} uger}}", + "timeagoInNbMonths": "{count, plural, =1{om {count} måned} other{om {count} måneder}}", + "timeagoInNbYears": "{count, plural, =1{om {count} år} other{om {count} år}}", + "timeagoNbMinutesAgo": "{count, plural, =1{{count} minut siden} other{{count} minutter siden}}", + "timeagoNbHoursAgo": "{count, plural, =1{{count} time siden} other{{count} timer siden}}", + "timeagoNbDaysAgo": "{count, plural, =1{{count} dag siden} other{{count} dage siden}}", + "timeagoNbWeeksAgo": "{count, plural, =1{{count} uge siden} other{{count} uger siden}}", + "timeagoNbMonthsAgo": "{count, plural, =1{{count} måned siden} other{{count} måneder siden}}", + "timeagoNbYearsAgo": "{count, plural, =1{{count} år siden} other{{count} år siden}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{{count} minut tilbage} other{{count} minutter tilbage}}", + "timeagoNbHoursRemaining": "{count, plural, =1{{count} time tilbage} other{{count} timer tilbage}}" } \ No newline at end of file diff --git a/lib/l10n/lila_de.arb b/lib/l10n/lila_de.arb index e3d2a3b0d4..7c6c1262b7 100644 --- a/lib/l10n/lila_de.arb +++ b/lib/l10n/lila_de.arb @@ -1547,5 +1547,23 @@ "studyNbChapters": "{count, plural, =1{{count} Kapitel} other{{count} Kapitel}}", "studyNbGames": "{count, plural, =1{{count} Partie} other{{count} Partien}}", "studyNbMembers": "{count, plural, =1{{count} Mitglied} other{{count} Mitglieder}}", - "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Füge deinen PGN Text hier ein, bis zu {count} Partie} other{Füge dein PGN Text hier ein, bis zu {count} Partien}}" + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Füge deinen PGN Text hier ein, bis zu {count} Partie} other{Füge dein PGN Text hier ein, bis zu {count} Partien}}", + "timeagoJustNow": "gerade eben", + "timeagoRightNow": "gerade jetzt", + "timeagoCompleted": "abgeschlossen", + "timeagoInNbSeconds": "{count, plural, =1{in {count} Sekunde} other{in {count} Sekunden}}", + "timeagoInNbMinutes": "{count, plural, =1{in {count} Minute} other{in {count} Minuten}}", + "timeagoInNbHours": "{count, plural, =1{in {count} Stunde} other{in {count} Stunden}}", + "timeagoInNbDays": "{count, plural, =1{in {count} Tag} other{in {count} Tagen}}", + "timeagoInNbWeeks": "{count, plural, =1{in {count} Woche} other{in {count} Wochen}}", + "timeagoInNbMonths": "{count, plural, =1{in {count} Monat} other{in {count} Monaten}}", + "timeagoInNbYears": "{count, plural, =1{in {count} Jahr} other{in {count} Jahren}}", + "timeagoNbMinutesAgo": "{count, plural, =1{vor {count} Minute} other{vor {count} Minuten}}", + "timeagoNbHoursAgo": "{count, plural, =1{vor {count} Stunde} other{vor {count} Stunden}}", + "timeagoNbDaysAgo": "{count, plural, =1{vor {count} Tag} other{vor {count} Tagen}}", + "timeagoNbWeeksAgo": "{count, plural, =1{vor {count} Woche} other{vor {count} Wochen}}", + "timeagoNbMonthsAgo": "{count, plural, =1{vor {count} Monat} other{vor {count} Monaten}}", + "timeagoNbYearsAgo": "{count, plural, =1{vor {count} Jahr} other{vor {count} Jahren}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{{count} Minute verbleibend} other{{count} Minuten verbleibend}}", + "timeagoNbHoursRemaining": "{count, plural, =1{{count} Stunde verbleiben} other{{count} Stunden verbleiben}}" } \ No newline at end of file diff --git a/lib/l10n/lila_el.arb b/lib/l10n/lila_el.arb index 880c8bc1f6..4c45237e95 100644 --- a/lib/l10n/lila_el.arb +++ b/lib/l10n/lila_el.arb @@ -1534,5 +1534,23 @@ "studyNbChapters": "{count, plural, =1{{count} Κεφάλαιο} other{{count} Κεφάλαια}}", "studyNbGames": "{count, plural, =1{{count} Παρτίδα} other{{count} Παρτίδες}}", "studyNbMembers": "{count, plural, =1{{count} Μέλος} other{{count} Μέλη}}", - "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Επικολλήστε το PGN εδώ, μέχρι {count} παρτίδα} other{Επικολλήστε το PGN εδώ, μέχρι {count} παρτίδες}}" + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Επικολλήστε το PGN εδώ, μέχρι {count} παρτίδα} other{Επικολλήστε το PGN εδώ, μέχρι {count} παρτίδες}}", + "timeagoJustNow": "μόλις τώρα", + "timeagoRightNow": "αυτή τη στιγμή", + "timeagoCompleted": "ολοκληρώθηκε", + "timeagoInNbSeconds": "{count, plural, =1{σε {count} δευτερόλεπτο} other{σε {count} δευτερόλεπτα}}", + "timeagoInNbMinutes": "{count, plural, =1{σε {count} λεπτό} other{σε {count} λεπτά}}", + "timeagoInNbHours": "{count, plural, =1{σε {count} ώρα} other{σε {count} ώρες}}", + "timeagoInNbDays": "{count, plural, =1{σε {count} ημέρα} other{σε {count} ημέρες}}", + "timeagoInNbWeeks": "{count, plural, =1{σε {count} εβδομάδα} other{σε {count} εβδομάδες}}", + "timeagoInNbMonths": "{count, plural, =1{σε {count} μήνα} other{σε {count} μήνες}}", + "timeagoInNbYears": "{count, plural, =1{σε {count} έτος} other{{count} έτη}}", + "timeagoNbMinutesAgo": "{count, plural, =1{{count} λεπτό πριν} other{{count} λεπτά πριν}}", + "timeagoNbHoursAgo": "{count, plural, =1{{count} ώρα πριν} other{{count} ώρες πριν}}", + "timeagoNbDaysAgo": "{count, plural, =1{{count} μέρα πριν} other{{count} ημέρες πριν}}", + "timeagoNbWeeksAgo": "{count, plural, =1{{count} εβδομάδα πριν} other{{count} εβδομάδες πριν}}", + "timeagoNbMonthsAgo": "{count, plural, =1{{count} μήνα πριν} other{{count} μήνες πριν}}", + "timeagoNbYearsAgo": "{count, plural, =1{{count} χρόνο πριν} other{{count} χρόνια πριν}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{απομένει {count} λεπτό} other{απομένουν {count} λεπτά}}", + "timeagoNbHoursRemaining": "{count, plural, =1{απομένει {count} ώρα} other{απομένουν {count} ώρες}}" } \ No newline at end of file diff --git a/lib/l10n/lila_en_US.arb b/lib/l10n/lila_en_US.arb index ce5bd7af84..b4a9aaf79c 100644 --- a/lib/l10n/lila_en_US.arb +++ b/lib/l10n/lila_en_US.arb @@ -1547,5 +1547,23 @@ "studyNbChapters": "{count, plural, =1{{count} Chapter} other{{count} Chapters}}", "studyNbGames": "{count, plural, =1{{count} Game} other{{count} Games}}", "studyNbMembers": "{count, plural, =1{{count} Member} other{{count} Members}}", - "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Paste your PGN text here, up to {count} game} other{Paste your PGN text here, up to {count} games}}" + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Paste your PGN text here, up to {count} game} other{Paste your PGN text here, up to {count} games}}", + "timeagoJustNow": "just now", + "timeagoRightNow": "right now", + "timeagoCompleted": "completed", + "timeagoInNbSeconds": "{count, plural, =1{in {count} second} other{in {count} seconds}}", + "timeagoInNbMinutes": "{count, plural, =1{in {count} minute} other{in {count} minutes}}", + "timeagoInNbHours": "{count, plural, =1{in {count} hour} other{in {count} hours}}", + "timeagoInNbDays": "{count, plural, =1{in {count} day} other{in {count} days}}", + "timeagoInNbWeeks": "{count, plural, =1{in {count} week} other{in {count} weeks}}", + "timeagoInNbMonths": "{count, plural, =1{in {count} month} other{in {count} months}}", + "timeagoInNbYears": "{count, plural, =1{in {count} year} other{in {count} years}}", + "timeagoNbMinutesAgo": "{count, plural, =1{{count} minute ago} other{{count} minutes ago}}", + "timeagoNbHoursAgo": "{count, plural, =1{{count} hour ago} other{{count} hours ago}}", + "timeagoNbDaysAgo": "{count, plural, =1{{count} day ago} other{{count} days ago}}", + "timeagoNbWeeksAgo": "{count, plural, =1{{count} week ago} other{{count} weeks ago}}", + "timeagoNbMonthsAgo": "{count, plural, =1{{count} month ago} other{{count} months ago}}", + "timeagoNbYearsAgo": "{count, plural, =1{{count} year ago} other{{count} years ago}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{{count} minute remaining} other{{count} minutes remaining}}", + "timeagoNbHoursRemaining": "{count, plural, =1{{count} hour remaining} other{{count} hours remaining}}" } \ No newline at end of file diff --git a/lib/l10n/lila_eo.arb b/lib/l10n/lila_eo.arb index ebd0a91db6..49b4fac67a 100644 --- a/lib/l10n/lila_eo.arb +++ b/lib/l10n/lila_eo.arb @@ -1464,5 +1464,23 @@ "studyNbChapters": "{count, plural, =1{{count} Ĉapitro} other{{count} Ĉapitroj}}", "studyNbGames": "{count, plural, =1{{count} Ludo} other{{count} Ludoj}}", "studyNbMembers": "{count, plural, =1{{count} Membro} other{{count} Membroj}}", - "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Algluu ĉi tie vian PGN kodon, maksimume ĝis {count} ludo} other{Algluu ĉi tie vian PGN kodon, ĝis maksimume {count} ludoj}}" + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Algluu ĉi tie vian PGN kodon, maksimume ĝis {count} ludo} other{Algluu ĉi tie vian PGN kodon, ĝis maksimume {count} ludoj}}", + "timeagoJustNow": "ĵus nun", + "timeagoRightNow": "ĵuse", + "timeagoCompleted": "finita", + "timeagoInNbSeconds": "{count, plural, =1{en {count} sekundo} other{en {count} sekundoj}}", + "timeagoInNbMinutes": "{count, plural, =1{en {count} minuto} other{en {count} minutoj}}", + "timeagoInNbHours": "{count, plural, =1{en {count} horo} other{en {count} horoj}}", + "timeagoInNbDays": "{count, plural, =1{en {count} tago} other{en {count} tagoj}}", + "timeagoInNbWeeks": "{count, plural, =1{en {count} semajno} other{en {count} semajnoj}}", + "timeagoInNbMonths": "{count, plural, =1{en {count} monato} other{en {count} monatoj}}", + "timeagoInNbYears": "{count, plural, =1{en {count} jaro} other{en {count} jaroj}}", + "timeagoNbMinutesAgo": "{count, plural, =1{antaŭ {count} minuto} other{antaŭ {count} minutoj}}", + "timeagoNbHoursAgo": "{count, plural, =1{antaŭ {count} horo} other{antaŭ {count} horoj}}", + "timeagoNbDaysAgo": "{count, plural, =1{antaŭ {count} tago} other{antaŭ {count} tagoj}}", + "timeagoNbWeeksAgo": "{count, plural, =1{antaŭ {count} semajno} other{antaŭ {count} semajnoj}}", + "timeagoNbMonthsAgo": "{count, plural, =1{antaŭ {count} monato} other{antaŭ {count} monatoj}}", + "timeagoNbYearsAgo": "{count, plural, =1{antaŭ {count} jaro} other{antaŭ {count} jaroj}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{{count} minuto restas} other{{count} minutoj restas}}", + "timeagoNbHoursRemaining": "{count, plural, =1{{count} horo restas} other{{count} horoj restas}}" } \ No newline at end of file diff --git a/lib/l10n/lila_es.arb b/lib/l10n/lila_es.arb index a6405e015d..f6d132be07 100644 --- a/lib/l10n/lila_es.arb +++ b/lib/l10n/lila_es.arb @@ -1547,5 +1547,23 @@ "studyNbChapters": "{count, plural, =1{{count} Capítulo} other{{count} Capítulos}}", "studyNbGames": "{count, plural, =1{{count} Partida} other{{count} Partidas}}", "studyNbMembers": "{count, plural, =1{{count} Miembro} other{{count} Miembros}}", - "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Pega aquí el código PGN, {count} partida como máximo} other{Pega aquí el código PGN, {count} partidas como máximo}}" + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Pega aquí el código PGN, {count} partida como máximo} other{Pega aquí el código PGN, {count} partidas como máximo}}", + "timeagoJustNow": "ahora mismo", + "timeagoRightNow": "ahora mismo", + "timeagoCompleted": "completado", + "timeagoInNbSeconds": "{count, plural, =1{en {count} segundo} other{en {count} segundos}}", + "timeagoInNbMinutes": "{count, plural, =1{en {count} minuto} other{en {count} minutos}}", + "timeagoInNbHours": "{count, plural, =1{en {count} hora} other{en {count} horas}}", + "timeagoInNbDays": "{count, plural, =1{en {count} día} other{en {count} días}}", + "timeagoInNbWeeks": "{count, plural, =1{en {count} semana} other{en {count} semanas}}", + "timeagoInNbMonths": "{count, plural, =1{en {count} mes} other{en {count} meses}}", + "timeagoInNbYears": "{count, plural, =1{en {count} año} other{en {count} años}}", + "timeagoNbMinutesAgo": "{count, plural, =1{hace {count} minuto} other{hace {count} minutos}}", + "timeagoNbHoursAgo": "{count, plural, =1{hace {count} hora} other{hace {count} horas}}", + "timeagoNbDaysAgo": "{count, plural, =1{hace {count} día} other{hace {count} días}}", + "timeagoNbWeeksAgo": "{count, plural, =1{hace {count} semana} other{hace {count} semanas}}", + "timeagoNbMonthsAgo": "{count, plural, =1{hace {count} mes} other{hace {count} meses}}", + "timeagoNbYearsAgo": "{count, plural, =1{hace {count} año} other{hace {count} años}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{{count} minutos restantes} other{{count} minutos restantes}}", + "timeagoNbHoursRemaining": "{count, plural, =1{{count} horas restantes} other{{count} horas restantes}}" } \ No newline at end of file diff --git a/lib/l10n/lila_et.arb b/lib/l10n/lila_et.arb index c301ed2780..c2e6886858 100644 --- a/lib/l10n/lila_et.arb +++ b/lib/l10n/lila_et.arb @@ -1329,5 +1329,23 @@ "studyNbChapters": "{count, plural, =1{{count} peatükk} other{{count} peatükki}}", "studyNbGames": "{count, plural, =1{{count} mäng} other{{count} mängu}}", "studyNbMembers": "{count, plural, =1{{count} liige} other{{count} liiget}}", - "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Aseta oma PGN tekst siia, kuni {count} mäng} other{Aseta oma PGN tekst siia, kuni {count} mängu}}" + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Aseta oma PGN tekst siia, kuni {count} mäng} other{Aseta oma PGN tekst siia, kuni {count} mängu}}", + "timeagoJustNow": "äsja", + "timeagoRightNow": "praegu", + "timeagoCompleted": "lõppenud", + "timeagoInNbSeconds": "{count, plural, =1{{count} sekundi pärast} other{{count} sekundi pärast}}", + "timeagoInNbMinutes": "{count, plural, =1{{count} minuti pärast} other{{count} minuti pärast}}", + "timeagoInNbHours": "{count, plural, =1{{count} tunni pärast} other{{count} tunni pärast}}", + "timeagoInNbDays": "{count, plural, =1{{count} päeva pärast} other{{count} päeva pärast}}", + "timeagoInNbWeeks": "{count, plural, =1{{count} nädala pärast} other{{count} nädala pärast}}", + "timeagoInNbMonths": "{count, plural, =1{{count} kuu pärast} other{{count} kuu pärast}}", + "timeagoInNbYears": "{count, plural, =1{{count} aasta pärast} other{{count} aasta pärast}}", + "timeagoNbMinutesAgo": "{count, plural, =1{{count} minut tagasi} other{{count} minutit tagasi}}", + "timeagoNbHoursAgo": "{count, plural, =1{{count} tund tagasi} other{{count} tundi tagasi}}", + "timeagoNbDaysAgo": "{count, plural, =1{{count} päev tagasi} other{{count} päeva tagasi}}", + "timeagoNbWeeksAgo": "{count, plural, =1{{count} nädal tagasi} other{{count} nädalat tagasi}}", + "timeagoNbMonthsAgo": "{count, plural, =1{{count} kuu tagasi} other{{count} kuud tagasi}}", + "timeagoNbYearsAgo": "{count, plural, =1{{count} aasta tagasi} other{{count} aastat tagasi}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{{count} minut jäänud} other{{count} minutit jäänud}}", + "timeagoNbHoursRemaining": "{count, plural, =1{{count} tund jäänud} other{{count} tundi jäänud}}" } \ No newline at end of file diff --git a/lib/l10n/lila_eu.arb b/lib/l10n/lila_eu.arb index 4f706e77ed..8234578b92 100644 --- a/lib/l10n/lila_eu.arb +++ b/lib/l10n/lila_eu.arb @@ -1547,5 +1547,23 @@ "studyNbChapters": "{count, plural, =1{Kapitulu {count}} other{{count} kapitulu}}", "studyNbGames": "{count, plural, =1{Partida {count}} other{{count} partida}}", "studyNbMembers": "{count, plural, =1{Kide {count}} other{{count} kide}}", - "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Itsatsi hemen zure PGNa, gehienez partida {count}} other{Itsatsi hemen zure PGNa, gehienez {count} partida}}" + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Itsatsi hemen zure PGNa, gehienez partida {count}} other{Itsatsi hemen zure PGNa, gehienez {count} partida}}", + "timeagoJustNow": "orain", + "timeagoRightNow": "orain", + "timeagoCompleted": "amaituta", + "timeagoInNbSeconds": "{count, plural, =1{segundo {count}en} other{{count} segundotan}}", + "timeagoInNbMinutes": "{count, plural, =1{minutu {count}en} other{{count} minututan}}", + "timeagoInNbHours": "{count, plural, =1{ordu {count}en} other{{count} ordutan}}", + "timeagoInNbDays": "{count, plural, =1{egun {count}en} other{{count} egunetan}}", + "timeagoInNbWeeks": "{count, plural, =1{aste {count}en} other{{count} egunetan}}", + "timeagoInNbMonths": "{count, plural, =1{hilabete {count}en} other{{count} hilabetetan}}", + "timeagoInNbYears": "{count, plural, =1{urte {count}en} other{{count} urtetan}}", + "timeagoNbMinutesAgo": "{count, plural, =1{orain dela minutu {count}} other{orain dela {count} minutu}}", + "timeagoNbHoursAgo": "{count, plural, =1{orain dela ordu {count}} other{orain dela {count} ordu}}", + "timeagoNbDaysAgo": "{count, plural, =1{orain dela egun {count}} other{orain dela {count} egun}}", + "timeagoNbWeeksAgo": "{count, plural, =1{orain dela aste {count}} other{orain dela {count} aste}}", + "timeagoNbMonthsAgo": "{count, plural, =1{orain dela hilabete {count}} other{orain dela {count} hilabete}}", + "timeagoNbYearsAgo": "{count, plural, =1{orain dela urte {count}} other{orain dela {count} urte}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{Minutu {count} falta da} other{{count} minutu falta dira}}", + "timeagoNbHoursRemaining": "{count, plural, =1{Ordu {count} falta da} other{{count} ordu falta dira}}" } \ No newline at end of file diff --git a/lib/l10n/lila_fa.arb b/lib/l10n/lila_fa.arb index 14c9eb2aa4..a88acf4766 100644 --- a/lib/l10n/lila_fa.arb +++ b/lib/l10n/lila_fa.arb @@ -1546,5 +1546,23 @@ "studyNbChapters": "{count, plural, =1{{count} بخش} other{{count} بخش}}", "studyNbGames": "{count, plural, =1{{count} بازی} other{{count} بازی}}", "studyNbMembers": "{count, plural, =1{{count} عضو} other{{count} عضو}}", - "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{متن PGN خود را در اینجا بچسبانید، تا {count} بازی} other{متن PGN خود را در اینجا بچسبانید، تا {count} بازی}}" + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{متن PGN خود را در اینجا بچسبانید، تا {count} بازی} other{متن PGN خود را در اینجا بچسبانید، تا {count} بازی}}", + "timeagoJustNow": "چند لحظه پیش", + "timeagoRightNow": "هم‌اکنون", + "timeagoCompleted": "کامل شده", + "timeagoInNbSeconds": "{count, plural, =1{تا {count} ثانیهٔ دیگر} other{تا {count} ثانیهٔ دیگر}}", + "timeagoInNbMinutes": "{count, plural, =1{تا {count} دقیقه دیگر} other{تا {count} دقیقه دیگر}}", + "timeagoInNbHours": "{count, plural, =1{تا {count} ساعت دیگر} other{تا {count} ساعت دیگر}}", + "timeagoInNbDays": "{count, plural, =1{تا {count} روز دیگر} other{تا {count} روز دیگر}}", + "timeagoInNbWeeks": "{count, plural, =1{تا {count} هفته دیگر} other{تا {count} هفته دیگر}}", + "timeagoInNbMonths": "{count, plural, =1{تا {count} ماه دیگر} other{تا {count} ماه دیگر}}", + "timeagoInNbYears": "{count, plural, =1{تا {count} سال دیگر} other{تا {count} سال دیگر}}", + "timeagoNbMinutesAgo": "{count, plural, =1{{count} دقیقه پیش} other{{count} دقیقه پیش}}", + "timeagoNbHoursAgo": "{count, plural, =1{{count} ساعت پیش} other{{count} ساعت پیش}}", + "timeagoNbDaysAgo": "{count, plural, =1{{count} روز پیش} other{{count} روز پیش}}", + "timeagoNbWeeksAgo": "{count, plural, =1{{count} هفته پیش} other{{count} هفته پیش}}", + "timeagoNbMonthsAgo": "{count, plural, =1{{count} ماه پیش} other{{count} ماه پیش}}", + "timeagoNbYearsAgo": "{count, plural, =1{{count} سال پیش} other{{count} سال پیش}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{{count} دقیقه باقی مانده} other{{count} دقیقه باقی مانده}}", + "timeagoNbHoursRemaining": "{count, plural, =1{{count} ساعت باقی مانده} other{{count} ساعت باقی مانده}}" } \ No newline at end of file diff --git a/lib/l10n/lila_fi.arb b/lib/l10n/lila_fi.arb index 4faba1b965..0e7ea3eda2 100644 --- a/lib/l10n/lila_fi.arb +++ b/lib/l10n/lila_fi.arb @@ -1547,5 +1547,23 @@ "studyNbChapters": "{count, plural, =1{{count} luku} other{{count} lukua}}", "studyNbGames": "{count, plural, =1{{count} peli} other{{count} peliä}}", "studyNbMembers": "{count, plural, =1{{count} jäsen} other{{count} jäsentä}}", - "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Liitä PGN tähän, enintään {count} peli} other{Liitä PGN tähän, enintään {count} peliä}}" + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Liitä PGN tähän, enintään {count} peli} other{Liitä PGN tähän, enintään {count} peliä}}", + "timeagoJustNow": "juuri äsken", + "timeagoRightNow": "juuri nyt", + "timeagoCompleted": "suoritettu", + "timeagoInNbSeconds": "{count, plural, =1{{count} sekunnin kuluttua} other{{count} sekunnin kuluttua}}", + "timeagoInNbMinutes": "{count, plural, =1{{count} minuutin kuluttua} other{{count} minuutin kuluttua}}", + "timeagoInNbHours": "{count, plural, =1{{count} tunnin kuluttua} other{{count} tunnin kuluttua}}", + "timeagoInNbDays": "{count, plural, =1{{count} päivän kuluttua} other{{count} päivän kuluttua}}", + "timeagoInNbWeeks": "{count, plural, =1{{count} viikon kuluttua} other{{count} viikon kuluttua}}", + "timeagoInNbMonths": "{count, plural, =1{{count} kuukauden kuluttua} other{{count} kuukauden kuluttua}}", + "timeagoInNbYears": "{count, plural, =1{{count} vuoden kuluttua} other{{count} vuoden kuluttua}}", + "timeagoNbMinutesAgo": "{count, plural, =1{{count} minuutti sitten} other{{count} minuuttia sitten}}", + "timeagoNbHoursAgo": "{count, plural, =1{{count} tunti sitten} other{{count} tuntia sitten}}", + "timeagoNbDaysAgo": "{count, plural, =1{{count} päivä sitten} other{{count} päivää sitten}}", + "timeagoNbWeeksAgo": "{count, plural, =1{{count} viikko sitten} other{{count} viikkoa sitten}}", + "timeagoNbMonthsAgo": "{count, plural, =1{{count} kuukausi sitten} other{{count} kuukautta sitten}}", + "timeagoNbYearsAgo": "{count, plural, =1{{count} vuosi sitten} other{{count} vuotta sitten}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{{count} minuutti jäljellä} other{{count} minuuttia jäljellä}}", + "timeagoNbHoursRemaining": "{count, plural, =1{{count} tunti jäljellä} other{{count} tuntia jäljellä}}" } \ No newline at end of file diff --git a/lib/l10n/lila_fr.arb b/lib/l10n/lila_fr.arb index 5095de4a54..117a364366 100644 --- a/lib/l10n/lila_fr.arb +++ b/lib/l10n/lila_fr.arb @@ -1547,5 +1547,23 @@ "studyNbChapters": "{count, plural, =1{{count} chapitre} other{{count} chapitres}}", "studyNbGames": "{count, plural, =1{{count} partie} other{{count} parties}}", "studyNbMembers": "{count, plural, =1{{count} membre} other{{count} membres}}", - "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Collez votre texte PGN ici, jusqu'à {count} partie} other{Collez votre texte PGN ici, jusqu'à {count} parties}}" + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Collez votre texte PGN ici, jusqu'à {count} partie} other{Collez votre texte PGN ici, jusqu'à {count} parties}}", + "timeagoJustNow": "Maintenant", + "timeagoRightNow": "à l'instant", + "timeagoCompleted": "terminé", + "timeagoInNbSeconds": "{count, plural, =1{dans {count} seconde} other{dans {count} secondes}}", + "timeagoInNbMinutes": "{count, plural, =1{dans {count} minute} other{dans {count} minutes}}", + "timeagoInNbHours": "{count, plural, =1{dans {count} heure} other{dans {count} heures}}", + "timeagoInNbDays": "{count, plural, =1{dans {count} jour} other{dans {count} jours}}", + "timeagoInNbWeeks": "{count, plural, =1{dans {count} semaine} other{dans {count} semaines}}", + "timeagoInNbMonths": "{count, plural, =1{dans {count} mois} other{dans {count} mois}}", + "timeagoInNbYears": "{count, plural, =1{dans {count} an} other{dans {count} ans}}", + "timeagoNbMinutesAgo": "{count, plural, =1{il y a {count} minute} other{il y a {count} minutes}}", + "timeagoNbHoursAgo": "{count, plural, =1{il y a {count} heure} other{il y a {count} heures}}", + "timeagoNbDaysAgo": "{count, plural, =1{il y a {count} jour} other{il y a {count} jours}}", + "timeagoNbWeeksAgo": "{count, plural, =1{il y a {count} semaine} other{il y a {count} semaines}}", + "timeagoNbMonthsAgo": "{count, plural, =1{il y a {count} mois} other{il y a {count} mois}}", + "timeagoNbYearsAgo": "{count, plural, =1{il y a {count} an} other{il y a {count} ans}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{{count} minute restante} other{{count} minutes restantes}}", + "timeagoNbHoursRemaining": "{count, plural, =1{{count} heure restante} other{{count} heures restantes}}" } \ No newline at end of file diff --git a/lib/l10n/lila_ga.arb b/lib/l10n/lila_ga.arb index 432fa0743d..3c6678e295 100644 --- a/lib/l10n/lila_ga.arb +++ b/lib/l10n/lila_ga.arb @@ -1372,5 +1372,20 @@ "studyNbChapters": "{count, plural, =1{{count} Caibidil} =2{{count} Chaibidil} few{{count} gCaibidil} many{{count} Caibidil} other{{count} Caibidil}}", "studyNbGames": "{count, plural, =1{{count} Cluiche} =2{{count} Chluiche} few{{count} gCluiche} many{{count} Cluiche} other{{count} Cluiche}}", "studyNbMembers": "{count, plural, =1{{count} Comhalta} =2{{count} Chomhalta} few{{count} gComhalta} many{{count} Comhalta} other{{count} Comhalta}}", - "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Greamaigh do théacs PGN anseo, suas le {count} cluiche} =2{Greamaigh do théacs PGN anseo, suas le {count} chluiche} few{Greamaigh do théacs PGN anseo, suas le {count} gcluiche} many{Greamaigh do théacs PGN anseo, suas le {count} cluiche} other{Greamaigh do théacs PGN anseo, suas le {count} cluiche}}" + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Greamaigh do théacs PGN anseo, suas le {count} cluiche} =2{Greamaigh do théacs PGN anseo, suas le {count} chluiche} few{Greamaigh do théacs PGN anseo, suas le {count} gcluiche} many{Greamaigh do théacs PGN anseo, suas le {count} cluiche} other{Greamaigh do théacs PGN anseo, suas le {count} cluiche}}", + "timeagoJustNow": "díreach anois", + "timeagoRightNow": "anois", + "timeagoInNbSeconds": "{count, plural, =1{i soicind amháín} =2{i {count} soicind} few{i {count} soicind} many{i {count} soicind} other{i {count} soicind}}", + "timeagoInNbMinutes": "{count, plural, =1{i nóiméad amháin} =2{i {count} nóiméad} few{i {count} nóiméad} many{i {count} nóiméad} other{i {count} nóiméad}}", + "timeagoInNbHours": "{count, plural, =1{in uair amháin} =2{i {count} uair} few{i {count} uair} many{i {count} uair} other{i {count} uair}}", + "timeagoInNbDays": "{count, plural, =1{i lá amháín} =2{i {count} lá} few{i {count} lá} many{i {count} lá} other{i {count} lá}}", + "timeagoInNbWeeks": "{count, plural, =1{i seachtain amháin} =2{i {count} sheachtain} few{i {count} seachtain} many{i {count} seachtain} other{i {count} seachtain}}", + "timeagoInNbMonths": "{count, plural, =1{i gceann míosa} =2{i {count} mhí} few{i {count} mhí} many{i {count} mí} other{i {count} mí}}", + "timeagoInNbYears": "{count, plural, =1{i gceann bliana} =2{i gceann {count} bhliain} few{i gceann {count} bhliain} many{i gceann {count} mbliain} other{i gceann {count} bliain}}", + "timeagoNbMinutesAgo": "{count, plural, =1{nóiméad amháin ó shin} =2{{count} nóiméad ó shin} few{{count} nóiméad ó shin} many{{count} nóiméad ó shin} other{{count} nóiméad ó shin}}", + "timeagoNbHoursAgo": "{count, plural, =1{uair amháin ó shin} =2{{count} uair ó shin} few{{count} uair ó shin} many{{count} uair ó shin} other{{count} uair ó shin}}", + "timeagoNbDaysAgo": "{count, plural, =1{lá amháin ó shin} =2{{count} lá ó shin} few{{count} lá ó shin} many{{count} lá ó shin} other{{count} lá ó shin}}", + "timeagoNbWeeksAgo": "{count, plural, =1{seachtain amháin ó shin} =2{{count} sheachtain ó shin} few{{count} sheachtain ó shin} many{{count} seachtain ó shin} other{{count} seachtain ó shin}}", + "timeagoNbMonthsAgo": "{count, plural, =1{mí amháin ó shin} =2{{count} mhí ó shin} few{{count} mí ó shin} many{{count} mí ó shin} other{{count} mí ó shin}}", + "timeagoNbYearsAgo": "{count, plural, =1{Bliain amháin ó shin} =2{{count} bhliain ó shin} few{{count} mbliain ó shin} many{{count} mbliain ó shin} other{{count} bliain ó shin}}" } \ No newline at end of file diff --git a/lib/l10n/lila_gl.arb b/lib/l10n/lila_gl.arb index 8f233c76ed..8ddee708e8 100644 --- a/lib/l10n/lila_gl.arb +++ b/lib/l10n/lila_gl.arb @@ -1547,5 +1547,23 @@ "studyNbChapters": "{count, plural, =1{{count} Capítulo} other{{count} Capítulos}}", "studyNbGames": "{count, plural, =1{{count} Partida} other{{count} Partidas}}", "studyNbMembers": "{count, plural, =1{{count} Membro} other{{count} Membros}}", - "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Pega o teu texto PGN aquí, ata {count} partida} other{Pega o teu texto PGN aquí, ata {count} partidas}}" + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Pega o teu texto PGN aquí, ata {count} partida} other{Pega o teu texto PGN aquí, ata {count} partidas}}", + "timeagoJustNow": "agora mesmo", + "timeagoRightNow": "agora", + "timeagoCompleted": "completado", + "timeagoInNbSeconds": "{count, plural, =1{en {count} segundo} other{en {count} segundos}}", + "timeagoInNbMinutes": "{count, plural, =1{en {count} minuto} other{en {count} minutos}}", + "timeagoInNbHours": "{count, plural, =1{en {count} hora} other{en {count} horas}}", + "timeagoInNbDays": "{count, plural, =1{en {count} día} other{en {count} días}}", + "timeagoInNbWeeks": "{count, plural, =1{en {count} semana} other{en {count} semanas}}", + "timeagoInNbMonths": "{count, plural, =1{en {count} mes} other{en {count} meses}}", + "timeagoInNbYears": "{count, plural, =1{en {count} ano} other{en {count} anos}}", + "timeagoNbMinutesAgo": "{count, plural, =1{Hai {count} minuto} other{Hai {count} minutos}}", + "timeagoNbHoursAgo": "{count, plural, =1{Hai {count} hora} other{Hai {count} horas}}", + "timeagoNbDaysAgo": "{count, plural, =1{Hai {count} día} other{Hai {count} días}}", + "timeagoNbWeeksAgo": "{count, plural, =1{Hai {count} semana} other{Hai {count} semanas}}", + "timeagoNbMonthsAgo": "{count, plural, =1{Hai {count} mes} other{Hai {count} meses}}", + "timeagoNbYearsAgo": "{count, plural, =1{Hai {count} ano} other{Hai {count} anos}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{{count} minuto restante} other{{count} minutos restantes}}", + "timeagoNbHoursRemaining": "{count, plural, =1{{count} hora restante} other{{count} horas restantes}}" } \ No newline at end of file diff --git a/lib/l10n/lila_gsw.arb b/lib/l10n/lila_gsw.arb index 8958162c02..95e7e08ea3 100644 --- a/lib/l10n/lila_gsw.arb +++ b/lib/l10n/lila_gsw.arb @@ -1547,5 +1547,23 @@ "studyNbChapters": "{count, plural, =1{{count} Kapitel} other{{count} Kapitäl}}", "studyNbGames": "{count, plural, =1{{count} Schpiel} other{{count} Schpiel}}", "studyNbMembers": "{count, plural, =1{{count} Mitglid} other{{count} Mitglider}}", - "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Füeg din PGN Tegscht da i, bis zu {count} Schpiel} other{Füeg din PGN Tegscht da i, bis zu {count} Schpiel}}" + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Füeg din PGN Tegscht da i, bis zu {count} Schpiel} other{Füeg din PGN Tegscht da i, bis zu {count} Schpiel}}", + "timeagoJustNow": "grad jetzt", + "timeagoRightNow": "genau jetzt", + "timeagoCompleted": "beändet", + "timeagoInNbSeconds": "{count, plural, =1{i {count} Sekunde} other{i {count} Sekunde}}", + "timeagoInNbMinutes": "{count, plural, =1{in {count} Minute} other{in {count} Minute}}", + "timeagoInNbHours": "{count, plural, =1{i {count} Schtund} other{i {count} Schtunde}}", + "timeagoInNbDays": "{count, plural, =1{i {count} Tag} other{i {count} Täg}}", + "timeagoInNbWeeks": "{count, plural, =1{i {count} Wuche} other{i {count} Wuche}}", + "timeagoInNbMonths": "{count, plural, =1{i {count} Monet} other{i {count} Mönet}}", + "timeagoInNbYears": "{count, plural, =1{i {count} Jahr} other{i {count} Jahr}}", + "timeagoNbMinutesAgo": "{count, plural, =1{vor {count} Minute} other{vor {count} Minute}}", + "timeagoNbHoursAgo": "{count, plural, =1{vor {count} Schtund} other{vor {count} Schtunde}}", + "timeagoNbDaysAgo": "{count, plural, =1{vor {count} Tag} other{vor {count} Täg}}", + "timeagoNbWeeksAgo": "{count, plural, =1{vor {count} Wuche} other{vor {count} Wuche}}", + "timeagoNbMonthsAgo": "{count, plural, =1{vor {count} Monet} other{vor {count} Mönet}}", + "timeagoNbYearsAgo": "{count, plural, =1{vor {count} Jahr} other{vor {count} Jahr}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{{count} Minute blibt} other{{count} Minute blibed}}", + "timeagoNbHoursRemaining": "{count, plural, =1{{count} Schtund blibt} other{{count} Schtunde blibed}}" } \ No newline at end of file diff --git a/lib/l10n/lila_he.arb b/lib/l10n/lila_he.arb index e817e5ae6d..7463e7342a 100644 --- a/lib/l10n/lila_he.arb +++ b/lib/l10n/lila_he.arb @@ -1547,5 +1547,23 @@ "studyNbChapters": "{count, plural, =1{פרק {count}} =2{{count} פרקים} many{{count} פרקים} other{{count} פרקים}}", "studyNbGames": "{count, plural, =1{{count} משחק} =2{{count} משחקים} many{{count} משחקים} other{{count} משחקים}}", "studyNbMembers": "{count, plural, =1{משתמש אחד} =2{{count} משתמשים} many{{count} משתמשים} other{{count} משתמשים}}", - "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{הדבק את טקסט הPGN שלך כאן, עד למשחק {count}} =2{הדבק את טקסט הPGN שלך כאן, עד ל{count} משחקים} many{הדבק את טקסט הPGN שלך כאן, עד ל{count} משחקים} other{הדבק את טקסט הPGN שלך כאן, עד ל{count} משחקים}}" + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{הדבק את טקסט הPGN שלך כאן, עד למשחק {count}} =2{הדבק את טקסט הPGN שלך כאן, עד ל{count} משחקים} many{הדבק את טקסט הPGN שלך כאן, עד ל{count} משחקים} other{הדבק את טקסט הPGN שלך כאן, עד ל{count} משחקים}}", + "timeagoJustNow": "בדיוק עכשיו", + "timeagoRightNow": "עכשיו", + "timeagoCompleted": "הושלם", + "timeagoInNbSeconds": "{count, plural, =1{עוד שנייה} =2{עוד {count} שניות} many{עוד {count} שניות} other{עוד {count} שניות}}", + "timeagoInNbMinutes": "{count, plural, =1{עוד דקה {count}} =2{עוד {count} דקות} many{עוד {count} דקות} other{עוד {count} דקות}}", + "timeagoInNbHours": "{count, plural, =1{עוד שעה {count}} =2{עוד {count} שעות} many{עוד {count} שעות} other{עוד {count} שעות}}", + "timeagoInNbDays": "{count, plural, =1{עוד יום {count}} =2{עוד {count} ימים} many{עוד {count} ימים} other{עוד {count} ימים}}", + "timeagoInNbWeeks": "{count, plural, =1{עוד שבוע {count}} =2{עוד {count} שבועות} many{עוד {count} שבועות} other{עוד {count} שבועות}}", + "timeagoInNbMonths": "{count, plural, =1{עוד חודש {count}} =2{עוד {count} חודשים} many{עוד {count} חודשים} other{עוד {count} חודשים}}", + "timeagoInNbYears": "{count, plural, =1{עוד שנה {count}} =2{עוד {count} שנים} many{עוד {count} שנים} other{עוד {count} שנים}}", + "timeagoNbMinutesAgo": "{count, plural, =1{לפני דקה {count}} =2{לפני {count} דקות} many{לפני {count} דקות} other{לפני {count} דקות}}", + "timeagoNbHoursAgo": "{count, plural, =1{לפני שעה {count}} =2{לפני {count} שעות} many{לפני {count} שעות} other{לפני {count} שעות}}", + "timeagoNbDaysAgo": "{count, plural, =1{לפני יום {count}} =2{לפני {count} ימים} many{לפני {count} ימים} other{לפני {count} ימים}}", + "timeagoNbWeeksAgo": "{count, plural, =1{לפני שבוע {count}} =2{לפני {count} שבועות} many{לפני {count} שבועות} other{לפני {count} שבועות}}", + "timeagoNbMonthsAgo": "{count, plural, =1{לפני חודש {count}} =2{לפני {count} חודשים} many{לפני {count} חודשים} other{לפני {count} חודשים}}", + "timeagoNbYearsAgo": "{count, plural, =1{לפני שנה {count}} =2{לפני {count} שנים} many{לפני {count} שנים} other{לפני {count} שנים}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{דקה {count} נותרה} =2{{count} דקות נותרו} many{{count} דקות נותרו} other{{count} דקות נותרו}}", + "timeagoNbHoursRemaining": "{count, plural, =1{שעה {count} נותרה} =2{{count} שעות נותרו} many{{count} שעות נותרו} other{{count} שעות נותרו}}" } \ No newline at end of file diff --git a/lib/l10n/lila_hi.arb b/lib/l10n/lila_hi.arb index b1f19b914f..e6f08b2986 100644 --- a/lib/l10n/lila_hi.arb +++ b/lib/l10n/lila_hi.arb @@ -1385,5 +1385,23 @@ "studyNbChapters": "{count, plural, =1{{count} अध्याय} other{{count} अध्याय}}", "studyNbGames": "{count, plural, =1{{count} खेल} other{{count} खेल}}", "studyNbMembers": "{count, plural, =1{{count} सदस्य} other{{count} सदस्य}}", - "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{यहां अपना PGN टेक्स्ट डाले,{count} खेल तक} other{यहां अपना PGN टेक्स्ट डाले,{count} खेल तक}}" + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{यहां अपना PGN टेक्स्ट डाले,{count} खेल तक} other{यहां अपना PGN टेक्स्ट डाले,{count} खेल तक}}", + "timeagoJustNow": "अभी", + "timeagoRightNow": "अभी", + "timeagoCompleted": "पूर्ण", + "timeagoInNbSeconds": "{count, plural, =1{{count} सेकेंड में} other{{count} सेकंड में}}", + "timeagoInNbMinutes": "{count, plural, =1{{count} मिनट में} other{{count} मिनट में}}", + "timeagoInNbHours": "{count, plural, =1{{count} घंटों में} other{{count} घंटों में}}", + "timeagoInNbDays": "{count, plural, =1{{count} दिन में} other{{count} दिनो में}}", + "timeagoInNbWeeks": "{count, plural, =1{{count} हफ़्ते में} other{{count} हफ़्तों में}}", + "timeagoInNbMonths": "{count, plural, =1{{count} महीने बाद​} other{{count} महीनो में}}", + "timeagoInNbYears": "{count, plural, =1{{count} साल में} other{{count} सालों में}}", + "timeagoNbMinutesAgo": "{count, plural, =1{{count} मिनट पहले} other{{count} मिनटों पहले}}", + "timeagoNbHoursAgo": "{count, plural, =1{{count} घंटे पहले} other{{count} घंटे पहले}}", + "timeagoNbDaysAgo": "{count, plural, =1{{count} दिन पहले} other{{count} दिनों पहले}}", + "timeagoNbWeeksAgo": "{count, plural, =1{{count} सप्ताह पहले} other{{count} सप्ताह पहले}}", + "timeagoNbMonthsAgo": "{count, plural, =1{{count} महीने पहले} other{{count} महीने पहले}}", + "timeagoNbYearsAgo": "{count, plural, =1{{count} वर्ष पहले} other{{count} वर्षों पहले}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{{count} मिनट बचा है} other{{count} मिनट बचे हैं}}", + "timeagoNbHoursRemaining": "{count, plural, =1{{count} घंटा बचा है} other{{count} घंटे बचे हैं}}" } \ No newline at end of file diff --git a/lib/l10n/lila_hr.arb b/lib/l10n/lila_hr.arb index c40eddef64..172a33e533 100644 --- a/lib/l10n/lila_hr.arb +++ b/lib/l10n/lila_hr.arb @@ -1397,5 +1397,21 @@ "studyNbChapters": "{count, plural, =1{{count} Poglavlje} few{{count} Poglavlja} other{{count} Poglavlja}}", "studyNbGames": "{count, plural, =1{{count} Partija} few{{count} Partije} other{{count} Partije}}", "studyNbMembers": "{count, plural, =1{{count} Član} few{{count} Član} other{{count} Članova}}", - "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Ovdje zalijepite svoj PGN tekst, do {count} igre} few{Ovdje zalijepite svoj PGN tekst, do {count} igri} other{Ovdje zalijepite svoj PGN tekst, do {count} igara}}" + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Ovdje zalijepite svoj PGN tekst, do {count} igre} few{Ovdje zalijepite svoj PGN tekst, do {count} igri} other{Ovdje zalijepite svoj PGN tekst, do {count} igara}}", + "timeagoJustNow": "upravo sada", + "timeagoRightNow": "upravo sada", + "timeagoCompleted": "završeno", + "timeagoInNbSeconds": "{count, plural, =1{za {count} sekunda} few{za {count} sekundi} other{za {count} sekunda}}", + "timeagoInNbMinutes": "{count, plural, =1{za {count} minutu} few{za {count} minute} other{za {count} minuta}}", + "timeagoInNbHours": "{count, plural, =1{za {count} sat} few{za {count} sata} other{za {count} sati}}", + "timeagoInNbDays": "{count, plural, =1{za {count} dan} few{za {count} dana} other{za {count} dana}}", + "timeagoInNbWeeks": "{count, plural, =1{za {count} tjedan} few{za {count} tjedna} other{za {count} tjedana}}", + "timeagoInNbMonths": "{count, plural, =1{za {count} mjesec} few{za {count} mjeseca} other{za {count} mjeseci}}", + "timeagoInNbYears": "{count, plural, =1{za {count} godinu} few{za {count} godine} other{za {count} godina}}", + "timeagoNbMinutesAgo": "{count, plural, =1{prije {count} minutu} few{prije {count} minute} other{prije {count} minuta}}", + "timeagoNbHoursAgo": "{count, plural, =1{prije {count} sat} few{prije {count} sata} other{prije {count} sati}}", + "timeagoNbDaysAgo": "{count, plural, =1{prije {count} dan} few{prije {count} dana} other{prije {count} dana}}", + "timeagoNbWeeksAgo": "{count, plural, =1{prije {count} tjedan} few{prije {count} tjedna} other{prije {count} tjedna}}", + "timeagoNbMonthsAgo": "{count, plural, =1{prije {count} mjesec} few{prije {count} mjeseca} other{prije {count} mjeseci}}", + "timeagoNbYearsAgo": "{count, plural, =1{prije {count} godinu} few{prije {count} godine} other{prije {count} godina}}" } \ No newline at end of file diff --git a/lib/l10n/lila_hu.arb b/lib/l10n/lila_hu.arb index 99a9e341b7..e16ee6540f 100644 --- a/lib/l10n/lila_hu.arb +++ b/lib/l10n/lila_hu.arb @@ -1467,5 +1467,23 @@ "studyNbChapters": "{count, plural, =1{{count} Fejezet} other{{count} Fejezet}}", "studyNbGames": "{count, plural, =1{{count} Játszma} other{{count} Játszma}}", "studyNbMembers": "{count, plural, =1{{count} Tag} other{{count} Tag}}", - "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Illeszd be a PGN szövegét legfeljebb {count} játszmáig} other{Illeszd be a PGN szövegét (legfeljebb {count} játszma)}}" + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Illeszd be a PGN szövegét legfeljebb {count} játszmáig} other{Illeszd be a PGN szövegét (legfeljebb {count} játszma)}}", + "timeagoJustNow": "épp most", + "timeagoRightNow": "épp most", + "timeagoCompleted": "befejeződött", + "timeagoInNbSeconds": "{count, plural, =1{{count} másodperc múlva} other{{count} másodperc múlva}}", + "timeagoInNbMinutes": "{count, plural, =1{{count} perc múlva} other{{count} perc múlva}}", + "timeagoInNbHours": "{count, plural, =1{{count} óra múlva} other{{count} óra múlva}}", + "timeagoInNbDays": "{count, plural, =1{{count} nap múlva} other{{count} nap múlva}}", + "timeagoInNbWeeks": "{count, plural, =1{{count} hét múlva} other{{count} hét múlva}}", + "timeagoInNbMonths": "{count, plural, =1{{count} hónap múlva} other{{count} hónap múlva}}", + "timeagoInNbYears": "{count, plural, =1{{count} év múlva} other{{count} év múlva}}", + "timeagoNbMinutesAgo": "{count, plural, =1{{count} perce} other{{count} perce}}", + "timeagoNbHoursAgo": "{count, plural, =1{{count} órája} other{{count} órája}}", + "timeagoNbDaysAgo": "{count, plural, =1{{count} napja} other{{count} napja}}", + "timeagoNbWeeksAgo": "{count, plural, =1{{count} hete} other{{count} hete}}", + "timeagoNbMonthsAgo": "{count, plural, =1{{count} hónapja} other{{count} hónapja}}", + "timeagoNbYearsAgo": "{count, plural, =1{{count} éve} other{{count} éve}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{{count} perc van hátra} other{{count} perc van hátra}}", + "timeagoNbHoursRemaining": "{count, plural, =1{{count} óra van hátra} other{{count} óra van hátra}}" } \ No newline at end of file diff --git a/lib/l10n/lila_hy.arb b/lib/l10n/lila_hy.arb index 03b1c78625..517a41f5ae 100644 --- a/lib/l10n/lila_hy.arb +++ b/lib/l10n/lila_hy.arb @@ -1379,5 +1379,21 @@ "studyNbChapters": "{count, plural, =1{{count} գլուխ} other{{count} գլուխ}}", "studyNbGames": "{count, plural, =1{{count} պարտիա} other{{count} պարտիա}}", "studyNbMembers": "{count, plural, =1{{count} մասնակից} other{{count} մասնակից}}", - "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Տեղադրեք տեսքտը PGN ձևաչափով, {count} պարտիայից ոչ ավելի} other{Տեղադրեք տեսքտը PGN ձևաչափով, {count} պարտիայից ոչ ավելի}}" + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Տեղադրեք տեսքտը PGN ձևաչափով, {count} պարտիայից ոչ ավելի} other{Տեղադրեք տեսքտը PGN ձևաչափով, {count} պարտիայից ոչ ավելի}}", + "timeagoJustNow": "հենց հիմա", + "timeagoRightNow": "հենց հիմա", + "timeagoCompleted": "ավարտված", + "timeagoInNbSeconds": "{count, plural, =1{{count} վայրկյան հետո} other{{count} վայրկյաններ հետո}}", + "timeagoInNbMinutes": "{count, plural, =1{{count} րոպե հետո} other{{count} րոպեներ հետո}}", + "timeagoInNbHours": "{count, plural, =1{{count} ժամ հետո} other{{count} ժամ հետո}}", + "timeagoInNbDays": "{count, plural, =1{{count} օր հետո} other{{count} օր հետո}}", + "timeagoInNbWeeks": "{count, plural, =1{{count} շաբաթ հետո} other{{count} շաբաթ հետո}}", + "timeagoInNbMonths": "{count, plural, =1{{count} ամիս հետո} other{{count} ամիս հետո}}", + "timeagoInNbYears": "{count, plural, =1{{count} տարի հետո} other{{count} տարի հետո}}", + "timeagoNbMinutesAgo": "{count, plural, =1{{count} րոպե առաջ} other{{count} րոպե առաջ}}", + "timeagoNbHoursAgo": "{count, plural, =1{{count} ժամ առաջ} other{{count} ժամ առաջ}}", + "timeagoNbDaysAgo": "{count, plural, =1{{count} օր առաջ} other{{count} օր առաջ}}", + "timeagoNbWeeksAgo": "{count, plural, =1{{count} շաբաթ առաջ} other{{count} շաբաթ առաջ}}", + "timeagoNbMonthsAgo": "{count, plural, =1{{count} ամիս առաջ} other{{count} ամիս առաջ}}", + "timeagoNbYearsAgo": "{count, plural, =1{{count} տարի առաջ} other{{count} տարի առաջ}}" } \ No newline at end of file diff --git a/lib/l10n/lila_id.arb b/lib/l10n/lila_id.arb index d3eb0ab68b..d8179e729a 100644 --- a/lib/l10n/lila_id.arb +++ b/lib/l10n/lila_id.arb @@ -1422,5 +1422,23 @@ "studyNbChapters": "{count, plural, other{{count} Bab}}", "studyNbGames": "{count, plural, other{{count} Permainan}}", "studyNbMembers": "{count, plural, other{{count} Anggota}}", - "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, other{Tempelkan PGN kamu disini, lebih dari {count} permainan}}" + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, other{Tempelkan PGN kamu disini, lebih dari {count} permainan}}", + "timeagoJustNow": "baru saja", + "timeagoRightNow": "sekarang", + "timeagoCompleted": "telah selesai", + "timeagoInNbSeconds": "{count, plural, other{dalam {count} detik}}", + "timeagoInNbMinutes": "{count, plural, other{dalam {count} menit}}", + "timeagoInNbHours": "{count, plural, other{dalam {count} jam}}", + "timeagoInNbDays": "{count, plural, other{dalam {count} hari}}", + "timeagoInNbWeeks": "{count, plural, other{dalam {count} minggu}}", + "timeagoInNbMonths": "{count, plural, other{dalam {count} bulan}}", + "timeagoInNbYears": "{count, plural, other{dalam {count} tahun}}", + "timeagoNbMinutesAgo": "{count, plural, other{{count} menit yang lalu}}", + "timeagoNbHoursAgo": "{count, plural, other{{count} jam yang lalu}}", + "timeagoNbDaysAgo": "{count, plural, other{{count} hari yang lalu}}", + "timeagoNbWeeksAgo": "{count, plural, other{{count} minggu yang lalu}}", + "timeagoNbMonthsAgo": "{count, plural, other{{count} bulan yang lalu}}", + "timeagoNbYearsAgo": "{count, plural, other{{count} tahun yang lalu}}", + "timeagoNbMinutesRemaining": "{count, plural, other{{count} menit tersisa}}", + "timeagoNbHoursRemaining": "{count, plural, other{{count} jam tersisa}}" } \ No newline at end of file diff --git a/lib/l10n/lila_it.arb b/lib/l10n/lila_it.arb index 12900b6999..0f01dfa7ff 100644 --- a/lib/l10n/lila_it.arb +++ b/lib/l10n/lila_it.arb @@ -1547,5 +1547,23 @@ "studyNbChapters": "{count, plural, =1{{count} capitolo} other{{count} capitoli}}", "studyNbGames": "{count, plural, =1{{count} partita} other{{count} partite}}", "studyNbMembers": "{count, plural, =1{{count} membro} other{{count} membri}}", - "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Incolla qui il testo PGN, massimo {count} partita} other{Incolla qui i testi PGN, massimo {count} partite}}" + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Incolla qui il testo PGN, massimo {count} partita} other{Incolla qui i testi PGN, massimo {count} partite}}", + "timeagoJustNow": "adesso", + "timeagoRightNow": "adesso", + "timeagoCompleted": "completato", + "timeagoInNbSeconds": "{count, plural, =1{tra {count} secondo} other{tra {count} secondi}}", + "timeagoInNbMinutes": "{count, plural, =1{tra {count} minuto} other{tra {count} minuti}}", + "timeagoInNbHours": "{count, plural, =1{tra {count} ora} other{tra {count} ore}}", + "timeagoInNbDays": "{count, plural, =1{tra {count} giorno} other{tra {count} giorni}}", + "timeagoInNbWeeks": "{count, plural, =1{tra {count} settimana} other{tra {count} settimane}}", + "timeagoInNbMonths": "{count, plural, =1{tra {count} mese} other{tra {count} mesi}}", + "timeagoInNbYears": "{count, plural, =1{tra {count} anno} other{tra {count} anni}}", + "timeagoNbMinutesAgo": "{count, plural, =1{{count} minuto fa} other{{count} minuti fa}}", + "timeagoNbHoursAgo": "{count, plural, =1{{count} ora fa} other{{count} ore fa}}", + "timeagoNbDaysAgo": "{count, plural, =1{{count} giorno fa} other{{count} giorni fa}}", + "timeagoNbWeeksAgo": "{count, plural, =1{{count} settimana fa} other{{count} settimane fa}}", + "timeagoNbMonthsAgo": "{count, plural, =1{{count} mese fa} other{{count} mesi fa}}", + "timeagoNbYearsAgo": "{count, plural, =1{{count} anno fa} other{{count} anni fa}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{{count} minuto rimanente} other{{count} minuti rimanenti}}", + "timeagoNbHoursRemaining": "{count, plural, =1{{count} ora rimanente} other{{count} ore rimanenti}}" } \ No newline at end of file diff --git a/lib/l10n/lila_ja.arb b/lib/l10n/lila_ja.arb index c22e797a86..2754e0ec51 100644 --- a/lib/l10n/lila_ja.arb +++ b/lib/l10n/lila_ja.arb @@ -1544,5 +1544,23 @@ "studyNbChapters": "{count, plural, other{{count} 章}}", "studyNbGames": "{count, plural, other{{count} 局}}", "studyNbMembers": "{count, plural, other{{count} メンバー}}", - "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, other{ここに PGN をペースト({count} 局まで)}}" + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, other{ここに PGN をペースト({count} 局まで)}}", + "timeagoJustNow": "たった今", + "timeagoRightNow": "たった今", + "timeagoCompleted": "完了", + "timeagoInNbSeconds": "{count, plural, other{{count} 秒後}}", + "timeagoInNbMinutes": "{count, plural, other{{count} 分後}}", + "timeagoInNbHours": "{count, plural, other{{count} 時間後}}", + "timeagoInNbDays": "{count, plural, other{{count} 日後}}", + "timeagoInNbWeeks": "{count, plural, other{{count} 週後}}", + "timeagoInNbMonths": "{count, plural, other{{count} か月後}}", + "timeagoInNbYears": "{count, plural, other{{count} 年後}}", + "timeagoNbMinutesAgo": "{count, plural, other{{count} 分前}}", + "timeagoNbHoursAgo": "{count, plural, other{{count} 時間前}}", + "timeagoNbDaysAgo": "{count, plural, other{{count} 日前}}", + "timeagoNbWeeksAgo": "{count, plural, other{{count} 週前}}", + "timeagoNbMonthsAgo": "{count, plural, other{{count} か月前}}", + "timeagoNbYearsAgo": "{count, plural, other{{count} 年前}}", + "timeagoNbMinutesRemaining": "{count, plural, other{残り {count} 分}}", + "timeagoNbHoursRemaining": "{count, plural, other{残り {count} 時間}}" } \ No newline at end of file diff --git a/lib/l10n/lila_kk.arb b/lib/l10n/lila_kk.arb index 4a083b5256..d002d954fe 100644 --- a/lib/l10n/lila_kk.arb +++ b/lib/l10n/lila_kk.arb @@ -1432,5 +1432,20 @@ "studyNbChapters": "{count, plural, =1{{count} бөлім} other{{count} бөлім}}", "studyNbGames": "{count, plural, =1{{count} ойын} other{{count} ойын}}", "studyNbMembers": "{count, plural, =1{{count} мүше} other{{count} мүше}}", - "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{PGN мәтінін осында қойыңыз, {count} ойын ғана} other{PGN мәтінін осында қойыңыз, {count} ойынға дейін}}" + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{PGN мәтінін осында қойыңыз, {count} ойын ғана} other{PGN мәтінін осында қойыңыз, {count} ойынға дейін}}", + "timeagoJustNow": "жаңа ғана", + "timeagoRightNow": "дәл қазір", + "timeagoInNbSeconds": "{count, plural, =1{{count} секундта} other{{count} секундта}}", + "timeagoInNbMinutes": "{count, plural, =1{{count} минутта} other{{count} минутта}}", + "timeagoInNbHours": "{count, plural, =1{{count} сағатта} other{{count} сағатта}}", + "timeagoInNbDays": "{count, plural, =1{{count} күннен кейін} other{{count} күннен кейін}}", + "timeagoInNbWeeks": "{count, plural, =1{{count} аптадан кейін} other{{count} аптадан кейін}}", + "timeagoInNbMonths": "{count, plural, =1{{count} айдан кейін} other{{count} айдан кейін}}", + "timeagoInNbYears": "{count, plural, =1{{count} жылдан кейін} other{{count} жылдан кейін}}", + "timeagoNbMinutesAgo": "{count, plural, =1{{count} минут бұрын} other{{count} минут бұрын}}", + "timeagoNbHoursAgo": "{count, plural, =1{{count} сағат бұрын} other{{count} сағат бұрын}}", + "timeagoNbDaysAgo": "{count, plural, =1{{count} күн бұрын} other{{count} күн бұрын}}", + "timeagoNbWeeksAgo": "{count, plural, =1{{count} апта бұрын} other{{count} апта бұрын}}", + "timeagoNbMonthsAgo": "{count, plural, =1{{count} ай бұрын} other{{count} ай бұрын}}", + "timeagoNbYearsAgo": "{count, plural, =1{{count} жыл бұрын} other{{count} жыл бұрын}}" } \ No newline at end of file diff --git a/lib/l10n/lila_ko.arb b/lib/l10n/lila_ko.arb index 7c83b06ed5..1d80dc93cc 100644 --- a/lib/l10n/lila_ko.arb +++ b/lib/l10n/lila_ko.arb @@ -1547,5 +1547,23 @@ "studyNbChapters": "{count, plural, other{{count} 챕터}}", "studyNbGames": "{count, plural, other{{count} 게임}}", "studyNbMembers": "{count, plural, other{멤버 {count}명}}", - "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, other{PGN을 여기에 붙여넣으세요. 최대 {count} 게임까지 가능합니다.}}" + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, other{PGN을 여기에 붙여넣으세요. 최대 {count} 게임까지 가능합니다.}}", + "timeagoJustNow": "방금", + "timeagoRightNow": "지금", + "timeagoCompleted": "종료됨", + "timeagoInNbSeconds": "{count, plural, other{{count}초 후}}", + "timeagoInNbMinutes": "{count, plural, other{{count}분 후}}", + "timeagoInNbHours": "{count, plural, other{{count}시간 후}}", + "timeagoInNbDays": "{count, plural, other{{count}일 후}}", + "timeagoInNbWeeks": "{count, plural, other{{count}주 후}}", + "timeagoInNbMonths": "{count, plural, other{{count}개월 후}}", + "timeagoInNbYears": "{count, plural, other{{count}년 후}}", + "timeagoNbMinutesAgo": "{count, plural, other{{count}분 전}}", + "timeagoNbHoursAgo": "{count, plural, other{{count}시간 전}}", + "timeagoNbDaysAgo": "{count, plural, other{{count}일 전}}", + "timeagoNbWeeksAgo": "{count, plural, other{{count}주 전}}", + "timeagoNbMonthsAgo": "{count, plural, other{{count}개월 전}}", + "timeagoNbYearsAgo": "{count, plural, other{{count}년 전}}", + "timeagoNbMinutesRemaining": "{count, plural, other{{count}분 남음}}", + "timeagoNbHoursRemaining": "{count, plural, other{{count}시간 남음}}" } \ No newline at end of file diff --git a/lib/l10n/lila_lb.arb b/lib/l10n/lila_lb.arb index ba01cf997c..68e84b1b7f 100644 --- a/lib/l10n/lila_lb.arb +++ b/lib/l10n/lila_lb.arb @@ -1482,5 +1482,23 @@ "studyNbChapters": "{count, plural, =1{{count} Kapitel} other{{count} Kapitel}}", "studyNbGames": "{count, plural, =1{{count} Partie} other{{count} Partien}}", "studyNbMembers": "{count, plural, =1{{count} Member} other{{count} Memberen}}", - "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{PGN Text hei asetzen, bis zu {count} Partie} other{PGN Text hei asetzen, bis zu {count} Partien}}" + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{PGN Text hei asetzen, bis zu {count} Partie} other{PGN Text hei asetzen, bis zu {count} Partien}}", + "timeagoJustNow": "elo grad", + "timeagoRightNow": "elo", + "timeagoCompleted": "eriwwer", + "timeagoInNbSeconds": "{count, plural, =1{an {count} Sekonn} other{an {count} Sekonnen}}", + "timeagoInNbMinutes": "{count, plural, =1{an {count} Minutt} other{an {count} Minutten}}", + "timeagoInNbHours": "{count, plural, =1{an {count} Stonn} other{an {count} Stonnen}}", + "timeagoInNbDays": "{count, plural, =1{an {count} Dag} other{an {count} Deeg}}", + "timeagoInNbWeeks": "{count, plural, =1{an {count} Woch} other{an {count} Wochen}}", + "timeagoInNbMonths": "{count, plural, =1{an {count} Mount} other{an {count} Méint}}", + "timeagoInNbYears": "{count, plural, =1{an {count} Joer} other{an {count} Joer}}", + "timeagoNbMinutesAgo": "{count, plural, =1{virun {count} Minutt} other{virun {count} Minutten}}", + "timeagoNbHoursAgo": "{count, plural, =1{virun {count} Stonn} other{virun {count} Stonnen}}", + "timeagoNbDaysAgo": "{count, plural, =1{virun {count} Dag} other{virun {count} Deeg}}", + "timeagoNbWeeksAgo": "{count, plural, =1{virun {count} Woch} other{virun {count} Wochen}}", + "timeagoNbMonthsAgo": "{count, plural, =1{virun {count} Mount} other{virun {count} Méint}}", + "timeagoNbYearsAgo": "{count, plural, =1{virun {count} Joer} other{virun {count} Joer}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{{count} Minutt iwwereg} other{{count} Minutten iwwereg}}", + "timeagoNbHoursRemaining": "{count, plural, =1{{count} Stonn iwwereg} other{{count} Stonnen iwwereg}}" } \ No newline at end of file diff --git a/lib/l10n/lila_lt.arb b/lib/l10n/lila_lt.arb index 2d69582878..34017383c1 100644 --- a/lib/l10n/lila_lt.arb +++ b/lib/l10n/lila_lt.arb @@ -1501,5 +1501,23 @@ "studyNbChapters": "{count, plural, =1{{count} skyrius} few{{count} skyriai} many{{count} skyrių} other{{count} skyrių}}", "studyNbGames": "{count, plural, =1{{count} partija} few{{count} partijos} many{{count} partijų} other{{count} partijų}}", "studyNbMembers": "{count, plural, =1{{count} narys} few{{count} nariai} many{{count} narių} other{{count} narių}}", - "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Įklijuokite savo PGN tekstą čia, iki {count} žaidimo} few{Įklijuokite savo PGN tekstą čia, iki {count} žaidimų} many{Įklijuokite savo PGN tekstą čia, iki {count} žaidimo} other{Įklijuokite savo PGN tekstą čia, iki {count} žaidimų}}" + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Įklijuokite savo PGN tekstą čia, iki {count} žaidimo} few{Įklijuokite savo PGN tekstą čia, iki {count} žaidimų} many{Įklijuokite savo PGN tekstą čia, iki {count} žaidimo} other{Įklijuokite savo PGN tekstą čia, iki {count} žaidimų}}", + "timeagoJustNow": "ką tik", + "timeagoRightNow": "dabar", + "timeagoCompleted": "užbaigta", + "timeagoInNbSeconds": "{count, plural, =1{po {count} sekundės} few{po {count} sekundžių} many{po {count} sekundės} other{po {count} sekundžių}}", + "timeagoInNbMinutes": "{count, plural, =1{po {count} minutės} few{po {count} minučių} many{po {count} minutės} other{po {count} minučių}}", + "timeagoInNbHours": "{count, plural, =1{po {count} valandos} few{po {count} valandų} many{po {count} valandos} other{po {count} valandų}}", + "timeagoInNbDays": "{count, plural, =1{po {count} dienos} few{po {count} dienų} many{po {count} dienos} other{po {count} dienų}}", + "timeagoInNbWeeks": "{count, plural, =1{po {count} savaitės} few{po {count} savaičių} many{po {count} savaitės} other{po {count} savaičių}}", + "timeagoInNbMonths": "{count, plural, =1{po {count} mėnesio} few{po {count} mėnesių} many{po {count} mėnesio} other{po {count} mėnesių}}", + "timeagoInNbYears": "{count, plural, =1{po {count} metų} few{po {count} metų} many{po {count} metų} other{po {count} metų}}", + "timeagoNbMinutesAgo": "{count, plural, =1{Prieš {count} minutę} few{Prieš {count} minutes} many{Prieš {count} minutės} other{Prieš {count} minučių}}", + "timeagoNbHoursAgo": "{count, plural, =1{Prieš {count} valandą} few{Prieš {count} valandas} many{Prieš {count} valandos} other{Prieš {count} valandų}}", + "timeagoNbDaysAgo": "{count, plural, =1{Prieš {count} dieną} few{Prieš {count} dienas} many{Prieš {count} dienos} other{Prieš {count} dienų}}", + "timeagoNbWeeksAgo": "{count, plural, =1{Prieš {count} savaitę} few{Prieš {count} savaites} many{Prieš {count} savaitės} other{Prieš {count} savaičių}}", + "timeagoNbMonthsAgo": "{count, plural, =1{Prieš {count} mėnesį} few{Prieš {count} mėnesius} many{Prieš {count} mėnesio} other{Prieš {count} mėnesių}}", + "timeagoNbYearsAgo": "{count, plural, =1{Prieš {count} metus} few{Prieš {count} metus} many{Prieš {count} metų} other{Prieš {count} metų}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{Liko {count} minutė} few{Liko {count} minutės} many{Liko {count} minučių} other{Liko {count} minučių}}", + "timeagoNbHoursRemaining": "{count, plural, =1{Liko {count} valanda} few{Liko {count} valandos} many{Liko {count} valandų} other{Liko {count} valandų}}" } \ No newline at end of file diff --git a/lib/l10n/lila_lv.arb b/lib/l10n/lila_lv.arb index 6cd1e1d8ab..1a2b200cb8 100644 --- a/lib/l10n/lila_lv.arb +++ b/lib/l10n/lila_lv.arb @@ -1391,5 +1391,20 @@ "studyNbChapters": "{count, plural, =0{{count} Nodaļas} =1{{count} Nodaļa} other{{count} Nodaļas}}", "studyNbGames": "{count, plural, =0{{count} Spēles} =1{{count} Spēle} other{{count} Spēles}}", "studyNbMembers": "{count, plural, =0{{count} Dalībnieki} =1{{count} Dalībnieks} other{{count} Dalībnieki}}", - "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =0{Ielīmējiet PGN tekstu šeit, ne vairāk kā {count} spēles} =1{Ielīmējiet PGN tekstu šeit, ne vairāk kā {count} spēli} other{Ielīmējiet PGN tekstu šeit, ne vairāk kā {count} spēles}}" + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =0{Ielīmējiet PGN tekstu šeit, ne vairāk kā {count} spēles} =1{Ielīmējiet PGN tekstu šeit, ne vairāk kā {count} spēli} other{Ielīmējiet PGN tekstu šeit, ne vairāk kā {count} spēles}}", + "timeagoJustNow": "tikko", + "timeagoRightNow": "tieši tagad", + "timeagoInNbSeconds": "{count, plural, =0{pēc {count} sekundēm} =1{pēc {count} sekundes} other{pēc {count} sekundēm}}", + "timeagoInNbMinutes": "{count, plural, =0{pēc {count} minūtēm} =1{pēc {count} minūtes} other{pēc {count} minūtēm}}", + "timeagoInNbHours": "{count, plural, =0{pēc {count} stundām} =1{pēc {count} stundas} other{pēc {count} stundām}}", + "timeagoInNbDays": "{count, plural, =0{pēc {count} dienām} =1{pēc {count} dienas} other{pēc {count} dienām}}", + "timeagoInNbWeeks": "{count, plural, =0{pēc {count} nedēļām} =1{pēc {count} nedēļas} other{pēc {count} nedēļām}}", + "timeagoInNbMonths": "{count, plural, =0{pēc {count} mēnešiem} =1{pēc {count} mēneša} other{pēc {count} mēnešiem}}", + "timeagoInNbYears": "{count, plural, =0{pēc {count} gadiem} =1{pēc {count} gada} other{pēc {count} gadiem}}", + "timeagoNbMinutesAgo": "{count, plural, =0{pirms {count} minūtēm} =1{pirms {count} minūtes} other{pirms {count} minūtēm}}", + "timeagoNbHoursAgo": "{count, plural, =0{pirms {count} stundām} =1{pirms {count} stundas} other{pirms {count} stundām}}", + "timeagoNbDaysAgo": "{count, plural, =0{pirms {count} dienām} =1{pirms {count} dienas} other{pirms {count} dienām}}", + "timeagoNbWeeksAgo": "{count, plural, =0{pirms {count} nedēļām} =1{pirms {count} nedēļas} other{pirms {count} nedēļām}}", + "timeagoNbMonthsAgo": "{count, plural, =0{pirms {count} mēnešiem} =1{pirms {count} mēneša} other{pirms {count} mēnešiem}}", + "timeagoNbYearsAgo": "{count, plural, =0{pirms {count} gadiem} =1{pirms {count} gada} other{pirms {count} gadiem}}" } \ No newline at end of file diff --git a/lib/l10n/lila_mk.arb b/lib/l10n/lila_mk.arb index e2408222e1..49572434fb 100644 --- a/lib/l10n/lila_mk.arb +++ b/lib/l10n/lila_mk.arb @@ -1024,5 +1024,20 @@ "studySave": "Зачувај", "studyGoodMove": "Добар потег", "studyMistake": "Грешка", - "studyBlunder": "Глупа грешка" + "studyBlunder": "Глупа грешка", + "timeagoJustNow": "тукушто", + "timeagoRightNow": "Тукушто", + "timeagoInNbSeconds": "{count, plural, =1{За {count} секунди} other{За {count} секунди}}", + "timeagoInNbMinutes": "{count, plural, =1{За {count} минута} other{За {count} минути}}", + "timeagoInNbHours": "{count, plural, =1{За {count} час} other{За {count} часови}}", + "timeagoInNbDays": "{count, plural, =1{За {count} ден} other{За {count} денови}}", + "timeagoInNbWeeks": "{count, plural, =1{За {count} седмица} other{За {count} седмици}}", + "timeagoInNbMonths": "{count, plural, =1{За {count} месец} other{За {count} месеци}}", + "timeagoInNbYears": "{count, plural, =1{За {count} година} other{За {count} години}}", + "timeagoNbMinutesAgo": "{count, plural, =1{Пред {count} минута} other{Пред {count} минути}}", + "timeagoNbHoursAgo": "{count, plural, =1{пред {count} час} other{Пред {count} часа}}", + "timeagoNbDaysAgo": "{count, plural, =1{пред {count} ден} other{Пред {count} дена}}", + "timeagoNbWeeksAgo": "{count, plural, =1{Пред {count} седмица} other{Пред {count} седмици}}", + "timeagoNbMonthsAgo": "{count, plural, =1{Пред {count} месец} other{Пред {count} месеци}}", + "timeagoNbYearsAgo": "{count, plural, =1{Пред {count} година} other{Пред {count} години}}" } \ No newline at end of file diff --git a/lib/l10n/lila_nb.arb b/lib/l10n/lila_nb.arb index 4af4ab2488..275cd990f9 100644 --- a/lib/l10n/lila_nb.arb +++ b/lib/l10n/lila_nb.arb @@ -1547,5 +1547,23 @@ "studyNbChapters": "{count, plural, =1{{count} kapittel} other{{count} kapitler}}", "studyNbGames": "{count, plural, =1{{count} parti} other{{count} partier}}", "studyNbMembers": "{count, plural, =1{{count} medlem} other{{count} medlemmer}}", - "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Sett inn PGN-teksten din her, maksimum {count} parti} other{Sett inn PGN-teksten din her, maksimum {count} partier}}" + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Sett inn PGN-teksten din her, maksimum {count} parti} other{Sett inn PGN-teksten din her, maksimum {count} partier}}", + "timeagoJustNow": "om litt", + "timeagoRightNow": "for litt siden", + "timeagoCompleted": "fullført", + "timeagoInNbSeconds": "{count, plural, =1{om {count} sekund} other{om {count} sekunder}}", + "timeagoInNbMinutes": "{count, plural, =1{om {count} minutt} other{om {count} minutter}}", + "timeagoInNbHours": "{count, plural, =1{om {count} time} other{om {count} timer}}", + "timeagoInNbDays": "{count, plural, =1{om {count} døgn} other{om {count} døgn}}", + "timeagoInNbWeeks": "{count, plural, =1{om {count} uke} other{om {count} uker}}", + "timeagoInNbMonths": "{count, plural, =1{om {count} måned} other{om {count} måneder}}", + "timeagoInNbYears": "{count, plural, =1{om {count} år} other{om {count} år}}", + "timeagoNbMinutesAgo": "{count, plural, =1{for {count} minutt siden} other{for {count} minutter siden}}", + "timeagoNbHoursAgo": "{count, plural, =1{for {count} time siden} other{for {count} timer siden}}", + "timeagoNbDaysAgo": "{count, plural, =1{for {count} døgn siden} other{for {count} døgn siden}}", + "timeagoNbWeeksAgo": "{count, plural, =1{for {count} uke siden} other{for {count} uker siden}}", + "timeagoNbMonthsAgo": "{count, plural, =1{for {count} måned siden} other{for {count} måneder siden}}", + "timeagoNbYearsAgo": "{count, plural, =1{for {count} år siden} other{for {count} år siden}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{{count} minutt igjen} other{{count} minutter igjen}}", + "timeagoNbHoursRemaining": "{count, plural, =1{{count} time igjen} other{{count} timer igjen}}" } \ No newline at end of file diff --git a/lib/l10n/lila_nl.arb b/lib/l10n/lila_nl.arb index 9af856ec1a..3bd8a36149 100644 --- a/lib/l10n/lila_nl.arb +++ b/lib/l10n/lila_nl.arb @@ -1546,5 +1546,23 @@ "studyNbChapters": "{count, plural, =1{{count} hoofdstuk} other{{count} hoofdstukken}}", "studyNbGames": "{count, plural, =1{{count} Partij} other{{count} Partijen}}", "studyNbMembers": "{count, plural, =1{{count} Deelnemer} other{{count} Deelnemers}}", - "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Plak je PGN tekst hier, tot {count} spel mogelijk} other{Plak je PGN tekst hier, tot {count} spellen mogelijk}}" + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Plak je PGN tekst hier, tot {count} spel mogelijk} other{Plak je PGN tekst hier, tot {count} spellen mogelijk}}", + "timeagoJustNow": "zojuist", + "timeagoRightNow": "op dit moment", + "timeagoCompleted": "voltooid", + "timeagoInNbSeconds": "{count, plural, =1{over {count} seconde} other{over {count} seconden}}", + "timeagoInNbMinutes": "{count, plural, =1{over {count} minuut} other{over {count} minuten}}", + "timeagoInNbHours": "{count, plural, =1{over {count} uur} other{over {count} uur}}", + "timeagoInNbDays": "{count, plural, =1{over {count} dag} other{over {count} dagen}}", + "timeagoInNbWeeks": "{count, plural, =1{over {count} week} other{over {count} weken}}", + "timeagoInNbMonths": "{count, plural, =1{over {count} maand} other{over {count} maanden}}", + "timeagoInNbYears": "{count, plural, =1{over {count} jaar} other{over {count} jaren}}", + "timeagoNbMinutesAgo": "{count, plural, =1{{count} minuut geleden} other{{count} minuten geleden}}", + "timeagoNbHoursAgo": "{count, plural, =1{{count} uur geleden} other{{count} uur geleden}}", + "timeagoNbDaysAgo": "{count, plural, =1{{count} dag geleden} other{{count} dagen geleden}}", + "timeagoNbWeeksAgo": "{count, plural, =1{{count} week geleden} other{{count} weken geleden}}", + "timeagoNbMonthsAgo": "{count, plural, =1{{count} maand geleden} other{{count} maanden geleden}}", + "timeagoNbYearsAgo": "{count, plural, =1{{count} jaar geleden} other{{count} jaar geleden}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{{count} minuut resterend} other{{count} minuten resterend}}", + "timeagoNbHoursRemaining": "{count, plural, =1{{count} uur resterend} other{{count} uur resterend}}" } \ No newline at end of file diff --git a/lib/l10n/lila_nn.arb b/lib/l10n/lila_nn.arb index e5dff01baa..acbde20304 100644 --- a/lib/l10n/lila_nn.arb +++ b/lib/l10n/lila_nn.arb @@ -1547,5 +1547,23 @@ "studyNbChapters": "{count, plural, =1{{count} kapittel} other{{count} kapittel}}", "studyNbGames": "{count, plural, =1{{count} parti} other{{count} parti}}", "studyNbMembers": "{count, plural, =1{{count} medlem} other{{count} medlemar}}", - "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Sett inn PGN-teksten din her, maksimum {count} parti} other{Sett inn PGN-teksten din her, maksimum {count} parti}}" + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Sett inn PGN-teksten din her, maksimum {count} parti} other{Sett inn PGN-teksten din her, maksimum {count} parti}}", + "timeagoJustNow": "for kort tid sidan", + "timeagoRightNow": "nett no", + "timeagoCompleted": "fullført", + "timeagoInNbSeconds": "{count, plural, =1{om {count} sekund} other{om {count} sekund}}", + "timeagoInNbMinutes": "{count, plural, =1{om {count} minutt} other{om {count} minutt}}", + "timeagoInNbHours": "{count, plural, =1{om {count} time} other{om {count} timar}}", + "timeagoInNbDays": "{count, plural, =1{om {count} dag} other{om {count} dagar}}", + "timeagoInNbWeeks": "{count, plural, =1{om {count} veke} other{om {count} veker}}", + "timeagoInNbMonths": "{count, plural, =1{om {count} månad} other{om {count} månader}}", + "timeagoInNbYears": "{count, plural, =1{om {count} år} other{om {count} år}}", + "timeagoNbMinutesAgo": "{count, plural, =1{{count} minutt sidan} other{{count} minutt sidan}}", + "timeagoNbHoursAgo": "{count, plural, =1{{count} time sidan} other{{count} timar sidan}}", + "timeagoNbDaysAgo": "{count, plural, =1{{count} dag sidan} other{{count} dagar sidan}}", + "timeagoNbWeeksAgo": "{count, plural, =1{{count} veke sidan} other{{count} veker sidan}}", + "timeagoNbMonthsAgo": "{count, plural, =1{{count} månad sidan} other{{count} månader sidan}}", + "timeagoNbYearsAgo": "{count, plural, =1{{count} år sidan} other{{count} år sidan}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{{count} minutt igjen} other{{count} minutt igjen}}", + "timeagoNbHoursRemaining": "{count, plural, =1{{count} time igjen} other{{count} timar igjen}}" } \ No newline at end of file diff --git a/lib/l10n/lila_pl.arb b/lib/l10n/lila_pl.arb index dcb008584b..62513b3258 100644 --- a/lib/l10n/lila_pl.arb +++ b/lib/l10n/lila_pl.arb @@ -1547,5 +1547,23 @@ "studyNbChapters": "{count, plural, =1{{count} rozdział} few{{count} rozdziały} many{{count} rozdziałów} other{{count} rozdziałów}}", "studyNbGames": "{count, plural, =1{{count} partia} few{{count} partie} many{{count} partii} other{{count} partii}}", "studyNbMembers": "{count, plural, =1{{count} uczestnik} few{{count} uczestników} many{{count} uczestników} other{{count} uczestników}}", - "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Wklej tutaj swój PGN, max {count} partię} few{Wklej tutaj swój PGN, max {count} partie} many{Wklej tutaj swój PGN, max {count} partii} other{Wklej tutaj swój PGN, max {count} partii}}" + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Wklej tutaj swój PGN, max {count} partię} few{Wklej tutaj swój PGN, max {count} partie} many{Wklej tutaj swój PGN, max {count} partii} other{Wklej tutaj swój PGN, max {count} partii}}", + "timeagoJustNow": "właśnie teraz", + "timeagoRightNow": "w tej chwili", + "timeagoCompleted": "ukończone", + "timeagoInNbSeconds": "{count, plural, =1{za {count} sekundę} few{za {count} sekundy} many{za {count} sekund} other{za {count} sekund}}", + "timeagoInNbMinutes": "{count, plural, =1{za {count} minutę} few{za {count} minuty} many{za {count} minuty} other{za {count} minut}}", + "timeagoInNbHours": "{count, plural, =1{za {count} godzinę} few{za {count} godziny} many{za {count} godzin} other{za {count} godzin}}", + "timeagoInNbDays": "{count, plural, =1{za {count} dzień} few{za {count} dni} many{za {count} dni} other{za {count} dni}}", + "timeagoInNbWeeks": "{count, plural, =1{za {count} tydzień} few{za {count} tygodnie} many{za {count} tygodni} other{za {count} tygodni}}", + "timeagoInNbMonths": "{count, plural, =1{za {count} miesiąc} few{za {count} miesiące} many{za {count} miesięcy} other{za {count} miesięcy}}", + "timeagoInNbYears": "{count, plural, =1{za {count} rok} few{za {count} lata} many{za {count} lat} other{za {count} lat}}", + "timeagoNbMinutesAgo": "{count, plural, =1{{count} minutę temu} few{{count} minuty temu} many{{count} minut temu} other{{count} minut temu}}", + "timeagoNbHoursAgo": "{count, plural, =1{{count} godzinę temu} few{{count} godziny temu} many{{count} godzin temu} other{{count} godzin temu}}", + "timeagoNbDaysAgo": "{count, plural, =1{{count} dzień temu} few{{count} dni temu} many{{count} dni temu} other{{count} dni temu}}", + "timeagoNbWeeksAgo": "{count, plural, =1{{count} tydzień temu} few{{count} tygodnie temu} many{{count} tygodni temu} other{{count} tygodni temu}}", + "timeagoNbMonthsAgo": "{count, plural, =1{{count} miesiąc temu} few{{count} miesiące temu} many{{count} miesięcy temu} other{{count} miesięcy temu}}", + "timeagoNbYearsAgo": "{count, plural, =1{{count} rok temu} few{{count} lata temu} many{{count} lat temu} other{{count} lat temu}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{Pozostała {count} minuta} few{Pozostały {count} minuty} many{Pozostało {count} minut} other{Pozostało {count} minut}}", + "timeagoNbHoursRemaining": "{count, plural, =1{Pozostała {count} godzina} few{Pozostały {count} godziny} many{Pozostało {count} godzin} other{Pozostało {count} godzin}}" } \ No newline at end of file diff --git a/lib/l10n/lila_pt.arb b/lib/l10n/lila_pt.arb index 1e0f65b0d3..7f5ab000d6 100644 --- a/lib/l10n/lila_pt.arb +++ b/lib/l10n/lila_pt.arb @@ -1546,5 +1546,23 @@ "studyNbChapters": "{count, plural, =1{{count} capítulo} other{{count} capítulos}}", "studyNbGames": "{count, plural, =1{{count} Jogo} other{{count} Jogos}}", "studyNbMembers": "{count, plural, =1{{count} membro} other{{count} membros}}", - "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Cole seu texto PGN aqui, até {count} jogo} other{Cole seu texto PGN aqui, até {count} jogos}}" + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Cole seu texto PGN aqui, até {count} jogo} other{Cole seu texto PGN aqui, até {count} jogos}}", + "timeagoJustNow": "agora mesmo", + "timeagoRightNow": "agora mesmo", + "timeagoCompleted": "concluído", + "timeagoInNbSeconds": "{count, plural, =1{em {count} segundos} other{em {count} segundos}}", + "timeagoInNbMinutes": "{count, plural, =1{dentro de {count} minutos} other{em {count} minutos}}", + "timeagoInNbHours": "{count, plural, =1{em {count} hora} other{em {count} horas}}", + "timeagoInNbDays": "{count, plural, =1{em {count} dia} other{em {count} dias}}", + "timeagoInNbWeeks": "{count, plural, =1{em {count} semana} other{em {count} semanas}}", + "timeagoInNbMonths": "{count, plural, =1{em {count} mês} other{em {count} meses}}", + "timeagoInNbYears": "{count, plural, =1{em {count} ano} other{em {count} anos}}", + "timeagoNbMinutesAgo": "{count, plural, =1{há {count} minuto} other{há {count} minutos}}", + "timeagoNbHoursAgo": "{count, plural, =1{há {count} hora} other{há {count} horas}}", + "timeagoNbDaysAgo": "{count, plural, =1{há {count} dia} other{há {count} dias}}", + "timeagoNbWeeksAgo": "{count, plural, =1{há {count} semana} other{há {count} semanas}}", + "timeagoNbMonthsAgo": "{count, plural, =1{há {count} mês} other{há {count} meses}}", + "timeagoNbYearsAgo": "{count, plural, =1{há {count} ano} other{há {count} anos}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{{count} minuto restante} other{{count} minutos restantes}}", + "timeagoNbHoursRemaining": "{count, plural, =1{{count} hora restante} other{{count} horas restantes}}" } \ No newline at end of file diff --git a/lib/l10n/lila_pt_BR.arb b/lib/l10n/lila_pt_BR.arb index dd1117d7ed..97c88fff21 100644 --- a/lib/l10n/lila_pt_BR.arb +++ b/lib/l10n/lila_pt_BR.arb @@ -1547,5 +1547,23 @@ "studyNbChapters": "{count, plural, =1{{count} Capítulo} other{{count} Capítulos}}", "studyNbGames": "{count, plural, =1{{count} Jogo} other{{count} Jogos}}", "studyNbMembers": "{count, plural, =1{{count} Membro} other{{count} Membros}}", - "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Cole seu texto PGN aqui, até {count} jogo} other{Cole seu texto PGN aqui, até {count} jogos}}" + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Cole seu texto PGN aqui, até {count} jogo} other{Cole seu texto PGN aqui, até {count} jogos}}", + "timeagoJustNow": "agora há pouco", + "timeagoRightNow": "agora mesmo", + "timeagoCompleted": "concluído", + "timeagoInNbSeconds": "{count, plural, =1{em {count} segundo} other{em {count} segundos}}", + "timeagoInNbMinutes": "{count, plural, =1{em {count} minuto} other{em {count} minutos}}", + "timeagoInNbHours": "{count, plural, =1{em {count} hora} other{em {count} horas}}", + "timeagoInNbDays": "{count, plural, =1{em {count} dia} other{em {count} dias}}", + "timeagoInNbWeeks": "{count, plural, =1{em {count} semana} other{em {count} semanas}}", + "timeagoInNbMonths": "{count, plural, =1{em {count} mês} other{em {count} meses}}", + "timeagoInNbYears": "{count, plural, =1{em {count} ano} other{em {count} anos}}", + "timeagoNbMinutesAgo": "{count, plural, =1{{count} minuto atrás} other{{count} minutos atrás}}", + "timeagoNbHoursAgo": "{count, plural, =1{{count} hora atrás} other{{count} horas atrás}}", + "timeagoNbDaysAgo": "{count, plural, =1{{count} dia atrás} other{{count} dias atrás}}", + "timeagoNbWeeksAgo": "{count, plural, =1{{count} semana atrás} other{{count} semanas atrás}}", + "timeagoNbMonthsAgo": "{count, plural, =1{{count} mês atrás} other{{count} meses atrás}}", + "timeagoNbYearsAgo": "{count, plural, =1{{count} ano atrás} other{{count} anos atrás}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{{count} minuto restante} other{{count} minutos restantes}}", + "timeagoNbHoursRemaining": "{count, plural, =1{{count} hora restante} other{{count} horas restantes}}" } \ No newline at end of file diff --git a/lib/l10n/lila_ro.arb b/lib/l10n/lila_ro.arb index 6710eb2dbe..014dba0315 100644 --- a/lib/l10n/lila_ro.arb +++ b/lib/l10n/lila_ro.arb @@ -1523,5 +1523,23 @@ "studyNbChapters": "{count, plural, =1{{count} capitol} few{{count} capitole} other{{count} capitole}}", "studyNbGames": "{count, plural, =1{{count} partidă} few{{count} partide} other{{count} partide}}", "studyNbMembers": "{count, plural, =1{{count} membru} few{{count} membri} other{{count} membri}}", - "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Lipiți textul PGN aici, până la {count} meci} few{Lipiți textul PGN aici, până la {count} meciuri} other{Lipiți textul PGN aici, până la {count} meciuri}}" + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Lipiți textul PGN aici, până la {count} meci} few{Lipiți textul PGN aici, până la {count} meciuri} other{Lipiți textul PGN aici, până la {count} meciuri}}", + "timeagoJustNow": "chiar acum", + "timeagoRightNow": "chiar acum", + "timeagoCompleted": "completat", + "timeagoInNbSeconds": "{count, plural, =1{în {count} secundă} few{în {count} secunde} other{în {count} de secunde}}", + "timeagoInNbMinutes": "{count, plural, =1{în {count} minut} few{în {count} minute} other{în {count} de minute}}", + "timeagoInNbHours": "{count, plural, =1{în {count} oră} few{în {count} ore} other{în {count} de ore}}", + "timeagoInNbDays": "{count, plural, =1{în {count} zi} few{în {count} zile} other{în {count} de zile}}", + "timeagoInNbWeeks": "{count, plural, =1{în {count} săptămână} few{în {count} săptămâni} other{în {count} de săptămâni}}", + "timeagoInNbMonths": "{count, plural, =1{în {count} lună} few{în {count} luni} other{în {count} de luni}}", + "timeagoInNbYears": "{count, plural, =1{în {count} an} few{în {count} ani} other{în {count} de ani}}", + "timeagoNbMinutesAgo": "{count, plural, =1{cu {count} minut în urmă} few{cu {count} minute în urmă} other{cu {count} de minute în urmă}}", + "timeagoNbHoursAgo": "{count, plural, =1{cu {count} oră în urmă} few{cu {count} ore în urmă} other{cu {count} de ore în urmă}}", + "timeagoNbDaysAgo": "{count, plural, =1{cu {count} zi în urmă} few{cu {count} zile în urmă} other{cu {count} de zile în urmă}}", + "timeagoNbWeeksAgo": "{count, plural, =1{cu {count} săptămână în urmă} few{cu {count} săptămâni în urmă} other{cu {count} de săptămâni în urmă}}", + "timeagoNbMonthsAgo": "{count, plural, =1{cu {count} lună în urmă} few{cu {count} luni în urmă} other{cu {count} de luni în urmă}}", + "timeagoNbYearsAgo": "{count, plural, =1{cu {count} an în urmă} few{cu {count} ani în urmă} other{cu {count} de ani în urmă}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{{count} minut rămas} few{{count} minute rămase} other{{count} minute rămase}}", + "timeagoNbHoursRemaining": "{count, plural, =1{{count} oră rămasă} few{{count} ore rămase} other{{count} ore rămase}}" } \ No newline at end of file diff --git a/lib/l10n/lila_ru.arb b/lib/l10n/lila_ru.arb index 2f09718b5b..c98d6638fd 100644 --- a/lib/l10n/lila_ru.arb +++ b/lib/l10n/lila_ru.arb @@ -1547,5 +1547,23 @@ "studyNbChapters": "{count, plural, =1{{count} глава} few{{count} главы} many{{count} глав} other{{count} глав}}", "studyNbGames": "{count, plural, =1{{count} партия} few{{count} партии} many{{count} партий} other{{count} партий}}", "studyNbMembers": "{count, plural, =1{{count} участник} few{{count} участника} many{{count} участников} other{{count} участников}}", - "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Вставьте текст в формате PGN, не больше {count} игры} few{Вставьте текст в формате PGN, не больше {count} игр} many{Вставьте текст в формате PGN, не больше {count} игр} other{Вставьте текст в формате PGN, не больше {count} игр}}" + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Вставьте текст в формате PGN, не больше {count} игры} few{Вставьте текст в формате PGN, не больше {count} игр} many{Вставьте текст в формате PGN, не больше {count} игр} other{Вставьте текст в формате PGN, не больше {count} игр}}", + "timeagoJustNow": "только что", + "timeagoRightNow": "прямо сейчас", + "timeagoCompleted": "завершено", + "timeagoInNbSeconds": "{count, plural, =1{через {count} секунду} few{через {count} секунды} many{через {count} секунд} other{через {count} секунд}}", + "timeagoInNbMinutes": "{count, plural, =1{через {count} минуту} few{через {count} минуты} many{через {count} минут} other{через {count} минут}}", + "timeagoInNbHours": "{count, plural, =1{через {count} час} few{через {count} часа} many{через {count} часов} other{через {count} часов}}", + "timeagoInNbDays": "{count, plural, =1{через {count} день} few{через {count} дня} many{через {count} дней} other{через {count} дней}}", + "timeagoInNbWeeks": "{count, plural, =1{через {count} неделю} few{через {count} недели} many{через {count} недель} other{через {count} недель}}", + "timeagoInNbMonths": "{count, plural, =1{через {count} месяц} few{через {count} месяца} many{через {count} месяцев} other{через {count} месяцев}}", + "timeagoInNbYears": "{count, plural, =1{через {count} год} few{через {count} года} many{через {count} лет} other{через {count} лет}}", + "timeagoNbMinutesAgo": "{count, plural, =1{{count} минуту назад} few{{count} минуты назад} many{{count} минут назад} other{{count} минут назад}}", + "timeagoNbHoursAgo": "{count, plural, =1{{count} час назад} few{{count} часа назад} many{{count} часов назад} other{{count} часов назад}}", + "timeagoNbDaysAgo": "{count, plural, =1{{count} день назад} few{{count} дня назад} many{{count} дней назад} other{{count} дней назад}}", + "timeagoNbWeeksAgo": "{count, plural, =1{{count} неделю назад} few{{count} недели назад} many{{count} недель назад} other{{count} недель назад}}", + "timeagoNbMonthsAgo": "{count, plural, =1{{count} месяц назад} few{{count} месяца назад} many{{count} месяцев назад} other{{count} месяцев назад}}", + "timeagoNbYearsAgo": "{count, plural, =1{{count} год назад} few{{count} года назад} many{{count} лет назад} other{{count} лет назад}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{осталась {count} минута} few{осталось {count} минуты} many{осталось {count} минут} other{осталось {count} минут}}", + "timeagoNbHoursRemaining": "{count, plural, =1{остался {count} час} few{осталось {count} часа} many{осталось {count} часов} other{осталось {count} часов}}" } \ No newline at end of file diff --git a/lib/l10n/lila_sk.arb b/lib/l10n/lila_sk.arb index 571fce1b2e..5508e92125 100644 --- a/lib/l10n/lila_sk.arb +++ b/lib/l10n/lila_sk.arb @@ -1547,5 +1547,23 @@ "studyNbChapters": "{count, plural, =1{{count} Kapitola} few{{count} Kapitoly} many{{count} Kapitol} other{{count} Kapitol}}", "studyNbGames": "{count, plural, =1{{count} Partia} few{{count} Partie} many{{count} Partií} other{{count} Partií}}", "studyNbMembers": "{count, plural, =1{{count} Člen} few{{count} Členovia} many{{count} Členov} other{{count} Členov}}", - "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Váš PGN text vložte sem, maximálne {count} partiu} few{Váš PGN text vložte sem, maximálne {count} partie} many{Váš PGN text vložte sem, maximálne {count} partií} other{Váš PGN text vložte sem, maximálne {count} partií}}" + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Váš PGN text vložte sem, maximálne {count} partiu} few{Váš PGN text vložte sem, maximálne {count} partie} many{Váš PGN text vložte sem, maximálne {count} partií} other{Váš PGN text vložte sem, maximálne {count} partií}}", + "timeagoJustNow": "práve teraz", + "timeagoRightNow": "práve teraz", + "timeagoCompleted": "ukončené", + "timeagoInNbSeconds": "{count, plural, =1{o {count} sekundu} few{o {count} sekundy} many{o {count} sekúnd} other{o {count} sekúnd}}", + "timeagoInNbMinutes": "{count, plural, =1{o {count} minútu} few{o {count} minút} many{o {count} minút} other{o {count} minút}}", + "timeagoInNbHours": "{count, plural, =1{o {count} hodinu} few{o {count} hodiny} many{o {count} hodín} other{o {count} hodín}}", + "timeagoInNbDays": "{count, plural, =1{o {count} deň} few{o {count} dni} many{o {count} dní} other{o {count} dní}}", + "timeagoInNbWeeks": "{count, plural, =1{o {count} týždeň} few{o {count} týždne} many{o {count} týždňov} other{o {count} týždňov}}", + "timeagoInNbMonths": "{count, plural, =1{o {count} mesiac} few{o {count} mesiace} many{o {count} mesiacov} other{o {count} mesiacov}}", + "timeagoInNbYears": "{count, plural, =1{o {count} rok} few{o {count} roky} many{o {count} rokov} other{o {count} rokov}}", + "timeagoNbMinutesAgo": "{count, plural, =1{pred {count} minútou} few{pred {count} minútami} many{pred {count} minútami} other{pred {count} minútami}}", + "timeagoNbHoursAgo": "{count, plural, =1{pred {count} hodinou} few{pred {count} hodinami} many{pred {count} hodinami} other{pred {count} hodinami}}", + "timeagoNbDaysAgo": "{count, plural, =1{pred {count} dňom} few{pred {count} dňami} many{pred {count} dňami} other{pred {count} dňami}}", + "timeagoNbWeeksAgo": "{count, plural, =1{pred {count} týždňom} few{pred {count} týždňami} many{pred {count} týždňami} other{pred {count} týždňami}}", + "timeagoNbMonthsAgo": "{count, plural, =1{pred {count} mesiacom} few{pred {count} mesiacmi} many{pred {count} mesiacmi} other{pred {count} mesiacmi}}", + "timeagoNbYearsAgo": "{count, plural, =1{pred {count} rokom} few{pred {count} rokmi} many{pred {count} rokmi} other{pred {count} rokmi}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{ostáva {count} minúta} few{ostávajú {count} minúty} many{ostáva {count} minút} other{ostáva {count} minút}}", + "timeagoNbHoursRemaining": "{count, plural, =1{ostáva {count} hodina} few{ostávajú {count} hodiny} many{ostáva {count} hodín} other{ostáva {count} hodín}}" } \ No newline at end of file diff --git a/lib/l10n/lila_sl.arb b/lib/l10n/lila_sl.arb index 59190d2975..483f68f94a 100644 --- a/lib/l10n/lila_sl.arb +++ b/lib/l10n/lila_sl.arb @@ -1470,5 +1470,23 @@ "studyNbChapters": "{count, plural, =1{{count} Poglavje} =2{{count} Poglavji} few{{count} Poglavja} other{{count} poglavij}}", "studyNbGames": "{count, plural, =1{{count} Partija} =2{{count} Partiji} few{{count} Partije} other{{count} Partij}}", "studyNbMembers": "{count, plural, =1{{count} Član} =2{{count} Člana} few{{count} Člani} other{{count} Članov}}", - "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Prilepite PGN besedilo, z največ {count} partijo} =2{Prilepite PGN besedilo, z največ {count} partijama} few{Prilepite PGN besedilo, z največ {count} partijami} other{Prilepite PGN besedilo, z največ {count} partijami}}" + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Prilepite PGN besedilo, z največ {count} partijo} =2{Prilepite PGN besedilo, z največ {count} partijama} few{Prilepite PGN besedilo, z največ {count} partijami} other{Prilepite PGN besedilo, z največ {count} partijami}}", + "timeagoJustNow": "pravkar", + "timeagoRightNow": "ta trenutek", + "timeagoCompleted": "končano", + "timeagoInNbSeconds": "{count, plural, =1{čez {count} sekund} =2{čez {count} sekundi} few{čez {count} sekunde} other{čez {count} sekund}}", + "timeagoInNbMinutes": "{count, plural, =1{čez {count} minuto} =2{čez {count} minuti} few{čez {count} minute} other{čez {count} minut}}", + "timeagoInNbHours": "{count, plural, =1{čez {count} uro} =2{čez {count} uri} few{čez {count} ure} other{čez {count} ur}}", + "timeagoInNbDays": "{count, plural, =1{čez {count} dan} =2{čez {count} dneva} few{čez {count} dnevih} other{čez {count} dni}}", + "timeagoInNbWeeks": "{count, plural, =1{čez {count} teden} =2{čez {count} tedna} few{čez {count} tedne} other{čez {count} tednov}}", + "timeagoInNbMonths": "{count, plural, =1{čez {count} mesec} =2{čez {count} meseca} few{čez {count} mesece} other{čez {count} mesecev}}", + "timeagoInNbYears": "{count, plural, =1{čez {count} leto} =2{čez {count} leti} few{čez {count} leta} other{čez {count} let}}", + "timeagoNbMinutesAgo": "{count, plural, =1{Pred {count} minuto} =2{Pred {count} minutama} few{Pred {count} minutami} other{Pred {count} minutami}}", + "timeagoNbHoursAgo": "{count, plural, =1{Pred {count} uro} =2{Pred {count} urama} few{Pred {count} urami} other{Pred {count} urami}}", + "timeagoNbDaysAgo": "{count, plural, =1{Pred {count} dnevom} =2{Pred {count} dnevoma} few{Pred {count} dnevi} other{Pred {count} dnevi}}", + "timeagoNbWeeksAgo": "{count, plural, =1{Pred {count} tednom} =2{Pred {count} tednoma} few{Pred {count} tedni} other{Pred {count} tedni}}", + "timeagoNbMonthsAgo": "{count, plural, =1{Pred {count} mesecem} =2{Pred {count} mesecema} few{Pred {count} meseci} other{Pred {count} meseci}}", + "timeagoNbYearsAgo": "{count, plural, =1{Pred {count} letom} =2{Pred {count} letoma} few{Pred {count} leti} other{Pred {count} leti}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{še {count} minuta} =2{še {count} minuti} few{še {count} minute} other{še {count} minut}}", + "timeagoNbHoursRemaining": "{count, plural, =1{še {count} ura} =2{še {count} uri} few{še {count} ure} other{še {count} ur}}" } \ No newline at end of file diff --git a/lib/l10n/lila_sq.arb b/lib/l10n/lila_sq.arb index ce29860b66..7d3f7a9259 100644 --- a/lib/l10n/lila_sq.arb +++ b/lib/l10n/lila_sq.arb @@ -1519,5 +1519,23 @@ "studyNbChapters": "{count, plural, =1{{count} Kapitull} other{{count} Kapituj}}", "studyNbGames": "{count, plural, =1{{count} Lojë} other{{count} Lojëra}}", "studyNbMembers": "{count, plural, =1{{count} Anëtar} other{{count} Anëtarë}}", - "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Hidhni këtu tekstin e PGN-s tuaj, deri në {count} lojë} other{Hidhni këtu tekstin e PGN-s tuaj, deri në {count} lojëra}}" + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Hidhni këtu tekstin e PGN-s tuaj, deri në {count} lojë} other{Hidhni këtu tekstin e PGN-s tuaj, deri në {count} lojëra}}", + "timeagoJustNow": "tani", + "timeagoRightNow": "pikërisht tani", + "timeagoCompleted": "mbaroi", + "timeagoInNbSeconds": "{count, plural, =1{në {count} sekondë} other{pas {count} sekondave}}", + "timeagoInNbMinutes": "{count, plural, =1{në {count} minutë} other{pas {count} minutave}}", + "timeagoInNbHours": "{count, plural, =1{në {count} orë} other{në {count} orë}}", + "timeagoInNbDays": "{count, plural, =1{në {count} ditë} other{pas {count} ditëve}}", + "timeagoInNbWeeks": "{count, plural, =1{në {count} javë} other{pas {count} javë}}", + "timeagoInNbMonths": "{count, plural, =1{në {count} muaj} other{pas {count} muajve}}", + "timeagoInNbYears": "{count, plural, =1{në {count} vit} other{pas {count} viteve}}", + "timeagoNbMinutesAgo": "{count, plural, =1{{count} minutë më parë} other{para {count} minutave}}", + "timeagoNbHoursAgo": "{count, plural, =1{{count} orë më parë} other{para {count} orëve}}", + "timeagoNbDaysAgo": "{count, plural, =1{{count} ditë më parë} other{para {count} ditëve}}", + "timeagoNbWeeksAgo": "{count, plural, =1{{count} javë më parë} other{para {count} jave}}", + "timeagoNbMonthsAgo": "{count, plural, =1{{count} muaj më parë} other{para {count} muajve}}", + "timeagoNbYearsAgo": "{count, plural, =1{{count} vit më parë} other{para {count} viteve}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{Edhe {count} minutë} other{Edhe {count} minuta}}", + "timeagoNbHoursRemaining": "{count, plural, =1{Edhe {count} orë} other{Edhe {count} orë}}" } \ No newline at end of file diff --git a/lib/l10n/lila_sr.arb b/lib/l10n/lila_sr.arb index 07bee68b5c..7047088736 100644 --- a/lib/l10n/lila_sr.arb +++ b/lib/l10n/lila_sr.arb @@ -1241,5 +1241,9 @@ "studyNbChapters": "{count, plural, =1{{count} Поглавље} few{{count} Поглављa} other{{count} Поглављa}}", "studyNbGames": "{count, plural, =1{{count} Партија} few{{count} Партијe} other{{count} Партија}}", "studyNbMembers": "{count, plural, =1{{count} Члан} few{{count} Чланa} other{{count} Чланова}}", - "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Налепите свој PGN текст овде, до {count} партије} few{Налепите свој PGN текст овде, до {count} партије} other{Налепите свој PGN текст овде, до {count} партија}}" + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Налепите свој PGN текст овде, до {count} партије} few{Налепите свој PGN текст овде, до {count} партије} other{Налепите свој PGN текст овде, до {count} партија}}", + "timeagoJustNow": "управо сада", + "timeagoRightNow": "управо сад", + "timeagoCompleted": "завршено", + "timeagoInNbSeconds": "{count, plural, =1{за {count} секунди} few{за {count} сати} other{за {count} дана}}" } \ No newline at end of file diff --git a/lib/l10n/lila_sv.arb b/lib/l10n/lila_sv.arb index b438affd01..6cbf08b278 100644 --- a/lib/l10n/lila_sv.arb +++ b/lib/l10n/lila_sv.arb @@ -1465,5 +1465,23 @@ "studyNbChapters": "{count, plural, =1{{count} Kapitel} other{{count} Kapitel}}", "studyNbGames": "{count, plural, =1{{count} partier} other{{count} partier}}", "studyNbMembers": "{count, plural, =1{{count} Medlem} other{{count} Medlemmar}}", - "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Klistra in din PGN-kod här, upp till {count} parti} other{Klistra in din PGN-kod här, upp till {count} partier}}" + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Klistra in din PGN-kod här, upp till {count} parti} other{Klistra in din PGN-kod här, upp till {count} partier}}", + "timeagoJustNow": "just nu", + "timeagoRightNow": "just nu", + "timeagoCompleted": "slutfört", + "timeagoInNbSeconds": "{count, plural, =1{om {count} sekund} other{om {count} sekunder}}", + "timeagoInNbMinutes": "{count, plural, =1{om {count} minut} other{om {count} minuter}}", + "timeagoInNbHours": "{count, plural, =1{om {count} timme} other{om {count} timmar}}", + "timeagoInNbDays": "{count, plural, =1{om {count} dag} other{om {count} dagar}}", + "timeagoInNbWeeks": "{count, plural, =1{om {count} vecka} other{om {count} veckor}}", + "timeagoInNbMonths": "{count, plural, =1{om {count} månad} other{om {count} månader}}", + "timeagoInNbYears": "{count, plural, =1{om {count} år} other{om {count} år}}", + "timeagoNbMinutesAgo": "{count, plural, =1{{count} minut sedan} other{{count} minuter sedan}}", + "timeagoNbHoursAgo": "{count, plural, =1{{count} timme sedan} other{{count} timmar sedan}}", + "timeagoNbDaysAgo": "{count, plural, =1{{count} dag sedan} other{{count} dagar sedan}}", + "timeagoNbWeeksAgo": "{count, plural, =1{{count} vecka sedan} other{{count} veckor sedan}}", + "timeagoNbMonthsAgo": "{count, plural, =1{{count} månad sedan} other{{count} månader sedan}}", + "timeagoNbYearsAgo": "{count, plural, =1{{count} år sedan} other{{count} år sedan}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{{count} minut återstår} other{{count} minuter återstår}}", + "timeagoNbHoursRemaining": "{count, plural, =1{{count} timme återstår} other{{count} timmar återstår}}" } \ No newline at end of file diff --git a/lib/l10n/lila_tr.arb b/lib/l10n/lila_tr.arb index 0e3a512927..afabc8dd64 100644 --- a/lib/l10n/lila_tr.arb +++ b/lib/l10n/lila_tr.arb @@ -1546,5 +1546,23 @@ "studyNbChapters": "{count, plural, =1{{count} Bölüm} other{{count} Bölüm}}", "studyNbGames": "{count, plural, =1{{count} oyun} other{{count} Oyun}}", "studyNbMembers": "{count, plural, =1{{count} Üye} other{{count} Üye}}", - "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{PGN metninizi buraya yapıştırın, en fazla {count} oyuna kadar} other{PGN metninizi buraya yapıştırın, en fazla {count} oyuna kadar}}" + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{PGN metninizi buraya yapıştırın, en fazla {count} oyuna kadar} other{PGN metninizi buraya yapıştırın, en fazla {count} oyuna kadar}}", + "timeagoJustNow": "şu anda", + "timeagoRightNow": "hemen şimdi", + "timeagoCompleted": "tamamlanmış", + "timeagoInNbSeconds": "{count, plural, =1{{count} saniyede} other{{count} saniyede}}", + "timeagoInNbMinutes": "{count, plural, =1{{count} dakikada} other{{count} dakikada}}", + "timeagoInNbHours": "{count, plural, =1{{count} saatte} other{{count} saatte}}", + "timeagoInNbDays": "{count, plural, =1{{count} günde} other{{count} günde}}", + "timeagoInNbWeeks": "{count, plural, =1{{count} haftada} other{{count} haftada}}", + "timeagoInNbMonths": "{count, plural, =1{{count} ayda} other{{count} ayda}}", + "timeagoInNbYears": "{count, plural, =1{{count} yılda} other{{count} yılda}}", + "timeagoNbMinutesAgo": "{count, plural, =1{{count} dakika önce} other{{count} dakika önce}}", + "timeagoNbHoursAgo": "{count, plural, =1{{count} saat önce} other{{count} saat önce}}", + "timeagoNbDaysAgo": "{count, plural, =1{{count} gün önce} other{{count} gün önce}}", + "timeagoNbWeeksAgo": "{count, plural, =1{{count} hafta önce} other{{count} hafta önce}}", + "timeagoNbMonthsAgo": "{count, plural, =1{{count} ay önce} other{{count} ay önce}}", + "timeagoNbYearsAgo": "{count, plural, =1{{count} yıl önce} other{{count} yıl önce}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{{count} dakika kaldı} other{{count} dakika kaldı}}", + "timeagoNbHoursRemaining": "{count, plural, =1{{count} saat kaldı} other{{count} saat kaldı}}" } \ No newline at end of file diff --git a/lib/l10n/lila_uk.arb b/lib/l10n/lila_uk.arb index 2fa93fc4d3..857036123a 100644 --- a/lib/l10n/lila_uk.arb +++ b/lib/l10n/lila_uk.arb @@ -1540,5 +1540,23 @@ "studyNbChapters": "{count, plural, =1{{count} розділ} few{{count} розділи} many{{count} розділів} other{{count} розділи}}", "studyNbGames": "{count, plural, =1{{count} Партія} few{{count} Партії} many{{count} Партій} other{{count} Партій}}", "studyNbMembers": "{count, plural, =1{{count} учасник} few{{count} учасники} many{{count} учасників} other{{count} учасників}}", - "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Вставте ваш PGN текст тут, до {count} гри} few{Вставте ваш PGN текст тут, до {count} ігор} many{Вставте ваш PGN текст тут, до {count} ігор} other{Вставте ваш PGN текст тут, до {count} ігор}}" + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, =1{Вставте ваш PGN текст тут, до {count} гри} few{Вставте ваш PGN текст тут, до {count} ігор} many{Вставте ваш PGN текст тут, до {count} ігор} other{Вставте ваш PGN текст тут, до {count} ігор}}", + "timeagoJustNow": "щойно", + "timeagoRightNow": "зараз", + "timeagoCompleted": "завершено", + "timeagoInNbSeconds": "{count, plural, =1{за {count} секунду} few{за {count} секунди} many{за {count} секунд} other{за {count} секунди}}", + "timeagoInNbMinutes": "{count, plural, =1{за {count} хвилину} few{за {count} хвилини} many{за {count} хвилин} other{за {count} хвилини}}", + "timeagoInNbHours": "{count, plural, =1{за {count} годину} few{за {count} години} many{за {count} годин} other{за {count} години}}", + "timeagoInNbDays": "{count, plural, =1{за {count} день} few{за {count} дні} many{за {count} днів} other{за {count} дня}}", + "timeagoInNbWeeks": "{count, plural, =1{за {count} тиждень} few{за {count} тижні} many{за {count} тижнів} other{за {count} тижня}}", + "timeagoInNbMonths": "{count, plural, =1{за {count} місяць} few{за {count} місяці} many{за {count} місяців} other{за {count} місяця}}", + "timeagoInNbYears": "{count, plural, =1{за {count} рік} few{за {count} роки} many{за {count} років} other{за {count} року}}", + "timeagoNbMinutesAgo": "{count, plural, =1{{count} хвилину тому} few{{count} хвилини тому} many{{count} хвилин тому} other{{count} хвилини тому}}", + "timeagoNbHoursAgo": "{count, plural, =1{{count} годину тому} few{{count} години тому} many{{count} годин тому} other{{count} години тому}}", + "timeagoNbDaysAgo": "{count, plural, =1{{count} день тому} few{{count} дні тому} many{{count} днів тому} other{{count} дня тому}}", + "timeagoNbWeeksAgo": "{count, plural, =1{{count} тиждень тому} few{{count} тижні тому} many{{count} тижнів тому} other{{count} тижня тому}}", + "timeagoNbMonthsAgo": "{count, plural, =1{{count} місяць тому} few{{count} місяці тому} many{{count} місяців тому} other{{count} місяця тому}}", + "timeagoNbYearsAgo": "{count, plural, =1{{count} рік тому} few{{count} роки тому} many{{count} років тому} other{{count} року тому}}", + "timeagoNbMinutesRemaining": "{count, plural, =1{залишилася {count} хвилина} few{залишилося {count} хвилини} many{залишилося {count} хвилин} other{залишилося {count} хвилини}}", + "timeagoNbHoursRemaining": "{count, plural, =1{залишилася {count} година} few{залишилося {count} години} many{залишилося {count} годин} other{залишилося {count} години}}" } \ No newline at end of file diff --git a/lib/l10n/lila_vi.arb b/lib/l10n/lila_vi.arb index 475b276a39..6cbaf6c5ad 100644 --- a/lib/l10n/lila_vi.arb +++ b/lib/l10n/lila_vi.arb @@ -1547,5 +1547,23 @@ "studyNbChapters": "{count, plural, other{{count} Chương}}", "studyNbGames": "{count, plural, other{{count} Ván cờ}}", "studyNbMembers": "{count, plural, other{{count} Thành viên}}", - "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, other{Dán PGN ở đây, tối đa {count} ván}}" + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, other{Dán PGN ở đây, tối đa {count} ván}}", + "timeagoJustNow": "vừa mới đây", + "timeagoRightNow": "ngay bây giờ", + "timeagoCompleted": "đã hoàn thành", + "timeagoInNbSeconds": "{count, plural, other{trong {count} giây}}", + "timeagoInNbMinutes": "{count, plural, other{trong {count} phút}}", + "timeagoInNbHours": "{count, plural, other{trong {count} giờ}}", + "timeagoInNbDays": "{count, plural, other{trong {count} ngày}}", + "timeagoInNbWeeks": "{count, plural, other{trong {count} tuần}}", + "timeagoInNbMonths": "{count, plural, other{trong {count} tháng}}", + "timeagoInNbYears": "{count, plural, other{trong {count} năm}}", + "timeagoNbMinutesAgo": "{count, plural, other{{count} phút trước}}", + "timeagoNbHoursAgo": "{count, plural, other{{count} giờ trước}}", + "timeagoNbDaysAgo": "{count, plural, other{{count} ngày trước}}", + "timeagoNbWeeksAgo": "{count, plural, other{{count} tuần trước}}", + "timeagoNbMonthsAgo": "{count, plural, other{{count} tháng trước}}", + "timeagoNbYearsAgo": "{count, plural, other{{count} năm trước}}", + "timeagoNbMinutesRemaining": "{count, plural, other{còn {count} phút}}", + "timeagoNbHoursRemaining": "{count, plural, other{còn {count} giờ}}" } \ No newline at end of file diff --git a/lib/l10n/lila_zh.arb b/lib/l10n/lila_zh.arb index 6f6c6f6e75..56d9a47d2e 100644 --- a/lib/l10n/lila_zh.arb +++ b/lib/l10n/lila_zh.arb @@ -1513,5 +1513,23 @@ "studyNbChapters": "{count, plural, other{共 {count} 章}}", "studyNbGames": "{count, plural, other{共 {count} 盘棋}}", "studyNbMembers": "{count, plural, other{{count} 位成员}}", - "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, other{在此粘贴你的 PGN 文本,最多支持 {count} 个游戏}}" + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, other{在此粘贴你的 PGN 文本,最多支持 {count} 个游戏}}", + "timeagoJustNow": "刚刚", + "timeagoRightNow": "刚刚", + "timeagoCompleted": "已完成", + "timeagoInNbSeconds": "{count, plural, other{在 {count} 秒内}}", + "timeagoInNbMinutes": "{count, plural, other{在 {count} 分钟内}}", + "timeagoInNbHours": "{count, plural, other{在 {count} 小时内}}", + "timeagoInNbDays": "{count, plural, other{在 {count} 天内}}", + "timeagoInNbWeeks": "{count, plural, other{在 {count} 周内}}", + "timeagoInNbMonths": "{count, plural, other{在 {count} 月内}}", + "timeagoInNbYears": "{count, plural, other{在 {count} 年内}}", + "timeagoNbMinutesAgo": "{count, plural, other{{count} 分钟前}}", + "timeagoNbHoursAgo": "{count, plural, other{{count} 小时前}}", + "timeagoNbDaysAgo": "{count, plural, other{{count} 天前}}", + "timeagoNbWeeksAgo": "{count, plural, other{{count} 周前}}", + "timeagoNbMonthsAgo": "{count, plural, other{{count} 月前}}", + "timeagoNbYearsAgo": "{count, plural, other{{count} 年前}}", + "timeagoNbMinutesRemaining": "{count, plural, other{还剩 {count} 分钟}}", + "timeagoNbHoursRemaining": "{count, plural, other{还剩 {count} 小时}}" } \ No newline at end of file diff --git a/lib/l10n/lila_zh_TW.arb b/lib/l10n/lila_zh_TW.arb index 50468d8f23..5c78ee9052 100644 --- a/lib/l10n/lila_zh_TW.arb +++ b/lib/l10n/lila_zh_TW.arb @@ -1543,5 +1543,23 @@ "studyNbChapters": "{count, plural, other{第{count}章}}", "studyNbGames": "{count, plural, other{{count}對局}}", "studyNbMembers": "{count, plural, other{{count}位成員}}", - "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, other{在此貼上PGN文本,最多可導入{count}個棋局}}" + "studyPasteYourPgnTextHereUpToNbGames": "{count, plural, other{在此貼上PGN文本,最多可導入{count}個棋局}}", + "timeagoJustNow": "剛剛", + "timeagoRightNow": "現在", + "timeagoCompleted": "已結束", + "timeagoInNbSeconds": "{count, plural, other{{count}秒後}}", + "timeagoInNbMinutes": "{count, plural, other{{count}分後}}", + "timeagoInNbHours": "{count, plural, other{{count}小時後}}", + "timeagoInNbDays": "{count, plural, other{{count}天後}}", + "timeagoInNbWeeks": "{count, plural, other{{count}週後}}", + "timeagoInNbMonths": "{count, plural, other{{count}個月後}}", + "timeagoInNbYears": "{count, plural, other{{count}年後}}", + "timeagoNbMinutesAgo": "{count, plural, other{{count}分前}}", + "timeagoNbHoursAgo": "{count, plural, other{{count}小時前}}", + "timeagoNbDaysAgo": "{count, plural, other{{count}天前}}", + "timeagoNbWeeksAgo": "{count, plural, other{{count}週前}}", + "timeagoNbMonthsAgo": "{count, plural, other{{count}個月前}}", + "timeagoNbYearsAgo": "{count, plural, other{{count}年前}}", + "timeagoNbMinutesRemaining": "{count, plural, other{剩下 {count} 分鐘}}", + "timeagoNbHoursRemaining": "{count, plural, other{剩下 {count} 小時}}" } \ No newline at end of file diff --git a/lib/src/constants.dart b/lib/src/constants.dart index 240db4322b..dc0d78eeea 100644 --- a/lib/src/constants.dart +++ b/lib/src/constants.dart @@ -50,6 +50,7 @@ final List boardShadows = defaultTargetPlatform == TargetPlatform.iOS ? [] : kElevationToShadow[1]!; const kCardTextScaleFactor = 1.64; +const kCardBorderRadius = BorderRadius.all(Radius.circular(10.0)); const kMaxClockTextScaleFactor = 1.94; const kEmptyWidget = SizedBox.shrink(); const kEmptyFen = '8/8/8/8/8/8/8/8 w - - 0 1'; diff --git a/lib/src/utils/l10n.dart b/lib/src/utils/l10n.dart index 2090626a20..21b4b4177c 100644 --- a/lib/src/utils/l10n.dart +++ b/lib/src/utils/l10n.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; +import 'package:lichess_mobile/l10n/l10n.dart'; /// Returns a localized string with a single placeholder replaced by a widget. /// @@ -39,22 +40,42 @@ Text l10nWithWidget( ); } -final _dayFormatter = DateFormat.E().add_jm(); -final _monthFormatter = DateFormat.MMMd(); -final _dateFormatterWithYear = DateFormat.yMMMd(); +final _dateFormatterWithYear = DateFormat.yMMMMd(); +final _dateFormatterWithYearShort = DateFormat.yMMMd(); -String relativeDate(DateTime date) { - final diff = date.difference(DateTime.now()); +/// Formats a date as a relative date string from now. +String relativeDate(AppLocalizations l10n, DateTime date, {bool shortDate = true}) { + final now = DateTime.now(); + final diff = date.difference(now); - return (!diff.isNegative && diff.inDays == 0) + final yearFormatter = shortDate ? _dateFormatterWithYearShort : _dateFormatterWithYear; + + if (diff.isNegative) { + return diff.inDays == 0 + ? diff.inHours == 0 + ? l10n.timeagoNbMinutesAgo(diff.inMinutes.abs()) + : l10n.timeagoNbHoursAgo(diff.inHours.abs()) + : diff.inDays == 1 + ? l10n.yesterday + : diff.inDays.abs() <= 7 + ? l10n.timeagoNbDaysAgo(diff.inDays.abs()) + : diff.inDays.abs() <= 30 + ? l10n.timeagoNbWeeksAgo((diff.inDays.abs() / 7).round()) + : diff.inDays.abs() <= 365 + ? l10n.timeagoNbMonthsAgo((diff.inDays.abs() / 30).round()) + : yearFormatter.format(date); + } + return diff.inDays == 0 ? diff.inHours == 0 - ? 'in ${diff.inMinutes} minute${diff.inMinutes > 1 ? 's' : ''}' // TODO translate with https://github.com/lichess-org/lila/blob/65b28ea8e43e0133df6c7ed40e03c2954f247d1e/translation/source/timeago.xml#L8 - : 'in ${diff.inHours} hour${diff.inHours > 1 ? 's' : ''}' // TODO translate with https://github.com/lichess-org/lila/blob/65b28ea8e43e0133df6c7ed40e03c2954f247d1e/translation/source/timeago.xml#L12 + ? l10n.timeagoInNbMinutes(diff.inMinutes) + : l10n.timeagoInNbHours(diff.inHours) : diff.inDays.abs() <= 7 - ? _dayFormatter.format(date) - : diff.inDays.abs() < 365 - ? _monthFormatter.format(date) - : _dateFormatterWithYear.format(date); + ? l10n.timeagoInNbDays(diff.inDays) + : diff.inDays.abs() <= 30 + ? l10n.timeagoInNbWeeks((diff.inDays.abs() / 7).round()) + : diff.inDays.abs() <= 365 + ? l10n.timeagoInNbMonths((diff.inDays.abs() / 30).round()) + : yearFormatter.format(date); } /// Returns a localized locale name. diff --git a/lib/src/view/broadcast/broadcast_list_screen.dart b/lib/src/view/broadcast/broadcast_list_screen.dart index 98aee4235c..af322f4220 100644 --- a/lib/src/view/broadcast/broadcast_list_screen.dart +++ b/lib/src/view/broadcast/broadcast_list_screen.dart @@ -7,6 +7,8 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:intl/intl.dart'; +import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast.dart'; import 'package:lichess_mobile/src/model/broadcast/broadcast_providers.dart'; import 'package:lichess_mobile/src/model/common/id.dart'; @@ -21,7 +23,6 @@ import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/shimmer.dart'; const kDefaultBroadcastImage = AssetImage('assets/images/broadcast_image.png'); -const kBroadcastGridItemBorderRadius = BorderRadius.all(Radius.circular(16.0)); const kBroadcastGridItemContentPadding = EdgeInsets.symmetric(horizontal: 12.0); /// A screen that displays a paginated list of broadcasts. @@ -127,14 +128,14 @@ class _BodyState extends ConsumerState<_Body> { final highTierGridDelegate = SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: itemsByRow, - crossAxisSpacing: 16.0, + crossAxisSpacing: 12.0, mainAxisSpacing: 16.0, childAspectRatio: 1.45, ); final lowTierGridDelegate = SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: itemsByRow + 1, - crossAxisSpacing: 16.0, + crossAxisSpacing: 12.0, mainAxisSpacing: 16.0, childAspectRatio: screenWidth >= 1200 @@ -207,8 +208,11 @@ class _BodyState extends ConsumerState<_Body> { sliver: SliverGrid.builder( gridDelegate: highTierGridDelegate, itemBuilder: - (context, index) => - BroadcastCard(worker: _worker!, broadcast: activeHighTier[index]), + (context, index) => BroadcastCard( + worker: _worker!, + broadcast: activeHighTier[index], + aspectRatio: highTierGridDelegate.childAspectRatio, + ), itemCount: activeHighTier.length, ), ), @@ -218,8 +222,11 @@ class _BodyState extends ConsumerState<_Body> { sliver: SliverGrid.builder( gridDelegate: lowTierGridDelegate, itemBuilder: - (context, index) => - BroadcastCard(worker: _worker!, broadcast: activeLowTier[index]), + (context, index) => BroadcastCard( + worker: _worker!, + broadcast: activeLowTier[index], + aspectRatio: lowTierGridDelegate.childAspectRatio, + ), itemCount: activeLowTier.length, ), ), @@ -236,9 +243,16 @@ class _BodyState extends ConsumerState<_Body> { (broadcasts.isLoading && index >= pastItemsCount - loadingItems) ? ShimmerLoading( isLoading: true, - child: BroadcastCard.loading(_worker!), + child: BroadcastCard.loading( + worker: _worker!, + aspectRatio: lowTierGridDelegate.childAspectRatio, + ), ) - : BroadcastCard(worker: _worker!, broadcast: section.$3[index]), + : BroadcastCard( + worker: _worker!, + broadcast: section.$3[index], + aspectRatio: lowTierGridDelegate.childAspectRatio, + ), itemCount: section.$3.length, ), ), @@ -256,12 +270,18 @@ class _BodyState extends ConsumerState<_Body> { } class BroadcastCard extends StatefulWidget { - const BroadcastCard({required this.broadcast, required this.worker, super.key}); + const BroadcastCard({ + required this.broadcast, + required this.worker, + required this.aspectRatio, + super.key, + }); final Broadcast broadcast; final ImageColorWorker worker; + final double aspectRatio; - const BroadcastCard.loading(this.worker) + const BroadcastCard.loading({required this.worker, required this.aspectRatio}) : broadcast = const Broadcast( tour: BroadcastTournamentData( id: BroadcastTournamentId(''), @@ -298,23 +318,7 @@ class BroadcastCard extends StatefulWidget { typedef _CardColors = ({Color primaryContainer, Color onPrimaryContainer}); final Map _colorsCache = {}; -Future<_CardColors?> _computeImageColors( - ImageColorWorker worker, - String imageUrl, - ByteData imageBytes, -) async { - final response = await worker.getImageColors(imageBytes.buffer.asUint32List()); - if (response != null) { - final (:primaryContainer, :onPrimaryContainer) = response; - final cardColors = ( - primaryContainer: Color(primaryContainer), - onPrimaryContainer: Color(onPrimaryContainer), - ); - _colorsCache[NetworkImage(imageUrl)] = cardColors; - return cardColors; - } - return null; -} +final _dateFormat = DateFormat.MMMd().add_jm(); class _BroadcastCartState extends State { _CardColors? _cardColors; @@ -378,7 +382,17 @@ class _BroadcastCartState extends State { _cardColors?.onPrimaryContainer.withValues(alpha: 0.8) ?? textShade(context, 0.8); final bgHsl = HSLColor.fromColor(backgroundColor); final liveHsl = HSLColor.fromColor(LichessColors.red); - final liveColor = (bgHsl.lightness <= 0.5 ? liveHsl.withLightness(0.9) : liveHsl).toColor(); + final liveColor = (bgHsl.lightness <= 0.6 ? liveHsl.withLightness(0.9) : liveHsl).toColor(); + + String? eventDate; + if (widget.broadcast.round.startsAt != null) { + final diff = widget.broadcast.round.startsAt!.difference(DateTime.now()); + if (!diff.isNegative && diff.inDays >= 1) { + eventDate = _dateFormat.format(widget.broadcast.round.startsAt!); + } else { + eventDate = relativeDate(context.l10n, widget.broadcast.round.startsAt!); + } + } return GestureDetector( onTap: () { @@ -399,7 +413,7 @@ class _BroadcastCartState extends State { duration: const Duration(milliseconds: 500), clipBehavior: Clip.hardEdge, decoration: BoxDecoration( - borderRadius: kBroadcastGridItemBorderRadius, + borderRadius: kCardBorderRadius, color: backgroundColor, boxShadow: Theme.of(context).platform == TargetPlatform.iOS ? null : kElevationToShadow[1], @@ -449,31 +463,25 @@ class _BroadcastCartState extends State { Padding( padding: kBroadcastGridItemContentPadding, child: Row( + mainAxisAlignment: + widget.broadcast.isLive + ? MainAxisAlignment.spaceBetween + : MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.baseline, textBaseline: TextBaseline.alphabetic, children: [ if (!widget.broadcast.isFinished) ...[ - Text( - widget.broadcast.round.name, - style: TextStyle( - fontSize: 14, - color: subTitleColor, - letterSpacing: -0.2, - ), - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), - const SizedBox(width: 5.0), - ], - if (widget.broadcast.round.startsAt != null) - Expanded( + Flexible( + flex: widget.broadcast.isLive ? 1 : 0, child: Text( - relativeDate(widget.broadcast.round.startsAt!), - style: TextStyle(fontSize: 12, color: subTitleColor), + widget.broadcast.round.name, + style: TextStyle(color: subTitleColor, letterSpacing: -0.2), overflow: TextOverflow.ellipsis, maxLines: 1, ), ), + const SizedBox(width: 5.0), + ], if (widget.broadcast.isLive) ...[ Row( mainAxisSize: MainAxisSize.min, @@ -509,7 +517,15 @@ class _BroadcastCartState extends State { ), ], ), - ], + ] else if (eventDate != null) + Flexible( + child: Text( + eventDate, + style: TextStyle(fontSize: 12, color: subTitleColor), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), ], ), ), @@ -519,13 +535,12 @@ class _BroadcastCartState extends State { ), child: Text( widget.broadcast.title, - maxLines: 2, + maxLines: widget.aspectRatio == 1.0 ? 3 : 2, overflow: TextOverflow.ellipsis, style: TextStyle( color: titleColor, fontWeight: FontWeight.bold, height: 1.0, - fontSize: 16, ), ), ), @@ -550,6 +565,24 @@ class _BroadcastCartState extends State { } } +Future<_CardColors?> _computeImageColors( + ImageColorWorker worker, + String imageUrl, + ByteData imageBytes, +) async { + final response = await worker.getImageColors(imageBytes.buffer.asUint32List()); + if (response != null) { + final (:primaryContainer, :onPrimaryContainer) = response; + final cardColors = ( + primaryContainer: Color(primaryContainer), + onPrimaryContainer: Color(onPrimaryContainer), + ); + _colorsCache[NetworkImage(imageUrl)] = cardColors; + return cardColors; + } + return null; +} + /// Pre-cache images and extract colors for broadcasts. Future preCacheBroadcastImages( BuildContext context, { diff --git a/lib/src/view/watch/watch_tab_screen.dart b/lib/src/view/watch/watch_tab_screen.dart index 45daf9673b..10d6988069 100644 --- a/lib/src/view/watch/watch_tab_screen.dart +++ b/lib/src/view/watch/watch_tab_screen.dart @@ -280,7 +280,7 @@ class _BroadcastTile extends ConsumerWidget { ), ] else if (broadcast.round.startsAt != null) ...[ const SizedBox(width: 5.0), - Text(relativeDate(broadcast.round.startsAt!)), + Text(relativeDate(context.l10n, broadcast.round.startsAt!)), ], ], ), diff --git a/lib/src/widgets/platform.dart b/lib/src/widgets/platform.dart index 5fbc4e265b..87fffb58b6 100644 --- a/lib/src/widgets/platform.dart +++ b/lib/src/widgets/platform.dart @@ -97,9 +97,7 @@ class PlatformCard extends StatelessWidget { shape: borderRadius != null ? RoundedRectangleBorder(borderRadius: borderRadius!) - : const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(10.0)), - ), + : const RoundedRectangleBorder(borderRadius: kCardBorderRadius), semanticContainer: semanticContainer, clipBehavior: clipBehavior, child: child, @@ -108,9 +106,7 @@ class PlatformCard extends StatelessWidget { shape: borderRadius != null ? RoundedRectangleBorder(borderRadius: borderRadius!) - : const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(10.0)), - ), + : const RoundedRectangleBorder(borderRadius: kCardBorderRadius), color: color, shadowColor: shadowColor, semanticContainer: semanticContainer, diff --git a/scripts/update-arb-from-crowdin.mjs b/scripts/update-arb-from-crowdin.mjs index 6c1ac02935..8efec3bb1d 100755 --- a/scripts/update-arb-from-crowdin.mjs +++ b/scripts/update-arb-from-crowdin.mjs @@ -40,6 +40,7 @@ const modules = [ 'storm', 'streamer', 'study', + 'timeago', ] // list of keys (per module) to include in the ARB file From 2081125796bc90460630cb4b71b0ee623a67b2ec Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 19 Dec 2024 10:34:00 +0100 Subject: [PATCH 947/979] Remove timeago --- lib/src/intl.dart | 68 ------------------- lib/src/view/game/game_list_tile.dart | 4 +- .../offline_correspondence_games_screen.dart | 9 +-- lib/src/view/home/home_tab_screen.dart | 5 +- lib/src/view/play/ongoing_games_screen.dart | 7 +- .../view/puzzle/puzzle_history_screen.dart | 4 +- lib/src/view/study/study_list_screen.dart | 4 +- lib/src/view/user/user_profile.dart | 4 +- pubspec.lock | 8 --- pubspec.yaml | 1 - 10 files changed, 14 insertions(+), 100 deletions(-) diff --git a/lib/src/intl.dart b/lib/src/intl.dart index b93d216850..f415ff0622 100644 --- a/lib/src/intl.dart +++ b/lib/src/intl.dart @@ -5,7 +5,6 @@ import 'package:intl/intl.dart'; import 'package:lichess_mobile/src/binding.dart'; import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; -import 'package:timeago/timeago.dart' as timeago; /// Setup [Intl.defaultLocale] and timeago locale and messages. Future setupIntl(WidgetsBinding widgetsBinding) async { @@ -22,72 +21,5 @@ Future setupIntl(WidgetsBinding widgetsBinding) async { Intl.defaultLocale = locale.toLanguageTag(); - // we need to setup timeago locale manually - final currentLocale = Intl.getCurrentLocale(); - final longLocale = Intl.canonicalizedLocale(currentLocale); - final messages = _timeagoLocales[longLocale]; - if (messages != null) { - timeago.setLocaleMessages(longLocale, messages); - timeago.setDefaultLocale(longLocale); - } else { - final shortLocale = Intl.shortLocale(currentLocale); - final messages = _timeagoLocales[shortLocale]; - if (messages != null) { - timeago.setLocaleMessages(shortLocale, messages); - timeago.setDefaultLocale(shortLocale); - } - } - return locale; } - -final Map _timeagoLocales = { - 'am': timeago.AmMessages(), - 'ar': timeago.ArMessages(), - 'az': timeago.AzMessages(), - 'be': timeago.BeMessages(), - 'bs': timeago.BsMessages(), - 'ca': timeago.CaMessages(), - 'cs': timeago.CsMessages(), - 'da': timeago.DaMessages(), - 'de': timeago.DeMessages(), - 'dv': timeago.DvMessages(), - 'en': timeago.EnMessages(), - 'es': timeago.EsMessages(), - 'et': timeago.EtMessages(), - 'fa': timeago.FaMessages(), - 'fi': timeago.FiMessages(), - 'fr': timeago.FrMessages(), - 'gr': timeago.GrMessages(), - 'he': timeago.HeMessages(), - 'hi': timeago.HiMessages(), - 'hr': timeago.HrMessages(), - 'hu': timeago.HuMessages(), - 'id': timeago.IdMessages(), - 'it': timeago.ItMessages(), - 'ja': timeago.JaMessages(), - 'km': timeago.KmMessages(), - 'ko': timeago.KoMessages(), - 'ku': timeago.KuMessages(), - 'lv': timeago.LvMessages(), - 'mn': timeago.MnMessages(), - 'ms_MY': timeago.MsMyMessages(), - 'nb_NO': timeago.NbNoMessages(), - 'nl': timeago.NlMessages(), - 'nn_NO': timeago.NnNoMessages(), - 'pl': timeago.PlMessages(), - 'pt_BR': timeago.PtBrMessages(), - 'ro': timeago.RoMessages(), - 'ru': timeago.RuMessages(), - 'sr': timeago.SrMessages(), - 'sv': timeago.SvMessages(), - 'ta': timeago.TaMessages(), - 'th': timeago.ThMessages(), - 'tk': timeago.TkMessages(), - 'tr': timeago.TrMessages(), - 'uk': timeago.UkMessages(), - 'ur': timeago.UrMessages(), - 'vi': timeago.ViMessages(), - 'zh_CN': timeago.ZhCnMessages(), - 'zh': timeago.ZhMessages(), -}; diff --git a/lib/src/view/game/game_list_tile.dart b/lib/src/view/game/game_list_tile.dart index 9aa7014cf9..ecf75f2250 100644 --- a/lib/src/view/game/game_list_tile.dart +++ b/lib/src/view/game/game_list_tile.dart @@ -11,6 +11,7 @@ import 'package:lichess_mobile/src/model/game/game_status.dart'; import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/styles/lichess_colors.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/utils/l10n.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/share.dart'; @@ -23,7 +24,6 @@ import 'package:lichess_mobile/src/widgets/board_thumbnail.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/user_full_name.dart'; -import 'package:timeago/timeago.dart' as timeago; final _dateFormatter = DateFormat.yMMMd().add_Hm(); @@ -420,7 +420,7 @@ class ExtendedGameListTile extends StatelessWidget { aiLevel: opponent.aiLevel, rating: opponent.rating, ), - subtitle: Text(timeago.format(game.lastMoveAt)), + subtitle: Text(relativeDate(context.l10n, game.lastMoveAt, shortDate: false)), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ diff --git a/lib/src/view/game/offline_correspondence_games_screen.dart b/lib/src/view/game/offline_correspondence_games_screen.dart index d5e51b49a6..c3aad1e893 100644 --- a/lib/src/view/game/offline_correspondence_games_screen.dart +++ b/lib/src/view/game/offline_correspondence_games_screen.dart @@ -4,13 +4,13 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/correspondence/correspondence_game_storage.dart'; import 'package:lichess_mobile/src/model/correspondence/offline_correspondence_game.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/utils/l10n.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/correspondence/offline_correspondence_game_screen.dart'; import 'package:lichess_mobile/src/widgets/board_preview.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:lichess_mobile/src/widgets/user_full_name.dart'; -import 'package:timeago/timeago.dart' as timeago; class OfflineCorrespondenceGamesScreen extends ConsumerWidget { const OfflineCorrespondenceGamesScreen({super.key}); @@ -69,12 +69,7 @@ class OfflineCorrespondenceGamePreview extends ConsumerWidget { children: [ UserFullNameWidget(user: game.opponent.user, style: Styles.boardPreviewTitle), if (game.myTimeLeft(lastModified) != null) - Text( - timeago.format( - DateTime.now().add(game.myTimeLeft(lastModified)!), - allowFromNow: true, - ), - ), + Text(relativeDate(context.l10n, DateTime.now().add(game.myTimeLeft(lastModified)!))), Icon(game.perf.icon, size: 40, color: DefaultTextStyle.of(context).style.color), ], ), diff --git a/lib/src/view/home/home_tab_screen.dart b/lib/src/view/home/home_tab_screen.dart index 31179d5972..e357b686b3 100644 --- a/lib/src/view/home/home_tab_screen.dart +++ b/lib/src/view/home/home_tab_screen.dart @@ -36,7 +36,6 @@ import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/misc.dart'; import 'package:lichess_mobile/src/widgets/user_full_name.dart'; -import 'package:timeago/timeago.dart' as timeago; import 'package:url_launcher/url_launcher.dart'; final editModeProvider = StateProvider((ref) => false); @@ -663,9 +662,9 @@ class _GamePreviewCarouselItem extends StatelessWidget { ], Text( game.secondsLeft != null && game.isMyTurn - ? timeago.format( + ? relativeDate( + context.l10n, DateTime.now().add(Duration(seconds: game.secondsLeft!)), - allowFromNow: true, ) : game.isMyTurn ? context.l10n.yourTurn diff --git a/lib/src/view/play/ongoing_games_screen.dart b/lib/src/view/play/ongoing_games_screen.dart index d9843a22a1..97a0471701 100644 --- a/lib/src/view/play/ongoing_games_screen.dart +++ b/lib/src/view/play/ongoing_games_screen.dart @@ -4,13 +4,13 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/account/ongoing_game.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/utils/l10n.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/game/game_screen.dart'; import 'package:lichess_mobile/src/widgets/board_preview.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/user_full_name.dart'; -import 'package:timeago/timeago.dart' as timeago; class OngoingGamesScreen extends ConsumerWidget { const OngoingGamesScreen({super.key}); @@ -83,10 +83,7 @@ class OngoingGamePreview extends ConsumerWidget { Text(game.isMyTurn ? context.l10n.yourTurn : context.l10n.waitingForOpponent), if (game.isMyTurn && game.secondsLeft != null) Text( - timeago.format( - DateTime.now().add(Duration(seconds: game.secondsLeft!)), - allowFromNow: true, - ), + relativeDate(context.l10n, DateTime.now().add(Duration(seconds: game.secondsLeft!))), ), ], ), diff --git a/lib/src/view/puzzle/puzzle_history_screen.dart b/lib/src/view/puzzle/puzzle_history_screen.dart index b5561ae58d..4fcf7eb8cd 100644 --- a/lib/src/view/puzzle/puzzle_history_screen.dart +++ b/lib/src/view/puzzle/puzzle_history_screen.dart @@ -9,6 +9,7 @@ import 'package:lichess_mobile/src/model/puzzle/puzzle_activity.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_angle.dart'; import 'package:lichess_mobile/src/model/puzzle/puzzle_theme.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/utils/l10n.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; @@ -16,7 +17,6 @@ import 'package:lichess_mobile/src/view/puzzle/puzzle_screen.dart'; import 'package:lichess_mobile/src/widgets/board_thumbnail.dart'; import 'package:lichess_mobile/src/widgets/feedback.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; -import 'package:timeago/timeago.dart' as timeago; final _dateFormatter = DateFormat.yMMMd(); @@ -155,7 +155,7 @@ class _BodyState extends ConsumerState<_Body> { final title = DateTime.now().difference(element).inDays >= 15 ? _dateFormatter.format(element) - : timeago.format(element); + : relativeDate(context.l10n, element); return Padding( padding: const EdgeInsets.only(left: _kPuzzlePadding).add(Styles.sectionTopPadding), child: Text( diff --git a/lib/src/view/study/study_list_screen.dart b/lib/src/view/study/study_list_screen.dart index a824f99ab8..e134f99849 100644 --- a/lib/src/view/study/study_list_screen.dart +++ b/lib/src/view/study/study_list_screen.dart @@ -7,6 +7,7 @@ import 'package:lichess_mobile/src/model/study/study_filter.dart'; import 'package:lichess_mobile/src/model/study/study_list_paginator.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; +import 'package:lichess_mobile/src/utils/l10n.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/lichess_assets.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; @@ -19,7 +20,6 @@ import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:lichess_mobile/src/widgets/platform_search_bar.dart'; import 'package:lichess_mobile/src/widgets/user_full_name.dart'; import 'package:logging/logging.dart'; -import 'package:timeago/timeago.dart' as timeago; final _logger = Logger('StudyListScreen'); @@ -385,7 +385,7 @@ class _StudySubtitle extends StatelessWidget { ), const TextSpan(text: ' • '), ], - TextSpan(text: timeago.format(study.updatedAt)), + TextSpan(text: relativeDate(context.l10n, study.updatedAt)), ], ), ); diff --git a/lib/src/view/user/user_profile.dart b/lib/src/view/user/user_profile.dart index 24a05b38c0..5fb7758813 100644 --- a/lib/src/view/user/user_profile.dart +++ b/lib/src/view/user/user_profile.dart @@ -10,13 +10,13 @@ import 'package:lichess_mobile/src/model/user/profile.dart'; import 'package:lichess_mobile/src/model/user/user.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/duration.dart'; +import 'package:lichess_mobile/src/utils/l10n.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/lichess_assets.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/user/user_screen.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:linkify/linkify.dart'; -import 'package:timeago/timeago.dart' as timeago; import 'package:url_launcher/url_launcher.dart'; import 'countries.dart'; @@ -112,7 +112,7 @@ class UserProfileWidget extends ConsumerWidget { Text('${context.l10n.memberSince} ${DateFormat.yMMMMd().format(user.createdAt!)}'), if (user.seenAt != null) ...[ const SizedBox(height: 5), - Text(context.l10n.lastSeenActive(timeago.format(user.seenAt!))), + Text(context.l10n.lastSeenActive(relativeDate(context.l10n, user.seenAt!))), ], if (user.playTime != null) ...[ const SizedBox(height: 5), diff --git a/pubspec.lock b/pubspec.lock index ed5e1b50a3..2319ddc62f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1473,14 +1473,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.3" - timeago: - dependency: "direct main" - description: - name: timeago - sha256: "054cedf68706bb142839ba0ae6b135f6b68039f0b8301cbe8784ae653d5ff8de" - url: "https://pub.dev" - source: hosted - version: "3.7.0" timezone: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 6e2f559d44..72f8e3d380 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -73,7 +73,6 @@ dependencies: url: https://github.com/lichess-org/dart-stockfish.git ref: 0b4d8f20f72beb43c853230dd18063bee242487d stream_transform: ^2.1.0 - timeago: ^3.6.0 url_launcher: ^6.1.9 visibility_detector: ^0.4.0 wakelock_plus: ^1.1.1 From 47fba3278efbdff0d34012339f2aeb11c3676093 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 19 Dec 2024 11:58:40 +0100 Subject: [PATCH 948/979] Fix engine evaluation started from new analysis screens Fixes #1013 --- .../model/analysis/analysis_controller.dart | 94 +++++++------------ .../model/analysis/analysis_preferences.dart | 3 + .../broadcast/broadcast_game_controller.dart | 89 +++++++----------- lib/src/model/engine/evaluation_service.dart | 15 ++- lib/src/model/puzzle/puzzle_controller.dart | 6 +- lib/src/model/study/study_controller.dart | 84 ++++++----------- 6 files changed, 116 insertions(+), 175 deletions(-) diff --git a/lib/src/model/analysis/analysis_controller.dart b/lib/src/model/analysis/analysis_controller.dart index 6e6bdb08be..008f3c249f 100644 --- a/lib/src/model/analysis/analysis_controller.dart +++ b/lib/src/model/analysis/analysis_controller.dart @@ -191,20 +191,11 @@ class AnalysisController extends _$AnalysisController implements PgnTreeNotifier ); if (analysisState.isEngineAvailable) { - evaluationService - .initEngine( - _evaluationContext, - options: EvaluationOptions( - multiPv: prefs.numEvalLines, - cores: prefs.numEngineCores, - searchTime: prefs.engineSearchTime, - ), - ) - .then((_) { - _startEngineEvalTimer = Timer(const Duration(milliseconds: 250), () { - _startEngineEval(); - }); - }); + evaluationService.initEngine(_evaluationContext, options: _evaluationOptions).then((_) { + _startEngineEvalTimer = Timer(const Duration(milliseconds: 250), () { + _startEngineEval(); + }); + }); } return analysisState; @@ -213,6 +204,9 @@ class AnalysisController extends _$AnalysisController implements PgnTreeNotifier EvaluationContext get _evaluationContext => EvaluationContext(variant: _variant, initialPosition: _root.position); + EvaluationOptions get _evaluationOptions => + ref.read(analysisPreferencesProvider).evaluationOptions; + void onUserMove(NormalMove move, {bool shouldReplace = false}) { if (!state.requireValue.position.isLegal(move)) return; @@ -360,17 +354,9 @@ class AnalysisController extends _$AnalysisController implements PgnTreeNotifier ); if (state.requireValue.isEngineAvailable) { - final prefs = ref.read(analysisPreferencesProvider); await ref .read(evaluationServiceProvider) - .initEngine( - _evaluationContext, - options: EvaluationOptions( - multiPv: prefs.numEvalLines, - cores: prefs.numEngineCores, - searchTime: prefs.engineSearchTime, - ), - ); + .initEngine(_evaluationContext, options: _evaluationOptions); _startEngineEval(); } else { _stopEngineEval(); @@ -381,15 +367,7 @@ class AnalysisController extends _$AnalysisController implements PgnTreeNotifier void setNumEvalLines(int numEvalLines) { ref.read(analysisPreferencesProvider.notifier).setNumEvalLines(numEvalLines); - ref - .read(evaluationServiceProvider) - .setOptions( - EvaluationOptions( - multiPv: numEvalLines, - cores: ref.read(analysisPreferencesProvider).numEngineCores, - searchTime: ref.read(analysisPreferencesProvider).engineSearchTime, - ), - ); + ref.read(evaluationServiceProvider).setOptions(_evaluationOptions); _root.updateAll((node) => node.eval = null); @@ -406,15 +384,7 @@ class AnalysisController extends _$AnalysisController implements PgnTreeNotifier void setEngineCores(int numEngineCores) { ref.read(analysisPreferencesProvider.notifier).setEngineCores(numEngineCores); - ref - .read(evaluationServiceProvider) - .setOptions( - EvaluationOptions( - multiPv: ref.read(analysisPreferencesProvider).numEvalLines, - cores: numEngineCores, - searchTime: ref.read(analysisPreferencesProvider).engineSearchTime, - ), - ); + ref.read(evaluationServiceProvider).setOptions(_evaluationOptions); _startEngineEval(); } @@ -422,15 +392,7 @@ class AnalysisController extends _$AnalysisController implements PgnTreeNotifier void setEngineSearchTime(Duration searchTime) { ref.read(analysisPreferencesProvider.notifier).setEngineSearchTime(searchTime); - ref - .read(evaluationServiceProvider) - .setOptions( - EvaluationOptions( - multiPv: ref.read(analysisPreferencesProvider).numEvalLines, - cores: ref.read(analysisPreferencesProvider).numEngineCores, - searchTime: searchTime, - ), - ); + ref.read(evaluationServiceProvider).setOptions(_evaluationOptions); _startEngineEval(); } @@ -553,6 +515,14 @@ class AnalysisController extends _$AnalysisController implements PgnTreeNotifier } } + void _refreshCurrentNode() { + state = AsyncData( + state.requireValue.copyWith( + currentNode: AnalysisCurrentNode.fromNode(_root.nodeAt(state.requireValue.currentPath)), + ), + ); + } + Future<(UciPath, FullOpening)?> _fetchOpening(Node fromNode, UciPath path) async { if (!kOpeningAllowedVariants.contains(_variant)) return null; @@ -572,15 +542,16 @@ class AnalysisController extends _$AnalysisController implements PgnTreeNotifier final curState = state.requireValue; if (curState.currentPath == path) { - state = AsyncData( - curState.copyWith(currentNode: AnalysisCurrentNode.fromNode(_root.nodeAt(path))), - ); + _refreshCurrentNode(); } } - void _startEngineEval() { + Future _startEngineEval() async { final curState = state.requireValue; if (!curState.isEngineAvailable) return; + await ref + .read(evaluationServiceProvider) + .ensureEngineInitialized(_evaluationContext, options: _evaluationOptions); ref .read(evaluationServiceProvider) .start( @@ -589,7 +560,13 @@ class AnalysisController extends _$AnalysisController implements PgnTreeNotifier initialPositionEval: _root.eval, shouldEmit: (work) => work.path == state.valueOrNull?.currentPath, ) - ?.forEach((t) => _root.updateAt(t.$1.path, (node) => node.eval = t.$2)); + ?.forEach((t) { + final (work, eval) = t; + _root.updateAt(work.path, (node) => node.eval = eval); + if (work.path == curState.currentPath && eval.searchTime >= work.searchTime) { + _refreshCurrentNode(); + } + }); } void _debouncedStartEngineEval() { @@ -601,12 +578,7 @@ class AnalysisController extends _$AnalysisController implements PgnTreeNotifier void _stopEngineEval() { ref.read(evaluationServiceProvider).stop(); // update the current node with last cached eval - final curState = state.requireValue; - state = AsyncData( - curState.copyWith( - currentNode: AnalysisCurrentNode.fromNode(_root.nodeAt(curState.currentPath)), - ), - ); + _refreshCurrentNode(); } void _listenToServerAnalysisEvents() { diff --git a/lib/src/model/analysis/analysis_preferences.dart b/lib/src/model/analysis/analysis_preferences.dart index 0916da00a0..5b16d74da4 100644 --- a/lib/src/model/analysis/analysis_preferences.dart +++ b/lib/src/model/analysis/analysis_preferences.dart @@ -99,6 +99,9 @@ class AnalysisPrefs with _$AnalysisPrefs implements Serializable { factory AnalysisPrefs.fromJson(Map json) { return _$AnalysisPrefsFromJson(json); } + + EvaluationOptions get evaluationOptions => + EvaluationOptions(multiPv: numEvalLines, cores: numEngineCores, searchTime: engineSearchTime); } Duration _searchTimeDefault() { diff --git a/lib/src/model/broadcast/broadcast_game_controller.dart b/lib/src/model/broadcast/broadcast_game_controller.dart index ee14654f14..1a13c90547 100644 --- a/lib/src/model/broadcast/broadcast_game_controller.dart +++ b/lib/src/model/broadcast/broadcast_game_controller.dart @@ -120,20 +120,11 @@ class BroadcastGameController extends _$BroadcastGameController implements PgnTr ); if (broadcastState.isLocalEvaluationEnabled) { - evaluationService - .initEngine( - _evaluationContext, - options: EvaluationOptions( - multiPv: prefs.numEvalLines, - cores: prefs.numEngineCores, - searchTime: ref.read(analysisPreferencesProvider).engineSearchTime, - ), - ) - .then((_) { - _startEngineEvalTimer = Timer(const Duration(milliseconds: 250), () { - _startEngineEval(); - }); - }); + evaluationService.initEngine(_evaluationContext, options: _evaluationOptions).then((_) { + _startEngineEvalTimer = Timer(const Duration(milliseconds: 250), () { + _startEngineEval(); + }); + }); } return broadcastState; @@ -251,6 +242,9 @@ class BroadcastGameController extends _$BroadcastGameController implements PgnTr EvaluationContext get _evaluationContext => EvaluationContext(variant: Variant.standard, initialPosition: _root.position); + EvaluationOptions get _evaluationOptions => + ref.read(analysisPreferencesProvider).evaluationOptions; + void onUserMove(NormalMove move) { if (!state.hasValue) return; @@ -388,17 +382,9 @@ class BroadcastGameController extends _$BroadcastGameController implements PgnTr ); if (state.requireValue.isLocalEvaluationEnabled) { - final prefs = ref.read(analysisPreferencesProvider); await ref .read(evaluationServiceProvider) - .initEngine( - _evaluationContext, - options: EvaluationOptions( - multiPv: prefs.numEvalLines, - cores: prefs.numEngineCores, - searchTime: ref.read(analysisPreferencesProvider).engineSearchTime, - ), - ); + .initEngine(_evaluationContext, options: _evaluationOptions); _startEngineEval(); } else { _stopEngineEval(); @@ -411,15 +397,7 @@ class BroadcastGameController extends _$BroadcastGameController implements PgnTr ref.read(analysisPreferencesProvider.notifier).setNumEvalLines(numEvalLines); - ref - .read(evaluationServiceProvider) - .setOptions( - EvaluationOptions( - multiPv: numEvalLines, - cores: ref.read(analysisPreferencesProvider).numEngineCores, - searchTime: ref.read(analysisPreferencesProvider).engineSearchTime, - ), - ); + ref.read(evaluationServiceProvider).setOptions(_evaluationOptions); _root.updateAll((node) => node.eval = null); @@ -435,15 +413,7 @@ class BroadcastGameController extends _$BroadcastGameController implements PgnTr void setEngineCores(int numEngineCores) { ref.read(analysisPreferencesProvider.notifier).setEngineCores(numEngineCores); - ref - .read(evaluationServiceProvider) - .setOptions( - EvaluationOptions( - multiPv: ref.read(analysisPreferencesProvider).numEvalLines, - cores: numEngineCores, - searchTime: ref.read(analysisPreferencesProvider).engineSearchTime, - ), - ); + ref.read(evaluationServiceProvider).setOptions(_evaluationOptions); _startEngineEval(); } @@ -451,15 +421,7 @@ class BroadcastGameController extends _$BroadcastGameController implements PgnTr void setEngineSearchTime(Duration searchTime) { ref.read(analysisPreferencesProvider.notifier).setEngineSearchTime(searchTime); - ref - .read(evaluationServiceProvider) - .setOptions( - EvaluationOptions( - multiPv: ref.read(analysisPreferencesProvider).numEvalLines, - cores: ref.read(analysisPreferencesProvider).numEngineCores, - searchTime: searchTime, - ), - ); + ref.read(evaluationServiceProvider).setOptions(_evaluationOptions); _startEngineEval(); } @@ -540,10 +502,13 @@ class BroadcastGameController extends _$BroadcastGameController implements PgnTr } } - void _startEngineEval() { + Future _startEngineEval() async { if (!state.hasValue) return; if (!state.requireValue.isLocalEvaluationEnabled) return; + await ref + .read(evaluationServiceProvider) + .ensureEngineInitialized(_evaluationContext, options: _evaluationOptions); ref .read(evaluationServiceProvider) .start( @@ -552,7 +517,21 @@ class BroadcastGameController extends _$BroadcastGameController implements PgnTr initialPositionEval: _root.eval, shouldEmit: (work) => work.path == state.valueOrNull?.currentPath, ) - ?.forEach((t) => _root.updateAt(t.$1.path, (node) => node.eval = t.$2)); + ?.forEach((t) { + final (work, eval) = t; + _root.updateAt(work.path, (node) => node.eval = eval); + if (work.path == state.requireValue.currentPath && eval.searchTime >= work.searchTime) { + _refreshCurrentNode(); + } + }); + } + + void _refreshCurrentNode() { + state = AsyncData( + state.requireValue.copyWith( + currentNode: AnalysisCurrentNode.fromNode(_root.nodeAt(state.requireValue.currentPath)), + ), + ); } void _debouncedStartEngineEval() { @@ -566,11 +545,7 @@ class BroadcastGameController extends _$BroadcastGameController implements PgnTr ref.read(evaluationServiceProvider).stop(); // update the current node with last cached eval - state = AsyncData( - state.requireValue.copyWith( - currentNode: AnalysisCurrentNode.fromNode(_root.nodeAt(state.requireValue.currentPath)), - ), - ); + _refreshCurrentNode(); } ({Duration? parentClock, Duration? clock}) _getClocks(UciPath path) { diff --git a/lib/src/model/engine/evaluation_service.dart b/lib/src/model/engine/evaluation_service.dart index e2006aac28..63673738eb 100644 --- a/lib/src/model/engine/evaluation_service.dart +++ b/lib/src/model/engine/evaluation_service.dart @@ -49,6 +49,8 @@ class EvaluationService { /// Initialize the engine with the given context and options. /// + /// If the engine is already initialized, it is disposed first. + /// /// An optional [engineFactory] can be provided, it defaults to Stockfish. /// /// If [options] is not provided, the default options are used. @@ -59,7 +61,7 @@ class EvaluationService { Engine Function() engineFactory = StockfishEngine.new, EvaluationOptions? options, }) async { - if (context != _context) { + if (_context != null || _engine != null) { await disposeEngine(); } _context = context; @@ -81,6 +83,17 @@ class EvaluationService { }); } + /// Ensure the engine is initialized with the given context and options. + Future ensureEngineInitialized( + EvaluationContext context, { + Engine Function() engineFactory = StockfishEngine.new, + EvaluationOptions? options, + }) async { + if (_engine == null || _context != context) { + await initEngine(context, engineFactory: engineFactory, options: options); + } + } + void setOptions(EvaluationOptions options) { stop(); _options = options; diff --git a/lib/src/model/puzzle/puzzle_controller.dart b/lib/src/model/puzzle/puzzle_controller.dart index 0353e41c9a..a28fd01021 100644 --- a/lib/src/model/puzzle/puzzle_controller.dart +++ b/lib/src/model/puzzle/puzzle_controller.dart @@ -447,8 +447,9 @@ class PuzzleController extends _$PuzzleController { return pgn; } - void _startEngineEval() { + Future _startEngineEval() async { if (!state.isEngineEnabled) return; + await ref.read(evaluationServiceProvider).ensureEngineInitialized(state.evaluationContext); _engineEvalDebounce( () => ref .read(evaluationServiceProvider) @@ -463,6 +464,9 @@ class PuzzleController extends _$PuzzleController { _gameTree.updateAt(work.path, (node) { node.eval = eval; }); + if (work.path == state.currentPath && eval.searchTime >= work.searchTime) { + state = state.copyWith(node: _gameTree.branchAt(state.currentPath).view); + } }), ); } diff --git a/lib/src/model/study/study_controller.dart b/lib/src/model/study/study_controller.dart index 6e2c2e6e73..7d44097006 100644 --- a/lib/src/model/study/study_controller.dart +++ b/lib/src/model/study/study_controller.dart @@ -137,14 +137,7 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { await evaluationService.disposeEngine(); evaluationService - .initEngine( - _evaluationContext(studyState.variant), - options: EvaluationOptions( - multiPv: prefs.numEvalLines, - cores: prefs.numEngineCores, - searchTime: ref.read(analysisPreferencesProvider).engineSearchTime, - ), - ) + .initEngine(_evaluationContext(studyState.variant), options: _evaluationOptions) .then((_) { _startEngineEvalTimer = Timer(const Duration(milliseconds: 250), () { _startEngineEval(); @@ -200,6 +193,9 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { EvaluationContext _evaluationContext(Variant variant) => EvaluationContext(variant: variant, initialPosition: _root.position); + EvaluationOptions get _evaluationOptions => + ref.read(analysisPreferencesProvider).evaluationOptions; + void onUserMove(NormalMove move) { if (!state.hasValue || state.requireValue.position == null) return; @@ -366,17 +362,9 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { ); if (state.requireValue.isEngineAvailable) { - final prefs = ref.read(analysisPreferencesProvider); await ref .read(evaluationServiceProvider) - .initEngine( - _evaluationContext(state.requireValue.variant), - options: EvaluationOptions( - multiPv: prefs.numEvalLines, - cores: prefs.numEngineCores, - searchTime: ref.read(analysisPreferencesProvider).engineSearchTime, - ), - ); + .initEngine(_evaluationContext(state.requireValue.variant), options: _evaluationOptions); _startEngineEval(); } else { _stopEngineEval(); @@ -389,15 +377,7 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { ref.read(analysisPreferencesProvider.notifier).setNumEvalLines(numEvalLines); - ref - .read(evaluationServiceProvider) - .setOptions( - EvaluationOptions( - multiPv: numEvalLines, - cores: ref.read(analysisPreferencesProvider).numEngineCores, - searchTime: ref.read(analysisPreferencesProvider).engineSearchTime, - ), - ); + ref.read(evaluationServiceProvider).setOptions(_evaluationOptions); _root.updateAll((node) => node.eval = null); @@ -413,15 +393,7 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { void setEngineCores(int numEngineCores) { ref.read(analysisPreferencesProvider.notifier).setEngineCores(numEngineCores); - ref - .read(evaluationServiceProvider) - .setOptions( - EvaluationOptions( - multiPv: ref.read(analysisPreferencesProvider).numEvalLines, - cores: numEngineCores, - searchTime: ref.read(analysisPreferencesProvider).engineSearchTime, - ), - ); + ref.read(evaluationServiceProvider).setOptions(_evaluationOptions); _startEngineEval(); } @@ -429,15 +401,7 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { void setEngineSearchTime(Duration searchTime) { ref.read(analysisPreferencesProvider.notifier).setEngineSearchTime(searchTime); - ref - .read(evaluationServiceProvider) - .setOptions( - EvaluationOptions( - multiPv: ref.read(analysisPreferencesProvider).numEvalLines, - cores: ref.read(analysisPreferencesProvider).numEngineCores, - searchTime: searchTime, - ), - ); + ref.read(evaluationServiceProvider).setOptions(_evaluationOptions); _startEngineEval(); } @@ -514,20 +478,34 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { } } + void _refreshCurrentNode() { + state = AsyncData( + state.requireValue.copyWith( + currentNode: StudyCurrentNode.fromNode(_root.nodeAt(state.requireValue.currentPath)), + ), + ); + } + void _startEngineEval() { - final state = this.state.valueOrNull; - if (state == null || !state.isEngineAvailable) return; + final curState = state.valueOrNull; + if (curState == null || !curState.isEngineAvailable) return; ref .read(evaluationServiceProvider) .start( - state.currentPath, - _root.branchesOn(state.currentPath).map(Step.fromNode), + curState.currentPath, + _root.branchesOn(curState.currentPath).map(Step.fromNode), // Note: AnalysisController passes _root.eval as initialPositionEval here, // but for studies this leads to false positive cache hits when switching between chapters. - shouldEmit: (work) => work.path == this.state.valueOrNull?.currentPath, + shouldEmit: (work) => work.path == state.valueOrNull?.currentPath, ) - ?.forEach((t) => _root.updateAt(t.$1.path, (node) => node.eval = t.$2)); + ?.forEach((t) { + final (work, eval) = t; + _root.updateAt(work.path, (node) => node.eval = eval); + if (work.path == state.requireValue.currentPath && eval.searchTime >= work.searchTime) { + _refreshCurrentNode(); + } + }); } void _debouncedStartEngineEval() { @@ -542,11 +520,7 @@ class StudyController extends _$StudyController implements PgnTreeNotifier { if (!state.hasValue) return; // update the current node with last cached eval - state = AsyncValue.data( - state.requireValue.copyWith( - currentNode: StudyCurrentNode.fromNode(_root.nodeAt(state.requireValue.currentPath)), - ), - ); + _refreshCurrentNode(); } } From 24a2747c20636450898b5b09b4ef52143556ab9f Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Sat, 7 Dec 2024 12:31:26 +0100 Subject: [PATCH 949/979] WIP on custom theme --- lib/src/app.dart | 29 +++- lib/src/init.dart | 19 +- .../model/settings/general_preferences.dart | 43 ++--- lib/src/utils/color_palette.dart | 6 + lib/src/utils/json.dart | 58 +++++++ lib/src/view/puzzle/puzzle_tab_screen.dart | 4 - .../settings/account_preferences_screen.dart | 1 - .../settings/app_background_mode_screen.dart | 2 +- lib/src/view/settings/board_theme_screen.dart | 20 +-- .../view/settings/settings_tab_screen.dart | 12 +- lib/src/view/settings/theme_screen.dart | 162 ++++++++++++++++-- lib/src/widgets/settings.dart | 3 + pubspec.lock | 8 + pubspec.yaml | 1 + 14 files changed, 286 insertions(+), 82 deletions(-) diff --git a/lib/src/app.dart b/lib/src/app.dart index 158855a519..40cf12ec4c 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -17,6 +17,7 @@ import 'package:lichess_mobile/src/navigation.dart'; import 'package:lichess_mobile/src/network/connectivity.dart'; import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/network/socket.dart'; +import 'package:lichess_mobile/src/styles/lichess_colors.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; @@ -139,13 +140,27 @@ class _AppState extends ConsumerState { final dynamicColorScheme = brightness == Brightness.light ? fixedLightScheme : fixedDarkScheme; - final colorScheme = - generalPrefs.systemColors && dynamicColorScheme != null - ? dynamicColorScheme - : ColorScheme.fromSeed( - seedColor: boardTheme.colors.darkSquare, - brightness: brightness, - ); + ColorScheme colorScheme; + if (generalPrefs.customThemeEnabled) { + if (generalPrefs.customThemeSeed != null) { + colorScheme = ColorScheme.fromSeed( + seedColor: generalPrefs.customThemeSeed!, + brightness: brightness, + ); + } else if (dynamicColorScheme != null) { + colorScheme = dynamicColorScheme; + } else { + colorScheme = ColorScheme.fromSeed( + seedColor: LichessColors.primary[500]!, + brightness: brightness, + ); + } + } else { + colorScheme = ColorScheme.fromSeed( + seedColor: boardTheme.colors.darkSquare, + brightness: brightness, + ); + } final cupertinoThemeData = CupertinoThemeData( primaryColor: colorScheme.primary, diff --git a/lib/src/init.dart b/lib/src/init.dart index 145117567f..d4ac98c44f 100644 --- a/lib/src/init.dart +++ b/lib/src/init.dart @@ -12,6 +12,7 @@ import 'package:lichess_mobile/src/db/secure_storage.dart'; import 'package:lichess_mobile/src/model/notifications/notification_service.dart'; import 'package:lichess_mobile/src/model/notifications/notifications.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; +import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; import 'package:lichess_mobile/src/utils/chessboard.dart'; import 'package:lichess_mobile/src/utils/color_palette.dart'; @@ -92,11 +93,19 @@ Future androidDisplayInitialization(WidgetsBinding widgetsBinding) async { await DynamicColorPlugin.getCorePalette().then((value) { setCorePalette(value); - if (getCorePalette() != null && prefs.getString(PrefCategory.board.storageKey) == null) { - prefs.setString( - PrefCategory.board.storageKey, - jsonEncode(BoardPrefs.defaults.copyWith(boardTheme: BoardTheme.system)), - ); + if (getCorePalette() != null) { + if (prefs.getString(PrefCategory.general.storageKey) == null) { + prefs.setString( + PrefCategory.general.storageKey, + jsonEncode(GeneralPrefs.defaults.copyWith(customThemeEnabled: true)), + ); + } + if (prefs.getString(PrefCategory.board.storageKey) == null) { + prefs.setString( + PrefCategory.board.storageKey, + jsonEncode(BoardPrefs.defaults.copyWith(boardTheme: BoardTheme.system)), + ); + } } }); } catch (e) { diff --git a/lib/src/model/settings/general_preferences.dart b/lib/src/model/settings/general_preferences.dart index 70f18c3e69..b61dfc3b77 100644 --- a/lib/src/model/settings/general_preferences.dart +++ b/lib/src/model/settings/general_preferences.dart @@ -1,8 +1,9 @@ -import 'dart:ui' show Locale; +import 'dart:ui' show Color, Locale; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; +import 'package:lichess_mobile/src/utils/json.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'general_preferences.freezed.dart'; @@ -26,7 +27,7 @@ class GeneralPreferences extends _$GeneralPreferences with PreferencesStorage setThemeMode(BackgroundThemeMode themeMode) { + Future setBackgroundThemeMode(BackgroundThemeMode themeMode) { return save(state.copyWith(themeMode: themeMode)); } @@ -46,9 +47,9 @@ class GeneralPreferences extends _$GeneralPreferences with PreferencesStorage toggleSystemColors() async { - await save(state.copyWith(systemColors: !state.systemColors)); - if (state.systemColors == false) { + Future toggleCustomTheme() async { + await save(state.copyWith(customThemeEnabled: !state.customThemeEnabled)); + if (state.customThemeEnabled == false) { final boardTheme = ref.read(boardPreferencesProvider).boardTheme; if (boardTheme == BoardTheme.system) { await ref.read(boardPreferencesProvider.notifier).setBoardTheme(BoardTheme.brown); @@ -57,27 +58,10 @@ class GeneralPreferences extends _$GeneralPreferences with PreferencesStorage? _localeToJson(Locale? locale) { - return locale != null - ? { - 'languageCode': locale.languageCode, - 'countryCode': locale.countryCode, - 'scriptCode': locale.scriptCode, - } - : null; -} -Locale? _localeFromJson(Map? json) { - if (json == null) { - return null; + Future setCustomThemeSeed(Color? color) { + return save(state.copyWith(customThemeSeed: color)); } - return Locale.fromSubtags( - languageCode: json['languageCode'] as String, - countryCode: json['countryCode'] as String?, - scriptCode: json['scriptCode'] as String?, - ); } @Freezed(fromJson: true, toJson: true) @@ -89,11 +73,14 @@ class GeneralPrefs with _$GeneralPrefs implements Serializable { @JsonKey(unknownEnumValue: SoundTheme.standard) required SoundTheme soundTheme, @JsonKey(defaultValue: 0.8) required double masterVolume, - /// Should enable system color palette (android 12+ only) - @JsonKey(defaultValue: true) required bool systemColors, + /// Should enable custom theme + @JsonKey(defaultValue: false) required bool customThemeEnabled, + + /// Custom theme seed color + @ColorConverter() Color? customThemeSeed, /// Locale to use in the app, use system locale if null - @JsonKey(toJson: _localeToJson, fromJson: _localeFromJson) Locale? locale, + @LocaleConverter() Locale? locale, }) = _GeneralPrefs; static const defaults = GeneralPrefs( @@ -101,7 +88,7 @@ class GeneralPrefs with _$GeneralPrefs implements Serializable { isSoundEnabled: true, soundTheme: SoundTheme.standard, masterVolume: 0.8, - systemColors: true, + customThemeEnabled: true, ); factory GeneralPrefs.fromJson(Map json) { diff --git a/lib/src/utils/color_palette.dart b/lib/src/utils/color_palette.dart index 6ecdd18d82..70c8d262b3 100644 --- a/lib/src/utils/color_palette.dart +++ b/lib/src/utils/color_palette.dart @@ -45,6 +45,12 @@ void setCorePalette(CorePalette? palette) { } } +Color? getCorePalettePrimary() { + return _corePalette?.primary != null + ? Color(_corePalette!.primary.get(50)) + : null; +} + /// Get the core palette if available (android 12+ only). CorePalette? getCorePalette() { return _corePalette; diff --git a/lib/src/utils/json.dart b/lib/src/utils/json.dart index 2bc6852ebe..4a19699502 100644 --- a/lib/src/utils/json.dart +++ b/lib/src/utils/json.dart @@ -1,6 +1,64 @@ +import 'dart:ui' show Color, Locale; + import 'package:deep_pick/deep_pick.dart'; +import 'package:json_annotation/json_annotation.dart'; import 'package:lichess_mobile/src/model/common/uci.dart'; +class LocaleConverter implements JsonConverter?> { + const LocaleConverter(); + + @override + Locale? fromJson(Map? json) { + if (json == null) { + return null; + } + return Locale.fromSubtags( + languageCode: json['languageCode'] as String, + countryCode: json['countryCode'] as String?, + scriptCode: json['scriptCode'] as String?, + ); + } + + @override + Map? toJson(Locale? locale) { + return locale != null + ? { + 'languageCode': locale.languageCode, + 'countryCode': locale.countryCode, + 'scriptCode': locale.scriptCode, + } + : null; + } +} + +class ColorConverter implements JsonConverter?> { + const ColorConverter(); + + @override + Color? fromJson(Map? json) { + return json != null + ? Color.from( + alpha: json['a'] as double, + red: json['r'] as double, + green: json['g'] as double, + blue: json['b'] as double, + ) + : null; + } + + @override + Map? toJson(Color? color) { + return color != null + ? { + 'a': color.a, + 'r': color.r, + 'g': color.g, + 'b': color.b, + } + : null; + } +} + extension UciExtension on Pick { /// Matches a UciCharPair from a string. UciCharPair asUciCharPairOrThrow() { diff --git a/lib/src/view/puzzle/puzzle_tab_screen.dart b/lib/src/view/puzzle/puzzle_tab_screen.dart index a870b218b9..189752b604 100644 --- a/lib/src/view/puzzle/puzzle_tab_screen.dart +++ b/lib/src/view/puzzle/puzzle_tab_screen.dart @@ -372,10 +372,6 @@ class _PuzzleMenuListTile extends StatelessWidget { leading: Icon( icon, size: Styles.mainListTileIconSize, - color: - Theme.of(context).platform == TargetPlatform.iOS - ? CupertinoTheme.of(context).primaryColor - : Theme.of(context).colorScheme.primary, ), title: Text(title, style: Styles.mainListTileTitle), subtitle: Text(subtitle, maxLines: 3), diff --git a/lib/src/view/settings/account_preferences_screen.dart b/lib/src/view/settings/account_preferences_screen.dart index 83c65bfa7a..bb77c2a493 100644 --- a/lib/src/view/settings/account_preferences_screen.dart +++ b/lib/src/view/settings/account_preferences_screen.dart @@ -116,7 +116,6 @@ class _AccountPreferencesScreenState extends ConsumerState ref .read(generalPreferencesProvider.notifier) - .setThemeMode(value ?? BackgroundThemeMode.system); + .setBackgroundThemeMode(value ?? BackgroundThemeMode.system); return SafeArea( child: ListView( diff --git a/lib/src/view/settings/board_theme_screen.dart b/lib/src/view/settings/board_theme_screen.dart index 6e3e128a1f..6654f3548d 100644 --- a/lib/src/view/settings/board_theme_screen.dart +++ b/lib/src/view/settings/board_theme_screen.dart @@ -2,9 +2,8 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; -import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; +import 'package:lichess_mobile/src/utils/color_palette.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; -import 'package:lichess_mobile/src/utils/system.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; @@ -30,18 +29,13 @@ class _Body extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final boardTheme = ref.watch(boardPreferencesProvider.select((p) => p.boardTheme)); - final hasSystemColors = ref.watch(generalPreferencesProvider.select((p) => p.systemColors)); + final hasSystemColors = getCorePalette() != null; - final androidVersion = ref.watch(androidVersionProvider).whenOrNull(data: (v) => v); - - final choices = - BoardTheme.values - .where( - (t) => - t != BoardTheme.system || - (hasSystemColors && androidVersion != null && androidVersion.sdkInt >= 31), - ) - .toList(); + final choices = BoardTheme.values + .where( + (t) => t != BoardTheme.system || hasSystemColors, + ) + .toList(); void onChanged(BoardTheme? value) => ref.read(boardPreferencesProvider.notifier).setBoardTheme(value ?? BoardTheme.brown); diff --git a/lib/src/view/settings/settings_tab_screen.dart b/lib/src/view/settings/settings_tab_screen.dart index f1dbdc0cfb..091fecab8b 100644 --- a/lib/src/view/settings/settings_tab_screen.dart +++ b/lib/src/view/settings/settings_tab_screen.dart @@ -207,11 +207,13 @@ class _Body extends ConsumerWidget { context, choices: BackgroundThemeMode.values, selectedItem: generalPrefs.themeMode, - labelBuilder: (t) => Text(AppBackgroundModeScreen.themeTitle(context, t)), - onSelectedItemChanged: - (BackgroundThemeMode? value) => ref - .read(generalPreferencesProvider.notifier) - .setThemeMode(value ?? BackgroundThemeMode.system), + labelBuilder: (t) => + Text(AppBackgroundModeScreen.themeTitle(context, t)), + onSelectedItemChanged: (BackgroundThemeMode? value) => ref + .read(generalPreferencesProvider.notifier) + .setBackgroundThemeMode( + value ?? BackgroundThemeMode.system, + ), ); } else { pushPlatformRoute( diff --git a/lib/src/view/settings/theme_screen.dart b/lib/src/view/settings/theme_screen.dart index 2e1307a8e0..90c8d500f4 100644 --- a/lib/src/view/settings/theme_screen.dart +++ b/lib/src/view/settings/theme_screen.dart @@ -3,18 +3,22 @@ import 'package:chessground/chessground.dart'; import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_colorpicker/flutter_colorpicker.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; +import 'package:lichess_mobile/src/styles/lichess_colors.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; +import 'package:lichess_mobile/src/utils/color_palette.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; -import 'package:lichess_mobile/src/utils/system.dart'; import 'package:lichess_mobile/src/view/settings/board_theme_screen.dart'; import 'package:lichess_mobile/src/view/settings/piece_set_screen.dart'; import 'package:lichess_mobile/src/widgets/adaptive_choice_picker.dart'; +import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; +import 'package:lichess_mobile/src/widgets/platform_alert_dialog.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:lichess_mobile/src/widgets/settings.dart'; @@ -41,7 +45,6 @@ class _Body extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final generalPrefs = ref.watch(generalPreferencesProvider); final boardPrefs = ref.watch(boardPreferencesProvider); - final androidVersionAsync = ref.watch(androidVersionProvider); const horizontalPadding = 16.0; @@ -50,7 +53,7 @@ class _Body extends ConsumerWidget { LayoutBuilder( builder: (context, constraints) { final double boardSize = math.min( - 400, + 250, constraints.biggest.shortestSide - horizontalPadding * 2, ); return Padding( @@ -82,22 +85,145 @@ class _Body extends ConsumerWidget { ListSection( hasLeading: true, children: [ - if (Theme.of(context).platform == TargetPlatform.android) - androidVersionAsync.maybeWhen( - data: - (version) => - version != null && version.sdkInt >= 31 - ? SwitchSettingTile( - leading: const Icon(Icons.colorize_outlined), - title: Text(context.l10n.mobileSystemColors), - value: generalPrefs.systemColors, - onChanged: (value) { - ref.read(generalPreferencesProvider.notifier).toggleSystemColors(); - }, - ) - : const SizedBox.shrink(), - orElse: () => const SizedBox.shrink(), + SwitchSettingTile( + leading: const Icon(Icons.colorize_outlined), + padding: Theme.of(context).platform == TargetPlatform.iOS + ? const EdgeInsets.symmetric(horizontal: 14, vertical: 8) + : null, + title: const Text('Custom theme'), + // TODO translate + subtitle: const Text( + 'Configure your own app theme using a seed color. Disable to use the chessboard theme.', + maxLines: 3, + ), + value: generalPrefs.customThemeEnabled, + onChanged: (value) { + ref + .read(generalPreferencesProvider.notifier) + .toggleCustomTheme(); + }, + ), + AnimatedCrossFade( + duration: const Duration(milliseconds: 300), + crossFadeState: generalPrefs.customThemeEnabled + ? CrossFadeState.showSecond + : CrossFadeState.showFirst, + firstChild: const SizedBox.shrink(), + secondChild: ListSection( + margin: EdgeInsets.zero, + cupertinoBorderRadius: BorderRadius.zero, + cupertinoClipBehavior: Clip.none, + children: [ + PlatformListTile( + leading: const Icon(Icons.color_lens), + title: const Text('Seed color'), + trailing: generalPrefs.customThemeSeed != null + ? Container( + width: 20, + height: 20, + decoration: BoxDecoration( + color: generalPrefs.customThemeSeed, + shape: BoxShape.circle, + ), + ) + : getCorePalette() != null + ? Text(context.l10n.mobileSystemColors) + : Container( + width: 20, + height: 20, + decoration: BoxDecoration( + color: LichessColors.primary[500], + shape: BoxShape.circle, + ), + ), + onTap: () { + showAdaptiveDialog( + context: context, + barrierDismissible: false, + builder: (context) { + final defaultColor = getCorePalettePrimary() ?? + LichessColors.primary[500]!; + bool useDefault = + generalPrefs.customThemeSeed == null; + Color color = + generalPrefs.customThemeSeed ?? defaultColor; + return StatefulBuilder( + builder: (context, setState) { + return PlatformAlertDialog( + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ColorPicker( + enableAlpha: false, + pickerColor: color, + onColorChanged: (c) { + setState(() { + useDefault = false; + color = c; + }); + }, + ), + SecondaryButton( + semanticsLabel: getCorePalette() != null + ? context.l10n.mobileSystemColors + : 'Default color', + onPressed: !useDefault + ? () { + setState(() { + useDefault = true; + color = defaultColor; + }); + } + : null, + child: Text( + getCorePalette() != null + ? context.l10n.mobileSystemColors + : 'Default color', + ), + ), + SecondaryButton( + semanticsLabel: context.l10n.cancel, + onPressed: () { + Navigator.of(context).pop(false); + }, + child: Text(context.l10n.cancel), + ), + SecondaryButton( + semanticsLabel: context.l10n.ok, + onPressed: () { + if (useDefault) { + Navigator.of(context).pop(null); + } else { + Navigator.of(context).pop(color); + } + }, + child: Text(context.l10n.ok), + ), + ], + ), + ), + ); + }, + ); + }, + ).then((color) { + if (color != false) { + ref + .read(generalPreferencesProvider.notifier) + .setCustomThemeSeed(color as Color?); + } + }); + }, + ), + ], ), + ), + ], + ), + ListSection( + hasLeading: true, + children: [ SettingsListTile( icon: const Icon(LichessIcons.chess_board), settingsLabel: Text(context.l10n.board), diff --git a/lib/src/widgets/settings.dart b/lib/src/widgets/settings.dart index b9daf76648..b553c49b03 100644 --- a/lib/src/widgets/settings.dart +++ b/lib/src/widgets/settings.dart @@ -71,6 +71,7 @@ class SwitchSettingTile extends StatelessWidget { required this.value, this.onChanged, this.leading, + this.padding, super.key, }); @@ -79,10 +80,12 @@ class SwitchSettingTile extends StatelessWidget { final bool value; final void Function(bool value)? onChanged; final Widget? leading; + final EdgeInsetsGeometry? padding; @override Widget build(BuildContext context) { return PlatformListTile( + padding: padding, leading: leading, title: _SettingsTitle(title: title), subtitle: subtitle, diff --git a/pubspec.lock b/pubspec.lock index 2319ddc62f..accc6a41dd 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -555,6 +555,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.4.1" + flutter_colorpicker: + dependency: "direct main" + description: + name: flutter_colorpicker + sha256: "969de5f6f9e2a570ac660fb7b501551451ea2a1ab9e2097e89475f60e07816ea" + url: "https://pub.dev" + source: hosted + version: "1.1.0" flutter_displaymode: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 72f8e3d380..bdcc0193d9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,6 +34,7 @@ dependencies: flutter: sdk: flutter flutter_appauth: ^8.0.0+1 + flutter_colorpicker: ^1.1.0 flutter_displaymode: ^0.6.0 flutter_layout_grid: ^2.0.1 flutter_linkify: ^6.0.0 From ed235150190893a3bc62bd49054182163dd979b6 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 16 Dec 2024 10:24:30 +0100 Subject: [PATCH 950/979] Reformat files --- lib/src/utils/color_palette.dart | 4 +- lib/src/utils/json.dart | 27 +++--- lib/src/view/puzzle/puzzle_tab_screen.dart | 5 +- .../settings/account_preferences_screen.dart | 5 +- lib/src/view/settings/board_theme_screen.dart | 7 +- .../view/settings/settings_tab_screen.dart | 12 ++- lib/src/view/settings/theme_screen.dart | 85 ++++++++++--------- 7 files changed, 63 insertions(+), 82 deletions(-) diff --git a/lib/src/utils/color_palette.dart b/lib/src/utils/color_palette.dart index 70c8d262b3..2419eb73b5 100644 --- a/lib/src/utils/color_palette.dart +++ b/lib/src/utils/color_palette.dart @@ -46,9 +46,7 @@ void setCorePalette(CorePalette? palette) { } Color? getCorePalettePrimary() { - return _corePalette?.primary != null - ? Color(_corePalette!.primary.get(50)) - : null; + return _corePalette?.primary != null ? Color(_corePalette!.primary.get(50)) : null; } /// Get the core palette if available (android 12+ only). diff --git a/lib/src/utils/json.dart b/lib/src/utils/json.dart index 4a19699502..cea4664d8c 100644 --- a/lib/src/utils/json.dart +++ b/lib/src/utils/json.dart @@ -23,10 +23,10 @@ class LocaleConverter implements JsonConverter?> { Map? toJson(Locale? locale) { return locale != null ? { - 'languageCode': locale.languageCode, - 'countryCode': locale.countryCode, - 'scriptCode': locale.scriptCode, - } + 'languageCode': locale.languageCode, + 'countryCode': locale.countryCode, + 'scriptCode': locale.scriptCode, + } : null; } } @@ -38,24 +38,17 @@ class ColorConverter implements JsonConverter?> { Color? fromJson(Map? json) { return json != null ? Color.from( - alpha: json['a'] as double, - red: json['r'] as double, - green: json['g'] as double, - blue: json['b'] as double, - ) + alpha: json['a'] as double, + red: json['r'] as double, + green: json['g'] as double, + blue: json['b'] as double, + ) : null; } @override Map? toJson(Color? color) { - return color != null - ? { - 'a': color.a, - 'r': color.r, - 'g': color.g, - 'b': color.b, - } - : null; + return color != null ? {'a': color.a, 'r': color.r, 'g': color.g, 'b': color.b} : null; } } diff --git a/lib/src/view/puzzle/puzzle_tab_screen.dart b/lib/src/view/puzzle/puzzle_tab_screen.dart index 189752b604..ed6b65764c 100644 --- a/lib/src/view/puzzle/puzzle_tab_screen.dart +++ b/lib/src/view/puzzle/puzzle_tab_screen.dart @@ -369,10 +369,7 @@ class _PuzzleMenuListTile extends StatelessWidget { Theme.of(context).platform == TargetPlatform.iOS ? const EdgeInsets.symmetric(vertical: 10.0, horizontal: 14.0) : null, - leading: Icon( - icon, - size: Styles.mainListTileIconSize, - ), + leading: Icon(icon, size: Styles.mainListTileIconSize), title: Text(title, style: Styles.mainListTileTitle), subtitle: Text(subtitle, maxLines: 3), trailing: diff --git a/lib/src/view/settings/account_preferences_screen.dart b/lib/src/view/settings/account_preferences_screen.dart index bb77c2a493..10031c8be6 100644 --- a/lib/src/view/settings/account_preferences_screen.dart +++ b/lib/src/view/settings/account_preferences_screen.dart @@ -113,10 +113,7 @@ class _AccountPreferencesScreenState extends ConsumerState t != BoardTheme.system || hasSystemColors, - ) - .toList(); + final choices = + BoardTheme.values.where((t) => t != BoardTheme.system || hasSystemColors).toList(); void onChanged(BoardTheme? value) => ref.read(boardPreferencesProvider.notifier).setBoardTheme(value ?? BoardTheme.brown); diff --git a/lib/src/view/settings/settings_tab_screen.dart b/lib/src/view/settings/settings_tab_screen.dart index 091fecab8b..c279dae0ca 100644 --- a/lib/src/view/settings/settings_tab_screen.dart +++ b/lib/src/view/settings/settings_tab_screen.dart @@ -207,13 +207,11 @@ class _Body extends ConsumerWidget { context, choices: BackgroundThemeMode.values, selectedItem: generalPrefs.themeMode, - labelBuilder: (t) => - Text(AppBackgroundModeScreen.themeTitle(context, t)), - onSelectedItemChanged: (BackgroundThemeMode? value) => ref - .read(generalPreferencesProvider.notifier) - .setBackgroundThemeMode( - value ?? BackgroundThemeMode.system, - ), + labelBuilder: (t) => Text(AppBackgroundModeScreen.themeTitle(context, t)), + onSelectedItemChanged: + (BackgroundThemeMode? value) => ref + .read(generalPreferencesProvider.notifier) + .setBackgroundThemeMode(value ?? BackgroundThemeMode.system), ); } else { pushPlatformRoute( diff --git a/lib/src/view/settings/theme_screen.dart b/lib/src/view/settings/theme_screen.dart index 90c8d500f4..03021beae7 100644 --- a/lib/src/view/settings/theme_screen.dart +++ b/lib/src/view/settings/theme_screen.dart @@ -87,9 +87,10 @@ class _Body extends ConsumerWidget { children: [ SwitchSettingTile( leading: const Icon(Icons.colorize_outlined), - padding: Theme.of(context).platform == TargetPlatform.iOS - ? const EdgeInsets.symmetric(horizontal: 14, vertical: 8) - : null, + padding: + Theme.of(context).platform == TargetPlatform.iOS + ? const EdgeInsets.symmetric(horizontal: 14, vertical: 8) + : null, title: const Text('Custom theme'), // TODO translate subtitle: const Text( @@ -98,16 +99,15 @@ class _Body extends ConsumerWidget { ), value: generalPrefs.customThemeEnabled, onChanged: (value) { - ref - .read(generalPreferencesProvider.notifier) - .toggleCustomTheme(); + ref.read(generalPreferencesProvider.notifier).toggleCustomTheme(); }, ), AnimatedCrossFade( duration: const Duration(milliseconds: 300), - crossFadeState: generalPrefs.customThemeEnabled - ? CrossFadeState.showSecond - : CrossFadeState.showFirst, + crossFadeState: + generalPrefs.customThemeEnabled + ? CrossFadeState.showSecond + : CrossFadeState.showFirst, firstChild: const SizedBox.shrink(), secondChild: ListSection( margin: EdgeInsets.zero, @@ -117,36 +117,35 @@ class _Body extends ConsumerWidget { PlatformListTile( leading: const Icon(Icons.color_lens), title: const Text('Seed color'), - trailing: generalPrefs.customThemeSeed != null - ? Container( - width: 20, - height: 20, - decoration: BoxDecoration( - color: generalPrefs.customThemeSeed, - shape: BoxShape.circle, - ), - ) - : getCorePalette() != null + trailing: + generalPrefs.customThemeSeed != null + ? Container( + width: 20, + height: 20, + decoration: BoxDecoration( + color: generalPrefs.customThemeSeed, + shape: BoxShape.circle, + ), + ) + : getCorePalette() != null ? Text(context.l10n.mobileSystemColors) : Container( - width: 20, - height: 20, - decoration: BoxDecoration( - color: LichessColors.primary[500], - shape: BoxShape.circle, - ), + width: 20, + height: 20, + decoration: BoxDecoration( + color: LichessColors.primary[500], + shape: BoxShape.circle, ), + ), onTap: () { showAdaptiveDialog( context: context, barrierDismissible: false, builder: (context) { - final defaultColor = getCorePalettePrimary() ?? - LichessColors.primary[500]!; - bool useDefault = - generalPrefs.customThemeSeed == null; - Color color = - generalPrefs.customThemeSeed ?? defaultColor; + final defaultColor = + getCorePalettePrimary() ?? LichessColors.primary[500]!; + bool useDefault = generalPrefs.customThemeSeed == null; + Color color = generalPrefs.customThemeSeed ?? defaultColor; return StatefulBuilder( builder: (context, setState) { return PlatformAlertDialog( @@ -165,17 +164,19 @@ class _Body extends ConsumerWidget { }, ), SecondaryButton( - semanticsLabel: getCorePalette() != null - ? context.l10n.mobileSystemColors - : 'Default color', - onPressed: !useDefault - ? () { - setState(() { - useDefault = true; - color = defaultColor; - }); - } - : null, + semanticsLabel: + getCorePalette() != null + ? context.l10n.mobileSystemColors + : 'Default color', + onPressed: + !useDefault + ? () { + setState(() { + useDefault = true; + color = defaultColor; + }); + } + : null, child: Text( getCorePalette() != null ? context.l10n.mobileSystemColors From 856dd001f9678959cc54e78dbd9f8f7f7cf02357 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 16 Dec 2024 15:22:30 +0100 Subject: [PATCH 951/979] Board adjustments --- lib/src/model/settings/board_preferences.dart | 19 ++ lib/src/view/settings/theme_screen.dart | 232 ++++++++++++------ lib/src/widgets/board_carousel_item.dart | 4 +- lib/src/widgets/board_preview.dart | 4 +- lib/src/widgets/board_thumbnail.dart | 8 +- pubspec.lock | 9 +- pubspec.yaml | 3 +- 7 files changed, 183 insertions(+), 96 deletions(-) diff --git a/lib/src/model/settings/board_preferences.dart b/lib/src/model/settings/board_preferences.dart index 65dd2b3f1b..84eefa4241 100644 --- a/lib/src/model/settings/board_preferences.dart +++ b/lib/src/model/settings/board_preferences.dart @@ -94,12 +94,23 @@ class BoardPreferences extends _$BoardPreferences with PreferencesStorage setShapeColor(ShapeColor shapeColor) { return save(state.copyWith(shapeColor: shapeColor)); } + + Future setBrightness(double brightness) { + return save(state.copyWith(brightness: brightness)); + } + + Future setHue(double hue) { + return save(state.copyWith(hue: hue)); + } } @Freezed(fromJson: true, toJson: true) class BoardPrefs with _$BoardPrefs implements Serializable { const BoardPrefs._(); + @Assert( + 'brightness == null || brightness >= -0.5 && brightness <= 0.5, hue == null || hue >= -1 && hue <= 1', + ) const factory BoardPrefs({ required PieceSet pieceSet, required BoardTheme boardTheme, @@ -126,6 +137,8 @@ class BoardPrefs with _$BoardPrefs implements Serializable { @JsonKey(defaultValue: ShapeColor.green, unknownEnumValue: ShapeColor.green) required ShapeColor shapeColor, @JsonKey(defaultValue: false) required bool showBorder, + @JsonKey(defaultValue: 0.0) required double brightness, + @JsonKey(defaultValue: 0.0) required double hue, }) = _BoardPrefs; static const defaults = BoardPrefs( @@ -145,12 +158,18 @@ class BoardPrefs with _$BoardPrefs implements Serializable { dragTargetKind: DragTargetKind.circle, shapeColor: ShapeColor.green, showBorder: false, + brightness: 0.0, + hue: 0.0, ); + bool get hasColorAdjustments => brightness != 0.0 || hue != 0.0; + ChessboardSettings toBoardSettings() { return ChessboardSettings( pieceAssets: pieceSet.assets, colorScheme: boardTheme.colors, + brightness: brightness, + hue: hue, border: showBorder ? BoardBorder(color: darken(boardTheme.colors.darkSquare, 0.2), width: 16.0) diff --git a/lib/src/view/settings/theme_screen.dart b/lib/src/view/settings/theme_screen.dart index 03021beae7..eb601d2a55 100644 --- a/lib/src/view/settings/theme_screen.dart +++ b/lib/src/view/settings/theme_screen.dart @@ -40,9 +40,25 @@ switch (shapeColor) { ShapeColor.yellow => 'Yellow', }; -class _Body extends ConsumerWidget { +class _Body extends ConsumerStatefulWidget { @override - Widget build(BuildContext context, WidgetRef ref) { + ConsumerState<_Body> createState() => _BodyState(); +} + +class _BodyState extends ConsumerState<_Body> { + late double brightness; + late double hue; + + @override + void initState() { + super.initState(); + final boardPrefs = ref.read(boardPreferencesProvider); + brightness = boardPrefs.brightness; + hue = boardPrefs.hue; + } + + @override + Widget build(BuildContext context) { final generalPrefs = ref.watch(generalPreferencesProvider); final boardPrefs = ref.watch(boardPreferencesProvider); @@ -74,6 +90,8 @@ class _Body extends ConsumerWidget { ), }.lock, settings: boardPrefs.toBoardSettings().copyWith( + brightness: brightness, + hue: hue, borderRadius: const BorderRadius.all(Radius.circular(4.0)), boxShadow: boardShadows, ), @@ -82,6 +100,136 @@ class _Body extends ConsumerWidget { ); }, ), + ListSection( + hasLeading: true, + children: [ + SettingsListTile( + icon: const Icon(LichessIcons.chess_board), + settingsLabel: Text(context.l10n.board), + settingsValue: boardPrefs.boardTheme.label, + onTap: () { + pushPlatformRoute( + context, + title: context.l10n.board, + builder: (context) => const BoardThemeScreen(), + ); + }, + ), + PlatformListTile( + leading: const Icon(Icons.brightness_6), + title: Slider.adaptive( + min: -0.5, + max: 0.5, + value: brightness, + onChanged: (value) { + setState(() { + brightness = value; + }); + }, + onChangeEnd: (value) { + ref.read(boardPreferencesProvider.notifier).setBrightness(brightness); + }, + ), + ), + PlatformListTile( + leading: const Icon(Icons.invert_colors), + title: Slider.adaptive( + min: -1.0, + max: 1.0, + value: hue, + onChanged: (value) { + setState(() { + hue = value; + }); + }, + onChangeEnd: (value) { + ref.read(boardPreferencesProvider.notifier).setHue(hue); + }, + ), + ), + AnimatedCrossFade( + duration: const Duration(milliseconds: 300), + crossFadeState: + brightness != 0.0 || hue != 0.0 + ? CrossFadeState.showSecond + : CrossFadeState.showFirst, + firstChild: const SizedBox.shrink(), + secondChild: PlatformListTile( + leading: const Icon(Icons.cancel), + title: Text(context.l10n.boardReset), + onTap: () { + setState(() { + brightness = 0.0; + hue = 0.0; + }); + ref.read(boardPreferencesProvider.notifier).setBrightness(0.0); + ref.read(boardPreferencesProvider.notifier).setHue(0.0); + }, + ), + ), + ], + ), + ListSection( + hasLeading: true, + children: [ + SettingsListTile( + icon: const Icon(LichessIcons.chess_pawn), + settingsLabel: Text(context.l10n.pieceSet), + settingsValue: boardPrefs.pieceSet.label, + onTap: () { + pushPlatformRoute( + context, + title: context.l10n.pieceSet, + builder: (context) => const PieceSetScreen(), + ); + }, + ), + SettingsListTile( + icon: const Icon(LichessIcons.arrow_full_upperright), + settingsLabel: const Text('Shape color'), + settingsValue: shapeColorL10n(context, boardPrefs.shapeColor), + onTap: () { + showChoicePicker( + context, + choices: ShapeColor.values, + selectedItem: boardPrefs.shapeColor, + labelBuilder: + (t) => Text.rich( + TextSpan( + children: [ + TextSpan(text: shapeColorL10n(context, t)), + const TextSpan(text: ' '), + WidgetSpan(child: Container(width: 15, height: 15, color: t.color)), + ], + ), + ), + onSelectedItemChanged: (ShapeColor? value) { + ref + .read(boardPreferencesProvider.notifier) + .setShapeColor(value ?? ShapeColor.green); + }, + ); + }, + ), + SwitchSettingTile( + leading: const Icon(Icons.location_on), + title: Text(context.l10n.preferencesBoardCoordinates), + value: boardPrefs.coordinates, + onChanged: (value) { + ref.read(boardPreferencesProvider.notifier).toggleCoordinates(); + }, + ), + SwitchSettingTile( + // TODO translate + leading: const Icon(Icons.border_outer), + title: const Text('Show border'), + value: boardPrefs.showBorder, + onChanged: (value) { + ref.read(boardPreferencesProvider.notifier).toggleBorder(); + }, + ), + ], + ), ListSection( hasLeading: true, children: [ @@ -91,12 +239,9 @@ class _Body extends ConsumerWidget { Theme.of(context).platform == TargetPlatform.iOS ? const EdgeInsets.symmetric(horizontal: 14, vertical: 8) : null, - title: const Text('Custom theme'), + title: const Text('App theme'), // TODO translate - subtitle: const Text( - 'Configure your own app theme using a seed color. Disable to use the chessboard theme.', - maxLines: 3, - ), + subtitle: const Text('Configure your own app theme using a seed color.', maxLines: 3), value: generalPrefs.customThemeEnabled, onChanged: (value) { ref.read(generalPreferencesProvider.notifier).toggleCustomTheme(); @@ -222,79 +367,6 @@ class _Body extends ConsumerWidget { ), ], ), - ListSection( - hasLeading: true, - children: [ - SettingsListTile( - icon: const Icon(LichessIcons.chess_board), - settingsLabel: Text(context.l10n.board), - settingsValue: boardPrefs.boardTheme.label, - onTap: () { - pushPlatformRoute( - context, - title: context.l10n.board, - builder: (context) => const BoardThemeScreen(), - ); - }, - ), - SettingsListTile( - icon: const Icon(LichessIcons.chess_pawn), - settingsLabel: Text(context.l10n.pieceSet), - settingsValue: boardPrefs.pieceSet.label, - onTap: () { - pushPlatformRoute( - context, - title: context.l10n.pieceSet, - builder: (context) => const PieceSetScreen(), - ); - }, - ), - SettingsListTile( - icon: const Icon(LichessIcons.arrow_full_upperright), - settingsLabel: const Text('Shape color'), - settingsValue: shapeColorL10n(context, boardPrefs.shapeColor), - onTap: () { - showChoicePicker( - context, - choices: ShapeColor.values, - selectedItem: boardPrefs.shapeColor, - labelBuilder: - (t) => Text.rich( - TextSpan( - children: [ - TextSpan(text: shapeColorL10n(context, t)), - const TextSpan(text: ' '), - WidgetSpan(child: Container(width: 15, height: 15, color: t.color)), - ], - ), - ), - onSelectedItemChanged: (ShapeColor? value) { - ref - .read(boardPreferencesProvider.notifier) - .setShapeColor(value ?? ShapeColor.green); - }, - ); - }, - ), - SwitchSettingTile( - leading: const Icon(Icons.location_on), - title: Text(context.l10n.preferencesBoardCoordinates), - value: boardPrefs.coordinates, - onChanged: (value) { - ref.read(boardPreferencesProvider.notifier).toggleCoordinates(); - }, - ), - SwitchSettingTile( - // TODO translate - leading: const Icon(Icons.border_outer), - title: const Text('Show border'), - value: boardPrefs.showBorder, - onChanged: (value) { - ref.read(boardPreferencesProvider.notifier).toggleBorder(); - }, - ), - ], - ), ], ); } diff --git a/lib/src/widgets/board_carousel_item.dart b/lib/src/widgets/board_carousel_item.dart index a6e12f65c1..90d50c71ba 100644 --- a/lib/src/widgets/board_carousel_item.dart +++ b/lib/src/widgets/board_carousel_item.dart @@ -84,14 +84,12 @@ class BoardCarouselItem extends ConsumerWidget { fen: fen, orientation: orientation, lastMove: lastMove, - settings: ChessboardSettings( + settings: boardPrefs.toBoardSettings().copyWith( enableCoordinates: false, borderRadius: const BorderRadius.only( topLeft: Radius.circular(10.0), topRight: Radius.circular(10.0), ), - pieceAssets: boardPrefs.pieceSet.assets, - colorScheme: boardPrefs.boardTheme.colors, ), ), ), diff --git a/lib/src/widgets/board_preview.dart b/lib/src/widgets/board_preview.dart index 531bc0a6d9..91846aca68 100644 --- a/lib/src/widgets/board_preview.dart +++ b/lib/src/widgets/board_preview.dart @@ -88,13 +88,11 @@ class _SmallBoardPreviewState extends ConsumerState { fen: widget.fen, orientation: widget.orientation, lastMove: widget.lastMove as NormalMove?, - settings: ChessboardSettings( + settings: boardPrefs.toBoardSettings().copyWith( enableCoordinates: false, borderRadius: const BorderRadius.all(Radius.circular(4.0)), boxShadow: boardShadows, animationDuration: const Duration(milliseconds: 150), - pieceAssets: boardPrefs.pieceSet.assets, - colorScheme: boardPrefs.boardTheme.colors, ), ), const SizedBox(width: 10.0), diff --git a/lib/src/widgets/board_thumbnail.dart b/lib/src/widgets/board_thumbnail.dart index 0607cad0dc..3f23c5b844 100644 --- a/lib/src/widgets/board_thumbnail.dart +++ b/lib/src/widgets/board_thumbnail.dart @@ -76,13 +76,11 @@ class _BoardThumbnailState extends ConsumerState { fen: widget.fen, orientation: widget.orientation, lastMove: widget.lastMove as NormalMove?, - settings: ChessboardSettings( + settings: boardPrefs.toBoardSettings().copyWith( enableCoordinates: false, borderRadius: const BorderRadius.all(Radius.circular(4.0)), boxShadow: boardShadows, - animationDuration: widget.animationDuration!, - pieceAssets: boardPrefs.pieceSet.assets, - colorScheme: boardPrefs.boardTheme.colors, + animationDuration: widget.animationDuration, ), ) : StaticChessboard( @@ -95,6 +93,8 @@ class _BoardThumbnailState extends ConsumerState { boxShadow: boardShadows, pieceAssets: boardPrefs.pieceSet.assets, colorScheme: boardPrefs.boardTheme.colors, + brightness: boardPrefs.brightness, + hue: boardPrefs.hue, ); final maybeTappableBoard = diff --git a/pubspec.lock b/pubspec.lock index accc6a41dd..45fb76ab74 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -201,11 +201,10 @@ packages: chessground: dependency: "direct main" description: - name: chessground - sha256: "118e11871baa08022be827087bc90b82f0bda535d504278787f9717ad949132b" - url: "https://pub.dev" - source: hosted - version: "6.1.0" + path: "../flutter-chessground" + relative: true + source: path + version: "6.2.0" ci: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index bdcc0193d9..8600a41706 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,7 +14,8 @@ dependencies: async: ^2.10.0 auto_size_text: ^3.0.0 cached_network_image: ^3.2.2 - chessground: ^6.1.0 + chessground: + path: ../flutter-chessground clock: ^1.1.1 collection: ^1.17.0 connectivity_plus: ^6.0.2 From 163604a80133d5c68eecb5e2798a7916e75bb290 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 16 Dec 2024 15:46:27 +0100 Subject: [PATCH 952/979] Fixes --- lib/src/model/settings/board_preferences.dart | 8 +- .../view/settings/settings_tab_screen.dart | 11 +- lib/src/view/settings/theme_screen.dart | 76 +++++++------ lib/src/widgets/board_carousel_item.dart | 101 ++++++++++-------- lib/src/widgets/change_colors.dart | 81 ++++++++++++++ 5 files changed, 185 insertions(+), 92 deletions(-) create mode 100644 lib/src/widgets/change_colors.dart diff --git a/lib/src/model/settings/board_preferences.dart b/lib/src/model/settings/board_preferences.dart index 84eefa4241..402e659789 100644 --- a/lib/src/model/settings/board_preferences.dart +++ b/lib/src/model/settings/board_preferences.dart @@ -95,12 +95,8 @@ class BoardPreferences extends _$BoardPreferences with PreferencesStorage setBrightness(double brightness) { - return save(state.copyWith(brightness: brightness)); - } - - Future setHue(double hue) { - return save(state.copyWith(hue: hue)); + Future adjustColors({double? brightness, double? hue}) { + return save(state.copyWith(brightness: brightness ?? state.brightness, hue: hue ?? state.hue)); } } diff --git a/lib/src/view/settings/settings_tab_screen.dart b/lib/src/view/settings/settings_tab_screen.dart index c279dae0ca..687952a257 100644 --- a/lib/src/view/settings/settings_tab_screen.dart +++ b/lib/src/view/settings/settings_tab_screen.dart @@ -222,10 +222,13 @@ class _Body extends ConsumerWidget { } }, ), - SettingsListTile( - icon: const Icon(Icons.palette_outlined), - settingsLabel: Text(context.l10n.mobileTheme), - settingsValue: '${boardPrefs.boardTheme.label} / ${boardPrefs.pieceSet.label}', + PlatformListTile( + leading: const Icon(Icons.palette_outlined), + title: Text(context.l10n.mobileTheme), + trailing: + Theme.of(context).platform == TargetPlatform.iOS + ? const CupertinoListTileChevron() + : null, onTap: () { pushPlatformRoute(context, title: 'Theme', builder: (context) => const ThemeScreen()); }, diff --git a/lib/src/view/settings/theme_screen.dart b/lib/src/view/settings/theme_screen.dart index eb601d2a55..e001bdcc25 100644 --- a/lib/src/view/settings/theme_screen.dart +++ b/lib/src/view/settings/theme_screen.dart @@ -17,6 +17,7 @@ import 'package:lichess_mobile/src/view/settings/board_theme_screen.dart'; import 'package:lichess_mobile/src/view/settings/piece_set_screen.dart'; import 'package:lichess_mobile/src/widgets/adaptive_choice_picker.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/change_colors.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform_alert_dialog.dart'; import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; @@ -75,25 +76,29 @@ class _BodyState extends ConsumerState<_Body> { return Padding( padding: const EdgeInsets.symmetric(horizontal: horizontalPadding, vertical: 16), child: Center( - child: Chessboard.fixed( - size: boardSize, - orientation: Side.white, - lastMove: const NormalMove(from: Square.e2, to: Square.e4), - fen: 'rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1', - shapes: - { - Circle(color: boardPrefs.shapeColor.color, orig: Square.fromName('b8')), - Arrow( - color: boardPrefs.shapeColor.color, - orig: Square.fromName('b8'), - dest: Square.fromName('c6'), - ), - }.lock, - settings: boardPrefs.toBoardSettings().copyWith( - brightness: brightness, - hue: hue, - borderRadius: const BorderRadius.all(Radius.circular(4.0)), - boxShadow: boardShadows, + child: ChangeColors( + brightness: brightness, + hue: hue, + child: Chessboard.fixed( + size: boardSize, + orientation: Side.white, + lastMove: const NormalMove(from: Square.e2, to: Square.e4), + fen: 'rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1', + shapes: + { + Circle(color: boardPrefs.shapeColor.color, orig: Square.fromName('b8')), + Arrow( + color: boardPrefs.shapeColor.color, + orig: Square.fromName('b8'), + dest: Square.fromName('c6'), + ), + }.lock, + settings: boardPrefs.toBoardSettings().copyWith( + brightness: 0.0, + hue: 0.0, + borderRadius: const BorderRadius.all(Radius.circular(4.0)), + boxShadow: boardShadows, + ), ), ), ), @@ -103,18 +108,6 @@ class _BodyState extends ConsumerState<_Body> { ListSection( hasLeading: true, children: [ - SettingsListTile( - icon: const Icon(LichessIcons.chess_board), - settingsLabel: Text(context.l10n.board), - settingsValue: boardPrefs.boardTheme.label, - onTap: () { - pushPlatformRoute( - context, - title: context.l10n.board, - builder: (context) => const BoardThemeScreen(), - ); - }, - ), PlatformListTile( leading: const Icon(Icons.brightness_6), title: Slider.adaptive( @@ -127,7 +120,7 @@ class _BodyState extends ConsumerState<_Body> { }); }, onChangeEnd: (value) { - ref.read(boardPreferencesProvider.notifier).setBrightness(brightness); + ref.read(boardPreferencesProvider.notifier).adjustColors(brightness: brightness); }, ), ), @@ -143,7 +136,7 @@ class _BodyState extends ConsumerState<_Body> { }); }, onChangeEnd: (value) { - ref.read(boardPreferencesProvider.notifier).setHue(hue); + ref.read(boardPreferencesProvider.notifier).adjustColors(hue: hue); }, ), ), @@ -162,8 +155,9 @@ class _BodyState extends ConsumerState<_Body> { brightness = 0.0; hue = 0.0; }); - ref.read(boardPreferencesProvider.notifier).setBrightness(0.0); - ref.read(boardPreferencesProvider.notifier).setHue(0.0); + ref + .read(boardPreferencesProvider.notifier) + .adjustColors(brightness: 0.0, hue: 0.0); }, ), ), @@ -172,6 +166,18 @@ class _BodyState extends ConsumerState<_Body> { ListSection( hasLeading: true, children: [ + SettingsListTile( + icon: const Icon(LichessIcons.chess_board), + settingsLabel: Text(context.l10n.board), + settingsValue: boardPrefs.boardTheme.label, + onTap: () { + pushPlatformRoute( + context, + title: context.l10n.board, + builder: (context) => const BoardThemeScreen(), + ); + }, + ), SettingsListTile( icon: const Icon(LichessIcons.chess_pawn), settingsLabel: Text(context.l10n.pieceSet), diff --git a/lib/src/widgets/board_carousel_item.dart b/lib/src/widgets/board_carousel_item.dart index 90d50c71ba..57fecd5e64 100644 --- a/lib/src/widgets/board_carousel_item.dart +++ b/lib/src/widgets/board_carousel_item.dart @@ -7,6 +7,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; +import 'package:lichess_mobile/src/widgets/change_colors.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; const _kBoardCarouselItemMargin = EdgeInsets.symmetric(vertical: 8.0, horizontal: 6.0); @@ -51,58 +52,64 @@ class BoardCarouselItem extends ConsumerWidget { return LayoutBuilder( builder: (context, constraints) { final boardSize = constraints.biggest.shortestSide - _kBoardCarouselItemMargin.horizontal; - final card = PlatformCard( - color: backgroundColor, - margin: - Theme.of(context).platform == TargetPlatform.iOS - ? EdgeInsets.zero - : _kBoardCarouselItemMargin, - child: AdaptiveInkWell( - splashColor: splashColor, - borderRadius: BorderRadius.circular(10), - onTap: onTap, - child: Stack( - children: [ - ShaderMask( - blendMode: BlendMode.dstOut, - shaderCallback: (bounds) { - return LinearGradient( - begin: Alignment.center, - end: Alignment.bottomCenter, - colors: [ - backgroundColor.withValues(alpha: 0.25), - backgroundColor.withValues(alpha: 1.0), - ], - stops: const [0.3, 1.00], - tileMode: TileMode.clamp, - ).createShader(bounds); - }, - child: SizedBox( - height: boardSize, - child: Chessboard.fixed( - size: boardSize, - fen: fen, - orientation: orientation, - lastMove: lastMove, - settings: boardPrefs.toBoardSettings().copyWith( - enableCoordinates: false, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(10.0), - topRight: Radius.circular(10.0), + final card = ChangeColors( + brightness: boardPrefs.brightness, + hue: boardPrefs.hue, + child: PlatformCard( + color: backgroundColor, + margin: + Theme.of(context).platform == TargetPlatform.iOS + ? EdgeInsets.zero + : _kBoardCarouselItemMargin, + child: AdaptiveInkWell( + splashColor: splashColor, + borderRadius: BorderRadius.circular(10), + onTap: onTap, + child: Stack( + children: [ + ShaderMask( + blendMode: BlendMode.dstOut, + shaderCallback: (bounds) { + return LinearGradient( + begin: Alignment.center, + end: Alignment.bottomCenter, + colors: [ + backgroundColor.withValues(alpha: 0.25), + backgroundColor.withValues(alpha: 1.0), + ], + stops: const [0.3, 1.00], + tileMode: TileMode.clamp, + ).createShader(bounds); + }, + child: SizedBox( + height: boardSize, + child: Chessboard.fixed( + size: boardSize, + fen: fen, + orientation: orientation, + lastMove: lastMove, + settings: boardPrefs.toBoardSettings().copyWith( + brightness: 0.0, + hue: 0.0, + enableCoordinates: false, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(10.0), + topRight: Radius.circular(10.0), + ), ), ), ), ), - ), - Positioned( - left: 0, - bottom: 8, - child: DefaultTextStyle.merge( - style: const TextStyle(color: Colors.white), - child: description, + Positioned( + left: 0, + bottom: 8, + child: DefaultTextStyle.merge( + style: const TextStyle(color: Colors.white), + child: description, + ), ), - ), - ], + ], + ), ), ), ); diff --git a/lib/src/widgets/change_colors.dart b/lib/src/widgets/change_colors.dart new file mode 100644 index 0000000000..38ce7eebe3 --- /dev/null +++ b/lib/src/widgets/change_colors.dart @@ -0,0 +1,81 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; + +// Based upon: https://stackoverflow.com/questions/64639589/how-to-adjust-hue-saturation-and-brightness-of-an-image-in-flutter +// from BananaNeil: https://stackoverflow.com/users/937841/banananeil. +// This is, in turn, based upon: https://stackoverflow.com/a/7917978/937841 + +/// Use the [ChangeColors] widget to change the brightness and hue of any widget, including images. +/// +/// Example: +/// +/// ``` +/// ChangeColors( +/// hue: 0.55, +/// brightness: 0.2, +/// child: Image.asset('myImage.png'), +/// ); +/// ``` +/// +/// To achieve a greyscale effect, you may also use the +/// [ChangeColors.greyscale] constructor. +/// +class ChangeColors extends StatelessWidget { + const ChangeColors({this.brightness = 0.0, this.hue = 0.0, required this.child, super.key}); + + /// Negative value will make it darker (-1 is darkest). + /// Positive value will make it lighter (1 is the maximum, but you can go above it). + /// Note: 0.0 is unchanged. + final double brightness; + + /// From -1.0 to 1.0 (Note: 1.0 wraps into -1.0, such as 1.2 is the same as -0.8). + /// Note: 0.0 is unchanged. Adding or subtracting multiples of 2.0 also keeps it unchanged. + final double hue; + + final Widget child; + + @override + Widget build(BuildContext context) { + return ColorFiltered( + colorFilter: ColorFilter.matrix(_adjustMatrix(hue: hue * pi, brightness: brightness)), + child: child, + ); + } +} + +List _adjustMatrix({required double hue, required double brightness}) { + if (hue == 0 && brightness == 0) { + // dart format off + return [ + 1, 0, 0, 0, 0, + 0, 1, 0, 0, 0, + 0, 0, 1, 0, 0, + 0, 0, 0, 1, 0, + ]; + // dart format on + } + final brightnessValue = brightness <= 0 ? brightness * 255 : brightness * 100; + return List.from([ + 0.213 + cos(hue) * 0.787 + sin(hue) * -0.213, + 0.715 + cos(hue) * -0.715 + sin(hue) * -0.715, + 0.072 + cos(hue) * -0.072 + sin(hue) * 0.928, + 0, + brightnessValue, + 0.213 + cos(hue) * -0.213 + sin(hue) * 0.143, + 0.715 + cos(hue) * 0.285 + sin(hue) * 0.140, + 0.072 + cos(hue) * -0.072 + sin(hue) * -0.283, + 0, + brightnessValue, + 0.213 + cos(hue) * -0.213 + sin(hue) * -0.787, + 0.715 + cos(hue) * -0.715 + sin(hue) * 0.715, + 0.072 + cos(hue) * 0.928 + sin(hue) * 0.072, + 0, + brightnessValue, + 0, + 0, + 0, + 1, + 0, + ]).map((i) => i).toList(); +} From 1bfe9ab947539cd389f9f5ef6b60b58a19e99403 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Mon, 16 Dec 2024 18:57:27 +0100 Subject: [PATCH 953/979] More fixes --- .../model/settings/general_preferences.dart | 10 +---- lib/src/view/settings/theme_screen.dart | 41 ++++++++++--------- lib/src/widgets/board_carousel_item.dart | 8 ++-- lib/src/widgets/board_preview.dart | 6 ++- lib/src/widgets/board_thumbnail.dart | 10 +++-- 5 files changed, 39 insertions(+), 36 deletions(-) diff --git a/lib/src/model/settings/general_preferences.dart b/lib/src/model/settings/general_preferences.dart index b61dfc3b77..f788e60dd5 100644 --- a/lib/src/model/settings/general_preferences.dart +++ b/lib/src/model/settings/general_preferences.dart @@ -49,14 +49,6 @@ class GeneralPreferences extends _$GeneralPreferences with PreferencesStorage toggleCustomTheme() async { await save(state.copyWith(customThemeEnabled: !state.customThemeEnabled)); - if (state.customThemeEnabled == false) { - final boardTheme = ref.read(boardPreferencesProvider).boardTheme; - if (boardTheme == BoardTheme.system) { - await ref.read(boardPreferencesProvider.notifier).setBoardTheme(BoardTheme.brown); - } - } else { - await ref.read(boardPreferencesProvider.notifier).setBoardTheme(BoardTheme.system); - } } Future setCustomThemeSeed(Color? color) { @@ -88,7 +80,7 @@ class GeneralPrefs with _$GeneralPrefs implements Serializable { isSoundEnabled: true, soundTheme: SoundTheme.standard, masterVolume: 0.8, - customThemeEnabled: true, + customThemeEnabled: false, ); factory GeneralPrefs.fromJson(Map json) { diff --git a/lib/src/view/settings/theme_screen.dart b/lib/src/view/settings/theme_screen.dart index e001bdcc25..10d58c8748 100644 --- a/lib/src/view/settings/theme_screen.dart +++ b/lib/src/view/settings/theme_screen.dart @@ -65,6 +65,8 @@ class _BodyState extends ConsumerState<_Body> { const horizontalPadding = 16.0; + final bool hasAjustedColors = brightness != 0.0 || hue != 0.0; + return ListView( children: [ LayoutBuilder( @@ -140,26 +142,27 @@ class _BodyState extends ConsumerState<_Body> { }, ), ), - AnimatedCrossFade( - duration: const Duration(milliseconds: 300), - crossFadeState: - brightness != 0.0 || hue != 0.0 - ? CrossFadeState.showSecond - : CrossFadeState.showFirst, - firstChild: const SizedBox.shrink(), - secondChild: PlatformListTile( - leading: const Icon(Icons.cancel), - title: Text(context.l10n.boardReset), - onTap: () { - setState(() { - brightness = 0.0; - hue = 0.0; - }); - ref - .read(boardPreferencesProvider.notifier) - .adjustColors(brightness: 0.0, hue: 0.0); - }, + PlatformListTile( + leading: Opacity( + opacity: hasAjustedColors ? 1.0 : 0.5, + child: const Icon(Icons.cancel), + ), + title: Opacity( + opacity: hasAjustedColors ? 1.0 : 0.5, + child: Text(context.l10n.boardReset), ), + onTap: + hasAjustedColors + ? () { + setState(() { + brightness = 0.0; + hue = 0.0; + }); + ref + .read(boardPreferencesProvider.notifier) + .adjustColors(brightness: 0.0, hue: 0.0); + } + : null, ), ], ), diff --git a/lib/src/widgets/board_carousel_item.dart b/lib/src/widgets/board_carousel_item.dart index 57fecd5e64..2559c8398f 100644 --- a/lib/src/widgets/board_carousel_item.dart +++ b/lib/src/widgets/board_carousel_item.dart @@ -53,8 +53,8 @@ class BoardCarouselItem extends ConsumerWidget { builder: (context, constraints) { final boardSize = constraints.biggest.shortestSide - _kBoardCarouselItemMargin.horizontal; final card = ChangeColors( - brightness: boardPrefs.brightness, hue: boardPrefs.hue, + brightness: boardPrefs.brightness, child: PlatformCard( color: backgroundColor, margin: @@ -88,14 +88,14 @@ class BoardCarouselItem extends ConsumerWidget { fen: fen, orientation: orientation, lastMove: lastMove, - settings: boardPrefs.toBoardSettings().copyWith( - brightness: 0.0, - hue: 0.0, + settings: ChessboardSettings( enableCoordinates: false, borderRadius: const BorderRadius.only( topLeft: Radius.circular(10.0), topRight: Radius.circular(10.0), ), + pieceAssets: boardPrefs.pieceSet.assets, + colorScheme: boardPrefs.boardTheme.colors, ), ), ), diff --git a/lib/src/widgets/board_preview.dart b/lib/src/widgets/board_preview.dart index 91846aca68..0f74802a2a 100644 --- a/lib/src/widgets/board_preview.dart +++ b/lib/src/widgets/board_preview.dart @@ -88,7 +88,11 @@ class _SmallBoardPreviewState extends ConsumerState { fen: widget.fen, orientation: widget.orientation, lastMove: widget.lastMove as NormalMove?, - settings: boardPrefs.toBoardSettings().copyWith( + settings: ChessboardSettings( + pieceAssets: boardPrefs.pieceSet.assets, + colorScheme: boardPrefs.boardTheme.colors, + brightness: boardPrefs.brightness, + hue: boardPrefs.hue, enableCoordinates: false, borderRadius: const BorderRadius.all(Radius.circular(4.0)), boxShadow: boardShadows, diff --git a/lib/src/widgets/board_thumbnail.dart b/lib/src/widgets/board_thumbnail.dart index 3f23c5b844..c13fa9e49f 100644 --- a/lib/src/widgets/board_thumbnail.dart +++ b/lib/src/widgets/board_thumbnail.dart @@ -76,11 +76,15 @@ class _BoardThumbnailState extends ConsumerState { fen: widget.fen, orientation: widget.orientation, lastMove: widget.lastMove as NormalMove?, - settings: boardPrefs.toBoardSettings().copyWith( + settings: ChessboardSettings( enableCoordinates: false, borderRadius: const BorderRadius.all(Radius.circular(4.0)), boxShadow: boardShadows, - animationDuration: widget.animationDuration, + animationDuration: widget.animationDuration!, + pieceAssets: boardPrefs.pieceSet.assets, + colorScheme: boardPrefs.boardTheme.colors, + hue: boardPrefs.hue, + brightness: boardPrefs.brightness, ), ) : StaticChessboard( @@ -93,8 +97,8 @@ class _BoardThumbnailState extends ConsumerState { boxShadow: boardShadows, pieceAssets: boardPrefs.pieceSet.assets, colorScheme: boardPrefs.boardTheme.colors, - brightness: boardPrefs.brightness, hue: boardPrefs.hue, + brightness: boardPrefs.brightness, ); final maybeTappableBoard = From 35e905e73aacc6dce24699f9f2cd6c30627ea5c7 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Tue, 17 Dec 2024 15:02:25 +0100 Subject: [PATCH 954/979] More work on custom theme --- lib/src/app.dart | 34 +- lib/src/constants.dart | 2 + lib/src/init.dart | 44 +- .../model/settings/general_preferences.dart | 23 +- .../view/settings/settings_tab_screen.dart | 2 - lib/src/view/settings/theme_screen.dart | 653 ++++++++++-------- 6 files changed, 437 insertions(+), 321 deletions(-) diff --git a/lib/src/app.dart b/lib/src/app.dart index 40cf12ec4c..a7740b476f 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -17,7 +17,6 @@ import 'package:lichess_mobile/src/navigation.dart'; import 'package:lichess_mobile/src/network/connectivity.dart'; import 'package:lichess_mobile/src/network/http.dart'; import 'package:lichess_mobile/src/network/socket.dart'; -import 'package:lichess_mobile/src/styles/lichess_colors.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/screen.dart'; @@ -140,27 +139,22 @@ class _AppState extends ConsumerState { final dynamicColorScheme = brightness == Brightness.light ? fixedLightScheme : fixedDarkScheme; - ColorScheme colorScheme; - if (generalPrefs.customThemeEnabled) { - if (generalPrefs.customThemeSeed != null) { - colorScheme = ColorScheme.fromSeed( - seedColor: generalPrefs.customThemeSeed!, - brightness: brightness, - ); - } else if (dynamicColorScheme != null) { - colorScheme = dynamicColorScheme; - } else { - colorScheme = ColorScheme.fromSeed( - seedColor: LichessColors.primary[500]!, - brightness: brightness, - ); - } - } else { - colorScheme = ColorScheme.fromSeed( + final ColorScheme colorScheme = switch (generalPrefs.appThemeSeed) { + AppThemeSeed.color => ColorScheme.fromSeed( + seedColor: generalPrefs.customThemeSeed ?? kDefaultSeedColor, + brightness: brightness, + ), + AppThemeSeed.board => ColorScheme.fromSeed( seedColor: boardTheme.colors.darkSquare, brightness: brightness, - ); - } + ), + AppThemeSeed.system => + dynamicColorScheme ?? + ColorScheme.fromSeed( + seedColor: boardTheme.colors.darkSquare, + brightness: brightness, + ), + }; final cupertinoThemeData = CupertinoThemeData( primaryColor: colorScheme.primary, diff --git a/lib/src/constants.dart b/lib/src/constants.dart index dc0d78eeea..079c0343fe 100644 --- a/lib/src/constants.dart +++ b/lib/src/constants.dart @@ -37,6 +37,8 @@ const kClueLessDeviation = 230; // UI +const kDefaultSeedColor = Color(0xFFD64F00); + const kGoldenRatio = 1.61803398875; /// Flex golden ratio base (flex has to be an int). diff --git a/lib/src/init.dart b/lib/src/init.dart index d4ac98c44f..3c47e67ed6 100644 --- a/lib/src/init.dart +++ b/lib/src/init.dart @@ -32,6 +32,11 @@ Future setupFirstLaunch() async { final appVersion = Version.parse(pInfo.version); final installedVersion = prefs.getString('installed_version'); + // TODO remove this migration code after a few releases + if (installedVersion != null && Version.parse(installedVersion) <= Version(0, 13, 9)) { + _migrateThemeSettings(); + } + if (installedVersion == null || Version.parse(installedVersion) != appVersion) { prefs.setString('installed_version', appVersion.canonicalizedVersion); } @@ -49,6 +54,26 @@ Future setupFirstLaunch() async { } } +Future _migrateThemeSettings() async { + final prefs = LichessBinding.instance.sharedPreferences; + try { + final stored = LichessBinding.instance.sharedPreferences.getString( + PrefCategory.general.storageKey, + ); + if (stored == null) { + return; + } + final generalPrefs = GeneralPrefs.fromJson(jsonDecode(stored) as Map); + final migrated = generalPrefs.copyWith( + // ignore: deprecated_member_use_from_same_package + appThemeSeed: generalPrefs.systemColors == true ? AppThemeSeed.system : AppThemeSeed.board, + ); + await prefs.setString(PrefCategory.general.storageKey, jsonEncode(migrated.toJson())); + } catch (e) { + _logger.warning('Failed to migrate theme settings: $e'); + } +} + Future initializeLocalNotifications(Locale locale) async { final l10n = await AppLocalizations.delegate.load(locale); await FlutterLocalNotificationsPlugin().initialize( @@ -86,27 +111,10 @@ Future preloadPieceImages() async { /// /// This is meant to be called once during app initialization. Future androidDisplayInitialization(WidgetsBinding widgetsBinding) async { - final prefs = LichessBinding.instance.sharedPreferences; - - // On android 12+ get core palette and set the board theme to system if it is not set + // On android 12+ set core palette and make system board try { await DynamicColorPlugin.getCorePalette().then((value) { setCorePalette(value); - - if (getCorePalette() != null) { - if (prefs.getString(PrefCategory.general.storageKey) == null) { - prefs.setString( - PrefCategory.general.storageKey, - jsonEncode(GeneralPrefs.defaults.copyWith(customThemeEnabled: true)), - ); - } - if (prefs.getString(PrefCategory.board.storageKey) == null) { - prefs.setString( - PrefCategory.board.storageKey, - jsonEncode(BoardPrefs.defaults.copyWith(boardTheme: BoardTheme.system)), - ); - } - } }); } catch (e) { _logger.fine('Device does not support core palette: $e'); diff --git a/lib/src/model/settings/general_preferences.dart b/lib/src/model/settings/general_preferences.dart index f788e60dd5..aa9571ee6c 100644 --- a/lib/src/model/settings/general_preferences.dart +++ b/lib/src/model/settings/general_preferences.dart @@ -1,7 +1,6 @@ import 'dart:ui' show Color, Locale; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; import 'package:lichess_mobile/src/utils/json.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -54,6 +53,10 @@ class GeneralPreferences extends _$GeneralPreferences with PreferencesStorage setCustomThemeSeed(Color? color) { return save(state.copyWith(customThemeSeed: color)); } + + Future setAppThemeSeed(AppThemeSeed seed) { + return save(state.copyWith(appThemeSeed: seed)); + } } @Freezed(fromJson: true, toJson: true) @@ -71,6 +74,12 @@ class GeneralPrefs with _$GeneralPrefs implements Serializable { /// Custom theme seed color @ColorConverter() Color? customThemeSeed, + @Deprecated('Use appThemeSeed instead') bool? systemColors, + + /// App theme seed + @JsonKey(unknownEnumValue: AppThemeSeed.color, defaultValue: AppThemeSeed.color) + required AppThemeSeed appThemeSeed, + /// Locale to use in the app, use system locale if null @LocaleConverter() Locale? locale, }) = _GeneralPrefs; @@ -81,6 +90,7 @@ class GeneralPrefs with _$GeneralPrefs implements Serializable { soundTheme: SoundTheme.standard, masterVolume: 0.8, customThemeEnabled: false, + appThemeSeed: AppThemeSeed.color, ); factory GeneralPrefs.fromJson(Map json) { @@ -88,6 +98,17 @@ class GeneralPrefs with _$GeneralPrefs implements Serializable { } } +enum AppThemeSeed { + /// The app theme is based on the user's system theme (only available on Android 10+). + system, + + /// The app theme is based on the chessboard. + board, + + /// The app theme is based on a specific color. + color, +} + /// Describes the background theme of the app. enum BackgroundThemeMode { /// Use either the light or dark theme based on what the user has selected in diff --git a/lib/src/view/settings/settings_tab_screen.dart b/lib/src/view/settings/settings_tab_screen.dart index 687952a257..6396d7287d 100644 --- a/lib/src/view/settings/settings_tab_screen.dart +++ b/lib/src/view/settings/settings_tab_screen.dart @@ -8,7 +8,6 @@ import 'package:lichess_mobile/src/model/account/account_repository.dart'; import 'package:lichess_mobile/src/model/auth/auth_controller.dart'; import 'package:lichess_mobile/src/model/auth/auth_session.dart'; import 'package:lichess_mobile/src/model/common/preloaded_data.dart'; -import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; import 'package:lichess_mobile/src/navigation.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; @@ -82,7 +81,6 @@ class _Body extends ConsumerWidget { }); final generalPrefs = ref.watch(generalPreferencesProvider); - final boardPrefs = ref.watch(boardPreferencesProvider); final authController = ref.watch(authControllerProvider); final userSession = ref.watch(authSessionProvider); final packageInfo = ref.read(preloadedDataProvider).requireValue.packageInfo; diff --git a/lib/src/view/settings/theme_screen.dart b/lib/src/view/settings/theme_screen.dart index 10d58c8748..c4f3a516b5 100644 --- a/lib/src/view/settings/theme_screen.dart +++ b/lib/src/view/settings/theme_screen.dart @@ -1,34 +1,51 @@ -import 'dart:math' as math; +import 'dart:ui' show ImageFilter; import 'package:chessground/chessground.dart'; import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_colorpicker/flutter_colorpicker.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/model/settings/general_preferences.dart'; -import 'package:lichess_mobile/src/styles/lichess_colors.dart'; import 'package:lichess_mobile/src/styles/lichess_icons.dart'; +import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/color_palette.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; import 'package:lichess_mobile/src/view/settings/board_theme_screen.dart'; import 'package:lichess_mobile/src/view/settings/piece_set_screen.dart'; +import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; import 'package:lichess_mobile/src/widgets/adaptive_choice_picker.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/change_colors.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; +import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/platform_alert_dialog.dart'; -import 'package:lichess_mobile/src/widgets/platform_scaffold.dart'; import 'package:lichess_mobile/src/widgets/settings.dart'; +const _kBoardSize = 200.0; + class ThemeScreen extends StatelessWidget { const ThemeScreen({super.key}); @override Widget build(BuildContext context) { - return PlatformScaffold(appBar: const PlatformAppBar(title: Text('Theme')), body: _Body()); + return PlatformWidget( + androidBuilder: (context) => const Scaffold(body: _Body()), + iosBuilder: + (context) => CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + automaticBackgroundVisibility: false, + backgroundColor: Styles.cupertinoAppBarColor + .resolveFrom(context) + .withValues(alpha: 0.0), + border: null, + ), + child: const _Body(), + ), + ); } } @@ -42,6 +59,8 @@ switch (shapeColor) { }; class _Body extends ConsumerStatefulWidget { + const _Body(); + @override ConsumerState<_Body> createState() => _BodyState(); } @@ -50,6 +69,10 @@ class _BodyState extends ConsumerState<_Body> { late double brightness; late double hue; + double headerOpacity = 0; + + bool openAdjustColorSection = false; + @override void initState() { super.initState(); @@ -58,220 +81,296 @@ class _BodyState extends ConsumerState<_Body> { hue = boardPrefs.hue; } + bool handleScrollNotification(ScrollNotification notification) { + if (notification is ScrollUpdateNotification && notification.depth == 0) { + final ScrollMetrics metrics = notification.metrics; + double scrollExtent = 0.0; + switch (metrics.axisDirection) { + case AxisDirection.up: + scrollExtent = metrics.extentAfter; + case AxisDirection.down: + scrollExtent = metrics.extentBefore; + case AxisDirection.right: + case AxisDirection.left: + break; + } + + final opacity = scrollExtent > 0.0 ? 1.0 : 0.0; + + if (opacity != headerOpacity) { + setState(() { + headerOpacity = opacity; + }); + } + } + return false; + } + + void _showColorPicker() { + final generalPrefs = ref.read(generalPreferencesProvider); + showAdaptiveDialog( + context: context, + barrierDismissible: false, + builder: (context) { + bool useDefault = generalPrefs.customThemeSeed == null; + Color color = generalPrefs.customThemeSeed ?? kDefaultSeedColor; + return StatefulBuilder( + builder: (context, setState) { + return PlatformAlertDialog( + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ColorPicker( + enableAlpha: false, + pickerColor: color, + onColorChanged: (c) { + setState(() { + useDefault = false; + color = c; + }); + }, + ), + SecondaryButton( + semanticsLabel: 'Default color', + onPressed: + !useDefault + ? () { + setState(() { + useDefault = true; + color = kDefaultSeedColor; + }); + } + : null, + child: const Text('Default color'), + ), + SecondaryButton( + semanticsLabel: context.l10n.cancel, + onPressed: () { + Navigator.of(context).pop(false); + }, + child: Text(context.l10n.cancel), + ), + SecondaryButton( + semanticsLabel: context.l10n.ok, + onPressed: () { + if (useDefault) { + Navigator.of(context).pop(null); + } else { + Navigator.of(context).pop(color); + } + }, + child: Text(context.l10n.ok), + ), + ], + ), + ), + ); + }, + ); + }, + ).then((color) { + if (color != false) { + ref.read(generalPreferencesProvider.notifier).setCustomThemeSeed(color as Color?); + } + }); + } + @override Widget build(BuildContext context) { final generalPrefs = ref.watch(generalPreferencesProvider); final boardPrefs = ref.watch(boardPreferencesProvider); - const horizontalPadding = 16.0; - final bool hasAjustedColors = brightness != 0.0 || hue != 0.0; - return ListView( - children: [ - LayoutBuilder( - builder: (context, constraints) { - final double boardSize = math.min( - 250, - constraints.biggest.shortestSide - horizontalPadding * 2, - ); - return Padding( - padding: const EdgeInsets.symmetric(horizontal: horizontalPadding, vertical: 16), - child: Center( - child: ChangeColors( - brightness: brightness, - hue: hue, - child: Chessboard.fixed( - size: boardSize, - orientation: Side.white, - lastMove: const NormalMove(from: Square.e2, to: Square.e4), - fen: 'rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1', - shapes: - { - Circle(color: boardPrefs.shapeColor.color, orig: Square.fromName('b8')), - Arrow( - color: boardPrefs.shapeColor.color, - orig: Square.fromName('b8'), - dest: Square.fromName('c6'), - ), - }.lock, - settings: boardPrefs.toBoardSettings().copyWith( - brightness: 0.0, - hue: 0.0, - borderRadius: const BorderRadius.all(Radius.circular(4.0)), - boxShadow: boardShadows, + final backgroundColor = Styles.cupertinoAppBarColor.resolveFrom(context); + + return NotificationListener( + onNotification: handleScrollNotification, + child: CustomScrollView( + slivers: [ + if (Theme.of(context).platform == TargetPlatform.iOS) + PinnedHeaderSliver( + child: ClipRect( + child: BackdropFilter( + enabled: backgroundColor.alpha != 0xFF, + filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0), + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + decoration: ShapeDecoration( + color: headerOpacity == 1.0 ? backgroundColor : backgroundColor.withAlpha(0), + shape: LinearBorder.bottom( + side: BorderSide( + color: + headerOpacity == 1.0 ? const Color(0x4D000000) : Colors.transparent, + width: 0.0, + ), + ), ), + padding: + Styles.bodyPadding + + EdgeInsets.only(top: MediaQuery.paddingOf(context).top), + child: _BoardPreview(boardPrefs: boardPrefs, brightness: brightness, hue: hue), ), ), ), - ); - }, - ), - ListSection( - hasLeading: true, - children: [ - PlatformListTile( - leading: const Icon(Icons.brightness_6), - title: Slider.adaptive( - min: -0.5, - max: 0.5, - value: brightness, - onChanged: (value) { - setState(() { - brightness = value; - }); - }, - onChangeEnd: (value) { - ref.read(boardPreferencesProvider.notifier).adjustColors(brightness: brightness); - }, - ), - ), - PlatformListTile( - leading: const Icon(Icons.invert_colors), - title: Slider.adaptive( - min: -1.0, - max: 1.0, - value: hue, - onChanged: (value) { - setState(() { - hue = value; - }); - }, - onChangeEnd: (value) { - ref.read(boardPreferencesProvider.notifier).adjustColors(hue: hue); - }, + ) + else + SliverAppBar( + pinned: true, + title: const Text('Theme'), + bottom: PreferredSize( + preferredSize: const Size.fromHeight(_kBoardSize + 16.0), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: _BoardPreview(boardPrefs: boardPrefs, brightness: brightness, hue: hue), + ), ), ), - PlatformListTile( - leading: Opacity( - opacity: hasAjustedColors ? 1.0 : 0.5, - child: const Icon(Icons.cancel), - ), - title: Opacity( - opacity: hasAjustedColors ? 1.0 : 0.5, - child: Text(context.l10n.boardReset), + SliverList.list( + children: [ + ListSection( + hasLeading: true, + children: [ + SettingsListTile( + icon: const Icon(LichessIcons.chess_board), + settingsLabel: Text(context.l10n.board), + settingsValue: boardPrefs.boardTheme.label, + onTap: () { + pushPlatformRoute( + context, + title: context.l10n.board, + builder: (context) => const BoardThemeScreen(), + ); + }, + ), + SettingsListTile( + icon: const Icon(LichessIcons.chess_pawn), + settingsLabel: Text(context.l10n.pieceSet), + settingsValue: boardPrefs.pieceSet.label, + onTap: () { + pushPlatformRoute( + context, + title: context.l10n.pieceSet, + builder: (context) => const PieceSetScreen(), + ); + }, + ), + SettingsListTile( + icon: const Icon(LichessIcons.arrow_full_upperright), + settingsLabel: const Text('Shape color'), + settingsValue: shapeColorL10n(context, boardPrefs.shapeColor), + onTap: () { + showChoicePicker( + context, + choices: ShapeColor.values, + selectedItem: boardPrefs.shapeColor, + labelBuilder: + (t) => Text.rich( + TextSpan( + children: [ + TextSpan(text: shapeColorL10n(context, t)), + const TextSpan(text: ' '), + WidgetSpan( + child: Container(width: 15, height: 15, color: t.color), + ), + ], + ), + ), + onSelectedItemChanged: (ShapeColor? value) { + ref + .read(boardPreferencesProvider.notifier) + .setShapeColor(value ?? ShapeColor.green); + }, + ); + }, + ), + SwitchSettingTile( + leading: const Icon(Icons.location_on), + title: Text(context.l10n.preferencesBoardCoordinates), + value: boardPrefs.coordinates, + onChanged: (value) { + ref.read(boardPreferencesProvider.notifier).toggleCoordinates(); + }, + ), + SwitchSettingTile( + // TODO translate + leading: const Icon(Icons.border_outer), + title: const Text('Show border'), + value: boardPrefs.showBorder, + onChanged: (value) { + ref.read(boardPreferencesProvider.notifier).toggleBorder(); + }, + ), + ], ), - onTap: - hasAjustedColors - ? () { + ListSection( + header: SettingsSectionTitle(context.l10n.advancedSettings), + hasLeading: true, + children: [ + PlatformListTile( + leading: const Icon(Icons.brightness_6), + title: Slider.adaptive( + min: -0.5, + max: 0.5, + value: brightness, + onChanged: (value) { setState(() { - brightness = 0.0; - hue = 0.0; + brightness = value; }); + }, + onChangeEnd: (value) { ref .read(boardPreferencesProvider.notifier) - .adjustColors(brightness: 0.0, hue: 0.0); - } - : null, - ), - ], - ), - ListSection( - hasLeading: true, - children: [ - SettingsListTile( - icon: const Icon(LichessIcons.chess_board), - settingsLabel: Text(context.l10n.board), - settingsValue: boardPrefs.boardTheme.label, - onTap: () { - pushPlatformRoute( - context, - title: context.l10n.board, - builder: (context) => const BoardThemeScreen(), - ); - }, - ), - SettingsListTile( - icon: const Icon(LichessIcons.chess_pawn), - settingsLabel: Text(context.l10n.pieceSet), - settingsValue: boardPrefs.pieceSet.label, - onTap: () { - pushPlatformRoute( - context, - title: context.l10n.pieceSet, - builder: (context) => const PieceSetScreen(), - ); - }, - ), - SettingsListTile( - icon: const Icon(LichessIcons.arrow_full_upperright), - settingsLabel: const Text('Shape color'), - settingsValue: shapeColorL10n(context, boardPrefs.shapeColor), - onTap: () { - showChoicePicker( - context, - choices: ShapeColor.values, - selectedItem: boardPrefs.shapeColor, - labelBuilder: - (t) => Text.rich( - TextSpan( - children: [ - TextSpan(text: shapeColorL10n(context, t)), - const TextSpan(text: ' '), - WidgetSpan(child: Container(width: 15, height: 15, color: t.color)), - ], - ), - ), - onSelectedItemChanged: (ShapeColor? value) { - ref - .read(boardPreferencesProvider.notifier) - .setShapeColor(value ?? ShapeColor.green); - }, - ); - }, - ), - SwitchSettingTile( - leading: const Icon(Icons.location_on), - title: Text(context.l10n.preferencesBoardCoordinates), - value: boardPrefs.coordinates, - onChanged: (value) { - ref.read(boardPreferencesProvider.notifier).toggleCoordinates(); - }, - ), - SwitchSettingTile( - // TODO translate - leading: const Icon(Icons.border_outer), - title: const Text('Show border'), - value: boardPrefs.showBorder, - onChanged: (value) { - ref.read(boardPreferencesProvider.notifier).toggleBorder(); - }, - ), - ], - ), - ListSection( - hasLeading: true, - children: [ - SwitchSettingTile( - leading: const Icon(Icons.colorize_outlined), - padding: - Theme.of(context).platform == TargetPlatform.iOS - ? const EdgeInsets.symmetric(horizontal: 14, vertical: 8) - : null, - title: const Text('App theme'), - // TODO translate - subtitle: const Text('Configure your own app theme using a seed color.', maxLines: 3), - value: generalPrefs.customThemeEnabled, - onChanged: (value) { - ref.read(generalPreferencesProvider.notifier).toggleCustomTheme(); - }, - ), - AnimatedCrossFade( - duration: const Duration(milliseconds: 300), - crossFadeState: - generalPrefs.customThemeEnabled - ? CrossFadeState.showSecond - : CrossFadeState.showFirst, - firstChild: const SizedBox.shrink(), - secondChild: ListSection( - margin: EdgeInsets.zero, - cupertinoBorderRadius: BorderRadius.zero, - cupertinoClipBehavior: Clip.none, - children: [ + .adjustColors(brightness: brightness); + }, + ), + ), PlatformListTile( - leading: const Icon(Icons.color_lens), - title: const Text('Seed color'), - trailing: + leading: const Icon(Icons.invert_colors), + title: Slider.adaptive( + min: -1.0, + max: 1.0, + value: hue, + onChanged: (value) { + setState(() { + hue = value; + }); + }, + onChangeEnd: (value) { + ref.read(boardPreferencesProvider.notifier).adjustColors(hue: hue); + }, + ), + ), + PlatformListTile( + leading: Opacity( + opacity: hasAjustedColors ? 1.0 : 0.5, + child: const Icon(Icons.cancel), + ), + title: Opacity( + opacity: hasAjustedColors ? 1.0 : 0.5, + child: Text(context.l10n.boardReset), + ), + onTap: + hasAjustedColors + ? () { + setState(() { + brightness = 0.0; + hue = 0.0; + }); + ref + .read(boardPreferencesProvider.notifier) + .adjustColors(brightness: 0.0, hue: 0.0); + } + : null, + ), + PlatformListTile( + leading: const Icon(Icons.colorize_outlined), + title: const Text('App theme'), + trailing: switch (generalPrefs.appThemeSeed) { + AppThemeSeed.board => Text(context.l10n.board), + AppThemeSeed.system => Text(context.l10n.mobileSystemColors), + AppThemeSeed.color => generalPrefs.customThemeSeed != null ? Container( width: 20, @@ -281,102 +380,96 @@ class _BodyState extends ConsumerState<_Body> { shape: BoxShape.circle, ), ) - : getCorePalette() != null - ? Text(context.l10n.mobileSystemColors) : Container( width: 20, height: 20, - decoration: BoxDecoration( - color: LichessColors.primary[500], + decoration: const BoxDecoration( + color: kDefaultSeedColor, shape: BoxShape.circle, ), ), + }, onTap: () { - showAdaptiveDialog( + showAdaptiveActionSheet( context: context, - barrierDismissible: false, - builder: (context) { - final defaultColor = - getCorePalettePrimary() ?? LichessColors.primary[500]!; - bool useDefault = generalPrefs.customThemeSeed == null; - Color color = generalPrefs.customThemeSeed ?? defaultColor; - return StatefulBuilder( - builder: (context, setState) { - return PlatformAlertDialog( - content: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ColorPicker( - enableAlpha: false, - pickerColor: color, - onColorChanged: (c) { - setState(() { - useDefault = false; - color = c; - }); - }, - ), - SecondaryButton( - semanticsLabel: - getCorePalette() != null - ? context.l10n.mobileSystemColors - : 'Default color', - onPressed: - !useDefault - ? () { - setState(() { - useDefault = true; - color = defaultColor; - }); - } - : null, - child: Text( - getCorePalette() != null - ? context.l10n.mobileSystemColors - : 'Default color', - ), - ), - SecondaryButton( - semanticsLabel: context.l10n.cancel, - onPressed: () { - Navigator.of(context).pop(false); + actions: + AppThemeSeed.values + .where((t) => t != AppThemeSeed.system || getCorePalette() != null) + .map( + (t) => BottomSheetAction( + makeLabel: + (context) => switch (t) { + AppThemeSeed.board => Text(context.l10n.board), + AppThemeSeed.system => Text( + context.l10n.mobileSystemColors, + ), + AppThemeSeed.color => const Text('Custom color'), }, - child: Text(context.l10n.cancel), - ), - SecondaryButton( - semanticsLabel: context.l10n.ok, - onPressed: () { - if (useDefault) { - Navigator.of(context).pop(null); - } else { - Navigator.of(context).pop(color); - } - }, - child: Text(context.l10n.ok), - ), - ], + onPressed: (context) { + ref + .read(generalPreferencesProvider.notifier) + .setAppThemeSeed(t); + + if (t == AppThemeSeed.color) { + _showColorPicker(); + } + }, + dismissOnPress: true, ), - ), - ); - }, - ); - }, - ).then((color) { - if (color != false) { - ref - .read(generalPreferencesProvider.notifier) - .setCustomThemeSeed(color as Color?); - } - }); + ) + .toList(), + ); }, ), ], ), - ), - ], + ], + ), + const SliverSafeArea( + top: false, + sliver: SliverToBoxAdapter(child: SizedBox(height: 16.0)), + ), + ], + ), + ); + } +} + +class _BoardPreview extends StatelessWidget { + const _BoardPreview({required this.boardPrefs, required this.brightness, required this.hue}); + + final BoardPrefs boardPrefs; + final double brightness; + final double hue; + + @override + Widget build(BuildContext context) { + return Center( + child: ChangeColors( + brightness: brightness, + hue: hue, + child: Chessboard.fixed( + size: _kBoardSize, + orientation: Side.white, + lastMove: const NormalMove(from: Square.e2, to: Square.e4), + fen: 'rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1', + shapes: + { + Circle(color: boardPrefs.shapeColor.color, orig: Square.fromName('b8')), + Arrow( + color: boardPrefs.shapeColor.color, + orig: Square.fromName('b8'), + dest: Square.fromName('c6'), + ), + }.lock, + settings: boardPrefs.toBoardSettings().copyWith( + brightness: 0.0, + hue: 0.0, + borderRadius: const BorderRadius.all(Radius.circular(4.0)), + boxShadow: boardShadows, + ), ), - ], + ), ); } } From 9b5e19c6ffc86c54434181cc1612d3600917d94a Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 18 Dec 2024 18:24:21 +0100 Subject: [PATCH 955/979] Tweak custom theme --- lib/main.dart | 17 +++++------------ lib/src/constants.dart | 2 +- lib/src/init.dart | 7 +++++-- lib/src/model/settings/general_preferences.dart | 4 ++-- lib/src/view/settings/theme_screen.dart | 5 ++++- 5 files changed, 17 insertions(+), 18 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index c04e4e8423..0ec26f1d80 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -26,6 +26,10 @@ Future main() async { await lichessBinding.preloadSharedPreferences(); + if (defaultTargetPlatform == TargetPlatform.android) { + await androidDisplayInitialization(widgetsBinding); + } + await preloadPieceImages(); await setupFirstLaunch(); @@ -38,18 +42,7 @@ Future main() async { await lichessBinding.initializeFirebase(); - if (defaultTargetPlatform == TargetPlatform.android) { - await androidDisplayInitialization(widgetsBinding); - } - - runApp( - ProviderScope( - observers: [ - ProviderLogger(), - ], - child: const AppInitializationScreen(), - ), - ); + runApp(ProviderScope(observers: [ProviderLogger()], child: const AppInitializationScreen())); } Future migrateSharedPreferences() async { diff --git a/lib/src/constants.dart b/lib/src/constants.dart index 079c0343fe..811cbe7051 100644 --- a/lib/src/constants.dart +++ b/lib/src/constants.dart @@ -37,7 +37,7 @@ const kClueLessDeviation = 230; // UI -const kDefaultSeedColor = Color(0xFFD64F00); +const kDefaultSeedColor = Color.fromARGB(255, 191, 128, 29); const kGoldenRatio = 1.61803398875; diff --git a/lib/src/init.dart b/lib/src/init.dart index 3c47e67ed6..f3132fe82c 100644 --- a/lib/src/init.dart +++ b/lib/src/init.dart @@ -65,8 +65,11 @@ Future _migrateThemeSettings() async { } final generalPrefs = GeneralPrefs.fromJson(jsonDecode(stored) as Map); final migrated = generalPrefs.copyWith( - // ignore: deprecated_member_use_from_same_package - appThemeSeed: generalPrefs.systemColors == true ? AppThemeSeed.system : AppThemeSeed.board, + appThemeSeed: + // ignore: deprecated_member_use_from_same_package + getCorePalette() != null && generalPrefs.systemColors == true + ? AppThemeSeed.system + : AppThemeSeed.board, ); await prefs.setString(PrefCategory.general.storageKey, jsonEncode(migrated.toJson())); } catch (e) { diff --git a/lib/src/model/settings/general_preferences.dart b/lib/src/model/settings/general_preferences.dart index aa9571ee6c..a612c4fa82 100644 --- a/lib/src/model/settings/general_preferences.dart +++ b/lib/src/model/settings/general_preferences.dart @@ -77,7 +77,7 @@ class GeneralPrefs with _$GeneralPrefs implements Serializable { @Deprecated('Use appThemeSeed instead') bool? systemColors, /// App theme seed - @JsonKey(unknownEnumValue: AppThemeSeed.color, defaultValue: AppThemeSeed.color) + @JsonKey(unknownEnumValue: AppThemeSeed.board, defaultValue: AppThemeSeed.board) required AppThemeSeed appThemeSeed, /// Locale to use in the app, use system locale if null @@ -90,7 +90,7 @@ class GeneralPrefs with _$GeneralPrefs implements Serializable { soundTheme: SoundTheme.standard, masterVolume: 0.8, customThemeEnabled: false, - appThemeSeed: AppThemeSeed.color, + appThemeSeed: AppThemeSeed.board, ); factory GeneralPrefs.fromJson(Map json) { diff --git a/lib/src/view/settings/theme_screen.dart b/lib/src/view/settings/theme_screen.dart index c4f3a516b5..3503eb1c58 100644 --- a/lib/src/view/settings/theme_screen.dart +++ b/lib/src/view/settings/theme_screen.dart @@ -121,8 +121,11 @@ class _BodyState extends ConsumerState<_Body> { child: Column( mainAxisSize: MainAxisSize.min, children: [ - ColorPicker( + HueRingPicker( enableAlpha: false, + colorPickerHeight: 200, + displayThumbColor: false, + portraitOnly: true, pickerColor: color, onColorChanged: (c) { setState(() { From cf7bab2c4b5afbacbcd5aa7003d449e48774d3ec Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 18 Dec 2024 18:55:40 +0100 Subject: [PATCH 956/979] Fix brightness adjust matrix --- lib/src/model/settings/board_preferences.dart | 2 +- lib/src/view/settings/theme_screen.dart | 4 ++-- lib/src/widgets/change_colors.dart | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/src/model/settings/board_preferences.dart b/lib/src/model/settings/board_preferences.dart index 402e659789..e6e310d265 100644 --- a/lib/src/model/settings/board_preferences.dart +++ b/lib/src/model/settings/board_preferences.dart @@ -105,7 +105,7 @@ class BoardPrefs with _$BoardPrefs implements Serializable { const BoardPrefs._(); @Assert( - 'brightness == null || brightness >= -0.5 && brightness <= 0.5, hue == null || hue >= -1 && hue <= 1', + 'brightness == null || brightness >= -1.0 && brightness <= 1.0, hue == null || hue >= -1 && hue <= 1', ) const factory BoardPrefs({ required PieceSet pieceSet, diff --git a/lib/src/view/settings/theme_screen.dart b/lib/src/view/settings/theme_screen.dart index 3503eb1c58..97fa11988e 100644 --- a/lib/src/view/settings/theme_screen.dart +++ b/lib/src/view/settings/theme_screen.dart @@ -314,8 +314,8 @@ class _BodyState extends ConsumerState<_Body> { PlatformListTile( leading: const Icon(Icons.brightness_6), title: Slider.adaptive( - min: -0.5, - max: 0.5, + min: -1.0, + max: 1.0, value: brightness, onChanged: (value) { setState(() { diff --git a/lib/src/widgets/change_colors.dart b/lib/src/widgets/change_colors.dart index 38ce7eebe3..97526f6541 100644 --- a/lib/src/widgets/change_colors.dart +++ b/lib/src/widgets/change_colors.dart @@ -55,7 +55,7 @@ List _adjustMatrix({required double hue, required double brightness}) { ]; // dart format on } - final brightnessValue = brightness <= 0 ? brightness * 255 : brightness * 100; + final brightnessValue = brightness <= 0 ? brightness * 100 : brightness * 100; return List.from([ 0.213 + cos(hue) * 0.787 + sin(hue) * -0.213, 0.715 + cos(hue) * -0.715 + sin(hue) * -0.715, From b5e323b3fb577335498c3d25e4fb03a57ba45f86 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Wed, 18 Dec 2024 18:58:32 +0100 Subject: [PATCH 957/979] Tweak --- lib/src/widgets/change_colors.dart | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/src/widgets/change_colors.dart b/lib/src/widgets/change_colors.dart index 97526f6541..54a41d5f13 100644 --- a/lib/src/widgets/change_colors.dart +++ b/lib/src/widgets/change_colors.dart @@ -17,10 +17,6 @@ import 'package:flutter/material.dart'; /// child: Image.asset('myImage.png'), /// ); /// ``` -/// -/// To achieve a greyscale effect, you may also use the -/// [ChangeColors.greyscale] constructor. -/// class ChangeColors extends StatelessWidget { const ChangeColors({this.brightness = 0.0, this.hue = 0.0, required this.child, super.key}); From 9d7117380a45676ea5e17edef1c75203ead262a3 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 19 Dec 2024 13:14:55 +0100 Subject: [PATCH 958/979] Update chessground --- pubspec.lock | 7 ++++--- pubspec.yaml | 3 +-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 45fb76ab74..ce3f85acbd 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -201,9 +201,10 @@ packages: chessground: dependency: "direct main" description: - path: "../flutter-chessground" - relative: true - source: path + name: chessground + sha256: "0913b4cb80ea514db5d2aa61006d1f8ca77bc353b8da30bc0c243b6d4276a113" + url: "https://pub.dev" + source: hosted version: "6.2.0" ci: dependency: transitive diff --git a/pubspec.yaml b/pubspec.yaml index 8600a41706..390dc33e60 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,8 +14,7 @@ dependencies: async: ^2.10.0 auto_size_text: ^3.0.0 cached_network_image: ^3.2.2 - chessground: - path: ../flutter-chessground + chessground: ^6.2.0 clock: ^1.1.1 collection: ^1.17.0 connectivity_plus: ^6.0.2 From bf64ac70dcce28d2f3cf7f0dde3fc31f617e0d7c Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 19 Dec 2024 13:15:05 +0100 Subject: [PATCH 959/979] Improve theme screen for tablets --- lib/src/view/settings/theme_screen.dart | 31 +++++++++++++++++++------ 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/lib/src/view/settings/theme_screen.dart b/lib/src/view/settings/theme_screen.dart index 97fa11988e..1b4c590d20 100644 --- a/lib/src/view/settings/theme_screen.dart +++ b/lib/src/view/settings/theme_screen.dart @@ -14,6 +14,7 @@ import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/utils/color_palette.dart'; import 'package:lichess_mobile/src/utils/l10n_context.dart'; import 'package:lichess_mobile/src/utils/navigation.dart'; +import 'package:lichess_mobile/src/utils/screen.dart'; import 'package:lichess_mobile/src/view/settings/board_theme_screen.dart'; import 'package:lichess_mobile/src/view/settings/piece_set_screen.dart'; import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; @@ -25,8 +26,6 @@ import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/platform_alert_dialog.dart'; import 'package:lichess_mobile/src/widgets/settings.dart'; -const _kBoardSize = 200.0; - class ThemeScreen extends StatelessWidget { const ThemeScreen({super.key}); @@ -186,6 +185,8 @@ class _BodyState extends ConsumerState<_Body> { final bool hasAjustedColors = brightness != 0.0 || hue != 0.0; + final boardSize = isTabletOrLarger(context) ? 350.0 : 200.0; + final backgroundColor = Styles.cupertinoAppBarColor.resolveFrom(context); return NotificationListener( @@ -213,7 +214,12 @@ class _BodyState extends ConsumerState<_Body> { padding: Styles.bodyPadding + EdgeInsets.only(top: MediaQuery.paddingOf(context).top), - child: _BoardPreview(boardPrefs: boardPrefs, brightness: brightness, hue: hue), + child: _BoardPreview( + size: boardSize, + boardPrefs: boardPrefs, + brightness: brightness, + hue: hue, + ), ), ), ), @@ -223,10 +229,15 @@ class _BodyState extends ConsumerState<_Body> { pinned: true, title: const Text('Theme'), bottom: PreferredSize( - preferredSize: const Size.fromHeight(_kBoardSize + 16.0), + preferredSize: Size.fromHeight(boardSize + 16.0), child: Padding( padding: const EdgeInsets.symmetric(vertical: 16.0), - child: _BoardPreview(boardPrefs: boardPrefs, brightness: brightness, hue: hue), + child: _BoardPreview( + size: boardSize, + boardPrefs: boardPrefs, + brightness: brightness, + hue: hue, + ), ), ), ), @@ -439,11 +450,17 @@ class _BodyState extends ConsumerState<_Body> { } class _BoardPreview extends StatelessWidget { - const _BoardPreview({required this.boardPrefs, required this.brightness, required this.hue}); + const _BoardPreview({ + required this.size, + required this.boardPrefs, + required this.brightness, + required this.hue, + }); final BoardPrefs boardPrefs; final double brightness; final double hue; + final double size; @override Widget build(BuildContext context) { @@ -452,7 +469,7 @@ class _BoardPreview extends StatelessWidget { brightness: brightness, hue: hue, child: Chessboard.fixed( - size: _kBoardSize, + size: size, orientation: Side.white, lastMove: const NormalMove(from: Square.e2, to: Square.e4), fen: 'rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1', From 6e2226a2cd27293fc38d93ac27f33000e0e38019 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 19 Dec 2024 16:11:50 +0100 Subject: [PATCH 960/979] Upgrade chessground, improve color filter --- lib/src/model/settings/board_preferences.dart | 18 +++-- lib/src/view/settings/theme_screen.dart | 19 ++--- lib/src/widgets/board_carousel_item.dart | 2 +- lib/src/widgets/change_colors.dart | 77 ------------------- pubspec.lock | 4 +- pubspec.yaml | 2 +- 6 files changed, 24 insertions(+), 98 deletions(-) delete mode 100644 lib/src/widgets/change_colors.dart diff --git a/lib/src/model/settings/board_preferences.dart b/lib/src/model/settings/board_preferences.dart index e6e310d265..3b8335e8fd 100644 --- a/lib/src/model/settings/board_preferences.dart +++ b/lib/src/model/settings/board_preferences.dart @@ -11,6 +11,9 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'board_preferences.freezed.dart'; part 'board_preferences.g.dart'; +const kBoardDefaultBrightnessFilter = 1.0; +const kBoardDefaultHueFilter = 0.0; + @riverpod class BoardPreferences extends _$BoardPreferences with PreferencesStorage { // ignore: avoid_public_notifier_properties @@ -104,9 +107,7 @@ class BoardPreferences extends _$BoardPreferences with PreferencesStorage= -1.0 && brightness <= 1.0, hue == null || hue >= -1 && hue <= 1', - ) + @Assert('brightness >= 0.2 && brightness <= 1.4, hue >= 0.0 && hue <= 360.0') const factory BoardPrefs({ required PieceSet pieceSet, required BoardTheme boardTheme, @@ -133,8 +134,8 @@ class BoardPrefs with _$BoardPrefs implements Serializable { @JsonKey(defaultValue: ShapeColor.green, unknownEnumValue: ShapeColor.green) required ShapeColor shapeColor, @JsonKey(defaultValue: false) required bool showBorder, - @JsonKey(defaultValue: 0.0) required double brightness, - @JsonKey(defaultValue: 0.0) required double hue, + @JsonKey(defaultValue: kBoardDefaultBrightnessFilter) required double brightness, + @JsonKey(defaultValue: kBoardDefaultBrightnessFilter) required double hue, }) = _BoardPrefs; static const defaults = BoardPrefs( @@ -154,11 +155,12 @@ class BoardPrefs with _$BoardPrefs implements Serializable { dragTargetKind: DragTargetKind.circle, shapeColor: ShapeColor.green, showBorder: false, - brightness: 0.0, - hue: 0.0, + brightness: kBoardDefaultBrightnessFilter, + hue: kBoardDefaultHueFilter, ); - bool get hasColorAdjustments => brightness != 0.0 || hue != 0.0; + bool get hasColorAdjustments => + brightness != kBoardDefaultBrightnessFilter || hue != kBoardDefaultHueFilter; ChessboardSettings toBoardSettings() { return ChessboardSettings( diff --git a/lib/src/view/settings/theme_screen.dart b/lib/src/view/settings/theme_screen.dart index 1b4c590d20..c147d15d54 100644 --- a/lib/src/view/settings/theme_screen.dart +++ b/lib/src/view/settings/theme_screen.dart @@ -183,7 +183,8 @@ class _BodyState extends ConsumerState<_Body> { final generalPrefs = ref.watch(generalPreferencesProvider); final boardPrefs = ref.watch(boardPreferencesProvider); - final bool hasAjustedColors = brightness != 0.0 || hue != 0.0; + final bool hasAjustedColors = + brightness != kBoardDefaultBrightnessFilter || hue != kBoardDefaultHueFilter; final boardSize = isTabletOrLarger(context) ? 350.0 : 200.0; @@ -325,8 +326,8 @@ class _BodyState extends ConsumerState<_Body> { PlatformListTile( leading: const Icon(Icons.brightness_6), title: Slider.adaptive( - min: -1.0, - max: 1.0, + min: 0.2, + max: 1.4, value: brightness, onChanged: (value) { setState(() { @@ -343,8 +344,8 @@ class _BodyState extends ConsumerState<_Body> { PlatformListTile( leading: const Icon(Icons.invert_colors), title: Slider.adaptive( - min: -1.0, - max: 1.0, + min: 0.0, + max: 360.0, value: hue, onChanged: (value) { setState(() { @@ -369,12 +370,12 @@ class _BodyState extends ConsumerState<_Body> { hasAjustedColors ? () { setState(() { - brightness = 0.0; - hue = 0.0; + brightness = kBoardDefaultBrightnessFilter; + hue = kBoardDefaultHueFilter; }); ref .read(boardPreferencesProvider.notifier) - .adjustColors(brightness: 0.0, hue: 0.0); + .adjustColors(brightness: brightness, hue: hue); } : null, ), @@ -465,7 +466,7 @@ class _BoardPreview extends StatelessWidget { @override Widget build(BuildContext context) { return Center( - child: ChangeColors( + child: BrightnessHueFilter( brightness: brightness, hue: hue, child: Chessboard.fixed( diff --git a/lib/src/widgets/board_carousel_item.dart b/lib/src/widgets/board_carousel_item.dart index 2559c8398f..acefe85ac3 100644 --- a/lib/src/widgets/board_carousel_item.dart +++ b/lib/src/widgets/board_carousel_item.dart @@ -52,7 +52,7 @@ class BoardCarouselItem extends ConsumerWidget { return LayoutBuilder( builder: (context, constraints) { final boardSize = constraints.biggest.shortestSide - _kBoardCarouselItemMargin.horizontal; - final card = ChangeColors( + final card = BrightnessHueFilter( hue: boardPrefs.hue, brightness: boardPrefs.brightness, child: PlatformCard( diff --git a/lib/src/widgets/change_colors.dart b/lib/src/widgets/change_colors.dart deleted file mode 100644 index 54a41d5f13..0000000000 --- a/lib/src/widgets/change_colors.dart +++ /dev/null @@ -1,77 +0,0 @@ -import 'dart:math'; - -import 'package:flutter/material.dart'; - -// Based upon: https://stackoverflow.com/questions/64639589/how-to-adjust-hue-saturation-and-brightness-of-an-image-in-flutter -// from BananaNeil: https://stackoverflow.com/users/937841/banananeil. -// This is, in turn, based upon: https://stackoverflow.com/a/7917978/937841 - -/// Use the [ChangeColors] widget to change the brightness and hue of any widget, including images. -/// -/// Example: -/// -/// ``` -/// ChangeColors( -/// hue: 0.55, -/// brightness: 0.2, -/// child: Image.asset('myImage.png'), -/// ); -/// ``` -class ChangeColors extends StatelessWidget { - const ChangeColors({this.brightness = 0.0, this.hue = 0.0, required this.child, super.key}); - - /// Negative value will make it darker (-1 is darkest). - /// Positive value will make it lighter (1 is the maximum, but you can go above it). - /// Note: 0.0 is unchanged. - final double brightness; - - /// From -1.0 to 1.0 (Note: 1.0 wraps into -1.0, such as 1.2 is the same as -0.8). - /// Note: 0.0 is unchanged. Adding or subtracting multiples of 2.0 also keeps it unchanged. - final double hue; - - final Widget child; - - @override - Widget build(BuildContext context) { - return ColorFiltered( - colorFilter: ColorFilter.matrix(_adjustMatrix(hue: hue * pi, brightness: brightness)), - child: child, - ); - } -} - -List _adjustMatrix({required double hue, required double brightness}) { - if (hue == 0 && brightness == 0) { - // dart format off - return [ - 1, 0, 0, 0, 0, - 0, 1, 0, 0, 0, - 0, 0, 1, 0, 0, - 0, 0, 0, 1, 0, - ]; - // dart format on - } - final brightnessValue = brightness <= 0 ? brightness * 100 : brightness * 100; - return List.from([ - 0.213 + cos(hue) * 0.787 + sin(hue) * -0.213, - 0.715 + cos(hue) * -0.715 + sin(hue) * -0.715, - 0.072 + cos(hue) * -0.072 + sin(hue) * 0.928, - 0, - brightnessValue, - 0.213 + cos(hue) * -0.213 + sin(hue) * 0.143, - 0.715 + cos(hue) * 0.285 + sin(hue) * 0.140, - 0.072 + cos(hue) * -0.072 + sin(hue) * -0.283, - 0, - brightnessValue, - 0.213 + cos(hue) * -0.213 + sin(hue) * -0.787, - 0.715 + cos(hue) * -0.715 + sin(hue) * 0.715, - 0.072 + cos(hue) * 0.928 + sin(hue) * 0.072, - 0, - brightnessValue, - 0, - 0, - 0, - 1, - 0, - ]).map((i) => i).toList(); -} diff --git a/pubspec.lock b/pubspec.lock index ce3f85acbd..decbb5c858 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -202,10 +202,10 @@ packages: dependency: "direct main" description: name: chessground - sha256: "0913b4cb80ea514db5d2aa61006d1f8ca77bc353b8da30bc0c243b6d4276a113" + sha256: "6ae599b48e8802e75d3b27e71816cb04c74667f54266d5d22f75f7fb11503c47" url: "https://pub.dev" source: hosted - version: "6.2.0" + version: "6.2.2" ci: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 390dc33e60..9306efaf4d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,7 +14,7 @@ dependencies: async: ^2.10.0 auto_size_text: ^3.0.0 cached_network_image: ^3.2.2 - chessground: ^6.2.0 + chessground: ^6.2.2 clock: ^1.1.1 collection: ^1.17.0 connectivity_plus: ^6.0.2 From 756635cc6fc57f7f726e6cd74c41fc135a31fa9b Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 19 Dec 2024 16:14:18 +0100 Subject: [PATCH 961/979] Remove old import --- lib/src/view/settings/theme_screen.dart | 1 - lib/src/widgets/board_carousel_item.dart | 1 - 2 files changed, 2 deletions(-) diff --git a/lib/src/view/settings/theme_screen.dart b/lib/src/view/settings/theme_screen.dart index c147d15d54..4d5e3fb293 100644 --- a/lib/src/view/settings/theme_screen.dart +++ b/lib/src/view/settings/theme_screen.dart @@ -20,7 +20,6 @@ import 'package:lichess_mobile/src/view/settings/piece_set_screen.dart'; import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; import 'package:lichess_mobile/src/widgets/adaptive_choice_picker.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; -import 'package:lichess_mobile/src/widgets/change_colors.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; import 'package:lichess_mobile/src/widgets/platform_alert_dialog.dart'; diff --git a/lib/src/widgets/board_carousel_item.dart b/lib/src/widgets/board_carousel_item.dart index acefe85ac3..a8b1a0ba3b 100644 --- a/lib/src/widgets/board_carousel_item.dart +++ b/lib/src/widgets/board_carousel_item.dart @@ -7,7 +7,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; import 'package:lichess_mobile/src/styles/styles.dart'; import 'package:lichess_mobile/src/widgets/buttons.dart'; -import 'package:lichess_mobile/src/widgets/change_colors.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; const _kBoardCarouselItemMargin = EdgeInsets.symmetric(vertical: 8.0, horizontal: 6.0); From 64d4ee2393fd4168547afdece36bb72b28d6782a Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 19 Dec 2024 16:20:21 +0100 Subject: [PATCH 962/979] Restore puzzle tab icon color --- lib/src/view/puzzle/puzzle_tab_screen.dart | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/src/view/puzzle/puzzle_tab_screen.dart b/lib/src/view/puzzle/puzzle_tab_screen.dart index ed6b65764c..a870b218b9 100644 --- a/lib/src/view/puzzle/puzzle_tab_screen.dart +++ b/lib/src/view/puzzle/puzzle_tab_screen.dart @@ -369,7 +369,14 @@ class _PuzzleMenuListTile extends StatelessWidget { Theme.of(context).platform == TargetPlatform.iOS ? const EdgeInsets.symmetric(vertical: 10.0, horizontal: 14.0) : null, - leading: Icon(icon, size: Styles.mainListTileIconSize), + leading: Icon( + icon, + size: Styles.mainListTileIconSize, + color: + Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoTheme.of(context).primaryColor + : Theme.of(context).colorScheme.primary, + ), title: Text(title, style: Styles.mainListTileTitle), subtitle: Text(subtitle, maxLines: 3), trailing: From 1809ec43497a1742e04bf3714bf517da5f08d377 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 19 Dec 2024 16:22:17 +0100 Subject: [PATCH 963/979] Fix theme screen default values --- lib/src/view/settings/theme_screen.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/view/settings/theme_screen.dart b/lib/src/view/settings/theme_screen.dart index 4d5e3fb293..ac1741be72 100644 --- a/lib/src/view/settings/theme_screen.dart +++ b/lib/src/view/settings/theme_screen.dart @@ -483,8 +483,8 @@ class _BoardPreview extends StatelessWidget { ), }.lock, settings: boardPrefs.toBoardSettings().copyWith( - brightness: 0.0, - hue: 0.0, + brightness: kBoardDefaultBrightnessFilter, + hue: kBoardDefaultHueFilter, borderRadius: const BorderRadius.all(Radius.circular(4.0)), boxShadow: boardShadows, ), From 74d557509d06a8bda93910cca85086c0a4fc923b Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 19 Dec 2024 16:26:41 +0100 Subject: [PATCH 964/979] Remove unused function --- lib/src/utils/color_palette.dart | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/src/utils/color_palette.dart b/lib/src/utils/color_palette.dart index 2419eb73b5..6ecdd18d82 100644 --- a/lib/src/utils/color_palette.dart +++ b/lib/src/utils/color_palette.dart @@ -45,10 +45,6 @@ void setCorePalette(CorePalette? palette) { } } -Color? getCorePalettePrimary() { - return _corePalette?.primary != null ? Color(_corePalette!.primary.get(50)) : null; -} - /// Get the core palette if available (android 12+ only). CorePalette? getCorePalette() { return _corePalette; From 341755c2d682a37d69f7c1649820acfad92736b2 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 19 Dec 2024 16:33:31 +0100 Subject: [PATCH 965/979] No need to migrate if core palette is null --- lib/src/init.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/src/init.dart b/lib/src/init.dart index f3132fe82c..655a4a5d31 100644 --- a/lib/src/init.dart +++ b/lib/src/init.dart @@ -55,6 +55,9 @@ Future setupFirstLaunch() async { } Future _migrateThemeSettings() async { + if (getCorePalette() == null) { + return; + } final prefs = LichessBinding.instance.sharedPreferences; try { final stored = LichessBinding.instance.sharedPreferences.getString( @@ -67,9 +70,7 @@ Future _migrateThemeSettings() async { final migrated = generalPrefs.copyWith( appThemeSeed: // ignore: deprecated_member_use_from_same_package - getCorePalette() != null && generalPrefs.systemColors == true - ? AppThemeSeed.system - : AppThemeSeed.board, + generalPrefs.systemColors == true ? AppThemeSeed.system : AppThemeSeed.board, ); await prefs.setString(PrefCategory.general.storageKey, jsonEncode(migrated.toJson())); } catch (e) { From 66216a7f4c6025de744026e518a883965e796aac Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 19 Dec 2024 16:41:53 +0100 Subject: [PATCH 966/979] Bump version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 9306efaf4d..40aa505221 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: lichess_mobile description: Lichess mobile app V2 publish_to: "none" -version: 0.13.10+001310 # See README.md for details about versioning +version: 0.13.11+001311 # See README.md for details about versioning environment: sdk: '^3.7.0-209.1.beta' From aeac25f73c59916532effbb06574a9e6f8787b3d Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Thu, 19 Dec 2024 17:45:06 +0100 Subject: [PATCH 967/979] Use the right onError Flutter method for tests --- test/test_provider_scope.dart | 39 ++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/test/test_provider_scope.dart b/test/test_provider_scope.dart index 05b4b91743..c025467349 100644 --- a/test/test_provider_scope.dart +++ b/test/test_provider_scope.dart @@ -142,8 +142,27 @@ Future makeTestProviderScope( if (userSession != null) kSessionStorageKey: jsonEncode(userSession.toJson()), }); + final flutterTestOnError = FlutterError.onError!; + + void ignoreOverflowErrors(FlutterErrorDetails details) { + bool isOverflowError = false; + final exception = details.exception; + + if (exception is FlutterError) { + isOverflowError = exception.diagnostics.any( + (e) => e.value.toString().contains('A RenderFlex overflowed by'), + ); + } + + if (isOverflowError) { + // debugPrint('Overflow error detected.'); + } else { + flutterTestOnError(details); + } + } + // TODO consider loading true fonts as well - FlutterError.onError = _ignoreOverflowErrors; + FlutterError.onError = ignoreOverflowErrors; return ProviderScope( key: key, @@ -210,21 +229,3 @@ Future makeTestProviderScope( child: TestSurface(size: surfaceSize, child: child), ); } - -void _ignoreOverflowErrors(FlutterErrorDetails details, {bool forceReport = false}) { - bool isOverflowError = false; - final exception = details.exception; - - if (exception is FlutterError) { - isOverflowError = exception.diagnostics.any( - (e) => e.value.toString().contains('A RenderFlex overflowed by'), - ); - } - - if (isOverflowError) { - // debugPrint('Overflow error detected.'); - } else { - FlutterError.dumpErrorToConsole(details, forceReport: forceReport); - throw exception; - } -} From 52dcf3ea9c5a75c22b12aa4ae9113e43e91e5a86 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 19 Dec 2024 18:37:50 +0100 Subject: [PATCH 968/979] Fix default value of board prefs --- lib/src/model/settings/board_preferences.dart | 2 +- lib/src/model/settings/general_preferences.dart | 8 -------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/lib/src/model/settings/board_preferences.dart b/lib/src/model/settings/board_preferences.dart index 3b8335e8fd..7cc12cafd0 100644 --- a/lib/src/model/settings/board_preferences.dart +++ b/lib/src/model/settings/board_preferences.dart @@ -135,7 +135,7 @@ class BoardPrefs with _$BoardPrefs implements Serializable { required ShapeColor shapeColor, @JsonKey(defaultValue: false) required bool showBorder, @JsonKey(defaultValue: kBoardDefaultBrightnessFilter) required double brightness, - @JsonKey(defaultValue: kBoardDefaultBrightnessFilter) required double hue, + @JsonKey(defaultValue: kBoardDefaultHueFilter) required double hue, }) = _BoardPrefs; static const defaults = BoardPrefs( diff --git a/lib/src/model/settings/general_preferences.dart b/lib/src/model/settings/general_preferences.dart index a612c4fa82..44c6350a00 100644 --- a/lib/src/model/settings/general_preferences.dart +++ b/lib/src/model/settings/general_preferences.dart @@ -46,10 +46,6 @@ class GeneralPreferences extends _$GeneralPreferences with PreferencesStorage toggleCustomTheme() async { - await save(state.copyWith(customThemeEnabled: !state.customThemeEnabled)); - } - Future setCustomThemeSeed(Color? color) { return save(state.copyWith(customThemeSeed: color)); } @@ -68,9 +64,6 @@ class GeneralPrefs with _$GeneralPrefs implements Serializable { @JsonKey(unknownEnumValue: SoundTheme.standard) required SoundTheme soundTheme, @JsonKey(defaultValue: 0.8) required double masterVolume, - /// Should enable custom theme - @JsonKey(defaultValue: false) required bool customThemeEnabled, - /// Custom theme seed color @ColorConverter() Color? customThemeSeed, @@ -89,7 +82,6 @@ class GeneralPrefs with _$GeneralPrefs implements Serializable { isSoundEnabled: true, soundTheme: SoundTheme.standard, masterVolume: 0.8, - customThemeEnabled: false, appThemeSeed: AppThemeSeed.board, ); From 8253c05c2868658c910c97112bdb1b8f72b1eb14 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Thu, 19 Dec 2024 23:18:14 +0100 Subject: [PATCH 969/979] Watch tab style fixes --- .../view/watch/live_tv_channels_screen.dart | 11 ++++- lib/src/view/watch/streamer_screen.dart | 49 +++++++++---------- lib/src/view/watch/watch_tab_screen.dart | 14 +++++- 3 files changed, 45 insertions(+), 29 deletions(-) diff --git a/lib/src/view/watch/live_tv_channels_screen.dart b/lib/src/view/watch/live_tv_channels_screen.dart index fdb96f412f..7afd2ddc54 100644 --- a/lib/src/view/watch/live_tv_channels_screen.dart +++ b/lib/src/view/watch/live_tv_channels_screen.dart @@ -1,3 +1,4 @@ +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/constants.dart'; @@ -68,7 +69,15 @@ class _Body extends ConsumerWidget { mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Text(game.channel.label, style: Styles.boardPreviewTitle), - Icon(game.channel.icon, color: context.lichessColors.brag, size: 30), + Icon( + game.channel.icon, + + color: + Theme.of(context).platform == TargetPlatform.iOS + ? CupertinoTheme.of(context).primaryColor + : Theme.of(context).colorScheme.primary, + size: 30, + ), UserFullNameWidget.player( user: game.player.asPlayer.user, aiLevel: game.player.asPlayer.aiLevel, diff --git a/lib/src/view/watch/streamer_screen.dart b/lib/src/view/watch/streamer_screen.dart index 4ab5b4ebbf..ac6d76c0f0 100644 --- a/lib/src/view/watch/streamer_screen.dart +++ b/lib/src/view/watch/streamer_screen.dart @@ -22,15 +22,14 @@ class StreamerScreen extends StatelessWidget { Widget _buildAndroid(BuildContext context) { return Scaffold( appBar: AppBar(title: Text(context.l10n.mobileLiveStreamers)), - body: ListView( - children: [ - ListSection( - showDividerBetweenTiles: true, - children: streamers - .map((e) => StreamerListTile(streamer: e, showSubtitle: true, maxSubtitleLines: 4)) - .toList(growable: false), - ), - ], + body: ListView.builder( + itemCount: streamers.length, + itemBuilder: + (context, index) => StreamerListTile( + streamer: streamers[index], + showSubtitle: true, + maxSubtitleLines: 4, + ), ), ); } @@ -41,22 +40,16 @@ class StreamerScreen extends StatelessWidget { child: CustomScrollView( slivers: [ SliverSafeArea( - sliver: SliverList( - delegate: SliverChildListDelegate([ - ListSection( - hasLeading: true, - children: - streamers - .map( - (e) => StreamerListTile( - streamer: e, - showSubtitle: true, - maxSubtitleLines: 4, - ), - ) - .toList(), - ), - ]), + sliver: SliverList.separated( + separatorBuilder: + (context, index) => const PlatformDivider(height: 1, cupertinoHasLeading: true), + itemCount: streamers.length, + itemBuilder: + (context, index) => StreamerListTile( + streamer: streamers[index], + showSubtitle: true, + maxSubtitleLines: 4, + ), ), ), ], @@ -79,6 +72,10 @@ class StreamerListTile extends StatelessWidget { @override Widget build(BuildContext context) { return PlatformListTile( + padding: + Theme.of(context).platform == TargetPlatform.iOS + ? const EdgeInsets.symmetric(horizontal: 14.0, vertical: 12.0) + : null, onTap: () async { final url = streamer.platform == 'twitch' ? streamer.twitch : streamer.youTube; if (!await launchUrl(Uri.parse(url!), mode: LaunchMode.externalApplication)) { @@ -90,7 +87,7 @@ class StreamerListTile extends StatelessWidget { Theme.of(context).platform == TargetPlatform.android ? const EdgeInsets.all(5.0) : EdgeInsets.zero, - child: Image.network(streamer.image), + child: Image.network(streamer.image, width: 50, height: 50, fit: BoxFit.cover), ), title: Padding( padding: const EdgeInsets.only(right: 5.0), diff --git a/lib/src/view/watch/watch_tab_screen.dart b/lib/src/view/watch/watch_tab_screen.dart index 10d6988069..1e6f6a39f1 100644 --- a/lib/src/view/watch/watch_tab_screen.dart +++ b/lib/src/view/watch/watch_tab_screen.dart @@ -219,6 +219,7 @@ class _BroadcastWidget extends ConsumerWidget { return broadcastList.when( data: (data) { return ListSection( + hasLeading: true, header: Text(context.l10n.broadcastBroadcasts), headerTrailing: NoPaddingTextButton( onPressed: () { @@ -268,7 +269,16 @@ class _BroadcastTile extends ConsumerWidget { builder: (context) => BroadcastRoundScreen(broadcast: broadcast), ); }, - leading: const Icon(LichessIcons.radio_tower_lichess), + leading: + broadcast.tour.imageUrl != null + ? Image.network( + broadcast.tour.imageUrl!, + width: 50.0, + height: 50.0, + fit: BoxFit.cover, + errorBuilder: (context, _, __) => const Icon(LichessIcons.radio_tower_lichess), + ) + : const Image(image: kDefaultBroadcastImage), subtitle: Row( children: [ Text(broadcast.round.name), @@ -369,7 +379,7 @@ class _StreamerWidget extends ConsumerWidget { return const SizedBox.shrink(); } return ListSection( - header: Text(context.l10n.streamerLichessStreamers), + header: Text(context.l10n.streamersMenu), hasLeading: true, headerTrailing: NoPaddingTextButton( onPressed: From 76a33db3e0ceb7bb8d0db6eff442969f11bc544f Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 20 Dec 2024 09:52:27 +0100 Subject: [PATCH 970/979] Remove custom color scheme for now --- lib/src/app.dart | 4 - .../model/settings/general_preferences.dart | 12 +- .../view/settings/board_settings_screen.dart | 41 ++--- .../view/settings/settings_tab_screen.dart | 4 +- lib/src/view/settings/theme_screen.dart | 170 ++++-------------- 5 files changed, 58 insertions(+), 173 deletions(-) diff --git a/lib/src/app.dart b/lib/src/app.dart index a7740b476f..663db27fb4 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -140,10 +140,6 @@ class _AppState extends ConsumerState { brightness == Brightness.light ? fixedLightScheme : fixedDarkScheme; final ColorScheme colorScheme = switch (generalPrefs.appThemeSeed) { - AppThemeSeed.color => ColorScheme.fromSeed( - seedColor: generalPrefs.customThemeSeed ?? kDefaultSeedColor, - brightness: brightness, - ), AppThemeSeed.board => ColorScheme.fromSeed( seedColor: boardTheme.colors.darkSquare, brightness: brightness, diff --git a/lib/src/model/settings/general_preferences.dart b/lib/src/model/settings/general_preferences.dart index 44c6350a00..7fa2643f2d 100644 --- a/lib/src/model/settings/general_preferences.dart +++ b/lib/src/model/settings/general_preferences.dart @@ -1,4 +1,4 @@ -import 'dart:ui' show Color, Locale; +import 'dart:ui' show Locale; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:lichess_mobile/src/model/settings/preferences_storage.dart'; @@ -46,10 +46,6 @@ class GeneralPreferences extends _$GeneralPreferences with PreferencesStorage setCustomThemeSeed(Color? color) { - return save(state.copyWith(customThemeSeed: color)); - } - Future setAppThemeSeed(AppThemeSeed seed) { return save(state.copyWith(appThemeSeed: seed)); } @@ -64,9 +60,6 @@ class GeneralPrefs with _$GeneralPrefs implements Serializable { @JsonKey(unknownEnumValue: SoundTheme.standard) required SoundTheme soundTheme, @JsonKey(defaultValue: 0.8) required double masterVolume, - /// Custom theme seed color - @ColorConverter() Color? customThemeSeed, - @Deprecated('Use appThemeSeed instead') bool? systemColors, /// App theme seed @@ -96,9 +89,6 @@ enum AppThemeSeed { /// The app theme is based on the chessboard. board, - - /// The app theme is based on a specific color. - color, } /// Describes the background theme of the app. diff --git a/lib/src/view/settings/board_settings_screen.dart b/lib/src/view/settings/board_settings_screen.dart index 61d3be6fda..ee48eee3b0 100644 --- a/lib/src/view/settings/board_settings_screen.dart +++ b/lib/src/view/settings/board_settings_screen.dart @@ -23,7 +23,10 @@ class BoardSettingsScreen extends StatelessWidget { } Widget _androidBuilder(BuildContext context) { - return Scaffold(appBar: AppBar(title: Text(context.l10n.board)), body: const _Body()); + return Scaffold( + appBar: AppBar(title: Text(context.l10n.preferencesGameBehavior)), + body: const _Body(), + ); } Widget _iosBuilder(BuildContext context) { @@ -43,7 +46,6 @@ class _Body extends ConsumerWidget { return ListView( children: [ ListSection( - header: SettingsSectionTitle(context.l10n.preferencesGameBehavior), hasLeading: false, showDivider: false, children: [ @@ -130,27 +132,6 @@ class _Body extends ConsumerWidget { ref.read(boardPreferencesProvider.notifier).togglePieceAnimation(); }, ), - SwitchSettingTile( - // TODO: Add l10n - title: const Text('Shape drawing'), - subtitle: const Text( - // TODO: translate - 'Draw shapes using two fingers: maintain one finger on an empty square and drag another finger to draw a shape.', - maxLines: 5, - textAlign: TextAlign.justify, - ), - value: boardPrefs.enableShapeDrawings, - onChanged: (value) { - ref.read(boardPreferencesProvider.notifier).toggleEnableShapeDrawings(); - }, - ), - ], - ), - ListSection( - header: SettingsSectionTitle(context.l10n.preferencesDisplay), - hasLeading: false, - showDivider: false, - children: [ if (Theme.of(context).platform == TargetPlatform.android && !isTabletOrLarger(context)) androidVersionAsync.maybeWhen( data: @@ -238,6 +219,20 @@ class _Body extends ConsumerWidget { } }, ), + SwitchSettingTile( + // TODO: Add l10n + title: const Text('Shape drawing'), + subtitle: const Text( + // TODO: translate + 'Draw shapes using two fingers: maintain one finger on an empty square and drag another finger to draw a shape.', + maxLines: 5, + textAlign: TextAlign.justify, + ), + value: boardPrefs.enableShapeDrawings, + onChanged: (value) { + ref.read(boardPreferencesProvider.notifier).toggleEnableShapeDrawings(); + }, + ), ], ), ], diff --git a/lib/src/view/settings/settings_tab_screen.dart b/lib/src/view/settings/settings_tab_screen.dart index 6396d7287d..7fefc444b3 100644 --- a/lib/src/view/settings/settings_tab_screen.dart +++ b/lib/src/view/settings/settings_tab_screen.dart @@ -233,7 +233,7 @@ class _Body extends ConsumerWidget { ), PlatformListTile( leading: const Icon(LichessIcons.chess_board), - title: Text(context.l10n.board), + title: Text(context.l10n.preferencesGameBehavior, overflow: TextOverflow.ellipsis), trailing: Theme.of(context).platform == TargetPlatform.iOS ? const CupertinoListTileChevron() @@ -241,7 +241,7 @@ class _Body extends ConsumerWidget { onTap: () { pushPlatformRoute( context, - title: context.l10n.board, + title: context.l10n.preferencesGameBehavior, builder: (context) => const BoardSettingsScreen(), ); }, diff --git a/lib/src/view/settings/theme_screen.dart b/lib/src/view/settings/theme_screen.dart index ac1741be72..8fe386ed46 100644 --- a/lib/src/view/settings/theme_screen.dart +++ b/lib/src/view/settings/theme_screen.dart @@ -4,7 +4,6 @@ import 'package:dartchess/dartchess.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_colorpicker/flutter_colorpicker.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lichess_mobile/src/constants.dart'; import 'package:lichess_mobile/src/model/settings/board_preferences.dart'; @@ -19,10 +18,8 @@ import 'package:lichess_mobile/src/view/settings/board_theme_screen.dart'; import 'package:lichess_mobile/src/view/settings/piece_set_screen.dart'; import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart'; import 'package:lichess_mobile/src/widgets/adaptive_choice_picker.dart'; -import 'package:lichess_mobile/src/widgets/buttons.dart'; import 'package:lichess_mobile/src/widgets/list.dart'; import 'package:lichess_mobile/src/widgets/platform.dart'; -import 'package:lichess_mobile/src/widgets/platform_alert_dialog.dart'; import 'package:lichess_mobile/src/widgets/settings.dart'; class ThemeScreen extends StatelessWidget { @@ -104,79 +101,6 @@ class _BodyState extends ConsumerState<_Body> { return false; } - void _showColorPicker() { - final generalPrefs = ref.read(generalPreferencesProvider); - showAdaptiveDialog( - context: context, - barrierDismissible: false, - builder: (context) { - bool useDefault = generalPrefs.customThemeSeed == null; - Color color = generalPrefs.customThemeSeed ?? kDefaultSeedColor; - return StatefulBuilder( - builder: (context, setState) { - return PlatformAlertDialog( - content: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - HueRingPicker( - enableAlpha: false, - colorPickerHeight: 200, - displayThumbColor: false, - portraitOnly: true, - pickerColor: color, - onColorChanged: (c) { - setState(() { - useDefault = false; - color = c; - }); - }, - ), - SecondaryButton( - semanticsLabel: 'Default color', - onPressed: - !useDefault - ? () { - setState(() { - useDefault = true; - color = kDefaultSeedColor; - }); - } - : null, - child: const Text('Default color'), - ), - SecondaryButton( - semanticsLabel: context.l10n.cancel, - onPressed: () { - Navigator.of(context).pop(false); - }, - child: Text(context.l10n.cancel), - ), - SecondaryButton( - semanticsLabel: context.l10n.ok, - onPressed: () { - if (useDefault) { - Navigator.of(context).pop(null); - } else { - Navigator.of(context).pop(color); - } - }, - child: Text(context.l10n.ok), - ), - ], - ), - ), - ); - }, - ); - }, - ).then((color) { - if (color != false) { - ref.read(generalPreferencesProvider.notifier).setCustomThemeSeed(color as Color?); - } - }); - } - @override Widget build(BuildContext context) { final generalPrefs = ref.watch(generalPreferencesProvider); @@ -246,6 +170,43 @@ class _BodyState extends ConsumerState<_Body> { ListSection( hasLeading: true, children: [ + if (getCorePalette() != null) + SettingsListTile( + icon: const Icon(Icons.colorize_outlined), + settingsLabel: const Text('Color scheme'), + settingsValue: switch (generalPrefs.appThemeSeed) { + AppThemeSeed.board => context.l10n.board, + AppThemeSeed.system => context.l10n.mobileSystemColors, + }, + onTap: () { + showAdaptiveActionSheet( + context: context, + actions: + AppThemeSeed.values + .where( + (t) => t != AppThemeSeed.system || getCorePalette() != null, + ) + .map( + (t) => BottomSheetAction( + makeLabel: + (context) => switch (t) { + AppThemeSeed.board => Text(context.l10n.board), + AppThemeSeed.system => Text( + context.l10n.mobileSystemColors, + ), + }, + onPressed: (context) { + ref + .read(generalPreferencesProvider.notifier) + .setAppThemeSeed(t); + }, + dismissOnPress: true, + ), + ) + .toList(), + ); + }, + ), SettingsListTile( icon: const Icon(LichessIcons.chess_board), settingsLabel: Text(context.l10n.board), @@ -378,63 +339,6 @@ class _BodyState extends ConsumerState<_Body> { } : null, ), - PlatformListTile( - leading: const Icon(Icons.colorize_outlined), - title: const Text('App theme'), - trailing: switch (generalPrefs.appThemeSeed) { - AppThemeSeed.board => Text(context.l10n.board), - AppThemeSeed.system => Text(context.l10n.mobileSystemColors), - AppThemeSeed.color => - generalPrefs.customThemeSeed != null - ? Container( - width: 20, - height: 20, - decoration: BoxDecoration( - color: generalPrefs.customThemeSeed, - shape: BoxShape.circle, - ), - ) - : Container( - width: 20, - height: 20, - decoration: const BoxDecoration( - color: kDefaultSeedColor, - shape: BoxShape.circle, - ), - ), - }, - onTap: () { - showAdaptiveActionSheet( - context: context, - actions: - AppThemeSeed.values - .where((t) => t != AppThemeSeed.system || getCorePalette() != null) - .map( - (t) => BottomSheetAction( - makeLabel: - (context) => switch (t) { - AppThemeSeed.board => Text(context.l10n.board), - AppThemeSeed.system => Text( - context.l10n.mobileSystemColors, - ), - AppThemeSeed.color => const Text('Custom color'), - }, - onPressed: (context) { - ref - .read(generalPreferencesProvider.notifier) - .setAppThemeSeed(t); - - if (t == AppThemeSeed.color) { - _showColorPicker(); - } - }, - dismissOnPress: true, - ), - ) - .toList(), - ); - }, - ), ], ), ], From 13e00cb0a8e1fb433a4c61f1de4eef8bc0e5dffa Mon Sep 17 00:00:00 2001 From: maxmitz Date: Fri, 20 Dec 2024 11:04:57 +0100 Subject: [PATCH 971/979] Save streak result after loading a new puzzle --- lib/src/model/puzzle/puzzle_controller.dart | 9 +++++---- lib/src/view/puzzle/puzzle_screen.dart | 4 ++-- lib/src/view/puzzle/puzzle_session_widget.dart | 2 +- lib/src/view/puzzle/streak_screen.dart | 7 ++----- 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/lib/src/model/puzzle/puzzle_controller.dart b/lib/src/model/puzzle/puzzle_controller.dart index a28fd01021..bfdfad35ec 100644 --- a/lib/src/model/puzzle/puzzle_controller.dart +++ b/lib/src/model/puzzle/puzzle_controller.dart @@ -232,13 +232,14 @@ class PuzzleController extends _$PuzzleController { return nextPuzzle; } - void loadPuzzle(PuzzleContext nextContext, {PuzzleStreak? nextStreak}) { + void onLoadPuzzle(PuzzleContext nextContext, {PuzzleStreak? nextStreak}) { ref.read(evaluationServiceProvider).disposeEngine(); state = _loadNewContext(nextContext, nextStreak ?? state.streak); + _saveCurrentStreakLocally(); } - void saveStreakResultLocally() { + void _saveCurrentStreakLocally() { ref.read(streakStorageProvider(initialContext.userId)).saveActiveStreak(state.streak!); } @@ -341,7 +342,7 @@ class PuzzleController extends _$PuzzleController { if (next != null && result == PuzzleResult.win && ref.read(puzzlePreferencesProvider).autoNext) { - loadPuzzle(next); + onLoadPuzzle(next); } } else { // one fail and streak is over @@ -366,7 +367,7 @@ class PuzzleController extends _$PuzzleController { if (nextContext != null) { await Future.delayed(const Duration(milliseconds: 250)); soundService.play(Sound.confirmation); - loadPuzzle( + onLoadPuzzle( nextContext, nextStreak: state.streak!.copyWith(index: state.streak!.index + 1), ); diff --git a/lib/src/view/puzzle/puzzle_screen.dart b/lib/src/view/puzzle/puzzle_screen.dart index 25df10f270..f53a633433 100644 --- a/lib/src/view/puzzle/puzzle_screen.dart +++ b/lib/src/view/puzzle/puzzle_screen.dart @@ -411,7 +411,7 @@ class _BottomBar extends ConsumerWidget { BottomBarButton( onTap: puzzleState.mode == PuzzleMode.view && puzzleState.nextContext != null - ? () => ref.read(ctrlProvider.notifier).loadPuzzle(puzzleState.nextContext!) + ? () => ref.read(ctrlProvider.notifier).onLoadPuzzle(puzzleState.nextContext!) : null, highlighted: true, label: context.l10n.puzzleContinueTraining, @@ -533,7 +533,7 @@ class _DifficultySelector extends ConsumerWidget { .read(ctrlProvider.notifier) .changeDifficulty(selectedDifficulty); if (context.mounted && nextContext != null) { - ref.read(ctrlProvider.notifier).loadPuzzle(nextContext); + ref.read(ctrlProvider.notifier).onLoadPuzzle(nextContext); } }); }, diff --git a/lib/src/view/puzzle/puzzle_session_widget.dart b/lib/src/view/puzzle/puzzle_session_widget.dart index 841c622b05..de00ea2404 100644 --- a/lib/src/view/puzzle/puzzle_session_widget.dart +++ b/lib/src/view/puzzle/puzzle_session_widget.dart @@ -107,7 +107,7 @@ class PuzzleSessionWidgetState extends ConsumerState { puzzle: puzzle, ); - ref.read(widget.ctrlProvider.notifier).loadPuzzle(nextContext); + ref.read(widget.ctrlProvider.notifier).onLoadPuzzle(nextContext); } finally { if (mounted) { setState(() { diff --git a/lib/src/view/puzzle/streak_screen.dart b/lib/src/view/puzzle/streak_screen.dart index f41847b616..3618d0ef64 100644 --- a/lib/src/view/puzzle/streak_screen.dart +++ b/lib/src/view/puzzle/streak_screen.dart @@ -192,10 +192,7 @@ class _Body extends ConsumerWidget { (context) => YesNoDialog( title: Text(context.l10n.mobileAreYouSure), content: const Text('No worries, your score will be saved locally.'), - onYes: () { - ref.read(ctrlProvider.notifier).saveStreakResultLocally(); - return Navigator.of(context).pop(true); - }, + onYes: () => Navigator.of(context).pop(true), onNo: () => Navigator.of(context).pop(false), ), ); @@ -345,7 +342,7 @@ class _RetryFetchPuzzleDialog extends ConsumerWidget { if (data != null) { ref .read(ctrlProvider.notifier) - .loadPuzzle( + .onLoadPuzzle( data, nextStreak: state.streak!.copyWith(index: state.streak!.index + 1), ); From a8f16ac9befc00f981279187ca6caed3cf77d58b Mon Sep 17 00:00:00 2001 From: maxmitz Date: Fri, 20 Dec 2024 11:15:05 +0100 Subject: [PATCH 972/979] Update puzzle_controller.dart Fix tests --- lib/src/model/puzzle/puzzle_controller.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/src/model/puzzle/puzzle_controller.dart b/lib/src/model/puzzle/puzzle_controller.dart index bfdfad35ec..d41d6ec734 100644 --- a/lib/src/model/puzzle/puzzle_controller.dart +++ b/lib/src/model/puzzle/puzzle_controller.dart @@ -240,7 +240,9 @@ class PuzzleController extends _$PuzzleController { } void _saveCurrentStreakLocally() { + if(state.streak != null) { ref.read(streakStorageProvider(initialContext.userId)).saveActiveStreak(state.streak!); + } } void _sendStreakResult() { From b81d94244e6fbf3ef2e3777c41050ad88a1de959 Mon Sep 17 00:00:00 2001 From: maxmitz Date: Fri, 20 Dec 2024 11:20:09 +0100 Subject: [PATCH 973/979] Fixed formatting --- lib/src/model/puzzle/puzzle_controller.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/model/puzzle/puzzle_controller.dart b/lib/src/model/puzzle/puzzle_controller.dart index d41d6ec734..48c0f19a5c 100644 --- a/lib/src/model/puzzle/puzzle_controller.dart +++ b/lib/src/model/puzzle/puzzle_controller.dart @@ -240,8 +240,8 @@ class PuzzleController extends _$PuzzleController { } void _saveCurrentStreakLocally() { - if(state.streak != null) { - ref.read(streakStorageProvider(initialContext.userId)).saveActiveStreak(state.streak!); + if (state.streak != null) { + ref.read(streakStorageProvider(initialContext.userId)).saveActiveStreak(state.streak!); } } From bd36c5fe9e35879c82f10f1032a326ea6240dab3 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 20 Dec 2024 11:48:07 +0100 Subject: [PATCH 974/979] Decode thumbnails at their display size --- lib/src/view/watch/streamer_screen.dart | 11 ++++++++++- lib/src/view/watch/watch_tab_screen.dart | 4 ++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/lib/src/view/watch/streamer_screen.dart b/lib/src/view/watch/streamer_screen.dart index ac6d76c0f0..49902b7115 100644 --- a/lib/src/view/watch/streamer_screen.dart +++ b/lib/src/view/watch/streamer_screen.dart @@ -71,6 +71,8 @@ class StreamerListTile extends StatelessWidget { @override Widget build(BuildContext context) { + final devicePixelRatio = MediaQuery.devicePixelRatioOf(context); + return PlatformListTile( padding: Theme.of(context).platform == TargetPlatform.iOS @@ -87,7 +89,14 @@ class StreamerListTile extends StatelessWidget { Theme.of(context).platform == TargetPlatform.android ? const EdgeInsets.all(5.0) : EdgeInsets.zero, - child: Image.network(streamer.image, width: 50, height: 50, fit: BoxFit.cover), + child: Image.network( + streamer.image, + width: 50, + height: 50, + cacheWidth: (50.0 * devicePixelRatio).toInt(), + cacheHeight: (50.0 * devicePixelRatio).toInt(), + fit: BoxFit.cover, + ), ), title: Padding( padding: const EdgeInsets.only(right: 5.0), diff --git a/lib/src/view/watch/watch_tab_screen.dart b/lib/src/view/watch/watch_tab_screen.dart index 1e6f6a39f1..73cbd0f712 100644 --- a/lib/src/view/watch/watch_tab_screen.dart +++ b/lib/src/view/watch/watch_tab_screen.dart @@ -260,6 +260,8 @@ class _BroadcastTile extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final devicePixelRatio = MediaQuery.devicePixelRatioOf(context); + return PlatformListTile( onTap: () { pushPlatformRoute( @@ -275,6 +277,8 @@ class _BroadcastTile extends ConsumerWidget { broadcast.tour.imageUrl!, width: 50.0, height: 50.0, + cacheWidth: (50.0 * devicePixelRatio).toInt(), + cacheHeight: (50.0 * devicePixelRatio).toInt(), fit: BoxFit.cover, errorBuilder: (context, _, __) => const Icon(LichessIcons.radio_tower_lichess), ) From b14824462d01fdfdbc7ae69ca72ab047522b9666 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 20 Dec 2024 11:59:36 +0100 Subject: [PATCH 975/979] Remove unused plugin --- pubspec.lock | 8 -------- pubspec.yaml | 1 - 2 files changed, 9 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index decbb5c858..11b9f1ddfe 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -555,14 +555,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.4.1" - flutter_colorpicker: - dependency: "direct main" - description: - name: flutter_colorpicker - sha256: "969de5f6f9e2a570ac660fb7b501551451ea2a1ab9e2097e89475f60e07816ea" - url: "https://pub.dev" - source: hosted - version: "1.1.0" flutter_displaymode: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 40aa505221..e30ca3a8a1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,7 +34,6 @@ dependencies: flutter: sdk: flutter flutter_appauth: ^8.0.0+1 - flutter_colorpicker: ^1.1.0 flutter_displaymode: ^0.6.0 flutter_layout_grid: ^2.0.1 flutter_linkify: ^6.0.0 From 37158c2f69a176713c87206d6d7d284b8eac0415 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 20 Dec 2024 12:00:00 +0100 Subject: [PATCH 976/979] Bump version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index e30ca3a8a1..04d1014f19 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: lichess_mobile description: Lichess mobile app V2 publish_to: "none" -version: 0.13.11+001311 # See README.md for details about versioning +version: 0.13.12+001312 # See README.md for details about versioning environment: sdk: '^3.7.0-209.1.beta' From 4ab6bb03937e46ad9ad598b194d0763473e2f37c Mon Sep 17 00:00:00 2001 From: Julien <120588494+julien4215@users.noreply.github.com> Date: Fri, 20 Dec 2024 12:17:59 +0100 Subject: [PATCH 977/979] Rename broadcast test file --- ...asts_list_screen_test.dart => broadcast_list_screen_test.dart} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/view/broadcast/{broadcasts_list_screen_test.dart => broadcast_list_screen_test.dart} (100%) diff --git a/test/view/broadcast/broadcasts_list_screen_test.dart b/test/view/broadcast/broadcast_list_screen_test.dart similarity index 100% rename from test/view/broadcast/broadcasts_list_screen_test.dart rename to test/view/broadcast/broadcast_list_screen_test.dart From e786cfa737d50a83105560c7f791a8c25bb53544 Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Fri, 20 Dec 2024 21:53:14 +0100 Subject: [PATCH 978/979] Fix wrong color filter applied to board editor and coord trainer --- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 11b9f1ddfe..78045e15d0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -202,10 +202,10 @@ packages: dependency: "direct main" description: name: chessground - sha256: "6ae599b48e8802e75d3b27e71816cb04c74667f54266d5d22f75f7fb11503c47" + sha256: "5cf1a5bcd95c2c043ebfb1c88775b3d05b3332908ae4bbd528f32a8003129850" url: "https://pub.dev" source: hosted - version: "6.2.2" + version: "6.2.3" ci: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 04d1014f19..11fbb27c78 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,7 +14,7 @@ dependencies: async: ^2.10.0 auto_size_text: ^3.0.0 cached_network_image: ^3.2.2 - chessground: ^6.2.2 + chessground: ^6.2.3 clock: ^1.1.1 collection: ^1.17.0 connectivity_plus: ^6.0.2 From 2a8263efe53eb2bbeaa24974423b75f17513f9bc Mon Sep 17 00:00:00 2001 From: Vincent Velociter Date: Sat, 21 Dec 2024 11:35:50 +0100 Subject: [PATCH 979/979] Remove justify --- lib/src/view/settings/board_settings_screen.dart | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/src/view/settings/board_settings_screen.dart b/lib/src/view/settings/board_settings_screen.dart index ee48eee3b0..5df25681e1 100644 --- a/lib/src/view/settings/board_settings_screen.dart +++ b/lib/src/view/settings/board_settings_screen.dart @@ -119,7 +119,6 @@ class _Body extends ConsumerWidget { // TODO translate 'Vibrate when moving pieces or capturing them.', maxLines: 5, - textAlign: TextAlign.justify, ), onChanged: (value) { ref.read(boardPreferencesProvider.notifier).toggleHapticFeedback(); @@ -141,7 +140,6 @@ class _Body extends ConsumerWidget { title: Text(context.l10n.mobileSettingsImmersiveMode), subtitle: Text( context.l10n.mobileSettingsImmersiveModeSubtitle, - textAlign: TextAlign.justify, maxLines: 5, ), value: boardPrefs.immersiveModeWhilePlaying ?? false, @@ -226,7 +224,6 @@ class _Body extends ConsumerWidget { // TODO: translate 'Draw shapes using two fingers: maintain one finger on an empty square and drag another finger to draw a shape.', maxLines: 5, - textAlign: TextAlign.justify, ), value: boardPrefs.enableShapeDrawings, onChanged: (value) {